Files
AIGC/frontend/src/views/Payments.vue

750 lines
20 KiB
Vue
Raw Normal View History

<template>
<div class="payments">
<!-- 页面标题 -->
<div class="page-header">
<h2>
<el-icon><CreditCard /></el-icon>
{{ $t('payments.title') }}
</h2>
</div>
<!-- 筛选和搜索 -->
<el-card class="filter-card">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8">
<el-select
v-model="filters.status"
:placeholder="$t('payments.statusPlaceholder')"
clearable
@change="handleFilterChange"
>
<el-option :label="$t('payments.allStatus')" value="" />
<el-option :label="$t('payments.pending')" value="PENDING" />
<el-option :label="$t('payments.paid')" value="SUCCESS" />
<el-option :label="$t('payments.failed')" value="FAILED" />
<el-option :label="$t('payments.cancelled')" value="CANCELLED" />
</el-select>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-input
v-model="filters.search"
:placeholder="$t('payments.searchPlaceholder')"
clearable
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-button @click="resetFilters">{{ $t('common.reset') }}</el-button>
<el-button type="success" @click="showSubscriptionDialog('standard')">{{ $t('subscription.standard') }}</el-button>
<el-button type="warning" @click="showSubscriptionDialog('professional')">{{ $t('subscription.professional') }}</el-button>
</el-col>
</el-row>
</el-card>
<!-- 支付记录列表 -->
<el-card class="payments-card">
<el-table
:data="payments"
v-loading="loading"
:empty-text="$t('subscription.noPointsHistory')"
>
<el-table-column prop="orderId" :label="$t('orders.orderNumber')" width="150">
<template #default="{ row }">
<router-link :to="`/orders/${row.orderId}`" class="order-link">
{{ row.orderId }}
</router-link>
</template>
</el-table-column>
<el-table-column prop="amount" :label="$t('orders.amount')" width="120">
<template #default="{ row }">
<span class="amount">{{ row.currency }} {{ row.amount }}</span>
</template>
</el-table-column>
<el-table-column prop="paymentMethod" :label="$t('orders.paymentMethod')" width="120">
<template #default="{ row }">
<el-tag :type="getPaymentMethodType(row.paymentMethod)">
{{ getPaymentMethodText(row.paymentMethod) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" :label="$t('orders.status')" width="120">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" :label="$t('orders.description')" min-width="200">
<template #default="{ row }">
<span class="description">{{ row.description }}</span>
</template>
</el-table-column>
<el-table-column prop="createdAt" :label="$t('orders.createTime')" width="160">
<template #default="{ row }">
{{ formatDate(row.createdAt) }}
</template>
</el-table-column>
<el-table-column prop="paidAt" :label="$t('orders.paidTime')" width="160">
<template #default="{ row }">
{{ row.paidAt ? formatDate(row.paidAt) : '-' }}
</template>
</el-table-column>
<el-table-column :label="$t('orders.operation')" width="280" fixed="right">
<template #default="{ row }">
<el-button
size="small"
@click="viewPaymentDetail(row)"
>
{{ $t('common.view') }}
</el-button>
<el-button
v-if="row.status === 'PENDING'"
size="small"
type="success"
@click="testPaymentComplete(row)"
>
{{ $t('common.confirmTest') }}
</el-button>
<el-button
size="small"
type="danger"
@click="handleDeletePayment(row)"
>
{{ $t('common.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.size"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 支付详情对话框 -->
<el-dialog
v-model="detailDialogVisible"
:title="$t('payments.paymentDetail')"
width="600px"
>
<div v-if="currentPayment">
<el-descriptions :column="2" border>
<el-descriptions-item :label="$t('orders.orderNumber')">{{ currentPayment.orderId }}</el-descriptions-item>
<el-descriptions-item :label="$t('orders.paymentMethod')">
<el-tag :type="getPaymentMethodType(currentPayment.paymentMethod)">
{{ getPaymentMethodText(currentPayment.paymentMethod) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item :label="$t('orders.amount')">
<span class="amount">{{ currentPayment.currency }} {{ currentPayment.amount }}</span>
</el-descriptions-item>
<el-descriptions-item :label="$t('orders.status')">
<el-tag :type="getStatusType(currentPayment.status)">
{{ getStatusText(currentPayment.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item :label="$t('payments.externalTransactionId')" v-if="currentPayment.externalTransactionId">
{{ currentPayment.externalTransactionId }}
</el-descriptions-item>
<el-descriptions-item :label="$t('orders.createTime')">{{ formatDate(currentPayment.createdAt) }}</el-descriptions-item>
<el-descriptions-item :label="$t('orders.paidTime')" v-if="currentPayment.paidAt">
{{ formatDate(currentPayment.paidAt) }}
</el-descriptions-item>
<el-descriptions-item :label="$t('payments.updateTime')">{{ formatDate(currentPayment.updatedAt) }}</el-descriptions-item>
</el-descriptions>
<div v-if="currentPayment.description" class="payment-description">
<h4>{{ $t('orders.description') }}</h4>
<p>{{ currentPayment.description }}</p>
</div>
</div>
</el-dialog>
<!-- 订阅对话框 -->
<el-dialog
v-model="subscriptionDialogVisible"
:title="subscriptionDialogTitle"
width="500px"
>
<div class="subscription-info">
<h3>{{ subscriptionInfo.title }}</h3>
<p class="price">${{ subscriptionInfo.price }}</p>
<p class="description">{{ subscriptionInfo.description }}</p>
<div class="benefits">
<h4>{{ $t('subscription.features') }}</h4>
<ul>
<li v-for="benefit in subscriptionInfo.benefits" :key="benefit">
{{ benefit }}
</li>
</ul>
</div>
<div class="points-info">
<el-tag type="success">支付完成后可获得 {{ subscriptionInfo.points }} 积分</el-tag>
</div>
<div class="payment-method">
<h4>选择支付方式</h4>
<el-radio-group v-model="selectedPaymentMethod" @change="updatePrice">
<el-radio label="ALIPAY">支付宝</el-radio>
<el-radio label="PAYPAL">PayPal</el-radio>
</el-radio-group>
<div class="converted-price" v-if="convertedPrice">
<p>支付金额<span class="price-display">{{ convertedPrice }}</span></p>
</div>
</div>
</div>
<template #footer>
<el-button @click="subscriptionDialogVisible = false">取消</el-button>
<el-button type="primary" @click="createSubscription" :loading="subscriptionLoading">
立即订阅
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useI18n } from 'vue-i18n'
import {
Money,
CreditCard,
Wallet,
User as Search,
User as Filter,
User as Plus,
User as View,
User as Refresh,
User as Download,
User as Upload,
Setting,
Check,
Close,
User as Warning
} from '@element-plus/icons-vue'
import { getPayments, testPaymentComplete as testPaymentCompleteApi, createTestPayment, deletePayment } from '@/api/payments'
import { useUserStore } from '@/stores/user'
const { t } = useI18n()
const userStore = useUserStore()
const loading = ref(false)
const payments = ref([])
// 筛选条件
const filters = reactive({
status: '',
search: ''
})
// 分页信息
const pagination = reactive({
page: 1,
size: 10,
total: 0
})
// 支付详情对话框
const detailDialogVisible = ref(false)
const currentPayment = ref(null)
// 订阅对话框
const subscriptionDialogVisible = ref(false)
const subscriptionLoading = ref(false)
const subscriptionType = ref('')
const selectedPaymentMethod = ref('ALIPAY')
const convertedPrice = ref('')
const exchangeRate = ref(7.2) // 美元对人民币汇率,可以根据实际情况调整
const subscriptionInfo = reactive({
title: '',
price: 0,
description: '',
benefits: [],
points: 0
})
// 计算属性
const subscriptionDialogTitle = computed(() => {
return subscriptionType.value === 'standard' ? '标准版订阅' : '专业版订阅'
})
// 获取支付方式类型
const getPaymentMethodType = (method) => {
const methodMap = {
'ALIPAY': 'primary',
'PAYPAL': 'success',
'WECHAT': 'success',
'UNIONPAY': 'warning'
}
return methodMap[method] || ''
}
// 获取支付方式文本
const getPaymentMethodText = (method) => {
const methodMap = {
'ALIPAY': '支付宝',
'PAYPAL': 'PayPal',
'WECHAT': '微信支付',
'UNIONPAY': '银联支付'
}
return methodMap[method] || method
}
// 获取状态类型
const getStatusType = (status) => {
const statusMap = {
'PENDING': 'warning',
'SUCCESS': 'success',
'FAILED': 'danger',
'CANCELLED': 'info'
}
return statusMap[status] || ''
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'PENDING': '待支付',
'SUCCESS': '支付成功',
'FAILED': '支付失败',
'CANCELLED': '已取消'
}
return statusMap[status] || status
}
// 格式化日期
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
// 获取支付记录列表
const fetchPayments = async () => {
try {
loading.value = true
const response = await getPayments({
page: pagination.page - 1,
size: pagination.size,
status: filters.status,
search: filters.search
})
if (response.success) {
payments.value = response.data
pagination.total = response.total || response.data.length
} else {
ElMessage.error(response.message || t('common.fetchPaymentsFailed'))
}
} catch (error) {
console.error('Fetch payments error:', error)
ElMessage.error(t('common.fetchPaymentsFailed'))
} finally {
loading.value = false
}
}
// 筛选变化
const handleFilterChange = () => {
pagination.page = 1
fetchPayments()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
fetchPayments()
}
// 重置筛选
const resetFilters = () => {
filters.status = ''
filters.search = ''
pagination.page = 1
fetchPayments()
}
// 分页大小变化
const handleSizeChange = (size) => {
pagination.size = size
pagination.page = 1
fetchPayments()
}
// 当前页变化
const handleCurrentChange = (page) => {
pagination.page = page
fetchPayments()
}
// 查看支付详情
const viewPaymentDetail = (payment) => {
currentPayment.value = payment
detailDialogVisible.value = true
}
// 更新价格显示
const updatePrice = () => {
if (selectedPaymentMethod.value === 'ALIPAY') {
// 支付宝使用人民币
const cnyPrice = (subscriptionInfo.price * exchangeRate.value).toFixed(2)
convertedPrice.value = `¥${cnyPrice}`
} else if (selectedPaymentMethod.value === 'PAYPAL') {
// PayPal使用美元
convertedPrice.value = `$${subscriptionInfo.price}`
}
}
// 显示订阅对话框
const showSubscriptionDialog = (type) => {
if (!userStore.isAuthenticated) {
ElMessage.warning(t('common.pleaseLoginFirst'))
return
}
subscriptionType.value = type
if (type === 'standard') {
subscriptionInfo.title = '标准版订阅'
subscriptionInfo.price = 59
subscriptionInfo.description = '适合个人用户的基础功能订阅'
subscriptionInfo.benefits = [
'基础AI功能使用',
'每月100次API调用',
'邮件技术支持',
'基础模板库访问'
]
subscriptionInfo.points = 200
} else if (type === 'professional') {
subscriptionInfo.title = '专业版订阅'
subscriptionInfo.price = 259
subscriptionInfo.description = '适合企业用户的高级功能订阅'
subscriptionInfo.benefits = [
'高级AI功能使用',
'每月1000次API调用',
'优先技术支持',
'完整模板库访问',
'API接口集成',
'数据分析报告'
]
subscriptionInfo.points = 1000
}
subscriptionDialogVisible.value = true
// 初始化价格显示
updatePrice()
}
// 创建订阅支付
const createSubscription = async () => {
try {
subscriptionLoading.value = true
// 根据支付方式确定实际支付金额
let actualAmount
if (selectedPaymentMethod.value === 'ALIPAY') {
// 支付宝使用人民币
actualAmount = (subscriptionInfo.price * exchangeRate.value).toFixed(2)
} else {
// PayPal使用美元
actualAmount = subscriptionInfo.price.toString()
}
const response = await createTestPayment({
amount: actualAmount,
method: selectedPaymentMethod.value
})
if (response.success) {
ElMessage.success(t('common.paymentRecordCreated', { title: subscriptionInfo.title }))
// 根据支付方式调用相应的支付接口
if (selectedPaymentMethod.value === 'ALIPAY') {
try {
const alipayResponse = await createAlipayPayment({
paymentId: response.data.id
})
if (alipayResponse.success) {
// 跳转到支付宝支付页面
window.open(alipayResponse.data.paymentUrl, '_blank')
ElMessage.success(t('common.redirectingToAlipay'))
} else {
ElMessage.error(alipayResponse.message || t('common.createAlipayFailed'))
}
} catch (error) {
console.error('创建支付宝支付失败:', error)
ElMessage.error(t('common.createAlipayFailed'))
}
} else if (selectedPaymentMethod.value === 'PAYPAL') {
try {
const paypalResponse = await createPayPalPayment({
paymentId: response.data.id
})
if (paypalResponse.success) {
// 跳转到PayPal支付页面
window.open(paypalResponse.data.paymentUrl, '_blank')
ElMessage.success(t('common.redirectingToPaypal'))
} else {
ElMessage.error(paypalResponse.message || t('common.createPaypalFailed'))
}
} catch (error) {
console.error('创建PayPal支付失败:', error)
ElMessage.error(t('common.createPaypalFailed'))
}
}
subscriptionDialogVisible.value = false
// 刷新支付记录列表
fetchPayments()
} else {
ElMessage.error(response.message || t('common.createSubscriptionFailed'))
}
} catch (error) {
console.error('Create subscription error:', error)
ElMessage.error(t('common.createSubscriptionFailed'))
} finally {
subscriptionLoading.value = false
}
}
// 测试支付完成
const testPaymentComplete = async (payment) => {
try {
await ElMessageBox.confirm(
t('common.confirmTestPayment', { orderId: payment.orderId }),
t('common.confirmTest'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning'
}
)
const response = await testPaymentCompleteApi(payment.id)
if (response.success) {
ElMessage.success(t('common.testPaymentSuccess'))
// 刷新支付记录列表
fetchPayments()
} else {
ElMessage.error(response.message || t('common.testPaymentFailed'))
}
} catch (error) {
if (error !== 'cancel') {
console.error('Test payment complete error:', error)
ElMessage.error(t('common.testPaymentFailed'))
}
}
}
// 删除支付记录
const handleDeletePayment = async (payment) => {
try {
await ElMessageBox.confirm(
`确定要删除支付记录 ${payment.orderId} 吗?`,
t('common.confirm'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning'
}
)
const response = await deletePayment(payment.id)
if (response.data?.success) {
ElMessage.success(t('common.deleteSuccess'))
// 刷新支付记录列表
fetchPayments()
} else {
ElMessage.error(response.data?.message || t('common.deleteFailed'))
}
} catch (error) {
if (error !== 'cancel') {
console.error('Delete payment error:', error)
ElMessage.error(t('common.deleteFailed'))
}
}
}
onMounted(() => {
fetchPayments()
})
</script>
<style scoped>
.payments {
max-width: 1200px;
margin: 0 auto;
}
.page-header {
margin-bottom: 20px;
}
.page-header h2 {
margin: 0;
color: #303133;
display: flex;
align-items: center;
gap: 8px;
}
.filter-card {
margin-bottom: 20px;
}
.payments-card {
margin-bottom: 20px;
}
.order-link {
color: #409EFF;
text-decoration: none;
font-weight: 500;
}
.order-link:hover {
text-decoration: underline;
}
.amount {
font-weight: 600;
color: #E6A23C;
}
.description {
color: #606266;
}
.pagination-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
.payment-description {
margin-top: 20px;
}
.payment-description h4 {
margin-bottom: 12px;
color: #303133;
}
.payment-description p {
color: #606266;
line-height: 1.6;
}
.subscription-info {
text-align: center;
}
.subscription-info h3 {
color: #409eff;
margin-bottom: 0.5rem;
}
.subscription-info .price {
font-size: 2rem;
font-weight: bold;
color: #f56c6c;
margin: 1rem 0;
}
.subscription-info .description {
color: #666;
margin-bottom: 1rem;
}
.subscription-info .benefits {
text-align: left;
margin: 1rem 0;
}
.subscription-info .benefits h4 {
color: #333;
margin-bottom: 0.5rem;
}
.subscription-info .benefits ul {
list-style: none;
padding: 0;
}
.subscription-info .benefits li {
padding: 0.25rem 0;
color: #666;
}
.subscription-info .benefits li:before {
content: "✓ ";
color: #67c23a;
font-weight: bold;
}
.subscription-info .points-info {
margin-top: 1rem;
}
.subscription-info .payment-method {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid #e4e7ed;
}
.subscription-info .payment-method h4 {
color: #333;
margin-bottom: 0.5rem;
}
.subscription-info .converted-price {
margin-top: 0.5rem;
padding: 0.5rem;
background-color: #f0f9ff;
border-radius: 4px;
border: 1px solid #b3d8ff;
}
.subscription-info .price-display {
font-size: 1.2rem;
font-weight: bold;
color: #409eff;
}
</style>