feat(frontend): 管理端改用 Gi Admin Pro 框架 + 腾讯云主题
前端此前只有本地实现、从未入库,本次首次纳入版本管理。
框架切换
· 以 Gi Admin Pro(Vue3 + Vite + TS + Arco Design Vue + Pinia)为底
· 剥离 14 个演示页面目录 / 121 个文件(demo 28、file 20、form 19、system 18…)
· 依赖 dependencies 35 → 19,devDependencies 31 → 27
· 保留框架核心:layout、Gi* 组件库、hooks、权限指令、错误页、登录页
主题:腾讯云 / TDesign 官方色板
· 主色 #0052D9(腾讯蓝)、成功 #00A870、警告 #ED7B2F、危险 #E34D59
· 页面底色 #f3f3f3、文字 rgba(0,0,0,90/60/40/26%)
· 覆盖 Arco 底层色板 + 显式语义别名(primary/success/warning/danger/link/neutral)
· 三个改了不生效也不报错的坑已记入注释:
1) Arco 色值是逗号分隔 RGB 三元组,不是 #RRGGBB
2) Arco 把色板定义在 body 而非 :root,必须写 `:root, body`
3) setting.json 的 themeColor 会以内联样式写进 body,优先级高于样式表
对接后端(三处契约差异在 @/utils/http 统一适配)
· 响应信封 {code,msg,data} → {code,success,message,data}
· Token 头 token → Authorization: Bearer
· 分页 {list,total} → 补一份 records,list 原样保留
· 40101 → 登录页;登录密码 RSA 加密(公钥走 /auth/public-key,轮换无需重建前端)
· 路由:后端只下发菜单键,前端在本地路由表上过滤(有哪些页面归代码,谁能看归后端)
已实现页面(浏览器实测通过)
· 登录:RSA 加密 + 自适应验证码/租户码 + 账号锁定提示
· 工作台:真实数据(组织 6 个 / 角色 1 个 / 权限 25 项)
· 组织架构:三级树 + CRUD + 占用校验
· 租户管理:开通租户 + 初始密码只显示一次的强确认
· 待建页:未实现菜单显示所属里程碑
· 权限菜单:学校管理员看不到「租户管理」
视觉资产按腾讯云主题重做
· logo.svg(渐变圆角方 + 填)、favicon.svg(小尺寸做减法)、login-hero.svg(材料归集)
· 登录页点阵纹理走主题变量,换主色自动跟随
· 文案改为产品实际能力:自动改名 / 强制压缩 / 隐私保护
清理模板遗留(约 17MB + 三处外部依赖)
· 百度统计脚本(上报到 gi-demo 作者的统计账号)
· 用户菜单「项目地址」(跳转模板作者的 Gitee 仓库)
· 两份 8.8MB mp3、388KB favicon.ico、57KB 模板插画、未引用字体与 loading.gif
· 「个人中心」指向已删除的 /system/account(点了 404)
· 头部头像在无 avatarUrl 时渲染成裂图 → 改为姓名首字兜底
后端修复
· 新增 GET /auth/public-key(前端加密登录密码用)
· DataSeeder.ancestor_path 生成错误:子节点本该是 /16/17/ 却写成 /17/
该字段是「按院系统计」的索引,错了会漏人/多人且不报错 —— 改为从库里读父节点路径
· seedDemoOrgs 曾放在租户作用域外 + admin 早退分支之后,导致启动失败 / 永不执行
· 示范租户补一套三级组织树(1 学校 / 2 院系 / 3 班级),供界面演示
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { Message, Notification } from '@arco-design/web-vue'
|
||||
import axios from 'axios'
|
||||
import NProgress from 'nprogress'
|
||||
import router from '@/router'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import 'nprogress/nprogress.css'
|
||||
|
||||
// 配置 NProgress
|
||||
NProgress.configure({ showSpinner: false })
|
||||
|
||||
/**
|
||||
* 本项目后端响应契约(《03-接口设计》§1.3):
|
||||
*
|
||||
* { code: 0 | 4xxxx | 5xxxx, msg: string, data: T }
|
||||
*
|
||||
* 与模板内部的 ApiRes 契约不同,差别有三处,都在下面这层适配里统一处理:
|
||||
*
|
||||
* | 维度 | 本项目后端 | 模板内部约定 |
|
||||
* |----------|--------------------|--------------------|
|
||||
* | 成功判定 | code === 0 | success: boolean |
|
||||
* | 提示文案 | msg | message |
|
||||
* | 分页列表 | { list, total } | { records, total } |
|
||||
*
|
||||
* ⚠️ 适配放在这里而不是逐个改 api 文件:这三条是全后端统一的契约,
|
||||
* 分散到每个 listAPI 去转换,漏一个就是一个"列表永远空着"的 bug。
|
||||
*/
|
||||
|
||||
/** 本项目后端的成功码 */
|
||||
const CODE_SUCCESS = 0
|
||||
/** 未登录 / token 失效 */
|
||||
const CODE_UNAUTHORIZED = 40101
|
||||
/** 无权限(含跨租户) */
|
||||
const CODE_FORBIDDEN = 40301
|
||||
/** 限流 */
|
||||
const CODE_RATE_LIMITED = 42901
|
||||
|
||||
/** 传输层 HTTP 状态码文案(业务错误不走这里,见下方响应拦截器) */
|
||||
const StatusCodeMessage: Record<number, string> = {
|
||||
400: '请求错误(400)',
|
||||
401: '未授权,请重新登录(401)',
|
||||
403: '拒绝访问(403)',
|
||||
404: '请求出错(404)',
|
||||
408: '请求超时(408)',
|
||||
500: '服务器错误(500)',
|
||||
501: '服务未实现(501)',
|
||||
502: '网络错误(502)',
|
||||
503: '服务不可用(503)',
|
||||
504: '网络超时(504)'
|
||||
}
|
||||
|
||||
/** 创建 axios 实例 */
|
||||
const http: AxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_PREFIX,
|
||||
timeout: 30 * 1000
|
||||
})
|
||||
|
||||
/**
|
||||
* 把分页字段补一份 records。
|
||||
* 后端返回 { list, total },模板的 useTable / PageRes 读 records。
|
||||
* 补而不替:list 原样保留,两边都能取到,避免破坏原有调用。
|
||||
*/
|
||||
function normalizePageShape(payload: unknown): unknown {
|
||||
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||||
const obj = payload as Record<string, unknown>
|
||||
if (Array.isArray(obj.list) && typeof obj.total === 'number' && obj.records === undefined) {
|
||||
return { ...obj, records: obj.list }
|
||||
}
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
/** 跳登录页(只跳一次,避免一个页面多个请求同时失效时反复跳) */
|
||||
let redirecting = false
|
||||
function toLogin() {
|
||||
if (redirecting) return
|
||||
redirecting = true
|
||||
const back = encodeURIComponent(router.currentRoute.value.fullPath)
|
||||
router.replace(`/login?redirect=${back}`).finally(() => {
|
||||
setTimeout(() => {
|
||||
redirecting = false
|
||||
}, 800)
|
||||
})
|
||||
}
|
||||
|
||||
// 请求拦截器
|
||||
http.interceptors.request.use(
|
||||
(config: AxiosRequestConfig) => {
|
||||
NProgress.start()
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
if (!config.headers) {
|
||||
config.headers = {}
|
||||
}
|
||||
// 本项目后端用标准的 Authorization: Bearer(《03-接口设计》§1.2),
|
||||
// 不是模板默认的 token 自定义头
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
NProgress.done()
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
http.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
NProgress.done()
|
||||
const raw = response.data
|
||||
|
||||
// 非信封响应(文件流等)直接放行
|
||||
if (!raw || typeof raw.code !== 'number') {
|
||||
return response
|
||||
}
|
||||
|
||||
// 未登录:清 token 并跳登录
|
||||
if (raw.code === CODE_UNAUTHORIZED) {
|
||||
toLogin()
|
||||
return Promise.reject(new Error(raw.msg || '登录已失效,请重新登录'))
|
||||
}
|
||||
|
||||
// 业务成功:适配成模板的 ApiRes 形状
|
||||
if (raw.code === CODE_SUCCESS) {
|
||||
response.data = {
|
||||
code: 200,
|
||||
success: true,
|
||||
message: raw.msg || 'success',
|
||||
data: normalizePageShape(raw.data)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// 业务失败:按文案长度选择 Message(短)或 Notification(长),与原实现一致
|
||||
const message = raw.msg || '服务器端错误'
|
||||
if (message.length <= 15) {
|
||||
Message.error(message)
|
||||
} else {
|
||||
Notification.error(message)
|
||||
}
|
||||
// 把后端业务码附在错误上,便于调用方按码分支(如租户重名)
|
||||
const err = new Error(message) as Error & { bizCode?: number, bizData?: unknown }
|
||||
err.bizCode = raw.code
|
||||
err.bizData = raw.data
|
||||
return Promise.reject(err)
|
||||
},
|
||||
(error) => {
|
||||
NProgress.done()
|
||||
Message.clear()
|
||||
const response = Object.assign({}, error.response)
|
||||
|
||||
// 401/403/429 这三个后端用真实 HTTP 状态码(《03-接口设计》§1.3)
|
||||
if (response?.status === 401) {
|
||||
toLogin()
|
||||
} else {
|
||||
const msg = StatusCodeMessage[response?.status] || '系统异常, 请检查网络或联系管理员!'
|
||||
Message.error(msg)
|
||||
}
|
||||
|
||||
const err = new Error(error.message) as Error & { status?: number }
|
||||
err.status = response?.status
|
||||
return Promise.reject(err)
|
||||
}
|
||||
)
|
||||
|
||||
/** 封装请求方法 */
|
||||
const request = <T = unknown>(config: AxiosRequestConfig): Promise<ApiRes<T>> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http
|
||||
.request<T>(config)
|
||||
.then((res: AxiosResponse) => resolve(res.data))
|
||||
.catch((err: { message: string }) => reject(err))
|
||||
})
|
||||
}
|
||||
|
||||
/** GET 请求 */
|
||||
const get = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<ApiRes<T>> => {
|
||||
return request({
|
||||
method: 'get',
|
||||
url,
|
||||
params,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
/** POST 请求 */
|
||||
const post = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<ApiRes<T>> => {
|
||||
return request({
|
||||
method: 'post',
|
||||
url,
|
||||
data: params,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
/** PUT 请求(本项目后端大量使用 PUT,模板原来只有 get/post) */
|
||||
const put = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<ApiRes<T>> => {
|
||||
return request({
|
||||
method: 'put',
|
||||
url,
|
||||
data: params,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
/** DELETE 请求 */
|
||||
const del = <T = any>(url: string, config?: AxiosRequestConfig): Promise<ApiRes<T>> => {
|
||||
return request({
|
||||
method: 'delete',
|
||||
url,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
export default { get, post, put, delete: del, request }
|
||||
Reference in New Issue
Block a user