Browse Source

Merge branch 'v2.0' into websiteManagement

王飞 1 year ago
parent
commit
b805e9f8df

+ 4 - 2
README.md

@@ -1,6 +1,6 @@
 <div align="center"> <a href="https://github.com/kailong321200875/vue-element-plus-admin"> <img width="100" src="./public/logo.png"> </a> <br> <br>
 
-[![license](https://img.shields.io/github/license/kailong321200875/vue-element-plus-admin.svg)](LICENSE) [![repo-size](https://img.shields.io/github/repo-size/kailong321200875/vue-element-plus-admin.svg)](repo-size) [![last-commit](https://img.shields.io/github/last-commit/kailong321200875/vue-element-plus-admin.svg)](last-commit) [![stars](https://img.shields.io/github/stars/kailong321200875/vue-element-plus-admin.svg)](stars) [![forks](https://img.shields.io/github/forks/kailong321200875/vue-element-plus-admin.svg)](forks) [![release](https://img.shields.io/github/release/kailong321200875/vue-element-plus-admin.svg)](release) [![downloads](https://img.shields.io/github/downloads/kailong321200875/vue-element-plus-admin/total.svg)](downloads) [![watchers](https://img.shields.io/github/watchers/kailong321200875/vue-element-plus-admin.svg)](watchers)
+[![license](https://img.shields.io/github/license/kailong321200875/vue-element-plus-admin.svg)](LICENSE) [![repo-size](https://img.shields.io/github/repo-size/kailong321200875/vue-element-plus-admin.svg)](repo-size) [![last-commit](https://img.shields.io/github/last-commit/kailong321200875/vue-element-plus-admin.svg)](last-commit) [![stars](https://img.shields.io/github/stars/kailong321200875/vue-element-plus-admin.svg)](stars) [![forks](https://img.shields.io/github/forks/kailong321200875/vue-element-plus-admin.svg)](forks) [![release](https://img.shields.io/github/release/kailong321200875/vue-element-plus-admin.svg)](release) [![watchers](https://img.shields.io/github/watchers/kailong321200875/vue-element-plus-admin.svg)](watchers)
 
 <h1>vue-element-plus-admin</h1>
 </div>
@@ -133,11 +133,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.jpg" />
+<img src="https://github.com/kailong321200875/my-image/raw/master/chat-0820.jpg" />
 
 ## License
 

+ 4 - 2
README.zh-CN.md

@@ -1,6 +1,6 @@
 <div align="center"> <a href="https://github.com/kailong321200875/vue-element-plus-admin"> <img width="100" src="./public/logo.png"> </a> <br> <br>
 
-[![license](https://img.shields.io/github/license/kailong321200875/vue-element-plus-admin.svg)](LICENSE) [![repo-size](https://img.shields.io/github/repo-size/kailong321200875/vue-element-plus-admin.svg)](repo-size) [![last-commit](https://img.shields.io/github/last-commit/kailong321200875/vue-element-plus-admin.svg)](last-commit) [![stars](https://img.shields.io/github/stars/kailong321200875/vue-element-plus-admin.svg)](stars) [![forks](https://img.shields.io/github/forks/kailong321200875/vue-element-plus-admin.svg)](forks) [![release](https://img.shields.io/github/release/kailong321200875/vue-element-plus-admin.svg)](release) [![downloads](https://img.shields.io/github/downloads/kailong321200875/vue-element-plus-admin/total.svg)](downloads) [![watchers](https://img.shields.io/github/watchers/kailong321200875/vue-element-plus-admin.svg)](watchers)
+[![license](https://img.shields.io/github/license/kailong321200875/vue-element-plus-admin.svg)](LICENSE) [![repo-size](https://img.shields.io/github/repo-size/kailong321200875/vue-element-plus-admin.svg)](repo-size) [![last-commit](https://img.shields.io/github/last-commit/kailong321200875/vue-element-plus-admin.svg)](last-commit) [![stars](https://img.shields.io/github/stars/kailong321200875/vue-element-plus-admin.svg)](stars) [![forks](https://img.shields.io/github/forks/kailong321200875/vue-element-plus-admin.svg)](forks) [![release](https://img.shields.io/github/release/kailong321200875/vue-element-plus-admin.svg)](release) [![watchers](https://img.shields.io/github/watchers/kailong321200875/vue-element-plus-admin.svg)](watchers)
 
 <h1>vue-element-plus-admin</h1>
 </div>
@@ -133,11 +133,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.jpg" />
+<img src="https://gitee.com/kailong110120130/my-image/raw/master/chat-0820.jpg" />
 
 ## 许可证
 

+ 4 - 1
mock/role/index.ts

@@ -295,9 +295,12 @@ const testList: string[] = [
   '/components/infotip',
   '/Components/InputPassword',
   '/Components/Sticky',
+  'function',
+  '/function/multiple-tabs',
+  '/function/multiple-tabs-demo/:id',
   '/hooks',
   '/hooks/useWatermark',
-  '/hooks/useOpenTab',
+  '/hooks/useTagsView',
   // '/hooks/useCrudSchemas',
   '/level',
   '/level/menu1',

+ 27 - 27
package.json

@@ -28,7 +28,7 @@
   "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",
@@ -37,13 +37,13 @@
     "dayjs": "^1.11.9",
     "echarts": "^5.4.3",
     "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",
     "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",
@@ -52,55 +52,55 @@
     "vue": "3.3.4",
     "vue-i18n": "9.2.2",
     "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",
+    "@vue-macros/volar": "^0.13.3",
     "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",
+    "unplugin-vue-define-options": "^1.3.15",
+    "vite": "4.4.9",
     "vite-plugin-ejs": "^1.6.4",
     "vite-plugin-eslint": "^1.8.1",
     "vite-plugin-mock": "2.9.6",
@@ -108,7 +108,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"

+ 2 - 2
src/api/login/index.ts

@@ -9,8 +9,8 @@ export const loginApi = (data: UserType): Promise<IResponse<UserType>> => {
   return request.post({ url: '/api/login/login', data, headersType: 'application/json' })
 }
 
-export const loginOutApi = (userId: string): Promise<IResponse> => {
-  return request.get({ url: `/api/login/logout/${userId}` })
+export const loginOutApi = (): Promise<IResponse> => {
+  return request.get({ url: '/user/loginOut' })
 }
 
 export const getUserListApi = ({ params }: AxiosConfig) => {

+ 2 - 2
src/api/login/types.ts

@@ -1,10 +1,10 @@
 export type UserLoginType = {
-  account: string
+  username: string
   password: string
 }
 
 export type UserType = {
-  account: string
+  username: string
   password: string
   role: string
   roleId: string

+ 2 - 2
src/components/Menu/src/components/useRenderMenuItem.tsx

@@ -6,6 +6,8 @@ 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'
@@ -17,8 +19,6 @@ export const useRenderMenuItem = (
         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) &&

+ 1 - 1
src/components/TabMenu/src/TabMenu.vue

@@ -141,7 +141,7 @@ export default defineComponent({
         id={`${variables.namespace}-menu`}
         class={[
           prefixCls,
-          'relative bg-[var(--left-menu-bg-color)] top-1px z-3000 layout-border__right',
+          'relative bg-[var(--left-menu-bg-color)] top-1px layout-border__right',
           {
             'w-[var(--tab-menu-max-width)]': !unref(collapse),
             'w-[var(--tab-menu-min-width)]': unref(collapse)

+ 4 - 4
src/components/Table/src/Table.vue

@@ -362,7 +362,7 @@ export default defineComponent({
             return children && children.length
               ? renderTreeTableColumn(children)
               : props?.slots?.default
-              ? props.slots.default(args)
+              ? props.slots.default(...args)
               : v?.formatter
               ? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
               : isImageUrl
@@ -371,7 +371,7 @@ export default defineComponent({
           }
         }
         if (props?.slots?.header) {
-          slots['header'] = (...args: any[]) => props.slots.header(args)
+          slots['header'] = (...args: any[]) => props.slots.header(...args)
         }
 
         return (
@@ -459,7 +459,7 @@ export default defineComponent({
               return children && children.length
                 ? renderTreeTableColumn(children)
                 : props?.slots?.default
-                ? props.slots.default(args)
+                ? props.slots.default(...args)
                 : v?.formatter
                 ? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
                 : isImageUrl
@@ -468,7 +468,7 @@ export default defineComponent({
             }
           }
           if (props?.slots?.header) {
-            slots['header'] = (...args: any[]) => props.slots.header(args)
+            slots['header'] = (...args: any[]) => props.slots.header(...args)
           }
 
           return (

+ 42 - 42
src/components/TagsView/src/TagsView.vue

@@ -12,6 +12,8 @@ import { useDesign } from '@/hooks/web/useDesign'
 import { useTemplateRefsList } from '@vueuse/core'
 import { ElScrollbar } from 'element-plus'
 import { useScrollTo } from '@/hooks/event/useScrollTo'
+import { useTagsView } from '@/hooks/web/useTagsView'
+import { cloneDeep } from 'lodash-es'
 
 const { getPrefixCls } = useDesign()
 
@@ -19,7 +21,9 @@ const prefixCls = getPrefixCls('tags-view')
 
 const { t } = useI18n()
 
-const { currentRoute, push, replace } = useRouter()
+const { currentRoute, push } = useRouter()
+
+const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage } = useTagsView()
 
 const permissionStore = usePermissionStore()
 
@@ -31,6 +35,10 @@ const visitedViews = computed(() => tagsViewStore.getVisitedViews)
 
 const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([])
 
+const selectedTag = computed(() => tagsViewStore.getSelectedTag)
+
+const setSelectTag = tagsViewStore.setSelectedTag
+
 const appStore = useAppStore()
 
 const tagsViewIcon = computed(() => appStore.getTagsViewIcon)
@@ -43,82 +51,73 @@ 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()
+  closeCurrent(view, () => {
+    if (isActive(view)) {
+      toLastView()
+    }
+  })
+}
+
+// 去最后一个
+const toLastView = () => {
+  const visitedViews = tagsViewStore.getVisitedViews
+  const latestView = visitedViews.slice(-1)[0]
+  if (latestView) {
+    push(latestView)
+  } else {
+    if (
+      unref(currentRoute).path === permissionStore.getAddRouters[0].path ||
+      unref(currentRoute).path === permissionStore.getAddRouters[0].redirect
+    ) {
+      addTags()
+      return
+    }
+    // You can set another route
+    push(permissionStore.getAddRouters[0].path)
   }
 }
 
 // 关闭全部
 const closeAllTags = () => {
-  tagsViewStore.delAllViews()
-  toLastView()
+  closeAll(() => {
+    toLastView()
+  })
 }
 
 // 关闭其它
 const closeOthersTags = () => {
-  tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
+  closeOther()
 }
 
 // 重新加载
 const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => {
-  if (!view) return
-  tagsViewStore.delCachedView()
-  const { path, query } = view
-  await nextTick()
-  replace({
-    path: '/redirect' + path,
-    query: query
-  })
+  refreshPage(view)
 }
 
 // 关闭左侧
 const closeLeftTags = () => {
-  tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
+  closeLeft()
 }
 
 // 关闭右侧
 const closeRightTags = () => {
-  tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
-}
-
-// 跳转到最后一个
-const toLastView = () => {
-  const visitedViews = tagsViewStore.getVisitedViews
-  const latestView = visitedViews.slice(-1)[0]
-  if (latestView) {
-    push(latestView)
-  } else {
-    if (
-      unref(currentRoute).path === permissionStore.getAddRouters[0].path ||
-      unref(currentRoute).path === permissionStore.getAddRouters[0].redirect
-    ) {
-      addTags()
-      return
-    }
-    // You can set another route
-    push(permissionStore.getAddRouters[0].path)
-  }
+  closeRight()
 }
 
 // 滚动到选中的tag
@@ -583,3 +582,4 @@ watch(
   }
 }
 </style>
+@/hooks/web/useTagsView

+ 5 - 12
src/components/UserInfo/src/UserInfo.vue

@@ -11,12 +11,9 @@ import LockDialog from './components/LockDialog.vue'
 import { ref, computed } from 'vue'
 import LockPage from './components/LockPage.vue'
 import { useLockStore } from '@/store/modules/lock'
-import { useAppStore } from '@/store/modules/app'
 
 const lockStore = useLockStore()
 
-const appStore = useAppStore()
-
 const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
 
 const tagsViewStore = useTagsViewStore()
@@ -27,12 +24,10 @@ const prefixCls = getPrefixCls('user-info')
 
 const { t } = useI18n()
 
-const { clear, getStorage } = useStorage()
+const { clear } = useStorage()
 
 const { replace } = useRouter()
 
-const userInfo = ref(getStorage(appStore.userInfo))
-
 const loginOut = () => {
   ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), {
     confirmButtonText: t('common.ok'),
@@ -40,7 +35,7 @@ const loginOut = () => {
     type: 'warning'
   })
     .then(async () => {
-      const res = await loginOutApi(userInfo.value.id).catch(() => {})
+      const res = await loginOutApi().catch(() => {})
       if (res) {
         clear()
         tagsViewStore.delAllViews()
@@ -66,14 +61,12 @@ const lockScreen = () => {
 <template>
   <ElDropdown class="custom-hover" :class="prefixCls" trigger="click">
     <div class="flex items-center">
-      <!-- <img
+      <img
         src="@/assets/imgs/avatar.jpg"
         alt=""
         class="w-[calc(var(--logo-height)-25px)] rounded-[50%]"
-      /> -->
-      <span class="<lg:hidden text-14px pl-[5px] text-[var(--top-header-text-color)]">{{
-        userInfo.name
-      }}</span>
+      />
+      <span class="<lg:hidden text-14px pl-[5px] text-[var(--top-header-text-color)]">Archer</span>
     </div>
     <template #dropdown>
       <ElDropdownMenu>

+ 1 - 1
src/config/axios/config.ts

@@ -36,7 +36,7 @@ const config: AxiosConfig = {
   /**
    * 接口成功返回状态码
    */
-  code: '200',
+  code: 0,
 
   /**
    * 接口请求超时时间

+ 1 - 1
src/config/axios/service.ts

@@ -27,7 +27,7 @@ axiosInstance.interceptors.response.use(
   (res: AxiosResponse) => {
     const url = res.config.url || ''
     abortControllerMap.delete(url)
-    return res
+    return res.data
   },
   (err: any) => err
 )

+ 1 - 1
src/config/axios/types/index.ts

@@ -22,7 +22,7 @@ interface AxiosConfig<T = AxiosResponse> {
     pro: string
     test: string
   }
-  code: string
+  code: number
   defaultHeaders: AxiosHeaders
   timeout: number
   interceptors: RequestInterceptors<T>

+ 63 - 0
src/hooks/web/useTagsView.ts

@@ -0,0 +1,63 @@
+import { useTagsViewStoreWithOut } from '@/store/modules/tagsView'
+import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router'
+import { computed, nextTick, unref } from 'vue'
+
+export const useTagsView = () => {
+  const tagsViewStore = useTagsViewStoreWithOut()
+
+  const { replace, currentRoute } = useRouter()
+
+  const selectedTag = computed(() => tagsViewStore.getSelectedTag)
+
+  const closeAll = (callback?: Fn) => {
+    tagsViewStore.delAllViews()
+    callback?.()
+  }
+
+  const closeLeft = (callback?: Fn) => {
+    tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
+    callback?.()
+  }
+
+  const closeRight = (callback?: Fn) => {
+    tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
+    callback?.()
+  }
+
+  const closeOther = (callback?: Fn) => {
+    tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
+    callback?.()
+  }
+
+  const closeCurrent = (view?: RouteLocationNormalizedLoaded, callback?: Fn) => {
+    if (view?.meta?.affix) return
+    tagsViewStore.delView(view || unref(currentRoute))
+
+    callback?.()
+  }
+
+  const refreshPage = async (view?: RouteLocationNormalizedLoaded, callback?: Fn) => {
+    tagsViewStore.delCachedView()
+    const { path, query } = view || unref(currentRoute)
+    await nextTick()
+    replace({
+      path: '/redirect' + path,
+      query: query
+    })
+    callback?.()
+  }
+
+  const setTitle = (title: string, path?: string) => {
+    tagsViewStore.setTitle(title, path)
+  }
+
+  return {
+    closeAll,
+    closeLeft,
+    closeRight,
+    closeOther,
+    closeCurrent,
+    refreshPage,
+    setTitle
+  }
+}

+ 4 - 1
src/locales/en.ts

@@ -164,7 +164,10 @@ export default {
     department: 'Department management',
     menuManagement: 'Menu management',
     // 权限测试页面
-    permission: 'Permission test page'
+    permission: 'Permission test page',
+    function: 'Function',
+    multipleTabs: 'Multiple tabs',
+    details: 'Details'
   },
   permission: {
     hasPermission: 'Please set the operation permission value'

+ 4 - 1
src/locales/zh-CN.ts

@@ -163,7 +163,10 @@ export default {
     PicturePreview: '表格图片预览',
     department: '部门管理',
     menuManagement: '菜单管理',
-    permission: '权限测试页'
+    permission: '权限测试页',
+    function: '功能',
+    multipleTabs: '多开标签页',
+    details: '详情页'
   },
   permission: {
     hasPermission: '请设置操作权限值'

+ 12 - 2
src/router/index.ts

@@ -10,7 +10,7 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
   {
     path: '/',
     component: Layout,
-    redirect: '/dashboard/workplace',
+    redirect: '/dashboard/analysis',
     name: 'Root',
     meta: {
       hidden: true
@@ -59,7 +59,7 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
   {
     path: '/dashboard',
     component: Layout,
-    redirect: '/dashboard/workplace',
+    redirect: '/dashboard/analysis',
     name: 'Dashboard',
     meta: {
       title: t('router.dashboard'),
@@ -67,6 +67,16 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
       alwaysShow: true
     },
     children: [
+      {
+        path: 'analysis',
+        component: () => import('@/views/Dashboard/Analysis.vue'),
+        name: 'Analysis',
+        meta: {
+          title: t('router.analysis'),
+          noCache: true,
+          affix: true
+        }
+      },
       {
         path: 'workplace',
         component: () => import('@/views/Dashboard/Workplace.vue'),

+ 19 - 2
src/store/modules/tagsView.ts

@@ -8,12 +8,14 @@ import { findIndex } from '@/utils'
 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 +23,9 @@ export const useTagsViewStore = defineStore('tagsView', {
     },
     getCachedViews(): string[] {
       return Array.from(this.cachedViews)
+    },
+    getSelectedTag(): RouteLocationNormalizedLoaded | undefined {
+      return this.selectedTag
     }
   },
   actions: {
@@ -85,7 +90,7 @@ export const useTagsViewStore = defineStore('tagsView', {
     // 删除所有tag
     delAllVisitedViews() {
       // const affixTags = this.visitedViews.filter((tag) => tag.meta.affix)
-      this.visitedViews = []
+      this.visitedViews = this.visitedViews.filter((tag) => tag.meta?.affix)
     },
     // 删除其它
     delOthersViews(view: RouteLocationNormalizedLoaded) {
@@ -131,6 +136,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
+        }
+      }
     }
   }
 })

+ 4 - 4
src/views/Authorization/Department/Department.vue

@@ -126,7 +126,7 @@ const crudSchemas = reactive<CrudSchema[]>([
     table: {
       slots: {
         default: (data: any) => {
-          const status = data[0].row.status
+          const status = data.row.status
           return (
             <>
               <ElTag type={status === 0 ? 'danger' : 'success'}>
@@ -222,13 +222,13 @@ const crudSchemas = reactive<CrudSchema[]>([
         default: (data: any) => {
           return (
             <>
-              <ElButton type="primary" onClick={() => action(data[0].row, 'edit')}>
+              <ElButton type="primary" onClick={() => action(data.row, 'edit')}>
                 {t('exampleDemo.edit')}
               </ElButton>
-              <ElButton type="success" onClick={() => action(data[0].row, 'detail')}>
+              <ElButton type="success" onClick={() => action(data.row, 'detail')}>
                 {t('exampleDemo.detail')}
               </ElButton>
-              <ElButton type="danger" onClick={() => delData(data[0].row)}>
+              <ElButton type="danger" onClick={() => delData(data.row)}>
                 {t('exampleDemo.del')}
               </ElButton>
             </>

+ 6 - 6
src/views/Authorization/Menu/Menu.vue

@@ -44,7 +44,7 @@ const tableColumns = reactive<TableColumn[]>([
     width: 80,
     slots: {
       default: (data: any) => {
-        const icon = data[0].row.meta.icon
+        const icon = data.row.meta.icon
         if (icon) {
           return (
             <>
@@ -62,7 +62,7 @@ const tableColumns = reactive<TableColumn[]>([
     label: t('menu.permission'),
     slots: {
       default: (data: any) => {
-        const permission = data[0].row.meta.permission
+        const permission = data.row.meta.permission
         return permission ? <>{permission.join(', ')}</> : null
       }
     }
@@ -72,7 +72,7 @@ const tableColumns = reactive<TableColumn[]>([
     label: t('menu.component'),
     slots: {
       default: (data: any) => {
-        const component = data[0].row.component
+        const component = data.row.component
         return <>{component === '#' ? '顶级目录' : component === '##' ? '子目录' : component}</>
       }
     }
@@ -88,8 +88,8 @@ const tableColumns = reactive<TableColumn[]>([
       default: (data: any) => {
         return (
           <>
-            <ElTag type={data[0].row.status === 0 ? 'danger' : 'success'}>
-              {data[0].row.status === 1 ? t('userDemo.enable') : t('userDemo.disable')}
+            <ElTag type={data.row.status === 0 ? 'danger' : 'success'}>
+              {data.row.status === 1 ? t('userDemo.enable') : t('userDemo.disable')}
             </ElTag>
           </>
         )
@@ -102,7 +102,7 @@ const tableColumns = reactive<TableColumn[]>([
     width: 240,
     slots: {
       default: (data: any) => {
-        const row = data[0].row
+        const row = data.row
         return (
           <>
             <ElButton type="primary" onClick={() => action(row, 'edit')}>

+ 3 - 3
src/views/Authorization/Role/Role.vue

@@ -47,8 +47,8 @@ const tableColumns = reactive<TableColumn[]>([
       default: (data: any) => {
         return (
           <>
-            <ElTag type={data[0].row.status === 0 ? 'danger' : 'success'}>
-              {data[0].row.status === 1 ? t('userDemo.enable') : t('userDemo.disable')}
+            <ElTag type={data.row.status === 0 ? 'danger' : 'success'}>
+              {data.row.status === 1 ? t('userDemo.enable') : t('userDemo.disable')}
             </ElTag>
           </>
         )
@@ -69,7 +69,7 @@ const tableColumns = reactive<TableColumn[]>([
     width: 240,
     slots: {
       default: (data: any) => {
-        const row = data[0].row
+        const row = data.row
         return (
           <>
             <ElButton type="primary" onClick={() => action(row, 'edit')}>

+ 1 - 1
src/views/Authorization/User/User.vue

@@ -167,7 +167,7 @@ const crudSchemas = reactive<CrudSchema[]>([
       fixed: 'right',
       slots: {
         default: (data: any) => {
-          const row = data[0].row as DepartmentUserItem
+          const row = data.row as DepartmentUserItem
           return (
             <>
               <ElButton type="primary" onClick={() => action(row, 'edit')}>

+ 2 - 2
src/views/Components/Table/DefaultTable.vue

@@ -1,7 +1,7 @@
 <script setup lang="tsx">
 import { ContentWrap } from '@/components/ContentWrap'
 import { useI18n } from '@/hooks/web/useI18n'
-import { Table, TableColumn, TableSlotDefault } from '@/components/Table'
+import { Table, TableColumn } from '@/components/Table'
 import { getTableListApi } from '@/api/table'
 import { TableData } from '@/api/table/types'
 import { ref, h } from 'vue'
@@ -87,7 +87,7 @@ const getTableList = async (params?: Params) => {
 
 getTableList()
 
-const actionFn = (data: TableSlotDefault) => {
+const actionFn = (data: any) => {
   console.log(data)
 }
 </script>

+ 2 - 2
src/views/Components/Table/TreeTable.vue

@@ -1,7 +1,7 @@
 <script setup lang="tsx">
 import { ContentWrap } from '@/components/ContentWrap'
 import { useI18n } from '@/hooks/web/useI18n'
-import { Table, TableColumn, TableSlotDefault } from '@/components/Table'
+import { Table, TableColumn } from '@/components/Table'
 import { getTreeTableListApi } from '@/api/table'
 import { reactive, unref } from 'vue'
 import { ElTag, ElButton } from 'element-plus'
@@ -86,7 +86,7 @@ const columns = reactive<TableColumn[]>([
   }
 ])
 
-const actionFn = (data: TableSlotDefault) => {
+const actionFn = (data) => {
   console.log(data)
 }
 </script>

+ 2 - 2
src/views/Components/Table/UseTableDemo.vue

@@ -30,8 +30,8 @@ const columns = reactive<TableColumn[]>([
     field: 'expand',
     type: 'expand',
     slots: {
-      default: (data: TableSlotDefault[]) => {
-        const { row } = data[0]
+      default: (data: TableSlotDefault) => {
+        const { row } = data
         return (
           <div class="ml-30px">
             <div>

+ 259 - 4
src/views/Dashboard/Workplace.vue

@@ -1,9 +1,111 @@
 <script setup lang="ts">
-import { ElRow, ElCol, ElSkeleton, ElCard } from 'element-plus'
+import { useTimeAgo } from '@/hooks/web/useTimeAgo'
+import { ElRow, ElCol, ElSkeleton, ElCard, ElDivider, ElLink } from 'element-plus'
 import { useI18n } from '@/hooks/web/useI18n'
-import { ref } from 'vue'
+import { ref, reactive } from 'vue'
+import { CountTo } from '@/components/CountTo'
+import { formatTime } from '@/utils'
+import { Echart } from '@/components/Echart'
+import { EChartsOption } from 'echarts'
+import { radarOption } from './echarts-data'
+import { Highlight } from '@/components/Highlight'
+import {
+  getCountApi,
+  getProjectApi,
+  getDynamicApi,
+  getTeamApi,
+  getRadarApi
+} from '@/api/dashboard/workplace'
+import type { WorkplaceTotal, Project, Dynamic, Team } from '@/api/dashboard/workplace/types'
+import { set } from 'lodash-es'
 
-const loading = ref(false)
+const loading = ref(true)
+
+// 获取统计数
+let totalSate = reactive<WorkplaceTotal>({
+  project: 0,
+  access: 0,
+  todo: 0
+})
+
+const getCount = async () => {
+  const res = await getCountApi().catch(() => {})
+  if (res) {
+    totalSate = Object.assign(totalSate, res.data)
+  }
+}
+
+let projects = reactive<Project[]>([])
+
+// 获取项目数
+const getProject = async () => {
+  const res = await getProjectApi().catch(() => {})
+  if (res) {
+    projects = Object.assign(projects, res.data)
+  }
+}
+
+// 获取动态
+let dynamics = reactive<Dynamic[]>([])
+
+const getDynamic = async () => {
+  const res = await getDynamicApi().catch(() => {})
+  if (res) {
+    dynamics = Object.assign(dynamics, res.data)
+  }
+}
+
+// 获取团队
+let team = reactive<Team[]>([])
+
+const getTeam = async () => {
+  const res = await getTeamApi().catch(() => {})
+  if (res) {
+    team = Object.assign(team, res.data)
+  }
+}
+
+// 获取指数
+let radarOptionData = reactive<EChartsOption>(radarOption) as EChartsOption
+
+const getRadar = async () => {
+  const res = await getRadarApi().catch(() => {})
+  if (res) {
+    set(
+      radarOptionData,
+      'radar.indicator',
+      res.data.map((v) => {
+        return {
+          name: t(v.name),
+          max: v.max
+        }
+      })
+    )
+    set(radarOptionData, 'series', [
+      {
+        name: `xxx${t('workplace.index')}`,
+        type: 'radar',
+        data: [
+          {
+            value: res.data.map((v) => v.personal),
+            name: t('workplace.personal')
+          },
+          {
+            value: res.data.map((v) => v.team),
+            name: t('workplace.team')
+          }
+        ]
+      }
+    ])
+  }
+}
+
+const getAllApi = async () => {
+  await Promise.all([getCount(), getProject(), getDynamic(), getTeam(), getRadar()])
+  loading.value = false
+}
+
+getAllApi()
 
 const { t } = useI18n()
 </script>
@@ -30,9 +132,162 @@ const { t } = useI18n()
               </div>
             </div>
           </ElCol>
-          <ElCol :xl="12" :lg="12" :md="12" :sm="24" :xs="24" />
+          <ElCol :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
+            <div class="flex h-70px items-center justify-end lt-sm:mt-20px">
+              <div class="px-8px text-right">
+                <div class="text-14px text-gray-400 mb-20px">{{ t('workplace.project') }}</div>
+                <CountTo
+                  class="text-20px"
+                  :start-val="0"
+                  :end-val="totalSate.project"
+                  :duration="2600"
+                />
+              </div>
+              <ElDivider direction="vertical" />
+              <div class="px-8px text-right">
+                <div class="text-14px text-gray-400 mb-20px">{{ t('workplace.toDo') }}</div>
+                <CountTo
+                  class="text-20px"
+                  :start-val="0"
+                  :end-val="totalSate.todo"
+                  :duration="2600"
+                />
+              </div>
+              <ElDivider direction="vertical" border-style="dashed" />
+              <div class="px-8px text-right">
+                <div class="text-14px text-gray-400 mb-20px">{{ t('workplace.access') }}</div>
+                <CountTo
+                  class="text-20px"
+                  :start-val="0"
+                  :end-val="totalSate.access"
+                  :duration="2600"
+                />
+              </div>
+            </div>
+          </ElCol>
         </ElRow>
       </ElSkeleton>
     </ElCard>
   </div>
+
+  <ElRow class="mt-20px" :gutter="20" justify="space-between">
+    <ElCol :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-20px">
+      <ElCard shadow="never">
+        <template #header>
+          <div class="flex justify-between">
+            <span>{{ t('workplace.project') }}</span>
+            <ElLink type="primary" :underline="false">{{ t('workplace.more') }}</ElLink>
+          </div>
+        </template>
+        <ElSkeleton :loading="loading" animated>
+          <ElRow>
+            <ElCol
+              v-for="(item, index) in projects"
+              :key="`card-${index}`"
+              :xl="8"
+              :lg="8"
+              :md="12"
+              :sm="24"
+              :xs="24"
+            >
+              <ElCard shadow="hover">
+                <div class="flex items-center">
+                  <Icon :icon="item.icon" :size="25" class="mr-10px" />
+                  <span class="text-16px">{{ item.name }}</span>
+                </div>
+                <div class="mt-15px text-14px text-gray-400">{{ t(item.message) }}</div>
+                <div class="mt-20px text-12px text-gray-400 flex justify-between">
+                  <span>{{ item.personal }}</span>
+                  <span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
+                </div>
+              </ElCard>
+            </ElCol>
+          </ElRow>
+        </ElSkeleton>
+      </ElCard>
+
+      <ElCard shadow="never" class="mt-20px">
+        <template #header>
+          <div class="flex justify-between">
+            <span>{{ t('workplace.dynamic') }}</span>
+            <ElLink type="primary" :underline="false">{{ t('workplace.more') }}</ElLink>
+          </div>
+        </template>
+        <ElSkeleton :loading="loading" animated>
+          <div v-for="(item, index) in dynamics" :key="`dynamics-${index}`">
+            <div class="flex items-center">
+              <img
+                src="@/assets/imgs/avatar.jpg"
+                alt=""
+                class="w-35px h-35px rounded-[50%] mr-20px"
+              />
+              <div>
+                <div class="text-14px">
+                  <Highlight :keys="item.keys.map((v) => t(v))">
+                    {{ t('workplace.pushCode') }}
+                  </Highlight>
+                </div>
+                <div class="mt-15px text-12px text-gray-400">
+                  {{ useTimeAgo(item.time) }}
+                </div>
+              </div>
+            </div>
+            <ElDivider />
+          </div>
+        </ElSkeleton>
+      </ElCard>
+    </ElCol>
+    <ElCol :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-20px">
+      <ElCard shadow="never">
+        <template #header>
+          <span>{{ t('workplace.shortcutOperation') }}</span>
+        </template>
+        <ElSkeleton :loading="loading" animated>
+          <ElRow>
+            <ElCol
+              v-for="item in 9"
+              :key="`card-${item}`"
+              :xl="12"
+              :lg="12"
+              :md="12"
+              :sm="24"
+              :xs="24"
+              class="mb-10px"
+            >
+              <ElLink type="default" :underline="false">
+                {{ t('workplace.operation') }}{{ item }}
+              </ElLink>
+            </ElCol>
+          </ElRow>
+        </ElSkeleton>
+      </ElCard>
+
+      <ElCard shadow="never" class="mt-20px">
+        <template #header>
+          <span>xx{{ t('workplace.index') }}</span>
+        </template>
+        <ElSkeleton :loading="loading" animated>
+          <Echart :options="radarOptionData" :height="400" />
+        </ElSkeleton>
+      </ElCard>
+
+      <ElCard shadow="never" class="mt-20px">
+        <template #header>
+          <span>{{ t('workplace.team') }}</span>
+        </template>
+        <ElSkeleton :loading="loading" animated>
+          <ElRow>
+            <ElCol v-for="item in team" :key="`team-${item.name}`" :span="12" class="mb-20px">
+              <div class="flex items-center">
+                <Icon :icon="item.icon" class="mr-10px" />
+                <ElLink type="default" :underline="false">
+                  {{ item.name }}
+                </ElLink>
+              </div>
+            </ElCol>
+          </ElRow>
+        </ElSkeleton>
+      </ElCard>
+    </ElCol>
+  </ElRow>
 </template>

+ 3 - 3
src/views/Example/Dialog/ExampleDialog.vue

@@ -212,13 +212,13 @@ const crudSchemas = reactive<CrudSchema[]>([
         default: (data: any) => {
           return (
             <>
-              <ElButton type="primary" onClick={() => action(data[0].row, 'edit')}>
+              <ElButton type="primary" onClick={() => action(data.row, 'edit')}>
                 {t('exampleDemo.edit')}
               </ElButton>
-              <ElButton type="success" onClick={() => action(data[0].row, 'detail')}>
+              <ElButton type="success" onClick={() => action(data.row, 'detail')}>
                 {t('exampleDemo.detail')}
               </ElButton>
-              <ElButton type="danger" onClick={() => delData(data[0].row)}>
+              <ElButton type="danger" onClick={() => delData(data.row)}>
                 {t('exampleDemo.del')}
               </ElButton>
             </>

+ 3 - 3
src/views/Example/Page/ExamplePage.vue

@@ -252,13 +252,13 @@ const crudSchemas: CrudSchema[] = [
         default: (data: any) => {
           return (
             <>
-              <ElButton type="primary" onClick={() => action(data[0].row, 'edit')}>
+              <ElButton type="primary" onClick={() => action(data.row, 'edit')}>
                 {t('exampleDemo.edit')}
               </ElButton>
-              <ElButton type="success" onClick={() => action(data[0].row, 'detail')}>
+              <ElButton type="success" onClick={() => action(data.row, 'detail')}>
                 {t('exampleDemo.detail')}
               </ElButton>
-              <ElButton type="danger" onClick={() => delData(data[0].row)}>
+              <ElButton type="danger" onClick={() => delData(data.row)}>
                 {t('exampleDemo.del')}
               </ElButton>
             </>

+ 19 - 0
src/views/Function/MultipleTabs.vue

@@ -0,0 +1,19 @@
+<script setup lang="ts">
+import { ContentWrap } from '@/components/ContentWrap'
+import { ElButton } from 'element-plus'
+import { useRouter } from 'vue-router'
+
+const { push } = useRouter()
+
+const openTab = (item: number) => {
+  push(`/function/multiple-tabs-demo/${item}`)
+}
+</script>
+
+<template>
+  <ContentWrap>
+    <ElButton v-for="item in 5" :key="item" type="primary" @click="openTab(item)">
+      打开详情页{{ item }}
+    </ElButton>
+  </ContentWrap>
+</template>

+ 19 - 0
src/views/Function/MultipleTabsDemo.vue

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

+ 2 - 2
src/views/Login/components/LoginForm.vue

@@ -48,7 +48,7 @@ const schema = reactive<FormSchema[]>([
     }
   },
   {
-    field: 'account',
+    field: 'username',
     label: t('login.username'),
     value: 'admin',
     component: 'Input',
@@ -62,7 +62,7 @@ const schema = reactive<FormSchema[]>([
   {
     field: 'password',
     label: t('login.password'),
-    value: '123456',
+    value: 'admin',
     component: 'InputPassword',
     colProps: {
       span: 24

+ 60 - 0
src/views/hooks/useTagsView.vue

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