diff --git a/.vscode/settings.json b/.vscode/settings.json index b074051d..bb2590be 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -224,10 +224,20 @@ "commentTranslate.multiLineMerge": true, "vue.server.hybridMode": true, "vitest.disableWorkspaceWarning": true, - "cSpell.words": ["tinymce", "vditor"], "typescript.tsdk": "node_modules/typescript/lib", "editor.linkedEditing": true, // 自动同步更改html标签, "vscodeCustomCodeColor.highlightValue": "v-access", // v-access显示的颜色 "vscodeCustomCodeColor.highlightValueColor": "#CCFFFF", - "oxc.enable": false + "oxc.enable": false, + "cSpell.words": [ + "archiver", + "axios", + "dotenv", + "isequal", + "jspm", + "napi", + "nolebase", + "rollup", + "vitest" + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eb3fe1b..8fb33f60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# 1.3.1 + +**REFACTOR** + +- 所有Modal/Drawer表单关闭前会进行表单数据对比来弹出提示框 +- 字典项颜色选择从`原生input type=color`改为`vue3-colorpicker`组件 +- 全局Header: ClientID 更改大小写 [spring的问题导致](https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC0BDS) + +**BUG FIX** + +- getVxePopupContainer逻辑调整 解决表格固定高度展开不全的问题 + +**FEATURES** + +- 字典渲染支持loading(length为0情况) + +**OTHERS** + +- useForm的组件改为异步导入(官方更新) bootstrap.js体积从2M降到600K 首屏加载速度提升 + # 1.3.0 注意: 如果你使用老版本的`文件上传`/`图片上传` 可暂时使用 diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index 2f08835f..5bd96e7c 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -54,7 +54,8 @@ "tinymce": "^7.3.0", "unplugin-vue-components": "^0.27.3", "vue": "catalog:", - "vue-router": "catalog:" + "vue-router": "catalog:", + "vue3-colorpicker": "^2.3.0" }, "devDependencies": { "@types/crypto-js": "^4.2.2", diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index 628f1a62..ca59e2e5 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/apps/web-antd/src/adapter/component/index.ts @@ -8,40 +8,80 @@ import type { Component } from 'vue'; import type { BaseFormComponentType } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; -import { computed, defineComponent, getCurrentInstance, h, ref } from 'vue'; +import { + computed, + 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'; -import { Tinymce as RichTextarea } from '#/components/tinymce'; -import { FileUpload, ImageUpload } from '#/components/upload'; import { FileUploadOld, ImageUploadOld } from '#/components/upload-old'; +const RichTextarea = defineAsyncComponent(() => + import('#/components/tinymce/index').then((res) => res.Tinymce), +); + +const FileUpload = defineAsyncComponent(() => + import('#/components/upload').then((res) => res.FileUpload), +); + +const ImageUpload = defineAsyncComponent(() => + import('#/components/upload').then((res) => res.ImageUpload), +); + +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, type: 'input' | 'select', diff --git a/apps/web-antd/src/api/request.ts b/apps/web-antd/src/api/request.ts index 3b8b382b..08d9a8f7 100644 --- a/apps/web-antd/src/api/request.ts +++ b/apps/web-antd/src/api/request.ts @@ -93,9 +93,12 @@ function createRequestClient(baseURL: string) { const language = preferences.app.locale.replace('-', '_'); config.headers['Accept-Language'] = language; config.headers['Content-Language'] = language; - // 添加全局clientId - config.headers.clientId = clientId; - + /** + * 添加全局clientId + * 关于header的clientId被错误绑定到实体类 + * https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC0BDS + */ + config.headers.ClientID = clientId; /** * 格式化get/delete参数 * 如果包含自定义的paramsSerializer则不走此逻辑 diff --git a/apps/web-antd/src/bootstrap.ts b/apps/web-antd/src/bootstrap.ts index 35b9924b..885e1e32 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'; @@ -50,12 +49,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/components/dict/src/index.vue b/apps/web-antd/src/components/dict/src/index.vue index 6c15c7f4..1edd1274 100644 --- a/apps/web-antd/src/components/dict/src/index.vue +++ b/apps/web-antd/src/components/dict/src/index.vue @@ -4,7 +4,7 @@ import type { DictData } from '#/api/system/dict/dict-data-model'; import { computed } from 'vue'; -import { Tag } from 'ant-design-vue'; +import { Spin, Tag } from 'ant-design-vue'; import { tagTypes } from './data'; @@ -41,12 +41,22 @@ const label = computed(() => { }); const tagComponent = computed(() => (color.value ? Tag : 'div')); + +const loading = computed(() => { + return props.dicts?.length === 0; +}); diff --git a/apps/web-antd/src/components/upload/src/hook.ts b/apps/web-antd/src/components/upload/src/hook.ts index 313ceae4..a45cd858 100644 --- a/apps/web-antd/src/components/upload/src/hook.ts +++ b/apps/web-antd/src/components/upload/src/hook.ts @@ -358,8 +358,7 @@ export function useUpload( ); } }, - // TODO: deepWatch参数需要删除 - { immediate: true, deep: props.deepWatch }, + { immediate: true }, ); return { diff --git a/apps/web-antd/src/components/upload/src/props.d.ts b/apps/web-antd/src/components/upload/src/props.d.ts index 11e289fc..3aa324c1 100644 --- a/apps/web-antd/src/components/upload/src/props.d.ts +++ b/apps/web-antd/src/components/upload/src/props.d.ts @@ -87,13 +87,6 @@ export interface BaseUploadProps { * @default false */ enableDragUpload?: boolean; - /** - * 是否开启深度监听 - * 默认外部的数组地址重新改变才会触发watch 不会监听内部元素的变化 - * 开启后 无论内部还是外部改变都会触发查询信息接口(包括上传后, 删除等操作都会触发) - * @default false - */ - deepWatch?: boolean; /** * 当ossId查询不到文件信息时 比如被删除了 * 是否保留列表对应的ossId 默认不保留 diff --git a/apps/web-antd/src/locales/langs/en-US/pages.json b/apps/web-antd/src/locales/langs/en-US/pages.json index 0da592ff..901a75a1 100644 --- a/apps/web-antd/src/locales/langs/en-US/pages.json +++ b/apps/web-antd/src/locales/langs/en-US/pages.json @@ -21,6 +21,7 @@ "preview": "Preview", "tip": "Tip", "enable": "On", - "disable": "Off" + "disable": "Off", + "beforeCloseTip": "You have unsaved changes. Are you sure you want to exit?" } } diff --git a/apps/web-antd/src/locales/langs/zh-CN/pages.json b/apps/web-antd/src/locales/langs/zh-CN/pages.json index 43066862..eab87798 100644 --- a/apps/web-antd/src/locales/langs/zh-CN/pages.json +++ b/apps/web-antd/src/locales/langs/zh-CN/pages.json @@ -21,6 +21,7 @@ "preview": "预览", "tip": "提示", "enable": "启用", - "disable": "禁用" + "disable": "禁用", + "beforeCloseTip": "您有未保存的更改,确认要退出吗?" } } diff --git a/apps/web-antd/src/router/routes/core.ts b/apps/web-antd/src/router/routes/core.ts index 4ecb68f8..2666cc68 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'), @@ -58,7 +58,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/store/dict.ts b/apps/web-antd/src/store/dict.ts index f47f944b..c24f4c3c 100644 --- a/apps/web-antd/src/store/dict.ts +++ b/apps/web-antd/src/store/dict.ts @@ -59,6 +59,7 @@ export const useDictStore = defineStore('app-dict', () => { } function resetCache() { + dictRequestCache.clear(); dictOptionsMap.clear(); /** * 不需要清空dictRequestCache 每次请求成功/失败都清空key diff --git a/apps/web-antd/src/utils/popup.ts b/apps/web-antd/src/utils/popup.ts index 0fa2e2e4..bfe71a48 100644 --- a/apps/web-antd/src/utils/popup.ts +++ b/apps/web-antd/src/utils/popup.ts @@ -29,43 +29,52 @@ interface BeforeCloseDiffProps { } /** - * @deprecated 注意为实验性功能 可能有api变动/被移除 + * 用于Drawer/Modal使用 判断表单是否有变动来决定是否弹窗提示 * @param props props * @returns hook - * - * 待解决问题: 网速慢情况直接关闭 会导致数据不一致问题 - * 但是使用api.lock会导致在报错情况无法关闭(因为目前代码没有finally) */ export function useBeforeCloseDiff(props: BeforeCloseDiffProps) { const { initializedGetter, currentGetter, compare } = props; + /** + * 记录初始值 json + */ const initialized = ref(''); + /** + * 是否已经初始化了 通过这个值判断是否需要进行对比 为false直接关闭 不弹窗 + */ const isInitialized = ref(false); - const isSubmitted = ref(false); - async function updateInitialized(data?: string) { + /** + * 标记是否已经完成初始化 后续需要进行对比 + * @param data 自定义初始化数据 可选 + */ + async function markInitialized(data?: string) { initialized.value = data || (await initializedGetter()); isInitialized.value = true; } - function setSubmitted() { - isSubmitted.value = true; + /** + * 重置初始化状态 需要在closed前调用 或者打开窗口时 + */ + function resetInitialized() { + initialized.value = ''; + isInitialized.value = false; } + /** + * 提供给useVbenForm/useVbenDrawer使用 + * @returns 是否允许关闭 + */ async function onBeforeClose(): Promise { // 如果还未初始化,直接允许关闭 if (!isInitialized.value) { return true; } - // 如果已经提交过,直接允许关闭 - if (isSubmitted.value) { - // 重置状态 - isSubmitted.value = false; - return true; - } try { + // 获取当前表单数据 const current = await currentGetter(); - + // 自定义比较的情况 if (isFunction(compare) && compare(initialized.value, current)) { return true; } else { @@ -79,7 +88,7 @@ export function useBeforeCloseDiff(props: BeforeCloseDiffProps) { return new Promise((resolve) => { Modal.confirm({ title: $t('pages.common.tip'), - content: $t('您有未保存的更改,确认要退出吗?'), + content: $t('pages.common.beforeCloseTip'), centered: true, okButtonProps: { danger: true }, cancelText: $t('common.cancel'), @@ -99,8 +108,8 @@ export function useBeforeCloseDiff(props: BeforeCloseDiffProps) { return { onBeforeClose, - updateInitialized, - setSubmitted, + markInitialized, + resetInitialized, }; } diff --git a/apps/web-antd/src/views/system/client/client-drawer.vue b/apps/web-antd/src/views/system/client/client-drawer.vue index bcbed4ec..e67cb46c 100644 --- a/apps/web-antd/src/views/system/client/client-drawer.vue +++ b/apps/web-antd/src/views/system/client/client-drawer.vue @@ -7,6 +7,7 @@ import { cloneDeep } from '@vben/utils'; import { useVbenForm } from '#/adapter/form'; import { clientAdd, clientInfo, clientUpdate } from '#/api/system/client'; +import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup'; import { drawerSchema } from './data'; import SecretInput from './secret-input.vue'; @@ -55,6 +56,13 @@ function setupForm(update: boolean) { ]); } +const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff( + { + initializedGetter: defaultFormValueGetter(formApi), + currentGetter: defaultFormValueGetter(formApi), + }, +); + // 提取生成状态字段Schema的函数 const getStatusSchema = (disabled: boolean) => [ { @@ -64,13 +72,15 @@ const getStatusSchema = (disabled: boolean) => [ ]; const [BasicDrawer, drawerApi] = useVbenDrawer({ - onCancel: handleCancel, + onBeforeClose, + onClosed: handleClosed, onConfirm: handleConfirm, async onOpenChange(isOpen) { if (!isOpen) { return null; } drawerApi.drawerLoading(true); + const { id } = drawerApi.getData() as { id?: number | string }; isUpdate.value = !!id; // 初始化 @@ -84,36 +94,39 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({ // 新增模式: 确保状态字段可用 formApi.updateSchema(getStatusSchema(false)); } + await markInitialized(); + drawerApi.drawerLoading(false); }, }); async function handleConfirm() { try { - drawerApi.drawerLoading(true); + drawerApi.lock(true); const { valid } = await formApi.validate(); if (!valid) { return; } const data = cloneDeep(await formApi.getValues()); await (isUpdate.value ? clientUpdate(data) : clientAdd(data)); + resetInitialized(); emit('reload'); - await handleCancel(); + drawerApi.close(); } catch (error) { console.error(error); } finally { - drawerApi.drawerLoading(false); + drawerApi.lock(false); } } -async function handleCancel() { - drawerApi.close(); +async function handleClosed() { await formApi.resetForm(); + resetInitialized(); }