Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3aa8147b1 | ||
|
|
0d240f083a | ||
|
|
c180cf54a8 | ||
|
|
2541706ac3 | ||
|
|
44b935e8f6 | ||
|
|
ea1ce0601a | ||
|
|
2989ecf126 | ||
|
|
ce94bf38d1 | ||
|
|
13bc185926 | ||
|
|
51cfd3e2eb | ||
|
|
c22cb3b35c | ||
|
|
621a2304e7 | ||
|
|
729337cdc5 | ||
|
|
4ef58b612f | ||
|
|
ba5d32244f | ||
|
|
efc2a194a3 | ||
|
|
6160c2e664 | ||
|
|
fbd1e9a38a | ||
|
|
17928cbc57 | ||
|
|
e7fc403c77 | ||
|
|
3f7ed95fdb | ||
|
|
7478f193f9 | ||
|
|
ba49d94bf4 | ||
|
|
ea9851ccd3 | ||
|
|
ec55f33655 | ||
|
|
7a85c714cb | ||
|
|
7b90d7f8de | ||
|
|
16f580c96d | ||
|
|
f1329a46e4 | ||
|
|
ef6df57dc5 | ||
|
|
95e5cd7134 | ||
|
|
9db7aa50a1 | ||
|
|
a9997984d5 | ||
|
|
acb47a17b4 | ||
|
|
9c5f4eaa3d | ||
|
|
361fb52345 | ||
|
|
5993e8d7d0 | ||
|
|
8648f16ed8 | ||
|
|
33aaadba60 | ||
|
|
437d87f19e | ||
|
|
dfcc8c2158 | ||
|
|
51a583fc1e | ||
|
|
396428104a | ||
|
|
ff2c25ff75 | ||
|
|
4f78bcb77c | ||
|
|
f62f59720d | ||
|
|
f296490569 |
2
.env
@@ -3,4 +3,4 @@ VITE_APP_TITLE = 'Vue Naive Admin'
|
|||||||
VITE_PORT = 3100
|
VITE_PORT = 3100
|
||||||
|
|
||||||
# 打包时自动生成CNAME文件,用于配置github pages自定义域名,如不需要可注释或者直接删除
|
# 打包时自动生成CNAME文件,用于配置github pages自定义域名,如不需要可注释或者直接删除
|
||||||
VITE_APP_GLOB_CNAME = 'template.qszone.com'
|
# VITE_APP_GLOB_CNAME = 'template.qszone.com'
|
||||||
16
.env.github
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 自定义域名CNAME
|
||||||
|
# VITE_APP_GLOB_CNAME = 'template.qszone.com'
|
||||||
|
|
||||||
|
# 资源公共路径,需要以 /开头和结尾
|
||||||
|
VITE_PUBLIC_PATH = '/vue-naive-admin/'
|
||||||
|
|
||||||
|
VITE_APP_USE_HASH = true
|
||||||
|
|
||||||
|
# 是否启用MOCK
|
||||||
|
VITE_APP_USE_MOCK = true
|
||||||
|
|
||||||
|
# base api
|
||||||
|
VITE_APP_GLOB_BASE_API = '/api'
|
||||||
|
|
||||||
|
# test base api
|
||||||
|
VITE_APP_GLOB_BASE_API_TEST = '/api-test'
|
||||||
11
.env.staging
@@ -1,11 +0,0 @@
|
|||||||
# 资源公共路径,需要以 /开头和结尾
|
|
||||||
VITE_PUBLIC_PATH = '/'
|
|
||||||
|
|
||||||
# 是否启用MOCK
|
|
||||||
VITE_APP_USE_MOCK = false
|
|
||||||
|
|
||||||
# base api
|
|
||||||
VITE_APP_GLOB_BASE_API = 'http://localhost:8080/api'
|
|
||||||
|
|
||||||
# test base api
|
|
||||||
VITE_APP_GLOB_BASE_API_TEST = 'http://localhost:8080/api-test'
|
|
||||||
38
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: use Node.js 16
|
||||||
|
uses: actions/setup-node@v2.1.2
|
||||||
|
with:
|
||||||
|
node-version: '16.x'
|
||||||
|
|
||||||
|
- name: use pnpm 6.32.2
|
||||||
|
uses: pnpm/action-setup@v2.2.1
|
||||||
|
with:
|
||||||
|
version: 6.32.2
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
pnpm install
|
||||||
|
pnpm run build:github
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
|
with:
|
||||||
|
publish_dir: ./dist
|
||||||
|
github_token: ${{ secrets.ACTIONS_DEPLOY_KEY }}
|
||||||
|
user_name: ${{ secrets.USER_NAME }}
|
||||||
|
user_email: ${{ secrets.USER_EMAIL }}
|
||||||
|
force_orphan: true
|
||||||
|
commit_message: deploy gh-pages
|
||||||
4
.vscode/extensions.json
vendored
@@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
"johnsoncodehk.volar",
|
"johnsoncodehk.volar",
|
||||||
"hollowtree.vue-snippets",
|
"hollowtree.vue-snippets",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"mikestead.dotenv",
|
"mikestead.dotenv",
|
||||||
"wayou.vscode-todo-highlight",
|
"wayou.vscode-todo-highlight",
|
||||||
"aaron-bond.better-comments"
|
"aaron-bond.better-comments",
|
||||||
|
"antfu.iconify"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
8
.vscode/settings.json
vendored
@@ -8,7 +8,7 @@
|
|||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"[html]": {
|
"[html]": {
|
||||||
"editor.defaultFormatter": "vscode.html-language-features"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
@@ -19,8 +19,12 @@
|
|||||||
"[vue]": {
|
"[vue]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
|
"[markdown]": {
|
||||||
|
"editor.defaultFormatter": "yzhang.markdown-all-in-one"
|
||||||
|
},
|
||||||
|
"eslint.validate": ["javascript", "javascriptreact", "typescript"],
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": true
|
||||||
},
|
},
|
||||||
"eslint.validate": ["javascript", "javascriptreact", "typescript"]
|
"editor.formatOnSave": true
|
||||||
}
|
}
|
||||||
|
|||||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Ronnie Zhang
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
81
README.md
@@ -1,37 +1,54 @@
|
|||||||
# VUE NAIVE ADMIN
|
<p align="center">
|
||||||
|
<a href="https://github.com/zclzone/vue-naive-admin">
|
||||||
|
<img alt="Vue Naive Admin Logo" width="200" src="https://assets.qszone.com/images/logo_qs.svg">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/zclzone/vue-naive-admin/actions"><img allt="checks" src="https://badgen.net/github/checks/zclzone/vue-naive-admin"/></a>
|
||||||
|
<a href="https://github.com/zclzone/vue-naive-admin"><img allt="stars" src="https://badgen.net/github/stars/zclzone/vue-naive-admin"/></a>
|
||||||
|
<a href="https://github.com/zclzone/vue-naive-admin"><img allt="forks" src="https://badgen.net/github/forks/zclzone/vue-naive-admin"/></a>
|
||||||
|
<a href="https://github.com/zclzone/vue-naive-admin/releases"><img allt="releases" src="https://badgen.net/github/releases/zclzone/vue-naive-admin"/></a>
|
||||||
|
<a href="./LICENSE"><img allt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
## 简介
|
|
||||||
|
|
||||||
Vue Naive Admin,一个基于 Vue3.0、Vite、Naive UI 的轻量级后台管理模板,没有集成 TypeScript,没有集成国际化,没有集成复杂的主题配置,上手成本非常低,对新手极其友好。不过麻雀虽小五脏俱全,权限、Mock、菜单、axios 封装、pinia、项目配置、样式配置、环境配置,以及一些经常用的基础组件封装等等这些该有的都有,参考多个 vue3 后台管理模板后以最简洁优雅的方式实现,非常适用于中小型项目或者个人项目。
|
### 简介
|
||||||
|
|
||||||
## 为什么要开发这个模板
|
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin),一个基于 Vue3.0、Vite、Naive UI 的后台管理模板,相较于其他比较流行的后台管理模板,此项目相对简洁、轻量,学习成本非常低,对新手极其友好。不过麻雀虽小五脏俱全,权限、Mock、菜单、axios 封装、pinia、项目配置、样式配置、环境配置,以及一些经常用的基础组件封装等等这些该有的都有,非常适用于中小型项目或者个人项目,也可此模板进行二次封装改造用于大型项目。
|
||||||
|
|
||||||
1. Vue3 和 Vite 已经趋于成熟,学习 vite 和 vue3 非常有必要,通过开发模板进行学习是一个很好的方式,事实也证明我确实从中获益良多
|
### 为什么要开发这个模板
|
||||||
2. 目前主流的 Vue3+Vite 后台管理模板都相对复杂,甚至感觉有点花里胡哨(没有贬低的意思,大部分的架构设计都很优秀,只是觉得集成了太多不实用的东西)
|
|
||||||
3. 自己搭的模板开发起来才最顺手。本人很反感拿别人的模板直接上手开发,如果非要拿别人的模板开发也会尽量先吃透再用,不吃透就没有代码的掌控感和安全感
|
|
||||||
|
|
||||||
## 功能
|
- Vue3 和 Vite 已经趋于成熟,学习 vite 和 vue3 非常有必要,通过开发模板进行学习是一个很好的方式,事实也证明我确实从中获益良多
|
||||||
|
- 目前主流的 Vue3+Vite 后台管理模板都相对复杂,甚至感觉有点花里胡哨(没有贬低的意思,大部分的架构设计都很优秀,只是觉得集成了太多不实用的东西)
|
||||||
|
|
||||||
- 🍒 集成 Naive UI,尤大推荐的 UI 组件库,很香,https://www.naiveui.com
|
### 功能
|
||||||
- 🍑 集成登陆、注销及权限验证(暂只支持角色页面权限,后续考虑添加按钮权限)
|
|
||||||
- 🍐 集成多环境配置,dev、测试、预发布和生产
|
- 🍒 集成 Naive UI,尤大推荐的 UI 组件库,[https://www.naiveui.com](https://www.naiveui.com)
|
||||||
- 🍎 集成 eslint + prettier,代码约束和格式化统一
|
- 🍑 集成登陆、注销及权限验证
|
||||||
|
- 🍐 集成多环境配置,dev、测试、生产和github pages环境
|
||||||
|
- 🍎 集成 Eslint + Prettier,代码约束和格式化统一
|
||||||
- 🍉 集成 Mock 接口服务,dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
|
- 🍉 集成 Mock 接口服务,dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
|
||||||
- 🍇 集成 unocss,antfu 大神开源的原子化 css 解决方案,非常轻量,目前我是自己写 scss 样式搭配着 unocss 使用的,很香
|
- 🍇 集成 unocss,antfu 大神开源的原子化 css 解决方案,非常轻量,目前我是自己写 scss 样式搭配着 unocss 使用的
|
||||||
- 🍍 集成 pinia,Vuex 的替代方案,轻量、简单、易用,很香
|
- 🍍 集成 Pinia,Vuex 的替代方案,轻量、简单、易用(尤大已表示不会有Vuex5,或者说pinia就是Vuex5)
|
||||||
- 🍏 集成 axios,支持多 axios 实例,支持线上环境免重新打包修改 baseURL
|
- 📦 集成 Vite 自动导入插件unplugin-vue-components,解放双手,开发效率直接起飞
|
||||||
|
- 🤹 集成 unplugin-icons插件,优雅使用iconify图标
|
||||||
|
- 🍏 二次封装 Axios,支持多 axios 实例,支持线上环境免重新打包修改 baseURL
|
||||||
- 🍌 二次封装全局 Dialog、Message、LoadingBar 组件
|
- 🍌 二次封装全局 Dialog、Message、LoadingBar 组件
|
||||||
- 🍋 二次封装 localStorage 和 sessionStorage,支持设置过期时间
|
- 🍋 二次封装 localStorage 和 sessionStorage,支持设置过期时间
|
||||||
|
|
||||||
## 预览
|
### 预览
|
||||||
|
|
||||||
[template.qszone.com](https://template.qszone.com)
|
[template.qszone.com](https://template.qszone.com)
|
||||||
|
|
||||||
## 文档
|
[github pages](https://zclzone.github.io/vue-naive-admin)
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
|
||||||
|
[Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
|
||||||
|
|
||||||
[羽雀文档:Vue Naive Admin](https://www.yuque.com/qszone/vue-naive-admin)
|
[羽雀文档:Vue Naive Admin](https://www.yuque.com/qszone/vue-naive-admin)
|
||||||
|
|
||||||
## 构建步骤
|
### 构建
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# 推荐配置git autocrlf 为 false(本项目规范使用lf换行符,此配置是为防止git自动将源文件转换为crlf)
|
# 推荐配置git autocrlf 为 false(本项目规范使用lf换行符,此配置是为防止git自动将源文件转换为crlf)
|
||||||
@@ -51,20 +68,20 @@ pnpm i # 或者 npm i
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## 发布
|
### 发布
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# 构建测试环境
|
# 构建测试环境
|
||||||
npm run build:test
|
npm run build:test
|
||||||
|
|
||||||
# 构建预发布环境
|
# 构建github pages环境
|
||||||
npm run build:staging
|
npm run build:github
|
||||||
|
|
||||||
# 构建生产环境
|
# 构建生产环境
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
## 其他指令
|
### 其他指令
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# eslint代码格式检查
|
# eslint代码格式检查
|
||||||
@@ -76,3 +93,23 @@ npm run lint:fix
|
|||||||
# 预览发布包效果(需先执行构建指令)
|
# 预览发布包效果(需先执行构建指令)
|
||||||
npm run preview
|
npm run preview
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 规范
|
||||||
|
|
||||||
|
#### git commit 规范
|
||||||
|
|
||||||
|
- `feat` 增加新功能
|
||||||
|
- `fix` 修复问题/BUG
|
||||||
|
- `style` 代码风格相关无影响运行结果的
|
||||||
|
- `perf` 优化/性能提升
|
||||||
|
- `refactor` 重构
|
||||||
|
- `revert` 撤销修改
|
||||||
|
- `test` 测试相关
|
||||||
|
- `docs` 文档/注释
|
||||||
|
- `chore` 依赖更新/脚手架配置修改等
|
||||||
|
- `workflow` 工作流改进
|
||||||
|
- `ci` 持续集成
|
||||||
|
- `types` 类型定义文件更改
|
||||||
|
- `wip` 开发中
|
||||||
|
- `mod` 不确定分类的修改
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * 扩展setup插件,支持在script标签中使用name属性
|
||||||
|
* usage: <script setup name="MyComp"></script>
|
||||||
|
*/
|
||||||
|
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * 组件库按需引入插件
|
||||||
|
* usage: 直接使用组件,无需在任何地方导入组件
|
||||||
|
*/
|
||||||
import Components from 'unplugin-vue-components/vite'
|
import Components from 'unplugin-vue-components/vite'
|
||||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
|
||||||
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
|
/**
|
||||||
|
* * unplugin-icons插件,自动引入iconify图标
|
||||||
|
* usage: https://github.com/antfu/unplugin-icons
|
||||||
|
* 图标库: https://icones.js.org/
|
||||||
|
*/
|
||||||
|
import Icons from 'unplugin-icons/vite'
|
||||||
|
|
||||||
import { unocss } from './unocss'
|
import { unocss } from './unocss'
|
||||||
import { configHtmlPlugin } from './html'
|
import { configHtmlPlugin } from './html'
|
||||||
@@ -12,10 +27,11 @@ import { configMockPlugin } from './mock'
|
|||||||
export function createVitePlugins(viteEnv, isBuild) {
|
export function createVitePlugins(viteEnv, isBuild) {
|
||||||
const plugins = [
|
const plugins = [
|
||||||
vue(),
|
vue(),
|
||||||
|
VueSetupExtend(),
|
||||||
Components({
|
Components({
|
||||||
resolvers: [NaiveUiResolver()],
|
resolvers: [NaiveUiResolver()],
|
||||||
}),
|
}),
|
||||||
VueSetupExtend(),
|
Icons({ compiler: 'vue3', autoInstall: true }),
|
||||||
unocss(),
|
unocss(),
|
||||||
configHtmlPlugin(viteEnv, isBuild),
|
configHtmlPlugin(viteEnv, isBuild),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function configMockPlugin(isBuild) {
|
|||||||
localEnabled: !isBuild,
|
localEnabled: !isBuild,
|
||||||
prodEnabled: isBuild,
|
prodEnabled: isBuild,
|
||||||
injectCode: `
|
injectCode: `
|
||||||
import { setupProdMockServer } from '../mock/_createProdServer';
|
import { setupProdMockServer } from '../mock/_create-prod-server';
|
||||||
setupProdMockServer();
|
setupProdMockServer();
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
|
|||||||
32
index.html
@@ -1,22 +1,18 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="cn">
|
<html lang="cn">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<meta http-equiv="Pragma" content="no-cache" />
|
||||||
|
<meta http-equiv="Cache-control" content="no-cache" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" href="/favicon.svg" />
|
||||||
|
<title><%= title %></title>
|
||||||
|
</head>
|
||||||
|
|
||||||
<head>
|
<body>
|
||||||
<meta charset="UTF-8" />
|
<div id="app"></div>
|
||||||
<meta http-equiv="Expires" content="0" />
|
<script type="module" src="/src/main.js"></script>
|
||||||
<meta http-equiv="Pragma" content="no-cache" />
|
</body>
|
||||||
<meta http-equiv="Cache-control" content="no-cache" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<title>
|
|
||||||
<%= title %>
|
|
||||||
</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -4,21 +4,21 @@ const users = {
|
|||||||
admin: {
|
admin: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: '大脸怪(admin)',
|
name: '大脸怪(admin)',
|
||||||
avatar: 'https://gitee.com/zclzone/res/raw/master/qs-zone/blob/img/lADPDiQ3QDTwsz3NAarNAaw_428_426.jpg',
|
avatar: 'https://assets.qszone.com/images/avatar.jpg',
|
||||||
email: 'Ronnie@123.com',
|
email: 'Ronnie@123.com',
|
||||||
role: ['admin'],
|
role: ['admin'],
|
||||||
},
|
},
|
||||||
editor: {
|
editor: {
|
||||||
id: 2,
|
id: 2,
|
||||||
name: '大脸怪(editor)',
|
name: '大脸怪(editor)',
|
||||||
avatar: 'https://gitee.com/zclzone/res/raw/master/qs-zone/blob/img/lADPDiQ3QDTwsz3NAarNAaw_428_426.jpg',
|
avatar: 'https://assets.qszone.com/images/avatar.jpg',
|
||||||
email: 'Ronnie@123.com',
|
email: 'Ronnie@123.com',
|
||||||
role: ['editor'],
|
role: ['editor'],
|
||||||
},
|
},
|
||||||
guest: {
|
guest: {
|
||||||
id: 3,
|
id: 3,
|
||||||
name: '访客(guest)',
|
name: '访客(guest)',
|
||||||
avatar: 'https://gitee.com/zclzone/res/raw/master/qs-zone/blob/img/lADPDiQ3QDTwsz3NAarNAaw_428_426.jpg',
|
avatar: 'https://assets.qszone.com/images/avatar.jpg',
|
||||||
role: [],
|
role: [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
40
package.json
@@ -7,41 +7,43 @@
|
|||||||
"lint:fix": "eslint --fix --ext .js,.vue .",
|
"lint:fix": "eslint --fix --ext .js,.vue .",
|
||||||
"build": "vite build && esno ./build/script",
|
"build": "vite build && esno ./build/script",
|
||||||
"build:test": "vite build --mode test && esno ./build/script",
|
"build:test": "vite build --mode test && esno ./build/script",
|
||||||
"build:staging": "vite build --mode staging && esno ./build/script",
|
"build:github": "vite build --mode github && esno ./build/script",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vicons/fa": "^0.11.0",
|
|
||||||
"axios": "^0.21.4",
|
"axios": "^0.21.4",
|
||||||
"dayjs": "^1.10.7",
|
"dayjs": "^1.11.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"md-editor-v3": "^1.10.2",
|
"md-editor-v3": "^1.11.4",
|
||||||
"mockjs": "^1.1.0",
|
"mockjs": "^1.1.0",
|
||||||
"pinia": "^2.0.11",
|
"pinia": "^2.0.13",
|
||||||
"vue": "^3.2.30",
|
"vue": "^3.2.31",
|
||||||
"vue-router": "^4.0.12"
|
"vue-router": "^4.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/mdi": "^1.1.9",
|
||||||
|
"@iconify-json/simple-icons": "^1.1.7",
|
||||||
"@unocss/preset-attributify": "^0.16.4",
|
"@unocss/preset-attributify": "^0.16.4",
|
||||||
"@unocss/preset-icons": "^0.16.4",
|
"@unocss/preset-icons": "^0.16.4",
|
||||||
"@unocss/preset-uno": "^0.16.4",
|
"@unocss/preset-uno": "^0.16.4",
|
||||||
"@vitejs/plugin-vue": "^1.10.2",
|
"@vitejs/plugin-vue": "^1.10.2",
|
||||||
"@vue/compiler-sfc": "^3.2.30",
|
"@vue/compiler-sfc": "^3.2.31",
|
||||||
"chalk": "^5.0.0",
|
"chalk": "^5.0.1",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"eslint": "^8.6.0",
|
"eslint": "^8.12.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-plugin-vue": "^8.2.0",
|
"eslint-plugin-vue": "^8.5.0",
|
||||||
"esno": "^0.13.0",
|
"esno": "^0.13.0",
|
||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.0.1",
|
||||||
"naive-ui": "^2.25.2",
|
"naive-ui": "^2.27.0",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.6.1",
|
||||||
"sass": "^1.38.1",
|
"sass": "^1.49.10",
|
||||||
"unocss": "^0.16.4",
|
"unocss": "^0.16.4",
|
||||||
"unplugin-vue-components": "^0.17.18",
|
"unplugin-icons": "^0.14.1",
|
||||||
"vite": "^2.8.0",
|
"unplugin-vue-components": "^0.17.21",
|
||||||
"vite-plugin-html": "^2.1.1",
|
"vite": "^2.9.1",
|
||||||
|
"vite-plugin-html": "^2.1.2",
|
||||||
"vite-plugin-mock": "^2.9.6",
|
"vite-plugin-mock": "^2.9.6",
|
||||||
"vite-plugin-vue-setup-extend": "^0.3.0"
|
"vite-plugin-vue-setup-extend": "^0.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
916
pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 2.0 KiB |
1
public/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 448 512" data-v-fba6e5d0=""><path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-92.2 312.9c-63.4 0-85.4-28.6-97.1-64.1c-16.3-51-21.5-84.3-63-84.3c-22.4 0-45.1 16.1-45.1 61.2c0 35.2 18 57.2 43.3 57.2c28.6 0 47.6-21.3 47.6-21.3l11.7 31.9s-19.8 19.4-61.2 19.4c-51.3 0-79.9-30.1-79.9-85.8c0-57.9 28.6-92 82.5-92c73.5 0 80.8 41.4 100.8 101.9c8.8 26.8 24.2 46.2 61.2 46.2c24.9 0 38.1-5.5 38.1-19.1c0-19.9-21.8-22-49.9-28.6c-30.4-7.3-42.5-23.1-42.5-48c0-40 32.3-52.4 65.2-52.4c37.4 0 60.1 13.6 63 46.6l-36.7 4.4c-1.5-15.8-11-22.4-28.6-22.4c-16.1 0-26 7.3-26 19.8c0 11 4.8 17.6 20.9 21.3c32.7 7.1 71.8 12 71.8 57.5c.1 36.7-30.7 50.6-76.1 50.6z" fill="#316c72"></path></svg>
|
||||||
|
After Width: | Height: | Size: 825 B |
12
src/App.vue
@@ -1,15 +1,15 @@
|
|||||||
<script setup>
|
|
||||||
import AppProvider from '@/components/AppProvider/index.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<app-provider>
|
<AppProvider>
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
</router-view>
|
</router-view>
|
||||||
</app-provider>
|
</AppProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import AppProvider from '@/components/AppProvider/index.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
#app {
|
#app {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
BIN
src/assets/images/404.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
src/assets/images/login_banner.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
src/assets/images/login_bg.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
1
src/assets/images/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 448 512" data-v-fba6e5d0=""><path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-92.2 312.9c-63.4 0-85.4-28.6-97.1-64.1c-16.3-51-21.5-84.3-63-84.3c-22.4 0-45.1 16.1-45.1 61.2c0 35.2 18 57.2 43.3 57.2c28.6 0 47.6-21.3 47.6-21.3l11.7 31.9s-19.8 19.4-61.2 19.4c-51.3 0-79.9-30.1-79.9-85.8c0-57.9 28.6-92 82.5-92c73.5 0 80.8 41.4 100.8 101.9c8.8 26.8 24.2 46.2 61.2 46.2c24.9 0 38.1-5.5 38.1-19.1c0-19.9-21.8-22-49.9-28.6c-30.4-7.3-42.5-23.1-42.5-48c0-40 32.3-52.4 65.2-52.4c37.4 0 60.1 13.6 63 46.6l-36.7 4.4c-1.5-15.8-11-22.4-28.6-22.4c-16.1 0-26 7.3-26 19.8c0 11 4.8 17.6 20.9 21.3c32.7 7.1 71.8 12 71.8 57.5c.1 36.7-30.7 50.6-76.1 50.6z" fill="#316c72"></path></svg>
|
||||||
|
After Width: | Height: | Size: 825 B |
|
Before Width: | Height: | Size: 59 KiB |
9
src/components/AppIcons/IconLogo.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 448 512">
|
||||||
|
<path
|
||||||
|
d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-92.2 312.9c-63.4 0-85.4-28.6-97.1-64.1c-16.3-51-21.5-84.3-63-84.3c-22.4 0-45.1 16.1-45.1 61.2c0 35.2 18 57.2 43.3 57.2c28.6 0 47.6-21.3 47.6-21.3l11.7 31.9s-19.8 19.4-61.2 19.4c-51.3 0-79.9-30.1-79.9-85.8c0-57.9 28.6-92 82.5-92c73.5 0 80.8 41.4 100.8 101.9c8.8 26.8 24.2 46.2 61.2 46.2c24.9 0 38.1-5.5 38.1-19.1c0-19.9-21.8-22-49.9-28.6c-30.4-7.3-42.5-23.1-42.5-48c0-40 32.3-52.4 65.2-52.4c37.4 0 60.1 13.6 63 46.6l-36.7 4.4c-1.5-15.8-11-22.4-28.6-22.4c-16.1 0-26 7.3-26 19.8c0 11 4.8 17.6 20.9 21.3c32.7 7.1 71.8 12 71.8 57.5c.1 36.7-30.7 50.6-76.1 50.6z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="IconLogo"></script>
|
||||||
12
src/components/AppIcons/index.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export { default as IconGitee } from '~icons/simple-icons/gitee'
|
||||||
|
|
||||||
|
export { default as IconChart } from '~icons/mdi/chart-bar'
|
||||||
|
export { default as IconGithub } from '~icons/mdi/github'
|
||||||
|
export { default as IconVue } from '~icons/mdi/vuejs'
|
||||||
|
export { default as IconHome } from '~icons/mdi/home'
|
||||||
|
export { default as IconLink } from '~icons/mdi/link-variant'
|
||||||
|
export { default as IconAlert } from '~icons/mdi/alert-circle-outline'
|
||||||
|
export { default as IconCircle } from '~icons/mdi/circle-outline'
|
||||||
|
export { default as IconMenu } from '~icons/mdi/menu'
|
||||||
|
|
||||||
|
export { default as IconLogo } from './IconLogo.vue'
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<template></template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { isNullOrUndef } from '@/utils/is'
|
import { isNullOrUndef } from '@/utils/is'
|
||||||
import { useDialog } from 'naive-ui'
|
import { useDialog } from 'naive-ui'
|
||||||
@@ -6,15 +8,15 @@ const NDialog = useDialog()
|
|||||||
|
|
||||||
class Dialog {
|
class Dialog {
|
||||||
success(title, option) {
|
success(title, option) {
|
||||||
this.showDialog('success', { title, ...option })
|
return this.showDialog('success', { title, ...option })
|
||||||
}
|
}
|
||||||
|
|
||||||
warning(title, option) {
|
warning(title, option) {
|
||||||
this.showDialog('warning', { title, ...option })
|
return this.showDialog('warning', { title, ...option })
|
||||||
}
|
}
|
||||||
|
|
||||||
error(title, option) {
|
error(title, option) {
|
||||||
this.showDialog('error', { title, ...option })
|
return this.showDialog('error', { title, ...option })
|
||||||
}
|
}
|
||||||
|
|
||||||
showDialog(type = 'success', option) {
|
showDialog(type = 'success', option) {
|
||||||
@@ -22,7 +24,7 @@ class Dialog {
|
|||||||
// ! 没有title的情况
|
// ! 没有title的情况
|
||||||
option.showIcon = false
|
option.showIcon = false
|
||||||
}
|
}
|
||||||
NDialog[type]({
|
return NDialog[type]({
|
||||||
positiveText: 'OK',
|
positiveText: 'OK',
|
||||||
closable: false,
|
closable: false,
|
||||||
...option,
|
...option,
|
||||||
@@ -30,7 +32,7 @@ class Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
confirm(option = {}) {
|
confirm(option = {}) {
|
||||||
this.showDialog(option.type || 'error', {
|
return this.showDialog(option.type || 'error', {
|
||||||
positiveText: '确定',
|
positiveText: '确定',
|
||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
onPositiveClick: option.confirm,
|
onPositiveClick: option.confirm,
|
||||||
@@ -48,5 +50,3 @@ Object.defineProperty(window, '$dialog', {
|
|||||||
writable: false,
|
writable: false,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template></template>
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<template></template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useLoadingBar } from 'naive-ui'
|
import { useLoadingBar } from 'naive-ui'
|
||||||
window['$loadingBar'] = useLoadingBar()
|
window['$loadingBar'] = useLoadingBar()
|
||||||
@@ -6,5 +8,3 @@ Object.defineProperty(window, '$loadingBar', {
|
|||||||
writable: false,
|
writable: false,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template></template>
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<template></template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useMessage } from 'naive-ui'
|
import { useMessage } from 'naive-ui'
|
||||||
|
|
||||||
@@ -68,5 +70,3 @@ Object.defineProperty(window, '$message', {
|
|||||||
writable: false,
|
writable: false,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template></template>
|
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<script setup>
|
|
||||||
import MessageContent from './MessageContent.vue'
|
|
||||||
import DialogContent from './DialogContent.vue'
|
|
||||||
import LoadingBar from './LoadingBar.vue'
|
|
||||||
|
|
||||||
import { useAppStore } from '@/store/modules/app'
|
|
||||||
const appStore = useAppStore()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-config-provider :theme-overrides="appStore.themeOverrides">
|
<n-config-provider :theme-overrides="useTheme.naiveThemeOverrides">
|
||||||
<n-loading-bar-provider>
|
<n-loading-bar-provider>
|
||||||
<loading-bar />
|
<LoadingBar />
|
||||||
<n-dialog-provider>
|
<n-dialog-provider>
|
||||||
<dialog-content />
|
<DialogContent />
|
||||||
<n-message-provider>
|
<n-message-provider>
|
||||||
<message-content />
|
<MessageContent />
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</n-message-provider>
|
</n-message-provider>
|
||||||
</n-dialog-provider>
|
</n-dialog-provider>
|
||||||
</n-loading-bar-provider>
|
</n-loading-bar-provider>
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import MessageContent from './MessageContent.vue'
|
||||||
|
import DialogContent from './DialogContent.vue'
|
||||||
|
import LoadingBar from './LoadingBar.vue'
|
||||||
|
import { useThemeStore } from '@/store/modules/theme'
|
||||||
|
|
||||||
|
const useTheme = useThemeStore()
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<n-breadcrumb>
|
||||||
|
<n-breadcrumb-item v-for="item in currentRoute.matched" :key="item.path" @click="handleBreadClick(item.path)">
|
||||||
|
{{ item.meta.title }}
|
||||||
|
</n-breadcrumb-item>
|
||||||
|
</n-breadcrumb>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -8,11 +16,3 @@ function handleBreadClick(path) {
|
|||||||
router.push(path)
|
router.push(path)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-breadcrumb>
|
|
||||||
<n-breadcrumb-item v-for="item in currentRoute.matched" :key="item.path" @click="handleBreadClick(item.path)">
|
|
||||||
{{ item.meta.title }}
|
|
||||||
</n-breadcrumb-item>
|
|
||||||
</n-breadcrumb>
|
|
||||||
</template>
|
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<n-dropdown :options="options" @select="handleSelect">
|
||||||
|
<div class="avatar">
|
||||||
|
<img :src="userStore.avatar" />
|
||||||
|
<span>{{ userStore.name }}</span>
|
||||||
|
</div>
|
||||||
|
</n-dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@@ -40,21 +49,21 @@ function switchRole() {
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: '大脸怪(admin)',
|
name: '大脸怪(admin)',
|
||||||
avatar: 'https://gitee.com/zclzone/res/raw/master/qs-zone/blob/img/lADPDiQ3QDTwsz3NAarNAaw_428_426.jpg',
|
avatar: 'https://assets.qszone.com/images/avatar.jpg',
|
||||||
email: 'Ronnie@123.com',
|
email: 'Ronnie@123.com',
|
||||||
role: ['admin'],
|
role: ['admin'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: '大脸怪(editor)',
|
name: '大脸怪(editor)',
|
||||||
avatar: 'https://gitee.com/zclzone/res/raw/master/qs-zone/blob/img/lADPDiQ3QDTwsz3NAarNAaw_428_426.jpg',
|
avatar: 'https://assets.qszone.com/images/avatar.jpg',
|
||||||
email: 'Ronnie@123.com',
|
email: 'Ronnie@123.com',
|
||||||
role: ['editor'],
|
role: ['editor'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
name: '访客(guest)',
|
name: '访客(guest)',
|
||||||
avatar: 'https://gitee.com/zclzone/res/raw/master/qs-zone/blob/img/lADPDiQ3QDTwsz3NAarNAaw_428_426.jpg',
|
avatar: 'https://assets.qszone.com/images/avatar.jpg',
|
||||||
role: [],
|
role: [],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -71,15 +80,6 @@ function switchRole() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-dropdown :options="options" @select="handleSelect">
|
|
||||||
<div class="avatar">
|
|
||||||
<img :src="userStore.avatar" />
|
|
||||||
<span>{{ userStore.name }}</span>
|
|
||||||
</div>
|
|
||||||
</n-dropdown>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.avatar {
|
.avatar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<header class="header">
|
||||||
|
<BreadCrumb />
|
||||||
|
<HeaderAction />
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import BreadCrumb from './BreadCrumb.vue'
|
import BreadCrumb from './BreadCrumb.vue'
|
||||||
import HeaderAction from './HeaderAction.vue'
|
import HeaderAction from './HeaderAction.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<header class="header">
|
|
||||||
<bread-crumb />
|
|
||||||
<header-action />
|
|
||||||
</header>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.header {
|
.header {
|
||||||
padding: 0 35px;
|
padding: 0 24px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
<script setup>
|
|
||||||
import { LastfmSquare } from '@vicons/fa'
|
|
||||||
const title = import.meta.env.VITE_APP_TITLE
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<n-icon size="36" color="#316c72">
|
<n-icon size="36" color="#316c72">
|
||||||
<lastfm-square />
|
<IconLogo />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
<router-link to="/">
|
<router-link to="/">
|
||||||
<n-gradient-text type="primary">{{ title }}</n-gradient-text>
|
<n-gradient-text type="primary">{{ title }}</n-gradient-text>
|
||||||
@@ -14,6 +9,11 @@ const title = import.meta.env.VITE_APP_TITLE
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { IconLogo } from '@/components/AppIcons'
|
||||||
|
const title = import.meta.env.VITE_APP_TITLE
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.logo {
|
.logo {
|
||||||
height: 64px;
|
height: 64px;
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<n-menu
|
||||||
|
class="side-menu"
|
||||||
|
accordion
|
||||||
|
:indent="12"
|
||||||
|
:root-indent="12"
|
||||||
|
:options="menuOptions"
|
||||||
|
:value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name"
|
||||||
|
@update:value="handleMenuSelect"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { computed, h } from 'vue'
|
import { computed, h } from 'vue'
|
||||||
import { usePermissionStore } from '@/store/modules/permission'
|
import { usePermissionStore } from '@/store/modules/permission'
|
||||||
|
|
||||||
import { NIcon } from 'naive-ui'
|
import { NIcon } from 'naive-ui'
|
||||||
import { ListAlt, CircleRegular } from '@vicons/fa'
|
import { IconCircle, IconMenu } from '@/components/AppIcons'
|
||||||
|
|
||||||
import { isExternal } from '@/utils/is'
|
import { isExternal } from '@/utils/is'
|
||||||
|
|
||||||
@@ -56,10 +68,10 @@ function generateOptions(routes, basePath) {
|
|||||||
path: resolvePath(basePath, route.path),
|
path: resolvePath(basePath, route.path),
|
||||||
}
|
}
|
||||||
if (route.children && route.children.length) {
|
if (route.children && route.children.length) {
|
||||||
curOption.icon = renderIcon(route.meta?.icon || ListAlt, { size: 16 })
|
curOption.icon = renderIcon(route.meta?.icon || IconMenu, { size: 16 })
|
||||||
curOption.children = generateOptions(route.children, resolvePath(basePath, route.path))
|
curOption.children = generateOptions(route.children, resolvePath(basePath, route.path))
|
||||||
} else {
|
} else {
|
||||||
curOption.icon = (route.meta?.icon && renderIcon(route.meta?.icon)) || renderIcon(CircleRegular, { size: 8 })
|
curOption.icon = (route.meta?.icon && renderIcon(route.meta?.icon)) || renderIcon(IconCircle, { size: 8 })
|
||||||
}
|
}
|
||||||
options.push(curOption)
|
options.push(curOption)
|
||||||
}
|
}
|
||||||
@@ -82,34 +94,19 @@ function handleMenuSelect(key, item) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-menu
|
|
||||||
class="side-menu"
|
|
||||||
accordion
|
|
||||||
:indent="12"
|
|
||||||
:root-indent="12"
|
|
||||||
:options="menuOptions"
|
|
||||||
:value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name"
|
|
||||||
@update:value="handleMenuSelect"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.n-menu {
|
.n-menu {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
.n-menu-item {
|
.n-menu-item-content {
|
||||||
margin-top: 0;
|
|
||||||
position: relative;
|
|
||||||
&::before {
|
&::before {
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background-color: unset !important;
|
background-color: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&.n-menu-item--selected {
|
&.n-menu-item-content--selected {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ import SideMenu from './SideMenu.vue'
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<side-logo />
|
<SideLogo />
|
||||||
<side-menu />
|
<SideMenu />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
87
src/layout/components/tags/index.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tags-wrapper" :style="{ height: useTheme.tags.height + 'px' }">
|
||||||
|
<n-space>
|
||||||
|
<n-tag
|
||||||
|
v-for="tag in useTag.tags"
|
||||||
|
:key="tag.path"
|
||||||
|
:type="useTag.activeTag === tag.path ? 'primary' : 'default'"
|
||||||
|
:closable="useTag.tags.length > 1"
|
||||||
|
@click="handleTagClick(tag.path)"
|
||||||
|
@close.stop="handleClose(tag.path)"
|
||||||
|
>
|
||||||
|
{{ tag.title }}
|
||||||
|
</n-tag>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="Tags">
|
||||||
|
import { watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useTagStore } from '@/store/modules/tag'
|
||||||
|
import { useThemeStore } from '@/store/modules/theme'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const useTag = useTagStore()
|
||||||
|
const useTheme = useThemeStore()
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
() => {
|
||||||
|
const { name, path } = route
|
||||||
|
const title = route.meta?.title
|
||||||
|
useTag.addTag({ name, path, title })
|
||||||
|
useTag.setActiveTag(path)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleTagClick = (path) => {
|
||||||
|
useTag.setActiveTag(path)
|
||||||
|
router.push(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = (path) => {
|
||||||
|
if (path === useTag.activeTag) {
|
||||||
|
const activeIndex = useTag.tags.findIndex((item) => item.path === path)
|
||||||
|
if (activeIndex > 0) {
|
||||||
|
router.push(useTag.tags[activeIndex - 1].path)
|
||||||
|
} else {
|
||||||
|
router.push(useTag.tags[activeIndex + 1].path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useTag.removeTag(path)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.tags-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 0 10px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 9;
|
||||||
|
.n-tag {
|
||||||
|
padding: 0 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
.n-tag__close {
|
||||||
|
margin-left: 5px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.7s;
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: $primaryColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: $primaryColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,28 +1,43 @@
|
|||||||
<script setup>
|
|
||||||
import AppHeader from './components/header/index.vue'
|
|
||||||
import SideMenu from './components/sidebar/index.vue'
|
|
||||||
import AppMain from './components/AppMain.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<n-layout has-sider position="absolute">
|
<n-layout has-sider position="absolute">
|
||||||
<n-layout-sider :width="200" :collapsed-width="0" :native-scrollbar="false">
|
<n-layout-sider bordered :width="200" :collapsed-width="0" :native-scrollbar="false">
|
||||||
<side-menu />
|
<SideBar />
|
||||||
</n-layout-sider>
|
</n-layout-sider>
|
||||||
<n-layout>
|
<n-layout>
|
||||||
<n-layout-header style="height: 100px; background-color: #f5f6fb">
|
<n-layout-header :style="{ height: useTheme.header.height + 'px' }" style="border-left: none">
|
||||||
<app-header />
|
<AppHeader />
|
||||||
</n-layout-header>
|
</n-layout-header>
|
||||||
|
|
||||||
<n-layout
|
<n-layout
|
||||||
position="absolute"
|
position="absolute"
|
||||||
content-style="padding: 0 35px 35px;height: 100%;"
|
style="background-color: #f5f6fb"
|
||||||
style="top: 100px; background-color: #f5f6fb"
|
:style="{ top: useTheme.header.height + 'px' }"
|
||||||
:native-scrollbar="false"
|
:native-scrollbar="false"
|
||||||
>
|
>
|
||||||
<app-main />
|
<AppTags v-if="useTheme.tags.visible" />
|
||||||
|
<AppMain />
|
||||||
</n-layout>
|
</n-layout>
|
||||||
</n-layout>
|
</n-layout>
|
||||||
</n-layout>
|
</n-layout>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import AppHeader from './components/header/index.vue'
|
||||||
|
import SideBar from './components/sidebar/index.vue'
|
||||||
|
import AppMain from './components/AppMain.vue'
|
||||||
|
import AppTags from './components/tags/index.vue'
|
||||||
|
import { useThemeStore } from '@/store/modules/theme'
|
||||||
|
|
||||||
|
const useTheme = useThemeStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.n-layout-header {
|
||||||
|
height: 60px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
border-left: 1px solid #eee;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
10
src/main.js
@@ -2,18 +2,18 @@ import '@/styles/index.scss'
|
|||||||
import 'uno.css'
|
import 'uno.css'
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
|
||||||
|
|
||||||
import { setupRouter } from '@/router'
|
import { setupRouter } from '@/router'
|
||||||
import { setupStore } from '@/store'
|
import { setupStore } from '@/store'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
async function bootstrap() {
|
function setupApp() {
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
setupStore(app)
|
setupStore(app)
|
||||||
|
|
||||||
setupRouter(app)
|
setupRouter(app)
|
||||||
|
|
||||||
app.mount('#app', true)
|
app.mount('#app')
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap()
|
setupApp()
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { createPageLoadingGuard } from './pageLoadingGuard'
|
import { createPageLoadingGuard } from './page-loading-guard'
|
||||||
import { createPermissionGuard } from './permissionGuard'
|
import { createPageTitleGuard } from './page-title-guard'
|
||||||
|
import { createPermissionGuard } from './permission-guard'
|
||||||
|
|
||||||
export function setupRouterGuard(router) {
|
export function setupRouterGuard(router) {
|
||||||
createPageLoadingGuard(router)
|
createPageLoadingGuard(router)
|
||||||
createPermissionGuard(router)
|
createPermissionGuard(router)
|
||||||
|
createPageTitleGuard(router)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
export function createPageLoadingGuard(router) {
|
export function createPageLoadingGuard(router) {
|
||||||
router.beforeEach(() => {
|
router.beforeEach(() => {
|
||||||
$loadingBar.start()
|
window.$loadingBar?.start()
|
||||||
})
|
})
|
||||||
|
|
||||||
router.afterEach(() => {
|
router.afterEach(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
$loadingBar.finish()
|
window.$loadingBar?.finish()
|
||||||
}, 200)
|
}, 200)
|
||||||
})
|
})
|
||||||
|
|
||||||
router.onError(() => {
|
router.onError(() => {
|
||||||
$loadingBar.error()
|
window.$loadingBar?.error()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
12
src/router/guard/page-title-guard.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const baseTitle = import.meta.env.VITE_APP_TITLE
|
||||||
|
|
||||||
|
export function createPageTitleGuard(router) {
|
||||||
|
router.afterEach((to) => {
|
||||||
|
const pageTitle = to.meta?.title
|
||||||
|
if (pageTitle) {
|
||||||
|
document.title = `${pageTitle} | ${baseTitle}`
|
||||||
|
} else {
|
||||||
|
document.title = baseTitle
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||||
import { setupRouterGuard } from './guard'
|
import { setupRouterGuard } from './guard'
|
||||||
import { basicRoutes } from './routes'
|
import { basicRoutes } from './routes'
|
||||||
|
|
||||||
|
const isHash = !!import.meta.env.VITE_APP_USE_HASH
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history: createWebHistory('/'),
|
history: isHash ? createWebHashHistory('/') : createWebHistory('/'),
|
||||||
routes: basicRoutes,
|
routes: basicRoutes,
|
||||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Layout from '@/layout/index.vue'
|
import Layout from '@/layout/index.vue'
|
||||||
import Home from '@/views/dashboard/index.vue'
|
import Home from '@/views/dashboard/index.vue'
|
||||||
import { ChartBar, Dove, Github, HouseDamage, Link, TimesCircle } from '@vicons/fa'
|
|
||||||
|
import { IconAlert, IconChart, IconGitee, IconGithub, IconHome, IconLink, IconVue } from '@/components/AppIcons'
|
||||||
|
|
||||||
export const basicRoutes = [
|
export const basicRoutes = [
|
||||||
{
|
{
|
||||||
@@ -39,7 +40,7 @@ export const basicRoutes = [
|
|||||||
redirect: '/home',
|
redirect: '/home',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
icon: ChartBar,
|
icon: IconChart,
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -48,7 +49,7 @@ export const basicRoutes = [
|
|||||||
component: Home,
|
component: Home,
|
||||||
meta: {
|
meta: {
|
||||||
title: '首页',
|
title: '首页',
|
||||||
icon: HouseDamage,
|
icon: IconHome,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -106,7 +107,7 @@ export const basicRoutes = [
|
|||||||
redirect: '/error-page/404',
|
redirect: '/error-page/404',
|
||||||
meta: {
|
meta: {
|
||||||
title: '错误页',
|
title: '错误页',
|
||||||
icon: TimesCircle,
|
icon: IconAlert,
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -126,7 +127,7 @@ export const basicRoutes = [
|
|||||||
component: Layout,
|
component: Layout,
|
||||||
meta: {
|
meta: {
|
||||||
title: '外部链接',
|
title: '外部链接',
|
||||||
icon: Link,
|
icon: IconLink,
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -134,7 +135,7 @@ export const basicRoutes = [
|
|||||||
path: 'https://github.com/zclzone/vue-naive-admin',
|
path: 'https://github.com/zclzone/vue-naive-admin',
|
||||||
meta: {
|
meta: {
|
||||||
title: '源码 - github',
|
title: '源码 - github',
|
||||||
icon: Github,
|
icon: IconGithub,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -142,14 +143,15 @@ export const basicRoutes = [
|
|||||||
path: 'https://gitee.com/zclzone/vue-naive-admin',
|
path: 'https://gitee.com/zclzone/vue-naive-admin',
|
||||||
meta: {
|
meta: {
|
||||||
title: '源码 - gitee',
|
title: '源码 - gitee',
|
||||||
|
icon: IconGitee,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'LINK-DOCS',
|
name: 'LINK-DOCS',
|
||||||
path: 'https://www.yuque.com/qszone/vue-naive-admin',
|
path: 'https://zclzone.github.io/vue-naive-admin-docs',
|
||||||
meta: {
|
meta: {
|
||||||
title: '文档 - 语雀',
|
title: '文档 - vuepress',
|
||||||
icon: Dove,
|
icon: IconVue,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
1
src/settings/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as themeSettings } from './theme.json'
|
||||||
14
src/settings/theme.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"tags": {
|
||||||
|
"visible": true,
|
||||||
|
"height": 50
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"height": 60
|
||||||
|
},
|
||||||
|
"naiveThemeOverrides": {
|
||||||
|
"common": {
|
||||||
|
"primaryColor": "#316c72"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { defineStore } from 'pinia'
|
|
||||||
|
|
||||||
export const useAppStore = defineStore('app', {
|
|
||||||
state() {
|
|
||||||
return {
|
|
||||||
themeOverrides: {
|
|
||||||
common: {
|
|
||||||
primaryColor: '#316c72',
|
|
||||||
primaryColorSuppl: '#316c72',
|
|
||||||
primaryColorHover: '#316c72',
|
|
||||||
successColorHover: '#316c72',
|
|
||||||
successColorSuppl: '#316c72',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
22
src/store/modules/tag.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useTagStore = defineStore('tag', {
|
||||||
|
state() {
|
||||||
|
return {
|
||||||
|
tags: [],
|
||||||
|
activeTag: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setActiveTag(path) {
|
||||||
|
this.activeTag = path
|
||||||
|
},
|
||||||
|
addTag(tag = {}) {
|
||||||
|
if (this.tags.some((item) => item.path === tag.path)) return
|
||||||
|
this.tags.push(tag)
|
||||||
|
},
|
||||||
|
removeTag(path) {
|
||||||
|
this.tags = this.tags.filter((tag) => tag.path !== path)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
13
src/store/modules/theme.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { themeSettings } from '@/settings'
|
||||||
|
export const useThemeStore = defineStore('theme', {
|
||||||
|
state() {
|
||||||
|
return themeSettings
|
||||||
|
},
|
||||||
|
getters: {},
|
||||||
|
actions: {
|
||||||
|
setTabVisible(visible) {
|
||||||
|
this.tags.visible = visible
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
2
src/utils/cache/index.js
vendored
@@ -1,4 +1,4 @@
|
|||||||
import { createWebStorage } from './webStorage'
|
import { createWebStorage } from './web-storage'
|
||||||
|
|
||||||
export const createLocalStorage = function (option = {}) {
|
export const createLocalStorage = function (option = {}) {
|
||||||
return createWebStorage({ prefixKey: option.prefixKey || '', storage: localStorage })
|
return createWebStorage({ prefixKey: option.prefixKey || '', storage: localStorage })
|
||||||
|
|||||||
@@ -1,3 +1,68 @@
|
|||||||
<template>
|
<template>
|
||||||
<h1>首页</h1>
|
<div>
|
||||||
|
<n-card>
|
||||||
|
<div flex items-center>
|
||||||
|
<img width="60" style="border-radius: 50%" :src="userStore.avatar" />
|
||||||
|
<div ml20>
|
||||||
|
<p text-16>Hello, {{ userStore.name }}</p>
|
||||||
|
<p op80 text-12 mt5>今天又是元气满满的一天</p>
|
||||||
|
</div>
|
||||||
|
<div flex ml-auto>
|
||||||
|
<n-statistic label="待办" :value="4">
|
||||||
|
<template #suffix> / 10 </template>
|
||||||
|
</n-statistic>
|
||||||
|
<n-statistic ml80 label="Stars">
|
||||||
|
<n-number-animation ref="starsNumberRef" show-separator :from="0" :to="999" />
|
||||||
|
</n-statistic>
|
||||||
|
<n-statistic ml80 label="Forks">
|
||||||
|
<n-number-animation ref="starsNumberRef" show-separator :from="0" :to="299" />
|
||||||
|
</n-statistic>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<div p15 flex>
|
||||||
|
<n-card title="项目" size="small" :segmented="true">
|
||||||
|
<template #header-extra>
|
||||||
|
<n-button text type="primary">更多</n-button>
|
||||||
|
</template>
|
||||||
|
<div class="card-list">
|
||||||
|
<n-card v-for="i in 10" :key="i" title="Vue Naive Admin" size="small">
|
||||||
|
<p op60>一个基于 Vue3.0、Vite、Naive UI 的轻量级后台管理模板</p>
|
||||||
|
</n-card>
|
||||||
|
<div class="blank"></div>
|
||||||
|
<div class="blank"></div>
|
||||||
|
<div class="blank"></div>
|
||||||
|
<div class="blank"></div>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
.n-card {
|
||||||
|
width: 300px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 10px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 1px 2px -2px #00000029, 0 3px 6px #0000001f, 0 5px 12px 4px #00000017;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.blank {
|
||||||
|
width: 300px;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,26 +1,31 @@
|
|||||||
<script setup>
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
const { replace } = useRouter()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-404">
|
<div class="page-404">
|
||||||
<n-result status="404" description="抱歉,您访问的页面不存在。">
|
<n-result status="404" description="抱歉,您访问的页面不存在。">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<img src="@/assets/imgs/404/404.png" width="500" />
|
<img src="@/assets/images/404.png" width="300" />
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<n-button @click="replace('/')">返回首页</n-button>
|
<n-button strong secondary type="primary" @click="replace('/')">返回首页</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-result>
|
</n-result>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
const { replace } = useRouter()
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.page-404 {
|
.page-404 {
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 50px;
|
||||||
|
bottom: 50px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div p24>
|
||||||
<div class="action-btns">
|
<div class="action-btns">
|
||||||
<n-button size="small" type="primary" @click="handleCreate">新建文章</n-button>
|
<n-button size="small" type="primary" @click="handleCreate">新建文章</n-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
:columns="columns"
|
:columns="columns"
|
||||||
:pagination="pagination"
|
:pagination="pagination"
|
||||||
:row-key="(row) => row.id"
|
:row-key="(row) => row.id"
|
||||||
max-height="calc(100vh - 260px)"
|
|
||||||
@update:checked-row-keys="handleCheck"
|
@update:checked-row-keys="handleCheck"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div pb-20>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<input v-model="post.title" type="text" placeholder="输入文章标题..." class="title" />
|
<input v-model="post.title" type="text" placeholder="输入文章标题..." class="title" />
|
||||||
<n-button type="primary" style="width: 80px" :loading="btnLoading" @click="handleSavePost">保存</n-button>
|
<n-button type="primary" style="width: 80px" :loading="btnLoading" @click="handleSavePost">保存</n-button>
|
||||||
</div>
|
</div>
|
||||||
<md-editor v-model="post.content" style="height: calc(100vh - 180px)" />
|
<MdEditor v-model="post.content" style="height: calc(100vh - 200px)" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-page">
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="left">
|
||||||
|
<img src="@/assets/images/login_banner.png" height="380" alt="login_banner" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-wrapper">
|
||||||
|
<h5 class="brand">
|
||||||
|
<img src="@/assets/images/logo.svg" width="45" mr-15 alt="logo" />
|
||||||
|
{{ title }}
|
||||||
|
</h5>
|
||||||
|
<div class="form-item" mt-35>
|
||||||
|
<input
|
||||||
|
v-model="loginInfo.name"
|
||||||
|
autofocus
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
placeholder="username"
|
||||||
|
@keydown.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-item" mt-35>
|
||||||
|
<input
|
||||||
|
v-model="loginInfo.password"
|
||||||
|
type="password"
|
||||||
|
class="input"
|
||||||
|
placeholder="password"
|
||||||
|
@keydown.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-item" mt-35>
|
||||||
|
<button class="submit-btn" @click="handleLogin">登录</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, unref } from 'vue'
|
import { ref, unref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@@ -53,150 +92,75 @@ async function handleLogin() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="login-page">
|
|
||||||
<div class="form-wrapper">
|
|
||||||
<h2 class="title">{{ title }}</h2>
|
|
||||||
<div class="form-item" mt-20>
|
|
||||||
<input
|
|
||||||
v-model="loginInfo.name"
|
|
||||||
autofocus
|
|
||||||
type="text"
|
|
||||||
class="input"
|
|
||||||
placeholder="username"
|
|
||||||
@keydown.enter="handleLogin"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-item" mt-20>
|
|
||||||
<input
|
|
||||||
v-model="loginInfo.password"
|
|
||||||
type="password"
|
|
||||||
class="input"
|
|
||||||
placeholder="password"
|
|
||||||
@keydown.enter="handleLogin"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-item" mt-20>
|
|
||||||
<button class="submit-btn" @click="handleLogin">登录</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@property --perA {
|
|
||||||
syntax: '<percentage>';
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 75%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@property --perB {
|
|
||||||
syntax: '<percentage>';
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 99%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@property --perC {
|
|
||||||
syntax: '<percentage>';
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 15%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@property --perD {
|
|
||||||
syntax: '<percentage>';
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 16%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@property --perE {
|
|
||||||
syntax: '<percentage>';
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 86%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@property --angle {
|
|
||||||
syntax: '<angle>';
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 0deg;
|
|
||||||
}
|
|
||||||
.login-page {
|
.login-page {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #e1e8ee;
|
background-image: url(@/assets/images/login_bg.jpg);
|
||||||
|
background-size: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-image: radial-gradient(
|
}
|
||||||
circle at var(--perE) 7%,
|
|
||||||
rgba(40, 40, 40, 0.04) 0%,
|
|
||||||
rgba(40, 40, 40, 0.04) 50%,
|
|
||||||
rgba(200, 200, 200, 0.04) 50%,
|
|
||||||
rgba(200, 200, 200, 0.04) 100%
|
|
||||||
),
|
|
||||||
radial-gradient(
|
|
||||||
circle at var(--perC) var(--perD),
|
|
||||||
rgba(99, 99, 99, 0.04) 0%,
|
|
||||||
rgba(99, 99, 99, 0.04) 50%,
|
|
||||||
rgba(45, 45, 45, 0.04) 50%,
|
|
||||||
rgba(45, 45, 45, 0.04) 100%
|
|
||||||
),
|
|
||||||
radial-gradient(
|
|
||||||
circle at var(--perA) var(--perB),
|
|
||||||
rgba(243, 243, 243, 0.04) 0%,
|
|
||||||
rgba(243, 243, 243, 0.04) 50%,
|
|
||||||
rgba(37, 37, 37, 0.04) 50%,
|
|
||||||
rgba(37, 37, 37, 0.04) 100%
|
|
||||||
),
|
|
||||||
linear-gradient(var(--angle), #22deed, #8759d7);
|
|
||||||
animation: move 30s infinite alternate linear;
|
|
||||||
|
|
||||||
@keyframes move {
|
.wrapper {
|
||||||
100% {
|
width: 100%;
|
||||||
--perA: 85%;
|
max-width: 1020px;
|
||||||
--perB: 49%;
|
box-shadow: 1.5px 3.99px 27px 0px rgb(0 0 0 / 10%);
|
||||||
--perC: 45%;
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
--perD: 39%;
|
|
||||||
--perE: 70%;
|
display: flex;
|
||||||
--angle: 360deg;
|
.left {
|
||||||
}
|
padding: 40px;
|
||||||
|
border-right: 1px solid #cccccc5e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-wrapper {
|
.form-wrapper {
|
||||||
text-align: center;
|
display: flex;
|
||||||
padding: 40px 50px;
|
flex-direction: column;
|
||||||
border-radius: 15px;
|
justify-content: center;
|
||||||
background-color: rgba(#fff, 0.2);
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
.brand {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
color: #6a6a6a;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: normal;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
.title {
|
display: flex;
|
||||||
font-size: 22px;
|
align-items: center;
|
||||||
color: #f3f3f3;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-item {
|
.form-item {
|
||||||
width: 240px;
|
width: 100%;
|
||||||
|
max-width: 360px;
|
||||||
|
height: 50px;
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40px;
|
height: 100%;
|
||||||
padding: 0 15px;
|
padding: 0 20px;
|
||||||
|
border: 1px solid #6a6a6a;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
color: $primaryColor;
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
color: #333;
|
transition: 0.5s;
|
||||||
transition: 0.3s;
|
|
||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: 0 0 5px #8759d7;
|
border-color: $primaryColor;
|
||||||
|
box-shadow: 0 0 5px $primaryColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40px;
|
height: 100%;
|
||||||
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: #6683d2;
|
background-color: $primaryColor;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<template></template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
@@ -19,5 +21,3 @@ replace({
|
|||||||
query,
|
query,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template></template>
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div p24>
|
||||||
|
<n-button type="error" @click="handleDelete">删除</n-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup name="TestDialog">
|
<script setup name="TestDialog">
|
||||||
const handleDelete = function () {
|
const handleDelete = function () {
|
||||||
$dialog.confirm({
|
$dialog.confirm({
|
||||||
@@ -11,7 +17,3 @@ const handleDelete = function () {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-button @click="handleDelete">删除</n-button>
|
|
||||||
</template>
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div p24>
|
||||||
|
<n-gradient-text gradient="linear-gradient(90deg, red 0%, green 50%, blue 100%)"> 注意查看提示语 </n-gradient-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!--使用keep-alive须设置name,注意请与对应的路由的name保持一致,方便统一处理-->
|
<!--使用keep-alive须设置name,注意请与对应的路由的name保持一致,方便统一处理-->
|
||||||
<script setup name="TEST-KEEP-ALIVE">
|
<script setup name="TEST-KEEP-ALIVE">
|
||||||
import { onMounted, onActivated, onDeactivated } from 'vue'
|
import { onMounted, onActivated, onDeactivated } from 'vue'
|
||||||
@@ -14,7 +20,3 @@ onDeactivated(() => {
|
|||||||
$message.success('触发onDeactivated')
|
$message.success('触发onDeactivated')
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-gradient-text gradient="linear-gradient(90deg, red 0%, green 50%, blue 100%)"> 注意查看提示语 </n-gradient-text>
|
|
||||||
</template>
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div p24>
|
||||||
|
<n-button type="primary" @click="handleLogin">点击登陆</n-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
function handleLogin() {
|
function handleLogin() {
|
||||||
$message.loading('登陆中...')
|
$message.loading('登陆中...')
|
||||||
@@ -10,7 +16,3 @@ function handleLogin() {
|
|||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-button @click="handleLogin">点击登陆</n-button>
|
|
||||||
</template>
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div p24>
|
||||||
<div class="content-box">
|
<div p20 bg="#fff">
|
||||||
<p text-12>测试12px</p>
|
<p text-12>测试12px</p>
|
||||||
<p text-13>测试13px</p>
|
<p text-13>测试13px</p>
|
||||||
<p text-14>测试14px</p>
|
<p text-14>测试14px</p>
|
||||||
@@ -14,10 +14,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<script setup></script>
|
||||||
.content-box {
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 15px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||