Merge branch 'master' into release

This commit is contained in:
kailong321200875 2023-08-27 09:57:38 +08:00
commit fa1735fbdb
49 changed files with 1053 additions and 573 deletions

View File

@ -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://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 - [vue-element-plus-admin](https://kailong110120130.gitee.io/vue-element-plus-admin) - Full version of the gitee site
account: **admin/admin test/test** account: **admin/admin**
`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
Online examples do not apply to menu filtering by default, but directly use Static routing 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~ If you find this project helpful, welcome sponsorship to show your support~
[Paypal Me](https://www.paypal.com/paypalme/ckl94)
<img src="https://github.com/kailong321200875/my-image/raw/master/pay.jpg" /> <img src="https://github.com/kailong321200875/my-image/raw/master/pay.jpg" />
## Group ## Group
<img src="https://github.com/kailong321200875/my-image/raw/master/chat-0820.jpg" /> <img src="https://github.com/kailong321200875/my-image/raw/master/chat-0903.jpg" />
## License ## License

View File

@ -31,11 +31,7 @@ vue-element-plus-admin 的定位是后台集成方案,不太适合当基础模
- [vue-element-plus-admin](https://element-plus-admin.cn/) - 完整版 github 站点 - [vue-element-plus-admin](https://element-plus-admin.cn/) - 完整版 github 站点
- [vue-element-plus-admin](https://kailong110120130.gitee.io/vue-element-plus-admin) - 完整版 gitee 站点 - [vue-element-plus-admin](https://kailong110120130.gitee.io/vue-element-plus-admin) - 完整版 gitee 站点
帐号:**admin/admin test/test** 帐号:**admin/admin**
`admin` 帐号用于模拟服务端控制权限,服务端返回什么就渲染什么
`test` 帐号用于模拟前端控制权限,服务端只返回需要显示的菜单 key前端进行匹配渲染
在线例子默认不适用菜单过滤,而是直接使用静态路由表 在线例子默认不适用菜单过滤,而是直接使用静态路由表
@ -133,11 +129,13 @@ pnpm run build:pro
如果你觉得这个项目有帮助,欢迎赞助以示支持~ 如果你觉得这个项目有帮助,欢迎赞助以示支持~
[Paypal Me](https://www.paypal.com/paypalme/ckl94)
<img src="https://gitee.com/kailong110120130/my-image/raw/master/pay.jpg" /> <img src="https://gitee.com/kailong110120130/my-image/raw/master/pay.jpg" />
## 交流群 ## 交流群
<img src="https://gitee.com/kailong110120130/my-image/raw/master/chat-0820.jpg" /> <img src="https://gitee.com/kailong110120130/my-image/raw/master/chat-0903.jpg" />
## 许可证 ## 许可证

View File

@ -168,9 +168,11 @@ export default [
const ids = body.ids const ids = body.ids
if (!ids) { if (!ids) {
return { return {
code: '500', data: {
code: 500,
message: '请选择需要删除的数据' message: '请选择需要删除的数据'
} }
}
} else { } else {
return { return {
data: { data: {
@ -203,9 +205,11 @@ export default [
const ids = body.ids const ids = body.ids
if (!ids) { if (!ids) {
return { return {
code: '500', data: {
code: 500,
message: '请选择需要删除的数据' message: '请选择需要删除的数据'
} }
}
} else { } else {
return { return {
data: { data: {

View File

@ -179,6 +179,14 @@ const adminList = [
meta: { meta: {
title: 'router.richText' 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', path: 'useTagsView',
component: 'views/hooks/useOpenTab', component: 'views/hooks/useTagsView',
name: 'UseOpenTab', name: 'UseTagsView',
meta: { 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/table/ref-table',
'/components/editor-demo', '/components/editor-demo',
'/components/editor-demo/editor', '/components/editor-demo/editor',
'/components/editor-demo/json-editor',
'/components/search', '/components/search',
'/components/descriptions', '/components/descriptions',
'/components/image-viewer', '/components/image-viewer',
@ -597,8 +614,9 @@ const testList: string[] = [
'/function/multiple-tabs-demo/:id', '/function/multiple-tabs-demo/:id',
'/hooks', '/hooks',
'/hooks/useWatermark', '/hooks/useWatermark',
'/hooks/useOpenTab', '/hooks/useTagsView',
// '/hooks/useCrudSchemas', '/hooks/useValidator',
'/hooks/useCrudSchemas',
'/level', '/level',
'/level/menu1', '/level/menu1',
'/level/menu1/menu1-1', '/level/menu1/menu1-1',
@ -1052,12 +1070,41 @@ export default [
url: '/role/list', url: '/role/list',
method: 'get', method: 'get',
timeout, timeout,
response: ({ query }) => { response: () => {
const { roleName } = query
return { return {
data: { data: {
code: code, 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
} }
} }
} }

View File

@ -246,9 +246,11 @@ export default [
const ids = body.ids const ids = body.ids
if (!ids) { if (!ids) {
return { return {
code: '500', data: {
code: 500,
message: '请选择需要删除的数据' message: '请选择需要删除的数据'
} }
}
} else { } else {
let i = List.length let i = List.length
while (i--) { while (i--) {

View File

@ -76,11 +76,13 @@ export default [
} }
if (!hasUser) { if (!hasUser) {
return { return {
data: {
code: 500, code: 500,
message: '账号或密码错误' message: '账号或密码错误'
} }
} }
} }
}
}, },
// 退出接口 // 退出接口
{ {

View File

@ -28,22 +28,22 @@
"dependencies": { "dependencies": {
"@iconify/iconify": "^3.1.1", "@iconify/iconify": "^3.1.1",
"@iconify/vue": "^4.1.1", "@iconify/vue": "^4.1.1",
"@vueuse/core": "^10.2.1", "@vueuse/core": "^10.3.0",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10", "@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^3.0.3", "@zxcvbn-ts/core": "^3.0.3",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"axios": "^1.4.0", "axios": "^1.4.0",
"dayjs": "^1.11.9", "dayjs": "^1.11.9",
"driver.js": "^1.2.1",
"echarts": "^5.4.3", "echarts": "^5.4.3",
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "^2.1.0",
"element-plus": "^2.3.8", "element-plus": "^2.3.9",
"intro.js": "^7.0.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.1.4", "pinia": "^2.1.6",
"pinia-plugin-persist": "^1.0.0", "pinia-plugin-persist": "^1.0.0",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"qs": "^6.11.2", "qs": "^6.11.2",
@ -51,56 +51,54 @@
"url": "^0.11.1", "url": "^0.11.1",
"vue": "3.3.4", "vue": "3.3.4",
"vue-i18n": "9.2.2", "vue-i18n": "9.2.2",
"vue-json-pretty": "^2.2.4",
"vue-router": "^4.2.4", "vue-router": "^4.2.4",
"vue-types": "^5.1.0" "vue-types": "^5.1.1"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.6.7", "@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.6.7", "@commitlint/config-conventional": "^17.7.0",
"@iconify/json": "^2.2.92", "@iconify/json": "^2.2.101",
"@intlify/unplugin-vue-i18n": "^0.12.2", "@intlify/unplugin-vue-i18n": "^0.12.2",
"@purge-icons/generated": "^0.9.0", "@purge-icons/generated": "^0.9.0",
"@types/intro.js": "^5.1.1",
"@types/lodash-es": "^4.17.8", "@types/lodash-es": "^4.17.8",
"@types/node": "^20.4.2", "@types/node": "^20.4.10",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.5.1", "@types/qrcode": "^1.5.1",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"@types/sortablejs": "^1.15.1", "@types/sortablejs": "^1.15.1",
"@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.1.0", "@typescript-eslint/parser": "^6.3.0",
"@unocss/transformer-variant-group": "^0.53.5", "@unocss/transformer-variant-group": "^0.55.0",
"@vitejs/plugin-legacy": "^4.1.0", "@vitejs/plugin-legacy": "^4.1.1",
"@vitejs/plugin-vue": "^4.2.3", "@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^3.0.1", "@vitejs/plugin-vue-jsx": "^3.0.1",
"@vue-macros/volar": "^0.12.2",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"consola": "^3.2.3", "consola": "^3.2.3",
"eslint": "^8.45.0", "eslint": "^8.47.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^9.0.0",
"eslint-define-config": "^1.21.0", "eslint-define-config": "^1.23.0",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.15.1", "eslint-plugin-vue": "^9.17.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"less": "^4.1.3", "less": "^4.2.0",
"lint-staged": "^13.2.3", "lint-staged": "^13.2.3",
"plop": "^3.1.2", "plop": "^3.1.2",
"postcss": "^8.4.26", "postcss": "^8.4.27",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"prettier": "^3.0.0", "prettier": "^3.0.1",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
"rollup": "^3.26.3", "rollup": "^3.28.0",
"stylelint": "^15.10.1", "stylelint": "^15.10.2",
"stylelint-config-html": "^1.1.0", "stylelint-config-html": "^1.1.0",
"stylelint-config-recommended": "^13.0.0", "stylelint-config-recommended": "^13.0.0",
"stylelint-config-standard": "^34.0.0", "stylelint-config-standard": "^34.0.0",
"stylelint-order": "^6.0.3", "stylelint-order": "^6.0.3",
"terser": "^5.19.1", "terser": "^5.19.2",
"typescript": "5.1.6", "typescript": "5.1.6",
"unocss": "^0.53.5", "unocss": "^0.55.0",
"unplugin-vue-define-options": "^1.3.11", "vite": "4.4.9",
"vite": "4.4.4",
"vite-plugin-ejs": "^1.6.4", "vite-plugin-ejs": "^1.6.4",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-mock": "2.9.6", "vite-plugin-mock": "2.9.6",
@ -108,7 +106,7 @@
"vite-plugin-purge-icons": "^0.9.2", "vite-plugin-purge-icons": "^0.9.2",
"vite-plugin-style-import": "2.0.0", "vite-plugin-style-import": "2.0.0",
"vite-plugin-svg-icons": "^2.0.1", "vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^1.8.5" "vue-tsc": "^1.8.8"
}, },
"engines": { "engines": {
"node": ">= 14.18.0" "node": ">= 14.18.0"

View File

@ -30,5 +30,5 @@ export const getAdminRoleApi = (
} }
export const getTestRoleApi = (params: RoleParams): Promise<IResponse<string[]>> => { export const getTestRoleApi = (params: RoleParams): Promise<IResponse<string[]>> => {
return request.get({ url: '/role/list', params }) return request.get({ url: '/role/list2', params })
} }

View File

@ -5,7 +5,7 @@ import { useRouter } from 'vue-router'
import { usePermissionStore } from '@/store/modules/permission' import { usePermissionStore } from '@/store/modules/permission'
import { filterBreadcrumb } from './helper' import { filterBreadcrumb } from './helper'
import { filter, treeToList } from '@/utils/tree' 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 { useI18n } from '@/hooks/web/useI18n'
import { Icon } from '@/components/Icon' import { Icon } from '@/components/Icon'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
@ -47,15 +47,15 @@ export default defineComponent({
const breadcrumbList = treeToList<AppRouteRecordRaw[]>(unref(levelList)) const breadcrumbList = treeToList<AppRouteRecordRaw[]>(unref(levelList))
return breadcrumbList.map((v) => { return breadcrumbList.map((v) => {
const disabled = !v.redirect || v.redirect === 'noredirect' const disabled = !v.redirect || v.redirect === 'noredirect'
const meta = v.meta as RouteMeta const meta = v.meta
return ( return (
<ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}> <ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}>
{meta?.icon && breadcrumbIcon.value ? ( {meta?.icon && breadcrumbIcon.value ? (
<> <>
<Icon icon={meta.icon} class="mr-[5px]"></Icon> {t(v?.meta?.title)} <Icon icon={meta.icon} class="mr-[5px]"></Icon> {t(v?.meta?.title || '')}
</> </>
) : ( ) : (
t(v?.meta?.title) t(v?.meta?.title || '')
)} )}
</ElBreadcrumbItem> </ElBreadcrumbItem>
) )

View File

@ -1,5 +1,4 @@
import { pathResolve } from '@/utils/routerHelper' import { pathResolve } from '@/utils/routerHelper'
import type { RouteMeta } from 'vue-router'
export const filterBreadcrumb = ( export const filterBreadcrumb = (
routes: AppRouteRecordRaw[], routes: AppRouteRecordRaw[],
@ -8,7 +7,7 @@ export const filterBreadcrumb = (
const res: AppRouteRecordRaw[] = [] const res: AppRouteRecordRaw[] = []
for (const route of routes) { for (const route of routes) {
const meta = route?.meta as RouteMeta const meta = route?.meta
if (meta.hidden && !meta.canTo) { if (meta.hidden && !meta.canTo) {
continue continue
} }

View File

@ -102,7 +102,7 @@ export default defineComponent({
) : null} ) : null}
<ElCollapseTransition> <ElCollapseTransition>
<div v-show={unref(show)} class={[`${prefixCls}-content`]}> <div v-show={unref(show)} class={[`${prefixCls}-content`, 'p-20px']}>
<ElDescriptions {...unref(getBindValue)}> <ElDescriptions {...unref(getBindValue)}>
{{ {{
extra: () => (slots['extra'] ? slots['extra']() : props.extra), extra: () => (slots['extra'] ? slots['extra']() : props.extra),

View File

@ -22,6 +22,7 @@ import {
} from 'element-plus' } from 'element-plus'
import { InputPassword } from '@/components/InputPassword' import { InputPassword } from '@/components/InputPassword'
import { Editor } from '@/components/Editor' import { Editor } from '@/components/Editor'
import { JsonEditor } from '@/components/JsonEditor'
import { ComponentName } from '../types' import { ComponentName } from '../types'
const componentMap: Recordable<Component, ComponentName> = { const componentMap: Recordable<Component, ComponentName> = {
@ -47,7 +48,8 @@ const componentMap: Recordable<Component, ComponentName> = {
InputPassword: InputPassword, InputPassword: InputPassword,
Editor: Editor, Editor: Editor,
TreeSelect: ElTreeSelect, TreeSelect: ElTreeSelect,
Upload: ElUpload Upload: ElUpload,
JsonEditor: JsonEditor
} }
export { componentMap } export { componentMap }

View File

@ -21,6 +21,7 @@ import {
UploadProps UploadProps
} from 'element-plus' } from 'element-plus'
import { IEditorConfig } from '@wangeditor/editor' import { IEditorConfig } from '@wangeditor/editor'
import { JsonEditorProps } from '@/components/JsonEditor'
import { CSSProperties } from 'vue' import { CSSProperties } from 'vue'
export interface PlaceholderModel { export interface PlaceholderModel {
@ -53,7 +54,8 @@ export enum ComponentNameEnum {
INPUT_PASSWORD = 'InputPassword', INPUT_PASSWORD = 'InputPassword',
EDITOR = 'Editor', EDITOR = 'Editor',
TREE_SELECT = 'TreeSelect', TREE_SELECT = 'TreeSelect',
UPLOAD = 'Upload' UPLOAD = 'Upload',
JSON_EDITOR = 'JsonEditor'
} }
type CamelCaseComponentName = keyof typeof ComponentNameEnum extends infer K type CamelCaseComponentName = keyof typeof ComponentNameEnum extends infer K
@ -620,6 +622,7 @@ export interface FormSchema {
| InputPasswordComponentProps | InputPasswordComponentProps
| TreeSelectComponentProps | TreeSelectComponentProps
| UploadComponentProps | UploadComponentProps
| JsonEditorProps
| any | any
/** /**

View File

@ -0,0 +1,4 @@
import JsonEditor from './src/JsonEditor.vue'
export type { JsonEditorProps } from './src/types'
export { JsonEditor }

View File

@ -0,0 +1,98 @@
<script setup lang="ts">
import VueJsonPretty from 'vue-json-pretty'
import 'vue-json-pretty/lib/styles.css'
import { propTypes } from '@/utils/propTypes'
import { computed } from 'vue'
const emits = defineEmits([
'update:modelValue',
'node-click',
'brackets-click',
'icon-click',
'selected-value'
])
const props = defineProps({
modelValue: {
type: Object,
default: () => ({})
},
deep: propTypes.number.def(5),
showLength: propTypes.bool.def(true),
showLineNumbers: propTypes.bool.def(true),
showLineNumber: propTypes.bool.def(true),
showIcon: propTypes.bool.def(true),
showDoubleQuotes: propTypes.bool.def(true),
virtual: propTypes.bool.def(false),
height: propTypes.number.def(400),
itemHeight: propTypes.number.def(20),
rootPath: propTypes.string.def('root'),
nodeSelectable: propTypes.func.def(),
selectableType: propTypes.oneOf<'multiple' | 'single'>(['multiple', 'single']).def(),
showSelectController: propTypes.bool.def(false),
selectOnClickNode: propTypes.bool.def(true),
highlightSelectedNode: propTypes.bool.def(true),
collapsedOnClickBrackets: propTypes.bool.def(true),
renderNodeKey: propTypes.func.def(),
renderNodeValue: propTypes.func.def(),
editable: propTypes.bool.def(true),
editableTrigger: propTypes.oneOf<'click' | 'dblclick'>(['click', 'dblclick']).def('click')
})
const data = computed(() => props.modelValue)
const localModelValue = computed({
get: () => data.value,
set: (val) => {
console.log(val)
emits('update:modelValue', val)
}
})
const nodeClick = (node: any) => {
emits('node-click', node)
}
const bracketsClick = (collapsed: boolean) => {
emits('brackets-click', collapsed)
}
const iconClick = (collapsed: boolean) => {
emits('icon-click', collapsed)
}
const selectedChange = (newVal: any, oldVal: any) => {
console.log(newVal, oldVal)
emits('selected-value', newVal, oldVal)
}
</script>
<template>
<VueJsonPretty
v-model:data="localModelValue"
:deep="deep"
:show-length="showLength"
:show-line-numbers="showLineNumbers"
:show-line-number="showLineNumber"
:show-icon="showIcon"
:show-double-quotes="showDoubleQuotes"
:virtual="virtual"
:height="height"
:item-height="itemHeight"
:root-path="rootPath"
:node-selectable="nodeSelectable"
:selectable-type="selectableType"
:show-select-controller="showSelectController"
:select-on-click-node="selectOnClickNode"
:highlight-selected-node="highlightSelectedNode"
:collapsed-on-click-brackets="collapsedOnClickBrackets"
:render-node-key="renderNodeKey"
:render-node-value="renderNodeValue"
:editable="editable"
:editable-trigger="editableTrigger"
@node-click="nodeClick"
@brackets-click="bracketsClick"
@icon-click="iconClick"
@selected-change="selectedChange"
/>
</template>

View File

@ -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'
}

View File

@ -1,24 +1,23 @@
import { ElSubMenu, ElMenuItem } from 'element-plus' import { ElSubMenu, ElMenuItem } from 'element-plus'
import type { RouteMeta } from 'vue-router'
import { hasOneShowingChild } from '../helper' import { hasOneShowingChild } from '../helper'
import { isUrl } from '@/utils/is' import { isUrl } from '@/utils/is'
import { useRenderMenuTitle } from './useRenderMenuTitle' import { useRenderMenuTitle } from './useRenderMenuTitle'
import { useDesign } from '@/hooks/web/useDesign' import { useDesign } from '@/hooks/web/useDesign'
import { pathResolve } from '@/utils/routerHelper' import { pathResolve } from '@/utils/routerHelper'
const { renderMenuTitle } = useRenderMenuTitle()
export const useRenderMenuItem = ( export const useRenderMenuItem = (
// allRouters: AppRouteRecordRaw[] = [], // allRouters: AppRouteRecordRaw[] = [],
menuMode: 'vertical' | 'horizontal' menuMode: 'vertical' | 'horizontal'
) => { ) => {
const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => { const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
return routers.map((v) => { return routers.map((v) => {
const meta = (v.meta ?? {}) as RouteMeta const meta = v.meta ?? {}
if (!meta.hidden) { if (!meta.hidden) {
const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v) const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/') const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
const { renderMenuTitle } = useRenderMenuTitle()
if ( if (
oneShowingChild && oneShowingChild &&
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) && (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&

View File

@ -1,4 +1,3 @@
import type { RouteMeta } from 'vue-router'
import { ref, unref } from 'vue' import { ref, unref } from 'vue'
import { findPath } from '@/utils/tree' import { findPath } from '@/utils/tree'
@ -21,7 +20,7 @@ export const hasOneShowingChild = (
const onlyOneChild = ref<OnlyOneChildType>() const onlyOneChild = ref<OnlyOneChildType>()
const showingChildren = children.filter((v) => { const showingChildren = children.filter((v) => {
const meta = (v.meta ?? {}) as RouteMeta const meta = v.meta ?? {}
if (meta.hidden) { if (meta.hidden) {
return false return false
} else { } else {

View File

@ -115,6 +115,14 @@ const dynamicRouterChange = (show: boolean) => {
appStore.setDynamicRouter(show) 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) const fixedMenu = ref(appStore.getFixedMenu)
@ -206,6 +214,11 @@ watch(
<ElSwitch v-model="dynamicRouter" @change="dynamicRouterChange" /> <ElSwitch v-model="dynamicRouter" @change="dynamicRouterChange" />
</div> </div>
<div class="flex justify-between items-center">
<span class="text-14px">{{ t('setting.serverDynamicRouter') }}</span>
<ElSwitch v-model="serverDynamicRouter" @change="serverDynamicRouterChange" />
</div>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-14px">{{ t('setting.fixedMenu') }}</span> <span class="text-14px">{{ t('setting.fixedMenu') }}</span>
<ElSwitch v-model="fixedMenu" @change="fixedMenuChange" /> <ElSwitch v-model="fixedMenu" @change="fixedMenuChange" />

View File

@ -105,7 +105,8 @@ export default defineComponent({
tabActive.value = item.children ? item.path : item.path.split('/')[0] tabActive.value = item.children ? item.path : item.path.split('/')[0]
if (item.children) { if (item.children) {
if (newPath === oldPath || !unref(showMenu)) { 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)) { if (unref(showMenu)) {
permissionStore.setMenuTabRouters( permissionStore.setMenuTabRouters(
@ -131,11 +132,6 @@ export default defineComponent({
return false return false
} }
const mouseleave = () => {
if (!unref(showMenu) || unref(fixedMenu)) return
showMenu.value = false
}
return () => ( return () => (
<div <div
id={`${variables.namespace}-menu`} id={`${variables.namespace}-menu`}
@ -147,7 +143,6 @@ export default defineComponent({
'w-[var(--tab-menu-min-width)]': unref(collapse) 'w-[var(--tab-menu-min-width)]': unref(collapse)
} }
]} ]}
onMouseleave={mouseleave}
> >
<ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height)-1px)]"> <ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height)-1px)]">
<div> <div>
@ -178,7 +173,7 @@ export default defineComponent({
<Icon icon={item?.meta?.icon}></Icon> <Icon icon={item?.meta?.icon}></Icon>
</div> </div>
{!unref(showTitle) ? undefined : ( {!unref(showTitle) ? undefined : (
<p class="break-words mt-5px px-2px">{t(item.meta?.title)}</p> <p class="break-words mt-5px px-2px">{t(item.meta?.title || '')}</p>
)} )}
</div> </div>
) )
@ -197,11 +192,11 @@ export default defineComponent({
</div> </div>
<Menu <Menu
class={[ class={[
'!absolute top-0', '!absolute top-0 z-4000',
{ {
'!left-[var(--tab-menu-min-width)]': unref(collapse), '!left-[var(--tab-menu-min-width)]': unref(collapse),
'!left-[var(--tab-menu-max-width)]': !unref(collapse), '!left-[var(--tab-menu-max-width)]': !unref(collapse),
'!w-[calc(var(--left-menu-max-width)+1px)]': unref(showMenu) || unref(fixedMenu), '!w-[var(--left-menu-max-width)]': unref(showMenu) || unref(fixedMenu),
'!w-0': !unref(showMenu) && !unref(fixedMenu) '!w-0': !unref(showMenu) && !unref(fixedMenu)
} }
]} ]}

View File

@ -1,5 +1,4 @@
import { getAllParentPath } from '@/components/Menu/src/helper' import { getAllParentPath } from '@/components/Menu/src/helper'
import type { RouteMeta } from 'vue-router'
import { isUrl } from '@/utils/is' import { isUrl } from '@/utils/is'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import { reactive } from 'vue' import { reactive } from 'vue'
@ -12,7 +11,7 @@ export const tabPathMap = reactive<TabMapTypes>({})
export const initTabMap = (routes: AppRouteRecordRaw[]) => { export const initTabMap = (routes: AppRouteRecordRaw[]) => {
for (const v of routes) { for (const v of routes) {
const meta = (v.meta ?? {}) as RouteMeta const meta = v.meta ?? {}
if (!meta?.hidden) { if (!meta?.hidden) {
tabPathMap[v.path] = [] tabPathMap[v.path] = []
} }
@ -26,7 +25,7 @@ export const filterMenusPath = (
const res: AppRouteRecordRaw[] = [] const res: AppRouteRecordRaw[] = []
for (const v of routes) { for (const v of routes) {
let data: Nullable<AppRouteRecordRaw> = null let data: Nullable<AppRouteRecordRaw> = null
const meta = (v.meta ?? {}) as RouteMeta const meta = v.meta ?? {}
if (!meta.hidden || meta.canTo) { if (!meta.hidden || meta.canTo) {
const allParentPath = getAllParentPath<AppRouteRecordRaw>(allRoutes, v.path) const allParentPath = getAllParentPath<AppRouteRecordRaw>(allRoutes, v.path)

View File

@ -12,6 +12,8 @@ import { useDesign } from '@/hooks/web/useDesign'
import { useTemplateRefsList } from '@vueuse/core' import { useTemplateRefsList } from '@vueuse/core'
import { ElScrollbar } from 'element-plus' import { ElScrollbar } from 'element-plus'
import { useScrollTo } from '@/hooks/event/useScrollTo' import { useScrollTo } from '@/hooks/event/useScrollTo'
import { useTagsView } from '@/hooks/web/useTagsView'
import { cloneDeep } from 'lodash-es'
const { getPrefixCls } = useDesign() const { getPrefixCls } = useDesign()
@ -19,7 +21,9 @@ const prefixCls = getPrefixCls('tags-view')
const { t } = useI18n() const { t } = useI18n()
const { currentRoute, push, replace } = useRouter() const { currentRoute, push } = useRouter()
const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage } = useTagsView()
const permissionStore = usePermissionStore() const permissionStore = usePermissionStore()
@ -31,6 +35,10 @@ const visitedViews = computed(() => tagsViewStore.getVisitedViews)
const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([]) const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([])
const selectedTag = computed(() => tagsViewStore.getSelectedTag)
const setSelectTag = tagsViewStore.setSelectedTag
const appStore = useAppStore() const appStore = useAppStore()
const tagsViewIcon = computed(() => appStore.getTagsViewIcon) const tagsViewIcon = computed(() => appStore.getTagsViewIcon)
@ -43,66 +51,30 @@ const initTags = () => {
for (const tag of unref(affixTagArr)) { for (const tag of unref(affixTagArr)) {
// Must have tag name // Must have tag name
if (tag.name) { if (tag.name) {
tagsViewStore.addVisitedView(tag) tagsViewStore.addVisitedView(cloneDeep(tag))
} }
} }
} }
const selectedTag = ref<RouteLocationNormalizedLoaded>()
// tag // tag
const addTags = () => { const addTags = () => {
const { name } = unref(currentRoute) const { name } = unref(currentRoute)
if (name) { if (name) {
selectedTag.value = unref(currentRoute) setSelectTag(unref(currentRoute))
tagsViewStore.addView(unref(currentRoute)) tagsViewStore.addView(unref(currentRoute))
} }
return false
} }
// tag // tag
const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => { const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => {
if (view?.meta?.affix) return closeCurrent(view, () => {
tagsViewStore.delView(view)
if (isActive(view)) { if (isActive(view)) {
toLastView() 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
}) })
} }
// //
const closeLeftTags = () => {
tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
}
//
const closeRightTags = () => {
tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
}
//
const toLastView = () => { const toLastView = () => {
const visitedViews = tagsViewStore.getVisitedViews const visitedViews = tagsViewStore.getVisitedViews
const latestView = visitedViews.slice(-1)[0] 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 // tag
const moveToCurrentTag = async () => { const moveToCurrentTag = async () => {
await nextTick() await nextTick()
@ -583,3 +582,4 @@ watch(
} }
} }
</style> </style>
@/hooks/web/useTagsView

View File

@ -1,10 +1,10 @@
import type { RouteMeta, RouteLocationNormalizedLoaded } from 'vue-router' import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { pathResolve } from '@/utils/routerHelper' import { pathResolve } from '@/utils/routerHelper'
export const filterAffixTags = (routes: AppRouteRecordRaw[], parentPath = '') => { export const filterAffixTags = (routes: AppRouteRecordRaw[], parentPath = '') => {
let tags: RouteLocationNormalizedLoaded[] = [] let tags: RouteLocationNormalizedLoaded[] = []
routes.forEach((route) => { routes.forEach((route) => {
const meta = route.meta as RouteMeta const meta = route.meta ?? {}
const tagPath = pathResolve(parentPath, route.path) const tagPath = pathResolve(parentPath, route.path)
if (meta?.affix) { if (meta?.affix) {
tags.push({ ...route, path: tagPath, fullPath: tagPath } as RouteLocationNormalizedLoaded) tags.push({ ...route, path: tagPath, fullPath: tagPath } as RouteLocationNormalizedLoaded)

49
src/hooks/web/useGuide.ts Normal file
View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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') => { export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'sessionStorage') => {
const setStorage = (key: string, value: any) => { 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 getStorage = (key: string) => {
const value = window[type].getItem(key) const value = window[type].getItem(key)
try { if (value) {
return JSON.parse(value || '') const { value: val } = JSON.parse(value)
} catch (error) { return val
} else {
return value return value
} }
} }
@ -18,8 +24,17 @@ export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'sessionSto
window[type].removeItem(key) window[type].removeItem(key)
} }
const clear = () => { const clear = (excludes?: string[]) => {
window[type].clear() // 获取排除项
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 { return {

View File

@ -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
}
}

View File

@ -1,56 +1,53 @@
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { FormItemRule } from 'element-plus'
const { t } = useI18n() const { t } = useI18n()
type Callback = (error?: string | Error | undefined) => void
interface LengthRange { interface LengthRange {
min: number min: number
max: number max: number
message: string message?: string
} }
export const useValidator = () => { export const useValidator = () => {
const required = (message?: string) => { const required = (message?: string): FormItemRule => {
return { return {
required: true, required: true,
message: message || t('common.required') message: message || t('common.required')
} }
} }
const lengthRange = (val: any, callback: Callback, options: LengthRange) => { const lengthRange = (options: LengthRange): FormItemRule => {
const { min, max, message } = options const { min, max, message } = options
if (val.length < min || val.length > max) {
callback(new Error(message)) return {
min,
max,
message: message || t('common.lengthRange', { min, max })
}
}
const notSpace = (message?: string): FormItemRule => {
return {
validator: (_, val, callback) => {
if (val?.indexOf(' ') !== -1) {
callback(new Error(message || t('common.notSpace')))
} else { } else {
callback() callback()
} }
} }
const notSpace = (val: any, callback: Callback, message: string) => {
// 用户名不能有空格
if (val.indexOf(' ') !== -1) {
callback(new Error(message))
} else {
callback()
} }
} }
const notSpecialCharacters = (val: any, callback: Callback, message: string) => { const notSpecialCharacters = (message?: string): FormItemRule => {
// 密码不能是特殊字符 return {
validator: (_, val, callback) => {
if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) { if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) {
callback(new Error(message)) callback(new Error(message || t('common.notSpecialCharacters')))
} else { } else {
callback() callback()
} }
} }
// 两个字符串是否想等
const isEqual = (val1: string, val2: string, callback: Callback, message: string) => {
if (val1 === val2) {
callback()
} else {
callback(new Error(message))
} }
} }
@ -58,7 +55,6 @@ export const useValidator = () => {
required, required,
lengthRange, lengthRange,
notSpace, notSpace,
notSpecialCharacters, notSpecialCharacters
isEqual
} }
} }

View File

@ -44,7 +44,11 @@ export default {
refresh: 'Refresh', refresh: 'Refresh',
fullscreen: 'Fullscreen', fullscreen: 'Fullscreen',
size: 'Size', 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: { lock: {
lockScreen: 'Lock screen', lockScreen: 'Lock screen',
@ -89,7 +93,9 @@ export default {
footer: 'Footer', footer: 'Footer',
uniqueOpened: 'Unique opened', uniqueOpened: 'Unique opened',
tagsViewIcon: 'Tags view icon', tagsViewIcon: 'Tags view icon',
dynamicRouter: 'Dynamic router', // 开启动态路由
dynamicRouter: 'Enable dynamic router',
serverDynamicRouter: 'Server dynamic router',
reExperienced: 'Please exit the login experience again', reExperienced: 'Please exit the login experience again',
fixedMenu: 'Fixed menu' fixedMenu: 'Fixed menu'
}, },
@ -143,6 +149,7 @@ export default {
defaultTable: 'Basic example', defaultTable: 'Basic example',
editor: 'Editor', editor: 'Editor',
richText: 'Rich text', richText: 'Rich text',
jsonEditor: 'JSON Editor',
dialog: 'Dialog', dialog: 'Dialog',
imageViewer: 'Image viewer', imageViewer: 'Image viewer',
descriptions: 'Descriptions', descriptions: 'Descriptions',
@ -296,6 +303,7 @@ export default {
verifyReset: 'Verify reset', verifyReset: 'Verify reset',
// 富文本编辑器 // 富文本编辑器
richText: 'Rich text', richText: 'Rich text',
jsonEditor: 'JSON Editor',
form: 'Form', form: 'Form',
// 远程加载 // 远程加载
remoteLoading: 'Remote loading', remoteLoading: 'Remote loading',
@ -318,7 +326,7 @@ export default {
guide: 'Guide', guide: 'Guide',
start: 'Start', start: 'Start',
message: 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: { iconDemo: {
icon: 'Icon', icon: 'Icon',
@ -444,7 +452,9 @@ export default {
}, },
richText: { richText: {
richText: 'Rich text', 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: { dialogDemo: {
dialog: 'Dialog', dialog: 'Dialog',

View File

@ -44,7 +44,11 @@ export default {
refresh: '刷新', refresh: '刷新',
fullscreen: '全屏', fullscreen: '全屏',
size: '尺寸', size: '尺寸',
columnSetting: '列设置' columnSetting: '列设置',
lengthRange: '长度在 {min} 到 {max} 个字符',
notSpace: '不能包含空格',
notSpecialCharacters: '不能包含特殊字符',
isEqual: '两次输入不一致'
}, },
lock: { lock: {
lockScreen: '锁定屏幕', lockScreen: '锁定屏幕',
@ -89,7 +93,8 @@ export default {
footer: '页脚', footer: '页脚',
uniqueOpened: '菜单手风琴', uniqueOpened: '菜单手风琴',
tagsViewIcon: '标签页图标', tagsViewIcon: '标签页图标',
dynamicRouter: '动态路由', dynamicRouter: '开启动态路由',
serverDynamicRouter: '服务端动态路由',
reExperienced: '请重新退出登录体验', reExperienced: '请重新退出登录体验',
fixedMenu: '固定菜单' fixedMenu: '固定菜单'
}, },
@ -143,6 +148,7 @@ export default {
defaultTable: '基础示例', defaultTable: '基础示例',
editor: '编辑器', editor: '编辑器',
richText: '富文本', richText: '富文本',
jsonEditor: 'JSON编辑器',
dialog: '弹窗', dialog: '弹窗',
imageViewer: '图片预览', imageViewer: '图片预览',
descriptions: '描述', descriptions: '描述',
@ -294,6 +300,8 @@ export default {
verifyReset: '验证重置', verifyReset: '验证重置',
// 富文本编辑器 // 富文本编辑器
richText: '富文本编辑器', richText: '富文本编辑器',
// JSON编辑器
jsonEditor: 'JSON编辑器',
form: '表单', form: '表单',
// 远程加载 // 远程加载
remoteLoading: '远程加载', remoteLoading: '远程加载',
@ -313,7 +321,7 @@ export default {
guide: '引导页', guide: '引导页',
start: '开始', start: '开始',
message: message:
'引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。引导页基于 intro.js' '引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。引导页基于 driver.js'
}, },
iconDemo: { iconDemo: {
icon: '图标', icon: '图标',
@ -437,7 +445,9 @@ export default {
}, },
richText: { richText: {
richText: '富文本', richText: '富文本',
richTextDes: '基于 wangeditor 二次封装' richTextDes: '基于 wangeditor 二次封装',
jsonEditor: 'JSON编辑器',
jsonEditorDes: '基于 vue-json-pretty 二次封装'
}, },
dialogDemo: { dialogDemo: {
dialog: '弹窗', dialog: '弹窗',

View File

@ -5,16 +5,12 @@ import type { RouteRecordRaw } from 'vue-router'
import { useTitle } from '@/hooks/web/useTitle' import { useTitle } from '@/hooks/web/useTitle'
import { useNProgress } from '@/hooks/web/useNProgress' import { useNProgress } from '@/hooks/web/useNProgress'
import { usePermissionStoreWithOut } from '@/store/modules/permission' import { usePermissionStoreWithOut } from '@/store/modules/permission'
// import { useDictStoreWithOut } from '@/store/modules/dict'
import { usePageLoading } from '@/hooks/web/usePageLoading' import { usePageLoading } from '@/hooks/web/usePageLoading'
// import { getDictApi } from '@/api/common'
const permissionStore = usePermissionStoreWithOut() const permissionStore = usePermissionStoreWithOut()
const appStore = useAppStoreWithOut() const appStore = useAppStoreWithOut()
// const dictStore = useDictStoreWithOut()
const { getStorage } = useStorage() const { getStorage } = useStorage()
const { start, done } = useNProgress() const { start, done } = useNProgress()
@ -30,14 +26,6 @@ router.beforeEach(async (to, from, next) => {
if (to.path === '/login') { if (to.path === '/login') {
next({ path: '/' }) next({ path: '/' })
} else { } else {
// if (!dictStore.getIsSetDict) {
// // 获取所有字典
// const res = await getDictApi()
// if (res) {
// dictStore.setDictObj(res.data)
// dictStore.setIsSetDict(true)
// }
// }
if (permissionStore.getIsAddRouters) { if (permissionStore.getIsAddRouters) {
next() next()
return return
@ -45,15 +33,14 @@ router.beforeEach(async (to, from, next) => {
// 开发者可根据实际情况进行修改 // 开发者可根据实际情况进行修改
const roleRouters = getStorage('roleRouters') || [] const roleRouters = getStorage('roleRouters') || []
const userInfo = getStorage(appStore.getUserInfo)
// 是否使用动态路由 // 是否使用动态路由
if (appStore.getDynamicRouter) { if (appStore.getDynamicRouter) {
userInfo.role === 'admin' appStore.serverDynamicRouter
? await permissionStore.generateRoutes('admin', roleRouters as AppCustomRouteRecordRaw[]) ? await permissionStore.generateRoutes('server', roleRouters as AppCustomRouteRecordRaw[])
: await permissionStore.generateRoutes('test', roleRouters as string[]) : await permissionStore.generateRoutes('frontEnd', roleRouters as string[])
} else { } else {
await permissionStore.generateRoutes('none') await permissionStore.generateRoutes('static')
} }
permissionStore.getAddRouters.forEach((route) => { permissionStore.getAddRouters.forEach((route) => {

View File

@ -220,6 +220,14 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
meta: { meta: {
title: t('router.richText') 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: { meta: {
hidden: true, hidden: true,
title: t('router.details'), title: t('router.details'),
canTo: true canTo: true,
activeMenu: '/function/multiple-tabs'
} }
} }
] ]
@ -362,15 +371,31 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
meta: { meta: {
title: 'useWatermark' 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'
// }
// }
] ]
}, },
{ {

View File

@ -21,6 +21,7 @@ interface AppState {
fixedHeader: boolean fixedHeader: boolean
greyMode: boolean greyMode: boolean
dynamicRouter: boolean dynamicRouter: boolean
serverDynamicRouter: boolean
pageLoading: boolean pageLoading: boolean
layout: LayoutType layout: LayoutType
title: string title: string
@ -42,7 +43,6 @@ export const useAppStore = defineStore('app', {
mobile: false, // 是否是移动端 mobile: false, // 是否是移动端
title: import.meta.env.VITE_APP_TITLE, // 标题 title: import.meta.env.VITE_APP_TITLE, // 标题
pageLoading: false, // 路由跳转loading pageLoading: false, // 路由跳转loading
breadcrumb: true, // 面包屑 breadcrumb: true, // 面包屑
breadcrumbIcon: true, // 面包屑图标 breadcrumbIcon: true, // 面包屑图标
collapse: false, // 折叠菜单 collapse: false, // 折叠菜单
@ -57,11 +57,12 @@ export const useAppStore = defineStore('app', {
fixedHeader: true, // 固定toolheader fixedHeader: true, // 固定toolheader
footer: true, // 显示页脚 footer: true, // 显示页脚
greyMode: false, // 是否开始灰色模式,用于特殊悼念日 greyMode: false, // 是否开始灰色模式,用于特殊悼念日
dynamicRouter: getStorage('dynamicRouter') || false, // 是否动态路由 dynamicRouter: getStorage('dynamicRouter'), // 是否动态路由
fixedMenu: getStorage('fixedMenu') || false, // 是否固定菜单 serverDynamicRouter: getStorage('serverDynamicRouter'), // 是否服务端渲染动态路由
fixedMenu: getStorage('fixedMenu'), // 是否固定菜单
layout: getStorage('layout') || 'classic', // layout布局 layout: getStorage('layout') || 'classic', // layout布局
isDark: getStorage('isDark') || false, // 是否是暗黑模式 isDark: getStorage('isDark'), // 是否是暗黑模式
currentSize: getStorage('default') || 'default', // 组件尺寸 currentSize: getStorage('default') || 'default', // 组件尺寸
theme: getStorage('theme') || { theme: getStorage('theme') || {
// 主题色 // 主题色
@ -138,6 +139,9 @@ export const useAppStore = defineStore('app', {
getDynamicRouter(): boolean { getDynamicRouter(): boolean {
return this.dynamicRouter return this.dynamicRouter
}, },
getServerDynamicRouter(): boolean {
return this.serverDynamicRouter
},
getFixedMenu(): boolean { getFixedMenu(): boolean {
return this.fixedMenu return this.fixedMenu
}, },
@ -216,6 +220,10 @@ export const useAppStore = defineStore('app', {
setStorage('dynamicRouter', dynamicRouter) setStorage('dynamicRouter', dynamicRouter)
this.dynamicRouter = dynamicRouter this.dynamicRouter = dynamicRouter
}, },
setServerDynamicRouter(serverDynamicRouter: boolean) {
setStorage('serverDynamicRouter', serverDynamicRouter)
this.serverDynamicRouter = serverDynamicRouter
},
setFixedMenu(fixedMenu: boolean) { setFixedMenu(fixedMenu: boolean) {
setStorage('fixedMenu', fixedMenu) setStorage('fixedMenu', fixedMenu)
this.fixedMenu = fixedMenu this.fixedMenu = fixedMenu

View File

@ -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)
}

View File

@ -1,6 +1,10 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { asyncRouterMap, constantRouterMap } from '@/router' import { asyncRouterMap, constantRouterMap } from '@/router'
import { generateRoutesFn1, generateRoutesFn2, flatMultiLevelRoutes } from '@/utils/routerHelper' import {
generateRoutesByFrontEnd,
generateRoutesByServer,
flatMultiLevelRoutes
} from '@/utils/routerHelper'
import { store } from '../index' import { store } from '../index'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
@ -34,17 +38,17 @@ export const usePermissionStore = defineStore('permission', {
}, },
actions: { actions: {
generateRoutes( generateRoutes(
type: 'admin' | 'test' | 'none', type: 'server' | 'frontEnd' | 'static',
routers?: AppCustomRouteRecordRaw[] | string[] routers?: AppCustomRouteRecordRaw[] | string[]
): Promise<unknown> { ): Promise<unknown> {
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
let routerMap: AppRouteRecordRaw[] = [] let routerMap: AppRouteRecordRaw[] = []
if (type === 'admin') { if (type === 'server') {
// 模拟后端过滤菜单 // 模拟后端过滤菜单
routerMap = generateRoutesFn2(routers as AppCustomRouteRecordRaw[]) routerMap = generateRoutesByServer(routers as AppCustomRouteRecordRaw[])
} else if (type === 'test') { } else if (type === 'frontEnd') {
// 模拟前端过滤菜单 // 模拟前端过滤菜单
routerMap = generateRoutesFn1(cloneDeep(asyncRouterMap), routers as string[]) routerMap = generateRoutesByFrontEnd(cloneDeep(asyncRouterMap), routers as string[])
} else { } else {
// 直接读取静态路由表 // 直接读取静态路由表
routerMap = cloneDeep(asyncRouterMap) routerMap = cloneDeep(asyncRouterMap)

View File

@ -4,16 +4,24 @@ import { getRawRoute } from '@/utils/routerHelper'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { store } from '../index' import { store } from '../index'
import { findIndex } from '@/utils' import { findIndex } from '@/utils'
import { useStorage } from '@/hooks/web/useStorage'
import { useAppStoreWithOut } from './app'
const appStore = useAppStoreWithOut()
const { getStorage } = useStorage()
export interface TagsViewState { export interface TagsViewState {
visitedViews: RouteLocationNormalizedLoaded[] visitedViews: RouteLocationNormalizedLoaded[]
cachedViews: Set<string> cachedViews: Set<string>
selectedTag?: RouteLocationNormalizedLoaded
} }
export const useTagsViewStore = defineStore('tagsView', { export const useTagsViewStore = defineStore('tagsView', {
state: (): TagsViewState => ({ state: (): TagsViewState => ({
visitedViews: [], visitedViews: [],
cachedViews: new Set() cachedViews: new Set(),
selectedTag: undefined
}), }),
getters: { getters: {
getVisitedViews(): RouteLocationNormalizedLoaded[] { getVisitedViews(): RouteLocationNormalizedLoaded[] {
@ -21,6 +29,9 @@ export const useTagsViewStore = defineStore('tagsView', {
}, },
getCachedViews(): string[] { getCachedViews(): string[] {
return Array.from(this.cachedViews) return Array.from(this.cachedViews)
},
getSelectedTag(): RouteLocationNormalizedLoaded | undefined {
return this.selectedTag
} }
}, },
actions: { actions: {
@ -44,7 +55,7 @@ export const useTagsViewStore = defineStore('tagsView', {
const cacheMap: Set<string> = new Set() const cacheMap: Set<string> = new Set()
for (const v of this.visitedViews) { for (const v of this.visitedViews) {
const item = getRawRoute(v) const item = getRawRoute(v)
const needCache = !item.meta?.noCache const needCache = !item?.meta?.noCache
if (!needCache) { if (!needCache) {
continue continue
} }
@ -85,7 +96,9 @@ export const useTagsViewStore = defineStore('tagsView', {
// 删除所有tag // 删除所有tag
delAllVisitedViews() { delAllVisitedViews() {
// const affixTags = this.visitedViews.filter((tag) => tag.meta.affix) // 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) { delOthersViews(view: RouteLocationNormalizedLoaded) {
@ -131,6 +144,18 @@ export const useTagsViewStore = defineStore('tagsView', {
break 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
}
}
} }
} }
}) })

View File

@ -3,7 +3,6 @@ import type {
Router, Router,
RouteLocationNormalized, RouteLocationNormalized,
RouteRecordNormalized, RouteRecordNormalized,
RouteMeta,
RouteRecordRaw RouteRecordRaw
} from 'vue-router' } from 'vue-router'
import { isUrl } from '@/utils/is' import { isUrl } from '@/utils/is'
@ -39,7 +38,7 @@ export const getRawRoute = (route: RouteLocationNormalized): RouteLocationNormal
} }
// 前端控制路由生成 // 前端控制路由生成
export const generateRoutesFn1 = ( export const generateRoutesByFrontEnd = (
routes: AppRouteRecordRaw[], routes: AppRouteRecordRaw[],
keys: string[], keys: string[],
basePath = '/' basePath = '/'
@ -47,7 +46,7 @@ export const generateRoutesFn1 = (
const res: AppRouteRecordRaw[] = [] const res: AppRouteRecordRaw[] = []
for (const route of routes) { for (const route of routes) {
const meta = route.meta as RouteMeta const meta = route.meta ?? {}
// skip some route // skip some route
if (meta.hidden && !meta.canTo) { if (meta.hidden && !meta.canTo) {
continue continue
@ -70,7 +69,7 @@ export const generateRoutesFn1 = (
if (isUrl(item) && (onlyOneChild === item || route.path === item)) { if (isUrl(item) && (onlyOneChild === item || route.path === item)) {
data = Object.assign({}, route) data = Object.assign({}, route)
} else { } else {
const routePath = onlyOneChild ?? pathResolve(basePath, route.path) const routePath = (onlyOneChild ?? pathResolve(basePath, route.path)).trim()
if (routePath === item || meta.followRoute === item) { if (routePath === item || meta.followRoute === item) {
data = Object.assign({}, route) data = Object.assign({}, route)
} }
@ -79,7 +78,11 @@ export const generateRoutesFn1 = (
// recursive child routes // recursive child routes
if (route.children && data) { 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) { if (data) {
res.push(data as AppRouteRecordRaw) 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[] = [] const res: AppRouteRecordRaw[] = []
for (const route of routes) { for (const route of routes) {
@ -112,7 +115,7 @@ export const generateRoutesFn2 = (routes: AppCustomRouteRecordRaw[]): AppRouteRe
} }
// recursive child routes // recursive child routes
if (route.children) { if (route.children) {
data.children = generateRoutesFn2(route.children) data.children = generateRoutesByServer(route.children)
} }
res.push(data as AppRouteRecordRaw) res.push(data as AppRouteRecordRaw)
} }
@ -122,7 +125,7 @@ export const generateRoutesFn2 = (routes: AppCustomRouteRecordRaw[]): AppRouteRe
export const pathResolve = (parentPath: string, path: string) => { export const pathResolve = (parentPath: string, path: string) => {
if (isUrl(path)) return path if (isUrl(path)) return path
const childPath = path.startsWith('/') || !path ? path : `/${path}` const childPath = path.startsWith('/') || !path ? path : `/${path}`
return `${parentPath}${childPath}`.replace(/\/\//g, '/') return `${parentPath}${childPath}`.replace(/\/\//g, '/').trim()
} }
// 路由降级 // 路由降级

View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { JsonEditor } from '@/components/JsonEditor'
import { useI18n } from '@/hooks/web/useI18n'
import { ref, watch } from 'vue'
const { t } = useI18n()
const defaultData = ref({
title: '标题',
content: '内容'
})
watch(
() => defaultData.value,
(val) => {
console.log(val)
},
{
deep: true
}
)
setTimeout(() => {
defaultData.value = {
title: '异步标题',
content: '异步内容'
}
}, 4000)
</script>
<template>
<ContentWrap :title="t('richText.jsonEditor')" :message="t('richText.jsonEditorDes')">
<JsonEditor v-model="defaultData" />
</ContentWrap>
</template>

View File

@ -442,6 +442,15 @@ const treeSelectData = [
} }
] ]
//
const getTreeSelectData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(treeSelectData)
}, 3000)
})
}
let id = 0 let id = 0
const imageUrl = ref('') const imageUrl = ref('')
@ -1533,8 +1542,9 @@ const schema = reactive<FormSchema[]>([
label: `${t('formDemo.treeSelect')}`, label: `${t('formDemo.treeSelect')}`,
component: 'TreeSelect', component: 'TreeSelect',
// option // option
optionApi: () => { optionApi: async () => {
return treeSelectData const res = await getTreeSelectData()
return res
} }
}, },
{ {
@ -1750,6 +1760,20 @@ const schema = reactive<FormSchema[]>([
) )
} }
} }
},
{
field: 'field85',
component: 'Divider',
label: t('formDemo.jsonEditor')
},
{
field: 'field86',
component: 'JsonEditor',
label: t('formDemo.default'),
value: {
a: 1,
b: 2
}
} }
]) ])
</script> </script>

View File

@ -3,10 +3,15 @@ import { ContentWrap } from '@/components/ContentWrap'
import { ElInput } from 'element-plus' import { ElInput } from 'element-plus'
import { ref } from 'vue' import { ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { useTagsView } from '@/hooks/web/useTagsView'
const { setTitle } = useTagsView()
const { params } = useRoute() const { params } = useRoute()
const val = ref(params.id as string) const val = ref(params.id as string)
setTitle(`详情页-${val.value}`)
</script> </script>
<template> <template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
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 { query } = useRoute()
const val = ref(query.id as string)
setTitle(`详情页query-${val.value}`)
</script>
<template>
<ContentWrap> 获取参数 <ElInput v-model="val" /> </ContentWrap>
</template>

View File

@ -1,15 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap' import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { useIntro } from '@/hooks/web/useIntro'
import { ElButton } from 'element-plus' import { ElButton } from 'element-plus'
import { useGuide } from '@/hooks/web/useGuide'
const { t } = useI18n() const { t } = useI18n()
const { introRef } = useIntro() const { drive } = useGuide()
const guideStart = () => { const guideStart = () => {
introRef.start() drive()
} }
</script> </script>

View File

@ -205,13 +205,6 @@ watch(
// //
const signIn = async () => { const signIn = async () => {
await permissionStore.generateRoutes('none').catch(() => {})
permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 访
})
permissionStore.setIsAddRouters(true)
push({ path: redirect.value || permissionStore.addRouters[0].path })
const formRef = await getElFormExpose() const formRef = await getElFormExpose()
await formRef?.validate(async (isValid) => { await formRef?.validate(async (isValid) => {
if (isValid) { if (isValid) {
@ -227,7 +220,7 @@ const signIn = async () => {
if (appStore.getDynamicRouter) { if (appStore.getDynamicRouter) {
getRole() getRole()
} else { } else {
await permissionStore.generateRoutes('none').catch(() => {}) await permissionStore.generateRoutes('static').catch(() => {})
permissionStore.getAddRouters.forEach((route) => { permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 访 addRoute(route as RouteRecordRaw) // 访
}) })
@ -248,17 +241,16 @@ const getRole = async () => {
const params = { const params = {
roleName: formData.username roleName: formData.username
} }
// admin -
// test -
const res = const res =
formData.username === 'admin' ? await getAdminRoleApi(params) : await getTestRoleApi(params) appStore.getDynamicRouter && appStore.getServerDynamicRouter
? await getAdminRoleApi(params)
: await getTestRoleApi(params)
if (res) { if (res) {
const routers = res.data || [] const routers = res.data || []
setStorage('roleRouters', routers) setStorage('roleRouters', routers)
appStore.getDynamicRouter && appStore.getServerDynamicRouter
formData.username === 'admin' ? await permissionStore.generateRoutes('server', routers).catch(() => {})
? await permissionStore.generateRoutes('admin', routers).catch(() => {}) : await permissionStore.generateRoutes('frontEnd', routers).catch(() => {})
: await permissionStore.generateRoutes('test', routers).catch(() => {})
permissionStore.getAddRouters.forEach((route) => { permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 访 addRoute(route as RouteRecordRaw) // 访

View File

@ -1,223 +1,186 @@
<script setup lang="ts"> <script setup lang="ts">
// import { ContentWrap } from '@/components/ContentWrap' import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
// import { Search } from '@/components/Search' import { useI18n } from '@/hooks/web/useI18n'
// import { useI18n } from '@/hooks/web/useI18n' import { reactive } from 'vue'
// import { ElButton, ElTag } from 'element-plus' import { JsonEditor } from '@/components/JsonEditor'
// import { Table } from '@/components/Table' import { ContentWrap } from '@/components/ContentWrap'
// import { getTableListApi, delTableListApi } from '@/api/table' import { ElRow, ElCol } from 'element-plus'
// import { useTable } from '@/hooks/web/useTable'
// import { TableData } from '@/api/table/types'
// import { h, ref, reactive } from 'vue'
// import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
// import { useDictStore } from '@/store/modules/dict'
// import { getDictOneApi } from '@/api/common'
// import { TableColumn } from '@/types/table'
// const dictStore = useDictStore() const { t } = useI18n()
// const { register, tableObject, methods } = useTable<TableData>({ const crudSchemas = reactive<CrudSchema[]>([
// getListApi: getTableListApi, {
// delListApi: delTableListApi, field: 'selection',
// response: { search: {
// list: 'list', hidden: true
// total: 'total' },
// } form: {
// }) hidden: true
},
detail: {
hidden: true
},
table: {
type: 'selection'
}
},
{
field: 'index',
label: t('tableDemo.index'),
type: 'index',
search: {
hidden: true
},
form: {
hidden: true
},
detail: {
hidden: true
}
},
{
field: 'title',
label: t('tableDemo.title'),
search: {
component: 'Input'
},
form: {
component: 'Input',
colProps: {
span: 24
}
},
detail: {
span: 24
}
},
{
field: 'author',
label: t('tableDemo.author'),
search: {
hidden: true
}
},
{
field: 'display_time',
label: t('tableDemo.displayTime'),
search: {
hidden: true
},
form: {
component: 'DatePicker',
componentProps: {
type: 'datetime',
valueFormat: 'YYYY-MM-DD HH:mm:ss'
}
}
},
{
field: 'importance',
label: t('tableDemo.importance'),
search: {
hidden: true
},
form: {
component: 'Select',
componentProps: {
style: {
width: '100%'
},
options: [
{
label: '重要',
value: 3
},
{
label: '良好',
value: 2
},
{
label: '一般',
value: 1
}
]
}
}
},
{
field: 'pageviews',
label: t('tableDemo.pageviews'),
search: {
hidden: true
},
form: {
component: 'InputNumber',
value: 0
}
},
{
field: 'content',
label: t('exampleDemo.content'),
search: {
hidden: true
},
table: {
show: false
},
form: {
component: 'Editor',
colProps: {
span: 24
}
},
detail: {
span: 24
}
},
{
field: 'action',
width: '260px',
label: t('tableDemo.action'),
search: {
hidden: true
},
form: {
hidden: true
},
detail: {
hidden: true
}
}
])
// const { getList, setSearchParams } = methods const { allSchemas } = useCrudSchemas(crudSchemas)
// getList()
// const { t } = useI18n()
// const crudSchemas = reactive<CrudSchema[]>([
// {
// field: 'index',
// label: t('tableDemo.index'),
// type: 'index',
// form: {
// show: false
// },
// detail: {
// show: false
// }
// },
// {
// field: 'title',
// label: t('tableDemo.title'),
// search: {
// show: true
// },
// form: {
// colProps: {
// span: 24
// }
// },
// detail: {
// span: 24
// }
// },
// {
// field: 'author',
// label: t('tableDemo.author')
// },
// {
// field: 'display_time',
// label: t('tableDemo.displayTime'),
// form: {
// component: 'DatePicker',
// componentProps: {
// type: 'datetime',
// valueFormat: 'YYYY-MM-DD HH:mm:ss'
// }
// }
// },
// {
// field: 'importance',
// label: t('tableDemo.importance'),
// formatter: (_: Recordable, __: TableColumn, cellValue: number) => {
// return h(
// ElTag,
// {
// type: cellValue === 1 ? 'success' : cellValue === 2 ? 'warning' : 'danger'
// },
// () =>
// cellValue === 1
// ? t('tableDemo.important')
// : cellValue === 2
// ? t('tableDemo.good')
// : t('tableDemo.commonly')
// )
// },
// search: {
// show: true,
// component: 'Select',
// componentProps: {
// options: dictStore.getDictObj.importance
// }
// },
// form: {
// component: 'Select',
// componentProps: {
// options: [
// {
// label: '',
// value: 3
// },
// {
// label: '',
// value: 2
// },
// {
// label: '',
// value: 1
// }
// ]
// }
// }
// },
// {
// field: 'importance2',
// label: `${t('tableDemo.importance')}2`,
// search: {
// show: true,
// component: 'Select',
// dictName: 'importance'
// }
// },
// {
// field: 'importance3',
// label: `${t('tableDemo.importance')}3`,
// search: {
// show: true,
// component: 'Select',
// api: async () => {
// const res = await getDictOneApi()
// return res.data
// }
// }
// },
// {
// field: 'pageviews',
// label: t('tableDemo.pageviews'),
// form: {
// component: 'InputNumber',
// value: 0
// }
// },
// {
// field: 'content',
// label: t('exampleDemo.content'),
// table: {
// show: false
// },
// form: {
// component: 'Editor',
// colProps: {
// span: 24
// }
// },
// detail: {
// span: 24
// }
// },
// {
// field: 'action',
// width: '260px',
// label: t('tableDemo.action'),
// form: {
// show: false
// },
// detail: {
// show: false
// }
// }
// ])
// const { allSchemas } = useCrudSchemas(crudSchemas)
// const delLoading = ref(false)
// const delData = async (row: TableData | null, multiple: boolean) => {
// tableObject.currentRow = row
// const { delList, getSelections } = methods
// const selections = await getSelections()
// delLoading.value = true
// await delList(
// multiple ? selections.map((v) => v.id) : [tableObject.currentRow?.id as string],
// multiple
// ).finally(() => {
// delLoading.value = false
// })
// }
</script> </script>
<template> <template>
<ContentWrap> <ContentWrap title="useCrudSchemas">
<!-- <Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" /> <ElRow :gutter="20">
<ElCol :span="24">
<div class="mb-10px"> <ContentWrap title="原始数据数据" class="mt-20px">
<ElButton :loading="delLoading" type="danger" @click="delData(null, true)"> <JsonEditor v-model="crudSchemas" />
{{ t('exampleDemo.del') }} </ContentWrap>
</ElButton> </ElCol>
</div> <ElCol :span="24">
<ContentWrap title="查询组件数据结构" class="mt-20px">
<Table <JsonEditor v-model="allSchemas.searchSchema" />
v-model:pageSize="tableObject.pageSize" </ContentWrap>
v-model:currentPage="tableObject.currentPage" </ElCol>
:columns="allSchemas.tableColumns" <ElCol :span="24">
:data="tableObject.tableList" <ContentWrap title="表单组件数据结构" class="mt-20px">
:loading="tableObject.loading" <JsonEditor v-model="allSchemas.formSchema" />
:pagination="{ </ContentWrap>
total: tableObject.total </ElCol>
}" <ElCol :span="24">
@register="register" <ContentWrap title="表格组件数据结构" class="mt-20px">
> <JsonEditor v-model="allSchemas.tableColumns" />
<template #action="{ row }"> </ContentWrap>
<ElButton type="danger" @click="delData(row, false)"> </ElCol>
{{ t('exampleDemo.del') }} <ElCol :span="24">
</ElButton> <ContentWrap title="表格组件数据结构" class="mt-20px">
</template> <JsonEditor v-model="allSchemas.detailSchema" />
</Table> --> </ContentWrap>
</ElCol>
</ElRow>
</ContentWrap> </ContentWrap>
</template> </template>

View File

@ -0,0 +1,60 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { ElButton } from 'element-plus'
import { useTagsView } from '@/hooks/web/useTagsView'
import { useRouter } from 'vue-router'
const { push } = useRouter()
const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage, setTitle } =
useTagsView()
const closeAllTabs = () => {
closeAll(() => {
push('/dashboard/analysis')
})
}
const closeLeftTabs = () => {
closeLeft()
}
const closeRightTabs = () => {
closeRight()
}
const closeOtherTabs = () => {
closeOther()
}
const refresh = () => {
refreshPage()
}
const closeCurrentTab = () => {
closeCurrent(undefined, () => {
push('/dashboard/analysis')
})
}
const setTabTitle = () => {
setTitle(new Date().getTime().toString())
}
const setAnalysisTitle = () => {
setTitle(`分析页-${new Date().getTime().toString()}`, '/dashboard/analysis')
}
</script>
<template>
<ContentWrap title="useTagsView">
<ElButton type="primary" @click="closeAllTabs"> 关闭所有标签页 </ElButton>
<ElButton type="primary" @click="closeLeftTabs"> 关闭左侧标签页 </ElButton>
<ElButton type="primary" @click="closeRightTabs"> 关闭右侧标签页 </ElButton>
<ElButton type="primary" @click="closeOtherTabs"> 关闭其他标签页 </ElButton>
<ElButton type="primary" @click="closeCurrentTab"> 关闭当前标签页 </ElButton>
<ElButton type="primary" @click="refresh"> 刷新当前标签页 </ElButton>
<ElButton type="primary" @click="setTabTitle"> 修改当前标题 </ElButton>
<ElButton type="primary" @click="setAnalysisTitle"> 修改分析页标题 </ElButton>
</ContentWrap>
</template>

View File

@ -0,0 +1,80 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { Form, FormSchema } from '@/components/Form'
import { useValidator } from '@/hooks/web/useValidator'
import { useForm } from '@/hooks/web/useForm'
import { reactive } from 'vue'
import { FormItemRule } from 'element-plus'
const { formRegister, formMethods } = useForm()
const { getFormData } = formMethods
const { required, lengthRange, notSpace, notSpecialCharacters } = useValidator()
const formSchema = reactive<FormSchema[]>([
{
field: 'field1',
label: '必填',
component: 'Input'
},
{
field: 'field2',
label: '长度范围',
component: 'Input'
},
{
field: 'field3',
label: '不能有空格',
component: 'Input'
},
{
field: 'field4',
label: '不能有特殊字符',
component: 'Input'
},
{
field: 'field5',
label: '是否相等-值1',
component: 'Input'
},
{
field: 'field6',
label: '是否相等-值2',
component: 'Input'
}
])
const rules = reactive<{
[key: string]: FormItemRule[]
}>({
field1: [required()],
field2: [
lengthRange({
min: 2,
max: 5
})
],
field3: [notSpace()],
field4: [notSpecialCharacters()],
field5: [
{
asyncValidator: async (_, val, callback) => {
const formData = await getFormData()
const { field6 } = formData
if (val !== field6) {
callback(new Error('两个值不相等'))
} else {
callback()
}
}
}
]
})
</script>
<template>
<ContentWrap title="useValidator">
<Form :schema="formSchema" :rules="rules" @register="formRegister" />
</ContentWrap>
</template>

View File

@ -27,10 +27,8 @@
"@intlify/unplugin-vue-i18n/types", "@intlify/unplugin-vue-i18n/types",
"vite/client", "vite/client",
"element-plus/global", "element-plus/global",
"@types/intro.js",
"@types/qrcode", "@types/qrcode",
"vite-plugin-svg-icons/client", "vite-plugin-svg-icons/client"
"unplugin-vue-define-options/macros-global"
] ]
}, },
"include": ["src", "types/**/*.d.ts", "mock/**/*.ts"] "include": ["src", "types/**/*.d.ts", "mock/**/*.ts"]

14
types/router.d.ts vendored
View File

@ -32,8 +32,8 @@ import { defineComponent } from 'vue'
permission: ['edit','add', 'delete'] permission: ['edit','add', 'delete']
} }
**/ **/
declare module 'vue-router' {
interface RouteMeta extends Record<string | number | symbol, unknown> { interface RouteMetaCustom extends Record<string | number | symbol, unknown> {
hidden?: boolean hidden?: boolean
alwaysShow?: boolean alwaysShow?: boolean
title?: string title?: string
@ -43,10 +43,12 @@ declare module 'vue-router' {
affix?: boolean affix?: boolean
activeMenu?: string activeMenu?: string
noTagsView?: boolean noTagsView?: boolean
followAuth?: string
canTo?: boolean canTo?: boolean
permission?: string[] permission?: string[]
} }
declare module 'vue-router' {
interface RouteMeta extends RouteMetaCustom {}
} }
type Component<T = any> = type Component<T = any> =
@ -57,7 +59,7 @@ type Component<T = any> =
declare global { declare global {
declare interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> { declare interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
name: string name: string
meta: RouteMeta meta: RouteMetaCustom
component?: Component | string component?: Component | string
children?: AppRouteRecordRaw[] children?: AppRouteRecordRaw[]
props?: Recordable props?: Recordable
@ -66,7 +68,7 @@ declare global {
declare interface AppCustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> { declare interface AppCustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
name: string name: string
meta: RouteMeta meta: RouteMetaCustom
component: string component: string
path: string path: string
redirect: string redirect: string

View File

@ -10,7 +10,6 @@ import { viteMockServe } from 'vite-plugin-mock'
import PurgeIcons from 'vite-plugin-purge-icons' import PurgeIcons from 'vite-plugin-purge-icons'
import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite" import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite"
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import DefineOptions from "unplugin-vue-define-options/vite"
import { createStyleImportPlugin, ElementPlusResolve } from 'vite-plugin-style-import' import { createStyleImportPlugin, ElementPlusResolve } from 'vite-plugin-style-import'
import UnoCSS from 'unocss/vite' import UnoCSS from 'unocss/vite'
@ -32,7 +31,12 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
return { return {
base: env.VITE_BASE_PATH, base: env.VITE_BASE_PATH,
plugins: [ plugins: [
Vue(), Vue({
script: {
// 开启defineModel
defineModel: true
}
}),
VueJsx(), VueJsx(),
// WindiCSS(), // WindiCSS(),
progress(), progress(),
@ -75,7 +79,6 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
setupProdMockServer() setupProdMockServer()
` `
}), }),
DefineOptions(),
ViteEjsPlugin({ ViteEjsPlugin({
title: env.VITE_APP_TITLE title: env.VITE_APP_TITLE
}), }),
@ -147,7 +150,8 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
'intro.js', 'intro.js',
'qrcode', 'qrcode',
'@wangeditor/editor', '@wangeditor/editor',
'@wangeditor/editor-for-vue' '@wangeditor/editor-for-vue',
'vue-json-pretty'
] ]
} }
} }