Compare commits

...

11 Commits

Author SHA1 Message Date
xiaoxian521
f80fbbed20 release: update 3.2.0 2022-03-22 00:37:24 +08:00
xiaoxian521
12c2365a26 perf: route rank is null 2022-03-17 19:53:19 +08:00
xiaoxian521
8a926c509f chore: update pnpm-lock 2022-03-17 19:07:07 +08:00
xiaoxian521
e87c38a9d2 perf: router rank 2022-03-17 19:00:01 +08:00
xiaoxian521
340a79d286 fix: router 2022-03-17 18:12:44 +08:00
xiaoxian521
45743a7c74 feat: use @pureadmin/theme 2022-03-17 15:45:11 +08:00
xiaoxian521
6887d4b1b8 chore: update 2022-03-17 12:14:22 +08:00
xiaoxian521
b850783ca7 chore: update dependencies 2022-03-17 11:28:54 +08:00
xiaoxian521
05e55ae9a1 perf: 同步完整版代码 2022-03-14 19:46:29 +08:00
xiaoxian521
f5b387231a perf: 同步完整版代码 2022-03-14 14:49:02 +08:00
xiaoxian521
79ebfb9284 perf: 同步完整版代码 2022-03-04 11:17:08 +08:00
69 changed files with 1864 additions and 1529 deletions

View File

@@ -61,6 +61,18 @@ module.exports = {
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-non-null-assertion": "off",
"vue/html-self-closing": [
"error",
{
html: {
void: "always",
normal: "always",
component: "always"
},
svg: "always",
math: "always"
}
],
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": [
"error", "error",
{ {

View File

@@ -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 vendored
View File

@@ -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": "locales",
"i18n-ally.localesPaths": ["src/plugins/i18n"],
"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": ["yaml", "js"],
"i18n-ally.enabledParsers": ["ts"],
"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"]
} }

View File

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

View File

@@ -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,16 +7,23 @@ 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";
import themePreprocessorPlugin from "@zougt/vite-plugin-theme-preprocessor"; import themePreprocessorPlugin from "@pureadmin/theme";
export function getPluginsList(command, VITE_LEGACY) { export function getPluginsList(command, VITE_LEGACY) {
const prodMock = true; const prodMock = true;
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 Normal file
View File

@@ -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 Normal file
View File

@@ -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: 按钮权限

View File

@@ -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: [
{ {

View File

@@ -1,11 +1,7 @@
{ {
"name": "pure-admin-thin", "name": "pure-admin-thin",
"version": "3.1.0", "version": "3.2.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,30 +26,32 @@
], ],
"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.11.0",
"element-plus": "^2.0.3", "element-plus": "^2.1.4",
"element-resize-detector": "^1.2.3", "element-resize-detector": "^1.2.3",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lodash-unified": "^1.0.2", "lodash-unified": "^1.0.2",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"path": "^0.12.7", "path": "^0.12.7",
"pinia": "^2.0.11", "pinia": "^2.0.12",
"qs": "^6.10.2", "qs": "^6.10.2",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"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-i18n": "^9.2.0-beta.32",
"vue-router": "^4.0.13", "vue-router": "^4.0.14",
"vue-types": "^4.1.1" "vue-types": "^4.1.1"
}, },
"devDependencies": { "devDependencies": {
@@ -63,9 +61,12 @@
"@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",
"@pureadmin/theme": "^0.0.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": "^4.14.180",
"@types/lodash-es": "^4.17.6", "@types/lodash-es": "^4.17.6",
"@types/mockjs": "1.0.3", "@types/mockjs": "1.0.3",
"@types/node": "14.14.14", "@types/node": "14.14.14",
@@ -78,7 +79,6 @@
"@vitejs/plugin-vue-jsx": "^1.3.8", "@vitejs/plugin-vue-jsx": "^1.3.8",
"@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^10.0.0", "@vue/eslint-config-typescript": "^10.0.0",
"@zougt/vite-plugin-theme-preprocessor": "^1.4.4",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "^8.8.0", "eslint": "^8.8.0",
@@ -94,23 +94,23 @@
"prettier": "^2.5.1", "prettier": "^2.5.1",
"pretty-quick": "3.1.1", "pretty-quick": "3.1.1",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"rollup": "^2.70.1",
"rollup-plugin-visualizer": "^5.6.0", "rollup-plugin-visualizer": "^5.6.0",
"sass": "^1.49.0", "sass": "^1.49.9",
"sass-loader": "^12.4.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", "typescript": "^4.6.2",
"unplugin-element-plus": "^0.2.0", "unplugin-element-plus": "^0.3.2",
"vite": "^2.8.6", "vite": "^2.9.0-beta.4",
"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"

1696
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{ {
"Version": "3.1.0", "Version": "3.2.0",
"Title": "PureAdmin", "Title": "PureAdmin",
"FixedHeader": true, "FixedHeader": true,
"HiddenSideBar": false, "HiddenSideBar": false,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

After

Width:  |  Height:  |  Size: 448 B

View File

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

After

Width:  |  Height:  |  Size: 477 B

View File

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

View File

@@ -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" />
<!-- 全屏 --> <!-- 全屏 -->
@@ -79,10 +82,11 @@ function translationEn() {
<el-dropdown-item <el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')" :style="getDropdownItemStyle(locale, 'en')"
@click="translationEn" @click="translationEn"
><el-icon class="check-en" v-show="locale === 'en'"
><IconifyIconOffline icon="check" /></el-icon
>English</el-dropdown-item
> >
<span class="check-en" v-show="locale === 'en'">
<IconifyIconOffline icon="check" /> </span
>English
</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
@@ -98,18 +102,18 @@ 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 <span
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" />
</el-icon> </span>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -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,38 +13,51 @@ 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" <span class="header-notice-icon">
><IconifyIconOffline icon="bell" <IconifyIconOffline icon="bell" />
/></el-icon> </span>
</el-badge> </el-badge>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-tabs v-model="activeName" class="dropdown-tabs"> <Tabs
centered
class="dropdown-tabs"
v-model:activeName="activeName"
@tabClick="tabClick"
>
<template v-for="item in notices" :key="item.key"> <template v-for="item in notices" :key="item.key">
<el-tab-pane <TabPane :tab="`${item.name}(${item.list.length})`">
:label="`${item.name}(${item.list.length})`"
:name="item.name"
>
<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>

View File

@@ -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() {
@@ -50,7 +50,7 @@ function hoverDescription(event, description) {
:size="30" :size="30"
:src="props.noticeItem.avatar" :src="props.noticeItem.avatar"
class="notice-container-avatar" class="notice-container-avatar"
></el-avatar> />
<div class="notice-container-text"> <div class="notice-container-text">
<div class="notice-text-title"> <div class="notice-text-title">
<el-tooltip <el-tooltip

View File

@@ -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: {
@@ -17,7 +17,7 @@ const props = defineProps({
v-for="(item, index) in props.list" v-for="(item, index) in props.list"
:noticeItem="item" :noticeItem="item"
:key="index" :key="index"
></NoticeItem> />
</div> </div>
<el-empty v-else description="暂无数据"></el-empty> <el-empty v-else description="暂无数据" />
</template> </template>

View File

@@ -26,7 +26,7 @@ emitter.on("openPanel", () => {
<IconifyIconOffline icon="close" /> <IconifyIconOffline icon="close" />
</el-icon> </el-icon>
</div> </div>
<div style="border-bottom: 1px solid #dcdfe6"></div> <div style="border-bottom: 1px solid #dcdfe6" />
<slot /> <slot />
</div> </div>
</div> </div>

View File

@@ -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 isFullscreen ? t('buttons.hsexitfullscreen') : t('buttons.hsfullscreen')
? $t('buttons.hsexitfullscreen')
: $t('buttons.hsfullscreen')
" "
:icon="isFullscreen ? 'team-iconexit-fullscreen' : 'team-iconfullscreen'" :icon="isFullscreen ? 'team-iconexit-fullscreen' : 'team-iconfullscreen'"
/> />

View File

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

View File

@@ -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>
<span class="el-input__icon">
<IconifyIconOffline icon="search" />
</span>
</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>

View File

@@ -0,0 +1,91 @@
<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')" />
<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>

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import { storageLocal, storageSession } from "/@/utils/storage"; import { storageLocal, storageSession } from "/@/utils/storage";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { createNewStyle, writeNewStyle } from "../../theme/element-plus"; import { createNewStyle, writeNewStyle } from "../../theme/element-plus";
import { toggleTheme } from "@zougt/vite-plugin-theme-preprocessor/dist/browser-utils"; import { toggleTheme } from "@pureadmin/theme/dist/browser-utils";
import dayIcon from "/@/assets/svg/day.svg?component"; import dayIcon from "/@/assets/svg/day.svg?component";
import darkIcon from "/@/assets/svg/dark.svg?component"; import darkIcon from "/@/assets/svg/dark.svg?component";
@@ -316,8 +316,7 @@ nextTick(() => {
:active-icon="dayIcon" :active-icon="dayIcon"
:inactive-icon="darkIcon" :inactive-icon="darkIcon"
@change="dataThemeChange" @change="dataThemeChange"
> />
</el-switch>
<el-divider>导航栏模式</el-divider> <el-divider>导航栏模式</el-divider>
<ul class="pure-theme"> <ul class="pure-theme">
@@ -327,8 +326,8 @@ nextTick(() => {
ref="verticalRef" ref="verticalRef"
@click="setLayoutModel('vertical')" @click="setLayoutModel('vertical')"
> >
<div></div> <div />
<div></div> <div />
</li> </li>
</el-tooltip> </el-tooltip>
@@ -338,8 +337,8 @@ nextTick(() => {
ref="horizontalRef" ref="horizontalRef"
@click="setLayoutModel('horizontal')" @click="setLayoutModel('horizontal')"
> >
<div></div> <div />
<div></div> <div />
</li> </li>
</el-tooltip> </el-tooltip>
@@ -349,8 +348,8 @@ nextTick(() => {
ref="mixRef" ref="mixRef"
@click="setLayoutModel('mix')" @click="setLayoutModel('mix')"
> >
<div></div> <div />
<div></div> <div />
</li> </li>
</el-tooltip> </el-tooltip>
</ul> </ul>
@@ -384,8 +383,7 @@ nextTick(() => {
active-text="" active-text=""
inactive-text="" inactive-text=""
@change="greyChange" @change="greyChange"
> />
</el-switch>
</li> </li>
<li v-show="!dataTheme"> <li v-show="!dataTheme">
<span>色弱模式</span> <span>色弱模式</span>
@@ -396,8 +394,7 @@ nextTick(() => {
active-text="" active-text=""
inactive-text="" inactive-text=""
@change="weekChange" @change="weekChange"
> />
</el-switch>
</li> </li>
<li> <li>
<span>隐藏标签页</span> <span>隐藏标签页</span>
@@ -408,8 +405,7 @@ nextTick(() => {
active-text="" active-text=""
inactive-text="" inactive-text=""
@change="tagsChange" @change="tagsChange"
> />
</el-switch>
</li> </li>
<li> <li>
<span>侧边栏Logo</span> <span>侧边栏Logo</span>
@@ -422,8 +418,7 @@ nextTick(() => {
active-text="" active-text=""
inactive-text="" inactive-text=""
@change="logoChange" @change="logoChange"
> />
</el-switch>
</li> </li>
<li> <li>
<span>标签页持久化</span> <span>标签页持久化</span>
@@ -434,8 +429,7 @@ nextTick(() => {
active-text="" active-text=""
inactive-text="" inactive-text=""
@change="multiTagsCacheChange" @change="multiTagsCacheChange"
> />
</el-switch>
</li> </li>
<li> <li>

View File

@@ -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 =
@@ -68,11 +69,7 @@ function translationEn() {
<template> <template>
<div class="horizontal-header"> <div class="horizontal-header">
<div class="horizontal-header-left" @click="backHome"> <div class="horizontal-header-left" @click="backHome">
<FontIcon <FontIcon icon="team-iconlogo" svg style="width: 35px; height: 35px" />
icon="team-iconlogo"
svg
style="width: 35px; height: 35px"
></FontIcon>
<h4>{{ title }}</h4> <h4>{{ title }}</h4>
</div> </div>
<el-menu <el-menu
@@ -91,6 +88,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" />
<!-- 全屏 --> <!-- 全屏 -->
@@ -103,15 +102,17 @@ function translationEn() {
<el-dropdown-item <el-dropdown-item
:style="getDropdownItemStyle(locale, 'zh')" :style="getDropdownItemStyle(locale, 'zh')"
@click="translationCh" @click="translationCh"
><el-icon class="check-zh" v-show="locale === 'zh'"
><IconifyIconOffline icon="check" /></el-icon
>简体中文</el-dropdown-item
> >
<span class="check-zh" v-show="locale === 'zh'">
<IconifyIconOffline icon="check" /> </span
>简体中文
</el-dropdown-item>
<el-dropdown-item <el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')" :style="getDropdownItemStyle(locale, 'en')"
@click="translationEn" @click="translationEn"
><el-icon class="check-en" v-show="locale === 'en'" >
><IconifyIconOffline icon="check" /></el-icon <span class="check-en" v-show="locale === 'en'">
<IconifyIconOffline icon="check" /> </span
>English</el-dropdown-item >English</el-dropdown-item
> >
</el-dropdown-menu> </el-dropdown-menu>
@@ -130,18 +131,18 @@ 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 <span
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" />
</el-icon> </span>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -18,11 +18,7 @@ const title =
class="sidebar-logo-link" class="sidebar-logo-link"
to="/" to="/"
> >
<FontIcon <FontIcon icon="team-iconlogo" svg style="width: 35px; height: 35px" />
icon="team-iconlogo"
svg
style="width: 35px; height: 35px"
></FontIcon>
<span class="sidebar-title">{{ title }}</span> <span class="sidebar-title">{{ title }}</span>
</router-link> </router-link>
<router-link <router-link
@@ -32,11 +28,7 @@ const title =
class="sidebar-logo-link" class="sidebar-logo-link"
to="/" to="/"
> >
<FontIcon <FontIcon icon="team-iconlogo" svg style="width: 35px; height: 35px" />
icon="team-iconlogo"
svg
style="width: 35px; height: 35px"
></FontIcon>
<span class="sidebar-title">{{ title }}</span> <span class="sidebar-title">{{ title }}</span>
</router-link> </router-link>
</transition> </transition>

View File

@@ -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 =
@@ -118,11 +119,9 @@ function translationEn() {
:index="resolvePath(route) || route.redirect" :index="resolvePath(route) || route.redirect"
> >
<template #title> <template #title>
<el-icon v-show="route.meta.icon" :class="route.meta.icon"> <div v-show="route.meta.icon" :class="['el-icon', route.meta.icon]">
<component <component :is="useRenderIcon(route.meta && route.meta.icon)" />
:is="useRenderIcon(route.meta && route.meta.icon)" </div>
></component>
</el-icon>
<span>{{ transformI18n(route.meta.title, route.meta.i18n) }}</span> <span>{{ transformI18n(route.meta.title, route.meta.i18n) }}</span>
<FontIcon <FontIcon
v-if="route.meta.extraIcon" v-if="route.meta.extraIcon"
@@ -131,11 +130,13 @@ function translationEn() {
style="position: absolute; right: 10px" style="position: absolute; right: 10px"
:icon="route.meta.extraIcon.name" :icon="route.meta.extraIcon.name"
:svg="route.meta.extraIcon.svg ? true : false" :svg="route.meta.extraIcon.svg ? true : false"
></FontIcon> />
</template> </template>
</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" />
<!-- 全屏 --> <!-- 全屏 -->
@@ -148,15 +149,15 @@ function translationEn() {
<el-dropdown-item <el-dropdown-item
:style="getDropdownItemStyle(locale, 'zh')" :style="getDropdownItemStyle(locale, 'zh')"
@click="translationCh" @click="translationCh"
><el-icon class="check-zh" v-show="locale === 'zh'" ><span class="check-zh" v-show="locale === 'zh'"
><IconifyIconOffline icon="check" /></el-icon ><IconifyIconOffline icon="check" /></span
>简体中文</el-dropdown-item >简体中文</el-dropdown-item
> >
<el-dropdown-item <el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')" :style="getDropdownItemStyle(locale, 'en')"
@click="translationEn" @click="translationEn"
><el-icon class="check-en" v-show="locale === 'en'" ><span class="check-en" v-show="locale === 'en'"
><IconifyIconOffline icon="check" /></el-icon ><IconifyIconOffline icon="check" /></span
>English</el-dropdown-item >English</el-dropdown-item
> >
</el-dropdown-menu> </el-dropdown-menu>
@@ -175,18 +176,18 @@ 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 <span
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" />
</el-icon> </span>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -148,7 +148,7 @@ function resolvePath(routePath) {
:class="{ 'submenu-title-noDropdown': !isNest }" :class="{ 'submenu-title-noDropdown': !isNest }"
:style="getNoDropdownStyle" :style="getNoDropdownStyle"
> >
<el-icon v-show="props.item.meta.icon"> <div class="el-icon" v-show="props.item.meta.icon">
<component <component
:is=" :is="
useRenderIcon( useRenderIcon(
@@ -156,8 +156,8 @@ function resolvePath(routePath) {
(props.item.meta && props.item.meta.icon) (props.item.meta && props.item.meta.icon)
) )
" "
></component> />
</el-icon> </div>
<div <div
v-if=" v-if="
!pureApp.sidebar.opened && !pureApp.sidebar.opened &&
@@ -203,7 +203,7 @@ function resolvePath(routePath) {
:style="getExtraIconStyle" :style="getExtraIconStyle"
:icon="onlyOneChild.meta.extraIcon.name" :icon="onlyOneChild.meta.extraIcon.name"
:svg="onlyOneChild.meta.extraIcon.svg ? true : false" :svg="onlyOneChild.meta.extraIcon.svg ? true : false"
></FontIcon> />
</div> </div>
</template> </template>
</el-menu-item> </el-menu-item>
@@ -216,11 +216,14 @@ function resolvePath(routePath) {
popper-append-to-body popper-append-to-body
> >
<template #title> <template #title>
<el-icon v-show="props.item.meta.icon" :class="props.item.meta.icon"> <div
v-show="props.item.meta.icon"
:class="['el-icon', props.item.meta.icon]"
>
<component <component
:is="useRenderIcon(props.item.meta && props.item.meta.icon)" :is="useRenderIcon(props.item.meta && props.item.meta.icon)"
></component> />
</el-icon> </div>
<span v-if="!menuMode">{{ <span v-if="!menuMode">{{
transformI18n(props.item.meta.title, props.item.meta.i18n) transformI18n(props.item.meta.title, props.item.meta.i18n)
}}</span> }}</span>
@@ -250,7 +253,7 @@ function resolvePath(routePath) {
style="position: absolute; right: 10px" style="position: absolute; right: 10px"
:icon="props.item.meta.extraIcon.name" :icon="props.item.meta.extraIcon.name"
:svg="props.item.meta.extraIcon.svg ? true : false" :svg="props.item.meta.extraIcon.svg ? true : false"
></FontIcon> />
</template> </template>
<sidebar-item <sidebar-item
v-for="child in props.item.children" v-for="child in props.item.children"

View File

@@ -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
@@ -428,6 +429,7 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
} }
function handleCommand(command: object) { function handleCommand(command: object) {
// @ts-expect-error
const { key, item } = command; const { key, item } = command;
onClickDrop(key, item); onClickDrop(key, item);
} }
@@ -662,7 +664,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
<router-link :to="item.path" <router-link :to="item.path"
>{{ transformI18n(item.meta.title, item.meta.i18n) }} >{{ transformI18n(item.meta.title, item.meta.i18n) }}
</router-link> </router-link>
<el-icon <span
v-if=" v-if="
iconIsActive(item, index) || iconIsActive(item, index) ||
(index === activeIndex && index !== 0) (index === activeIndex && index !== 0)
@@ -671,12 +673,12 @@ const getContextMenuStyle = computed((): CSSProperties => {
@click.stop="deleteMenu(item)" @click.stop="deleteMenu(item)"
> >
<IconifyIconOffline icon="close-bold" /> <IconifyIconOffline icon="close-bold" />
</el-icon> </span>
<div <div
:ref="'schedule' + index" :ref="'schedule' + index"
v-if="showModel !== 'card'" v-if="showModel !== 'card'"
:class="[scheduleIsActive(item)]" :class="[scheduleIsActive(item)]"
></div> />
</div> </div>
</div> </div>
</div> </div>
@@ -701,7 +703,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>
@@ -709,13 +711,13 @@ const getContextMenuStyle = computed((): CSSProperties => {
<!-- 右侧功能按钮 --> <!-- 右侧功能按钮 -->
<ul class="right-button"> <ul class="right-button">
<li> <li>
<el-icon <span
:title="$t('buttons.hsrefreshRoute')" :title="t('buttons.hsrefreshRoute')"
class="el-icon-refresh-right rotate" class="el-icon-refresh-right rotate"
@click="onFresh" @click="onFresh"
> >
<IconifyIconOffline icon="refresh-right" /> <IconifyIconOffline icon="refresh-right" />
</el-icon> </span>
</li> </li>
<li> <li>
<el-dropdown <el-dropdown
@@ -723,9 +725,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
placement="bottom-end" placement="bottom-end"
@command="handleCommand" @command="handleCommand"
> >
<el-icon> <IconifyIconOffline icon="arrow-down" />
<IconifyIconOffline icon="arrow-down" />
</el-icon>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item <el-dropdown-item
@@ -740,14 +740,14 @@ 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>
</el-dropdown> </el-dropdown>
</li> </li>
<li> <li>
<slot></slot> <slot />
</li> </li>
</ul> </ul>
</div> </div>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="frame" v-loading="loading"> <div class="frame" v-loading="loading">
<iframe :src="frameSrc" class="frame-iframe" ref="frameRef"></iframe> <iframe :src="frameSrc" class="frame-iframe" ref="frameRef" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@@ -1,12 +1,12 @@
import { computed } from "vue"; import { computed } from "vue";
import { router } from "/@/router"; import { router } from "/@/router";
import { getConfig } from "/@/config";
import { emitter } from "/@/utils/mitt"; import { emitter } from "/@/utils/mitt";
import { routeMetaType } from "../types"; import { routeMetaType } from "../types";
import { transformI18n } from "/@/plugins/i18n"; import { transformI18n } from "/@/plugins/i18n";
import { storageSession } from "/@/utils/storage"; import { storageSession } from "/@/utils/storage";
import { useAppStoreHook } from "/@/store/modules/app"; import { useAppStoreHook } from "/@/store/modules/app";
import { remainingPaths } from "/@/router/modules/index"; import { remainingPaths } from "/@/router/modules/index";
import { Title } from "../../../public/serverConfig.json";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme"; import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
export function useNav() { export function useNav() {
@@ -30,6 +30,7 @@ export function useNav() {
// 动态title // 动态title
function changeTitle(meta: routeMetaType) { function changeTitle(meta: routeMetaType) {
const Title = getConfig().Title;
if (Title) if (Title)
document.title = `${transformI18n(meta.title, meta.i18n)} | ${Title}`; document.title = `${transformI18n(meta.title, meta.i18n)} | ${Title}`;
else document.title = transformI18n(meta.title, meta.i18n); else document.title = transformI18n(meta.title, meta.i18n);

View File

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

View File

@@ -16,5 +16,5 @@ replace({
</script> </script>
<template> <template>
<div></div> <div />
</template> </template>

View File

@@ -1,8 +1,8 @@
/* 动态改变element-plus主题色 */ /* 动态改变element-plus主题色 */
import rgbHex from "rgb-hex"; import rgbHex from "rgb-hex";
import { convert } from "css-color-function"; import epCss from "./element.scss";
import { TinyColor } from "@ctrl/tinycolor"; import { TinyColor } from "@ctrl/tinycolor";
import epCss from "element-plus/dist/index.css"; import { convert } from "css-color-function";
// 色值表 // 色值表
const formula = { const formula = {

View File

@@ -0,0 +1,2 @@
/* 通过scss模块本地导入element-plus的全局样式文件解决vite2.7.13版本后使用 import epCss from "element-plus/dist/index.css",打包后加载不到样式的问题 */
@import "element-plus/dist/index.css";

View File

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

View File

@@ -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 Normal file
View File

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

View File

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

View File

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

View File

@@ -1,21 +0,0 @@
export default {
hshome: "Home",
hslogin: "Login",
hssysManagement: "System Manage",
hsBaseinfo: "Base Info",
hserror: "Error Page",
hsfourZeroFour: "404",
hsfourZeroOne: "401",
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"
};

View File

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

View File

@@ -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: "关闭全部标签页"
};

View File

@@ -1,21 +0,0 @@
export default {
hshome: "首页",
hslogin: "登陆",
hssysManagement: "系统管理",
hsBaseinfo: "基础信息",
hserror: "错误页面",
hsfourZeroFour: "404",
hsfourZeroOne: "401",
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: "外链"
};

View File

@@ -1,4 +1,5 @@
import { isUrl } from "/@/utils/is"; import { isUrl } from "/@/utils/is";
import { getConfig } from "/@/config";
import { toRouteType } from "./types"; import { toRouteType } from "./types";
import { openLink } from "/@/utils/link"; import { openLink } from "/@/utils/link";
import NProgress from "/@/utils/progress"; import NProgress from "/@/utils/progress";
@@ -7,7 +8,6 @@ import { findIndex } from "lodash-unified";
import { transformI18n } from "/@/plugins/i18n"; import { transformI18n } from "/@/plugins/i18n";
import remainingRouter from "./modules/remaining"; import remainingRouter from "./modules/remaining";
import { storageSession } from "/@/utils/storage"; import { storageSession } from "/@/utils/storage";
import { Title } from "../../public/serverConfig.json";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { usePermissionStoreHook } from "/@/store/modules/permission"; import { usePermissionStoreHook } from "/@/store/modules/permission";
import { Router, RouteMeta, createRouter, RouteRecordName } from "vue-router"; import { Router, RouteMeta, createRouter, RouteRecordName } from "vue-router";
@@ -57,6 +57,7 @@ router.beforeEach((to: toRouteType, _from, next) => {
if (!externalLink) if (!externalLink)
to.matched.some(item => { to.matched.some(item => {
if (!item.meta.title) return ""; if (!item.meta.title) return "";
const Title = getConfig().Title;
if (Title) if (Title)
document.title = `${transformI18n( document.title = `${transformI18n(
item.meta.title, item.meta.title,

View File

@@ -3,20 +3,19 @@ const Layout = () => import("/@/layout/index.vue");
const errorRouter = { const errorRouter = {
path: "/error", path: "/error",
name: "error",
component: Layout, component: Layout,
redirect: "/error/401", redirect: "/error/403",
meta: { meta: {
icon: "position", icon: "information-line",
title: $t("menus.hserror"), title: $t("menus.hserror"),
i18n: true, i18n: true,
rank: 7 rank: 9
}, },
children: [ children: [
{ {
path: "/error/401", path: "/error/403",
name: "401", name: "403",
component: () => import("/@/views/error/401.vue"), component: () => import("/@/views/error/403.vue"),
meta: { meta: {
title: $t("menus.hsfourZeroOne"), title: $t("menus.hsfourZeroOne"),
i18n: true i18n: true
@@ -30,6 +29,15 @@ const errorRouter = {
title: $t("menus.hsfourZeroFour"), title: $t("menus.hsfourZeroFour"),
i18n: true i18n: true
} }
},
{
path: "/error/500",
name: "500",
component: () => import("/@/views/error/500.vue"),
meta: {
title: $t("menus.hsFive"),
i18n: true
}
} }
] ]
}; };

View File

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

View File

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

View File

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

View File

@@ -22,6 +22,14 @@ import { getAsyncRoutes } from "/@/api/routes";
// 按照路由中meta下的rank等级升序来排序路由 // 按照路由中meta下的rank等级升序来排序路由
function ascending(arr: any[]) { function ascending(arr: any[]) {
arr.forEach(v => {
if (v?.meta?.rank === null) v.meta.rank = undefined;
if (v?.meta?.rank === 0) {
if (v.name !== "home" && v.path !== "/") {
console.warn("rank only the home page can be 0");
}
}
});
return arr.sort( return arr.sort(
(a: { meta: { rank: number } }, b: { meta: { rank: number } }) => { (a: { meta: { rank: number } }, b: { meta: { rank: number } }) => {
return a?.meta?.rank - b?.meta?.rank; return a?.meta?.rank - b?.meta?.rank;

View File

@@ -1,7 +1,7 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { store } from "/@/store"; import { store } from "/@/store";
import { userType } from "./types"; import { userType } from "./types";
import { useRouter } from "vue-router"; import { router } from "/@/router";
import { getLogin, refreshToken } from "/@/api/user"; import { getLogin, refreshToken } from "/@/api/user";
import { storageLocal, storageSession } from "/@/utils/storage"; import { storageLocal, storageSession } from "/@/utils/storage";
import { getToken, setToken, removeToken } from "/@/utils/auth"; import { getToken, setToken, removeToken } from "/@/utils/auth";
@@ -64,7 +64,7 @@ export const useUserStore = defineStore({
} }
} }
]); ]);
useRouter().push("/login"); router.push("/login");
}, },
// 刷新token // 刷新token
async refreshToken(data) { async refreshToken(data) {

View File

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

View File

@@ -202,7 +202,7 @@
.horizontal-header-right { .horizontal-header-right {
display: flex; display: flex;
min-width: 280px; min-width: 340px;
align-items: center; align-items: center;
color: $subMenuActiveText; color: $subMenuActiveText;
justify-content: flex-end; justify-content: flex-end;
@@ -216,6 +216,12 @@
} }
} }
.search-container {
&:hover {
background: $menuHover;
}
}
.screen-full { .screen-full {
cursor: pointer; cursor: pointer;
@@ -603,15 +609,12 @@ body[layout="vertical"] {
} }
} }
/* 无子菜单 */ /* 无子菜单 */
.el-sub-menu__title,
.el-menu-item [class^="el-icon"] { .el-menu-item [class^="el-icon"] {
right: 5px; right: 5px;
} }
.el-sub-menu__title [class^="el-icon"] {
right: 2px;
}
.submenu-title-noDropdown { .submenu-title-noDropdown {
background: transparent !important; background: transparent !important;
} }

View File

@@ -1,73 +0,0 @@
<script setup lang="ts">
import imgs from "/@/assets/401.gif";
import { ref } from "vue";
const img = ref(`${imgs}?${new Date()}`);
</script>
<template>
<div class="errPage-container">
<el-row>
<el-col :span="12">
<h1 class="text-jumbo text-ginormous">Pure Admin</h1>
<h2>你没有权限去该页面</h2>
</el-col>
<el-col :span="12">
<img
:src="img"
width="313"
height="428"
alt="Girl has dropped her ice cream."
/>
</el-col>
</el-row>
</div>
</template>
<style lang="scss" scoped>
.errPage-container {
width: 800px;
max-width: 100%;
margin: 100px auto;
.pan-back-btn {
background: #008489;
color: #fff;
border: none !important;
}
.pan-gif {
margin: 0 auto;
display: block;
}
.pan-img {
display: block;
margin: 0 auto;
width: 100%;
}
.text-jumbo {
font-size: 60px;
font-weight: 700;
color: #484848;
}
.list-unstyled {
font-size: 14px;
li {
padding-bottom: 5px;
}
a {
color: #008489;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
</style>

62
src/views/error/403.vue Normal file
View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import noAccess from "/@/assets/status/403.svg?component";
</script>
<template>
<div class="flex justify-center items-center h-screen-sm">
<noAccess />
<div class="ml-12">
<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>
</template>

View File

@@ -1,250 +1,62 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import noExist from "/@/assets/status/404.svg?component";
import four from "/@/assets/404.png";
import four_cloud from "/@/assets/404_cloud.png";
const message = computed(() => {
return "The webmaster said that you can not enter this page...";
});
</script> </script>
<template> <template>
<div class="wscn-http404-container"> <div class="flex justify-center items-center h-screen-sm">
<div class="wscn-http404"> <noExist />
<div class="pic-404"> <div class="ml-12">
<img class="pic-404__parent" :src="four" alt="404" /> <p
<img class="pic-404__child left" :src="four_cloud" alt="404" /> class="font-medium text-4xl mb-4"
<img class="pic-404__child mid" :src="four_cloud" alt="404" /> v-motion
<img class="pic-404__child right" :src="four_cloud" alt="404" /> :initial="{
</div> opacity: 0,
<div class="bullshit"> y: 100
<div class="bullshit__oops">Pure Admin</div> }"
<div class="bullshit__headline">{{ message }}</div> :enter="{
<div class="bullshit__info"> opacity: 1,
Please check that the URL you entered is correct, or click the button y: 0,
below to return to the homepage. transition: {
</div> delay: 100
<a href="" class="bullshit__return-home">Back to home</a> }
</div> }"
>
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>
<style lang="scss" scoped>
.wscn-http404-container {
transform: translate(-50%, -50%);
position: absolute;
top: 40%;
left: 50%;
}
.wscn-http404 {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
.pic-404 {
position: relative;
float: left;
width: 600px;
overflow: hidden;
&__parent {
width: 100%;
}
&__child {
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px;
left: 420px;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px;
left: 500px;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
}
position: absolute;
&.left {
width: 80px;
top: 17px;
left: 220px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
&.mid {
width: 46px;
top: 10px;
left: 420px;
opacity: 0;
animation-name: cloudMid;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1.2s;
}
&.right {
width: 62px;
top: 100px;
left: 500px;
opacity: 0;
animation-name: cloudRight;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
}
}
.bullshit {
@keyframes slideUp {
0% {
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&__oops {
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__headline {
font-size: 20px;
line-height: 24px;
color: #222;
font-weight: bold;
opacity: 0;
margin-bottom: 10px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&__info {
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
margin-bottom: 30px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #fff;
opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
}
}
</style>

62
src/views/error/500.vue Normal file
View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import noServer from "/@/assets/status/500.svg?component";
</script>
<template>
<div class="flex justify-center items-center h-screen-sm">
<noServer />
<div class="ml-12">
<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>
</template>

View File

@@ -20,12 +20,16 @@ function changRole(value) {
</script> </script>
<template> <template>
<div> <el-card>
<el-radio-group v-model="auth" @change="changRole"> <template #header>
<el-radio-button label="admin"></el-radio-button> <div class="card-header">
<el-radio-button label="test"></el-radio-button> <el-radio-group v-model="auth" @change="changRole">
</el-radio-group> <el-radio-button label="admin" />
<el-radio-button label="test" />
</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>

View File

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

View File

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

View File

@@ -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"), "@build": pathResolve("build")
//解决开发环境下的警告
"vue-i18n": "vue-i18n/dist/vue-i18n.cjs.js"
}; };
const { dependencies, devDependencies, name, version } = pkg; const { dependencies, devDependencies, name, version } = pkg;
@@ -89,7 +87,7 @@ export default ({ command, mode }: ConfigEnv): UserConfigExport => {
"element-plus/lib/locale/lang/en", "element-plus/lib/locale/lang/en",
"element-plus/lib/locale/lang/zh-cn" "element-plus/lib/locale/lang/zh-cn"
], ],
exclude: ["@zougt/vite-plugin-theme-preprocessor/dist/browser-utils"] exclude: ["@pureadmin/theme/dist/browser-utils"]
}, },
build: { build: {
sourcemap: false, sourcemap: false,