mirror of
https://github.com/zclzone/vue-naive-admin.git
synced 2025-12-28 12:10:20 +08:00
Compare commits
131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
921e0d18e9 | ||
|
|
3e259877c6 | ||
|
|
a431a2cf85 | ||
|
|
c7c8691164 | ||
|
|
1d246d1cd6 | ||
|
|
1854d2cec4 | ||
|
|
478d8fccd1 | ||
|
|
0accfe3a4a | ||
|
|
4bd50e2f9e | ||
|
|
72857b3862 | ||
|
|
a18ffce56c | ||
|
|
a2222c4dc0 | ||
|
|
a357ac36ee | ||
|
|
5e1acd7b8e | ||
|
|
8bbd04e668 | ||
|
|
0c5c3df645 | ||
|
|
ed577cd41a | ||
|
|
456ad78dd2 | ||
|
|
ba20e7a96b | ||
|
|
3ed1aafa80 | ||
|
|
5cf1212847 | ||
|
|
81e0bb7b78 | ||
|
|
f3125ddec8 | ||
|
|
3b3cb7ba34 | ||
|
|
d7c1063102 | ||
|
|
a5aa8a353f | ||
|
|
cbb6ca4f6b | ||
|
|
2ece015dae | ||
|
|
a80f83a011 | ||
|
|
65d4d3848d | ||
|
|
53830256f4 | ||
|
|
22c59c208f | ||
|
|
b43f87035b | ||
|
|
20eee94630 | ||
|
|
c504ad065c | ||
|
|
0e764ac748 | ||
|
|
8f15e1c655 | ||
|
|
d702a6703b | ||
|
|
d49af8b574 | ||
|
|
a036602d3e | ||
|
|
eb173dc38e | ||
|
|
e5f1ee25c3 | ||
|
|
fe11e18197 | ||
|
|
0f9fb9f1c9 | ||
|
|
50f96b99c7 | ||
|
|
2eb936bcac | ||
|
|
b50731881c | ||
|
|
d4a5cffd81 | ||
|
|
70eab22f65 | ||
|
|
0247f3ebfa | ||
|
|
a610c1c6d0 | ||
|
|
5a9c0fb584 | ||
|
|
cad72b3b73 | ||
|
|
9edd0e5ad6 | ||
|
|
ae6db3ed3c | ||
|
|
dcab55055c | ||
|
|
5aa4d3d5ae | ||
|
|
eea9fc79f7 | ||
|
|
526792e22f | ||
|
|
855202962c | ||
|
|
74a58fafb9 | ||
|
|
ad451dae1e | ||
|
|
35ed004b2e | ||
|
|
fcdd31c935 | ||
|
|
386d9ec27a | ||
|
|
e84dd01365 | ||
|
|
6da56ec881 | ||
|
|
4e9e3469b0 | ||
|
|
3b86597eff | ||
|
|
ebffe52c7c | ||
|
|
6d863e1a63 | ||
|
|
b2e1c2d22c | ||
|
|
2e283c3b19 | ||
|
|
6300758fd0 | ||
|
|
b5717f6b8d | ||
|
|
ff11cdf73f | ||
|
|
9a01e42915 | ||
|
|
7c74578afc | ||
|
|
a6a8002c59 | ||
|
|
daf747348e | ||
|
|
60b5ce1817 | ||
|
|
4c75be67f2 | ||
|
|
be1c875a72 | ||
|
|
329a6e29cb | ||
|
|
af907932fb | ||
|
|
681b3144d1 | ||
|
|
8304970a59 | ||
|
|
5cef8e4a01 | ||
|
|
0b50e1dbee | ||
|
|
88a93c4e57 | ||
|
|
38ae35ee95 | ||
|
|
c7471a66db | ||
|
|
a4531be904 | ||
|
|
c58605de54 | ||
|
|
c3dc0b4b2c | ||
|
|
2d3e9988ec | ||
|
|
9548a0bfc8 | ||
|
|
dda778fdde | ||
|
|
98e3f13185 | ||
|
|
7e79c51630 | ||
|
|
181aed4897 | ||
|
|
c626d2b785 | ||
|
|
ed79e81b13 | ||
|
|
264119a142 | ||
|
|
4c1c77821f | ||
|
|
67b11f04fc | ||
|
|
649fe1d4e8 | ||
|
|
c3192423c6 | ||
|
|
911fc74305 | ||
|
|
2fcfd6b4d1 | ||
|
|
bac868d071 | ||
|
|
ea5460488a | ||
|
|
61d42ead21 | ||
|
|
a98555beb1 | ||
|
|
820eb516ce | ||
|
|
6cd0dc1eff | ||
|
|
5dcb2958a1 | ||
|
|
c25476278b | ||
|
|
99ddb4fe70 | ||
|
|
82c47ffc72 | ||
|
|
1f1678800f | ||
|
|
efdd89cd50 | ||
|
|
100b91a118 | ||
|
|
26b71f0ec6 | ||
|
|
92e7ada37b | ||
|
|
2d879d0592 | ||
|
|
8806a6cb43 | ||
|
|
85a04fd06d | ||
|
|
4a5b8dd005 | ||
|
|
2f7da255e5 | ||
|
|
6664ae8f7b |
@@ -5,10 +5,7 @@ VITE_PUBLIC_PATH = '/'
|
||||
VITE_USE_MOCK = true
|
||||
|
||||
# 是否启用MOCK
|
||||
VITE_USE_PROXY = false
|
||||
|
||||
# 代理类型(跟启动和构建环境无关) 'dev' | 'test' | 'prod'
|
||||
VITE_PROXY_TYPE = 'dev'
|
||||
VITE_USE_PROXY = true
|
||||
|
||||
# base api
|
||||
VITE_BASE_API = '/api'
|
||||
62
.eslint-global-variables.json
Normal file
62
.eslint-global-variables.json
Normal 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
|
||||
}
|
||||
}
|
||||
15
.eslintrc.js
15
.eslintrc.js
@@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
38
.github/workflows/deploy.yml
vendored
38
.github/workflows/deploy.yml
vendored
@@ -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
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"endOfLine": "lf",
|
||||
"htmlWhitespaceSensitivity": "ignore"
|
||||
}
|
||||
14
.vscode/launch.json
vendored
Normal file
14
.vscode/launch.json
vendored
Normal 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>/**"]
|
||||
}
|
||||
]
|
||||
}
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,8 +1,4 @@
|
||||
{
|
||||
"path-intellisense.mappings": {
|
||||
"@/": "${workspaceRoot}/src",
|
||||
"~/": "${workspaceRoot}"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"prettier.printWidth": 120,
|
||||
|
||||
207
README.EN.md
Normal file
207
README.EN.md
Normal 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 template,Based on the latest technology stack of front-end such as `Vue3、Vite3、Pinia、Unocss and Naive UI`. Compared with other more popular backend management templates, this project is more concise, lightweight, fresh style, very low learning costs, ideal for small and medium-sized projects or personal projects.
|
||||
|
||||
### Features
|
||||
|
||||
- 🍒 Integrated [Naive UI](https://www.naiveui.com),recommended by Evan You.
|
||||
- 🍑 Integrated login, logout and permission verification.
|
||||
- 🍐 Integrated multi-environment configuration, dev, test, production and github pages environments.
|
||||
- 🍎 Integrated `eslint + prettier`.
|
||||
- 🍌 Integrated `husky + commitlint`.
|
||||
- 🍉 Integrated `Mock`.
|
||||
- 🍍 Integrated `pinia`,lightweight, simple and easy to use alternative to vuex.
|
||||
- 📦 Integrated `unplugin` auto import.
|
||||
- 🤹 Integrated `iconify` icon,support custom svg icons.
|
||||
- 🍇 Integrated `unocss`.
|
||||
|
||||
### Preview
|
||||
|
||||
[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
|
||||
|
||||
# Preview(Need to build first)
|
||||
pnpm preview
|
||||
|
||||
# Commit(husky+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
204
README.md
@@ -1,115 +1,223 @@
|
||||
<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">
|
||||
<img alt="Vue Naive Admin Logo" width="200" src="./src/assets/images/logo.png">
|
||||
</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>
|
||||
<a href="./LICENSE"><img alt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
|
||||
</p>
|
||||
|
||||
<p align='center'>
|
||||
<b>English</b> |
|
||||
<a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.zh-CN.md">简体中文</a>
|
||||
<b>中文</b> |
|
||||
<a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.EN.md">English</a>
|
||||
</p>
|
||||
|
||||
### Introduction
|
||||
### 简介
|
||||
|
||||
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin) is a **completely open source free and commercially allowed ** admin template,Based on the latest technology stack of front-end such as `Vue3、Vite3、Pinia、Unocss and Naive UI`. Compared with other more popular backend management templates, this project is more concise, lightweight, fresh style, very low learning costs, ideal for small and medium-sized projects or personal projects.
|
||||
[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.
|
||||
- 🍑 Integrated login, logout and permission verification.
|
||||
- 🍐 Integrated multi-environment configuration, dev, test, production and github pages environments.
|
||||
- 🍎 Integrated `eslint + prettier`.
|
||||
- 🍌 Integrated `husky + commitlint`.
|
||||
- 🍉 Integrated `Mock`.
|
||||
- 🍍 Integrated `pinia`,lightweight, simple and easy to use alternative to vuex.
|
||||
- 📦 Integrated `unplugin` auto import.
|
||||
- 🤹 Integrated `iconify` icon,support custom svg icons.
|
||||
- 🍇 Integrated `unocss`.
|
||||
- 🍒 集成 [Naive UI](https://www.naiveui.com)
|
||||
- 🍑 集成登陆、注销及权限验证
|
||||
- 🍐 集成多环境配置,dev、测试、生产环境
|
||||
- 🍎 集成 `eslint + prettier`,代码约束和格式化统一
|
||||
- 🍌 集成 `husky + commitlint`,代码提交规范化
|
||||
- 🍉 集成 `mock` 接口服务,dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
|
||||
- 🍍 集成 `pinia`,vuex 的替代方案,轻量、简单、易用
|
||||
- 📦 集成 `unplugin` 插件,自动导入,解放双手,开发效率直接起飞
|
||||
- 🤹 集成 `iconify` 图标,支持自定义 svg 图标, 优雅使用icon
|
||||
- 🍇 集成 `unocss`,antfu 开源的原子 css 解决方案,非常轻量
|
||||
|
||||
### Preview
|
||||
### 预览
|
||||
|
||||
[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
|
||||
# Recommended setup git autocrlf 为 false
|
||||
# 推荐配置git autocrlf 为 false(本项目规范使用lf换行符,此配置是为防止git自动将源文件转换为crlf)
|
||||
# 不清楚为什么要这样做的请参考这篇文章:https://www.freesion.com/article/4532642129
|
||||
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
|
||||
# 安装依赖(建议使用pnpm: https://pnpm.io/zh/installation)
|
||||
npm i -g pnpm # 装了可忽略
|
||||
pnpm i # 或者 npm i
|
||||
|
||||
# Start
|
||||
# 启动
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Build and Release
|
||||
### 构建发布
|
||||
|
||||
```shell
|
||||
# Test Environment
|
||||
# 构建测试环境
|
||||
pnpm build:test
|
||||
|
||||
# Github Environment
|
||||
# 构建github pages环境
|
||||
pnpm build:github
|
||||
|
||||
# Prod Environment
|
||||
# 构建生产环境
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### Other
|
||||
### 其他指令
|
||||
|
||||
```shell
|
||||
# eslint check
|
||||
# eslint代码格式检查
|
||||
pnpm lint
|
||||
|
||||
# eslint check and fix
|
||||
# 代码检查并修复
|
||||
pnpm lint:fix
|
||||
|
||||
# Preview(Need to build first)
|
||||
# 预览发布包效果(需先执行构建指令)
|
||||
pnpm preview
|
||||
|
||||
# Commit(husky+commitlint)
|
||||
# 提交代码(husky+commitlint)
|
||||
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)
|
||||
|
||||
#### preview
|
||||
#### 预览
|
||||
|
||||
- [https://admin.qszone.com](https://admin.qszone.com)
|
||||
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
|
||||
|
||||
### Communication group & About the author
|
||||
### 使用该项目的开源项目
|
||||
|
||||
- [gin-vue-blog](https://github.com/szluyu99/gin-vue-blog): Golang 全栈博客项目, 博客后台的前端基于 vue-naive-admin,对接真实后端服务,实现了后端控制路由等特性。
|
||||
|
||||
|
||||
### 入群交流 & 关于作者
|
||||
|
||||
<a href="https://blog.qszone.com/about/">
|
||||
<img src="https://assets.qszone.com/images/about.png" style="max-width: 400px" />
|
||||
</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>
|
||||
|
||||
115
README.zh-CN.md
115
README.zh-CN.md
@@ -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>
|
||||
|
||||
|
||||
@@ -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_,
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './define'
|
||||
export * from './proxy'
|
||||
@@ -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
|
||||
}
|
||||
@@ -1 +1,33 @@
|
||||
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'), ''),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { resolve } from 'path'
|
||||
import DefineOptions from 'unplugin-vue-define-options/vite'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||
@@ -19,7 +18,6 @@ import { getSrcPath } from '../utils'
|
||||
const customIconPath = resolve(getSrcPath(), 'assets/svg')
|
||||
|
||||
export default [
|
||||
DefineOptions(),
|
||||
AutoImport({
|
||||
imports: ['vue', 'vue-router'],
|
||||
dts: false,
|
||||
@@ -33,7 +31,10 @@ export default [
|
||||
defaultClass: 'inline-block',
|
||||
}),
|
||||
Components({
|
||||
resolvers: [NaiveUiResolver(), IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })],
|
||||
resolvers: [
|
||||
NaiveUiResolver(),
|
||||
IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' }),
|
||||
],
|
||||
dts: false,
|
||||
}),
|
||||
createSvgIconsPlugin({
|
||||
|
||||
@@ -4,7 +4,7 @@ import dotenv from 'dotenv'
|
||||
|
||||
/**
|
||||
* * 项目根路径
|
||||
* @descrition 结尾不带/
|
||||
* @description 结尾不带/
|
||||
*/
|
||||
export function getRootPath() {
|
||||
return path.resolve(process.cwd())
|
||||
@@ -13,14 +13,12 @@ export function getRootPath() {
|
||||
/**
|
||||
* * 项目src路径
|
||||
* @param srcName src目录名称(默认: "src")
|
||||
* @descrition 结尾不带斜杠
|
||||
* @description 结尾不带斜杠
|
||||
*/
|
||||
export function getSrcPath(srcName = 'src') {
|
||||
return path.resolve(getRootPath(), srcName)
|
||||
}
|
||||
|
||||
const httpsReg = /^https:\/\//
|
||||
|
||||
export function convertEnv(envOptions) {
|
||||
const result = {}
|
||||
if (!envOptions) return result
|
||||
|
||||
3
docs/使用unocss.md
Normal file
3
docs/使用unocss.md
Normal file
@@ -0,0 +1,3 @@
|
||||
推荐阅读作者在掘金的文章:
|
||||
|
||||
[保熟的UnoCSS使用指北,优雅使用antfu大佬的原子化CSS](https://juejin.cn/post/7142466784971456548)
|
||||
40
docs/使用图标.md
Normal file
40
docs/使用图标.md
Normal 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
32
docs/安装pnpm.md
Normal 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
|
||||
```
|
||||
@@ -7,7 +7,7 @@
|
||||
<meta http-equiv="Cache-control" content="no-cache" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<link rel="stylesheet" href="/resource/loading.css" />
|
||||
|
||||
<title><%= title %></title>
|
||||
@@ -17,7 +17,7 @@
|
||||
<div id="app">
|
||||
<!-- 白屏时的loading效果 -->
|
||||
<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">
|
||||
<div class="left-0 top-0 loading-spin-item"></div>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"~/*": ["./*"],
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"jsx": "preserve"
|
||||
"jsx": "preserve",
|
||||
"allowJs": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ const posts = [
|
||||
author: '大脸怪',
|
||||
category: 'Http',
|
||||
description: '谈谈前端缓存的理解',
|
||||
content: '> 背景\n\n公司有个vue-cli3移动端web项目发版更新后发现部分用户手机在钉钉内置浏览器打开出现了缓存',
|
||||
content:
|
||||
'> 背景\n\n公司有个vue-cli3移动端web项目发版更新后发现部分用户手机在钉钉内置浏览器打开出现了缓存',
|
||||
isRecommend: true,
|
||||
isPublish: true,
|
||||
createDate: '2021-06-10T18:51:19.000Z',
|
||||
@@ -49,7 +50,8 @@ const posts = [
|
||||
author: '大脸怪',
|
||||
category: 'JavaScript',
|
||||
description: '简单介绍下在 Promise 类中,有5 种静态方法及它们的使用场景',
|
||||
content: '## 1. Promise.all\n\n并行执行多个 promise,并等待所有 promise 都准备就绪。再对它们进行处理。',
|
||||
content:
|
||||
'## 1. Promise.all\n\n并行执行多个 promise,并等待所有 promise 都准备就绪。再对它们进行处理。',
|
||||
isRecommend: true,
|
||||
isPublish: true,
|
||||
createDate: '2021-02-22T22:37:06.000Z',
|
||||
@@ -65,7 +67,9 @@ export default [
|
||||
const { title, pageNo, pageSize } = data.query
|
||||
let pageData = []
|
||||
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 (pageSize) {
|
||||
while (pageData.length < pageSize) {
|
||||
|
||||
115
package.json
115
package.json
@@ -2,71 +2,80 @@
|
||||
"name": "vue-naive-admin",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"build:github": "vite build --mode github && esno ./build/script",
|
||||
"build:test": "vite build --mode test",
|
||||
"cz": "cz",
|
||||
"dev": "vite",
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
"lint:fix": "eslint --fix --ext .js,.vue .",
|
||||
"lint:staged": "lint-staged",
|
||||
"build": "vite build",
|
||||
"build:test": "vite build --mode test",
|
||||
"build:github": "vite build --mode github && esno ./build/script",
|
||||
"preview": "vite preview",
|
||||
"prepare": "husky install",
|
||||
"cz": "cz"
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^8.4.2",
|
||||
"axios": "^0.21.4",
|
||||
"dayjs": "^1.11.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"md-editor-v3": "^1.11.4",
|
||||
"mockjs": "^1.1.0",
|
||||
"pinia": "^2.0.13",
|
||||
"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"
|
||||
"lint-staged": {
|
||||
"*.{js,vue}": [
|
||||
"eslint --ext .js,.vue ."
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "node_modules/cz-customizable"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,vue}": [
|
||||
"eslint --ext .js,.vue ."
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"@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
6116
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 120,
|
||||
singleQuote: true,
|
||||
semi: false,
|
||||
endOfLine: 'lf',
|
||||
}
|
||||
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
@@ -10,12 +10,6 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-svg {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.loading-spin__container {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
|
||||
@@ -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() {
|
||||
const key = '__THEME_COLOR__'
|
||||
const defaultColor = '#316c72'
|
||||
@@ -21,5 +7,3 @@ function addThemeColorCssVars() {
|
||||
}
|
||||
|
||||
addThemeColorCssVars()
|
||||
|
||||
initSvgLogo('#loadingLogo')
|
||||
|
||||
BIN
public/resource/logo.png
Normal file
BIN
public/resource/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
@@ -1,2 +1 @@
|
||||
export * from './theme.json'
|
||||
export * from './proxy-config'
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
@@ -11,7 +11,27 @@
|
||||
"primaryColor": "#316C72FF",
|
||||
"primaryColorHover": "#316C72E3",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import request from '@/utils/http'
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
getUser: () => request.get('/user'),
|
||||
|
||||
BIN
src/assets/images/logo.png
Normal file
BIN
src/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
@@ -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 |
@@ -1,11 +1,21 @@
|
||||
<template>
|
||||
<footer text-14 f-c-c flex-col color="#6a6a6a">
|
||||
<footer f-c-c flex-col text-14 color="#6a6a6a">
|
||||
<p>
|
||||
Copyright©2022
|
||||
<a href="https://github.com/zclzone" target="__blank" hover="decoration-underline color-primary"> 大脸怪</a>
|
||||
Copyright © 2022-present
|
||||
<a
|
||||
href="https://github.com/zclzone"
|
||||
target="__blank"
|
||||
hover="decoration-underline color-primary"
|
||||
>
|
||||
Ronnie Zhang
|
||||
</a>
|
||||
</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
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
<template>
|
||||
<n-config-provider wh-full :theme-overrides="naiveThemeOverrides">
|
||||
<n-loading-bar-provider>
|
||||
<n-dialog-provider>
|
||||
<n-notification-provider>
|
||||
<n-message-provider>
|
||||
<slot></slot>
|
||||
<NaiveProviderContent />
|
||||
</n-message-provider>
|
||||
</n-notification-provider>
|
||||
</n-dialog-provider>
|
||||
</n-loading-bar-provider>
|
||||
<n-config-provider
|
||||
wh-full
|
||||
:locale="zhCN"
|
||||
:date-locale="dateZhCN"
|
||||
:theme="appStore.isDark ? darkTheme : undefined"
|
||||
:theme-overrides="naiveThemeOverrides"
|
||||
>
|
||||
<slot />
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { useLoadingBar, useDialog, useMessage, useNotification } from 'naive-ui'
|
||||
import { zhCN, dateZhCN, darkTheme } from 'naive-ui'
|
||||
import { useCssVar } from '@vueuse/core'
|
||||
import { kebabCase } from 'lodash-es'
|
||||
import { setupMessage, setupDialog } from '@/utils/common/naiveTools'
|
||||
import { naiveThemeOverrides } from '~/settings'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
function setupCssVar() {
|
||||
const common = naiveThemeOverrides.common
|
||||
@@ -28,23 +26,5 @@ function setupCssVar() {
|
||||
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()
|
||||
setupNaiveTools()
|
||||
},
|
||||
render() {
|
||||
return h('div')
|
||||
},
|
||||
})
|
||||
setupCssVar()
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="wrapper" @mousewheel.prevent="handleMouseWheel">
|
||||
<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 />
|
||||
</div>
|
||||
<div class="right" @click="handleMouseWheel({ wheelDelta: -120 })">
|
||||
<div class="right dark:bg-dark!" @click="handleMouseWheel({ wheelDelta: -120 })">
|
||||
<icon-ic:baseline-keyboard-arrow-right />
|
||||
</div>
|
||||
</template>
|
||||
@@ -23,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { debounce } from '@/utils'
|
||||
import { debounce, useResize } from '@/utils'
|
||||
|
||||
defineProps({
|
||||
showArrow: {
|
||||
@@ -38,16 +38,16 @@ const wrapper = ref(null)
|
||||
const isOverflow = ref(false)
|
||||
|
||||
const refreshIsOverflow = debounce(() => {
|
||||
const wrapperWidth = wrapper.value.offsetWidth
|
||||
const contentWidth = content.value.offsetWidth
|
||||
const wrapperWidth = wrapper.value?.offsetWidth
|
||||
const contentWidth = content.value?.offsetWidth
|
||||
isOverflow.value = contentWidth > wrapperWidth
|
||||
resetTranslateX(wrapperWidth, contentWidth)
|
||||
}, 200)
|
||||
|
||||
function handleMouseWheel(e) {
|
||||
const { wheelDelta } = e
|
||||
const wrapperWidth = wrapper.value.offsetWidth
|
||||
const contentWidth = content.value.offsetWidth
|
||||
const wrapperWidth = wrapper.value?.offsetWidth
|
||||
const contentWidth = content.value?.offsetWidth
|
||||
/**
|
||||
* @wheelDelta 平行滚动的值 >0: 右移 <0: 左移
|
||||
* @translateX 内容translateX的值
|
||||
@@ -76,17 +76,39 @@ const resetTranslateX = debounce(function (wrapperWidth, contentWidth) {
|
||||
}
|
||||
}, 200)
|
||||
|
||||
const observer = new MutationObserver(refreshIsOverflow)
|
||||
const observers = ref([])
|
||||
onMounted(() => {
|
||||
refreshIsOverflow()
|
||||
|
||||
window.addEventListener('resize', refreshIsOverflow)
|
||||
// 监听内容宽度刷新是否超出
|
||||
observer.observe(content.value, { childList: true })
|
||||
observers.value.push(useResize(document.body, refreshIsOverflow))
|
||||
observers.value.push(useResize(content.value, refreshIsOverflow))
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', refreshIsOverflow)
|
||||
observer.disconnect()
|
||||
observers.value.forEach((item) => {
|
||||
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>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { renderIcon, renderCustomIcon } from '@/utils/icon'
|
||||
import { renderIcon, renderCustomIcon } from '@/utils'
|
||||
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<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 />
|
||||
<AppFooter v-if="showFooter" mt-15 />
|
||||
<n-back-top :bottom="20" />
|
||||
</section>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<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" />
|
||||
<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" />
|
||||
</template>
|
||||
</header>
|
||||
|
||||
<n-card rounded-10 flex-1>
|
||||
<n-card flex-1 rounded-10>
|
||||
<slot />
|
||||
</n-card>
|
||||
</AppPage>
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
<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]">
|
||||
<slot />
|
||||
</n-space>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<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>
|
||||
<div :style="{ width: contentWidth + 'px' }" flex-shrink-0>
|
||||
@@ -10,7 +15,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { isNullOrWhitespace } from '@/utils/is'
|
||||
import { isNullOrWhitespace } from '@/utils'
|
||||
|
||||
defineProps({
|
||||
label: {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<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 />
|
||||
<template v-if="showFooter" #footer>
|
||||
<footer flex justify-end>
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { utils, writeFile } from 'xlsx'
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* @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 initQuery = { ...props.queryItems }
|
||||
const tableData = ref([])
|
||||
@@ -87,13 +89,18 @@ async function handleQuery() {
|
||||
if (props.isPagination && props.remote) {
|
||||
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
|
||||
pagination.itemCount = data.total ?? data.length
|
||||
} catch (error) {
|
||||
tableData.value = []
|
||||
pagination.itemCount = 0
|
||||
} finally {
|
||||
emit('onDataChange', tableData.value)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
@@ -122,9 +129,21 @@ function 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({
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handleExport,
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isNullOrWhitespace } from '@/utils/is'
|
||||
import { isNullOrWhitespace } from '@/utils'
|
||||
|
||||
const ACTIONS = {
|
||||
view: '查看',
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
<template>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<KeepAlive :include="keepAliveRouteNames">
|
||||
<component :is="Component" v-if="appStore.reloadFlag" :key="route.meta?.key || route.fullPath" />
|
||||
<KeepAlive :include="keepAliveNames">
|
||||
<component :is="Component" v-if="!tagStore.reloading" :key="route.fullPath" />
|
||||
</KeepAlive>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useRouter } from 'vue-router'
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
import { useTagsStore } from '@/store'
|
||||
const tagStore = useTagsStore()
|
||||
|
||||
const allRoutes = router.getRoutes()
|
||||
const keepAliveRouteNames = computed(() => {
|
||||
return allRoutes.filter((route) => route.meta?.keepAlive).map((route) => route.name)
|
||||
const keepAliveNames = computed(() => {
|
||||
return tagStore.tags.filter((item) => item.keepAlive).map((item) => item.name)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { renderCustomIcon, renderIcon } from '@/utils/icon'
|
||||
import { renderCustomIcon, renderIcon } from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
18
src/layout/components/header/components/ThemeMode.vue
Normal file
18
src/layout/components/header/components/ThemeMode.vue
Normal 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>
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<n-dropdown :options="options" @select="handleSelect">
|
||||
<div flex items-center cursor-pointer>
|
||||
<img :src="userStore.avatar" mr10 w-35 h-35 rounded-full />
|
||||
<div flex cursor-pointer items-center>
|
||||
<img :src="userStore.avatar" mr10 h-35 w-35 rounded-full />
|
||||
<span>{{ userStore.name }}</span>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { renderIcon } from '@/utils/icon'
|
||||
import { useUserStore } from '@/store'
|
||||
import { renderIcon } from '@/utils'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<div flex items-center>
|
||||
<MenuCollapse />
|
||||
<BreadCrumb ml-15 />
|
||||
<BreadCrumb ml-15 hidden sm:block />
|
||||
</div>
|
||||
<div ml-auto flex items-center>
|
||||
<MessageNotification />
|
||||
<ThemeMode />
|
||||
<GithubSite />
|
||||
<FullScreen />
|
||||
<UserAvatar />
|
||||
@@ -16,4 +18,6 @@ import MenuCollapse from './components/MenuCollapse.vue'
|
||||
import FullScreen from './components/FullScreen.vue'
|
||||
import UserAvatar from './components/UserAvatar.vue'
|
||||
import GithubSite from './components/GithubSite.vue'
|
||||
import ThemeMode from './components/ThemeMode.vue'
|
||||
import MessageNotification from './components/MessageNotification.vue'
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<router-link h-60 f-c-c to="/">
|
||||
<icon-custom-logo text-36 color-primary></icon-custom-logo>
|
||||
<h2 v-show="!appStore.collapsed" ml-10 color-primary text-16 font-bold max-w-140 flex-shrink-0>
|
||||
<img src="@/assets/images/logo.png" height="42" />
|
||||
<h2 v-show="!appStore.collapsed" ml-10 max-w-140 flex-shrink-0 text-16 font-bold color-primary>
|
||||
{{ title }}
|
||||
</h2>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useAppStore } from '@/store'
|
||||
const title = import.meta.env.VITE_TITLE
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
<template>
|
||||
<n-menu
|
||||
ref="menu"
|
||||
class="side-menu"
|
||||
accordion
|
||||
:indent="18"
|
||||
:collapsed-icon-size="22"
|
||||
:collapsed-width="64"
|
||||
:options="menuOptions"
|
||||
:value="curRoute.meta?.activeMenu || curRoute.name"
|
||||
:value="activeKey"
|
||||
@update:value="handleMenuSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
|
||||
import { isExternal } from '@/utils/is'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { renderCustomIcon, renderIcon } from '@/utils/icon'
|
||||
import { usePermissionStore } from '@/store'
|
||||
import { renderCustomIcon, renderIcon, isExternal } from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
const curRoute = useRoute()
|
||||
const permissionStore = usePermissionStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const activeKey = computed(() => curRoute.meta?.activeMenu || curRoute.name)
|
||||
|
||||
const menuOptions = computed(() => {
|
||||
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) {
|
||||
if (isExternal(path)) return path
|
||||
return (
|
||||
@@ -47,7 +52,9 @@ function getMenuItem(route, basePath = '') {
|
||||
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
|
||||
|
||||
@@ -55,25 +62,28 @@ function getMenuItem(route, basePath = '') {
|
||||
// 单个子路由处理
|
||||
const singleRoute = visibleChildren[0]
|
||||
menuItem = {
|
||||
...menuItem,
|
||||
label: singleRoute.meta?.title || singleRoute.name,
|
||||
key: singleRoute.name,
|
||||
path: resolvePath(menuItem.path, singleRoute.path),
|
||||
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) {
|
||||
menuItem = getMenuItem(visibleItems[0], menuItem.path)
|
||||
} 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 {
|
||||
menuItem.children = visibleChildren
|
||||
.map((item) => getMenuItem(item, menuItem.path))
|
||||
.sort((a, b) => a.order - b.order)
|
||||
}
|
||||
|
||||
return menuItem
|
||||
}
|
||||
|
||||
@@ -86,13 +96,9 @@ function getIcon(meta) {
|
||||
function handleMenuSelect(key, item) {
|
||||
if (isExternal(item.path)) {
|
||||
window.open(item.path)
|
||||
} else {
|
||||
if (item.path === curRoute.path) {
|
||||
appStore.reloadPage()
|
||||
} else {
|
||||
router.push(item.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -11,9 +11,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useTagsStore } from '@/store/modules/tags'
|
||||
import { renderIcon } from '@/utils/icon'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useTagsStore } from '@/store'
|
||||
import { renderIcon } from '@/utils'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
@@ -37,7 +36,6 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:show'])
|
||||
|
||||
const tagsStore = useTagsStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const options = computed(() => [
|
||||
{
|
||||
@@ -67,7 +65,9 @@ const options = computed(() => [
|
||||
{
|
||||
label: '关闭右侧',
|
||||
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' }),
|
||||
},
|
||||
])
|
||||
@@ -77,11 +77,7 @@ const actionMap = new Map([
|
||||
[
|
||||
'reload',
|
||||
() => {
|
||||
if (route.meta?.keepAlive) {
|
||||
// 重置keepAlive
|
||||
route.meta.key = +new Date()
|
||||
}
|
||||
appStore.reloadPage()
|
||||
tagsStore.reloadTag(route.path, route.meta?.keepAlive)
|
||||
},
|
||||
],
|
||||
[
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
<template>
|
||||
<ScrollX>
|
||||
<ScrollX ref="scrollXRef" class="bg-white dark:bg-dark!">
|
||||
<n-tag
|
||||
v-for="tag in tagsStore.tags"
|
||||
ref="tabRefs"
|
||||
: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'"
|
||||
:closable="tagsStore.tags.length > 1"
|
||||
@click="handleTagClick(tag.path)"
|
||||
@close.stop="tagsStore.removeTag(tag.path)"
|
||||
@contextmenu.prevent="handleContextMenu($event, tag)"
|
||||
>
|
||||
<template v-if="tag.icon" #icon>
|
||||
<TheIcon :icon="tag.icon" class="mr-4" />
|
||||
</template>
|
||||
{{ tag.title }}
|
||||
</n-tag>
|
||||
<ContextMenu
|
||||
@@ -24,12 +28,14 @@
|
||||
|
||||
<script setup>
|
||||
import ContextMenu from './ContextMenu.vue'
|
||||
import { useTagsStore } from '@/store/modules/tags'
|
||||
import { useTagsStore } from '@/store'
|
||||
import ScrollX from '@/components/common/ScrollX.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const tagsStore = useTagsStore()
|
||||
const tabRefs = ref([])
|
||||
const scrollXRef = ref(null)
|
||||
|
||||
const contextMenuOption = reactive({
|
||||
show: false,
|
||||
@@ -41,9 +47,23 @@ const contextMenuOption = reactive({
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
const { name, path } = route
|
||||
const { name, fullPath: path } = route
|
||||
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 }
|
||||
)
|
||||
|
||||
@@ -11,14 +11,19 @@
|
||||
<SideBar />
|
||||
</n-layout-sider>
|
||||
|
||||
<article flex-1 flex-col overflow-hidden>
|
||||
<header bg-white px-15 border-b bc-eee flex items-center :style="`height: ${header.height}px`">
|
||||
<article flex-col flex-1 overflow-hidden>
|
||||
<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 />
|
||||
</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` }" />
|
||||
</section>
|
||||
<section flex-1 overflow-hidden>
|
||||
<section flex-1 overflow-hidden bg-hex-f5f6fb dark:bg-hex-101014>
|
||||
<AppMain />
|
||||
</section>
|
||||
</article>
|
||||
@@ -30,7 +35,7 @@ import AppHeader from './components/header/index.vue'
|
||||
import SideBar from './components/sidebar/index.vue'
|
||||
import AppMain from './components/AppMain.vue'
|
||||
import AppTags from './components/tags/index.vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useAppStore } from '@/store'
|
||||
import { header, tags } from '~/settings'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
/** 重置样式 */
|
||||
import '@/styles/reset.css'
|
||||
import '@/styles/index.scss'
|
||||
import 'uno.css'
|
||||
import '@/styles/global.scss'
|
||||
import 'virtual:svg-icons-register'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { setupRouter } from '@/router'
|
||||
import { setupStore } from '@/store'
|
||||
import App from './App.vue'
|
||||
import { setupNaiveDiscreteApi } from './utils'
|
||||
|
||||
async function setupApp() {
|
||||
const app = createApp(App)
|
||||
|
||||
setupStore(app)
|
||||
setupNaiveDiscreteApi()
|
||||
|
||||
await setupRouter(app)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getToken, refreshAccessToken } from '@/utils/token'
|
||||
import { isNullOrWhitespace } from '@/utils/is'
|
||||
import { getToken, refreshAccessToken, isNullOrWhitespace } from '@/utils'
|
||||
|
||||
const WHITE_LIST = ['/login', '/404']
|
||||
export function createPermissionGuard(router) {
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||
import { setupRouterGuard } from './guard'
|
||||
import { basicRoutes as routes, EMPTY_ROUTE, NOT_FOUND_ROUTE } from './routes'
|
||||
import { getToken } from '@/utils/token'
|
||||
import { isNullOrWhitespace } from '@/utils/is'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { basicRoutes, EMPTY_ROUTE, NOT_FOUND_ROUTE } from './routes'
|
||||
import { getToken, isNullOrWhitespace } from '@/utils'
|
||||
import { useUserStore, usePermissionStore } from '@/store'
|
||||
|
||||
const isHash = import.meta.env.VITE_USE_HASH === 'true'
|
||||
export const router = createRouter({
|
||||
history: isHash ? createWebHashHistory('/') : createWebHistory('/'),
|
||||
routes,
|
||||
routes: basicRoutes,
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
})
|
||||
|
||||
export async function resetRouter() {
|
||||
router.getRoutes().forEach((route) => {
|
||||
const { name } = route
|
||||
router.hasRoute(name) && router.removeRoute(name)
|
||||
})
|
||||
export async function setupRouter(app) {
|
||||
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() {
|
||||
@@ -46,8 +52,14 @@ export async function addDynamicRoutes() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupRouter(app) {
|
||||
await addDynamicRoutes()
|
||||
setupRouterGuard(router)
|
||||
app.use(router)
|
||||
export function getRouteNames(routes) {
|
||||
return routes.map((route) => getRouteName(route)).flat(1)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -3,3 +3,5 @@ import { createPinia } from 'pinia'
|
||||
export function setupStore(app) {
|
||||
app.use(createPinia())
|
||||
}
|
||||
|
||||
export * from './modules'
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { useDark } from '@vueuse/core'
|
||||
|
||||
const isDark = useDark()
|
||||
export const useAppStore = defineStore('app', {
|
||||
state() {
|
||||
return {
|
||||
reloadFlag: true,
|
||||
collapsed: false,
|
||||
isDark,
|
||||
}
|
||||
},
|
||||
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() {
|
||||
this.collapsed = !this.collapsed
|
||||
},
|
||||
setCollapsed(collapsed) {
|
||||
this.collapsed = collapsed
|
||||
},
|
||||
/** 设置暗黑模式 */
|
||||
setDark(isDark) {
|
||||
this.isDark = isDark
|
||||
},
|
||||
/** 切换/关闭 暗黑模式 */
|
||||
toggleDark() {
|
||||
this.isDark = !this.isDark
|
||||
},
|
||||
},
|
||||
})
|
||||
4
src/store/modules/index.js
Normal file
4
src/store/modules/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './app'
|
||||
export * from './permission'
|
||||
export * from './tags'
|
||||
export * from './user'
|
||||
@@ -53,5 +53,8 @@ export const usePermissionStore = defineStore('permission', {
|
||||
this.accessRoutes = accessRoutes
|
||||
return accessRoutes
|
||||
},
|
||||
resetPermission() {
|
||||
this.$reset()
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { sStorage } from '@/utils/cache'
|
||||
import { sStorage } from '@/utils'
|
||||
|
||||
export const activeTag = sStorage.get('activeTag')
|
||||
export const tags = sStorage.get('tags')
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { activeTag, tags, WITHOUT_TAG_PATHS } from './helpers'
|
||||
import { router } from '@/router'
|
||||
import { sStorage } from '@/utils/cache'
|
||||
import { sStorage } from '@/utils'
|
||||
|
||||
export const useTagsStore = defineStore('tag', {
|
||||
state() {
|
||||
return {
|
||||
tags: tags || [],
|
||||
activeTag: activeTag || '',
|
||||
reloading: false,
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
activeIndex() {
|
||||
return this.tags.findIndex((item) => item.path === this.activeTag)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setActiveTag(path) {
|
||||
this.activeTag = path
|
||||
@@ -20,20 +26,32 @@ export const useTagsStore = defineStore('tag', {
|
||||
sStorage.set('tags', tags)
|
||||
},
|
||||
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)
|
||||
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) {
|
||||
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))
|
||||
if (path === this.activeTag) {
|
||||
router.push(this.tags[this.tags.length - 1].path)
|
||||
}
|
||||
},
|
||||
removeOther(curPath = this.activeTag) {
|
||||
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)
|
||||
}
|
||||
},
|
||||
resetTags() {
|
||||
this.setTags([])
|
||||
this.setActiveTag('')
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { removeToken } from '@/utils/token'
|
||||
import { toLogin } from '@/utils/auth'
|
||||
import { resetRouter } from '@/router'
|
||||
import { useTagsStore, usePermissionStore } from '@/store'
|
||||
import { removeToken, toLogin } from '@/utils'
|
||||
import api from '@/api'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
@@ -35,8 +36,13 @@ export const useUserStore = defineStore('user', {
|
||||
}
|
||||
},
|
||||
async logout() {
|
||||
const { resetTags } = useTagsStore()
|
||||
const { resetPermission } = usePermissionStore()
|
||||
removeToken()
|
||||
this.userInfo = {}
|
||||
resetTags()
|
||||
resetPermission()
|
||||
resetRouter()
|
||||
this.$reset()
|
||||
toLogin()
|
||||
},
|
||||
setUserInfo(userInfo = {}) {
|
||||
@@ -1,14 +1,8 @@
|
||||
html {
|
||||
font-size: 4px; // * 1rem = 4px 方便unocss计算:在unocss中 1字体单位 = 0.25rem,相当于 1等份 = 1px
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #f2f2f2;
|
||||
font-family: 'Encode Sans Condensed', sans-serif;
|
||||
}
|
||||
|
||||
#app {
|
||||
@@ -16,7 +10,7 @@ body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* router view transition fade-slide */
|
||||
/* transition fade-slide */
|
||||
.fade-slide-leave-active,
|
||||
.fade-slide-enter-active {
|
||||
transition: all 0.3s;
|
||||
@@ -32,7 +26,6 @@ body {
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
/* 自定义滚动条样式 */
|
||||
.cus-scroll {
|
||||
overflow: auto;
|
||||
@@ -12,7 +12,7 @@ html {
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
@@ -33,8 +33,3 @@ textarea {
|
||||
border: none;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@@ -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
17
src/utils/auth/auth.js
Normal 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
2
src/utils/auth/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './auth'
|
||||
export * from './token'
|
||||
@@ -1,4 +1,4 @@
|
||||
import { lStorage } from './cache'
|
||||
import { lStorage } from '@/utils'
|
||||
import api from '@/api'
|
||||
|
||||
const TOKEN_CODE = 'access_token'
|
||||
@@ -23,9 +23,11 @@ export async function refreshAccessToken() {
|
||||
}
|
||||
const { time } = tokenItem
|
||||
// token生成或者刷新后30分钟内不执行刷新
|
||||
if (new Date().getTime() - time <= 1000 * 60 * 30) return
|
||||
if (Date.now() - time <= 1000 * 60 * 30) return
|
||||
try {
|
||||
const res = await api.refreshToken()
|
||||
setToken(res.data.token)
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
90
src/utils/common/common.js
Normal file
90
src/utils/common/common.js
Normal 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
|
||||
}
|
||||
4
src/utils/common/index.js
Normal file
4
src/utils/common/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './common'
|
||||
export * from './is'
|
||||
export * from './icon'
|
||||
export * from './naiveTools'
|
||||
@@ -102,7 +102,7 @@ export function ifNull(val, def = '') {
|
||||
|
||||
export function isUrl(path) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
let loadingMessage = null
|
||||
@@ -77,3 +80,20 @@ export function setupDialog(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)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { isNullOrUndef } from '@/utils/is'
|
||||
import { removeToken } from '@/utils/token'
|
||||
import { toLogin } from '@/utils/auth'
|
||||
import { useUserStore } from '@/store'
|
||||
|
||||
export function addBaseParams(params) {
|
||||
if (!params.userId) {
|
||||
@@ -9,21 +6,14 @@ export function addBaseParams(params) {
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveResError(error = {}) {
|
||||
let { code, message } = error
|
||||
if (isNullOrUndef(code)) {
|
||||
// 未知错误
|
||||
code = -1
|
||||
message = message ?? '接口未知异常!'
|
||||
} else {
|
||||
export function resolveResError(code, message) {
|
||||
switch (code) {
|
||||
case 400:
|
||||
message = message ?? '请求参数错误'
|
||||
break
|
||||
case 401:
|
||||
message = message ?? '登录已过期'
|
||||
removeToken()
|
||||
toLogin()
|
||||
useUserStore().logout()
|
||||
break
|
||||
case 403:
|
||||
message = message ?? '没有权限'
|
||||
@@ -35,9 +25,8 @@ export function resolveResError(error = {}) {
|
||||
message = message ?? '服务器异常'
|
||||
break
|
||||
default:
|
||||
message = message ?? '操作异常!'
|
||||
message = message ?? `【${code}】: 未知异常!`
|
||||
break
|
||||
}
|
||||
}
|
||||
return { code, message }
|
||||
return message
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios'
|
||||
import { repReject, repResolve, reqReject, reqResolve } from './interceptors'
|
||||
import { resReject, resResolve, reqReject, reqResolve } from './interceptors'
|
||||
|
||||
export function createAxios(options = {}) {
|
||||
const defaultOptions = {
|
||||
@@ -10,10 +10,10 @@ export function createAxios(options = {}) {
|
||||
...options,
|
||||
})
|
||||
service.interceptors.request.use(reqResolve, reqReject)
|
||||
service.interceptors.response.use(repResolve, repReject)
|
||||
service.interceptors.response.use(resResolve, resReject)
|
||||
return service
|
||||
}
|
||||
|
||||
export default createAxios({
|
||||
export const request = createAxios({
|
||||
baseURL: import.meta.env.VITE_BASE_API,
|
||||
})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getToken } from '@/utils/token'
|
||||
import { toLogin } from '@/utils/auth'
|
||||
import { getToken } from '@/utils'
|
||||
import { resolveResError } from './helpers'
|
||||
|
||||
export function reqResolve(config) {
|
||||
@@ -10,9 +9,7 @@ export function reqResolve(config) {
|
||||
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
// * 未登录或者token过期的情况下,跳转登录页重新登录
|
||||
toLogin()
|
||||
return Promise.reject({ code: '-1', message: '未登录' })
|
||||
return Promise.reject({ code: 401, message: '登录已过期,请重新登录!' })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,19 +25,34 @@ export function reqReject(error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
export function repResolve(response) {
|
||||
const { noNeedTip } = response.config
|
||||
if (response.data?.code !== 0) {
|
||||
const { code, message } = resolveResError(response?.data)
|
||||
!noNeedTip && $message.error(message)
|
||||
return Promise.reject({ code, message, error: response?.data })
|
||||
export function resResolve(response) {
|
||||
// TODO: 处理不同的 response.headers
|
||||
const { data, status, config, statusText } = response
|
||||
if (data?.code !== 0) {
|
||||
const code = data?.code ?? status
|
||||
|
||||
/** 根据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) {
|
||||
const { noNeedTip } = error.response?.config || error.config
|
||||
const { code, message } = resolveResError(error.response?.data)
|
||||
!noNeedTip && $message.error(message)
|
||||
export function resReject(error) {
|
||||
if (!error || !error.response) {
|
||||
const code = error?.code
|
||||
/** 根据code处理对应的操作,并返回处理后的message */
|
||||
const message = resolveResError(code, error.message)
|
||||
window.$message?.error(message)
|
||||
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 })
|
||||
}
|
||||
|
||||
@@ -1,76 +1,4 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
export * from './common'
|
||||
export * from './storage'
|
||||
export * from './http'
|
||||
export * from './auth'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isNullOrUndef } from '@/utils/is'
|
||||
import { isNullOrUndef } from '@/utils'
|
||||
|
||||
class Storage {
|
||||
constructor(option) {
|
||||
@@ -14,7 +14,7 @@ class Storage {
|
||||
const stringData = JSON.stringify({
|
||||
value,
|
||||
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)
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class Storage {
|
||||
try {
|
||||
const data = JSON.parse(val)
|
||||
const { value, time, expire } = data
|
||||
if (isNullOrUndef(expire) || expire > new Date().getTime()) {
|
||||
if (isNullOrUndef(expire) || expire > Date.now()) {
|
||||
return { value, time }
|
||||
}
|
||||
this.remove(key)
|
||||
@@ -4,25 +4,33 @@
|
||||
<n-card title="按钮 Button">
|
||||
<n-space>
|
||||
<n-button>Default</n-button>
|
||||
<n-button type="tertiary"> Tertiary </n-button>
|
||||
<n-button type="primary"> Primary </n-button>
|
||||
<n-button type="info"> Info </n-button>
|
||||
<n-button type="success"> Success </n-button>
|
||||
<n-button type="warning"> Warning </n-button>
|
||||
<n-button type="error"> Error </n-button>
|
||||
<n-button type="tertiary">Tertiary</n-button>
|
||||
<n-button type="primary">Primary</n-button>
|
||||
<n-button type="info">Info</n-button>
|
||||
<n-button type="success">Success</n-button>
|
||||
<n-button type="warning">Warning</n-button>
|
||||
<n-button type="error">Error</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card title="带 Icon 的按钮">
|
||||
<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">
|
||||
<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 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 type="primary"> <TheIcon icon="majesticons:eye-line" :size="18" class="mr-5" /> 查看 </n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
@@ -30,15 +38,18 @@
|
||||
<n-space size="large" mt-30>
|
||||
<n-card min-w-340 title="通知 Notification">
|
||||
<n-space>
|
||||
<n-button @click="notify('info')"> 信息 </n-button>
|
||||
<n-button @click="notify('success')"> 成功 </n-button>
|
||||
<n-button @click="notify('warning')"> 警告 </n-button>
|
||||
<n-button @click="notify('error')"> 错误 </n-button>
|
||||
<n-button @click="notify('info')">信息</n-button>
|
||||
<n-button @click="notify('success')">成功</n-button>
|
||||
<n-button @click="notify('warning')">警告</n-button>
|
||||
<n-button @click="notify('error')">错误</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<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 min-w-340 title="消息提醒 Message">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div w-350>
|
||||
<n-input v-model:value="inputVal" />
|
||||
<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>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
@@ -13,7 +13,7 @@ export default {
|
||||
children: [
|
||||
{
|
||||
name: 'BaseComponents',
|
||||
path: 'idnex',
|
||||
path: 'index',
|
||||
component: () => import('./index.vue'),
|
||||
meta: {
|
||||
title: '基础组件',
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<CommonPage show-footer>
|
||||
<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>
|
||||
playground:
|
||||
@@ -10,57 +13,57 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div f-c-c mt-20 w-350 rounded-10 b-1 bc-ccc>
|
||||
<div flex w-360 flex-wrap justify-around p-10>
|
||||
<div w-50 h-50 b-1 rounded-5 f-c-c p-10 m-20>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<div mt-20 w-350 f-c-c flex-col>
|
||||
<div flex flex-wrap justify-around rounded-10 p-10 border="1 solid #ccc">
|
||||
<div m-20 h-50 w-50 f-c-c rounded-5 p-10 border="1 solid">
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black self-end></span>
|
||||
<div m-20 h-50 w-50 flex justify-between rounded-5 p-10 border="1 solid">
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 self-end rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black self-center></span>
|
||||
<span w-6 h-6 rounded-3 bg-black self-end></span>
|
||||
<div m-20 h-50 w-50 flex justify-between rounded-5 p-10 border="1 solid">
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 self-center rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 self-end rounded-3 bg-black dark:bg-white />
|
||||
</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>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
<div flex-col justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 f-c-c flex-col p-10 m-20>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<div m-20 h-50 w-50 flex-col items-center justify-between rounded-5 p-10 border="1 solid">
|
||||
<div w-full flex justify-between>
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<div h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<div w-full flex justify-between>
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 flex-col justify-between p-10 m-20>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<div m-20 h-50 w-50 flex-col justify-between rounded-5 p-10 border="1 solid">
|
||||
<div w-full flex justify-between>
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<div w-full flex justify-between>
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<div w-full flex justify-between>
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 mt-10 text-14 font-normal color-gray>Flex 骰子</h2>
|
||||
</div>
|
||||
<h2 font-normal text-14 text-center w-350 mt-10 color-gray>Flex 骰子</h2>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
@@ -1,31 +1,40 @@
|
||||
<template>
|
||||
<CommonPage :show-header="false" title="32323">
|
||||
<div h-60 pl-20 pr-20 flex items-center bg-white>
|
||||
<CommonPage>
|
||||
<div h-60 flex items-center bg-white pl-20 pr-20 dark:bg-dark>
|
||||
<input
|
||||
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"
|
||||
placeholder="输入文章标题..."
|
||||
/>
|
||||
<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>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MdEditor from 'md-editor-v3'
|
||||
import { MdEditor } from 'md-editor-v3'
|
||||
import 'md-editor-v3/lib/style.css'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
defineOptions({ name: 'MDEditor' })
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// refs
|
||||
let post = ref({})
|
||||
let btnLoading = ref(false)
|
||||
|
||||
function handleSavePost(e) {
|
||||
function handleSavePost() {
|
||||
btnLoading.value = true
|
||||
$message.loading('正在保存...')
|
||||
setTimeout(() => {
|
||||
|
||||
46
src/views/demo/editor/rich-text.vue
Normal file
46
src/views/demo/editor/rich-text.vue
Normal 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>
|
||||
@@ -4,18 +4,18 @@ export default {
|
||||
name: 'Demo',
|
||||
path: '/demo',
|
||||
component: Layout,
|
||||
redirect: '/demo/crud-table',
|
||||
redirect: '/demo/crud',
|
||||
meta: {
|
||||
title: '示例页面',
|
||||
customIcon: 'logo',
|
||||
icon: 'uil:pagelines',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
order: 3,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'CrudTable',
|
||||
path: 'crud-table',
|
||||
name: 'Crud',
|
||||
path: 'crud',
|
||||
component: () => import('./table/index.vue'),
|
||||
meta: {
|
||||
title: 'CRUD表格',
|
||||
@@ -37,5 +37,29 @@ export default {
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import request from '@/utils/http'
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
getPosts: (params = {}) => request.get('posts', { params }),
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<template>
|
||||
<CommonPage show-footer title="文章">
|
||||
<template #action>
|
||||
<n-button type="primary" @click="handleAdd">
|
||||
<TheIcon icon="material-symbols:add" :size="18" class="mr-5" /> 新建文章
|
||||
<div>
|
||||
<n-button type="primary" secondary @click="$table?.handleExport()">
|
||||
<TheIcon icon="mdi:download" :size="18" class="mr-5" />
|
||||
导出
|
||||
</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>
|
||||
|
||||
<CrudTable
|
||||
@@ -14,6 +21,7 @@
|
||||
:columns="columns"
|
||||
:get-data="api.getPosts"
|
||||
@on-checked="onChecked"
|
||||
@on-data-change="(data) => (tableData = data)"
|
||||
>
|
||||
<template #queryBar>
|
||||
<QueryBarItem label="标题" :label-width="50">
|
||||
@@ -21,7 +29,7 @@
|
||||
v-model:value="queryItems.title"
|
||||
type="text"
|
||||
placeholder="请输入标题"
|
||||
@keydown.enter="$table?.handleSearch"
|
||||
@keypress.enter="$table?.handleSearch"
|
||||
/>
|
||||
</QueryBarItem>
|
||||
</template>
|
||||
@@ -82,21 +90,21 @@
|
||||
|
||||
<script setup>
|
||||
import { NButton, NSwitch } from 'naive-ui'
|
||||
import { formatDateTime } from '@/utils'
|
||||
import { renderIcon } from '@/utils/icon'
|
||||
import { formatDateTime, renderIcon, isNullOrUndef } from '@/utils'
|
||||
import { useCRUD } from '@/composables'
|
||||
import api from './api'
|
||||
import { isNullOrUndef } from '@/utils/is'
|
||||
|
||||
defineOptions({ name: 'CrudTable' })
|
||||
defineOptions({ name: 'Crud' })
|
||||
|
||||
const $table = ref(null)
|
||||
/** 表格数据,触发搜索的时候会更新这个值 */
|
||||
const tableData = ref([])
|
||||
/** QueryBar筛选参数(可选) */
|
||||
const queryItems = ref({})
|
||||
/** 补充参数(可选) */
|
||||
const extraParams = ref({})
|
||||
|
||||
onMounted(() => {
|
||||
onActivated(() => {
|
||||
$table.value?.handleSearch()
|
||||
})
|
||||
|
||||
@@ -143,6 +151,7 @@ const columns = [
|
||||
width: 240,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
hideInExcel: true,
|
||||
render(row) {
|
||||
return [
|
||||
h(
|
||||
@@ -174,7 +183,10 @@ const columns = [
|
||||
style: 'margin-left: 15px;',
|
||||
onClick: () => handleDelete(row.id),
|
||||
},
|
||||
{ default: () => '删除', icon: renderIcon('material-symbols:delete-outline', { size: 14 }) }
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: renderIcon('material-symbols:delete-outline', { size: 14 }),
|
||||
}
|
||||
),
|
||||
]
|
||||
},
|
||||
|
||||
84
src/views/demo/upload/index.vue
Normal file
84
src/views/demo/upload/index.vue
Normal 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(``)">
|
||||
MD
|
||||
</n-button>
|
||||
<n-button
|
||||
dashed
|
||||
type="primary"
|
||||
@click="copy(`<img src="${item.url}" />`)"
|
||||
>
|
||||
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>
|
||||
@@ -1,4 +1,4 @@
|
||||
import request from '@/utils/http'
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
login: (data) => request.post('/auth/login', data, { noNeedToken: true }),
|
||||
|
||||
@@ -2,41 +2,65 @@
|
||||
<AppPage :show-footer="true" bg-cover :style="{ backgroundImage: `url(${bgImg})` }">
|
||||
<div
|
||||
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" />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div mt-30>
|
||||
<h5 f-c-c text-24 font-normal color="#6a6a6a">
|
||||
<img src="@/assets/images/logo.png" height="50" class="mr-10" />
|
||||
{{ title }}
|
||||
</h5>
|
||||
<div mt-32>
|
||||
<n-input
|
||||
v-model:value="loginInfo.name"
|
||||
autofocus
|
||||
class="text-16 items-center h-50 pl-10"
|
||||
placeholder="admin"
|
||||
class="h-48 items-center text-16"
|
||||
placeholder="name"
|
||||
:maxlength="20"
|
||||
/>
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-material-symbols:account-circle-outline class="mr-8 text-20 opacity-40" />
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
<div mt-30>
|
||||
<div mt-32>
|
||||
<n-input
|
||||
v-model:value="loginInfo.password"
|
||||
class="text-16 items-center h-50 pl-10"
|
||||
class="h-48 items-center text-16"
|
||||
type="password"
|
||||
show-password-on="mousedown"
|
||||
placeholder="123456"
|
||||
placeholder="password"
|
||||
:maxlength="20"
|
||||
@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 mt-20>
|
||||
<n-checkbox :checked="isRemember" label="记住我" :on-update:checked="(val) => (isRemember = val)" />
|
||||
</div>
|
||||
|
||||
<div mt-20>
|
||||
<n-button w-full h-50 rounded-5 text-16 type="primary" :loading="loading" @click="handleLogin">
|
||||
<n-button
|
||||
h-50
|
||||
w-full
|
||||
rounded-5
|
||||
text-16
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</n-button>
|
||||
</div>
|
||||
@@ -46,8 +70,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { lStorage } from '@/utils/cache'
|
||||
import { setToken } from '@/utils/token'
|
||||
import { lStorage, setToken } from '@/utils'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import bgImg from '@/assets/images/login_bg.webp'
|
||||
import api from './api'
|
||||
|
||||
3
src/views/multi-menu/a-1/a-1-1/index.vue
Normal file
3
src/views/multi-menu/a-1/a-1-1/index.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>a-1-1</div>
|
||||
</template>
|
||||
3
src/views/multi-menu/a-1/a-1-2/index.vue
Normal file
3
src/views/multi-menu/a-1/a-1-2/index.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>a-1-2</div>
|
||||
</template>
|
||||
8
src/views/multi-menu/a-1/index.vue
Normal file
8
src/views/multi-menu/a-1/index.vue
Normal 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
Reference in New Issue
Block a user