Merge branch 'main' of https://gitlab.guxuan.icu/Leo_Ding/GPU_Web
This commit is contained in:
commit
4247e292c5
@ -251,12 +251,25 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () =>
|
component: () =>
|
||||||
import("@/views/admin/account/cost/myOrder/index.vue"),
|
import("@/views/admin/account/cost/myOrder/index.vue"),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: "balance",
|
||||||
|
name: "balance",
|
||||||
|
component: () =>
|
||||||
|
import("@/views/admin/account/cost/balance/index.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "myMoney",
|
path: "myMoney",
|
||||||
name: "myMoney",
|
name: "myMoney",
|
||||||
component: () =>
|
component: () =>
|
||||||
import("@/views/admin/account/cost/myMoney/index.vue"),
|
import("@/views/admin/account/cost/myMoney/index.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "deposit",
|
||||||
|
name: "deposit",
|
||||||
|
component: () =>
|
||||||
|
import("@/views/admin/account/cost/deposit/index.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "exchange",
|
path: "exchange",
|
||||||
name: "exchange",
|
name: "exchange",
|
||||||
|
|||||||
830
src/views/admin/account/cost/balance/index.vue
Normal file
830
src/views/admin/account/cost/balance/index.vue
Normal file
@ -0,0 +1,830 @@
|
|||||||
|
<template>
|
||||||
|
<div class="recharge-page">
|
||||||
|
<!-- 面包屑导航 -->
|
||||||
|
<a-breadcrumb class="breadcrumb">
|
||||||
|
<a-breadcrumb-item>
|
||||||
|
<router-link to="/">首页</router-link>
|
||||||
|
</a-breadcrumb-item>
|
||||||
|
<a-breadcrumb-item>我的余额</a-breadcrumb-item>
|
||||||
|
</a-breadcrumb>
|
||||||
|
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">我的余额</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- 余额和充值部分 -->
|
||||||
|
<div class="balance-section">
|
||||||
|
<!-- 余额卡片(带充值方式) -->
|
||||||
|
<a-card class="balance-card" :bordered="false">
|
||||||
|
<div class="balance-header">
|
||||||
|
<div class="balance-info">
|
||||||
|
<div class="balance-amount">
|
||||||
|
<div class="amount-label">账户余额</div>
|
||||||
|
<div class="amount-value">¥{{ formatCurrency(balance) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="recharge-content">
|
||||||
|
<!-- 充值金额选择 -->
|
||||||
|
<div class="recharge-section">
|
||||||
|
<h3 class="section-title">充值金额(¥)</h3>
|
||||||
|
<div class="amount-options">
|
||||||
|
<div
|
||||||
|
v-for="option in amountOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:class="['amount-option', { 'selected': selectedAmount === option.value }]"
|
||||||
|
@click="selectAmount(option.value)"
|
||||||
|
>
|
||||||
|
<div class="amount-number">¥{{ option.value }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自定义金额输入 -->
|
||||||
|
<div class="recharge-section">
|
||||||
|
<h3 class="section-title">自定义充值金额</h3>
|
||||||
|
<div class="custom-amount">
|
||||||
|
<a-input
|
||||||
|
v-model:value="customAmount"
|
||||||
|
placeholder="请输入金额"
|
||||||
|
size="large"
|
||||||
|
:disabled="loading"
|
||||||
|
@change="handleCustomAmountChange"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<span class="input-prefix">¥</span>
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
<div v-if="customAmountError" class="error-text">
|
||||||
|
{{ customAmountError }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 充值方式 -->
|
||||||
|
<div class="payment-methods-container">
|
||||||
|
<div class="payment-methods-title">充值方式</div>
|
||||||
|
<div class="payment-methods-row">
|
||||||
|
<div
|
||||||
|
v-for="method in paymentMethods"
|
||||||
|
:key="method.value"
|
||||||
|
:class="['payment-method-item', { 'selected': selectedPaymentMethod === method.value }]"
|
||||||
|
@click="selectPaymentMethod(method.value)"
|
||||||
|
>
|
||||||
|
<div class="method-icon">
|
||||||
|
<component :is="method.icon" />
|
||||||
|
</div>
|
||||||
|
<div class="method-info">
|
||||||
|
<div class="method-name">{{ method.name }}</div>
|
||||||
|
<div v-if="method.description" class="method-desc">{{ method.description }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedPaymentMethod === method.value" class="method-selected">
|
||||||
|
<CheckCircleFilled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-section">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="!canRecharge"
|
||||||
|
@click="handleRecharge"
|
||||||
|
class="recharge-btn"
|
||||||
|
>
|
||||||
|
立即充值
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 充值记录 -->
|
||||||
|
<a-card class="records-card" title="充值记录">
|
||||||
|
<!-- 筛选条件 -->
|
||||||
|
<div class="filter-section">
|
||||||
|
<a-range-picker
|
||||||
|
v-model:value="dateRange"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
placeholder="开始时间 - 结束时间"
|
||||||
|
style="width: 300px; margin-right: 16px;"
|
||||||
|
/>
|
||||||
|
<a-button type="primary" @click="handleSearch" :loading="searchLoading">
|
||||||
|
<template #icon><SearchOutlined /></template>
|
||||||
|
查询
|
||||||
|
</a-button>
|
||||||
|
<a-button style="margin-left: 8px" @click="handleReset">
|
||||||
|
重置
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 记录表格 -->
|
||||||
|
<div class="records-table">
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="rechargeRecords"
|
||||||
|
:pagination="pagination"
|
||||||
|
:loading="tableLoading"
|
||||||
|
@change="handleTableChange"
|
||||||
|
size="middle"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'amount'">
|
||||||
|
<span class="amount-cell">¥{{ record.amount }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'rechargeTime'">
|
||||||
|
{{ formatDateTime(record.rechargeTime) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'paymentMethod'">
|
||||||
|
<a-tag :color="getPaymentMethodColor(record.paymentMethod)">
|
||||||
|
{{ getPaymentMethodLabel(record.paymentMethod) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'accountBalance'">
|
||||||
|
¥{{ formatCurrency(record.accountBalance) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'status'">
|
||||||
|
<a-tag :color="getStatusColor(record.status)">
|
||||||
|
{{ getStatusLabel(record.status) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'action'">
|
||||||
|
<a-button type="link" size="small" @click="viewDetail(record)">详情</a-button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #emptyText>
|
||||||
|
<div class="empty-state">
|
||||||
|
<a-empty description="暂无充值记录" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 充值确认模态框 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="rechargeModalVisible"
|
||||||
|
title="确认充值"
|
||||||
|
:confirm-loading="modalLoading"
|
||||||
|
@ok="handleRechargeConfirm"
|
||||||
|
@cancel="handleRechargeCancel"
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<div class="recharge-confirm">
|
||||||
|
<div class="confirm-item">
|
||||||
|
<span class="confirm-label">充值金额:</span>
|
||||||
|
<span class="confirm-value">¥{{ selectedRechargeAmount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="confirm-item">
|
||||||
|
<span class="confirm-label">充值方式:</span>
|
||||||
|
<span class="confirm-value">{{ getPaymentMethodLabel(selectedPaymentMethod) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="confirm-item">
|
||||||
|
<span class="confirm-label">当前余额:</span>
|
||||||
|
<span class="confirm-value">¥{{ formatCurrency(balance) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="confirm-item">
|
||||||
|
<span class="confirm-label">充值后余额:</span>
|
||||||
|
<span class="confirm-value highlight">¥{{ formatCurrency(balance + selectedRechargeAmount) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="confirm-tips">
|
||||||
|
<InfoCircleOutlined />
|
||||||
|
充值成功后,金额将立即到账
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
CheckCircleFilled,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
AlipayOutlined,
|
||||||
|
WechatOutlined,
|
||||||
|
BankOutlined
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
// 用户余额
|
||||||
|
const balance = ref(5.00)
|
||||||
|
|
||||||
|
// 充值金额选项
|
||||||
|
const amountOptions = ref([
|
||||||
|
{ value: 100, label: '100元' },
|
||||||
|
{ value: 500, label: '500元' },
|
||||||
|
{ value: 1000, label: '1000元' },
|
||||||
|
{ value: 5000, label: '5000元' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const selectedAmount = ref(0)
|
||||||
|
const customAmount = ref('')
|
||||||
|
const customAmountError = ref('')
|
||||||
|
|
||||||
|
// 充值方式
|
||||||
|
const paymentMethods = ref([
|
||||||
|
{ value: 'alipay', name: '支付宝支付', icon: AlipayOutlined, color: '#1677ff' },
|
||||||
|
{ value: 'wechat', name: '微信支付', icon: WechatOutlined, color: '#07c160' },
|
||||||
|
{ value: 'bank', name: '对公付款', icon: BankOutlined, color: '#722ed1' }
|
||||||
|
])
|
||||||
|
const selectedPaymentMethod = ref('alipay')
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false)
|
||||||
|
const tableLoading = ref(false)
|
||||||
|
const searchLoading = ref(false)
|
||||||
|
const modalLoading = ref(false)
|
||||||
|
|
||||||
|
// 模态框
|
||||||
|
const rechargeModalVisible = ref(false)
|
||||||
|
|
||||||
|
// 筛选条件
|
||||||
|
const dateRange = ref([])
|
||||||
|
|
||||||
|
// 表格数据
|
||||||
|
const rechargeRecords = ref([])
|
||||||
|
|
||||||
|
// 表格列
|
||||||
|
const columns = ref([
|
||||||
|
{ title: '充值金额', dataIndex: 'amount', key: 'amount', align: 'right', width: 120 },
|
||||||
|
{ title: '充值时间', dataIndex: 'rechargeTime', key: 'rechargeTime', width: 180, sorter: true },
|
||||||
|
{ title: '支付方式', dataIndex: 'paymentMethod', key: 'paymentMethod', width: 120 },
|
||||||
|
{ title: '账户余额', dataIndex: 'accountBalance', key: 'accountBalance', align: 'right', width: 140 },
|
||||||
|
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
||||||
|
{ title: '操作', key: 'action', width: 80, align: 'center' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total) => `共 ${total} 条记录`,
|
||||||
|
pageSizeOptions: ['10', '20', '50', '100']
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选中金额(优先自定义)
|
||||||
|
const selectedRechargeAmount = computed(() => {
|
||||||
|
if (selectedAmount.value > 0) return selectedAmount.value
|
||||||
|
const amount = parseFloat(customAmount.value)
|
||||||
|
return isNaN(amount) || amount <= 0 ? 0 : amount
|
||||||
|
})
|
||||||
|
|
||||||
|
const canRecharge = computed(() => {
|
||||||
|
return selectedRechargeAmount.value > 0 && selectedPaymentMethod.value && !loading.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// 格式化货币(带千分位)
|
||||||
|
const formatCurrency = (value) => {
|
||||||
|
return parseFloat(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDateTime = (dateTime) => {
|
||||||
|
return dayjs(dateTime).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择预设金额
|
||||||
|
const selectAmount = (amount) => {
|
||||||
|
selectedAmount.value = amount
|
||||||
|
customAmount.value = ''
|
||||||
|
customAmountError.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义金额校验
|
||||||
|
const handleCustomAmountChange = () => {
|
||||||
|
selectedAmount.value = 0
|
||||||
|
const val = customAmount.value.trim()
|
||||||
|
if (!val) {
|
||||||
|
customAmountError.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const amount = parseFloat(val)
|
||||||
|
if (isNaN(amount) || amount <= 0) {
|
||||||
|
customAmountError.value = '请输入有效金额'
|
||||||
|
} else if (amount < 1) {
|
||||||
|
customAmountError.value = '充值金额不能小于1元'
|
||||||
|
} else if (amount > 50000) {
|
||||||
|
customAmountError.value = '单次最大充值金额为50000元'
|
||||||
|
} else {
|
||||||
|
customAmountError.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择支付方式
|
||||||
|
const selectPaymentMethod = (method) => {
|
||||||
|
selectedPaymentMethod.value = method
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付方式标签 & 颜色
|
||||||
|
const getPaymentMethodLabel = (method) => {
|
||||||
|
const payment = paymentMethods.value.find(m => m.value === method)
|
||||||
|
return payment ? payment.name : method
|
||||||
|
}
|
||||||
|
const getPaymentMethodColor = (method) => {
|
||||||
|
const payment = paymentMethods.value.find(m => m.value === method)
|
||||||
|
return payment ? payment.color : 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态标签 & 颜色
|
||||||
|
const getStatusLabel = (status) => {
|
||||||
|
const map = { success: '成功', processing: '处理中', failed: '失败', pending: '待支付' }
|
||||||
|
return map[status] || status
|
||||||
|
}
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const map = { success: 'green', processing: 'blue', failed: 'red', pending: 'orange' }
|
||||||
|
return map[status] || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 充值主流程
|
||||||
|
const handleRecharge = () => {
|
||||||
|
if (selectedRechargeAmount.value <= 0) return message.error('请选择或输入充值金额')
|
||||||
|
if (!selectedPaymentMethod.value) return message.error('请选择支付方式')
|
||||||
|
rechargeModalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认充值
|
||||||
|
const handleRechargeConfirm = () => {
|
||||||
|
modalLoading.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
balance.value += selectedRechargeAmount.value
|
||||||
|
const now = new Date()
|
||||||
|
const newRecord = {
|
||||||
|
key: rechargeRecords.value.length + 1,
|
||||||
|
amount: selectedRechargeAmount.value,
|
||||||
|
rechargeTime: now,
|
||||||
|
paymentMethod: selectedPaymentMethod.value,
|
||||||
|
accountBalance: balance.value,
|
||||||
|
status: 'success'
|
||||||
|
}
|
||||||
|
rechargeRecords.value.unshift(newRecord)
|
||||||
|
pagination.total += 1
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
selectedAmount.value = 0
|
||||||
|
customAmount.value = ''
|
||||||
|
customAmountError.value = ''
|
||||||
|
|
||||||
|
modalLoading.value = false
|
||||||
|
rechargeModalVisible.value = false
|
||||||
|
message.success(`充值成功!¥${selectedRechargeAmount.value} 已到账`)
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRechargeCancel = () => {
|
||||||
|
rechargeModalVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 充值记录查询
|
||||||
|
const handleSearch = () => {
|
||||||
|
searchLoading.value = true
|
||||||
|
pagination.current = 1
|
||||||
|
loadRechargeRecords()
|
||||||
|
}
|
||||||
|
const handleReset = () => {
|
||||||
|
dateRange.value = []
|
||||||
|
pagination.current = 1
|
||||||
|
loadRechargeRecords()
|
||||||
|
}
|
||||||
|
const handleTableChange = (pag) => {
|
||||||
|
pagination.current = pag.current
|
||||||
|
pagination.pageSize = pag.pageSize
|
||||||
|
loadRechargeRecords()
|
||||||
|
}
|
||||||
|
const viewDetail = (record) => {
|
||||||
|
message.info(`充值¥${record.amount},时间:${formatDateTime(record.rechargeTime)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载记录(含模拟数据)
|
||||||
|
const loadRechargeRecords = () => {
|
||||||
|
tableLoading.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
const mockData = generateMockData(35)
|
||||||
|
let filtered = [...mockData]
|
||||||
|
|
||||||
|
if (dateRange.value?.length === 2) {
|
||||||
|
const start = dayjs(dateRange.value[0]).startOf('day')
|
||||||
|
const end = dayjs(dateRange.value[1]).endOf('day')
|
||||||
|
filtered = filtered.filter(r => {
|
||||||
|
const d = dayjs(r.rechargeTime)
|
||||||
|
return d.isAfter(start) && d.isBefore(end)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const startIdx = (pagination.current - 1) * pagination.pageSize
|
||||||
|
rechargeRecords.value = filtered.slice(startIdx, startIdx + pagination.pageSize)
|
||||||
|
pagination.total = filtered.length
|
||||||
|
|
||||||
|
tableLoading.value = false
|
||||||
|
searchLoading.value = false
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成模拟数据(无算力点)
|
||||||
|
const generateMockData = (count) => {
|
||||||
|
const methods = ['alipay', 'wechat', 'bank']
|
||||||
|
const statuses = ['success', 'processing', 'failed', 'pending']
|
||||||
|
let currentBalance = 5.00
|
||||||
|
const data = []
|
||||||
|
|
||||||
|
for (let i = 1; i <= count; i++) {
|
||||||
|
const amount = [100, 500, 1000, 5000][Math.floor(Math.random() * 4)]
|
||||||
|
const method = methods[Math.floor(Math.random() * methods.length)]
|
||||||
|
const status = statuses[Math.floor(Math.random() * statuses.length)]
|
||||||
|
|
||||||
|
if (status === 'success') currentBalance += amount
|
||||||
|
|
||||||
|
data.push({
|
||||||
|
key: i,
|
||||||
|
amount: amount,
|
||||||
|
rechargeTime: dayjs().subtract(Math.floor(Math.random() * 30), 'day').toDate(),
|
||||||
|
paymentMethod: method,
|
||||||
|
accountBalance: currentBalance,
|
||||||
|
status: status
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.sort((a, b) => new Date(b.rechargeTime) - new Date(a.rechargeTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
rechargeRecords.value = generateMockData(15)
|
||||||
|
pagination.total = rechargeRecords.value.length
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.recharge-page {
|
||||||
|
padding: 24px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f1f1f;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 余额区域 */
|
||||||
|
.balance-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-card {
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-card :deep(.ant-card-body) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.balance-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-info {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-amount {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-label {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-value {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f1f1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 充值方式容器 */
|
||||||
|
.payment-methods-container {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-methods-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f1f1f;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-methods-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.payment-methods-row {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 2px solid #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
transition: all 0.2s;
|
||||||
|
min-width: 150px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.payment-method-item {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-item:hover {
|
||||||
|
border-color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-item.selected {
|
||||||
|
border-color: #1890ff;
|
||||||
|
background: #f0f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-item:nth-child(1) .method-icon { color: #1677ff; }
|
||||||
|
.payment-method-item:nth-child(2) .method-icon { color: #07c160; }
|
||||||
|
.payment-method-item:nth-child(3) .method-icon { color: #722ed1; }
|
||||||
|
|
||||||
|
.method-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1f1f1f;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-selected {
|
||||||
|
color: #52c41a;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 充值内容 */
|
||||||
|
.recharge-content {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f1f1f;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 金额选项 */
|
||||||
|
.amount-options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.amount-options {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.amount-options {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-option {
|
||||||
|
border: 2px solid #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
background: #fff;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-option:hover {
|
||||||
|
border-color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-option.selected {
|
||||||
|
border-color: #1890ff;
|
||||||
|
background: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义金额 */
|
||||||
|
.custom-amount {
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-amount :deep(.ant-input) {
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-prefix {
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮 */
|
||||||
|
.action-section {
|
||||||
|
margin-top: 32px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-btn {
|
||||||
|
width: 200px;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 充值记录 */
|
||||||
|
.records-card {
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-card :deep(.ant-card-head) {
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-card :deep(.ant-card-head-title) {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-card :deep(.ant-card-body) {
|
||||||
|
padding: 0 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filter-section {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-table {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-cell {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 模态框 */
|
||||||
|
.recharge-confirm {
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-label {
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f1f1f;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-value.highlight {
|
||||||
|
color: #52c41a;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-tips {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f6ffed;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #52c41a;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-tips .anticon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
537
src/views/admin/account/cost/deposit/index.vue
Normal file
537
src/views/admin/account/cost/deposit/index.vue
Normal file
@ -0,0 +1,537 @@
|
|||||||
|
<template>
|
||||||
|
<div class="withdrawal-management">
|
||||||
|
<!-- 提现账户信息卡片 -->
|
||||||
|
<a-row :gutter="24" class="mb-6">
|
||||||
|
|
||||||
|
<a-col :span="24">
|
||||||
|
<a-card title="可提现金额">
|
||||||
|
<div class="balance-info">
|
||||||
|
<div class="balance-amount">
|
||||||
|
<span class="amount">{{ formatCurrency(accountInfo.availableBalance) }}</span>
|
||||||
|
<span class="currency">元</span>
|
||||||
|
</div>
|
||||||
|
<div class="balance-desc">
|
||||||
|
当前可提现金额,提现将在1-3个工作日内到账
|
||||||
|
</div>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
class="w-full mt-4"
|
||||||
|
@click="showWithdrawalModal"
|
||||||
|
:disabled="accountInfo.availableBalance <= 0"
|
||||||
|
>
|
||||||
|
去提现
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<!-- 提现记录查询 -->
|
||||||
|
<a-card title="提现记录" class="mb-6">
|
||||||
|
<template #extra>
|
||||||
|
<a-range-picker
|
||||||
|
v-model:value="searchParams.dateRange"
|
||||||
|
:format="dateFormat"
|
||||||
|
@change="handleDateChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 查询条件 -->
|
||||||
|
<a-form layout="inline" :model="searchParams" class="mb-4">
|
||||||
|
<a-form-item label="提现单号">
|
||||||
|
<a-input
|
||||||
|
v-model:value="searchParams.orderNo"
|
||||||
|
placeholder="请输入提现单号"
|
||||||
|
@pressEnter="handleSearch"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="账户">
|
||||||
|
<a-select
|
||||||
|
v-model:value="searchParams.account"
|
||||||
|
placeholder="请选择账户"
|
||||||
|
style="width: 150px"
|
||||||
|
allowClear
|
||||||
|
>
|
||||||
|
<a-select-option v-for="account in accountOptions" :key="account.value">
|
||||||
|
{{ account.label }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="状态">
|
||||||
|
<a-select
|
||||||
|
v-model:value="searchParams.status"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
style="width: 150px"
|
||||||
|
allowClear
|
||||||
|
>
|
||||||
|
<a-select-option value="pending">处理中</a-select-option>
|
||||||
|
<a-select-option value="success">成功</a-select-option>
|
||||||
|
<a-select-option value="failed">失败</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item>
|
||||||
|
<a-button type="primary" @click="handleSearch">查询</a-button>
|
||||||
|
<a-button @click="handleReset" class="ml-2">重置</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
|
||||||
|
<!-- 提现记录表格 -->
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="withdrawalRecords"
|
||||||
|
:pagination="pagination"
|
||||||
|
@change="handleTableChange"
|
||||||
|
:loading="loading"
|
||||||
|
rowKey="id"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'amount'">
|
||||||
|
<span class="amount-cell">{{ formatCurrency(record.amount) }}元</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="column.key === 'status'">
|
||||||
|
<a-tag :color="getStatusColor(record.status)">
|
||||||
|
{{ getStatusText(record.status) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="column.key === 'createdAt'">
|
||||||
|
{{ formatDate(record.createdAt) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="column.key === 'completedAt'">
|
||||||
|
{{ record.completedAt ? formatDate(record.completedAt) : '-' }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="column.key === 'actions'">
|
||||||
|
<a-button type="link" @click="viewDetail(record)">查看详情</a-button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- 提现弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="withdrawalModal.visible"
|
||||||
|
title="提现申请"
|
||||||
|
@ok="handleWithdrawalSubmit"
|
||||||
|
@cancel="handleWithdrawalCancel"
|
||||||
|
:confirm-loading="withdrawalModal.confirming"
|
||||||
|
>
|
||||||
|
<a-form
|
||||||
|
ref="withdrawalFormRef"
|
||||||
|
:model="withdrawalForm"
|
||||||
|
:rules="withdrawalRules"
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
|
<a-form-item label="提现金额" name="amount">
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="withdrawalForm.amount"
|
||||||
|
placeholder="请输入提现金额"
|
||||||
|
:min="1"
|
||||||
|
:max="accountInfo.availableBalance"
|
||||||
|
style="width: 100%"
|
||||||
|
:formatter="value => `¥ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')"
|
||||||
|
:parser="value => value.replace(/¥\s?|(,*)/g, '')"
|
||||||
|
/>
|
||||||
|
<div class="amount-hint mt-2">
|
||||||
|
<div>可提现金额: {{ formatCurrency(accountInfo.availableBalance) }}元</div>
|
||||||
|
<div>单笔最低提现: 1元</div>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="提现到账账户" name="accountId">
|
||||||
|
<a-select
|
||||||
|
v-model:value="withdrawalForm.accountId"
|
||||||
|
placeholder="请选择提现账户"
|
||||||
|
>
|
||||||
|
<a-select-option :value="accountInfo.id">
|
||||||
|
{{ accountInfo.bank }} ({{ accountInfo.number }})
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-alert
|
||||||
|
v-if="withdrawalForm.amount"
|
||||||
|
:message="`预计到账金额: ${formatCurrency(calculateActualAmount(withdrawalForm.amount))}元`"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
// 账户信息
|
||||||
|
const accountInfo = reactive({
|
||||||
|
id: '1',
|
||||||
|
type: '银行账户',
|
||||||
|
number: '6228 **** **** 5678',
|
||||||
|
name: '张三',
|
||||||
|
bank: '中国工商银行',
|
||||||
|
availableBalance: 5000.00
|
||||||
|
})
|
||||||
|
|
||||||
|
// 提现记录查询参数
|
||||||
|
const searchParams = reactive({
|
||||||
|
orderNo: '',
|
||||||
|
account: undefined,
|
||||||
|
status: undefined,
|
||||||
|
dateRange: [dayjs().subtract(30, 'day'), dayjs()]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: total => `共 ${total} 条记录`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '提现单号',
|
||||||
|
dataIndex: 'orderNo',
|
||||||
|
key: 'orderNo',
|
||||||
|
width: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '账户',
|
||||||
|
key: 'account',
|
||||||
|
width: 180,
|
||||||
|
render: (_, record) => `${record.bank} (${record.accountNumber})`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '提现金额',
|
||||||
|
key: 'amount',
|
||||||
|
width: 120,
|
||||||
|
align: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '手续费',
|
||||||
|
dataIndex: 'fee',
|
||||||
|
key: 'fee',
|
||||||
|
width: 100,
|
||||||
|
align: 'right',
|
||||||
|
render: fee => fee ? `${fee}元` : '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
key: 'status',
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '申请时间',
|
||||||
|
key: 'createdAt',
|
||||||
|
width: 180
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '完成时间',
|
||||||
|
key: 'completedAt',
|
||||||
|
width: 180
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'actions',
|
||||||
|
width: 100,
|
||||||
|
fixed: 'right'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 提现记录数据
|
||||||
|
const withdrawalRecords = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const accountOptions = ref([
|
||||||
|
{ value: '1', label: '工商银行(尾号5678)' },
|
||||||
|
{ value: '2', label: '建设银行(尾号1234)' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 提现弹窗
|
||||||
|
const withdrawalModal = reactive({
|
||||||
|
visible: false,
|
||||||
|
confirming: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 提现表单
|
||||||
|
const withdrawalFormRef = ref()
|
||||||
|
const withdrawalForm = reactive({
|
||||||
|
amount: undefined,
|
||||||
|
accountId: accountInfo.id
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const withdrawalRules = {
|
||||||
|
amount: [
|
||||||
|
{ required: true, message: '请输入提现金额' },
|
||||||
|
{ type: 'number', min: 1, max: accountInfo.availableBalance, message: `提现金额需在1-${accountInfo.availableBalance}元之间` }
|
||||||
|
],
|
||||||
|
accountId: [
|
||||||
|
{ required: true, message: '请选择提现账户' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日期格式
|
||||||
|
const dateFormat = 'YYYY-MM-DD'
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
fetchWithdrawalRecords()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取提现记录
|
||||||
|
const fetchWithdrawalRecords = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 模拟API调用
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
const mockData = Array.from({ length: 35 }, (_, index) => ({
|
||||||
|
id: index + 1,
|
||||||
|
orderNo: `TX${Date.now()}${index}`,
|
||||||
|
bank: ['工商银行', '建设银行', '农业银行'][index % 3],
|
||||||
|
accountNumber: `6228 **** **** ${1000 + index}`,
|
||||||
|
amount: Math.floor(Math.random() * 10000) + 100,
|
||||||
|
fee: Math.floor(Math.random() * 10),
|
||||||
|
status: ['pending', 'success', 'failed'][index % 3],
|
||||||
|
createdAt: dayjs().subtract(index, 'day').format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
completedAt: index % 3 !== 0 ? dayjs().subtract(index, 'day').add(1, 'hour').format('YYYY-MM-DD HH:mm:ss') : null
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 应用筛选
|
||||||
|
let filteredData = mockData
|
||||||
|
if (searchParams.orderNo) {
|
||||||
|
filteredData = filteredData.filter(item => item.orderNo.includes(searchParams.orderNo))
|
||||||
|
}
|
||||||
|
if (searchParams.account) {
|
||||||
|
filteredData = filteredData.filter(item => item.accountNumber.endsWith(searchParams.account))
|
||||||
|
}
|
||||||
|
if (searchParams.status) {
|
||||||
|
filteredData = filteredData.filter(item => item.status === searchParams.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用日期范围筛选
|
||||||
|
if (searchParams.dateRange && searchParams.dateRange[0] && searchParams.dateRange[1]) {
|
||||||
|
const start = searchParams.dateRange[0]
|
||||||
|
const end = searchParams.dateRange[1]
|
||||||
|
filteredData = filteredData.filter(item => {
|
||||||
|
const date = dayjs(item.createdAt)
|
||||||
|
return date.isAfter(start) && date.isBefore(end)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const start = (pagination.current - 1) * pagination.pageSize
|
||||||
|
const end = start + pagination.pageSize
|
||||||
|
withdrawalRecords.value = filteredData.slice(start, end)
|
||||||
|
pagination.total = filteredData.length
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取提现记录失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格分页变化
|
||||||
|
const handleTableChange = (page) => {
|
||||||
|
pagination.current = page.current
|
||||||
|
pagination.pageSize = page.pageSize
|
||||||
|
fetchWithdrawalRecords()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.current = 1
|
||||||
|
fetchWithdrawalRecords()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置查询
|
||||||
|
const handleReset = () => {
|
||||||
|
searchParams.orderNo = ''
|
||||||
|
searchParams.account = undefined
|
||||||
|
searchParams.status = undefined
|
||||||
|
searchParams.dateRange = [dayjs().subtract(30, 'day'), dayjs()]
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日期变化
|
||||||
|
const handleDateChange = () => {
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示提现弹窗
|
||||||
|
const showWithdrawalModal = () => {
|
||||||
|
withdrawalModal.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理提现提交
|
||||||
|
const handleWithdrawalSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await withdrawalFormRef.value.validate()
|
||||||
|
withdrawalModal.confirming = true
|
||||||
|
|
||||||
|
// 模拟API调用
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
message.success('提现申请提交成功')
|
||||||
|
|
||||||
|
// 更新账户余额
|
||||||
|
accountInfo.availableBalance -= withdrawalForm.amount
|
||||||
|
|
||||||
|
// 重置表单并关闭弹窗
|
||||||
|
withdrawalFormRef.value.resetFields()
|
||||||
|
withdrawalModal.visible = false
|
||||||
|
|
||||||
|
// 刷新记录
|
||||||
|
fetchWithdrawalRecords()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提现提交失败:', error)
|
||||||
|
} finally {
|
||||||
|
withdrawalModal.confirming = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理提现取消
|
||||||
|
const handleWithdrawalCancel = () => {
|
||||||
|
withdrawalFormRef.value.resetFields()
|
||||||
|
withdrawalModal.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const viewDetail = (record) => {
|
||||||
|
message.info(`查看提现记录详情: ${record.orderNo}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算实际到账金额
|
||||||
|
const calculateActualAmount = (amount) => {
|
||||||
|
const fee = amount * 0.01 // 假设手续费1%
|
||||||
|
return amount - fee
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化货币
|
||||||
|
const formatCurrency = (amount) => {
|
||||||
|
return amount.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态颜色
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const colors = {
|
||||||
|
pending: 'orange',
|
||||||
|
success: 'green',
|
||||||
|
failed: 'red'
|
||||||
|
}
|
||||||
|
return colors[status] || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const texts = {
|
||||||
|
pending: '处理中',
|
||||||
|
success: '成功',
|
||||||
|
failed: '失败'
|
||||||
|
}
|
||||||
|
return texts[status] || status
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.withdrawal-management {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-info .info-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-info .label {
|
||||||
|
color: #666;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-info .value {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-info .balance-amount {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-info .amount {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-info .currency {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #666;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-info .balance-desc {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-hint {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-hint div {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-cell {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-6 {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-2 {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-4 {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ml-2 {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-full {
|
||||||
|
float: inline-end;
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,60 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-row :gutter="[18, 18]">
|
<a-row :gutter="[18, 18]">
|
||||||
<a-col :span="24">
|
|
||||||
<a-row :gutter="18">
|
|
||||||
<a-col :span="12" style="display: flex;">
|
|
||||||
<a-card style="flex: 1; display: flex; flex-direction: column;">
|
|
||||||
<template #title>
|
|
||||||
<span>可开票: </span>
|
|
||||||
<span>¥ </span><span class="bold">0.00</span>
|
|
||||||
</template>
|
|
||||||
<template #extra>
|
|
||||||
<a-button type="primary" ghost>去开票</a-button>
|
|
||||||
</template>
|
|
||||||
<div style="display: flex;justify-content: space-around;align-items: center;line-height: 30px;">
|
|
||||||
<div>
|
|
||||||
<div>可开票金额</div>
|
|
||||||
<div><span>¥ </span><span class="bold">0.00</span></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div> </div>
|
|
||||||
<div>=</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div>累计充值</div>
|
|
||||||
<div><span>¥ </span><span class="bold">0.00</span></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div> </div>
|
|
||||||
<div>-</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div>充值已开票</div>
|
|
||||||
<div><span>¥ </span><span class="bold">0.00</span></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div> </div>
|
|
||||||
<div>-</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div>账单已开票</div>
|
|
||||||
<div><span>¥ </span><span class="bold">0.00</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a-card>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="12" style="display: flex;">
|
|
||||||
<a-card style="flex: 1; display: flex; flex-direction: column;">
|
|
||||||
<template #title>
|
|
||||||
<span>发票信息: </span>
|
|
||||||
</template>
|
|
||||||
<div>
|
|
||||||
暂无发票信息<a href="javascript:;" @click="visibleOpen = true">去添加</a>
|
|
||||||
</div>
|
|
||||||
</a-card>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="24">
|
<a-col :span="24">
|
||||||
<a-card title="开票记录">
|
<a-card title="开票记录">
|
||||||
<a-table :dataSource="listData" :columns="columns" bordered :pagination="paginationState"
|
<a-table :dataSource="listData" :columns="columns" bordered :pagination="paginationState"
|
||||||
|
|||||||
@ -33,8 +33,31 @@
|
|||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="24">
|
<a-col :span="24">
|
||||||
<a-card>
|
<a-card>
|
||||||
<a-table :dataSource="listData" :columns="columns" bordered :pagination="paginationState"
|
<!-- 批量操作区域 -->
|
||||||
@change="onTableChange">
|
<div class="batch-operations" style="margin-bottom: 16px;">
|
||||||
|
<a-space>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleBatchInvoice"
|
||||||
|
:disabled="selectedRowKeys.length === 0"
|
||||||
|
>
|
||||||
|
批量去开票
|
||||||
|
</a-button>
|
||||||
|
<span v-if="selectedRowKeys.length > 0">
|
||||||
|
已选择 {{ selectedRowKeys.length }} 条记录
|
||||||
|
</span>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-table
|
||||||
|
:dataSource="listData"
|
||||||
|
:columns="columns"
|
||||||
|
bordered
|
||||||
|
:pagination="paginationState"
|
||||||
|
:rowSelection="rowSelection"
|
||||||
|
@change="onTableChange"
|
||||||
|
:rowKey="record => record.id || record.serial_number"
|
||||||
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'created_at'">
|
<template v-if="column.key === 'created_at'">
|
||||||
<div>
|
<div>
|
||||||
@ -46,37 +69,400 @@
|
|||||||
{{ dayjs(record.billing_start).format('YYYY-MM-DD HH:mm') +"~" +dayjs(record.billing_end).format('YYYY-MM-DD HH:mm') }}
|
{{ dayjs(record.billing_start).format('YYYY-MM-DD HH:mm') +"~" +dayjs(record.billing_end).format('YYYY-MM-DD HH:mm') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-if="column.key === 'price'">
|
||||||
|
<div>
|
||||||
|
¥{{ record.price || '0.00' }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'real_amount'">
|
||||||
|
<div style="color: #ff4d4f; font-weight: 500;">
|
||||||
|
-¥{{ record.real_amount || '0.00' }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<a-space>
|
||||||
|
<a-button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="handleSingleInvoice(record)"
|
||||||
|
>
|
||||||
|
去开票
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
||||||
|
<!-- 单个开票模态框 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="singleInvoiceModalVisible"
|
||||||
|
:title="isBatch ? '批量开票' : '开票申请'"
|
||||||
|
@ok="handleSingleInvoiceConfirm"
|
||||||
|
@cancel="handleInvoiceModalCancel"
|
||||||
|
:confirm-loading="singleInvoiceLoading"
|
||||||
|
centered
|
||||||
|
width="800px"
|
||||||
|
:okText="isBatch ? '确认批量开票' : '确认开票'"
|
||||||
|
>
|
||||||
|
<div class="invoice-modal-content">
|
||||||
|
<!-- 订单信息区域 -->
|
||||||
|
<div class="order-info-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<a-icon type="shopping" />
|
||||||
|
<span style="margin-left: 8px;">订单信息</span>
|
||||||
|
</div>
|
||||||
|
<div class="order-info-grid">
|
||||||
|
<template v-if="isBatch">
|
||||||
|
<!-- 批量开票显示多个订单 -->
|
||||||
|
<div class="order-info-item">
|
||||||
|
<span class="info-label">订单数量:</span>
|
||||||
|
<span class="info-value">{{ selectedRows.length }} 个订单</span>
|
||||||
|
</div>
|
||||||
|
<div class="order-info-item">
|
||||||
|
<span class="info-label">总金额:</span>
|
||||||
|
<span class="info-value total-amount">¥{{ selectedTotalAmount.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="order-list-preview" style="grid-column: span 2;">
|
||||||
|
<div class="preview-title">订单列表:</div>
|
||||||
|
<div class="order-list">
|
||||||
|
<div v-for="(order, index) in selectedRows.slice(0, 3)" :key="order.id" class="order-item">
|
||||||
|
<span>{{ order.serial_number }}</span>
|
||||||
|
<span>-¥{{ order.real_amount || '0.00' }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedRows.length > 3" class="order-more">
|
||||||
|
等 {{ selectedRows.length }} 个订单
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<!-- 单个开票显示单个订单 -->
|
||||||
|
<div class="order-info-item">
|
||||||
|
<span class="info-label">订单编号:</span>
|
||||||
|
<span class="info-value">{{ currentInvoiceRecord?.serial_number || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="order-info-item">
|
||||||
|
<span class="info-label">开票金额:</span>
|
||||||
|
<span class="info-value">¥{{ currentInvoiceRecord?.real_amount || '0.00' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 发票信息区域 -->
|
||||||
|
<div class="invoice-info-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<a-icon type="file-text" />
|
||||||
|
<span style="margin-left: 8px;">发票信息</span>
|
||||||
|
</div>
|
||||||
|
<a-form
|
||||||
|
ref="invoiceFormRef"
|
||||||
|
:model="invoiceFormData"
|
||||||
|
:rules="invoiceFormRules"
|
||||||
|
layout="vertical"
|
||||||
|
class="invoice-form"
|
||||||
|
>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="发票类型" name="invoiceType" required>
|
||||||
|
<a-select
|
||||||
|
v-model:value="invoiceFormData.invoiceType"
|
||||||
|
placeholder="请选择发票类型"
|
||||||
|
>
|
||||||
|
<a-select-option value="normal">普通发票</a-select-option>
|
||||||
|
<a-select-option value="special">专用发票</a-select-option>
|
||||||
|
<a-select-option value="electronic">电子发票</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="开票类型" name="billingType" required>
|
||||||
|
<a-select
|
||||||
|
v-model:value="invoiceFormData.billingType"
|
||||||
|
placeholder="请选择开票类型"
|
||||||
|
>
|
||||||
|
<a-select-option value="personal">个人</a-select-option>
|
||||||
|
<a-select-option value="company">企业</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-form-item label="抬头类型" name="titleType" required>
|
||||||
|
<a-radio-group v-model:value="invoiceFormData.titleType">
|
||||||
|
<a-radio value="personal">个人抬头</a-radio>
|
||||||
|
<a-radio value="company">公司抬头</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item
|
||||||
|
label="发票抬头"
|
||||||
|
name="invoiceTitle"
|
||||||
|
required
|
||||||
|
v-if="invoiceFormData.titleType === 'company'"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model:value="invoiceFormData.invoiceTitle"
|
||||||
|
placeholder="请选择发票抬头"
|
||||||
|
:options="invoiceTitleOptions"
|
||||||
|
@search="handleSearchInvoiceTitle"
|
||||||
|
show-search
|
||||||
|
allow-clear
|
||||||
|
>
|
||||||
|
<template #dropdownRender="{ menuNode: menu }">
|
||||||
|
<v-nodes :vnodes="menu" />
|
||||||
|
<a-divider style="margin: 4px 0" />
|
||||||
|
<div
|
||||||
|
style="padding: 8px; cursor: pointer;"
|
||||||
|
@click="handleAddInvoiceTitle"
|
||||||
|
>
|
||||||
|
<a-icon type="plus" /> 新增发票抬头
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item
|
||||||
|
label="发票抬头"
|
||||||
|
name="personalTitle"
|
||||||
|
required
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:value="invoiceFormData.personalTitle"
|
||||||
|
placeholder="请输入个人姓名"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item
|
||||||
|
label="纳税人识别号"
|
||||||
|
name="taxNumber"
|
||||||
|
v-if="invoiceFormData.titleType === 'company' && invoiceFormData.invoiceTitle"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:value="invoiceFormData.taxNumber"
|
||||||
|
placeholder="请输入纳税人识别号"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="备注" name="remark">
|
||||||
|
<a-textarea
|
||||||
|
v-model:value="invoiceFormData.remark"
|
||||||
|
placeholder="请输入开票备注(可选)"
|
||||||
|
:rows="2"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 新增发票抬头模态框 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="addTitleModalVisible"
|
||||||
|
title="新增发票抬头"
|
||||||
|
@ok="handleAddTitleConfirm"
|
||||||
|
@cancel="addTitleModalVisible = false"
|
||||||
|
width="500px"
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<a-form :model="newTitleForm" layout="vertical">
|
||||||
|
<a-form-item label="公司名称" required>
|
||||||
|
<a-input
|
||||||
|
v-model:value="newTitleForm.companyName"
|
||||||
|
placeholder="请输入公司全称"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="纳税人识别号" required>
|
||||||
|
<a-input
|
||||||
|
v-model:value="newTitleForm.taxNumber"
|
||||||
|
placeholder="请输入纳税人识别号"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="公司地址">
|
||||||
|
<a-input
|
||||||
|
v-model:value="newTitleForm.address"
|
||||||
|
placeholder="请输入公司地址"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="开户银行">
|
||||||
|
<a-input
|
||||||
|
v-model:value="newTitleForm.bank"
|
||||||
|
placeholder="请输入开户银行"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="银行账号">
|
||||||
|
<a-input
|
||||||
|
v-model:value="newTitleForm.bankAccount"
|
||||||
|
placeholder="请输入银行账号"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onBeforeMount } from 'vue'
|
import { ref, onBeforeMount, computed, reactive, nextTick } from 'vue'
|
||||||
import { usePagination } from '@/hooks'
|
import { usePagination } from '@/hooks'
|
||||||
import { useList } from '@/apis/admin'
|
import { useList } from '@/apis/admin'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
// const listData=ref([ {title:1}])
|
import { message, FormInstance } from 'ant-design-vue'
|
||||||
// const paginationState=ref({
|
import type { Rule } from 'ant-design-vue/es/form'
|
||||||
// total: 0,
|
|
||||||
// current: 1,
|
// 模拟表格数据
|
||||||
// pageSize: 10,
|
const mockData = [
|
||||||
// showTotal: (total) => `总 ${total} 条数据`,
|
{
|
||||||
// pageSizeOptions: ['10', '20', '30', '40'],
|
id: '1',
|
||||||
// })
|
serial_number: 'SN202312270001',
|
||||||
|
created_at: '2023-12-27 10:30:00',
|
||||||
|
host_id: 'HOST-001',
|
||||||
|
host_case_id: 'CASE-001',
|
||||||
|
computing_power_model: 'RTX 4090',
|
||||||
|
gpu_count: 2,
|
||||||
|
billing_start: '2023-12-27 10:00:00',
|
||||||
|
billing_end: '2023-12-27 11:00:00',
|
||||||
|
bill_time: '1小时',
|
||||||
|
price: '50.00',
|
||||||
|
real_amount: '100.00',
|
||||||
|
status: '待开票'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
serial_number: 'SN202312270002',
|
||||||
|
created_at: '2023-12-27 11:45:00',
|
||||||
|
host_id: 'HOST-002',
|
||||||
|
host_case_id: 'CASE-002',
|
||||||
|
computing_power_model: 'RTX 4080',
|
||||||
|
gpu_count: 1,
|
||||||
|
billing_start: '2023-12-27 11:30:00',
|
||||||
|
billing_end: '2023-12-27 12:30:00',
|
||||||
|
bill_time: '1小时',
|
||||||
|
price: '40.00',
|
||||||
|
real_amount: '40.00',
|
||||||
|
status: '待开票'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
serial_number: 'SN202312270003',
|
||||||
|
created_at: '2023-12-27 14:20:00',
|
||||||
|
host_id: 'HOST-003',
|
||||||
|
host_case_id: 'CASE-003',
|
||||||
|
computing_power_model: 'RTX 3090',
|
||||||
|
gpu_count: 4,
|
||||||
|
billing_start: '2023-12-27 14:00:00',
|
||||||
|
billing_end: '2023-12-27 15:00:00',
|
||||||
|
bill_time: '1小时',
|
||||||
|
price: '30.00',
|
||||||
|
real_amount: '120.00',
|
||||||
|
status: '已开票'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
serial_number: 'SN202312270004',
|
||||||
|
created_at: '2023-12-27 16:15:00',
|
||||||
|
host_id: 'HOST-004',
|
||||||
|
host_case_id: 'CASE-004',
|
||||||
|
computing_power_model: 'A100',
|
||||||
|
gpu_count: 2,
|
||||||
|
billing_start: '2023-12-27 16:00:00',
|
||||||
|
billing_end: '2023-12-27 17:00:00',
|
||||||
|
bill_time: '1小时',
|
||||||
|
price: '80.00',
|
||||||
|
real_amount: '160.00',
|
||||||
|
status: '待开票'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
serial_number: 'SN202312270005',
|
||||||
|
created_at: '2023-12-27 18:30:00',
|
||||||
|
host_id: 'HOST-005',
|
||||||
|
host_case_id: 'CASE-005',
|
||||||
|
computing_power_model: 'H100',
|
||||||
|
gpu_count: 1,
|
||||||
|
billing_start: '2023-12-27 18:00:00',
|
||||||
|
billing_end: '2023-12-27 19:00:00',
|
||||||
|
bill_time: '1小时',
|
||||||
|
price: '100.00',
|
||||||
|
real_amount: '100.00',
|
||||||
|
status: '待开票'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 表格数据
|
||||||
const { listData, paginationState, resetPagination, searchFormData } = usePagination()
|
const { listData, paginationState, resetPagination, searchFormData } = usePagination()
|
||||||
|
|
||||||
|
// 选中的行
|
||||||
|
const selectedRowKeys = ref<string[]>([])
|
||||||
|
const selectedRows = ref<any[]>([])
|
||||||
|
|
||||||
|
// 模态框状态
|
||||||
|
const singleInvoiceModalVisible = ref(false)
|
||||||
|
const addTitleModalVisible = ref(false)
|
||||||
|
const singleInvoiceLoading = ref(false)
|
||||||
|
const isBatch = ref(false) // 是否为批量开票
|
||||||
|
|
||||||
|
// 当前操作的开票记录
|
||||||
|
const currentInvoiceRecord = ref<any>(null)
|
||||||
|
|
||||||
|
// 表单引用
|
||||||
|
const invoiceFormRef = ref<FormInstance>()
|
||||||
|
|
||||||
|
// 发票抬头选项
|
||||||
|
const invoiceTitleOptions = ref([
|
||||||
|
{ value: 'company1', label: '北京科技有限公司' },
|
||||||
|
{ value: 'company2', label: '上海信息技术有限公司' },
|
||||||
|
{ value: 'company3', label: '广州人工智能有限公司' },
|
||||||
|
{ value: 'company4', label: '深圳云计算有限公司' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 新增发票抬头表单
|
||||||
|
const newTitleForm = reactive({
|
||||||
|
companyName: '',
|
||||||
|
taxNumber: '',
|
||||||
|
address: '',
|
||||||
|
bank: '',
|
||||||
|
bankAccount: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 发票表单数据
|
||||||
|
const invoiceFormData = reactive({
|
||||||
|
invoiceType: undefined,
|
||||||
|
billingType: undefined,
|
||||||
|
titleType: 'company',
|
||||||
|
invoiceTitle: undefined,
|
||||||
|
personalTitle: '',
|
||||||
|
taxNumber: '',
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const invoiceFormRules = reactive<Record<string, Rule[]>>({
|
||||||
|
invoiceType: [
|
||||||
|
{ required: true, message: '请选择发票类型', trigger: 'change' }
|
||||||
|
],
|
||||||
|
billingType: [
|
||||||
|
{ required: true, message: '请选择开票类型', trigger: 'change' }
|
||||||
|
],
|
||||||
|
titleType: [
|
||||||
|
{ required: true, message: '请选择抬头类型', trigger: 'change' }
|
||||||
|
],
|
||||||
|
invoiceTitle: [
|
||||||
|
{ required: true, message: '请选择发票抬头', trigger: 'change' }
|
||||||
|
],
|
||||||
|
personalTitle: [
|
||||||
|
{ required: true, message: '请输入个人姓名', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
taxNumber: [
|
||||||
|
{ required: true, message: '请输入纳税人识别号', trigger: 'blur' },
|
||||||
|
{ pattern: /^[A-Z0-9]{15}$|^[A-Z0-9]{18}$|^[A-Z0-9]{20}$/, message: '请输入正确的纳税人识别号' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
const columns = ref([
|
const columns = ref([
|
||||||
// { title: '订单号', dataIndex: 'flowNum', key: 'flowNum' },
|
|
||||||
// { title: '订单创建时间', dataIndex: 'transactionTime', key: 'transactionTime' },
|
|
||||||
// { title: '产品名称', dataIndex: 'flowNum', key: 'flowNum' },
|
|
||||||
// { title: '计费方式', dataIndex: 'flowNum', key: 'flowNum' },
|
|
||||||
// { title: '订单类型', dataIndex: 'flowNum', key: 'flowNum' },
|
|
||||||
// { title: '订单状态', dataIndex: 'flowNum', key: 'flowNum' },
|
|
||||||
// { title: '订单金额', dataIndex: 'flowNum', key: 'flowNum' },
|
|
||||||
// { title: '操作', dataIndex: 'flowNum', key: 'flowNum' },
|
|
||||||
{ title: '流水号', dataIndex: 'serial_number', key: 'serial_number' },
|
{ title: '流水号', dataIndex: 'serial_number', key: 'serial_number' },
|
||||||
{ title: '交易时间', dataIndex: 'created_at', key: 'created_at' },
|
{ title: '交易时间', dataIndex: 'created_at', key: 'created_at' },
|
||||||
{ title: '主机ID', dataIndex: 'host_id', key: 'host_id' },
|
{ title: '主机ID', dataIndex: 'host_id', key: 'host_id' },
|
||||||
@ -87,25 +473,61 @@ const columns = ref([
|
|||||||
{ title: '计费时长', dataIndex: 'bill_time', key: 'bill_time' },
|
{ title: '计费时长', dataIndex: 'bill_time', key: 'bill_time' },
|
||||||
{ title: '单价', dataIndex: 'price', key: 'price' },
|
{ title: '单价', dataIndex: 'price', key: 'price' },
|
||||||
{ title: '交易金额', dataIndex: 'real_amount', key: 'real_amount' },
|
{ title: '交易金额', dataIndex: 'real_amount', key: 'real_amount' },
|
||||||
|
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 100,
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
])
|
])
|
||||||
onBeforeMount(() => {
|
|
||||||
getPageList()
|
|
||||||
|
|
||||||
|
// 行选择配置
|
||||||
|
const rowSelection = computed(() => ({
|
||||||
|
selectedRowKeys: selectedRowKeys.value,
|
||||||
|
onChange: (selectedKeys: string[], selectedRows: any[]) => {
|
||||||
|
selectedRowKeys.value = selectedKeys
|
||||||
|
selectedRows.value = selectedRows
|
||||||
|
},
|
||||||
|
getCheckboxProps: (record: any) => ({
|
||||||
|
disabled: record.status === '已开票'
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 计算选中记录的总金额
|
||||||
|
const selectedTotalAmount = computed(() => {
|
||||||
|
return selectedRows.value.reduce((total, record) => {
|
||||||
|
const amount = parseFloat(record.real_amount) || 0
|
||||||
|
return total + amount
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化模拟数据
|
||||||
|
const initMockData = () => {
|
||||||
|
listData.value = mockData
|
||||||
|
paginationState.total = mockData.length
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
// 初始化模拟数据
|
||||||
|
initMockData()
|
||||||
|
// 如果需要真实数据,可以调用 getPageList()
|
||||||
})
|
})
|
||||||
|
|
||||||
const getPageList = async () => {
|
const getPageList = async () => {
|
||||||
try {
|
try {
|
||||||
const { pageSize, current } = paginationState
|
const { pageSize, current } = paginationState
|
||||||
const res: any = await useList({ pageSize: pageSize, pageNum: current });
|
const res: any = await useList({ pageSize: pageSize, pageNum: current })
|
||||||
listData.value = res.list;
|
listData.value = res.list
|
||||||
paginationState.total = res.count;
|
paginationState.total = res.count
|
||||||
console.log('订单列表:', res);
|
console.log('订单列表:', res)
|
||||||
|
|
||||||
// advantageList.value = list;
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('产品优势请求失败:', error);
|
console.error('产品优势请求失败:', error)
|
||||||
|
// 如果API请求失败,使用模拟数据
|
||||||
|
initMockData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页
|
* 分页
|
||||||
*/
|
*/
|
||||||
@ -114,19 +536,347 @@ function onTableChange({ current, pageSize }) {
|
|||||||
paginationState.pageSize = pageSize
|
paginationState.pageSize = pageSize
|
||||||
getPageList()
|
getPageList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 搜索
|
* 搜索
|
||||||
*/
|
*/
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
resetPagination()
|
resetPagination()
|
||||||
|
selectedRowKeys.value = [] // 重置选择
|
||||||
|
selectedRows.value = []
|
||||||
getPageList()
|
getPageList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置
|
* 重置
|
||||||
*/
|
*/
|
||||||
function handleResetSearch() {
|
function handleResetSearch() {
|
||||||
searchFormData.value = {}
|
searchFormData.value = {}
|
||||||
resetPagination()
|
resetPagination()
|
||||||
|
selectedRowKeys.value = [] // 重置选择
|
||||||
|
selectedRows.value = []
|
||||||
getPageList()
|
getPageList()
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
/**
|
||||||
|
* 单个去开票
|
||||||
|
*/
|
||||||
|
function handleSingleInvoice(record: any) {
|
||||||
|
currentInvoiceRecord.value = record
|
||||||
|
isBatch.value = false
|
||||||
|
resetInvoiceForm()
|
||||||
|
singleInvoiceModalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量去开票
|
||||||
|
*/
|
||||||
|
function handleBatchInvoice() {
|
||||||
|
if (selectedRowKeys.value.length === 0) {
|
||||||
|
message.warning('请先选择要开票的记录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isBatch.value = true
|
||||||
|
resetInvoiceForm()
|
||||||
|
singleInvoiceModalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置发票表单
|
||||||
|
*/
|
||||||
|
function resetInvoiceForm() {
|
||||||
|
invoiceFormData.invoiceType = undefined
|
||||||
|
invoiceFormData.billingType = undefined
|
||||||
|
invoiceFormData.titleType = 'company'
|
||||||
|
invoiceFormData.invoiceTitle = undefined
|
||||||
|
invoiceFormData.personalTitle = ''
|
||||||
|
invoiceFormData.taxNumber = ''
|
||||||
|
invoiceFormData.remark = ''
|
||||||
|
|
||||||
|
// 清除表单验证状态
|
||||||
|
nextTick(() => {
|
||||||
|
invoiceFormRef.value?.clearValidate()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭开票模态框
|
||||||
|
*/
|
||||||
|
function handleInvoiceModalCancel() {
|
||||||
|
singleInvoiceModalVisible.value = false
|
||||||
|
resetInvoiceForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认开票
|
||||||
|
*/
|
||||||
|
async function handleSingleInvoiceConfirm() {
|
||||||
|
try {
|
||||||
|
// 验证表单
|
||||||
|
await invoiceFormRef.value?.validate()
|
||||||
|
|
||||||
|
singleInvoiceLoading.value = true
|
||||||
|
|
||||||
|
// 根据抬头类型获取抬头信息
|
||||||
|
const titleInfo = invoiceFormData.titleType === 'company'
|
||||||
|
? {
|
||||||
|
titleType: 'company',
|
||||||
|
companyName: invoiceFormData.invoiceTitle,
|
||||||
|
taxNumber: invoiceFormData.taxNumber
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
titleType: 'personal',
|
||||||
|
personalName: invoiceFormData.personalTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备开票数据
|
||||||
|
const invoiceData = {
|
||||||
|
...titleInfo,
|
||||||
|
invoiceType: invoiceFormData.invoiceType,
|
||||||
|
billingType: invoiceFormData.billingType,
|
||||||
|
remark: invoiceFormData.remark,
|
||||||
|
orders: isBatch.value
|
||||||
|
? selectedRows.value.map(row => ({
|
||||||
|
id: row.id,
|
||||||
|
serialNumber: row.serial_number,
|
||||||
|
amount: row.real_amount
|
||||||
|
}))
|
||||||
|
: [{
|
||||||
|
id: currentInvoiceRecord.value.id,
|
||||||
|
serialNumber: currentInvoiceRecord.value.serial_number,
|
||||||
|
amount: currentInvoiceRecord.value.real_amount
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('提交开票数据:', invoiceData)
|
||||||
|
|
||||||
|
// 模拟API调用延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||||
|
|
||||||
|
const successMessage = isBatch.value
|
||||||
|
? `批量开票申请已提交成功,共 ${selectedRows.value.length} 条记录`
|
||||||
|
: '开票申请已提交成功!'
|
||||||
|
|
||||||
|
message.success(successMessage)
|
||||||
|
singleInvoiceModalVisible.value = false
|
||||||
|
|
||||||
|
// 清空选择
|
||||||
|
selectedRowKeys.value = []
|
||||||
|
selectedRows.value = []
|
||||||
|
|
||||||
|
// 刷新列表
|
||||||
|
getPageList()
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('表单验证失败:', error)
|
||||||
|
if (error && typeof error === 'object' && 'errorFields' in error) {
|
||||||
|
message.error('请完善开票信息')
|
||||||
|
} else {
|
||||||
|
const errorMessage = isBatch.value
|
||||||
|
? '批量开票申请提交失败,请稍后重试'
|
||||||
|
: '开票申请提交失败,请稍后重试'
|
||||||
|
message.error(errorMessage)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
singleInvoiceLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索发票抬头
|
||||||
|
*/
|
||||||
|
function handleSearchInvoiceTitle(value: string) {
|
||||||
|
console.log('搜索发票抬头:', value)
|
||||||
|
// 这里可以实现搜索逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加发票抬头
|
||||||
|
*/
|
||||||
|
function handleAddInvoiceTitle() {
|
||||||
|
addTitleModalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认新增发票抬头
|
||||||
|
*/
|
||||||
|
function handleAddTitleConfirm() {
|
||||||
|
if (!newTitleForm.companyName || !newTitleForm.taxNumber) {
|
||||||
|
message.error('请填写公司名称和纳税人识别号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到选项列表
|
||||||
|
const newOption = {
|
||||||
|
value: newTitleForm.companyName,
|
||||||
|
label: newTitleForm.companyName
|
||||||
|
}
|
||||||
|
|
||||||
|
invoiceTitleOptions.value.push(newOption)
|
||||||
|
|
||||||
|
// 设置当前选择的发票抬头
|
||||||
|
invoiceFormData.invoiceTitle = newTitleForm.companyName
|
||||||
|
invoiceFormData.taxNumber = newTitleForm.taxNumber
|
||||||
|
|
||||||
|
// 重置表单并关闭模态框
|
||||||
|
Object.keys(newTitleForm).forEach(key => {
|
||||||
|
newTitleForm[key] = ''
|
||||||
|
})
|
||||||
|
addTitleModalVisible.value = false
|
||||||
|
|
||||||
|
message.success('发票抬头添加成功')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.batch-operations {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-modal-content {
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-info-section,
|
||||||
|
.invoice-info-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1890ff;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-amount {
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-list-preview {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-list {
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px;
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-form {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义滚动条 */
|
||||||
|
.invoice-modal-content::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-modal-content::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-modal-content::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-modal-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 适配响应式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.batch-operations {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-info-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-info-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 必填字段标记 */
|
||||||
|
:deep(.ant-form-item-required::before) {
|
||||||
|
content: '*';
|
||||||
|
color: #ff4d4f;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单间距优化 */
|
||||||
|
:deep(.ant-form-item) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -61,15 +61,18 @@ const menuItems: MenuItem[] = [
|
|||||||
visible: true,
|
visible: true,
|
||||||
children: [
|
children: [
|
||||||
{ path: '/layout/admin/myMoney', name: '费用总览', visible: true, disabled: false },
|
{ path: '/layout/admin/myMoney', name: '费用总览', visible: true, disabled: false },
|
||||||
|
{ path: '/layout/admin/balance', name: '余额管理', visible: true, disabled: false },
|
||||||
|
{ path: '/layout/admin/invoice', name: '发票管理', disabled: false, visible: true },
|
||||||
|
{ path: '/layout/admin/deposit', name: '提现管理', disabled: false, visible: true },
|
||||||
// 消费明细设置为不可见
|
// 消费明细设置为不可见
|
||||||
{ path: '/layout/admin/exchange', name: '算力点兑换', visible: false, 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 },
|
||||||
{ path: '/layout/admin/coupon', name: '优惠券(待开发)', disabled: true, visible: true },
|
// { 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/voucher', name: '代金券(待开发)', disabled: true, visible: true },
|
||||||
{ path: '/layout/admin/contract', name: '合同(待开发)', disabled: true, visible: true },
|
// { path: '/layout/admin/contract', name: '合同(待开发)', disabled: true, visible: true },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -7,134 +7,538 @@
|
|||||||
<span class="page-title">容器实例</span>
|
<span class="page-title">容器实例</span>
|
||||||
<span class="warning-tip">
|
<span class="warning-tip">
|
||||||
<exclamation-circle-outlined class="warning-icon" />
|
<exclamation-circle-outlined class="warning-icon" />
|
||||||
实例连续关机15天会释放实例,实例释放会导致数据清空且不可恢复,释放前实例在数据在。
|
实例连续关机15天会释放实例,实例释放会导致数据清空且不可恢复,释放前实例数据仍在。
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="header-quick-actions">
|
|
||||||
<span class="quick-action-item">
|
|
||||||
<bell-outlined class="quick-action-icon" />
|
|
||||||
订阅GPU通知
|
|
||||||
</span>
|
|
||||||
<span class="quick-action-item">
|
|
||||||
<key-outlined class="quick-action-icon" />
|
|
||||||
设置密钥登录
|
|
||||||
</span>
|
|
||||||
<span class="quick-action-item">
|
|
||||||
<appstore-outlined class="quick-action-icon" />
|
|
||||||
小程序管理实例
|
|
||||||
</span>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-bottom">
|
<div class="header-bottom">
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<a-button type="primary" @click="handleRent" class="action-btn">租用新实例</a-button>
|
<a-button type="primary" @click="handleRent" class="action-btn">新建实例</a-button>
|
||||||
<!-- <a-button @click="handleRenew" class="action-btn">批量续费</a-button> -->
|
<a-button @click="handleBatchRenew" class="action-btn">批量续费</a-button>
|
||||||
<!-- <a-button class="refresh-btn">
|
<a-button class="refresh-btn" @click="refreshData">
|
||||||
<reload-outlined />
|
<reload-outlined />
|
||||||
</a-button> -->
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-filter">
|
<div class="header-filter">
|
||||||
<a-select placeholder="筛选标签" style="width: 160px;" size="large">
|
<a-select
|
||||||
<a-select-option value="all">全部标签</a-select-option>
|
v-model:value="filterStatus"
|
||||||
|
placeholder="筛选状态"
|
||||||
|
style="width: 160px;"
|
||||||
|
size="large"
|
||||||
|
@change="handleSearch"
|
||||||
|
>
|
||||||
|
<a-select-option value="all">全部状态</a-select-option>
|
||||||
<a-select-option value="running">运行中</a-select-option>
|
<a-select-option value="running">运行中</a-select-option>
|
||||||
<a-select-option value="stopped">已停止</a-select-option>
|
<a-select-option value="stopped">已停止</a-select-option>
|
||||||
|
<a-select-option value="error">异常</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
<a-input-search placeholder="搜索实例名称/ID" style="width: 240px; margin-left: 12px;" size="large"
|
<a-input-search
|
||||||
@search="onSearch" />
|
v-model:value="searchKeyword"
|
||||||
|
placeholder="搜索实例名称/ID"
|
||||||
|
style="width: 240px; margin-left: 12px;"
|
||||||
|
size="large"
|
||||||
|
@search="handleSearch"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<a-table :dataSource="listData" :columns="columns" bordered :pagination="paginationState" @change="onTableChange">
|
<a-table
|
||||||
|
:dataSource="tableData"
|
||||||
|
:columns="columns"
|
||||||
|
bordered
|
||||||
|
:pagination="paginationState"
|
||||||
|
:loading="loading"
|
||||||
|
@change="onTableChange"
|
||||||
|
>
|
||||||
|
<!-- 状态列 -->
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'gpu_model'">
|
<template v-if="column.key === 'status'">
|
||||||
<div>
|
<span :class="`status-${record.status}`">
|
||||||
{{ 'GPU型号:' + record.gpu_model,'GPU数量:' + record.gpu_count ,'单卡显存GB:' + record.gpu_memory_gb , 'CPU核数:' + record.cpu_cores , '内存(MB):' + record.memory_mb}}
|
{{ getStatusText(record.status) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 规格详情列 -->
|
||||||
|
<template v-else-if="column.key === 'spec'">
|
||||||
|
<div class="spec-info">
|
||||||
|
<div class="spec-summary">
|
||||||
|
<div>{{ record.gpu_model }}</div>
|
||||||
|
<div class="spec-detail-trigger" @click="showSpecDetail(record)">
|
||||||
|
<eye-outlined /> 查看详情
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 健康状态列 -->
|
||||||
|
<template v-else-if="column.key === 'health_status'">
|
||||||
|
<span :class="`health-${record.health_status}`">
|
||||||
|
{{ getHealthStatusText(record.health_status) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作列 -->
|
||||||
|
<template v-else-if="column.key === 'actions'">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<a-button
|
||||||
|
v-if="record.status === 'running'"
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
@click="handlePowerOff(record)"
|
||||||
|
>
|
||||||
|
关机
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
v-else-if="record.status === 'stopped'"
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="handlePowerOn(record)"
|
||||||
|
>
|
||||||
|
开机
|
||||||
|
</a-button>
|
||||||
|
|
||||||
|
<a-dropdown :trigger="['click']">
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu @click="({ key }) => handleMenuClick(key, record)">
|
||||||
|
<a-menu-item key="reset">
|
||||||
|
<template #icon>
|
||||||
|
<redo-outlined />
|
||||||
|
</template>
|
||||||
|
容器重置
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="release" danger>
|
||||||
|
<template #icon>
|
||||||
|
<delete-outlined />
|
||||||
|
</template>
|
||||||
|
释放实例
|
||||||
|
</a-menu-item>
|
||||||
|
|
||||||
|
<!-- <a-menu-item key="monitor">
|
||||||
|
<template #icon>
|
||||||
|
<dashboard-outlined />
|
||||||
|
</template>
|
||||||
|
监控
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="log">
|
||||||
|
<template #icon>
|
||||||
|
<file-text-outlined />
|
||||||
|
</template>
|
||||||
|
日志
|
||||||
|
</a-menu-item> -->
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
<a-button type="link" size="small">
|
||||||
|
更多
|
||||||
|
<down-outlined />
|
||||||
|
</a-button>
|
||||||
|
</a-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页 -->
|
|
||||||
|
|
||||||
|
<!-- 规格详情模态框 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="specModalVisible"
|
||||||
|
title="规格详情"
|
||||||
|
width="500px"
|
||||||
|
:footer="null"
|
||||||
|
>
|
||||||
|
<div class="spec-detail-content">
|
||||||
|
<a-descriptions :column="1" bordered size="small">
|
||||||
|
<a-descriptions-item label="GPU型号">{{ specDetail.gpu_model }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="GPU数量">{{ specDetail.gpu_count }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="单卡显存">{{ specDetail.gpu_memory_gb }} GB</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="CPU核数">{{ specDetail.cpu_cores }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="内存">{{ specDetail.memory_mb }} MB</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="系统盘">{{ specDetail.system_disk }} GB</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="数据盘">{{ specDetail.data_disk || '无' }} GB</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="网络带宽">{{ specDetail.bandwidth || '100' }} Mbps</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 重置确认模态框 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="resetModalVisible"
|
||||||
|
title="确认重置"
|
||||||
|
@ok="confirmReset"
|
||||||
|
@cancel="resetModalVisible = false"
|
||||||
|
>
|
||||||
|
<div class="modal-content">
|
||||||
|
<exclamation-circle-outlined class="warning-icon" style="color: #faad14; font-size: 24px; margin-right: 12px;" />
|
||||||
|
<div>
|
||||||
|
<p style="font-weight: bold; margin-bottom: 8px;">确认要重置容器实例吗?</p>
|
||||||
|
<p style="color: #666; margin-bottom: 4px;">• 重置将清空容器内的所有数据</p>
|
||||||
|
<p style="color: #666; margin-bottom: 4px;">• 系统盘和数据盘将恢复初始状态</p>
|
||||||
|
<p style="color: #666;">• 实例配置和网络设置保持不变</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #okText>
|
||||||
|
<span style="color: #ff4d4f;">确认重置</span>
|
||||||
|
</template>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 释放确认模态框 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="releaseModalVisible"
|
||||||
|
title="确认释放"
|
||||||
|
@ok="confirmRelease"
|
||||||
|
@cancel="releaseModalVisible = false"
|
||||||
|
:ok-button-props="{ danger: true }"
|
||||||
|
>
|
||||||
|
<div class="modal-content">
|
||||||
|
<exclamation-circle-outlined class="warning-icon" style="color: #ff4d4f; font-size: 24px; margin-right: 12px;" />
|
||||||
|
<div>
|
||||||
|
<p style="font-weight: bold; margin-bottom: 8px;">确认要释放实例吗?</p>
|
||||||
|
<p style="color: #ff4d4f; margin-bottom: 4px;">• 释放后实例将被永久删除</p>
|
||||||
|
<p style="color: #ff4d4f; margin-bottom: 4px;">• 所有数据将无法恢复</p>
|
||||||
|
<p style="color: #ff4d4f; margin-bottom: 4px;">• 网络配置、IP地址将被回收</p>
|
||||||
|
<p style="color: #666;">请确保已备份重要数据!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onBeforeMount } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { usePagination } from '@/hooks'
|
|
||||||
import { hostCaseList } from '@/apis/admin'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import {
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
DownOutlined,
|
||||||
|
RedoOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
DashboardOutlined,
|
||||||
|
FileTextOutlined
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
import type { TableColumnType } from 'ant-design-vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
import dayjs from 'dayjs'
|
|
||||||
const { listData, loading, showLoading, hideLoading, paginationState, resetPagination, searchFormData } = usePagination()
|
|
||||||
const columns = ref([
|
|
||||||
{ title: '实例ID', dataIndex: 'id', key: 'id' },
|
|
||||||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
|
||||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
|
||||||
{ title: '规格详情', dataIndex: 'gpu_model', key: 'gpu_model' },
|
|
||||||
{ title: '本地磁盘', dataIndex: 'system_disk', key: 'system_disk' },
|
|
||||||
{ title: '健康状态', dataIndex: 'health_status', key: 'health_status' },
|
|
||||||
{ title: '付费方式', dataIndex: 'price_type', key: 'price_type' },
|
|
||||||
{ title: '释放时间', dataIndex: 'release_at', key: 'release_at' },
|
|
||||||
{ title: '停机时间', dataIndex: 'down_at', key: 'down_at' },
|
|
||||||
{ title: 'SSH登陆', dataIndex: 'ssh_link', key: 'ssh_link' },
|
|
||||||
{ title: '操作', dataIndex: 'aciton', key: 'aciton' },
|
|
||||||
])
|
|
||||||
onBeforeMount(() => {
|
|
||||||
getPageList()
|
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
const filterStatus = ref('all')
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const specModalVisible = ref(false)
|
||||||
|
const resetModalVisible = ref(false)
|
||||||
|
const releaseModalVisible = ref(false)
|
||||||
|
|
||||||
|
// 当前操作实例
|
||||||
|
const currentInstance = ref<any>(null)
|
||||||
|
const specDetail = ref({
|
||||||
|
gpu_model: '',
|
||||||
|
gpu_count: 0,
|
||||||
|
gpu_memory_gb: 0,
|
||||||
|
cpu_cores: 0,
|
||||||
|
memory_mb: 0,
|
||||||
|
system_disk: 0,
|
||||||
|
data_disk: 0,
|
||||||
|
bandwidth: 0
|
||||||
})
|
})
|
||||||
const getPageList = async () => {
|
|
||||||
try {
|
|
||||||
const { pageSize, current } = paginationState
|
|
||||||
const res: any = await hostCaseList({ pageSize: pageSize, pageNum: current });
|
|
||||||
listData.value = res.list;
|
|
||||||
paginationState.total = res?.Total;
|
|
||||||
|
|
||||||
} catch (error: any) {
|
// 分页状态
|
||||||
console.error('产品优势请求失败:', error);
|
const paginationState = ref({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
const mockData = ref([
|
||||||
|
{
|
||||||
|
id: 'ins-2024010001',
|
||||||
|
name: 'AI训练实例-01',
|
||||||
|
status: 'running',
|
||||||
|
gpu_model: 'NVIDIA A100',
|
||||||
|
gpu_count: 2,
|
||||||
|
gpu_memory_gb: 80,
|
||||||
|
cpu_cores: 8,
|
||||||
|
memory_mb: 16384,
|
||||||
|
system_disk: 200,
|
||||||
|
data_disk: 500,
|
||||||
|
health_status: 'healthy',
|
||||||
|
price_type: '按量付费',
|
||||||
|
release_at: '2024-12-31',
|
||||||
|
down_at: '-',
|
||||||
|
ssh_link: 'ssh root@10.0.0.1',
|
||||||
|
bandwidth: 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ins-2024010002',
|
||||||
|
name: '推理服务实例',
|
||||||
|
status: 'stopped',
|
||||||
|
gpu_model: 'NVIDIA V100',
|
||||||
|
gpu_count: 1,
|
||||||
|
gpu_memory_gb: 32,
|
||||||
|
cpu_cores: 4,
|
||||||
|
memory_mb: 8192,
|
||||||
|
system_disk: 100,
|
||||||
|
data_disk: 200,
|
||||||
|
health_status: 'healthy',
|
||||||
|
price_type: '包月',
|
||||||
|
release_at: '2024-11-30',
|
||||||
|
down_at: '2024-10-15',
|
||||||
|
ssh_link: 'ssh root@10.0.0.2',
|
||||||
|
bandwidth: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ins-2024010003',
|
||||||
|
name: '开发测试实例',
|
||||||
|
status: 'running',
|
||||||
|
gpu_model: 'NVIDIA RTX 4090',
|
||||||
|
gpu_count: 1,
|
||||||
|
gpu_memory_gb: 24,
|
||||||
|
cpu_cores: 12,
|
||||||
|
memory_mb: 32768,
|
||||||
|
system_disk: 500,
|
||||||
|
data_disk: 1000,
|
||||||
|
health_status: 'warning',
|
||||||
|
price_type: '按量付费',
|
||||||
|
release_at: '2025-01-15',
|
||||||
|
down_at: '-',
|
||||||
|
ssh_link: 'ssh root@10.0.0.3',
|
||||||
|
bandwidth: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ins-2024010004',
|
||||||
|
name: '数据科学实例',
|
||||||
|
status: 'error',
|
||||||
|
gpu_model: 'NVIDIA T4',
|
||||||
|
gpu_count: 1,
|
||||||
|
gpu_memory_gb: 16,
|
||||||
|
cpu_cores: 8,
|
||||||
|
memory_mb: 16384,
|
||||||
|
system_disk: 150,
|
||||||
|
data_disk: 300,
|
||||||
|
health_status: 'error',
|
||||||
|
price_type: '包年',
|
||||||
|
release_at: '2024-12-15',
|
||||||
|
down_at: '2024-10-10',
|
||||||
|
ssh_link: 'ssh root@10.0.0.4',
|
||||||
|
bandwidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ins-2024010005',
|
||||||
|
name: '深度学习实例',
|
||||||
|
status: 'running',
|
||||||
|
gpu_model: 'NVIDIA A6000',
|
||||||
|
gpu_count: 4,
|
||||||
|
gpu_memory_gb: 48,
|
||||||
|
cpu_cores: 16,
|
||||||
|
memory_mb: 65536,
|
||||||
|
system_disk: 1000,
|
||||||
|
data_disk: 2000,
|
||||||
|
health_status: 'healthy',
|
||||||
|
price_type: '按量付费',
|
||||||
|
release_at: '2025-03-20',
|
||||||
|
down_at: '-',
|
||||||
|
ssh_link: 'ssh root@10.0.0.5',
|
||||||
|
bandwidth: 2000
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns = ref<TableColumnType[]>([
|
||||||
|
{ title: '实例ID', dataIndex: 'id', key: 'id', width: 150 },
|
||||||
|
{ title: '名称', dataIndex: 'name', key: 'name', width: 180 },
|
||||||
|
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
||||||
|
{ title: '规格详情', dataIndex: 'spec', key: 'spec', width: 200 },
|
||||||
|
{ title: '本地磁盘', dataIndex: 'system_disk', key: 'system_disk', width: 120 },
|
||||||
|
{ title: '健康状态', dataIndex: 'health_status', key: 'health_status', width: 120 },
|
||||||
|
{ title: '付费方式', dataIndex: 'price_type', key: 'price_type', width: 120 },
|
||||||
|
{ title: '释放时间', dataIndex: 'release_at', key: 'release_at', width: 120 },
|
||||||
|
{ title: '停机时间', dataIndex: 'down_at', key: 'down_at', width: 120 },
|
||||||
|
{ title: 'SSH登录', dataIndex: 'ssh_link', key: 'ssh_link', width: 150 },
|
||||||
|
{ title: '操作', key: 'actions', width: 150 }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 计算表格数据(应用筛选)
|
||||||
|
const tableData = computed(() => {
|
||||||
|
let filtered = [...mockData.value]
|
||||||
|
|
||||||
|
// 状态筛选
|
||||||
|
if (filterStatus.value !== 'all') {
|
||||||
|
filtered = filtered.filter(item => item.status === filterStatus.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关键词搜索
|
||||||
|
if (searchKeyword.value) {
|
||||||
|
const keyword = searchKeyword.value.toLowerCase()
|
||||||
|
filtered = filtered.filter(item =>
|
||||||
|
item.id.toLowerCase().includes(keyword) ||
|
||||||
|
item.name.toLowerCase().includes(keyword)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新分页总数
|
||||||
|
paginationState.value.total = filtered.length
|
||||||
|
|
||||||
|
// 分页处理
|
||||||
|
const start = (paginationState.value.current - 1) * paginationState.value.pageSize
|
||||||
|
const end = start + paginationState.value.pageSize
|
||||||
|
return filtered.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 状态文本映射
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
running: '运行中',
|
||||||
|
stopped: '已停止',
|
||||||
|
error: '异常'
|
||||||
|
}
|
||||||
|
return map[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 健康状态文本映射
|
||||||
|
const getHealthStatusText = (status: string) => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
healthy: '健康',
|
||||||
|
warning: '警告',
|
||||||
|
error: '异常'
|
||||||
|
}
|
||||||
|
return map[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示规格详情
|
||||||
|
const showSpecDetail = (record: any) => {
|
||||||
|
specDetail.value = {
|
||||||
|
gpu_model: record.gpu_model,
|
||||||
|
gpu_count: record.gpu_count,
|
||||||
|
gpu_memory_gb: record.gpu_memory_gb,
|
||||||
|
cpu_cores: record.cpu_cores,
|
||||||
|
memory_mb: record.memory_mb,
|
||||||
|
system_disk: record.system_disk,
|
||||||
|
data_disk: record.data_disk,
|
||||||
|
bandwidth: record.bandwidth
|
||||||
|
}
|
||||||
|
specModalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关机操作
|
||||||
|
const handlePowerOff = (record: any) => {
|
||||||
|
currentInstance.value = record
|
||||||
|
message.loading({ content: '正在关机...', key: 'powerOff', duration: 0 })
|
||||||
|
setTimeout(() => {
|
||||||
|
record.status = 'stopped'
|
||||||
|
record.down_at = new Date().toISOString().split('T')[0]
|
||||||
|
message.success({ content: '关机成功', key: 'powerOff' })
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开机操作
|
||||||
|
const handlePowerOn = (record: any) => {
|
||||||
|
currentInstance.value = record
|
||||||
|
message.loading({ content: '正在开机...', key: 'powerOn', duration: 0 })
|
||||||
|
setTimeout(() => {
|
||||||
|
record.status = 'running'
|
||||||
|
record.down_at = '-'
|
||||||
|
message.success({ content: '开机成功', key: 'powerOn' })
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更多菜单点击
|
||||||
|
const handleMenuClick = (key: string, record: any) => {
|
||||||
|
currentInstance.value = record
|
||||||
|
switch (key) {
|
||||||
|
case 'reset':
|
||||||
|
resetModalVisible.value = true
|
||||||
|
break
|
||||||
|
case 'release':
|
||||||
|
releaseModalVisible.value = true
|
||||||
|
break
|
||||||
|
case 'monitor':
|
||||||
|
message.info('跳转到监控页面')
|
||||||
|
break
|
||||||
|
case 'log':
|
||||||
|
message.info('跳转到日志页面')
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* 分页
|
// 确认重置
|
||||||
*/
|
const confirmReset = () => {
|
||||||
function onTableChange({ current, pageSize }) {
|
if (!currentInstance.value) return
|
||||||
paginationState.current = current
|
|
||||||
paginationState.pageSize = pageSize
|
message.loading({ content: '正在重置实例...', key: 'reset', duration: 0 })
|
||||||
getPageList()
|
setTimeout(() => {
|
||||||
}
|
// 模拟重置操作
|
||||||
/**
|
currentInstance.value.health_status = 'healthy'
|
||||||
* 搜索
|
message.success({ content: '实例重置成功', key: 'reset' })
|
||||||
*/
|
resetModalVisible.value = false
|
||||||
function handleSearch() {
|
}, 2000)
|
||||||
resetPagination()
|
|
||||||
getPageList()
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 重置
|
|
||||||
*/
|
|
||||||
function handleResetSearch() {
|
|
||||||
searchFormData.value = {}
|
|
||||||
resetPagination()
|
|
||||||
getPageList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 租用实例
|
// 确认释放
|
||||||
const handleRent = () => {
|
const confirmRelease = () => {
|
||||||
router.push('/layout/admin/instanceCreate');
|
if (!currentInstance.value) return
|
||||||
|
|
||||||
|
message.loading({ content: '正在释放实例...', key: 'release', duration: 0 })
|
||||||
|
setTimeout(() => {
|
||||||
|
// 从数据中移除
|
||||||
|
const index = mockData.value.findIndex(item => item.id === currentInstance.value.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
mockData.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
message.success({ content: '实例释放成功', key: 'release' })
|
||||||
|
releaseModalVisible.value = false
|
||||||
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量续费
|
||||||
|
const handleBatchRenew = () => {
|
||||||
|
message.info('批量续费功能')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
const refreshData = () => {
|
||||||
|
loading.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false
|
||||||
|
message.success('数据已刷新')
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
paginationState.value.current = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页变化
|
||||||
|
const onTableChange = (pag: any) => {
|
||||||
|
paginationState.value = {
|
||||||
|
...paginationState.value,
|
||||||
|
current: pag.current,
|
||||||
|
pageSize: pag.pageSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建实例
|
||||||
|
const handleRent = () => {
|
||||||
|
router.push('/layout/admin/instanceCreate')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
paginationState.value.total = mockData.value.length
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.instance-list {
|
.instance-list {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: #f5f7fa;
|
background: #f5f7fa;
|
||||||
// min-height: 100vh;
|
min-height: calc(100vh - 48px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-section {
|
.header-section {
|
||||||
@ -185,33 +589,6 @@ const handleRent = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-quick-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
.quick-action-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: #1890ff;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 0.3s;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #40a9ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-action-icon {
|
|
||||||
margin-right: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-bottom {
|
.header-bottom {
|
||||||
@ -240,6 +617,13 @@ const handleRent = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #1890ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,49 +639,113 @@ const handleRent = () => {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid #e8e8e8;
|
border: 1px solid #e8e8e8;
|
||||||
|
|
||||||
|
:deep(.ant-table) {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
background: #fafafa;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2d3d;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.instance-table {
|
.spec-info {
|
||||||
.empty-state {
|
.spec-summary {
|
||||||
padding: 60px 20px;
|
// display: flex;
|
||||||
text-align: center;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
.empty-icon {
|
|
||||||
font-size: 64px;
|
.spec-detail-trigger {
|
||||||
color: #d9d9d9;
|
color: #1890ff;
|
||||||
margin-bottom: 16px;
|
cursor: pointer;
|
||||||
}
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
.empty-text {
|
align-items: center;
|
||||||
font-size: 16px;
|
gap: 4px;
|
||||||
color: #8c8c8c;
|
|
||||||
margin-bottom: 8px;
|
&:hover {
|
||||||
}
|
color: #40a9ff;
|
||||||
|
}
|
||||||
.empty-desc {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #bfbfbf;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 状态样式
|
.spec-detail-content {
|
||||||
:deep(.status-running) {
|
padding: 8px 0;
|
||||||
color: #52c41a;
|
|
||||||
background: #f6ffed;
|
:deep(.ant-descriptions-item-label) {
|
||||||
padding: 4px 8px;
|
font-weight: 500;
|
||||||
border-radius: 4px;
|
background: #fafafa;
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.status-stopped) {
|
.action-buttons {
|
||||||
color: #ff4d4f;
|
display: flex;
|
||||||
background: #fff2f0;
|
align-items: center;
|
||||||
padding: 4px 8px;
|
gap: 8px;
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
:deep(.ant-btn-link) {
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 状态样式
|
||||||
|
.status-running {
|
||||||
|
color: #52c41a;
|
||||||
|
background: #f6ffed;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-stopped {
|
||||||
|
color: #fa8c16;
|
||||||
|
background: #fff7e6;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
color: #ff4d4f;
|
||||||
|
background: #fff2f0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 健康状态样式
|
||||||
|
.health-healthy {
|
||||||
|
color: #52c41a;
|
||||||
|
background: #f6ffed;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-warning {
|
||||||
|
color: #faad14;
|
||||||
|
background: #fffbe6;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-error {
|
||||||
|
color: #ff4d4f;
|
||||||
|
background: #fff2f0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.pagination-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -320,16 +768,11 @@ const handleRent = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.pagination-right {
|
.modal-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
|
|
||||||
.goto-text {
|
|
||||||
color: #8c8c8c;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式设计
|
// 响应式设计
|
||||||
@ -339,10 +782,6 @@ const handleRent = () => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-quick-actions {
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,10 +797,6 @@ const handleRent = () => {
|
|||||||
.header-left {
|
.header-left {
|
||||||
min-width: auto;
|
min-width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-quick-actions {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-bottom {
|
.header-bottom {
|
||||||
@ -381,10 +816,9 @@ const handleRent = () => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
.pagination-left,
|
.pagination-left {
|
||||||
.pagination-right {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user