Compare commits

...

4 Commits

Author SHA1 Message Date
xiaoxian521
f0ff132561 release: update 6.2.0 2025-10-30 09:42:46 +08:00
xiaoxian521
6106ebf2cc release: update 6.1.0 2025-08-07 15:50:34 +08:00
xiaoxian521
df315bb7d2 docs: update README 2025-05-27 18:51:15 +08:00
xiaoxian521
d2543cf9d3 release: update 6.0.0 2025-04-25 09:50:52 +08:00
88 changed files with 3769 additions and 3299 deletions

2
.nvmrc
View File

@@ -1 +1 @@
v22.12.0
v22.20.0

View File

@@ -1,4 +1,5 @@
{
"tailwindCSS.experimental.configFile": "src/style/tailwind.css",
"editor.formatOnType": true,
"editor.formatOnSave": true,
"[vue]": {

View File

@@ -8,14 +8,6 @@
The simplified version is based on the shelf extracted from [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin), which contains main functions and is more suitable for actual project development. The packaged size is introduced globally [element-plus](https://element-plus.org) is still below `2.3MB`, and the full version of the code will be permanently synchronized. After enabling `brotli` compression and `cdn` to replace the local library mode, the package size is less than `350kb`
## `js` version
[Click me to view js version](https://pure-admin.cn/pages/js/)
## `max` version
[Click me to view the max version](https://pure-admin.cn/pages/max/)
## Supporting video
[Click me to view UI design](https://www.bilibili.com/video/BV17g411T7rq)
@@ -26,7 +18,7 @@ The simplified version is based on the shelf extracted from [vue-pure-admin](htt
[Click me to view vue-pure-admin documentation](https://pure-admin.cn/)
[Click me to view @pureadmin/utils documentation](https://pure-admin-utils.netlify.app)
## Quality service, software outsourcing, sponsorship support
## Premium service
[Click me to view details](https://pure-admin.cn/pages/service/)

View File

@@ -12,14 +12,6 @@
当前是非国际化版本,如果您需要国际化版本 [请点击](https://github.com/pure-admin/pure-admin-thin/tree/i18n)
## `js` 版本
[点我查看 js 版本](https://pure-admin.cn/pages/js/)
## `max` 版本
[点我查看 max 版本](https://pure-admin.cn/pages/max/)
## 配套视频
[点我查看 UI 设计](https://www.bilibili.com/video/BV17g411T7rq)
@@ -30,7 +22,7 @@
[点我查看 vue-pure-admin 文档](https://pure-admin.cn/)
[点我查看 @pureadmin/utils 文档](https://pure-admin-utils.netlify.app)
## 优质服务、软件外包、赞助支持
## 高级服务
[点我查看详情](https://pure-admin.cn/pages/service/)

View File

@@ -22,8 +22,8 @@ const include = [
/**
* 在预构建中强制排除的依赖项
* 温馨提示:所有以 `@iconify-icons/` 开头引入的的本地图标模块,都应该加入到下面的 `exclude` 里,因为平台推荐的使用方式是哪里需要哪里引入而且都是单个的引入,不需要预构建,直接让浏览器加载就好
* 温馨提示:平台推荐的使用方式是哪里需要哪里引入而且都是单个的引入,不需要预构建,直接让浏览器加载就好
*/
const exclude = ["@iconify-icons/ep", "@iconify-icons/ri"];
const exclude = ["@iconify/json"];
export { include, exclude };

View File

@@ -2,8 +2,10 @@ import { cdn } from "./cdn";
import vue from "@vitejs/plugin-vue";
import { viteBuildInfo } from "./info";
import svgLoader from "vite-svg-loader";
import Icons from "unplugin-icons/vite";
import type { PluginOption } from "vite";
import vueJsx from "@vitejs/plugin-vue-jsx";
import tailwindcss from "@tailwindcss/vite";
import { configCompressPlugin } from "./compress";
import removeNoMatch from "vite-plugin-router-warn";
import { visualizer } from "rollup-plugin-visualizer";
@@ -17,6 +19,7 @@ export function getPluginsList(
): PluginOption[] {
const lifecycle = process.env.npm_lifecycle_event;
return [
tailwindcss(),
vue(),
// jsx、tsx语法支持
vueJsx(),
@@ -46,6 +49,11 @@ export function getPluginsList(
}),
// svg组件化支持
svgLoader(),
// 自动按需加载图标
Icons({
compiler: "vue3",
scale: 1
}),
VITE_CDN ? cdn : null,
configCompressPlugin(VITE_COMPRESSION),
// 线上环境删除console

View File

@@ -1,26 +1,25 @@
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginVue from "eslint-plugin-vue";
import * as parserVue from "vue-eslint-parser";
import configPrettier from "eslint-config-prettier";
import pluginPrettier from "eslint-plugin-prettier";
import { defineFlatConfig } from "eslint-define-config";
import * as parserTypeScript from "@typescript-eslint/parser";
import pluginTypeScript from "@typescript-eslint/eslint-plugin";
import { defineConfig, globalIgnores } from "eslint/config";
export default defineFlatConfig([
{
...js.configs.recommended,
ignores: [
export default defineConfig([
globalIgnores([
"**/.*",
"dist/*",
"*.d.ts",
"public/*",
"src/assets/**",
"src/**/iconfont/**"
],
]),
{
...js.configs.recommended,
languageOptions: {
globals: {
// index.d.ts
// types/index.d.ts
RefType: "readonly",
EmitType: "readonly",
TargetContext: "readonly",
@@ -73,21 +72,10 @@ export default defineFlatConfig([
]
}
},
{
...tseslint.config({
extends: [...tseslint.configs.recommended],
files: ["**/*.?([cm])ts", "**/*.?([cm])tsx"],
languageOptions: {
parser: parserTypeScript,
parserOptions: {
sourceType: "module",
warnOnUnsupportedTypeScriptVersion: false
}
},
plugins: {
"@typescript-eslint": pluginTypeScript
},
rules: {
...pluginTypeScript.configs.strict.rules,
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-redeclare": "error",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-explicit-any": "off",
@@ -114,20 +102,20 @@ export default defineFlatConfig([
}
]
}
},
}),
{
files: ["**/*.d.ts"],
rules: {
"eslint-comments/no-unlimited-disable": "off",
"import/no-duplicates": "off",
"no-restricted-syntax": "off",
"unused-imports/no-unused-vars": "off"
}
},
{
files: ["**/*.?([cm])js"],
rules: {
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-var-requires": "off"
"@typescript-eslint/no-require-imports": "off"
}
},
{
@@ -148,18 +136,19 @@ export default defineFlatConfig([
jsx: true
},
extraFileExtensions: [".vue"],
parser: "@typescript-eslint/parser",
parser: tseslint.parser,
sourceType: "module"
}
},
plugins: {
"@typescript-eslint": tseslint.plugin,
vue: pluginVue
},
processor: pluginVue.processors[".vue"],
rules: {
...pluginVue.configs.base.rules,
...pluginVue.configs["vue3-essential"].rules,
...pluginVue.configs["vue3-recommended"].rules,
...pluginVue.configs.essential.rules,
...pluginVue.configs.recommended.rules,
"no-undef": "off",
"no-unused-vars": "off",
"vue/no-v-html": "off",

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@@ -10,9 +10,6 @@
/>
<title>pure-admin-thin</title>
<link rel="icon" href="/favicon.ico" />
<script>
window.process = {};
</script>
</head>
<body>

View File

@@ -1,6 +1,6 @@
{
"name": "pure-admin-thin",
"version": "5.9.0",
"version": "6.2.0",
"private": true,
"type": "module",
"scripts": {
@@ -49,95 +49,94 @@
},
"dependencies": {
"@pureadmin/descriptions": "^1.2.1",
"@pureadmin/table": "^3.2.1",
"@pureadmin/utils": "^2.5.0",
"@vueuse/core": "^12.0.0",
"@vueuse/motion": "^2.2.6",
"@pureadmin/table": "^3.3.0",
"@pureadmin/utils": "^2.6.2",
"@vueuse/core": "^14.0.0",
"@vueuse/motion": "^3.0.3",
"animate.css": "^4.1.1",
"axios": "^1.7.9",
"dayjs": "^1.11.13",
"echarts": "^5.5.1",
"element-plus": "^2.9.0",
"axios": "^1.12.2",
"dayjs": "^1.11.18",
"echarts": "^6.0.0",
"element-plus": "^2.11.5",
"js-cookie": "^3.0.5",
"localforage": "^1.10.0",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
"pinia": "^2.3.0",
"pinyin-pro": "^3.26.0",
"qs": "^6.13.1",
"pinia": "^3.0.3",
"pinyin-pro": "^3.27.0",
"qs": "^6.14.0",
"responsive-storage": "^2.2.0",
"sortablejs": "^1.15.6",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-tippy": "^6.5.0",
"vue-types": "^5.1.3"
"vue": "^3.5.22",
"vue-router": "^4.6.3",
"vue-tippy": "^6.7.1",
"vue-types": "^6.0.0"
},
"devDependencies": {
"@commitlint/cli": "^19.6.0",
"@commitlint/config-conventional": "^19.6.0",
"@commitlint/types": "^19.5.0",
"@eslint/js": "^9.16.0",
"@faker-js/faker": "^9.3.0",
"@iconify-icons/ep": "^1.2.12",
"@iconify-icons/ri": "^1.2.10",
"@iconify/vue": "^4.2.0",
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@commitlint/types": "^20.0.0",
"@eslint/js": "^9.38.0",
"@faker-js/faker": "^10.1.0",
"@iconify/json": "^2.2.400",
"@iconify/vue": "4.2.0",
"@tailwindcss/vite": "^4.1.16",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20.17.9",
"@types/node": "^20.19.23",
"@types/nprogress": "^0.2.3",
"@types/path-browserify": "^1.0.3",
"@types/qs": "^6.9.17",
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"autoprefixer": "^10.4.20",
"@types/qs": "^6.14.0",
"@types/sortablejs": "^1.15.9",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.1",
"boxen": "^8.0.1",
"code-inspector-plugin": "^0.18.2",
"cssnano": "^7.0.6",
"eslint": "^9.16.0",
"eslint-config-prettier": "^9.1.0",
"eslint-define-config": "^2.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.32.0",
"code-inspector-plugin": "^1.2.10",
"cssnano": "^7.1.1",
"eslint": "^9.38.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-vue": "^10.5.1",
"gradient-string": "^3.0.0",
"husky": "^9.1.7",
"lint-staged": "^15.2.10",
"postcss": "^8.4.49",
"postcss-html": "^1.7.0",
"postcss-import": "^16.1.0",
"lint-staged": "^16.2.6",
"postcss": "^8.5.6",
"postcss-html": "^1.8.0",
"postcss-load-config": "^6.0.1",
"postcss-scss": "^4.0.9",
"prettier": "^3.4.2",
"prettier": "^3.6.2",
"rimraf": "^6.0.1",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.82.0",
"stylelint": "^16.11.0",
"stylelint-config-recess-order": "^5.1.1",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-standard-scss": "^13.1.0",
"stylelint-prettier": "^5.0.2",
"svgo": "^3.3.2",
"tailwindcss": "^3.4.16",
"typescript": "5.6.3",
"vite": "^6.0.3",
"rollup-plugin-visualizer": "^6.0.5",
"sass": "^1.93.2",
"stylelint": "^16.25.0",
"stylelint-config-recess-order": "^7.4.0",
"stylelint-config-recommended-vue": "^1.6.1",
"stylelint-config-standard-scss": "^14.0.0",
"stylelint-prettier": "^5.0.3",
"svgo": "^4.0.0",
"tailwindcss": "^4.1.16",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.2",
"unplugin-icons": "^22.5.0",
"vite": "^7.1.12",
"vite-plugin-cdn-import": "^1.0.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-fake-server": "^2.1.4",
"vite-plugin-fake-server": "^2.2.0",
"vite-plugin-remove-console": "^2.2.0",
"vite-plugin-router-warn": "^1.0.0",
"vite-svg-loader": "^5.1.0",
"vue-eslint-parser": "^9.4.3",
"vue-tsc": "^2.1.10"
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.1.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=22.0.0",
"node": "^20.19.0 || >=22.13.0",
"pnpm": ">=9"
},
"pnpm": {
"allowedDeprecatedVersions": {
"are-we-there-yet": "*",
"sourcemap-codec": "*",
"lodash.isequal": "*",
"domexception": "*",
"w3c-hr-time": "*",
"inflight": "*",
@@ -148,10 +147,16 @@
"abab": "*",
"glob": "*"
},
"peerDependencyRules": {
"allowedVersions": {
"eslint": "9"
}
}
"onlyBuiltDependencies": [
"@parcel/watcher",
"core-js",
"es5-ext",
"esbuild",
"typeit",
"vue-demi"
],
"ignoredBuiltDependencies": [
"@tailwindcss/oxide"
]
}
}

5936
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,6 @@
/** @type {import('postcss-load-config').Config} */
export default {
plugins: {
"postcss-import": {},
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {})
}
};

View File

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

File diff suppressed because one or more lines are too long

View File

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

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 332 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3.5 4H1V3h2V1h1v2.5zM13 3V1h-1v2.5l.5.5H15V3zm-1 9.5V15h1v-2h2v-1h-2.5zM1 12v1h2v2h1v-2.5l-.5-.5zm11-1.5-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5zM10 7H6v2h4z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3.5 4H1V3h2V1h1v2.5zM13 3V1h-1v2.5l.5.5H15V3zm-1 9.5V15h1v-2h2v-1h-2.5zM1 12v1h2v2h1v-2.5l-.5-.5zm11-1.5-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5zM10 7H6v2h4z"/></svg>

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 308 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3 12h10V4H3zm2-6h6v4H5zM2 6H1V2.5l.5-.5H5v1H2zm13-3.5V6h-1V3h-3V2h3.5zM14 10h1v3.5l-.5.5H11v-1h3zM2 13h3v1H1.5l-.5-.5V10h1z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3 12h10V4H3zm2-6h6v4H5zM2 6H1V2.5l.5-.5H5v1H2zm13-3.5V6h-1V3h-3V2h3.5zM14 10h1v3.5l-.5.5H11v-1h3zM2 13h3v1H1.5l-.5-.5V10h1z"/></svg>

Before

Width:  |  Height:  |  Size: 302 B

After

Width:  |  Height:  |  Size: 283 B

View File

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

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 360 B

View File

@@ -1 +1 @@
<svg width="32" height="32" fill="currentColor" aria-hidden="true" data-icon="holder" viewBox="64 64 896 896"><path d="M300 276.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97m0 284a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 228a56 56 0 1 0 112 0 56 56 0 0 0-112 0m0 284a56 56 0 1 0 112 0 56 56 0 0 0-112 0M300 844.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 796a56 56 0 1 0 112 0 56 56 0 0 0-112 0"/></svg>
<svg width="32" height="32" fill="currentColor" data-icon="holder" viewBox="64 64 896 896"><path d="M300 276.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97m0 284a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 228a56 56 0 1 0 112 0 56 56 0 0 0-112 0m0 284a56 56 0 1 0 112 0 56 56 0 0 0-112 0M300 844.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 796a56 56 0 1 0 112 0 56 56 0 0 0-112 0"/></svg>

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 373 B

View File

@@ -8,8 +8,8 @@ import {
} from "./index";
import { ref, computed } from "vue";
import { isFunction } from "@pureadmin/utils";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
import Fullscreen from "~icons/ri/fullscreen-fill";
import ExitFullscreen from "~icons/ri/fullscreen-exit-fill";
defineOptions({
name: "ReDialog"
@@ -79,7 +79,7 @@ const fullscreenClass = computed(() => {
"el-dialog__close",
"-translate-x-2",
"cursor-pointer",
"hover:!text-[red]"
"hover:text-[red]!"
];
});

View File

@@ -1,6 +1,6 @@
import type { iconType } from "./types";
import { h, defineComponent, type Component } from "vue";
import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
import { FontIcon, IconifyIconOnline, IconifyIconOffline } from "../index";
/**
* 支持 `iconfont`、自定义 `svg` 以及 `iconify` 中所有的图标
@@ -49,10 +49,12 @@ export function useRenderIcon(icon: any, attrs?: iconType): Component {
return defineComponent({
name: "Icon",
render() {
const IconifyIcon =
icon && icon.includes(":") ? IconifyIconOnline : IconifyIconOffline;
if (!icon) return;
const IconifyIcon = icon.includes(":")
? IconifyIconOnline
: IconifyIconOffline;
return h(IconifyIcon, {
icon: icon,
icon,
...attrs
});
}

View File

@@ -27,8 +27,7 @@ export default defineComponent({
return h(
"svg",
{
class: "icon-svg",
"aria-hidden": true
class: "icon-svg"
},
{
default: () => [

View File

@@ -13,10 +13,26 @@ export default defineComponent({
render() {
if (typeof this.icon === "object") addIcon(this.icon, this.icon);
const attrs = this.$attrs;
if (typeof this.icon === "string") {
return h(
IconifyIcon,
{
icon: this.icon,
"aria-hidden": false,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
...attrs
},
{
default: () => []
}
);
} else {
return h(
this.icon,
{
"aria-hidden": false,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
@@ -27,4 +43,5 @@ export default defineComponent({
}
);
}
}
});

View File

@@ -17,6 +17,7 @@ export default defineComponent({
IconifyIcon,
{
icon: `${this.icon}`,
"aria-hidden": false,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },

View File

@@ -1,14 +1,23 @@
// 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载
import { getSvgInfo } from "@pureadmin/utils";
import { addIcon } from "@iconify/vue/dist/offline";
// https://icon-sets.iconify.design/ep/?keyword=ep
import EpHomeFilled from "~icons/ep/home-filled?raw";
// https://icon-sets.iconify.design/ri/?keyword=ri
import RiSearchLine from "~icons/ri/search-line?raw";
import RiInformationLine from "~icons/ri/information-line?raw";
const icons = [
// Element Plus Icon: https://github.com/element-plus/element-plus-icons
["ep/home-filled", EpHomeFilled],
// Remix Icon: https://github.com/Remix-Design/RemixIcon
["ri/search-line", RiSearchLine],
["ri/information-line", RiInformationLine]
];
// 本地菜单图标,后端在路由的 icon 中返回对应的图标字符串并且前端在此处使用 addIcon 添加即可渲染菜单图标
// @iconify-icons/ep
import Lollipop from "@iconify-icons/ep/lollipop";
import HomeFilled from "@iconify-icons/ep/home-filled";
addIcon("ep:lollipop", Lollipop);
addIcon("ep:home-filled", HomeFilled);
// @iconify-icons/ri
import Search from "@iconify-icons/ri/search-line";
import InformationLine from "@iconify-icons/ri/information-line";
addIcon("ri:search-line", Search);
addIcon("ri:information-line", InformationLine);
icons.forEach(([name, icon]) => {
addIcon(name as string, getSvgInfo(icon as string));
});

View File

@@ -17,8 +17,8 @@ import {
getKeyList
} from "@pureadmin/utils";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
import Fullscreen from "~icons/ri/fullscreen-fill";
import ExitFullscreen from "~icons/ri/fullscreen-exit-fill";
import DragIcon from "@/assets/table-bar/drag.svg?component";
import ExpandIcon from "@/assets/table-bar/expand.svg?component";
import RefreshIcon from "@/assets/table-bar/refresh.svg?component";
@@ -86,9 +86,9 @@ export default defineComponent({
"text-black",
"dark:text-white",
"duration-100",
"hover:!text-primary",
"hover:text-primary!",
"cursor-pointer",
"outline-none"
"outline-hidden"
];
});
@@ -250,12 +250,12 @@ export default defineComponent({
<div
{...attrs}
class={[
"w-[99/100]",
"w-full",
"px-2",
"pb-2",
"bg-bg_color",
isFullscreen.value
? ["!w-full", "!h-full", "z-[2002]", "fixed", "inset-0"]
? ["h-full!", "z-2002", "fixed", "inset-0"]
: "mt-2"
]}
>
@@ -312,7 +312,7 @@ export default defineComponent({
>
<div class={[topClass.value]}>
<el-checkbox
class="!-mr-1"
class="-mr-1!"
label="列展示"
v-model={checkAll.value}
indeterminate={isIndeterminate.value}
@@ -342,8 +342,8 @@ export default defineComponent({
class={[
"drag-btn w-[16px] mr-2",
isFixedColumn(item)
? "!cursor-no-drop"
: "!cursor-grab"
? "cursor-no-drop!"
: "cursor-grab!"
]}
onMouseenter={(event: {
preventDefault: () => void;

View File

@@ -98,7 +98,6 @@
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
transition: 0.1s;
}
.pure-segmented-group {

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { h, onMounted, ref, useSlots } from "vue";
import { type TippyOptions, useTippy } from "vue-tippy";
<script setup lang="ts">
import { h, onMounted, ref } from "vue";
import { type TippyOptions, type TippyContent, useTippy } from "vue-tippy";
defineOptions({
name: "ReText"
@@ -17,7 +17,10 @@ const props = defineProps({
}
});
const $slots = useSlots();
const slots = defineSlots<{
content: () => TippyContent;
default: () => any;
}>();
const textRef = ref();
const tippyFunc = ref();
@@ -33,7 +36,7 @@ const isTextEllipsis = (el: HTMLElement) => {
};
const getTippyProps = () => ({
content: h($slots.content || $slots.default),
content: h(slots.content || slots.default),
...props.tippyProps
});

View File

@@ -13,10 +13,10 @@ $ripple-animation-visible-opacity: 0.25 !default;
z-index: 0;
width: 100%;
height: 100%;
contain: strict;
overflow: hidden;
pointer-events: none;
border-radius: inherit;
contain: strict;
}
&__animation {

View File

@@ -32,8 +32,8 @@ const calculate = (
const offset = el.getBoundingClientRect();
// 获取点击位置距离 el 的垂直和水平距离
let localX = e.clientX - offset.left;
let localY = e.clientY - offset.top;
const localX = e.clientX - offset.left;
const localY = e.clientY - offset.top;
let radius = 0;
let scale = 0.3;

View File

@@ -1,4 +1,4 @@
<script lang="ts" setup>
<script setup lang="ts">
import { getConfig } from "@/config";
const TITLE = getConfig("Title");
@@ -10,7 +10,7 @@ const TITLE = getConfig("Title");
>
Copyright © 2020-present
<a
class="hover:text-primary"
class="hover:text-primary!"
href="https://github.com/pure-admin"
target="_blank"
>

View File

@@ -7,8 +7,8 @@ import LaySidebarFullScreen from "../lay-sidebar/components/SidebarFullScreen.vu
import LaySidebarBreadCrumb from "../lay-sidebar/components/SidebarBreadCrumb.vue";
import LaySidebarTopCollapse from "../lay-sidebar/components/SidebarTopCollapse.vue";
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import Setting from "@iconify-icons/ri/settings-3-line";
import LogoutCircleRLine from "~icons/ri/logout-circle-r-line";
import Setting from "~icons/ri/settings-3-line";
const {
layout,
@@ -24,7 +24,7 @@ const {
</script>
<template>
<div class="navbar bg-[#fff] shadow-sm shadow-[rgba(0,21,41,0.08)]">
<div class="navbar bg-[#fff] shadow-xs shadow-[rgba(0,21,41,0.08)]">
<LaySidebarTopCollapse
v-if="device === 'mobile'"
class="hamburger-container"

View File

@@ -49,7 +49,7 @@ function hoverDescription(event, description) {
<template>
<div
class="notice-container border-b-[1px] border-solid border-[#f0f0f0] dark:border-[#303030]"
class="notice-container border-0 border-b-[1px] border-solid border-[#f0f0f0] dark:border-[#303030]"
>
<el-avatar
v-if="noticeItem.avatar"
@@ -112,7 +112,7 @@ function hoverDescription(event, description) {
max-width: 238px;
}
</style>
<style scoped lang="scss">
<style lang="scss" scoped>
.notice-container {
display: flex;
align-items: flex-start;

View File

@@ -2,7 +2,7 @@
import { ref, computed } from "vue";
import { noticesData } from "./data";
import NoticeList from "./components/NoticeList.vue";
import BellIcon from "@iconify-icons/ep/bell";
import BellIcon from "~icons/ep/bell";
const noticesNum = ref(0);
const notices = ref(noticesData);

View File

@@ -3,7 +3,7 @@ import { emitter } from "@/utils/mitt";
import { onClickOutside } from "@vueuse/core";
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import CloseIcon from "@iconify-icons/ep/close";
import CloseIcon from "~icons/ep/close";
const target = ref(null);
const show = ref<Boolean>(false);
@@ -15,7 +15,7 @@ const iconClass = computed(() => {
"flex",
"justify-center",
"items-center",
"outline-none",
"outline-hidden",
"rounded-[4px]",
"cursor-pointer",
"transition-colors",
@@ -49,7 +49,7 @@ onBeforeUnmount(() => {
<div class="right-panel-background" />
<div ref="target" class="right-panel bg-bg_color">
<div
class="project-configuration border-b-[1px] border-solid border-[var(--pure-border-color)]"
class="project-configuration border-0 border-b-[1px] border-solid border-[var(--pure-border-color)]"
>
<h4 class="dark:text-white">系统配置</h4>
<span
@@ -74,7 +74,7 @@ onBeforeUnmount(() => {
</el-scrollbar>
<div
class="flex justify-end p-3 border-t-[1px] border-solid border-[var(--pure-border-color)]"
class="flex justify-end p-3 border-0 border-t-[1px] border-solid border-[var(--pure-border-color)]"
>
<el-button
v-tippy="{
@@ -117,8 +117,8 @@ onBeforeUnmount(() => {
width: 100%;
max-width: 280px;
box-shadow: 0 0 15px 0 rgb(0 0 0 / 5%);
transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
transform: translate(100%);
transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
}
.show {

View File

@@ -2,10 +2,10 @@
import { useNav } from "@/layout/hooks/useNav";
import MdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
import EnterOutlined from "@/assets/svg/enter_outlined.svg?component";
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
import ArrowUpLine from "~icons/ri/arrow-up-line";
import ArrowDownLine from "~icons/ri/arrow-down-line";
withDefaults(defineProps<{ total: number }>(), {
withDefaults(defineProps<{ total?: number }>(), {
total: 0
});

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { optionsItem } from "../types";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import StarIcon from "@iconify-icons/ep/star";
import CloseIcon from "@iconify-icons/ep/close";
import StarIcon from "~icons/ep/star";
import CloseIcon from "~icons/ep/close";
interface Props {
item: optionsItem;

View File

@@ -11,7 +11,7 @@ import { ref, computed, shallowRef, watch } from "vue";
import { useDebounceFn, onKeyStroke } from "@vueuse/core";
import { usePermissionStoreHook } from "@/store/modules/permission";
import { cloneDeep, isAllEmpty, storageLocal } from "@pureadmin/utils";
import SearchIcon from "@iconify-icons/ri/search-line";
import SearchIcon from "~icons/ri/search-line";
interface Props {
/** 弹窗显隐 */

View File

@@ -14,7 +14,7 @@ function handleSearch() {
class="search-container w-[40px] h-[48px] flex-c cursor-pointer navbar-bg-hover"
@click="handleSearch"
>
<IconifyIconOffline icon="ri:search-line" />
<IconifyIconOffline icon="ri/search-line" />
</div>
<SearchModal v-model:value="show" />
</div>

View File

@@ -18,9 +18,9 @@ import Segmented, { type OptionsType } from "@/components/ReSegmented";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import { useDark, useGlobal, debounce, isNumber } from "@pureadmin/utils";
import Check from "@iconify-icons/ep/check";
import LeftArrow from "@iconify-icons/ri/arrow-left-s-line";
import RightArrow from "@iconify-icons/ri/arrow-right-s-line";
import Check from "~icons/ep/check";
import LeftArrow from "~icons/ri/arrow-left-s-line?width=20&height=20";
import RightArrow from "~icons/ri/arrow-right-s-line?width=20&height=20";
import DayIcon from "@/assets/svg/day.svg?component";
import DarkIcon from "@/assets/svg/dark.svg?component";
import SystemIcon from "@/assets/svg/system.svg?component";
@@ -187,7 +187,7 @@ const getThemeColor = computed(() => {
});
const pClass = computed(() => {
return ["mb-[12px]", "font-medium", "text-sm", "dark:text-white"];
return ["mb-[12px]!", "font-medium", "text-sm", "dark:text-white"];
});
const themeOptions = computed<Array<OptionsType>>(() => {
@@ -333,7 +333,7 @@ onUnmounted(() => removeMatchMedia);
"
/>
<p :class="['mt-5', pClass]">主题色</p>
<p :class="['mt-5!', pClass]">主题色</p>
<ul class="theme-color">
<li
v-for="(item, index) in themeColors"
@@ -352,7 +352,7 @@ onUnmounted(() => removeMatchMedia);
</li>
</ul>
<p :class="['mt-5', pClass]">导航模式</p>
<p :class="['mt-5!', pClass]">导航模式</p>
<ul class="pure-theme">
<li
ref="verticalRef"
@@ -395,7 +395,7 @@ onUnmounted(() => removeMatchMedia);
</ul>
<span v-if="useAppStoreHook().getViewportWidth > 1280">
<p :class="['mt-5', pClass]">页宽</p>
<p :class="['mt-5!', pClass]">页宽</p>
<Segmented
resize
class="mb-2 select-none"
@@ -424,21 +424,19 @@ onUnmounted(() => removeMatchMedia);
>
<IconifyIconOffline
:icon="settings.stretch ? RightArrow : LeftArrow"
height="20"
/>
<div
class="flex-grow border-b border-dashed"
class="grow border-0 border-b border-dashed"
style="border-color: var(--el-color-primary)"
/>
<IconifyIconOffline
:icon="settings.stretch ? LeftArrow : RightArrow"
height="20"
/>
</div>
</button>
</span>
<p :class="['mt-4', pClass]">页签风格</p>
<p :class="['mt-4!', pClass]">页签风格</p>
<Segmented
resize
class="select-none"
@@ -447,7 +445,7 @@ onUnmounted(() => removeMatchMedia);
@change="onChange"
/>
<p class="mt-5 font-medium text-sm dark:text-white">界面显示</p>
<p class="mt-5! font-medium text-sm dark:text-white">界面显示</p>
<ul class="setting">
<li>
<span class="dark:text-white">灰色模式</span>

View File

@@ -10,8 +10,8 @@ import { usePermissionStoreHook } from "@/store/modules/permission";
import LaySidebarItem from "../lay-sidebar/components/SidebarItem.vue";
import LaySidebarFullScreen from "../lay-sidebar/components/SidebarFullScreen.vue";
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import Setting from "@iconify-icons/ri/settings-3-line";
import LogoutCircleRLine from "~icons/ri/logout-circle-r-line";
import Setting from "~icons/ri/settings-3-line";
const menuRef = ref();
const showLogo = ref(

View File

@@ -10,8 +10,8 @@ import { usePermissionStoreHook } from "@/store/modules/permission";
import LaySidebarExtraIcon from "../lay-sidebar/components/SidebarExtraIcon.vue";
import LaySidebarFullScreen from "../lay-sidebar/components/SidebarFullScreen.vue";
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import Setting from "@iconify-icons/ri/settings-3-line";
import LogoutCircleRLine from "~icons/ri/logout-circle-r-line";
import Setting from "~icons/ri/settings-3-line";
const menuRef = ref();
const defaultActive = ref(null);

View File

@@ -104,12 +104,12 @@ watch(
</script>
<template>
<el-breadcrumb class="!leading-[50px] select-none" separator="/">
<el-breadcrumb class="leading-[50px]! select-none" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item
v-for="item in levelList"
:key="item.path"
class="!inline !items-stretch"
class="inline! items-stretch!"
>
<a @click.prevent="handleLink(item)">
{{ item.meta.title }}

View File

@@ -3,10 +3,10 @@ import { computed } from "vue";
import { useGlobal } from "@pureadmin/utils";
import { useNav } from "@/layout/hooks/useNav";
import ArrowLeft from "@iconify-icons/ri/arrow-left-double-fill";
import ArrowLeft from "~icons/ri/arrow-left-double-fill";
interface Props {
isActive: boolean;
isActive?: boolean;
}
withDefaults(defineProps<Props>(), {

View File

@@ -16,10 +16,10 @@ import {
useAttrs
} from "vue";
import ArrowUp from "@iconify-icons/ep/arrow-up-bold";
import EpArrowDown from "@iconify-icons/ep/arrow-down-bold";
import ArrowLeft from "@iconify-icons/ep/arrow-left-bold";
import ArrowRight from "@iconify-icons/ep/arrow-right-bold";
import ArrowUp from "~icons/ep/arrow-up-bold";
import EpArrowDown from "~icons/ep/arrow-down-bold";
import ArrowLeft from "~icons/ep/arrow-left-bold";
import ArrowRight from "~icons/ep/arrow-right-bold";
const attrs = useAttrs();
const { layout, isCollapse, tooltipEffect, getDivStyle } = useNav();
@@ -60,6 +60,21 @@ const getSubMenuIconStyle = computed((): CSSProperties => {
};
});
const textClass = computed(() => {
const item = props.item;
const baseClass = "w-full! text-inherit!";
if (
layout.value !== "horizontal" &&
isCollapse.value &&
!toRaw(item.meta.icon) &&
((layout.value === "vertical" && item.parentId === null) ||
(layout.value === "mix" && item.pathList.length === 2))
) {
return `${baseClass} min-w-[54px]! text-center! px-3!`;
}
return baseClass;
});
const expandCloseIcon = computed(() => {
if (!getConfig()?.MenuArrowIconNoTransition) return "";
return {
@@ -143,7 +158,7 @@ function resolvePath(routePath) {
item?.pathList?.length === 2)
"
truncated
class="!w-full !pl-4 !text-inherit"
class="w-full! px-3! min-w-[54px]! text-center! text-inherit!"
>
{{ onlyOneChild.meta.title }}
</el-text>
@@ -155,7 +170,7 @@ function resolvePath(routePath) {
offset: [0, -10],
theme: tooltipEffect
}"
class="!w-full !text-inherit"
class="w-full! text-inherit!"
>
{{ onlyOneChild.meta.title }}
</ReText>
@@ -194,15 +209,7 @@ function resolvePath(routePath) {
offset: [0, -10],
theme: tooltipEffect
}"
:class="{
'!w-full': true,
'!text-inherit': true,
'!pl-4':
layout !== 'horizontal' &&
isCollapse &&
!toRaw(item.meta.icon) &&
item.parentId === null
}"
:class="textClass"
>
{{ item.meta.title }}
</ReText>

View File

@@ -3,10 +3,10 @@ import { computed } from "vue";
import { useGlobal } from "@pureadmin/utils";
import { useNav } from "@/layout/hooks/useNav";
import MenuFold from "@iconify-icons/ri/menu-fold-fill";
import MenuFold from "~icons/ri/menu-fold-fill";
interface Props {
isActive: boolean;
isActive?: boolean;
}
withDefaults(defineProps<Props>(), {
@@ -21,7 +21,7 @@ const iconClass = computed(() => {
"mb-1",
"w-[16px]",
"h-[16px]",
"inline-block",
"inline-block!",
"align-middle",
"cursor-pointer",
"duration-[100ms]"

View File

@@ -60,11 +60,11 @@ const { title, getLogo } = useNav();
height: 32px;
margin: 2px 0 0 12px;
overflow: hidden;
text-overflow: ellipsis;
font-size: 18px;
font-weight: 600;
line-height: 32px;
color: var(--pure-theme-sub-menu-active-text);
text-overflow: ellipsis;
white-space: nowrap;
}
}

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import MenuFold from "@iconify-icons/ri/menu-fold-fill";
import MenuUnfold from "@iconify-icons/ri/menu-unfold-fill";
import MenuFold from "~icons/ri/menu-fold-fill";
import MenuUnfold from "~icons/ri/menu-unfold-fill";
interface Props {
isActive: boolean;
isActive?: boolean;
}
withDefaults(defineProps<Props>(), {
@@ -27,7 +27,7 @@ const toggleClick = () => {
>
<IconifyIconOffline
:icon="isActive ? MenuFold : MenuUnfold"
class="inline-block align-middle hover:text-primary dark:hover:!text-white"
class="inline-block! align-middle hover:text-primary dark:hover:text-white!"
/>
</div>
</template>

View File

@@ -59,10 +59,10 @@
color: var(--el-color-primary);
cursor: pointer;
border-radius: 4px;
transform: translate(0, -50%);
transition:
background-color 0.12s,
color 0.12s;
transform: translate(0, -50%);
&:hover {
color: rgb(0 0 0 / 88%) !important;
@@ -127,10 +127,10 @@
font-weight: normal;
color: var(--el-text-color-primary);
white-space: nowrap;
outline: 0;
list-style-type: none;
background: #fff;
border-radius: 4px;
outline: 0;
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
li {

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { emitter } from "@/utils/mitt";
import NProgress from "@/utils/progress";
import { RouteConfigs } from "../../types";
import { useTags } from "../../hooks/useTag";
import { routerArrays } from "@/layout/types";
@@ -17,11 +18,11 @@ import {
useResizeObserver
} from "@pureadmin/utils";
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import ArrowDown from "@iconify-icons/ri/arrow-down-s-line";
import ArrowRightSLine from "@iconify-icons/ri/arrow-right-s-line";
import ArrowLeftSLine from "@iconify-icons/ri/arrow-left-s-line";
import ExitFullscreen from "~icons/ri/fullscreen-exit-fill";
import Fullscreen from "~icons/ri/fullscreen-fill";
import ArrowDown from "~icons/ri/arrow-down-s-line";
import ArrowRightSLine from "~icons/ri/arrow-right-s-line";
import ArrowLeftSLine from "~icons/ri/arrow-left-s-line";
const {
Close,
@@ -204,12 +205,14 @@ function dynamicRouteTag(value: string): void {
/** 刷新路由 */
function onFresh() {
NProgress.start();
const { fullPath, query } = unref(route);
router.replace({
path: "/redirect" + fullPath,
query
});
handleAliveRoute(route as ToRouteType, "refresh");
NProgress.done();
}
function deleteDynamicTag(obj: any, current: any, tag?: string) {
@@ -252,7 +255,7 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
if (tag === "other") {
spliceRoute(1, 1, true);
} else if (tag === "left") {
spliceRoute(fixedTags.length, valueIndex - 1, true);
spliceRoute(fixedTags.length, valueIndex - fixedTags.length);
} else if (tag === "right") {
spliceRoute(valueIndex + 1, multiTags.value.length);
} else {
@@ -351,7 +354,7 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
break;
}
setTimeout(() => {
showMenuModel(route.fullPath, route.query);
showMenuModel(route.fullPath, route.query, route.params);
});
}
@@ -386,15 +389,18 @@ function disabledMenus(value: boolean, fixedTag = false) {
function showMenuModel(
currentPath: string,
query: object = {},
params: object = {},
refresh = false
) {
const allRoute = multiTags.value;
const routeLength = multiTags.value.length;
let currentIndex = -1;
if (isAllEmpty(query)) {
currentIndex = allRoute.findIndex(v => v.path === currentPath);
} else {
if (!isAllEmpty(params)) {
currentIndex = allRoute.findIndex(v => isEqual(v.params, params));
} else if (!isAllEmpty(query)) {
currentIndex = allRoute.findIndex(v => isEqual(v.query, query));
} else {
currentIndex = allRoute.findIndex(v => v.path === currentPath);
}
function fixedTagDisabled() {
if (allRoute[currentIndex]?.meta?.fixedTag) {
@@ -460,14 +466,14 @@ function openMenu(tag, e) {
} else if (route.path !== tag.path && route.name !== tag.name) {
// 右键菜单不匹配当前路由,隐藏刷新
tagsViews[0].show = false;
showMenuModel(tag.path, tag.query);
showMenuModel(tag.path, tag.query, tag.params);
} else if (multiTags.value.length === 2 && route.path !== tag.path) {
showMenus(true);
// 只有两个标签时不显示关闭其他标签页
tagsViews[4].show = false;
} else if (route.path === tag.path) {
// 右键当前激活的菜单
showMenuModel(tag.path, tag.query, true);
showMenuModel(tag.path, tag.query, tag.params);
} else {
showMenuModel(tag.path, tag.query, tag.params, true);
}
currentSelect.value = tag;
@@ -509,6 +515,7 @@ function tagOnClick(item) {
} else {
router.push({ path });
}
emitter.emit("tagOnClick", item);
}
onClickOutside(contextmenuRef, closeMenu, {
@@ -586,7 +593,7 @@ onBeforeUnmount(() => {
>
<template v-if="showModel !== 'chrome'">
<span
class="tag-title dark:!text-text_color_primary dark:hover:!text-primary"
class="tag-title dark:text-text_color_primary! dark:hover:text-primary!"
>
{{ item.meta.title }}
</span>

View File

@@ -17,13 +17,22 @@ const loading = ref(true);
const currentRoute = useRoute();
const frameSrc = ref<string>("");
const frameRef = ref<HTMLElement | null>(null);
const fallbackTimer = ref<number | null>(null);
if (unref(currentRoute.meta)?.frameSrc) {
frameSrc.value = unref(currentRoute.meta)?.frameSrc as string;
}
unref(currentRoute.meta)?.frameLoading === false && hideLoading();
function clearFallbackTimer() {
if (fallbackTimer.value !== null) {
clearTimeout(fallbackTimer.value);
fallbackTimer.value = null;
}
}
function hideLoading() {
loading.value = false;
clearFallbackTimer();
}
function init() {
@@ -32,32 +41,42 @@ function init() {
if (!iframe) return;
const _frame = iframe as any;
if (_frame.attachEvent) {
_frame.attachEvent("onload", () => {
hideLoading();
});
_frame.attachEvent("onload", hideLoading);
} else {
iframe.onload = () => {
hideLoading();
};
iframe.onload = hideLoading;
}
});
}
let isRedirect = false;
watch(
() => currentRoute.fullPath,
path => {
if (
currentRoute.name === "Redirect" &&
path.includes(props.frameInfo?.fullPath)
props.frameInfo?.fullPath &&
path.includes(props.frameInfo.fullPath)
) {
frameSrc.value = path; // redirect时置换成任意值待重定向后 重新赋值
isRedirect = true;
loading.value = true;
return;
}
// 重新赋值
if (props.frameInfo?.fullPath === path) {
frameSrc.value = props.frameInfo?.frameSrc;
if (props.frameInfo?.fullPath === path && isRedirect) {
loading.value = true;
clearFallbackTimer();
const url = new URL(props.frameInfo.frameSrc, window.location.origin);
const joinChar = url.search ? "&" : "?";
frameSrc.value = `${props.frameInfo.frameSrc}${joinChar}t=${Date.now()}`;
fallbackTimer.value = window.setTimeout(() => {
if (loading.value) {
hideLoading();
}
}, 1500);
isRedirect = false;
}
},
{ immediate: true }
);
onMounted(() => {

View File

@@ -12,8 +12,8 @@ import { useAppStoreHook } from "@/store/modules/app";
import { useUserStoreHook } from "@/store/modules/user";
import { useGlobal, isAllEmpty } from "@pureadmin/utils";
import { usePermissionStoreHook } from "@/store/modules/permission";
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import ExitFullscreen from "~icons/ri/fullscreen-exit-fill";
import Fullscreen from "~icons/ri/fullscreen-fill";
const errorInfo =
"The current routing configuration is incorrect, please check the configuration";

View File

@@ -20,13 +20,13 @@ import {
hasClass
} from "@pureadmin/utils";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import CloseAllTags from "@iconify-icons/ri/subtract-line";
import CloseOtherTags from "@iconify-icons/ri/text-spacing";
import CloseRightTags from "@iconify-icons/ri/text-direction-l";
import CloseLeftTags from "@iconify-icons/ri/text-direction-r";
import RefreshRight from "@iconify-icons/ep/refresh-right";
import Close from "@iconify-icons/ep/close";
import Fullscreen from "~icons/ri/fullscreen-fill";
import CloseAllTags from "~icons/ri/subtract-line";
import CloseOtherTags from "~icons/ri/text-spacing";
import CloseRightTags from "~icons/ri/text-direction-l";
import CloseLeftTags from "~icons/ri/text-direction-r";
import RefreshRight from "~icons/ep/refresh-right";
import Close from "~icons/ep/close";
export function useTags() {
const route = useRoute();
@@ -113,14 +113,21 @@ export function useTags() {
]);
function conditionHandle(item, previous, next) {
const currentName = route.name || "";
const itemName = item.name || "";
if (isBoolean(route?.meta?.showLink) && route?.meta?.showLink === false) {
if (Object.keys(route.query).length > 0) {
return isEqual(route.query, item.query) ? previous : next;
return currentName === itemName && isEqual(route.query, item.query)
? previous
: next;
} else {
return isEqual(route.params, item.params) ? previous : next;
return currentName === itemName && isEqual(route.params, item.params)
? previous
: next;
}
} else {
return route.path === item.path ? previous : next;
return currentName === itemName ? previous : next;
}
}

View File

@@ -208,8 +208,8 @@ const LayHeader = defineComponent({
height: 100%;
&::after {
display: table;
clear: both;
display: table;
content: "";
}

View File

@@ -1,4 +1,4 @@
import type { IconifyIcon } from "@iconify/vue";
import type { FunctionalComponent } from "vue";
const { VITE_HIDE_HOME } = import.meta.env;
export const routerArrays: Array<RouteConfigs> =
@@ -6,9 +6,10 @@ export const routerArrays: Array<RouteConfigs> =
? [
{
path: "/welcome",
name: "Welcome",
meta: {
title: "首页",
icon: "ep:home-filled"
icon: "ep/home-filled"
}
}
]
@@ -16,7 +17,7 @@ export const routerArrays: Array<RouteConfigs> =
export type routeMetaType = {
title?: string;
icon?: string | IconifyIcon;
icon?: string | FunctionalComponent;
showLink?: boolean;
savedPosition?: boolean;
auths?: Array<string>;
@@ -36,7 +37,7 @@ export type multiTagsType = {
};
export type tagsViewsType = {
icon: string | IconifyIcon;
icon: string | FunctionalComponent;
text: string;
divided: boolean;
disabled: boolean;

View File

@@ -6,7 +6,13 @@ import { buildHierarchyTree } from "@/utils/tree";
import remainingRouter from "./modules/remaining";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { usePermissionStoreHook } from "@/store/modules/permission";
import { isUrl, openLink, storageLocal, isAllEmpty } from "@pureadmin/utils";
import {
isUrl,
openLink,
cloneDeep,
isAllEmpty,
storageLocal
} from "@pureadmin/utils";
import {
ascending,
getTopMenu,
@@ -20,9 +26,9 @@ import {
} from "./utils";
import {
type Router,
createRouter,
type RouteRecordRaw,
type RouteComponent
type RouteComponent,
createRouter
} from "vue-router";
import {
type DataInfo,
@@ -54,6 +60,9 @@ export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes(
formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity))))
);
/** 初始的静态路由,用于退出登录时重置路由 */
const initConstantRoutes: Array<RouteRecordRaw> = cloneDeep(constantRoutes);
/** 用于渲染菜单,保持原始层级 */
export const constantMenus: Array<RouteComponent> = ascending(
routes.flat(Infinity)
@@ -84,20 +93,25 @@ export const router: Router = createRouter({
}
});
/** 记录已经加载的页面路径 */
const loadedPaths = new Set<string>();
/** 重置已加载页面记录 */
export function resetLoadedPaths() {
loadedPaths.clear();
}
/** 重置路由 */
export function resetRouter() {
router.getRoutes().forEach(route => {
const { name, meta } = route;
if (name && router.hasRoute(name) && meta?.backstage) {
router.removeRoute(name);
router.options.routes = formatTwoStageRoutes(
formatFlatteningRoutes(
buildHierarchyTree(ascending(routes.flat(Infinity)))
)
);
router.clearRoutes();
for (const route of initConstantRoutes.concat(...(remainingRouter as any))) {
router.addRoute(route);
}
});
router.options.routes = formatTwoStageRoutes(
formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity))))
);
usePermissionStoreHook().clearAllCachePage();
resetLoadedPaths();
}
/** 路由白名单 */
@@ -106,6 +120,12 @@ const whiteList = ["/login"];
const { VITE_HIDE_HOME } = import.meta.env;
router.beforeEach((to: ToRouteType, _from, next) => {
to.meta.loaded = loadedPaths.has(to.path);
if (!to.meta.loaded) {
NProgress.start();
}
if (to.meta?.keepAlive) {
handleAliveRoute(to, "add");
// 页面整体刷新和点击标签页刷新
@@ -114,7 +134,6 @@ router.beforeEach((to: ToRouteType, _from, next) => {
}
}
const userInfo = storageLocal().getItem<DataInfo<number>>(userKey);
NProgress.start();
const externalLink = isUrl(to?.name as string);
if (!externalLink) {
to.matched.some(item => {
@@ -199,7 +218,8 @@ router.beforeEach((to: ToRouteType, _from, next) => {
}
});
router.afterEach(() => {
router.afterEach(to => {
loadedPaths.add(to.path);
NProgress.done();
});

View File

@@ -2,7 +2,7 @@ export default {
path: "/error",
redirect: "/error/403",
meta: {
icon: "ri:information-line",
icon: "ri/information-line",
// showLink: false,
title: "异常页面",
rank: 9

View File

@@ -7,7 +7,7 @@ export default {
component: Layout,
redirect: "/welcome",
meta: {
icon: "ep:home-filled",
icon: "ep/home-filled",
title: "首页",
rank: 0
},

View File

@@ -7,8 +7,27 @@ export default [
component: () => import("@/views/login/index.vue"),
meta: {
title: "登录",
showLink: false,
rank: 101
showLink: false
}
},
// 全屏403无权访问页面
{
path: "/access-denied",
name: "AccessDenied",
component: () => import("@/views/error/403.vue"),
meta: {
title: "403",
showLink: false
}
},
// 全屏500服务器出错页面
{
path: "/server-error",
name: "ServerError",
component: () => import("@/views/error/500.vue"),
meta: {
title: "500",
showLink: false
}
},
{
@@ -16,8 +35,7 @@ export default [
component: Layout,
meta: {
title: "加载中...",
showLink: false,
rank: 102
showLink: false
},
children: [
{

View File

@@ -139,12 +139,17 @@ function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
}
}
/** 动态路由注册完成后再添加全屏404页面不存在页面避免刷新动态路由页面时误跳转到404页面 */
function addPathMatch() {
if (!router.hasRoute("pathMatch")) {
router.addRoute({
path: "/:pathMatch(.*)",
name: "pathMatch",
redirect: "/error/404"
path: "/:pathMatch(.*)*",
name: "PageNotFound",
component: () => import("@/views/error/404.vue"),
meta: {
title: "404",
showLink: false
}
});
}
}
@@ -172,6 +177,8 @@ function handleAsyncRoutes(routeList) {
const flattenRouters: any = router
.getRoutes()
.find(n => n.path === "/");
// 保持router.options.routes[0].children与path为"/"的children一致防止数据不一致导致异常
flattenRouters.children = router.options.routes[0].children;
router.addRoute(flattenRouters);
}
}

View File

@@ -8,8 +8,7 @@ import {
responsiveStorageNameSpace
} from "../utils";
export const useAppStore = defineStore({
id: "pure-app",
export const useAppStore = defineStore("pure-app", {
state: (): appType => ({
sidebar: {
opened:
@@ -77,9 +76,6 @@ export const useAppStore = defineStore({
},
setViewportSize(size) {
this.viewportSize = size;
},
setSortSwap(val) {
this.sortSwap = val;
}
}
});

View File

@@ -6,8 +6,7 @@ import {
responsiveStorageNameSpace
} from "../utils";
export const useEpThemeStore = defineStore({
id: "pure-epTheme",
export const useEpThemeStore = defineStore("pure-epTheme", {
state: () => ({
epThemeColor:
storageLocal().getItem<StorageConfigs>(

View File

@@ -14,8 +14,7 @@ import {
} from "../utils";
import { usePermissionStoreHook } from "./permission";
export const useMultiTagsStore = defineStore({
id: "pure-multiTags",
export const useMultiTagsStore = defineStore("pure-multiTags", {
state: () => ({
// 存储标签页信息(路由信息)
multiTags: storageLocal().getItem<StorageConfigs>(
@@ -24,12 +23,12 @@ export const useMultiTagsStore = defineStore({
? storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}tags`
)
: [
: ([
...routerArrays,
...usePermissionStoreHook().flatteningRoutes.filter(
v => v?.meta?.fixedTag
)
],
] as any),
multiTagsCache: storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}configure`
)?.multiTagsCache
@@ -81,22 +80,15 @@ export const useMultiTagsStore = defineStore({
if (isBoolean(tagVal?.meta?.showLink) && !tagVal?.meta?.showLink)
return;
const tagPath = tagVal.path;
// 判断tag是否已存在
const tagHasExits = this.multiTags.some(tag => {
return tag.path === tagPath;
return (
tag.path === tagPath &&
isEqual(tag?.query, tagVal?.query) &&
isEqual(tag?.params, tagVal?.params)
);
});
// 判断tag中的query键值是否相等
const tagQueryHasExits = this.multiTags.some(tag => {
return isEqual(tag?.query, tagVal?.query);
});
// 判断tag中的params键值是否相等
const tagParamsHasExits = this.multiTags.some(tag => {
return isEqual(tag?.params, tagVal?.params);
});
if (tagHasExits && tagQueryHasExits && tagParamsHasExits) return;
if (tagHasExits) return;
// 动态路由可打开的最大数量
const dynamicLevel = tagVal?.meta?.dynamicLevel ?? -1;

View File

@@ -2,7 +2,6 @@ import { defineStore } from "pinia";
import {
type cacheType,
store,
debounce,
ascending,
getKeyList,
filterTree,
@@ -12,8 +11,7 @@ import {
} from "../utils";
import { useMultiTagsStoreHook } from "./multiTags";
export const usePermissionStore = defineStore({
id: "pure-permission",
export const usePermissionStore = defineStore("pure-permission", {
state: () => ({
// 静态路由生成的菜单
constantMenus,
@@ -31,24 +29,11 @@ export const usePermissionStore = defineStore({
filterTree(ascending(this.constantMenus.concat(routes)))
);
this.flatteningRoutes = formatFlatteningRoutes(
this.constantMenus.concat(routes)
this.constantMenus.concat(routes) as any
);
},
cacheOperate({ mode, name }: cacheType) {
const delIndex = this.cachePageList.findIndex(v => v === name);
switch (mode) {
case "refresh":
this.cachePageList = this.cachePageList.filter(v => v !== name);
break;
case "add":
this.cachePageList.push(name);
break;
case "delete":
delIndex !== -1 && this.cachePageList.splice(delIndex, 1);
break;
}
/** 监听缓存页面是否存在于标签页,不存在则删除 */
debounce(() => {
clearCache() {
let cacheLength = this.cachePageList.length;
const nameList = getKeyList(useMultiTagsStoreHook().multiTags, "name");
while (cacheLength > 0) {
@@ -60,7 +45,22 @@ export const usePermissionStore = defineStore({
);
cacheLength--;
}
})();
},
cacheOperate({ mode, name }: cacheType) {
const delIndex = this.cachePageList.findIndex(v => v === name);
switch (mode) {
case "refresh":
this.cachePageList = this.cachePageList.filter(v => v !== name);
this.clearCache();
break;
case "add":
this.cachePageList.push(name);
break;
case "delete":
delIndex !== -1 && this.cachePageList.splice(delIndex, 1);
this.clearCache();
break;
}
},
/** 清空缓存页面 */
clearAllCachePage() {

View File

@@ -1,8 +1,7 @@
import { defineStore } from "pinia";
import { type setType, store, getConfig } from "../utils";
export const useSettingStore = defineStore({
id: "pure-setting",
export const useSettingStore = defineStore("pure-setting", {
state: (): setType => ({
title: getConfig().Title,
fixedHeader: getConfig().FixedHeader,

View File

@@ -16,8 +16,7 @@ import {
import { useMultiTagsStoreHook } from "./multiTags";
import { type DataInfo, setToken, removeToken, userKey } from "@/utils/auth";
export const useUserStore = defineStore({
id: "pure-user",
export const useUserStore = defineStore("pure-user", {
state: (): userType => ({
// 头像
avatar: storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? "",

View File

@@ -83,8 +83,8 @@
.el-upload-list__item.is-ready &.el-icon--close {
width: 24px;
height: 24px;
border-radius: 4px;
outline: none;
border-radius: 4px;
transition:
background-color 0.2s,
color 0.2s;
@@ -117,8 +117,8 @@
}
& .el-message__closeBtn {
border-radius: 4px;
outline: none;
border-radius: 4px;
transition:
background-color 0.2s,
color 0.2s;

View File

@@ -1,12 +1,3 @@
*,
::before,
::after {
box-sizing: border-box;
border-color: currentColor;
border-style: solid;
border-width: 0;
}
#app {
width: 100%;
height: 100%;
@@ -25,7 +16,8 @@ body {
width: 100%;
height: 100%;
margin: 0;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
font-family:
"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
line-height: inherit;
-moz-osx-font-smoothing: grayscale;
@@ -57,8 +49,9 @@ code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
font-size: 1em;
}

View File

@@ -172,10 +172,6 @@
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: var(--pure-theme-sub-menu-active-text) !important;
i {
color: var(--pure-theme-sub-menu-active-text) !important;
}
}
.is-active {
@@ -191,8 +187,8 @@
.el-menu-item.is-active.nest-menu::before {
position: absolute;
inset: 0 8px;
margin: 4px 0;
clear: both;
margin: 4px 0;
content: "";
background: var(--el-color-primary) !important;
border-radius: 3px;
@@ -212,13 +208,13 @@
position: absolute;
top: 0;
left: 0;
clear: both;
width: 2px;
height: 100%;
clear: both;
content: "";
background-color: var(--pure-theme-menu-active-before);
transition: all var(--pure-transition-duration) ease-in-out;
transform: translateY(0);
transition: all var(--pure-transition-duration) ease-in-out;
}
.el-menu--collapse .outer-most.el-sub-menu > .el-sub-menu__title::before {
@@ -240,8 +236,8 @@
.is-active.submenu-title-noDropdown.outer-most::before {
position: absolute;
inset: 0 8px;
margin: 4px 0;
clear: both;
margin: 4px 0;
content: "";
background: var(--el-color-primary) !important;
border-radius: 3px;
@@ -270,10 +266,6 @@
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: var(--pure-theme-sub-menu-active-text) !important;
i {
color: var(--pure-theme-sub-menu-active-text) !important;
}
}
/* 子菜单中还有子菜单 */
@@ -374,10 +366,6 @@
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: var(--pure-theme-sub-menu-active-text) !important;
i {
color: var(--pure-theme-sub-menu-active-text) !important;
}
}
.nest-menu .el-sub-menu > .el-sub-menu__title,
@@ -435,11 +423,11 @@
height: 32px;
margin: 2px 0 0 12px;
overflow: hidden;
text-overflow: ellipsis;
font-size: 18px;
font-weight: 600;
line-height: 32px;
color: var(--pure-theme-sub-menu-active-text);
text-overflow: ellipsis;
white-space: nowrap;
}
}
@@ -530,10 +518,6 @@
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: var(--pure-theme-sub-menu-active-text) !important;
i {
color: var(--pure-theme-sub-menu-active-text) !important;
}
}
.is-active {
@@ -566,8 +550,8 @@
&.hideSidebar {
.sidebar-container {
pointer-events: none;
transition-duration: 0.3s;
transform: translate3d(-$sideBarWidth, 0, 0);
transition-duration: 0.3s;
}
}
}
@@ -610,10 +594,10 @@ body[layout="vertical"] {
.el-sub-menu {
& > .el-sub-menu__title {
& > span {
visibility: visible;
width: 100%;
height: 100%;
text-align: center;
visibility: visible;
}
}
}
@@ -653,6 +637,10 @@ body[layout="horizontal"] {
@include merge-style($sideBarWidth);
.el-menu {
--el-menu-hover-text-color: var(--pure-theme-menu-text) !important;
}
.fixed-header,
.main-container {
transition: none !important;
@@ -674,6 +662,7 @@ body[layout="mix"] {
.el-menu {
--el-menu-hover-bg-color: transparent !important;
--el-menu-hover-text-color: var(--pure-theme-menu-text) !important;
}
.hideSidebar {
@@ -702,10 +691,10 @@ body[layout="mix"] {
padding: 0;
& > span {
visibility: visible;
width: 100%;
height: 100%;
text-align: center;
visibility: visible;
}
}
}

View File

@@ -1,21 +1,46 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
@layer components {
.flex-c {
@custom-variant dark (&:is(.dark *));
@theme {
--color-bg_color: var(--el-bg-color);
--color-primary: var(--el-color-primary);
--color-text_color_primary: var(--el-text-color-primary);
--color-text_color_regular: var(--el-text-color-regular);
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
@utility flex-c {
@apply flex justify-center items-center;
}
.flex-ac {
@utility flex-ac {
@apply flex justify-around items-center;
}
.flex-bc {
@utility flex-bc {
@apply flex justify-between items-center;
}
.navbar-bg-hover {
@apply dark:text-white dark:hover:!bg-[#242424];
}
@utility navbar-bg-hover {
@apply dark:text-white dark:hover:bg-[#242424]!;
}

View File

@@ -10,7 +10,6 @@ import type {
PureHttpRequestConfig
} from "./types.d";
import { stringify } from "qs";
import NProgress from "../progress";
import { getToken, formatToken } from "@/utils/auth";
import { useUserStoreHook } from "@/store/modules/user";
@@ -61,8 +60,6 @@ class PureHttp {
private httpInterceptorsRequest(): void {
PureHttp.axiosInstance.interceptors.request.use(
async (config: PureHttpRequestConfig): Promise<any> => {
// 开启进度条动画
NProgress.start();
// 优先判断post/get等方法是否传入回调否则执行初始化设置等回调
if (typeof config.beforeRequestCallback === "function") {
config.beforeRequestCallback(config);
@@ -121,8 +118,6 @@ class PureHttp {
instance.interceptors.response.use(
(response: PureHttpResponse) => {
const $config = response.config;
// 关闭进度条动画
NProgress.done();
// 优先判断post/get等方法是否传入回调否则执行初始化设置等回调
if (typeof $config.beforeResponseCallback === "function") {
$config.beforeResponseCallback(response);
@@ -137,8 +132,6 @@ class PureHttp {
(error: PureHttpError) => {
const $error = error;
$error.isCancelRequest = Axios.isCancel($error);
// 关闭进度条动画
NProgress.done();
// 所有的响应异常 区分来源为取消请求/非取消请求
return Promise.reject($error);
}

View File

@@ -8,6 +8,8 @@ type messageTypes = "info" | "success" | "warning" | "error";
interface MessageParams {
/** 消息类型,可选 `info` 、`success` 、`warning` 、`error` ,默认 `info` */
type?: messageTypes;
/** 是否纯色,默认 `false` */
plain?: boolean;
/** 自定义图标,该属性会覆盖 `type` 的图标 */
icon?: any;
/** 是否将 `message` 属性作为 `HTML` 片段处理,默认 `false` */
@@ -18,14 +20,14 @@ interface MessageParams {
duration?: number;
/** 是否显示关闭按钮,默认值 `false` */
showClose?: boolean;
/** 文字是否居中,默认 `false` */
center?: boolean;
/** `Message` 距离窗口顶部的偏移量,默认 `20` */
/** `Message` 距离窗口顶部的偏移量,默认 `16` */
offset?: number;
/** 设置组件的根元素,默认 `document.body` */
appendTo?: string | HTMLElement;
/** 合并内容相同的消息,不支持 `VNode` 类型的消息,默认值 `false` */
grouping?: boolean;
/** 重复次数,类似于 `Badge` 。当和 `grouping` 属性一起使用时作为初始数量使用,默认值 `1` */
repeatNum?: number;
/** 关闭时的回调函数, 参数为被关闭的 `message` 实例 */
onClose?: Function | null;
}
@@ -48,28 +50,30 @@ const message = (
const {
icon,
type = "info",
plain = false,
dangerouslyUseHTMLString = false,
customClass = "antd",
duration = 2000,
showClose = false,
center = false,
offset = 20,
offset = 16,
appendTo = document.body,
grouping = false,
repeatNum = 1,
onClose
} = params;
return ElMessage({
message,
type,
icon,
type,
plain,
dangerouslyUseHTMLString,
duration,
showClose,
center,
offset,
appendTo,
grouping,
repeatNum,
// 全局搜 pure-message 即可知道该类的样式位置
customClass: customClass === "antd" ? "pure-message" : "",
onClose: () => (isFunction(onClose) ? onClose() : null)

View File

@@ -4,10 +4,11 @@ import mitt from "mitt";
/** 全局公共事件需要在此处添加类型 */
type Events = {
openPanel: string;
tagViewsChange: string;
tagViewsShowModel: string;
tagOnClick: string;
logoChange: boolean;
tagViewsChange: string;
changLayoutRoute: string;
tagViewsShowModel: string;
};
export const emitter: Emitter<Events> = mitt<Events>();

View File

@@ -9,6 +9,7 @@ const Print = function (dom, options?: object): PrintFunction {
options = options || {};
// @ts-expect-error
if (!(this instanceof Print)) return new Print(dom, options);
// @ts-expect-error
this.conf = {
styleStr: "",
// Elements that need to dynamically get and set the height
@@ -18,19 +19,26 @@ const Print = function (dom, options?: object): PrintFunction {
// Callback after printing
printDoneCallBack: null
};
// @ts-expect-error
for (const key in this.conf) {
if (key && options.hasOwnProperty(key)) {
// @ts-expect-error
this.conf[key] = options[key];
}
}
if (typeof dom === "string") {
// @ts-expect-error
this.dom = document.querySelector(dom);
} else {
// @ts-expect-error
this.dom = this.isDOM(dom) ? dom : dom.$el;
}
// @ts-expect-error
if (this.conf.setDomHeightArr && this.conf.setDomHeightArr.length) {
// @ts-expect-error
this.setDomHeight(this.conf.setDomHeightArr);
}
// @ts-expect-error
this.init();
};
@@ -132,8 +140,10 @@ Print.prototype = {
"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);

View File

@@ -10,12 +10,14 @@ const router = useRouter();
</script>
<template>
<div class="flex justify-center items-center h-[640px]">
<div
class="flex flex-col md:flex-row justify-center items-center min-h-full w-full p-4 md:p-0"
>
<noAccess />
<div class="ml-12">
<div class="mt-8 md:ml-12 md:mt-0 text-center md:text-left">
<p
v-motion
class="font-medium text-4xl mb-4 dark:text-white"
class="font-medium text-4xl mb-4! dark:text-white"
:initial="{
opacity: 0,
y: 100
@@ -32,7 +34,7 @@ const router = useRouter();
</p>
<p
v-motion
class="mb-4 text-gray-500"
class="text-xl mb-4! text-gray-500"
:initial="{
opacity: 0,
y: 100
@@ -50,6 +52,7 @@ const router = useRouter();
<el-button
v-motion
type="primary"
class="block mx-auto md:inline-block md:mx-0"
:initial="{
opacity: 0,
y: 100
@@ -68,3 +71,9 @@ const router = useRouter();
</div>
</div>
</template>
<style scoped>
.main-content {
margin: 0 !important;
}
</style>

View File

@@ -10,12 +10,14 @@ const router = useRouter();
</script>
<template>
<div class="flex justify-center items-center h-[640px]">
<div
class="flex flex-col md:flex-row justify-center items-center min-h-full w-full p-4 md:p-0"
>
<noExist />
<div class="ml-12">
<div class="mt-8 md:ml-12 md:mt-0 text-center md:text-left">
<p
v-motion
class="font-medium text-4xl mb-4 dark:text-white"
class="font-medium text-4xl mb-4! dark:text-white"
:initial="{
opacity: 0,
y: 100
@@ -32,7 +34,7 @@ const router = useRouter();
</p>
<p
v-motion
class="mb-4 text-gray-500"
class="text-xl mb-4! text-gray-500"
:initial="{
opacity: 0,
y: 100
@@ -50,6 +52,7 @@ const router = useRouter();
<el-button
v-motion
type="primary"
class="block mx-auto md:inline-block md:mx-0"
:initial="{
opacity: 0,
y: 100
@@ -68,3 +71,9 @@ const router = useRouter();
</div>
</div>
</template>
<style scoped>
.main-content {
margin: 0 !important;
}
</style>

View File

@@ -10,12 +10,14 @@ const router = useRouter();
</script>
<template>
<div class="flex justify-center items-center h-[640px]">
<div
class="flex flex-col md:flex-row justify-center items-center min-h-full w-full p-4 md:p-0"
>
<noServer />
<div class="ml-12">
<div class="mt-8 md:ml-12 md:mt-0 text-center md:text-left">
<p
v-motion
class="font-medium text-4xl mb-4 dark:text-white"
class="font-medium text-4xl mb-4! dark:text-white"
:initial="{
opacity: 0,
y: 100
@@ -32,7 +34,7 @@ const router = useRouter();
</p>
<p
v-motion
class="mb-4 text-gray-500"
class="text-xl mb-4! text-gray-500"
:initial="{
opacity: 0,
y: 100
@@ -50,6 +52,7 @@ const router = useRouter();
<el-button
v-motion
type="primary"
class="block mx-auto md:inline-block md:mx-0"
:initial="{
opacity: 0,
y: 100
@@ -68,3 +71,9 @@ const router = useRouter();
</div>
</div>
</template>
<style scoped>
.main-content {
margin: 0 !important;
}
</style>

View File

@@ -3,26 +3,30 @@ import Motion from "./utils/motion";
import { useRouter } from "vue-router";
import { message } from "@/utils/message";
import { loginRules } from "./utils/rule";
import { ref, reactive, toRaw } from "vue";
import { debounce } from "@pureadmin/utils";
import { useNav } from "@/layout/hooks/useNav";
import { useEventListener } from "@vueuse/core";
import type { FormInstance } from "element-plus";
import { useLayout } from "@/layout/hooks/useLayout";
import { useUserStoreHook } from "@/store/modules/user";
import { initRouter, getTopMenu } from "@/router/utils";
import { bg, avatar, illustration } from "./utils/static";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { ref, reactive, toRaw, onMounted, onBeforeUnmount } from "vue";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import dayIcon from "@/assets/svg/day.svg?component";
import darkIcon from "@/assets/svg/dark.svg?component";
import Lock from "@iconify-icons/ri/lock-fill";
import User from "@iconify-icons/ri/user-3-fill";
import Lock from "~icons/ri/lock-fill";
import User from "~icons/ri/user-3-fill";
defineOptions({
name: "Login"
});
const router = useRouter();
const loading = ref(false);
const disabled = ref(false);
const ruleFormRef = ref<FormInstance>();
const { initStorage } = useLayout();
@@ -39,18 +43,25 @@ const ruleForm = reactive({
const onLogin = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid, fields) => {
await formEl.validate(valid => {
if (valid) {
loading.value = true;
useUserStoreHook()
.loginByUsername({ username: ruleForm.username, password: "admin123" })
.loginByUsername({
username: ruleForm.username,
password: ruleForm.password
})
.then(res => {
if (res.success) {
// 获取后端路由
return initRouter().then(() => {
router.push(getTopMenu(true).path).then(() => {
disabled.value = true;
router
.push(getTopMenu(true).path)
.then(() => {
message("登录成功", { type: "success" });
});
})
.finally(() => (disabled.value = false));
});
} else {
message("登录失败", { type: "error" });
@@ -61,19 +72,19 @@ const onLogin = async (formEl: FormInstance | undefined) => {
});
};
/** 使用公共函数,避免`removeEventListener`失效 */
function onkeypress({ code }: KeyboardEvent) {
if (["Enter", "NumpadEnter"].includes(code)) {
onLogin(ruleFormRef.value);
}
}
const immediateDebounce: any = debounce(
formRef => onLogin(formRef),
1000,
true
);
onMounted(() => {
window.document.addEventListener("keypress", onkeypress);
});
onBeforeUnmount(() => {
window.document.removeEventListener("keypress", onkeypress);
useEventListener(document, "keydown", ({ code }) => {
if (
["Enter", "NumpadEnter"].includes(code) &&
!disabled.value &&
!loading.value
)
immediateDebounce(ruleFormRef.value);
});
</script>
@@ -98,7 +109,7 @@ onBeforeUnmount(() => {
<div class="login-form">
<avatar class="avatar" />
<Motion>
<h2 class="outline-none">{{ title }}</h2>
<h2 class="outline-hidden">{{ title }}</h2>
</Motion>
<el-form
@@ -141,10 +152,11 @@ onBeforeUnmount(() => {
<Motion :delay="250">
<el-button
class="w-full mt-4"
class="w-full mt-4!"
size="default"
type="primary"
:loading="loading"
:disabled="disabled"
@click="onLogin(ruleFormRef)"
>
登录

View File

@@ -6,7 +6,7 @@ export const REGEXP_PWD =
/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/;
/** 登录校验 */
const loginRules = reactive(<FormRules>{
const loginRules = reactive<FormRules>({
password: [
{
validator: (rule, value, callback) => {

View File

@@ -8,7 +8,7 @@ defineOptions({
<template>
<div>
<p class="mb-2">当前拥有的code列表{{ getAuths() }}</p>
<p class="mb-2!">当前拥有的code列表{{ getAuths() }}</p>
<el-card shadow="never" class="mb-2">
<template #header>

View File

@@ -11,8 +11,8 @@ defineOptions({
<template>
<div>
<p class="mb-2">当前拥有的code列表{{ permissions }}</p>
<p v-show="permissions?.[0] === '*:*:*'" class="mb-2">
<p class="mb-2!">当前拥有的code列表{{ permissions }}</p>
<p v-show="permissions?.[0] === '*:*:*'" class="mb-2!">
*:*:* 代表拥有全部按钮级别权限
</p>

View File

@@ -44,7 +44,7 @@ function onChange() {
<template>
<div>
<p class="mb-2">
<p class="mb-2!">
模拟后台根据不同角色返回对应路由观察左侧菜单变化管理员角色可查看系统管理菜单普通角色不可查看系统管理菜单
</p>
<el-card shadow="never" :style="elStyle">
@@ -53,7 +53,7 @@ function onChange() {
<span>当前角色{{ username }}</span>
</div>
</template>
<el-select v-model="username" class="!w-[160px]" @change="onChange">
<el-select v-model="username" class="w-[160px]!" @change="onChange">
<el-option
v-for="item in options"
:key="item.value"

View File

@@ -1,19 +0,0 @@
import type { Config } from "tailwindcss";
export default {
darkMode: "class",
corePlugins: {
preflight: false
},
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
bg_color: "var(--el-bg-color)",
primary: "var(--el-color-primary)",
text_color_primary: "var(--el-text-color-primary)",
text_color_regular: "var(--el-text-color-regular)"
}
}
}
} satisfies Config;

View File

@@ -4,10 +4,11 @@
"module": "ESNext",
"moduleResolution": "bundler",
"strict": false,
"strictFunctionTypes": false,
"noImplicitThis": true,
"jsx": "preserve",
"importHelpers": true,
"experimentalDecorators": true,
"strictFunctionTypes": false,
"skipLibCheck": true,
"esModuleInterop": true,
"isolatedModules": true,
@@ -34,6 +35,7 @@
"vite/client",
"element-plus/global",
"@pureadmin/table/volar",
"unplugin-icons/types/vue",
"@pureadmin/descriptions/volar"
]
},

View File

@@ -120,6 +120,7 @@ declare module "vue" {
}
interface ComponentCustomProperties {
$storage: ResponsiveStorage;
$message: (typeof import("element-plus"))["ElMessage"];
$notify: (typeof import("element-plus"))["ElNotification"];
$msgbox: (typeof import("element-plus"))["ElMessageBox"];

2
types/index.d.ts vendored
View File

@@ -75,8 +75,6 @@ interface ComponentElRef<T extends HTMLElement = HTMLDivElement> {
$el: T;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function parseInt(s: string | number, radix?: number): number;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function parseFloat(string: string | number): number;

9
types/router.d.ts vendored
View File

@@ -15,9 +15,9 @@ declare global {
/** 菜单名称(兼容国际化、非国际化,如何用国际化的写法就必须在根目录的`locales`文件夹下对应添加) `必填` */
title: string;
/** 菜单图标 `可选` */
icon?: string | FunctionalComponent | IconifyIcon;
icon?: string | FunctionalComponent;
/** 菜单名称右侧的额外图标 */
extraIcon?: string | FunctionalComponent | IconifyIcon;
extraIcon?: string | FunctionalComponent;
/** 是否在菜单中显示(默认`true``可选` */
showLink?: boolean;
/** 是否显示父级菜单 `可选` */
@@ -56,6 +56,8 @@ declare global {
* 而通过设置`activePath`指定激活菜单即可获得高亮,`activePath`为指定激活菜单的`path`
*/
activePath?: string;
/** 当前页面是否已经加载过 */
loaded?: boolean;
}
/**
@@ -91,7 +93,7 @@ declare global {
/** 菜单名称(兼容国际化、非国际化,如何用国际化的写法就必须在根目录的`locales`文件夹下对应添加)`必填` */
title: string;
/** 菜单图标 `可选` */
icon?: string | FunctionalComponent | IconifyIcon;
icon?: string | FunctionalComponent;
/** 是否在菜单中显示(默认`true``可选` */
showLink?: boolean;
/** 菜单升序排序,值越高排的越后(只针对顶级路由)`可选` */
@@ -104,5 +106,6 @@ declare global {
// https://router.vuejs.org/zh/guide/advanced/meta.html#typescript
declare module "vue-router" {
// eslint-disable-next-line
interface RouteMeta extends CustomizeRouteMeta {}
}

View File

@@ -1,4 +1,4 @@
import Vue, { VNode } from "vue";
import type Vue, { type VNode } from "vue";
declare module "*.tsx" {
import Vue from "compatible-vue";
@@ -7,7 +7,9 @@ declare module "*.tsx" {
declare global {
namespace JSX {
// eslint-disable-next-line
interface Element extends VNode {}
// eslint-disable-next-line
interface ElementClass extends Vue {}
interface ElementAttributesProperty {
$props: any;

View File

@@ -1,5 +1,6 @@
declare module "*.vue" {
import type { DefineComponent } from "vue";
// eslint-disable-next-line
const component: DefineComponent<{}, {}, any>;
export default component;
}