Commit 75a5a491 by renyizhao

添加api应用使用说明

parent 36bd014c
...@@ -65,9 +65,10 @@ ...@@ -65,9 +65,10 @@
:style="{ background: 'url(' + i.homeImage + ')',backgroundSize: 'cover' }"> :style="{ background: 'url(' + i.homeImage + ')',backgroundSize: 'cover' }">
<div class="item"><h3 class="title">{{ i.title }}</h3> <div class="item"><h3 class="title">{{ i.title }}</h3>
<p class="desc" v-html="i.description"></p> <p class="desc" v-html="i.description"></p>
<p class="info flex-justify-end" @click="openAssembly(i)"><span class="btn">{{ <p class="info flex-justify-end" style="height: 52px" @click="openAssembly(i)">
i.information <span class="btn">{{ i.information }} </span>
}} </span></p></div> </p>
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -112,19 +112,109 @@ getDetail() ...@@ -112,19 +112,109 @@ getDetail()
color: #303233; color: #303233;
font-size: 16px; font-size: 16px;
line-height: 1.8; line-height: 1.8;
word-break: break-word;
// 段落
p { p {
margin: 0 0 12px; margin: 0 0 12px;
} }
// 标题(wangEditor 工具栏的"标题1~6"输出真实 h1~h6
h1, h2, h3, h4, h5, h6 {
font-weight: bold;
color: #303233;
line-height: 1.4;
margin: 24px 0 12px;
}
h1 { font-size: 28px; }
h2 { font-size: 24px; }
h3 { font-size: 20px; }
h4 { font-size: 18px; }
h5 { font-size: 16px; }
h6 { font-size: 14px; }
// 强调
strong { font-weight: bold; }
em { font-style: italic; }
u { text-decoration: underline; }
s, del { text-decoration: line-through; }
// 列表
ul, ol {
margin: 0 0 12px;
padding-left: 24px;
li { margin-bottom: 4px; }
}
ul li { list-style: disc; }
ol li { list-style: decimal; }
// 引用
blockquote {
margin: 12px 0;
padding: 8px 16px;
border-left: 4px solid #2E77E3;
background-color: #f5f7fa;
color: #626566;
}
// 代码块 / 行内代码
pre {
margin: 12px 0;
padding: 12px 16px;
background-color: #f5f7fa;
border-radius: 4px;
font-family: Consolas, Monaco, "Courier New", monospace;
font-size: 14px;
overflow-x: auto;
code {
background: transparent;
padding: 0;
font-size: inherit;
}
}
code {
background-color: #f5f7fa;
padding: 2px 6px;
border-radius: 3px;
font-family: Consolas, Monaco, "Courier New", monospace;
font-size: 14px;
color: #c7254e;
}
// 链接
a {
color: #2E77E3;
text-decoration: none;
&:hover { text-decoration: underline; }
}
// 图片
img { img {
height: auto; height: auto;
max-width: 100%; max-width: 100%;
} }
// 表格
table { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin: 12px 0;
th, td {
border: 1px solid #dcdfe6;
padding: 8px 12px;
text-align: left;
}
th {
background-color: #f5f7fa;
font-weight: bold;
}
}
// 分割线
hr {
border: none;
border-top: 1px solid #dcdfe6;
margin: 20px 0;
} }
} }
......
...@@ -107,6 +107,7 @@ ...@@ -107,6 +107,7 @@
<el-tabs v-model="mainTab" class="main-tabs"> <el-tabs v-model="mainTab" class="main-tabs">
<el-tab-pane label="应用说明" name="detail"></el-tab-pane> <el-tab-pane label="应用说明" name="detail"></el-tab-pane>
<el-tab-pane label="接口文档" name="doc"></el-tab-pane> <el-tab-pane label="接口文档" name="doc"></el-tab-pane>
<el-tab-pane label="使用说明" name="usage"></el-tab-pane>
</el-tabs> </el-tabs>
<div class="tab-content-area"> <div class="tab-content-area">
...@@ -150,6 +151,13 @@ ...@@ -150,6 +151,13 @@
<!-- 接口文档 HTML --> <!-- 接口文档 HTML -->
<div class="doc-content" v-html="productData.doc"></div> <div class="doc-content" v-html="productData.doc"></div>
</div> </div>
<!-- 使用说明 tab (通用) -->
<UsageGuide
v-else-if="mainTab === 'usage'"
:appName="productData.name"
:apiEndPoints="productData.apiEndPoints"
/>
</div> </div>
</div> </div>
...@@ -266,6 +274,7 @@ import { OfficeBuilding, Phone } from '@element-plus/icons-vue' ...@@ -266,6 +274,7 @@ import { OfficeBuilding, Phone } from '@element-plus/icons-vue'
import QRCode from "qrcode"; import QRCode from "qrcode";
import {PayOrderStatusEnum} from "@/utils/constants.js"; import {PayOrderStatusEnum} from "@/utils/constants.js";
import request from "@/utils/request.js"; import request from "@/utils/request.js";
import UsageGuide from "./components/UsageGuide.vue";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
......
<template>
<div class="usage-guide main-content">
<!-- 顶部概览 -->
<section class="section">
<h2 class="section-title">一、概述</h2>
<p class="paragraph">本使用说明适用于普惠平台「API 服务市场」中<span class="hl-bold">所有</span>应用,介绍通用的鉴权、签名与调用流程。文档中出现的接口地址、请求 / 响应字段以本应用「<span class="hl-bold">接口文档</span>」Tab 中公布的内容为准,本说明只描述与具体应用无关的<span class="hl-bold">通用规则</span></p>
</section>
<!-- 当前应用调用地址 -->
<section v-if="apiEndPoints && apiEndPoints.length" class="section">
<h2 class="section-title">二、本应用接口地址</h2>
<p class="paragraph">本应用对外开放以下接口,具体参数详见「<span class="hl-bold">接口文档</span>」Tab:</p>
<el-table :data="apiEndPoints" border stripe size="default" class="endpoint-table">
<el-table-column prop="name" label="接口名称" min-width="180" />
<el-table-column label="请求方法" width="100" align="center">
<template #default="scope">
<el-tag size="small" :type="methodTagType(scope.row.method)">{{ scope.row.method }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="path" label="请求路径" min-width="360">
<template #default="scope">
<code class="path-code">{{ scope.row.path }}</code>
</template>
</el-table-column>
</el-table>
<p class="paragraph tip">实际请求时需要在路径前拼接网关前缀 <code>{{ gatewayPrefix }}</code>,完整地址形如:</p>
<p class="paragraph tip"><code class="full-url">{{ gatewayPrefix }}{{ apiEndPoints[0].path }}</code></p>
</section>
<!-- 准备工作 -->
<section class="section">
<h2 class="section-title">{{ hasEndpoint ? '三' : '二' }}、准备工作</h2>
<ol class="ordered">
<li><span class="hl-bold">注册并登录普惠平台</span>:在客户端完成账号注册、登录;部分应用要求完成企业认证后才能购买。</li>
<li><span class="hl-bold">购买资源包</span>:在「API 服务市场」找到 <span v-if="appName" class="hl">{{ appName }}</span><span v-else>目标应用</span>,进入详情页选择合适的资源包规格,完成支付。资源包购买后立即生效,有效期内可调用,过期后未使用次数作废。</li>
<li>
<span class="hl-bold">获取调用凭据</span>:购买完成后,前往「控制台 → 应用凭据」获取本应用对应的 <code>appId</code><code>appSecret</code>
<ul class="unordered">
<li><code>appId</code>:应用唯一标识,公开传输。</li>
<li><code>appSecret</code>:调用签名密钥,<span class="hl-bold">仅在服务端保存</span>,严禁写入前端代码、客户端仓库或公开示例。</li>
<li>忘记凭据或怀疑泄漏时,请在凭据管理页重置 <code>appSecret</code>,重置后旧密钥立即失效。</li>
</ul>
</li>
</ol>
</section>
<!-- 签名机制 -->
<section class="section">
<h2 class="section-title">{{ hasEndpoint ? '四' : '三' }}、通用签名机制</h2>
<p class="paragraph">平台采用 <code>appId / timestamp / nonce / sign</code> 四 Header 签名。</p>
<h3 class="sub-title">1. Header 说明</h3>
<el-table :data="headerRows" border size="default" class="kv-table">
<el-table-column prop="key" label="Header" width="180" />
<el-table-column prop="required" label="必填" width="80" align="center" />
<el-table-column prop="desc" label="说明" min-width="360" />
</el-table>
<h3 class="sub-title">2. 拼串 + SHA256 规则</h3>
<p class="paragraph">将以下三个<span class="hl-bold">Header 中已存在的明文值</span> + <span class="hl-bold">本地的 <code>appSecret</code></span>,按<span class="hl-bold">键名字典升序</span>拼成查询串:</p>
<pre class="code-block">appId=&lt;appId&gt;&amp;nonce=&lt;nonce&gt;&amp;timestamp=&lt;timestamp&gt;&amp;appSecret=&lt;appSecret&gt;</pre>
<p class="paragraph">对该字符串做 UTF-8 编码后计算 SHA256,结果取 32 位小写十六进制,即为 <code>sign</code> Header 的值。</p>
<ul class="unordered">
<li>顺序必须严格按字母升序 <code>appId → nonce → timestamp</code>(不要按写入 Header 的顺序)。</li>
<li>中间用 <code>&amp;</code> 分隔,<code>=</code> 直接连接键值,<span class="hl-bold">不要对值做 URLEncode</span></li>
<li>服务端的 <code>appSecret</code> 仅参与哈希比对,客户端的 <code>appSecret</code> 也不出现在 Header / Body / URL 上。</li>
</ul>
<h3 class="sub-title">3. ⚠️ 重要:appSecret 不出网</h3>
<p class="paragraph warning">真正在 Header 里传输的只有 4 个字段:<code>appId / timestamp / nonce / sign</code><code>appSecret</code> 只在你本地参与 SHA256 计算,服务端拿到 <code>appId</code> 后会从凭据库里查出对应的 <code>appSecret</code>,用同样的拼串规则算一次哈希,只有两个哈希值完全一致才放行。抓包、网络嗅探、网关日志都<span class="hl-bold">不会泄露 appSecret</span></p>
</section>
<!-- 通用调用流程 -->
<section class="section">
<h2 class="section-title">{{ hasEndpoint ? '五' : '四' }}、通用调用流程</h2>
<ol class="ordered">
<li>从控制台拿到 <code>appId</code><code>appSecret</code></li>
<li>生成 <code>timestamp</code>(Unix 毫秒)与 <code>nonce</code>(12 位以上随机数,2 分钟内不可重复)。</li>
<li><code>appId &amp; nonce &amp; timestamp &amp; appSecret</code> 顺序拼串 → SHA256 → 得到 <code>sign</code></li>
<li><code>appId / timestamp / nonce / sign</code> 四个值分别放入对应 Header,按业务要求发送请求(<code>POST</code> / <code>GET</code> / <code>multipart</code> 等)。</li>
<li>解析响应:<code>code = 0</code> 表示成功,资源包扣 1 次;非 0 表示失败,不扣费。</li>
</ol>
</section>
<!-- 通用错误码 -->
<section class="section">
<h2 class="section-title">{{ hasEndpoint ? '六' : '五' }}、通用错误码</h2>
<el-table :data="errorCodeRows" border size="default" class="kv-table">
<el-table-column prop="code" label="错误码" width="160" />
<el-table-column prop="msg" label="含义" min-width="200" />
<el-table-column prop="action" label="处理建议" min-width="320" />
</el-table>
</section>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface ApiEndpoint {
id?: number | string
name?: string
method?: string
path?: string
industryApplications?: any[]
}
const props = defineProps<{
appName?: string
apiEndPoints?: ApiEndpoint[]
gatewayPrefix?: string
}>()
const gatewayPrefix = computed(() => props.gatewayPrefix || '/open-api/external')
const hasEndpoint = computed(() => Array.isArray(props.apiEndPoints) && props.apiEndPoints.length > 0)
const headerRows = [
{ key: 'appId', required: '是', desc: '资源包对应的应用 ID,明文传输' },
{ key: 'timestamp', required: '是', desc: '请求时刻的 Unix 毫秒时间戳,服务端允许 ±60 秒偏差' },
{ key: 'nonce', required: '是', desc: '随机字符串,长度 ≥ 10 位(建议 12 位),单次请求唯一,2 分钟内不可重复' },
{ key: 'sign', required: '是', desc: '32 位小写 SHA256 签名(hex),由 appId / nonce / timestamp / appSecret 拼串后哈希得到' },
{ key: 'appSecret', required: '—', desc: '不出现在 Header / Body / URL,仅在本地参与 SHA256 运算' },
]
const errorCodeRows = [
{ code: '0', msg: '成功', action: '—' },
{ code: '1010001021', msg: 'Header 异常', action: '检查四个签名头是否齐全,nonce 长度是否 ≥ 10,sign 是否与本地重算一致' },
{ code: '1010001024', msg: 'timestamp 异常', action: '校准本机时间(建议 NTP 同步),重新生成 timestamp 和 sign' },
{ code: '1010001025', msg: '无效的 appId', action: '请确认 appId' },
{ code: '1010001017', msg: '接口不存在', action: '检查请求地址,确认路径与方法是否拼写正确' },
{ code: '1010001022', msg: '未订阅该接口', action: '在 API 服务市场购买对应的资源包' },
{ code: '1010001023', msg: '接口调用额度不足或已过期', action: '资源包次数用完或已过有效期,需重新购买' },
{ code: '1010001019', msg: '用户密钥信息不存在', action: '凭据可能已被重置,前往控制台重新获取 appSecret' },
]
function methodTagType(method?: string) {
switch ((method || '').toUpperCase()) {
case 'GET': return 'success'
case 'POST': return 'primary'
case 'PUT': return 'warning'
case 'DELETE': return 'danger'
default: return 'info'
}
}
</script>
<style scoped lang="scss">
.usage-guide {
font-size: 14px;
line-height: 1.8;
color: #303233;
// 显式声明,避免被父级 flex 布局影响行内元素的换行点
white-space: normal;
// CJK 文本 keep-all:禁止在任意两个汉字之间断行,只允许在空格 / 标点处断行
// 配合 overflow-wrap 处理超长 URL / 英文单词
word-break: keep-all;
overflow-wrap: break-word;
}
.usage-guide :deep(strong),
.usage-guide strong,
.hl-bold {
display: inline;
font-weight: 700;
font-style: normal;
color: var(--el-color-primary);
}
// 局部覆盖全局 .app-wrapper :after 残留规则
// src/assets/styles/index-CMeKdScn.scss 中有:
// .app-wrapper :after { content: ""; display: table; clear: both; }
// 该选择器有空格 = 匹配 .app-wrapper 任意后代,会把 span.hl-bold
// 行内元素的 ::after 强制变成 display: table,触发意外换行
// .usage-guide 作用域内重置,不影响其它页面
.usage-guide :deep(*)::after {
content: none !important;
display: inline !important;
clear: none !important;
}
.section {
margin-bottom: 28px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #1f2329;
margin: 0 0 12px;
padding-left: 10px;
border-left: 4px solid var(--el-color-primary);
}
.sub-title {
font-size: 15px;
font-weight: 600;
color: #303233;
margin: 18px 0 8px;
}
.paragraph {
margin: 6px 0;
color: #4a4f57;
white-space: normal;
// 关键:CJK 不在汉字之间断行,strong / code 不会变成"断行点"
word-break: keep-all;
overflow-wrap: break-word;
}
.paragraph.warning {
background: #fff7e6;
border: 1px solid #ffd591;
color: #874d00;
padding: 10px 14px;
border-radius: 6px;
}
.paragraph.tip {
margin-top: 12px;
color: #5a6573;
font-size: 13px;
}
.ordered,
.unordered {
margin: 6px 0 6px 20px;
padding: 0;
}
.ordered li,
.unordered li {
margin-bottom: 4px;
color: #4a4f57;
}
.unordered {
list-style: disc;
}
.hl {
color: var(--el-color-primary);
font-weight: 600;
}
code {
background: #f3f4f6;
padding: 1px 6px;
border-radius: 4px;
font-size: 13px;
color: #c7254e;
font-family: Consolas, Menlo, monospace;
}
.code-block {
background: #1e1e1e;
color: #d4d4d4;
padding: 12px 16px;
border-radius: 6px;
font-family: Consolas, Menlo, monospace;
font-size: 13px;
line-height: 1.6;
margin: 8px 0;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
.endpoint-table,
.kv-table {
margin: 8px 0 4px;
}
.path-code {
background: #f3f4f6;
padding: 2px 8px;
border-radius: 4px;
color: #303233;
font-size: 13px;
font-family: Consolas, Menlo, monospace;
}
.full-url {
display: inline-block;
margin-top: 4px;
background: #ecf5ff;
color: #256dff;
border: 1px solid #d6e8ff;
padding: 4px 10px;
border-radius: 4px;
font-family: Consolas, Menlo, monospace;
font-size: 13px;
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment