400 lines
12 KiB
JavaScript
400 lines
12 KiB
JavaScript
/**
|
|
* 后端菜单到前端路由的适配层:兼容旧页面地址、限制可用路由和参数,并生成导航分组。
|
|
* 菜单可见性不是权限校验,所有实际操作仍须由后端按当前身份授权。
|
|
*/
|
|
// 合并旧账单入口与财务入口;全树最多保留一个由 /bills 转换得到的财务项。
|
|
function e(e) {
|
|
let t = (e) =>
|
|
e.some(
|
|
(e) =>
|
|
e.to === `/finance` ||
|
|
e.to === `/finance?tab=bills` ||
|
|
(e.children && t(e.children)),
|
|
),
|
|
n = t(e);
|
|
function r(e) {
|
|
return e.flatMap((e) => {
|
|
if (e.children) {
|
|
let t = r(e.children);
|
|
return t.length
|
|
? [
|
|
{
|
|
...e,
|
|
children: t,
|
|
},
|
|
]
|
|
: [];
|
|
}
|
|
return e.to?.split(`?`)[0] === `/bills`
|
|
? n
|
|
? []
|
|
: ((n = !0),
|
|
[
|
|
{
|
|
...e,
|
|
label: `财务`,
|
|
to: `/finance`,
|
|
},
|
|
])
|
|
: [e];
|
|
});
|
|
}
|
|
return r(e);
|
|
}
|
|
/** 路径长度和匹配 query 数量决定激活优先级,避免父菜单与更具体子菜单同时高亮。 */
|
|
function t(e, t, n = []) {
|
|
let r =
|
|
t.path === `/products/cvm`
|
|
? `/products/cloud-server`
|
|
: t.path === `/bills` || t.path.startsWith(`/bills/`)
|
|
? t.path.replace(`/bills`, `/finance`)
|
|
: t.path;
|
|
function i(e) {
|
|
let [n = ``, i = ``] = e.split(`?`);
|
|
if (r !== n && !r.startsWith(`${n}/`)) return -1;
|
|
let a = [...new URLSearchParams(i)];
|
|
return a.every(([e, n]) => String(t.query[e] ?? ``) === n)
|
|
? n.length * 100 + a.length
|
|
: -1;
|
|
}
|
|
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(
|
|
`.`,
|
|
),
|
|
),
|
|
r = {
|
|
"index.htm": `/`,
|
|
"home.htm": `/`,
|
|
"cart/goodsList.htm": `/products`,
|
|
"cart/cart.htm": `/cart`,
|
|
"finance.htm": `/finance`,
|
|
"order.htm": `/orders`,
|
|
"account.htm": `/account`,
|
|
"security.htm": `/account/api-keys`,
|
|
"ticket.htm": `/tickets`,
|
|
"help.htm": `/help`,
|
|
"news.htm": `/news`,
|
|
"announcement.htm": `/notices`,
|
|
"api.htm": `/account/api-keys`,
|
|
"log.htm": `/logbook`,
|
|
"invoice.htm": `/invoices`,
|
|
"recommend.htm": `/recommend`,
|
|
"voucher.htm": `/vouchers`,
|
|
"host_transfer.htm": `/transfer`,
|
|
"security_group.htm": `/security/groups`,
|
|
"ssh_key.htm": `/security/ssh-keys`,
|
|
"message.htm": `/messages`,
|
|
"msgDetail.htm": `/messages`,
|
|
"group_rules.htm": `/security/groups`,
|
|
"withdrawal.htm": `/finance?tab=withdraw`,
|
|
"orderDetail.htm": `/orders/order`,
|
|
"addTicket.htm": `/tickets/create`,
|
|
"ticketDetails.htm": `/tickets/ticket`,
|
|
"issuedInvoice.htm": `/invoices/request`,
|
|
"security_ssh.htm": `/security/ssh-keys`,
|
|
"security_log.htm": `/logbook?type=api`,
|
|
},
|
|
i = {
|
|
45: `/transfer`,
|
|
},
|
|
a = new Set(
|
|
Object.values(r).filter(
|
|
(e) => ![`/`, `/security/groups`, `/vouchers`, `/logbook`].includes(e),
|
|
),
|
|
);
|
|
// 图标由本地 Lucide 名称决定,不直接采用后端传入的任意图标实现。
|
|
function o(e) {
|
|
let t = e.to?.split(`?`)[0];
|
|
return (
|
|
(t &&
|
|
{
|
|
"/": `lucide:house`,
|
|
"/products": `lucide:shopping-cart`,
|
|
"/cart": `lucide:shopping-basket`,
|
|
"/cloud-servers": `lucide:server`,
|
|
"/dedicated-servers": `lucide:server`,
|
|
"/bills": `lucide:receipt-text`,
|
|
"/finance": `lucide:wallet`,
|
|
"/orders": `lucide:clipboard-list`,
|
|
"/invoices": `lucide:receipt`,
|
|
"/recommend": `lucide:gift`,
|
|
"/vouchers": `lucide:ticket`,
|
|
"/account": `lucide:user`,
|
|
"/account/api-keys": `lucide:key-round`,
|
|
"/account/connections": `lucide:link`,
|
|
"/messages": `lucide:mail`,
|
|
"/logbook": `lucide:scroll-text`,
|
|
"/tickets": `lucide:headphones`,
|
|
"/help": `lucide:circle-help`,
|
|
"/resources": `lucide:book-open`,
|
|
"/notices": `lucide:megaphone`,
|
|
"/news": `lucide:newspaper`,
|
|
"/transfer": `lucide:truck`,
|
|
}[t]) ||
|
|
(e.href
|
|
? `lucide:external-link`
|
|
: e.children?.length
|
|
? `lucide:folder`
|
|
: `lucide:circle-help`)
|
|
);
|
|
}
|
|
/** 根据服务器模块菜单纠正列表类型或补齐唯一模块 m;多模块时不擅自替用户选择。 */
|
|
function s(e, t) {
|
|
if (![`/cloud-servers`, `/dedicated-servers`].includes(t.path)) return;
|
|
let n = new Set(),
|
|
r;
|
|
function i(e) {
|
|
for (let a of e) {
|
|
a.children && i(a.children);
|
|
let [e, o = ``] = a.to?.split(`?`) ?? [],
|
|
s = new URLSearchParams(o).get(`m`);
|
|
(s &&
|
|
s === String(t.query.m ?? ``) &&
|
|
e &&
|
|
[`/cloud-servers`, `/dedicated-servers`].includes(e) &&
|
|
e !== t.path &&
|
|
(r = {
|
|
path: e,
|
|
query: {
|
|
...t.query,
|
|
m: s,
|
|
},
|
|
}),
|
|
e === t.path && s && /^[1-9]\d*$/.test(s) && n.add(s));
|
|
}
|
|
}
|
|
if ((i(e), r)) return r;
|
|
if (!(n.size !== 1 || n.has(String(t.query.m ?? ``))))
|
|
return {
|
|
path: t.path,
|
|
query: {
|
|
...t.query,
|
|
m: [...n][0],
|
|
},
|
|
};
|
|
}
|
|
// 把已规范化的后端菜单装入现有分组,账户子页和资源中心有固定归属,其他落到末组。
|
|
function c(e, t, n) {
|
|
let r = e.map((e) => ({
|
|
...e,
|
|
items: [...(t.find((t) => t.label === e.label)?.items ?? [])],
|
|
})),
|
|
i = (e) => e.children?.flatMap(i) ?? (e.to ? [e.to.split(`?`)[0]] : []);
|
|
for (let t of n) {
|
|
let n = i(t),
|
|
a = e.findIndex((e) =>
|
|
e.items.some((e) =>
|
|
[e.to, ...(e.children?.map((e) => e.to) ?? [])].some(
|
|
(e) => e && n.includes(e),
|
|
),
|
|
),
|
|
);
|
|
(a < 0 &&
|
|
n.some((e) => e.startsWith(`/account/`)) &&
|
|
(a = e.findIndex((e) => e.items.some((e) => e.to === `/account`))),
|
|
a < 0 &&
|
|
n.includes(`/resources`) &&
|
|
(a = e.findIndex((e) => e.items.some((e) => e.to === `/help`))),
|
|
a < 0 && (a = r.length - 1),
|
|
!(a < 0) &&
|
|
r[a].items.push({
|
|
label: t.label,
|
|
icon: o(t),
|
|
backend: t,
|
|
}));
|
|
}
|
|
return r.filter((e) => e.items.length);
|
|
}
|
|
// 遍历树生成面包屑;相同路径时选择包含更多匹配 query 的最具体节点。
|
|
function l(e, n) {
|
|
let r = [];
|
|
function i(e, a) {
|
|
let o = e
|
|
.filter((e) => e.to)
|
|
.map((e) => ({
|
|
to: e.to,
|
|
}));
|
|
for (let s of e) {
|
|
let e = [...a, s.label];
|
|
if (s.to && t(s.to, n, o)) {
|
|
let [t = ``, n = ``] = s.to.split(`?`);
|
|
r.push({
|
|
labels: e,
|
|
path: t,
|
|
score: t.length * 100 + [...new URLSearchParams(n)].length,
|
|
});
|
|
}
|
|
s.children && i(s.children, e);
|
|
}
|
|
}
|
|
return (i(e, []), r.sort((e, t) => t.score - e.score)[0]?.labels ?? []);
|
|
}
|
|
// 使用后端菜单时移除已被接管的前端默认入口,避免同一功能重复出现。
|
|
function u(e, t) {
|
|
let n = new Set(),
|
|
r = (e) =>
|
|
e.forEach((e) => {
|
|
(e.to && n.add(e.to.split(`?`)[0]), e.children && r(e.children));
|
|
});
|
|
r(t);
|
|
let i = (e) => !a.has(e) && !n.has(e);
|
|
return e
|
|
.map((e) => ({
|
|
...e,
|
|
items: e.items.flatMap((e) => {
|
|
if (e.children) {
|
|
let t = e.children.filter((e) => i(e.to));
|
|
return t.length
|
|
? [
|
|
{
|
|
...e,
|
|
children: t,
|
|
},
|
|
]
|
|
: [];
|
|
}
|
|
return !e.to || i(e.to) ? [e] : [];
|
|
}),
|
|
}))
|
|
.filter((e) => e.items.length);
|
|
}
|
|
/** 外链只允许无用户名/密码的 HTTP(S) URL;控制字符和反斜杠直接拒绝。 */
|
|
function d(e) {
|
|
if (
|
|
typeof e != `string` ||
|
|
!/^https?:\/\//i.test(e) ||
|
|
/[\u0000-\u0020\\]/.test(e)
|
|
)
|
|
return ``;
|
|
try {
|
|
let t = new URL(e);
|
|
return t.username || t.password ? `` : t.href;
|
|
} catch {
|
|
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) => {
|
|
if (
|
|
!e ||
|
|
typeof e != `object` ||
|
|
typeof e.name != `string` ||
|
|
!e.name.trim()
|
|
)
|
|
return [];
|
|
let s = {
|
|
key: `${t}${e.id ?? o}`,
|
|
label: e.name.slice(0, 100),
|
|
confirm: Number(e.second_reminder) === 1,
|
|
},
|
|
c = f(e.child, `${s.key}-`, a + 1);
|
|
if (c.length) return ((s.children = c), [s]);
|
|
if ([`module`, `res_module`].includes(e.menu_type)) {
|
|
let t = {
|
|
云服务器: `/cloud-servers`,
|
|
物理服务器: `/dedicated-servers`,
|
|
}[e.name.trim()],
|
|
n = Number(e.id);
|
|
return (
|
|
t && Number.isSafeInteger(n) && n > 0
|
|
? (s.to = `${t}?m=${n}`)
|
|
: (s.disabledReason = `此菜单暂未适配当前控制台`),
|
|
[s]
|
|
);
|
|
}
|
|
if (e.menu_type === `embedded`)
|
|
return ((s.disabledReason = `此菜单暂未适配当前控制台`), [s]);
|
|
let l = typeof e.url == `string` ? e.url.trim() : ``,
|
|
u = l.split(`?`)[0] || ``,
|
|
p = u.replace(/^\//, ``).replace(/^clientarea\//, ``),
|
|
m = /^plugin\/([1-9]\d*)\/([a-zA-Z_]+\.htm)$/.exec(p),
|
|
h = r[p] || (m ? (m[2] === `index.htm` ? i[m[1]] : r[m[2]]) : void 0),
|
|
g = /^plugin\/([1-9]\d*)\/source\.htm$/.exec(p);
|
|
if (g) s.to = `/resources?plugin=${g[1]}`;
|
|
else if (h || n.has(u)) {
|
|
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`],
|
|
"/finance": [`tab`, `status`],
|
|
"/logbook": [`type`],
|
|
"/security/groups": [`id`],
|
|
"/orders/order": [`id`],
|
|
"/tickets/ticket": [`id`],
|
|
"/messages": [`id`],
|
|
}[e] ?? []) {
|
|
let e = n.getAll(t),
|
|
i = e[0] ?? ``,
|
|
a =
|
|
t === `id`
|
|
? /^[1-9]\d*$/.test(i)
|
|
: t === `tab`
|
|
? [
|
|
`bills`,
|
|
`transaction`,
|
|
`credit`,
|
|
`withdraw`,
|
|
`freeze`,
|
|
].includes(i)
|
|
: t === `status`
|
|
? [
|
|
`all`,
|
|
`Unpaid`,
|
|
`Paid`,
|
|
`Refunded`,
|
|
`WaitUpload`,
|
|
`WaitReview`,
|
|
`ReviewFail`,
|
|
].includes(i)
|
|
: [`api`, `system`].includes(i);
|
|
!r.has(t) && e.length === 1 && a && r.set(t, i);
|
|
}
|
|
s.to = e + (r.size ? `?${r}` : ``);
|
|
} else
|
|
e.menu_type === `custom` && d(l)
|
|
? (s.href = d(l))
|
|
: (s.disabledReason = `此功能暂未开放`);
|
|
return [s];
|
|
}),
|
|
s = o.filter((e) => e.to === `/account`);
|
|
// 旧后台可能把多个入口映射到 /account;合并后仍保留任一入口要求的二次确认。
|
|
return s.length < 2
|
|
? o
|
|
: o.flatMap((e) =>
|
|
e.to === `/account`
|
|
? e === s[0]
|
|
? [
|
|
{
|
|
...e,
|
|
label: `账户`,
|
|
confirm: s.some((e) => e.confirm),
|
|
},
|
|
]
|
|
: []
|
|
: [e],
|
|
);
|
|
}
|
|
export {
|
|
c as a,
|
|
t as c,
|
|
f as i,
|
|
e as l,
|
|
o as n,
|
|
d as o,
|
|
s as r,
|
|
u as s,
|
|
l as t,
|
|
};
|