第一次提交:Vue3后台前端最新版本

This commit is contained in:
XingQue
2026-06-14 02:43:42 +08:00
commit cf07faccc4
68 changed files with 29381 additions and 0 deletions

View File

@@ -0,0 +1,813 @@
<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>
</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'
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 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>