646 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="points-exchange-page">
<!-- 面包屑导航 -->
<a-breadcrumb class="breadcrumb">
<a-breadcrumb-item>
<router-link to="/layout/admin/myMoney">费用总览</router-link>
</a-breadcrumb-item>
<a-breadcrumb-item>算力点兑换</a-breadcrumb-item>
</a-breadcrumb>
<!-- 余额卡片 -->
<a-card class="balance-card">
<div class="balance-info">
<div class="balance-item">
<div class="balance-label">
<span class="label-text">我的余额</span>
</div>
<div class="balance-value">
<span class="currency">¥</span>
<span class="amount">{{ userInfo.balance }}</span>
</div>
</div>
<div class="balance-item">
<div class="balance-label">
<span class="label-text">算力点</span>
</div>
<div class="balance-value">
<span class="points">{{ formatPoints(points) }}</span>
</div>
</div>
</div>
</a-card>
<!-- 兑换卡片 -->
<a-card class="exchange-card" title="兑换算力点">
<div class="exchange-content">
<!-- 兑换金额选择 -->
<div class="exchange-section">
<div class="amount-options">
<div class="section-title">选择兑换金额</div>
<a-button v-for="option in amountOptions" :key="option.value"
:class="['amount-option', { 'selected': selectedAmount === option.value }]"
@click="selectAmount(option.value)" size="large">
<div class="amount-option-content">
<div class="amount-number">{{ option.value }}</div>
</div>
</a-button>
</div>
<!-- 自定义输入 -->
<div class="custom-amount">
<a-input-group compact>
<a-input v-model:value="customAmount" placeholder="输入其他金额" size="large" @change="handleCustomAmountChange"
:disabled="loading" />
</a-input-group>
<p class="input-name">用户自定义输入算力点数量</p>
<div v-if="customAmountError" class="error-text">
<ExclamationCircleOutlined />
{{ customAmountError }}
</div>
</div>
<div class="title">兑换说明算力点换算比例为人民币1 元 = 1 算力点,算力点兑换后不可退。</div>
</div>
<!-- 用户协议确认 -->
<div class="agreement-section">
<a-checkbox v-model:checked="agreementChecked" :disabled="loading">
在确认兑换算力点前,我已阅读并确认
<a @click="showAgreementModal" class="agreement-link">《用户协议》</a>
</a-checkbox>
<div v-if="!agreementChecked && showAgreementError" class="error-text">
请阅读并确认用户协议
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<a-button type="primary" size="large" @click="handleExchange" :loading="loading" :disabled="!canExchange" class="exchange-button">
确认兑换
</a-button>
</div>
</div>
</a-card>
<!-- 主要内容区域 -->
<div class="main-content">
<a-card class="history-card" title="算力点兑换历史">
<a-table :columns="historyColumns"
:data-source="listData"
row-key="key"
:pagination="paginationState"
@change="onTableChange">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<span :style="{ color: getStatusColor(record.status) }">{{ getStatusText(record.status) }}</span>
</template>
<template v-else-if="column.key === 'amount' || column.key === 'points'">
<span>{{ formatCurrency(record.amount) }}</span>
</template>
<template v-else>
{{ record[column.dataIndex] }}
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 用户协议模态框 -->
<a-modal v-model:visible="agreementModalVisible" title="用户协议" width="800px" :footer="null">
<div class="agreement-content">
<h3>算力点兑换服务协议</h3>
<p>欢迎使用算力点兑换服务。在兑换算力点前,请您仔细阅读以下协议内容:</p>
<h4>一、兑换规则</h4>
<p>1. 算力点兑换比例为1元人民币兑换1算力点。</p>
<p>2. 最大单次兑换金额为10000元。</p>
<p>3. 兑换操作一经确认,不可撤销或退款。</p>
<h4>二、使用规则</h4>
<p>1. 算力点可用于平台提供的各项计算服务。</p>
<p>2. 算力点不设有效期,但平台保留根据业务调整的权利。</p>
<p>3. 算力点不可转让、不可提现、不可兑换为现金。</p>
<h4>三、免责声明</h4>
<p>1. 如因系统维护、升级等需要暂停服务,平台将提前公告。</p>
<p>2. 用户应妥善保管账户信息,因用户原因造成的损失平台不承担责任。</p>
<div class="agreement-footer">
<a-checkbox v-model:checked="modalAgreementChecked">
我已阅读并同意以上协议
</a-checkbox>
<a-button type="primary" @click="handleAgreementConfirm" :disabled="!modalAgreementChecked">
确认并关闭
</a-button>
</div>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import {
ExclamationCircleOutlined
} from '@ant-design/icons-vue'
import { message, Modal } from 'ant-design-vue'
import { getExchangeList, exchangePoint } from '@/apis/admin'
import { usePagination } from '@/hooks'
const { listData, paginationState, resetPagination, searchFormData } = usePagination()
// 用户余额和算力点
const balance = ref(0)
const points = ref(0)
// 金额选项
const amountOptions = ref([
{ value: 100, label: '100' },
{ value: 200, label: '200' },
{ value: 1000, label: '1000' },
{ value: 5000, label: '5000' }
])
// 历史记录列定义
const historyColumns = [
{
title: '兑换金额',
dataIndex: 'exchange_amount',
key: 'exchange_amount',
align: 'center'
},
{
title: '获得算力点',
dataIndex: 'exchange_point',
key: 'exchange_point',
align: 'center'
},
{
title: '兑换时间',
dataIndex: 'created_at',
key: 'created_at'
},
]
// 选择的金额
const selectedAmount = ref(0)
const customAmount = ref('')
const customAmountError = ref('')
// 用户协议确认
const agreementChecked = ref(false)
const showAgreementError = ref(false)
// 加载状态
const loading = ref(false)
// 模态框
const agreementModalVisible = ref(false)
const modalAgreementChecked = ref(false)
// 计算选中的兑换金额
const selectedExchangeAmount = computed(() => {
if (selectedAmount.value > 0) {
return selectedAmount.value
}
const amount = parseFloat(customAmount.value)
return isNaN(amount) || amount <= 0 ? 0 : amount
})
// 检查是否可以兑换
const canExchange = computed(() => {
return selectedExchangeAmount.value > 0 &&
agreementChecked.value &&
selectedExchangeAmount.value <= balance.value &&
!loading.value
})
// 检查自定义金额是否有效
const isCustomAmountValid = computed(() => {
const amount = parseFloat(customAmount.value)
if (isNaN(amount) || amount <= 0) {
return false
}
if (amount > 10000) {
customAmountError.value = '单次最大兑换金额为10000元'
return false
}
if (amount > balance.value) {
customAmountError.value = '兑换金额不能超过余额'
return false
}
customAmountError.value = ''
return true
})
// 格式化货币
const formatCurrency = (value: any): string => {
const num = typeof value === 'number' ? value : parseFloat(value);
if (isNaN(num)) return '0.00';
return num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
const formatPoints = (value: any): string => {
const num = typeof value === 'number' ? value : parseFloat(value);
if (isNaN(num)) return '0';
return Math.floor(num).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
// 选择金额
const selectAmount = (amount: number) => {
selectedAmount.value = amount
customAmount.value = ''
customAmountError.value = ''
}
// 处理自定义金额变化
const handleCustomAmountChange = () => {
selectedAmount.value = 0
if (customAmount.value) {
const amount = parseFloat(customAmount.value)
if (!isNaN(amount)) {
if (amount > 10000) {
customAmountError.value = '单次最大兑换金额为10000元'
} else if (amount > userInfo.value.balance) {
customAmountError.value = '兑换金额不能超过余额'
} else {
customAmountError.value = ''
}
}
} else {
customAmountError.value = ''
}
}
// 显示用户协议模态框
const showAgreementModal = () => {
agreementModalVisible.value = true
}
// 处理协议确认
const handleAgreementConfirm = () => {
agreementChecked.value = true
agreementModalVisible.value = false
modalAgreementChecked.value = false
message.success('已确认用户协议')
}
// 处理兑换
const handleExchange = async () => {
showAgreementError.value = false
if (!agreementChecked.value) {
showAgreementError.value = true
message.error('请先阅读并确认用户协议')
return
}
if (selectedExchangeAmount.value > userInfo.value.balance) {
message.error('兑换金额不能超过余额')
return
}
Modal.confirm({
title: '确认兑换',
content: `您确定要兑换 ¥${selectedExchangeAmount.value} 获得 ${selectedExchangeAmount.value} 算力点吗?此操作不可撤销。`,
okText: '确认兑换',
cancelText: '取消',
async onOk() {
await performExchange()
}
})
}
// 执行兑换
const performExchange = async () => {
loading.value = true
try {
// 调用兑换接口
const response:any = await exchangePoint({
exchange_value: selectedExchangeAmount.value
})
console.log('兑换结果:', response)
if (response.code === 1) {
// 兑换成功,更新余额和算力点
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
userInfo.balance -= selectedExchangeAmount.value;
userInfo.computingPowerPoint += selectedExchangeAmount.value;
localStorage.setItem('userInfo', JSON.stringify(userInfo));
// 更新本地状态
balance.value = userInfo.balance;
points.value = userInfo.computingPowerPoint;
// 刷新历史记录
await getPageList();
// 清空选择
clearSelection();
agreementChecked.value = false;
message.success(`兑换成功`);
} else {
message.error(response.msg || '兑换失败');
}
} catch (error: any) {
console.error('兑换失败:', error);
message.error(error.msg || '兑换失败,请稍后重试');
} finally {
loading.value = false;
}
}
// 清空选择
const clearSelection = () => {
selectedAmount.value = 0
customAmount.value = ''
customAmountError.value = ''
}
// 获取状态颜色
const getStatusColor = (status: string) => {
const colors: Record<string, string> = {
success: 'green',
pending: 'orange',
failed: 'red'
}
return colors[status] || 'default'
}
// 获取状态文本
const getStatusText = (status: string) => {
const texts: Record<string, string> = {
success: '成功',
pending: '处理中',
failed: '失败'
}
return texts[status] || status
}
// 获取兑换历史列表
const getPageList = async () => {
try {
const { pageSize, current } = paginationState
const res: any = await getExchangeList({
page_size: pageSize,
page_num: current,
})
if (res.data && Array.isArray(res.data)) {
listData.value = res.data
paginationState.total = res.total || res.data.length
} else {
listData.value = []
paginationState.total = 0
}
} catch (error: any) {
console.error('兑换历史请求失败:', error)
listData.value = []
paginationState.total = 0
}
}
// 表格分页变化
function onTableChange({ current, pageSize }: { current: number, pageSize: number }) {
paginationState.current = current
paginationState.pageSize = pageSize
getPageList()
}
// 初始化
onMounted(() => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
if (userInfo.balance && userInfo.computingPowerPoint) {
balance.value = userInfo.balance;
points.value = userInfo.computingPowerPoint;
}
getPageList();
})
</script>
<style scoped>
/* 样式保持不变,同上 */
.points-exchange-page {
padding: 20px;
/* min-height: 100vh; */
}
.breadcrumb {
margin-bottom: 16px;
}
.balance-card {
margin-bottom: 20px;
border-radius: 12px;
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); */
}
.balance-info {
display: flex;
gap: 40px;
margin-bottom: 16px;
}
.balance-item {
flex: 1;
}
.balance-label {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.label-text {
font-size: 20px;
font-weight: 500;
color: rgba(56, 56, 56, 1);
}
.balance-value {
font-size: 18px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
.balance-value .currency {
font-size: 20px;
color: rgba(56, 56, 56, 1);
margin-right: 4px;
}
.balance-value .points {
color: rgba(56, 56, 56, 1);
}
.exchange-card {
margin-bottom: 20px;
border-radius: 12px;
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); */
}
.exchange-content {
padding: 8px 0;
}
.exchange-section {
margin-bottom: 32px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 8px;
width: 120px;
}
.amount-options {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 6px;
margin-bottom: 24px;
}
@media (max-width: 768px) {
.amount-options {
grid-template-columns: repeat(2, 1fr);
}
}
.amount-option {
height: auto !important;
border-radius: 8px !important;
border: 2px solid #f0f0f0 !important;
background: #fff !important;
transition: all 0.3s ease;
}
.amount-option:hover {
border-color: #1890ff !important;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2);
}
.amount-option.selected {
border-color: #1890ff !important;
background: #f0f5ff !important;
}
.amount-option-content {
text-align: center;
}
.amount-number {
font-size: 20px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 4px;
}
.custom-amount {
width: 345px;
margin-bottom: 24px;
margin-left: 180px;
}
.custom-amount input {
height: 45px;
}
.input-name {
width: 350px;
text-align: center;
font-size: 14px;
font-weight: 400;
color: rgba(166, 166, 166, 1);
height: 35px;
line-height: 35px;
}
.title {
font-size: 14px;
font-weight: 400;
letter-spacing: 0px;
line-height: 32.93px;
color: rgba(166, 166, 166, 1);
}
.error-text {
color: #ff4d4f;
font-size: 12px;
margin-top: 8px;
display: flex;
align-items: center;
gap: 4px;
}
.agreement-section {
margin: 24px 0;
padding: 16px;
background: #fafafa;
border-radius: 8px;
}
.agreement-link {
color: #1890ff;
margin-left: 4px;
}
.agreement-link:hover {
color: #40a9ff;
}
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 16px;
margin-top: 32px;
}
.exchange-button {
width: 120px;
height: 48px;
}
.history-card {
border-radius: 12px;
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); */
}
.agreement-content {
max-height: 60vh;
overflow-y: auto;
padding: 0 8px;
}
.agreement-content h3 {
color: rgba(0, 0, 0, 0.85);
margin-bottom: 16px;
}
.agreement-content h4 {
color: rgba(0, 0, 0, 0.85);
margin: 16px 0 8px;
}
.agreement-content p {
color: rgba(0, 0, 0, 0.65);
margin-bottom: 12px;
line-height: 1.6;
}
.agreement-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
:deep(.ant-btn-loading) {
opacity: 0.8;
}
</style>