融合PK头像头像功能

This commit is contained in:
2026-02-08 15:33:10 +08:00
parent c6435c6db5
commit 76d83fc77e
55 changed files with 5403 additions and 14 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>