diff --git a/auto-imports.d.ts b/auto-imports.d.ts index d90ac97..a1e2a71 100644 --- a/auto-imports.d.ts +++ b/auto-imports.d.ts @@ -266,6 +266,7 @@ declare global { const useRoute: typeof import('vue-router')['useRoute'] const useRouter: typeof import('vue-router')['useRouter'] const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth'] + const useSafeNForm: typeof import('./src/utils/use-safe-n-form-auto-imports')['useSafeNForm'] const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] @@ -608,6 +609,7 @@ declare module 'vue' { readonly useRoute: UnwrapRef readonly useRouter: UnwrapRef readonly useSSRWidth: UnwrapRef + readonly useSafeNForm: UnwrapRef readonly useScreenOrientation: UnwrapRef readonly useScreenSafeArea: UnwrapRef readonly useScriptTag: UnwrapRef diff --git a/package.json b/package.json index 39c4334..fa1aec4 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@unhead/vue": "^2.0.14", "@vueuse/core": "^13.9.0", "highlight.js": "^11.11.1", + "lodash-es": "^4.17.21", "naive-ui": "^2.43.1", "pinia": "^3.0.3", "primeicons": "^7.0.0", @@ -89,6 +90,7 @@ "@tsconfig/node22": "^22.0.2", "@types/html-minifier-terser": "^7.0.2", "@types/jsdom": "^27.0.0", + "@types/lodash-es": "^4.17.12", "@types/node": "^22.18.1", "@vant/auto-import-resolver": "^1.3.0", "@vitejs/plugin-vue": "^6.0.1", @@ -124,6 +126,7 @@ "stylelint-define-config": "^16.24.0", "svgo": "^4.0.0", "tinyglobby": "^0.2.15", + "type-fest": "^5.1.0", "typescript": "~5.9.2", "unocss": "^66.5.1", "unocss-preset-animations": "^1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3906dbd..588cc77 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: highlight.js: specifier: ^11.11.1 version: 11.11.1 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 naive-ui: specifier: ^2.43.1 version: 2.43.1(vue@3.5.22(typescript@5.9.2)) @@ -120,6 +123,9 @@ importers: '@types/jsdom': specifier: ^27.0.0 version: 27.0.0 + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 '@types/node': specifier: ^22.18.1 version: 22.18.11 @@ -225,6 +231,9 @@ importers: tinyglobby: specifier: ^0.2.15 version: 0.2.15 + type-fest: + specifier: ^5.1.0 + version: 5.1.0 typescript: specifier: ~5.9.2 version: 5.9.2 @@ -5167,6 +5176,10 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + terser@5.44.0: resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} engines: {node: '>=10'} @@ -5259,6 +5272,10 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} + type-fest@5.1.0: + resolution: {integrity: sha512-wQ531tuWvB6oK+pchHIu5lHe5f5wpSCqB8Kf4dWQRbOYc9HTge7JL0G4Qd44bh6QuJCccIzL3bugb8GI0MwHrg==} + engines: {node: '>=20'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -11011,6 +11028,8 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + tagged-tag@1.0.0: {} + terser@5.44.0: dependencies: '@jridgewell/source-map': 0.3.11 @@ -11088,6 +11107,10 @@ snapshots: type-fest@0.20.2: {} + type-fest@5.1.0: + dependencies: + tagged-tag: 1.0.0 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 diff --git a/src/pages/demos/naive-ui-demo.page.vue b/src/pages/demos/naive-ui-demo/index.page.vue similarity index 92% rename from src/pages/demos/naive-ui-demo.page.vue rename to src/pages/demos/naive-ui-demo/index.page.vue index 33ddbdd..bab7462 100644 --- a/src/pages/demos/naive-ui-demo.page.vue +++ b/src/pages/demos/naive-ui-demo/index.page.vue @@ -1,12 +1,12 @@ + + diff --git a/src/utils/use-safe-n-form-auto-imports.tsx b/src/utils/use-safe-n-form-auto-imports.tsx new file mode 100644 index 0000000..ced402d --- /dev/null +++ b/src/utils/use-safe-n-form-auto-imports.tsx @@ -0,0 +1,121 @@ +/** + * https://www.naiveui.com/zh-CN/os-theme/components/form + * + * FIXME: `NForm` 和 `NFormItem` 的 slots 还没有实现。`NFormItemGi`组件。 + */ + +import { get, set } from 'lodash-es'; +import type { FormInst, FormItemProps, FormProps } from 'naive-ui'; +import { NForm, NFormItem, formItemProps, NInput, formProps } from 'naive-ui'; +import type { Get, Paths } from 'type-fest'; +import type { SlotsType } from 'vue'; +import { Comment } from 'vue'; + +type UseSafeNFormOptions = { + initialFormValue?: FormValue; +}; + +export function useSafeNForm = Record>( + options: UseSafeNFormOptions = {}, +) { + const formRef = ref(null); + const formValue = ref(structuredClone(toRaw(options.initialFormValue)) || ({} as T)); + + // 创建类型安全的 Form 组件 + type SafeNFormProps = FormProps; + + type SafeNFormSlots = SlotsType<{ + default?: { count?: number }; + }>; + + const SafeNForm = defineComponent( + (props, ctx) => { + return () => ( + { + formRef.value = inst as unknown as FormInst; + }} + > + {ctx.slots.default?.({})} + + ); + }, + { + name: 'SafeNForm', + inheritAttrs: true, + props: formProps, + }, + ); + // <<<<< + + // >>>>> 创建类型安全的 FormItem 组件 + type SafeNFormItemProps

& string> = FormItemProps & { + path: P; + }; + + type SafeNFormItemDefaultSlot

& string> = { + value: Get; + setValue: (val: Get) => void; + }; + + const SafeNFormItemImpl = defineComponent< + SafeNFormItemProps & string>, + /* Emits */ [], + /* EE */ never, + SlotsType<{ default: SafeNFormItemDefaultSlot & string> }> + >( + (props, ctx) => { + return () => { + const value = get(formValue.value, props.path); + function setValue(val: typeof value) { + set(formValue.value, props.path, val); + } + + const defaultSlotContent = ctx.slots.default?.({ + value, + setValue, + }); + + // 如果没有提供默认 slot 内容,则渲染一个 NInput 作为默认输入组件 + const renderDefaultNInput = defaultSlotContent?.some((v) => v.type !== Comment) ? null : ( + + ); + + return ( + + {defaultSlotContent} + {renderDefaultNInput} + + ); + }; + }, + { + name: 'SafeNFormItem', + inheritAttrs: false, + props: Object.keys(formItemProps) as unknown as [keyof FormItemProps], + }, + ); + + // Expose a generic constructor so template literals narrow `path`. + type SafeNFormItemComponent = { + new

& string>( + props: SafeNFormItemProps

, + ): { + $props: SafeNFormItemProps

; + $slots: { + default?: (scope: SafeNFormItemDefaultSlot

) => VNode[]; + }; + }; + }; + const SafeNFormItem = SafeNFormItemImpl as SafeNFormItemComponent; + // <<<<< + + return { + formValue, + SafeNForm, + SafeNFormItem, + formRef, + }; +} diff --git a/typed-router.d.ts b/typed-router.d.ts index 595fbe1..b2381a2 100644 --- a/typed-router.d.ts +++ b/typed-router.d.ts @@ -149,7 +149,7 @@ declare module 'vue-router/auto-routes' { views: | never } - 'src/pages/demos/naive-ui-demo.page.vue': { + 'src/pages/demos/naive-ui-demo/index.page.vue': { routes: | 'DemosNaiveUiDemo' views: