Files
ApiServer-Web-admin_dashboa…/src/views/user/UserBalance.vue
T
shiran c5d4dbc0db
Build and Deploy Vue3 / build (push) Successful in 1m44s
Build and Deploy Vue3 / deploy (push) Successful in 47s
feat: 完善订单定价与支付收益管理
2026-07-20 18:33:46 +08:00

785 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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="user-balance-container">
<!-- 顶部信息栏 -->
<div class="page-header">
<div class="header-left">
<el-button @click="router.go(-1)" link class="back-btn">
<el-icon><ArrowLeft /></el-icon> 返回
</el-button>
<el-divider direction="vertical" />
<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="支付订单ID" width="150" show-overflow-tooltip>
<template #default="{ row }">
<el-link v-if="row.PaymentOrderId" type="primary" :underline="false" @click="router.push({ path: '/order/list', query: { key: row.PaymentOrderId } })">{{ row.PaymentOrderId }}</el-link>
<span v-else>-</span>
</template>
</el-table-column>
<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 v-if="balanceForm.state === 'add'" label="收益入账">
<div class="account-amount-field">
<el-switch
v-model="balanceForm.account_amount"
active-text="计入 007pay"
inactive-text="不计入"
/>
<div class="form-tip">加款同时生成 007pay 手工入账记录用于收益统计</div>
</div>
</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="支付平台订单id" prop="paymentOrderId">
<el-input v-model="recordForm.paymentOrderId" placeholder="请输入支付平台订单ID" />
</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, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Refresh, ArrowLeft } from '@element-plus/icons-vue'
import { getUserBalanceRecord, editUserBalance, addUserConsumption, getUserBalanceCount, getUserList } from '@/api/admin/user'
const route = useRoute()
const router = useRouter()
// 余额类型映射
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: '',
account_amount: false
})
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: '',
paymentOrderId: ''
})
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: 10, 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.total || 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: '',
account_amount: false
})
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: '',
paymentOrderId: ''
})
recordDialogVisible.value = true
}
// 提交余额修改
const submitBalanceForm = () => {
balanceFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = { ...balanceForm }
if (submitData.state !== 'add' || !submitData.account_amount) {
delete submitData.account_amount
}
const res = await editUserBalance(submitData)
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参数')
}
})
// 监听路由参数变化
watch(
() => route.query.user_id,
(newUserId) => {
if (newUserId && newUserId !== queryParams.user_id) {
queryParams.user_id = newUserId
recordParams.user_id = newUserId
fetchUserBalance()
}
},
{ immediate: true }
)
watch(
() => balanceForm.state,
(state) => {
if (state !== 'add') {
balanceForm.account_amount = false
}
}
)
</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: 12px;
}
.back-btn {
font-size: 14px;
color: #606266;
}
.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;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.account-amount-field {
display: flex;
flex-direction: column;
gap: 4px;
}
/* 响应式 */
@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>