融合PK头像头像功能
70
CLAUDE.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 项目概述
|
||||
|
||||
yolo-web-frontend 是一个 Vue 3 Web 前端应用,主要作为 Electron 桌面应用的 UI 层运行,提供 TikTok 自动化管理功能。
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
npm run dev # 启动开发服务器 (端口 5173)
|
||||
npm run build # 生产环境构建 (输出到 dist/)
|
||||
npm run preview # 预览生产构建
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **框架**: Vue 3 + Composition API (`<script setup>`)
|
||||
- **构建工具**: Vite 5
|
||||
- **UI 组件库**: Element Plus
|
||||
- **样式**: Tailwind CSS 3 + Less
|
||||
- **状态管理**: Pinia
|
||||
- **路由**: Vue Router 5 (Hash History 模式)
|
||||
- **国际化**: Vue I18n (中/英文)
|
||||
- **HTTP 请求**: Axios
|
||||
- **实时通信**: GoEasy
|
||||
|
||||
## 架构说明
|
||||
|
||||
### 路径别名
|
||||
`@` 映射到 `src/` 目录
|
||||
|
||||
### 目录结构
|
||||
- `src/pages/` - 顶层页面组件 (LoginPage, ConfigPage, UpdateChecker)
|
||||
- `src/views/` - 业务视图页面
|
||||
- `src/views/tk/` - TikTok 相关主视图
|
||||
- `src/views/tk-mini/` - TikTok Mini 程序视图
|
||||
- `src/layout/` - 布局组件 (WorkbenchLayout, TkLayout)
|
||||
- `src/components/` - 可复用组件
|
||||
- `src/components/tk-mini/` - TikTok Mini 专用组件
|
||||
- `src/stores/` - Pinia 状态存储
|
||||
- `src/api/` - API 请求模块
|
||||
- `src/utils/` - 工具函数
|
||||
- `src/locales/` - 国际化翻译文件
|
||||
|
||||
### 应用入口流程
|
||||
1. `main.js` 初始化 Vue 应用,注册 Pinia、Router、I18n、Element Plus
|
||||
2. `App.vue` 作为根组件,管理页面状态 (login → config → browser)
|
||||
3. 在 Electron 环境下会进行强制更新检查
|
||||
|
||||
### Electron 集成
|
||||
应用通过 `window.electronAPI` 与 Electron 主进程通信,主要接口包括:
|
||||
- 视图管理: `showViews()`, `hideViews()`
|
||||
- 自动化控制: `startTikTokAutomation()`, `stopTikTokAutomation()`
|
||||
- 状态同步: `getRotationStatus()`, `getGreetingStats()`
|
||||
- 登录管理: `logout()`, `checkHealth()`
|
||||
|
||||
使用 `src/utils/electronBridge.js` 中的 `isElectron()` 判断运行环境。
|
||||
|
||||
### 本地存储键
|
||||
- `user_data` - 用户登录信息
|
||||
- `autoDm_runConfig` - 自动化运行配置
|
||||
|
||||
## 代码规范
|
||||
|
||||
- Vue 组件使用 `<script setup>` 语法
|
||||
- 组件文件使用 PascalCase 命名
|
||||
- 工具函数使用 camelCase 命名
|
||||
- 样式优先使用 Tailwind CSS 类,复杂样式使用 Less
|
||||
6
package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "yolo-web-frontend",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"goeasy": "^2.14.9",
|
||||
"vue": "^3.5.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -2100,6 +2101,11 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/goeasy": {
|
||||
"version": "2.14.9",
|
||||
"resolved": "https://registry.npmmirror.com/goeasy/-/goeasy-2.14.9.tgz",
|
||||
"integrity": "sha512-AlHx7PCWfIUWKAvjILJx6AzM1GmquGM1MxxA7gPH6TiR9eTtiieJNk9nIXvVYRm0iTC8AfEnekRY2Clghm9xrA=="
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"goeasy": "^2.14.9",
|
||||
"vue": "^3.5.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
229
src/api/pk-mini.js
Normal file
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* PK Mini 模块 API
|
||||
* 使用独立的 axios 实例,指向 pk.hanxiaokj.cn
|
||||
*/
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 创建独立的 axios 实例
|
||||
const pkAxios = axios.create({
|
||||
baseURL: 'http://192.168.2.22:8086/',
|
||||
// baseURL: 'https://pk.hanxiaokj.cn/',
|
||||
timeout: 60000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器 - 使用主项目的 vvtoken
|
||||
pkAxios.interceptors.request.use((config) => {
|
||||
// 优先使用 vvtoken
|
||||
const vvtoken = localStorage.getItem('token')
|
||||
if (vvtoken) {
|
||||
config.headers['vvtoken'] = vvtoken
|
||||
} else {
|
||||
// 兼容:尝试从 user_data 获取
|
||||
const userData = JSON.parse(localStorage.getItem('user_data') || '{}')
|
||||
if (userData.token) {
|
||||
config.headers['vvtoken'] = userData.tokenValue
|
||||
}
|
||||
}
|
||||
return config
|
||||
}, (error) => {
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
||||
// 响应拦截器
|
||||
pkAxios.interceptors.response.use((response) => {
|
||||
return addPrefixToHeaderIcon(response.data)
|
||||
}, (error) => {
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
||||
// 处理 headerIcon 的前缀和 country 的翻译
|
||||
function addPrefixToHeaderIcon(data) {
|
||||
const PREFIX = 'https://vv-1317974657.cos.ap-shanghai.myqcloud.com/headerIcon/'
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach(item => addPrefixToHeaderIcon(item))
|
||||
return data
|
||||
}
|
||||
|
||||
if (typeof data === 'object' && data !== null) {
|
||||
for (const key in data) {
|
||||
if ((key === 'headerIcon' || key === 'anchorIcon') && data.hasOwnProperty(key)) {
|
||||
const value = data[key]
|
||||
// 如果已经是完整 URL,不再添加前缀
|
||||
if (typeof value === 'string' && (value.startsWith('http://') || value.startsWith('https://'))) {
|
||||
// 已经是完整 URL,不处理
|
||||
} else if (value) {
|
||||
// 只有文件名,添加前缀
|
||||
data[key] = PREFIX + String(value)
|
||||
}
|
||||
} else if (typeof data[key] === 'object' && data[key] !== null) {
|
||||
addPrefixToHeaderIcon(data[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// GET 请求
|
||||
function getAxios({ url, params }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
pkAxios.get(url, { params }).then(res => {
|
||||
if (res.code == 200) {
|
||||
resolve(res.data)
|
||||
} else {
|
||||
ElMessage.error(res.code + ' ' + res.msg)
|
||||
reject(res)
|
||||
}
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// POST 请求
|
||||
function postAxios({ url, data }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
pkAxios.post(url, data).then(res => {
|
||||
if (res.code == 200) {
|
||||
resolve(res.data)
|
||||
} else {
|
||||
ElMessage.error(res.code + ' ' + res.msg)
|
||||
reject(res)
|
||||
}
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// API 接口导出
|
||||
export function getUserInfo(data) {
|
||||
return postAxios({ url: 'user/getUserInfo', data })
|
||||
}
|
||||
|
||||
export function editUserInfo(data) {
|
||||
return postAxios({ url: 'user/updateUserInfo', data })
|
||||
}
|
||||
|
||||
export function getPkList(data) {
|
||||
return postAxios({ url: 'pk/pkList', data })
|
||||
}
|
||||
|
||||
export function getNoticeList(data) {
|
||||
return postAxios({ url: 'systemMessage/list', data })
|
||||
}
|
||||
|
||||
export function getAnchorList(data) {
|
||||
return postAxios({ url: 'anchor/list', data })
|
||||
}
|
||||
|
||||
export function addAnchor(data) {
|
||||
return postAxios({ url: 'anchor/add', data })
|
||||
}
|
||||
|
||||
export function delAnchor(data) {
|
||||
return postAxios({ url: 'anchor/deleteMyAnchor', data })
|
||||
}
|
||||
|
||||
export function editAnchor(data) {
|
||||
return postAxios({ url: 'anchor/updateAnchorInfo', data })
|
||||
}
|
||||
|
||||
export function getAnchorAvatar(data) {
|
||||
const url = 'https://python.yolojt.com/api/' + data.name
|
||||
// 使用原生 fetch 避免全局 axios 拦截器干扰
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.code == 200) {
|
||||
resolve(result.data)
|
||||
} else {
|
||||
reject(result)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function getPkInfo(data) {
|
||||
return postAxios({ url: 'user/queryMyAllPkData', data })
|
||||
}
|
||||
|
||||
export function releasePkInfo(data) {
|
||||
return postAxios({ url: 'pk/addPkData', data })
|
||||
}
|
||||
|
||||
export function editPkInfo(data) {
|
||||
return postAxios({ url: 'pk/updatePkInfoById', data })
|
||||
}
|
||||
|
||||
export function delPkInfo(data) {
|
||||
return postAxios({ url: 'pk/deletePkDataWithId', data })
|
||||
}
|
||||
|
||||
export function topPkInfo(data) {
|
||||
return postAxios({ url: 'user/pinToTop', data })
|
||||
}
|
||||
|
||||
export function cancelTopPkInfo(data) {
|
||||
return postAxios({ url: 'user/cancelPin', data })
|
||||
}
|
||||
|
||||
export function getIntegralDetail(data) {
|
||||
return postAxios({ url: 'user/pointsDetail', data })
|
||||
}
|
||||
|
||||
export function getPkRecord(data) {
|
||||
return postAxios({ url: 'user/handlePkInfo', data })
|
||||
}
|
||||
|
||||
export function signIn(data) {
|
||||
return postAxios({ url: 'user/signIn', data })
|
||||
}
|
||||
|
||||
export function editEmail(data) {
|
||||
return postAxios({ url: 'user/updateUserMail', data })
|
||||
}
|
||||
|
||||
export function getOtp() {
|
||||
return getAxios({ url: 'otp/getotp' })
|
||||
}
|
||||
|
||||
export function getAnchorListById(data) {
|
||||
return postAxios({ url: 'pk/listUninvitedPublishedAnchorsByUserId', data })
|
||||
}
|
||||
|
||||
export function createPkRecord(data) {
|
||||
return postAxios({ url: 'pk/createPkRecord', data })
|
||||
}
|
||||
|
||||
export function queryPkRecord(data) {
|
||||
return postAxios({ url: 'pk/singleRecord', data })
|
||||
}
|
||||
|
||||
export function pkArticleDetail(data) {
|
||||
return postAxios({ url: 'pk/pkInfoDetail', data })
|
||||
}
|
||||
|
||||
export function updatePkRecordStatus(data) {
|
||||
return postAxios({ url: 'pk/updatePkStatus', data })
|
||||
}
|
||||
|
||||
export function queryPkDetail(data) {
|
||||
return postAxios({ url: 'pk/fetchDetailPkDataWithId', data })
|
||||
}
|
||||
|
||||
export function resendEmail(data) {
|
||||
return postAxios({ url: 'user/resendMail', data })
|
||||
}
|
||||
|
||||
export function logout(data) {
|
||||
return postAxios({ url: 'user/logout', data })
|
||||
}
|
||||
BIN
src/assets/pk-mini/AnchorLibrary.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
src/assets/pk-mini/Delete.png
Normal file
|
After Width: | Height: | Size: 629 B |
BIN
src/assets/pk-mini/Editor.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/assets/pk-mini/Email.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/pk-mini/InTotal.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src/assets/pk-mini/Invitation.png
Normal file
|
After Width: | Height: | Size: 1006 B |
BIN
src/assets/pk-mini/InvitationSelected.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/pk-mini/PKInformation.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
src/assets/pk-mini/PKRecord.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src/assets/pk-mini/PKbackground.png
Normal file
|
After Width: | Height: | Size: 268 KiB |
BIN
src/assets/pk-mini/Password.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/pk-mini/Points.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src/assets/pk-mini/PointsList.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
src/assets/pk-mini/Publish.png
Normal file
|
After Width: | Height: | Size: 530 B |
BIN
src/assets/pk-mini/PublishSelected.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/pk-mini/Reset.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/assets/pk-mini/Search.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
src/assets/pk-mini/bg.png
Normal file
|
After Width: | Height: | Size: 544 KiB |
BIN
src/assets/pk-mini/embellish.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/pk-mini/logo.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
src/assets/pk-mini/messageVS.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src/assets/pk-mini/pkMessageleft.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src/assets/pk-mini/pkMessageright.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
src/assets/pk-mini/selectSidebar.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/pk-mini/switchEmail.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
src/assets/pk-mini/switchvx.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
src/assets/pk-mini/topPosition.png
Normal file
|
After Width: | Height: | Size: 659 B |
BIN
src/assets/pk-mini/unpinned.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
255
src/components/pk-mini/PkAppaside.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div class="app-aside">
|
||||
<!-- Logo -->
|
||||
<div class="logo">
|
||||
<div class="logo-icon">PK</div>
|
||||
</div>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<div class="navigation">
|
||||
<div
|
||||
v-for="item in navigationModule"
|
||||
:key="item.id"
|
||||
class="nav-card"
|
||||
:class="{ active: item.id === activeId }"
|
||||
@click="handleClick(item.id)"
|
||||
>
|
||||
<span class="material-icons-round nav-icon">{{ item.icon }}</span>
|
||||
<div class="nav-name">{{ item.name }}</div>
|
||||
<div v-if="item.id === 'message' && unreadCount > 0" class="red-dot">
|
||||
{{ unreadCount > 99 ? '99+' : unreadCount }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户头像 -->
|
||||
<div class="avatar-section">
|
||||
<el-popover placement="right-end" :width="200" trigger="click">
|
||||
<template #reference>
|
||||
<img class="avatar-img" :src="userInfo.headerIcon || defaultAvatar" alt="avatar" />
|
||||
</template>
|
||||
<div class="avatar-menu">
|
||||
<div class="avatar-name">{{ userInfo.nickName || '用户' }}</div>
|
||||
<div class="menu-item" @click="handleSignIn">签到</div>
|
||||
<div class="menu-item" @click="handleSettings">设置</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getMainUserData, setStorage, getStorage } from '@/utils/pk-mini/storage'
|
||||
import { goEasyGetConversations } from '@/utils/pk-mini/goeasy'
|
||||
import { signIn } from '@/api/pk-mini'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const emit = defineEmits(['navigate'])
|
||||
|
||||
const props = defineProps({
|
||||
active: {
|
||||
type: String,
|
||||
default: 'pk'
|
||||
}
|
||||
})
|
||||
|
||||
const defaultAvatar = 'https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/default-avatar.png'
|
||||
const userInfo = ref({})
|
||||
const unreadCount = ref(0)
|
||||
const activeId = ref('pk')
|
||||
|
||||
const navigationModule = [
|
||||
{ id: 'pk', name: 'PK', icon: 'sports_esports' },
|
||||
{ id: 'forum', name: '站内信', icon: 'mail' },
|
||||
{ id: 'message', name: '消息', icon: 'chat' },
|
||||
{ id: 'mine', name: '我的', icon: 'person' }
|
||||
]
|
||||
|
||||
function handleClick(id) {
|
||||
activeId.value = id
|
||||
setStorage('activeId', id)
|
||||
emit('navigate', id)
|
||||
}
|
||||
|
||||
function handleSignIn() {
|
||||
if (!userInfo.value.id) return
|
||||
signIn({ userId: userInfo.value.id }).then(() => {
|
||||
ElMessage.success('签到成功')
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function handleSettings() {
|
||||
ElMessage.info('设置功能开发中')
|
||||
}
|
||||
|
||||
function getChatList() {
|
||||
goEasyGetConversations().then((res) => {
|
||||
if (res?.content?.unreadTotal) {
|
||||
unreadCount.value = res.content.unreadTotal
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 获取用户信息
|
||||
const userData = getMainUserData()
|
||||
if (userData) {
|
||||
userInfo.value = userData
|
||||
}
|
||||
|
||||
// 获取保存的 activeId
|
||||
const savedId = getStorage('activeId')
|
||||
if (savedId) {
|
||||
activeId.value = savedId
|
||||
}
|
||||
|
||||
// 获取未读消息数
|
||||
setTimeout(() => {
|
||||
getChatList()
|
||||
}, 2000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.app-aside {
|
||||
width: 100%;
|
||||
height: 95%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #4fcacd, #03aba8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(79, 202, 205, 0.4);
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.nav-card {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.nav-card:hover {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.nav-card.active {
|
||||
background: white;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 24px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.nav-card.active .nav-icon {
|
||||
color: #03aba8;
|
||||
}
|
||||
|
||||
.nav-name {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.nav-card.active .nav-name {
|
||||
color: #03aba8;
|
||||
}
|
||||
|
||||
.red-dot {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
background-color: #ff4444;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: 2px solid white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.avatar-img:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.avatar-menu {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.avatar-name {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: #03aba8;
|
||||
}
|
||||
</style>
|
||||
427
src/components/pk-mini/chat/MiniPKMessage.vue
Normal file
@@ -0,0 +1,427 @@
|
||||
<template>
|
||||
<div class="chat-message-mini-pk">
|
||||
<!-- 用户A -->
|
||||
<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="gender" :style="{ background: ArticleDetailsA.sex == 1 ? '#59D8DB' : '#F3876F' }">
|
||||
{{ ArticleDetailsA.sex == 1 ? $t('pkMini.man') : $t('pkMini.woman') }}
|
||||
</div>
|
||||
<div class="Country">{{ ArticleDetailsA.country }}</div>
|
||||
</div>
|
||||
<div class="time">
|
||||
{{ $t('pkMini.PKTime') + TimestamptolocalTime(PkIDInfodata.pkTime * 1000) }}
|
||||
</div>
|
||||
<div class="PKinformation">
|
||||
<div class="gold">
|
||||
<img class="gold-img" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/gold.png" alt="" />
|
||||
<div class="sessions-content">
|
||||
{{ $t('pkMini.GoldCoin') }}
|
||||
<div class="gold-num">{{ ArticleDetailsA.coin }}K</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sessions">
|
||||
<img class="sessions-img" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/session.png" alt="" />
|
||||
<div class="sessions-content">
|
||||
{{ $t('pkMini.session') }}
|
||||
<div class="gold-num">{{ PkIDInfodata.pkNumber + $t('pkMini.match') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="Remarks">{{ $t('pkMini.Note') + ArticleDetailsA.remark }}</div>
|
||||
</div>
|
||||
|
||||
<!-- VS -->
|
||||
<div class="messageVS">
|
||||
<img class="messageVS-img" src="@/assets/pk-mini/messageVS.png" alt="" />
|
||||
</div>
|
||||
|
||||
<!-- 用户B -->
|
||||
<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="gender" :style="{ background: ArticleDetailsB.sex == 1 ? '#59D8DB' : '#F3876F' }">
|
||||
{{ ArticleDetailsB.sex == 1 ? $t('pkMini.man') : $t('pkMini.woman') }}
|
||||
</div>
|
||||
<div class="Country">{{ ArticleDetailsB.country }}</div>
|
||||
</div>
|
||||
<div class="time">
|
||||
{{ $t('pkMini.PKTime') + TimestamptolocalTime(PkIDInfodata.pkTime * 1000) }}
|
||||
</div>
|
||||
<div class="PKinformation">
|
||||
<div class="gold">
|
||||
<img class="gold-img" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/gold.png" alt="" />
|
||||
<div class="sessions-content">
|
||||
{{ $t('pkMini.GoldCoin') }}
|
||||
<div class="gold-num">{{ ArticleDetailsB.coin }}K</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sessions">
|
||||
<img class="sessions-img" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/session.png" alt="" />
|
||||
<div class="sessions-content">
|
||||
{{ $t('pkMini.session') }}
|
||||
<div class="gold-num">{{ PkIDInfodata.pkNumber + $t('pkMini.match') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="Remarks">{{ $t('pkMini.Note') + ArticleDetailsB.remark }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<div class="btn" v-if="PkIDInfodata.pkStatus === 0 && ArticleDetailsB.senderId != info.id">
|
||||
<div class="messagebtn-left" @click="agree()">{{ $t('pkMini.agree') }}</div>
|
||||
<div class="messagebtn-right" @click="refuse()">{{ $t('pkMini.Refuse') }}</div>
|
||||
</div>
|
||||
<div v-if="PkIDInfodata.pkStatus === 1" class="messageHint">{{ $t('pkMini.HaveAgreedToTheInvitation') }}</div>
|
||||
<div v-if="PkIDInfodata.pkStatus === 2" class="messageHint">{{ $t('pkMini.HaveRefusedTheInvitation') }}</div>
|
||||
<div v-if="PkIDInfodata.pkStatus === 0 && ArticleDetailsB.senderId == info.id" class="messageHint">
|
||||
{{ $t('pkMini.WaitForTheOtherPartyResponse') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 同意邀请提示弹窗 -->
|
||||
<el-dialog v-model="agreedialog" center :title="$t('pkMini.Hint')" width="400" align-center>
|
||||
<div class="dialog-content">
|
||||
<div class="dialog-content-text">
|
||||
<div>{{ $t('pkMini.AfterASuccessfulInvitationThePKCannotBeModifiedOrDeletedPleaseOperateWithCaution') }}</div>
|
||||
</div>
|
||||
<div class="myanchor-dialog-btn">
|
||||
<div class="remindermyAnchorDialogReset" @click="agreedialog = false">{{ $t('pkMini.Cancel') }}</div>
|
||||
<div class="remindermyAnchorDialogConfirm" @click="agreedialogConfirm">{{ $t('pkMini.Confirm') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 拒绝邀请提示弹窗 -->
|
||||
<el-dialog v-model="refusedialog" center :title="$t('pkMini.Hint')" width="400" align-center>
|
||||
<div class="dialog-content">
|
||||
<div class="dialog-content-text">
|
||||
<div>{{ $t('pkMini.AreYouSureYouWantToDeclineThisInvitation') }}</div>
|
||||
</div>
|
||||
<div class="myanchor-dialog-btn">
|
||||
<div class="remindermyAnchorDialogReset" @click="refusedialog = false">{{ $t('pkMini.Cancel') }}</div>
|
||||
<div class="remindermyAnchorDialogConfirm" @click="refusedialogConfirm">{{ $t('pkMini.Confirm') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { queryPkRecord, pkArticleDetail, updatePkRecordStatus } from '@/api/pk-mini'
|
||||
import { getPromiseStorage } from '@/utils/pk-mini/storage'
|
||||
import { TimestamptolocalTime } from '@/utils/pk-mini/timeConversion'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const info = ref({})
|
||||
const PkIDInfodata = ref({})
|
||||
const ArticleDetailsA = ref({})
|
||||
const ArticleDetailsB = ref({})
|
||||
const agreedialog = ref(false)
|
||||
const refusedialog = ref(false)
|
||||
const newValitem = ref({})
|
||||
|
||||
function agreedialogConfirm() {
|
||||
updatePkRecordStatus({
|
||||
id: newValitem.value.payload.customData.id,
|
||||
pkStatus: 1
|
||||
}).then(() => {
|
||||
ElMessage.success('同意成功')
|
||||
PkIDInfodata.value.pkStatus = 1
|
||||
agreedialog.value = false
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function refusedialogConfirm() {
|
||||
updatePkRecordStatus({
|
||||
id: newValitem.value.payload.customData.id,
|
||||
pkStatus: 2
|
||||
}).then(() => {
|
||||
ElMessage.success('拒绝成功')
|
||||
PkIDInfodata.value.pkStatus = 2
|
||||
refusedialog.value = false
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function agree() {
|
||||
agreedialog.value = true
|
||||
}
|
||||
|
||||
function refuse() {
|
||||
refusedialog.value = true
|
||||
}
|
||||
|
||||
watch(() => props.item, (newVal) => {
|
||||
newValitem.value = newVal
|
||||
queryPkRecord({ id: newVal.payload.customData.id }).then((res) => {
|
||||
PkIDInfodata.value = res
|
||||
}).catch(() => {})
|
||||
|
||||
pkArticleDetail({
|
||||
id: newVal.payload.customData.pkIdA,
|
||||
userId: info.value.id,
|
||||
from: 2
|
||||
}).then((res) => {
|
||||
ArticleDetailsA.value = res
|
||||
}).catch(() => {})
|
||||
|
||||
pkArticleDetail({
|
||||
id: newVal.payload.customData.pkIdB,
|
||||
userId: info.value.id,
|
||||
from: 2
|
||||
}).then((res) => {
|
||||
ArticleDetailsB.value = res
|
||||
}).catch(() => {})
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
getPromiseStorage('user').then((res) => {
|
||||
info.value = res
|
||||
}).catch(() => {})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-message-mini-pk {
|
||||
width: 325px;
|
||||
height: 820px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.messageVS {
|
||||
width: 67px;
|
||||
height: 67px;
|
||||
margin-top: -33.5px;
|
||||
margin-bottom: -33.5px;
|
||||
z-index: 2;
|
||||
}
|
||||
.messageVS-img {
|
||||
width: 67px;
|
||||
height: 67px;
|
||||
}
|
||||
.userA {
|
||||
width: 90%;
|
||||
height: 335px;
|
||||
background-color: #c0e8e8;
|
||||
border-top-left-radius: 20px;
|
||||
border-top-right-radius: 20px;
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.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 {
|
||||
width: 90%;
|
||||
height: 30px;
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.gender {
|
||||
font-size: 14px;
|
||||
padding: 5px 20px;
|
||||
background-color: #999;
|
||||
border-radius: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
.Country {
|
||||
font-size: 14px;
|
||||
padding: 5px 20px;
|
||||
background-color: #e4f9f9;
|
||||
border-radius: 20px;
|
||||
color: #03aba8;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.time {
|
||||
width: 90%;
|
||||
height: 20px;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
}
|
||||
.PKinformation {
|
||||
width: 90%;
|
||||
height: 50px;
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
.gold, .sessions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.gold-img, .sessions-img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.sessions-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
.gold-num {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #000000;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.Remarks {
|
||||
width: 90%;
|
||||
height: 90px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.userB {
|
||||
width: 90%;
|
||||
height: 315px;
|
||||
background-color: #f8e4e0;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
padding-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.btn {
|
||||
margin-top: 20px;
|
||||
width: 90%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.messageHint {
|
||||
margin-top: 20px;
|
||||
width: 90%;
|
||||
height: 50px;
|
||||
font-size: 20px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.messagebtn-left {
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
background-color: #f0836c;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
.messagebtn-left:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.messagebtn-right {
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
background-color: #4fcacd;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
.messagebtn-right:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.dialog-content {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.dialog-content-text {
|
||||
width: 90%;
|
||||
height: 200px;
|
||||
background-color: #c0e8e8;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #03aba8;
|
||||
border: 1px solid #03aba8;
|
||||
}
|
||||
.myanchor-dialog-btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.remindermyAnchorDialogReset {
|
||||
width: 150px;
|
||||
height: 40px;
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
background: linear-gradient(0deg, #e4ffff, #ffffff);
|
||||
color: #03aba8;
|
||||
font-size: 18px;
|
||||
border-radius: 100px;
|
||||
border: 1px solid #4fcacd;
|
||||
cursor: pointer;
|
||||
}
|
||||
.remindermyAnchorDialogConfirm {
|
||||
width: 150px;
|
||||
height: 40px;
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
background: linear-gradient(0deg, #4fcacd, #5fdbde);
|
||||
color: #ffffff;
|
||||
font-size: 18px;
|
||||
border-radius: 100px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
116
src/components/pk-mini/chat/PKMessage.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="pk-message-card" @click="showDetail">
|
||||
<div class="pk-message-header">
|
||||
<img class="pk-icon" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/pk.png" alt="PK" />
|
||||
<span class="pk-title">PK 邀请</span>
|
||||
</div>
|
||||
<div class="pk-message-body">
|
||||
<div class="pk-status" :class="statusClass">{{ statusText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { queryPkRecord } from '@/api/pk-mini'
|
||||
import { getPromiseStorage } from '@/utils/pk-mini/storage'
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const pkInfo = ref({})
|
||||
const info = ref({})
|
||||
|
||||
const statusText = computed(() => {
|
||||
switch (pkInfo.value.pkStatus) {
|
||||
case 0: return '等待响应'
|
||||
case 1: return '已同意'
|
||||
case 2: return '已拒绝'
|
||||
default: return '查看详情'
|
||||
}
|
||||
})
|
||||
|
||||
const statusClass = computed(() => {
|
||||
switch (pkInfo.value.pkStatus) {
|
||||
case 0: return 'pending'
|
||||
case 1: return 'accepted'
|
||||
case 2: return 'rejected'
|
||||
default: return ''
|
||||
}
|
||||
})
|
||||
|
||||
function showDetail() {
|
||||
// 可以扩展为打开详情弹窗
|
||||
}
|
||||
|
||||
watch(() => props.item, (newVal) => {
|
||||
if (newVal?.payload?.customData?.id) {
|
||||
queryPkRecord({ id: newVal.payload.customData.id }).then((res) => {
|
||||
pkInfo.value = res
|
||||
}).catch(() => {})
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
getPromiseStorage('user').then((res) => {
|
||||
info.value = res
|
||||
}).catch(() => {})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pk-message-card {
|
||||
width: 200px;
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, #e4f9f9, #ffffff);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #4fcacd;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.pk-message-card:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 12px rgba(79, 202, 205, 0.3);
|
||||
}
|
||||
.pk-message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.pk-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.pk-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #03aba8;
|
||||
}
|
||||
.pk-message-body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.pk-status {
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.pk-status.pending {
|
||||
background-color: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
.pk-status.accepted {
|
||||
background-color: #e8f5e9;
|
||||
color: #43a047;
|
||||
}
|
||||
.pk-status.rejected {
|
||||
background-color: #ffebee;
|
||||
color: #e53935;
|
||||
}
|
||||
</style>
|
||||
54
src/components/pk-mini/chat/PictureMessage.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="picture-message" @click="dialogVisibleClick">
|
||||
<img class="picture-message-img" :src="item.payload.url" alt="">
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" fullscreen>
|
||||
<div class="dialog-content">
|
||||
<img class="dialog-img" :src="item.payload.url" alt="">
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
|
||||
function dialogVisibleClick() {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.picture-message {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.picture-message-img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.dialog-content {
|
||||
width: 98vw;
|
||||
height: 95vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.dialog-img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
91
src/components/pk-mini/chat/VoiceMessage.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="voice-message" @click="playAudio">
|
||||
<div class="voice-icon">
|
||||
<span class="material-icons-round">{{ isPlaying ? 'pause' : 'play_arrow' }}</span>
|
||||
</div>
|
||||
<div class="voice-duration">{{ size }}s</div>
|
||||
<div class="voice-bar" :style="{ width: barWidth }"></div>
|
||||
</div>
|
||||
<audio ref="audioRef" :src="item" @ended="onAudioEnded" style="display: none;"></audio>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
senderId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const audioRef = ref(null)
|
||||
const isPlaying = ref(false)
|
||||
|
||||
const barWidth = computed(() => {
|
||||
const minWidth = 60
|
||||
const maxWidth = 200
|
||||
const width = Math.min(maxWidth, minWidth + props.size * 5)
|
||||
return width + 'px'
|
||||
})
|
||||
|
||||
function playAudio() {
|
||||
if (audioRef.value) {
|
||||
if (isPlaying.value) {
|
||||
audioRef.value.pause()
|
||||
isPlaying.value = false
|
||||
} else {
|
||||
audioRef.value.play()
|
||||
isPlaying.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onAudioEnded() {
|
||||
isPlaying.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.voice-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
background-color: #e4f9f9;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.voice-message:hover {
|
||||
background-color: #d0f0f0;
|
||||
}
|
||||
.voice-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background-color: #03aba8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.voice-duration {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.voice-bar {
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #03aba8, #4fcacd);
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
442
src/components/pk-mini/mine/AnchorLibrary.vue
Normal file
@@ -0,0 +1,442 @@
|
||||
<template>
|
||||
<!-- 主播库 -->
|
||||
<div class="anchor-library">
|
||||
<el-splitter>
|
||||
<el-splitter-panel :size="75">
|
||||
<div class="demo-panel">
|
||||
<!-- 主播列表 -->
|
||||
<div class="anchor-list" v-if="list.length > 0">
|
||||
<div class="anchor-card" v-for="(item, index) in list" :key="index">
|
||||
<div class="card-content">
|
||||
<div class="card-avatar">
|
||||
<img :src="item.headerIcon" alt="" />
|
||||
</div>
|
||||
<div class="personal-info">
|
||||
<div class="name">{{ item.anchorId }}</div>
|
||||
<div class="info-row">
|
||||
<div class="gender" :class="item.gender == 1 ? 'male' : 'female'">
|
||||
{{ item.gender == 1 ? '男' : '女' }}
|
||||
</div>
|
||||
<div class="country">{{ item.country }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<div class="action-btn" @click="handleEdit(item)">
|
||||
<img :src="iconEditor" alt="编辑" />
|
||||
</div>
|
||||
<div class="action-btn" @click="handleDelete(item)">
|
||||
<img :src="iconDelete" alt="删除" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-tip" v-else>您还没有主播,快去添加吧!</div>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
|
||||
<!-- 右侧添加主播表单 -->
|
||||
<el-splitter-panel :size="25" :resizable="false">
|
||||
<div class="form-panel">
|
||||
<div class="form-title">
|
||||
<img class="title-icon" :src="iconEmbellish" alt="" />
|
||||
<span>{{ isEditing ? '修改主播' : '添加我的主播' }}</span>
|
||||
<img class="title-icon" :src="iconEmbellish" alt="" />
|
||||
</div>
|
||||
|
||||
<div class="form-content">
|
||||
<!-- 主播名称 -->
|
||||
<div class="form-row">
|
||||
<el-input v-model="formData.anchorName" placeholder="请输入主播名称" @blur="handleBlur" />
|
||||
</div>
|
||||
|
||||
<!-- 国家 -->
|
||||
<div class="form-row">
|
||||
<el-select-v2
|
||||
v-model="formData.country"
|
||||
:options="countryOptions"
|
||||
placeholder="请选择国家"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 性别 -->
|
||||
<div class="form-row">
|
||||
<el-select-v2
|
||||
v-model="formData.gender"
|
||||
:options="genderOptions"
|
||||
placeholder="请选择性别"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<div class="confirm-btn" @click="handleSubmit">确认</div>
|
||||
<div class="reset-btn" @click="handleReset">重置</div>
|
||||
<div class="reset-btn" v-if="isEditing" @click="handleCancel">取消</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
|
||||
<!-- 删除确认弹窗 -->
|
||||
<el-dialog v-model="showDeleteDialog" title="提示" width="300" align-center>
|
||||
<span>确认删除此主播?</span>
|
||||
<template #footer>
|
||||
<el-button @click="showDeleteDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmDelete">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getAnchorList, addAnchor, delAnchor, editAnchor, getAnchorAvatar } from '@/api/pk-mini'
|
||||
import { getMainUserData } from '@/utils/pk-mini/storage'
|
||||
import { getCountryNamesArray } from '@/utils/pk-mini/countryUtil'
|
||||
import { ElMessage, ElLoading } from 'element-plus'
|
||||
|
||||
// 导入本地图片
|
||||
import iconEditor from '@/assets/pk-mini/Editor.png'
|
||||
import iconDelete from '@/assets/pk-mini/Delete.png'
|
||||
import iconEmbellish from '@/assets/pk-mini/embellish.png'
|
||||
|
||||
function getUserId(user) {
|
||||
return user?.id || user?.userId || user?.uid || null
|
||||
}
|
||||
|
||||
const currentUser = ref({})
|
||||
const list = ref([])
|
||||
|
||||
// 表单
|
||||
const formData = ref({
|
||||
anchorName: '',
|
||||
country: null,
|
||||
gender: null,
|
||||
anchorIcon: ''
|
||||
})
|
||||
const isEditing = ref(false)
|
||||
const editingId = ref(null)
|
||||
|
||||
// 弹窗
|
||||
const showDeleteDialog = ref(false)
|
||||
const deleteItem = ref(null)
|
||||
|
||||
// 选项
|
||||
const countryOptions = ref([])
|
||||
const genderOptions = [
|
||||
{ value: 1, label: '男' },
|
||||
{ value: 2, label: '女' }
|
||||
]
|
||||
|
||||
// 加载主播列表
|
||||
async function loadAnchorList() {
|
||||
const userId = getUserId(currentUser.value)
|
||||
if (!userId) return
|
||||
|
||||
try {
|
||||
const res = await getAnchorList({ id: userId })
|
||||
list.value = res || []
|
||||
} catch (e) {
|
||||
console.error('加载主播库失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 主播名称失焦查询头像
|
||||
async function handleBlur() {
|
||||
if (!formData.value.anchorName) return
|
||||
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '正在查询主播...',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
|
||||
try {
|
||||
const res = await getAnchorAvatar({ name: formData.value.anchorName })
|
||||
formData.value.anchorIcon = res
|
||||
ElMessage.success('查询成功')
|
||||
} catch (e) {
|
||||
console.error('查询失败', e)
|
||||
} finally {
|
||||
loading.close()
|
||||
}
|
||||
}
|
||||
|
||||
// 提交
|
||||
async function handleSubmit() {
|
||||
const userId = getUserId(currentUser.value)
|
||||
if (!userId) return
|
||||
|
||||
if (!formData.value.anchorName) {
|
||||
ElMessage.error('请输入主播名称')
|
||||
return
|
||||
}
|
||||
if (!formData.value.gender) {
|
||||
ElMessage.error('请选择性别')
|
||||
return
|
||||
}
|
||||
if (!formData.value.country) {
|
||||
ElMessage.error('请选择国家')
|
||||
return
|
||||
}
|
||||
|
||||
const data = {
|
||||
anchorId: formData.value.anchorName,
|
||||
headerIcon: formData.value.anchorIcon,
|
||||
gender: formData.value.gender,
|
||||
country: formData.value.country,
|
||||
createUserId: userId
|
||||
}
|
||||
|
||||
try {
|
||||
if (isEditing.value) {
|
||||
await editAnchor({ ...data, id: editingId.value })
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
await addAnchor(data)
|
||||
ElMessage.success('添加成功')
|
||||
}
|
||||
loadAnchorList()
|
||||
handleReset()
|
||||
} catch (e) {
|
||||
console.error('提交失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
formData.value = { anchorName: '', country: null, gender: null, anchorIcon: '' }
|
||||
isEditing.value = false
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
// 取消
|
||||
function handleCancel() {
|
||||
handleReset()
|
||||
}
|
||||
|
||||
// 编辑
|
||||
function handleEdit(item) {
|
||||
isEditing.value = true
|
||||
editingId.value = item.id
|
||||
formData.value = {
|
||||
anchorName: item.anchorId,
|
||||
country: item.country,
|
||||
gender: item.gender,
|
||||
anchorIcon: item.headerIcon?.split('/').pop() || ''
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
function handleDelete(item) {
|
||||
deleteItem.value = item
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
try {
|
||||
await delAnchor({ id: deleteItem.value.id })
|
||||
ElMessage.success('删除成功')
|
||||
showDeleteDialog.value = false
|
||||
loadAnchorList()
|
||||
handleReset()
|
||||
} catch (e) {
|
||||
console.error('删除失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
countryOptions.value = getCountryNamesArray()
|
||||
currentUser.value = getMainUserData() || {}
|
||||
const userId = getUserId(currentUser.value)
|
||||
if (userId) {
|
||||
loadAnchorList()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.anchor-library {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.demo-panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.anchor-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.anchor-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background: url('https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/PKbackground.png') no-repeat center/cover;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.card-content:hover {
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.card-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.card-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.personal-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.gender {
|
||||
padding: 2px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.gender.male { background: #59d8db; }
|
||||
.gender.female { background: #f3876f; }
|
||||
|
||||
.country {
|
||||
padding: 2px 15px;
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.action-btn:hover { transform: scale(1.2); }
|
||||
|
||||
.action-btn img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
color: #03aba8;
|
||||
}
|
||||
|
||||
.form-panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-left: 1px solid #e0f0f0;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
width: 40px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
width: 80%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
width: 80%;
|
||||
padding: 12px;
|
||||
background: linear-gradient(to top, #4fcacd, #5fdbde);
|
||||
color: white;
|
||||
border-radius: 25px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
margin-top: 80px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.confirm-btn:hover {
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
width: 80%;
|
||||
padding: 12px;
|
||||
background: linear-gradient(to top, #e4ffff, #ffffff);
|
||||
border: 1px solid #4fcacd;
|
||||
color: #03aba8;
|
||||
border-radius: 25px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
</style>
|
||||
478
src/components/pk-mini/mine/PKRecord.vue
Normal file
@@ -0,0 +1,478 @@
|
||||
<template>
|
||||
<!-- 我的PK记录 -->
|
||||
<div class="pk-record">
|
||||
<el-splitter>
|
||||
<el-splitter-panel>
|
||||
<div class="demo-panel">
|
||||
<!-- 选项卡 -->
|
||||
<div class="tab-header">
|
||||
<div
|
||||
class="tab-item"
|
||||
v-for="item in tabOptions"
|
||||
:key="item.value"
|
||||
@click="switchTab(item.value)"
|
||||
:class="{ active: activeTab === item.value }"
|
||||
>
|
||||
<img class="tab-icon" :src="activeTab === item.value ? item.selectedIcon : item.icon" alt="" />
|
||||
<span class="tab-label">{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<div class="record-list" v-if="list.length > 0">
|
||||
<div
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
class="record-item"
|
||||
:class="{ selected: selectedData === item }"
|
||||
@click="selectRecord(item)"
|
||||
>
|
||||
<!-- 左侧信息 -->
|
||||
<div class="record-info">
|
||||
<img class="record-avatar" :src="item.anchorIconA" alt="" />
|
||||
<div class="record-detail">
|
||||
<div class="record-name">{{ item.anchorIdA }}</div>
|
||||
<div class="record-time">PK时间: {{ formatTime(item.pkTime * 1000) }}</div>
|
||||
<div class="record-coins" v-if="item.userACoins != null">
|
||||
<img class="coin-icon" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/gold.png" alt="" />
|
||||
<span>实际金币数: {{ formatCoin(item.userACoins) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VS 图标 -->
|
||||
<div class="vs-icon">
|
||||
<img src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/session.png" alt="" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧信息 -->
|
||||
<div class="record-info right">
|
||||
<div class="record-detail">
|
||||
<div class="record-name">{{ item.anchorIdB }}</div>
|
||||
<div class="record-time">PK时间: {{ formatTime(item.pkTime * 1000) }}</div>
|
||||
<div class="record-coins" v-if="item.userBCoins != null">
|
||||
<img class="coin-icon" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/gold.png" alt="" />
|
||||
<span>实际金币数: {{ formatCoin(item.userBCoins) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<img class="record-avatar" :src="item.anchorIconB" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty-tip" v-else>您还没有PK记录!</div>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
|
||||
<!-- 右侧详情 -->
|
||||
<el-splitter-panel :size="30" :resizable="false">
|
||||
<div class="detail-panel" v-if="selectedData">
|
||||
<!-- 双方头像 -->
|
||||
<div class="detail-avatars">
|
||||
<img class="detail-avatar" :src="selectedData.anchorIconA" alt="" />
|
||||
<img class="detail-avatar" :src="selectedData.anchorIconB" alt="" />
|
||||
</div>
|
||||
|
||||
<!-- 总计 -->
|
||||
<div class="detail-total">
|
||||
<div class="total-card">
|
||||
<span class="total-num">总共:{{ formatCoin(selectedData.userACoins) }}</span>
|
||||
<img class="total-icon" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/session.png" alt="" />
|
||||
<span class="total-num">总共:{{ formatCoin(selectedData.userBCoins) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 每局详情 -->
|
||||
<div class="detail-rounds">
|
||||
<div class="rounds-column left">
|
||||
<div
|
||||
v-for="(item, index) in roundDetails"
|
||||
:key="'a-' + index"
|
||||
class="round-item"
|
||||
:class="item.anchorCoinA > item.anchorCoinB ? 'win' : 'lose'"
|
||||
>
|
||||
第{{ index + 1 }}回: {{ formatCoin(item.anchorCoinA) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounds-column right">
|
||||
<div
|
||||
v-for="(item, index) in roundDetails"
|
||||
:key="'b-' + index"
|
||||
class="round-item"
|
||||
:class="item.anchorCoinB > item.anchorCoinA ? 'win' : 'lose'"
|
||||
>
|
||||
第{{ index + 1 }}回: {{ formatCoin(item.anchorCoinB) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty-detail" v-else>
|
||||
<span>选择右侧的记录,可立即查看详细信息</span>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getPkRecord, queryPkDetail } from '@/api/pk-mini'
|
||||
import { getMainUserData } from '@/utils/pk-mini/storage'
|
||||
import { TimestamptolocalTime } from '@/utils/pk-mini/timeConversion'
|
||||
|
||||
// 导入本地图片
|
||||
import iconPublish from '@/assets/pk-mini/Publish.png'
|
||||
import iconPublishSelected from '@/assets/pk-mini/PublishSelected.png'
|
||||
import iconInvitation from '@/assets/pk-mini/Invitation.png'
|
||||
import iconInvitationSelected from '@/assets/pk-mini/InvitationSelected.png'
|
||||
|
||||
// 获取用户 ID(兼容不同的字段名)
|
||||
function getUserId(user) {
|
||||
return user?.id || user?.userId || user?.uid || null
|
||||
}
|
||||
|
||||
const currentUser = ref({})
|
||||
const activeTab = ref(1)
|
||||
const list = ref([])
|
||||
const postedList = ref([]) // 发布的PK
|
||||
const invitedList = ref([]) // 邀请的PK
|
||||
const selectedData = ref(null)
|
||||
const roundDetails = ref([])
|
||||
|
||||
const tabOptions = [
|
||||
{
|
||||
label: '发布的PK',
|
||||
value: 1,
|
||||
icon: iconPublish,
|
||||
selectedIcon: iconPublishSelected
|
||||
},
|
||||
{
|
||||
label: '邀请的PK',
|
||||
value: 2,
|
||||
icon: iconInvitation,
|
||||
selectedIcon: iconInvitationSelected
|
||||
}
|
||||
]
|
||||
|
||||
const formatTime = TimestamptolocalTime
|
||||
|
||||
function formatCoin(value) {
|
||||
if (value == null) return '0'
|
||||
if (value >= 10000) {
|
||||
return (value / 10000).toFixed(1) + 'w'
|
||||
} else if (value >= 1000) {
|
||||
return (value / 1000).toFixed(1) + 'k'
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function switchTab(value) {
|
||||
activeTab.value = value
|
||||
selectedData.value = null
|
||||
roundDetails.value = []
|
||||
list.value = value === 1 ? postedList.value : invitedList.value
|
||||
}
|
||||
|
||||
async function selectRecord(item) {
|
||||
selectedData.value = item
|
||||
try {
|
||||
const res = await queryPkDetail({ id: item.id })
|
||||
roundDetails.value = res || []
|
||||
} catch (e) {
|
||||
console.error('获取PK详情失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecords(type) {
|
||||
const userId = getUserId(currentUser.value)
|
||||
if (!userId) return
|
||||
try {
|
||||
const res = await getPkRecord({
|
||||
type: type,
|
||||
userId: userId,
|
||||
page: 0,
|
||||
size: 50
|
||||
})
|
||||
if (type === 1) {
|
||||
postedList.value = res || []
|
||||
if (activeTab.value === 1) {
|
||||
list.value = postedList.value
|
||||
}
|
||||
} else {
|
||||
invitedList.value = res || []
|
||||
if (activeTab.value === 2) {
|
||||
list.value = invitedList.value
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载PK记录失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
currentUser.value = getMainUserData() || {}
|
||||
const userId = getUserId(currentUser.value)
|
||||
|
||||
console.log('[PKRecord] 当前用户数据:', currentUser.value)
|
||||
console.log('[PKRecord] 解析的用户 ID:', userId)
|
||||
|
||||
if (userId) {
|
||||
loadRecords(1)
|
||||
loadRecords(2)
|
||||
} else {
|
||||
console.warn('[PKRecord] 未找到用户 ID,无法加载数据')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.pk-record {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.demo-panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
display: flex;
|
||||
padding: 20px 30px;
|
||||
gap: 80px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 15px 30px;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
border-bottom-color: #03aba8;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
font-size: 20px;
|
||||
color: #636363;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-label {
|
||||
color: #03aba8;
|
||||
}
|
||||
|
||||
.record-list {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
background: url('https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/PKbackground.png') no-repeat center/cover;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.record-item:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.record-item.selected {
|
||||
background-color: #fffbfa;
|
||||
border: 1px solid #f4d0c9;
|
||||
}
|
||||
|
||||
.record-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.record-info.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.record-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.record-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.record-info.right .record-detail {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.record-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.record-coins {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.coin-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.vs-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.vs-icon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
color: #03aba8;
|
||||
}
|
||||
|
||||
// 右侧详情
|
||||
.detail-panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-left: 1px solid #03aba82f;
|
||||
}
|
||||
|
||||
.detail-avatars {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detail-avatar {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.detail-total {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.total-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding: 15px;
|
||||
background: linear-gradient(90deg, #e4ffff, #fff, #e4ffff);
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.total-num {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.total-icon {
|
||||
width: 35px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.detail-rounds {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rounds-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
border-radius: 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.rounds-column.left {
|
||||
background: #dffefc;
|
||||
border: 1px solid #86e1e3;
|
||||
}
|
||||
|
||||
.rounds-column.right {
|
||||
background: #fbece9;
|
||||
border: 1px solid #f4d0c9;
|
||||
}
|
||||
|
||||
.round-item {
|
||||
padding: 12px 15px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #03aba8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.round-item.win {
|
||||
background: #d1f6f7;
|
||||
}
|
||||
|
||||
.round-item.lose {
|
||||
background: #f9dfd9;
|
||||
}
|
||||
|
||||
.empty-detail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-left: 1px solid #03aba82f;
|
||||
font-size: 18px;
|
||||
color: #03aba8;
|
||||
}
|
||||
</style>
|
||||
883
src/components/pk-mini/mine/PKmessage.vue
Normal file
@@ -0,0 +1,883 @@
|
||||
<template>
|
||||
<!-- PK信息 -->
|
||||
<div class="pk-message">
|
||||
<el-splitter>
|
||||
<el-splitter-panel :size="70" :min="50">
|
||||
<div class="demo-panel">
|
||||
<!-- PK信息列表 -->
|
||||
<div class="pk-list" v-infinite-scroll="loadMore" v-if="list.length > 0">
|
||||
<div class="pk-card" v-for="(item, index) in list" :key="index">
|
||||
<div class="card-content">
|
||||
<div class="card-avatar">
|
||||
<img :src="item.anchorIcon" alt="" />
|
||||
</div>
|
||||
<div class="personal-info">
|
||||
<div class="name">{{ item.anchorId }}</div>
|
||||
<div class="info-row">
|
||||
<div class="gender" :class="item.sex == 1 ? 'male' : 'female'">
|
||||
{{ item.sex == 1 ? '男' : '女' }}
|
||||
</div>
|
||||
<div class="country">{{ item.country }}</div>
|
||||
<div class="stat-item">
|
||||
<img class="stat-icon" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/gold.png" alt="" />
|
||||
<span>金币: <b>{{ item.coin }}K</b></span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<img class="stat-icon" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/session.png" alt="" />
|
||||
<span>场次: <b>{{ item.pkNumber }}场</b></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pk-time">PK时间(本地时间): {{ formatTime(item.pkTime * 1000) }}</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<div class="action-btn" @click="handleTop(item)">
|
||||
<img v-if="!item.isPin" :src="iconTopPosition" alt="置顶" />
|
||||
<img v-else :src="iconUnpinned" alt="取消置顶" />
|
||||
</div>
|
||||
<div class="action-btn" @click="handleEdit(item)">
|
||||
<img :src="iconEditor" alt="编辑" />
|
||||
</div>
|
||||
<div class="action-btn" @click="handleDelete(item)">
|
||||
<img :src="iconDelete" alt="删除" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-tip" v-else>您还没有PK信息,快去添加吧!</div>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
|
||||
<!-- 右侧发布新PK表单 -->
|
||||
<el-splitter-panel :size="25" :resizable="false">
|
||||
<div class="form-panel">
|
||||
<div class="form-title">
|
||||
<img class="title-icon" :src="iconEmbellish" alt="" />
|
||||
<span>{{ isEditing ? '修改PK信息' : '发布新PK' }}</span>
|
||||
<img class="title-icon" :src="iconEmbellish" alt="" />
|
||||
</div>
|
||||
|
||||
<div class="form-content">
|
||||
<!-- 主播名称 -->
|
||||
<div class="form-row">
|
||||
<el-input v-model="formData.anchorName" placeholder="请输入主播名称" @blur="handleAnchorBlur" />
|
||||
<div class="select-anchor-btn" @click="showAnchorDialog = true">选择我的主播</div>
|
||||
</div>
|
||||
|
||||
<!-- 国家 -->
|
||||
<div class="form-row">
|
||||
<el-select-v2
|
||||
v-model="formData.country"
|
||||
:options="countryOptions"
|
||||
placeholder="请选择国家"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 性别 -->
|
||||
<div class="form-row">
|
||||
<el-select-v2
|
||||
v-model="formData.gender"
|
||||
:options="genderOptions"
|
||||
placeholder="请选择性别"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- PK时间 -->
|
||||
<div class="form-row">
|
||||
<el-date-picker
|
||||
v-model="formData.pkTime"
|
||||
type="datetime"
|
||||
placeholder="请选择PK时间"
|
||||
style="width: 100%"
|
||||
format="YYYY/MM/DD HH:mm"
|
||||
value-format="x"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 金币和场次 -->
|
||||
<div class="form-row two-col">
|
||||
<div class="col">
|
||||
<div class="label">金币数(单位为K)</div>
|
||||
<el-input-number v-model="formData.coin" :min="0" controls-position="right" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="label">场次</div>
|
||||
<el-input-number v-model="formData.pkNumber" :min="1" controls-position="right" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 备注 -->
|
||||
<div class="form-row">
|
||||
<textarea v-model="formData.remark" placeholder="请输入备注(选填)" maxlength="50"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<div class="confirm-btn" @click="handleSubmit">确认</div>
|
||||
<div class="reset-btn" @click="handleReset">重置</div>
|
||||
<div class="reset-btn" v-if="isEditing" @click="handleCancel">取消</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
|
||||
<!-- 删除确认弹窗 -->
|
||||
<el-dialog v-model="showDeleteDialog" title="提示" width="300" align-center>
|
||||
<span>确认删除该主播的PK信息?</span>
|
||||
<template #footer>
|
||||
<el-button @click="showDeleteDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmDelete">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 选择主播弹窗 -->
|
||||
<el-dialog v-model="showAnchorDialog" title="选择我的主播" width="800" align-center>
|
||||
<div class="anchor-dialog-content">
|
||||
<div class="anchor-list">
|
||||
<div
|
||||
v-for="(item, index) in anchorLibrary"
|
||||
:key="index"
|
||||
class="anchor-item"
|
||||
:class="{ selected: selectedAnchor === item }"
|
||||
@click="selectedAnchor = item"
|
||||
>
|
||||
<img class="anchor-avatar" :src="item.headerIcon" alt="" />
|
||||
<div class="anchor-info">
|
||||
<div class="anchor-name">{{ item.anchorId }}</div>
|
||||
<div class="anchor-meta">
|
||||
<span class="gender" :class="item.gender == 1 ? 'male' : 'female'">
|
||||
{{ item.gender == 1 ? '男' : '女' }}
|
||||
</span>
|
||||
<span class="country">{{ item.country }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="anchorLibrary.length === 0" class="empty-anchor">暂无主播</div>
|
||||
</div>
|
||||
<div class="dialog-btns">
|
||||
<div class="reset-btn" @click="showAnchorDialog = false">取消</div>
|
||||
<div class="confirm-btn" @click="confirmSelectAnchor">确认</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 置顶弹窗 -->
|
||||
<el-dialog v-model="showTopDialog" title="置顶" width="500" align-center>
|
||||
<div class="top-dialog-content">
|
||||
<p class="top-tip">置顶后,您的PK信息将在首页优先展示,可以获得更多曝光机会。</p>
|
||||
<el-select-v2
|
||||
v-model="topDuration"
|
||||
:options="topDurationOptions"
|
||||
placeholder="请选择置顶时长"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="dialog-btns">
|
||||
<div class="reset-btn" @click="showTopDialog = false">取消</div>
|
||||
<div class="confirm-btn" @click="confirmTop">确认置顶</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 取消置顶弹窗 -->
|
||||
<el-dialog v-model="showCancelTopDialog" title="取消置顶" width="400" align-center>
|
||||
<div class="cancel-top-content">
|
||||
<p>确认取消置顶?取消后您的PK信息将不再优先展示。</p>
|
||||
<div class="dialog-btns">
|
||||
<div class="reset-btn" @click="showCancelTopDialog = false">取消</div>
|
||||
<div class="confirm-btn" @click="confirmCancelTop">确认取消</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import {
|
||||
getPkInfo,
|
||||
releasePkInfo,
|
||||
editPkInfo,
|
||||
delPkInfo,
|
||||
topPkInfo,
|
||||
cancelTopPkInfo,
|
||||
getAnchorList,
|
||||
getAnchorAvatar
|
||||
} from '@/api/pk-mini'
|
||||
import { getMainUserData } from '@/utils/pk-mini/storage'
|
||||
import { getCountryNamesArray } from '@/utils/pk-mini/countryUtil'
|
||||
import { TimestamptolocalTime } from '@/utils/pk-mini/timeConversion'
|
||||
import { ElMessage, ElLoading } from 'element-plus'
|
||||
|
||||
// 导入本地图片
|
||||
import iconEditor from '@/assets/pk-mini/Editor.png'
|
||||
import iconDelete from '@/assets/pk-mini/Delete.png'
|
||||
import iconEmbellish from '@/assets/pk-mini/embellish.png'
|
||||
import iconTopPosition from '@/assets/pk-mini/topPosition.png'
|
||||
import iconUnpinned from '@/assets/pk-mini/unpinned.png'
|
||||
|
||||
// 获取用户 ID
|
||||
function getUserId(user) {
|
||||
return user?.id || user?.userId || user?.uid || null
|
||||
}
|
||||
|
||||
const currentUser = ref({})
|
||||
const list = ref([])
|
||||
const page = ref(0)
|
||||
const formatTime = TimestamptolocalTime
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
anchorName: '',
|
||||
country: null,
|
||||
gender: null,
|
||||
pkTime: null,
|
||||
coin: null,
|
||||
pkNumber: null,
|
||||
remark: '',
|
||||
anchorIcon: ''
|
||||
})
|
||||
const isEditing = ref(false)
|
||||
const editingId = ref(null)
|
||||
|
||||
// 弹窗状态
|
||||
const showDeleteDialog = ref(false)
|
||||
const showAnchorDialog = ref(false)
|
||||
const showTopDialog = ref(false)
|
||||
const showCancelTopDialog = ref(false)
|
||||
const deleteItem = ref(null)
|
||||
const topItem = ref(null)
|
||||
const selectedAnchor = ref(null)
|
||||
const topDuration = ref(null)
|
||||
const topDurationOptions = ref([])
|
||||
|
||||
// 主播库
|
||||
const anchorLibrary = ref([])
|
||||
|
||||
// 选项
|
||||
const countryOptions = ref([])
|
||||
const genderOptions = [
|
||||
{ value: 1, label: '男' },
|
||||
{ value: 2, label: '女' }
|
||||
]
|
||||
|
||||
// 加载PK信息列表
|
||||
async function loadPkList() {
|
||||
const userId = getUserId(currentUser.value)
|
||||
if (!userId) return
|
||||
|
||||
try {
|
||||
const res = await getPkInfo({
|
||||
userId: userId,
|
||||
page: page.value,
|
||||
size: 10
|
||||
})
|
||||
if (res && res.length > 0) {
|
||||
list.value.push(...res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载PK信息失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
function loadMore() {
|
||||
page.value++
|
||||
loadPkList()
|
||||
}
|
||||
|
||||
// 加载主播库
|
||||
async function loadAnchorLibrary() {
|
||||
const userId = getUserId(currentUser.value)
|
||||
if (!userId) return
|
||||
|
||||
try {
|
||||
const res = await getAnchorList({ id: userId })
|
||||
anchorLibrary.value = res || []
|
||||
} catch (e) {
|
||||
console.error('加载主播库失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 主播名称失焦时查询头像
|
||||
async function handleAnchorBlur() {
|
||||
if (!formData.value.anchorName) return
|
||||
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '正在查询主播...',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
|
||||
try {
|
||||
const res = await getAnchorAvatar({ name: formData.value.anchorName })
|
||||
formData.value.anchorIcon = res
|
||||
ElMessage.success('查询成功')
|
||||
} catch (e) {
|
||||
console.error('查询主播失败', e)
|
||||
} finally {
|
||||
loading.close()
|
||||
}
|
||||
}
|
||||
|
||||
// 选择主播确认
|
||||
function confirmSelectAnchor() {
|
||||
if (!selectedAnchor.value) {
|
||||
ElMessage.warning('请选择一个主播')
|
||||
return
|
||||
}
|
||||
|
||||
formData.value.anchorName = selectedAnchor.value.anchorId
|
||||
formData.value.gender = selectedAnchor.value.gender
|
||||
formData.value.country = selectedAnchor.value.country
|
||||
formData.value.anchorIcon = selectedAnchor.value.headerIcon?.split('/').pop() || ''
|
||||
showAnchorDialog.value = false
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
const userId = getUserId(currentUser.value)
|
||||
if (!userId) return
|
||||
|
||||
// 验证
|
||||
if (!formData.value.anchorName) {
|
||||
ElMessage.error('请输入主播名称')
|
||||
return
|
||||
}
|
||||
if (!formData.value.gender) {
|
||||
ElMessage.error('请选择性别')
|
||||
return
|
||||
}
|
||||
if (!formData.value.pkTime) {
|
||||
ElMessage.error('请选择PK时间')
|
||||
return
|
||||
}
|
||||
if (formData.value.pkTime < Date.now()) {
|
||||
ElMessage.error('PK时间不能早于当前时间')
|
||||
return
|
||||
}
|
||||
if (!formData.value.country) {
|
||||
ElMessage.error('请选择国家')
|
||||
return
|
||||
}
|
||||
if (!formData.value.coin) {
|
||||
ElMessage.error('请输入金币数')
|
||||
return
|
||||
}
|
||||
if (!formData.value.pkNumber) {
|
||||
ElMessage.error('请输入场次')
|
||||
return
|
||||
}
|
||||
|
||||
const data = {
|
||||
anchorId: formData.value.anchorName,
|
||||
pkTime: formData.value.pkTime / 1000,
|
||||
sex: formData.value.gender,
|
||||
country: formData.value.country,
|
||||
coin: formData.value.coin,
|
||||
remark: formData.value.remark || '',
|
||||
status: 0,
|
||||
senderId: userId,
|
||||
anchorIcon: formData.value.anchorIcon,
|
||||
pkNumber: formData.value.pkNumber
|
||||
}
|
||||
|
||||
try {
|
||||
if (isEditing.value) {
|
||||
await editPkInfo({ ...data, id: editingId.value })
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
await releasePkInfo(data)
|
||||
ElMessage.success('发布成功')
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
list.value = []
|
||||
page.value = 0
|
||||
loadPkList()
|
||||
handleReset()
|
||||
} catch (e) {
|
||||
console.error('提交失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
function handleReset() {
|
||||
formData.value = {
|
||||
anchorName: '',
|
||||
country: null,
|
||||
gender: null,
|
||||
pkTime: null,
|
||||
coin: null,
|
||||
pkNumber: null,
|
||||
remark: '',
|
||||
anchorIcon: ''
|
||||
}
|
||||
isEditing.value = false
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
// 取消编辑
|
||||
function handleCancel() {
|
||||
handleReset()
|
||||
}
|
||||
|
||||
// 编辑
|
||||
function handleEdit(item) {
|
||||
isEditing.value = true
|
||||
editingId.value = item.id
|
||||
formData.value = {
|
||||
anchorName: item.anchorId,
|
||||
country: item.country,
|
||||
gender: item.sex,
|
||||
pkTime: item.pkTime * 1000,
|
||||
coin: item.coin,
|
||||
pkNumber: item.pkNumber,
|
||||
remark: item.remark || '',
|
||||
anchorIcon: item.anchorIcon?.split('/').pop() || ''
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
function handleDelete(item) {
|
||||
deleteItem.value = item
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
try {
|
||||
await delPkInfo({ id: deleteItem.value.id })
|
||||
ElMessage.success('删除成功')
|
||||
showDeleteDialog.value = false
|
||||
|
||||
// 刷新列表
|
||||
list.value = []
|
||||
page.value = 0
|
||||
loadPkList()
|
||||
} catch (e) {
|
||||
console.error('删除失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 置顶
|
||||
function handleTop(item) {
|
||||
topItem.value = item
|
||||
if (!item.isPin) {
|
||||
// 计算置顶时长选项
|
||||
const currentTime = Math.floor(Date.now() / 1000)
|
||||
const timeDiff = item.pkTime - currentTime
|
||||
if (timeDiff <= 0) {
|
||||
topDurationOptions.value = [{ value: 0, label: '已过期' }]
|
||||
} else {
|
||||
const hours = Math.ceil(timeDiff / 3600)
|
||||
topDurationOptions.value = Array.from({ length: Math.min(hours, 24) }, (_, i) => ({
|
||||
value: currentTime + (i + 1) * 3600,
|
||||
label: `${i + 1}小时`
|
||||
}))
|
||||
}
|
||||
showTopDialog.value = true
|
||||
} else {
|
||||
showCancelTopDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmTop() {
|
||||
if (!topDuration.value) {
|
||||
ElMessage.warning('请选择置顶时长')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await topPkInfo({
|
||||
articleId: topItem.value.id,
|
||||
pinExpireTime: topDuration.value
|
||||
})
|
||||
ElMessage.success('置顶成功')
|
||||
showTopDialog.value = false
|
||||
|
||||
// 刷新列表
|
||||
list.value = []
|
||||
page.value = 0
|
||||
loadPkList()
|
||||
} catch (e) {
|
||||
console.error('置顶失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmCancelTop() {
|
||||
try {
|
||||
await cancelTopPkInfo({ articleId: topItem.value.id })
|
||||
ElMessage.success('已取消置顶')
|
||||
showCancelTopDialog.value = false
|
||||
|
||||
// 刷新列表
|
||||
list.value = []
|
||||
page.value = 0
|
||||
loadPkList()
|
||||
} catch (e) {
|
||||
console.error('取消置顶失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
countryOptions.value = getCountryNamesArray()
|
||||
currentUser.value = getMainUserData() || {}
|
||||
|
||||
const userId = getUserId(currentUser.value)
|
||||
console.log('[PKmessage] 当前用户:', currentUser.value)
|
||||
console.log('[PKmessage] 用户ID:', userId)
|
||||
|
||||
if (userId) {
|
||||
loadPkList()
|
||||
loadAnchorLibrary()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.pk-message {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.demo-panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pk-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.pk-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background: url('https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/PKbackground.png') no-repeat center/cover;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.card-content:hover {
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.card-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-right: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.personal-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.gender {
|
||||
padding: 2px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.gender.male {
|
||||
background: #59d8db;
|
||||
}
|
||||
|
||||
.gender.female {
|
||||
background: #f3876f;
|
||||
}
|
||||
|
||||
.country {
|
||||
padding: 2px 15px;
|
||||
background: #e4f9f9;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
color: #03aba8;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.pk-time {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.action-btn img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
color: #03aba8;
|
||||
}
|
||||
|
||||
// 右侧表单
|
||||
.form-panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-left: 1px solid #e0f0f0;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
width: 40px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
width: 90%;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-row.two-col {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.col {
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.col .label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.form-row textarea {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
border: 1px solid #4fcacd;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
resize: none;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.select-anchor-btn {
|
||||
margin-top: 10px;
|
||||
padding: 8px 15px;
|
||||
background: linear-gradient(to top, #4fcacd, #5fdbde);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.select-anchor-btn:hover {
|
||||
transform: scale(1.02);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
width: 80%;
|
||||
padding: 12px;
|
||||
background: linear-gradient(to top, #4fcacd, #5fdbde);
|
||||
color: white;
|
||||
border-radius: 25px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.confirm-btn:hover {
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
width: 80%;
|
||||
padding: 12px;
|
||||
background: linear-gradient(to top, #e4ffff, #ffffff);
|
||||
border: 1px solid #4fcacd;
|
||||
color: #03aba8;
|
||||
border-radius: 25px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
// 弹窗内容
|
||||
.anchor-dialog-content {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.anchor-list {
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
background: #e0f4f1;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.anchor-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.anchor-item:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.anchor-item.selected {
|
||||
background: #fffbfa;
|
||||
border: 1px solid #f4d0c9;
|
||||
}
|
||||
|
||||
.anchor-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.anchor-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.anchor-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.anchor-meta {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.empty-anchor {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.dialog-btns {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 30px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.dialog-btns .confirm-btn,
|
||||
.dialog-btns .reset-btn {
|
||||
width: 150px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.top-dialog-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.top-tip {
|
||||
color: #999;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cancel-top-content {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cancel-top-content p {
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
151
src/components/pk-mini/mine/PointsList.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<!-- 积分列表 -->
|
||||
<div class="points-container">
|
||||
<div class="points-header">
|
||||
<img class="points-icon" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/Points.png" alt="" />
|
||||
<div class="points-text">
|
||||
我的积分: <span class="points-num">{{ currentUser.points || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="points-list" v-if="pointsList.length > 0">
|
||||
<div class="points-item" v-for="(item, index) in pointsList" :key="index">
|
||||
<div class="item-content" :class="item.status == 1 ? 'positive' : 'negative'">
|
||||
<div class="event">{{ item.info }}</div>
|
||||
<div class="number">{{ item.number }}</div>
|
||||
<div class="time">{{ formatTime(item.time * 1000) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-tip" v-else>您还没有积分记录!</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getIntegralDetail } from '@/api/pk-mini'
|
||||
import { getMainUserData } from '@/utils/pk-mini/storage'
|
||||
import { TimestamptolocalTime } from '@/utils/pk-mini/timeConversion'
|
||||
|
||||
function getUserId(user) {
|
||||
return user?.id || user?.userId || user?.uid || null
|
||||
}
|
||||
|
||||
const currentUser = ref({})
|
||||
const pointsList = ref([])
|
||||
const page = ref(0)
|
||||
const formatTime = TimestamptolocalTime
|
||||
|
||||
async function loadPointsList() {
|
||||
const userId = getUserId(currentUser.value)
|
||||
if (!userId) return
|
||||
|
||||
try {
|
||||
const res = await getIntegralDetail({
|
||||
page: page.value,
|
||||
size: 30,
|
||||
userId: userId
|
||||
})
|
||||
if (res && res.length > 0) {
|
||||
pointsList.value.push(...res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载积分记录失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
currentUser.value = getMainUserData() || {}
|
||||
const userId = getUserId(currentUser.value)
|
||||
if (userId) {
|
||||
loadPointsList()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.points-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.points-header {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.points-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
margin-right: 18px;
|
||||
}
|
||||
|
||||
.points-text {
|
||||
font-size: 24px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.points-num {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.points-list {
|
||||
width: 100%;
|
||||
height: calc(100% - 70px);
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.points-item {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
width: 90%;
|
||||
height: 60px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-content.positive {
|
||||
background: #dffefc;
|
||||
}
|
||||
|
||||
.item-content.negative {
|
||||
background: #fbece9;
|
||||
}
|
||||
|
||||
.event {
|
||||
color: #03aba8;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.number {
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.time {
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
height: calc(100% - 70px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
color: #03aba8;
|
||||
}
|
||||
</style>
|
||||
@@ -53,6 +53,17 @@
|
||||
主播列表
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- PK 工作台 Tab -->
|
||||
<button @click="currentView = 'pk_mini'"
|
||||
class="w-full aspect-square rounded-xl flex items-center justify-center transition-all duration-200 group relative"
|
||||
:class="currentView === 'pk_mini' ? 'bg-blue-600 text-white shadow-lg shadow-blue-900/30' : 'text-slate-400 hover:bg-slate-800 hover:text-white'">
|
||||
<span class="material-icons-round text-2xl">sports_esports</span>
|
||||
<div
|
||||
class="absolute left-14 bg-slate-800 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-50">
|
||||
PK工作台
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto">
|
||||
@@ -117,15 +128,20 @@
|
||||
|
||||
<!-- Tab 4: Fan Workbench - bigBrother 权限 -->
|
||||
<div v-show="currentView === 'FanWorkbench'" class="absolute inset-0 z-20 bg-gray-50 h-full overflow-hidden p-4">
|
||||
<PermissionMask
|
||||
permission-key="bigBrother"
|
||||
title="大哥工作台未开通"
|
||||
<PermissionMask
|
||||
permission-key="bigBrother"
|
||||
title="大哥工作台未开通"
|
||||
description="您当前没有使用大哥工作台功能的权限"
|
||||
:placeholder-image="placeholderBigBrother"
|
||||
>
|
||||
<FanWorkbench />
|
||||
</PermissionMask>
|
||||
</div>
|
||||
|
||||
<!-- Tab 5: PK Mini 工作台 - 无需权限控制 -->
|
||||
<div v-show="currentView === 'pk_mini'" class="absolute inset-0 z-20 h-full overflow-hidden">
|
||||
<PkMiniWorkbench />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -138,6 +154,7 @@ import TkWorkbenches from '@/views/tk/Workbenches.vue'
|
||||
import HostsList from '@/views/tk/HostsList.vue'
|
||||
import ConfigPage from '@/pages/ConfigPage.vue'
|
||||
import FanWorkbench from '@/views/tk/FanWorkbench.vue'
|
||||
import PkMiniWorkbench from '@/views/pk-mini/PkMiniWorkbench.vue'
|
||||
import PermissionMask from '@/components/PermissionMask.vue'
|
||||
|
||||
// 占位图片 - 无权限时显示的工作台截图
|
||||
|
||||
@@ -179,5 +179,62 @@ export default {
|
||||
SA: "Saudi Arabia", SB: "Solomon Islands", SC: "Seychelles", SD: "Sudan", SE: "Sweden", SG: "Singapore", SI: "Slovenia", SJ: "Svalbard and Jan Mayen", SK: "Slovakia", SL: "Sierra Leone", SM: "San Marino", SN: "Senegal", SO: "Somalia", SR: "Suriname", SS: "South Sudan", ST: "Sao Tome and Principe", SV: "El Salvador", SX: "Sint Maarten", SY: "Syria", SZ: "Eswatini",
|
||||
TC: "Turks and Caicos Islands", TD: "Chad", TF: "French Southern Territories", TG: "Togo", TH: "Thailand", TJ: "Tajikistan", TK: "Tokelau", TL: "Timor-Leste", TM: "Turkmenistan", TN: "Tunisia", TO: "Tonga", TR: "Turkey", TT: "Trinidad and Tobago", TV: "Tuvalu", TW: "Taiwan", TZ: "Tanzania", UA: "Ukraine", UG: "Uganda", UM: "United States Minor Outlying Islands", US: "United States", UY: "Uruguay", UZ: "Uzbekistan",
|
||||
VA: "Vatican City", VC: "Saint Vincent and the Grenadines", VE: "Venezuela", VG: "British Virgin Islands", VI: "U.S. Virgin Islands", VN: "Vietnam", VN1: "Vietnam", VU: "Vanuatu", WS: "Samoa", YE: "Yemen", YT: "Mayotte", ZA: "South Africa", ZM: "Zambia", ZW: "Zimbabwe"
|
||||
},
|
||||
// PK Mini module translations
|
||||
pkMini: {
|
||||
// Navigation
|
||||
pkHall: 'PK Hall',
|
||||
todayPK: 'Today PK',
|
||||
forum: 'In-site Message',
|
||||
message: 'Message',
|
||||
mine: 'Mine',
|
||||
// PK Hall
|
||||
selectCountry: 'Select Country',
|
||||
selectGender: 'Select Gender',
|
||||
male: 'Male',
|
||||
female: 'Female',
|
||||
minCoin: 'Min Coins',
|
||||
maxCoin: 'Max Coins',
|
||||
search: 'Search',
|
||||
reset: 'Reset',
|
||||
pkTime: 'PK Time',
|
||||
goldCoin: 'Gold Coins',
|
||||
session: 'Session',
|
||||
send: 'Send',
|
||||
selectHostToChat: 'Select a host to start chatting',
|
||||
// Mine page
|
||||
anchorLibrary: 'Anchor Library',
|
||||
pkInfo: 'PK Info',
|
||||
pkRecord: 'PK Record',
|
||||
pointsList: 'Points List',
|
||||
myPoints: 'My Points',
|
||||
totalPoints: 'Total Points',
|
||||
addAnchor: 'Add Anchor',
|
||||
deleteAnchor: 'Delete',
|
||||
noAnchor: 'No anchors yet',
|
||||
noPkInfo: 'No PK info yet',
|
||||
noPkRecord: 'No PK record yet',
|
||||
noPoints: 'No points record yet',
|
||||
// Status
|
||||
pending: 'Pending',
|
||||
matched: 'Matched',
|
||||
closed: 'Closed',
|
||||
accepted: 'Accepted',
|
||||
rejected: 'Rejected',
|
||||
waitConfirm: 'Wait Confirm',
|
||||
// Message
|
||||
noConversation: 'No conversations',
|
||||
selectConversation: 'Select a conversation to start chatting',
|
||||
inputMessage: 'Enter message...',
|
||||
sendImage: 'Send Image',
|
||||
// Common
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
delete: 'Delete',
|
||||
edit: 'Edit',
|
||||
save: 'Save',
|
||||
loading: 'Loading...',
|
||||
noData: 'No data',
|
||||
noNotice: 'No in-site messages'
|
||||
}
|
||||
}
|
||||
@@ -159,16 +159,62 @@ export default {
|
||||
},
|
||||
countries: {
|
||||
AD: "安道尔", AE: "阿拉伯联合酋长国", AF: "阿富汗", AG: "安提瓜和巴布达", AI: "安圭拉", AL: "阿尔巴尼亚", AM: "亚美尼亚", AO: "安哥拉", AQ: "南极洲", AR: "阿根廷", AS: "美属萨摩亚", AT: "奥地利", AU: "澳大利亚", AU1: "澳大利亚", AW: "阿鲁巴", AX: "奥兰群岛", AZ: "阿塞拜疆",
|
||||
BA: "波斯尼亚和黑塞哥维那", BB: "巴巴多斯", BD: "孟加拉国", BE: "比利时", BF: "布基纳法索", BG: "保加利亚", BH: "巴林", BI: "布隆迪", BJ: "贝宁", BL: "圣巴泰勒米", BM: "百慕大群岛", BN: "文莱达鲁萨兰国", BO: "玻利维亚", BQ: "博奈尔、圣尤斯特歇斯和萨巴", BR: "巴西", BS: "巴哈马", BT: "不丹", BV: "布韦岛", BW: "博茨瓦纳", BY: "白俄罗斯", BZ: "伯利兹",
|
||||
CA: "加拿大", CA1: "加拿大", CC: "科科斯(基林)群岛", CD: "刚果民主共和国", CF: "中非共和国", CG: "刚果共和国", CH: "瑞士", CI: "科特迪瓦", CK: "库克群岛", CL: "智利", CM: "喀麦隆", CN: "中国", CO: "哥伦比亚", CR: "哥斯达黎加", CU: "古巴", CV: "佛得角", CW: "库拉索", CX: "圣诞岛", CY: "塞浦路斯", CZ: "捷克共和国",
|
||||
DE: "德国", DG: "迪戈加西亚岛", DJ: "吉布提", DK: "丹麦", DM: "多米尼克", DO: "多米尼加共和国", DZ: "阿尔及利亚", EC: "厄瓜多尔", EE: "爱沙尼亚", EG: "埃及", EH: "西撒哈拉", ER: "厄立特里亚", ES: "西班牙", ET: "埃塞俄比亚", FI: "芬兰", FJ: "斐济", FK: "福克兰群岛", FM: "密克罗尼西亚", FO: "法罗群岛", FR: "法国",
|
||||
GA: "加蓬", GB: "英国", GD: "格林纳达", GE: "格鲁吉亚", GF: "法属圭亚那", GG: "根西岛", GH: "加纳", GI: "直布罗陀", GL: "格陵兰", GM: "冈比亚", GN: "几内亚", GP: "瓜德罗普", GQ: "赤道几内亚", GR: "希腊", GS: "南乔治亚和南桑德威奇群岛", GT: "危地马拉", GU: "关岛", GW: "几内亚比绍", GY: "圭亚那",
|
||||
HK: "中国香港特别行政区", HM: "赫德岛和麦克唐纳群岛", HN: "洪都拉斯", HR: "克罗地亚", HT: "海地", HU: "匈牙利", ID: "印度尼西亚", IE: "爱尔兰", IL: "以色列", IM: "马恩岛", IN: "印度", IO: "英属印度洋领地", IQ: "伊拉克", IR: "伊朗", IS: "冰岛", IT: "意大利",
|
||||
JE: "泽西岛", JM: "牙买加", JO: "约旦", JP: "日本", JP1: "日本", KE: "肯尼亚", KG: "吉尔吉斯斯坦", KH: "柬埔寨", KI: "基里巴斯", KM: "科摩罗", KN: "圣基茨和尼维斯", KP: "朝鲜", KR: "韩国", KR1: "韩国", KW: "科威特", KY: "开曼群岛", KZ: "哈萨克斯坦",
|
||||
LA: "老挝", LB: "黎巴嫩", LC: "圣卢西亚", LI: "列支敦士登", LK: "斯里兰卡", LR: "利比里亚", LS: "莱索托", LT: "立陶宛", LU: "卢森堡", LV: "拉脱维亚", LY: "利比亚", MA: "摩洛哥", MC: "摩纳哥", MD: "摩尔多瓦", ME: "黑山", MF: "圣马丁", MG: "马达加斯加", MH: "马绍尔群岛", MK: "北马其顿", ML: "马里", MM: "缅甸", MN: "蒙古", MO: "中国澳门特别行政区", MP: "北马里亚纳群岛", MQ: "马提尼克", MR: "毛里塔尼亚", MS: "蒙特塞拉特", MT: "马耳他", MU: "毛里求斯", MV: "马尔代夫", MW: "马拉维", MX: "墨西哥", MY: "马来西亚", MZ: "莫桑比克",
|
||||
NA: "纳米比亚", NC: "新喀里多尼亚", NE: "尼日尔", NF: "诺福克岛", NG: "尼日利亚", NI: "尼加拉瓜", NL: "荷兰", NO: "挪威", NP: "尼泊尔", NR: "瑙鲁", NU: "纽埃", NZ: "新西兰", OM: "阿曼", PA: "巴拿马", PE: "秘鲁", PF: "法属玻利尼西亚", PG: "巴布亚新几内亚", PH: "菲律宾", PK: "巴基斯坦", PL: "波兰", PM: "圣皮埃尔和密克隆群岛", PN: "皮特凯恩群岛", PR: "波多黎各", PS: "巴勒斯坦", PT: "葡萄牙", PW: "帕劳", PY: "巴拉圭", QA: "卡塔尔", RE: "留尼汪", RO: "罗马尼亚", RS: "塞尔维亚", RU: "俄罗斯", RW: "卢旺达",
|
||||
SA: "沙特阿拉伯", SB: "索罗门群岛", SC: "塞舌尔", SD: "苏丹", SE: "瑞典", SG: "新加坡", SI: "斯洛文尼亚", SJ: "斯瓦尔巴和扬马延", SK: "斯洛伐克", SL: "塞拉利昂", SM: "圣马利诺", SN: "塞内加尔", SO: "索马里", SR: "苏里南", SS: "南苏丹", ST: "圣多美和普林西比", SV: "萨尔瓦多", SX: "荷属圣马丁", SY: "叙利亚", SZ: "斯威士兰",
|
||||
TC: "特克斯和凯科斯群岛", TD: "乍得", TF: "法属南部领地", TG: "多哥", TH: "泰国", TJ: "塔吉克斯坦", TK: "托克劳群岛", TL: "东帝汶", TM: "土库曼斯坦", TN: "突尼斯", TO: "汤加", TR: "土耳其", TT: "特立尼达和多巴哥", TV: "图瓦卢", TW: "台湾", TZ: "坦桑尼亚", UA: "乌克兰", UG: "乌干达", UM: "美国本土外小岛屿", US: "美国", UY: "乌拉圭", UZ: "乌兹别克斯坦",
|
||||
VA: "梵蒂冈", VC: "圣文森特", VE: "委内瑞拉", VG: "英属维尔京群岛", VI: "美属维尔京群岛", VN: "越南", VN1: "越南", VU: "瓦努阿图", WS: "萨摩亚", YE: "也门", YT: "马约特岛", ZA: "南非", ZM: "赞比亚", ZW: "津巴布韦"
|
||||
},
|
||||
// PK Mini 模块翻译
|
||||
pkMini: {
|
||||
// 导航
|
||||
pkHall: 'PK大厅',
|
||||
todayPK: '今日PK',
|
||||
forum: '站内信',
|
||||
message: '消息',
|
||||
mine: '我的',
|
||||
// PK 大厅
|
||||
selectCountry: '选择国家',
|
||||
selectGender: '选择性别',
|
||||
male: '男',
|
||||
female: '女',
|
||||
minCoin: '最小金币数',
|
||||
maxCoin: '最大金币数',
|
||||
search: '搜索',
|
||||
reset: '重置',
|
||||
pkTime: 'PK时间',
|
||||
goldCoin: '金币',
|
||||
session: '场次',
|
||||
send: '发送',
|
||||
selectHostToChat: '选择左侧主播开始聊天',
|
||||
// 我的页面
|
||||
anchorLibrary: '主播库',
|
||||
pkInfo: 'PK信息',
|
||||
pkRecord: 'PK记录',
|
||||
pointsList: '积分列表',
|
||||
myPoints: '我的积分',
|
||||
totalPoints: '总积分',
|
||||
addAnchor: '添加主播',
|
||||
deleteAnchor: '删除',
|
||||
noAnchor: '暂无主播',
|
||||
noPkInfo: '暂无发布的 PK',
|
||||
noPkRecord: '暂无 PK 记录',
|
||||
noPoints: '暂无积分记录',
|
||||
// 状态
|
||||
pending: '等待匹配',
|
||||
matched: '已匹配',
|
||||
closed: '已关闭',
|
||||
accepted: '已同意',
|
||||
rejected: '已拒绝',
|
||||
waitConfirm: '待确认',
|
||||
// 消息
|
||||
noConversation: '暂无会话',
|
||||
selectConversation: '选择左侧会话开始聊天',
|
||||
inputMessage: '输入消息...',
|
||||
sendImage: '发送图片',
|
||||
// 通用
|
||||
confirm: '确认',
|
||||
cancel: '取消',
|
||||
delete: '删除',
|
||||
edit: '编辑',
|
||||
save: '保存',
|
||||
loading: '加载中...',
|
||||
noData: '暂无数据',
|
||||
noNotice: '暂无站内信'
|
||||
}
|
||||
}
|
||||
45
src/stores/pk-mini/notice.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const pkNoticeStore = defineStore('pkNoticeNum', {
|
||||
state: () => {
|
||||
return { data: { num: 0 } }
|
||||
},
|
||||
actions: {
|
||||
increment() {
|
||||
this.data.num++
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const pkTokenStore = defineStore('pkToken', {
|
||||
state: () => {
|
||||
return { token: '' }
|
||||
},
|
||||
actions: {
|
||||
setToken(token) {
|
||||
this.token = token
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const pkUserStore = defineStore('pkUser', {
|
||||
state: () => {
|
||||
return { user: {} }
|
||||
},
|
||||
actions: {
|
||||
setUser(user) {
|
||||
this.user = user
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const pkIMloginStore = defineStore('pkIMlogin', {
|
||||
state: () => {
|
||||
return { IMstate: false }
|
||||
},
|
||||
actions: {
|
||||
setIMstate(state) {
|
||||
this.IMstate = state
|
||||
}
|
||||
}
|
||||
})
|
||||
136
src/utils/pk-mini/countryUtil.js
Normal file
@@ -0,0 +1,136 @@
|
||||
// 国家数据 - 简化版本,直接内嵌数据
|
||||
const zhCountries = {
|
||||
"CN": "中国",
|
||||
"US": "美国",
|
||||
"JP": "日本",
|
||||
"KR": "韩国",
|
||||
"GB": "英国",
|
||||
"DE": "德国",
|
||||
"FR": "法国",
|
||||
"IT": "意大利",
|
||||
"ES": "西班牙",
|
||||
"RU": "俄罗斯",
|
||||
"BR": "巴西",
|
||||
"IN": "印度",
|
||||
"AU": "澳大利亚",
|
||||
"CA": "加拿大",
|
||||
"MX": "墨西哥",
|
||||
"ID": "印度尼西亚",
|
||||
"TH": "泰国",
|
||||
"VN": "越南",
|
||||
"MY": "马来西亚",
|
||||
"SG": "新加坡",
|
||||
"PH": "菲律宾",
|
||||
"TW": "中国台湾",
|
||||
"HK": "中国香港",
|
||||
"AE": "阿联酋",
|
||||
"SA": "沙特阿拉伯",
|
||||
"TR": "土耳其",
|
||||
"EG": "埃及",
|
||||
"ZA": "南非",
|
||||
"NG": "尼日利亚",
|
||||
"AR": "阿根廷",
|
||||
"CL": "智利",
|
||||
"CO": "哥伦比亚",
|
||||
"PE": "秘鲁",
|
||||
"PL": "波兰",
|
||||
"NL": "荷兰",
|
||||
"BE": "比利时",
|
||||
"SE": "瑞典",
|
||||
"NO": "挪威",
|
||||
"DK": "丹麦",
|
||||
"FI": "芬兰",
|
||||
"AT": "奥地利",
|
||||
"CH": "瑞士",
|
||||
"PT": "葡萄牙",
|
||||
"GR": "希腊",
|
||||
"CZ": "捷克",
|
||||
"RO": "罗马尼亚",
|
||||
"HU": "匈牙利",
|
||||
"UA": "乌克兰",
|
||||
"IL": "以色列",
|
||||
"PK": "巴基斯坦",
|
||||
"BD": "孟加拉国",
|
||||
"NZ": "新西兰"
|
||||
}
|
||||
|
||||
const enCountries = {
|
||||
"CN": "China",
|
||||
"US": "United States",
|
||||
"JP": "Japan",
|
||||
"KR": "South Korea",
|
||||
"GB": "United Kingdom",
|
||||
"DE": "Germany",
|
||||
"FR": "France",
|
||||
"IT": "Italy",
|
||||
"ES": "Spain",
|
||||
"RU": "Russia",
|
||||
"BR": "Brazil",
|
||||
"IN": "India",
|
||||
"AU": "Australia",
|
||||
"CA": "Canada",
|
||||
"MX": "Mexico",
|
||||
"ID": "Indonesia",
|
||||
"TH": "Thailand",
|
||||
"VN": "Vietnam",
|
||||
"MY": "Malaysia",
|
||||
"SG": "Singapore",
|
||||
"PH": "Philippines",
|
||||
"TW": "Taiwan",
|
||||
"HK": "Hong Kong",
|
||||
"AE": "UAE",
|
||||
"SA": "Saudi Arabia",
|
||||
"TR": "Turkey",
|
||||
"EG": "Egypt",
|
||||
"ZA": "South Africa",
|
||||
"NG": "Nigeria",
|
||||
"AR": "Argentina",
|
||||
"CL": "Chile",
|
||||
"CO": "Colombia",
|
||||
"PE": "Peru",
|
||||
"PL": "Poland",
|
||||
"NL": "Netherlands",
|
||||
"BE": "Belgium",
|
||||
"SE": "Sweden",
|
||||
"NO": "Norway",
|
||||
"DK": "Denmark",
|
||||
"FI": "Finland",
|
||||
"AT": "Austria",
|
||||
"CH": "Switzerland",
|
||||
"PT": "Portugal",
|
||||
"GR": "Greece",
|
||||
"CZ": "Czech Republic",
|
||||
"RO": "Romania",
|
||||
"HU": "Hungary",
|
||||
"UA": "Ukraine",
|
||||
"IL": "Israel",
|
||||
"PK": "Pakistan",
|
||||
"BD": "Bangladesh",
|
||||
"NZ": "New Zealand"
|
||||
}
|
||||
|
||||
// 创建中文名称到国家代码的映射
|
||||
const zhNameToCode = {}
|
||||
Object.entries(zhCountries).forEach(([code, zhName]) => {
|
||||
zhNameToCode[zhName] = code
|
||||
})
|
||||
|
||||
// 获取国家名称数组,value 固定为中文名称,label 根据当前语言变化
|
||||
export function getCountryNamesArray() {
|
||||
const currentLanguage = localStorage.getItem('language') || 'ZH'
|
||||
|
||||
return Object.entries(zhCountries).map(([code, zhName]) => ({
|
||||
value: zhName,
|
||||
label: currentLanguage === 'ZH' ? zhName : enCountries[code]
|
||||
}))
|
||||
}
|
||||
|
||||
// 根据中文名称获取当前语言环境的翻译
|
||||
export function translateCountryName(zhName) {
|
||||
const currentLanguage = localStorage.getItem('language') || 'ZH'
|
||||
const code = zhNameToCode[zhName]
|
||||
|
||||
if (!code) return zhName
|
||||
|
||||
return currentLanguage === 'ZH' ? zhName : enCountries[code]
|
||||
}
|
||||
212
src/utils/pk-mini/goeasy.js
Normal file
@@ -0,0 +1,212 @@
|
||||
import GoEasy from 'goeasy'
|
||||
import { pkIMloginStore } from '@/stores/pk-mini/notice.js'
|
||||
|
||||
// PK Mini 模块专用 GoEasy 实例
|
||||
let pkGoEasyInstance = null
|
||||
|
||||
// 获取或创建 PK GoEasy 实例
|
||||
export function getPkGoEasy() {
|
||||
if (!pkGoEasyInstance) {
|
||||
pkGoEasyInstance = GoEasy.getInstance({
|
||||
host: 'hangzhou.goeasy.io',
|
||||
appkey: 'PC-a88037e060ed4753bb316ac7239e62d9',
|
||||
modules: ['im']
|
||||
})
|
||||
}
|
||||
return pkGoEasyInstance
|
||||
}
|
||||
|
||||
// 初始化 PK GoEasy (在 PkMiniWorkbench 挂载时调用)
|
||||
export function initPkGoEasy() {
|
||||
return getPkGoEasy()
|
||||
}
|
||||
|
||||
// 链接 IM (登录 IM)
|
||||
export function goEasyLink(data) {
|
||||
const goeasy = getPkGoEasy()
|
||||
const counter = pkIMloginStore()
|
||||
return new Promise((resolve, reject) => {
|
||||
goeasy.connect({
|
||||
id: data.id,
|
||||
data: { avatar: data.avatar, nickname: data.nickname },
|
||||
otp: data.key,
|
||||
onSuccess: function () {
|
||||
console.log('PK IM 连接成功')
|
||||
counter.setIMstate(true)
|
||||
resolve(true)
|
||||
},
|
||||
onFailed: function (error) {
|
||||
console.log('PK IM 连接失败,错误码:' + error.code + ',错误信息:' + error.content)
|
||||
reject(error)
|
||||
},
|
||||
onProgress: function (attempts) {
|
||||
console.log('PK IM 正在重连中...')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 断开 IM
|
||||
export function goEasyDisConnect() {
|
||||
const goeasy = getPkGoEasy()
|
||||
return new Promise((resolve, reject) => {
|
||||
goeasy.disconnect({
|
||||
onSuccess: function () {
|
||||
resolve(true)
|
||||
},
|
||||
onFailed: function (error) {
|
||||
console.log('断开失败, code:' + error.code + ',error:' + error.content)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 获取会话列表
|
||||
export function goEasyGetConversations() {
|
||||
const goeasy = getPkGoEasy()
|
||||
const im = goeasy.im
|
||||
return new Promise((resolve, reject) => {
|
||||
im.latestConversations({
|
||||
onSuccess: function (result) {
|
||||
resolve(result)
|
||||
},
|
||||
onFailed: function (error) {
|
||||
console.log('获取会话列表失败,错误码:' + error.code + ' content:' + error.content)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 获取指定会话的消息列表
|
||||
export function goEasyGetMessages(data) {
|
||||
const goeasy = getPkGoEasy()
|
||||
const im = goeasy.im
|
||||
return new Promise((resolve, reject) => {
|
||||
im.history({
|
||||
id: data.id,
|
||||
type: GoEasy.IM_SCENE.PRIVATE,
|
||||
lastTimestamp: data.timestamp,
|
||||
limit: 30,
|
||||
onSuccess: function (result) {
|
||||
resolve(result.content)
|
||||
},
|
||||
onFailed: function (error) {
|
||||
console.log('获取消息列表失败,错误码:' + error.code + ' content:' + error.content)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 发送文本消息
|
||||
export function goEasySendMessage(data) {
|
||||
const goeasy = getPkGoEasy()
|
||||
const im = goeasy.im
|
||||
let textMessage = im.createTextMessage({
|
||||
text: data.text,
|
||||
to: {
|
||||
type: GoEasy.IM_SCENE.PRIVATE,
|
||||
id: data.id,
|
||||
data: { avatar: data.avatar, nickname: data.nickname }
|
||||
}
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
im.sendMessage({
|
||||
message: textMessage,
|
||||
onSuccess: function () {
|
||||
resolve(textMessage)
|
||||
},
|
||||
onFailed: function (error) {
|
||||
console.log('Failed to send private message,code:' + error.code + ' ,error ' + error.content)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 发送图片消息
|
||||
export function goEasySendImageMessage(data) {
|
||||
const goeasy = getPkGoEasy()
|
||||
const im = goeasy.im
|
||||
const message = im.createImageMessage({
|
||||
file: data.imagefile,
|
||||
to: {
|
||||
type: GoEasy.IM_SCENE.PRIVATE,
|
||||
id: data.id,
|
||||
data: { avatar: data.avatar, nickname: data.nickname }
|
||||
}
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
im.sendMessage({
|
||||
message: message,
|
||||
onSuccess: function () {
|
||||
resolve(message)
|
||||
},
|
||||
onFailed: function (error) {
|
||||
console.log('Failed to send message,code:' + error.code + ',error' + error.content)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 发送 PK 消息
|
||||
export function goEasySendPKMessage(data) {
|
||||
const goeasy = getPkGoEasy()
|
||||
const im = goeasy.im
|
||||
const customData = {
|
||||
id: data.msgid,
|
||||
pkIdA: data.pkIdA,
|
||||
pkIdB: data.pkIdB
|
||||
}
|
||||
|
||||
const order = {
|
||||
customData: customData,
|
||||
link: 'https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/pk.png',
|
||||
text: 'PK'
|
||||
}
|
||||
|
||||
const customMessage = im.createCustomMessage({
|
||||
type: 'pk',
|
||||
payload: order,
|
||||
to: {
|
||||
type: GoEasy.IM_SCENE.PRIVATE,
|
||||
id: data.id,
|
||||
data: { avatar: data.avatar, nickname: data.nickname }
|
||||
}
|
||||
})
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
im.sendMessage({
|
||||
message: customMessage,
|
||||
onSuccess: function () {
|
||||
resolve(customMessage)
|
||||
},
|
||||
onFailed: function (error) {
|
||||
console.log('Failed to send message,code:' + error.code + ',error' + error.content)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 消息已读
|
||||
export function goEasyMessageRead(data) {
|
||||
const goeasy = getPkGoEasy()
|
||||
const im = goeasy.im
|
||||
return new Promise((resolve, reject) => {
|
||||
im.markMessageAsRead({
|
||||
id: data.id,
|
||||
type: GoEasy.IM_SCENE.PRIVATE,
|
||||
onSuccess: function () {
|
||||
resolve(true)
|
||||
},
|
||||
onFailed: function (error) {
|
||||
console.log('标记私聊已读失败', error)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
79
src/utils/pk-mini/storage.js
Normal file
@@ -0,0 +1,79 @@
|
||||
// PK Mini 模块专用 Storage 工具
|
||||
// 使用 pk_mini_ 前缀避免与主项目冲突
|
||||
|
||||
const PREFIX = 'pk_mini_'
|
||||
|
||||
export function setStorage(key, value) {
|
||||
localStorage.setItem(PREFIX + key, JSON.stringify(value))
|
||||
}
|
||||
|
||||
export function getStorage(key) {
|
||||
const value = localStorage.getItem(PREFIX + key)
|
||||
if (value) {
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (e) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function getPromiseStorage(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const value = getStorage(key)
|
||||
if (value) {
|
||||
resolve(value)
|
||||
} else {
|
||||
reject(new Error('Key not found: ' + key))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function clearStorage(key) {
|
||||
localStorage.removeItem(PREFIX + key)
|
||||
}
|
||||
|
||||
export function getPromiseSessionStorage(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const value = sessionStorage.getItem(PREFIX + key)
|
||||
if (value) {
|
||||
try {
|
||||
resolve(JSON.parse(value))
|
||||
} catch (e) {
|
||||
resolve(value)
|
||||
}
|
||||
} else {
|
||||
reject(new Error('Key not found: ' + key))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function setSessionStorage(key, value) {
|
||||
sessionStorage.setItem(PREFIX + key, JSON.stringify(value))
|
||||
}
|
||||
|
||||
// 获取主项目的用户数据(用于获取 token 和用户信息)
|
||||
// 主项目使用 'user' 键存储用户信息
|
||||
export function getMainUserData() {
|
||||
// 优先从 'user' 获取(主项目当前使用的键)
|
||||
let userData = localStorage.getItem('user')
|
||||
if (userData) {
|
||||
try {
|
||||
return JSON.parse(userData)
|
||||
} catch (e) {
|
||||
// 解析失败,继续尝试其他键
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容:尝试从 'user_data' 获取(旧键名)
|
||||
userData = localStorage.getItem('user_data')
|
||||
if (userData) {
|
||||
try {
|
||||
return JSON.parse(userData)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
13
src/utils/pk-mini/timeConversion.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// 时间戳转换为本地时间,格式为 YYYY/MM/DD hh:mm
|
||||
export function TimestamptolocalTime(date) {
|
||||
if (!date || isNaN(date)) return ''
|
||||
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const hours = String(d.getHours()).padStart(2, '0')
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||||
|
||||
return `${year}/${month}/${day} ${hours}:${minutes}`
|
||||
}
|
||||
27
src/utils/pk-mini/timeDisplay.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// 记录上次调用时间
|
||||
let lastTimestamp = null
|
||||
|
||||
/**
|
||||
* 比较时间戳是否超过 5 分钟
|
||||
* @param {number} timestamp - 要比较的时间戳(毫秒)
|
||||
* @returns {boolean} - 是否超过 5 分钟或第一次调用
|
||||
*/
|
||||
export function timeDisplay(timestamp) {
|
||||
// 第一次调用直接返回 true
|
||||
if (lastTimestamp === null) {
|
||||
lastTimestamp = timestamp
|
||||
return true
|
||||
}
|
||||
|
||||
// 计算时间差(毫秒)
|
||||
const timeDiff = Math.abs(timestamp - lastTimestamp)
|
||||
lastTimestamp = timestamp
|
||||
|
||||
// 5 分钟 = 300,000 毫秒
|
||||
return timeDiff > 300000
|
||||
}
|
||||
|
||||
// 重置时间戳(在组件卸载时调用)
|
||||
export function resetTimeDisplay() {
|
||||
lastTimestamp = null
|
||||
}
|
||||
94
src/views/pk-mini/Forum.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<!-- 站内信页面 -->
|
||||
<div class="forum">
|
||||
<div class="forum-list">
|
||||
<el-card
|
||||
v-for="(item, index) in noticeList"
|
||||
:key="index"
|
||||
class="notice-card"
|
||||
>
|
||||
<template #header>
|
||||
<div class="card-header">{{ item.title }}</div>
|
||||
</template>
|
||||
<div class="card-body">{{ item.content }}</div>
|
||||
<template #footer>
|
||||
<div class="card-footer">{{ item.time }}</div>
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<div v-if="noticeList.length === 0" class="empty-tip">暂无站内信</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getNoticeList } from '@/api/pk-mini'
|
||||
|
||||
const noticeList = ref([])
|
||||
const page = ref(0)
|
||||
|
||||
async function loadNotices() {
|
||||
try {
|
||||
const res = await getNoticeList({ page: page.value, size: 20 })
|
||||
noticeList.value = res || []
|
||||
} catch (e) {
|
||||
console.error('加载站内信失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadNotices()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.forum {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.forum-list {
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.notice-card {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #4fcacd;
|
||||
background: linear-gradient(180deg, #e4ffff, #ffffff);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
346
src/views/pk-mini/Message.vue
Normal file
@@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<!-- 消息页面 -->
|
||||
<div class="message-page">
|
||||
<el-splitter class="message-splitter">
|
||||
<!-- 会话列表 -->
|
||||
<el-splitter-panel :size="25" :min="20" :max="35">
|
||||
<div class="conversation-list">
|
||||
<div
|
||||
v-for="(item, index) in chatList"
|
||||
:key="index"
|
||||
class="conversation-item"
|
||||
:class="{ active: selectedChat === item }"
|
||||
@click="selectChat(item)"
|
||||
>
|
||||
<el-badge :value="item.unread > 0 ? item.unread : ''" :max="99">
|
||||
<div class="conv-avatar">
|
||||
<img :src="item.data?.avatar || defaultAvatar" alt="" />
|
||||
</div>
|
||||
</el-badge>
|
||||
<div class="conv-info">
|
||||
<div class="conv-header">
|
||||
<span class="conv-name">{{ item.data?.nickname || '用户' }}</span>
|
||||
<span class="conv-time">{{ formatTime(item.lastMessage?.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="conv-preview">{{ item.lastMessage?.payload?.text || '' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="chatList.length === 0" class="empty-tip">暂无会话</div>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<el-splitter-panel>
|
||||
<div v-if="selectedChat" class="chat-container">
|
||||
<div class="chat-messages" ref="chatMessagesRef">
|
||||
<div
|
||||
v-for="(msg, index) in messagesList"
|
||||
:key="index"
|
||||
class="message-item"
|
||||
: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 v-if="msg.type === 'text'" class="text-message">{{ msg.payload.text }}</div>
|
||||
<PictureMessage v-else-if="msg.type === 'image'" :item="msg" />
|
||||
<PKMessage v-else-if="msg.type === 'pk'" :item="msg" />
|
||||
<VoiceMessage v-else-if="msg.type === 'audio'" :item="msg.payload.url" :size="msg.payload.duration" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-input-area">
|
||||
<div class="input-toolbar">
|
||||
<div class="toolbar-btn" @click="handleSendImage">
|
||||
<span class="material-icons-round">image</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<textarea
|
||||
v-model="inputText"
|
||||
placeholder="输入消息..."
|
||||
@keydown.enter.prevent="sendMessage"
|
||||
></textarea>
|
||||
<el-button type="primary" @click="sendMessage">发送</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="chat-placeholder">
|
||||
<span class="material-icons-round placeholder-icon">chat_bubble_outline</span>
|
||||
<p>选择左侧会话开始聊天</p>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
|
||||
<!-- 隐藏的文件输入 -->
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style="display: none"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getMainUserData } from '@/utils/pk-mini/storage'
|
||||
import { TimestamptolocalTime } from '@/utils/pk-mini/timeConversion'
|
||||
// GoEasy 暂时禁用(订阅未续费)
|
||||
// import {
|
||||
// goEasyGetConversations,
|
||||
// goEasyGetMessages,
|
||||
// goEasySendMessage,
|
||||
// goEasySendImageMessage,
|
||||
// goEasyMessageRead,
|
||||
// getPkGoEasy
|
||||
// } from '@/utils/pk-mini/goeasy'
|
||||
// import GoEasy from 'goeasy'
|
||||
import PictureMessage from '@/components/pk-mini/chat/PictureMessage.vue'
|
||||
import PKMessage from '@/components/pk-mini/chat/PKMessage.vue'
|
||||
import VoiceMessage from '@/components/pk-mini/chat/VoiceMessage.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const defaultAvatar = 'https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/default-avatar.png'
|
||||
const chatList = ref([])
|
||||
const selectedChat = ref(null)
|
||||
const messagesList = ref([])
|
||||
const inputText = ref('')
|
||||
const currentUser = ref({})
|
||||
const chatMessagesRef = ref(null)
|
||||
const fileInputRef = ref(null)
|
||||
|
||||
const formatTime = TimestamptolocalTime
|
||||
|
||||
async function loadConversations() {
|
||||
// GoEasy 暂时禁用
|
||||
ElMessage.warning('消息功能暂时不可用(GoEasy 订阅未续费)')
|
||||
}
|
||||
|
||||
async function selectChat(item) {
|
||||
// GoEasy 暂时禁用
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
if (chatMessagesRef.value) {
|
||||
chatMessagesRef.value.scrollTop = chatMessagesRef.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
// GoEasy 暂时禁用
|
||||
ElMessage.warning('消息功能暂时不可用(GoEasy 订阅未续费)')
|
||||
}
|
||||
|
||||
function handleSendImage() {
|
||||
// GoEasy 暂时禁用
|
||||
ElMessage.warning('消息功能暂时不可用(GoEasy 订阅未续费)')
|
||||
}
|
||||
|
||||
async function handleFileSelect(event) {
|
||||
event.target.value = ''
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
currentUser.value = getMainUserData() || {}
|
||||
// GoEasy 暂时禁用,不加载会话
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.message-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message-splitter {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.conversation-list {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.conversation-item:hover, .conversation-item.active {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.conv-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.conv-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.conv-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.conv-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.conv-name {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.conv-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.conv-preview {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.message-item.mine {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin: 0 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.text-message {
|
||||
padding: 12px 16px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message-item.mine .text-message {
|
||||
background: #7bbd0093;
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.input-toolbar {
|
||||
display: flex;
|
||||
padding: 10px 15px;
|
||||
background: #e4f9f9;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.toolbar-btn .material-icons-round {
|
||||
color: #03aba8;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
display: flex;
|
||||
padding: 10px 15px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.input-box textarea {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
height: 50px;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.chat-placeholder {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #03aba8;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 80px;
|
||||
opacity: 0.3;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
142
src/views/pk-mini/Mine.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<!-- 我的页面 -->
|
||||
<div class="mine-page">
|
||||
<!-- 顶部选项卡 -->
|
||||
<div class="tab-bar">
|
||||
<div
|
||||
v-for="item in tabs"
|
||||
:key="item.value"
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === item.value }"
|
||||
@click="activeTab = item.value"
|
||||
>
|
||||
<img class="tab-icon" :class="item.iconClass" :src="item.icon" alt="" />
|
||||
<span class="tab-label">{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容区 -->
|
||||
<div class="tab-content">
|
||||
<AnchorLibrary v-if="activeTab === 1" />
|
||||
<PKmessage v-if="activeTab === 2" />
|
||||
<PKRecord v-if="activeTab === 3" />
|
||||
<PointsList v-if="activeTab === 4" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import AnchorLibrary from '@/components/pk-mini/mine/AnchorLibrary.vue'
|
||||
import PKmessage from '@/components/pk-mini/mine/PKmessage.vue'
|
||||
import PKRecord from '@/components/pk-mini/mine/PKRecord.vue'
|
||||
import PointsList from '@/components/pk-mini/mine/PointsList.vue'
|
||||
|
||||
// 导入本地图片
|
||||
import iconAnchorLibrary from '@/assets/pk-mini/AnchorLibrary.png'
|
||||
import iconPKInformation from '@/assets/pk-mini/PKInformation.png'
|
||||
import iconPKRecord from '@/assets/pk-mini/PKRecord.png'
|
||||
import iconPointsList from '@/assets/pk-mini/PointsList.png'
|
||||
|
||||
const activeTab = ref(1)
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
value: 1,
|
||||
label: '主播库',
|
||||
icon: iconAnchorLibrary
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: 'PK信息',
|
||||
icon: iconPKInformation,
|
||||
iconClass: 'pk-info-icon'
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
label: '我的PK记录',
|
||||
icon: iconPKRecord
|
||||
},
|
||||
{
|
||||
value: 4,
|
||||
label: '积分列表',
|
||||
icon: iconPointsList,
|
||||
iconClass: 'points-icon'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.mine-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
width: 100%;
|
||||
height: 110px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
height: 90px;
|
||||
margin: 0 10px;
|
||||
border-radius: 24px;
|
||||
background-color: #cef1eb;
|
||||
border: 2px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab-item:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
background: linear-gradient(90deg, #e4ffff, #ffffff);
|
||||
border-color: #03aba8;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.tab-icon.pk-info-icon {
|
||||
width: 50px;
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
.tab-icon.points-icon {
|
||||
width: 65px;
|
||||
height: 69px;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
font-size: 24px;
|
||||
color: #636363;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-label {
|
||||
color: #03aba8;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
height: calc(100% - 110px);
|
||||
background-color: #ffffff;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
865
src/views/pk-mini/PkHall.vue
Normal file
@@ -0,0 +1,865 @@
|
||||
<template>
|
||||
<!-- PK 大厅页面 -->
|
||||
<div class="pk-hall">
|
||||
<!-- 顶部筛选面板 - 固定高度 -->
|
||||
<div class="control-panel">
|
||||
<!-- PK大厅/今日PK 切换 -->
|
||||
<div class="switch-box">
|
||||
<div class="switch-text" @click="switchMode">PK大厅</div>
|
||||
<div class="switch-text" @click="switchMode">今日PK</div>
|
||||
<div class="switch-slider" :class="{ 'slide-right': !isHallMode }">
|
||||
{{ isHallMode ? 'PK大厅' : '今日PK' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 国家和性别选择 -->
|
||||
<div class="select-box">
|
||||
<el-select-v2
|
||||
v-model="countryValue"
|
||||
:options="countryOptions"
|
||||
placeholder="国家"
|
||||
filterable
|
||||
clearable
|
||||
class="filter-select"
|
||||
/>
|
||||
<el-select-v2
|
||||
v-model="genderValue"
|
||||
:options="genderOptions"
|
||||
placeholder="性别"
|
||||
clearable
|
||||
class="filter-select"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 金币数量 -->
|
||||
<div class="coin-box">
|
||||
<div class="coin-item">
|
||||
<div class="coin-label">最小金币数(单位为K)</div>
|
||||
<el-input-number v-model="minCoin" :min="0" controls-position="right" />
|
||||
</div>
|
||||
<div class="coin-item">
|
||||
<div class="coin-label">最大金币数(单位为K)</div>
|
||||
<el-input-number v-model="maxCoin" :min="0" controls-position="right" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间选择 (仅PK大厅模式) -->
|
||||
<div class="time-box" :class="{ 'is-hidden': !isHallMode }">
|
||||
|
||||
<el-date-picker
|
||||
v-model="timeRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="最小PK时间"
|
||||
end-placeholder="最大PK时间"
|
||||
format="YYYY/MM/DD HH:mm"
|
||||
value-format="x"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和重置按钮 -->
|
||||
<div class="btn-box">
|
||||
<div class="search-btn" @click="handleSearch">
|
||||
<img class="btn-icon" :src="iconSearch" alt="" />
|
||||
<span>搜索</span>
|
||||
</div>
|
||||
<div class="reset-btn" @click="handleReset">
|
||||
<img class="btn-icon" :src="iconReset" alt="" />
|
||||
<span>重置</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表和聊天区域 -->
|
||||
<el-splitter class="pk-splitter">
|
||||
<el-splitter-panel>
|
||||
<el-splitter>
|
||||
<!-- 列表面板 -->
|
||||
<el-splitter-panel :size="70" :resizable="false">
|
||||
<div class="list-panel">
|
||||
<div
|
||||
v-infinite-scroll="loadMore"
|
||||
:infinite-scroll-distance="100"
|
||||
class="pk-list"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in pkList"
|
||||
:key="index"
|
||||
class="pk-card"
|
||||
:class="{ selected: selectedItem === item }"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<div class="pk-avatar">
|
||||
<img :src="item.anchorIcon" alt="" />
|
||||
</div>
|
||||
<div class="pk-info">
|
||||
<!-- 个人信息 -->
|
||||
<div class="pk-personal">
|
||||
<span class="pk-name">{{ item.disPlayId }}</span>
|
||||
<span class="pk-gender" :class="item.sex === 1 ? 'male' : 'female'">
|
||||
{{ item.sex === 1 ? '男' : '女' }}
|
||||
</span>
|
||||
<span class="pk-country">{{ item.country }}</span>
|
||||
</div>
|
||||
<!-- 时间 -->
|
||||
<div class="pk-time">PK时间(本地时间): {{ formatTime(item.pkTime * 1000) }}</div>
|
||||
<!-- PK信息 -->
|
||||
<div class="pk-stats">
|
||||
<img class="stat-icon" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/gold.png" alt="" />
|
||||
<span>金币: {{ item.coin }}K</span>
|
||||
<img class="stat-icon session-icon" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/session.png" alt="" />
|
||||
<span>场次: {{ item.pkNumber }}场</span>
|
||||
</div>
|
||||
<!-- 备注 -->
|
||||
<div class="pk-remark">{{ item.remark }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="pkList.length === 0" class="empty-tip">暂无数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
|
||||
<!-- 聊天面板 -->
|
||||
<el-splitter-panel :size="30" :resizable="false">
|
||||
<div class="chat-panel">
|
||||
<div v-if="selectedItem" class="chat-container">
|
||||
<div class="chat-header">{{ chatUserInfo.nickName || '聊天' }}</div>
|
||||
<div class="chat-messages" ref="chatMessagesRef">
|
||||
<div
|
||||
v-for="(msg, index) in messagesList"
|
||||
:key="index"
|
||||
class="message-item"
|
||||
:class="{ mine: msg.senderId == currentUser.id }"
|
||||
>
|
||||
<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-content">
|
||||
<div v-if="msg.type === 'text'" class="text-message">{{ msg.payload.text }}</div>
|
||||
<PictureMessage v-else-if="msg.type === 'image'" :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" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 聊天输入区 -->
|
||||
<div class="chat-input-area">
|
||||
<div class="input-controls">
|
||||
<div class="control-btns">
|
||||
<div class="control-btn" @click="handleSendImage">
|
||||
<img src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/Album.png" alt="" />
|
||||
</div>
|
||||
<div class="control-btn" @click="handleInvite">
|
||||
<img src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/chat_invite.png" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="send-btn" @click="sendMessage">发送</div>
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<textarea
|
||||
v-model="inputText"
|
||||
placeholder="输入消息..."
|
||||
@keydown.enter.prevent="sendMessage"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="chat-placeholder">
|
||||
<span>右方选择主播立即聊天</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
|
||||
<!-- 隐藏的文件输入 -->
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style="display: none"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { getPkList, getUserInfo } from '@/api/pk-mini'
|
||||
import { getCountryNamesArray } from '@/utils/pk-mini/countryUtil'
|
||||
import { TimestamptolocalTime } from '@/utils/pk-mini/timeConversion'
|
||||
import { getMainUserData } from '@/utils/pk-mini/storage'
|
||||
// GoEasy 暂时禁用(订阅未续费)
|
||||
// import {
|
||||
// goEasyGetMessages,
|
||||
// goEasySendMessage,
|
||||
// goEasySendImageMessage,
|
||||
// goEasyMessageRead,
|
||||
// getPkGoEasy
|
||||
// } from '@/utils/pk-mini/goeasy'
|
||||
// import GoEasy from 'goeasy'
|
||||
import PictureMessage from '@/components/pk-mini/chat/PictureMessage.vue'
|
||||
import MiniPKMessage from '@/components/pk-mini/chat/MiniPKMessage.vue'
|
||||
import VoiceMessage from '@/components/pk-mini/chat/VoiceMessage.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 导入本地图片
|
||||
import iconSearch from '@/assets/pk-mini/Search.png'
|
||||
import iconReset from '@/assets/pk-mini/Reset.png'
|
||||
|
||||
// 获取用户 ID(兼容不同的字段名)
|
||||
function getUserId(user) {
|
||||
return user?.id || user?.userId || user?.uid || null
|
||||
}
|
||||
|
||||
// 状态
|
||||
const isHallMode = ref(true) // true: PK大厅, false: 今日PK
|
||||
const countryValue = ref(null)
|
||||
const genderValue = ref(null)
|
||||
const minCoin = ref(null)
|
||||
const maxCoin = ref(null)
|
||||
const timeRange = ref(null)
|
||||
const pkList = ref([])
|
||||
const hallList = ref([])
|
||||
const todayList = ref([])
|
||||
const page = ref(0)
|
||||
const selectedItem = ref(null)
|
||||
const chatUserInfo = ref({})
|
||||
const messagesList = ref([])
|
||||
const inputText = ref('')
|
||||
const currentUser = ref({})
|
||||
const chatMessagesRef = ref(null)
|
||||
const fileInputRef = ref(null)
|
||||
|
||||
const countryOptions = ref([])
|
||||
const genderOptions = [
|
||||
{ value: 1, label: '男' },
|
||||
{ value: 2, label: '女' }
|
||||
]
|
||||
|
||||
const formatTime = TimestamptolocalTime
|
||||
|
||||
// 切换 PK大厅/今日PK
|
||||
function switchMode() {
|
||||
isHallMode.value = !isHallMode.value
|
||||
selectedItem.value = null
|
||||
if (isHallMode.value) {
|
||||
pkList.value = hallList.value
|
||||
} else {
|
||||
pkList.value = todayList.value
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function handleSearch() {
|
||||
page.value = 0
|
||||
if (isHallMode.value) {
|
||||
hallList.value = []
|
||||
} else {
|
||||
todayList.value = []
|
||||
}
|
||||
pkList.value = []
|
||||
loadPkList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
countryValue.value = null
|
||||
genderValue.value = null
|
||||
minCoin.value = null
|
||||
maxCoin.value = null
|
||||
timeRange.value = null
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
function loadMore() {
|
||||
loadPkList()
|
||||
}
|
||||
|
||||
// 加载PK列表
|
||||
async function loadPkList() {
|
||||
const userId = getUserId(currentUser.value)
|
||||
if (!userId) return
|
||||
|
||||
const body = {
|
||||
status: 0,
|
||||
page: page.value,
|
||||
size: 10,
|
||||
userId: userId,
|
||||
condition: {
|
||||
type: isHallMode.value ? 2 : 1
|
||||
}
|
||||
}
|
||||
|
||||
if (countryValue.value) body.condition.country = countryValue.value
|
||||
if (genderValue.value) body.condition.sex = genderValue.value
|
||||
|
||||
if (minCoin.value != null && maxCoin.value != null) {
|
||||
body.condition.coin = { start: minCoin.value, end: maxCoin.value }
|
||||
} else if (minCoin.value != null && maxCoin.value == null) {
|
||||
ElMessage.error('请输入最大金币数')
|
||||
return
|
||||
} else if (minCoin.value == null && maxCoin.value != null) {
|
||||
ElMessage.error('请输入最小金币数')
|
||||
return
|
||||
}
|
||||
|
||||
if (timeRange.value && isHallMode.value) {
|
||||
body.condition.pkTime = {
|
||||
start: timeRange.value[0] / 1000,
|
||||
end: timeRange.value[1] / 1000
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getPkList(body)
|
||||
if (res && res.length > 0) {
|
||||
if (isHallMode.value) {
|
||||
hallList.value.push(...res)
|
||||
pkList.value = hallList.value
|
||||
} else {
|
||||
todayList.value.push(...res)
|
||||
pkList.value = todayList.value
|
||||
}
|
||||
page.value++
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载 PK 列表失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 点击主播卡片
|
||||
async function handleItemClick(item) {
|
||||
selectedItem.value = item
|
||||
|
||||
try {
|
||||
const res = await getUserInfo({ id: item.senderId })
|
||||
chatUserInfo.value = res
|
||||
|
||||
// GoEasy 暂时禁用,聊天功能不可用
|
||||
messagesList.value = []
|
||||
ElMessage.warning('聊天功能暂时不可用(GoEasy 订阅未续费)')
|
||||
} catch (e) {
|
||||
console.error('获取聊天信息失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
function onMessageReceived(message) {
|
||||
// GoEasy 暂时禁用
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
if (chatMessagesRef.value) {
|
||||
chatMessagesRef.value.scrollTop = chatMessagesRef.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
async function sendMessage() {
|
||||
// GoEasy 暂时禁用
|
||||
ElMessage.warning('聊天功能暂时不可用(GoEasy 订阅未续费)')
|
||||
}
|
||||
|
||||
// 发送图片
|
||||
function handleSendImage() {
|
||||
// GoEasy 暂时禁用
|
||||
ElMessage.warning('聊天功能暂时不可用(GoEasy 订阅未续费)')
|
||||
}
|
||||
|
||||
async function handleFileSelect(event) {
|
||||
// GoEasy 暂时禁用
|
||||
event.target.value = ''
|
||||
}
|
||||
|
||||
// PK邀请
|
||||
function handleInvite() {
|
||||
ElMessage.info('PK邀请功能开发中')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
countryOptions.value = getCountryNamesArray()
|
||||
currentUser.value = getMainUserData() || {}
|
||||
const userId = getUserId(currentUser.value)
|
||||
|
||||
console.log('[PkHall] 当前用户数据:', currentUser.value)
|
||||
console.log('[PkHall] 解析的用户 ID:', userId)
|
||||
|
||||
// 同时加载今日PK和PK大厅数据
|
||||
if (userId) {
|
||||
// 加载今日PK
|
||||
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 {
|
||||
console.warn('[PkHall] 未找到用户 ID,无法加载数据')
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// GoEasy 暂时禁用
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.pk-hall {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// 顶部控制面板 - 固定高度
|
||||
.control-panel {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
min-height: 100px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding: 0 20px;
|
||||
background: linear-gradient(180deg, #f0fffe, #ffffff);
|
||||
border-bottom: 1px solid #e0f0f0;
|
||||
}
|
||||
|
||||
.pk-splitter {
|
||||
flex: 1;
|
||||
height: calc(100% - 100px);
|
||||
}
|
||||
|
||||
// 切换按钮
|
||||
.switch-box {
|
||||
position: relative;
|
||||
width: 280px;
|
||||
height: 50px;
|
||||
background-color: #4fcacd;
|
||||
border-radius: 25px;
|
||||
box-shadow: -3px 3px 4px #45aaac inset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.switch-text {
|
||||
width: 50%;
|
||||
height: 50px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
font-size: 18px;
|
||||
line-height: 50px;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.switch-slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 140px;
|
||||
height: 50px;
|
||||
border-radius: 25px;
|
||||
color: #03aba8;
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(180deg, #e4ffff, #ffffff);
|
||||
transition: left 0.3s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.switch-slider.slide-right {
|
||||
left: 140px;
|
||||
}
|
||||
|
||||
// 选择器
|
||||
.select-box {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
// 金币输入
|
||||
.coin-box {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.coin-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.coin-label {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
// 时间选择
|
||||
.time-box {
|
||||
width: 380px;
|
||||
}
|
||||
|
||||
// 按钮
|
||||
.btn-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-btn, .reset-btn {
|
||||
width: 80px;
|
||||
height: 30px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: linear-gradient(0deg, #4FCACD, #5FDBDE);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: white;
|
||||
border: 1px solid #03aba8;
|
||||
color: #03aba8;
|
||||
}
|
||||
|
||||
.search-btn:hover, .reset-btn:hover {
|
||||
transform: scale(1.05);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
// 列表面板
|
||||
.list-panel {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pk-list {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: 16px 0 0 16px;
|
||||
}
|
||||
|
||||
.pk-card {
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
background: url('https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/PKbackground.png') no-repeat center/cover;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.pk-card:hover {
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.pk-card.selected {
|
||||
background-color: #fffbfa;
|
||||
border: 1px solid #f4d0c9;
|
||||
}
|
||||
|
||||
.pk-avatar {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-right: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pk-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.pk-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pk-personal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.pk-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pk-gender {
|
||||
padding: 2px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pk-gender.male {
|
||||
background: #59d8db;
|
||||
}
|
||||
|
||||
.pk-gender.female {
|
||||
background: #f3876f;
|
||||
}
|
||||
|
||||
.pk-country {
|
||||
padding: 2px 10px;
|
||||
background: #e4f9f9;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
color: #03aba8;
|
||||
}
|
||||
|
||||
.pk-time {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.pk-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.session-icon {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.pk-remark {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
color: #03aba8;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
// 聊天面板
|
||||
.chat-panel {
|
||||
height: 100%;
|
||||
border-left: 1px solid #03aba82f;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
height: 50px;
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.message-item.mine {
|
||||
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 {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 8px solid transparent;
|
||||
border-right: 8px solid #f5f5f5;
|
||||
border-bottom: 8px solid transparent;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.message-item.mine .message-triangle {
|
||||
border-right: none;
|
||||
border-left: 8px solid #7bbd0093;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 65%;
|
||||
}
|
||||
|
||||
.text-message {
|
||||
padding: 10px 15px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message-item.mine .text-message {
|
||||
background: #7bbd0093;
|
||||
}
|
||||
|
||||
// 输入区域
|
||||
.chat-input-area {
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.input-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
background: #e4f9f9;
|
||||
}
|
||||
|
||||
.control-btns {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: white;
|
||||
box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.control-btn img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
padding: 8px 20px;
|
||||
color: #03aba8;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.send-btn:hover {
|
||||
background: #03aba82d;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.input-box textarea {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chat-placeholder {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #03aba8;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.time-box {
|
||||
width: 380px;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.time-box.is-hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden; // 仍然占位,但看不见
|
||||
pointer-events: none; // 不能点击
|
||||
}
|
||||
</style>
|
||||
107
src/views/pk-mini/PkMiniWorkbench.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="pk-mini-workbench">
|
||||
<el-container class="pk-container">
|
||||
<!-- 左侧导航栏 -->
|
||||
<el-aside class="pk-aside" width="80px">
|
||||
<PkAppaside :active="activeTab" @navigate="handleNavigate" />
|
||||
</el-aside>
|
||||
<!-- 右侧主体内容 -->
|
||||
<el-main class="pk-main">
|
||||
<KeepAlive>
|
||||
<component :is="currentComponent" />
|
||||
</KeepAlive>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import PkAppaside from '@/components/pk-mini/PkAppaside.vue'
|
||||
import PkHall from './PkHall.vue'
|
||||
import Forum from './Forum.vue'
|
||||
import Message from './Message.vue'
|
||||
import Mine from './Mine.vue'
|
||||
import { initPkGoEasy, goEasyLink, goEasyDisConnect } from '@/utils/pk-mini/goeasy'
|
||||
import { getOtp } from '@/api/pk-mini'
|
||||
import { getMainUserData } from '@/utils/pk-mini/storage'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const activeTab = ref('pk')
|
||||
|
||||
const componentMap = {
|
||||
pk: PkHall,
|
||||
forum: Forum,
|
||||
message: Message,
|
||||
mine: Mine
|
||||
}
|
||||
|
||||
const currentComponent = computed(() => componentMap[activeTab.value])
|
||||
|
||||
const handleNavigate = (tab) => {
|
||||
activeTab.value = tab
|
||||
}
|
||||
|
||||
// 自动连接 IM
|
||||
async function autoLinkIM() {
|
||||
try {
|
||||
const userData = getMainUserData()
|
||||
if (!userData || !userData.id) {
|
||||
console.log('PK Mini: 用户未登录,跳过 IM 连接')
|
||||
return
|
||||
}
|
||||
|
||||
const otp = await getOtp()
|
||||
const data = {
|
||||
id: String(userData.id),
|
||||
avatar: userData.headerIcon || '',
|
||||
nickname: userData.nickName || userData.username || '',
|
||||
key: otp
|
||||
}
|
||||
|
||||
await goEasyLink(data)
|
||||
console.log('PK Mini: IM 连接成功')
|
||||
} catch (err) {
|
||||
console.error('PK Mini: IM 连接失败', err)
|
||||
ElMessage.warning('PK工作台聊天系统连接失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化 GoEasy
|
||||
initPkGoEasy()
|
||||
// 自动连接 IM
|
||||
autoLinkIM()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 断开 IM 连接
|
||||
goEasyDisConnect().catch(() => {})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.pk-mini-workbench {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url(@/assets/pk-mini/bg.png);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.pk-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pk-aside {
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.pk-main {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||