Compare commits

..

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

149 changed files with 6563 additions and 7994 deletions

11
.eslintignore Normal file
View File

@ -0,0 +1,11 @@
public
dist
*.d.ts
/src/assets
package.json
eslint.config.js
.prettierrc.js
commitlint.config.js
postcss.config.js
tailwind.config.ts
stylelint.config.js

120
.eslintrc.js Normal file
View File

@ -0,0 +1,120 @@
module.exports = {
root: true,
env: {
node: true
},
globals: {
// Ref sugar (take 2)
$: "readonly",
$$: "readonly",
$ref: "readonly",
$shallowRef: "readonly",
$computed: "readonly",
// index.d.ts
// global.d.ts
Fn: "readonly",
PromiseFn: "readonly",
RefType: "readonly",
LabelValueOptions: "readonly",
EmitType: "readonly",
TargetContext: "readonly",
ComponentElRef: "readonly",
ComponentRef: "readonly",
ElRef: "readonly",
global: "readonly",
ForDataType: "readonly",
ComponentRoutes: "readonly",
// script setup
defineProps: "readonly",
defineEmits: "readonly",
defineExpose: "readonly",
withDefaults: "readonly"
},
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/eslint-config-typescript"
],
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
ecmaVersion: 2020,
sourceType: "module",
jsxPragma: "React",
ecmaFeatures: {
jsx: true
}
},
overrides: [
{
files: ["*.ts", "*.vue"],
rules: {
"no-undef": "off"
}
},
{
files: ["*.vue"],
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
extraFileExtensions: [".vue"],
ecmaVersion: "latest",
ecmaFeatures: {
jsx: true
}
},
rules: {
"no-undef": "off"
}
}
],
rules: {
"vue/no-v-html": "off",
"vue/require-default-prop": "off",
"vue/require-explicit-emits": "off",
"vue/multi-word-component-names": "off",
"@typescript-eslint/no-explicit-any": "off", // any
"no-debugger": "off",
"@typescript-eslint/explicit-module-boundary-types": "off", // setup()
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"vue/html-self-closing": [
"error",
{
html: {
void: "always",
normal: "always",
component: "always"
},
svg: "always",
math: "always"
}
],
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}
],
"no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}
],
"prettier/prettier": [
"error",
{
endOfLine: "auto"
}
]
}
};

View File

@ -3,7 +3,7 @@
"prettier --cache --ignore-unknown --write", "prettier --cache --ignore-unknown --write",
"eslint --cache --fix" "eslint --cache --fix"
], ],
"{!(package)*.json,*.code-snippets,.!({browserslist,npm,nvm})*rc}": [ "{!(package)*.json,*.code-snippets,.!({browserslist,nvm})*rc}": [
"prettier --cache --write--parser json" "prettier --cache --write--parser json"
], ],
"package.json": ["prettier --cache --write"], "package.json": ["prettier --cache --write"],

5
.npmrc
View File

@ -1,4 +1,3 @@
shell-emulator=true
shamefully-hoist=true shamefully-hoist=true
enable-pre-post-scripts=false strict-peer-dependencies=false
strict-peer-dependencies=false shell-emulator=true

2
.nvmrc
View File

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

View File

@ -1,7 +1,6 @@
{ {
"recommendations": [ "recommendations": [
"christian-kohler.path-intellisense", "christian-kohler.path-intellisense",
"warmthsea.vscode-custom-code-color",
"vscode-icons-team.vscode-icons", "vscode-icons-team.vscode-icons",
"davidanson.vscode-markdownlint", "davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker", "ms-azuretools.vscode-docker",
@ -16,4 +15,4 @@
"antfu.iconify", "antfu.iconify",
"Vue.volar" "Vue.volar"
] ]
} }

16
.vscode/settings.json vendored
View File

@ -27,17 +27,5 @@
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit" "source.fixAll.eslint": "explicit"
}, },
"iconify.excludes": [ "iconify.excludes": ["el"]
"el" }
],
"vscodeCustomCodeColor.highlightValue": [
"v-loading",
"v-auth",
"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 WORKDIR /app
RUN corepack enable 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 RUN npm config set registry https://registry.npmmirror.com

View File

@ -8,27 +8,20 @@
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` 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 ## Supporting video
[Click me to view UI design](https://www.bilibili.com/video/BV17g411T7rq) [Click me to view UI design](https://www.bilibili.com/video/BV17g411T7rq)
[Click me to view the rapid development tutorial](https://www.bilibili.com/video/BV1kg411v7QT) [Click me to view the rapid development tutorial](https://www.bilibili.com/video/BV1kg411v7QT)
[Click me to view all pages and function demonstrations of vue-pure-admin](https://www.bilibili.com/video/BV1Rx4y1U7Mv)
## Nanny-level documents ## Nanny-level documents
[Click me to view vue-pure-admin documentation](https://pure-admin.cn/) [Click me to view vue-pure-admin documentation](https://yiming_chang.gitee.io/pure-admin-doc)
[Click me to view @pureadmin/utils documentation](https://pure-admin-utils.netlify.app) [Click me to view @pureadmin/utils documentation](https://pure-admin-utils.netlify.app)
## Quality service, software outsourcing, sponsorship support ## Quality service, software outsourcing, sponsorship support
[Click me to view details](https://pure-admin.cn/pages/service/) [Click me to view details](https://yiming_chang.gitee.io/pure-admin-doc/pages/service/)
## Preview ## Preview

View File

@ -12,27 +12,20 @@
当前是非国际化版本,如果您需要国际化版本 [请点击](https://github.com/pure-admin/pure-admin-thin/tree/i18n) 当前是非国际化版本,如果您需要国际化版本 [请点击](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) [点我查看 UI 设计](https://www.bilibili.com/video/BV17g411T7rq)
[点我查看快速开发教程](https://www.bilibili.com/video/BV1kg411v7QT) [点我查看快速开发教程](https://www.bilibili.com/video/BV1kg411v7QT)
[点我查看 vue-pure-admin 的所有页面、功能演示](https://www.bilibili.com/video/BV1Rx4y1U7Mv)
## 配套保姆级文档 ## 配套保姆级文档
[点我查看 vue-pure-admin 文档](https://pure-admin.cn/) [点我查看 vue-pure-admin 文档](https://yiming_chang.gitee.io/pure-admin-doc)
[点我查看 @pureadmin/utils 文档](https://pure-admin-utils.netlify.app) [点我查看 @pureadmin/utils 文档](https://pure-admin-utils.netlify.app)
## 优质服务、软件外包、赞助支持 ## 优质服务、软件外包、赞助支持
[点我查看详情](https://pure-admin.cn/pages/service/) [点我查看详情](https://yiming_chang.gitee.io/pure-admin-doc/pages/service/)
## 预览 ## 预览

View File

@ -3,6 +3,7 @@ import { Plugin as importToCDN } from "vite-plugin-cdn-import";
/** /**
* @description `cdn`使cdn模式 .env.production VITE_CDN true * @description `cdn`使cdn模式 .env.production VITE_CDN true
* cdnhttps://www.bootcdn.cn当然你也可以选择 https://unpkg.com 或者 https://www.jsdelivr.com * cdnhttps://www.bootcdn.cn当然你也可以选择 https://unpkg.com 或者 https://www.jsdelivr.com
* mockjs不能用cdn模式引入mockjs使
* 使jscss文件cdn * 使jscss文件cdn
*/ */
export const cdn = importToCDN({ export const cdn = importToCDN({

View File

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

View File

@ -2,15 +2,14 @@ import { cdn } from "./cdn";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import { viteBuildInfo } from "./info"; import { viteBuildInfo } from "./info";
import svgLoader from "vite-svg-loader"; import svgLoader from "vite-svg-loader";
import Icons from "unplugin-icons/vite";
import type { PluginOption } from "vite"; import type { PluginOption } from "vite";
import vueJsx from "@vitejs/plugin-vue-jsx"; import vueJsx from "@vitejs/plugin-vue-jsx";
import tailwindcss from "@tailwindcss/vite";
import { configCompressPlugin } from "./compress"; import { configCompressPlugin } from "./compress";
import removeNoMatch from "vite-plugin-router-warn"; import removeNoMatch from "vite-plugin-router-warn";
import { visualizer } from "rollup-plugin-visualizer"; import { visualizer } from "rollup-plugin-visualizer";
import removeConsole from "vite-plugin-remove-console"; import removeConsole from "vite-plugin-remove-console";
import { codeInspectorPlugin } from "code-inspector-plugin"; import { themePreprocessorPlugin } from "@pureadmin/theme";
import { genScssMultipleScopeVars } from "../src/layout/theme";
import { vitePluginFakeServer } from "vite-plugin-fake-server"; import { vitePluginFakeServer } from "vite-plugin-fake-server";
export function getPluginsList( export function getPluginsList(
@ -19,20 +18,9 @@ export function getPluginsList(
): PluginOption[] { ): PluginOption[] {
const lifecycle = process.env.npm_lifecycle_event; const lifecycle = process.env.npm_lifecycle_event;
return [ return [
tailwindcss(),
vue(), vue(),
// jsx、tsx语法支持 // jsx、tsx语法支持
vueJsx(), vueJsx(),
/**
* DOM IDE
* Mac Option + Shift
* Windows Alt + Shift
* https://inspector.fe-dev.cn/guide/start.html
*/
codeInspectorPlugin({
bundler: "vite",
hideConsole: true
}),
viteBuildInfo(), viteBuildInfo(),
/** /**
* vue-router动态路由警告No match found for location with path * vue-router动态路由警告No match found for location with path
@ -47,13 +35,15 @@ export function getPluginsList(
infixName: false, infixName: false,
enableProd: true enableProd: true
}), }),
// 自定义主题
themePreprocessorPlugin({
scss: {
multipleScopeVars: genScssMultipleScopeVars(),
extract: true
}
}),
// svg组件化支持 // svg组件化支持
svgLoader(), svgLoader(),
// 自动按需加载图标
Icons({
compiler: "vue3",
scale: 1
}),
VITE_CDN ? cdn : null, VITE_CDN ? cdn : null,
configCompressPlugin(VITE_COMPRESSION), configCompressPlugin(VITE_COMPRESSION),
// 线上环境删除console // 线上环境删除console

View File

@ -48,7 +48,7 @@ const __APP_INFO__ = {
}; };
/** 处理环境变量 */ /** 处理环境变量 */
const wrapperEnv = (envConf: Recordable): ViteEnv => { const warpperEnv = (envConf: Recordable): ViteEnv => {
// 默认值 // 默认值
const ret: ViteEnv = { const ret: ViteEnv = {
VITE_PORT: 8848, VITE_PORT: 8848,
@ -107,4 +107,4 @@ const getPackageSize = options => {
}); });
}; };
export { root, pathResolve, alias, __APP_INFO__, wrapperEnv, getPackageSize }; export { root, pathResolve, alias, __APP_INFO__, warpperEnv, getPackageSize };

View File

@ -1,25 +1,19 @@
import js from "@eslint/js"; import js from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginVue from "eslint-plugin-vue"; import pluginVue from "eslint-plugin-vue";
import * as parserVue from "vue-eslint-parser"; import * as parserVue from "vue-eslint-parser";
import configPrettier from "eslint-config-prettier"; import configPrettier from "eslint-config-prettier";
import pluginPrettier from "eslint-plugin-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([ export default defineFlatConfig([
globalIgnores([
"**/.*",
"dist/*",
"*.d.ts",
"public/*",
"src/assets/**",
"src/**/iconfont/**"
]),
{ {
...js.configs.recommended, ...js.configs.recommended,
ignores: ["src/assets/**", "src/**/iconfont/**"],
languageOptions: { languageOptions: {
globals: { globals: {
// types/index.d.ts // index.d.ts
RefType: "readonly", RefType: "readonly",
EmitType: "readonly", EmitType: "readonly",
TargetContext: "readonly", TargetContext: "readonly",
@ -72,18 +66,26 @@ export default defineConfig([
] ]
} }
}, },
...tseslint.config({ {
extends: [...tseslint.configs.recommended],
files: ["**/*.?([cm])ts", "**/*.?([cm])tsx"], files: ["**/*.?([cm])ts", "**/*.?([cm])tsx"],
languageOptions: {
parser: parserTypeScript,
parserOptions: {
sourceType: "module"
}
},
plugins: {
"@typescript-eslint": pluginTypeScript
},
rules: { rules: {
...pluginTypeScript.configs.strict.rules,
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-redeclare": "error", "@typescript-eslint/no-redeclare": "error",
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/prefer-as-const": "warn", "@typescript-eslint/prefer-as-const": "warn",
"@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-unsafe-function-type": "off",
"@typescript-eslint/no-import-type-side-effects": "error", "@typescript-eslint/no-import-type-side-effects": "error",
"@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/consistent-type-imports": [ "@typescript-eslint/consistent-type-imports": [
@ -102,20 +104,20 @@ export default defineConfig([
} }
] ]
} }
}), },
{ {
files: ["**/*.d.ts"], files: ["**/*.d.ts"],
rules: { rules: {
"eslint-comments/no-unlimited-disable": "off", "eslint-comments/no-unlimited-disable": "off",
"import/no-duplicates": "off", "import/no-duplicates": "off",
"no-restricted-syntax": "off",
"unused-imports/no-unused-vars": "off" "unused-imports/no-unused-vars": "off"
} }
}, },
{ {
files: ["**/*.?([cm])js"], files: ["**/*.?([cm])js"],
rules: { rules: {
"@typescript-eslint/no-require-imports": "off" "@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-var-requires": "off"
} }
}, },
{ {
@ -136,19 +138,18 @@ export default defineConfig([
jsx: true jsx: true
}, },
extraFileExtensions: [".vue"], extraFileExtensions: [".vue"],
parser: tseslint.parser, parser: "@typescript-eslint/parser",
sourceType: "module" sourceType: "module"
} }
}, },
plugins: { plugins: {
"@typescript-eslint": tseslint.plugin,
vue: pluginVue vue: pluginVue
}, },
processor: pluginVue.processors[".vue"], processor: pluginVue.processors[".vue"],
rules: { rules: {
...pluginVue.configs.base.rules, ...pluginVue.configs.base.rules,
...pluginVue.configs.essential.rules, ...pluginVue.configs["vue3-essential"].rules,
...pluginVue.configs.recommended.rules, ...pluginVue.configs["vue3-recommended"].rules,
"no-undef": "off", "no-undef": "off",
"no-unused-vars": "off", "no-unused-vars": "off",
"vue/no-v-html": "off", "vue/no-v-html": "off",

View File

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

View File

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

View File

@ -10,13 +10,9 @@ export default defineFakeRoute([
return { return {
success: true, success: true,
data: { data: {
avatar: "https://avatars.githubusercontent.com/u/44761321",
username: "admin", username: "admin",
nickname: "小铭",
// 一个用户可能有多个角色 // 一个用户可能有多个角色
roles: ["admin"], roles: ["admin"],
// 按钮级别权限
permissions: ["*:*:*"],
accessToken: "eyJhbGciOiJIUzUxMiJ9.admin", accessToken: "eyJhbGciOiJIUzUxMiJ9.admin",
refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh", refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh",
expires: "2030/10/30 00:00:00" expires: "2030/10/30 00:00:00"
@ -26,11 +22,9 @@ export default defineFakeRoute([
return { return {
success: true, success: true,
data: { data: {
avatar: "https://avatars.githubusercontent.com/u/52823142",
username: "common", username: "common",
nickname: "小林", // 一个用户可能有多个角色
roles: ["common"], roles: ["common"],
permissions: ["permission:btn:add", "permission:btn:edit"],
accessToken: "eyJhbGciOiJIUzUxMiJ9.common", accessToken: "eyJhbGciOiJIUzUxMiJ9.common",
refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh", refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh",
expires: "2030/10/30 00:00:00" expires: "2030/10/30 00:00:00"

View File

@ -1,6 +1,6 @@
{ {
"name": "pure-admin-thin", "name": "pure-admin-thin",
"version": "6.0.0", "version": "5.2.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@ -13,6 +13,7 @@
"preview:build": "pnpm build && vite preview", "preview:build": "pnpm build && vite preview",
"typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck", "typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
"svgo": "svgo -f . -r", "svgo": "svgo -f . -r",
"cloc": "NODE_OPTIONS=--max-old-space-size=4096 cloc . --exclude-dir=node_modules --exclude-lang=YAML",
"clean:cache": "rimraf .eslintcache && rimraf pnpm-lock.yaml && rimraf node_modules && pnpm store prune && pnpm install", "clean:cache": "rimraf .eslintcache && rimraf pnpm-lock.yaml && rimraf node_modules && pnpm store prune && pnpm install",
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix", "lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"", "lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
@ -22,7 +23,6 @@
"preinstall": "npx only-allow pnpm" "preinstall": "npx only-allow pnpm"
}, },
"keywords": [ "keywords": [
"pure-admin-thin",
"vue-pure-admin", "vue-pure-admin",
"element-plus", "element-plus",
"tailwindcss", "tailwindcss",
@ -39,7 +39,7 @@
"url": "git+https://github.com/pure-admin/pure-admin-thin.git" "url": "git+https://github.com/pure-admin/pure-admin-thin.git"
}, },
"bugs": { "bugs": {
"url": "https://github.com/pure-admin/vue-pure-admin/issues" "url": "https://github.com/pure-admin/pure-admin-thin/issues"
}, },
"license": "MIT", "license": "MIT",
"author": { "author": {
@ -49,111 +49,100 @@
}, },
"dependencies": { "dependencies": {
"@pureadmin/descriptions": "^1.2.1", "@pureadmin/descriptions": "^1.2.1",
"@pureadmin/table": "^3.2.1", "@pureadmin/table": "^3.1.2",
"@pureadmin/utils": "^2.6.0", "@pureadmin/utils": "^2.4.7",
"@vueuse/core": "^13.1.0", "@vueuse/core": "^10.9.0",
"@vueuse/motion": "^3.0.3", "@vueuse/motion": "^2.1.0",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"axios": "^1.9.0", "axios": "^1.6.8",
"dayjs": "^1.11.13", "dayjs": "^1.11.10",
"echarts": "^5.6.0", "echarts": "^5.5.0",
"element-plus": "^2.9.8", "element-plus": "^2.6.2",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"path-browserify": "^1.0.1", "path": "^0.12.7",
"pinia": "^3.0.2", "pinia": "^2.1.7",
"pinyin-pro": "^3.26.0", "pinyin-pro": "^3.19.6",
"qs": "^6.14.0", "qs": "^6.12.0",
"responsive-storage": "^2.2.0", "responsive-storage": "^2.2.0",
"sortablejs": "^1.15.6", "sortablejs": "^1.15.2",
"vue": "^3.5.13", "vue": "^3.4.21",
"vue-router": "^4.5.0", "vue-router": "^4.3.0",
"vue-tippy": "^6.7.0", "vue-tippy": "^6.4.1",
"vue-types": "^6.0.0" "vue-types": "^5.1.1"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^19.8.0", "@commitlint/cli": "^19.2.1",
"@commitlint/config-conventional": "^19.8.0", "@commitlint/config-conventional": "^19.1.0",
"@commitlint/types": "^19.8.0", "@commitlint/types": "^19.0.3",
"@eslint/js": "^9.25.1", "@eslint/js": "^8.57.0",
"@faker-js/faker": "^9.7.0", "@faker-js/faker": "^8.4.1",
"@iconify/json": "^2.2.331", "@iconify-icons/ep": "^1.2.12",
"@iconify/vue": "4.2.0", "@iconify-icons/ri": "^1.2.10",
"@tailwindcss/vite": "^4.1.4", "@iconify/vue": "^4.1.1",
"@pureadmin/theme": "^3.2.0",
"@types/gradient-string": "^1.1.5",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/node": "^20.17.30", "@types/node": "^20.11.30",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/path-browserify": "^1.0.3", "@types/qs": "^6.9.14",
"@types/qs": "^6.9.18",
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@vitejs/plugin-vue": "^5.2.3", "@typescript-eslint/eslint-plugin": "^7.3.1",
"@vitejs/plugin-vue-jsx": "^4.1.2", "@typescript-eslint/parser": "^7.3.1",
"boxen": "^8.0.1", "@vitejs/plugin-vue": "^5.0.4",
"code-inspector-plugin": "^0.20.10", "@vitejs/plugin-vue-jsx": "^3.1.0",
"cssnano": "^7.0.6", "autoprefixer": "^10.4.19",
"eslint": "^9.25.1", "boxen": "^7.1.1",
"eslint-config-prettier": "^10.1.2", "cloc": "^2.11.0",
"eslint-plugin-prettier": "^5.2.6", "cssnano": "^6.1.1",
"eslint-plugin-vue": "^10.0.0", "eslint": "^8.57.0",
"gradient-string": "^3.0.0", "eslint-config-prettier": "^9.1.0",
"husky": "^9.1.7", "eslint-define-config": "^2.1.0",
"lint-staged": "^15.5.1", "eslint-plugin-prettier": "^5.1.3",
"postcss": "^8.5.3", "eslint-plugin-vue": "^9.23.0",
"postcss-html": "^1.8.0", "gradient-string": "^2.0.2",
"postcss-load-config": "^6.0.1", "husky": "^9.0.11",
"lint-staged": "^15.2.2",
"postcss": "^8.4.38",
"postcss-html": "^1.6.0",
"postcss-import": "^16.1.0",
"postcss-scss": "^4.0.9", "postcss-scss": "^4.0.9",
"prettier": "^3.5.3", "prettier": "^3.2.5",
"rimraf": "^6.0.1", "rimraf": "^5.0.5",
"rollup-plugin-visualizer": "^5.14.0", "rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.87.0", "sass": "^1.72.0",
"stylelint": "^16.19.0", "stylelint": "^16.2.1",
"stylelint-config-recess-order": "^6.0.0", "stylelint-config-recess-order": "^5.0.0",
"stylelint-config-recommended-vue": "^1.6.0", "stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-standard-scss": "^14.0.0", "stylelint-config-standard-scss": "^13.0.0",
"stylelint-prettier": "^5.0.3", "stylelint-prettier": "^5.0.0",
"svgo": "^3.3.2", "svgo": "^3.2.0",
"tailwindcss": "^4.1.4", "tailwindcss": "^3.4.1",
"typescript": "^5.8.3", "typescript": "^5.4.3",
"typescript-eslint": "^8.31.0", "vite": "^5.2.3",
"unplugin-icons": "^22.1.0", "vite-plugin-cdn-import": "^0.3.5",
"vite": "^6.3.3",
"vite-plugin-cdn-import": "^1.0.1",
"vite-plugin-compression": "^0.5.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-remove-console": "^2.2.0",
"vite-plugin-router-warn": "^1.0.0", "vite-plugin-router-warn": "^1.0.0",
"vite-svg-loader": "^5.1.0", "vite-svg-loader": "^5.1.0",
"vue-eslint-parser": "^10.1.3", "vue-eslint-parser": "^9.4.2",
"vue-tsc": "^2.2.10" "vue-tsc": "^1.8.27"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=22.0.0", "node": "^18.18.0 || ^20.9.0 || >=21.1.0",
"pnpm": ">=9" "pnpm": ">=8.6.10"
}, },
"packageManager": "pnpm@8.6.10",
"pnpm": { "pnpm": {
"allowedDeprecatedVersions": { "allowedDeprecatedVersions": {
"are-we-there-yet": "*",
"sourcemap-codec": "*", "sourcemap-codec": "*",
"lodash.isequal": "*",
"domexception": "*", "domexception": "*",
"w3c-hr-time": "*", "w3c-hr-time": "*",
"inflight": "*",
"npmlog": "*",
"rimraf": "*",
"stable": "*", "stable": "*",
"gauge": "*", "abab": "*"
"abab": "*", }
"glob": "*"
},
"onlyBuiltDependencies": [
"@parcel/watcher",
"core-js",
"es5-ext",
"esbuild",
"typeit",
"vue-demi"
]
} }
} }

11034
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} */ /** @type {import('postcss-load-config').Config} */
export default { export default {
plugins: { plugins: {
"postcss-import": {},
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}) ...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {})
} }
}; };

View File

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

View File

@ -8,9 +8,8 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { ElConfigProvider } from "element-plus"; import { ElConfigProvider } from "element-plus";
import zhCn from "element-plus/dist/locale/zh-cn.mjs";
import { ReDialog } from "@/components/ReDialog"; import { ReDialog } from "@/components/ReDialog";
import zhCn from "element-plus/es/locale/lang/zh-cn";
export default defineComponent({ export default defineComponent({
name: "app", name: "app",
components: { components: {

View File

@ -3,16 +3,10 @@ import { http } from "@/utils/http";
export type UserResult = { export type UserResult = {
success: boolean; success: boolean;
data: { data: {
/** 头像 */
avatar: string;
/** 用户名 */ /** 用户名 */
username: string; username: string;
/** 昵称 */ /** 当前登陆用户的角色 */
nickname: string;
/** 当前登录用户的角色 */
roles: Array<string>; roles: Array<string>;
/** 按钮级别权限 */
permissions: Array<string>;
/** `token` */ /** `token` */
accessToken: string; accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */ /** 用于调用刷新`accessToken`的接口时所需的`token` */
@ -39,7 +33,7 @@ export const getLogin = (data?: object) => {
return http.request<UserResult>("post", "/login", { data }); return http.request<UserResult>("post", "/login", { data });
}; };
/** 刷新`token` */ /** 刷新token */
export const refreshTokenApi = (data?: object) => { export const refreshTokenApi = (data?: object) => {
return http.request<RefreshTokenResult>("post", "/refresh-token", { data }); return http.request<RefreshTokenResult>("post", "/refresh-token", { data });
}; };

View File

@ -28,7 +28,8 @@
c = document.createElement("div"); c = document.createElement("div");
(c.innerHTML = e._iconfont_svg_string_2208059), (c.innerHTML = e._iconfont_svg_string_2208059),
(c = c.getElementsByTagName("svg")[0]) && (c = c.getElementsByTagName("svg")[0]) &&
((c.style.position = "absolute"), (c.setAttribute("aria-hidden", "true"),
(c.style.position = "absolute"),
(c.style.width = 0), (c.style.width = 0),
(c.style.height = 0), (c.style.height = 0),
(c.style.overflow = "hidden"), (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 +0,0 @@
<svg width="32" height="32" fill="currentColor" data-icon="holder" viewBox="64 64 896 896"><path d="M300 276.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97m0 284a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 228a56 56 0 1 0 112 0 56 56 0 0 0-112 0m0 284a56 56 0 1 0 112 0 56 56 0 0 0-112 0M300 844.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 796a56 56 0 1 0 112 0 56 56 0 0 0-112 0"/></svg>

Before

Width:  |  Height:  |  Size: 373 B

View File

@ -29,11 +29,9 @@ const addDialog = (options: DialogOptions) => {
const closeDialog = (options: DialogOptions, index: number, args?: any) => { const closeDialog = (options: DialogOptions, index: number, args?: any) => {
dialogStore.value[index].visible = false; dialogStore.value[index].visible = false;
options.closeCallBack && options.closeCallBack({ options, index, args }); options.closeCallBack && options.closeCallBack({ options, index, args });
const closeDelay = options?.closeDelay ?? 200;
useTimeoutFn(() => { useTimeoutFn(() => {
dialogStore.value.splice(index, 1); dialogStore.value.splice(index, 1);
}, closeDelay); }, 200);
}; };
/** /**
@ -53,8 +51,8 @@ const closeAllDialog = () => {
/** 使`addDialog` /** 使`addDialog`
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L4 * https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L4
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L12 * https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L13
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L22 * https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L20
*/ */
const ReDialog = withInstall(reDialog); const ReDialog = withInstall(reDialog);

View File

@ -1,21 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
closeDialog,
dialogStore,
type EventType, type EventType,
type ButtonProps, type ButtonProps,
type DialogOptions, type DialogOptions
closeDialog,
dialogStore
} from "./index"; } from "./index";
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import { isFunction } from "@pureadmin/utils"; import { isFunction } from "@pureadmin/utils";
import Fullscreen from "~icons/ri/fullscreen-fill"; import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import ExitFullscreen from "~icons/ri/fullscreen-exit-fill"; import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
defineOptions({
name: "ReDialog"
});
const sureBtnMap = ref({});
const fullscreen = ref(false); const fullscreen = ref(false);
const footerButtons = computed(() => { const footerButtons = computed(() => {
@ -42,28 +37,11 @@ const footerButtons = computed(() => {
type: "primary", type: "primary",
text: true, text: true,
bg: true, bg: true,
popconfirm: options?.popconfirm,
btnClick: ({ dialog: { options, index } }) => { btnClick: ({ dialog: { options, index } }) => {
if (options?.sureBtnLoading) { const done = () =>
sureBtnMap.value[index] = Object.assign(
{},
sureBtnMap.value[index],
{
loading: true
}
);
}
const closeLoading = () => {
if (options?.sureBtnLoading) {
sureBtnMap.value[index].loading = false;
}
};
const done = () => {
closeLoading();
closeDialog(options, index, { command: "sure" }); closeDialog(options, index, { command: "sure" });
};
if (options?.beforeSure && isFunction(options?.beforeSure)) { if (options?.beforeSure && isFunction(options?.beforeSure)) {
options.beforeSure(done, { options, index, closeLoading }); options.beforeSure(done, { options, index });
} else { } else {
done(); done();
} }
@ -79,7 +57,7 @@ const fullscreenClass = computed(() => {
"el-dialog__close", "el-dialog__close",
"-translate-x-2", "-translate-x-2",
"cursor-pointer", "cursor-pointer",
"hover:text-[red]!" "hover:!text-[red]"
]; ];
}); });
@ -171,35 +149,19 @@ function handleClose(
<component :is="options?.footerRenderer({ options, index })" /> <component :is="options?.footerRenderer({ options, index })" />
</template> </template>
<span v-else> <span v-else>
<template v-for="(btn, key) in footerButtons(options)" :key="key"> <el-button
<el-popconfirm v-for="(btn, key) in footerButtons(options)"
v-if="btn.popconfirm" :key="key"
v-bind="btn.popconfirm" v-bind="btn"
@confirm=" @click="
btn.btnClick({ btn.btnClick({
dialog: { options, index }, dialog: { options, index },
button: { btn, index: key } button: { btn, index: key }
}) })
" "
> >
<template #reference> {{ btn?.label }}
<el-button v-bind="btn">{{ btn?.label }}</el-button> </el-button>
</template>
</el-popconfirm>
<el-button
v-else
v-bind="btn"
:loading="key === 1 && sureBtnMap[index]?.loading"
@click="
btn.btnClick({
dialog: { options, index },
button: { btn, index: key }
})
"
>
{{ btn?.label }}
</el-button>
</template>
</span> </span>
</template> </template>
</el-dialog> </el-dialog>

View File

@ -11,13 +11,6 @@ type ArgsType = {
/** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */ /** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */
command: "cancel" | "sure" | "close"; command: "cancel" | "sure" | "close";
}; };
type ButtonType =
| "primary"
| "success"
| "warning"
| "danger"
| "info"
| "text";
/** https://element-plus.org/zh-CN/component/dialog.html#attributes */ /** https://element-plus.org/zh-CN/component/dialog.html#attributes */
type DialogProps = { type DialogProps = {
@ -65,34 +58,6 @@ type DialogProps = {
destroyOnClose?: boolean; destroyOnClose?: boolean;
}; };
//element-plus.org/zh-CN/component/popconfirm.html#attributes
type Popconfirm = {
/** 标题 */
title?: string;
/** 确定按钮文字 */
confirmButtonText?: string;
/** 取消按钮文字 */
cancelButtonText?: string;
/** 确定按钮类型,默认 `primary` */
confirmButtonType?: ButtonType;
/** 取消按钮类型,默认 `text` */
cancelButtonType?: ButtonType;
/** 自定义图标,默认 `QuestionFilled` */
icon?: string | Component;
/** `Icon` 颜色,默认 `#f90` */
iconColor?: string;
/** 是否隐藏 `Icon`,默认 `false` */
hideIcon?: boolean;
/** 关闭时的延迟,默认 `200` */
hideAfter?: number;
/** 是否将 `popover` 的下拉列表插入至 `body` 元素,默认 `true` */
teleported?: boolean;
/** 当 `popover` 组件长时间不触发且 `persistent` 属性设置为 `false` 时, `popover` 将会被删除,默认 `false` */
persistent?: boolean;
/** 弹层宽度,最小宽度 `150px`,默认 `150` */
width?: string | number;
};
type BtnClickDialog = { type BtnClickDialog = {
options?: DialogOptions; options?: DialogOptions;
index?: number; index?: number;
@ -121,8 +86,6 @@ type ButtonProps = {
round?: boolean; round?: boolean;
/** 是否为圆形按钮,默认 `false` */ /** 是否为圆形按钮,默认 `false` */
circle?: boolean; circle?: boolean;
/** 确定按钮的 `Popconfirm` 气泡确认框相关配置 */
popconfirm?: Popconfirm;
/** 是否为加载中状态,默认 `false` */ /** 是否为加载中状态,默认 `false` */
loading?: boolean; loading?: boolean;
/** 自定义加载中状态图标组件 */ /** 自定义加载中状态图标组件 */
@ -160,10 +123,6 @@ interface DialogOptions extends DialogProps {
props?: any; props?: any;
/** 是否隐藏 `Dialog` 按钮操作区的内容 */ /** 是否隐藏 `Dialog` 按钮操作区的内容 */
hideFooter?: boolean; hideFooter?: boolean;
/** 确定按钮的 `Popconfirm` 气泡确认框相关配置 */
popconfirm?: Popconfirm;
/** 点击确定按钮后是否开启 `loading` 加载动画 */
sureBtnLoading?: boolean;
/** /**
* @description * @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} * @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 +220,10 @@ interface DialogOptions extends DialogProps {
done: Function, done: Function,
{ {
options, options,
index, index
closeLoading
}: { }: {
options: DialogOptions; options: DialogOptions;
index: number; index: number;
/** 关闭确定按钮的 `loading` 加载动画 */
closeLoading: Function;
} }
) => void; ) => void;
} }

View File

@ -6,7 +6,7 @@ import fontIcon from "./src/iconfont";
const IconifyIconOffline = iconifyIconOffline; const IconifyIconOffline = iconifyIconOffline;
/** 在线图标组件 */ /** 在线图标组件 */
const IconifyIconOnline = iconifyIconOnline; const IconifyIconOnline = iconifyIconOnline;
/** `iconfont`组件 */ /** iconfont组件 */
const FontIcon = fontIcon; const FontIcon = fontIcon;
export { IconifyIconOffline, IconifyIconOnline, FontIcon }; export { IconifyIconOffline, IconifyIconOnline, FontIcon };

View File

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

View File

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

View File

@ -13,35 +13,18 @@ export default defineComponent({
render() { render() {
if (typeof this.icon === "object") addIcon(this.icon, this.icon); if (typeof this.icon === "object") addIcon(this.icon, this.icon);
const attrs = this.$attrs; const attrs = this.$attrs;
if (typeof this.icon === "string") { return h(
return h( IconifyIcon,
IconifyIcon, {
{ icon: this.icon,
icon: this.icon, style: attrs?.style
"aria-hidden": false, ? Object.assign(attrs.style, { outline: "none" })
style: attrs?.style : { outline: "none" },
? Object.assign(attrs.style, { outline: "none" }) ...attrs
: { outline: "none" }, },
...attrs {
}, default: () => []
{ }
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, IconifyIcon,
{ {
icon: `${this.icon}`, icon: `${this.icon}`,
"aria-hidden": false,
style: attrs?.style style: attrs?.style
? Object.assign(attrs.style, { outline: "none" }) ? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" }, : { outline: "none" },

View File

@ -1,23 +1,14 @@
// 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载 // 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载
import { getSvgInfo } from "@pureadmin/utils";
import { addIcon } from "@iconify/vue/dist/offline"; 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 添加即可渲染菜单图标 // 本地菜单图标,后端在路由的 icon 中返回对应的图标字符串并且前端在此处使用 addIcon 添加即可渲染菜单图标
icons.forEach(([name, icon]) => { // @iconify-icons/ep
addIcon(name as string, getSvgInfo(icon as string)); 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

@ -1,14 +1,5 @@
import Sortable from "sortablejs";
import { useEpThemeStoreHook } from "@/store/modules/epTheme"; import { useEpThemeStoreHook } from "@/store/modules/epTheme";
import { import { defineComponent, ref, computed, type PropType, nextTick } from "vue";
type PropType,
ref,
unref,
computed,
nextTick,
defineComponent,
getCurrentInstance
} from "vue";
import { import {
delay, delay,
cloneDeep, cloneDeep,
@ -17,13 +8,12 @@ import {
getKeyList getKeyList
} from "@pureadmin/utils"; } from "@pureadmin/utils";
import Fullscreen from "~icons/ri/fullscreen-fill"; import Sortable from "sortablejs";
import ExitFullscreen from "~icons/ri/fullscreen-exit-fill"; import DragIcon from "./svg/drag.svg?component";
import DragIcon from "@/assets/table-bar/drag.svg?component"; import ExpandIcon from "./svg/expand.svg?component";
import ExpandIcon from "@/assets/table-bar/expand.svg?component"; import RefreshIcon from "./svg/refresh.svg?component";
import RefreshIcon from "@/assets/table-bar/refresh.svg?component"; import SettingIcon from "./svg/settings.svg?component";
import SettingIcon from "@/assets/table-bar/settings.svg?component"; import CollapseIcon from "./svg/collapse.svg?component";
import CollapseIcon from "@/assets/table-bar/collapse.svg?component";
const props = { const props = {
/** 头部最左边的标题 */ /** 头部最左边的标题 */
@ -43,24 +33,18 @@ const props = {
isExpandAll: { isExpandAll: {
type: Boolean, type: Boolean,
default: true default: true
},
tableKey: {
type: [String, Number] as PropType<string | number>,
default: "0"
} }
}; };
export default defineComponent({ export default defineComponent({
name: "PureTableBar", name: "PureTableBar",
props, props,
emits: ["refresh", "fullscreen"], emits: ["refresh"],
setup(props, { emit, slots, attrs }) { setup(props, { emit, slots, attrs }) {
const size = ref("default"); const size = ref("default");
const loading = ref(false); const loading = ref(false);
const checkAll = ref(true); const checkAll = ref(true);
const isFullscreen = ref(false);
const isIndeterminate = ref(false); const isIndeterminate = ref(false);
const instance = getCurrentInstance()!;
const isExpandAll = ref(props.isExpandAll); const isExpandAll = ref(props.isExpandAll);
const filterColumns = cloneDeep(props?.columns).filter(column => const filterColumns = cloneDeep(props?.columns).filter(column =>
isBoolean(column?.hide) isBoolean(column?.hide)
@ -86,9 +70,9 @@ export default defineComponent({
"text-black", "text-black",
"dark:text-white", "dark:text-white",
"duration-100", "duration-100",
"hover:text-primary!", "hover:!text-primary",
"cursor-pointer", "cursor-pointer",
"outline-hidden" "outline-none"
]; ];
}); });
@ -116,11 +100,6 @@ export default defineComponent({
toggleRowExpansionAll(props.tableRef.data, isExpandAll.value); toggleRowExpansionAll(props.tableRef.data, isExpandAll.value);
} }
function onFullscreen() {
isFullscreen.value = !isFullscreen.value;
emit("fullscreen", isFullscreen.value);
}
function toggleRowExpansionAll(data, isExpansion) { function toggleRowExpansionAll(data, isExpansion) {
data.forEach(item => { data.forEach(item => {
props.tableRef.toggleRowExpansion(item, isExpansion); props.tableRef.toggleRowExpansion(item, isExpansion);
@ -139,7 +118,6 @@ export default defineComponent({
} }
function handleCheckedColumnsChange(value: string[]) { function handleCheckedColumnsChange(value: string[]) {
checkedColumns.value = value;
const checkedCount = value.length; const checkedCount = value.length;
checkAll.value = checkedCount === checkColumnList.length; checkAll.value = checkedCount === checkColumnList.length;
isIndeterminate.value = isIndeterminate.value =
@ -188,9 +166,9 @@ export default defineComponent({
const rowDrop = (event: { preventDefault: () => void }) => { const rowDrop = (event: { preventDefault: () => void }) => {
event.preventDefault(); event.preventDefault();
nextTick(() => { nextTick(() => {
const wrapper: HTMLElement = ( const wrapper: HTMLElement = document.querySelector(
instance?.proxy?.$refs[`GroupRef${unref(props.tableKey)}`] as any ".el-checkbox-group>div"
).$el.firstElementChild; );
Sortable.create(wrapper, { Sortable.create(wrapper, {
animation: 300, animation: 300,
handle: ".drag-btn", handle: ".drag-btn",
@ -247,18 +225,7 @@ export default defineComponent({
return () => ( return () => (
<> <>
<div <div {...attrs} class="w-[99/100] mt-2 px-2 pb-2 bg-bg_color">
{...attrs}
class={[
"w-full",
"px-2",
"pb-2",
"bg-bg_color",
isFullscreen.value
? ["h-full!", "z-2002", "fixed", "inset-0"]
: "mt-2"
]}
>
<div class="flex justify-between w-full h-[60px] p-4"> <div class="flex justify-between w-full h-[60px] p-4">
{slots?.title ? ( {slots?.title ? (
slots.title() slots.title()
@ -312,7 +279,7 @@ export default defineComponent({
> >
<div class={[topClass.value]}> <div class={[topClass.value]}>
<el-checkbox <el-checkbox
class="-mr-1!" class="!-mr-1"
label="列展示" label="列展示"
v-model={checkAll.value} v-model={checkAll.value}
indeterminate={isIndeterminate.value} indeterminate={isIndeterminate.value}
@ -326,8 +293,7 @@ export default defineComponent({
<div class="pt-[6px] pl-[11px]"> <div class="pt-[6px] pl-[11px]">
<el-scrollbar max-height="36vh"> <el-scrollbar max-height="36vh">
<el-checkbox-group <el-checkbox-group
ref={`GroupRef${unref(props.tableKey)}`} v-model={checkedColumns.value}
modelValue={checkedColumns.value}
onChange={value => handleCheckedColumnsChange(value)} onChange={value => handleCheckedColumnsChange(value)}
> >
<el-space <el-space
@ -335,23 +301,22 @@ export default defineComponent({
alignment="flex-start" alignment="flex-start"
size={0} size={0}
> >
{checkColumnList.map((item, index) => { {checkColumnList.map(item => {
return ( return (
<div class="flex items-center"> <div class="flex items-center">
<DragIcon <DragIcon
class={[ class={[
"drag-btn w-[16px] mr-2", "drag-btn w-[16px] mr-2",
isFixedColumn(item) isFixedColumn(item)
? "cursor-no-drop!" ? "!cursor-no-drop"
: "cursor-grab!" : "!cursor-grab"
]} ]}
onMouseenter={(event: { onMouseenter={(event: {
preventDefault: () => void; preventDefault: () => void;
}) => rowDrop(event)} }) => rowDrop(event)}
/> />
<el-checkbox <el-checkbox
key={index} key={item}
label={item}
value={item} value={item}
onChange={value => onChange={value =>
handleCheckColumnListChange(value, item) handleCheckColumnListChange(value, item)
@ -372,14 +337,6 @@ export default defineComponent({
</el-scrollbar> </el-scrollbar>
</div> </div>
</el-popover> </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>
</div> </div>
{slots.default({ {slots.default({

View File

Before

Width:  |  Height:  |  Size: 439 B

After

Width:  |  Height:  |  Size: 439 B

View File

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

After

Width:  |  Height:  |  Size: 392 B

View File

Before

Width:  |  Height:  |  Size: 161 B

After

Width:  |  Height:  |  Size: 161 B

View File

Before

Width:  |  Height:  |  Size: 235 B

After

Width:  |  Height:  |  Size: 235 B

View File

Before

Width:  |  Height:  |  Size: 840 B

After

Width:  |  Height:  |  Size: 840 B

View File

@ -1,20 +1,11 @@
.pure-segmented { .pure-segmented {
--pure-control-padding-horizontal: 12px;
--pure-control-padding-horizontal-sm: 8px;
--pure-segmented-track-padding: 2px;
--pure-segmented-line-width: 1px;
--pure-segmented-border-radius-small: 4px;
--pure-segmented-border-radius-base: 6px;
--pure-segmented-border-radius-large: 8px;
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
padding: var(--pure-segmented-track-padding); padding: 2px;
font-size: var(--el-font-size-base); font-size: 14px;
color: rgba(0, 0, 0, 0.65); color: rgba(0, 0, 0, 0.65);
background-color: rgb(0 0 0 / 4%); background-color: rgb(0 0 0 / 4%);
border-radius: var(--pure-segmented-border-radius-base); border-radius: 2px;
} }
.pure-segmented-block { .pure-segmented-block {
@ -32,74 +23,6 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/* small */
.pure-segmented.pure-segmented--small {
border-radius: var(--pure-segmented-border-radius-small);
}
.pure-segmented.pure-segmented--small .pure-segmented-item {
border-radius: var(--el-border-radius-small);
}
.pure-segmented.pure-segmented--small .pure-segmented-item > div {
min-height: calc(
var(--el-component-size-small) - var(--pure-segmented-track-padding) * 2
);
line-height: calc(
var(--el-component-size-small) - var(--pure-segmented-track-padding) * 2
);
padding: 0
calc(
var(--pure-control-padding-horizontal-sm) -
var(--pure-segmented-line-width)
);
}
/* large */
.pure-segmented.pure-segmented--large {
border-radius: var(--pure-segmented-border-radius-large);
}
.pure-segmented.pure-segmented--large .pure-segmented-item {
border-radius: calc(
var(--el-border-radius-base) + var(--el-border-radius-small)
);
}
.pure-segmented.pure-segmented--large .pure-segmented-item > div {
min-height: calc(
var(--el-component-size-large) - var(--pure-segmented-track-padding) * 2
);
line-height: calc(
var(--el-component-size-large) - var(--pure-segmented-track-padding) * 2
);
padding: 0
calc(
var(--pure-control-padding-horizontal) - var(--pure-segmented-line-width)
);
font-size: var(--el-font-size-medium);
}
/* default */
.pure-segmented-item {
position: relative;
text-align: center;
cursor: pointer;
border-radius: var(--el-border-radius-base);
transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.pure-segmented .pure-segmented-item > div {
min-height: calc(
var(--el-component-size) - var(--pure-segmented-track-padding) * 2
);
line-height: calc(
var(--el-component-size) - var(--pure-segmented-track-padding) * 2
);
padding: 0
calc(
var(--pure-control-padding-horizontal) - var(--pure-segmented-line-width)
);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.pure-segmented-group { .pure-segmented-group {
position: relative; position: relative;
display: flex; display: flex;
@ -129,6 +52,23 @@
will-change: transform, width; will-change: transform, width;
} }
.pure-segmented-item {
position: relative;
text-align: center;
cursor: pointer;
border-radius: 4px;
transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.pure-segmented-item > div {
min-height: 28px;
line-height: 28px;
padding: 0 11px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.pure-segmented-item > input { .pure-segmented-item > input {
position: absolute; position: absolute;
inset-block-start: 0; inset-block-start: 0;

View File

@ -1,14 +1,5 @@
import "./index.css"; import "./index.css";
import type { OptionsType } from "./type";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { import {
useDark,
isNumber,
isFunction,
useResizeObserver
} from "@pureadmin/utils";
import {
type PropType,
h, h,
ref, ref,
toRef, toRef,
@ -17,6 +8,14 @@ import {
defineComponent, defineComponent,
getCurrentInstance getCurrentInstance
} from "vue"; } from "vue";
import type { OptionsType } from "./type";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import {
isFunction,
isNumber,
useDark,
useResizeObserver
} from "@pureadmin/utils";
const props = { const props = {
options: { options: {
@ -33,20 +32,6 @@ const props = {
block: { block: {
type: Boolean, type: Boolean,
default: false default: false
},
/** 控件尺寸 */
size: {
type: String as PropType<"small" | "default" | "large">
},
/** 是否全局禁用,默认 `false` */
disabled: {
type: Boolean,
default: false
},
/** 当内容发生变化时,设置 `resize` 可使其自适应容器位置 */
resize: {
type: Boolean,
default: false
} }
}; };
@ -67,7 +52,7 @@ export default defineComponent({
: ref(0); : ref(0);
function handleChange({ option, index }, event: Event) { function handleChange({ option, index }, event: Event) {
if (props.disabled || option.disabled) return; if (option.disabled) return;
event.preventDefault(); event.preventDefault();
isNumber(props.modelValue) isNumber(props.modelValue)
? emit("update:modelValue", index) ? emit("update:modelValue", index)
@ -77,7 +62,6 @@ export default defineComponent({
} }
function handleMouseenter({ option, index }, event: Event) { function handleMouseenter({ option, index }, event: Event) {
if (props.disabled) return;
event.preventDefault(); event.preventDefault();
curMouseActive.value = index; curMouseActive.value = index;
if (option.disabled || curIndex.value === index) { if (option.disabled || curIndex.value === index) {
@ -90,7 +74,6 @@ export default defineComponent({
} }
function handleMouseleave(_, event: Event) { function handleMouseleave(_, event: Event) {
if (props.disabled) return;
event.preventDefault(); event.preventDefault();
curMouseActive.value = -1; curMouseActive.value = -1;
} }
@ -98,14 +81,13 @@ export default defineComponent({
function handleInit(index = curIndex.value) { function handleInit(index = curIndex.value) {
nextTick(() => { nextTick(() => {
const curLabelRef = instance?.proxy?.$refs[`labelRef${index}`] as ElRef; const curLabelRef = instance?.proxy?.$refs[`labelRef${index}`] as ElRef;
if (!curLabelRef) return;
width.value = curLabelRef.clientWidth; width.value = curLabelRef.clientWidth;
translateX.value = curLabelRef.offsetLeft; translateX.value = curLabelRef.offsetLeft;
initStatus.value = true; initStatus.value = true;
}); });
} }
function handleResizeInit() { if (props.block) {
useResizeObserver(".pure-segmented", () => { useResizeObserver(".pure-segmented", () => {
nextTick(() => { nextTick(() => {
handleInit(curIndex.value); handleInit(curIndex.value);
@ -113,8 +95,6 @@ export default defineComponent({
}); });
} }
(props.block || props.resize) && handleResizeInit();
watch( watch(
() => curIndex.value, () => curIndex.value,
index => { index => {
@ -123,14 +103,11 @@ export default defineComponent({
}); });
}, },
{ {
deep: true,
immediate: true immediate: true
} }
); );
watch(() => props.size, handleResizeInit, {
immediate: true
});
const rendLabel = () => { const rendLabel = () => {
return props.options.map((option, index) => { return props.options.map((option, index) => {
return ( return (
@ -138,16 +115,14 @@ export default defineComponent({
ref={`labelRef${index}`} ref={`labelRef${index}`}
class={[ class={[
"pure-segmented-item", "pure-segmented-item",
(props.disabled || option?.disabled) && option?.disabled && "pure-segmented-item-disabled"
"pure-segmented-item-disabled"
]} ]}
style={{ style={{
background: background:
curMouseActive.value === index ? segmentedItembg.value : "", curMouseActive.value === index ? segmentedItembg.value : "",
color: props.disabled color:
? null !option.disabled &&
: !option.disabled && (curIndex.value === index || curMouseActive.value === index)
(curIndex.value === index || curMouseActive.value === index)
? isDark.value ? isDark.value
? "rgba(255, 255, 255, 0.85)" ? "rgba(255, 255, 255, 0.85)"
: "rgba(0,0,0,.88)" : "rgba(0,0,0,.88)"
@ -192,12 +167,7 @@ export default defineComponent({
return () => ( return () => (
<div <div
class={{ class={["pure-segmented", props.block ? "pure-segmented-block" : ""]}
"pure-segmented": true,
"pure-segmented-block": props.block,
"pure-segmented--large": props.size === "large",
"pure-segmented--small": props.size === "small"
}}
> >
<div class="pure-segmented-group"> <div class="pure-segmented-group">
<div <div

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import axios from "axios";
import type { App } from "vue"; import type { App } from "vue";
import axios from "axios";
let config: object = {}; let config: object = {};
const { VITE_PUBLIC_PATH } = import.meta.env; const { VITE_PUBLIC_PATH } = import.meta.env;
@ -35,7 +35,7 @@ export const getPlatformConfig = async (app: App): Promise<undefined> => {
}) })
.then(({ data: config }) => { .then(({ data: config }) => {
let $config = app.config.globalProperties.$config; let $config = app.config.globalProperties.$config;
// 自动注入系统配置 // 自动注入项目配置
if (app && $config && typeof config === "object") { if (app && $config && typeof config === "object") {
$config = Object.assign($config, config); $config = Object.assign($config, config);
app.config.globalProperties.$config = $config; app.config.globalProperties.$config = $config;

View File

@ -2,7 +2,7 @@ import { hasAuth } from "@/router/utils";
import type { Directive, DirectiveBinding } from "vue"; import type { Directive, DirectiveBinding } from "vue";
export const auth: Directive = { export const auth: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding<string | Array<string>>) { mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding; const { value } = binding;
if (value) { if (value) {
!hasAuth(value) && el.parentNode?.removeChild(el); !hasAuth(value) && el.parentNode?.removeChild(el);

View File

@ -3,13 +3,13 @@ import { useEventListener } from "@vueuse/core";
import { copyTextToClipboard } from "@pureadmin/utils"; import { copyTextToClipboard } from "@pureadmin/utils";
import type { Directive, DirectiveBinding } from "vue"; import type { Directive, DirectiveBinding } from "vue";
export interface CopyEl extends HTMLElement { interface CopyEl extends HTMLElement {
copyValue: string; copyValue: string;
} }
/** 文本复制指令(默认双击复制) */ /** 文本复制指令(默认双击复制) */
export const copy: Directive = { export const copy: Directive = {
mounted(el: CopyEl, binding: DirectiveBinding<string>) { mounted(el: CopyEl, binding: DirectiveBinding) {
const { value } = binding; const { value } = binding;
if (value) { if (value) {
el.copyValue = value; el.copyValue = value;

View File

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

View File

@ -3,7 +3,7 @@ import type { Directive, DirectiveBinding } from "vue";
import { subBefore, subAfter, isFunction } from "@pureadmin/utils"; import { subBefore, subAfter, isFunction } from "@pureadmin/utils";
export const longpress: Directive = { export const longpress: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding<Function>) { mounted(el: HTMLElement, binding: DirectiveBinding) {
const cb = binding.value; const cb = binding.value;
if (cb && isFunction(cb)) { if (cb && isFunction(cb)) {
let timer = null; let timer = null;

View File

@ -1,29 +1,16 @@
import { import {
isArray, isFunction,
throttle,
debounce,
isObject, isObject,
isFunction isArray,
debounce,
throttle
} from "@pureadmin/utils"; } from "@pureadmin/utils";
import { useEventListener } from "@vueuse/core"; import { useEventListener } from "@vueuse/core";
import type { Directive, DirectiveBinding } from "vue"; import type { Directive, DirectiveBinding } from "vue";
export interface OptimizeOptions {
/** 事件名 */
event: string;
/** 事件触发的方法 */
fn: (...params: any) => any;
/** 是否立即执行 */
immediate?: boolean;
/** 防抖或节流的延迟时间(防抖默认:`200`毫秒、节流默认:`1000`毫秒) */
timeout?: number;
/** 传递的参数 */
params?: any;
}
/** 防抖v-optimize或v-optimize:debounce、节流v-optimize:throttle指令 */ /** 防抖v-optimize或v-optimize:debounce、节流v-optimize:throttle指令 */
export const optimize: Directive = { export const optimize: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding<OptimizeOptions>) { mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding; const { value } = binding;
const optimizeType = binding.arg ?? "debounce"; const optimizeType = binding.arg ?? "debounce";
const type = ["debounce", "throttle"].find(t => t === optimizeType); const type = ["debounce", "throttle"].find(t => t === optimizeType);

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

@ -2,10 +2,8 @@ import "./index.scss";
import { isObject } from "@pureadmin/utils"; import { isObject } from "@pureadmin/utils";
import type { Directive, DirectiveBinding } from "vue"; import type { Directive, DirectiveBinding } from "vue";
export interface RippleOptions { interface RippleOptions {
/** 自定义`ripple`颜色,支持`tailwindcss` */
class?: string; class?: string;
/** 是否从中心扩散 */
center?: boolean; center?: boolean;
circle?: boolean; circle?: boolean;
} }
@ -32,8 +30,8 @@ const calculate = (
const offset = el.getBoundingClientRect(); const offset = el.getBoundingClientRect();
// 获取点击位置距离 el 的垂直和水平距离 // 获取点击位置距离 el 的垂直和水平距离
const localX = e.clientX - offset.left; let localX = e.clientX - offset.left;
const localY = e.clientY - offset.top; let localY = e.clientY - offset.top;
let radius = 0; let radius = 0;
let scale = 0.3; let scale = 0.3;
@ -222,6 +220,13 @@ function updated(el: HTMLElement, binding: RippleDirectiveBinding) {
updateRipple(el, binding, wasEnabled); updateRipple(el, binding, wasEnabled);
} }
/**
* @description v-ripple
* @use
* 1. v-ripple ripple
* 2. v-ripple="{ class: 'text-red' }" ripple tailwindcss color
* 3. v-ripple.center
*/
export const Ripple: Directive = { export const Ripple: Directive = {
mounted, mounted,
unmounted, unmounted,

View File

@ -1,9 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import LayFrame from "../lay-frame/index.vue"; import Footer from "./footer/index.vue";
import LayFooter from "../lay-footer/index.vue";
import { useTags } from "@/layout/hooks/useTag";
import { useGlobal, isNumber } from "@pureadmin/utils"; import { useGlobal, isNumber } from "@pureadmin/utils";
import BackTopIcon from "@/assets/svg/back_top.svg?component"; import KeepAliveFrame from "./keepAliveFrame/index.vue";
import backTop from "@/assets/svg/back_top.svg?component";
import { h, computed, Transition, defineComponent } from "vue"; import { h, computed, Transition, defineComponent } from "vue";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
@ -11,7 +10,6 @@ const props = defineProps({
fixedHeader: Boolean fixedHeader: Boolean
}); });
const { showModel } = useTags();
const { $storage, $config } = useGlobal<GlobalPropertiesApi>(); const { $storage, $config } = useGlobal<GlobalPropertiesApi>();
const isKeepAlive = computed(() => { const isKeepAlive = computed(() => {
@ -51,17 +49,9 @@ const getMainWidth = computed(() => {
const getSectionStyle = computed(() => { const getSectionStyle = computed(() => {
return [ return [
hideTabs.value && layout ? "padding-top: 48px;" : "", hideTabs.value && layout ? "padding-top: 48px;" : "",
!hideTabs.value && layout !hideTabs.value && layout ? "padding-top: 81px;" : "",
? showModel.value == "chrome"
? "padding-top: 85px;"
: "padding-top: 81px;"
: "",
hideTabs.value && !layout.value ? "padding-top: 48px;" : "", hideTabs.value && !layout.value ? "padding-top: 48px;" : "",
!hideTabs.value && !layout.value !hideTabs.value && !layout.value ? "padding-top: 81px;" : "",
? showModel.value == "chrome"
? "padding-top: 85px;"
: "padding-top: 81px;"
: "",
props.fixedHeader props.fixedHeader
? "" ? ""
: `padding-top: 0;${ : `padding-top: 0;${
@ -107,15 +97,15 @@ const transitionMain = defineComponent({
<template> <template>
<section <section
:class="[fixedHeader ? 'app-main' : 'app-main-nofixed-header']" :class="[props.fixedHeader ? 'app-main' : 'app-main-nofixed-header']"
:style="getSectionStyle" :style="getSectionStyle"
> >
<router-view> <router-view>
<template #default="{ Component, route }"> <template #default="{ Component, route }">
<LayFrame :currComp="Component" :currRoute="route"> <KeepAliveFrame :currComp="Component" :currRoute="route">
<template #default="{ Comp, fullPath, frameInfo }"> <template #default="{ Comp, fullPath, frameInfo }">
<el-scrollbar <el-scrollbar
v-if="fixedHeader" v-if="props.fixedHeader"
:wrap-style="{ :wrap-style="{
display: 'flex', display: 'flex',
'flex-wrap': 'wrap', 'flex-wrap': 'wrap',
@ -134,7 +124,7 @@ const transitionMain = defineComponent({
title="回到顶部" title="回到顶部"
target=".app-main .el-scrollbar__wrap" target=".app-main .el-scrollbar__wrap"
> >
<BackTopIcon /> <backTop />
</el-backtop> </el-backtop>
<div class="grow"> <div class="grow">
<transitionMain :route="route"> <transitionMain :route="route">
@ -158,7 +148,7 @@ const transitionMain = defineComponent({
/> />
</transitionMain> </transitionMain>
</div> </div>
<LayFooter v-if="!hideFooter" /> <Footer v-if="!hideFooter" />
</el-scrollbar> </el-scrollbar>
<div v-else class="grow"> <div v-else class="grow">
<transitionMain :route="route"> <transitionMain :route="route">
@ -183,12 +173,12 @@ const transitionMain = defineComponent({
</transitionMain> </transitionMain>
</div> </div>
</template> </template>
</LayFrame> </KeepAliveFrame>
</template> </template>
</router-view> </router-view>
<!-- 页脚 --> <!-- 页脚 -->
<LayFooter v-if="!hideFooter && !fixedHeader" /> <Footer v-if="!hideFooter && !props.fixedHeader" />
</section> </section>
</template> </template>

View File

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

View File

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { getConfig } from "@/config"; import { getConfig } from "@/config";
import { useMultiFrame } from "@/layout/hooks/useMultiFrame";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { type Component, shallowRef, watch, computed } from "vue"; import { type Component, shallowRef, watch, computed } from "vue";
import { type RouteRecordRaw, RouteLocationNormalizedLoaded } from "vue-router"; import { type RouteRecordRaw, RouteLocationNormalizedLoaded } from "vue-router";
import { useMultiFrame } from "@/layout/components/keepAliveFrame/useMultiFrame";
const props = defineProps<{ const props = defineProps<{
currRoute: RouteLocationNormalizedLoaded; currRoute: RouteLocationNormalizedLoaded;
@ -20,7 +20,7 @@ const keep = computed(() => {
!!props.currRoute.meta?.frameSrc !!props.currRoute.meta?.frameSrc
); );
}); });
// LayFrame // frameView
const normalComp = computed(() => !keep.value && props.currComp); const normalComp = computed(() => !keep.value && props.currComp);
watch(useMultiTagsStoreHook().multiTags, (tags: any) => { watch(useMultiTagsStoreHook().multiTags, (tags: any) => {
@ -65,7 +65,7 @@ watch(
</script> </script>
<template> <template>
<template v-for="[fullPath, Comp] in compList" :key="fullPath"> <template v-for="[fullPath, Comp] in compList" :key="fullPath">
<div v-show="fullPath === currRoute.fullPath" class="w-full h-full"> <div v-show="fullPath === props.currRoute.fullPath" class="w-full h-full">
<slot <slot
:fullPath="fullPath" :fullPath="fullPath"
:Comp="Comp" :Comp="Comp"
@ -74,6 +74,6 @@ watch(
</div> </div>
</template> </template>
<div v-show="!keep" class="w-full h-full"> <div v-show="!keep" class="w-full h-full">
<slot :Comp="normalComp" :fullPath="currRoute.fullPath" frameInfo /> <slot :Comp="normalComp" :fullPath="props.currRoute.fullPath" frameInfo />
</div> </div>
</template> </template>

View File

@ -1,23 +0,0 @@
<script setup lang="ts">
import { PropType } from "vue";
import { ListItem } from "../data";
import NoticeItem from "./NoticeItem.vue";
defineProps({
list: {
type: Array as PropType<Array<ListItem>>,
default: () => []
},
emptyText: {
type: String,
default: ""
}
});
</script>
<template>
<div v-if="list.length">
<NoticeItem v-for="(item, index) in list" :key="index" :noticeItem="item" />
</div>
<el-empty v-else :description="emptyText" />
</template>

View File

@ -1,97 +0,0 @@
export interface ListItem {
avatar: string;
title: string;
datetime: string;
type: string;
description: string;
status?: "primary" | "success" | "warning" | "info" | "danger";
extra?: string;
}
export interface TabItem {
key: string;
name: string;
list: ListItem[];
emptyText: string;
}
export const noticesData: TabItem[] = [
{
key: "1",
name: "通知",
list: [],
emptyText: "暂无通知"
},
{
key: "2",
name: "消息",
list: [
{
avatar: "https://xiaoxian521.github.io/hyperlink/svg/smile1.svg",
title: "小铭 评论了你",
description: "诚在于心,信在于行,诚信在于心行合一。",
datetime: "今天",
type: "2"
},
{
avatar: "https://xiaoxian521.github.io/hyperlink/svg/smile2.svg",
title: "李白 回复了你",
description: "长风破浪会有时,直挂云帆济沧海。",
datetime: "昨天",
type: "2"
},
{
avatar: "https://xiaoxian521.github.io/hyperlink/svg/smile5.svg",
title: "标题",
description:
"请将鼠标移动到此处以便测试超长的消息在此处将如何处理。本例中设置的描述最大行数为2超过2行的描述内容将被省略并且可以通过tooltip查看完整内容",
datetime: "时间",
type: "2"
}
],
emptyText: "暂无消息"
},
{
key: "3",
name: "待办",
list: [
{
avatar: "",
title: "第三方紧急代码变更",
description:
"小林提交于 2024-05-10需在 2024-05-11 前完成代码变更任务",
datetime: "",
extra: "马上到期",
status: "danger",
type: "3"
},
{
avatar: "",
title: "版本发布",
description: "指派小铭于 2024-06-18 前完成更新并发布",
datetime: "",
extra: "已耗时 8 天",
status: "warning",
type: "3"
},
{
avatar: "",
title: "新功能开发",
description: "开发多租户管理",
datetime: "",
extra: "进行中",
type: "3"
},
{
avatar: "",
title: "任务名称",
description: "任务需要在 2030-10-30 10:00 前启动",
datetime: "",
extra: "未开始",
status: "info",
type: "3"
}
],
emptyText: "暂无待办"
}
];

View File

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

View File

@ -1,33 +0,0 @@
<template>
<svg class="w-full h-full">
<defs>
<symbol id="geometry-left" viewBox="0 0 214 36">
<path d="M17 0h197v36H0v-2c4.5 0 9-3.5 9-8V8c0-4.5 3.5-8 8-8z" />
</symbol>
<symbol id="geometry-right" viewBox="0 0 214 36">
<use xlink:href="#geometry-left" />
</symbol>
<clipPath>
<rect width="100%" height="100%" x="0" />
</clipPath>
</defs>
<svg width="51%" height="100%">
<use
xlink:href="#geometry-left"
width="214"
height="36"
fill="currentColor"
/>
</svg>
<g transform="scale(-1, 1)">
<svg width="51%" height="100%" x="-100%" y="0">
<use
xlink:href="#geometry-right"
width="214"
height="36"
fill="currentColor"
/>
</svg>
</g>
</svg>
</template>

View File

@ -1,14 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import Search from "./search/index.vue";
import Notice from "./notice/index.vue";
import mixNav from "./sidebar/mixNav.vue";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import LaySearch from "../lay-search/index.vue"; import FullScreen from "./sidebar/fullScreen.vue";
import LayNotice from "../lay-notice/index.vue"; import Breadcrumb from "./sidebar/breadCrumb.vue";
import LayNavMix from "../lay-sidebar/NavMix.vue"; import topCollapse from "./sidebar/topCollapse.vue";
import LaySidebarFullScreen from "../lay-sidebar/components/SidebarFullScreen.vue"; import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import LaySidebarBreadCrumb from "../lay-sidebar/components/SidebarBreadCrumb.vue"; import Setting from "@iconify-icons/ri/settings-3-line";
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";
const { const {
layout, layout,
@ -24,28 +23,28 @@ const {
</script> </script>
<template> <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 <topCollapse
v-if="device === 'mobile'" v-if="device === 'mobile'"
class="hamburger-container" class="hamburger-container"
:is-active="pureApp.sidebar.opened" :is-active="pureApp.sidebar.opened"
@toggleClick="toggleSideBar" @toggleClick="toggleSideBar"
/> />
<LaySidebarBreadCrumb <Breadcrumb
v-if="layout !== 'mix' && device !== 'mobile'" v-if="layout !== 'mix' && device !== 'mobile'"
class="breadcrumb-container" class="breadcrumb-container"
/> />
<LayNavMix v-if="layout === 'mix'" /> <mixNav v-if="layout === 'mix'" />
<div v-if="layout === 'vertical'" class="vertical-header-right"> <div v-if="layout === 'vertical'" class="vertical-header-right">
<!-- 菜单搜索 --> <!-- 菜单搜索 -->
<LaySearch id="header-search" /> <Search id="header-search" />
<!-- 全屏 --> <!-- 全屏 -->
<LaySidebarFullScreen id="full-screen" /> <FullScreen id="full-screen" />
<!-- 消息通知 --> <!-- 消息通知 -->
<LayNotice id="header-notice" /> <Notice id="header-notice" />
<!-- 退出登录 --> <!-- 退出登录 -->
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none"> <span class="el-dropdown-link navbar-bg-hover select-none">
@ -66,7 +65,7 @@ const {
</el-dropdown> </el-dropdown>
<span <span
class="set-icon navbar-bg-hover" class="set-icon navbar-bg-hover"
title="打开系统配置" title="打开项目配置"
@click="onPanel" @click="onPanel"
> >
<IconifyIconOffline :icon="Setting" /> <IconifyIconOffline :icon="Setting" />
@ -124,7 +123,7 @@ const {
} }
.logout { .logout {
width: 120px; max-width: 120px;
::v-deep(.el-dropdown-menu__item) { ::v-deep(.el-dropdown-menu__item) {
display: inline-flex; display: inline-flex;

View File

@ -0,0 +1,146 @@
export interface ListItem {
avatar: string;
title: string;
datetime: string;
type: string;
description: string;
status?: "primary" | "success" | "warning" | "info" | "danger";
extra?: string;
}
export interface TabItem {
key: string;
name: string;
list: ListItem[];
}
export const noticesData: TabItem[] = [
{
key: "1",
name: "通知",
list: [
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
title: "你收到了 12 份新周报",
datetime: "一年前",
description: "",
type: "1"
},
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
title: "你推荐的 前端高手 已通过第三轮面试",
datetime: "一年前",
description: "",
type: "1"
},
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png",
title: "这种模板可以区分多种通知类型",
datetime: "一年前",
description: "",
type: "1"
},
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
title:
"展示标题内容超过一行后的处理方式如果内容超过1行将自动截断并支持tooltip显示完整标题。",
datetime: "一年前",
description: "",
type: "1"
},
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
title: "左侧图标用于区分不同的类型",
datetime: "一年前",
description: "",
type: "1"
},
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
title: "左侧图标用于区分不同的类型",
datetime: "一年前",
description: "",
type: "1"
}
]
},
{
key: "2",
name: "消息",
list: [
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
title: "李白 评论了你",
description: "长风破浪会有时,直挂云帆济沧海",
datetime: "一年前",
type: "2"
},
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
title: "李白 回复了你",
description: "行路难,行路难,多歧路,今安在。",
datetime: "一年前",
type: "2"
},
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
title: "标题",
description:
"请将鼠标移动到此处以便测试超长的消息在此处将如何处理。本例中设置的描述最大行数为2超过2行的描述内容将被省略并且可以通过tooltip查看完整内容",
datetime: "一年前",
type: "2"
}
]
},
{
key: "3",
name: "待办",
list: [
{
avatar: "",
title: "任务名称",
description: "任务需要在 2022-11-16 20:00 前启动",
datetime: "",
extra: "未开始",
status: "info",
type: "3"
},
{
avatar: "",
title: "第三方紧急代码变更",
description:
"一拳提交于 2022-11-16需在 2022-11-18 前完成代码变更任务",
datetime: "",
extra: "马上到期",
status: "danger",
type: "3"
},
{
avatar: "",
title: "信息安全考试",
description: "指派小仙于 2022-12-12 前完成更新并发布",
datetime: "",
extra: "已耗时 8 天",
status: "warning",
type: "3"
},
{
avatar: "",
title: "vue-pure-admin 版本发布",
description: "vue-pure-admin 版本发布",
datetime: "",
extra: "进行中",
type: "3"
}
]
}
];

View File

@ -1,34 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from "vue"; import { ref } from "vue";
import { noticesData } from "./data"; import { noticesData } from "./data";
import NoticeList from "./components/NoticeList.vue"; import NoticeList from "./noticeList.vue";
import BellIcon from "~icons/ep/bell"; import Bell from "@iconify-icons/ep/bell";
const noticesNum = ref(0); const noticesNum = ref(0);
const notices = ref(noticesData); const notices = ref(noticesData);
const activeKey = ref(noticesData[0]?.key); const activeKey = ref(noticesData[0].key);
notices.value.map(v => (noticesNum.value += v.list.length)); notices.value.map(v => (noticesNum.value += v.list.length));
const getLabel = computed(
() => item =>
item.name + (item.list.length > 0 ? `(${item.list.length})` : "")
);
</script> </script>
<template> <template>
<el-dropdown trigger="click" placement="bottom-end"> <el-dropdown trigger="click" placement="bottom-end">
<span <span class="dropdown-badge navbar-bg-hover select-none">
:class="[ <el-badge :value="noticesNum" :max="99">
'dropdown-badge',
'navbar-bg-hover',
'select-none',
Number(noticesNum) !== 0 && 'mr-[10px]'
]"
>
<el-badge :value="Number(noticesNum) === 0 ? '' : noticesNum" :max="99">
<span class="header-notice-icon"> <span class="header-notice-icon">
<IconifyIconOffline :icon="BellIcon" /> <IconifyIconOffline :icon="Bell" />
</span> </span>
</el-badge> </el-badge>
</span> </span>
@ -47,10 +35,13 @@ const getLabel = computed(
/> />
<span v-else> <span v-else>
<template v-for="item in notices" :key="item.key"> <template v-for="item in notices" :key="item.key">
<el-tab-pane :label="getLabel(item)" :name="`${item.key}`"> <el-tab-pane
:label="`${item.name}(${item.list.length})`"
:name="`${item.key}`"
>
<el-scrollbar max-height="330px"> <el-scrollbar max-height="330px">
<div class="noticeList-container"> <div class="noticeList-container">
<NoticeList :list="item.list" :emptyText="item.emptyText" /> <NoticeList :list="item.list" />
</div> </div>
</el-scrollbar> </el-scrollbar>
</el-tab-pane> </el-tab-pane>
@ -69,6 +60,7 @@ const getLabel = computed(
justify-content: center; justify-content: center;
width: 40px; width: 40px;
height: 48px; height: 48px;
margin-right: 10px;
cursor: pointer; cursor: pointer;
.header-notice-icon { .header-notice-icon {

View File

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ListItem } from "../data"; import { ListItem } from "./data";
import { ref, PropType, nextTick } from "vue"; import { ref, PropType, nextTick } from "vue";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import { deviceDetection } from "@pureadmin/utils"; import { deviceDetection } from "@pureadmin/utils";
defineProps({ const props = defineProps({
noticeItem: { noticeItem: {
type: Object as PropType<ListItem>, type: Object as PropType<ListItem>,
default: () => {} default: () => {}
@ -49,12 +49,12 @@ function hoverDescription(event, description) {
<template> <template>
<div <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 <el-avatar
v-if="noticeItem.avatar" v-if="props.noticeItem.avatar"
:size="30" :size="30"
:src="noticeItem.avatar" :src="props.noticeItem.avatar"
class="notice-container-avatar" class="notice-container-avatar"
/> />
<div class="notice-container-text"> <div class="notice-container-text">
@ -63,7 +63,7 @@ function hoverDescription(event, description) {
popper-class="notice-title-popper" popper-class="notice-title-popper"
:effect="tooltipEffect" :effect="tooltipEffect"
:disabled="!titleTooltip" :disabled="!titleTooltip"
:content="noticeItem.title" :content="props.noticeItem.title"
placement="top-start" placement="top-start"
:enterable="!isMobile" :enterable="!isMobile"
> >
@ -72,16 +72,16 @@ function hoverDescription(event, description) {
class="notice-title-content" class="notice-title-content"
@mouseover="hoverTitle" @mouseover="hoverTitle"
> >
{{ noticeItem.title }} {{ props.noticeItem.title }}
</div> </div>
</el-tooltip> </el-tooltip>
<el-tag <el-tag
v-if="noticeItem?.extra" v-if="props.noticeItem?.extra"
:type="noticeItem?.status" :type="props.noticeItem?.status"
size="small" size="small"
class="notice-title-extra" class="notice-title-extra"
> >
{{ noticeItem?.extra }} {{ props.noticeItem?.extra }}
</el-tag> </el-tag>
</div> </div>
@ -89,19 +89,19 @@ function hoverDescription(event, description) {
popper-class="notice-title-popper" popper-class="notice-title-popper"
:effect="tooltipEffect" :effect="tooltipEffect"
:disabled="!descriptionTooltip" :disabled="!descriptionTooltip"
:content="noticeItem.description" :content="props.noticeItem.description"
placement="top-start" placement="top-start"
> >
<div <div
ref="descriptionRef" ref="descriptionRef"
class="notice-text-description" class="notice-text-description"
@mouseover="hoverDescription($event, noticeItem.description)" @mouseover="hoverDescription($event, props.noticeItem.description)"
> >
{{ noticeItem.description }} {{ props.noticeItem.description }}
</div> </div>
</el-tooltip> </el-tooltip>
<div class="notice-text-datetime text-[#00000073] dark:text-white"> <div class="notice-text-datetime text-[#00000073] dark:text-white">
{{ noticeItem.datetime }} {{ props.noticeItem.datetime }}
</div> </div>
</div> </div>
</div> </div>
@ -112,7 +112,7 @@ function hoverDescription(event, description) {
max-width: 238px; max-width: 238px;
} }
</style> </style>
<style lang="scss" scoped> <style scoped lang="scss">
.notice-container { .notice-container {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
import { PropType } from "vue";
import { ListItem } from "./data";
import NoticeItem from "./noticeItem.vue";
const props = defineProps({
list: {
type: Array as PropType<Array<ListItem>>,
default: () => []
}
});
</script>
<template>
<div v-if="props.list.length">
<NoticeItem
v-for="(item, index) in props.list"
:key="index"
:noticeItem="item"
/>
</div>
<el-empty v-else description="暂无消息" />
</template>

View File

@ -3,7 +3,7 @@ import { emitter } from "@/utils/mitt";
import { onClickOutside } from "@vueuse/core"; import { onClickOutside } from "@vueuse/core";
import { ref, computed, onMounted, onBeforeUnmount } from "vue"; import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange"; import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import CloseIcon from "~icons/ep/close"; import Close from "@iconify-icons/ep/close";
const target = ref(null); const target = ref(null);
const show = ref<Boolean>(false); const show = ref<Boolean>(false);
@ -15,7 +15,7 @@ const iconClass = computed(() => {
"flex", "flex",
"justify-center", "justify-center",
"items-center", "items-center",
"outline-hidden", "outline-none",
"rounded-[4px]", "rounded-[4px]",
"cursor-pointer", "cursor-pointer",
"transition-colors", "transition-colors",
@ -49,9 +49,9 @@ onBeforeUnmount(() => {
<div class="right-panel-background" /> <div class="right-panel-background" />
<div ref="target" class="right-panel bg-bg_color"> <div ref="target" class="right-panel bg-bg_color">
<div <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> <h4 class="dark:text-white">项目配置</h4>
<span <span
v-tippy="{ v-tippy="{
content: '关闭配置', content: '关闭配置',
@ -64,7 +64,7 @@ onBeforeUnmount(() => {
class="dark:text-white" class="dark:text-white"
width="18px" width="18px"
height="18px" height="18px"
:icon="CloseIcon" :icon="Close"
@click="show = !show" @click="show = !show"
/> />
</span> </span>
@ -74,7 +74,7 @@ onBeforeUnmount(() => {
</el-scrollbar> </el-scrollbar>
<div <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 <el-button
v-tippy="{ v-tippy="{
@ -117,8 +117,8 @@ onBeforeUnmount(() => {
width: 100%; width: 100%;
max-width: 280px; max-width: 280px;
box-shadow: 0 0 15px 0 rgb(0 0 0 / 5%); 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); transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
transform: translate(100%);
} }
.show { .show {

View File

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import MdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component"; import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
import EnterOutlined from "@/assets/svg/enter_outlined.svg?component"; import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
import ArrowUpLine from "~icons/ri/arrow-up-line"; import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
import ArrowDownLine from "~icons/ri/arrow-down-line"; import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
withDefaults(defineProps<{ total?: number }>(), { const props = withDefaults(defineProps<{ total: number }>(), {
total: 0 total: 0
}); });
@ -15,7 +15,7 @@ const { device } = useNav();
<template> <template>
<div class="search-footer text-[#333] dark:text-white"> <div class="search-footer text-[#333] dark:text-white">
<span class="search-footer-item"> <span class="search-footer-item">
<EnterOutlined class="icon" /> <enterOutlined class="icon" />
确认 确认
</span> </span>
<span class="search-footer-item"> <span class="search-footer-item">
@ -24,11 +24,14 @@ const { device } = useNav();
切换 切换
</span> </span>
<span class="search-footer-item"> <span class="search-footer-item">
<MdiKeyboardEsc class="icon" /> <mdiKeyboardEsc class="icon" />
关闭 关闭
</span> </span>
<p v-if="device !== 'mobile' && total > 0" class="search-footer-total"> <p
{{ `${total}` }} v-if="device !== 'mobile' && props.total > 0"
class="search-footer-total"
>
{{ props.total }}
</p> </p>
</div> </div>
</template> </template>

View File

@ -160,7 +160,7 @@ defineExpose({ handleScroll });
</template> </template>
<template v-if="collectList.length"> <template v-if="collectList.length">
<div :style="titleStyle"> <div :style="titleStyle">
{{ `收藏${collectList.length > 1 ? "(可拖拽排序)" : ""}` }} 收藏{{ collectList.length > 1 ? "(可拖拽排序)" : "" }}
</div> </div>
<div class="collect-container"> <div class="collect-container">
<div <div

View File

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { optionsItem } from "../types"; import type { optionsItem } from "../types";
import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import StarIcon from "~icons/ep/star"; import Star from "@iconify-icons/ep/star";
import CloseIcon from "~icons/ep/close"; import Close from "@iconify-icons/ep/close";
interface Props { interface Props {
item: optionsItem; item: optionsItem;
@ -32,12 +32,12 @@ function handleDelete(item) {
</span> </span>
<IconifyIconOffline <IconifyIconOffline
v-show="item.type === 'history'" v-show="item.type === 'history'"
:icon="StarIcon" :icon="Star"
class="w-[18px] h-[18px] mr-2 hover:text-[#d7d5d4]" class="w-[18px] h-[18px] mr-2 hover:text-[#d7d5d4]"
@click.stop="handleCollect(item)" @click.stop="handleCollect(item)"
/> />
<IconifyIconOffline <IconifyIconOffline
:icon="CloseIcon" :icon="Close"
class="w-[18px] h-[18px] hover:text-[#d7d5d4] cursor-pointer" class="w-[18px] h-[18px] hover:text-[#d7d5d4] cursor-pointer"
@click.stop="handleDelete(item)" @click.stop="handleDelete(item)"
/> />

View File

@ -11,7 +11,7 @@ import { ref, computed, shallowRef, watch } from "vue";
import { useDebounceFn, onKeyStroke } from "@vueuse/core"; import { useDebounceFn, onKeyStroke } from "@vueuse/core";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
import { cloneDeep, isAllEmpty, storageLocal } from "@pureadmin/utils"; import { cloneDeep, isAllEmpty, storageLocal } from "@pureadmin/utils";
import SearchIcon from "~icons/ri/search-line"; import Search from "@iconify-icons/ri/search-line";
interface Props { interface Props {
/** 弹窗显隐 */ /** 弹窗显隐 */
@ -294,7 +294,7 @@ onKeyStroke("ArrowDown", handleDown);
> >
<template #prefix> <template #prefix>
<IconifyIconOffline <IconifyIconOffline
:icon="SearchIcon" :icon="Search"
class="text-primary w-[24px] h-[24px]" class="text-primary w-[24px] h-[24px]"
/> />
</template> </template>

View File

@ -4,7 +4,7 @@ import { useResizeObserver } from "@pureadmin/utils";
import { useEpThemeStoreHook } from "@/store/modules/epTheme"; import { useEpThemeStoreHook } from "@/store/modules/epTheme";
import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { ref, computed, getCurrentInstance, onMounted } from "vue"; import { ref, computed, getCurrentInstance, onMounted } from "vue";
import EnterOutlined from "@/assets/svg/enter_outlined.svg?component"; import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
interface Emits { interface Emits {
(e: "update:value", val: string): void; (e: "update:value", val: string): void;
@ -83,7 +83,7 @@ defineExpose({ handleScroll });
<span class="result-item-title"> <span class="result-item-title">
{{ item.meta?.title }} {{ item.meta?.title }}
</span> </span>
<EnterOutlined /> <enterOutlined />
</div> </div>
</div> </div>
</template> </template>

View File

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

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { SearchModal } from "./components";
import { useBoolean } from "../../hooks/useBoolean"; import { useBoolean } from "../../hooks/useBoolean";
import SearchModal from "./components/SearchModal.vue";
const { bool: show, toggle } = useBoolean(); const { bool: show, toggle } = useBoolean();
function handleSearch() { function handleSearch() {
@ -14,7 +14,7 @@ function handleSearch() {
class="search-container w-[40px] h-[48px] flex-c cursor-pointer navbar-bg-hover" class="search-container w-[40px] h-[48px] flex-c cursor-pointer navbar-bg-hover"
@click="handleSearch" @click="handleSearch"
> >
<IconifyIconOffline icon="ri/search-line" /> <IconifyIconOffline icon="ri:search-line" />
</div> </div>
<SearchModal v-model:value="show" /> <SearchModal v-model:value="show" />
</div> </div>

View File

@ -9,21 +9,22 @@ import {
onUnmounted, onUnmounted,
onBeforeMount onBeforeMount
} from "vue"; } from "vue";
import panel from "../panel/index.vue";
import { emitter } from "@/utils/mitt"; import { emitter } from "@/utils/mitt";
import LayPanel from "../lay-panel/index.vue";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import { useAppStoreHook } from "@/store/modules/app"; import { useAppStoreHook } from "@/store/modules/app";
import { toggleTheme } from "@pureadmin/theme/dist/browser-utils";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import Segmented, { type OptionsType } from "@/components/ReSegmented"; import Segmented, { type OptionsType } from "@/components/ReSegmented";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange"; import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import { useDark, useGlobal, debounce, isNumber } from "@pureadmin/utils"; import { useDark, useGlobal, debounce, isNumber } from "@pureadmin/utils";
import Check from "~icons/ep/check"; import Check from "@iconify-icons/ep/check";
import LeftArrow from "~icons/ri/arrow-left-s-line?width=20&height=20"; import LeftArrow from "@iconify-icons/ri/arrow-left-s-line";
import RightArrow from "~icons/ri/arrow-right-s-line?width=20&height=20"; import RightArrow from "@iconify-icons/ri/arrow-right-s-line";
import DayIcon from "@/assets/svg/day.svg?component"; import dayIcon from "@/assets/svg/day.svg?component";
import DarkIcon from "@/assets/svg/dark.svg?component"; import darkIcon from "@/assets/svg/dark.svg?component";
import SystemIcon from "@/assets/svg/system.svg?component"; import systemIcon from "@/assets/svg/system.svg?component";
const { device } = useNav(); const { device } = useNav();
const { isDark } = useDark(); const { isDark } = useDark();
@ -47,7 +48,9 @@ const {
if (unref(layoutTheme)) { if (unref(layoutTheme)) {
const layout = unref(layoutTheme).layout; const layout = unref(layoutTheme).layout;
const theme = unref(layoutTheme).theme; const theme = unref(layoutTheme).theme;
document.documentElement.setAttribute("data-theme", theme); toggleTheme({
scopeName: `layout-theme-${theme}`
});
setLayoutModel(layout); setLayoutModel(layout);
} }
@ -142,20 +145,18 @@ function setFalse(Doms): any {
} }
/** 页宽 */ /** 页宽 */
const stretchTypeOptions = computed<Array<OptionsType>>(() => { const stretchTypeOptions: Array<OptionsType> = [
return [ {
{ label: "固定",
label: "固定", tip: "紧凑页面,轻松找到所需信息",
tip: "紧凑页面,轻松找到所需信息", value: "fixed"
value: "fixed" },
}, {
{ label: "自定义",
label: "自定义", tip: "最小1280、最大1600",
tip: "最小1280、最大1600", value: "custom"
value: "custom" }
} ];
];
});
const setStretch = value => { const setStretch = value => {
settings.stretch = value; settings.stretch = value;
@ -187,28 +188,28 @@ const getThemeColor = computed(() => {
}); });
const pClass = 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>>(() => { const themeOptions = computed<Array<OptionsType>>(() => {
return [ return [
{ {
label: "浅色", label: "浅色",
icon: DayIcon, icon: dayIcon,
theme: "light", theme: "light",
tip: "清新启航,点亮舒适的工作界面", tip: "清新启航,点亮舒适的工作界面",
iconAttrs: { fill: isDark.value ? "#fff" : "#000" } iconAttrs: { fill: isDark.value ? "#fff" : "#000" }
}, },
{ {
label: "深色", label: "深色",
icon: DarkIcon, icon: darkIcon,
theme: "dark", theme: "dark",
tip: "月光序曲,沉醉于夜的静谧雅致", tip: "月光序曲,沉醉于夜的静谧雅致",
iconAttrs: { fill: isDark.value ? "#fff" : "#000" } iconAttrs: { fill: isDark.value ? "#fff" : "#000" }
}, },
{ {
label: "自动", label: "自动",
icon: SystemIcon, icon: systemIcon,
theme: "system", theme: "system",
tip: "同步时光,界面随晨昏自然呼应", tip: "同步时光,界面随晨昏自然呼应",
iconAttrs: { fill: isDark.value ? "#fff" : "#000" } iconAttrs: { fill: isDark.value ? "#fff" : "#000" }
@ -216,25 +217,18 @@ const themeOptions = computed<Array<OptionsType>>(() => {
]; ];
}); });
const markOptions = computed<Array<OptionsType>>(() => { const markOptions: Array<OptionsType> = [
return [ {
{ label: "灵动",
label: "灵动", tip: "灵动标签,添趣生辉",
tip: "灵动标签,添趣生辉", value: "smart"
value: "smart" },
}, {
{ label: "卡片",
label: "卡片", tip: "卡片标签,高效浏览",
tip: "卡片标签,高效浏览", value: "card"
value: "card" }
}, ];
{
label: "谷歌",
tip: "谷歌风格,经典美观",
value: "chrome"
}
];
});
/** 设置导航模式 */ /** 设置导航模式 */
function setLayoutModel(layout: string) { function setLayoutModel(layout: string) {
@ -297,7 +291,7 @@ function watchSystemThemeChange() {
} }
onBeforeMount(() => { onBeforeMount(() => {
/* 初始化系统配置 */ /* 初始化项目配置 */
nextTick(() => { nextTick(() => {
watchSystemThemeChange(); watchSystemThemeChange();
settings.greyVal && settings.greyVal &&
@ -313,11 +307,10 @@ onUnmounted(() => removeMatchMedia);
</script> </script>
<template> <template>
<LayPanel> <panel>
<div class="p-5"> <div class="p-5">
<p :class="pClass">整体风格</p> <p :class="pClass">整体风格</p>
<Segmented <Segmented
resize
class="select-none" class="select-none"
:modelValue="overallStyle === 'system' ? 2 : dataTheme ? 1 : 0" :modelValue="overallStyle === 'system' ? 2 : dataTheme ? 1 : 0"
:options="themeOptions" :options="themeOptions"
@ -333,7 +326,7 @@ onUnmounted(() => removeMatchMedia);
" "
/> />
<p :class="['mt-5!', pClass]">主题色</p> <p :class="['mt-5', pClass]">主题色</p>
<ul class="theme-color"> <ul class="theme-color">
<li <li
v-for="(item, index) in themeColors" v-for="(item, index) in themeColors"
@ -352,7 +345,7 @@ onUnmounted(() => removeMatchMedia);
</li> </li>
</ul> </ul>
<p :class="['mt-5!', pClass]">导航模式</p> <p :class="['mt-5', pClass]">导航模式</p>
<ul class="pure-theme"> <ul class="pure-theme">
<li <li
ref="verticalRef" ref="verticalRef"
@ -395,9 +388,8 @@ onUnmounted(() => removeMatchMedia);
</ul> </ul>
<span v-if="useAppStoreHook().getViewportWidth > 1280"> <span v-if="useAppStoreHook().getViewportWidth > 1280">
<p :class="['mt-5!', pClass]">页宽</p> <p :class="['mt-5', pClass]">页宽</p>
<Segmented <Segmented
resize
class="mb-2 select-none" class="mb-2 select-none"
:modelValue="isNumber(settings.stretch) ? 1 : 0" :modelValue="isNumber(settings.stretch) ? 1 : 0"
:options="stretchTypeOptions" :options="stretchTypeOptions"
@ -424,28 +416,29 @@ onUnmounted(() => removeMatchMedia);
> >
<IconifyIconOffline <IconifyIconOffline
:icon="settings.stretch ? RightArrow : LeftArrow" :icon="settings.stretch ? RightArrow : LeftArrow"
height="20"
/> />
<div <div
class="grow border-0 border-b border-dashed" class="flex-grow border-b border-dashed"
style="border-color: var(--el-color-primary)" style="border-color: var(--el-color-primary)"
/> />
<IconifyIconOffline <IconifyIconOffline
:icon="settings.stretch ? LeftArrow : RightArrow" :icon="settings.stretch ? LeftArrow : RightArrow"
height="20"
/> />
</div> </div>
</button> </button>
</span> </span>
<p :class="['mt-4!', pClass]">页签风格</p> <p :class="['mt-4', pClass]">页签风格</p>
<Segmented <Segmented
resize
class="select-none" class="select-none"
:modelValue="markValue === 'smart' ? 0 : markValue === 'card' ? 1 : 2" :modelValue="markValue === 'smart' ? 0 : 1"
:options="markOptions" :options="markOptions"
@change="onChange" @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"> <ul class="setting">
<li> <li>
<span class="dark:text-white">灰色模式</span> <span class="dark:text-white">灰色模式</span>
@ -511,7 +504,7 @@ onUnmounted(() => removeMatchMedia);
</li> </li>
</ul> </ul>
</div> </div>
</LayPanel> </panel>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

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

View File

@ -3,13 +3,13 @@ import { computed } from "vue";
import { useGlobal } from "@pureadmin/utils"; import { useGlobal } from "@pureadmin/utils";
import { useNav } from "@/layout/hooks/useNav"; 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 { interface Props {
isActive?: boolean; isActive: boolean;
} }
withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
isActive: false isActive: false
}); });
@ -34,7 +34,7 @@ const toggleClick = () => {
<template> <template>
<div <div
v-tippy="{ v-tippy="{
content: isActive ? '点击折叠' : '点击展开', content: props.isActive ? '点击折叠' : '点击展开',
theme: tooltipEffect, theme: tooltipEffect,
hideOnClick: 'toggle', hideOnClick: 'toggle',
placement: 'right' placement: 'right'
@ -45,7 +45,7 @@ const toggleClick = () => {
<IconifyIconOffline <IconifyIconOffline
:icon="ArrowLeft" :icon="ArrowLeft"
:class="[iconClass, themeColor === 'light' ? '' : 'text-primary']" :class="[iconClass, themeColor === 'light' ? '' : 'text-primary']"
:style="{ transform: isActive ? 'none' : 'rotateY(180deg)' }" :style="{ transform: props.isActive ? 'none' : 'rotateY(180deg)' }"
/> />
</div> </div>
</template> </template>

View File

@ -2,7 +2,7 @@
import { toRaw } from "vue"; import { toRaw } from "vue";
import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useRenderIcon } from "@/components/ReIcon/src/hooks";
defineProps({ const props = defineProps({
extraIcon: { extraIcon: {
type: String, type: String,
default: "" default: ""
@ -11,9 +11,9 @@ defineProps({
</script> </script>
<template> <template>
<div v-if="extraIcon" class="flex justify-center items-center"> <div v-if="props.extraIcon" class="flex justify-center items-center">
<component <component
:is="useRenderIcon(toRaw(extraIcon))" :is="useRenderIcon(toRaw(props.extraIcon))"
class="w-[30px] h-[30px]" class="w-[30px] h-[30px]"
/> />
</div> </div>

View File

@ -1,34 +1,26 @@
<script setup lang="ts"> <script setup lang="ts">
import { emitter } from "@/utils/mitt"; import Search from "../search/index.vue";
import Notice from "../notice/index.vue";
import FullScreen from "./fullScreen.vue";
import SidebarItem from "./sidebarItem.vue";
import { isAllEmpty } from "@pureadmin/utils";
import { ref, nextTick, computed } from "vue";
import { useNav } from "@/layout/hooks/useNav"; 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 { usePermissionStoreHook } from "@/store/modules/permission";
import LaySidebarItem from "../lay-sidebar/components/SidebarItem.vue"; import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import LaySidebarFullScreen from "../lay-sidebar/components/SidebarFullScreen.vue"; import Setting from "@iconify-icons/ri/settings-3-line";
import LogoutCircleRLine from "~icons/ri/logout-circle-r-line";
import Setting from "~icons/ri/settings-3-line";
const menuRef = ref(); const menuRef = ref();
const showLogo = ref(
storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}configure`
)?.showLogo ?? true
);
const { const {
route, route,
title, title,
logout, logout,
backTopMenu,
onPanel, onPanel,
getLogo, getLogo,
username, username,
userAvatar, userAvatar,
backTopMenu,
avatarsStyle avatarsStyle
} = useNav(); } = useNav();
@ -39,12 +31,6 @@ const defaultActive = computed(() =>
nextTick(() => { nextTick(() => {
menuRef.value?.handleResize(); menuRef.value?.handleResize();
}); });
onMounted(() => {
emitter.on("logoChange", key => {
showLogo.value = key;
});
});
</script> </script>
<template> <template>
@ -52,18 +38,19 @@ onMounted(() => {
v-loading="usePermissionStoreHook().wholeMenus.length === 0" v-loading="usePermissionStoreHook().wholeMenus.length === 0"
class="horizontal-header" 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" /> <img :src="getLogo()" alt="logo" />
<span>{{ title }}</span> <span>{{ title }}</span>
</div> </div>
<el-menu <el-menu
ref="menuRef" ref="menuRef"
router
mode="horizontal" mode="horizontal"
popper-class="pure-scrollbar" popper-class="pure-scrollbar"
class="horizontal-header-menu" class="horizontal-header-menu"
:default-active="defaultActive" :default-active="defaultActive"
> >
<LaySidebarItem <sidebar-item
v-for="route in usePermissionStoreHook().wholeMenus" v-for="route in usePermissionStoreHook().wholeMenus"
:key="route.path" :key="route.path"
:item="route" :item="route"
@ -72,11 +59,11 @@ onMounted(() => {
</el-menu> </el-menu>
<div class="horizontal-header-right"> <div class="horizontal-header-right">
<!-- 菜单搜索 --> <!-- 菜单搜索 -->
<LaySearch id="header-search" /> <Search id="header-search" />
<!-- 全屏 --> <!-- 全屏 -->
<LaySidebarFullScreen id="full-screen" /> <FullScreen id="full-screen" />
<!-- 消息通知 --> <!-- 消息通知 -->
<LayNotice id="header-notice" /> <Notice id="header-notice" />
<!-- 退出登录 --> <!-- 退出登录 -->
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover"> <span class="el-dropdown-link navbar-bg-hover">
@ -97,7 +84,7 @@ onMounted(() => {
</el-dropdown> </el-dropdown>
<span <span
class="set-icon navbar-bg-hover" class="set-icon navbar-bg-hover"
title="打开系统配置" title="打开项目配置"
@click="onPanel" @click="onPanel"
> >
<IconifyIconOffline :icon="Setting" /> <IconifyIconOffline :icon="Setting" />
@ -112,7 +99,7 @@ onMounted(() => {
} }
.logout { .logout {
width: 120px; max-width: 120px;
::v-deep(.el-dropdown-menu__item) { ::v-deep(.el-dropdown-menu__item) {
display: inline-flex; display: inline-flex;

View File

@ -3,13 +3,13 @@ import { computed } from "vue";
import { useGlobal } from "@pureadmin/utils"; import { useGlobal } from "@pureadmin/utils";
import { useNav } from "@/layout/hooks/useNav"; 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 { interface Props {
isActive?: boolean; isActive: boolean;
} }
withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
isActive: false isActive: false
}); });
@ -21,7 +21,7 @@ const iconClass = computed(() => {
"mb-1", "mb-1",
"w-[16px]", "w-[16px]",
"h-[16px]", "h-[16px]",
"inline-block!", "inline-block",
"align-middle", "align-middle",
"cursor-pointer", "cursor-pointer",
"duration-[100ms]" "duration-[100ms]"
@ -44,14 +44,14 @@ const toggleClick = () => {
<div class="left-collapse"> <div class="left-collapse">
<IconifyIconOffline <IconifyIconOffline
v-tippy="{ v-tippy="{
content: isActive ? '点击折叠' : '点击展开', content: props.isActive ? '点击折叠' : '点击展开',
theme: tooltipEffect, theme: tooltipEffect,
hideOnClick: 'toggle', hideOnClick: 'toggle',
placement: 'right' placement: 'right'
}" }"
:icon="MenuFold" :icon="MenuFold"
:class="[iconClass, themeColor === 'light' ? '' : 'text-primary']" :class="[iconClass, themeColor === 'light' ? '' : 'text-primary']"
:style="{ transform: isActive ? 'none' : 'rotateY(180deg)' }" :style="{ transform: props.isActive ? 'none' : 'rotateY(180deg)' }"
@click="toggleClick" @click="toggleClick"
/> />
</div> </div>

View File

@ -3,6 +3,10 @@ import { computed } from "vue";
import { isUrl } from "@pureadmin/utils"; import { isUrl } from "@pureadmin/utils";
import { menuType } from "@/layout/types"; import { menuType } from "@/layout/types";
defineOptions({
name: "LinkItem"
});
const props = defineProps<{ const props = defineProps<{
to: menuType; to: menuType;
}>(); }>();

View File

@ -2,7 +2,7 @@
import { getTopMenu } from "@/router/utils"; import { getTopMenu } from "@/router/utils";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
defineProps({ const props = defineProps({
collapse: Boolean collapse: Boolean
}); });
@ -10,11 +10,11 @@ const { title, getLogo } = useNav();
</script> </script>
<template> <template>
<div class="sidebar-logo-container" :class="{ collapses: collapse }"> <div class="sidebar-logo-container" :class="{ collapses: props.collapse }">
<transition name="sidebarLogoFade"> <transition name="sidebarLogoFade">
<router-link <router-link
v-if="collapse" v-if="props.collapse"
key="collapse" key="props.collapse"
:title="title" :title="title"
class="sidebar-logo-link" class="sidebar-logo-link"
:to="getTopMenu()?.path ?? '/'" :to="getTopMenu()?.path ?? '/'"
@ -60,11 +60,11 @@ const { title, getLogo } = useNav();
height: 32px; height: 32px;
margin: 2px 0 0 12px; margin: 2px 0 0 12px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
line-height: 32px; line-height: 32px;
color: var(--pure-theme-sub-menu-active-text); color: $subMenuActiveText;
text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
} }

View File

@ -1,17 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import extraIcon from "./extraIcon.vue";
import Search from "../search/index.vue";
import Notice from "../notice/index.vue";
import FullScreen from "./fullScreen.vue";
import { isAllEmpty } from "@pureadmin/utils"; import { isAllEmpty } from "@pureadmin/utils";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import LaySearch from "../lay-search/index.vue";
import LayNotice from "../lay-notice/index.vue";
import { ref, toRaw, watch, onMounted, nextTick } from "vue"; import { ref, toRaw, watch, onMounted, nextTick } from "vue";
import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { getParentPaths, findRouteByPath } from "@/router/utils"; import { getParentPaths, findRouteByPath } from "@/router/utils";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
import LaySidebarExtraIcon from "../lay-sidebar/components/SidebarExtraIcon.vue"; import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import LaySidebarFullScreen from "../lay-sidebar/components/SidebarFullScreen.vue"; import Setting from "@iconify-icons/ri/settings-3-line";
import LogoutCircleRLine from "~icons/ri/logout-circle-r-line";
import Setting from "~icons/ri/settings-3-line";
const menuRef = ref(); const menuRef = ref();
const defaultActive = ref(null); const defaultActive = ref(null);
@ -85,18 +84,18 @@ watch(
<span class="select-none"> <span class="select-none">
{{ route.meta.title }} {{ route.meta.title }}
</span> </span>
<LaySidebarExtraIcon :extraIcon="route.meta.extraIcon" /> <extraIcon :extraIcon="route.meta.extraIcon" />
</div> </div>
</template> </template>
</el-menu-item> </el-menu-item>
</el-menu> </el-menu>
<div class="horizontal-header-right"> <div class="horizontal-header-right">
<!-- 菜单搜索 --> <!-- 菜单搜索 -->
<LaySearch id="header-search" /> <Search id="header-search" />
<!-- 全屏 --> <!-- 全屏 -->
<LaySidebarFullScreen id="full-screen" /> <FullScreen id="full-screen" />
<!-- 消息通知 --> <!-- 消息通知 -->
<LayNotice id="header-notice" /> <Notice id="header-notice" />
<!-- 退出登录 --> <!-- 退出登录 -->
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none"> <span class="el-dropdown-link navbar-bg-hover select-none">
@ -117,7 +116,7 @@ watch(
</el-dropdown> </el-dropdown>
<span <span
class="set-icon navbar-bg-hover" class="set-icon navbar-bg-hover"
title="打开系统配置" title="打开项目配置"
@click="onPanel" @click="onPanel"
> >
<IconifyIconOffline :icon="Setting" /> <IconifyIconOffline :icon="Setting" />
@ -132,7 +131,7 @@ watch(
} }
.logout { .logout {
width: 120px; max-width: 120px;
::v-deep(.el-dropdown-menu__item) { ::v-deep(.el-dropdown-menu__item) {
display: inline-flex; display: inline-flex;

View File

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import path from "path";
import { getConfig } from "@/config"; import { getConfig } from "@/config";
import { posix } from "path-browserify"; import LinkItem from "./linkItem.vue";
import { menuType } from "@/layout/types"; import { menuType } from "../../types";
import extraIcon from "./extraIcon.vue";
import { ReText } from "@/components/ReText"; import { ReText } from "@/components/ReText";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import SidebarLinkItem from "./SidebarLinkItem.vue";
import SidebarExtraIcon from "./SidebarExtraIcon.vue";
import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { import {
type PropType, type PropType,
@ -16,10 +16,10 @@ import {
useAttrs useAttrs
} from "vue"; } from "vue";
import ArrowUp from "~icons/ep/arrow-up-bold"; import ArrowUp from "@iconify-icons/ep/arrow-up-bold";
import EpArrowDown from "~icons/ep/arrow-down-bold"; import EpArrowDown from "@iconify-icons/ep/arrow-down-bold";
import ArrowLeft from "~icons/ep/arrow-left-bold"; import ArrowLeft from "@iconify-icons/ep/arrow-left-bold";
import ArrowRight from "~icons/ep/arrow-right-bold"; import ArrowRight from "@iconify-icons/ep/arrow-right-bold";
const attrs = useAttrs(); const attrs = useAttrs();
const { layout, isCollapse, tooltipEffect, getDivStyle } = useNav(); 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(() => { const expandCloseIcon = computed(() => {
if (!getConfig()?.MenuArrowIconNoTransition) return ""; if (!getConfig()?.MenuArrowIconNoTransition) return "";
return { return {
@ -113,15 +98,16 @@ function resolvePath(routePath) {
if (httpReg.test(routePath) || httpReg.test(props.basePath)) { if (httpReg.test(routePath) || httpReg.test(props.basePath)) {
return routePath || props.basePath; return routePath || props.basePath;
} else { } else {
return posix.resolve(props.basePath, routePath); // 使path.posix.resolvepath.resolve windows使electron
return path.posix.resolve(props.basePath, routePath);
} }
} }
</script> </script>
<template> <template>
<SidebarLinkItem <link-item
v-if=" v-if="
hasOneShowingChild(item.children, item) && hasOneShowingChild(props.item.children, props.item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren) (!onlyOneChild.children || onlyOneChild.noShowingChildren)
" "
:to="item" :to="item"
@ -133,7 +119,7 @@ function resolvePath(routePath) {
v-bind="attrs" v-bind="attrs"
> >
<div <div
v-if="toRaw(item.meta.icon)" v-if="toRaw(props.item.meta.icon)"
class="sub-menu-icon" class="sub-menu-icon"
:style="getSubMenuIconStyle" :style="getSubMenuIconStyle"
> >
@ -141,24 +127,24 @@ function resolvePath(routePath) {
:is=" :is="
useRenderIcon( useRenderIcon(
toRaw(onlyOneChild.meta.icon) || toRaw(onlyOneChild.meta.icon) ||
(item.meta && toRaw(item.meta.icon)) (props.item.meta && toRaw(props.item.meta.icon))
) )
" "
/> />
</div> </div>
<el-text <el-text
v-if=" v-if="
(!item?.meta.icon && (!props.item?.meta.icon &&
isCollapse && isCollapse &&
layout === 'vertical' && layout === 'vertical' &&
item?.pathList?.length === 1) || props.item?.pathList?.length === 1) ||
(!onlyOneChild.meta.icon && (!onlyOneChild.meta.icon &&
isCollapse && isCollapse &&
layout === 'mix' && layout === 'mix' &&
item?.pathList?.length === 2) props.item?.pathList?.length === 2)
" "
truncated truncated
class="w-full! px-3! min-w-[54px]! text-center! text-inherit!" class="!px-4 !text-inherit"
> >
{{ onlyOneChild.meta.title }} {{ onlyOneChild.meta.title }}
</el-text> </el-text>
@ -170,54 +156,61 @@ function resolvePath(routePath) {
offset: [0, -10], offset: [0, -10],
theme: tooltipEffect theme: tooltipEffect
}" }"
class="w-full! text-inherit!" class="!text-inherit"
> >
{{ onlyOneChild.meta.title }} {{ onlyOneChild.meta.title }}
</ReText> </ReText>
<SidebarExtraIcon :extraIcon="onlyOneChild.meta.extraIcon" /> <extraIcon :extraIcon="onlyOneChild.meta.extraIcon" />
</div> </div>
</template> </template>
</el-menu-item> </el-menu-item>
</SidebarLinkItem> </link-item>
<el-sub-menu <el-sub-menu
v-else v-else
ref="subMenu" ref="subMenu"
teleported teleported
:index="resolvePath(item.path)" :index="resolvePath(props.item.path)"
v-bind="expandCloseIcon" v-bind="expandCloseIcon"
> >
<template #title> <template #title>
<div <div
v-if="toRaw(item.meta.icon)" v-if="toRaw(props.item.meta.icon)"
:style="getSubMenuIconStyle" :style="getSubMenuIconStyle"
class="sub-menu-icon" class="sub-menu-icon"
> >
<component :is="useRenderIcon(item.meta && toRaw(item.meta.icon))" /> <component
:is="useRenderIcon(props.item.meta && toRaw(props.item.meta.icon))"
/>
</div> </div>
<ReText <ReText
v-if=" v-if="
layout === 'mix' && toRaw(item.meta.icon) !(
? !isCollapse || item?.pathList?.length !== 2 layout === 'vertical' &&
: !( isCollapse &&
layout === 'vertical' && toRaw(props.item.meta.icon) &&
isCollapse && props.item.parentId === null
toRaw(item.meta.icon) && )
item.parentId === null
)
" "
:tippyProps="{ :tippyProps="{
offset: [0, -10], offset: [0, -10],
theme: tooltipEffect theme: tooltipEffect
}" }"
:class="textClass" :class="{
'!text-inherit': true,
'!px-4':
layout !== 'horizontal' &&
isCollapse &&
!toRaw(props.item.meta.icon) &&
props.item.parentId === null
}"
> >
{{ item.meta.title }} {{ props.item.meta.title }}
</ReText> </ReText>
<SidebarExtraIcon v-if="!isCollapse" :extraIcon="item.meta.extraIcon" /> <extraIcon v-if="!isCollapse" :extraIcon="props.item.meta.extraIcon" />
</template> </template>
<sidebar-item <sidebar-item
v-for="child in item.children" v-for="child in props.item.children"
:key="child.path" :key="child.path"
:is-nest="true" :is-nest="true"
:item="child" :item="child"

View File

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

View File

@ -1,16 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import Logo from "./logo.vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { emitter } from "@/utils/mitt"; import { emitter } from "@/utils/mitt";
import SidebarItem from "./sidebarItem.vue";
import LeftCollapse from "./leftCollapse.vue";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import CenterCollapse from "./centerCollapse.vue";
import { responsiveStorageNameSpace } from "@/config"; import { responsiveStorageNameSpace } from "@/config";
import { storageLocal, isAllEmpty } from "@pureadmin/utils"; import { storageLocal, isAllEmpty } from "@pureadmin/utils";
import { findRouteByPath, getParentPaths } from "@/router/utils"; import { findRouteByPath, getParentPaths } from "@/router/utils";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue"; import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
import LaySidebarLogo from "../lay-sidebar/components/SidebarLogo.vue";
import LaySidebarItem from "../lay-sidebar/components/SidebarItem.vue";
import LaySidebarLeftCollapse from "../lay-sidebar/components/SidebarLeftCollapse.vue";
import LaySidebarCenterCollapse from "../lay-sidebar/components/SidebarCenterCollapse.vue";
const route = useRoute(); const route = useRoute();
const isShow = ref(false); const isShow = ref(false);
@ -93,12 +93,13 @@ onBeforeUnmount(() => {
@mouseenter.prevent="isShow = true" @mouseenter.prevent="isShow = true"
@mouseleave.prevent="isShow = false" @mouseleave.prevent="isShow = false"
> >
<LaySidebarLogo v-if="showLogo" :collapse="isCollapse" /> <Logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar <el-scrollbar
wrap-class="scrollbar-wrapper" wrap-class="scrollbar-wrapper"
:class="[device === 'mobile' ? 'mobile' : 'pc']" :class="[device === 'mobile' ? 'mobile' : 'pc']"
> >
<el-menu <el-menu
router
unique-opened unique-opened
mode="vertical" mode="vertical"
popper-class="pure-scrollbar" popper-class="pure-scrollbar"
@ -108,7 +109,7 @@ onBeforeUnmount(() => {
:popper-effect="tooltipEffect" :popper-effect="tooltipEffect"
:default-active="defaultActive" :default-active="defaultActive"
> >
<LaySidebarItem <sidebar-item
v-for="routes in menuData" v-for="routes in menuData"
:key="routes.path" :key="routes.path"
:item="routes" :item="routes"
@ -117,12 +118,12 @@ onBeforeUnmount(() => {
/> />
</el-menu> </el-menu>
</el-scrollbar> </el-scrollbar>
<LaySidebarCenterCollapse <CenterCollapse
v-if="device !== 'mobile' && (isShow || isCollapse)" v-if="device !== 'mobile' && (isShow || isCollapse)"
:is-active="pureApp.sidebar.opened" :is-active="pureApp.sidebar.opened"
@toggleClick="toggleSideBar" @toggleClick="toggleSideBar"
/> />
<LaySidebarLeftCollapse <LeftCollapse
v-if="device !== 'mobile'" v-if="device !== 'mobile'"
:is-active="pureApp.sidebar.opened" :is-active="pureApp.sidebar.opened"
@toggleClick="toggleSideBar" @toggleClick="toggleSideBar"

View File

@ -41,13 +41,6 @@
padding-right: 24px; padding-right: 24px;
} }
&.chrome-item {
padding-right: 0;
padding-left: 0;
margin-right: -18px;
box-shadow: none;
}
.el-icon-close { .el-icon-close {
position: absolute; position: absolute;
top: 50%; top: 50%;
@ -59,10 +52,10 @@
color: var(--el-color-primary); color: var(--el-color-primary);
cursor: pointer; cursor: pointer;
border-radius: 4px; border-radius: 4px;
transform: translate(0, -50%);
transition: transition:
background-color 0.12s, background-color 0.12s,
color 0.12s; color 0.12s;
transform: translate(0, -50%);
&:hover { &:hover {
color: rgb(0 0 0 / 88%) !important; color: rgb(0 0 0 / 88%) !important;
@ -83,14 +76,6 @@
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
&.chrome-scroll-container {
padding-top: 4px;
.fixed-tag {
padding: 0 !important;
}
}
.tab { .tab {
position: relative; position: relative;
float: left; float: left;
@ -104,16 +89,6 @@
&:nth-child(1) { &:nth-child(1) {
padding: 0 12px; padding: 0 12px;
} }
&.chrome-item {
&:nth-child(1) {
padding: 0;
}
}
}
.fixed-tag {
padding: 0 12px;
} }
} }
} }
@ -127,10 +102,10 @@
font-weight: normal; font-weight: normal;
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
white-space: nowrap; white-space: nowrap;
outline: 0;
list-style-type: none; list-style-type: none;
background: #fff; background: #fff;
border-radius: 4px; border-radius: 4px;
outline: 0;
box-shadow: 0 2px 8px rgb(0 0 0 / 15%); box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
li { li {
@ -194,29 +169,9 @@
color: #fff; color: #fff;
box-shadow: 0 0 0.7px #888; box-shadow: 0 0 0.7px #888;
.chrome-tab {
z-index: 10;
}
.chrome-tab__bg {
color: var(--el-color-primary-light-9) !important;
}
.tag-title { .tag-title {
color: var(--el-color-primary) !important; color: var(--el-color-primary) !important;
} }
.chrome-close-btn {
color: var(--el-color-primary);
&:hover {
background-color: var(--el-color-primary);
}
}
.chrome-tab-divider {
opacity: 0;
}
} }
.arrow-left, .arrow-left,
@ -303,69 +258,3 @@
background: var(--el-color-primary); background: var(--el-color-primary);
animation: schedule-out-width 200ms ease-in; animation: schedule-out-width 200ms ease-in;
} }
/* 谷歌风格的页签 */
.chrome-tab {
position: relative;
display: inline-flex;
gap: 16px;
align-items: center;
justify-content: center;
padding: 0 24px;
white-space: nowrap;
cursor: pointer;
.tag-title {
padding: 0;
}
.chrome-tab-divider {
position: absolute;
right: 7px;
width: 1px;
height: 14px;
background-color: #2b2d2f;
}
&:hover {
z-index: 10;
.chrome-tab__bg {
color: #dee1e6;
}
.tag-title {
color: #1f1f1f;
}
.chrome-tab-divider {
opacity: 0;
}
}
.chrome-tab__bg {
position: absolute;
top: 0;
left: 0;
z-index: -10;
width: 100%;
height: 100%;
color: transparent;
pointer-events: none;
}
.chrome-close-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
color: #666;
border-radius: 50%;
&:hover {
color: white;
background-color: #b1b3b8;
}
}
}

View File

@ -4,11 +4,9 @@ import { RouteConfigs } from "../../types";
import { useTags } from "../../hooks/useTag"; import { useTags } from "../../hooks/useTag";
import { routerArrays } from "@/layout/types"; import { routerArrays } from "@/layout/types";
import { onClickOutside } from "@vueuse/core"; import { onClickOutside } from "@vueuse/core";
import TagChrome from "./components/TagChrome.vue";
import { handleAliveRoute, getTopMenu } from "@/router/utils"; import { handleAliveRoute, getTopMenu } from "@/router/utils";
import { useSettingStoreHook } from "@/store/modules/settings"; import { useSettingStoreHook } from "@/store/modules/settings";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { usePermissionStoreHook } from "@/store/modules/permission";
import { ref, watch, unref, toRaw, nextTick, onBeforeUnmount } from "vue"; import { ref, watch, unref, toRaw, nextTick, onBeforeUnmount } from "vue";
import { import {
delay, delay,
@ -17,11 +15,11 @@ import {
useResizeObserver useResizeObserver
} from "@pureadmin/utils"; } from "@pureadmin/utils";
import ExitFullscreen from "~icons/ri/fullscreen-exit-fill"; import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
import Fullscreen from "~icons/ri/fullscreen-fill"; import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import ArrowDown from "~icons/ri/arrow-down-s-line"; import ArrowDown from "@iconify-icons/ri/arrow-down-s-line";
import ArrowRightSLine from "~icons/ri/arrow-right-s-line"; import ArrowRightSLine from "@iconify-icons/ri/arrow-right-s-line";
import ArrowLeftSLine from "~icons/ri/arrow-left-s-line"; import ArrowLeftSLine from "@iconify-icons/ri/arrow-left-s-line";
const { const {
Close, Close,
@ -36,7 +34,6 @@ const {
buttonLeft, buttonLeft,
showModel, showModel,
translateX, translateX,
isFixedTag,
pureSetting, pureSetting,
activeIndex, activeIndex,
getTabStyle, getTabStyle,
@ -60,10 +57,6 @@ const contextmenuRef = ref();
const isShowArrow = ref(false); const isShowArrow = ref(false);
const topPath = getTopMenu()?.path; const topPath = getTopMenu()?.path;
const { VITE_HIDE_HOME } = import.meta.env; const { VITE_HIDE_HOME } = import.meta.env;
const fixedTags = [
...routerArrays,
...usePermissionStoreHook().flatteningRoutes.filter(v => v?.meta?.fixedTag)
];
const dynamicTagView = async () => { const dynamicTagView = async () => {
await nextTick(); await nextTick();
@ -233,13 +226,10 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
other?: boolean other?: boolean
): void => { ): void => {
if (other) { if (other) {
useMultiTagsStoreHook().handleTags( useMultiTagsStoreHook().handleTags("equal", [
"equal", VITE_HIDE_HOME === "false" ? routerArrays[0] : toRaw(getTopMenu()),
[ obj
VITE_HIDE_HOME === "false" ? fixedTags : toRaw(getTopMenu()), ]);
obj
].flat()
);
} else { } else {
useMultiTagsStoreHook().handleTags("splice", "", { useMultiTagsStoreHook().handleTags("splice", "", {
startIndex, startIndex,
@ -252,7 +242,7 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
if (tag === "other") { if (tag === "other") {
spliceRoute(1, 1, true); spliceRoute(1, 1, true);
} else if (tag === "left") { } else if (tag === "left") {
spliceRoute(fixedTags.length, valueIndex - 1, true); spliceRoute(1, valueIndex - 1);
} else if (tag === "right") { } else if (tag === "right") {
spliceRoute(valueIndex + 1, multiTags.value.length); spliceRoute(valueIndex + 1, multiTags.value.length);
} else { } else {
@ -329,11 +319,10 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
case 5: case 5:
// //
useMultiTagsStoreHook().handleTags("splice", "", { useMultiTagsStoreHook().handleTags("splice", "", {
startIndex: fixedTags.length, startIndex: 1,
length: multiTags.value.length length: multiTags.value.length
}); });
router.push(topPath); router.push(topPath);
// router.push(fixedTags[fixedTags.length - 1]?.path);
handleAliveRoute(route as ToRouteType); handleAliveRoute(route as ToRouteType);
break; break;
case 6: case 6:
@ -372,14 +361,10 @@ function showMenus(value: boolean) {
}); });
} }
function disabledMenus(value: boolean, fixedTag = false) { function disabledMenus(value: boolean) {
Array.of(1, 2, 3, 4, 5).forEach(v => { Array.of(1, 2, 3, 4, 5).forEach(v => {
tagsViews[v].disabled = value; tagsViews[v].disabled = value;
}); });
if (fixedTag) {
tagsViews[2].show = false;
tagsViews[2].disabled = true;
}
} }
/** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是顶级菜单,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */ /** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是顶级菜单,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */
@ -396,13 +381,6 @@ function showMenuModel(
} else { } else {
currentIndex = allRoute.findIndex(v => isEqual(v.query, query)); currentIndex = allRoute.findIndex(v => isEqual(v.query, query));
} }
function fixedTagDisabled() {
if (allRoute[currentIndex]?.meta?.fixedTag) {
Array.of(1, 2, 3, 4, 5).forEach(v => {
tagsViews[v].disabled = true;
});
}
}
showMenus(true); showMenus(true);
@ -421,7 +399,6 @@ function showMenuModel(
tagsViews[v].disabled = false; tagsViews[v].disabled = false;
}); });
tagsViews[2].disabled = true; tagsViews[2].disabled = true;
fixedTagDisabled();
} else if (currentIndex === 1 && routeLength === 2) { } else if (currentIndex === 1 && routeLength === 2) {
disabledMenus(false); disabledMenus(false);
// //
@ -429,7 +406,6 @@ function showMenuModel(
tagsViews[v].show = false; tagsViews[v].show = false;
tagsViews[v].disabled = true; tagsViews[v].disabled = true;
}); });
fixedTagDisabled();
} else if (routeLength - 1 === currentIndex && currentIndex !== 0) { } else if (routeLength - 1 === currentIndex && currentIndex !== 0) {
// //
tagsViews[3].show = false; tagsViews[3].show = false;
@ -437,31 +413,29 @@ function showMenuModel(
tagsViews[v].disabled = false; tagsViews[v].disabled = false;
}); });
tagsViews[3].disabled = true; tagsViews[3].disabled = true;
if (allRoute[currentIndex - 1]?.meta?.fixedTag) {
tagsViews[2].show = false;
tagsViews[2].disabled = true;
}
fixedTagDisabled();
} else if (currentIndex === 0 || currentPath === `/redirect${topPath}`) { } else if (currentIndex === 0 || currentPath === `/redirect${topPath}`) {
// //
disabledMenus(true); disabledMenus(true);
} else { } else {
disabledMenus(false, allRoute[currentIndex - 1]?.meta?.fixedTag); disabledMenus(false);
fixedTagDisabled();
} }
} }
function openMenu(tag, e) { function openMenu(tag, e) {
closeMenu(); closeMenu();
if (tag.path === topPath || tag?.meta?.fixedTag) { if (tag.path === topPath) {
// fixedTag //
showMenus(false); showMenus(false);
tagsViews[0].show = true; tagsViews[0].show = true;
} else if (route.path !== tag.path && route.name !== tag.name) { } else if (route.path !== tag.path && route.name !== tag.name) {
// //
tagsViews[0].show = false; tagsViews[0].show = false;
showMenuModel(tag.path, tag.query); showMenuModel(tag.path, tag.query);
} else if (multiTags.value.length === 2 && route.path !== tag.path) { } else if (
// eslint-disable-next-line no-dupe-else-if
multiTags.value.length === 2 &&
route.path !== tag.path
) {
showMenus(true); showMenus(true);
// //
tagsViews[4].show = false; tagsViews[4].show = false;
@ -509,7 +483,7 @@ function tagOnClick(item) {
} else { } else {
router.push({ path }); router.push({ path });
} }
emitter.emit("tagOnClick", item); // showMenuModel(item?.path, item?.query);
} }
onClickOutside(contextmenuRef, closeMenu, { onClickOutside(contextmenuRef, closeMenu, {
@ -566,7 +540,6 @@ onBeforeUnmount(() => {
<div <div
ref="scrollbarDom" ref="scrollbarDom"
class="scroll-container" class="scroll-container"
:class="showModel === 'chrome' && 'chrome-scroll-container'"
@wheel.prevent="handleWheel" @wheel.prevent="handleWheel"
> >
<div ref="tabDom" class="tab select-none" :style="getTabStyle"> <div ref="tabDom" class="tab select-none" :style="getTabStyle">
@ -574,57 +547,32 @@ onBeforeUnmount(() => {
v-for="(item, index) in multiTags" v-for="(item, index) in multiTags"
:ref="'dynamic' + index" :ref="'dynamic' + index"
:key="index" :key="index"
:class="[ :class="['scroll-item is-closable', linkIsActive(item)]"
'scroll-item is-closable',
linkIsActive(item),
showModel === 'chrome' && 'chrome-item',
isFixedTag(item) && 'fixed-tag'
]"
@contextmenu.prevent="openMenu(item, $event)" @contextmenu.prevent="openMenu(item, $event)"
@mouseenter.prevent="onMouseenter(index)" @mouseenter.prevent="onMouseenter(index)"
@mouseleave.prevent="onMouseleave(index)" @mouseleave.prevent="onMouseleave(index)"
@click="tagOnClick(item)" @click="tagOnClick(item)"
> >
<template v-if="showModel !== 'chrome'"> <span
<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 }}
{{ item.meta.title }} </span>
</span> <span
<span v-if="
v-if=" iconIsActive(item, index) ||
isFixedTag(item) (index === activeIndex && index !== 0)
? false "
: iconIsActive(item, index) || class="el-icon-close"
(index === activeIndex && index !== 0) @click.stop="deleteMenu(item)"
" >
class="el-icon-close" <IconifyIconOffline :icon="Close" />
@click.stop="deleteMenu(item)" </span>
> <span
<IconifyIconOffline :icon="Close" /> v-if="showModel !== 'card'"
</span> :ref="'schedule' + index"
<span :class="[scheduleIsActive(item)]"
v-if="showModel !== 'card'" />
:ref="'schedule' + index"
:class="[scheduleIsActive(item)]"
/>
</template>
<div v-else class="chrome-tab">
<div class="chrome-tab__bg">
<TagChrome />
</div>
<span class="tag-title">
{{ item.meta.title }}
</span>
<span
v-if="isFixedTag(item) ? false : index !== 0"
class="chrome-close-btn"
@click.stop="deleteMenu(item)"
>
<IconifyIconOffline :icon="Close" />
</span>
<span class="chrome-tab-divider" />
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@ import { useRoute } from "vue-router";
import { ref, unref, watch, onMounted, nextTick } from "vue"; import { ref, unref, watch, onMounted, nextTick } from "vue";
defineOptions({ defineOptions({
name: "LayFrame" name: "FrameView"
}); });
const props = defineProps<{ const props = defineProps<{

View File

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

View File

@ -1,22 +1,21 @@
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { getConfig } from "@/config"; import { getConfig } from "@/config";
import { emitter } from "@/utils/mitt"; import { emitter } from "@/utils/mitt";
import Avatar from "@/assets/user.jpg"; import userAvatar from "@/assets/user.jpg";
import { getTopMenu } from "@/router/utils"; import { getTopMenu } from "@/router/utils";
import { useFullscreen } from "@vueuse/core"; import { useFullscreen } from "@vueuse/core";
import { useGlobal } from "@pureadmin/utils";
import type { routeMetaType } from "../types"; import type { routeMetaType } from "../types";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import { router, remainingPaths } from "@/router"; import { router, remainingPaths } from "@/router";
import { computed, type CSSProperties } from "vue"; import { computed, type CSSProperties } from "vue";
import { useAppStoreHook } from "@/store/modules/app"; import { useAppStoreHook } from "@/store/modules/app";
import { useUserStoreHook } from "@/store/modules/user"; import { useUserStoreHook } from "@/store/modules/user";
import { useGlobal, isAllEmpty } from "@pureadmin/utils";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
import ExitFullscreen from "~icons/ri/fullscreen-exit-fill"; import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
import Fullscreen from "~icons/ri/fullscreen-fill"; import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
const errorInfo = const errorInfo = "当前路由配置不正确,请检查配置";
"The current routing configuration is incorrect, please check the configuration";
export function useNav() { export function useNav() {
const route = useRoute(); const route = useRoute();
@ -37,18 +36,9 @@ export function useNav() {
}; };
}); });
/** 头像(如果头像为空则使用 src/assets/user.jpg */ /** 用户名 */
const userAvatar = computed(() => {
return isAllEmpty(useUserStoreHook()?.avatar)
? Avatar
: useUserStoreHook()?.avatar;
});
/** 昵称(如果昵称为空则显示用户名) */
const username = computed(() => { const username = computed(() => {
return isAllEmpty(useUserStoreHook()?.nickname) return useUserStoreHook()?.username;
? useUserStoreHook()?.username
: useUserStoreHook()?.nickname;
}); });
const avatarsStyle = computed(() => { const avatarsStyle = computed(() => {

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