diff --git a/app/config/primevue.js b/app/config/primevue.js index b6daa49..3d0725c 100644 --- a/app/config/primevue.js +++ b/app/config/primevue.js @@ -1,8 +1,10 @@ +/** PrimeVue 客户端统一配置:中文交互文案、蓝色主色以及 .app-dark 暗色切换。 */ import { definePreset } from "@primeuix/themes"; import Aura from "@primeuix/themes/aura"; import locale from "./locale.zh-CN.json"; // Use the MIT theme package, with the console's blue/slate/zinc palette. +// 只覆盖 Aura 的语义色阶,保留组件自身设计令牌;浅色用 slate,深色用 zinc。 const preset = definePreset(Aura, { semantic: { primary: Object.fromEntries( diff --git a/app/lib/_plugin-vue_export-helper.js b/app/lib/_plugin-vue_export-helper.js index f7c3750..842e894 100644 --- a/app/lib/_plugin-vue_export-helper.js +++ b/app/lib/_plugin-vue_export-helper.js @@ -1,3 +1,4 @@ +/** 为恢复的 Vue 组件附加 scopeId 等编译元数据;兼容对象组件和带 __vccOpts 的组件。 */ var e = (e, t) => { let n = e.__vccOpts || e; for (let [e, r] of t) n[e] = r; diff --git a/app/lib/auth-redirect.js b/app/lib/auth-redirect.js index 50d0b41..1c67dad 100644 --- a/app/lib/auth-redirect.js +++ b/app/lib/auth-redirect.js @@ -1,3 +1,4 @@ +/** 登录后的站内回跳规则;只接受单斜杠开头路径,拒绝外站、反斜杠和控制字符。 */ function e(e) { if ( typeof e != `string` || @@ -7,6 +8,7 @@ function e(e) { ) return `/`; try { + // 路径解码后再次检查,避免编码后的双斜杠或反斜杠绕过站内限制。 let t = decodeURIComponent(e.split(/[?#]/, 1)[0] || ``); if (t.startsWith(`//`) || /[\\\u0000-\u0020\u007f]/.test(t)) return `/`; } catch { @@ -14,6 +16,7 @@ function e(e) { } return e; } +// 首页不需要额外 query;无效的 redirect 也会被归一为首页。 function t(t) { let n = e(t); return n === `/` @@ -22,6 +25,7 @@ function t(t) { redirect: n, }; } +// t 是调用方提供的登录/注册入口,n 是待校验回跳路径;整段编码后作为单一参数附加。 function n(t, n) { let r = e(n); return r === `/` ? t : `${t}?redirect=${encodeURIComponent(r)}`; diff --git a/app/lib/auth.js b/app/lib/auth.js index a7aab16..ae48b73 100644 --- a/app/lib/auth.js +++ b/app/lib/auth.js @@ -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(); diff --git a/app/lib/backend-menu.js b/app/lib/backend-menu.js index b2fa4a8..0349f18 100644 --- a/app/lib/backend-menu.js +++ b/app/lib/backend-menu.js @@ -1,3 +1,8 @@ +/** + * 后端菜单到前端路由的适配层:兼容旧页面地址、限制可用路由和参数,并生成导航分组。 + * 菜单可见性不是权限校验,所有实际操作仍须由后端按当前身份授权。 + */ +// 合并旧账单入口与财务入口;全树最多保留一个由 /bills 转换得到的财务项。 function e(e) { let t = (e) => e.some( @@ -36,6 +41,7 @@ function e(e) { } return r(e); } +/** 路径长度和匹配 query 数量决定激活优先级,避免父菜单与更具体子菜单同时高亮。 */ function t(e, t, n = []) { let r = t.path === `/products/cvm` @@ -54,6 +60,7 @@ function t(e, t, n = []) { let a = i(e); return a >= 0 && !n.some((e) => i(e.to) > a); } +// 只映射前端已实现的路由;旧版 .htm/插件入口必须显式登记,不透传任意路径。 var n = new Set( `/./products./products/cloud-server./products/dedicated-server./cart./cloud-servers./dedicated-servers./security/groups./security/ssh-keys./security/domains./transfer./finance./orders./bills./invoices./invoices/settings./invoices/request./recommend./vouchers./account./account/api-keys./account/connections./account/realname./messages./logbook./tickets./tickets/create./help./notices./news./resources`.split( `.`, @@ -99,6 +106,7 @@ var n = new Set( (e) => ![`/`, `/security/groups`, `/vouchers`, `/logbook`].includes(e), ), ); +// 图标由本地 Lucide 名称决定,不直接采用后端传入的任意图标实现。 function o(e) { let t = e.to?.split(`?`)[0]; return ( @@ -134,6 +142,7 @@ function o(e) { : `lucide:circle-help`) ); } +/** 根据服务器模块菜单纠正列表类型或补齐唯一模块 m;多模块时不擅自替用户选择。 */ function s(e, t) { if (![`/cloud-servers`, `/dedicated-servers`].includes(t.path)) return; let n = new Set(), @@ -168,6 +177,7 @@ function s(e, t) { }, }; } +// 把已规范化的后端菜单装入现有分组,账户子页和资源中心有固定归属,其他落到末组。 function c(e, t, n) { let r = e.map((e) => ({ ...e, @@ -199,6 +209,7 @@ function c(e, t, n) { } return r.filter((e) => e.items.length); } +// 遍历树生成面包屑;相同路径时选择包含更多匹配 query 的最具体节点。 function l(e, n) { let r = []; function i(e, a) { @@ -222,6 +233,7 @@ function l(e, n) { } return (i(e, []), r.sort((e, t) => t.score - e.score)[0]?.labels ?? []); } +// 使用后端菜单时移除已被接管的前端默认入口,避免同一功能重复出现。 function u(e, t) { let n = new Set(), r = (e) => @@ -250,6 +262,7 @@ function u(e, t) { })) .filter((e) => e.items.length); } +/** 外链只允许无用户名/密码的 HTTP(S) URL;控制字符和反斜杠直接拒绝。 */ function d(e) { if ( typeof e != `string` || @@ -264,6 +277,10 @@ function d(e) { return ``; } } +/** + * 规范化后端菜单树:最多三层、每层最多 100 项,文本最多 100 字。 + * 无法适配的菜单保留禁用原因而非猜测跳转;二次提醒 second_reminder 作为确认标记保留。 + */ function f(e, t = ``, a = 0) { if (!Array.isArray(e) || a > 2) return []; let o = e.slice(0, 100).flatMap((e, o) => { @@ -307,6 +324,7 @@ function f(e, t = ``, a = 0) { let [e, t = ``] = (h || u).split(`?`), n = new URLSearchParams(l.split(`?`)[1] ?? ``), r = new URLSearchParams(t); + // 只保留各页面明确支持且值域有效的单值 query,不能透传后端提供的任意参数。 for (let t of { "/bills": [`status`], "/orders": [`status`], @@ -351,6 +369,7 @@ function f(e, t = ``, a = 0) { return [s]; }), s = o.filter((e) => e.to === `/account`); + // 旧后台可能把多个入口映射到 /account;合并后仍保留任一入口要求的二次确认。 return s.length < 2 ? o : o.flatMap((e) => diff --git a/app/lib/bandwidth-display.js b/app/lib/bandwidth-display.js index 091eb10..8d0983a 100644 --- a/app/lib/bandwidth-display.js +++ b/app/lib/bandwidth-display.js @@ -1,3 +1,4 @@ +/** 商品文案统一显示“峰值 NMbps”;边界约束避免误改其他标识符或带符号的数值。 */ function e(e) { return e.replace( /(? t(e(n))); } +// 递归排序对象键并省略 undefined,使相同内容不受属性插入顺序影响。 function r(e) { return Array.isArray(e) ? e.map(r) @@ -32,12 +36,14 @@ function r(e) { function i(t) { return JSON.stringify(r(e(t))); } +// 编辑期间后端若删除了已保存的自定义字段,不能静默丢字段继续保存。 function a(e, t) { if (!e) return; let n = e.item.self_defined_field ?? {}; if (Object.keys(n).some((e) => !t.includes(e))) throw Error(`商品自定义字段已调整,请返回购物车删除该项后重新配置`); } +// 选中态只在内存中跨购物车/编辑页传递,不把商品配置长期写入浏览器存储。 var o = null; async function s(e) { let t = new TextEncoder().encode(i(e)); @@ -46,6 +52,7 @@ async function s(e) { (e) => e.toString(16).padStart(2, `0`), ).join(``); } +/** 记录离开购物车前各项指纹与选中位置;SHA-256 用于内容匹配,不承担身份认证。 */ async function c(e, t) { let n = [...t]; o = { @@ -53,6 +60,7 @@ async function c(e, t) { indices: n, }; } +/** 一次性消费选中态;相同商品按其重复出现次序匹配,避免全部映射到同一位置。 */ async function l(e) { let t = o; if (!t) return null; @@ -67,6 +75,10 @@ async function l(e) { return a === void 0 || r.has(a) ? [] : (r.add(a), [a]); }); } +/** + * 返回编辑保存函数:n 为 API 客户端,r 包含 position/item,u 回调检查报价是否仍有效。 + * 保存前重取购物车并比较原始项,提交 expected_item 供后端进一步检测并发变更。 + */ function u(n, r) { let a = r ? t(r.item) : null, c = !1; @@ -89,6 +101,7 @@ function u(n, r) { ...a, ...d, }); + // 成功后同步指纹,返回购物车时仍能识别刚编辑过的选中商品。 let p = o; return ( p && diff --git a/app/lib/cloud-instance-management.js b/app/lib/cloud-instance-management.js index c52b646..362e97b 100644 --- a/app/lib/cloud-instance-management.js +++ b/app/lib/cloud-instance-management.js @@ -1,4 +1,6 @@ +/** 云实例管理表单的协议校验:防御报价、订单、NAT、VPC 和磁盘规格。 */ var e = (e) => e === `mf_cloud` || e === `remf_cloud`; +// 空对象表示没有防御选项;非空配置必须给出合法的 defence 列表及当前防御值。 function t(e) { let t = () => { throw Error(`防御配置无效,请重试`); @@ -40,6 +42,7 @@ function t(e) { current: a, }; } +// 允许价格为 0,但不能把缺失值或空串转换成免费报价。 function n(e) { if ( e == null || @@ -50,6 +53,7 @@ function n(e) { throw Error(`未获得有效报价,请重试`); return String(e); } +// 未取得明确订单号时阻止进入支付;提示先核对订单,避免用户重复下单。 function r(e) { if ( (typeof e != `string` && typeof e != `number`) || @@ -58,6 +62,7 @@ function r(e) { throw Error(`订单编号未返回,请先在订单管理中检查,避免重复下单`); return e; } +/** web 转发只提交域名/内部端口;其他转发使用协议码,null 外部端口表示由后端分配。 */ function i(e, t) { if (!Number.isInteger(t.int_port) || t.int_port < 1 || t.int_port > 65535) throw Error(`内部端口应为 1–65535 的整数`); @@ -95,6 +100,7 @@ function i(e, t) { }), }; } +/** VPC 名称必填;网段可留空由后端处理,填写时须满足 IPv4 CIDR 及允许掩码。 */ function a(e, t, n = []) { if (!e.trim()) throw Error(`请输入 VPC 名称`); if (!t.trim()) @@ -117,6 +123,7 @@ function a(e, t, n = []) { ips: t.trim(), }; } +// 将磁盘档位转换为可选范围,并向内对齐步长;无合法容量区间的档位不展示。 function o(e, t = {}) { return e.flatMap((e, n) => { let r = e.type === `step` || e.type === `total`, @@ -151,6 +158,7 @@ function o(e, t = {}) { ]; }); } +// 同时检查整数、上下界和步长;浮点容差仅用于除法误差,不放宽容量边界。 function s(e, t) { return ( Number.isInteger(e) && diff --git a/app/lib/common-advanced-rules.js b/app/lib/common-advanced-rules.js index 9f94ec4..de0fd5f 100644 --- a/app/lib/common-advanced-rules.js +++ b/app/lib/common-advanced-rules.js @@ -1,3 +1,7 @@ +/** + * 通用商品高级联动规则:用条件项限制结果项,并同步修正不再可选的表单值。 + * 处理副本而非原始配置;无解、规则冲突或协议不完整时失败,不能返回看似合法的报价配置。 + */ var e = (e) => (e && typeof e == `object` && !Array.isArray(e) ? e : {}), t = (t) => (Array.isArray(t) ? t.map(e) : []), n = (e) => JSON.parse(JSON.stringify(e)), @@ -10,6 +14,7 @@ var e = (e) => (e && typeof e == `object` && !Array.isArray(e) ? e : {}), o = (e = `商品高级规则数据不完整`) => { throw Error(e); }; +// 只执行 enabled 规则;每条必须有 condition/result,且 combine_mode 与 operator 已知。 function s(n) { return n == null ? [] @@ -48,6 +53,7 @@ function s(n) { }) : o(); } +// 判断一个值是否命中规则:数量按闭区间比较,普通选项按正整数子项 id 比较。 function c(e, t, n) { return n.values.some((n) => { if (r(e)) @@ -66,6 +72,7 @@ function c(e, t, n) { return ((!Number.isSafeInteger(i) || i <= 0) && o(), t === i); }); } +// in 条件任一值命中即可,in 结果要求全部已选值合法;not_in 则不能有任一命中。 function l(e, t, n) { let r = a(t).map((t) => c(e, t, n)); return n.operator === `not_in` @@ -74,11 +81,13 @@ function l(e, t, n) { ? r.some(Boolean) : !!r.length && r.every(Boolean); } +// 操作系统的实际可选 id 位于 version 层,不能把系统分类 id 当成镜像 id。 function u(e) { return e.option_type === `os` ? t(e.subs).flatMap((e) => t(e.version).map((e) => Number(e.id))) : t(e.subs).map((e) => Number(e.id)); } +// 将实例既有配置转成规则可读的表单值;级联配置不参与普通项求值。 function d(e) { let n = {}; for (let o of e) { @@ -100,6 +109,7 @@ function d(e) { } return n; } +/** 限制一个结果项:离散值过滤 subs,数量值按原始步长切分合法区间,而非枚举每个整数。 */ function f(e, n, i) { if ( (e.option_type === `cascade` && o(`高级规则不能将级联项作为普通配置提交`), @@ -135,6 +145,7 @@ function f(e, n, i) { i = a + Math.floor((Number(t.qty_max) - a) / s) * s; if (!Number.isSafeInteger(r) || !Number.isSafeInteger(i) || i < r) return []; + // 用规则区间边界切段,保留相邻边界之间整段允许的数量,避免容量范围越大越慢。 let o = Math.floor((i - r) / s) + 1, l = [ ...new Set([ @@ -176,6 +187,7 @@ function f(e, n, i) { ((e.qty_min = Math.min(...l.map((e) => Number(e.qty_min)))), (e.qty_max = Math.max(...l.map((e) => Number(e.qty_max))))); } +// 选中值被规则排除时,回落到第一个合法值;无任何合法值时抛错,而非留下无效选择。 function p(e, n, s) { let c = i(e), l = String(e.option_name || c), @@ -208,6 +220,10 @@ function p(e, n, s) { } else t.includes(Number(d)) || (n[c] = t[0]); } } +/** + * r/a/c/u 分别为可编辑选项、表单、原始规则和实例固定配置。 + * 反复应用至表单稳定;重复状态表示循环冲突,固定配置不满足结果项时禁止继续。 + */ function m(r, a, c, u = []) { let m = r.map((t) => n(e(t))), h = s(c); diff --git a/app/lib/complete-list.js b/app/lib/complete-list.js index de3495c..4b96e4f 100644 --- a/app/lib/complete-list.js +++ b/app/lib/complete-list.js @@ -1,3 +1,7 @@ +/** + * 拉取完整关联列表:e(page, limit) 返回 {list, count?},t 判断调用上下文是否仍有效。 + * 按正整数 id 去重;总数缺失时以不足一页结束,无新增项时拒绝,避免无限分页。 + */ async function e(e, t = () => !0) { let n = [], r = new Set(); @@ -19,6 +23,7 @@ async function e(e, t = () => !0) { if (s === n.length) throw Error(`未能加载完整列表,请刷新重试`); } } +/** 按本次请求的每个 id 汇总批量关联结果;缺失/重复结果均不能被判作成功。 */ function t(e, t) { let n = ( Array.isArray(e) ? e : e && typeof e == `object` && `id` in e ? [e] : [] diff --git a/app/lib/composables-2.js b/app/lib/composables-2.js index a159c69..ad67c2f 100644 --- a/app/lib/composables-2.js +++ b/app/lib/composables-2.js @@ -1,2 +1,3 @@ +// 恢复模块保留的空函数导出;调用不产生副作用,不应在这里推断或补造原始实现。 var e = (e) => {}; export { e as t }; diff --git a/app/lib/configuration-display.js b/app/lib/configuration-display.js index cc22824..5ea1713 100644 --- a/app/lib/configuration-display.js +++ b/app/lib/configuration-display.js @@ -1,3 +1,5 @@ +/** 跨供应商规格展示适配;只统一显示标签与区域信息,不改写可提交的商品配置。 */ +// 类型码优先识别区域/系统,其余按现有中文同义词归一,未知名称保留。 function e(e, t) { let n = typeof e == `string` ? e.trim() : ``; return t === `area` || Number(t) === 12 @@ -24,6 +26,7 @@ function t(e) { ? `VPC 网络` : t; } +// 部分接口将配置包成 JSON 字符串;格式错误或数组不能当成配置对象使用。 function n(e) { if (typeof e == `string`) try { @@ -36,6 +39,7 @@ function n(e) { function r(e) { return typeof e == `string` ? e.trim() : ``; } +/** i 为模块名,a/o 为优先和后备配置;按模块字段差异提取 region/line/networkType。 */ function i(i, a, o) { let s = n(a), c = n(o), diff --git a/app/lib/content-route.js b/app/lib/content-route.js index c843d4d..c5dda64 100644 --- a/app/lib/content-route.js +++ b/app/lib/content-route.js @@ -1,11 +1,14 @@ +/** 内容页路由参数校验:id 必须是无前导零的正整数字符串,并处于 JS 安全整数范围。 */ function e(e) { if (typeof e != `string` || !/^[1-9]\d*$/.test(e)) return null; let t = Number(e); return Number.isSafeInteger(t) ? t : null; } +// 只接受当前前端实现的帮助、新闻、公告内容类型。 function t(e) { return e === `help` || e === `news` || e === `notices` ? e : null; } +// 通过后端启用的插件记录确定类型;未知/停用插件不生成内容路由。 function n(t, n) { let r = e(t); return r diff --git a/app/lib/dict.js b/app/lib/dict.js index fcf686c..ff99d2b 100644 --- a/app/lib/dict.js +++ b/app/lib/dict.js @@ -1,3 +1,4 @@ +/** 列表共用的实例/订单/交易状态文案和日期展示;未知状态保留原值便于识别新协议。 */ import { n as product_name_n } from "~/lib/product-name.js"; var t = { Unpaid: { @@ -79,6 +80,7 @@ var t = { function a(e) { return i[e] ?? e; } +// 订单优先展示脱敏后的商品名列表,缺失时回落到订单名称/描述。 function o(t) { let n = t.product_names; if (Array.isArray(n)) { @@ -102,12 +104,14 @@ function u(e) { function d(e) { return n[e]?.severity ?? `secondary`; } +// 时间戳由后端以秒提供,使用浏览器本地时区显示;0/空值按未提供处理。 function f(e) { if (!e) return `--`; let t = new Date(e * 1e3), n = (e) => String(e).padStart(2, `0`); return `${t.getFullYear()}-${n(t.getMonth() + 1)}-${n(t.getDate())} ${n(t.getHours())}:${n(t.getMinutes())}`; } +// 纯日期场景与完整日期时间共用同样的秒时间戳约定。 function p(e) { if (!e) return `--`; let t = new Date(e * 1e3), diff --git a/app/lib/finance-resource-chart.js b/app/lib/finance-resource-chart.js index f5fd6ac..c990520 100644 --- a/app/lib/finance-resource-chart.js +++ b/app/lib/finance-resource-chart.js @@ -1,8 +1,10 @@ +/** 财务模块监控数据转为图表百分比;非法或缺测值保留 null,不伪造零使用率。 */ function e(e) { if (typeof e != `number` && (typeof e != `string` || !e.trim())) return null; let t = Number(e); return Number.isFinite(t) ? t : null; } +/** 已是百分比的曲线直接归一;容量曲线按同一时间戳匹配总量与已用/空闲量后计算。 */ function t(t, n) { let r = t.list ?? [], i = t.label ?? [], @@ -21,6 +23,7 @@ function t(t, n) { data: e.map((e) => o(e.value)), }; } + // 不能按数组下标直接相除:不同曲线可能缺点,必须用时间戳对齐。 let c = i.findIndex((e) => /总|total|allocated/i.test(e)), l = c >= 0 ? c : 0, u = i.findIndex((e) => /空闲|可用|free|available/i.test(e)), diff --git a/app/lib/instance-data.js b/app/lib/instance-data.js index 592a3e9..8458b55 100644 --- a/app/lib/instance-data.js +++ b/app/lib/instance-data.js @@ -1,13 +1,16 @@ +/** 多供应商实例数据展示适配;缺失/无效数值用 null 表示,避免误显示为有效的 0。 */ function e(e) { if (e == null || e === ``) return null; let t = Number(e); return Number.isFinite(t) ? t : null; } +// 远控端口必须处于 TCP/UDP 有效范围,空值和非整数返回 null。 function t(t) { if (typeof t != `number` && typeof t != `string`) return null; let n = e(t); return n !== null && Number.isInteger(n) && n >= 1 && n <= 65535 ? n : null; } +// 兼容接口返回对象或序列化对象,格式不符合时返回空对象供上层做缺值展示。 function n(e) { if (typeof e == `string`) try { @@ -26,12 +29,14 @@ function r(e) { } return Array.isArray(e) ? e.filter((e) => e && typeof e == `object`) : []; } +/** 将秒、毫秒或日期字符串归一为 Unix 秒;超过 10^12 的数值按毫秒处理。 */ function i(t) { let n = e(t); if (n !== null) return n > 0xe8d4a51000 ? Math.floor(n / 1e3) : n || void 0; let r = Date.parse(String(t ?? ``).replace(` `, `T`)); return Number.isFinite(r) ? Math.floor(r / 1e3) : void 0; } +// 容量统一为 GB(1024 进制),无单位时使用 t;“不限”按既有展示协议返回 0。 function a(e, t = `GB`) { if (typeof e == `string` && /不限|unlimited/i.test(e)) return 0; let n = /^\s*(-?\d+(?:\.\d+)?(?:e[+-]?\d+)?)\s*([KMGT]?B)?\s*$/i.exec( @@ -50,6 +55,10 @@ function a(e, t = `GB`) { }[r] ?? 1); return Number.isFinite(i) ? i : null; } +/** + * t 为模块名,r 为实例详情,i 为实时流量/配额响应;区分总量、计量模式和小时统计。 + * kky_ecs 的完整小时数据不冒充本周期累计量;配额是否加载与配额是否存在单独表达。 + */ function o(t, r, i) { let o = n(r), s = n(i), @@ -134,6 +143,7 @@ function o(t, r, i) { (T = l.mode === `traffic`), d && (T = d === `traffic`)); } + // 只有非小时统计才允许由总量和剩余量互推;否则不同时间口径不可直接相减。 return ( !E && x !== null && @@ -173,6 +183,7 @@ function o(t, r, i) { } ); } +// 大容量按 GB/TB 展示;null 用破折号,而不是把缺失值当作零。 function s(e) { return e === null ? `—` @@ -180,6 +191,7 @@ function s(e) { ? `${(e / 1024).toFixed(2)} TB` : `${e.toFixed(2)} GB`; } +// 小流量继续降为 MB/KB/B,避免低于 1 GB 的真实用量全部显示成 0.00 GB。 function c(e) { return e === null ? `—` @@ -193,6 +205,7 @@ function c(e) { ? `${(e * 1024 ** 2).toFixed(2)} KB` : `${(e * 1024 ** 3).toFixed(0)} B`; } +/** 归一磁盘名称、容量和时间,保留 raw 供模块特有操作读取原始字段。 */ function l(t) { let o = n(t); return r(o.disk ?? o.disks ?? o.disk_list).map((t, n) => { @@ -211,6 +224,7 @@ function l(t) { }; }); } +// 镜像名称按接口字段优先级读取,最后使用财务模块 host_data 中的系统名。 function u(e) { let t = n(e), i = n(t.image), @@ -224,6 +238,7 @@ function u(e) { url: String(t.image_icon_url ?? i.icon_url ?? ``), }; } +// 操作日志存在多种字段命名;只归一数据,不在此层改变日志正文。 function d(e, t) { let r = n(e); return { diff --git a/app/lib/instance-status.js b/app/lib/instance-status.js index 3e66f81..34c8109 100644 --- a/app/lib/instance-status.js +++ b/app/lib/instance-status.js @@ -1,3 +1,4 @@ +/** 实例业务状态与电源状态的统一显示;暂停原因只在确实暂停时展示。 */ function e(e, t) { let n = typeof t == `string` && @@ -14,6 +15,7 @@ function e(e, t) { }[e.suspend_type || ``] || `暂停原因暂未提供,请联系支持`; } +// 业务状态优先于电源状态;只有 Active 实例才显示运行/关机等即时状态。 function t(e, t = `Active`, n = !1) { let r = { Unpaid: `未付款`, @@ -57,6 +59,7 @@ function t(e, t = `Active`, n = !1) { severity: `warn`, }; } +// 为已支持的模块选择状态接口;未知模块或非法实例编号返回 null,不拼接猜测路径。 function n(e, t) { if (!/^[1-9]\d*$/.test(String(t))) return null; let n = e.replace(/^mf_finance(?=$|_)/, `remf_finance`); @@ -83,6 +86,7 @@ function n(e, t) { } : null; } +/** e 为 API 客户端;仅返回字符串电源状态,缺少状态时返回空串供 UI 显示未知。 */ async function r(e, t, r) { let i = n(t, r); if (!i) return ``; diff --git a/app/lib/interval.js b/app/lib/interval.js index 932ff6e..b94b363 100644 --- a/app/lib/interval.js +++ b/app/lib/interval.js @@ -1,2 +1,3 @@ +// 恢复代码的定时器导出入口;调用方负责在组件卸载时 clearInterval,模块不管理生命周期。 var e = globalThis.setInterval; export { e as t }; diff --git a/app/lib/invoice.js b/app/lib/invoice.js index a835a99..6e25d4e 100644 --- a/app/lib/invoice.js +++ b/app/lib/invoice.js @@ -1,3 +1,7 @@ +/** + * 发票资料完整分页加载;e(page, 50) 返回 {list, count?},保留字符串形式的资料 id。 + * 去重后仍达不到 count 且本页没有新记录时失败,避免将不完整资料当成完整列表。 + */ async function e(e) { let t = [], n = new Set(); diff --git a/app/lib/modules.js b/app/lib/modules.js index ab6fe88..169dfcc 100644 --- a/app/lib/modules.js +++ b/app/lib/modules.js @@ -1,3 +1,4 @@ +/** 商品模块分类和操作系统图标选择;模块标识仍按后端协议保留,不作为面向会员的文案。 */ var e = { cloud: [ `mf_cloud`, @@ -17,6 +18,7 @@ var e = { ], common: [`idcsmart_common`, `mf_finance_common`], }; +/** 优先采用合法站内/HTTP(S)图片地址;否则从 e/n 的系统描述推断本地图标。 */ function t(e, t, n) { let r = String(e ?? ``).trim(); if (r && !/[\u0000-\u0020\u007f\\]/.test(r)) { @@ -27,6 +29,7 @@ function t(e, t, n) { return r; } catch {} } + // t 是恢复导出的保留参数;当前实现以 e 和 n 匹配系统别名,未知系统显示 Other。 let i = [ [`Windows`, [`windows`, `winserver`]], [`Ubuntu`, [`ubuntu`]], diff --git a/app/lib/navigation-state.js b/app/lib/navigation-state.js index f564347..df11268 100644 --- a/app/lib/navigation-state.js +++ b/app/lib/navigation-state.js @@ -1,3 +1,4 @@ +/** 按 auth store 实例共享导航加载状态,切换身份立即丢弃旧菜单及旧请求结果。 */ import { watch } from "vue"; import { ref } from "vue"; import { effectScope } from "vue"; @@ -28,6 +29,7 @@ function a(a, o) { }, ), ); + // e 为强制刷新;并发请求复用 p,已加载的菜单默认不重复请求。 function m(e = !1) { if (!a.isLoggedIn) return Promise.resolve(); if (p) return p; @@ -68,6 +70,7 @@ function a(a, o) { load: m, }; } +// WeakMap 不延长 store 生命周期;同一应用内多个导航消费者共享同一份响应式状态。 var o = new WeakMap(); function s(e, t) { let n = o.get(e); diff --git a/app/lib/oauth.js b/app/lib/oauth.js index 768fe94..5e35650 100644 --- a/app/lib/oauth.js +++ b/app/lib/oauth.js @@ -1,4 +1,6 @@ +/** OAuth 提供商列表与回调结果适配;跳转地址必须经过站内/HTTP(S)规则校验。 */ import { o as backend_menu_o } from "~/lib/backend-menu.js"; +// 仅接受合法提供商标识,标准化图片与绑定状态;不可信的任意协议地址不会直接使用。 function t(t) { return Array.isArray(t) ? t.flatMap((t) => { @@ -32,6 +34,10 @@ function n(t, n) { : backend_menu_o(t) : ``; } +/** + * 将轮询结果分为 login、bind、expired;t 为前端 origin,r 可为后端 origin。 + * 旧 oauth.htm 仅在可信 origin 上迁移为前端 /oauth/bind,不放行任意外站回跳。 + */ function r(e, t, r = t) { if (!e || typeof e != `object` || Array.isArray(e)) return { diff --git a/app/lib/order-image-continuation.js b/app/lib/order-image-continuation.js index 261d3bf..9be7e15 100644 --- a/app/lib/order-image-continuation.js +++ b/app/lib/order-image-continuation.js @@ -1,4 +1,6 @@ +/** 商品下单跨登录的镜像选择续接;Symbol 用作组件树注入键,不将状态暴露为全局字符串键。 */ var e = Symbol(`order-login-continuation`); +// 只接受正安全整数;字符串须为规范十进制形式,避免前导零或指数写法产生歧义。 function t(e) { if (typeof e != `number` && typeof e != `string`) return; let t = Number(e); @@ -17,6 +19,7 @@ function n(e) { !/[\\\u0000-\u0020\u007f]/.test(t) ); } +/** 校验站内回跳并替换 order_image;无效镜像参数被删除,无效路径统一回商品列表。 */ function r(e, r) { try { if ( diff --git a/app/lib/os-name.js b/app/lib/os-name.js index 73c901e..a91c7bc 100644 --- a/app/lib/os-name.js +++ b/app/lib/os-name.js @@ -1,3 +1,4 @@ +/** 镜像名称展示清理:统一系统品牌/版本分隔,移除文件扩展名和冗余的 64 位标记。 */ var e = { debian: `Debian`, ubuntu: `Ubuntu`, @@ -33,6 +34,7 @@ var e = { gpt: `GPT`, bt: `BT`, }; +// 仅改写已知系统前缀及可识别的版本词;不认识的尾部原样保留,避免丢失镜像特征。 function n(n) { let r = n.match( /^(Debian|Ubuntu|CentOS|Windows|Rocky(?:[ _-]+Linux)?|AlmaLinux|Fedora|openEuler|FreeBSD|TencentOS|Alpine(?:[ _-]+Linux)?)(?=$|[\d\s_-])/i, @@ -56,6 +58,7 @@ function n(n) { } return o.join(` `) + i; } +// 这是显示名称处理,不改变实际镜像 id;完全为空的优化版名称显示“默认镜像”。 function r(e) { if (typeof e != `string`) return ``; let t = e.trim(); diff --git a/app/lib/password-2.js b/app/lib/password-2.js index 3f22ee7..40a8615 100644 --- a/app/lib/password-2.js +++ b/app/lib/password-2.js @@ -1,3 +1,8 @@ +/** + * 生成便于辨认的随机密码建议:至少含小写、大写和数字各一位,排除易混字符。 + * 现有实现使用 Math.random(非密码学随机源),不能当作高强度密钥/令牌生成器; + * 即使 e 小于 3,三个必选字符也会保留。此处注释如实说明现有行为,不改变协议。 + */ function e(e = 12) { let t = (e) => e[Math.floor(Math.random() * e.length)], n = [ diff --git a/app/lib/product-description-data.js b/app/lib/product-description-data.js index 673dc61..850397e 100644 --- a/app/lib/product-description-data.js +++ b/app/lib/product-description-data.js @@ -1,3 +1,4 @@ +/** 优先使用商品已有描述;缺失时按商品 id 补查,最多等待 5 秒,失败不阻塞配置页。 */ async function e(e, t) { if (typeof t.description == `string`) return t.description; let n = Number(t.id); @@ -18,6 +19,7 @@ async function e(e, t) { r = Array.isArray(t?.list) ? t.list.find((e) => e && Number(e.id) === n) : void 0; + // 返回值仍是后端富文本,展示端必须经过 HTML 清洗,不能直接视为可信 HTML。 return typeof r?.description == `string` ? r.description : ``; } catch { return ``; diff --git a/app/lib/product-fields.js b/app/lib/product-fields.js index 0d425e1..7d2ff17 100644 --- a/app/lib/product-fields.js +++ b/app/lib/product-fields.js @@ -1,3 +1,4 @@ +/** 商品自定义字段协议适配、默认值与提交前校验;服务端仍负责最终字段与规则校验。 */ function e(e) { return e && typeof e == `object` && !Array.isArray(e) ? e : null; } @@ -7,6 +8,7 @@ function t(e) { function n(e) { return e === !0 || e === 1 || e === `1`; } +// 下拉选项兼容对象数组、纯值数组及逗号/换行分隔字符串,最终统一为 value/label。 function r(n) { return ( Array.isArray(n) @@ -33,6 +35,10 @@ function r(n) { ) .filter((e) => e.value !== ``); } +/** + * 将后端 PCRE 风格 regexpr 的可表达部分转换为 JS RegExp。 + * 不支持的高级构造标记 patternServerOnly,不能假装浏览器已经完成等价验证。 + */ function i(e) { let n = t(e).trim(); if (!n) return {}; @@ -44,6 +50,7 @@ function i(e) { "{": `}`, "<": `>`, }; + // 识别 /.../i、{...} 等包围符,跳过转义字符并处理成对分隔符的嵌套。 if (/[^a-zA-Z0-9\\\s\0]/.test(i)) { let e = a[i] ?? i, t = 1, @@ -82,6 +89,7 @@ function i(e) { patternFlags: s || void 0, patternServerOnly: !0, }; + // 逐字符翻译锚点、扩展空白模式和非捕获/非贪婪约定;字符组内不执行这些改写。 let c = ``, l = !1; for (let e = 0; e < n.length; e++) { @@ -140,6 +148,7 @@ function i(e) { patternFlags: s || void 0, }; } +/** 只保留当前组件支持的字段类型,id 去重;未知类型不生成一个错误的可提交控件。 */ function a(a) { let o = e(a), s = Array.isArray(a) ? a : Array.isArray(o?.data) ? o.data : [], @@ -180,11 +189,13 @@ function a(a) { } return c.filter((e, t, n) => n.findIndex((t) => t.id === e.id) === t); } +// explain 仅用于说明,不进入提交模型;其他类型初始为空,由控件和校验共同处理。 function o(e) { return Object.fromEntries( e.filter((e) => e.kind !== `explain`).map((e) => [e.id, (e.kind, ``)]), ); } +/** 逐字段检查必填、选项值和可本地执行的正则;布尔字段按后端约定归一为 "1"/空串。 */ function s(e, t) { for (let n of e) { if (n.kind === `explain`) continue; diff --git a/app/lib/product-name.js b/app/lib/product-name.js index 2d1f6e1..16190e0 100644 --- a/app/lib/product-name.js +++ b/app/lib/product-name.js @@ -1,9 +1,11 @@ +/** 商品展示名称去除内部 -L 后缀;此处不改商品 id,也不影响后端商品匹配。 */ function e(e) { return typeof e == `string` && /-L\s*$/.test(e); } function t(e) { return typeof e == `string` ? e.replace(/-L\s*$/, ``).trim() : ``; } +// 在文本中仅替换已知商品全名,先按长度降序并转义正则,避免短名误伤长名或标识符。 function n(n, r) { if (!n) return ``; let i = [...new Set(r.filter((t) => e(t)).map((e) => e.trim()))].sort( diff --git a/app/lib/product-order-module.js b/app/lib/product-order-module.js index 7a562dd..090cd09 100644 --- a/app/lib/product-order-module.js +++ b/app/lib/product-order-module.js @@ -1,9 +1,11 @@ +/** 下单前确认真实配置模块;本地 DCIM 与转售 DCIM 使用不同配置组件,不能仅看商品名。 */ function e(e) { let t = typeof e.module == `string` ? e.module.trim() : ``, n = typeof e.res_module == `string` ? e.res_module.trim() : ``; if (t === `mf_dcim`) return `mf_dcim`; if (t === `remf_dcim` || (!t && n === `mf_dcim`)) return `remf_dcim`; } +// 已知非 mf_cloud 模块可直接使用;缺失/含糊时读取配置接口,取不到模块就明确失败。 async function t(t, n) { let r = typeof n.module == `string` ? n.module.trim() : ``, i = e(n); diff --git a/app/lib/purchase-limit-state.js b/app/lib/purchase-limit-state.js index b24051f..7eddbce 100644 --- a/app/lib/purchase-limit-state.js +++ b/app/lib/purchase-limit-state.js @@ -1,3 +1,4 @@ +/** 购买页限购检查的响应式状态;序号隔离连续修改数量引发的乱序返回。 */ import { ref } from "vue"; function t(t) { let n = ref(!1), @@ -5,15 +6,18 @@ function t(t) { i = ref(!1), a = ref(``), o = 0; + // 配置变化时重置并作废在途响应,不能沿用上一份 allowed 状态。 function s() { (o++, (n.value = !1), (r.value = !1), (i.value = !1), (a.value = ``)); } + // t 为真实限购请求函数,e 为当前购买信息;只有最后一次请求能更新页面状态。 async function c(e) { let s = ++o; ((n.value = !0), (r.value = !1), (i.value = !1), (a.value = ``)); try { let n = await t(e); if (s !== o) return; + // checked 代表检查已结束,不代表允许购买;协议异常时 allowed 继续保持 false。 if ( !n || typeof n != `object` || diff --git a/app/lib/remote-console.js b/app/lib/remote-console.js index 677d050..a8d5fa1 100644 --- a/app/lib/remote-console.js +++ b/app/lib/remote-console.js @@ -1,3 +1,4 @@ +/** 从实例页打开前端远控页;允许模块显式列举,网关会话仍由远控页和后端建立。 */ var e = new Set([ `kky_ecs`, `mf_cloud`, @@ -9,12 +10,14 @@ var e = new Set([ `remf_finance_common`, `idcsmart_common`, ]); +// 实例编号限制为最多十位的正整数字符串,拒绝任意路径/查询片段注入。 function t(t, n) { let r = String(t); if (!/^[1-9]\d{0,9}$/.test(r) || !e.has(n)) throw Error(`实例信息无效,请刷新后重试`); return `/remote-console?hostId=${r}&module=${n}`; } +// 新窗口不保留 opener,且不附带 Referer;这里不传递 JWT 或远控密码到 URL。 function n(e, n) { window.open(t(e, n), `_blank`, `noopener,noreferrer`); } diff --git a/app/lib/renewal-quote.js b/app/lib/renewal-quote.js index d987e14..b96cfcc 100644 --- a/app/lib/renewal-quote.js +++ b/app/lib/renewal-quote.js @@ -1,9 +1,11 @@ +/** 续费周期显示与报价重试规则;只允许对可恢复的临时报错重试一次,不重复提交续费订单。 */ function e(e) { return (e ?? []).map((e) => ({ ...e, name_show: e.name_show?.trim() || e.billing_cycle, })); } +// 网络/超时/限流/部分服务异常属于可恢复报价失败,其他业务拒绝应直接交给用户处理。 function t(e) { if (!e || typeof e != `object` || !(`status` in e)) return !1; let t = e.status; @@ -23,6 +25,7 @@ function n(e) { ? e.message : `获取续费信息失败`; } +/** e 为取价操作,n 判断当前报价上下文是否仍有效;失效时不自动发第二次请求。 */ async function r(e, n) { try { return await e(); diff --git a/app/lib/rolldown-runtime.js b/app/lib/rolldown-runtime.js index 4fed41f..d267e70 100644 --- a/app/lib/rolldown-runtime.js +++ b/app/lib/rolldown-runtime.js @@ -1,3 +1,8 @@ +/** + * 恢复代码依赖的 CJS/ESM 互操作辅助,不包含具体业务。 + * o 延迟且仅初始化一次 CommonJS 模块;s/c/l 通过 getter 保留导出的读取语义。 + * u 仅在宿主提供 require 时使用它,浏览器中不支持的外部 require 会明确报错。 + */ var e = Object.create, t = Object.defineProperty, n = Object.getOwnPropertyDescriptor, diff --git a/app/lib/sanitize.js b/app/lib/sanitize.js index 8d8c8fe..1b4c409 100644 --- a/app/lib/sanitize.js +++ b/app/lib/sanitize.js @@ -1,3 +1,7 @@ +/** + * 在浏览器 DOM 中重新构造允许的富文本,不直接信任后端商品/内容 HTML。 + * 白名单外的普通容器展开子内容;script、表单和非 HTML 命名空间整段丢弃。 + */ var e = new Set( `a.p.br.hr.div.span.h1.h2.h3.h4.h5.h6.b.strong.i.em.u.s.del.ins.sub.sup.small.mark.blockquote.pre.code.kbd.ul.ol.li.dl.dt.dd.table.thead.tbody.tfoot.tr.th.td.caption.colgroup.col.img.figure.figcaption`.split( `.`, @@ -30,6 +34,7 @@ var e = new Set( `video`, `source`, ]); +// 图片额外允许有限位图类型的 base64 data URL;普通链接可用 mailto/tel,不允许脚本协议。 function n(e, t = !1) { if (!e || /[\u0000-\u001f\u007f]/.test(e)) return !1; if ( @@ -48,6 +53,7 @@ function n(e, t = !1) { return !1; } } +/** 清洗标签和属性,只保留有限排版;新窗口链接强制添加 noopener noreferrer。 */ function r(r) { if (!r) return ``; let i = new DOMParser().parseFromString(r, `text/html`), @@ -103,6 +109,7 @@ function r(r) { for (let e of Array.from(i.body.childNodes)) o(e, a); return a.innerHTML; } +// 对历史双重实体编码最多解码两次;返回值只是解码文本,不能代替上面的 HTML 清洗。 function i(e) { if (!e) return ``; let t = e; diff --git a/app/lib/server-page.js b/app/lib/server-page.js index 785d8de..51ab295 100644 --- a/app/lib/server-page.js +++ b/app/lib/server-page.js @@ -1,7 +1,13 @@ +/** + * 服务器列表分类适配:优先使用后端 classified-v1 分页,旧协议才完整拉取后在前端分类。 + * 分类结果和请求只在当前工厂实例内缓存,invalidate 作废在途结果对应的代次。 + */ +// 这些既有通用商品在当前业务中按独立服务器展示;变更须与商品配置约定一起核对。 var e = new Set([3997, 5174, 5226, 5227, 5631, 5914, 6243]); function t(t) { return t.module === `idcsmart_common` && e.has(Number(t.product_id)); } +/** e 为 API,n 为目标模块集合,r 可选后端菜单 id;有菜单时由 /home/host 决定分类。 */ function n(e, n, r) { if (r !== void 0 && (!Number.isSafeInteger(r) || r <= 0)) throw Error(`服务器导航参数无效`); @@ -20,6 +26,7 @@ function n(e, n, r) { f, p, m = new Map(); + // 相同参数的正在进行请求共用 Promise;完成即释放,不把分页响应无限期存储。 function h(t) { let i = r === void 0 @@ -52,6 +59,7 @@ function n(e, n, r) { } return o; } + // 通用模块需读取实例详情确认实际模块;同商品共用分类结果,缺 product_id 时按实例区分。 async function g(t, n) { let a = t.product_id || -t.id; if (c.has(a)) return c.get(a); @@ -68,6 +76,7 @@ function n(e, n, r) { l.get(a) === o && l.delete(a); } } + /** 旧协议完整加载列表并去重,最多四个并发详情请求补齐分类,再按目标模块筛选。 */ async function _(n, o, c) { let l = [], u = {}, @@ -142,6 +151,7 @@ function n(e, n, r) { r === void 0 ? l.filter((e) => (t(e) ? a.has(`mf_dcim`) : a.has(i(e.module || ``)))) : l; + // 跨分类全量结果按自然中文/数字顺序排序后再分页,避免每页内部排序冒充全局排序。 if (n.orderby === `dedicate_ip` || n.orderby === `name`) { let e = n.orderby; b.sort( @@ -156,6 +166,7 @@ function n(e, n, r) { list: b, }; } + /** 对外列表加载入口;探测后端能力后选择直接分页或 30 秒的完整分类缓存。 */ async function v(t) { if (!o) return e.get(`/host`, t); let n = Object.fromEntries( @@ -212,6 +223,7 @@ function n(e, n, r) { Number(s.count) === s.list.length && (a = s)); } + // 老协议缓存最多四组筛选条件;加载中共用 Promise,失败移除,成功后才开始 30 秒 TTL。 let s = Date.now(); for (let [e, t] of u) t.expires <= s && u.delete(e); let c = u.get(r); @@ -245,6 +257,7 @@ function n(e, n, r) { }; } return Object.assign(v, { + // 常规失效保留商品分类;参数为 true 时连分类缓存也清空(例如商品/模块归属已变化)。 invalidate(e = !1) { (d++, u.clear(), m.clear(), (p = void 0), e && (c.clear(), l.clear())); }, diff --git a/app/lib/storage-capacity.js b/app/lib/storage-capacity.js index 0552382..8a09bcb 100644 --- a/app/lib/storage-capacity.js +++ b/app/lib/storage-capacity.js @@ -1,5 +1,10 @@ +/** + * 财务/通用商品配置适配与容量文案解析:将数字类型码转换为前端可编辑模型。 + * 数据不完整时明确失败,不能用空选项、零库存或默认价格掩盖后端配置错误。 + */ import { n as product_fields_n } from "~/lib/product-fields.js"; import { t as bandwidth_display_t } from "~/lib/bandwidth-display.js"; +// 类型码来自既有商品接口:n 为数量类,r 为离散选择类(含级联根)。 var n = new Set([4, 7, 9, 11, 14, 15, 16, 17, 18, 19]), r = new Set([1, 2, 6, 8, 10, 13, 20]); function i(e) { @@ -14,6 +19,7 @@ function o(e) { function s(e) { throw Error(`${e},请刷新重试或联系客服`); } +// 数值不能靠 Number("") 得到伪造的 0;同时校验有限值和业务下界。 function c(e, t, n = 0) { ((typeof e != `number` && typeof e != `string`) || String(e).trim() === ``) && s(t); @@ -27,6 +33,7 @@ function l(e) { function u(e, t = `商品配置数据不完整`) { return (Array.isArray(e) || s(t), e); } +// 至少一个选项且 value 唯一,避免控件无法区分不同条目。 function d(e) { return ( (!e.length || new Set(e.map((e) => e.value)).size !== e.length) && @@ -34,6 +41,7 @@ function d(e) { e ); } +// 仅清理区域文案中的“第一区/1区”等展示后缀,不改区域 id。 function f(e) { let t = `(?:第\\s*)?(?:[一二三四五六七八九十百零〇两]+|[0-90-9]+)\\s*区`; return e @@ -43,6 +51,7 @@ function f(e) { .replace(/^[\s\-—_/·]+|[\s\-—_/·]+$/g, ``) .trim(); } +/** 展平系统版本、区域嵌套或普通子选项;返回统一 value/label,系统额外保留 group。 */ function p(e, n) { if (n === 5) { let t = i(e.sub); @@ -95,6 +104,10 @@ function p(e, n) { }), ); } +/** + * 转换一个配置项:布尔、系统、选择、级联或数量。 + * 数量上下界与各子区间求交,保留不连续 ranges;最大值为 0 的禁用数量项不展示。 + */ function m(e) { let t = i(e); t || s(`商品配置数据不完整`); @@ -171,6 +184,7 @@ function m(e) { d ); } +// 主机名/密码分别带 show 和 modify,展示与是否允许修改是两个独立开关。 function h(e, t) { let n = i(e); return { @@ -179,6 +193,7 @@ function h(e, t) { value: a(n?.[t]), }; } +/** 转换下单自定义字段,复用 PCRE 适配;不支持类型、空下拉选项或重复 id 均报错。 */ function g(t) { if (t == null) return []; let n = u(t, `自定义字段配置不完整`).map((t) => { @@ -227,6 +242,10 @@ function g(t) { n ); } +/** + * 商品配置的总入口:兼容业务数据和 data 包装,生成选项、周期、库存及联动定义。 + * stock 为 null 表示未启用库存控制,不能与真实的 0 库存混淆。 + */ function _(e) { let t = i(e), n = i(t?.product) ? t : i(t?.data), @@ -270,6 +289,7 @@ function _(e) { } ); } +/** 将 seq/sneq 联动解析为本地模型;只保留当前配置项存在且选项有效的完整规则。 */ function v(e, t) { let n = new Map(t.map((e) => [e.id, e])); function r(e) { @@ -332,12 +352,14 @@ function v(e, t) { ]; }); } +// seq 表示命中集合/闭区间,sneq 表示其补集;供条件判断和结果过滤共用。 function y(e, t) { let n = e.values.includes(t) || e.ranges.some((e) => t !== void 0 && t >= e.min && t <= e.max); return e.relation === `seq` ? n : !n; } +/** 当前表单 n 激活规则 t 后,求出选项 e 的可用交集;不会直接修改输入选项。 */ function b(e, t, n) { let r = t.filter((e) => y(e, n[e.optionId])).flatMap((e) => e.results); return e.map((e) => { @@ -357,6 +379,7 @@ function b(e, t, n) { ]; for (let e of t) { let t = []; + // 在规则边界按原步长分段,而非逐容量枚举;多条结果规则顺序求交。 for (let r of n) { let n = [ ...new Set([ @@ -395,6 +418,7 @@ function b(e, t, n) { }; }); } +/** 解析级联接口的子配置与默认选择,校验 id 不与根/其他子项重复,默认值必须可选。 */ function x(e, t) { let n = u( i(u(e, `商品级联配置数据不完整`)[0])?.son, @@ -423,6 +447,7 @@ function x(e, t) { values: r, }; } +// 容量单位保留 B 与 iB 的文案区别,不在此处擅自做十进制/二进制容量换算。 function S(e) { let t = /^([kmgtpe])(?:i?b)?$|^(b)$/i.exec(e.trim()); return t @@ -431,6 +456,10 @@ function S(e) { : `${t[1].toUpperCase()}${/ib$/i.test(e.trim()) ? `iB` : `B`}` : ``; } +/** + * 从单一磁盘规格文案提取 size/unit/type;多容量、乘数、价格或未知残留词均返回 null。 + * t 仅在原文缺单位时作后备,NFKC 先统一全角字符,避免宽松匹配造成错误容量展示。 + */ function C(e, t = ``) { let n = e.normalize(`NFKC`).trim(); if (!n || /[×*¥¥$€]|(?:^|[\s([{])[-+]\s*\d/.test(n)) return null; diff --git a/app/lib/ticketAttachments.js b/app/lib/ticketAttachments.js index 145ccb1..da71c33 100644 --- a/app/lib/ticketAttachments.js +++ b/app/lib/ticketAttachments.js @@ -1,3 +1,5 @@ +/** 工单附件展示适配;文件名、存储名与可访问 URL 分开,不把存储键直接当链接。 */ +// 允许 HTTP(S) 和显式相对路径,拒绝控制字符、反斜杠及脚本/data 等协议。 function e(e) { if ( !/^(?:https?:\/\/|\/(?!\/)|\.\.?\/)/i.test(e) || @@ -11,6 +13,7 @@ function e(e) { return !1; } } +// 仅在地址合法且文件名为支持的位图扩展名时启用图片预览,SVG 不走内嵌预览。 function t(t) { return ( !!t.href && @@ -18,6 +21,7 @@ function t(t) { /\.(?:avif|bmp|gif|ico|jpe?g|png|webp)$/i.test(t.name) ); } +// 历史存储名使用 ^ 分隔前缀,展示时取末段;URI 解码失败仍保留可读的原名。 function n(e, t) { let n = e @@ -30,6 +34,7 @@ function n(e, t) { } catch {} return n.split(`^`).at(-1) || t; } +/** 兼容字符串及对象附件;无地址仍保留名称/存储名和明确占位,不伪造下载链接。 */ function r(t) { return Array.isArray(t) ? t.map((t, r) => { diff --git a/app/lib/useCaptcha.js b/app/lib/useCaptcha.js index 6e10605..aa15c33 100644 --- a/app/lib/useCaptcha.js +++ b/app/lib/useCaptcha.js @@ -1,3 +1,7 @@ +/** + * 登录/注册等场景的人机验证状态:先读站点开关,再识别图片或极验提供商。 + * /captcha API 走统一后端 origin,但图片刷新 /captcha/tp_captcha/... 始终走前端同源反代。 + */ import { onMounted } from "vue"; import { watch } from "vue"; import { onScopeDispose } from "vue"; @@ -6,6 +10,7 @@ import { ApiError } from "~/lib/auth.js"; import { useApi } from "~/lib/auth.js"; import { t as useSiteConfig_t } from "~/lib/useSiteConfig.js"; var s = (e) => e === !0 || e === 1 || e === `1`; +// 登录可由全局开关或后端“失败三次后验证”状态开启,其他场景读取各自开关。 function c(e, t) { return s(e?.[`captcha_client_${t}`]) ? !0 @@ -13,6 +18,7 @@ function c(e, t) { s(e?.captcha_client_login_error) && s(e?.captcha_client_login_error_3_times); } +/** 只解析 HTML 中的提供商标识,不执行其中脚本;未知插件或异常极验 id 明确报错。 */ function l(e) { if (typeof e != `string` || e.length > 262144) throw Error(`验证码响应无效`); let t = new DOMParser().parseFromString(e, `text/html`), @@ -39,6 +45,7 @@ function l(e) { }; throw Error(`当前验证码插件暂不受支持,请联系管理员`); } +// 极验四个结果字段均需非空且有长度上限;真实挑战仍由服务端校验。 function u(e) { if (!e || typeof e != `object`) return !1; let t = e; @@ -47,6 +54,7 @@ function u(e) { typeof t[e] == `string` && t[e].trim().length > 0 && t[e].length <= 8192, ); } +/** d 为场景名;无场景时由调用方手动控制加载,组件销毁会取消请求并清空凭证。 */ function d(d) { let f = useSiteConfig_t(), p = useApi(), @@ -63,9 +71,11 @@ function d(d) { w = 0, T = null, E = !1; + // 每次加载或失效都清掉旧答案和 token,避免把不同验证码的结果混在一起提交。 function D() { ((y.value = ``), (v.value = ``)); } + // 序号 w 和销毁标记 C 防止旧响应回填;定时 AbortController 限制同源图片请求。 async function O() { if (C || b.value) return; let e = ++w; @@ -117,6 +127,7 @@ function d(d) { (clearTimeout(n), !C && e === w && (b.value = !1)); } } + // 仅在当前确实需要极验且没有加载时接受完成结果,按后端约定序列化四个字段。 function k(e) { (D(), !C && @@ -132,6 +143,7 @@ function d(d) { })), (v.value = e.lot_number))); } + // 提交前将“配置未加载”“正在加载”“未完成”等情况区分,空串才表示可继续。 function A() { return S.value ? `验证码设置正在加载,请稍候` @@ -153,6 +165,7 @@ function d(d) { : `请先重新加载图形验证码` : ``; } + // 后端报错可临时强制开启验证码,刷新后再由页面让用户重试原动作,不自动重复登录。 async function j(e) { let t = e instanceof ApiError && s(e.data?.captcha); return (t && ((E = !0), (m.value = !0)), m.value && (await O()), t); @@ -178,6 +191,7 @@ function d(d) { f.config.value || (x.value = `验证码设置加载失败,请刷新页面重试`))); })), + // 组件离开后即使不可取消的 API 请求返回,也不能继续写入当前验证码状态。 onScopeDispose(() => { ((C = !0), (w += 1), T?.abort(), D()); }), diff --git a/app/lib/useListPage.js b/app/lib/useListPage.js index d8db333..7679ad4 100644 --- a/app/lib/useListPage.js +++ b/app/lib/useListPage.js @@ -1,3 +1,7 @@ +/** + * 列表页面共用加载器:分页、同请求去重、身份隔离、缓存首屏与刷新失败保留旧结果。 + * 缓存来自统一 auth 模块;旧查询/旧账号/已卸载组件的响应不能覆盖当前列表。 + */ import { onMounted } from "vue"; import { watch } from "vue"; import { onScopeDispose } from "vue"; @@ -8,6 +12,7 @@ import { useApi } from "~/lib/auth.js"; import { writeApiCache } from "~/lib/auth.js"; import { readApiCache } from "~/lib/auth.js"; import { clearApiCache } from "~/lib/auth.js"; +/** u 为接口路径,d 可提供查询参数、外部 loader、行键和缓存命名空间等现有列表选项。 */ function u(u, d = {}) { let f = useApi(), p = @@ -26,6 +31,7 @@ function u(u, d = {}) { w = !0, T = !1, E = null; + // 身份变更或查询构造失败时统一作废在途序号,清理可见数据,防止跨账号残留。 function D() { (S++, (h.value = []), @@ -50,6 +56,7 @@ function u(u, d = {}) { onScopeDispose(() => { ((T = !0), S++, (E = null)); })); + // 优先使用业务行键,否则使用 id;没有稳定键的行不做对象复用。 function O(e) { if (d.rowKey) return d.rowKey(e); if ( @@ -60,6 +67,7 @@ function u(u, d = {}) { ) return e.id; } + /** 构造查询快照、读取一次初始缓存,再发真实请求刷新;同参数同身份共享 Promise。 */ function k() { if (T) return Promise.resolve(); let e, @@ -103,6 +111,7 @@ function u(u, d = {}) { try { let r = d.loader ? await d.loader(e) : await f.get(u, e); if (!o()) return; + // 复用同 id 的行对象以保留 UI 引用,但删除新响应已不存在的字段,避免旧值残留。 let i = new Map( h.value.flatMap((e) => { let t = O(e); @@ -133,6 +142,7 @@ function u(u, d = {}) { A, )); } catch (e) { + // 只有同一查询已拥有结果时才可保留旧列表;新筛选失败不能继续展示上一查询。 if (!o()) return; ((v.value = e instanceof Error ? e.message : `加载失败`), (!m || !l) && ((h.value = []), (g.value = 0), (y.value = !1))); @@ -151,6 +161,7 @@ function u(u, d = {}) { j ); } + // PrimeVue 页码从 0 开始,后端接口从 1 开始;切筛选的 reload 则回到第一页。 function A(e) { ((b.value = e.page + 1), (x.value = e.rows), k()); } diff --git a/app/lib/useMailbox.js b/app/lib/useMailbox.js index 3fc6ade..a93c4d1 100644 --- a/app/lib/useMailbox.js +++ b/app/lib/useMailbox.js @@ -1,3 +1,4 @@ +/** 顶栏站内信摘要共享状态;切换账号同步清空,旧账号或较早刷新结果不得覆盖新状态。 */ import { watch } from "vue"; import { useState } from "#app"; import { useApi } from "~/lib/auth.js"; @@ -25,6 +26,7 @@ function i() { flush: `sync`, }, ); + // 接口现有摘要位于 credit_limit 字段;失败保留本会话既有摘要,不中断页面主流程。 async function d() { if (!l.token) return; let e = l.token, diff --git a/app/lib/useOAuth.js b/app/lib/useOAuth.js index 200639f..3b1f0e9 100644 --- a/app/lib/useOAuth.js +++ b/app/lib/useOAuth.js @@ -1,3 +1,4 @@ +/** OAuth 授权发起器:校验提供商和返回 URL,再整页跳转;不在前端构造第三方授权参数。 */ import { onBeforeUnmount } from "vue"; import { ref } from "vue"; import { useApi } from "~/lib/auth.js"; @@ -10,6 +11,7 @@ function i() { onBeforeUnmount(() => { s = !1; }); + // busy 防止重复发起,卸载标记防止用户离开页面后被迟到的授权响应再次跳走。 async function c(e) { if (s && !a.value) { if (((o.value = ``), !/^[a-z][a-z0-9_]*$/i.test(e))) { @@ -23,6 +25,7 @@ function i() { let n = oauth_r(t?.url, window.location.origin); if (!n) throw Error(`授权地址无效,请联系支持`); try { + // 仅记录当前标签页曾发起授权,不存储 OAuth 凭证或 JWT。 sessionStorage.setItem(`console-oauth-pending`, `1`); } catch {} window.location.assign(n); diff --git a/app/lib/useOfficialSiteContent.js b/app/lib/useOfficialSiteContent.js index a8ae059..8a91ed3 100644 --- a/app/lib/useOfficialSiteContent.js +++ b/app/lib/useOfficialSiteContent.js @@ -1,5 +1,7 @@ +/** 官网 CMS 内容状态与安全展示辅助;默认文案是页面占位,不是实时资源/业绩数据。 */ import { useState } from "#app"; import { useApi } from "~/lib/auth.js"; +// 默认评价隐藏,未填写的联系方式/备案不虚构;实际内容由 CMS 接口覆盖。 var n = { meta: { title: `破碎工坊网络 - 云计算与基础设施服务`, @@ -342,6 +344,7 @@ var n = { function r(e) { return typeof e == `object` && !!e && !Array.isArray(e); } +// 对象递归合并缺省值,数组整体替换,避免 CMS 删除条目后又被默认数组补回。 function i(e, t) { if (Array.isArray(e)) return Array.isArray(t) ? t : e; if (!r(e)) return t === void 0 ? e : t; @@ -352,12 +355,14 @@ function i(e, t) { for (let [t, r] of Object.entries(n)) a[t] = t in e ? i(e[t], r) : r; return a; } +// useState 初始化时使用深拷贝,页面编辑不会修改模块内的默认文案。 function a() { return JSON.parse(JSON.stringify(n)); } function o(e) { return i(n, e); } +// 链接允许站内路径、锚点、HTTP(S)/mailto/tel;未知协议和反斜杠回落为不可跳转的 #。 function s(e) { if (typeof e != `string`) return `#`; let t = e.trim(); @@ -367,6 +372,7 @@ function s(e) { ? t : `#`; } +// 图片来源比链接更严格,不允许 mailto/tel、data 或协议相对 URL。 function c(e) { if (typeof e != `string`) return ``; let t = e.trim(); @@ -376,14 +382,17 @@ function c(e) { ? t : ``; } +// 主题颜色只接受六位十六进制值,非法 CMS 值使用调用方给出的默认色。 function l(e, t) { return typeof e == `string` && /^#[0-9a-fA-F]{6}$/.test(e) ? e : t; } +// 一般加载共享在途 Promise;显式强制刷新可重新请求,失败保留默认/既有内容。 var u = null; function d() { let n = useState(`official-site-content`, a), r = useState(`official-site-content-loaded`, () => !1), i = useState(`official-site-content-error`, () => ``); + // loaded 表示本轮尝试已结束(含失败);错误单独暴露,避免布局挂载后无限重试。 async function s(e = !1) { return r.value && !e ? n.value diff --git a/app/lib/useSiteConfig.js b/app/lib/useSiteConfig.js index 121630f..9a958ad 100644 --- a/app/lib/useSiteConfig.js +++ b/app/lib/useSiteConfig.js @@ -1,3 +1,4 @@ +/** 公共站点配置:全应用共享成功结果和在途请求,失败可重试,页面错误文案各自维护。 */ import { ref } from "vue"; import { computed } from "vue"; import { useState } from "#app"; @@ -6,6 +7,7 @@ var i = null; function a() { let a = useState(`site-config`, () => null), o = ref(``); + // /common 控制验证码、充值、资料锁定等前端入口;这些开关不替代后端权限校验。 async function s() { if (((o.value = ``), a.value)) return a.value; i ||= useApi() @@ -25,6 +27,7 @@ function a() { ); } } + // 后端开关可能是布尔、数字或字符串,仅把明确的 1/true 当成开启。 let c = (e) => e === 1 || e === `1` || e === !0; return { config: a, diff --git a/app/lib/useTheme.js b/app/lib/useTheme.js index 045c80d..a6a706d 100644 --- a/app/lib/useTheme.js +++ b/app/lib/useTheme.js @@ -1,5 +1,7 @@ +/** 浅色/深色/跟随系统三态主题,使用共享 Nuxt 状态与 html.app-dark 驱动组件和原生控件。 */ import { computed } from "vue"; import { useState } from "#app"; +// 存储不可用或值未知时安全回落 auto,不让浏览器隐私限制阻断页面初始化。 function n(e) { try { let t = (e ?? globalThis.localStorage)?.getItem(`theme`); @@ -28,6 +30,7 @@ function s() { document.documentElement.classList.contains(`app-dark`), ), l = () => window.matchMedia(`(prefers-color-scheme: dark)`); + // 仅 auto 读取系统偏好;显式 light/dark 不受系统变化影响。 function u() { c.value = s.value === `dark` || (s.value === `auto` && l().matches); } @@ -43,12 +46,14 @@ function s() { m = () => { s.value === `auto` && f(); }; + // init 前先移除旧监听,dispose 供应用入口卸载时释放媒体查询监听。 function h() { (p?.removeEventListener(`change`, m), (p = null)); } function g() { (h(), (s.value = n()), f(), (p = l()), p.addEventListener(`change`, m)); } + // 调用方传入已知模式;持久化失败不影响当前页面切换。 function _(e) { s.value = e; try { diff --git a/app/lib/useVerifyCode.js b/app/lib/useVerifyCode.js index 87069fa..c77c79b 100644 --- a/app/lib/useVerifyCode.js +++ b/app/lib/useVerifyCode.js @@ -1,3 +1,4 @@ +/** 邮件/手机验证码发送:复用人机验证,成功后倒计时,组件销毁清理定时器。 */ import { onScopeDispose } from "vue"; import { ref } from "vue"; import { useApi } from "~/lib/auth.js"; @@ -11,6 +12,7 @@ function i(i) { function u() { c &&= (clearInterval(c), null); } + // 默认为 60 秒;重复启动先清旧定时器,防止多次递减造成提前结束。 function d() { l || (u(), @@ -22,6 +24,7 @@ function i(i) { onScopeDispose(() => { ((l = !0), u()); }); + /** e 为 email 或手机场景,t 为收件账号,n 为账号缺失提示;发送中/倒计时中直接忽略。 */ async function f(e, t, n) { if (l || o.value || s.value > 0) return; if (((i.error.value = ``), !t)) { @@ -58,6 +61,7 @@ function i(i) { phone: t, }); } + // 发送成功才开始倒计时;已消费的人机验证失效/刷新,不能在下一次发送时复用。 (d(), !l && i.captcha.needed.value && diff --git a/app/lib/useWithdrawAvailability.js b/app/lib/useWithdrawAvailability.js index b4f594f..6bb9d0d 100644 --- a/app/lib/useWithdrawAvailability.js +++ b/app/lib/useWithdrawAvailability.js @@ -1,3 +1,4 @@ +/** 余额/推广提现入口可用性;按场景分别缓存规则,复用提现对话框中的规则判定。 */ import { computed } from "vue"; import { useState } from "#app"; import { useApi } from "~/lib/auth.js"; @@ -16,6 +17,7 @@ function i(i = ``) { s = computed(() => a.value.loading), c = computed(() => a.value.loaded), l = computed(() => a.value.error); + // 已成功加载默认不重取,e 可强制刷新;失败保持 loaded=false,允许后续重试。 async function u(e = !1) { if (!(a.value.loading || (a.value.loaded && !e))) { ((a.value.loading = !0), (a.value.error = ``)); diff --git a/app/lib/wechat-binding.js b/app/lib/wechat-binding.js index c58c14e..ee79c0a 100644 --- a/app/lib/wechat-binding.js +++ b/app/lib/wechat-binding.js @@ -1,4 +1,6 @@ +/** 微信扫码绑定的短期续接数据;存储由调用方传入,token 不放进页面跳转 URL。 */ var e = `console.wechat-binding`; +// 写入期限取服务端截止时间和五分钟上限的较早值,已过期会话立即拒绝。 function t(t, n, r = Date.now()) { if (!t.ticket || !t.token) throw Error(`扫码会话无效,请重新扫码`); let i = Math.min(r + 3e5, t.expire_time ? t.expire_time * 1e3 : r + 3e5); @@ -12,6 +14,7 @@ function t(t, n, r = Date.now()) { }), ); } +// 读取时重新校验结构和期限;格式损坏、过期或异常长有效期均删除,避免继续复用。 function n(t, n = Date.now()) { try { let r = JSON.parse(t.getItem(e) || `null`); diff --git a/app/middleware/auth.global.js b/app/middleware/auth.global.js index 3a31aff..f983285 100644 --- a/app/middleware/auth.global.js +++ b/app/middleware/auth.global.js @@ -1,3 +1,4 @@ +/** 客户端全局鉴权与安全回跳;旧平台凭证入口先脱敏,再由专页向后端确认身份。 */ import { defineNuxtRouteMiddleware, navigateTo } from "#app"; import { useAuth } from "~/lib/auth.js"; import { r as safeRedirect } from "~/lib/auth-redirect.js"; @@ -5,6 +6,7 @@ import { isAuthPage, isPublicRoute } from "~/utils/access.js"; export default defineNuxtRouteMiddleware((to) => { const auth = useAuth(); + // queryParam 是旧平台携带的敏感 JWT:只进入内存待验证状态,立即从地址和路由中移除。 if (to.path === "/home.htm") { if (Object.hasOwn(to.query, "queryParam") || !auth.legacyLoginPending) { auth.prepareLegacyLogin(to.query.queryParam); @@ -27,6 +29,7 @@ export default defineNuxtRouteMiddleware((to) => { } return; } + // 非公开页保留安全的站内目的地,但绝不把旧平台凭证夹带进登录 redirect。 if (!auth.isLoggedIn && !isPublicRoute(to)) { const query = { ...to.query }; delete query.queryParam; @@ -43,6 +46,7 @@ export default defineNuxtRouteMiddleware((to) => { query: redirect === "/" ? {} : { redirect }, }); } + // 已登录用户通常无需返回登录页;OAuth 回调、微信绑定和官网仍允许访问。 if ( auth.isLoggedIn && isAuthPage(to.path) && diff --git a/app/plugins/01-ui.client.js b/app/plugins/01-ui.client.js index e0361ee..9e79df6 100644 --- a/app/plugins/01-ui.client.js +++ b/app/plugins/01-ui.client.js @@ -1,3 +1,4 @@ +/** 最早安装 Pinia 和 PrimeVue 服务,确保后续启动插件能使用 store、弹窗和提示。 */ import { defineNuxtPlugin } from "#app"; import { createPinia } from "pinia"; import PrimeVue from "primevue/config"; @@ -10,6 +11,7 @@ import options from "~/config/primevue.js"; export default defineNuxtPlugin({ name: "console-ui", order: -20, + // 客户端插件只注册一次;具体主题/中文文案集中在 config/primevue.js。 setup({ vueApp }) { vueApp.use(createPinia()); vueApp.use(PrimeVue, options); diff --git a/app/plugins/02-bootstrap.client.js b/app/plugins/02-bootstrap.client.js index 6f4b889..dcf590c 100644 --- a/app/plugins/02-bootstrap.client.js +++ b/app/plugins/02-bootstrap.client.js @@ -1,3 +1,4 @@ +/** 应用启动加载公共配置;已有登录态时并行预取菜单和账户概览,单项失败不阻断启动。 */ import { defineNuxtPlugin } from "#app"; import { useAuth, useApi } from "~/lib/auth.js"; import { t as useSiteConfig } from "~/lib/useSiteConfig.js"; @@ -9,6 +10,7 @@ export default defineNuxtPlugin({ async setup() { const auth = useAuth(); const tasks = [useSiteConfig().load()]; + // 旧平台入口的 JWT 尚待专页核验,不能在此提前当成可信会话加载私有资料。 if (auth.isLoggedIn && window.location.pathname !== "/home.htm") { tasks.push(useNavigation(auth, useApi()).load(), auth.fetchOverview()); } diff --git a/app/plugins/03-session.client.js b/app/plugins/03-session.client.js index 55eafe6..ee8f215 100644 --- a/app/plugins/03-session.client.js +++ b/app/plugins/03-session.client.js @@ -1,3 +1,4 @@ +/** 同源多标签页会话同步:监听 jwt/clear 变更,退出时将当前受保护页面替换为登录页。 */ import { defineNuxtPlugin } from "#app"; import { useAuth, isValidJwt } from "~/lib/auth.js"; import { r as safeRedirect } from "~/lib/auth-redirect.js"; @@ -9,6 +10,7 @@ export default defineNuxtPlugin({ setup(app) { const auth = useAuth(); const router = app.$router; + // 只处理 localStorage 会话事件;读取最新值而非 event.newValue,避免积压事件倒退状态。 function reconcileSession(event) { if ( event.storageArea !== localStorage || @@ -30,6 +32,7 @@ export default defineNuxtPlugin({ } } window.addEventListener("storage", reconcileSession); + // 应用销毁时解绑,避免热重载或重新挂载后叠加处理同一 storage 事件。 app.vueApp.onUnmount(() => window.removeEventListener("storage", reconcileSession), ); diff --git a/app/utils/access.js b/app/utils/access.js index b3bb008..ad77329 100644 --- a/app/utils/access.js +++ b/app/utils/access.js @@ -1,3 +1,4 @@ +/** 前端公开路由清单,供路由守卫和跨标签退出共用;后端接口仍须独立鉴权。 */ const authPages = new Set([ "/login", "/register", @@ -16,6 +17,7 @@ const shopPages = new Set([ "/products/dedicated-server/customize", ]); +// 商品可匿名浏览,但 edit_position 表示编辑用户购物车,不能按公开商品页放行。 export function isPublicShop(route) { if (Object.hasOwn(route.query || {}, "edit_position")) return false; const path = route.path.replace(/\/+$/, "") || "/"; @@ -24,6 +26,7 @@ export function isPublicShop(route) { export function isAuthPage(path) { return authPages.has(path.replace(/\/+$/, "") || "/"); } +// /home.htm 是待核验的过渡入口,公开仅表示能打开核验页,不表示已获登录权限。 export function isPublicRoute(route) { return ( route.path === "/home.htm" || isAuthPage(route.path) || isPublicShop(route) diff --git a/app/utils/brand.js b/app/utils/brand.js index 47deb15..d23adee 100644 --- a/app/utils/brand.js +++ b/app/utils/brand.js @@ -1 +1,2 @@ +// 品牌图片统一使用 public 中的站内资源,页面和布局不要各自拼接部署路径。 export const logoUrl = "/images/brand/logo.png"; diff --git a/app/utils/crypto.js b/app/utils/crypto.js index 31858d5..4bbae02 100644 --- a/app/utils/crypto.js +++ b/app/utils/crypto.js @@ -1,3 +1,4 @@ +/** 提供现有调用方使用的 SHA-256 函数;这是摘要算法,不是加密或密码存储方案。 */ import CryptoJS from "crypto-js"; export function getCrypto() { return CryptoJS.SHA256; diff --git a/app/utils/load-module.js b/app/utils/load-module.js index db1ba6a..d03de64 100644 --- a/app/utils/load-module.js +++ b/app/utils/load-module.js @@ -1,3 +1,4 @@ +// 恢复模块的加载入口,原样调用工厂并返回其结果/Promise;异常由调用方处理,不做静默回退。 export function loadModule(load) { return load(); }