@scorpioz/suna-components 0.0.2

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.
Files changed (38) hide show
  1. package/README.md +52 -0
  2. package/button/index.ts +3 -0
  3. package/button/src/Button.vue +42 -0
  4. package/button/src/props.ts +24 -0
  5. package/button/src/types.ts +26 -0
  6. package/confirm-button/index.ts +2 -0
  7. package/confirm-button/src/ConfirmButton.vue +52 -0
  8. package/confirm-button/src/props.ts +50 -0
  9. package/debounce-button/index.ts +2 -0
  10. package/debounce-button/src/DebounceButton.vue +65 -0
  11. package/debounce-button/src/props.ts +15 -0
  12. package/drawer/index.ts +3 -0
  13. package/drawer/src/Drawer.vue +120 -0
  14. package/drawer/src/props.ts +76 -0
  15. package/drawer/src/types.ts +8 -0
  16. package/drawer-form/index.ts +2 -0
  17. package/drawer-form/src/DrawerForm.vue +164 -0
  18. package/drawer-form/src/props.ts +74 -0
  19. package/form/index.ts +3 -0
  20. package/form/src/Form.vue +183 -0
  21. package/form/src/form-items/FCascader.vue +25 -0
  22. package/form/src/form-items/FCheckboxGroup.vue +33 -0
  23. package/form/src/form-items/FDatePicker.vue +24 -0
  24. package/form/src/form-items/FInput.vue +26 -0
  25. package/form/src/form-items/FInputNumber.vue +26 -0
  26. package/form/src/form-items/FRadioGroup.vue +33 -0
  27. package/form/src/form-items/FRate.vue +24 -0
  28. package/form/src/form-items/FSelect.vue +34 -0
  29. package/form/src/form-items/FSwitch.vue +24 -0
  30. package/form/src/form-items/FText.vue +16 -0
  31. package/form/src/form-items/FTextarea.vue +27 -0
  32. package/form/src/path.ts +39 -0
  33. package/form/src/props.ts +73 -0
  34. package/form/src/types.ts +168 -0
  35. package/form/src/useFormField.ts +137 -0
  36. package/form/src/useFormItems.ts +85 -0
  37. package/index.ts +82 -0
  38. package/package.json +50 -0
@@ -0,0 +1,16 @@
1
+ <script setup lang="ts">
2
+ import { useFormField } from "../useFormField";
3
+
4
+ const props = defineProps({
5
+ modelValue: { default: undefined },
6
+ item: { type: Object, default: () => ({}) },
7
+ form: { type: Object, default: () => ({}) },
8
+ });
9
+ const emit = defineEmits(["update:modelValue", "change"]);
10
+
11
+ const { formatTextVal } = useFormField(props, emit);
12
+ </script>
13
+
14
+ <template>
15
+ <div class="suna-form__text">{{ formatTextVal }}</div>
16
+ </template>
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import { ElInput } from "element-plus";
3
+ import { useFormField } from "../useFormField";
4
+
5
+ const props = defineProps({
6
+ modelValue: { default: undefined },
7
+ item: { type: Object, default: () => ({}) },
8
+ form: { type: Object, default: () => ({}) },
9
+ });
10
+ const emit = defineEmits(["update:modelValue", "change"]);
11
+
12
+ const { valueTmp, showText, formatTextVal, handleChange, blur, focus } =
13
+ useFormField(props, emit);
14
+ </script>
15
+
16
+ <template>
17
+ <ElInput
18
+ v-if="!showText"
19
+ type="textarea"
20
+ :model-value="valueTmp"
21
+ v-bind="item.attrs"
22
+ @update:model-value="handleChange"
23
+ @blur="blur"
24
+ @focus="focus"
25
+ />
26
+ <span v-else class="suna-form__text">{{ formatTextVal }}</span>
27
+ </template>
@@ -0,0 +1,39 @@
1
+ /**
2
+ * 按点号路径读取 / 写入对象嵌套字段。
3
+ * 仅覆盖配置驱动表单所需的最小能力,避免引入 lodash 依赖。
4
+ *
5
+ * @example
6
+ * getPath({ a: { b: 1 } }, "a.b") // => 1
7
+ * setPath(obj, "a.b", 2) // obj.a.b === 2
8
+ */
9
+
10
+ /** 读取 obj 上 path 对应的值;路径不存在返回 undefined。 */
11
+ export function getPath(obj: Record<string, any> | undefined, path: string): any {
12
+ if (!obj || !path) return undefined;
13
+ return path.split(".").reduce((acc, key) => {
14
+ return acc == null ? acc : acc[key];
15
+ }, obj as any);
16
+ }
17
+
18
+ /**
19
+ * 在 obj 上按 path 写入 value,途中不存在的键会自动创建为对象。
20
+ * 就地修改并返回 obj。
21
+ */
22
+ export function setPath(
23
+ obj: Record<string, any>,
24
+ path: string,
25
+ value: any
26
+ ): Record<string, any> {
27
+ if (!path) return obj;
28
+ const keys = path.split(".");
29
+ let cur = obj;
30
+ for (let i = 0; i < keys.length - 1; i++) {
31
+ const key = keys[i];
32
+ if (cur[key] == null || typeof cur[key] !== "object") {
33
+ cur[key] = {};
34
+ }
35
+ cur = cur[key];
36
+ }
37
+ cur[keys[keys.length - 1]] = value;
38
+ return obj;
39
+ }
@@ -0,0 +1,73 @@
1
+ import type { PropType } from "vue";
2
+ import type { ButtonType } from "../../button/src/types";
3
+ import type { FormItem, FormFieldType } from "./types";
4
+
5
+ /**
6
+ * SunaForm 的运行时 props 声明。
7
+ * 注:modelValue(表单数据对象)通过 defineModel 声明,不在此处。
8
+ */
9
+ export const formProps = {
10
+ /** 字段配置数组 */
11
+ formItems: {
12
+ type: Array as PropType<FormItem[]>,
13
+ required: true as const,
14
+ },
15
+
16
+ // ---- el-form 显式暴露属性 ----
17
+ /** 标签宽度 */
18
+ labelWidth: { type: [String, Number], default: "100px" },
19
+ /** 标签对齐方式 */
20
+ labelPosition: {
21
+ type: String as PropType<"left" | "right" | "top">,
22
+ default: "right",
23
+ },
24
+ /** 表单控件尺寸 */
25
+ size: {
26
+ type: String as PropType<"large" | "default" | "small">,
27
+ default: "default",
28
+ },
29
+ /** 栅格间隔(px) */
30
+ gutter: { type: Number, default: 12 },
31
+ /** 未指定 span 时的默认栅格占比(默认 12 = 两列) */
32
+ defaultSpan: { type: Number, default: 12 },
33
+
34
+ // ---- 渲染模式 ----
35
+ /** 渲染模式:normal 可编辑 / text 只读详情 */
36
+ showType: {
37
+ type: String as PropType<"normal" | "text">,
38
+ default: "normal",
39
+ },
40
+
41
+ // ---- 内置底部操作栏(语义同 Drawer footer,保持一致) ----
42
+ /** 是否显示操作栏 */
43
+ withActions: { type: Boolean, default: true },
44
+ /** 按钮对齐方式 */
45
+ actionAlign: {
46
+ type: String as PropType<"left" | "center" | "right">,
47
+ default: "right",
48
+ },
49
+ /** 是否显示提交按钮 */
50
+ showSubmit: { type: Boolean, default: true },
51
+ /** 是否显示重置按钮 */
52
+ showReset: { type: Boolean, default: true },
53
+ /** 是否显示取消按钮 */
54
+ showCancel: { type: Boolean, default: true },
55
+ /** 提交按钮文字 */
56
+ submitText: { type: String, default: "提交" },
57
+ /** 重置按钮文字 */
58
+ resetText: { type: String, default: "重置" },
59
+ /** 取消按钮文字 */
60
+ cancelText: { type: String, default: "取消" },
61
+ /** 提交按钮类型 */
62
+ submitButtonType: { type: String as PropType<ButtonType>, default: "primary" },
63
+ /** 重置按钮类型 */
64
+ resetButtonType: { type: String as PropType<ButtonType>, default: "default" },
65
+ /** 取消按钮类型 */
66
+ cancelButtonType: { type: String as PropType<ButtonType>, default: "default" },
67
+ /** 提交按钮加载中 */
68
+ submitLoading: { type: Boolean, default: false },
69
+ /** 提交按钮防抖等待时间(ms) */
70
+ submitWait: { type: Number, default: 300 },
71
+ /** 提交按钮是否前缘防抖 */
72
+ submitLeading: { type: Boolean, default: true },
73
+ };
@@ -0,0 +1,168 @@
1
+ import type {
2
+ InputProps,
3
+ InputNumberProps,
4
+ SelectProps,
5
+ SwitchProps,
6
+ RadioGroupProps,
7
+ CheckboxGroupProps,
8
+ DatePickerProps,
9
+ CascaderProps,
10
+ RateProps,
11
+ FormItemRule,
12
+ } from "element-plus";
13
+
14
+ /**
15
+ * 配置驱动的 Form 字段类型定义。
16
+ *
17
+ * 设计:以 `type` 作为判别字段的「可辨识联合」(discriminated union),
18
+ * 每个变体的 `attrs` 精确指向对应 Element Plus 控件的 props,从而在书写配置时
19
+ * 获得 `attrs` 的自动补全与类型校验。
20
+ */
21
+
22
+ /** 选项字段名自定义(用于 select / radio / checkbox / cascader 的 options) */
23
+ export interface OptionFieldProps {
24
+ /** 选项的 label 取值字段,默认 "label" */
25
+ label?: string;
26
+ /** 选项的 value 取值字段,默认 "value" */
27
+ value?: string;
28
+ /** 选项是否禁用取值字段,默认 "disabled" */
29
+ disabled?: string;
30
+ /** 级联子节点取值字段(cascader),默认 "children" */
31
+ children?: string;
32
+ }
33
+
34
+ /** 详情模式(showType: "text")下的文本格式化:函数 或 value→文本 的映射 */
35
+ export type FormatterText =
36
+ | ((value: any, form: Record<string, any>) => any)
37
+ | Record<string | number, any>;
38
+
39
+ /** 所有字段配置的公共基础 */
40
+ export interface BaseFormItem {
41
+ /** 字段在表单数据中的 key;支持点号嵌套路径,如 "user.name" */
42
+ prop?: string;
43
+ /** 标签文字 */
44
+ label?: string;
45
+ /** 栅格占比(1-24);不传则使用表单的 defaultSpan */
46
+ span?: number;
47
+ /** 栅格左侧偏移 */
48
+ offset?: number;
49
+ /** 是否显示该项,默认 true */
50
+ showItem?: boolean;
51
+ /** 用具名插槽整体替换该项的渲染(不渲染内置控件) */
52
+ slot?: string;
53
+ /** 是否必填:为 true 时自动追加必填校验规则与「*」标记 */
54
+ required?: boolean;
55
+ /** 显式校验规则,与自动必填规则合并后传给 el-form-item */
56
+ rules?: FormItemRule[];
57
+ /** 标签后的提示信息(tooltip) */
58
+ tooltip?: string;
59
+ /** 该项 label 宽度,覆盖表单级 labelWidth */
60
+ labelWidth?: string | number;
61
+ /** 详情模式下的文本格式化 */
62
+ formatterText?: FormatterText;
63
+ /** 值变化回调 */
64
+ onChange?: (value: any, form: Record<string, any>) => void;
65
+ }
66
+
67
+ export interface InputFormItem extends BaseFormItem {
68
+ type: "input";
69
+ /** 透传给 ElInput 的属性(modelValue 由表单接管,请勿在此设置) */
70
+ attrs?: Partial<InputProps>;
71
+ }
72
+
73
+ export interface TextareaFormItem extends BaseFormItem {
74
+ type: "textarea";
75
+ /** 透传给 ElInput(type=textarea);rows / autosize 等写在这里 */
76
+ attrs?: Partial<InputProps>;
77
+ }
78
+
79
+ export interface SelectFormItem extends BaseFormItem {
80
+ type: "select";
81
+ /** 选项数组 */
82
+ options: Record<string, any>[];
83
+ /** 选项字段名自定义 */
84
+ optionProps?: OptionFieldProps;
85
+ /** 透传给 ElSelect 的属性 */
86
+ attrs?: Partial<SelectProps>;
87
+ }
88
+
89
+ export interface RadioGroupFormItem extends BaseFormItem {
90
+ type: "radio-group";
91
+ options: Record<string, any>[];
92
+ optionProps?: OptionFieldProps;
93
+ /** 透传给 ElRadioGroup 的属性 */
94
+ attrs?: Partial<RadioGroupProps>;
95
+ }
96
+
97
+ export interface CheckboxGroupFormItem extends BaseFormItem {
98
+ type: "checkbox-group";
99
+ options: Record<string, any>[];
100
+ optionProps?: OptionFieldProps;
101
+ /** 透传给 ElCheckboxGroup 的属性 */
102
+ attrs?: Partial<CheckboxGroupProps>;
103
+ }
104
+
105
+ export interface SwitchFormItem extends BaseFormItem {
106
+ type: "switch";
107
+ /** 透传给 ElSwitch 的属性 */
108
+ attrs?: Partial<SwitchProps>;
109
+ }
110
+
111
+ export interface InputNumberFormItem extends BaseFormItem {
112
+ type: "input-number";
113
+ /** 透传给 ElInputNumber 的属性 */
114
+ attrs?: Partial<InputNumberProps>;
115
+ }
116
+
117
+ export interface DatePickerFormItem extends BaseFormItem {
118
+ type: "date-picker";
119
+ /** 透传给 ElDatePicker 的属性 */
120
+ attrs?: Partial<DatePickerProps>;
121
+ }
122
+
123
+ export interface CascaderFormItem extends BaseFormItem {
124
+ type: "cascader";
125
+ options: Record<string, any>[];
126
+ optionProps?: OptionFieldProps;
127
+ /** 透传给 ElCascader 的属性 */
128
+ attrs?: Partial<CascaderProps>;
129
+ }
130
+
131
+ export interface RateFormItem extends BaseFormItem {
132
+ type: "rate";
133
+ /** 透传给 ElRate 的属性 */
134
+ attrs?: Partial<RateProps>;
135
+ }
136
+
137
+ /** 纯文本展示项(始终只读,常用于在可编辑表单中嵌入不可编辑信息) */
138
+ export interface TextFormItem extends BaseFormItem {
139
+ type: "text";
140
+ }
141
+
142
+ /** 内置支持的字段类型联合 */
143
+ export type FormItemType =
144
+ | InputFormItem
145
+ | TextareaFormItem
146
+ | SelectFormItem
147
+ | RadioGroupFormItem
148
+ | CheckboxGroupFormItem
149
+ | SwitchFormItem
150
+ | InputNumberFormItem
151
+ | DatePickerFormItem
152
+ | CascaderFormItem
153
+ | RateFormItem
154
+ | TextFormItem;
155
+
156
+ /** 纯插槽项:不渲染任何控件,仅占用栅格并交给具名插槽 */
157
+ export interface SlotItem {
158
+ slot: string;
159
+ span?: number;
160
+ offset?: number;
161
+ showItem?: boolean;
162
+ }
163
+
164
+ /** 一个表单项 = 内置字段类型 或 纯插槽项 */
165
+ export type FormItem = FormItemType | SlotItem;
166
+
167
+ /** 字段 type 取值(用于校验/映射) */
168
+ export type FormFieldType = FormItemType["type"];
@@ -0,0 +1,137 @@
1
+ import { ref, watchEffect, computed, inject } from "vue";
2
+ import type { ComputedRef } from "vue";
3
+
4
+ const SHOW_TYPE_KEY = "suna:showType";
5
+
6
+ /**
7
+ * 字段组件级 composable:管理 v-model 代理、详情文本、选项文本回显。
8
+ *
9
+ * 在每个 F*.vue 的 setup 中调用:
10
+ * ```ts
11
+ * const field = useFormField(props, emit)
12
+ * ```
13
+ */
14
+ export function useFormField(
15
+ props: {
16
+ modelValue?: any;
17
+ item: Record<string, any>;
18
+ form: Record<string, any>;
19
+ },
20
+ emit: {
21
+ (e: "update:modelValue", val: any): void;
22
+ (e: "change", val: any): void;
23
+ }
24
+ ) {
25
+ // ---- 值代理 ----
26
+ const valueTmp = ref(props.modelValue);
27
+ watchEffect(() => {
28
+ valueTmp.value = props.modelValue;
29
+ });
30
+
31
+ // ---- 详情/只读模式(表单级控制) ----
32
+ const showType = inject<ComputedRef<"normal" | "text"> | null>(SHOW_TYPE_KEY, null);
33
+ const showText = computed(() => showType?.value === "text");
34
+
35
+ // ---- 详情文本格式化 ----
36
+ const formatTextVal = computed(() => {
37
+ // 情况检查:如果 item 上没有 formatterText,直接返回原始值
38
+ const item = props.item as any;
39
+ const value = props.modelValue;
40
+ if (!item?.formatterText) {
41
+ // 级联需要树形遍历回显,formatterText 也走下面统一出口
42
+ if (item?.type === "cascader") {
43
+ return getCascaderLabel(item.options || [], value);
44
+ }
45
+ // select/radio/checkbox 需要扁平选项回显
46
+ if (["select", "radio-group", "checkbox-group"].includes(item?.type)) {
47
+ return _resolveOptionLabel(value);
48
+ }
49
+ return value ?? "";
50
+ }
51
+ // 函数格式化
52
+ if (typeof item.formatterText === "function") {
53
+ return item.formatterText(value, props.form);
54
+ }
55
+ // 映射格式化
56
+ return item.formatterText[value] ?? value ?? "";
57
+ });
58
+
59
+ // ---- 选项文本回显 ----
60
+ function _resolveOptionLabel(value: any): string | any[] {
61
+ const item = props.item as any;
62
+ const options: Record<string, any>[] = item.options || [];
63
+ const optProps = Object.assign(
64
+ { label: "label", value: "value" },
65
+ item.optionProps || {}
66
+ );
67
+ if (Array.isArray(value)) {
68
+ return value
69
+ .map((v) => {
70
+ const opt = options.find((o) => o[optProps.value] === v);
71
+ return opt ? opt[optProps.label] : v;
72
+ })
73
+ .filter(Boolean)
74
+ .join(", ");
75
+ }
76
+ const opt = options.find((o) => o[optProps.value] === value);
77
+ return opt ? opt[optProps.label] : value;
78
+ }
79
+
80
+ // ---- 级联选中项完整文本 ----
81
+ function getCascaderLabel(list: Record<string, any>[], val: any): string {
82
+ if (!list || !val) return val;
83
+ const optProps = Object.assign(
84
+ { label: "label", value: "value", children: "children" },
85
+ (props.item as any)?.optionProps || {}
86
+ );
87
+ const parts: string[] = [];
88
+ const path = Array.isArray(val) ? val : [val];
89
+ let nodes = list;
90
+ for (const v of path) {
91
+ const node = nodes.find((n) => n[optProps.value] === v);
92
+ if (!node) break;
93
+ parts.push(node[optProps.label] as string);
94
+ nodes = node[optProps.children] || [];
95
+ }
96
+ return parts.join(" / ");
97
+ }
98
+
99
+ // ---- 事件转发 ----
100
+ const handleChange = (val: any) => {
101
+ emit("update:modelValue", val);
102
+ emit("change", val);
103
+ (props.item as any)?.onChange?.(val, props.form);
104
+ };
105
+
106
+ const handleInput = (val: string) => {
107
+ emit("update:modelValue", val);
108
+ (props.item as any)?.onInput?.(val);
109
+ };
110
+
111
+ const clear = () => {
112
+ emit("update:modelValue", null);
113
+ (props.item as any)?.clear?.();
114
+ };
115
+
116
+ const blur = (evt: any) => {
117
+ (props.item as any)?.blur?.(evt);
118
+ };
119
+ const focus = (evt: any) => {
120
+ (props.item as any)?.focus?.(evt);
121
+ };
122
+
123
+ return {
124
+ valueTmp,
125
+ showText,
126
+ formatTextVal,
127
+ resolveOptionLabel: _resolveOptionLabel,
128
+ getCascaderLabel,
129
+ handleChange,
130
+ handleInput,
131
+ clear,
132
+ blur,
133
+ focus,
134
+ };
135
+ }
136
+
137
+ export type UseFormFieldReturn = ReturnType<typeof useFormField>;
@@ -0,0 +1,85 @@
1
+ import type { Component } from "vue";
2
+ import type { FormItemRule } from "element-plus";
3
+ import FInput from "./form-items/FInput.vue";
4
+ import FTextarea from "./form-items/FTextarea.vue";
5
+ import FSelect from "./form-items/FSelect.vue";
6
+ import FRadioGroup from "./form-items/FRadioGroup.vue";
7
+ import FCheckboxGroup from "./form-items/FCheckboxGroup.vue";
8
+ import FSwitch from "./form-items/FSwitch.vue";
9
+ import FInputNumber from "./form-items/FInputNumber.vue";
10
+ import FDatePicker from "./form-items/FDatePicker.vue";
11
+ import FCascader from "./form-items/FCascader.vue";
12
+ import FRate from "./form-items/FRate.vue";
13
+ import FText from "./form-items/FText.vue";
14
+ import type { FormItem, FormFieldType } from "./types";
15
+
16
+ /**
17
+ * 表单级 composable:将字段配置映射为组件、生成校验规则、计算栅格跨度。
18
+ */
19
+ export function useFormItems(defaultSpan: number) {
20
+ /** type → component 映射 */
21
+ const componentMap: Record<string, Component> = {
22
+ input: FInput,
23
+ textarea: FTextarea,
24
+ select: FSelect,
25
+ "radio-group": FRadioGroup,
26
+ "checkbox-group": FCheckboxGroup,
27
+ switch: FSwitch,
28
+ "input-number": FInputNumber,
29
+ "date-picker": FDatePicker,
30
+ cascader: FCascader,
31
+ rate: FRate,
32
+ text: FText,
33
+ };
34
+
35
+ /** 根据 type 获取对应的字段组件 */
36
+ function getType(type: string | undefined): Component | undefined {
37
+ if (!type) return undefined;
38
+ return componentMap[type];
39
+ }
40
+
41
+ /**
42
+ * 为表单项生成校验规则:
43
+ * - `required: true` 自动生成必填规则
44
+ * - 与 `item.rules` 数组合并
45
+ * - 输入型(input/textarea)默认 trigger = "blur",选择型默认 trigger = "change"
46
+ */
47
+ function getRules(item: FormItem): FormItemRule[] {
48
+ const itemAny = item as any;
49
+ // 非字段配置项(纯插槽)直接返回空规则
50
+ if (itemAny.type == null) return [];
51
+
52
+ const isInput = ["input", "textarea"].includes(itemAny.type);
53
+ const trigger = (itemAny.trigger as string) || (isInput ? "blur" : "change");
54
+ const rules: FormItemRule[] = [];
55
+
56
+ // 自动必填规则
57
+ if (itemAny.required) {
58
+ const triggerMessage = isInput ? "请输入" : "请选择";
59
+ rules.push({
60
+ required: true,
61
+ message: `${triggerMessage}${itemAny.label ?? ""}`,
62
+ trigger: trigger as "blur" | "change",
63
+ });
64
+ }
65
+
66
+ // 用户自定义规则(合并)
67
+ if (itemAny.rules) {
68
+ if (Array.isArray(itemAny.rules)) {
69
+ rules.push(...itemAny.rules);
70
+ } else {
71
+ rules.push(itemAny.rules);
72
+ }
73
+ }
74
+
75
+ return rules;
76
+ }
77
+
78
+ /** 计算该字段占据的栅格列数 */
79
+ function getSpan(item: FormItem): number {
80
+ const itemAny = item as any;
81
+ return itemAny.span ?? defaultSpan;
82
+ }
83
+
84
+ return { getType, getRules, getSpan };
85
+ }
package/index.ts ADDED
@@ -0,0 +1,82 @@
1
+ import type { App, Component } from "vue";
2
+ // 全局引入 Element Plus 完整样式(只需一次,各组件内不再单独 import xxx.css)。
3
+ // 使用纯 CSS 的 dist/index.css,而非 element-plus/es/.../style/css 入口——
4
+ // 后者在 VitePress SSR 构建时会被 Node 原生 ESM 加载器当作 JS 解析而报错。
5
+ import "element-plus/dist/index.css";
6
+ import { Button } from "./button";
7
+ import { DebounceButton } from "./debounce-button";
8
+ import { ConfirmButton } from "./confirm-button";
9
+ import { Drawer } from "./drawer";
10
+ import { Form } from "./form";
11
+ import { DrawerForm } from "./drawer-form";
12
+
13
+ /** 组件清单:新增组件时在此登记,baseName 为去掉前缀后的 PascalCase 名称 */
14
+ interface ComponentItem {
15
+ baseName: string;
16
+ component: Component;
17
+ }
18
+
19
+ const componentList: ComponentItem[] = [
20
+ { baseName: "Button", component: Button },
21
+ { baseName: "DebounceButton", component: DebounceButton },
22
+ { baseName: "ConfirmButton", component: ConfirmButton },
23
+ { baseName: "Form", component: Form },
24
+ { baseName: "DrawerForm", component: DrawerForm },
25
+ ];
26
+
27
+ /** 将任意前缀转为 PascalCase:suna -> Suna、my-ui -> MyUi、s -> S */
28
+ function toPascalCase(str: string): string {
29
+ return str
30
+ .split(/[-_\s]+/)
31
+ .filter(Boolean)
32
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
33
+ .join("");
34
+ }
35
+
36
+ export interface CreateComponentsOptions {
37
+ /** 组件注册前缀,默认 "suna" */
38
+ prefix?: string;
39
+ }
40
+
41
+ export interface ComponentsPlugin {
42
+ prefix: string;
43
+ install: (app: App) => void;
44
+ }
45
+
46
+ /**
47
+ * 创建可配置前缀的组件库插件。
48
+ *
49
+ * @example
50
+ * // 默认前缀 suna —— 注册为 <SunaButton />
51
+ * app.use(createComponents())
52
+ *
53
+ * // 自定义前缀 —— 注册为 <SButton />、<MyButton /> 等
54
+ * app.use(createComponents({ prefix: "S" }))
55
+ * app.use(createComponents({ prefix: "my" }))
56
+ */
57
+ export function createComponents(
58
+ options: CreateComponentsOptions = {}
59
+ ): ComponentsPlugin {
60
+ const prefix = options.prefix ?? "suna";
61
+ return {
62
+ prefix,
63
+ install(app: App) {
64
+ const namespace = toPascalCase(prefix);
65
+ componentList.forEach(({ baseName, component }) => {
66
+ app.component(namespace + baseName, component);
67
+ });
68
+ },
69
+ };
70
+ }
71
+
72
+ export { Button, DebounceButton, ConfirmButton, Drawer, Form, DrawerForm };
73
+ export type {
74
+ ButtonType,
75
+ ButtonSize,
76
+ NativeButtonType,
77
+ ElButtonType,
78
+ } from "./button";
79
+ export type { DrawerDirection, DrawerSize, DrawerFooterAlign } from "./drawer";
80
+
81
+ /** 默认插件:等价于 createComponents({ prefix: "suna" }),注册为 <SunaButton /> */
82
+ export default createComponents();
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@scorpioz/suna-components",
3
+ "version": "0.0.2",
4
+ "description": "Vue 3 component library based on Element Plus – config-driven Form, configurable Drawer, debounce/confirm buttons, and more.",
5
+ "type": "module",
6
+ "files": [
7
+ "index.ts",
8
+ "button/",
9
+ "debounce-button/",
10
+ "confirm-button/",
11
+ "drawer/",
12
+ "drawer-form/",
13
+ "form/",
14
+ "package.json",
15
+ "README.md"
16
+ ],
17
+ "main": "./index.ts",
18
+ "module": "./index.ts",
19
+ "types": "./index.ts",
20
+ "exports": {
21
+ ".": "./index.ts"
22
+ },
23
+ "sideEffects": [
24
+ "./index.ts"
25
+ ],
26
+ "license": "MIT",
27
+ "keywords": [
28
+ "vue",
29
+ "vue3",
30
+ "element-plus",
31
+ "component",
32
+ "form",
33
+ "drawer"
34
+ ],
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/scorpioz/web-component.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/scorpioz/web-component/issues"
41
+ },
42
+ "homepage": "https://github.com/scorpioz/web-component#readme",
43
+ "peerDependencies": {
44
+ "element-plus": "^2.14.0",
45
+ "vue": "^3.5.0"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
50
+ }