Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
841bab0d63 | ||
|
|
453148fc8d | ||
|
|
7ec078bd7a | ||
|
|
dd0bc3e6e8 | ||
|
|
8c665c727b | ||
|
|
da98aa1c7d | ||
|
|
51b47ea722 | ||
|
|
220a7800f7 | ||
|
|
230e3a72d9 | ||
|
|
0cefadc2a5 | ||
|
|
2f1b747243 | ||
|
|
296d5ea6f0 | ||
|
|
3a415703d4 | ||
|
|
006f730457 | ||
|
|
606c5a2df0 | ||
|
|
30c375cc1d | ||
|
|
ddf14053da | ||
|
|
38edbcb68a | ||
|
|
3e54a82abb | ||
|
|
df6225a752 | ||
|
|
63c1f2f132 | ||
|
|
0bb2a904e7 | ||
|
|
ef3aaa5be5 | ||
|
|
869a68812c | ||
|
|
fd0032e0e9 | ||
|
|
b53d7daaa1 | ||
|
|
856bdfd0ee | ||
|
|
9f9884759c | ||
|
|
7dad43d003 | ||
|
|
7762e02b31 | ||
|
|
e5768fa1e3 | ||
|
|
7ee613d8cf | ||
|
|
80a5b7f053 | ||
|
|
eb160731da | ||
|
|
789231a7f4 | ||
|
|
6ea6e1c267 | ||
|
|
d971e7e4ba | ||
|
|
215998dc66 | ||
|
|
40f9ac1a6b | ||
|
|
6ec5588ed4 | ||
|
|
380e5768c4 | ||
|
|
5856f601fa | ||
|
|
d10b8f0e96 | ||
|
|
3860cf9ebb | ||
|
|
94b46d9bf6 | ||
|
|
4df7d44bf1 | ||
|
|
42b8aca37b | ||
|
|
0c96d0e937 | ||
|
|
b540f5599f | ||
|
|
18b8a81640 | ||
|
|
06b3afc2de | ||
|
|
3088773ebe |
@@ -11,9 +11,12 @@ module.exports = {
|
||||
{ value: 'ci', name:'ci: 修改 CI 配置、脚本' },
|
||||
{ value: 'chore', name:'chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)' },
|
||||
{ value: 'revert', name:'revert: 回滚 commit' },
|
||||
{ value: 'wip', name:'wip: 开发中' },
|
||||
{ value: 'mod', name:'mod: 不确定分类的修改' },
|
||||
{ value: 'release', name:'release: 发布' },
|
||||
],
|
||||
scopes: [
|
||||
['custom', '自定义3'],
|
||||
['custom', '自定义'],
|
||||
['projects', '项目搭建'],
|
||||
['components', '组件相关'],
|
||||
['utils', 'utils 相关'],
|
||||
|
||||
2
.env
@@ -1,3 +1,3 @@
|
||||
VITE_APP_TITLE = 'Vue Naive Admin'
|
||||
VITE_TITLE = 'Vue Naive Admin'
|
||||
|
||||
VITE_PORT = 3100
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
VITE_PUBLIC_PATH = '/'
|
||||
|
||||
# 是否启用MOCK
|
||||
VITE_APP_USE_MOCK = true
|
||||
VITE_USE_MOCK = true
|
||||
|
||||
# proxy
|
||||
VITE_PROXY = [["/api","http://localhost:8080"],["/api-test","localhost:8080"]]
|
||||
# 是否启用MOCK
|
||||
VITE_USE_PROXY = false
|
||||
|
||||
# 代理类型(跟启动和构建环境无关) 'dev' | 'test' | 'prod'
|
||||
VITE_PROXY_TYPE = 'dev'
|
||||
|
||||
# base api
|
||||
VITE_APP_BASE_API = '/api'
|
||||
|
||||
# test base api
|
||||
VITE_APP_BASE_API_TEST = '/api-test'
|
||||
VITE_BASE_API = '/api'
|
||||
11
.env.github
@@ -1,16 +1,13 @@
|
||||
# 自定义域名CNAME
|
||||
# VITE_APP_GLOB_CNAME = 'template.qszone.com'
|
||||
# VITE_CNAME = 'template.qszone.com'
|
||||
|
||||
# 资源公共路径,需要以 /开头和结尾
|
||||
VITE_PUBLIC_PATH = '/vue-naive-admin/'
|
||||
|
||||
VITE_APP_USE_HASH = true
|
||||
VITE_USE_HASH = true
|
||||
|
||||
# 是否启用MOCK
|
||||
VITE_APP_USE_MOCK = true
|
||||
VITE_USE_MOCK = true
|
||||
|
||||
# base api
|
||||
VITE_APP_BASE_API = '/api'
|
||||
|
||||
# test base api
|
||||
VITE_APP_BASE_API_TEST = '/api-test'
|
||||
VITE_BASE_API = '/api'
|
||||
@@ -2,10 +2,13 @@
|
||||
VITE_PUBLIC_PATH = '/'
|
||||
|
||||
# 是否启用MOCK
|
||||
VITE_APP_USE_MOCK = true
|
||||
VITE_USE_MOCK = true
|
||||
|
||||
# base api
|
||||
VITE_APP_BASE_API = '/api'
|
||||
VITE_BASE_API = '/api'
|
||||
|
||||
# test base api
|
||||
VITE_APP_BASE_API_TEST = '/api-test'
|
||||
# 是否启用压缩
|
||||
VITE_USE_COMPRESS = true
|
||||
|
||||
# 压缩类型
|
||||
VITE_COMPRESS_TYPE = gzip
|
||||
@@ -1,10 +1,7 @@
|
||||
VITE_PUBLIC_PATH = '/'
|
||||
|
||||
# 是否启用MOCK
|
||||
VITE_APP_USE_MOCK = true
|
||||
VITE_USE_MOCK = true
|
||||
|
||||
# base api
|
||||
VITE_APP_BASE_API = '/api'
|
||||
|
||||
# test base api
|
||||
VITE_APP_BASE_API_TEST = '/api-test'
|
||||
VITE_BASE_API = '/api'
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npm run lint
|
||||
npm run lint:staged
|
||||
|
||||
29
.vscode/settings.json
vendored
@@ -1,9 +1,34 @@
|
||||
{
|
||||
"path-intellisense.mappings": {
|
||||
"@/": "${workspaceRoot}/src",
|
||||
"~/": "${workspaceRoot}"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"prettier.printWidth": 120,
|
||||
"prettier.singleQuote": true,
|
||||
"prettier.semi": false,
|
||||
"prettier.endOfLine": "lf",
|
||||
"files.eol": "\n",
|
||||
"editor.formatOnSave": false,
|
||||
|
||||
"[javascript]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"cssrem.rootFontSize": 4, // 适配unocss,1rem = 4px ==> 0.25rem = 1px
|
||||
"files.associations": {
|
||||
"*.env.*": "dotenv",
|
||||
"*.css": "postcss"
|
||||
}
|
||||
}
|
||||
|
||||
131
README.md
@@ -10,113 +10,106 @@
|
||||
<a href="./LICENSE"><img allt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
|
||||
</p>
|
||||
|
||||
<p align='center'>
|
||||
<b>English</b> |
|
||||
<a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.zh-CN.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
### 简介
|
||||
### Introduction
|
||||
|
||||
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin)是一个 **完全开源免费且允许商用** 的后台管理模板,基于 Vue3、Vite2、Pinia 和 Naive UI等前端最新技术栈。相较于其他比较流行的后台管理模板,此项目相对简洁、轻量,学习成本非常低。麻雀虽小,五脏俱全,权限、Mock、菜单、axios 封装、pinia、项目配置、样式配置、环境配置,以及一些经常用的基础组件封装等等这些该有的都有,非常适合中小型项目或者个人项目。
|
||||
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin) is a **completely open source free and commercially allowed ** admin template,Based on the latest technology stack of front-end such as `Vue3、Vite3、Pinia、Unocss and Naive UI`. Compared with other more popular backend management templates, this project is more concise, lightweight, fresh style, very low learning costs, ideal for small and medium-sized projects or personal projects.
|
||||
|
||||
### 为什么要开发这个模板
|
||||
### Features
|
||||
|
||||
- Vue3 和 Vite 已经趋于成熟,学习 vite 和 vue3 非常有必要,通过开发模板进行学习是一个很好的方式,事实也证明我确实从中获益良多
|
||||
- 目前主流的 Vue3+Vite 后台管理模板都相对复杂,甚至感觉有点花里胡哨(没有贬低的意思,大部分的架构设计都很优秀,只是觉得集成了太多不实用的东西)
|
||||
- 🍒 Integrated [Naive UI](https://www.naiveui.com),recommended by Evan You.
|
||||
- 🍑 Integrated login, logout and permission verification.
|
||||
- 🍐 Integrated multi-environment configuration, dev, test, production and github pages environments.
|
||||
- 🍎 Integrated `eslint + prettier`.
|
||||
- 🍌 Integrated `husky + commitlint`.
|
||||
- 🍉 Integrated `Mock`.
|
||||
- 🍍 Integrated `pinia`,lightweight, simple and easy to use alternative to vuex.
|
||||
- 📦 Integrated `unplugin` auto import.
|
||||
- 🤹 Integrated `iconify` icon,support custom svg icons.
|
||||
- 🍇 Integrated `unocss`.
|
||||
|
||||
### 功能
|
||||
### Preview
|
||||
|
||||
- 🍒 集成 Naive UI,尤大推荐的 UI 组件库,[https://www.naiveui.com](https://www.naiveui.com)
|
||||
- 🍑 集成登陆、注销及权限验证
|
||||
- 🍐 集成多环境配置,dev、测试、生产和github pages环境
|
||||
- 🍎 集成 Eslint + Prettier,代码约束和格式化统一
|
||||
- 🍉 集成 Mock 接口服务,dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
|
||||
- 🍇 集成 unocss,antfu 大神开源的原子化 css 解决方案,非常轻量,目前我是自己写 scss 样式搭配着 unocss 使用的
|
||||
- 🍍 集成 Pinia,Vuex 的替代方案,轻量、简单、易用(尤大已表示不会有Vuex5,或者说pinia就是Vuex5)
|
||||
- 📦 集成 Vite 自动导入插件unplugin-vue-components,解放双手,开发效率直接起飞
|
||||
- 🤹 集成 unplugin-icons插件,优雅使用iconify图标
|
||||
- 🍏 二次封装 Axios,支持多 axios 实例
|
||||
- 🍌 二次封装全局 Dialog、Message、LoadingBar 组件
|
||||
- 🍋 二次封装 localStorage 和 sessionStorage,支持设置过期时间
|
||||
[https://template.qszone.com](https://template.qszone.com)
|
||||
|
||||
### 预览
|
||||
[https://zclzone.github.io/vue-naive-admin](https://zclzone.github.io/vue-naive-admin)
|
||||
|
||||
[template.qszone.com](https://template.qszone.com)
|
||||
|
||||
[github pages](https://zclzone.github.io/vue-naive-admin)
|
||||
|
||||
### 文档
|
||||
### Docs
|
||||
|
||||
[Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
|
||||
|
||||
[羽雀文档:Vue Naive Admin](https://www.yuque.com/qszone/vue-naive-admin)
|
||||
|
||||
### 构建
|
||||
### Getting Started
|
||||
|
||||
```shell
|
||||
# 推荐配置git autocrlf 为 false(本项目规范使用lf换行符,此配置是为防止git自动将源文件转换为crlf)
|
||||
# 不清楚为什么要这样做的请参考这篇文章:https://www.freesion.com/article/4532642129
|
||||
# Recommended setup git autocrlf 为 false
|
||||
git config --global core.autocrlf false
|
||||
|
||||
# 克隆项目
|
||||
# Clone Project
|
||||
git clone https://github.com/zclzone/vue-naive-admin.git
|
||||
|
||||
# 进入项目目录
|
||||
cd vue-naive-admin
|
||||
|
||||
# 安装依赖(建议使用pnpm: https://pnpm.io/zh/installation)
|
||||
pnpm i # 或者 npm i
|
||||
# Install dependencies(Recommended use pnpm: https://pnpm.io/zh/installation)
|
||||
npm i -g pnpm # Installed and can be ignored
|
||||
pnpm i # or npm i
|
||||
|
||||
# 启动
|
||||
npm run dev
|
||||
# Start
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 发布
|
||||
### Build and Release
|
||||
|
||||
```shell
|
||||
# 构建测试环境
|
||||
npm run build:test
|
||||
# Test Environment
|
||||
pnpm build:test
|
||||
|
||||
# 构建github pages环境
|
||||
npm run build:github
|
||||
# Github Environment
|
||||
pnpm build:github
|
||||
|
||||
# 构建生产环境
|
||||
npm run build
|
||||
# Prod Environment
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### 其他指令
|
||||
### Other
|
||||
|
||||
```shell
|
||||
# eslint代码格式检查
|
||||
npm run lint
|
||||
# eslint check
|
||||
pnpm lint
|
||||
|
||||
# 代码检查并修复
|
||||
npm run lint:fix
|
||||
# eslint check and fix
|
||||
pnpm lint:fix
|
||||
|
||||
# 预览发布包效果(需先执行构建指令)
|
||||
npm run preview
|
||||
# Preview(Need to build first)
|
||||
pnpm preview
|
||||
|
||||
# Commit(husky+commitlint)
|
||||
pnpm cz
|
||||
```
|
||||
|
||||
### 规范
|
||||
### TS version: Qs Admin
|
||||
|
||||
#### git commit 规范
|
||||
#### source code
|
||||
|
||||
- gitub: [https://github.com/zclzone/qs-admin](https://github.com/zclzone/qs-admin)
|
||||
- gitee: [https://gitee.com/zclzone/qs-admin-ts](https://gitee.com/zclzone/qs-admin-ts)
|
||||
|
||||
#### preview
|
||||
|
||||
- [https://admin.qszone.com](https://admin.qszone.com)
|
||||
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
|
||||
|
||||
### Communication group & About the author
|
||||
|
||||
<a href="https://blog.qszone.com/about/">
|
||||
<img src="https://assets.qszone.com/images/about.png" style="max-width: 400px" />
|
||||
</a>
|
||||
|
||||
- `feat` 增加新功能
|
||||
- `fix` 修复问题/BUG
|
||||
- `style` 代码风格相关无影响运行结果的
|
||||
- `perf` 优化/性能提升
|
||||
- `refactor` 重构
|
||||
- `revert` 撤销修改
|
||||
- `test` 测试相关
|
||||
- `docs` 文档/注释
|
||||
- `chore` 依赖更新/脚手架配置修改等
|
||||
- `workflow` 工作流改进
|
||||
- `ci` 持续集成
|
||||
- `types` 类型定义文件更改
|
||||
- `wip` 开发中
|
||||
- `mod` 不确定分类的修改
|
||||
- `release` 发布
|
||||
|
||||
### 入群交流
|
||||
|
||||
<p>
|
||||
<img src="https://assets.qszone.com/image/入群.png" />
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
115
README.zh-CN.md
Normal file
@@ -0,0 +1,115 @@
|
||||
<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="./LICENSE"><img allt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
|
||||
</p>
|
||||
|
||||
<p align='center'>
|
||||
<b>简体中文</b> |
|
||||
<a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.md">English</a>
|
||||
</p>
|
||||
|
||||
### 简介
|
||||
|
||||
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin) 是一个 **完全开源免费且允许商用** 的后台管理模板,基于 `Vue3、Vite3、Pinia、Unocss 和 Naive UI` 等前端最新技术栈。相较于其他比较流行的后台管理模板,此项目更加简洁、轻量,风格清新,学习成本非常低,非常适合中小型项目或者个人项目。
|
||||
|
||||
### 功能
|
||||
|
||||
- 🍒 集成 [Naive UI](https://www.naiveui.com)
|
||||
- 🍑 集成登陆、注销及权限验证
|
||||
- 🍐 集成多环境配置,dev、测试、生产和github pages环境
|
||||
- 🍎 集成 `eslint + prettier`,代码约束和格式化统一
|
||||
- 🍌 集成 `husky + commitlint`,代码提交规范化
|
||||
- 🍉 集成 `mock` 接口服务,dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
|
||||
- 🍍 集成 `pinia`,vuex 的替代方案,轻量、简单、易用
|
||||
- 📦 集成 `unplugin` 插件,自动导入,解放双手,开发效率直接起飞
|
||||
- 🤹 集成 `iconify` 图标,支持自定义 svg 图标, 优雅使用icon
|
||||
- 🍇 集成 `unocss`,antfu 开源的原子 css 解决方案,非常轻量
|
||||
|
||||
### 预览
|
||||
|
||||
[https://template.qszone.com](https://template.qszone.com)
|
||||
|
||||
[https://zclzone.github.io/vue-naive-admin](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)
|
||||
|
||||
### 快速开始
|
||||
|
||||
```shell
|
||||
# 推荐配置git autocrlf 为 false(本项目规范使用lf换行符,此配置是为防止git自动将源文件转换为crlf)
|
||||
# 不清楚为什么要这样做的请参考这篇文章:https://www.freesion.com/article/4532642129
|
||||
git config --global core.autocrlf false
|
||||
|
||||
# 克隆项目
|
||||
git clone https://github.com/zclzone/vue-naive-admin.git
|
||||
|
||||
# 进入项目目录
|
||||
cd vue-naive-admin
|
||||
|
||||
# 安装依赖(建议使用pnpm: https://pnpm.io/zh/installation)
|
||||
npm i -g pnpm # 装了可忽略
|
||||
pnpm i # 或者 npm i
|
||||
|
||||
# 启动
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 构建发布
|
||||
|
||||
```shell
|
||||
# 构建测试环境
|
||||
pnpm build:test
|
||||
|
||||
# 构建github pages环境
|
||||
pnpm build:github
|
||||
|
||||
# 构建生产环境
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### 其他指令
|
||||
|
||||
```shell
|
||||
# eslint代码格式检查
|
||||
pnpm lint
|
||||
|
||||
# 代码检查并修复
|
||||
pnpm lint:fix
|
||||
|
||||
# 预览发布包效果(需先执行构建指令)
|
||||
pnpm preview
|
||||
|
||||
# 提交代码(husky+commitlint)
|
||||
pnpm cz
|
||||
```
|
||||
|
||||
### TS 版本: Qs Admin
|
||||
|
||||
#### 源码
|
||||
|
||||
- gitub: [https://github.com/zclzone/qs-admin](https://github.com/zclzone/qs-admin)
|
||||
- gitee: [https://gitee.com/zclzone/qs-admin-ts](https://gitee.com/zclzone/qs-admin-ts)
|
||||
|
||||
#### 预览
|
||||
|
||||
- [https://admin.qszone.com](https://admin.qszone.com)
|
||||
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
|
||||
|
||||
### 入群交流 & 关于作者
|
||||
|
||||
<a href="https://blog.qszone.com/about/">
|
||||
<img src="https://assets.qszone.com/images/about.png" style="max-width: 400px" />
|
||||
</a>
|
||||
|
||||
|
||||
13
build/config/define.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
/**
|
||||
* * 此处定义的是全局常量,启动或打包后将添加到window中
|
||||
* https://vitejs.cn/config/#define
|
||||
*/
|
||||
|
||||
// 项目构建时间
|
||||
const _BUILD_TIME_ = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss'))
|
||||
|
||||
export const viteDefine = {
|
||||
_BUILD_TIME_,
|
||||
}
|
||||
2
build/config/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './define'
|
||||
export * from './proxy'
|
||||
15
build/config/proxy.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { getProxyConfig } from '../../settings'
|
||||
|
||||
export function createViteProxy(isUseProxy = true, proxyType) {
|
||||
if (!isUseProxy) return undefined
|
||||
|
||||
const proxyConfig = getProxyConfig(proxyType)
|
||||
const proxy = {
|
||||
[proxyConfig.prefix]: {
|
||||
target: proxyConfig.target,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(new RegExp(`^${proxyConfig.prefix}`), ''),
|
||||
},
|
||||
}
|
||||
return proxy
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { createHtmlPlugin } from 'vite-plugin-html'
|
||||
|
||||
export function configHtmlPlugin(viteEnv, isBuild) {
|
||||
const { VITE_APP_TITLE } = viteEnv
|
||||
const { VITE_TITLE } = viteEnv
|
||||
|
||||
const htmlPlugin = createHtmlPlugin({
|
||||
minify: isBuild,
|
||||
inject: {
|
||||
data: {
|
||||
title: VITE_APP_TITLE,
|
||||
title: VITE_TITLE,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -14,6 +14,8 @@ import Unocss from 'unocss/vite'
|
||||
|
||||
// rollup打包分析插件
|
||||
import visualizer from 'rollup-plugin-visualizer'
|
||||
// 压缩
|
||||
import viteCompression from 'vite-plugin-compression'
|
||||
|
||||
import { configHtmlPlugin } from './html'
|
||||
import { configMockPlugin } from './mock'
|
||||
@@ -22,10 +24,14 @@ import unplugin from './unplugin'
|
||||
export function createVitePlugins(viteEnv, isBuild) {
|
||||
const plugins = [vue(), vueSetupExtend(), ...unplugin, configHtmlPlugin(viteEnv, isBuild), Unocss()]
|
||||
|
||||
if (viteEnv?.VITE_APP_USE_MOCK) {
|
||||
if (viteEnv?.VITE_USE_MOCK) {
|
||||
plugins.push(configMockPlugin(isBuild))
|
||||
}
|
||||
|
||||
if (viteEnv.VITE_USE_COMPRESS) {
|
||||
plugins.push(viteCompression({ algorithm: viteEnv.VITE_COMPRESS_TYPE || 'gzip' }))
|
||||
}
|
||||
|
||||
if (isBuild) {
|
||||
plugins.push(
|
||||
visualizer({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { viteMockServe } from 'vite-plugin-mock'
|
||||
|
||||
export function configMockPlugin(isBuild) {
|
||||
return viteMockServe({
|
||||
mockPath: 'mock/modules',
|
||||
mockPath: 'mock/api',
|
||||
localEnabled: !isBuild,
|
||||
prodEnabled: isBuild,
|
||||
injectCode: `
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolve } from 'path'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||
@@ -10,13 +11,16 @@ import IconsResolver from 'unplugin-icons/resolver'
|
||||
* 图标库: https://icones.js.org/
|
||||
*/
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
|
||||
|
||||
import { getRootPath } from '../utils'
|
||||
import { getSrcPath } from '../utils'
|
||||
|
||||
const customIconPath = resolve(getSrcPath(), 'assets/svg')
|
||||
|
||||
const customIconPath = getRootPath('src', 'assets/icons')
|
||||
export default [
|
||||
AutoImport({
|
||||
imports: ['vue', 'vue-router'],
|
||||
dts: false,
|
||||
}),
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
@@ -28,5 +32,12 @@ export default [
|
||||
}),
|
||||
Components({
|
||||
resolvers: [NaiveUiResolver(), IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })],
|
||||
dts: false,
|
||||
}),
|
||||
createSvgIconsPlugin({
|
||||
iconDirs: [customIconPath],
|
||||
symbolId: 'icon-custom-[dir]-[name]',
|
||||
inject: 'body-last',
|
||||
customDomId: '__CUSTOM_SVG_ICON__',
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { resolve } from 'path'
|
||||
import chalk from 'chalk'
|
||||
import { writeFileSync } from 'fs-extra'
|
||||
import { OUTPUT_DIR } from '../constant'
|
||||
import { getEnvConfig, getRootPath } from '../utils'
|
||||
|
||||
export function runBuildCNAME() {
|
||||
const { VITE_APP_CNAME } = getEnvConfig()
|
||||
if (!VITE_APP_CNAME) return
|
||||
const { VITE_CNAME } = getEnvConfig()
|
||||
if (!VITE_CNAME) return
|
||||
try {
|
||||
writeFileSync(getRootPath(`${OUTPUT_DIR}/CNAME`), VITE_APP_CNAME)
|
||||
writeFileSync(resolve(getRootPath(), `${OUTPUT_DIR}/CNAME`), VITE_CNAME)
|
||||
} catch (error) {
|
||||
console.log(chalk.red('CNAME file failed to package:\n' + error))
|
||||
}
|
||||
|
||||
@@ -2,53 +2,38 @@ import fs from 'fs'
|
||||
import path from 'path'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
const httpsReg = /^https:\/\//
|
||||
|
||||
export function wrapperEnv(envOptions) {
|
||||
if (!envOptions) return {}
|
||||
const ret = {}
|
||||
|
||||
for (const key in envOptions) {
|
||||
let val = envOptions[key]
|
||||
if (['true', 'false'].includes(val)) {
|
||||
val = val === 'true'
|
||||
}
|
||||
if (['VITE_PORT'].includes(key)) {
|
||||
val = +val
|
||||
}
|
||||
if (key === 'VITE_PROXY' && val && typeof val === 'string') {
|
||||
try {
|
||||
val = JSON.parse(val.replace(/'/g, '"'))
|
||||
} catch (error) {
|
||||
val = ''
|
||||
}
|
||||
}
|
||||
ret[key] = val
|
||||
if (typeof val === 'string') {
|
||||
process.env[key] = val
|
||||
} else if (typeof val === 'object') {
|
||||
process.env[key] = JSON.stringify(val)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
/**
|
||||
* * 项目根路径
|
||||
* @descrition 结尾不带/
|
||||
*/
|
||||
export function getRootPath() {
|
||||
return path.resolve(process.cwd())
|
||||
}
|
||||
|
||||
export function createProxy(list = []) {
|
||||
const ret = {}
|
||||
for (const [prefix, target] of list) {
|
||||
const isHttps = httpsReg.test(target)
|
||||
/**
|
||||
* * 项目src路径
|
||||
* @param srcName src目录名称(默认: "src")
|
||||
* @descrition 结尾不带斜杠
|
||||
*/
|
||||
export function getSrcPath(srcName = 'src') {
|
||||
return path.resolve(getRootPath(), srcName)
|
||||
}
|
||||
|
||||
// https://github.com/http-party/node-http-proxy#options
|
||||
ret[prefix] = {
|
||||
target: target,
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''),
|
||||
// https is require secure=false
|
||||
...(isHttps ? { secure: false } : {}),
|
||||
}
|
||||
const httpsReg = /^https:\/\//
|
||||
|
||||
export function convertEnv(envOptions) {
|
||||
const result = {}
|
||||
if (!envOptions) return result
|
||||
|
||||
for (const envKey in envOptions) {
|
||||
let envVal = envOptions[envKey]
|
||||
if (['true', 'false'].includes(envVal)) envVal = envVal === 'true'
|
||||
|
||||
if (['VITE_PORT'].includes(envKey)) envVal = +envVal
|
||||
|
||||
result[envKey] = envVal
|
||||
}
|
||||
return ret
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,7 +50,7 @@ function getConfFiles() {
|
||||
return ['.env', '.env.local', '.env.production']
|
||||
}
|
||||
|
||||
export function getEnvConfig(match = 'VITE_APP_GLOB_', confFiles = getConfFiles()) {
|
||||
export function getEnvConfig(match = 'VITE_', confFiles = getConfFiles()) {
|
||||
let envConfig = {}
|
||||
confFiles.forEach((item) => {
|
||||
try {
|
||||
@@ -85,7 +70,3 @@ export function getEnvConfig(match = 'VITE_APP_GLOB_', confFiles = getConfFiles(
|
||||
})
|
||||
return envConfig
|
||||
}
|
||||
|
||||
export function getRootPath(...dir) {
|
||||
return path.resolve(process.cwd(), ...dir)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
module.exports = {
|
||||
ignores: [(commit) => commit.includes('init')],
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
'type-enum': [
|
||||
2,
|
||||
'always',
|
||||
[
|
||||
'feat',
|
||||
'fix',
|
||||
'docs',
|
||||
'style',
|
||||
'refactor',
|
||||
'perf',
|
||||
'test',
|
||||
'build',
|
||||
'ci',
|
||||
'chore',
|
||||
'revert',
|
||||
'wip',
|
||||
'mod',
|
||||
'release',
|
||||
],
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
|
||||
<!-- 白屏时的loading效果 -->
|
||||
<div class="loading-container">
|
||||
<div id="loadingLogo" class="loading-svg"></div>
|
||||
@@ -30,7 +29,6 @@
|
||||
<div class="loading-title"><%= title %></div>
|
||||
</div>
|
||||
<script src="/resource/loading.js"></script>
|
||||
|
||||
</div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"~/*": ["./*"],
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"jsx": "preserve"
|
||||
|
||||
5
mock/api/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import auth from './auth'
|
||||
import user from './user'
|
||||
import post from './post'
|
||||
|
||||
export default [...auth, ...user, ...post]
|
||||
@@ -34,7 +34,7 @@ export default [
|
||||
{
|
||||
id: 28,
|
||||
title: '如何优雅的给图片添加水印',
|
||||
author: '张传龙',
|
||||
author: '大脸怪',
|
||||
category: 'JavaScript',
|
||||
description: '优雅的给图片添加水印',
|
||||
content: '我之前写过一篇文章记录了一次上传图片的优化史',
|
||||
@@ -47,7 +47,7 @@ export default [
|
||||
{
|
||||
id: 26,
|
||||
title: '前端缓存的理解',
|
||||
author: '张传龙',
|
||||
author: '大脸怪',
|
||||
category: 'Http',
|
||||
description: '谈谈前端缓存的理解',
|
||||
content: '> 背景\n\n公司有个vue-cli3移动端web项目发版更新后发现部分用户手机在钉钉内置浏览器打开出现了缓存',
|
||||
@@ -56,22 +56,10 @@ export default [
|
||||
createDate: '2021-06-10T18:51:19.000Z',
|
||||
updateDate: '2021-09-17T09:33:24.000Z',
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
title: '使用jQuery的load方法帮女朋友实现套娃Html',
|
||||
author: '张传龙',
|
||||
category: 'JavaScript',
|
||||
description: '最近女朋友刚入职新公司,接到的第一个任务就是将一个网站所有的页面合并成一个页面',
|
||||
content: '最近女朋友刚入职新公司,接到的第一个任务就是将一个网站所有的页面合并成一个页面',
|
||||
isRecommend: true,
|
||||
isPublish: true,
|
||||
createDate: '2021-05-26T15:26:06.000Z',
|
||||
updateDate: '2021-09-17T09:33:24.000Z',
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
title: 'Promise的五个静态方法',
|
||||
author: '张传龙',
|
||||
author: '大脸怪',
|
||||
category: 'JavaScript',
|
||||
description: '简单介绍下在 Promise 类中,有5 种静态方法及它们的使用场景',
|
||||
content: '## 1. Promise.all\n\n并行执行多个 promise,并等待所有 promise 都准备就绪。再对它们进行处理。',
|
||||
@@ -1,11 +1,6 @@
|
||||
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
|
||||
|
||||
const modules = import.meta.globEager('./modules/*.js')
|
||||
const mockModules = []
|
||||
Object.keys(modules).forEach((key) => {
|
||||
mockModules.push(...modules[key].default)
|
||||
})
|
||||
import api from './api'
|
||||
|
||||
export function setupProdMockServer() {
|
||||
createProdMockServer(mockModules)
|
||||
createProdMockServer(api)
|
||||
}
|
||||
|
||||
20
package.json
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "vue-naive-admin",
|
||||
"version": "0.4.0",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
"lint:fix": "eslint --fix --ext .js,.vue .",
|
||||
"lint:staged": "lint-staged",
|
||||
"build": "vite build",
|
||||
"build:test": "vite build --mode test",
|
||||
"build:github": "vite build --mode github && esno ./build/script",
|
||||
@@ -16,6 +17,7 @@
|
||||
"@vueuse/core": "^8.4.2",
|
||||
"axios": "^0.21.4",
|
||||
"dayjs": "^1.11.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"md-editor-v3": "^1.11.4",
|
||||
"mockjs": "^1.1.0",
|
||||
"pinia": "^2.0.13",
|
||||
@@ -25,7 +27,7 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.3",
|
||||
"@commitlint/config-conventional": "^17.0.3",
|
||||
"@iconify/json": "^2.1.63",
|
||||
"@iconify/json": "^2.1.99",
|
||||
"@iconify/vue": "^3.2.1",
|
||||
"@vitejs/plugin-vue": "^1.10.2",
|
||||
"@vue/compiler-sfc": "^3.2.31",
|
||||
@@ -41,22 +43,30 @@
|
||||
"esno": "^0.13.0",
|
||||
"fs-extra": "^10.0.1",
|
||||
"husky": "^8.0.1",
|
||||
"naive-ui": "^2.30.3",
|
||||
"lint-staged": "^13.0.3",
|
||||
"naive-ui": "^2.32.1",
|
||||
"prettier": "^2.6.1",
|
||||
"rollup-plugin-visualizer": "^5.6.0",
|
||||
"sass": "^1.49.10",
|
||||
"unocss": "^0.43.2",
|
||||
"unplugin-auto-import": "^0.8.8",
|
||||
"unplugin-auto-import": "^0.9.2",
|
||||
"unplugin-icons": "^0.14.1",
|
||||
"unplugin-vue-components": "^0.17.21",
|
||||
"vite": "^2.9.9",
|
||||
"vite": "^3.0.9",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"vite-plugin-mock": "^2.9.6",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-setup-extend-plus": "^0.1.0"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "node_modules/cz-customizable"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,vue}": [
|
||||
"eslint --ext .js,.vue ."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
1744
pnpm-lock.yaml
generated
@@ -3,5 +3,4 @@ module.exports = {
|
||||
singleQuote: true,
|
||||
semi: false,
|
||||
endOfLine: 'lf',
|
||||
bracketSameLine: true,
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
.loading-svg {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
color: var(--primaryColor);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.loading-spin__container {
|
||||
@@ -45,7 +45,7 @@
|
||||
position: absolute;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
background-color: var(--primaryColor);
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 8px;
|
||||
-webkit-animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
* 初始化加载效果的svg格式logo
|
||||
* @param {string} id - 元素id
|
||||
*/
|
||||
function initSvgLogo(id) {
|
||||
const svgStr = `<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" style="fill:currentColor"></path></svg>`;
|
||||
const appEl = document.querySelector(id);
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = svgStr;
|
||||
function initSvgLogo(id) {
|
||||
const svgStr = `<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" style="fill:currentColor"></path></svg>`
|
||||
const appEl = document.querySelector(id)
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = svgStr
|
||||
if (appEl) {
|
||||
appEl.appendChild(div);
|
||||
appEl.appendChild(div)
|
||||
}
|
||||
}
|
||||
|
||||
function addThemeColorCssVars() {
|
||||
const key = '__THEME_COLOR__'
|
||||
const defaultColor = '#316c72';
|
||||
const themeColor = window.localStorage.getItem(key) || defaultColor;
|
||||
const cssVars = `--primaryColor: ${themeColor}`;
|
||||
document.documentElement.style.cssText = cssVars;
|
||||
const key = '__THEME_COLOR__'
|
||||
const defaultColor = '#316c72'
|
||||
const themeColor = window.localStorage.getItem(key) || defaultColor
|
||||
const cssVars = `--primary-color: ${themeColor}`
|
||||
document.documentElement.style.cssText = cssVars
|
||||
}
|
||||
|
||||
addThemeColorCssVars();
|
||||
addThemeColorCssVars()
|
||||
|
||||
initSvgLogo('#loadingLogo');
|
||||
initSvgLogo('#loadingLogo')
|
||||
|
||||
2
settings/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './theme.json'
|
||||
export * from './proxy-config'
|
||||
18
settings/proxy-config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const proxyConfigMappings = {
|
||||
dev: {
|
||||
prefix: '/api',
|
||||
target: 'http://localhost:8080',
|
||||
},
|
||||
test: {
|
||||
prefix: '/api',
|
||||
target: 'http://localhost:8080',
|
||||
},
|
||||
prod: {
|
||||
prefix: '/api',
|
||||
target: 'http://localhost:8080',
|
||||
},
|
||||
}
|
||||
|
||||
export function getProxyConfig(envType = 'dev') {
|
||||
return proxyConfigMappings[envType]
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"header": {
|
||||
"height": 60
|
||||
},
|
||||
"tags": {
|
||||
"visible": true,
|
||||
"height": 50
|
||||
},
|
||||
"header": {
|
||||
"height": 60
|
||||
},
|
||||
"naiveThemeOverrides": {
|
||||
"common": {
|
||||
"primaryColor": "#316C72FF",
|
||||
@@ -9,12 +9,3 @@
|
||||
<script setup>
|
||||
import AppProvider from '@/components/common/AppProvider.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#app {
|
||||
height: 100%;
|
||||
.n-config-provider {
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { defAxios as request } from '@/utils/http'
|
||||
|
||||
export const login = (data) => {
|
||||
return request({
|
||||
url: '/auth/login',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const refreshToken = () => {
|
||||
return request({
|
||||
url: '/auth/refreshToken',
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
6
src/api/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export default {
|
||||
getUser: () => request.get('/user'),
|
||||
refreshToken: () => request.post('/auth/refreshToken'),
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { defAxios as request } from '@/utils/http'
|
||||
|
||||
export function getPosts(params = {}) {
|
||||
return request({
|
||||
url: '/posts',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function getPostById({ id }) {
|
||||
return request({
|
||||
url: `/post/${id}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function savePost(id, data = {}) {
|
||||
if (id) {
|
||||
return request({
|
||||
url: `/post/${id}`,
|
||||
method: 'put',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
return request({
|
||||
url: '/post',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function deletePost(id) {
|
||||
return request({
|
||||
url: `/post/${id}`,
|
||||
method: 'delete',
|
||||
})
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { defAxios as request } from '@/utils/http'
|
||||
|
||||
export function getUsers(params = {}) {
|
||||
return request({
|
||||
url: '/users',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function getUser(id) {
|
||||
if (id) {
|
||||
return request({
|
||||
url: `/user/${id}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
return request({
|
||||
url: '/user',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function saveUser(data = {}, id) {
|
||||
if (id) {
|
||||
return request({
|
||||
url: '/user',
|
||||
method: 'put',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
return request({
|
||||
url: `/user/${id}`,
|
||||
method: 'put',
|
||||
data,
|
||||
})
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 825 B |
|
Before Width: | Height: | Size: 46 KiB |
BIN
src/assets/images/404.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 57 KiB |
BIN
src/assets/images/login_banner.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 23 KiB |
BIN
src/assets/images/login_bg.webp
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
1
src/assets/svg/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"><path fill="currentColor" 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>
|
||||
|
After Width: | Height: | Size: 811 B |
13
src/components/common/AppFooter.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<footer text-14 f-c-c flex-col color="#6a6a6a">
|
||||
<p>
|
||||
Copyright©2022
|
||||
<a href="https://github.com/zclzone" target="__blank" hover="decoration-underline color-primary"> 大脸怪</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="http://beian.miit.gov.cn/" target="__blank" hover="decoration-underline color-primary">
|
||||
赣ICP备2020015008号-1
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<n-config-provider :theme-overrides="themStore.naiveThemeOverrides">
|
||||
<n-config-provider wh-full :theme-overrides="naiveThemeOverrides">
|
||||
<n-loading-bar-provider>
|
||||
<n-dialog-provider>
|
||||
<n-notification-provider>
|
||||
@@ -16,24 +16,18 @@
|
||||
<script setup>
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { useLoadingBar, useDialog, useMessage, useNotification } from 'naive-ui'
|
||||
|
||||
import { useCssVar } from '@vueuse/core'
|
||||
import { useThemeStore } from '@/store/modules/theme'
|
||||
import { kebabCase } from 'lodash-es'
|
||||
import { setupMessage, setupDialog } from '@/utils/common/naiveTools'
|
||||
import { naiveThemeOverrides } from '~/settings'
|
||||
|
||||
const themStore = useThemeStore()
|
||||
watch(
|
||||
() => themStore.naiveThemeOverrides.common,
|
||||
(vars) => {
|
||||
for (const key in vars) {
|
||||
useCssVar(`--${key}`, document.documentElement).value = vars[key]
|
||||
if (key === 'primaryColor') {
|
||||
window.localStorage.setItem('__THEME_COLOR__', vars[key])
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
function setupCssVar() {
|
||||
const common = naiveThemeOverrides.common
|
||||
for (const key in common) {
|
||||
useCssVar(`--${kebabCase(key)}`, document.documentElement).value = common[key] || ''
|
||||
if (key === 'primaryColor') window.localStorage.setItem('__THEME_COLOR__', common[key] || '')
|
||||
}
|
||||
}
|
||||
|
||||
// 挂载naive组件的方法至window, 以便在全局使用
|
||||
function setupNaiveTools() {
|
||||
@@ -46,6 +40,7 @@ function setupNaiveTools() {
|
||||
|
||||
const NaiveProviderContent = defineComponent({
|
||||
setup() {
|
||||
setupCssVar()
|
||||
setupNaiveTools()
|
||||
},
|
||||
render() {
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
:class="{ overflow: isOverflow && showArrow }"
|
||||
:style="{
|
||||
transform: `translateX(${translateX}px)`,
|
||||
}">
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
25
src/components/custom/CustomIcon.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
import { renderCustomIcon } from '@/utils/icon'
|
||||
|
||||
const props = defineProps({
|
||||
/** 图标名称(图片的文件名) */
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 14,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const iconCom = computed(() => renderCustomIcon(props.icon, props))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="iconCom" />
|
||||
</template>
|
||||
24
src/components/custom/SvgIcon.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup name="SvgIcon">
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: 'icon-custom',
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'currentColor',
|
||||
},
|
||||
})
|
||||
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.icon}`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg aria-hidden="true" width="1em" height="1em">
|
||||
<use :xlink:href="symbolId" :fill="color" />
|
||||
</svg>
|
||||
</template>
|
||||
@@ -1,14 +1,18 @@
|
||||
<template>
|
||||
<n-breadcrumb>
|
||||
<n-breadcrumb-item v-for="item in route.matched" :key="item.path" @click="handleBreadClick(item.path)">
|
||||
<component :is="renderIcon(item.meta?.icon, { size: 16 })" v-if="item.meta?.icon" />
|
||||
<n-breadcrumb-item
|
||||
v-for="item in route.matched.filter((item) => !!item.meta?.title)"
|
||||
:key="item.path"
|
||||
@click="handleBreadClick(item.path)"
|
||||
>
|
||||
<component :is="getIcon(item.meta)" />
|
||||
{{ item.meta.title }}
|
||||
</n-breadcrumb-item>
|
||||
</n-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { renderIcon } from '@/utils/icon'
|
||||
import { renderCustomIcon, renderIcon } from '@/utils/icon'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -17,4 +21,10 @@ function handleBreadClick(path) {
|
||||
if (path === route.path) return
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
function getIcon(meta) {
|
||||
if (meta?.customIcon) return renderCustomIcon(meta.customIcon, { size: 18 })
|
||||
if (meta?.icon) return renderIcon(meta.icon, { size: 18 })
|
||||
return null
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<n-icon mr20 size="18" style="cursor: pointer" @click="toggle">
|
||||
<icon-ant-design:fullscreen-outlined v-if="isFullscreen" />
|
||||
<icon-ant-design:fullscreen-exit-outlined v-if="isFullscreen" />
|
||||
<icon-ant-design:fullscreen-outlined v-else />
|
||||
</n-icon>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<n-icon mr20 size="18" style="cursor: pointer" @click="handleLinkClick">
|
||||
<n-icon mr-20 size="18" style="cursor: pointer" @click="handleLinkClick">
|
||||
<icon-mdi:github />
|
||||
</n-icon>
|
||||
</template>
|
||||
@@ -9,5 +9,3 @@ function handleLinkClick() {
|
||||
window.open('https://github.com/zclzone/vue-naive-admin')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<n-icon size="20" style="cursor: pointer" @click="appStore.switchCollapsed">
|
||||
<n-icon size="20" cursor-pointer @click="appStore.switchCollapsed">
|
||||
<icon-mdi:format-indent-increase v-if="appStore.collapsed" />
|
||||
<icon-mdi:format-indent-decrease v-else />
|
||||
</n-icon>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<n-dropdown :options="options" @select="handleSelect">
|
||||
<div class="avatar">
|
||||
<img :src="userStore.avatar" />
|
||||
<div flex items-center cursor-pointer>
|
||||
<img :src="userStore.avatar" mr10 w-35 h-35 rounded-full />
|
||||
<span>{{ userStore.name }}</span>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
@@ -35,18 +35,3 @@ function handleSelect(key) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
img {
|
||||
width: 100%;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<div class="h-left">
|
||||
<header px-15 h-full flex items-center>
|
||||
<div flex items-center>
|
||||
<MenuCollapse />
|
||||
<BreadCrumb ml-15 />
|
||||
</div>
|
||||
<div class="h-right">
|
||||
<div ml-auto flex items-center>
|
||||
<GithubSite />
|
||||
<FullScreen />
|
||||
<UserAvatar />
|
||||
@@ -19,21 +19,3 @@ import FullScreen from './components/FullScreen.vue'
|
||||
import UserAvatar from './components/UserAvatar.vue'
|
||||
import GithubSite from './components/GithubSite.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
padding: 0 15px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.h-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.h-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<router-link h-60 f-c-c to="/">
|
||||
<icon-custom-logo text-36></icon-custom-logo>
|
||||
<icon-custom-logo text-36 color-primary></icon-custom-logo>
|
||||
<h2 v-show="!appStore.collapsed" ml-10 color-primary text-16 font-bold max-w-140 flex-shrink-0>
|
||||
{{ title }}
|
||||
</h2>
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
const title = import.meta.env.VITE_APP_TITLE
|
||||
const title = import.meta.env.VITE_TITLE
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
:collapsed-width="64"
|
||||
:options="menuOptions"
|
||||
:value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name"
|
||||
@update:value="handleMenuSelect" />
|
||||
@update:value="handleMenuSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -15,7 +16,7 @@ import { usePermissionStore } from '@/store/modules/permission'
|
||||
|
||||
import { isExternal } from '@/utils/is'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { renderIcon } from '@/utils/icon'
|
||||
import { renderCustomIcon, renderIcon } from '@/utils/icon'
|
||||
|
||||
const router = useRouter()
|
||||
const permissionStore = usePermissionStore()
|
||||
@@ -23,7 +24,7 @@ const appStore = useAppStore()
|
||||
const { currentRoute } = router
|
||||
|
||||
const menuOptions = computed(() => {
|
||||
return permissionStore.menus.map((item) => getMenuItem(item)).sort((a, b) => a.index - b.index)
|
||||
return permissionStore.menus.map((item) => getMenuItem(item)).sort((a, b) => a.order - b.order)
|
||||
})
|
||||
|
||||
function resolvePath(basePath, path) {
|
||||
@@ -42,8 +43,8 @@ function getMenuItem(route, basePath = '') {
|
||||
label: (route.meta && route.meta.title) || route.name,
|
||||
key: route.name,
|
||||
path: resolvePath(basePath, route.path),
|
||||
icon: route.meta?.icon ? renderIcon(route.meta?.icon, { size: 16 }) : renderIcon('mdi:circle-outline', { size: 8 }),
|
||||
index: route.meta?.index || 0,
|
||||
icon: getIcon(route.meta),
|
||||
order: route.meta?.order || 0,
|
||||
}
|
||||
|
||||
const visibleChildren = route.children ? route.children.filter((item) => item.name && !item.isHidden) : []
|
||||
@@ -57,27 +58,31 @@ function getMenuItem(route, basePath = '') {
|
||||
label: singleRoute.meta?.title || singleRoute.name,
|
||||
key: singleRoute.name,
|
||||
path: resolvePath(menuItem.path, singleRoute.path),
|
||||
icon: singleRoute.meta?.icon
|
||||
? renderIcon(singleRoute.meta?.icon, { size: 16 })
|
||||
: renderIcon('mdi:circle-outline', { size: 8 }),
|
||||
index: menuItem.index,
|
||||
icon: getIcon(singleRoute.meta),
|
||||
order: menuItem.order,
|
||||
}
|
||||
const visibleItems = singleRoute.children ? singleRoute.children.filter((item) => item.name && !item.isHidden) : []
|
||||
|
||||
if (visibleItems.length === 1) {
|
||||
menuItem = getMenuItem(visibleItems[0], menuItem.path)
|
||||
} else if (visibleItems.length > 1) {
|
||||
menuItem.children = visibleItems.map((item) => getMenuItem(item, menuItem.path)).sort((a, b) => a.index - b.index)
|
||||
menuItem.children = visibleItems.map((item) => getMenuItem(item, menuItem.path)).sort((a, b) => a.order - b.order)
|
||||
}
|
||||
} else {
|
||||
menuItem.children = visibleChildren
|
||||
.map((item) => getMenuItem(item, menuItem.path))
|
||||
.sort((a, b) => a.index - b.index)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
}
|
||||
|
||||
return menuItem
|
||||
}
|
||||
|
||||
function getIcon(meta) {
|
||||
if (meta?.customIcon) return renderCustomIcon(meta.customIcon, { size: 18 })
|
||||
if (meta?.icon) return renderIcon(meta.icon, { size: 18 })
|
||||
return null
|
||||
}
|
||||
|
||||
function handleMenuSelect(key, item) {
|
||||
if (isExternal(item.path)) {
|
||||
window.open(item.path)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<n-dropdown
|
||||
:show="dropdownShow"
|
||||
:show="show"
|
||||
:options="options"
|
||||
:x="x"
|
||||
:y="y"
|
||||
placement="bottom-start"
|
||||
@clickoutside="handleHideDropdown"
|
||||
@select="handleSelect" />
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -71,15 +72,6 @@ const options = computed(() => [
|
||||
},
|
||||
])
|
||||
|
||||
const dropdownShow = computed({
|
||||
get() {
|
||||
return props.show
|
||||
},
|
||||
set(show) {
|
||||
emit('update:show', show)
|
||||
},
|
||||
})
|
||||
|
||||
const actionMap = new Map([
|
||||
[
|
||||
'reload',
|
||||
@@ -114,7 +106,7 @@ const actionMap = new Map([
|
||||
])
|
||||
|
||||
function handleHideDropdown() {
|
||||
dropdownShow.value = false
|
||||
emit('update:show', false)
|
||||
}
|
||||
|
||||
function handleSelect(key) {
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
<template>
|
||||
<ScrollX :class="`h-${useTheme.tags.height}`">
|
||||
<ScrollX>
|
||||
<n-tag
|
||||
v-for="tag in tagsStore.tags"
|
||||
:key="tag.path"
|
||||
class="px-15 mx-5 rounded-4 cursor-pointer hover:color-primary"
|
||||
:type="tagsStore.activeTag === tag.path ? 'primary' : 'default'"
|
||||
:closable="tagsStore.tags.length > 1"
|
||||
@click="handleTagClick(tag.path)"
|
||||
@close.stop="tagsStore.removeTag(tag.path)"
|
||||
@contextmenu.prevent="handleContextMenu($event, tag)">
|
||||
@contextmenu.prevent="handleContextMenu($event, tag)"
|
||||
>
|
||||
{{ tag.title }}
|
||||
</n-tag>
|
||||
<ContextMenu
|
||||
v-if="contextMenuOption.show"
|
||||
v-model:show="contextMenuOption.show"
|
||||
:current-path="contextMenuOption.currentPath"
|
||||
:x="contextMenuOption.x"
|
||||
:y="contextMenuOption.y"
|
||||
/>
|
||||
</ScrollX>
|
||||
|
||||
<ContextMenu
|
||||
v-model:show="contextMenuOption.show"
|
||||
:current-path="contextMenuOption.currentPath"
|
||||
:x="contextMenuOption.x"
|
||||
:y="contextMenuOption.y" />
|
||||
</template>
|
||||
|
||||
<script setup name="Tags">
|
||||
import ContextMenu from './ContextMenu.vue'
|
||||
import { useTagsStore } from '@/store/modules/tags'
|
||||
import { useThemeStore } from '@/store/modules/theme'
|
||||
import ScrollX from '@/components/common/ScrollX.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const tagsStore = useTagsStore()
|
||||
const useTheme = useThemeStore()
|
||||
|
||||
const contextMenuOption = reactive({
|
||||
show: false,
|
||||
@@ -72,25 +73,14 @@ async function handleContextMenu(e, tagItem) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.n-tag {
|
||||
padding: 0 15px;
|
||||
margin: 0 5px;
|
||||
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: var(--primaryColor);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
color: var(--primaryColor);
|
||||
}
|
||||
<style>
|
||||
.n-tag__close {
|
||||
box-sizing: content-box;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
padding: 2px;
|
||||
transform: scale(0.9);
|
||||
transform: translateX(5px);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
<template>
|
||||
<n-layout has-sider style="height: 100%">
|
||||
<n-layout has-sider h-full>
|
||||
<n-layout-sider
|
||||
bordered
|
||||
collapse-mode="width"
|
||||
:collapsed-width="64"
|
||||
:width="220"
|
||||
:native-scrollbar="false"
|
||||
:collapsed="appStore.collapsed">
|
||||
:collapsed="appStore.collapsed"
|
||||
>
|
||||
<SideBar />
|
||||
</n-layout-sider>
|
||||
<n-layout>
|
||||
<n-layout-header :style="{ height: useTheme.header.height + 'px' }">
|
||||
<n-layout-header bg-white border-b bc-eee :style="`height: ${header.height ?? 60}px`">
|
||||
<AppHeader />
|
||||
</n-layout-header>
|
||||
|
||||
<n-layout style="background-color: #f5f6fb" :style="`height: calc(100% - ${useTheme.header.height}px)`">
|
||||
<AppTags v-if="useTheme.tags.visible" />
|
||||
<n-layout bg="#f5f6fb" :style="`height: calc(100% - ${header.height ?? 60}px)`">
|
||||
<AppTags v-if="tags.visible" :style="`height: ${tags.height ?? 50}px`" />
|
||||
<AppMain
|
||||
class="cur-scroll border-t bc-eee"
|
||||
:style="{
|
||||
height: `calc(100% - ${useTheme.tags.visible ? useTheme.tags.height : 0}px)`,
|
||||
overflow: 'auto',
|
||||
}" />
|
||||
class="cus-scroll border-t bc-eee overflow-auto"
|
||||
:style="{ height: `calc(100% - ${tags.visible ? tags.height ?? 50 : 0}px)` }"
|
||||
/>
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
@@ -32,17 +31,8 @@ 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'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { header, tags } from '~/settings'
|
||||
|
||||
const useTheme = useThemeStore()
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.n-layout-header {
|
||||
height: 60px;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import '@/styles/reset.css'
|
||||
import '@/styles/variables.css'
|
||||
import '@/styles/index.scss'
|
||||
import 'uno.css'
|
||||
import 'virtual:svg-icons-register'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { setupRouter } from '@/router'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const baseTitle = import.meta.env.VITE_APP_TITLE
|
||||
const baseTitle = import.meta.env.VITE_TITLE
|
||||
|
||||
export function createPageTitleGuard(router) {
|
||||
router.afterEach((to) => {
|
||||
|
||||
@@ -3,42 +3,40 @@ import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { NOT_FOUND_ROUTE } from '@/router/routes'
|
||||
import { getToken, refreshAccessToken, removeToken } from '@/utils/token'
|
||||
import { toLogin } from '@/utils/auth'
|
||||
import { isNullOrWhitespace } from '@/utils/is'
|
||||
|
||||
const WHITE_LIST = ['/login', '/redirect']
|
||||
const WHITE_LIST = ['/login']
|
||||
export function createPermissionGuard(router) {
|
||||
const userStore = useUserStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
router.beforeEach(async (to) => {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/' })
|
||||
} else {
|
||||
if (userStore.userId) {
|
||||
// 已经拿到用户信息
|
||||
refreshAccessToken()
|
||||
next()
|
||||
} else {
|
||||
await userStore.getUserInfo().catch((error) => {
|
||||
removeToken()
|
||||
toLogin()
|
||||
$message.error(error.message || '获取用户信息失败!')
|
||||
return
|
||||
})
|
||||
const accessRoutes = permissionStore.generateRoutes(userStore.role)
|
||||
accessRoutes.forEach((route) => {
|
||||
!router.hasRoute(route.name) && router.addRoute(route)
|
||||
})
|
||||
router.addRoute(NOT_FOUND_ROUTE)
|
||||
next({ ...to, replace: true })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (WHITE_LIST.includes(to.path)) {
|
||||
next()
|
||||
} else {
|
||||
next({ path: '/login', query: { ...to.query, redirect: to.path } })
|
||||
}
|
||||
|
||||
/** 没有token的情况 */
|
||||
if (isNullOrWhitespace(token)) {
|
||||
if (WHITE_LIST.includes(to.path)) return true
|
||||
return { path: 'login', query: { ...to.query, redirect: to.path } }
|
||||
}
|
||||
|
||||
/** 有token的情况 */
|
||||
|
||||
if (to.path === '/login') return { path: '/', replace: true }
|
||||
|
||||
// 已经拿到用户信息
|
||||
if (userStore.userId) {
|
||||
refreshAccessToken()
|
||||
return true
|
||||
}
|
||||
await userStore.getUserInfo().catch((error) => {
|
||||
removeToken()
|
||||
toLogin()
|
||||
$message.error(error.message || '获取用户信息失败!')
|
||||
})
|
||||
const accessRoutes = permissionStore.generateRoutes(userStore.role)
|
||||
accessRoutes.forEach((route) => {
|
||||
!router.hasRoute(route.name) && router.addRoute(route)
|
||||
})
|
||||
router.addRoute(NOT_FOUND_ROUTE)
|
||||
return { ...to, replace: true }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router
|
||||
import { setupRouterGuard } from './guard'
|
||||
import { basicRoutes } from './routes'
|
||||
|
||||
const isHash = !!import.meta.env.VITE_APP_USE_HASH
|
||||
const isHash = import.meta.env.VITE_USE_HASH === 'true'
|
||||
export const router = createRouter({
|
||||
history: isHash ? createWebHashHistory('/') : createWebHistory('/'),
|
||||
routes: [],
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Layout from '@/layout/index.vue'
|
||||
import Home from '@/views/dashboard/index.vue'
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export const basicRoutes = [
|
||||
{
|
||||
@@ -8,21 +7,9 @@ export const basicRoutes = [
|
||||
component: () => import('@/views/error-page/404.vue'),
|
||||
isHidden: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'REDIRECT',
|
||||
path: '/redirect',
|
||||
component: Layout,
|
||||
isHidden: true,
|
||||
children: [
|
||||
{
|
||||
name: 'REDIRECT_NAME',
|
||||
path: '',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'LOGIN',
|
||||
name: 'Login',
|
||||
path: '/login',
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
isHidden: true,
|
||||
@@ -31,97 +18,6 @@ export const basicRoutes = [
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Dashboard',
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/home',
|
||||
meta: {
|
||||
title: 'Dashboard',
|
||||
icon: 'mdi:chart-bar',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Home',
|
||||
path: 'home',
|
||||
component: Home,
|
||||
meta: {
|
||||
title: '首页',
|
||||
icon: 'mdi:home',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'ErrorPage',
|
||||
path: '/error-page',
|
||||
component: Layout,
|
||||
redirect: '/error-page/404',
|
||||
meta: {
|
||||
title: '错误页',
|
||||
icon: 'mdi:alert-circle-outline',
|
||||
index: 4,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'ERROR-404',
|
||||
path: '404',
|
||||
component: () => import('@/views/error-page/404.vue'),
|
||||
meta: {
|
||||
title: '404',
|
||||
icon: 'mdi:alert-circle-outline',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Test',
|
||||
path: '/test',
|
||||
component: Layout,
|
||||
redirect: '/test/unocss',
|
||||
meta: {
|
||||
title: '基础功能测试',
|
||||
icon: 'mdi:menu',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Unocss',
|
||||
path: 'unocss',
|
||||
component: () => import('@/views/test-page/unocss/index.vue'),
|
||||
meta: {
|
||||
title: '测试unocss',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Message',
|
||||
path: 'message',
|
||||
component: () => import('@/views/test-page/message/index.vue'),
|
||||
meta: {
|
||||
title: '测试Message',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Dialog',
|
||||
path: 'dialog',
|
||||
component: () => import('@/views/test-page/dialog/index.vue'),
|
||||
meta: {
|
||||
title: '测试Dialog',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'TestKeepAlive',
|
||||
path: 'keep-alive',
|
||||
component: () => import('@/views/test-page/keep-alive/index.vue'),
|
||||
meta: {
|
||||
title: '测试Keep-Alive',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'ExternalLink',
|
||||
path: '/external-link',
|
||||
@@ -129,6 +25,7 @@ export const basicRoutes = [
|
||||
meta: {
|
||||
title: '外部链接',
|
||||
icon: 'mdi:link-variant',
|
||||
order: 2,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
@@ -166,10 +63,10 @@ export const NOT_FOUND_ROUTE = {
|
||||
isHidden: true,
|
||||
}
|
||||
|
||||
const modules = import.meta.globEager('./modules/*.js')
|
||||
const modules = import.meta.glob('@/views/**/route.js', { eager: true })
|
||||
const asyncRoutes = []
|
||||
Object.keys(modules).forEach((key) => {
|
||||
asyncRoutes.push(...modules[key].default)
|
||||
asyncRoutes.push(modules[key].default)
|
||||
})
|
||||
|
||||
export { asyncRoutes }
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import Layout from '@/layout/index.vue'
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'Example',
|
||||
path: '/example',
|
||||
component: Layout,
|
||||
redirect: '/example/table',
|
||||
meta: {
|
||||
title: '组件示例',
|
||||
role: ['admin'],
|
||||
icon: 'mdi:menu',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Table',
|
||||
path: 'table',
|
||||
component: () => import('@/views/examples/table/index.vue'),
|
||||
redirect: '/example/table/post',
|
||||
meta: {
|
||||
title: '表格',
|
||||
role: ['admin'],
|
||||
icon: 'mdi:table',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'PostList',
|
||||
path: 'post',
|
||||
component: () => import('@/views/examples/table/post/index.vue'),
|
||||
meta: {
|
||||
title: '文章列表',
|
||||
role: ['admin'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'PostCreate',
|
||||
path: 'post-create',
|
||||
component: () => import('@/views/examples/table/post/PostCreate.vue'),
|
||||
meta: {
|
||||
title: '创建文章',
|
||||
role: ['admin'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -1 +0,0 @@
|
||||
export { default as themeSettings } from './theme.json'
|
||||
@@ -2,10 +2,15 @@ import { defineStore } from 'pinia'
|
||||
import { asyncRoutes, basicRoutes } from '@/router/routes'
|
||||
|
||||
function hasPermission(route, role) {
|
||||
// * 不需要权限直接返回true
|
||||
if (!route.meta?.requireAuth) return true
|
||||
|
||||
const routeRole = route.meta?.role ? route.meta.role : []
|
||||
if (!role.length || !routeRole.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
// * 登录用户没有角色或者路由没有设置角色判定为没有权限
|
||||
if (!role.length || !routeRole.length) return false
|
||||
|
||||
// * 路由指定的角色包含任一登录用户角色则判定有权限
|
||||
return role.some((item) => routeRole.includes(item))
|
||||
}
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@ import { sStorage } from '@/utils/cache'
|
||||
export const activeTag = sStorage.get('activeTag')
|
||||
export const tags = sStorage.get('tags')
|
||||
|
||||
export const WITHOUT_TAG_PATHS = ['/404', '/login', '/redirect']
|
||||
export const WITHOUT_TAG_PATHS = ['/404', '/login']
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { themeSettings } from '@/settings'
|
||||
export const useThemeStore = defineStore('theme', {
|
||||
state() {
|
||||
return {
|
||||
tags: themeSettings.tag || { visible: true, height: 50 },
|
||||
header: themeSettings.header || { height: 60 },
|
||||
naiveThemeOverrides: themeSettings.naiveThemeOverrides || {
|
||||
common: {
|
||||
primaryColor: '#316C72FF',
|
||||
primaryColorHover: '#316C72E3',
|
||||
primaryColorPressed: '#2B4C59FF',
|
||||
primaryColorSuppl: '#316C7263',
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setTabVisible(visible) {
|
||||
this.tags.visible = visible
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { getUser } from '@/api/user'
|
||||
import { removeToken } from '@/utils/token'
|
||||
import { toLogin } from '@/utils/auth'
|
||||
import api from '@/api'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state() {
|
||||
@@ -26,7 +26,7 @@ export const useUserStore = defineStore('user', {
|
||||
actions: {
|
||||
async getUserInfo() {
|
||||
try {
|
||||
const res = await getUser()
|
||||
const res = await api.getUser()
|
||||
if (res.code === 0) {
|
||||
const { id, name, avatar, role } = res.data
|
||||
this.userInfo = { id, name, avatar, role }
|
||||
|
||||
@@ -11,6 +11,11 @@ body {
|
||||
font-family: 'Encode Sans Condensed', sans-serif;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* router view transition fade-slide */
|
||||
.fade-slide-leave-active,
|
||||
.fade-slide-enter-active {
|
||||
@@ -28,27 +33,24 @@ body {
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
.cur-scroll {
|
||||
&::-webkit-scrollbar{
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
.cus-scroll {
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb{
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-corner{
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #bfbfbf;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb:hover{
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
:root {
|
||||
--primaryColor: #316c72;
|
||||
}
|
||||
@@ -3,6 +3,6 @@ import { router } from '@/router'
|
||||
export function toLogin() {
|
||||
router.replace({
|
||||
path: '/login',
|
||||
query: { ...router.currentRoute.query, redirect: router.currentRoute.path },
|
||||
query: { ...router.currentRoute.value.query, redirect: router.currentRoute.value.path },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { repReject, repResolve, reqReject, reqResolve } from './interceptors'
|
||||
|
||||
export function createAxios(options = {}) {
|
||||
const defaultOptions = {
|
||||
baseURL: import.meta.env.VITE_APP_BASE_API,
|
||||
timeout: 12000,
|
||||
}
|
||||
const service = axios.create({
|
||||
@@ -15,8 +14,6 @@ export function createAxios(options = {}) {
|
||||
return service
|
||||
}
|
||||
|
||||
export const defAxios = createAxios()
|
||||
|
||||
export const testAxios = createAxios({
|
||||
baseURL: import.meta.env.VITE_APP_BASE_API_TEST,
|
||||
export default createAxios({
|
||||
baseURL: import.meta.env.VITE_BASE_API,
|
||||
})
|
||||
|
||||
@@ -16,17 +16,14 @@ export function reqResolve(config) {
|
||||
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
/**
|
||||
* * 未登录或者token过期的情况下
|
||||
* * 跳转登录页重新登录,携带当前路由及参数,登录成功会回到原来的页面
|
||||
*/
|
||||
// * 未登录或者token过期的情况下,跳转登录页重新登录
|
||||
toLogin()
|
||||
return Promise.reject({ code: '-1', message: '未登录' })
|
||||
}
|
||||
|
||||
/**
|
||||
* * jwt token
|
||||
* ! 认证方案: Bearer
|
||||
* * 加上 token
|
||||
* ! 认证方案: JWT Bearer
|
||||
*/
|
||||
config.headers.Authorization = config.headers.Authorization || 'Bearer ' + token
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { h } from 'vue'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { NIcon } from 'naive-ui'
|
||||
import SvgIcon from '@/components/custom/SvgIcon.vue'
|
||||
|
||||
export function renderIcon(icon, props = { size: 12 }) {
|
||||
return () => h(NIcon, props, { default: () => h(Icon, { icon }) })
|
||||
}
|
||||
|
||||
export function renderCustomIcon(icon, props = { size: 12 }) {
|
||||
return () => h(NIcon, props, { default: () => h(SvgIcon, { icon }) })
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { lStorage } from './cache'
|
||||
import { refreshToken } from '@/api/auth'
|
||||
import api from '@/api'
|
||||
|
||||
const TOKEN_CODE = 'access_token'
|
||||
const DURATION = 6 * 60 * 60
|
||||
@@ -25,7 +25,7 @@ export async function refreshAccessToken() {
|
||||
// token生成或者刷新后30分钟内不执行刷新
|
||||
if (new Date().getTime() - time <= 1000 * 60 * 30) return
|
||||
try {
|
||||
const res = await refreshToken()
|
||||
const res = await api.refreshToken()
|
||||
if (res.code === 0) {
|
||||
setToken(res.data.token)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div h-full flex>
|
||||
<n-result m-auto status="404" description="抱歉,您访问的页面不存在。">
|
||||
<template #icon>
|
||||
<img src="@/assets/images/404.png" width="500" />
|
||||
<img src="@/assets/images/404.webp" width="500" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<n-button @click="replace('/')">返回首页</n-button>
|
||||
|
||||
24
src/views/error-page/route.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: 'ErrorPage',
|
||||
path: '/error-page',
|
||||
component: Layout,
|
||||
redirect: '/error-page/404',
|
||||
meta: {
|
||||
title: '错误页',
|
||||
icon: 'mdi:alert-circle-outline',
|
||||
order: 99,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'ERROR-404',
|
||||
path: '404',
|
||||
component: () => import('./404.vue'),
|
||||
meta: {
|
||||
title: '404',
|
||||
icon: 'tabler:error-404',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
v-model="post.title"
|
||||
class="flex-1 pt-15 pb-15 mr-20 text-20 font-bold color-primary"
|
||||
type="text"
|
||||
placeholder="输入文章标题..." />
|
||||
placeholder="输入文章标题..."
|
||||
/>
|
||||
<n-button type="primary" style="width: 80px" :loading="btnLoading" @click="handleSavePost">保存</n-button>
|
||||
</div>
|
||||
<MdEditor v-model="post.content" style="height: calc(100vh - 220px)" />
|
||||
|
||||
13
src/views/examples/table/post/api.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export default {
|
||||
getPosts: (params = {}) => request.get('posts', { params }),
|
||||
getPostById: (id) => request.get(`/post/${id}`),
|
||||
savePost: (id, data = {}) => {
|
||||
if (id) {
|
||||
return request.put(`/post/${id}`, data)
|
||||
}
|
||||
return request.post('/post', data)
|
||||
},
|
||||
deletePost: (id) => request.delete(`/post/${id}`),
|
||||
}
|
||||
@@ -11,7 +11,8 @@
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
:row-key="(row) => row.id"
|
||||
@update:checked-row-keys="handleCheck" />
|
||||
@update:checked-row-keys="handleCheck"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { h } from 'vue'
|
||||
import { NButton, NSwitch } from 'naive-ui'
|
||||
import { getPosts } from '@/api/post'
|
||||
import { formatDateTime } from '@/utils'
|
||||
import api from './api'
|
||||
import { renderIcon } from '@/utils/icon'
|
||||
|
||||
export const usePostTable = () => {
|
||||
// refs
|
||||
@@ -34,8 +35,9 @@ export const usePostTable = () => {
|
||||
if (row && row.id) {
|
||||
row.recommending = true
|
||||
setTimeout(() => {
|
||||
$message.success(row.isRecommend ? '已取消推荐' : '已推荐')
|
||||
row.isRecommend = !row.isRecommend
|
||||
row.recommending = false
|
||||
$message.success(row.isRecommend ? '已推荐' : '已取消推荐')
|
||||
}, 800)
|
||||
}
|
||||
}
|
||||
@@ -44,8 +46,9 @@ export const usePostTable = () => {
|
||||
if (row && row.id) {
|
||||
row.publishing = true
|
||||
setTimeout(() => {
|
||||
$message.success(row.isPublish ? '已取消推荐' : '已推荐')
|
||||
row.isPublish = !row.isPublish
|
||||
row.publishing = false
|
||||
$message.success(row.isPublish ? '已发布' : '已取消发布')
|
||||
}, 800)
|
||||
}
|
||||
}
|
||||
@@ -53,12 +56,13 @@ export const usePostTable = () => {
|
||||
function initColumns() {
|
||||
columns.value = [
|
||||
{ type: 'selection' },
|
||||
{ title: '标题', key: 'title', width: 150 },
|
||||
{ title: '分类', key: 'category', width: 80 },
|
||||
{ title: '标题', key: 'title', width: 150, ellipsis: { tooltip: true } },
|
||||
{ title: '分类', key: 'category', width: 80, ellipsis: { tooltip: true } },
|
||||
{
|
||||
title: '描述',
|
||||
key: 'description',
|
||||
width: 200,
|
||||
ellipsis: { tooltip: true },
|
||||
},
|
||||
{ title: '创建人', key: 'author', width: 80 },
|
||||
{
|
||||
@@ -86,7 +90,8 @@ export const usePostTable = () => {
|
||||
render(row) {
|
||||
return h(NSwitch, {
|
||||
size: 'small',
|
||||
defaultValue: row['isRecommend'],
|
||||
value: row['isRecommend'],
|
||||
rubberBand: false,
|
||||
loading: !!row.recommending,
|
||||
onUpdateValue: () => handleRecommend(row),
|
||||
})
|
||||
@@ -101,7 +106,8 @@ export const usePostTable = () => {
|
||||
render(row) {
|
||||
return h(NSwitch, {
|
||||
size: 'small',
|
||||
defaultValue: row['isPublish'],
|
||||
rubberBand: false,
|
||||
value: row['isPublish'],
|
||||
loading: !!row.publishing,
|
||||
onUpdateValue: () => handlePublish(row),
|
||||
})
|
||||
@@ -123,7 +129,7 @@ export const usePostTable = () => {
|
||||
style: 'margin-left: 15px;',
|
||||
onClick: () => handleDelete(row),
|
||||
},
|
||||
{ default: () => '删除' }
|
||||
{ default: () => '删除', icon: renderIcon('material-symbols:delete-outline', { size: 14 }) }
|
||||
),
|
||||
]
|
||||
},
|
||||
@@ -133,7 +139,7 @@ export const usePostTable = () => {
|
||||
|
||||
async function getTableData() {
|
||||
try {
|
||||
const res = await getPosts()
|
||||
const res = await api.getPosts()
|
||||
if (res.code === 0) {
|
||||
return res.data
|
||||
}
|
||||
|
||||
53
src/views/examples/table/route.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: 'Example',
|
||||
path: '/example',
|
||||
component: Layout,
|
||||
redirect: '/example/table',
|
||||
meta: {
|
||||
title: '组件示例',
|
||||
icon: 'mdi:menu',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
order: 3,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Table',
|
||||
path: 'table',
|
||||
component: () => import('./index.vue'),
|
||||
redirect: '/example/table/post',
|
||||
meta: {
|
||||
title: '表格',
|
||||
icon: 'mdi:table',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'PostList',
|
||||
path: 'post',
|
||||
component: () => import('./post/index.vue'),
|
||||
meta: {
|
||||
title: '文章列表',
|
||||
icon: 'material-symbols:auto-awesome-outline-rounded',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'PostCreate',
|
||||
path: 'post-create',
|
||||
component: () => import('./post/PostCreate.vue'),
|
||||
meta: {
|
||||
title: '创建文章',
|
||||
icon: 'material-symbols:auto-awesome-outline-rounded',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
5
src/views/login/api.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export default {
|
||||
login: (data) => request.post('/auth/login', data),
|
||||
}
|
||||
@@ -1,25 +1,22 @@
|
||||
<template>
|
||||
<div class="login-bg" f-c-c h-full>
|
||||
<div class="login-wrapper" flex w-full max-w-1020>
|
||||
<div p-40 border-r border-gray-200>
|
||||
<img src="@/assets/images/login_banner.png" height="380" alt="login_banner" />
|
||||
<div class="cus-scroll h-full py-15 flex-col overflow-auto bg-cover" :style="{ backgroundImage: `url(${bgImg})` }">
|
||||
<div m-auto p-15 f-c-c min-w-345 rounded-10 card-shadow bg-white dark:bg-dark bg-opacity-60>
|
||||
<div w-380 hidden md:block px-20 py-35>
|
||||
<img src="@/assets/images/login_banner.webp" w-full alt="login_banner" />
|
||||
</div>
|
||||
|
||||
<div w-full f-c-c flex-col>
|
||||
<h5 f-c-c w-full p-15 text-24 font-normal color="#6a6a6a">
|
||||
<icon-custom-logo mr30 text-50 />
|
||||
{{ title }}
|
||||
</h5>
|
||||
<div mt-30 w-full max-w-360>
|
||||
<div w-320 flex-col px-20 py-35>
|
||||
<h5 f-c-c text-24 font-normal color="#6a6a6a"><icon-custom-logo mr-30 text-50 color-primary />{{ title }}</h5>
|
||||
<div mt-30>
|
||||
<n-input
|
||||
v-model:value="loginInfo.name"
|
||||
autofocus
|
||||
class="text-16 items-center h-50 pl-10"
|
||||
placeholder="admin"
|
||||
:maxlength="20">
|
||||
</n-input>
|
||||
:maxlength="20"
|
||||
/>
|
||||
</div>
|
||||
<div mt-30 w-full max-w-360>
|
||||
<div mt-30>
|
||||
<n-input
|
||||
v-model:value="loginInfo.password"
|
||||
class="text-16 items-center h-50 pl-10"
|
||||
@@ -27,31 +24,36 @@
|
||||
show-password-on="mousedown"
|
||||
placeholder="123456"
|
||||
:maxlength="20"
|
||||
@keydown.enter="handleLogin" />
|
||||
@keydown.enter="handleLogin"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div mt-20 w-full max-w-360>
|
||||
<div mt-20>
|
||||
<n-checkbox :checked="isRemember" label="记住我" :on-update:checked="(val) => (isRemember = val)" />
|
||||
</div>
|
||||
|
||||
<div mt-20 w-full max-w-360>
|
||||
<n-button w-full h-50 rounded-5 text-16 type="primary" @click="handleLogin">登录</n-button>
|
||||
<div mt-20>
|
||||
<n-button w-full h-50 rounded-5 text-16 type="primary" :loading="loging" @click="handleLogin">
|
||||
登录
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AppFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { login } from '@/api/auth'
|
||||
import { lStorage } from '@/utils/cache'
|
||||
import { setToken } from '@/utils/token'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import bgImg from '@/assets/images/login_bg.webp'
|
||||
import api from './api'
|
||||
|
||||
const title = import.meta.env.VITE_APP_TITLE
|
||||
const title = import.meta.env.VITE_TITLE
|
||||
|
||||
const router = useRouter()
|
||||
const query = unref(router.currentRoute).query
|
||||
const { query } = useRoute()
|
||||
|
||||
const loginInfo = ref({
|
||||
name: '',
|
||||
@@ -69,6 +71,7 @@ function initLoginInfo() {
|
||||
}
|
||||
|
||||
const isRemember = useStorage('isRemember', false)
|
||||
const loging = ref(false)
|
||||
async function handleLogin() {
|
||||
const { name, password } = loginInfo.value
|
||||
if (!name || !password) {
|
||||
@@ -77,7 +80,8 @@ async function handleLogin() {
|
||||
}
|
||||
try {
|
||||
$message.loading('正在验证...')
|
||||
const res = await login({ name, password: password.toString() })
|
||||
loging.value = true
|
||||
const res = await api.login({ name, password: password.toString() })
|
||||
if (res.code === 0) {
|
||||
$message.success('登录成功')
|
||||
setToken(res.data.token)
|
||||
@@ -99,17 +103,6 @@ async function handleLogin() {
|
||||
} catch (error) {
|
||||
$message.error(error.message)
|
||||
}
|
||||
loging.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-bg {
|
||||
background-image: url(@/assets/images/login_bg.jpg);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.login-wrapper {
|
||||
box-shadow: 1.5px 3.99px 27px 0px rgb(0 0 0 / 10%);
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<template></template>
|
||||
|
||||
<script setup>
|
||||
const { currentRoute, replace } = useRouter()
|
||||
|
||||
const { query } = currentRoute.value
|
||||
let { redirect } = query
|
||||
Reflect.deleteProperty(query, 'redirect')
|
||||
|
||||
if (Array.isArray(redirect)) {
|
||||
redirect = redirect.join('/')
|
||||
}
|
||||
if (redirect.startsWith('/redirect')) {
|
||||
redirect = '/'
|
||||
}
|
||||
|
||||
replace({
|
||||
path: redirect.startsWith('/') ? redirect : '/' + redirect,
|
||||
query,
|
||||
})
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div p24>
|
||||
<n-button type="error" @click="handleDelete">删除</n-button>
|
||||
<n-button type="error" @click="handleDelete"> <icon-mi:delete mr-5 />删除</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div p24>
|
||||
<n-button type="primary" @click="handleLogin">点击登陆</n-button>
|
||||
<n-button type="primary" @click="handleLogin">
|
||||
<icon-mdi:login mr-5 />
|
||||
登陆
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
52
src/views/test-page/route.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: 'Test',
|
||||
path: '/test',
|
||||
component: Layout,
|
||||
redirect: '/test/unocss',
|
||||
meta: {
|
||||
title: '基础功能测试',
|
||||
customIcon: 'logo',
|
||||
order: 1,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Unocss',
|
||||
path: 'unocss',
|
||||
component: () => import('./unocss/index.vue'),
|
||||
meta: {
|
||||
title: '测试unocss',
|
||||
icon: 'material-symbols:auto-awesome-outline-rounded',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Message',
|
||||
path: 'message',
|
||||
component: () => import('./message/index.vue'),
|
||||
meta: {
|
||||
title: '测试Message',
|
||||
icon: 'material-symbols:auto-awesome-outline-rounded',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Dialog',
|
||||
path: 'dialog',
|
||||
component: () => import('./dialog/index.vue'),
|
||||
meta: {
|
||||
title: '测试Dialog',
|
||||
icon: 'material-symbols:auto-awesome-outline-rounded',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'TestKeepAlive',
|
||||
path: 'keep-alive',
|
||||
component: () => import('./keep-alive/index.vue'),
|
||||
meta: {
|
||||
title: '测试Keep-Alive',
|
||||
keepAlive: true,
|
||||
icon: 'material-symbols:auto-awesome-outline-rounded',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -11,10 +11,10 @@
|
||||
</p>
|
||||
|
||||
<div flex mt-20>
|
||||
<div flex p-20 rounded-5 bg-white>
|
||||
<div flex p-20 rounded-5 bg="#fff">
|
||||
<div text-20 font-600>Flex布局</div>
|
||||
<div flex w-360 flex-wrap justify-around ml-15 p-10>
|
||||
<div w-50 h-50 b-1 rounded-5 flex justify-center items-center p-10 m-20>
|
||||
<div w-50 h-50 b-1 rounded-5 f-c-c p-10 m-20>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20>
|
||||
@@ -27,16 +27,16 @@
|
||||
<span w-6 h-6 rounded-3 bg-black self-end></span>
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20>
|
||||
<div flex flex-col justify-between>
|
||||
<div flex-col justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
<div flex flex-col justify-between>
|
||||
<div flex-col justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 flex flex-col justify-between items-center p-10 m-20>
|
||||
<div w-50 h-50 b-1 rounded-5 f-c-c flex-col p-10 m-20>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
@@ -47,7 +47,7 @@
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 flex flex-col justify-between p-10 m-20>
|
||||
<div w-50 h-50 b-1 rounded-5 flex-col justify-between p-10 m-20>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
@@ -63,32 +63,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div flex ml-35 p-20 rounded-5 bg="#fff">
|
||||
<div text-20 font-600>字体:</div>
|
||||
<div ml-15 p-10 pl-30 pr-30 rounded-5>
|
||||
<p text-12>font-size: 12px</p>
|
||||
<p text-16>font-size: 16px</p>
|
||||
<p text-20>font-size: 20px</p>
|
||||
|
||||
<p font-300 mt-10>font-weight: 300</p>
|
||||
<p font-600>font-weight: 600</p>
|
||||
<p font-bold>font-weight: bold</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div flex p-20 ml-35 rounded-5 bg-white>
|
||||
<div text-20 font-600>颜色:</div>
|
||||
<div ml-15 p-10 pl-30 pr-30 rounded-5>
|
||||
<p color="#881337">color: #881337</p>
|
||||
<p c-pink-500>color: #ec4899</p>
|
||||
|
||||
<p bg="pink" mt-10>background: pink</p>
|
||||
<p bg="#2563eb" mt-10>background: #2563eb</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
class="w-300 flex-shrink-0 mt-10 mb-10 cursor-pointer"
|
||||
hover:card-shadow
|
||||
title="Vue Naive Admin"
|
||||
size="small">
|
||||
size="small"
|
||||
>
|
||||
<p op-60>一个基于 Vue3.0、Vite、Naive UI 的轻量级后台管理模板</p>
|
||||
</n-card>
|
||||
<div w-300 h-0></div>
|
||||
20
src/views/workbench/route.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/workbench',
|
||||
children: [
|
||||
{
|
||||
name: 'Workbench',
|
||||
path: 'workbench',
|
||||
component: () => import('./index.vue'),
|
||||
meta: {
|
||||
title: '工作台',
|
||||
icon: 'mdi:home',
|
||||
order: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,16 +1,45 @@
|
||||
import { defineConfig, presetAttributify, presetUno } from 'unocss'
|
||||
|
||||
export default defineConfig({
|
||||
exclude: ['node_modules', '.git', '.github', '.husky', '.vscode', 'build', 'dist', 'mock', 'public', './stats.html'],
|
||||
presets: [presetUno(), presetAttributify()],
|
||||
shortcuts: [
|
||||
['f-c-c', 'flex justify-center items-center'],
|
||||
['text-ellipsis', 'truncate'],
|
||||
['wh-full', 'w-full h-full'],
|
||||
['f-c-c', 'flex justify-center items-center'],
|
||||
['flex-col', 'flex flex-col'],
|
||||
['absolute-lt', 'absolute left-0 top-0'],
|
||||
['absolute-lb', 'absolute left-0 bottom-0'],
|
||||
['absolute-rt', 'absolute right-0 top-0'],
|
||||
['absolute-rb', 'absolute right-0 bottom-0'],
|
||||
['absolute-center', 'absolute-lt f-c-c wh-full'],
|
||||
['text-ellipsis', 'truncate'],
|
||||
],
|
||||
rules: [
|
||||
[/^bc-(.+)$/, ([, color]) => ({ 'border-color': `#${color}` })],
|
||||
['color-primary', { color: 'var(--primaryColor)' }],
|
||||
['bgc-primary', { backgroundColor: 'var(--primaryColor)' }],
|
||||
['card-shadow', { 'box-shadow': '0 1px 2px -2px #00000029, 0 3px 6px #0000001f, 0 5px 12px 4px #00000017' }],
|
||||
],
|
||||
presets: [presetUno(), presetAttributify()],
|
||||
theme: {
|
||||
colors: {
|
||||
primary: 'var(--primary-color)',
|
||||
primary_hover: 'var(--primary-color-hover)',
|
||||
primary_pressed: 'var(--primary-color-pressed)',
|
||||
primary_active: 'var(--primary-color-active)',
|
||||
info: 'var(--info-color)',
|
||||
info_hover: 'var(--info-color-hover)',
|
||||
info_pressed: 'var(--info-color-pressed)',
|
||||
info_active: 'var(--info-color-active)',
|
||||
success: 'var(--success-color)',
|
||||
success_hover: 'var(--success-color-hover)',
|
||||
success_pressed: 'var(--success-color-pressed)',
|
||||
success_active: 'var(--success-color-active)',
|
||||
warning: 'var(--warning-color)',
|
||||
warning_hover: 'var(--warning-color-hover)',
|
||||
warning_pressed: 'var(--warning-color-pressed)',
|
||||
warning_active: 'var(--warning-color-active)',
|
||||
error: 'var(--error-color)',
|
||||
error_hover: 'var(--error-color-hover)',
|
||||
error_pressed: 'var(--error-color-pressed)',
|
||||
error_active: 'var(--error-color-active)',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import path from 'path'
|
||||
|
||||
import { wrapperEnv, createProxy } from './build/utils'
|
||||
import { convertEnv, getSrcPath, getRootPath } from './build/utils'
|
||||
import { createViteProxy, viteDefine } from './build/config'
|
||||
import { createVitePlugins } from './build/plugin'
|
||||
import { OUTPUT_DIR } from './build/constant'
|
||||
|
||||
export default defineConfig(({ command, mode }) => {
|
||||
const root = process.cwd()
|
||||
const srcPath = getSrcPath()
|
||||
const rootPath = getRootPath()
|
||||
const isBuild = command === 'build'
|
||||
|
||||
const env = loadEnv(mode, process.cwd())
|
||||
const viteEnv = wrapperEnv(env)
|
||||
const { VITE_PORT, VITE_PUBLIC_PATH, VITE_PROXY } = viteEnv
|
||||
const viteEnv = convertEnv(env)
|
||||
const { VITE_PORT, VITE_PUBLIC_PATH, VITE_USE_PROXY, VITE_PROXY_TYPE } = viteEnv
|
||||
|
||||
return {
|
||||
root,
|
||||
base: VITE_PUBLIC_PATH || '/',
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
'~': rootPath,
|
||||
'@': srcPath,
|
||||
},
|
||||
},
|
||||
define: viteDefine,
|
||||
plugins: createVitePlugins(viteEnv, isBuild),
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: VITE_PORT,
|
||||
proxy: createProxy(VITE_PROXY),
|
||||
open: false,
|
||||
proxy: createViteProxy(VITE_USE_PROXY, VITE_PROXY_TYPE),
|
||||
},
|
||||
build: {
|
||||
target: 'es2015',
|
||||
|
||||