Files
ApiServer-Web-admin_dashboa…/src/views/user/UserBalance.vue
T
wlkjyy 54f78e15fe
Build and Deploy Vue3 / build (push) Successful in 1m12s
Build and Deploy Vue3 / deploy (push) Successful in 5m2s
feat: 优化工单和用户管理功能
- 工单模块改为列表形式,支持点击进入详情页回复
- 新增工单列表页面(TicketList.vue)和详情页面(TicketDetail.vue)
- 工单详情页支持图片上传、快捷回复、定时刷新
- 消息按ID排序,时间显示优化(今天/昨天/星期/完整日期)
- 定时刷新时不显示loading,且只在数据变化时更新UI
- 用户列表直接使用API返回的cover字段作为头像,减少HTTP请求
- 修复用户余额页面balance_type参数undefined问题
2025-12-16 11:29:52 +08:00

712 lines
19 KiB
Vue

<template>
<div class="user-balance-container">
<!-- 顶部信息栏 -->
<div class="page-header">
<div class="header-left">
<h2 class="page-title">用户余额管理</h2>
<div class="user-info">
<span class="user-name">{{ getCurrentUserName() }}</span>
<span class="user-id">ID: {{ queryParams.user_id }}</span>
</div>
</div>
<el-button type="primary" @click="fetchUserBalance">
<el-icon><Refresh /></el-icon>刷新数据
</el-button>
</div>
<!-- 余额卡片 -->
<div class="balance-cards" v-loading="loading">
<div
v-for="balance in balanceList"
:key="balance.type"
class="balance-card"
:class="`balance-card-${balance.type}`"
>
<div class="card-header">
<span class="balance-type">{{ getBalanceTypeName(balance.type) }}</span>
<span class="balance-id">ID: {{ balance.id }}</span>
</div>
<div class="card-body">
<div class="balance-amount">
<span class="currency">¥</span>
<span class="amount">{{ (balance.price / 100).toFixed(2) }}</span>
</div>
<div class="balance-time">更新于 {{ formatDateTime(balance.UpdatedAt) }}</div>
</div>
<div class="card-footer">
<el-button size="small" @click="handleEditBalance(balance.type)">修改余额</el-button>
<el-button size="small" @click="handleAddRecord(balance.type)">添加记录</el-button>
</div>
</div>
</div>
<!-- 余额记录 -->
<div class="record-section">
<div class="section-header">
<h3 class="section-title">余额变动记录</h3>
</div>
<div class="table-wrapper">
<el-table
:data="recordList"
v-loading="loading"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="Id" label="记录ID" width="100" />
<el-table-column label="余额类型" width="120">
<template #default="{ row }">
<el-tag size="small">{{ getBalanceTypeByRecordId(row.BalanceId) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="变动类型" width="100">
<template #default="{ row }">
<el-tag v-if="row.Change" type="success" size="small">增加</el-tag>
<el-tag v-else type="danger" size="small">减少</el-tag>
</template>
</el-table-column>
<el-table-column label="变动金额" width="150">
<template #default="{ row }">
<span :class="row.Change ? 'amount-plus' : 'amount-minus'">
{{ row.Change ? '+' : '-' }}¥{{ (row.Price / 100).toFixed(2) }}
</span>
</template>
</el-table-column>
<el-table-column label="变动后余额" width="150">
<template #default="{ row }">
<span class="balance-after">¥{{ ((row.BalanceAfter || 0) / 100).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column prop="Note" label="备注" min-width="200" show-overflow-tooltip />
<el-table-column label="创建时间" width="180">
<template #default="{ row }">
{{ formatDateTime(row.CreatedAt) }}
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="recordParams.page"
v-model:page-size="recordParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="recordTotal"
@size-change="handleRecordSizeChange"
@current-change="handleRecordCurrentChange"
background
/>
</div>
</div>
</div>
<!-- 修改余额对话框 -->
<el-dialog v-model="balanceDialogVisible" title="修改用户余额" width="500px">
<el-form ref="balanceFormRef" :model="balanceForm" :rules="balanceRules" label-width="100px">
<el-form-item label="余额类型">
<el-input :value="getBalanceTypeNameByApiType(balanceForm.balance_type)" disabled />
</el-form-item>
<el-form-item label="当前余额">
<el-input :value="currentBalanceDisplay" disabled>
<template #prepend>¥</template>
</el-input>
</el-form-item>
<el-form-item label="变动类型" prop="state">
<el-radio-group v-model="balanceForm.state">
<el-radio label="add">增加</el-radio>
<el-radio label="sub">减少</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="变动金额" prop="price">
<el-input v-model.number="balanceForm.price" placeholder="请输入变动金额">
<template #prepend>¥</template>
</el-input>
</el-form-item>
<el-form-item label="备注" prop="note">
<el-input v-model="balanceForm.note" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="balanceDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitBalanceForm">确定</el-button>
</template>
</el-dialog>
<!-- 添加记录对话框 -->
<el-dialog v-model="recordDialogVisible" title="添加消费记录" width="500px">
<el-form ref="recordFormRef" :model="recordForm" :rules="recordRules" label-width="120px">
<el-form-item label="余额类型">
<el-input :value="getBalanceTypeNameByApiType(recordForm.balance_type)" disabled />
</el-form-item>
<el-form-item label="是否执行到余额" prop="apply_balance">
<el-switch v-model="recordForm.apply_balance" active-text="是" inactive-text="否" />
</el-form-item>
<el-form-item label="变动类型" prop="state">
<el-radio-group v-model="recordForm.state">
<el-radio label="add">增加</el-radio>
<el-radio label="sub">减少</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="变动金额" prop="price">
<el-input v-model.number="recordForm.price" placeholder="请输入金额">
<template #prepend>¥</template>
</el-input>
</el-form-item>
<el-form-item label="备注" prop="note">
<el-input v-model="recordForm.note" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="recordDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitRecordForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import { getUserBalance, getUserBalanceRecord, editUserBalance, addUserConsumption, getUserBalanceCount, getUserList } from '@/api/admin/user'
const route = useRoute()
// 余额类型映射
const userBalance = {
entity: { name: '云钻', type: 'cloud_diamond' },
general: { name: '云币', type: 'cloud_coin' },
withdraw: { name: '云点', type: 'cloud_points' }
}
// 查询参数
const queryParams = reactive({
user_id: route.query.user_id || ''
})
// 余额记录查询参数
const recordParams = reactive({
user_id: '',
balance_type: '', // 初始化为空字符串,避免 undefined
page: 1,
count: 10
})
// 状态数据
const loading = ref(false)
const balanceInfo = ref(null)
const balanceList = ref([])
const recordList = ref([])
const recordTotal = ref(0)
const userList = ref([])
// 对话框
const balanceDialogVisible = ref(false)
const recordDialogVisible = ref(false)
const balanceFormRef = ref(null)
const recordFormRef = ref(null)
// 余额表单
const balanceForm = reactive({
user_id: '',
balance_type: '',
price: 0,
state: 'add',
note: ''
})
const balanceRules = {
price: [
{ required: true, message: '请输入变动金额', trigger: 'blur' },
{ type: 'number', message: '请输入数字', trigger: 'blur' }
],
state: [{ required: true, message: '请选择变动类型', trigger: 'change' }],
note: [{ required: true, message: '请输入备注', trigger: 'blur' }]
}
// 记录表单
const recordForm = reactive({
user_id: '',
balance_type: '',
apply_balance: false,
price: 0,
state: 'add',
note: ''
})
const recordRules = {
price: [
{ required: true, message: '请输入金额', trigger: 'blur' },
{ type: 'number', message: '请输入数字', trigger: 'blur' }
],
state: [{ required: true, message: '请选择状态类型', trigger: 'change' }],
note: [{ required: true, message: '请输入备注', trigger: 'blur' }]
}
// 当前余额显示
const currentBalanceDisplay = computed(() => {
const balance = balanceList.value.find(b => userBalance[b.type]?.type === balanceForm.balance_type)
return balance ? (balance.price / 100).toFixed(2) : '0.00'
})
// 获取用户列表(用于显示用户名)
const getUserListData = async () => {
try {
const res = await getUserList({ page: 1, count: 1000, key: '' })
if (res.data.code === 200) {
userList.value = res.data.data.data || []
}
} catch (error) {
console.error('获取用户列表错误:', error)
}
}
// 获取当前用户名
const getCurrentUserName = () => {
if (!queryParams.user_id) return '未知用户'
const user = userList.value.find(u => u.UserId === queryParams.user_id)
return user ? user.UserName : `用户${queryParams.user_id}`
}
// 获取余额类型名称
const getBalanceTypeName = (type) => {
return userBalance[type]?.name || type
}
// 通过API类型获取余额类型名称
const getBalanceTypeNameByApiType = (apiType) => {
const entry = Object.values(userBalance).find(b => b.type === apiType)
return entry ? entry.name : apiType
}
// 通过记录的BalanceId获取余额类型名称
const getBalanceTypeByRecordId = (balanceId) => {
if (!balanceId) return '未知类型'
const balance = balanceList.value.find(b => b.id === balanceId)
return balance ? getBalanceTypeName(balance.type) : '未知类型'
}
// 格式化时间
const formatDateTime = (dateStr) => {
if (!dateStr) return '-'
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', { hour12: false })
}
// 获取用户余额
const fetchUserBalance = async () => {
if (!queryParams.user_id) {
ElMessage.warning('缺少用户ID参数')
return
}
loading.value = true
try {
const res = await getUserBalanceCount({ user_id: queryParams.user_id })
if (res.data.code === 200) {
balanceInfo.value = res.data.data
const returnedData = Array.isArray(res.data.data) ? res.data.data : []
// 定义默认余额
const defaultBalances = [
{ id: '-', userId: queryParams.user_id, price: 0, type: 'entity', CreatedAt: new Date().toISOString(), UpdatedAt: new Date().toISOString() },
{ id: '-', userId: queryParams.user_id, price: 0, type: 'general', CreatedAt: new Date().toISOString(), UpdatedAt: new Date().toISOString() },
{ id: '-', userId: queryParams.user_id, price: 0, type: 'withdraw', CreatedAt: new Date().toISOString(), UpdatedAt: new Date().toISOString() }
]
// 检查缺失的余额类型
const requiredTypes = ['entity', 'general', 'withdraw']
const missingTypes = requiredTypes.filter(type => !returnedData.some(item => item.type === type))
// 初始化缺失的余额类型
if (missingTypes.length > 0) {
const initPromises = missingTypes.map(type =>
editUserBalance({
user_id: queryParams.user_id,
balance_type: userBalance[type].type,
price: 0,
state: 'add',
note: `系统自动初始化${getBalanceTypeName(type)}余额`
})
)
await Promise.all(initPromises)
// 重新获取余额
const newRes = await getUserBalanceCount({ user_id: queryParams.user_id })
if (newRes.data.code === 200) {
const newReturnedData = Array.isArray(newRes.data.data) ? newRes.data.data : []
balanceList.value = defaultBalances.map(defaultItem =>
newReturnedData.find(item => item.type === defaultItem.type) || defaultItem
)
}
} else {
balanceList.value = defaultBalances.map(defaultItem =>
returnedData.find(item => item.type === defaultItem.type) || defaultItem
)
}
recordParams.user_id = queryParams.user_id
fetchBalanceRecord()
} else {
ElMessage.error(res.data.message || '获取用户余额失败')
}
} catch (error) {
console.error('获取用户余额错误:', error)
ElMessage.error('获取用户余额失败')
} finally {
loading.value = false
}
}
// 获取余额记录
const fetchBalanceRecord = async () => {
loading.value = true
try {
const res = await getUserBalanceRecord(recordParams)
if (res.data.code === 200) {
recordList.value = res.data.data.data || []
recordTotal.value = res.data.data.all_count || 0
} else {
ElMessage.error(res.data.message || '获取余额记录失败')
}
} catch (error) {
console.error('获取余额记录错误:', error)
ElMessage.error('获取余额记录失败')
} finally {
loading.value = false
}
}
// 修改余额
const handleEditBalance = (balanceType) => {
const apiType = userBalance[balanceType]?.type
balanceFormRef.value?.resetFields()
Object.assign(balanceForm, {
user_id: queryParams.user_id,
balance_type: apiType,
price: 0,
state: 'add',
note: ''
})
balanceDialogVisible.value = true
}
// 添加记录
const handleAddRecord = (balanceType) => {
const apiType = userBalance[balanceType]?.type
recordFormRef.value?.resetFields()
Object.assign(recordForm, {
user_id: queryParams.user_id,
balance_type: apiType,
apply_balance: false,
price: 0,
state: 'add',
note: ''
})
recordDialogVisible.value = true
}
// 提交余额修改
const submitBalanceForm = () => {
balanceFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const res = await editUserBalance(balanceForm)
if (res.data.code === 200) {
ElMessage.success('修改成功')
balanceDialogVisible.value = false
fetchUserBalance()
} else {
ElMessage.error(res.data.message || '修改失败')
}
} catch (error) {
console.error('修改余额错误:', error)
ElMessage.error('修改失败')
}
}
})
}
// 提交记录添加
const submitRecordForm = () => {
recordFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const res = await addUserConsumption(recordForm)
if (res.data.code === 200) {
ElMessage.success('添加成功')
recordDialogVisible.value = false
fetchUserBalance()
} else {
ElMessage.error(res.data.message || '添加失败')
}
} catch (error) {
console.error('添加记录错误:', error)
ElMessage.error('添加失败')
}
}
})
}
// 分页
const handleRecordSizeChange = () => {
fetchBalanceRecord()
}
const handleRecordCurrentChange = () => {
fetchBalanceRecord()
}
// 初始化
onMounted(() => {
getUserListData()
if (queryParams.user_id) {
fetchUserBalance()
} else {
ElMessage.warning('缺少用户ID参数')
}
})
</script>
<style scoped>
.user-balance-container {
padding: 0;
background: #f5f7fa;
min-height: 100vh;
}
/* 页面头部 */
.page-header {
background: #fff;
padding: 24px 32px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e4e7ed;
margin-bottom: 24px;
}
.header-left {
display: flex;
align-items: center;
gap: 24px;
}
.page-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #303133;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background: #f5f7fa;
border-radius: 4px;
}
.user-name {
font-size: 14px;
font-weight: 500;
color: #303133;
}
.user-id {
font-size: 13px;
color: #909399;
}
/* 余额卡片 */
.balance-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
padding: 0 32px 24px;
}
.balance-card {
background: #fff;
border-radius: 8px;
padding: 24px;
border: 1px solid #e4e7ed;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.balance-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #409eff, #66b1ff);
}
.balance-card-entity::before {
background: linear-gradient(90deg, #67c23a, #85ce61);
}
.balance-card-general::before {
background: linear-gradient(90deg, #409eff, #66b1ff);
}
.balance-card-withdraw::before {
background: linear-gradient(90deg, #e6a23c, #f0c78a);
}
.balance-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
.balance-card .card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.balance-type {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.balance-id {
font-size: 12px;
color: #909399;
background: #f5f7fa;
padding: 4px 8px;
border-radius: 4px;
}
.balance-card .card-body {
margin-bottom: 20px;
}
.balance-amount {
display: flex;
align-items: baseline;
margin-bottom: 8px;
}
.balance-amount .currency {
font-size: 20px;
color: #606266;
margin-right: 4px;
}
.balance-amount .amount {
font-size: 32px;
font-weight: 600;
color: #303133;
}
.balance-time {
font-size: 13px;
color: #909399;
}
.balance-card .card-footer {
display: flex;
gap: 8px;
}
.balance-card .card-footer .el-button {
flex: 1;
}
/* 记录区域 */
.record-section {
background: #fff;
margin: 0 32px 32px;
border-radius: 8px;
border: 1px solid #e4e7ed;
overflow: hidden;
}
.section-header {
padding: 20px 24px;
border-bottom: 1px solid #e4e7ed;
}
.section-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.table-wrapper {
padding: 0;
}
.table-wrapper :deep(.el-table) {
border: none;
}
.table-wrapper :deep(.el-table td),
.table-wrapper :deep(.el-table th) {
border-bottom: 1px solid #f0f2f5;
}
.table-wrapper :deep(.el-table tr:hover > td) {
background-color: #fafafa;
}
.amount-plus {
color: #67c23a;
font-weight: 600;
font-size: 14px;
}
.amount-minus {
color: #f56c6c;
font-weight: 600;
font-size: 14px;
}
.balance-after {
color: #303133;
font-weight: 500;
}
.pagination-wrapper {
padding: 16px 24px;
display: flex;
justify-content: flex-end;
border-top: 1px solid #e4e7ed;
background: #fafafa;
}
/* 响应式 */
@media (max-width: 1200px) {
.balance-cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.balance-cards {
grid-template-columns: 1fr;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.header-left {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
}
</style>