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

131 Commits

Author SHA1 Message Date
zclzone
921e0d18e9 mod: update settings.json 2023-10-07 12:57:37 +08:00
zclzone
3e259877c6 chore: update deps 2023-10-06 22:41:16 +08:00
zclzone
a431a2cf85 fix(other): loginInfo.username绑定错误
ISSUES CLOSED: #55
2023-10-06 22:39:39 +08:00
zclzone
c7c8691164 Merge branch 'main' of https://gitee.com/zclzone/vue-naive-admin 2023-09-26 22:53:50 +08:00
张传龙
1d246d1cd6 fix: 描述 2023-09-26 16:54:12 +08:00
张传龙
1854d2cec4 style: 移除不再需要的样式 2023-09-21 09:25:03 +08:00
zclzone
478d8fccd1 feat: 首页icon 2023-09-18 22:46:49 +08:00
zclzone
0accfe3a4a feat: 调整首页echarts图 2023-09-17 12:25:12 +08:00
zclzone
4bd50e2f9e refactor: 重构首页 2023-09-14 22:54:50 +08:00
zclzone
72857b3862 Merge branch 'main' of https://github.com/zclzone/vue-naive-admin 2023-09-09 17:51:19 +08:00
zclzone
a18ffce56c style: 修改图标 2023-09-09 17:50:55 +08:00
Ronnie Zhang
a2222c4dc0 docs: Update README.EN.md 2023-09-09 17:43:05 +08:00
Ronnie Zhang
a357ac36ee Update README.md 2023-09-09 17:42:11 +08:00
Ronnie Zhang
5e1acd7b8e Update README.md 2023-09-06 11:01:40 +08:00
Ronnie Zhang
8bbd04e668 docs: Update README.md 2023-09-06 11:00:59 +08:00
Ronnie Zhang
0c5c3df645 docs: Update README.md 2023-09-06 10:59:34 +08:00
Ronnie Zhang
ed577cd41a docs: Update README.md 2023-09-01 21:14:52 +08:00
Ronnie Zhang
456ad78dd2 Merge pull request #48 from xtazhangzp/main 2023-09-01 20:59:38 +08:00
v_zhangzp
ba20e7a96b fix: 修改时间方法Date.now() 2023-08-31 17:03:07 +08:00
张传龙
3ed1aafa80 chore: update deps 2023-08-28 11:08:06 +08:00
张传龙
5cf1212847 build: 锁定不能升级的依赖包 2023-08-28 10:51:32 +08:00
张传龙
81e0bb7b78 feat: md-editor暗黑主题调整 2023-08-25 10:52:04 +08:00
张传龙
f3125ddec8 chore: update deps 2023-08-25 10:42:18 +08:00
张传龙
3b3cb7ba34 docs: update readme 2023-08-17 18:50:46 +08:00
张传龙
d7c1063102 feat: 更新logo 2023-08-17 18:30:24 +08:00
张传龙
a5aa8a353f build: 新增一键启动脚本,F5直接启动项目 2023-08-15 10:49:44 +08:00
张传龙
cbb6ca4f6b chore: update deps 2023-08-10 16:12:40 +08:00
Ronnie Zhang
2ece015dae docs: update readme 2023-08-05 16:40:26 +08:00
zclzone
a80f83a011 mod: merge commit from github 2023-08-04 22:14:12 +08:00
zclzone
65d4d3848d fix(deps): 修复自定义指令引起的热更新失效问题
ISSUES CLOSED: #45
2023-08-04 22:07:20 +08:00
zclzone
53830256f4 fix: size type 2023-08-04 21:53:55 +08:00
张传龙
22c59c208f feat: add unocss preset rem to px 2023-08-03 17:20:21 +08:00
张传龙
b43f87035b chore: rollback vite-plugin-mock version 2023-07-28 01:10:01 +08:00
张传龙
20eee94630 chore: update unocss config 2023-07-28 00:56:14 +08:00
张传龙
c504ad065c chore: update deps 2023-07-27 14:25:07 +08:00
张传龙
0e764ac748 feat: add message notification 2023-07-21 16:01:35 +08:00
张传龙
8f15e1c655 fix: 多余的reloadPage 2023-07-13 14:46:41 +08:00
张传龙
d702a6703b fix: keepAlive 2023-07-11 15:07:37 +08:00
张传龙
d49af8b574 docs: update readme 2023-07-07 15:33:20 +08:00
张传龙
a036602d3e Merge branch 'main' of https://github.com/zclzone/vue-naive-admin 2023-07-07 15:09:04 +08:00
张传龙
eb173dc38e docs: update readme 2023-07-07 15:05:25 +08:00
zclzone
e5f1ee25c3 mod: update copyright 2023-07-01 10:41:25 +08:00
张传龙
fe11e18197 Merge branch 'main' of https://github.com/zclzone/vue-naive-admin 2023-06-27 17:18:24 +08:00
张传龙
0f9fb9f1c9 Merge branch 'main' of https://gitee.com/zclzone/vue-naive-admin 2023-06-27 17:16:56 +08:00
张传龙
50f96b99c7 style: sort package.json 2023-06-27 17:16:45 +08:00
Ronnie Zhang
2eb936bcac Merge pull request #43 from mizhexiaoxiao/main 2023-06-26 20:46:00 +08:00
mizhexiaoxiao
b50731881c fix: 在中文输入法输入字母按enter键会触发查询 2023-06-26 10:46:43 +08:00
zclzone
d4a5cffd81 mod: update copyright 2023-06-23 18:45:58 +08:00
zclzone
70eab22f65 style: 修改注释 2023-06-21 22:06:40 +08:00
zclzone
0247f3ebfa refactor: 简化proxy配置 2023-06-21 21:53:33 +08:00
张传龙
a610c1c6d0 docs: update readme 2023-06-19 09:06:15 +08:00
Ronnie Zhang
5a9c0fb584 Merge pull request #41 from szluyu99/main 2023-06-19 09:01:24 +08:00
szluyu99
cad72b3b73 docs: README 新增使用该项目的开源项目 2023-06-18 21:56:09 +08:00
zclzone
9edd0e5ad6 style: 暗黑模式细节 2023-06-17 10:43:03 +08:00
zclzone
ae6db3ed3c feat: tag标签增加图标展示 2023-06-17 10:42:33 +08:00
张传龙
dcab55055c fix(utils): 修复\\$message后于接口请求定义问题
ISSUES CLOSED: #39
2023-06-15 10:26:03 +08:00
张传龙
5aa4d3d5ae Merge branch 'main' of https://gitee.com/zclzone/vue-naive-admin 2023-06-03 16:45:11 +08:00
张传龙
eea9fc79f7 style: lint fix 2023-06-03 16:38:45 +08:00
张传龙
526792e22f chore: update .prettierrc 2023-06-03 16:34:50 +08:00
张传龙
855202962c chore: update path-intellisense settings 2023-06-03 16:34:01 +08:00
zclzone
74a58fafb9 docs: update README 2023-05-24 21:18:18 +08:00
zclzone
ad451dae1e mod: merge 2023-05-13 23:12:49 +08:00
zclzone
35ed004b2e chore: 移除plugin unplugin-vue-define-options(vue官方已支持) 2023-05-13 17:25:44 +08:00
zclzone
fcdd31c935 chore: update deps 2023-05-13 17:24:43 +08:00
张传龙
386d9ec27a style: lint fix 2023-05-08 14:15:03 +08:00
张传龙
e84dd01365 feat: add @unocss/eslint-config 2023-05-08 14:13:59 +08:00
zclzone
6da56ec881 Merge branch 'main' of https://github.com/zclzone/vue-naive-admin 2023-05-07 22:25:09 +08:00
zclzone
4e9e3469b0 chore: update deps 2023-05-07 22:24:46 +08:00
zclzone
3b86597eff style: lint fix 2023-05-07 22:20:57 +08:00
zclzone
ebffe52c7c style: lint fix 2023-05-07 22:05:13 +08:00
zclzone
6d863e1a63 chore: eslint -> @zclzone/eslint-config 2023-05-07 22:00:25 +08:00
Ronnie Zhang
b2e1c2d22c Merge pull request #35 from liaocp666/patch-1
docs: 修正文档中的错别字
2023-05-03 20:25:42 +08:00
Kent Liao
2e283c3b19 修正错别字 2023-04-28 10:58:00 +08:00
zclzone
6300758fd0 fix: 更新失效图片链接 2023-04-23 22:35:33 +08:00
zclzone
b5717f6b8d build: gh-pages -> vercel 2023-04-23 22:18:00 +08:00
张传龙
ff11cdf73f build: remove vercel env 2023-04-23 14:46:12 +08:00
张传龙
9a01e42915 build: add vercel.json 2023-04-23 14:38:50 +08:00
zclzone
7c74578afc build: 触发构建 2023-04-20 21:56:53 +08:00
zclzone
a6a8002c59 build: 触发构建 2023-04-20 21:21:12 +08:00
zclzone
daf747348e chore: 添加vercel环境 2023-04-20 20:56:21 +08:00
张传龙
60b5ce1817 fix: redirect 2023-04-17 14:14:10 +08:00
张传龙
4c75be67f2 feat: 图片上传 2023-04-17 13:36:14 +08:00
张传龙
be1c875a72 Merge branch 'main' of https://gitee.com/zclzone/vue-naive-admin 2023-04-17 13:24:48 +08:00
张传龙
329a6e29cb chore: update jsconfig.json 2023-04-17 13:18:44 +08:00
zclzone
af907932fb perf: 优化ScrollX 2023-04-16 21:41:27 +08:00
张传龙
681b3144d1 feat: 增加表格导出功能 2023-04-13 12:13:32 +08:00
张传龙
8304970a59 chore: update deps 2023-04-13 08:20:45 +08:00
zclzone
5cef8e4a01 fix: 暗黑模式细节 2023-03-12 21:25:14 +08:00
张传龙
0b50e1dbee Merge branch 'main' of https://github.com/zclzone/vue-naive-admin 2023-03-08 11:01:00 +08:00
张传龙
88a93c4e57 chore: update deps 2023-03-08 10:58:08 +08:00
zclzone
38ae35ee95 fix: 文件名称 2023-02-12 12:26:47 +08:00
zclzone
c7471a66db docs: 默认中文README 2023-02-12 12:25:49 +08:00
zclzone
a4531be904 fix: 修复暗色模式下部分显示问题 2023-02-12 12:10:26 +08:00
张传龙
c58605de54 refactor: 重构暗黑模式 2023-01-30 15:37:10 +08:00
zclzone
c3dc0b4b2c chore: update deps 2022-12-31 18:17:36 +08:00
zclzone
2d3e9988ec chore: update deps 2022-12-31 18:07:05 +08:00
zclzone
9548a0bfc8 feat: 集成简易暗黑模式 2022-12-18 19:19:42 +08:00
张传龙
dda778fdde mod: update 2022-12-12 09:32:50 +08:00
张传龙
98e3f13185 fix: 示例组件name 2022-12-09 14:17:21 +08:00
张传龙
7e79c51630 feat: 全局中文设定 2022-12-08 15:08:27 +08:00
zclzone
181aed4897 chore: update deps 2022-11-28 12:13:02 +08:00
zclzone
c626d2b785 feat: 当前标签页始终显示在视野内 2022-11-27 20:04:11 +08:00
zclzone
ed79e81b13 perf: 切换tab时自动展开对应的菜单 2022-11-25 09:16:01 +08:00
zclzone
264119a142 fix: $message 2022-11-24 21:05:42 +08:00
zclzone
4c1c77821f feat: 增加多级菜单示例 2022-11-24 18:16:37 +08:00
zclzone
67b11f04fc fix: fullPath 2022-11-14 18:29:41 +08:00
zclzone
649fe1d4e8 fix: route path 2022-11-12 19:09:24 +08:00
张传龙
c3192423c6 mod: remove 2022-10-31 15:23:40 +08:00
张传龙
911fc74305 chore: update deps 2022-10-31 14:09:07 +08:00
张传龙
2fcfd6b4d1 docs: add docs 2022-10-28 15:03:04 +08:00
张传龙
bac868d071 docs: add docs 2022-10-27 17:15:34 +08:00
张传龙
ea5460488a docs: add docs 2022-10-26 17:23:08 +08:00
张传龙
61d42ead21 chore: update deps 2022-10-21 17:39:01 +08:00
张传龙
a98555beb1 chore: update deps 2022-10-19 17:59:09 +08:00
张传龙
820eb516ce docs: update readme 2022-10-16 16:46:56 +08:00
张传龙
6cd0dc1eff chore: update deps 2022-10-09 17:56:23 +08:00
张传龙
5dcb2958a1 chore: update deps 2022-10-04 17:07:34 +08:00
张传龙
c25476278b fix: rich text editor keep alive 2022-09-29 16:53:54 +08:00
张传龙
99ddb4fe70 feat: rich text editor 2022-09-29 08:55:26 +08:00
张传龙
82c47ffc72 chore: update deps 2022-09-28 16:44:01 +08:00
张传龙
1f1678800f style: md editor 2022-09-28 16:43:44 +08:00
张传龙
efdd89cd50 feat: back top 2022-09-26 17:40:41 +08:00
张传龙
100b91a118 style: reset.css 2022-09-25 17:14:01 +08:00
张传龙
26b71f0ec6 perf: unocss demo 2022-09-25 17:13:35 +08:00
张传龙
92e7ada37b fix: toLogin 2022-09-24 15:40:34 +08:00
张传龙
2d879d0592 refactor: http interceptors 2022-09-24 15:27:26 +08:00
张传龙
8806a6cb43 fix: keep-alive key 2022-09-24 14:44:59 +08:00
张传龙
85a04fd06d fix: logout 2022-09-21 17:14:41 +08:00
张传龙
4a5b8dd005 fix: fix 2022-09-21 17:13:39 +08:00
张传龙
2f7da255e5 style: naive theme 2022-09-18 20:18:55 +08:00
张传龙
6664ae8f7b refactor: folders 2022-09-18 20:05:40 +08:00
108 changed files with 5389 additions and 3442 deletions

View File

@@ -5,10 +5,7 @@ VITE_PUBLIC_PATH = '/'
VITE_USE_MOCK = true VITE_USE_MOCK = true
# 是否启用MOCK # 是否启用MOCK
VITE_USE_PROXY = false VITE_USE_PROXY = true
# 代理类型(跟启动和构建环境无关) 'dev' | 'test' | 'prod'
VITE_PROXY_TYPE = 'dev'
# base api # base api
VITE_BASE_API = '/api' VITE_BASE_API = '/api'

View File

@@ -0,0 +1,62 @@
{
"globals": {
"$loadingBar": true,
"$message": true,
"defineOptions": true,
"$dialog": true,
"$notification": true,
"EffectScope": true,
"computed": true,
"createApp": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"effectScope": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useRoute": true,
"useRouter": true,
"useSlots": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}

View File

@@ -1,15 +0,0 @@
module.exports = {
root: true,
extends: ['plugin:vue/vue3-recommended', 'plugin:prettier/recommended'],
rules: {
'prettier/prettier': 'error',
'vue/valid-template-root': 'off',
'vue/no-multiple-template-root': 'off',
'vue/multi-word-component-names': [
'error',
{
ignores: ['index', '401', '404'],
},
],
},
}

View File

@@ -1,38 +0,0 @@
name: deploy
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: use Node.js 16
uses: actions/setup-node@v2.1.2
with:
node-version: '16.x'
- name: use pnpm 6.32.2
uses: pnpm/action-setup@v2.2.1
with:
version: 6.32.2
- name: Build
run: |
pnpm install
pnpm run build:github
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
publish_dir: ./dist
github_token: ${{ secrets.ACTIONS_DEPLOY_KEY }}
user_name: ${{ secrets.USER_NAME }}
user_email: ${{ secrets.USER_EMAIL }}
force_orphan: true
commit_message: deploy gh-pages

7
.prettierrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"printWidth": 100,
"singleQuote": true,
"semi": false,
"endOfLine": "lf",
"htmlWhitespaceSensitivity": "ignore"
}

14
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "一键启动",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "dev"],
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"]
}
]
}

View File

@@ -1,8 +1,4 @@
{ {
"path-intellisense.mappings": {
"@/": "${workspaceRoot}/src",
"~/": "${workspaceRoot}"
},
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"prettier.printWidth": 120, "prettier.printWidth": 120,

207
README.EN.md Normal file
View File

@@ -0,0 +1,207 @@
<p align="center">
<a href="https://github.com/zclzone/vue-naive-admin">
<img alt="Vue Naive Admin Logo" width="200" src="./src/assets/images/logo.png">
</a>
</p>
<p align="center">
<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">中文</a>
</p>
### Introduction
[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
- 🍒 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
[https://template.qszone.com](https://template.qszone.com)
[https://base.isme.top](https://base.isme.top)
### Docs
[Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
### Getting Started
```shell
# 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
# Install dependencies(Recommended use pnpm: https://pnpm.io/zh/installation)
npm i -g pnpm # Installed and can be ignored
pnpm i # or npm i
# Start
pnpm dev
```
### Build and Release
```shell
# Test Environment
pnpm build:test
# Github Environment
pnpm build:github
# Prod Environment
pnpm build
```
### Other
```shell
# eslint check
pnpm lint
# eslint check and fix
pnpm lint:fix
# PreviewNeed to build first
pnpm preview
# Commithusky+commitlint
pnpm cz
```
### Directory description
```
Vue Naive Admin
|-- .github // github相关如推送github仓库后自动部署gh pages
|-- .husky // git commit钩子
|-- .vscode // vscode编辑器相关
| |-- extensions.json // 插件推荐
| |-- settings.json // 项目级别的vscode配置优先级大于全局vscode配置
|-- build // 构建相关配置
| |-- constant.js // 构建相关的常量
| |-- utils.js // 构建相关的工具方法
| |-- config
| | |-- define.js // 注入全局常量启动或打包后将添加到window中
| | |-- proxy.js // 代理配置
| |-- plugin
| | |-- html.js // vite-plugin-html插件用于注入变量或者html标签
| | |-- mock.js // vite-plugin-mock插件处理mock
| | |-- unplugin.js // unplugin相关插件包含DefineOptions和自动导入
| |-- script // 打包完成后执行的一些node脚本不重要
| |-- build-cname.js // 自动生成cname
|-- mock // mock
| |-- utils.js // mock请求需要用到的工具方法
| |-- api // mock接口
|-- public // 公共资源文件夹下的文件会在打包后会直接加到dist根目录下
|-- settings // 项目配置
| |-- proxy-config.js // 代理配置文件
| |-- theme.json // 主题配置项,主题色等
|-- src
| |-- api // 公共api
| |-- assets // 静态资源
| | |-- images // 图片
| | |-- svg // svg图标
| |-- components // 全局组件
| | |-- common // 公共组件
| | |-- icon // icon相关组件
| | |-- page // 页面组件
| | |-- query-bar // 查询选项
| | |-- table // 封装的表格组件
| |-- composables // 封装的组合式函数
| |-- layout // 布局相关组件
| | |-- components
| | |-- AppMain.vue // 主体内容
| | |-- header // 头部
| | |-- sidebar // 侧边菜单栏
| | |-- tags // 多页签栏
| |-- router // 路由
| | |-- guard // 路由守卫
| | |-- routes // 路由列表
| |-- store // 状态管理pinia
| | |-- modules // 模块
| | |-- app // 管理页面重新加载、折叠菜单栏和keepAlive等
| | |-- permission // 权限相关,管理权限菜单
| | |-- tags // 管理多页签
| | |-- user // 用户模块,管理用户信息、登录登出
| |-- styles // 样式
| |-- utils // 封装的工具方法
| | |-- auth // 权限相关如token、跳转登录页等
| | |-- common // 通用
| | |-- http // 封装axios
| | |-- storage // 封装localStorage和sessionStorage
| |-- views // 页面
| | |-- demo // 示例
| | |-- error-page // 错误页
| | |-- login // 登录页
| | |-- workbench // 首页
| |-- App.vue
| |-- main.js
|-- .cz-config.js // git提交配置
|-- .editorconfig // 编辑器配置
|-- .env // 环境文件,所有环境都会载入
|-- .env.development // 开发环境文件
|-- .env.production // 生产环境文件
|-- .env.test // 测试环境文件
|-- .eslintignore // eslint忽略
|-- .eslintrc.js // eslint配置
|-- .gitignore // git忽略
|-- .prettierignore // prettier格式化忽略
|-- commitlint.config.js // commitlint规范配置
|-- index.html
|-- jsconfig.json // js配置
|-- LICENSE // 协议
|-- package.json // 依赖描述文件
|-- pnpm-lock.yaml // 依赖锁定文件
|-- prettier.config.js // prettier格式化配置
|-- README.md // 项目描述文档(英文)
|-- README.zh-CN.md // 项目描述文档(中文)
|-- unocss.config.js // unocss配置
|-- vite.config.js // vite配置
```
### TS version: Qs Admin
#### source code
- github: [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)
### Open source projects that use this project:
- [gin-vue-blog](https://github.com/szluyu99/gin-vue-blog): A full-stack blog project in Golang, the frontend of the blog backend is based on vue-naive-admin and integrates with a real backend service, implementing features such as backend-controlled routing.
### 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>

204
README.md
View File

@@ -1,115 +1,223 @@
<p align="center"> <p align="center">
<a href="https://github.com/zclzone/vue-naive-admin"> <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"> <img alt="Vue Naive Admin Logo" width="200" src="./src/assets/images/logo.png">
</a> </a>
</p> </p>
<p align="center"> <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="./LICENSE"><img alt="MIT License" src="https://badgen.net/github/license/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>
<p align='center'> <p align='center'>
<b>English</b> | <b>中文</b> |
<a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.zh-CN.md">简体中文</a> <a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.EN.md">English</a>
</p> </p>
### Introduction ### 简介
[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. [Vue Naive Admin](https://github.com/zclzone/vue-naive-admin) 是一个 **开源免费且允许商用** 的后台管理模板,基于 `Vue3、Vite4、Pinia、Unocss Naive UI` 等前端最新技术栈。相较于其他比较流行的后台管理模板,此项目更加简洁、轻量,风格清新,上手成本非常低,非常适合中小型项目或者个人项目。
### Features ### 功能
- 🍒 Integrated [Naive UI](https://www.naiveui.com)recommended by Evan You. - 🍒 集成 [Naive UI](https://www.naiveui.com)
- 🍑 Integrated login, logout and permission verification. - 🍑 集成登陆、注销及权限验证
- 🍐 Integrated multi-environment configuration, dev, test, production and github pages environments. - 🍐 集成多环境配置dev、测试、生产环境
- 🍎 Integrated `eslint + prettier`. - 🍎 集成 `eslint + prettier`,代码约束和格式化统一
- 🍌 Integrated `husky + commitlint`. - 🍌 集成 `husky + commitlint`,代码提交规范化
- 🍉 Integrated `Mock`. - 🍉 集成 `mock` 接口服务dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
- 🍍 Integrated `pinia`lightweight, simple and easy to use alternative to vuex. - 🍍 集成 `pinia`vuex 的替代方案,轻量、简单、易用
- 📦 Integrated `unplugin` auto import. - 📦 集成 `unplugin` 插件,自动导入,解放双手,开发效率直接起飞
- 🤹 Integrated `iconify` iconsupport custom svg icons. - 🤹 集成 `iconify` 图标,支持自定义 svg 图标, 优雅使用icon
- 🍇 Integrated `unocss`. - 🍇 集成 `unocss`antfu 开源的原子 css 解决方案,非常轻量
### Preview ### 预览
[https://template.qszone.com](https://template.qszone.com) [https://template.qszone.com](https://template.qszone.com)
[https://zclzone.github.io/vue-naive-admin](https://zclzone.github.io/vue-naive-admin) [https://template.isme.top](https://template.isme.top)
### Docs [https://base.isme.top](https://base.isme.top)
[Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs) ### 文档
项目文档: [Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
### Getting Started 从0到1搭建后台: [从0到1带你搭建Vite+Vue3+Pinia+Naive UI后台](https://juejin.cn/column/7093180796424421384)
如何安装pnpm: [安装pnpm](docs/安装pnpm.md)
如何使用图标: [使用图标](docs/使用图标.md)
如何使用unocss: [保熟的UnoCSS使用指北优雅使用antfu大佬的原子化CSS](https://juejin.cn/post/7142466784971456548)
### 快速开始
```shell ```shell
# Recommended setup git autocrlf 为 false # 推荐配置git autocrlf 为 false本项目规范使用lf换行符此配置是为防止git自动将源文件转换为crlf
# 不清楚为什么要这样做的请参考这篇文章https://www.freesion.com/article/4532642129
git config --global core.autocrlf false git config --global core.autocrlf false
# Clone Project # 克隆项目
git clone https://github.com/zclzone/vue-naive-admin.git git clone https://github.com/zclzone/vue-naive-admin.git
# 进入项目目录
cd vue-naive-admin cd vue-naive-admin
# Install dependencies(Recommended use pnpm: https://pnpm.io/zh/installation) # 安装依赖(建议使用pnpm: https://pnpm.io/zh/installation)
npm i -g pnpm # Installed and can be ignored npm i -g pnpm # 装了可忽略
pnpm i # or npm i pnpm i # 或者 npm i
# Start # 启动
pnpm dev pnpm dev
``` ```
### Build and Release ### 构建发布
```shell ```shell
# Test Environment # 构建测试环境
pnpm build:test pnpm build:test
# Github Environment # 构建github pages环境
pnpm build:github pnpm build:github
# Prod Environment # 构建生产环境
pnpm build pnpm build
``` ```
### Other ### 其他指令
```shell ```shell
# eslint check # eslint代码格式检查
pnpm lint pnpm lint
# eslint check and fix # 代码检查并修复
pnpm lint:fix pnpm lint:fix
# PreviewNeed to build first # 预览发布包效果(需先执行构建指令
pnpm preview pnpm preview
# Commithusky+commitlint # 提交代码husky+commitlint
pnpm cz pnpm cz
``` ```
### TS version: Qs Admin
#### source code ### 目录说明
- gitub: [https://github.com/zclzone/qs-admin](https://github.com/zclzone/qs-admin) ```
Vue Naive Admin
|-- .github // github相关如推送github仓库后自动部署gh pages
|-- .husky // git commit钩子
|-- .vscode // vscode编辑器相关
| |-- extensions.json // 插件推荐
| |-- settings.json // 项目级别的vscode配置优先级大于全局vscode配置
|-- build // 构建相关配置
| |-- constant.js // 构建相关的常量
| |-- utils.js // 构建相关的工具方法
| |-- config
| | |-- define.js // 注入全局常量启动或打包后将添加到window中
| | |-- proxy.js // 代理配置
| |-- plugin
| | |-- html.js // vite-plugin-html插件用于注入变量或者html标签
| | |-- mock.js // vite-plugin-mock插件处理mock
| | |-- unplugin.js // unplugin相关插件包含DefineOptions和自动导入
| |-- script // 打包完成后执行的一些node脚本不重要
| |-- build-cname.js // 自动生成cname
|-- mock // mock
| |-- utils.js // mock请求需要用到的工具方法
| |-- api // mock接口
|-- public // 公共资源文件夹下的文件会在打包后会直接加到dist根目录下
|-- settings // 项目配置
| |-- proxy-config.js // 代理配置文件
| |-- theme.json // 主题配置项,主题色等
|-- src
| |-- api // 公共api
| |-- assets // 静态资源
| | |-- images // 图片
| | |-- svg // svg图标
| |-- components // 全局组件
| | |-- common // 公共组件
| | |-- icon // icon相关组件
| | |-- page // 页面组件
| | |-- query-bar // 查询选项
| | |-- table // 封装的表格组件
| |-- composables // 封装的组合式函数
| |-- layout // 布局相关组件
| | |-- components
| | |-- AppMain.vue // 主体内容
| | |-- header // 头部
| | |-- sidebar // 侧边菜单栏
| | |-- tags // 多页签栏
| |-- router // 路由
| | |-- guard // 路由守卫
| | |-- routes // 路由列表
| |-- store // 状态管理pinia
| | |-- modules // 模块
| | |-- app // 管理页面重新加载、折叠菜单栏和keepAlive等
| | |-- permission // 权限相关,管理权限菜单
| | |-- tags // 管理多页签
| | |-- user // 用户模块,管理用户信息、登录登出
| |-- styles // 样式
| |-- utils // 封装的工具方法
| | |-- auth // 权限相关如token、跳转登录页等
| | |-- common // 通用
| | |-- http // 封装axios
| | |-- storage // 封装localStorage和sessionStorage
| |-- views // 页面
| | |-- demo // 示例
| | |-- error-page // 错误页
| | |-- login // 登录页
| | |-- workbench // 首页
| |-- App.vue
| |-- main.js
|-- .cz-config.js // git提交配置
|-- .editorconfig // 编辑器配置
|-- .env // 环境文件,所有环境都会载入
|-- .env.development // 开发环境文件
|-- .env.production // 生产环境文件
|-- .env.test // 测试环境文件
|-- .eslintignore // eslint忽略
|-- .eslintrc.js // eslint配置
|-- .gitignore // git忽略
|-- .prettierignore // prettier格式化忽略
|-- commitlint.config.js // commitlint规范配置
|-- index.html
|-- jsconfig.json // js配置
|-- LICENSE // 协议
|-- package.json // 依赖描述文件
|-- pnpm-lock.yaml // 依赖锁定文件
|-- prettier.config.js // prettier格式化配置
|-- README.md // 项目描述文档(英文)
|-- README.zh-CN.md // 项目描述文档(中文)
|-- unocss.config.js // unocss配置
|-- vite.config.js // vite配置
```
### TS 版本: Qs Admin
#### 源码
- github: [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) - 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://admin.qszone.com](https://admin.qszone.com)
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin) - [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
### Communication group & About the author ### 使用该项目的开源项目
- [gin-vue-blog](https://github.com/szluyu99/gin-vue-blog): Golang 全栈博客项目, 博客后台的前端基于 vue-naive-admin对接真实后端服务实现了后端控制路由等特性。
### 入群交流 & 关于作者
<a href="https://blog.qszone.com/about/"> <a href="https://blog.qszone.com/about/">
<img src="https://assets.qszone.com/images/about.png" style="max-width: 400px" /> <img src="https://assets.qszone.com/images/about.png" style="max-width: 400px" />
</a> </a>
### ☕ 赞助我
> 开源不易,请作者喝杯咖啡吧
<p>
<img src="https://assets.qszone.com/images/zhifu_weixin.jpg" style="width: 220px" />
<img src="https://assets.qszone.com/images/zhifu_zhifubao.jpg" style="width: 220px" />
</p>

View File

@@ -1,115 +0,0 @@
<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>

View File

@@ -1,13 +0,0 @@
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_,
}

View File

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

View File

@@ -1,15 +0,0 @@
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 +1,33 @@
export const OUTPUT_DIR = 'dist' export const OUTPUT_DIR = 'dist'
export const PROXY_CONFIG = {
/**
* @desc 替换匹配值
* @请求路径 http://localhost:3100/api/user
* @转发路径 http://localhost:8080/user
*/
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp('^/api'), ''),
},
/**
* @desc 不替换匹配值
* @请求路径 http://localhost:3100/api/v2/user
* @转发路径 http://localhost:8080/api/v2/user
*/
'/api/v2': {
target: 'http://localhost:8080',
changeOrigin: true,
},
/**
* @desc 替换部分匹配值
* @请求路径 http://localhost:3100/api/v3/user
* @转发路径 http://localhost:8080/user
*/
'/api/v3': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp('^/api'), ''),
},
}

View File

@@ -1,5 +1,4 @@
import { resolve } from 'path' import { resolve } from 'path'
import DefineOptions from 'unplugin-vue-define-options/vite'
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
@@ -19,7 +18,6 @@ import { getSrcPath } from '../utils'
const customIconPath = resolve(getSrcPath(), 'assets/svg') const customIconPath = resolve(getSrcPath(), 'assets/svg')
export default [ export default [
DefineOptions(),
AutoImport({ AutoImport({
imports: ['vue', 'vue-router'], imports: ['vue', 'vue-router'],
dts: false, dts: false,
@@ -33,7 +31,10 @@ export default [
defaultClass: 'inline-block', defaultClass: 'inline-block',
}), }),
Components({ Components({
resolvers: [NaiveUiResolver(), IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })], resolvers: [
NaiveUiResolver(),
IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' }),
],
dts: false, dts: false,
}), }),
createSvgIconsPlugin({ createSvgIconsPlugin({

View File

@@ -4,7 +4,7 @@ import dotenv from 'dotenv'
/** /**
* * 项目根路径 * * 项目根路径
* @descrition 结尾不带/ * @description 结尾不带/
*/ */
export function getRootPath() { export function getRootPath() {
return path.resolve(process.cwd()) return path.resolve(process.cwd())
@@ -13,14 +13,12 @@ export function getRootPath() {
/** /**
* * 项目src路径 * * 项目src路径
* @param srcName src目录名称(默认: "src") * @param srcName src目录名称(默认: "src")
* @descrition 结尾不带斜杠 * @description 结尾不带斜杠
*/ */
export function getSrcPath(srcName = 'src') { export function getSrcPath(srcName = 'src') {
return path.resolve(getRootPath(), srcName) return path.resolve(getRootPath(), srcName)
} }
const httpsReg = /^https:\/\//
export function convertEnv(envOptions) { export function convertEnv(envOptions) {
const result = {} const result = {}
if (!envOptions) return result if (!envOptions) return result

3
docs/使用unocss.md Normal file
View File

@@ -0,0 +1,3 @@
推荐阅读作者在掘金的文章:
[保熟的UnoCSS使用指北优雅使用antfu大佬的原子化CSS](https://juejin.cn/post/7142466784971456548)

40
docs/使用图标.md Normal file
View File

@@ -0,0 +1,40 @@
## 使用 iconify 图标
首先去图标库地址:[icones](https://icones.js.org/) 找合适的图标
### 1. 结合 unocss 使用
```html
<i i-carbon-sun />
<i class="i-carbon-sun" />
```
### 2. 结合插件 unplugin-icons 自定义标签使用
`<icon-[iconify图标名称]`
```html
<icon-ant-design:fullscreen-exit-outlined />
<icon-ant-design:fullscreen-outlined />
```
这种方式还支持自定义 svg 图标,本项目自定义 svg 图标固定放在 src/assets/svg 下
`<icon-custom-[svg图标文件名]`
```
<icon-custom-logo />
```
具体配置参看 build/plugin/unplugin.js
### 3. 结合 Naive UI 的 NIcon 组件封装使用
```html
<!-- iconify图标 -->
<TheIcon icon="material-symbols:delete-outline" />
<!-- 自定义svg图标 -->
<TheIcon icon="logo" type="custom" />
```
封装组件参看 src/components/icon

32
docs/安装pnpm.md Normal file
View File

@@ -0,0 +1,32 @@
## 安装pnpm
### 使用Corepack安装推荐
从 v16.13 开始Node.js 发布了 Corepack 来管理包管理器。 这是一项实验性功能,需要通过运行如下脚本来启用它:
```
npx corepack enable // 可能需要管理员权限
```
这将自动在您的系统上安装 pnpm。 但是,它可能不是最新版本的 pnpm。 若要升级,请检查[最新的 pnpm 版本](https://github.com/pnpm/pnpm/releases/latest) 并运行,如 7.14.0
```
corepack prepare pnpm@7.14.0 --activate
```
如果是 Node.js v16.17 或者更新的版本,可以直接安装最新版本的 pnpm
```
corepack prepare pnpm@latest --activate
```
### 使用npm安装
```
npm i -g pnpm
```
更新,卸了重新装
```
npm uninstall -g pnpm
npm i -g pnpm
```

View File

@@ -7,7 +7,7 @@
<meta http-equiv="Cache-control" content="no-cache" /> <meta http-equiv="Cache-control" content="no-cache" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.svg" /> <link rel="icon" href="/favicon.png" />
<link rel="stylesheet" href="/resource/loading.css" /> <link rel="stylesheet" href="/resource/loading.css" />
<title><%= title %></title> <title><%= title %></title>
@@ -17,7 +17,7 @@
<div id="app"> <div id="app">
<!-- 白屏时的loading效果 --> <!-- 白屏时的loading效果 -->
<div class="loading-container"> <div class="loading-container">
<div id="loadingLogo" class="loading-svg"></div> <img src="/resource/logo.png" alt="logo" height="128" />
<div class="loading-spin__container"> <div class="loading-spin__container">
<div class="loading-spin"> <div class="loading-spin">
<div class="left-0 top-0 loading-spin-item"></div> <div class="left-0 top-0 loading-spin-item"></div>

View File

@@ -1,11 +1,14 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext",
"baseUrl": "./", "baseUrl": "./",
"moduleResolution": "node",
"paths": { "paths": {
"~/*": ["./*"], "~/*": ["./*"],
"@/*": ["src/*"] "@/*": ["src/*"]
}, },
"jsx": "preserve" "jsx": "preserve",
"allowJs": true
}, },
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]
} }

View File

@@ -38,7 +38,8 @@ const posts = [
author: '大脸怪', author: '大脸怪',
category: 'Http', category: 'Http',
description: '谈谈前端缓存的理解', description: '谈谈前端缓存的理解',
content: '> 背景\n\n公司有个vue-cli3移动端web项目发版更新后发现部分用户手机在钉钉内置浏览器打开出现了缓存', content:
'> 背景\n\n公司有个vue-cli3移动端web项目发版更新后发现部分用户手机在钉钉内置浏览器打开出现了缓存',
isRecommend: true, isRecommend: true,
isPublish: true, isPublish: true,
createDate: '2021-06-10T18:51:19.000Z', createDate: '2021-06-10T18:51:19.000Z',
@@ -49,7 +50,8 @@ const posts = [
author: '大脸怪', author: '大脸怪',
category: 'JavaScript', category: 'JavaScript',
description: '简单介绍下在 Promise 类中有5 种静态方法及它们的使用场景', description: '简单介绍下在 Promise 类中有5 种静态方法及它们的使用场景',
content: '## 1. Promise.all\n\n并行执行多个 promise并等待所有 promise 都准备就绪。再对它们进行处理。', content:
'## 1. Promise.all\n\n并行执行多个 promise并等待所有 promise 都准备就绪。再对它们进行处理。',
isRecommend: true, isRecommend: true,
isPublish: true, isPublish: true,
createDate: '2021-02-22T22:37:06.000Z', createDate: '2021-02-22T22:37:06.000Z',
@@ -65,7 +67,9 @@ export default [
const { title, pageNo, pageSize } = data.query const { title, pageNo, pageSize } = data.query
let pageData = [] let pageData = []
let total = 60 let total = 60
const filterData = posts.filter((item) => item.title.includes(title) || (!title && title !== 0)) const filterData = posts.filter(
(item) => item.title.includes(title) || (!title && title !== 0)
)
if (filterData.length) { if (filterData.length) {
if (pageSize) { if (pageSize) {
while (pageData.length < pageSize) { while (pageData.length < pageSize) {

View File

@@ -2,71 +2,80 @@
"name": "vue-naive-admin", "name": "vue-naive-admin",
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"build": "vite build",
"build:github": "vite build --mode github && esno ./build/script",
"build:test": "vite build --mode test",
"cz": "cz",
"dev": "vite", "dev": "vite",
"lint": "eslint --ext .js,.vue .", "lint": "eslint --ext .js,.vue .",
"lint:fix": "eslint --fix --ext .js,.vue .", "lint:fix": "eslint --fix --ext .js,.vue .",
"lint:staged": "lint-staged", "lint:staged": "lint-staged",
"build": "vite build",
"build:test": "vite build --mode test",
"build:github": "vite build --mode github && esno ./build/script",
"preview": "vite preview",
"prepare": "husky install", "prepare": "husky install",
"cz": "cz" "preview": "vite preview"
}, },
"dependencies": { "lint-staged": {
"@vueuse/core": "^8.4.2", "*.{js,vue}": [
"axios": "^0.21.4", "eslint --ext .js,.vue ."
"dayjs": "^1.11.0", ]
"lodash-es": "^4.17.21",
"md-editor-v3": "^1.11.4",
"mockjs": "^1.1.0",
"pinia": "^2.0.13",
"vue": "^3.2.39",
"vue-router": "^4.1.5"
},
"devDependencies": {
"@commitlint/cli": "^17.0.3",
"@commitlint/config-conventional": "^17.0.3",
"@iconify/json": "^2.1.99",
"@iconify/vue": "^3.2.1",
"@vitejs/plugin-vue": "^1.10.2",
"@vue/compiler-sfc": "^3.2.31",
"chalk": "^5.0.1",
"commitizen": "^4.2.4",
"cz-conventional-changelog": "^3.3.0",
"cz-customizable": "^6.9.0",
"dotenv": "^10.0.0",
"eslint": "^8.12.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.5.0",
"esno": "^0.13.0",
"fs-extra": "^10.0.1",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"naive-ui": "^2.33.3",
"prettier": "^2.6.1",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "^1.49.10",
"unocss": "^0.43.2",
"unplugin-auto-import": "^0.9.2",
"unplugin-icons": "^0.14.9",
"unplugin-vue-components": "^0.17.21",
"unplugin-vue-define-options": "^0.11.2",
"vite": "^3.1.1",
"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"
}, },
"config": { "config": {
"commitizen": { "commitizen": {
"path": "node_modules/cz-customizable" "path": "node_modules/cz-customizable"
} }
}, },
"lint-staged": { "eslintConfig": {
"*.{js,vue}": [ "extends": [
"eslint --ext .js,.vue ." "@zclzone",
"@unocss",
".eslint-global-variables.json"
] ]
},
"dependencies": {
"@unocss/eslint-config": "^0.55.7",
"@vueuse/core": "^10.4.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "5.1.12",
"axios": "^1.5.1",
"dayjs": "^1.11.10",
"echarts": "^5.4.3",
"lodash-es": "^4.17.21",
"md-editor-v3": "^4.7.0",
"mockjs": "^1.1.0",
"pinia": "^2.1.6",
"vite": "^4.4.11",
"vue": "3.3.4",
"vue-echarts": "^6.6.1",
"vue-router": "^4.2.5",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@commitlint/cli": "^17.7.2",
"@commitlint/config-conventional": "^17.7.0",
"@iconify/json": "^2.2.125",
"@iconify/vue": "^4.1.1",
"@unocss/preset-rem-to-px": "^0.55.7",
"@vitejs/plugin-vue": "^4.4.0",
"@vue/compiler-sfc": "^3.3.4",
"@zclzone/eslint-config": "^0.0.4",
"chalk": "^5.3.0",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"cz-customizable": "^7.0.0",
"dotenv": "^16.3.1",
"esno": "^0.17.0",
"fs-extra": "^11.1.1",
"husky": "^8.0.3",
"lint-staged": "^14.0.1",
"naive-ui": "^2.35.0",
"rollup-plugin-visualizer": "^5.9.2",
"sass": "^1.69.0",
"unocss": "0.55.3",
"unplugin-auto-import": "^0.16.6",
"unplugin-icons": "^0.16.6",
"unplugin-vue-components": "^0.25.2",
"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"
} }
} }

6116
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
module.exports = {
printWidth: 120,
singleQuote: true,
semi: false,
endOfLine: 'lf',
}

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -10,12 +10,6 @@
height: 100%; height: 100%;
} }
.loading-svg {
width: 128px;
height: 128px;
color: var(--primary-color);
}
.loading-spin__container { .loading-spin__container {
width: 56px; width: 56px;
height: 56px; height: 56px;

View File

@@ -1,17 +1,3 @@
/**
* 初始化加载效果的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
if (appEl) {
appEl.appendChild(div)
}
}
function addThemeColorCssVars() { function addThemeColorCssVars() {
const key = '__THEME_COLOR__' const key = '__THEME_COLOR__'
const defaultColor = '#316c72' const defaultColor = '#316c72'
@@ -21,5 +7,3 @@ function addThemeColorCssVars() {
} }
addThemeColorCssVars() addThemeColorCssVars()
initSvgLogo('#loadingLogo')

BIN
public/resource/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

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

View File

@@ -1,18 +0,0 @@
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

@@ -11,7 +11,27 @@
"primaryColor": "#316C72FF", "primaryColor": "#316C72FF",
"primaryColorHover": "#316C72E3", "primaryColorHover": "#316C72E3",
"primaryColorPressed": "#2B4C59FF", "primaryColorPressed": "#2B4C59FF",
"primaryColorSuppl": "#316C7263" "primaryColorSuppl": "#316C72E3",
"infoColor": "#2080F0FF",
"infoColorHover": "#4098FCFF",
"infoColorPressed": "#1060C9FF",
"infoColorSuppl": "#4098FCFF",
"successColor": "#18A058FF",
"successColorHover": "#36AD6AFF",
"successColorPressed": "#0C7A43FF",
"successColorSuppl": "#36AD6AFF",
"warningColor": "#F0A020FF",
"warningColorHover": "#FCB040FF",
"warningColorPressed": "#C97C10FF",
"warningColorSuppl": "#FCB040FF",
"errorColor": "#D03050FF",
"errorColorHover": "#DE576DFF",
"errorColorPressed": "#AB1F3FFF",
"errorColorSuppl": "#DE576DFF"
} }
} }
} }

View File

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

BIN
src/assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

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

Before

Width:  |  Height:  |  Size: 811 B

View File

@@ -1,11 +1,21 @@
<template> <template>
<footer text-14 f-c-c flex-col color="#6a6a6a"> <footer f-c-c flex-col text-14 color="#6a6a6a">
<p> <p>
Copyright©2022 Copyright © 2022-present
<a href="https://github.com/zclzone" target="__blank" hover="decoration-underline color-primary"> 大脸怪</a> <a
href="https://github.com/zclzone"
target="__blank"
hover="decoration-underline color-primary"
>
Ronnie Zhang
</a>
</p> </p>
<p> <p>
<a href="http://beian.miit.gov.cn/" target="__blank" hover="decoration-underline color-primary"> <a
href="http://beian.miit.gov.cn/"
target="__blank"
hover="decoration-underline color-primary"
>
赣ICP备2020015008号-1 赣ICP备2020015008号-1
</a> </a>
</p> </p>

View File

@@ -1,25 +1,23 @@
<template> <template>
<n-config-provider wh-full :theme-overrides="naiveThemeOverrides"> <n-config-provider
<n-loading-bar-provider> wh-full
<n-dialog-provider> :locale="zhCN"
<n-notification-provider> :date-locale="dateZhCN"
<n-message-provider> :theme="appStore.isDark ? darkTheme : undefined"
<slot></slot> :theme-overrides="naiveThemeOverrides"
<NaiveProviderContent /> >
</n-message-provider> <slot />
</n-notification-provider>
</n-dialog-provider>
</n-loading-bar-provider>
</n-config-provider> </n-config-provider>
</template> </template>
<script setup> <script setup>
import { defineComponent, h } from 'vue' import { zhCN, dateZhCN, darkTheme } from 'naive-ui'
import { useLoadingBar, useDialog, useMessage, useNotification } from 'naive-ui'
import { useCssVar } from '@vueuse/core' import { useCssVar } from '@vueuse/core'
import { kebabCase } from 'lodash-es' import { kebabCase } from 'lodash-es'
import { setupMessage, setupDialog } from '@/utils/common/naiveTools'
import { naiveThemeOverrides } from '~/settings' import { naiveThemeOverrides } from '~/settings'
import { useAppStore } from '@/store'
const appStore = useAppStore()
function setupCssVar() { function setupCssVar() {
const common = naiveThemeOverrides.common const common = naiveThemeOverrides.common
@@ -28,23 +26,5 @@ function setupCssVar() {
if (key === 'primaryColor') window.localStorage.setItem('__THEME_COLOR__', common[key] || '') if (key === 'primaryColor') window.localStorage.setItem('__THEME_COLOR__', common[key] || '')
} }
} }
// 挂载naive组件的方法至window, 以便在全局使用
function setupNaiveTools() {
window.$loadingBar = useLoadingBar()
window.$notification = useNotification()
window.$message = setupMessage(useMessage())
window.$dialog = setupDialog(useDialog())
}
const NaiveProviderContent = defineComponent({
setup() {
setupCssVar() setupCssVar()
setupNaiveTools()
},
render() {
return h('div')
},
})
</script> </script>

View File

@@ -1,10 +1,10 @@
<template> <template>
<div ref="wrapper" class="wrapper" @mousewheel.prevent="handleMouseWheel"> <div ref="wrapper" class="wrapper" @mousewheel.prevent="handleMouseWheel">
<template v-if="showArrow && isOverflow"> <template v-if="showArrow && isOverflow">
<div class="left" @click="handleMouseWheel({ wheelDelta: 120 })"> <div class="left dark:bg-dark!" @click="handleMouseWheel({ wheelDelta: 120 })">
<icon-ic:baseline-keyboard-arrow-left /> <icon-ic:baseline-keyboard-arrow-left />
</div> </div>
<div class="right" @click="handleMouseWheel({ wheelDelta: -120 })"> <div class="right dark:bg-dark!" @click="handleMouseWheel({ wheelDelta: -120 })">
<icon-ic:baseline-keyboard-arrow-right /> <icon-ic:baseline-keyboard-arrow-right />
</div> </div>
</template> </template>
@@ -23,7 +23,7 @@
</template> </template>
<script setup> <script setup>
import { debounce } from '@/utils' import { debounce, useResize } from '@/utils'
defineProps({ defineProps({
showArrow: { showArrow: {
@@ -38,16 +38,16 @@ const wrapper = ref(null)
const isOverflow = ref(false) const isOverflow = ref(false)
const refreshIsOverflow = debounce(() => { const refreshIsOverflow = debounce(() => {
const wrapperWidth = wrapper.value.offsetWidth const wrapperWidth = wrapper.value?.offsetWidth
const contentWidth = content.value.offsetWidth const contentWidth = content.value?.offsetWidth
isOverflow.value = contentWidth > wrapperWidth isOverflow.value = contentWidth > wrapperWidth
resetTranslateX(wrapperWidth, contentWidth) resetTranslateX(wrapperWidth, contentWidth)
}, 200) }, 200)
function handleMouseWheel(e) { function handleMouseWheel(e) {
const { wheelDelta } = e const { wheelDelta } = e
const wrapperWidth = wrapper.value.offsetWidth const wrapperWidth = wrapper.value?.offsetWidth
const contentWidth = content.value.offsetWidth const contentWidth = content.value?.offsetWidth
/** /**
* @wheelDelta 平行滚动的值 >0 右移 <0: 左移 * @wheelDelta 平行滚动的值 >0 右移 <0: 左移
* @translateX 内容translateX的值 * @translateX 内容translateX的值
@@ -76,17 +76,39 @@ const resetTranslateX = debounce(function (wrapperWidth, contentWidth) {
} }
}, 200) }, 200)
const observer = new MutationObserver(refreshIsOverflow) const observers = ref([])
onMounted(() => { onMounted(() => {
refreshIsOverflow() refreshIsOverflow()
window.addEventListener('resize', refreshIsOverflow) observers.value.push(useResize(document.body, refreshIsOverflow))
// 监听内容宽度刷新是否超出 observers.value.push(useResize(content.value, refreshIsOverflow))
observer.observe(content.value, { childList: true })
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('resize', refreshIsOverflow) observers.value.forEach((item) => {
observer.disconnect() item?.disconnect()
})
})
function handleScroll(x, width) {
const wrapperWidth = wrapper.value?.offsetWidth
const contentWidth = content.value?.offsetWidth
if (contentWidth <= wrapperWidth) return
// 当 x 小于可视范围的最小值时
if (x < -translateX.value + 150) {
translateX.value = -(x - 150)
resetTranslateX(wrapperWidth, contentWidth)
}
// 当 x 大于可视范围的最大值时
if (x + width > -translateX.value + wrapperWidth) {
translateX.value = wrapperWidth - (x + width)
resetTranslateX(wrapperWidth, contentWidth)
}
}
defineExpose({
handleScroll,
}) })
</script> </script>

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { renderIcon, renderCustomIcon } from '@/utils/icon' import { renderIcon, renderCustomIcon } from '@/utils'
const props = defineProps({ const props = defineProps({
icon: { icon: {

View File

@@ -1,8 +1,9 @@
<template> <template>
<transition name="fade-slide" mode="out-in" appear> <transition name="fade-slide" mode="out-in" appear>
<section class="cus-scroll-y wh-full p-15 flex-col bg-[#f5f6fb]"> <section class="cus-scroll-y wh-full flex-col bg-[#f5f6fb] p-15 dark:bg-hex-121212">
<slot /> <slot />
<AppFooter v-if="showFooter" mt-15 /> <AppFooter v-if="showFooter" mt-15 />
<n-back-top :bottom="20" />
</section> </section>
</transition> </transition>
</template> </template>

View File

@@ -1,14 +1,14 @@
<template> <template>
<AppPage :show-footer="showFooter"> <AppPage :show-footer="showFooter">
<header v-if="showHeader" px-15 mb-15 min-h-45 flex justify-between items-center> <header v-if="showHeader" mb-15 min-h-45 flex items-center justify-between px-15>
<slot v-if="$slots.header" name="header" /> <slot v-if="$slots.header" name="header" />
<template v-else> <template v-else>
<h2 color="#333" text-22 font-normal>{{ title || route.meta?.title }}</h2> <h2 text-22 font-normal text-hex-333 dark:text-hex-ccc>{{ title || route.meta?.title }}</h2>
<slot name="action" /> <slot name="action" />
</template> </template>
</header> </header>
<n-card rounded-10 flex-1> <n-card flex-1 rounded-10>
<slot /> <slot />
</n-card> </n-card>
</AppPage> </AppPage>

View File

@@ -1,5 +1,16 @@
<template> <template>
<div min-h-60 p-15 flex items-start justify-between b-1 bc-ccc rounded-8 bg="#fafafc"> <div
bg="#fafafc"
min-h-60
flex
items-start
justify-between
b-1
rounded-8
p-15
bc-ccc
dark:bg-black
>
<n-space wrap :size="[35, 15]"> <n-space wrap :size="[35, 15]">
<slot /> <slot />
</n-space> </n-space>

View File

@@ -1,6 +1,11 @@
<template> <template>
<div flex items-center> <div flex items-center>
<label v-if="!isNullOrWhitespace(label)" w-80 flex-shrink-0 :style="{ width: labelWidth + 'px' }"> <label
v-if="!isNullOrWhitespace(label)"
w-80
flex-shrink-0
:style="{ width: labelWidth + 'px' }"
>
{{ label }} {{ label }}
</label> </label>
<div :style="{ width: contentWidth + 'px' }" flex-shrink-0> <div :style="{ width: contentWidth + 'px' }" flex-shrink-0>
@@ -10,7 +15,7 @@
</template> </template>
<script setup> <script setup>
import { isNullOrWhitespace } from '@/utils/is' import { isNullOrWhitespace } from '@/utils'
defineProps({ defineProps({
label: { label: {

View File

@@ -1,5 +1,12 @@
<template> <template>
<n-modal v-model:show="show" :style="{ width }" preset="card" :title="title" size="huge" :bordered="false"> <n-modal
v-model:show="show"
:style="{ width }"
preset="card"
:title="title"
size="huge"
:bordered="false"
>
<slot /> <slot />
<template v-if="showFooter" #footer> <template v-if="showFooter" #footer>
<footer flex justify-end> <footer flex justify-end>

View File

@@ -17,6 +17,8 @@
</template> </template>
<script setup> <script setup>
import { utils, writeFile } from 'xlsx'
const props = defineProps({ const props = defineProps({
/** /**
* @remote true: 后端分页 false 前端分页 * @remote true: 后端分页 false 前端分页
@@ -73,7 +75,7 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['update:queryItems', 'onChecked']) const emit = defineEmits(['update:queryItems', 'onChecked', 'onDataChange'])
const loading = ref(false) const loading = ref(false)
const initQuery = { ...props.queryItems } const initQuery = { ...props.queryItems }
const tableData = ref([]) const tableData = ref([])
@@ -87,13 +89,18 @@ async function handleQuery() {
if (props.isPagination && props.remote) { if (props.isPagination && props.remote) {
paginationParams = { pageNo: pagination.page, pageSize: pagination.pageSize } paginationParams = { pageNo: pagination.page, pageSize: pagination.pageSize }
} }
const { data } = await props.getData({ ...props.queryItems, ...props.extraParams, ...paginationParams }) const { data } = await props.getData({
...props.queryItems,
...props.extraParams,
...paginationParams,
})
tableData.value = data?.pageData || data tableData.value = data?.pageData || data
pagination.itemCount = data.total ?? data.length pagination.itemCount = data.total ?? data.length
} catch (error) { } catch (error) {
tableData.value = [] tableData.value = []
pagination.itemCount = 0 pagination.itemCount = 0
} finally { } finally {
emit('onDataChange', tableData.value)
loading.value = false loading.value = false
} }
} }
@@ -122,9 +129,21 @@ function onChecked(rowKeys) {
emit('onChecked', rowKeys) emit('onChecked', rowKeys)
} }
} }
function handleExport(columns = props.columns, data = tableData.value) {
if (!data?.length) return $message.warning('没有数据')
const columnsData = columns.filter((item) => !!item.title && !item.hideInExcel)
const thKeys = columnsData.map((item) => item.key)
const thData = columnsData.map((item) => item.title)
const trData = data.map((item) => thKeys.map((key) => item[key]))
const sheet = utils.aoa_to_sheet([thData, ...trData])
const workBook = utils.book_new()
utils.book_append_sheet(workBook, sheet, '数据报表')
writeFile(workBook, '数据报表.xlsx')
}
defineExpose({ defineExpose({
handleSearch, handleSearch,
handleReset, handleReset,
handleExport,
}) })
</script> </script>

View File

@@ -1,4 +1,4 @@
import { isNullOrWhitespace } from '@/utils/is' import { isNullOrWhitespace } from '@/utils'
const ACTIONS = { const ACTIONS = {
view: '查看', view: '查看',

View File

@@ -1,19 +1,16 @@
<template> <template>
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<KeepAlive :include="keepAliveRouteNames"> <KeepAlive :include="keepAliveNames">
<component :is="Component" v-if="appStore.reloadFlag" :key="route.meta?.key || route.fullPath" /> <component :is="Component" v-if="!tagStore.reloading" :key="route.fullPath" />
</KeepAlive> </KeepAlive>
</router-view> </router-view>
</template> </template>
<script setup> <script setup>
import { useAppStore } from '@/store/modules/app' import { useTagsStore } from '@/store'
import { useRouter } from 'vue-router' const tagStore = useTagsStore()
const appStore = useAppStore()
const router = useRouter()
const allRoutes = router.getRoutes() const keepAliveNames = computed(() => {
const keepAliveRouteNames = computed(() => { return tagStore.tags.filter((item) => item.keepAlive).map((item) => item.name)
return allRoutes.filter((route) => route.meta?.keepAlive).map((route) => route.name)
}) })
</script> </script>

View File

@@ -12,7 +12,7 @@
</template> </template>
<script setup> <script setup>
import { renderCustomIcon, renderIcon } from '@/utils/icon' import { renderCustomIcon, renderIcon } from '@/utils'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()

View File

@@ -6,7 +6,7 @@
</template> </template>
<script setup> <script setup>
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store'
const appStore = useAppStore() const appStore = useAppStore()
</script> </script>

View File

@@ -0,0 +1,82 @@
<template>
<n-popover trigger="click" placement="bottom" @update:show="handlePopoverShow">
<template #trigger>
<n-badge :value="count" mr-20 cursor-pointer>
<n-icon size="18" color-black dark="color-hex-fff">
<icon-material-symbols:notifications-outline />
</n-icon>
</n-badge>
</template>
<n-tabs v-model:value="activeTab" type="line" justify-content="space-around" animated>
<n-tab-pane
v-for="tab in tabs"
:key="tab.name"
:name="tab.name"
:tab="tab.title + `(${tab.messages.length})`"
>
<ul class="cus-scroll-y max-h-200 w-220">
<li
v-for="(item, index) in tab.messages"
:key="index"
class="flex-col py-12"
border-t="1 solid gray-200"
:style="index > 0 ? '' : 'border: none;'"
>
<span mb-4 text-ellipsis>{{ item.content }}</span>
<span text-hex-bbb>{{ item.time }}</span>
</li>
</ul>
</n-tab-pane>
</n-tabs>
</n-popover>
</template>
<script setup>
import { formatDateTime } from '@/utils'
const activeTab = ref('')
const tabs = [
{
name: 'zan',
title: '点赞',
messages: [
{ content: '你的文章《XX》收到一条点赞', time: formatDateTime() },
{ content: '你的文章《YY》收到一条点赞', time: formatDateTime() },
{ content: '你的文章《AA》收到一条点赞', time: formatDateTime() },
{ content: '你的文章《BB》收到一条点赞', time: formatDateTime() },
{ content: '你的文章《CC》收到一条点赞', time: formatDateTime() },
{ content: '你的文章《DD》收到一条点赞', time: formatDateTime() },
],
},
{
name: 'star',
title: '关注',
messages: [
{ content: '张三 关注了你', time: formatDateTime() },
{ content: '王五 关注了你', time: formatDateTime() },
],
},
{
name: 'comment',
title: '评论',
messages: [
{ content: '张三 评论了你的文章《XX》"学到了"', time: formatDateTime() },
{ content: '李四 评论了你的文章《YY》"不如Vue"', time: formatDateTime() },
],
},
]
const count = ref(tabs.map((item) => item.messages).flat().length)
watch(activeTab, (v) => {
if (count === 0) return
const tabIndex = tabs.findIndex((item) => item.name === v)
count.value -= tabs[tabIndex].messages.length
if (count.value < 0) count.value = 0
})
function handlePopoverShow(show) {
if (show && !activeTab.value) {
activeTab.value = tabs[0]?.name
}
}
</script>

View File

@@ -0,0 +1,18 @@
<script setup>
import { useAppStore } from '@/store'
import { useDark, useToggle } from '@vueuse/core'
const appStore = useAppStore()
const isDark = useDark()
const toggleDark = () => {
appStore.toggleDark()
useToggle(isDark)()
}
</script>
<template>
<n-icon mr-20 cursor-pointer size="18" @click="toggleDark">
<icon-mdi-moon-waning-crescent v-if="isDark" />
<icon-mdi-white-balance-sunny v-else />
</n-icon>
</template>

View File

@@ -1,15 +1,15 @@
<template> <template>
<n-dropdown :options="options" @select="handleSelect"> <n-dropdown :options="options" @select="handleSelect">
<div flex items-center cursor-pointer> <div flex cursor-pointer items-center>
<img :src="userStore.avatar" mr10 w-35 h-35 rounded-full /> <img :src="userStore.avatar" mr10 h-35 w-35 rounded-full />
<span>{{ userStore.name }}</span> <span>{{ userStore.name }}</span>
</div> </div>
</n-dropdown> </n-dropdown>
</template> </template>
<script setup> <script setup>
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store'
import { renderIcon } from '@/utils/icon' import { renderIcon } from '@/utils'
const userStore = useUserStore() const userStore = useUserStore()

View File

@@ -1,9 +1,11 @@
<template> <template>
<div flex items-center> <div flex items-center>
<MenuCollapse /> <MenuCollapse />
<BreadCrumb ml-15 /> <BreadCrumb ml-15 hidden sm:block />
</div> </div>
<div ml-auto flex items-center> <div ml-auto flex items-center>
<MessageNotification />
<ThemeMode />
<GithubSite /> <GithubSite />
<FullScreen /> <FullScreen />
<UserAvatar /> <UserAvatar />
@@ -16,4 +18,6 @@ import MenuCollapse from './components/MenuCollapse.vue'
import FullScreen from './components/FullScreen.vue' import FullScreen from './components/FullScreen.vue'
import UserAvatar from './components/UserAvatar.vue' import UserAvatar from './components/UserAvatar.vue'
import GithubSite from './components/GithubSite.vue' import GithubSite from './components/GithubSite.vue'
import ThemeMode from './components/ThemeMode.vue'
import MessageNotification from './components/MessageNotification.vue'
</script> </script>

View File

@@ -1,14 +1,14 @@
<template> <template>
<router-link h-60 f-c-c to="/"> <router-link h-60 f-c-c to="/">
<icon-custom-logo text-36 color-primary></icon-custom-logo> <img src="@/assets/images/logo.png" height="42" />
<h2 v-show="!appStore.collapsed" ml-10 color-primary text-16 font-bold max-w-140 flex-shrink-0> <h2 v-show="!appStore.collapsed" ml-10 max-w-140 flex-shrink-0 text-16 font-bold color-primary>
{{ title }} {{ title }}
</h2> </h2>
</router-link> </router-link>
</template> </template>
<script setup> <script setup>
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store'
const title = import.meta.env.VITE_TITLE const title = import.meta.env.VITE_TITLE
const appStore = useAppStore() const appStore = useAppStore()

View File

@@ -1,32 +1,37 @@
<template> <template>
<n-menu <n-menu
ref="menu"
class="side-menu" class="side-menu"
accordion accordion
:indent="18" :indent="18"
:collapsed-icon-size="22" :collapsed-icon-size="22"
:collapsed-width="64" :collapsed-width="64"
:options="menuOptions" :options="menuOptions"
:value="curRoute.meta?.activeMenu || curRoute.name" :value="activeKey"
@update:value="handleMenuSelect" @update:value="handleMenuSelect"
/> />
</template> </template>
<script setup> <script setup>
import { usePermissionStore } from '@/store/modules/permission' import { usePermissionStore } from '@/store'
import { renderCustomIcon, renderIcon, isExternal } from '@/utils'
import { isExternal } from '@/utils/is'
import { useAppStore } from '@/store/modules/app'
import { renderCustomIcon, renderIcon } from '@/utils/icon'
const router = useRouter() const router = useRouter()
const curRoute = useRoute() const curRoute = useRoute()
const permissionStore = usePermissionStore() const permissionStore = usePermissionStore()
const appStore = useAppStore()
const activeKey = computed(() => curRoute.meta?.activeMenu || curRoute.name)
const menuOptions = computed(() => { const menuOptions = computed(() => {
return permissionStore.menus.map((item) => getMenuItem(item)).sort((a, b) => a.order - b.order) return permissionStore.menus.map((item) => getMenuItem(item)).sort((a, b) => a.order - b.order)
}) })
const menu = ref(null)
watch(curRoute, async () => {
await nextTick()
menu.value?.showOption()
})
function resolvePath(basePath, path) { function resolvePath(basePath, path) {
if (isExternal(path)) return path if (isExternal(path)) return path
return ( return (
@@ -47,7 +52,9 @@ function getMenuItem(route, basePath = '') {
order: route.meta?.order || 0, order: route.meta?.order || 0,
} }
const visibleChildren = route.children ? route.children.filter((item) => item.name && !item.isHidden) : [] const visibleChildren = route.children
? route.children.filter((item) => item.name && !item.isHidden)
: []
if (!visibleChildren.length) return menuItem if (!visibleChildren.length) return menuItem
@@ -55,25 +62,28 @@ function getMenuItem(route, basePath = '') {
// 单个子路由处理 // 单个子路由处理
const singleRoute = visibleChildren[0] const singleRoute = visibleChildren[0]
menuItem = { menuItem = {
...menuItem,
label: singleRoute.meta?.title || singleRoute.name, label: singleRoute.meta?.title || singleRoute.name,
key: singleRoute.name, key: singleRoute.name,
path: resolvePath(menuItem.path, singleRoute.path), path: resolvePath(menuItem.path, singleRoute.path),
icon: getIcon(singleRoute.meta), icon: getIcon(singleRoute.meta),
order: menuItem.order,
} }
const visibleItems = singleRoute.children ? singleRoute.children.filter((item) => item.name && !item.isHidden) : [] const visibleItems = singleRoute.children
? singleRoute.children.filter((item) => item.name && !item.isHidden)
: []
if (visibleItems.length === 1) { if (visibleItems.length === 1) {
menuItem = getMenuItem(visibleItems[0], menuItem.path) menuItem = getMenuItem(visibleItems[0], menuItem.path)
} else if (visibleItems.length > 1) { } else if (visibleItems.length > 1) {
menuItem.children = visibleItems.map((item) => getMenuItem(item, menuItem.path)).sort((a, b) => a.order - b.order) menuItem.children = visibleItems
.map((item) => getMenuItem(item, menuItem.path))
.sort((a, b) => a.order - b.order)
} }
} else { } else {
menuItem.children = visibleChildren menuItem.children = visibleChildren
.map((item) => getMenuItem(item, menuItem.path)) .map((item) => getMenuItem(item, menuItem.path))
.sort((a, b) => a.order - b.order) .sort((a, b) => a.order - b.order)
} }
return menuItem return menuItem
} }
@@ -86,14 +96,10 @@ function getIcon(meta) {
function handleMenuSelect(key, item) { function handleMenuSelect(key, item) {
if (isExternal(item.path)) { if (isExternal(item.path)) {
window.open(item.path) window.open(item.path)
} else {
if (item.path === curRoute.path) {
appStore.reloadPage()
} else { } else {
router.push(item.path) router.push(item.path)
} }
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -11,9 +11,8 @@
</template> </template>
<script setup> <script setup>
import { useTagsStore } from '@/store/modules/tags' import { useTagsStore } from '@/store'
import { renderIcon } from '@/utils/icon' import { renderIcon } from '@/utils'
import { useAppStore } from '@/store/modules/app'
const props = defineProps({ const props = defineProps({
show: { show: {
@@ -37,7 +36,6 @@ const props = defineProps({
const emit = defineEmits(['update:show']) const emit = defineEmits(['update:show'])
const tagsStore = useTagsStore() const tagsStore = useTagsStore()
const appStore = useAppStore()
const options = computed(() => [ const options = computed(() => [
{ {
@@ -67,7 +65,9 @@ const options = computed(() => [
{ {
label: '关闭右侧', label: '关闭右侧',
key: 'close-right', key: 'close-right',
disabled: tagsStore.tags.length <= 1 || props.currentPath === tagsStore.tags[tagsStore.tags.length - 1].path, disabled:
tagsStore.tags.length <= 1 ||
props.currentPath === tagsStore.tags[tagsStore.tags.length - 1].path,
icon: renderIcon('mdi:arrow-expand-right', { size: '14px' }), icon: renderIcon('mdi:arrow-expand-right', { size: '14px' }),
}, },
]) ])
@@ -77,11 +77,7 @@ const actionMap = new Map([
[ [
'reload', 'reload',
() => { () => {
if (route.meta?.keepAlive) { tagsStore.reloadTag(route.path, route.meta?.keepAlive)
// 重置keepAlive
route.meta.key = +new Date()
}
appStore.reloadPage()
}, },
], ],
[ [

View File

@@ -1,15 +1,19 @@
<template> <template>
<ScrollX> <ScrollX ref="scrollXRef" class="bg-white dark:bg-dark!">
<n-tag <n-tag
v-for="tag in tagsStore.tags" v-for="tag in tagsStore.tags"
ref="tabRefs"
:key="tag.path" :key="tag.path"
class="px-15 mx-5 rounded-4 cursor-pointer hover:color-primary" class="mx-5 cursor-pointer rounded-4 px-15 hover:color-primary"
:type="tagsStore.activeTag === tag.path ? 'primary' : 'default'" :type="tagsStore.activeTag === tag.path ? 'primary' : 'default'"
:closable="tagsStore.tags.length > 1" :closable="tagsStore.tags.length > 1"
@click="handleTagClick(tag.path)" @click="handleTagClick(tag.path)"
@close.stop="tagsStore.removeTag(tag.path)" @close.stop="tagsStore.removeTag(tag.path)"
@contextmenu.prevent="handleContextMenu($event, tag)" @contextmenu.prevent="handleContextMenu($event, tag)"
> >
<template v-if="tag.icon" #icon>
<TheIcon :icon="tag.icon" class="mr-4" />
</template>
{{ tag.title }} {{ tag.title }}
</n-tag> </n-tag>
<ContextMenu <ContextMenu
@@ -24,12 +28,14 @@
<script setup> <script setup>
import ContextMenu from './ContextMenu.vue' import ContextMenu from './ContextMenu.vue'
import { useTagsStore } from '@/store/modules/tags' import { useTagsStore } from '@/store'
import ScrollX from '@/components/common/ScrollX.vue' import ScrollX from '@/components/common/ScrollX.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const tagsStore = useTagsStore() const tagsStore = useTagsStore()
const tabRefs = ref([])
const scrollXRef = ref(null)
const contextMenuOption = reactive({ const contextMenuOption = reactive({
show: false, show: false,
@@ -41,9 +47,23 @@ const contextMenuOption = reactive({
watch( watch(
() => route.path, () => route.path,
() => { () => {
const { name, path } = route const { name, fullPath: path } = route
const title = route.meta?.title const title = route.meta?.title
tagsStore.addTag({ name, path, title }) const icon = route.meta?.icon
const keepAlive = route.meta?.keepAlive
tagsStore.addTag({ name, path, title, icon, keepAlive })
},
{ immediate: true }
)
watch(
() => tagsStore.activeIndex,
async (activeIndex) => {
await nextTick()
const activeTabElement = tabRefs.value[activeIndex]?.$el
if (!activeTabElement) return
const { offsetLeft: x, offsetWidth: width } = activeTabElement
scrollXRef.value?.handleScroll(x + width, width)
}, },
{ immediate: true } { immediate: true }
) )

View File

@@ -11,14 +11,19 @@
<SideBar /> <SideBar />
</n-layout-sider> </n-layout-sider>
<article flex-1 flex-col overflow-hidden> <article flex-col flex-1 overflow-hidden>
<header bg-white px-15 border-b bc-eee flex items-center :style="`height: ${header.height}px`"> <header
border-b="1 solid #eee"
class="flex items-center bg-white px-15"
dark="bg-dark border-0"
:style="`height: ${header.height}px`"
>
<AppHeader /> <AppHeader />
</header> </header>
<section v-if="tags.visible" border-b bc-eee> <section v-if="tags.visible" hidden border-b bc-eee sm:block dark:border-0>
<AppTags :style="{ height: `${tags.height}px` }" /> <AppTags :style="{ height: `${tags.height}px` }" />
</section> </section>
<section flex-1 overflow-hidden> <section flex-1 overflow-hidden bg-hex-f5f6fb dark:bg-hex-101014>
<AppMain /> <AppMain />
</section> </section>
</article> </article>
@@ -30,7 +35,7 @@ import AppHeader from './components/header/index.vue'
import SideBar from './components/sidebar/index.vue' import SideBar from './components/sidebar/index.vue'
import AppMain from './components/AppMain.vue' import AppMain from './components/AppMain.vue'
import AppTags from './components/tags/index.vue' import AppTags from './components/tags/index.vue'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store'
import { header, tags } from '~/settings' import { header, tags } from '~/settings'
const appStore = useAppStore() const appStore = useAppStore()

View File

@@ -1,17 +1,20 @@
/** 重置样式 */
import '@/styles/reset.css' import '@/styles/reset.css'
import '@/styles/index.scss'
import 'uno.css' import 'uno.css'
import '@/styles/global.scss'
import 'virtual:svg-icons-register' import 'virtual:svg-icons-register'
import { createApp } from 'vue' import { createApp } from 'vue'
import { setupRouter } from '@/router' import { setupRouter } from '@/router'
import { setupStore } from '@/store' import { setupStore } from '@/store'
import App from './App.vue' import App from './App.vue'
import { setupNaiveDiscreteApi } from './utils'
async function setupApp() { async function setupApp() {
const app = createApp(App) const app = createApp(App)
setupStore(app) setupStore(app)
setupNaiveDiscreteApi()
await setupRouter(app) await setupRouter(app)

View File

@@ -1,5 +1,4 @@
import { getToken, refreshAccessToken } from '@/utils/token' import { getToken, refreshAccessToken, isNullOrWhitespace } from '@/utils'
import { isNullOrWhitespace } from '@/utils/is'
const WHITE_LIST = ['/login', '/404'] const WHITE_LIST = ['/login', '/404']
export function createPermissionGuard(router) { export function createPermissionGuard(router) {

View File

@@ -1,24 +1,30 @@
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router' import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
import { setupRouterGuard } from './guard' import { setupRouterGuard } from './guard'
import { basicRoutes as routes, EMPTY_ROUTE, NOT_FOUND_ROUTE } from './routes' import { basicRoutes, EMPTY_ROUTE, NOT_FOUND_ROUTE } from './routes'
import { getToken } from '@/utils/token' import { getToken, isNullOrWhitespace } from '@/utils'
import { isNullOrWhitespace } from '@/utils/is' import { useUserStore, usePermissionStore } from '@/store'
import { useUserStore } from '@/store/modules/user'
import { usePermissionStore } from '@/store/modules/permission'
const isHash = import.meta.env.VITE_USE_HASH === 'true' const isHash = import.meta.env.VITE_USE_HASH === 'true'
export const router = createRouter({ export const router = createRouter({
history: isHash ? createWebHashHistory('/') : createWebHistory('/'), history: isHash ? createWebHashHistory('/') : createWebHistory('/'),
routes, routes: basicRoutes,
scrollBehavior: () => ({ left: 0, top: 0 }), scrollBehavior: () => ({ left: 0, top: 0 }),
}) })
export async function resetRouter() { export async function setupRouter(app) {
router.getRoutes().forEach((route) => {
const { name } = route
router.hasRoute(name) && router.removeRoute(name)
})
await addDynamicRoutes() await addDynamicRoutes()
setupRouterGuard(router)
app.use(router)
}
export async function resetRouter() {
const basicRouteNames = getRouteNames(basicRoutes)
router.getRoutes().forEach((route) => {
const name = route.name
if (!basicRouteNames.includes(name)) {
router.removeRoute(name)
}
})
} }
export async function addDynamicRoutes() { export async function addDynamicRoutes() {
@@ -46,8 +52,14 @@ export async function addDynamicRoutes() {
} }
} }
export async function setupRouter(app) { export function getRouteNames(routes) {
await addDynamicRoutes() return routes.map((route) => getRouteName(route)).flat(1)
setupRouterGuard(router) }
app.use(router)
function getRouteName(route) {
const names = [route.name]
if (route.children && route.children.length) {
names.push(...route.children.map((item) => getRouteName(item)).flat(1))
}
return names
} }

View File

@@ -3,3 +3,5 @@ import { createPinia } from 'pinia'
export function setupStore(app) { export function setupStore(app) {
app.use(createPinia()) app.use(createPinia())
} }
export * from './modules'

View File

@@ -1,29 +1,28 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { useDark } from '@vueuse/core'
const isDark = useDark()
export const useAppStore = defineStore('app', { export const useAppStore = defineStore('app', {
state() { state() {
return { return {
reloadFlag: true,
collapsed: false, collapsed: false,
isDark,
} }
}, },
actions: { actions: {
async reloadPage() {
$loadingBar.start()
this.reloadFlag = false
await nextTick()
this.reloadFlag = true
setTimeout(() => {
document.documentElement.scrollTo({ left: 0, top: 0 })
$loadingBar.finish()
}, 100)
},
switchCollapsed() { switchCollapsed() {
this.collapsed = !this.collapsed this.collapsed = !this.collapsed
}, },
setCollapsed(collapsed) { setCollapsed(collapsed) {
this.collapsed = collapsed this.collapsed = collapsed
}, },
/** 设置暗黑模式 */
setDark(isDark) {
this.isDark = isDark
},
/** 切换/关闭 暗黑模式 */
toggleDark() {
this.isDark = !this.isDark
},
}, },
}) })

View File

@@ -0,0 +1,4 @@
export * from './app'
export * from './permission'
export * from './tags'
export * from './user'

View File

@@ -53,5 +53,8 @@ export const usePermissionStore = defineStore('permission', {
this.accessRoutes = accessRoutes this.accessRoutes = accessRoutes
return accessRoutes return accessRoutes
}, },
resetPermission() {
this.$reset()
},
}, },
}) })

View File

@@ -1,4 +1,4 @@
import { sStorage } from '@/utils/cache' import { sStorage } from '@/utils'
export const activeTag = sStorage.get('activeTag') export const activeTag = sStorage.get('activeTag')
export const tags = sStorage.get('tags') export const tags = sStorage.get('tags')

View File

@@ -1,15 +1,21 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { activeTag, tags, WITHOUT_TAG_PATHS } from './helpers' import { activeTag, tags, WITHOUT_TAG_PATHS } from './helpers'
import { router } from '@/router' import { router } from '@/router'
import { sStorage } from '@/utils/cache' import { sStorage } from '@/utils'
export const useTagsStore = defineStore('tag', { export const useTagsStore = defineStore('tag', {
state() { state() {
return { return {
tags: tags || [], tags: tags || [],
activeTag: activeTag || '', activeTag: activeTag || '',
reloading: false,
} }
}, },
getters: {
activeIndex() {
return this.tags.findIndex((item) => item.path === this.activeTag)
},
},
actions: { actions: {
setActiveTag(path) { setActiveTag(path) {
this.activeTag = path this.activeTag = path
@@ -20,20 +26,32 @@ export const useTagsStore = defineStore('tag', {
sStorage.set('tags', tags) sStorage.set('tags', tags)
}, },
addTag(tag = {}) { addTag(tag = {}) {
if (WITHOUT_TAG_PATHS.includes(tag.path)) return
let findItem = this.tags.find((item) => item.path === tag.path)
if (findItem) findItem = tag
else this.setTags([...this.tags, tag])
this.setActiveTag(tag.path) this.setActiveTag(tag.path)
if (WITHOUT_TAG_PATHS.includes(tag.path) || this.tags.some((item) => item.path === tag.path)) return },
this.setTags([...this.tags, tag]) async reloadTag(path, keepAlive) {
const findItem = this.tags.find((item) => item.path === path)
// 更新key可让keepAlive失效
if (findItem && keepAlive) findItem.keepAlive = false
$loadingBar.start()
this.reloading = true
await nextTick()
this.reloading = false
findItem.keepAlive = keepAlive
setTimeout(() => {
document.documentElement.scrollTo({ left: 0, top: 0 })
$loadingBar.finish()
}, 100)
}, },
removeTag(path) { removeTag(path) {
if (path === this.activeTag) {
const activeIndex = this.tags.findIndex((item) => item.path === path)
if (activeIndex > 0) {
router.push(this.tags[activeIndex - 1].path)
} else {
router.push(this.tags[activeIndex + 1].path)
}
}
this.setTags(this.tags.filter((tag) => tag.path !== path)) this.setTags(this.tags.filter((tag) => tag.path !== path))
if (path === this.activeTag) {
router.push(this.tags[this.tags.length - 1].path)
}
}, },
removeOther(curPath = this.activeTag) { removeOther(curPath = this.activeTag) {
this.setTags(this.tags.filter((tag) => tag.path === curPath)) this.setTags(this.tags.filter((tag) => tag.path === curPath))
@@ -57,5 +75,9 @@ export const useTagsStore = defineStore('tag', {
router.push(filterTags[filterTags.length - 1].path) router.push(filterTags[filterTags.length - 1].path)
} }
}, },
resetTags() {
this.setTags([])
this.setActiveTag('')
},
}, },
}) })

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { removeToken } from '@/utils/token' import { resetRouter } from '@/router'
import { toLogin } from '@/utils/auth' import { useTagsStore, usePermissionStore } from '@/store'
import { removeToken, toLogin } from '@/utils'
import api from '@/api' import api from '@/api'
export const useUserStore = defineStore('user', { export const useUserStore = defineStore('user', {
@@ -35,8 +36,13 @@ export const useUserStore = defineStore('user', {
} }
}, },
async logout() { async logout() {
const { resetTags } = useTagsStore()
const { resetPermission } = usePermissionStore()
removeToken() removeToken()
this.userInfo = {} resetTags()
resetPermission()
resetRouter()
this.$reset()
toLogin() toLogin()
}, },
setUserInfo(userInfo = {}) { setUserInfo(userInfo = {}) {

View File

@@ -1,14 +1,8 @@
html {
font-size: 4px; // * 1rem = 4px 方便unocss计算在unocss中 1字体单位 = 0.25rem相当于 1等份 = 1px
}
html, html,
body { body {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
background-color: #f2f2f2;
font-family: 'Encode Sans Condensed', sans-serif;
} }
#app { #app {
@@ -16,7 +10,7 @@ body {
height: 100%; height: 100%;
} }
/* router view transition fade-slide */ /* transition fade-slide */
.fade-slide-leave-active, .fade-slide-leave-active,
.fade-slide-enter-active { .fade-slide-enter-active {
transition: all 0.3s; transition: all 0.3s;
@@ -32,7 +26,6 @@ body {
transform: translateX(30px); transform: translateX(30px);
} }
/* 自定义滚动条样式 */
/* 自定义滚动条样式 */ /* 自定义滚动条样式 */
.cus-scroll { .cus-scroll {
overflow: auto; overflow: auto;

View File

@@ -12,7 +12,7 @@ html {
a { a {
text-decoration: none; text-decoration: none;
color: #333; color: inherit;
} }
a:hover, a:hover,
@@ -33,8 +33,3 @@ textarea {
border: none; border: none;
resize: none; resize: none;
} }
body {
font-size: 14px;
font-weight: 400;
}

View File

@@ -1,14 +0,0 @@
import { router } from '@/router'
export function toLogin() {
router.replace({
path: '/login',
query: { ...router.currentRoute.value.query, redirect: router.currentRoute.value.path },
})
}
export function toFourZeroFour() {
router.replace({
path: '/404',
})
}

17
src/utils/auth/auth.js Normal file
View File

@@ -0,0 +1,17 @@
import { router } from '@/router'
export function toLogin() {
const currentRoute = unref(router.currentRoute)
const needRedirect =
!currentRoute.meta.requireAuth && !['/404', '/login'].includes(router.currentRoute.value.path)
router.replace({
path: '/login',
query: needRedirect ? { ...currentRoute.query, redirect: currentRoute.path } : {},
})
}
export function toFourZeroFour() {
router.replace({
path: '/404',
})
}

2
src/utils/auth/index.js Normal file
View File

@@ -0,0 +1,2 @@
export * from './auth'
export * from './token'

View File

@@ -1,4 +1,4 @@
import { lStorage } from './cache' import { lStorage } from '@/utils'
import api from '@/api' import api from '@/api'
const TOKEN_CODE = 'access_token' const TOKEN_CODE = 'access_token'
@@ -23,9 +23,11 @@ export async function refreshAccessToken() {
} }
const { time } = tokenItem const { time } = tokenItem
// token生成或者刷新后30分钟内不执行刷新 // token生成或者刷新后30分钟内不执行刷新
if (new Date().getTime() - time <= 1000 * 60 * 30) return if (Date.now() - time <= 1000 * 60 * 30) return
try { try {
const res = await api.refreshToken() const res = await api.refreshToken()
setToken(res.data.token) setToken(res.data.token)
} catch (error) {} } catch (error) {
console.error(error)
}
} }

View File

@@ -0,0 +1,90 @@
import dayjs from 'dayjs'
/**
* @desc 格式化时间
* @param {(Object|string|number)} time
* @param {string} format
* @returns {string | null}
*/
export function formatDateTime(time = undefined, format = 'YYYY-MM-DD HH:mm:ss') {
return dayjs(time).format(format)
}
export function formatDate(date = undefined, format = 'YYYY-MM-DD') {
return formatDateTime(date, format)
}
/**
* @desc 函数节流
* @param {Function} fn
* @param {Number} wait
* @returns {Function}
*/
export function throttle(fn, wait) {
var context, args
var previous = 0
return function () {
var now = +new Date()
context = this
args = arguments
if (now - previous > wait) {
fn.apply(context, args)
previous = now
}
}
}
/**
* @desc 函数防抖
* @param {Function} func
* @param {number} wait
* @param {boolean} immediate
* @return {*}
*/
export function debounce(method, wait, immediate) {
let timeout
return function (...args) {
let context = this
if (timeout) {
clearTimeout(timeout)
}
// 立即执行需要两个条件一是immediate为true二是timeout未被赋值或被置为null
if (immediate) {
/**
* 如果定时器不存在则立即执行并设置一个定时器wait毫秒后将定时器置为null
* 这样确保立即执行后wait毫秒内不会被再次触发
*/
let callNow = !timeout
timeout = setTimeout(() => {
timeout = null
}, wait)
if (callNow) {
method.apply(context, args)
}
} else {
// 如果immediate为false则函数wait毫秒后执行
timeout = setTimeout(() => {
/**
* args是一个类数组对象所以使用fn.apply
* 也可写作method.call(context, ...args)
*/
method.apply(context, args)
}, wait)
}
}
}
/**
*
* @param {HTMLElement} el
* @param {Function} cb
* @return {ResizeObserver}
*/
export function useResize(el, cb) {
const observer = new ResizeObserver((entries) => {
cb(entries[0].contentRect)
})
observer.observe(el)
return observer
}

View File

@@ -0,0 +1,4 @@
export * from './common'
export * from './is'
export * from './icon'
export * from './naiveTools'

View File

@@ -102,7 +102,7 @@ export function ifNull(val, def = '') {
export function isUrl(path) { export function isUrl(path) {
const reg = const reg =
/(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/ /(((^https?:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)$/
return reg.test(path) return reg.test(path)
} }

View File

@@ -1,4 +1,7 @@
import { isNullOrUndef } from '@/utils/is' import * as NaiveUI from 'naive-ui'
import { isNullOrUndef } from '@/utils'
import { naiveThemeOverrides as themeOverrides } from '~/settings'
import { useAppStore } from '@/store/modules/app'
export function setupMessage(NMessage) { export function setupMessage(NMessage) {
let loadingMessage = null let loadingMessage = null
@@ -77,3 +80,20 @@ export function setupDialog(NDialog) {
return NDialog return NDialog
} }
export function setupNaiveDiscreteApi() {
const appStore = useAppStore()
const configProviderProps = computed(() => ({
theme: appStore.isDark ? NaiveUI.darkTheme : undefined,
themeOverrides,
}))
const { message, dialog, notification, loadingBar } = NaiveUI.createDiscreteApi(
['message', 'dialog', 'notification', 'loadingBar'],
{ configProviderProps }
)
window.$loadingBar = loadingBar
window.$notification = notification
window.$message = setupMessage(message)
window.$dialog = setupDialog(dialog)
}

View File

@@ -1,7 +1,4 @@
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store'
import { isNullOrUndef } from '@/utils/is'
import { removeToken } from '@/utils/token'
import { toLogin } from '@/utils/auth'
export function addBaseParams(params) { export function addBaseParams(params) {
if (!params.userId) { if (!params.userId) {
@@ -9,21 +6,14 @@ export function addBaseParams(params) {
} }
} }
export function resolveResError(error = {}) { export function resolveResError(code, message) {
let { code, message } = error
if (isNullOrUndef(code)) {
// 未知错误
code = -1
message = message ?? '接口未知异常!'
} else {
switch (code) { switch (code) {
case 400: case 400:
message = message ?? '请求参数错误' message = message ?? '请求参数错误'
break break
case 401: case 401:
message = message ?? '登录已过期' message = message ?? '登录已过期'
removeToken() useUserStore().logout()
toLogin()
break break
case 403: case 403:
message = message ?? '没有权限' message = message ?? '没有权限'
@@ -35,9 +25,8 @@ export function resolveResError(error = {}) {
message = message ?? '服务器异常' message = message ?? '服务器异常'
break break
default: default:
message = message ?? '操作异常!' message = message ?? `${code}】: 未知异常!`
break break
} }
} return message
return { code, message }
} }

View File

@@ -1,5 +1,5 @@
import axios from 'axios' import axios from 'axios'
import { repReject, repResolve, reqReject, reqResolve } from './interceptors' import { resReject, resResolve, reqReject, reqResolve } from './interceptors'
export function createAxios(options = {}) { export function createAxios(options = {}) {
const defaultOptions = { const defaultOptions = {
@@ -10,10 +10,10 @@ export function createAxios(options = {}) {
...options, ...options,
}) })
service.interceptors.request.use(reqResolve, reqReject) service.interceptors.request.use(reqResolve, reqReject)
service.interceptors.response.use(repResolve, repReject) service.interceptors.response.use(resResolve, resReject)
return service return service
} }
export default createAxios({ export const request = createAxios({
baseURL: import.meta.env.VITE_BASE_API, baseURL: import.meta.env.VITE_BASE_API,
}) })

View File

@@ -1,5 +1,4 @@
import { getToken } from '@/utils/token' import { getToken } from '@/utils'
import { toLogin } from '@/utils/auth'
import { resolveResError } from './helpers' import { resolveResError } from './helpers'
export function reqResolve(config) { export function reqResolve(config) {
@@ -10,9 +9,7 @@ export function reqResolve(config) {
const token = getToken() const token = getToken()
if (!token) { if (!token) {
// * 未登录或者token过期的情况下,跳转登录页重新登录 return Promise.reject({ code: 401, message: '登录已过期,请重新登录!' })
toLogin()
return Promise.reject({ code: '-1', message: '未登录' })
} }
/** /**
@@ -28,19 +25,34 @@ export function reqReject(error) {
return Promise.reject(error) return Promise.reject(error)
} }
export function repResolve(response) { export function resResolve(response) {
const { noNeedTip } = response.config // TODO: 处理不同的 response.headers
if (response.data?.code !== 0) { const { data, status, config, statusText } = response
const { code, message } = resolveResError(response?.data) if (data?.code !== 0) {
!noNeedTip && $message.error(message) const code = data?.code ?? status
return Promise.reject({ code, message, error: response?.data })
/** 根据code处理对应的操作并返回处理后的message */
const message = resolveResError(code, data?.message ?? statusText)
/** 需要错误提醒 */
!config.noNeedTip && window.$message?.error(message)
return Promise.reject({ code, message, error: data || response })
} }
return Promise.resolve(response?.data) return Promise.resolve(data)
} }
export function repReject(error) { export function resReject(error) {
const { noNeedTip } = error.response?.config || error.config if (!error || !error.response) {
const { code, message } = resolveResError(error.response?.data) const code = error?.code
!noNeedTip && $message.error(message) /** 根据code处理对应的操作并返回处理后的message */
const message = resolveResError(code, error.message)
window.$message?.error(message)
return Promise.reject({ code, message, error }) return Promise.reject({ code, message, error })
} }
const { data, status, config } = error.response
const code = data?.code ?? status
const message = resolveResError(code, data?.message ?? error.message)
/** 需要错误提醒 */
!config?.noNeedTip && window.$message?.error(message)
return Promise.reject({ code, message, error: error.response?.data || error.response })
}

View File

@@ -1,76 +1,4 @@
import dayjs from 'dayjs' export * from './common'
export * from './storage'
/** export * from './http'
* @desc 格式化时间 export * from './auth'
* @param {(Object|string|number)} time
* @param {string} format
* @returns {string | null}
*/
export function formatDateTime(time = undefined, format = 'YYYY-MM-DD HH:mm:ss') {
return dayjs(time).format(format)
}
export function formatDate(date = undefined, format = 'YYYY-MM-DD') {
return formatDateTime(date, format)
}
/**
* @desc 函数节流
* @param {Function} fn
* @param {Number} wait
* @returns {Function}
*/
export function throttle(fn, wait) {
var context, args
var previous = 0
return function () {
var now = +new Date()
context = this
args = arguments
if (now - previous > wait) {
fn.apply(context, args)
previous = now
}
}
}
/**
* @desc 函数防抖
* @param {Function} func
* @param {number} wait
* @param {boolean} immediate
* @return {*}
*/
export function debounce(method, wait, immediate) {
let timeout
return function (...args) {
let context = this
if (timeout) {
clearTimeout(timeout)
}
// 立即执行需要两个条件一是immediate为true二是timeout未被赋值或被置为null
if (immediate) {
/**
* 如果定时器不存在则立即执行并设置一个定时器wait毫秒后将定时器置为null
* 这样确保立即执行后wait毫秒内不会被再次触发
*/
let callNow = !timeout
timeout = setTimeout(() => {
timeout = null
}, wait)
if (callNow) {
method.apply(context, args)
}
} else {
// 如果immediate为false则函数wait毫秒后执行
timeout = setTimeout(() => {
/**
* args是一个类数组对象所以使用fn.apply
* 也可写作method.call(context, ...args)
*/
method.apply(context, args)
}, wait)
}
}
}

View File

@@ -1,4 +1,4 @@
import { isNullOrUndef } from '@/utils/is' import { isNullOrUndef } from '@/utils'
class Storage { class Storage {
constructor(option) { constructor(option) {
@@ -14,7 +14,7 @@ class Storage {
const stringData = JSON.stringify({ const stringData = JSON.stringify({
value, value,
time: Date.now(), time: Date.now(),
expire: !isNullOrUndef(expire) ? new Date().getTime() + expire * 1000 : null, expire: !isNullOrUndef(expire) ? Date.now() + expire * 1000 : null,
}) })
this.storage.setItem(this.getKey(key), stringData) this.storage.setItem(this.getKey(key), stringData)
} }
@@ -30,7 +30,7 @@ class Storage {
try { try {
const data = JSON.parse(val) const data = JSON.parse(val)
const { value, time, expire } = data const { value, time, expire } = data
if (isNullOrUndef(expire) || expire > new Date().getTime()) { if (isNullOrUndef(expire) || expire > Date.now()) {
return { value, time } return { value, time }
} }
this.remove(key) this.remove(key)

View File

@@ -15,14 +15,22 @@
<n-card title="带 Icon 的按钮"> <n-card title="带 Icon 的按钮">
<n-space> <n-space>
<n-button type="info"> <TheIcon icon="material-symbols:add" :size="18" class="mr-5" /> 新增 </n-button> <n-button type="info">
<TheIcon icon="material-symbols:add" :size="18" class="mr-5" />
新增
</n-button>
<n-button type="error"> <n-button type="error">
<TheIcon icon="material-symbols:delete-outline" :size="18" class="mr-5" /> 删除 <TheIcon icon="material-symbols:delete-outline" :size="18" class="mr-5" />
删除
</n-button> </n-button>
<n-button type="warning"> <n-button type="warning">
<TheIcon icon="material-symbols:edit-outline" :size="18" class="mr-5" /> 编辑 <TheIcon icon="material-symbols:edit-outline" :size="18" class="mr-5" />
编辑
</n-button>
<n-button type="primary">
<TheIcon icon="majesticons:eye-line" :size="18" class="mr-5" />
查看
</n-button> </n-button>
<n-button type="primary"> <TheIcon icon="majesticons:eye-line" :size="18" class="mr-5" /> 查看 </n-button>
</n-space> </n-space>
</n-card> </n-card>
</n-space> </n-space>
@@ -38,7 +46,10 @@
</n-card> </n-card>
<n-card min-w-340 title="确认弹窗 Dialog"> <n-card min-w-340 title="确认弹窗 Dialog">
<n-button type="error" @click="handleDelete"> <icon-mi:delete mr-5 />删除</n-button> <n-button type="error" @click="handleDelete">
<icon-mi:delete mr-5 />
删除
</n-button>
</n-card> </n-card>
<n-card min-w-340 title="消息提醒 Message"> <n-card min-w-340 title="消息提醒 Message">

View File

@@ -3,7 +3,7 @@
<div w-350> <div w-350>
<n-input v-model:value="inputVal" /> <n-input v-model:value="inputVal" />
<n-input-number v-model:value="number" mt-30 /> <n-input-number v-model:value="number" mt-30 />
<p mt-20 text-center color-gray text-14>右击标签重新加载可重置keep-alive</p> <p mt-20 text-center text-14 color-gray>右击标签重新加载可重置keep-alive</p>
</div> </div>
</CommonPage> </CommonPage>
</template> </template>

View File

@@ -13,7 +13,7 @@ export default {
children: [ children: [
{ {
name: 'BaseComponents', name: 'BaseComponents',
path: 'idnex', path: 'index',
component: () => import('./index.vue'), component: () => import('./index.vue'),
meta: { meta: {
title: '基础组件', title: '基础组件',

View File

@@ -1,7 +1,10 @@
<template> <template>
<CommonPage show-footer> <CommonPage show-footer>
<p> <p>
文档<a hover-decoration-underline c-blue href="https://uno.antfu.me/" target="_blank">https://uno.antfu.me/</a> 文档
<a c-blue hover-decoration-underline href="https://uno.antfu.me/" target="_blank">
https://uno.antfu.me/
</a>
</p> </p>
<p> <p>
playground playground
@@ -10,57 +13,57 @@
</a> </a>
</p> </p>
<div f-c-c mt-20 w-350 rounded-10 b-1 bc-ccc> <div mt-20 w-350 f-c-c flex-col>
<div flex w-360 flex-wrap justify-around p-10> <div flex flex-wrap justify-around rounded-10 p-10 border="1 solid #ccc">
<div w-50 h-50 b-1 rounded-5 f-c-c p-10 m-20> <div m-20 h-50 w-50 f-c-c rounded-5 p-10 border="1 solid">
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div> </div>
<div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20> <div m-20 h-50 w-50 flex justify-between rounded-5 p-10 border="1 solid">
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black self-end></span> <span h-6 w-6 self-end rounded-3 bg-black dark:bg-white />
</div> </div>
<div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20> <div m-20 h-50 w-50 flex justify-between rounded-5 p-10 border="1 solid">
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black self-center></span> <span h-6 w-6 self-center rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black self-end></span> <span h-6 w-6 self-end rounded-3 bg-black dark:bg-white />
</div> </div>
<div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20> <div m-20 h-50 w-50 flex justify-between rounded-5 p-10 border="1 solid">
<div flex-col justify-between> <div flex-col justify-between>
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div> </div>
<div flex-col justify-between> <div flex-col justify-between>
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div> </div>
</div> </div>
<div w-50 h-50 b-1 rounded-5 f-c-c flex-col p-10 m-20> <div m-20 h-50 w-50 flex-col items-center justify-between rounded-5 p-10 border="1 solid">
<div flex w-full justify-between> <div w-full flex justify-between>
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div> </div>
<span w-6 h-6 rounded-3 bg-black></span> <div h-6 w-6 rounded-3 bg-black dark:bg-white />
<div flex w-full justify-between> <div w-full flex justify-between>
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div> </div>
</div> </div>
<div w-50 h-50 b-1 rounded-5 flex-col justify-between p-10 m-20> <div m-20 h-50 w-50 flex-col justify-between rounded-5 p-10 border="1 solid">
<div flex w-full justify-between> <div w-full flex justify-between>
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div> </div>
<div flex w-full justify-between> <div w-full flex justify-between>
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div> </div>
<div flex w-full justify-between> <div w-full flex justify-between>
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div> </div>
</div> </div>
</div> </div>
<h2 mt-10 text-14 font-normal color-gray>Flex 骰子</h2>
</div> </div>
<h2 font-normal text-14 text-center w-350 mt-10 color-gray>Flex 骰子</h2>
</CommonPage> </CommonPage>
</template> </template>

View File

@@ -1,31 +1,40 @@
<template> <template>
<CommonPage :show-header="false" title="32323"> <CommonPage>
<div h-60 pl-20 pr-20 flex items-center bg-white> <div h-60 flex items-center bg-white pl-20 pr-20 dark:bg-dark>
<input <input
v-model="post.title" v-model="post.title"
class="flex-1 pt-15 pb-15 mr-20 text-20 font-bold color-primary" class="mr-20 flex-1 pb-15 pt-15 text-20 font-bold color-primary"
dark:bg-dark
type="text" type="text"
placeholder="输入文章标题..." placeholder="输入文章标题..."
/> />
<n-button type="primary" style="width: 80px" :loading="btnLoading" @click="handleSavePost"> <n-button type="primary" style="width: 80px" :loading="btnLoading" @click="handleSavePost">
<TheIcon v-if="!btnLoading" icon="line-md:confirm-circle" class="mr-5" :size="18" /> 保存 <TheIcon v-if="!btnLoading" icon="line-md:confirm-circle" class="mr-5" :size="18" />
保存
</n-button> </n-button>
</div> </div>
<MdEditor v-model="post.content" style="height: calc(100vh - 250px)" /> <MdEditor
v-model="post.content"
:theme="appStore.isDark ? 'dark' : 'light'"
style="height: calc(100vh - 305px)"
/>
</CommonPage> </CommonPage>
</template> </template>
<script setup> <script setup>
import MdEditor from 'md-editor-v3' import { MdEditor } from 'md-editor-v3'
import 'md-editor-v3/lib/style.css' import 'md-editor-v3/lib/style.css'
import { useAppStore } from '@/store'
defineOptions({ name: 'MDEditor' }) defineOptions({ name: 'MDEditor' })
const appStore = useAppStore()
// refs // refs
let post = ref({}) let post = ref({})
let btnLoading = ref(false) let btnLoading = ref(false)
function handleSavePost(e) { function handleSavePost() {
btnLoading.value = true btnLoading.value = true
$message.loading('正在保存...') $message.loading('正在保存...')
setTimeout(() => { setTimeout(() => {

View File

@@ -0,0 +1,46 @@
<template>
<AppPage>
<div class="h-full flex-col" border="1 solid #ccc" dark:bg-dark>
<WangToolbar
border-b="1px solid #ccc"
:editor="editorRef"
:default-config="toolbarConfig"
mode="default"
/>
<WangEditor
v-model="valueHtml"
style="flex: 1; overflow-y: hidden"
:default-config="editorConfig"
mode="default"
@on-created="handleCreated"
/>
</div>
</AppPage>
</template>
<script setup>
import '@wangeditor/editor/dist/css/style.css'
import { Editor as WangEditor, Toolbar as WangToolbar } from '@wangeditor/editor-for-vue'
defineOptions({ name: 'RichTextEditor' })
const editorRef = shallowRef()
const toolbarConfig = { excludeKeys: 'fullScreen' }
const editorConfig = { placeholder: '请输入内容...', MENU_CONF: {} }
const valueHtml = ref('')
const handleCreated = (editor) => {
editorRef.value = editor
}
</script>
<style>
html.dark {
--w-e-textarea-bg-color: #333;
--w-e-textarea-color: #fff;
--w-e-toolbar-bg-color: #333;
--w-e-toolbar-color: #fff;
--w-e-toolbar-active-bg-color: #666;
--w-e-toolbar-active-color: #fff;
/* ...其他... */
}
</style>

View File

@@ -4,18 +4,18 @@ export default {
name: 'Demo', name: 'Demo',
path: '/demo', path: '/demo',
component: Layout, component: Layout,
redirect: '/demo/crud-table', redirect: '/demo/crud',
meta: { meta: {
title: '示例页面', title: '示例页面',
customIcon: 'logo', icon: 'uil:pagelines',
role: ['admin'], role: ['admin'],
requireAuth: true, requireAuth: true,
order: 3, order: 3,
}, },
children: [ children: [
{ {
name: 'CrudTable', name: 'Crud',
path: 'crud-table', path: 'crud',
component: () => import('./table/index.vue'), component: () => import('./table/index.vue'),
meta: { meta: {
title: 'CRUD表格', title: 'CRUD表格',
@@ -37,5 +37,29 @@ export default {
keepAlive: true, keepAlive: true,
}, },
}, },
{
name: 'RichTextEditor',
path: 'rich-text',
component: () => import('./editor/rich-text.vue'),
meta: {
title: '富文本编辑器',
icon: 'ic:sharp-text-rotation-none',
role: ['admin'],
requireAuth: true,
keepAlive: true,
},
},
{
name: 'Upload',
path: 'upload',
component: () => import('./upload/index.vue'),
meta: {
title: '图片上传',
icon: 'mdi:upload',
role: ['admin'],
requireAuth: true,
keepAlive: true,
},
},
], ],
} }

View File

@@ -1,4 +1,4 @@
import request from '@/utils/http' import { request } from '@/utils'
export default { export default {
getPosts: (params = {}) => request.get('posts', { params }), getPosts: (params = {}) => request.get('posts', { params }),

View File

@@ -1,9 +1,16 @@
<template> <template>
<CommonPage show-footer title="文章"> <CommonPage show-footer title="文章">
<template #action> <template #action>
<n-button type="primary" @click="handleAdd"> <div>
<TheIcon icon="material-symbols:add" :size="18" class="mr-5" /> 新建文章 <n-button type="primary" secondary @click="$table?.handleExport()">
<TheIcon icon="mdi:download" :size="18" class="mr-5" />
导出
</n-button> </n-button>
<n-button type="primary" class="ml-16" @click="handleAdd">
<TheIcon icon="material-symbols:add" :size="18" class="mr-5" />
新建文章
</n-button>
</div>
</template> </template>
<CrudTable <CrudTable
@@ -14,6 +21,7 @@
:columns="columns" :columns="columns"
:get-data="api.getPosts" :get-data="api.getPosts"
@on-checked="onChecked" @on-checked="onChecked"
@on-data-change="(data) => (tableData = data)"
> >
<template #queryBar> <template #queryBar>
<QueryBarItem label="标题" :label-width="50"> <QueryBarItem label="标题" :label-width="50">
@@ -21,7 +29,7 @@
v-model:value="queryItems.title" v-model:value="queryItems.title"
type="text" type="text"
placeholder="请输入标题" placeholder="请输入标题"
@keydown.enter="$table?.handleSearch" @keypress.enter="$table?.handleSearch"
/> />
</QueryBarItem> </QueryBarItem>
</template> </template>
@@ -82,21 +90,21 @@
<script setup> <script setup>
import { NButton, NSwitch } from 'naive-ui' import { NButton, NSwitch } from 'naive-ui'
import { formatDateTime } from '@/utils' import { formatDateTime, renderIcon, isNullOrUndef } from '@/utils'
import { renderIcon } from '@/utils/icon'
import { useCRUD } from '@/composables' import { useCRUD } from '@/composables'
import api from './api' import api from './api'
import { isNullOrUndef } from '@/utils/is'
defineOptions({ name: 'CrudTable' }) defineOptions({ name: 'Crud' })
const $table = ref(null) const $table = ref(null)
/** 表格数据,触发搜索的时候会更新这个值 */
const tableData = ref([])
/** QueryBar筛选参数可选 */ /** QueryBar筛选参数可选 */
const queryItems = ref({}) const queryItems = ref({})
/** 补充参数(可选) */ /** 补充参数(可选) */
const extraParams = ref({}) const extraParams = ref({})
onMounted(() => { onActivated(() => {
$table.value?.handleSearch() $table.value?.handleSearch()
}) })
@@ -143,6 +151,7 @@ const columns = [
width: 240, width: 240,
align: 'center', align: 'center',
fixed: 'right', fixed: 'right',
hideInExcel: true,
render(row) { render(row) {
return [ return [
h( h(
@@ -174,7 +183,10 @@ const columns = [
style: 'margin-left: 15px;', style: 'margin-left: 15px;',
onClick: () => handleDelete(row.id), onClick: () => handleDelete(row.id),
}, },
{ default: () => '删除', icon: renderIcon('material-symbols:delete-outline', { size: 14 }) } {
default: () => '删除',
icon: renderIcon('material-symbols:delete-outline', { size: 14 }),
}
), ),
] ]
}, },

View File

@@ -0,0 +1,84 @@
<template>
<CommonPage>
<n-upload
class="mx-auto w-[75%] p-20 text-center"
:custom-request="handleUpload"
:show-file-list="false"
accept=".png,.jpg,.jpeg"
@before-upload="onBeforeUpload"
>
<n-upload-dragger>
<div class="h-150 f-c-c flex-col">
<TheIcon icon="mdi:upload" :size="68" class="mb-12 c-primary" />
<n-text class="text-14 c-gray">点击或者拖动文件到该区域来上传</n-text>
</div>
</n-upload-dragger>
</n-upload>
<n-card v-if="imgList && imgList.length" class="mt-16 items-center">
<n-image-group>
<n-space justify="space-between" align="center">
<n-card v-for="(item, index) in imgList" :key="index" class="w-280 hover:card-shadow">
<div class="h-160 f-c-c">
<n-image width="200" :src="item.url" />
</div>
<n-space class="mt-16" justify="space-evenly">
<n-button dashed type="primary" @click="copy(item.url)">url</n-button>
<n-button dashed type="primary" @click="copy(`![${item.fileName}](${item.url})`)">
MD
</n-button>
<n-button
dashed
type="primary"
@click="copy(`&lt;img src=&quot;${item.url}&quot; /&gt;`)"
>
img
</n-button>
</n-space>
</n-card>
<div v-for="i in 4" :key="i" class="w-280" />
</n-space>
</n-image-group>
</n-card>
</CommonPage>
</template>
<script setup>
import { useClipboard } from '@vueuse/core'
defineOptions({ name: 'Upload' })
const { copy, copied } = useClipboard()
const imgList = reactive([
{ url: 'https://cdn.qszone.com/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
{ url: 'https://cdn.qszone.com/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
{ url: 'https://cdn.qszone.com/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
{ url: 'https://cdn.qszone.com/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
])
watch(copied, (val) => {
val && $message.success('已复制到剪切板')
})
function onBeforeUpload({ file }) {
if (!file.file?.type.startsWith('image/')) {
$message.error('只能上传图片')
return false
}
return true
}
async function handleUpload({ file, onFinish }) {
if (!file || !file.type) {
$message.error('请选择文件')
}
// 模拟上传
$message.loading('上传中...')
setTimeout(() => {
$message.success('上传成功')
imgList.push({ fileName: file.name, url: URL.createObjectURL(file.file) })
onFinish()
}, 1500)
}
</script>

View File

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

View File

@@ -2,41 +2,65 @@
<AppPage :show-footer="true" bg-cover :style="{ backgroundImage: `url(${bgImg})` }"> <AppPage :show-footer="true" bg-cover :style="{ backgroundImage: `url(${bgImg})` }">
<div <div
style="transform: translateY(25px)" style="transform: translateY(25px)"
class="m-auto p-15 f-c-c min-w-345 max-w-700 rounded-10 card-shadow bg-white bg-opacity-60" class="m-auto max-w-700 min-w-345 f-c-c rounded-10 bg-white bg-opacity-60 p-15 card-shadow"
dark:bg-dark
> >
<div w-380 hidden md:block px-20 py-35> <div hidden w-380 px-20 py-35 md:block>
<img src="@/assets/images/login_banner.webp" w-full alt="login_banner" /> <img src="@/assets/images/login_banner.webp" w-full alt="login_banner" />
</div> </div>
<div w-320 flex-col px-20 py-35> <div w-320 flex-col px-20 py-35>
<h5 f-c-c text-24 font-normal color="#6a6a6a"><icon-custom-logo mr-10 text-50 color-primary />{{ title }}</h5> <h5 f-c-c text-24 font-normal color="#6a6a6a">
<div mt-30> <img src="@/assets/images/logo.png" height="50" class="mr-10" />
{{ title }}
</h5>
<div mt-32>
<n-input <n-input
v-model:value="loginInfo.name" v-model:value="loginInfo.name"
autofocus autofocus
class="text-16 items-center h-50 pl-10" class="h-48 items-center text-16"
placeholder="admin" placeholder="name"
:maxlength="20" :maxlength="20"
/> >
<template #prefix>
<icon-material-symbols:account-circle-outline class="mr-8 text-20 opacity-40" />
</template>
</n-input>
</div> </div>
<div mt-30> <div mt-32>
<n-input <n-input
v-model:value="loginInfo.password" v-model:value="loginInfo.password"
class="text-16 items-center h-50 pl-10" class="h-48 items-center text-16"
type="password" type="password"
show-password-on="mousedown" show-password-on="mousedown"
placeholder="123456" placeholder="password"
:maxlength="20" :maxlength="20"
@keydown.enter="handleLogin" @keydown.enter="handleLogin"
>
<template #prefix>
<icon-ri:lock-password-line class="mr-8 text-20 opacity-40" />
</template>
</n-input>
</div>
<div mt-20>
<n-checkbox
:checked="isRemember"
label="记住我"
:on-update:checked="(val) => (isRemember = val)"
/> />
</div> </div>
<div mt-20> <div mt-20>
<n-checkbox :checked="isRemember" label="记住我" :on-update:checked="(val) => (isRemember = val)" /> <n-button
</div> h-50
w-full
<div mt-20> rounded-5
<n-button w-full h-50 rounded-5 text-16 type="primary" :loading="loading" @click="handleLogin"> text-16
type="primary"
:loading="loading"
@click="handleLogin"
>
登录 登录
</n-button> </n-button>
</div> </div>
@@ -46,8 +70,7 @@
</template> </template>
<script setup> <script setup>
import { lStorage } from '@/utils/cache' import { lStorage, setToken } from '@/utils'
import { setToken } from '@/utils/token'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import bgImg from '@/assets/images/login_bg.webp' import bgImg from '@/assets/images/login_bg.webp'
import api from './api' import api from './api'

View File

@@ -0,0 +1,3 @@
<template>
<div>a-1-1</div>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<div>a-1-2</div>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<CommonPage>
<div>a-1</div>
<div pl-20>
<RouterView />
</div>
</CommonPage>
</template>

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