2026-01-16 17:40:19 +08:00

864 lines
20 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">{{ userInfo.computingPowerPoint }}</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" style="width: calc(100% - 120px)"
@change="handleCustomAmountChange" :disabled="loading" />
<!-- <a-button type="primary" size="large" style="width: 120px" :disabled="!isCustomAmountValid || loading"
@click="handleCustomAmountConfirm">
确认
</a-button> -->
</a-input-group>
<p class="input-name">用户自定义输入算力点数量</p>
<div v-if="customAmountError" class="error-text">
<ExclamationCircleOutlined />
{{ customAmountError }}
</div>
</div>
</div>
<!-- 兑换规则说明 -->
<div class="exchange-section">
<h3 class="section-title">兑换规则</h3>
<div class="exchange-rules">
<div class="rule-item">
<CheckCircleOutlined class="rule-icon" />
<span>算力点兑换比例为人民币 <strong>1元 = 1算力点</strong></span>
</div>
<div class="rule-item">
<ExclamationCircleOutlined class="rule-icon" />
<span><strong>算力点不可退</strong>,兑换前请确认需求</span>
</div>
<div class="rule-item">
<InfoCircleOutlined class="rule-icon" />
<span>兑换后算力点立即生效,可用于平台所有计算服务</span>
</div>
<div class="rule-item">
<SafetyOutlined class="rule-icon" />
<span>兑换过程安全加密,保障您的资金安全</span>
</div>
</div>
</div>
<!-- 兑换信息展示 -->
<div v-if="selectedExchangeAmount > 0" class="exchange-preview">
<div class="preview-card">
<div class="preview-header">
<div class="preview-title">兑换明细</div>
<a-button type="text" @click="clearSelection">
<CloseOutlined />
</a-button>
</div>
<div class="preview-content">
<div class="preview-item">
<span class="preview-label">兑换金额:</span>
<span class="preview-value">¥{{ selectedExchangeAmount }}</span>
</div>
<div class="preview-item">
<span class="preview-label">获得算力点:</span>
<span class="preview-value points-value">{{ selectedExchangeAmount }} 算力点</span>
</div>
<div class="preview-item">
<span class="preview-label">兑换后余额:</span>
<span class="preview-value">¥{{ formatCurrency(userInfo.balance - selectedExchangeAmount) }}</span>
</div>
<div class="preview-item">
<span class="preview-label">兑换后算力点:</span>
<span class="preview-value points-value">{{ formatPoints(userInfo.computingPowerPoint + selectedExchangeAmount) }}
算力点</span>
</div>
</div>
</div>
</div>
<!-- 用户协议确认 -->
<div class="agreement-section">
<a-checkbox v-model:checked="agreementChecked" :disabled="loading">
在确认兑换算力点前,我已阅读并确认
<a @click="showAgreementModal" class="agreement-link">《用户协议》</a>
</a-checkbox>
<div v-if="!agreementChecked && showAgreementError" class="error-text">
请阅读并确认用户协议
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<a-button type="primary" size="large" :loading="loading" :disabled="!canExchange" @click="handleExchange"
class="exchange-button">
<template #icon>
<ThunderboltOutlined />
</template>
确认兑换
</a-button>
<a-button size="large" @click="handleCancel" :disabled="loading" class="cancel-button">
取消
</a-button>
</div>
</div>
</a-card>
<!-- 主要内容区域 -->
<div class="main-content">
<a-card class="history-card" title="算力点兑换历史">
<a-table :columns="historyColumns" bordered :data-source="exchangeHistory" row-key="key" :pagination="false">
<!-- 自定义状态列的渲染 -->
<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. 最小兑换金额为1元最大单次兑换金额为10000元。</p>
<p>3. 兑换操作一经确认,不可撤销或退款。</p>
<h4>二、使用规则</h4>
<p>1. 算力点可用于平台提供的各项计算服务。</p>
<p>2. 算力点不设有效期,但平台保留根据业务调整的权利。</p>
<p>3. 算力点不可转让、不可提现、不可兑换为现金。</p>
<h4>三、免责声明</h4>
<p>1. 如因系统维护、升级等需要暂停服务,平台将提前公告。</p>
<p>2. 用户应妥善保管账户信息,因用户原因造成的损失平台不承担责任。</p>
<div class="agreement-footer">
<a-checkbox v-model:checked="modalAgreementChecked">
我已阅读并同意以上协议
</a-checkbox>
<a-button type="primary" @click="handleAgreementConfirm" :disabled="!modalAgreementChecked">
确认并关闭
</a-button>
</div>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import {
DollarCircleOutlined,
ThunderboltOutlined,
InfoCircleOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
SafetyOutlined,
CloseOutlined
} from '@ant-design/icons-vue'
import { message, Modal, Divider } from 'ant-design-vue'
import {exchangePoint} from '@/apis/home'
// 用户余额和算力点
const balance = ref(3568.5)
const points = ref(1250)
const userInfo=ref({})
// 金额选项
const amountOptions = ref([
{ value: 100, label: '100' },
{ value: 200, label: '200' },
{ value: 1000, label: '1000' },
{ value: 5000, label: '5000' }
])
// 注意:这里只定义 columns 结构,不包含 customRender
const historyColumns = [
{
title: '兑换金额',
dataIndex: 'amount',
key: 'amount',
align: 'right'
},
{
title: '获得算力点',
dataIndex: 'points',
key: 'points',
align: 'right'
},
{
title: '兑换时间',
dataIndex: 'time',
key: 'time'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center'
}
]
// 选择的金额
const selectedAmount = ref(0)
const customAmount = ref('')
const customAmountError = ref('')
// 用户协议确认
const agreementChecked = ref(false)
const showAgreementError = ref(false)
// 加载状态
const loading = ref(false)
// 模态框
const agreementModalVisible = ref(false)
const modalAgreementChecked = ref(false)
// 兑换记录
const exchangeHistory = ref([
{ key: 1, amount: 1000, points: 1000, time: '2024-03-15 14:30:22', status: 'success' },
{ key: 2, amount: 500, points: 500, time: '2024-03-10 09:15:45', status: 'success' },
{ key: 3, amount: 2000, points: 2000, time: '2024-03-05 16:20:33', status: 'success' },
{ key: 4, amount: 100, points: 100, time: '2024-02-28 11:45:12', status: 'success' },
{ key: 5, amount: 5000, points: 5000, time: '2024-02-20 13:05:27', status: 'success' }
])
// 计算选中的兑换金额
const selectedExchangeAmount = computed(() => {
if (selectedAmount.value > 0) {
return selectedAmount.value
}
const amount = parseFloat(customAmount.value)
return isNaN(amount) || amount <= 0 ? 0 : amount
})
// 检查是否可以兑换
const canExchange = computed(() => {
return selectedExchangeAmount.value > 0 &&
agreementChecked.value &&
selectedExchangeAmount.value <= balance.value &&
!loading.value
})
// 检查自定义金额是否有效
const isCustomAmountValid = computed(() => {
const amount = parseFloat(customAmount.value)
if (isNaN(amount) || amount <= 0) {
return false
}
if (amount < 100) {
customAmountError.value = '最小兑换金额为100元'
return false
}
if (amount > 10000) {
customAmountError.value = '单次最大兑换金额为10000元'
return false
}
if (amount > balance.value) {
customAmountError.value = '兑换金额不能超过余额'
return false
}
customAmountError.value = ''
return true
})
// 格式化货币
const formatCurrency = (value) => {
return value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
// 格式化算力点
const formatPoints = (value) => {
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
// 选择金额
const selectAmount = (amount) => {
selectedAmount.value = amount
customAmount.value = ''
customAmountError.value = ''
}
// 处理自定义金额变化
const handleCustomAmountChange = () => {
selectedAmount.value = 0
if (customAmount.value) {
const amount = parseFloat(customAmount.value)
if (!isNaN(amount)) {
if (amount < 1) {
customAmountError.value = '最小兑换金额为1元'
} else if (amount > 10000) {
customAmountError.value = '单次最大兑换金额为10000元'
} else if (amount > userInfo.value.balance) {
customAmountError.value = '兑换金额不能超过余额'
} else {
customAmountError.value = ''
}
}
} else {
customAmountError.value = ''
}
}
// 确认自定义金额
const handleCustomAmountConfirm = () => {
if (isCustomAmountValid.value) {
selectedAmount.value = 0
message.success(`已选择兑换¥${customAmount.value}`)
}
}
// 清空选择
const clearSelection = () => {
selectedAmount.value = 0
customAmount.value = ''
customAmountError.value = ''
}
// 显示用户协议模态框
const showAgreementModal = () => {
agreementModalVisible.value = true
}
// 处理协议确认
const handleAgreementConfirm = () => {
agreementChecked.value = true
agreementModalVisible.value = false
modalAgreementChecked.value = false
message.success('已确认用户协议')
}
// 处理兑换
const handleExchange = () => {
showAgreementError.value = false
if (!agreementChecked.value) {
showAgreementError.value = true
message.error('请先阅读并确认用户协议')
return
}
if (selectedExchangeAmount.value > userInfo.value.balance) {
message.error('兑换金额不能超过余额')
return
}
Modal.confirm({
title: '确认兑换',
content: `您确定要兑换 ¥${selectedExchangeAmount.value} 获得 ${selectedExchangeAmount.value} 算力点吗?此操作不可撤销。`,
okText: '确认兑换',
cancelText: '取消',
onOk: performExchange
})
}
// 执行兑换
const performExchange = async() => {
loading.value = true
try {
const params={
exchange_value:selectedExchangeAmount.value
}
const res=await exchangePoint(params)
console.log(res)
message.success(`兑换成功!获得 ${selectedExchangeAmount.value} 算力点`)
loading.value=false
} catch (error) {
loading.value=false
}
}
// 处理取消
const handleCancel = () => {
clearSelection()
agreementChecked.value = false
showAgreementError.value = false
}
// 获取状态颜色
const getStatusColor = (status) => {
const colors = {
success: 'green',
pending: 'orange',
failed: 'red'
}
return colors[status] || 'default'
}
// 获取状态文本
const getStatusText = (status) => {
const texts = {
success: '成功',
pending: '处理中',
failed: '失败'
}
return texts[status] || status
}
onMounted(() => {
const userInfoStr=localStorage.getItem("userInfo")
if(userInfoStr){
userInfo.value = JSON.parse(userInfoStr);
}
})
</script>
<style scoped>
/* 你的样式保持不变 */
.points-exchange-page {
padding: 20px;
/* min-height: 100vh; */
}
.breadcrumb {
margin-bottom: 16px;
}
.page-header {
margin-bottom: 24px;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 8px;
}
.page-description {
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
margin: 0;
}
.main-content {}
.balance-card {
margin-bottom: 20px;
border-radius: 12px;
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); */
}
.balance-info {
display: flex;
gap: 40px;
margin-bottom: 16px;
}
.balance-item {
flex: 1;
}
.balance-label {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: #f0f5ff;
border-radius: 8px;
margin-right: 12px;
color: #1890ff;
font-size: 18px;
}
.label-text {
font-size: 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);
}
.balance-value .points-unit {
font-size: 16px;
color: rgba(0, 0, 0, 0.45);
margin-left: 8px;
}
.balance-tips {
padding: 12px 16px;
background: #f6ffed;
border-radius: 8px;
color: #52c41a;
font-size: 14px;
}
.balance-tips :deep(.anticon) {
margin-right: 8px;
}
.exchange-card {
margin-bottom: 20px;
border-radius: 12px;
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); */
}
.exchange-content {
padding: 8px 0;
}
.exchange-section {
margin-bottom: 32px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 8px;
width: 120px;
}
.section-subtitle {
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
margin-bottom: 20px;
}
.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;
/* padding: 6px !important; */
border-radius: 8px !important;
border: 2px solid #f0f0f0 !important;
background: #fff !important;
transition: all 0.3s ease;
}
.amount-option:hover {
border-color: #1890ff !important;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2);
}
.amount-option.selected {
border-color: #1890ff !important;
background: #f0f5ff !important;
}
.amount-option-content {
text-align: center;
}
.amount-number {
font-size: 20px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 4px;
}
.amount-points {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.custom-amount {
width: 460px;
margin-bottom: 24px;
margin-left: 170px;
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;
}
}
.error-text {
color: #ff4d4f;
font-size: 12px;
margin-top: 8px;
display: flex;
align-items: center;
gap: 4px;
}
.exchange-rules {
background: #fafafa;
padding: 20px;
border-radius: 8px;
}
.rule-item {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
}
.rule-item:last-child {
margin-bottom: 0;
}
.rule-icon {
margin-right: 12px;
margin-top: 2px;
font-size: 16px;
}
.rule-item:nth-child(1) .rule-icon {
color: #52c41a;
}
.rule-item:nth-child(2) .rule-icon {
color: #faad14;
}
.rule-item:nth-child(3) .rule-icon {
color: #1890ff;
}
.rule-item:nth-child(4) .rule-icon {
color: #722ed1;
}
.exchange-preview {
margin: 24px 0;
}
.preview-card {
background: #f6ffed;
border: 1px solid #b7eb8f;
border-radius: 8px;
padding: 20px;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.preview-title {
font-size: 16px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
.preview-content {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px 60px;
}
@media (max-width: 576px) {
.preview-content {
grid-template-columns: 1fr;
}
}
.preview-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.preview-label {
color: rgba(0, 0, 0, 0.45);
}
.preview-value {
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
.preview-value.points-value {
color: #722ed1;
}
.agreement-section {
margin: 24px 0;
padding: 16px;
background: #fafafa;
border-radius: 8px;
}
.agreement-link {
color: #1890ff;
margin-left: 4px;
}
.agreement-link:hover {
color: #40a9ff;
}
.action-buttons {
display: flex;
gap: 16px;
margin-top: 32px;
}
.exchange-button {
flex: 1;
height: 48px;
font-size: 16px;
font-weight: 500;
}
.cancel-button {
width: 120px;
height: 48px;
}
.history-card {
border-radius: 12px;
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); */
}
.exchange-amount {
color: #1890ff;
font-weight: 500;
}
.exchange-points {
color: #722ed1;
font-weight: 500;
}
.view-all-link {
text-align: center;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
margin-top: 16px;
}
.agreement-content {
max-height: 60vh;
overflow-y: auto;
padding: 0 8px;
}
.agreement-content h3 {
color: rgba(0, 0, 0, 0.85);
margin-bottom: 16px;
}
.agreement-content h4 {
color: rgba(0, 0, 0, 0.85);
margin: 16px 0 8px;
}
.agreement-content p {
color: rgba(0, 0, 0, 0.65);
margin-bottom: 12px;
line-height: 1.6;
}
.agreement-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
:deep(.ant-btn-loading) {
opacity: 0.8;
}
</style>