This commit is contained in:
qiuyuan 2025-12-30 15:46:26 +08:00
parent 98ccb19fbe
commit f2b2ab6b9e
5 changed files with 1448 additions and 2 deletions

View File

@ -201,6 +201,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",

View 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>

View File

@ -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>

View 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>

View File

@ -56,6 +56,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 },