1
0
mirror of https://github.com/zclzone/vue-naive-admin.git synced 2025-12-28 12:10:20 +08:00

52 Commits

Author SHA1 Message Date
张传龙
841bab0d63 release: release v1.0.0 2022-08-27 14:51:08 +08:00
张传龙
453148fc8d build: update commitlint 2022-08-27 14:50:34 +08:00
张传龙
7ec078bd7a Revert "mod: gh-pages gzip"
This reverts commit dd0bc3e6e8.
2022-08-27 14:47:18 +08:00
张传龙
dd0bc3e6e8 mod: gh-pages gzip 2022-08-27 14:36:21 +08:00
张传龙
8c665c727b style: format 2022-08-27 14:26:14 +08:00
张传龙
da98aa1c7d docs: update readme 2022-08-27 14:23:12 +08:00
张传龙
51b47ea722 chore: upgrade to vite3 2022-08-27 14:22:09 +08:00
张传龙
220a7800f7 refactor: refactor 2022-08-27 14:09:32 +08:00
张传龙
230e3a72d9 chore: update deps 2022-08-27 12:03:58 +08:00
张传龙
0cefadc2a5 refactor: custom icon 2022-08-27 11:46:34 +08:00
张传龙
2f1b747243 feat: add compress plugin 2022-08-27 11:04:07 +08:00
张传龙
296d5ea6f0 perf: png replace with webp 2022-08-27 10:58:22 +08:00
张传龙
3a415703d4 perf: table demo 2022-08-27 10:36:07 +08:00
张传龙
006f730457 perf: table demo 2022-08-26 22:48:03 +08:00
张传龙
606c5a2df0 docs: update readme 2022-08-25 18:26:29 +08:00
张传龙
30c375cc1d fix(components): fix tags contenxtmenu
ISSUES CLOSED: #23
2022-08-18 10:56:28 +08:00
张传龙
ddf14053da chore: update deps 2022-08-18 09:42:08 +08:00
张传龙
38edbcb68a mod: update mock data 2022-08-18 09:39:09 +08:00
Ronnie Zhang
3e54a82abb Merge pull request #22 from amplest/main
fix: get请求无法接收参数
2022-08-12 22:59:43 +08:00
Xiongxing
df6225a752 fix: get请求无法接收参数 2022-08-12 21:10:39 +08:00
张传龙
63c1f2f132 refactor: routes sort 2022-08-08 15:44:16 +08:00
张传龙
0bb2a904e7 refactor: adjust routes 2022-08-08 15:36:43 +08:00
张传龙
ef3aaa5be5 refactor: refactor async routes 2022-08-07 22:25:28 +08:00
张传龙
869a68812c chore: adjust commitlink config 2022-08-04 18:04:08 +08:00
张传龙
fd0032e0e9 docs: update readme 2022-08-02 09:34:25 +08:00
张传龙
b53d7daaa1 docs: update readme 2022-08-02 09:31:17 +08:00
张传龙
856bdfd0ee docs: update readme 2022-07-31 18:08:13 +08:00
张传龙
9f9884759c refactor: simplify permission-guard 2022-07-30 22:11:53 +08:00
张传龙
7dad43d003 docs: update readme 2022-07-29 16:48:58 +08:00
张传龙
7762e02b31 refactor: refactor api usage 2022-07-25 18:36:22 +08:00
张传龙
e5768fa1e3 style: modify login page 2022-07-23 22:17:23 +08:00
张传龙
7ee613d8cf style: modify footer 2022-07-23 22:10:01 +08:00
张传龙
80a5b7f053 style: modify custom scrollbar 2022-07-21 17:47:11 +08:00
张传龙
eb160731da feat: login page compatible mobile 2022-07-20 18:26:38 +08:00
张传龙
789231a7f4 style: format 2022-07-20 09:13:07 +08:00
张传龙
6ea6e1c267 chore: update settings.json 2022-07-20 09:10:40 +08:00
张传龙
d971e7e4ba fix: fix incorrent judgment of isHash 2022-07-19 16:29:07 +08:00
张传龙
215998dc66 perf: optimize login page 2022-07-17 20:37:44 +08:00
张传龙
40f9ac1a6b fix: fix incorrent usage of vue router 2022-07-17 14:54:45 +08:00
张传龙
6ec5588ed4 chore: setup lint-staged 2022-07-15 14:48:40 +08:00
张传龙
380e5768c4 chore: update settings.json 2022-07-15 14:06:57 +08:00
张传龙
5856f601fa style: use unocss rewrite css 2022-07-14 18:05:47 +08:00
张传龙
d10b8f0e96 chore(prettier): update prettier config 2022-07-14 18:04:00 +08:00
张传龙
3860cf9ebb style: simplify unocss test page 2022-07-14 16:40:25 +08:00
张传龙
94b46d9bf6 chore(unocss): update unocss config 2022-07-14 16:39:24 +08:00
张传龙
4df7d44bf1 docs: modify annotation 2022-07-13 22:58:09 +08:00
张传龙
42b8aca37b docs(readme): update readme 2022-07-11 16:07:07 +08:00
张传龙
0c96d0e937 docs(readme): update readme 2022-07-11 12:29:48 +08:00
张传龙
b540f5599f fix: fix incorrect text 2022-07-11 12:28:33 +08:00
张传龙
18b8a81640 fix(other): disabled unplugin generate .d.ts 2022-07-10 22:24:23 +08:00
张传龙
06b3afc2de build(deps): update unplugin deps 2022-07-10 22:20:21 +08:00
92376
3088773ebe fix: modify exit full screen icon
* !1 退出全屏 icon
* 退出全屏 icon
2022-07-10 12:55:45 +00:00
100 changed files with 2485 additions and 966 deletions

View File

@@ -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
View File

@@ -1,3 +1,3 @@
VITE_APP_TITLE = 'Vue Naive Admin'
VITE_TITLE = 'Vue Naive Admin'
VITE_PORT = 3100

View File

@@ -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'

View File

@@ -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'

View File

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

View File

@@ -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'

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint
npm run lint:staged

29
.vscode/settings.json vendored
View File

@@ -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, // 适配unocss1rem = 4px ==> 0.25rem = 1px
"files.associations": {
"*.env.*": "dotenv",
"*.css": "postcss"
}
}

131
README.md
View File

@@ -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 templateBased 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` iconsupport 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 包,减少打包体积
- 🍇 集成 unocssantfu 大神开源的原子化 css 解决方案,非常轻量,目前我是自己写 scss 样式搭配着 unocss 使用的
- 🍍 集成 PiniaVuex 的替代方案轻量、简单、易用尤大已表示不会有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
# PreviewNeed to build first
pnpm preview
# Commithusky+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
View 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
View 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
View File

@@ -0,0 +1,2 @@
export * from './define'
export * from './proxy'

15
build/config/proxy.js Normal file
View 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
}

View File

@@ -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,
},
},
})

View File

@@ -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({

View File

@@ -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: `

View File

@@ -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__',
}),
]

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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',
],
],
},
}

View File

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

View File

@@ -2,6 +2,7 @@
"compilerOptions": {
"baseUrl": "./",
"paths": {
"~/*": ["./*"],
"@/*": ["src/*"]
},
"jsx": "preserve"

5
mock/api/index.js Normal file
View File

@@ -0,0 +1,5 @@
import auth from './auth'
import user from './user'
import post from './post'
export default [...auth, ...user, ...post]

View File

@@ -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 都准备就绪。再对它们进行处理。',

View File

@@ -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)
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -3,5 +3,4 @@ module.exports = {
singleQuote: true,
semi: false,
endOfLine: 'lf',
bracketSameLine: true,
}

View File

@@ -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;

View File

@@ -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 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
View File

@@ -0,0 +1,2 @@
export * from './theme.json'
export * from './proxy-config'

18
settings/proxy-config.js Normal file
View 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]
}

View File

@@ -1,11 +1,11 @@
{
"header": {
"height": 60
},
"tags": {
"visible": true,
"height": 50
},
"header": {
"height": 60
},
"naiveThemeOverrides": {
"common": {
"primaryColor": "#316C72FF",

View File

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

View File

@@ -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
View File

@@ -0,0 +1,6 @@
import request from '@/utils/http'
export default {
getUser: () => request.get('/user'),
refreshToken: () => request.post('/auth/refreshToken'),
}

View File

@@ -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',
})
}

View File

@@ -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,
})
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

BIN
src/assets/images/404.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

1
src/assets/svg/logo.svg Normal file
View 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

View 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>

View File

@@ -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])
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] || '')
}
}
},
{ immediate: true }
)
}
// 挂载naive组件的方法至window, 以便在全局使用
function setupNaiveTools() {
@@ -46,6 +40,7 @@ function setupNaiveTools() {
const NaiveProviderContent = defineComponent({
setup() {
setupCssVar()
setupNaiveTools()
},
render() {

View File

@@ -15,7 +15,8 @@
:class="{ overflow: isOverflow && showArrow }"
:style="{
transform: `translateX(${translateX}px)`,
}">
}"
>
<slot />
</div>
</div>

View 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>

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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>
</ScrollX>
<ContextMenu
v-if="contextMenuOption.show"
v-model:show="contextMenuOption.show"
:current-path="contextMenuOption.currentPath"
:x="contextMenuOption.x"
:y="contextMenuOption.y" />
:y="contextMenuOption.y"
/>
</ScrollX>
</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;
<style>
.n-tag__close {
box-sizing: content-box;
border-radius: 50%;
font-size: 12px;
padding: 2px;
border-radius: 50%;
transition: all 0.7s;
&:hover {
color: #fff;
background-color: var(--primaryColor);
}
}
&:hover {
color: var(--primaryColor);
}
transform: scale(0.9);
transform: translateX(5px);
transition: all 0.3s;
}
</style>

View File

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

View File

@@ -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'

View File

@@ -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) => {

View File

@@ -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) {
/** 没有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()
next()
} else {
return true
}
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 } })
}
}
return { ...to, replace: true }
})
}

View File

@@ -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: [],

View File

@@ -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 }

View File

@@ -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'],
},
},
],
},
],
},
]

View File

@@ -1 +0,0 @@
export { default as themeSettings } from './theme.json'

View File

@@ -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))
}

View File

@@ -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']

View File

@@ -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
},
},
})

View File

@@ -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 }

View File

@@ -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;
}
}
}

View File

@@ -1,3 +0,0 @@
:root {
--primaryColor: #316c72;
}

View File

@@ -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 },
})
}

View File

@@ -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,
})

View File

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

View File

@@ -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 }) })
}

View File

@@ -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)
}

View File

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

View 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',
},
},
],
}

View File

@@ -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)" />

View 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}`),
}

View File

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

View File

@@ -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
}

View 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
View File

@@ -0,0 +1,5 @@
import request from '@/utils/http'
export default {
login: (data) => request.post('/auth/login', data),
}

View File

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

View File

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

View File

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

View File

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

View 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',
},
},
],
}

View File

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

View File

@@ -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.0ViteNaive UI 的轻量级后台管理模板</p>
</n-card>
<div w-300 h-0></div>

View 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,
},
},
],
}

View File

@@ -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)',
},
},
})

View File

@@ -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',