Compare commits

..

No commits in common. "main" and "v5.7.0" have entirely different histories.
main ... v5.7.0

105 changed files with 4164 additions and 4274 deletions

2
.nvmrc
View File

@ -1 +1 @@
v22.14.0
v20.13.1

View File

@ -36,7 +36,6 @@
"v-copy",
"v-longpress",
"v-optimize",
"v-perms",
"v-ripple"
],
"vscodeCustomCodeColor.highlightValueColor": "#b392f0",

View File

@ -1,8 +1,8 @@
FROM node:20-alpine as build-stage
FROM node:18-alpine as build-stage
WORKDIR /app
RUN corepack enable
RUN corepack prepare pnpm@latest --activate
RUN corepack prepare pnpm@8.6.10 --activate
RUN npm config set registry https://registry.npmmirror.com

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)
@ -23,12 +15,12 @@ The simplified version is based on the shelf extracted from [vue-pure-admin](htt
## Nanny-level documents
[Click me to view vue-pure-admin documentation](https://pure-admin.cn/)
[Click me to view vue-pure-admin documentation](https://pure-admin.github.io/pure-admin-doc)
[Click me to view @pureadmin/utils documentation](https://pure-admin-utils.netlify.app)
## Quality service, software outsourcing, sponsorship support
[Click me to view details](https://pure-admin.cn/pages/service/)
[Click me to view details](https://pure-admin.github.io/pure-admin-doc/pages/service/)
## Preview

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)
@ -27,12 +19,12 @@
## 配套保姆级文档
[点我查看 vue-pure-admin 文档](https://pure-admin.cn/)
[点我查看 vue-pure-admin 文档](https://pure-admin.github.io/pure-admin-doc)
[点我查看 @pureadmin/utils 文档](https://pure-admin-utils.netlify.app)
## 优质服务、软件外包、赞助支持
[点我查看详情](https://pure-admin.cn/pages/service/)
[点我查看详情](https://pure-admin.github.io/pure-admin-doc/pages/service/)
## 预览

View File

@ -1,13 +1,13 @@
import type { Plugin } from "vite";
import gradient from "gradient-string";
import { getPackageSize } from "./utils";
import dayjs, { type Dayjs } from "dayjs";
import duration from "dayjs/plugin/duration";
import gradientString from "gradient-string";
import boxen, { type Options as BoxenOptions } from "boxen";
dayjs.extend(duration);
const welcomeMessage = gradient(["cyan", "magenta"]).multiline(
`您好! 欢迎使用 pure-admin 开源项目\n我们为您精心准备了下面两个贴心的保姆级文档\nhttps://pure-admin.cn\nhttps://pure-admin-utils.netlify.app`
const welcomeMessage = gradientString("cyan", "magenta").multiline(
`您好! 欢迎使用 pure-admin 开源项目\n我们为您精心准备了下面两个贴心的保姆级文档\nhttps://pure-admin.github.io/pure-admin-doc\nhttps://pure-admin-utils.netlify.app`
);
const boxenOptions: BoxenOptions = {
@ -41,7 +41,7 @@ export function viteBuildInfo(): Plugin {
callback: (size: string) => {
console.log(
boxen(
gradient(["cyan", "magenta"]).multiline(
gradientString("cyan", "magenta").multiline(
`🎉 恭喜打包完成(总用时${dayjs
.duration(endTime.diff(startTime))
.format("mm分ss秒")}${size}`

View File

@ -22,8 +22,12 @@ const include = [
/**
*
* 使
* `@iconify-icons/` `exclude` 使
*/
const exclude = ["@iconify/json"];
const exclude = [
"@iconify-icons/ep",
"@iconify-icons/ri",
"@pureadmin/theme/dist/browser-utils"
];
export { include, exclude };

View File

@ -2,15 +2,15 @@ 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 Inspector from "vite-plugin-vue-inspector";
import { configCompressPlugin } from "./compress";
import removeNoMatch from "vite-plugin-router-warn";
import { visualizer } from "rollup-plugin-visualizer";
import removeConsole from "vite-plugin-remove-console";
import { codeInspectorPlugin } from "code-inspector-plugin";
import { themePreprocessorPlugin } from "@pureadmin/theme";
import { genScssMultipleScopeVars } from "../src/layout/theme";
import { vitePluginFakeServer } from "vite-plugin-fake-server";
export function getPluginsList(
@ -19,20 +19,11 @@ export function getPluginsList(
): PluginOption[] {
const lifecycle = process.env.npm_lifecycle_event;
return [
tailwindcss(),
vue(),
// jsx、tsx语法支持
vueJsx(),
/**
* DOM IDE
* Mac Option + Shift
* Windows Alt + Shift
* https://inspector.fe-dev.cn/guide/start.html
*/
codeInspectorPlugin({
bundler: "vite",
hideConsole: true
}),
// 按下Command(⌘)+Shift(⇧)然后点击页面元素会自动打开本地IDE并跳转到对应的代码位置
Inspector(),
viteBuildInfo(),
/**
* vue-router动态路由警告No match found for location with path
@ -47,13 +38,15 @@ export function getPluginsList(
infixName: false,
enableProd: true
}),
// 自定义主题
themePreprocessorPlugin({
scss: {
multipleScopeVars: genScssMultipleScopeVars(),
extract: true
}
}),
// svg组件化支持
svgLoader(),
// 自动按需加载图标
Icons({
compiler: "vue3",
scale: 1
}),
VITE_CDN ? cdn : null,
configCompressPlugin(VITE_COMPRESSION),
// 线上环境删除console

View File

@ -1,25 +1,26 @@
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 { defineConfig, globalIgnores } from "eslint/config";
import { defineFlatConfig } from "eslint-define-config";
import * as parserTypeScript from "@typescript-eslint/parser";
import pluginTypeScript from "@typescript-eslint/eslint-plugin";
export default defineConfig([
globalIgnores([
export default defineFlatConfig([
{
...js.configs.recommended,
ignores: [
"**/.*",
"dist/*",
"*.d.ts",
"public/*",
"src/assets/**",
"src/**/iconfont/**"
]),
{
...js.configs.recommended,
],
languageOptions: {
globals: {
// types/index.d.ts
// index.d.ts
RefType: "readonly",
EmitType: "readonly",
TargetContext: "readonly",
@ -72,18 +73,26 @@ export default defineConfig([
]
}
},
...tseslint.config({
extends: [...tseslint.configs.recommended],
{
files: ["**/*.?([cm])ts", "**/*.?([cm])tsx"],
languageOptions: {
parser: parserTypeScript,
parserOptions: {
sourceType: "module"
}
},
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",
"@typescript-eslint/prefer-as-const": "warn",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-unsafe-function-type": "off",
"@typescript-eslint/no-import-type-side-effects": "error",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/consistent-type-imports": [
@ -102,20 +111,20 @@ export default defineConfig([
}
]
}
}),
},
{
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-require-imports": "off",
"@typescript-eslint/no-var-requires": "off"
}
},
{
@ -136,19 +145,18 @@ export default defineConfig([
jsx: true
},
extraFileExtensions: [".vue"],
parser: tseslint.parser,
parser: "@typescript-eslint/parser",
sourceType: "module"
}
},
plugins: {
"@typescript-eslint": tseslint.plugin,
vue: pluginVue
},
processor: pluginVue.processors[".vue"],
rules: {
...pluginVue.configs.base.rules,
...pluginVue.configs.essential.rules,
...pluginVue.configs.recommended.rules,
...pluginVue.configs["vue3-essential"].rules,
...pluginVue.configs["vue3-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,6 +10,9 @@
/>
<title>pure-admin-thin</title>
<link rel="icon" href="/favicon.ico" />
<script>
window.process = {};
</script>
</head>
<body>

View File

@ -23,34 +23,17 @@ const permissionRouter = {
}
},
{
path: "/permission/button",
path: "/permission/button/index",
name: "PermissionButton",
meta: {
title: "按钮权限",
roles: ["admin", "common"]
},
children: [
{
path: "/permission/button/router",
component: "permission/button/index",
name: "PermissionButtonRouter",
meta: {
title: "路由返回按钮权限",
roles: ["admin", "common"],
auths: [
"permission:btn:add",
"permission:btn:edit",
"permission:btn:delete"
]
}
},
{
path: "/permission/button/login",
component: "permission/button/perms",
name: "PermissionButtonLogin",
meta: {
title: "登录接口返回按钮权限"
}
}
]
}
]
};

View File

@ -15,8 +15,6 @@ export default defineFakeRoute([
nickname: "小铭",
// 一个用户可能有多个角色
roles: ["admin"],
// 按钮级别权限
permissions: ["*:*:*"],
accessToken: "eyJhbGciOiJIUzUxMiJ9.admin",
refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh",
expires: "2030/10/30 00:00:00"
@ -30,7 +28,6 @@ export default defineFakeRoute([
username: "common",
nickname: "小林",
roles: ["common"],
permissions: ["permission:btn:add", "permission:btn:edit"],
accessToken: "eyJhbGciOiJIUzUxMiJ9.common",
refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh",
expires: "2030/10/30 00:00:00"

View File

@ -1,6 +1,6 @@
{
"name": "pure-admin-thin",
"version": "6.0.0",
"version": "5.7.0",
"private": true,
"type": "module",
"scripts": {
@ -49,94 +49,96 @@
},
"dependencies": {
"@pureadmin/descriptions": "^1.2.1",
"@pureadmin/table": "^3.2.1",
"@pureadmin/utils": "^2.6.0",
"@vueuse/core": "^13.1.0",
"@vueuse/motion": "^3.0.3",
"@pureadmin/table": "^3.1.2",
"@pureadmin/utils": "^2.4.7",
"@vueuse/core": "^10.10.0",
"@vueuse/motion": "^2.2.3",
"animate.css": "^4.1.1",
"axios": "^1.9.0",
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"element-plus": "^2.9.8",
"axios": "^1.7.2",
"dayjs": "^1.11.11",
"echarts": "^5.5.0",
"element-plus": "^2.7.4",
"js-cookie": "^3.0.5",
"localforage": "^1.10.0",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
"pinia": "^3.0.2",
"pinyin-pro": "^3.26.0",
"qs": "^6.14.0",
"path": "^0.12.7",
"pinia": "^2.1.7",
"pinyin-pro": "^3.21.1",
"qs": "^6.12.1",
"responsive-storage": "^2.2.0",
"sortablejs": "^1.15.6",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-tippy": "^6.7.0",
"vue-types": "^6.0.0"
"sortablejs": "^1.15.2",
"vue": "^3.4.27",
"vue-router": "^4.3.2",
"vue-tippy": "^6.4.1",
"vue-types": "^5.1.2"
},
"devDependencies": {
"@commitlint/cli": "^19.8.0",
"@commitlint/config-conventional": "^19.8.0",
"@commitlint/types": "^19.8.0",
"@eslint/js": "^9.25.1",
"@faker-js/faker": "^9.7.0",
"@iconify/json": "^2.2.331",
"@iconify/vue": "4.2.0",
"@tailwindcss/vite": "^4.1.4",
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@commitlint/types": "^19.0.3",
"@eslint/js": "^9.4.0",
"@faker-js/faker": "^8.4.1",
"@iconify-icons/ep": "^1.2.12",
"@iconify-icons/ri": "^1.2.10",
"@iconify/vue": "^4.1.2",
"@pureadmin/theme": "^3.2.0",
"@types/gradient-string": "^1.1.6",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20.17.30",
"@types/node": "^20.14.2",
"@types/nprogress": "^0.2.3",
"@types/path-browserify": "^1.0.3",
"@types/qs": "^6.9.18",
"@types/qs": "^6.9.15",
"@types/sortablejs": "^1.15.8",
"@vitejs/plugin-vue": "^5.2.3",
"@vitejs/plugin-vue-jsx": "^4.1.2",
"boxen": "^8.0.1",
"code-inspector-plugin": "^0.20.10",
"cssnano": "^7.0.6",
"eslint": "^9.25.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-vue": "^10.0.0",
"gradient-string": "^3.0.0",
"husky": "^9.1.7",
"lint-staged": "^15.5.1",
"postcss": "^8.5.3",
"postcss-html": "^1.8.0",
"postcss-load-config": "^6.0.1",
"@typescript-eslint/eslint-plugin": "^7.12.0",
"@typescript-eslint/parser": "^7.12.0",
"@vitejs/plugin-vue": "^5.0.5",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"autoprefixer": "^10.4.19",
"boxen": "^7.1.1",
"cssnano": "^7.0.2",
"eslint": "^9.4.0",
"eslint-config-prettier": "^9.1.0",
"eslint-define-config": "^2.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.26.0",
"gradient-string": "^2.0.2",
"husky": "^9.0.11",
"lint-staged": "^15.2.5",
"postcss": "^8.4.38",
"postcss-html": "^1.7.0",
"postcss-import": "^16.1.0",
"postcss-scss": "^4.0.9",
"prettier": "^3.5.3",
"rimraf": "^6.0.1",
"rollup-plugin-visualizer": "^5.14.0",
"sass": "^1.87.0",
"stylelint": "^16.19.0",
"stylelint-config-recess-order": "^6.0.0",
"stylelint-config-recommended-vue": "^1.6.0",
"stylelint-config-standard-scss": "^14.0.0",
"stylelint-prettier": "^5.0.3",
"prettier": "^3.3.1",
"rimraf": "^5.0.7",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.77.4",
"stylelint": "^16.6.1",
"stylelint-config-recess-order": "^5.0.1",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-standard-scss": "^13.1.0",
"stylelint-prettier": "^5.0.0",
"svgo": "^3.3.2",
"tailwindcss": "^4.1.4",
"typescript": "^5.8.3",
"typescript-eslint": "^8.31.0",
"unplugin-icons": "^22.1.0",
"vite": "^6.3.3",
"tailwindcss": "^3.4.4",
"typescript": "^5.4.5",
"vite": "^5.2.12",
"vite-plugin-cdn-import": "^1.0.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-fake-server": "^2.2.0",
"vite-plugin-fake-server": "^2.1.1",
"vite-plugin-remove-console": "^2.2.0",
"vite-plugin-router-warn": "^1.0.0",
"vite-plugin-vue-inspector": "^5.1.2",
"vite-svg-loader": "^5.1.0",
"vue-eslint-parser": "^10.1.3",
"vue-tsc": "^2.2.10"
"vue-eslint-parser": "^9.4.3",
"vue-tsc": "^1.8.27"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=22.0.0",
"node": "^18.18.0 || ^20.9.0 || >=21.1.0",
"pnpm": ">=9"
},
"pnpm": {
"allowedDeprecatedVersions": {
"are-we-there-yet": "*",
"sourcemap-codec": "*",
"lodash.isequal": "*",
"domexception": "*",
"w3c-hr-time": "*",
"inflight": "*",
@ -147,13 +149,10 @@
"abab": "*",
"glob": "*"
},
"onlyBuiltDependencies": [
"@parcel/watcher",
"core-js",
"es5-ext",
"esbuild",
"typeit",
"vue-demi"
]
"peerDependencyRules": {
"allowedVersions": {
"eslint": "9"
}
}
}
}

6818
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,10 @@
/** @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": "6.0.0",
"Version": "5.7.0",
"Title": "PureAdmin",
"FixedHeader": true,
"HiddenSideBar": false,

View File

@ -11,8 +11,6 @@ export type UserResult = {
nickname: string;
/** 当前登录用户的角色 */
roles: Array<string>;
/** 按钮级别权限 */
permissions: Array<string>;
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */

View File

@ -28,7 +28,8 @@
c = document.createElement("div");
(c.innerHTML = e._iconfont_svg_string_2208059),
(c = c.getElementsByTagName("svg")[0]) &&
((c.style.position = "absolute"),
(c.setAttribute("aria-hidden", "true"),
(c.style.position = "absolute"),
(c.style.width = 0),
(c.style.height = 0),
(c.style.overflow = "hidden"),

View File

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

Before

Width:  |  Height:  |  Size: 332 B

After

Width:  |  Height:  |  Size: 351 B

View File

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

Before

Width:  |  Height:  |  Size: 308 B

After

Width:  |  Height:  |  Size: 327 B

View File

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

Before

Width:  |  Height:  |  Size: 283 B

After

Width:  |  Height:  |  Size: 302 B

View File

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

Before

Width:  |  Height:  |  Size: 360 B

After

Width:  |  Height:  |  Size: 379 B

View File

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

Before

Width:  |  Height:  |  Size: 373 B

After

Width:  |  Height:  |  Size: 392 B

View File

@ -8,14 +8,13 @@ import {
} from "./index";
import { ref, computed } from "vue";
import { isFunction } from "@pureadmin/utils";
import Fullscreen from "~icons/ri/fullscreen-fill";
import ExitFullscreen from "~icons/ri/fullscreen-exit-fill";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
defineOptions({
name: "ReDialog"
});
const sureBtnMap = ref({});
const fullscreen = ref(false);
const footerButtons = computed(() => {
@ -44,26 +43,10 @@ const footerButtons = computed(() => {
bg: true,
popconfirm: options?.popconfirm,
btnClick: ({ dialog: { options, index } }) => {
if (options?.sureBtnLoading) {
sureBtnMap.value[index] = Object.assign(
{},
sureBtnMap.value[index],
{
loading: true
}
);
}
const closeLoading = () => {
if (options?.sureBtnLoading) {
sureBtnMap.value[index].loading = false;
}
};
const done = () => {
closeLoading();
const done = () =>
closeDialog(options, index, { command: "sure" });
};
if (options?.beforeSure && isFunction(options?.beforeSure)) {
options.beforeSure(done, { options, index, closeLoading });
options.beforeSure(done, { options, index });
} else {
done();
}
@ -79,7 +62,7 @@ const fullscreenClass = computed(() => {
"el-dialog__close",
"-translate-x-2",
"cursor-pointer",
"hover:text-[red]!"
"hover:!text-[red]"
];
});
@ -189,7 +172,6 @@ function handleClose(
<el-button
v-else
v-bind="btn"
:loading="key === 1 && sureBtnMap[index]?.loading"
@click="
btn.btnClick({
dialog: { options, index },

View File

@ -69,11 +69,11 @@ type DialogProps = {
type Popconfirm = {
/** 标题 */
title?: string;
/** 确按钮文字 */
/** 确按钮文字 */
confirmButtonText?: string;
/** 取消按钮文字 */
cancelButtonText?: string;
/** 确按钮类型,默认 `primary` */
/** 确按钮类型,默认 `primary` */
confirmButtonType?: ButtonType;
/** 取消按钮类型,默认 `text` */
cancelButtonType?: ButtonType;
@ -121,7 +121,7 @@ type ButtonProps = {
round?: boolean;
/** 是否为圆形按钮,默认 `false` */
circle?: boolean;
/** 确按钮的 `Popconfirm` 气泡确认框相关配置 */
/** 确按钮的 `Popconfirm` 气泡确认框相关配置 */
popconfirm?: Popconfirm;
/** 是否为加载中状态,默认 `false` */
loading?: boolean;
@ -160,10 +160,8 @@ interface DialogOptions extends DialogProps {
props?: any;
/** 是否隐藏 `Dialog` 按钮操作区的内容 */
hideFooter?: boolean;
/** 确按钮的 `Popconfirm` 气泡确认框相关配置 */
/** 确按钮的 `Popconfirm` 气泡确认框相关配置 */
popconfirm?: Popconfirm;
/** 点击确定按钮后是否开启 `loading` 加载动画 */
sureBtnLoading?: boolean;
/**
* @description
* @see {@link https://element-plus.org/zh-CN/component/dialog.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%A4%B4%E9%83%A8}
@ -261,13 +259,10 @@ interface DialogOptions extends DialogProps {
done: Function,
{
options,
index,
closeLoading
index
}: {
options: DialogOptions;
index: number;
/** 关闭确定按钮的 `loading` 加载动画 */
closeLoading: Function;
}
) => void;
}

View File

@ -1,10 +1,10 @@
import type { iconType } from "./types";
import { h, defineComponent, type Component } from "vue";
import { FontIcon, IconifyIconOnline, IconifyIconOffline } from "../index";
import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
/**
* `iconfont` `svg` `iconify`
* @see {@link https://pure-admin.cn/pages/icon/}
* @see {@link https://pure-admin.github.io/pure-admin-doc/pages/icon/}
* @param icon
* @param attrs iconType
* @returns Component
@ -49,12 +49,10 @@ export function useRenderIcon(icon: any, attrs?: iconType): Component {
return defineComponent({
name: "Icon",
render() {
if (!icon) return;
const IconifyIcon = icon.includes(":")
? IconifyIconOnline
: IconifyIconOffline;
const IconifyIcon =
icon && icon.includes(":") ? IconifyIconOnline : IconifyIconOffline;
return h(IconifyIcon, {
icon,
icon: icon,
...attrs
});
}

View File

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

View File

@ -13,12 +13,10 @@ 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" },
@ -28,20 +26,5 @@ export default defineComponent({
default: () => []
}
);
} else {
return h(
this.icon,
{
"aria-hidden": false,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
...attrs
},
{
default: () => []
}
);
}
}
});

View File

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

View File

@ -1,23 +1,14 @@
// 这里存放本地图标,在 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 添加即可渲染菜单图标
icons.forEach(([name, icon]) => {
addIcon(name as string, getSvgInfo(icon as string));
});
// @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);

View File

@ -1,5 +0,0 @@
import perms from "./src/perms";
const Perms = perms;
export { Perms };

View File

@ -1,20 +0,0 @@
import { defineComponent, Fragment } from "vue";
import { hasPerms } from "@/utils/auth";
export default defineComponent({
name: "Perms",
props: {
value: {
type: undefined,
default: []
}
},
setup(props, { slots }) {
return () => {
if (!slots) return null;
return hasPerms(props.value) ? (
<Fragment>{slots.default?.()}</Fragment>
) : null;
};
}
});

View File

@ -17,8 +17,6 @@ import {
getKeyList
} from "@pureadmin/utils";
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";
@ -53,12 +51,11 @@ const props = {
export default defineComponent({
name: "PureTableBar",
props,
emits: ["refresh", "fullscreen"],
emits: ["refresh"],
setup(props, { emit, slots, attrs }) {
const size = ref("default");
const loading = ref(false);
const checkAll = ref(true);
const isFullscreen = ref(false);
const isIndeterminate = ref(false);
const instance = getCurrentInstance()!;
const isExpandAll = ref(props.isExpandAll);
@ -86,9 +83,9 @@ export default defineComponent({
"text-black",
"dark:text-white",
"duration-100",
"hover:text-primary!",
"hover:!text-primary",
"cursor-pointer",
"outline-hidden"
"outline-none"
];
});
@ -116,11 +113,6 @@ export default defineComponent({
toggleRowExpansionAll(props.tableRef.data, isExpandAll.value);
}
function onFullscreen() {
isFullscreen.value = !isFullscreen.value;
emit("fullscreen", isFullscreen.value);
}
function toggleRowExpansionAll(data, isExpansion) {
data.forEach(item => {
props.tableRef.toggleRowExpansion(item, isExpansion);
@ -247,18 +239,7 @@ export default defineComponent({
return () => (
<>
<div
{...attrs}
class={[
"w-full",
"px-2",
"pb-2",
"bg-bg_color",
isFullscreen.value
? ["h-full!", "z-2002", "fixed", "inset-0"]
: "mt-2"
]}
>
<div {...attrs} class="w-[99/100] mt-2 px-2 pb-2 bg-bg_color">
<div class="flex justify-between w-full h-[60px] p-4">
{slots?.title ? (
slots.title()
@ -312,7 +293,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 +323,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;
@ -372,14 +353,6 @@ export default defineComponent({
</el-scrollbar>
</div>
</el-popover>
<el-divider direction="vertical" />
<iconifyIconOffline
class={["w-[16px]", iconClass.value]}
icon={isFullscreen.value ? ExitFullscreen : Fullscreen}
v-tippy={isFullscreen.value ? "退出全屏" : "全屏"}
onClick={() => onFullscreen()}
/>
</div>
</div>
{slots.default({

View File

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

View File

@ -6,7 +6,7 @@ export interface OptionsType {
label?: string | (() => VNode | Component);
/**
* @description `useRenderIcon`
* @see {@link https://pure-admin.cn/pages/icon/#%E9%80%9A%E7%94%A8%E5%9B%BE%E6%A0%87-userendericon-hooks }
* @see {@link https://pure-admin.github.io/pure-admin-doc/pages/icon/#%E9%80%9A%E7%94%A8%E5%9B%BE%E6%A0%87-userendericon-hooks }
*/
icon?: string | Component;
/** 图标属性、样式配置 */

View File

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

View File

@ -2,5 +2,4 @@ export * from "./auth";
export * from "./copy";
export * from "./longpress";
export * from "./optimize";
export * from "./perms";
export * from "./ripple";

View File

@ -1,15 +0,0 @@
import { hasPerms } from "@/utils/auth";
import type { Directive, DirectiveBinding } from "vue";
export const perms: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding<string | Array<string>>) {
const { value } = binding;
if (value) {
!hasPerms(value) && el.parentNode?.removeChild(el);
} else {
throw new Error(
"[Directive: perms]: need perms! Like v-perms=\"['btn.add','btn.edit']\""
);
}
}
};

View File

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

View File

@ -1,4 +1,4 @@
<script setup lang="ts">
<script lang="ts" setup>
import { getConfig } from "@/config";
const TITLE = getConfig("Title");

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 "~icons/ri/logout-circle-r-line";
import Setting from "~icons/ri/settings-3-line";
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import Setting from "@iconify-icons/ri/settings-3-line";
const {
layout,
@ -24,7 +24,7 @@ const {
</script>
<template>
<div class="navbar bg-[#fff] shadow-xs shadow-[rgba(0,21,41,0.08)]">
<div class="navbar bg-[#fff] shadow-sm 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-0 border-b-[1px] border-solid border-[#f0f0f0] dark:border-[#303030]"
class="notice-container 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 lang="scss" scoped>
<style scoped lang="scss">
.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 "~icons/ep/bell";
import BellIcon from "@iconify-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 "~icons/ep/close";
import CloseIcon from "@iconify-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-hidden",
"outline-none",
"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-0 border-b-[1px] border-solid border-[var(--pure-border-color)]"
class="project-configuration 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-0 border-t-[1px] border-solid border-[var(--pure-border-color)]"
class="flex justify-end p-3 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%);
transform: translate(100%);
transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
transform: translate(100%);
}
.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 "~icons/ri/arrow-up-line";
import ArrowDownLine from "~icons/ri/arrow-down-line";
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
import ArrowDownLine from "@iconify-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 "~icons/ep/star";
import CloseIcon from "~icons/ep/close";
import StarIcon from "@iconify-icons/ep/star";
import CloseIcon from "@iconify-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 "~icons/ri/search-line";
import SearchIcon from "@iconify-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

@ -13,14 +13,15 @@ import { emitter } from "@/utils/mitt";
import LayPanel from "../lay-panel/index.vue";
import { useNav } from "@/layout/hooks/useNav";
import { useAppStoreHook } from "@/store/modules/app";
import { toggleTheme } from "@pureadmin/theme/dist/browser-utils";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import Segmented, { type OptionsType } from "@/components/ReSegmented";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import { useDark, useGlobal, debounce, isNumber } from "@pureadmin/utils";
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 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 DayIcon from "@/assets/svg/day.svg?component";
import DarkIcon from "@/assets/svg/dark.svg?component";
import SystemIcon from "@/assets/svg/system.svg?component";
@ -47,7 +48,9 @@ const {
if (unref(layoutTheme)) {
const layout = unref(layoutTheme).layout;
const theme = unref(layoutTheme).theme;
document.documentElement.setAttribute("data-theme", theme);
toggleTheme({
scopeName: `layout-theme-${theme}`
});
setLayoutModel(layout);
}
@ -187,7 +190,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 +336,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 +355,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 +398,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,19 +427,21 @@ onUnmounted(() => removeMatchMedia);
>
<IconifyIconOffline
:icon="settings.stretch ? RightArrow : LeftArrow"
height="20"
/>
<div
class="grow border-0 border-b border-dashed"
class="flex-grow 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"
@ -445,7 +450,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

@ -1,24 +1,17 @@
<script setup lang="ts">
import { emitter } from "@/utils/mitt";
import { isAllEmpty } from "@pureadmin/utils";
import { ref, nextTick, computed } from "vue";
import { useNav } from "@/layout/hooks/useNav";
import LaySearch from "../lay-search/index.vue";
import LayNotice from "../lay-notice/index.vue";
import { responsiveStorageNameSpace } from "@/config";
import { ref, nextTick, computed, onMounted } from "vue";
import { storageLocal, isAllEmpty } from "@pureadmin/utils";
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 "~icons/ri/logout-circle-r-line";
import Setting from "~icons/ri/settings-3-line";
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import Setting from "@iconify-icons/ri/settings-3-line";
const menuRef = ref();
const showLogo = ref(
storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}configure`
)?.showLogo ?? true
);
const {
route,
@ -39,12 +32,6 @@ const defaultActive = computed(() =>
nextTick(() => {
menuRef.value?.handleResize();
});
onMounted(() => {
emitter.on("logoChange", key => {
showLogo.value = key;
});
});
</script>
<template>
@ -52,7 +39,7 @@ onMounted(() => {
v-loading="usePermissionStoreHook().wholeMenus.length === 0"
class="horizontal-header"
>
<div v-if="showLogo" class="horizontal-header-left" @click="backTopMenu">
<div class="horizontal-header-left" @click="backTopMenu">
<img :src="getLogo()" alt="logo" />
<span>{{ title }}</span>
</div>

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 "~icons/ri/logout-circle-r-line";
import Setting from "~icons/ri/settings-3-line";
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import Setting from "@iconify-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 "~icons/ri/arrow-left-double-fill";
import ArrowLeft from "@iconify-icons/ri/arrow-left-double-fill";
interface Props {
isActive?: boolean;
isActive: boolean;
}
withDefaults(defineProps<Props>(), {

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import path from "path";
import { getConfig } from "@/config";
import { posix } from "path-browserify";
import { menuType } from "@/layout/types";
import { ReText } from "@/components/ReText";
import { useNav } from "@/layout/hooks/useNav";
@ -16,10 +16,10 @@ import {
useAttrs
} from "vue";
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";
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";
const attrs = useAttrs();
const { layout, isCollapse, tooltipEffect, getDivStyle } = useNav();
@ -60,21 +60,6 @@ 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 {
@ -113,7 +98,8 @@ function resolvePath(routePath) {
if (httpReg.test(routePath) || httpReg.test(props.basePath)) {
return routePath || props.basePath;
} else {
return posix.resolve(props.basePath, routePath);
// 使path.posix.resolvepath.resolve windows使electron
return path.posix.resolve(props.basePath, routePath);
}
}
</script>
@ -158,7 +144,7 @@ function resolvePath(routePath) {
item?.pathList?.length === 2)
"
truncated
class="w-full! px-3! min-w-[54px]! text-center! text-inherit!"
class="!w-full !pl-4 !text-inherit"
>
{{ onlyOneChild.meta.title }}
</el-text>
@ -170,7 +156,7 @@ function resolvePath(routePath) {
offset: [0, -10],
theme: tooltipEffect
}"
class="w-full! text-inherit!"
class="!w-full !text-inherit"
>
{{ onlyOneChild.meta.title }}
</ReText>
@ -209,7 +195,15 @@ function resolvePath(routePath) {
offset: [0, -10],
theme: tooltipEffect
}"
:class="textClass"
:class="{
'!w-full': true,
'!text-inherit': true,
'!pl-4':
layout !== 'horizontal' &&
isCollapse &&
!toRaw(item.meta.icon) &&
item.parentId === null
}"
>
{{ 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 "~icons/ri/menu-fold-fill";
import MenuFold from "@iconify-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);
color: $subMenuActiveText;
text-overflow: ellipsis;
white-space: nowrap;
}
}

View File

@ -1,9 +1,9 @@
<script setup lang="ts">
import MenuFold from "~icons/ri/menu-fold-fill";
import MenuUnfold from "~icons/ri/menu-unfold-fill";
import MenuFold from "@iconify-icons/ri/menu-fold-fill";
import MenuUnfold from "@iconify-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

@ -17,11 +17,11 @@ import {
useResizeObserver
} from "@pureadmin/utils";
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";
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";
const {
Close,
@ -36,7 +36,6 @@ const {
buttonLeft,
showModel,
translateX,
isFixedTag,
pureSetting,
activeIndex,
getTabStyle,
@ -509,7 +508,6 @@ function tagOnClick(item) {
} else {
router.push({ path });
}
emitter.emit("tagOnClick", item);
}
onClickOutside(contextmenuRef, closeMenu, {
@ -578,7 +576,7 @@ onBeforeUnmount(() => {
'scroll-item is-closable',
linkIsActive(item),
showModel === 'chrome' && 'chrome-item',
isFixedTag(item) && 'fixed-tag'
!isAllEmpty(item?.meta?.fixedTag) && 'fixed-tag'
]"
@contextmenu.prevent="openMenu(item, $event)"
@mouseenter.prevent="onMouseenter(index)"
@ -587,16 +585,16 @@ 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>
<span
v-if="
isFixedTag(item)
? false
: iconIsActive(item, index) ||
isAllEmpty(item?.meta?.fixedTag)
? iconIsActive(item, index) ||
(index === activeIndex && index !== 0)
: false
"
class="el-icon-close"
@click.stop="deleteMenu(item)"
@ -617,7 +615,7 @@ onBeforeUnmount(() => {
{{ item.meta.title }}
</span>
<span
v-if="isFixedTag(item) ? false : index !== 0"
v-if="isAllEmpty(item?.meta?.fixedTag) ? index !== 0 : false"
class="chrome-close-btn"
@click.stop="deleteMenu(item)"
>

View File

@ -6,9 +6,14 @@ import { routerArrays } from "@/layout/types";
import { router, resetRouter } from "@/router";
import type { themeColorsType } from "../types";
import { useAppStoreHook } from "@/store/modules/app";
import { useGlobal, storageLocal } from "@pureadmin/utils";
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { darken, lighten, useGlobal, storageLocal } from "@pureadmin/utils";
import {
darken,
lighten,
toggleTheme
} from "@pureadmin/theme/dist/browser-utils";
export function useDataThemeChange() {
const { layoutTheme, layout } = useLayout();
@ -49,7 +54,9 @@ export function useDataThemeChange() {
isClick = true
) {
layoutTheme.value.theme = theme;
document.documentElement.setAttribute("data-theme", theme);
toggleTheme({
scopeName: `layout-theme-${theme}`
});
// 如果非isClick保留之前的themeColor
const storageThemeColor = $storage.layout.themeColor;
$storage.layout = {

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 "~icons/ri/fullscreen-exit-fill";
import Fullscreen from "~icons/ri/fullscreen-fill";
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
import Fullscreen from "@iconify-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 "~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";
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";
export function useTags() {
const route = useRoute();
@ -124,12 +124,6 @@ export function useTags() {
}
}
const isFixedTag = computed(() => {
return item => {
return isBoolean(item?.meta?.fixedTag) && item?.meta?.fixedTag === true;
};
});
const iconIsActive = computed(() => {
return (item, index) => {
if (index === 0) return;
@ -226,7 +220,6 @@ export function useTags() {
buttonTop,
buttonLeft,
translateX,
isFixedTag,
pureSetting,
activeIndex,
getTabStyle,

View File

@ -208,8 +208,8 @@ const LayHeader = defineComponent({
height: 100%;
&::after {
clear: both;
display: table;
clear: both;
content: "";
}
@ -222,7 +222,7 @@ const LayHeader = defineComponent({
.app-mask {
position: absolute;
top: 0;
z-index: 2001;
z-index: 999;
width: 100%;
height: 100%;
background: #000;

129
src/layout/theme/index.ts Normal file
View File

@ -0,0 +1,129 @@
/**
* @description 使
*/
import type { multipleScopeVarsOptions } from "@pureadmin/theme";
/** 预设主题色 */
const themeColors = {
/* 亮白色 */
light: {
subMenuActiveText: "#000000d9",
menuBg: "#fff",
menuHover: "#f6f6f6",
subMenuBg: "#fff",
subMenuActiveBg: "#e0ebf6",
menuText: "rgb(0 0 0 / 60%)",
sidebarLogo: "#fff",
menuTitleHover: "#000",
menuActiveBefore: "#4091f7"
},
/* 道奇蓝 */
default: {
subMenuActiveText: "#fff",
menuBg: "#001529",
menuHover: "rgb(64 145 247 / 15%)",
subMenuBg: "#0f0303",
subMenuActiveBg: "#4091f7",
menuText: "rgb(254 254 254 / 65%)",
sidebarLogo: "#002140",
menuTitleHover: "#fff",
menuActiveBefore: "#4091f7"
},
/* 深紫罗兰色 */
saucePurple: {
subMenuActiveText: "#fff",
menuBg: "#130824",
menuHover: "rgb(105 58 201 / 15%)",
subMenuBg: "#000",
subMenuActiveBg: "#693ac9",
menuText: "#7a80b4",
sidebarLogo: "#1f0c38",
menuTitleHover: "#fff",
menuActiveBefore: "#693ac9"
},
/* 深粉色 */
pink: {
subMenuActiveText: "#fff",
menuBg: "#28081a",
menuHover: "rgb(216 68 147 / 15%)",
subMenuBg: "#000",
subMenuActiveBg: "#d84493",
menuText: "#7a80b4",
sidebarLogo: "#3f0d29",
menuTitleHover: "#fff",
menuActiveBefore: "#d84493"
},
/* 猩红色 */
dusk: {
subMenuActiveText: "#fff",
menuBg: "#2a0608",
menuHover: "rgb(225 60 57 / 15%)",
subMenuBg: "#000",
subMenuActiveBg: "#e13c39",
menuText: "rgb(254 254 254 / 65.1%)",
sidebarLogo: "#42090c",
menuTitleHover: "#fff",
menuActiveBefore: "#e13c39"
},
/* 橙红色 */
volcano: {
subMenuActiveText: "#fff",
menuBg: "#2b0e05",
menuHover: "rgb(232 95 51 / 15%)",
subMenuBg: "#0f0603",
subMenuActiveBg: "#e85f33",
menuText: "rgb(254 254 254 / 65%)",
sidebarLogo: "#441708",
menuTitleHover: "#fff",
menuActiveBefore: "#e85f33"
},
/* 绿宝石 */
mingQing: {
subMenuActiveText: "#fff",
menuBg: "#032121",
menuHover: "rgb(89 191 193 / 15%)",
subMenuBg: "#000",
subMenuActiveBg: "#59bfc1",
menuText: "#7a80b4",
sidebarLogo: "#053434",
menuTitleHover: "#fff",
menuActiveBefore: "#59bfc1"
},
/* 酸橙绿 */
auroraGreen: {
subMenuActiveText: "#fff",
menuBg: "#0b1e15",
menuHover: "rgb(96 172 128 / 15%)",
subMenuBg: "#000",
subMenuActiveBg: "#60ac80",
menuText: "#7a80b4",
sidebarLogo: "#112f21",
menuTitleHover: "#fff",
menuActiveBefore: "#60ac80"
}
};
/**
* @description
*/
export const genScssMultipleScopeVars = (): multipleScopeVarsOptions[] => {
const result = [] as multipleScopeVarsOptions[];
Object.keys(themeColors).forEach(key => {
result.push({
scopeName: `layout-theme-${key}`,
varsContent: `
$subMenuActiveText: ${themeColors[key].subMenuActiveText} !default;
$menuBg: ${themeColors[key].menuBg} !default;
$menuHover: ${themeColors[key].menuHover} !default;
$subMenuBg: ${themeColors[key].subMenuBg} !default;
$subMenuActiveBg: ${themeColors[key].subMenuActiveBg} !default;
$menuText: ${themeColors[key].menuText} !default;
$sidebarLogo: ${themeColors[key].sidebarLogo} !default;
$menuTitleHover: ${themeColors[key].menuTitleHover} !default;
$menuActiveBefore: ${themeColors[key].menuActiveBefore} !default;
`
} as multipleScopeVarsOptions);
});
return result;
};

View File

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

View File

@ -42,9 +42,7 @@ app.component("FontIcon", FontIcon);
// 全局注册按钮级别权限组件
import { Auth } from "@/components/ReAuth";
import { Perms } from "@/components/RePerms";
app.component("Auth", Auth);
app.component("Perms", Perms);
// 全局注册vue-tippy
import "tippy.js/dist/tippy.css";

View File

@ -107,7 +107,6 @@ import {
ElWatermark,
ElTour,
ElTourStep,
ElSegmented,
/**
* 便 element-plus 使
* https://github.com/element-plus/element-plus/blob/dev/packages/element-plus/plugin.ts#L11-L16
@ -222,8 +221,7 @@ const components = [
ElUpload,
ElWatermark,
ElTour,
ElTourStep,
ElSegmented
ElTourStep
];
const plugins = [

View File

@ -6,13 +6,7 @@ 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,
cloneDeep,
isAllEmpty,
storageLocal
} from "@pureadmin/utils";
import { isUrl, openLink, storageLocal, isAllEmpty } from "@pureadmin/utils";
import {
ascending,
getTopMenu,
@ -26,9 +20,9 @@ import {
} from "./utils";
import {
type Router,
createRouter,
type RouteRecordRaw,
type RouteComponent,
createRouter
type RouteComponent
} from "vue-router";
import {
type DataInfo,
@ -60,9 +54,6 @@ 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)
@ -95,13 +86,17 @@ export const router: Router = createRouter({
/** 重置路由 */
export function resetRouter() {
router.clearRoutes();
for (const route of initConstantRoutes.concat(...(remainingRouter as any))) {
router.addRoute(route);
}
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))))
formatFlatteningRoutes(
buildHierarchyTree(ascending(routes.flat(Infinity)))
)
);
}
});
usePermissionStoreHook().clearAllCachePage();
}

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

@ -172,8 +172,6 @@ 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);
}
}
@ -357,7 +355,7 @@ function getAuths(): Array<string> {
return router.currentRoute.value.meta.auths as Array<string>;
}
/** 是否有按钮级别的权限(根据路由`meta`中的`auths`字段进行判断)*/
/** 是否有按钮级别的权限 */
function hasAuth(value: string | Array<string>): boolean {
if (!value) return false;
/** 从当前路由的`meta`字段里获取按钮级别的所有自定义`code`值 */

View File

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

View File

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

View File

@ -14,7 +14,8 @@ import {
} from "../utils";
import { usePermissionStoreHook } from "./permission";
export const useMultiTagsStore = defineStore("pure-multiTags", {
export const useMultiTagsStore = defineStore({
id: "pure-multiTags",
state: () => ({
// 存储标签页信息(路由信息)
multiTags: storageLocal().getItem<StorageConfigs>(
@ -23,12 +24,12 @@ export const useMultiTagsStore = defineStore("pure-multiTags", {
? storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}tags`
)
: ([
: [
...routerArrays,
...usePermissionStoreHook().flatteningRoutes.filter(
v => v?.meta?.fixedTag
)
] as any),
],
multiTagsCache: storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}configure`
)?.multiTagsCache

View File

@ -12,7 +12,8 @@ import {
} from "../utils";
import { useMultiTagsStoreHook } from "./multiTags";
export const usePermissionStore = defineStore("pure-permission", {
export const usePermissionStore = defineStore({
id: "pure-permission",
state: () => ({
// 静态路由生成的菜单
constantMenus,
@ -30,7 +31,7 @@ export const usePermissionStore = defineStore("pure-permission", {
filterTree(ascending(this.constantMenus.concat(routes)))
);
this.flatteningRoutes = formatFlatteningRoutes(
this.constantMenus.concat(routes) as any
this.constantMenus.concat(routes)
);
},
cacheOperate({ mode, name }: cacheType) {

View File

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

View File

@ -16,7 +16,8 @@ import {
import { useMultiTagsStoreHook } from "./multiTags";
import { type DataInfo, setToken, removeToken, userKey } from "@/utils/auth";
export const useUserStore = defineStore("pure-user", {
export const useUserStore = defineStore({
id: "pure-user",
state: (): userType => ({
// 头像
avatar: storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? "",
@ -26,9 +27,6 @@ export const useUserStore = defineStore("pure-user", {
nickname: storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? "",
// 页面级别权限
roles: storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [],
// 按钮级别权限
permissions:
storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? [],
// 是否勾选了登录页的免登录
isRemembered: false,
// 登录页的免登录存储几天默认7天
@ -51,10 +49,6 @@ export const useUserStore = defineStore("pure-user", {
SET_ROLES(roles: Array<string>) {
this.roles = roles;
},
/** 存储按钮级别权限 */
SET_PERMS(permissions: Array<string>) {
this.permissions = permissions;
},
/** 存储是否勾选了登录页的免登录 */
SET_ISREMEMBERED(bool: boolean) {
this.isRemembered = bool;
@ -80,7 +74,6 @@ export const useUserStore = defineStore("pure-user", {
logOut() {
this.username = "";
this.roles = [];
this.permissions = [];
removeToken();
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
resetRouter();

View File

@ -41,7 +41,6 @@ export type userType = {
username?: string;
nickname?: string;
roles?: Array<string>;
permissions?: Array<string>;
isRemembered?: boolean;
loginDay?: number;
};

View File

@ -83,8 +83,8 @@
.el-upload-list__item.is-ready &.el-icon--close {
width: 24px;
height: 24px;
outline: none;
border-radius: 4px;
outline: none;
transition:
background-color 0.2s,
color 0.2s;
@ -117,8 +117,8 @@
}
& .el-message__closeBtn {
outline: none;
border-radius: 4px;
outline: none;
transition:
background-color 0.2s,
color 0.2s;
@ -163,10 +163,9 @@
/* 仿 el-scrollbar 滚动条样式支持大多数浏览器如Chrome、Edge、Firefox、Safari等。整体暗色风格在 src/style/dark.scss 文件进行了适配 */
.pure-scrollbar {
scrollbar-color: rgb(221 222 224) transparent; /* 滑块颜色、轨道颜色 */
/* Firefox */
scrollbar-width: thin; /* 可选值为 'auto', 'thin', 'none' */
scrollbar-color: rgb(221 222 224) transparent; /* 滑块颜色、轨道颜色 */
::-webkit-scrollbar {
width: 6px; /* 滚动条宽度 */
}

View File

@ -1,8 +1,7 @@
@use "theme";
@use "transition";
@use "element-plus";
@use "sidebar";
@use "dark";
@import "./transition";
@import "./element-plus";
@import "./sidebar";
@import "./dark";
/* 自定义全局 CssVar */
:root {
@ -14,16 +13,6 @@
/* switch关闭状态下的color 需要时可取用 */
--pure-switch-off-color: #a6a6a6;
/** 主题色 */
--pure-theme-sub-menu-active-text: initial;
--pure-theme-menu-bg: none;
--pure-theme-menu-hover: none;
--pure-theme-sub-menu-bg: transparent;
--pure-theme-menu-text: initial;
--pure-theme-sidebar-logo: none;
--pure-theme-menu-title-hover: initial;
--pure-theme-menu-active-before: transparent;
}
/* 灰色模式 */

View File

@ -1,3 +1,12 @@
*,
::before,
::after {
box-sizing: border-box;
border-color: currentColor;
border-style: solid;
border-width: 0;
}
#app {
width: 100%;
height: 100%;
@ -16,8 +25,7 @@ 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;
@ -49,9 +57,8 @@ 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

@ -1,5 +1,7 @@
/* $sideBarWidth: vertical 模式下主体内容距离网页文档左侧的距离 */
@mixin merge-style($sideBarWidth) {
$menuActiveText: #7a80b4;
@media screen and (width >= 150px) and (width <= 420px) {
.app-main-nofixed-header {
overflow-y: hidden;
@ -92,7 +94,7 @@
height: 100%;
overflow: visible;
font-size: 0;
background: var(--pure-theme-menu-bg) !important;
background: $menuBg;
border-right: 1px solid var(--pure-border-color);
/* 展开动画 */
@ -148,11 +150,11 @@
.el-menu-item,
.el-sub-menu__title {
height: 50px;
color: var(--pure-theme-menu-text);
color: $menuText;
background-color: transparent !important;
&:hover {
color: var(--pure-theme-menu-title-hover) !important;
color: $menuTitleHover !important;
}
div,
@ -171,15 +173,15 @@
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: var(--pure-theme-sub-menu-active-text) !important;
color: $subMenuActiveText !important;
i {
color: var(--pure-theme-sub-menu-active-text) !important;
color: $subMenuActiveText !important;
}
}
.is-active {
color: var(--pure-theme-sub-menu-active-text) !important;
color: $subMenuActiveText !important;
transition: color 0.3s;
}
@ -191,8 +193,8 @@
.el-menu-item.is-active.nest-menu::before {
position: absolute;
inset: 0 8px;
clear: both;
margin: 4px 0;
clear: both;
content: "";
background: var(--el-color-primary) !important;
border-radius: 3px;
@ -202,7 +204,7 @@
& .el-sub-menu .el-menu-item {
min-width: $sideBarWidth !important;
font-size: 14px;
background-color: var(--pure-theme-sub-menu-bg) !important;
background-color: $subMenuBg !important;
}
/* 有子集的激活菜单左侧小竖条 */
@ -212,13 +214,13 @@
position: absolute;
top: 0;
left: 0;
clear: both;
width: 2px;
height: 100%;
clear: both;
content: "";
background-color: var(--pure-theme-menu-active-before);
transform: translateY(0);
background-color: $menuActiveBefore;
transition: all var(--pure-transition-duration) ease-in-out;
transform: translateY(0);
}
.el-menu--collapse .outer-most.el-sub-menu > .el-sub-menu__title::before {
@ -240,8 +242,8 @@
.is-active.submenu-title-noDropdown.outer-most::before {
position: absolute;
inset: 0 8px;
clear: both;
margin: 4px 0;
clear: both;
content: "";
background: var(--el-color-primary) !important;
border-radius: 3px;
@ -251,7 +253,7 @@
/* vertical 菜单折叠 */
.el-menu--vertical {
.el-menu--popup {
background-color: var(--pure-theme-sub-menu-bg) !important;
background-color: $subMenuBg !important;
.el-menu-item {
span {
@ -269,10 +271,10 @@
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: var(--pure-theme-sub-menu-active-text) !important;
color: $subMenuActiveText !important;
i {
color: var(--pure-theme-sub-menu-active-text) !important;
color: $subMenuActiveText !important;
}
}
@ -280,23 +282,23 @@
.el-menu .el-sub-menu__title {
min-width: $sideBarWidth !important;
font-size: 14px;
background-color: var(--pure-theme-sub-menu-bg) !important;
background-color: $subMenuBg !important;
}
.el-menu-item,
.el-sub-menu__title {
height: 50px;
line-height: 50px;
color: var(--pure-theme-menu-text);
background-color: var(--pure-theme-sub-menu-bg);
color: $menuText;
background-color: $subMenuBg;
&:hover {
color: var(--pure-theme-menu-title-hover) !important;
color: $menuTitleHover !important;
}
}
.is-active {
color: var(--pure-theme-sub-menu-active-text) !important;
color: $subMenuActiveText !important;
transition: color 0.3s;
}
@ -340,15 +342,15 @@
}
.el-menu--popup {
background-color: var(--pure-theme-sub-menu-bg) !important;
background-color: $subMenuBg !important;
a > .is-active.submenu-title-noDropdown {
border-bottom: none;
}
.el-menu-item {
color: var(--pure-theme-menu-text);
background-color: var(--pure-theme-sub-menu-bg);
color: $menuText;
background-color: $subMenuBg;
span {
font-size: 14px;
@ -356,7 +358,7 @@
}
.el-sub-menu__title {
color: var(--pure-theme-menu-text);
color: $menuText;
}
}
@ -364,31 +366,31 @@
.el-menu .el-sub-menu__title {
min-width: $sideBarWidth !important;
font-size: 14px;
background-color: var(--pure-theme-sub-menu-bg) !important;
background-color: $subMenuBg !important;
&:hover {
color: var(--pure-theme-menu-title-hover) !important;
color: $menuTitleHover !important;
}
}
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: var(--pure-theme-sub-menu-active-text) !important;
color: $subMenuActiveText !important;
i {
color: var(--pure-theme-sub-menu-active-text) !important;
color: $subMenuActiveText !important;
}
}
.nest-menu .el-sub-menu > .el-sub-menu__title,
.el-menu-item {
&:hover {
color: var(--pure-theme-menu-title-hover) !important;
color: $menuTitleHover !important;
}
}
.el-menu-item.is-active {
color: var(--pure-theme-sub-menu-active-text) !important;
color: $subMenuActiveText !important;
transition: color 0.3s;
}
@ -413,7 +415,7 @@
justify-content: space-around;
width: 100%;
height: 48px;
background: var(--pure-theme-menu-bg) !important;
background: $menuBg;
.horizontal-header-left {
display: flex;
@ -435,11 +437,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);
color: $subMenuActiveText;
text-overflow: ellipsis;
white-space: nowrap;
}
}
@ -456,7 +458,7 @@
align-items: center;
justify-content: flex-end;
min-width: 340px;
color: var(--pure-theme-sub-menu-active-text);
color: $subMenuActiveText;
/* 搜索 */
.search-container,
@ -469,13 +471,13 @@
/* 设置 */
.set-icon {
&:hover {
background: var(--pure-theme-menu-hover);
background: $menuHover;
}
}
.dropdown-badge {
height: 48px;
color: var(--pure-theme-sub-menu-active-text);
color: $subMenuActiveText;
}
.el-dropdown-link {
@ -484,7 +486,7 @@
justify-content: space-around;
height: 48px;
padding: 10px;
color: var(--pure-theme-sub-menu-active-text);
color: $subMenuActiveText;
cursor: pointer;
p {
@ -509,10 +511,10 @@
.el-menu-item,
.el-sub-menu__title {
padding-right: var(--el-menu-base-level-padding);
color: var(--pure-theme-menu-text);
color: $menuText;
&:hover {
color: var(--pure-theme-menu-title-hover) !important;
color: $menuTitleHover !important;
}
}
@ -520,7 +522,7 @@
.el-sub-menu__title {
height: 48px;
line-height: 48px;
background: var(--pure-theme-menu-bg) !important;
background: $menuBg;
svg {
position: static !important;
@ -529,15 +531,15 @@
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: var(--pure-theme-sub-menu-active-text) !important;
color: $subMenuActiveText !important;
i {
color: var(--pure-theme-sub-menu-active-text) !important;
color: $subMenuActiveText !important;
}
}
.is-active {
color: var(--pure-theme-sub-menu-active-text) !important;
color: $subMenuActiveText !important;
transition: color 0.3s;
}
}
@ -558,7 +560,6 @@
}
.sidebar-container {
z-index: 2001;
width: $sideBarWidth;
transition: transform var(--pure-transition-duration);
}
@ -566,8 +567,8 @@
&.hideSidebar {
.sidebar-container {
pointer-events: none;
transform: translate3d(-$sideBarWidth, 0, 0);
transition-duration: 0.3s;
transform: translate3d(-$sideBarWidth, 0, 0);
}
}
}
@ -583,7 +584,7 @@ body[layout="vertical"] {
}
.sidebar-logo-container {
background: var(--pure-theme-sidebar-logo);
background: $sidebarLogo;
}
.hideSidebar {
@ -610,10 +611,10 @@ body[layout="vertical"] {
.el-sub-menu {
& > .el-sub-menu__title {
& > span {
visibility: visible;
width: 100%;
height: 100%;
text-align: center;
visibility: visible;
}
}
}
@ -653,10 +654,6 @@ 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;
@ -678,7 +675,6 @@ body[layout="mix"] {
.el-menu {
--el-menu-hover-bg-color: transparent !important;
--el-menu-hover-text-color: var(--pure-theme-menu-text) !important;
}
.hideSidebar {
@ -707,10 +703,10 @@ body[layout="mix"] {
padding: 0;
& > span {
visibility: visible;
width: 100%;
height: 100%;
text-align: center;
visibility: visible;
}
}
}

View File

@ -1,46 +1,21 @@
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
@tailwind base;
@tailwind components;
@tailwind utilities;
@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 {
@layer components {
.flex-c {
@apply flex justify-center items-center;
}
@utility flex-ac {
.flex-ac {
@apply flex justify-around items-center;
}
@utility flex-bc {
.flex-bc {
@apply flex justify-between items-center;
}
@utility navbar-bg-hover {
@apply dark:text-white dark:hover:bg-[#242424]!;
.navbar-bg-hover {
@apply dark:text-white dark:hover:!bg-[#242424];
}
}

View File

@ -1,95 +0,0 @@
/* 亮白色 */
html[data-theme="light"] {
--pure-theme-sub-menu-active-text: #000000d9;
--pure-theme-menu-bg: #fff;
--pure-theme-menu-hover: #f6f6f6;
--pure-theme-sub-menu-bg: #fff;
--pure-theme-menu-text: rgb(0 0 0 / 60%);
--pure-theme-sidebar-logo: #fff;
--pure-theme-menu-title-hover: #000;
--pure-theme-menu-active-before: #4091f7;
}
/* 道奇蓝 */
html[data-theme="default"] {
--pure-theme-sub-menu-active-text: #fff;
--pure-theme-menu-bg: #001529;
--pure-theme-menu-hover: rgb(64 145 247 / 15%);
--pure-theme-sub-menu-bg: #0f0303;
--pure-theme-menu-text: rgb(254 254 254 / 65%);
--pure-theme-sidebar-logo: #002140;
--pure-theme-menu-title-hover: #fff;
--pure-theme-menu-active-before: #4091f7;
}
/* 深紫罗兰色 */
html[data-theme="saucePurple"] {
--pure-theme-sub-menu-active-text: #fff;
--pure-theme-menu-bg: #130824;
--pure-theme-menu-hover: rgb(105 58 201 / 15%);
--pure-theme-sub-menu-bg: #000;
--pure-theme-menu-text: #7a80b4;
--pure-theme-sidebar-logo: #1f0c38;
--pure-theme-menu-title-hover: #fff;
--pure-theme-menu-active-before: #693ac9;
}
/* 深粉色 */
html[data-theme="pink"] {
--pure-theme-sub-menu-active-text: #fff;
--pure-theme-menu-bg: #28081a;
--pure-theme-menu-hover: rgb(216 68 147 / 15%);
--pure-theme-sub-menu-bg: #000;
--pure-theme-menu-text: #7a80b4;
--pure-theme-sidebar-logo: #3f0d29;
--pure-theme-menu-title-hover: #fff;
--pure-theme-menu-active-before: #d84493;
}
/* 猩红色 */
html[data-theme="dusk"] {
--pure-theme-sub-menu-active-text: #fff;
--pure-theme-menu-bg: #2a0608;
--pure-theme-menu-hover: rgb(225 60 57 / 15%);
--pure-theme-sub-menu-bg: #000;
--pure-theme-menu-text: rgb(254 254 254 / 65.1%);
--pure-theme-sidebar-logo: #42090c;
--pure-theme-menu-title-hover: #fff;
--pure-theme-menu-active-before: #e13c39;
}
/* 橙红色 */
html[data-theme="volcano"] {
--pure-theme-sub-menu-active-text: #fff;
--pure-theme-menu-bg: #2b0e05;
--pure-theme-menu-hover: rgb(232 95 51 / 15%);
--pure-theme-sub-menu-bg: #0f0603;
--pure-theme-menu-text: rgb(254 254 254 / 65%);
--pure-theme-sidebar-logo: #441708;
--pure-theme-menu-title-hover: #fff;
--pure-theme-menu-active-before: #e85f33;
}
/* 绿宝石 */
html[data-theme="mingQing"] {
--pure-theme-sub-menu-active-text: #fff;
--pure-theme-menu-bg: #032121;
--pure-theme-menu-hover: rgb(89 191 193 / 15%);
--pure-theme-sub-menu-bg: #000;
--pure-theme-menu-text: #7a80b4;
--pure-theme-sidebar-logo: #053434;
--pure-theme-menu-title-hover: #fff;
--pure-theme-menu-active-before: #59bfc1;
}
/* 酸橙绿 */
html[data-theme="auroraGreen"] {
--pure-theme-sub-menu-active-text: #fff;
--pure-theme-menu-bg: #0b1e15;
--pure-theme-menu-hover: rgb(96 172 128 / 15%);
--pure-theme-sub-menu-bg: #000;
--pure-theme-menu-text: #7a80b4;
--pure-theme-sidebar-logo: #112f21;
--pure-theme-menu-title-hover: #fff;
--pure-theme-menu-active-before: #60ac80;
}

View File

@ -1,6 +1,6 @@
import Cookies from "js-cookie";
import { storageLocal } from "@pureadmin/utils";
import { useUserStoreHook } from "@/store/modules/user";
import { storageLocal, isString, isIncludeAllChildren } from "@pureadmin/utils";
export interface DataInfo<T> {
/** token */
@ -17,8 +17,6 @@ export interface DataInfo<T> {
nickname?: string;
/** 当前登录用户的角色 */
roles?: Array<string>;
/** 当前登录用户的按钮级别权限 */
permissions?: Array<string>;
}
export const userKey = "user-info";
@ -43,7 +41,7 @@ export function getToken(): DataInfo<number> {
* @description `token``token`
* `accessToken`访使`token``refreshToken``accessToken``token``refreshToken`30`accessToken`2`expires``accessToken`
* `accessToken``expires``refreshToken`key值为authorized-token的cookie里
* `avatar``username``nickname``roles``permissions``refreshToken``expires`key值为`user-info`localStorage里`multipleTabsKey`
* `avatar``username``nickname``roles``refreshToken``expires`key值为`user-info`localStorage里`multipleTabsKey`
*/
export function setToken(data: DataInfo<Date>) {
let expires = 0;
@ -68,20 +66,18 @@ export function setToken(data: DataInfo<Date>) {
: {}
);
function setUserKey({ avatar, username, nickname, roles, permissions }) {
function setUserKey({ avatar, username, nickname, roles }) {
useUserStoreHook().SET_AVATAR(avatar);
useUserStoreHook().SET_USERNAME(username);
useUserStoreHook().SET_NICKNAME(nickname);
useUserStoreHook().SET_ROLES(roles);
useUserStoreHook().SET_PERMS(permissions);
storageLocal().setItem(userKey, {
refreshToken,
expires,
avatar,
username,
nickname,
roles,
permissions
roles
});
}
@ -91,8 +87,7 @@ export function setToken(data: DataInfo<Date>) {
avatar: data?.avatar ?? "",
username,
nickname: data?.nickname ?? "",
roles,
permissions: data?.permissions ?? []
roles
});
} else {
const avatar =
@ -103,14 +98,11 @@ export function setToken(data: DataInfo<Date>) {
storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? "";
const roles =
storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [];
const permissions =
storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? [];
setUserKey({
avatar,
username,
nickname,
roles,
permissions
roles
});
}
}
@ -126,16 +118,3 @@ export function removeToken() {
export const formatToken = (token: string): string => {
return "Bearer " + token;
};
/** 是否有按钮级别的权限(根据登录接口返回的`permissions`字段进行判断)*/
export const hasPerms = (value: string | Array<string>): boolean => {
if (!value) return false;
const allPerms = "*:*:*";
const { permissions } = useUserStoreHook();
if (!permissions) return false;
if (permissions.length === 1 && permissions[0] === allPerms) return true;
const isAuths = isString(value)
? permissions.includes(value)
: isIncludeAllChildren(value, permissions);
return isAuths ? true : false;
};

View File

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

View File

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

View File

@ -9,7 +9,6 @@ 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
@ -19,26 +18,19 @@ 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();
};
@ -140,10 +132,8 @@ 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);
@ -182,7 +172,7 @@ Print.prototype = {
if (!frameWindow.document.execCommand("print", false, null)) {
frameWindow.print();
}
} catch {
} catch (e) {
frameWindow.print();
}
frameWindow.close();

View File

@ -15,7 +15,7 @@ const router = useRouter();
<div class="ml-12">
<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 +32,7 @@ const router = useRouter();
</p>
<p
v-motion
class="mb-4! text-gray-500"
class="mb-4 text-gray-500"
:initial="{
opacity: 0,
y: 100

View File

@ -15,7 +15,7 @@ const router = useRouter();
<div class="ml-12">
<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 +32,7 @@ const router = useRouter();
</p>
<p
v-motion
class="mb-4! text-gray-500"
class="mb-4 text-gray-500"
:initial="{
opacity: 0,
y: 100

View File

@ -15,7 +15,7 @@ const router = useRouter();
<div class="ml-12">
<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 +32,7 @@ const router = useRouter();
</p>
<p
v-motion
class="mb-4! text-gray-500"
class="mb-4 text-gray-500"
:initial="{
opacity: 0,
y: 100

View File

@ -3,30 +3,26 @@ 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 "~icons/ri/lock-fill";
import User from "~icons/ri/user-3-fill";
import Lock from "@iconify-icons/ri/lock-fill";
import User from "@iconify-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();
@ -43,25 +39,18 @@ const ruleForm = reactive({
const onLogin = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate(valid => {
await formEl.validate((valid, fields) => {
if (valid) {
loading.value = true;
useUserStoreHook()
.loginByUsername({
username: ruleForm.username,
password: ruleForm.password
})
.loginByUsername({ username: ruleForm.username, password: "admin123" })
.then(res => {
if (res.success) {
//
return initRouter().then(() => {
disabled.value = true;
router
.push(getTopMenu(true).path)
.then(() => {
router.push(getTopMenu(true).path).then(() => {
message("登录成功", { type: "success" });
})
.finally(() => (disabled.value = false));
});
});
} else {
message("登录失败", { type: "error" });
@ -72,19 +61,19 @@ const onLogin = async (formEl: FormInstance | undefined) => {
});
};
const immediateDebounce: any = debounce(
formRef => onLogin(formRef),
1000,
true
);
/** 使用公共函数,避免`removeEventListener`失效 */
function onkeypress({ code }: KeyboardEvent) {
if (code === "Enter") {
onLogin(ruleFormRef.value);
}
}
useEventListener(document, "keydown", ({ code }) => {
if (
["Enter", "NumpadEnter"].includes(code) &&
!disabled.value &&
!loading.value
)
immediateDebounce(ruleFormRef.value);
onMounted(() => {
window.document.addEventListener("keypress", onkeypress);
});
onBeforeUnmount(() => {
window.document.removeEventListener("keypress", onkeypress);
});
</script>
@ -109,7 +98,7 @@ useEventListener(document, "keydown", ({ code }) => {
<div class="login-form">
<avatar class="avatar" />
<Motion>
<h2 class="outline-hidden">{{ title }}</h2>
<h2 class="outline-none">{{ title }}</h2>
</Motion>
<el-form
@ -152,11 +141,10 @@ useEventListener(document, "keydown", ({ code }) => {
<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

@ -2,13 +2,13 @@
import { hasAuth, getAuths } from "@/router/utils";
defineOptions({
name: "PermissionButtonRouter"
name: "PermissionButton"
});
</script>
<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

@ -1,109 +0,0 @@
<script setup lang="ts">
import { hasPerms } from "@/utils/auth";
import { useUserStoreHook } from "@/store/modules/user";
const { permissions } = useUserStoreHook();
defineOptions({
name: "PermissionButtonLogin"
});
</script>
<template>
<div>
<p class="mb-2!">当前拥有的code列表{{ permissions }}</p>
<p v-show="permissions?.[0] === '*:*:*'" class="mb-2!">
*:*:* 代表拥有全部按钮级别权限
</p>
<el-card shadow="never" class="mb-2">
<template #header>
<div class="card-header">组件方式判断权限</div>
</template>
<el-space wrap>
<Perms value="permission:btn:add">
<el-button plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
</Perms>
<Perms :value="['permission:btn:edit']">
<el-button plain type="primary">
拥有code['permission:btn:edit'] 权限可见
</el-button>
</Perms>
<Perms
:value="[
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
]"
>
<el-button plain type="danger">
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</Perms>
</el-space>
</el-card>
<el-card shadow="never" class="mb-2">
<template #header>
<div class="card-header">函数方式判断权限</div>
</template>
<el-space wrap>
<el-button v-if="hasPerms('permission:btn:add')" plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
<el-button
v-if="hasPerms(['permission:btn:edit'])"
plain
type="primary"
>
拥有code['permission:btn:edit'] 权限可见
</el-button>
<el-button
v-if="
hasPerms([
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
])
"
plain
type="danger"
>
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</el-space>
</el-card>
<el-card shadow="never">
<template #header>
<div class="card-header">
指令方式判断权限该方式不能动态修改权限
</div>
</template>
<el-space wrap>
<el-button v-perms="'permission:btn:add'" plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
<el-button v-perms="['permission:btn:edit']" plain type="primary">
拥有code['permission:btn:edit'] 权限可见
</el-button>
<el-button
v-perms="[
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
]"
plain
type="danger"
>
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</el-space>
</el-card>
</div>
</template>

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"

19
tailwind.config.ts Normal file
View File

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

View File

@ -5,7 +5,7 @@ declare module "vue" {
export interface ComponentCustomProperties {
/** `Loading` 动画加载指令具体看https://element-plus.org/zh-CN/component/loading.html#%E6%8C%87%E4%BB%A4 */
vLoading: Directive<Element, boolean>;
/** 按钮权限指令(根据路由`meta`中的`auths`字段进行判断)*/
/** 按钮权限指令 */
vAuth: Directive<HTMLElement, string | Array<string>>;
/** 文本复制指令(默认双击复制) */
vCopy: Directive<CopyEl, string>;
@ -13,8 +13,6 @@ declare module "vue" {
vLongpress: Directive<HTMLElement, Function>;
/** 防抖、节流指令 */
vOptimize: Directive<HTMLElement, OptimizeOptions>;
/** 按钮权限指令(根据登录接口返回的`permissions`字段进行判断)*/
vPerms: Directive<HTMLElement, string | Array<string>>;
/**
* `v-ripple`
* 1. `v-ripple``ripple`

View File

@ -7,7 +7,6 @@ declare module "vue" {
IconifyIconOnline: (typeof import("../src/components/ReIcon"))["IconifyIconOnline"];
FontIcon: (typeof import("../src/components/ReIcon"))["FontIcon"];
Auth: (typeof import("../src/components/ReAuth"))["Auth"];
Perms: (typeof import("../src/components/RePerms"))["Perms"];
}
}
@ -116,11 +115,9 @@ declare module "vue" {
ElWatermark: (typeof import("element-plus"))["ElWatermark"];
ElTour: (typeof import("element-plus"))["ElTour"];
ElTourStep: (typeof import("element-plus"))["ElTourStep"];
ElSegmented: (typeof import("element-plus"))["ElSegmented"];
}
interface ComponentCustomProperties {
$storage: ResponsiveStorage;
$message: (typeof import("element-plus"))["ElMessage"];
$notify: (typeof import("element-plus"))["ElNotification"];
$msgbox: (typeof import("element-plus"))["ElMessageBox"];

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