646 lines
16 KiB
Vue
646 lines
16 KiB
Vue
<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> |