diff --git a/apps/backend-mock/api/demo/bigint.ts b/apps/backend-mock/api/demo/bigint.ts new file mode 100644 index 000000000..880cc5ea8 --- /dev/null +++ b/apps/backend-mock/api/demo/bigint.ts @@ -0,0 +1,28 @@ +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + const data = ` + { + "code": 0, + "message": "success", + "data": [ + { + "id": 123456789012345678901234567890123456789012345678901234567890, + "name": "John Doe", + "age": 30, + "email": "john-doe@demo.com" + }, + { + "id": 987654321098765432109876543210987654321098765432109876543210, + "name": "Jane Smith", + "age": 25, + "email": "jane@demo.com" + } + ] + } + `; + setHeader(event, 'Content-Type', 'application/json'); + return data; +}); diff --git a/apps/web-antd/src/adapter/form.ts b/apps/web-antd/src/adapter/form.ts index d0932a0da..eb1ac968e 100644 --- a/apps/web-antd/src/adapter/form.ts +++ b/apps/web-antd/src/adapter/form.ts @@ -8,63 +8,63 @@ import type { ComponentType } from './component'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; import { $t } from '@vben/locales'; -/** 手机号正则表达式(中国) */ const MOBILE_REGEX = /(?:0|86|\+86)?1[3-9]\d{9}/; -setupVbenForm({ - config: { - // ant design vue组件库默认都是 v-model:value - baseModelPropName: 'value', +async function initSetupVbenForm() { + setupVbenForm({ + config: { + // ant design vue组件库默认都是 v-model:value + baseModelPropName: 'value', - // 一些组件是 v-model:checked 或者 v-model:fileList - modelPropNameMap: { - Checkbox: 'checked', - Radio: 'checked', - RichTextarea: 'modelValue', - Switch: 'checked', - Upload: 'fileList', + // 一些组件是 v-model:checked 或者 v-model:fileList + modelPropNameMap: { + Checkbox: 'checked', + Radio: 'checked', + Switch: 'checked', + Upload: 'fileList', + }, }, - }, - defineRules: { - // 输入项目必填国际化适配 - required: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { - return $t('ui.formRules.required', [ctx.label]); - } - return true; - }, - // 选择项目必填国际化适配 - selectRequired: (value, _params, ctx) => { - if (value === undefined || value === null) { - return $t('ui.formRules.selectRequired', [ctx.label]); - } - return true; - }, - // 手机号非必填 - mobile: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { + defineRules: { + // 输入项目必填国际化适配 + required: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return $t('ui.formRules.required', [ctx.label]); + } return true; - } else if (!MOBILE_REGEX.test(value)) { - return $t('ui.formRules.mobile', [ctx.label]); - } - return true; + }, + // 选择项目必填国际化适配 + selectRequired: (value, _params, ctx) => { + if (value === undefined || value === null) { + return $t('ui.formRules.selectRequired', [ctx.label]); + } + return true; + }, + // 手机号非必填 + mobile: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return true; + } else if (!MOBILE_REGEX.test(value)) { + return $t('ui.formRules.mobile', [ctx.label]); + } + return true; + }, + // 手机号必填 + mobileRequired: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return $t('ui.formRules.required', [ctx.label]); + } + if (!MOBILE_REGEX.test(value)) { + return $t('ui.formRules.mobile', [ctx.label]); + } + return true; + }, }, - // 手机号必填 - mobileRequired: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { - return $t('ui.formRules.required', [ctx.label]); - } - if (!MOBILE_REGEX.test(value)) { - return $t('ui.formRules.mobile', [ctx.label]); - } - return true; - }, - }, -}); + }); +} const useVbenForm = useForm; -export { useVbenForm, z }; +export { initSetupVbenForm, useVbenForm, z }; export type VbenFormSchema = FormSchema; export type { VbenFormProps }; diff --git a/apps/web-antd/src/bootstrap.ts b/apps/web-antd/src/bootstrap.ts index 68d19ee1a..0f1ab09fb 100644 --- a/apps/web-antd/src/bootstrap.ts +++ b/apps/web-antd/src/bootstrap.ts @@ -14,6 +14,7 @@ import { $t, setupI18n } from '#/locales'; import { setupFormCreate } from '#/plugins/form-create'; import { initComponentAdapter } from './adapter/component'; +import { initSetupVbenForm } from './adapter/form'; import App from './app.vue'; import { router } from './router'; @@ -21,6 +22,9 @@ async function bootstrap(namespace: string) { // 初始化组件适配器 await initComponentAdapter(); + // 初始化表单组件 + await initSetupVbenForm(); + // // 设置弹窗的默认配置 // setDefaultModalProps({ // fullscreenButton: false, diff --git a/apps/web-ele/src/adapter/form.ts b/apps/web-ele/src/adapter/form.ts index 84e9a045e..e2f7e4626 100644 --- a/apps/web-ele/src/adapter/form.ts +++ b/apps/web-ele/src/adapter/form.ts @@ -8,56 +8,55 @@ import type { ComponentType } from './component'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; import { $t } from '@vben/locales'; -/** 手机号正则表达式(中国) */ const MOBILE_REGEX = /(?:0|86|\+86)?1[3-9]\d{9}/; -setupVbenForm({ - config: { - modelPropNameMap: { - Upload: 'fileList', - CheckboxGroup: 'model-value', +async function initSetupVbenForm() { + setupVbenForm({ + config: { + modelPropNameMap: { + Upload: 'fileList', + CheckboxGroup: 'model-value', + }, }, - }, - defineRules: { - // 输入项目必填国际化适配 - required: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { - return $t('ui.formRules.required', [ctx.label]); - } - return true; - }, - // 选择项目必填国际化适配 - selectRequired: (value, _params, ctx) => { - if (value === undefined || value === null) { - return $t('ui.formRules.selectRequired', [ctx.label]); - } - return true; - }, - // 手机号非必填 - mobile: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { + defineRules: { + required: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return $t('ui.formRules.required', [ctx.label]); + } return true; - } else if (!MOBILE_REGEX.test(value)) { - return $t('ui.formRules.mobile', [ctx.label]); - } - return true; + }, + selectRequired: (value, _params, ctx) => { + if (value === undefined || value === null) { + return $t('ui.formRules.selectRequired', [ctx.label]); + } + return true; + }, + // 手机号非必填 + mobile: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return true; + } else if (!MOBILE_REGEX.test(value)) { + return $t('ui.formRules.mobile', [ctx.label]); + } + return true; + }, + // 手机号必填 + mobileRequired: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return $t('ui.formRules.required', [ctx.label]); + } + if (!MOBILE_REGEX.test(value)) { + return $t('ui.formRules.mobile', [ctx.label]); + } + return true; + }, }, - // 手机号必填 - mobileRequired: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { - return $t('ui.formRules.required', [ctx.label]); - } - if (!MOBILE_REGEX.test(value)) { - return $t('ui.formRules.mobile', [ctx.label]); - } - return true; - }, - }, -}); + }); +} const useVbenForm = useForm; -export { useVbenForm, z }; +export { initSetupVbenForm, useVbenForm, z }; export type VbenFormSchema = FormSchema; export type { VbenFormProps }; diff --git a/apps/web-ele/src/bootstrap.ts b/apps/web-ele/src/bootstrap.ts index b82949fb9..c366b2784 100644 --- a/apps/web-ele/src/bootstrap.ts +++ b/apps/web-ele/src/bootstrap.ts @@ -14,12 +14,17 @@ import { $t, setupI18n } from '#/locales'; import { setupFormCreate } from '#/plugins/form-create'; import { initComponentAdapter } from './adapter/component'; +import { initSetupVbenForm } from './adapter/form'; import App from './app.vue'; import { router } from './router'; async function bootstrap(namespace: string) { // 初始化组件适配器 await initComponentAdapter(); + + // 初始化表单组件 + await initSetupVbenForm(); + // // 设置弹窗的默认配置 // setDefaultModalProps({ // fullscreenButton: false, diff --git a/apps/web-naive/src/adapter/form.ts b/apps/web-naive/src/adapter/form.ts index cc3435f01..19fba4f89 100644 --- a/apps/web-naive/src/adapter/form.ts +++ b/apps/web-naive/src/adapter/form.ts @@ -8,58 +8,59 @@ import type { ComponentType } from './component'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; import { $t } from '@vben/locales'; -/** 手机号正则表达式(中国) */ const MOBILE_REGEX = /(?:0|86|\+86)?1[3-9]\d{9}/; -setupVbenForm({ - config: { - // naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效 - emptyStateValue: null, - baseModelPropName: 'value', - modelPropNameMap: { - Checkbox: 'checked', - Radio: 'checked', - Upload: 'fileList', +async function initSetupVbenForm() { + setupVbenForm({ + config: { + // naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效 + emptyStateValue: null, + baseModelPropName: 'value', + modelPropNameMap: { + Checkbox: 'checked', + Radio: 'checked', + Upload: 'fileList', + }, }, - }, - defineRules: { - required: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { - return $t('ui.formRules.required', [ctx.label]); - } - return true; - }, - selectRequired: (value, _params, ctx) => { - if (value === undefined || value === null) { - return $t('ui.formRules.selectRequired', [ctx.label]); - } - return true; - }, - // 手机号非必填 - mobile: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { + defineRules: { + required: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return $t('ui.formRules.required', [ctx.label]); + } return true; - } else if (!MOBILE_REGEX.test(value)) { - return $t('ui.formRules.phone', [ctx.label]); - } - return true; + }, + selectRequired: (value, _params, ctx) => { + if (value === undefined || value === null) { + return $t('ui.formRules.selectRequired', [ctx.label]); + } + return true; + }, + // 手机号非必填 + mobile: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return true; + } else if (!MOBILE_REGEX.test(value)) { + return $t('ui.formRules.mobile', [ctx.label]); + } + return true; + }, + // 手机号必填 + mobileRequired: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return $t('ui.formRules.required', [ctx.label]); + } + if (!MOBILE_REGEX.test(value)) { + return $t('ui.formRules.mobile', [ctx.label]); + } + return true; + }, }, - // 手机号必填 - mobileRequired: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { - return $t('ui.formRules.required', [ctx.label]); - } - if (!MOBILE_REGEX.test(value)) { - return $t('ui.formRules.phone', [ctx.label]); - } - return true; - }, - }, -}); + }); +} const useVbenForm = useForm; -export { useVbenForm, z }; +export { initSetupVbenForm, useVbenForm, z }; export type VbenFormSchema = FormSchema; export type { VbenFormProps }; diff --git a/apps/web-naive/src/bootstrap.ts b/apps/web-naive/src/bootstrap.ts index 5fddd7d87..df0b2cbb8 100644 --- a/apps/web-naive/src/bootstrap.ts +++ b/apps/web-naive/src/bootstrap.ts @@ -12,12 +12,16 @@ import { useTitle } from '@vueuse/core'; import { $t, setupI18n } from '#/locales'; import { initComponentAdapter } from './adapter/component'; +import { initSetupVbenForm } from './adapter/form'; import App from './app.vue'; import { router } from './router'; async function bootstrap(namespace: string) { // 初始化组件适配器 - initComponentAdapter(); + await initComponentAdapter(); + + // 初始化表单组件 + await initSetupVbenForm(); // // 设置弹窗的默认配置 // setDefaultModalProps({ diff --git a/docs/src/components/common-ui/vben-ellipsis-text.md b/docs/src/components/common-ui/vben-ellipsis-text.md index 109f1161c..ce6c0334a 100644 --- a/docs/src/components/common-ui/vben-ellipsis-text.md +++ b/docs/src/components/common-ui/vben-ellipsis-text.md @@ -26,6 +26,12 @@ outline: deep +## 自动显示 tooltip + +通过`tooltip-when-ellipsis`设置,仅在文本长度超出导致省略号出现时才触发 tooltip。 + + + ## API ### Props @@ -37,6 +43,8 @@ outline: deep | maxWidth | 文本区域最大宽度 | `number \| string` | `'100%'` | | placement | 提示浮层的位置 | `'bottom'\|'left'\|'right'\|'top'` | `'top'` | | tooltip | 启用文本提示 | `boolean` | `true` | +| tooltipWhenEllipsis | 内容超出,自动启用文本提示 | `boolean` | `false` | +| ellipsisThreshold | 设置 tooltipWhenEllipsis 后才生效,文本截断检测的像素差异阈值,越大则判断越严格,如果碰见异常情况可以自己设置阈值 | `number` | `3` | | tooltipBackgroundColor | 提示文本的背景颜色 | `string` | - | | tooltipColor | 提示文本的颜色 | `string` | - | | tooltipFontSize | 提示文本的大小 | `string` | - | diff --git a/docs/src/demos/vben-ellipsis-text/auto-display/index.vue b/docs/src/demos/vben-ellipsis-text/auto-display/index.vue new file mode 100644 index 000000000..fb5a32a50 --- /dev/null +++ b/docs/src/demos/vben-ellipsis-text/auto-display/index.vue @@ -0,0 +1,16 @@ + + diff --git a/docs/src/en/guide/essentials/settings.md b/docs/src/en/guide/essentials/settings.md index b7cb3a2de..e5aa71a83 100644 --- a/docs/src/en/guide/essentials/settings.md +++ b/docs/src/en/guide/essentials/settings.md @@ -238,6 +238,7 @@ const defaultPreferences: Preferences = { }, logo: { enable: true, + fit: 'contain', source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp', }, navigation: { @@ -431,6 +432,8 @@ interface HeaderPreferences { interface LogoPreferences { /** Whether the logo is visible */ enable: boolean; + /** Logo image fitting method */ + fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'; /** Logo URL */ source: string; } diff --git a/docs/src/guide/essentials/settings.md b/docs/src/guide/essentials/settings.md index cd7c9380d..cca572d86 100644 --- a/docs/src/guide/essentials/settings.md +++ b/docs/src/guide/essentials/settings.md @@ -237,6 +237,7 @@ const defaultPreferences: Preferences = { }, logo: { enable: true, + fit: 'contain', source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp', }, navigation: { @@ -431,6 +432,8 @@ interface HeaderPreferences { interface LogoPreferences { /** logo是否可见 */ enable: boolean; + /** logo图片适应方式 */ + fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'; /** logo地址 */ source: string; } diff --git a/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap b/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap index df20b55d6..1cccbbb27 100644 --- a/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap @@ -61,6 +61,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj }, "logo": { "enable": true, + "fit": "contain", "source": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp", }, "navigation": { diff --git a/packages/@core/preferences/src/config.ts b/packages/@core/preferences/src/config.ts index 70d15c3e3..835eed557 100644 --- a/packages/@core/preferences/src/config.ts +++ b/packages/@core/preferences/src/config.ts @@ -62,6 +62,7 @@ const defaultPreferences: Preferences = { logo: { enable: true, + fit: 'contain', source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp', }, navigation: { diff --git a/packages/@core/preferences/src/types.ts b/packages/@core/preferences/src/types.ts index cc0fefe31..e640edb5c 100644 --- a/packages/@core/preferences/src/types.ts +++ b/packages/@core/preferences/src/types.ts @@ -134,6 +134,8 @@ interface HeaderPreferences { interface LogoPreferences { /** logo是否可见 */ enable: boolean; + /** logo图片适应方式 */ + fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'; /** logo地址 */ source: string; } diff --git a/packages/@core/ui-kit/form-ui/src/form-api.ts b/packages/@core/ui-kit/form-ui/src/form-api.ts index 4c68a7e5e..ae317aee6 100644 --- a/packages/@core/ui-kit/form-ui/src/form-api.ts +++ b/packages/@core/ui-kit/form-ui/src/form-api.ts @@ -11,7 +11,7 @@ import type { Recordable } from '@vben-core/typings'; import type { FormActions, FormSchema, VbenFormProps } from './types'; -import { toRaw } from 'vue'; +import { isRef, toRaw } from 'vue'; import { Store } from '@vben-core/shared/store'; import { @@ -100,9 +100,26 @@ export class FormApi { getFieldComponentRef( fieldName: string, ): T | undefined { - return this.componentRefMap.has(fieldName) - ? (this.componentRefMap.get(fieldName) as T) + let target = this.componentRefMap.has(fieldName) + ? (this.componentRefMap.get(fieldName) as ComponentPublicInstance) : undefined; + if ( + target && + target.$.type.name === 'AsyncComponentWrapper' && + target.$.subTree.ref + ) { + if (Array.isArray(target.$.subTree.ref)) { + if ( + target.$.subTree.ref.length > 0 && + isRef(target.$.subTree.ref[0]?.r) + ) { + target = target.$.subTree.ref[0]?.r.value as ComponentPublicInstance; + } + } else if (isRef(target.$.subTree.ref.r)) { + target = target.$.subTree.ref.r.value as ComponentPublicInstance; + } + } + return target as T; } /** diff --git a/packages/@core/ui-kit/form-ui/src/use-form-context.ts b/packages/@core/ui-kit/form-ui/src/use-form-context.ts index 60148bfba..4ef182edf 100644 --- a/packages/@core/ui-kit/form-ui/src/use-form-context.ts +++ b/packages/@core/ui-kit/form-ui/src/use-form-context.ts @@ -10,7 +10,7 @@ import { createContext } from '@vben-core/shadcn-ui'; import { isString, mergeWithArrayOverride, set } from '@vben-core/shared/utils'; import { useForm } from 'vee-validate'; -import { object } from 'zod'; +import { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod'; import { getDefaultsForSchema } from 'zod-defaults'; type ExtendFormProps = VbenFormProps & { formApi: ExtendedFormApi }; @@ -52,7 +52,12 @@ export function useFormInitial( if (Reflect.has(item, 'defaultValue')) { set(initialValues, item.fieldName, item.defaultValue); } else if (item.rules && !isString(item.rules)) { + // 检查规则是否适合提取默认值 + const customDefaultValue = getCustomDefaultValue(item.rules); zodObject[item.fieldName] = item.rules; + if (customDefaultValue !== undefined) { + initialValues[item.fieldName] = customDefaultValue; + } } }); @@ -64,6 +69,38 @@ export function useFormInitial( } return mergeWithArrayOverride(initialValues, zodDefaults); } + // 自定义默认值提取逻辑 + function getCustomDefaultValue(rule: any): any { + if (rule instanceof ZodString) { + return ''; // 默认为空字符串 + } else if (rule instanceof ZodNumber) { + return null; // 默认为 null(避免显示 0) + } else if (rule instanceof ZodObject) { + // 递归提取嵌套对象的默认值 + const defaultValues: Record = {}; + for (const [key, valueSchema] of Object.entries(rule.shape)) { + defaultValues[key] = getCustomDefaultValue(valueSchema); + } + return defaultValues; + } else if (rule instanceof ZodIntersection) { + // 对于交集类型,从schema 提取默认值 + const leftDefaultValue = getCustomDefaultValue(rule._def.left); + const rightDefaultValue = getCustomDefaultValue(rule._def.right); + + // 如果左右两边都能提取默认值,合并它们 + if ( + typeof leftDefaultValue === 'object' && + typeof rightDefaultValue === 'object' + ) { + return { ...leftDefaultValue, ...rightDefaultValue }; + } + + // 否则优先使用左边的默认值 + return leftDefaultValue ?? rightDefaultValue; + } else { + return undefined; // 其他类型不提供默认值 + } + } return { delegatedSlots, diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/avatar/avatar.vue b/packages/@core/ui-kit/shadcn-ui/src/components/avatar/avatar.vue index 4f630e196..a63f349c3 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/components/avatar/avatar.vue +++ b/packages/@core/ui-kit/shadcn-ui/src/components/avatar/avatar.vue @@ -5,6 +5,8 @@ import type { AvatarRootProps, } from 'radix-vue'; +import type { CSSProperties } from 'vue'; + import type { ClassType } from '@vben-core/typings'; import { computed } from 'vue'; @@ -16,6 +18,7 @@ interface Props extends AvatarFallbackProps, AvatarImageProps, AvatarRootProps { class?: ClassType; dot?: boolean; dotClass?: ClassType; + fit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'; size?: number; } @@ -28,6 +31,15 @@ const props = withDefaults(defineProps(), { as: 'button', dot: false, dotClass: 'bg-green-500', + fit: 'cover', +}); + +const imageStyle = computed(() => { + const { fit } = props; + if (fit) { + return { objectFit: fit }; + } + return {}; }); const text = computed(() => { @@ -51,7 +63,7 @@ const rootStyle = computed(() => { class="relative flex flex-shrink-0 items-center" > - + {{ text }} (), { logoSize: 32, src: '', theme: 'light', + fit: 'cover', }); @@ -53,6 +58,7 @@ withDefaults(defineProps(), { :alt="text" :src="src" :size="logoSize" + :fit="fit" class="relative rounded-none bg-transparent" />