Files
AIGC/frontend/src/views/Payments.vue
blandarebiter 90b5118e45 perf(backend+frontend): 列表API响应体积优化 3.1MB→145KB (↓95.4%)
- 后端: JPQL构造器投影排除LONGTEXT大字段(uploadedImages/videoReferenceImages)
- 后端: DTO层过滤非分镜图类型的base64内联resultUrl
- 前端: 列表缩略图从video改为img loading=lazy,消除172并发请求
- 前端: download函数增加resultUrl懒加载(详情接口兜底)
- 文档: 新增性能优化报告 docs/performance-optimization-report.md
2026-04-10 18:46:37 +08:00

750 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="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>