496 lines
15 KiB
JavaScript
496 lines
15 KiB
JavaScript
/**
|
||
* 财务/通用商品配置适配与容量文案解析:将数字类型码转换为前端可编辑模型。
|
||
* 数据不完整时明确失败,不能用空选项、零库存或默认价格掩盖后端配置错误。
|
||
*/
|
||
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 };
|