@aspire-ui/element-component-pro 1.0.7 → 1.0.8

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/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-9514f173]{margin-right:2px}.ecp-pro-form-item__help-icon[data-v-9514f173]{margin-left:4px;color:#909399;cursor:help;font-size:14px}.ecp-pro-form-item__help-icon[data-v-9514f173]:hover{color:#409eff}.ecp-pro-form-item__help-item[data-v-9514f173]{margin-bottom:4px}.ecp-pro-form-item__help-item[data-v-9514f173]: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}}
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}}
@@ -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,16 @@
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
+ /** 千分位格式化展示(整数部分),小数部分固定 m 位 */
14
+ export declare function formatWithThousands(value: number, decimalPlaces: number): string;
15
+ /** 编辑态:无千分位,去掉多余尾部 0(在 m 位精度内) */
16
+ export declare function numberToEditString(value: number, decimalPlaces: number): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspire-ui/element-component-pro",
3
- "version": "1.0.07",
3
+ "version": "1.0.08",
4
4
  "description": "Element UI 二次封装组件库,基于 Vue 2.7 + TypeScript + setup 语法糖,实现 VbenAdmin 风格的 Pro 组件",
5
5
  "type": "module",
6
6
  "main": "./dist/element-component-pro.umd.js",
@@ -0,0 +1,122 @@
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
+ } from '../utils/formattedNumber'
23
+
24
+ const props = withDefaults(
25
+ defineProps<{
26
+ value?: unknown
27
+ placeholder?: string
28
+ disabled?: boolean
29
+ /** 整数部分允许的最大位数,默认 5 */
30
+ integerDigits?: number
31
+ /** 小数保留位数,默认 6 */
32
+ decimalPlaces?: number
33
+ /** 进位方式,默认四舍五入 */
34
+ rounding?: FormattedNumberRounding
35
+ }>(),
36
+ {
37
+ integerDigits: 5,
38
+ decimalPlaces: 6,
39
+ rounding: 'round',
40
+ }
41
+ )
42
+
43
+ const emit = defineEmits<{ (e: 'input', value: unknown): void }>()
44
+
45
+ const focused = ref(false)
46
+ const displayText = ref('')
47
+
48
+ const intN = () => Math.max(0, Math.floor(props.integerDigits ?? 5))
49
+ const decM = () => Math.max(0, Math.floor(props.decimalPlaces ?? 6))
50
+
51
+ function parseExternalToNumber(v: unknown): number | null {
52
+ if (v === null || v === undefined || v === '') return null
53
+ if (typeof v === 'number') {
54
+ return Number.isFinite(v) ? v : null
55
+ }
56
+ const s = stripNumberGrouping(String(v))
57
+ if (s === '' || s === '-') return null
58
+ const num = Number(s)
59
+ return Number.isFinite(num) ? num : null
60
+ }
61
+
62
+ function syncDisplayFromValue() {
63
+ if (focused.value) return
64
+ const num = parseExternalToNumber(props.value)
65
+ if (num === null) {
66
+ displayText.value = ''
67
+ return
68
+ }
69
+ displayText.value = formatWithThousands(
70
+ normalizeNumericValue(num, intN(), decM(), props.rounding),
71
+ decM()
72
+ )
73
+ }
74
+
75
+ watch(
76
+ () => [props.value, props.integerDigits, props.decimalPlaces, props.rounding],
77
+ () => syncDisplayFromValue(),
78
+ { immediate: true, deep: true }
79
+ )
80
+
81
+ function onInput(val: string) {
82
+ const clean = sanitizeNumericInput(val)
83
+ displayText.value = clean
84
+ if (clean === '' || clean === '-') return
85
+ const num = Number(clean)
86
+ if (!Number.isFinite(num)) return
87
+ emit('input', num)
88
+ }
89
+
90
+ function onFocus() {
91
+ focused.value = true
92
+ const raw = stripNumberGrouping(displayText.value)
93
+ const num = parseExternalToNumber(raw === '' ? props.value : raw)
94
+ if (num === null) {
95
+ displayText.value = ''
96
+ return
97
+ }
98
+ displayText.value = numberToEditString(
99
+ normalizeNumericValue(num, intN(), decM(), props.rounding),
100
+ decM()
101
+ )
102
+ }
103
+
104
+ function onBlur() {
105
+ focused.value = false
106
+ const raw = stripNumberGrouping(displayText.value)
107
+ if (raw === '' || raw === '-') {
108
+ displayText.value = ''
109
+ emit('input', undefined)
110
+ return
111
+ }
112
+ const parsed = Number(sanitizeNumericInput(raw))
113
+ if (!Number.isFinite(parsed)) {
114
+ displayText.value = ''
115
+ emit('input', undefined)
116
+ return
117
+ }
118
+ const final = normalizeNumericValue(parsed, intN(), decM(), props.rounding)
119
+ displayText.value = formatWithThousands(final, decM())
120
+ emit('input', final)
121
+ }
122
+ </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
 
@@ -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
  }
@@ -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,118 @@
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
+ /** 千分位格式化展示(整数部分),小数部分固定 m 位 */
100
+ export function formatWithThousands(value: number, decimalPlaces: number): string {
101
+ if (Number.isNaN(value) || !Number.isFinite(value)) return ''
102
+ const fixed = value.toFixed(Math.max(0, decimalPlaces))
103
+ const neg = fixed.startsWith('-')
104
+ const body = neg ? fixed.slice(1) : fixed
105
+ const [intPart, decPart] = body.split('.')
106
+ const withSep = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
107
+ if (decimalPlaces <= 0 || decPart === undefined) return (neg ? '-' : '') + withSep
108
+ return (neg ? '-' : '') + withSep + '.' + decPart
109
+ }
110
+
111
+ /** 编辑态:无千分位,去掉多余尾部 0(在 m 位精度内) */
112
+ export function numberToEditString(value: number, decimalPlaces: number): string {
113
+ if (Number.isNaN(value) || !Number.isFinite(value)) return ''
114
+ let s = value.toFixed(Math.max(0, decimalPlaces))
115
+ s = s.replace(/(\.\d*?)0+$/, '$1')
116
+ s = s.replace(/\.$/, '')
117
+ return s
118
+ }