Files
console-dev/app/lib/storage-capacity.js
T
cieanyau 651c36367f
前端验证 / verify (push) Failing after 15m2s
接入 ESLint 与 Prettier 并加入 CI 静态门禁
2026-11-07 01:24:36 +08:00

496 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 财务/通用商品配置适配与容量文案解析:将数字类型码转换为前端可编辑模型。
* 数据不完整时明确失败,不能用空选项、零库存或默认价格掩盖后端配置错误。
*/
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) {
return typeof e == `object` && e && !Array.isArray(e) ? e : null;
}
function a(e) {
return typeof e == `string` ? e : typeof e == `number` ? String(e) : ``;
}
function o(e) {
return e === !0 || e === 1 || e === `1`;
}
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);
let r = Number(e);
return ((!Number.isFinite(r) || r < n) && s(t), r);
}
function l(e) {
let t = c(e, `商品配置标识无效`, 1);
return (Number.isSafeInteger(t) || s(`商品配置标识无效`), t);
}
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) &&
s(`商品配置选项不完整`),
e
);
}
// 清理区域文案中的“第一区/1区”等编号及冗余分隔,不改区域 id。
function f(e) {
let t = `(?:第\\s*)?(?:[一二三四五六七八九十百零〇两]+|[0-90-9]+)\\s*区`;
return e
.replace(RegExp(`[((]\\s*${t}\\s*[))]`, `g`), ``)
.replace(RegExp(t, `g`), ``)
.replace(/\s{2,}/g, ` `)
.replace(/^[\s\-—_/·]+|[\s\-—_/·]+$/g, ``)
.trim();
}
/** 展平系统版本、区域嵌套或普通子选项;返回统一 value/label,系统额外保留 group。 */
function p(e, n) {
if (n === 5) {
let t = i(e.sub);
return (
t || s(`操作系统配置不完整`),
d(
Object.entries(t).flatMap(([e, t]) =>
u(i(t)?.child).map((t) => {
let n = i(t);
return (
n || s(`操作系统配置不完整`),
{
value: l(n.id),
label: a(n.version || n.option_name).trim() || e,
group: e,
}
);
}),
),
)
);
}
return d(
n === 12
? u(e.sub).flatMap((e) => {
let t = i(e);
return (
t || s(`区域配置不完整`),
u(t.area).map((e) => {
let n = i(e);
n || s(`区域配置不完整`);
let r = [f(a(t.option_name)), f(a(n.area_zh || n.area))].filter(
Boolean,
);
return {
value: l(n.id),
label: [...new Set(r)].join(` / `) || `默认区域`,
};
})
);
})
: u(e.sub).map((e) => {
let n = i(e);
n || s(`商品配置选项不完整`);
let r = l(n.id);
return {
value: r,
label: bandwidth_display_t(a(n.option_name).trim()) || String(r),
};
}),
);
}
/**
* 转换一个配置项:布尔、系统、选择、级联或数量。
* 数量上下界与各子区间求交,保留不连续 ranges;最大值为 0 的禁用数量项不展示。
*/
function m(e) {
let t = i(e);
t || s(`商品配置数据不完整`);
let o = c(t.option_type, `商品配置类型无效`);
!r.has(o) &&
!n.has(o) &&
![3, 5, 12].includes(o) &&
s(`该商品配置类型暂不支持`);
let d = {
id: String(l(t.id)),
label: a(t.option_name).trim() || `配置选项`,
kind:
o === 20
? `cascade`
: o === 3
? `boolean`
: o === 5
? `os`
: n.has(o)
? `quantity`
: `choice`,
choices: [],
min: 0,
max: 0,
step: 1,
unit: a(t.unit) || (o === 14 ? `GB` : ``),
description: a(t.notes),
};
if (d.kind === `boolean`)
return (
(d.max = 1),
(d.choices = [
{
value: 0,
label: `否`,
},
{
value: 1,
label: `是`,
},
]),
d
);
if (d.kind !== `quantity`) return ((d.choices = p(t, o)), d);
if (
((d.min = c(t.qty_minimum, `商品数量配置无效`)),
(d.max = c(t.qty_maximum, `商品数量配置无效`, d.min)),
d.max === 0)
)
return null;
d.step =
t.qty_stage === void 0 || t.qty_stage === null || t.qty_stage === ``
? 1
: c(t.qty_stage, `商品数量步长配置无效`, Number.MIN_VALUE);
let f = u(t.sub)
.map((e) => {
let t = i(e);
t || s(`商品数量范围配置无效`);
let n = Math.max(d.min, c(t.qty_minimum, `商品数量范围配置无效`)),
r = Math.min(d.max, c(t.qty_maximum, `商品数量范围配置无效`));
return r < n
? null
: {
min: n,
max: r,
step: d.step,
};
})
.filter((e) => e !== null);
return (
(!f.length || !f.some((e) => e.min === d.min)) &&
s(`商品数量范围配置不完整`),
(d.ranges = f),
d
);
}
// 主机名/密码分别带 show 和 modify,展示与是否允许修改是两个独立开关。
function h(e, t) {
let n = i(e);
return {
show: o(n?.show),
editable: o(n?.modify),
value: a(n?.[t]),
};
}
/** 转换下单自定义字段,复用 PCRE 适配;不支持类型、空下拉选项或重复 id 均报错。 */
function g(t) {
if (t == null) return [];
let n = u(t, `自定义字段配置不完整`).map((t) => {
let n = i(t);
n || s(`自定义字段配置不完整`);
let r = {
text: `text`,
password: `password`,
textarea: `textarea`,
dropdown: `select`,
tickbox: `boolean`,
checkbox: `boolean`,
link: `link`,
explain: `explain`,
}[a(n.field_type)];
r || s(`该商品自定义字段类型暂不支持`);
let c = product_fields_n(n.regexpr),
u = [];
if (r === `select`) {
let e =
typeof n.field_option == `string`
? n.field_option.split(`,`)
: Array.isArray(n.field_option) &&
n.field_option.every((e) => typeof e == `string`)
? n.field_option
: [];
((u = [...new Set(e.map((e) => e.trim()).filter(Boolean))].map((e) => ({
value: e,
label: e,
}))),
u.length || s(`自定义字段选项不完整`));
}
return {
id: String(l(n.id)),
label: a(n.field_name).trim() || `自定义字段`,
kind: r,
required: o(n.is_required),
...c,
explain: a(n.explain_content),
options: u,
description: a(n.description),
};
});
return (
new Set(n.map((e) => e.id)).size !== n.length && s(`自定义字段标识重复`),
n
);
}
/**
* 商品配置的总入口:兼容业务数据和 data 包装,生成选项、周期、库存及联动定义。
* stock 为 null 表示未启用库存控制,不能与真实的 0 库存混淆。
*/
function _(e) {
let t = i(e),
n = i(t?.product) ? t : i(t?.data),
r = i(n?.product);
((!n || !r || (n.status !== void 0 && Number(n.status) !== 200)) &&
s(`商品配置数据不完整`),
n.links !== void 0 &&
n.links !== null &&
!Array.isArray(n.links) &&
s(`商品联动配置数据不完整`));
let l = u(n.option)
.map(m)
.filter((e) => e !== null);
new Set(l.map((e) => e.id)).size !== l.length && s(`商品配置标识重复`);
let d = u(r.cycle, `商品周期配置不完整`).map((e) => {
let t = i(e),
n = a(t?.billingcycle).trim();
return (
n || s(`商品周期配置不完整`),
{
value: n,
label: a(t?.billingcycle_zh).trim() || n,
}
);
});
(!d.length || new Set(d.map((e) => e.value)).size !== d.length) &&
s(`商品周期配置不完整`);
let f = o(r.stock_control) ? c(r.qty, `商品库存配置无效`) : null;
return (
f !== null && !Number.isSafeInteger(f) && s(`商品库存配置无效`),
{
options: l,
cycles: d,
allowQty: o(n.allow_qty ?? r.allow_qty),
stock: f,
hostname: h(r.host, `host`),
password: h(r.password, `password`),
customFields: g(n.custom_fields),
links: v(n.links ?? [], l),
linkDefinitions: JSON.parse(JSON.stringify(n.links ?? [])),
}
);
}
/** 将 seq/sneq 联动解析为本地模型;只保留当前配置项存在且选项有效的完整规则。 */
function v(e, t) {
let n = new Map(t.map((e) => [e.id, e]));
function r(e) {
let t = i(e),
r = String(l(t?.config_id)),
o = n.get(r);
(!o || ![`seq`, `sneq`].includes(a(t?.relation))) &&
s(`商品联动配置数据不完整`);
let u = i(t?.sub_id);
(!u || !Object.keys(u).length) && s(`商品联动选项不完整`);
let d =
o.kind === `quantity`
? Object.values(u).map((e) => {
let t = i(e),
n = c(t?.qty_minimum, `商品联动数量范围无效`);
return {
min: n,
max: c(t?.qty_maximum, `商品联动数量范围无效`, n),
};
})
: [],
f = [];
if (o.kind !== `quantity`)
for (let e of Object.keys(u)) {
if (!/^\d+$/.test(e)) return null;
let t = Number(e);
if (
!Number.isSafeInteger(t) ||
(o.kind === `cascade` && t <= 0) ||
!o.choices.some((e) => e.value === t)
)
return null;
f.push(t);
}
return {
optionId: r,
relation: t.relation,
values: f,
ranges: d,
};
}
return u(e).flatMap((e) => {
let t = i(e);
if ((t || s(`商品联动配置数据不完整`), !n.has(String(t.config_id))))
return [];
let a = r(t);
if (!a) return [];
let o = u(t.result, `商品联动结果不完整`);
o.length || s(`商品联动结果不完整`);
let c = o.filter((e) => n.has(String(i(e)?.config_id)));
if (!c.length) return [];
let l = c.map(r);
return l.some((e) => e === null)
? []
: [
{
...a,
results: l,
},
];
});
}
// 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) => {
let t = r.filter((t) => t.optionId === e.id);
if (!t.length) return e;
if (e.kind !== `quantity`)
return {
...e,
choices: e.choices.filter((e) => t.every((t) => y(t, e.value))),
};
let n = e.ranges ?? [
{
min: e.min,
max: e.max,
step: e.step,
},
];
for (let e of t) {
let t = [];
// 在规则边界按原步长分段,而非逐容量枚举;多条结果规则顺序求交。
for (let r of n) {
let n = [
...new Set([
0,
Math.floor((r.max - r.min) / r.step + 1e-7) + 1,
...e.ranges.flatMap((e) => [
Math.max(0, Math.ceil((e.min - r.min) / r.step - 1e-7)),
Math.floor((e.max - r.min) / r.step + 1e-7) + 1,
]),
]),
]
.filter(
(e) =>
e >= 0 && e <= Math.floor((r.max - r.min) / r.step + 1e-7) + 1,
)
.sort((e, t) => e - t);
for (let i = 0; i < n.length - 1; i++) {
let a = Number((r.min + n[i] * r.step).toFixed(8)),
o = Number((r.min + (n[i + 1] - 1) * r.step).toFixed(8));
o >= a &&
y(e, a) &&
t.push({
min: a,
max: o,
step: r.step,
});
}
}
n = t;
}
return {
...e,
ranges: n,
min: n[0]?.min ?? 0,
max: n.at(-1)?.max ?? -1,
};
});
}
/** 解析级联接口的子配置与默认选择,校验 id 不与根/其他子项重复,默认值必须可选。 */
function x(e, t) {
let n = u(
i(u(e, `商品级联配置数据不完整`)[0])?.son,
`商品级联配置数据不完整`,
),
r = {};
return {
options: n.map((e) => {
let n = i(e);
n || s(`商品级联配置数据不完整`);
let a = m({
...n,
option_type: 20,
});
(a.id === t || r[a.id] !== void 0) && s(`商品级联配置标识重复`);
let o = l(n.checkSubId);
return (
a.choices.some((e) => e.value === o) || s(`商品级联默认配置无效`),
(r[a.id] = o),
{
...a,
cascadeRoot: t,
}
);
}),
values: r,
};
}
// 容量单位保留 B 与 iB 的文案区别,不在此处擅自做十进制/二进制容量换算。
function S(e) {
let t = /^([kmgtpe])(?:i?b)?$|^(b)$/i.exec(e.trim());
return t
? t[2]
? `B`
: `${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;
let r = [...n.matchAll(/\d+(?:\.\d+)?\s*(?:[kmgtpe](?:i?b)?|b)(?![a-z])/gi)],
i = r[0],
a;
if (r.length === 1) a = S(/[a-z]+$/i.exec(i[0])?.[0] ?? ``);
else if (r.length === 0 && S(t)) {
let e = [...n.matchAll(/\d+(?:\.\d+)?/g)];
if (e.length !== 1) return null;
((i = e[0]), (a = S(t)));
} else return null;
if (!i || !a) return null;
let o = Number(/^\d+(?:\.\d+)?/.exec(i[0])?.[0]);
if (!Number.isFinite(o)) return null;
let s = n.slice(0, i.index) + n.slice(i.index + i[0].length),
c = [...s.matchAll(/NVMe|SSD|HDD|SAS|SATA/gi)].map((e) =>
e[0].toUpperCase() === `NVME` ? `NVMe` : e[0].toUpperCase(),
);
return s
.replace(
/NVMe|SSD|HDD|SAS|SATA|免费赠送|免费|赠送|系统盘|数据盘|云硬盘|云磁盘|云盘|硬盘|磁盘|存储|容量|高性能|高效|普通|标准|通用型|性能型|弹性|本地/gi,
``,
)
.replace(/[\s()[\]{}::·]/g, ``)
? null
: {
size: o,
unit: a,
type: [...new Set(c)].join(` `),
};
}
export { _ as a, v as i, b as n, x as r, C as t };