first commit
This commit is contained in:
552
src/pages/subscriptions/index.vue
Normal file
552
src/pages/subscriptions/index.vue
Normal file
@@ -0,0 +1,552 @@
|
||||
<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="320" 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>
|
||||
</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 } 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 }
|
||||
})
|
||||
}
|
||||
}
|
||||
</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>
|
||||
Reference in New Issue
Block a user