Repository: zclzone/qs-admin
Branch: main
Commit: 19784f20dbe9
Files: 111
Total size: 102.7 KB
Directory structure:
gitextract_7z28qrgi/
├── .cz-config.js
├── .gitignore
├── .husky/
│ ├── commit-msg
│ └── pre-commit
├── .vscode/
│ ├── extensions.json
│ └── settings.json
├── LICENSE
├── README.md
├── README.zh-CN.md
├── build/
│ ├── config/
│ │ ├── define.ts
│ │ ├── index.ts
│ │ └── proxy.ts
│ ├── plugins/
│ │ ├── html.ts
│ │ ├── index.ts
│ │ ├── mock.ts
│ │ └── unplugin.ts
│ └── utils.ts
├── commitlint.config.js
├── index.html
├── mock/
│ ├── api/
│ │ ├── auth.js
│ │ ├── index.js
│ │ ├── post.js
│ │ └── user.js
│ ├── index.js
│ └── utils.js
├── package.json
├── public/
│ └── loading/
│ ├── index.css
│ └── index.js
├── settings/
│ ├── proxy-config.ts
│ └── theme.json
├── src/
│ ├── App.vue
│ ├── api/
│ │ └── index.ts
│ ├── components/
│ │ ├── common/
│ │ │ ├── AppFooter.vue
│ │ │ ├── AppProvider.vue
│ │ │ └── ScrollX.vue
│ │ ├── custom/
│ │ │ ├── CustomIcon.vue
│ │ │ ├── SvgIcon.vue
│ │ │ └── TheIcon.vue
│ │ └── page/
│ │ ├── AppPage.vue
│ │ └── CommonPage.vue
│ ├── layout/
│ │ ├── AppMain.vue
│ │ ├── header/
│ │ │ ├── components/
│ │ │ │ ├── BreadCrumb.vue
│ │ │ │ ├── FullScreen.vue
│ │ │ │ ├── GithubSite.vue
│ │ │ │ ├── MenuCollapse.vue
│ │ │ │ ├── ThemeMode.vue
│ │ │ │ └── UserAvatar.vue
│ │ │ └── index.vue
│ │ ├── index.vue
│ │ ├── sidebar/
│ │ │ ├── components/
│ │ │ │ ├── SideLogo.vue
│ │ │ │ └── SideMenu.vue
│ │ │ └── index.vue
│ │ └── tab/
│ │ ├── components/
│ │ │ └── ContextMenu.vue
│ │ └── index.vue
│ ├── main.ts
│ ├── router/
│ │ ├── guard/
│ │ │ ├── index.ts
│ │ │ ├── page-loading-guard.ts
│ │ │ ├── page-title-guard.ts
│ │ │ └── permission-guard.ts
│ │ ├── index.ts
│ │ └── routes/
│ │ └── index.ts
│ ├── store/
│ │ ├── index.ts
│ │ └── modules/
│ │ ├── app/
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── permission/
│ │ │ ├── helpers.ts
│ │ │ └── index.ts
│ │ ├── tab/
│ │ │ ├── helpers.ts
│ │ │ └── index.ts
│ │ ├── theme/
│ │ │ ├── helpers.ts
│ │ │ └── index.ts
│ │ └── user/
│ │ └── index.ts
│ ├── styles/
│ │ ├── index.scss
│ │ └── reset.css
│ ├── utils/
│ │ ├── auth/
│ │ │ ├── index.ts
│ │ │ ├── router.ts
│ │ │ └── token.ts
│ │ ├── common/
│ │ │ ├── color.ts
│ │ │ ├── common.ts
│ │ │ ├── crypto.ts
│ │ │ ├── icon.ts
│ │ │ ├── index.ts
│ │ │ ├── is.ts
│ │ │ └── naiveTools.ts
│ │ ├── http/
│ │ │ ├── helpers.ts
│ │ │ ├── index.ts
│ │ │ └── interceptors.ts
│ │ ├── index.ts
│ │ └── storage/
│ │ ├── index.ts
│ │ ├── local.ts
│ │ └── session.ts
│ └── views/
│ ├── demo/
│ │ ├── animation/
│ │ │ └── index.vue
│ │ ├── route.ts
│ │ ├── table/
│ │ │ ├── api.ts
│ │ │ └── index.vue
│ │ └── unocss/
│ │ └── index.vue
│ ├── error-page/
│ │ ├── 404.vue
│ │ └── route.ts
│ ├── login/
│ │ ├── api.ts
│ │ └── index.vue
│ └── workbench/
│ ├── index.vue
│ └── route.ts
├── tsconfig.json
├── types/
│ ├── axios.d.ts
│ ├── env.d.ts
│ ├── global.d.ts
│ ├── router.d.ts
│ ├── shims.d.ts
│ └── theme.d.ts
├── uno.config.ts
├── vercel.json
└── vite.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .cz-config.js
================================================
module.exports = {
types: [
{ value: 'feat', name:'feat: 新增功能' },
{ value: 'fix', name:'fix: 修复bug' },
{ value: 'docs', name:'docs: 文档变更' },
{ value: 'style', name:'style: 代码格式(不影响功能,例如空格、分号等格式修正)' },
{ value: 'refactor', name:'refactor: 代码重构(不包括 bug 修复、功能新增)' },
{ value: 'perf', name:'perf: 性能优化' },
{ value: 'test', name:'test: 添加、修改测试用例' },
{ value: 'build', name:'build: 构建流程、外部依赖变更(如升级 npm 包、修改 脚手架 配置等)' },
{ value: 'ci', name:'ci: 修改 CI 配置、脚本' },
{ value: 'chore', name:'chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)' },
{ value: 'revert', name:'revert: 回滚 commit' },
{ value: 'wip', name:'wip: 开发中' },
{ value: 'mod', name:'mod: 不确定分类的修改' },
],
scopes: [
['custom', '自定义'],
['projects', '项目搭建'],
['components', '组件相关'],
['utils', 'utils 相关'],
['styles', '样式相关'],
['deps', '项目依赖'],
['other', '其他修改'],
].map(([value, description]) => {
return {
value,
name: `${value.padEnd(30)} (${description})`
}
}),
messages: {
type: '确保本次提交遵循 Angular 规范!选择你要提交的类型:\n',
scope: '选择一个 scope(可选):',
customScope: '请输入自定义的 scope:',
subject: '填写简短精炼的变更描述:',
body: '填写更加详细的变更描述(可选)。使用 "|" 换行:',
breaking: '列举非兼容性重大的变更(可选):',
footer: '列举出所有变更的 Issues Closed(可选)。 例如: #31, #34:',
confirmCommit: '确认提交?'
},
allowBreakingChanges: ['feat', 'fix'],
subjectLimit: 100,
breaklineChar: '|'
}
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
stats.html
types/components.d.ts
types/auto-imports.d.ts
================================================
FILE: .husky/commit-msg
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint --edit
================================================
FILE: .husky/pre-commit
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint:staged
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"vue.volar",
"sdras.vue-vscode-snippets",
"antfu.unocss",
"esbenp.prettier-vscode",
"christian-kohler.path-intellisense",
"dbaeumer.vscode-eslint",
"mikestead.dotenv",
"antfu.iconify"
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"path-intellisense.mappings": {
"@/": "${workspaceRoot}/src",
"~/": "${workspaceRoot}"
},
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"prettier.printWidth": 120,
"prettier.singleQuote": true,
"prettier.semi": false,
"prettier.endOfLine": "lf",
"files.eol": "\n",
"[javascript]": {
"editor.formatOnSave": false
},
"[typescript]": {
"editor.formatOnSave": false
},
"[typescriptreact]": {
"editor.formatOnSave": false
},
"[vue]": {
"editor.formatOnSave": false
},
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"files.associations": {
"*.env.*": "dotenv",
"*.css": "postcss"
}
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 Ronnie Zhang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
English |
简体中文
> Due to the author's limited energy, the ts version is no longer maintained. Rrecommended to use the js version.
js version: https://github.com/zclzone/vue-naive-admin
### Introduction
[Qs Admin](https://github.com/zclzone/qs-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、TypeScript、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://admin-ts.isme.top](https://admin-ts.isme.top)
[https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
### Getting Started
```shell
# Recommended setup git autocrlf 为 false
git config --global core.autocrlf false
# Clone Project
git clone https://github.com/zclzone/qs-admin.git
cd qs-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
```
### JS Version: Vue Naive Admin
#### Source code
- gitub: [https://github.com/zclzone/vue-naive-admin](https://github.com/zclzone/vue-naive-admin)
- gitee: [https://gitee.com/zclzone/vue-naive-admin](https://gitee.com/zclzone/vue-naive-admin)
#### Preview
- [https://template.isme.top](https://template.isme.top)
- [https://zclzone.github.io/vue-naive-admin](https://zclzone.github.io/vue-naive-admin)
### Communication group & About the author
================================================
FILE: README.zh-CN.md
================================================
简体中文 |
English
> 由于作者精力有限,ts版本不再维护,推荐使用js版本。
js 版本: https://github.com/zclzone/vue-naive-admin
### 简介
[Qs Admin](https://github.com/zclzone/qs-admin) 是一个 **完全开源免费且允许商用** 的后台管理模板,基于 `Vue3、Vite3、TypeScript、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://admin.isme.top](https://admin.isme.top)
[https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-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/qs-admin.git
# 进入项目目录
cd qs-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
```
### JS 版本: Vue Naive Admin
#### 源码
- gitub: [https://github.com/zclzone/vue-naive-admin](https://github.com/zclzone/vue-naive-admin)
- gitee: [https://gitee.com/zclzone/vue-naive-admin](https://gitee.com/zclzone/vue-naive-admin)
#### 预览
- [https://template.isme.top](https://template.isme.top)
- [https://zclzone.github.io/vue-naive-admin](https://zclzone.github.io/vue-naive-admin)
### 入群交流 & 关于作者
================================================
FILE: build/config/define.ts
================================================
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_,
}
================================================
FILE: build/config/index.ts
================================================
export * from './define'
export * from './proxy'
================================================
FILE: build/config/proxy.ts
================================================
import type { ProxyOptions } from 'vite'
import { getProxyConfig } from '../../settings/proxy-config'
export function createViteProxy(isUseProxy = true, proxyType: ProxyType) {
if (!isUseProxy)
return undefined
const proxyConfig = getProxyConfig(proxyType)
const proxy: Record = {
[proxyConfig.prefix]: {
target: proxyConfig.target,
changeOrigin: true,
rewrite: (path: string) => path.replace(new RegExp(`^${proxyConfig.prefix}`), ''),
},
}
return proxy
}
================================================
FILE: build/plugins/html.ts
================================================
import { createHtmlPlugin } from 'vite-plugin-html'
export function setupHtmlPlugin(viteEnv: ViteEnv) {
const { VITE_APP_TITLE } = viteEnv
const htmlPlugin = createHtmlPlugin({
minify: true,
inject: {
data: {
title: VITE_APP_TITLE,
},
},
})
return htmlPlugin
}
================================================
FILE: build/plugins/index.ts
================================================
import type { PluginOption } from 'vite'
import vue from '@vitejs/plugin-vue'
import unocss from 'unocss/vite'
import { visualizer } from 'rollup-plugin-visualizer'
import viteCompression from 'vite-plugin-compression'
import unplugins from './unplugin'
import { setupHtmlPlugin } from './html'
import { setupMockPlugin } from './mock'
export function setupVitePlugins(viteEnv: ViteEnv, isBuild: boolean): PluginOption[] {
const plugins = [vue(), ...unplugins, unocss(), setupHtmlPlugin(viteEnv)]
if (viteEnv.VITE_USE_MOCK)
plugins.push(setupMockPlugin(isBuild))
if (viteEnv.VITE_USE_COMPRESS) {
plugins.push(
viteCompression({ algorithm: viteEnv.VITE_COMPRESS_TYPE || 'gzip' }),
)
}
if (isBuild) {
plugins.push(
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
}),
)
}
return plugins
}
================================================
FILE: build/plugins/mock.ts
================================================
import { viteMockServe } from 'vite-plugin-mock'
export function setupMockPlugin(isBuild: boolean) {
return viteMockServe({
mockPath: 'mock/api',
localEnabled: !isBuild,
prodEnabled: isBuild,
injectCode: `
import { setupProdMockServer } from '../mock';
setupProdMockServer();
`,
})
}
================================================
FILE: build/plugins/unplugin.ts
================================================
import { resolve } from 'node:path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
import IconsResolver from 'unplugin-icons/resolver'
/**
* * unplugin-icons插件,自动引入iconify图标
* usage: https://github.com/antfu/unplugin-icons
* 图标库: https://icones.js.org/
*/
import Icons from 'unplugin-icons/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { getSrcPath } from '../utils'
const customIconPath = resolve(getSrcPath(), 'assets/svg')
export default [
AutoImport({
imports: ['vue', 'vue-router'],
dts: 'types/auto-imports.d.ts',
}),
Icons({
compiler: 'vue3',
customCollections: {
custom: FileSystemIconLoader(customIconPath),
},
scale: 1,
defaultClass: 'inline-block',
}),
Components({
resolvers: [NaiveUiResolver(), IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })],
dts: 'types/components.d.ts',
}),
createSvgIconsPlugin({
iconDirs: [customIconPath],
symbolId: 'icon-custom-[dir]-[name]',
inject: 'body-last',
customDomId: '__CUSTOM_SVG_ICON__',
}),
]
================================================
FILE: build/utils.ts
================================================
import path from 'node:path'
/**
* * 项目根路径
* @descrition 结尾不带/
*/
export function getRootPath() {
return path.resolve(process.cwd())
}
/**
* * 项目src路径
* @param srcName src目录名称(默认: "src")
* @descrition 结尾不带斜杠
*/
export function getSrcPath(srcName = 'src') {
return path.resolve(getRootPath(), srcName)
}
/**
* * 转换env配置
* @param envOptions
* @descrition boolean和数字类型转换
*/
export function convertEnv(envOptions: Record): ViteEnv {
const result: any = {}
if (!envOptions)
return result
for (const envKey in envOptions) {
let envVal = envOptions[envKey]
if (['true', 'false'].includes(envVal))
envVal = envVal === 'true'
if (['VITE_PORT'].includes(envKey))
envVal = +envVal
result[envKey] = envVal
}
return result
}
================================================
FILE: commitlint.config.js
================================================
module.exports = {
ignores: [commit => commit.includes('first commit')],
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat',
'fix',
'docs',
'style',
'refactor',
'perf',
'test',
'build',
'ci',
'chore',
'revert',
'wip',
'mod',
],
],
},
}
================================================
FILE: index.html
================================================
<%= title %>
<%= title %>
================================================
FILE: mock/api/auth.js
================================================
import { resolveToken } from '../utils'
const token = {
admin: 'admin',
editor: 'editor',
}
export default [
{
url: '/api/auth/login',
method: 'post',
response: (options) => {
const { name } = options.body
if (['admin', 'editor'].includes(name)) {
return {
code: 0,
data: {
token: token[name],
},
}
}
else {
return {
code: -1,
message: '没有此用户',
}
}
},
},
{
url: '/api/auth/refreshToken',
method: 'post',
response: ({ headers }) => {
return {
code: 0,
data: {
token: resolveToken(headers?.authorization),
},
}
},
},
]
================================================
FILE: mock/api/index.js
================================================
import auth from './auth'
import user from './user'
import table from './post'
export default [...auth, ...user, ...table]
================================================
FILE: mock/api/post.js
================================================
const posts = [
{
title: '使用纯css优雅配置移动端rem布局',
author: '大脸怪',
category: 'Css',
description: '通常配置rem布局会使用js进行处理,比如750的设计稿会这样...',
content: '通常配置rem布局会使用js进行处理,比如750的设计稿会这样',
isRecommend: true,
isPublish: true,
createDate: '2021-11-04T04:03:36.000Z',
updateDate: '2021-11-04T04:03:36.000Z',
},
{
title: 'Vue2&Vue3项目风格指南',
author: 'Ronnie',
category: 'Vue',
description: '总结的Vue2和Vue3的项目风格',
content: '### 1. 命名风格\n\n> 文件夹如果是由多个单词组成,应该始终是横线连接 ',
isRecommend: true,
isPublish: true,
createDate: '2021-10-25T08:57:47.000Z',
updateDate: '2022-02-28T04:02:39.000Z',
},
{
title: '如何优雅的给图片添加水印',
author: '大脸怪',
category: 'JavaScript',
description: '优雅的给图片添加水印',
content: '我之前写过一篇文章记录了一次上传图片的优化史',
isRecommend: true,
isPublish: true,
createDate: '2021-06-24T18:46:19.000Z',
updateDate: '2021-09-23T07:51:22.000Z',
},
{
title: '前端缓存的理解',
author: '大脸怪',
category: 'Http',
description: '谈谈前端缓存的理解',
content: '> 背景\n\n公司有个vue-cli3移动端web项目发版更新后发现部分用户手机在钉钉内置浏览器打开出现了缓存',
isRecommend: true,
isPublish: true,
createDate: '2021-06-10T18:51:19.000Z',
updateDate: '2021-09-17T09:33:24.000Z',
},
{
title: 'Promise的五个静态方法',
author: '大脸怪',
category: 'JavaScript',
description: '简单介绍下在 Promise 类中,有5 种静态方法及它们的使用场景',
content: '## 1. Promise.all\n\n并行执行多个 promise,并等待所有 promise 都准备就绪。再对它们进行处理。',
isRecommend: true,
isPublish: true,
createDate: '2021-02-22T22:37:06.000Z',
updateDate: '2021-09-17T09:33:24.000Z',
},
]
export default [
{
url: '/api/posts',
method: 'get',
response: (data = {}) => {
const { title, pageNo, pageSize } = data.query
let pageData = []
let total = 60
const filterData = posts.filter(item => item.title.includes(title) || (!title && title !== 0))
if (filterData.length) {
if (pageSize) {
while (pageData.length < pageSize)
pageData.push(filterData[Math.round(Math.random() * (filterData.length - 1))])
}
else {
pageData = filterData
}
pageData = pageData.map((item, index) => ({
id: pageSize * (pageNo - 1) + index + 1,
...item,
}))
}
else {
total = 0
}
return {
code: 0,
message: 'ok',
data: {
pageData,
total,
pageNo,
pageSize,
},
}
},
},
{
url: '/api/post',
method: 'post',
response: ({ body }) => {
return {
code: 0,
message: 'ok',
data: body,
}
},
},
{
url: '/api/post/:id',
method: 'put',
response: ({ query, body }) => {
return {
code: 0,
message: 'ok',
data: {
id: query.id,
body,
},
}
},
},
{
url: '/api/post/:id',
method: 'delete',
response: ({ query }) => {
return {
code: 0,
message: 'ok',
data: {
id: query.id,
},
}
},
},
]
================================================
FILE: mock/api/user.js
================================================
import { resolveToken } from '../utils'
const users = {
admin: {
id: 1,
name: '大脸怪(admin)',
avatar: 'https://static.isme.top/images/avatar.jpg',
email: 'Ronnie@123.com',
role: ['admin'],
},
editor: {
id: 2,
name: '大脸怪(editor)',
avatar: 'https://static.isme.top/images/avatar.jpg',
email: 'Ronnie@123.com',
role: ['editor'],
},
guest: {
id: 3,
name: '访客(guest)',
avatar: 'https://static.isme.top/images/avatar.jpg',
role: [],
},
}
export default [
{
url: '/api/user',
method: 'get',
response: ({ headers }) => {
const token = resolveToken(headers?.authorization)
return {
code: 0,
data: {
...(users[token] || users.guest),
},
}
},
},
]
================================================
FILE: mock/index.js
================================================
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
import api from './api'
export function setupProdMockServer() {
createProdMockServer(api)
}
================================================
FILE: mock/utils.js
================================================
export function resolveToken(authorization) {
/**
* * jwt token
* * Bearer + token
* ! 认证方案: Bearer
*/
const reqTokenSplit = authorization.split(' ')
if (reqTokenSplit.length === 2)
return reqTokenSplit[1]
return ''
}
================================================
FILE: package.json
================================================
{
"private": true,
"repository": {
"url": "https://github.com/zclzone"
},
"license": "MIT",
"author": {
"name": "Ronnie Zhang",
"email": "zclzone@outlook.com",
"url": "https://github.com/zclzone"
},
"scripts": {
"build": "vite build",
"build:github": "vite build --mode github",
"dev": "vite",
"lint": "eslint --ext .js,.ts,.vue .",
"lint:fix": "eslint --fix --ext .js,.ts,.vue .",
"lint:staged": "lint-staged",
"prepare": "husky install",
"preview": "vite preview"
},
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --ext .js,.ts,.vue ."
]
},
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
}
},
"eslintConfig": {
"extends": "@antfu"
},
"dependencies": {
"@vueuse/core": "^10.1.2",
"@zclzone/crud": "^0.0.13",
"axios": "^1.4.0",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.8",
"lodash-es": "^4.17.21",
"mockjs": "^1.1.0",
"pinia": "^2.1.4",
"vue": "^3.3.4",
"vue-router": "^4.2.2"
},
"devDependencies": {
"@antfu/eslint-config": "^0.39.5",
"@commitlint/cli": "^17.0.3",
"@commitlint/config-conventional": "^17.0.3",
"@iconify/json": "^2.1.78",
"@iconify/vue": "^3.2.1",
"@types/crypto-js": "^4.1.1",
"@types/lodash-es": "^4.17.6",
"@types/node": "^18.0.4",
"@vitejs/plugin-vue": "^4.2.3",
"colord": "^2.9.2",
"commitizen": "^4.2.5",
"cz-conventional-changelog": "^3.3.0",
"cz-customizable": "^6.9.1",
"eslint": "^8.19.0",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"naive-ui": "^2.34.4",
"rollup-plugin-visualizer": "^5.8.2",
"sass": "^1.53.0",
"typescript": "^4.6.4",
"unocss": "0.46.5",
"unplugin-auto-import": "^0.16.4",
"unplugin-icons": "^0.16.3",
"unplugin-vue-components": "^0.25.1",
"vite": "^4.3.9",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.0",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^0.38.9"
}
}
================================================
FILE: public/loading/index.css
================================================
.loading-container {
position: fixed;
left: 0;
top: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.loading-spin__container {
width: 56px;
height: 56px;
margin: 36px 0;
}
.loading-spin {
position: relative;
height: 100%;
animation: loadingSpin 1s linear infinite;
}
.left-0 {
left: 0;
}
.right-0 {
right: 0;
}
.top-0 {
top: 0;
}
.bottom-0 {
bottom: 0;
}
.loading-spin-item {
position: absolute;
height: 16px;
width: 16px;
background-color: var(--primary-color);
border-radius: 8px;
-webkit-animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes loadingSpin {
from {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loadingPulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.loading-delay-500 {
-webkit-animation-delay: 500ms;
animation-delay: 500ms;
}
.loading-delay-1000 {
-webkit-animation-delay: 1000ms;
animation-delay: 1000ms;
}
.loading-delay-1500 {
-webkit-animation-delay: 1500ms;
animation-delay: 1500ms;
}
.loading-title {
font-size: 28px;
font-weight: 500;
color: #6a6a6a;
}
================================================
FILE: public/loading/index.js
================================================
function addThemeColorCssVars() {
const key = '__THEME_COLOR__'
const defaultColor = '#316c72'
const themeColor = window.localStorage.getItem(key) || defaultColor
const cssVars = `--primary-color: ${themeColor}`
document.documentElement.style.cssText = cssVars
}
addThemeColorCssVars()
================================================
FILE: settings/proxy-config.ts
================================================
const proxyConfigMappings: Record = {
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: ProxyType = 'dev'): ProxyConfig {
return proxyConfigMappings[envType]
}
================================================
FILE: settings/theme.json
================================================
{
"isMobile": false,
"darkMode": false,
"sider": {
"width": 220,
"collapsedWidth": 64,
"collapsed": false
},
"tab": {
"visible": true,
"height": 50
},
"header": {
"visible": true,
"height": 60
},
"primaryColor": "#316c72",
"otherColor": {
"info": "#2080F0",
"success": "#18A058",
"warning": "#F0A020",
"error": "#D03050"
}
}
================================================
FILE: src/App.vue
================================================
================================================
FILE: src/api/index.ts
================================================
import { request } from '@/utils'
export default {
getUser: () => request.get('/user'),
refreshToken: () => request.post('/auth/refreshToken'),
}
================================================
FILE: src/components/common/AppFooter.vue
================================================
================================================
FILE: src/components/common/AppProvider.vue
================================================
================================================
FILE: src/components/common/ScrollX.vue
================================================
================================================
FILE: src/components/custom/CustomIcon.vue
================================================
================================================
FILE: src/components/custom/SvgIcon.vue
================================================
================================================
FILE: src/components/custom/TheIcon.vue
================================================
================================================
FILE: src/components/page/AppPage.vue
================================================
================================================
FILE: src/components/page/CommonPage.vue
================================================
{{ title || route.meta?.title }}
================================================
FILE: src/layout/AppMain.vue
================================================
================================================
FILE: src/layout/header/components/BreadCrumb.vue
================================================
{{ item.meta.title }}
================================================
FILE: src/layout/header/components/FullScreen.vue
================================================
================================================
FILE: src/layout/header/components/GithubSite.vue
================================================
================================================
FILE: src/layout/header/components/MenuCollapse.vue
================================================
================================================
FILE: src/layout/header/components/ThemeMode.vue
================================================
================================================
FILE: src/layout/header/components/UserAvatar.vue
================================================
{{ userStore.name }}
================================================
FILE: src/layout/header/index.vue
================================================
================================================
FILE: src/layout/index.vue
================================================
================================================
FILE: src/layout/sidebar/components/SideLogo.vue
================================================
{{ title }}
================================================
FILE: src/layout/sidebar/components/SideMenu.vue
================================================
================================================
FILE: src/layout/sidebar/index.vue
================================================
================================================
FILE: src/layout/tab/components/ContextMenu.vue
================================================
================================================
FILE: src/layout/tab/index.vue
================================================
{{ tab.title }}
================================================
FILE: src/main.ts
================================================
import '@/styles/reset.css'
import '@/styles/index.scss'
import 'uno.css'
import 'virtual:svg-icons-register'
import { createApp } from 'vue'
import App from './App.vue'
import { setupStore } from './store'
import { setupRouter } from './router'
import { setupNaiveDiscreteApi } from './utils'
async function setupApp() {
const app = createApp(App)
setupStore(app)
setupNaiveDiscreteApi()
await setupRouter(app)
app.mount('#app')
}
setupApp()
================================================
FILE: src/router/guard/index.ts
================================================
import type { Router } from 'vue-router'
import { createPageLoadingGuard } from './page-loading-guard'
import { createPageTitleGuard } from './page-title-guard'
import { createPermissionGuard } from './permission-guard'
export function setupRouterGuard(router: Router) {
createPageLoadingGuard(router)
createPermissionGuard(router)
createPageTitleGuard(router)
}
================================================
FILE: src/router/guard/page-loading-guard.ts
================================================
import type { Router } from 'vue-router'
export function createPageLoadingGuard(router: Router) {
router.beforeEach(() => {
window.$loadingBar?.start()
})
router.afterEach(() => {
setTimeout(() => {
window.$loadingBar?.finish()
}, 200)
})
router.onError(() => {
window.$loadingBar?.error()
})
}
================================================
FILE: src/router/guard/page-title-guard.ts
================================================
import type { Router } from 'vue-router'
const baseTitle: string = import.meta.env.VITE_APP_TITLE
export function createPageTitleGuard(router: Router) {
router.afterEach((to) => {
const pageTitle = to.meta?.title
if (pageTitle)
document.title = `${pageTitle} | ${baseTitle}`
else
document.title = baseTitle
})
}
================================================
FILE: src/router/guard/permission-guard.ts
================================================
import type { Router } from 'vue-router'
import { getToken, isNullOrWhitespace, refreshAccessToken } from '@/utils'
const WHITE_LIST = ['/login']
export function createPermissionGuard(router: Router) {
router.beforeEach(async (to) => {
const token = getToken()
/** 没有token的情况 */
if (isNullOrWhitespace(token)) {
if (WHITE_LIST.includes(to.path))
return true
return { path: 'login', query: { ...to.query, redirect: to.path } }
}
/** 有token的情况 */
if (to.path === '/login')
return { path: '/' }
refreshAccessToken()
return true
})
}
================================================
FILE: src/router/index.ts
================================================
import type { App } from 'vue'
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
import { setupRouterGuard } from './guard'
import { EMPTY_ROUTE, NOT_FOUND_ROUTE, basicRoutes } from './routes'
import { getToken, isNullOrWhitespace } from '@/utils'
import { usePermissionStore, useUserStore } from '@/store'
import type { RouteType, RoutesType } from '~/types/router'
const isHash = import.meta.env.VITE_USE_HASH === 'true'
export const router = createRouter({
history: isHash ? createWebHashHistory('/') : createWebHistory('/'),
routes: basicRoutes,
scrollBehavior: () => ({ left: 0, top: 0 }),
})
export async function setupRouter(app: App) {
await addDynamicRoutes()
setupRouterGuard(router)
app.use(router)
}
export async function addDynamicRoutes() {
const token = getToken()
// 没有token情况
if (isNullOrWhitespace(token)) {
router.addRoute(EMPTY_ROUTE)
return
}
// 有token的情况
try {
const userStore = useUserStore()
const permissionStore = usePermissionStore()
!userStore.userId && (await userStore.getUserInfo())
const accessRoutes = permissionStore.generateRoutes(userStore.role)
accessRoutes.forEach((route: RouteType) => {
!router.hasRoute(route.name) && router.addRoute(route)
})
router.hasRoute(EMPTY_ROUTE.name) && router.removeRoute(EMPTY_ROUTE.name)
router.addRoute(NOT_FOUND_ROUTE)
}
catch (error) {
console.error(error)
}
}
export async function resetRouter() {
const basicRouteNames = getRouteNames(basicRoutes)
router.getRoutes().forEach((route) => {
const name = route.name as string
if (!basicRouteNames.includes(name))
router.removeRoute(name)
})
}
export function getRouteNames(routes: RoutesType) {
return routes.map(route => getRouteName(route)).flat(1)
}
function getRouteName(route: RouteType) {
const names = [route.name]
if (route.children && route.children.length)
names.push(...route.children.map(item => getRouteName(item as RouteType)).flat(1))
return names
}
================================================
FILE: src/router/routes/index.ts
================================================
import type { RouteModule, RouteType, RoutesType } from '~/types/router'
const Layout = () => import('@/layout/index.vue')
export const basicRoutes: RoutesType = [
{
name: '404',
path: '/404',
component: () => import('@/views/error-page/404.vue'),
isHidden: true,
},
{
name: 'Login',
path: '/login',
component: () => import('@/views/login/index.vue'),
isHidden: true,
meta: {
title: '登录页',
},
},
{
name: 'ExternalLink',
path: '/external-link',
component: Layout,
meta: {
title: '外部链接',
icon: 'mdi:link-variant',
order: 3,
},
children: [
{
name: 'LinkGithubSrc',
path: 'https://github.com/zclzone/qs-admin',
component: () => {},
meta: {
title: '源码 - github',
icon: 'mdi:github',
},
},
{
name: 'LinkGiteeSrc',
path: 'https://gitee.com/zclzone/qs-admin-ts',
component: () => {},
meta: {
title: '源码 - gitee',
icon: 'simple-icons:gitee',
},
},
],
},
]
export const NOT_FOUND_ROUTE: RouteType = {
name: 'NotFound',
path: '/:pathMatch(.*)*',
redirect: '/404',
isHidden: true,
}
export const EMPTY_ROUTE: RouteType = {
name: 'Empty',
path: '/:pathMatch(.*)*',
component: () => {},
}
const modules = import.meta.glob('@/views/**/route.ts', { eager: true }) as RouteModule
const asyncRoutes: RoutesType = []
Object.keys(modules).forEach((key) => {
asyncRoutes.push(modules[key].default)
})
export { asyncRoutes }
================================================
FILE: src/store/index.ts
================================================
import { createPinia } from 'pinia'
import type { App } from 'vue'
export function setupStore(app: App) {
app.use(createPinia())
}
export * from './modules'
================================================
FILE: src/store/modules/app/index.ts
================================================
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state() {
return {
reloadFlag: true,
}
},
actions: {
async reloadPage() {
window.$loadingBar?.start()
this.reloadFlag = false
await nextTick()
this.reloadFlag = true
setTimeout(() => {
document.documentElement.scrollTo({ left: 0, top: 0 })
window.$loadingBar?.finish()
}, 100)
},
},
})
================================================
FILE: src/store/modules/index.ts
================================================
export * from './app'
export * from './permission'
export * from './tab'
export * from './theme'
export * from './user'
================================================
FILE: src/store/modules/permission/helpers.ts
================================================
import type { RouteType, RoutesType } from '~/types/router'
function hasPermission(route: RouteType, role: string[]) {
// * 不需要权限直接返回true
if (!route.meta?.requireAuth)
return true
const routeRole = route.meta?.role ? route.meta.role : []
// * 登录用户没有角色或者路由没有设置角色判定为没有权限
if (!role.length || !routeRole.length)
return false
// * 路由指定的角色包含任一登录用户角色则判定有权限
return role.some(item => routeRole.includes(item))
}
export function filterAsyncRoutes(routes: RoutesType = [], role: Array): RoutesType {
const ret: RoutesType = []
routes.forEach((route) => {
if (hasPermission(route, role)) {
const curRoute: RouteType = {
...route,
children: [],
}
if (route.children && route.children.length)
curRoute.children = filterAsyncRoutes(route.children, role) || []
else
Reflect.deleteProperty(curRoute, 'children')
ret.push(curRoute)
}
})
return ret
}
================================================
FILE: src/store/modules/permission/index.ts
================================================
import { defineStore } from 'pinia'
import { filterAsyncRoutes } from './helpers'
import { asyncRoutes, basicRoutes } from '@/router/routes'
import type { RoutesType } from '~/types/router'
export const usePermissionStore = defineStore('permission', {
state() {
return {
accessRoutes: [],
}
},
getters: {
routes(): RoutesType {
return basicRoutes.concat(this.accessRoutes)
},
menus(): RoutesType {
return this.routes.filter(route => route.name && !route.isHidden)
},
},
actions: {
generateRoutes(role: Array = []): RoutesType {
const accessRoutes = filterAsyncRoutes(asyncRoutes, role)
this.accessRoutes = accessRoutes
return accessRoutes
},
resetPermission() {
this.$reset()
},
},
})
================================================
FILE: src/store/modules/tab/helpers.ts
================================================
import { getSession } from '@/utils'
export const activeTab = getSession('activeTab')
export const tabs = getSession('tabs')
export const WITHOUT_TAB_PATHS = ['/404', '/login']
================================================
FILE: src/store/modules/tab/index.ts
================================================
import { defineStore } from 'pinia'
import { WITHOUT_TAB_PATHS, activeTab, tabs } from './helpers'
import { router } from '@/router'
import { setSession } from '@/utils'
export interface TabItem {
name: string
path: string
title?: string
}
export const useTabStore = defineStore('tab', {
state() {
return {
tabs: > tabs || [],
activeTab: activeTab || '',
}
},
actions: {
setActiveTab(path: string) {
this.activeTab = path
setSession('activeTab', path)
},
setTabs(tabs: Array) {
this.tabs = tabs
setSession('tabs', tabs)
},
addTab(tab: TabItem) {
this.setActiveTab(tab.path)
if (WITHOUT_TAB_PATHS.includes(tab.path) || this.tabs.some(item => item.path === tab.path))
return
this.setTabs([...this.tabs, tab])
},
removeTab(path: string) {
if (path === this.activeTab) {
const activeIndex = this.tabs.findIndex(item => item.path === path)
if (activeIndex > 0)
router.push(this.tabs[activeIndex - 1].path)
else
router.push(this.tabs[activeIndex + 1].path)
}
this.setTabs(this.tabs.filter(tab => tab.path !== path))
},
removeOther(curPath: string) {
this.setTabs(this.tabs.filter(tab => tab.path === curPath))
if (curPath !== this.activeTab)
router.push(this.tabs[this.tabs.length - 1].path)
},
removeLeft(curPath: string) {
const curIndex = this.tabs.findIndex(item => item.path === curPath)
const filterTabs = this.tabs.filter((item, index) => index >= curIndex)
this.setTabs(filterTabs)
if (!filterTabs.find(item => item.path === this.activeTab))
router.push(filterTabs[filterTabs.length - 1].path)
},
removeRight(curPath: string) {
const curIndex = this.tabs.findIndex(item => item.path === curPath)
const filterTabs = this.tabs.filter((item, index) => index <= curIndex)
this.setTabs(filterTabs)
if (!filterTabs.find(item => item.path === this.activeTab))
router.push(filterTabs[filterTabs.length - 1].path)
},
resetTabs() {
this.setTabs([])
this.setActiveTab('')
},
},
})
================================================
FILE: src/store/modules/theme/helpers.ts
================================================
import type { GlobalThemeOverrides } from 'naive-ui'
import themeSetting from '~/settings/theme.json'
import { addColorAlpha, getColorPalette } from '@/utils'
type ColorType = 'primary' | 'info' | 'success' | 'warning' | 'error'
type ColorScene = '' | 'Suppl' | 'Hover' | 'Pressed' | 'Active'
type ColorKey = `${ColorType}Color${ColorScene}`
type ThemeColor = Partial>
interface ColorAction {
scene: ColorScene
handler: (color: string) => string
}
/** 初始化主题配置 */
export function initThemeSettings(): Theme.Setting {
const isMobile = themeSetting.isMobile || false
const darkMode = themeSetting.darkMode || false
const sider = themeSetting.sider || { width: 220, collapsedWidth: 64, collapsed: false }
const header = themeSetting.header || { visible: true, height: 60 }
const tab = themeSetting.tab || { visible: true, height: 50 }
const primaryColor = themeSetting.primaryColor || '#316C72'
const otherColor = themeSetting.otherColor || {
info: '#0099ad',
success: '#52c41a',
warning: '#faad14',
error: '#f5222d',
}
return { isMobile, darkMode, sider, header, tab, primaryColor, otherColor }
}
/** 获取naive的主题颜色 */
export function getNaiveThemeOverrides(colors: Record): GlobalThemeOverrides {
const { primary, info, success, warning, error } = colors
const themeColors = getThemeColors([
['primary', primary],
['info', info],
['success', success],
['warning', warning],
['error', error],
])
const colorLoading = primary
return {
common: {
...themeColors,
},
LoadingBar: {
colorLoading,
},
}
}
/** 获取主题颜色的各种场景对应的颜色 */
function getThemeColors(colors: [ColorType, string][]) {
const colorActions: ColorAction[] = [
{ scene: '', handler: color => color },
{ scene: 'Suppl', handler: color => color },
{ scene: 'Hover', handler: color => getColorPalette(color, 5) },
{ scene: 'Pressed', handler: color => getColorPalette(color, 7) },
{ scene: 'Active', handler: color => addColorAlpha(color, 0.1) },
]
const themeColor: ThemeColor = {}
colors.forEach((color) => {
colorActions.forEach((action) => {
const [colorType, colorValue] = color
const colorKey: ColorKey = `${colorType}Color${action.scene}`
themeColor[colorKey] = action.handler(colorValue)
})
})
return themeColor
}
================================================
FILE: src/store/modules/theme/index.ts
================================================
import type { GlobalThemeOverrides } from 'naive-ui'
import { darkTheme } from 'naive-ui'
import type { BuiltInGlobalTheme } from 'naive-ui/es/themes/interface'
import { defineStore } from 'pinia'
import { getNaiveThemeOverrides, initThemeSettings } from './helpers'
type ThemeState = Theme.Setting
export const useThemeStore = defineStore('theme-store', {
state: (): ThemeState => initThemeSettings(),
getters: {
naiveThemeOverrides(): GlobalThemeOverrides {
return getNaiveThemeOverrides({ primary: this.primaryColor, ...this.otherColor })
},
naiveTheme(): BuiltInGlobalTheme | undefined {
return this.darkMode ? darkTheme : undefined
},
},
actions: {
setIsMobile(isMobile: boolean) {
this.isMobile = isMobile
},
/** 设置暗黑模式 */
setDarkMode(darkMode: boolean) {
this.darkMode = darkMode
},
/** 切换/关闭 暗黑模式 */
toggleDarkMode() {
this.darkMode = !this.darkMode
},
/** 切换/关闭 折叠侧边栏 */
toggleCollapsed() {
this.sider.collapsed = !this.sider.collapsed
},
/** 设置 折叠侧边栏 */
setCollapsed(collapsed: boolean) {
this.sider.collapsed = collapsed
},
/** 设置主题色 */
setPrimaryColor(color: string) {
this.primaryColor = color
},
},
})
================================================
FILE: src/store/modules/user/index.ts
================================================
import { defineStore } from 'pinia'
import { removeToken, toLogin } from '@/utils'
import { usePermissionStore, useTabStore } from '@/store'
import { resetRouter } from '@/router'
import api from '@/api'
interface UserInfo {
id?: string
name?: string
avatar?: string
role?: Array
}
export const useUserStore = defineStore('user', {
state() {
return {
userInfo: {},
}
},
getters: {
userId(): string {
return this.userInfo.id || ''
},
name(): string {
return this.userInfo.name || ''
},
avatar(): string {
return this.userInfo.avatar || ''
},
role(): Array {
return this.userInfo.role || []
},
},
actions: {
async getUserInfo() {
try {
const res: any = await api.getUser()
if (res.code === 0) {
const { id, name, avatar, role } = res.data
this.userInfo = { id, name, avatar, role }
return Promise.resolve(res.data)
}
else {
return Promise.reject(res)
}
}
catch (error) {
return Promise.reject(error)
}
},
async logout() {
const { resetTabs } = useTabStore()
const { resetPermission } = usePermissionStore()
removeToken()
resetPermission()
resetTabs()
resetRouter()
this.$reset()
toLogin()
},
setUserInfo(userInfo = {}) {
this.userInfo = { ...this.userInfo, ...userInfo }
},
},
})
================================================
FILE: src/styles/index.scss
================================================
html {
font-size: 4px; // * 方便unocss计算:1单位 = 0.25rem = 1px
}
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
background-color: #f2f2f2;
font-family: 'Encode Sans Condensed', sans-serif;
}
#app {
width: 100%;
height: 100%;
}
/* router view transition fade-slide */
.fade-slide-leave-active,
.fade-slide-enter-active {
transition: all 0.3s;
}
.fade-slide-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.fade-slide-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* 自定义滚动条样式 */
.cus-scroll {
overflow: auto;
&::-webkit-scrollbar {
width: 8;
height: 8px;
}
}
.cus-scroll-x {
overflow-x: auto;
&::-webkit-scrollbar {
width: 0;
height: 8px;
}
}
.cus-scroll-y {
overflow-y: auto;
&::-webkit-scrollbar {
width: 8px;
height: 0;
}
}
.cus-scroll,
.cus-scroll-x,
.cus-scroll-y {
&::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 4px;
}
&:hover {
&::-webkit-scrollbar-thumb {
background: #bfbfbf;
}
&::-webkit-scrollbar-thumb:hover {
background: var(--primary-color);
}
}
}
================================================
FILE: src/styles/reset.css
================================================
html {
box-sizing: border-box;
}
*,
::before,
::after {
margin: 0;
padding: 0;
box-sizing: inherit;
}
a {
text-decoration: none;
color: inherit;
}
a:hover,
a:link,
a:visited,
a:active {
text-decoration: none;
}
ol,
ul {
list-style: none;
}
input,
textarea {
outline: none;
border: none;
resize: none;
}
body {
font-size: 14px;
font-weight: 400;
}
================================================
FILE: src/utils/auth/index.ts
================================================
export * from './router'
export * from './token'
================================================
FILE: src/utils/auth/router.ts
================================================
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',
})
}
================================================
FILE: src/utils/auth/token.ts
================================================
import { getLocal, getLocalExpire, removeLocal, setLocal } from '@/utils'
import api from '@/api'
const TOKEN_CODE = 'access_token'
/** token过期时间:6小时 */
const DURATION = 6 * 60 * 60
export function getToken() {
return getLocal(TOKEN_CODE)
}
export function setToken(token: string) {
setLocal(TOKEN_CODE, token, DURATION)
}
export function removeToken() {
removeLocal(TOKEN_CODE)
}
export async function refreshAccessToken() {
const expire: number | null = getLocalExpire(TOKEN_CODE)
// * token没有过期时间或者token离过期时间超过30分钟则不执行刷新
if (!expire || expire - new Date().getTime() > 1000 * 60 * 30)
return
try {
const res: any = await api.refreshToken()
if (res.code === 0)
setToken(res.data.token)
}
catch {
// 无感刷新,有异常也不提示
}
}
================================================
FILE: src/utils/common/color.ts
================================================
import { colord, extend } from 'colord'
import mixPlugin from 'colord/plugins/mix'
import type { HsvColor } from 'colord'
extend([mixPlugin])
type ColorIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
const hueStep = 2
const saturationStep = 16
const saturationStep2 = 5
const brightnessStep1 = 5
const brightnessStep2 = 15
const lightColorCount = 5
const darkColorCount = 4
/**
* 根据颜色获取调色板颜色(从左至右颜色从浅到深,6为主色号)
* @param color - 颜色
* @param index - 调色板的对应的色号(6为主色号)
* @description 算法实现从ant-design调色板算法中借鉴 https://github.com/ant-design/ant-design/blob/master/components/style/color/colorPalette.less
*/
export function getColorPalette(color: string, index: ColorIndex) {
if (index === 6)
return color
const isLight = index < 6
const hsv = colord(color).toHsv()
const i = isLight ? lightColorCount + 1 - index : index - lightColorCount - 1
const newHsv: HsvColor = {
h: getHue(hsv, i, isLight),
s: getSaturation(hsv, i, isLight),
v: getValue(hsv, i, isLight),
}
return colord(newHsv).toHex()
}
/**
* 根据颜色获取调色板颜色所有颜色
* @param color - 颜色
*/
export function getAllColorPalette(color: string) {
const indexs: ColorIndex[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
return indexs.map(index => getColorPalette(color, index))
}
/**
* 获取色相渐变
* @param hsv - hsv格式颜色值
* @param i - 与6的相对距离
* @param isLight - 是否是亮颜色
*/
function getHue(hsv: HsvColor, i: number, isLight: boolean) {
let hue: number
if (hsv.h >= 60 && hsv.h <= 240) {
// 冷色调
// 减淡变亮 色相顺时针旋转 更暖
// 加深变暗 色相逆时针旋转 更冷
hue = isLight ? hsv.h - hueStep * i : hsv.h + hueStep * i
}
else {
// 暖色调
// 减淡变亮 色相逆时针旋转 更暖
// 加深变暗 色相顺时针旋转 更冷
hue = isLight ? hsv.h + hueStep * i : hsv.h - hueStep * i
}
if (hue < 0)
hue += 360
else if (hue >= 360)
hue -= 360
return hue
}
/**
* 获取饱和度渐变
* @param hsv - hsv格式颜色值
* @param i - 与6的相对距离
* @param isLight - 是否是亮颜色
*/
function getSaturation(hsv: HsvColor, i: number, isLight: boolean) {
let saturation: number
if (isLight)
saturation = hsv.s - saturationStep * i
else if (i === darkColorCount)
saturation = hsv.s + saturationStep
else
saturation = hsv.s + saturationStep2 * i
if (saturation > 100)
saturation = 100
if (isLight && i === lightColorCount && saturation > 10)
saturation = 10
if (saturation < 6)
saturation = 6
return saturation
}
/**
* 获取明度渐变
* @param hsv - hsv格式颜色值
* @param i - 与6的相对距离
* @param isLight - 是否是亮颜色
*/
function getValue(hsv: HsvColor, i: number, isLight: boolean) {
let value: number
if (isLight)
value = hsv.v + brightnessStep1 * i
else
value = hsv.v - brightnessStep2 * i
if (value > 100)
value = 100
return value
}
/**
* 给颜色加透明度
* @param color - 颜色
* @param alpha - 透明度(0 - 1)
*/
export function addColorAlpha(color: string, alpha: number) {
return colord(color).alpha(alpha).toHex()
}
/**
* 颜色混合
* @param firstColor - 第一个颜色
* @param secondColor - 第二个颜色
* @param ratio - 第二个颜色占比
*/
export function mixColor(firstColor: string, secondColor: string, ratio: number) {
return colord(firstColor).mix(secondColor, ratio).toHex()
}
/**
* 是否是白颜色
* @param color - 颜色
*/
export function isWhiteColor(color: string) {
return colord(color).isEqual('#ffffff')
}
================================================
FILE: src/utils/common/common.ts
================================================
import dayjs from 'dayjs'
type Time = undefined | string | Date
/** 格式化时间,默认格式:YYYY-MM-DD HH:mm:ss */
export function formatDateTime(time: Time, format = 'YYYY-MM-DD HH:mm:ss'): string {
return dayjs(time).format(format)
}
/** 格式化日期,默认格式:YYYY-MM-DD */
export function formatDate(date: Time = undefined, format = 'YYYY-MM-DD') {
return formatDateTime(date, format)
}
================================================
FILE: src/utils/common/crypto.ts
================================================
import CryptoJS from 'crypto-js'
const CryptoSecret = '__SecretKey__'
/**
* 加密数据
* @param data - 数据
*/
export function encrypto(data: any) {
const newData = JSON.stringify(data)
return CryptoJS.AES.encrypt(newData, CryptoSecret).toString()
}
/**
* 解密数据
* @param cipherText - 密文
*/
export function decrypto(cipherText: string) {
const bytes = CryptoJS.AES.decrypt(cipherText, CryptoSecret)
const originalText = bytes.toString(CryptoJS.enc.Utf8)
if (originalText)
return JSON.parse(originalText)
return null
}
================================================
FILE: src/utils/common/icon.ts
================================================
import { h } from 'vue'
import { Icon } from '@iconify/vue'
import { NIcon } from 'naive-ui'
import SvgIcon from '@/components/custom/SvgIcon.vue'
interface Props {
size?: number
color?: string
}
export function renderIcon(icon: string, props: Props = { size: 12 }) {
return () => h(NIcon, props, { default: () => h(Icon, { icon }) })
}
export function renderCustomIcon(icon: string, props: Props = { size: 12 }) {
return () => h(NIcon, props, { default: () => h(SvgIcon, { icon }) })
}
================================================
FILE: src/utils/common/index.ts
================================================
export * from './common'
export * from './color'
export * from './crypto'
export * from './icon'
export * from './is'
export * from './naiveTools'
================================================
FILE: src/utils/common/is.ts
================================================
const toString = Object.prototype.toString
export function is(val: unknown, type: string): boolean {
return toString.call(val) === `[object ${type}]`
}
export function isDef(val: any): boolean {
return typeof val !== 'undefined'
}
export function isUndef(val: any): boolean {
return typeof val === 'undefined'
}
export function isNull(val: any): boolean {
return val === null
}
export function isWhitespace(val: any): boolean {
return val === ''
}
export function isObject(val: any): boolean {
return !isNull(val) && is(val, 'Object')
}
export function isArray(val: any): boolean {
return val && Array.isArray(val)
}
export function isString(val: any): boolean {
return is(val, 'String')
}
export function isNumber(val: any): boolean {
return is(val, 'Number')
}
export function isBoolean(val: any): boolean {
return is(val, 'Boolean')
}
export function isDate(val: any): boolean {
return is(val, 'Date')
}
export function isRegExp(val: any): boolean {
return is(val, 'RegExp')
}
export function isFunction(val: any): boolean {
return typeof val === 'function'
}
export function isPromise(val: any): boolean {
return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch)
}
export function isElement(val: any): boolean {
return isObject(val) && !!val.tagName
}
/** null or undefined */
export function isNullOrUndef(val: any): boolean {
return isNull(val) || isUndef(val)
}
/** null or undefined or 空字符 */
export function isNullOrWhitespace(val: any): boolean {
return isNullOrUndef(val) || isWhitespace(val)
}
/** 空数组 or 空字符 or 空map or 空set or 空对象 */
export function isEmpty(val: any): boolean {
if (isArray(val) || isString(val))
return val.length === 0
if (val instanceof Map || val instanceof Set)
return val.size === 0
if (isObject(val))
return Object.keys(val).length === 0
return false
}
/**
* * 类似mysql的IFNULL函数
* @description 当第一个参数为null/undefined/'' 则返回第二个参数作为备用值,否则返回第一个参数
*/
export function ifNull(val: any, def: any = '') {
return isNullOrWhitespace(val) ? def : val
}
export function isUrl(path: string): boolean {
const reg
= /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/
return reg.test(path)
}
================================================
FILE: src/utils/common/naiveTools.ts
================================================
import * as NaiveUI from 'naive-ui'
import { useThemeStore } from '@/store'
export function setupNaiveDiscreteApi() {
const themeStore = useThemeStore()
const configProviderProps = computed(() => ({
theme: themeStore.naiveTheme,
themeOverrides: themeStore.naiveThemeOverrides,
}))
const { message, dialog, notification, loadingBar } = NaiveUI.createDiscreteApi(
['message', 'dialog', 'notification', 'loadingBar'],
{ configProviderProps },
)
window.$loadingBar = loadingBar
window.$notification = notification
window.$message = message
window.$dialog = dialog
}
================================================
FILE: src/utils/http/helpers.ts
================================================
import type { ErrorResolveResponse } from '~/types/axios'
import { useUserStore } from '@/store'
/** 自定义错误 */
export class AxiosRejectError extends Error {
code?: number | string
data?: any
constructor(rejectData: ErrorResolveResponse, options?: ErrorOptions) {
const { code, message, data } = rejectData
super(message, options)
this.code = code
this.data = data
}
}
export function resolveResError(code: number | string | undefined, message = ''): string {
switch (code) {
case 400:
message = message ?? '请求参数错误'
break
case 401:
message = message ?? '登录已过期'
useUserStore().logout()
break
case 403:
message = message ?? '没有权限'
break
case 404:
message = message ?? '资源或接口不存在'
break
case 500:
message = message ?? '服务器异常'
break
default:
message = message ?? `【${code}】: 未知异常!`
break
}
return message
}
================================================
FILE: src/utils/http/index.ts
================================================
import axios from 'axios'
import { reqReject, reqResolve, resReject, resResolve } from './interceptors'
export function createAxios(options = {}) {
const defaultOptions = {
timeout: 12000,
}
const service = axios.create({
...defaultOptions,
...options,
})
service.interceptors.request.use(reqResolve, reqReject)
service.interceptors.response.use(resResolve, resReject)
return service
}
export const request = createAxios({
baseURL: import.meta.env.VITE_BASE_API,
})
================================================
FILE: src/utils/http/interceptors.ts
================================================
import type { AxiosError, AxiosResponse } from 'axios'
import { AxiosRejectError, resolveResError } from './helpers'
import { getToken } from '~/src/utils/auth/token'
import type { RequestConfig } from '~/types/axios'
/** 请求拦截 */
export function reqResolve(config: RequestConfig) {
// 处理不需要token的请求
if (config.noNeedToken)
return config
const token = getToken()
if (!token)
return Promise.reject(new AxiosRejectError({ code: 401, message: '登录已过期,请重新登录!' }))
/**
* * 加上 token
* ! 认证方案: JWT Bearer
*/
const Authorization = config.headers?.Authorization || `Bearer ${token}`
if (config.headers)
config.headers.Authorization = config.headers.Authorization || `Bearer ${token}`
else
config.headers = { Authorization }
return config
}
/** 请求错误拦截 */
export function reqReject(error: AxiosError) {
return Promise.reject(error)
}
/** 响应拦截 */
export function resResolve(response: AxiosResponse) {
// 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)
const { noNeedTip } = config as RequestConfig
!noNeedTip && window.$message?.error(message)
return Promise.reject(new AxiosRejectError({ code, message, data: data || response }))
}
return Promise.resolve(data)
}
/** 响应错误拦截 */
export function resReject(error: AxiosError) {
if (!error || !error.response) {
const code = error?.code
/** 根据code处理对应的操作,并返回处理后的message */
const message = resolveResError(code, error.message)
window.$message?.error(message)
return Promise.reject(new AxiosRejectError({ code, message, data: error }))
}
const { data, status, config } = error.response
let { code, message } = data as AxiosRejectError
code = code ?? status
message = message ?? error.message
message = resolveResError(code, message)
/** 需要错误提醒 */
const { noNeedTip } = config as RequestConfig
!noNeedTip && window.$message?.error(message)
return Promise.reject(new AxiosRejectError({ code, message, data: error.response?.data || error.response }))
}
================================================
FILE: src/utils/index.ts
================================================
export * from './auth'
export * from './common'
export * from './http'
export * from './storage'
================================================
FILE: src/utils/storage/index.ts
================================================
export * from './local'
export * from './session'
================================================
FILE: src/utils/storage/local.ts
================================================
import { decrypto, encrypto } from '@/utils'
interface StorageData {
value: unknown
expire: number | null
}
/** 默认缓存期限为7天 */
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7
export function setLocal(key: string, value: unknown, expire: number | null = DEFAULT_CACHE_TIME) {
const storageData: StorageData = { value, expire: expire !== null ? new Date().getTime() + expire * 1000 : null }
const json = encrypto(storageData)
window.localStorage.setItem(key, json)
}
export function getLocal(key: string) {
const json = window.localStorage.getItem(key)
if (json) {
let storageData: StorageData | null = null
try {
storageData = decrypto(json)
}
catch {}
if (storageData) {
const { value, expire } = storageData
// 没有过期时间或者在有效期内则直接返回
if (expire === null || expire >= Date.now())
return value as T
}
removeLocal(key)
return null
}
return null
}
export function getLocalExpire(key: string): number | null {
const json = window.localStorage.getItem(key)
if (json) {
let storageData: StorageData | null = null
try {
storageData = decrypto(json)
}
catch {}
if (storageData) {
const { expire } = storageData
return expire
}
return null
}
return null
}
export function removeLocal(key: string) {
window.localStorage.removeItem(key)
}
export function clearLocal() {
window.localStorage.clear()
}
================================================
FILE: src/utils/storage/session.ts
================================================
import { decrypto, encrypto } from '@/utils'
export function setSession(key: string, value: unknown) {
const json = encrypto(value)
sessionStorage.setItem(key, json)
}
export function getSession(key: string) {
const json = sessionStorage.getItem(key)
let data: T | null = null
if (json) {
try {
data = decrypto(json)
}
catch {}
}
return data
}
export function removeSession(key: string) {
window.sessionStorage.removeItem(key)
}
export function clearSession() {
window.sessionStorage.clear()
}
================================================
FILE: src/views/demo/animation/index.vue
================================================
打乱
{{ item.num }}
================================================
FILE: src/views/demo/route.ts
================================================
import type { RouteType } from '~/types/router'
const Layout = () => import('@/layout/index.vue')
export default {
name: 'Demo',
path: '/demo',
component: Layout,
redirect: '/demo/unocss',
meta: {
title: '示例页面',
customIcon: 'logo',
role: ['admin'],
requireAuth: true,
order: 1,
},
children: [
{
name: 'Unocss',
path: 'unocss',
component: () => import('@/views/demo/unocss/index.vue'),
meta: {
title: 'unocss',
icon: 'logos:unocss',
role: ['admin'],
requireAuth: true,
},
},
{
name: 'Animation',
path: 'animation',
component: () => import('@/views/demo/animation/index.vue'),
meta: {
title: 'animation',
icon: 'clarity:animation-line',
role: ['admin'],
requireAuth: true,
},
},
{
name: 'Table',
path: 'table',
component: () => import('@/views/demo/table/index.vue'),
meta: {
title: '表格',
icon: 'mdi:table',
role: ['admin'],
requireAuth: true,
},
},
],
} as RouteType
================================================
FILE: src/views/demo/table/api.ts
================================================
import { request } from '@/utils'
export default {
getPosts: (params = {}) => request.get('posts', { params }),
getPostById: (id: string) => request.get(`/post/${id}`),
addPost: (data: any) => request.post('/post', data),
updatePost: (data: any) => request.put(`/post/${data.id}`, data),
deletePost: (id: string) => request.delete(`/post/${id}`),
}
================================================
FILE: src/views/demo/table/index.vue
================================================
导出
新建文章
================================================
FILE: src/views/demo/unocss/index.vue
================================================
文档:https://uno.antfu.me/
playground:
https://unocss.antfu.me/play/
================================================
FILE: src/views/error-page/404.vue
================================================
返回首页
================================================
FILE: src/views/error-page/route.ts
================================================
import type { RouteType } from '~/types/router'
const Layout = () => import('@/layout/index.vue')
export default {
name: 'ErrorPage',
path: '/error-page',
component: Layout,
redirect: '/error-page/404',
meta: {
title: 'ErrorPage',
order: 99,
icon: 'mdi:alert-circle-outline',
},
children: [
{
name: 'ERROR-404',
path: '404',
component: () => import('./404.vue'),
meta: {
title: '404',
icon: 'tabler:error-404',
},
},
],
} as RouteType
================================================
FILE: src/views/login/api.ts
================================================
import { request } from '@/utils'
import type { RequestConfig } from '~/types/axios'
export default {
login: (data = {}) => request.post('/auth/login', data, { noNeedToken: true } as RequestConfig),
}
================================================
FILE: src/views/login/index.vue
================================================
{{ title }}
登录
================================================
FILE: src/views/workbench/index.vue
================================================
更多
一个基于 Vue3、Vite3、TypeScript、Pinia、Unocss、Naive UI 的轻量级后台管理模板
================================================
FILE: src/views/workbench/route.ts
================================================
import type { RouteType } from '~/types/router'
const Layout = () => import('@/layout/index.vue')
export default {
name: 'Dashboard',
path: '/',
component: Layout,
redirect: '/workbench',
meta: {
order: 0,
},
children: [
{
name: 'Workbench',
path: 'workbench',
component: () => import('./index.vue'),
meta: {
title: '工作台',
icon: 'mdi:home',
},
},
],
} as RouteType
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": ".",
"module": "ESNext",
"target": "ESNext",
"lib": ["DOM", "ESNext"],
"strict": true,
"esModuleInterop": true,
"jsx": "preserve",
"moduleResolution": "node",
"resolveJsonModule": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"paths": {
"~/*": ["./*"],
"@/*": ["./src/*"]
},
"types": ["node", "vite/client", "unplugin-icons/types/vue"]
},
"exclude": ["node_modules", "dist"]
}
================================================
FILE: types/axios.d.ts
================================================
import type { AxiosRequestConfig } from 'axios'
interface RequestConfig extends AxiosRequestConfig {
/** 接口是否需要token */
noNeedToken?: boolean
/** 接口是否需要错误提醒 */
noNeedTip?: boolean
}
interface ErrorResolveResponse {
code?: number | string
message: string
data?: any
}
================================================
FILE: types/env.d.ts
================================================
type ProxyType = 'dev' | 'test' | 'prod';
interface ViteEnv {
VITE_PORT: number
VITE_USE_MOCK?: boolean
VITE_USE_PROXY?: boolean
VITE_USE_HASH?: boolean
VITE_APP_TITLE: string
VITE_PUBLIC_PATH: string
VITE_BASE_API: string
VITE_PROXY_TYPE?: ProxyType
VITE_USE_COMPRESS?: boolean
VITE_COMPRESS_TYPE?: 'gzip' | 'brotliCompress' | 'deflate' | 'deflateRaw'
}
interface ProxyConfig {
/** 匹配代理的前缀,接口地址匹配到此前缀将代理的target地址 */
prefix: string
/** 代理目标地址,后端真实接口地址 */
target: string
}
================================================
FILE: types/global.d.ts
================================================
interface Window {
$loadingBar?: import('naive-ui').LoadingBarProviderInst;
$dialog?: import('naive-ui').DialogProviderInst;
$message?: import('naive-ui').MessageProviderInst;
$notification?: import('naive-ui').NotificationProviderInst;
}
================================================
FILE: types/router.d.ts
================================================
import { RouteRecordRaw } from 'vue-router'
interface Meta {
title?: string
icon?: string
customIcon?: string
order?: number
role?: Array
requireAuth?: boolean
}
interface RouteItem {
name: string
path: string
redirect?: string
isHidden?: boolean
meta?: Meta,
children?: RoutesType
}
type RouteType = RouteRecordRaw & RouteItem
type RoutesType = Array
/** 前端导入的路由模块 */
type RouteModule = Record
================================================
FILE: types/shims.d.ts
================================================
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
declare module '@zclzone/crud'
================================================
FILE: types/theme.d.ts
================================================
/** 侧边栏 */
interface Sider {
width: number
/** 折叠时的宽度 */
collapsedWidth: number
/** 是否折叠 */
collapsed: boolean
}
/** 头部样式 */
interface Header {
/** 是否显示 */
visible: boolean
/** 头部高度 */
height: number;
}
/** 标多页签样式 */
interface Tab {
/** 是否显示 */
visible: boolean
/** 头部高度 */
height: number;
}
interface OtherColor {
/** 信息 */
info: string
/** 成功 */
success: string
/** 警告 */
warning: string
/** 错误 */
error: string
}
declare namespace Theme {
interface Setting {
isMobile: boolean
darkMode: boolean
sider: Sider
header: Header
tab: Tab
/** 主题颜色 */
primaryColor: string
otherColor: OtherColor
}
}
================================================
FILE: uno.config.ts
================================================
import { defineConfig, presetAttributify, presetUno } from 'unocss'
export default defineConfig({
exclude: ['node_modules', '.git', '.github', '.husky', '.vscode', 'build', 'dist', 'mock', 'public', 'types', './stats.html'],
presets: [presetUno({ dark: 'class' }), presetAttributify()],
shortcuts: [
['wh-full', 'w-full h-full'],
['f-c-c', 'flex justify-center items-center'],
['flex-col', 'flex flex-col'],
['absolute-lt', 'absolute left-0 top-0'],
['absolute-lb', 'absolute left-0 bottom-0'],
['absolute-rt', 'absolute right-0 top-0'],
['absolute-rb', 'absolute right-0 bottom-0'],
['absolute-center', 'absolute-lt f-c-c wh-full'],
['text-ellipsis', 'truncate'],
],
rules: [
[/^bc-(.+)$/, ([, color]) => ({ 'border-color': `#${color}` })],
['card-shadow', { 'box-shadow': '0 1px 2px -2px #00000029, 0 3px 6px #0000001f, 0 5px 12px 4px #00000017' }],
],
theme: {
colors: {
primary: 'var(--primary-color)',
primary_hover: 'var(--primary-color-hover)',
primary_pressed: 'var(--primary-color-pressed)',
primary_active: 'var(--primary-color-active)',
info: 'var(--info-color)',
info_hover: 'var(--info-color-hover)',
info_pressed: 'var(--info-color-pressed)',
info_active: 'var(--info-color-active)',
success: 'var(--success-color)',
success_hover: 'var(--success-color-hover)',
success_pressed: 'var(--success-color-pressed)',
success_active: 'var(--success-color-active)',
warning: 'var(--warning-color)',
warning_hover: 'var(--warning-color-hover)',
warning_pressed: 'var(--warning-color-pressed)',
warning_active: 'var(--warning-color-active)',
error: 'var(--error-color)',
error_hover: 'var(--error-color-hover)',
error_pressed: 'var(--error-color-pressed)',
error_active: 'var(--error-color-active)',
dark: '#18181c',
},
},
})
================================================
FILE: vercel.json
================================================
{
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}
================================================
FILE: vite.config.ts
================================================
import type { ConfigEnv } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import { convertEnv, getRootPath, getSrcPath } from './build/utils'
import { createViteProxy, viteDefine } from './build/config'
import { setupVitePlugins } from './build/plugins'
export default defineConfig((configEnv: ConfigEnv) => {
const srcPath = getSrcPath()
const rootPath = getRootPath()
const isBuild = configEnv.command === 'build'
const viteEnv = convertEnv(loadEnv(configEnv.mode, process.cwd()))
const { VITE_PORT, VITE_PUBLIC_PATH, VITE_USE_PROXY, VITE_PROXY_TYPE } = viteEnv
return {
base: VITE_PUBLIC_PATH,
resolve: {
alias: {
'~': rootPath,
'@': srcPath,
},
},
define: viteDefine,
plugins: setupVitePlugins(viteEnv, isBuild),
server: {
host: '0.0.0.0',
port: VITE_PORT,
open: false,
proxy: createViteProxy(VITE_USE_PROXY, VITE_PROXY_TYPE as ProxyType),
},
build: {
reportCompressedSize: false,
sourcemap: false,
chunkSizeWarningLimit: 1024, // chunk 大小警告的限制(单位kb)
commonjsOptions: {
ignoreTryCatch: false,
},
},
}
})