@zerodep-dev/vue-tsx-pro-ui 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/code-editor/CodeEditor.d.ts +18 -0
- package/dist/code-editor/CodeEditor.js +57 -0
- package/dist/code-editor/CodeEditor.js.map +1 -0
- package/dist/code-editor/editor.d.ts +52 -0
- package/dist/code-editor/editor.js +225 -0
- package/dist/code-editor/editor.js.map +1 -0
- package/dist/code-editor/index.d.ts +9 -0
- package/dist/code-editor/index.js +3 -0
- package/dist/config/lang/en-US.js +21 -3
- package/dist/config/lang/en-US.js.map +1 -1
- package/dist/config/lang/format.d.ts +2 -1
- package/dist/config/lang/format.js +9 -2
- package/dist/config/lang/format.js.map +1 -1
- package/dist/config/lang/lang.d.ts +19 -6
- package/dist/config/lang/lang.js +11 -2
- package/dist/config/lang/lang.js.map +1 -1
- package/dist/config/lang/zh-CN.js +21 -3
- package/dist/config/lang/zh-CN.js.map +1 -1
- package/dist/diagram/index.d.ts +2 -1
- package/dist/diagram/index.js +2 -1
- package/dist/diagram/layout.d.ts +57 -0
- package/dist/diagram/layout.js +50 -0
- package/dist/diagram/layout.js.map +1 -0
- package/dist/form/Field.d.ts +7 -0
- package/dist/form/Field.js +14 -3
- package/dist/form/Field.js.map +1 -1
- package/dist/form/useForm.d.ts +4 -1
- package/dist/form/useForm.js +35 -13
- package/dist/form/useForm.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +8 -5
- package/dist/input/Combobox.js +1 -1
- package/dist/input/Combobox.js.map +1 -1
- package/dist/input/Mentions.js +1 -1
- package/dist/input/Mentions.js.map +1 -1
- package/dist/input/PeoplePickerPanel.js +11 -11
- package/dist/input/PeoplePickerPanel.js.map +1 -1
- package/dist/input/Select.js +1 -1
- package/dist/input/Select.js.map +1 -1
- package/dist/input/TreeSelect.js +1 -1
- package/dist/input/TreeSelect.js.map +1 -1
- package/dist/input/Upload.js +2 -1
- package/dist/input/Upload.js.map +1 -1
- package/dist/input/condition.d.ts +1 -1
- package/dist/input/condition.js +32 -14
- package/dist/input/condition.js.map +1 -1
- package/dist/input/people.d.ts +15 -14
- package/dist/input/people.js +5 -5
- package/dist/input/people.js.map +1 -1
- package/dist/input/remote-items.d.ts +4 -2
- package/dist/input/remote-items.js +7 -2
- package/dist/input/remote-items.js.map +1 -1
- package/dist/patterns/QueryTable.d.ts +3 -0
- package/dist/patterns/QueryTable.js +39 -14
- package/dist/patterns/QueryTable.js.map +1 -1
- package/dist/patterns/messages.d.ts +2 -0
- package/dist/patterns/messages.js +2 -0
- package/dist/patterns/messages.js.map +1 -1
- package/dist/utils/data/clone.d.ts +8 -0
- package/dist/utils/data/clone.js +21 -0
- package/dist/utils/data/clone.js.map +1 -0
- package/dist/utils/data/date.d.ts +10 -0
- package/dist/utils/data/date.js +13 -1
- package/dist/utils/data/date.js.map +1 -1
- package/dist/utils/data/numeric.d.ts +7 -0
- package/dist/utils/data/numeric.js +46 -1
- package/dist/utils/data/numeric.js.map +1 -1
- package/dist/utils/dom/download.d.ts +8 -1
- package/dist/utils/dom/download.js +19 -2
- package/dist/utils/dom/download.js.map +1 -1
- package/dist/utils/request/error-message.d.ts +17 -0
- package/dist/utils/request/error-message.js +50 -0
- package/dist/utils/request/error-message.js.map +1 -0
- package/dist/utils/request/http.d.ts +68 -4
- package/dist/utils/request/http.js +116 -23
- package/dist/utils/request/http.js.map +1 -1
- package/dist/utils/request/use-autosave.d.ts +35 -0
- package/dist/utils/request/use-autosave.js +69 -0
- package/dist/utils/request/use-autosave.js.map +1 -0
- package/package.json +57 -4
|
@@ -32,6 +32,13 @@ export declare function shiftDecimal(text: string, places: number): string;
|
|
|
32
32
|
* 有 Decimal 时按十进制精确比较;number 与 bigint 之间不经过浮点数(大整数不丢精度);NaN 排在最后
|
|
33
33
|
*/
|
|
34
34
|
export declare function compareNumeric(a: NumericValue, b: NumericValue): number;
|
|
35
|
+
/**
|
|
36
|
+
* 能精确比较的值 → 十进制文本:number(NaN、Infinity 不算)、bigint、Decimal、十进制写法的文字;其余是 undefined。
|
|
37
|
+
* 条件比较(input/condition.ts)用它:与流程引擎模型包的规则一致,长小数、大整数不丢精度
|
|
38
|
+
*/
|
|
39
|
+
export declare function decimalTextOf(value: unknown): string | undefined;
|
|
40
|
+
/** 两段十进制文本(decimalTextOf 给出的)按数值比:负数、0、正数;不经过浮点数 */
|
|
41
|
+
export declare function compareDecimalText(a: string, b: string): number;
|
|
35
42
|
/** 两个数值是否相等(1 与 1n、Decimal('1.0') 与 1 都算相等) */
|
|
36
43
|
export declare const sameNumeric: (a: NumericValue, b: NumericValue) => boolean;
|
|
37
44
|
/** 数值写回成什么类型:number、bigint,或某个 Decimal 构造函数(使用方 Decimal.clone 配过精度的也行) */
|
|
@@ -63,6 +63,51 @@ function compareNumeric(a, b) {
|
|
|
63
63
|
if (isDecimal(b)) return compareWithDecimal(a, b, b.constructor);
|
|
64
64
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
65
65
|
}
|
|
66
|
+
/** 十进制写法(可带指数):'19.90'、'-.5'、'1e3' */
|
|
67
|
+
var DECIMAL_TEXT = /^([+-]?)(\d+)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$|^([+-]?)\.(\d+)(?:[eE]([+-]?\d+))?$/;
|
|
68
|
+
function decimalParts(text) {
|
|
69
|
+
const match = DECIMAL_TEXT.exec(text);
|
|
70
|
+
if (!match) return void 0;
|
|
71
|
+
const [, sign1, int1, frac1, exp1, sign2, frac2, exp2] = match;
|
|
72
|
+
const intDigits = int1 ?? "";
|
|
73
|
+
const digits = intDigits + (frac1 ?? frac2 ?? "");
|
|
74
|
+
const point = intDigits.length + Number(exp1 ?? exp2 ?? 0);
|
|
75
|
+
const padded = point < 0 ? "0".repeat(-point) + digits : point > digits.length ? digits + "0".repeat(point - digits.length) : digits;
|
|
76
|
+
const at = Math.max(point, 0);
|
|
77
|
+
const int = padded.slice(0, at).replace(/^0+/, "");
|
|
78
|
+
const frac = padded.slice(at).replace(/0+$/, "");
|
|
79
|
+
return {
|
|
80
|
+
negative: (sign1 ?? sign2) === "-" && (int !== "" || frac !== ""),
|
|
81
|
+
int,
|
|
82
|
+
frac
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 能精确比较的值 → 十进制文本:number(NaN、Infinity 不算)、bigint、Decimal、十进制写法的文字;其余是 undefined。
|
|
87
|
+
* 条件比较(input/condition.ts)用它:与流程引擎模型包的规则一致,长小数、大整数不丢精度
|
|
88
|
+
*/
|
|
89
|
+
function decimalTextOf(value) {
|
|
90
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : void 0;
|
|
91
|
+
if (typeof value === "bigint" || isDecimal(value)) return numericText(value);
|
|
92
|
+
if (typeof value === "string" && DECIMAL_TEXT.test(value.trim())) return value.trim();
|
|
93
|
+
}
|
|
94
|
+
/** 两段十进制文本(decimalTextOf 给出的)按数值比:负数、0、正数;不经过浮点数 */
|
|
95
|
+
function compareDecimalText(a, b) {
|
|
96
|
+
const x = decimalParts(a);
|
|
97
|
+
const y = decimalParts(b);
|
|
98
|
+
if (!x || !y) return a === b ? 0 : a < b ? -1 : 1;
|
|
99
|
+
if (x.negative !== y.negative) return x.negative ? -1 : 1;
|
|
100
|
+
let magnitude = 0;
|
|
101
|
+
if (x.int.length !== y.int.length) magnitude = x.int.length < y.int.length ? -1 : 1;
|
|
102
|
+
else if (x.int !== y.int) magnitude = x.int < y.int ? -1 : 1;
|
|
103
|
+
else {
|
|
104
|
+
const width = Math.max(x.frac.length, y.frac.length);
|
|
105
|
+
const left = x.frac.padEnd(width, "0");
|
|
106
|
+
const right = y.frac.padEnd(width, "0");
|
|
107
|
+
magnitude = left === right ? 0 : left < right ? -1 : 1;
|
|
108
|
+
}
|
|
109
|
+
return x.negative ? -magnitude : magnitude;
|
|
110
|
+
}
|
|
66
111
|
/** 两个数值是否相等(1 与 1n、Decimal('1.0') 与 1 都算相等) */
|
|
67
112
|
var sameNumeric = (a, b) => compareNumeric(a, b) === 0;
|
|
68
113
|
/** 值的类型:Decimal 取它自己的构造函数 */
|
|
@@ -128,6 +173,6 @@ function snapNumeric(value, step, min, max, kind) {
|
|
|
128
173
|
return snapped;
|
|
129
174
|
}
|
|
130
175
|
//#endregion
|
|
131
|
-
export { addNumeric, compareNumeric, isDecimal, isNumeric, isNumericText, multiplyNumeric, numericKind, numericText, sameNumeric, shiftDecimal, snapNumeric, toNumber, toNumericKind };
|
|
176
|
+
export { addNumeric, compareDecimalText, compareNumeric, decimalTextOf, isDecimal, isNumeric, isNumericText, multiplyNumeric, numericKind, numericText, sameNumeric, shiftDecimal, snapNumeric, toNumber, toNumericKind };
|
|
132
177
|
|
|
133
178
|
//# sourceMappingURL=numeric.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"numeric.js","names":[],"sources":["../../../src/utils/data/numeric.ts"],"sourcesContent":["/**\n * 内置支持的数值类型:number、bigint 与 decimal.js 的 Decimal(金额、精确运算)。\n * 组件的数值属性都接受它们(NumericValue),排序、格式化、表单提交不丢精度。\n *\n * compareNumeric(new Decimal('1.5'), 1.25) // 1:不同类型也按大小比\n * numericText(new Decimal('1e21')) // '1000000000000000000000':提交给服务端的十进制文本\n *\n * 这里不在运行时导入 decimal.js:按实例自己的构造函数识别与运算(使用方可能用 Decimal.clone 配了精度),\n * 没用到 Decimal 的应用不会因此打包它。\n */\nimport type Decimal from 'decimal.js';\nimport { snapToStep } from './number';\n\n/** 组件接受的数值 */\nexport type NumericValue = number | bigint | Decimal;\n\ntype DecimalConstructor = typeof Decimal;\n\n/** 是不是 decimal.js 的 Decimal(含 Decimal.clone 出来的构造函数的实例) */\nexport function isDecimal(value: unknown): value is Decimal {\n if (typeof value !== 'object' || value === null) return false;\n const constructor = (value as { constructor?: Partial<DecimalConstructor> }).constructor;\n return typeof constructor?.isDecimal === 'function' && constructor.isDecimal(value);\n}\n\n/** 能读成数字的文字:接口常把金额给成十进制字符串('19.90');空白、Infinity、不是数字的都不算 */\nexport function isNumericText(value: unknown): value is string {\n return typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value));\n}\n\n/** 是不是组件接受的数值(NaN 也算 number,由使用方决定怎么处理) */\nexport function isNumeric(value: unknown): value is NumericValue {\n return typeof value === 'number' || typeof value === 'bigint' || isDecimal(value);\n}\n\n/** 转成 number:只给需要浮点数的地方用(动画的中间帧、图表坐标、aria-valuenow),会丢精度 */\nexport function toNumber(value: NumericValue): number {\n if (typeof value === 'number') return value;\n if (typeof value === 'bigint') return Number(value);\n return value.toNumber();\n}\n\n/** 十进制文本,不用科学计数法:表单提交、复制给服务端;NaN、Infinity 原样 */\nexport function numericText(value: NumericValue): string {\n if (typeof value === 'bigint') return value.toString();\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) return String(value);\n // 1e21 以上、1e-6 以下 String() 会写成科学计数法;toLocaleString 不带分组时给出完整的十进制\n const plain = value === 0 || (Math.abs(value) < 1e21 && Math.abs(value) >= 1e-6);\n return plain\n ? String(value)\n : value.toLocaleString('en-US', { useGrouping: false, maximumFractionDigits: 20 });\n }\n return value.isFinite() ? value.toFixed() : value.toString();\n}\n\n/**\n * 十进制文本的小数点左移 places 位(除以 10 的 places 次方;负数右移),按文字移位、不经过浮点数:\n * `shiftDecimal('155353.2', 4)` → `'15.53532'`。不是普通十进制写法的文本(NaN、科学计数法)原样返回\n */\nexport function shiftDecimal(text: string, places: number): string {\n const match = /^([+-]?)(\\d*)(?:\\.(\\d*))?$/.exec(text);\n if (!match || places === 0 || !/\\d/.test(text)) return text;\n const [, sign, whole = '', fraction = ''] = match;\n const digits = whole + fraction;\n const point = whole.length - places;\n const padded = point <= 0 ? '0'.repeat(1 - point) + digits : digits.padEnd(point, '0');\n const at = Math.max(point, 1);\n const integer = padded.slice(0, at).replace(/^0+(?=\\d)/, '');\n const decimal = padded.slice(at).replace(/0+$/, '');\n return `${sign}${integer}${decimal ? `.${decimal}` : ''}`;\n}\n\n/** 用 Decimal 的构造函数(从 a 或 b 里找)把两个值都转成 Decimal 比较 */\nfunction compareWithDecimal(a: NumericValue, b: NumericValue, Ctor: DecimalConstructor): number {\n const left = isDecimal(a) ? a : new Ctor(numericText(a));\n return left.cmp(isDecimal(b) ? b : new Ctor(numericText(b)));\n}\n\n/**\n * 按大小比较两个数值,不同类型混着也行:返回负数、0 或正数(同 Array.prototype.sort 的约定)。\n * 有 Decimal 时按十进制精确比较;number 与 bigint 之间不经过浮点数(大整数不丢精度);NaN 排在最后\n */\nexport function compareNumeric(a: NumericValue, b: NumericValue): number {\n if (typeof a === 'number' && Number.isNaN(a))\n return typeof b === 'number' && Number.isNaN(b) ? 0 : 1;\n if (typeof b === 'number' && Number.isNaN(b)) return -1;\n if (isDecimal(a)) return compareWithDecimal(a, b, a.constructor as DecimalConstructor);\n if (isDecimal(b)) return compareWithDecimal(a, b, b.constructor as DecimalConstructor);\n // number、bigint 之间:JavaScript 的关系运算符对混合类型按数学值比较,不丢精度\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\n/** 两个数值是否相等(1 与 1n、Decimal('1.0') 与 1 都算相等) */\nexport const sameNumeric = (a: NumericValue, b: NumericValue) => compareNumeric(a, b) === 0;\n\n/** 数值写回成什么类型:number、bigint,或某个 Decimal 构造函数(使用方 Decimal.clone 配过精度的也行) */\nexport type NumericKind = 'number' | 'bigint' | DecimalConstructor;\n\n/** 值的类型:Decimal 取它自己的构造函数 */\nexport function numericKind(value: NumericValue): NumericKind {\n if (typeof value === 'number') return 'number';\n if (typeof value === 'bigint') return 'bigint';\n return value.constructor as DecimalConstructor;\n}\n\n/** 十进制文本('-12.5')→ 整数,四舍五入(远离 0) */\nfunction roundedBigInt(text: string): bigint {\n const [whole = '0', fraction = ''] = text.replace(/^[+-]/, '').split('.');\n const negative = text.startsWith('-');\n let result = BigInt(whole || '0');\n if (Number(fraction[0] ?? 0) >= 5) result += 1n;\n return negative ? -result : result;\n}\n\n/**\n * 转成指定类型;value 也可以是十进制文本('-12.5',输入框解析出来的)。\n * bigint 四舍五入到整数,Decimal 按十进制文本转(不经过浮点数)\n */\nexport function toNumericKind(value: NumericValue | string, kind: NumericKind): NumericValue {\n const text = () => (typeof value === 'string' ? value : numericText(value));\n if (kind === 'number') return typeof value === 'string' ? Number(value) : toNumber(value);\n if (kind === 'bigint') return typeof value === 'bigint' ? value : roundedBigInt(text());\n return isDecimal(value) && value.constructor === kind ? value : new kind(text());\n}\n\n/** 按 kind 精确相加:Decimal 用它的构造函数算,bigint 整数相加,number 浮点相加(再由 snapNumeric 去掉误差) */\nexport function addNumeric(value: NumericValue, delta: NumericValue, kind: NumericKind) {\n const left = toNumericKind(value, kind);\n const right = toNumericKind(delta, kind);\n if (typeof left === 'number') return left + (right as number);\n if (typeof left === 'bigint') return left + (right as bigint);\n return (left as Decimal).plus(right as Decimal);\n}\n\n/** value 的 times 倍(times 是整数,如 -1、10),按 kind 精确运算 */\nexport function multiplyNumeric(value: NumericValue, times: number, kind: NumericKind) {\n const typed = toNumericKind(value, kind);\n if (typeof typed === 'number') return typed * times;\n if (typeof typed === 'bigint') return typed * BigInt(times);\n return (typed as Decimal).times(times);\n}\n\n/**\n * 夹在 [min, max] 之间并对齐到 min + n × step(没有 min 时对齐到 0 + n × step),按 kind 精确运算:\n * number 走 snapToStep(按小数位取整去掉浮点误差),bigint 的步长至少为 1,Decimal 用值自己的构造函数\n */\nexport function snapNumeric(\n value: NumericValue,\n step: NumericValue,\n min: NumericValue | undefined,\n max: NumericValue | undefined,\n kind: NumericKind,\n): NumericValue {\n if (kind === 'number') {\n return snapToStep(\n toNumber(value),\n toNumber(step),\n min == null ? -Infinity : toNumber(min),\n max == null ? Infinity : toNumber(max),\n );\n }\n const at = (item: NumericValue) => toNumericKind(item, kind);\n let snapped: NumericValue;\n if (kind === 'bigint') {\n const size = (at(step) as bigint) > 0n ? (at(step) as bigint) : 1n;\n const base = min == null ? 0n : (at(min) as bigint);\n const offset = (at(value) as bigint) - base;\n // 整数除法四舍五入(远离 0)\n const half = offset < 0n ? -size : size;\n snapped = base + ((2n * offset + half) / (2n * size)) * size;\n } else {\n const base = (min == null ? new kind(0) : at(min)) as Decimal;\n const size = at(step) as Decimal;\n snapped = base.plus((at(value) as Decimal).minus(base).div(size).round().times(size));\n }\n if (min != null && compareNumeric(snapped, min) < 0) snapped = at(min);\n if (max != null && compareNumeric(snapped, max) > 0) snapped = at(max);\n return snapped;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"numeric.js","names":[],"sources":["../../../src/utils/data/numeric.ts"],"sourcesContent":["/**\n * 内置支持的数值类型:number、bigint 与 decimal.js 的 Decimal(金额、精确运算)。\n * 组件的数值属性都接受它们(NumericValue),排序、格式化、表单提交不丢精度。\n *\n * compareNumeric(new Decimal('1.5'), 1.25) // 1:不同类型也按大小比\n * numericText(new Decimal('1e21')) // '1000000000000000000000':提交给服务端的十进制文本\n *\n * 这里不在运行时导入 decimal.js:按实例自己的构造函数识别与运算(使用方可能用 Decimal.clone 配了精度),\n * 没用到 Decimal 的应用不会因此打包它。\n */\nimport type Decimal from 'decimal.js';\nimport { snapToStep } from './number';\n\n/** 组件接受的数值 */\nexport type NumericValue = number | bigint | Decimal;\n\ntype DecimalConstructor = typeof Decimal;\n\n/** 是不是 decimal.js 的 Decimal(含 Decimal.clone 出来的构造函数的实例) */\nexport function isDecimal(value: unknown): value is Decimal {\n if (typeof value !== 'object' || value === null) return false;\n const constructor = (value as { constructor?: Partial<DecimalConstructor> }).constructor;\n return typeof constructor?.isDecimal === 'function' && constructor.isDecimal(value);\n}\n\n/** 能读成数字的文字:接口常把金额给成十进制字符串('19.90');空白、Infinity、不是数字的都不算 */\nexport function isNumericText(value: unknown): value is string {\n return typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value));\n}\n\n/** 是不是组件接受的数值(NaN 也算 number,由使用方决定怎么处理) */\nexport function isNumeric(value: unknown): value is NumericValue {\n return typeof value === 'number' || typeof value === 'bigint' || isDecimal(value);\n}\n\n/** 转成 number:只给需要浮点数的地方用(动画的中间帧、图表坐标、aria-valuenow),会丢精度 */\nexport function toNumber(value: NumericValue): number {\n if (typeof value === 'number') return value;\n if (typeof value === 'bigint') return Number(value);\n return value.toNumber();\n}\n\n/** 十进制文本,不用科学计数法:表单提交、复制给服务端;NaN、Infinity 原样 */\nexport function numericText(value: NumericValue): string {\n if (typeof value === 'bigint') return value.toString();\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) return String(value);\n // 1e21 以上、1e-6 以下 String() 会写成科学计数法;toLocaleString 不带分组时给出完整的十进制\n const plain = value === 0 || (Math.abs(value) < 1e21 && Math.abs(value) >= 1e-6);\n return plain\n ? String(value)\n : value.toLocaleString('en-US', { useGrouping: false, maximumFractionDigits: 20 });\n }\n return value.isFinite() ? value.toFixed() : value.toString();\n}\n\n/**\n * 十进制文本的小数点左移 places 位(除以 10 的 places 次方;负数右移),按文字移位、不经过浮点数:\n * `shiftDecimal('155353.2', 4)` → `'15.53532'`。不是普通十进制写法的文本(NaN、科学计数法)原样返回\n */\nexport function shiftDecimal(text: string, places: number): string {\n const match = /^([+-]?)(\\d*)(?:\\.(\\d*))?$/.exec(text);\n if (!match || places === 0 || !/\\d/.test(text)) return text;\n const [, sign, whole = '', fraction = ''] = match;\n const digits = whole + fraction;\n const point = whole.length - places;\n const padded = point <= 0 ? '0'.repeat(1 - point) + digits : digits.padEnd(point, '0');\n const at = Math.max(point, 1);\n const integer = padded.slice(0, at).replace(/^0+(?=\\d)/, '');\n const decimal = padded.slice(at).replace(/0+$/, '');\n return `${sign}${integer}${decimal ? `.${decimal}` : ''}`;\n}\n\n/** 用 Decimal 的构造函数(从 a 或 b 里找)把两个值都转成 Decimal 比较 */\nfunction compareWithDecimal(a: NumericValue, b: NumericValue, Ctor: DecimalConstructor): number {\n const left = isDecimal(a) ? a : new Ctor(numericText(a));\n return left.cmp(isDecimal(b) ? b : new Ctor(numericText(b)));\n}\n\n/**\n * 按大小比较两个数值,不同类型混着也行:返回负数、0 或正数(同 Array.prototype.sort 的约定)。\n * 有 Decimal 时按十进制精确比较;number 与 bigint 之间不经过浮点数(大整数不丢精度);NaN 排在最后\n */\nexport function compareNumeric(a: NumericValue, b: NumericValue): number {\n if (typeof a === 'number' && Number.isNaN(a))\n return typeof b === 'number' && Number.isNaN(b) ? 0 : 1;\n if (typeof b === 'number' && Number.isNaN(b)) return -1;\n if (isDecimal(a)) return compareWithDecimal(a, b, a.constructor as DecimalConstructor);\n if (isDecimal(b)) return compareWithDecimal(a, b, b.constructor as DecimalConstructor);\n // number、bigint 之间:JavaScript 的关系运算符对混合类型按数学值比较,不丢精度\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\n/** 十进制写法(可带指数):'19.90'、'-.5'、'1e3' */\nconst DECIMAL_TEXT =\n /^([+-]?)(\\d+)(?:\\.(\\d*))?(?:[eE]([+-]?\\d+))?$|^([+-]?)\\.(\\d+)(?:[eE]([+-]?\\d+))?$/;\n\n/** 拆开的十进制数:符号、去掉前导零的整数位、去掉末尾零的小数位 */\ninterface DecimalParts {\n negative: boolean;\n int: string;\n frac: string;\n}\n\nfunction decimalParts(text: string): DecimalParts | undefined {\n const match = DECIMAL_TEXT.exec(text);\n if (!match) return undefined;\n const [, sign1, int1, frac1, exp1, sign2, frac2, exp2] = match;\n const intDigits = int1 ?? '';\n const digits = intDigits + (frac1 ?? frac2 ?? '');\n // 小数点按指数移动后,再拆成整数位与小数位\n const point = intDigits.length + Number(exp1 ?? exp2 ?? 0);\n const padded =\n point < 0\n ? '0'.repeat(-point) + digits\n : point > digits.length\n ? digits + '0'.repeat(point - digits.length)\n : digits;\n const at = Math.max(point, 0);\n const int = padded.slice(0, at).replace(/^0+/, '');\n const frac = padded.slice(at).replace(/0+$/, '');\n return { negative: (sign1 ?? sign2) === '-' && (int !== '' || frac !== ''), int, frac };\n}\n\n/**\n * 能精确比较的值 → 十进制文本:number(NaN、Infinity 不算)、bigint、Decimal、十进制写法的文字;其余是 undefined。\n * 条件比较(input/condition.ts)用它:与流程引擎模型包的规则一致,长小数、大整数不丢精度\n */\nexport function decimalTextOf(value: unknown): string | undefined {\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : undefined;\n if (typeof value === 'bigint' || isDecimal(value)) return numericText(value);\n if (typeof value === 'string' && DECIMAL_TEXT.test(value.trim())) return value.trim();\n return undefined;\n}\n\n/** 两段十进制文本(decimalTextOf 给出的)按数值比:负数、0、正数;不经过浮点数 */\nexport function compareDecimalText(a: string, b: string): number {\n const x = decimalParts(a);\n const y = decimalParts(b);\n if (!x || !y) return a === b ? 0 : a < b ? -1 : 1;\n if (x.negative !== y.negative) return x.negative ? -1 : 1;\n let magnitude = 0;\n if (x.int.length !== y.int.length) magnitude = x.int.length < y.int.length ? -1 : 1;\n else if (x.int !== y.int) magnitude = x.int < y.int ? -1 : 1;\n else {\n const width = Math.max(x.frac.length, y.frac.length);\n const left = x.frac.padEnd(width, '0');\n const right = y.frac.padEnd(width, '0');\n magnitude = left === right ? 0 : left < right ? -1 : 1;\n }\n return x.negative ? -magnitude : magnitude;\n}\n\n/** 两个数值是否相等(1 与 1n、Decimal('1.0') 与 1 都算相等) */\nexport const sameNumeric = (a: NumericValue, b: NumericValue) => compareNumeric(a, b) === 0;\n\n/** 数值写回成什么类型:number、bigint,或某个 Decimal 构造函数(使用方 Decimal.clone 配过精度的也行) */\nexport type NumericKind = 'number' | 'bigint' | DecimalConstructor;\n\n/** 值的类型:Decimal 取它自己的构造函数 */\nexport function numericKind(value: NumericValue): NumericKind {\n if (typeof value === 'number') return 'number';\n if (typeof value === 'bigint') return 'bigint';\n return value.constructor as DecimalConstructor;\n}\n\n/** 十进制文本('-12.5')→ 整数,四舍五入(远离 0) */\nfunction roundedBigInt(text: string): bigint {\n const [whole = '0', fraction = ''] = text.replace(/^[+-]/, '').split('.');\n const negative = text.startsWith('-');\n let result = BigInt(whole || '0');\n if (Number(fraction[0] ?? 0) >= 5) result += 1n;\n return negative ? -result : result;\n}\n\n/**\n * 转成指定类型;value 也可以是十进制文本('-12.5',输入框解析出来的)。\n * bigint 四舍五入到整数,Decimal 按十进制文本转(不经过浮点数)\n */\nexport function toNumericKind(value: NumericValue | string, kind: NumericKind): NumericValue {\n const text = () => (typeof value === 'string' ? value : numericText(value));\n if (kind === 'number') return typeof value === 'string' ? Number(value) : toNumber(value);\n if (kind === 'bigint') return typeof value === 'bigint' ? value : roundedBigInt(text());\n return isDecimal(value) && value.constructor === kind ? value : new kind(text());\n}\n\n/** 按 kind 精确相加:Decimal 用它的构造函数算,bigint 整数相加,number 浮点相加(再由 snapNumeric 去掉误差) */\nexport function addNumeric(value: NumericValue, delta: NumericValue, kind: NumericKind) {\n const left = toNumericKind(value, kind);\n const right = toNumericKind(delta, kind);\n if (typeof left === 'number') return left + (right as number);\n if (typeof left === 'bigint') return left + (right as bigint);\n return (left as Decimal).plus(right as Decimal);\n}\n\n/** value 的 times 倍(times 是整数,如 -1、10),按 kind 精确运算 */\nexport function multiplyNumeric(value: NumericValue, times: number, kind: NumericKind) {\n const typed = toNumericKind(value, kind);\n if (typeof typed === 'number') return typed * times;\n if (typeof typed === 'bigint') return typed * BigInt(times);\n return (typed as Decimal).times(times);\n}\n\n/**\n * 夹在 [min, max] 之间并对齐到 min + n × step(没有 min 时对齐到 0 + n × step),按 kind 精确运算:\n * number 走 snapToStep(按小数位取整去掉浮点误差),bigint 的步长至少为 1,Decimal 用值自己的构造函数\n */\nexport function snapNumeric(\n value: NumericValue,\n step: NumericValue,\n min: NumericValue | undefined,\n max: NumericValue | undefined,\n kind: NumericKind,\n): NumericValue {\n if (kind === 'number') {\n return snapToStep(\n toNumber(value),\n toNumber(step),\n min == null ? -Infinity : toNumber(min),\n max == null ? Infinity : toNumber(max),\n );\n }\n const at = (item: NumericValue) => toNumericKind(item, kind);\n let snapped: NumericValue;\n if (kind === 'bigint') {\n const size = (at(step) as bigint) > 0n ? (at(step) as bigint) : 1n;\n const base = min == null ? 0n : (at(min) as bigint);\n const offset = (at(value) as bigint) - base;\n // 整数除法四舍五入(远离 0)\n const half = offset < 0n ? -size : size;\n snapped = base + ((2n * offset + half) / (2n * size)) * size;\n } else {\n const base = (min == null ? new kind(0) : at(min)) as Decimal;\n const size = at(step) as Decimal;\n snapped = base.plus((at(value) as Decimal).minus(base).div(size).round().times(size));\n }\n if (min != null && compareNumeric(snapped, min) < 0) snapped = at(min);\n if (max != null && compareNumeric(snapped, max) > 0) snapped = at(max);\n return snapped;\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 浏览器里存文件、挑文件:导出代码、CSV、SVG;导入一份 JSON、一张表。
|
|
3
3
|
* 内容原样写入:给 Excel 打开的 CSV 要由调用方在开头加 BOM(`'\uFEFF' + csv`),中文才不乱码。
|
|
4
4
|
*
|
|
5
5
|
* downloadFile('theme.ts', code, 'text/typescript');
|
|
6
|
+
* const file = await pickFile('.json');
|
|
7
|
+
* if (file) importFrom(await file.text());
|
|
6
8
|
*/
|
|
7
9
|
export declare function downloadFile(name: string, content: BlobPart, type: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* 弹出系统的选文件对话框,挑一个文件;取消时为 null。要在用户的点击里调(浏览器只在用户操作时允许弹)。
|
|
12
|
+
* accept 同 input 的 accept(`.json`、`image/*`)。
|
|
13
|
+
*/
|
|
14
|
+
export declare function pickFile(accept?: string): Promise<File | null>;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
//#region src/utils/dom/download.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* 浏览器里存文件、挑文件:导出代码、CSV、SVG;导入一份 JSON、一张表。
|
|
4
4
|
* 内容原样写入:给 Excel 打开的 CSV 要由调用方在开头加 BOM(`'\uFEFF' + csv`),中文才不乱码。
|
|
5
5
|
*
|
|
6
6
|
* downloadFile('theme.ts', code, 'text/typescript');
|
|
7
|
+
* const file = await pickFile('.json');
|
|
8
|
+
* if (file) importFrom(await file.text());
|
|
7
9
|
*/
|
|
8
10
|
function downloadFile(name, content, type) {
|
|
9
11
|
const url = URL.createObjectURL(new Blob([content], { type }));
|
|
@@ -13,7 +15,22 @@ function downloadFile(name, content, type) {
|
|
|
13
15
|
}).click();
|
|
14
16
|
setTimeout(() => URL.revokeObjectURL(url), 4e4);
|
|
15
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* 弹出系统的选文件对话框,挑一个文件;取消时为 null。要在用户的点击里调(浏览器只在用户操作时允许弹)。
|
|
20
|
+
* accept 同 input 的 accept(`.json`、`image/*`)。
|
|
21
|
+
*/
|
|
22
|
+
function pickFile(accept) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
const input = Object.assign(document.createElement("input"), {
|
|
25
|
+
type: "file",
|
|
26
|
+
...accept ? { accept } : {}
|
|
27
|
+
});
|
|
28
|
+
input.addEventListener("change", () => resolve(input.files?.[0] ?? null), { once: true });
|
|
29
|
+
input.addEventListener("cancel", () => resolve(null), { once: true });
|
|
30
|
+
input.click();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
16
33
|
//#endregion
|
|
17
|
-
export { downloadFile };
|
|
34
|
+
export { downloadFile, pickFile };
|
|
18
35
|
|
|
19
36
|
//# sourceMappingURL=download.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"download.js","names":[],"sources":["../../../src/utils/dom/download.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"file":"download.js","names":[],"sources":["../../../src/utils/dom/download.ts"],"sourcesContent":["/**\n * 浏览器里存文件、挑文件:导出代码、CSV、SVG;导入一份 JSON、一张表。\n * 内容原样写入:给 Excel 打开的 CSV 要由调用方在开头加 BOM(`'\\uFEFF' + csv`),中文才不乱码。\n *\n * downloadFile('theme.ts', code, 'text/typescript');\n * const file = await pickFile('.json');\n * if (file) importFrom(await file.text());\n */\nexport function downloadFile(name: string, content: BlobPart, type: string): void {\n const url = URL.createObjectURL(new Blob([content], { type }));\n const link = Object.assign(document.createElement('a'), { href: url, download: name });\n link.click();\n // 点击后浏览器才开始读这个地址:Safari、Firefox 里立即回收会让下载失败,过一会儿再回收(同 FileSaver.js)\n setTimeout(() => URL.revokeObjectURL(url), 40_000);\n}\n\n/**\n * 弹出系统的选文件对话框,挑一个文件;取消时为 null。要在用户的点击里调(浏览器只在用户操作时允许弹)。\n * accept 同 input 的 accept(`.json`、`image/*`)。\n */\nexport function pickFile(accept?: string): Promise<File | null> {\n return new Promise((resolve) => {\n const input = Object.assign(document.createElement('input'), {\n type: 'file',\n ...(accept ? { accept } : {}),\n });\n input.addEventListener('change', () => resolve(input.files?.[0] ?? null), { once: true });\n input.addEventListener('cancel', () => resolve(null), { once: true });\n input.click();\n });\n}\n"],"mappings":";;;;;;;;;AAQA,SAAgB,aAAa,MAAc,SAAmB,MAAoB;CAChF,MAAM,MAAM,IAAI,gBAAgB,IAAI,KAAK,CAAC,OAAO,GAAG,EAAE,KAAK,CAAC,CAAC;CAE7D,OADoB,OAAO,SAAS,cAAc,GAAG,GAAG;EAAE,MAAM;EAAK,UAAU;CAAK,CACpF,CAAA,CAAK,MAAM;CAEX,iBAAiB,IAAI,gBAAgB,GAAG,GAAG,GAAM;AACnD;;;;;AAMA,SAAgB,SAAS,QAAuC;CAC9D,OAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,QAAQ,OAAO,OAAO,SAAS,cAAc,OAAO,GAAG;GAC3D,MAAM;GACN,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC7B,CAAC;EACD,MAAM,iBAAiB,gBAAgB,QAAQ,MAAM,QAAQ,MAAM,IAAI,GAAG,EAAE,MAAM,KAAK,CAAC;EACxF,MAAM,iBAAiB,gBAAgB,QAAQ,IAAI,GAAG,EAAE,MAAM,KAAK,CAAC;EACpE,MAAM,MAAM;CACd,CAAC;AACH"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Lang } from '../../config/lang/lang';
|
|
2
|
+
/** 错误上的业务错误码;认不出时为 undefined */
|
|
3
|
+
export declare function errorCodeOf(error: unknown): string | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* 给人看的错误文案:码在 lang.errorCodes 里有文案时用它(params 填进去);没有时用错误自己的 message(后端给的兜底、
|
|
6
|
+
* 应用的上传函数抛的说明),再没有用 lang.requestFailed
|
|
7
|
+
*/
|
|
8
|
+
export declare function errorMessage(error: unknown, lang: Lang): string;
|
|
9
|
+
/**
|
|
10
|
+
* 只按码取的文案;错误没有码、码没有文案时为 undefined。组件有自己的笼统提示时用它
|
|
11
|
+
* (远程选项:`errorCodeText(error, lang) ?? lang.listLoadFailed`),不把底层的英文说明露给用户
|
|
12
|
+
*/
|
|
13
|
+
export declare function errorCodeText(error: unknown, lang: Lang): string | undefined;
|
|
14
|
+
/** 一个码的文案(也用于 errors 明细里按字段的码);语言包里没有时为 undefined */
|
|
15
|
+
export declare function codeText(code: string, params: Readonly<Record<string, unknown>> | undefined, lang: Lang): string | undefined;
|
|
16
|
+
/** 当前语言下的 errorMessage(在 setup 里调用);返回的函数随 Provider 切换语言 */
|
|
17
|
+
export declare function useErrorMessage(): (error: unknown) => string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useLang } from "../../config/context.js";
|
|
2
|
+
import { HttpError, isAbortError } from "./http.js";
|
|
3
|
+
//#region src/utils/request/error-message.ts
|
|
4
|
+
/**
|
|
5
|
+
* 错误的文案:按业务错误码(flow 仓库 docs/decisions/0008)从语言包的 errorCodes 取,组件显示请求失败时都经过这里。
|
|
6
|
+
* 码的来源:HttpError 的 code(后端回的 `flow.`、`query.`、应用自己的,或请求层的 `http.`);取消是 `http.aborted`;
|
|
7
|
+
* 别的包的错误带 `包.码` 写法的 code 字段也认(query 的本地库 `query.quota-exceeded`、flow 的 FlowError)。
|
|
8
|
+
*
|
|
9
|
+
* const describe = useErrorMessage();
|
|
10
|
+
* describe(error); // → '数据已被他人修改或删除,请刷新后重试'
|
|
11
|
+
*
|
|
12
|
+
* 应用与 flow-ui 补自己命名空间的文案:`defineLang(zhCN, { errorCodes: { 'app.quota': '额度不足' } })`(与自带的合并)。
|
|
13
|
+
*/
|
|
14
|
+
/** 错误上的业务错误码;认不出时为 undefined */
|
|
15
|
+
function errorCodeOf(error) {
|
|
16
|
+
if (error instanceof HttpError) return error.code;
|
|
17
|
+
if (isAbortError(error)) return "http.aborted";
|
|
18
|
+
const code = typeof error === "object" && error !== null ? error.code : void 0;
|
|
19
|
+
return typeof code === "string" && code.includes(".") ? code : void 0;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 给人看的错误文案:码在 lang.errorCodes 里有文案时用它(params 填进去);没有时用错误自己的 message(后端给的兜底、
|
|
23
|
+
* 应用的上传函数抛的说明),再没有用 lang.requestFailed
|
|
24
|
+
*/
|
|
25
|
+
function errorMessage(error, lang) {
|
|
26
|
+
return errorCodeText(error, lang) ?? (error instanceof Error && error.message ? error.message : lang.requestFailed);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 只按码取的文案;错误没有码、码没有文案时为 undefined。组件有自己的笼统提示时用它
|
|
30
|
+
* (远程选项:`errorCodeText(error, lang) ?? lang.listLoadFailed`),不把底层的英文说明露给用户
|
|
31
|
+
*/
|
|
32
|
+
function errorCodeText(error, lang) {
|
|
33
|
+
const code = errorCodeOf(error);
|
|
34
|
+
if (code === void 0) return void 0;
|
|
35
|
+
return codeText(code, error.params, lang);
|
|
36
|
+
}
|
|
37
|
+
/** 一个码的文案(也用于 errors 明细里按字段的码);语言包里没有时为 undefined */
|
|
38
|
+
function codeText(code, params, lang) {
|
|
39
|
+
const text = lang.errorCodes[code];
|
|
40
|
+
return typeof text === "function" ? text(params ?? {}) : text;
|
|
41
|
+
}
|
|
42
|
+
/** 当前语言下的 errorMessage(在 setup 里调用);返回的函数随 Provider 切换语言 */
|
|
43
|
+
function useErrorMessage() {
|
|
44
|
+
const lang = useLang();
|
|
45
|
+
return (error) => errorMessage(error, lang);
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { codeText, errorCodeOf, errorCodeText, errorMessage, useErrorMessage };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=error-message.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-message.js","names":[],"sources":["../../../src/utils/request/error-message.ts"],"sourcesContent":["/**\n * 错误的文案:按业务错误码(flow 仓库 docs/decisions/0008)从语言包的 errorCodes 取,组件显示请求失败时都经过这里。\n * 码的来源:HttpError 的 code(后端回的 `flow.`、`query.`、应用自己的,或请求层的 `http.`);取消是 `http.aborted`;\n * 别的包的错误带 `包.码` 写法的 code 字段也认(query 的本地库 `query.quota-exceeded`、flow 的 FlowError)。\n *\n * const describe = useErrorMessage();\n * describe(error); // → '数据已被他人修改或删除,请刷新后重试'\n *\n * 应用与 flow-ui 补自己命名空间的文案:`defineLang(zhCN, { errorCodes: { 'app.quota': '额度不足' } })`(与自带的合并)。\n */\nimport { useLang } from '../../config/context';\nimport type { Lang } from '../../config/lang/lang';\nimport { HttpError, isAbortError } from './http';\n\n/** 错误上的业务错误码;认不出时为 undefined */\nexport function errorCodeOf(error: unknown): string | undefined {\n if (error instanceof HttpError) return error.code;\n if (isAbortError(error)) return 'http.aborted';\n const code =\n typeof error === 'object' && error !== null ? (error as { code?: unknown }).code : undefined;\n // 只认 `包.码` 的写法:Node 的 ENOENT、DOMException 的数字 code 不算\n return typeof code === 'string' && code.includes('.') ? code : undefined;\n}\n\n/**\n * 给人看的错误文案:码在 lang.errorCodes 里有文案时用它(params 填进去);没有时用错误自己的 message(后端给的兜底、\n * 应用的上传函数抛的说明),再没有用 lang.requestFailed\n */\nexport function errorMessage(error: unknown, lang: Lang): string {\n return (\n errorCodeText(error, lang) ??\n (error instanceof Error && error.message ? error.message : lang.requestFailed)\n );\n}\n\n/**\n * 只按码取的文案;错误没有码、码没有文案时为 undefined。组件有自己的笼统提示时用它\n * (远程选项:`errorCodeText(error, lang) ?? lang.listLoadFailed`),不把底层的英文说明露给用户\n */\nexport function errorCodeText(error: unknown, lang: Lang): string | undefined {\n const code = errorCodeOf(error);\n if (code === undefined) return undefined;\n return codeText(code, (error as { params?: Readonly<Record<string, unknown>> }).params, lang);\n}\n\n/** 一个码的文案(也用于 errors 明细里按字段的码);语言包里没有时为 undefined */\nexport function codeText(\n code: string,\n params: Readonly<Record<string, unknown>> | undefined,\n lang: Lang,\n): string | undefined {\n const text = lang.errorCodes[code];\n return typeof text === 'function' ? text(params ?? {}) : text;\n}\n\n/** 当前语言下的 errorMessage(在 setup 里调用);返回的函数随 Provider 切换语言 */\nexport function useErrorMessage(): (error: unknown) => string {\n const lang = useLang();\n return (error) => errorMessage(error, lang);\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,SAAgB,YAAY,OAAoC;CAC9D,IAAI,iBAAiB,WAAW,OAAO,MAAM;CAC7C,IAAI,aAAa,KAAK,GAAG,OAAO;CAChC,MAAM,OACJ,OAAO,UAAU,YAAY,UAAU,OAAQ,MAA6B,OAAO,KAAA;CAErF,OAAO,OAAO,SAAS,YAAY,KAAK,SAAS,GAAG,IAAI,OAAO,KAAA;AACjE;;;;;AAMA,SAAgB,aAAa,OAAgB,MAAoB;CAC/D,OACE,cAAc,OAAO,IAAI,MACxB,iBAAiB,SAAS,MAAM,UAAU,MAAM,UAAU,KAAK;AAEpE;;;;;AAMA,SAAgB,cAAc,OAAgB,MAAgC;CAC5E,MAAM,OAAO,YAAY,KAAK;CAC9B,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;CAC/B,OAAO,SAAS,MAAO,MAAyD,QAAQ,IAAI;AAC9F;;AAGA,SAAgB,SACd,MACA,QACA,MACoB;CACpB,MAAM,OAAO,KAAK,WAAW;CAC7B,OAAO,OAAO,SAAS,aAAa,KAAK,UAAU,CAAC,CAAC,IAAI;AAC3D;;AAGA,SAAgB,kBAA8C;CAC5D,MAAM,OAAO,QAAQ;CACrB,QAAQ,UAAU,aAAa,OAAO,IAAI;AAC5C"}
|
|
@@ -30,8 +30,52 @@ export interface HttpClient {
|
|
|
30
30
|
/** 发出一次请求,返回解析(拆包)后的数据;失败时以 HttpError 拒绝,取消时以 AbortError 拒绝 */
|
|
31
31
|
request<T = unknown>(request: HttpRequest): Promise<T>;
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
33
|
+
/** 成功的码 */
|
|
34
|
+
export declare const OK_CODE = "ok";
|
|
35
|
+
/**
|
|
36
|
+
* 请求层自己出的错的码(0008 的 `http.` 命名空间,只在前端产生)。取消不当作错误,仍以 AbortError 拒绝,
|
|
37
|
+
* errorCodeOf 把它认作 `http.aborted`
|
|
38
|
+
*/
|
|
39
|
+
export declare const HTTP_ERROR_CODES: readonly ['http.network', 'http.timeout', 'http.aborted', 'http.status', 'http.parse'];
|
|
40
|
+
/** 请求层的错误码 */
|
|
41
|
+
export type HttpErrorCode = (typeof HTTP_ERROR_CODES)[number];
|
|
42
|
+
/**
|
|
43
|
+
* 错误明细的一项(0008):按字段的(表单校验:field、code),或按位置的(流程定义的问题:node、edge);没有的部分省掉。
|
|
44
|
+
* 后端只给说明、不给代号时有 message
|
|
45
|
+
*/
|
|
46
|
+
export interface ApiErrorDetail {
|
|
47
|
+
/** 问题的代号(required、max……) */
|
|
48
|
+
readonly code?: string;
|
|
49
|
+
/** 字段路径(`email`、`lines[0].price`) */
|
|
50
|
+
readonly field?: string;
|
|
51
|
+
/** 后端给的说明,只作兜底 */
|
|
52
|
+
readonly message?: string;
|
|
53
|
+
/** 文案里要填的参数 */
|
|
54
|
+
readonly params?: Readonly<Record<string, unknown>>;
|
|
55
|
+
/** 别的位置信息(节点、连线……) */
|
|
56
|
+
readonly [key: string]: unknown;
|
|
57
|
+
}
|
|
58
|
+
/** 后端回的响应体(0008 第 1 节):成功的 code 是 `ok`,失败是业务错误码 */
|
|
59
|
+
export interface ApiEnvelope<T = unknown> {
|
|
60
|
+
/** `ok` 或业务错误码 */
|
|
61
|
+
readonly code: string;
|
|
62
|
+
/** 后端给的说明,只作兜底:界面按 code 取文案 */
|
|
63
|
+
readonly message?: string;
|
|
64
|
+
/** 文案里要填的参数 */
|
|
65
|
+
readonly params?: Readonly<Record<string, unknown>>;
|
|
66
|
+
/** 按字段、按问题的明细(表单校验) */
|
|
67
|
+
readonly errors?: readonly ApiErrorDetail[];
|
|
68
|
+
/** 成功时的数据 */
|
|
69
|
+
readonly data?: T;
|
|
70
|
+
}
|
|
71
|
+
/** 请求失败时的上下文:业务错误码、发生在哪个请求、拿到的响应 */
|
|
34
72
|
export interface HttpErrorContext {
|
|
73
|
+
/** 业务错误码:后端回的(`flow.`、`query.`、应用自己的)或请求层的(`http.`) */
|
|
74
|
+
code: string;
|
|
75
|
+
/** 文案里要填的参数 */
|
|
76
|
+
params?: Readonly<Record<string, unknown>>;
|
|
77
|
+
/** 按字段、按问题的明细 */
|
|
78
|
+
errors?: readonly ApiErrorDetail[];
|
|
35
79
|
/** 出错的请求 */
|
|
36
80
|
request: HttpRequest;
|
|
37
81
|
/** 响应;网络错误、超时时没有 */
|
|
@@ -41,10 +85,16 @@ export interface HttpErrorContext {
|
|
|
41
85
|
/** 底层的错误(网络错误、超时) */
|
|
42
86
|
cause?: unknown;
|
|
43
87
|
}
|
|
44
|
-
/**
|
|
88
|
+
/** 请求失败:后端回了业务错误码、状态码不是 2xx、网络错误、超时、响应读不懂,或 onResponse 判定的业务错误 */
|
|
45
89
|
export declare class HttpError extends Error {
|
|
46
90
|
/** 错误名,同类名:日志与 instanceof 之外的判断用 */
|
|
47
91
|
readonly name = "HttpError";
|
|
92
|
+
/** 业务错误码(见 HttpErrorContext.code);界面按它取文案 */
|
|
93
|
+
readonly code: string;
|
|
94
|
+
/** 文案里要填的参数 */
|
|
95
|
+
readonly params: Readonly<Record<string, unknown>>;
|
|
96
|
+
/** 按字段、按问题的明细;useForm 的 submit 据此标到字段上 */
|
|
97
|
+
readonly errors: readonly ApiErrorDetail[];
|
|
48
98
|
/** 出错的请求 */
|
|
49
99
|
readonly request: HttpRequest;
|
|
50
100
|
/** 响应;网络错误、超时时没有 */
|
|
@@ -53,7 +103,7 @@ export declare class HttpError extends Error {
|
|
|
53
103
|
readonly status: number;
|
|
54
104
|
/** 响应体(错误详情) */
|
|
55
105
|
readonly data?: unknown;
|
|
56
|
-
constructor(message: string, { request, response, data, cause }: HttpErrorContext);
|
|
106
|
+
constructor(message: string, { code, params, errors, request, response, data, cause }: HttpErrorContext);
|
|
57
107
|
}
|
|
58
108
|
/** 是不是主动取消(abort)造成的拒绝:界面上不当作错误 */
|
|
59
109
|
export declare const isAbortError: (error: unknown) => boolean;
|
|
@@ -67,7 +117,12 @@ export interface HttpClientOptions {
|
|
|
67
117
|
timeout?: number;
|
|
68
118
|
/** 发出之前改请求(加令牌、追踪编号);可以返回一个新的 Request */
|
|
69
119
|
onRequest?: (request: Request) => Request | void | Promise<Request | void>;
|
|
70
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* 按 0008 的约定拆响应体(见文件开头),默认 true:键都在 code、message、params、errors、data 里、code 是文字的对象
|
|
122
|
+
* 才当作包装,其余原样交出。后端用别的包装时给 false,在 onResponse 里拆
|
|
123
|
+
*/
|
|
124
|
+
envelope?: boolean;
|
|
125
|
+
/** 成功的响应(拆包之后)的数据交给它:再加工、判定别的业务错误(抛 HttpError),返回值就是请求的结果 */
|
|
71
126
|
onResponse?: (data: unknown, context: {
|
|
72
127
|
request: HttpRequest;
|
|
73
128
|
response: Response;
|
|
@@ -95,6 +150,15 @@ export interface FetchClient extends HttpClient {
|
|
|
95
150
|
}
|
|
96
151
|
/** 原生 fetch 的薄封装(见文件开头) */
|
|
97
152
|
export declare function createHttpClient(options?: HttpClientOptions): FetchClient;
|
|
153
|
+
/**
|
|
154
|
+
* 在任意请求客户端上加常用方法的简写,相对地址前面拼上 prefix(斜杠不重复;完整地址原样)。
|
|
155
|
+
* 一个模块的接口都在同一个前缀下时用它,不必每次写全地址;useHttp() 拿到的客户端也能这样包:
|
|
156
|
+
*
|
|
157
|
+
* const api = scopeHttp(useHttp(), '/flow');
|
|
158
|
+
* api.get('/queries/todo', { query: { page: 1 }, signal }); // GET /flow/queries/todo?page=1
|
|
159
|
+
* api.post('/commands/claim', { workItemId });
|
|
160
|
+
*/
|
|
161
|
+
export declare function scopeHttp(client: HttpClient, prefix?: string): FetchClient;
|
|
98
162
|
/**
|
|
99
163
|
* 当前配置的请求客户端(在 setup 里调用):createUi / Provider 的 http;都没给时是一个默认的 createHttpClient()
|
|
100
164
|
*/
|
|
@@ -4,28 +4,51 @@ import { fromWire, toWire, wireText } from "./wire.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* 请求层:组件(远程选项、上传、列表查询)只依赖 HttpClient 接口,应用经 createUi / Provider 的 http 给出实现。
|
|
6
6
|
* 默认实现 createHttpClient 是原生 fetch 的薄封装(参考 ky、ofetch):baseURL、query、JSON 请求体、超时与取消、
|
|
7
|
-
*
|
|
7
|
+
* 拆响应体、带码的错误(HttpError)、请求与响应钩子。用 axios、ky 时只要写一个 request:
|
|
8
8
|
*
|
|
9
9
|
* createUi({ http: { request: (r) => axios.request({ ...r, params: r.query, data: r.body }).then((res) => res.data) } })
|
|
10
10
|
*
|
|
11
11
|
* 值按线上的 typed JSON 传(见 wire.ts):请求体里的 bigint、Decimal、CalendarDate 写成文字并附上 `$types`,
|
|
12
12
|
* 地址栏参数写成同样的文字,带 `$types` 的响应先还原再交给 onResponse;`wire: false` 时按普通 JSON。
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* 响应体按业务错误码的约定拆开(flow 仓库 docs/decisions/0008,query、flow 的后端都这样回):HTTP 一律 200,
|
|
15
|
+
* `{ code: 'ok', data }` 交出 data,`{ code: 'flow.work-item-not-open', message, params, errors }` 抛带这个码的 HttpError;
|
|
16
|
+
* 请求层自己出的错也带码:`http.network`、`http.timeout`、`http.status`(网关回了非 2xx)、`http.parse`(响应读不懂)。
|
|
17
|
+
* 组件按码从语言包取文案(见 error-message.ts)。后端用别的包装(`{ code: 0, data }`)时关掉 envelope,在 onResponse 里拆:
|
|
15
18
|
*
|
|
16
19
|
* const http = createHttpClient({
|
|
17
20
|
* baseURL: '/api',
|
|
21
|
+
* envelope: false,
|
|
18
22
|
* onRequest: (request) => request.headers.set('Authorization', `Bearer ${token()}`),
|
|
19
23
|
* onResponse: (body: Envelope, context) => {
|
|
20
|
-
* if (body.code !== 0) throw new HttpError(body.message, { ...context, data: body });
|
|
24
|
+
* if (body.code !== 0) throw new HttpError(body.message, { ...context, code: `app.${body.code}`, data: body });
|
|
21
25
|
* return body.data;
|
|
22
26
|
* },
|
|
23
27
|
* });
|
|
24
28
|
*/
|
|
25
|
-
/**
|
|
29
|
+
/** 成功的码 */
|
|
30
|
+
var OK_CODE = "ok";
|
|
31
|
+
/**
|
|
32
|
+
* 请求层自己出的错的码(0008 的 `http.` 命名空间,只在前端产生)。取消不当作错误,仍以 AbortError 拒绝,
|
|
33
|
+
* errorCodeOf 把它认作 `http.aborted`
|
|
34
|
+
*/
|
|
35
|
+
var HTTP_ERROR_CODES = [
|
|
36
|
+
"http.network",
|
|
37
|
+
"http.timeout",
|
|
38
|
+
"http.aborted",
|
|
39
|
+
"http.status",
|
|
40
|
+
"http.parse"
|
|
41
|
+
];
|
|
42
|
+
/** 请求失败:后端回了业务错误码、状态码不是 2xx、网络错误、超时、响应读不懂,或 onResponse 判定的业务错误 */
|
|
26
43
|
var HttpError = class extends Error {
|
|
27
44
|
/** 错误名,同类名:日志与 instanceof 之外的判断用 */
|
|
28
45
|
name = "HttpError";
|
|
46
|
+
/** 业务错误码(见 HttpErrorContext.code);界面按它取文案 */
|
|
47
|
+
code;
|
|
48
|
+
/** 文案里要填的参数 */
|
|
49
|
+
params;
|
|
50
|
+
/** 按字段、按问题的明细;useForm 的 submit 据此标到字段上 */
|
|
51
|
+
errors;
|
|
29
52
|
/** 出错的请求 */
|
|
30
53
|
request;
|
|
31
54
|
/** 响应;网络错误、超时时没有 */
|
|
@@ -34,8 +57,11 @@ var HttpError = class extends Error {
|
|
|
34
57
|
status;
|
|
35
58
|
/** 响应体(错误详情) */
|
|
36
59
|
data;
|
|
37
|
-
constructor(message, { request, response, data, cause }) {
|
|
60
|
+
constructor(message, { code, params, errors, request, response, data, cause }) {
|
|
38
61
|
super(message, { cause });
|
|
62
|
+
this.code = code;
|
|
63
|
+
this.params = params ?? {};
|
|
64
|
+
this.errors = errors ?? [];
|
|
39
65
|
this.request = request;
|
|
40
66
|
this.response = response;
|
|
41
67
|
this.status = response?.status ?? 0;
|
|
@@ -46,7 +72,8 @@ var HttpError = class extends Error {
|
|
|
46
72
|
var isAbortError = (error) => error instanceof DOMException && error.name === "AbortError";
|
|
47
73
|
/** 原生 fetch 的薄封装(见文件开头) */
|
|
48
74
|
function createHttpClient(options = {}) {
|
|
49
|
-
const { baseURL = "", timeout: defaultTimeout = 3e4, onRequest, onResponse, wire = {} } = options;
|
|
75
|
+
const { baseURL = "", timeout: defaultTimeout = 3e4, onRequest, onResponse, wire = {}, envelope: unwrap = true } = options;
|
|
76
|
+
const revive = (value) => wire === false ? value : fromWire(value, wire);
|
|
50
77
|
async function request(spec) {
|
|
51
78
|
const { method = "GET", body, signal, timeout = defaultTimeout } = spec;
|
|
52
79
|
const headers = new Headers({
|
|
@@ -68,23 +95,68 @@ function createHttpClient(options = {}) {
|
|
|
68
95
|
response = await (options.fetch ?? fetch)(prepared);
|
|
69
96
|
} catch (cause) {
|
|
70
97
|
if (signal?.aborted) throw cause;
|
|
71
|
-
|
|
98
|
+
const timedOut = cause instanceof DOMException && cause.name === "TimeoutError";
|
|
99
|
+
throw new HttpError(timedOut ? `请求超时(${timeout / 1e3} 秒):${method} ${spec.url}` : `网络错误:${method} ${spec.url}`, {
|
|
100
|
+
code: timedOut ? "http.timeout" : "http.network",
|
|
101
|
+
params: timedOut ? { seconds: timeout / 1e3 } : void 0,
|
|
72
102
|
request: spec,
|
|
73
103
|
cause
|
|
74
104
|
});
|
|
75
105
|
}
|
|
76
|
-
const
|
|
77
|
-
const data = wire === false ? raw : fromWire(raw, wire);
|
|
78
|
-
if (!response.ok) throw new HttpError(`请求失败(${`${response.status}${response.statusText ? ` ${response.statusText}` : ""}`}):${method} ${spec.url}`, {
|
|
106
|
+
const context = {
|
|
79
107
|
request: spec,
|
|
80
|
-
response
|
|
108
|
+
response
|
|
109
|
+
};
|
|
110
|
+
const { body: raw, malformed } = await readBody(response, spec.responseType);
|
|
111
|
+
let data;
|
|
112
|
+
try {
|
|
113
|
+
data = malformed ? raw : revive(raw);
|
|
114
|
+
} catch (cause) {
|
|
115
|
+
throw new HttpError(`响应读不懂:${method} ${spec.url}`, {
|
|
116
|
+
...context,
|
|
117
|
+
code: "http.parse",
|
|
118
|
+
data: raw,
|
|
119
|
+
cause
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const envelope = unwrap && isEnvelope(data) ? data : void 0;
|
|
123
|
+
if (envelope && envelope.code !== "ok") throw new HttpError(envelope.message || envelope.code, {
|
|
124
|
+
...context,
|
|
125
|
+
code: envelope.code,
|
|
126
|
+
params: envelope.params,
|
|
127
|
+
errors: envelope.errors,
|
|
81
128
|
data
|
|
82
129
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
130
|
+
if (!response.ok) throw new HttpError(`请求失败(${`${response.status}${response.statusText ? ` ${response.statusText}` : ""}`}):${method} ${spec.url}`, {
|
|
131
|
+
...context,
|
|
132
|
+
code: "http.status",
|
|
133
|
+
params: { status: response.status },
|
|
134
|
+
data
|
|
135
|
+
});
|
|
136
|
+
if (malformed) throw new HttpError(`响应不是 JSON:${method} ${spec.url}`, {
|
|
137
|
+
...context,
|
|
138
|
+
code: "http.parse",
|
|
139
|
+
data
|
|
140
|
+
});
|
|
141
|
+
const result = envelope ? revive(envelope.data) : data;
|
|
142
|
+
return onResponse ? onResponse(result, context) : result;
|
|
87
143
|
}
|
|
144
|
+
return scopeHttp({ request });
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 在任意请求客户端上加常用方法的简写,相对地址前面拼上 prefix(斜杠不重复;完整地址原样)。
|
|
148
|
+
* 一个模块的接口都在同一个前缀下时用它,不必每次写全地址;useHttp() 拿到的客户端也能这样包:
|
|
149
|
+
*
|
|
150
|
+
* const api = scopeHttp(useHttp(), '/flow');
|
|
151
|
+
* api.get('/queries/todo', { query: { page: 1 }, signal }); // GET /flow/queries/todo?page=1
|
|
152
|
+
* api.post('/commands/claim', { workItemId });
|
|
153
|
+
*/
|
|
154
|
+
function scopeHttp(client, prefix = "") {
|
|
155
|
+
const at = (url) => !prefix || isAbsolute(url) ? url : joinPath(prefix, url);
|
|
156
|
+
const request = (spec) => client.request({
|
|
157
|
+
...spec,
|
|
158
|
+
url: at(spec.url)
|
|
159
|
+
});
|
|
88
160
|
const withBody = (method) => (url, body, rest) => request({
|
|
89
161
|
...rest,
|
|
90
162
|
method,
|
|
@@ -108,6 +180,10 @@ function createHttpClient(options = {}) {
|
|
|
108
180
|
patch: withBody("PATCH")
|
|
109
181
|
};
|
|
110
182
|
}
|
|
183
|
+
/** 完整地址(https://…、//host/…):不拼 baseURL、前缀 */
|
|
184
|
+
var isAbsolute = (url) => /^[a-z][a-z\d+.-]*:/i.test(url) || url.startsWith("//");
|
|
185
|
+
/** 两段路径接起来,中间只留一个斜杠 */
|
|
186
|
+
var joinPath = (base, path) => `${base.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
111
187
|
var fallback;
|
|
112
188
|
/**
|
|
113
189
|
* 当前配置的请求客户端(在 setup 里调用):createUi / Provider 的 http;都没给时是一个默认的 createHttpClient()
|
|
@@ -117,7 +193,7 @@ function useHttp() {
|
|
|
117
193
|
}
|
|
118
194
|
/** 地址 + query:相对地址拼在 baseURL 后面(斜杠不重复),null、undefined 的参数跳过 */
|
|
119
195
|
function buildUrl(baseURL, url, query) {
|
|
120
|
-
let full =
|
|
196
|
+
let full = isAbsolute(url) || !baseURL ? url : joinPath(baseURL, url);
|
|
121
197
|
const params = new URLSearchParams();
|
|
122
198
|
for (const [key, value] of Object.entries(query ?? {})) {
|
|
123
199
|
if (value == null) continue;
|
|
@@ -128,27 +204,44 @@ function buildUrl(baseURL, url, query) {
|
|
|
128
204
|
if (search) full += `${full.includes("?") ? "&" : "?"}${search}`;
|
|
129
205
|
return full;
|
|
130
206
|
}
|
|
207
|
+
var ENVELOPE_KEYS = /* @__PURE__ */ new Set([
|
|
208
|
+
"code",
|
|
209
|
+
"message",
|
|
210
|
+
"params",
|
|
211
|
+
"errors",
|
|
212
|
+
"data"
|
|
213
|
+
]);
|
|
214
|
+
/** 是不是 0008 的响应体:code 是文字、没有别的键(不把恰好有 code 字段的业务对象当成包装) */
|
|
215
|
+
function isEnvelope(value) {
|
|
216
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
217
|
+
const record = value;
|
|
218
|
+
return typeof record.code === "string" && Object.keys(record).every((key) => ENVELOPE_KEYS.has(key));
|
|
219
|
+
}
|
|
131
220
|
/** 普通对象与数组按 JSON 发送 */
|
|
132
221
|
function isJsonBody(body) {
|
|
133
222
|
if (body == null || typeof body !== "object") return false;
|
|
134
223
|
const prototype = Object.getPrototypeOf(body);
|
|
135
224
|
return Array.isArray(body) || prototype === Object.prototype || prototype === null;
|
|
136
225
|
}
|
|
226
|
+
/** 读响应体;声明是 JSON 却解析不了(网关的 HTML 错误页)时交出原文并标上 malformed */
|
|
137
227
|
async function readBody(response, type) {
|
|
138
|
-
if (response.status === 204 || response.status === 205 || response.headers.get("content-length") === "0") return;
|
|
228
|
+
if (response.status === 204 || response.status === 205 || response.headers.get("content-length") === "0") return { body: void 0 };
|
|
139
229
|
const contentType = response.headers.get("content-type") ?? "";
|
|
140
230
|
const kind = type ?? (/[/+]json\b/.test(contentType) ? "json" : contentType.startsWith("text/") ? "text" : "blob");
|
|
141
|
-
if (kind === "blob") return response.blob();
|
|
231
|
+
if (kind === "blob") return { body: await response.blob() };
|
|
142
232
|
const text = await response.text();
|
|
143
|
-
if (kind === "text") return text;
|
|
144
|
-
if (text === "") return void 0;
|
|
233
|
+
if (kind === "text") return { body: text };
|
|
234
|
+
if (text === "") return { body: void 0 };
|
|
145
235
|
try {
|
|
146
|
-
return JSON.parse(text);
|
|
236
|
+
return { body: JSON.parse(text) };
|
|
147
237
|
} catch {
|
|
148
|
-
return
|
|
238
|
+
return {
|
|
239
|
+
body: text,
|
|
240
|
+
malformed: true
|
|
241
|
+
};
|
|
149
242
|
}
|
|
150
243
|
}
|
|
151
244
|
//#endregion
|
|
152
|
-
export { HttpError, createHttpClient, isAbortError, useHttp };
|
|
245
|
+
export { HTTP_ERROR_CODES, HttpError, OK_CODE, createHttpClient, isAbortError, scopeHttp, useHttp };
|
|
153
246
|
|
|
154
247
|
//# sourceMappingURL=http.js.map
|