Compare commits

...

36 Commits

Author SHA1 Message Date
xiaoxian521
3701161022 release: update 3.1.0 2022-03-03 22:57:11 +08:00
xiaoxian521
e3898df731 fix: tags 2022-03-03 22:50:39 +08:00
xiaoxian521
32172220f7 fix: initRouter 2022-03-03 22:40:53 +08:00
xiaoxian521
fcdfe6d0c0 perf: menu 2022-03-03 09:27:53 +08:00
xiaoxian521
67bc933d5c fix: router is initialized first 2022-03-03 09:18:30 +08:00
xiaoxian521
feb1532549 fix: selector Component 2022-03-02 21:21:47 +08:00
xiaoxian521
1722ee7a9b perf: merge 2022-03-02 20:48:46 +08:00
xiaoxian521
710e119397 feat: add about page 2022-03-02 20:47:14 +08:00
Line
f21d9f38e5 fix: PureHttpResoponse => PureHttpResponse (#207)
Co-authored-by: yang.li12 <yang.li12@bluefocus.com>
2022-03-02 18:15:21 +08:00
xiaoxian521
727c0fe3c0 feat: print and perf style 2022-03-02 13:56:07 +08:00
一万
e3fda52801 feat: add iframe router 2022-03-02 11:15:51 +08:00
xiaoxian521
37762e45fd chore: update vite latest 2022-03-01 11:24:39 +08:00
啝裳
d43316f7c9 feat: watermark (#203)
* feat: add watermark
2022-03-01 10:44:26 +08:00
一万
6971ba6c53 fix: 跳转路由新增标签页 (#199)
* fix: 跳转路由新增标签页

* fix: 跳转路由新增标签页兼容

* perf: 删除取消watch监听
2022-02-28 22:08:56 +08:00
hexiaobang
a175cf9fe0 fix: 解决toggleClass函数多次运行后html元素的class中会遗留大量无效空格的问题 (#198) 2022-02-27 11:23:45 +08:00
xiaoxian521
9927e6f217 fix: router 2022-02-27 11:17:45 +08:00
xiaoxian521
bc300eab18 feat: 添加运行、打包信息, 使用lodash-unified替换lodash-es, lodash-unified支持ESM 2022-02-27 02:44:29 +08:00
xiaoxian521
3ca4729421 fix: delete routerView 2022-02-23 15:13:36 +08:00
烂柯人
dfaa76cc29 docs: update (#192) 2022-02-18 16:12:40 +08:00
xiaoxian521
7103b04283 fix: axios type 2022-02-18 14:54:16 +08:00
xiaoxian521
d2ddb49314 fix: axios post and get type 2022-02-18 14:51:58 +08:00
xiaoxian521
a4a042bfd7 fix: el-button plain model theme bug 2022-02-18 11:45:18 +08:00
xiaoxian521
a9a8115d46 perf: useRenderIcon hooks 2022-02-18 10:53:03 +08:00
huohuoit
3676014eb6 fix: 解决页面横向滚动条不显示问题 (#190)
Co-authored-by: Qingxuan001 <534794892@qqcom>
2022-02-17 18:13:09 +08:00
xiaoxian521
52910602ff fix: vxe-table icon not show 2022-02-16 11:23:20 +08:00
xiaoxian521
a2d1cf0e5f docs: update 2022-02-15 15:22:48 +08:00
xiaoxian521
8e886e83e7 ci: update 2022-02-15 15:05:58 +08:00
xiaoxian521
3e8dfb2bcb release: update 3.0 2022-02-14 23:19:32 +08:00
xiaoxian521
6fb0f7ef4d fix: mixNav route 2022-02-14 23:12:04 +08:00
xiaoxian521
08983fdbc1 fix: 固定vite@2.7.13,2.8+打包有问题 2022-02-14 22:51:36 +08:00
啝裳
ef05b2b614 feat: add mix nav (#187)
feat: add mix nav
2022-02-14 22:09:39 +08:00
xiaoxian521
a2dde02994 docs: update 2022-02-10 13:13:38 +08:00
rich1e
7544cf058e fix: epThemeColor error
Closes #183
2022-02-10 12:54:16 +08:00
xiaoxian521
43193fd2b6 docs: update 2022-02-10 12:01:36 +08:00
xiaoxian521
25c55c5d1d chore: update eslint@8.8.0 2022-02-07 15:59:04 +08:00
xiaoxian521
217258b972 chore: update element-plus@2.0.0 2022-02-07 13:43:54 +08:00
109 changed files with 3620 additions and 2153 deletions

View File

@@ -37,7 +37,7 @@ module.exports = {
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/prettier/@typescript-eslint"
"@vue/eslint-config-typescript"
],
parser: "vue-eslint-parser",
parserOptions: {
@@ -50,6 +50,10 @@ module.exports = {
}
},
rules: {
"vue/no-v-html": "off",
"vue/require-default-prop": "off",
"vue/require-explicit-emits": "off",
"vue/multi-word-component-names": "off",
"@typescript-eslint/no-explicit-any": "off", // any
"no-debugger": "off",
"@typescript-eslint/explicit-module-boundary-types": "off", // setup()

34
.github/workflows/preview.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: preview
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: "16"
registry-url: https://registry.npmjs.com/
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: latest
- name: run deploy.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm install && pnpm deploy

View File

@@ -1,7 +1,6 @@
module.exports = {
bracketSpacing: true,
jsxBracketSameLine: true,
singleQuote: false,
arrowParens: 'avoid',
trailingComma: 'none'
arrowParens: "avoid",
trailingComma: "none"
};

View File

@@ -1,3 +1,27 @@
# 3.1.0 (2022-3-3)
### 🎫 Feat
- iframe supports dynamic loading
- Watermark example
- Print examples (pictures, tables, echarts)
- Add running and packaging information, use `lodash-unified` to replace `lodash-es`, `lodash-unified` supports `ESM` and is compatible with `CJS`
### 🐞 Bug fixes
- Fixed jumping to another menu page alone in one menu page, the routing page jumped but the tab page was not displayed
- Fixed the route that returns dynamic level 3 and above in the background, and the menu does not correspond to the page
# 3.0 (2022-2-14)
### 🎫 Feat
- Added mix navigation
### 🐞 Bug fixes
- Fix tab page bug
# 2.9.0 (2022-2-5)
### 🎫 Feat

View File

@@ -1,3 +1,27 @@
# 3.1.0 (2022-3-3)
### 🎫 Feat
- iframe supports dynamic loading
- Watermark example
- Print examples (pictures, tables, echarts)
- Add running and packaging information, use `lodash-unified` to replace `lodash-es`, `lodash-unified` supports `ESM` and is compatible with `CJS`
### 🐞 Bug fixes
- Fixed jumping to another menu page alone in one menu page, the routing page jumped but the tab page was not displayed
- Fixed the route that returns dynamic level 3 and above in the background, and the menu does not correspond to the page
# 3.0 (2022-2-14)
### 🎫 Feat
- Added mix navigation
### 🐞 Bug fixes
- Fix tab page bug
# 2.9.0 (2022-2-5)
### 🎫 Feat

View File

@@ -1,3 +1,27 @@
# 3.1.0 (2022-3-3)
### 🎫 Feat
- iframe 支持动态加载
- 水印示例
- 打印示例图片、表格、echarts
- 添加运行、打包信息, 使用`lodash-unified`替换`lodash-es`,`lodash-unified`支持`ESM`同时兼容`CJS`
### 🐞 Bug fixes
- 修复在一个菜单页面内单独跳转到另一个菜单页面,路由页面跳转了但是标签页不显示的情况
- 修复后台返回动态三级及以上的路由,出现菜单与页面不对应的情况
# 3.0 (2022-2-14)
### 🎫 Feat
- 添加混合导航
### 🐞 Bug fixes
- 修复标签页 bug
# 2.9.0(2022-2-5)
### 🎫 Feat

View File

@@ -25,7 +25,7 @@ vue-pure-admin is a free and open source middle and back-end template. Using the
## Preview
- [vue-pure-admin](http://yiming_chang.gitee.io/manages)
- [vue-pure-admin](https://vue-pure-admin.vercel.app)
<p align="center">
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b4857fc7eb7d4c0f8deeefc644c1f7dd~tplv-k3u1fbpfcp-watermark.awebp?">
@@ -46,7 +46,7 @@ Open the project in Gitpod (free online dev environment for GitHub) and start co
```bash
git clone https://github.com/xiaoxian521/vue-pure-admin.git
or
git clone https://github.com.cnpmjs.org/xiaoxian521/vue-pure-admin.git
git clone https://gitee.com/yiming_chang/vue-pure-admin.git
```
- Installation dependencies
@@ -124,21 +124,15 @@ If you think this project is helpful to you, you can help the author buy a cup o
<img src="http://yiming_chang.gitee.io/manages/pay.jpg" width="150px" height="150px" />
## WeChat Exchange Group
For the better development of the project, you can choose to donate 10 yuan and add the following WeChat to pull you into the group. After adding, please consciously send a screenshot of the donation
<img src="http://yiming_chang.gitee.io/manages/kf.jpg" width="150px" height="195px" />
## License
In principle, no fees and copyrights are charged, so you can use it with confidence
In principle, no fees and copyrights are charged, and you can use it with confidence, but if you need secondary open source, please contact the author for permission!
[MIT © xiaoxian521-2020](./LICENSE)
## Backers
Thank you very much for your support, I believe the project will get better and better! ! ! :heart:
Thank you very much for your support, I believe the project will get better and better :heart:
| xueyuheng | taolei1990 | hang-kim | madwolfcrazy | limuen |
| :--------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------: |
@@ -146,6 +140,6 @@ Thank you very much for your support, I believe the project will get better and
## Contributors
This project exists thanks to all the people who contribute!!! :heart:
This project exists thanks to all the people who contribute :heart:
<a href="https://github.com/xiaoxian521/vue-pure-admin/graphs/contributors"><img src="https://contrib.rocks/image?repo=xiaoxian521/vue-pure-admin" /></a>

View File

@@ -25,7 +25,7 @@ vue-pure-admin 是一个免费开源的中后台模版。使用了最新的`vue3
## 预览
- [vue-pure-admin](http://yiming_chang.gitee.io/manages)
- [vue-pure-admin](https://vue-pure-admin.vercel.app)
<p align="center">
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b4857fc7eb7d4c0f8deeefc644c1f7dd~tplv-k3u1fbpfcp-watermark.awebp?">
@@ -46,7 +46,7 @@ vue-pure-admin 是一个免费开源的中后台模版。使用了最新的`vue3
```bash
git clone https://github.com/xiaoxian521/vue-pure-admin.git
or
git clone https://github.com.cnpmjs.org/xiaoxian521/vue-pure-admin.git
git clone https://gitee.com/yiming_chang/vue-pure-admin.git
```
- 安装依赖
@@ -124,21 +124,21 @@ pnpm build
<img src="http://yiming_chang.gitee.io/manages/pay.jpg" width="150px" height="150px" />
## 微信交流群
## QQ 交流群
为了项目更好的发展,你可选择捐赠 10 元后添加下图微信拉你进群,添加后请自觉发捐赠截图
群里严禁`黄``赌``毒``vpn`等违法行为!
<img src="http://yiming_chang.gitee.io/manages/kf.jpg" width="150px" height="195px" />
<img src="http://yiming_chang.gitee.io/manages/qq.jpg" width="150px" height="225px" />
## 许可证
原则上不收取任何费用及版权,可以放心使用
原则上不收取任何费用及版权,可以放心使用,不过如需二次开源(比如用此平台二次开发并开源)请联系作者获取许可!
[MIT © xiaoxian521-2020](./LICENSE)
## 捐赠者
非常感谢你们的支持,相信项目会越来越好:heart:
非常感谢你们的支持,相信项目会越来越好 :heart:
| xueyuheng | taolei1990 | hang-kim | madwolfcrazy | limuen |
| :--------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------: |
@@ -146,6 +146,6 @@ pnpm build
## 贡献者
这个项目的存在感谢所有做出贡献的人:heart:
这个项目的存在感谢所有做出贡献的人 :heart:
<a href="https://github.com/xiaoxian521/vue-pure-admin/graphs/contributors"><img src="https://contrib.rocks/image?repo=xiaoxian521/vue-pure-admin" /></a>

85
build/info.ts Normal file
View File

@@ -0,0 +1,85 @@
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";
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;
let endTime: Dayjs;
return {
name: "vite:buildInfo",
configResolved(resolvedConfig: { command: string }) {
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") {
endTime = dayjs(new Date());
recursiveDirectory(staticPath, () => {
console.log(
bold(
green(
`恭喜打包完成🎉(总用时${dayjs
.duration(endTime.diff(startTime))
.format("mm分ss秒")},打包后的大小为${formatBytes(
sum(fileListTotal)
)}`
)
)
);
});
}
}
};
}

View File

@@ -1,4 +1,5 @@
import vue from "@vitejs/plugin-vue";
import { viteBuildInfo } from "./info";
import svgLoader from "vite-svg-loader";
import legacy from "@vitejs/plugin-legacy";
import vueJsx from "@vitejs/plugin-vue-jsx";
@@ -21,6 +22,7 @@ export function getPluginsList(command, VITE_LEGACY) {
WindiCSS(),
// 线上环境删除console
removeConsole(),
viteBuildInfo(),
// 修改layout文件夹下的文件时自动重载浏览器 解决 https://github.com/xiaoxian521/vue-pure-admin/issues/170
liveReload(["src/layout/**/*", "src/router/**/*"]),
// 自定义主题

30
deploy.sh Normal file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env sh
# Replace packaging path
sed -i "s#VITE_PUBLIC_PATH = /#VITE_PUBLIC_PATH = /vue-pure-admin/#g" $(pwd)/.env.production
# Make sure the script throws the error encountered
set -e
pnpm build
cd dist
touch README.md .nojekyll
# deploy to github
if [ -z "$GITHUB_TOKEN" ]; then
msg='deploy'
githubUrl=git@github.com:xiaoxian521/vue-pure-admin.git
else
msg='ci: Automatic deployment from github actions'
githubUrl=https://xiaoxian521:${GITHUB_TOKEN}@github.com/xiaoxian521/vue-pure-admin.git
git config --global user.name "xiaoxian521"
git config --global user.email "1923740402@qq.com"
fi
git init
git add -A
git commit -m "${msg}"
# Push to github gh-pages branch
git push -f $githubUrl master:gh-pages
cd -
rm -rf dist

View File

@@ -14,20 +14,16 @@
<body>
<div id="app">
<style>
* {
margin: 0;
padding: 0;
}
html,
body {
body,
#app {
width: 100%;
height: 100%;
display: flex;
position: relative;
justify-content: center;
align-items: center;
overflow: hidden;
font-family: "Reggae One", cursive;
}
.loader,
@@ -53,6 +49,8 @@
transform: translateZ(0);
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
top: 0;
transform: translate(-50%, 0);
}
.loader:before,
@@ -96,7 +94,7 @@
}
}
</style>
<div class="loader">Loading...</div>
<div class="loader"></div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>

View File

@@ -41,7 +41,7 @@ const permissionRouter = {
title: "menus.permission",
icon: "lollipop",
i18n: true,
rank: 3
rank: 7
},
children: [
{
@@ -64,6 +64,46 @@ const permissionRouter = {
]
};
const frameRouter = {
path: "/iframe",
name: "reFrame",
redirect: "/iframe/pure",
meta: {
icon: "monitor",
title: "menus.hsExternalPage",
i18n: true,
rank: 10
},
children: [
{
path: "/iframe/pure",
name: "reFramePure",
meta: {
i18n: true,
title: "menus.hsPureDocument",
frameSrc: "https://pure-admin-doc.vercel.app"
}
},
{
path: "/external",
name: "https://pure-admin-doc.vercel.app",
meta: {
title: "menus.externalLink",
i18n: true
}
},
{
path: "/iframe/ep",
name: "reFrameEp",
meta: {
i18n: true,
title: "menus.hsEpDocument",
frameSrc: "https://element-plus.gitee.io/zh-CN/"
}
}
]
};
const tabsRouter = {
path: "/tabs",
name: "reTabs",
@@ -72,7 +112,7 @@ const tabsRouter = {
icon: "IF-team-icontabs",
title: "menus.hstabs",
i18n: true,
rank: 8
rank: 11
},
children: [
{
@@ -113,6 +153,7 @@ export default [
code: 0,
info: [
tabsRouter,
frameRouter,
systemRouter,
setDifAuthority("v-admin", permissionRouter)
]

View File

@@ -1,16 +1,13 @@
{
"name": "vue-pure-admin",
"version": "2.9.0",
"version": "3.1.0",
"private": true,
"engines": {
"node": ">= 16",
"pnpm": ">= 6"
},
"scripts": {
"dev": "cross-env --max_old_space_size=4096 vite",
"serve": "pnpm dev",
"build": "rimraf dist && cross-env vite build",
"report": "rimraf dist && cross-env vite build",
"deploy": "bash deploy.sh",
"preview": "vite preview",
"preview:build": "pnpm build && vite preview",
"clean:cache": "rm -rf node_modules && rm -rf .eslintcache && pnpm install",
@@ -33,20 +30,21 @@
"@ctrl/tinycolor": "^3.4.0",
"@logicflow/core": "0.7.1",
"@logicflow/extension": "0.7.1",
"@vueuse/core": "^7.5.5",
"@vueuse/core": "^7.6.2",
"@vueuse/motion": "^2.0.0-beta.9",
"@vueuse/shared": "^7.5.5",
"@vueuse/shared": "^7.6.2",
"animate.css": "^4.1.1",
"axios": "^0.25.0",
"cropperjs": "^1.5.11",
"css-color-function": "^1.3.3",
"dayjs": "^1.10.7",
"driver.js": "^0.9.8",
"echarts": "^5.2.1",
"element-plus": "1.3.0-beta.1",
"echarts": "^5.3.0",
"element-plus": "^2.0.3",
"element-resize-detector": "^1.2.3",
"js-cookie": "^3.0.1",
"lodash-es": "^4.17.21",
"lodash-unified": "^1.0.2",
"mitt": "^3.0.0",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
@@ -57,13 +55,13 @@
"responsive-storage": "^1.0.11",
"rgb-hex": "^4.0.0",
"v-contextmenu": "3.0.0",
"vue": "^3.2.29",
"vue": "^3.2.31",
"vue-i18n": "^9.2.0-beta.30",
"vue-json-pretty": "^2.0.2",
"vue-router": "^4.0.12",
"vue-router": "^4.0.13",
"vue-types": "^4.1.1",
"vuedraggable": "4.1.0",
"vxe-table": "^4.1.18",
"vxe-table": "^4.1.21",
"wangeditor": "^4.7.9",
"xe-utils": "^3.5.2",
"xgplayer": "2.28.0"
@@ -78,48 +76,55 @@
"@iconify/vue": "^3.1.3",
"@types/element-resize-detector": "1.1.3",
"@types/js-cookie": "^3.0.1",
"@types/lodash-es": "^4.17.6",
"@types/mockjs": "1.0.3",
"@types/node": "14.14.14",
"@types/nprogress": "0.2.0",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "4.31.0",
"@typescript-eslint/parser": "4.31.0",
"@vitejs/plugin-legacy": "^1.6.4",
"@vitejs/plugin-vue": "^2.1.0",
"@vitejs/plugin-vue-jsx": "^1.3.3",
"@vue/eslint-config-prettier": "6.0.0",
"@vue/eslint-config-typescript": "7.0.0",
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/parser": "^5.10.2",
"@vitejs/plugin-legacy": "^1.7.1",
"@vitejs/plugin-vue": "^2.2.4",
"@vitejs/plugin-vue-jsx": "^1.3.8",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^10.0.0",
"@zougt/vite-plugin-theme-preprocessor": "^1.4.4",
"autoprefixer": "^10.4.2",
"cross-env": "7.0.3",
"eslint": "7.30.0",
"eslint-plugin-prettier": "3.4.0",
"eslint-plugin-vue": "7.17.0",
"husky": "7.0.2",
"eslint": "^8.8.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.4.1",
"font-awesome": "^4.7.0",
"husky": "^7.0.4",
"lint-staged": "11.1.2",
"postcss": "8.2.6",
"picocolors": "^1.0.0",
"postcss": "^8.4.6",
"postcss-html": "^1.3.0",
"postcss-import": "14.0.0",
"prettier": "2.3.2",
"postcss-scss": "^4.0.3",
"prettier": "^2.5.1",
"pretty-quick": "3.1.1",
"rimraf": "3.0.2",
"rollup-plugin-visualizer": "^5.5.4",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "^1.49.7",
"sass-loader": "^12.4.0",
"stylelint": "13.13.1",
"stylelint-config-prettier": "8.0.2",
"stylelint-config-standard": "22.0.0",
"stylelint-order": "4.1.0",
"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.5.5",
"unplugin-element-plus": "^0.2.0",
"vite": "^2.7.13",
"vite": "^2.8.6",
"vite-plugin-live-reload": "^2.1.0",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-remove-console": "^0.0.6",
"vite-plugin-style-import": "^1.4.1",
"vite-plugin-windicss": "^1.6.1",
"vite-plugin-style-import": "1.4.1",
"vite-plugin-windicss": "^1.8.2",
"vite-svg-loader": "2.2.0",
"vue-eslint-parser": "7.10.0",
"windicss": "^3.4.3"
"vue-eslint-parser": "^8.2.0",
"windicss": "^3.5.1"
},
"repository": "git@github.com:xiaoxian521/vue-pure-admin.git",
"author": "xiaoxian521",

2252
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -169,7 +169,8 @@ export default defineComponent({
style={{
color: props.color,
fontSize: props.fontSize
}}>
}}
>
{state.displayValue}
</span>
</>

View File

@@ -43,7 +43,8 @@ export default defineComponent({
<div
class="scroll-num"
// @ts-ignore
style={{ "--i": props.i, "--delay": props.delay }}>
style={{ "--i": props.i, "--delay": props.delay }}
>
<ul ref="ul" style={{ fontSize: "32px" }}>
<li>0</li>
<li>1</li>

View File

@@ -133,7 +133,8 @@ export default defineComponent({
<>
<div
class={useAttrs({ excludeListeners: true, excludeKeys: ["class"] })}
style={this.getWrapperStyle}>
style={this.getWrapperStyle}
>
<img
ref="imgElRef"
src={this.props.src}

View File

@@ -9,7 +9,7 @@
background: #fff;
font-size: 66px;
color: #fff;
box-shadow: 0 0 6px rgba(0, 0, 0, 0.5);
box-shadow: 0 0 6px rgb(0 0 0 / 50%);
text-align: center;
font-family: "Helvetica Neue";
}
@@ -58,7 +58,7 @@
.m-flipper.down.go .front::before {
transform-origin: 50% 100%;
animation: frontFlipDown 0.6s ease-in-out both;
box-shadow: 0 -2px 6px rgba(255, 255, 255, 0.3);
box-shadow: 0 -2px 6px rgb(255 255 255 / 30%);
backface-visibility: hidden;
}
@@ -85,7 +85,7 @@
.m-flipper.up.go .front::after {
transform-origin: 50% 0;
animation: frontFlipUp 0.6s ease-in-out both;
box-shadow: 0 2px 6px rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 6px rgb(255 255 255 / 30%);
backface-visibility: hidden;
}

View File

@@ -126,7 +126,7 @@ onMounted(() => {
}
.iconfont {
font-size: 25px;
font-size: 18px;
}
.control-container p {

View File

@@ -1,8 +1,14 @@
import { iconType } from "./types";
import { h, defineComponent, Component } from "vue";
import { IconifyIconOffline, FontIcon } from "../index";
// 支持fontawesome4、5+、iconfont、remixicon、element-plus的icons、自定义svg
export function useRenderIcon(icon: string): Component {
/**
* 支持fontawesome4、5+、iconfont、remixicon、element-plus的icons、自定义svg
* @param icon 必传 string 图标
* @param attrs 可选 iconType 属性
* @returns Component
*/
export function useRenderIcon(icon: string, attrs?: iconType): Component {
// iconfont
const ifReg = /^IF-/;
// typeof icon === "function" 属于SVG
@@ -19,7 +25,8 @@ export function useRenderIcon(icon: string): Component {
render() {
return h(FontIcon, {
icon: iconName,
iconType
iconType,
...attrs
});
}
});
@@ -31,7 +38,8 @@ export function useRenderIcon(icon: string): Component {
name: "Icon",
render() {
return h(IconifyIconOffline, {
icon: icon
icon: icon,
...attrs
});
}
});

View File

@@ -24,6 +24,9 @@ import Location from "@iconify-icons/ep/location";
import Tickets from "@iconify-icons/ep/tickets";
import OfficeBuilding from "@iconify-icons/ep/office-building";
import Notebook from "@iconify-icons/ep/notebook";
import Rank from "@iconify-icons/ep/rank";
import videoPlay from "@iconify-icons/ep/video-play";
import Monitor from "@iconify-icons/ep/monitor";
addIcon("check", Check);
addIcon("menu", Menu);
addIcon("home-filled", HomeFilled);
@@ -46,16 +49,23 @@ addIcon("location", Location);
addIcon("tickets", Tickets);
addIcon("office-building", OfficeBuilding);
addIcon("notebook", Notebook);
addIcon("video-play", videoPlay);
addIcon("rank", Rank);
addIcon("monitor", Monitor);
// remixicon
import arrowRightSLine from "@iconify-icons/ri/arrow-right-s-line";
import arrowLeftSLine from "@iconify-icons/ri/arrow-left-s-line";
import logoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import nodeTree from "@iconify-icons/ri/node-tree";
import ubuntuFill from "@iconify-icons/ri/ubuntu-fill";
import questionLine from "@iconify-icons/ri/question-line";
addIcon("arrow-right-s-line", arrowRightSLine);
addIcon("arrow-left-s-line", arrowLeftSLine);
addIcon("logout-circle-r-line", logoutCircleRLine);
addIcon("node-tree", nodeTree);
addIcon("ubuntu-fill", ubuntuFill);
addIcon("question-line", questionLine);
// Font Awesome 4
import faUser from "@iconify-icons/fa/user";

View File

@@ -0,0 +1,18 @@
export interface iconType {
// iconify (https://docs.iconify.design/icon-components/vue/#properties)
inline?: boolean;
width?: string | number;
height?: string | number;
horizontalFlip?: boolean;
verticalFlip?: boolean;
flip?: string;
rotate?: number | string;
color?: string;
horizontalAlign?: boolean;
verticalAlign?: boolean;
align?: string;
onLoad?: Function;
// all icon
style?: object;
}

View File

@@ -19,10 +19,6 @@ type resultType = {
info: Array<undefined>;
};
export interface mapInter {
loading: boolean;
}
let MarkerCluster;
let map: MapConfigureInter;

View File

@@ -299,10 +299,12 @@ export default defineComponent({
cursor: unref(rateDisabled) ? "auto" : "pointer",
textAlign: "center"
}}
key={key}>
key={key}
>
<div
ref={`hsdiv${props.HsKey}${key}`}
class={`hs-item ${[unref(classes)[key] + key]}`}>
class={`hs-item ${[unref(classes)[key] + key]}`}
>
<span>{item}</span>
</div>
</td>

View File

@@ -111,20 +111,24 @@ export default defineComponent({
class="vue-splitter-container clearfix"
style={(unref(cursor), unref(userSelect))}
onMouseup={() => onMouseUp()}
onMousemove={() => onMouseMove(event)}>
onMousemove={() => onMouseMove(event)}
>
<div
class={unref(leftClass)}
style={{ [unref(type)]: unref(percent) + "%" }}>
style={{ [unref(type)]: unref(percent) + "%" }}
>
{ctx.slots.paneL()}
</div>
<resizer
style={`${unref([resizeType])}:${unref(percent)}%`}
split={props.splitSet?.split}
onMousedown={() => onMouseDown()}
onClick={() => onClick()}></resizer>
onClick={() => onClick()}
></resizer>
<div
class={unref(rightClass)}
style={{ [unref(type)]: 100 - unref(percent) + "%" }}>
style={{ [unref(type)]: 100 - unref(percent) + "%" }}
>
{ctx.slots.paneR()}
</div>
<div v-show={unref(active)} class="vue-splitter-container-mask"></div>

View File

@@ -1,21 +1,18 @@
.splitter-pane-resizer {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
background: #000;
position: absolute;
opacity: 0.2;
z-index: 1;
-moz-background-clip: padding;
-webkit-background-clip: padding;
background-clip: padding;
background-clip: padding-box;
}
.splitter-pane-resizer.horizontal {
height: 11px;
margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
border-top: 5px solid rgb(255 255 255 / 0%);
border-bottom: 5px solid rgb(255 255 255 / 0%);
cursor: row-resize;
width: 100%;
}
@@ -24,7 +21,7 @@
width: 11px;
height: 100%;
margin-left: -5px;
border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0);
border-left: 5px solid rgb(255 255 255 / 0%);
border-right: 5px solid rgb(255 255 255 / 0%);
cursor: col-resize;
}

View File

@@ -7,7 +7,6 @@ import {
defineComponent,
getCurrentInstance
} from "vue";
import { RouterView } from "vue-router";
import backTop from "/@/assets/svg/back_top.svg?component";
import { usePermissionStoreHook } from "/@/store/modules/permission";

View File

@@ -1,71 +1,43 @@
<script setup lang="ts">
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { emitter } from "/@/utils/mitt";
import { useNav } from "../hooks/nav";
import { useRoute } from "vue-router";
import Notice from "./notice/index.vue";
import mixNav from "./sidebar/mixNav.vue";
import avatars from "/@/assets/avatars.jpg";
import { transformI18n } from "/@/plugins/i18n";
import Hamburger from "./sidebar/hamBurger.vue";
import { useRouter, useRoute } from "vue-router";
import { storageSession } from "/@/utils/storage";
import { watch, getCurrentInstance } from "vue";
import Breadcrumb from "./sidebar/breadCrumb.vue";
import { useAppStoreHook } from "/@/store/modules/app";
import { unref, watch, getCurrentInstance } from "vue";
import { deviceDetection } from "/@/utils/deviceDetection";
import screenfull from "../components/screenfull/index.vue";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import globalization from "/@/assets/svg/globalization.svg?component";
const route = useRoute();
const { locale } = useI18n();
const instance =
getCurrentInstance().appContext.config.globalProperties.$storage;
const pureApp = useAppStoreHook();
const router = useRouter();
const route = useRoute();
let usename = storageSession.getItem("info")?.username;
const { locale } = useI18n();
const getDropdownItemStyle = computed(() => {
return t => {
return {
background: locale.value === t ? useEpThemeStoreHook().epThemeColor : "",
color: locale.value === t ? "#f4f4f5" : "#000"
};
};
});
const {
logout,
onPanel,
changeTitle,
toggleSideBar,
pureApp,
usename,
getDropdownItemStyle
} = useNav();
watch(
() => locale.value,
() => {
//@ts-ignore
document.title = transformI18n(
//@ts-ignore
unref(route.meta.title),
unref(route.meta.i18n)
); // 动态title
changeTitle(route.meta);
}
);
// 退出登录
const logout = (): void => {
storageSession.removeItem("info");
router.push("/login");
};
function onPanel() {
emitter.emit("openPanel");
}
function toggleSideBar() {
pureApp.toggleSideBar();
}
// 简体中文
function translationCh() {
instance.locale = { locale: "zh" };
locale.value = "zh";
}
// English
function translationEn() {
instance.locale = { locale: "en" };
locale.value = "en";
@@ -75,14 +47,17 @@ function translationEn() {
<template>
<div class="navbar">
<Hamburger
v-if="pureApp.layout !== 'mix'"
:is-active="pureApp.sidebar.opened"
class="hamburger-container"
@toggleClick="toggleSideBar"
/>
<Breadcrumb class="breadcrumb-container" />
<Breadcrumb v-if="pureApp.layout !== 'mix'" class="breadcrumb-container" />
<div class="vertical-header-right">
<mixNav v-if="pureApp.layout === 'mix'" />
<div v-if="pureApp.layout === 'vertical'" class="vertical-header-right">
<!-- 通知 -->
<Notice id="header-notice" />
<!-- 全屏 -->
@@ -93,7 +68,7 @@ function translationEn() {
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle('zh')"
:style="getDropdownItemStyle(locale, 'zh')"
@click="translationCh"
><IconifyIconOffline
class="check-zh"
@@ -102,7 +77,7 @@ function translationEn() {
/>简体中文</el-dropdown-item
>
<el-dropdown-item
:style="getDropdownItemStyle('en')"
:style="getDropdownItemStyle(locale, 'en')"
@click="translationEn"
><el-icon class="check-en" v-show="locale === 'en'"
><IconifyIconOffline icon="check" /></el-icon
@@ -234,14 +209,8 @@ function translationEn() {
}
.translation {
.el-dropdown-menu__item {
padding: 5px 40px !important;
}
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
::v-deep(.el-dropdown-menu__item) {
padding: 5px 40px;
}
.check-zh {
@@ -258,16 +227,10 @@ function translationEn() {
.logout {
max-width: 120px;
.el-dropdown-menu__item {
::v-deep(.el-dropdown-menu__item) {
min-width: 100%;
display: inline-flex;
flex-wrap: wrap;
}
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
}
}
</style>

View File

@@ -125,7 +125,7 @@ emitter.on("openPanel", () => {
&:hover {
cursor: pointer;
color: red;
color: var(--el-color-primary);
}
}
}

View File

@@ -10,7 +10,7 @@ import {
getCurrentInstance
} from "vue";
import rgbHex from "rgb-hex";
import { find } from "lodash-es";
import { find } from "lodash-unified";
import { getConfig } from "/@/config";
import { useRouter } from "vue-router";
import panel from "../panel/index.vue";
@@ -61,6 +61,7 @@ let themeColors = ref<Array<themeColorsType>>([
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")) ||
@@ -101,7 +102,7 @@ const getThemeColorStyle = computed(() => {
};
});
function changeStorageConfigure(key, val) {
function storageConfigureChange<T>(key: string, val: T): void {
const storageConfigure = instance.configure;
storageConfigure[key] = val;
instance.configure = storageConfigure;
@@ -110,14 +111,14 @@ function changeStorageConfigure(key, val) {
function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
const targetEl = target || document.body;
let { className } = targetEl;
className = className.replace(clsName, "");
className = className.replace(clsName, "").trim();
targetEl.className = flag ? `${className} ${clsName} ` : className;
}
// 灰色模式设置
const greyChange = (value): void => {
toggleClass(settings.greyVal, "html-grey", document.querySelector("html"));
changeStorageConfigure("grey", value);
storageConfigureChange("grey", value);
};
// 色弱模式设置
@@ -127,29 +128,30 @@ const weekChange = (value): void => {
"html-weakness",
document.querySelector("html")
);
changeStorageConfigure("weak", value);
storageConfigureChange("weak", value);
};
const tagsChange = () => {
let showVal = settings.tabsVal;
changeStorageConfigure("hideTabs", showVal);
storageConfigureChange("hideTabs", showVal);
emitter.emit("tagViewsChange", showVal);
};
const multiTagsCacheChange = () => {
let multiTagsCache = settings.multiTagsCache;
changeStorageConfigure("multiTagsCache", multiTagsCache);
storageConfigureChange("multiTagsCache", multiTagsCache);
useMultiTagsStoreHook().multiTagsCacheChange(multiTagsCache);
};
// 清空缓存并返回登录页
function onReset() {
toggleClass(getConfig().Grey, "html-grey", document.querySelector("html"));
toggleClass(
getConfig().Weak,
"html-weakness",
document.querySelector("html")
);
router.push("/login");
const { Grey, Weak, MultiTagsCache, EpThemeColor, Layout } = getConfig();
useAppStoreHook().setLayout(Layout);
useEpThemeStoreHook().setEpThemeColor(EpThemeColor);
useMultiTagsStoreHook().multiTagsCacheChange(MultiTagsCache);
toggleClass(Grey, "html-grey", document.querySelector("html"));
toggleClass(Weak, "html-weakness", document.querySelector("html"));
useMultiTagsStoreHook().handleTags("equal", [
{
path: "/welcome",
@@ -161,23 +163,20 @@ function onReset() {
}
}
]);
useMultiTagsStoreHook().multiTagsCacheChange(getConfig().MultiTagsCache);
useEpThemeStoreHook().setEpThemeColor(getConfig().EpThemeColor);
storageLocal.clear();
storageSession.clear();
router.push("/login");
}
function onChange(label) {
changeStorageConfigure("showModel", label);
storageConfigureChange("showModel", label);
emitter.emit("tagViewsShowModel", label);
}
// 侧边栏Logo
function logoChange() {
unref(logoVal)
? changeStorageConfigure("showLogo", true)
: changeStorageConfigure("showLogo", false);
? storageConfigureChange("showLogo", true)
: storageConfigureChange("showLogo", false);
emitter.emit("logoChange", unref(logoVal));
}
@@ -192,10 +191,17 @@ watch(instance, ({ layout }) => {
case "vertical":
toggleClass(true, isSelect, unref(verticalRef));
debounce(setFalse([horizontalRef]), 50);
debounce(setFalse([mixRef]), 50);
break;
case "horizontal":
toggleClass(true, isSelect, unref(horizontalRef));
debounce(setFalse([verticalRef]), 50);
debounce(setFalse([mixRef]), 50);
break;
case "mix":
toggleClass(true, isSelect, unref(mixRef));
debounce(setFalse([verticalRef]), 50);
debounce(setFalse([horizontalRef]), 50);
break;
}
});
@@ -315,7 +321,7 @@ nextTick(() => {
<el-divider>导航栏模式</el-divider>
<ul class="pure-theme">
<el-tooltip class="item" content="左侧菜单模式" placement="bottom">
<el-tooltip class="item" content="左侧模式" placement="bottom">
<li
:class="layoutTheme.layout === 'vertical' ? $style.isSelect : ''"
ref="verticalRef"
@@ -326,7 +332,7 @@ nextTick(() => {
</li>
</el-tooltip>
<el-tooltip class="item" content="顶部菜单模式" placement="bottom">
<el-tooltip class="item" content="顶部模式" placement="bottom">
<li
:class="layoutTheme.layout === 'horizontal' ? $style.isSelect : ''"
ref="horizontalRef"
@@ -336,6 +342,17 @@ nextTick(() => {
<div></div>
</li>
</el-tooltip>
<el-tooltip class="item" content="混合模式" placement="bottom">
<li
:class="layoutTheme.layout === 'mix' ? $style.isSelect : ''"
ref="mixRef"
@click="setLayoutModel('mix')"
>
<div></div>
<div></div>
</li>
</el-tooltip>
</ul>
<el-divider v-show="!dataTheme">主题色</el-divider>
@@ -481,15 +498,14 @@ nextTick(() => {
.pure-theme {
margin-top: 25px;
width: 100%;
height: 100px;
height: 50px;
display: flex;
flex-wrap: wrap;
justify-content: space-around;
li {
margin: 10px;
width: 36%;
height: 70px;
width: 18%;
height: 45px;
background: #f0f2f5;
position: relative;
overflow: hidden;
@@ -527,6 +543,27 @@ nextTick(() => {
}
}
}
&:nth-child(3) {
div {
&:nth-child(1) {
width: 100%;
height: 30%;
background: #1b2a47;
box-shadow: 0 0 1px #888;
}
&:nth-child(2) {
width: 30%;
height: 70%;
bottom: 0;
left: 0;
background: #fff;
box-shadow: 0 0 1px #888;
position: absolute;
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { isEqual } from "lodash-es";
import { isEqual } from "lodash-unified";
import { transformI18n } from "/@/plugins/i18n";
import { getParentPaths, findRouteByPath } from "/@/router/utils";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";

View File

@@ -1,126 +1,68 @@
<script setup lang="ts">
import {
computed,
unref,
watch,
nextTick,
onMounted,
getCurrentInstance
} from "vue";
import { useI18n } from "vue-i18n";
import { emitter } from "/@/utils/mitt";
import { useNav } from "../../hooks/nav";
import Notice from "../notice/index.vue";
import { templateRef } from "@vueuse/core";
import SidebarItem from "./sidebarItem.vue";
import avatars from "/@/assets/avatars.jpg";
import screenfull from "../screenfull/index.vue";
import { useRoute, useRouter } from "vue-router";
import { storageSession } from "/@/utils/storage";
import { deviceDetection } from "/@/utils/deviceDetection";
import { watch, nextTick, onMounted, getCurrentInstance } from "vue";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import globalization from "/@/assets/svg/globalization.svg?component";
const route = useRoute();
const { locale } = 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 = templateRef<ElRef | null>("menu", null);
const route = useRoute();
const router = useRouter();
const routers = useRouter().options.routes;
let usename = storageSession.getItem("info")?.username;
const { locale, t } = useI18n();
const {
logout,
backHome,
onPanel,
changeTitle,
handleResize,
menuSelect,
usename,
getDropdownItemStyle
} = useNav();
const getDropdownItemStyle = computed(() => {
return t => {
return {
background: locale.value === t ? "#1b2a47" : "",
color: locale.value === t ? "#f4f4f5" : "#000"
};
};
onMounted(() => {
nextTick(() => {
handleResize(menuRef.value);
});
});
watch(
() => locale.value,
() => {
//@ts-ignore
// 动态title
document.title = t(unref(route.meta.title));
changeTitle(route.meta);
}
);
// 退出登录
const logout = (): void => {
storageSession.removeItem("info");
router.push("/login");
};
function onPanel() {
emitter.emit("openPanel");
}
const activeMenu = computed((): string => {
const { meta, path } = route;
if (meta.activeMenu) {
// @ts-ignore
return meta.activeMenu;
watch(
() => route.path,
() => {
menuSelect(route.path, routers);
}
return path;
});
);
const menuSelect = (indexPath: string): void => {
let parentPath = "";
let parentPathIndex = indexPath.lastIndexOf("/");
if (parentPathIndex > 0) {
parentPath = indexPath.slice(0, parentPathIndex);
}
// 找到当前路由的信息
function findCurrentRoute(routes) {
return routes.map(item => {
if (item.path === indexPath) {
// 切换左侧菜单 通知标签页
emitter.emit("changLayoutRoute", {
indexPath,
parentPath
});
} else {
if (item.children) findCurrentRoute(item.children);
}
});
}
findCurrentRoute(routers);
};
function backHome() {
router.push("/welcome");
}
function handleResize() {
// @ts-ignore
menuRef.value.handleResize();
}
// 简体中文
function translationCh() {
instance.locale = { locale: "zh" };
locale.value = "zh";
handleResize();
handleResize(menuRef.value);
}
// English
function translationEn() {
instance.locale = { locale: "en" };
locale.value = "en";
handleResize();
handleResize(menuRef.value);
}
onMounted(() => {
nextTick(() => {
handleResize();
});
});
</script>
<template>
@@ -135,12 +77,11 @@ onMounted(() => {
</div>
<el-menu
ref="menu"
:default-active="activeMenu"
unique-opened
router
class="horizontal-header-menu"
mode="horizontal"
@select="menuSelect"
:default-active="route.path"
router
@select="indexPath => menuSelect(indexPath, routers)"
>
<sidebar-item
v-for="route in usePermissionStoreHook().wholeMenus"
@@ -160,14 +101,14 @@ onMounted(() => {
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle('zh')"
:style="getDropdownItemStyle(locale, 'zh')"
@click="translationCh"
><el-icon class="check-zh" v-show="locale === 'zh'"
><IconifyIconOffline icon="check" /></el-icon
>简体中文</el-dropdown-item
>
<el-dropdown-item
:style="getDropdownItemStyle('en')"
:style="getDropdownItemStyle(locale, 'en')"
@click="translationEn"
><el-icon class="check-en" v-show="locale === 'en'"
><IconifyIconOffline icon="check" /></el-icon
@@ -207,14 +148,8 @@ onMounted(() => {
<style lang="scss" scoped>
.translation {
.el-dropdown-menu__item {
padding: 5px 40px !important;
}
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
::v-deep(.el-dropdown-menu__item) {
padding: 5px 40px;
}
.check-zh {
@@ -231,16 +166,10 @@ onMounted(() => {
.logout {
max-width: 120px;
.el-dropdown-menu__item {
::v-deep(.el-dropdown-menu__item) {
min-width: 100%;
display: inline-flex;
flex-wrap: wrap;
}
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
}
}
</style>

View File

@@ -0,0 +1,239 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import Notice from "../notice/index.vue";
import { useNav } from "../../hooks/nav";
import { templateRef } from "@vueuse/core";
import avatars from "/@/assets/avatars.jpg";
import { transformI18n } from "/@/plugins/i18n";
import screenfull from "../screenfull/index.vue";
import { useRoute, useRouter } from "vue-router";
import { deviceDetection } from "/@/utils/deviceDetection";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import { getParentPaths, findRouteByPath } from "/@/router/utils";
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 } = useI18n();
const routers = useRouter().options.routes;
const menuRef = templateRef<ElRef | null>("menu", null);
const instance =
getCurrentInstance().appContext.config.globalProperties.$storage;
const {
logout,
onPanel,
changeTitle,
toggleSideBar,
handleResize,
menuSelect,
resolvePath,
pureApp,
usename,
getDropdownItemStyle
} = useNav();
let defaultActive = ref(null);
function getDefaultActive(routePath) {
const wholeMenus = usePermissionStoreHook().wholeMenus;
// 当前路由的父级路径
const parentRoutes = getParentPaths(routePath, wholeMenus)[0];
defaultActive.value = findRouteByPath(
parentRoutes,
wholeMenus
)?.children[0]?.path;
}
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>
<el-menu
ref="menu"
class="horizontal-header-menu"
mode="horizontal"
:default-active="defaultActive"
router
@select="indexPath => menuSelect(indexPath, routers)"
>
<el-menu-item
v-for="route in usePermissionStoreHook().wholeMenus"
:key="route.path"
:index="resolvePath(route) || route.redirect"
>
<template #title>
<el-icon v-show="route.meta.icon" :class="route.meta.icon">
<component
:is="useRenderIcon(route.meta && route.meta.icon)"
></component>
</el-icon>
<span>{{ transformI18n(route.meta.title, route.meta.i18n) }}</span>
<FontIcon
v-if="route.meta.extraIcon"
width="30px"
height="30px"
style="position: absolute; right: 10px"
:icon="route.meta.extraIcon.name"
:svg="route.meta.extraIcon.svg ? true : false"
></FontIcon>
</template>
</el-menu-item>
</el-menu>
<div class="horizontal-header-right">
<!-- 通知 -->
<Notice id="header-notice" />
<!-- 全屏 -->
<screenfull id="header-screenfull" v-show="!deviceDetection()" />
<!-- 国际化 -->
<el-dropdown id="header-translation" trigger="click">
<globalization />
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'zh')"
@click="translationCh"
><el-icon class="check-zh" v-show="locale === 'zh'"
><IconifyIconOffline icon="check" /></el-icon
>简体中文</el-dropdown-item
>
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')"
@click="translationEn"
><el-icon class="check-en" v-show="locale === 'en'"
><IconifyIconOffline icon="check" /></el-icon
>English</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 退出登陆 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link">
<img :src="avatars" />
<p>{{ usename }}</p>
</span>
<template #dropdown>
<el-dropdown-menu class="logout">
<el-dropdown-item @click="logout">
<IconifyIconOffline
icon="logout-circle-r-line"
style="margin: 5px"
/>
{{ $t("buttons.hsLoginOut") }}</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-icon
class="el-icon-setting"
:title="$t('buttons.hssystemSet')"
@click="onPanel"
>
<IconifyIconOffline icon="setting" />
</el-icon>
</div>
</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;
}
.check-zh {
position: absolute;
left: 20px;
}
.check-en {
position: absolute;
left: 20px;
}
}
.logout {
max-width: 120px;
::v-deep(.el-dropdown-menu__item) {
min-width: 100%;
display: inline-flex;
flex-wrap: wrap;
}
}
</style>

View File

@@ -1,21 +1,14 @@
<script setup lang="ts">
import {
ref,
PropType,
nextTick,
computed,
CSSProperties,
getCurrentInstance
} from "vue";
import { ref, PropType, nextTick, computed, CSSProperties } from "vue";
import path from "path";
import { useNav } from "../../hooks/nav";
import { childrenType } from "../../types";
import { transformI18n } from "/@/plugins/i18n";
import { useAppStoreHook } from "/@/store/modules/app";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
const instance = getCurrentInstance().appContext.app.config.globalProperties;
const menuMode = instance.$storage.layout?.layout === "vertical";
const pureApp = useAppStoreHook();
const { pureApp } = useNav();
const menuMode = ["vertical", "mix"].includes(pureApp.layout);
const props = defineProps({
item: {
@@ -105,7 +98,6 @@ function hoverMenu(key) {
: Object.assign(key, {
showTooltip: false
});
hoverMenuMap.set(key, true);
});
}
@@ -119,6 +111,10 @@ function hasOneShowingChild(
return true;
});
if (showingChildren[0]?.meta?.showParent) {
return false;
}
if (showingChildren.length === 1) {
return true;
}
@@ -132,8 +128,8 @@ function hasOneShowingChild(
function resolvePath(routePath) {
const httpReg = /^http(s?):\/\//;
if (httpReg.test(routePath)) {
return props.basePath + "/" + routePath;
if (httpReg.test(routePath) || httpReg.test(props.basePath)) {
return routePath || props.basePath;
} else {
return path.resolve(props.basePath, routePath);
}
@@ -162,6 +158,18 @@ function resolvePath(routePath) {
"
></component>
</el-icon>
<div
v-if="
!pureApp.sidebar.opened &&
pureApp.layout === 'mix' &&
props.item?.pathList?.length === 2
"
:style="getDivStyle"
>
<span :style="getMenuTextStyle">
{{ transformI18n(onlyOneChild.meta.title, onlyOneChild.meta.i18n) }}
</span>
</div>
<template #title>
<div :style="getDivStyle">
<span v-if="!menuMode">{{

View File

@@ -1,60 +1,59 @@
<script setup lang="ts">
import Logo from "./logo.vue";
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 { computed, ref, onBeforeMount } from "vue";
import { useAppStoreHook } from "/@/store/modules/app";
import { ref, computed, watch, onBeforeMount } from "vue";
import { findRouteByPath, getParentPaths } from "/@/router/utils";
import { usePermissionStoreHook } from "/@/store/modules/permission";
const route = useRoute();
const pureApp = useAppStoreHook();
const router = useRouter().options.routes;
const routers = useRouter().options.routes;
const showLogo = ref(
storageLocal.getItem("responsive-configure")?.showLogo ?? true
);
const isCollapse = computed(() => {
return !pureApp.getSidebarStatus;
});
const activeMenu = computed((): string => {
const { meta, path } = route;
if (meta.activeMenu) {
// @ts-ignore
return meta.activeMenu;
}
return path;
const { pureApp, isCollapse, menuSelect } = useNav();
let subMenuData = ref([]);
const menuData = computed(() => {
return pureApp.layout === "mix"
? subMenuData.value
: usePermissionStoreHook().wholeMenus;
});
const menuSelect = (indexPath: string): void => {
let parentPath = "";
let parentPathIndex = indexPath.lastIndexOf("/");
if (parentPathIndex > 0) {
parentPath = indexPath.slice(0, parentPathIndex);
}
// 找到当前路由的信息
// eslint-disable-next-line no-inner-declarations
function findCurrentRoute(routes) {
return routes.map(item => {
if (item.path === indexPath) {
// 切换左侧菜单 通知标签页
emitter.emit("changLayoutRoute", {
indexPath,
parentPath
});
} else {
if (item.children) findCurrentRoute(item.children);
}
});
}
findCurrentRoute(router);
};
function getSubMenuData(path) {
// path的上级路由组成的数组
const parentPathArr = getParentPaths(
path,
usePermissionStoreHook().wholeMenus
);
// 当前路由的父级路由信息
const parenetRoute = findRouteByPath(
parentPathArr[0] || path,
usePermissionStoreHook().wholeMenus
);
if (!parenetRoute?.children) return;
subMenuData.value = parenetRoute?.children;
}
getSubMenuData(route.path);
onBeforeMount(() => {
emitter.on("logoChange", key => {
showLogo.value = key;
});
});
watch(
() => route.path,
() => {
getSubMenuData(route.path);
menuSelect(route.path, routers);
}
);
</script>
<template>
@@ -62,21 +61,21 @@ onBeforeMount(() => {
<Logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:default-active="route.path"
:collapse="isCollapse"
unique-opened
router
:collapse-transition="false"
mode="vertical"
class="outer-most"
@select="menuSelect"
@select="indexPath => menuSelect(indexPath, routers)"
>
<sidebar-item
v-for="route in usePermissionStoreHook().wholeMenus"
:key="route.path"
:item="route"
v-for="routes in menuData"
:key="routes.path"
:item="routes"
class="outer-most"
:base-path="route.path"
:base-path="routes.path"
/>
</el-menu>
</el-scrollbar>

View File

@@ -18,36 +18,6 @@
}
}
@-webkit-keyframes rotate {
from {
-webkit-transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
}
}
@-moz-keyframes rotate {
from {
-moz-transform: rotate(0deg);
}
to {
-moz-transform: rotate(360deg);
}
}
@-o-keyframes rotate {
from {
-o-transform: rotate(0deg);
}
to {
-o-transform: rotate(360deg);
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
@@ -80,7 +50,7 @@
.scroll-item {
border-radius: 3px 3px 0 0;
padding: 0 6px 0 6px;
padding: 0 6px;
box-shadow: 0 0 1px #888;
position: relative;
margin-right: 4px;
@@ -92,7 +62,7 @@
.el-icon-close {
font-size: 10px;
color: #1890ff;
color: var(--el-color-primary);
cursor: pointer;
position: absolute;
top: 50%;
@@ -123,7 +93,7 @@
a {
text-decoration: none;
color: #666;
padding: 0 4px 0 4px;
padding: 0 4px;
}
.scroll-container {
@@ -190,7 +160,8 @@
align-items: center;
&:hover {
background: #eee;
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
}
svg {
@@ -236,7 +207,7 @@
}
.scroll-item.is-active {
background-color: #eaf4fe;
background-color: var(--el-color-primary-light-9);
position: relative;
color: #fff;
@@ -249,7 +220,7 @@
}
a {
color: #1890ff;
color: var(--el-color-primary);
}
}
@@ -288,10 +259,10 @@
/* 卡片模式下鼠标移入显示蓝色边框 */
.card-in {
color: #1890ff;
color: var(--el-color-primary);
a {
color: #1890ff;
color: var(--el-color-primary);
}
}
@@ -312,7 +283,7 @@
position: absolute;
left: 0;
bottom: 0;
background: #1890ff;
background: var(--el-color-primary);
}
/* 灵动模式下鼠标移入显示蓝色进度条 */
@@ -322,7 +293,7 @@
position: absolute;
left: 0;
bottom: 0;
background: #1890ff;
background: var(--el-color-primary);
animation: scheduleInWidth 400ms ease-in;
}
@@ -333,14 +304,11 @@
position: absolute;
left: 0;
bottom: 0;
background: #1890ff;
background: var(--el-color-primary);
animation: scheduleOutWidth 400ms ease-in;
}
/* 刷新按钮动画效果 */
.refresh-button {
-webkit-animation: rotate 600ms linear infinite;
-moz-animation: rotate 600ms linear infinite;
-o-animation: rotate 600ms linear infinite;
animation: rotate 600ms linear infinite;
}

View File

@@ -21,10 +21,10 @@ import closeRight from "/@/assets/svg/close_right.svg?component";
import { emitter } from "/@/utils/mitt";
import { $t as t } from "/@/plugins/i18n";
import { isEqual, isEmpty } from "lodash-es";
import { transformI18n } from "/@/plugins/i18n";
import { storageLocal } from "/@/utils/storage";
import { useRoute, useRouter } from "vue-router";
import { isEqual, isEmpty } from "lodash-unified";
import { RouteConfigs, tagsViewsType } from "../../types";
import { useSettingStoreHook } from "/@/store/modules/settings";
import { handleAliveRoute, delAliveRoutes } from "/@/router/utils";
@@ -106,7 +106,11 @@ const iconIsActive = computed(() => {
const dynamicTagView = () => {
const index = multiTags.value.findIndex(item => {
return item.path === route.path;
if (item?.query) {
return isEqual(route?.query, item?.query);
} else {
return item.path === route.path;
}
});
moveToView(index);
};
@@ -423,6 +427,11 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
});
}
function handleCommand(command: object) {
const { key, item } = command;
onClickDrop(key, item);
}
// 触发右键中菜单的点击事件
function selectTag(key, item) {
onClickDrop(key, item, currentSelect.value);
@@ -709,7 +718,11 @@ const getContextMenuStyle = computed((): CSSProperties => {
</el-icon>
</li>
<li>
<el-dropdown trigger="click" placement="bottom-end">
<el-dropdown
trigger="click"
placement="bottom-end"
@command="handleCommand"
>
<el-icon>
<IconifyIconOffline icon="arrow-down" />
</el-icon>
@@ -718,11 +731,15 @@ const getContextMenuStyle = computed((): CSSProperties => {
<el-dropdown-item
v-for="(item, key) in tagsViews"
:key="key"
:command="{ key, item }"
:divided="item.divided"
:disabled="item.disabled"
@click="onClickDrop(key, item)"
>
<component :is="item.icon" :key="key" />
<component
:is="item.icon"
:key="key"
style="margin-right: 6px"
/>
{{ $t(item.text) }}
</el-dropdown-item>
</el-dropdown-menu>

63
src/layout/frameView.vue Normal file
View File

@@ -0,0 +1,63 @@
<template>
<div class="frame" v-loading="loading">
<iframe :src="frameSrc" class="frame-iframe" ref="frameRef"></iframe>
</div>
</template>
<script lang="ts" setup>
import { useRoute } from "vue-router";
import { ref, unref, onMounted, nextTick } from "vue";
const loading = ref(false);
const currentRoute = useRoute();
const frameSrc = ref<string>("");
const frameRef = ref<HTMLElement | null>(null);
if (unref(currentRoute.meta)?.frameSrc) {
frameSrc.value = unref(currentRoute.meta)?.frameSrc as string;
}
function hideLoading() {
loading.value = false;
}
function init() {
nextTick(() => {
const iframe = unref(frameRef);
if (!iframe) return;
const _frame = iframe as any;
if (_frame.attachEvent) {
_frame.attachEvent("onload", () => {
hideLoading();
});
} else {
iframe.onload = () => {
hideLoading();
};
}
});
}
onMounted(() => {
loading.value = true;
init();
});
</script>
<style lang="scss" scoped>
.frame {
height: 100vh;
z-index: 998;
.frame-iframe {
width: 100%;
height: 100%;
overflow: hidden;
border: 0;
box-sizing: border-box;
}
}
.main-content {
margin: 0 !important;
}
</style>

117
src/layout/hooks/nav.ts Normal file
View File

@@ -0,0 +1,117 @@
import { computed } from "vue";
import { router } from "/@/router";
import { emitter } from "/@/utils/mitt";
import { routeMetaType } from "../types";
import { transformI18n } from "/@/plugins/i18n";
import { storageSession } from "/@/utils/storage";
import { useAppStoreHook } from "/@/store/modules/app";
import { remainingPaths } from "/@/router/modules/index";
import { Title } from "../../../public/serverConfig.json";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
export function useNav() {
const pureApp = useAppStoreHook();
// 用户名
const usename: string = storageSession.getItem("info")?.username;
// 设置国际化选中后的样式
const getDropdownItemStyle = computed(() => {
return (locale, t) => {
return {
background: locale === t ? useEpThemeStoreHook().epThemeColor : "",
color: locale === t ? "#f4f4f5" : "#000"
};
};
});
const isCollapse = computed(() => {
return !pureApp.getSidebarStatus;
});
// 动态title
function changeTitle(meta: routeMetaType) {
if (Title)
document.title = `${transformI18n(meta.title, meta.i18n)} | ${Title}`;
else document.title = transformI18n(meta.title, meta.i18n);
}
// 退出登录
function logout() {
storageSession.removeItem("info");
router.push("/login");
}
function backHome() {
router.push("/welcome");
}
function onPanel() {
emitter.emit("openPanel");
}
function toggleSideBar() {
pureApp.toggleSideBar();
}
function handleResize(menuRef) {
menuRef.handleResize();
}
function resolvePath(route) {
const httpReg = /^http(s?):\/\//;
const routeChildPath = route.children[0]?.path;
if (httpReg.test(routeChildPath)) {
return route.path + "/" + routeChildPath;
} else {
return routeChildPath;
}
}
function menuSelect(indexPath: string, routers): void {
if (isRemaining(indexPath)) return;
let parentPath = "";
const parentPathIndex = indexPath.lastIndexOf("/");
if (parentPathIndex > 0) {
parentPath = indexPath.slice(0, parentPathIndex);
}
// 找到当前路由的信息
function findCurrentRoute(indexPath: string, routes) {
return routes.map(item => {
if (item.path === indexPath) {
if (item.redirect) {
findCurrentRoute(item.redirect, item.children);
} else {
// 切换左侧菜单 通知标签页
emitter.emit("changLayoutRoute", {
indexPath,
parentPath
});
}
} else {
if (item.children) findCurrentRoute(indexPath, item.children);
}
});
}
findCurrentRoute(indexPath, routers);
}
// 判断路径是否参与菜单
function isRemaining(path: string): boolean {
return remainingPaths.includes(path);
}
return {
logout,
backHome,
onPanel,
changeTitle,
toggleSideBar,
menuSelect,
handleResize,
resolvePath,
isCollapse,
pureApp,
usename,
getDropdownItemStyle
};
}

View File

@@ -171,7 +171,8 @@ const layoutHeader = defineComponent({
},
{
default: () => [
!pureSetting.hiddenSideBar && layout.value.includes("vertical")
!pureSetting.hiddenSideBar &&
(layout.value.includes("vertical") || layout.value.includes("mix"))
? h(navbar)
: h("div"),
!pureSetting.hiddenSideBar && layout.value.includes("horizontal")
@@ -213,7 +214,10 @@ const layoutHeader = defineComponent({
@click="useAppStoreHook().toggleSideBar()"
/>
<Vertical
v-show="!pureSetting.hiddenSideBar && layout.includes('vertical')"
v-show="
!pureSetting.hiddenSideBar &&
(layout.includes('vertical') || layout.includes('mix'))
"
/>
<div
:class="[

View File

@@ -1,15 +0,0 @@
<template>
<router-view>
<template #default="{ Component, route }">
<transition appear name="fade-transform" mode="out-in">
<component :is="Component" :key="route.fullPath" />
</transition>
</template>
</router-view>
</template>
<script lang="ts">
export default {
name: "layoutParentView"
};
</script>

View File

@@ -19,7 +19,7 @@ $subMenuBg: #0f0303 !default;
/* 有无子集的激活菜单背景 */
$subMenuActiveBg: #4091f7 !default;
$navTextColor: #fff !default;
$menuText: rgba(254, 254, 254, 0.65) !default;
$menuText: rgb(254 254 254 / 65%) !default;
/* logo背景颜色 */
$sidebarLogo: #002140 !default;

View File

@@ -5,7 +5,7 @@ $menuHover: #e13c39;
$subMenuBg: #000;
$subMenuActiveBg: #e13c39;
$navTextColor: red;
$menuText: rgba(254, 254, 254, 0.651);
$menuText: rgb(254 254 254 / 65.1%);
$sidebarLogo: #42090c;
$menuTitleHover: #fff;
$menuActiveBefore: #e13c39;

View File

@@ -5,7 +5,7 @@ $menuHover: #e85f33;
$subMenuBg: #0f0603;
$subMenuActiveBg: #e85f33;
$navTextColor: #fff;
$menuText: rgba(254, 254, 254, 0.65);
$menuText: rgb(254 254 254 / 65%);
$sidebarLogo: #441708;
$menuTitleHover: #fff;
$menuActiveBefore: #e85f33;

View File

@@ -5,7 +5,7 @@ $menuHover: #f6da4d;
$subMenuBg: #0f0603;
$subMenuActiveBg: #f6da4d;
$navTextColor: #fff;
$menuText: rgba(254, 254, 254, 0.65);
$menuText: rgb(254 254 254 / 65%);
$sidebarLogo: #443b05;
$menuTitleHover: #fff;
$menuActiveBefore: #f6da4d;

View File

@@ -11,18 +11,20 @@ export const routerArrays: Array<RouteConfigs> = [
}
];
export type routeMetaType = {
title?: string;
i18n?: boolean;
icon?: string;
showLink?: boolean;
savedPosition?: boolean;
authority?: Array<string>;
};
export type RouteConfigs = {
path?: string;
parentPath?: string;
query?: object;
meta?: {
title?: string;
i18n?: boolean;
icon?: string;
showLink?: boolean;
savedPosition?: boolean;
authority?: Array<string>;
};
meta?: routeMetaType;
children?: RouteConfigs[];
name?: string;
};
@@ -65,12 +67,15 @@ export type childrenType = {
icon?: string;
title?: string;
i18n?: boolean;
showParent?: boolean;
extraIcon?: {
svg?: boolean;
name?: string;
};
};
showTooltip?: boolean;
parentId?: number;
pathList?: number[];
};
export type themeColorsType = {

View File

@@ -37,14 +37,10 @@ app.component("IconifyIconOnline", IconifyIconOnline);
app.component("FontIcon", FontIcon);
getServerConfig(app).then(async config => {
app.use(router);
await router.isReady();
injectResponsiveStorage(app, config);
setupStore(app);
app
.use(router)
.use(MotionPlugin)
.use(useElementPlus)
.use(useTable)
.use(usI18n);
await router.isReady();
app.use(MotionPlugin).use(useElementPlus).use(useTable).use(usI18n);
app.mount("#app");
});

View File

@@ -44,6 +44,12 @@ import {
ElCollapse,
ElCollapseItem,
ElTreeV2,
ElTable,
ElTableColumn,
ElLink,
ElColorPicker,
ElSelect,
ElOption,
// 指令
ElLoading,
ElInfiniteScroll
@@ -96,7 +102,13 @@ const components = [
ElEmpty,
ElCollapse,
ElCollapseItem,
ElTreeV2
ElTreeV2,
ElTable,
ElTableColumn,
ElLink,
ElColorPicker,
ElSelect,
ElOption
];
export function useElementPlus(app: App) {

View File

@@ -32,7 +32,14 @@ export default {
permissionPage: "Page Permission",
permissionButton: "Button Permission",
hstabs: "Tabs Operate",
hsMenuTree: "Menu Tree",
hsguide: "Guide",
externalLink: "External Link"
hsAble: "Able",
hsMenuTree: "Menu Tree",
hsWatermark: "Water Mark",
hsPrint: "Print",
hsExternalPage: "External Page",
hsPureDocument: "Pure Doc(Embedded)",
externalLink: "Pure Doc(External)",
hsEpDocument: "Element Plus Doc(Embedded)",
hsAbout: "About"
};

View File

@@ -1,6 +1,6 @@
// 多组件库的国际化和本地项目国际化兼容
import { App } from "vue";
import { set } from "lodash-es";
import { set } from "lodash-unified";
import { createI18n } from "vue-i18n";
import { localesConfigs } from "./config";
import { storageLocal } from "/@/utils/storage";
@@ -11,7 +11,10 @@ import { storageLocal } from "/@/utils/storage";
* @param isI18n 如果true,获取对应的消息,否则返回本身
* @returns message
*/
export function transformI18n(message: string | object = "", isI18n = false) {
export function transformI18n(
message: string | unknown | object = "",
isI18n: boolean | unknown = false
) {
if (!message) {
return "";
}

View File

@@ -32,7 +32,14 @@ export default {
permissionPage: "页面权限",
permissionButton: "按钮权限",
hstabs: "标签页操作",
hsMenuTree: "菜单树结构",
hsguide: "引导页",
externalLink: "外链"
hsAble: "功能",
hsMenuTree: "菜单树结构",
hsWatermark: "水印",
hsPrint: "打印",
hsExternalPage: "外部页面",
hsPureDocument: "平台文档(内嵌)",
externalLink: "平台文档(外链)",
hsEpDocument: "Element Plus文档(内嵌)",
hsAbout: "关于"
};

View File

@@ -1,6 +1,7 @@
import "xe-utils";
import { App } from "vue";
import { i18n } from "../i18n/index";
import "font-awesome/css/font-awesome.min.css";
import {
// 核心
VXETable,

View File

@@ -1,11 +1,13 @@
import { isUrl } from "/@/utils/is";
import { toRouteType } from "./types";
import { openLink } from "/@/utils/link";
import NProgress from "/@/utils/progress";
import { constantRoutes } from "./modules";
import { split, findIndex } from "lodash-es";
import { findIndex } from "lodash-unified";
import { transformI18n } from "/@/plugins/i18n";
import remainingRouter from "./modules/remaining";
import { storageSession } from "/@/utils/storage";
import { Title } from "../../public/serverConfig.json";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import { Router, RouteMeta, createRouter, RouteRecordName } from "vue-router";
@@ -51,21 +53,22 @@ router.beforeEach((to: toRouteType, _from, next) => {
}
const name = storageSession.getItem("info");
NProgress.start();
const externalLink = to?.redirectedFrom?.fullPath;
const externalLink = isUrl(to?.name);
if (!externalLink)
to.matched.some(item => {
item.meta.title
? (document.title = transformI18n(
item.meta.title as string,
item.meta?.i18n as boolean
))
: "";
if (!item.meta.title) return "";
if (Title)
document.title = `${transformI18n(
item.meta.title,
item.meta?.i18n
)} | ${Title}`;
else document.title = transformI18n(item.meta.title, item.meta?.i18n);
});
if (name) {
if (_from?.name) {
// 如果路由包含http 则是超链接 反之是普通路由
if (externalLink && externalLink.includes("http")) {
openLink(`http${split(externalLink, "http")[1]}`);
// name为超链接
if (externalLink) {
openLink(to?.name);
NProgress.done();
} else {
next();

View File

@@ -0,0 +1,46 @@
import { $t } from "/@/plugins/i18n";
const Layout = () => import("/@/layout/index.vue");
const ableRouter = {
path: "/able",
name: "components",
component: Layout,
redirect: "/able/menuTree",
meta: {
icon: "ubuntu-fill",
title: $t("menus.hsAble"),
i18n: true,
rank: 3
},
children: [
{
path: "/able/menuTree",
name: "reMenuTree",
component: () => import("/@/views/able/menu-tree.vue"),
meta: {
title: $t("menus.hsMenuTree"),
i18n: true
}
},
{
path: "/able/watermark",
name: "reWatermark",
component: () => import("/@/views/able/watermark.vue"),
meta: {
title: $t("menus.hsWatermark"),
i18n: true
}
},
{
path: "/able/print",
name: "rePrint",
component: () => import("/@/views/able/print.vue"),
meta: {
title: $t("menus.hsPrint"),
i18n: true
}
}
]
};
export default ableRouter;

View File

@@ -0,0 +1,28 @@
import { $t } from "/@/plugins/i18n";
const Layout = () => import("/@/layout/index.vue");
const aboutRouter = {
path: "/about",
name: "reAbout",
component: Layout,
redirect: "/about",
meta: {
icon: "question-line",
title: $t("menus.hsAbout"),
i18n: true,
rank: 12
},
children: [
{
path: "/about",
name: "reAbout",
component: () => import("/@/views/about.vue"),
meta: {
title: $t("menus.hsAbout"),
i18n: true
}
}
]
};
export default aboutRouter;

View File

@@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const componentsRouter = {
path: "/components",

View File

@@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const editorRouter = {
path: "/editor",

View File

@@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const errorRouter = {
path: "/error",
@@ -10,7 +10,7 @@ const errorRouter = {
icon: "position",
title: $t("menus.hserror"),
i18n: true,
rank: 7
rank: 9
},
children: [
{

View File

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

View File

@@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const flowChartRouter = {
path: "/flowChart",

View File

@@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const guideRouter = {
path: "/guide",
@@ -10,7 +10,7 @@ const guideRouter = {
icon: "guide",
title: $t("menus.hsguide"),
i18n: true,
rank: 10
rank: 11
},
children: [
{

View File

@@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const homeRouter = {
path: "/",

View File

@@ -1,11 +1,11 @@
// 静态路由
import about from "./about";
import homeRouter from "./home";
import ableRouter from "./able";
import errorRouter from "./error";
import guideRouter from "./guide";
import editorRouter from "./editor";
import nestedRouter from "./nested";
import menuTreeRouter from "./menuTree";
import externalLink from "./externalLink";
import flowChartRouter from "./flowchart";
import remainingRouter from "./remaining";
import componentsRouter from "./components";
@@ -16,26 +16,32 @@ import {
formatTwoStageRoutes,
formatFlatteningRoutes
} from "../utils";
import { buildHierarchyTree } from "/@/utils/tree";
// 原始静态路由(未做任何处理)
const routes = [
about,
homeRouter,
ableRouter,
errorRouter,
guideRouter,
nestedRouter,
externalLink,
editorRouter,
menuTreeRouter,
flowChartRouter,
componentsRouter
];
// 导出处理后的静态路由(三级及以上的路由全部拍成二级)
export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes(
formatFlatteningRoutes(ascending(routes))
formatFlatteningRoutes(buildHierarchyTree(ascending(routes)))
);
// 用于渲染菜单,保持原始层级
export const constantMenus: Array<RouteComponent> = ascending(routes).concat(
...remainingRouter
);
// 不参与菜单的路由
export const remainingPaths = Object.keys(remainingRouter).map(v => {
return remainingRouter[v].path;
});

View File

@@ -1,28 +0,0 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const menuTreeRouter = {
path: "/menuTree",
name: "reMenuTree",
component: Layout,
redirect: "/menuTree/index",
meta: {
icon: "node-tree",
title: $t("menus.hsMenuTree"),
i18n: true,
rank: 9
},
children: [
{
path: "/menuTree/index",
name: "reMenuTree",
component: () => import("/@/views/menu-tree/index.vue"),
meta: {
title: $t("menus.hsMenuTree"),
i18n: true
}
}
]
};
export default menuTreeRouter;

View File

@@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const nestedRouter = {
path: "/nested",
@@ -10,12 +10,11 @@ const nestedRouter = {
title: $t("menus.hsmenus"),
icon: "histogram",
i18n: true,
rank: 5
rank: 8
},
children: [
{
path: "/nested/menu1",
component: () => import("/@/layout/routerView/parent.vue"),
name: "Menu1",
meta: {
title: $t("menus.hsmenu1"),
@@ -36,7 +35,6 @@ const nestedRouter = {
},
{
path: "/nested/menu1/menu1-2",
component: () => import("/@/layout/routerView/parent.vue"),
name: "Menu1-2",
redirect: "/nested/menu1/menu1-2/menu1-2-1",
meta: {

View File

@@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const remainingRouter = [
{

View File

@@ -8,10 +8,12 @@ import {
} from "vue-router";
import { router } from "./index";
import { loadEnv } from "../../build";
import Layout from "/@/layout/index.vue";
import { useTimeoutFn } from "@vueuse/core";
import { RouteConfigs } from "/@/layout/types";
import { buildHierarchyTree } from "/@/utils/tree";
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}");
@@ -125,6 +127,10 @@ function initRouter(name: string) {
// 最终路由进行升序
ascending(router.options.routes[0].children);
if (!router.hasRoute(v?.name)) router.addRoute(v);
const flattenRouters = router
.getRoutes()
.find(n => n.path === "/");
router.addRoute(flattenRouters);
}
resolve(router);
}
@@ -146,14 +152,15 @@ function initRouter(name: string) {
*/
function formatFlatteningRoutes(routesList: RouteRecordRaw[]) {
if (routesList.length === 0) return routesList;
for (let i = 0; i < routesList.length; i++) {
if (routesList[i].children) {
routesList = routesList
let hierarchyList = buildHierarchyTree(routesList);
for (let i = 0; i < hierarchyList.length; i++) {
if (hierarchyList[i].children) {
hierarchyList = hierarchyList
.slice(0, i + 1)
.concat(routesList[i].children, routesList.slice(i + 1));
.concat(hierarchyList[i].children, hierarchyList.slice(i + 1));
}
}
return routesList;
return hierarchyList;
}
/**
@@ -216,6 +223,8 @@ function addAsyncRoutes(arrRoutes: Array<RouteRecordRaw>) {
arrRoutes.forEach((v: RouteRecordRaw) => {
if (v.redirect) {
v.component = Layout;
} else if (v.meta?.frameSrc) {
v.component = IFrame;
} else {
const index = modulesRoutesKeys.findIndex(ev => ev.includes(v.path));
v.component = modulesRoutes[modulesRoutesKeys[index]];

View File

@@ -8,16 +8,29 @@ export const useEpThemeStore = defineStore({
state: () => ({
epThemeColor:
storageLocal.getItem("responsive-layout")?.epThemeColor ??
getConfig().EpThemeColor
getConfig().EpThemeColor,
epTheme:
storageLocal.getItem("responsive-layout")?.theme ?? getConfig().Theme
}),
getters: {
getEpThemeColor() {
return this.epThemeColor;
},
// 用于mix导航模式下hamburger-svg的fill属性
fill() {
if (this.epTheme === "light") {
return "#409eff";
} else if (this.epTheme === "yellow") {
return "#d25f00";
} else {
return "#fff";
}
}
},
actions: {
setEpThemeColor(newColor) {
const layout = storageLocal.getItem("responsive-layout");
this.epTheme = layout?.theme;
this.epThemeColor = newColor;
layout.epThemeColor = newColor;
storageLocal.setItem("responsive-layout", layout);

View File

@@ -1,6 +1,7 @@
import { defineStore } from "pinia";
import { store } from "/@/store";
import { isEqual } from "lodash-es";
import { isUrl } from "/@/utils/is";
import { isEqual } from "lodash-unified";
import { storageLocal } from "/@/utils/storage";
import { multiType, positionType } from "./types";
@@ -54,6 +55,7 @@ export const useMultiTagsStore = defineStore({
case "push":
{
const tagVal = value as multiType;
if (isUrl(tagVal?.name)) return;
const tagPath = tagVal?.path;
// 判断tag是否已存在
const tagHasExits = this.multiTags.some(tag => {

View File

@@ -1,7 +1,7 @@
import { defineStore } from "pinia";
import { store } from "/@/store";
import { cacheType } from "./types";
import { cloneDeep } from "lodash-es";
import { cloneDeep } from "lodash-unified";
import { RouteConfigs } from "/@/layout/types";
import { constantMenus } from "/@/router/modules";
import { ascending, filterTree } from "/@/router/utils";

View File

@@ -19,7 +19,7 @@
}
.el-overlay {
background-color: rgba(0, 0, 0, 0.05) !important;
background-color: rgb(0 0 0 / 5%) !important;
}
.el-drawer {

View File

@@ -25,7 +25,7 @@
}
.el-dropdown-menu {
padding: 2px 0 2px 0 !important;
padding: 2px 0 !important;
}
.el-range-separator {
@@ -38,14 +38,16 @@
/* 动态改变cssvar 用于主题切换 https://github.com/element-plus/element-plus/issues/4856#issuecomment-1000174357 */
.el-button--primary {
--el-button-bg-color: var(--el-color-primary) !important;
--el-button-border-color: var(--el-color-primary) !important;
--el-button-hover-bg-color: var(--el-color-primary-light-2) !important;
--el-button-hover-border-color: var(--el-color-primary-light-2) !important;
--el-button-active-bg-color: var(--el-color-primary-active) !important;
--el-button-active-border-color: var(--el-color-primary-active) !important;
}
/* button--primary plain */
.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

@@ -11,9 +11,9 @@ body {
padding: 0;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
Microsoft YaHei, Arial, sans-serif;
text-rendering: optimizelegibility;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
}
html {
@@ -22,6 +22,11 @@ html {
box-sizing: border-box;
}
#app {
width: 100%;
height: 100%;
}
label {
font-weight: 700;
}
@@ -74,19 +79,11 @@ ul {
/* 灰色模式 */
.html-grey {
filter: grayscale(100%);
-webkit-filter: grayscale(100%);
-moz-filter: grayscale(100%);
-ms-filter: grayscale(100%);
-o-filter: grayscale(100%);
}
/* 色弱模式 */
.html-weakness {
filter: invert(80%);
-webkit-filter: invert(80%);
-moz-filter: invert(80%);
-ms-filter: invert(80%);
-o-filter: invert(80%);
}
.pc-spacing {

View File

@@ -23,16 +23,6 @@
margin-left: $sideBarWidth;
position: relative;
background: #f0f2f5;
@media screen and (min-width: 150px) and (max-width: 420px) {
.app-main .el-scrollbar__view:first-child {
overflow-y: hidden;
}
}
@media screen and (min-width: 420px) {
.app-main .el-scrollbar__view:first-child {
overflow: hidden;
}
}
}
.fixed-header {
@@ -502,9 +492,7 @@
background-color: $menuActiveBefore;
content: "";
clear: both;
-webkit-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
-webkit-transform: translateY(0);
transform: translateY(0);
}
@@ -517,9 +505,7 @@
background-color: $menuActiveBefore;
content: "";
clear: both;
-webkit-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
-webkit-transform: translateY(0);
transform: translateY(0);
}
@@ -530,7 +516,6 @@
position: absolute;
height: 0;
width: 3px;
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
top: 50%;
}
@@ -643,3 +628,64 @@ body[layout="horizontal"] {
transition: none !important;
}
}
body[layout="mix"] {
$sideBarWidth: 210px;
@include merge-style($sideBarWidth);
.el-menu {
--el-menu-hover-bg-color: transparent !important;
}
.hideSidebar {
.fixed-header {
width: calc(100% - 54px);
transition: width 0.28s;
}
.sidebar-container {
width: 54px !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 {
& > .el-sub-menu__title {
& > span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
}
/* 无子菜单 */
.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

@@ -3,7 +3,7 @@ import {
resultType,
PureHttpError,
RequestMethods,
PureHttpResoponse,
PureHttpResponse,
PureHttpRequestConfig
} from "./types.d";
import qs from "qs";
@@ -91,7 +91,7 @@ class PureHttp {
private httpInterceptorsResponse(): void {
const instance = PureHttp.axiosInstance;
instance.interceptors.response.use(
(response: PureHttpResoponse) => {
(response: PureHttpResponse) => {
const $config = response.config;
// 关闭进度条动画
NProgress.done();
@@ -145,21 +145,21 @@ class PureHttp {
}
// 单独抽离的post工具函数
public post<T>(
public post<T, P>(
url: string,
params?: T,
config?: PureHttpRequestConfig
): Promise<T> {
return this.request<T>("post", url, params, config);
): Promise<P> {
return this.request<P>("post", url, params, config);
}
// 单独抽离的get工具函数
public get<T>(
public get<T, P>(
url: string,
params?: T,
config?: PureHttpRequestConfig
): Promise<T> {
return this.request<T>("get", url, params, config);
): Promise<P> {
return this.request<P>("get", url, params, config);
}
}

View File

@@ -18,13 +18,13 @@ export interface PureHttpError extends AxiosError {
isCancelRequest?: boolean;
}
export interface PureHttpResoponse extends AxiosResponse {
export interface PureHttpResponse extends AxiosResponse {
config: PureHttpRequestConfig;
}
export interface PureHttpRequestConfig extends AxiosRequestConfig {
beforeRequestCallback?: (request: PureHttpRequestConfig) => void;
beforeResponseCallback?: (response: PureHttpResoponse) => void;
beforeResponseCallback?: (response: PureHttpResponse) => void;
}
export default class PureHttp {
@@ -34,6 +34,14 @@ export default class PureHttp {
param?: AxiosRequestConfig,
axiosConfig?: PureHttpRequestConfig
): Promise<T>;
post<T>(url: string, params?: T, config?: PureHttpRequestConfig): Promise<T>;
get<T>(url: string, params?: T, config?: PureHttpRequestConfig): Promise<T>;
post<T, P>(
url: string,
params?: T,
config?: PureHttpRequestConfig
): Promise<P>;
get<T, P>(
url: string,
params?: T,
config?: PureHttpRequestConfig
): Promise<P>;
}

View File

@@ -94,8 +94,9 @@ export const isServer = typeof window === "undefined";
export const isClient = !isServer;
export function isUrl(path: string): boolean {
export function isUrl<T>(path: T): boolean {
const reg =
/(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
// @ts-expect-error
return reg.test(path);
}

View File

@@ -1,5 +1,6 @@
export const openLink = (link: string) => {
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");

View File

@@ -1,3 +1,5 @@
import type { FunctionArgs } from "@vueuse/core";
export const hasClass = (ele: RefType<any>, cls: string): any => {
return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)"));
};
@@ -40,3 +42,16 @@ export const toggleClass = (
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;
});
};
}

226
src/utils/print.ts Normal file
View File

@@ -0,0 +1,226 @@
interface PrintFunction {
extendOptions: Function;
getStyle: Function;
setDomHeight: Function;
toPrint: Function;
}
const Print = function (dom, options?: object): PrintFunction {
options = options || {};
// @ts-expect-error
if (!(this instanceof Print)) return new Print(dom, options);
this.conf = {
styleStr: "",
// Elements that need to dynamically get and set the height
setDomHeightArr: [],
// Echart dom List
echartDomArr: [],
// Callback before printing
printBeforeFn: null,
// Callback after printing
printDoneCallBack: null
};
for (const key in this.conf) {
// eslint-disable-next-line no-prototype-builtins
if (key && options.hasOwnProperty(key)) {
this.conf[key] = options[key];
}
}
if (typeof dom === "string") {
this.dom = document.querySelector(dom);
} else {
this.dom = this.isDOM(dom) ? dom : dom.$el;
}
if (this.conf.setDomHeightArr && this.conf.setDomHeightArr.length) {
this.setDomHeight(this.conf.setDomHeightArr);
}
this.init();
};
Print.prototype = {
/**
* init
*/
init: function (): void {
const content = this.getStyle() + this.getHtml();
this.writeIframe(content);
},
/**
* Configuration property extension
* @param {Object} obj
* @param {Object} obj2
*/
extendOptions: function <T>(obj, obj2: T): T {
for (const k in obj2) {
obj[k] = obj2[k];
}
return obj;
},
/**
Copy all styles of the original page
*/
getStyle: function (): string {
let str = "";
const styles: NodeListOf<Element> = document.querySelectorAll("style,link");
for (let i = 0; i < styles.length; i++) {
str += styles[i].outerHTML;
}
str += `<style>.no-print{display:none;}${this.conf.styleStr}</style>`;
return str;
},
// form assignment
getHtml: function (): Element {
const inputs = document.querySelectorAll("input");
const selects = document.querySelectorAll("select");
const textareas = document.querySelectorAll("textarea");
for (let k = 0; k < inputs.length; k++) {
if (inputs[k].type == "checkbox" || inputs[k].type == "radio") {
if (inputs[k].checked == true) {
inputs[k].setAttribute("checked", "checked");
} else {
inputs[k].removeAttribute("checked");
}
} else if (inputs[k].type == "text") {
inputs[k].setAttribute("value", inputs[k].value);
} else {
inputs[k].setAttribute("value", inputs[k].value);
}
}
for (let k2 = 0; k2 < textareas.length; k2++) {
if (textareas[k2].type == "textarea") {
textareas[k2].innerHTML = textareas[k2].value;
}
}
for (let k3 = 0; k3 < selects.length; k3++) {
if (selects[k3].type == "select-one") {
const child = selects[k3].children;
for (const i in child) {
if (child[i].tagName == "OPTION") {
// @ts-ignore
if (child[i].selected == true) {
child[i].setAttribute("selected", "selected");
} else {
child[i].removeAttribute("selected");
}
}
}
}
}
return this.dom.outerHTML;
},
/**
create iframe
*/
writeIframe: function (content) {
let w: Document | Window;
let doc: Document;
const iframe: HTMLIFrameElement = document.createElement("iframe");
const f: HTMLIFrameElement = document.body.appendChild(iframe);
iframe.id = "myIframe";
iframe.setAttribute(
"style",
"position:absolute;width:0;height:0;top:-10px;left:-10px;"
);
// eslint-disable-next-line prefer-const
w = f.contentWindow || f.contentDocument;
// eslint-disable-next-line prefer-const
doc = f.contentDocument || f.contentWindow.document;
doc.open();
doc.write(content);
doc.close();
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
iframe.onload = function (): void {
// Before popping, callback
if (_this.conf.printBeforeFn) {
_this.conf.printBeforeFn({ doc });
}
_this.drawEchartImg(doc).then(() => {
_this.toPrint(w);
setTimeout(function () {
document.body.removeChild(iframe);
// After popup, callback
if (_this.conf.printDoneCallBack) {
_this.conf.printDoneCallBack();
}
}, 100);
});
};
},
/**
* echarts printing
* @param {Object} doc iframe window
*/
drawEchartImg(doc): Promise<void> {
return new Promise<void>(resolve => {
if (this.conf.echartDomArr && this.conf.echartDomArr.length > 0) {
this.conf.echartDomArr.forEach(e => {
const dom = doc.querySelector("#" + e.$el.id);
const img = new Image();
const w = dom.offsetWidth + "px";
const H = dom.offsetHeight + "px";
img.style.width = w;
img.style.height = H;
img.src = e.imgSrc;
dom.innerHTML = "";
dom.appendChild(img);
});
}
resolve();
});
},
/**
Print
*/
toPrint: function (frameWindow): void {
try {
setTimeout(function () {
frameWindow.focus();
try {
if (!frameWindow.document.execCommand("print", false, null)) {
frameWindow.print();
}
} catch (e) {
frameWindow.print();
}
frameWindow.close();
}, 10);
} catch (err) {
console.error(err);
}
},
isDOM:
typeof HTMLElement === "object"
? function (obj) {
return obj instanceof HTMLElement;
}
: function (obj) {
return (
obj &&
typeof obj === "object" &&
obj.nodeType === 1 &&
typeof obj.nodeName === "string"
);
},
/**
* Set the height of the specified dom element by getting the existing height of the dom element and setting
* @param {Array} arr
*/
setDomHeight(arr) {
if (arr && arr.length) {
arr.forEach(name => {
const domArr = document.querySelectorAll(name);
domArr.forEach(dom => {
dom.style.height = dom.offsetHeight + "px";
});
});
}
}
};
export default Print;

View File

@@ -20,7 +20,7 @@ export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
theme: config.Theme ?? "default",
darkMode: config.DarkMode ?? false,
sidebarStatus: config.SidebarStatus ?? true,
epThemeColor: config.EpThemeColor ?? "409EFF"
epThemeColor: config.EpThemeColor ?? "#409EFF"
}
},
configure: {

View File

@@ -46,3 +46,22 @@ export function deleteChildren(menuTree, pathList = []) {
}
return menuTree;
}
// 创建层级关系
export function buildHierarchyTree(menuTree, pathList = []) {
if (!Array.isArray(menuTree)) {
console.warn("menuTree must be an array");
return;
}
if (!menuTree || menuTree.length === 0) return;
for (const [key, node] of menuTree.entries()) {
node.id = key;
node.parentId = pathList.length ? pathList[pathList.length - 1] : null;
node.pathList = [...pathList, node.id];
const hasChildren = node.children && node.children.length > 0;
if (hasChildren) {
buildHierarchyTree(node.children, node.pathList);
}
}
return menuTree;
}

116
src/utils/watermark.ts Normal file
View File

@@ -0,0 +1,116 @@
import {
ref,
Ref,
unref,
shallowRef,
onBeforeUnmount,
getCurrentInstance
} from "vue";
import { isDef } from "/@/utils/is";
import { useRafThrottle } from "/@/utils/operate";
import { addResizeListener, removeResizeListener } from "/@/utils/resize";
const domSymbol = Symbol("watermark-dom");
type attr = {
font?: string;
fillStyle?: string;
};
export function useWatermark(
appendEl: Ref<HTMLElement | null> = ref(document.body) as Ref<HTMLElement>
) {
const func = useRafThrottle(function () {
const el = unref(appendEl);
if (!el) return;
const { clientHeight: height, clientWidth: width } = el;
updateWatermark({ height, width });
});
const id = domSymbol.toString();
const watermarkEl = shallowRef<HTMLElement>();
const clear = () => {
const domId = unref(watermarkEl);
watermarkEl.value = undefined;
const el = unref(appendEl);
if (!el) return;
domId && el.removeChild(domId);
removeResizeListener(el, func);
};
function createBase64(str: string, attr?: attr) {
const can = document.createElement("canvas");
const width = 300;
const height = 240;
Object.assign(can, { width, height });
const cans = can.getContext("2d");
if (cans) {
cans.rotate((-20 * Math.PI) / 120);
cans.font = attr?.font ?? "15px Reggae One";
cans.fillStyle = attr?.fillStyle ?? "rgba(0, 0, 0, 0.15)";
cans.textAlign = "left";
cans.textBaseline = "middle";
cans.fillText(str, width / 20, height);
}
return can.toDataURL("image/png");
}
function updateWatermark(
options: {
width?: number;
height?: number;
str?: string;
attr?: attr;
} = {}
) {
const el = unref(watermarkEl);
if (!el) return;
if (isDef(options.width)) {
el.style.width = `${options.width}px`;
}
if (isDef(options.height)) {
el.style.height = `${options.height}px`;
}
if (isDef(options.str)) {
el.style.background = `url(${createBase64(
options.str,
options.attr
)}) left top repeat`;
}
}
const createWatermark = (str: string, attr?: attr) => {
if (unref(watermarkEl)) {
updateWatermark({ str, attr });
return id;
}
const div = document.createElement("div");
watermarkEl.value = div;
div.id = id;
div.style.pointerEvents = "none";
div.style.top = "0px";
div.style.left = "0px";
div.style.position = "absolute";
div.style.zIndex = "100000";
const el = unref(appendEl);
if (!el) return id;
const { clientHeight: height, clientWidth: width } = el;
updateWatermark({ str, width, height, attr });
el.appendChild(div);
return id;
};
function setWatermark(str: string, attr?: attr) {
createWatermark(str, attr);
addResizeListener(document.documentElement, func);
const instance = getCurrentInstance();
if (instance) {
onBeforeUnmount(() => {
clear();
});
}
}
return { setWatermark, clear };
}

View File

@@ -0,0 +1,83 @@
<script lang="ts">
export default {
name: "reMenuTree"
};
</script>
<script setup lang="ts">
import { ref, computed } from "vue";
import type { ElTreeV2 } from "element-plus";
import { transformI18n } from "/@/plugins/i18n";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
import { extractPathList, deleteChildren } from "/@/utils/tree";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import type { TreeNode } from "element-plus/es/components/tree-v2/src/types";
interface treeNode extends TreeNode {
meta: {
title: string;
i18n: boolean;
};
}
const query = ref("");
let dataProps = ref({
value: "uniqueId",
children: "children"
});
const treeRef = ref<InstanceType<typeof ElTreeV2>>();
let menusData = computed(() => {
return deleteChildren(usePermissionStoreHook().menusTree);
});
let expandedKeys = extractPathList(menusData.value);
const onQueryChanged = (query: string) => {
// @ts-expect-error
treeRef.value!.filter(query);
};
const filterMethod = (query: string, node: treeNode) => {
return transformI18n(node.meta.title, node.meta.i18n)!.indexOf(query) !== -1;
};
</script>
<template>
<el-card>
<template #header>
<div class="card-header">
<span class="font-medium"
>菜单树结构采用element-plus的
<el-link
href="https://element-plus.gitee.io/zh-CN/component/tree-v2.html"
target="_blank"
:icon="useRenderIcon('node-tree')"
style="font-size: 16px; margin: 0 5px 4px 0"
>Tree V2</el-link
>组件并支持国际化</span
>
</div>
</template>
<el-input
class="mb-4"
v-model="query"
placeholder="请输入关键字查找"
clearable
@input="onQueryChanged"
></el-input>
<el-tree-v2
ref="treeRef"
:data="menusData"
:props="dataProps"
show-checkbox
:height="500"
:filter-method="filterMethod"
:default-expanded-keys="expandedKeys"
>
<template #default="{ data }">
<span>{{ $t(data.meta.title) }}</span>
</template>
</el-tree-v2>
</el-card>
</template>

329
src/views/able/print.vue Normal file
View File

@@ -0,0 +1,329 @@
<script setup lang="ts">
import Print from "/@/utils/print";
import { reactive, ref } from "vue";
import { VxeTablePropTypes } from "vxe-table";
import { ReLine } from "/@/components/ReCharts/index";
interface User {
date: string;
name: string;
address: string;
}
const demo1 = reactive({
tableData: [
{
id: 10001,
name: "Test1",
role: "Develop",
sex: "Man",
age: 28,
address: "test abc"
},
{
id: 10002,
name: "Test2",
role: "Test",
sex: "Women",
age: 22,
address: "Guangzhou"
},
{
id: 10003,
name: "Test3",
role: "PM",
sex: "Man",
age: 32,
address: "Shanghai"
},
{
id: 10004,
name: "Test4",
role: "Designer",
sex: "Women",
age: 24,
address: "Shanghai"
}
]
});
const value = ref("1");
const options = [
{
value: "1",
el: ".el-table",
label: "Element-Plus Table"
},
{
value: "2",
el: ".vxe-table",
label: "Vxe Table"
},
{
value: "3",
el: ".echart",
label: "Echart"
},
{
value: "4",
el: ".img",
label: "Image"
}
];
function onPrint() {
let el = options.filter(v => v.value === value.value)[0]?.el;
Print(el).toPrint;
}
const headerCellStyle: VxeTablePropTypes.HeaderCellStyle = ({ column }) => {
if (column.property === "name") {
return {
backgroundColor: "#f60",
color: "#ffffff"
};
}
};
const rowStyle: VxeTablePropTypes.RowStyle = ({ rowIndex }) => {
if ([2, 3, 5].includes(rowIndex)) {
return {
backgroundColor: "red",
color: "#ffffff"
};
}
};
const cellStyle: VxeTablePropTypes.CellStyle = ({ row, column }) => {
if (column.property === "sex") {
if (row.sex >= "1") {
return {
backgroundColor: "#187"
};
} else if (row.age === 26) {
return {
backgroundColor: "#2db7f5"
};
}
}
};
const tableRowClassName = ({ rowIndex }: { row: User; rowIndex: number }) => {
if (rowIndex === 1) {
return "warning-row";
} else if (rowIndex === 3) {
return "success-row";
}
return "";
};
const tableData: User[] = [
{
date: "2016-05-03",
name: "Tom",
address: "No. 189, Grove St, Los Angeles"
},
{
date: "2016-05-02",
name: "Tom",
address: "No. 189, Grove St, Los Angeles"
},
{
date: "2016-05-04",
name: "Tom",
address: "No. 189, Grove St, Los Angeles"
},
{
date: "2016-05-01",
name: "Tom",
address: "No. 189, Grove St, Los Angeles"
}
];
</script>
<template>
<el-card>
<template #header>
<div class="card-header">
<span class="font-medium">打印功能报表图表图片</span>
<div>
<el-select
v-model="value"
class="m-2"
placeholder="Select"
size="small"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
>
</el-option>
</el-select>
<el-button size="small" type="primary" @click="onPrint"
>打印</el-button
>
</div>
</div>
</template>
<el-row :gutter="24">
<el-col
:xs="22"
:sm="22"
:md="11"
:lg="11"
:xl="11"
style="margin: 10px; border: 0.01rem solid var(--el-color-primary)"
v-motion
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 200
}
}"
>
<p class="font-medium pt-1">Element-Plus Table</p>
<el-table
class="el-table"
:data="tableData"
border
style="margin: 40px auto; width: 100%"
:row-class-name="tableRowClassName"
>
<el-table-column prop="date" label="Date" width="180" />
<el-table-column prop="name" label="Name" width="180" />
<el-table-column prop="address" label="Address" />
</el-table>
</el-col>
<el-col
:xs="22"
:sm="22"
:md="11"
:lg="11"
:xl="11"
style="margin: 10px; border: 0.01rem solid var(--el-color-primary)"
v-motion
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 200
}
}"
>
<p class="font-medium pt-1">Vxe Table</p>
<vxe-table
class="vxe-table"
border
style="margin: 40px auto"
:header-cell-style="headerCellStyle"
:row-style="rowStyle"
:cell-style="cellStyle"
:data="demo1.tableData"
>
<vxe-column type="seq" width="60"></vxe-column>
<vxe-column field="name" title="Name"></vxe-column>
<vxe-column field="sex" title="Sex"></vxe-column>
<vxe-column field="age" title="Age"></vxe-column>
<vxe-column field="attr1" title="Attr1"></vxe-column>
<vxe-column
field="address"
title="Address"
show-overflow
></vxe-column>
</vxe-table>
</el-col>
<el-col
:xs="22"
:sm="22"
:md="11"
:lg="11"
:xl="11"
style="
width: 200px;
height: 300px;
margin: 10px;
border: 0.01rem solid var(--el-color-primary);
"
v-motion
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 200
}
}"
>
<p class="font-medium pt-1">Echart</p>
<ReLine class="echart" style="margin: 0 auto" />
</el-col>
<el-col
:xs="22"
:sm="22"
:md="11"
:lg="11"
:xl="11"
style="
width: 200px;
height: 300px;
margin: 10px;
border: 0.01rem solid var(--el-color-primary);
"
v-motion
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 200
}
}"
>
<p class="font-medium pt-1">Image</p>
<img
src="../../assets/avatars.jpg"
alt="avatars"
class="img"
style="width: 200px; height: 200px; margin: 50px auto"
/>
</el-col>
</el-row>
</el-card>
</template>
<style lang="scss" scoped>
:deep(.el-table__row.warning-row) {
--el-table-tr-bg-color: var(--el-color-warning-light-9);
}
:deep(.el-table__row.success-row) {
--el-table-tr-bg-color: var(--el-color-success-light-9);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import { ref } from "vue";
import { useWatermark } from "/@/utils/watermark";
let color = ref("#409EFF");
let value = ref("vue-pure-admin");
const { setWatermark, clear } = useWatermark();
</script>
<template>
<el-card>
<template #header>
<div class="card-header">
<span class="font-medium">页面水印功能</span>
</div>
</template>
<span>请输入要创建水印的值</span
><el-input
class="mb-4 mr-4"
style="width: 200px"
v-model="value"
clearable
></el-input>
<span>请选择要创建水印的颜色</span
><el-color-picker v-model="color" show-alpha />
<br />
<el-button @click="setWatermark(value, { fillStyle: color })"
>创建</el-button
>
<el-button @click="clear">清除</el-button>
</el-card>
</template>
<style scoped></style>

136
src/views/about.vue Normal file
View File

@@ -0,0 +1,136 @@
<script setup lang="ts">
export interface schemaItem {
field: string;
label: string;
}
// eslint-disable-next-line no-undef
const { pkg, lastBuildTime } = __APP_INFO__;
const { dependencies, devDependencies, version } = pkg;
const schema: schemaItem[] = [];
const devSchema: schemaItem[] = [];
Object.keys(dependencies).forEach(key => {
schema.push({ field: dependencies[key], label: key });
});
Object.keys(devDependencies).forEach(key => {
devSchema.push({ field: devDependencies[key], label: key });
});
</script>
<template>
<div>
<el-card class="box-card mb-4" shadow="never">
<template #header>
<div class="card-header">
<span class="font-medium">关于</span>
</div>
</template>
<span style="font-size: 15px">
Pure-Admin 是一个基于Vue3Vite2TypeScriptElement-Plus
的中后台解决方案它可以帮助您快速搭建企业级中后台提供现成的开箱解决方案及丰富的示例原则上不收取任何费用及版权可以放心使用不过如需二次开源比如用此平台二次开发并开源请联系作者获取许可
</span>
</el-card>
<el-card class="box-card m-4" shadow="hover">
<template #header>
<div class="card-header">
<span class="font-medium">项目信息</span>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="版本" label-align="left" align="left">
<el-tag>{{ version }}</el-tag>
</el-descriptions-item>
<el-descriptions-item
label="最后编译时间"
label-align="left"
align="left"
>
<el-tag>{{ lastBuildTime }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="文档地址" label-align="left" align="left">
<a href="https://pure-admin-doc.vercel.app" target="_blank">
<span style="color: var(--el-color-primary)">文档地址</span>
</a>
</el-descriptions-item>
<el-descriptions-item label="预览地址" label-align="left" align="left">
<a href="https://vue-pure-admin.vercel.app" target="_blank">
<span style="color: var(--el-color-primary)">预览地址</span>
</a>
</el-descriptions-item>
<el-descriptions-item label="Github" label-align="left" align="left">
<a
href="https://github.com/xiaoxian521/vue-pure-admin"
target="_blank"
>
<span style="color: var(--el-color-primary)">Github</span>
</a>
</el-descriptions-item>
<el-descriptions-item label="QQ交流群" label-align="left" align="left">
<a href="https://jq.qq.com/?_wv=1027&k=HntMx0dt" target="_blank"
><span style="color: var(--el-color-primary)"
>点击链接加入群聊Pure Admin</span
></a
>
</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card class="box-card m-4" shadow="hover">
<template #header>
<div class="card-header">
<span class="font-medium">生产环境依赖</span>
</div>
</template>
<el-descriptions border>
<el-descriptions-item
:label="item.label"
label-align="left"
align="left"
v-for="(item, index) in schema"
:key="index"
>
<a
:href="'https://www.npmjs.com/package/' + item.label"
target="_blank"
>
<span style="color: var(--el-color-primary)">{{ item.field }}</span>
</a>
</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card class="box-card m-4" shadow="hover">
<template #header>
<div class="card-header">
<span class="font-medium">开发环境依赖</span>
</div>
</template>
<el-descriptions border>
<el-descriptions-item
:label="item.label"
label-align="left"
align="left"
v-for="(item, index) in devSchema"
:key="index"
>
<a
:href="'https://www.npmjs.com/package/' + item.label"
target="_blank"
>
<span style="color: var(--el-color-primary)">{{ item.field }}</span>
</a>
</el-descriptions-item>
</el-descriptions>
</el-card>
</div>
</template>
<style lang="scss" scoped>
.main-content {
margin: 0 !important;
}
</style>

View File

@@ -7,13 +7,19 @@ const url = ref(`${VITE_PUBLIC_PATH}html/button.html`);
</script>
<template>
<iframe :src="url" frameborder="0" class="iframe"></iframe>
<el-card>
<template #header>
<div class="card-header">
<span class="font-medium">通过iframe引入按钮页面</span>
</div>
</template>
<iframe :src="url" frameborder="0" class="iframe"></iframe>
</el-card>
</template>
<style scoped>
.iframe {
width: 98%;
height: 90vh;
border-radius: 6px;
width: 100%;
height: 60vh;
}
</style>

View File

@@ -5,7 +5,12 @@ import menuDynamic from "./menuDynamic.vue";
</script>
<template>
<div>
<el-card>
<template #header>
<div class="card-header">
<span class="font-medium">右键菜单组件</span>
</div>
</template>
<el-row :gutter="24">
<el-col :xs="24" :sm="10" :md="10" :lg="8" :xl="10">
<!-- 基本使用 -->
@@ -20,5 +25,5 @@ import menuDynamic from "./menuDynamic.vue";
<menuDynamic />
</el-col>
</el-row>
</div>
</el-card>
</template>

View File

@@ -3,35 +3,32 @@ import { ReNormalCountTo, ReboundCountTo } from "/@/components/ReCountTo";
</script>
<template>
<div>
<el-row :gutter="24">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-card shadow="always">
<ReNormalCountTo
prefix="$"
:duration="1000"
:color="'#409EFF'"
:fontSize="'2.3em'"
:startVal="1"
:endVal="1000"
/>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-card shadow="always">
<ul class="flex">
<ReboundCountTo
v-for="(num, inx) of [1, 6, 6, 6]"
:key="inx"
:i="num"
:blur="inx"
:delay="inx + 1"
/>
</ul>
</el-card>
</el-col>
</el-row>
</div>
<el-card>
<template #header>
<div class="card-header">
<span class="font-medium">数字动画组件</span>
</div>
</template>
<div class="flex items-center">
<ReNormalCountTo
prefix="$"
:duration="1000"
:color="'#409EFF'"
:fontSize="'2em'"
:startVal="1"
:endVal="1000"
/>
<ul class="flex ml-8">
<ReboundCountTo
v-for="(num, inx) of [1, 6, 6, 6]"
:key="inx"
:i="num"
:blur="inx"
:delay="inx + 1"
/>
</ul>
</div>
</el-card>
</template>
<style lang="scss" scoped>

View File

@@ -3,9 +3,9 @@ import { ref, nextTick, getCurrentInstance } from "vue";
import Cropper from "/@/components/ReCropper";
import img from "./picture.jpeg";
const instance = getCurrentInstance();
let info = ref<object>(null);
let cropperImg = ref<string>("");
const instance = getCurrentInstance();
const onCropper = (): void => {
nextTick(() => {
@@ -25,14 +25,19 @@ const onCropper = (): void => {
</script>
<template>
<div>
<el-card>
<template #header>
<div class="card-header">
<span class="font-medium">图片裁剪组件</span>
</div>
</template>
<div class="cropper-container">
<Cropper ref="refCropper" :width="'40vw'" :src="img" />
<img :src="cropperImg" class="croppered" v-if="cropperImg" />
</div>
<el-button type="primary" @click="onCropper">裁剪</el-button>
<p v-if="cropperImg">裁剪后图片信息{{ info }}</p>
</div>
</el-card>
</template>
<style scoped>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import draggable from "vuedraggable/src/vuedraggable";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
let gridLists = ref<Array<Object>>([
{ grid: "cn", num: 1 },
@@ -47,77 +48,92 @@ onMounted(() => {
</script>
<template>
<div class="drag-container">
<!-- grid列表拖拽 -->
<el-row :gutter="25">
<el-col :xs="25" :sm="8" :md="8" :lg="8">
<el-card>
<template #header>
<div class="card-header">
<span>grid列表拖拽</span>
</div>
</template>
<draggable
v-model="gridLists"
class="grid-container"
item-key="grid"
animation="300"
chosenClass="chosen"
forceFallback="true"
>
<template #item="{ element }">
<div :class="'item' + ' ' + 'item-' + element.num">
{{ element.num }}
<el-card>
<template #header>
<div class="card-header">
<span
>拖拽组件采用开源的<el-link
href="https://sortablejs.github.io/vue.draggable.next/#/simple"
target="_blank"
:icon="useRenderIcon('rank')"
style="font-size: 16px; margin: 0 4px 5px"
>vuedraggable</el-link
></span
>
</div>
</template>
<div class="drag-container">
<!-- grid列表拖拽 -->
<el-row :gutter="25">
<el-col :xs="25" :sm="8" :md="8" :lg="8">
<el-card>
<template #header>
<div class="card-header">
<span>grid列表拖拽</span>
</div>
</template>
</draggable>
</el-card>
</el-col>
<el-col :xs="25" :sm="8" :md="8" :lg="8">
<el-card>
<template #header>
<div class="card-header">
<span>单列拖拽</span>
</div>
</template>
<!-- 单列拖拽 -->
<draggable
v-model="lists"
item-key="name"
@change="change"
chosen-class="chosen"
force-fallback="true"
animation="300"
>
<template #item="{ element, index }">
<div class="item-single">{{ element.name }} {{ index }}</div>
</template>
</draggable>
</el-card>
</el-col>
<el-col :xs="25" :sm="8" :md="8" :lg="8">
<el-card>
<template #header>
<div class="card-header">
<span>拖拽实现元素位置切换</span>
</div>
</template>
<!-- 拖拽实现元素位置切换 -->
<div class="cut-container">
<div
class="item-cut"
v-for="(item, index) in cutLists"
:key="index"
<draggable
v-model="gridLists"
class="grid-container"
item-key="grid"
animation="300"
chosenClass="chosen"
forceFallback="true"
>
<p>{{ item.name }}</p>
<template #item="{ element }">
<div :class="'item' + ' ' + 'item-' + element.num">
{{ element.num }}
</div>
</template>
</draggable>
</el-card>
</el-col>
<el-col :xs="25" :sm="8" :md="8" :lg="8">
<el-card>
<template #header>
<div class="card-header">
<span>单列拖拽</span>
</div>
</template>
<!-- 单列拖拽 -->
<draggable
v-model="lists"
item-key="name"
@change="change"
chosen-class="chosen"
force-fallback="true"
animation="300"
>
<template #item="{ element, index }">
<div class="item-single">{{ element.name }} {{ index }}</div>
</template>
</draggable>
</el-card>
</el-col>
<el-col :xs="25" :sm="8" :md="8" :lg="8">
<el-card>
<template #header>
<div class="card-header">
<span>拖拽实现元素位置切换</span>
</div>
</template>
<!-- 拖拽实现元素位置切换 -->
<div class="cut-container">
<div
class="item-cut"
v-for="(item, index) in cutLists"
:key="index"
>
<p>{{ item.name }}</p>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</el-card>
</el-col>
</el-row>
</div>
</el-card>
</template>
<style lang="scss" scoped>
@@ -140,6 +156,7 @@ onMounted(() => {
.item-cut {
font-size: 1.5em;
height: 77px;
line-height: 77px;
text-align: center;
border: 1px solid #e5e4e9;
cursor: move;

View File

@@ -8,6 +8,6 @@ import { Amap } from "/@/components/ReMap";
<style scoped>
.main-content {
margin: 0;
margin: 0 !important;
}
</style>

View File

@@ -16,27 +16,34 @@ const settingTB: ContextProps = reactive({
</script>
<template>
<div class="split-pane">
<splitpane :splitSet="settingLR">
<!-- #paneL 表示指定该组件为左侧面板 -->
<template #paneL>
<!-- 自定义左侧面板的内容 -->
<div class="dv-a">A</div>
</template>
<!-- #paneR 表示指定该组件为右侧面板 -->
<template #paneR>
<!-- 再次将右侧面板进行拆分 -->
<splitpane :splitSet="settingTB">
<template #paneL>
<div class="dv-b">B</div>
</template>
<template #paneR>
<div class="dv-c">C</div>
</template>
</splitpane>
</template>
</splitpane>
</div>
<el-card>
<template #header>
<div class="card-header">
<span class="font-medium">切割面板组件</span>
</div>
</template>
<div class="split-pane">
<splitpane :splitSet="settingLR">
<!-- #paneL 表示指定该组件为左侧面板 -->
<template #paneL>
<!-- 自定义左侧面板的内容 -->
<div class="dv-a">A</div>
</template>
<!-- #paneR 表示指定该组件为右侧面板 -->
<template #paneR>
<!-- 再次将右侧面板进行拆分 -->
<splitpane :splitSet="settingTB">
<template #paneL>
<div class="dv-b">B</div>
</template>
<template #paneR>
<div class="dv-c">C</div>
</template>
</splitpane>
</template>
</splitpane>
</div>
</el-card>
</template>
<style lang="scss" scoped>

View File

@@ -2,13 +2,16 @@
import { onMounted } from "vue";
import Player from "xgplayer/dist/simple_player";
import volume from "xgplayer/es/controls/volume";
import playbackRate from "xgplayer/es/controls/playbackRate";
import screenShot from "xgplayer/es/controls/screenShot";
import { deviceDetection } from "/@/utils/deviceDetection";
import playbackRate from "xgplayer/es/controls/playbackRate";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
onMounted(() => {
new Player({
id: "mse",
// 默认静音
volume: 0,
autoplay: false,
screenShot: true,
url: "https://sf1-hscdn-tos.pstatp.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-720p.mp4",
@@ -23,7 +26,22 @@ onMounted(() => {
</script>
<template>
<div id="mse"></div>
<el-card>
<template #header>
<div class="card-header">
<span class="font-medium"
>视频组件采用开源的<el-link
href="https://v2.h5player.bytedance.com"
target="_blank"
:icon="useRenderIcon('video-play')"
style="font-size: 16px; margin: 0 4px 5px"
>西瓜播放器</el-link
></span
>
</div>
</template>
<div id="mse"></div>
</el-card>
</template>
<style scoped>

View File

@@ -5,12 +5,12 @@ export default {
</script>
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref, unref } from "vue";
import WangEditor from "wangeditor";
import { onMounted, onBeforeUnmount, ref, unref } from "vue";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
// eslint-disable-next-line no-undef
const editor = ref(null);
const html = ref(null);
const editor = ref(null);
let instance: WangEditor;
onMounted(() => {
@@ -29,17 +29,26 @@ onBeforeUnmount(() => {
</script>
<template>
<div>
<el-card>
<template #header>
<div class="card-header">
<span class="font-medium"
>编辑器组件采用开源的<el-link
href="https://www.wangeditor.com"
target="_blank"
:icon="useRenderIcon('edit')"
style="font-size: 16px; margin: 0 4px 5px"
>wangeditor</el-link
></span
>
</div>
</template>
<div ref="editor"></div>
<div :innerHTML="html"></div>
</div>
</el-card>
</template>
<style lang="scss" scoped>
.main-content {
margin: 40px;
}
:deep(.w-e-text-container) {
z-index: 99 !important;
}

View File

@@ -1,13 +1,15 @@
<script setup lang="ts">
import { ref, unref, onMounted } from "vue";
import LogicFlow from "@logicflow/core";
import { Snapshot, BpmnElement, Menu } from "@logicflow/extension";
import demoData from "./dataTurbo.json";
import "@logicflow/core/dist/style/index.css";
import "@logicflow/extension/lib/style/index.css";
import LogicFlow from "@logicflow/core";
import { ref, unref, onMounted } from "vue";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
import { BpmnNode } from "/@/components/ReFlowChart/src/config";
import { Snapshot, BpmnElement, Menu } from "@logicflow/extension";
import { Control, NodePanel, DataDialog } from "/@/components/ReFlowChart";
import { toLogicflowData } from "/@/components/ReFlowChart/src/adpterForTurbo";
import { BpmnNode } from "/@/components/ReFlowChart/src/config";
import demoData from "./dataTurbo.json";
let lf = ref(null);
let graphData = ref(null);
@@ -57,37 +59,52 @@ onMounted(() => {
</script>
<template>
<div class="logic-flow-view">
<!-- 辅助工具栏 -->
<Control
class="demo-control"
v-if="lf"
:lf="lf"
:catTurboData="false"
@catData="catData"
></Control>
<!-- 节点面板 -->
<NodePanel :lf="lf" :nodeList="nodeList"></NodePanel>
<!-- 画布 -->
<div id="LF-Turbo"></div>
<!-- 数据查看面板 -->
<el-dialog
customClass="flow-dialog"
title="数据"
v-model="dataVisible"
width="50%"
>
<el-scrollbar>
<DataDialog :graphData="graphData"></DataDialog>
</el-scrollbar>
</el-dialog>
</div>
<el-card>
<template #header>
<div class="card-header">
<span class="font-medium"
>流程图组件采用开源的<el-link
href="http://logic-flow.org/"
target="_blank"
:icon="useRenderIcon('set-up')"
style="font-size: 16px; margin: 0 4px 5px"
>LogicFlow</el-link
></span
>
</div>
</template>
<div class="logic-flow-view">
<!-- 辅助工具栏 -->
<Control
class="demo-control"
v-if="lf"
:lf="lf"
:catTurboData="false"
@catData="catData"
></Control>
<!-- 节点面板 -->
<NodePanel :lf="lf" :nodeList="nodeList"></NodePanel>
<!-- 画布 -->
<div id="LF-Turbo"></div>
<!-- 数据查看面板 -->
<el-dialog
customClass="flow-dialog"
title="数据"
v-model="dataVisible"
width="50%"
>
<el-scrollbar>
<DataDialog :graphData="graphData"></DataDialog>
</el-scrollbar>
</el-dialog>
</div>
</el-card>
</template>
<style scoped>
#LF-Turbo {
width: 100%;
height: calc(100vh - 90px);
height: 70vh;
}
.logic-flow-view {
@@ -137,8 +154,4 @@ onMounted(() => {
height: 85vh;
overflow: auto;
}
.main-content {
margin: 0;
}
</style>

View File

@@ -1,14 +1,7 @@
<script lang="ts">
export default {
name: "reGuide"
};
</script>
<script setup lang="ts">
import Driver from "driver.js";
import "driver.js/dist/driver.min.css";
// 步骤
const steps = [
{
element: "#header-notice",
@@ -72,8 +65,14 @@ const guide = () => {
</script>
<template>
<div>
<p>引导页对于一些第一次进入项目的人很有用你可以简单介绍下项目的功能</p>
<el-card>
<template #header>
<div class="card-header">
<span class="font-medium"
>引导页对于一些第一次进入项目的人很有用你可以简单介绍下项目的功能</span
>
</div>
</template>
<el-button
type="primary"
style="margin-top: 10px"
@@ -81,7 +80,7 @@ const guide = () => {
>
打开引导页
</el-button>
</div>
</el-card>
</template>
<style>

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