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://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)
<img src="https://github.com/kailong321200875/my-image/raw/master/pay.jpg" />
## 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

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://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)
<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,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 {

View File

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

View File

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

View File

@ -76,8 +76,10 @@ export default [
}
if (!hasUser) {
return {
code: 500,
message: '账号或密码错误'
data: {
code: 500,
message: '账号或密码错误'
}
}
}
}

View File

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

View File

@ -30,5 +30,5 @@ export const getAdminRoleApi = (
}
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 { 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<AppRouteRecordRaw[]>(unref(levelList))
return breadcrumbList.map((v) => {
const disabled = !v.redirect || v.redirect === 'noredirect'
const meta = v.meta as RouteMeta
const meta = v.meta
return (
<ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}>
{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>
)

View File

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

View File

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

View File

@ -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<Component, ComponentName> = {
@ -47,7 +48,8 @@ const componentMap: Recordable<Component, ComponentName> = {
InputPassword: InputPassword,
Editor: Editor,
TreeSelect: ElTreeSelect,
Upload: ElUpload
Upload: ElUpload,
JsonEditor: JsonEditor
}
export { componentMap }

View File

@ -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
/**

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 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<AppRouteRecordRaw>(allRouters, v.path).join('/')
const { renderMenuTitle } = useRenderMenuTitle()
if (
oneShowingChild &&
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&

View File

@ -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<OnlyOneChildType>()
const showingChildren = children.filter((v) => {
const meta = (v.meta ?? {}) as RouteMeta
const meta = v.meta ?? {}
if (meta.hidden) {
return false
} else {

View File

@ -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(
<ElSwitch v-model="dynamicRouter" @change="dynamicRouterChange" />
</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">
<span class="text-14px">{{ t('setting.fixedMenu') }}</span>
<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]
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 () => (
<div
id={`${variables.namespace}-menu`}
@ -147,7 +143,6 @@ export default defineComponent({
'w-[var(--tab-menu-min-width)]': unref(collapse)
}
]}
onMouseleave={mouseleave}
>
<ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height)-1px)]">
<div>
@ -178,7 +173,7 @@ export default defineComponent({
<Icon icon={item?.meta?.icon}></Icon>
</div>
{!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>
)
@ -197,11 +192,11 @@ export default defineComponent({
</div>
<Menu
class={[
'!absolute top-0',
'!absolute top-0 z-4000',
{
'!left-[var(--tab-menu-min-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)
}
]}

View File

@ -1,5 +1,4 @@
import { getAllParentPath } from '@/components/Menu/src/helper'
import type { RouteMeta } from 'vue-router'
import { isUrl } from '@/utils/is'
import { cloneDeep } from 'lodash-es'
import { reactive } from 'vue'
@ -12,7 +11,7 @@ export const tabPathMap = reactive<TabMapTypes>({})
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<AppRouteRecordRaw> = null
const meta = (v.meta ?? {}) as RouteMeta
const meta = v.meta ?? {}
if (!meta.hidden || meta.canTo) {
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 { 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<RouteLocationNormalizedLoaded[]>([])
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<RouteLocationNormalizedLoaded>()
// 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(
}
}
</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'
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)

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') => {
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 {

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

View File

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

View File

@ -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: '弹窗',

View File

@ -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) => {

View File

@ -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'
// }
// }
]
},
{

View File

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

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 { 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<unknown> {
return new Promise<void>((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)

View File

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

View File

@ -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()
}
// 路由降级

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
const imageUrl = ref('')
@ -1533,8 +1542,9 @@ const schema = reactive<FormSchema[]>([
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<FormSchema[]>([
)
}
}
},
{
field: 'field85',
component: 'Divider',
label: t('formDemo.jsonEditor')
},
{
field: 'field86',
component: 'JsonEditor',
label: t('formDemo.default'),
value: {
a: 1,
b: 2
}
}
])
</script>

View File

@ -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}`)
</script>
<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">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { useIntro } from '@/hooks/web/useIntro'
import { ElButton } from 'element-plus'
import { useGuide } from '@/hooks/web/useGuide'
const { t } = useI18n()
const { introRef } = useIntro()
const { drive } = useGuide()
const guideStart = () => {
introRef.start()
drive()
}
</script>

View File

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

View File

@ -1,223 +1,186 @@
<script setup lang="ts">
// import { ContentWrap } from '@/components/ContentWrap'
// import { Search } from '@/components/Search'
// import { useI18n } from '@/hooks/web/useI18n'
// import { ElButton, ElTag } from 'element-plus'
// import { Table } from '@/components/Table'
// import { getTableListApi, delTableListApi } from '@/api/table'
// 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'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { useI18n } from '@/hooks/web/useI18n'
import { reactive } from 'vue'
import { JsonEditor } from '@/components/JsonEditor'
import { ContentWrap } from '@/components/ContentWrap'
import { ElRow, ElCol } from 'element-plus'
// const dictStore = useDictStore()
const { t } = useI18n()
// const { register, tableObject, methods } = useTable<TableData>({
// getListApi: getTableListApi,
// delListApi: delTableListApi,
// response: {
// list: 'list',
// total: 'total'
// }
// })
const crudSchemas = reactive<CrudSchema[]>([
{
field: 'selection',
search: {
hidden: true
},
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
// 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
// })
// }
const { allSchemas } = useCrudSchemas(crudSchemas)
</script>
<template>
<ContentWrap>
<!-- <Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
<div class="mb-10px">
<ElButton :loading="delLoading" type="danger" @click="delData(null, true)">
{{ t('exampleDemo.del') }}
</ElButton>
</div>
<Table
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
:columns="allSchemas.tableColumns"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
@register="register"
>
<template #action="{ row }">
<ElButton type="danger" @click="delData(row, false)">
{{ t('exampleDemo.del') }}
</ElButton>
</template>
</Table> -->
<ContentWrap title="useCrudSchemas">
<ElRow :gutter="20">
<ElCol :span="24">
<ContentWrap title="原始数据数据" class="mt-20px">
<JsonEditor v-model="crudSchemas" />
</ContentWrap>
</ElCol>
<ElCol :span="24">
<ContentWrap title="查询组件数据结构" class="mt-20px">
<JsonEditor v-model="allSchemas.searchSchema" />
</ContentWrap>
</ElCol>
<ElCol :span="24">
<ContentWrap title="表单组件数据结构" class="mt-20px">
<JsonEditor v-model="allSchemas.formSchema" />
</ContentWrap>
</ElCol>
<ElCol :span="24">
<ContentWrap title="表格组件数据结构" class="mt-20px">
<JsonEditor v-model="allSchemas.tableColumns" />
</ContentWrap>
</ElCol>
<ElCol :span="24">
<ContentWrap title="表格组件数据结构" class="mt-20px">
<JsonEditor v-model="allSchemas.detailSchema" />
</ContentWrap>
</ElCol>
</ElRow>
</ContentWrap>
</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",
"vite/client",
"element-plus/global",
"@types/intro.js",
"@types/qrcode",
"vite-plugin-svg-icons/client",
"unplugin-vue-define-options/macros-global"
"vite-plugin-svg-icons/client"
]
},
"include": ["src", "types/**/*.d.ts", "mock/**/*.ts"]

34
types/router.d.ts vendored
View File

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

View File

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