Full Code of zclzone/qs-admin for AI

main 19784f20dbe9 cached
111 files
102.7 KB
32.6k tokens
152 symbols
1 requests
Download .txt
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
================================================
<p align="center">
  <a href="https://github.com/zclzone/qs-admin">
    <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/qs-admin/actions"><img allt="checks" src="https://badgen.net/github/checks/zclzone/qs-admin"/></a>
  <a href="https://github.com/zclzone/qs-admin"><img allt="stars" src="https://badgen.net/github/stars/zclzone/qs-admin"/></a>
  <a href="https://github.com/zclzone/qs-admin"><img allt="forks" src="https://badgen.net/github/forks/zclzone/qs-admin"/></a>
  <a href="./LICENSE"><img allt="MIT License" src="https://badgen.net/github/license/zclzone/qs-admin"/></a>
</p>

<p align='center'>
  <b>English</b> | 
  <a href="https://github.com/zclzone/qs-admin/blob/main/README.zh-CN.md">简体中文</a>
</p>

> 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

<a href="https://blog.isme.top/about/">
  <img src="https://static.isme.top/images/about.png" style="max-width: 400px" />
</a>




================================================
FILE: README.zh-CN.md
================================================
<p align="center">
  <a href="https://github.com/zclzone/qs-admin">
    <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/qs-admin/actions"><img allt="checks" src="https://badgen.net/github/checks/zclzone/qs-admin"/></a>
  <a href="https://github.com/zclzone/qs-admin"><img allt="stars" src="https://badgen.net/github/stars/zclzone/qs-admin"/></a>
  <a href="https://github.com/zclzone/qs-admin"><img allt="forks" src="https://badgen.net/github/forks/zclzone/qs-admin"/></a>
  <a href="./LICENSE"><img allt="MIT License" src="https://badgen.net/github/license/zclzone/qs-admin"/></a>
</p>

<p align='center'>
  <b>简体中文</b> | 
  <a href="https://github.com/zclzone/qs-admin">English</a>
</p>

> 由于作者精力有限,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)

### 入群交流 & 关于作者

<a href="https://blog.isme.top/about/">
  <img src="https://static.isme.top/images/about.png" style="max-width: 400px" />
</a>




================================================
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<string, string | ProxyOptions> = {
    [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<string, any>): 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
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" href="/favicon.png" />
    <link rel="stylesheet" href="/loading/index.css" />

    <title><%= title %></title>
  </head>
  <body class="dark:text-white dark:bg-hex-121212">
    <div id="app">
      <!-- 白屏时的loading效果 -->
      <div class="loading-container">
        <img src="/loading/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>
            <div class="left-0 bottom-0 loading-spin-item loading-delay-500"></div>
            <div class="right-0 top-0 loading-spin-item loading-delay-1000"></div>
            <div class="right-0 bottom-0 loading-spin-item loading-delay-1500"></div>
          </div>
        </div>
        <div class="loading-title"><%= title %></div>
      </div>
      <script src="/loading/index.js"></script>
    </div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>


================================================
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<ProxyType, ProxyConfig> = {
  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
================================================
<script setup lang="ts">
import AppProvider from '@/components/common/AppProvider.vue'
</script>

<template>
  <AppProvider>
    <router-view v-slot="{ Component }">
      <component :is="Component" />
    </router-view>
  </AppProvider>
</template>


================================================
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
================================================
<template>
  <footer f-c-c flex-col text-14 color="#6a6a6a">
    <p>
      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"
      >
        赣ICP备2020015008号-1
      </a>
    </p>
  </footer>
</template>


================================================
FILE: src/components/common/AppProvider.vue
================================================
<script setup lang="ts">
import { kebabCase } from 'lodash-es'
import { useCssVar } from '@vueuse/core'
import type { GlobalThemeOverrides } from 'naive-ui'
import { useThemeStore } from '@/store'

type ThemeVars = Exclude<GlobalThemeOverrides['common'], undefined>
type ThemeVarsKeys = keyof ThemeVars

const themeStore = useThemeStore()

watch(
  () => themeStore.naiveThemeOverrides.common,
  (common) => {
    for (const key in common) {
      useCssVar(`--${kebabCase(key)}`, document.documentElement).value = common[key as ThemeVarsKeys] || ''
      if (key === 'primaryColor')
        window.localStorage.setItem('__THEME_COLOR__', common[key as ThemeVarsKeys] || '')
    }
  },
  { immediate: true },
)

watch(
  () => themeStore.darkMode,
  (newValue) => {
    if (newValue)
      document.documentElement.classList.add('dark')
    else
      document.documentElement.classList.remove('dark')
  },
  {
    immediate: true,
  },
)

function handleWindowResize() {
  themeStore.setIsMobile(document.body.offsetWidth <= 640)
}
onMounted(() => {
  handleWindowResize()
  window.addEventListener('resize', handleWindowResize)
})
onBeforeUnmount(() => {
  window.removeEventListener('resize', handleWindowResize)
})
</script>

<template>
  <n-config-provider wh-full :theme-overrides="themeStore.naiveThemeOverrides" :theme="themeStore.naiveTheme">
    <slot />
  </n-config-provider>
</template>


================================================
FILE: src/components/common/ScrollX.vue
================================================
<script setup lang="ts">
import { useDebounceFn } from '@vueuse/core'

interface Props {
  showArrow?: boolean
}

withDefaults(defineProps<Props>(), {
  showArrow: true,
})

const translateX = ref(0)
const content = ref<HTMLElement | null>(null)
const wrapper = ref<HTMLElement | null>(null)
const isOverflow = ref(false)

const resetTranslateX = useDebounceFn((wrapperWidth, contentWidth) => {
  if (!isOverflow.value)
    translateX.value = 0
  else if (-translateX.value > contentWidth - wrapperWidth)
    translateX.value = wrapperWidth - contentWidth
  else if (translateX.value > 0)
    translateX.value = 0
}, 200)

const refreshIsOverflow = useDebounceFn(() => {
  const wrapperWidth = wrapper.value?.offsetWidth || 0
  const contentWidth = content.value?.offsetWidth || 0
  isOverflow.value = contentWidth > wrapperWidth
  resetTranslateX(wrapperWidth, contentWidth)
}, 200)

function handleMouseWheel(e: { wheelDelta: number }) {
  const { wheelDelta } = e
  const wrapperWidth = wrapper.value?.offsetWidth || 0
  const contentWidth = content.value?.offsetWidth || 0
  /**
   * @wheelDelta 平行滚动的值 >0: 右移  <0: 左移
   * @translateX 内容translateX的值
   * @wrapperWidth 容器的宽度
   * @contentWidth 内容的宽度
   */
  if (wheelDelta < 0) {
    if (wrapperWidth > contentWidth && translateX.value < -10)
      return
    if (wrapperWidth <= contentWidth && contentWidth + translateX.value - wrapperWidth < -10)
      return
  }
  if (wheelDelta > 0 && translateX.value > 10)
    return

  translateX.value += wheelDelta
  resetTranslateX(wrapperWidth, contentWidth)
}

const observer = new MutationObserver(refreshIsOverflow)
onMounted(() => {
  refreshIsOverflow()

  window.addEventListener('resize', refreshIsOverflow)
  // 监听内容宽度刷新是否超出
  observer.observe(content.value!, { childList: true })
})
onBeforeUnmount(() => {
  window.removeEventListener('resize', refreshIsOverflow)
  observer.disconnect()
})
</script>

<template>
  <div ref="wrapper" class="wrapper" @mousewheel.prevent="handleMouseWheel">
    <template v-if="showArrow && isOverflow">
      <div class="left" @click="handleMouseWheel({ wheelDelta: 120 })">
        <icon-ic:baseline-keyboard-arrow-left />
      </div>
      <div class="right" @click="handleMouseWheel({ wheelDelta: -120 })">
        <icon-ic:baseline-keyboard-arrow-right />
      </div>
    </template>

    <div
      ref="content"
      class="content"
      :class="{ overflow: isOverflow && showArrow }"
      :style="{
        transform: `translateX(${translateX}px)`,
      }"
    >
      <slot />
    </div>
  </div>
</template>

<style lang="scss" scoped>
.wrapper {
  display: flex;

  z-index: 9;
  overflow: hidden;
  position: relative;
  .content {
    padding: 0 10px;
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    transition: transform 0.5s;
    &.overflow {
      padding-left: 30px;
      padding-right: 30px;
    }
  }
  .left,
  .right {
    background-color: #fff;
    position: absolute;
    top: 0;
    bottom: 0;
    margin: auto;

    width: 20px;
    height: 35px;
    display: flex;
    align-items: center;
    justify-content: center;

    font-size: 18px;
    border: 1px solid #e0e0e6;
    border-radius: 2px;

    z-index: 2;
    cursor: pointer;
  }
  .left {
    left: 0;
  }
  .right {
    right: 0;
  }
}
</style>


================================================
FILE: src/components/custom/CustomIcon.vue
================================================
<script setup lang="ts">
import { renderCustomIcon } from '@/utils'

interface Props {
  /** 图标名称(图片的文件名) */
  icon: string
  color?: string
  size?: number
}
const props = withDefaults(defineProps<Props>(), {
  size: 14,
})

const iconCom = computed(() => renderCustomIcon(props.icon, props))
</script>

<template>
  <component :is="iconCom" />
</template>


================================================
FILE: src/components/custom/SvgIcon.vue
================================================
<script setup lang="ts">
interface Props {
  /** 图标名称(图片的文件名) */
  icon: string
  /** 前缀 */
  prefix?: string
  color?: string
}
const props = withDefaults(defineProps<Props>(), {
  prefix: 'icon-custom',
  color: 'currentColor',
})

defineOptions({ name: 'SvgIcon' })

const symbolId = computed(() => `#${props.prefix}-${props.icon}`)
</script>

<template>
  <svg aria-hidden="true" width="1em" height="1em">
    <use :xlink:href="symbolId" :fill="color" />
  </svg>
</template>


================================================
FILE: src/components/custom/TheIcon.vue
================================================
<script setup lang="ts">
import { renderCustomIcon, renderIcon } from '@/utils'

const props = withDefaults(defineProps<Props>(), {
  size: 14,
  color: undefined,
  type: 'iconify',
})

interface Props {
  icon: string
  size?: number
  color?: string
  /** iconify | custom */
  type?: string
}

const iconCom = computed(() =>
  props.type === 'iconify'
    ? renderIcon(props.icon, { size: props.size, color: props.color })
    : renderCustomIcon(props.icon, { size: props.size, color: props.color }),
)
</script>

<template>
  <component :is="iconCom" />
</template>


================================================
FILE: src/components/page/AppPage.vue
================================================
<script setup lang="ts">
interface Props {
  showFooter?: boolean
}
withDefaults(defineProps<Props>(), {
  showFooter: false,
})
</script>

<template>
  <transition name="fade-slide" mode="out-in" appear>
    <section class="cus-scroll-y wh-full p-15 flex-col bg-[#f5f6fb]" dark:bg-hex-121212>
      <slot />
      <AppFooter v-if="showFooter" mt-15 />
    </section>
  </transition>
</template>


================================================
FILE: src/components/page/CommonPage.vue
================================================
<script setup lang="ts">
interface Props {
  showFooter?: boolean
  showHeader?: boolean
  title?: string
}
withDefaults(defineProps<Props>(), {
  showFooter: false,
  showHeader: true,
  title: undefined,
})

const route = useRoute()
</script>

<template>
  <AppPage :show-footer="showFooter">
    <header v-if="showHeader" px-15 mb-15 min-h-45 flex justify-between items-center>
      <slot v-if="$slots.header" name="header" />
      <template v-else>
        <h2 text-22 font-normal>
          {{ title || route.meta?.title }}
        </h2>
        <slot name="action" />
      </template>
    </header>

    <n-card rounded-10 flex-1>
      <slot />
    </n-card>
  </AppPage>
</template>


================================================
FILE: src/layout/AppMain.vue
================================================
<script setup lang="ts">
import { useAppStore } from '@/store'

const appStore = useAppStore()
</script>

<template>
  <router-view v-slot="{ Component, route }">
    <component :is="Component" v-if="appStore.reloadFlag" :key="route.path" />
  </router-view>
</template>


================================================
FILE: src/layout/header/components/BreadCrumb.vue
================================================
<script setup lang="ts">
import { renderCustomIcon, renderIcon } from '@/utils'
import type { Meta } from '~/types/router'

const router = useRouter()
const route = useRoute()

function handleBreadClick(path: string) {
  if (path === route.path)
    return
  router.push(path)
}

function getIcon(meta?: Meta, size = 16) {
  if (meta?.customIcon)
    return renderCustomIcon(meta.customIcon, { size })
  if (meta?.icon)
    return renderIcon(meta.icon, { size })
  return null
}
</script>

<template>
  <n-breadcrumb>
    <n-breadcrumb-item v-for="item in route.matched.filter(item => !!item.meta?.title)" :key="item.path" @click="handleBreadClick(item.path)">
      <component :is="getIcon(item.meta)" />
      {{ item.meta.title }}
    </n-breadcrumb-item>
  </n-breadcrumb>
</template>


================================================
FILE: src/layout/header/components/FullScreen.vue
================================================
<script setup lang="ts">
import { useFullscreen } from '@vueuse/core'

const { isFullscreen, toggle } = useFullscreen()
</script>

<template>
  <n-icon mr-20 cursor-pointer size="18" @click="toggle">
    <icon-ant-design:fullscreen-exit-outlined v-if="isFullscreen" />
    <icon-ant-design:fullscreen-outlined v-else />
  </n-icon>
</template>


================================================
FILE: src/layout/header/components/GithubSite.vue
================================================
<script setup lang="ts">
function handleLinkClick() {
  window.open('https://github.com/zclzone/qs-admin')
}
</script>

<template>
  <n-icon mr-20 size="18" style="cursor: pointer" @click="handleLinkClick">
    <icon-mdi:github />
  </n-icon>
</template>


================================================
FILE: src/layout/header/components/MenuCollapse.vue
================================================
<script setup lang="ts">
import { useThemeStore } from '@/store'

const themeStore = useThemeStore()
</script>

<template>
  <n-icon size="20" cursor-pointer @click="themeStore.toggleCollapsed()">
    <icon-mdi:format-indent-increase v-if="themeStore.sider.collapsed" />
    <icon-mdi:format-indent-decrease v-else />
  </n-icon>
</template>


================================================
FILE: src/layout/header/components/ThemeMode.vue
================================================
<script lang="ts" setup>
import { useThemeStore } from '@/store'

const theme = useThemeStore()
</script>

<template>
  <n-icon mr-20 cursor-pointer size="18" @click="theme.toggleDarkMode">
    <icon-mdi-moon-waning-crescent v-if="theme.darkMode" />
    <icon-mdi-white-balance-sunny v-else />
  </n-icon>
</template>


================================================
FILE: src/layout/header/components/UserAvatar.vue
================================================
<script setup lang="ts">
import { useUserStore } from '@/store'
import { renderIcon } from '@/utils'

const userStore = useUserStore()

const options = [
  {
    label: '退出登录',
    key: 'logout',
    icon: renderIcon('mdi:exit-to-app', { size: 14 }),
  },
]

function handleSelect(key: string) {
  if (key === 'logout') {
    window.$dialog?.info({
      content: '确认退出?',
      title: '提示',
      positiveText: '确定',
      negativeText: '取消',
      onPositiveClick() {
        userStore.logout()
        window.$message?.success('已退出登录!')
      },
    })
  }
}
</script>

<template>
  <n-dropdown :options="options" @select="handleSelect">
    <div flex items-center cursor-pointer>
      <img :src="userStore.avatar" mr10 w-35 h-35 rounded-full>
      <span hidden sm:block>{{ userStore.name }}</span>
    </div>
  </n-dropdown>
</template>


================================================
FILE: src/layout/header/index.vue
================================================
<script setup lang="ts">
import BreadCrumb from './components/BreadCrumb.vue'
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'
</script>

<template>
  <div flex items-center>
    <MenuCollapse />
    <BreadCrumb ml-15 hidden sm:block />
  </div>
  <div ml-auto flex items-center>
    <ThemeMode />
    <GithubSite />
    <FullScreen />
    <UserAvatar />
  </div>
</template>


================================================
FILE: src/layout/index.vue
================================================
<script setup lang="ts">
import SideBar from './sidebar/index.vue'
import AppHeader from './header/index.vue'
import AppTab from './tab/index.vue'
import AppMain from './AppMain.vue'

import { useThemeStore } from '@/store'

const themeStore = useThemeStore()
</script>

<template>
  <n-layout has-sider wh-full>
    <n-layout-sider
      v-if="!themeStore.isMobile"
      bordered
      collapse-mode="width"
      :collapsed-width="themeStore.sider.collapsedWidth"
      :width="themeStore.sider.width"
      :native-scrollbar="false"
      :collapsed="themeStore.sider.collapsed"
    >
      <SideBar />
    </n-layout-sider>
    <n-drawer
      v-else
      :width="themeStore.sider.width"
      :auto-focus="false"
      :show="!themeStore.sider.collapsed"
      placement="left"
      display-directive="show"
      @mask-click="themeStore.setCollapsed(true)"
    >
      <SideBar />
    </n-drawer>

    <article flex-1 flex-col overflow-hidden>
      <header
        bg-white px-15 border-b bc-eee flex items-center dark="bg-dark border-0"
        :style="`height: ${themeStore.header.height}px`"
      >
        <AppHeader />
      </header>
      <section v-if="themeStore.tab.visible" border-b bc-eee dark:border-0 hidden sm:block>
        <AppTab />
      </section>
      <section flex-1 overflow-hidden bg="#f5f6fb" dark:bg-hex-101014>
        <AppMain />
      </section>
    </article>
  </n-layout>
</template>


================================================
FILE: src/layout/sidebar/components/SideLogo.vue
================================================
<script setup lang="ts">
import { useThemeStore } from '@/store'

const title = import.meta.env.VITE_APP_TITLE

const themeStore = useThemeStore()
</script>

<template>
  <router-link class="h-60 f-c-c" to="/">
    <img src="@/assets/images/logo.png" height="42">
    <h2 v-show="!themeStore.sider.collapsed" class="ml-20 color-primary text-18 font-bold max-w-140 flex-shrink-0">
      {{ title }}
    </h2>
  </router-link>
</template>


================================================
FILE: src/layout/sidebar/components/SideMenu.vue
================================================
<script setup lang="ts">
import type { MenuInst, MenuOption } from 'naive-ui'
import type { Meta, RouteType } from '~/types/router'
import { useAppStore, usePermissionStore, useThemeStore } from '@/store'
import { isUrl, renderCustomIcon, renderIcon } from '@/utils'

const router = useRouter()
const currentRoute = useRoute()
const permissionStore = usePermissionStore()
const themeStore = useThemeStore()
const appStore = useAppStore()

const menu = ref<MenuInst>()
watch(currentRoute, async () => {
  await nextTick()
  menu.value?.showOption()
})

const menuOptions = computed(() => {
  return permissionStore.menus.map(item => getMenuItem(item)).sort((a, b) => a.order - b.order)
})

function resolvePath(basePath: string, path: string) {
  if (isUrl(path))
    return path
  return (
    `/${
    [basePath, path]
      .filter(path => !!path && path !== '/')
      .map(path => path.replace(/(^\/)|(\/$)/g, ''))
      .join('/')}`
  )
}

interface MenuItem {
  label: string
  key: string
  path: string
  icon: (() => import('vue').VNodeChild) | null
  order: number
  children?: Array<MenuItem>
}

function getMenuItem(route: RouteType, basePath = ''): MenuItem {
  let menuItem: MenuItem = {
    label: (route.meta && route.meta.title) || route.name,
    key: route.name,
    path: resolvePath(basePath, route.path),
    icon: getIcon(route.meta),
    order: route.meta?.order || 0,
  }

  const visibleChildren = route.children ? route.children.filter((item: RouteType) => item.name && !item.isHidden) : []

  if (!visibleChildren.length)
    return menuItem

  if (visibleChildren.length === 1) {
    // 单个子路由处理
    const singleRoute = visibleChildren[0]
    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: RouteType) => 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)
  }
  else {
    menuItem.children = visibleChildren
      .map(item => getMenuItem(item, menuItem.path))
      .sort((a, b) => a.order - b.order)
  }

  return menuItem
}

function getIcon(meta?: Meta): (() => import('vue').VNodeChild) | null {
  if (meta?.customIcon)
    return renderCustomIcon(meta.customIcon, { size: 18 })
  if (meta?.icon)
    return renderIcon(meta.icon, { size: 18 })
  return null
}

function handleMenuSelect(key: string, item: MenuOption) {
  const menuItem = item as MenuItem & MenuOption
  if (isUrl(menuItem.path)) {
    window.open(menuItem.path)
    return
  }
  if (menuItem.path === currentRoute.path && !currentRoute.meta?.keepAlive)
    appStore.reloadPage()
  else
    router.push(menuItem.path)

  // 手机端自动收起菜单
  themeStore.isMobile && themeStore.setCollapsed(true)
}
</script>

<template>
  <n-menu
    ref="menu"
    class="side-menu"
    accordion
    :indent="18"
    :collapsed-icon-size="22"
    :collapsed-width="64"
    :options="menuOptions"
    :value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name"
    @update:value="handleMenuSelect"
  />
</template>

<style lang="scss">
.side-menu:not(.n-menu--collapsed) {
  .n-menu-item-content {
    &::before {
      left: 5px;
      right: 5px;
    }
    &.n-menu-item-content--selected,
    &:hover {
      &::before {
        border-left: 4px solid var(--primary-color);
      }
    }
  }
}
</style>


================================================
FILE: src/layout/sidebar/index.vue
================================================
<script setup lang="ts">
import SideLogo from './components/SideLogo.vue'
import SideMenu from './components/SideMenu.vue'
</script>

<template>
  <SideLogo />
  <SideMenu />
</template>


================================================
FILE: src/layout/tab/components/ContextMenu.vue
================================================
<script setup lang="ts">
import { useAppStore, useTabStore } from '@/store'
import { renderIcon } from '@/utils'

interface Props {
  show?: boolean
  currentPath?: string
  x: number
  y: number
}

const props = withDefaults(defineProps<Props>(), {
  show: false,
  currentPath: '',
})

const emit = defineEmits(['update:show'])

const tabStore = useTabStore()
const appStore = useAppStore()

const options = computed(() => [
  {
    label: '重新加载',
    key: 'reload',
    disabled: props.currentPath !== tabStore.activeTab,
    icon: renderIcon('mdi:refresh', { size: 14 }),
  },
  {
    label: '关闭',
    key: 'close',
    disabled: tabStore.tabs.length <= 1,
    icon: renderIcon('mdi:close', { size: 14 }),
  },
  {
    label: '关闭其他',
    key: 'close-other',
    disabled: tabStore.tabs.length <= 1,
    icon: renderIcon('mdi:arrow-expand-horizontal', { size: 14 }),
  },
  {
    label: '关闭左侧',
    key: 'close-left',
    disabled: tabStore.tabs.length <= 1 || props.currentPath === tabStore.tabs[0].path,
    icon: renderIcon('mdi:arrow-expand-left', { size: 14 }),
  },
  {
    label: '关闭右侧',
    key: 'close-right',
    disabled: tabStore.tabs.length <= 1 || props.currentPath === tabStore.tabs[tabStore.tabs.length - 1].path,
    icon: renderIcon('mdi:arrow-expand-right', { size: 14 }),
  },
])

const dropdownShow = computed({
  get() {
    return props.show
  },
  set(show) {
    emit('update:show', show)
  },
})

const actionMap = new Map([
  [
    'reload',
    () => {
      appStore.reloadPage()
    },
  ],
  [
    'close',
    () => {
      tabStore.removeTab(props.currentPath)
    },
  ],
  [
    'close-other',
    () => {
      tabStore.removeOther(props.currentPath)
    },
  ],
  [
    'close-left',
    () => {
      tabStore.removeLeft(props.currentPath)
    },
  ],
  [
    'close-right',
    () => {
      tabStore.removeRight(props.currentPath)
    },
  ],
])

function handleHideDropdown() {
  dropdownShow.value = false
}

function handleSelect(key: string) {
  const actionFn = actionMap.get(key)
  actionFn && actionFn()
  handleHideDropdown()
}
</script>

<template>
  <n-dropdown
    :show="dropdownShow"
    :options="options"
    :x="x"
    :y="y"
    placement="bottom-start"
    @clickoutside="handleHideDropdown"
    @select="handleSelect"
  />
</template>


================================================
FILE: src/layout/tab/index.vue
================================================
<script setup lang="ts">
import ContextMenu from './components/ContextMenu.vue'
import type { TabItem } from '@/store'
import { useTabStore, useThemeStore } from '@/store'
import ScrollX from '@/components/common/ScrollX.vue'

const route = useRoute()
const router = useRouter()
const tabStore = useTabStore()
const useTheme = useThemeStore()

interface ContextMenuOption {
  show: boolean
  x: number
  y: number
  currentPath: string
}

const contextMenuOption = reactive<ContextMenuOption>({
  show: false,
  x: 0,
  y: 0,
  currentPath: '',
})

watch(
  () => route.path,
  () => {
    const { name, fullPath: path } = route
    const title = route.meta?.title as string || ''
    tabStore.addTab({ name: name as string, path, title })
  },
  { immediate: true },
)

function handleTagClick(path: string) {
  tabStore.setActiveTab(path)
  router.push(path)
}

function showContextMenu() {
  contextMenuOption.show = true
}
function hideContextMenu() {
  contextMenuOption.show = false
}
function setContextMenu(x: number, y: number, currentPath: string) {
  Object.assign(contextMenuOption, { x, y, currentPath })
}

// 右击菜单
async function handleContextMenu(e: MouseEvent, tabItem: TabItem) {
  const { clientX, clientY } = e
  hideContextMenu()
  setContextMenu(clientX, clientY, tabItem.path)
  await nextTick()
  showContextMenu()
}
</script>

<template>
  <ScrollX bg-white dark:bg-dark :style="{ height: `${useTheme.tab.height}px` }">
    <n-tag
      v-for="tab in tabStore.tabs"
      :key="tab.path"
      px-15 mx-5 rounded-4 cursor-pointer
      :type="tabStore.activeTab === tab.path ? 'primary' : 'default'"
      :closable="tabStore.tabs.length > 1"
      @click="handleTagClick(tab.path)"
      @close.stop="tabStore.removeTab(tab.path)"
      @contextmenu.prevent="handleContextMenu($event, tab)"
    >
      {{ tab.title }}
    </n-tag>
  </ScrollX>

  <ContextMenu
    v-model:show="contextMenuOption.show"
    :current-path="contextMenuOption.currentPath"
    :x="contextMenuOption.x"
    :y="contextMenuOption.y"
  />
</template>

<style lang="scss">
.n-tag__close {
  box-sizing: content-box;
  border-radius: 50%;
  font-size: 12px;
  padding: 2px;
  transform: scale(.9);
  transform: translateX(5px);
  transition: all 0.3s;
}
</style>


================================================
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: <boolean> 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<string>): 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: <RoutesType> [],
    }
  },
  getters: {
    routes(): RoutesType {
      return basicRoutes.concat(this.accessRoutes)
    },
    menus(): RoutesType {
      return this.routes.filter(route => route.name && !route.isHidden)
    },
  },
  actions: {
    generateRoutes(role: Array<string> = []): 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: <Array<TabItem>> tabs || [],
      activeTab: <string> activeTab || '',
    }
  },
  actions: {
    setActiveTab(path: string) {
      this.activeTab = path
      setSession('activeTab', path)
    },
    setTabs(tabs: Array<TabItem>) {
      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<Record<ColorKey, string>>

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<ColorType, string>): 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<string>
}

export const useUserStore = defineStore('user', {
  state() {
    return {
      userInfo: <UserInfo> {},
    }
  },
  getters: {
    userId(): string {
      return this.userInfo.id || ''
    },
    name(): string {
      return this.userInfo.name || ''
    },
    avatar(): string {
      return this.userInfo.avatar || ''
    },
    role(): Array<string> {
      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<T>(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<T>(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
================================================
<script setup lang="ts">
import { shuffle } from 'lodash-es'

interface NumItem {
  id: number
  num: number
}
const numList = ref<NumItem[]>(new Array(81).fill(undefined).map((item, index) => ({ id: index, num: index % 9 + 1 })))
</script>

<template>
  <CommonPage :show-footer="true">
    <n-button type="primary" @click="numList = shuffle(numList)">
      打乱
    </n-button>
    <transition-group tag="ul" move-class="transition-500" mt-15 w-510 f-c-c flex-wrap p-30 rounded-15 bg-white dark:bg-dark>
      <li v-for="item in numList" :key="item.id" f-c-c w-40 h-40 m-5 border-1 bc-ccc rounded-full color-primary>
        {{ item.num }}
      </li>
    </transition-group>
  </CommonPage>
</template>


================================================
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
================================================
<script setup lang="ts">
import { NButton, NSwitch } from 'naive-ui'
import { CrudModal, CrudTable, QueryBarItem, useCRUD } from '@zclzone/crud'
import api from './api'
import { formatDateTime, isNullOrUndef, renderIcon } from '@/utils'

const $table = ref<any>(null)
/** QueryBar筛选参数(可选) */
const queryItems = ref<any>({})
/** 补充参数(可选) */
const extraParams = ref<any>({})

const {
  modalVisible,
  modalAction,
  modalTitle,
  modalLoading,
  handleAdd,
  handleDelete,
  handleEdit,
  handleView,
  handleSave,
  modalForm,
  modalFormRef,
} = useCRUD({
  name: '文章',
  initForm: { author: '大脸怪' },
  doCreate: api.addPost,
  doDelete: api.deletePost,
  doUpdate: api.updatePost,
  refresh: () => $table.value?.handleSearch(),
})

const columns: any = [
  { type: 'selection', fixed: 'left' },
  {
    title: '发布',
    key: 'isPublish',
    width: 60,
    align: 'center',
    fixed: 'left',
    render(row: any) {
      return h(NSwitch, {
        size: 'small',
        rubberBand: false,
        value: row.isPublish,
        loading: !!row.publishing,
        onUpdateValue: () => handlePublish(row),
      })
    },
  },
  { title: '标题', key: 'title', width: 150, ellipsis: { tooltip: true } },
  { title: '分类', key: 'category', width: 80, ellipsis: { tooltip: true } },
  { title: '创建人', key: 'author', width: 80 },
  {
    title: '创建时间',
    key: 'createDate',
    width: 150,
    render(row: any) {
      return h('span', formatDateTime(row.createDate))
    },
  },
  {
    title: '最后更新时间',
    key: 'updateDate',
    width: 150,
    render(row: any) {
      return h('span', formatDateTime(row.updateDate))
    },
  },
  {
    title: '操作',
    key: 'actions',
    width: 240,
    align: 'center',
    fixed: 'right',
    hideInExcel: true,
    render(row: any) {
      return [
        h(
          NButton,
          {
            size: 'small',
            type: 'primary',
            secondary: true,
            onClick: () => handleView(row),
          },
          { default: () => '查看', icon: renderIcon('majesticons:eye-line', { size: 14 }) },
        ),
        h(
          NButton,
          {
            size: 'small',
            type: 'primary',
            style: 'margin-left: 15px;',
            onClick: () => handleEdit(row),
          },
          { default: () => '编辑', icon: renderIcon('material-symbols:edit-outline', { size: 14 }) },
        ),

        h(
          NButton,
          {
            size: 'small',
            type: 'error',
            style: 'margin-left: 15px;',
            onClick: () => handleDelete(row.id),
          },
          { default: () => '删除', icon: renderIcon('material-symbols:delete-outline', { size: 14 }) },
        ),
      ]
    },
  },
]

// 选中事件
function onChecked(rowKeys: string[]) {
  if (rowKeys.length)
    window.$message?.info(`选中${rowKeys.join(' ')}`)
}

// 发布
function handlePublish(row: any) {
  if (isNullOrUndef(row.id))
    return

  row.publishing = true
  setTimeout(() => {
    row.isPublish = !row.isPublish
    row.publishing = false
    window.$message?.success(row.isPublish ? '已发布' : '已取消发布')
  }, 1000)
}

onMounted(() => {
  $table.value?.handleSearch()
})
</script>

<template>
  <CommonPage show-footer title="文章">
    <template #action>
      <div>
        <NButton type="primary" secondary @click="$table?.handleExport()">
          <TheIcon icon="mdi:download" :size="18" class="mr-5" /> 导出
        </NButton>
        <NButton type="primary" class="ml-16" @click="handleAdd">
          <TheIcon icon="material-symbols:add" :size="18" class="mr-5" /> 新建文章
        </NButton>
      </div>
    </template>

    <CrudTable
      ref="$table"
      v-model:query-items="queryItems"
      :extra-params="extraParams"
      :scroll-x="1200"
      :columns="columns"
      :get-data="api.getPosts"
      @on-checked="onChecked"
    >
      <template #queryBar>
        <QueryBarItem label="标题" :label-width="50">
          <n-input
            v-model:value="queryItems.title"
            type="text"
            placeholder="请输入标题"
            @keydown.enter="$table?.handleSearch"
          />
        </QueryBarItem>
      </template>
    </CrudTable>
    <!-- 新增/编辑/查看 -->
    <CrudModal
      v-model:visible="modalVisible"
      :title="modalTitle"
      :loading="modalLoading"
      :show-footer="modalAction !== 'view'"
      @on-save="handleSave"
    >
      <n-form
        ref="modalFormRef"
        label-placement="left"
        label-align="left"
        :label-width="80"
        :model="modalForm"
        :disabled="modalAction === 'view'"
      >
        <n-form-item label="作者" path="author">
          <n-input v-model:value="modalForm.author" disabled />
        </n-form-item>
        <n-form-item
          label="文章标题"
          path="title"
          :rule="{
            required: true,
            message: '请输入文章标题',
            trigger: ['input', 'blur'],
          }"
        >
          <n-input v-model:value="modalForm.title" placeholder="请输入文章标题" />
        </n-form-item>
        <n-form-item
          label="文章内容"
          path="content"
          :rule="{
            required: true,
            message: '请输入文章内容',
            trigger: ['input', 'blur'],
          }"
        >
          <n-input
            v-model:value="modalForm.content"
            placeholder="请输入文章内容"
            type="textarea"
            :autosize="{
              minRows: 3,
              maxRows: 5,
            }"
          />
        </n-form-item>
      </n-form>
    </CrudModal>
  </CommonPage>
</template>


================================================
FILE: src/views/demo/unocss/index.vue
================================================
<template>
  <CommonPage :show-footer="true">
    <p>
      文档:<a hover-decoration-underline c-blue href="https://uno.antfu.me/" target="_blank">https://uno.antfu.me/</a>
    </p>
    <p>
      playground:
      <a c-blue hover-decoration-underline href="https://unocss.antfu.me/play/" target="_blank">
        https://unocss.antfu.me/play/
      </a>
    </p>

    <div f-c-c flex-col mt-20 w-350>
      <div flex flex-wrap justify-around p-10 rounded-10 b-1 bc-ccc>
        <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 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 dark:bg-white />
          <span w-6 h-6 rounded-3 bg-black dark:bg-white self-end />
        </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 dark:bg-white />
          <span w-6 h-6 rounded-3 bg-black dark:bg-white self-center />
          <span w-6 h-6 rounded-3 bg-black dark:bg-white self-end />
        </div>
        <div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20>
          <div flex-col justify-between>
            <span w-6 h-6 rounded-3 bg-black dark:bg-white />
            <span w-6 h-6 rounded-3 bg-black dark:bg-white />
          </div>
          <div flex-col justify-between>
            <span w-6 h-6 rounded-3 bg-black dark:bg-white />
            <span w-6 h-6 rounded-3 bg-black dark:bg-white />
          </div>
        </div>
        <div w-50 h-50 b-1 rounded-5 flex-col justify-between items-center p-10 m-20>
          <div flex w-full justify-between>
            <span w-6 h-6 rounded-3 bg-black dark:bg-white />
            <span w-6 h-6 rounded-3 bg-black dark:bg-white />
          </div>
          <div w-6 h-6 rounded-3 bg-black dark:bg-white />
          <div flex w-full justify-between>
            <span w-6 h-6 rounded-3 bg-black dark:bg-white />
            <span w-6 h-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 dark:bg-white />
            <span w-6 h-6 rounded-3 bg-black dark:bg-white />
          </div>
          <div flex w-full justify-between>
            <span w-6 h-6 rounded-3 bg-black dark:bg-white />
            <span w-6 h-6 rounded-3 bg-black dark:bg-white />
          </div>
          <div flex w-full justify-between>
            <span w-6 h-6 rounded-3 bg-black dark:bg-white />
            <span w-6 h-6 rounded-3 bg-black dark:bg-white />
          </div>
        </div>
      </div>
      <h2 font-normal text-14 mt-10 color-gray>
        Flex 骰子
      </h2>
    </div>
  </CommonPage>
</template>


================================================
FILE: src/views/error-page/404.vue
================================================
<script setup lang="ts">
const { replace } = useRouter()
</script>

<template>
  <AppPage>
    <n-result m-auto status="404" description="抱歉,您访问的页面不存在。">
      <template #icon>
        <img src="@/assets/images/404.webp" width="500">
      </template>
      <template #footer>
        <n-button @click="replace('/')">
          返回首页
        </n-button>
      </template>
    </n-result>
  </AppPage>
</template>


================================================
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
================================================
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import api from './api'
import { getLocal, removeLocal, setLocal, setToken } from '@/utils'
import bgImg from '@/assets/images/login_bg.webp'
import { addDynamicRoutes } from '@/router'

const title: string = import.meta.env.VITE_APP_TITLE

const router = useRouter()
const route = useRoute()
const query = route.query

interface LoginInfo {
  name: string
  password: string
}

const loginInfo = ref<LoginInfo>({
  name: '',
  password: '',
})

const localLoginInfo = getLocal('loginInfo') as LoginInfo
if (localLoginInfo) {
  loginInfo.value.name = localLoginInfo.name || ''
  loginInfo.value.password = localLoginInfo.password || ''
}

const loging = ref<boolean>(false)
const isRemember = useStorage('isRemember', false)
async function handleLogin() {
  const { name, password } = loginInfo.value
  if (!name || !password) {
    window.$message?.warning('请输入用户名和密码')
    return
  }
  try {
    loging.value = true
    const res: any = await api.login({ name, password: password.toString() })
    window.$notification?.success({ title: '登录成功!', duration: 2500 })
    setToken(res.data.token)
    if (isRemember.value)
      setLocal('loginInfo', { name, password })
    else
      removeLocal('loginInfo')

    await addDynamicRoutes()
    if (query.redirect) {
      const path = query.redirect as string
      Reflect.deleteProperty(query, 'redirect')
      router.push({ path, query })
    }
    else {
      router.push('/')
    }
  }
  catch (error) {
    console.error(error)
  }
  loging.value = false
}
</script>

<template>
  <AppPage :show-footer="true" bg-cover :style="{ backgroundImage: `url(${bgImg})` }">
    <div m-auto p-15 f-c-c min-w-345 rounded-10 card-shadow bg-white dark:bg-dark bg-opacity-60>
      <div w-380 hidden md:block px-20 py-35>
        <img src="@/assets/images/login_banner.webp" w-full alt="login_banner">
      </div>

      <div w-320 flex-col px-20 py-35>
        <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-30>
          <n-input
            v-model:value="loginInfo.name"
            autofocus
            class="text-16 items-center h-50 pl-10"
            placeholder="admin"
            :maxlength="20"
          />
        </div>
        <div mt-30>
          <n-input
            v-model:value="loginInfo.password"
            class="text-16 items-center h-50 pl-10"
            type="password"
            show-password-on="mousedown"
            placeholder="123456"
            :maxlength="20"
            @keydown.enter="handleLogin"
          />
        </div>

        <div mt-20>
          <n-checkbox :checked="isRemember" label="记住我" :on-update:checked="(val:boolean) => (isRemember = val)" />
        </div>

        <div mt-20>
          <n-button w-full h-50 rounded-5 text-16 type="primary" :loading="loging" @click="handleLogin">
            登录
          </n-button>
        </div>
      </div>
    </div>
  </AppPage>
</template>


================================================
FILE: src/views/workbench/index.vue
================================================
<script setup lang="ts">
import { useUserStore } from '@/store'

const userStore = useUserStore()
</script>

<template>
  <AppPage :show-footer="true" min-w-375>
    <div flex-1 min-w-375>
      <n-watermark
        :content="userStore.name"
        cross
        selectable
        :font-size="16"
        :line-height="16"
        :width="192"
        :height="128"
        :x-offset="12"
        :y-offset="28"
        :rotate="-15"
      >
        <n-card rounded-10>
          <div flex items-center>
            <img rounded-full width="60" :src="userStore.avatar">
            <div ml-20>
              <p text-16>
                Hello, {{ userStore.name }} !
              </p>
              <n-gradient-text
                mt-5 text-12 op-60
                gradient="linear-gradient(90deg, red 0%, green 50%, blue 100%)"
              >
                他日若遂凌云志,敢笑黄巢不丈夫~
              </n-gradient-text>
            </div>
            <div ml-auto items-center hidden md:flex>
              <n-statistic label="待办" :value="4" w-80>
                <template #suffix>
                  / 10
                </template>
              </n-statistic>
              <n-statistic label="Stars" w-80 ml-20>
                <a href="https://github.com/zclzone/qs-admin">
                  <img allt="stars" src="https://badgen.net/github/stars/zclzone/qs-admin">
                </a>
              </n-statistic>
              <n-statistic label="Forks" w-80 ml-20>
                <a href="https://github.com/zclzone/qs-admin">
                  <img allt="forks" src="https://badgen.net/github/forks/zclzone/qs-admin">
                </a>
              </n-statistic>
            </div>
          </div>
        </n-card>
      </n-watermark>

      <n-card title="项目" size="small" :segmented="true" mt-15 rounded-10>
        <template #header-extra>
          <n-button text type="primary">
            更多
          </n-button>
        </template>
        <div flex flex-wrap justify-around>
          <n-card
            v-for="i in 20"
            :key="i"
            w-300 flex-shrink-0 mt-10 mb-10 cursor-pointer
            hover:card-shadow
            title="奇思Admin"
            size="small"
          >
            <p op-60>
              一个基于 Vue3、Vite3、TypeScript、Pinia、Unocss、Naive UI 的轻量级后台管理模板
            </p>
          </n-card>
          <div w-300 h-0 />
          <div w-300 h-0 />
          <div w-300 h-0 />
          <div w-300 h-0 />
        </div>
      </n-card>
    </div>
  </AppPage>
</template>


================================================
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<string>
  requireAuth?: boolean
}

interface RouteItem {
  name: string
  path: string
  redirect?: string
  isHidden?: boolean
  meta?: Meta,
  children?: RoutesType
}

type RouteType = RouteRecordRaw & RouteItem

type RoutesType = Array<RouteType>

/** 前端导入的路由模块 */
type RouteModule = Record<string, { default: RouteType }>

================================================
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,
      },
    },
  }
})
Download .txt
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
Download .txt
SYMBOL INDEX (152 symbols across 43 files)

FILE: build/config/proxy.ts
  function createViteProxy (line 4) | function createViteProxy(isUseProxy = true, proxyType: ProxyType) {

FILE: build/plugins/html.ts
  function setupHtmlPlugin (line 3) | function setupHtmlPlugin(viteEnv: ViteEnv) {

FILE: build/plugins/index.ts
  function setupVitePlugins (line 11) | function setupVitePlugins(viteEnv: ViteEnv, isBuild: boolean): PluginOpt...

FILE: build/plugins/mock.ts
  function setupMockPlugin (line 3) | function setupMockPlugin(isBuild: boolean) {

FILE: build/utils.ts
  function getRootPath (line 7) | function getRootPath() {
  function getSrcPath (line 16) | function getSrcPath(srcName = 'src') {
  function convertEnv (line 25) | function convertEnv(envOptions: Record<string, any>): ViteEnv {

FILE: mock/index.js
  function setupProdMockServer (line 4) | function setupProdMockServer() {

FILE: mock/utils.js
  function resolveToken (line 1) | function resolveToken(authorization) {

FILE: public/loading/index.js
  function addThemeColorCssVars (line 1) | function addThemeColorCssVars() {

FILE: settings/proxy-config.ts
  function getProxyConfig (line 16) | function getProxyConfig(envType: ProxyType = 'dev'): ProxyConfig {

FILE: src/main.ts
  function setupApp (line 12) | async function setupApp() {

FILE: src/router/guard/index.ts
  function setupRouterGuard (line 6) | function setupRouterGuard(router: Router) {

FILE: src/router/guard/page-loading-guard.ts
  function createPageLoadingGuard (line 3) | function createPageLoadingGuard(router: Router) {

FILE: src/router/guard/page-title-guard.ts
  function createPageTitleGuard (line 5) | function createPageTitleGuard(router: Router) {

FILE: src/router/guard/permission-guard.ts
  constant WHITE_LIST (line 4) | const WHITE_LIST = ['/login']
  function createPermissionGuard (line 5) | function createPermissionGuard(router: Router) {

FILE: src/router/index.ts
  function setupRouter (line 16) | async function setupRouter(app: App) {
  function addDynamicRoutes (line 22) | async function addDynamicRoutes() {
  function resetRouter (line 48) | async function resetRouter() {
  function getRouteNames (line 57) | function getRouteNames(routes: RoutesType) {
  function getRouteName (line 61) | function getRouteName(route: RouteType) {

FILE: src/router/routes/index.ts
  constant NOT_FOUND_ROUTE (line 55) | const NOT_FOUND_ROUTE: RouteType = {
  constant EMPTY_ROUTE (line 62) | const EMPTY_ROUTE: RouteType = {

FILE: src/store/index.ts
  function setupStore (line 4) | function setupStore(app: App) {

FILE: src/store/modules/app/index.ts
  method state (line 4) | state() {
  method reloadPage (line 10) | async reloadPage() {

FILE: src/store/modules/permission/helpers.ts
  function hasPermission (line 3) | function hasPermission(route: RouteType, role: string[]) {
  function filterAsyncRoutes (line 18) | function filterAsyncRoutes(routes: RoutesType = [], role: Array<string>)...

FILE: src/store/modules/permission/index.ts
  method state (line 7) | state() {
  method routes (line 13) | routes(): RoutesType {
  method menus (line 16) | menus(): RoutesType {
  method generateRoutes (line 21) | generateRoutes(role: Array<string> = []): RoutesType {
  method resetPermission (line 26) | resetPermission() {

FILE: src/store/modules/tab/helpers.ts
  constant WITHOUT_TAB_PATHS (line 6) | const WITHOUT_TAB_PATHS = ['/404', '/login']

FILE: src/store/modules/tab/index.ts
  type TabItem (line 6) | interface TabItem {
  method state (line 13) | state() {
  method setActiveTab (line 20) | setActiveTab(path: string) {
  method setTabs (line 24) | setTabs(tabs: Array<TabItem>) {
  method addTab (line 28) | addTab(tab: TabItem) {
  method removeTab (line 34) | removeTab(path: string) {
  method removeOther (line 45) | removeOther(curPath: string) {
  method removeLeft (line 50) | removeLeft(curPath: string) {
  method removeRight (line 57) | removeRight(curPath: string) {
  method resetTabs (line 64) | resetTabs() {

FILE: src/store/modules/theme/helpers.ts
  type ColorType (line 5) | type ColorType = 'primary' | 'info' | 'success' | 'warning' | 'error'
  type ColorScene (line 6) | type ColorScene = '' | 'Suppl' | 'Hover' | 'Pressed' | 'Active'
  type ColorKey (line 7) | type ColorKey = `${ColorType}Color${ColorScene}`
  type ThemeColor (line 8) | type ThemeColor = Partial<Record<ColorKey, string>>
  type ColorAction (line 10) | interface ColorAction {
  function initThemeSettings (line 16) | function initThemeSettings(): Theme.Setting {
  function getNaiveThemeOverrides (line 33) | function getNaiveThemeOverrides(colors: Record<ColorType, string>): Glob...
  function getThemeColors (line 57) | function getThemeColors(colors: [ColorType, string][]) {

FILE: src/store/modules/theme/index.ts
  type ThemeState (line 7) | type ThemeState = Theme.Setting
  method naiveThemeOverrides (line 12) | naiveThemeOverrides(): GlobalThemeOverrides {
  method naiveTheme (line 15) | naiveTheme(): BuiltInGlobalTheme | undefined {
  method setIsMobile (line 20) | setIsMobile(isMobile: boolean) {
  method setDarkMode (line 24) | setDarkMode(darkMode: boolean) {
  method toggleDarkMode (line 28) | toggleDarkMode() {
  method toggleCollapsed (line 32) | toggleCollapsed() {
  method setCollapsed (line 36) | setCollapsed(collapsed: boolean) {
  method setPrimaryColor (line 40) | setPrimaryColor(color: string) {

FILE: src/store/modules/user/index.ts
  type UserInfo (line 7) | interface UserInfo {
  method state (line 15) | state() {
  method userId (line 21) | userId(): string {
  method name (line 24) | name(): string {
  method avatar (line 27) | avatar(): string {
  method role (line 30) | role(): Array<string> {
  method getUserInfo (line 35) | async getUserInfo() {
  method logout (line 51) | async logout() {
  method setUserInfo (line 61) | setUserInfo(userInfo = {}) {

FILE: src/utils/auth/router.ts
  function toLogin (line 3) | function toLogin() {
  function toFourZeroFour (line 12) | function toFourZeroFour() {

FILE: src/utils/auth/token.ts
  constant TOKEN_CODE (line 4) | const TOKEN_CODE = 'access_token'
  constant DURATION (line 6) | const DURATION = 6 * 60 * 60
  function getToken (line 8) | function getToken() {
  function setToken (line 12) | function setToken(token: string) {
  function removeToken (line 16) | function removeToken() {
  function refreshAccessToken (line 20) | async function refreshAccessToken() {

FILE: src/utils/common/color.ts
  type ColorIndex (line 7) | type ColorIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
  function getColorPalette (line 23) | function getColorPalette(color: string, index: ColorIndex) {
  function getAllColorPalette (line 44) | function getAllColorPalette(color: string) {
  function getHue (line 55) | function getHue(hsv: HsvColor, i: number, isLight: boolean) {
  function getSaturation (line 84) | function getSaturation(hsv: HsvColor, i: number, isLight: boolean) {
  function getValue (line 113) | function getValue(hsv: HsvColor, i: number, isLight: boolean) {
  function addColorAlpha (line 132) | function addColorAlpha(color: string, alpha: number) {
  function mixColor (line 142) | function mixColor(firstColor: string, secondColor: string, ratio: number) {
  function isWhiteColor (line 150) | function isWhiteColor(color: string) {

FILE: src/utils/common/common.ts
  type Time (line 3) | type Time = undefined | string | Date
  function formatDateTime (line 6) | function formatDateTime(time: Time, format = 'YYYY-MM-DD HH:mm:ss'): str...
  function formatDate (line 11) | function formatDate(date: Time = undefined, format = 'YYYY-MM-DD') {

FILE: src/utils/common/crypto.ts
  function encrypto (line 9) | function encrypto(data: any) {
  function decrypto (line 18) | function decrypto(cipherText: string) {

FILE: src/utils/common/icon.ts
  type Props (line 6) | interface Props {
  function renderIcon (line 11) | function renderIcon(icon: string, props: Props = { size: 12 }) {
  function renderCustomIcon (line 15) | function renderCustomIcon(icon: string, props: Props = { size: 12 }) {

FILE: src/utils/common/is.ts
  function is (line 3) | function is(val: unknown, type: string): boolean {
  function isDef (line 7) | function isDef(val: any): boolean {
  function isUndef (line 11) | function isUndef(val: any): boolean {
  function isNull (line 15) | function isNull(val: any): boolean {
  function isWhitespace (line 19) | function isWhitespace(val: any): boolean {
  function isObject (line 23) | function isObject(val: any): boolean {
  function isArray (line 27) | function isArray(val: any): boolean {
  function isString (line 31) | function isString(val: any): boolean {
  function isNumber (line 35) | function isNumber(val: any): boolean {
  function isBoolean (line 39) | function isBoolean(val: any): boolean {
  function isDate (line 43) | function isDate(val: any): boolean {
  function isRegExp (line 47) | function isRegExp(val: any): boolean {
  function isFunction (line 51) | function isFunction(val: any): boolean {
  function isPromise (line 55) | function isPromise(val: any): boolean {
  function isElement (line 59) | function isElement(val: any): boolean {
  function isNullOrUndef (line 64) | function isNullOrUndef(val: any): boolean {
  function isNullOrWhitespace (line 69) | function isNullOrWhitespace(val: any): boolean {
  function isEmpty (line 74) | function isEmpty(val: any): boolean {
  function ifNull (line 91) | function ifNull(val: any, def: any = '') {
  function isUrl (line 95) | function isUrl(path: string): boolean {

FILE: src/utils/common/naiveTools.ts
  function setupNaiveDiscreteApi (line 4) | function setupNaiveDiscreteApi() {

FILE: src/utils/http/helpers.ts
  class AxiosRejectError (line 5) | class AxiosRejectError extends Error {
    method constructor (line 8) | constructor(rejectData: ErrorResolveResponse, options?: ErrorOptions) {
  function resolveResError (line 15) | function resolveResError(code: number | string | undefined, message = ''...

FILE: src/utils/http/index.ts
  function createAxios (line 4) | function createAxios(options = {}) {

FILE: src/utils/http/interceptors.ts
  function reqResolve (line 7) | function reqResolve(config: RequestConfig) {
  function reqReject (line 30) | function reqReject(error: AxiosError) {
  function resResolve (line 35) | function resResolve(response: AxiosResponse) {
  function resReject (line 51) | function resReject(error: AxiosError) {

FILE: src/utils/storage/local.ts
  type StorageData (line 3) | interface StorageData {
  constant DEFAULT_CACHE_TIME (line 9) | const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7
  function setLocal (line 11) | function setLocal(key: string, value: unknown, expire: number | null = D...
  function getLocal (line 17) | function getLocal<T>(key: string) {
  function getLocalExpire (line 37) | function getLocalExpire(key: string): number | null {
  function removeLocal (line 54) | function removeLocal(key: string) {
  function clearLocal (line 58) | function clearLocal() {

FILE: src/utils/storage/session.ts
  function setSession (line 3) | function setSession(key: string, value: unknown) {
  function getSession (line 8) | function getSession<T>(key: string) {
  function removeSession (line 20) | function removeSession(key: string) {
  function clearSession (line 24) | function clearSession() {

FILE: types/axios.d.ts
  type RequestConfig (line 3) | interface RequestConfig extends AxiosRequestConfig {
  type ErrorResolveResponse (line 10) | interface ErrorResolveResponse {

FILE: types/env.d.ts
  type ProxyType (line 1) | type ProxyType = 'dev' | 'test' | 'prod';
  type ViteEnv (line 3) | interface ViteEnv {
  type ProxyConfig (line 16) | interface ProxyConfig {

FILE: types/global.d.ts
  type Window (line 1) | interface Window {

FILE: types/router.d.ts
  type Meta (line 3) | interface Meta {
  type RouteItem (line 12) | interface RouteItem {
  type RouteType (line 21) | type RouteType = RouteRecordRaw & RouteItem
  type RoutesType (line 23) | type RoutesType = Array<RouteType>
  type RouteModule (line 26) | type RouteModule = Record<string, { default: RouteType }>

FILE: types/theme.d.ts
  type Sider (line 2) | interface Sider {
  type Header (line 11) | interface Header {
  type Tab (line 21) | interface Tab {
  type OtherColor (line 28) | interface OtherColor {
  type Setting (line 40) | interface Setting {
Condensed preview — 111 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (122K chars).
[
  {
    "path": ".cz-config.js",
    "chars": 1532,
    "preview": "module.exports = {\n  types: [\n    { value: 'feat',      name:'feat:      新增功能' },\n    { value: 'fix',       name:'fix:  "
  },
  {
    "path": ".gitignore",
    "chars": 337,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
  },
  {
    "path": ".husky/commit-msg",
    "chars": 88,
    "preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpx --no-install commitlint --edit\n"
  },
  {
    "path": ".husky/pre-commit",
    "chars": 73,
    "preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpm run lint:staged\n"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 247,
    "preview": "{\n  \"recommendations\": [\n    \"vue.volar\",\n    \"sdras.vue-vscode-snippets\",\n    \"antfu.unocss\",\n    \"esbenp.prettier-vsco"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 821,
    "preview": "{\r\n  \"path-intellisense.mappings\": {\r\n    \"@/\": \"${workspaceRoot}/src\",\r\n    \"~/\": \"${workspaceRoot}\"\r\n  },\r\n  \"editor.f"
  },
  {
    "path": "LICENSE",
    "chars": 1069,
    "preview": "MIT License\n\nCopyright (c) 2023 Ronnie Zhang\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "README.md",
    "chars": 3387,
    "preview": "<p align=\"center\">\n  <a href=\"https://github.com/zclzone/qs-admin\">\n    <img alt=\"Vue Naive Admin Logo\" width=\"200\" src="
  },
  {
    "path": "README.zh-CN.md",
    "chars": 2787,
    "preview": "<p align=\"center\">\n  <a href=\"https://github.com/zclzone/qs-admin\">\n    <img alt=\"Vue Naive Admin Logo\" width=\"200\" src="
  },
  {
    "path": "build/config/define.ts",
    "chars": 251,
    "preview": "import dayjs from 'dayjs'\r\n\r\n/**\r\n * * 此处定义的是全局常量,启动或打包后将添加到window中\r\n * https://vitejs.cn/config/#define\r\n */\r\n\r\n// 项目构建"
  },
  {
    "path": "build/config/index.ts",
    "chars": 51,
    "preview": "export * from './define'\r\nexport * from './proxy'\r\n"
  },
  {
    "path": "build/config/proxy.ts",
    "chars": 546,
    "preview": "import type { ProxyOptions } from 'vite'\r\nimport { getProxyConfig } from '../../settings/proxy-config'\r\n\r\nexport functio"
  },
  {
    "path": "build/plugins/html.ts",
    "chars": 318,
    "preview": "import { createHtmlPlugin } from 'vite-plugin-html'\r\n\r\nexport function setupHtmlPlugin(viteEnv: ViteEnv) {\r\n  const { VI"
  },
  {
    "path": "build/plugins/index.ts",
    "chars": 911,
    "preview": "import type { PluginOption } from 'vite'\r\nimport vue from '@vitejs/plugin-vue'\r\nimport unocss from 'unocss/vite'\r\nimport"
  },
  {
    "path": "build/plugins/mock.ts",
    "chars": 334,
    "preview": "import { viteMockServe } from 'vite-plugin-mock'\r\n\r\nexport function setupMockPlugin(isBuild: boolean) {\r\n  return viteMo"
  },
  {
    "path": "build/plugins/unplugin.ts",
    "chars": 1309,
    "preview": "import { resolve } from 'node:path'\r\nimport AutoImport from 'unplugin-auto-import/vite'\r\nimport Components from 'unplugi"
  },
  {
    "path": "build/utils.ts",
    "chars": 829,
    "preview": "import path from 'node:path'\r\n\r\n/**\r\n * * 项目根路径\r\n * @descrition 结尾不带/\r\n */\r\nexport function getRootPath() {\r\n  return pa"
  },
  {
    "path": "commitlint.config.js",
    "chars": 421,
    "preview": "module.exports = {\n  ignores: [commit => commit.includes('first commit')],\n  extends: ['@commitlint/config-conventional'"
  },
  {
    "path": "index.html",
    "chars": 1123,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "mock/api/auth.js",
    "chars": 775,
    "preview": "import { resolveToken } from '../utils'\r\n\r\nconst token = {\r\n  admin: 'admin',\r\n  editor: 'editor',\r\n}\r\n\r\nexport default "
  },
  {
    "path": "mock/api/index.js",
    "chars": 129,
    "preview": "import auth from './auth'\r\nimport user from './user'\r\nimport table from './post'\r\n\r\nexport default [...auth, ...user, .."
  },
  {
    "path": "mock/api/post.js",
    "chars": 3112,
    "preview": "const posts = [\n  {\n    title: '使用纯css优雅配置移动端rem布局',\n    author: '大脸怪',\n    category: 'Css',\n    description: '通常配置rem布局"
  },
  {
    "path": "mock/api/user.js",
    "chars": 815,
    "preview": "import { resolveToken } from '../utils'\r\n\r\nconst users = {\r\n  admin: {\r\n    id: 1,\r\n    name: '大脸怪(admin)',\r\n    avatar:"
  },
  {
    "path": "mock/index.js",
    "chars": 181,
    "preview": "import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'\r\nimport api from './api'\r\n\r\nexport funct"
  },
  {
    "path": "mock/utils.js",
    "chars": 255,
    "preview": "export function resolveToken(authorization) {\r\n  /**\r\n   * * jwt token\r\n   * * Bearer + token\r\n   * ! 认证方案: Bearer\r\n   *"
  },
  {
    "path": "package.json",
    "chars": 2055,
    "preview": "{\n  \"private\": true,\n  \"repository\": {\n    \"url\": \"https://github.com/zclzone\"\n  },\n  \"license\": \"MIT\",\n  \"author\": {\n  "
  },
  {
    "path": "public/loading/index.css",
    "chars": 1451,
    "preview": ".loading-container {\r\n  position: fixed;\r\n  left: 0;\r\n  top: 0;\r\n  display: flex;\r\n  flex-direction: column;\r\n  justify-"
  },
  {
    "path": "public/loading/index.js",
    "chars": 308,
    "preview": "function addThemeColorCssVars() {\r\n  const key = '__THEME_COLOR__'\r\n  const defaultColor = '#316c72'\r\n  const themeColor"
  },
  {
    "path": "settings/proxy-config.ts",
    "chars": 412,
    "preview": "const proxyConfigMappings: Record<ProxyType, ProxyConfig> = {\r\n  dev: {\r\n    prefix: '/api',\r\n    target: 'http://localh"
  },
  {
    "path": "settings/theme.json",
    "chars": 415,
    "preview": "{\r\n  \"isMobile\": false,\r\n  \"darkMode\": false,\r\n  \"sider\": {\r\n    \"width\": 220,\r\n    \"collapsedWidth\": 64,\r\n    \"collapse"
  },
  {
    "path": "src/App.vue",
    "chars": 250,
    "preview": "<script setup lang=\"ts\">\nimport AppProvider from '@/components/common/AppProvider.vue'\n</script>\n\n<template>\n  <AppProvi"
  },
  {
    "path": "src/api/index.ts",
    "chars": 151,
    "preview": "import { request } from '@/utils'\n\nexport default {\n  getUser: () => request.get('/user'),\n  refreshToken: () => request"
  },
  {
    "path": "src/components/common/AppFooter.vue",
    "chars": 489,
    "preview": "<template>\n  <footer f-c-c flex-col text-14 color=\"#6a6a6a\">\n    <p>\n      Copyright © 2022-present\n      <a\n        hre"
  },
  {
    "path": "src/components/common/AppProvider.vue",
    "chars": 1400,
    "preview": "<script setup lang=\"ts\">\nimport { kebabCase } from 'lodash-es'\nimport { useCssVar } from '@vueuse/core'\nimport type { Gl"
  },
  {
    "path": "src/components/common/ScrollX.vue",
    "chars": 3304,
    "preview": "<script setup lang=\"ts\">\nimport { useDebounceFn } from '@vueuse/core'\n\ninterface Props {\n  showArrow?: boolean\n}\n\nwithDe"
  },
  {
    "path": "src/components/custom/CustomIcon.vue",
    "chars": 358,
    "preview": "<script setup lang=\"ts\">\nimport { renderCustomIcon } from '@/utils'\n\ninterface Props {\n  /** 图标名称(图片的文件名) */\n  icon: str"
  },
  {
    "path": "src/components/custom/SvgIcon.vue",
    "chars": 480,
    "preview": "<script setup lang=\"ts\">\ninterface Props {\n  /** 图标名称(图片的文件名) */\n  icon: string\n  /** 前缀 */\n  prefix?: string\n  color?: "
  },
  {
    "path": "src/components/custom/TheIcon.vue",
    "chars": 571,
    "preview": "<script setup lang=\"ts\">\nimport { renderCustomIcon, renderIcon } from '@/utils'\n\nconst props = withDefaults(defineProps<"
  },
  {
    "path": "src/components/page/AppPage.vue",
    "chars": 396,
    "preview": "<script setup lang=\"ts\">\ninterface Props {\n  showFooter?: boolean\n}\nwithDefaults(defineProps<Props>(), {\n  showFooter: f"
  },
  {
    "path": "src/components/page/CommonPage.vue",
    "chars": 694,
    "preview": "<script setup lang=\"ts\">\ninterface Props {\n  showFooter?: boolean\n  showHeader?: boolean\n  title?: string\n}\nwithDefaults"
  },
  {
    "path": "src/layout/AppMain.vue",
    "chars": 271,
    "preview": "<script setup lang=\"ts\">\nimport { useAppStore } from '@/store'\n\nconst appStore = useAppStore()\n</script>\n\n<template>\n  <"
  },
  {
    "path": "src/layout/header/components/BreadCrumb.vue",
    "chars": 789,
    "preview": "<script setup lang=\"ts\">\nimport { renderCustomIcon, renderIcon } from '@/utils'\nimport type { Meta } from '~/types/route"
  },
  {
    "path": "src/layout/header/components/FullScreen.vue",
    "chars": 344,
    "preview": "<script setup lang=\"ts\">\nimport { useFullscreen } from '@vueuse/core'\n\nconst { isFullscreen, toggle } = useFullscreen()\n"
  },
  {
    "path": "src/layout/header/components/GithubSite.vue",
    "chars": 255,
    "preview": "<script setup lang=\"ts\">\nfunction handleLinkClick() {\n  window.open('https://github.com/zclzone/qs-admin')\n}\n</script>\n\n"
  },
  {
    "path": "src/layout/header/components/MenuCollapse.vue",
    "chars": 342,
    "preview": "<script setup lang=\"ts\">\nimport { useThemeStore } from '@/store'\n\nconst themeStore = useThemeStore()\n</script>\n\n<templat"
  },
  {
    "path": "src/layout/header/components/ThemeMode.vue",
    "chars": 318,
    "preview": "<script lang=\"ts\" setup>\nimport { useThemeStore } from '@/store'\n\nconst theme = useThemeStore()\n</script>\n\n<template>\n  "
  },
  {
    "path": "src/layout/header/components/UserAvatar.vue",
    "chars": 843,
    "preview": "<script setup lang=\"ts\">\nimport { useUserStore } from '@/store'\nimport { renderIcon } from '@/utils'\n\nconst userStore = "
  },
  {
    "path": "src/layout/header/index.vue",
    "chars": 594,
    "preview": "<script setup lang=\"ts\">\nimport BreadCrumb from './components/BreadCrumb.vue'\nimport MenuCollapse from './components/Men"
  },
  {
    "path": "src/layout/index.vue",
    "chars": 1428,
    "preview": "<script setup lang=\"ts\">\nimport SideBar from './sidebar/index.vue'\nimport AppHeader from './header/index.vue'\nimport App"
  },
  {
    "path": "src/layout/sidebar/components/SideLogo.vue",
    "chars": 437,
    "preview": "<script setup lang=\"ts\">\nimport { useThemeStore } from '@/store'\n\nconst title = import.meta.env.VITE_APP_TITLE\n\nconst th"
  },
  {
    "path": "src/layout/sidebar/components/SideMenu.vue",
    "chars": 3710,
    "preview": "<script setup lang=\"ts\">\nimport type { MenuInst, MenuOption } from 'naive-ui'\nimport type { Meta, RouteType } from '~/ty"
  },
  {
    "path": "src/layout/sidebar/index.vue",
    "chars": 187,
    "preview": "<script setup lang=\"ts\">\nimport SideLogo from './components/SideLogo.vue'\nimport SideMenu from './components/SideMenu.vu"
  },
  {
    "path": "src/layout/tab/components/ContextMenu.vue",
    "chars": 2297,
    "preview": "<script setup lang=\"ts\">\nimport { useAppStore, useTabStore } from '@/store'\nimport { renderIcon } from '@/utils'\n\ninterf"
  },
  {
    "path": "src/layout/tab/index.vue",
    "chars": 2263,
    "preview": "<script setup lang=\"ts\">\nimport ContextMenu from './components/ContextMenu.vue'\nimport type { TabItem } from '@/store'\ni"
  },
  {
    "path": "src/main.ts",
    "chars": 456,
    "preview": "import '@/styles/reset.css'\nimport '@/styles/index.scss'\nimport 'uno.css'\nimport 'virtual:svg-icons-register'\n\nimport { "
  },
  {
    "path": "src/router/guard/index.ts",
    "chars": 370,
    "preview": "import type { Router } from 'vue-router'\nimport { createPageLoadingGuard } from './page-loading-guard'\nimport { createPa"
  },
  {
    "path": "src/router/guard/page-loading-guard.ts",
    "chars": 332,
    "preview": "import type { Router } from 'vue-router'\n\nexport function createPageLoadingGuard(router: Router) {\n  router.beforeEach(("
  },
  {
    "path": "src/router/guard/page-title-guard.ts",
    "chars": 342,
    "preview": "import type { Router } from 'vue-router'\n\nconst baseTitle: string = import.meta.env.VITE_APP_TITLE\n\nexport function crea"
  },
  {
    "path": "src/router/guard/permission-guard.ts",
    "chars": 597,
    "preview": "import type { Router } from 'vue-router'\nimport { getToken, isNullOrWhitespace, refreshAccessToken } from '@/utils'\n\ncon"
  },
  {
    "path": "src/router/index.ts",
    "chars": 2040,
    "preview": "import type { App } from 'vue'\nimport { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'\nimport "
  },
  {
    "path": "src/router/routes/index.ts",
    "chars": 1575,
    "preview": "import type { RouteModule, RouteType, RoutesType } from '~/types/router'\n\nconst Layout = () => import('@/layout/index.vu"
  },
  {
    "path": "src/store/index.ts",
    "chars": 169,
    "preview": "import { createPinia } from 'pinia'\r\nimport type { App } from 'vue'\r\n\r\nexport function setupStore(app: App) {\r\n  app.use"
  },
  {
    "path": "src/store/modules/app/index.ts",
    "chars": 464,
    "preview": "import { defineStore } from 'pinia'\n\nexport const useAppStore = defineStore('app', {\n  state() {\n    return {\n      relo"
  },
  {
    "path": "src/store/modules/index.ts",
    "chars": 120,
    "preview": "export * from './app'\nexport * from './permission'\nexport * from './tab'\nexport * from './theme'\nexport * from './user'\n"
  },
  {
    "path": "src/store/modules/permission/helpers.ts",
    "chars": 946,
    "preview": "import type { RouteType, RoutesType } from '~/types/router'\n\nfunction hasPermission(route: RouteType, role: string[]) {\n"
  },
  {
    "path": "src/store/modules/permission/index.ts",
    "chars": 801,
    "preview": "import { defineStore } from 'pinia'\nimport { filterAsyncRoutes } from './helpers'\nimport { asyncRoutes, basicRoutes } fr"
  },
  {
    "path": "src/store/modules/tab/helpers.ts",
    "chars": 179,
    "preview": "import { getSession } from '@/utils'\n\nexport const activeTab = getSession('activeTab')\nexport const tabs = getSession('t"
  },
  {
    "path": "src/store/modules/tab/index.ts",
    "chars": 2214,
    "preview": "import { defineStore } from 'pinia'\nimport { WITHOUT_TAB_PATHS, activeTab, tabs } from './helpers'\nimport { router } fro"
  },
  {
    "path": "src/store/modules/theme/helpers.ts",
    "chars": 2458,
    "preview": "import type { GlobalThemeOverrides } from 'naive-ui'\r\nimport themeSetting from '~/settings/theme.json'\r\nimport { addColo"
  },
  {
    "path": "src/store/modules/theme/index.ts",
    "chars": 1304,
    "preview": "import type { GlobalThemeOverrides } from 'naive-ui'\r\nimport { darkTheme } from 'naive-ui'\r\nimport type { BuiltInGlobalT"
  },
  {
    "path": "src/store/modules/user/index.ts",
    "chars": 1487,
    "preview": "import { defineStore } from 'pinia'\nimport { removeToken, toLogin } from '@/utils'\nimport { usePermissionStore, useTabSt"
  },
  {
    "path": "src/styles/index.scss",
    "chars": 1202,
    "preview": "html {\r\n  font-size: 4px; // * 方便unocss计算:1单位 = 0.25rem = 1px\r\n}\r\n\r\nhtml,\r\nbody {\r\n  width: 100%;\r\n  height: 100%;\r\n  ov"
  },
  {
    "path": "src/styles/reset.css",
    "chars": 419,
    "preview": "html {\r\n  box-sizing: border-box;\r\n}\r\n\r\n*,\r\n::before,\r\n::after {\r\n  margin: 0;\r\n  padding: 0;\r\n  box-sizing: inherit;\r\n}"
  },
  {
    "path": "src/utils/auth/index.ts",
    "chars": 49,
    "preview": "export * from './router'\nexport * from './token'\n"
  },
  {
    "path": "src/utils/auth/router.ts",
    "chars": 460,
    "preview": "import { router } from '@/router'\r\n\r\nexport function toLogin() {\r\n  const currentRoute = unref(router.currentRoute)\r\n  c"
  },
  {
    "path": "src/utils/auth/token.ts",
    "chars": 801,
    "preview": "import { getLocal, getLocalExpire, removeLocal, setLocal } from '@/utils'\r\nimport api from '@/api'\r\n\r\nconst TOKEN_CODE ="
  },
  {
    "path": "src/utils/common/color.ts",
    "chars": 3413,
    "preview": "import { colord, extend } from 'colord'\r\nimport mixPlugin from 'colord/plugins/mix'\r\nimport type { HsvColor } from 'colo"
  },
  {
    "path": "src/utils/common/common.ts",
    "chars": 373,
    "preview": "import dayjs from 'dayjs'\n\ntype Time = undefined | string | Date\n\n/** 格式化时间,默认格式:YYYY-MM-DD HH:mm:ss */\nexport function "
  },
  {
    "path": "src/utils/common/crypto.ts",
    "chars": 534,
    "preview": "import CryptoJS from 'crypto-js'\n\nconst CryptoSecret = '__SecretKey__'\n\n/**\n * 加密数据\n * @param data - 数据\n */\nexport funct"
  },
  {
    "path": "src/utils/common/icon.ts",
    "chars": 498,
    "preview": "import { h } from 'vue'\nimport { Icon } from '@iconify/vue'\nimport { NIcon } from 'naive-ui'\nimport SvgIcon from '@/comp"
  },
  {
    "path": "src/utils/common/index.ts",
    "chars": 147,
    "preview": "export * from './common'\nexport * from './color'\nexport * from './crypto'\nexport * from './icon'\nexport * from './is'\nex"
  },
  {
    "path": "src/utils/common/is.ts",
    "chars": 2353,
    "preview": "const toString = Object.prototype.toString\n\nexport function is(val: unknown, type: string): boolean {\n  return toString."
  },
  {
    "path": "src/utils/common/naiveTools.ts",
    "chars": 598,
    "preview": "import * as NaiveUI from 'naive-ui'\nimport { useThemeStore } from '@/store'\n\nexport function setupNaiveDiscreteApi() {\n "
  },
  {
    "path": "src/utils/http/helpers.ts",
    "chars": 933,
    "preview": "import type { ErrorResolveResponse } from '~/types/axios'\nimport { useUserStore } from '@/store'\n\n/** 自定义错误 */\nexport cl"
  },
  {
    "path": "src/utils/http/index.ts",
    "chars": 496,
    "preview": "import axios from 'axios'\nimport { reqReject, reqResolve, resReject, resResolve } from './interceptors'\n\nexport function"
  },
  {
    "path": "src/utils/http/interceptors.ts",
    "chars": 2206,
    "preview": "import type { AxiosError, AxiosResponse } from 'axios'\nimport { AxiosRejectError, resolveResError } from './helpers'\nimp"
  },
  {
    "path": "src/utils/index.ts",
    "chars": 97,
    "preview": "export * from './auth'\nexport * from './common'\nexport * from './http'\nexport * from './storage'\n"
  },
  {
    "path": "src/utils/storage/index.ts",
    "chars": 50,
    "preview": "export * from './local'\nexport * from './session'\n"
  },
  {
    "path": "src/utils/storage/local.ts",
    "chars": 1429,
    "preview": "import { decrypto, encrypto } from '@/utils'\n\ninterface StorageData {\n  value: unknown\n  expire: number | null\n}\n\n/** 默认"
  },
  {
    "path": "src/utils/storage/session.ts",
    "chars": 537,
    "preview": "import { decrypto, encrypto } from '@/utils'\n\nexport function setSession(key: string, value: unknown) {\n  const json = e"
  },
  {
    "path": "src/views/demo/animation/index.vue",
    "chars": 726,
    "preview": "<script setup lang=\"ts\">\r\nimport { shuffle } from 'lodash-es'\n\r\ninterface NumItem {\r\n  id: number\r\n  num: number\r\n}\r\ncon"
  },
  {
    "path": "src/views/demo/route.ts",
    "chars": 1114,
    "preview": "import type { RouteType } from '~/types/router'\n\nconst Layout = () => import('@/layout/index.vue')\n\nexport default {\n  n"
  },
  {
    "path": "src/views/demo/table/api.ts",
    "chars": 360,
    "preview": "import { request } from '@/utils'\n\nexport default {\n  getPosts: (params = {}) => request.get('posts', { params }),\n  get"
  },
  {
    "path": "src/views/demo/table/index.vue",
    "chars": 5536,
    "preview": "<script setup lang=\"ts\">\nimport { NButton, NSwitch } from 'naive-ui'\nimport { CrudModal, CrudTable, QueryBarItem, useCRU"
  },
  {
    "path": "src/views/demo/unocss/index.vue",
    "chars": 2844,
    "preview": "<template>\n  <CommonPage :show-footer=\"true\">\n    <p>\n      文档:<a hover-decoration-underline c-blue href=\"https://uno.an"
  },
  {
    "path": "src/views/error-page/404.vue",
    "chars": 430,
    "preview": "<script setup lang=\"ts\">\r\nconst { replace } = useRouter()\r\n</script>\r\n\r\n<template>\r\n  <AppPage>\r\n    <n-result m-auto st"
  },
  {
    "path": "src/views/error-page/route.ts",
    "chars": 519,
    "preview": "import type { RouteType } from '~/types/router'\n\nconst Layout = () => import('@/layout/index.vue')\n\nexport default {\n  n"
  },
  {
    "path": "src/views/login/api.ts",
    "chars": 204,
    "preview": "import { request } from '@/utils'\nimport type { RequestConfig } from '~/types/axios'\n\nexport default {\n  login: (data = "
  },
  {
    "path": "src/views/login/index.vue",
    "chars": 3188,
    "preview": "<script setup lang=\"ts\">\r\nimport { useStorage } from '@vueuse/core'\r\nimport api from './api'\r\nimport { getLocal, removeL"
  },
  {
    "path": "src/views/workbench/index.vue",
    "chars": 2616,
    "preview": "<script setup lang=\"ts\">\r\nimport { useUserStore } from '@/store'\r\n\r\nconst userStore = useUserStore()\r\n</script>\r\n\r\n<temp"
  },
  {
    "path": "src/views/workbench/route.ts",
    "chars": 441,
    "preview": "import type { RouteType } from '~/types/router'\n\nconst Layout = () => import('@/layout/index.vue')\n\nexport default {\n  n"
  },
  {
    "path": "tsconfig.json",
    "chars": 541,
    "preview": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"module\": \"ESNext\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"ESNext\""
  },
  {
    "path": "types/axios.d.ts",
    "chars": 282,
    "preview": "import type { AxiosRequestConfig } from 'axios'\n\ninterface RequestConfig extends AxiosRequestConfig {\n  /** 接口是否需要token "
  },
  {
    "path": "types/env.d.ts",
    "chars": 525,
    "preview": "type ProxyType = 'dev' | 'test' | 'prod';\r\n\r\ninterface ViteEnv {\r\n  VITE_PORT: number\r\n  VITE_USE_MOCK?: boolean\r\n  VITE"
  },
  {
    "path": "types/global.d.ts",
    "chars": 251,
    "preview": "interface Window {\r\n  $loadingBar?: import('naive-ui').LoadingBarProviderInst;\r\n  $dialog?: import('naive-ui').DialogPro"
  },
  {
    "path": "types/router.d.ts",
    "chars": 499,
    "preview": "import { RouteRecordRaw } from 'vue-router'\r\n\r\ninterface Meta {\r\n  title?: string\r\n  icon?: string\r\n  customIcon?: strin"
  },
  {
    "path": "types/shims.d.ts",
    "chars": 184,
    "preview": "declare module '*.vue' {\r\n  import type { DefineComponent } from 'vue'\r\n  const component: DefineComponent<{}, {}, any>\r"
  },
  {
    "path": "types/theme.d.ts",
    "chars": 727,
    "preview": "/** 侧边栏 */\r\ninterface Sider {\r\n  width: number\r\n  /** 折叠时的宽度 */\r\n  collapsedWidth: number\r\n  /** 是否折叠 */\r\n  collapsed: b"
  },
  {
    "path": "uno.config.ts",
    "chars": 1971,
    "preview": "import { defineConfig, presetAttributify, presetUno } from 'unocss'\r\n\r\nexport default defineConfig({\r\n  exclude: ['node_"
  },
  {
    "path": "vercel.json",
    "chars": 96,
    "preview": "{\n  \"rewrites\": [\n    {\n      \"source\": \"/(.*)\",\n      \"destination\": \"/index.html\"\n    }\n  ]\n}\n"
  },
  {
    "path": "vite.config.ts",
    "chars": 1161,
    "preview": "import type { ConfigEnv } from 'vite'\nimport { defineConfig, loadEnv } from 'vite'\n\nimport { convertEnv, getRootPath, ge"
  }
]

About this extraction

This page contains the full source code of the zclzone/qs-admin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 111 files (102.7 KB), approximately 32.6k tokens, and a symbol index with 152 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!