This commit is contained in:
@@ -57,6 +57,11 @@ export const productApi = {
|
||||
getProductApiConfigByCode: (productCode) => request.get(`/products/code/${productCode}/api-config`),
|
||||
getProductApiConfigsByProductIDs: (productIds) => request.get('/products/api-configs', {
|
||||
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}`),
|
||||
|
||||
// 获取我的订阅使用情况 (需认证)
|
||||
getMySubscriptionUsage: (id) => request.get(`/my/subscriptions/${id}/usage`)
|
||||
getMySubscriptionUsage: (id) => request.get(`/my/subscriptions/${id}/usage`),
|
||||
|
||||
// 取消我的订阅 (需认证)
|
||||
cancelMySubscription: (id) => request.post(`/my/subscriptions/${id}/cancel`)
|
||||
}
|
||||
|
||||
// 财务相关接口
|
||||
|
||||
@@ -73,12 +73,12 @@
|
||||
<!-- 已订阅的产品 -->
|
||||
<el-button
|
||||
v-else-if="isSubscribed"
|
||||
type="info"
|
||||
disabled
|
||||
class="action-btn subscribed-btn"
|
||||
type="danger"
|
||||
@click="handleCancelSubscribe"
|
||||
class="action-btn cancel-subscribe-btn"
|
||||
size="small"
|
||||
>
|
||||
已订阅
|
||||
取消订阅
|
||||
</el-button>
|
||||
|
||||
<!-- 可订阅的产品 -->
|
||||
@@ -104,10 +104,14 @@ const props = defineProps({
|
||||
isSubscribed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
subscription: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['view-detail', 'subscribe'])
|
||||
const emit = defineEmits(['view-detail', 'subscribe', 'cancel-subscribe'])
|
||||
|
||||
// 格式化价格
|
||||
const formatPrice = (price) => {
|
||||
@@ -134,6 +138,11 @@ const handleViewDetail = () => {
|
||||
const handleSubscribe = () => {
|
||||
emit('subscribe', props.product)
|
||||
}
|
||||
|
||||
// 取消订阅
|
||||
const handleCancelSubscribe = () => {
|
||||
emit('cancel-subscribe', props.product)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -357,19 +366,17 @@ const handleSubscribe = () => {
|
||||
box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.subscribed-btn {
|
||||
background: rgba(100, 116, 139, 0.1);
|
||||
border-color: rgba(100, 116, 139, 0.2);
|
||||
color: #64748b;
|
||||
cursor: not-allowed;
|
||||
.cancel-subscribe-btn {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
border-color: #ef4444;
|
||||
color: white;
|
||||
box-shadow: 0 2px 4px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.subscribed-btn:hover {
|
||||
background: rgba(100, 116, 139, 0.1);
|
||||
border-color: rgba(100, 116, 139, 0.2);
|
||||
color: #64748b;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
.cancel-subscribe-btn:hover {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||
border-color: #dc2626;
|
||||
box-shadow: 0 4px 8px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.disabled-btn {
|
||||
|
||||
@@ -414,13 +414,25 @@
|
||||
<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>
|
||||
<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>
|
||||
<el-button type="success" size="default"
|
||||
@click="copyToClipboard(JSON.stringify(decryptedData, null, 2))" class="w-full">
|
||||
复制解密内容
|
||||
</el-button>
|
||||
</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>
|
||||
@@ -545,6 +557,8 @@ const productsLoading = ref(false)
|
||||
const debugging = ref(false)
|
||||
const encrypting = ref(false)
|
||||
const autoSelecting = ref(false) // 新增:自动选择状态
|
||||
const isSelectingProduct = ref(false) // 防止重复选择产品的标志
|
||||
const lastSelectedProductId = ref(null) // 记录最后选择的产品ID
|
||||
const userProducts = ref([])
|
||||
const apiConfig = ref(null)
|
||||
const selectedProduct = ref(null)
|
||||
@@ -650,12 +664,22 @@ onMounted(async () => {
|
||||
|
||||
// 监听路由参数变化,自动选择产品
|
||||
watch(
|
||||
() => route.params.productId,
|
||||
async (newProductId) => {
|
||||
() => route.query.productId || route.params.productId,
|
||||
async (newProductId, oldProductId) => {
|
||||
// 防止重复触发:如果产品ID没有变化,不执行
|
||||
if (newProductId === oldProductId) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果正在选择产品,不重复执行
|
||||
if (isSelectingProduct.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (newProductId && userProducts.value.length > 0) {
|
||||
await autoSelectProduct(newProductId)
|
||||
} else if (!newProductId && userProducts.value.length > 0) {
|
||||
// 如果没有指定产品,选择第一个
|
||||
} else if (!newProductId && userProducts.value.length > 0 && !selectedProduct.value) {
|
||||
// 如果没有指定产品且当前没有选中产品,选择第一个
|
||||
await selectProduct(userProducts.value[0])
|
||||
}
|
||||
}
|
||||
@@ -663,6 +687,18 @@ watch(
|
||||
|
||||
// 自动选择产品
|
||||
const autoSelectProduct = async (productId) => {
|
||||
// 防止重复执行
|
||||
if (isSelectingProduct.value) {
|
||||
console.log('正在选择产品,跳过重复请求')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果已经选择了相同的产品,不重复选择
|
||||
if (lastSelectedProductId.value === productId && selectedProduct.value) {
|
||||
console.log('产品已选择,跳过重复选择:', productId)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果用户产品列表为空,等待加载完成
|
||||
if (!userProducts.value.length) {
|
||||
console.log('等待用户产品列表加载完成...')
|
||||
@@ -686,7 +722,13 @@ const autoSelectProduct = async (productId) => {
|
||||
|
||||
if (targetProduct) {
|
||||
console.log('自动选择产品:', targetProduct)
|
||||
isSelectingProduct.value = true
|
||||
try {
|
||||
await selectProduct(targetProduct)
|
||||
lastSelectedProductId.value = productId
|
||||
} finally {
|
||||
isSelectingProduct.value = false
|
||||
}
|
||||
} else {
|
||||
console.warn('未找到指定的产品:', productId)
|
||||
ElMessage.warning(`未找到产品ID为 ${productId} 的产品,请手动选择`)
|
||||
@@ -699,7 +741,7 @@ const loadUserProducts = async () => {
|
||||
try {
|
||||
const response = await subscriptionApi.getMySubscriptions({
|
||||
page: 1,
|
||||
page_size: 100
|
||||
page_size: 1000
|
||||
})
|
||||
|
||||
if (response.success && response.data?.items) {
|
||||
@@ -712,17 +754,24 @@ const loadUserProducts = async () => {
|
||||
}))
|
||||
console.log("route.params", route)
|
||||
|
||||
// 检查是否有params参数指定产品
|
||||
if (route.params.productId && userProducts.value.length > 0) {
|
||||
// 检查是否有query或params参数指定产品
|
||||
const productId = route.query.productId || route.params.productId
|
||||
if (productId && userProducts.value.length > 0) {
|
||||
await nextTick()
|
||||
|
||||
await autoSelectProduct(route.params.productId)
|
||||
} else if (userProducts.value.length > 0) {
|
||||
// 没有指定产品时,默认选择第一个
|
||||
// 使用 autoSelectProduct 会自动处理防重复逻辑
|
||||
await autoSelectProduct(productId)
|
||||
} else if (userProducts.value.length > 0 && !selectedProduct.value) {
|
||||
// 没有指定产品时,默认选择第一个(仅在未选择产品时)
|
||||
autoSelecting.value = true
|
||||
isSelectingProduct.value = true
|
||||
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 {
|
||||
// 如果没有订阅产品,显示提示信息
|
||||
userProducts.value = []
|
||||
@@ -753,6 +802,15 @@ const loadApiKeys = async () => {
|
||||
|
||||
// 选择产品
|
||||
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密钥已经加载
|
||||
if (!debugForm.accessId || !debugForm.secretKey) {
|
||||
ElMessage.warning('正在加载API密钥,请稍候...')
|
||||
@@ -764,6 +822,7 @@ const selectProduct = async (product) => {
|
||||
debugForm.params = {}
|
||||
debugResult.value = null
|
||||
encryptedData.value = null
|
||||
decryptedData.value = null
|
||||
activeTab.value = 'basic_info' // 重置Tab
|
||||
productDocumentation.value = null // 重置文档
|
||||
|
||||
@@ -1106,6 +1165,11 @@ const handleDebug = async () => {
|
||||
}
|
||||
|
||||
debugging.value = true
|
||||
// 清空之前的调试结果,确保UI实时更新
|
||||
debugResult.value = null
|
||||
decryptedData.value = null
|
||||
await nextTick() // 确保DOM更新
|
||||
|
||||
const startTime = new Date()
|
||||
|
||||
try {
|
||||
@@ -1118,22 +1182,6 @@ const handleDebug = async () => {
|
||||
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. 加密参数
|
||||
const encryptedParams = await encryptWithAES(parsedParams, debugForm.secretKey)
|
||||
if (!encryptedParams) {
|
||||
@@ -1159,7 +1207,7 @@ const handleDebug = async () => {
|
||||
console.log('产品API调用成功:', responseData)
|
||||
const endTime = new Date()
|
||||
|
||||
// 5. 保存调试结果
|
||||
// 5. 保存调试结果 - 立即更新,确保UI实时显示
|
||||
debugResult.value = createDebugResult(
|
||||
selectedProduct.value,
|
||||
requestBody,
|
||||
@@ -1170,6 +1218,9 @@ const handleDebug = async () => {
|
||||
responseData.success && responseData.data?.code === 0
|
||||
)
|
||||
|
||||
// 确保DOM更新后再进行解密操作
|
||||
await nextTick()
|
||||
|
||||
// 6. 如果响应成功且包含加密数据,自动解密
|
||||
console.log('responseData', responseData)
|
||||
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) {
|
||||
// 使用 nextTick 确保响应式更新
|
||||
decryptedData.value = decryptResult.data
|
||||
await nextTick()
|
||||
ElMessage.success('调试完成,数据已自动解密')
|
||||
} else {
|
||||
ElMessage.warning('调试完成,但数据解密失败:' + (decryptResult.message || '未知错误'))
|
||||
@@ -1223,6 +1276,9 @@ const handleDebug = async () => {
|
||||
false
|
||||
)
|
||||
|
||||
// 确保DOM更新
|
||||
await nextTick()
|
||||
|
||||
ElMessage.error('API调用失败:' + apiError.message)
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1239,6 +1295,9 @@ const handleDebug = async () => {
|
||||
endTime,
|
||||
false
|
||||
)
|
||||
|
||||
// 确保DOM更新
|
||||
await nextTick()
|
||||
} finally {
|
||||
debugging.value = false
|
||||
}
|
||||
|
||||
@@ -10,6 +10,15 @@
|
||||
</div>
|
||||
<div class="list-page-actions">
|
||||
<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
|
||||
v-if="!isSubscribed"
|
||||
type="primary"
|
||||
@@ -20,10 +29,11 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="success"
|
||||
disabled
|
||||
type="danger"
|
||||
@click="handleCancelSubscription"
|
||||
:loading="cancelling"
|
||||
>
|
||||
已订阅
|
||||
取消订阅
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="isSubscribed"
|
||||
@@ -260,7 +270,7 @@
|
||||
|
||||
<script setup>
|
||||
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 { marked } from 'marked'
|
||||
|
||||
@@ -272,6 +282,8 @@ const loading = ref(false)
|
||||
const product = ref(null)
|
||||
const userSubscriptions = ref([])
|
||||
const subscribing = ref(false)
|
||||
const cancelling = ref(false)
|
||||
const downloading = ref(false)
|
||||
const activeTab = ref('content')
|
||||
const currentTimestamp = ref('')
|
||||
|
||||
@@ -288,6 +300,12 @@ const isSubscribed = computed(() => {
|
||||
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(() => {
|
||||
loadUserSubscriptions()
|
||||
@@ -321,7 +339,7 @@ const startTimestampUpdate = () => {
|
||||
// 加载用户订阅
|
||||
const loadUserSubscriptions = async () => {
|
||||
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 || []
|
||||
} catch (error) {
|
||||
console.error('加载用户订阅失败:', error)
|
||||
@@ -528,19 +546,53 @@ const handleSubscribe = async () => {
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('订阅失败:', error)
|
||||
ElMessage.error('订阅失败')
|
||||
const errorMessage = error.response?.data?.message || error.message || '订阅失败'
|
||||
ElMessage.error(errorMessage)
|
||||
}
|
||||
} finally {
|
||||
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 = () => {
|
||||
if (!product.value) return
|
||||
router.push({
|
||||
name: 'ApiDebugger',
|
||||
params: { productId: product.value.id }
|
||||
query: { productId: product.value.id }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -659,6 +711,65 @@ const getDefaultErrorCodes = () => {
|
||||
| 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 文档
|
||||
const downloadMarkdown = (type) => {
|
||||
if (!product.value?.documentation) {
|
||||
|
||||
@@ -50,9 +50,15 @@
|
||||
</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">
|
||||
<ProductCard v-for="product in products" :key="product.id" :product="product"
|
||||
:is-subscribed="product.is_subscribed" @view-detail="handleViewDetail"
|
||||
@subscribe="handleSubscribe" />
|
||||
<ProductCard
|
||||
v-for="product in products"
|
||||
:key="product.id"
|
||||
:product="product"
|
||||
:is-subscribed="getProductSubscriptionStatus(product.id)"
|
||||
:subscription="getProductSubscription(product.id)"
|
||||
@view-detail="handleViewDetail"
|
||||
@subscribe="handleSubscribe"
|
||||
@cancel-subscribe="handleCancelSubscribe" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -65,7 +71,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { categoryApi, productApi } from '@/api'
|
||||
import { categoryApi, productApi, subscriptionApi } from '@/api'
|
||||
import FilterItem from '@/components/common/FilterItem.vue'
|
||||
import FilterSection from '@/components/common/FilterSection.vue'
|
||||
import ListPageLayout from '@/components/common/ListPageLayout.vue'
|
||||
@@ -81,6 +87,7 @@ const categories = ref([])
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(12)
|
||||
const userSubscriptions = ref([]) // 用户订阅列表
|
||||
|
||||
// 筛选条件
|
||||
const filters = reactive({
|
||||
@@ -96,6 +103,7 @@ let searchTimer = null
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadCategories()
|
||||
loadUserSubscriptions()
|
||||
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 () => {
|
||||
loading.value = true
|
||||
@@ -122,6 +154,9 @@ const loadProducts = async () => {
|
||||
const response = await productApi.getProducts(params)
|
||||
products.value = response.data?.items || []
|
||||
total.value = response.data?.total || 0
|
||||
|
||||
// 重新加载订阅列表以确保状态同步
|
||||
await loadUserSubscriptions()
|
||||
} catch (error) {
|
||||
console.error('加载产品失败:', error)
|
||||
ElMessage.error('加载产品失败')
|
||||
@@ -191,12 +226,51 @@ const handleSubscribe = async (product) => {
|
||||
await productApi.subscribeProduct(product.id)
|
||||
ElMessage.success('订阅成功')
|
||||
|
||||
// 重新加载产品列表以更新订阅状态
|
||||
// 重新加载订阅列表和产品列表以更新订阅状态
|
||||
await loadUserSubscriptions()
|
||||
await loadProducts()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="320" fixed="right">
|
||||
<el-table-column label="操作" width="400" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center space-x-2">
|
||||
<el-button
|
||||
@@ -155,6 +155,13 @@
|
||||
>
|
||||
在线调试
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleCancelSubscription(row)"
|
||||
>
|
||||
取消订阅
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -216,7 +223,7 @@ 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'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -315,9 +315,18 @@ const router = createRouter({
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
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()
|
||||
} 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')
|
||||
return
|
||||
}
|
||||
@@ -338,7 +347,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
}
|
||||
|
||||
// 已登录用户访问认证页面,重定向到数据大厅
|
||||
if (to.path.startsWith('/auth') && userStore.isLoggedIn) {
|
||||
if (isAuthRoute && userStore.isLoggedIn) {
|
||||
next('/products')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -416,17 +416,43 @@ export const useUserStore = defineStore('user', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化标志,防止重复初始化
|
||||
let isInitializing = false
|
||||
|
||||
// 初始化
|
||||
const init = async () => {
|
||||
// 监听认证错误事件
|
||||
authEventBus.onAuthError(handleAuthError)
|
||||
// 如果已经初始化完成,直接返回
|
||||
if (initialized.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// 监听版本更新事件
|
||||
// 如果正在初始化,等待完成
|
||||
if (isInitializing) {
|
||||
// 等待初始化完成
|
||||
while (isInitializing) {
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
isInitializing = true
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -455,6 +481,9 @@ export const useUserStore = defineStore('user', () => {
|
||||
}
|
||||
initialized.value = true
|
||||
}
|
||||
} finally {
|
||||
isInitializing = false
|
||||
}
|
||||
}
|
||||
|
||||
// 清理
|
||||
|
||||
Reference in New Issue
Block a user