@aspire-ui/element-component-pro 1.0.7 → 1.0.9
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/README.md +1 -0
- package/dist/ProForm/FormattedNumberInput.vue.d.ts +55 -0
- package/dist/ProForm/index.d.ts +2 -1
- package/dist/element-component-pro.es.js +836 -654
- package/dist/element-component-pro.es.js.map +1 -1
- package/dist/element-component-pro.umd.js +2 -2
- package/dist/element-component-pro.umd.js.map +1 -1
- package/dist/index.d.ts +78 -26
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/utils/formattedNumber.d.ts +20 -0
- package/package.json +1 -1
- package/src/ProForm/FormattedNumberInput.vue +128 -0
- package/src/ProForm/ProFormItem.vue +11 -0
- package/src/ProForm/index.ts +2 -1
- package/src/index.ts +5 -2
- package/src/types/index.ts +2 -1
- package/src/utils/formattedNumber.ts +126 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { VueConstructor } from 'vue';
|
|
2
2
|
import { default as ProTable, TableAction } from './ProTable';
|
|
3
|
-
import { default as ProForm, ProFormItem, FormActions } from './ProForm';
|
|
3
|
+
import { default as ProForm, ProFormItem, FormActions, FormattedNumberInput } from './ProForm';
|
|
4
4
|
import { default as ProDescriptions } from './ProDescriptions';
|
|
5
5
|
import { useForm } from './ProForm/useForm';
|
|
6
6
|
import { useDescription } from './ProDescriptions/useDescription';
|
|
7
7
|
import { useProTable } from './ProTable/useProTable';
|
|
8
8
|
import { useComponentSetting } from './useComponentSetting';
|
|
9
9
|
|
|
10
|
-
export { ProForm, ProFormItem, FormActions, useForm };
|
|
10
|
+
export { ProForm, ProFormItem, FormActions, FormattedNumberInput, useForm };
|
|
11
11
|
export { ProTable, useProTable, TableAction };
|
|
12
12
|
export { ProDescriptions, useDescription };
|
|
13
13
|
export { useComponentSetting };
|
|
@@ -16,6 +16,7 @@ export type { UseProTableReturn, UseProTablePropsReactive } from './ProTable/use
|
|
|
16
16
|
export type { UseDescriptionReturn, UseDescriptionPropsReactive } from './ProDescriptions/useDescription';
|
|
17
17
|
export * from './ProTable/types';
|
|
18
18
|
export * from './types';
|
|
19
|
+
export * from './utils/formattedNumber';
|
|
19
20
|
export declare function install(Vue: VueConstructor): void;
|
|
20
21
|
declare const _default: {
|
|
21
22
|
install: typeof install;
|
|
@@ -52,16 +53,16 @@ declare const _default: {
|
|
|
52
53
|
type: import('vue').PropType<boolean>;
|
|
53
54
|
default: boolean;
|
|
54
55
|
};
|
|
56
|
+
immediate: {
|
|
57
|
+
type: import('vue').PropType<boolean>;
|
|
58
|
+
default: boolean;
|
|
59
|
+
};
|
|
55
60
|
treeProps: {
|
|
56
61
|
type: import('vue').PropType<{
|
|
57
62
|
hasChildren?: string;
|
|
58
63
|
children?: string;
|
|
59
64
|
}>;
|
|
60
65
|
};
|
|
61
|
-
immediate: {
|
|
62
|
-
type: import('vue').PropType<boolean>;
|
|
63
|
-
default: boolean;
|
|
64
|
-
};
|
|
65
66
|
rowSelection: {
|
|
66
67
|
type: import('vue').PropType<{
|
|
67
68
|
type?: "checkbox" | "radio";
|
|
@@ -222,16 +223,16 @@ declare const _default: {
|
|
|
222
223
|
type: import('vue').PropType<boolean>;
|
|
223
224
|
default: boolean;
|
|
224
225
|
};
|
|
226
|
+
immediate: {
|
|
227
|
+
type: import('vue').PropType<boolean>;
|
|
228
|
+
default: boolean;
|
|
229
|
+
};
|
|
225
230
|
treeProps: {
|
|
226
231
|
type: import('vue').PropType<{
|
|
227
232
|
hasChildren?: string;
|
|
228
233
|
children?: string;
|
|
229
234
|
}>;
|
|
230
235
|
};
|
|
231
|
-
immediate: {
|
|
232
|
-
type: import('vue').PropType<boolean>;
|
|
233
|
-
default: boolean;
|
|
234
|
-
};
|
|
235
236
|
rowSelection: {
|
|
236
237
|
type: import('vue').PropType<{
|
|
237
238
|
type?: "checkbox" | "radio";
|
|
@@ -433,16 +434,16 @@ declare const _default: {
|
|
|
433
434
|
type: import('vue').PropType<boolean>;
|
|
434
435
|
default: boolean;
|
|
435
436
|
};
|
|
437
|
+
immediate: {
|
|
438
|
+
type: import('vue').PropType<boolean>;
|
|
439
|
+
default: boolean;
|
|
440
|
+
};
|
|
436
441
|
treeProps: {
|
|
437
442
|
type: import('vue').PropType<{
|
|
438
443
|
hasChildren?: string;
|
|
439
444
|
children?: string;
|
|
440
445
|
}>;
|
|
441
446
|
};
|
|
442
|
-
immediate: {
|
|
443
|
-
type: import('vue').PropType<boolean>;
|
|
444
|
-
default: boolean;
|
|
445
|
-
};
|
|
446
447
|
rowSelection: {
|
|
447
448
|
type: import('vue').PropType<{
|
|
448
449
|
type?: "checkbox" | "radio";
|
|
@@ -637,16 +638,16 @@ declare const _default: {
|
|
|
637
638
|
type: import('vue').PropType<boolean>;
|
|
638
639
|
default: boolean;
|
|
639
640
|
};
|
|
641
|
+
immediate: {
|
|
642
|
+
type: import('vue').PropType<boolean>;
|
|
643
|
+
default: boolean;
|
|
644
|
+
};
|
|
640
645
|
treeProps: {
|
|
641
646
|
type: import('vue').PropType<{
|
|
642
647
|
hasChildren?: string;
|
|
643
648
|
children?: string;
|
|
644
649
|
}>;
|
|
645
650
|
};
|
|
646
|
-
immediate: {
|
|
647
|
-
type: import('vue').PropType<boolean>;
|
|
648
|
-
default: boolean;
|
|
649
|
-
};
|
|
650
651
|
rowSelection: {
|
|
651
652
|
type: import('vue').PropType<{
|
|
652
653
|
type?: "checkbox" | "radio";
|
|
@@ -807,16 +808,16 @@ declare const _default: {
|
|
|
807
808
|
type: import('vue').PropType<boolean>;
|
|
808
809
|
default: boolean;
|
|
809
810
|
};
|
|
811
|
+
immediate: {
|
|
812
|
+
type: import('vue').PropType<boolean>;
|
|
813
|
+
default: boolean;
|
|
814
|
+
};
|
|
810
815
|
treeProps: {
|
|
811
816
|
type: import('vue').PropType<{
|
|
812
817
|
hasChildren?: string;
|
|
813
818
|
children?: string;
|
|
814
819
|
}>;
|
|
815
820
|
};
|
|
816
|
-
immediate: {
|
|
817
|
-
type: import('vue').PropType<boolean>;
|
|
818
|
-
default: boolean;
|
|
819
|
-
};
|
|
820
821
|
rowSelection: {
|
|
821
822
|
type: import('vue').PropType<{
|
|
822
823
|
type?: "checkbox" | "radio";
|
|
@@ -1053,16 +1054,16 @@ declare const _default: {
|
|
|
1053
1054
|
type: import('vue').PropType<boolean>;
|
|
1054
1055
|
default: boolean;
|
|
1055
1056
|
};
|
|
1057
|
+
immediate: {
|
|
1058
|
+
type: import('vue').PropType<boolean>;
|
|
1059
|
+
default: boolean;
|
|
1060
|
+
};
|
|
1056
1061
|
treeProps: {
|
|
1057
1062
|
type: import('vue').PropType<{
|
|
1058
1063
|
hasChildren?: string;
|
|
1059
1064
|
children?: string;
|
|
1060
1065
|
}>;
|
|
1061
1066
|
};
|
|
1062
|
-
immediate: {
|
|
1063
|
-
type: import('vue').PropType<boolean>;
|
|
1064
|
-
default: boolean;
|
|
1065
|
-
};
|
|
1066
1067
|
rowSelection: {
|
|
1067
1068
|
type: import('vue').PropType<{
|
|
1068
1069
|
type?: "checkbox" | "radio";
|
|
@@ -2571,5 +2572,56 @@ declare const _default: {
|
|
|
2571
2572
|
dropDownActions: import('./ProTable').TableActionItem[];
|
|
2572
2573
|
stopButtonPropagation: boolean;
|
|
2573
2574
|
}>;
|
|
2575
|
+
FormattedNumberInput: import('vue').DefineComponent<{
|
|
2576
|
+
disabled: {
|
|
2577
|
+
type: import('vue').PropType<boolean>;
|
|
2578
|
+
};
|
|
2579
|
+
value: {
|
|
2580
|
+
type: import('vue').PropType<unknown>;
|
|
2581
|
+
};
|
|
2582
|
+
placeholder: {
|
|
2583
|
+
type: import('vue').PropType<string>;
|
|
2584
|
+
};
|
|
2585
|
+
integerDigits: {
|
|
2586
|
+
type: import('vue').PropType<number>;
|
|
2587
|
+
default: number;
|
|
2588
|
+
};
|
|
2589
|
+
decimalPlaces: {
|
|
2590
|
+
type: import('vue').PropType<number>;
|
|
2591
|
+
default: number;
|
|
2592
|
+
};
|
|
2593
|
+
rounding: {
|
|
2594
|
+
type: import('vue').PropType<import('.').FormattedNumberRounding>;
|
|
2595
|
+
default: string;
|
|
2596
|
+
};
|
|
2597
|
+
}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
2598
|
+
input: (value: unknown) => void;
|
|
2599
|
+
}, string, Readonly<import('vue').ExtractPropTypes<{
|
|
2600
|
+
disabled: {
|
|
2601
|
+
type: import('vue').PropType<boolean>;
|
|
2602
|
+
};
|
|
2603
|
+
value: {
|
|
2604
|
+
type: import('vue').PropType<unknown>;
|
|
2605
|
+
};
|
|
2606
|
+
placeholder: {
|
|
2607
|
+
type: import('vue').PropType<string>;
|
|
2608
|
+
};
|
|
2609
|
+
integerDigits: {
|
|
2610
|
+
type: import('vue').PropType<number>;
|
|
2611
|
+
default: number;
|
|
2612
|
+
};
|
|
2613
|
+
decimalPlaces: {
|
|
2614
|
+
type: import('vue').PropType<number>;
|
|
2615
|
+
default: number;
|
|
2616
|
+
};
|
|
2617
|
+
rounding: {
|
|
2618
|
+
type: import('vue').PropType<import('.').FormattedNumberRounding>;
|
|
2619
|
+
default: string;
|
|
2620
|
+
};
|
|
2621
|
+
}>>, {
|
|
2622
|
+
integerDigits: number;
|
|
2623
|
+
decimalPlaces: number;
|
|
2624
|
+
rounding: import('.').FormattedNumberRounding;
|
|
2625
|
+
}>;
|
|
2574
2626
|
};
|
|
2575
2627
|
export default _default;
|
package/dist/style.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.ecp-pro-table[data-v-c5638c20]{padding:16px;background:#fff;width:100%;box-sizing:border-box}.ecp-pro-table[data-v-c5638c20] .el-table{width:100%!important}.ecp-pro-table__header[data-v-c5638c20]{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.ecp-pro-table__title-wrapper[data-v-c5638c20]{display:flex;align-items:center;gap:4px}.ecp-pro-table__title[data-v-c5638c20]{font-size:16px;font-weight:600}.ecp-pro-table__help[data-v-c5638c20]{color:#909399;cursor:help}.ecp-pro-table__toolbar[data-v-c5638c20]{display:flex;align-items:center;gap:8px}.ecp-pro-table__body[data-v-c5638c20]{width:100%}.ecp-pro-table__pagination[data-v-c5638c20]{margin-top:16px;display:flex;justify-content:flex-end}.ecp-pro-table__col-help[data-v-c5638c20]{margin-left:4px;color:#909399;cursor:help}.ecp-table-action[data-v-45a58e7c],.ecp-table-action__item[data-v-45a58e7c]{display:inline-flex;align-items:center;gap:4px}.ecp-table-action__icon[data-v-45a58e7c]{margin-right:4px}.ecp-table-action__more[data-v-45a58e7c]{display:inline-flex;align-items:center}.ecp-table-action__dropdown-item[data-v-45a58e7c]{display:inline-flex;align-items:center;gap:4px}.ecp-tree-select[data-v-f30bba11]{position:relative;width:100%}.ecp-tree-select__filter-inner[data-v-f30bba11]{margin-bottom:8px}.ecp-tree-select__dropdown[data-v-f30bba11]{position:absolute;top:100%;left:0;right:0;max-height:280px;overflow:auto;background:#fff;border:1px solid #dcdfe6;border-radius:4px;margin-top:4px;z-index:1000;padding:8px}.ecp-tree-select__loading[data-v-f30bba11]{padding:24px;text-align:center;color:#909399;font-size:14px}.ecp-pro-form-item__colon[data-v-
|
|
1
|
+
.ecp-pro-table[data-v-c5638c20]{padding:16px;background:#fff;width:100%;box-sizing:border-box}.ecp-pro-table[data-v-c5638c20] .el-table{width:100%!important}.ecp-pro-table__header[data-v-c5638c20]{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.ecp-pro-table__title-wrapper[data-v-c5638c20]{display:flex;align-items:center;gap:4px}.ecp-pro-table__title[data-v-c5638c20]{font-size:16px;font-weight:600}.ecp-pro-table__help[data-v-c5638c20]{color:#909399;cursor:help}.ecp-pro-table__toolbar[data-v-c5638c20]{display:flex;align-items:center;gap:8px}.ecp-pro-table__body[data-v-c5638c20]{width:100%}.ecp-pro-table__pagination[data-v-c5638c20]{margin-top:16px;display:flex;justify-content:flex-end}.ecp-pro-table__col-help[data-v-c5638c20]{margin-left:4px;color:#909399;cursor:help}.ecp-table-action[data-v-45a58e7c],.ecp-table-action__item[data-v-45a58e7c]{display:inline-flex;align-items:center;gap:4px}.ecp-table-action__icon[data-v-45a58e7c]{margin-right:4px}.ecp-table-action__more[data-v-45a58e7c]{display:inline-flex;align-items:center}.ecp-table-action__dropdown-item[data-v-45a58e7c]{display:inline-flex;align-items:center;gap:4px}.ecp-tree-select[data-v-f30bba11]{position:relative;width:100%}.ecp-tree-select__filter-inner[data-v-f30bba11]{margin-bottom:8px}.ecp-tree-select__dropdown[data-v-f30bba11]{position:absolute;top:100%;left:0;right:0;max-height:280px;overflow:auto;background:#fff;border:1px solid #dcdfe6;border-radius:4px;margin-top:4px;z-index:1000;padding:8px}.ecp-tree-select__loading[data-v-f30bba11]{padding:24px;text-align:center;color:#909399;font-size:14px}.ecp-pro-form-item__colon[data-v-d3466c67]{margin-right:2px}.ecp-pro-form-item__help-icon[data-v-d3466c67]{margin-left:4px;color:#909399;cursor:help;font-size:14px}.ecp-pro-form-item__help-icon[data-v-d3466c67]:hover{color:#409eff}.ecp-pro-form-item__help-item[data-v-d3466c67]{margin-bottom:4px}.ecp-pro-form-item__help-item[data-v-d3466c67]:last-child{margin-bottom:0}.ecp-form-actions[data-v-489c88d2]{text-align:right}.ecp-form-actions__advance[data-v-489c88d2]{margin-right:8px}.el-icon-d-arrow-left.up[data-v-489c88d2]{transform:rotate(90deg)}.el-icon-d-arrow-left.down[data-v-489c88d2]{transform:rotate(-90deg)}.ecp-pro-form[data-v-bf70afca]{padding:16px;position:relative}.ecp-pro-form__advance[data-v-bf70afca]{margin-bottom:16px}.ecp-pro-form_col[data-v-bf70afca]{position:relative;float:right}.el-icon-d-arrow-left.up[data-v-bf70afca]{transform:rotate(90deg)}.el-icon-d-arrow-left.down[data-v-bf70afca]{transform:rotate(-90deg)}.ecp-form-actions__advance[data-v-bf70afca]{position:absolute;bottom:0;left:50%;transform:translate(-50%,-50%)}.ecp-pro-descriptions[data-v-7d6cd376]{width:100%;box-sizing:border-box}.ecp-pro-descriptions__header[data-v-7d6cd376]{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;gap:12px}.ecp-pro-descriptions__title-wrap[data-v-7d6cd376]{display:flex;align-items:center;gap:6px}.ecp-pro-descriptions__title[data-v-7d6cd376]{font-size:16px;font-weight:600;color:#303133}.ecp-pro-descriptions__help[data-v-7d6cd376],.ecp-pro-descriptions__toggle[data-v-7d6cd376]{color:#909399}.ecp-pro-descriptions__toggle .el-icon-arrow-down[data-v-7d6cd376]{margin-left:4px;transition:transform .2s ease}.ecp-pro-descriptions__toggle .el-icon-arrow-down.is-expanded[data-v-7d6cd376]{transform:rotate(180deg)}.ecp-pro-descriptions__body[data-v-7d6cd376]{display:grid;border-top:1px solid #ebeef5;border-left:1px solid #ebeef5;overflow:hidden}.ecp-pro-descriptions__body.is-collapsed[data-v-7d6cd376]{overflow:hidden}.ecp-pro-descriptions__body[data-v-7d6cd376]:not(.is-bordered){border-top:0;border-left:0;gap:12px 16px}.ecp-pro-descriptions__item[data-v-7d6cd376]{display:flex;min-width:0;border-right:1px solid #ebeef5;border-bottom:1px solid #ebeef5}.ecp-pro-descriptions__body:not(.is-bordered) .ecp-pro-descriptions__item[data-v-7d6cd376]{border-right:0;border-bottom:0}.ecp-pro-descriptions__label[data-v-7d6cd376],.ecp-pro-descriptions__content[data-v-7d6cd376]{min-width:0;box-sizing:border-box;word-break:break-word}.ecp-pro-descriptions__label[data-v-7d6cd376]{flex:0 0 120px;padding:12px 16px;color:#606266;background:#fafafa}.ecp-pro-descriptions__content[data-v-7d6cd376]{flex:1;padding:12px 16px;color:#303133;background:#fff}.ecp-pro-descriptions__body:not(.is-bordered) .ecp-pro-descriptions__label[data-v-7d6cd376]{flex-basis:auto;padding:0;margin-right:8px;background:transparent;font-weight:500}.ecp-pro-descriptions__body:not(.is-bordered) .ecp-pro-descriptions__content[data-v-7d6cd376]{padding:0;background:transparent}.ecp-pro-descriptions__body.is-small .ecp-pro-descriptions__label[data-v-7d6cd376],.ecp-pro-descriptions__body.is-small .ecp-pro-descriptions__content[data-v-7d6cd376]{padding-top:8px;padding-bottom:8px;font-size:13px}@media (max-width: 767px){.ecp-pro-descriptions__item[data-v-7d6cd376]{flex-direction:column}.ecp-pro-descriptions__label[data-v-7d6cd376]{flex-basis:auto}}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -78,7 +78,7 @@ export interface ProFormProps {
|
|
|
78
78
|
components?: Record<string, unknown>;
|
|
79
79
|
}
|
|
80
80
|
/** ProForm 内置表单项组件类型 */
|
|
81
|
-
export type ProFormBuiltInComponent = 'input' | 'select' | 'api-select' | 'tree-select' | 'date-picker' | 'date-range' | 'input-number' | 'switch' | 'cascader' | 'checkbox' | 'radio';
|
|
81
|
+
export type ProFormBuiltInComponent = 'input' | 'select' | 'api-select' | 'tree-select' | 'date-picker' | 'date-range' | 'input-number' | 'formatted-number' | 'switch' | 'cascader' | 'checkbox' | 'radio';
|
|
82
82
|
/** 自定义组件:组件名(string)或 Vue 组件选项/构造函数(object | Function) */
|
|
83
83
|
export type ProFormCustomComponent = string | object | ((...args: unknown[]) => unknown);
|
|
84
84
|
/** ProForm 表单项配置 */
|
|
@@ -93,7 +93,7 @@ export interface ProFormSchema {
|
|
|
93
93
|
colon?: boolean;
|
|
94
94
|
/**
|
|
95
95
|
* 组件类型:
|
|
96
|
-
* - 内置:'input' | 'select' | 'date-picker' | 'date-range' | 'input-number' | 'switch' | 'cascader' | 'checkbox' | 'radio'
|
|
96
|
+
* - 内置:'input' | 'select' | 'date-picker' | 'date-range' | 'input-number' | 'formatted-number' | 'switch' | 'cascader' | 'checkbox' | 'radio'
|
|
97
97
|
* - 自定义组件名:任意字符串,对应全局或 ProForm 传入的 components 中注册的组件
|
|
98
98
|
* - 内联组件:直接传入 Vue 组件选项对象或构造函数
|
|
99
99
|
*/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** 进位方式:向下取整、向上取整、四舍五入 */
|
|
2
|
+
export type FormattedNumberRounding = 'floor' | 'ceil' | 'round';
|
|
3
|
+
/** 仅保留数字、至多一个小数点、可选前导负号 */
|
|
4
|
+
export declare function sanitizeNumericInput(raw: string): string;
|
|
5
|
+
export declare function stripNumberGrouping(s: string): string;
|
|
6
|
+
/** 按整数位数上限约束绝对值 */
|
|
7
|
+
export declare function clampByIntegerDigits(value: number, integerDigits: number, decimalPlaces: number): number;
|
|
8
|
+
export declare function roundToDecimals(value: number, decimalPlaces: number, rounding: FormattedNumberRounding): number;
|
|
9
|
+
/**
|
|
10
|
+
* 解析 → 按整数位夹紧 → 按小数位与进位方式舍入 → 再夹紧(防止舍入后越界)
|
|
11
|
+
*/
|
|
12
|
+
export declare function normalizeNumericValue(value: number, integerDigits: number, decimalPlaces: number, rounding: FormattedNumberRounding): number;
|
|
13
|
+
/**
|
|
14
|
+
* 固定小数位数字符串(无千分位),用于表单存值以保留配置的小数位数(number 无法保留尾随 0)。
|
|
15
|
+
*/
|
|
16
|
+
export declare function toFixedDecimalString(value: number, decimalPlaces: number): string;
|
|
17
|
+
/** 千分位格式化展示(整数部分),小数部分固定 m 位 */
|
|
18
|
+
export declare function formatWithThousands(value: number, decimalPlaces: number): string;
|
|
19
|
+
/** 编辑态:无千分位,去掉多余尾部 0(在 m 位精度内) */
|
|
20
|
+
export declare function numberToEditString(value: number, decimalPlaces: number): string;
|
package/package.json
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-input
|
|
3
|
+
:value="displayText"
|
|
4
|
+
:placeholder="placeholder"
|
|
5
|
+
:disabled="disabled"
|
|
6
|
+
v-bind="$attrs"
|
|
7
|
+
@input="onInput"
|
|
8
|
+
@focus="onFocus"
|
|
9
|
+
@blur="onBlur"
|
|
10
|
+
/>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import { ref, watch } from 'vue'
|
|
15
|
+
import type { FormattedNumberRounding } from '../utils/formattedNumber'
|
|
16
|
+
import {
|
|
17
|
+
formatWithThousands,
|
|
18
|
+
normalizeNumericValue,
|
|
19
|
+
numberToEditString,
|
|
20
|
+
sanitizeNumericInput,
|
|
21
|
+
stripNumberGrouping,
|
|
22
|
+
toFixedDecimalString,
|
|
23
|
+
} from '../utils/formattedNumber'
|
|
24
|
+
|
|
25
|
+
const props = withDefaults(
|
|
26
|
+
defineProps<{
|
|
27
|
+
value?: unknown
|
|
28
|
+
placeholder?: string
|
|
29
|
+
disabled?: boolean
|
|
30
|
+
/** 整数部分允许的最大位数,默认 5 */
|
|
31
|
+
integerDigits?: number
|
|
32
|
+
/** 小数保留位数,默认 6 */
|
|
33
|
+
decimalPlaces?: number
|
|
34
|
+
/** 进位方式,默认四舍五入 */
|
|
35
|
+
rounding?: FormattedNumberRounding
|
|
36
|
+
}>(),
|
|
37
|
+
{
|
|
38
|
+
integerDigits: 5,
|
|
39
|
+
decimalPlaces: 6,
|
|
40
|
+
rounding: 'round',
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const emit = defineEmits<{ (e: 'input', value: unknown): void }>()
|
|
45
|
+
|
|
46
|
+
const focused = ref(false)
|
|
47
|
+
const displayText = ref('')
|
|
48
|
+
|
|
49
|
+
const intN = () => Math.max(0, Math.floor(props.integerDigits ?? 5))
|
|
50
|
+
const decM = () => Math.max(0, Math.floor(props.decimalPlaces ?? 6))
|
|
51
|
+
|
|
52
|
+
function parseExternalToNumber(v: unknown): number | null {
|
|
53
|
+
if (v === null || v === undefined || v === '') return null
|
|
54
|
+
if (typeof v === 'number') {
|
|
55
|
+
return Number.isFinite(v) ? v : null
|
|
56
|
+
}
|
|
57
|
+
const s = stripNumberGrouping(String(v))
|
|
58
|
+
if (s === '' || s === '-') return null
|
|
59
|
+
const num = Number(s)
|
|
60
|
+
return Number.isFinite(num) ? num : null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function syncDisplayFromValue() {
|
|
64
|
+
if (focused.value) return
|
|
65
|
+
const num = parseExternalToNumber(props.value)
|
|
66
|
+
if (num === null) {
|
|
67
|
+
displayText.value = ''
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
displayText.value = formatWithThousands(
|
|
71
|
+
normalizeNumericValue(num, intN(), decM(), props.rounding),
|
|
72
|
+
decM()
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
watch(
|
|
77
|
+
() => [props.value, props.integerDigits, props.decimalPlaces, props.rounding],
|
|
78
|
+
() => syncDisplayFromValue(),
|
|
79
|
+
{ immediate: true, deep: true }
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
function emitStoredValue(normalized: number) {
|
|
83
|
+
emit('input', toFixedDecimalString(normalized, decM()))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function onInput(val: string) {
|
|
87
|
+
const clean = sanitizeNumericInput(val)
|
|
88
|
+
displayText.value = clean
|
|
89
|
+
if (clean === '' || clean === '-') return
|
|
90
|
+
const num = Number(clean)
|
|
91
|
+
if (!Number.isFinite(num)) return
|
|
92
|
+
const normalized = normalizeNumericValue(num, intN(), decM(), props.rounding)
|
|
93
|
+
emitStoredValue(normalized)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function onFocus() {
|
|
97
|
+
focused.value = true
|
|
98
|
+
const raw = stripNumberGrouping(displayText.value)
|
|
99
|
+
const num = parseExternalToNumber(raw === '' ? props.value : raw)
|
|
100
|
+
if (num === null) {
|
|
101
|
+
displayText.value = ''
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
displayText.value = numberToEditString(
|
|
105
|
+
normalizeNumericValue(num, intN(), decM(), props.rounding),
|
|
106
|
+
decM()
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function onBlur() {
|
|
111
|
+
focused.value = false
|
|
112
|
+
const raw = stripNumberGrouping(displayText.value)
|
|
113
|
+
if (raw === '' || raw === '-') {
|
|
114
|
+
displayText.value = ''
|
|
115
|
+
emit('input', undefined)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
const parsed = Number(sanitizeNumericInput(raw))
|
|
119
|
+
if (!Number.isFinite(parsed)) {
|
|
120
|
+
displayText.value = ''
|
|
121
|
+
emit('input', undefined)
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
const final = normalizeNumericValue(parsed, intN(), decM(), props.rounding)
|
|
125
|
+
displayText.value = formatWithThousands(final, decM())
|
|
126
|
+
emitStoredValue(final)
|
|
127
|
+
}
|
|
128
|
+
</script>
|
|
@@ -64,6 +64,15 @@
|
|
|
64
64
|
v-bind="effectiveComponentProps"
|
|
65
65
|
v-on="effectiveComponentListeners"
|
|
66
66
|
/>
|
|
67
|
+
<FormattedNumberInput
|
|
68
|
+
v-else-if="schema.component === 'formatted-number'"
|
|
69
|
+
:value="formModel[schema.field]"
|
|
70
|
+
:placeholder="schema.placeholder || (autoPlaceholder ? `请输入${schema.label}` : undefined)"
|
|
71
|
+
:disabled="effectiveDisabled"
|
|
72
|
+
v-bind="effectiveComponentProps"
|
|
73
|
+
v-on="effectiveComponentListeners"
|
|
74
|
+
@input="setFieldValue"
|
|
75
|
+
/>
|
|
67
76
|
<el-select
|
|
68
77
|
class="ecp-pro-form-item__select"
|
|
69
78
|
v-else-if="schema.component === 'select'"
|
|
@@ -171,12 +180,14 @@
|
|
|
171
180
|
<script setup lang="ts">
|
|
172
181
|
import { computed, useSlots, h } from 'vue'
|
|
173
182
|
import ApiSelect from './ApiSelect.vue'
|
|
183
|
+
import FormattedNumberInput from './FormattedNumberInput.vue'
|
|
174
184
|
import TreeSelect from './TreeSelect.vue'
|
|
175
185
|
import type { ProFormSchema, RenderCallbackParams } from '../types'
|
|
176
186
|
import { normalizeTooltipConfig } from '../utils/tooltip'
|
|
177
187
|
|
|
178
188
|
const BUILT_IN_COMPONENTS: Set<string> = new Set([
|
|
179
189
|
'input', 'select', 'api-select', 'tree-select', 'date-picker', 'date-range', 'input-number',
|
|
190
|
+
'formatted-number',
|
|
180
191
|
'switch', 'cascader', 'checkbox', 'radio',
|
|
181
192
|
])
|
|
182
193
|
|
package/src/ProForm/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ProForm from './ProForm.vue'
|
|
2
2
|
import ProFormItem from './ProFormItem.vue'
|
|
3
3
|
import FormActions from './FormActions.vue'
|
|
4
|
+
import FormattedNumberInput from './FormattedNumberInput.vue'
|
|
4
5
|
import { useForm } from './useForm'
|
|
5
|
-
export { ProForm, ProFormItem, FormActions, useForm }
|
|
6
|
+
export { ProForm, ProFormItem, FormActions, FormattedNumberInput, useForm }
|
|
6
7
|
export default ProForm
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { VueConstructor } from 'vue'
|
|
2
2
|
import ProTable, { TableAction } from './ProTable'
|
|
3
|
-
import ProForm, { ProFormItem, FormActions } from './ProForm'
|
|
3
|
+
import ProForm, { ProFormItem, FormActions, FormattedNumberInput } from './ProForm'
|
|
4
4
|
import ProDescriptions from './ProDescriptions'
|
|
5
5
|
import { useForm } from './ProForm/useForm'
|
|
6
6
|
import { useDescription } from './ProDescriptions/useDescription'
|
|
7
7
|
import { useProTable } from './ProTable/useProTable'
|
|
8
8
|
import { useComponentSetting } from './useComponentSetting'
|
|
9
9
|
|
|
10
|
-
export { ProForm, ProFormItem, FormActions, useForm }
|
|
10
|
+
export { ProForm, ProFormItem, FormActions, FormattedNumberInput, useForm }
|
|
11
11
|
export { ProTable, useProTable, TableAction }
|
|
12
12
|
export { ProDescriptions, useDescription }
|
|
13
13
|
export { useComponentSetting }
|
|
@@ -16,6 +16,7 @@ export type { UseProTableReturn, UseProTablePropsReactive } from './ProTable/use
|
|
|
16
16
|
export type { UseDescriptionReturn, UseDescriptionPropsReactive } from './ProDescriptions/useDescription'
|
|
17
17
|
export * from './ProTable/types'
|
|
18
18
|
export * from './types'
|
|
19
|
+
export * from './utils/formattedNumber'
|
|
19
20
|
|
|
20
21
|
const components = [
|
|
21
22
|
{ name: 'ProTable', component: ProTable },
|
|
@@ -23,6 +24,7 @@ const components = [
|
|
|
23
24
|
{ name: 'ProForm', component: ProForm },
|
|
24
25
|
{ name: 'ProFormItem', component: ProFormItem },
|
|
25
26
|
{ name: 'FormActions', component: FormActions },
|
|
27
|
+
{ name: 'FormattedNumberInput', component: FormattedNumberInput },
|
|
26
28
|
{ name: 'ProDescriptions', component: ProDescriptions },
|
|
27
29
|
]
|
|
28
30
|
|
|
@@ -38,4 +40,5 @@ export default {
|
|
|
38
40
|
ProForm,
|
|
39
41
|
ProDescriptions,
|
|
40
42
|
TableAction,
|
|
43
|
+
FormattedNumberInput,
|
|
41
44
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -93,6 +93,7 @@ export type ProFormBuiltInComponent =
|
|
|
93
93
|
| 'date-picker'
|
|
94
94
|
| 'date-range'
|
|
95
95
|
| 'input-number'
|
|
96
|
+
| 'formatted-number'
|
|
96
97
|
| 'switch'
|
|
97
98
|
| 'cascader'
|
|
98
99
|
| 'checkbox'
|
|
@@ -113,7 +114,7 @@ export interface ProFormSchema {
|
|
|
113
114
|
colon?: boolean
|
|
114
115
|
/**
|
|
115
116
|
* 组件类型:
|
|
116
|
-
* - 内置:'input' | 'select' | 'date-picker' | 'date-range' | 'input-number' | 'switch' | 'cascader' | 'checkbox' | 'radio'
|
|
117
|
+
* - 内置:'input' | 'select' | 'date-picker' | 'date-range' | 'input-number' | 'formatted-number' | 'switch' | 'cascader' | 'checkbox' | 'radio'
|
|
117
118
|
* - 自定义组件名:任意字符串,对应全局或 ProForm 传入的 components 中注册的组件
|
|
118
119
|
* - 内联组件:直接传入 Vue 组件选项对象或构造函数
|
|
119
120
|
*/
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/** 进位方式:向下取整、向上取整、四舍五入 */
|
|
2
|
+
export type FormattedNumberRounding = 'floor' | 'ceil' | 'round'
|
|
3
|
+
|
|
4
|
+
/** 仅保留数字、至多一个小数点、可选前导负号 */
|
|
5
|
+
export function sanitizeNumericInput(raw: string): string {
|
|
6
|
+
const t = raw.trim()
|
|
7
|
+
if (!t) return ''
|
|
8
|
+
let i = 0
|
|
9
|
+
let res = ''
|
|
10
|
+
if (t[0] === '-') {
|
|
11
|
+
res = '-'
|
|
12
|
+
i = 1
|
|
13
|
+
}
|
|
14
|
+
let dot = false
|
|
15
|
+
for (; i < t.length; i++) {
|
|
16
|
+
const c = t[i]
|
|
17
|
+
if (c >= '0' && c <= '9') {
|
|
18
|
+
res += c
|
|
19
|
+
continue
|
|
20
|
+
}
|
|
21
|
+
if (c === '.' && !dot) {
|
|
22
|
+
dot = true
|
|
23
|
+
if (res === '' || res === '-') res += '0'
|
|
24
|
+
res += '.'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return res
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function stripNumberGrouping(s: string): string {
|
|
31
|
+
return s.replace(/,/g, '').trim()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function maxAbsValue(integerDigits: number, decimalPlaces: number): number {
|
|
35
|
+
const maxInt = Math.pow(10, integerDigits) - 1
|
|
36
|
+
if (decimalPlaces <= 0) return maxInt
|
|
37
|
+
return maxInt + (1 - Math.pow(10, -decimalPlaces))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 按整数位数上限约束绝对值 */
|
|
41
|
+
export function clampByIntegerDigits(
|
|
42
|
+
value: number,
|
|
43
|
+
integerDigits: number,
|
|
44
|
+
decimalPlaces: number
|
|
45
|
+
): number {
|
|
46
|
+
const max = maxAbsValue(integerDigits, decimalPlaces)
|
|
47
|
+
const sign = value < 0 ? -1 : 1
|
|
48
|
+
const abs = Math.abs(value)
|
|
49
|
+
if (abs <= max) return value
|
|
50
|
+
return sign * max
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function roundToDecimals(
|
|
54
|
+
value: number,
|
|
55
|
+
decimalPlaces: number,
|
|
56
|
+
rounding: FormattedNumberRounding
|
|
57
|
+
): number {
|
|
58
|
+
if (decimalPlaces <= 0) {
|
|
59
|
+
switch (rounding) {
|
|
60
|
+
case 'floor':
|
|
61
|
+
return Math.floor(value)
|
|
62
|
+
case 'ceil':
|
|
63
|
+
return Math.ceil(value)
|
|
64
|
+
default:
|
|
65
|
+
return Math.round(value)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const factor = Math.pow(10, decimalPlaces)
|
|
69
|
+
const x = value * factor
|
|
70
|
+
let y: number
|
|
71
|
+
switch (rounding) {
|
|
72
|
+
case 'floor':
|
|
73
|
+
y = Math.floor(x)
|
|
74
|
+
break
|
|
75
|
+
case 'ceil':
|
|
76
|
+
y = Math.ceil(x)
|
|
77
|
+
break
|
|
78
|
+
default:
|
|
79
|
+
y = Math.round(x)
|
|
80
|
+
}
|
|
81
|
+
return y / factor
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 解析 → 按整数位夹紧 → 按小数位与进位方式舍入 → 再夹紧(防止舍入后越界)
|
|
86
|
+
*/
|
|
87
|
+
export function normalizeNumericValue(
|
|
88
|
+
value: number,
|
|
89
|
+
integerDigits: number,
|
|
90
|
+
decimalPlaces: number,
|
|
91
|
+
rounding: FormattedNumberRounding
|
|
92
|
+
): number {
|
|
93
|
+
let v = clampByIntegerDigits(value, integerDigits, decimalPlaces)
|
|
94
|
+
v = roundToDecimals(v, decimalPlaces, rounding)
|
|
95
|
+
v = clampByIntegerDigits(v, integerDigits, decimalPlaces)
|
|
96
|
+
return v
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 固定小数位数字符串(无千分位),用于表单存值以保留配置的小数位数(number 无法保留尾随 0)。
|
|
101
|
+
*/
|
|
102
|
+
export function toFixedDecimalString(value: number, decimalPlaces: number): string {
|
|
103
|
+
if (Number.isNaN(value) || !Number.isFinite(value)) return ''
|
|
104
|
+
return value.toFixed(Math.max(0, decimalPlaces))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 千分位格式化展示(整数部分),小数部分固定 m 位 */
|
|
108
|
+
export function formatWithThousands(value: number, decimalPlaces: number): string {
|
|
109
|
+
if (Number.isNaN(value) || !Number.isFinite(value)) return ''
|
|
110
|
+
const fixed = value.toFixed(Math.max(0, decimalPlaces))
|
|
111
|
+
const neg = fixed.startsWith('-')
|
|
112
|
+
const body = neg ? fixed.slice(1) : fixed
|
|
113
|
+
const [intPart, decPart] = body.split('.')
|
|
114
|
+
const withSep = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|
115
|
+
if (decimalPlaces <= 0 || decPart === undefined) return (neg ? '-' : '') + withSep
|
|
116
|
+
return (neg ? '-' : '') + withSep + '.' + decPart
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 编辑态:无千分位,去掉多余尾部 0(在 m 位精度内) */
|
|
120
|
+
export function numberToEditString(value: number, decimalPlaces: number): string {
|
|
121
|
+
if (Number.isNaN(value) || !Number.isFinite(value)) return ''
|
|
122
|
+
let s = value.toFixed(Math.max(0, decimalPlaces))
|
|
123
|
+
s = s.replace(/(\.\d*?)0+$/, '$1')
|
|
124
|
+
s = s.replace(/\.$/, '')
|
|
125
|
+
return s
|
|
126
|
+
}
|