Files
tyapi-frontend/src/pages/subscriptions/index.vue
2025-12-04 12:44:54 +08:00

604 lines
15 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>
<ListPageLayout
title="我的订阅"
subtitle="管理您已订阅的数据产品"
>
<!-- 统计信息 -->
<template #actions>
<div class="flex gap-4">
<div class="stat-item">
<div class="stat-value">{{ stats.total_subscriptions || 0 }}</div>
<div class="stat-label">总订阅数</div>
</div>
</div>
</template>
<template #filters>
<FilterSection>
<FilterItem label="搜索订阅">
<el-input
v-model="filters.keyword"
placeholder="输入产品名称或编号"
clearable
@input="handleSearch"
class="w-full"
/>
</FilterItem>
<FilterItem label="产品名称">
<el-input
v-model="filters.product_name"
placeholder="输入产品名称"
clearable
@input="handleSearch"
class="w-full"
/>
</FilterItem>
<FilterItem label="订阅时间">
<el-date-picker
v-model="filters.timeRange"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
@change="handleTimeRangeChange"
class="w-full"
/>
</FilterItem>
<template #stats>
共找到 {{ total }} 个订阅
</template>
<template #buttons>
<el-button @click="resetFilters">重置筛选</el-button>
<el-button type="primary" @click="loadSubscriptions">应用筛选</el-button>
</template>
</FilterSection>
</template>
<template #table>
<div v-if="loading" class="flex justify-center items-center py-12">
<el-loading size="large" />
</div>
<div v-else-if="subscriptions.length === 0" class="text-center py-12">
<el-empty description="暂无订阅数据">
<el-button type="primary" @click="$router.push('/products')">
去数据大厅订阅产品
</el-button>
</el-empty>
</div>
<div v-else class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<el-table
:data="subscriptions"
style="width: 100%"
:header-cell-style="{
background: '#f8fafc',
color: '#475569',
fontWeight: '600',
fontSize: '14px'
}"
:cell-style="{
fontSize: '14px',
color: '#1e293b'
}"
>
<el-table-column prop="product.name" label="产品名称" min-width="200">
<template #default="{ row }">
<div>
<div class="font-medium text-gray-900">{{ row.product?.name || '未知产品' }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="product.category.name" label="产品分类" width="120">
<template #default="{ row }">
<el-tag size="small" type="info" effect="light">
{{ row.product?.category?.name || '未分类' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="product.code" label="产品编码" width="120">
<template #default="{ row }">
<span class="font-mono text-sm text-gray-600">{{ row.product?.code || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="price" label="订阅价格" width="120">
<template #default="{ row }">
<span class="font-semibold text-green-600">¥{{ formatPrice(row.price) }}</span>
</template>
</el-table-column>
<!-- <el-table-column prop="api_used" label="API调用次数" width="140">
<template #default="{ row }">
<span class="font-medium">{{ row.api_used || 0 }}</span>
</template>
</el-table-column> -->
<el-table-column prop="created_at" label="订阅时间" width="160">
<template #default="{ row }">
<div class="text-sm">
<div class="text-gray-900">{{ formatDate(row.created_at) }}</div>
<div class="text-gray-500">{{ formatTime(row.created_at) }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="400" fixed="right">
<template #default="{ row }">
<div class="flex items-center space-x-2">
<el-button
size="small"
type="primary"
@click="handleViewProduct(row.product)"
>
查看产品
</el-button>
<el-button
size="small"
type="success"
@click="handleViewUsage(row)"
>
使用情况
</el-button>
<el-button
size="small"
type="warning"
@click="goToApiDebugger(row.product)"
>
在线调试
</el-button>
<el-button
size="small"
type="danger"
@click="handleCancelSubscription(row)"
>
取消订阅
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
</template>
<template #pagination>
<el-pagination
v-if="total > 0"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</template>
<template #extra>
<!-- 使用情况弹窗 -->
<el-dialog
v-model="usageDialogVisible"
title="订阅使用情况"
width="600px"
class="usage-dialog"
>
<div v-if="selectedSubscription" class="space-y-6">
<div class="grid grid-cols-2 gap-6">
<!-- <div class="usage-stat-card">
<div class="usage-stat-value">{{ selectedSubscription.api_used || 0 }}</div>
<div class="usage-stat-label">已使用API调用次数</div>
</div> -->
<div class="usage-stat-card">
<div class="usage-stat-value">¥{{ formatPrice(selectedSubscription.price) }}</div>
<div class="usage-stat-label">订阅价格</div>
</div>
</div>
<div class="usage-info">
<div class="info-item">
<span class="info-label">产品名称:</span>
<span class="info-value">{{ selectedSubscription.product?.name }}</span>
</div>
<div class="info-item">
<span class="info-label">订阅时间:</span>
<span class="info-value">{{ formatDate(selectedSubscription.created_at) }}</span>
</div>
</div>
</div>
</el-dialog>
</template>
</ListPageLayout>
</template>
<script setup>
import { subscriptionApi } from '@/api'
import FilterItem from '@/components/common/FilterItem.vue'
import FilterSection from '@/components/common/FilterSection.vue'
import ListPageLayout from '@/components/common/ListPageLayout.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const router = useRouter()
// 响应式数据
const loading = ref(false)
const subscriptions = ref([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const usageDialogVisible = ref(false)
const selectedSubscription = ref(null)
// 统计数据
const stats = ref({
total_subscriptions: 0
})
// 筛选条件
const filters = reactive({
keyword: '',
product_name: '',
timeRange: []
})
// 搜索防抖
let searchTimer = null
// 初始化
onMounted(() => {
loadSubscriptions()
loadStats()
})
// 加载订阅列表
const loadSubscriptions = async () => {
loading.value = true
try {
const params = {
page: currentPage.value,
page_size: pageSize.value,
keyword: filters.keyword,
product_name: filters.product_name
}
// 添加时间范围参数
if (filters.timeRange && filters.timeRange.length === 2) {
params.start_time = filters.timeRange[0]
params.end_time = filters.timeRange[1]
}
const response = await subscriptionApi.getMySubscriptions(params)
subscriptions.value = response.data?.items || []
total.value = response.data?.total || 0
} catch (error) {
console.error('加载订阅失败:', error)
ElMessage.error('加载订阅失败')
} finally {
loading.value = false
}
}
// 加载统计数据
const loadStats = async () => {
try {
const response = await subscriptionApi.getMySubscriptionStats()
stats.value = response.data || {
total_subscriptions: 0
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
}
// 格式化价格
const formatPrice = (price) => {
if (!price) return '0.00'
return Number(price).toFixed(2)
}
// 格式化日期
const formatDate = (date) => {
if (!date) return '-'
return new Date(date).toLocaleDateString('zh-CN')
}
// 格式化时间
const formatTime = (date) => {
if (!date) return '-'
return new Date(date).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})
}
// 处理筛选变化
const handleFilterChange = () => {
currentPage.value = 1
loadSubscriptions()
}
// 处理搜索
const handleSearch = () => {
if (searchTimer) {
clearTimeout(searchTimer)
}
searchTimer = setTimeout(() => {
currentPage.value = 1
loadSubscriptions()
}, 500)
}
// 处理时间范围变化
const handleTimeRangeChange = () => {
currentPage.value = 1
loadSubscriptions()
}
// 重置筛选
const resetFilters = () => {
Object.keys(filters).forEach(key => {
if (key === 'timeRange') {
filters[key] = []
} else {
filters[key] = ''
}
})
currentPage.value = 1
loadSubscriptions()
}
// 处理分页大小变化
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
loadSubscriptions()
}
// 处理当前页变化
const handleCurrentChange = (page) => {
currentPage.value = page
loadSubscriptions()
}
// 查看产品
const handleViewProduct = (product) => {
if (product) {
// 跳转到产品详情页面
router.push(`/products/${product.id}`)
}
}
// 查看使用情况
const handleViewUsage = (subscription) => {
selectedSubscription.value = subscription
usageDialogVisible.value = true
}
// 跳转到在线调试页面
const goToApiDebugger = (product) => {
if (product) {
router.push({
name: 'ApiDebugger',
params: { productId: product.id }
})
}
}
// 取消订阅
const handleCancelSubscription = async (subscription) => {
if (!subscription || !subscription.id) {
ElMessage.error('订阅信息不完整')
return
}
try {
// 显示确认对话框
await ElMessageBox.confirm(
`确定要取消订阅 "${subscription.product?.name || '该产品'}" 吗取消后将无法继续使用该产品的API服务。`,
'取消订阅确认',
{
confirmButtonText: '确定取消',
cancelButtonText: '我再想想',
type: 'warning',
dangerouslyUseHTMLString: false
}
)
// 用户确认后执行取消操作
loading.value = true
try {
await subscriptionApi.cancelMySubscription(subscription.id)
ElMessage.success('取消订阅成功')
// 重新加载订阅列表
await loadSubscriptions()
// 重新加载统计数据
await loadStats()
} catch (error) {
console.error('取消订阅失败:', error)
const errorMessage = error.response?.data?.message || error.message || '取消订阅失败'
ElMessage.error(errorMessage)
} finally {
loading.value = false
}
} catch (error) {
// 用户取消操作,不做任何处理
if (error !== 'cancel') {
console.error('取消订阅操作异常:', error)
}
}
}
</script>
<style scoped>
/* 统计项样式 */
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 24px;
background: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(226, 232, 240, 0.6);
border-radius: 12px;
min-width: 120px;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: #1e293b;
line-height: 1;
margin-bottom: 4px;
}
.stat-label {
font-size: 13px;
color: #64748b;
font-weight: 500;
}
/* 使用情况弹窗样式 */
.usage-dialog :deep(.el-dialog) {
border-radius: 16px;
overflow: hidden;
}
.usage-dialog :deep(.el-dialog__header) {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-bottom: 1px solid rgba(226, 232, 240, 0.6);
padding: 20px 24px;
}
.usage-dialog :deep(.el-dialog__title) {
font-size: 18px;
font-weight: 600;
color: #1e293b;
}
.usage-dialog :deep(.el-dialog__body) {
padding: 24px;
}
.usage-dialog :deep(.el-dialog__footer) {
background: rgba(248, 250, 252, 0.5);
border-top: 1px solid rgba(226, 232, 240, 0.4);
padding: 16px 24px;
}
/* 使用情况统计卡片 */
.usage-stat-card {
background: rgba(248, 250, 252, 0.8);
border: 1px solid rgba(226, 232, 240, 0.6);
border-radius: 12px;
padding: 20px;
text-align: center;
transition: all 0.2s ease;
}
.usage-stat-card:hover {
background: rgba(255, 255, 255, 0.9);
border-color: rgba(59, 130, 246, 0.3);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.usage-stat-value {
font-size: 28px;
font-weight: 700;
color: #1e293b;
line-height: 1;
margin-bottom: 8px;
}
.usage-stat-label {
font-size: 14px;
color: #64748b;
font-weight: 500;
}
/* 使用情况信息 */
.usage-info {
background: rgba(248, 250, 252, 0.5);
border: 1px solid rgba(226, 232, 240, 0.4);
border-radius: 8px;
padding: 16px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid rgba(226, 232, 240, 0.3);
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
font-size: 14px;
color: #64748b;
font-weight: 500;
}
.info-value {
font-size: 14px;
color: #1e293b;
font-weight: 600;
}
/* 表格样式优化 */
:deep(.el-table) {
border-radius: 8px;
overflow: hidden;
}
:deep(.el-table th) {
background: #f8fafc !important;
border-bottom: 1px solid #e2e8f0;
}
:deep(.el-table td) {
border-bottom: 1px solid #f1f5f9;
}
:deep(.el-table tr:hover > td) {
background: #f8fafc !important;
}
/* 响应式设计 */
@media (max-width: 768px) {
.stat-item {
padding: 12px 16px;
min-width: 100px;
}
.stat-value {
font-size: 20px;
}
.stat-label {
font-size: 12px;
}
.usage-stat-card {
padding: 16px;
}
.usage-stat-value {
font-size: 24px;
}
.usage-stat-label {
font-size: 13px;
}
}
</style>