Merge branch 'main' of https://gitlab.guxuan.icu/Leo_Ding/GPU_Web
This commit is contained in:
commit
2c08ca7a73
@ -239,6 +239,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () =>
|
component: () =>
|
||||||
import("@/views/admin/account/cost/myMoney/index.vue"),
|
import("@/views/admin/account/cost/myMoney/index.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "exchange",
|
||||||
|
name: "exchange",
|
||||||
|
component: () =>
|
||||||
|
import("@/views/admin/account/cost/exchange/index.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "myMoneyDetail",
|
path: "myMoneyDetail",
|
||||||
name: "myMoneyDetail",
|
name: "myMoneyDetail",
|
||||||
|
|||||||
895
src/views/admin/account/cost/exchange/index.vue
Normal file
895
src/views/admin/account/cost/exchange/index.vue
Normal file
@ -0,0 +1,895 @@
|
|||||||
|
<template>
|
||||||
|
<div class="points-exchange-page">
|
||||||
|
<!-- 面包屑导航 -->
|
||||||
|
<a-breadcrumb class="breadcrumb">
|
||||||
|
<a-breadcrumb-item>
|
||||||
|
<router-link to="/layout/admin/myMoney">费用总览</router-link>
|
||||||
|
</a-breadcrumb-item>
|
||||||
|
<a-breadcrumb-item>算力点兑换</a-breadcrumb-item>
|
||||||
|
</a-breadcrumb>
|
||||||
|
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">算力点兑换</h1>
|
||||||
|
<p class="page-description">使用余额兑换算力点,提升计算能力</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- 余额卡片 -->
|
||||||
|
<a-card class="balance-card" :bordered="false">
|
||||||
|
<div class="balance-info">
|
||||||
|
<div class="balance-item">
|
||||||
|
<div class="balance-label">
|
||||||
|
<span class="icon-wrapper">
|
||||||
|
<DollarCircleOutlined />
|
||||||
|
</span>
|
||||||
|
<span class="label-text">我的余额</span>
|
||||||
|
</div>
|
||||||
|
<div class="balance-value">
|
||||||
|
<span class="currency">¥</span>
|
||||||
|
<span class="amount">{{ formatCurrency(balance) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="balance-item">
|
||||||
|
<div class="balance-label">
|
||||||
|
<span class="icon-wrapper">
|
||||||
|
<ThunderboltOutlined />
|
||||||
|
</span>
|
||||||
|
<span class="label-text">当前算力点</span>
|
||||||
|
</div>
|
||||||
|
<div class="balance-value">
|
||||||
|
<span class="points">{{ formatPoints(points) }}</span>
|
||||||
|
<span class="points-unit">算力点</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="balance-tips">
|
||||||
|
<InfoCircleOutlined />
|
||||||
|
<span>余额可用于兑换算力点,算力点可用于平台计算服务</span>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- 兑换卡片 -->
|
||||||
|
<a-card class="exchange-card" title="兑换算力点">
|
||||||
|
<div class="exchange-content">
|
||||||
|
<!-- 兑换金额选择 -->
|
||||||
|
<div class="exchange-section">
|
||||||
|
<h3 class="section-title">选择兑换金额</h3>
|
||||||
|
<p class="section-subtitle">请选择或输入要兑换的金额</p>
|
||||||
|
|
||||||
|
<div class="amount-options">
|
||||||
|
<a-button
|
||||||
|
v-for="option in amountOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:class="['amount-option', { 'selected': selectedAmount === option.value }]"
|
||||||
|
@click="selectAmount(option.value)"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<div class="amount-option-content">
|
||||||
|
<div class="amount-number">¥{{ option.value }}</div>
|
||||||
|
<div class="amount-points">= {{ option.value }} 算力点</div>
|
||||||
|
</div>
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自定义输入 -->
|
||||||
|
<div class="custom-amount">
|
||||||
|
<a-input-group compact>
|
||||||
|
<a-input
|
||||||
|
v-model:value="customAmount"
|
||||||
|
placeholder="输入其他金额"
|
||||||
|
size="large"
|
||||||
|
style="width: calc(100% - 120px)"
|
||||||
|
@change="handleCustomAmountChange"
|
||||||
|
:disabled="loading"
|
||||||
|
/>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
style="width: 120px"
|
||||||
|
:disabled="!isCustomAmountValid || loading"
|
||||||
|
@click="handleCustomAmountConfirm"
|
||||||
|
>
|
||||||
|
确认
|
||||||
|
</a-button>
|
||||||
|
</a-input-group>
|
||||||
|
<div v-if="customAmountError" class="error-text">
|
||||||
|
<ExclamationCircleOutlined />
|
||||||
|
{{ customAmountError }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 兑换规则说明 -->
|
||||||
|
<div class="exchange-section">
|
||||||
|
<h3 class="section-title">兑换规则</h3>
|
||||||
|
<div class="exchange-rules">
|
||||||
|
<div class="rule-item">
|
||||||
|
<CheckCircleOutlined class="rule-icon" />
|
||||||
|
<span>算力点兑换比例为人民币 <strong>1元 = 1算力点</strong></span>
|
||||||
|
</div>
|
||||||
|
<div class="rule-item">
|
||||||
|
<ExclamationCircleOutlined class="rule-icon" />
|
||||||
|
<span><strong>算力点不可退</strong>,兑换前请确认需求</span>
|
||||||
|
</div>
|
||||||
|
<div class="rule-item">
|
||||||
|
<InfoCircleOutlined class="rule-icon" />
|
||||||
|
<span>兑换后算力点立即生效,可用于平台所有计算服务</span>
|
||||||
|
</div>
|
||||||
|
<div class="rule-item">
|
||||||
|
<SafetyOutlined class="rule-icon" />
|
||||||
|
<span>兑换过程安全加密,保障您的资金安全</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 兑换信息展示 -->
|
||||||
|
<div v-if="selectedExchangeAmount > 0" class="exchange-preview">
|
||||||
|
<div class="preview-card">
|
||||||
|
<div class="preview-header">
|
||||||
|
<div class="preview-title">兑换明细</div>
|
||||||
|
<a-button type="text" @click="clearSelection">
|
||||||
|
<CloseOutlined />
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<div class="preview-content">
|
||||||
|
<div class="preview-item">
|
||||||
|
<span class="preview-label">兑换金额:</span>
|
||||||
|
<span class="preview-value">¥{{ selectedExchangeAmount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-item">
|
||||||
|
<span class="preview-label">获得算力点:</span>
|
||||||
|
<span class="preview-value points-value">{{ selectedExchangeAmount }} 算力点</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-item">
|
||||||
|
<span class="preview-label">兑换后余额:</span>
|
||||||
|
<span class="preview-value">¥{{ formatCurrency(balance - selectedExchangeAmount) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-item">
|
||||||
|
<span class="preview-label">兑换后算力点:</span>
|
||||||
|
<span class="preview-value points-value">{{ formatPoints(points + selectedExchangeAmount) }} 算力点</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户协议确认 -->
|
||||||
|
<div class="agreement-section">
|
||||||
|
<a-checkbox v-model:checked="agreementChecked" :disabled="loading">
|
||||||
|
在确认兑换算力点前,我已阅读并确认
|
||||||
|
<a @click="showAgreementModal" class="agreement-link">《用户协议》</a>
|
||||||
|
</a-checkbox>
|
||||||
|
<div v-if="!agreementChecked && showAgreementError" class="error-text">
|
||||||
|
请阅读并确认用户协议
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="!canExchange"
|
||||||
|
@click="handleExchange"
|
||||||
|
class="exchange-button"
|
||||||
|
>
|
||||||
|
<template #icon><ThunderboltOutlined /></template>
|
||||||
|
确认兑换
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="large"
|
||||||
|
@click="handleCancel"
|
||||||
|
:disabled="loading"
|
||||||
|
class="cancel-button"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户协议模态框 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="agreementModalVisible"
|
||||||
|
title="用户协议"
|
||||||
|
width="800px"
|
||||||
|
:footer="null"
|
||||||
|
>
|
||||||
|
<div class="agreement-content">
|
||||||
|
<h3>算力点兑换服务协议</h3>
|
||||||
|
<p>欢迎使用算力点兑换服务。在兑换算力点前,请您仔细阅读以下协议内容:</p>
|
||||||
|
|
||||||
|
<h4>一、兑换规则</h4>
|
||||||
|
<p>1. 算力点兑换比例为1元人民币兑换1算力点。</p>
|
||||||
|
<p>2. 最小兑换金额为100元,最大单次兑换金额为10000元。</p>
|
||||||
|
<p>3. 兑换操作一经确认,不可撤销或退款。</p>
|
||||||
|
|
||||||
|
<h4>二、使用规则</h4>
|
||||||
|
<p>1. 算力点可用于平台提供的各项计算服务。</p>
|
||||||
|
<p>2. 算力点不设有效期,但平台保留根据业务调整的权利。</p>
|
||||||
|
<p>3. 算力点不可转让、不可提现、不可兑换为现金。</p>
|
||||||
|
|
||||||
|
<h4>三、免责声明</h4>
|
||||||
|
<p>1. 如因系统维护、升级等需要暂停服务,平台将提前公告。</p>
|
||||||
|
<p>2. 用户应妥善保管账户信息,因用户原因造成的损失平台不承担责任。</p>
|
||||||
|
|
||||||
|
<div class="agreement-footer">
|
||||||
|
<a-checkbox v-model:checked="modalAgreementChecked">
|
||||||
|
我已阅读并同意以上协议
|
||||||
|
</a-checkbox>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleAgreementConfirm"
|
||||||
|
:disabled="!modalAgreementChecked"
|
||||||
|
>
|
||||||
|
确认并关闭
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import {
|
||||||
|
DollarCircleOutlined,
|
||||||
|
ThunderboltOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
SafetyOutlined,
|
||||||
|
CloseOutlined
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
import { message, Modal } from 'ant-design-vue'
|
||||||
|
|
||||||
|
// 用户余额和算力点
|
||||||
|
const balance = ref(3568.50)
|
||||||
|
const points = ref(1250)
|
||||||
|
|
||||||
|
// 金额选项
|
||||||
|
const amountOptions = ref([
|
||||||
|
{ value: 100, label: '100元' },
|
||||||
|
{ value: 200, label: '200元' },
|
||||||
|
{ value: 500, label: '500元' },
|
||||||
|
{ value: 1000, label: '1000元' },
|
||||||
|
{ value: 2000, label: '2000元' },
|
||||||
|
{ value: 5000, label: '5000元' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 选择的金额
|
||||||
|
const selectedAmount = ref(0)
|
||||||
|
const customAmount = ref('')
|
||||||
|
const customAmountError = ref('')
|
||||||
|
|
||||||
|
// 用户协议确认
|
||||||
|
const agreementChecked = ref(false)
|
||||||
|
const showAgreementError = ref(false)
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 模态框
|
||||||
|
const agreementModalVisible = ref(false)
|
||||||
|
const modalAgreementChecked = ref(false)
|
||||||
|
|
||||||
|
// 兑换记录
|
||||||
|
const exchangeHistory = ref([
|
||||||
|
{ key: 1, amount: 1000, points: 1000, time: '2024-03-15 14:30:22', status: 'success' },
|
||||||
|
{ key: 2, amount: 500, points: 500, time: '2024-03-10 09:15:45', status: 'success' },
|
||||||
|
{ key: 3, amount: 2000, points: 2000, time: '2024-03-05 16:20:33', status: 'success' },
|
||||||
|
{ key: 4, amount: 100, points: 100, time: '2024-02-28 11:45:12', status: 'success' },
|
||||||
|
{ key: 5, amount: 5000, points: 5000, time: '2024-02-20 13:05:27', status: 'success' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const historyColumns = [
|
||||||
|
{
|
||||||
|
title: '兑换金额',
|
||||||
|
dataIndex: 'amount',
|
||||||
|
key: 'amount',
|
||||||
|
align: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '获得算力点',
|
||||||
|
dataIndex: 'points',
|
||||||
|
key: 'points',
|
||||||
|
align: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '兑换时间',
|
||||||
|
dataIndex: 'time',
|
||||||
|
key: 'time'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 计算选中的兑换金额
|
||||||
|
const selectedExchangeAmount = computed(() => {
|
||||||
|
if (selectedAmount.value > 0) {
|
||||||
|
return selectedAmount.value
|
||||||
|
}
|
||||||
|
const amount = parseFloat(customAmount.value)
|
||||||
|
return isNaN(amount) || amount <= 0 ? 0 : amount
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查是否可以兑换
|
||||||
|
const canExchange = computed(() => {
|
||||||
|
return selectedExchangeAmount.value > 0 &&
|
||||||
|
agreementChecked.value &&
|
||||||
|
selectedExchangeAmount.value <= balance.value &&
|
||||||
|
!loading.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查自定义金额是否有效
|
||||||
|
const isCustomAmountValid = computed(() => {
|
||||||
|
const amount = parseFloat(customAmount.value)
|
||||||
|
if (isNaN(amount) || amount <= 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (amount < 100) {
|
||||||
|
customAmountError.value = '最小兑换金额为100元'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (amount > 10000) {
|
||||||
|
customAmountError.value = '单次最大兑换金额为10000元'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (amount > balance.value) {
|
||||||
|
customAmountError.value = '兑换金额不能超过余额'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
customAmountError.value = ''
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 格式化货币
|
||||||
|
const formatCurrency = (value) => {
|
||||||
|
return value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化算力点
|
||||||
|
const formatPoints = (value) => {
|
||||||
|
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择金额
|
||||||
|
const selectAmount = (amount) => {
|
||||||
|
selectedAmount.value = amount
|
||||||
|
customAmount.value = ''
|
||||||
|
customAmountError.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理自定义金额变化
|
||||||
|
const handleCustomAmountChange = () => {
|
||||||
|
selectedAmount.value = 0
|
||||||
|
if (customAmount.value) {
|
||||||
|
const amount = parseFloat(customAmount.value)
|
||||||
|
if (!isNaN(amount)) {
|
||||||
|
if (amount < 100) {
|
||||||
|
customAmountError.value = '最小兑换金额为100元'
|
||||||
|
} else if (amount > 10000) {
|
||||||
|
customAmountError.value = '单次最大兑换金额为10000元'
|
||||||
|
} else if (amount > balance.value) {
|
||||||
|
customAmountError.value = '兑换金额不能超过余额'
|
||||||
|
} else {
|
||||||
|
customAmountError.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
customAmountError.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认自定义金额
|
||||||
|
const handleCustomAmountConfirm = () => {
|
||||||
|
if (isCustomAmountValid.value) {
|
||||||
|
selectedAmount.value = 0
|
||||||
|
message.success(`已选择兑换¥${customAmount.value}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空选择
|
||||||
|
const clearSelection = () => {
|
||||||
|
selectedAmount.value = 0
|
||||||
|
customAmount.value = ''
|
||||||
|
customAmountError.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示用户协议模态框
|
||||||
|
const showAgreementModal = () => {
|
||||||
|
agreementModalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理协议确认
|
||||||
|
const handleAgreementConfirm = () => {
|
||||||
|
agreementChecked.value = true
|
||||||
|
agreementModalVisible.value = false
|
||||||
|
modalAgreementChecked.value = false
|
||||||
|
message.success('已确认用户协议')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理兑换
|
||||||
|
const handleExchange = () => {
|
||||||
|
showAgreementError.value = false
|
||||||
|
|
||||||
|
if (!agreementChecked.value) {
|
||||||
|
showAgreementError.value = true
|
||||||
|
message.error('请先阅读并确认用户协议')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedExchangeAmount.value > balance.value) {
|
||||||
|
message.error('兑换金额不能超过余额')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认兑换',
|
||||||
|
content: `您确定要兑换 ¥${selectedExchangeAmount.value} 获得 ${selectedExchangeAmount.value} 算力点吗?此操作不可撤销。`,
|
||||||
|
okText: '确认兑换',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: performExchange
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行兑换
|
||||||
|
const performExchange = () => {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
// 模拟API请求
|
||||||
|
setTimeout(() => {
|
||||||
|
// 更新余额和算力点
|
||||||
|
balance.value -= selectedExchangeAmount.value
|
||||||
|
points.value += selectedExchangeAmount.value
|
||||||
|
|
||||||
|
// 添加兑换记录
|
||||||
|
const now = new Date()
|
||||||
|
const timeStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
|
||||||
|
|
||||||
|
exchangeHistory.value.unshift({
|
||||||
|
key: exchangeHistory.value.length + 1,
|
||||||
|
amount: selectedExchangeAmount.value,
|
||||||
|
points: selectedExchangeAmount.value,
|
||||||
|
time: timeStr,
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重置选择
|
||||||
|
clearSelection()
|
||||||
|
agreementChecked.value = false
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
|
||||||
|
message.success(`兑换成功!获得 ${selectedExchangeAmount.value} 算力点`)
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理取消
|
||||||
|
const handleCancel = () => {
|
||||||
|
clearSelection()
|
||||||
|
agreementChecked.value = false
|
||||||
|
showAgreementError.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (timeStr) => {
|
||||||
|
return timeStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态颜色
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const colors = {
|
||||||
|
success: 'green',
|
||||||
|
pending: 'orange',
|
||||||
|
failed: 'red'
|
||||||
|
}
|
||||||
|
return colors[status] || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const texts = {
|
||||||
|
success: '成功',
|
||||||
|
pending: '处理中',
|
||||||
|
failed: '失败'
|
||||||
|
}
|
||||||
|
return texts[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化加载
|
||||||
|
onMounted(() => {
|
||||||
|
// 可以在这里加载用户余额和算力点数据
|
||||||
|
console.log('页面加载完成')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.points-exchange-page {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-description {
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 40px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-item {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: #f0f5ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-right: 12px;
|
||||||
|
color: #1890ff;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-value .currency {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #52c41a;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-value .points {
|
||||||
|
color: #722ed1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-value .points-unit {
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-tips {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #f6ffed;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #52c41a;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-tips :deep(.anticon) {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-content {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-section {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtitle {
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.amount-options {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-option {
|
||||||
|
height: auto !important;
|
||||||
|
padding: 16px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
border: 2px solid #f0f0f0 !important;
|
||||||
|
background: #fff !important;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-option:hover {
|
||||||
|
border-color: #1890ff !important;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-option.selected {
|
||||||
|
border-color: #1890ff !important;
|
||||||
|
background: #f0f5ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-option-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-number {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-points {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-amount {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-rules {
|
||||||
|
background: #fafafa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-item:nth-child(1) .rule-icon {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-item:nth-child(2) .rule-icon {
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-item:nth-child(3) .rule-icon {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-item:nth-child(4) .rule-icon {
|
||||||
|
color: #722ed1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-preview {
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-card {
|
||||||
|
background: #f6ffed;
|
||||||
|
border: 1px solid #b7eb8f;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.preview-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-label {
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-value {
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-value.points-value {
|
||||||
|
color: #722ed1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-section {
|
||||||
|
margin: 24px 0;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-link {
|
||||||
|
color: #1890ff;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-link:hover {
|
||||||
|
color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-button {
|
||||||
|
flex: 1;
|
||||||
|
height: 48px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-button {
|
||||||
|
width: 120px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-amount {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-points {
|
||||||
|
color: #722ed1;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-all-link {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-content {
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-content h3 {
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-content h4 {
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
margin: 16px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-content p {
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态 */
|
||||||
|
:deep(.ant-btn-loading) {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -354,7 +354,7 @@ const goToRecharge = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const goToBills = () => {
|
const goToBills = () => {
|
||||||
router.push('/bills')
|
router.push('/layout/admin/myMoneyDetail')
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToCoupons = () => {
|
const goToCoupons = () => {
|
||||||
@ -364,7 +364,7 @@ const goToCoupons = () => {
|
|||||||
const goToExchange = () => {
|
const goToExchange = () => {
|
||||||
message.info('跳转到兑换页面')
|
message.info('跳转到兑换页面')
|
||||||
// 这里应该实现跳转到兑换页面的逻辑
|
// 这里应该实现跳转到兑换页面的逻辑
|
||||||
// router.push('/exchange')
|
router.push('/layout/admin/exchange')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
544
src/views/admin/account/cost/myMoneyDetail/index.vue
Normal file
544
src/views/admin/account/cost/myMoneyDetail/index.vue
Normal file
@ -0,0 +1,544 @@
|
|||||||
|
<template>
|
||||||
|
<div class="expense-detail-page">
|
||||||
|
<!-- 面包屑导航 -->
|
||||||
|
<a-breadcrumb class="breadcrumb">
|
||||||
|
<a-breadcrumb-item>
|
||||||
|
<router-link to="/layout/admin/myMoney">费用总览</router-link>
|
||||||
|
</a-breadcrumb-item>
|
||||||
|
<a-breadcrumb-item>消费明细</a-breadcrumb-item>
|
||||||
|
</a-breadcrumb>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 筛选区域 -->
|
||||||
|
<div class="filter-section">
|
||||||
|
<a-card size="small" class="filter-card">
|
||||||
|
<a-form layout="inline" :model="filterForm">
|
||||||
|
<a-form-item label="交易时间">
|
||||||
|
<a-range-picker
|
||||||
|
v-model:value="filterForm.dateRange"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="收支类型">
|
||||||
|
<a-select
|
||||||
|
v-model:value="filterForm.incomeExpenseType"
|
||||||
|
placeholder="请选择"
|
||||||
|
style="width: 120px"
|
||||||
|
>
|
||||||
|
<a-select-option value="">全部</a-select-option>
|
||||||
|
<a-select-option value="income">收入</a-select-option>
|
||||||
|
<a-select-option value="expense">支出</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="交易类型">
|
||||||
|
<a-select
|
||||||
|
v-model:value="filterForm.transactionType"
|
||||||
|
placeholder="请选择"
|
||||||
|
style="width: 120px"
|
||||||
|
>
|
||||||
|
<a-select-option value="">全部</a-select-option>
|
||||||
|
<a-select-option
|
||||||
|
v-for="type in transactionTypeOptions"
|
||||||
|
:key="type.value"
|
||||||
|
:value="type.value"
|
||||||
|
>
|
||||||
|
{{ type.label }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<a-button type="primary" @click="handleSearch">
|
||||||
|
<template #icon><SearchOutlined /></template>
|
||||||
|
查询
|
||||||
|
</a-button>
|
||||||
|
<a-button style="margin-left: 8px" @click="handleReset">
|
||||||
|
<template #icon><RedoOutlined /></template>
|
||||||
|
重置
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格区域 -->
|
||||||
|
<div class="table-section">
|
||||||
|
<a-card>
|
||||||
|
<!-- 表格容器,实现滚动效果 -->
|
||||||
|
<div class="table-container">
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="tableData"
|
||||||
|
:pagination="pagination"
|
||||||
|
:scroll="{ x: 1500, y: 500 }"
|
||||||
|
:loading="loading"
|
||||||
|
@change="handleTableChange"
|
||||||
|
size="middle"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<!-- 流水号列 -->
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'serialNumber'">
|
||||||
|
<a-tag color="blue">{{ record.serialNumber }}</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 收支类型列 -->
|
||||||
|
<template v-else-if="column.key === 'incomeExpenseType'">
|
||||||
|
<a-tag :color="record.incomeExpenseType === 'income' ? 'green' : 'red'">
|
||||||
|
{{ record.incomeExpenseType === 'income' ? '收入' : '支出' }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 交易金额列 -->
|
||||||
|
<template v-else-if="column.key === 'amount'">
|
||||||
|
<span :class="record.incomeExpenseType === 'income' ? 'income-amount' : 'expense-amount'">
|
||||||
|
{{ record.incomeExpenseType === 'income' ? '+' : '-' }}{{ formatCurrency(record.amount) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 账户余额列 -->
|
||||||
|
<template v-else-if="column.key === 'balance'">
|
||||||
|
<span class="balance-amount">{{ formatCurrency(record.balance) }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 交易时间列 -->
|
||||||
|
<template v-else-if="column.key === 'transactionTime'">
|
||||||
|
{{ formatDateTime(record.transactionTime) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 交易渠道列 -->
|
||||||
|
<template v-else-if="column.key === 'channel'">
|
||||||
|
<a-tag :color="getChannelColor(record.channel)">
|
||||||
|
{{ getChannelLabel(record.channel) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计信息 -->
|
||||||
|
<div class="summary-info">
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">总收入:</span>
|
||||||
|
<span class="summary-value income">{{ formatCurrency(summary.totalIncome) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">总支出:</span>
|
||||||
|
<span class="summary-value expense">{{ formatCurrency(summary.totalExpense) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">净收入:</span>
|
||||||
|
<span class="summary-value net-income">{{ formatCurrency(summary.netIncome) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { SearchOutlined, RedoOutlined } from '@ant-design/icons-vue'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '流水号',
|
||||||
|
dataIndex: 'serialNumber',
|
||||||
|
key: 'serialNumber',
|
||||||
|
width: 150,
|
||||||
|
fixed: 'left'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '交易时间',
|
||||||
|
dataIndex: 'transactionTime',
|
||||||
|
key: 'transactionTime',
|
||||||
|
width: 180,
|
||||||
|
sorter: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '收支类型',
|
||||||
|
dataIndex: 'incomeExpenseType',
|
||||||
|
key: 'incomeExpenseType',
|
||||||
|
width: 120,
|
||||||
|
filters: [
|
||||||
|
{ text: '收入', value: 'income' },
|
||||||
|
{ text: '支出', value: 'expense' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '交易类型',
|
||||||
|
dataIndex: 'transactionType',
|
||||||
|
key: 'transactionType',
|
||||||
|
width: 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '交易渠道',
|
||||||
|
dataIndex: 'channel',
|
||||||
|
key: 'channel',
|
||||||
|
width: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '交易金额',
|
||||||
|
dataIndex: 'amount',
|
||||||
|
key: 'amount',
|
||||||
|
width: 150,
|
||||||
|
align: 'right',
|
||||||
|
sorter: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '账户余额',
|
||||||
|
dataIndex: 'balance',
|
||||||
|
key: 'balance',
|
||||||
|
width: 150,
|
||||||
|
align: 'right',
|
||||||
|
sorter: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '备注',
|
||||||
|
dataIndex: 'remark',
|
||||||
|
key: 'remark',
|
||||||
|
width: 200,
|
||||||
|
ellipsis: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 100,
|
||||||
|
fixed: 'right',
|
||||||
|
align: 'center',
|
||||||
|
slots: { customRender: 'action' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 交易类型选项
|
||||||
|
const transactionTypeOptions = [
|
||||||
|
{ value: 'shopping', label: '购物消费' },
|
||||||
|
{ value: 'transfer', label: '转账' },
|
||||||
|
{ value: 'withdraw', label: '提现' },
|
||||||
|
{ value: 'recharge', label: '充值' },
|
||||||
|
{ value: 'refund', label: '退款' },
|
||||||
|
{ value: 'salary', label: '工资收入' },
|
||||||
|
{ value: 'investment', label: '投资收益' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 交易渠道映射
|
||||||
|
const channelMap = {
|
||||||
|
'alipay': '支付宝',
|
||||||
|
'wechat': '微信支付',
|
||||||
|
'bank': '银行卡',
|
||||||
|
'cash': '现金',
|
||||||
|
'credit': '信用卡'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渠道颜色映射
|
||||||
|
const channelColorMap = {
|
||||||
|
'alipay': 'blue',
|
||||||
|
'wechat': 'green',
|
||||||
|
'bank': 'purple',
|
||||||
|
'cash': 'orange',
|
||||||
|
'credit': 'red'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选表单
|
||||||
|
const filterForm = reactive({
|
||||||
|
dateRange: [],
|
||||||
|
incomeExpenseType: '',
|
||||||
|
transactionType: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total) => `共 ${total} 条记录`,
|
||||||
|
pageSizeOptions: ['10', '20', '50', '100']
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref([])
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 统计信息
|
||||||
|
const summary = reactive({
|
||||||
|
totalIncome: 0,
|
||||||
|
totalExpense: 0,
|
||||||
|
netIncome: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生成模拟数据
|
||||||
|
const generateMockData = (count) => {
|
||||||
|
const mockData = []
|
||||||
|
const transactionTypes = ['shopping', 'transfer', 'withdraw', 'recharge', 'refund', 'salary', 'investment']
|
||||||
|
const channels = ['alipay', 'wechat', 'bank', 'cash', 'credit']
|
||||||
|
const incomeExpenseTypes = ['income', 'expense']
|
||||||
|
const remarks = [
|
||||||
|
'购物消费', '转账给朋友', 'ATM取现', '账户充值', '商品退款',
|
||||||
|
'工资收入', '理财产品收益', '餐饮消费', '交通出行', '生活缴费'
|
||||||
|
]
|
||||||
|
|
||||||
|
let balance = 10000
|
||||||
|
|
||||||
|
for (let i = 1; i <= count; i++) {
|
||||||
|
const incomeExpenseType = incomeExpenseTypes[Math.floor(Math.random() * incomeExpenseTypes.length)]
|
||||||
|
const amount = Math.floor(Math.random() * 5000) + 10
|
||||||
|
|
||||||
|
if (incomeExpenseType === 'income') {
|
||||||
|
balance += amount
|
||||||
|
} else {
|
||||||
|
balance -= amount
|
||||||
|
}
|
||||||
|
|
||||||
|
const transactionType = transactionTypes[Math.floor(Math.random() * transactionTypes.length)]
|
||||||
|
const transactionTypeLabel = transactionTypeOptions.find(item => item.value === transactionType)?.label || transactionType
|
||||||
|
|
||||||
|
mockData.push({
|
||||||
|
key: i,
|
||||||
|
serialNumber: `SN${String(i).padStart(8, '0')}`,
|
||||||
|
transactionTime: dayjs().subtract(Math.floor(Math.random() * 30), 'day').format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
incomeExpenseType,
|
||||||
|
transactionType: transactionTypeLabel,
|
||||||
|
channel: channels[Math.floor(Math.random() * channels.length)],
|
||||||
|
amount,
|
||||||
|
balance,
|
||||||
|
remark: remarks[Math.floor(Math.random() * remarks.length)]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockData
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算统计信息
|
||||||
|
const calculateSummary = (data) => {
|
||||||
|
let totalIncome = 0
|
||||||
|
let totalExpense = 0
|
||||||
|
|
||||||
|
data.forEach(item => {
|
||||||
|
if (item.incomeExpenseType === 'income') {
|
||||||
|
totalIncome += item.amount
|
||||||
|
} else {
|
||||||
|
totalExpense += item.amount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
summary.totalIncome = totalIncome
|
||||||
|
summary.totalExpense = totalExpense
|
||||||
|
summary.netIncome = totalIncome - totalExpense
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化货币
|
||||||
|
const formatCurrency = (value) => {
|
||||||
|
return `¥${value.toFixed(2)}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期时间
|
||||||
|
const formatDateTime = (dateTime) => {
|
||||||
|
return dayjs(dateTime).format('YYYY-MM-DD HH:mm')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取渠道标签
|
||||||
|
const getChannelLabel = (channel) => {
|
||||||
|
return channelMap[channel] || channel
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取渠道颜色
|
||||||
|
const getChannelColor = (channel) => {
|
||||||
|
return channelColorMap[channel] || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.current = 1
|
||||||
|
loadTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理重置
|
||||||
|
const handleReset = () => {
|
||||||
|
filterForm.dateRange = []
|
||||||
|
filterForm.incomeExpenseType = ''
|
||||||
|
filterForm.transactionType = ''
|
||||||
|
pagination.current = 1
|
||||||
|
loadTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理表格变化(分页、排序、筛选)
|
||||||
|
const handleTableChange = (pag, filters, sorter) => {
|
||||||
|
pagination.current = pag.current
|
||||||
|
pagination.pageSize = pag.pageSize
|
||||||
|
loadTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载表格数据
|
||||||
|
const loadTableData = () => {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
// 模拟API请求延迟
|
||||||
|
setTimeout(() => {
|
||||||
|
const allData = generateMockData(200)
|
||||||
|
|
||||||
|
// 模拟筛选逻辑
|
||||||
|
let filteredData = [...allData]
|
||||||
|
|
||||||
|
if (filterForm.incomeExpenseType) {
|
||||||
|
filteredData = filteredData.filter(item => item.incomeExpenseType === filterForm.incomeExpenseType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterForm.transactionType) {
|
||||||
|
filteredData = filteredData.filter(item => {
|
||||||
|
const transactionTypeValue = transactionTypeOptions.find(option => option.label === item.transactionType)?.value
|
||||||
|
return transactionTypeValue === filterForm.transactionType
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟分页
|
||||||
|
const startIndex = (pagination.current - 1) * pagination.pageSize
|
||||||
|
const endIndex = startIndex + pagination.pageSize
|
||||||
|
const pageData = filteredData.slice(startIndex, endIndex)
|
||||||
|
|
||||||
|
tableData.value = pageData
|
||||||
|
pagination.total = filteredData.length
|
||||||
|
|
||||||
|
// 计算统计信息(基于当前筛选条件下的所有数据)
|
||||||
|
calculateSummary(filteredData)
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadTableData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.expense-detail-page {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-description {
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-info {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value.income {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value.expense {
|
||||||
|
color: #f5222d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value.net-income {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.income-amount {
|
||||||
|
color: #52c41a;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expense-amount {
|
||||||
|
color: #f5222d;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-amount {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.expense-detail-page {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-info {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card :deep(.ant-form) {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card :deep(.ant-form-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -62,6 +62,7 @@ const menuItems: MenuItem[] = [
|
|||||||
children: [
|
children: [
|
||||||
{ path: '/layout/admin/myMoney', name: '费用总览', visible: true, disabled: false },
|
{ path: '/layout/admin/myMoney', name: '费用总览', visible: true, disabled: false },
|
||||||
// 消费明细设置为不可见
|
// 消费明细设置为不可见
|
||||||
|
{ path: '/layout/admin/exchange', name: '算力点兑换', visible: false, disabled: false },
|
||||||
{ path: '/layout/admin/myMoneyDetail', name: '消费明细', visible: false, disabled: false },
|
{ path: '/layout/admin/myMoneyDetail', name: '消费明细', visible: false, disabled: false },
|
||||||
{ path: '/layout/admin/myOrder', name: '我的订单', visible: true, disabled: false },
|
{ path: '/layout/admin/myOrder', name: '我的订单', visible: true, disabled: false },
|
||||||
{ path: '/layout/admin/flow', name: '账单明细', visible: true, disabled: false },
|
{ path: '/layout/admin/flow', name: '账单明细', visible: true, disabled: false },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user