Compare commits

..

1 Commits
v5.2.2 ... main

Author SHA1 Message Date
XiaoDaiGua-Ray
49af61a339 version: v5.2.2 2025-08-09 19:04:29 +08:00
16 changed files with 959 additions and 389 deletions

View File

@ -22,6 +22,7 @@
"cSpell.words": [ "cSpell.words": [
"baomitu", "baomitu",
"bezier", "bezier",
"Cascader",
"Clickoutside", "Clickoutside",
"codabar", "codabar",
"commitmsg", "commitmsg",
@ -44,6 +45,7 @@
"siderbar", "siderbar",
"snapline", "snapline",
"stylelint", "stylelint",
"unocss",
"WUJIE", "WUJIE",
"zlevel" "zlevel"
] ]

View File

@ -1,3 +1,23 @@
## 5.2.2
## Feats
- `RForm` 组件相关
- 新增 `submitWhenEnter` 配置项,允许在按下回车键时自动触发表单的校验,如果校验成功则会自动触发 `onFinish` 事件
- 新增 `onFinish` 配置项,允许在表单校验成功后自动触发的事件
- 新增 `autocomplete` 配置项,允许配置表单的自动完成功能,默认配置为 `off`
- 新增 `loading` 配置项,允许配置表单的加载状态
- 新增 `loadingDescription` 配置项,允许配置表单的加载状态的描述
- `useForm` 相关
- 新增 `validateTargetField` 方法,允许验证指定表单项的规则
- 初始化方法现在支持传入函数,允许动态获取表单的初始化值与规则
- `formModel` 方法现在会默认联合 `Recordable` 类型,获取初始化类型中未获取到的类型时,默认推到为 `any` 类型
- 新增了 `formConditionRef` 属性,现在可以在内部解构获取一个 `ref` 包裹的响应式初始化表单对象值
- 新增了 `updateFormCondition` 方法,允许更新表单的值,该方法会覆盖初始化值
- 更新依赖为主流版本
- 新增 `unocss` 原子化样式库,但是不推荐全量使用,仅作为一些简单的样式片段使用,否则在调试的时候将会是灾难
> 新增 `unocss` 后,在使用 `ProTable` 组件的流体高度最外层父元素配置时,可以便捷的配置 `h-full` 即可。
## 5.2.1 ## 5.2.1
## Feats ## Feats

View File

@ -1,7 +1,7 @@
{ {
"name": "ray-template", "name": "ray-template",
"private": false, "private": false,
"version": "5.2.1", "version": "5.2.2",
"type": "module", "type": "module",
"engines": { "engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0", "node": "^18.0.0 || ^20.0.0 || >=22.0.0",
@ -48,8 +48,8 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mockjs": "1.1.0", "mockjs": "1.1.0",
"naive-ui": "^2.42.0", "naive-ui": "^2.42.0",
"pinia": "^3.0.1", "pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.2.0", "pinia-plugin-persistedstate": "^4.4.1",
"print-js": "^1.6.0", "print-js": "^1.6.0",
"vue": "^3.5.17", "vue": "^3.5.17",
"vue-demi": "0.14.10", "vue-demi": "0.14.10",
@ -59,8 +59,6 @@
"vue3-next-qrcode": "3.0.2" "vue3-next-qrcode": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@amap/amap-jsapi-types": "0.0.15",
"@ant-design/icons-vue": "7.0.1",
"@commitlint/cli": "19.7.1", "@commitlint/cli": "19.7.1",
"@commitlint/config-conventional": "19.7.1", "@commitlint/config-conventional": "19.7.1",
"@eslint/js": "9.28.0", "@eslint/js": "9.28.0",
@ -70,7 +68,6 @@
"@types/jsbarcode": "3.11.4", "@types/jsbarcode": "3.11.4",
"@types/lodash-es": "4.17.12", "@types/lodash-es": "4.17.12",
"@types/mockjs": "1.0.10", "@types/mockjs": "1.0.10",
"@types/three": "0.171.0",
"@typescript-eslint/eslint-plugin": "8.24.0", "@typescript-eslint/eslint-plugin": "8.24.0",
"@typescript-eslint/parser": "8.24.0", "@typescript-eslint/parser": "8.24.0",
"@vitejs/plugin-vue": "5.2.3", "@vitejs/plugin-vue": "5.2.3",
@ -96,6 +93,7 @@
"sass": "1.86.3", "sass": "1.86.3",
"svg-sprite-loader": "6.0.11", "svg-sprite-loader": "6.0.11",
"typescript": "5.8.3", "typescript": "5.8.3",
"unocss": "66.3.3",
"unplugin-auto-import": "19.1.2", "unplugin-auto-import": "19.1.2",
"unplugin-vue-components": "0.28.0", "unplugin-vue-components": "0.28.0",
"vite": "6.3.5", "vite": "6.3.5",

834
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,41 @@
import { NForm } from 'naive-ui' import { NForm, NSpin } from 'naive-ui'
import props from './props' import props from './props'
import { call } from '@/utils' import { call, unrefElement } from '@/utils'
import { useTemplateRef } from 'vue' import { useTemplateRef } from 'vue'
import { useEventListener } from '@vueuse/core'
import type { RFormInst } from './types' import type { RFormInst } from './types'
import type { FormProps } from 'naive-ui' import type { ShallowRef } from 'vue'
export default defineComponent({ export default defineComponent({
name: 'RForm', name: 'RForm',
props, props,
setup(props, { expose }) { setup(props, { expose }) {
const formRef = useTemplateRef<RFormInst>('formRef') const formRef = useTemplateRef<RFormInst>('formRef')
const currentSubmitFn = computed(() => props.onFinish ?? Promise.resolve)
const bindKeydownListener = (e: KeyboardEvent) => {
const keyCode = e.code
if (keyCode === 'Enter') {
e.stopPropagation()
e.preventDefault()
formRef.value?.validate().then(currentSubmitFn.value)
}
}
if (props.submitWhenEnter) {
useEventListener(
formRef as unknown as ShallowRef<HTMLElement>,
'keydown',
bindKeydownListener,
{
capture: true,
},
)
}
onMounted(() => { onMounted(() => {
// 主动调用 register 方法,满足 useForm 方法正常调用 // 主动调用 register 方法,满足 useForm 方法正常调用
@ -20,6 +44,16 @@ export default defineComponent({
if (onRegister && formRef.value) { if (onRegister && formRef.value) {
call(onRegister, formRef.value) call(onRegister, formRef.value)
} }
if (formRef.value) {
const formElement = unrefElement(
formRef.value as unknown as HTMLFormElement,
)
if (formElement) {
formElement.autocomplete = props.autocomplete
}
}
}) })
expose() expose()
@ -30,13 +64,22 @@ export default defineComponent({
}, },
render() { render() {
const { $attrs, $props, $slots } = this const { $attrs, $props, $slots } = this
const { loading, loadingDescription, ...restProps } = $props
return ( return (
<NForm {...$attrs} {...($props as FormProps)} ref="formRef"> <NSpin
{{ show={loading}
...$slots, description={loadingDescription}
style={{
height: 'auto',
}} }}
</NForm> >
<NForm {...$attrs} {...restProps} ref="formRef">
{{
...$slots,
}}
</NForm>
</NSpin>
) )
}, },
}) })

View File

@ -40,10 +40,11 @@ const useForm = <
T extends Recordable = Recordable, T extends Recordable = Recordable,
R extends RFormRules = RFormRules, R extends RFormRules = RFormRules,
>( >(
model?: T, model?: T | (() => T),
rules?: R, rules?: R | (() => R),
) => { ) => {
const formRef = shallowRef<RFormInst>() const formRef = shallowRef<RFormInst>()
const formModelRef = ref<T>()
const register = (inst: RFormInst) => { const register = (inst: RFormInst) => {
if (inst) { if (inst) {
@ -61,6 +62,15 @@ const useForm = <
return formRef.value return formRef.value
} }
// 初始化 formModelRef 的值,根据 model 的类型进行初始化
const initialFormModel = () => {
if (typeof model === 'function') {
formModelRef.value = model() ?? ({} as T)
} else {
formModelRef.value = cloneDeep(model) ?? ({} as T)
}
}
/** /**
* *
* @description * @description
@ -91,6 +101,9 @@ const useForm = <
* vue * vue
* Form * Form
* *
* 5.2.2 formConditionRef ref
* hook
*
* @example * @example
* *
* interface FormModel { * interface FormModel {
@ -109,7 +122,13 @@ const useForm = <
* formModelRef.value = formModel() * formModelRef.value = formModel()
* } * }
*/ */
const formModel = () => cloneDeep(model) || ({} as T) const formModel = (): T & Recordable => {
if (typeof model === 'function') {
return model()
}
return cloneDeep(model) || ({} as T)
}
/** /**
* *
@ -118,12 +137,17 @@ const useForm = <
* *
* useForm rules * useForm rules
*/ */
const formRules = () => cloneDeep(rules) || ({} as R) const formRules = () => {
if (typeof rules === 'function') {
return rules()
}
return cloneDeep(rules) || ({} as R)
}
/** /**
* *
* @param values * @param values
* @param extraValues
* *
* @warning * @warning
* undefined * undefined
@ -137,14 +161,80 @@ const useForm = <
* useForm * useForm
* *
*/ */
const reset = <Values extends T = T>( const reset = <Values extends T = T>(values?: Values & Recordable) => {
values: Values & Recordable, formModelRef.value = Object.assign(
extraValues?: Recordable, formModelRef.value as T,
) => { formModel(),
Object.assign(values ?? {}, formModel(), extraValues) values,
)
restoreValidation() restoreValidation()
} }
/**
*
* @param key key
*
* @see https://www.naiveui.com/zh-CN/dark/components/form#partially-apply-rules.vue
*
* @description
*
*
* rules key
*
*
* @example
* const [register, { validateTargetField }] = useForm(
* {
* name: null,
* },
* {
* name: {
* required: true,
* message: 'name is required',
* trigger: ['blur', 'change'],
* type: 'string',
* key: 'name',
* },
* },
* )
*
* validateTargetField('name')
*/
const validateTargetField = (key: string) => {
if (!key || typeof key !== 'string') {
throw new Error(
`[useForm-validateTargetField]: except key is string, but got ${typeof key}.`,
)
}
return validate(void 0, (rules) => {
return rules?.key === key
})
}
/**
*
* @description
*
* 使
*
* @example
* const [register, { updateFormCondition }] = useForm(
* {
* name: null,
* },
* )
*
* updateFormCondition({
* name: 'John',
* })
*/
const updateFormCondition = (values: T & Recordable) => {
formModelRef.value = Object.assign(formModelRef.value as T, values)
}
initialFormModel()
return [ return [
register, register,
{ {
@ -154,6 +244,9 @@ const useForm = <
formModel, formModel,
formRules, formRules,
reset, reset,
validateTargetField,
formConditionRef: formModelRef as Ref<T>,
updateFormCondition,
}, },
] as const ] as const
} }

View File

@ -1,10 +1,75 @@
import { formProps } from 'naive-ui' import { formProps } from 'naive-ui'
import { omit } from 'lodash-es'
import type { MaybeArray } from '@/types' import type { MaybeArray, AnyFC } from '@/types'
import type { RFormInst } from './types' import type { RFormInst } from './types'
const props = { const props = {
...formProps, ...omit(formProps, ['onSubmit']),
/**
*
* @description
*
*
* @default false
*/
loading: {
type: Boolean,
default: false,
},
/**
*
* @description
*
*
* @default undefined
*/
loadingDescription: {
type: String,
default: void 0,
},
/**
*
* @description
*
*
* @default 'off'
*/
autocomplete: {
type: String as PropType<AutoFillBase>,
default: 'off',
},
/**
*
* @description
* onFinish
* onFinish 使
*
*
*
*
* Enter
* NSelect, NInput
*
*
* @default false
*/
submitWhenEnter: {
type: Boolean,
default: false,
},
/**
*
* @description
* submitWhenEnter true
* submitWhenEnter 使
*
* @default null
*/
onFinish: {
type: Function as PropType<AnyFC>,
default: null,
},
/** /**
* *
* @description * @description

View File

@ -31,8 +31,9 @@ import type { TransitionProps } from './types'
/** /**
* *
* 使用宏编译模式时可以使用 defineOptions 声明组件选项 * @description
* 常用方法即是声明该组件的 name inheritAttrs 等属性 * 使用宏编译模式时可以使用 defineOptions 声明组件选项
* 常用方法即是声明该组件的 name inheritAttrs 等属性
*/ */
defineOptions({ defineOptions({
name: 'RTransitionComponent', name: 'RTransitionComponent',

View File

@ -5,6 +5,26 @@ import collapseGridProps from '../../base/RCollapseGrid/src/props'
import type { GridProps } from 'naive-ui' import type { GridProps } from 'naive-ui'
export const collapseProps = Object.assign({}, formProps, {
...collapseGridProps,
open: {
type: Boolean,
default: true,
},
cols: {
type: [Number, String] as PropType<GridProps['cols']>,
default: '4 xs:1 s:2 m:2 l:4 xl:4 2xl:6',
},
bordered: {
type: Boolean,
default: true,
},
responsive: {
type: String as PropType<GridProps['responsive']>,
default: 'screen',
},
})
/** /**
* *
* @description * @description
@ -13,25 +33,7 @@ import type { GridProps } from 'naive-ui'
*/ */
export default defineComponent({ export default defineComponent({
name: 'RCollapse', name: 'RCollapse',
props: Object.assign({}, formProps, { props: collapseProps,
...collapseGridProps,
open: {
type: Boolean,
default: true,
},
cols: {
type: [Number, String] as PropType<GridProps['cols']>,
default: '4 xs:1 s:2 m:2 l:4 xl:4 2xl:6',
},
bordered: {
type: Boolean,
default: true,
},
responsive: {
type: String as PropType<GridProps['responsive']>,
default: 'screen',
},
}),
render() { render() {
const { $slots, $props } = this const { $slots, $props } = this
const { labelPlacement, showFeedback, ...rest } = $props const { labelPlacement, showFeedback, ...rest } = $props

View File

@ -5,6 +5,7 @@ import './app-components/provider/provider.scss' // 初始化 provider 包注入
import 'vue3-next-qrcode/es/style.css' // vue3-next-qrcode 样式 import 'vue3-next-qrcode/es/style.css' // vue3-next-qrcode 样式
import 'virtual:svg-icons-register' // vite-plugin-svg-icons 脚本,启用 svg 雪碧图 import 'virtual:svg-icons-register' // vite-plugin-svg-icons 脚本,启用 svg 雪碧图
import 'virtual:uno.css'
import { setupRouter } from './router' import { setupRouter } from './router'
import { setupStore } from './store' import { setupStore } from './store'

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
type RecordKey = string | number | symbol export type RecordKey = string | number | symbol
/** /**
* *

View File

@ -10,9 +10,12 @@ import {
NRadio, NRadio,
NRadioGroup, NRadioGroup,
NCard, NCard,
NText,
NSwitch,
} from 'naive-ui' } from 'naive-ui'
import { useForm } from '@/components' import { useForm } from '@/components'
import { useHookPlusRequest } from '@/axios'
interface FormModel { interface FormModel {
name: string | null name: string | null
@ -29,12 +32,12 @@ export default defineComponent({
const [ const [
register, register,
{ {
getFormInstance,
validate, validate,
restoreValidation, restoreValidation,
formModel,
formRules, formRules,
reset, reset,
validateTargetField,
formConditionRef,
}, },
] = useForm<FormModel>( ] = useForm<FormModel>(
{ {
@ -71,101 +74,124 @@ export default defineComponent({
}, },
) )
/** const formLoading = ref(false)
*
* @description
* useForm
* 使 formModel model
*
* vue 使
* 便
*/
const condition = ref(formModel())
/** const { run: runHookPlusRequest } = useHookPlusRequest(
* () => {
* @param key return new Promise((resolve, reject) => {
* validate()
* @see https://www.naiveui.com/zh-CN/dark/components/form#partially-apply-rules.vue .then(() => {
* formLoading.value = true
* @description
* setTimeout(() => {
*/ window.$message.success('校验成功')
const onlyValidateSection = (key: string) => { resolve(true)
validate(void 0, (rules) => { }, 500)
return rules?.key === key })
}) .catch(reject)
} .finally(() => {
formLoading.value = false
})
})
},
{
manual: true,
},
)
return { return {
register, register,
condition, formConditionRef,
restoreValidation, restoreValidation,
validate, validate,
formRules, formRules,
onlyValidateSection, validateTargetField,
reset, reset,
formLoading,
runHookPlusRequest,
} }
}, },
render() { render() {
const { condition } = this const { formConditionRef } = this
const { const {
register, register,
restoreValidation, restoreValidation,
validate,
formRules, formRules,
onlyValidateSection, validateTargetField,
reset, reset,
runHookPlusRequest,
} = this } = this
return ( return (
<NCard title="useForm 表单校验"> <NCard
<RForm onRegister={register} rules={formRules()} model={condition}> title={() => (
<NGrid cols={24} xGap={24}> <NFlex align="center">
<NFormItemGi label="姓名" path="name" span={12}> <NText>useForm </NText>
<NInput v-model:value={condition.name} /> <NSwitch v-model:value={this.formLoading} />
</NFormItemGi> </NFlex>
<NFormItemGi label="年龄" path="age" span={12}> )}
<NInputNumber >
v-model:value={condition.age} {{
showButton={false} default: () => (
style="width: 100%" <RForm
/> onRegister={register}
</NFormItemGi> rules={formRules()}
<NFormItemGi label="出生日期" path="date" span={12}> model={formConditionRef}
<NDatePicker v-model:value={condition.date} style="width: 100%" /> submitWhenEnter
</NFormItemGi> onFinish={() => {
<NFormItemGi label="性别" path="gender" span={12}> window.$message.success('表单提交成功')
<NRadioGroup v-model:value={condition.gender}> }}
<NRadio value="girl"></NRadio> loading={this.formLoading}
<NRadio value="man"></NRadio> >
</NRadioGroup> <NGrid cols={24} xGap={24}>
</NFormItemGi> <NFormItemGi label="姓名" path="name" span={12}>
<NFormItemGi label="备注信息" span={24}> <NInput v-model:value={formConditionRef.name} />
<NInput type="textarea" v-model:value={condition.remark} /> </NFormItemGi>
</NFormItemGi> <NFormItemGi label="年龄" path="age" span={12}>
<NFormItemGi span={24}> <NInputNumber
<NFlex justify="flex-end" style="width: 100%"> v-model:value={formConditionRef.age}
<NButton type="info" onClick={() => reset(this.condition)}> showButton={false}
/>
</NButton> </NFormItemGi>
<NButton type="warning" onClick={restoreValidation.bind(this)}> <NFormItemGi label="出生日期" path="date" span={12}>
<NDatePicker v-model:value={formConditionRef.date} />
</NButton> </NFormItemGi>
<NButton <NFormItemGi label="性别" path="gender" span={12}>
type="primary" <NRadioGroup v-model:value={formConditionRef.gender}>
onClick={() => onlyValidateSection('name')} <NRadio value="girl"></NRadio>
> <NRadio value="man"></NRadio>
</NRadioGroup>
</NButton> </NFormItemGi>
<NButton type="primary" onClick={() => validate()}> <NFormItemGi label="备注信息" span={24}>
<NInput
</NButton> type="textarea"
<NButton attrType="reset"></NButton> v-model:value={formConditionRef.remark}
</NFlex> />
</NFormItemGi> </NFormItemGi>
</NGrid> <NFormItemGi span={24}>
</RForm> <NFlex>
<NButton type="info" onClick={() => reset()}>
</NButton>
<NButton type="warning" onClick={restoreValidation}>
</NButton>
<NButton
type="primary"
onClick={() => validateTargetField('name')}
>
</NButton>
<NButton type="primary" onClick={runHookPlusRequest}>
</NButton>
</NFlex>
</NFormItemGi>
</NGrid>
</RForm>
),
'header-extra': () => '输入表单的时候,试试按下 Enter 键',
}}
</NCard> </NCard>
) )
}, },

5
uno.config.ts Normal file
View File

@ -0,0 +1,5 @@
import { defineConfig } from 'unocss'
export default defineConfig({
safelist: ['w-full', 'h-full'],
})

View File

@ -88,6 +88,7 @@
"useModel": true, "useModel": true,
"useTemplateRef": true, "useTemplateRef": true,
"Slot": true, "Slot": true,
"Slots": true "Slots": true,
"NFormItem": true
} }
} }

View File

@ -7,6 +7,7 @@
export {} export {}
declare global { declare global {
const EffectScope: typeof import('vue')['EffectScope'] const EffectScope: typeof import('vue')['EffectScope']
const NFormItem: typeof import('naive-ui')['NFormItem']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const computed: typeof import('vue')['computed'] const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp'] const createApp: typeof import('vue')['createApp']

View File

@ -13,6 +13,7 @@ import viteEslint from 'vite-plugin-eslint'
import mockDevServerPlugin from 'vite-plugin-mock-dev-server' import mockDevServerPlugin from 'vite-plugin-mock-dev-server'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import unpluginViteComponents from 'unplugin-vue-components/vite' import unpluginViteComponents from 'unplugin-vue-components/vite'
import unoCSS from 'unocss/vite'
import { cdn as viteCDNPlugin } from 'vite-plugin-cdn2' import { cdn as viteCDNPlugin } from 'vite-plugin-cdn2'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
@ -212,6 +213,7 @@ function baseOptions(mode: string): PluginOption[] {
inject: 'body-last', inject: 'body-last',
customDomId: '__svg__icons__dom__', customDomId: '__svg__icons__dom__',
}), }),
unoCSS(),
] ]
} }