2025-11-24 16:06:44 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="list-page-container">
|
|
|
|
|
|
<div class="list-page-card">
|
|
|
|
|
|
<!-- 页面头部 -->
|
|
|
|
|
|
<div class="list-page-header">
|
|
|
|
|
|
<div class="flex justify-between items-start">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h1 class="list-page-title">{{ product?.name || '产品详情' }}</h1>
|
|
|
|
|
|
<p class="list-page-subtitle">{{ product?.description || '查看产品详细信息' }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="list-page-actions">
|
|
|
|
|
|
<el-button @click="$router.back()">返回</el-button>
|
2025-12-04 12:44:54 +08:00
|
|
|
|
<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>
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="!isSubscribed"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
@click="handleSubscribe"
|
|
|
|
|
|
:loading="subscribing"
|
|
|
|
|
|
>
|
|
|
|
|
|
订阅产品
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-else
|
2025-12-04 12:44:54 +08:00
|
|
|
|
type="danger"
|
|
|
|
|
|
@click="handleCancelSubscription"
|
|
|
|
|
|
:loading="cancelling"
|
2025-11-24 16:06:44 +08:00
|
|
|
|
>
|
2025-12-04 12:44:54 +08:00
|
|
|
|
取消订阅
|
2025-11-24 16:06:44 +08:00
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="isSubscribed"
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
@click="goToApiDebugger"
|
|
|
|
|
|
>
|
|
|
|
|
|
前往在线调试
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 产品详情内容 -->
|
|
|
|
|
|
<div class="list-page-table">
|
|
|
|
|
|
<div v-if="loading" class="flex justify-center items-center py-12">
|
|
|
|
|
|
<el-loading size="large" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else-if="!product" class="text-center py-12">
|
|
|
|
|
|
<el-empty description="产品不存在" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else class="product-detail-content">
|
|
|
|
|
|
<!-- 基本信息 -->
|
|
|
|
|
|
<div class="detail-section">
|
|
|
|
|
|
<h3 class="section-title">基本信息</h3>
|
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<label class="info-label">产品编号</label>
|
|
|
|
|
|
<span class="info-value">{{ product.code }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<label class="info-label">产品名称</label>
|
|
|
|
|
|
<span class="info-value">{{ product.name }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<label class="info-label">产品分类</label>
|
|
|
|
|
|
<span class="info-value">{{ product.category?.name || '未分类' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<label class="info-label">产品类型</label>
|
|
|
|
|
|
<span class="info-value">
|
|
|
|
|
|
<el-tag :type="product.is_package ? 'success' : 'info'" size="small">
|
|
|
|
|
|
{{ product.is_package ? '组合包' : '单品' }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<label class="info-label">价格</label>
|
|
|
|
|
|
<span class="info-value price">¥{{ formatPrice(product.price) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<label class="info-label">状态</label>
|
|
|
|
|
|
<span class="info-value">
|
|
|
|
|
|
<el-tag :type="product.is_enabled ? 'success' : 'danger'" size="small">
|
|
|
|
|
|
{{ product.is_enabled ? '已启用' : '已禁用' }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 产品描述 -->
|
|
|
|
|
|
<div class="detail-section">
|
|
|
|
|
|
<h3 class="section-title">产品描述</h3>
|
|
|
|
|
|
<div class="description-content">
|
|
|
|
|
|
{{ product.description || '暂无描述' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 组合包信息 -->
|
|
|
|
|
|
<div v-if="product.is_package && product.package_items && product.package_items.length > 0" class="detail-section">
|
|
|
|
|
|
<h3 class="section-title">组合包内容</h3>
|
|
|
|
|
|
<div class="package-items-container">
|
|
|
|
|
|
<div class="package-summary">
|
|
|
|
|
|
<el-alert
|
|
|
|
|
|
title="此组合包包含以下产品"
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #default>
|
|
|
|
|
|
<p>共 {{ product.package_items.length }} 个产品,总价值 ¥{{ calculatePackageTotalPrice() }}</p>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-alert>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="package-items-grid">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="item in product.package_items"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
class="package-item-card"
|
|
|
|
|
|
@click="openProductDetail(item.product_id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="package-item-header">
|
|
|
|
|
|
<div class="package-item-info">
|
|
|
|
|
|
<h4 class="package-item-name">{{ item.product_name }}</h4>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="package-item-quantity">
|
|
|
|
|
|
<el-tag type="primary" size="small">{{ item.product_code }}</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="package-item-price">
|
|
|
|
|
|
<span class="price-label">价值:</span>
|
|
|
|
|
|
<span class="price-value">¥{{ formatPrice(item.price) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 内容Tab -->
|
|
|
|
|
|
<div class="detail-section">
|
|
|
|
|
|
<el-tabs v-model="activeTab" type="card" class="content-tabs">
|
|
|
|
|
|
<!-- 产品内容Tab -->
|
|
|
|
|
|
<el-tab-pane label="产品内容" name="content">
|
|
|
|
|
|
<div class="tab-content">
|
|
|
|
|
|
<div class="content-richtext" v-html="product.content || '<p>暂无内容</p>'"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 请求方式Tab -->
|
|
|
|
|
|
<el-tab-pane label="请求方式" name="basic_info">
|
|
|
|
|
|
<div class="tab-content">
|
|
|
|
|
|
<!-- 请求URL信息 -->
|
|
|
|
|
|
<div v-if="product.documentation?.request_url" class="request-url-section">
|
|
|
|
|
|
<h4 class="request-url-title">请求地址</h4>
|
|
|
|
|
|
<div class="request-url-content">
|
|
|
|
|
|
<div class="request-method">
|
|
|
|
|
|
<el-tag :type="getMethodTagType(product.documentation?.request_method)" size="small">
|
|
|
|
|
|
{{ product.documentation?.request_method || 'POST' }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="request-url">
|
|
|
|
|
|
<code>{{ product.documentation?.request_url }}</code>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
@click="copyRequestUrl"
|
|
|
|
|
|
class="copy-btn"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-icon><DocumentCopy /></el-icon>
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 时间戳备注 -->
|
|
|
|
|
|
<div class="timestamp-note">
|
|
|
|
|
|
<el-alert
|
|
|
|
|
|
title="时间戳说明"
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #default>
|
|
|
|
|
|
<p>URL中的 <code>t=13位时间戳</code> 参数需要实时生成当前时间,当前时间戳:<strong>{{ currentTimestamp }}</strong></p>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-alert>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="product.documentation?.basic_info" class="doc-content" ref="basicInfoRef" v-html="renderMarkdown(product.documentation.basic_info)"></div>
|
|
|
|
|
|
<div v-else class="doc-content" ref="basicInfoRef" v-html="renderMarkdown(getDefaultBasicInfo())"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 请求参数Tab -->
|
|
|
|
|
|
<el-tab-pane label="请求参数" name="request_params">
|
|
|
|
|
|
<div class="tab-content">
|
|
|
|
|
|
<div v-if="product.documentation?.request_params" class="doc-content" ref="requestParamsRef" v-html="renderMarkdown(product.documentation.request_params)"></div>
|
|
|
|
|
|
<div v-else class="no-content">
|
|
|
|
|
|
<el-empty description="暂无请求参数说明" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 返回字段说明Tab -->
|
|
|
|
|
|
<el-tab-pane label="返回字段说明" name="response_fields">
|
|
|
|
|
|
<div class="tab-content">
|
|
|
|
|
|
<div v-if="product.documentation?.response_fields">
|
|
|
|
|
|
<div class="tab-content-actions">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
@click="downloadMarkdown('response_fields')"
|
|
|
|
|
|
class="download-btn"
|
|
|
|
|
|
>
|
|
|
|
|
|
下载文档
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="doc-content" ref="responseFieldsRef" v-html="renderMarkdown(product.documentation.response_fields)"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="no-content">
|
|
|
|
|
|
<el-empty description="暂无返回字段说明" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 响应示例Tab -->
|
|
|
|
|
|
<el-tab-pane label="响应示例" name="response_example">
|
|
|
|
|
|
<div class="tab-content">
|
|
|
|
|
|
<div v-if="product.documentation?.response_example">
|
|
|
|
|
|
<div class="tab-content-actions">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
@click="downloadMarkdown('response_example')"
|
|
|
|
|
|
class="download-btn"
|
|
|
|
|
|
>
|
|
|
|
|
|
下载示例
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="doc-content" ref="responseExampleRef" v-html="renderMarkdown(product.documentation.response_example)"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="no-content">
|
|
|
|
|
|
<el-empty description="暂无响应示例" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 错误代码Tab -->
|
|
|
|
|
|
<el-tab-pane label="错误代码" name="error_codes">
|
|
|
|
|
|
<div class="tab-content">
|
|
|
|
|
|
<div v-if="product.documentation?.error_codes" class="doc-content" ref="errorCodesRef" v-html="renderMarkdown(product.documentation.error_codes)"></div>
|
|
|
|
|
|
<div v-else class="doc-content" ref="errorCodesRef" v-html="renderMarkdown(getDefaultErrorCodes())"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { productApi, subscriptionApi } from '@/api'
|
2025-12-04 12:44:54 +08:00
|
|
|
|
import { DocumentCopy, Download } from '@element-plus/icons-vue'
|
2025-11-24 16:06:44 +08:00
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
|
import { marked } from 'marked'
|
|
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
|
|
|
|
|
|
// 响应式数据
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const product = ref(null)
|
|
|
|
|
|
const userSubscriptions = ref([])
|
|
|
|
|
|
const subscribing = ref(false)
|
2025-12-04 12:44:54 +08:00
|
|
|
|
const cancelling = ref(false)
|
|
|
|
|
|
const downloading = ref(false)
|
2025-11-24 16:06:44 +08:00
|
|
|
|
const activeTab = ref('content')
|
|
|
|
|
|
const currentTimestamp = ref('')
|
|
|
|
|
|
|
|
|
|
|
|
// DOM 引用
|
|
|
|
|
|
const basicInfoRef = ref(null)
|
|
|
|
|
|
const requestParamsRef = ref(null)
|
|
|
|
|
|
const responseFieldsRef = ref(null)
|
|
|
|
|
|
const responseExampleRef = ref(null)
|
|
|
|
|
|
const errorCodesRef = ref(null)
|
|
|
|
|
|
|
|
|
|
|
|
// 计算属性
|
|
|
|
|
|
const isSubscribed = computed(() => {
|
|
|
|
|
|
if (!product.value || !userSubscriptions.value.length) return false
|
|
|
|
|
|
return userSubscriptions.value.some(sub => sub.product_id === product.value.id)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-04 12:44:54 +08:00
|
|
|
|
// 获取当前产品的订阅信息
|
|
|
|
|
|
const currentSubscription = computed(() => {
|
|
|
|
|
|
if (!product.value || !userSubscriptions.value.length) return null
|
|
|
|
|
|
return userSubscriptions.value.find(sub => sub.product_id === product.value.id)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-11-24 16:06:44 +08:00
|
|
|
|
// 初始化
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
loadUserSubscriptions()
|
|
|
|
|
|
loadProductDetail().then(() => {
|
|
|
|
|
|
addCollapsibleFeatures()
|
|
|
|
|
|
})
|
|
|
|
|
|
startTimestampUpdate()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 组件卸载时清理定时器
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
if (timestampTimer.value) {
|
|
|
|
|
|
clearInterval(timestampTimer.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 时间戳更新定时器
|
|
|
|
|
|
const timestampTimer = ref(null)
|
|
|
|
|
|
|
|
|
|
|
|
// 开始时间戳更新
|
|
|
|
|
|
const startTimestampUpdate = () => {
|
|
|
|
|
|
// 立即更新一次
|
|
|
|
|
|
currentTimestamp.value = getCurrentTimestamp()
|
|
|
|
|
|
|
|
|
|
|
|
// 每秒更新一次
|
|
|
|
|
|
timestampTimer.value = setInterval(() => {
|
|
|
|
|
|
currentTimestamp.value = getCurrentTimestamp()
|
|
|
|
|
|
}, 1000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载用户订阅
|
|
|
|
|
|
const loadUserSubscriptions = async () => {
|
|
|
|
|
|
try {
|
2025-12-04 12:44:54 +08:00
|
|
|
|
const response = await subscriptionApi.getMySubscriptions({ page: 1, page_size: 1000 })
|
2025-11-24 16:06:44 +08:00
|
|
|
|
userSubscriptions.value = response.data?.items || []
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载用户订阅失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载产品详情
|
|
|
|
|
|
const loadProductDetail = async () => {
|
|
|
|
|
|
const productId = route.params.id
|
|
|
|
|
|
if (!productId) {
|
|
|
|
|
|
ElMessage.error('产品ID不存在')
|
|
|
|
|
|
router.push('/products')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 添加 with_document 参数获取文档信息
|
|
|
|
|
|
const response = await productApi.getProductDetail(productId, { with_document: true })
|
|
|
|
|
|
product.value = response.data
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载产品详情失败:', error)
|
|
|
|
|
|
ElMessage.error('加载产品详情失败')
|
|
|
|
|
|
router.push('/products')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化价格
|
|
|
|
|
|
const formatPrice = (price) => {
|
|
|
|
|
|
if (!price) return '0.00'
|
|
|
|
|
|
return Number(price).toFixed(2)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算组合包总价
|
|
|
|
|
|
const calculatePackageTotalPrice = () => {
|
|
|
|
|
|
if (!product.value?.package_items || !product.value.package_items.length) {
|
|
|
|
|
|
return '0.00'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const total = product.value.package_items.reduce((sum, item) => {
|
|
|
|
|
|
return sum + item.price
|
|
|
|
|
|
}, 0)
|
|
|
|
|
|
|
|
|
|
|
|
return formatPrice(total)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Markdown渲染方法
|
|
|
|
|
|
const renderMarkdown = (content) => {
|
|
|
|
|
|
if (!content) return '<p>暂无内容</p>'
|
|
|
|
|
|
try {
|
|
|
|
|
|
return marked(content)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Markdown渲染失败:', error)
|
|
|
|
|
|
return content
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加折叠功能到文档内容
|
|
|
|
|
|
const addCollapsibleFeatures = () => {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
const refs = [basicInfoRef, requestParamsRef, responseFieldsRef, responseExampleRef, errorCodesRef]
|
|
|
|
|
|
|
|
|
|
|
|
refs.forEach(ref => {
|
|
|
|
|
|
if (!ref.value) return
|
|
|
|
|
|
|
|
|
|
|
|
const container = ref.value
|
|
|
|
|
|
|
|
|
|
|
|
// 为子产品标题(h2)添加折叠功能
|
|
|
|
|
|
const h2Elements = container.querySelectorAll('h2')
|
|
|
|
|
|
h2Elements.forEach((h2) => {
|
|
|
|
|
|
// 检查是否已经有折叠按钮
|
|
|
|
|
|
if (h2.querySelector('.collapse-toggle')) return
|
|
|
|
|
|
|
|
|
|
|
|
// 检查下一个兄弟元素是否是已经处理过的折叠内容
|
|
|
|
|
|
if (h2.nextElementSibling?.classList.contains('collapsible-content')) return
|
|
|
|
|
|
|
|
|
|
|
|
// 查找下一个 h2、h1 或分隔线(hr)之间的内容
|
|
|
|
|
|
let nextSibling = h2.nextElementSibling
|
|
|
|
|
|
let contentEnd = null
|
|
|
|
|
|
|
|
|
|
|
|
// 找到下一个 h2、h1 或分隔线(hr)
|
|
|
|
|
|
while (nextSibling) {
|
|
|
|
|
|
if (nextSibling.tagName === 'H2' || nextSibling.tagName === 'H1' || nextSibling.tagName === 'HR') {
|
|
|
|
|
|
contentEnd = nextSibling
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
nextSibling = nextSibling.nextElementSibling
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果找到内容,创建折叠容器
|
|
|
|
|
|
if (h2.nextElementSibling && h2.nextElementSibling !== contentEnd) {
|
|
|
|
|
|
// 创建折叠按钮(放在 h2 内部,作为可点击的标题)
|
|
|
|
|
|
const toggleBtn = document.createElement('button')
|
|
|
|
|
|
toggleBtn.className = 'collapse-toggle'
|
|
|
|
|
|
toggleBtn.innerHTML = '<span class="collapse-icon">▼</span>'
|
|
|
|
|
|
toggleBtn.setAttribute('aria-expanded', 'true')
|
|
|
|
|
|
toggleBtn.setAttribute('title', '点击展开/折叠')
|
|
|
|
|
|
|
|
|
|
|
|
// 将按钮插入到 h2 内部(在文本前面)
|
|
|
|
|
|
const h2Text = h2.textContent
|
|
|
|
|
|
h2.innerHTML = ''
|
|
|
|
|
|
h2.appendChild(toggleBtn)
|
|
|
|
|
|
h2.appendChild(document.createTextNode(' ' + h2Text))
|
|
|
|
|
|
h2.style.cursor = 'pointer'
|
|
|
|
|
|
h2.style.userSelect = 'none'
|
|
|
|
|
|
|
|
|
|
|
|
// 创建内容容器
|
|
|
|
|
|
const contentWrapper = document.createElement('div')
|
|
|
|
|
|
contentWrapper.className = 'collapsible-content'
|
|
|
|
|
|
|
|
|
|
|
|
// 移动内容到容器中
|
|
|
|
|
|
let sibling = h2.nextElementSibling
|
|
|
|
|
|
while (sibling && sibling !== contentEnd) {
|
|
|
|
|
|
const nextSibling = sibling.nextElementSibling
|
|
|
|
|
|
contentWrapper.appendChild(sibling)
|
|
|
|
|
|
sibling = nextSibling
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 插入内容容器到 h2 后面
|
|
|
|
|
|
h2.parentNode.insertBefore(contentWrapper, h2.nextSibling)
|
|
|
|
|
|
|
|
|
|
|
|
// 添加点击事件(h2 和按钮都可以点击)
|
|
|
|
|
|
const toggleCollapse = () => {
|
|
|
|
|
|
const isExpanded = toggleBtn.getAttribute('aria-expanded') === 'true'
|
|
|
|
|
|
contentWrapper.style.display = isExpanded ? 'none' : 'block'
|
|
|
|
|
|
toggleBtn.setAttribute('aria-expanded', !isExpanded)
|
|
|
|
|
|
toggleBtn.querySelector('.collapse-icon').textContent = isExpanded ? '▶' : '▼'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
toggleBtn.addEventListener('click', (e) => {
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
toggleCollapse()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
h2.addEventListener('click', (e) => {
|
|
|
|
|
|
if (e.target !== toggleBtn && !toggleBtn.contains(e.target)) {
|
|
|
|
|
|
toggleCollapse()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 为 JSON 代码块(pre)添加折叠功能
|
|
|
|
|
|
const preElements = container.querySelectorAll('pre')
|
|
|
|
|
|
preElements.forEach(pre => {
|
|
|
|
|
|
// 检查是否已经有折叠按钮
|
|
|
|
|
|
if (pre.previousElementSibling?.classList.contains('code-collapse-toggle')) return
|
|
|
|
|
|
|
|
|
|
|
|
// 创建折叠按钮
|
|
|
|
|
|
const toggleBtn = document.createElement('button')
|
|
|
|
|
|
toggleBtn.className = 'code-collapse-toggle'
|
|
|
|
|
|
toggleBtn.innerHTML = '<span class="collapse-icon">▼</span> <span class="collapse-text">展开/折叠代码</span>'
|
|
|
|
|
|
toggleBtn.setAttribute('aria-expanded', 'true')
|
|
|
|
|
|
|
|
|
|
|
|
// 插入按钮
|
|
|
|
|
|
pre.parentNode.insertBefore(toggleBtn, pre)
|
|
|
|
|
|
|
|
|
|
|
|
// 添加点击事件
|
|
|
|
|
|
toggleBtn.addEventListener('click', () => {
|
|
|
|
|
|
const isExpanded = toggleBtn.getAttribute('aria-expanded') === 'true'
|
|
|
|
|
|
pre.style.display = isExpanded ? 'none' : 'block'
|
|
|
|
|
|
toggleBtn.setAttribute('aria-expanded', !isExpanded)
|
|
|
|
|
|
toggleBtn.querySelector('.collapse-icon').textContent = isExpanded ? '▶' : '▼'
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 监听 activeTab 变化,重新添加折叠功能
|
|
|
|
|
|
watch(activeTab, () => {
|
|
|
|
|
|
addCollapsibleFeatures()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 监听产品数据变化,重新添加折叠功能
|
|
|
|
|
|
watch(() => product.value?.documentation, () => {
|
|
|
|
|
|
addCollapsibleFeatures()
|
|
|
|
|
|
}, { deep: true })
|
|
|
|
|
|
|
|
|
|
|
|
// 订阅产品
|
|
|
|
|
|
const handleSubscribe = async () => {
|
|
|
|
|
|
if (!product.value) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await ElMessageBox.confirm(
|
|
|
|
|
|
`确定要订阅产品"${product.value.name}"吗?`,
|
|
|
|
|
|
'确认订阅',
|
|
|
|
|
|
{
|
|
|
|
|
|
confirmButtonText: '确定订阅',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'info'
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
subscribing.value = true
|
|
|
|
|
|
|
|
|
|
|
|
await productApi.subscribeProduct(product.value.id)
|
|
|
|
|
|
ElMessage.success('订阅成功')
|
|
|
|
|
|
|
|
|
|
|
|
// 重新加载用户订阅
|
|
|
|
|
|
await loadUserSubscriptions()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (error !== 'cancel') {
|
|
|
|
|
|
console.error('订阅失败:', error)
|
2025-12-04 12:44:54 +08:00
|
|
|
|
const errorMessage = error.response?.data?.message || error.message || '订阅失败'
|
|
|
|
|
|
ElMessage.error(errorMessage)
|
2025-11-24 16:06:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
subscribing.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 12:44:54 +08:00
|
|
|
|
// 取消订阅
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 16:06:44 +08:00
|
|
|
|
// 前往在线调试
|
|
|
|
|
|
const goToApiDebugger = () => {
|
|
|
|
|
|
if (!product.value) return
|
|
|
|
|
|
router.push({
|
|
|
|
|
|
name: 'ApiDebugger',
|
2025-12-04 12:44:54 +08:00
|
|
|
|
query: { productId: product.value.id }
|
2025-11-24 16:06:44 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取请求方法标签类型
|
|
|
|
|
|
const getMethodTagType = (method) => {
|
|
|
|
|
|
switch (method) {
|
|
|
|
|
|
case 'GET':
|
|
|
|
|
|
return 'info'
|
|
|
|
|
|
case 'POST':
|
|
|
|
|
|
return 'primary'
|
|
|
|
|
|
case 'PUT':
|
|
|
|
|
|
return 'warning'
|
|
|
|
|
|
case 'DELETE':
|
|
|
|
|
|
return 'danger'
|
|
|
|
|
|
default:
|
|
|
|
|
|
return 'info'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 复制请求URL
|
|
|
|
|
|
const copyRequestUrl = () => {
|
|
|
|
|
|
if (!product.value?.documentation?.request_url) {
|
|
|
|
|
|
ElMessage.warning('请求地址不存在')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
navigator.clipboard.writeText(product.value.documentation.request_url).then(() => {
|
|
|
|
|
|
ElMessage.success('请求地址已复制到剪贴板')
|
|
|
|
|
|
}).catch(err => {
|
|
|
|
|
|
console.error('复制失败:', err)
|
|
|
|
|
|
ElMessage.error('复制失败')
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取当前时间戳
|
|
|
|
|
|
const getCurrentTimestamp = () => {
|
|
|
|
|
|
return Date.now().toString()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 打开产品详情页(新标签页)
|
|
|
|
|
|
const openProductDetail = (productId) => {
|
|
|
|
|
|
if (!productId) {
|
|
|
|
|
|
ElMessage.warning('产品ID不存在')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const url = `/products/${productId}`
|
|
|
|
|
|
window.open(url, '_blank')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取默认的请求方式内容
|
|
|
|
|
|
const getDefaultBasicInfo = () => {
|
|
|
|
|
|
return `## 请求头
|
|
|
|
|
|
|
|
|
|
|
|
| 字段名 | 类型 | 必填 | 描述 |
|
|
|
|
|
|
|--------|------|------|------|
|
|
|
|
|
|
| Access-Id | string | 是 | 账号的 Access-Id |
|
|
|
|
|
|
|
|
|
|
|
|
对于业务请求参数
|
|
|
|
|
|
|
|
|
|
|
|
通过加密后得到 Base64 字符串,将其放入到请求体中,字段名为 \`data\`,以此方式进行传参。
|
|
|
|
|
|
|
|
|
|
|
|
\`\`\`json
|
|
|
|
|
|
{
|
|
|
|
|
|
"data": "xxxx(base64)"
|
|
|
|
|
|
}
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
对接响应得到的公共参数
|
|
|
|
|
|
|
|
|
|
|
|
\`\`\`json
|
|
|
|
|
|
{
|
|
|
|
|
|
"code": "int",
|
|
|
|
|
|
"message": "string",
|
|
|
|
|
|
"transaction_id": "string", // 流水号
|
|
|
|
|
|
"data": "string"
|
|
|
|
|
|
}
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
**data** 字段为加密的数据,需要解密后查看。
|
|
|
|
|
|
|
|
|
|
|
|
## 加密和解密机制
|
|
|
|
|
|
|
|
|
|
|
|
账户获得的密钥(**Access Key**)是一个 16 进制字符串,使用 AES-128 加密算法。
|
|
|
|
|
|
|
|
|
|
|
|
### 加密过程:
|
|
|
|
|
|
|
|
|
|
|
|
- 加密模式:**AES-CBC 模式**。
|
|
|
|
|
|
- 密钥长度:**128 位(16 字节)**。
|
|
|
|
|
|
- 填充方式:**PKCS7 填充**。
|
|
|
|
|
|
- **IV(初始化向量)**:IV 长度为 16 字节(128 位),每次加密时随机生成。
|
|
|
|
|
|
- 加密后,将 **IV** 和密文拼接在一起进行传输。
|
|
|
|
|
|
- 最后,将拼接了 IV 的密文通过 **Base64 编码**,方便在网络或文件中传输。
|
|
|
|
|
|
|
|
|
|
|
|
### 解密过程:
|
|
|
|
|
|
|
|
|
|
|
|
- 解密时,首先从 Base64 解码后的数据中提取前 16 字节作为 **IV**。
|
|
|
|
|
|
- 然后使用提取的 **IV**,通过 AES-CBC 模式解密剩余部分的密文。
|
|
|
|
|
|
- 解密后去除 **PKCS7 填充**,即可得到原始明文。`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取默认的错误代码内容
|
|
|
|
|
|
const getDefaultErrorCodes = () => {
|
|
|
|
|
|
return `## 错误代码
|
|
|
|
|
|
|
|
|
|
|
|
| code | message |
|
|
|
|
|
|
|------|---------|
|
|
|
|
|
|
| 0 | 业务成功 |
|
|
|
|
|
|
| 1000 | 查询为空 |
|
|
|
|
|
|
| 1001 | 接口异常 |
|
|
|
|
|
|
| 1002 | 参数解密失败 |
|
|
|
|
|
|
| 1003 | 基础参数校验不正确 |
|
|
|
|
|
|
| 1004 | 未经授权的IP |
|
|
|
|
|
|
| 1005 | 缺少Access-Id |
|
|
|
|
|
|
| 1006 | 未经授权的AccessId |
|
|
|
|
|
|
| 1007 | 账户余额不足,无法请求 |
|
|
|
|
|
|
| 1008 | 未开通此产品 |
|
|
|
|
|
|
| 2001 | 业务失败 |`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 12:44:54 +08:00
|
|
|
|
// 下载接口文档
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 16:06:44 +08:00
|
|
|
|
// 下载 Markdown 文档
|
|
|
|
|
|
const downloadMarkdown = (type) => {
|
|
|
|
|
|
if (!product.value?.documentation) {
|
|
|
|
|
|
ElMessage.warning('文档内容不存在')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let content = ''
|
|
|
|
|
|
let filename = ''
|
|
|
|
|
|
|
|
|
|
|
|
if (type === 'response_fields') {
|
|
|
|
|
|
content = product.value.documentation.response_fields || ''
|
|
|
|
|
|
filename = `${product.value.name || '产品'}_返回字段说明.md`
|
|
|
|
|
|
} else if (type === 'response_example') {
|
|
|
|
|
|
content = product.value.documentation.response_example || ''
|
|
|
|
|
|
filename = `${product.value.name || '产品'}_响应示例.md`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!content) {
|
|
|
|
|
|
ElMessage.warning('该部分内容为空,无法下载')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建 Blob 对象
|
|
|
|
|
|
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' })
|
|
|
|
|
|
|
|
|
|
|
|
// 创建下载链接
|
|
|
|
|
|
const url = URL.createObjectURL(blob)
|
|
|
|
|
|
const link = document.createElement('a')
|
|
|
|
|
|
link.href = url
|
|
|
|
|
|
link.download = filename
|
|
|
|
|
|
|
|
|
|
|
|
// 触发下载
|
|
|
|
|
|
document.body.appendChild(link)
|
|
|
|
|
|
link.click()
|
|
|
|
|
|
|
|
|
|
|
|
// 清理
|
|
|
|
|
|
document.body.removeChild(link)
|
|
|
|
|
|
URL.revokeObjectURL(url)
|
|
|
|
|
|
|
|
|
|
|
|
ElMessage.success('文档下载成功')
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.product-detail-content {
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-section {
|
|
|
|
|
|
margin-bottom: 32px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-section:last-child {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
padding-bottom: 8px;
|
|
|
|
|
|
border-bottom: 1px solid rgba(226, 232, 240, 0.6);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-label {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-value {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-value.price {
|
|
|
|
|
|
color: #dc2626;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.description-content {
|
|
|
|
|
|
background: rgba(248, 250, 252, 0.5);
|
|
|
|
|
|
border: 1px solid rgba(226, 232, 240, 0.4);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.documentation-content {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.8);
|
|
|
|
|
|
border: 1px solid rgba(226, 232, 240, 0.6);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.subscription-info {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 组合包样式 */
|
|
|
|
|
|
.package-items-container {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-summary {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-items-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-item-card {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.8);
|
|
|
|
|
|
border: 1px solid rgba(226, 232, 240, 0.6);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-item-card:hover {
|
|
|
|
|
|
border-color: rgba(59, 130, 246, 0.5);
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
|
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.95);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-item-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-item-info {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-item-name {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
|
margin: 0 0 4px 0;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-item-code {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-family: 'Courier New', monospace;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-item-quantity {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
margin-left: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-item-price,
|
|
|
|
|
|
.package-item-total {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-item-price:last-child,
|
|
|
|
|
|
.package-item-total:last-child {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.price-label,
|
|
|
|
|
|
.total-label {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.price-value {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.total-value {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
color: #dc2626;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Tab样式 */
|
|
|
|
|
|
.content-tabs {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-tabs :deep(.el-tabs__header) {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
background: linear-gradient(135deg, #464daa 0%, #a1a6f6 100%);
|
|
|
|
|
|
border-radius: 12px 12px 0 0;
|
|
|
|
|
|
padding: 0 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-tabs :deep(.el-tabs__nav-wrap) {
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-tabs :deep(.el-tabs__nav) {
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-tabs :deep(.el-tabs__item) {
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.9);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
padding: 16px 24px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-tabs :deep(.el-tabs__item:hover) {
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.15);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-tabs :deep(.el-tabs__item.is-active) {
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.25);
|
|
|
|
|
|
border-bottom: 2px solid #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-tabs :deep(.el-tabs__active-bar) {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-tabs :deep(.el-tabs__content) {
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
border-radius: 0 0 12px 12px;
|
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
|
border-top: none;
|
|
|
|
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-content {
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
min-height: 400px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content {
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
color: #374151;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(h1) {
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
margin: 24px 0 16px 0;
|
|
|
|
|
|
padding-bottom: 8px;
|
|
|
|
|
|
border-bottom: 2px solid #e5e7eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(h2) {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
margin: 20px 0 12px 0;
|
|
|
|
|
|
padding-bottom: 6px;
|
|
|
|
|
|
border-bottom: 1px solid #e5e7eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(h3) {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #374151;
|
|
|
|
|
|
margin: 16px 0 8px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(h4) {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #374151;
|
|
|
|
|
|
margin: 12px 0 6px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(p) {
|
|
|
|
|
|
margin: 12px 0;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(table) {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(th) {
|
|
|
|
|
|
background: linear-gradient(135deg, #7179e6 0%, #8b91f0 100%);
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(td) {
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
border-bottom: 1px solid #e5e7eb;
|
|
|
|
|
|
border-right: 1px solid #e5e7eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(td:last-child) {
|
|
|
|
|
|
border-right: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(tr:last-child td) {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(tr:nth-child(even)) {
|
|
|
|
|
|
background-color: #f9fafb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(code) {
|
|
|
|
|
|
background-color: #f3f4f6;
|
|
|
|
|
|
color: #dc2626;
|
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(pre) {
|
|
|
|
|
|
background: #1f2937;
|
|
|
|
|
|
color: #f9fafb;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(pre code) {
|
|
|
|
|
|
background-color: transparent;
|
|
|
|
|
|
color: #f9fafb;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(ul),
|
|
|
|
|
|
.doc-content :deep(ol) {
|
|
|
|
|
|
margin: 12px 0;
|
|
|
|
|
|
padding-left: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(li) {
|
|
|
|
|
|
margin: 6px 0;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(blockquote) {
|
|
|
|
|
|
border-left: 4px solid #667eea;
|
|
|
|
|
|
margin: 16px 0;
|
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
|
border-radius: 0 8px 8px 0;
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 折叠功能样式 */
|
|
|
|
|
|
.doc-content :deep(.collapse-toggle) {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: #667eea;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
transition: transform 0.2s ease;
|
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(.collapse-toggle:hover) {
|
|
|
|
|
|
color: #4f46e5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(.collapse-toggle .collapse-icon) {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
transition: transform 0.2s ease;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
width: 16px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(h2 .collapse-toggle) {
|
|
|
|
|
|
margin-right: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(.collapsible-content) {
|
|
|
|
|
|
margin-left: 24px;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
padding-left: 16px;
|
|
|
|
|
|
border-left: 2px solid #e5e7eb;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(.code-collapse-toggle) {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
background: #f3f4f6;
|
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
margin: 12px 0 8px 0;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #374151;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(.code-collapse-toggle:hover) {
|
|
|
|
|
|
background: #e5e7eb;
|
|
|
|
|
|
border-color: #d1d5db;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(.code-collapse-toggle .collapse-icon) {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #667eea;
|
|
|
|
|
|
transition: transform 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(.code-collapse-toggle .collapse-text) {
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Tab 标签下载按钮样式 */
|
|
|
|
|
|
.tab-content-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.download-btn {
|
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
|
min-height: auto;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.download-btn:hover {
|
|
|
|
|
|
color: #66b1ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.no-content {
|
|
|
|
|
|
padding: 60px 0;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
background: #f9fafb;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
border: 2px dashed #d1d5db;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 请求URL样式 */
|
|
|
|
|
|
.request-url-section {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.request-url-title {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
padding-bottom: 8px;
|
|
|
|
|
|
border-bottom: 1px solid #e5e7eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.request-url-content {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.request-method {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.request-url {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #374151;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.copy-btn {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
background-color: #e0e7ff;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
color: #4f46e5;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background-color 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.copy-btn:hover {
|
|
|
|
|
|
background-color: #d1d5db;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.copy-btn :deep(.el-icon) {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 时间戳备注样式 */
|
|
|
|
|
|
.timestamp-note {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
padding-top: 16px;
|
|
|
|
|
|
border-top: 1px dashed #e5e7eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 响应式设计 */
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.detail-section {
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-value {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-value.price {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-items-grid {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-item-card {
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-item-name {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-item-price,
|
|
|
|
|
|
.package-item-total {
|
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.price-value,
|
|
|
|
|
|
.total-value {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-tabs :deep(.el-tabs__header) {
|
|
|
|
|
|
padding: 0 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-tabs :deep(.el-tabs__item) {
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-content {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
min-height: 300px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(h1) {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(h2) {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(h3) {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(table) {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-content :deep(th),
|
|
|
|
|
|
.doc-content :deep(td) {
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 请求URL移动端样式 */
|
|
|
|
|
|
.request-url-section {
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.request-url-title {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.request-url-content {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.request-url {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
padding: 6px 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.copy-btn {
|
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.copy-btn :deep(.el-icon) {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|