- 后端: JPQL构造器投影排除LONGTEXT大字段(uploadedImages/videoReferenceImages) - 后端: DTO层过滤非分镜图类型的base64内联resultUrl - 前端: 列表缩略图从video改为img loading=lazy,消除172并发请求 - 前端: download函数增加resultUrl懒加载(详情接口兜底) - 文档: 新增性能优化报告 docs/performance-optimization-report.md
750 lines
20 KiB
Vue
750 lines
20 KiB
Vue
<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>
|