pk优化版
This commit is contained in:
8
.env.development
Normal file
8
.env.development
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 后端 api地址
|
||||||
|
VITE_API_BASE_URL=http://192.168.2.22:8101
|
||||||
|
# 注册地址
|
||||||
|
VITE_REGISTER_API_URL=http://192.168.2.22:48080
|
||||||
|
# pk api地址
|
||||||
|
VITE_PK_MINI_API_URL=http://192.168.2.22:8086
|
||||||
|
# 商店地址
|
||||||
|
VITE_SHOP_URL=http://192.168.2.128:8085
|
||||||
8
.env.production
Normal file
8
.env.production
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 后端 api地址
|
||||||
|
VITE_API_BASE_URL=https://crawlclient.api.yolozs.com
|
||||||
|
# 注册地址
|
||||||
|
VITE_REGISTER_API_URL=https://backstageapi.yolozs.com
|
||||||
|
# pk api地址
|
||||||
|
VITE_PK_MINI_API_URL=https://pk.hanxiaokj.cn
|
||||||
|
# 商店地址
|
||||||
|
VITE_SHOP_URL=https://待填写
|
||||||
@@ -2,5 +2,5 @@ import { getAxios } from '@/utils/axios.js'
|
|||||||
|
|
||||||
// 获取当前生效的公告列表
|
// 获取当前生效的公告列表
|
||||||
export function getActiveNotices() {
|
export function getActiveNotices() {
|
||||||
return getAxios({ url: '/api/notice/active' })
|
return getAxios({ url: '/api/common/notice' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,27 +4,26 @@
|
|||||||
*/
|
*/
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { ENV } from '@/config'
|
||||||
|
|
||||||
// 创建独立的 axios 实例
|
// 创建独立的 axios 实例
|
||||||
const pkAxios = axios.create({
|
const pkAxios = axios.create({
|
||||||
baseURL: 'http://192.168.2.22:8086/',
|
baseURL: ENV.PK_MINI_API_URL + '/',
|
||||||
// baseURL: 'https://pk.hanxiaokj.cn/',
|
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 请求拦截器 - 使用主项目的 vvtoken
|
// 请求拦截器 - PK Mini 后端使用 vvtoken 请求头
|
||||||
pkAxios.interceptors.request.use((config) => {
|
pkAxios.interceptors.request.use((config) => {
|
||||||
// 优先使用 vvtoken
|
const token = localStorage.getItem('token')
|
||||||
const vvtoken = localStorage.getItem('token')
|
if (token) {
|
||||||
if (vvtoken) {
|
config.headers['vvtoken'] = token
|
||||||
config.headers['vvtoken'] = vvtoken
|
|
||||||
} else {
|
} else {
|
||||||
// 兼容:尝试从 user_data 获取
|
// 兼容:尝试从 user_data 获取
|
||||||
const userData = JSON.parse(localStorage.getItem('user_data') || '{}')
|
const userData = JSON.parse(localStorage.getItem('user_data') || '{}')
|
||||||
if (userData.token) {
|
if (userData.tokenValue) {
|
||||||
config.headers['vvtoken'] = userData.tokenValue
|
config.headers['vvtoken'] = userData.tokenValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { ENV } from '@/config'
|
||||||
|
|
||||||
// 创建独立的 axios 实例,避免被全局拦截器影响
|
// 创建独立的 axios 实例,避免被全局拦截器影响
|
||||||
const registerAxios = axios.create({
|
const registerAxios = axios.create({
|
||||||
@@ -7,9 +8,7 @@ const registerAxios = axios.create({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 注册接口使用 tkNewAdmin 后端
|
// 注册接口使用 tkNewAdmin 后端
|
||||||
const REGISTER_BASE_URL = process.env.NODE_ENV === 'development'
|
const REGISTER_BASE_URL = ENV.REGISTER_API_URL
|
||||||
? 'http://192.168.2.22:48080'
|
|
||||||
: 'https://backstageapi.yolozs.com'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 租户注册
|
* 租户注册
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 252 KiB |
@@ -16,6 +16,10 @@
|
|||||||
<!-- 工具栏 -->
|
<!-- 工具栏 -->
|
||||||
<div class="p-4 border-b border-gray-100 space-y-3">
|
<div class="p-4 border-b border-gray-100 space-y-3">
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button @click="showAddDialog = true"
|
||||||
|
class="px-3 py-1.5 text-sm bg-green-100 text-green-700 hover:bg-green-200 rounded border border-green-300">
|
||||||
|
+ 添加主播
|
||||||
|
</button>
|
||||||
<button @click="selectAll"
|
<button @click="selectAll"
|
||||||
class="px-3 py-1.5 text-sm bg-blue-100 text-blue-700 hover:bg-blue-200 rounded border border-blue-300">全选</button>
|
class="px-3 py-1.5 text-sm bg-blue-100 text-blue-700 hover:bg-blue-200 rounded border border-blue-300">全选</button>
|
||||||
<button @click="selectNone"
|
<button @click="selectNone"
|
||||||
@@ -164,6 +168,84 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加主播弹窗 -->
|
||||||
|
<div v-if="showAddDialog" class="fixed inset-0 bg-black/50 flex items-center justify-center"
|
||||||
|
style="z-index: 10000">
|
||||||
|
<div class="bg-white rounded-xl shadow-2xl w-full max-w-lg mx-4">
|
||||||
|
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-600">添加主播</h3>
|
||||||
|
<button @click="closeAddDialog" class="text-gray-700 hover:text-gray-700 text-xl">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 space-y-4">
|
||||||
|
<!-- 主播ID输入 -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">主播ID(每行一个,支持批量粘贴)</label>
|
||||||
|
<textarea v-model="addForm.idsText" rows="6" placeholder="粘贴主播ID,每行一个 例如: anchor_001 anchor_002 anchor_003"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-blue-500 focus:outline-none resize-none font-mono"></textarea>
|
||||||
|
<div class="text-xs text-gray-400 mt-1">
|
||||||
|
已输入 {{ parsedIds.length }} 个ID
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 邀请类型 -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">邀请类型</label>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button @click="addForm.invitationType = '1'"
|
||||||
|
:class="['px-4 py-2 rounded-lg text-sm border transition-all', addForm.invitationType === '1' ? 'bg-blue-500 text-white border-blue-500' : 'bg-white text-gray-600 border-gray-300 hover:border-blue-300']">
|
||||||
|
普票
|
||||||
|
</button>
|
||||||
|
<button @click="addForm.invitationType = '2'"
|
||||||
|
:class="['px-4 py-2 rounded-lg text-sm border transition-all', addForm.invitationType === '2' ? 'bg-yellow-500 text-white border-yellow-500' : 'bg-white text-gray-600 border-gray-300 hover:border-yellow-300']">
|
||||||
|
金票
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 国家选择 -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">国家</label>
|
||||||
|
<select v-model="addForm.country"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-blue-500 focus:outline-none">
|
||||||
|
<option v-for="c in COUNTRY_OPTIONS" :key="c.value" :value="c.value">{{ c.label }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 等级选择 -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">主播等级</label>
|
||||||
|
<select v-model="addForm.hostsLevel"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-blue-500 focus:outline-none">
|
||||||
|
<optgroup v-for="parent in LEVEL_OPTIONS" :key="parent.value" :label="parent.label + '级'">
|
||||||
|
<option v-for="child in parent.children" :key="child.value" :value="child.value">
|
||||||
|
{{ child.label }}
|
||||||
|
</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部按钮 -->
|
||||||
|
<div class="p-4 border-t border-gray-200 flex justify-between items-center">
|
||||||
|
<span v-if="addStatus" :class="['text-sm', addStatus.type === 'success' ? 'text-green-600' : 'text-red-600']">
|
||||||
|
{{ addStatus.message }}
|
||||||
|
</span>
|
||||||
|
<span v-else></span>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button @click="closeAddDialog"
|
||||||
|
class="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg">
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button @click="handleAddHosts" :disabled="addLoading || parsedIds.length === 0"
|
||||||
|
class="px-4 py-2 text-sm bg-green-500 text-white rounded-lg hover:bg-green-600 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
{{ addLoading ? '导入中...' : `导入 ${parsedIds.length} 个主播` }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -231,6 +313,57 @@ const maxCount = ref(100)
|
|||||||
const selectedLevels = ref(new Set())
|
const selectedLevels = ref(new Set())
|
||||||
const showLevelDropdown = ref(false)
|
const showLevelDropdown = ref(false)
|
||||||
|
|
||||||
|
// 添加主播弹窗状态
|
||||||
|
const showAddDialog = ref(false)
|
||||||
|
const addLoading = ref(false)
|
||||||
|
const addStatus = ref(null)
|
||||||
|
const addForm = ref({
|
||||||
|
idsText: '',
|
||||||
|
invitationType: '1',
|
||||||
|
country: '美国',
|
||||||
|
hostsLevel: 'A1',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 国家选项
|
||||||
|
const COUNTRY_OPTIONS = [
|
||||||
|
{ label: '美国', value: '美国', eng: 'United States' },
|
||||||
|
{ label: '英国', value: '英国', eng: 'United Kingdom' },
|
||||||
|
{ label: '加拿大', value: '加拿大', eng: 'Canada' },
|
||||||
|
{ label: '澳大利亚', value: '澳大利亚', eng: 'Australia' },
|
||||||
|
{ label: '德国', value: '德国', eng: 'Germany' },
|
||||||
|
{ label: '法国', value: '法国', eng: 'France' },
|
||||||
|
{ label: '日本', value: '日本', eng: 'Japan' },
|
||||||
|
{ label: '韩国', value: '韩国', eng: 'South Korea' },
|
||||||
|
{ label: '巴西', value: '巴西', eng: 'Brazil' },
|
||||||
|
{ label: '印度尼西亚', value: '印度尼西亚', eng: 'Indonesia' },
|
||||||
|
{ label: '墨西哥', value: '墨西哥', eng: 'Mexico' },
|
||||||
|
{ label: '菲律宾', value: '菲律宾', eng: 'Philippines' },
|
||||||
|
{ label: '越南', value: '越南', eng: 'Vietnam' },
|
||||||
|
{ label: '泰国', value: '泰国', eng: 'Thailand' },
|
||||||
|
{ label: '马来西亚', value: '马来西亚', eng: 'Malaysia' },
|
||||||
|
{ label: '沙特阿拉伯', value: '沙特阿拉伯', eng: 'Saudi Arabia' },
|
||||||
|
{ label: '西班牙', value: '西班牙', eng: 'Spain' },
|
||||||
|
{ label: '意大利', value: '意大利', eng: 'Italy' },
|
||||||
|
{ label: '土耳其', value: '土耳其', eng: 'Turkey' },
|
||||||
|
{ label: '埃及', value: '埃及', eng: 'Egypt' },
|
||||||
|
{ label: '尼日利亚', value: '尼日利亚', eng: 'Nigeria' },
|
||||||
|
{ label: '哥伦比亚', value: '哥伦比亚', eng: 'Colombia' },
|
||||||
|
{ label: '阿根廷', value: '阿根廷', eng: 'Argentina' },
|
||||||
|
{ label: '智利', value: '智利', eng: 'Chile' },
|
||||||
|
{ label: '秘鲁', value: '秘鲁', eng: 'Peru' },
|
||||||
|
{ label: '以色列', value: '以色列', eng: 'Israel' },
|
||||||
|
{ label: '伊拉克', value: '伊拉克', eng: 'Iraq' },
|
||||||
|
{ label: '约旦', value: '约旦', eng: 'Jordan' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 解析输入的主播ID列表
|
||||||
|
const parsedIds = computed(() => {
|
||||||
|
return addForm.value.idsText
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(id => id.length > 0)
|
||||||
|
})
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
watch(() => props.visible, (newVal) => {
|
watch(() => props.visible, (newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
@@ -273,6 +406,7 @@ const loadHosts = async () => {
|
|||||||
if (!isElectron()) return
|
if (!isElectron()) return
|
||||||
try {
|
try {
|
||||||
const data = await window.electronAPI.loadAnchorData()
|
const data = await window.electronAPI.loadAnchorData()
|
||||||
|
console.log('加载主播数据:', data)
|
||||||
hosts.value = data
|
hosts.value = data
|
||||||
selected.value = new Set()
|
selected.value = new Set()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -413,22 +547,89 @@ const invertSelect = () => {
|
|||||||
selected.value = next
|
selected.value = next
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteSelected = () => {
|
const deleteSelected = async () => {
|
||||||
if (!selected.value.size) return
|
if (!selected.value.size) return
|
||||||
if (!confirm(`确认删除选中的 ${selected.value.size} 项吗?`)) return
|
if (!confirm(`确认删除选中的 ${selected.value.size} 项吗?`)) return
|
||||||
|
|
||||||
const remaining = hosts.value.filter(h => !selected.value.has(h.anchorId))
|
if (isElectron()) {
|
||||||
hosts.value = remaining
|
try {
|
||||||
|
const ids = Array.from(selected.value)
|
||||||
|
const result = await window.electronAPI.deleteAnchorData(ids)
|
||||||
|
if (result.success) {
|
||||||
|
console.log('[HostListDialog] 删除成功,重新加载数据')
|
||||||
|
await loadHosts()
|
||||||
|
} else {
|
||||||
|
console.error('[HostListDialog] 删除失败:', result.error)
|
||||||
|
// fallback: 前端本地删除
|
||||||
|
hosts.value = hosts.value.filter(h => !selected.value.has(h.anchorId))
|
||||||
selected.value = new Set()
|
selected.value = new Set()
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[HostListDialog] 删除异常:', e)
|
||||||
|
hosts.value = hosts.value.filter(h => !selected.value.has(h.anchorId))
|
||||||
|
selected.value = new Set()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hosts.value = hosts.value.filter(h => !selected.value.has(h.anchorId))
|
||||||
|
selected.value = new Set()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onClose = () => emit('close')
|
const onClose = () => emit('close')
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (isElectron()) {
|
|
||||||
await window.electronAPI.saveAnchorData(JSON.parse(JSON.stringify(hosts.value)))
|
|
||||||
}
|
|
||||||
emit('save', hosts.value)
|
emit('save', hosts.value)
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭添加弹窗
|
||||||
|
const closeAddDialog = () => {
|
||||||
|
showAddDialog.value = false
|
||||||
|
addForm.value = { idsText: '', invitationType: '1', country: '美国', hostsLevel: 'A1' }
|
||||||
|
addStatus.value = null
|
||||||
|
addLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量添加主播
|
||||||
|
const handleAddHosts = async () => {
|
||||||
|
const ids = parsedIds.value
|
||||||
|
if (ids.length === 0) return
|
||||||
|
|
||||||
|
addLoading.value = true
|
||||||
|
addStatus.value = null
|
||||||
|
|
||||||
|
// 查找国家英文名
|
||||||
|
const countryObj = COUNTRY_OPTIONS.find(c => c.value === addForm.value.country)
|
||||||
|
|
||||||
|
// 构造批量记录
|
||||||
|
const records = ids.map(hostsId => ({
|
||||||
|
hostsId,
|
||||||
|
invitationType: addForm.value.invitationType,
|
||||||
|
country: addForm.value.country || null,
|
||||||
|
countryEng: countryObj?.eng || null,
|
||||||
|
hostsLevel: addForm.value.hostsLevel || null,
|
||||||
|
createTime: Date.now(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (isElectron()) {
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.addAnchorData(records)
|
||||||
|
if (result.success) {
|
||||||
|
addStatus.value = { type: 'success', message: `成功导入 ${ids.length} 个主播` }
|
||||||
|
// 重新加载列表
|
||||||
|
await loadHosts()
|
||||||
|
// 延迟关闭弹窗
|
||||||
|
setTimeout(() => closeAddDialog(), 1000)
|
||||||
|
} else {
|
||||||
|
addStatus.value = { type: 'error', message: result.error || '导入失败' }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[HostListDialog] 添加主播失败:', e)
|
||||||
|
addStatus.value = { type: 'error', message: '导入异常: ' + String(e) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addStatus.value = { type: 'error', message: '非 Electron 环境,无法添加' }
|
||||||
|
}
|
||||||
|
addLoading.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="activeNotices.length > 0"
|
<!-- info / 无 category 的公告:滚动栏显示 title -->
|
||||||
:class="['notice-bar', `notice-bar--${currentNotice.type || 'info'}`]">
|
<div v-if="infoNotices.length > 0"
|
||||||
|
:class="['notice-bar', 'notice-bar--info']">
|
||||||
<!-- 图标 -->
|
<!-- 图标 -->
|
||||||
<span class="material-icons-round notice-bar__icon">campaign</span>
|
<span class="material-icons-round notice-bar__icon">campaign</span>
|
||||||
|
|
||||||
<!-- 滚动内容区域 -->
|
<!-- 滚动内容区域 -->
|
||||||
<div class="notice-bar__content" ref="wrapRef">
|
<div class="notice-bar__content" ref="wrapRef">
|
||||||
<div class="notice-bar__text" ref="textRef" :style="animationStyle">
|
<div class="notice-bar__text" ref="textRef" :style="animationStyle">
|
||||||
{{ currentNotice.content }}
|
{{ currentNotice.title }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 多条公告时显示计数 -->
|
<!-- 多条公告时显示计数 -->
|
||||||
<span v-if="activeNotices.length > 1" class="notice-bar__count">
|
<span v-if="infoNotices.length > 1" class="notice-bar__count">
|
||||||
{{ currentIndex + 1 }}/{{ activeNotices.length }}
|
{{ currentIndex + 1 }}/{{ infoNotices.length }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- 关闭按钮 -->
|
<!-- 关闭按钮 -->
|
||||||
@@ -22,6 +23,28 @@
|
|||||||
<span class="material-icons-round text-base">close</span>
|
<span class="material-icons-round text-base">close</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- danger / warning 的公告:弹窗逐条显示 title + content -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="currentAlert?.title"
|
||||||
|
width="480px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
align-center
|
||||||
|
>
|
||||||
|
<div class="alert-notice__content" v-html="currentAlert?.content"></div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button
|
||||||
|
v-if="alertIndex < alertNotices.length - 1"
|
||||||
|
@click="nextAlert"
|
||||||
|
>
|
||||||
|
下一条 ({{ alertIndex + 1 }}/{{ alertNotices.length }})
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" @click="closeAlert">
|
||||||
|
{{ alertIndex < alertNotices.length - 1 ? '全部关闭' : '我知道了' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -37,11 +60,12 @@ const props = defineProps({
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const noticeStore = useNoticeStore()
|
const noticeStore = useNoticeStore()
|
||||||
|
|
||||||
const activeNotices = computed(() => noticeStore.activeNotices)
|
// ========== info 滚动栏 ==========
|
||||||
|
const infoNotices = computed(() => noticeStore.infoNotices)
|
||||||
|
|
||||||
// 当前显示的公告索引(多条轮播)
|
// 当前显示的公告索引(多条轮播)
|
||||||
const currentIndex = ref(0)
|
const currentIndex = ref(0)
|
||||||
const currentNotice = computed(() => activeNotices.value[currentIndex.value] || {})
|
const currentNotice = computed(() => infoNotices.value[currentIndex.value] || {})
|
||||||
|
|
||||||
// 滚动动画
|
// 滚动动画
|
||||||
const wrapRef = ref(null)
|
const wrapRef = ref(null)
|
||||||
@@ -78,10 +102,10 @@ let rotateTimer = null
|
|||||||
|
|
||||||
const startRotate = () => {
|
const startRotate = () => {
|
||||||
stopRotate()
|
stopRotate()
|
||||||
if (activeNotices.value.length <= 1) return
|
if (infoNotices.value.length <= 1) return
|
||||||
|
|
||||||
rotateTimer = setInterval(() => {
|
rotateTimer = setInterval(() => {
|
||||||
currentIndex.value = (currentIndex.value + 1) % activeNotices.value.length
|
currentIndex.value = (currentIndex.value + 1) % infoNotices.value.length
|
||||||
}, 8000) // 每 8 秒切换
|
}, 8000) // 每 8 秒切换
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +122,7 @@ const handleClose = () => {
|
|||||||
if (notice && notice.id) {
|
if (notice && notice.id) {
|
||||||
noticeStore.dismissNotice(notice.id)
|
noticeStore.dismissNotice(notice.id)
|
||||||
// 调整索引
|
// 调整索引
|
||||||
if (currentIndex.value >= activeNotices.value.length) {
|
if (currentIndex.value >= infoNotices.value.length) {
|
||||||
currentIndex.value = 0
|
currentIndex.value = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,7 +134,7 @@ watch(currentNotice, () => {
|
|||||||
calculateScroll()
|
calculateScroll()
|
||||||
}, { flush: 'post' })
|
}, { flush: 'post' })
|
||||||
|
|
||||||
watch(() => activeNotices.value.length, (len) => {
|
watch(() => infoNotices.value.length, (len) => {
|
||||||
if (len > 1) {
|
if (len > 1) {
|
||||||
startRotate()
|
startRotate()
|
||||||
} else {
|
} else {
|
||||||
@@ -118,9 +142,43 @@ watch(() => activeNotices.value.length, (len) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ========== danger / warning 弹窗 ==========
|
||||||
|
const alertNotices = computed(() => noticeStore.alertNotices)
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const alertIndex = ref(0)
|
||||||
|
|
||||||
|
const currentAlert = computed(() => alertNotices.value[alertIndex.value] || null)
|
||||||
|
|
||||||
|
// 下一条弹窗公告
|
||||||
|
const nextAlert = () => {
|
||||||
|
// 关闭当前这条
|
||||||
|
if (currentAlert.value) {
|
||||||
|
noticeStore.dismissNotice(currentAlert.value.id)
|
||||||
|
}
|
||||||
|
alertIndex.value++
|
||||||
|
if (alertIndex.value >= alertNotices.value.length) {
|
||||||
|
dialogVisible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭全部弹窗公告
|
||||||
|
const closeAlert = () => {
|
||||||
|
alertNotices.value.forEach(n => noticeStore.dismissNotice(n.id))
|
||||||
|
dialogVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有弹窗类公告时自动弹出
|
||||||
|
watch(alertNotices, (list) => {
|
||||||
|
if (list.length > 0 && !dialogVisible.value) {
|
||||||
|
alertIndex.value = 0
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// ========== 生命周期 ==========
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
calculateScroll()
|
calculateScroll()
|
||||||
if (activeNotices.value.length > 1) {
|
if (infoNotices.value.length > 1) {
|
||||||
startRotate()
|
startRotate()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -151,31 +209,6 @@ onUnmounted(() => {
|
|||||||
color: #3b82f6;
|
color: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-bar--warning {
|
|
||||||
background-color: #fffbeb;
|
|
||||||
color: #92400e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notice-bar--warning .notice-bar__icon {
|
|
||||||
color: #f59e0b;
|
|
||||||
}
|
|
||||||
.notice-bar--danger {
|
|
||||||
background-color: rgb(253, 220, 220);
|
|
||||||
color: #f15252;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notice-bar--danger .notice-bar__icon {
|
|
||||||
color: #ff0000;
|
|
||||||
}
|
|
||||||
.notice-bar--urgent {
|
|
||||||
background-color: #fef2f2;
|
|
||||||
color: #991b1b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notice-bar--urgent .notice-bar__icon {
|
|
||||||
color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 图标 */
|
/* 图标 */
|
||||||
.notice-bar__icon {
|
.notice-bar__icon {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
@@ -246,4 +279,19 @@ onUnmounted(() => {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
background-color: rgba(0, 0, 0, 0.06);
|
background-color: rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 弹窗公告内容(v-html 渲染,需要用 :deep 穿透 scoped) */
|
||||||
|
.alert-notice__content {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-notice__content :deep(p) {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-notice__content :deep(p:last-child) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -33,9 +33,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||||
import { getMainUserData, setStorage, getStorage } from '@/utils/pk-mini/storage'
|
import { getMainUserData, setStorage, getStorage } from '@/utils/pk-mini/storage'
|
||||||
import { goEasyGetConversations } from '@/utils/pk-mini/goeasy'
|
import { goEasyGetConversations, getPkGoEasy, GoEasy } from '@/utils/pk-mini/goeasy'
|
||||||
|
import { pkUnreadStore } from '@/stores/pk-mini/notice.js'
|
||||||
import { signIn } from '@/api/pk-mini'
|
import { signIn } from '@/api/pk-mini'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
@@ -50,7 +51,8 @@ const props = defineProps({
|
|||||||
|
|
||||||
const defaultAvatar = 'https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/default-avatar.png'
|
const defaultAvatar = 'https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/default-avatar.png'
|
||||||
const userInfo = ref({})
|
const userInfo = ref({})
|
||||||
const unreadCount = ref(0)
|
const unreadStore = pkUnreadStore()
|
||||||
|
const unreadCount = computed(() => unreadStore.count)
|
||||||
const activeId = ref('pk')
|
const activeId = ref('pk')
|
||||||
|
|
||||||
const navigationModule = [
|
const navigationModule = [
|
||||||
@@ -79,30 +81,42 @@ function handleSettings() {
|
|||||||
|
|
||||||
function getChatList() {
|
function getChatList() {
|
||||||
goEasyGetConversations().then((res) => {
|
goEasyGetConversations().then((res) => {
|
||||||
if (res?.content?.unreadTotal) {
|
unreadStore.setCount(res?.content?.unreadTotal || 0)
|
||||||
unreadCount.value = res.content.unreadTotal
|
|
||||||
}
|
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onMessageReceived() {
|
||||||
|
unreadStore.setCount(unreadStore.count + 1)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 获取用户信息
|
|
||||||
const userData = getMainUserData()
|
const userData = getMainUserData()
|
||||||
if (userData) {
|
if (userData) {
|
||||||
userInfo.value = userData
|
userInfo.value = userData
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取保存的 activeId
|
|
||||||
const savedId = getStorage('activeId')
|
const savedId = getStorage('activeId')
|
||||||
if (savedId) {
|
if (savedId) {
|
||||||
activeId.value = savedId
|
activeId.value = savedId
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取未读消息数
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
getChatList()
|
getChatList()
|
||||||
|
const goeasy = getPkGoEasy()
|
||||||
|
if (goeasy) {
|
||||||
|
goeasy.im.on(GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, onMessageReceived)
|
||||||
|
}
|
||||||
}, 2000)
|
}, 2000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
const goeasy = getPkGoEasy()
|
||||||
|
if (goeasy) {
|
||||||
|
try {
|
||||||
|
goeasy.im.off(GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, onMessageReceived)
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat-message-mini-pk">
|
<div class="chat-message-mini-pk" :class="{ compact: compact }">
|
||||||
|
<!-- 卡片区域(相对定位容器,VS 绝对叠在中间) -->
|
||||||
|
<div class="pk-cards">
|
||||||
<!-- 用户A -->
|
<!-- 用户A -->
|
||||||
<div class="userA">
|
<div class="userA">
|
||||||
<div class="Avatar">
|
|
||||||
<img class="AvatarImg" :src="ArticleDetailsA.anchorIcon" alt="" />
|
|
||||||
<div class="name">{{ ArticleDetailsA.anchorId }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="genderAndCountry">
|
<div class="genderAndCountry">
|
||||||
<div class="gender" :style="{ background: ArticleDetailsA.sex == 1 ? '#59D8DB' : '#F3876F' }">
|
<div class="gender" :style="{ background: ArticleDetailsA.sex == 1 ? '#59D8DB' : '#F3876F' }">
|
||||||
{{ ArticleDetailsA.sex == 1 ? $t('pkMini.man') : $t('pkMini.woman') }}
|
{{ ArticleDetailsA.sex == 1 ? $t('pkMini.man') : $t('pkMini.woman') }}
|
||||||
@@ -34,17 +32,13 @@
|
|||||||
<div class="Remarks">{{ $t('pkMini.Note') + ArticleDetailsA.remark }}</div>
|
<div class="Remarks">{{ $t('pkMini.Note') + ArticleDetailsA.remark }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- VS -->
|
<!-- VS 叠在两卡片中间缝 -->
|
||||||
<div class="messageVS">
|
<div class="messageVS">
|
||||||
<img class="messageVS-img" src="@/assets/pk-mini/messageVS.png" alt="" />
|
<img class="messageVS-img" src="@/assets/pk-mini/messageVS.png" alt="" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 用户B -->
|
<!-- 用户B -->
|
||||||
<div class="userB">
|
<div class="userB">
|
||||||
<div class="Avatar">
|
|
||||||
<img class="AvatarImg" :src="ArticleDetailsB.anchorIcon" alt="" />
|
|
||||||
<div class="name">{{ ArticleDetailsB.anchorId }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="genderAndCountry">
|
<div class="genderAndCountry">
|
||||||
<div class="gender" :style="{ background: ArticleDetailsB.sex == 1 ? '#59D8DB' : '#F3876F' }">
|
<div class="gender" :style="{ background: ArticleDetailsB.sex == 1 ? '#59D8DB' : '#F3876F' }">
|
||||||
{{ ArticleDetailsB.sex == 1 ? $t('pkMini.man') : $t('pkMini.woman') }}
|
{{ ArticleDetailsB.sex == 1 ? $t('pkMini.man') : $t('pkMini.woman') }}
|
||||||
@@ -72,6 +66,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="Remarks">{{ $t('pkMini.Note') + ArticleDetailsB.remark }}</div>
|
<div class="Remarks">{{ $t('pkMini.Note') + ArticleDetailsB.remark }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 按钮 -->
|
<!-- 按钮 -->
|
||||||
<div class="btn" v-if="PkIDInfodata.pkStatus === 0 && ArticleDetailsB.senderId != info.id">
|
<div class="btn" v-if="PkIDInfodata.pkStatus === 0 && ArticleDetailsB.senderId != info.id">
|
||||||
@@ -123,6 +118,10 @@ const props = defineProps({
|
|||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
compact: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -196,58 +195,43 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chat-message-mini-pk {
|
.chat-message-mini-pk {
|
||||||
width: 325px;
|
width: 560px;
|
||||||
height: 820px;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
.pk-cards {
|
||||||
|
position: relative;
|
||||||
|
width: 90%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.messageVS {
|
.messageVS {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
width: 67px;
|
width: 67px;
|
||||||
height: 67px;
|
height: 67px;
|
||||||
margin-top: -33.5px;
|
|
||||||
margin-bottom: -33.5px;
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.messageVS-img {
|
.messageVS-img {
|
||||||
width: 67px;
|
width: 67px;
|
||||||
height: 67px;
|
height: 67px;
|
||||||
}
|
}
|
||||||
.userA {
|
.userA {
|
||||||
width: 90%;
|
width: 100%;
|
||||||
height: 335px;
|
|
||||||
background-color: #c0e8e8;
|
background-color: #c0e8e8;
|
||||||
border-top-left-radius: 20px;
|
border-radius: 20px 20px 0 0;
|
||||||
border-top-right-radius: 20px;
|
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
padding-bottom: 15px;
|
||||||
.Avatar {
|
|
||||||
width: 90%;
|
|
||||||
height: 50px;
|
|
||||||
margin-top: 15px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.AvatarImg {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
.name {
|
|
||||||
width: calc(100% - 60px);
|
|
||||||
margin-left: 10px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #000000;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
.genderAndCountry {
|
.genderAndCountry {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
@@ -314,15 +298,13 @@ onMounted(() => {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
.userB {
|
.userB {
|
||||||
width: 90%;
|
width: 100%;
|
||||||
height: 315px;
|
|
||||||
background-color: #f8e4e0;
|
background-color: #f8e4e0;
|
||||||
border-bottom-left-radius: 20px;
|
border-radius: 0 0 20px 20px;
|
||||||
border-bottom-right-radius: 20px;
|
|
||||||
padding-top: 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding-bottom: 15px;
|
||||||
}
|
}
|
||||||
.btn {
|
.btn {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
@@ -424,4 +406,29 @@ onMounted(() => {
|
|||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 紧凑模式:适配 PK 大厅 350px 窄聊天框 */
|
||||||
|
.compact.chat-message-mini-pk {
|
||||||
|
width: 300px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
.compact .pk-cards {
|
||||||
|
width: 92%;
|
||||||
|
}
|
||||||
|
.compact .userA {
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.compact .userB {
|
||||||
|
border-radius: 0 0 12px 12px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.compact .messageVS {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
.compact .messageVS-img {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 我的PK记录 -->
|
<!-- 我的PK记录 -->
|
||||||
<div class="pk-record">
|
<div class="pk-record">
|
||||||
<el-splitter>
|
<div class="pk-layout">
|
||||||
<el-splitter-panel>
|
<div class="left-panel">
|
||||||
<div class="demo-panel">
|
<div class="demo-panel">
|
||||||
<!-- 选项卡 -->
|
<!-- 选项卡 -->
|
||||||
<div class="tab-header">
|
<div class="tab-header">
|
||||||
@@ -62,10 +62,10 @@
|
|||||||
|
|
||||||
<div class="empty-tip" v-else>您还没有PK记录!</div>
|
<div class="empty-tip" v-else>您还没有PK记录!</div>
|
||||||
</div>
|
</div>
|
||||||
</el-splitter-panel>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧详情 -->
|
<!-- 右侧详情 -->
|
||||||
<el-splitter-panel :size="30" :resizable="false">
|
<div class="right-panel">
|
||||||
<div class="detail-panel" v-if="selectedData">
|
<div class="detail-panel" v-if="selectedData">
|
||||||
<!-- 双方头像 -->
|
<!-- 双方头像 -->
|
||||||
<div class="detail-avatars">
|
<div class="detail-avatars">
|
||||||
@@ -110,8 +110,8 @@
|
|||||||
<div class="empty-detail" v-else>
|
<div class="empty-detail" v-else>
|
||||||
<span>选择右侧的记录,可立即查看详细信息</span>
|
<span>选择右侧的记录,可立即查看详细信息</span>
|
||||||
</div>
|
</div>
|
||||||
</el-splitter-panel>
|
</div>
|
||||||
</el-splitter>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -232,6 +232,28 @@ onMounted(() => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pk-layout {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
width: 380px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-left: 1px solid #03aba82f;
|
||||||
|
}
|
||||||
|
|
||||||
.demo-panel {
|
.demo-panel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -378,53 +400,54 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px;
|
padding: 15px;
|
||||||
border-left: 1px solid #03aba82f;
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-avatars {
|
.detail-avatars {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 40px;
|
gap: 20px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-avatar {
|
.detail-avatar {
|
||||||
width: 70px;
|
width: 50px;
|
||||||
height: 70px;
|
height: 50px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-total {
|
.detail-total {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-card {
|
.total-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
padding: 15px;
|
padding: 10px;
|
||||||
background: linear-gradient(90deg, #e4ffff, #fff, #e4ffff);
|
background: linear-gradient(90deg, #e4ffff, #fff, #e4ffff);
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-num {
|
.total-num {
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-icon {
|
.total-icon {
|
||||||
width: 35px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-rounds {
|
.detail-rounds {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,9 +455,9 @@ onMounted(() => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
padding: 15px;
|
padding: 10px;
|
||||||
border-radius: 16px;
|
border-radius: 12px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,9 +472,9 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.round-item {
|
.round-item {
|
||||||
padding: 12px 15px;
|
padding: 8px 10px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #03aba8;
|
color: #03aba8;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
10
src/config/index.js
Normal file
10
src/config/index.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export const ENV = {
|
||||||
|
// 主 API 地址
|
||||||
|
API_BASE_URL: import.meta.env.VITE_API_BASE_URL,
|
||||||
|
// 注册 API 地址(tkNewAdmin 后端)
|
||||||
|
REGISTER_API_URL: import.meta.env.VITE_REGISTER_API_URL,
|
||||||
|
// PK Mini API 地址
|
||||||
|
PK_MINI_API_URL: import.meta.env.VITE_PK_MINI_API_URL,
|
||||||
|
// YOLO 商店 iframe 地址
|
||||||
|
SHOP_URL: import.meta.env.VITE_SHOP_URL,
|
||||||
|
}
|
||||||
@@ -3,20 +3,22 @@
|
|||||||
*
|
*
|
||||||
* GoEasy 续费后,将 GOEASY_ENABLED 改为 true 即可开启消息功能
|
* GoEasy 续费后,将 GOEASY_ENABLED 改为 true 即可开启消息功能
|
||||||
*/
|
*/
|
||||||
|
import { ENV } from '@/config'
|
||||||
|
|
||||||
export const PK_MINI_CONFIG = {
|
export const PK_MINI_CONFIG = {
|
||||||
// GoEasy 开关 - 续费后改为 true
|
// GoEasy 开关 - 续费后改为 true
|
||||||
GOEASY_ENABLED: false,
|
GOEASY_ENABLED: true,
|
||||||
|
|
||||||
// GoEasy 配置
|
// GoEasy 配置
|
||||||
GOEASY: {
|
GOEASY: {
|
||||||
HOST: 'hangzhou.goeasy.io',
|
HOST: 'hangzhou.goeasy.io',
|
||||||
APP_KEY: 'BC-a88037e060ed4753bb316ac7239e62d9',
|
APP_KEY: 'PC-a88037e060ed4753bb316ac7239e62d9',
|
||||||
},
|
},
|
||||||
|
|
||||||
// API 基础地址
|
// API 基础地址(从中心配置读取,随环境自动切换)
|
||||||
API_BASE_URL: 'http://192.168.2.22:8086/',
|
get API_BASE_URL() {
|
||||||
// API_BASE_URL: 'https://pk.hanxiaokj.cn/',
|
return ENV.PK_MINI_API_URL + '/'
|
||||||
|
},
|
||||||
|
|
||||||
// 头像 CDN 地址
|
// 头像 CDN 地址
|
||||||
AVATAR_CDN_PREFIX: 'https://vv-1317974657.cos.ap-shanghai.myqcloud.com/headerIcon/',
|
AVATAR_CDN_PREFIX: 'https://vv-1317974657.cos.ap-shanghai.myqcloud.com/headerIcon/',
|
||||||
|
|||||||
@@ -142,7 +142,7 @@
|
|||||||
<div v-show="currentView === 'shop'" class="absolute inset-0 z-20 h-full overflow-hidden">
|
<div v-show="currentView === 'shop'" class="absolute inset-0 z-20 h-full overflow-hidden">
|
||||||
<iframe
|
<iframe
|
||||||
v-if="adminLoaded"
|
v-if="adminLoaded"
|
||||||
src="http://192.168.2.128:8085/"
|
:src="shopUrl"
|
||||||
class="w-full h-full border-0"
|
class="w-full h-full border-0"
|
||||||
allow="clipboard-read; clipboard-write"
|
allow="clipboard-read; clipboard-write"
|
||||||
></iframe>
|
></iframe>
|
||||||
@@ -161,6 +161,7 @@ import ConfigPage from '@/pages/ConfigPage.vue'
|
|||||||
import FanWorkbench from '@/views/tk/FanWorkbench.vue'
|
import FanWorkbench from '@/views/tk/FanWorkbench.vue'
|
||||||
import PkMiniWorkbench from '@/views/pk-mini/PkMiniWorkbench.vue'
|
import PkMiniWorkbench from '@/views/pk-mini/PkMiniWorkbench.vue'
|
||||||
import PermissionMask from '@/components/PermissionMask.vue'
|
import PermissionMask from '@/components/PermissionMask.vue'
|
||||||
|
import { ENV } from '@/config'
|
||||||
|
|
||||||
// 占位图片 - 无权限时显示的工作台截图
|
// 占位图片 - 无权限时显示的工作台截图
|
||||||
import placeholderTk from '@/assets/placeholder-tk.png'
|
import placeholderTk from '@/assets/placeholder-tk.png'
|
||||||
@@ -173,6 +174,7 @@ const emit = defineEmits(['logout', 'go-back', 'stop-all'])
|
|||||||
const currentView = ref('tk') // Default Tab
|
const currentView = ref('tk') // Default Tab
|
||||||
const autoDmMode = ref('config') // Default Sub-state: 'config' or 'browser'
|
const autoDmMode = ref('config') // Default Sub-state: 'config' or 'browser'
|
||||||
const adminLoaded = ref(false) // 懒加载:首次切换到管理后台时才加载 iframe
|
const adminLoaded = ref(false) // 懒加载:首次切换到管理后台时才加载 iframe
|
||||||
|
const shopUrl = ENV.SHOP_URL
|
||||||
|
|
||||||
const handleGoToBrowser = async () => {
|
const handleGoToBrowser = async () => {
|
||||||
autoDmMode.value = 'browser'
|
autoDmMode.value = 'browser'
|
||||||
|
|||||||
@@ -244,6 +244,23 @@ export default {
|
|||||||
save: '保存',
|
save: '保存',
|
||||||
loading: '加载中...',
|
loading: '加载中...',
|
||||||
noData: '暂无数据',
|
noData: '暂无数据',
|
||||||
noNotice: '暂无站内信'
|
noNotice: '暂无站内信',
|
||||||
|
// MiniPKMessage 邀请卡片
|
||||||
|
man: '男',
|
||||||
|
woman: '女',
|
||||||
|
PKTime: 'PK时间:',
|
||||||
|
GoldCoin: '金币:',
|
||||||
|
match: '场',
|
||||||
|
Note: '备注:',
|
||||||
|
agree: '同意',
|
||||||
|
Refuse: '拒绝',
|
||||||
|
HaveAgreedToTheInvitation: '已同意邀请',
|
||||||
|
HaveRefusedTheInvitation: '已拒绝邀请',
|
||||||
|
WaitForTheOtherPartyResponse: '等待对方响应',
|
||||||
|
Hint: '提示',
|
||||||
|
AfterASuccessfulInvitationThePKCannotBeModifiedOrDeletedPleaseOperateWithCaution: '同意后PK邀请将无法修改或删除,请谨慎操作',
|
||||||
|
AreYouSureYouWantToDeclineThisInvitation: '确定要拒绝此邀请吗?',
|
||||||
|
Cancel: '取消',
|
||||||
|
Confirm: '确认'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
|
|
||||||
<!-- Left Side: Form -->
|
<!-- Left Side: Form -->
|
||||||
<div class="w-full md:w-1/2 p-8 md:p-12 lg:p-16 flex flex-col justify-center">
|
<div class="w-full md:w-1/2 p-8 md:p-12 lg:p-16 flex flex-col justify-center">
|
||||||
<!-- Header / Logo -->
|
<!-- Header / Logo(注册页隐藏,避免表单被挤出屏幕) -->
|
||||||
<div class="flex justify-center">
|
<div v-show="mode === 'login'" class="flex justify-center">
|
||||||
<img :src="logo" alt="Logo" class="w-[200px] h-auto" />
|
<img :src="logo" alt="Logo" class="w-[200px] h-auto" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,18 +8,29 @@ export const useNoticeStore = defineStore('notice', () => {
|
|||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const dismissedIds = ref([]) // 当前会话已关闭的公告 ID
|
const dismissedIds = ref([]) // 当前会话已关闭的公告 ID
|
||||||
const lastFetchTime = ref(null)
|
const lastFetchTime = ref(null)
|
||||||
const useMock = ref(true) // 后台接口就绪后改为 false
|
const useMock = ref(false) // 后台接口就绪后改为 false
|
||||||
|
|
||||||
// 过滤已关闭公告后的有效列表
|
// 过滤已关闭公告后的有效列表
|
||||||
const activeNotices = computed(() =>
|
const activeNotices = computed(() =>
|
||||||
notices.value.filter(n => !dismissedIds.value.includes(n.id))
|
notices.value.filter(n => !dismissedIds.value.includes(n.id))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// info 或无 category 的公告 → 滚动栏显示 title
|
||||||
|
const infoNotices = computed(() =>
|
||||||
|
activeNotices.value.filter(n => !n.category || n.category === 'info')
|
||||||
|
)
|
||||||
|
|
||||||
|
// danger / warning 的公告 → 弹窗显示 title + content
|
||||||
|
const alertNotices = computed(() =>
|
||||||
|
activeNotices.value.filter(n => n.category === 'danger' || n.category === 'warning')
|
||||||
|
)
|
||||||
|
|
||||||
// 是否有可显示的公告
|
// 是否有可显示的公告
|
||||||
const hasNotices = computed(() => activeNotices.value.length > 0)
|
const hasNotices = computed(() => activeNotices.value.length > 0)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从后台拉取公告
|
* 从后台拉取公告
|
||||||
|
* 全局 axios 拦截器在 code==0 时返回 response.data.data,即数组本身
|
||||||
*/
|
*/
|
||||||
const fetchNotices = async () => {
|
const fetchNotices = async () => {
|
||||||
if (isLoading.value) return
|
if (isLoading.value) return
|
||||||
@@ -31,10 +42,9 @@ export const useNoticeStore = defineStore('notice', () => {
|
|||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getActiveNotices()
|
const res = await getActiveNotices()
|
||||||
if (res && res.data) {
|
console.log('[NoticeStore] 获取公告', res)
|
||||||
notices.value = res.data
|
notices.value = Array.isArray(res) ? res : []
|
||||||
lastFetchTime.value = Date.now()
|
lastFetchTime.value = Date.now()
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[NoticeStore] 获取公告失败:', error)
|
console.error('[NoticeStore] 获取公告失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -56,8 +66,9 @@ export const useNoticeStore = defineStore('notice', () => {
|
|||||||
*/
|
*/
|
||||||
const loadMockNotices = () => {
|
const loadMockNotices = () => {
|
||||||
notices.value = [
|
notices.value = [
|
||||||
{ id: 1, content: '欢迎使用 Yolo 系统,如有问题请联系管理员。', type: 'info' },
|
{ id: 1, title: 'YOLO 系统公告', content: '<p>欢迎使用 Yolo 系统,如有问题请联系管理员。</p>', category: 'info' },
|
||||||
{ id: 2, content: '系统将于本周六凌晨 2:00-4:00 进行维护升级,届时服务将暂停,请提前做好安排。', type: 'warning' },
|
{ id: 2, title: '系统维护通知', content: '<p>系统将于本周六凌晨 2:00-4:00 进行维护升级,届时服务将暂停,请提前做好安排。</p>', category: 'warning' },
|
||||||
|
{ id: 3, title: '紧急安全通知', content: '<p>请所有用户立即更新客户端至最新版本。</p>', category: 'danger' },
|
||||||
]
|
]
|
||||||
lastFetchTime.value = Date.now()
|
lastFetchTime.value = Date.now()
|
||||||
}
|
}
|
||||||
@@ -66,6 +77,8 @@ export const useNoticeStore = defineStore('notice', () => {
|
|||||||
// 状态
|
// 状态
|
||||||
notices,
|
notices,
|
||||||
activeNotices,
|
activeNotices,
|
||||||
|
infoNotices,
|
||||||
|
alertNotices,
|
||||||
hasNotices,
|
hasNotices,
|
||||||
isLoading,
|
isLoading,
|
||||||
useMock,
|
useMock,
|
||||||
|
|||||||
@@ -43,3 +43,20 @@ export const pkIMloginStore = defineStore('pkIMlogin', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const pkUnreadStore = defineStore('pkUnread', {
|
||||||
|
state: () => {
|
||||||
|
return { count: 0 }
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setCount(count) {
|
||||||
|
this.count = count
|
||||||
|
},
|
||||||
|
decrease(num = 1) {
|
||||||
|
this.count = Math.max(0, this.count - num)
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
this.count = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -8,24 +8,13 @@ import router from '@/router'
|
|||||||
|
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { usePythonBridge, } from '@/utils/pythonBridge'
|
import { usePythonBridge, } from '@/utils/pythonBridge'
|
||||||
|
import { ENV } from '@/config'
|
||||||
|
|
||||||
const { stopScript } = usePythonBridge();
|
const { stopScript } = usePythonBridge();
|
||||||
|
|
||||||
|
|
||||||
// 请求地址前缀
|
// 请求地址前缀
|
||||||
let baseURL = ''
|
const baseURL = ENV.API_BASE_URL
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
// 生产环境
|
|
||||||
// baseURL = "https://api.tkpage.yolozs.com"
|
|
||||||
baseURL = "http://192.168.2.22:8101"
|
|
||||||
// baseURL = "https://crawlclient.api.yolozs.com"
|
|
||||||
} else {
|
|
||||||
// 测试环境
|
|
||||||
// baseURL = "http://120.26.251.180:8085/"
|
|
||||||
// 开发环境
|
|
||||||
baseURL = "https://crawlclient.api.yolozs.com"
|
|
||||||
// baseURL = "http://api.tkpage.vvtiktok.cn"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 请求拦截器
|
// 请求拦截器
|
||||||
axios.interceptors.request.use((config) => {
|
axios.interceptors.request.use((config) => {
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export function goEasyGetConversations() {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
im.latestConversations({
|
im.latestConversations({
|
||||||
onSuccess: function (result) {
|
onSuccess: function (result) {
|
||||||
|
console.log('会话列表', result)
|
||||||
resolve(result)
|
resolve(result)
|
||||||
},
|
},
|
||||||
onFailed: function (error) {
|
onFailed: function (error) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 消息页面 -->
|
<!-- 消息页面 -->
|
||||||
<div class="message-page">
|
<div class="message-page">
|
||||||
<el-splitter class="message-splitter">
|
<div class="message-layout">
|
||||||
<!-- 会话列表 -->
|
<!-- 会话列表 -->
|
||||||
<el-splitter-panel :size="25" :min="20" :max="35">
|
<div class="conversation-panel">
|
||||||
<div class="conversation-list">
|
<div class="conversation-list">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in chatList"
|
v-for="(item, index) in chatList"
|
||||||
@@ -13,9 +13,6 @@
|
|||||||
@click="selectChat(item)"
|
@click="selectChat(item)"
|
||||||
>
|
>
|
||||||
<el-badge :value="item.unread > 0 ? item.unread : ''" :max="99">
|
<el-badge :value="item.unread > 0 ? item.unread : ''" :max="99">
|
||||||
<div class="conv-avatar">
|
|
||||||
<img :src="item.data?.avatar || defaultAvatar" alt="" />
|
|
||||||
</div>
|
|
||||||
</el-badge>
|
</el-badge>
|
||||||
<div class="conv-info">
|
<div class="conv-info">
|
||||||
<div class="conv-header">
|
<div class="conv-header">
|
||||||
@@ -28,10 +25,10 @@
|
|||||||
|
|
||||||
<div v-if="chatList.length === 0" class="empty-tip">暂无会话</div>
|
<div v-if="chatList.length === 0" class="empty-tip">暂无会话</div>
|
||||||
</div>
|
</div>
|
||||||
</el-splitter-panel>
|
</div>
|
||||||
|
|
||||||
<!-- 消息列表 -->
|
<!-- 消息列表 -->
|
||||||
<el-splitter-panel>
|
<div class="chat-panel">
|
||||||
<div v-if="selectedChat" class="chat-container">
|
<div v-if="selectedChat" class="chat-container">
|
||||||
<div class="chat-messages" ref="chatMessagesRef">
|
<div class="chat-messages" ref="chatMessagesRef">
|
||||||
<div
|
<div
|
||||||
@@ -40,13 +37,10 @@
|
|||||||
class="message-item"
|
class="message-item"
|
||||||
:class="{ mine: msg.senderId == currentUser.id }"
|
:class="{ mine: msg.senderId == currentUser.id }"
|
||||||
>
|
>
|
||||||
<div class="message-avatar">
|
|
||||||
<img :src="msg.senderId == currentUser.id ? currentUser.headerIcon : selectedChat.data?.avatar" alt="" />
|
|
||||||
</div>
|
|
||||||
<div class="message-bubble">
|
<div class="message-bubble">
|
||||||
<div v-if="msg.type === 'text'" class="text-message">{{ msg.payload.text }}</div>
|
<div v-if="msg.type === 'text'" class="text-message">{{ msg.payload.text }}</div>
|
||||||
<PictureMessage v-else-if="msg.type === 'image'" :item="msg" />
|
<PictureMessage v-else-if="msg.type === 'image'" :item="msg" />
|
||||||
<PKMessage v-else-if="msg.type === 'pk'" :item="msg" />
|
<MiniPKMessage v-else-if="msg.type === 'pk'" :item="msg" />
|
||||||
<VoiceMessage v-else-if="msg.type === 'audio'" :item="msg.payload.url" :size="msg.payload.duration" />
|
<VoiceMessage v-else-if="msg.type === 'audio'" :item="msg.payload.url" :size="msg.payload.duration" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,8 +67,8 @@
|
|||||||
<span class="material-icons-round placeholder-icon">chat_bubble_outline</span>
|
<span class="material-icons-round placeholder-icon">chat_bubble_outline</span>
|
||||||
<p>选择左侧会话开始聊天</p>
|
<p>选择左侧会话开始聊天</p>
|
||||||
</div>
|
</div>
|
||||||
</el-splitter-panel>
|
</div>
|
||||||
</el-splitter>
|
</div>
|
||||||
|
|
||||||
<!-- 隐藏的文件输入 -->
|
<!-- 隐藏的文件输入 -->
|
||||||
<input
|
<input
|
||||||
@@ -88,7 +82,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
||||||
import { getMainUserData } from '@/utils/pk-mini/storage'
|
import { getMainUserData } from '@/utils/pk-mini/storage'
|
||||||
import { TimestamptolocalTime } from '@/utils/pk-mini/timeConversion'
|
import { TimestamptolocalTime } from '@/utils/pk-mini/timeConversion'
|
||||||
import { isGoEasyEnabled } from '@/config/pk-mini'
|
import { isGoEasyEnabled } from '@/config/pk-mini'
|
||||||
@@ -102,9 +96,10 @@ import {
|
|||||||
GoEasy
|
GoEasy
|
||||||
} from '@/utils/pk-mini/goeasy'
|
} from '@/utils/pk-mini/goeasy'
|
||||||
import PictureMessage from '@/components/pk-mini/chat/PictureMessage.vue'
|
import PictureMessage from '@/components/pk-mini/chat/PictureMessage.vue'
|
||||||
import PKMessage from '@/components/pk-mini/chat/PKMessage.vue'
|
import MiniPKMessage from '@/components/pk-mini/chat/MiniPKMessage.vue'
|
||||||
import VoiceMessage from '@/components/pk-mini/chat/VoiceMessage.vue'
|
import VoiceMessage from '@/components/pk-mini/chat/VoiceMessage.vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { pkUnreadStore } from '@/stores/pk-mini/notice.js'
|
||||||
|
|
||||||
const defaultAvatar = 'https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/default-avatar.png'
|
const defaultAvatar = 'https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/default-avatar.png'
|
||||||
const chatList = ref([])
|
const chatList = ref([])
|
||||||
@@ -114,6 +109,7 @@ const inputText = ref('')
|
|||||||
const currentUser = ref({})
|
const currentUser = ref({})
|
||||||
const chatMessagesRef = ref(null)
|
const chatMessagesRef = ref(null)
|
||||||
const fileInputRef = ref(null)
|
const fileInputRef = ref(null)
|
||||||
|
const unreadStore = pkUnreadStore()
|
||||||
|
|
||||||
const formatTime = TimestamptolocalTime
|
const formatTime = TimestamptolocalTime
|
||||||
|
|
||||||
@@ -125,7 +121,7 @@ async function loadConversations() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await goEasyGetConversations()
|
const result = await goEasyGetConversations()
|
||||||
chatList.value = result?.content || []
|
chatList.value = result?.content?.conversations || []
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载会话列表失败', e)
|
console.error('加载会话列表失败', e)
|
||||||
}
|
}
|
||||||
@@ -136,12 +132,15 @@ async function selectChat(item) {
|
|||||||
|
|
||||||
selectedChat.value = item
|
selectedChat.value = item
|
||||||
try {
|
try {
|
||||||
const messages = await goEasyGetMessages({ id: item.userId, timestamp: null })
|
const messages = await goEasyGetMessages({ id: String(item.userId), timestamp: null })
|
||||||
messagesList.value = messages || []
|
messagesList.value = messages || []
|
||||||
await nextTick()
|
await nextTick()
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
// 异步卡片内容加载完后再滚一次
|
||||||
|
setTimeout(scrollToBottom, 300)
|
||||||
// 标记消息已读
|
// 标记消息已读
|
||||||
goEasyMessageRead({ id: item.userId }).catch(() => {})
|
goEasyMessageRead({ id: String(item.userId) }).catch(() => {})
|
||||||
|
unreadStore.decrease(item.unread || 0)
|
||||||
item.unread = 0
|
item.unread = 0
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载消息失败', e)
|
console.error('加载消息失败', e)
|
||||||
@@ -166,7 +165,7 @@ async function sendMessage() {
|
|||||||
try {
|
try {
|
||||||
const msg = await goEasySendMessage({
|
const msg = await goEasySendMessage({
|
||||||
text: inputText.value,
|
text: inputText.value,
|
||||||
id: selectedChat.value.userId,
|
id: String(selectedChat.value.userId),
|
||||||
avatar: currentUser.value.headerIcon,
|
avatar: currentUser.value.headerIcon,
|
||||||
nickname: currentUser.value.nickName
|
nickname: currentUser.value.nickName
|
||||||
})
|
})
|
||||||
@@ -202,7 +201,7 @@ async function handleFileSelect(event) {
|
|||||||
try {
|
try {
|
||||||
const msg = await goEasySendImageMessage({
|
const msg = await goEasySendImageMessage({
|
||||||
imagefile: file,
|
imagefile: file,
|
||||||
id: selectedChat.value.userId,
|
id: String(selectedChat.value.userId),
|
||||||
avatar: currentUser.value.headerIcon,
|
avatar: currentUser.value.headerIcon,
|
||||||
nickname: currentUser.value.nickName
|
nickname: currentUser.value.nickName
|
||||||
})
|
})
|
||||||
@@ -219,14 +218,25 @@ onMounted(() => {
|
|||||||
currentUser.value = getMainUserData() || {}
|
currentUser.value = getMainUserData() || {}
|
||||||
if (isGoEasyEnabled()) {
|
if (isGoEasyEnabled()) {
|
||||||
loadConversations()
|
loadConversations()
|
||||||
// 监听新消息
|
|
||||||
const goeasy = getPkGoEasy()
|
const goeasy = getPkGoEasy()
|
||||||
if (goeasy) {
|
if (goeasy) {
|
||||||
goeasy.im.on(GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, onMessageReceived)
|
goeasy.im.on(GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, onMessageReceived)
|
||||||
|
goeasy.im.on(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, onConversationsUpdated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 切换回消息页面时,滚到聊天记录最底部
|
||||||
|
nextTick(() => setTimeout(scrollToBottom, 300))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听 chatMessagesRef 出现(selectedChat 从 null 变为有值时 DOM 才渲染)
|
||||||
|
watch(chatMessagesRef, (el) => {
|
||||||
|
if (el) setTimeout(scrollToBottom, 300)
|
||||||
|
})
|
||||||
|
|
||||||
|
function onConversationsUpdated(conversations) {
|
||||||
|
chatList.value = conversations.conversations || []
|
||||||
|
}
|
||||||
|
|
||||||
function onMessageReceived(message) {
|
function onMessageReceived(message) {
|
||||||
if (!isGoEasyEnabled()) return
|
if (!isGoEasyEnabled()) return
|
||||||
// 更新会话列表中的未读数
|
// 更新会话列表中的未读数
|
||||||
@@ -249,6 +259,7 @@ onUnmounted(() => {
|
|||||||
if (goeasy) {
|
if (goeasy) {
|
||||||
try {
|
try {
|
||||||
goeasy.im.off(GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, onMessageReceived)
|
goeasy.im.off(GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, onMessageReceived)
|
||||||
|
goeasy.im.off(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, onConversationsUpdated)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('清理 GoEasy 监听器失败', e)
|
console.warn('清理 GoEasy 监听器失败', e)
|
||||||
}
|
}
|
||||||
@@ -262,13 +273,27 @@ onUnmounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border: 1px solid #f1f5f9; // slate-100
|
border: 1px solid #f1f5f9;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); // shadow-sm
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-splitter {
|
.message-layout {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-panel {
|
||||||
|
width: 280px;
|
||||||
|
min-width: 220px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-right: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-panel {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,11 +71,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 列表和聊天区域 -->
|
<!-- 列表和聊天区域 -->
|
||||||
<el-splitter class="pk-splitter">
|
<div class="content-area">
|
||||||
<el-splitter-panel>
|
|
||||||
<el-splitter>
|
|
||||||
<!-- 列表面板 -->
|
<!-- 列表面板 -->
|
||||||
<el-splitter-panel :size="70" :resizable="false">
|
|
||||||
<div class="list-panel">
|
<div class="list-panel">
|
||||||
<div
|
<div
|
||||||
v-infinite-scroll="loadMore"
|
v-infinite-scroll="loadMore"
|
||||||
@@ -120,10 +117,8 @@
|
|||||||
<div v-if="pkList.length === 0" class="empty-tip">暂无数据</div>
|
<div v-if="pkList.length === 0" class="empty-tip">暂无数据</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-splitter-panel>
|
|
||||||
|
|
||||||
<!-- 聊天面板 -->
|
<!-- 聊天面板 -->
|
||||||
<el-splitter-panel :size="30" :resizable="false">
|
|
||||||
<div class="chat-panel">
|
<div class="chat-panel">
|
||||||
<div v-if="selectedItem" class="chat-container">
|
<div v-if="selectedItem" class="chat-container">
|
||||||
<div class="chat-header">{{ chatUserInfo.nickName || '聊天' }}</div>
|
<div class="chat-header">{{ chatUserInfo.nickName || '聊天' }}</div>
|
||||||
@@ -132,16 +127,13 @@
|
|||||||
v-for="(msg, index) in messagesList"
|
v-for="(msg, index) in messagesList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="message-item"
|
class="message-item"
|
||||||
:class="{ mine: msg.senderId == currentUser.id }"
|
:class="{ mine: msg.senderId == currentUser.id, 'pk-message': msg.type === 'pk' }"
|
||||||
>
|
>
|
||||||
<div class="message-avatar">
|
|
||||||
<img :src="msg.senderId == currentUser.id ? currentUser.headerIcon : chatUserInfo.headerIcon" alt="" />
|
|
||||||
</div>
|
|
||||||
<div class="message-triangle" v-if="msg.type === 'text'"></div>
|
<div class="message-triangle" v-if="msg.type === 'text'"></div>
|
||||||
<div class="message-content">
|
<div class="message-content">
|
||||||
<div v-if="msg.type === 'text'" class="text-message">{{ msg.payload.text }}</div>
|
<div v-if="msg.type === 'text'" class="text-message">{{ msg.payload.text }}</div>
|
||||||
<PictureMessage v-else-if="msg.type === 'image'" :item="msg" />
|
<PictureMessage v-else-if="msg.type === 'image'" :item="msg" />
|
||||||
<MiniPKMessage v-else-if="msg.type === 'pk'" :item="msg" />
|
<MiniPKMessage v-else-if="msg.type === 'pk'" :item="msg" :compact="true" />
|
||||||
<VoiceMessage v-else-if="msg.type === 'audio'" :item="msg.payload.url" :size="msg.payload.duration" />
|
<VoiceMessage v-else-if="msg.type === 'audio'" :item="msg.payload.url" :size="msg.payload.duration" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,10 +164,7 @@
|
|||||||
<span>右方选择主播立即聊天</span>
|
<span>右方选择主播立即聊天</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-splitter-panel>
|
</div>
|
||||||
</el-splitter>
|
|
||||||
</el-splitter-panel>
|
|
||||||
</el-splitter>
|
|
||||||
|
|
||||||
<!-- 隐藏的文件输入 -->
|
<!-- 隐藏的文件输入 -->
|
||||||
<input
|
<input
|
||||||
@@ -388,6 +377,7 @@ async function loadPkList() {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载 PK 列表失败', e)
|
console.error('加载 PK 列表失败', e)
|
||||||
|
noMore.value = true
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -403,12 +393,14 @@ async function handleItemClick(item) {
|
|||||||
|
|
||||||
if (isGoEasyEnabled()) {
|
if (isGoEasyEnabled()) {
|
||||||
// GoEasy 已启用,加载聊天消息
|
// GoEasy 已启用,加载聊天消息
|
||||||
const messages = await goEasyGetMessages({ id: item.senderId, timestamp: null })
|
const messages = await goEasyGetMessages({ id: String(item.senderId), timestamp: null })
|
||||||
messagesList.value = messages || []
|
messagesList.value = messages || []
|
||||||
await nextTick()
|
await nextTick()
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
// 异步卡片内容加载完后再滚一次
|
||||||
|
setTimeout(scrollToBottom, 300)
|
||||||
// 标记消息已读
|
// 标记消息已读
|
||||||
goEasyMessageRead({ id: item.senderId }).catch(() => {})
|
goEasyMessageRead({ id: String(item.senderId) }).catch(() => {})
|
||||||
} else {
|
} else {
|
||||||
messagesList.value = []
|
messagesList.value = []
|
||||||
ElMessage.warning('聊天功能暂时不可用(GoEasy 订阅未续费)')
|
ElMessage.warning('聊天功能暂时不可用(GoEasy 订阅未续费)')
|
||||||
@@ -446,7 +438,7 @@ async function sendMessage() {
|
|||||||
try {
|
try {
|
||||||
const msg = await goEasySendMessage({
|
const msg = await goEasySendMessage({
|
||||||
text: inputText.value,
|
text: inputText.value,
|
||||||
id: selectedItem.value.senderId,
|
id: String(selectedItem.value.senderId),
|
||||||
avatar: currentUser.value.headerIcon,
|
avatar: currentUser.value.headerIcon,
|
||||||
nickname: currentUser.value.nickName
|
nickname: currentUser.value.nickName
|
||||||
})
|
})
|
||||||
@@ -483,7 +475,7 @@ async function handleFileSelect(event) {
|
|||||||
try {
|
try {
|
||||||
const msg = await goEasySendImageMessage({
|
const msg = await goEasySendImageMessage({
|
||||||
imagefile: file,
|
imagefile: file,
|
||||||
id: selectedItem.value.senderId,
|
id: String(selectedItem.value.senderId),
|
||||||
avatar: currentUser.value.headerIcon,
|
avatar: currentUser.value.headerIcon,
|
||||||
nickname: currentUser.value.nickName
|
nickname: currentUser.value.nickName
|
||||||
})
|
})
|
||||||
@@ -540,7 +532,16 @@ async function confirmInvite() {
|
|||||||
const pkRecord = await createPkRecord({
|
const pkRecord = await createPkRecord({
|
||||||
pkIdA: selectedItem.value.id,
|
pkIdA: selectedItem.value.id,
|
||||||
pkIdB: selectedAnchor.value.id,
|
pkIdB: selectedAnchor.value.id,
|
||||||
userId: userId
|
userIdA: selectedItem.value.senderId,
|
||||||
|
userIdB: userId,
|
||||||
|
pkTime: selectedItem.value.pkTime,
|
||||||
|
pkNumber: selectedItem.value.pkNumber,
|
||||||
|
anchorIdA: selectedItem.value.anchorId,
|
||||||
|
anchorIdB: selectedAnchor.value.anchorId,
|
||||||
|
anchorIconA: selectedItem.value.anchorIcon,
|
||||||
|
anchorIconB: selectedAnchor.value.anchorIcon,
|
||||||
|
piIdA: selectedItem.value.id,
|
||||||
|
piIdB: selectedAnchor.value.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 发送 PK 邀请消息
|
// 发送 PK 邀请消息
|
||||||
@@ -548,7 +549,7 @@ async function confirmInvite() {
|
|||||||
msgid: pkRecord.id,
|
msgid: pkRecord.id,
|
||||||
pkIdA: selectedItem.value.id,
|
pkIdA: selectedItem.value.id,
|
||||||
pkIdB: selectedAnchor.value.id,
|
pkIdB: selectedAnchor.value.id,
|
||||||
id: selectedItem.value.senderId,
|
id: String(selectedItem.value.senderId),
|
||||||
avatar: currentUser.value.headerIcon,
|
avatar: currentUser.value.headerIcon,
|
||||||
nickname: currentUser.value.nickName
|
nickname: currentUser.value.nickName
|
||||||
})
|
})
|
||||||
@@ -573,31 +574,9 @@ onMounted(() => {
|
|||||||
console.log('[PkHall] 当前用户数据:', currentUser.value)
|
console.log('[PkHall] 当前用户数据:', currentUser.value)
|
||||||
console.log('[PkHall] 解析的用户 ID:', userId)
|
console.log('[PkHall] 解析的用户 ID:', userId)
|
||||||
|
|
||||||
// 同时加载今日PK和PK大厅数据
|
// 初始加载 PK 大厅数据(通过 loadPkList 统一管理 page/loading/noMore 状态)
|
||||||
if (userId) {
|
if (userId) {
|
||||||
// 加载今日PK
|
loadPkList()
|
||||||
getPkList({
|
|
||||||
status: 0,
|
|
||||||
page: 0,
|
|
||||||
size: 10,
|
|
||||||
userId: userId,
|
|
||||||
condition: { type: 1 }
|
|
||||||
}).then(res => {
|
|
||||||
todayList.value = res || []
|
|
||||||
}).catch(() => {})
|
|
||||||
|
|
||||||
// 加载PK大厅
|
|
||||||
getPkList({
|
|
||||||
status: 0,
|
|
||||||
page: 0,
|
|
||||||
size: 10,
|
|
||||||
userId: userId,
|
|
||||||
condition: { type: 2 }
|
|
||||||
}).then(res => {
|
|
||||||
hallList.value = res || []
|
|
||||||
pkList.value = hallList.value
|
|
||||||
page.value = 1
|
|
||||||
}).catch(() => {})
|
|
||||||
} else {
|
} else {
|
||||||
console.warn('[PkHall] 未找到用户 ID,无法加载数据')
|
console.warn('[PkHall] 未找到用户 ID,无法加载数据')
|
||||||
}
|
}
|
||||||
@@ -644,9 +623,10 @@ onUnmounted(() => {
|
|||||||
border-bottom: 1px solid #f1f5f9; // slate-100
|
border-bottom: 1px solid #f1f5f9; // slate-100
|
||||||
}
|
}
|
||||||
|
|
||||||
.pk-splitter {
|
.content-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: calc(100% - 100px);
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换按钮
|
// 切换按钮
|
||||||
@@ -769,7 +749,8 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
// 列表面板
|
// 列表面板
|
||||||
.list-panel {
|
.list-panel {
|
||||||
height: 100%;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -894,9 +875,12 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
// 聊天面板
|
// 聊天面板
|
||||||
.chat-panel {
|
.chat-panel {
|
||||||
height: 100%;
|
width: 350px;
|
||||||
|
flex-shrink: 0;
|
||||||
border-left: 1px solid #f1f5f9; // slate-100
|
border-left: 1px solid #f1f5f9; // slate-100
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-container {
|
.chat-container {
|
||||||
@@ -929,21 +913,6 @@ onUnmounted(() => {
|
|||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-avatar {
|
|
||||||
width: 45px;
|
|
||||||
height: 45px;
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 0 10px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-avatar img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-triangle {
|
.message-triangle {
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
@@ -962,6 +931,10 @@ onUnmounted(() => {
|
|||||||
max-width: 65%;
|
max-width: 65%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-item.pk-message .message-content {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.text-message {
|
.text-message {
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
|
|||||||
Reference in New Issue
Block a user