diff --git a/README.md b/README.md index 390b166..137828c 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,7 @@ If you need a basic template, please switch to the `mini` branch. `mini` simply - [vue-element-plus-admin](https://element-plus-admin.cn/) - Full version of the github site - [vue-element-plus-admin](https://kailong110120130.gitee.io/vue-element-plus-admin) - Full version of the gitee site -account: **admin/admin test/test** - -`admin` account is used to simulate the control permission of the server, and render whatever the server returns - -`test` account is used to simulate the front-end control authority. The server only returns the menu key to be displayed, and the front-end performs matching rendering +account: **admin/admin** Online examples do not apply to menu filtering by default, but directly use Static routing @@ -133,11 +129,13 @@ Support modern browsers, not IE If you find this project helpful, welcome sponsorship to show your support~ +[Paypal Me](https://www.paypal.com/paypalme/ckl94) + ## Group - + ## License diff --git a/README.zh-CN.md b/README.zh-CN.md index 4c1cbd1..d60a610 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -31,11 +31,7 @@ vue-element-plus-admin 的定位是后台集成方案,不太适合当基础模 - [vue-element-plus-admin](https://element-plus-admin.cn/) - 完整版 github 站点 - [vue-element-plus-admin](https://kailong110120130.gitee.io/vue-element-plus-admin) - 完整版 gitee 站点 -帐号:**admin/admin test/test** - -`admin` 帐号用于模拟服务端控制权限,服务端返回什么就渲染什么 - -`test` 帐号用于模拟前端控制权限,服务端只返回需要显示的菜单 key,前端进行匹配渲染 +帐号:**admin/admin** 在线例子默认不适用菜单过滤,而是直接使用静态路由表 @@ -133,11 +129,13 @@ pnpm run build:pro 如果你觉得这个项目有帮助,欢迎赞助以示支持~ +[Paypal Me](https://www.paypal.com/paypalme/ckl94) + ## 交流群 - + ## 许可证 diff --git a/mock/department/index.ts b/mock/department/index.ts index 34b5341..b04bdd5 100644 --- a/mock/department/index.ts +++ b/mock/department/index.ts @@ -168,8 +168,10 @@ export default [ const ids = body.ids if (!ids) { return { - code: '500', - message: '请选择需要删除的数据' + data: { + code: 500, + message: '请选择需要删除的数据' + } } } else { return { @@ -203,8 +205,10 @@ export default [ const ids = body.ids if (!ids) { return { - code: '500', - message: '请选择需要删除的数据' + data: { + code: 500, + message: '请选择需要删除的数据' + } } } else { return { diff --git a/mock/role/index.ts b/mock/role/index.ts index decf4ba..b663613 100644 --- a/mock/role/index.ts +++ b/mock/role/index.ts @@ -179,6 +179,14 @@ const adminList = [ meta: { title: 'router.richText' } + }, + { + path: 'json-editor', + component: 'views/Components/Editor/JsonEditor', + name: 'JsonEditor', + meta: { + title: 'router.jsonEditor' + } } ] }, @@ -323,21 +331,29 @@ const adminList = [ } }, { - path: 'useOpenTab', - component: 'views/hooks/useOpenTab', - name: 'UseOpenTab', + path: 'useTagsView', + component: 'views/hooks/useTagsView', + name: 'UseTagsView', meta: { - title: 'useOpenTab' + title: 'useTagsView' + } + }, + { + path: 'useValidator', + component: 'views/hooks/useValidator', + name: 'UseValidator', + meta: { + title: 'useValidator' + } + }, + { + path: 'useCrudSchemas', + component: 'views/hooks/useCrudSchemas', + name: 'UseCrudSchemas', + meta: { + title: 'useCrudSchemas' } } - // { - // path: 'useCrudSchemas', - // component: 'views/hooks/useCrudSchemas', - // name: 'UseCrudSchemas', - // meta: { - // title: 'useCrudSchemas' - // } - // } ] }, { @@ -580,6 +596,7 @@ const testList: string[] = [ '/components/table/ref-table', '/components/editor-demo', '/components/editor-demo/editor', + '/components/editor-demo/json-editor', '/components/search', '/components/descriptions', '/components/image-viewer', @@ -597,8 +614,9 @@ const testList: string[] = [ '/function/multiple-tabs-demo/:id', '/hooks', '/hooks/useWatermark', - '/hooks/useOpenTab', - // '/hooks/useCrudSchemas', + '/hooks/useTagsView', + '/hooks/useValidator', + '/hooks/useCrudSchemas', '/level', '/level/menu1', '/level/menu1/menu1-1', @@ -1052,12 +1070,41 @@ export default [ url: '/role/list', method: 'get', timeout, - response: ({ query }) => { - const { roleName } = query + response: () => { return { data: { code: code, - data: roleName === 'admin' ? adminList : testList + data: adminList + } + } + } + }, + { + url: '/role/table', + method: 'get', + timeout, + response: () => { + return { + data: { + code: code, + data: { + list: List, + total: 4 + } + } + } + } + }, + // 列表接口 + { + url: '/role/list2', + method: 'get', + timeout, + response: () => { + return { + data: { + code: code, + data: testList } } } diff --git a/mock/table/index.ts b/mock/table/index.ts index 04a3d78..fed1457 100644 --- a/mock/table/index.ts +++ b/mock/table/index.ts @@ -246,8 +246,10 @@ export default [ const ids = body.ids if (!ids) { return { - code: '500', - message: '请选择需要删除的数据' + data: { + code: 500, + message: '请选择需要删除的数据' + } } } else { let i = List.length diff --git a/mock/user/index.ts b/mock/user/index.ts index 0b6b30e..a366a71 100644 --- a/mock/user/index.ts +++ b/mock/user/index.ts @@ -76,8 +76,10 @@ export default [ } if (!hasUser) { return { - code: 500, - message: '账号或密码错误' + data: { + code: 500, + message: '账号或密码错误' + } } } } diff --git a/package.json b/package.json index af94f99..79a9bcf 100644 --- a/package.json +++ b/package.json @@ -28,22 +28,22 @@ "dependencies": { "@iconify/iconify": "^3.1.1", "@iconify/vue": "^4.1.1", - "@vueuse/core": "^10.2.1", + "@vueuse/core": "^10.3.0", "@wangeditor/editor": "^5.1.23", "@wangeditor/editor-for-vue": "^5.1.10", "@zxcvbn-ts/core": "^3.0.3", "animate.css": "^4.1.1", "axios": "^1.4.0", "dayjs": "^1.11.9", + "driver.js": "^1.2.1", "echarts": "^5.4.3", "echarts-wordcloud": "^2.1.0", - "element-plus": "^2.3.8", - "intro.js": "^7.0.1", + "element-plus": "^2.3.9", "lodash-es": "^4.17.21", "mitt": "^3.0.1", "mockjs": "^1.1.0", "nprogress": "^0.2.0", - "pinia": "^2.1.4", + "pinia": "^2.1.6", "pinia-plugin-persist": "^1.0.0", "qrcode": "^1.5.3", "qs": "^6.11.2", @@ -51,56 +51,54 @@ "url": "^0.11.1", "vue": "3.3.4", "vue-i18n": "9.2.2", + "vue-json-pretty": "^2.2.4", "vue-router": "^4.2.4", - "vue-types": "^5.1.0" + "vue-types": "^5.1.1" }, "devDependencies": { - "@commitlint/cli": "^17.6.7", - "@commitlint/config-conventional": "^17.6.7", - "@iconify/json": "^2.2.92", + "@commitlint/cli": "^17.7.1", + "@commitlint/config-conventional": "^17.7.0", + "@iconify/json": "^2.2.101", "@intlify/unplugin-vue-i18n": "^0.12.2", "@purge-icons/generated": "^0.9.0", - "@types/intro.js": "^5.1.1", "@types/lodash-es": "^4.17.8", - "@types/node": "^20.4.2", + "@types/node": "^20.4.10", "@types/nprogress": "^0.2.0", "@types/qrcode": "^1.5.1", "@types/qs": "^6.9.7", "@types/sortablejs": "^1.15.1", - "@typescript-eslint/eslint-plugin": "^6.1.0", - "@typescript-eslint/parser": "^6.1.0", - "@unocss/transformer-variant-group": "^0.53.5", - "@vitejs/plugin-legacy": "^4.1.0", + "@typescript-eslint/eslint-plugin": "^6.3.0", + "@typescript-eslint/parser": "^6.3.0", + "@unocss/transformer-variant-group": "^0.55.0", + "@vitejs/plugin-legacy": "^4.1.1", "@vitejs/plugin-vue": "^4.2.3", "@vitejs/plugin-vue-jsx": "^3.0.1", - "@vue-macros/volar": "^0.12.2", "autoprefixer": "^10.4.14", "consola": "^3.2.3", - "eslint": "^8.45.0", - "eslint-config-prettier": "^8.8.0", - "eslint-define-config": "^1.21.0", + "eslint": "^8.47.0", + "eslint-config-prettier": "^9.0.0", + "eslint-define-config": "^1.23.0", "eslint-plugin-prettier": "^5.0.0", - "eslint-plugin-vue": "^9.15.1", + "eslint-plugin-vue": "^9.17.0", "husky": "^8.0.3", - "less": "^4.1.3", + "less": "^4.2.0", "lint-staged": "^13.2.3", "plop": "^3.1.2", - "postcss": "^8.4.26", + "postcss": "^8.4.27", "postcss-html": "^1.5.0", "postcss-less": "^6.0.0", - "prettier": "^3.0.0", + "prettier": "^3.0.1", "rimraf": "^5.0.1", - "rollup": "^3.26.3", - "stylelint": "^15.10.1", + "rollup": "^3.28.0", + "stylelint": "^15.10.2", "stylelint-config-html": "^1.1.0", "stylelint-config-recommended": "^13.0.0", "stylelint-config-standard": "^34.0.0", "stylelint-order": "^6.0.3", - "terser": "^5.19.1", + "terser": "^5.19.2", "typescript": "5.1.6", - "unocss": "^0.53.5", - "unplugin-vue-define-options": "^1.3.11", - "vite": "4.4.4", + "unocss": "^0.55.0", + "vite": "4.4.9", "vite-plugin-ejs": "^1.6.4", "vite-plugin-eslint": "^1.8.1", "vite-plugin-mock": "2.9.6", @@ -108,7 +106,7 @@ "vite-plugin-purge-icons": "^0.9.2", "vite-plugin-style-import": "2.0.0", "vite-plugin-svg-icons": "^2.0.1", - "vue-tsc": "^1.8.5" + "vue-tsc": "^1.8.8" }, "engines": { "node": ">= 14.18.0" diff --git a/src/api/login/index.ts b/src/api/login/index.ts index ccddc37..22bbaf8 100644 --- a/src/api/login/index.ts +++ b/src/api/login/index.ts @@ -30,5 +30,5 @@ export const getAdminRoleApi = ( } export const getTestRoleApi = (params: RoleParams): Promise> => { - return request.get({ url: '/role/list', params }) + return request.get({ url: '/role/list2', params }) } diff --git a/src/components/Breadcrumb/src/Breadcrumb.vue b/src/components/Breadcrumb/src/Breadcrumb.vue index 0a2dcc5..cc79f1b 100644 --- a/src/components/Breadcrumb/src/Breadcrumb.vue +++ b/src/components/Breadcrumb/src/Breadcrumb.vue @@ -5,7 +5,7 @@ import { useRouter } from 'vue-router' import { usePermissionStore } from '@/store/modules/permission' import { filterBreadcrumb } from './helper' import { filter, treeToList } from '@/utils/tree' -import type { RouteLocationNormalizedLoaded, RouteMeta } from 'vue-router' +import type { RouteLocationNormalizedLoaded } from 'vue-router' import { useI18n } from '@/hooks/web/useI18n' import { Icon } from '@/components/Icon' import { useAppStore } from '@/store/modules/app' @@ -47,15 +47,15 @@ export default defineComponent({ const breadcrumbList = treeToList(unref(levelList)) return breadcrumbList.map((v) => { const disabled = !v.redirect || v.redirect === 'noredirect' - const meta = v.meta as RouteMeta + const meta = v.meta return ( {meta?.icon && breadcrumbIcon.value ? ( <> - {t(v?.meta?.title)} + {t(v?.meta?.title || '')} ) : ( - t(v?.meta?.title) + t(v?.meta?.title || '') )} ) diff --git a/src/components/Breadcrumb/src/helper.ts b/src/components/Breadcrumb/src/helper.ts index fb3ec19..690cff9 100644 --- a/src/components/Breadcrumb/src/helper.ts +++ b/src/components/Breadcrumb/src/helper.ts @@ -1,5 +1,4 @@ import { pathResolve } from '@/utils/routerHelper' -import type { RouteMeta } from 'vue-router' export const filterBreadcrumb = ( routes: AppRouteRecordRaw[], @@ -8,7 +7,7 @@ export const filterBreadcrumb = ( const res: AppRouteRecordRaw[] = [] for (const route of routes) { - const meta = route?.meta as RouteMeta + const meta = route?.meta if (meta.hidden && !meta.canTo) { continue } diff --git a/src/components/Descriptions/src/Descriptions.vue b/src/components/Descriptions/src/Descriptions.vue index 20dcc71..40fdc25 100644 --- a/src/components/Descriptions/src/Descriptions.vue +++ b/src/components/Descriptions/src/Descriptions.vue @@ -102,7 +102,7 @@ export default defineComponent({ ) : null} -
+
{{ extra: () => (slots['extra'] ? slots['extra']() : props.extra), diff --git a/src/components/Form/src/helper/componentMap.ts b/src/components/Form/src/helper/componentMap.ts index b40c8ce..1f69009 100644 --- a/src/components/Form/src/helper/componentMap.ts +++ b/src/components/Form/src/helper/componentMap.ts @@ -22,6 +22,7 @@ import { } from 'element-plus' import { InputPassword } from '@/components/InputPassword' import { Editor } from '@/components/Editor' +import { JsonEditor } from '@/components/JsonEditor' import { ComponentName } from '../types' const componentMap: Recordable = { @@ -47,7 +48,8 @@ const componentMap: Recordable = { InputPassword: InputPassword, Editor: Editor, TreeSelect: ElTreeSelect, - Upload: ElUpload + Upload: ElUpload, + JsonEditor: JsonEditor } export { componentMap } diff --git a/src/components/Form/src/types/index.ts b/src/components/Form/src/types/index.ts index 8d13e0d..2d08d84 100644 --- a/src/components/Form/src/types/index.ts +++ b/src/components/Form/src/types/index.ts @@ -21,6 +21,7 @@ import { UploadProps } from 'element-plus' import { IEditorConfig } from '@wangeditor/editor' +import { JsonEditorProps } from '@/components/JsonEditor' import { CSSProperties } from 'vue' export interface PlaceholderModel { @@ -53,7 +54,8 @@ export enum ComponentNameEnum { INPUT_PASSWORD = 'InputPassword', EDITOR = 'Editor', TREE_SELECT = 'TreeSelect', - UPLOAD = 'Upload' + UPLOAD = 'Upload', + JSON_EDITOR = 'JsonEditor' } type CamelCaseComponentName = keyof typeof ComponentNameEnum extends infer K @@ -620,6 +622,7 @@ export interface FormSchema { | InputPasswordComponentProps | TreeSelectComponentProps | UploadComponentProps + | JsonEditorProps | any /** diff --git a/src/components/JsonEditor/index.ts b/src/components/JsonEditor/index.ts new file mode 100644 index 0000000..53a2d06 --- /dev/null +++ b/src/components/JsonEditor/index.ts @@ -0,0 +1,4 @@ +import JsonEditor from './src/JsonEditor.vue' +export type { JsonEditorProps } from './src/types' + +export { JsonEditor } diff --git a/src/components/JsonEditor/src/JsonEditor.vue b/src/components/JsonEditor/src/JsonEditor.vue new file mode 100644 index 0000000..a1c0a43 --- /dev/null +++ b/src/components/JsonEditor/src/JsonEditor.vue @@ -0,0 +1,98 @@ + + + diff --git a/src/components/JsonEditor/src/types/index.ts b/src/components/JsonEditor/src/types/index.ts new file mode 100644 index 0000000..d77097f --- /dev/null +++ b/src/components/JsonEditor/src/types/index.ts @@ -0,0 +1,23 @@ +export interface JsonEditorProps { + value: any + deep?: number + showLength?: boolean + showLineNumbers?: boolean + showLineNumber?: boolean + showIcon?: boolean + showDoubleQuotes?: boolean + virtual?: boolean + height?: number + itemHeight?: number + rootPath?: string + nodeSelectable?: (...args: any[]) => boolean + selectableType?: 'multiple' | 'single' + showSelectController?: boolean + selectOnClickNode?: boolean + highlightSelectedNode?: boolean + collapsedOnClickBrackets?: boolean + renderNodeKey?: (...args: any[]) => any + renderNodeValue?: (...args: any[]) => any + editable?: boolean + editableTrigger?: 'click' | 'dblclick' +} diff --git a/src/components/Menu/src/components/useRenderMenuItem.tsx b/src/components/Menu/src/components/useRenderMenuItem.tsx index 17a520a..d7fede2 100644 --- a/src/components/Menu/src/components/useRenderMenuItem.tsx +++ b/src/components/Menu/src/components/useRenderMenuItem.tsx @@ -1,24 +1,23 @@ import { ElSubMenu, ElMenuItem } from 'element-plus' -import type { RouteMeta } from 'vue-router' import { hasOneShowingChild } from '../helper' import { isUrl } from '@/utils/is' import { useRenderMenuTitle } from './useRenderMenuTitle' import { useDesign } from '@/hooks/web/useDesign' import { pathResolve } from '@/utils/routerHelper' +const { renderMenuTitle } = useRenderMenuTitle() + export const useRenderMenuItem = ( // allRouters: AppRouteRecordRaw[] = [], menuMode: 'vertical' | 'horizontal' ) => { const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => { return routers.map((v) => { - const meta = (v.meta ?? {}) as RouteMeta + const meta = v.meta ?? {} if (!meta.hidden) { const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v) const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath(allRouters, v.path).join('/') - const { renderMenuTitle } = useRenderMenuTitle() - if ( oneShowingChild && (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) && diff --git a/src/components/Menu/src/helper.ts b/src/components/Menu/src/helper.ts index b483881..003cf10 100644 --- a/src/components/Menu/src/helper.ts +++ b/src/components/Menu/src/helper.ts @@ -1,4 +1,3 @@ -import type { RouteMeta } from 'vue-router' import { ref, unref } from 'vue' import { findPath } from '@/utils/tree' @@ -21,7 +20,7 @@ export const hasOneShowingChild = ( const onlyOneChild = ref() const showingChildren = children.filter((v) => { - const meta = (v.meta ?? {}) as RouteMeta + const meta = v.meta ?? {} if (meta.hidden) { return false } else { diff --git a/src/components/Setting/src/components/InterfaceDisplay.vue b/src/components/Setting/src/components/InterfaceDisplay.vue index 8e7d779..e8fbde6 100644 --- a/src/components/Setting/src/components/InterfaceDisplay.vue +++ b/src/components/Setting/src/components/InterfaceDisplay.vue @@ -115,6 +115,14 @@ const dynamicRouterChange = (show: boolean) => { appStore.setDynamicRouter(show) } +// 服务端动态路由 +const serverDynamicRouter = ref(appStore.getServerDynamicRouter) + +const serverDynamicRouterChange = (show: boolean) => { + ElMessage.info(t('setting.reExperienced')) + appStore.setServerDynamicRouter(show) +} + // 固定菜单 const fixedMenu = ref(appStore.getFixedMenu) @@ -206,6 +214,11 @@ watch(
+
+ {{ t('setting.serverDynamicRouter') }} + +
+
{{ t('setting.fixedMenu') }} diff --git a/src/components/TabMenu/src/TabMenu.vue b/src/components/TabMenu/src/TabMenu.vue index 3305113..4ca39b2 100644 --- a/src/components/TabMenu/src/TabMenu.vue +++ b/src/components/TabMenu/src/TabMenu.vue @@ -105,7 +105,8 @@ export default defineComponent({ tabActive.value = item.children ? item.path : item.path.split('/')[0] if (item.children) { if (newPath === oldPath || !unref(showMenu)) { - showMenu.value = unref(fixedMenu) ? true : !unref(showMenu) + // showMenu.value = unref(fixedMenu) ? true : !unref(showMenu) + showMenu.value = !unref(showMenu) } if (unref(showMenu)) { permissionStore.setMenuTabRouters( @@ -131,11 +132,6 @@ export default defineComponent({ return false } - const mouseleave = () => { - if (!unref(showMenu) || unref(fixedMenu)) return - showMenu.value = false - } - return () => (
@@ -178,7 +173,7 @@ export default defineComponent({
{!unref(showTitle) ? undefined : ( -

{t(item.meta?.title)}

+

{t(item.meta?.title || '')}

)}
) @@ -197,11 +192,11 @@ export default defineComponent({
({}) export const initTabMap = (routes: AppRouteRecordRaw[]) => { for (const v of routes) { - const meta = (v.meta ?? {}) as RouteMeta + const meta = v.meta ?? {} if (!meta?.hidden) { tabPathMap[v.path] = [] } @@ -26,7 +25,7 @@ export const filterMenusPath = ( const res: AppRouteRecordRaw[] = [] for (const v of routes) { let data: Nullable = null - const meta = (v.meta ?? {}) as RouteMeta + const meta = v.meta ?? {} if (!meta.hidden || meta.canTo) { const allParentPath = getAllParentPath(allRoutes, v.path) diff --git a/src/components/TagsView/src/TagsView.vue b/src/components/TagsView/src/TagsView.vue index 3eb2d98..86baaa3 100644 --- a/src/components/TagsView/src/TagsView.vue +++ b/src/components/TagsView/src/TagsView.vue @@ -12,6 +12,8 @@ import { useDesign } from '@/hooks/web/useDesign' import { useTemplateRefsList } from '@vueuse/core' import { ElScrollbar } from 'element-plus' import { useScrollTo } from '@/hooks/event/useScrollTo' +import { useTagsView } from '@/hooks/web/useTagsView' +import { cloneDeep } from 'lodash-es' const { getPrefixCls } = useDesign() @@ -19,7 +21,9 @@ const prefixCls = getPrefixCls('tags-view') const { t } = useI18n() -const { currentRoute, push, replace } = useRouter() +const { currentRoute, push } = useRouter() + +const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage } = useTagsView() const permissionStore = usePermissionStore() @@ -31,6 +35,10 @@ const visitedViews = computed(() => tagsViewStore.getVisitedViews) const affixTagArr = ref([]) +const selectedTag = computed(() => tagsViewStore.getSelectedTag) + +const setSelectTag = tagsViewStore.setSelectedTag + const appStore = useAppStore() const tagsViewIcon = computed(() => appStore.getTagsViewIcon) @@ -43,66 +51,30 @@ const initTags = () => { for (const tag of unref(affixTagArr)) { // Must have tag name if (tag.name) { - tagsViewStore.addVisitedView(tag) + tagsViewStore.addVisitedView(cloneDeep(tag)) } } } -const selectedTag = ref() - // 新增tag const addTags = () => { const { name } = unref(currentRoute) if (name) { - selectedTag.value = unref(currentRoute) + setSelectTag(unref(currentRoute)) tagsViewStore.addView(unref(currentRoute)) } - return false } // 关闭选中的tag const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => { - if (view?.meta?.affix) return - tagsViewStore.delView(view) - if (isActive(view)) { - toLastView() - } -} - -// 关闭全部 -const closeAllTags = () => { - tagsViewStore.delAllViews() - toLastView() -} - -// 关闭其它 -const closeOthersTags = () => { - tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded) -} - -// 重新加载 -const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => { - if (!view) return - tagsViewStore.delCachedView() - const { path, query } = view - await nextTick() - replace({ - path: '/redirect' + path, - query: query + closeCurrent(view, () => { + if (isActive(view)) { + toLastView() + } }) } -// 关闭左侧 -const closeLeftTags = () => { - tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded) -} - -// 关闭右侧 -const closeRightTags = () => { - tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded) -} - -// 跳转到最后一个 +// 去最后一个 const toLastView = () => { const visitedViews = tagsViewStore.getVisitedViews const latestView = visitedViews.slice(-1)[0] @@ -121,6 +93,33 @@ const toLastView = () => { } } +// 关闭全部 +const closeAllTags = () => { + closeAll(() => { + toLastView() + }) +} + +// 关闭其它 +const closeOthersTags = () => { + closeOther() +} + +// 重新加载 +const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => { + refreshPage(view) +} + +// 关闭左侧 +const closeLeftTags = () => { + closeLeft() +} + +// 关闭右侧 +const closeRightTags = () => { + closeRight() +} + // 滚动到选中的tag const moveToCurrentTag = async () => { await nextTick() @@ -583,3 +582,4 @@ watch( } } +@/hooks/web/useTagsView diff --git a/src/components/TagsView/src/helper.ts b/src/components/TagsView/src/helper.ts index 22f6a50..912eb8e 100644 --- a/src/components/TagsView/src/helper.ts +++ b/src/components/TagsView/src/helper.ts @@ -1,10 +1,10 @@ -import type { RouteMeta, RouteLocationNormalizedLoaded } from 'vue-router' +import type { RouteLocationNormalizedLoaded } from 'vue-router' import { pathResolve } from '@/utils/routerHelper' export const filterAffixTags = (routes: AppRouteRecordRaw[], parentPath = '') => { let tags: RouteLocationNormalizedLoaded[] = [] routes.forEach((route) => { - const meta = route.meta as RouteMeta + const meta = route.meta ?? {} const tagPath = pathResolve(parentPath, route.path) if (meta?.affix) { tags.push({ ...route, path: tagPath, fullPath: tagPath } as RouteLocationNormalizedLoaded) diff --git a/src/hooks/web/useGuide.ts b/src/hooks/web/useGuide.ts new file mode 100644 index 0000000..7fd2fb0 --- /dev/null +++ b/src/hooks/web/useGuide.ts @@ -0,0 +1,49 @@ +import { Config, driver } from 'driver.js' +import 'driver.js/dist/driver.css' +import { useDesign } from '@/hooks/web/useDesign' +import { useI18n } from '@/hooks/web/useI18n' + +const { t } = useI18n() + +const { variables } = useDesign() + +export const useGuide = (options?: Config) => { + const driverObj = driver( + options || { + showProgress: true, + nextBtnText: t('common.nextLabel'), + prevBtnText: t('common.prevLabel'), + doneBtnText: t('common.doneLabel'), + steps: [ + { + element: `#${variables.namespace}-menu`, + popover: { + title: t('common.menu'), + description: t('common.menuDes'), + side: 'right' + } + }, + { + element: `#${variables.namespace}-tool-header`, + popover: { + title: t('common.tool'), + description: t('common.toolDes'), + side: 'left' + } + }, + { + element: `#${variables.namespace}-tags-view`, + popover: { + title: t('common.tagsView'), + description: t('common.tagsViewDes'), + side: 'bottom' + } + } + ] + } + ) + + return { + ...driverObj + } +} diff --git a/src/hooks/web/useIntro.ts b/src/hooks/web/useIntro.ts deleted file mode 100644 index 85604df..0000000 --- a/src/hooks/web/useIntro.ts +++ /dev/null @@ -1,47 +0,0 @@ -import introJs from 'intro.js' -import { IntroJs, Step, Options } from 'intro.js' -import 'intro.js/introjs.css' -import { useI18n } from '@/hooks/web/useI18n' -import { useDesign } from '@/hooks/web/useDesign' - -export const useIntro = (setps?: Step[], options?: Options) => { - const { t } = useI18n() - - const { variables } = useDesign() - - const defaultSetps: Step[] = setps || [ - { - element: `#${variables.namespace}-menu`, - title: t('common.menu'), - intro: t('common.menuDes'), - position: 'right' - }, - { - element: `#${variables.namespace}-tool-header`, - title: t('common.tool'), - intro: t('common.toolDes'), - position: 'left' - }, - { - element: `#${variables.namespace}-tags-view`, - title: t('common.tagsView'), - intro: t('common.tagsViewDes'), - position: 'bottom' - } - ] - - const defaultOptions: Options = options || { - prevLabel: t('common.prevLabel'), - nextLabel: t('common.nextLabel'), - skipLabel: t('common.skipLabel'), - doneLabel: t('common.doneLabel') - } - - const introRef: IntroJs = introJs() - - introRef.addSteps(defaultSetps).setOptions(defaultOptions) - - return { - introRef - } -} diff --git a/src/hooks/web/useStorage.ts b/src/hooks/web/useStorage.ts index cd077df..e33a6a5 100644 --- a/src/hooks/web/useStorage.ts +++ b/src/hooks/web/useStorage.ts @@ -1,15 +1,21 @@ -import { isArray, isObject } from '@/utils/is' +// 获取传入的值的类型 +const getValueType = (value: any) => { + const type = Object.prototype.toString.call(value) + return type.slice(8, -1) +} export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'sessionStorage') => { const setStorage = (key: string, value: any) => { - window[type].setItem(key, isArray(value) || isObject(value) ? JSON.stringify(value) : value) + const valueType = getValueType(value) + window[type].setItem(key, JSON.stringify({ type: valueType, value })) } const getStorage = (key: string) => { const value = window[type].getItem(key) - try { - return JSON.parse(value || '') - } catch (error) { + if (value) { + const { value: val } = JSON.parse(value) + return val + } else { return value } } @@ -18,8 +24,17 @@ export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'sessionSto window[type].removeItem(key) } - const clear = () => { - window[type].clear() + const clear = (excludes?: string[]) => { + // 获取排除项 + const keys = Object.keys(window[type]) + const defaultExcludes = ['dynamicRouter', 'serverDynamicRouter'] + const excludesArr = excludes ? [...excludes, ...defaultExcludes] : defaultExcludes + const excludesKeys = excludesArr ? keys.filter((key) => !excludesArr.includes(key)) : keys + // 排除项不清除 + excludesKeys.forEach((key) => { + window[type].removeItem(key) + }) + // window[type].clear() } return { diff --git a/src/hooks/web/useTagsView.ts b/src/hooks/web/useTagsView.ts new file mode 100644 index 0000000..31eadb0 --- /dev/null +++ b/src/hooks/web/useTagsView.ts @@ -0,0 +1,63 @@ +import { useTagsViewStoreWithOut } from '@/store/modules/tagsView' +import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router' +import { computed, nextTick, unref } from 'vue' + +export const useTagsView = () => { + const tagsViewStore = useTagsViewStoreWithOut() + + const { replace, currentRoute } = useRouter() + + const selectedTag = computed(() => tagsViewStore.getSelectedTag) + + const closeAll = (callback?: Fn) => { + tagsViewStore.delAllViews() + callback?.() + } + + const closeLeft = (callback?: Fn) => { + tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded) + callback?.() + } + + const closeRight = (callback?: Fn) => { + tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded) + callback?.() + } + + const closeOther = (callback?: Fn) => { + tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded) + callback?.() + } + + const closeCurrent = (view?: RouteLocationNormalizedLoaded, callback?: Fn) => { + if (view?.meta?.affix) return + tagsViewStore.delView(view || unref(currentRoute)) + + callback?.() + } + + const refreshPage = async (view?: RouteLocationNormalizedLoaded, callback?: Fn) => { + tagsViewStore.delCachedView() + const { path, query } = view || unref(currentRoute) + await nextTick() + replace({ + path: '/redirect' + path, + query: query + }) + callback?.() + } + + const setTitle = (title: string, path?: string) => { + tagsViewStore.setTitle(title, path) + } + + return { + closeAll, + closeLeft, + closeRight, + closeOther, + closeCurrent, + refreshPage, + setTitle + } +} diff --git a/src/hooks/web/useValidator.ts b/src/hooks/web/useValidator.ts index a0d36c3..151e35b 100644 --- a/src/hooks/web/useValidator.ts +++ b/src/hooks/web/useValidator.ts @@ -1,56 +1,53 @@ import { useI18n } from '@/hooks/web/useI18n' +import { FormItemRule } from 'element-plus' const { t } = useI18n() -type Callback = (error?: string | Error | undefined) => void - interface LengthRange { min: number max: number - message: string + message?: string } export const useValidator = () => { - const required = (message?: string) => { + const required = (message?: string): FormItemRule => { return { required: true, message: message || t('common.required') } } - const lengthRange = (val: any, callback: Callback, options: LengthRange) => { + const lengthRange = (options: LengthRange): FormItemRule => { const { min, max, message } = options - if (val.length < min || val.length > max) { - callback(new Error(message)) - } else { - callback() + + return { + min, + max, + message: message || t('common.lengthRange', { min, max }) } } - const notSpace = (val: any, callback: Callback, message: string) => { - // 用户名不能有空格 - if (val.indexOf(' ') !== -1) { - callback(new Error(message)) - } else { - callback() + const notSpace = (message?: string): FormItemRule => { + return { + validator: (_, val, callback) => { + if (val?.indexOf(' ') !== -1) { + callback(new Error(message || t('common.notSpace'))) + } else { + callback() + } + } } } - const notSpecialCharacters = (val: any, callback: Callback, message: string) => { - // 密码不能是特殊字符 - if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) { - callback(new Error(message)) - } else { - callback() - } - } - - // 两个字符串是否想等 - const isEqual = (val1: string, val2: string, callback: Callback, message: string) => { - if (val1 === val2) { - callback() - } else { - callback(new Error(message)) + const notSpecialCharacters = (message?: string): FormItemRule => { + return { + validator: (_, val, callback) => { + if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) { + callback(new Error(message || t('common.notSpecialCharacters'))) + } else { + callback() + } + } } } @@ -58,7 +55,6 @@ export const useValidator = () => { required, lengthRange, notSpace, - notSpecialCharacters, - isEqual + notSpecialCharacters } } diff --git a/src/locales/en.ts b/src/locales/en.ts index 5a0042e..990852e 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -44,7 +44,11 @@ export default { refresh: 'Refresh', fullscreen: 'Fullscreen', size: 'Size', - columnSetting: 'Column setting' + columnSetting: 'Column setting', + lengthRange: 'The length should be between {min} and {max}', + notSpace: 'Spaces are not allowed', + notSpecialCharacters: 'Special characters are not allowed', + isEqual: 'The two are not equal' }, lock: { lockScreen: 'Lock screen', @@ -89,7 +93,9 @@ export default { footer: 'Footer', uniqueOpened: 'Unique opened', tagsViewIcon: 'Tags view icon', - dynamicRouter: 'Dynamic router', + // 开启动态路由 + dynamicRouter: 'Enable dynamic router', + serverDynamicRouter: 'Server dynamic router', reExperienced: 'Please exit the login experience again', fixedMenu: 'Fixed menu' }, @@ -143,6 +149,7 @@ export default { defaultTable: 'Basic example', editor: 'Editor', richText: 'Rich text', + jsonEditor: 'JSON Editor', dialog: 'Dialog', imageViewer: 'Image viewer', descriptions: 'Descriptions', @@ -296,6 +303,7 @@ export default { verifyReset: 'Verify reset', // 富文本编辑器 richText: 'Rich text', + jsonEditor: 'JSON Editor', form: 'Form', // 远程加载 remoteLoading: 'Remote loading', @@ -318,7 +326,7 @@ export default { guide: 'Guide', start: 'Start', message: - 'The guide page is very useful for some people who enter the project for the first time. You can briefly introduce the functions of the project. The boot page is based on intro js' + 'The guide page is very useful for some people who enter the project for the first time. You can briefly introduce the functions of the project. The boot page is based on driver.js' }, iconDemo: { icon: 'Icon', @@ -444,7 +452,9 @@ export default { }, richText: { richText: 'Rich text', - richTextDes: 'Secondary packaging based on wangeditor' + richTextDes: 'Secondary packaging based on wangeditor', + jsonEditor: 'JSON Editor', + jsonEditorDes: 'Secondary packaging based on vue-json-pretty' }, dialogDemo: { dialog: 'Dialog', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 305f2ca..012b94d 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -44,7 +44,11 @@ export default { refresh: '刷新', fullscreen: '全屏', size: '尺寸', - columnSetting: '列设置' + columnSetting: '列设置', + lengthRange: '长度在 {min} 到 {max} 个字符', + notSpace: '不能包含空格', + notSpecialCharacters: '不能包含特殊字符', + isEqual: '两次输入不一致' }, lock: { lockScreen: '锁定屏幕', @@ -89,7 +93,8 @@ export default { footer: '页脚', uniqueOpened: '菜单手风琴', tagsViewIcon: '标签页图标', - dynamicRouter: '动态路由', + dynamicRouter: '开启动态路由', + serverDynamicRouter: '服务端动态路由', reExperienced: '请重新退出登录体验', fixedMenu: '固定菜单' }, @@ -143,6 +148,7 @@ export default { defaultTable: '基础示例', editor: '编辑器', richText: '富文本', + jsonEditor: 'JSON编辑器', dialog: '弹窗', imageViewer: '图片预览', descriptions: '描述', @@ -294,6 +300,8 @@ export default { verifyReset: '验证重置', // 富文本编辑器 richText: '富文本编辑器', + // JSON编辑器 + jsonEditor: 'JSON编辑器', form: '表单', // 远程加载 remoteLoading: '远程加载', @@ -313,7 +321,7 @@ export default { guide: '引导页', start: '开始', message: - '引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。引导页基于 intro.js' + '引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。引导页基于 driver.js' }, iconDemo: { icon: '图标', @@ -437,7 +445,9 @@ export default { }, richText: { richText: '富文本', - richTextDes: '基于 wangeditor 二次封装' + richTextDes: '基于 wangeditor 二次封装', + jsonEditor: 'JSON编辑器', + jsonEditorDes: '基于 vue-json-pretty 二次封装' }, dialogDemo: { dialog: '弹窗', diff --git a/src/permission.ts b/src/permission.ts index 92eebfb..67d6ffa 100644 --- a/src/permission.ts +++ b/src/permission.ts @@ -5,16 +5,12 @@ import type { RouteRecordRaw } from 'vue-router' import { useTitle } from '@/hooks/web/useTitle' import { useNProgress } from '@/hooks/web/useNProgress' import { usePermissionStoreWithOut } from '@/store/modules/permission' -// import { useDictStoreWithOut } from '@/store/modules/dict' import { usePageLoading } from '@/hooks/web/usePageLoading' -// import { getDictApi } from '@/api/common' const permissionStore = usePermissionStoreWithOut() const appStore = useAppStoreWithOut() -// const dictStore = useDictStoreWithOut() - const { getStorage } = useStorage() const { start, done } = useNProgress() @@ -30,14 +26,6 @@ router.beforeEach(async (to, from, next) => { if (to.path === '/login') { next({ path: '/' }) } else { - // if (!dictStore.getIsSetDict) { - // // 获取所有字典 - // const res = await getDictApi() - // if (res) { - // dictStore.setDictObj(res.data) - // dictStore.setIsSetDict(true) - // } - // } if (permissionStore.getIsAddRouters) { next() return @@ -45,15 +33,14 @@ router.beforeEach(async (to, from, next) => { // 开发者可根据实际情况进行修改 const roleRouters = getStorage('roleRouters') || [] - const userInfo = getStorage(appStore.getUserInfo) // 是否使用动态路由 if (appStore.getDynamicRouter) { - userInfo.role === 'admin' - ? await permissionStore.generateRoutes('admin', roleRouters as AppCustomRouteRecordRaw[]) - : await permissionStore.generateRoutes('test', roleRouters as string[]) + appStore.serverDynamicRouter + ? await permissionStore.generateRoutes('server', roleRouters as AppCustomRouteRecordRaw[]) + : await permissionStore.generateRoutes('frontEnd', roleRouters as string[]) } else { - await permissionStore.generateRoutes('none') + await permissionStore.generateRoutes('static') } permissionStore.getAddRouters.forEach((route) => { diff --git a/src/router/index.ts b/src/router/index.ts index 0e37e54..06c3b10 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -220,6 +220,14 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [ meta: { title: t('router.richText') } + }, + { + path: 'json-editor', + component: () => import('@/views/Components/Editor/JsonEditor.vue'), + name: 'JsonEditor', + meta: { + title: t('router.jsonEditor') + } } ] }, @@ -339,7 +347,8 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [ meta: { hidden: true, title: t('router.details'), - canTo: true + canTo: true, + activeMenu: '/function/multiple-tabs' } } ] @@ -362,15 +371,31 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [ meta: { title: 'useWatermark' } + }, + { + path: 'useTagsView', + component: () => import('@/views/hooks/useTagsView.vue'), + name: 'UseTagsView', + meta: { + title: 'useTagsView' + } + }, + { + path: 'useValidator', + component: () => import('@/views/hooks/useValidator.vue'), + name: 'UseValidator', + meta: { + title: 'useValidator' + } + }, + { + path: 'useCrudSchemas', + component: () => import('@/views/hooks/useCrudSchemas.vue'), + name: 'UseCrudSchemas', + meta: { + title: 'useCrudSchemas' + } } - // { - // path: 'useCrudSchemas', - // component: () => import('@/views/hooks/useCrudSchemas.vue'), - // name: 'UseCrudSchemas', - // meta: { - // title: 'useCrudSchemas' - // } - // } ] }, { diff --git a/src/store/modules/app.ts b/src/store/modules/app.ts index 733a3f7..3247708 100644 --- a/src/store/modules/app.ts +++ b/src/store/modules/app.ts @@ -21,6 +21,7 @@ interface AppState { fixedHeader: boolean greyMode: boolean dynamicRouter: boolean + serverDynamicRouter: boolean pageLoading: boolean layout: LayoutType title: string @@ -42,7 +43,6 @@ export const useAppStore = defineStore('app', { mobile: false, // 是否是移动端 title: import.meta.env.VITE_APP_TITLE, // 标题 pageLoading: false, // 路由跳转loading - breadcrumb: true, // 面包屑 breadcrumbIcon: true, // 面包屑图标 collapse: false, // 折叠菜单 @@ -57,11 +57,12 @@ export const useAppStore = defineStore('app', { fixedHeader: true, // 固定toolheader footer: true, // 显示页脚 greyMode: false, // 是否开始灰色模式,用于特殊悼念日 - dynamicRouter: getStorage('dynamicRouter') || false, // 是否动态路由 - fixedMenu: getStorage('fixedMenu') || false, // 是否固定菜单 + dynamicRouter: getStorage('dynamicRouter'), // 是否动态路由 + serverDynamicRouter: getStorage('serverDynamicRouter'), // 是否服务端渲染动态路由 + fixedMenu: getStorage('fixedMenu'), // 是否固定菜单 layout: getStorage('layout') || 'classic', // layout布局 - isDark: getStorage('isDark') || false, // 是否是暗黑模式 + isDark: getStorage('isDark'), // 是否是暗黑模式 currentSize: getStorage('default') || 'default', // 组件尺寸 theme: getStorage('theme') || { // 主题色 @@ -138,6 +139,9 @@ export const useAppStore = defineStore('app', { getDynamicRouter(): boolean { return this.dynamicRouter }, + getServerDynamicRouter(): boolean { + return this.serverDynamicRouter + }, getFixedMenu(): boolean { return this.fixedMenu }, @@ -216,6 +220,10 @@ export const useAppStore = defineStore('app', { setStorage('dynamicRouter', dynamicRouter) this.dynamicRouter = dynamicRouter }, + setServerDynamicRouter(serverDynamicRouter: boolean) { + setStorage('serverDynamicRouter', serverDynamicRouter) + this.serverDynamicRouter = serverDynamicRouter + }, setFixedMenu(fixedMenu: boolean) { setStorage('fixedMenu', fixedMenu) this.fixedMenu = fixedMenu diff --git a/src/store/modules/dict.ts b/src/store/modules/dict.ts deleted file mode 100644 index 93dd46e..0000000 --- a/src/store/modules/dict.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { defineStore } from 'pinia' -import { store } from '../index' - -export interface DictState { - isSetDict: boolean - dictObj: Recordable -} - -export const useDictStore = defineStore('dict', { - state: (): DictState => ({ - isSetDict: false, - dictObj: {} - }), - getters: { - getDictObj(): Recordable { - return this.dictObj - }, - getIsSetDict(): boolean { - return this.isSetDict - } - }, - actions: { - setDictObj(dictObj: Recordable) { - this.dictObj = dictObj - }, - setIsSetDict(isSetDict: boolean) { - this.isSetDict = isSetDict - } - } -}) - -export const useDictStoreWithOut = () => { - return useDictStore(store) -} diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts index 63eb0da..48f5a85 100644 --- a/src/store/modules/permission.ts +++ b/src/store/modules/permission.ts @@ -1,6 +1,10 @@ import { defineStore } from 'pinia' import { asyncRouterMap, constantRouterMap } from '@/router' -import { generateRoutesFn1, generateRoutesFn2, flatMultiLevelRoutes } from '@/utils/routerHelper' +import { + generateRoutesByFrontEnd, + generateRoutesByServer, + flatMultiLevelRoutes +} from '@/utils/routerHelper' import { store } from '../index' import { cloneDeep } from 'lodash-es' @@ -34,17 +38,17 @@ export const usePermissionStore = defineStore('permission', { }, actions: { generateRoutes( - type: 'admin' | 'test' | 'none', + type: 'server' | 'frontEnd' | 'static', routers?: AppCustomRouteRecordRaw[] | string[] ): Promise { return new Promise((resolve) => { let routerMap: AppRouteRecordRaw[] = [] - if (type === 'admin') { + if (type === 'server') { // 模拟后端过滤菜单 - routerMap = generateRoutesFn2(routers as AppCustomRouteRecordRaw[]) - } else if (type === 'test') { + routerMap = generateRoutesByServer(routers as AppCustomRouteRecordRaw[]) + } else if (type === 'frontEnd') { // 模拟前端过滤菜单 - routerMap = generateRoutesFn1(cloneDeep(asyncRouterMap), routers as string[]) + routerMap = generateRoutesByFrontEnd(cloneDeep(asyncRouterMap), routers as string[]) } else { // 直接读取静态路由表 routerMap = cloneDeep(asyncRouterMap) diff --git a/src/store/modules/tagsView.ts b/src/store/modules/tagsView.ts index 34281ef..1d608bf 100644 --- a/src/store/modules/tagsView.ts +++ b/src/store/modules/tagsView.ts @@ -4,16 +4,24 @@ import { getRawRoute } from '@/utils/routerHelper' import { defineStore } from 'pinia' import { store } from '../index' import { findIndex } from '@/utils' +import { useStorage } from '@/hooks/web/useStorage' +import { useAppStoreWithOut } from './app' + +const appStore = useAppStoreWithOut() + +const { getStorage } = useStorage() export interface TagsViewState { visitedViews: RouteLocationNormalizedLoaded[] cachedViews: Set + selectedTag?: RouteLocationNormalizedLoaded } export const useTagsViewStore = defineStore('tagsView', { state: (): TagsViewState => ({ visitedViews: [], - cachedViews: new Set() + cachedViews: new Set(), + selectedTag: undefined }), getters: { getVisitedViews(): RouteLocationNormalizedLoaded[] { @@ -21,6 +29,9 @@ export const useTagsViewStore = defineStore('tagsView', { }, getCachedViews(): string[] { return Array.from(this.cachedViews) + }, + getSelectedTag(): RouteLocationNormalizedLoaded | undefined { + return this.selectedTag } }, actions: { @@ -44,7 +55,7 @@ export const useTagsViewStore = defineStore('tagsView', { const cacheMap: Set = new Set() for (const v of this.visitedViews) { const item = getRawRoute(v) - const needCache = !item.meta?.noCache + const needCache = !item?.meta?.noCache if (!needCache) { continue } @@ -85,7 +96,9 @@ export const useTagsViewStore = defineStore('tagsView', { // 删除所有tag delAllVisitedViews() { // const affixTags = this.visitedViews.filter((tag) => tag.meta.affix) - this.visitedViews = [] + this.visitedViews = getStorage(appStore.getUserInfo) + ? this.visitedViews.filter((tag) => tag?.meta?.affix) + : [] }, // 删除其它 delOthersViews(view: RouteLocationNormalizedLoaded) { @@ -131,6 +144,18 @@ export const useTagsViewStore = defineStore('tagsView', { break } } + }, + // 设置当前选中的tag + setSelectedTag(tag: RouteLocationNormalizedLoaded) { + this.selectedTag = tag + }, + setTitle(title: string, path?: string) { + for (const v of this.visitedViews) { + if (v.path === (path ?? this.selectedTag?.path)) { + v.meta.title = title + break + } + } } } }) diff --git a/src/utils/routerHelper.ts b/src/utils/routerHelper.ts index b49377d..b4e7548 100644 --- a/src/utils/routerHelper.ts +++ b/src/utils/routerHelper.ts @@ -3,7 +3,6 @@ import type { Router, RouteLocationNormalized, RouteRecordNormalized, - RouteMeta, RouteRecordRaw } from 'vue-router' import { isUrl } from '@/utils/is' @@ -39,7 +38,7 @@ export const getRawRoute = (route: RouteLocationNormalized): RouteLocationNormal } // 前端控制路由生成 -export const generateRoutesFn1 = ( +export const generateRoutesByFrontEnd = ( routes: AppRouteRecordRaw[], keys: string[], basePath = '/' @@ -47,7 +46,7 @@ export const generateRoutesFn1 = ( const res: AppRouteRecordRaw[] = [] for (const route of routes) { - const meta = route.meta as RouteMeta + const meta = route.meta ?? {} // skip some route if (meta.hidden && !meta.canTo) { continue @@ -70,7 +69,7 @@ export const generateRoutesFn1 = ( if (isUrl(item) && (onlyOneChild === item || route.path === item)) { data = Object.assign({}, route) } else { - const routePath = onlyOneChild ?? pathResolve(basePath, route.path) + const routePath = (onlyOneChild ?? pathResolve(basePath, route.path)).trim() if (routePath === item || meta.followRoute === item) { data = Object.assign({}, route) } @@ -79,7 +78,11 @@ export const generateRoutesFn1 = ( // recursive child routes if (route.children && data) { - data.children = generateRoutesFn1(route.children, keys, pathResolve(basePath, data.path)) + data.children = generateRoutesByFrontEnd( + route.children, + keys, + pathResolve(basePath, data.path) + ) } if (data) { res.push(data as AppRouteRecordRaw) @@ -89,7 +92,7 @@ export const generateRoutesFn1 = ( } // 后端控制路由生成 -export const generateRoutesFn2 = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => { +export const generateRoutesByServer = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => { const res: AppRouteRecordRaw[] = [] for (const route of routes) { @@ -112,7 +115,7 @@ export const generateRoutesFn2 = (routes: AppCustomRouteRecordRaw[]): AppRouteRe } // recursive child routes if (route.children) { - data.children = generateRoutesFn2(route.children) + data.children = generateRoutesByServer(route.children) } res.push(data as AppRouteRecordRaw) } @@ -122,7 +125,7 @@ export const generateRoutesFn2 = (routes: AppCustomRouteRecordRaw[]): AppRouteRe export const pathResolve = (parentPath: string, path: string) => { if (isUrl(path)) return path const childPath = path.startsWith('/') || !path ? path : `/${path}` - return `${parentPath}${childPath}`.replace(/\/\//g, '/') + return `${parentPath}${childPath}`.replace(/\/\//g, '/').trim() } // 路由降级 diff --git a/src/views/Components/Editor/JsonEditor.vue b/src/views/Components/Editor/JsonEditor.vue new file mode 100644 index 0000000..c508fbd --- /dev/null +++ b/src/views/Components/Editor/JsonEditor.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/views/Components/Form/DefaultForm.vue b/src/views/Components/Form/DefaultForm.vue index dffcdf3..4fc24ce 100644 --- a/src/views/Components/Form/DefaultForm.vue +++ b/src/views/Components/Form/DefaultForm.vue @@ -442,6 +442,15 @@ const treeSelectData = [ } ] +// 模拟远程加载 +const getTreeSelectData = () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(treeSelectData) + }, 3000) + }) +} + let id = 0 const imageUrl = ref('') @@ -1533,8 +1542,9 @@ const schema = reactive([ label: `${t('formDemo.treeSelect')}`, component: 'TreeSelect', // 远程加载option - optionApi: () => { - return treeSelectData + optionApi: async () => { + const res = await getTreeSelectData() + return res } }, { @@ -1750,6 +1760,20 @@ const schema = reactive([ ) } } + }, + { + field: 'field85', + component: 'Divider', + label: t('formDemo.jsonEditor') + }, + { + field: 'field86', + component: 'JsonEditor', + label: t('formDemo.default'), + value: { + a: 1, + b: 2 + } } ]) diff --git a/src/views/Function/MultipleTabsDemo.vue b/src/views/Function/MultipleTabsDemo.vue index e8089bd..9421a9a 100644 --- a/src/views/Function/MultipleTabsDemo.vue +++ b/src/views/Function/MultipleTabsDemo.vue @@ -3,10 +3,15 @@ import { ContentWrap } from '@/components/ContentWrap' import { ElInput } from 'element-plus' import { ref } from 'vue' import { useRoute } from 'vue-router' +import { useTagsView } from '@/hooks/web/useTagsView' + +const { setTitle } = useTagsView() const { params } = useRoute() const val = ref(params.id as string) + +setTitle(`详情页-${val.value}`)