初始化
This commit is contained in:
945
mokee-gateway-web/_backup_20260613_widget/IntegrationDoc.vue
Normal file
945
mokee-gateway-web/_backup_20260613_widget/IntegrationDoc.vue
Normal file
@@ -0,0 +1,945 @@
|
||||
<!--
|
||||
系统接入文档页面(公开访问,通过 docToken 路由访问)
|
||||
|
||||
路由:/docs/:token
|
||||
页面路由在 router 中配置为无需登录即可访问(公开文档页)
|
||||
|
||||
内容包含 8 个章节:
|
||||
1. 当前系统配置 — 展示系统基本信息(编码、名称、前后端地址)
|
||||
2. 架构概览 — ASCII 架构图 + 5 步流程说明 + 3 条关键规则
|
||||
3. 前端接入 — 完整的 Axios 实例和路由守卫复制即用代码
|
||||
4. 后端接入 — Java/Python/Go 三种语言的 UserContext 中间件示例
|
||||
5. API 接口参考 — GET 用户信息、GET 菜单路由、POST 退出登录
|
||||
6. 数据库初始化 — 生成当前系统的菜单和角色初始化 SQL
|
||||
7. 创建网关管理员账号 — 生成 gateway-app-admin 账号的 SQL
|
||||
8. 对接测试清单 — 6 项验收检查
|
||||
|
||||
数据来源:
|
||||
- 系统信息:GET /zgapi/v1/admin/system/doc/:token(公开接口,无需登录)
|
||||
|
||||
SQL 生成逻辑:
|
||||
- menuSql:根据当前系统的 systemCode 生成菜单、角色、角色-菜单关联的 INSERT 语句
|
||||
- adminSql:生成应用系统管理员账号、系统绑定、角色分配的 INSERT 语句
|
||||
密码使用 BCrypt 密文($2a$10$...),账号格式为 admin_{systemCode}
|
||||
角色编码固定为 gateway-app-admin(仅能看到自己绑定系统的数据)
|
||||
所有 INSERT 使用 NOT EXISTS 子查询防止重复执行
|
||||
-->
|
||||
<template>
|
||||
<div class="doc-page">
|
||||
<div class="doc-container">
|
||||
<!-- ====== 页头 ====== -->
|
||||
<header class="doc-hero">
|
||||
<div class="doc-hero-left">
|
||||
<svg class="doc-logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
|
||||
</svg>
|
||||
<div>
|
||||
<h1>{{ sys?.systemName || '系统' }} — 接入文档</h1>
|
||||
<p>Mokee Gateway 统一登录网关 · 系统对接完整指南</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-hero-right">
|
||||
<el-tag size="large" type="primary" effect="dark" round>系统编码: {{ sys?.systemCode }}</el-tag>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ====== 一、系统信息 ====== -->
|
||||
<section class="doc-section">
|
||||
<h2 class="section-h2"><span class="s-num">1</span>当前系统配置</h2>
|
||||
<el-descriptions :column="2" border size="large">
|
||||
<el-descriptions-item label="系统编码"><code class="inline-code">{{ sys?.systemCode }}</code></el-descriptions-item>
|
||||
<el-descriptions-item label="系统名称">{{ sys?.systemName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="前端地址"><code class="inline-code">{{ sys?.frontUrl }}</code></el-descriptions-item>
|
||||
<el-descriptions-item label="后端地址"><code class="inline-code">{{ sys?.backendUrl }}</code></el-descriptions-item>
|
||||
<el-descriptions-item label="系统描述" :span="2">{{ sys?.description || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</section>
|
||||
|
||||
<!-- ====== 二、架构概览 ====== -->
|
||||
<section class="doc-section">
|
||||
<h2 class="section-h2"><span class="s-num">2</span>架构概览</h2>
|
||||
<div class="arch-diagram">
|
||||
<pre class="arch-text">┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
│ 业务前端 │────▶│ Mokee Gateway │────▶│ 业务后端 │
|
||||
│ (Vue/React) │ │ gw.server.zgitm.com │ │ (任意语言) │
|
||||
│ │ │ │ │ │
|
||||
│ 携带 Token │ │ 验证 Token │ │ 从请求头读取: │
|
||||
│ + SystemCode │ │ 识别系统编码 │ │ X-User-Id │
|
||||
│ │ │ 转发请求 │ │ X-User-Name │
|
||||
│ │ │ 注入用户信息头 │ │ X-System-Code │
|
||||
└──────────────┘ └──────────────────┘ └──────────────────┘</pre>
|
||||
</div>
|
||||
<div class="flow-steps">
|
||||
<div class="flow-step"><b>①</b> 用户访问业务前端 → 无 Token → 跳转统一登录页</div>
|
||||
<div class="flow-step"><b>②</b> 登录成功 → 获得 JWT Token → 回跳到业务前端</div>
|
||||
<div class="flow-step"><b>③</b> 业务前端所有 API 请求 → 网关 (携带 Authorization + X-System-Code)</div>
|
||||
<div class="flow-step"><b>④</b> 网关验证 Token → 注入用户信息请求头 → 转发到业务后端</div>
|
||||
<div class="flow-step"><b>⑤</b> 业务后端从请求头读取用户信息 → 执行业务逻辑</div>
|
||||
</div>
|
||||
|
||||
<div class="arch-warnings">
|
||||
<div class="warn-item">
|
||||
<b>🔴 关键规则一:前端只调网关</b>
|
||||
<p>业务前端的 <code>baseURL</code> 必须配置为 <b>网关地址</b>(如 <code>{{ gatewayUrl }}</code>),<b>绝对不能</b>直接调用业务后端。网关根据请求头 <code>X-System-Code</code> 自动识别并转发到对应后端。</p>
|
||||
</div>
|
||||
<div class="warn-item">
|
||||
<b>🔴 关键规则二:后端必须开跨域 (CORS)</b>
|
||||
<p>业务后端需要允许来自网关域名的跨域请求。网关转发请求时 origin 是浏览器地址,不是网关地址。</p>
|
||||
<p><b>Java Spring Boot 配置:</b></p>
|
||||
<div class="code-box"><pre><code>@Configuration
|
||||
public class CorsConfig implements WebMvcConfigurer {
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedMethods("*")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true);
|
||||
}
|
||||
}</code></pre></div>
|
||||
</div>
|
||||
<div class="warn-item">
|
||||
<b>🔴 关键规则三:业务后端不再做登录/鉴权</b>
|
||||
<p>Token 验证由网关完成。业务后端只需从请求头读取用户信息,不需要再验证 Token、不需要查询用户表。</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ====== 三、前端接入 ====== -->
|
||||
<section class="doc-section">
|
||||
<h2 class="section-h2"><span class="s-num">3</span>前端接入 (完整代码)</h2>
|
||||
|
||||
<h3 class="section-h3">3.1 安装依赖</h3>
|
||||
<div class="code-box">
|
||||
<div class="code-label">Shell</div>
|
||||
<pre><code>npm install axios</code></pre>
|
||||
</div>
|
||||
|
||||
<h3 class="section-h3">3.2 环境变量 (.env)</h3>
|
||||
<div class="code-box">
|
||||
<div class="code-label">.env</div>
|
||||
<pre><code>VITE_GATEWAY_URL = {{ gatewayUrl }}
|
||||
VITE_LOGIN_URL = {{ loginUrl }}
|
||||
VITE_SYSTEM_CODE = {{ sys?.systemCode || 'YOUR_CODE' }}</code></pre>
|
||||
</div>
|
||||
|
||||
<h3 class="section-h3">3.3 Axios 实例 (src/utils/request.ts)</h3>
|
||||
<div class="code-box">
|
||||
<div class="code-label">TypeScript</div>
|
||||
<pre><code>import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const SYSTEM_CODE = import.meta.env.VITE_SYSTEM_CODE
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: import.meta.env.VITE_GATEWAY_URL,
|
||||
timeout: 30000,
|
||||
})
|
||||
|
||||
// 从 Cookie 读取 Token(登录后网关写入的跨域 Cookie)
|
||||
function getToken(): string | null {
|
||||
const match = document.cookie.match(/(?:^|;\\s*)token=([^;]*)/)
|
||||
return match ? decodeURIComponent(match[1]) : null
|
||||
}
|
||||
|
||||
// 请求拦截器:携带 Token 和系统编码
|
||||
request.interceptors.request.use(config => {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
// 关键:网关根据此头识别业务系统
|
||||
config.headers['X-System-Code'] = SYSTEM_CODE
|
||||
return config
|
||||
})
|
||||
|
||||
// 响应拦截器:401 自动跳转登录
|
||||
request.interceptors.response.use(
|
||||
res => res.data.code === 200 ? res.data : Promise.reject(res.data),
|
||||
err => {
|
||||
if (err.response?.status === 401) {
|
||||
window.location.href = `${import.meta.env.VITE_LOGIN_URL}?systemCode=${SYSTEM_CODE}`
|
||||
}
|
||||
return Promise.reject(err)
|
||||
}
|
||||
)
|
||||
|
||||
export default request</code></pre>
|
||||
</div>
|
||||
|
||||
<h3 class="section-h3">3.4 路由守卫 (src/router/index.ts)</h3>
|
||||
<div class="code-box">
|
||||
<div class="code-label">TypeScript</div>
|
||||
<pre><code>import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const SYSTEM_CODE = import.meta.env.VITE_SYSTEM_CODE
|
||||
const LOGIN_URL = import.meta.env.VITE_LOGIN_URL
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [ /* 你的路由 */ ],
|
||||
})
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = document.cookie.match(/(?:^|;\\s*)token=([^;]*)/)?.[1]
|
||||
if (!token && to.path !== '/login') {
|
||||
// 无 Token → 跳统一登录页
|
||||
window.location.href = `${LOGIN_URL}?systemCode=${SYSTEM_CODE}`
|
||||
return
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
export default router</code></pre>
|
||||
</div>
|
||||
|
||||
<h3 class="section-h3">3.5 登录成功回调处理</h3>
|
||||
<p class="text-p">
|
||||
用户在统一登录页登录成功后,网关将 Token 写入 <b>Cookie</b>(domain: <code>.zgitm.com</code>,同域子站共享),
|
||||
然后跳回业务前端的地址(即 <code>VITE_LOGIN_URL</code> 配置的地址)。
|
||||
</p>
|
||||
<p class="text-p">
|
||||
业务前端<b>无需自行实现登录接口</b>,只需从 Cookie 中读取 Token 即可。
|
||||
</p>
|
||||
<div class="code-box">
|
||||
<div class="code-label">TypeScript — 读取 Cookie 中的 Token</div>
|
||||
<pre><code>// 从 Cookie 读取 Token(登录后网关自动写入的跨域 Cookie)
|
||||
function getToken(): string | null {
|
||||
const match = document.cookie.match(/(?:^|;\\s*)token=([^;]*)/)
|
||||
return match ? decodeURIComponent(match[1]) : null
|
||||
}
|
||||
|
||||
// App.vue onMounted 或 main.ts
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
// 用户已登录,正常渲染应用
|
||||
// 如需获取用户信息,调用网关 API:
|
||||
const res = await request.get('/zgapi/v1/auth/user/info')
|
||||
console.log('当前用户:', res.data)
|
||||
} else {
|
||||
// 无 Token → 跳统一登录页
|
||||
window.location.href = `${import.meta.env.VITE_LOGIN_URL}?systemCode=${SYSTEM_CODE}`
|
||||
}</code></pre>
|
||||
</div>
|
||||
<div class="warn-item" style="margin-top:16px">
|
||||
<b>💡 Token 存储说明</b>
|
||||
<p>Token 存在 Cookie 中(而非 localStorage),因为 Cookie 支持跨子域共享(<code>.zgitm.com</code>),
|
||||
这样统一登录、管理后台、文件服务等不同子站都能读取到同一个 Token。<br>
|
||||
Cookie 有效期 2 小时,与 Token 过期时间一致。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ====== 四、后端接入 ====== -->
|
||||
<section class="doc-section">
|
||||
<h2 class="section-h2"><span class="s-num">4</span>后端接入 (完整代码)</h2>
|
||||
|
||||
<h3 class="section-h3">4.1 网关转发到后端时注入的请求头</h3>
|
||||
<el-table :data="headerTable" border stripe size="large" style="width:100%">
|
||||
<el-table-column prop="name" label="请求头名称" width="200" />
|
||||
<el-table-column prop="type" label="数据类型" width="120" />
|
||||
<el-table-column prop="desc" label="说明" />
|
||||
</el-table>
|
||||
<p class="text-p" style="margin-top:16px"><b>重要:</b>这些请求头由网关注入,业务后端可以信任这些值。不需要再验证 Token 或查询用户信息。</p>
|
||||
|
||||
<h3 class="section-h3">4.2 Java — UserContext 工具类</h3>
|
||||
<p class="text-p">用 ThreadLocal 缓存请求头,业务代码零侵入获取用户信息。</p>
|
||||
<div class="code-box">
|
||||
<div class="code-label">Java</div>
|
||||
<pre><code>// ====== UserContext.java ======
|
||||
import java.util.*;
|
||||
|
||||
public class UserContext {
|
||||
private static final ThreadLocal<Map<String, String>> CTX = new ThreadLocal<>();
|
||||
|
||||
public static void set(Map<String, String> user) { CTX.set(user); }
|
||||
public static Map<String, String> get() { return CTX.get(); }
|
||||
public static void clear() { CTX.remove(); }
|
||||
|
||||
// 快捷方法
|
||||
public static String getUserId() { return get().get("X-User-Id"); }
|
||||
public static String getUserName() { return get().get("X-User-Name"); }
|
||||
public static String getUserAccount(){ return get().get("X-User-Account"); }
|
||||
public static String getSystemCode() { return get().get("X-System-Code"); }
|
||||
public static String getSystemId() { return get().get("X-System-Id"); }
|
||||
}
|
||||
|
||||
// ====== UserContextFilter.java ======
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
@Component
|
||||
public class UserContextFilter implements Filter {
|
||||
private static final String[] HEADERS = {
|
||||
"X-User-Id", "X-User-Name", "X-User-Account",
|
||||
"X-User-Email", "X-User-Post", "X-User-Roles",
|
||||
"X-System-Id", "X-System-Code", "X-System-Name"
|
||||
};
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest request = (HttpServletRequest) req;
|
||||
Map<String, String> user = new HashMap<>();
|
||||
for (String h : HEADERS) {
|
||||
String v = request.getHeader(h);
|
||||
if (v != null) user.put(h, v);
|
||||
}
|
||||
UserContext.set(user);
|
||||
try {
|
||||
chain.doFilter(req, res);
|
||||
} finally {
|
||||
UserContext.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 业务代码中使用 ======
|
||||
@RestController
|
||||
public class OrderController {
|
||||
|
||||
@GetMapping("/api/orders")
|
||||
public List<Order> list() {
|
||||
String userId = UserContext.getUserId(); // "123"
|
||||
String userName = UserContext.getUserName(); // "张三"
|
||||
String systemCode = UserContext.getSystemCode(); // "msg"
|
||||
return orderService.findByUser(userId, systemCode);
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h3 class="section-h3">4.3 Python — Flask 中间件</h3>
|
||||
<div class="code-box">
|
||||
<div class="code-label">Python</div>
|
||||
<pre><code># ====== middleware.py ======
|
||||
from flask import request, g
|
||||
|
||||
USER_HEADERS = [
|
||||
'X-User-Id', 'X-User-Name', 'X-User-Account',
|
||||
'X-User-Email', 'X-User-Post', 'X-User-Roles',
|
||||
'X-System-Id', 'X-System-Code', 'X-System-Name',
|
||||
]
|
||||
|
||||
def user_context_middleware():
|
||||
"""将网关注入的用户信息存到 Flask g 对象"""
|
||||
for h in USER_HEADERS:
|
||||
val = request.headers.get(h)
|
||||
if val:
|
||||
setattr(g, h.replace('-', '_').lower(), val)
|
||||
|
||||
# 注册到 app (Flask)
|
||||
# app.before_request(user_context_middleware)
|
||||
|
||||
# ====== 业务代码中使用 ======
|
||||
from flask import g
|
||||
|
||||
@app.route('/api/orders')
|
||||
def list_orders():
|
||||
user_id = g.x_user_id # "123"
|
||||
user_name = g.x_user_name # "张三"
|
||||
system_code = g.x_system_code # "msg"
|
||||
return order_service.query(user_id, system_code)</code></pre>
|
||||
</div>
|
||||
|
||||
<h3 class="section-h3">4.4 Go — 中间件</h3>
|
||||
<div class="code-box">
|
||||
<div class="code-label">Go</div>
|
||||
<pre><code>// ====== middleware.go ======
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
func UserContext(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
for _, h := range []string{
|
||||
"X-User-Id", "X-User-Name", "X-User-Account",
|
||||
"X-User-Email", "X-User-Roles",
|
||||
"X-System-Id", "X-System-Code",
|
||||
} {
|
||||
if v := r.Header.Get(h); v != "" {
|
||||
ctx = context.WithValue(ctx, contextKey(h), v)
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// 快捷函数
|
||||
func GetUserID(ctx context.Context) string {
|
||||
return ctx.Value(contextKey("X-User-Id")).(string)
|
||||
}
|
||||
func GetSystemCode(ctx context.Context) string {
|
||||
return ctx.Value(contextKey("X-System-Code")).(string)
|
||||
}
|
||||
|
||||
// ====== main.go: 注册中间件 ======
|
||||
// http.Handle("/api/", middleware.UserContext(yourHandler))
|
||||
|
||||
// ====== 业务代码 ======
|
||||
func listOrders(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context()) // "123"
|
||||
systemCode := middleware.GetSystemCode(r.Context()) // "msg"
|
||||
// ...
|
||||
}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ====== 五、API 接口参考 ====== -->
|
||||
<section class="doc-section">
|
||||
<h2 class="section-h2"><span class="s-num">5</span>API 接口参考</h2>
|
||||
|
||||
<h3 class="section-h3">5.1 获取当前用户信息</h3>
|
||||
<div class="api-card">
|
||||
<div class="api-method get">GET</div>
|
||||
<div class="api-path">/zgapi/v1/auth/user/info</div>
|
||||
<div class="api-headers">
|
||||
<b>请求头:</b>Authorization: Bearer {token}
|
||||
</div>
|
||||
<div class="api-response">
|
||||
<b>响应:</b>
|
||||
<div class="code-box"><pre><code>{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"userId": 1,
|
||||
"username": "admin",
|
||||
"realName": "管理员",
|
||||
"email": "admin@zgitm.com",
|
||||
"phone": "13800000000",
|
||||
"avatar": null,
|
||||
"post": "管理员"
|
||||
}
|
||||
}</code></pre></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-h3">5.2 获取系统菜单路由</h3>
|
||||
<div class="api-card">
|
||||
<div class="api-method get">GET</div>
|
||||
<div class="api-path">/zgapi/v1/auth/menu/router?systemCode={{ sys?.systemCode || 'YOUR_CODE' }}</div>
|
||||
<div class="api-headers">
|
||||
<b>请求头:</b>Authorization: Bearer {token}
|
||||
</div>
|
||||
<div class="api-response">
|
||||
<b>响应:</b>树形菜单结构,包含 path / name / component / children / meta(含 title, icon)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-h3">5.3 退出登录</h3>
|
||||
<div class="api-card">
|
||||
<div class="api-method post">POST</div>
|
||||
<div class="api-path">/zgapi/v1/auth/logout</div>
|
||||
<div class="api-headers">
|
||||
<b>请求头:</b>Authorization: Bearer {token}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ====== 六、数据库初始化 ====== -->
|
||||
<section class="doc-section">
|
||||
<h2 class="section-h2"><span class="s-num">6</span>数据库初始化</h2>
|
||||
<p class="text-p">在网关平台的数据库执行以下 SQL,为「{{ sys?.systemName }}」创建默认菜单和角色。</p>
|
||||
<div class="code-box">
|
||||
<div class="code-label">SQL</div>
|
||||
<pre><code>{{ menuSql }}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ====== 七、创建网关管理员账号 ====== -->
|
||||
<section class="doc-section">
|
||||
<h2 class="section-h2"><span class="s-num">7</span>创建网关管理员账号</h2>
|
||||
<p class="text-p">
|
||||
业务系统接入后,系统管理员需要登录网关管理后台维护本系统的菜单和角色。
|
||||
执行以下 SQL 创建一个<b>应用系统管理员</b>账号(角色编码 <code>gateway-app-admin</code>),该管理员只能看到自己绑定系统的数据。
|
||||
</p>
|
||||
<div class="code-box">
|
||||
<div class="code-label">SQL — 创建应用系统管理员</div>
|
||||
<pre><code>{{ adminSql }}</code></pre>
|
||||
</div>
|
||||
<div class="warn-item" style="margin-top:16px">
|
||||
<b>⚠️ 注意</b>
|
||||
<p>密码需用 BCrypt 加密。可使用在线工具或 Java <code>BCrypt.hashpw("your_password", BCrypt.gensalt())</code> 生成密文。<br>
|
||||
如果已创建过用户,跳过步骤 1,只执行步骤 2 和 3 绑定系统和角色即可。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ====== 八、测试清单 ====== -->
|
||||
<section class="doc-section">
|
||||
<h2 class="section-h2"><span class="s-num">8</span>对接测试清单</h2>
|
||||
<div class="checklist">
|
||||
<div v-for="(item, i) in testChecklist" :key="i" class="check-item">
|
||||
<el-icon :size="20" color="var(--color-primary)"><component :is="getIconComponent(item.done ? 'CircleCheckFilled' : 'CircleClose')" /></el-icon>
|
||||
<span>{{ item.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ====== 九、文件对象存储服务 ====== -->
|
||||
<section class="doc-section">
|
||||
<h2 class="section-h2"><span class="s-num">9</span>文件对象存储服务</h2>
|
||||
<p class="text-p">
|
||||
文件上传/下载/删除由独立的<b>文件对象存储服务</b>提供,该服务直接对接网关数据库,上传后文件信息自动出现在文件列表中。
|
||||
</p>
|
||||
|
||||
<el-descriptions :column="2" border size="large" style="margin-bottom:20px">
|
||||
<el-descriptions-item label="服务地址"><code class="inline-code">https://file.wg.zgitm.com</code></el-descriptions-item>
|
||||
<el-descriptions-item label="认证方式">Bearer Token(一次一密,每次操作前重新获取)</el-descriptions-item>
|
||||
<el-descriptions-item label="最大文件">100MB</el-descriptions-item>
|
||||
<el-descriptions-item label="Token 有效期">12 小时</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<h3 class="section-h3">9.1 获取 Token</h3>
|
||||
<div class="api-card">
|
||||
<div class="api-method post">POST</div>
|
||||
<div class="api-path">/api/token</div>
|
||||
<div class="api-headers"><b>Content-Type:</b> application/json;charset=utf-8</div>
|
||||
<p style="margin:8px 0"><b>请求体:</b></p>
|
||||
<div class="code-box"><pre><code>{
|
||||
"systemCode": "{{ sys?.systemCode || 'YOUR_CODE' }}"
|
||||
}</code></pre></div>
|
||||
<p style="margin:8px 0"><b>响应:</b></p>
|
||||
<div class="code-box"><pre><code>{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzM4NCJ9...",
|
||||
"expiresIn": 43200,
|
||||
"systemName": "系统名称"
|
||||
}
|
||||
}</code></pre></div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-h3">9.2 上传文件</h3>
|
||||
<div class="api-card">
|
||||
<div class="api-method post">POST</div>
|
||||
<div class="api-path">/api/upload</div>
|
||||
<div class="api-headers"><b>Authorization:</b> Bearer {token} | <b>Content-Type:</b> multipart/form-data</div>
|
||||
<p style="margin:8px 0"><b>curl 示例:</b></p>
|
||||
<div class="code-box"><pre><code># 获取 Token
|
||||
TOKEN=$(curl -s -X POST https://file.wg.zgitm.com/api/token \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"systemCode":"{{ sys?.systemCode || 'YOUR_CODE' }}"}' | jq -r '.data.token')
|
||||
|
||||
# 上传
|
||||
curl -X POST https://file.wg.zgitm.com/api/upload \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-F "file=@/path/to/file.pdf"</code></pre></div>
|
||||
<p style="margin:8px 0"><b>成功响应:</b></p>
|
||||
<div class="code-box"><pre><code>{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"fileId": "a1b2c3d4e5f67890...",
|
||||
"fileName": "报告.pdf",
|
||||
"fileSize": 1048576,
|
||||
"uploadTime": "2026-06-13T10:30:00"
|
||||
}
|
||||
}</code></pre></div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-h3">9.3 下载文件</h3>
|
||||
<div class="api-card">
|
||||
<div class="api-method get">GET</div>
|
||||
<div class="api-path">/api/download/{fileId}</div>
|
||||
<div class="api-headers"><b>Authorization:</b> Bearer {token}</div>
|
||||
<p style="margin:8px 0">
|
||||
成功返回文件二进制流。<br>
|
||||
<b>Token 两种传递方式:</b><br>
|
||||
① Header:<code>Authorization: Bearer <token></code><br>
|
||||
② Query 参数:<code>/api/download/{fileId}?token=<token></code>(浏览器直链下载时使用)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 class="section-h3">9.4 删除文件</h3>
|
||||
<div class="api-card">
|
||||
<div class="api-method get">GET</div>
|
||||
<div class="api-path">/api/delete/{fileId}</div>
|
||||
<div class="api-headers"><b>Authorization:</b> Bearer {token}</div>
|
||||
<p style="margin:8px 0">删除会同时清理磁盘文件与数据库记录,不可恢复。</p>
|
||||
</div>
|
||||
|
||||
<div class="warn-item" style="margin-top:16px">
|
||||
<b>⚠️ 注意事项</b>
|
||||
<p>1. 每次操作前需重新获取 Token(一次一密)。<br>
|
||||
2. 文件最大 100MB,超时 12 小时。<br>
|
||||
3. 上传成功后文件信息自动写入网关数据库 sys_file 表。<br>
|
||||
4. 删除不可恢复,请谨慎操作。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ====== 十、对外开放 API(邮件发送) ====== -->
|
||||
<section class="doc-section">
|
||||
<h2 class="section-h2"><span class="s-num">10</span>对外开放 API</h2>
|
||||
<p class="text-p">
|
||||
以下接口通过<b>一次性 Token</b> 认证,Token 获取后 12 小时内有效,<b>每次调用业务接口即消费</b>。
|
||||
</p>
|
||||
|
||||
<h3 class="section-h3">10.1 获取 Token</h3>
|
||||
<div class="api-card">
|
||||
<div class="api-method post">POST</div>
|
||||
<div class="api-path">/zgapi/v1/open/token</div>
|
||||
<div class="api-headers"><b>Content-Type:</b> application/json;charset=UTF-8</div>
|
||||
<p style="margin:8px 0"><b>请求体:</b></p>
|
||||
<div class="code-box"><pre><code>{
|
||||
"systemCode": "{{ sys?.systemCode || 'YOUR_CODE' }}"
|
||||
}</code></pre></div>
|
||||
<p style="margin:8px 0"><b>响应:</b></p>
|
||||
<div class="code-box"><pre><code>{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzM4NCJ9...",
|
||||
"expiresIn": 43200,
|
||||
"systemName": "系统名称",
|
||||
"usageNote": "此 Token 只能消费一次,消费后立即失效"
|
||||
}
|
||||
}</code></pre></div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-h3">10.2 发送邮件(消费 Token)</h3>
|
||||
<div class="api-card">
|
||||
<div class="api-method post">POST</div>
|
||||
<div class="api-path">/zgapi/v1/open/email/send</div>
|
||||
<div class="api-headers"><b>Authorization:</b> Bearer {token} | <b>Content-Type:</b> application/json;charset=UTF-8</div>
|
||||
<p style="margin:8px 0"><b>请求体:</b></p>
|
||||
<div class="code-box"><pre><code>{
|
||||
"to": "user@example.com",
|
||||
"subject": "邮件主题",
|
||||
"content": "<h1>邮件内容</h1>",
|
||||
"contentType": "html"
|
||||
}</code></pre></div>
|
||||
<p style="margin:8px 0">
|
||||
<b>参数说明:</b><br>
|
||||
<code>to</code> — 收件人邮箱(必填)<br>
|
||||
<code>subject</code> — 邮件主题(必填)<br>
|
||||
<code>content</code> — 邮件内容(必填,支持 HTML)<br>
|
||||
<code>contentType</code> — <code>"html"</code>(默认)或 <code>"text"</code>
|
||||
</p>
|
||||
<p style="margin:8px 0"><b>curl 示例:</b></p>
|
||||
<div class="code-box"><pre><code># 1. 获取 Token
|
||||
TOKEN=$(curl -s -X POST https://gw.server.zgitm.com/zgapi/v1/open/token \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"systemCode":"{{ sys?.systemCode || 'YOUR_CODE' }}"}' | jq -r '.data.token')
|
||||
|
||||
# 2. 发送邮件
|
||||
curl -X POST https://gw.server.zgitm.com/zgapi/v1/open/email/send \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"to": "user@example.com",
|
||||
"subject": "测试邮件",
|
||||
"content": "<h1>你好</h1><p>这是一封测试邮件</p>",
|
||||
"contentType": "html"
|
||||
}'</code></pre></div>
|
||||
<p style="margin:8px 0"><b>成功响应:</b></p>
|
||||
<div class="code-box"><pre><code>{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"msg": "发送成功",
|
||||
"tokenConsumed": true
|
||||
}
|
||||
}</code></pre></div>
|
||||
<p style="margin:8px 0">
|
||||
<b>注意:</b>邮件发送成功后才消费 Token;发送失败不消费 Token,可重试。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="warn-item" style="margin-top:16px">
|
||||
<b>⚠️ 注意事项</b>
|
||||
<p>1. 每个 Token 只能使用一次,调用任意业务接口即消费。<br>
|
||||
2. 发送邮件前请先获取新 Token。<br>
|
||||
3. 发件人为 <code>admin@zgitm.com</code>(阿里企业邮箱)。<br>
|
||||
4. 编码统一使用 UTF-8。</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { CircleCheckFilled, CircleClose } from '@element-plus/icons-vue'
|
||||
import { getIconComponent } from '@/utils/icons'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const route = useRoute()
|
||||
const sys = ref<any>(null) // 系统信息对象(systemCode, systemName, frontUrl, backendUrl, description)
|
||||
// docToken 从 URL params 读取,用于请求系统文档信息
|
||||
const token = route.params.token as string
|
||||
|
||||
// 网关地址和登录地址从环境变量读取,未配置时使用默认值
|
||||
const gatewayUrl = computed(() => import.meta.env.VITE_GATEWAY_URL || 'https://gw.server.zgitm.com')
|
||||
const loginUrl = computed(() => import.meta.env.VITE_LOGIN_URL || 'https://login.user.zgitm.com')
|
||||
|
||||
// 网关转发时注入的 HTTP 请求头列表
|
||||
// 业务后端从这些请求头中读取用户信息,无需自行验证 Token
|
||||
const headerTable = [
|
||||
{ name: 'X-User-Id', type: 'String', desc: '当前登录用户的唯一ID' },
|
||||
{ name: 'X-User-Name', type: 'String', desc: '用户真实姓名' },
|
||||
{ name: 'X-User-Account', type: 'String', desc: '用户登录账号' },
|
||||
{ name: 'X-User-Email', type: 'String', desc: '用户邮箱' },
|
||||
{ name: 'X-User-Post', type: 'String', desc: '用户岗位' },
|
||||
{ name: 'X-User-Roles', type: 'JSON', desc: '用户角色列表 [{roleId, roleName, roleCode}]' },
|
||||
{ name: 'X-System-Id', type: 'String', desc: '当前业务系统的数据库ID' },
|
||||
{ name: 'X-System-Code', type: 'String', desc: '当前业务系统的唯一编码' },
|
||||
{ name: 'X-System-Name', type: 'String', desc: '当前业务系统的显示名称' },
|
||||
]
|
||||
|
||||
/**
|
||||
* 生成菜单和角色初始化 SQL
|
||||
*
|
||||
* 内容:
|
||||
* 1. 创建"系统管理"和"业务管理"两个顶级目录
|
||||
* 2. 创建"用户管理"、"角色管理"、"订单管理"三个子菜单
|
||||
* 3. 创建"管理员"和"普通用户"两个角色
|
||||
* 4. 将所有菜单分配给管理员角色
|
||||
*
|
||||
* SQL 特点:
|
||||
* - 使用 SELECT ... FROM sys_system WHERE system_code = 'xxx' 子查询获取当前系统的 system_id
|
||||
* - 子菜单的 parent_id 通过自连接获取对应父菜单 ID
|
||||
*/
|
||||
const menuSql = computed(() => {
|
||||
const sc = sys.value?.systemCode || 'YOUR_SYSTEM_CODE'
|
||||
const sn = sys.value?.systemName || '系统'
|
||||
return `-- =============================================
|
||||
-- ${sn} 菜单 & 角色初始化 SQL
|
||||
-- 系统编码: ${sc}
|
||||
-- 将 system_code 替换为你的编码后直接执行
|
||||
-- =============================================
|
||||
|
||||
-- 1. 创建顶级菜单目录 (parent_id = 0)
|
||||
INSERT INTO sys_menu (system_id, parent_id, menu_name, menu_path, component, icon, menu_type, sort_order, status)
|
||||
SELECT s.id, 0, '系统管理', '/system', 'Layout', 'Setting', 1, 1, 1
|
||||
FROM sys_system s WHERE s.system_code = '${sc}';
|
||||
|
||||
INSERT INTO sys_menu (system_id, parent_id, menu_name, menu_path, component, icon, menu_type, sort_order, status)
|
||||
SELECT s.id, 0, '业务管理', '/biz', 'Layout', 'Document', 1, 2, 1
|
||||
FROM sys_system s WHERE s.system_code = '${sc}';
|
||||
|
||||
-- 2. 创建子菜单 (parent_id = 对应父菜单ID)
|
||||
INSERT INTO sys_menu (system_id, parent_id, menu_name, menu_path, component, icon, menu_type, sort_order, status)
|
||||
SELECT s.id, m.id, '用户管理', '/system/user', 'system/user/index', 'User', 2, 1, 1
|
||||
FROM sys_system s, sys_menu m
|
||||
WHERE s.system_code = '${sc}' AND m.menu_name = '系统管理' AND m.system_id = s.id;
|
||||
|
||||
INSERT INTO sys_menu (system_id, parent_id, menu_name, menu_path, component, icon, menu_type, sort_order, status)
|
||||
SELECT s.id, m.id, '角色管理', '/system/role', 'system/role/index', 'Avatar', 2, 2, 1
|
||||
FROM sys_system s, sys_menu m
|
||||
WHERE s.system_code = '${sc}' AND m.menu_name = '系统管理' AND m.system_id = s.id;
|
||||
|
||||
INSERT INTO sys_menu (system_id, parent_id, menu_name, menu_path, component, icon, menu_type, sort_order, status)
|
||||
SELECT s.id, m.id, '订单管理', '/biz/order', 'biz/order/index', 'Tickets', 2, 1, 1
|
||||
FROM sys_system s, sys_menu m
|
||||
WHERE s.system_code = '${sc}' AND m.menu_name = '业务管理' AND m.system_id = s.id;
|
||||
|
||||
-- 3. 创建角色
|
||||
INSERT INTO sys_role (system_id, role_name, role_code, description, status)
|
||||
SELECT s.id, '管理员', 'admin', '拥有全部权限', 1
|
||||
FROM sys_system s WHERE s.system_code = '${sc}';
|
||||
|
||||
INSERT INTO sys_role (system_id, role_name, role_code, description, status)
|
||||
SELECT s.id, '普通用户', 'user', '拥有基本权限', 1
|
||||
FROM sys_system s WHERE s.system_code = '${sc}';
|
||||
|
||||
-- 4. 将菜单分配给角色
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT r.id, m.id
|
||||
FROM sys_role r, sys_menu m, sys_system s
|
||||
WHERE r.role_code = 'admin'
|
||||
AND r.system_id = s.id
|
||||
AND m.system_id = s.id
|
||||
AND s.system_code = '${sc}';`
|
||||
})
|
||||
|
||||
/**
|
||||
* 生成网关管理员账号创建 SQL
|
||||
*
|
||||
* 步骤:
|
||||
* 1. 创建管理员用户(用户名 admin_{systemCode},密码为 admin123 的 BCrypt 密文)
|
||||
* 使用 NOT EXISTS 防止重复创建
|
||||
* 2. 绑定用户到 gateway 系统(sys_user_system 表)
|
||||
* 3. 分配 gateway-app-admin 角色(该角色只能看到自己绑定系统的数据)
|
||||
*
|
||||
* 注意:
|
||||
* - BCrypt 密文需自行生成替换,当前示例密文对应密码 "admin123"
|
||||
* - 如果用户已存在,步骤 1 跳过,只执行步骤 2、3
|
||||
* - 所有 INSERT 使用 NOT EXISTS 防止重复执行
|
||||
*/
|
||||
const adminSql = computed(() => {
|
||||
const sc = sys.value?.systemCode || 'YOUR_SYSTEM_CODE'
|
||||
return `-- =============================================
|
||||
-- 创建网关管理平台的应用系统管理员账号
|
||||
-- 角色编码: gateway-app-admin (只能看到自己绑定系统的数据)
|
||||
-- =============================================
|
||||
|
||||
-- 1. 创建管理员用户(密码 'admin123' 的 BCrypt 密文,建议自行生成替换)
|
||||
INSERT INTO sys_user (id, username, password, real_name, email, phone, status, is_deleted)
|
||||
SELECT REPLACE(UUID(),'-',''), 'admin_${sc}',
|
||||
'$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi',
|
||||
'${sys.value?.systemName || '系统'}管理员', 'admin@${sc}.com', NULL, 1, 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_user WHERE username = 'admin_${sc}');
|
||||
|
||||
-- 2. 绑定管理员到网关系统 (gateway)
|
||||
INSERT INTO sys_user_system (id, user_id, system_id)
|
||||
SELECT REPLACE(UUID(),'-',''), u.id, s.id
|
||||
FROM sys_user u, sys_system s
|
||||
WHERE u.username = 'admin_${sc}'
|
||||
AND s.system_code = 'gateway'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_user_system us
|
||||
WHERE us.user_id = u.id AND us.system_id = s.id
|
||||
);
|
||||
|
||||
-- 3. 分配 gateway-app-admin 角色(gateway 系统下的应用管理员角色)
|
||||
INSERT INTO sys_user_role (id, user_id, role_id)
|
||||
SELECT REPLACE(UUID(),'-',''), u.id, r.id
|
||||
FROM sys_user u, sys_role r, sys_system s
|
||||
WHERE u.username = 'admin_${sc}'
|
||||
AND r.role_code = 'gateway-app-admin'
|
||||
AND r.system_id = s.id
|
||||
AND s.system_code = 'gateway'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_user_role ur
|
||||
WHERE ur.user_id = u.id AND ur.role_id = r.id
|
||||
);`
|
||||
})
|
||||
|
||||
// 测试清单(全部默认未完成,由接入方自行勾选确认)
|
||||
const testChecklist = computed(() => [
|
||||
{ text: '访问业务前端首页 → 自动跳转到统一登录页', done: false },
|
||||
{ text: '使用测试账号登录 → 成功进入业务前端', done: false },
|
||||
{ text: '调用 GET /zgapi/v1/auth/user/info → 返回用户信息', done: false },
|
||||
{ text: '业务后端 Controller 中 UserContext.getUserId() 能获取到用户ID', done: false },
|
||||
{ text: '未放行的 API 请求被网关返回 403', done: false },
|
||||
{ text: '退出登录后访问业务页面 → 重新跳转登录页', done: false },
|
||||
])
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await request.get('/zgapi/v1/admin/system/doc/' + token)
|
||||
sys.value = res.data
|
||||
} catch { /* ignore */ }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ===== 整体 ===== */
|
||||
.doc-page { min-height:100vh; background:var(--bg-page); padding:20px; }
|
||||
.doc-container { max-width:960px; margin:0 auto; }
|
||||
|
||||
/* ===== 页头 ===== */
|
||||
.doc-hero {
|
||||
display:flex; align-items:center; justify-content:space-between;
|
||||
padding:28px 32px; background:var(--bg-surface);
|
||||
border-radius:var(--radius-xl); border:1px solid var(--border-color);
|
||||
margin-bottom:24px; box-shadow:var(--shadow-sm);
|
||||
}
|
||||
.doc-hero-left { display:flex; align-items:center; gap:14px; }
|
||||
.doc-logo { width:36px; height:36px; color:var(--color-primary); flex-shrink:0; }
|
||||
.doc-hero-left h1 { font-size:22px; font-weight:700; margin:0 0 4px; color:var(--text-primary); }
|
||||
.doc-hero-left p { font-size:14px; color:var(--text-muted); margin:0; }
|
||||
|
||||
/* ===== 章节 ===== */
|
||||
.doc-section {
|
||||
background:var(--bg-surface); border-radius:var(--radius-xl);
|
||||
border:1px solid var(--border-color); padding:28px 32px;
|
||||
margin-bottom:20px; box-shadow:var(--shadow-sm);
|
||||
}
|
||||
.section-h2 {
|
||||
display:flex; align-items:center; gap:12px;
|
||||
font-size:20px; font-weight:700; color:var(--text-primary);
|
||||
margin:0 0 20px; padding-bottom:16px;
|
||||
border-bottom:1px solid var(--border-light);
|
||||
}
|
||||
.s-num {
|
||||
display:inline-flex; align-items:center; justify-content:center;
|
||||
width:32px; height:32px; border-radius:var(--radius-md);
|
||||
background:var(--color-primary); color:#fff;
|
||||
font-size:15px; font-weight:700; flex-shrink:0;
|
||||
}
|
||||
.section-h3 {
|
||||
font-size:16px; font-weight:600; color:var(--text-primary);
|
||||
margin:24px 0 10px;
|
||||
}
|
||||
.section-h3:first-of-type { margin-top:0; }
|
||||
|
||||
/* ===== 文字 ===== */
|
||||
.text-p { font-size:14px; color:var(--text-secondary); line-height:1.7; margin:0 0 12px; }
|
||||
.inline-code {
|
||||
background:var(--color-primary-bg); color:var(--color-primary);
|
||||
padding:2px 8px; border-radius:4px; font-size:13px;
|
||||
font-family:'SF Mono','Consolas',monospace;
|
||||
}
|
||||
|
||||
/* ===== 架构图 ===== */
|
||||
.arch-text {
|
||||
background:#1e293b; color:#e2e8f0; padding:16px 20px;
|
||||
border-radius:var(--radius-md); font-size:13px;
|
||||
line-height:1.7; overflow-x:auto; white-space:pre;
|
||||
font-family:'SF Mono','Consolas',monospace;
|
||||
}
|
||||
.flow-steps { margin-top:16px; display:flex; flex-direction:column; gap:10px; }
|
||||
.flow-step { font-size:14px; color:var(--text-secondary); display:flex; align-items:flex-start; gap:8px; line-height:1.6; }
|
||||
.flow-step b { color:var(--color-primary); flex-shrink:0; }
|
||||
|
||||
/* ===== 代码块 ===== */
|
||||
.code-box {
|
||||
position:relative; background:#1e293b; border-radius:var(--radius-md);
|
||||
margin-bottom:16px; overflow:hidden;
|
||||
}
|
||||
.code-label {
|
||||
position:absolute; top:8px; right:12px; z-index:1;
|
||||
font-size:11px; color:rgba(255,255,255,0.45); font-weight:600;
|
||||
letter-spacing:0.5px; text-transform:uppercase;
|
||||
}
|
||||
.code-box pre {
|
||||
margin:0; padding:20px; overflow-x:auto;
|
||||
}
|
||||
.code-box code {
|
||||
font-family:'SF Mono','Fira Code','Consolas',monospace;
|
||||
font-size:13px; line-height:1.75; color:#e2e8f0; white-space:pre;
|
||||
}
|
||||
|
||||
/* ===== API 卡片 ===== */
|
||||
.api-card {
|
||||
border:1px solid var(--border-color); border-radius:var(--radius-lg);
|
||||
padding:20px; margin-bottom:16px;
|
||||
}
|
||||
.api-method {
|
||||
display:inline-block; padding:2px 10px; border-radius:4px;
|
||||
font-size:12px; font-weight:700; color:#fff; margin-bottom:8px;
|
||||
}
|
||||
.api-method.get { background:#10b981; }
|
||||
.api-method.post { background:#6366f1; }
|
||||
.api-method.put { background:#f59e0b; }
|
||||
.api-path {
|
||||
font-size:15px; font-weight:600; color:var(--text-primary);
|
||||
font-family:'SF Mono','Consolas',monospace; margin-bottom:8px;
|
||||
}
|
||||
.api-headers { font-size:14px; color:var(--text-secondary); margin-bottom:8px; }
|
||||
.api-response { font-size:14px; color:var(--text-secondary); }
|
||||
|
||||
/* ===== 测试清单 ===== */
|
||||
.checklist { display:flex; flex-direction:column; gap:14px; }
|
||||
.check-item { display:flex; align-items:center; gap:10px; font-size:14px; color:var(--text-secondary); }
|
||||
|
||||
/* 架构警告 */
|
||||
.arch-warnings { margin-top:16px; display:flex; flex-direction:column; gap:12px; }
|
||||
.warn-item {
|
||||
background:#fef2f2; border:1px solid #fecaca; border-radius:var(--radius-md);
|
||||
padding:14px 18px;
|
||||
}
|
||||
.warn-item b { color:#dc2626; font-size:14px; display:block; margin-bottom:4px; }
|
||||
.warn-item p { color:var(--text-secondary); font-size:14px; margin:0 0 8px; line-height:1.7; }
|
||||
|
||||
@media (max-width:768px) {
|
||||
.doc-hero { flex-direction:column; gap:12px; align-items:flex-start; }
|
||||
.doc-section { padding:20px; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user