This commit is contained in:
2025-12-04 12:44:54 +08:00
parent d687bf67b1
commit 3f33e5c2f1
8 changed files with 447 additions and 99 deletions

View File

@@ -57,6 +57,11 @@ export const productApi = {
getProductApiConfigByCode: (productCode) => request.get(`/products/code/${productCode}/api-config`), getProductApiConfigByCode: (productCode) => request.get(`/products/code/${productCode}/api-config`),
getProductApiConfigsByProductIDs: (productIds) => request.get('/products/api-configs', { getProductApiConfigsByProductIDs: (productIds) => request.get('/products/api-configs', {
params: { product_ids: productIds.join(',') } params: { product_ids: productIds.join(',') }
}),
// 下载接口文档支持PDF和Markdown
downloadProductDocumentation: (productId) => request.get(`/products/${productId}/documentation/download`, {
responseType: 'blob'
}) })
} }
@@ -81,7 +86,10 @@ export const subscriptionApi = {
getMySubscriptionDetail: (id) => request.get(`/my/subscriptions/${id}`), getMySubscriptionDetail: (id) => request.get(`/my/subscriptions/${id}`),
// 获取我的订阅使用情况 (需认证) // 获取我的订阅使用情况 (需认证)
getMySubscriptionUsage: (id) => request.get(`/my/subscriptions/${id}/usage`) getMySubscriptionUsage: (id) => request.get(`/my/subscriptions/${id}/usage`),
// 取消我的订阅 (需认证)
cancelMySubscription: (id) => request.post(`/my/subscriptions/${id}/cancel`)
} }
// 财务相关接口 // 财务相关接口

View File

@@ -73,12 +73,12 @@
<!-- 已订阅的产品 --> <!-- 已订阅的产品 -->
<el-button <el-button
v-else-if="isSubscribed" v-else-if="isSubscribed"
type="info" type="danger"
disabled @click="handleCancelSubscribe"
class="action-btn subscribed-btn" class="action-btn cancel-subscribe-btn"
size="small" size="small"
> >
订阅 取消订阅
</el-button> </el-button>
<!-- 可订阅的产品 --> <!-- 可订阅的产品 -->
@@ -104,10 +104,14 @@ const props = defineProps({
isSubscribed: { isSubscribed: {
type: Boolean, type: Boolean,
default: false default: false
},
subscription: {
type: Object,
default: null
} }
}) })
const emit = defineEmits(['view-detail', 'subscribe']) const emit = defineEmits(['view-detail', 'subscribe', 'cancel-subscribe'])
// 格式化价格 // 格式化价格
const formatPrice = (price) => { const formatPrice = (price) => {
@@ -134,6 +138,11 @@ const handleViewDetail = () => {
const handleSubscribe = () => { const handleSubscribe = () => {
emit('subscribe', props.product) emit('subscribe', props.product)
} }
// 取消订阅
const handleCancelSubscribe = () => {
emit('cancel-subscribe', props.product)
}
</script> </script>
<style scoped> <style scoped>
@@ -357,19 +366,17 @@ const handleSubscribe = () => {
box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3); box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3);
} }
.subscribed-btn { .cancel-subscribe-btn {
background: rgba(100, 116, 139, 0.1); background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
border-color: rgba(100, 116, 139, 0.2); border-color: #ef4444;
color: #64748b; color: white;
cursor: not-allowed; box-shadow: 0 2px 4px rgba(239, 68, 68, 0.2);
} }
.subscribed-btn:hover { .cancel-subscribe-btn:hover {
background: rgba(100, 116, 139, 0.1); background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
border-color: rgba(100, 116, 139, 0.2); border-color: #dc2626;
color: #64748b; box-shadow: 0 4px 8px rgba(239, 68, 68, 0.3);
transform: none;
box-shadow: none;
} }
.disabled-btn { .disabled-btn {

View File

@@ -414,13 +414,25 @@
<div v-if="decryptedData" class="mt-3 pt-3 border-t border-gray-200"> <div v-if="decryptedData" class="mt-3 pt-3 border-t border-gray-200">
<h5 class="text-xs font-semibold text-gray-700 mb-1">解密后的内容</h5> <h5 class="text-xs font-semibold text-gray-700 mb-1">解密后的内容</h5>
<pre <pre
class="bg-green-50 p-2 rounded text-xs overflow-x-auto m-0 mb-1.5 max-h-45 border border-green-200 font-mono text-green-800"> class="bg-green-50 p-2 rounded text-xs overflow-x-auto m-0 mb-1.5 max-h-45 border border-green-200 font-mono text-green-800"
:key="`decrypted-${Date.now()}`">
{{ JSON.stringify(decryptedData, null, 2) }}</pre> {{ JSON.stringify(decryptedData, null, 2) }}</pre>
<el-button type="success" size="default" <el-button type="success" size="default"
@click="copyToClipboard(JSON.stringify(decryptedData, null, 2))" class="w-full"> @click="copyToClipboard(JSON.stringify(decryptedData, null, 2))" class="w-full">
复制解密内容 复制解密内容
</el-button> </el-button>
</div> </div>
<!-- 解密加载状态 -->
<div v-else-if="debugging && debugResult && debugResult.success && debugResult.response?.data?.data"
class="mt-3 pt-3 border-t border-gray-200">
<div class="flex items-center gap-2 text-sm text-gray-500">
<svg class="animate-spin h-4 w-4 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>正在解密响应数据...</span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -545,6 +557,8 @@ const productsLoading = ref(false)
const debugging = ref(false) const debugging = ref(false)
const encrypting = ref(false) const encrypting = ref(false)
const autoSelecting = ref(false) // 新增:自动选择状态 const autoSelecting = ref(false) // 新增:自动选择状态
const isSelectingProduct = ref(false) // 防止重复选择产品的标志
const lastSelectedProductId = ref(null) // 记录最后选择的产品ID
const userProducts = ref([]) const userProducts = ref([])
const apiConfig = ref(null) const apiConfig = ref(null)
const selectedProduct = ref(null) const selectedProduct = ref(null)
@@ -650,12 +664,22 @@ onMounted(async () => {
// 监听路由参数变化,自动选择产品 // 监听路由参数变化,自动选择产品
watch( watch(
() => route.params.productId, () => route.query.productId || route.params.productId,
async (newProductId) => { async (newProductId, oldProductId) => {
// 防止重复触发如果产品ID没有变化不执行
if (newProductId === oldProductId) {
return
}
// 如果正在选择产品,不重复执行
if (isSelectingProduct.value) {
return
}
if (newProductId && userProducts.value.length > 0) { if (newProductId && userProducts.value.length > 0) {
await autoSelectProduct(newProductId) await autoSelectProduct(newProductId)
} else if (!newProductId && userProducts.value.length > 0) { } else if (!newProductId && userProducts.value.length > 0 && !selectedProduct.value) {
// 如果没有指定产品,选择第一个 // 如果没有指定产品且当前没有选中产品,选择第一个
await selectProduct(userProducts.value[0]) await selectProduct(userProducts.value[0])
} }
} }
@@ -663,6 +687,18 @@ watch(
// 自动选择产品 // 自动选择产品
const autoSelectProduct = async (productId) => { const autoSelectProduct = async (productId) => {
// 防止重复执行
if (isSelectingProduct.value) {
console.log('正在选择产品,跳过重复请求')
return
}
// 如果已经选择了相同的产品,不重复选择
if (lastSelectedProductId.value === productId && selectedProduct.value) {
console.log('产品已选择,跳过重复选择:', productId)
return
}
// 如果用户产品列表为空,等待加载完成 // 如果用户产品列表为空,等待加载完成
if (!userProducts.value.length) { if (!userProducts.value.length) {
console.log('等待用户产品列表加载完成...') console.log('等待用户产品列表加载完成...')
@@ -686,7 +722,13 @@ const autoSelectProduct = async (productId) => {
if (targetProduct) { if (targetProduct) {
console.log('自动选择产品:', targetProduct) console.log('自动选择产品:', targetProduct)
await selectProduct(targetProduct) isSelectingProduct.value = true
try {
await selectProduct(targetProduct)
lastSelectedProductId.value = productId
} finally {
isSelectingProduct.value = false
}
} else { } else {
console.warn('未找到指定的产品:', productId) console.warn('未找到指定的产品:', productId)
ElMessage.warning(`未找到产品ID为 ${productId} 的产品,请手动选择`) ElMessage.warning(`未找到产品ID为 ${productId} 的产品,请手动选择`)
@@ -699,7 +741,7 @@ const loadUserProducts = async () => {
try { try {
const response = await subscriptionApi.getMySubscriptions({ const response = await subscriptionApi.getMySubscriptions({
page: 1, page: 1,
page_size: 100 page_size: 1000
}) })
if (response.success && response.data?.items) { if (response.success && response.data?.items) {
@@ -712,16 +754,23 @@ const loadUserProducts = async () => {
})) }))
console.log("route.params", route) console.log("route.params", route)
// 检查是否有params参数指定产品 // 检查是否有query或params参数指定产品
if (route.params.productId && userProducts.value.length > 0) { const productId = route.query.productId || route.params.productId
if (productId && userProducts.value.length > 0) {
await nextTick() await nextTick()
// 使用 autoSelectProduct 会自动处理防重复逻辑
await autoSelectProduct(route.params.productId) await autoSelectProduct(productId)
} else if (userProducts.value.length > 0) { } else if (userProducts.value.length > 0 && !selectedProduct.value) {
// 没有指定产品时,默认选择第一个 // 没有指定产品时,默认选择第一个(仅在未选择产品时)
autoSelecting.value = true autoSelecting.value = true
await selectProduct(userProducts.value[0]) isSelectingProduct.value = true
autoSelecting.value = false try {
await selectProduct(userProducts.value[0])
lastSelectedProductId.value = userProducts.value[0].id || userProducts.value[0].product_id
} finally {
isSelectingProduct.value = false
autoSelecting.value = false
}
} }
} else { } else {
// 如果没有订阅产品,显示提示信息 // 如果没有订阅产品,显示提示信息
@@ -753,6 +802,15 @@ const loadApiKeys = async () => {
// 选择产品 // 选择产品
const selectProduct = async (product) => { const selectProduct = async (product) => {
// 防止重复选择相同产品
const productId = product.product_id || product.id
if (selectedProduct.value &&
(selectedProduct.value.id === productId || selectedProduct.value.product_id === productId) &&
!isSelectingProduct.value) {
console.log('产品已选择,跳过重复加载:', productId)
return
}
// 确保API密钥已经加载 // 确保API密钥已经加载
if (!debugForm.accessId || !debugForm.secretKey) { if (!debugForm.accessId || !debugForm.secretKey) {
ElMessage.warning('正在加载API密钥请稍候...') ElMessage.warning('正在加载API密钥请稍候...')
@@ -764,6 +822,7 @@ const selectProduct = async (product) => {
debugForm.params = {} debugForm.params = {}
debugResult.value = null debugResult.value = null
encryptedData.value = null encryptedData.value = null
decryptedData.value = null
activeTab.value = 'basic_info' // 重置Tab activeTab.value = 'basic_info' // 重置Tab
productDocumentation.value = null // 重置文档 productDocumentation.value = null // 重置文档
@@ -1106,6 +1165,11 @@ const handleDebug = async () => {
} }
debugging.value = true debugging.value = true
// 清空之前的调试结果确保UI实时更新
debugResult.value = null
decryptedData.value = null
await nextTick() // 确保DOM更新
const startTime = new Date() const startTime = new Date()
try { try {
@@ -1118,22 +1182,6 @@ const handleDebug = async () => {
return return
} }
// 1.5. 类型转换:将 page_size 和 page_num 从字符串转换为数字
if (parsedParams && typeof parsedParams === 'object') {
if (parsedParams.page_size !== undefined && typeof parsedParams.page_size === 'string') {
const pageSize = parseInt(parsedParams.page_size, 10)
if (!isNaN(pageSize)) {
parsedParams.page_size = pageSize
}
}
if (parsedParams.page_num !== undefined && typeof parsedParams.page_num === 'string') {
const pageNum = parseInt(parsedParams.page_num, 10)
if (!isNaN(pageNum)) {
parsedParams.page_num = pageNum
}
}
}
// 2. 加密参数 // 2. 加密参数
const encryptedParams = await encryptWithAES(parsedParams, debugForm.secretKey) const encryptedParams = await encryptWithAES(parsedParams, debugForm.secretKey)
if (!encryptedParams) { if (!encryptedParams) {
@@ -1159,7 +1207,7 @@ const handleDebug = async () => {
console.log('产品API调用成功:', responseData) console.log('产品API调用成功:', responseData)
const endTime = new Date() const endTime = new Date()
// 5. 保存调试结果 // 5. 保存调试结果 - 立即更新确保UI实时显示
debugResult.value = createDebugResult( debugResult.value = createDebugResult(
selectedProduct.value, selectedProduct.value,
requestBody, requestBody,
@@ -1170,6 +1218,9 @@ const handleDebug = async () => {
responseData.success && responseData.data?.code === 0 responseData.success && responseData.data?.code === 0
) )
// 确保DOM更新后再进行解密操作
await nextTick()
// 6. 如果响应成功且包含加密数据,自动解密 // 6. 如果响应成功且包含加密数据,自动解密
console.log('responseData', responseData) console.log('responseData', responseData)
if (responseData.success && responseData.data?.code === 0 && responseData.data?.data && typeof responseData.data.data === 'string') { if (responseData.success && responseData.data?.code === 0 && responseData.data?.data && typeof responseData.data.data === 'string') {
@@ -1180,7 +1231,9 @@ const handleDebug = async () => {
) )
if (decryptResult.success) { if (decryptResult.success) {
// 使用 nextTick 确保响应式更新
decryptedData.value = decryptResult.data decryptedData.value = decryptResult.data
await nextTick()
ElMessage.success('调试完成,数据已自动解密') ElMessage.success('调试完成,数据已自动解密')
} else { } else {
ElMessage.warning('调试完成,但数据解密失败:' + (decryptResult.message || '未知错误')) ElMessage.warning('调试完成,但数据解密失败:' + (decryptResult.message || '未知错误'))
@@ -1223,6 +1276,9 @@ const handleDebug = async () => {
false false
) )
// 确保DOM更新
await nextTick()
ElMessage.error('API调用失败' + apiError.message) ElMessage.error('API调用失败' + apiError.message)
} }
} catch (error) { } catch (error) {
@@ -1239,6 +1295,9 @@ const handleDebug = async () => {
endTime, endTime,
false false
) )
// 确保DOM更新
await nextTick()
} finally { } finally {
debugging.value = false debugging.value = false
} }

View File

@@ -10,6 +10,15 @@
</div> </div>
<div class="list-page-actions"> <div class="list-page-actions">
<el-button @click="$router.back()">返回</el-button> <el-button @click="$router.back()">返回</el-button>
<el-button
v-if="product?.documentation"
type="info"
@click="downloadDocumentation"
:loading="downloading"
>
<el-icon><Download /></el-icon>
{{ product?.documentation?.pdf_file_path ? '下载PDF文档' : '下载接口文档' }}
</el-button>
<el-button <el-button
v-if="!isSubscribed" v-if="!isSubscribed"
type="primary" type="primary"
@@ -20,10 +29,11 @@
</el-button> </el-button>
<el-button <el-button
v-else v-else
type="success" type="danger"
disabled @click="handleCancelSubscription"
:loading="cancelling"
> >
订阅 取消订阅
</el-button> </el-button>
<el-button <el-button
v-if="isSubscribed" v-if="isSubscribed"
@@ -260,7 +270,7 @@
<script setup> <script setup>
import { productApi, subscriptionApi } from '@/api' import { productApi, subscriptionApi } from '@/api'
import { DocumentCopy } from '@element-plus/icons-vue' import { DocumentCopy, Download } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { marked } from 'marked' import { marked } from 'marked'
@@ -272,6 +282,8 @@ const loading = ref(false)
const product = ref(null) const product = ref(null)
const userSubscriptions = ref([]) const userSubscriptions = ref([])
const subscribing = ref(false) const subscribing = ref(false)
const cancelling = ref(false)
const downloading = ref(false)
const activeTab = ref('content') const activeTab = ref('content')
const currentTimestamp = ref('') const currentTimestamp = ref('')
@@ -288,6 +300,12 @@ const isSubscribed = computed(() => {
return userSubscriptions.value.some(sub => sub.product_id === product.value.id) return userSubscriptions.value.some(sub => sub.product_id === product.value.id)
}) })
// 获取当前产品的订阅信息
const currentSubscription = computed(() => {
if (!product.value || !userSubscriptions.value.length) return null
return userSubscriptions.value.find(sub => sub.product_id === product.value.id)
})
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
loadUserSubscriptions() loadUserSubscriptions()
@@ -321,7 +339,7 @@ const startTimestampUpdate = () => {
// 加载用户订阅 // 加载用户订阅
const loadUserSubscriptions = async () => { const loadUserSubscriptions = async () => {
try { try {
const response = await subscriptionApi.getMySubscriptions({ page: 1, page_size: 100 }) const response = await subscriptionApi.getMySubscriptions({ page: 1, page_size: 1000 })
userSubscriptions.value = response.data?.items || [] userSubscriptions.value = response.data?.items || []
} catch (error) { } catch (error) {
console.error('加载用户订阅失败:', error) console.error('加载用户订阅失败:', error)
@@ -528,19 +546,53 @@ const handleSubscribe = async () => {
} catch (error) { } catch (error) {
if (error !== 'cancel') { if (error !== 'cancel') {
console.error('订阅失败:', error) console.error('订阅失败:', error)
ElMessage.error('订阅失败') const errorMessage = error.response?.data?.message || error.message || '订阅失败'
ElMessage.error(errorMessage)
} }
} finally { } finally {
subscribing.value = false subscribing.value = false
} }
} }
// 取消订阅
const handleCancelSubscription = async () => {
if (!product.value || !currentSubscription.value) return
try {
await ElMessageBox.confirm(
`确定要取消订阅产品"${product.value.name}"吗取消后将无法继续使用该产品的API服务。`,
'取消订阅确认',
{
confirmButtonText: '确定取消',
cancelButtonText: '我再想想',
type: 'warning'
}
)
cancelling.value = true
await subscriptionApi.cancelMySubscription(currentSubscription.value.id)
ElMessage.success('取消订阅成功')
// 重新加载用户订阅
await loadUserSubscriptions()
} catch (error) {
if (error !== 'cancel') {
console.error('取消订阅失败:', error)
const errorMessage = error.response?.data?.message || error.message || '取消订阅失败'
ElMessage.error(errorMessage)
}
} finally {
cancelling.value = false
}
}
// 前往在线调试 // 前往在线调试
const goToApiDebugger = () => { const goToApiDebugger = () => {
if (!product.value) return if (!product.value) return
router.push({ router.push({
name: 'ApiDebugger', name: 'ApiDebugger',
params: { productId: product.value.id } query: { productId: product.value.id }
}) })
} }
@@ -659,6 +711,65 @@ const getDefaultErrorCodes = () => {
| 2001 | 业务失败 |` | 2001 | 业务失败 |`
} }
// 下载接口文档
const downloadDocumentation = async () => {
if (!product.value) {
ElMessage.warning('产品信息不存在')
return
}
downloading.value = true
try {
// 根据是否有PDF文件路径判断文件类型
const hasPDF = product.value.documentation?.pdf_file_path
// 使用原生fetch以获取完整的响应信息包括headers
const token = localStorage.getItem('access_token')
const tokenType = localStorage.getItem('token_type') || 'Bearer'
const headers = {
'Authorization': token ? `${tokenType} ${token}` : ''
}
const response = await fetch(`/api/v1/products/${product.value.id}/documentation/download`, {
headers
})
if (!response.ok) {
throw new Error('下载失败')
}
// 获取Content-Type
const contentType = response.headers.get('content-type') || ''
const isPDF = contentType.includes('application/pdf') || hasPDF
// 获取文件内容
const blob = await response.blob()
// 创建下载链接
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
const extension = isPDF ? 'pdf' : 'md'
const filename = `${product.value.name || '产品'}_接口文档.${extension}`
link.download = filename
// 触发下载
document.body.appendChild(link)
link.click()
// 清理
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success('文档下载成功')
} catch (error) {
console.error('下载接口文档失败:', error)
ElMessage.error('下载接口文档失败')
} finally {
downloading.value = false
}
}
// 下载 Markdown 文档 // 下载 Markdown 文档
const downloadMarkdown = (type) => { const downloadMarkdown = (type) => {
if (!product.value?.documentation) { if (!product.value?.documentation) {

View File

@@ -50,9 +50,15 @@
</div> </div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 p-4"> <div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 p-4">
<ProductCard v-for="product in products" :key="product.id" :product="product" <ProductCard
:is-subscribed="product.is_subscribed" @view-detail="handleViewDetail" v-for="product in products"
@subscribe="handleSubscribe" /> :key="product.id"
:product="product"
:is-subscribed="getProductSubscriptionStatus(product.id)"
:subscription="getProductSubscription(product.id)"
@view-detail="handleViewDetail"
@subscribe="handleSubscribe"
@cancel-subscribe="handleCancelSubscribe" />
</div> </div>
</template> </template>
@@ -65,7 +71,7 @@
</template> </template>
<script setup> <script setup>
import { categoryApi, productApi } from '@/api' import { categoryApi, productApi, subscriptionApi } from '@/api'
import FilterItem from '@/components/common/FilterItem.vue' import FilterItem from '@/components/common/FilterItem.vue'
import FilterSection from '@/components/common/FilterSection.vue' import FilterSection from '@/components/common/FilterSection.vue'
import ListPageLayout from '@/components/common/ListPageLayout.vue' import ListPageLayout from '@/components/common/ListPageLayout.vue'
@@ -81,6 +87,7 @@ const categories = ref([])
const total = ref(0) const total = ref(0)
const currentPage = ref(1) const currentPage = ref(1)
const pageSize = ref(12) const pageSize = ref(12)
const userSubscriptions = ref([]) // 用户订阅列表
// 筛选条件 // 筛选条件
const filters = reactive({ const filters = reactive({
@@ -96,6 +103,7 @@ let searchTimer = null
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
loadCategories() loadCategories()
loadUserSubscriptions()
loadProducts() loadProducts()
}) })
@@ -109,6 +117,30 @@ const loadCategories = async () => {
} }
} }
// 加载用户订阅列表
const loadUserSubscriptions = async () => {
try {
const response = await subscriptionApi.getMySubscriptions({ page: 1, page_size: 1000 })
userSubscriptions.value = response.data?.items || []
} catch (error) {
// 如果未登录或获取失败,清空订阅列表
console.error('加载用户订阅失败:', error)
userSubscriptions.value = []
}
}
// 获取产品的订阅状态
const getProductSubscriptionStatus = (productId) => {
if (!productId || !userSubscriptions.value.length) return false
return userSubscriptions.value.some(sub => sub.product_id === productId)
}
// 获取产品的订阅信息
const getProductSubscription = (productId) => {
if (!productId || !userSubscriptions.value.length) return null
return userSubscriptions.value.find(sub => sub.product_id === productId) || null
}
// 加载产品列表 // 加载产品列表
const loadProducts = async () => { const loadProducts = async () => {
loading.value = true loading.value = true
@@ -122,6 +154,9 @@ const loadProducts = async () => {
const response = await productApi.getProducts(params) const response = await productApi.getProducts(params)
products.value = response.data?.items || [] products.value = response.data?.items || []
total.value = response.data?.total || 0 total.value = response.data?.total || 0
// 重新加载订阅列表以确保状态同步
await loadUserSubscriptions()
} catch (error) { } catch (error) {
console.error('加载产品失败:', error) console.error('加载产品失败:', error)
ElMessage.error('加载产品失败') ElMessage.error('加载产品失败')
@@ -191,12 +226,51 @@ const handleSubscribe = async (product) => {
await productApi.subscribeProduct(product.id) await productApi.subscribeProduct(product.id)
ElMessage.success('订阅成功') ElMessage.success('订阅成功')
// 重新加载产品列表以更新订阅状态 // 重新加载订阅列表和产品列表以更新订阅状态
await loadUserSubscriptions()
await loadProducts() await loadProducts()
} catch (error) { } catch (error) {
if (error !== 'cancel') { if (error !== 'cancel') {
console.error('订阅失败:', error) console.error('订阅失败:', error)
ElMessage.error('订阅失败') const errorMessage = error.response?.data?.message || error.message || '订阅失败'
ElMessage.error(errorMessage)
}
}
}
// 取消订阅
const handleCancelSubscribe = async (product) => {
if (!product) return
// 获取该产品的订阅信息
const subscription = getProductSubscription(product.id)
if (!subscription || !subscription.id) {
ElMessage.error('订阅信息不完整')
return
}
try {
await ElMessageBox.confirm(
`确定要取消订阅产品"${product.name}"吗取消后将无法继续使用该产品的API服务。`,
'取消订阅确认',
{
confirmButtonText: '确定取消',
cancelButtonText: '我再想想',
type: 'warning'
}
)
await subscriptionApi.cancelMySubscription(subscription.id)
ElMessage.success('取消订阅成功')
// 重新加载订阅列表和产品列表以更新订阅状态
await loadUserSubscriptions()
await loadProducts()
} catch (error) {
if (error !== 'cancel') {
console.error('取消订阅失败:', error)
const errorMessage = error.response?.data?.message || error.message || '取消订阅失败'
ElMessage.error(errorMessage)
} }
} }
} }

View File

@@ -131,7 +131,7 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="320" fixed="right"> <el-table-column label="操作" width="400" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<el-button <el-button
@@ -155,6 +155,13 @@
> >
在线调试 在线调试
</el-button> </el-button>
<el-button
size="small"
type="danger"
@click="handleCancelSubscription(row)"
>
取消订阅
</el-button>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
@@ -216,7 +223,7 @@ import { subscriptionApi } from '@/api'
import FilterItem from '@/components/common/FilterItem.vue' import FilterItem from '@/components/common/FilterItem.vue'
import FilterSection from '@/components/common/FilterSection.vue' import FilterSection from '@/components/common/FilterSection.vue'
import ListPageLayout from '@/components/common/ListPageLayout.vue' import ListPageLayout from '@/components/common/ListPageLayout.vue'
import { ElMessage } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
const router = useRouter() const router = useRouter()
@@ -383,6 +390,50 @@ const goToApiDebugger = (product) => {
}) })
} }
} }
// 取消订阅
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> </script>
<style scoped> <style scoped>

View File

@@ -315,9 +315,18 @@ const router = createRouter({
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
const userStore = useUserStore() const userStore = useUserStore()
// 等待userStore初始化完成 // 对于不需要认证的路由(如登录页),不等待初始化,直接放行
if (!userStore.initialized) { const isAuthRoute = to.path.startsWith('/auth')
const requiresAuth = to.meta.requiresAuth
// 只有在需要认证的路由上才等待初始化
if (requiresAuth && !userStore.initialized) {
await userStore.init() await userStore.init()
} else if (!userStore.initialized) {
// 对于不需要认证的路由,异步初始化但不阻塞
userStore.init().catch(err => {
console.warn('UserStore初始化失败:', err)
})
} }
// 设置页面标题 // 设置页面标题
@@ -326,7 +335,7 @@ router.beforeEach(async (to, from, next) => {
} }
// 检查是否需要认证 // 检查是否需要认证
if (to.meta.requiresAuth && !userStore.isLoggedIn) { if (requiresAuth && !userStore.isLoggedIn) {
next('/auth/login') next('/auth/login')
return return
} }
@@ -338,7 +347,7 @@ router.beforeEach(async (to, from, next) => {
} }
// 已登录用户访问认证页面,重定向到数据大厅 // 已登录用户访问认证页面,重定向到数据大厅
if (to.path.startsWith('/auth') && userStore.isLoggedIn) { if (isAuthRoute && userStore.isLoggedIn) {
next('/products') next('/products')
return return
} }

View File

@@ -135,9 +135,9 @@ export const useUserStore = defineStore('user', () => {
// 检查用户信息是否完整 // 检查用户信息是否完整
const isUserInfoComplete = computed(() => { const isUserInfoComplete = computed(() => {
return user.value && return user.value &&
user.value.id && user.value.id &&
user.value.phone && user.value.phone &&
user.value.user_type !== undefined user.value.user_type !== undefined
}) })
// 强制刷新用户信息 // 强制刷新用户信息
@@ -416,44 +416,73 @@ export const useUserStore = defineStore('user', () => {
} }
} }
// 初始化标志,防止重复初始化
let isInitializing = false
// 初始化 // 初始化
const init = async () => { const init = async () => {
// 监听认证错误事件 // 如果已经初始化完成,直接返回
authEventBus.onAuthError(handleAuthError) if (initialized.value) {
// 监听版本更新事件
window.addEventListener('version:logout', handleVersionLogout)
window.addEventListener('version:refresh', handleVersionRefresh)
// 进行版本检查
if (!checkVersions()) {
return return
} }
if (accessToken.value && !user.value) { // 如果正在初始化,等待完成
// 有token但无用户信息自动拉取 if (isInitializing) {
loading.value = true // 等待初始化完成
try { while (isInitializing) {
const result = await fetchUserProfile() await new Promise(resolve => setTimeout(resolve, 50))
isAuthenticated.value = result.success }
return
}
// 如果认证成功,启动版本检查器 isInitializing = true
if (result.success) {
try {
// 监听认证错误事件(只注册一次)
if (!authEventBus.listeners.includes(handleAuthError)) {
authEventBus.onAuthError(handleAuthError)
}
// 监听版本更新事件(只注册一次)
if (!window.hasVersionListeners) {
window.addEventListener('version:logout', handleVersionLogout)
window.addEventListener('version:refresh', handleVersionRefresh)
window.hasVersionListeners = true
}
// 进行版本检查
if (!checkVersions()) {
initialized.value = true
return
}
if (accessToken.value && !user.value) {
// 有token但无用户信息自动拉取
loading.value = true
try {
const result = await fetchUserProfile()
isAuthenticated.value = result.success
// 如果认证成功,启动版本检查器
if (result.success) {
versionChecker.startAutoCheck()
}
} catch {
isAuthenticated.value = false
logout()
} finally {
loading.value = false
initialized.value = true
}
} else {
// 如果已经认证,启动版本检查器
if (isAuthenticated.value) {
versionChecker.startAutoCheck() versionChecker.startAutoCheck()
} }
} catch {
isAuthenticated.value = false
logout()
} finally {
loading.value = false
initialized.value = true initialized.value = true
} }
} else { } finally {
// 如果已经认证,启动版本检查器 isInitializing = false
if (isAuthenticated.value) {
versionChecker.startAutoCheck()
}
initialized.value = true
} }
} }