Browse Source

perf: 同步完整版代码

i18n
xiaoxian521 3 years ago
parent
commit
f5b387231a
  1. 1
      .vscode/extensions.json
  2. 12
      .vscode/settings.json
  3. 10
      README.md
  4. 8
      build/plugins.ts
  5. 30
      locales/en.yaml
  6. 30
      locales/zh-CN.yaml
  7. 3
      mock/asyncRoutes.ts
  8. 32
      package.json
  9. 1451
      pnpm-lock.yaml
  10. 1
      src/assets/svg/enter_outlined.svg
  11. 1
      src/assets/svg/mdi_keyboard_esc.svg
  12. 24
      src/components/ReIcon/src/iconifyIconOffline.ts
  13. 9
      src/layout/components/navbar.vue
  14. 38
      src/layout/components/notice/index.vue
  15. 2
      src/layout/components/notice/noticeItem.vue
  16. 2
      src/layout/components/notice/noticeList.vue
  17. 7
      src/layout/components/screenfull/index.vue
  18. 42
      src/layout/components/search/components/SearchFooter.vue
  19. 165
      src/layout/components/search/components/SearchModal.vue
  20. 93
      src/layout/components/search/components/SearchResult.vue
  21. 3
      src/layout/components/search/components/index.ts
  22. 30
      src/layout/components/search/index.vue
  23. 9
      src/layout/components/sidebar/horizontal.vue
  24. 9
      src/layout/components/sidebar/mixNav.vue
  25. 23
      src/layout/components/tag/index.vue
  26. 26
      src/layout/hooks/useBoolean.ts
  27. 0
      src/layout/redirect.vue
  28. 6
      src/main.ts
  29. 6
      src/plugins/element-plus/index.ts
  30. 72
      src/plugins/i18n.ts
  31. 20
      src/plugins/i18n/config.ts
  32. 21
      src/plugins/i18n/en/buttons.ts
  33. 22
      src/plugins/i18n/en/menus.ts
  34. 77
      src/plugins/i18n/index.ts
  35. 21
      src/plugins/i18n/zh-CN/buttons.ts
  36. 22
      src/plugins/i18n/zh-CN/menus.ts
  37. 3
      src/router/modules/error.ts
  38. 25
      src/router/modules/externalLink.ts
  39. 3
      src/router/modules/index.ts
  40. 3
      src/router/modules/remaining.ts
  41. 5
      src/style/element-plus.scss
  42. 6
      src/style/sidebar.scss
  43. 54
      src/views/error/403.vue
  44. 54
      src/views/error/404.vue
  45. 54
      src/views/error/500.vue
  46. 8
      src/views/permission/button/index.vue
  47. 14
      src/views/permission/page/index.vue
  48. 6
      src/views/type.ts
  49. 4
      vite.config.ts

1
.vscode/extensions.json

@ -1,6 +1,5 @@
{ {
"recommendations": [ "recommendations": [
"johnsoncodehk.vscode-typescript-vue-plugin",
"voorjaar.windicss-intellisense", "voorjaar.windicss-intellisense",
"vscode-icons-team.vscode-icons", "vscode-icons-team.vscode-icons",
"davidanson.vscode-markdownlint", "davidanson.vscode-markdownlint",

12
.vscode/settings.json

@ -1,13 +1,9 @@
{ {
"editor.formatOnType": true, "editor.formatOnType": true,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"javascript.updateImportsOnFileMove.enabled": "always",
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[javascript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"editor.tabSize": 2, "editor.tabSize": 2,
"editor.formatOnPaste": true, "editor.formatOnPaste": true,
"files.autoSave": "afterDelay", "files.autoSave": "afterDelay",
@ -30,14 +26,12 @@
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": true
}, },
"typescript.tsdk": "node_modules/typescript/lib",
"i18n-ally.localesPaths": ["src/plugins/i18n"],
"i18n-ally.localesPaths": "locales",
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true, "i18n-ally.sortKeys": true,
"i18n-ally.namespace": true, "i18n-ally.namespace": true,
"i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.enabledParsers": ["yaml", "js"],
"i18n-ally.sourceLanguage": "en", "i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN", "i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": ["vue", "react"]
"i18n-ally.enabledFrameworks": ["vue"]
} }

10
README.md

@ -23,15 +23,15 @@
## 捐赠 ## 捐赠
如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持
如果你觉得这个项目对您有帮助,可以帮作者买一杯果汁 🍹 表示支持
<img src="http://yiming_chang.gitee.io/manages/pay.jpg" width="150px" height="150px" />
<img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f69bf13c5b854ed5b699807cafa0e3ce~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp?" width="150px" height="150px" />
## QQ 交流群 ## QQ 交流群
群里严禁`黄`、`赌`、`毒`、`vpn`等违法行为! 群里严禁`黄`、`赌`、`毒`、`vpn`等违法行为!
<img src="http://yiming_chang.gitee.io/manages/qq.jpg" width="150px" height="225px" />
<img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f0697596aec84661b724f6eebdf8db17~tplv-k3u1fbpfcp-watermark.awebp?" width="150px" height="225px" />
## 用法 ## 用法
@ -47,7 +47,7 @@ pnpm add 包名
pnpm remove 包名 pnpm remove 包名
我认为你应该先 fork 项目去开发,以便我更新时可以同步拉取更新!!!
我认为你应该先 fork 项目去开发,以便我更新时可以同步拉取更新!!!
## ⚠️ 注意 ## ⚠️ 注意
@ -56,3 +56,5 @@ pnpm remove 包名
## 许可证 ## 许可证
原则上不收取任何费用及版权,可以放心使用,不过如需二次开源(比如用此平台二次开发并开源)请联系作者获取许可! 原则上不收取任何费用及版权,可以放心使用,不过如需二次开源(比如用此平台二次开发并开源)请联系作者获取许可!
[MIT © xiaoxian521-2020](./LICENSE)

8
build/plugins.ts

@ -1,3 +1,4 @@
import { resolve } from "path";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import { viteBuildInfo } from "./info"; import { viteBuildInfo } from "./info";
import svgLoader from "vite-svg-loader"; import svgLoader from "vite-svg-loader";
@ -6,6 +7,7 @@ import vueJsx from "@vitejs/plugin-vue-jsx";
import WindiCSS from "vite-plugin-windicss"; import WindiCSS from "vite-plugin-windicss";
import { viteMockServe } from "vite-plugin-mock"; import { viteMockServe } from "vite-plugin-mock";
import liveReload from "vite-plugin-live-reload"; import liveReload from "vite-plugin-live-reload";
import VueI18n from "@intlify/vite-plugin-vue-i18n";
import ElementPlus from "unplugin-element-plus/vite"; import ElementPlus from "unplugin-element-plus/vite";
import { visualizer } from "rollup-plugin-visualizer"; import { visualizer } from "rollup-plugin-visualizer";
import removeConsole from "vite-plugin-remove-console"; import removeConsole from "vite-plugin-remove-console";
@ -16,6 +18,12 @@ export function getPluginsList(command, VITE_LEGACY) {
const lifecycle = process.env.npm_lifecycle_event; const lifecycle = process.env.npm_lifecycle_event;
return [ return [
vue(), vue(),
// https://github.com/intlify/bundle-tools/tree/main/packages/vite-plugin-vue-i18n
VueI18n({
runtimeOnly: true,
compositionOnly: true,
include: [resolve("locales/**")]
}),
// jsx、tsx语法支持 // jsx、tsx语法支持
vueJsx(), vueJsx(),
WindiCSS(), WindiCSS(),

30
locales/en.yaml

@ -0,0 +1,30 @@
buttons:
hsLoginOut: LoginOut
hsfullscreen: FullScreen
hsexitfullscreen: ExitFullscreen
hsrefreshRoute: RefreshRoute
hslogin: Login
hsadd: Add
hsmark: Mark/Cancel
hssave: Save
hssearch: Search
hsexpendAll: Expand All
hscollapseAll: Collapse All
hssystemSet: Open ProjectConfig
hsdelete: Delete
hsreload: Reload
hscloseCurrentTab: Close CurrentTab
hscloseLeftTabs: Close LeftTabs
hscloseRightTabs: Close RightTabs
hscloseOtherTabs: Close OtherTabs
hscloseAllTabs: Close AllTabs
menus:
hshome: Home
hslogin: Login
hserror: Error Page
hsfourZeroFour: "404"
hsfourZeroOne: "403"
hsFive: "500"
permission: Permission Manage
permissionPage: Page Permission
permissionButton: Button Permission

30
locales/zh-CN.yaml

@ -0,0 +1,30 @@
buttons:
hsLoginOut: 退出系统
hsfullscreen: 全屏
hsexitfullscreen: 退出全屏
hsrefreshRoute: 刷新路由
hslogin: 登陆
hsadd: 新增
hsmark: 标记/取消
hssave: 保存
hssearch: 搜索
hsexpendAll: 全部展开
hscollapseAll: 全部折叠
hssystemSet: 打开项目配置
hsdelete: 删除
hsreload: 重新加载
hscloseCurrentTab: 关闭当前标签页
hscloseLeftTabs: 关闭左侧标签页
hscloseRightTabs: 关闭右侧标签页
hscloseOtherTabs: 关闭其他标签页
hscloseAllTabs: 关闭全部标签页
menus:
hshome: 首页
hslogin: 登陆
hserror: 错误页面
hsfourZeroFour: "404"
hsfourZeroOne: "403"
hsFive: "500"
permission: 权限管理
permissionPage: 页面权限
permissionButton: 按钮权限

3
mock/asyncRoutes.ts

@ -3,13 +3,12 @@ import { MockMethod } from "vite-plugin-mock";
const permissionRouter = { const permissionRouter = {
path: "/permission", path: "/permission",
name: "permission",
redirect: "/permission/page/index", redirect: "/permission/page/index",
meta: { meta: {
title: "menus.permission", title: "menus.permission",
icon: "lollipop", icon: "lollipop",
i18n: true, i18n: true,
rank: 3
rank: 7
}, },
children: [ children: [
{ {

32
package.json

@ -2,10 +2,6 @@
"name": "pure-admin-thin", "name": "pure-admin-thin",
"version": "3.1.0", "version": "3.1.0",
"private": true, "private": true,
"engines": {
"node": ">= 16",
"pnpm": ">= 6"
},
"scripts": { "scripts": {
"dev": "cross-env --max_old_space_size=4096 vite", "dev": "cross-env --max_old_space_size=4096 vite",
"serve": "pnpm dev", "serve": "pnpm dev",
@ -30,14 +26,15 @@
], ],
"dependencies": { "dependencies": {
"@ctrl/tinycolor": "^3.4.0", "@ctrl/tinycolor": "^3.4.0",
"@vueuse/core": "^7.6.2",
"@pureadmin/components": "^1.0.2",
"@vueuse/core": "^8.0.1",
"@vueuse/motion": "^2.0.0-beta.9", "@vueuse/motion": "^2.0.0-beta.9",
"@vueuse/shared": "^7.6.2",
"@vueuse/shared": "^8.0.1",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"axios": "^0.25.0",
"axios": "^0.26.1",
"css-color-function": "^1.3.3", "css-color-function": "^1.3.3",
"dayjs": "^1.10.7", "dayjs": "^1.10.7",
"element-plus": "^2.0.3",
"element-plus": "^2.1.1",
"element-resize-detector": "^1.2.3", "element-resize-detector": "^1.2.3",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@ -52,8 +49,8 @@
"responsive-storage": "^1.0.11", "responsive-storage": "^1.0.11",
"rgb-hex": "^4.0.0", "rgb-hex": "^4.0.0",
"vue": "^3.2.31", "vue": "^3.2.31",
"vue-i18n": "^9.2.0-beta.30",
"vue-router": "^4.0.13",
"vue-i18n": "^9.2.0-beta.32",
"vue-router": "^4.0.14",
"vue-types": "^4.1.1" "vue-types": "^4.1.1"
}, },
"devDependencies": { "devDependencies": {
@ -63,7 +60,8 @@
"@iconify-icons/fa": "^1.1.1", "@iconify-icons/fa": "^1.1.1",
"@iconify-icons/fa-solid": "^1.1.2", "@iconify-icons/fa-solid": "^1.1.2",
"@iconify-icons/ri": "^1.1.1", "@iconify-icons/ri": "^1.1.1",
"@iconify/vue": "^3.1.3",
"@iconify/vue": "^3.1.4",
"@intlify/vite-plugin-vue-i18n": "^3.3.1",
"@types/element-resize-detector": "1.1.3", "@types/element-resize-detector": "1.1.3",
"@types/js-cookie": "^3.0.1", "@types/js-cookie": "^3.0.1",
"@types/lodash-es": "^4.17.6", "@types/lodash-es": "^4.17.6",
@ -95,22 +93,22 @@
"pretty-quick": "3.1.1", "pretty-quick": "3.1.1",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"rollup-plugin-visualizer": "^5.6.0", "rollup-plugin-visualizer": "^5.6.0",
"sass": "^1.49.0",
"sass-loader": "^12.4.0",
"sass": "^1.49.9",
"sass-loader": "^12.6.0",
"stylelint": "^14.3.0", "stylelint": "^14.3.0",
"stylelint-config-html": "^1.0.0", "stylelint-config-html": "^1.0.0",
"stylelint-config-prettier": "^9.0.3", "stylelint-config-prettier": "^9.0.3",
"stylelint-config-recommended": "^6.0.0", "stylelint-config-recommended": "^6.0.0",
"stylelint-config-standard": "^24.0.0", "stylelint-config-standard": "^24.0.0",
"stylelint-order": "^5.0.0", "stylelint-order": "^5.0.0",
"typescript": "^4.5.5",
"unplugin-element-plus": "^0.2.0",
"vite": "^2.8.6",
"typescript": "^4.6.2",
"unplugin-element-plus": "^0.3.2",
"vite": "2.7.13",
"vite-plugin-live-reload": "^2.1.0", "vite-plugin-live-reload": "^2.1.0",
"vite-plugin-mock": "^2.9.6", "vite-plugin-mock": "^2.9.6",
"vite-plugin-remove-console": "^0.0.6", "vite-plugin-remove-console": "^0.0.6",
"vite-plugin-style-import": "1.4.1", "vite-plugin-style-import": "1.4.1",
"vite-plugin-windicss": "^1.8.2",
"vite-plugin-windicss": "^1.8.3",
"vite-svg-loader": "2.2.0", "vite-svg-loader": "2.2.0",
"vue-eslint-parser": "^8.2.0", "vue-eslint-parser": "^8.2.0",
"windicss": "^3.5.1" "windicss": "^3.5.1"

1451
pnpm-lock.yaml
File diff suppressed because it is too large
View File

1
src/assets/svg/enter_outlined.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--ant-design" width="20" height="20" preserveAspectRatio="xMidYMid meet" viewBox="0 0 1024 1024"><path fill="currentColor" d="M864 170h-60c-4.4 0-8 3.6-8 8v518H310v-73c0-6.7-7.8-10.5-13-6.3l-141.9 112a8 8 0 0 0 0 12.6l141.9 112c5.3 4.2 13 .4 13-6.3v-75h498c35.3 0 64-28.7 64-64V178c0-4.4-3.6-8-8-8z"></path></svg>

1
src/assets/svg/mdi_keyboard_esc.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--mdi" width="20" height="20" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1V7m10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2Z"></path></svg>

24
src/components/ReIcon/src/iconifyIconOffline.ts

@ -24,6 +24,10 @@ import Location from "@iconify-icons/ep/location";
import Tickets from "@iconify-icons/ep/tickets"; import Tickets from "@iconify-icons/ep/tickets";
import OfficeBuilding from "@iconify-icons/ep/office-building"; import OfficeBuilding from "@iconify-icons/ep/office-building";
import Notebook from "@iconify-icons/ep/notebook"; import Notebook from "@iconify-icons/ep/notebook";
import Rank from "@iconify-icons/ep/rank";
import videoPlay from "@iconify-icons/ep/video-play";
import Monitor from "@iconify-icons/ep/monitor";
import Search from "@iconify-icons/ep/search";
addIcon("check", Check); addIcon("check", Check);
addIcon("menu", Menu); addIcon("menu", Menu);
addIcon("home-filled", HomeFilled); addIcon("home-filled", HomeFilled);
@ -46,16 +50,36 @@ addIcon("location", Location);
addIcon("tickets", Tickets); addIcon("tickets", Tickets);
addIcon("office-building", OfficeBuilding); addIcon("office-building", OfficeBuilding);
addIcon("notebook", Notebook); addIcon("notebook", Notebook);
addIcon("video-play", videoPlay);
addIcon("rank", Rank);
addIcon("monitor", Monitor);
addIcon("search", Search);
// remixicon // remixicon
import arrowRightSLine from "@iconify-icons/ri/arrow-right-s-line"; import arrowRightSLine from "@iconify-icons/ri/arrow-right-s-line";
import arrowLeftSLine from "@iconify-icons/ri/arrow-left-s-line"; import arrowLeftSLine from "@iconify-icons/ri/arrow-left-s-line";
import logoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line"; import logoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import nodeTree from "@iconify-icons/ri/node-tree"; import nodeTree from "@iconify-icons/ri/node-tree";
import ubuntuFill from "@iconify-icons/ri/ubuntu-fill";
import questionLine from "@iconify-icons/ri/question-line";
import checkboxCircleLine from "@iconify-icons/ri/checkbox-circle-line";
import informationLine from "@iconify-icons/ri/information-line";
import closeCircleLine from "@iconify-icons/ri/close-circle-line";
import arrowUpLine from "@iconify-icons/ri/arrow-up-line";
import arrowDownLine from "@iconify-icons/ri/arrow-down-line";
import bookmark2Line from "@iconify-icons/ri/bookmark-2-line";
addIcon("arrow-right-s-line", arrowRightSLine); addIcon("arrow-right-s-line", arrowRightSLine);
addIcon("arrow-left-s-line", arrowLeftSLine); addIcon("arrow-left-s-line", arrowLeftSLine);
addIcon("logout-circle-r-line", logoutCircleRLine); addIcon("logout-circle-r-line", logoutCircleRLine);
addIcon("node-tree", nodeTree); addIcon("node-tree", nodeTree);
addIcon("ubuntu-fill", ubuntuFill);
addIcon("question-line", questionLine);
addIcon("checkbox-circle-line", checkboxCircleLine);
addIcon("information-line", informationLine);
addIcon("close-circle-line", closeCircleLine);
addIcon("arrow-up-line", arrowUpLine);
addIcon("arrow-down-line", arrowDownLine);
addIcon("bookmark-2-line", bookmark2Line);
// Font Awesome 4 // Font Awesome 4
import faUser from "@iconify-icons/fa/user"; import faUser from "@iconify-icons/fa/user";

9
src/layout/components/navbar.vue

@ -2,6 +2,7 @@
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useNav } from "../hooks/nav"; import { useNav } from "../hooks/nav";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import Search from "./search/index.vue";
import Notice from "./notice/index.vue"; import Notice from "./notice/index.vue";
import mixNav from "./sidebar/mixNav.vue"; import mixNav from "./sidebar/mixNav.vue";
import avatars from "/@/assets/avatars.jpg"; import avatars from "/@/assets/avatars.jpg";
@ -13,7 +14,7 @@ import screenfull from "../components/screenfull/index.vue";
import globalization from "/@/assets/svg/globalization.svg?component"; import globalization from "/@/assets/svg/globalization.svg?component";
const route = useRoute(); const route = useRoute();
const { locale } = useI18n();
const { locale, t } = useI18n();
const instance = const instance =
getCurrentInstance().appContext.config.globalProperties.$storage; getCurrentInstance().appContext.config.globalProperties.$storage;
const { const {
@ -58,6 +59,8 @@ function translationEn() {
<mixNav v-if="pureApp.layout === 'mix'" /> <mixNav v-if="pureApp.layout === 'mix'" />
<div v-if="pureApp.layout === 'vertical'" class="vertical-header-right"> <div v-if="pureApp.layout === 'vertical'" class="vertical-header-right">
<!-- 菜单搜索 -->
<Search />
<!-- 通知 --> <!-- 通知 -->
<Notice id="header-notice" /> <Notice id="header-notice" />
<!-- 全屏 --> <!-- 全屏 -->
@ -98,14 +101,14 @@ function translationEn() {
<IconifyIconOffline <IconifyIconOffline
icon="logout-circle-r-line" icon="logout-circle-r-line"
style="margin: 5px" style="margin: 5px"
/>{{ $t("buttons.hsLoginOut") }}</el-dropdown-item
/>{{ t("buttons.hsLoginOut") }}</el-dropdown-item
> >
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<el-icon <el-icon
class="el-icon-setting" class="el-icon-setting"
:title="$t('buttons.hssystemSet')"
:title="t('buttons.hssystemSet')"
@click="onPanel" @click="onPanel"
> >
<IconifyIconOffline icon="setting" /> <IconifyIconOffline icon="setting" />

38
src/layout/components/notice/index.vue

@ -1,8 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import NoticeList from "./noticeList.vue";
import { noticesData } from "./data"; import { noticesData } from "./data";
import NoticeList from "./noticeList.vue";
import { templateRef } from "@vueuse/core";
import { Tabs, TabPane } from "@pureadmin/components";
const dropdownDom = templateRef<ElRef | null>("dropdownDom", null);
const activeName = ref(noticesData[0].name); const activeName = ref(noticesData[0].name);
const notices = ref(noticesData); const notices = ref(noticesData);
@ -10,10 +13,15 @@ let noticesNum = ref(0);
notices.value.forEach(notice => { notices.value.forEach(notice => {
noticesNum.value += notice.list.length; noticesNum.value += notice.list.length;
}); });
function tabClick() {
// @ts-expect-error
dropdownDom.value.handleOpen();
}
</script> </script>
<template> <template>
<el-dropdown trigger="click" placement="bottom-end">
<el-dropdown ref="dropdownDom" trigger="click" placement="bottom-end">
<span class="dropdown-badge"> <span class="dropdown-badge">
<el-badge :value="noticesNum" :max="99"> <el-badge :value="noticesNum" :max="99">
<el-icon class="header-notice-icon" <el-icon class="header-notice-icon"
@ -23,25 +31,33 @@ notices.value.forEach(notice => {
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-tabs v-model="activeName" class="dropdown-tabs">
<template v-for="item in notices" :key="item.key">
<el-tab-pane
:label="`${item.name}(${item.list.length})`"
:name="item.name"
<Tabs
centered
class="dropdown-tabs"
v-model:activeName="activeName"
@tabClick="tabClick"
> >
<template v-for="item in notices" :key="item.key">
<TabPane :tab="`${item.name}(${item.list.length})`">
<el-scrollbar max-height="330px"> <el-scrollbar max-height="330px">
<div class="noticeList-container"> <div class="noticeList-container">
<NoticeList :list="item.list" /> <NoticeList :list="item.list" />
</div> </div>
</el-scrollbar> </el-scrollbar>
</el-tab-pane>
</TabPane>
</template> </template>
</el-tabs>
</Tabs>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
</template> </template>
<style>
.ant-tabs-dropdown {
z-index: 2900 !important;
}
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
.dropdown-badge { .dropdown-badge {
display: flex; display: flex;
@ -79,4 +95,8 @@ notices.value.forEach(notice => {
padding: 15px 24px 0 24px; padding: 15px 24px 0 24px;
} }
} }
:deep(.ant-tabs-nav) {
margin-bottom: 0;
}
</style> </style>

2
src/layout/components/notice/noticeItem.vue

@ -10,8 +10,8 @@ const props = defineProps({
}); });
const titleRef = ref(null); const titleRef = ref(null);
const descriptionRef = ref(null);
const titleTooltip = ref(false); const titleTooltip = ref(false);
const descriptionRef = ref(null);
const descriptionTooltip = ref(false); const descriptionTooltip = ref(false);
function hoverTitle() { function hoverTitle() {

2
src/layout/components/notice/noticeList.vue

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { PropType } from "vue"; import { PropType } from "vue";
import NoticeItem from "./noticeItem.vue";
import { ListItem } from "./data"; import { ListItem } from "./data";
import NoticeItem from "./noticeItem.vue";
const props = defineProps({ const props = defineProps({
list: { list: {

7
src/layout/components/screenfull/index.vue

@ -1,5 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useFullscreen } from "@vueuse/core"; import { useFullscreen } from "@vueuse/core";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { isFullscreen, toggle } = useFullscreen(); const { isFullscreen, toggle } = useFullscreen();
</script> </script>
@ -7,9 +10,7 @@ const { isFullscreen, toggle } = useFullscreen();
<div class="screen-full" @click="toggle"> <div class="screen-full" @click="toggle">
<FontIcon <FontIcon
:title=" :title="
isFullscreen
? $t('buttons.hsexitfullscreen')
: $t('buttons.hsfullscreen')
isFullscreen ? t('buttons.hsexitfullscreen') : t('buttons.hsfullscreen')
" "
:icon="isFullscreen ? 'team-iconexit-fullscreen' : 'team-iconfullscreen'" :icon="isFullscreen ? 'team-iconexit-fullscreen' : 'team-iconfullscreen'"
/> />

42
src/layout/components/search/components/SearchFooter.vue

@ -0,0 +1,42 @@
<template>
<div class="search-footer">
<span class="search-footer-item">
<enterOutlined class="icon" />
确认
</span>
<span class="search-footer-item">
<IconifyIconOffline icon="arrow-up-line" class="icon" />
<IconifyIconOffline icon="arrow-down-line" class="icon" />
切换
</span>
<span class="search-footer-item">
<mdiKeyboardEsc class="icon" />
关闭
</span>
</div>
</template>
<script lang="ts" setup>
import enterOutlined from "/@/assets/svg/enter_outlined.svg?component";
import mdiKeyboardEsc from "/@/assets/svg/mdi_keyboard_esc.svg?component";
</script>
<style lang="scss" scoped>
.search-footer {
display: flex;
color: #333;
.search-footer-item {
display: flex;
align-items: center;
margin-right: 14px;
}
.icon {
padding: 2px;
margin-right: 3px;
font-size: 20px;
box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff,
0 1px 2px 1px #1e235a66;
}
}
</style>

165
src/layout/components/search/components/SearchModal.vue

@ -0,0 +1,165 @@
<script lang="ts" setup>
import { useRouter } from "vue-router";
import SearchResult from "./SearchResult.vue";
import SearchFooter from "./SearchFooter.vue";
import { deleteChildren } from "/@/utils/tree";
import { transformI18n } from "/@/plugins/i18n";
import { useDebounceFn, onKeyStroke } from "@vueuse/core";
import { ref, watch, computed, nextTick, shallowRef } from "vue";
import { usePermissionStoreHook } from "/@/store/modules/permission";
interface Props {
/** 弹窗显隐 */
value: boolean;
}
interface Emits {
(e: "update:value", val: boolean): void;
}
const emit = defineEmits<Emits>();
const props = withDefaults(defineProps<Props>(), {});
const router = useRouter();
const keyword = ref("");
const activePath = ref("");
const inputRef = ref<HTMLInputElement | null>(null);
const resultOptions = shallowRef([]);
const handleSearch = useDebounceFn(search, 300);
/** 菜单树形结构 */
const menusData = computed(() => {
return deleteChildren(usePermissionStoreHook().menusTree);
});
const show = computed({
get() {
return props.value;
},
set(val: boolean) {
emit("update:value", val);
}
});
watch(show, async val => {
if (val) {
/** 自动聚焦 */
await nextTick();
inputRef.value?.focus();
}
});
/** 将菜单树形结构扁平化为一维数组,用于菜单查询 */
function flatTree(arr) {
const res = [];
function deep(arr) {
arr.forEach(item => {
res.push(item);
item.children && deep(item.children);
});
}
deep(arr);
return res;
}
/** 查询 */
function search() {
const flatMenusData = flatTree(menusData.value);
resultOptions.value = flatMenusData.filter(
menu =>
keyword.value &&
transformI18n(menu.meta?.title, menu.meta?.i18n)
.toLocaleLowerCase()
.includes(keyword.value.toLocaleLowerCase().trim())
);
if (resultOptions.value?.length > 0) {
activePath.value = resultOptions.value[0].path;
} else {
activePath.value = "";
}
}
function handleClose() {
show.value = false;
/** 延时处理防止用户看到某些操作 */
setTimeout(() => {
resultOptions.value = [];
keyword.value = "";
}, 200);
}
/** key up */
function handleUp() {
const { length } = resultOptions.value;
if (length === 0) return;
const index = resultOptions.value.findIndex(
item => item.path === activePath.value
);
if (index === 0) {
activePath.value = resultOptions.value[length - 1].path;
} else {
activePath.value = resultOptions.value[index - 1].path;
}
}
/** key down */
function handleDown() {
const { length } = resultOptions.value;
if (length === 0) return;
const index = resultOptions.value.findIndex(
item => item.path === activePath.value
);
if (index + 1 === length) {
activePath.value = resultOptions.value[0].path;
} else {
activePath.value = resultOptions.value[index + 1].path;
}
}
/** key enter */
function handleEnter() {
const { length } = resultOptions.value;
if (length === 0 || activePath.value === "") return;
router.push(activePath.value);
handleClose();
}
onKeyStroke("Enter", handleEnter);
onKeyStroke("ArrowUp", handleUp);
onKeyStroke("ArrowDown", handleDown);
</script>
<template>
<el-dialog top="5vh" v-model="show" :before-close="handleClose">
<el-input
ref="inputRef"
v-model="keyword"
clearable
placeholder="请输入关键词搜索"
@input="handleSearch"
>
<template #prefix>
<el-icon class="el-input__icon">
<IconifyIconOffline icon="search" />
</el-icon>
</template>
</el-input>
<div class="search-result-container">
<el-empty v-if="resultOptions.length === 0" description="暂无搜索结果" />
<SearchResult
v-else
v-model:value="activePath"
:options="resultOptions"
@click="handleEnter"
/>
</div>
<template #footer>
<SearchFooter />
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.search-result-container {
margin-top: 20px;
}
</style>

93
src/layout/components/search/components/SearchResult.vue

@ -0,0 +1,93 @@
<template>
<div class="result">
<template v-for="item in options" :key="item.path">
<div
class="result-item"
:style="{
background:
item?.path === active ? useEpThemeStoreHook().epThemeColor : '',
color: item.path === active ? '#fff' : ''
}"
@click="handleTo"
@mouseenter="handleMouse(item)"
>
<component
:is="useRenderIcon(item.meta?.icon ?? 'bookmark-2-line')"
></component>
<span class="result-item-title">{{ t(item.meta?.title) }}</span>
<enterOutlined />
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
import enterOutlined from "/@/assets/svg/enter_outlined.svg?component";
const { t } = useI18n();
interface optionsItem {
path: string;
meta?: {
icon?: string;
title?: string;
};
}
interface Props {
value: string;
options: Array<optionsItem>;
}
interface Emits {
(e: "update:value", val: string): void;
(e: "enter"): void;
}
const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<Emits>();
const active = computed({
get() {
return props.value;
},
set(val: string) {
emit("update:value", val);
}
});
/** 鼠标移入 */
async function handleMouse(item) {
active.value = item.path;
}
function handleTo() {
emit("enter");
}
</script>
<style lang="scss" scoped>
.result {
padding-bottom: 12px;
&-item {
display: flex;
align-items: center;
height: 56px;
margin-top: 8px;
padding: 14px;
border-radius: 4px;
background: #e5e7eb;
cursor: pointer;
&-title {
display: flex;
flex: 1;
margin-left: 5px;
}
}
}
</style>

3
src/layout/components/search/components/index.ts

@ -0,0 +1,3 @@
import SearchModal from "./SearchModal.vue";
export { SearchModal };

30
src/layout/components/search/index.vue

@ -0,0 +1,30 @@
<script lang="ts" setup>
import { SearchModal } from "./components";
import useBoolean from "../../hooks/useBoolean";
const { bool: show, toggle } = useBoolean();
function handleSearch() {
toggle();
}
</script>
<template>
<div class="search-container" @click="handleSearch">
<IconifyIconOffline icon="search" />
</div>
<SearchModal v-model:value="show" />
</template>
<style lang="scss" scoped>
.search-container {
display: flex;
align-items: center;
justify-content: center;
height: 48px;
width: 40px;
cursor: pointer;
&:hover {
background: #f6f6f6;
}
}
</style>

9
src/layout/components/sidebar/horizontal.vue

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useNav } from "../../hooks/nav"; import { useNav } from "../../hooks/nav";
import Search from "../search/index.vue";
import Notice from "../notice/index.vue"; import Notice from "../notice/index.vue";
import { templateRef } from "@vueuse/core"; import { templateRef } from "@vueuse/core";
import SidebarItem from "./sidebarItem.vue"; import SidebarItem from "./sidebarItem.vue";
@ -13,7 +14,7 @@ import { usePermissionStoreHook } from "/@/store/modules/permission";
import globalization from "/@/assets/svg/globalization.svg?component"; import globalization from "/@/assets/svg/globalization.svg?component";
const route = useRoute(); const route = useRoute();
const { locale } = useI18n();
const { locale, t } = useI18n();
const routers = useRouter().options.routes; const routers = useRouter().options.routes;
const menuRef = templateRef<ElRef | null>("menu", null); const menuRef = templateRef<ElRef | null>("menu", null);
const instance = const instance =
@ -91,6 +92,8 @@ function translationEn() {
/> />
</el-menu> </el-menu>
<div class="horizontal-header-right"> <div class="horizontal-header-right">
<!-- 菜单搜索 -->
<Search />
<!-- 通知 --> <!-- 通知 -->
<Notice id="header-notice" /> <Notice id="header-notice" />
<!-- 全屏 --> <!-- 全屏 -->
@ -130,14 +133,14 @@ function translationEn() {
icon="logout-circle-r-line" icon="logout-circle-r-line"
style="margin: 5px" style="margin: 5px"
/> />
{{ $t("buttons.hsLoginOut") }}</el-dropdown-item
{{ t("buttons.hsLoginOut") }}</el-dropdown-item
> >
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<el-icon <el-icon
class="el-icon-setting" class="el-icon-setting"
:title="$t('buttons.hssystemSet')"
:title="t('buttons.hssystemSet')"
@click="onPanel" @click="onPanel"
> >
<IconifyIconOffline icon="setting" /> <IconifyIconOffline icon="setting" />

9
src/layout/components/sidebar/mixNav.vue

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import Search from "../search/index.vue";
import Notice from "../notice/index.vue"; import Notice from "../notice/index.vue";
import { useNav } from "../../hooks/nav"; import { useNav } from "../../hooks/nav";
import { templateRef } from "@vueuse/core"; import { templateRef } from "@vueuse/core";
@ -16,7 +17,7 @@ import globalization from "/@/assets/svg/globalization.svg?component";
import { ref, watch, nextTick, onMounted, getCurrentInstance } from "vue"; import { ref, watch, nextTick, onMounted, getCurrentInstance } from "vue";
const route = useRoute(); const route = useRoute();
const { locale } = useI18n();
const { locale, t } = useI18n();
const routers = useRouter().options.routes; const routers = useRouter().options.routes;
const menuRef = templateRef<ElRef | null>("menu", null); const menuRef = templateRef<ElRef | null>("menu", null);
const instance = const instance =
@ -136,6 +137,8 @@ function translationEn() {
</el-menu-item> </el-menu-item>
</el-menu> </el-menu>
<div class="horizontal-header-right"> <div class="horizontal-header-right">
<!-- 菜单搜索 -->
<Search />
<!-- 通知 --> <!-- 通知 -->
<Notice id="header-notice" /> <Notice id="header-notice" />
<!-- 全屏 --> <!-- 全屏 -->
@ -175,14 +178,14 @@ function translationEn() {
icon="logout-circle-r-line" icon="logout-circle-r-line"
style="margin: 5px" style="margin: 5px"
/> />
{{ $t("buttons.hsLoginOut") }}</el-dropdown-item
{{ t("buttons.hsLoginOut") }}</el-dropdown-item
> >
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<el-icon <el-icon
class="el-icon-setting" class="el-icon-setting"
:title="$t('buttons.hssystemSet')"
:title="t('buttons.hssystemSet')"
@click="onPanel" @click="onPanel"
> >
<IconifyIconOffline icon="setting" /> <IconifyIconOffline icon="setting" />

23
src/layout/components/tag/index.vue

@ -19,12 +19,12 @@ import closeLeft from "/@/assets/svg/close_left.svg?component";
import closeOther from "/@/assets/svg/close_other.svg?component"; import closeOther from "/@/assets/svg/close_other.svg?component";
import closeRight from "/@/assets/svg/close_right.svg?component"; import closeRight from "/@/assets/svg/close_right.svg?component";
import { useI18n } from "vue-i18n";
import { emitter } from "/@/utils/mitt"; import { emitter } from "/@/utils/mitt";
import { $t as t } from "/@/plugins/i18n";
import { transformI18n } from "/@/plugins/i18n";
import { storageLocal } from "/@/utils/storage"; import { storageLocal } from "/@/utils/storage";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { isEqual, isEmpty } from "lodash-unified"; import { isEqual, isEmpty } from "lodash-unified";
import { transformI18n, $t } from "/@/plugins/i18n";
import { RouteConfigs, tagsViewsType } from "../../types"; import { RouteConfigs, tagsViewsType } from "../../types";
import { useSettingStoreHook } from "/@/store/modules/settings"; import { useSettingStoreHook } from "/@/store/modules/settings";
import { handleAliveRoute, delAliveRoutes } from "/@/router/utils"; import { handleAliveRoute, delAliveRoutes } from "/@/router/utils";
@ -33,6 +33,7 @@ import { usePermissionStoreHook } from "/@/store/modules/permission";
import { toggleClass, removeClass, hasClass } from "/@/utils/operate"; import { toggleClass, removeClass, hasClass } from "/@/utils/operate";
import { templateRef, useResizeObserver, useDebounceFn } from "@vueuse/core"; import { templateRef, useResizeObserver, useDebounceFn } from "@vueuse/core";
const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const translateX = ref<number>(0); const translateX = ref<number>(0);
@ -193,42 +194,42 @@ const handleScroll = (offset: number): void => {
const tagsViews = reactive<Array<tagsViewsType>>([ const tagsViews = reactive<Array<tagsViewsType>>([
{ {
icon: refresh, icon: refresh,
text: t("buttons.hsreload"),
text: $t("buttons.hsreload"),
divided: false, divided: false,
disabled: false, disabled: false,
show: true show: true
}, },
{ {
icon: close, icon: close,
text: t("buttons.hscloseCurrentTab"),
text: $t("buttons.hscloseCurrentTab"),
divided: false, divided: false,
disabled: multiTags.value.length > 1 ? false : true, disabled: multiTags.value.length > 1 ? false : true,
show: true show: true
}, },
{ {
icon: closeLeft, icon: closeLeft,
text: t("buttons.hscloseLeftTabs"),
text: $t("buttons.hscloseLeftTabs"),
divided: true, divided: true,
disabled: multiTags.value.length > 1 ? false : true, disabled: multiTags.value.length > 1 ? false : true,
show: true show: true
}, },
{ {
icon: closeRight, icon: closeRight,
text: t("buttons.hscloseRightTabs"),
text: $t("buttons.hscloseRightTabs"),
divided: false, divided: false,
disabled: multiTags.value.length > 1 ? false : true, disabled: multiTags.value.length > 1 ? false : true,
show: true show: true
}, },
{ {
icon: closeOther, icon: closeOther,
text: t("buttons.hscloseOtherTabs"),
text: $t("buttons.hscloseOtherTabs"),
divided: true, divided: true,
disabled: multiTags.value.length > 2 ? false : true, disabled: multiTags.value.length > 2 ? false : true,
show: true show: true
}, },
{ {
icon: closeAll, icon: closeAll,
text: t("buttons.hscloseAllTabs"),
text: $t("buttons.hscloseAllTabs"),
divided: false, divided: false,
disabled: multiTags.value.length > 1 ? false : true, disabled: multiTags.value.length > 1 ? false : true,
show: true show: true
@ -701,7 +702,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
> >
<li v-if="item.show" @click="selectTag(key, item)"> <li v-if="item.show" @click="selectTag(key, item)">
<component :is="item.icon" :key="key" /> <component :is="item.icon" :key="key" />
{{ $t(item.text) }}
{{ t(item.text) }}
</li> </li>
</div> </div>
</ul> </ul>
@ -710,7 +711,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
<ul class="right-button"> <ul class="right-button">
<li> <li>
<el-icon <el-icon
:title="$t('buttons.hsrefreshRoute')"
:title="t('buttons.hsrefreshRoute')"
class="el-icon-refresh-right rotate" class="el-icon-refresh-right rotate"
@click="onFresh" @click="onFresh"
> >
@ -740,7 +741,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
:key="key" :key="key"
style="margin-right: 6px" style="margin-right: 6px"
/> />
{{ $t(item.text) }}
{{ t(item.text) }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>

26
src/layout/hooks/useBoolean.ts

@ -0,0 +1,26 @@
import { ref } from "vue";
export default function useBoolean(initValue = false) {
const bool = ref(initValue);
function setBool(value: boolean) {
bool.value = value;
}
function setTrue() {
setBool(true);
}
function setFalse() {
setBool(false);
}
function toggle() {
setBool(!bool.value);
}
return {
bool,
setBool,
setTrue,
setFalse,
toggle
};
}

0
src/views/redirect.vue → src/layout/redirect.vue

6
src/main.ts

@ -3,7 +3,7 @@ import router from "./router";
import { setupStore } from "/@/store"; import { setupStore } from "/@/store";
import { getServerConfig } from "./config"; import { getServerConfig } from "./config";
import { createApp, Directive } from "vue"; import { createApp, Directive } from "vue";
import { usI18n } from "../src/plugins/i18n";
import { useI18n } from "../src/plugins/i18n";
import { MotionPlugin } from "@vueuse/motion"; import { MotionPlugin } from "@vueuse/motion";
import { useElementPlus } from "../src/plugins/element-plus"; import { useElementPlus } from "../src/plugins/element-plus";
import { injectResponsiveStorage } from "/@/utils/storage/responsive"; import { injectResponsiveStorage } from "/@/utils/storage/responsive";
@ -12,6 +12,8 @@ import "animate.css";
import "virtual:windi.css"; import "virtual:windi.css";
// 导入公共样式 // 导入公共样式
import "./style/index.scss"; import "./style/index.scss";
import "@pureadmin/components/dist/index.css";
import "@pureadmin/components/dist/theme.css";
// 导入字体图标 // 导入字体图标
import "./assets/iconfont/iconfont.js"; import "./assets/iconfont/iconfont.js";
import "./assets/iconfont/iconfont.css"; import "./assets/iconfont/iconfont.css";
@ -39,6 +41,6 @@ getServerConfig(app).then(async config => {
await router.isReady(); await router.isReady();
injectResponsiveStorage(app, config); injectResponsiveStorage(app, config);
setupStore(app); setupStore(app);
app.use(MotionPlugin).use(useElementPlus).use(usI18n);
app.use(MotionPlugin).use(useI18n).use(useElementPlus);
app.mount("#app"); app.mount("#app");
}); });

6
src/plugins/element-plus/index.ts

@ -32,6 +32,8 @@ import {
ElEmpty, ElEmpty,
ElCollapse, ElCollapse,
ElCollapseItem, ElCollapseItem,
ElDialog,
ElCard,
// 指令 // 指令
ElLoading, ElLoading,
ElInfiniteScroll ElInfiniteScroll
@ -72,7 +74,9 @@ const components = [
ElAvatar, ElAvatar,
ElEmpty, ElEmpty,
ElCollapse, ElCollapse,
ElCollapseItem
ElCollapseItem,
ElDialog,
ElCard
]; ];
export function useElementPlus(app: App) { export function useElementPlus(app: App) {

72
src/plugins/i18n.ts

@ -0,0 +1,72 @@
// 多组件库的国际化和本地项目国际化兼容
import { App, WritableComputedRef } from "vue";
import { storageLocal } from "/@/utils/storage";
import { type I18n, createI18n } from "vue-i18n";
// element-plus国际化
import enLocale from "element-plus/lib/locale/lang/en";
import zhLocale from "element-plus/lib/locale/lang/zh-cn";
function siphonI18n(prefix = "zh-CN") {
return Object.fromEntries(
Object.entries(import.meta.globEager("../../locales/*.y(a)?ml")).map(
([key, value]) => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)[1];
return [matched, value.default];
}
)
)[prefix];
}
export const localesConfigs = {
zh: {
...siphonI18n("zh-CN"),
...zhLocale
},
en: {
...siphonI18n("en"),
...enLocale
}
};
/**
*
* @param message message
* @param isI18n true,,
* @returns message
*/
export function transformI18n(
message: string | unknown | object = "",
isI18n: boolean | unknown = false
) {
if (!message) {
return "";
}
// 处理存储动态路由的title,格式 {zh:"",en:""}
if (typeof message === "object") {
const locale: string | WritableComputedRef<string> | any =
i18n.global.locale;
return message[locale?.value];
}
if (isI18n) {
return i18n.global.t.call(i18n.global.locale, message);
} else {
return message;
}
}
// 此函数只是配合i18n Ally插件来进行国际化智能提示,并无实际意义(只对提示起作用),如果不需要国际化可删除
export const $t = (key: string) => key;
export const i18n: I18n = createI18n({
legacy: false,
locale: storageLocal.getItem("responsive-locale")?.locale ?? "zh",
fallbackLocale: "en",
messages: localesConfigs
});
export function useI18n(app: App) {
app.use(i18n);
}

20
src/plugins/i18n/config.ts

@ -1,20 +0,0 @@
import { siphonI18n } from "./index";
// element-plus国际化
import enLocale from "element-plus/lib/locale/lang/en";
import zhLocale from "element-plus/lib/locale/lang/zh-cn";
// 项目内自定义国际化
const zhModules = import.meta.globEager("./zh-CN/**/*.ts");
const enModules = import.meta.globEager("./en/**/*.ts");
export const localesConfigs = {
zh: {
...siphonI18n(zhModules, "zh-CN"),
...zhLocale
},
en: {
...siphonI18n(enModules, "en"),
...enLocale
}
};

21
src/plugins/i18n/en/buttons.ts

@ -1,21 +0,0 @@
export default {
hsLoginOut: "LoginOut",
hsfullscreen: "FullScreen",
hsexitfullscreen: "ExitFullscreen",
hsrefreshRoute: "RefreshRoute",
hslogin: "Login",
hsadd: "Add",
hsmark: "Mark/Cancel",
hssave: "Save",
hssearch: "Search",
hsexpendAll: "Expand All",
hscollapseAll: "Collapse All",
hssystemSet: "Open ProjectConfig",
hsdelete: "Delete",
hsreload: "Reload",
hscloseCurrentTab: "Close CurrentTab",
hscloseLeftTabs: "Close LeftTabs",
hscloseRightTabs: "Close RightTabs",
hscloseOtherTabs: "Close OtherTabs",
hscloseAllTabs: "Close AllTabs"
};

22
src/plugins/i18n/en/menus.ts

@ -1,22 +0,0 @@
export default {
hshome: "Home",
hslogin: "Login",
hssysManagement: "System Manage",
hsBaseinfo: "Base Info",
hserror: "Error Page",
hsfourZeroFour: "404",
hsfourZeroOne: "403",
hsFive: "500",
hsmenus: "MultiLevel Menu",
hsmenu1: "Menu1",
"hsmenu1-1": "Menu1-1",
"hsmenu1-2": "Menu1-2",
"hsmenu1-2-1": "Menu1-2-1",
"hsmenu1-2-2": "Menu1-2-2",
"hsmenu1-3": "Menu1-3",
hsmenu2: "Menu2",
permission: "Permission Manage",
permissionPage: "Page Permission",
permissionButton: "Button Permission",
externalLink: "External Link"
};

77
src/plugins/i18n/index.ts

@ -1,77 +0,0 @@
// 多组件库的国际化和本地项目国际化兼容
import { App } from "vue";
import { set } from "lodash-unified";
import { createI18n } from "vue-i18n";
import { localesConfigs } from "./config";
import { storageLocal } from "/@/utils/storage";
/**
*
* @param message message
* @param isI18n true,,
* @returns message
*/
export function transformI18n(
message: string | unknown | object = "",
isI18n: boolean | unknown = false
) {
if (!message) {
return "";
}
// 处理存储动态路由的title,格式 {zh:"",en:""}
if (typeof message === "object") {
return message[i18n.global?.locale];
}
if (isI18n) {
//@ts-ignore
return i18n.global.tc.call(i18n.global, message);
} else {
return message;
}
}
/**
*
* @param langs
* @param prefix zh-CN
* @returns obj {.**}
*/
export function siphonI18n(
langs: Record<string, Record<string, any>>,
prefix = "zh-CN"
) {
const langsObj: Recordable = {};
Object.keys(langs).forEach((key: string) => {
let fileName = key.replace(`./${prefix}/`, "").replace(/^\.\//, "");
fileName = fileName.substring(0, fileName.lastIndexOf("."));
const keyList = fileName.split("/");
const moduleName = keyList.shift();
const objKey = keyList.join(".");
const langFileModule = langs[key].default;
if (moduleName) {
if (objKey) {
set(langsObj, moduleName, langsObj[moduleName] || {});
set(langsObj[moduleName], objKey, langFileModule);
} else {
set(langsObj, moduleName, langFileModule || {});
}
}
});
return langsObj;
}
// 此函数只是配合i18n Ally插件来进行国际化智能提示,并无实际意义(只对提示起作用),如果不需要国际化可删除
export const $t = (key: string) => key;
export const i18n = createI18n({
locale: storageLocal.getItem("responsive-locale")?.locale ?? "zh",
fallbackLocale: "en",
messages: localesConfigs
});
export function usI18n(app: App) {
app.use(i18n);
}

21
src/plugins/i18n/zh-CN/buttons.ts

@ -1,21 +0,0 @@
export default {
hsLoginOut: "退出系统",
hsfullscreen: "全屏",
hsexitfullscreen: "退出全屏",
hsrefreshRoute: "刷新路由",
hslogin: "登陆",
hsadd: "新增",
hsmark: "标记/取消",
hssave: "保存",
hssearch: "搜索",
hsexpendAll: "全部展开",
hscollapseAll: "全部折叠",
hssystemSet: "打开项目配置",
hsdelete: "删除",
hsreload: "重新加载",
hscloseCurrentTab: "关闭当前标签页",
hscloseLeftTabs: "关闭左侧标签页",
hscloseRightTabs: "关闭右侧标签页",
hscloseOtherTabs: "关闭其他标签页",
hscloseAllTabs: "关闭全部标签页"
};

22
src/plugins/i18n/zh-CN/menus.ts

@ -1,22 +0,0 @@
export default {
hshome: "首页",
hslogin: "登陆",
hssysManagement: "系统管理",
hsBaseinfo: "基础信息",
hserror: "错误页面",
hsfourZeroFour: "404",
hsfourZeroOne: "403",
hsFive: "500",
hsmenus: "多级菜单",
hsmenu1: "菜单1",
"hsmenu1-1": "菜单1-1",
"hsmenu1-2": "菜单1-2",
"hsmenu1-2-1": "菜单1-2-1",
"hsmenu1-2-2": "菜单1-2-2",
"hsmenu1-3": "菜单1-3",
hsmenu2: "菜单2",
permission: "权限管理",
permissionPage: "页面权限",
permissionButton: "按钮权限",
externalLink: "外链"
};

3
src/router/modules/error.ts

@ -3,11 +3,10 @@ const Layout = () => import("/@/layout/index.vue");
const errorRouter = { const errorRouter = {
path: "/error", path: "/error",
name: "error",
component: Layout, component: Layout,
redirect: "/error/403", redirect: "/error/403",
meta: { meta: {
icon: "position",
icon: "information-line",
title: $t("menus.hserror"), title: $t("menus.hserror"),
i18n: true, i18n: true,
rank: 9 rank: 9

25
src/router/modules/externalLink.ts

@ -1,25 +0,0 @@
import { $t } from "/@/plugins/i18n";
const Layout = () => import("/@/layout/index.vue");
const externalLink = {
path: "/externals",
component: Layout,
meta: {
icon: "link",
title: $t("menus.externalLink"),
i18n: true,
rank: 190
},
children: [
{
path: "/external",
name: "https://pure-admin-doc.vercel.app",
meta: {
title: $t("menus.externalLink"),
i18n: true
}
}
]
};
export default externalLink;

3
src/router/modules/index.ts

@ -1,7 +1,6 @@
// 静态路由 // 静态路由
import homeRouter from "./home"; import homeRouter from "./home";
import errorRouter from "./error"; import errorRouter from "./error";
import externalLink from "./externalLink";
import remainingRouter from "./remaining"; import remainingRouter from "./remaining";
import { RouteRecordRaw, RouteComponent } from "vue-router"; import { RouteRecordRaw, RouteComponent } from "vue-router";
@ -13,7 +12,7 @@ import {
import { buildHierarchyTree } from "/@/utils/tree"; import { buildHierarchyTree } from "/@/utils/tree";
// 原始静态路由(未做任何处理) // 原始静态路由(未做任何处理)
const routes = [homeRouter, errorRouter, externalLink];
const routes = [homeRouter, errorRouter];
// 导出处理后的静态路由(三级及以上的路由全部拍成二级) // 导出处理后的静态路由(三级及以上的路由全部拍成二级)
export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes( export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes(

3
src/router/modules/remaining.ts

@ -15,7 +15,6 @@ const remainingRouter = [
}, },
{ {
path: "/redirect", path: "/redirect",
name: "redirect",
component: Layout, component: Layout,
meta: { meta: {
icon: "home-filled", icon: "home-filled",
@ -28,7 +27,7 @@ const remainingRouter = [
{ {
path: "/redirect/:path(.*)", path: "/redirect/:path(.*)",
name: "redirect", name: "redirect",
component: () => import("/@/views/redirect.vue")
component: () => import("/@/layout/redirect.vue")
} }
] ]
} }

5
src/style/element-plus.scss

@ -36,6 +36,11 @@
z-index: 99999 !important; z-index: 99999 !important;
} }
// 自定义popover的类名
.pure-popper {
padding: 0 !important;
}
/* 动态改变cssvar 用于主题切换 https://github.com/element-plus/element-plus/issues/4856#issuecomment-1000174357 */ /* 动态改变cssvar 用于主题切换 https://github.com/element-plus/element-plus/issues/4856#issuecomment-1000174357 */
.el-button--primary { .el-button--primary {
--el-button-active-bg-color: var(--el-color-primary-active) !important; --el-button-active-bg-color: var(--el-color-primary-active) !important;

6
src/style/sidebar.scss

@ -216,6 +216,12 @@
} }
} }
.search-container {
&:hover {
background: $menuHover;
}
}
.screen-full { .screen-full {
cursor: pointer; cursor: pointer;

54
src/views/error/403.vue

@ -6,9 +6,57 @@ import noAccess from "/@/assets/status/403.svg?component";
<div class="flex justify-center items-center h-screen-sm"> <div class="flex justify-center items-center h-screen-sm">
<noAccess /> <noAccess />
<div class="ml-12"> <div class="ml-12">
<p class="font-medium text-4xl mb-4">403</p>
<p class="mb-4 text-gray-500">抱歉你无权访问该页面</p>
<el-button type="primary" @click="$router.push('/')">返回首页</el-button>
<p
class="font-medium text-4xl mb-4"
v-motion
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 100
}
}"
>
403
</p>
<p
class="mb-4 text-gray-500"
v-motion
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 300
}
}"
>
抱歉你无权访问该页面
</p>
<el-button
type="primary"
@click="$router.push('/')"
v-motion
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 500
}
}"
>返回首页</el-button
>
</div> </div>
</div> </div>
</template> </template>

54
src/views/error/404.vue

@ -6,9 +6,57 @@ import noExist from "/@/assets/status/404.svg?component";
<div class="flex justify-center items-center h-screen-sm"> <div class="flex justify-center items-center h-screen-sm">
<noExist /> <noExist />
<div class="ml-12"> <div class="ml-12">
<p class="font-medium text-4xl mb-4">404</p>
<p class="mb-4 text-gray-500">抱歉你访问的页面不存在</p>
<el-button type="primary" @click="$router.push('/')">返回首页</el-button>
<p
class="font-medium text-4xl mb-4"
v-motion
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 100
}
}"
>
404
</p>
<p
class="mb-4 text-gray-500"
v-motion
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 300
}
}"
>
抱歉你访问的页面不存在
</p>
<el-button
type="primary"
@click="$router.push('/')"
v-motion
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 500
}
}"
>返回首页</el-button
>
</div> </div>
</div> </div>
</template> </template>

54
src/views/error/500.vue

@ -6,9 +6,57 @@ import noServer from "/@/assets/status/500.svg?component";
<div class="flex justify-center items-center h-screen-sm"> <div class="flex justify-center items-center h-screen-sm">
<noServer /> <noServer />
<div class="ml-12"> <div class="ml-12">
<p class="font-medium text-4xl mb-4">403</p>
<p class="mb-4 text-gray-500">抱歉服务器出错了</p>
<el-button type="primary" @click="$router.push('/')">返回首页</el-button>
<p
class="font-medium text-4xl mb-4"
v-motion
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 100
}
}"
>
403
</p>
<p
class="mb-4 text-gray-500"
v-motion
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 300
}
}"
>
抱歉服务器出错了
</p>
<el-button
type="primary"
@click="$router.push('/')"
v-motion
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 500
}
}"
>返回首页</el-button
>
</div> </div>
</div> </div>
</template> </template>

8
src/views/permission/button/index.vue

@ -20,12 +20,16 @@ function changRole(value) {
</script> </script>
<template> <template>
<div>
<el-card>
<template #header>
<div class="card-header">
<el-radio-group v-model="auth" @change="changRole"> <el-radio-group v-model="auth" @change="changRole">
<el-radio-button label="admin"></el-radio-button> <el-radio-button label="admin"></el-radio-button>
<el-radio-button label="test"></el-radio-button> <el-radio-button label="test"></el-radio-button>
</el-radio-group> </el-radio-group>
</div>
</template>
<p v-auth="'v-admin'">只有admin可看</p> <p v-auth="'v-admin'">只有admin可看</p>
<p v-auth="'v-test'">只有test可看</p> <p v-auth="'v-test'">只有test可看</p>
</div>
</el-card>
</template> </template>

14
src/views/permission/page/index.vue

@ -29,19 +29,23 @@ function changRole() {
</script> </script>
<template> <template>
<div>
<h4>
<el-card>
<template #header>
<div class="card-header">
<span>
当前角色 当前角色
<span style="font-size: 26px">{{ purview }}</span> <span style="font-size: 26px">{{ purview }}</span>
<p style="color: #ffa500"> <p style="color: #ffa500">
查看左侧菜单变化(系统管理)模拟后台根据不同角色返回对应路由 查看左侧菜单变化(系统管理)模拟后台根据不同角色返回对应路由
</p>
</h4>
</p></span
>
</div>
</template>
<el-button <el-button
type="primary" type="primary"
@click="changRole" @click="changRole"
:icon="useRenderIcon('user', { color: '#fff' })" :icon="useRenderIcon('user', { color: '#fff' })"
>切换角色</el-button >切换角色</el-button
> >
</div>
</el-card>
</template> </template>

6
src/views/type.ts

@ -1,6 +0,0 @@
export type infoType = {
svg?: string;
code?: number;
info?: string;
accessToken?: string;
};

4
vite.config.ts

@ -16,9 +16,7 @@ const pathResolve = (dir: string): string => {
// 设置别名 // 设置别名
const alias: Record<string, string> = { const alias: Record<string, string> = {
"/@": pathResolve("src"), "/@": pathResolve("src"),
"@build": pathResolve("build"),
//解决开发环境下的警告
"vue-i18n": "vue-i18n/dist/vue-i18n.cjs.js"
"@build": pathResolve("build")
}; };
const { dependencies, devDependencies, name, version } = pkg; const { dependencies, devDependencies, name, version } = pkg;

Loading…
Cancel
Save