Files
a_long_vue/src/views/user/PlayerDetail.vue
XingQue 75417417e5 小程序配置增加自动提现多商户管理;解冻成功后联动刷新详情余额。
新增商户列表/保存/证书上传入口(8080a);解冻面板回写余额并提示变动明细。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 02:44:44 +08:00

853 lines
27 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="player-detail">
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<el-skeleton :rows="8" animated />
</div>
<div v-else class="detail-content">
<!-- 顶部导航栏 -->
<div class="detail-header">
<div class="header-left">
<el-button type="primary" :icon="ArrowLeft" @click="goBack">返回</el-button>
<span class="page-title">打手详情</span>
</div>
<div class="header-right">
<el-button
v-if="!isEditMode"
type="warning"
:icon="Edit"
@click="toggleEditMode"
:disabled="!playerInfo.yonghuid"
>编辑</el-button>
<template v-else>
<el-button type="success" :icon="Check" @click="saveChanges" :loading="saving">保存</el-button>
<el-button :icon="Close" @click="cancelEdit">取消</el-button>
</template>
</div>
</div>
<!-- 主要内容区域 -->
<el-row :gutter="24">
<!-- 左侧基础信息卡片 + 统计卡片 -->
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
<!-- 基础信息卡片 -->
<el-card class="info-card" shadow="hover">
<template #header>
<div class="card-header">
<span>👤 基础信息</span>
</div>
</template>
<div class="base-info">
<div class="avatar-wrapper">
<el-avatar :size="100" :src="getFullImageUrl(playerInfo.avatar)" fit="cover">
<el-icon><Avatar /></el-icon>
</el-avatar>
<div class="status-badges">
<el-tag
:type="playerInfo.zaixianzhuangtai === 1 ? 'success' : 'info'"
size="small"
effect="dark"
>
{{ playerInfo.zaixianzhuangtai === 1 ? '在线' : '离线' }}
</el-tag>
<el-tag
:type="playerInfo.zhanghaozhuangtai === 1 ? 'success' : 'danger'"
size="small"
effect="dark"
>
{{ playerInfo.zhanghaozhuangtai === 1 ? '正常' : '封禁' }}
</el-tag>
<el-tag
:type="playerInfo.zhuangtai === 1 ? 'success' : 'warning'"
size="small"
effect="dark"
>
{{ playerInfo.zhuangtai === 1 ? '空闲' : '接单中' }}
</el-tag>
</div>
</div>
<el-descriptions :column="1" border>
<el-descriptions-item label="UID">
<span class="copyable" @click="copyText(playerInfo.yonghuid)">
{{ playerInfo.yonghuid || '--' }} <el-icon><CopyDocument /></el-icon>
</span>
</el-descriptions-item>
<el-descriptions-item label="昵称">{{ playerInfo.nicheng || '--' }}</el-descriptions-item>
<el-descriptions-item label="称号">
<div class="chenghao-wrapper">
<span>{{ playerInfo.chenghao || '无' }}</span>
<!-- 金牌打手按钮非编辑模式下显示 -->
<el-button
v-if="!isEditMode"
size="small"
:type="isGoldTitle ? 'danger' : 'primary'"
@click="handleChenghaoChange"
:disabled="!hasChenghaoPermission"
>
{{ isGoldTitle ? '降级为普通打手' : '升级为金牌选手' }}
</el-button>
</div>
</el-descriptions-item>
<el-descriptions-item label="电话">{{ playerInfo.dianhua || '--' }}</el-descriptions-item>
<el-descriptions-item label="微信号">{{ playerInfo.wechat || '--' }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ playerInfo.phone || '--' }}</el-descriptions-item>
<el-descriptions-item label="注册时间">{{ formatTime(playerInfo.create_time) }}</el-descriptions-item>
<el-descriptions-item label="IP地址">
<span class="copyable" @click="copyText(playerInfo.ip)">
{{ playerInfo.ip || '--' }} <el-icon><CopyDocument /></el-icon>
</span>
</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
<!-- 统计卡片保持不变 -->
<el-card class="info-card" shadow="hover">
<template #header>
<div class="card-header">
<span>📊 数据统计</span>
</div>
</template>
<el-row :gutter="16">
<el-col :span="12">
<div class="stat-item">
<div class="stat-label">接单总量</div>
<div class="stat-value">{{ playerInfo.jiedanzongliang || 0 }}</div>
</div>
</el-col>
<el-col :span="12">
<div class="stat-item">
<div class="stat-label">成交总量</div>
<div class="stat-value success">{{ playerInfo.chengjiaozongliang || 0 }}</div>
</div>
</el-col>
<el-col :span="12">
<div class="stat-item">
<div class="stat-label">退款总量</div>
<div class="stat-value danger">{{ playerInfo.tuikuanliang || 0 }}</div>
</div>
</el-col>
<el-col :span="12">
<div class="stat-item">
<div class="stat-label">今日接单</div>
<div class="stat-value">{{ playerInfo.jinrijiedan || 0 }}</div>
</div>
</el-col>
</el-row>
<el-divider />
<el-row :gutter="16" align="middle">
<el-col :span="12">
<div class="stat-item">
<div class="stat-label">成交率</div>
<el-progress
type="circle"
:percentage="completionRate"
:width="80"
:stroke-width="6"
status="success"
/>
</div>
</el-col>
<el-col :span="12">
<div class="stat-percent">{{ completionRate }}%</div>
</el-col>
</el-row>
<el-row :gutter="16" align="middle">
<el-col :span="12">
<div class="stat-item">
<div class="stat-label">退款率</div>
<el-progress
type="circle"
:percentage="refundRate"
:width="80"
:stroke-width="6"
status="warning"
/>
</div>
</el-col>
<el-col :span="12">
<div class="stat-percent">{{ refundRate }}%</div>
</el-col>
</el-row>
</el-card>
</el-col>
<!-- 右侧详细信息和可编辑字段 -->
<el-col :xs="24" :sm="24" :md="16" :lg="16" :xl="16">
<!-- 财务信息卡片 -->
<el-card class="info-card" shadow="hover">
<template #header>
<div class="card-header">
<span>💰 财务信息</span>
</div>
</template>
<el-form :model="editData" label-width="120px" :disabled="!isEditMode">
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="可提现余额">
<el-input-number
v-if="isEditMode"
v-model="editData.yue"
:precision="2"
:min="0"
:step="10"
style="width: 100%"
/>
<span v-else>¥{{ formatPrice(playerInfo.yue) }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="赚取总额">
<span>¥{{ formatPrice(playerInfo.zonge) }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="积分">
<el-input-number
v-if="isEditMode"
v-model="editData.jifen"
:min="0"
:step="10"
style="width: 100%"
/>
<span v-else>{{ playerInfo.jifen || 0 }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="押金">
<el-input-number
v-if="isEditMode"
v-model="editData.yajin"
:precision="2"
:min="0"
:step="10"
style="width: 100%"
/>
<span v-else>¥{{ formatPrice(playerInfo.yajin) }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="今日收益">
<span>¥{{ formatPrice(playerInfo.jinrishouyi) }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="今月收益">
<span>¥{{ formatPrice(playerInfo.jinyueshouyi) }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 状态信息卡片 -->
<el-card class="info-card" shadow="hover">
<template #header>
<div class="card-header">
<span> 状态管理</span>
</div>
</template>
<el-form :model="isEditMode ? editData : playerInfo" label-width="120px">
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="在线状态">
<el-select
v-if="isEditMode"
v-model="editData.zaixianzhuangtai"
style="width: 100%"
>
<el-option :value="1" label="在线" />
<el-option :value="0" label="离线" />
</el-select>
<span v-else>{{ playerInfo.zaixianzhuangtai === 1 ? '在线' : '离线' }}</span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="账号状态">
<el-select
v-if="isEditMode"
v-model="editData.zhanghaozhuangtai"
style="width: 100%"
>
<el-option :value="1" label="正常" />
<el-option :value="0" label="封禁" />
</el-select>
<span v-else>{{ playerInfo.zhanghaozhuangtai === 1 ? '正常' : '封禁' }}</span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="工作状态">
<el-select
v-if="isEditMode"
v-model="editData.zhuangtai"
style="width: 100%"
>
<el-option :value="1" label="空闲" />
<el-option :value="0" label="接单中" />
</el-select>
<span v-else>{{ playerInfo.zhuangtai === 1 ? '空闲' : '接单中' }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 会员信息卡片只读+操作按钮 -->
<el-card class="info-card" shadow="hover">
<template #header>
<div class="card-header">
<span>🎫 会员信息</span>
<el-button
type="primary"
size="small"
@click="openMemberDialog"
:disabled="!hasMemberPermission"
>添加会员</el-button>
</div>
</template>
<div class="member-list">
<div v-for="member in memberList" :key="member.id" class="member-item">
<el-tag :type="member.is_active ? 'success' : 'info'" effect="plain">
{{ member.huiyuan_name }} 到期: {{ formatTime(member.daoqi_time) }}
</el-tag>
<el-button
type="danger"
size="small"
link
@click="removeMember(member)"
:disabled="!hasMemberPermission"
>移除</el-button>
</div>
<div v-if="memberList.length === 0" class="empty-message">暂无会员点击上方按钮添加</div>
</div>
</el-card>
<!-- 其他信息卡片 -->
<el-card class="info-card" shadow="hover">
<template #header>
<div class="card-header">
<span>📝 其他信息</span>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="邀请人ID">
<span class="copyable" @click="copyText(playerInfo.yaoqingren)">
{{ playerInfo.yaoqingren || '--' }} <el-icon><CopyDocument /></el-icon>
</span>
</el-descriptions-item>
<el-descriptions-item label="介绍">
<el-tooltip v-if="playerInfo.jieshao" :content="playerInfo.jieshao" placement="top">
<span class="truncate">{{ playerInfo.jieshao }}</span>
</el-tooltip>
<span v-else>--</span>
</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
</el-row>
<el-row v-if="playerInfo.yonghuid" :gutter="20" class="freeze-section">
<el-col :span="12">
<FreezeConfigPanel
:yonghuid="playerInfo.yonghuid"
:leixing="1"
title="打手佣金"
:is-edit-mode="true"
@unfrozen="onFreezeUnfrozen"
/>
</el-col>
<el-col :span="12">
<FreezeConfigPanel
:yonghuid="playerInfo.yonghuid"
:leixing="5"
title="打手押金"
:is-edit-mode="true"
@unfrozen="onFreezeUnfrozen"
/>
</el-col>
</el-row>
</div>
<!-- 添加会员弹窗 -->
<el-dialog v-model="memberDialogVisible" title="添加会员" width="400px">
<el-form :model="memberForm" label-width="80px">
<el-form-item label="会员类型" required>
<el-select v-model="memberForm.huiyuan_id" placeholder="请选择会员" style="width: 100%">
<el-option
v-for="item in allMembers"
:key="item.huiyuan_id"
:label="item.jieshao"
:value="item.huiyuan_id"
/>
</el-select>
</el-form-item>
<el-form-item label="天数" required>
<el-input-number v-model="memberForm.days" :min="1" :step="1" style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="memberDialogVisible = false">取消</el-button>
<el-button type="primary" @click="addMember" :loading="memberAdding">确认</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, Edit, Check, Close, CopyDocument, Avatar } from '@element-plus/icons-vue'
import request from '@/utils/request'
import FreezeConfigPanel from '@/components/FreezeConfigPanel.vue'
const route = useRoute()
const router = useRouter()
// 基础数据
const loading = ref(false)
const saving = ref(false)
const playerId = ref('')
const username = ref(localStorage.getItem('username') || '')
const playerInfo = ref({})
const originalInfo = ref({})
const memberList = ref([])
const allMembers = ref([])
const isEditMode = ref(false)
const editData = ref({})
// 会员弹窗
const memberDialogVisible = ref(false)
const memberForm = reactive({ huiyuan_id: '', days: 30 })
const memberAdding = ref(false)
// 权限控制
const userPermissions = ref([])
const isGoldTitle = computed(() => playerInfo.value.chenghao === '金牌选手')
const hasMemberPermission = computed(() => userPermissions.value.includes('001ff') || userPermissions.value.includes('001gg'))
const hasChenghaoPermission = computed(() => userPermissions.value.includes('001hh'))
// 辅助函数
const getFullImageUrl = (url) => {
if (!url) return ''
const ossUrl = window.$ossURL || ''
return url.startsWith('http') ? url : ossUrl + url
}
const formatPrice = (val) => parseFloat(val || 0).toFixed(2)
const formatTime = (time) => {
if (!time) return '--'
const date = new Date(time)
return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2,'0')}-${date.getDate().toString().padStart(2,'0')} ${date.getHours().toString().padStart(2,'0')}:${date.getMinutes().toString().padStart(2,'0')}`
}
const copyText = (text) => {
if (!text || text === '--') return
navigator.clipboard.writeText(text).then(() => ElMessage.success('复制成功')).catch(() => ElMessage.error('复制失败'))
}
const goBack = () => router.back()
// 统计
const completionRate = computed(() => {
const total = playerInfo.value.jiedanzongliang || 0
const completed = playerInfo.value.chengjiaozongliang || 0
return total === 0 ? 0 : Math.round((completed / total) * 100)
})
const refundRate = computed(() => {
const completed = playerInfo.value.chengjiaozongliang || 0
const refund = playerInfo.value.tuikuanliang || 0
return completed === 0 ? 0 : Math.round((refund / completed) * 100)
})
// 获取详情
const fetchDetail = async () => {
if (!playerId.value) return
loading.value = true
try {
const res = await request.post('/houtai/kefuhqdsxq', {
phone: username.value,
uid: playerId.value
})
if (res.code === 0) {
const data = res.data
playerInfo.value = data.user_info || {}
originalInfo.value = JSON.parse(JSON.stringify(playerInfo.value))
memberList.value = data.member_list || []
allMembers.value = data.all_members || []
userPermissions.value = data.permissions || []
} else if (res.code === 403) {
ElMessage.error('无权限访问')
router.back()
} else {
ElMessage.error(res.msg || '获取打手详情失败')
}
} catch (error) {
console.error('获取打手详情失败', error)
ElMessage.error('获取打手详情失败')
} finally {
loading.value = false
}
}
// 解冻后同步上方「可提现余额/押金」展示(冻结面板与详情卡片区联动)
const onFreezeUnfrozen = async (payload) => {
const ur = payload?.unfreeze_result
if (ur) {
const after = parseFloat(ur.balance_after)
if (!Number.isNaN(after)) {
if (payload.leixing === 5) {
playerInfo.value.yajin = after
if (isEditMode.value) editData.value.yajin = after
} else if (payload.leixing === 1) {
playerInfo.value.yue = after
if (isEditMode.value) editData.value.yue = after
}
}
}
await fetchDetail()
}
// 编辑模式
const toggleEditMode = () => {
editData.value = JSON.parse(JSON.stringify(playerInfo.value))
isEditMode.value = true
}
const cancelEdit = () => {
isEditMode.value = false
editData.value = {}
}
const saveChanges = async () => {
const changes = {}
for (const key in editData.value) {
if (editData.value[key] !== playerInfo.value[key]) {
changes[key] = editData.value[key]
}
}
if (Object.keys(changes).length === 0) {
ElMessage.warning('未做任何修改')
cancelEdit()
return
}
saving.value = true
try {
let success = true
for (const [key, val] of Object.entries(changes)) {
let caozuo = ''
let value = val
switch (key) {
case 'yue':
caozuo = val > playerInfo.value.yue ? 'jia_yue' : 'jian_yue'
value = Math.abs(val - playerInfo.value.yue)
break
case 'jifen':
caozuo = val > playerInfo.value.jifen ? 'jia_jifen' : 'jian_jifen'
value = Math.abs(val - playerInfo.value.jifen)
break
case 'yajin':
caozuo = val > playerInfo.value.yajin ? 'jia_yajin' : 'jian_yajin'
value = Math.abs(val - playerInfo.value.yajin)
break
case 'zhanghaozhuangtai':
caozuo = 'gai_zhanghaozhuangtai'
value = val
break
case 'zaixianzhuangtai':
caozuo = 'gai_zaixianzhuangtai'
value = val
break
case 'zhuangtai':
caozuo = 'gai_zhuangtai'
value = val
break
default:
continue
}
const res = await request.post('/houtai/kefuxgds', {
username: username.value,
dashou_id: playerId.value,
caozuo: caozuo,
value: value
})
if (res.code !== 0) {
success = false
ElMessage.error(`修改 ${key} 失败:${res.msg}`)
break
}
}
if (success) {
ElMessage.success('修改成功')
await fetchDetail()
cancelEdit()
}
} catch (error) {
ElMessage.error('修改失败')
} finally {
saving.value = false
}
}
// 称号修改
const handleChenghaoChange = async () => {
const newTitle = isGoldTitle.value ? '普通打手' : '金牌选手'
try {
const res = await request.post('/houtai/kefuxgds', {
username: username.value,
dashou_id: playerId.value,
caozuo: 'gai_chenghao',
value: newTitle
})
if (res.code === 0) {
ElMessage.success(`称号已修改为“${newTitle}`)
await fetchDetail()
} else {
ElMessage.error(res.msg || '修改称号失败')
}
} catch (error) {
ElMessage.error('修改称号失败')
}
}
// 添加会员
const openMemberDialog = () => {
memberForm.huiyuan_id = ''
memberForm.days = 30
memberDialogVisible.value = true
}
const addMember = async () => {
if (!memberForm.huiyuan_id) {
ElMessage.warning('请选择会员类型')
return
}
memberAdding.value = true
try {
const res = await request.post('/houtai/kefuxgds', {
username: username.value,
dashou_id: playerId.value,
caozuo: 'jia_huiyuan',
huiyuan_id: memberForm.huiyuan_id,
value: memberForm.days
})
if (res.code === 0) {
ElMessage.success('添加会员成功')
memberDialogVisible.value = false
await fetchDetail()
} else {
ElMessage.error(res.msg || '添加失败')
}
} catch (error) {
ElMessage.error('添加失败')
} finally {
memberAdding.value = false
}
}
const removeMember = async (member) => {
ElMessageBox.confirm(`确定移除会员“${member.huiyuan_name}”吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await request.post('/houtai/kefuxgds', {
username: username.value,
dashou_id: playerId.value,
caozuo: 'jian_huiyuan',
huiyuan_id: member.huiyuan_id
})
if (res.code === 0) {
ElMessage.success('移除成功')
await fetchDetail()
} else {
ElMessage.error(res.msg || '移除失败')
}
}).catch(() => {})
}
// 关键:从路由参数中获取 playerId参数名必须为 playerId
onMounted(() => {
playerId.value = route.params.playerId
if (!playerId.value) {
ElMessage.error('打手ID不存在')
router.back()
return
}
fetchDetail()
})
</script>
<style scoped>
/* 保持原有样式,新增以下 */
.chenghao-wrapper {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.member-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.member-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.empty-message {
color: #999;
text-align: center;
padding: 20px;
}
/* 其余样式保持不变(从原代码复制) */
.player-detail {
padding: 20px;
background-color: #f5f9fc;
min-height: 100%;
}
.loading-container {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 2px 8px rgba(0,0,0,0.02);
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
background: white;
padding: 16px 24px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.02);
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #1a2634;
}
.header-right {
display: flex;
gap: 12px;
}
.info-card {
margin-bottom: 24px;
border-radius: 12px;
overflow: hidden;
}
.card-header {
font-weight: 600;
color: #1a2634;
}
.base-info {
padding: 8px;
}
.avatar-wrapper {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
}
.status-badges {
margin-top: 12px;
display: flex;
gap: 8px;
justify-content: center;
}
.copyable {
cursor: pointer;
color: #409eff;
display: inline-flex;
align-items: center;
gap: 4px;
}
.copyable:hover {
text-decoration: underline;
}
.stat-item {
text-align: center;
padding: 12px 0;
}
.stat-label {
font-size: 13px;
color: #6b7a88;
margin-bottom: 4px;
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: #1a2634;
}
.stat-value.success {
color: #67c23a;
}
.stat-value.danger {
color: #f56c6c;
}
.stat-percent {
font-size: 24px;
font-weight: 600;
color: #1a2634;
text-align: center;
}
.el-divider {
margin: 16px 0;
}
.member-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.member-tag {
font-size: 13px;
}
.truncate {
max-width: 200px;
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 手机适配 */
@media screen and (max-width: 768px) {
.player-detail {
padding: 12px;
}
.detail-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.header-right {
width: 100%;
justify-content: flex-end;
}
.stat-value {
font-size: 20px;
}
}
</style>