docs: annotate all shared frontend modules in Chinese
This commit is contained in:
1 parent
2be21c1c62
commit
408f8e5c98
55 files changed
+349
No files matched your search
@@ -1,7 +1,13 @@
|
||||
/**
|
||||
* 控制台统一请求与登录会话入口。
|
||||
* 页面通过 useApi 访问 /console/v1,通过 useAuth 共享登录状态;本模块只在客户端使用。
|
||||
* 后端业务成功必须同时满足 HTTP 成功和响应信封 status === 200,不能仅检查 HTTP 状态。
|
||||
*/
|
||||
import CryptoJS from "crypto-js";
|
||||
import { defineStore } from "pinia";
|
||||
import { navigateTo, useRuntimeConfig } from "#app";
|
||||
import { computed, ref } from "vue";
|
||||
// 将供应商/模块内部错误转成面向会员的提示,避免在 UI 中暴露上游实现细节。
|
||||
var ce = [
|
||||
[/产品类型错误.{0,30}安全组[^\n]*/g, `该商品未接入安全组`],
|
||||
[/商品未关联.{0,20}模块接口/g, `该商品暂不可用`],
|
||||
@@ -26,6 +32,7 @@ var le = [
|
||||
];
|
||||
var ue =
|
||||
/模块接口|自定义配置|模块未关联|模块标识|上游|第三方云|[a-z]{2,}[_-][a-z0-9_]+|\bDCIM\b|whmcs|reserver|https?:\/\//i;
|
||||
/** 非字符串输入视为空;替换后仍包含内部标识的消息统一降级,不将原始异常直接展示。 */
|
||||
function W(e) {
|
||||
let t = typeof e == `string` ? e : ``;
|
||||
if (!t) return ``;
|
||||
@@ -41,6 +48,7 @@ function W(e) {
|
||||
n.length < 2 || ue.test(n) ? `操作失败,请稍后重试或联系客服` : n
|
||||
);
|
||||
}
|
||||
// 操作日志中的熔断事件单独翻译;其余日志保留原文,由调用方决定展示方式。
|
||||
function de(e) {
|
||||
let t = typeof e == `string` ? e : ``;
|
||||
return /^\s*上游熔断解除[::]/u.test(t)
|
||||
@@ -49,18 +57,21 @@ function de(e) {
|
||||
? `服务连接暂时异常,请稍后重试`
|
||||
: t;
|
||||
}
|
||||
// 列表缓存最多 32 项,每项不超过 200 条、存活 60 秒;身份切换或写操作会使其失效。
|
||||
var fe = 32;
|
||||
var pe = 200;
|
||||
var me = 6e4;
|
||||
var G = new Map();
|
||||
var he = Symbol(`uninitialized`);
|
||||
var ge = 0;
|
||||
// 递增代次,阻止清空之前发出的旧请求在完成后重新写回缓存。
|
||||
function _e() {
|
||||
(ge++, G.clear());
|
||||
}
|
||||
function ve() {
|
||||
return ge;
|
||||
}
|
||||
/** 按身份/命名空间、接口及参数生成稳定键;对象键排序避免同义参数重复缓存。 */
|
||||
function ye(e, t, n = ``) {
|
||||
return JSON.stringify([e, n, t], (e, t) =>
|
||||
t && typeof t == `object` && !Array.isArray(t)
|
||||
@@ -72,15 +83,18 @@ function ye(e, t, n = ``) {
|
||||
: t,
|
||||
);
|
||||
}
|
||||
// 缓存只保存可 JSON 序列化的数据;读写均复制,避免页面修改共享对象。
|
||||
function be(e) {
|
||||
return JSON.parse(JSON.stringify(e));
|
||||
}
|
||||
/** e 为当前身份,t 为缓存键;切换身份清空缓存,命中时移动到 Map 尾部维护 LRU。 */
|
||||
function xe(e, t, n = Date.now()) {
|
||||
he !== e && (_e(), (he = e));
|
||||
for (let [e, t] of G) t.expires <= n && G.delete(e);
|
||||
let r = G.get(t);
|
||||
if (r) return (G.delete(t), G.set(t, r), be(r.data));
|
||||
}
|
||||
/** 仅接受身份和请求开始时代次均未变化的列表结果;过大的结果或序列化失败不缓存。 */
|
||||
function Se(e, t, n, r, i = Date.now()) {
|
||||
if (e === he && r === ge) {
|
||||
if (!Array.isArray(n.list) || n.list.length > pe) {
|
||||
@@ -101,6 +115,7 @@ function Se(e, t, n, r, i = Date.now()) {
|
||||
}
|
||||
}
|
||||
var Ce = 2147483647;
|
||||
// 购买协议的整数范围与后端有符号 32 位整数一致;位置允许 0,商品和数量必须大于 0。
|
||||
function K(e, t) {
|
||||
let n =
|
||||
typeof e == `number`
|
||||
@@ -124,6 +139,10 @@ function Te(e) {
|
||||
qty: K(t.qty, 1),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 从实际写请求提取限购检查所需字段;未匹配的接口返回 null,不进入购买预检。
|
||||
* 购物车位置去重,批量添加最多 1000 项;此处校验不替代后端的最终授权。
|
||||
*/
|
||||
function Ee(e, t, n) {
|
||||
if (e === `POST` && t === `/product/settle`)
|
||||
return {
|
||||
@@ -174,6 +193,7 @@ function Ee(e, t, n) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// 预检必须明确允许/拒绝,拒绝时必须给出非空原因;缺失字段不能被当成放行。
|
||||
function De(e) {
|
||||
if (!e || typeof e != `object` || Array.isArray(e)) return !1;
|
||||
let t = e;
|
||||
@@ -183,6 +203,7 @@ function De(e) {
|
||||
(t.allowed || !!t.message.trim())
|
||||
);
|
||||
}
|
||||
/** 统一传递 HTTP/业务状态和可供安全验证等流程读取的数据,不丢失错误分类。 */
|
||||
class ApiError extends Error {
|
||||
status;
|
||||
data;
|
||||
@@ -201,6 +222,10 @@ function requestTimeoutError() {
|
||||
return new ApiError(408, "请求超时,请稍后重试");
|
||||
}
|
||||
|
||||
/**
|
||||
* 截止时间覆盖整个异步操作(含预检和 JSON/Blob 解码),而非只覆盖收到响应头之前。
|
||||
* 超时会中止 fetch;finally 清理计时器,调用方仍须在 await 后检查取消状态。
|
||||
*/
|
||||
async function withRequestDeadline(
|
||||
operation,
|
||||
timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
@@ -232,16 +257,19 @@ function assertRequestActive(signal) {
|
||||
if (signal.aborted) throw requestTimeoutError();
|
||||
}
|
||||
|
||||
// 已分类的业务错误原样传播;浏览器底层网络异常统一为状态 0,避免泄露内部信息。
|
||||
function normalizeRequestError(error) {
|
||||
return error instanceof ApiError
|
||||
? error
|
||||
: new ApiError(0, "网络异常,请检查网络后重试");
|
||||
}
|
||||
|
||||
/** 在 Nuxt 上下文中创建客户端;apiBase 只配置后端 origin,本模块统一追加接口前缀。 */
|
||||
function useApi() {
|
||||
const config = useRuntimeConfig();
|
||||
const auth = useAuth();
|
||||
|
||||
// 过期请求的 401 只能清理它所属的会话,不能把随后重新登录的新账号退出。
|
||||
function clearUnauthorizedSession(requestToken) {
|
||||
if (
|
||||
!requestToken ||
|
||||
@@ -269,6 +297,7 @@ function useApi() {
|
||||
: {};
|
||||
}
|
||||
|
||||
// HTTP 401 优先于业务信封;其他错误优先使用非 200 的业务状态,并脱敏错误消息。
|
||||
function responseError(response, payload, requestToken, fallbackMessage) {
|
||||
const envelope = responseObject(payload);
|
||||
const status =
|
||||
@@ -285,6 +314,7 @@ function useApi() {
|
||||
);
|
||||
}
|
||||
|
||||
/** 无 status 的 data 数组仅在调用方明确开启兼容项时接受,其他格式异常均拒绝。 */
|
||||
async function decodeResponse(
|
||||
response,
|
||||
requestToken,
|
||||
@@ -307,6 +337,7 @@ function useApi() {
|
||||
throw responseError(response, payload, requestToken, fallbackMessage);
|
||||
}
|
||||
|
||||
// 数组参数编码为 key[];默认省略 null/空串,少数筛选接口可显式保留空串。
|
||||
function apiUrl(path, query, keepEmpty = false) {
|
||||
let url = `${String(config.public.apiBase ?? "")}/console/v1${path}`;
|
||||
const search = new URLSearchParams();
|
||||
@@ -325,6 +356,7 @@ function useApi() {
|
||||
return url;
|
||||
}
|
||||
|
||||
/** JSON 请求主路径:固定本次身份和请求体 → 必要时限购预检 → 请求 → 验证信封。 */
|
||||
async function request(method, path, options = {}) {
|
||||
// Capture identity before the first await, including when logout clears it immediately.
|
||||
const requestToken = auth.token;
|
||||
@@ -349,6 +381,7 @@ function useApi() {
|
||||
);
|
||||
}
|
||||
if (purchase) {
|
||||
// 等待预检期间身份或表单可能变化;二次比较确保真正提交的是已核验的内容。
|
||||
const authToken = auth.token;
|
||||
const permission = await request("POST", "/purchase_limit/check", {
|
||||
body: purchase,
|
||||
@@ -391,6 +424,7 @@ function useApi() {
|
||||
options.allowStatuslessDataArray,
|
||||
);
|
||||
assertRequestActive(signal);
|
||||
// 普通写操作清空列表缓存;只读报价和限购预检不应让正在展示的列表无故失效。
|
||||
if (
|
||||
method !== "GET" &&
|
||||
path !== "/purchase_limit/check" &&
|
||||
@@ -405,6 +439,7 @@ function useApi() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 文件使用 multipart/form-data,由浏览器生成 boundary;不要手动设置 Content-Type。 */
|
||||
async function upload(file) {
|
||||
const body = new FormData();
|
||||
body.append("file", file);
|
||||
@@ -434,6 +469,7 @@ function useApi() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 下载仍校验 JSON 错误信封;成功后创建临时下载链接,并在点击后释放对象 URL。 */
|
||||
async function download(path, data, filename = "export.xlsx", options = {}) {
|
||||
const method = options.method ?? "GET";
|
||||
const headers = { Accept: "*/*" };
|
||||
@@ -529,6 +565,8 @@ function useApi() {
|
||||
del: (path, query, body) => request("DELETE", path, { query, body }),
|
||||
};
|
||||
}
|
||||
// 既有后端登录协议要求固定 AES-CBC 参数;这是传输字段兼容,不是安全密码存储。
|
||||
// 修改必须与后端协商,HTTPS 仍不可省略,前端公开的固定参数不能视为秘密。
|
||||
var ot = CryptoJS.enc.Utf8.parse(`idcsmart.finance`);
|
||||
var st = CryptoJS.enc.Utf8.parse(`9311019310287172`);
|
||||
function ct(e) {
|
||||
@@ -538,6 +576,7 @@ function ct(e) {
|
||||
iv: st,
|
||||
}).toString();
|
||||
}
|
||||
/** 按安全验证、限流、超时和服务异常分类登录错误,屏蔽 HTML/SQL/堆栈等内部内容。 */
|
||||
function lt(e, t = `登录失败,请稍后重试`) {
|
||||
let n = e && typeof e == `object` ? e : {},
|
||||
r = typeof n.message == `string` ? n.message.trim() : ``,
|
||||
@@ -563,11 +602,13 @@ function lt(e, t = `登录失败,请稍后重试`) {
|
||||
? t
|
||||
: r;
|
||||
}
|
||||
// 这里只检查凭证形状,不验证签名和权限;真实性须由携带凭证的后端请求确认。
|
||||
function ut(e) {
|
||||
return (
|
||||
typeof e == `string` && e.length > 0 && e.length <= 8192 && !/\s/.test(e)
|
||||
);
|
||||
}
|
||||
/** 从接口成功/失败的常见包装中识别额外安全验证挑战,未找到时返回 null。 */
|
||||
function dt(e) {
|
||||
if (!e || typeof e != `object`) return null;
|
||||
let t = e,
|
||||
@@ -586,6 +627,7 @@ function dt(e) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// 收到挑战不能被当成业务已完成;将挑战附在异常 data 上交给安全验证对话框。
|
||||
function ft(e) {
|
||||
let t = dt(e);
|
||||
if (t)
|
||||
@@ -599,6 +641,7 @@ var pt = {
|
||||
phone_code: `手机验证码`,
|
||||
certification: `实名认证`,
|
||||
};
|
||||
/** 将后端允许的验证方式转成选择项;未知方式不展示,逐项 token 优先于公共 token。 */
|
||||
function mt(e) {
|
||||
return (
|
||||
Array.isArray(e.available_methods) ? e.available_methods : []
|
||||
@@ -628,6 +671,10 @@ function mt(e) {
|
||||
: [];
|
||||
});
|
||||
}
|
||||
/**
|
||||
* JWT 持久化到 localStorage 是现有会话约定,仍需防范 XSS;不在日志中输出凭证。
|
||||
* sessionVersion 隔离跨登录的异步结果,overviewPromise 合并同一会话的并发首页请求。
|
||||
*/
|
||||
const useAuth = defineStore("auth", () => {
|
||||
const token = ref(localStorage.getItem("jwt"));
|
||||
const overview = ref(null);
|
||||
@@ -638,6 +685,7 @@ const useAuth = defineStore("auth", () => {
|
||||
let sessionVersion = 0;
|
||||
let legacySession = null;
|
||||
|
||||
// 仅在登录或服务端核验成功后调用;先清理上一会话的缓存与未完成旧平台登录。
|
||||
function loginWithJwt(jwt) {
|
||||
if (!ut(jwt))
|
||||
throw Error("登录响应无效,请重新登录;若持续失败,请联系客服。");
|
||||
@@ -646,6 +694,7 @@ const useAuth = defineStore("auth", () => {
|
||||
localStorage.setItem("jwt", jwt);
|
||||
}
|
||||
|
||||
// 旧平台带来的凭证先暂存待验证,不立即写成已登录状态,避免路由守卫误放行。
|
||||
function prepareLegacyLogin(jwt = localStorage.getItem("jwt")) {
|
||||
clear();
|
||||
legacySession = {
|
||||
@@ -657,12 +706,17 @@ const useAuth = defineStore("auth", () => {
|
||||
legacyLoginPending.value = true;
|
||||
}
|
||||
|
||||
// 退出/切换身份会中止旧验证,并丢弃其结果。
|
||||
function cancelLegacyLogin() {
|
||||
legacySession?.controller?.abort();
|
||||
legacySession = null;
|
||||
legacyLoginPending.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 /index 实际核验旧平台 JWT;并发点击共享 Promise,失败后允许同一入口重试。
|
||||
* no-store/omit/no-referrer 避免缓存、Cookie 和 Referer 携带额外身份信息。
|
||||
*/
|
||||
function verifyLegacyLogin() {
|
||||
const session = legacySession;
|
||||
if (!session?.jwt) {
|
||||
@@ -731,6 +785,7 @@ const useAuth = defineStore("auth", () => {
|
||||
return promise;
|
||||
}
|
||||
|
||||
/** 密码/手机验证码登录共用入口;安全验证结果由调用方传入,成功后才保存 JWT。 */
|
||||
async function login(
|
||||
account,
|
||||
password,
|
||||
@@ -773,6 +828,7 @@ const useAuth = defineStore("auth", () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 在请求完成前共享 Promise;会话版本不一致时拒绝回填,防止旧账号资料覆盖新账号。
|
||||
function loadOverview() {
|
||||
if (!overviewPromise) {
|
||||
const version = sessionVersion;
|
||||
@@ -791,6 +847,7 @@ const useAuth = defineStore("auth", () => {
|
||||
return overviewPromise;
|
||||
}
|
||||
|
||||
// 页面首次读取允许使用本会话已有资料;需要强制刷新余额等信息时调用 refreshOverview。
|
||||
async function fetchOverview() {
|
||||
return overview.value ?? loadOverview();
|
||||
}
|
||||
@@ -799,6 +856,7 @@ const useAuth = defineStore("auth", () => {
|
||||
return loadOverview();
|
||||
}
|
||||
|
||||
// 本地退出立即生效,不等待远端吊销成功;请求已同步捕获旧 Bearer,不会误用新身份。
|
||||
function logout() {
|
||||
// request() captures the original Bearer synchronously; remote revocation never owns local logout.
|
||||
const revocation = useApi().post("/logout");
|
||||
@@ -806,6 +864,7 @@ const useAuth = defineStore("auth", () => {
|
||||
void revocation.catch(() => {});
|
||||
}
|
||||
|
||||
// 所有会话切换走同一清理入口,使缓存、旧平台验证和账户资料同时失效。
|
||||
function resetSessionState() {
|
||||
_e();
|
||||
cancelLegacyLogin();
|
||||
|
||||
Reference in new issue
Block a user