docs: annotate all shared frontend modules in Chinese

This commit is contained in:
cieanyau committed 2026-11-06 00:30:15 +08:00
1 parent 2be21c1c62
commit 408f8e5c98
55 files changed
+349

No files matched your search

+2
View File
@@ -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(
+1
View File
@@ -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;
+4
View File
@@ -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)}`;
+59
View File
@@ -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();
+19
View File
@@ -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) =>
+1
View File
@@ -1,3 +1,4 @@
/** 商品文案统一显示“峰值 NMbps”;边界约束避免误改其他标识符或带符号的数值。 */
function e(e) {
return e.replace(
/(?<![A-Za-z0-9_.+−-])(\d+(?:\.\d+)?)\s*Mbps\s*突发峰值带宽(?![A-Za-z0-9_])/gi,
+13
View File
@@ -1,3 +1,5 @@
/** 购物车编辑的数据快照、并发校验与选中项恢复;所有比较以规范化的可提交字段为准。 */
// 合并原商品和编辑值,剔除展示字段,统一数量和可选配置的默认值。
function e(e, t = {}) {
let n = {
...e,
@@ -11,12 +13,14 @@ function e(e, t = {}) {
self_defined_field: n.self_defined_field ?? {},
};
}
// 隔离编辑页和列表页的可变对象;只用于接口提供的 JSON 数据。
function t(e) {
return JSON.parse(JSON.stringify(e));
}
function n(n) {
return n.map((n) => 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 &&
+8
View File
@@ -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) &&
+16
View File
@@ -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);
+5
View File
@@ -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] : []
+1
View File
@@ -1,2 +1,3 @@
// 恢复模块保留的空函数导出;调用不产生副作用,不应在这里推断或补造原始实现。
var e = (e) => {};
export { e as t };
+4
View File
@@ -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),
+3
View File
@@ -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
+4
View File
@@ -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),
+3
View File
@@ -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)),
+15
View File
@@ -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 {
+4
View File
@@ -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 ``;
+1
View File
@@ -1,2 +1,3 @@
// 恢复代码的定时器导出入口;调用方负责在组件卸载时 clearInterval,模块不管理生命周期。
var e = globalThis.setInterval;
export { e as t };
+4
View File
@@ -1,3 +1,7 @@
/**
* 发票资料完整分页加载;e(page, 50) 返回 {list, count?},保留字符串形式的资料 id。
* 去重后仍达不到 count 且本页没有新记录时失败,避免将不完整资料当成完整列表。
*/
async function e(e) {
let t = [],
n = new Set();
+3
View File
@@ -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`]],
+3
View File
@@ -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);
+6
View File
@@ -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 {
+3
View File
@@ -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 (
+3
View File
@@ -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();
+5
View File
@@ -1,3 +1,8 @@
/**
* 生成便于辨认的随机密码建议:至少含小写、大写和数字各一位,排除易混字符。
* 现有实现使用 Math.random(非密码学随机源),不能当作高强度密钥/令牌生成器;
* 即使 e 小于 3,三个必选字符也会保留。此处注释如实说明现有行为,不改变协议。
*/
function e(e = 12) {
let t = (e) => e[Math.floor(Math.random() * e.length)],
n = [
+2
View File
@@ -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 ``;
+11
View File
@@ -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;
+2
View File
@@ -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(
+2
View File
@@ -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);
+4
View File
@@ -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` ||
+3
View File
@@ -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`);
}
+3
View File
@@ -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();
+5
View File
@@ -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,
+7
View File
@@ -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;
+13
View File
@@ -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()));
},
+29
View File
@@ -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;
+5
View File
@@ -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) => {
+14
View File
@@ -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());
}),
+11
View File
@@ -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());
}
+2
View File
@@ -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,
+3
View File
@@ -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);
+9
View File
@@ -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
+3
View File
@@ -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,
+5
View File
@@ -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 {
+4
View File
@@ -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 &&
+2
View File
@@ -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 = ``));
+3
View File
@@ -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`);
+4
View File
@@ -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) &&
+2
View File
@@ -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);
+2
View File
@@ -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());
}
+3
View File
@@ -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),
);
+3
View File
@@ -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)
+1
View File
@@ -1 +1,2 @@
// 品牌图片统一使用 public 中的站内资源,页面和布局不要各自拼接部署路径。
export const logoUrl = "/images/brand/logo.png";
+1
View File
@@ -1,3 +1,4 @@
/** 提供现有调用方使用的 SHA-256 函数;这是摘要算法,不是加密或密码存储方案。 */
import CryptoJS from "crypto-js";
export function getCrypto() {
return CryptoJS.SHA256;
+1
View File
@@ -1,3 +1,4 @@
// 恢复模块的加载入口,原样调用工厂并返回其结果/Promise;异常由调用方处理,不做静默回退。
export function loadModule(load) {
return load();
}