628 lines
25 KiB
Vue
628 lines
25 KiB
Vue
<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,
|
||
})
|
||
|
||
// 请求拦截器:携带 Token 和系统编码
|
||
request.interceptors.request.use(config => {
|
||
const token = localStorage.getItem('token')
|
||
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) {
|
||
localStorage.removeItem('token')
|
||
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 = localStorage.getItem('token')
|
||
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 写入 localStorage 并跳回业务前端。业务前端无需处理登录请求,只需从 localStorage 读取 Token。</p>
|
||
<div class="code-box">
|
||
<div class="code-label">TypeScript</div>
|
||
<pre><code>// main.ts 或 App.vue onMounted
|
||
const token = localStorage.getItem('token')
|
||
if (token) {
|
||
// 用户已登录,正常渲染应用
|
||
// 如需获取用户信息,调用网关 API:
|
||
const res = await request.get('/zgapi/v1/auth/user/info')
|
||
console.log('当前用户:', res.data)
|
||
}</code></pre>
|
||
</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>
|
||
<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>
|
||
</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)
|
||
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')
|
||
|
||
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: '当前业务系统的显示名称' },
|
||
]
|
||
|
||
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}';`
|
||
})
|
||
|
||
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>
|