diff --git a/.vscode/settings.json b/.vscode/settings.json index 8b76b276..da724dd1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -223,5 +223,16 @@ "commentTranslate.multiLineMerge": true, "vue.server.hybridMode": true, "typescript.tsdk": "node_modules/typescript/lib", - "oxc.enable": false + "oxc.enable": false, + "cSpell.words": [ + "archiver", + "axios", + "dotenv", + "isequal", + "jspm", + "napi", + "nolebase", + "rollup", + "vitest" + ] } diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index 7ae93072..5394c813 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/apps/web-antd/src/adapter/component/index.ts @@ -8,35 +8,64 @@ import type { Component } from 'vue'; import type { BaseFormComponentType } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; -import { defineComponent, getCurrentInstance, h, ref } from 'vue'; +import { + defineAsyncComponent, + defineComponent, + getCurrentInstance, + h, + ref, +} from 'vue'; import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import { $t } from '@vben/locales'; -import { - AutoComplete, - Button, - Checkbox, - CheckboxGroup, - DatePicker, - Divider, - Input, - InputNumber, - InputPassword, - Mentions, - notification, - Radio, - RadioGroup, - RangePicker, - Rate, - Select, - Space, - Switch, - Textarea, - TimePicker, - TreeSelect, - Upload, -} from 'ant-design-vue'; +import { notification } from 'ant-design-vue'; + +const AutoComplete = defineAsyncComponent( + () => import('ant-design-vue/es/auto-complete'), +); +const Button = defineAsyncComponent(() => import('ant-design-vue/es/button')); +const Checkbox = defineAsyncComponent( + () => import('ant-design-vue/es/checkbox'), +); +const CheckboxGroup = defineAsyncComponent(() => + import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup), +); +const DatePicker = defineAsyncComponent( + () => import('ant-design-vue/es/date-picker'), +); +const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider')); +const Input = defineAsyncComponent(() => import('ant-design-vue/es/input')); +const InputNumber = defineAsyncComponent( + () => import('ant-design-vue/es/input-number'), +); +const InputPassword = defineAsyncComponent(() => + import('ant-design-vue/es/input').then((res) => res.InputPassword), +); +const Mentions = defineAsyncComponent( + () => import('ant-design-vue/es/mentions'), +); +const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio')); +const RadioGroup = defineAsyncComponent(() => + import('ant-design-vue/es/radio').then((res) => res.RadioGroup), +); +const RangePicker = defineAsyncComponent(() => + import('ant-design-vue/es/date-picker').then((res) => res.RangePicker), +); +const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate')); +const Select = defineAsyncComponent(() => import('ant-design-vue/es/select')); +const Space = defineAsyncComponent(() => import('ant-design-vue/es/space')); +const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch')); +const Textarea = defineAsyncComponent(() => + import('ant-design-vue/es/input').then((res) => res.Textarea), +); +const TimePicker = defineAsyncComponent( + () => import('ant-design-vue/es/time-picker'), +); +const TreeSelect = defineAsyncComponent( + () => import('ant-design-vue/es/tree-select'), +); +const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload')); const withDefaultPlaceholder = ( component: T, diff --git a/apps/web-antd/src/bootstrap.ts b/apps/web-antd/src/bootstrap.ts index fe7d9650..e4aaf405 100644 --- a/apps/web-antd/src/bootstrap.ts +++ b/apps/web-antd/src/bootstrap.ts @@ -1,8 +1,7 @@ import { createApp, watchEffect } from 'vue'; import { registerAccessDirective } from '@vben/access'; -import { initTippy, registerLoadingDirective } from '@vben/common-ui'; -import { MotionPlugin } from '@vben/plugins/motion'; +import { registerLoadingDirective } from '@vben/common-ui/es/loading'; import { preferences } from '@vben/preferences'; import { initStores } from '@vben/stores'; import '@vben/styles'; @@ -47,12 +46,14 @@ async function bootstrap(namespace: string) { registerAccessDirective(app); // 初始化 tippy + const { initTippy } = await import('@vben/common-ui/es/tippy'); initTippy(app); // 配置路由及路由守卫 app.use(router); // 配置Motion插件 + const { MotionPlugin } = await import('@vben/plugins/motion'); app.use(MotionPlugin); // 动态更新标题 diff --git a/apps/web-antd/src/router/routes/core.ts b/apps/web-antd/src/router/routes/core.ts index 9db6312d..ad8fc861 100644 --- a/apps/web-antd/src/router/routes/core.ts +++ b/apps/web-antd/src/router/routes/core.ts @@ -2,10 +2,10 @@ import type { RouteRecordRaw } from 'vue-router'; import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; -import { AuthPageLayout, BasicLayout } from '#/layouts'; import { $t } from '#/locales'; -import Login from '#/views/_core/authentication/login.vue'; +const BasicLayout = () => import('#/layouts/basic.vue'); +const AuthPageLayout = () => import('#/layouts/auth.vue'); /** 全局404页面 */ const fallbackNotFoundRoute: RouteRecordRaw = { component: () => import('#/views/_core/fallback/not-found.vue'), @@ -50,7 +50,7 @@ const coreRoutes: RouteRecordRaw[] = [ { name: 'Login', path: 'login', - component: Login, + component: () => import('#/views/_core/authentication/login.vue'), meta: { title: $t('page.auth.login'), }, diff --git a/apps/web-antd/src/utils/TimeUtils.ts b/apps/web-antd/src/utils/TimeUtils.ts new file mode 100644 index 00000000..3d3ab455 --- /dev/null +++ b/apps/web-antd/src/utils/TimeUtils.ts @@ -0,0 +1,46 @@ +import dayjs from 'dayjs'; + +/** 时间段选择器拓展 */ +export const rangePickerExtend = () => { + return { + showTime: { + format: 'HH:mm:ss', + defaultValue: [ + dayjs('00:00:00', 'HH:mm:ss'), + dayjs('23:59:59', 'HH:mm:ss'), + ], + }, + // 如果需要10位时间戳(秒级)可以使用 valueFormat: 'X' + valueFormat: 'YYYY-MM-DD HH:mm:ss', + format: 'YYYY-MM-DD HH:mm:ss', // 显示格式 + placeholder: ['开始时间', '结束时间'], + ranges: { + 今天: [dayjs().startOf('day'), dayjs().endOf('day')], + + 昨天: [ + dayjs().subtract(1, 'day').startOf('day'), + dayjs().subtract(1, 'day').endOf('day'), + ], + + 本周: [dayjs().startOf('week'), dayjs().endOf('day')], + + 本月: [dayjs().startOf('month'), dayjs().endOf('day')], + + 最近7天: [ + dayjs().subtract(7, 'day').startOf('day'), + dayjs().endOf('day'), + ], + + 最近30天: [ + dayjs().subtract(30, 'day').startOf('day'), + dayjs().endOf('day'), + ], + }, + transformDateFunc: (dates: any) => { + if (dates && dates.length === 2) { + return [dates.createTime[0], dates.createTime[1]].join(','); // 格式化为后台支持的时间格式 + } + return {}; + }, + }; +}; diff --git a/apps/web-ele/src/adapter/component/index.ts b/apps/web-ele/src/adapter/component/index.ts index 5de17b75..cf7340a2 100644 --- a/apps/web-ele/src/adapter/component/index.ts +++ b/apps/web-ele/src/adapter/component/index.ts @@ -8,31 +8,121 @@ import type { Component } from 'vue'; import type { BaseFormComponentType } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; -import { defineComponent, getCurrentInstance, h, ref } from 'vue'; +import { + defineAsyncComponent, + defineComponent, + getCurrentInstance, + h, + ref, +} from 'vue'; import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import { $t } from '@vben/locales'; -import { - ElButton, - ElCheckbox, - ElCheckboxButton, - ElCheckboxGroup, - ElDatePicker, - ElDivider, - ElInput, - ElInputNumber, - ElNotification, - ElRadio, - ElRadioButton, - ElRadioGroup, - ElSelectV2, - ElSpace, - ElSwitch, - ElTimePicker, - ElTreeSelect, - ElUpload, -} from 'element-plus'; +import { ElNotification } from 'element-plus'; + +const ElButton = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/button/index'), + import('element-plus/es/components/button/style/css'), + ]).then(([res]) => res.ElButton), +); +const ElCheckbox = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/checkbox/index'), + import('element-plus/es/components/checkbox/style/css'), + ]).then(([res]) => res.ElCheckbox), +); +const ElCheckboxButton = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/checkbox/index'), + import('element-plus/es/components/checkbox-button/style/css'), + ]).then(([res]) => res.ElCheckboxButton), +); +const ElCheckboxGroup = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/checkbox/index'), + import('element-plus/es/components/checkbox-group/style/css'), + ]).then(([res]) => res.ElCheckboxGroup), +); +const ElDatePicker = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/date-picker/index'), + import('element-plus/es/components/date-picker/style/css'), + ]).then(([res]) => res.ElDatePicker), +); +const ElDivider = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/divider/index'), + import('element-plus/es/components/divider/style/css'), + ]).then(([res]) => res.ElDivider), +); +const ElInput = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/input/index'), + import('element-plus/es/components/input/style/css'), + ]).then(([res]) => res.ElInput), +); +const ElInputNumber = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/input-number/index'), + import('element-plus/es/components/input-number/style/css'), + ]).then(([res]) => res.ElInputNumber), +); +const ElRadio = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/radio/index'), + import('element-plus/es/components/radio/style/css'), + ]).then(([res]) => res.ElRadio), +); +const ElRadioButton = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/radio/index'), + import('element-plus/es/components/radio-button/style/css'), + ]).then(([res]) => res.ElRadioButton), +); +const ElRadioGroup = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/radio/index'), + import('element-plus/es/components/radio-group/style/css'), + ]).then(([res]) => res.ElRadioGroup), +); +const ElSelectV2 = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/select-v2/index'), + import('element-plus/es/components/select-v2/style/css'), + ]).then(([res]) => res.ElSelectV2), +); +const ElSpace = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/space/index'), + import('element-plus/es/components/space/style/css'), + ]).then(([res]) => res.ElSpace), +); +const ElSwitch = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/switch/index'), + import('element-plus/es/components/switch/style/css'), + ]).then(([res]) => res.ElSwitch), +); +const ElTimePicker = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/time-picker/index'), + import('element-plus/es/components/time-picker/style/css'), + ]).then(([res]) => res.ElTimePicker), +); +const ElTreeSelect = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/tree-select/index'), + import('element-plus/es/components/tree-select/style/css'), + ]).then(([res]) => res.ElTreeSelect), +); +const ElUpload = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/upload/index'), + import('element-plus/es/components/upload/style/css'), + ]).then(([res]) => res.ElUpload), +); const withDefaultPlaceholder = ( component: T, diff --git a/apps/web-ele/src/bootstrap.ts b/apps/web-ele/src/bootstrap.ts index 7e9e1cd4..be054f80 100644 --- a/apps/web-ele/src/bootstrap.ts +++ b/apps/web-ele/src/bootstrap.ts @@ -1,8 +1,7 @@ import { createApp, watchEffect } from 'vue'; import { registerAccessDirective } from '@vben/access'; -import { initTippy, registerLoadingDirective } from '@vben/common-ui'; -import { MotionPlugin } from '@vben/plugins/motion'; +import { registerLoadingDirective } from '@vben/common-ui'; import { preferences } from '@vben/preferences'; import { initStores } from '@vben/stores'; import '@vben/styles'; @@ -49,12 +48,14 @@ async function bootstrap(namespace: string) { registerAccessDirective(app); // 初始化 tippy + const { initTippy } = await import('@vben/common-ui/es/tippy'); initTippy(app); // 配置路由及路由守卫 app.use(router); // 配置Motion插件 + const { MotionPlugin } = await import('@vben/plugins/motion'); app.use(MotionPlugin); // 动态更新标题 diff --git a/apps/web-ele/src/router/routes/core.ts b/apps/web-ele/src/router/routes/core.ts index 7218da22..4a527a31 100644 --- a/apps/web-ele/src/router/routes/core.ts +++ b/apps/web-ele/src/router/routes/core.ts @@ -2,10 +2,10 @@ import type { RouteRecordRaw } from 'vue-router'; import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; -import { AuthPageLayout, BasicLayout } from '#/layouts'; import { $t } from '#/locales'; -import Login from '#/views/_core/authentication/login.vue'; +const BasicLayout = () => import('#/layouts/basic.vue'); +const AuthPageLayout = () => import('#/layouts/auth.vue'); /** 全局404页面 */ const fallbackNotFoundRoute: RouteRecordRaw = { component: () => import('#/views/_core/fallback/not-found.vue'), @@ -50,7 +50,7 @@ const coreRoutes: RouteRecordRaw[] = [ { name: 'Login', path: 'login', - component: Login, + component: () => import('#/views/_core/authentication/login.vue'), meta: { title: $t('page.auth.login'), }, diff --git a/apps/web-ele/src/views/demos/form/basic.vue b/apps/web-ele/src/views/demos/form/basic.vue index 771665a6..0ecab586 100644 --- a/apps/web-ele/src/views/demos/form/basic.vue +++ b/apps/web-ele/src/views/demos/form/basic.vue @@ -1,7 +1,7 @@ diff --git a/docs/src/demos/vben-alert/confirm/index.vue b/docs/src/demos/vben-alert/confirm/index.vue index 4dba6308..723445d9 100644 --- a/docs/src/demos/vben-alert/confirm/index.vue +++ b/docs/src/demos/vben-alert/confirm/index.vue @@ -1,6 +1,10 @@ diff --git a/packages/@core/preferences/src/preferences.ts b/packages/@core/preferences/src/preferences.ts index 5c958267..23c5f8be 100644 --- a/packages/@core/preferences/src/preferences.ts +++ b/packages/@core/preferences/src/preferences.ts @@ -198,9 +198,16 @@ class PreferenceManager { window .matchMedia('(prefers-color-scheme: dark)') .addEventListener('change', ({ matches: isDark }) => { - this.updatePreferences({ - theme: { mode: isDark ? 'dark' : 'light' }, - }); + // 如果偏好设置中主题模式为auto,则跟随系统更新 + if (this.state.theme.mode === 'auto') { + this.updatePreferences({ + theme: { mode: isDark ? 'dark' : 'light' }, + }); + // 恢复为auto模式 + this.updatePreferences({ + theme: { mode: 'auto' }, + }); + } }); } diff --git a/packages/@core/ui-kit/menu-ui/src/components/menu.vue b/packages/@core/ui-kit/menu-ui/src/components/menu.vue index f570aa72..7a737008 100644 --- a/packages/@core/ui-kit/menu-ui/src/components/menu.vue +++ b/packages/@core/ui-kit/menu-ui/src/components/menu.vue @@ -23,7 +23,6 @@ import { import { useNamespace } from '@vben-core/composables'; import { Ellipsis } from '@vben-core/icons'; -import { isHttpUrl } from '@vben-core/shared/utils'; import { useResizeObserver } from '@vueuse/core'; @@ -248,9 +247,6 @@ function handleMenuItemClick(data: MenuItemClicked) { if (!path || !parentPaths) { return; } - if (!isHttpUrl(path)) { - activePath.value = path; - } emit('select', path, parentPaths); } diff --git a/packages/@core/ui-kit/menu-ui/src/components/sub-menu.vue b/packages/@core/ui-kit/menu-ui/src/components/sub-menu.vue index 17bc1215..7a82e724 100644 --- a/packages/@core/ui-kit/menu-ui/src/components/sub-menu.vue +++ b/packages/@core/ui-kit/menu-ui/src/components/sub-menu.vue @@ -208,6 +208,8 @@ onBeforeUnmount(() => { nsMenu.e('popup-container'), is(rootMenu.theme, true), opened ? '' : 'hidden', + 'overflow-auto', + 'max-h-[calc(var(--radix-hover-card-content-available-height)-20px)]', ]" :content-props="contentProps" :open="true" diff --git a/packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts b/packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts index d022d1cd..20e4254c 100644 --- a/packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts +++ b/packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts @@ -1,10 +1,10 @@ -import type { Component } from 'vue'; +import type { Component, VNode } from 'vue'; import type { Recordable } from '@vben-core/typings'; -import type { AlertProps, BeforeCloseScope } from './alert'; +import type { AlertProps, BeforeCloseScope, PromptProps } from './alert'; -import { h, ref, render } from 'vue'; +import { h, nextTick, ref, render } from 'vue'; import { useSimpleLocale } from '@vben-core/composables'; import { Input } from '@vben-core/shadcn-ui'; @@ -130,40 +130,58 @@ export function vbenConfirm( } export async function vbenPrompt( - options: Omit & { - beforeClose?: (scope: { - isConfirm: boolean; - value: T | undefined; - }) => boolean | Promise | undefined; - component?: Component; - componentProps?: Recordable; - defaultValue?: T; - modelPropName?: string; - }, + options: PromptProps, ): Promise { const { component: _component, componentProps: _componentProps, + componentSlots, content, defaultValue, modelPropName: _modelPropName, ...delegated } = options; - const contents: Component[] = []; + const modelValue = ref(defaultValue); + const inputComponentRef = ref(null); + const staticContents: Component[] = []; + if (isString(content)) { - contents.push(h('span', content)); - } else { - contents.push(content); + staticContents.push(h('span', content)); + } else if (content) { + staticContents.push(content as Component); } - const componentProps = _componentProps || {}; + const modelPropName = _modelPropName || 'modelValue'; - componentProps[modelPropName] = modelValue.value; - componentProps[`onUpdate:${modelPropName}`] = (val: any) => { - modelValue.value = val; + const componentProps = { ..._componentProps }; + + // 每次渲染时都会重新计算的内容函数 + const contentRenderer = () => { + const currentProps = { ...componentProps }; + + // 设置当前值 + currentProps[modelPropName] = modelValue.value; + + // 设置更新处理函数 + currentProps[`onUpdate:${modelPropName}`] = (val: T) => { + modelValue.value = val; + }; + + // 创建输入组件 + inputComponentRef.value = h( + _component || Input, + currentProps, + componentSlots, + ); + + // 返回包含静态内容和输入组件的数组 + return h( + 'div', + { class: 'flex flex-col gap-2' }, + { default: () => [...staticContents, inputComponentRef.value] }, + ); }; - const componentRef = h(_component || Input, componentProps); - contents.push(componentRef); + const props: AlertProps & Recordable = { ...delegated, async beforeClose(scope: BeforeCloseScope) { @@ -174,23 +192,46 @@ export async function vbenPrompt( }); } }, - content: h( - 'div', - { class: 'flex flex-col gap-2' }, - { default: () => contents }, - ), - onOpened() { - // 组件挂载完成后,自动聚焦到输入组件 - if ( - componentRef.component?.exposed && - isFunction(componentRef.component.exposed.focus) - ) { - componentRef.component.exposed.focus(); - } else if (componentRef.el && isFunction(componentRef.el.focus)) { - componentRef.el.focus(); + // 使用函数形式,每次渲染都会重新计算内容 + content: contentRenderer, + contentMasking: true, + async onOpened() { + await nextTick(); + const componentRef: null | VNode = inputComponentRef.value; + if (componentRef) { + if ( + componentRef.component?.exposed && + isFunction(componentRef.component.exposed.focus) + ) { + componentRef.component.exposed.focus(); + } else { + if (componentRef.el) { + if ( + isFunction(componentRef.el.focus) && + ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'].includes( + componentRef.el.tagName, + ) + ) { + componentRef.el.focus(); + } else if (isFunction(componentRef.el.querySelector)) { + const focusableElement = componentRef.el.querySelector( + 'input, select, textarea, button', + ); + if (focusableElement && isFunction(focusableElement.focus)) { + focusableElement.focus(); + } + } else if ( + componentRef.el.nextElementSibling && + isFunction(componentRef.el.nextElementSibling.focus) + ) { + componentRef.el.nextElementSibling.focus(); + } + } + } } }, }; + await vbenConfirm(props); return modelValue.value; } diff --git a/packages/@core/ui-kit/popup-ui/src/alert/alert.ts b/packages/@core/ui-kit/popup-ui/src/alert/alert.ts index 6a574daa..73d832df 100644 --- a/packages/@core/ui-kit/popup-ui/src/alert/alert.ts +++ b/packages/@core/ui-kit/popup-ui/src/alert/alert.ts @@ -1,4 +1,6 @@ -import type { Component } from 'vue'; +import type { Component, VNode, VNodeArrayChildren } from 'vue'; + +import type { Recordable } from '@vben-core/typings'; export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning'; @@ -13,6 +15,11 @@ export type AlertProps = { ) => boolean | Promise | undefined; /** 边框 */ bordered?: boolean; + /** + * 按钮对齐方式 + * @default 'end' + */ + buttonAlign?: 'center' | 'end' | 'start'; /** 取消按钮的标题 */ cancelText?: string; /** 是否居中显示 */ @@ -25,10 +32,41 @@ export type AlertProps = { content: Component | string; /** 弹窗内容的额外样式 */ contentClass?: string; + /** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/ + contentMasking?: boolean; + /** 弹窗底部内容(与按钮在同一个容器中) */ + footer?: Component | string; /** 弹窗的图标(在标题的前面) */ icon?: Component | IconType; + /** + * 弹窗遮罩模糊效果 + */ + overlayBlur?: number; /** 是否显示取消按钮 */ showCancel?: boolean; /** 弹窗标题 */ title?: string; }; + +/** Prompt属性 */ +export type PromptProps = { + /** 关闭前的回调,如果返回false,则终止关闭 */ + beforeClose?: (scope: { + isConfirm: boolean; + value: T | undefined; + }) => boolean | Promise | undefined; + /** 用于接受用户输入的组件 */ + component?: Component; + /** 输入组件的属性 */ + componentProps?: Recordable; + /** 输入组件的插槽 */ + componentSlots?: + | (() => any) + | Recordable + | VNode + | VNodeArrayChildren; + /** 默认值 */ + defaultValue?: T; + /** 输入组件的值属性名 */ + modelPropName?: string; +} & Omit; diff --git a/packages/@core/ui-kit/popup-ui/src/alert/alert.vue b/packages/@core/ui-kit/popup-ui/src/alert/alert.vue index 2cd334b7..135b0278 100644 --- a/packages/@core/ui-kit/popup-ui/src/alert/alert.vue +++ b/packages/@core/ui-kit/popup-ui/src/alert/alert.vue @@ -3,7 +3,7 @@ import type { Component } from 'vue'; import type { AlertProps } from './alert'; -import { computed, h, nextTick, ref, watch } from 'vue'; +import { computed, h, nextTick, ref } from 'vue'; import { useSimpleLocale } from '@vben-core/composables'; import { @@ -30,6 +30,7 @@ import { cn } from '@vben-core/shared/utils'; const props = withDefaults(defineProps(), { bordered: true, + buttonAlign: 'end', centered: true, containerClass: 'w-[520px]', }); @@ -38,14 +39,12 @@ const open = defineModel('open', { default: false }); const { $t } = useSimpleLocale(); const components = globalShareState.getComponents(); const isConfirm = ref(false); -watch(open, async (val) => { - await nextTick(); - if (val) { - isConfirm.value = false; - } else { - emits('closed', isConfirm.value); - } -}); + +function onAlertClosed() { + emits('closed', isConfirm.value); + isConfirm.value = false; +} + const getIconRender = computed(() => { let iconRender: Component | null = null; if (props.icon) { @@ -99,6 +98,7 @@ function handleCancel() { const loading = ref(false); async function handleOpenChange(val: boolean) { + await nextTick(); if (!val && props.beforeClose) { loading.value = true; try { @@ -119,15 +119,16 @@ async function handleOpenChange(val: boolean) { {{ $t(title) }} - + - + -
- +
+ + {{ cancelText || $t('cancel') }} - + import type { ExtendedModalApi, ModalProps } from './modal'; -import { computed, nextTick, provide, ref, useId, watch } from 'vue'; +import { computed, nextTick, provide, ref, unref, useId, watch } from 'vue'; import { useIsMobile, @@ -34,6 +34,7 @@ interface Props extends ModalProps { const props = withDefaults(defineProps(), { appendToMain: false, + destroyOnClose: true, modalApi: undefined, }); @@ -67,6 +68,7 @@ const { confirmText, contentClass, description, + destroyOnClose, draggable, footer: showFooter, footerClass, @@ -100,10 +102,15 @@ const { dragging, transform } = useModalDraggable( shouldDraggable, ); +const firstOpened = ref(false); +const isClosed = ref(false); + watch( () => state?.value?.isOpen, async (v) => { if (v) { + isClosed.value = false; + if (!firstOpened.value) firstOpened.value = true; await nextTick(); if (!contentRef.value) return; const innerContentRef = contentRef.value.getContentRef(); @@ -113,6 +120,7 @@ watch( dialogRef.value.style.transform = `translate(${offsetX}px, ${offsetY}px)`; } }, + { immediate: true }, ); watch( @@ -176,6 +184,15 @@ const getAppendTo = computed(() => { ? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div` : undefined; }); + +const getForceMount = computed(() => { + return !unref(destroyOnClose); +}); + +function handleClosed() { + isClosed.value = true; + props.modalApi?.onClosed(); +} diff --git a/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogContent.vue b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogContent.vue index b14dc0ec..c3d5c143 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogContent.vue +++ b/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogContent.vue @@ -61,7 +61,7 @@ defineExpose({