This commit is contained in:
Leo_Ding 2025-12-30 15:10:30 +08:00
commit 0246b06729
6 changed files with 1430 additions and 432 deletions

BIN
src/assets/nav.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
src/assets/rognqishili.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

View File

@ -233,6 +233,18 @@ const routes: RouteRecordRaw[] = [
component: () =>
import("@/views/admin/account/cost/myOrder/index.vue"),
},
{
path: "myMoney",
name: "myMoney",
component: () =>
import("@/views/admin/account/cost/myMoney/index.vue"),
},
{
path: "myMoneyDetail",
name: "myMoneyDetail",
component: () =>
import("@/views/admin/account/cost/myMoneyDetail/index.vue"),
},
{
path: "voucher",
name: "voucher",

View File

@ -0,0 +1,681 @@
<template>
<div class="home-page">
<!-- 顶部资产卡片区域 -->
<a-row :gutter="24" class="asset-cards">
<!-- 左侧可用余额卡片 -->
<a-col :span="8">
<a-card :bordered="false" class="card balance-card">
<div class="fee-header">
<div class="fee-title">可用余额</div>
<a-button type="link" size="small" class="fee-titleb" @click="goToBills">查看消费明细</a-button>
</div>
<a-divider />
<div class="money">¥ {{ formatAmount(balance) }}</div>
<div class="money-btn">
<div><a-button type="primary" size="small" @click="goToRecharge">充值</a-button></div>
<div><a-button size="small">提现</a-button></div>
</div>
</a-card>
</a-col>
<!-- 右侧算力点和算力券卡片 -->
<a-col :span="16">
<!-- 算力点卡片 -->
<a-card title="算力点" :bordered="false" class="card computing-card">
<div class="computing-content">
<div class="computing-amount">
<span class="amount-value">{{ formatComputingPoints(computingPoints) }}</span>
<span class="amount-unit"></span>
</div>
<div class="computing-actions">
<a-button type="primary" size="small" @click="goToExchange">去兑换</a-button>
</div>
</div>
</a-card>
<!-- 算力券卡片 -->
<a-card title="算力券" :bordered="false" class="card coupon-card">
<div class="coupon-content">
<div class="coupon-amount">
<span class="amount-value">{{ availableCoupons }}</span>
<span class="amount-unit"></span>
</div>
<div class="coupon-actions">
<a-button type="primary" size="small" @click="goToCoupons">去查看</a-button>
</div>
</div>
</a-card>
</a-col>
</a-row>
<!-- 底部账单区域 -->
<div class="bill-section">
<div class="bill-header">
<h3 class="bill-title">账单明细</h3>
<div class="date-range">
<span class="date-label">日期范围</span>
<a-range-picker
v-model:value="dateRange"
:placeholder="['开始时间', '结束时间']"
@change="handleDateChange"
style="width: 250px;"
/>
</div>
</div>
<!-- 账单表格 -->
<a-table
:columns="columns"
:data-source="billData"
:pagination="pagination"
@change="handleTableChange"
:loading="loading"
class="bill-table"
:scroll="{ x: 1200 }"
>
<!-- 流水号列 -->
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'serialNumber'">
<span class="serial-number">{{ record.serialNumber }}</span>
</template>
<!-- 交易类型列 -->
<template v-else-if="column.key === 'transactionType'">
<a-tag :color="getTransactionTypeColor(record.transactionType)">
{{ record.transactionType }}
</a-tag>
</template>
<!-- 金额相关列 -->
<template v-else-if="['transactionAmount', 'originalPrice', 'discountAmount', 'balancePayment', 'voucherDeduction'].includes(column.key)">
<span :class="{
'amount-positive': record[column.key] > 0,
'amount-negative': record[column.key] < 0
}">
¥ {{ formatAmount(Math.abs(record[column.key])) }}
</span>
</template>
</template>
</a-table>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import {
message,
TableProps
} from 'ant-design-vue'
import type { Dayjs } from 'dayjs'
import router from '@/router'
//
const balance = ref<number>(5.00)
const computingPoints = ref<number>(1023)
const availableCoupons = ref<number>(3)
//
const dateRange = ref<[Dayjs, Dayjs]>()
//
interface BillRecord {
key: string
serialNumber: string
transactionTime: string
transactionType: string
productName: string
transactionAmount: number
originalPrice: number
discountAmount: number
balancePayment: number
voucherDeduction: number
}
const loading = ref(false)
const billData = ref<BillRecord[]>([])
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `${total} 条记录`
})
//
const columns = computed(() => [
{
title: '流水号',
dataIndex: 'serialNumber',
key: 'serialNumber',
width: 180,
ellipsis: true
},
{
title: '交易时间',
dataIndex: 'transactionTime',
key: 'transactionTime',
width: 170,
sorter: true
},
{
title: '交易类型',
dataIndex: 'transactionType',
key: 'transactionType',
width: 120,
filters: [
{ text: '充值', value: '充值' },
{ text: '消费', value: '消费' },
{ text: '提现', value: '提现' },
{ text: '退款', value: '退款' }
]
},
{
title: '产品名称',
dataIndex: 'productName',
key: 'productName',
width: 150,
ellipsis: true
},
{
title: '交易金额',
dataIndex: 'transactionAmount',
key: 'transactionAmount',
width: 120,
align: 'right'
},
{
title: '原价',
dataIndex: 'originalPrice',
key: 'originalPrice',
width: 120,
align: 'right'
},
{
title: '优惠金额',
dataIndex: 'discountAmount',
key: 'discountAmount',
width: 120,
align: 'right'
},
{
title: '余额支付',
dataIndex: 'balancePayment',
key: 'balancePayment',
width: 120,
align: 'right'
},
{
title: '代金券抵扣',
dataIndex: 'voucherDeduction',
key: 'voucherDeduction',
width: 120,
align: 'right'
}
])
//
onMounted(() => {
fetchBillData()
})
//
const formatAmount = (amount: number): string => {
return amount.toFixed(2)
}
//
const formatComputingPoints = (points: number): string => {
return points.toLocaleString()
}
//
const getTransactionTypeColor = (type: string): string => {
const colorMap: Record<string, string> = {
'充值': 'green',
'消费': 'blue',
'提现': 'orange',
'退款': 'purple'
}
return colorMap[type] || 'default'
}
//
const handleDateChange = (dates: [Dayjs, Dayjs] | null) => {
if (dates) {
console.log('选择的日期范围:', dates)
//
fetchBillData()
}
}
//
const handleTableChange: TableProps['onChange'] = (pag, filters, sorter) => {
pagination.value.current = pag.current!
pagination.value.pageSize = pag.pageSize!
//
fetchBillData()
}
//
const fetchBillData = () => {
loading.value = true
// API
setTimeout(() => {
//
const mockData: BillRecord[] = [
{
key: '1',
serialNumber: 'TX202401010001',
transactionTime: '2024-01-01 10:30:25',
transactionType: '充值',
productName: '账户充值',
transactionAmount: 100.00,
originalPrice: 100.00,
discountAmount: 0.00,
balancePayment: 0.00,
voucherDeduction: 0.00
},
{
key: '2',
serialNumber: 'TX202401010002',
transactionTime: '2024-01-01 14:20:15',
transactionType: '消费',
productName: 'GPU算力服务',
transactionAmount: -50.00,
originalPrice: 60.00,
discountAmount: 10.00,
balancePayment: 40.00,
voucherDeduction: 10.00
},
{
key: '3',
serialNumber: 'TX202401020001',
transactionTime: '2024-01-02 09:15:30',
transactionType: '充值',
productName: '账户充值',
transactionAmount: 200.00,
originalPrice: 200.00,
discountAmount: 0.00,
balancePayment: 0.00,
voucherDeduction: 0.00
},
{
key: '4',
serialNumber: 'TX202401030001',
transactionTime: '2024-01-03 16:45:20',
transactionType: '消费',
productName: '存储空间',
transactionAmount: -30.00,
originalPrice: 30.00,
discountAmount: 0.00,
balancePayment: 30.00,
voucherDeduction: 0.00
},
{
key: '5',
serialNumber: 'TX202401040001',
transactionTime: '2024-01-04 11:10:05',
transactionType: '提现',
productName: '余额提现',
transactionAmount: -100.00,
originalPrice: 100.00,
discountAmount: 0.00,
balancePayment: 100.00,
voucherDeduction: 0.00
},
{
key: '6',
serialNumber: 'TX202401050001',
transactionTime: '2024-01-05 15:30:40',
transactionType: '退款',
productName: 'GPU算力服务',
transactionAmount: 25.00,
originalPrice: 25.00,
discountAmount: 0.00,
balancePayment: 0.00,
voucherDeduction: 0.00
}
]
billData.value = mockData
pagination.value.total = mockData.length
loading.value = false
}, 500)
}
//
const goToRecharge = () => {
router.push('/recharge')
}
const goToBills = () => {
router.push('/bills')
}
const goToCoupons = () => {
router.push('/rights')
}
const goToExchange = () => {
message.info('跳转到兑换页面')
//
// router.push('/exchange')
}
</script>
<style lang="scss" scoped>
.home-page {
padding: 24px;
background: #f0f2f5;
min-height: 100vh;
}
.asset-cards {
margin-bottom: 24px;
.ant-col-8, .ant-col-16 {
display: flex;
flex-direction: column;
}
}
.card {
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
background: #fff;
border: 1px solid #e8e8e8;
height: 100%;
:deep(.ant-card-head) {
border-bottom: 1px solid #f0f0f0;
min-height: 48px;
background: #fafafa;
.ant-card-head-title {
font-size: 16px;
font-weight: 600;
padding: 12px 0;
color: #333;
}
}
:deep(.ant-card-body) {
padding: 24px;
}
}
//
.balance-card {
.fee-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.fee-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.fee-titleb {
font-size: 12px;
color: #1890ff;
padding: 0;
height: auto;
}
:deep(.ant-divider) {
margin: 16px 0;
}
.money {
font-size: 32px;
font-weight: 700;
color: #1890ff;
text-align: center;
margin: 20px 0 30px;
line-height: 1;
}
.money-btn {
display: flex;
justify-content: center;
gap: 16px;
div {
flex: 1;
text-align: center;
:deep(.ant-btn) {
width: 100px;
height: 36px;
border-radius: 4px;
font-weight: 500;
&.ant-btn-primary {
background: #1890ff;
border-color: #1890ff;
&:hover {
background: #40a9ff;
border-color: #40a9ff;
}
}
&:not(.ant-btn-primary) {
color: #666;
border-color: #d9d9d9;
&:hover {
color: #1890ff;
border-color: #1890ff;
}
}
}
}
}
}
//
.computing-card, .coupon-card {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.computing-content, .coupon-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px 0;
.amount-value {
font-size: 36px;
font-weight: 600;
color: #1890ff; //
line-height: 1;
margin-right: 8px;
}
.amount-unit {
font-size: 18px;
color: #666;
font-weight: 500;
}
.computing-actions, .coupon-actions {
margin-top: 24px;
:deep(.ant-btn) {
width: 100px;
height: 32px;
border-radius: 4px;
font-weight: 500;
&.ant-btn-primary {
background: #1890ff;
border-color: #1890ff;
&:hover {
background: #40a9ff;
border-color: #40a9ff;
}
}
}
}
}
//
.bill-section {
background: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
border: 1px solid #e8e8e8;
}
.bill-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
.bill-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
.date-range {
display: flex;
align-items: center;
.date-label {
font-size: 14px;
color: #666;
margin-right: 12px;
}
:deep(.ant-picker) {
border-radius: 4px;
border-color: #d9d9d9;
&:hover {
border-color: #40a9ff;
}
&.ant-picker-focused {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
}
}
.bill-table {
:deep(.ant-table) {
border-radius: 6px;
overflow: hidden;
border: 1px solid #f0f0f0;
.ant-table-thead > tr > th {
background: #fafafa;
font-weight: 600;
color: #333;
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
&:not(:last-child) {
border-right: 1px solid #f0f0f0;
}
}
.ant-table-tbody > tr > td {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
&:not(:last-child) {
border-right: 1px solid #f0f0f0;
}
&:hover {
background: #fafafa;
}
}
.ant-table-tbody > tr:last-child > td {
border-bottom: none;
}
}
.serial-number {
font-family: 'Courier New', monospace;
color: #1890ff;
font-weight: 500;
}
.amount-positive {
color: #52c41a;
font-weight: 500;
}
.amount-negative {
color: #ff4d4f;
font-weight: 500;
}
}
//
@media (max-width: 768px) {
.home-page {
padding: 12px;
}
.asset-cards {
.ant-col-8, .ant-col-16 {
width: 100%;
}
.ant-col-8 {
margin-bottom: 16px;
}
}
.bill-header {
flex-direction: column;
align-items: stretch;
gap: 16px;
.date-range {
justify-content: space-between;
:deep(.ant-picker) {
width: 100%;
}
}
}
.money {
font-size: 28px;
}
.computing-content, .coupon-content {
.amount-value {
font-size: 32px;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -27,16 +27,16 @@ import { h, reactive, computed } from 'vue';
import { useRouter } from 'vue-router';
import {
HomeOutlined,
FolderOpenOutlined,
ConsoleSqlOutlined,
GlobalOutlined,
LaptopOutlined,
MoneyCollectOutlined,
TeamOutlined,
AppstoreAddOutlined
} from '@ant-design/icons-vue';
import type { MenuMode, MenuTheme } from 'ant-design-vue';
import { ItemType } from 'ant-design-vue';
// ItemType 使 Ant Design Vue MenuItem
import type { MenuProps } from 'ant-design-vue';
const router = useRouter();
@ -45,38 +45,40 @@ interface MenuItem {
path: string;
name: string;
icon?: any;
disabled?: boolean; //
children?: Omit<MenuItem, 'icon'>[]; // icon
disabled?: boolean;
visible?: boolean;
children?: Omit<MenuItem, 'icon'>[];
}
const menuItems: MenuItem[] = [
// { path: '/layout/admin/home', name: '', icon: HomeOutlined },
{ path: '/layout/admin/instance', name: '容器实例', icon: ConsoleSqlOutlined },
// { path: '/layout/admin/fileStore', name: '', icon: FolderOpenOutlined },
{ path: '/layout/admin/image', name: '镜像', icon: GlobalOutlined },
// { path: '/layout/publicData', name: '', icon: LaptopOutlined },
{ path: '/layout/admin/home', name: '总览', icon: HomeOutlined, visible: true },
{ path: '/layout/admin/instance', name: '容器实例', icon: ConsoleSqlOutlined, visible: true },
{ path: '/layout/admin/image', name: '镜像', icon: GlobalOutlined, visible: true },
{
path: '',
name: '费用',
icon: MoneyCollectOutlined,
visible: true,
children: [
// { path: '/layout/admin/costDetail', name: '' },
{ path: '/layout/admin/myOrder', name: '我的订单' },
{ path: '/layout/admin/flow', name: '账单明细' },
{ path: '/layout/admin/coupon', name: '优惠券(待开发)', disabled: true },
{ path: '/layout/admin/invoice', name: '发票(待开发)', disabled: true },
{ path: '/layout/admin/voucher', name: '代金券(待开发)', disabled: true },
{ path: '/layout/admin/contract', name: '合同(待开发)', disabled: true },
{ path: '/layout/admin/myMoney', name: '费用总览', visible: true, disabled: false },
//
{ path: '/layout/admin/myMoneyDetail', name: '消费明细', visible: false, disabled: false },
{ path: '/layout/admin/myOrder', name: '我的订单', visible: true, disabled: false },
{ path: '/layout/admin/flow', name: '账单明细', visible: true, disabled: false },
{ path: '/layout/admin/coupon', name: '优惠券(待开发)', disabled: true, visible: true },
{ path: '/layout/admin/invoice', name: '发票(待开发)', disabled: true, visible: true },
{ path: '/layout/admin/voucher', name: '代金券(待开发)', disabled: true, visible: true },
{ path: '/layout/admin/contract', name: '合同(待开发)', disabled: true, visible: true },
],
},
{
path: '',
name: '账号',
icon: TeamOutlined,
visible: true,
children: [
{ path: '/layout/admin/security', name: '账号安全' },
{ path: '/layout/admin/history', name: '访问记录' },
// { path: '/controlPanel/security', name: '' },
{ path: '/layout/admin/security', name: '账号安全', visible: true },
{ path: '/layout/admin/history', name: '访问记录', visible: true },
],
},
];
@ -89,60 +91,111 @@ const state = reactive({
openKeys: ['/controlPanel/fee', '/controlPanel/account'],
});
// 使 MenuProps['items']
// MenuItemType
type MenuItemType = NonNullable<MenuProps['items']>[number];
//
function getItem(
label: string,
key: string,
icon?: any,
children?: ItemType[],
children?: MenuItemType[],
type?: 'group',
disabled?: boolean // disabled
): ItemType {
disabled?: boolean
): MenuItemType | null {
return {
key,
icon,
children,
label,
type,
disabled, // disabled
} as ItemType;
disabled,
} as MenuItemType;
}
// menuItems a-menu items
const items = computed(() => {
return menuItems.map((item) => {
return menuItems
.filter(item => item.visible !== false)
.map((item) => {
if (item.children && item.children.length > 0) {
const childItems = item.children.map((child) =>
getItem(child.name, child.path, undefined, undefined, undefined, child.disabled)
// visible false
const childItems = item.children
.filter(child => child.visible !== false)
.map((child) =>
getItem(
child.name,
child.path,
undefined,
undefined,
undefined,
child.disabled
)
)
.filter(Boolean) as MenuItemType[];
//
if (childItems.length === 0) {
return getItem(
item.name,
item.path,
h(item.icon),
undefined, //
undefined,
item.disabled
);
return getItem(item.name, item.path, h(item.icon), childItems, undefined, item.disabled);
} else {
return getItem(item.name, item.path, h(item.icon), undefined, undefined, item.disabled);
}
});
return getItem(
item.name,
item.path,
h(item.icon),
childItems,
undefined,
item.disabled
);
} else {
return getItem(
item.name,
item.path,
h(item.icon),
undefined,
undefined,
item.disabled
);
}
})
.filter(Boolean) as MenuItemType[];
});
const handleMenuSelect = ({ key }: { key: string }) => {
// a-menu select
const targetItem = findMenuItemByKey(menuItems, key);
if (targetItem?.disabled) {
//
const allItems = flattenMenuItems(menuItems);
const targetItem = allItems.find(item => item.path === key);
//
if (targetItem?.disabled || targetItem?.visible === false) {
return;
}
router.push(key);
};
//
function findMenuItemByKey(items: MenuItem[], key: string): MenuItem | undefined {
for (const item of items) {
if (item.path === key) return item;
//
function flattenMenuItems(items: MenuItem[]): MenuItem[] {
let result: MenuItem[] = [];
items.forEach(item => {
result.push(item);
if (item.children) {
const found = findMenuItemByKey(item.children, key);
if (found) return found;
result = result.concat(flattenMenuItems(item.children));
}
}
return undefined;
});
return result;
}
//
const changeMode = (checked: boolean) => {
state.mode = checked ? 'vertical' : 'inline';
};