release: update 3.4.5

This commit is contained in:
xiaoxian521 2022-08-22 21:34:55 +08:00
parent 13427998f3
commit c07e60e114
133 changed files with 3481 additions and 3277 deletions

View File

@ -8,4 +8,4 @@ VITE_ROUTER_HISTORY = "hash"
VITE_PROXY_DOMAIN_REAL = ""
# 是否为打包后的文件提供传统浏览器兼容性支持 支持 true 不支持 false
VITE_LEGACY = false
VITE_LEGACY = false

View File

@ -49,6 +49,29 @@ module.exports = {
jsx: true
}
},
overrides: [
{
files: ["*.ts", "*.vue"],
rules: {
"no-undef": "off"
}
},
{
files: ["*.vue"],
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
extraFileExtensions: [".vue"],
ecmaVersion: "latest",
ecmaFeatures: {
jsx: true
}
},
rules: {
"no-undef": "off"
}
}
],
rules: {
"vue/no-v-html": "off",
"vue/require-default-prop": "off",

3
.gitignore vendored
View File

@ -4,7 +4,7 @@ dist
dist-ssr
*.local
.eslintcache
.stylelintcache
report.html
yarn.lock
npm-debug.log*
@ -18,3 +18,4 @@ tests/**/coverage/
*.ntvs*
*.njsproj
*.sln
tsconfig.tsbuildinfo

View File

@ -5,11 +5,11 @@
"stylelint.vscode-stylelint",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"johnsoncodehk.volar",
"lokalise.i18n-ally",
"mikestead.dotenv",
"eamodio.gitlens",
"antfu.iconify",
"antfu.unocss"
"antfu.unocss",
"Vue.volar"
]
}

View File

@ -33,5 +33,6 @@
"i18n-ally.enabledParsers": ["yaml", "js"],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": ["vue"]
"i18n-ally.enabledFrameworks": ["vue"],
"iconify.excludes": ["el"]
}

View File

@ -6,7 +6,7 @@
## introduce
The lite version is based on the shelf extracted from [vue-pure-admin](https://github.com/xiaoxian521/vue-pure-admin), which contains the main functions and is more suitable for actual project development, the packaged size is only more than `3MB`
The lite version is based on the shelf extracted from [vue-pure-admin](https://github.com/xiaoxian521/vue-pure-admin), which contains the main functions and is more suitable for actual project development, the packaged size is only `3MB`
## Supporting Video

View File

@ -6,7 +6,7 @@
## 介绍
精简版是基于[vue-pure-admin](https://github.com/xiaoxian521/vue-pure-admin)提炼出的架子,包含主体功能,更适合实际项目开发,打包后的大小仅 `3MB`
精简版是基于[vue-pure-admin](https://github.com/xiaoxian521/vue-pure-admin)提炼出的架子,包含主体功能,更适合实际项目开发,打包后的大小仅 `3MB`
## 配套视频
@ -29,9 +29,9 @@
## QQ 交流群
群里严禁`黄`、`赌`、`毒`、`vpn`等违法行为!
一群已满,下面是二群,群里严禁`黄`、`赌`、`毒`、`vpn`等违法行为!
<img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f0697596aec84661b724f6eebdf8db17~tplv-k3u1fbpfcp-watermark.awebp?" width="150px" height="225px" />
<img src="https://pure-admin-doc.vercel.app/img/support/qq.png" width="150px" height="225px" />
## 用法

View File

@ -30,7 +30,7 @@ const warpperEnv = (envConf: Recordable): ViteEnv => {
// 跨域代理重写
const regExps = (value: string, reg: string): string => {
return value.replace(new RegExp(reg, "g"), "");
return value.replace(new RegExp(`^${reg}`, "g"), "");
};
// 环境变量

View File

@ -1,45 +1,10 @@
import { readdir, stat } from "fs";
import type { Plugin } from "vite";
import dayjs, { Dayjs } from "dayjs";
import { sum } from "lodash-unified";
import duration from "dayjs/plugin/duration";
import { green, blue, bold } from "picocolors";
import { getPackageSize } from "@pureadmin/utils";
dayjs.extend(duration);
const staticPath = "dist";
const fileListTotal: number[] = [];
const recursiveDirectory = (folder: string, callback: Function): void => {
readdir(folder, (err, files: string[]) => {
if (err) throw err;
let count = 0;
const checkEnd = () => {
++count == files.length && callback();
};
files.forEach((item: string) => {
stat(folder + "/" + item, async (err, stats) => {
if (err) throw err;
if (stats.isFile()) {
fileListTotal.push(stats.size);
checkEnd();
} else if (stats.isDirectory()) {
recursiveDirectory(`${staticPath}/${item}/`, checkEnd);
}
});
});
files.length === 0 && callback();
});
};
const formatBytes = (a: number, b?: number): string => {
if (0 == a) return "0 Bytes";
const c = 1024,
d = b || 2,
e = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
f = Math.floor(Math.log(a) / Math.log(c));
return parseFloat((a / Math.pow(c, f)).toFixed(d)) + " " + e[f];
};
export function viteBuildInfo(): Plugin {
let config: { command: string };
let startTime: Dayjs;
@ -50,34 +15,34 @@ export function viteBuildInfo(): Plugin {
config = resolvedConfig;
},
buildStart() {
console.log(
bold(
green(
`👏欢迎使用${blue(
"[vue-pure-admin]"
)}star哦💖 https://github.com/xiaoxian521/vue-pure-admin`
)
)
);
if (config.command === "build") {
startTime = dayjs(new Date());
}
},
closeBundle() {
if (config.command === "build") {
console.log(
bold(
green(
`👏欢迎使用${blue(
"[vue-pure-admin]"
)}star哦💖 https://github.com/xiaoxian521/vue-pure-admin`
)
)
);
endTime = dayjs(new Date());
recursiveDirectory(staticPath, () => {
console.log(
bold(
green(
`恭喜打包完成🎉(总用时${dayjs
.duration(endTime.diff(startTime))
.format("mm分ss秒")}${formatBytes(
sum(fileListTotal)
)}`
getPackageSize({
callback: (size: string) => {
console.log(
bold(
green(
`🎉恭喜打包完成(总用时${dayjs
.duration(endTime.diff(startTime))
.format("mm分ss秒")}${size}`
)
)
)
);
);
}
});
}
}

View File

@ -12,6 +12,7 @@ import { visualizer } from "rollup-plugin-visualizer";
import removeConsole from "vite-plugin-remove-console";
import themePreprocessorPlugin from "@pureadmin/theme";
import { genScssMultipleScopeVars } from "/@/layout/theme";
import DefineOptions from "unplugin-vue-define-options/vite";
export function getPluginsList(command, VITE_LEGACY) {
const prodMock = true;
@ -27,8 +28,9 @@ export function getPluginsList(command, VITE_LEGACY) {
// jsx、tsx语法支持
vueJsx(),
Unocss(),
DefineOptions(),
// 线上环境删除console
removeConsole(),
removeConsole({ external: ["src/assets/iconfont/iconfont.js"] }),
viteBuildInfo(),
// 自定义主题
themePreprocessorPlugin({

View File

@ -2,9 +2,14 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
<title>pure-admin-thin</title>
<link rel="icon" href="/favicon.ico" />
<script>
window.process = {};
</script>

View File

@ -30,3 +30,43 @@ menus:
permissionButton: Button Permission
status:
hsLoad: Loading...
login:
username: Username
password: Password
verifyCode: VerifyCode
remember: Remember Password
sure: Sure Password
forget: Forget Password?
login: Login
thirdLogin: Third Login
phoneLogin: Phone Login
qRCodeLogin: QRCode Login
register: Register
weChatLogin: WeChat Login
alipayLogin: Alipay Login
qqLogin: QQ Login
weiboLogin: Weibo Login
phone: Phone
smsVerifyCode: SMS VerifyCode
back: Back
test: Mock Test
tip: After scanning the code, click "Confirm" to complete the login
definite: Definite
loginSuccess: Login Success
registerSuccess: Regist Success
tickPrivacy: Please tick Privacy Policy
readAccept: I have read it carefully and accept
privacyPolicy: Privacy Policy
getVerifyCode: Get VerifyCode
info: Seconds
usernameReg: Please enter username
passwordReg: Please enter password
verifyCodeReg: Please enter verify code
verifyCodeCorrectReg: Please enter correct verify code
verifyCodeSixReg: Please enter a 6-digit verify code
phoneReg: Please enter the phone
phoneCorrectReg: Please enter the correct phone number format
passwordRuleReg: The password format should be any combination of 8-18 digits
passwordSureReg: Please enter confirm password
passwordDifferentReg: The two passwords do not match!
passwordUpdateReg: Password has been updated

View File

@ -3,7 +3,7 @@ buttons:
hsfullscreen: 全屏
hsexitfullscreen: 退出全屏
hsrefreshRoute: 刷新路由
hslogin:
hslogin:
hsadd: 新增
hsmark: 标记/取消
hssave: 保存
@ -20,7 +20,7 @@ buttons:
hscloseAllTabs: 关闭全部标签页
menus:
hshome: 首页
hslogin:
hslogin:
hserror: 错误页面
hsfourZeroFour: "404"
hsfourZeroOne: "403"
@ -30,3 +30,43 @@ menus:
permissionButton: 按钮权限
status:
hsLoad: 加载中...
login:
username: 账号
password: 密码
verifyCode: 验证码
remember: 记住密码
sure: 确认密码
forget: 忘记密码?
login: 登录
thirdLogin: 第三方登录
phoneLogin: 手机登录
qRCodeLogin: 二维码登录
register: 注册
weChatLogin: 微信登录
alipayLogin: 支付宝登录
qqLogin: QQ登录
weiboLogin: 微博登录
phone: 手机号码
smsVerifyCode: 短信验证码
back: 返回
test: 模拟测试
tip: 扫码后点击"确认",即可完成登录
definite: 确定
loginSuccess: 登录成功
registerSuccess: 注册成功
tickPrivacy: 请勾选隐私政策
readAccept: 我已仔细阅读并接受
privacyPolicy: 《隐私政策》
getVerifyCode: 获取验证码
info: 秒后重新获取
usernameReg: 请输入账号
passwordReg: 请输入密码
verifyCodeReg: 请输入验证码
verifyCodeCorrectReg: 请输入正确的验证码
verifyCodeSixReg: 请输入6位数字验证码
phoneReg: 请输入手机号码
phoneCorrectReg: 请输入正确的手机号码格式
passwordRuleReg: 密码格式应为8-18位数字、字母、符号的任意两种组合
passwordSureReg: 请输入确认密码
passwordDifferentReg: 两次密码不一致!
passwordUpdateReg: 修改密码成功

View File

@ -3,7 +3,6 @@ import { MockMethod } from "vite-plugin-mock";
const permissionRouter = {
path: "/permission",
redirect: "/permission/page/index",
meta: {
title: "menus.permission",
icon: "lollipop",
@ -12,14 +11,14 @@ const permissionRouter = {
children: [
{
path: "/permission/page/index",
name: "permissionPage",
name: "PermissionPage",
meta: {
title: "menus.permissionPage"
}
},
{
path: "/permission/button/index",
name: "permissionButton",
name: "PermissionButton",
meta: {
title: "menus.permissionButton",
authority: []

View File

@ -1,15 +1,17 @@
{
"name": "pure-admin-thin",
"version": "3.3.0",
"version": "3.4.5",
"private": true,
"scripts": {
"dev": "cross-env --max_old_space_size=4096 vite",
"serve": "pnpm dev",
"build": "rimraf dist && cross-env vite build",
"build": "rimraf dist && cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build",
"build:staging": "rimraf dist && cross-env vite build --mode staging",
"report": "rimraf dist && cross-env vite build",
"preview": "vite preview",
"preview:build": "pnpm build && vite preview",
"typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
"cloc": "cross-env --max_old_space_size=4096 cloc . --exclude-dir=node_modules --exclude-lang=YAML",
"clean:cache": "rm -rf node_modules && rm -rf .eslintcache && pnpm install",
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
"lint:prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
@ -27,15 +29,19 @@
],
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
"@pureadmin/components": "^1.0.6",
"@vueuse/core": "^8.4.2",
"@pureadmin/components": "^1.1.0",
"@pureadmin/descriptions": "^1.1.0",
"@pureadmin/table": "^1.2.0",
"@pureadmin/utils": "^0.1.1",
"@vueuse/core": "^9.1.0",
"@vueuse/motion": "^2.0.0-beta.12",
"@vueuse/shared": "^8.4.2",
"@vueuse/shared": "^9.1.0",
"animate.css": "^4.1.1",
"axios": "^0.27.2",
"css-color-function": "^1.3.3",
"dayjs": "^1.11.2",
"element-plus": "2.1.11",
"dayjs": "^1.11.4",
"echarts": "^5.3.3",
"element-plus": "^2.2.14",
"element-resize-detector": "^1.2.3",
"js-cookie": "^3.0.1",
"lodash": "^4.17.21",
@ -45,24 +51,26 @@
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"path": "^0.12.7",
"pinia": "^2.0.14",
"qrcode": "^1.5.0",
"qs": "^6.10.2",
"pinia": "^2.0.19",
"qrcode": "^1.5.1",
"qs": "^6.11.0",
"resize-observer-polyfill": "^1.5.1",
"responsive-storage": "1.0.11",
"responsive-storage": "^2.1.0",
"rgb-hex": "^4.0.0",
"vue": "^3.2.33",
"vue-i18n": "^9.2.0-beta.35",
"vue-router": "^4.0.15",
"vue-types": "^4.1.1"
"vue": "^3.2.37",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.3",
"vue-types": "^4.2.1",
"vxe-table": "^4.3.0-beta.5",
"xe-utils": "^3.5.6"
},
"devDependencies": {
"@commitlint/cli": "13.1.0",
"@commitlint/config-conventional": "13.1.0",
"@iconify-icons/ep": "^1.2.4",
"@iconify-icons/ri": "^1.2.1",
"@iconify/vue": "^3.2.0",
"@intlify/vite-plugin-vue-i18n": "^3.4.0",
"@iconify-icons/ep": "^1.2.7",
"@iconify-icons/ri": "^1.2.3",
"@iconify/vue": "^3.2.1",
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@pureadmin/theme": "^2.4.0",
"@types/element-resize-detector": "1.1.3",
"@types/js-cookie": "^3.0.1",
@ -71,48 +79,62 @@
"@types/mockjs": "1.0.3",
"@types/node": "14.14.14",
"@types/nprogress": "0.2.0",
"@types/qrcode": "^1.4.2",
"@types/qrcode": "^1.5.0",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/parser": "^5.10.2",
"@vitejs/plugin-legacy": "^1.8.2",
"@vitejs/plugin-vue": "^2.3.2",
"@vitejs/plugin-vue-jsx": "^1.3.10",
"@vitejs/plugin-legacy": "^2.0.0",
"@vitejs/plugin-vue": "^3.0.1",
"@vitejs/plugin-vue-jsx": "^2.0.0",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^10.0.0",
"autoprefixer": "^10.4.5",
"@vue/runtime-core": "^3.2.37",
"autoprefixer": "^10.4.8",
"cloc": "^2.10.0",
"cross-env": "7.0.3",
"eslint": "^8.8.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.4.1",
"husky": "7.0.2",
"font-awesome": "^4.7.0",
"husky": "^7.0.4",
"lint-staged": "11.1.2",
"picocolors": "^1.0.0",
"postcss": "^8.4.6",
"postcss-html": "^1.3.0",
"postcss-import": "14.0.0",
"postcss-scss": "^4.0.3",
"postcss": "^8.4.16",
"postcss-html": "^1.5.0",
"postcss-import": "^14.1.0",
"postcss-scss": "^4.0.4",
"prettier": "^2.5.1",
"pretty-quick": "3.1.1",
"rimraf": "3.0.2",
"rollup": "^2.70.1",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "^1.51.0",
"rollup-plugin-visualizer": "^5.7.1",
"sass": "^1.53.0",
"sass-loader": "^13.0.2",
"stylelint": "^14.3.0",
"stylelint-config-html": "^1.0.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-recommended": "^6.0.0",
"stylelint-config-standard": "^24.0.0",
"stylelint-order": "^5.0.0",
"typescript": "^4.6.3",
"unocss": "^0.33.2",
"vite": "^2.9.8",
"typescript": "^4.7.4",
"unocss": "^0.45.9",
"unplugin-vue-define-options": "^0.7.3",
"vite": "^3.0.9",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-remove-console": "^0.0.7",
"vite-svg-loader": "^3.3.0",
"vue-eslint-parser": "^8.2.0"
"vite-plugin-remove-console": "^1.1.0",
"vite-svg-loader": "^3.4.0",
"vue-eslint-parser": "^8.2.0",
"vue-tsc": "^0.40.1"
},
"repository": "git@github.com:xiaoxian521/vue-pure-admin.git",
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"rollup",
"terser",
"webpack"
]
}
},
"repository": "git@github.com:xiaoxian521/pure-admin-thin.git",
"author": "xiaoxian521",
"license": "MIT"
}

1214
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,5 +1,10 @@
import { http } from "../utils/http";
export const getAsyncRoutes = (params?: object) => {
return http.request("get", "/getAsyncRoutes", { params });
type Result = {
code: number;
info: Array<any>;
};
export const getAsyncRoutes = (params?: object) => {
return http.request<Result>("get", "/getAsyncRoutes", { params });
};

View File

@ -1,14 +1,14 @@
import { http } from "../utils/http";
interface userType extends Promise<any> {
type Result = {
svg?: string;
code?: number;
info?: object;
}
};
// 获取验证码
export const getVerify = (): userType => {
return http.request("get", "/captcha");
export const getVerify = () => {
return http.request<Result>("get", "/captcha");
};
// 登录

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -2,12 +2,11 @@ import iconifyIconOffline from "./src/iconifyIconOffline";
import iconifyIconOnline from "./src/iconifyIconOnline";
import fontIcon from "./src/iconfont";
export const IconifyIconOffline = iconifyIconOffline;
export const IconifyIconOnline = iconifyIconOnline;
export const FontIcon = fontIcon;
/** 离线图标组件 */
const IconifyIconOffline = iconifyIconOffline;
/** 在线图标组件 */
const IconifyIconOnline = iconifyIconOnline;
/** iconfont组件 */
const FontIcon = fontIcon;
export default {
IconifyIconOffline,
IconifyIconOnline,
FontIcon
};
export { IconifyIconOffline, IconifyIconOnline, FontIcon };

View File

@ -1,14 +1,14 @@
import { iconType } from "./types";
import { h, defineComponent, Component } from "vue";
import { IconifyIconOffline, FontIcon } from "../index";
import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
/**
* fontawesome45+iconfontremixiconelement-plus的iconssvg
* @param icon string
* @param icon
* @param attrs iconType
* @returns Component
*/
export function useRenderIcon(icon: string, attrs?: iconType): Component {
export function useRenderIcon(icon: any, attrs?: iconType): Component {
// iconfont
const ifReg = /^IF-/;
// typeof icon === "function" 属于SVG
@ -30,14 +30,16 @@ export function useRenderIcon(icon: string, attrs?: iconType): Component {
});
}
});
} else if (typeof icon === "function") {
} else if (typeof icon === "function" || typeof icon?.render === "function") {
// svg
return icon;
} else {
return defineComponent({
name: "Icon",
render() {
return h(IconifyIconOffline, {
const IconifyIcon =
attrs && attrs["online"] ? IconifyIconOnline : IconifyIconOffline;
return h(IconifyIcon, {
icon: icon,
...attrs
});

View File

@ -2,7 +2,7 @@ import { h, defineComponent } from "vue";
// 封装iconfont组件默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code
export default defineComponent({
name: "fontIcon",
name: "FontIcon",
props: {
icon: {
type: String,

View File

@ -28,22 +28,26 @@ import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import InformationLine from "@iconify-icons/ri/information-line";
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
import Bookmark from "@iconify-icons/ri/bookmark-2-line";
import Bookmark2Line from "@iconify-icons/ri/bookmark-2-line";
import User from "@iconify-icons/ri/user-3-fill";
import Lock from "@iconify-icons/ri/lock-fill";
import MenuUnfold from "@iconify-icons/ri/menu-unfold-fill";
import MenuFold from "@iconify-icons/ri/menu-fold-fill";
addIcon("arrow-right-s-line", ArrowRightSLine);
addIcon("arrow-left-s-line", ArrowLeftSLine);
addIcon("logout-circle-r-line", LogoutCircleRLine);
addIcon("information-line", InformationLine);
addIcon("arrow-up-line", ArrowUpLine);
addIcon("arrow-down-line", ArrowDownLine);
addIcon("bookmark", Bookmark);
addIcon("bookmark-2-line", Bookmark2Line);
addIcon("user", User);
addIcon("lock", Lock);
addIcon("menu-unfold", MenuUnfold);
addIcon("menu-fold", MenuFold);
// Iconify Icon在Vue里离线使用用于内网环境https://docs.iconify.design/icon-components/vue/offline.html
export default defineComponent({
name: "IconifyIcon",
name: "IconifyIconOffline",
components: { IconifyIcon },
props: {
icon: {
@ -57,6 +61,9 @@ export default defineComponent({
IconifyIcon,
{
icon: `${this.icon}`,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
...attrs
},
{

View File

@ -1,9 +1,9 @@
import { h, defineComponent } from "vue";
import { Icon as IconifyIcon } from "@iconify/vue";
// Iconify Icon在Vue里在线使用用于外网环境 https://docs.iconify.design/icon-components/vue/offline.html
// Iconify Icon在Vue里在线使用用于外网环境
export default defineComponent({
name: "IconifyIcon",
name: "IconifyIconOnline",
components: { IconifyIcon },
props: {
icon: {
@ -17,6 +17,9 @@ export default defineComponent({
IconifyIcon,
{
icon: `${this.icon}`,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
...attrs
},
{

View File

@ -11,7 +11,9 @@ export interface iconType {
horizontalAlign?: boolean;
verticalAlign?: boolean;
align?: string;
online?: boolean;
onLoad?: Function;
includes?: Function;
// all icon
style?: object;

View File

@ -1,12 +1,7 @@
import { App } from "vue";
import reImageVerify from "./src/index.vue";
import { withInstall } from "@pureadmin/utils";
export const ReImageVerify = Object.assign(reImageVerify, {
install(app: App) {
app.component(reImageVerify.name, reImageVerify);
}
});
/** 图形验证码组件 */
export const ReImageVerify = withInstall(reImageVerify);
export default {
ReImageVerify
};
export default ReImageVerify;

View File

@ -1,13 +1,11 @@
<script lang="ts">
export default {
name: "ReImageVerify"
};
</script>
<script setup lang="ts">
import { watch } from "vue";
import { useImageVerify } from "./hooks";
defineOptions({
name: "ReImageVerify"
});
interface Props {
code?: string;
}

View File

@ -1,10 +1,7 @@
import { App } from "vue";
import reQrcode from "./src/index";
import { withInstall } from "@pureadmin/utils";
export const ReQrcode = Object.assign(reQrcode, {
install(app: App) {
app.component(reQrcode.name, reQrcode);
}
});
/** 二维码组件 */
export const ReQrcode = withInstall(reQrcode);
export default ReQrcode;

View File

@ -8,8 +8,8 @@ import {
defineComponent
} from "vue";
import "./index.scss";
import { isString } from "/@/utils/is";
import { cloneDeep } from "lodash-unified";
import { isString } from "@pureadmin/utils";
import { propTypes } from "/@/utils/propTypes";
import { IconifyIconOffline } from "../../ReIcon";
import QRCode, { QRCodeRenderersOptions } from "qrcode";
@ -78,7 +78,7 @@ export default defineComponent({
const _width: number = await getOriginWidth(unref(renderText), options);
options.scale =
props.width === 0 ? undefined : (props.width / _width) * 4;
const canvasRef: HTMLCanvasElement = await toCanvas(
const canvasRef: any = await toCanvas(
unref(wrapRef) as HTMLCanvasElement,
unref(renderText),
options
@ -246,7 +246,7 @@ export default defineComponent({
>
<div class="absolute top-[50%] left-[50%] font-bold">
<IconifyIconOffline
class="cursor-pointer outline-none"
class="cursor-pointer"
icon="refresh-right"
width="30"
color="var(--el-color-primary)"

View File

@ -1,7 +1,7 @@
import { Directive } from "vue";
import type { DirectiveBinding, VNode } from "vue";
import { Directive, type DirectiveBinding, type VNode } from "vue";
import elementResizeDetectorMaker from "element-resize-detector";
import type { Erd } from "element-resize-detector";
import { optimizeFps } from "@pureadmin/utils";
import { emitter } from "/@/utils/mitt";
const erd: Erd = elementResizeDetectorMaker({
@ -14,7 +14,9 @@ export const resize: Directive = {
const width = elem.offsetWidth;
const height = elem.offsetHeight;
if (binding?.instance) {
emitter.emit("resize", { detail: { width, height } });
optimizeFps(() => {
emitter.emit("resize", { detail: { width, height } });
})();
} else {
vnode.el.dispatchEvent(
new CustomEvent("resize", { detail: { width, height } })

View File

@ -1,23 +1,18 @@
<script setup lang="ts">
import {
h,
ref,
computed,
Transition,
defineComponent,
getCurrentInstance
} from "vue";
import { useGlobal } from "@pureadmin/utils";
import backTop from "/@/assets/svg/back_top.svg?component";
import { h, computed, Transition, defineComponent } from "vue";
import { usePermissionStoreHook } from "/@/store/modules/permission";
const props = defineProps({
fixedHeader: Boolean
});
const keepAlive: Boolean = ref(
getCurrentInstance().appContext.config.globalProperties.$config?.KeepAlive
);
const instance =
getCurrentInstance().appContext.app.config.globalProperties.$storage;
const { $storage, $config } = useGlobal<GlobalPropertiesApi>();
const keepAlive = computed(() => {
return $config?.KeepAlive;
});
const transitions = computed(() => {
return route => {
@ -26,11 +21,11 @@ const transitions = computed(() => {
});
const hideTabs = computed(() => {
return instance?.configure.hideTabs;
return $storage?.configure.hideTabs;
});
const layout = computed(() => {
return instance?.layout.layout === "vertical";
return $storage?.layout.layout === "vertical";
});
const getSectionStyle = computed(() => {

View File

@ -1,65 +1,51 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { useNav } from "../hooks/nav";
import { useRoute } from "vue-router";
import Search from "./search/index.vue";
import Notice from "./notice/index.vue";
import mixNav from "./sidebar/mixNav.vue";
import avatars from "/@/assets/avatars.jpg";
import Hamburger from "./sidebar/hamBurger.vue";
import { watch, getCurrentInstance } from "vue";
import { useNav } from "/@/layout/hooks/useNav";
import Breadcrumb from "./sidebar/breadCrumb.vue";
import { deviceDetection } from "/@/utils/deviceDetection";
import { deviceDetection } from "@pureadmin/utils";
import topCollapse from "./sidebar/topCollapse.vue";
import screenfull from "../components/screenfull/index.vue";
import { useTranslationLang } from "../hooks/useTranslationLang";
import globalization from "/@/assets/svg/globalization.svg?component";
const route = useRoute();
const { locale, t } = useI18n();
const instance =
getCurrentInstance().appContext.config.globalProperties.$storage;
const {
layout,
device,
logout,
onPanel,
changeTitle,
toggleSideBar,
pureApp,
username,
avatarsStyle,
getDropdownItemStyle
toggleSideBar,
getDropdownItemStyle,
getDropdownItemClass
} = useNav();
watch(
() => locale.value,
() => {
changeTitle(route.meta);
}
);
function translationCh() {
instance.locale = { locale: "zh" };
locale.value = "zh";
}
function translationEn() {
instance.locale = { locale: "en" };
locale.value = "en";
}
const { t, locale, translationCh, translationEn } = useTranslationLang();
</script>
<template>
<div class="navbar">
<Hamburger
v-if="pureApp.layout !== 'mix'"
:is-active="pureApp.sidebar.opened"
<div
class="navbar bg-[#fff] shadow-sm shadow-[rgba(0, 21, 41, 0.08)] dark:shadow-[#0d0d0d]"
>
<topCollapse
v-if="device === 'mobile'"
class="hamburger-container"
:is-active="pureApp.sidebar.opened"
@toggleClick="toggleSideBar"
/>
<Breadcrumb v-if="pureApp.layout !== 'mix'" class="breadcrumb-container" />
<Breadcrumb
v-if="layout !== 'mix' && device !== 'mobile'"
class="breadcrumb-container"
/>
<mixNav v-if="pureApp.layout === 'mix'" />
<mixNav v-if="layout === 'mix'" />
<div v-if="pureApp.layout === 'vertical'" class="vertical-header-right">
<div v-if="layout === 'vertical'" class="vertical-header-right">
<!-- 菜单搜索 -->
<Search />
<!-- 通知 -->
@ -68,34 +54,41 @@ function translationEn() {
<screenfull id="header-screenfull" v-show="!deviceDetection()" />
<!-- 国际化 -->
<el-dropdown id="header-translation" trigger="click">
<globalization />
<globalization
class="navbar-bg-hover w-40px h-48px p-11px cursor-pointer outline-none"
/>
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'zh')"
:class="['!dark:color-white', getDropdownItemClass(locale, 'zh')]"
@click="translationCh"
><IconifyIconOffline
>
<IconifyIconOffline
class="check-zh"
v-show="locale === 'zh'"
icon="check"
/></el-dropdown-item
>
/>
简体中文
</el-dropdown-item>
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')"
:class="['!dark:color-white', getDropdownItemClass(locale, 'en')]"
@click="translationEn"
>
<span class="check-en" v-show="locale === 'en'">
<IconifyIconOffline icon="check" /> </span
>English
<IconifyIconOffline icon="check" />
</span>
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 退出登 -->
<!-- 退出登 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link">
<span class="el-dropdown-link navbar-bg-hover">
<img v-if="avatars" :src="avatars" :style="avatarsStyle" />
<p v-if="username">{{ username }}</p>
<p v-if="username" class="dark:color-white">{{ username }}</p>
</span>
<template #dropdown>
<el-dropdown-menu class="logout">
@ -103,13 +96,14 @@ function translationEn() {
<IconifyIconOffline
icon="logout-circle-r-line"
style="margin: 5px"
/>{{ t("buttons.hsLoginOut") }}</el-dropdown-item
>
/>
{{ t("buttons.hsLoginOut") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span
class="el-icon-setting"
class="el-icon-setting navbar-bg-hover"
:title="t('buttons.hssystemSet')"
@click="onPanel"
>
@ -124,16 +118,12 @@ function translationEn() {
width: 100%;
height: 48px;
overflow: hidden;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.hamburger-container {
line-height: 48px;
height: 100%;
float: left;
cursor: pointer;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
}
.vertical-header-right {
@ -144,31 +134,6 @@ function translationEn() {
color: #000000d9;
justify-content: flex-end;
:deep(.dropdown-badge) {
&:hover {
background: #f6f6f6;
}
}
.screen-full {
cursor: pointer;
&:hover {
background: #f6f6f6;
}
}
.globalization {
height: 48px;
width: 40px;
padding: 11px;
cursor: pointer;
&:hover {
background: #f6f6f6;
}
}
.el-dropdown-link {
height: 48px;
padding: 10px;
@ -178,10 +143,6 @@ function translationEn() {
cursor: pointer;
color: #000000d9;
&:hover {
background: #f6f6f6;
}
p {
font-size: 14px;
}
@ -200,15 +161,12 @@ function translationEn() {
display: flex;
cursor: pointer;
align-items: center;
&:hover {
background: #f6f6f6;
}
}
}
.breadcrumb-container {
float: left;
margin-left: 16px;
}
}

View File

@ -15,14 +15,13 @@ notices.value.forEach(notice => {
});
function tabClick() {
// @ts-expect-error
dropdownDom.value.handleOpen();
(dropdownDom as any).value.handleOpen();
}
</script>
<template>
<el-dropdown ref="dropdownDom" trigger="click" placement="bottom-end">
<span class="dropdown-badge">
<span class="dropdown-badge navbar-bg-hover select-none">
<el-badge :value="noticesNum" :max="99">
<span class="header-notice-icon">
<IconifyIconOffline icon="bell" />

View File

@ -44,7 +44,9 @@ function hoverDescription(event, description) {
</script>
<template>
<div class="notice-container">
<div
class="notice-container border-b-1 border-[#f0f0f0] dark:border-[#303030]"
>
<el-avatar
v-if="props.noticeItem.avatar"
:size="30"
@ -52,7 +54,7 @@ function hoverDescription(event, description) {
class="notice-container-avatar"
/>
<div class="notice-container-text">
<div class="notice-text-title">
<div class="notice-text-title color-[#000000d9] dark:color-white">
<el-tooltip
popper-class="notice-title-popper"
:disabled="!titleTooltip"
@ -72,7 +74,8 @@ function hoverDescription(event, description) {
:type="props.noticeItem?.status"
size="small"
class="notice-title-extra"
>{{ props.noticeItem?.extra }}
>
{{ props.noticeItem?.extra }}
</el-tag>
</div>
@ -90,7 +93,7 @@ function hoverDescription(event, description) {
{{ props.noticeItem.description }}
</div>
</el-tooltip>
<div class="notice-text-datetime">
<div class="notice-text-datetime color-[#00000073] dark:color-white">
{{ props.noticeItem.datetime }}
</div>
</div>
@ -108,7 +111,7 @@ function hoverDescription(event, description) {
align-items: flex-start;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
// border-bottom: 1px solid #f0f0f0;
.notice-container-avatar {
margin-right: 16px;
@ -127,7 +130,6 @@ function hoverDescription(event, description) {
font-weight: 400;
font-size: 14px;
line-height: 1.5715;
color: rgba(0, 0, 0, 0.85);
cursor: pointer;
.notice-title-content {
@ -149,7 +151,6 @@ function hoverDescription(event, description) {
.notice-text-datetime {
font-size: 12px;
line-height: 1.5715;
color: rgba(0, 0, 0, 0.45);
}
.notice-text-description {

View File

@ -5,7 +5,7 @@ import { emitter } from "/@/utils/mitt";
let show = ref<Boolean>(false);
const target = ref(null);
onClickOutside(target, event => {
onClickOutside(target, (event: any) => {
if (event.clientX > target.value.offsetLeft) return;
show.value = false;
});
@ -18,15 +18,15 @@ emitter.on("openPanel", () => {
<template>
<div :class="{ show: show }" class="right-panel-container">
<div class="right-panel-background" />
<div ref="target" class="right-panel">
<div ref="target" class="right-panel bg-white dark:bg-dark">
<div class="right-panel-items">
<div class="project-configuration">
<h3>项目配置</h3>
<el-icon title="关闭配置" class="el-icon-close" @click="show = !show">
<IconifyIconOffline icon="close-bold" />
<IconifyIconOffline icon="close" />
</el-icon>
</div>
<div style="border-bottom: 1px solid #dcdfe6" />
<div class="border-b-1 border-[#dcdfe6] dark:border-[#303030]" />
<slot />
</div>
</div>
@ -62,7 +62,7 @@ emitter.on("openPanel", () => {
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.05);
transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
transform: translate(100%);
background: #fff;
// background: #fff;
z-index: 40000;
}
@ -120,7 +120,7 @@ emitter.on("openPanel", () => {
margin-left: 10px;
i {
font-size: 16px;
font-size: 20px;
margin-right: 20px;
&:hover {

View File

@ -1,13 +1,16 @@
<script setup lang="ts">
import { useFullscreen } from "@vueuse/core";
import { useI18n } from "vue-i18n";
import { useFullscreen } from "@vueuse/core";
const { t } = useI18n();
const { isFullscreen, toggle } = useFullscreen();
</script>
<template>
<div class="screen-full" @click="toggle">
<div
class="screen-full w-36px h-48px flex-ac cursor-pointer navbar-bg-hover"
@click="toggle"
>
<FontIcon
:title="
isFullscreen ? t('buttons.hsexitfullscreen') : t('buttons.hsfullscreen')
@ -16,13 +19,3 @@ const { isFullscreen, toggle } = useFullscreen();
/>
</div>
</template>
<style lang="scss" scoped>
.screen-full {
width: 36px;
height: 48px;
display: flex;
align-items: center;
justify-content: space-around;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="search-footer">
<div class="search-footer color-[#333] dark:color-white">
<span class="search-footer-item">
<enterOutlined class="icon" />
确认
@ -23,7 +23,6 @@ import mdiKeyboardEsc from "/@/assets/svg/mdi_keyboard_esc.svg?component";
<style lang="scss" scoped>
.search-footer {
display: flex;
color: #333;
.search-footer-item {
display: flex;

View File

@ -2,8 +2,9 @@
import { useRouter } from "vue-router";
import SearchResult from "./SearchResult.vue";
import SearchFooter from "./SearchFooter.vue";
import { deleteChildren } from "/@/utils/tree";
import { useNav } from "/@/layout/hooks/useNav";
import { transformI18n } from "/@/plugins/i18n";
import { deleteChildren } from "@pureadmin/utils";
import { useDebounceFn, onKeyStroke } from "@vueuse/core";
import { ref, watch, computed, nextTick, shallowRef } from "vue";
import { usePermissionStoreHook } from "/@/store/modules/permission";
@ -17,6 +18,7 @@ interface Emits {
(e: "update:value", val: boolean): void;
}
const { device } = useNav();
const emit = defineEmits<Emits>();
const props = withDefaults(defineProps<Props>(), {});
const router = useRouter();
@ -130,7 +132,12 @@ onKeyStroke("ArrowDown", handleDown);
</script>
<template>
<el-dialog top="5vh" v-model="show" :before-close="handleClose">
<el-dialog
top="5vh"
:width="device === 'mobile' ? '80vw' : '50vw'"
v-model="show"
:before-close="handleClose"
>
<el-input
ref="inputRef"
v-model="keyword"

View File

@ -1,24 +1,3 @@
<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')" />
<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";
@ -49,6 +28,17 @@ interface Emits {
const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<Emits>();
const itemStyle = computed(() => {
return item => {
return {
background:
item?.path === active.value ? useEpThemeStoreHook().epThemeColor : "",
color: item.path === active.value ? "#fff" : "",
fontSize: item.path === active.value ? "16px" : "14px"
};
};
});
const active = computed({
get() {
return props.value;
@ -67,6 +57,24 @@ function handleTo() {
emit("enter");
}
</script>
<template>
<div class="result">
<template v-for="item in options" :key="item.path">
<div
class="result-item dark:bg-[#1d1d1d]"
:style="itemStyle(item)"
@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>
<style lang="scss" scoped>
.result {
padding-bottom: 12px;
@ -78,8 +86,9 @@ function handleTo() {
margin-top: 8px;
padding: 14px;
border-radius: 4px;
background: #e5e7eb;
cursor: pointer;
border: 0.1px solid #ccc;
transition: all 0.3s;
&-title {
display: flex;

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { SearchModal } from "./components";
import useBoolean from "../../hooks/useBoolean";
import { useBoolean } from "../../hooks/useBoolean";
const { bool: show, toggle } = useBoolean();
function handleSearch() {
toggle();
@ -8,23 +8,11 @@ function handleSearch() {
</script>
<template>
<div class="search-container" @click="handleSearch">
<div
class="search-container w-40px h-48px flex-c cursor-pointer navbar-bg-hover"
@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

@ -1,75 +1,57 @@
<script setup lang="ts">
import {
reactive,
ref,
unref,
watch,
reactive,
computed,
nextTick,
useCssModule,
getCurrentInstance
useCssModule
} from "vue";
import { find } from "lodash-unified";
import { getConfig } from "/@/config";
import { useRouter } from "vue-router";
import panel from "../panel/index.vue";
import { emitter } from "/@/utils/mitt";
import { resetRouter } from "/@/router";
import { templateRef } from "@vueuse/core";
import { debounce } from "/@/utils/debounce";
import { themeColorsType } from "../../types";
import { routerArrays } from "/@/layout/types";
import { useNav } from "/@/layout/hooks/useNav";
import { useAppStoreHook } from "/@/store/modules/app";
import { shadeBgColor } from "../../theme/element-plus";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import { storageLocal, storageSession } from "/@/utils/storage";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { createNewStyle, writeNewStyle } from "../../theme/element-plus";
import { useDataThemeChange } from "/@/layout/hooks/useDataThemeChange";
import {
useDark,
debounce,
useGlobal,
storageLocal,
storageSession
} from "@pureadmin/utils";
import { toggleTheme } from "@pureadmin/theme/dist/browser-utils";
import dayIcon from "/@/assets/svg/day.svg?component";
import darkIcon from "/@/assets/svg/dark.svg?component";
const router = useRouter();
const { device } = useNav();
const { isDark } = useDark();
const { isSelect } = useCssModule();
const body = document.documentElement as HTMLElement;
const instance =
getCurrentInstance().appContext.app.config.globalProperties.$storage;
const instanceConfig =
getCurrentInstance().appContext.app.config.globalProperties.$config;
let themeColors = ref<Array<themeColorsType>>([
//
{ color: "#1b2a47", themeColor: "default" },
//
{ color: "#ffffff", themeColor: "light" },
//
{ color: "#f5222d", themeColor: "dusk" },
//
{ color: "#fa541c", themeColor: "volcano" },
//
{ color: "#fadb14", themeColor: "yellow" },
// 绿
{ color: "#13c2c2", themeColor: "mingQing" },
// 绿
{ color: "#52c41a", themeColor: "auroraGreen" },
//
{ color: "#eb2f96", themeColor: "pink" },
//
{ color: "#722ed1", themeColor: "saucePurple" }
]);
const { $storage } = useGlobal<GlobalPropertiesApi>();
const mixRef = templateRef<HTMLElement | null>("mixRef", null);
const verticalRef = templateRef<HTMLElement | null>("verticalRef", null);
const horizontalRef = templateRef<HTMLElement | null>("horizontalRef", null);
const mixRef = templateRef<HTMLElement | null>("mixRef", null);
let layoutTheme =
ref(storageLocal.getItem("responsive-layout")) ||
ref({
layout: instanceConfig?.Layout ?? "vertical",
theme: instanceConfig?.Theme ?? "default"
});
const {
body,
dataTheme,
layoutTheme,
themeColors,
dataThemeChange,
setEpThemeColor,
setLayoutThemeColor
} = useDataThemeChange();
// bodylayoutsrc/style/sidebar.scss
/* body添加layout属性作用于src/style/sidebar.scss */
if (unref(layoutTheme)) {
let layout = unref(layoutTheme).layout;
let theme = unref(layoutTheme).theme;
@ -79,20 +61,18 @@ if (unref(layoutTheme)) {
setLayoutModel(layout);
}
//
const markValue = ref(instance.configure?.showModel ?? "smart");
/** 默认灵动模式 */
const markValue = ref($storage.configure?.showModel ?? "smart");
const logoVal = ref(instance.configure?.showLogo ?? true);
const epThemeColor = ref(useEpThemeStoreHook().getEpThemeColor);
const logoVal = ref($storage.configure?.showLogo ?? true);
const settings = reactive({
greyVal: instance.configure.grey,
weakVal: instance.configure.weak,
tabsVal: instance.configure.hideTabs,
showLogo: instance.configure.showLogo,
showModel: instance.configure.showModel,
multiTagsCache: instance.configure.multiTagsCache
greyVal: $storage.configure.grey,
weakVal: $storage.configure.weak,
tabsVal: $storage.configure.hideTabs,
showLogo: $storage.configure.showLogo,
showModel: $storage.configure.showModel,
multiTagsCache: $storage.configure.multiTagsCache
});
const getThemeColorStyle = computed(() => {
@ -101,10 +81,17 @@ const getThemeColorStyle = computed(() => {
};
});
/** 当网页为暗黑模式时不显示亮白色切换选项 */
const showThemeColors = computed(() => {
return themeColor => {
return themeColor === "light" && isDark.value ? false : true;
};
});
function storageConfigureChange<T>(key: string, val: T): void {
const storageConfigure = instance.configure;
const storageConfigure = $storage.configure;
storageConfigure[key] = val;
instance.configure = storageConfigure;
$storage.configure = storageConfigure;
}
function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
@ -114,13 +101,13 @@ function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
targetEl.className = flag ? `${className} ${clsName} ` : className;
}
//
/** 灰色模式设置 */
const greyChange = (value): void => {
toggleClass(settings.greyVal, "html-grey", document.querySelector("html"));
storageConfigureChange("grey", value);
};
//
/** 色弱模式设置 */
const weekChange = (value): void => {
toggleClass(
settings.weakVal,
@ -133,7 +120,7 @@ const weekChange = (value): void => {
const tagsChange = () => {
let showVal = settings.tabsVal;
storageConfigureChange("hideTabs", showVal);
emitter.emit("tagViewsChange", showVal);
emitter.emit("tagViewsChange", showVal as unknown as string);
};
const multiTagsCacheChange = () => {
@ -142,27 +129,19 @@ const multiTagsCacheChange = () => {
useMultiTagsStoreHook().multiTagsCacheChange(multiTagsCache);
};
//
/** 清空缓存并返回登录页 */
function onReset() {
router.push("/login");
const { Grey, Weak, MultiTagsCache, EpThemeColor, Layout } = getConfig();
useAppStoreHook().setLayout(Layout);
useEpThemeStoreHook().setEpThemeColor(EpThemeColor);
setEpThemeColor(EpThemeColor);
useMultiTagsStoreHook().multiTagsCacheChange(MultiTagsCache);
toggleClass(Grey, "html-grey", document.querySelector("html"));
toggleClass(Weak, "html-weakness", document.querySelector("html"));
useMultiTagsStoreHook().handleTags("equal", [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "menus.hshome",
icon: "home-filled"
}
}
]);
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
storageLocal.clear();
storageSession.clear();
resetRouter();
}
function onChange(label) {
@ -170,7 +149,7 @@ function onChange(label) {
emitter.emit("tagViewsShowModel", label);
}
// Logo
/** 侧边栏Logo */
function logoChange() {
unref(logoVal)
? storageConfigureChange("showLogo", true)
@ -184,7 +163,9 @@ function setFalse(Doms): any {
});
}
watch(instance, ({ layout }) => {
watch($storage, ({ layout }) => {
/* 设置wangeditorV5主题色 */
body.style.setProperty("--w-e-toolbar-active-color", layout["epThemeColor"]);
switch (layout["layout"]) {
case "vertical":
toggleClass(true, isSelect, unref(verticalRef));
@ -204,7 +185,7 @@ watch(instance, ({ layout }) => {
}
});
//
/** 主题色 激活选择项 */
const getThemeColor = computed(() => {
return current => {
if (
@ -223,83 +204,27 @@ const getThemeColor = computed(() => {
};
});
//
/** 设置导航模式 */
function setLayoutModel(layout: string) {
layoutTheme.value.layout = layout;
window.document.body.setAttribute("layout", layout);
instance.layout = {
$storage.layout = {
layout,
theme: layoutTheme.value.theme,
darkMode: instance.layout.darkMode,
sidebarStatus: instance.layout.sidebarStatus,
epThemeColor: instance.layout.epThemeColor
darkMode: $storage.layout?.darkMode,
sidebarStatus: $storage.layout?.sidebarStatus,
epThemeColor: $storage.layout?.epThemeColor
};
useAppStoreHook().setLayout(layout);
}
//
let tempLayoutThemeColor;
//
function setLayoutThemeColor(theme: string) {
tempLayoutThemeColor = instance.layout.theme;
layoutTheme.value.theme = theme;
toggleTheme({
scopeName: `layout-theme-${theme}`
});
instance.layout = {
layout: useAppStoreHook().layout,
theme,
darkMode: dataTheme.value,
sidebarStatus: instance.layout.sidebarStatus,
epThemeColor: instance.layout.epThemeColor
};
if (theme === "default" || theme === "light") {
setEpThemeColor(getConfig().EpThemeColor);
} else {
const colors = find(themeColors.value, { themeColor: theme });
setEpThemeColor(colors.color);
}
}
// ep
const setEpThemeColor = (color: string) => {
// @ts-expect-error
writeNewStyle(createNewStyle(color));
useEpThemeStoreHook().setEpThemeColor(color);
body.style.setProperty("--el-color-primary-active", shadeBgColor(color));
};
let dataTheme = ref<boolean>(instance.layout.darkMode);
//
function dataThemeChange() {
if (dataTheme.value) {
body.setAttribute("data-theme", "dark");
setLayoutThemeColor("light");
} else {
body.setAttribute("data-theme", "");
tempLayoutThemeColor && setLayoutThemeColor(tempLayoutThemeColor);
instance.layout = {
layout: useAppStoreHook().layout,
theme: instance.layout.theme,
darkMode: dataTheme.value,
sidebarStatus: instance.layout.sidebarStatus,
epThemeColor: instance.layout.epThemeColor
};
}
}
//
/* 初始化项目配置 */
nextTick(() => {
settings.greyVal &&
document.querySelector("html")?.setAttribute("class", "html-grey");
settings.weakVal &&
document.querySelector("html")?.setAttribute("class", "html-weakness");
settings.tabsVal && tagsChange();
// @ts-expect-error
writeNewStyle(createNewStyle(epThemeColor.value));
dataThemeChange();
});
</script>
@ -329,7 +254,12 @@ nextTick(() => {
</li>
</el-tooltip>
<el-tooltip class="item" content="顶部模式" placement="bottom">
<el-tooltip
v-if="device !== 'mobile'"
class="item"
content="顶部模式"
placement="bottom"
>
<li
:class="layoutTheme.layout === 'horizontal' ? $style.isSelect : ''"
ref="horizontalRef"
@ -340,7 +270,12 @@ nextTick(() => {
</li>
</el-tooltip>
<el-tooltip class="item" content="混合模式" placement="bottom">
<el-tooltip
v-if="device !== 'mobile'"
class="item"
content="混合模式"
placement="bottom"
>
<li
:class="layoutTheme.layout === 'mix' ? $style.isSelect : ''"
ref="mixRef"
@ -352,11 +287,12 @@ nextTick(() => {
</el-tooltip>
</ul>
<el-divider v-show="!dataTheme">主题色</el-divider>
<ul class="theme-color" v-show="!dataTheme">
<el-divider>主题色</el-divider>
<ul class="theme-color">
<li
v-for="(item, index) in themeColors"
:key="index"
v-show="showThemeColors(item.themeColor)"
:style="getThemeColorStyle(item.color)"
@click="setLayoutThemeColor(item.themeColor)"
>
@ -372,7 +308,7 @@ nextTick(() => {
<el-divider>界面显示</el-divider>
<ul class="setting">
<li v-show="!dataTheme">
<li>
<span>灰色模式</span>
<el-switch
v-model="settings.greyVal"
@ -383,7 +319,7 @@ nextTick(() => {
@change="greyChange"
/>
</li>
<li v-show="!dataTheme">
<li>
<span>色弱模式</span>
<el-switch
v-model="settings.weakVal"
@ -446,13 +382,13 @@ nextTick(() => {
@click="onReset"
>
<IconifyIconOffline
icon="logout-circle-r-line"
icon="fa-sign-out"
width="15"
height="15"
style="margin-right: 4px"
/>
清空缓存并返回登录页</el-button
>
清空缓存并返回登录页
</el-button>
</panel>
</template>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { isEqual } from "lodash-unified";
import { transformI18n } from "/@/plugins/i18n";
import { ref, watch, onMounted, toRaw } from "vue";
import { getParentPaths, findRouteByPath } from "/@/router/utils";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { useRoute, useRouter, RouteLocationMatched } from "vue-router";
@ -9,24 +9,29 @@ import { useRoute, useRouter, RouteLocationMatched } from "vue-router";
const route = useRoute();
const levelList = ref([]);
const router = useRouter();
const routes = router.options.routes;
const multiTags = useMultiTagsStoreHook().multiTags;
const routes: any = router.options.routes;
const multiTags: any = useMultiTagsStoreHook().multiTags;
const isDashboard = (route: RouteLocationMatched): boolean | string => {
const name = route && (route.name as string);
if (!name) {
return false;
}
return name.trim().toLocaleLowerCase() === "welcome".toLocaleLowerCase();
if (!name) return false;
return name.trim().toLocaleLowerCase() === "Welcome".toLocaleLowerCase();
};
const getBreadcrumb = (): void => {
//
let currentRoute;
if (Object.keys(route.query).length > 0) {
multiTags.forEach(item => {
if (isEqual(route.query, item?.query)) {
currentRoute = item;
currentRoute = toRaw(item);
}
});
} else if (Object.keys(route.params).length > 0) {
multiTags.forEach(item => {
if (isEqual(route.params, item?.params)) {
currentRoute = toRaw(item);
}
});
} else {
@ -38,29 +43,12 @@ const getBreadcrumb = (): void => {
let matched = [];
//
parentRoutes.forEach(path => {
if (path !== "/") {
matched.push(findRouteByPath(path, routes));
}
if (path !== "/") matched.push(findRouteByPath(path, routes));
});
if (router.currentRoute.value.meta?.refreshRedirect) {
matched.unshift(
findRouteByPath(
router.currentRoute.value.meta.refreshRedirect as string,
routes
)
);
} else {
//
matched = matched.filter(item => {
return !item.redirect || (item.redirect && item.children.length !== 1);
});
}
if (currentRoute?.path !== "/welcome") {
matched.push(currentRoute);
}
const first = matched[0];
if (!isDashboard(first)) {
if (currentRoute?.path !== "/welcome") matched.push(currentRoute);
if (!isDashboard(matched[0])) {
matched = [
{
path: "/welcome",
@ -70,59 +58,51 @@ const getBreadcrumb = (): void => {
].concat(matched);
}
matched.forEach((item, index) => {
if (currentRoute?.query || currentRoute?.params) return;
if (item?.children) {
item.children.forEach(v => {
if (v?.meta?.title === item?.meta?.title) {
matched.splice(index, 1);
}
});
}
});
levelList.value = matched.filter(
item => item?.meta && item?.meta.title !== false
);
};
getBreadcrumb();
const handleLink = (item: RouteLocationMatched): void => {
const { redirect, path } = item;
if (redirect) {
router.push(redirect as any);
} else {
router.push(path);
}
};
onMounted(() => {
getBreadcrumb();
});
watch(
() => route.path,
() => getBreadcrumb()
);
watch(
() => route.query,
() => getBreadcrumb()
);
const handleLink = (item: RouteLocationMatched): any => {
const { redirect, path } = item;
if (redirect) {
router.push(redirect.toString());
return;
() => {
getBreadcrumb();
}
router.push(path);
};
);
</script>
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<el-breadcrumb class="!leading-[50px] select-none" separator="/">
<transition-group appear name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
<span
v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
class="no-redirect"
>{{ transformI18n(item.meta.title) }}</span
>
<a v-else @click.prevent="handleLink(item)">
<el-breadcrumb-item v-for="item in levelList" :key="item.path">
<a @click.prevent="handleLink(item)">
{{ transformI18n(item.meta.title) }}
</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>

View File

@ -1,63 +0,0 @@
<script setup lang="ts">
import { ref } from "vue";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
export interface Props {
isActive: boolean;
}
const props = withDefaults(defineProps<Props>(), {
isActive: false
});
const fillColor = ref<string>("");
const emit = defineEmits<{
(e: "toggleClick"): void;
}>();
const toggleClick = () => {
emit("toggleClick");
};
</script>
<template>
<div
:class="classes.container"
:title="props.isActive ? '点击折叠' : '点击展开'"
@click="toggleClick"
@mouseenter="fillColor = useEpThemeStoreHook().epThemeColor"
@mouseleave="fillColor = ''"
>
<svg
:fill="fillColor"
:class="['hamburger', props.isActive ? 'is-active' : '']"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
>
<path
d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
/>
</svg>
</div>
</template>
<style module="classes" scoped>
.container {
padding: 0 15px;
}
</style>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.is-active {
transform: rotate(180deg);
}
</style>

View File

@ -1,70 +1,39 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { useNav } from "../../hooks/nav";
import { ref, watch } from "vue";
import Search from "../search/index.vue";
import Notice from "../notice/index.vue";
import { templateRef } from "@vueuse/core";
import SidebarItem from "./sidebarItem.vue";
import avatars from "/@/assets/avatars.jpg";
import { useNav } from "/@/layout/hooks/useNav";
import screenfull from "../screenfull/index.vue";
import { useRoute, useRouter } from "vue-router";
import { deviceDetection } from "/@/utils/deviceDetection";
import { watch, nextTick, onMounted, getCurrentInstance } from "vue";
import { deviceDetection } from "@pureadmin/utils";
import { useTranslationLang } from "../../hooks/useTranslationLang";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import globalization from "/@/assets/svg/globalization.svg?component";
const route = useRoute();
const { locale, t } = useI18n();
const routers = useRouter().options.routes;
const menuRef = templateRef<ElRef | null>("menu", null);
const instance =
getCurrentInstance().appContext.config.globalProperties.$storage;
const title =
getCurrentInstance().appContext.config.globalProperties.$config?.Title;
const menuRef = ref();
const { t, route, locale, translationCh, translationEn } =
useTranslationLang(menuRef);
const {
title,
routers,
logout,
backHome,
onPanel,
changeTitle,
handleResize,
menuSelect,
username,
avatarsStyle,
getDropdownItemStyle
getDropdownItemStyle,
getDropdownItemClass
} = useNav();
onMounted(() => {
nextTick(() => {
handleResize(menuRef.value);
});
});
watch(
() => locale.value,
() => {
changeTitle(route.meta);
}
);
watch(
() => route.path,
() => {
menuSelect(route.path, routers);
}
);
function translationCh() {
instance.locale = { locale: "zh" };
locale.value = "zh";
handleResize(menuRef.value);
}
function translationEn() {
instance.locale = { locale: "en" };
locale.value = "en";
handleResize(menuRef.value);
}
</script>
<template>
@ -74,11 +43,11 @@ function translationEn() {
<h4>{{ title }}</h4>
</div>
<el-menu
ref="menu"
class="horizontal-header-menu"
mode="horizontal"
:default-active="route.path"
router
ref="menuRef"
mode="horizontal"
class="horizontal-header-menu"
:default-active="route.path"
@select="indexPath => menuSelect(indexPath, routers)"
>
<sidebar-item
@ -97,33 +66,39 @@ function translationEn() {
<screenfull id="header-screenfull" v-show="!deviceDetection()" />
<!-- 国际化 -->
<el-dropdown id="header-translation" trigger="click">
<globalization />
<globalization
class="navbar-bg-hover w-40px h-48px p-11px cursor-pointer outline-none"
/>
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'zh')"
:class="['!dark:color-white', getDropdownItemClass(locale, 'zh')]"
@click="translationCh"
>
<span class="check-zh" v-show="locale === 'zh'">
<IconifyIconOffline icon="check" /> </span
>简体中文
<IconifyIconOffline icon="check" />
</span>
简体中文
</el-dropdown-item>
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')"
:class="['!dark:color-white', getDropdownItemClass(locale, 'en')]"
@click="translationEn"
>
<span class="check-en" v-show="locale === 'en'">
<IconifyIconOffline icon="check" /> </span
>English</el-dropdown-item
>
<IconifyIconOffline icon="check" />
</span>
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 退出登 -->
<!-- 退出登 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link">
<span class="el-dropdown-link navbar-bg-hover">
<img v-if="avatars" :src="avatars" :style="avatarsStyle" />
<p v-if="username">{{ username }}</p>
<p v-if="username" class="dark:color-white">{{ username }}</p>
</span>
<template #dropdown>
<el-dropdown-menu class="logout">
@ -132,13 +107,13 @@ function translationEn() {
icon="logout-circle-r-line"
style="margin: 5px"
/>
{{ t("buttons.hsLoginOut") }}</el-dropdown-item
>
{{ t("buttons.hsLoginOut") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span
class="el-icon-setting"
class="el-icon-setting navbar-bg-hover"
:title="t('buttons.hssystemSet')"
@click="onPanel"
>

View File

@ -0,0 +1,47 @@
<script setup lang="ts">
import { useDark } from "@pureadmin/utils";
interface Props {
isActive: boolean;
}
const props = withDefaults(defineProps<Props>(), {
isActive: false
});
const { isDark } = useDark();
const emit = defineEmits<{
(e: "toggleClick"): void;
}>();
const toggleClick = () => {
emit("toggleClick");
};
</script>
<template>
<div class="container">
<el-tooltip
placement="right"
:effect="isDark ? 'dark' : 'light'"
:content="props.isActive ? '点击折叠' : '点击展开'"
>
<IconifyIconOffline
:icon="props.isActive ? 'menu-fold' : 'menu-unfold'"
class="cursor-pointer inline-block align-middle color-primary hover:color-primary !dark:hover:color-white w-16px h-16px ml-4 mb-1"
@click="toggleClick"
/>
</el-tooltip>
</div>
</template>
<style lang="scss" scoped>
.container {
position: absolute;
bottom: 0;
width: 100%;
height: 40px;
line-height: 40px;
box-shadow: 0 0 6px -2px var(--el-color-primary);
}
</style>

View File

@ -1,11 +1,11 @@
<script setup lang="ts">
import { getCurrentInstance } from "vue";
import { useNav } from "/@/layout/hooks/useNav";
const props = defineProps({
collapse: Boolean
});
const title =
getCurrentInstance().appContext.config.globalProperties.$config?.Title;
const { title } = useNav();
</script>
<template>

View File

@ -1,47 +1,39 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import Search from "../search/index.vue";
import Notice from "../notice/index.vue";
import { useNav } from "../../hooks/nav";
import { templateRef } from "@vueuse/core";
import avatars from "/@/assets/avatars.jpg";
import { useNav } from "/@/layout/hooks/useNav";
import { transformI18n } from "/@/plugins/i18n";
import screenfull from "../screenfull/index.vue";
import { useRoute, useRouter } from "vue-router";
import { deviceDetection } from "/@/utils/deviceDetection";
import { deviceDetection } from "@pureadmin/utils";
import { ref, toRaw, watch, onMounted } from "vue";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import { getParentPaths, findRouteByPath } from "/@/router/utils";
import { useTranslationLang } from "../../hooks/useTranslationLang";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import globalization from "/@/assets/svg/globalization.svg?component";
import { ref, watch, nextTick, onMounted, getCurrentInstance } from "vue";
const route = useRoute();
const { locale, t } = useI18n();
const routers = useRouter().options.routes;
const menuRef = templateRef<ElRef | null>("menu", null);
const instance =
getCurrentInstance().appContext.config.globalProperties.$storage;
const menuRef = ref();
let defaultActive = ref(null);
const { t, route, locale, translationCh, translationEn } =
useTranslationLang(menuRef);
const {
device,
routers,
logout,
onPanel,
changeTitle,
toggleSideBar,
handleResize,
menuSelect,
resolvePath,
pureApp,
username,
avatarsStyle,
getDropdownItemStyle
getDropdownItemStyle,
getDropdownItemClass
} = useNav();
let defaultActive = ref(null);
function getDefaultActive(routePath) {
const wholeMenus = usePermissionStoreHook().wholeMenus;
//
/** 当前路由的父级路径 */
const parentRoutes = getParentPaths(routePath, wholeMenus)[0];
defaultActive.value = findRouteByPath(
parentRoutes,
@ -51,67 +43,24 @@ function getDefaultActive(routePath) {
onMounted(() => {
getDefaultActive(route.path);
nextTick(() => {
handleResize(menuRef.value);
});
});
watch(
() => locale.value,
() => {
changeTitle(route.meta);
}
);
watch(
() => route.path,
() => {
getDefaultActive(route.path);
}
);
function translationCh() {
instance.locale = { locale: "zh" };
locale.value = "zh";
handleResize(menuRef.value);
}
function translationEn() {
instance.locale = { locale: "en" };
locale.value = "en";
handleResize(menuRef.value);
}
</script>
<template>
<div class="horizontal-header">
<div
:class="classes.container"
:title="pureApp.sidebar.opened ? '点击折叠' : '点击展开'"
@click="toggleSideBar"
>
<svg
:fill="useEpThemeStoreHook().fill"
:class="[
'hamburger',
pureApp.sidebar.opened ? 'is-active-hamburger' : ''
]"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
>
<path
d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
/>
</svg>
</div>
<div v-if="device !== 'mobile'" class="horizontal-header">
<el-menu
ref="menu"
class="horizontal-header-menu"
mode="horizontal"
:default-active="defaultActive"
router
ref="menuRef"
mode="horizontal"
class="horizontal-header-menu"
:default-active="defaultActive"
@select="indexPath => menuSelect(indexPath, routers)"
>
<el-menu-item
@ -120,10 +69,15 @@ function translationEn() {
:index="resolvePath(route) || route.redirect"
>
<template #title>
<div v-show="route.meta.icon" :class="['el-icon', route.meta.icon]">
<component :is="useRenderIcon(route.meta && route.meta.icon)" />
<div
v-if="toRaw(route.meta.icon)"
:class="['sub-menu-icon', route.meta.icon]"
>
<component
:is="useRenderIcon(route.meta && toRaw(route.meta.icon))"
/>
</div>
<span>{{ transformI18n(route.meta.title) }}</span>
<span class="select-none">{{ transformI18n(route.meta.title) }}</span>
<FontIcon
v-if="route.meta.extraIcon"
width="30px"
@ -144,31 +98,39 @@ function translationEn() {
<screenfull id="header-screenfull" v-show="!deviceDetection()" />
<!-- 国际化 -->
<el-dropdown id="header-translation" trigger="click">
<globalization />
<globalization
class="navbar-bg-hover w-40px h-48px p-11px cursor-pointer outline-none"
/>
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'zh')"
:class="['!dark:color-white', getDropdownItemClass(locale, 'zh')]"
@click="translationCh"
><span class="check-zh" v-show="locale === 'zh'"
><IconifyIconOffline icon="check" /></span
>简体中文</el-dropdown-item
>
<span class="check-zh" v-show="locale === 'zh'">
<IconifyIconOffline icon="check" />
</span>
简体中文
</el-dropdown-item>
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')"
:class="['!dark:color-white', getDropdownItemClass(locale, 'en')]"
@click="translationEn"
><span class="check-en" v-show="locale === 'en'"
><IconifyIconOffline icon="check" /></span
>English</el-dropdown-item
>
<span class="check-en" v-show="locale === 'en'">
<IconifyIconOffline icon="check" />
</span>
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 退出登陆 -->
<!-- 退出登 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link">
<span class="el-dropdown-link navbar-bg-hover">
<img v-if="avatars" :src="avatars" :style="avatarsStyle" />
<p v-if="username">{{ username }}</p>
<p v-if="username" class="dark:color-white">{{ username }}</p>
</span>
<template #dropdown>
<el-dropdown-menu class="logout">
@ -177,13 +139,13 @@ function translationEn() {
icon="logout-circle-r-line"
style="margin: 5px"
/>
{{ t("buttons.hsLoginOut") }}</el-dropdown-item
>
{{ t("buttons.hsLoginOut") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span
class="el-icon-setting"
class="el-icon-setting navbar-bg-hover"
:title="t('buttons.hssystemSet')"
@click="onPanel"
>
@ -193,26 +155,7 @@ function translationEn() {
</div>
</template>
<style module="classes" scoped>
.container {
padding: 0 15px;
}
</style>
<style lang="scss" scoped>
.hamburger {
width: 20px;
height: 20px;
&:hover {
cursor: pointer;
}
}
.is-active-hamburger {
transform: rotate(180deg);
}
.translation {
::v-deep(.el-dropdown-menu__item) {
padding: 5px 40px;

View File

@ -1,14 +1,12 @@
<script setup lang="ts">
import path from "path";
import { useNav } from "../../hooks/nav";
import { childrenType } from "../../types";
import { useNav } from "/@/layout/hooks/useNav";
import { transformI18n } from "/@/plugins/i18n";
import { useAppStoreHook } from "/@/store/modules/app";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
import { ref, PropType, nextTick, computed, CSSProperties } from "vue";
import { ref, toRaw, PropType, nextTick, computed, CSSProperties } from "vue";
const { pureApp } = useNav();
const menuMode = ["vertical", "mix"].includes(pureApp.layout);
const { layout, isCollapse } = useNav();
const props = defineProps({
item: {
@ -25,7 +23,7 @@ const props = defineProps({
});
const getExtraIconStyle = computed((): CSSProperties => {
if (useAppStoreHook().getSidebarStatus) {
if (!isCollapse.value) {
return {
position: "absolute",
right: "10px"
@ -46,7 +44,7 @@ const getNoDropdownStyle = computed((): CSSProperties => {
const getDivStyle = computed((): CSSProperties => {
return {
width: pureApp.sidebar.opened ? "" : "100%",
width: !isCollapse.value ? "" : "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
@ -54,9 +52,8 @@ const getDivStyle = computed((): CSSProperties => {
};
});
const getMenuTextStyle = computed((): CSSProperties => {
const getMenuTextStyle = computed(() => {
return {
width: pureApp.sidebar.opened ? "125px" : "",
overflow: "hidden",
textOverflow: "ellipsis",
outline: "none"
@ -65,14 +62,14 @@ const getMenuTextStyle = computed((): CSSProperties => {
const getSubTextStyle = computed((): CSSProperties => {
return {
width: pureApp.sidebar.opened ? "125px" : "",
width: !isCollapse.value ? "210px" : "",
display: "inline-block",
overflow: "hidden",
textOverflow: "ellipsis"
};
});
const getSpanStyle = computed((): CSSProperties => {
const getSpanStyle = computed(() => {
return {
overflow: "hidden",
textOverflow: "ellipsis"
@ -148,21 +145,31 @@ function resolvePath(routePath) {
:class="{ 'submenu-title-noDropdown': !isNest }"
:style="getNoDropdownStyle"
>
<div class="sub-menu-icon" v-show="props.item.meta.icon">
<div class="sub-menu-icon" v-if="toRaw(props.item.meta.icon)">
<component
:is="
useRenderIcon(
onlyOneChild.meta.icon ||
(props.item.meta && props.item.meta.icon)
toRaw(onlyOneChild.meta.icon) ||
(props.item.meta && toRaw(props.item.meta.icon))
)
"
/>
</div>
<div
v-if="
!pureApp.sidebar.opened &&
pureApp.layout === 'mix' &&
props.item?.pathList?.length === 2
isCollapse &&
layout === 'vertical' &&
props.item?.pathList?.length === 1
"
:style="getDivStyle"
>
<span :style="getMenuTextStyle">
{{ transformI18n(onlyOneChild.meta.title) }}
</span>
</div>
<div
v-if="
isCollapse && layout === 'mix' && props.item?.pathList?.length === 2
"
:style="getDivStyle"
>
@ -172,9 +179,9 @@ function resolvePath(routePath) {
</div>
<template #title>
<div :style="getDivStyle">
<span v-if="!menuMode">{{
transformI18n(onlyOneChild.meta.title)
}}</span>
<span v-if="layout === 'horizontal'">
{{ transformI18n(onlyOneChild.meta.title) }}
</span>
<el-tooltip
v-else
placement="top"
@ -205,24 +212,21 @@ function resolvePath(routePath) {
</el-menu-item>
</template>
<el-sub-menu
v-else
ref="subMenu"
:index="resolvePath(props.item.path)"
popper-append-to-body
>
<el-sub-menu v-else ref="subMenu" :index="resolvePath(props.item.path)">
<template #title>
<div v-show="props.item.meta.icon" class="sub-menu-icon">
<div v-if="toRaw(props.item.meta.icon)" class="sub-menu-icon">
<component
:is="useRenderIcon(props.item.meta && props.item.meta.icon)"
:is="useRenderIcon(props.item.meta && toRaw(props.item.meta.icon))"
/>
</div>
<span v-if="!menuMode">{{ transformI18n(props.item.meta.title) }}</span>
<span v-if="layout === 'horizontal'">
{{ transformI18n(props.item.meta.title) }}
</span>
<el-tooltip
v-else
placement="top"
:offset="-10"
:disabled="!pureApp.sidebar.opened || !props.item.showTooltip"
:disabled="!isCollapse || !props.item.showTooltip"
>
<template #content>
{{ transformI18n(props.item.meta.title) }}

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
interface Props {
isActive: boolean;
}
const props = withDefaults(defineProps<Props>(), {
isActive: false
});
const emit = defineEmits<{
(e: "toggleClick"): void;
}>();
const toggleClick = () => {
emit("toggleClick");
};
</script>
<template>
<div
class="px-3 mr-1 navbar-bg-hover"
:title="props.isActive ? '点击折叠' : '点击展开'"
@click="toggleClick"
>
<IconifyIconOffline
:icon="props.isActive ? 'menu-fold' : 'menu-unfold'"
class="inline-block align-middle hover:color-primary !dark:hover:color-white"
/>
</div>
</template>

View File

@ -1,26 +1,28 @@
<script setup lang="ts">
import Logo from "./logo.vue";
import { useRoute } from "vue-router";
import { emitter } from "/@/utils/mitt";
import { useNav } from "../../hooks/nav";
import SidebarItem from "./sidebarItem.vue";
import { storageLocal } from "/@/utils/storage";
import { useRoute, useRouter } from "vue-router";
import leftCollapse from "./leftCollapse.vue";
import type { StorageConfigs } from "/#/index";
import { useNav } from "/@/layout/hooks/useNav";
import { storageLocal } from "@pureadmin/utils";
import { ref, computed, watch, onBeforeMount } from "vue";
import { findRouteByPath, getParentPaths } from "/@/router/utils";
import { usePermissionStoreHook } from "/@/store/modules/permission";
const route = useRoute();
const routers = useRouter().options.routes;
const showLogo = ref(
storageLocal.getItem("responsive-configure")?.showLogo ?? true
storageLocal.getItem<StorageConfigs>("responsive-configure")?.showLogo ?? true
);
const { pureApp, isCollapse, menuSelect } = useNav();
const { routers, device, pureApp, isCollapse, menuSelect, toggleSideBar } =
useNav();
let subMenuData = ref([]);
const menuData = computed(() => {
return pureApp.layout === "mix"
return pureApp.layout === "mix" && device.value !== "mobile"
? subMenuData.value
: usePermissionStoreHook().wholeMenus;
});
@ -59,25 +61,33 @@ watch(
<template>
<div :class="['sidebar-container', showLogo ? 'has-logo' : '']">
<Logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-scrollbar
wrap-class="scrollbar-wrapper"
:class="[device === 'mobile' ? 'mobile' : 'pc']"
>
<el-menu
:default-active="route.path"
:collapse="isCollapse"
unique-opened
router
:collapse-transition="false"
unique-opened
mode="vertical"
class="outer-most"
class="outer-most select-none"
:collapse="isCollapse"
:default-active="route.path"
:collapse-transition="false"
@select="indexPath => menuSelect(indexPath, routers)"
>
<sidebar-item
v-for="routes in menuData"
:key="routes.path"
:item="routes"
class="outer-most"
:base-path="routes.path"
class="outer-most select-none"
/>
</el-menu>
</el-scrollbar>
<leftCollapse
v-if="device !== 'mobile'"
:is-active="pureApp.sidebar.opened"
@toggleClick="toggleSideBar"
/>
</div>
</template>

View File

@ -43,7 +43,7 @@
font-size: 14px;
display: flex;
align-items: center;
color: var(--el-text-color-regular);
color: var(--el-text-color-primary);
background: #fff;
position: relative;
box-shadow: 0 0 1px #888;
@ -92,7 +92,7 @@
a {
text-decoration: none;
color: #666;
color: var(--el-text-color-primary);
padding: 0 4px;
}
@ -144,7 +144,7 @@
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
color: #000000d9;
color: var(--el-text-color-primary);
font-weight: normal;
font-size: 13px;
white-space: nowrap;
@ -160,7 +160,7 @@
align-items: center;
&:hover {
background: var(--el-color-primary-light-9);
// background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
}
@ -173,8 +173,6 @@
}
.el-dropdown-menu {
padding: 0;
li {
width: 100%;
margin: 0;
@ -207,7 +205,7 @@
}
.scroll-item.is-active {
background-color: var(--el-color-primary-light-9);
// background-color: var(--el-color-primary-light-9);
position: relative;
color: #fff;
@ -220,7 +218,7 @@
}
a {
color: var(--el-color-primary);
color: var(--el-color-primary) !important;
}
}
@ -228,7 +226,7 @@
.arrow-right {
width: 40px;
height: 38px;
color: #00000073;
color: var(--el-text-color-primary);
position: relative;
svg {

View File

@ -1,115 +1,53 @@
<script setup lang="ts">
import {
ref,
watch,
unref,
toRaw,
reactive,
nextTick,
computed,
ComputedRef,
CSSProperties,
onBeforeMount,
getCurrentInstance
} from "vue";
import close from "/@/assets/svg/close.svg?component";
import refresh from "/@/assets/svg/refresh.svg?component";
import closeAll from "/@/assets/svg/close_all.svg?component";
import closeLeft from "/@/assets/svg/close_left.svg?component";
import closeOther from "/@/assets/svg/close_other.svg?component";
import closeRight from "/@/assets/svg/close_right.svg?component";
import { useI18n } from "vue-i18n";
import { emitter } from "/@/utils/mitt";
import { storageLocal } from "/@/utils/storage";
import { useRoute, useRouter } from "vue-router";
import { RouteConfigs } from "../../types";
import { useTags } from "../../hooks/useTag";
import { routerArrays } from "/@/layout/types";
import { isEqual, isEmpty } from "lodash-unified";
import { transformI18n, $t } from "/@/plugins/i18n";
import { RouteConfigs, tagsViewsType } from "../../types";
import { toggleClass, removeClass } from "@pureadmin/utils";
import { useResizeObserver, useDebounceFn } from "@vueuse/core";
import { useSettingStoreHook } from "/@/store/modules/settings";
import { handleAliveRoute, delAliveRoutes } from "/@/router/utils";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import { toggleClass, removeClass, hasClass } from "/@/utils/operate";
import { templateRef, useResizeObserver, useDebounceFn } from "@vueuse/core";
import { ref, watch, unref, toRaw, nextTick, onBeforeMount } from "vue";
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const translateX = ref<number>(0);
const activeIndex = ref<number>(-1);
let refreshButton = "refresh-button";
const instance = getCurrentInstance();
const pureSetting = useSettingStoreHook();
const tabDom = templateRef<HTMLElement | null>("tabDom", null);
const containerDom = templateRef<HTMLElement | null>("containerDom", null);
const scrollbarDom = templateRef<HTMLElement | null>("scrollbarDom", null);
const showTags =
ref(storageLocal.getItem("responsive-configure").hideTabs) ?? "false";
let multiTags: ComputedRef<Array<RouteConfigs>> = computed(() => {
return useMultiTagsStoreHook()?.multiTags;
});
const {
route,
router,
visible,
showTags,
instance,
multiTags,
tagsViews,
buttonTop,
buttonLeft,
showModel,
translateX,
activeIndex,
getTabStyle,
iconIsActive,
linkIsActive,
currentSelect,
scheduleIsActive,
getContextMenuStyle,
closeMenu,
onMounted,
onMouseenter,
onMouseleave,
transformI18n
} = useTags();
const linkIsActive = computed(() => {
return item => {
if (Object.keys(route.query).length === 0) {
if (route.path === item.path) {
return "is-active";
} else {
return "";
}
} else {
if (isEqual(route?.query, item?.query)) {
return "is-active";
} else {
return "";
}
}
};
});
const scheduleIsActive = computed(() => {
return item => {
if (Object.keys(route.query).length === 0) {
if (route.path === item.path) {
return "schedule-active";
} else {
return "";
}
} else {
if (isEqual(route?.query, item?.query)) {
return "schedule-active";
} else {
return "";
}
}
};
});
const iconIsActive = computed(() => {
return (item, index) => {
if (index === 0) return;
if (Object.keys(route.query).length === 0) {
if (route.path === item.path) {
return true;
} else {
return false;
}
} else {
if (isEqual(route?.query, item?.query)) {
return true;
} else {
return false;
}
}
};
});
const tabDom = ref();
const containerDom = ref();
const scrollbarDom = ref();
const dynamicTagView = () => {
const index = multiTags.value.findIndex(item => {
if (item?.query) {
return isEqual(route?.query, item?.query);
if (item.query) {
return isEqual(route.query, item.query);
} else if (item.params) {
return isEqual(route.params, item.params);
} else {
return item.path === route.path;
}
@ -117,23 +55,9 @@ const dynamicTagView = () => {
moveToView(index);
};
watch([route], () => {
activeIndex.value = -1;
dynamicTagView();
});
useResizeObserver(
scrollbarDom,
useDebounceFn(() => {
dynamicTagView();
}, 200)
);
const tabNavPadding = 10;
const moveToView = (index: number): void => {
if (!instance.refs["dynamic" + index]) {
return;
}
const tabNavPadding = 10;
if (!instance.refs["dynamic" + index]) return;
const tabItemEl = instance.refs["dynamic" + index][0];
const tabItemElOffsetLeft = (tabItemEl as HTMLElement)?.offsetLeft;
const tabItemOffsetWidth = (tabItemEl as HTMLElement)?.offsetWidth;
@ -143,7 +67,6 @@ const moveToView = (index: number): void => {
: 0;
//
const tabDomWidth = tabDom.value ? tabDom.value?.offsetWidth : 0;
if (tabDomWidth < scrollbarDomWidth || tabItemElOffsetLeft === 0) {
translateX.value = 0;
} else if (tabItemElOffsetLeft < -translateX.value) {
@ -192,68 +115,6 @@ const handleScroll = (offset: number): void => {
}
};
const tagsViews = reactive<Array<tagsViewsType>>([
{
icon: refresh,
text: $t("buttons.hsreload"),
divided: false,
disabled: false,
show: true
},
{
icon: close,
text: $t("buttons.hscloseCurrentTab"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeLeft,
text: $t("buttons.hscloseLeftTabs"),
divided: true,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeRight,
text: $t("buttons.hscloseRightTabs"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeOther,
text: $t("buttons.hscloseOtherTabs"),
divided: true,
disabled: multiTags.value.length > 2 ? false : true,
show: true
},
{
icon: closeAll,
text: $t("buttons.hscloseAllTabs"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
}
]);
//
const showModel = ref(
storageLocal.getItem("responsive-configure")?.showModel || "smart"
);
if (!showModel.value) {
const configure = storageLocal.getItem("responsive-configure");
configure.showModel = "card";
storageLocal.setItem("responsive-configure", configure);
}
let visible = ref(false);
let buttonLeft = ref(0);
let buttonTop = ref(0);
//
let currentSelect = ref({});
function dynamicRouteTag(value: string, parentPath: string): void {
const hasValue = multiTags.value.some(item => {
return item.path === value;
@ -278,11 +139,12 @@ function dynamicRouteTag(value: string, parentPath: string): void {
});
}
}
concatPath(router.options.routes, value, parentPath);
concatPath(router.options.routes as any, value, parentPath);
}
//
/** 刷新路由 */
function onFresh() {
const refreshButton = "refresh-button";
toggleClass(true, refreshButton, document.querySelector(".rotate"));
const { fullPath, query } = unref(route);
router.replace({
@ -302,6 +164,10 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
if (item.path === obj.path) {
return item.query === obj.query;
}
} else if (item.params) {
if (item.path === obj.path) {
return item.params === obj.params;
}
} else {
return item.path === obj.path;
}
@ -313,23 +179,12 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
other?: boolean
): void => {
if (other) {
useMultiTagsStoreHook().handleTags("equal", [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "menus.hshome",
icon: "home-filled"
}
},
obj
]);
useMultiTagsStoreHook().handleTags("equal", [routerArrays[0], obj]);
} else {
// @ts-ignore
delAliveRouteList = useMultiTagsStoreHook().handleTags("splice", "", {
startIndex,
length
});
}) as any;
}
};
@ -351,24 +206,25 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
: handleAliveRoute(route.matched, "delete");
// tagtag
if (tag === "left") return;
nextTick(() => {
router.push({
path: newRoute[0].path,
query: newRoute[0].query
});
});
if (newRoute[0]?.query) {
router.push({ name: newRoute[0].name, query: newRoute[0].query });
} else if (newRoute[0]?.params) {
router.push({ name: newRoute[0].name, params: newRoute[0].params });
} else {
router.push({ path: newRoute[0].path });
}
} else {
//
tag ? delAliveRoutes(delAliveRouteList) : delAliveRoutes([obj]);
if (!multiTags.value.length) return;
let isHasActiveTag = multiTags.value.some(item => {
return item.path === route.path;
});
!isHasActiveTag &&
router.push({
path: newRoute[0].path,
query: newRoute[0].query
});
if (multiTags.value.some(item => item.path === route.path)) return;
if (newRoute[0]?.query) {
router.push({ name: newRoute[0].name, query: newRoute[0].query });
} else if (newRoute[0]?.params) {
router.push({ name: newRoute[0].name, params: newRoute[0].params });
} else {
router.push({ path: newRoute[0].path });
}
}
}
@ -385,7 +241,8 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
path: selectRoute.path,
meta: selectRoute.meta,
name: selectRoute.name,
query: selectRoute.query
query: selectRoute?.query,
params: selectRoute?.params
};
} else {
selectTagRoute = { path: route.path, meta: route.meta };
@ -394,7 +251,7 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
//
switch (key) {
case 0:
//
//
onFresh();
break;
case 1:
@ -428,21 +285,16 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
});
}
function handleCommand(command: object) {
// @ts-expect-error
function handleCommand(command: any) {
const { key, item } = command;
onClickDrop(key, item);
}
//
/** 触发右键中菜单的点击事件 */
function selectTag(key, item) {
onClickDrop(key, item, currentSelect.value);
}
function closeMenu() {
visible.value = false;
}
function showMenus(value: boolean) {
Array.of(1, 2, 3, 4, 5).forEach(v => {
tagsViews[v].show = value;
@ -455,7 +307,7 @@ function disabledMenus(value: boolean) {
});
}
//
/** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */
function showMenuModel(
currentPath: string,
query: object = {},
@ -515,7 +367,7 @@ function openMenu(tag, e) {
//
showMenus(false);
tagsViews[0].show = true;
} else if (route.path !== tag.path) {
} else if (route.path !== tag.path && route.name !== tag.name) {
//
tagsViews[0].show = false;
showMenuModel(tag.path, tag.query);
@ -543,63 +395,36 @@ function openMenu(tag, e) {
} else {
buttonLeft.value = left;
}
pureSetting.hiddenSideBar
useSettingStoreHook().hiddenSideBar
? (buttonTop.value = e.clientY)
: (buttonTop.value = e.clientY - 40);
setTimeout(() => {
nextTick(() => {
visible.value = true;
}, 10);
}
// tags
function tagOnClick(item) {
router.push({
path: item?.path,
query: item?.query
});
showMenuModel(item?.path, item?.query);
}
//
function onMouseenter(index) {
if (index) activeIndex.value = index;
if (unref(showModel) === "smart") {
if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
return;
toggleClass(true, "schedule-in", instance.refs["schedule" + index][0]);
toggleClass(false, "schedule-out", instance.refs["schedule" + index][0]);
} else {
if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
toggleClass(true, "card-in", instance.refs["dynamic" + index][0]);
toggleClass(false, "card-out", instance.refs["dynamic" + index][0]);
}
}
//
function onMouseleave(index) {
activeIndex.value = -1;
if (unref(showModel) === "smart") {
if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
return;
toggleClass(false, "schedule-in", instance.refs["schedule" + index][0]);
toggleClass(true, "schedule-out", instance.refs["schedule" + index][0]);
} else {
if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
toggleClass(false, "card-in", instance.refs["dynamic" + index][0]);
toggleClass(true, "card-out", instance.refs["dynamic" + index][0]);
}
}
watch(
() => visible.value,
val => {
if (val) {
document.body.addEventListener("click", closeMenu);
/** 触发tags标签切换 */
function tagOnClick(item) {
const { name, path } = item;
if (name) {
if (item.query) {
router.push({
name,
query: item.query
});
} else if (item.params) {
router.push({
name,
params: item.params
});
} else {
document.body.removeEventListener("click", closeMenu);
router.push({ name });
}
} else {
router.push({ path });
}
);
// showMenuModel(item?.path, item?.query);
}
onBeforeMount(() => {
if (!instance) return;
@ -608,9 +433,9 @@ onBeforeMount(() => {
showMenuModel(route.fullPath);
//
emitter.on("tagViewsChange", key => {
if (unref(showTags) === key) return;
showTags.value = key;
emitter.on("tagViewsChange", (key: any) => {
if (unref(showTags as any) === key) return;
(showTags as any).value = key;
});
//
@ -627,14 +452,18 @@ onBeforeMount(() => {
});
});
const getTabStyle = computed((): CSSProperties => {
return {
transform: `translateX(${translateX.value}px)`
};
watch([route], () => {
activeIndex.value = -1;
dynamicTagView();
});
const getContextMenuStyle = computed((): CSSProperties => {
return { left: buttonLeft.value + "px", top: buttonTop.value + "px" };
onMounted(() => {
useResizeObserver(
scrollbarDom,
useDebounceFn(() => {
dynamicTagView();
}, 200)
);
});
</script>
@ -661,8 +490,11 @@ const getContextMenuStyle = computed((): CSSProperties => {
@mouseleave.prevent="onMouseleave(index)"
@click="tagOnClick(item)"
>
<router-link :to="item.path"
>{{ transformI18n(item.meta.title) }}
<router-link
:to="item.path"
class="!dark:color-text_color_primary !dark:hover:color-primary"
>
{{ transformI18n(item.meta.title) }}
</router-link>
<span
v-if="
@ -703,7 +535,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
>
<li v-if="item.show" @click="selectTag(key, item)">
<component :is="toRaw(item.icon)" :key="key" />
{{ t(item.text) }}
{{ transformI18n(item.text) }}
</li>
</div>
</ul>
@ -712,7 +544,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
<ul class="right-button">
<li>
<span
:title="t('buttons.hsrefreshRoute')"
:title="transformI18n('buttons.hsrefreshRoute')"
class="el-icon-refresh-right rotate"
@click="onFresh"
>
@ -725,7 +557,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
placement="bottom-end"
@command="handleCommand"
>
<IconifyIconOffline icon="arrow-down" />
<IconifyIconOffline icon="arrow-down" class="dark:color-white" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
@ -740,7 +572,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
:key="key"
style="margin-right: 6px"
/>
{{ t(item.text) }}
{{ transformI18n(item.text) }}
</el-dropdown-item>
</el-dropdown-menu>
</template>

View File

@ -1,20 +1,14 @@
<template>
<div
class="frame"
v-loading="loading"
:element-loading-text="t('status.hsLoad')"
>
<iframe :src="frameSrc" class="frame-iframe" ref="frameRef" />
</div>
</template>
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { ref, unref, onMounted, nextTick } from "vue";
defineOptions({
name: "FrameView"
});
const { t } = useI18n();
const loading = ref(false);
const loading = ref(true);
const currentRoute = useRoute();
const frameSrc = ref<string>("");
const frameRef = ref<HTMLElement | null>(null);
@ -22,6 +16,7 @@ const frameRef = ref<HTMLElement | null>(null);
if (unref(currentRoute.meta)?.frameSrc) {
frameSrc.value = unref(currentRoute.meta)?.frameSrc as string;
}
unref(currentRoute.meta)?.frameLoading === false && hideLoading();
function hideLoading() {
loading.value = false;
@ -45,11 +40,20 @@ function init() {
}
onMounted(() => {
loading.value = true;
init();
});
</script>
<template>
<div
class="frame"
v-loading="loading"
:element-loading-text="t('status.hsLoad')"
>
<iframe :src="frameSrc" class="frame-iframe" ref="frameRef" />
</div>
</template>
<style lang="scss" scoped>
.frame {
height: calc(100vh - 88px);

View File

@ -1,6 +1,6 @@
import { ref } from "vue";
export default function useBoolean(initValue = false) {
export function useBoolean(initValue = false) {
const bool = ref(initValue);
function setBool(value: boolean) {

View File

@ -0,0 +1,116 @@
import { ref } from "vue";
import { getConfig } from "/@/config";
import { find } from "lodash-unified";
import { useLayout } from "./useLayout";
import { themeColorsType } from "../types";
import { TinyColor } from "@ctrl/tinycolor";
import { useGlobal } from "@pureadmin/utils";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import {
darken,
lighten,
toggleTheme
} from "@pureadmin/theme/dist/browser-utils";
export function useDataThemeChange() {
const { layoutTheme, layout } = useLayout();
const themeColors = ref<Array<themeColorsType>>([
/* 道奇蓝(默认) */
{ color: "#1b2a47", themeColor: "default" },
/* 亮白色 */
{ color: "#ffffff", themeColor: "light" },
/* 猩红色 */
{ color: "#f5222d", themeColor: "dusk" },
/* 橙红色 */
{ color: "#fa541c", themeColor: "volcano" },
/* 金色 */
{ color: "#fadb14", themeColor: "yellow" },
/* 绿宝石 */
{ color: "#13c2c2", themeColor: "mingQing" },
/* 酸橙绿 */
{ color: "#52c41a", themeColor: "auroraGreen" },
/* 深粉色 */
{ color: "#eb2f96", themeColor: "pink" },
/* 深紫罗兰色 */
{ color: "#722ed1", themeColor: "saucePurple" }
]);
const { $storage } = useGlobal<GlobalPropertiesApi>();
const dataTheme = ref<boolean>($storage?.layout?.darkMode);
const body = document.documentElement as HTMLElement;
/** 设置导航主题色 */
function setLayoutThemeColor(theme = "default") {
layoutTheme.value.theme = theme;
toggleTheme({
scopeName: `layout-theme-${theme}`
});
$storage.layout = {
layout: layout.value,
theme,
darkMode: dataTheme.value,
sidebarStatus: $storage.layout?.sidebarStatus,
epThemeColor: $storage.layout?.epThemeColor
};
if (theme === "default" || theme === "light") {
setEpThemeColor(getConfig().EpThemeColor);
} else {
const colors = find(themeColors.value, { themeColor: theme });
setEpThemeColor(colors.color);
}
}
/**
* @description hover和active颜色
* @see {@link https://element-plus.org/zh-CN/component/button.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E9%A2%9C%E8%89%B2}
*/
const shadeBgColor = (color: string): string => {
return new TinyColor(color).shade(10).toString();
};
/** 设置ep主题色 */
const setEpThemeColor = (color: string) => {
useEpThemeStoreHook().setEpThemeColor(color);
body.style.setProperty("--el-color-primary-active", shadeBgColor(color));
document.documentElement.style.setProperty("--el-color-primary", color);
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(
`--el-color-primary-light-${i}`,
lighten(color, i / 10)
);
}
for (let i = 1; i <= 2; i++) {
document.documentElement.style.setProperty(
`--el-color-primary-dark-${i}`,
darken(color, i / 10)
);
}
};
/** 日间、夜间主题切换 */
function dataThemeChange() {
/* 如果当前是light夜间主题默认切换到default主题 */
if (useEpThemeStoreHook().epTheme === "light" && dataTheme.value) {
setLayoutThemeColor("default");
} else {
setLayoutThemeColor(useEpThemeStoreHook().epTheme);
}
if (dataTheme.value) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
return {
body,
dataTheme,
layoutTheme,
themeColors,
dataThemeChange,
setEpThemeColor,
setLayoutThemeColor
};
}

View File

@ -0,0 +1,60 @@
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { routerArrays } from "../types";
import { useGlobal } from "@pureadmin/utils";
import { useMultiTagsStore } from "/@/store/modules/multiTags";
export function useLayout() {
const { $storage, $config } = useGlobal<GlobalPropertiesApi>();
const initStorage = () => {
/** 路由 */
if (
useMultiTagsStore().multiTagsCache &&
(!$storage.tags || $storage.tags.length === 0)
) {
$storage.tags = routerArrays;
}
/** 国际化 */
if (!$storage.locale) {
$storage.locale = { locale: $config?.Locale ?? "zh" };
useI18n().locale.value = $config?.Locale ?? "zh";
}
/** 导航 */
if (!$storage.layout) {
$storage.layout = {
layout: $config?.Layout ?? "vertical",
theme: $config?.Theme ?? "default",
darkMode: $config?.DarkMode ?? false,
sidebarStatus: $config?.SidebarStatus ?? true,
epThemeColor: $config?.EpThemeColor ?? "#409EFF"
};
}
/** 灰色模式、色弱模式、隐藏标签页 */
if (!$storage.configure) {
$storage.configure = {
grey: $config?.Grey ?? false,
weak: $config?.Weak ?? false,
hideTabs: $config?.HideTabs ?? false,
showLogo: $config?.ShowLogo ?? true,
showModel: $config?.ShowModel ?? "smart",
multiTagsCache: $config?.MultiTagsCache ?? false
};
}
};
/** 清空缓存后从serverConfig.json读取默认配置并赋值到storage中 */
const layout = computed(() => {
return $storage?.layout.layout;
});
const layoutTheme = computed(() => {
return $storage.layout;
});
return {
layout,
layoutTheme,
initStorage
};
}

View File

@ -1,22 +1,28 @@
import { computed } from "vue";
import { router } from "/@/router";
import { getConfig } from "/@/config";
import { useRouter } from "vue-router";
import { emitter } from "/@/utils/mitt";
import { routeMetaType } from "../types";
import { remainingPaths } from "/@/router";
import type { StorageConfigs } from "/#/index";
import { routerArrays } from "/@/layout/types";
import { transformI18n } from "/@/plugins/i18n";
import { storageSession } from "/@/utils/storage";
import { useAppStoreHook } from "/@/store/modules/app";
import { remainingPaths, resetRouter } from "/@/router";
import { storageSession, useGlobal } from "@pureadmin/utils";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
const errorInfo = "当前路由配置不正确,请检查配置";
export function useNav() {
const pureApp = useAppStoreHook();
// 用户名
const username: string = storageSession.getItem("info")?.username;
const routers = useRouter().options.routes;
/** 用户名 */
const username: string =
storageSession.getItem<StorageConfigs>("info")?.username;
// 设置国际化选中后的样式
/** 设置国际化选中后的样式 */
const getDropdownItemStyle = computed(() => {
return (locale, t) => {
return {
@ -26,6 +32,12 @@ export function useNav() {
};
});
const getDropdownItemClass = computed(() => {
return (locale, t) => {
return locale === t ? "" : "!dark:hover:color-primary";
};
});
const avatarsStyle = computed(() => {
return username ? { marginRight: "10px" } : "";
});
@ -34,17 +46,32 @@ export function useNav() {
return !pureApp.getSidebarStatus;
});
// 动态title
const device = computed(() => {
return pureApp.getDevice;
});
const { $storage, $config } = useGlobal<GlobalPropertiesApi>();
const layout = computed(() => {
return $storage?.layout?.layout;
});
const title = computed(() => {
return $config.Title;
});
/** 动态title */
function changeTitle(meta: routeMetaType) {
const Title = getConfig().Title;
if (Title) document.title = `${transformI18n(meta.title)} | ${Title}`;
else document.title = transformI18n(meta.title);
}
// 退出登录
/** 退出登录 */
function logout() {
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
storageSession.removeItem("info");
router.push("/login");
resetRouter();
}
function backHome() {
@ -60,7 +87,7 @@ export function useNav() {
}
function handleResize(menuRef) {
menuRef.handleResize();
menuRef?.handleResize();
}
function resolvePath(route) {
@ -81,7 +108,7 @@ export function useNav() {
if (parentPathIndex > 0) {
parentPath = indexPath.slice(0, parentPathIndex);
}
// 找到当前路由的信息
/** 找到当前路由的信息 */
function findCurrentRoute(indexPath: string, routes) {
if (!routes) return console.error(errorInfo);
return routes.map(item => {
@ -89,7 +116,7 @@ export function useNav() {
if (item.redirect) {
findCurrentRoute(item.redirect, item.children);
} else {
// 切换左侧菜单 通知标签页
/** 切换左侧菜单 通知标签页 */
emitter.emit("changLayoutRoute", {
indexPath,
parentPath
@ -103,13 +130,18 @@ export function useNav() {
findCurrentRoute(indexPath, routers);
}
// 判断路径是否参与菜单
/** 判断路径是否参与菜单 */
function isRemaining(path: string): boolean {
return remainingPaths.includes(path);
}
return {
title,
device,
layout,
logout,
routers,
$storage,
backHome,
onPanel,
changeTitle,
@ -121,6 +153,7 @@ export function useNav() {
pureApp,
username,
avatarsStyle,
getDropdownItemStyle
getDropdownItemStyle,
getDropdownItemClass
};
}

218
src/layout/hooks/useTag.ts Normal file
View File

@ -0,0 +1,218 @@
import {
ref,
unref,
watch,
computed,
reactive,
onMounted,
CSSProperties,
getCurrentInstance
} from "vue";
import { tagsViewsType } from "../types";
import { isEqual } from "lodash-unified";
import type { StorageConfigs } from "/#/index";
import { useEventListener } from "@vueuse/core";
import { useRoute, useRouter } from "vue-router";
import { transformI18n, $t } from "/@/plugins/i18n";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { storageLocal, toggleClass, hasClass } from "@pureadmin/utils";
import close from "/@/assets/svg/close.svg?component";
import refresh from "/@/assets/svg/refresh.svg?component";
import closeAll from "/@/assets/svg/close_all.svg?component";
import closeLeft from "/@/assets/svg/close_left.svg?component";
import closeOther from "/@/assets/svg/close_other.svg?component";
import closeRight from "/@/assets/svg/close_right.svg?component";
export function useTags() {
const route = useRoute();
const router = useRouter();
const instance = getCurrentInstance();
const buttonTop = ref(0);
const buttonLeft = ref(0);
const translateX = ref(0);
const visible = ref(false);
const activeIndex = ref(-1);
// 当前右键选中的路由信息
const currentSelect = ref({});
/** 显示模式,默认灵动模式 */
const showModel = ref(
storageLocal.getItem<StorageConfigs>("responsive-configure")?.showModel ||
"smart"
);
/** 是否隐藏标签页,默认显示 */
const showTags =
ref(
storageLocal.getItem<StorageConfigs>("responsive-configure").hideTabs
) ?? ref("false");
const multiTags: any = computed(() => {
return useMultiTagsStoreHook().multiTags;
});
const tagsViews = reactive<Array<tagsViewsType>>([
{
icon: refresh,
text: $t("buttons.hsreload"),
divided: false,
disabled: false,
show: true
},
{
icon: close,
text: $t("buttons.hscloseCurrentTab"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeLeft,
text: $t("buttons.hscloseLeftTabs"),
divided: true,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeRight,
text: $t("buttons.hscloseRightTabs"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeOther,
text: $t("buttons.hscloseOtherTabs"),
divided: true,
disabled: multiTags.value.length > 2 ? false : true,
show: true
},
{
icon: closeAll,
text: $t("buttons.hscloseAllTabs"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
}
]);
function conditionHandle(item, previous, next) {
if (
Object.keys(route.query).length === 0 &&
Object.keys(route.params).length === 0
) {
return route.path === item.path ? previous : next;
} else if (Object.keys(route.query).length > 0) {
return isEqual(route.query, item.query) ? previous : next;
} else {
return isEqual(route.params, item.params) ? previous : next;
}
}
const iconIsActive = computed(() => {
return (item, index) => {
if (index === 0) return;
return conditionHandle(item, true, false);
};
});
const linkIsActive = computed(() => {
return item => {
return conditionHandle(item, "is-active", "");
};
});
const scheduleIsActive = computed(() => {
return item => {
return conditionHandle(item, "schedule-active", "");
};
});
const getTabStyle = computed((): CSSProperties => {
return {
transform: `translateX(${translateX.value}px)`
};
});
const getContextMenuStyle = computed((): CSSProperties => {
return { left: buttonLeft.value + "px", top: buttonTop.value + "px" };
});
const closeMenu = () => {
visible.value = false;
};
/** 鼠标移入添加激活样式 */
function onMouseenter(index) {
if (index) activeIndex.value = index;
if (unref(showModel) === "smart") {
if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
return;
toggleClass(true, "schedule-in", instance.refs["schedule" + index][0]);
toggleClass(false, "schedule-out", instance.refs["schedule" + index][0]);
} else {
if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
toggleClass(true, "card-in", instance.refs["dynamic" + index][0]);
toggleClass(false, "card-out", instance.refs["dynamic" + index][0]);
}
}
/** 鼠标移出恢复默认样式 */
function onMouseleave(index) {
activeIndex.value = -1;
if (unref(showModel) === "smart") {
if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
return;
toggleClass(false, "schedule-in", instance.refs["schedule" + index][0]);
toggleClass(true, "schedule-out", instance.refs["schedule" + index][0]);
} else {
if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
toggleClass(false, "card-in", instance.refs["dynamic" + index][0]);
toggleClass(true, "card-out", instance.refs["dynamic" + index][0]);
}
}
onMounted(() => {
if (!showModel.value) {
const configure = storageLocal.getItem<StorageConfigs>(
"responsive-configure"
);
configure.showModel = "card";
storageLocal.setItem("responsive-configure", configure);
}
});
watch(
() => visible.value,
() => {
useEventListener(document, "click", closeMenu);
}
);
return {
route,
router,
visible,
showTags,
instance,
multiTags,
showModel,
tagsViews,
buttonTop,
buttonLeft,
translateX,
activeIndex,
getTabStyle,
iconIsActive,
linkIsActive,
currentSelect,
scheduleIsActive,
getContextMenuStyle,
$t,
closeMenu,
onMounted,
onMouseenter,
onMouseleave,
transformI18n
};
}

View File

@ -0,0 +1,37 @@
import { useNav } from "./useNav";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { watch, type Ref } from "vue";
export function useTranslationLang(ref?: Ref) {
const { $storage, changeTitle, handleResize } = useNav();
const { locale, t } = useI18n();
const route = useRoute();
function translationCh() {
$storage.locale = { locale: "zh" };
locale.value = "zh";
ref && handleResize(ref.value);
}
function translationEn() {
$storage.locale = { locale: "en" };
locale.value = "en";
ref && handleResize(ref.value);
}
watch(
() => locale.value,
() => {
changeTitle(route.meta);
}
);
return {
t,
route,
locale,
translationCh,
translationEn
};
}

View File

@ -1,20 +1,11 @@
<script setup lang="ts">
import {
h,
reactive,
computed,
onMounted,
defineComponent,
getCurrentInstance
} from "vue";
import { setType } from "./types";
import { useI18n } from "vue-i18n";
import { routerArrays } from "./types";
import { emitter } from "/@/utils/mitt";
import { useLayout } from "./hooks/useLayout";
import { useAppStoreHook } from "/@/store/modules/app";
import { deviceDetection } from "/@/utils/deviceDetection";
import { useMultiTagsStore } from "/@/store/modules/multiTags";
import { useSettingStoreHook } from "/@/store/modules/settings";
import { deviceDetection, useDark, useGlobal } from "@pureadmin/utils";
import { h, reactive, computed, onMounted, defineComponent } from "vue";
import backTop from "/@/assets/svg/back_top.svg?component";
import fullScreen from "/@/assets/svg/full_screen.svg?component";
@ -27,51 +18,11 @@ import setting from "./components/setting/index.vue";
import Vertical from "./components/sidebar/vertical.vue";
import Horizontal from "./components/sidebar/horizontal.vue";
const { isDark } = useDark();
const { layout } = useLayout();
const isMobile = deviceDetection();
const pureSetting = useSettingStoreHook();
const instance = getCurrentInstance().appContext.app.config.globalProperties;
// serverConfig.jsonstorage
const layout = computed(() => {
//
if (
useMultiTagsStore().multiTagsCache &&
(!instance.$storage.tags || instance.$storage.tags.length === 0)
) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
instance.$storage.tags = routerArrays;
}
//
if (!instance.$storage.locale) {
// eslint-disable-next-line
instance.$storage.locale = { locale: instance.$config?.Locale ?? "zh" };
useI18n().locale.value = instance.$config?.Locale ?? "zh";
}
//
if (!instance.$storage.layout) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
instance.$storage.layout = {
layout: instance.$config?.Layout ?? "vertical",
theme: instance.$config?.Theme ?? "default",
darkMode: instance.$config?.DarkMode ?? false,
sidebarStatus: instance.$config?.SidebarStatus ?? true,
epThemeColor: instance.$config?.EpThemeColor ?? "#409EFF"
};
}
//
if (!instance.$storage.configure) {
// eslint-disable-next-line
instance.$storage.configure = {
grey: instance.$config?.Grey ?? false,
weak: instance.$config?.Weak ?? false,
hideTabs: instance.$config?.HideTabs ?? false,
showLogo: instance.$config?.ShowLogo ?? true,
showModel: instance.$config?.ShowModel ?? "smart",
multiTagsCache: instance.$config?.MultiTagsCache ?? false
};
}
return instance.$storage?.layout.layout;
});
const { $storage } = useGlobal<GlobalPropertiesApi>();
const set: setType = reactive({
sidebar: computed(() => {
@ -96,18 +47,18 @@ const set: setType = reactive({
}),
hideTabs: computed(() => {
return instance.$storage?.configure.hideTabs;
return $storage?.configure.hideTabs;
})
});
function setTheme(layoutModel: string) {
window.document.body.setAttribute("layout", layoutModel);
instance.$storage.layout = {
$storage.layout = {
layout: `${layoutModel}`,
theme: instance.$storage.layout?.theme,
darkMode: instance.$storage.layout?.darkMode,
sidebarStatus: instance.$storage.layout?.sidebarStatus,
epThemeColor: instance.$storage.layout?.epThemeColor
theme: $storage.layout?.theme,
darkMode: $storage.layout?.darkMode,
sidebarStatus: $storage.layout?.sidebarStatus,
epThemeColor: $storage.layout?.epThemeColor
};
}
@ -123,7 +74,7 @@ let isAutoCloseSidebar = true;
emitter.on("resize", ({ detail }) => {
if (isMobile) return;
let { width } = detail;
width <= 670 ? setTheme("vertical") : setTheme(useAppStoreHook().layout);
width <= 760 ? setTheme("vertical") : setTheme(useAppStoreHook().layout);
/** width app-wrapper
* 0 < width <= 760 隐藏侧边栏
* 760 < width <= 990 折叠侧边栏
@ -138,7 +89,7 @@ emitter.on("resize", ({ detail }) => {
isAutoCloseSidebar = false;
}
} else if (width > 990) {
if (!set.sidebar.isClickHamburger) {
if (!set.sidebar.isClickCollapse) {
toggle("desktop", true);
isAutoCloseSidebar = true;
}
@ -165,7 +116,9 @@ const layoutHeader = defineComponent({
class: { "fixed-header": set.fixedHeader },
style: [
set.hideTabs && layout.value.includes("horizontal")
? "box-shadow: 0 1px 4px rgb(0 21 41 / 8%);"
? isDark.value
? "box-shadow: 0 1px 4px #0d0d0d"
: "box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08)"
: ""
]
},
@ -185,10 +138,14 @@ const layoutHeader = defineComponent({
default: () => [
h(
"span",
{ onClick: onFullScreen },
{
onClick: onFullScreen
},
{
default: () => [
!pureSetting.hiddenSideBar ? h(fullScreen) : h(exitScreen)
!pureSetting.hiddenSideBar
? h(fullScreen, { class: "dark:color-white" })
: h(exitScreen, { class: "dark:color-white" })
]
}
)
@ -234,7 +191,8 @@ const layoutHeader = defineComponent({
<el-backtop
title="回到顶部"
target=".main-container .el-scrollbar__wrap"
><backTop />
>
<backTop />
</el-backtop>
<layout-header />
<!-- 主体内容 -->

View File

@ -2,6 +2,10 @@
import { unref } from "vue";
import { useRouter } from "vue-router";
defineOptions({
name: "Redirect"
});
const { currentRoute, replace } = useRouter();
const { params, query } = unref(currentRoute);

View File

@ -1,84 +0,0 @@
/* 动态改变element-plus主题色 */
import rgbHex from "rgb-hex";
import epCss from "./element.scss";
import { TinyColor } from "@ctrl/tinycolor";
import { convert } from "css-color-function";
// 色值表
const formula = {
"shade-1": "color(primary shade(10%))",
"light-1": "color(primary tint(10%))",
"light-2": "color(primary tint(20%))",
"light-3": "color(primary tint(30%))",
"light-4": "color(primary tint(40%))",
"light-5": "color(primary tint(50%))",
"light-6": "color(primary tint(60%))",
"light-7": "color(primary tint(70%))",
"light-8": "color(primary tint(80%))",
"light-9": "color(primary tint(90%))"
};
// 把生成的样式表写入到style中
export const writeNewStyle = (newStyle: string): void => {
const style = window.document.createElement("style");
style.innerText = newStyle;
window.document.head.appendChild(style);
};
// 根据主题色,生成最新的样式表
export const createNewStyle = (
primaryStyle: Record<string, any>
): Record<string, any> => {
// 根据主色生成色值表
const colors = createColors(primaryStyle);
// 在当前ep的默认样式表中标记需要替换的色值
let cssText = getStyleTemplate(epCss);
// 遍历生成的色值表,在 默认样式表 进行全局替换
Object.keys(colors).forEach(key => {
cssText = cssText.replace(
new RegExp("(:|\\s+)" + key, "g"),
"$1" + colors[key]
);
});
return cssText;
};
export const createColors = (
primary: Record<string, any>
): Record<string, any> => {
if (!primary) return;
const colors = {
primary
};
Object.keys(formula).forEach(key => {
const value = formula[key].replace(/primary/, primary);
colors[key] = "#" + rgbHex(convert(value));
});
return colors;
};
const getStyleTemplate = (data: Record<string, any>): Record<string, any> => {
const colorMap = {
"#3a8ee6": "shade-1",
"#409eff": "primary",
"#53a8ff": "light-1",
"#66b1ff": "light-2",
"#79bbff": "light-3",
"#8cc5ff": "light-4",
"#a0cfff": "light-5",
"#b3d8ff": "light-6",
"#c6e2ff": "light-7",
"#d9ecff": "light-8",
"#ecf5ff": "light-9"
};
Object.keys(colorMap).forEach(key => {
const value = colorMap[key];
data = data.replace(new RegExp(key, "ig"), value);
});
return data;
};
// 自动计算hover和active颜色 https://element-plus.gitee.io/zh-CN/component/button.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E9%A2%9C%E8%89%B2-%E6%B5%8B%E8%AF%95%E7%89%88
export const shadeBgColor = (color: string): string => {
return new TinyColor(color).shade(10).toString();
};

View File

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

View File

@ -1,6 +1,20 @@
/**
* @description 使
*/
import { EpThemeColor } from "../../../public/serverConfig.json";
type MultipleScopeVarsItem = {
scopeName: string;
varsContent: string;
};
/** 将vxe默认主题色和ep默认主题色保持一致 */
const vxeColor = EpThemeColor;
/** 预设主题色 */
const themeColors = {
default: {
color: "#409EFF",
vxeColor,
subMenuActiveText: "#fff",
menuBg: "#001529",
menuHover: "#4091f7",
@ -13,7 +27,7 @@ const themeColors = {
menuActiveBefore: "#4091f7"
},
light: {
color: "#409EFF",
vxeColor,
subMenuActiveText: "#409eff",
menuBg: "#fff",
menuHover: "#e0ebf6",
@ -26,7 +40,7 @@ const themeColors = {
menuActiveBefore: "#4091f7"
},
dusk: {
color: "#f5222d",
vxeColor: "#f5222d",
subMenuActiveText: "#fff",
menuBg: "#2a0608",
menuHover: "#e13c39",
@ -39,7 +53,7 @@ const themeColors = {
menuActiveBefore: "#e13c39"
},
volcano: {
color: "#fa541c",
vxeColor: "#fa541c",
subMenuActiveText: "#fff",
menuBg: "#2b0e05",
menuHover: "#e85f33",
@ -52,7 +66,7 @@ const themeColors = {
menuActiveBefore: "#e85f33"
},
yellow: {
color: "#fadb14",
vxeColor: "#fadb14",
subMenuActiveText: "#d25f00",
menuBg: "#2b2503",
menuHover: "#f6da4d",
@ -65,7 +79,7 @@ const themeColors = {
menuActiveBefore: "#f6da4d"
},
mingQing: {
color: "#13c2c2",
vxeColor: "#13c2c2",
subMenuActiveText: "#fff",
menuBg: "#032121",
menuHover: "#59bfc1",
@ -78,7 +92,7 @@ const themeColors = {
menuActiveBefore: "#59bfc1"
},
auroraGreen: {
color: "#52c41a",
vxeColor: "#52c41a",
subMenuActiveText: "#fff",
menuBg: "#0b1e15",
menuHover: "#60ac80",
@ -91,7 +105,7 @@ const themeColors = {
menuActiveBefore: "#60ac80"
},
pink: {
color: "#eb2f96",
vxeColor: "#eb2f96",
subMenuActiveText: "#fff",
menuBg: "#28081a",
menuHover: "#d84493",
@ -104,7 +118,7 @@ const themeColors = {
menuActiveBefore: "#d84493"
},
saucePurple: {
color: "#722ed1",
vxeColor: "#722ed1",
subMenuActiveText: "#fff",
menuBg: "#130824",
menuHover: "#693ac9",
@ -118,19 +132,28 @@ const themeColors = {
}
};
type MultipleScopeVarsItem = {
scopeName: string;
path: string;
varsContent: string;
};
export function genScssMultipleScopeVars(): MultipleScopeVarsItem[] {
/**
* @description
*/
export const genScssMultipleScopeVars = (): MultipleScopeVarsItem[] => {
const result = [] as MultipleScopeVarsItem[];
Object.keys(themeColors).forEach(key => {
result.push({
scopeName: `layout-theme-${key}`,
varsContent: `$primary-color: ${themeColors[key].color} !default;$vxe-primary-color: $primary-color;$subMenuActiveText: ${themeColors[key].subMenuActiveText} !default;$menuBg: ${themeColors[key].menuBg} !default;$menuHover: ${themeColors[key].menuHover} !default;$subMenuBg: ${themeColors[key].subMenuBg} !default;$subMenuActiveBg: ${themeColors[key].subMenuActiveBg} !default;$navTextColor: ${themeColors[key].navTextColor} !default;$menuText: ${themeColors[key].menuText} !default;$sidebarLogo: ${themeColors[key].sidebarLogo} !default;$menuTitleHover: ${themeColors[key].menuTitleHover} !default;$menuActiveBefore: ${themeColors[key].menuActiveBefore} !default;`
varsContent: `
$vxe-primary-color: ${themeColors[key].vxeColor} !default;
$subMenuActiveText: ${themeColors[key].subMenuActiveText} !default;
$menuBg: ${themeColors[key].menuBg} !default;
$menuHover: ${themeColors[key].menuHover} !default;
$subMenuBg: ${themeColors[key].subMenuBg} !default;
$subMenuActiveBg: ${themeColors[key].subMenuActiveBg} !default;
$navTextColor: ${themeColors[key].navTextColor} !default;
$menuText: ${themeColors[key].menuText} !default;
$sidebarLogo: ${themeColors[key].sidebarLogo} !default;
$menuTitleHover: ${themeColors[key].menuTitleHover} !default;
$menuActiveBefore: ${themeColors[key].menuActiveBefore} !default;
`
} as MultipleScopeVarsItem);
});
return result;
}
};

View File

@ -12,7 +12,6 @@ export const routerArrays: Array<RouteConfigs> = [
export type routeMetaType = {
title?: string;
i18n?: boolean;
icon?: string;
showLink?: boolean;
savedPosition?: boolean;
@ -23,6 +22,7 @@ export type RouteConfigs = {
path?: string;
parentPath?: string;
query?: object;
params?: object;
meta?: routeMetaType;
children?: RouteConfigs[];
name?: string;
@ -44,7 +44,7 @@ export interface setType {
sidebar: {
opened: boolean;
withoutAnimation: boolean;
isClickHamburger: boolean;
isClickCollapse: boolean;
};
device: string;
fixedHeader: boolean;
@ -65,7 +65,6 @@ export type childrenType = {
meta?: {
icon?: string;
title?: string;
i18n?: boolean;
showParent?: boolean;
extraIcon?: {
svg?: boolean;

View File

@ -6,7 +6,12 @@ import { getServerConfig } from "./config";
import { createApp, Directive } from "vue";
import { useI18n } from "../src/plugins/i18n";
import { MotionPlugin } from "@vueuse/motion";
import { injectResponsiveStorage } from "/@/utils/storage/responsive";
// import { useEcharts } from "/@/plugins/echarts";
// import { useTable } from "../src/plugins/vxe-table";
import { injectResponsiveStorage } from "/@/utils/responsive";
// import Table from "@pureadmin/table";
// import PureDescriptions from "@pureadmin/descriptions";
import "uno.css";
import "animate.css";
@ -17,6 +22,7 @@ import "./style/index.scss";
import "element-plus/dist/index.css";
import "@pureadmin/components/dist/index.css";
import "@pureadmin/components/dist/theme.css";
import "@pureadmin/components/dist/dark.scss";
// 导入字体图标
import "./assets/iconfont/iconfont.js";
import "./assets/iconfont/iconfont.css";
@ -45,5 +51,9 @@ getServerConfig(app).then(async config => {
injectResponsiveStorage(app, config);
setupStore(app);
app.use(MotionPlugin).use(useI18n).use(ElementPlus);
// .use(useEcharts);
// .use(Table);
// .use(PureDescriptions);
// .use(useTable);
app.mount("#app");
});

View File

@ -1,6 +1,8 @@
import { createProdMockServer } from "vite-plugin-mock/es/createProdMockServer";
const modules = import.meta.globEager("../mock/*.ts");
const modules: Record<string, any> = import.meta.glob("../mock/*.ts", {
eager: true
});
const mockModules = [];
Object.keys(modules).forEach(key => {

View File

@ -0,0 +1,41 @@
import type { App } from "vue";
import * as echarts from "echarts/core";
import { SVGRenderer } from "echarts/renderers";
import { PieChart, BarChart, LineChart } from "echarts/charts";
import {
GridComponent,
TitleComponent,
LegendComponent,
GraphicComponent,
ToolboxComponent,
TooltipComponent,
DataZoomComponent,
VisualMapComponent
} from "echarts/components";
const { use } = echarts;
use([
PieChart,
BarChart,
LineChart,
SVGRenderer,
GridComponent,
TitleComponent,
LegendComponent,
GraphicComponent,
ToolboxComponent,
TooltipComponent,
DataZoomComponent,
VisualMapComponent
]);
/**
* @description echarts
* @see {@link https://echarts.apache.org/handbook/zh/basics/import#%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5-echarts-%E5%9B%BE%E8%A1%A8%E5%92%8C%E7%BB%84%E4%BB%B6}
*/
export function useEcharts(app: App) {
app.config.globalProperties.$echarts = echarts;
}
export default echarts;

View File

@ -1,6 +1,8 @@
import { App, Component } from "vue";
import {
ElTag,
ElAffix,
ElSkeleton,
ElBreadcrumb,
ElBreadcrumbItem,
ElScrollbar,
@ -8,21 +10,30 @@ import {
ElButton,
ElCol,
ElRow,
ElSpace,
ElDivider,
ElCard,
ElDropdown,
ElDialog,
ElMenu,
ElMenuItem,
ElDropdownItem,
ElDropdownMenu,
ElIcon,
ElInput,
ElForm,
ElFormItem,
ElPopover,
ElPopper,
ElTooltip,
ElDrawer,
ElPagination,
ElAlert,
ElRadio,
ElRadioButton,
ElRadioGroup,
ElDescriptions,
ElDescriptionsItem,
ElBacktop,
ElSwitch,
ElBadge,
@ -32,8 +43,22 @@ import {
ElEmpty,
ElCollapse,
ElCollapseItem,
ElDialog,
ElCard,
ElTable,
ElTableColumn,
ElLink,
ElColorPicker,
ElSelect,
ElOption,
ElTimeline,
ElTimelineItem,
ElResult,
ElSteps,
ElStep,
ElTree,
ElTreeV2,
ElPopconfirm,
ElCheckbox,
ElCheckboxGroup,
// 指令
ElLoading,
ElInfiniteScroll
@ -44,6 +69,8 @@ const plugins = [ElLoading, ElInfiniteScroll];
const components = [
ElTag,
ElAffix,
ElSkeleton,
ElBreadcrumb,
ElBreadcrumbItem,
ElScrollbar,
@ -51,21 +78,30 @@ const components = [
ElButton,
ElCol,
ElRow,
ElSpace,
ElDivider,
ElCard,
ElDropdown,
ElDialog,
ElMenu,
ElMenuItem,
ElDropdownItem,
ElDropdownMenu,
ElIcon,
ElInput,
ElForm,
ElFormItem,
ElPopover,
ElPopper,
ElTooltip,
ElDrawer,
ElPagination,
ElAlert,
ElRadio,
ElRadioButton,
ElRadioGroup,
ElDescriptions,
ElDescriptionsItem,
ElBacktop,
ElSwitch,
ElBadge,
@ -75,8 +111,22 @@ const components = [
ElEmpty,
ElCollapse,
ElCollapseItem,
ElDialog,
ElCard
ElTree,
ElTreeV2,
ElPopconfirm,
ElCheckbox,
ElCheckboxGroup,
ElTable,
ElTableColumn,
ElLink,
ElColorPicker,
ElSelect,
ElOption,
ElTimeline,
ElTimelineItem,
ElResult,
ElSteps,
ElStep
];
export function useElementPlus(app: App) {

View File

@ -1,6 +1,7 @@
// 多组件库的国际化和本地项目国际化兼容
import { App, WritableComputedRef } from "vue";
import { storageLocal } from "/@/utils/storage";
import type { StorageConfigs } from "/#/index";
import { storageLocal } from "@pureadmin/utils";
import { type I18n, createI18n } from "vue-i18n";
// element-plus国际化
@ -9,12 +10,12 @@ 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];
}
)
Object.entries(
import.meta.glob("../../locales/*.y(a)?ml", { eager: true })
).map(([key, value]: any) => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)[1];
return [matched, value.default];
})
)[prefix];
}
@ -62,7 +63,8 @@ export const $t = (key: string) => key;
export const i18n: I18n = createI18n({
legacy: false,
locale: storageLocal.getItem("responsive-locale")?.locale ?? "zh",
locale:
storageLocal.getItem<StorageConfigs>("responsive-locale")?.locale ?? "zh",
fallbackLocale: "en",
messages: localesConfigs
});

View File

@ -0,0 +1,6 @@
@import "vxe-table/styles/variable.scss";
@import "vxe-table/styles/modules.scss";
i {
border-color: initial;
}

View File

@ -0,0 +1,114 @@
import "xe-utils";
import "./index.scss";
import XEUtils from "xe-utils";
import { App, unref } from "vue";
import { i18n } from "/@/plugins/i18n";
import "font-awesome/css/font-awesome.min.css";
import zh from "vxe-table/lib/locale/lang/zh-CN";
import en from "vxe-table/lib/locale/lang/en-US";
import {
// 核心
VXETable,
// 表格功能
Icon,
Filter,
Edit,
Menu,
Export,
Keyboard,
Validator,
// 可选组件
Column,
Colgroup,
Grid,
Tooltip,
Toolbar,
Pager,
Form,
FormItem,
FormGather,
Checkbox,
CheckboxGroup,
Radio,
RadioGroup,
RadioButton,
Switch,
Input,
Select,
Optgroup,
Option,
Textarea,
Button,
Modal,
List,
Pulldown,
// 表格
Table
} from "vxe-table";
// 全局默认参数
VXETable.setup({
size: "medium",
version: 0,
zIndex: 1002,
table: {
// 自动监听父元素的变化去重新计算表格
autoResize: true,
// 鼠标移到行是否要高亮显示
highlightHoverRow: true
},
input: {
clearable: true
},
i18n: (key, args) => {
return unref(i18n.global.locale) === "zh"
? XEUtils.toFormatString(XEUtils.get(zh, key), args)
: XEUtils.toFormatString(XEUtils.get(en, key), args);
},
translate(key) {
const NAMESPACED = ["el.", "buttons."];
if (key && NAMESPACED.findIndex(v => key.includes(v)) !== -1) {
return i18n.global.t.call(i18n.global.locale, key);
}
return key;
}
});
export function useTable(app: App) {
app
.use(Icon)
.use(Filter)
.use(Edit)
.use(Menu)
.use(Export)
.use(Keyboard)
.use(Validator)
// 可选组件
.use(Column)
.use(Colgroup)
.use(Grid)
.use(Tooltip)
.use(Toolbar)
.use(Pager)
.use(Form)
.use(FormItem)
.use(FormGather)
.use(Checkbox)
.use(CheckboxGroup)
.use(Radio)
.use(RadioGroup)
.use(RadioButton)
.use(Switch)
.use(Input)
.use(Select)
.use(Optgroup)
.use(Option)
.use(Textarea)
.use(Button)
.use(Modal)
.use(List)
.use(Pulldown)
// 安装表格
.use(Table);
}

View File

@ -1,32 +1,32 @@
import { isUrl } from "/@/utils/is";
import { getConfig } from "/@/config";
import { toRouteType } from "./types";
import { openLink } from "/@/utils/link";
import NProgress from "/@/utils/progress";
import { findIndex } from "lodash-unified";
import type { StorageConfigs } from "/#/index";
import { transformI18n } from "/@/plugins/i18n";
import { storageSession } from "/@/utils/storage";
import { buildHierarchyTree } from "/@/utils/tree";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import {
Router,
RouteMeta,
createRouter,
RouteRecordRaw,
RouteComponent,
RouteRecordName
RouteComponent
} from "vue-router";
import {
ascending,
initRouter,
getHistoryMode,
getParentPaths,
findRouteByPath,
handleAliveRoute,
formatTwoStageRoutes,
formatFlatteningRoutes
} from "./utils";
import {
buildHierarchyTree,
openLink,
isUrl,
storageSession
} from "@pureadmin/utils";
import homeRouter from "./modules/home";
import errorRouter from "./modules/error";
@ -53,7 +53,7 @@ export const remainingPaths = Object.keys(remainingRouter).map(v => {
// 创建路由实例
export const router: Router = createRouter({
history: getHistoryMode(),
routes: constantRoutes.concat(...remainingRouter),
routes: constantRoutes.concat(...(remainingRouter as any)),
strict: true,
scrollBehavior(to, from, savedPosition) {
return new Promise(resolve => {
@ -70,6 +70,19 @@ export const router: Router = createRouter({
}
});
// 重置路由
export function resetRouter() {
router.getRoutes().forEach(route => {
const { name, meta } = route;
if (name && router.hasRoute(name) && meta?.backstage) {
router.removeRoute(name);
router.options.routes = formatTwoStageRoutes(
formatFlatteningRoutes(buildHierarchyTree(ascending(routes)))
);
}
});
}
// 路由白名单
const whiteList = ["/login"];
@ -78,13 +91,13 @@ router.beforeEach((to: toRouteType, _from, next) => {
const newMatched = to.matched;
handleAliveRoute(newMatched, "add");
// 页面整体刷新和点击标签页刷新
if (_from.name === undefined || _from.name === "redirect") {
if (_from.name === undefined || _from.name === "Redirect") {
handleAliveRoute(newMatched);
}
}
const name = storageSession.getItem("info");
const name = storageSession.getItem<StorageConfigs>("info");
NProgress.start();
const externalLink = isUrl(to?.name);
const externalLink = isUrl(to?.name as string);
if (!externalLink)
to.matched.some(item => {
if (!item.meta.title) return "";
@ -97,7 +110,7 @@ router.beforeEach((to: toRouteType, _from, next) => {
if (_from?.name) {
// name为超链接
if (externalLink) {
openLink(to?.name);
openLink(to?.name as string);
NProgress.done();
} else {
next();
@ -107,69 +120,22 @@ router.beforeEach((to: toRouteType, _from, next) => {
if (usePermissionStoreHook().wholeMenus.length === 0)
initRouter(name.username).then((router: Router) => {
if (!useMultiTagsStoreHook().getMultiTagsCache) {
const handTag = (
path: string,
parentPath: string,
name: RouteRecordName,
meta: RouteMeta
): void => {
const { path } = to;
const index = findIndex(remainingRouter, v => {
return v.path == path;
});
const routes: any =
index === -1
? router.options.routes[0].children
: router.options.routes;
const route = findRouteByPath(path, routes);
// query、params模式路由传参数的标签页不在此处处理
if (route && route.meta?.title) {
useMultiTagsStoreHook().handleTags("push", {
path,
parentPath,
name,
meta
path: route.path,
name: route.name,
meta: route.meta
});
};
// 未开启标签页缓存,刷新页面重定向到顶级路由(参考标签页操作例子,只针对静态路由)
if (to.meta?.refreshRedirect) {
const routes = router.options.routes;
const { refreshRedirect } = to.meta;
const { name, meta } = findRouteByPath(refreshRedirect, routes);
handTag(
refreshRedirect,
getParentPaths(refreshRedirect, routes)[1],
name,
meta
);
return router.push(refreshRedirect);
} else {
const { path } = to;
const index = findIndex(remainingRouter, v => {
return v.path == path;
});
const routes =
index === -1
? router.options.routes[0].children
: router.options.routes;
const route = findRouteByPath(path, routes);
const routePartent = getParentPaths(path, routes);
// 未开启标签页缓存,刷新页面重定向到顶级路由(参考标签页操作例子,只针对动态路由)
if (
path !== routes[0].path &&
route?.meta?.rank !== 0 &&
routePartent.length === 0
) {
if (!route?.meta?.refreshRedirect) return;
const { name, meta } = findRouteByPath(
route.meta.refreshRedirect,
routes
);
handTag(
route.meta?.refreshRedirect,
getParentPaths(route.meta?.refreshRedirect, routes)[0],
name,
meta
);
return router.push(route.meta?.refreshRedirect);
} else {
handTag(
route.path,
routePartent[routePartent.length - 1],
route.name,
route.meta
);
return router.push(path);
}
}
}
router.push(to.fullPath);

View File

@ -1,9 +1,8 @@
import { $t } from "/@/plugins/i18n";
const Layout = () => import("/@/layout/index.vue");
import type { RouteConfigsTable } from "/#/index";
const errorRouter = {
const errorRouter: RouteConfigsTable = {
path: "/error",
component: Layout,
redirect: "/error/403",
meta: {
icon: "information-line",

View File

@ -1,9 +1,10 @@
import { $t } from "/@/plugins/i18n";
import type { RouteConfigsTable } from "/#/index";
const Layout = () => import("/@/layout/index.vue");
const homeRouter = {
const homeRouter: RouteConfigsTable = {
path: "/",
name: "home",
name: "Home",
component: Layout,
redirect: "/welcome",
meta: {
@ -14,8 +15,8 @@ const homeRouter = {
children: [
{
path: "/welcome",
name: "welcome",
component: () => import("/@/views/welcome.vue"),
name: "Welcome",
component: () => import("/@/views/welcome/index.vue"),
meta: {
title: $t("menus.hshome")
}

View File

@ -1,10 +1,11 @@
import { $t } from "/@/plugins/i18n";
import type { RouteConfigsTable } from "/#/index";
const Layout = () => import("/@/layout/index.vue");
const remainingRouter = [
const remainingRouter: Array<RouteConfigsTable> = [
{
path: "/login",
name: "login",
name: "Login",
component: () => import("/@/views/login/index.vue"),
meta: {
title: $t("menus.hslogin"),
@ -24,7 +25,7 @@ const remainingRouter = [
children: [
{
path: "/redirect/:path(.*)",
name: "redirect",
name: "Redirect",
component: () => import("/@/layout/redirect.vue")
}
]

View File

@ -3,7 +3,6 @@ import { RouteLocationNormalized } from "vue-router";
export interface toRouteType extends RouteLocationNormalized {
meta: {
keepAlive?: boolean;
refreshRedirect: string;
dynamicLevel?: string;
};
}

View File

@ -7,12 +7,13 @@ import {
RouteRecordNormalized
} from "vue-router";
import { router } from "./index";
import { isProxy, toRaw } from "vue";
import { loadEnv } from "../../build";
import { cloneDeep } from "lodash-unified";
import { useTimeoutFn } from "@vueuse/core";
import { RouteConfigs } from "/@/layout/types";
import { buildHierarchyTree } from "/@/utils/tree";
import { buildHierarchyTree } from "@pureadmin/utils";
import { usePermissionStoreHook } from "/@/store/modules/permission";
const Layout = () => import("/@/layout/index.vue");
const IFrame = () => import("/@/layout/frameView.vue");
// https://cn.vitejs.dev/guide/features.html#glob-import
const modulesRoutes = import.meta.glob("/src/views/**/*.{vue,tsx}");
@ -25,7 +26,7 @@ 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 !== "/") {
if (v.name !== "Home" && v.path !== "/") {
console.warn("rank only the home page can be 0");
}
}
@ -39,7 +40,7 @@ function ascending(arr: any[]) {
// 过滤meta中showLink为false的路由
function filterTree(data: RouteComponent[]) {
const newTree = data.filter(
const newTree = cloneDeep(data).filter(
(v: { meta: { showLink: boolean } }) => v.meta?.showLink !== false
);
newTree.forEach(
@ -86,7 +87,7 @@ function getParentPaths(path: string, routes: RouteRecordRaw[]) {
function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
let res = routes.find((item: { path: string }) => item.path == path);
if (res) {
return res;
return isProxy(res) ? toRaw(res) : res;
} else {
for (let i = 0; i < routes.length; i++) {
if (
@ -95,7 +96,7 @@ function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
) {
res = findRouteByPath(path, routes[i].children);
if (res) {
return res;
return isProxy(res) ? toRaw(res) : res;
}
}
}
@ -103,14 +104,14 @@ function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
}
}
// 重置路由
function resetRouter(): void {
router.getRoutes().forEach(route => {
const { name } = route;
if (name) {
router.hasRoute(name) && router.removeRoute(name);
}
});
function addPathMatch() {
if (!router.hasRoute("pathMatch")) {
router.addRoute({
path: "/:pathMatch(.*)",
name: "pathMatch",
redirect: "/error/404"
});
}
}
// 初始化路由
@ -135,7 +136,7 @@ function initRouter(name: string) {
// 最终路由进行升序
ascending(router.options.routes[0].children);
if (!router.hasRoute(v?.name)) router.addRoute(v);
const flattenRouters = router
const flattenRouters: any = router
.getRoutes()
.find(n => n.path === "/");
router.addRoute(flattenRouters);
@ -145,10 +146,7 @@ function initRouter(name: string) {
);
usePermissionStoreHook().changeSetting(info);
}
router.addRoute({
path: "/:pathMatch(.*)",
redirect: "/error/404"
});
addPathMatch();
});
});
}
@ -229,18 +227,18 @@ function addAsyncRoutes(arrRoutes: Array<RouteRecordRaw>) {
if (!arrRoutes || !arrRoutes.length) return;
const modulesRoutesKeys = Object.keys(modulesRoutes);
arrRoutes.forEach((v: RouteRecordRaw) => {
if (v.redirect) {
v.component = Layout;
} else if (v.meta?.frameSrc) {
v.component = IFrame;
} else {
// 对后端传component组件路径和不传做兼容如果后端传component组件路径那么path可以随便写如果不传component组件路径会根path保持一致
const index = v?.component
? // @ts-expect-error
modulesRoutesKeys.findIndex(ev => ev.includes(v.component))
: modulesRoutesKeys.findIndex(ev => ev.includes(v.path));
v.component = modulesRoutes[modulesRoutesKeys[index]];
}
// 将backstage属性加入meta标识此路由为后端返回路由
v.meta.backstage = true;
// 父级的redirect属性取值如果子级存在且父级的redirect属性不存在默认取第一个子级的path如果子级存在且父级的redirect属性存在取存在的redirect属性会覆盖默认值
if (v?.children && !v.redirect) v.redirect = v.children[0].path;
// 父级的name属性取值如果子级存在且父级的name属性不存在默认取第一个子级的name如果子级存在且父级的name属性存在取存在的name属性会覆盖默认值
if (v?.children && !v.name) v.name = v.children[0].name;
if (v.meta?.frameSrc) v.component = IFrame;
// 对后端传component组件路径和不传做兼容如果后端传component组件路径那么path可以随便写如果不传component组件路径会跟path保持一致
const index = v?.component
? modulesRoutesKeys.findIndex(ev => ev.includes(v.component as any))
: modulesRoutesKeys.findIndex(ev => ev.includes(v.path));
v.component = modulesRoutes[modulesRoutesKeys[index]];
if (v.children) {
addAsyncRoutes(v.children);
}
@ -295,7 +293,6 @@ export {
ascending,
filterTree,
initRouter,
resetRouter,
hasPermissions,
getHistoryMode,
addAsyncRoutes,

View File

@ -2,22 +2,23 @@ import { store } from "/@/store";
import { appType } from "./types";
import { defineStore } from "pinia";
import { getConfig } from "/@/config";
import { storageLocal } from "/@/utils/storage";
import { deviceDetection } from "/@/utils/deviceDetection";
import type { StorageConfigs } from "/#/index";
import { deviceDetection, storageLocal } from "@pureadmin/utils";
export const useAppStore = defineStore({
id: "pure-app",
state: (): appType => ({
sidebar: {
opened:
storageLocal.getItem("responsive-layout")?.sidebarStatus ??
getConfig().SidebarStatus,
storageLocal.getItem<StorageConfigs>("responsive-layout")
?.sidebarStatus ?? getConfig().SidebarStatus,
withoutAnimation: false,
isClickHamburger: false
isClickCollapse: false
},
// 这里的layout用于监听容器拖拉后恢复对应的导航模式
layout:
storageLocal.getItem("responsive-layout")?.layout ?? getConfig().Layout,
storageLocal.getItem<StorageConfigs>("responsive-layout")?.layout ??
getConfig().Layout,
device: deviceDetection() ? "mobile" : "desktop"
}),
getters: {
@ -30,7 +31,7 @@ export const useAppStore = defineStore({
},
actions: {
TOGGLE_SIDEBAR(opened?: boolean, resize?: string) {
const layout = storageLocal.getItem("responsive-layout");
const layout = storageLocal.getItem<StorageConfigs>("responsive-layout");
if (opened && resize) {
this.sidebar.withoutAnimation = true;
this.sidebar.opened = true;
@ -42,19 +43,16 @@ export const useAppStore = defineStore({
} else if (!opened && !resize) {
this.sidebar.withoutAnimation = false;
this.sidebar.opened = !this.sidebar.opened;
this.sidebar.isClickHamburger = !this.sidebar.opened;
this.sidebar.isClickCollapse = !this.sidebar.opened;
layout.sidebarStatus = this.sidebar.opened;
}
storageLocal.setItem("responsive-layout", layout);
},
TOGGLE_DEVICE(device: string) {
this.device = device;
},
async toggleSideBar(opened?: boolean, resize?: string) {
await this.TOGGLE_SIDEBAR(opened, resize);
},
toggleDevice(device) {
this.TOGGLE_DEVICE(device);
toggleDevice(device: string) {
this.device = device;
},
setLayout(layout) {
this.layout = layout;

View File

@ -1,16 +1,18 @@
import { store } from "/@/store";
import { defineStore } from "pinia";
import { getConfig } from "/@/config";
import { storageLocal } from "/@/utils/storage";
import type { StorageConfigs } from "/#/index";
import { storageLocal } from "@pureadmin/utils";
export const useEpThemeStore = defineStore({
id: "pure-epTheme",
state: () => ({
epThemeColor:
storageLocal.getItem("responsive-layout")?.epThemeColor ??
storageLocal.getItem<StorageConfigs>("responsive-layout")?.epThemeColor ??
getConfig().EpThemeColor,
epTheme:
storageLocal.getItem("responsive-layout")?.theme ?? getConfig().Theme
storageLocal.getItem<StorageConfigs>("responsive-layout")?.theme ??
getConfig().Theme
}),
getters: {
getEpThemeColor() {
@ -28,8 +30,8 @@ export const useEpThemeStore = defineStore({
}
},
actions: {
setEpThemeColor(newColor) {
const layout = storageLocal.getItem("responsive-layout");
setEpThemeColor(newColor: string): void {
const layout = storageLocal.getItem<StorageConfigs>("responsive-layout");
this.epTheme = layout?.theme;
this.epThemeColor = newColor;
layout.epThemeColor = newColor;

View File

@ -1,27 +1,21 @@
import { defineStore } from "pinia";
import { store } from "/@/store";
import { isUrl } from "/@/utils/is";
import { isEqual } from "lodash-unified";
import { storageLocal } from "/@/utils/storage";
import type { StorageConfigs } from "/#/index";
import { routerArrays } from "/@/layout/types";
import { multiType, positionType } from "./types";
import { isUrl, storageLocal } from "@pureadmin/utils";
export const useMultiTagsStore = defineStore({
id: "pure-multiTags",
state: () => ({
// 存储标签页信息(路由信息)
multiTags: storageLocal.getItem("responsive-configure").multiTagsCache
? storageLocal.getItem("responsive-tags")
: [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "menus.hshome",
icon: "home-filled"
}
}
],
multiTagsCache: storageLocal.getItem("responsive-configure").multiTagsCache
multiTags: storageLocal.getItem<StorageConfigs>("responsive-configure")
.multiTagsCache
? storageLocal.getItem<StorageConfigs>("responsive-tags")
: [...routerArrays],
multiTagsCache: storageLocal.getItem<StorageConfigs>("responsive-configure")
.multiTagsCache
}),
getters: {
getMultiTagsCache() {
@ -54,8 +48,13 @@ export const useMultiTagsStore = defineStore({
case "push":
{
const tagVal = value as multiType;
// 不添加到标签页
if (tagVal?.meta?.hiddenTag) return;
// 如果是外链无需添加信息到标签页
if (isUrl(tagVal?.name)) return;
const tagPath = tagVal?.path;
// 如果title为空拒绝添加空信息到标签页
if (tagVal?.meta?.title.length === 0) return;
const tagPath = tagVal.path;
// 判断tag是否已存在
const tagHasExits = this.multiTags.some(tag => {
return tag.path === tagPath;
@ -63,20 +62,24 @@ export const useMultiTagsStore = defineStore({
// 判断tag中的query键值是否相等
const tagQueryHasExits = this.multiTags.some(tag => {
return isEqual(tag.query, tagVal?.query);
return isEqual(tag?.query, tagVal?.query);
});
if (tagHasExits && tagQueryHasExits) return;
// 判断tag中的params键值是否相等
const tagParamsHasExits = this.multiTags.some(tag => {
return isEqual(tag?.params, tagVal?.params);
});
if (tagHasExits && tagQueryHasExits && tagParamsHasExits) return;
// 动态路由可打开的最大数量
const dynamicLevel = tagVal?.meta?.dynamicLevel ?? -1;
if (dynamicLevel > 0) {
// dynamicLevel动态路由可打开的数量
// 获取到已经打开的动态路由数, 判断是否大于dynamicLevel
if (
this.multiTags.filter(e => e?.path === tagPath).length >=
dynamicLevel
) {
// 关闭第一个
// 如果当前已打开的动态路由数大于dynamicLevel替换第一个动态路由标签
const index = this.multiTags.findIndex(
item => item?.path === tagPath
);

View File

@ -14,8 +14,8 @@ export type appType = {
sidebar: {
opened: boolean;
withoutAnimation: boolean;
// 判断是否手动点击Hamburger
isClickHamburger: boolean;
// 判断是否手动点击Collapse
isClickCollapse: boolean;
};
layout: string;
device: string;
@ -27,6 +27,7 @@ export type multiType = {
name: string;
meta: any;
query?: object;
params?: object;
};
export type setType = {

View File

@ -2,7 +2,8 @@ import { defineStore } from "pinia";
import { store } from "/@/store";
import { userType } from "./types";
import { router } from "/@/router";
import { storageSession } from "/@/utils/storage";
import { routerArrays } from "/@/layout/types";
import { storageSession } from "@pureadmin/utils";
import { getLogin, refreshToken } from "/@/api/user";
import { getToken, setToken, removeToken } from "/@/utils/auth";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
@ -25,7 +26,7 @@ export const useUserStore = defineStore({
name,
// 前端生成的验证码(按实际需求替换)
verifyCode: "",
// 登陆显示组件判断 0登陆 1手机登陆 2二维码登陆 3注册 4忘记密码默认0登陆
// 登录显示组件判断 0登录 1手机登录 2二维码登录 3注册 4忘记密码默认0登录
currentPage: 0
}),
actions: {
@ -62,16 +63,7 @@ export const useUserStore = defineStore({
this.name = "";
removeToken();
storageSession.clear();
useMultiTagsStoreHook().handleTags("equal", [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "menus.hshome",
icon: "home-filled"
}
}
]);
useMultiTagsStoreHook().handleTags("equal", routerArrays);
router.push("/login");
},
// 刷新token

View File

@ -1,28 +1,163 @@
/* 暗黑模式 */
[data-theme="dark"] {
filter: invert(0.9) hue-rotate(180deg);
@import "element-plus/theme-chalk/src/dark/css-vars.scss";
img,
.icon-svg,
.login-container {
filter: invert(1) hue-rotate(180deg);
/* 暗黑模式适配 */
html.dark {
/* 自定义深色背景颜色 */
// --el-bg-color: #020409;
$border-style: #303030;
$color-white: #fff;
.navbar,
.tags-view,
.contextmenu,
.sidebar-container,
.horizontal-header,
.sidebar-logo-container,
.horizontal-header .el-sub-menu__title,
.horizontal-header .submenu-title-noDropdown {
background: var(--el-bg-color) !important;
}
// element plus
.el-radio-button__original-radio:checked + .el-radio-button__inner,
.el-image-viewer__close,
.el-image-viewer__actions__inner,
.el-image-viewer__next,
.el-image-viewer__prev {
color: #000 !important;
.app-main {
background: #020409 !important;
}
.el-overlay {
background-color: rgb(0 0 0 / 5%) !important;
.frame {
filter: invert(0.9) hue-rotate(180deg);
}
.el-drawer {
box-shadow: 0 8px 10px -5px rgb(0 0 0 / 1%), 0 16px 24px 2px rgb(0 0 0 / 2%),
0 6px 30px 5px rgb(0 0 0 / 1%);
.ant-tabs {
background: var(--el-bg-color);
color: $color-white;
}
/* 标签页 */
.tags-view {
.arrow-left,
.arrow-right {
box-shadow: none;
}
.arrow-right {
border-left: 1px solid $border-style;
}
.arrow-left,
.arrow-right,
.right-button li {
border-right: 1px solid $border-style;
}
}
/* vxe-table */
.vxe-table--header-wrapper,
.vxe-table--body-wrapper {
color: var(--el-text-color-primary);
background: var(--el-bg-color) !important;
}
.vxe-table--render-default.border--full .vxe-header--column,
.vxe-table--render-default.border--full .vxe-body--column,
.vxe-table--render-default.border--full .vxe-footer--column {
background-image: linear-gradient(
var(--el-border-color-lighter),
var(--el-border-color-lighter)
),
linear-gradient(
var(--el-border-color-lighter),
var(--el-border-color-lighter)
);
}
/* 表头 */
.vxe-table--header-wrapper {
background: #262727 !important;
}
.vxe-table--render-wrapper,
.vxe-table--main-wrapper {
border: none;
}
.vxe-pager.is--perfect,
.vxe-table--render-default .vxe-table--border-line {
border: 1px solid var(--el-border-color-lighter);
}
.vxe-table--header-border-line {
border-bottom: 1px solid var(--el-border-color-lighter) !important;
}
.vxe-body--row.row--hover,
.vxe-pager {
background-color: #262727;
}
.vxe-input--inner,
.vxe-pager .vxe-pager--jump-prev,
.vxe-pager .vxe-pager--prev-btn,
.vxe-pager .vxe-pager--next-btn,
.vxe-pager .vxe-pager--jump-next,
.vxe-pager .vxe-pager--num-btn,
.vxe-pager .vxe-pager--jump .vxe-pager--goto {
background-color: transparent;
color: var(--el-text-color-primary);
// outline: none !important;
}
.vxe-select-option--wrapper {
background: var(--el-bg-color) !important;
}
.vxe-select-option:not(.is--disabled).is--hover {
background: var(--el-color-primary-light-6) !important;
}
.vxe-modal--wrapper.type--modal .vxe-modal--box,
.vxe-modal--wrapper.type--alert .vxe-modal--box,
.vxe-modal--wrapper.type--confirm .vxe-modal--box,
.vxe-form {
background: var(--el-bg-color) !important;
}
.vxe-modal--box,
.vxe-modal--header {
border: none;
background: var(--el-bg-color) !important;
}
.vxe-modal--title,
.vxe-button--content {
color: var(--el-text-color-primary);
}
.vxe-button.type--button.size--medium:hover {
background: var(--el-color-primary) !important;
}
/* 项目配置面板 */
.right-panel-items {
.el-divider__text {
--el-bg-color: var(--el-bg-color);
}
.el-divider--horizontal {
border-top: none;
}
}
/* element-plus */
.el-table__cell {
background: var(--el-bg-color);
}
.el-card {
--el-card-bg-color: var(--el-bg-color);
// border: none !important;
}
.el-backtop {
--el-backtop-bg-color: var(--el-color-primary-light-9);
--el-backtop-hover-bg-color: var(--el-color-primary);
}
.el-dropdown-menu__item:not(.is-disabled):hover {
background: transparent;
}
}

View File

@ -25,7 +25,7 @@
}
.el-dropdown-menu {
padding: 2px 0 !important;
padding: 0 !important;
}
.el-range-separator {
@ -46,13 +46,6 @@
padding: 0 !important;
}
/* 动态改变cssvar 用于主题切换 https://github.com/element-plus/element-plus/issues/4856#issuecomment-1000174357 */
.el-button--primary,
.el-button--primary.is-plain {
--el-button-active-bg-color: var(--el-color-primary) !important;
--el-button-active-border-color: var(--el-color-primary) !important;
}
/* nprogress适配ep的primary */
#nprogress {
& .bar {

View File

@ -4,6 +4,10 @@
@import "./sidebar.scss";
@import "./dark.scss";
:root {
--pure-transition-duration: 0.016s;
}
body {
width: 100%;
height: 100%;
@ -47,3 +51,8 @@ html {
.mobile-spacing {
margin: 0;
}
/* 重置vxe-table中pager样式 */
.vxe-pager .vxe-pager--num-btn:not(.is--disabled).is--active {
color: #fff !important;
}

View File

@ -6,6 +6,12 @@
}
}
@mixin relative {
position: relative;
width: 100%;
height: 100%;
}
@mixin scrollBar {
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
@ -20,9 +26,3 @@
border-radius: 20px;
}
}
@mixin relative {
position: relative;
width: 100%;
height: 100%;
}

View File

@ -16,17 +16,18 @@
.sub-menu-icon {
vertical-align: middle;
margin-right: 5px;
font-size: 18px;
display: inline-flex;
justify-content: center;
align-items: center;
margin-right: 5px;
}
.main-container {
height: 100vh;
min-height: 100%;
transition: margin-left 0.1s;
/* main-content属性动画 */
transition: margin-left var(--pure-transition-duration);
margin-left: $sideBarWidth;
position: relative;
background: #f0f2f5;
@ -43,7 +44,8 @@
right: 0;
z-index: 998;
width: calc(100% - 210px);
transition: width 0.1s;
/* fixed-header属性左上角动画 */
transition: width var(--pure-transition-duration);
}
.main-hidden {
@ -63,7 +65,8 @@
}
.sidebar-container {
transition: width 0.1s;
/* 展开动画 */
transition: width var(--pure-transition-duration);
width: $sideBarWidth !important;
background: $menuBg;
height: 100%;
@ -80,22 +83,21 @@
overflow-x: hidden !important;
}
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out,
0s padding-right ease-in-out;
}
.el-scrollbar__bar.is-vertical {
right: 0;
}
.el-scrollbar {
height: 100%;
height: calc(100% - 44px);
}
&.has-logo {
.el-scrollbar {
height: calc(100% - 50px);
.el-scrollbar.pc {
/* logo: 48px、leftCollapse: 40px、leftCollapse-shadow: 4px */
height: calc(100% - 92px);
}
.el-scrollbar.mobile {
height: 100%;
}
}
@ -121,7 +123,7 @@
.el-sub-menu__title {
height: 50px;
color: $menuText;
padding: 0 20px 0 40px;
background-color: transparent !important;
&:hover {
color: $menuTitleHover !important;
@ -164,7 +166,7 @@
/* 无子集的激活菜单背景 */
.is-active.submenu-title-noDropdown.outer-most {
background: $subMenuActiveBg;
background: $subMenuActiveBg !important;
}
/* 有子集的激活菜单背景 */
@ -189,7 +191,7 @@
align-items: center;
padding-left: 10px;
cursor: pointer;
transition: all 0.2s ease;
transition: all 0.125s ease;
i {
font-size: 30px;
@ -219,54 +221,46 @@
color: $subMenuActiveText;
justify-content: flex-end;
/* 搜索 */
.search-container,
/* 告警 */
.dropdown-badge,
/* 全屏 */
.screen-full,
/* 国际化 */
.globalization,
/* 登陆名 */
.el-dropdown-link,
/* 设置 */
.el-icon-setting {
&:hover {
background: $menuHover;
}
}
.dropdown-badge {
height: 48px;
color: $subMenuActiveText;
&:hover {
background: $menuHover;
}
}
.search-container {
&:hover {
background: $menuHover;
}
}
.screen-full {
cursor: pointer;
&:hover {
background: $menuHover;
}
}
.globalization {
height: 48px;
width: 40px;
height: 48px;
padding: 11px;
outline: none;
cursor: pointer;
color: $subMenuActiveText;
&:hover {
background: $menuHover;
}
}
.el-dropdown-link {
height: 48px;
padding: 10px;
display: flex;
cursor: pointer;
align-items: center;
justify-content: space-around;
cursor: pointer;
color: $subMenuActiveText;
&:hover {
background: $menuHover;
}
p {
font-size: 14px;
}
@ -285,18 +279,14 @@
display: flex;
cursor: pointer;
align-items: center;
&:hover {
background: $menuHover;
}
}
}
.el-menu {
border: none;
height: 100%;
background-color: transparent;
width: 100% !important;
background-color: transparent;
}
.el-menu-item,
@ -322,7 +312,6 @@
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: $subMenuActiveText !important;
border-bottom-color: #409eff;
i {
color: $subMenuActiveText !important;
@ -332,7 +321,6 @@
.is-active {
transition: color 0.3s;
color: $subMenuActiveText !important;
border-bottom-color: #409eff;
}
}
@ -344,15 +332,6 @@
.el-menu-item {
span {
font-size: 12px;
margin-left: 10px;
}
}
.el-sub-menu__title {
color: $menuText;
span {
margin-left: 10px;
}
}
}
@ -402,15 +381,19 @@
.el-menu-item,
.el-sub-menu {
i {
width: 20px;
text-align: center;
font-size: 16px;
}
// i {
// width: 20px;
// text-align: center;
// font-size: 16px;
// }
i.fa {
margin-right: 5px;
font-size: 16px;
// i.fa {
// margin-right: 5px;
// font-size: 16px;
// }
.el-menu-tooltip__trigger {
width: 54px;
padding: 18px !important;
}
}
}
@ -431,16 +414,11 @@
span {
font-size: 12px;
margin-left: 10px;
}
}
.el-sub-menu__title {
color: $menuText;
span {
margin-left: 10px;
}
}
}
@ -495,7 +473,9 @@
/* 有子菜单 */
.el-menu--collapse
.is-active.outer-most.el-sub-menu
> .el-sub-menu__title::before {
> .el-sub-menu__title::before,
/* 无子菜单 */
.el-menu--collapse .is-active.submenu-title-noDropdown.outer-most::before {
position: absolute;
top: 0;
left: 2px;
@ -504,20 +484,7 @@
background-color: $menuActiveBefore;
content: "";
clear: both;
transition: all 0.2s ease-in-out;
transform: translateY(0);
}
/* 无子菜单 */
.el-menu--collapse .is-active.submenu-title-noDropdown.outer-most::before {
position: absolute;
top: 0;
width: 2px;
height: 100%;
background-color: $menuActiveBefore;
content: "";
clear: both;
transition: all 0.2s ease-in-out;
transition: all 0.125s ease-in-out;
transform: translateY(0);
}
@ -536,7 +503,7 @@
.mobile {
.fixed-header {
width: 100% !important;
transition: width 0.1s;
transition: width var(--pure-transition-duration);
}
.main-container {
@ -544,7 +511,7 @@
}
.sidebar-container {
transition: transform 0.1s;
transition: transform var(--pure-transition-duration);
width: $sideBarWidth;
}
@ -556,14 +523,6 @@
}
}
}
/* vertical菜单下hideSidebar去除动画 */
.withoutAnimation {
.main-container,
.sidebar-container {
transition: none;
}
}
}
body[layout="vertical"] {
@ -581,26 +540,22 @@ body[layout="vertical"] {
.hideSidebar {
.fixed-header {
width: calc(100% - 54px);
transition: width 0.1s;
transition: width var(--pure-transition-duration);
}
.sidebar-container {
transition: width 0.125s;
width: 54px !important;
.is-active.submenu-title-noDropdown.outer-most {
background: transparent !important;
}
}
.main-container {
margin-left: 54px;
}
.submenu-title-noDropdown {
padding: 0 !important;
position: relative;
.el-tooltip {
padding: 0 !important;
}
}
/* 菜单折叠 */
.el-menu--collapse {
.el-sub-menu {
@ -619,20 +574,31 @@ body[layout="vertical"] {
background: transparent !important;
}
/* 有无子菜单 */
.el-sub-menu__title,
.el-menu-item [class^="el-icon"] {
right: 2px;
.el-sub-menu__title {
padding: 0 18px !important;
}
}
.el-menu-tooltip__trigger {
padding: 0 18px;
}
.sub-menu-icon {
margin-right: 0;
}
}
.sub-menu-icon {
width: 24px;
/* 搜索 */
.search-container,
/* 告警 */
.dropdown-badge,
/* 全屏 */
.screen-full,
/* 国际化 */
.globalization,
/* 登陆名 */
.el-dropdown-link,
/* 设置 */
.el-icon-setting {
&:hover {
background: #f6f6f6;
}
}
}
@ -650,6 +616,10 @@ body[layout="mix"] {
$sideBarWidth: 210px;
@include merge-style($sideBarWidth);
.el-menu--collapse {
width: 54px;
}
.el-menu {
--el-menu-hover-bg-color: transparent !important;
}
@ -657,26 +627,22 @@ body[layout="mix"] {
.hideSidebar {
.fixed-header {
width: calc(100% - 54px);
transition: width 0.1s;
transition: width var(--pure-transition-duration);
}
.sidebar-container {
transition: width 0.125s;
width: 54px !important;
.is-active.submenu-title-noDropdown.outer-most {
background: transparent !important;
}
}
.main-container {
margin-left: 54px;
}
.submenu-title-noDropdown {
padding: 0 !important;
position: relative;
.el-tooltip {
padding: 0 !important;
}
}
/* 菜单折叠 */
.el-menu--collapse {
.el-sub-menu {
@ -690,19 +656,6 @@ body[layout="mix"] {
}
}
}
/* 无子菜单 */
.el-menu-item [class^="el-icon"] {
right: 5px;
}
.el-sub-menu__title [class^="el-icon"] {
right: 2px;
}
.submenu-title-noDropdown {
background: transparent !important;
}
}
}
}

View File

@ -40,3 +40,16 @@
.breadcrumb-leave-active {
position: absolute;
}
/**
* @description 重置el-menu的展开收起动画时长
* @see {@link https://github.com/element-plus/element-plus/issues/4509#issuecomment-980165001}
*/
.outer-most .el-collapse-transition-leave-active,
.outer-most .el-collapse-transition-enter-active {
transition: 0.12s all ease-in-out !important;
}
.horizontal-collapse-transition {
transition: var(--pure-transition-duration) all !important;
}

5
src/utils/README.md Normal file
View File

@ -0,0 +1,5 @@
### 注意
- [文档](https://pure-admin-utils-docs.vercel.app/)
- [npm](https://www.npmjs.com/package/@pureadmin/utils)
- vue-pure-admin 从 3.3.0 版本之后(不包括 3.3.0 版本),大部分工具和 hooks 都集成到了[@pureadmin/utils](https://pure-admin-utils-docs.vercel.app/)

View File

@ -1,39 +0,0 @@
import { unref } from "vue";
import type { Ref } from "vue";
type FunctionArgs<Args extends any[] = any[], Return = void> = (
...args: Args
) => Return;
type MaybeRef<T> = T | Ref<T>;
// 延迟函数
export const delay = (timeout: number) =>
new Promise(resolve => setTimeout(resolve, timeout));
/**
*
* @param fn
* @param timeout
* @param immediate
* @returns
*/
export const debounce = <T extends FunctionArgs>(
fn: T,
timeout: MaybeRef<number> = 200,
immediate = false
) => {
let timmer: TimeoutHandle;
const wait = unref(timeout);
return () => {
timmer && clearTimeout(timmer);
if (immediate) {
if (!timmer) {
fn();
}
timmer = setTimeout(() => (timmer = null), wait);
} else {
timmer = setTimeout(fn, wait);
}
};
};

View File

@ -1,37 +0,0 @@
interface deviceInter {
match: Fn;
}
interface BrowserInter {
browser: string;
version: string;
}
// 检测设备类型(手机返回true,反之)
export const deviceDetection = () => {
const sUserAgent: deviceInter = navigator.userAgent.toLowerCase();
// const bIsIpad = sUserAgent.match(/ipad/i) == "ipad";
const bIsIphoneOs = sUserAgent.match(/iphone os/i) == "iphone os";
const bIsMidp = sUserAgent.match(/midp/i) == "midp";
const bIsUc7 = sUserAgent.match(/rv:1.2.3.4/i) == "rv:1.2.3.4";
const bIsUc = sUserAgent.match(/ucweb/i) == "ucweb";
const bIsAndroid = sUserAgent.match(/android/i) == "android";
const bIsCE = sUserAgent.match(/windows ce/i) == "windows ce";
const bIsWM = sUserAgent.match(/windows mobile/i) == "windows mobile";
return (
bIsIphoneOs || bIsMidp || bIsUc7 || bIsUc || bIsAndroid || bIsCE || bIsWM
);
};
// 获取浏览器型号以及版本
export const getBrowserInfo = () => {
const ua = navigator.userAgent.toLowerCase();
const re = /(msie|firefox|chrome|opera|version).*?([\d.]+)/;
const m = ua.match(re);
const Sys: BrowserInter = {
browser: m[1].replace(/version/, "'safari"),
version: m[2]
};
return Sys;
};

View File

@ -1,118 +0,0 @@
const toString = Object.prototype.toString;
export function is(val: unknown, type: string) {
return toString.call(val) === `[object ${type}]`;
}
export function isDef<T = unknown>(val?: T): val is T {
return typeof val !== "undefined";
}
export function isUnDef<T = unknown>(val?: T): val is T {
return !isDef(val);
}
export function isObject(val: any): val is Record<any, any> {
return val !== null && is(val, "Object");
}
export function isEmpty<T = unknown>(val: T): val is T {
if (isArray(val) || isString(val)) {
return val.length === 0;
}
if (val instanceof Map || val instanceof Set) {
return val.size === 0;
}
if (isObject(val)) {
return Object.keys(val).length === 0;
}
return false;
}
export function isDate(val: unknown): val is Date {
return is(val, "Date");
}
export function isNull(val: unknown): val is null {
return val === null;
}
export function isNullAndUnDef(val: unknown): val is null | undefined {
return isUnDef(val) && isNull(val);
}
export function isNullOrUnDef(val: unknown): val is null | undefined {
return isUnDef(val) || isNull(val);
}
export function isNumber(val: unknown): val is number {
return is(val, "Number");
}
export function isPromise<T = any>(val: unknown): val is Promise<T> {
return (
is(val, "Promise") &&
isObject(val) &&
isFunction(val.then) &&
isFunction(val.catch)
);
}
export function isString(val: unknown): val is string {
return is(val, "String");
}
export function isFunction(val: unknown): val is Function {
return typeof val === "function";
}
export function isBoolean(val: unknown): val is boolean {
return is(val, "Boolean");
}
export function isRegExp(val: unknown): val is RegExp {
return is(val, "RegExp");
}
export function isArray(val: any): val is Array<any> {
return val && Array.isArray(val);
}
export function isWindow(val: any): val is Window {
return typeof window !== "undefined" && is(val, "Window");
}
export function isElement(val: unknown): val is Element {
return isObject(val) && !!val.tagName;
}
export const isServer = typeof window === "undefined";
export const isClient = !isServer;
/** url链接正则 */
export function isUrl<T>(value: T): boolean {
const reg =
// eslint-disable-next-line no-useless-escape
/(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
// @ts-expect-error
return reg.test(value);
}
/** 手机号码正则 */
export function isPhone<T>(value: T): boolean {
const reg =
/^[1](([3][0-9])|([4][0,1,4-9])|([5][0-3,5-9])|([6][2,5,6,7])|([7][0-8])|([8][0-9])|([9][0-3,5-9]))[0-9]{8}$/;
// @ts-expect-error
return reg.test(value);
}
/** 邮箱正则 */
export function isEmail<T>(value: T): boolean {
const reg = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;
// @ts-expect-error
return reg.test(value);
}

View File

@ -1,13 +0,0 @@
export const openLink = <T>(link: T): void => {
const $a: HTMLElement = document.createElement("a");
// @ts-expect-error
$a.setAttribute("href", link);
$a.setAttribute("target", "_blank");
$a.setAttribute("rel", "noreferrer noopener");
$a.setAttribute("id", "external");
document.getElementById("external") &&
document.body.removeChild(document.getElementById("external"));
document.body.appendChild($a);
$a.click();
$a.remove();
};

View File

@ -1,54 +0,0 @@
interface ProxyLoader {
loadCss(src: string): any;
loadScript(src: string): Promise<any>;
loadScriptConcurrent(src: Array<string>): Promise<any>;
}
class loaderProxy implements ProxyLoader {
constructor() {}
protected scriptLoaderCache: Array<string> = [];
public loadCss = (src: string): any => {
const element: HTMLLinkElement = document.createElement("link");
element.rel = "stylesheet";
element.href = src;
document.body.appendChild(element);
};
public loadScript = async (src: string): Promise<any> => {
if (this.scriptLoaderCache.includes(src)) {
return src;
} else {
const element: HTMLScriptElement = document.createElement("script");
element.src = src;
document.body.appendChild(element);
element.onload = () => {
return this.scriptLoaderCache.push(src);
};
}
};
public loadScriptConcurrent = async (
srcList: Array<string>
): Promise<any> => {
if (Array.isArray(srcList)) {
const len: number = srcList.length;
if (len > 0) {
let count = 0;
srcList.map(src => {
if (src) {
this.loadScript(src).then(() => {
count++;
if (count === len) {
return;
}
});
}
});
}
}
};
}
export const loader = new loaderProxy();

View File

@ -1,38 +0,0 @@
import { ElMessage } from "element-plus";
// 消息
const Message = (message: string): any => {
return ElMessage({
showClose: true,
message
});
};
// 成功
const successMessage = (message: string): any => {
return ElMessage({
showClose: true,
message,
type: "success"
});
};
// 警告
const warnMessage = (message: string): any => {
return ElMessage({
showClose: true,
message,
type: "warning"
});
};
// 失败
const errorMessage = (message: string): any => {
return ElMessage({
showClose: true,
message,
type: "error"
});
};
export { Message, successMessage, warnMessage, errorMessage };

View File

@ -11,7 +11,7 @@ type Events = {
openPanel: string;
tagViewsChange: string;
tagViewsShowModel: string;
logoChange: string;
logoChange: boolean;
changLayoutRoute: {
indexPath: string;
parentPath: string;

View File

@ -1,57 +0,0 @@
import type { FunctionArgs } from "@vueuse/core";
export const hasClass = (ele: RefType<any>, cls: string): any => {
return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)"));
};
export const addClass = (
ele: RefType<any>,
cls: string,
extracls?: string
): any => {
if (!hasClass(ele, cls)) ele.className += " " + cls;
if (extracls) {
if (!hasClass(ele, extracls)) ele.className += " " + extracls;
}
};
export const removeClass = (
ele: RefType<any>,
cls: string,
extracls?: string
): any => {
if (hasClass(ele, cls)) {
const reg = new RegExp("(\\s|^)" + cls + "(\\s|$)");
ele.className = ele.className.replace(reg, " ").trim();
}
if (extracls) {
if (hasClass(ele, extracls)) {
const regs = new RegExp("(\\s|^)" + extracls + "(\\s|$)");
ele.className = ele.className.replace(regs, " ").trim();
}
}
};
export const toggleClass = (
flag: boolean,
clsName: string,
target?: RefType<any>
): any => {
const targetEl = target || document.body;
let { className } = targetEl;
className = className.replace(clsName, "");
targetEl.className = flag ? `${className} ${clsName} ` : className;
};
export function useRafThrottle<T extends FunctionArgs>(fn: T): T {
let locked = false;
// @ts-ignore
return function (...args) {
if (locked) return;
locked = true;
window.requestAnimationFrame(() => {
fn.apply(this, args);
locked = false;
});
};
}

View File

@ -98,8 +98,7 @@ Print.prototype = {
const child = selects[k3].children;
for (const i in child) {
if (child[i].tagName == "OPTION") {
// @ts-ignore
if (child[i].selected == true) {
if ((child[i] as any).selected == true) {
child[i].setAttribute("selected", "selected");
} else {
child[i].removeAttribute("selected");

Some files were not shown because too many files have changed in this diff Show More