Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ffb20eb90d | ||
|
5b452a450e |
@ -1,24 +1,25 @@
|
||||
# 配置项文档:https://editorconfig.org(修改配置后重启编辑器)
|
||||
# 修改配置后重启编辑器
|
||||
# 配置项文档:https://editorconfig.org/
|
||||
|
||||
## 告知 EditorConfig 插件,当前即是根文件
|
||||
# 告知 EditorConfig 插件,当前即是根文件
|
||||
root = true
|
||||
|
||||
## 适用全部文件
|
||||
# 适用全部文件
|
||||
[*]
|
||||
### 设置字符集
|
||||
## 设置字符集
|
||||
charset = utf-8
|
||||
### 缩进风格 space | tab,建议 space
|
||||
## 缩进风格 space | tab,建议 space(会自动继承给 Prettier)
|
||||
indent_style = space
|
||||
### 缩进的空格数
|
||||
## 缩进的空格数(会自动继承给 Prettier)
|
||||
indent_size = 2
|
||||
### 换行符类型 lf | cr | crlf,一般都是设置为 lf
|
||||
## 换行符类型 lf | cr | crlf,一般都是设置为 lf
|
||||
end_of_line = lf
|
||||
### 是否在文件末尾插入空白行
|
||||
## 是否在文件末尾插入空白行
|
||||
insert_final_newline = true
|
||||
### 是否删除一行中的前后空格
|
||||
## 是否删除一行中的前后空格
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
## 适用 .md 文件
|
||||
# 适用 .md 文件
|
||||
[*.md]
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
|
5
.env
@ -1,7 +1,4 @@
|
||||
# 所有环境的环境变量(命名必须以 VITE_ 开头)
|
||||
# 所有环境自定义的环境变量(命名必须以 VITE_ 开头)
|
||||
|
||||
## 项目标题
|
||||
VITE_APP_TITLE = V3 Admin Vite
|
||||
|
||||
## 路由模式 hash 或 html5
|
||||
VITE_ROUTER_HISTORY = hash
|
||||
|
@ -1,7 +1,10 @@
|
||||
# 开发环境的环境变量(命名必须以 VITE_ 开头)
|
||||
# 开发环境自定义的环境变量(命名必须以 VITE_ 开头)
|
||||
|
||||
## 后端接口地址(如果解决跨域问题采用反向代理就只需写相对路径)
|
||||
VITE_BASE_URL = /api/v1
|
||||
## 后端接口公共路径(如果解决跨域问题采用反向代理就只需写公共路径)
|
||||
VITE_BASE_API = '/api/v1'
|
||||
|
||||
## 开发环境域名和静态资源公共路径(一般 / 或 ./ 都可以)
|
||||
VITE_PUBLIC_PATH = /
|
||||
## 路由模式 hash 或 html5
|
||||
VITE_ROUTER_HISTORY = 'hash'
|
||||
|
||||
## 开发环境地址前缀(一般 '/','./' 都可以)
|
||||
VITE_PUBLIC_PATH = '/'
|
||||
|
@ -1,7 +1,10 @@
|
||||
# 生产环境的环境变量(命名必须以 VITE_ 开头)
|
||||
# 生产环境自定义的环境变量(命名必须以 VITE_ 开头)
|
||||
|
||||
## 后端接口地址(如果解决跨域问题采用 CORS 就需要写绝对路径)
|
||||
VITE_BASE_URL = https://apifoxmock.com/m1/2930465-2145633-default/api/v1
|
||||
## 后端接口公共路径(如果解决跨域问题采用 CORS 就需要写全路径)
|
||||
VITE_BASE_API = 'https://mock.mengxuegu.com/mock/63218b5fb4c53348ed2bc212/api/v1'
|
||||
|
||||
## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/v3-admin-vite/ 域名下就需要填写 /v3-admin-vite/)
|
||||
VITE_PUBLIC_PATH = /v3-admin-vite/
|
||||
## 路由模式 hash 或 html5
|
||||
VITE_ROUTER_HISTORY = 'hash'
|
||||
|
||||
## 打包路径(就是网站前缀,例如部署到 https://un-pany.github.io/v3-admin-vite/ 域名下,就需要填写 /v3-admin-vite/)
|
||||
VITE_PUBLIC_PATH = '/v3-admin-vite/'
|
||||
|
13
.env.staging
@ -1,7 +1,10 @@
|
||||
# 预发布环境的环境变量(命名必须以 VITE_ 开头)
|
||||
# 预发布环境自定义的环境变量(命名必须以 VITE_ 开头)
|
||||
|
||||
## 后端接口地址(如果解决跨域问题采用 CORS 就需要写绝对路径)
|
||||
VITE_BASE_URL = https://apifoxmock.com/m1/2930465-2145633-default/api/v1
|
||||
## 后端接口公共路径(如果解决跨域问题采用 CORS 就需要写全路径)
|
||||
VITE_BASE_API = 'https://mock.mengxuegu.com/mock/63218b5fb4c53348ed2bc212/api/v1'
|
||||
|
||||
## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/ 域名下就需要填写 /)
|
||||
VITE_PUBLIC_PATH = /
|
||||
## 路由模式 hash 或 html5
|
||||
VITE_ROUTER_HISTORY = 'hash'
|
||||
|
||||
## 打包路径(就是网站前缀,例如部署到 https://un-pany.github.io/v3-admin-vite/ 域名下,就需要填写 /v3-admin-vite/)
|
||||
VITE_PUBLIC_PATH = '/v3-admin-vite/'
|
||||
|
8
.eslintignore
Normal file
@ -0,0 +1,8 @@
|
||||
# Eslint 会忽略的文件
|
||||
|
||||
.DS_Store
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.npmrc
|
75
.eslintrc.cjs
Normal file
@ -0,0 +1,75 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
es6: true
|
||||
},
|
||||
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,
|
||||
tsx: true
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
// TS
|
||||
"@typescript-eslint/no-unused-expressions": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"no-debugger": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_"
|
||||
}
|
||||
],
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_"
|
||||
}
|
||||
],
|
||||
// Vue
|
||||
"vue/no-v-html": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/require-explicit-emits": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/html-self-closing": [
|
||||
"error",
|
||||
{
|
||||
html: {
|
||||
void: "always",
|
||||
normal: "always",
|
||||
component: "always"
|
||||
},
|
||||
svg: "always",
|
||||
math: "always"
|
||||
}
|
||||
],
|
||||
// Prettier
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
endOfLine: "auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
8
.github/workflows/deploy.yml
vendored
@ -14,18 +14,18 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
- name: Setup Node.js 20.17.0
|
||||
uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: 22.12.0
|
||||
node-version: 20.17.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10.2.0
|
||||
version: 9.11.0
|
||||
|
||||
- name: Build
|
||||
run: pnpm install && pnpm build
|
||||
run: pnpm install && pnpm build:prod
|
||||
|
||||
- name: Deploy
|
||||
uses: JamesIves/github-pages-deploy-action@releases/v3
|
||||
|
6
.github/workflows/release.yml
vendored
@ -16,11 +16,9 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set node
|
||||
uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
registry-url: https://registry.npmjs.org/
|
||||
node-version: lts/*
|
||||
node-version: 20.17.0
|
||||
|
||||
- run: npx changelogithub
|
||||
env:
|
||||
|
31
.gitignore
vendored
@ -1,18 +1,35 @@
|
||||
# Common
|
||||
dist
|
||||
node_modules
|
||||
.eslintcache
|
||||
vite.config.*.timestamp*
|
||||
# Git 会忽略的文件
|
||||
|
||||
# MacOS
|
||||
.DS_Store
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
.eslintcache
|
||||
|
||||
# Local env files
|
||||
*.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Use the pnpm
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.json
|
||||
!.vscode/*.code-snippets
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Use the PNPM
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
@ -1,4 +1,2 @@
|
||||
# 全局 ts 类型检查(此操作会增加 git commit 时长)
|
||||
npx vue-tsc
|
||||
# 执行 lint-staged 中配置的任务
|
||||
npx vue-tsc --noEmit
|
||||
npx lint-staged
|
||||
|
3
.npmrc
@ -1,5 +1,8 @@
|
||||
# China mirror of npm
|
||||
registry = https://registry.npmmirror.com
|
||||
|
||||
# 通过该配置兜底解决组件没有类型提示的问题
|
||||
shamefully-hoist = true
|
||||
|
||||
# 安装依赖时锁定版本号
|
||||
save-exact = true
|
||||
|
8
.prettierignore
Normal file
@ -0,0 +1,8 @@
|
||||
# Prettier 会忽略的文件
|
||||
|
||||
.DS_Store
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.npmrc
|
3
.vscode/extensions.json
vendored
@ -1,8 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"vue.volar",
|
||||
"editorconfig.editorconfig",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"vue.volar",
|
||||
"antfu.unocss",
|
||||
"vitest.explorer",
|
||||
"wiensss.region-highlighter"
|
||||
|
9
.vscode/hook.code-snippets
vendored
@ -1,15 +1,16 @@
|
||||
{
|
||||
"Vue3 Composable 代码结构一键生成": {
|
||||
"prefix": "Vue3 Composable",
|
||||
"Vue3 Hook 代码结构一键生成": {
|
||||
"prefix": "Vue3 Hook",
|
||||
"body": [
|
||||
"import { ref } from \"vue\"\n",
|
||||
"const refName1 = ref<string>(\"这是一个响应式变量\")\n",
|
||||
"export function useName() {",
|
||||
"export function useHookName() {",
|
||||
"\tconst refName2 = ref<string>(\"这是一个响应式变量\")\n",
|
||||
"\tconst fnName = () => {}\n",
|
||||
"\treturn { refName1, refName2, fnName }",
|
||||
"}",
|
||||
"$1"
|
||||
],
|
||||
"description": "Vue3 Composable"
|
||||
"description": "Vue3 Hook"
|
||||
}
|
||||
}
|
||||
|
74
.vscode/settings.json
vendored
@ -1,53 +1,31 @@
|
||||
{
|
||||
// Use workspace TypeScript version
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
|
||||
// Disable the default formatter, use eslint instead
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
|
||||
// Auto fix
|
||||
"prettier.enable": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
|
||||
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "style/*", "severity": "off", "fixable": true },
|
||||
{ "rule": "format/*", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-indent", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-spacing", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-spaces", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-order", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-dangle", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-newline", "severity": "off", "fixable": true },
|
||||
{ "rule": "*quotes", "severity": "off", "fixable": true },
|
||||
{ "rule": "*semi", "severity": "off", "fixable": true }
|
||||
],
|
||||
|
||||
// Enable eslint for all supported languages
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml",
|
||||
"toml",
|
||||
"xml",
|
||||
"gql",
|
||||
"graphql",
|
||||
"astro",
|
||||
"svelte",
|
||||
"css",
|
||||
"less",
|
||||
"scss",
|
||||
"pcss",
|
||||
"postcss"
|
||||
]
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
|
4
.vscode/vue.code-snippets
vendored
@ -4,9 +4,7 @@
|
||||
"body": [
|
||||
"<script lang=\"ts\" setup></script>\n",
|
||||
"<template>",
|
||||
"\t<div class=\"app-container\">",
|
||||
"\t\t...",
|
||||
"\t</div>",
|
||||
"\t<div class=\"app-container\"></div>",
|
||||
"</template>\n",
|
||||
"<style lang=\"scss\" scoped></style>",
|
||||
"$1"
|
||||
|
277
README.md
@ -1,227 +1,160 @@
|
||||
<div align="center">
|
||||
<img alt="logo" width="120" height="120" src="./src/common/assets/images/layouts/logo.png">
|
||||
<img alt="V3 Admin Vite Logo" width="120" height="120" src="./src/assets/layouts/logo.png">
|
||||
<h1>V3 Admin Vite</h1>
|
||||
<span>English | <a href="./README.zh-CN.md">中文</a></span>
|
||||
</div>
|
||||
|
||||
[](https://github.com/un-pany/v3-admin-vite/releases)
|
||||
[](https://github.com/un-pany/v3-admin-vite/stargazers)
|
||||
[](https://gitee.com/un-pany/v3-admin-vite/stargazers)
|
||||
## ⚡ Introduction
|
||||
|
||||
<b>English | <a href="./README.zh-CN.md">中文</a></b>
|
||||
V3 Admin Vite is a free and open source middle and background management system basic solution, based on mainstream framework such as Vue3, TypeScript, Element Plus, Pinia and Vite
|
||||
|
||||
## Introduction
|
||||
- Vue Cli 5.x: [v3-admin](https://github.com/un-pany/v3-admin)
|
||||
- Electron desktop: [v3-electron-vite](https://github.com/un-pany/v3-electron-vite)
|
||||
|
||||
V3 Admin Vite is a well-crafted backend management system template, built with popular technologies such as Vue3, Vite, TypeScript, and Element Plus
|
||||
China repository: [Gitee](https://gitee.com/un-pany/v3-admin-vite)
|
||||
|
||||
## Notifications
|
||||
## 📚 Document
|
||||
|
||||
> [!NOTE]
|
||||
> Powered by love! All source code is free and open-source. If you find it helpful, feel free to give a star to support!
|
||||
- Chinese documentation: [link](https://juejin.cn/post/7089377403717287972)
|
||||
- Chinese getting started tutorial: [link](https://juejin.cn/column/7207659644487139387)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Welcome to experience the brand-new version 5.0, currently in the beta stage. It will be a masterpiece!
|
||||
## 📺 Online preview
|
||||
|
||||
> [!WARNING]
|
||||
> Version 4.x will no longer be maintained unless there are critical bugs! [Click to switch to the 4.x branch](https://github.com/un-pany/v3-admin-vite/tree/4.x)
|
||||
| Location | account | Link |
|
||||
| ------------ | ------------------- | ----------------------------------------------- |
|
||||
| github-pages | `admin` or `editor` | [link](https://un-pany.github.io/v3-admin-vite) |
|
||||
|
||||
> [!TIP]
|
||||
> Paid services are officially launched! If you don’t want to do it yourself but want to remove TS or other modules, try the lazy package! [Click to check it out](https://github.com/un-pany/v3-admin-vite/issues/225)
|
||||
## ❤️ Generate electricity with love
|
||||
|
||||
> [!TIP]
|
||||
> If you have mobile web app needs, try the new open-source template. [MobVue](https://github.com/un-pany/mobvue)
|
||||
- **Completely free**:But hopefully you order a star !!!
|
||||
- **Very concise**:No complicated encapsulation, no complicated type gymnastics, out of the box
|
||||
- **Detailed annotations**:Each configuration item is written with as detailed comments as possible
|
||||
- **Latest dependencies**: Regularly update all third-party dependencies to the latest version
|
||||
- **Very specification**: The code style is unified, the naming style is unified, and the comment style is unified
|
||||
|
||||
## Usage
|
||||
## Feature
|
||||
|
||||
<details>
|
||||
<summary>Recommended Environment</summary>
|
||||
- **Vue3**:The latest Vue3 composition API using Vue3 + script setup
|
||||
- **Element Plus**:Vue3 version of Element UI
|
||||
- **Pinia**: An alternative to Vuex in Vue3
|
||||
- **Vite**:Really fast
|
||||
- **Vue Router**:router
|
||||
- **TypeScript**:JavaScript With Syntax For Types
|
||||
- **PNPM**:Faster, disk space saving package management tool
|
||||
- **Scss**:Consistent with Element Plus
|
||||
- **CSS variable**:Mainly controls the layout and color of the item
|
||||
- **ESlint**:Code verification
|
||||
- **Prettier**: Code formatting
|
||||
- **Axios**: Promise based HTTP client (encapsulated)
|
||||
- **UnoCSS**: Real-time atomized CSS engine with high performance and flexibility
|
||||
- **Mobile Compatible**: The layout is compatible with mobile page resolution
|
||||
|
||||
<br>
|
||||
## Functions
|
||||
|
||||
- Latest version of `Visual Studio Code`
|
||||
- Install the recommended plugins in the `.vscode/extensions.json` file
|
||||
- `node` 20.x or 22+
|
||||
- `pnpm` 9.x or 10+
|
||||
- **User management**: Log in and out of the demo
|
||||
- **Authority management**: Page-level permissions (dynamic routing), button-level permissions (directive permissions, permission functions), and route navigation guards
|
||||
- **Multiple Environments**: Development, Staging, Production
|
||||
- **Multiple themes**: Normal, Dark, Dark Blue, three theme modes
|
||||
- **Multiple layouts**:Left, Top, Left Top, three layout modes
|
||||
- **Error page**: 403, 404
|
||||
- **Dashboard**: Display different Dashboard pages according to different users
|
||||
- **Other functions**:SVG, Dynamic Sidebar, Dynamic Breadcrumb Navigation, Tabbed Navigation, Screenfull, Adaptive Shrink Sidebar, Hook (Composables)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Local Development</summary>
|
||||
|
||||
<br>
|
||||
## 🚀 Development
|
||||
|
||||
```bash
|
||||
# Clone the project
|
||||
# configure
|
||||
1. installation of the recommended plugins in the .vscode directory
|
||||
2. node version 18.x or 20+
|
||||
3. pnpm version 8.x or latest
|
||||
|
||||
# clone
|
||||
git clone https://github.com/un-pany/v3-admin-vite.git
|
||||
|
||||
# Enter the project directory
|
||||
# enter the project directory
|
||||
cd v3-admin-vite
|
||||
|
||||
# Install dependencies
|
||||
# install dependencies
|
||||
pnpm i
|
||||
|
||||
# Start the development server
|
||||
# start the service
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Build</summary>
|
||||
|
||||
<br>
|
||||
## ✔️ Preview
|
||||
|
||||
```bash
|
||||
# Build for the staging environment
|
||||
pnpm build:staging
|
||||
# stage environment
|
||||
pnpm preview:stage
|
||||
|
||||
# Build for the production environment
|
||||
pnpm build
|
||||
# prod environment
|
||||
pnpm preview:prod
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Local Preview</summary>
|
||||
|
||||
<br>
|
||||
## 📦️ Multi-environment packaging
|
||||
|
||||
```bash
|
||||
# Execute the build command first to generate the dist directory, then run the preview command
|
||||
pnpm preview
|
||||
# build the stage environment
|
||||
pnpm build:stage
|
||||
|
||||
# build the prod environment
|
||||
pnpm build:prod
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Code Check</summary>
|
||||
|
||||
<br>
|
||||
## 🔧 Code inspection
|
||||
|
||||
```bash
|
||||
# Code linting and formatting
|
||||
# code formatting
|
||||
pnpm lint
|
||||
|
||||
# Unit tests
|
||||
# unit test
|
||||
pnpm test
|
||||
```
|
||||
|
||||
</details>
|
||||
## Git commit specification reference
|
||||
|
||||
<details>
|
||||
<summary>Commit Guidelines</summary>
|
||||
- `feat` add new functions
|
||||
- `fix` Fix issues/bugs
|
||||
- `perf` Optimize performance
|
||||
- `style` Change the code style without affecting the running result
|
||||
- `refactor` Re-factor code
|
||||
- `revert` Undo changes
|
||||
- `test` Test related, does not involve changes to business code
|
||||
- `docs` Documentation and Annotation
|
||||
- `chore` Updating dependencies/modifying scaffolding configuration, etc.
|
||||
- `workflow` Work flow Improvements
|
||||
- `ci` CICD
|
||||
- `types` Type definition
|
||||
- `wip` In development
|
||||
|
||||
<br>
|
||||
## Project preview
|
||||
|
||||
`feat` New feature
|
||||

|
||||

|
||||

|
||||
|
||||
`fix` Bug fix
|
||||
## 💕 Contributors
|
||||
|
||||
`perf` Performance improvement
|
||||
|
||||
`refactor` Code refactoring
|
||||
|
||||
`docs` Documentation and comments
|
||||
|
||||
`types` Type-related changes
|
||||
|
||||
`test` Unit tests related
|
||||
|
||||
`ci` Continuous integration, workflows
|
||||
|
||||
`revert` Revert changes
|
||||
|
||||
`chore` Chores (update dependencies, modify configurations, etc)
|
||||
|
||||
</details>
|
||||
|
||||
## Links
|
||||
|
||||
**Online Preview**: [github-pages](https://un-pany.github.io/v3-admin-vite)
|
||||
|
||||
**Chinese Documentation**: [link](https://juejin.cn/post/7089377403717287972)
|
||||
|
||||
**Zero to Hero Tutorial**: [link](https://juejin.cn/column/7207659644487139387)
|
||||
|
||||
**Mobile Web App**: [mobvue](https://github.com/un-pany/mobvue)
|
||||
|
||||
**Electron Desktop Version**: [v3-electron-vite](https://github.com/un-pany/v3-electron-vite)
|
||||
|
||||
**Chinese Repository**: [gitee](https://gitee.com/un-pany/v3-admin-vite)
|
||||
|
||||
**Optional Group**: [check how to join](https://github.com/un-pany/v3-admin-vite/issues/191)
|
||||
|
||||
**Donations**: [buy a coffee for the author](https://github.com/un-pany/v3-admin-vite/issues/69)
|
||||
|
||||
**Releases & Changelog**: [releases](https://github.com/un-pany/v3-admin-vite/releases)
|
||||
|
||||
## Features
|
||||
|
||||
**Simplified structure**: No complex encapsulation, no complicated type gymnastics, just enough to meet the needs
|
||||
|
||||
**Detailed comments**: Every configuration item comes with as detailed comments as possible
|
||||
|
||||
**Latest dependencies**: Keeps all third-party dependencies up to date
|
||||
|
||||
**Consistency**: Unified code style, naming conventions, and comment style
|
||||
|
||||
## Built-in Features
|
||||
|
||||
**User Management**: Login, logout demonstration
|
||||
|
||||
**Permission Management**: Page-level permissions (dynamic routing), button-level permissions (permission directives, permission functions), route guards
|
||||
|
||||
**Multiple Environments**: Development, staging, and production environments
|
||||
|
||||
**Multiple Themes**: Normal, dark, and deep blue themes
|
||||
|
||||
**Multiple Layouts**: Left-side, top, and hybrid layouts
|
||||
|
||||
**Homepage**: Different dashboard pages for different users
|
||||
|
||||
**Error Pages**: 403, 404
|
||||
|
||||
**Mobile Compatibility**: Layouts compatible with mobile screen resolutions
|
||||
|
||||
**Others**: SVG sprite sheet, dynamic sidebar, dynamic breadcrumbs, tab navigation, content zoom and fullscreen, composable functions
|
||||
|
||||
## Tech Stack
|
||||
|
||||
**Vue3**: Vue3 + script setup with the latest Vue3 Composition API
|
||||
|
||||
**Element Plus**: The Vue3 version of Element UI
|
||||
|
||||
**Pinia**: The legendary Vuex5
|
||||
|
||||
**Vite**: Really fast
|
||||
|
||||
**Vue Router**: The routing system
|
||||
|
||||
**TypeScript**: A superset of JavaScript
|
||||
|
||||
**pnpm**: A faster, disk-space-saving package manager
|
||||
|
||||
**Scss**: Consistent with Element Plus
|
||||
|
||||
**CSS Variables**: Primarily controls layout and color in the project
|
||||
|
||||
**ESLint**: Code linting and formatting
|
||||
|
||||
**Axios**: Sends network requests
|
||||
|
||||
**UnoCSS**: A high-performance, flexible atomic CSS engine
|
||||
|
||||
## Project Preview Image
|
||||
|
||||

|
||||
|
||||
## Contributors
|
||||
|
||||
A big thank you to all the contributors!
|
||||
Thanks to all the contributors!
|
||||
|
||||
<a href="https://github.com/un-pany/v3-admin-vite/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=un-pany/v3-admin-vite">
|
||||
<img src="https://contrib.rocks/image?repo=un-pany/v3-admin-vite" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
## 💕 Thanks star
|
||||
|
||||
[MIT](./LICENSE) License © 2022-PRESENT [pany](https://github.com/pany-ang)
|
||||
Small projects are not easy to get a star, if you like this project, welcome to support a star! This is the only motivation for the author to maintain it on an ongoing basis (whisper: it's free after all)
|
||||
|
||||
## ☕ Donate
|
||||
|
||||
[See how to donate](https://github.com/un-pany/v3-admin-vite/issues/69)
|
||||
|
||||
## Group
|
||||
|
||||
[See how to join a group chat](https://github.com/un-pany/v3-admin-vite/issues/191)
|
||||
|
||||
## 📄 License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
|
||||
Copyright (c) 2022-present [pany](https://github.com/pany-ang)
|
||||
|
265
README.zh-CN.md
@ -1,55 +1,73 @@
|
||||
<div align="center">
|
||||
<img alt="logo" width="120" height="120" src="./src/common/assets/images/layouts/logo.png">
|
||||
<img alt="V3 Admin Vite Logo" width="120" height="120" src="./src/assets/layouts/logo.png">
|
||||
<h1>V3 Admin Vite</h1>
|
||||
<span><a href="./README.md">English</a> | 中文</span>
|
||||
</div>
|
||||
|
||||
[](https://github.com/un-pany/v3-admin-vite/releases)
|
||||
[](https://github.com/un-pany/v3-admin-vite/stargazers)
|
||||
[](https://gitee.com/un-pany/v3-admin-vite/stargazers)
|
||||
## ⚡ 简介
|
||||
|
||||
<b><a href="./README.md">English</a> | 中文</b>
|
||||
V3 Admin Vite 是一个免费开源的中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus、Pinia 和 Vite 等主流技术
|
||||
|
||||
## 简介
|
||||
- Vue Cli 5.x 版: [v3-admin](https://github.com/un-pany/v3-admin)
|
||||
- Electron 桌面版: [v3-electron-vite](https://github.com/un-pany/v3-electron-vite)
|
||||
|
||||
V3 Admin Vite 是一个精心制作的后台管理系统模板,基于 Vue3、Vite、TypeScript、Element Plus 等主流技术
|
||||
国内仓库:[Gitee](https://gitee.com/un-pany/v3-admin-vite)
|
||||
|
||||
## 通知
|
||||
## 📚 文档
|
||||
|
||||
> [!NOTE]
|
||||
> 为爱发电!所有源码均免费开源,如果对你有帮助,欢迎点个 Star 支持一下!
|
||||
- 中文文档:[链接](https://juejin.cn/post/7089377403717287972)
|
||||
- 手摸手教程:[链接](https://juejin.cn/column/7207659644487139387)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 欢迎体验全新的 5.0 版本,目前正在 beta 阶段,它将是一次匠心之作!
|
||||
## 📺 在线预览
|
||||
|
||||
> [!WARNING]
|
||||
> 4.x 版本如果没有严重的 BUG 将不再维护
|
||||
| 位置 | 账号 | 链接 |
|
||||
| ------------ | --------------- | ----------------------------------------------- |
|
||||
| github-pages | admin 或 editor | [链接](https://un-pany.github.io/v3-admin-vite) |
|
||||
|
||||
> [!TIP]
|
||||
> 正式推出付费服务,如果不想自己动手,但想移除 TS 或其他模块?试试懒人套餐
|
||||
## ❤️ 用爱发电
|
||||
|
||||
> [!TIP]
|
||||
> 如果你有移动端 H5 需求,试试新的开源模板。[MobVue](https://github.com/un-pany/mobvue)
|
||||
- **完全免费**:但希望你点一个 star !!!
|
||||
- **非常简洁**:没有复杂的封装,没有复杂的类型体操,开箱即用
|
||||
- **详细的注释**:各个配置项都写有尽可能详细的注释
|
||||
- **最新的依赖**: 定期更新所有三方依赖至最新版
|
||||
- **有一点规整**: 代码风格统一,命名风格统一,注释风格统一
|
||||
|
||||
## 使用
|
||||
## 特性
|
||||
|
||||
<details>
|
||||
<summary>推荐环境</summary>
|
||||
- **Vue3**:采用 Vue3 + script setup 最新的 Vue3 组合式 API
|
||||
- **Element Plus**:Element UI 的 Vue3 版本
|
||||
- **Pinia**: 传说中的 Vuex5
|
||||
- **Vite**:真的很快
|
||||
- **Vue Router**:路由路由
|
||||
- **TypeScript**:JavaScript 语言的超集
|
||||
- **PNPM**:更快速的,节省磁盘空间的包管理工具
|
||||
- **Scss**:和 Element Plus 保持一致
|
||||
- **CSS 变量**:主要控制项目的布局和颜色
|
||||
- **ESlint**:代码校验
|
||||
- **Prettier**:代码格式化
|
||||
- **Axios**:发送网络请求(已封装好)
|
||||
- **UnoCSS**:具有高性能且极具灵活性的即时原子化 CSS 引擎
|
||||
- **兼容移动端**: 布局兼容移动端页面分辨率
|
||||
|
||||
<br>
|
||||
## 功能
|
||||
|
||||
- 新版 `Visual Studio Code`
|
||||
- 安装 `.vscode/extensions.json` 文件中推荐的插件
|
||||
- `node` 20.x 或 22+
|
||||
- `pnpm` 9.x 或 10+
|
||||
- **用户管理**:登录、登出演示
|
||||
- **权限管理**:页面级权限(动态路由)、按钮级权限(指令权限、权限函数)、路由守卫
|
||||
- **多环境**:开发环境(development)、预发布环境(staging)、正式环境(production)
|
||||
- **多主题**:普通、黑暗、深蓝, 三种主题模式
|
||||
- **多布局**:左侧、顶部、混合, 三种布局模式
|
||||
- **错误页面**: 403、404
|
||||
- **Dashboard**:根据不同用户显示不同的 Dashboard 页面
|
||||
- **其他内置功能**:SVG、动态侧边栏、动态面包屑、标签页快捷导航、Screenfull 全屏、自适应收缩侧边栏、Hook(Composables)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>本地开发</summary>
|
||||
|
||||
<br>
|
||||
## 🚀 开发
|
||||
|
||||
```bash
|
||||
# 配置
|
||||
1. 一键安装 .vscode 目录中推荐的插件
|
||||
2. node 版本 18.x 或 20+
|
||||
3. pnpm 版本 8.x 或最新版
|
||||
|
||||
# 克隆项目
|
||||
git clone https://github.com/un-pany/v3-admin-vite.git
|
||||
|
||||
@ -63,165 +81,80 @@ pnpm i
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>打包构建</summary>
|
||||
|
||||
<br>
|
||||
## ✔️ 预览
|
||||
|
||||
```bash
|
||||
# 打包构建预发布环境
|
||||
pnpm build:staging
|
||||
# 预览预发布环境
|
||||
pnpm preview:stage
|
||||
|
||||
# 打包构建生产环境
|
||||
pnpm build
|
||||
# 预览正式环境
|
||||
pnpm preview:prod
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>本地预览</summary>
|
||||
|
||||
<br>
|
||||
## 📦️ 多环境打包
|
||||
|
||||
```bash
|
||||
# 先执行打包构建命令生成 dist 目录后再执行以下预览命令
|
||||
pnpm preview
|
||||
# 构建预发布环境
|
||||
pnpm build:stage
|
||||
|
||||
# 构建正式环境
|
||||
pnpm build:prod
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>代码检查</summary>
|
||||
|
||||
<br>
|
||||
## 🔧 代码检查
|
||||
|
||||
```bash
|
||||
# 代码校验与格式化
|
||||
# 代码格式化
|
||||
pnpm lint
|
||||
|
||||
# 单元测试
|
||||
pnpm test
|
||||
```
|
||||
|
||||
</details>
|
||||
## Git 提交规范参考
|
||||
|
||||
<details>
|
||||
<summary>代码提交规范</summary>
|
||||
|
||||
<br>
|
||||
|
||||
`feat` 新功能
|
||||
|
||||
`fix` 修复错误
|
||||
|
||||
`perf` 性能优化
|
||||
|
||||
`refactor` 重构代码
|
||||
|
||||
`docs` 文档和注释
|
||||
|
||||
`types` 类型相关
|
||||
|
||||
`test` 单测相关
|
||||
|
||||
`ci` 持续集成、工作流
|
||||
|
||||
`revert` 撤销更改
|
||||
|
||||
`chore` 琐事(更新依赖、修改配置等)
|
||||
|
||||
</details>
|
||||
|
||||
## 链接
|
||||
|
||||
**在线预览**:[github-pages](https://un-pany.github.io/v3-admin-vite)
|
||||
|
||||
**中文文档**:[链接](https://juejin.cn/post/7445151895121543209)
|
||||
|
||||
**零基础教程**:[链接](https://juejin.cn/column/7207659644487139387)
|
||||
|
||||
**移动端 H5**:[mobvue](https://github.com/un-pany/mobvue)
|
||||
|
||||
**Electron 桌面版**:[v3-electron-vite](https://github.com/un-pany/v3-electron-vite)
|
||||
|
||||
**国内仓库**:[gitee](https://gitee.com/un-pany/v3-admin-vite)
|
||||
|
||||
**可有可无的群**:[查看进群方式](https://github.com/un-pany/v3-admin-vite/issues/191)
|
||||
|
||||
**捐赠**:[请作者喝咖啡](https://github.com/un-pany/v3-admin-vite/issues/69)
|
||||
|
||||
**发行版 & 更新日志**:[releases](https://github.com/un-pany/v3-admin-vite/releases)
|
||||
|
||||
## 特性
|
||||
|
||||
**结构精简**:没有复杂的封装,没有复杂的类型体操,刚好够用
|
||||
|
||||
**详细的注释**:各个配置项都写有尽可能详细的注释
|
||||
|
||||
**最新的依赖**:及时更新所有三方依赖至最新版
|
||||
|
||||
**有一点规范**:代码风格统一、命名风格统一、注释风格统一
|
||||
|
||||
## 内置功能
|
||||
|
||||
**用户管理**:登录、登出演示
|
||||
|
||||
**权限管理**:页面级权限(动态路由)、按钮级权限(权限指令、权限函数)、路由守卫
|
||||
|
||||
**多环境**:开发环境(development)、预发布环境(staging)、生产环境(production)
|
||||
|
||||
**多主题**:普通、黑暗、深蓝, 三种主题模式
|
||||
|
||||
**多布局**:左侧、顶部、混合, 三种布局模式
|
||||
|
||||
**首页**:根据不同用户显示不同的 Dashboard 页面
|
||||
|
||||
**错误页**:403、404
|
||||
|
||||
**兼容移动端**:布局兼容移动端页面分辨率
|
||||
|
||||
**其他**:SVG 雪碧图、动态侧边栏、动态面包屑、标签页快捷导航、内容区放大与全屏、组合式函数
|
||||
|
||||
## 技术栈
|
||||
|
||||
**Vue3**:采用 Vue3 + script setup 最新的 Vue3 组合式 API
|
||||
|
||||
**Element Plus**:Element UI 的 Vue3 版本
|
||||
|
||||
**Pinia**:传说中的 Vuex5
|
||||
|
||||
**Vite**:真的很快
|
||||
|
||||
**Vue Router**:路由路由
|
||||
|
||||
**TypeScript**:JavaScript 语言的超集
|
||||
|
||||
**pnpm**:更快速的,节省磁盘空间的包管理工具
|
||||
|
||||
**Scss**:和 Element Plus 保持一致
|
||||
|
||||
**CSS 变量**:主要控制项目的布局和颜色
|
||||
|
||||
**ESLint**:代码校验与格式化
|
||||
|
||||
**Axios**:发送网络请求(已封装好)
|
||||
|
||||
**UnoCSS**:具有高性能且极具灵活性的即时原子化 CSS 引擎
|
||||
- `feat` 增加新的业务功能
|
||||
- `fix` 修复业务问题/BUG
|
||||
- `perf` 优化性能
|
||||
- `style` 更改代码风格, 不影响运行结果
|
||||
- `refactor` 重构代码
|
||||
- `revert` 撤销更改
|
||||
- `test` 测试相关, 不涉及业务代码的更改
|
||||
- `docs` 文档和注释相关
|
||||
- `chore` 更新依赖/修改脚手架配置等琐事
|
||||
- `workflow` 工作流改进
|
||||
- `ci` 持续集成相关
|
||||
- `types` 类型定义文件更改
|
||||
- `wip` 开发中
|
||||
|
||||
## 项目预览图
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 贡献者
|
||||
## 💕 贡献者
|
||||
|
||||
在此感谢所有的贡献者!
|
||||
感谢所有的贡献者!
|
||||
|
||||
<a href="https://github.com/un-pany/v3-admin-vite/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=un-pany/v3-admin-vite">
|
||||
<img src="https://contrib.rocks/image?repo=un-pany/v3-admin-vite" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
## 💕 感谢 Star
|
||||
|
||||
[MIT](./LICENSE) License © 2022-PRESENT [pany](https://github.com/pany-ang)
|
||||
小项目获取 star 不易,如果你喜欢这个项目的话,欢迎支持一个 star!这是作者持续维护的唯一动力(小声:毕竟是免费的)
|
||||
|
||||
## ☕ Donate
|
||||
|
||||
[查看捐赠方式](https://github.com/un-pany/v3-admin-vite/issues/69)
|
||||
|
||||
## 可有可无的群
|
||||
|
||||
[查看进群方式](https://github.com/un-pany/v3-admin-vite/issues/191)
|
||||
|
||||
## 📄 License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
|
||||
Copyright (c) 2022-present [pany](https://github.com/pany-ang)
|
||||
|
16
changelogithub.config.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"types": {
|
||||
"feat": { "title": "Feat" },
|
||||
"fix": { "title": "Fix" },
|
||||
"perf": { "title": "Perf" },
|
||||
"style": { "title": "Style" },
|
||||
"refactor": { "title": "Refactor" },
|
||||
"revert": { "title": "Revert" },
|
||||
"test": { "title": "Test" },
|
||||
"docs": { "title": "Docs" },
|
||||
"chore": { "title": "Chore" },
|
||||
"workflow": { "title": "Workflow" },
|
||||
"ci": { "title": "CI" },
|
||||
"types": { "title": "Types" }
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import antfu from "@antfu/eslint-config"
|
||||
|
||||
// 更多自定义配置可查阅仓库:https://github.com/antfu/eslint-config
|
||||
export default antfu(
|
||||
{
|
||||
// 使用外部格式化程序格式化 css、html、markdown 等文件
|
||||
formatters: true,
|
||||
// 启用样式规则
|
||||
stylistic: {
|
||||
// 缩进级别
|
||||
indent: 2,
|
||||
// 引号风格 'single' | 'double'
|
||||
quotes: "double",
|
||||
// 是否启用分号
|
||||
semi: false
|
||||
},
|
||||
// 忽略文件
|
||||
ignores: []
|
||||
},
|
||||
{
|
||||
// 对所有文件都生效的规则
|
||||
rules: {
|
||||
// vue
|
||||
"vue/block-order": ["error", { order: ["script", "template", "style"] }],
|
||||
"vue/attributes-order": "off",
|
||||
// ts
|
||||
"ts/no-use-before-define": "off",
|
||||
// node
|
||||
"node/prefer-global/process": "off",
|
||||
// style
|
||||
"style/comma-dangle": ["error", "never"],
|
||||
"style/brace-style": ["error", "1tbs"],
|
||||
// regexp
|
||||
"regexp/no-unused-capturing-group": "off",
|
||||
// other
|
||||
"no-console": "off",
|
||||
"no-debugger": "off",
|
||||
"symbol-description": "off",
|
||||
"antfu/if-newline": "off",
|
||||
"unicorn/no-instanceof-builtins": "off"
|
||||
}
|
||||
}
|
||||
)
|
103
package.json
@ -1,24 +1,34 @@
|
||||
{
|
||||
"name": "v3-admin-vite",
|
||||
"version": "4.5.5",
|
||||
"description": "一个免费开源的中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus、Pinia 和 Vite 等主流技术",
|
||||
"author": {
|
||||
"name": "pany",
|
||||
"email": "939630029@qq.com",
|
||||
"url": "https://github.com/pany-ang"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/un-pany/v3-admin-vite.git"
|
||||
},
|
||||
"type": "module",
|
||||
"version": "5.0.0-beta.6",
|
||||
"description": "A crafted admin template, built with Vue3, Vite, TypeScript, Element Plus, and more",
|
||||
"author": "pany <939630029@qq.com> (https://github.com/pany-ang)",
|
||||
"repository": "https://github.com/un-pany/v3-admin-vite",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:staging": "vue-tsc && vite build --mode staging",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --fix",
|
||||
"build:stage": "vue-tsc --noEmit && vite build --mode staging",
|
||||
"build:prod": "vue-tsc --noEmit && vite build",
|
||||
"preview:stage": "pnpm build:stage && vite preview",
|
||||
"preview:prod": "pnpm build:prod && vite preview",
|
||||
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,tests,types}/**/*.{vue,js,jsx,ts,tsx}\" --fix",
|
||||
"lint:prettier": "prettier --write \"{src,tests,types}/**/*.{vue,js,jsx,ts,tsx,json,css,less,scss,html,md}\"",
|
||||
"lint": "pnpm lint:eslint && pnpm lint:prettier",
|
||||
"prepare": "husky",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "2.3.1",
|
||||
"axios": "1.8.4",
|
||||
"axios": "1.7.7",
|
||||
"dayjs": "1.11.13",
|
||||
"element-plus": "2.9.7",
|
||||
"element-plus": "2.8.8",
|
||||
"js-cookie": "3.0.5",
|
||||
"lodash-es": "4.17.21",
|
||||
"mitt": "3.0.1",
|
||||
@ -26,39 +36,68 @@
|
||||
"nprogress": "0.2.0",
|
||||
"path-browserify": "1.0.1",
|
||||
"path-to-regexp": "8.2.0",
|
||||
"pinia": "3.0.2",
|
||||
"pinia": "2.2.6",
|
||||
"screenfull": "6.0.2",
|
||||
"vue": "3.5.13",
|
||||
"vue-router": "4.5.0",
|
||||
"vxe-table": "4.6.25"
|
||||
"vue-router": "4.4.5",
|
||||
"vxe-table": "4.6.23",
|
||||
"vxe-table-plugin-element": "4.0.4",
|
||||
"xe-utils": "3.5.31"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "4.12.0",
|
||||
"@types/js-cookie": "3.0.6",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/node": "22.14.1",
|
||||
"@types/node": "22.9.0",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@types/path-browserify": "1.0.3",
|
||||
"@vitejs/plugin-vue": "5.2.3",
|
||||
"@vitejs/plugin-vue-jsx": "4.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.14.0",
|
||||
"@typescript-eslint/parser": "8.14.0",
|
||||
"@vitejs/plugin-vue": "5.2.0",
|
||||
"@vitejs/plugin-vue-jsx": "4.1.0",
|
||||
"@vue/eslint-config-prettier": "9.0.0",
|
||||
"@vue/eslint-config-typescript": "13.0.0",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
"eslint": "9.24.0",
|
||||
"eslint-plugin-format": "1.0.1",
|
||||
"happy-dom": "17.4.4",
|
||||
"husky": "9.1.7",
|
||||
"lint-staged": "15.5.1",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"eslint-plugin-vue": "9.31.0",
|
||||
"husky": "9.1.6",
|
||||
"jsdom": "25.0.1",
|
||||
"lint-staged": "15.2.10",
|
||||
"prettier": "3.3.3",
|
||||
"sass": "1.78.0",
|
||||
"typescript": "5.8.3",
|
||||
"unocss": "66.1.0-beta.12",
|
||||
"unplugin-auto-import": "19.1.2",
|
||||
"unplugin-svg-component": "0.12.1",
|
||||
"unplugin-vue-components": "28.5.0",
|
||||
"vite": "6.3.2",
|
||||
"typescript": "5.6.3",
|
||||
"unocss": "0.64.1",
|
||||
"vite": "5.4.11",
|
||||
"vite-plugin-svg-icons": "2.0.1",
|
||||
"vite-svg-loader": "5.1.0",
|
||||
"vitest": "3.1.1",
|
||||
"vue-tsc": "2.2.8"
|
||||
"vitest": "2.1.5",
|
||||
"vue-eslint-parser": "9.4.3",
|
||||
"vue-tsc": "2.1.10"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix"
|
||||
}
|
||||
"*.{vue,js,jsx,ts,tsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{css,less,scss,html,md}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"package.json": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"keywords": [
|
||||
"vue",
|
||||
"vue3",
|
||||
"admin",
|
||||
"vue-admin",
|
||||
"vue3-admin",
|
||||
"vite",
|
||||
"vite-admin",
|
||||
"element-plus",
|
||||
"element-plus-admin",
|
||||
"ts",
|
||||
"typescript"
|
||||
],
|
||||
"license": "MIT"
|
||||
}
|
||||
|
7090
pnpm-lock.yaml
generated
22
prettier.config.js
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 修改配置后重启编辑器
|
||||
* 配置项文档:https://prettier.io/docs/en/configuration.html
|
||||
* @type {import("prettier").Config}
|
||||
*/
|
||||
|
||||
export default {
|
||||
/** 每一行的宽度 */
|
||||
printWidth: 120,
|
||||
/** 在对象中的括号之间是否用空格来间隔 */
|
||||
bracketSpacing: true,
|
||||
/** 箭头函数的参数无论有几个,都要括号包裹 */
|
||||
arrowParens: "always",
|
||||
/** 换行符的使用 */
|
||||
endOfLine: "auto",
|
||||
/** 是否采用单引号 */
|
||||
singleQuote: false,
|
||||
/** 对象或者数组的最后一个元素后面不要加逗号 */
|
||||
trailingComma: "none",
|
||||
/** 是否加分号 */
|
||||
semi: false
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
/* 白屏阶段会执行的 CSS 加载动画 */
|
||||
/** 白屏阶段会执行的 CSS 加载动画 */
|
||||
|
||||
#app-loading {
|
||||
position: relative;
|
||||
|
@ -1,4 +1,5 @@
|
||||
// Tip: Simple judgments may not fully cover
|
||||
if (/MSIE\s|Trident\//.test(navigator.userAgent)) {
|
||||
document.body.innerHTML = "<strong>Sorry, this browser is currently not supported. We recommend using the latest version of a modern browser. For example, Chrome/Firefox/Edge.</strong>"
|
||||
if (/MSIE\s|Trident\//.test(window.navigator.userAgent)) {
|
||||
document.body.innerHTML =
|
||||
"<strong>Sorry, this browser is currently not supported. We recommend using the latest version of a modern browser. For example, Chrome/Firefox/Edge.</strong>"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 66 KiB |
25
src/App.vue
@ -1,20 +1,27 @@
|
||||
<script lang="ts" setup>
|
||||
import { useGreyAndColorWeakness } from "@@/composables/useGreyAndColorWeakness"
|
||||
import { usePany } from "@@/composables/usePany"
|
||||
import { useTheme } from "@@/composables/useTheme"
|
||||
import { useTheme } from "@/hooks/useTheme"
|
||||
import { useGreyAndColorWeakness } from "@/hooks/useGreyAndColorWeakness"
|
||||
import { ElNotification } from "element-plus"
|
||||
import zhCn from "element-plus/es/locale/lang/zh-cn" // Element Plus 中文包
|
||||
|
||||
const { initTheme } = useTheme()
|
||||
const { initGreyAndColorWeakness } = useGreyAndColorWeakness()
|
||||
const { initStarNotification, initStoreNotification } = usePany()
|
||||
|
||||
// 初始化主题
|
||||
/** 初始化主题 */
|
||||
initTheme()
|
||||
// 初始化灰色模式和色弱模式
|
||||
/** 初始化灰色模式和色弱模式 */
|
||||
initGreyAndColorWeakness()
|
||||
// 初始化通知
|
||||
initStarNotification()
|
||||
initStoreNotification()
|
||||
|
||||
/** 作者小心思 */
|
||||
ElNotification({
|
||||
title: "Hello",
|
||||
type: "success",
|
||||
dangerouslyUseHTMLString: true,
|
||||
message:
|
||||
"<a style='color: teal' target='_blank' href='https://github.com/un-pany/v3-admin-vite'>小项目获取 star 不易,如果你喜欢这个项目的话,欢迎点击这里支持一个 star !这是作者持续维护的唯一动力(小声:毕竟是免费的)</a>",
|
||||
duration: 0,
|
||||
position: "bottom-right"
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -19,20 +19,17 @@ const SELECT_RESPONSE_DATA = {
|
||||
message: "获取 Select 数据成功"
|
||||
}
|
||||
|
||||
const ERROR_MESSAGE = "接口发生错误"
|
||||
|
||||
/** 模拟接口 */
|
||||
export function getSelectDataApi() {
|
||||
return new Promise<typeof SELECT_RESPONSE_DATA>((resolve, reject) => {
|
||||
// 模拟接口响应时间 2s
|
||||
setTimeout(() => {
|
||||
// 模拟接口调用成功
|
||||
if (Math.random() < 0.8) {
|
||||
// 模拟接口调用成功
|
||||
resolve(SELECT_RESPONSE_DATA)
|
||||
} else {
|
||||
// 模拟接口调用出错
|
||||
reject(new Error(ERROR_MESSAGE))
|
||||
ElMessage.error(ERROR_MESSAGE)
|
||||
reject(new Error("接口发生错误"))
|
||||
}
|
||||
}, 2000)
|
||||
})
|
27
src/api/login/index.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { request } from "@/utils/service"
|
||||
import type * as Login from "./types/login"
|
||||
|
||||
/** 获取登录验证码 */
|
||||
export function getLoginCodeApi() {
|
||||
return request<Login.LoginCodeResponseData>({
|
||||
url: "login/code",
|
||||
method: "get"
|
||||
})
|
||||
}
|
||||
|
||||
/** 登录并返回 Token */
|
||||
export function loginApi(data: Login.LoginRequestData) {
|
||||
return request<Login.LoginResponseData>({
|
||||
url: "users/login",
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取用户详情 */
|
||||
export function getUserInfoApi() {
|
||||
return request<Login.UserInfoResponseData>({
|
||||
url: "users/info",
|
||||
method: "get"
|
||||
})
|
||||
}
|
@ -7,6 +7,8 @@ export interface LoginRequestData {
|
||||
code: string
|
||||
}
|
||||
|
||||
export type CaptchaResponseData = ApiResponseData<string>
|
||||
export type LoginCodeResponseData = ApiResponseData<string>
|
||||
|
||||
export type LoginResponseData = ApiResponseData<{ token: string }>
|
||||
|
||||
export type UserInfoResponseData = ApiResponseData<{ username: string; roles: string[] }>
|
37
src/api/table/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { request } from "@/utils/service"
|
||||
import type * as Table from "./types/table"
|
||||
|
||||
/** 增 */
|
||||
export function createTableDataApi(data: Table.CreateOrUpdateTableRequestData) {
|
||||
return request({
|
||||
url: "table",
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 删 */
|
||||
export function deleteTableDataApi(id: string) {
|
||||
return request({
|
||||
url: `table/${id}`,
|
||||
method: "delete"
|
||||
})
|
||||
}
|
||||
|
||||
/** 改 */
|
||||
export function updateTableDataApi(data: Table.CreateOrUpdateTableRequestData) {
|
||||
return request({
|
||||
url: "table",
|
||||
method: "put",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 查 */
|
||||
export function getTableDataApi(params: Table.TableRequestData) {
|
||||
return request<Table.TableResponseData>({
|
||||
url: "table",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
export interface CreateOrUpdateTableRequestData {
|
||||
id?: number
|
||||
id?: string
|
||||
username: string
|
||||
password?: string
|
||||
}
|
||||
@ -18,7 +18,7 @@ export interface TableRequestData {
|
||||
export interface TableData {
|
||||
createTime: string
|
||||
email: string
|
||||
id: number
|
||||
id: string
|
||||
phone: string
|
||||
roles: string
|
||||
status: boolean
|
BIN
src/assets/docs/preview1.png
Normal file
After Width: | Height: | Size: 101 KiB |
BIN
src/assets/docs/preview2.png
Normal file
After Width: | Height: | Size: 103 KiB |
BIN
src/assets/docs/preview3.png
Normal file
After Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
BIN
src/assets/layouts/logo-text-1.png
Normal file
After Width: | Height: | Size: 373 KiB |
BIN
src/assets/layouts/logo-text-2.png
Normal file
After Width: | Height: | Size: 407 KiB |
BIN
src/assets/layouts/logo.png
Normal file
After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
@ -1,37 +0,0 @@
|
||||
import type * as Tables from "./type"
|
||||
import { request } from "@/http/axios"
|
||||
|
||||
/** 增 */
|
||||
export function createTableDataApi(data: Tables.CreateOrUpdateTableRequestData) {
|
||||
return request({
|
||||
url: "tables",
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 删 */
|
||||
export function deleteTableDataApi(id: number) {
|
||||
return request({
|
||||
url: `tables/${id}`,
|
||||
method: "delete"
|
||||
})
|
||||
}
|
||||
|
||||
/** 改 */
|
||||
export function updateTableDataApi(data: Tables.CreateOrUpdateTableRequestData) {
|
||||
return request({
|
||||
url: "tables",
|
||||
method: "put",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 查 */
|
||||
export function getTableDataApi(params: Tables.TableRequestData) {
|
||||
return request<Tables.TableResponseData>({
|
||||
url: "tables",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import type * as Users from "./type"
|
||||
import { request } from "@/http/axios"
|
||||
|
||||
/** 获取当前登录用户详情 */
|
||||
export function getCurrentUserApi() {
|
||||
return request<Users.CurrentUserResponseData>({
|
||||
url: "users/me",
|
||||
method: "get"
|
||||
})
|
||||
}
|
@ -1 +0,0 @@
|
||||
export type CurrentUserResponseData = ApiResponseData<{ username: string, roles: string[] }>
|
@ -1,11 +0,0 @@
|
||||
## 目录说明
|
||||
|
||||
- `common/assets/icons/preserve-color` 目录下存放带颜色的 svg icon
|
||||
|
||||
- `common/assets/icons` 目录存放的 svg icon 会被插件重写 `fill` 和 `stroke` 属性,使得图片自带的颜色丢失,从而继承父元素的颜色
|
||||
|
||||
## 使用说明
|
||||
|
||||
`common/assets/icons/preserve-color` 目录下需要添加 `preserve-color/` 前缀,像这样: `<SvgIcon name="preserve-color/name" />`
|
||||
|
||||
`common/assets/icons` 目录下则不需要,像这样: `<SvgIcon name="name" />`
|
Before Width: | Height: | Size: 492 KiB |
Before Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 8.4 KiB |
@ -1,8 +0,0 @@
|
||||
export interface NotifyItem {
|
||||
avatar?: string
|
||||
title: string
|
||||
datetime?: string
|
||||
description?: string
|
||||
status?: "primary" | "success" | "info" | "warning" | "danger"
|
||||
extra?: string
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useTheme } from "@@/composables/useTheme"
|
||||
import { MagicStick } from "@element-plus/icons-vue"
|
||||
|
||||
const { themeList, activeThemeName, setTheme } = useTheme()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dropdown trigger="click">
|
||||
<div>
|
||||
<el-tooltip effect="dark" content="主题模式" placement="bottom">
|
||||
<el-icon :size="20">
|
||||
<MagicStick />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="(theme, index) in themeList"
|
||||
:key="index"
|
||||
:disabled="activeThemeName === theme.name"
|
||||
@click="(e: MouseEvent) => setTheme(e, theme.name)"
|
||||
>
|
||||
<span>{{ theme.title }}</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
@ -1,42 +0,0 @@
|
||||
function initStarNotification() {
|
||||
setTimeout(() => {
|
||||
ElNotification({
|
||||
title: "为爱发电!",
|
||||
type: "success",
|
||||
message: h(
|
||||
"div",
|
||||
null,
|
||||
[
|
||||
h("div", null, "所有源码均免费开源,如果对你有帮助,欢迎点个 Star 支持一下!"),
|
||||
h("a", { style: "color: teal", target: "_blank", href: "https://github.com/un-pany/v3-admin-vite" }, "点击传送")
|
||||
]
|
||||
),
|
||||
duration: 0,
|
||||
position: "bottom-right"
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function initStoreNotification() {
|
||||
setTimeout(() => {
|
||||
ElNotification({
|
||||
title: "懒人服务?",
|
||||
type: "warning",
|
||||
message: h(
|
||||
"div",
|
||||
null,
|
||||
[
|
||||
h("div", null, "不想自己动手,但想移除 TS 或其他模块?也有懒人套餐!"),
|
||||
h("a", { style: "color: teal", target: "_blank", href: "https://github.com/un-pany/v3-admin-vite/issues/225" }, "点击查看")
|
||||
]
|
||||
),
|
||||
duration: 0,
|
||||
position: "bottom-right"
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
|
||||
/** 作者的小心思 */
|
||||
export function usePany() {
|
||||
return { initStarNotification, initStoreNotification }
|
||||
}
|
16
src/common/utils/cache/cookies.ts
vendored
@ -1,16 +0,0 @@
|
||||
// 统一处理 Cookie
|
||||
|
||||
import { CacheKey } from "@@/constants/cache-key"
|
||||
import Cookies from "js-cookie"
|
||||
|
||||
export function getToken() {
|
||||
return Cookies.get(CacheKey.TOKEN)
|
||||
}
|
||||
|
||||
export function setToken(token: string) {
|
||||
Cookies.set(CacheKey.TOKEN, token)
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
Cookies.remove(CacheKey.TOKEN)
|
||||
}
|
60
src/common/utils/cache/local-storage.ts
vendored
@ -1,60 +0,0 @@
|
||||
// 统一处理 localStorage
|
||||
|
||||
import type { LayoutsConfig } from "@/layouts/config"
|
||||
import type { TagView } from "@/pinia/stores/tags-view"
|
||||
import type { ThemeName } from "@@/composables/useTheme"
|
||||
import type { SidebarClosed, SidebarOpened } from "@@/constants/app-key"
|
||||
import { CacheKey } from "@@/constants/cache-key"
|
||||
|
||||
// #region 系统布局配置
|
||||
export function getLayoutsConfig() {
|
||||
const json = localStorage.getItem(CacheKey.CONFIG_LAYOUT)
|
||||
return json ? (JSON.parse(json) as LayoutsConfig) : null
|
||||
}
|
||||
export function setLayoutsConfig(settings: LayoutsConfig) {
|
||||
localStorage.setItem(CacheKey.CONFIG_LAYOUT, JSON.stringify(settings))
|
||||
}
|
||||
export function removeLayoutsConfig() {
|
||||
localStorage.removeItem(CacheKey.CONFIG_LAYOUT)
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region 侧边栏状态
|
||||
export function getSidebarStatus() {
|
||||
return localStorage.getItem(CacheKey.SIDEBAR_STATUS)
|
||||
}
|
||||
export function setSidebarStatus(sidebarStatus: SidebarOpened | SidebarClosed) {
|
||||
localStorage.setItem(CacheKey.SIDEBAR_STATUS, sidebarStatus)
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region 正在应用的主题名称
|
||||
export function getActiveThemeName() {
|
||||
return localStorage.getItem(CacheKey.ACTIVE_THEME_NAME) as ThemeName | null
|
||||
}
|
||||
export function setActiveThemeName(themeName: ThemeName) {
|
||||
localStorage.setItem(CacheKey.ACTIVE_THEME_NAME, themeName)
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region 标签栏
|
||||
export function getVisitedViews() {
|
||||
const json = localStorage.getItem(CacheKey.VISITED_VIEWS)
|
||||
return JSON.parse(json ?? "[]") as TagView[]
|
||||
}
|
||||
export function setVisitedViews(views: TagView[]) {
|
||||
views.forEach((view) => {
|
||||
// 删除不必要的属性,防止 JSON.stringify 处理到循环引用
|
||||
delete view.matched
|
||||
delete view.redirectedFrom
|
||||
})
|
||||
localStorage.setItem(CacheKey.VISITED_VIEWS, JSON.stringify(views))
|
||||
}
|
||||
export function getCachedViews() {
|
||||
const json = localStorage.getItem(CacheKey.CACHED_VIEWS)
|
||||
return JSON.parse(json ?? "[]") as string[]
|
||||
}
|
||||
export function setCachedViews(views: string[]) {
|
||||
localStorage.setItem(CacheKey.CACHED_VIEWS, JSON.stringify(views))
|
||||
}
|
||||
// #endregion
|
@ -1,13 +0,0 @@
|
||||
import { useUserStore } from "@/pinia/stores/user"
|
||||
import { isArray } from "@@/utils/validate"
|
||||
|
||||
/** 全局权限判断函数,和权限指令 v-permission 功能类似 */
|
||||
export function checkPermission(permissionRoles: string[]): boolean {
|
||||
if (isArray(permissionRoles) && permissionRoles.length > 0) {
|
||||
const { roles } = useUserStore()
|
||||
return roles.some(role => permissionRoles.includes(role))
|
||||
} else {
|
||||
console.error("参数必须是一个数组且长度大于 0,参考:checkPermission(['admin', 'editor'])")
|
||||
return false
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
/** 判断是否为数组 */
|
||||
export function isArray<T>(arg: T) {
|
||||
return Array.isArray ? Array.isArray(arg) : Object.prototype.toString.call(arg) === "[object Array]"
|
||||
}
|
||||
|
||||
/** 判断是否为字符串 */
|
||||
export function isString(str: unknown) {
|
||||
return typeof str === "string" || str instanceof String
|
||||
}
|
||||
|
||||
/** 判断是否为外链 */
|
||||
export function isExternal(path: string) {
|
||||
const reg = /^(https?:|mailto:|tel:)/
|
||||
return reg.test(path)
|
||||
}
|
@ -1,16 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import type { NotifyItem } from "./type"
|
||||
import { type ListItem } from "./data"
|
||||
|
||||
interface Props {
|
||||
data: NotifyItem[]
|
||||
list: ListItem[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-empty v-if="props.data.length === 0" />
|
||||
<el-card v-else v-for="(item, index) in props.data" :key="index" shadow="never" class="card-container">
|
||||
<el-empty v-if="props.list.length === 0" />
|
||||
<el-card v-else v-for="(item, index) in props.list" :key="index" shadow="never" class="card-container">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
@ -18,12 +18,10 @@ const props = defineProps<Props>()
|
||||
<span class="card-title">{{ item.title }}</span>
|
||||
<el-tag v-if="item.extra" :type="item.status" effect="plain" size="small">{{ item.extra }}</el-tag>
|
||||
</span>
|
||||
<div class="card-time">
|
||||
{{ item.datetime }}
|
||||
</div>
|
||||
<div class="card-time">{{ item.datetime }}</div>
|
||||
</div>
|
||||
<div v-if="item.avatar" class="card-avatar">
|
||||
<img :src="item.avatar" width="34">
|
||||
<img :src="item.avatar" width="34" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,21 +1,29 @@
|
||||
import type { NotifyItem } from "./type"
|
||||
export interface ListItem {
|
||||
avatar?: string
|
||||
title: string
|
||||
datetime?: string
|
||||
description?: string
|
||||
status?: "primary" | "success" | "info" | "warning" | "danger"
|
||||
extra?: string
|
||||
}
|
||||
|
||||
export const notifyData: NotifyItem[] = [
|
||||
export const notifyData: ListItem[] = [
|
||||
{
|
||||
avatar: "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
|
||||
title: "V3 Admin Vite 上线啦",
|
||||
datetime: "两年前",
|
||||
description: "一个免费开源的中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus、Pinia 和 Vite 等主流技术"
|
||||
datetime: "一年前",
|
||||
description:
|
||||
"一个免费开源的中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus、Pinia 和 Vite 等主流技术"
|
||||
},
|
||||
{
|
||||
avatar: "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
|
||||
title: "V3 Admin 上线啦",
|
||||
datetime: "三年前",
|
||||
datetime: "两年前",
|
||||
description: "一个中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus 和 Pinia"
|
||||
}
|
||||
]
|
||||
|
||||
export const messageData: NotifyItem[] = [
|
||||
export const messageData: ListItem[] = [
|
||||
{
|
||||
avatar: "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
|
||||
title: "来自楚门的世界",
|
||||
@ -36,7 +44,7 @@ export const messageData: NotifyItem[] = [
|
||||
}
|
||||
]
|
||||
|
||||
export const todoData: NotifyItem[] = [
|
||||
export const todoData: ListItem[] = [
|
||||
{
|
||||
title: "任务名称",
|
||||
description: "这家伙很懒,什么都没留下",
|
@ -1,29 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import type { NotifyItem } from "./type"
|
||||
import { ref, computed } from "vue"
|
||||
import { ElMessage } from "element-plus"
|
||||
import { Bell } from "@element-plus/icons-vue"
|
||||
import { messageData, notifyData, todoData } from "./data"
|
||||
import List from "./List.vue"
|
||||
import NotifyList from "./NotifyList.vue"
|
||||
import { type ListItem, notifyData, messageData, todoData } from "./data"
|
||||
|
||||
type TabName = "通知" | "消息" | "待办"
|
||||
|
||||
interface DataItem {
|
||||
name: TabName
|
||||
type: "primary" | "success" | "warning" | "danger" | "info"
|
||||
list: NotifyItem[]
|
||||
list: ListItem[]
|
||||
}
|
||||
|
||||
/** 角标当前值 */
|
||||
const badgeValue = computed(() => data.value.reduce((sum, item) => sum + item.list.length, 0))
|
||||
|
||||
const badgeValue = computed(() => {
|
||||
return data.value.reduce((sum, item) => sum + item.list.length, 0)
|
||||
})
|
||||
/** 角标最大值 */
|
||||
const badgeMax = 99
|
||||
|
||||
/** 面板宽度 */
|
||||
const popoverWidth = 350
|
||||
|
||||
/** 当前 Tab */
|
||||
const activeName = ref<TabName>("通知")
|
||||
|
||||
/** 所有数据 */
|
||||
const data = ref<DataItem[]>([
|
||||
// 通知数据
|
||||
@ -46,7 +45,7 @@ const data = ref<DataItem[]>([
|
||||
}
|
||||
])
|
||||
|
||||
function handleHistory() {
|
||||
const handleHistory = () => {
|
||||
ElMessage.success(`跳转到${activeName.value}历史页面`)
|
||||
}
|
||||
</script>
|
||||
@ -65,20 +64,18 @@ function handleHistory() {
|
||||
</template>
|
||||
<template #default>
|
||||
<el-tabs v-model="activeName" class="demo-tabs" stretch>
|
||||
<el-tab-pane v-for="(item, index) in data" :key="index" :name="item.name">
|
||||
<el-tab-pane v-for="(item, index) in data" :name="item.name" :key="index">
|
||||
<template #label>
|
||||
{{ item.name }}
|
||||
<el-badge :value="item.list.length" :max="badgeMax" :type="item.type" />
|
||||
</template>
|
||||
<el-scrollbar height="400px">
|
||||
<List :data="item.list" />
|
||||
<NotifyList :list="item.list" />
|
||||
</el-scrollbar>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="notify-history">
|
||||
<el-button link @click="handleHistory">
|
||||
查看{{ activeName }}历史
|
||||
</el-button>
|
||||
<el-button link @click="handleHistory">查看{{ activeName }}历史</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
@ -86,6 +83,10 @@ function handleHistory() {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notify {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.notify-history {
|
||||
text-align: center;
|
||||
padding-top: 12px;
|
@ -1,4 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watchEffect } from "vue"
|
||||
import { ElMessage } from "element-plus"
|
||||
import screenfull from "screenfull"
|
||||
|
||||
interface Props {
|
||||
@ -20,52 +22,44 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
})
|
||||
|
||||
const CONTENT_LARGE = "content-large"
|
||||
|
||||
const CONTENT_FULL = "content-full"
|
||||
|
||||
const classList = document.body.classList
|
||||
|
||||
// #region 全屏
|
||||
//#region 全屏
|
||||
const isEnabled = screenfull.isEnabled
|
||||
const isFullscreen = ref<boolean>(false)
|
||||
const fullscreenTips = computed(() => (isFullscreen.value ? props.exitTips : props.openTips))
|
||||
const fullscreenSvgName = computed(() => (isFullscreen.value ? "fullscreen-exit" : "fullscreen"))
|
||||
|
||||
function handleFullscreenClick() {
|
||||
const handleFullscreenClick = () => {
|
||||
const dom = document.querySelector(props.element) || undefined
|
||||
isEnabled ? screenfull.toggle(dom) : ElMessage.warning("您的浏览器无法工作")
|
||||
}
|
||||
|
||||
function handleFullscreenChange() {
|
||||
const handleFullscreenChange = () => {
|
||||
isFullscreen.value = screenfull.isFullscreen
|
||||
// 退出全屏时清除相关的 class
|
||||
isFullscreen.value || classList.remove(CONTENT_LARGE, CONTENT_FULL)
|
||||
}
|
||||
|
||||
watchEffect((onCleanup) => {
|
||||
if (isEnabled) {
|
||||
// 挂载组件时自动执行
|
||||
screenfull.on("change", handleFullscreenChange)
|
||||
// 卸载组件时自动执行
|
||||
onCleanup(() => {
|
||||
screenfull.off("change", handleFullscreenChange)
|
||||
})
|
||||
onCleanup(() => screenfull.off("change", handleFullscreenChange))
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
//#endregion
|
||||
|
||||
// #region 内容区
|
||||
//#region 内容区
|
||||
const isContentLarge = ref<boolean>(false)
|
||||
const contentLargeTips = computed(() => (isContentLarge.value ? "内容区复原" : "内容区放大"))
|
||||
const contentLargeSvgName = computed(() => (isContentLarge.value ? "fullscreen-exit" : "fullscreen"))
|
||||
|
||||
function handleContentLargeClick() {
|
||||
const handleContentLargeClick = () => {
|
||||
isContentLarge.value = !isContentLarge.value
|
||||
// 内容区放大时,将不需要的组件隐藏
|
||||
classList.toggle(CONTENT_LARGE, isContentLarge.value)
|
||||
}
|
||||
|
||||
function handleContentFullClick() {
|
||||
const handleContentFullClick = () => {
|
||||
// 取消内容区放大
|
||||
isContentLarge.value && handleContentLargeClick()
|
||||
// 内容区全屏时,将不需要的组件隐藏
|
||||
@ -73,28 +67,24 @@ function handleContentFullClick() {
|
||||
// 开启全屏
|
||||
handleFullscreenClick()
|
||||
}
|
||||
// #endregion
|
||||
//#endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 全屏 -->
|
||||
<el-tooltip v-if="!props.content" effect="dark" :content="fullscreenTips" placement="bottom">
|
||||
<SvgIcon :name="fullscreenSvgName" @click="handleFullscreenClick" class="svg-icon" />
|
||||
<el-tooltip v-if="!content" effect="dark" :content="fullscreenTips" placement="bottom">
|
||||
<SvgIcon :name="fullscreenSvgName" @click="handleFullscreenClick" />
|
||||
</el-tooltip>
|
||||
<!-- 内容区 -->
|
||||
<el-dropdown v-else :disabled="isFullscreen">
|
||||
<SvgIcon :name="contentLargeSvgName" class="svg-icon" />
|
||||
<SvgIcon :name="contentLargeSvgName" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<!-- 内容区放大 -->
|
||||
<el-dropdown-item @click="handleContentLargeClick">
|
||||
{{ contentLargeTips }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleContentLargeClick">{{ contentLargeTips }}</el-dropdown-item>
|
||||
<!-- 内容区全屏 -->
|
||||
<el-dropdown-item @click="handleContentFullClick">
|
||||
内容区全屏
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleContentFullClick">内容区全屏</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { useDevice } from "@@/composables/useDevice"
|
||||
import { useDevice } from "@/hooks/useDevice"
|
||||
|
||||
interface Props {
|
||||
total: number
|
||||
@ -14,16 +14,16 @@ const { isMobile } = useDevice()
|
||||
<div class="search-footer">
|
||||
<template v-if="!isMobile">
|
||||
<span class="search-footer-item">
|
||||
<SvgIcon name="keyboard-enter" class="svg-icon" />
|
||||
<SvgIcon name="keyboard-enter" />
|
||||
<span>确认</span>
|
||||
</span>
|
||||
<span class="search-footer-item">
|
||||
<SvgIcon name="keyboard-up" class="svg-icon" />
|
||||
<SvgIcon name="keyboard-down" class="svg-icon" />
|
||||
<SvgIcon name="keyboard-up" />
|
||||
<SvgIcon name="keyboard-down" />
|
||||
<span>切换</span>
|
||||
</span>
|
||||
<span class="search-footer-item">
|
||||
<SvgIcon name="keyboard-esc" class="svg-icon" />
|
||||
<SvgIcon name="keyboard-esc" />
|
||||
<span>关闭</span>
|
||||
</span>
|
||||
</template>
|
@ -1,12 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ElScrollbar } from "element-plus"
|
||||
import type { RouteRecordNameGeneric, RouteRecordRaw } from "vue-router"
|
||||
import { usePermissionStore } from "@/pinia/stores/permission"
|
||||
import { useDevice } from "@@/composables/useDevice"
|
||||
import { isExternal } from "@@/utils/validate"
|
||||
import { computed, ref, shallowRef } from "vue"
|
||||
import { type RouteRecordName, type RouteRecordRaw, useRouter } from "vue-router"
|
||||
import { usePermissionStore } from "@/store/modules/permission"
|
||||
import SearchResult from "./SearchResult.vue"
|
||||
import SearchFooter from "./SearchFooter.vue"
|
||||
import { ElMessage, ElScrollbar } from "element-plus"
|
||||
import { cloneDeep, debounce } from "lodash-es"
|
||||
import Footer from "./Footer.vue"
|
||||
import Result from "./Result.vue"
|
||||
import { useDevice } from "@/hooks/useDevice"
|
||||
import { isExternal } from "@/utils/validate"
|
||||
|
||||
/** 控制 modal 显隐 */
|
||||
const modelValue = defineModel<boolean>({ required: true })
|
||||
@ -16,31 +17,32 @@ const { isMobile } = useDevice()
|
||||
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
const scrollbarRef = ref<InstanceType<typeof ElScrollbar> | null>(null)
|
||||
const resultRef = ref<InstanceType<typeof Result> | null>(null)
|
||||
const searchResultRef = ref<InstanceType<typeof SearchResult> | null>(null)
|
||||
|
||||
const keyword = ref<string>("")
|
||||
const result = shallowRef<RouteRecordRaw[]>([])
|
||||
const activeRouteName = ref<RouteRecordNameGeneric | undefined>(undefined)
|
||||
const resultList = shallowRef<RouteRecordRaw[]>([])
|
||||
const activeRouteName = ref<RouteRecordName | undefined>(undefined)
|
||||
/** 是否按下了上键或下键(用于解决和 mouseenter 事件的冲突) */
|
||||
const isPressUpOrDown = ref<boolean>(false)
|
||||
|
||||
/** 控制搜索对话框宽度 */
|
||||
const modalWidth = computed(() => (isMobile.value ? "80vw" : "40vw"))
|
||||
/** 树形菜单 */
|
||||
const menus = computed(() => cloneDeep(usePermissionStore().routes))
|
||||
const menusData = computed(() => cloneDeep(usePermissionStore().routes))
|
||||
|
||||
/** 搜索(防抖) */
|
||||
const handleSearch = debounce(() => {
|
||||
const flatMenus = flatTree(menus.value)
|
||||
const _keywords = keyword.value.toLocaleLowerCase().trim()
|
||||
result.value = flatMenus.filter(menu => keyword.value ? menu.meta?.title?.toLocaleLowerCase().includes(_keywords) : false)
|
||||
const flatMenusData = flatTree(menusData.value)
|
||||
resultList.value = flatMenusData.filter((menu) =>
|
||||
keyword.value ? menu.meta?.title?.toLocaleLowerCase().includes(keyword.value.toLocaleLowerCase().trim()) : false
|
||||
)
|
||||
// 默认选中搜索结果的第一项
|
||||
const length = result.value?.length
|
||||
activeRouteName.value = length > 0 ? result.value[0].name : undefined
|
||||
const length = resultList.value?.length
|
||||
activeRouteName.value = length > 0 ? resultList.value[0].name : undefined
|
||||
}, 500)
|
||||
|
||||
/** 将树形菜单扁平化为一维数组,用于菜单搜索 */
|
||||
function flatTree(arr: RouteRecordRaw[], result: RouteRecordRaw[] = []) {
|
||||
const flatTree = (arr: RouteRecordRaw[], result: RouteRecordRaw[] = []) => {
|
||||
arr.forEach((item) => {
|
||||
result.push(item)
|
||||
item.children && flatTree(item.children, result)
|
||||
@ -49,36 +51,36 @@ function flatTree(arr: RouteRecordRaw[], result: RouteRecordRaw[] = []) {
|
||||
}
|
||||
|
||||
/** 关闭搜索对话框 */
|
||||
function handleClose() {
|
||||
const handleClose = () => {
|
||||
modelValue.value = false
|
||||
// 延时处理防止用户看到重置数据的操作
|
||||
setTimeout(() => {
|
||||
keyword.value = ""
|
||||
result.value = []
|
||||
resultList.value = []
|
||||
}, 200)
|
||||
}
|
||||
|
||||
/** 根据下标位置进行滚动 */
|
||||
function scrollTo(index: number) {
|
||||
if (!resultRef.value) return
|
||||
const scrollTop = resultRef.value.getScrollTop(index)
|
||||
const scrollTo = (index: number) => {
|
||||
if (!searchResultRef.value) return
|
||||
const scrollTop = searchResultRef.value.getScrollTop(index)
|
||||
// 手动控制 el-scrollbar 滚动条滚动,设置滚动条到顶部的距离
|
||||
scrollbarRef.value?.setScrollTop(scrollTop)
|
||||
}
|
||||
|
||||
/** 键盘上键 */
|
||||
function handleUp() {
|
||||
const handleUp = () => {
|
||||
isPressUpOrDown.value = true
|
||||
const { length } = result.value
|
||||
const { length } = resultList.value
|
||||
if (length === 0) return
|
||||
// 获取该 name 在菜单中第一次出现的位置
|
||||
const index = result.value.findIndex(item => item.name === activeRouteName.value)
|
||||
const index = resultList.value.findIndex((item) => item.name === activeRouteName.value)
|
||||
// 如果已处在顶部
|
||||
if (index === 0) {
|
||||
const bottomName = result.value[length - 1].name
|
||||
const bottomName = resultList.value[length - 1].name
|
||||
// 如果顶部和底部的 bottomName 相同,且长度大于 1,就再跳一个位置(可解决遇到首尾两个相同 name 导致的上键不能生效的问题)
|
||||
if (activeRouteName.value === bottomName && length > 1) {
|
||||
activeRouteName.value = result.value[length - 2].name
|
||||
activeRouteName.value = resultList.value[length - 2].name
|
||||
scrollTo(length - 2)
|
||||
} else {
|
||||
// 跳转到底部
|
||||
@ -86,24 +88,24 @@ function handleUp() {
|
||||
scrollTo(length - 1)
|
||||
}
|
||||
} else {
|
||||
activeRouteName.value = result.value[index - 1].name
|
||||
activeRouteName.value = resultList.value[index - 1].name
|
||||
scrollTo(index - 1)
|
||||
}
|
||||
}
|
||||
|
||||
/** 键盘下键 */
|
||||
function handleDown() {
|
||||
const handleDown = () => {
|
||||
isPressUpOrDown.value = true
|
||||
const { length } = result.value
|
||||
const { length } = resultList.value
|
||||
if (length === 0) return
|
||||
// 获取该 name 在菜单中最后一次出现的位置(可解决遇到连续两个相同 name 导致的下键不能生效的问题)
|
||||
const index = result.value.map(item => item.name).lastIndexOf(activeRouteName.value)
|
||||
const index = resultList.value.map((item) => item.name).lastIndexOf(activeRouteName.value)
|
||||
// 如果已处在底部
|
||||
if (index === length - 1) {
|
||||
const topName = result.value[0].name
|
||||
const topName = resultList.value[0].name
|
||||
// 如果底部和顶部的 topName 相同,且长度大于 1,就再跳一个位置(可解决遇到首尾两个相同 name 导致的下键不能生效的问题)
|
||||
if (activeRouteName.value === topName && length > 1) {
|
||||
activeRouteName.value = result.value[1].name
|
||||
activeRouteName.value = resultList.value[1].name
|
||||
scrollTo(1)
|
||||
} else {
|
||||
// 跳转到顶部
|
||||
@ -111,29 +113,36 @@ function handleDown() {
|
||||
scrollTo(0)
|
||||
}
|
||||
} else {
|
||||
activeRouteName.value = result.value[index + 1].name
|
||||
activeRouteName.value = resultList.value[index + 1].name
|
||||
scrollTo(index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
/** 键盘回车键 */
|
||||
function handleEnter() {
|
||||
const { length } = result.value
|
||||
const handleEnter = () => {
|
||||
const { length } = resultList.value
|
||||
if (length === 0) return
|
||||
const name = activeRouteName.value
|
||||
const path = result.value.find(item => item.name === name)?.path
|
||||
if (path && isExternal(path)) return window.open(path, "_blank", "noopener, noreferrer")
|
||||
if (!name) return ElMessage.warning("无法通过搜索进入该菜单,请为对应的路由设置唯一的 Name")
|
||||
const path = resultList.value.find((item) => item.name === name)?.path
|
||||
if (path && isExternal(path)) {
|
||||
window.open(path, "_blank", "noopener, noreferrer")
|
||||
return
|
||||
}
|
||||
if (!name) {
|
||||
ElMessage.warning("无法通过搜索进入该菜单,请为对应的路由设置唯一的 Name")
|
||||
return
|
||||
}
|
||||
try {
|
||||
router.push({ name })
|
||||
} catch {
|
||||
return ElMessage.warning("该菜单有必填的动态参数,无法通过搜索进入")
|
||||
ElMessage.error("该菜单有必填的动态参数,无法通过搜索进入")
|
||||
return
|
||||
}
|
||||
handleClose()
|
||||
}
|
||||
|
||||
/** 释放上键或下键 */
|
||||
function handleReleaseUpOrDown() {
|
||||
const handleReleaseUpOrDown = () => {
|
||||
isPressUpOrDown.value = false
|
||||
}
|
||||
</script>
|
||||
@ -141,38 +150,38 @@ function handleReleaseUpOrDown() {
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="modelValue"
|
||||
:before-close="handleClose"
|
||||
:width="modalWidth"
|
||||
top="5vh"
|
||||
class="search-modal__private"
|
||||
append-to-body
|
||||
@opened="inputRef?.focus()"
|
||||
@closed="inputRef?.blur()"
|
||||
@keydown.up="handleUp"
|
||||
@keydown.down="handleDown"
|
||||
@keydown.enter="handleEnter"
|
||||
@keyup.up.down="handleReleaseUpOrDown"
|
||||
:before-close="handleClose"
|
||||
:width="modalWidth"
|
||||
top="5vh"
|
||||
class="search-modal__private"
|
||||
append-to-body
|
||||
>
|
||||
<el-input ref="inputRef" v-model="keyword" placeholder="搜索菜单" size="large" clearable @input="handleSearch">
|
||||
<el-input ref="inputRef" v-model="keyword" @input="handleSearch" placeholder="搜索菜单" size="large" clearable>
|
||||
<template #prefix>
|
||||
<SvgIcon name="search" class="svg-icon" />
|
||||
<SvgIcon name="search" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-empty v-if="result.length === 0" description="暂无搜索结果" :image-size="100" />
|
||||
<el-empty v-if="resultList.length === 0" description="暂无搜索结果" :image-size="100" />
|
||||
<template v-else>
|
||||
<p>搜索结果</p>
|
||||
<el-scrollbar ref="scrollbarRef" max-height="40vh" always>
|
||||
<Result
|
||||
ref="resultRef"
|
||||
<SearchResult
|
||||
ref="searchResultRef"
|
||||
v-model="activeRouteName"
|
||||
:data="result"
|
||||
:is-press-up-or-down="isPressUpOrDown"
|
||||
:list="resultList"
|
||||
:isPressUpOrDown="isPressUpOrDown"
|
||||
@click="handleEnter"
|
||||
/>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Footer :total="result.length" />
|
||||
<SearchFooter :total="resultList.length" />
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
@ -1,22 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RouteRecordNameGeneric, RouteRecordRaw } from "vue-router"
|
||||
import { getCurrentInstance, onBeforeMount, onBeforeUnmount, onMounted, ref } from "vue"
|
||||
import { type RouteRecordName, type RouteRecordRaw } from "vue-router"
|
||||
|
||||
interface Props {
|
||||
data: RouteRecordRaw[]
|
||||
list: RouteRecordRaw[]
|
||||
isPressUpOrDown: boolean
|
||||
}
|
||||
|
||||
/** 选中的菜单 */
|
||||
const modelValue = defineModel<RouteRecordName | undefined>({ required: true })
|
||||
const props = defineProps<Props>()
|
||||
|
||||
/** 选中的菜单 */
|
||||
const modelValue = defineModel<RouteRecordNameGeneric | undefined>({ required: true })
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
const scrollbarHeight = ref<number>(0)
|
||||
|
||||
/** 菜单的样式 */
|
||||
function itemStyle(item: RouteRecordRaw) {
|
||||
const itemStyle = (item: RouteRecordRaw) => {
|
||||
const flag = item.name === modelValue.value
|
||||
return {
|
||||
background: flag ? "var(--el-color-primary)" : "",
|
||||
@ -25,39 +24,38 @@ function itemStyle(item: RouteRecordRaw) {
|
||||
}
|
||||
|
||||
/** 鼠标移入 */
|
||||
function handleMouseenter(item: RouteRecordRaw) {
|
||||
const handleMouseenter = (item: RouteRecordRaw) => {
|
||||
// 如果上键或下键与 mouseenter 事件同时生效,则以上下键为准,不执行该函数的赋值逻辑
|
||||
if (props.isPressUpOrDown) return
|
||||
modelValue.value = item.name
|
||||
}
|
||||
|
||||
/** 计算滚动可视区高度 */
|
||||
function getScrollbarHeight() {
|
||||
const getScrollbarHeight = () => {
|
||||
// el-scrollbar max-height="40vh"
|
||||
scrollbarHeight.value = Number((window.innerHeight * 0.4).toFixed(1))
|
||||
}
|
||||
|
||||
/** 根据下标计算到顶部的距离 */
|
||||
function getScrollTop(index: number) {
|
||||
const getScrollTop = (index: number) => {
|
||||
const currentInstance = instance?.proxy?.$refs[`resultItemRef${index}`] as HTMLDivElement[]
|
||||
if (!currentInstance) return 0
|
||||
const currentRef = currentInstance[0]
|
||||
// 128 = 两个 result-item (56 + 56 = 112)高度与上下 margin(8 + 8 = 16)大小之和
|
||||
const scrollTop = currentRef.offsetTop + 128
|
||||
const scrollTop = currentRef.offsetTop + 128 // 128 = 两个 result-item (56 + 56 = 112)高度与上下 margin(8 + 8 = 16)大小之和
|
||||
return scrollTop > scrollbarHeight.value ? scrollTop - scrollbarHeight.value : 0
|
||||
}
|
||||
|
||||
// 在组件挂载前添加窗口大小变化事件监听器
|
||||
/** 在组件挂载前添加窗口大小变化事件监听器 */
|
||||
onBeforeMount(() => {
|
||||
window.addEventListener("resize", getScrollbarHeight)
|
||||
})
|
||||
|
||||
// 在组件挂载时立即计算滚动可视区高度
|
||||
/** 在组件挂载时立即计算滚动可视区高度 */
|
||||
onMounted(() => {
|
||||
getScrollbarHeight()
|
||||
})
|
||||
|
||||
// 在组件卸载前移除窗口大小变化事件监听器
|
||||
/** 在组件卸载前移除窗口大小变化事件监听器 */
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", getScrollbarHeight)
|
||||
})
|
||||
@ -69,25 +67,25 @@ defineExpose({ getScrollTop })
|
||||
<!-- 外层 div 不能删除,是用来接收父组件 click 事件的 -->
|
||||
<div>
|
||||
<div
|
||||
v-for="(item, index) in props.data"
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
:ref="`resultItemRef${index}`"
|
||||
class="result-item"
|
||||
:style="itemStyle(item)"
|
||||
@mouseenter="handleMouseenter(item)"
|
||||
>
|
||||
<SvgIcon v-if="item.meta?.svgIcon" :name="item.meta.svgIcon" class="svg-icon" />
|
||||
<SvgIcon v-if="item.meta?.svgIcon" :name="item.meta.svgIcon" />
|
||||
<component v-else-if="item.meta?.elIcon" :is="item.meta.elIcon" class="el-icon" />
|
||||
<span class="result-item-title">
|
||||
{{ item.meta?.title }}
|
||||
</span>
|
||||
<SvgIcon v-if="modelValue && modelValue === item.name" name="keyboard-enter" class="svg-icon" />
|
||||
<SvgIcon v-if="modelValue && modelValue === item.name" name="keyboard-enter" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@@/assets/styles/mixins.scss";
|
||||
@import "@/styles/mixins.scss";
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
@ -1,21 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import Modal from "./Modal.vue"
|
||||
import { ref } from "vue"
|
||||
import SearchModal from "./SearchModal.vue"
|
||||
|
||||
/** 控制 modal 显隐 */
|
||||
const visible = ref<boolean>(false)
|
||||
|
||||
const modalVisible = ref<boolean>(false)
|
||||
/** 打开 modal */
|
||||
function handleOpen() {
|
||||
visible.value = true
|
||||
const handleOpen = () => {
|
||||
modalVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-tooltip effect="dark" content="搜索菜单" placement="bottom">
|
||||
<SvgIcon name="search" @click="handleOpen" class="svg-icon" />
|
||||
<SvgIcon name="search" @click="handleOpen" />
|
||||
</el-tooltip>
|
||||
<Modal v-model="visible" />
|
||||
<SearchModal v-model="modalVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
29
src/components/SvgIcon/index.vue
Normal file
@ -0,0 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
|
||||
interface Props {
|
||||
prefix?: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
prefix: "icon"
|
||||
})
|
||||
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.name}`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg class="svg-icon">
|
||||
<use :href="symbolId" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
49
src/components/ThemeSwitch/index.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<script lang="ts" setup>
|
||||
import { type ThemeName, useTheme } from "@/hooks/useTheme"
|
||||
import { MagicStick } from "@element-plus/icons-vue"
|
||||
|
||||
const { themeList, activeThemeName, setTheme } = useTheme()
|
||||
|
||||
const handleChangeTheme = ({ clientX, clientY }: MouseEvent, themeName: ThemeName) => {
|
||||
const maxRadius = Math.hypot(
|
||||
Math.max(clientX, window.innerWidth - clientX),
|
||||
Math.max(clientY, window.innerHeight - clientY)
|
||||
)
|
||||
const style = document.documentElement.style
|
||||
style.setProperty("--v3-theme-x", clientX + "px")
|
||||
style.setProperty("--v3-theme-y", clientY + "px")
|
||||
style.setProperty("--v3-theme-r", maxRadius + "px")
|
||||
const handler = () => {
|
||||
setTheme(themeName)
|
||||
}
|
||||
document.startViewTransition ? document.startViewTransition(handler) : handler()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dropdown trigger="click">
|
||||
<div>
|
||||
<el-tooltip effect="dark" content="主题模式" placement="bottom">
|
||||
<el-icon :size="20">
|
||||
<MagicStick />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="(theme, index) in themeList"
|
||||
:key="index"
|
||||
:disabled="activeThemeName === theme.name"
|
||||
@click="
|
||||
(e: MouseEvent) => {
|
||||
handleChangeTheme(e, theme.name)
|
||||
}
|
||||
"
|
||||
>
|
||||
<span>{{ theme.title }}</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
@ -1,9 +1,9 @@
|
||||
import { LayoutModeEnum } from "@@/constants/app-key"
|
||||
import { getLayoutsConfig } from "@@/utils/cache/local-storage"
|
||||
import { getConfigLayout } from "@/utils/cache/local-storage"
|
||||
import { LayoutModeEnum } from "@/constants/app-key"
|
||||
|
||||
/** 项目配置类型 */
|
||||
export interface LayoutsConfig {
|
||||
/** 是否显示设置按钮和面板 */
|
||||
export interface LayoutSettings {
|
||||
/** 是否显示 Settings Panel */
|
||||
showSettings: boolean
|
||||
/** 布局模式 */
|
||||
layoutMode: LayoutModeEnum
|
||||
@ -13,7 +13,7 @@ export interface LayoutsConfig {
|
||||
showLogo: boolean
|
||||
/** 是否固定 Header */
|
||||
fixedHeader: boolean
|
||||
/** 是否显示页脚 */
|
||||
/** 是否显示页脚 Footer */
|
||||
showFooter: boolean
|
||||
/** 是否显示消息通知 */
|
||||
showNotify: boolean
|
||||
@ -34,7 +34,7 @@ export interface LayoutsConfig {
|
||||
}
|
||||
|
||||
/** 默认配置 */
|
||||
const DEFAULT_CONFIG: LayoutsConfig = {
|
||||
const defaultSettings: LayoutSettings = {
|
||||
layoutMode: LayoutModeEnum.Left,
|
||||
showSettings: true,
|
||||
showTagsView: true,
|
||||
@ -52,4 +52,4 @@ const DEFAULT_CONFIG: LayoutsConfig = {
|
||||
}
|
||||
|
||||
/** 项目配置 */
|
||||
export const layoutsConfig: LayoutsConfig = { ...DEFAULT_CONFIG, ...getLayoutsConfig() }
|
||||
export const layoutSettings: LayoutSettings = { ...defaultSettings, ...getConfigLayout() }
|
28
src/config/route.ts
Normal file
@ -0,0 +1,28 @@
|
||||
/** 路由配置 */
|
||||
interface RouteSettings {
|
||||
/**
|
||||
* 是否开启动态路由功能?
|
||||
* 1. 开启后需要后端配合,在查询用户详情接口返回当前用户可以用来判断并加载动态路由的字段(该项目用的是角色 roles 字段)
|
||||
* 2. 假如项目不需要根据不同的用户来显示不同的页面,则应该将 dynamic: false
|
||||
*/
|
||||
dynamic: boolean
|
||||
/** 当动态路由功能关闭时:
|
||||
* 1. 应该将所有路由都写到常驻路由里面(表明所有登录的用户能访问的页面都是一样的)
|
||||
* 2. 系统自动给当前登录用户赋值一个没有任何作用的默认角色
|
||||
*/
|
||||
defaultRoles: Array<string>
|
||||
/**
|
||||
* 是否开启三级及其以上路由缓存功能?
|
||||
* 1. 开启后会进行路由降级(把三级及其以上的路由转化为二级路由)
|
||||
* 2. 由于都会转成二级路由,所以二级及其以上路由有内嵌子路由将会失效
|
||||
*/
|
||||
thirdLevelRouteCache: boolean
|
||||
}
|
||||
|
||||
const routeSettings: RouteSettings = {
|
||||
dynamic: true,
|
||||
defaultRoles: ["DEFAULT_ROLE"],
|
||||
thirdLevelRouteCache: false
|
||||
}
|
||||
|
||||
export default routeSettings
|
@ -1,4 +1,4 @@
|
||||
import type { RouteLocationNormalizedGeneric, RouteRecordNameGeneric } from "vue-router"
|
||||
import { type RouteLocationNormalized, type RouteRecordNameGeneric } from "vue-router"
|
||||
|
||||
/** 免登录白名单(匹配路由 path) */
|
||||
const whiteListByPath: string[] = ["/login"]
|
||||
@ -7,7 +7,9 @@ const whiteListByPath: string[] = ["/login"]
|
||||
const whiteListByName: RouteRecordNameGeneric[] = []
|
||||
|
||||
/** 判断是否在白名单 */
|
||||
export function isWhiteList(to: RouteLocationNormalizedGeneric) {
|
||||
const isWhiteList = (to: RouteLocationNormalized) => {
|
||||
// path 和 name 任意一个匹配上即可
|
||||
return whiteListByPath.includes(to.path) || whiteListByName.includes(to.name)
|
||||
return whiteListByPath.indexOf(to.path) !== -1 || whiteListByName.indexOf(to.name) !== -1
|
||||
}
|
||||
|
||||
export default isWhiteList
|
@ -13,10 +13,8 @@ export enum LayoutModeEnum {
|
||||
|
||||
/** 侧边栏打开状态常量 */
|
||||
export const SIDEBAR_OPENED = "opened"
|
||||
|
||||
/** 侧边栏关闭状态常量 */
|
||||
export const SIDEBAR_CLOSED = "closed"
|
||||
|
||||
export type SidebarOpened = typeof SIDEBAR_OPENED
|
||||
|
||||
export type SidebarClosed = typeof SIDEBAR_CLOSED
|
@ -1,7 +1,7 @@
|
||||
const SYSTEM_NAME = "v3-admin-vite"
|
||||
|
||||
/** 缓存数据时用到的 Key */
|
||||
export class CacheKey {
|
||||
class CacheKey {
|
||||
static readonly TOKEN = `${SYSTEM_NAME}-token-key`
|
||||
static readonly CONFIG_LAYOUT = `${SYSTEM_NAME}-config-layout-key`
|
||||
static readonly SIDEBAR_STATUS = `${SYSTEM_NAME}-sidebar-status-key`
|
||||
@ -9,3 +9,5 @@ export class CacheKey {
|
||||
static readonly VISITED_VIEWS = `${SYSTEM_NAME}-visited-views-key`
|
||||
static readonly CACHED_VIEWS = `${SYSTEM_NAME}-cached-views-key`
|
||||
}
|
||||
|
||||
export default CacheKey
|
7
src/directives/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { type App } from "vue"
|
||||
import { permission } from "./permission"
|
||||
|
||||
/** 挂载自定义指令 */
|
||||
export function loadDirectives(app: App) {
|
||||
app.directive("permission", permission)
|
||||
}
|
17
src/directives/permission/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { type Directive } from "vue"
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
|
||||
/** 权限指令,和权限判断函数 checkPermission 功能类似 */
|
||||
export const permission: Directive = {
|
||||
mounted(el, binding) {
|
||||
const { value: permissionRoles } = binding
|
||||
const { roles } = useUserStore()
|
||||
if (Array.isArray(permissionRoles) && permissionRoles.length > 0) {
|
||||
const hasPermission = roles.some((role) => permissionRoles.includes(role))
|
||||
// hasPermission || (el.style.display = "none") // 隐藏
|
||||
hasPermission || el.parentNode?.removeChild(el) // 销毁
|
||||
} else {
|
||||
throw new Error(`need roles! Like v-permission="['admin','editor']"`)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
import { useAppStore } from "@/pinia/stores/app"
|
||||
import { DeviceEnum } from "@@/constants/app-key"
|
||||
import { computed } from "vue"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
import { DeviceEnum } from "@/constants/app-key"
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const isMobile = computed(() => appStore.device === DeviceEnum.Mobile)
|
||||
const isDesktop = computed(() => appStore.device === DeviceEnum.Desktop)
|
||||
|
||||
/** 设备类型 Composable */
|
||||
export function useDevice() {
|
||||
return { isMobile, isDesktop }
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { ref, onMounted } from "vue"
|
||||
|
||||
type OptionValue = string | number
|
||||
|
||||
/** Select 需要的数据格式 */
|
||||
@ -15,7 +17,6 @@ interface FetchSelectProps {
|
||||
api: () => Promise<ApiData>
|
||||
}
|
||||
|
||||
/** 下拉选择器 Composable */
|
||||
export function useFetchSelect(props: FetchSelectProps) {
|
||||
const { api } = props
|
||||
|
||||
@ -23,20 +24,26 @@ export function useFetchSelect(props: FetchSelectProps) {
|
||||
const options = ref<SelectOption[]>([])
|
||||
const value = ref<OptionValue>("")
|
||||
|
||||
// 调用接口获取数据
|
||||
/** 调用接口获取数据 */
|
||||
const loadData = () => {
|
||||
loading.value = true
|
||||
options.value = []
|
||||
api().then((res) => {
|
||||
options.value = res.data
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
api()
|
||||
.then((res) => {
|
||||
options.value = res.data
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
return { loading, options, value }
|
||||
return {
|
||||
loading,
|
||||
options,
|
||||
value
|
||||
}
|
||||
}
|
@ -1,4 +1,13 @@
|
||||
import type { LoadingOptions } from "element-plus"
|
||||
import { type LoadingOptions, ElLoading } from "element-plus"
|
||||
|
||||
const defaultOptions = {
|
||||
lock: true,
|
||||
text: "加载中..."
|
||||
}
|
||||
|
||||
interface LoadingInstance {
|
||||
close: () => void
|
||||
}
|
||||
|
||||
interface UseFullscreenLoading {
|
||||
<T extends (...args: Parameters<T>) => ReturnType<T>>(
|
||||
@ -7,18 +16,8 @@ interface UseFullscreenLoading {
|
||||
): (...args: Parameters<T>) => Promise<ReturnType<T>>
|
||||
}
|
||||
|
||||
interface LoadingInstance {
|
||||
close: () => void
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
lock: true,
|
||||
text: "加载中..."
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 全屏加载 Composable
|
||||
* @description 传入一个函数 fn,在它执行周期内,加上「全屏」Loading
|
||||
* 传入一个函数 fn,在它执行周期内,加上「全屏」loading
|
||||
* @param fn 要执行的函数
|
||||
* @param options LoadingOptions
|
||||
* @returns 返回一个新的函数,该函数返回一个 Promise
|
||||
@ -27,10 +26,10 @@ export const useFullscreenLoading: UseFullscreenLoading = (fn, options = {}) =>
|
||||
let loadingInstance: LoadingInstance
|
||||
return async (...args) => {
|
||||
try {
|
||||
loadingInstance = ElLoading.service({ ...DEFAULT_OPTIONS, ...options })
|
||||
loadingInstance = ElLoading.service({ ...defaultOptions, ...options })
|
||||
return await fn(...args)
|
||||
} finally {
|
||||
loadingInstance.close()
|
||||
loadingInstance?.close()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
import { useSettingsStore } from "@/pinia/stores/settings"
|
||||
import { watchEffect } from "vue"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
|
||||
const GREY_MODE = "grey-mode"
|
||||
const COLOR_WEAKNESS = "color-weakness"
|
||||
|
||||
const classList = document.documentElement.classList
|
||||
|
||||
/** 初始化 */
|
||||
function initGreyAndColorWeakness() {
|
||||
const initGreyAndColorWeakness = () => {
|
||||
const settingsStore = useSettingsStore()
|
||||
watchEffect(() => {
|
||||
classList.toggle(GREY_MODE, settingsStore.showGreyMode)
|
||||
@ -14,7 +14,7 @@ function initGreyAndColorWeakness() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 灰色模式和色弱模式 Composable */
|
||||
/** 灰色模式和色弱模式 hook */
|
||||
export function useGreyAndColorWeakness() {
|
||||
return { initGreyAndColorWeakness }
|
||||
}
|
@ -1,17 +1,16 @@
|
||||
import { useSettingsStore } from "@/pinia/stores/settings"
|
||||
import { LayoutModeEnum } from "@@/constants/app-key"
|
||||
import { computed } from "vue"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import { LayoutModeEnum } from "@/constants/app-key"
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const isLeft = computed(() => settingsStore.layoutMode === LayoutModeEnum.Left)
|
||||
const isTop = computed(() => settingsStore.layoutMode === LayoutModeEnum.Top)
|
||||
const isLeftTop = computed(() => settingsStore.layoutMode === LayoutModeEnum.LeftTop)
|
||||
|
||||
function setLayoutMode(mode: LayoutModeEnum) {
|
||||
const setLayoutMode = (mode: LayoutModeEnum) => {
|
||||
settingsStore.layoutMode = mode
|
||||
}
|
||||
|
||||
/** 布局模式 Composable */
|
||||
export function useLayoutMode() {
|
||||
return { isLeft, isTop, isLeftTop, setLayoutMode }
|
||||
}
|
@ -1,3 +1,13 @@
|
||||
import { reactive } from "vue"
|
||||
|
||||
interface DefaultPaginationData {
|
||||
total: number
|
||||
currentPage: number
|
||||
pageSizes: number[]
|
||||
pageSize: number
|
||||
layout: string
|
||||
}
|
||||
|
||||
interface PaginationData {
|
||||
total?: number
|
||||
currentPage?: number
|
||||
@ -7,7 +17,7 @@ interface PaginationData {
|
||||
}
|
||||
|
||||
/** 默认的分页参数 */
|
||||
const DEFAULT_PAGINATION_DATA = {
|
||||
const defaultPaginationData: DefaultPaginationData = {
|
||||
total: 0,
|
||||
currentPage: 1,
|
||||
pageSizes: [10, 20, 50],
|
||||
@ -15,15 +25,14 @@ const DEFAULT_PAGINATION_DATA = {
|
||||
layout: "total, sizes, prev, pager, next, jumper"
|
||||
}
|
||||
|
||||
/** 分页 Composable */
|
||||
export function usePagination(initPaginationData: PaginationData = {}) {
|
||||
// 合并分页参数
|
||||
const paginationData = reactive({ ...DEFAULT_PAGINATION_DATA, ...initPaginationData })
|
||||
// 改变当前页码
|
||||
export function usePagination(initialPaginationData: PaginationData = {}) {
|
||||
/** 合并分页参数 */
|
||||
const paginationData = reactive({ ...defaultPaginationData, ...initialPaginationData })
|
||||
/** 改变当前页码 */
|
||||
const handleCurrentChange = (value: number) => {
|
||||
paginationData.currentPage = value
|
||||
}
|
||||
// 改变每页显示条数
|
||||
/** 改变页面大小 */
|
||||
const handleSizeChange = (value: number) => {
|
||||
paginationData.pageSize = value
|
||||
}
|
@ -1,34 +1,28 @@
|
||||
import type { Handler } from "mitt"
|
||||
import type { RouteLocationNormalizedGeneric } from "vue-router"
|
||||
import mitt from "mitt"
|
||||
import { onBeforeUnmount } from "vue"
|
||||
import mitt, { type Handler } from "mitt"
|
||||
import { type RouteLocationNormalized } from "vue-router"
|
||||
|
||||
/** 回调函数的类型 */
|
||||
type Callback = (route: RouteLocationNormalizedGeneric) => void
|
||||
type Callback = (route: RouteLocationNormalized) => void
|
||||
|
||||
const emitter = mitt()
|
||||
|
||||
const key = Symbol("ROUTE_CHANGE")
|
||||
|
||||
let latestRoute: RouteLocationNormalizedGeneric
|
||||
let latestRoute: RouteLocationNormalized
|
||||
|
||||
/** 设置最新的路由信息,触发路由变化事件 */
|
||||
export function setRouteChange(to: RouteLocationNormalizedGeneric) {
|
||||
export const setRouteChange = (to: RouteLocationNormalized) => {
|
||||
// 触发事件
|
||||
emitter.emit(key, to)
|
||||
// 缓存最新的路由信息
|
||||
latestRoute = to
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 订阅路由变化 Composable
|
||||
* @description 1. 单独用 watch 监听路由会浪费渲染性能
|
||||
* @description 2. 可优先选择使用该发布订阅模式去进行分发管理
|
||||
*/
|
||||
/** 单独监听路由会浪费渲染性能,使用发布订阅模式去进行分发管理 */
|
||||
export function useRouteListener() {
|
||||
// 回调函数集合
|
||||
/** 回调函数集合 */
|
||||
const callbackList: Callback[] = []
|
||||
|
||||
// 监听路由变化(可以选择立即执行)
|
||||
/** 监听路由变化(可以选择立即执行) */
|
||||
const listenerRouteChange = (callback: Callback, immediate = false) => {
|
||||
// 缓存回调函数
|
||||
callbackList.push(callback)
|
||||
@ -38,14 +32,16 @@ export function useRouteListener() {
|
||||
immediate && latestRoute && callback(latestRoute)
|
||||
}
|
||||
|
||||
// 移除路由变化事件监听器
|
||||
/** 移除路由变化事件监听器 */
|
||||
const removeRouteListener = (callback: Callback) => {
|
||||
emitter.off(key, callback as Handler)
|
||||
}
|
||||
|
||||
// 组件销毁前移除监听器
|
||||
/** 组件销毁前移除监听器 */
|
||||
onBeforeUnmount(() => {
|
||||
callbackList.forEach(removeRouteListener)
|
||||
for (let i = 0; i < callbackList.length; i++) {
|
||||
removeRouteListener(callbackList[i])
|
||||
}
|
||||
})
|
||||
|
||||
return { listenerRouteChange, removeRouteListener }
|
@ -1,8 +1,7 @@
|
||||
import { getActiveThemeName, setActiveThemeName } from "@@/utils/cache/local-storage"
|
||||
import { setCssVar } from "@@/utils/css"
|
||||
import { ref, watchEffect } from "vue"
|
||||
import { getActiveThemeName, setActiveThemeName } from "@/utils/cache/local-storage"
|
||||
|
||||
const DEFAULT_THEME_NAME = "normal"
|
||||
|
||||
type DefaultThemeName = typeof DEFAULT_THEME_NAME
|
||||
|
||||
/** 注册的主题名称, 其中 DefaultThemeName 是必填的 */
|
||||
@ -33,33 +32,23 @@ const themeList: ThemeList[] = [
|
||||
const activeThemeName = ref<ThemeName>(getActiveThemeName() || DEFAULT_THEME_NAME)
|
||||
|
||||
/** 设置主题 */
|
||||
function setTheme({ clientX, clientY }: MouseEvent, value: ThemeName) {
|
||||
const maxRadius = Math.hypot(
|
||||
Math.max(clientX, window.innerWidth - clientX),
|
||||
Math.max(clientY, window.innerHeight - clientY)
|
||||
)
|
||||
setCssVar("--v3-theme-x", `${clientX}px`)
|
||||
setCssVar("--v3-theme-y", `${clientY}px`)
|
||||
setCssVar("--v3-theme-r", `${maxRadius}px`)
|
||||
const handler = () => {
|
||||
activeThemeName.value = value
|
||||
}
|
||||
document.startViewTransition ? document.startViewTransition(handler) : handler()
|
||||
const setTheme = (value: ThemeName) => {
|
||||
activeThemeName.value = value
|
||||
}
|
||||
|
||||
/** 在 html 根元素上挂载 class */
|
||||
function addHtmlClass(value: ThemeName) {
|
||||
const addHtmlClass = (value: ThemeName) => {
|
||||
document.documentElement.classList.add(value)
|
||||
}
|
||||
|
||||
/** 在 html 根元素上移除其他主题 class */
|
||||
function removeHtmlClass(value: ThemeName) {
|
||||
const otherThemeNameList = themeList.map(item => item.name).filter(name => name !== value)
|
||||
const removeHtmlClass = (value: ThemeName) => {
|
||||
const otherThemeNameList = themeList.map((item) => item.name).filter((name) => name !== value)
|
||||
document.documentElement.classList.remove(...otherThemeNameList)
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
function initTheme() {
|
||||
const initTheme = () => {
|
||||
// watchEffect 来收集副作用
|
||||
watchEffect(() => {
|
||||
const value = activeThemeName.value
|
||||
@ -69,7 +58,7 @@ function initTheme() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 主题 Composable */
|
||||
/** 主题 hook */
|
||||
export function useTheme() {
|
||||
return { themeList, activeThemeName, initTheme, setTheme }
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { ref, watch } from "vue"
|
||||
|
||||
/** 项目标题 */
|
||||
const VITE_APP_TITLE = import.meta.env.VITE_APP_TITLE ?? "V3 Admin Vite"
|
||||
|
||||
@ -5,18 +7,17 @@ const VITE_APP_TITLE = import.meta.env.VITE_APP_TITLE ?? "V3 Admin Vite"
|
||||
const dynamicTitle = ref<string>("")
|
||||
|
||||
/** 设置标题 */
|
||||
function setTitle(title?: string) {
|
||||
const setTitle = (title?: string) => {
|
||||
dynamicTitle.value = title ? `${VITE_APP_TITLE} | ${title}` : VITE_APP_TITLE
|
||||
}
|
||||
|
||||
// 监听标题变化
|
||||
/** 监听标题变化 */
|
||||
watch(dynamicTitle, (value, oldValue) => {
|
||||
if (document && value !== oldValue) {
|
||||
document.title = value
|
||||
}
|
||||
})
|
||||
|
||||
/** 标题 Composable */
|
||||
export function useTitle() {
|
||||
return { setTitle }
|
||||
}
|
@ -1,8 +1,16 @@
|
||||
import type { Ref } from "vue"
|
||||
import { type Ref, onBeforeUnmount, ref } from "vue"
|
||||
import { debounce } from "lodash-es"
|
||||
|
||||
type Observer = {
|
||||
watermarkElMutationObserver?: MutationObserver
|
||||
parentElMutationObserver?: MutationObserver
|
||||
parentElResizeObserver?: ResizeObserver
|
||||
}
|
||||
|
||||
type DefaultConfig = typeof defaultConfig
|
||||
|
||||
/** 默认配置 */
|
||||
const DEFAULT_CONFIG = {
|
||||
const defaultConfig = {
|
||||
/** 防御(默认开启,能防御水印被删除或隐藏,但可能会有性能损耗) */
|
||||
defense: true,
|
||||
/** 文本颜色 */
|
||||
@ -21,50 +29,45 @@ const DEFAULT_CONFIG = {
|
||||
height: 200
|
||||
}
|
||||
|
||||
type DefaultConfig = typeof DEFAULT_CONFIG
|
||||
|
||||
interface Observer {
|
||||
watermarkElMutationObserver?: MutationObserver
|
||||
parentElMutationObserver?: MutationObserver
|
||||
parentElResizeObserver?: ResizeObserver
|
||||
}
|
||||
|
||||
/** body 元素 */
|
||||
const bodyEl = ref<HTMLElement>(document.body)
|
||||
|
||||
/**
|
||||
* @name 水印 Composable
|
||||
* @description 1. 可以选择传入挂载水印的容器元素,默认是 body
|
||||
* @description 2. 做了水印防御,能有效防御别人打开控制台删除或隐藏水印
|
||||
* 创建水印
|
||||
* 1. 可以选择传入挂载水印的容器元素,默认是 body
|
||||
* 2. 做了水印防御,能有效防御别人打开控制台删除或隐藏水印
|
||||
*/
|
||||
export function useWatermark(parentEl: Ref<HTMLElement | null> = bodyEl) {
|
||||
// 备份文本
|
||||
/** 备份文本 */
|
||||
let backupText: string
|
||||
// 最终配置
|
||||
/** 最终配置 */
|
||||
let mergeConfig: DefaultConfig
|
||||
// 水印元素
|
||||
/** 水印元素 */
|
||||
let watermarkEl: HTMLElement | null = null
|
||||
// 观察器
|
||||
/** 观察器 */
|
||||
const observer: Observer = {
|
||||
watermarkElMutationObserver: undefined,
|
||||
parentElMutationObserver: undefined,
|
||||
parentElResizeObserver: undefined
|
||||
}
|
||||
|
||||
// 设置水印
|
||||
/** 设置水印 */
|
||||
const setWatermark = (text: string, config: Partial<DefaultConfig> = {}) => {
|
||||
if (!parentEl.value) return console.warn("请在 DOM 挂载完成后再调用 setWatermark 方法设置水印")
|
||||
if (!parentEl.value) {
|
||||
console.warn("请在 DOM 挂载完成后再调用 setWatermark 方法设置水印")
|
||||
return
|
||||
}
|
||||
// 备份文本
|
||||
backupText = text
|
||||
// 合并配置
|
||||
mergeConfig = { ...DEFAULT_CONFIG, ...config }
|
||||
mergeConfig = { ...defaultConfig, ...config }
|
||||
// 创建或更新水印元素
|
||||
watermarkEl ? updateWatermarkEl() : createWatermarkEl()
|
||||
// 监听水印元素和容器元素的变化
|
||||
addElListener(parentEl.value)
|
||||
}
|
||||
|
||||
// 创建水印元素
|
||||
/** 创建水印元素 */
|
||||
const createWatermarkEl = () => {
|
||||
const isBody = parentEl.value!.tagName.toLowerCase() === bodyEl.value.tagName.toLowerCase()
|
||||
const watermarkElPosition = isBody ? "fixed" : "absolute"
|
||||
@ -83,7 +86,7 @@ export function useWatermark(parentEl: Ref<HTMLElement | null> = bodyEl) {
|
||||
parentEl.value!.appendChild(watermarkEl)
|
||||
}
|
||||
|
||||
// 更新水印元素
|
||||
/** 更新水印元素 */
|
||||
const updateWatermarkEl = (
|
||||
options: Partial<{
|
||||
width: number
|
||||
@ -96,7 +99,7 @@ export function useWatermark(parentEl: Ref<HTMLElement | null> = bodyEl) {
|
||||
options.height && (watermarkEl.style.height = `${options.height}px`)
|
||||
}
|
||||
|
||||
// 创建 base64 图片
|
||||
/** 创建 base64 图片 */
|
||||
const createBase64 = () => {
|
||||
const { color, opacity, size, family, angle, width, height } = mergeConfig
|
||||
const canvasEl = document.createElement("canvas")
|
||||
@ -113,7 +116,7 @@ export function useWatermark(parentEl: Ref<HTMLElement | null> = bodyEl) {
|
||||
return canvasEl.toDataURL()
|
||||
}
|
||||
|
||||
// 清除水印
|
||||
/** 清除水印 */
|
||||
const clearWatermark = () => {
|
||||
if (!parentEl.value || !watermarkEl) return
|
||||
// 移除对水印元素和容器元素的监听
|
||||
@ -129,14 +132,14 @@ export function useWatermark(parentEl: Ref<HTMLElement | null> = bodyEl) {
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新水印(防御时调用)
|
||||
/** 刷新水印(防御时调用) */
|
||||
const updateWatermark = debounce(() => {
|
||||
clearWatermark()
|
||||
createWatermarkEl()
|
||||
addElListener(parentEl.value!)
|
||||
}, 100)
|
||||
|
||||
// 监听水印元素和容器元素的变化(DOM 变化 & DOM 大小变化)
|
||||
/** 监听水印元素和容器元素的变化(DOM 变化 & DOM 大小变化) */
|
||||
const addElListener = (targetNode: HTMLElement) => {
|
||||
// 判断是否开启防御
|
||||
if (mergeConfig.defense) {
|
||||
@ -156,7 +159,7 @@ export function useWatermark(parentEl: Ref<HTMLElement | null> = bodyEl) {
|
||||
}
|
||||
}
|
||||
|
||||
// 移除对水印元素和容器元素的监听,传参可指定要移除哪个监听,不传默认移除全部监听
|
||||
/** 移除对水印元素和容器元素的监听,传参可指定要移除哪个监听,不传默认移除全部监听 */
|
||||
const removeListener = (kind: "mutation" | "resize" | "all" = "all") => {
|
||||
// 移除 mutation 监听
|
||||
if (kind === "mutation" || kind === "all") {
|
||||
@ -172,7 +175,7 @@ export function useWatermark(parentEl: Ref<HTMLElement | null> = bodyEl) {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 DOM 变化
|
||||
/** 监听 DOM 变化 */
|
||||
const addMutationListener = (targetNode: HTMLElement) => {
|
||||
// 当观察到变动时执行的回调
|
||||
const mutationCallback = debounce((mutationList: MutationRecord[]) => {
|
||||
@ -211,7 +214,7 @@ export function useWatermark(parentEl: Ref<HTMLElement | null> = bodyEl) {
|
||||
})
|
||||
}
|
||||
|
||||
// 监听 DOM 大小变化
|
||||
/** 监听 DOM 大小变化 */
|
||||
const addResizeListener = (targetNode: HTMLElement) => {
|
||||
// 当 targetNode 元素大小变化时去更新整个水印的大小
|
||||
const resizeCallback = debounce(() => {
|
||||
@ -224,7 +227,7 @@ export function useWatermark(parentEl: Ref<HTMLElement | null> = bodyEl) {
|
||||
observer.parentElResizeObserver.observe(targetNode)
|
||||
}
|
||||
|
||||
// 在组件卸载前移除水印以及各种监听
|
||||
/** 在组件卸载前移除水印以及各种监听 */
|
||||
onBeforeUnmount(() => {
|
||||
clearWatermark()
|
||||
})
|
7
src/icons/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { type App } from "vue"
|
||||
import SvgIcon from "@/components/SvgIcon/index.vue" // Svg Component
|
||||
import "virtual:svg-icons-register"
|
||||
|
||||
export function loadSvg(app: App) {
|
||||
app.component("SvgIcon", SvgIcon)
|
||||
}
|
1
src/icons/svg/404.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg t="1651119499039" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9021" width="200" height="200"><path d="M512 720m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" p-id="9022"></path><path d="M480 416v184c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V416c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8z" p-id="9023"></path><path d="M955.7 856l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48z m-783.5-27.9L512 239.9l339.8 588.2H172.2z" p-id="9024"></path></svg>
|
After Width: | Height: | Size: 548 B |
1
src/icons/svg/bug.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg t="1651119031318" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8881" width="200" height="200"><path d="M940 512H792V412c76.8 0 139-62.2 139-139 0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 34.8-28.2 63-63 63H232c-34.8 0-63-28.2-63-63 0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 76.8 62.2 139 139 139v100H84c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h148v96c0 6.5 0.2 13 0.7 19.3C164.1 728.6 116 796.7 116 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-44.2 23.9-82.9 59.6-103.7 6 17.2 13.6 33.6 22.7 49 24.3 41.5 59 76.2 100.5 100.5S460.5 960 512 960s99.8-13.9 141.3-38.2c41.5-24.3 76.2-59 100.5-100.5 9.1-15.5 16.7-31.9 22.7-49C812.1 793.1 836 831.8 836 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-79.3-48.1-147.4-116.7-176.7 0.4-6.4 0.7-12.8 0.7-19.3v-96h148c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM716 680c0 36.8-9.7 72-27.8 102.9-17.7 30.3-43 55.6-73.3 73.3-20.1 11.8-42 20-64.9 24.3V484c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v396.5c-22.9-4.3-44.8-12.5-64.9-24.3-30.3-17.7-55.6-43-73.3-73.3C317.7 752 308 716.8 308 680V412h408v268z" p-id="8882"></path><path d="M304 280h56c4.4 0 8-3.6 8-8 0-28.3 5.9-53.2 17.1-73.5 10.6-19.4 26-34.8 45.4-45.4C450.9 142 475.7 136 504 136h16c28.3 0 53.2 5.9 73.5 17.1 19.4 10.6 34.8 26 45.4 45.4C650 218.9 656 243.7 656 272c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-40-8.8-76.7-25.9-108.1-17.2-31.5-42.5-56.8-74-74C596.7 72.8 560 64 520 64h-16c-40 0-76.7 8.8-108.1 25.9-31.5 17.2-56.8 42.5-74 74C304.8 195.3 296 232 296 272c0 4.4 3.6 8 8 8z" p-id="8883"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
1
src/icons/svg/component.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1672728665955" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3482" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M64 64h384v384H64V64z m0 512h384v384H64V576z m512 0h384v384H576V576z m192-128c106.039 0 192-85.961 192-192S874.039 64 768 64s-192 85.961-192 192 85.961 192 192 192z" p-id="3483"></path></svg>
|
After Width: | Height: | Size: 522 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 747 B After Width: | Height: | Size: 747 B |
Before Width: | Height: | Size: 746 B After Width: | Height: | Size: 746 B |
Before Width: | Height: | Size: 223 B After Width: | Height: | Size: 223 B |
Before Width: | Height: | Size: 241 B After Width: | Height: | Size: 241 B |
Before Width: | Height: | Size: 694 B After Width: | Height: | Size: 694 B |