diff --git a/.lintstagedrc.mjs b/.lintstagedrc.mjs index e68d8a3e..94b0192a 100644 --- a/.lintstagedrc.mjs +++ b/.lintstagedrc.mjs @@ -1,4 +1,10 @@ export default { + '*.md': ['prettier --cache --ignore-unknown --write'], + '*.vue': [ + 'prettier --write', + 'eslint --cache --fix', + 'stylelint --fix --allow-empty-input', + ], '*.{js,jsx,ts,tsx}': [ 'prettier --cache --ignore-unknown --write', 'eslint --cache --fix', @@ -7,14 +13,8 @@ export default { 'prettier --cache --ignore-unknown --write', 'stylelint --fix --allow-empty-input', ], - '*.md': ['prettier --cache --ignore-unknown --write'], - '*.vue': [ - 'prettier --write', - 'eslint --cache --fix', - 'stylelint --fix --allow-empty-input', - ], + 'package.json': ['prettier --cache --write'], '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [ 'prettier --cache --write--parser json', ], - 'package.json': ['prettier --cache --write'], }; diff --git a/.vscode/launch.json b/.vscode/launch.json index 7740e810..e9673304 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "url": "http://localhost:5555", "env": { "NODE_ENV": "development" }, "sourceMaps": true, - "webRoot": "${workspaceFolder}" + "webRoot": "${workspaceFolder}/playground" }, { "type": "chrome", @@ -18,7 +18,7 @@ "url": "http://localhost:5666", "env": { "NODE_ENV": "development" }, "sourceMaps": true, - "webRoot": "${workspaceFolder}" + "webRoot": "${workspaceFolder}/apps/web-antd" }, { "type": "chrome", @@ -27,7 +27,7 @@ "url": "http://localhost:5777", "env": { "NODE_ENV": "development" }, "sourceMaps": true, - "webRoot": "${workspaceFolder}" + "webRoot": "${workspaceFolder}/apps/web-ele" }, { "type": "chrome", @@ -36,7 +36,7 @@ "url": "http://localhost:5888", "env": { "NODE_ENV": "development" }, "sourceMaps": true, - "webRoot": "${workspaceFolder}" + "webRoot": "${workspaceFolder}/apps/web-naive" } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index a727cdf9..fc7c8f23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,22 @@ # 1.1.4 +**REFACTOR** + +- 菜单选择组件重构为Table形式 + **Features** - 通用的vxe-table排序事件(排序逻辑改为在排序事件中处理而非在api处理) +- getDict/getDictOptions 提取公共逻辑 减少冗余代码 + +**BUG FIXES** + +- 字典项为空时getDict方法无限调用接口((无奈兼容 不给字典item本来就是错误用法)) +- 表格排序翻页会丢失排序参数 + +**OTHERS** + +- 用户管理 新增只获取一次(mounted)默认密码而非每次打开modal都获取 # 1.1.3 diff --git a/apps/backend-mock/api/table/list.ts b/apps/backend-mock/api/table/list.ts index 4a0db94e..55b88eaa 100644 --- a/apps/backend-mock/api/table/list.ts +++ b/apps/backend-mock/api/table/list.ts @@ -43,6 +43,31 @@ export default eventHandler(async (event) => { await sleep(600); - const { page, pageSize } = getQuery(event); - return usePageResponseSuccess(page as string, pageSize as string, mockData); + const { page, pageSize, sortBy, sortOrder } = getQuery(event); + const listData = structuredClone(mockData); + if (sortBy && Reflect.has(listData[0], sortBy as string)) { + listData.sort((a, b) => { + if (sortOrder === 'asc') { + if (sortBy === 'price') { + return ( + Number.parseFloat(a[sortBy as string]) - + Number.parseFloat(b[sortBy as string]) + ); + } else { + return a[sortBy as string] > b[sortBy as string] ? 1 : -1; + } + } else { + if (sortBy === 'price') { + return ( + Number.parseFloat(b[sortBy as string]) - + Number.parseFloat(a[sortBy as string]) + ); + } else { + return a[sortBy as string] < b[sortBy as string] ? 1 : -1; + } + } + }); + } + + return usePageResponseSuccess(page as string, pageSize as string, listData); }); diff --git a/apps/backend-mock/utils/mock-data.ts b/apps/backend-mock/utils/mock-data.ts index 71970a28..057588e3 100644 --- a/apps/backend-mock/utils/mock-data.ts +++ b/apps/backend-mock/utils/mock-data.ts @@ -4,6 +4,7 @@ export interface UserInfo { realName: string; roles: string[]; username: string; + homePath?: string; } export const MOCK_USERS: UserInfo[] = [ @@ -20,6 +21,7 @@ export const MOCK_USERS: UserInfo[] = [ realName: 'Admin', roles: ['admin'], username: 'admin', + homePath: '/workspace', }, { id: 2, @@ -27,6 +29,7 @@ export const MOCK_USERS: UserInfo[] = [ realName: 'Jack', roles: ['user'], username: 'jack', + homePath: '/analytics', }, ]; diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index 370bcdf3..750c7936 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/apps/web-antd/src/adapter/component/index.ts @@ -4,13 +4,12 @@ */ import type { BaseFormComponentType } from '@vben/common-ui'; - import type { Component, SetupContext } from 'vue'; -import { h } from 'vue'; +import { Tinymce as RichTextarea } from '#/components/tinymce'; +import { FileUpload, ImageUpload } from '#/components/upload'; import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import { $t } from '@vben/locales'; - import { AutoComplete, Button, @@ -35,9 +34,7 @@ import { TreeSelect, Upload, } from 'ant-design-vue'; - -import { Tinymce as RichTextarea } from '#/components/tinymce'; -import { FileUpload, ImageUpload } from '#/components/upload'; +import { h } from 'vue'; const withDefaultPlaceholder = ( component: T, diff --git a/apps/web-antd/src/adapter/vxe-table.ts b/apps/web-antd/src/adapter/vxe-table.ts index 857751b3..9b3236d6 100644 --- a/apps/web-antd/src/adapter/vxe-table.ts +++ b/apps/web-antd/src/adapter/vxe-table.ts @@ -1,12 +1,9 @@ -import { h, type Ref } from 'vue'; - -import { - setupVbenVxeTable, - useVbenVxeGrid, - type VxeGridDefines, -} from '@vben/plugins/vxe-table'; +import type { VxeGridDefines, VxeGridPropTypes } from '@vben/plugins/vxe-table'; +import type { Ref } from 'vue'; +import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; import { Button, Image } from 'ant-design-vue'; +import { h } from 'vue'; import { useVbenForm } from './form'; @@ -133,6 +130,7 @@ export function vxeCheckboxChecked( /** * 通用的vxe-table排序事件 支持单/多字段排序 + * @deprecated 翻页后排序会丢失,使用addSortParams代替 * @param tableApi api * @param sortParams 排序参数 */ @@ -151,3 +149,23 @@ export function vxeSortEvent( const isAsc = sortList.map((item) => item.order).join(','); tableApi.query({ orderByColumn, isAsc }); } + +/** + * 通用的 排序参数添加到请求参数中 + * @param params 请求参数 + * @param sortList vxe-table的排序参数 + */ +export function addSortParams( + params: Record, + sortList: VxeGridPropTypes.ProxyAjaxQuerySortCheckedParams[], +) { + // 这里是排序取消 length为0 就不添加参数了 + if (sortList.length === 0) { + return; + } + // 支持单/多字段排序 + const orderByColumn = sortList.map((item) => item.field).join(','); + const isAsc = sortList.map((item) => item.order).join(','); + params.orderByColumn = orderByColumn; + params.isAsc = isAsc; +} diff --git a/apps/web-antd/src/api/monitor/online/index.ts b/apps/web-antd/src/api/monitor/online/index.ts index 4d8224a2..19ef27ed 100644 --- a/apps/web-antd/src/api/monitor/online/index.ts +++ b/apps/web-antd/src/api/monitor/online/index.ts @@ -36,5 +36,5 @@ export function forceLogout(tokenId: string) { * @returns void */ export function forceLogout2(tokenId: string) { - return requestClient.postWithMsg(`${Api.root}/${tokenId}`); + return requestClient.deleteWithMsg(`${Api.root}/myself/${tokenId}`); } diff --git a/apps/web-antd/src/api/system/menu/index.ts b/apps/web-antd/src/api/system/menu/index.ts index 65c3b4f9..e4f14c17 100644 --- a/apps/web-antd/src/api/system/menu/index.ts +++ b/apps/web-antd/src/api/system/menu/index.ts @@ -1,7 +1,7 @@ -import type { Menu, MenuOption, MenuResp } from './model'; - import type { ID, IDS } from '#/api/common'; +import type { Menu, MenuOption, MenuResp } from './model'; + import { requestClient } from '#/api/request'; enum Api { diff --git a/apps/web-antd/src/api/system/menu/model.d.ts b/apps/web-antd/src/api/system/menu/model.d.ts index 0234425f..0a988bc2 100644 --- a/apps/web-antd/src/api/system/menu/model.d.ts +++ b/apps/web-antd/src/api/system/menu/model.d.ts @@ -33,6 +33,8 @@ export interface MenuOption { weight: number; children: MenuOption[]; key: string; // 实际上不存在 ide报错 + menuType: string; + icon: string; } /** diff --git a/apps/web-antd/src/api/system/oss/index.ts b/apps/web-antd/src/api/system/oss/index.ts index 990c8e7c..ded2b422 100644 --- a/apps/web-antd/src/api/system/oss/index.ts +++ b/apps/web-antd/src/api/system/oss/index.ts @@ -1,7 +1,7 @@ -import type { OssFile } from './model'; - import type { ID, IDS, PageQuery, PageResult } from '#/api/common'; +import type { OssFile } from './model'; + import { ContentTypeEnum } from '#/api/helper'; import { requestClient } from '#/api/request'; diff --git a/apps/web-antd/src/components/tree/index.ts b/apps/web-antd/src/components/tree/index.ts index 3dfcd39a..f142790a 100644 --- a/apps/web-antd/src/components/tree/index.ts +++ b/apps/web-antd/src/components/tree/index.ts @@ -1 +1,2 @@ +export { default as MenuSelectTable } from './src/menu-select-table.vue'; export { default as TreeSelectPanel } from './src/tree-select-panel.vue'; diff --git a/apps/web-antd/src/components/tree/src/data.tsx b/apps/web-antd/src/components/tree/src/data.tsx new file mode 100644 index 00000000..2bbaf317 --- /dev/null +++ b/apps/web-antd/src/components/tree/src/data.tsx @@ -0,0 +1,85 @@ +import type { VxeGridProps } from '#/adapter/vxe-table'; +import type { ID } from '#/api/common'; +import type { MenuOption } from '#/api/system/menu/model'; + +import { h, markRaw } from 'vue'; + +import { FolderIcon, MenuIcon, OkButtonIcon, VbenIcon } from '@vben/icons'; + +export interface Permission { + checked: boolean; + id: ID; + label: string; +} + +export interface MenuPermissionOption extends MenuOption { + permissions: Permission[]; +} + +const menuTypes = { + C: { icon: markRaw(MenuIcon), value: '菜单' }, + F: { icon: markRaw(OkButtonIcon), value: '按钮' }, + M: { icon: markRaw(FolderIcon), value: '目录' }, +}; + +export const nodeOptions = [ + { label: '节点关联', value: true }, + { label: '节点独立', value: false }, +]; + +export const columns: VxeGridProps['columns'] = [ + { + type: 'checkbox', + title: '菜单名称', + field: 'label', + treeNode: true, + headerAlign: 'left', + align: 'left', + width: 230, + }, + { + title: '图标', + field: 'icon', + width: 80, + slots: { + default: ({ row }) => { + if (row?.icon === '#') { + return ''; + } + return ( + + + + ); + }, + }, + }, + { + title: '类型', + field: 'menuType', + width: 80, + slots: { + default: ({ row }) => { + const current = menuTypes[row.menuType as 'C' | 'F' | 'M']; + if (!current) { + return '未知'; + } + return ( + + {h(current.icon, { class: 'size-[18px]' })} + {current.value} + + ); + }, + }, + }, + { + title: '权限标识', + field: 'permissions', + headerAlign: 'left', + align: 'left', + slots: { + default: 'permissions', + }, + }, +]; diff --git a/apps/web-antd/src/components/tree/src/helper.tsx b/apps/web-antd/src/components/tree/src/helper.tsx new file mode 100644 index 00000000..9ce64a21 --- /dev/null +++ b/apps/web-antd/src/components/tree/src/helper.tsx @@ -0,0 +1,133 @@ +import type { MenuPermissionOption } from './data'; + +import type { useVbenVxeGrid } from '#/adapter/vxe-table'; +import type { MenuOption } from '#/api/system/menu/model'; + +import { eachTree, treeToList } from '@vben/utils'; + +import { difference, isEmpty, isUndefined } from 'lodash-es'; + +/** + * 权限列设置是否全选 + * @param record 行记录 + * @param checked 是否选中 + */ +export function setPermissionsChecked( + record: MenuPermissionOption, + checked: boolean, +) { + if (record?.permissions?.length > 0) { + // 全部设置为选中 + record.permissions.forEach((permission) => { + permission.checked = checked; + }); + } +} + +/** + * 设置当前行 & 所有子节点选中状态 + * @param record 行 + * @param checked 是否选中 + */ +export function rowAndChildrenChecked( + record: MenuPermissionOption, + checked: boolean, +) { + // 当前行选中 + setPermissionsChecked(record, checked); + // 所有子节点选中 + record?.children?.forEach?.((permission) => { + rowAndChildrenChecked(permission as MenuPermissionOption, checked); + }); +} + +/** + * void方法 会直接修改原始数据 + * 将树结构转为 tree+permissions结构 + * @param menus 后台返回的menu + */ +export function menusWithPermissions(menus: MenuOption[]) { + eachTree(menus, (item: MenuPermissionOption) => { + if (item.children && item.children.length > 0) { + /** + * 所有为按钮的节点提取出来 + * 需要注意 这里需要过滤目录下直接是按钮的情况item.menuType !== 'M' + * 将按钮往children添加而非加到permissions + */ + const permissions = item.children.filter( + (child: MenuOption) => child.menuType === 'F' && item.menuType !== 'M', + ); + // 取差集 + const diffCollection = difference(item.children, permissions); + // 更新后的children 即去除按钮 + item.children = diffCollection; + + // permissions作为字段添加到item + const permissionsArr = permissions.map((permission) => { + return { + id: permission.id, + label: permission.label, + checked: false, + }; + }); + item.permissions = permissionsArr; + } + }); +} + +/** + * 设置表格选中 + * @param checkedKeys 选中的keys + * @param menus 菜单 转换后的菜单 + * @param tableApi api + * @param association 是否节点关联 + */ +export function setTableChecked( + checkedKeys: (number | string)[], + menus: MenuPermissionOption[], + tableApi: ReturnType['1'], + association: boolean, +) { + // tree转list + const menuList: MenuPermissionOption[] = treeToList(menus); + // 拿到勾选的行数据 + let checkedRows = menuList.filter((item) => checkedKeys.includes(item.id)); + + /** + * 节点独立切换到节点关联 只需要最末尾的数据 即children为空 + */ + if (!association) { + checkedRows = checkedRows.filter( + (item) => isUndefined(item.children) || isEmpty(item.children), + ); + } + + // 设置行选中 & permissions选中 + checkedRows.forEach((item) => { + tableApi.grid.setCheckboxRow(item, true); + if (item?.permissions?.length > 0) { + item.permissions.forEach((permission) => { + if (checkedKeys.includes(permission.id)) { + permission.checked = true; + } + }); + } + }); + + /** + * 节点独立切换到节点关联 + * 勾选后还需要过滤权限没有任何勾选的情况 这时候取消行的勾选 + */ + if (!association) { + const emptyRows = checkedRows.filter((item) => { + if (isUndefined(item.permissions) || isEmpty(item.permissions)) { + return false; + } + return item.permissions.every( + (permission) => permission.checked === false, + ); + }); + // 设置为不选中 + tableApi.grid.setCheckboxRow(emptyRows, false); + } +} diff --git a/apps/web-antd/src/components/tree/src/hook.tsx b/apps/web-antd/src/components/tree/src/hook.tsx new file mode 100644 index 00000000..31b52bdd --- /dev/null +++ b/apps/web-antd/src/components/tree/src/hook.tsx @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { TourProps } from 'ant-design-vue'; + +import { defineComponent, ref } from 'vue'; + +import { useLocalStorage } from '@vueuse/core'; +import { Tour } from 'ant-design-vue'; + +/** + * 全屏引导 + * @returns value + */ +export function useFullScreenGuide() { + const open = ref(false); + /** + * 是否已读 只显示一次 + */ + const read = useLocalStorage('menu_select_fullscreen_read', false); + + function openGuide() { + if (!read.value) { + open.value = true; + } + } + + function closeGuide() { + open.value = false; + read.value = true; + } + + const steps: TourProps['steps'] = [ + { + title: '提示', + description: '点击这里可以全屏', + target: () => + document.querySelector( + 'div#menu-select-table .vxe-tools--operate > button[title="全屏"]', + )!, + }, + ]; + + const FullScreenGuide = defineComponent({ + name: 'FullScreenGuide', + inheritAttrs: false, + setup() { + return () => ( + + ); + }, + }); + + return { + FullScreenGuide, + openGuide, + closeGuide, + }; +} diff --git a/apps/web-antd/src/components/tree/src/menu-select-table.vue b/apps/web-antd/src/components/tree/src/menu-select-table.vue new file mode 100644 index 00000000..a8a570b6 --- /dev/null +++ b/apps/web-antd/src/components/tree/src/menu-select-table.vue @@ -0,0 +1,412 @@ + + + + + + diff --git a/apps/web-antd/src/components/tree/src/tree-select-panel.vue b/apps/web-antd/src/components/tree/src/tree-select-panel.vue index 9a2a7744..0e478084 100644 --- a/apps/web-antd/src/components/tree/src/tree-select-panel.vue +++ b/apps/web-antd/src/components/tree/src/tree-select-panel.vue @@ -3,7 +3,9 @@ import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface'; import type { DataNode } from 'ant-design-vue/es/tree'; import type { CheckInfo } from 'ant-design-vue/es/vc-tree/props'; -import { computed, nextTick, onMounted, type PropType, ref, watch } from 'vue'; +import type { PropType } from 'vue'; + +import { computed, nextTick, onMounted, ref, watch } from 'vue'; import { findGroupParentIds, treeToList } from '@vben/utils'; @@ -108,8 +110,8 @@ const stop = watch([checkedKeys, () => props.treeData], () => { * @param info info.halfCheckedKeys为父节点的ID */ type CheckedState = - | { checked: T[]; halfChecked: T[] } - | T[]; + | T[] + | { checked: T[]; halfChecked: T[] }; function handleChecked(checkedStateKeys: CheckedState, info: CheckInfo) { // 数组的话为节点关联 if (Array.isArray(checkedStateKeys)) { diff --git a/apps/web-antd/src/components/upload/src/image-upload.vue b/apps/web-antd/src/components/upload/src/image-upload.vue index 7548ea1b..a000e05b 100644 --- a/apps/web-antd/src/components/upload/src/image-upload.vue +++ b/apps/web-antd/src/components/upload/src/image-upload.vue @@ -2,15 +2,13 @@ import type { UploadFile, UploadProps } from 'ant-design-vue'; import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface'; -import { ref, toRefs, watch } from 'vue'; - -import { $t } from '@vben/locales'; - -import { PlusOutlined } from '@ant-design/icons-vue'; -import { message, Modal, Upload } from 'ant-design-vue'; -import { isArray, isFunction, isObject, isString } from 'lodash-es'; - import { uploadApi } from '#/api'; +import { ossInfo } from '#/api/system/oss'; +import { PlusOutlined } from '@ant-design/icons-vue'; +import { $t } from '@vben/locales'; +import { message, Modal, Upload } from 'ant-design-vue'; +import { isArray, isFunction, isObject, isString, uniqueId } from 'lodash-es'; +import { ref, toRefs, watch } from 'vue'; import { checkImageFileType, defaultImageAccept } from './helper'; import { UploadResultStatus } from './typing'; @@ -37,7 +35,7 @@ const props = withDefaults( multiple?: boolean; // support xxx.xxx.xx // 返回的字段 默认url - resultField?: 'fileName' | 'ossId' | 'url' | string; + resultField?: 'fileName' | 'ossId' | 'url'; value?: string | string[]; }>(), { @@ -50,7 +48,7 @@ const props = withDefaults( accept: () => defaultImageAccept, multiple: false, api: uploadApi, - resultField: '', + resultField: 'url', }, ); const emit = defineEmits(['change', 'update:value', 'delete']); @@ -74,7 +72,7 @@ const isFirstRender = ref(true); watch( () => props.value, - (v) => { + async (v) => { if (isInnerOperate.value) { isInnerOperate.value = false; return; @@ -90,19 +88,40 @@ watch( } // 直接赋值 可能为string | string[] value = v; - fileList.value = _fileList.map((item, i) => { - if (item && isString(item)) { - return { - uid: `${-i}`, - name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)), - status: 'done', - url: item, - }; - } else if (item && isObject(item)) { - return item; + const withUrlList: UploadProps['fileList'] = []; + for (const item of _fileList) { + // ossId情况 + if (props.resultField === 'ossId') { + const resp = await ossInfo([item]); + if (item && isString(item)) { + withUrlList.push({ + uid: item, // ossId作为uid 方便getValue获取 + name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)), + status: 'done', + url: resp?.[0]?.url, + }); + } else if (item && isObject(item)) { + withUrlList.push({ + ...(item as any), + uid: item, + url: resp?.[0]?.url, + }); + } + } else { + // 非ossId情况 + if (item && isString(item)) { + withUrlList.push({ + uid: uniqueId(), + name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)), + status: 'done', + url: item, + }); + } else if (item && isObject(item)) { + withUrlList.push(item); + } } - return null; - }) as UploadProps['fileList']; + } + fileList.value = withUrlList; } if (!isFirstRender.value) { emit('change', value); @@ -200,12 +219,17 @@ async function customRequest(info: UploadRequestOption) { } function getValue() { + console.log(fileList.value); const list = (fileList.value || []) .filter((item) => item?.status === UploadResultStatus.DONE) .map((item: any) => { if (item?.response && props?.resultField) { return item?.response?.[props.resultField]; } + // ossId兼容 uid为ossId直接返回 + if (props.resultField === 'ossId' && item.uid) { + return item.uid; + } // 适用于已经有图片 回显的情况 会默认在init处理为{url: 'xx'} if (item?.url) { return item.url; diff --git a/apps/web-antd/src/locales/index.ts b/apps/web-antd/src/locales/index.ts index 11fad689..ddb3022f 100644 --- a/apps/web-antd/src/locales/index.ts +++ b/apps/web-antd/src/locales/index.ts @@ -1,8 +1,6 @@ import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales'; import type { Locale } from 'ant-design-vue/es/locale'; - import type { App } from 'vue'; -import { ref } from 'vue'; import { $t, @@ -10,10 +8,10 @@ import { loadLocalesMapFromDir, } from '@vben/locales'; import { preferences } from '@vben/preferences'; - import antdEnLocale from 'ant-design-vue/es/locale/en_US'; import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN'; import dayjs from 'dayjs'; +import { ref } from 'vue'; const antdLocale = ref(antdDefaultLocale); diff --git a/apps/web-antd/src/router/guard.ts b/apps/web-antd/src/router/guard.ts index fce5a892..cbb5235e 100644 --- a/apps/web-antd/src/router/guard.ts +++ b/apps/web-antd/src/router/guard.ts @@ -54,7 +54,9 @@ function setupAccessGuard(router: Router) { if (coreRouteNames.includes(to.name as string)) { if (to.path === LOGIN_PATH && accessStore.accessToken) { return decodeURIComponent( - (to.query?.redirect as string) || DEFAULT_HOME_PATH, + (to.query?.redirect as string) || + userStore.userInfo?.homePath || + DEFAULT_HOME_PATH, ); } return true; @@ -72,7 +74,10 @@ function setupAccessGuard(router: Router) { return { path: LOGIN_PATH, // 如不需要,直接删除 query - query: { redirect: encodeURIComponent(to.fullPath) }, + query: + to.fullPath === DEFAULT_HOME_PATH + ? {} + : { redirect: encodeURIComponent(to.fullPath) }, // 携带当前跳转的页面,登录后重新跳转该页面 replace: true, }; @@ -102,7 +107,10 @@ function setupAccessGuard(router: Router) { accessStore.setAccessMenus(accessibleMenus); accessStore.setAccessRoutes(accessibleRoutes); accessStore.setIsAccessChecked(true); - const redirectPath = (from.query.redirect ?? to.fullPath) as string; + const redirectPath = (from.query.redirect ?? + (to.path === DEFAULT_HOME_PATH + ? userInfo.homePath || DEFAULT_HOME_PATH + : to.fullPath)) as string; return { ...router.resolve(decodeURIComponent(redirectPath)), diff --git a/apps/web-antd/src/utils/dict.ts b/apps/web-antd/src/utils/dict.ts index 9e4388c0..792e90b2 100644 --- a/apps/web-antd/src/utils/dict.ts +++ b/apps/web-antd/src/utils/dict.ts @@ -1,15 +1,22 @@ -import type { DictData } from '#/api/system/dict/dict-data-model'; - import { dictDataInfo } from '#/api/system/dict/dict-data'; -import { type Option, useDictStore } from '#/store/dict'; +import { useDictStore } from '#/store/dict'; + +/** + * 抽取公共逻辑的基础方法 + * @param dictName 字典名称 + * @param dataGetter 获取字典数据的函数 + * @returns 数据 + */ +function fetchAndCacheDictData( + dictName: string, + dataGetter: () => T[], +): T[] { + const { dictRequestCache, setDictInfo } = useDictStore(); + // 有调用方决定如何获取数据 + const dataList = dataGetter(); -// todo 重复代码的封装 -export function getDict(dictName: string): DictData[] { - const { dictRequestCache, getDict, setDictInfo } = useDictStore(); - // 这里拿到 - const dictList = getDict(dictName); // 检查请求状态缓存 - if (dictList.length === 0 && !dictRequestCache.has(dictName)) { + if (dataList.length === 0 && !dictRequestCache.has(dictName)) { dictRequestCache.set( dictName, dictDataInfo(dictName) @@ -20,31 +27,36 @@ export function getDict(dictName: string): DictData[] { }) .finally(() => { // 移除请求状态缓存 - dictRequestCache.delete(dictName); + /** + * 这里主要判断字典item为空的情况(无奈兼容 不给字典item本来就是错误用法) + * 会导致if一直进入逻辑导致接口无限刷新 + * 在这里dictList为空时 不删除缓存 + */ + if (dataList.length > 0) { + dictRequestCache.delete(dictName); + } }), ); } - return dictList; + return dataList; } -export function getDictOptions(dictName: string): Option[] { - const { dictRequestCache, getDictOptions, setDictInfo } = useDictStore(); - const dictOptionList = getDictOptions(dictName); - // 检查请求状态缓存 - if (dictOptionList.length === 0 && !dictRequestCache.has(dictName)) { - dictRequestCache.set( - dictName, - dictDataInfo(dictName) - .then((resp) => { - // 缓存到store 这样就不用重复获取了 - // 内部处理了push的逻辑 这里不用push - setDictInfo(dictName, resp); - }) - .finally(() => { - // 移除请求状态缓存 - dictRequestCache.delete(dictName); - }), - ); - } - return dictOptionList; +/** + * 这里是提供给渲染标签使用的方法 + * @param dictName 字典名称 + * @returns 字典信息 + */ +export function getDict(dictName: string) { + const { getDict } = useDictStore(); + return fetchAndCacheDictData(dictName, () => getDict(dictName)); +} + +/** + * 一般是Select, Radio, Checkbox等组件使用 + * @param dictName 字典名称 + * @returns Options数组 + */ +export function getDictOptions(dictName: string) { + const { getDictOptions } = useDictStore(); + return fetchAndCacheDictData(dictName, () => getDictOptions(dictName)); } diff --git a/apps/web-antd/src/views/_core/authentication/code-login.vue b/apps/web-antd/src/views/_core/authentication/code-login.vue index dfada340..fc8a2329 100644 --- a/apps/web-antd/src/views/_core/authentication/code-login.vue +++ b/apps/web-antd/src/views/_core/authentication/code-login.vue @@ -15,6 +15,7 @@ import { useAuthStore } from '#/store'; defineOptions({ name: 'CodeLogin' }); const loading = ref(false); +const CODE_LENGTH = 6; const tenantInfo = ref({ tenantEnabled: false, @@ -98,7 +99,9 @@ const formSchema = computed((): VbenFormSchema[] => { }, fieldName: 'code', label: $t('authentication.code'), - rules: z.string().min(1, { message: $t('authentication.codeTip') }), + rules: z.string().length(CODE_LENGTH, { + message: $t('authentication.codeTip', [CODE_LENGTH]), + }), }, ]; }); diff --git a/apps/web-antd/src/views/_core/profile/components/online-device.vue b/apps/web-antd/src/views/_core/profile/components/online-device.vue index 45ceae68..62407855 100644 --- a/apps/web-antd/src/views/_core/profile/components/online-device.vue +++ b/apps/web-antd/src/views/_core/profile/components/online-device.vue @@ -10,7 +10,9 @@ import { columns } from '#/views/monitor/online/data'; const gridOptions: VxeGridProps = { columns, keepSource: true, - pagerConfig: {}, + pagerConfig: { + enabled: false, + }, proxyConfig: { ajax: { query: async () => { diff --git a/apps/web-antd/src/views/common.tsx b/apps/web-antd/src/views/common.tsx index 2e257dab..b42ce6b7 100644 --- a/apps/web-antd/src/views/common.tsx +++ b/apps/web-antd/src/views/common.tsx @@ -1,18 +1,34 @@ import { defineComponent } from 'vue'; -import { Fallback } from '@vben/common-ui'; +import { Page } from '@vben/common-ui'; +import { openWindow } from '@vben/utils'; + +import { Result } from 'ant-design-vue'; export default defineComponent({ name: 'CommonSkeleton', setup() { return () => ( -
- -
+ + + {{ + extra: ( +
+ openWindow('http://106.55.255.76')} + type="primary" + > + 前往工作流版本演示站 + +
+ ), + }} +
+
); }, }); diff --git a/apps/web-antd/src/views/dashboard/analytics/analytics-trends.vue b/apps/web-antd/src/views/dashboard/analytics/analytics-trends.vue index fadfc917..9bd90fc8 100644 --- a/apps/web-antd/src/views/dashboard/analytics/analytics-trends.vue +++ b/apps/web-antd/src/views/dashboard/analytics/analytics-trends.vue @@ -1,11 +1,8 @@