├── .env.development
├── .env.production
├── .env.test
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── CHANGELOG.md
├── CHANGELOG_zh.md
├── LICENSE
├── README.md
├── components.d.ts
├── deploy.sh
├── index.html
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
└── favicon.ico
├── src
├── App.vue
├── api
│ ├── http
│ │ ├── cancelAbort.ts
│ │ ├── composables
│ │ │ └── useApi.ts
│ │ ├── createDialog.ts
│ │ ├── fetch.ts
│ │ ├── index.ts
│ │ ├── interceptors
│ │ │ └── interceptors.ts
│ │ └── typing.ts
│ └── moduels
│ │ ├── basicModel.ts
│ │ ├── demo
│ │ ├── app.ts
│ │ └── dataModel
│ │ │ └── appModel.ts
│ │ └── fast-api
│ │ ├── login
│ │ ├── index.ts
│ │ └── model
│ │ │ └── model.ts
│ │ └── menu
│ │ ├── index.ts
│ │ └── model
│ │ └── menuModel.ts
├── assets
│ ├── gif
│ │ └── 404.gif
│ ├── icons
│ │ └── area.svg
│ ├── logo.png
│ └── svg
│ │ └── background.svg
├── components
│ ├── AntVG2
│ │ ├── G2Chart.vue
│ │ ├── composables
│ │ │ └── useInnerChart.ts
│ │ └── useChart.ts
│ ├── AppProvider
│ │ ├── AppProvider.vue
│ │ └── OuterFeedback.tsx
│ ├── Application
│ │ └── Settings
│ │ │ ├── ThemeTool.vue
│ │ │ └── type.d.ts
│ ├── Button
│ │ └── PButton.vue
│ ├── Error
│ │ ├── Error403.vue
│ │ ├── Error404.vue
│ │ └── Error500.vue
│ ├── Form
│ │ ├── component.ts
│ │ ├── components
│ │ │ ├── PearForm.vue
│ │ │ └── PearFormItem.tsx
│ │ └── composables
│ │ │ ├── usePearForm.ts
│ │ │ └── usePearFormModel.ts
│ ├── Icon
│ │ └── Icon.vue
│ ├── Modal
│ │ └── PModal.vue
│ ├── PageWrapper
│ │ ├── Breadcrumb
│ │ │ ├── Breadcrumb.vue
│ │ │ └── useBreadcrumb.ts
│ │ └── PageWrapper.vue
│ └── Table
│ │ ├── components
│ │ ├── ColumnSetting.vue
│ │ ├── PearTable.vue
│ │ ├── Reload.vue
│ │ ├── ResizeHeight.vue
│ │ ├── SizeSetting.vue
│ │ └── TableTools.vue
│ │ └── composables
│ │ ├── useColumns.ts
│ │ ├── usePagination.ts
│ │ ├── usePearTable.ts
│ │ ├── useSearchFormExpand.ts
│ │ ├── useTableBaseConfig.ts
│ │ ├── useTableContext.ts
│ │ └── useTableRequest.ts
├── composables
│ ├── useBreakPoint.ts
│ ├── useContext.ts
│ ├── usePromiseFn.ts
│ ├── useRouterViewRefresh.ts
│ └── useUiConfig
│ │ └── useUiConfig.ts
├── config
│ ├── index.ts
│ ├── menu.config.ts
│ └── theme.config.ts
├── enums
│ └── breakPointEnum.ts
├── layouts
│ ├── BasicLayout.vue
│ ├── ParentLayout.tsx
│ ├── content
│ │ ├── PearContent.vue
│ │ ├── RouteTabs.vue
│ │ ├── TabRefresh.vue
│ │ ├── TabsAction.vue
│ │ └── useRouteTab.ts
│ ├── createLayoutContextData.ts
│ ├── footer
│ │ └── Footer.vue
│ ├── header
│ │ ├── AppSetting.vue
│ │ ├── FullScreen.vue
│ │ ├── PearHeader.vue
│ │ └── UserDropdown.vue
│ ├── index.ts
│ ├── menu
│ │ ├── PearMenu.vue
│ │ └── useMenu.ts
│ ├── sider
│ │ ├── AppLogo.vue
│ │ └── PearSider.vue
│ └── useLayoutBreakPoint.ts
├── main.ts
├── mock
│ ├── createFetchSever.ts
│ ├── mockUtil.ts
│ ├── modules
│ │ ├── chartData.ts
│ │ ├── gdp.json
│ │ ├── system.ts
│ │ ├── tableDemo.ts
│ │ └── useApiHooks.ts
│ └── useMock.ts
├── router
│ ├── guard
│ │ ├── index.ts
│ │ └── permissionGuard.ts
│ ├── index.ts
│ ├── modules
│ │ ├── components
│ │ │ └── index.ts
│ │ ├── dashboard
│ │ │ └── index.ts
│ │ ├── errors.ts
│ │ ├── errors
│ │ │ └── index.ts
│ │ ├── feature
│ │ │ └── index.ts
│ │ ├── form
│ │ │ └── index.ts
│ │ ├── login.ts
│ │ ├── root.ts
│ │ ├── system
│ │ │ └── index.ts
│ │ ├── table
│ │ │ └── index.ts
│ │ ├── top
│ │ │ └── index.ts
│ │ └── util-demo
│ │ │ └── index.ts
│ ├── routes.ts
│ └── util.tsx
├── store
│ ├── index.ts
│ └── modules
│ │ ├── app.ts
│ │ └── userInfo.ts
├── style
│ ├── global.less
│ └── transition.less
├── utils
│ ├── componentUtil.ts
│ └── utils.ts
└── views
│ ├── demo
│ ├── components
│ │ └── antvG2
│ │ │ ├── index.vue
│ │ │ ├── renderGameChart.ts
│ │ │ └── service.ts
│ ├── dashboard
│ │ ├── analysis
│ │ │ ├── index.vue
│ │ │ └── renderChart
│ │ │ │ ├── renderDynamicChart.ts
│ │ │ │ └── renderLineChart.ts
│ │ └── workspace
│ │ │ └── index.vue
│ ├── feature
│ │ └── keep-alive
│ │ │ └── index.vue
│ ├── form
│ │ ├── BasicFormDemo.vue
│ │ ├── UseFormDemo.vue
│ │ └── UseFormRefDemo.vue
│ ├── system
│ │ ├── account
│ │ │ ├── hidePage.vue
│ │ │ └── index.vue
│ │ └── menus
│ │ │ └── index.vue
│ ├── table
│ │ ├── BasicTableDemo.vue
│ │ ├── DefTableHead.vue
│ │ ├── SearchTableDemo.vue
│ │ └── service.ts
│ ├── top-level
│ │ ├── index.vue
│ │ └── index2.vue
│ └── utils-demo
│ │ ├── composables
│ │ └── usePromiseFn.vue
│ │ └── http
│ │ ├── service.ts
│ │ ├── topAwait.vue
│ │ └── useApiHooks.vue
│ ├── error
│ ├── 403.vue
│ ├── 404.vue
│ └── 500.vue
│ ├── fast-api
│ └── role
│ │ └── index.vue
│ └── login
│ ├── index.vue
│ └── type.ts
├── tsconfig.json
├── types
├── env.d.ts
├── global.d.ts
└── window.d.ts
└── vite.config.ts
/.env.development:
--------------------------------------------------------------------------------
1 | VITE_FETCH_PREFIX_URL =
2 |
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | VITE_FETCH_PREFIX_URL =
2 |
--------------------------------------------------------------------------------
/.env.test:
--------------------------------------------------------------------------------
1 | VITE_BASIC_FETCH_URL = https://localhost:3000
2 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // eslint-disable-next-line @typescript-eslint/no-var-requires
3 | const { defineConfig } = require('eslint-define-config')
4 | module.exports = defineConfig({
5 | root: true,
6 | env: {
7 | browser: true,
8 | node: true,
9 | es6: true
10 | },
11 | parser: 'vue-eslint-parser',
12 | parserOptions: {
13 | parser: '@typescript-eslint/parser',
14 | ecmaVersion: 2020,
15 | sourceType: 'module',
16 | jsxPragma: 'React',
17 | ecmaFeatures: {
18 | jsx: true
19 | }
20 | },
21 | extends: [
22 | 'plugin:vue/vue3-recommended',
23 | 'plugin:@typescript-eslint/recommended',
24 | 'prettier',
25 | 'plugin:prettier/recommended'
26 | ],
27 | rules: {
28 | '@typescript-eslint/explicit-module-boundary-types': 'off',
29 | '@typescript-eslint/no-empty-function': 'off',
30 | '@typescript-eslint/no-explicit-any': 'off',
31 | 'vue/one-component-per-file': 'off',
32 | '@typescript-eslint/ban-ts-comment': 'off',
33 | 'vue/multiline-html-element-content-newline': 'off',
34 | 'vue/singleline-html-element-content-newline': 'off',
35 | 'vue/max-attributes-per-line': 'off',
36 | 'vue/require-default-prop': 'off',
37 | 'vue/no-setup-props-destructure': 'off',
38 | 'vue/multi-word-component-names': 'off',
39 | 'vue/html-self-closing': [
40 | 'error',
41 | {
42 | html: {
43 | void: 'always',
44 | normal: 'never',
45 | component: 'always',
46 | },
47 | svg: 'always',
48 | math: 'always',
49 | },
50 | ],
51 | }
52 | })
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 | .idea
7 | dist.zip
8 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "trailingComma": "none",
5 | "vueIndentScriptAndStyle": true,
6 | "printWidth": 100,
7 | "tabWidth": 2,
8 | "useTabs": false,
9 | "quoteProps": "as-needed",
10 | "bracketSpacing": true,
11 | "jsxSingleQuote": false,
12 | "arrowParens": "always",
13 | "insertPragma": false,
14 | "requirePragma": false,
15 | "proseWrap": "never",
16 | "htmlWhitespaceSensitivity": "strict",
17 | "endOfLine": "auto"
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [1.0.5](https://github.com/pearadmin/pear-admin-naive/releases/tag/1.0.5) (2022-05-20)
2 | ### Feature
3 | * **upgrade deps** some deps upgrade
4 | * **KeepAlive** router support `keep-alive`
5 | * **Docs** add docs
6 |
7 |
8 | ## [1.0.4](https://github.com/pearadmin/pear-admin-naive/releases/tag/1.0.4) (2022-02-21)
9 | ### Optimize
10 | * **upgrade deps** some deps upgrade
11 | * **g2 data source changed** form `github` to `mock`
12 |
13 | ## [1.0.3](https://github.com/pearadmin/pear-admin-naive/releases/tag/1.0.3) (2022-01-05)
14 | ### Feature
15 | * **Docs** add docs site
16 | * **PearForm** more feature with `PearForm` demo
17 | * **Route** add `lateral route mode` in router
18 | * **ErrorPage** add `404`, `403`, `500` error pages
19 | * **useApi:** setting `redo:true` with `hooks: useApi`, can specify `debounce` as number(ms) to enable function throttling requests
20 | * **useTableRequest:** setting `redo:true` with `tableHooks: useTableRequest`, can specify `debounce` as number(ms) to enable function throttling requests
21 | * **routeTabs** add `close left`, `close right`, `close other` feature in RouteTab
22 |
23 | ### Fix
24 | * **PageWrapper** fix `PageWrapper` only has default slot letTopRight padding
25 |
26 | ## [1.0.2](https://github.com/pearadmin/pear-admin-naive/releases/tag/1.0.2) (2021-12-20)
27 |
28 | ### Optimize
29 | * **composables:** optimize `useForm` 、` useTable `
30 |
31 | ### Refactor
32 | * **Component** Modify the way to register components inside `PearForm`, `PearTable` to fix the problem of not rendering after hot update in development mode
33 |
34 | ### Feature
35 | * **PearForm** `PearFormItem` supports functional props passing
36 | * **PearTable** PearTable query table header adds `pick up` and `expand` feature
37 | * **pages:** `BasisFormDemo` page optimization, new custom query table header function, query table header support automatic request function after parameter change
38 |
39 | ## [1.0.1](https://github.com/pearadmin/pear-admin-naive/releases/tag/1.0.1) (2021-12-15)
40 |
41 | ### Feature
42 | * **composables:** Optimize `useContext`, used it in `layouts` Component and `BasicTable` Component
43 | * **layout:** Mobile view Support
44 |
45 | ## [1.0.0](https://github.com/pearadmin/pear-admin-naive/releases/tag/1.0.0) (2021-12-04)
46 |
47 | * init project
48 |
--------------------------------------------------------------------------------
/CHANGELOG_zh.md:
--------------------------------------------------------------------------------
1 | ### Feature
2 | * **依赖升级** 更新部分依赖
3 | * **unocss** 带破坏性更新: 使用unocss替换windicss
4 |
5 |
6 | ## [1.0.5](https://github.com/pearadmin/pear-admin-naive/releases/tag/1.0.5) (2022-05-20)
7 | ### Feature
8 | * **依赖升级** 更新部分依赖
9 | * **KeepAlive** 路由支持KeepAlive配置
10 | * **Docs** 新增Docs文档
11 |
12 |
13 | ## [1.0.4](https://github.com/pearadmin/pear-admin-naive/releases/tag/1.0.4) (2022-02-21)
14 | ### Optimize
15 | * **依赖升级** 更新部分依赖
16 | * **g2图表数据源切换** 从github中的请求移至mock中。
17 |
18 | ## [1.0.3](https://github.com/pearadmin/pear-admin-naive/releases/tag/1.0.3) (2022-01-05)
19 | ### Feature
20 | * **Docs** 新增使用文档
21 | * **PearForm** 表单 PearForm Demo更多功能
22 | * **Route** 新增平级模式路由
23 | * **ErrorPage** 完善404,403,500错误页面
24 | * **useApi:** 新增设置 redo:true时,可指定 `debounce` 为number(ms)来开启函数节流请求
25 | * **useTableRequest:** 新增设置 redo:true时,可指定 `debounce` 为number(ms)来开启函数节流请求
26 | * **routeTabs** 新增 RouteTab`关闭左侧`,`关闭右侧`,`关闭其它` 功能
27 |
28 | ### Fix
29 | * **PageWrapper** 修复PageWrapper只有内容时左上右边距存在的问题
30 |
31 | ## [1.0.2](https://github.com/pearadmin/pear-admin-naive/releases/tag/1.0.2) (2021-12-20)
32 |
33 | ### Optimize
34 | * **composables:** 优化 `useForm` 、` useTable `
35 |
36 | ### Refactor
37 | * **Component** 修改PearForm, PearTable内部注册组件的方式,修正开发模式下,热更新后不渲染的问题
38 |
39 | ### Feature
40 | * **PearForm** PearFormItem支持函数式props传递
41 | * **PearTable** PearTable查询表头新增`收起`、`展开`功能
42 | * **pages:** basisFormDemo页面优化,新增自定义查询表头功能,查询表头支持参数改变后自动请求功能
43 |
44 |
45 | ## [1.0.1](https://github.com/pearadmin/pear-admin-naive/releases/tag/1.0.1) (2021-12-15)
46 |
47 | ### Feature
48 | * **composables:** 优化 `useContext`, 在`layout` 和 `BasicTable`使用
49 | * **layout:** 移动端展示支持
50 |
51 | ## [1.0.0](https://github.com/pearadmin/pear-admin-naive/releases/tag/1.0.0) (2021-12-04)
52 |
53 | * 初始化项目
54 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 落小梅
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Pear Admin Naive
6 |
7 |
8 |
9 | 开 箱 即 用 的 Vue3 与 Naive UI 企 业 级 开 发 模 板
10 |
11 |
12 |
预览
13 |
14 | [官 网](http://www.pearadmin.com/) | [交流](https://jq.qq.com/?_wv=1027&k=5OdSmve) | [社区](http://forum.pearadmin.com/)
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
40 |
41 | ### 使用文档
42 | [文档地址](http://naive-doc.pearadmin.com/)
43 |
44 | ### 🌈 项目概述
45 |
46 | - 基于 Vue 3 setup script语法 与 Naive UI 实现的通用中后台管理模板。整合最新技术高效快速开发,前后端分离模式,开箱即用。
47 | - 借鉴Vben的思想(但不包涵任何相关代码)
48 |
49 | [//]: # (### ☘ 更新日志)
50 |
51 | [//]: # (更新日志 [查看日志](https://gitee.com/pear-admin/pear-admin-naive-min/releases))
52 |
53 | ### 🍚 功能概览
54 |
55 | - [x] 请求模块: 请求使用umi-request,支持供常用的调用方式和hook调用方式(useApi)。
56 |
57 | ### 🔨 项目结构
58 |
59 | ```
60 | Pear Admin Naive Min
61 | │
62 | ├─src 源码
63 | │
64 | └─package.json Npm 配置
65 |
66 | ```
67 |
68 | ### ⚡ 快速启动
69 |
70 | ```
71 |
72 | 切换环境
73 |
74 | nvm install 16.0.0
75 |
76 | nvm use 16.0.0
77 |
78 | 安装依赖
79 |
80 | npm install --global pnpm
81 |
82 | pnpm install
83 |
84 | 启动项目
85 |
86 | pnpm run serve
87 |
88 | ```
89 |
90 | ### 📖 帮助文档
91 |
92 | [项目文档](http://naive-doc.pearadmin.com/)
93 | 除却需要jsx支持的组件外,其它均采用 setup-script 语法,[详情](https://github.com/vuejs/rfcs/pull/227#issuecomment-870105222)
94 |
95 |
96 | 👉 编写中
97 |
98 | [//]: # (### 🍎 预览界面)
99 |
100 | [//]: # ()
101 | [//]: # (| 预览 | 界面 |)
102 |
103 | [//]: # (| --- | --- |)
104 |
105 | [//]: # (|  |  |)
106 |
107 | ### 💐 特别鸣谢
108 |
109 | - 👉 Vue Next:[https://github.com/vuejs/vue-next](https://github.com/vuejs/vue-next)
110 | - 👉 Vue Use:[https://github.com/vueuse/vueuse](https://github.com/vueuse/vueuse)
111 | - 👉 Naive UI:[https://github.com/TuSimple/naive-ui](https://github.com/TuSimple/naive-ui)
112 |
113 | ### 🍻 贡献代码
114 |
115 |
116 |
117 | 1. 欢迎提交 [pull request](https://gitee.com/pear-admin/pear-admin-naive/pulls),注意对应提交对应 `develop` 分支
118 |
119 | 2. 欢迎提交 [issue](https://gitee.com/pear-admin/pear-admin-naive/issues),请写清楚遇到问题的原因、开发环境、复显步骤。
120 |
121 |
122 |
123 | ### start 趋势
124 |
125 | [](https://giteye.net/chart/GVB5WBKG)
126 |
127 | ### 贡献列表
128 |
129 | [](https://giteye.net/chart/8EQS6NZQ)
130 |
131 | 感谢每一位贡献代码的朋友。
132 |
133 | 如果对您有帮助,您可以点右上角 💘Star💘 支持
134 |
--------------------------------------------------------------------------------
/components.d.ts:
--------------------------------------------------------------------------------
1 | // generated by unplugin-vue-components
2 | // We suggest you to commit this file into source control
3 | // Read more: https://github.com/vuejs/vue-next/pull/3399
4 |
5 | declare module 'vue' {
6 | export interface GlobalComponents {
7 | AppLogo: typeof import('./src/layouts/sider/AppLogo.vue')['default']
8 | AppProvider: typeof import('./src/components/AppProvider/AppProvider.vue')['default']
9 | AppSetting: typeof import('./src/layouts/header/AppSetting.vue')['default']
10 | BasicLayout: typeof import('./src/layouts/BasicLayout.vue')['default']
11 | Breadcrumb: typeof import('./src/components/PageWrapper/Breadcrumb/Breadcrumb.vue')['default']
12 | ColumnSetting: typeof import('./src/components/Table/components/ColumnSetting.vue')['default']
13 | Error403: typeof import('./src/components/Error/Error403.vue')['default']
14 | Error404: typeof import('./src/components/Error/Error404.vue')['default']
15 | Error500: typeof import('./src/components/Error/Error500.vue')['default']
16 | Footer: typeof import('./src/layouts/footer/Footer.vue')['default']
17 | FullScreen: typeof import('./src/layouts/header/FullScreen.vue')['default']
18 | G2Chart: typeof import('./src/components/AntVG2/G2Chart.vue')['default']
19 | Icon: typeof import('./src/components/Icon/Icon.vue')['default']
20 | NA: typeof import('naive-ui')['NA']
21 | NAlert: typeof import('naive-ui')['NAlert']
22 | NAvatar: typeof import('naive-ui')['NAvatar']
23 | NBadge: typeof import('naive-ui')['NBadge']
24 | NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
25 | NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
26 | NButton: typeof import('naive-ui')['NButton']
27 | NCalendar: typeof import('naive-ui')['NCalendar']
28 | NCard: typeof import('naive-ui')['NCard']
29 | NCheckbox: typeof import('naive-ui')['NCheckbox']
30 | NConfigProvider: typeof import('naive-ui')['NConfigProvider']
31 | NDataTable: typeof import('naive-ui')['NDataTable']
32 | NDialogProvider: typeof import('naive-ui')['NDialogProvider']
33 | NDivider: typeof import('naive-ui')['NDivider']
34 | NDropdown: typeof import('naive-ui')['NDropdown']
35 | NElement: typeof import('naive-ui')['NElement']
36 | NForm: typeof import('naive-ui')['NForm']
37 | NFormItemGi: typeof import('naive-ui')['NFormItemGi']
38 | NGi: typeof import('naive-ui')['NGi']
39 | NGlobalStyle: typeof import('naive-ui')['NGlobalStyle']
40 | NGradientText: typeof import('naive-ui')['NGradientText']
41 | NGrid: typeof import('naive-ui')['NGrid']
42 | NH4: typeof import('naive-ui')['NH4']
43 | NH5: typeof import('naive-ui')['NH5']
44 | NInputGroup: typeof import('naive-ui')['NInputGroup']
45 | NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
46 | NInputNumber: typeof import('naive-ui')['NInputNumber']
47 | NLayout: typeof import('naive-ui')['NLayout']
48 | NLayoutContent: typeof import('naive-ui')['NLayoutContent']
49 | NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
50 | NLayoutSider: typeof import('naive-ui')['NLayoutSider']
51 | NMenu: typeof import('naive-ui')['NMenu']
52 | NMessageProvider: typeof import('naive-ui')['NMessageProvider']
53 | NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
54 | NPageHeader: typeof import('naive-ui')['NPageHeader']
55 | NPopover: typeof import('naive-ui')['NPopover']
56 | NPopselect: typeof import('naive-ui')['NPopselect']
57 | NResult: typeof import('naive-ui')['NResult']
58 | NScrollbar: typeof import('naive-ui')['NScrollbar']
59 | NSlider: typeof import('naive-ui')['NSlider']
60 | NSpace: typeof import('naive-ui')['NSpace']
61 | NSpin: typeof import('naive-ui')['NSpin']
62 | NStatistic: typeof import('naive-ui')['NStatistic']
63 | NSwitch: typeof import('naive-ui')['NSwitch']
64 | NTag: typeof import('naive-ui')['NTag']
65 | NText: typeof import('naive-ui')['NText']
66 | NThemeEditor: typeof import('naive-ui')['NThemeEditor']
67 | NTooltip: typeof import('naive-ui')['NTooltip']
68 | PageWrapper: typeof import('./src/components/PageWrapper/PageWrapper.vue')['default']
69 | PButton: typeof import('./src/components/Button/PButton.vue')['default']
70 | PearContent: typeof import('./src/layouts/content/PearContent.vue')['default']
71 | PearForm: typeof import('./src/components/Form/components/PearForm.vue')['default']
72 | PearHeader: typeof import('./src/layouts/header/PearHeader.vue')['default']
73 | PearMenu: typeof import('./src/layouts/menu/PearMenu.vue')['default']
74 | PearSider: typeof import('./src/layouts/sider/PearSider.vue')['default']
75 | PearTable: typeof import('./src/components/Table/components/PearTable.vue')['default']
76 | PModal: typeof import('./src/components/Modal/PModal.vue')['default']
77 | Reload: typeof import('./src/components/Table/components/Reload.vue')['default']
78 | ResizeHeight: typeof import('./src/components/Table/components/ResizeHeight.vue')['default']
79 | RouteTabs: typeof import('./src/layouts/content/RouteTabs.vue')['default']
80 | SizeSetting: typeof import('./src/components/Table/components/SizeSetting.vue')['default']
81 | TableTools: typeof import('./src/components/Table/components/TableTools.vue')['default']
82 | TabRefresh: typeof import('./src/layouts/content/TabRefresh.vue')['default']
83 | TabsAction: typeof import('./src/layouts/content/TabsAction.vue')['default']
84 | ThemeTool: typeof import('./src/components/Application/Settings/ThemeTool.vue')['default']
85 | UserDropdown: typeof import('./src/layouts/header/UserDropdown.vue')['default']
86 | }
87 | }
88 |
89 | export { }
90 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | pnpm run build &&
3 | pwd &&
4 | zip -q -r -o dist.zip dist &&
5 | scp -i '/Users/jiabinbin/.ssh/root' dist.zip root@115.126.75.120:/www/admin/naive.pearadmin.com_80/ &&
6 | ssh -i '/Users/jiabinbin/.ssh/root' root@115.126.75.120 'cd /www/admin/naive.pearadmin.com_80/ ; ./deploy.sh' &&
7 | rm -rf dist.zip &&
8 | rm -rf dist &&
9 | git status &&
10 | git add . &&
11 | git status &&
12 | git commit -m 'feat: routes sort' &&
13 | git push origin master &&
14 | echo 'task finished'
15 | ##
16 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pear Admin Naive
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pear-admin-naive",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "serve": "vite",
6 | "serve:force": "vite --force",
7 | "serve:force:debug": "vite --force --debug",
8 | "dev": "pnpm run serve",
9 | "build": "rimraf dist && vue-tsc --noEmit && vite build",
10 | "ts:check": "vue-tsc --noEmit",
11 | "preview:local": "rimraf dist && vite build && vite preview",
12 | "clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite",
13 | "clean:lib": "rimraf node_modules",
14 | "lint:eslint": "eslint --cache --max-warnings 0 \"src/**/*.{vue,ts,tsx}\" --fix",
15 | "lint:prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\""
16 | },
17 | "dependencies": {
18 | "@antv/data-set": "^0.11.8",
19 | "@antv/g2": "^4.2.1",
20 | "@iconify/iconify": "^2.2.1",
21 | "@types/lodash": "^4.14.182",
22 | "@vueuse/components": "^7.7.1",
23 | "@vueuse/core": "^6.9.2",
24 | "date-fns": "^2.28.0",
25 | "mockjs": "^1.1.0",
26 | "pinia": "^2.0.14",
27 | "qs": "^6.10.3",
28 | "umi-request": "^1.4.0",
29 | "vue": "^3.2.35",
30 | "vue-router": "^4.0.15"
31 | },
32 | "devDependencies": {
33 | "@iconify/json": "^1.1.461",
34 | "@nabla/vite-plugin-eslint": "^1.4.0",
35 | "@purge-icons/generated": "^0.7.0",
36 | "@types/lodash-es": "^4.17.6",
37 | "@types/node": "^17.0.35",
38 | "@typescript-eslint/eslint-plugin": "^4.33.0",
39 | "@typescript-eslint/parser": "^4.33.0",
40 | "@vitejs/plugin-legacy": "^1.8.2",
41 | "@vitejs/plugin-vue": "^2.3.3",
42 | "@vitejs/plugin-vue-jsx": "^1.3.10",
43 | "@vue/eslint-config-prettier": "^7.0.0",
44 | "@vue/eslint-config-typescript": "^10.0.0",
45 | "canvas": "^2.9.1",
46 | "eslint": "^7.32.0",
47 | "eslint-define-config": "^1.4.1",
48 | "eslint-plugin-prettier": "^4.0.0",
49 | "eslint-plugin-vue": "^8.7.1",
50 | "less": "^4.1.2",
51 | "lodash-es": "^4.17.21",
52 | "naive-ui": "^2.29.0",
53 | "postcss": "^8.4.14",
54 | "prettier": "^2.6.2",
55 | "rimraf": "^3.0.2",
56 | "rollup": "^2.74.1",
57 | "typescript": "^4.6.4",
58 | "unocss": "^0.34.0",
59 | "unplugin-vue-components": "^0.17.21",
60 | "unplugin-vue-define-options": "^0.6.1",
61 | "vfonts": "^0.1.0",
62 | "vite": "^2.9.9",
63 | "vite-plugin-mock": "^2.9.6",
64 | "vite-plugin-purge-icons": "^0.7.0",
65 | "vite-plugin-windicss": "^1.8.4",
66 | "vue-tsc": "^0.31.4",
67 | "windicss": "^3.5.4"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'postcss-windicss': { /* ... */ },
4 | },
5 | }
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pearadmin/pear-admin-naive/bba0ae576bd86e83d6581fb33b7cb9c16cce87ad/public/favicon.ico
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
20 |
--------------------------------------------------------------------------------
/src/api/http/cancelAbort.ts:
--------------------------------------------------------------------------------
1 | const controller = new AbortController() // 创建一个控制器
2 | const { signal } = controller
3 |
4 | signal.addEventListener('abort', () => {
5 | // console.log('aborted!')
6 | })
7 |
--------------------------------------------------------------------------------
/src/api/http/composables/useApi.ts:
--------------------------------------------------------------------------------
1 | import { computed, ref, watch } from 'vue'
2 | import type { ComputedRef, Ref, UnwrapRef } from 'vue'
3 | import type { RequestOptionsInit } from 'umi-request'
4 | import type { MaybeRef } from '@vueuse/core'
5 | import { get } from '@vueuse/core'
6 | import { merge, omit } from 'lodash-es'
7 | import request from '../fetch'
8 | import { useDebounceFn } from '@vueuse/core'
9 |
10 | export type FetchMethod = 'get' | 'post' | 'delete' | 'put' | 'patch' | 'head' | 'options' | 'rpc'
11 |
12 | export interface UseApiOptions extends RequestOptionsInit {
13 | url: MaybeRef
14 | method?: FetchMethod
15 | data?: MaybeRef
16 | params?: MaybeRef
17 | showErrorType?: 'Message' | 'Dialog' | 'Notification'
18 | }
19 |
20 | export interface UseApiConfig> {
21 | immediate?: boolean
22 | redo?: boolean
23 | initialData?: T
24 | throwErr?: boolean
25 | debounce?: number
26 | }
27 |
28 | export interface UseApiReturnType {
29 | // data: Ref>
30 | data: Ref>
31 | loading: Ref>
32 | finished: Ref>
33 | error: Ref>
34 | executor: ComputedRef<(args?: RequestOptionsInit) => Promise>
35 | }
36 |
37 | export function useApi(
38 | options: UseApiOptions,
39 | config?: UseApiConfig
40 | ): UseApiReturnType {
41 | let useConfig: UseApiConfig = {
42 | immediate: true,
43 | initialData: null,
44 | redo: false,
45 | throwErr: false,
46 | debounce: 0
47 | }
48 |
49 | if (config) {
50 | useConfig = { ...useConfig, ...config }
51 | }
52 |
53 | // http url
54 | const fetchUrl = computed((): string => {
55 | return get(options.url)
56 | })
57 |
58 | // http params
59 | const fetchOptions = computed((): RequestOptionsInit => {
60 | return {
61 | ...omit(options, 'data', 'params', 'url', 'method'),
62 | data: get(options.data),
63 | params: get(options.params)
64 | }
65 | })
66 |
67 | // return define
68 | const data = ref(useConfig.initialData as T)
69 | const loading = ref(false)
70 | const finished = ref(false)
71 | const error = ref(null)
72 |
73 | const executor = computed((): ((args?: RequestOptionsInit) => Promise) => {
74 | return (args?: RequestOptionsInit) => {
75 | const method = options.method ?? 'get'
76 | return new Promise((resolve, reject) => {
77 | loading.value = true
78 | finished.value = false
79 | data.value = null
80 | error.value = null
81 | const requestOption = merge({}, fetchOptions.value, args)
82 | request[method](fetchUrl.value, requestOption)
83 | .then((response) => {
84 | data.value = response.data as T
85 | error.value = null
86 | resolve(response.data as T)
87 | })
88 | .catch((err) => {
89 | data.value = null
90 | error.value = err
91 | if (useConfig.throwErr) {
92 | throw err
93 | } else {
94 | reject(err)
95 | console.error('fetch fail => ', err)
96 | }
97 | })
98 | .finally(() => {
99 | loading.value = false
100 | finished.value = true
101 | })
102 | })
103 | }
104 | })
105 |
106 | const debouncedFn = useDebounceFn(async () => {
107 | await get(executor)()
108 | }, useConfig?.debounce)
109 |
110 | watch(
111 | [fetchUrl, fetchOptions],
112 | async () => {
113 | if (useConfig.redo) {
114 | if (useConfig.debounce && useConfig.debounce > 0) {
115 | await debouncedFn()
116 | } else {
117 | await get(executor)()
118 | }
119 | }
120 | },
121 | { deep: true }
122 | )
123 |
124 | if (useConfig.immediate) {
125 | get(executor)()
126 | }
127 |
128 | return {
129 | data,
130 | loading,
131 | finished,
132 | error,
133 | executor
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/api/http/createDialog.ts:
--------------------------------------------------------------------------------
1 | import type { DialogOptions } from 'naive-ui'
2 |
3 | interface CreateDialog extends DialogOptions {
4 | duration?: number
5 | }
6 |
7 | /**
8 | * dialog duration
9 | * @param options
10 | */
11 | export function createDialog(options: CreateDialog) {
12 | if (window['dialogInstance']) {
13 | clearTimeout(window['dialogInstance'])
14 | }
15 | window.$dialog.create(options)
16 | if (options.duration) {
17 | window['dialogInstance'] = setTimeout(() => {
18 | window.$dialog.destroyAll()
19 | }, options.duration)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/api/http/fetch.ts:
--------------------------------------------------------------------------------
1 | import { extend } from 'umi-request'
2 | import { errorHandler, requestInterceptor, responseInterceptor } from './interceptors/interceptors'
3 |
4 | const request = extend({
5 | prefix: import.meta.env.VITE_FETCH_PREFIX_URL as string,
6 | timeout: 6 * 1000 * 5,
7 | errorHandler
8 | })
9 |
10 | request.interceptors.request.use(requestInterceptor)
11 | request.interceptors.response.use(responseInterceptor)
12 |
13 | export default request
14 |
--------------------------------------------------------------------------------
/src/api/http/index.ts:
--------------------------------------------------------------------------------
1 | import { useApi } from './composables/useApi'
2 | import http from './fetch'
3 |
4 | export { useApi, http }
5 |
--------------------------------------------------------------------------------
/src/api/http/interceptors/interceptors.ts:
--------------------------------------------------------------------------------
1 | import type { RequestOptionsInit } from 'umi-request'
2 | import { createDialog } from '@/api/http/createDialog'
3 |
4 | export function requestInterceptor(url: string, options: RequestOptionsInit) {
5 | // todo
6 | const token = sessionStorage.getItem('token')
7 | if (token) {
8 | const header = new Headers()
9 | header.set('token', token)
10 | options.headers = {
11 | ...options.headers,
12 | ...header
13 | }
14 | }
15 | return {
16 | url,
17 | options
18 | }
19 | }
20 |
21 | // response拦截器, 处理response
22 | export async function responseInterceptor(response, options) {
23 | const data = await response.clone().json()
24 | const { code, msg } = data
25 | if (code === -1) {
26 | const { showErrorType = 'Message' } = options
27 | switch (showErrorType) {
28 | default:
29 | window.$message.error(msg)
30 | break
31 | case 'Dialog':
32 | createDialog({
33 | type: 'error',
34 | title: '提示',
35 | content: msg,
36 | duration: 3000
37 | })
38 | break
39 | case 'Notification':
40 | window.$notification.error({
41 | title: '提示',
42 | duration: 3000,
43 | content: msg
44 | })
45 | break
46 | }
47 | }
48 |
49 | if (options.showErrorType) {
50 | }
51 | return response
52 | }
53 |
54 | export function errorHandler(error) {
55 | const codeMap = {
56 | '500': '哦豁~服务器熄火啦',
57 | '404': '哦豁~啥都没有找到'
58 | }
59 | if (error.response) {
60 | // 请求已发送但服务端返回状态码非 2xx 的响应
61 | console.log(codeMap[error.data.status])
62 | } else {
63 | // 请求初始化时出错或者没有响应返回的异常
64 | window.$notification.error({
65 | title: 'Emmm',
66 | duration: 3000,
67 | content: '哦豁~网络好像有点小毛病哦'
68 | })
69 | }
70 | throw error
71 | }
72 |
--------------------------------------------------------------------------------
/src/api/http/typing.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pearadmin/pear-admin-naive/bba0ae576bd86e83d6581fb33b7cb9c16cce87ad/src/api/http/typing.ts
--------------------------------------------------------------------------------
/src/api/moduels/basicModel.ts:
--------------------------------------------------------------------------------
1 | export interface BasicModel {
2 | code: number
3 | msg: string
4 | }
5 |
--------------------------------------------------------------------------------
/src/api/moduels/demo/app.ts:
--------------------------------------------------------------------------------
1 | import { useApi } from '@/api/http'
2 | import type { Ref } from 'vue'
3 | import type { CaptureModel, CaptureParams, LoginForm, LoginResData } from './dataModel/appModel'
4 |
5 | export enum Api {
6 | GetCapture = '/user/getCapture',
7 | Login = '/user/login'
8 | }
9 |
10 | export const getCapture = (params: CaptureParams) => {
11 | return useApi({
12 | url: Api.GetCapture,
13 | method: 'get',
14 | params
15 | })
16 | }
17 |
18 | export const login = (data: Ref) => {
19 | return useApi(
20 | {
21 | url: Api.Login,
22 | method: 'post',
23 | data,
24 | showErrorType: 'Dialog'
25 | },
26 | { immediate: false }
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/api/moduels/demo/dataModel/appModel.ts:
--------------------------------------------------------------------------------
1 | export interface CaptureModel {
2 | code: string
3 | image: string
4 | }
5 |
6 | export interface CaptureParams {
7 | timestamp: number
8 | }
9 |
10 | export interface LoginForm {
11 | username: string
12 | password: string
13 | }
14 |
15 | export interface LoginResData {
16 | permissions: any[]
17 | routes: any[]
18 | token: string
19 | userInfo: {
20 | username: string
21 | password: string
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/api/moduels/fast-api/login/index.ts:
--------------------------------------------------------------------------------
1 | import { useApi } from '@/api/http'
2 | import type { Ref } from 'vue'
3 | import type { LoginModel, LoginResData } from '@/api/moduels/fast-api/login/model/model'
4 |
5 | enum Api {
6 | userLogin = 'user/login/'
7 | }
8 |
9 | export const useLogin = (data: Ref) => {
10 | return useApi(
11 | {
12 | url: Api.userLogin,
13 | data,
14 | method: 'post',
15 | showErrorType: 'Notification'
16 | },
17 | { immediate: false }
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/src/api/moduels/fast-api/login/model/model.ts:
--------------------------------------------------------------------------------
1 | import type { BasicModel } from '@/api/moduels/basicModel'
2 |
3 | export interface LoginModel {
4 | username: string
5 | password: string
6 | }
7 |
8 | export interface LoginResData extends BasicModel {
9 | token: string
10 | userinfo: {
11 | USERID: number | string
12 | USERNAME: string
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/api/moduels/fast-api/menu/index.ts:
--------------------------------------------------------------------------------
1 | import { useApi } from '@/api/http'
2 | import type { MenuModel } from '@/api/moduels/fast-api/menu/model/menuModel'
3 |
4 | export enum MenuApiEnum {
5 | menuRecords = 'menu/menusList'
6 | }
7 |
8 | export const getMenuRecords = (data: Recordable) => {
9 | return useApi(
10 | {
11 | url: MenuApiEnum.menuRecords,
12 | data,
13 | method: 'post'
14 | },
15 | { immediate: false }
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/api/moduels/fast-api/menu/model/menuModel.ts:
--------------------------------------------------------------------------------
1 | export interface MenuModel {
2 | [key: string]: string
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/gif/404.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pearadmin/pear-admin-naive/bba0ae576bd86e83d6581fb33b7cb9c16cce87ad/src/assets/gif/404.gif
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pearadmin/pear-admin-naive/bba0ae576bd86e83d6581fb33b7cb9c16cce87ad/src/assets/logo.png
--------------------------------------------------------------------------------
/src/assets/svg/background.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/AntVG2/G2Chart.vue:
--------------------------------------------------------------------------------
1 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/components/AntVG2/composables/useInnerChart.ts:
--------------------------------------------------------------------------------
1 | import type { ComputedRef, Ref, UnwrapRef } from 'vue'
2 | import { onMounted, onUnmounted, ref, watch } from 'vue'
3 | import { Chart } from '@antv/g2'
4 | import type { G2ChartProps } from '@/components/AntVG2/G2Chart.vue'
5 | import { isEmpty, merge } from 'lodash-es'
6 | import { useEventListener } from '@vueuse/core'
7 |
8 | export interface UseInnerChartReturn {
9 | chartRefEl: Ref>>
10 | chart: Ref>>
11 | }
12 |
13 | export function useInnerChart(props: ComputedRef): UseInnerChartReturn {
14 | // chart config
15 | const initialCfg = ref({})
16 |
17 | // chart render HtmlElement
18 | const chartRefEl = ref>(null)
19 |
20 | // chart instance
21 | const chart = ref>(null)
22 |
23 | useEventListener(
24 | 'resize',
25 | () => {
26 | if (chart.value) {
27 | chart.value.forceFit()
28 | chart.value.render()
29 | }
30 | },
31 | { passive: true }
32 | )
33 |
34 | /**
35 | * if chart initial config change reBuild chart
36 | */
37 | watch(
38 | () => props.value.initialChartConfig,
39 | (cfg) => {
40 | if (!isEmpty(cfg)) {
41 | if (!chartRefEl.value || !chart.value) {
42 | initialCfg.value = merge({}, cfg)
43 | } else {
44 | const width = (cfg?.width as number) ?? chart.value.width
45 | const height = (cfg?.height as number) ?? chart.value.width
46 | chart.value?.changeSize(width, height)
47 | }
48 | }
49 | },
50 | {
51 | immediate: true,
52 | deep: true
53 | }
54 | )
55 |
56 | onMounted(() => {
57 | chart.value = new Chart(
58 | merge({}, initialCfg.value, {
59 | container: chartRefEl.value as HTMLElement
60 | })
61 | )
62 | chart.value?.render()
63 | })
64 |
65 | onUnmounted(() => {
66 | chart.value?.destroy()
67 | })
68 |
69 | return {
70 | chartRefEl,
71 | chart
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/AntVG2/useChart.ts:
--------------------------------------------------------------------------------
1 | import type { Chart } from '@antv/g2'
2 | import type { G2ChartProps } from '@/components/AntVG2/G2Chart.vue'
3 | import { nextTick, onMounted, ref } from 'vue'
4 | import G2Chart from '@/components/AntVG2/G2Chart.vue'
5 |
6 | export function useChart(options: Partial) {
7 | const chartRefEl = ref>(null)
8 | const chartInstance = ref>(null)
9 |
10 | onMounted(async () => {
11 | chartRefEl.value?.updChartProps(options)
12 | await nextTick()
13 | chartInstance.value = chartRefEl.value?.chart as Chart
14 | })
15 |
16 | const methods = {
17 | getChart: async () => {
18 | await nextTick()
19 | return chartInstance.value
20 | },
21 | updChartProps: (props: Partial) => {
22 | chartRefEl.value?.updChartProps(props)
23 | }
24 | }
25 |
26 | return {
27 | chartRefEl,
28 | chartInstance,
29 | methods
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/AppProvider/AppProvider.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/components/AppProvider/OuterFeedback.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import { useNotification, useMessage, useDialog } from 'naive-ui'
3 |
4 | /**
5 | * message
6 | */
7 | export const OuterMessage = defineComponent({
8 | setup() {
9 | window.$message = useMessage()
10 | return () => null
11 | }
12 | })
13 |
14 | /**
15 | * notification
16 | */
17 | export const OuterNotification = defineComponent({
18 | setup() {
19 | window.$notification = useNotification()
20 | return () => null
21 | }
22 | })
23 |
24 | /**
25 | * dialog | modal
26 | */
27 | export const OuterDialog = defineComponent({
28 | setup() {
29 | const dialog = useDialog()
30 | window.$dialog = dialog
31 | return () => null
32 | }
33 | })
34 |
--------------------------------------------------------------------------------
/src/components/Application/Settings/ThemeTool.vue:
--------------------------------------------------------------------------------
1 |
7 |
53 |
54 |
55 |
62 | 主题: {{ currentThemeName }}
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/components/Application/Settings/type.d.ts:
--------------------------------------------------------------------------------
1 | import { ThemeName } from '@/store/modules/app'
2 | export interface ThemeOption {
3 | label: string
4 | value: ThemeName
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/Button/PButton.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/components/Error/Error403.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 | 返回首页
25 |
26 | {{ timeRef }} 秒后返回首页
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/components/Error/Error404.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 | 返回首页
25 |
26 | {{ timeRef }} 秒后返回首页
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/components/Error/Error500.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 | 返回首页
25 |
26 | {{ timeRef }} 秒后返回首页
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/components/Form/component.ts:
--------------------------------------------------------------------------------
1 | import { NInput, NSelect, NCheckbox, NRadio, NSwitch, NDatePicker } from 'naive-ui'
2 |
3 | export type ComponentMap = Map
4 |
5 | const map: ComponentMap = new Map()
6 |
7 | map.set('NInput', NInput)
8 | map.set('NSelect', NSelect)
9 | map.set('NCheckbox', NCheckbox)
10 | map.set('NRadio', NRadio)
11 | map.set('NSwitch', NSwitch)
12 | map.set('NDatePicker', NDatePicker)
13 |
14 | export { map as componentMap }
15 |
--------------------------------------------------------------------------------
/src/components/Form/components/PearForm.vue:
--------------------------------------------------------------------------------
1 |
143 |
144 |
145 |
146 |
147 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
--------------------------------------------------------------------------------
/src/components/Form/components/PearFormItem.tsx:
--------------------------------------------------------------------------------
1 | import { computed, defineComponent } from 'vue'
2 | import type { DefineComponent, PropType } from 'vue'
3 | import { componentMap } from '@/components/Form/component'
4 | import type { FormSchema } from '@/components/Form/components/PearForm.vue'
5 | import { isFunction } from 'lodash-es'
6 |
7 | export default defineComponent({
8 | name: 'PearFormItem',
9 | props: {
10 | schema: {
11 | type: Object as PropType,
12 | required: false,
13 | default: () => ({})
14 | },
15 | formModelRef: {
16 | type: Object as PropType,
17 | default: () => ({})
18 | }
19 | },
20 | setup(props, { attrs }) {
21 | const Component = computed((): DefineComponent => {
22 | const name = props.schema?.component ? props.schema.component : 'NInput'
23 | return componentMap.get(name) as DefineComponent
24 | })
25 |
26 | const comProps = computed((): Recordable => {
27 | // 支持动态props
28 | const keys = Object.keys(props.schema?.componentProps ?? {})
29 | /**
30 | * componentProps: {
31 | * aaa: 'aaa',
32 | * xxx: (formModelRef) => {
33 | * return xxx
34 | * },
35 | * onXXX: () => function onXXX(){}
36 | * }
37 | * 排除以on开头的,一般来说,以on开头的多为函数,所以不做处理
38 | */
39 | const innerProps = keys.reduce((resProps, key) => {
40 | const itemProp = props.schema?.componentProps ?? null
41 | if (itemProp) {
42 | return {
43 | ...resProps,
44 | [key]:
45 | isFunction(itemProp[key]) && !key.startsWith('on')
46 | ? itemProp[key](props.formModelRef)
47 | : itemProp[key]
48 | }
49 | }
50 | return resProps
51 | }, {} as Recordable)
52 | return innerProps
53 | })
54 |
55 | const comSlots = computed(() => {
56 | return props.schema?.componentSlots ?? {}
57 | })
58 |
59 | return () => {
60 | const DynamicComponent = Component.value
61 | return (
62 |
67 | )
68 | }
69 | }
70 | })
71 |
--------------------------------------------------------------------------------
/src/components/Form/composables/usePearForm.ts:
--------------------------------------------------------------------------------
1 | import type { PearFormProps, PearFormExpose } from '@/components/Form/components/PearForm.vue'
2 | import { isRef, nextTick, onUnmounted, ref, unref, watchEffect } from 'vue'
3 | import type { Ref } from 'vue'
4 | import type { MaybeRef } from '@vueuse/core'
5 | import { makeDestructurable } from '@vueuse/core'
6 |
7 | export interface UseFormMethods {
8 | values: Ref
9 | getFormValue: () => Recordable
10 | restoreValidation: () => Promise
11 | updFormProps: (formProps?: Partial) => Promise
12 | validate: (args?: any) => Promise
13 | reset: () => void
14 | }
15 |
16 | export function usePearForm(pearFormProps?: MaybeRef>) {
17 | const formExpose = ref>(null)
18 |
19 | function registerForm(expose: PearFormExpose) {
20 | formExpose.value = expose
21 | if (pearFormProps) {
22 | expose.updFormProps(unref(pearFormProps))
23 | }
24 | }
25 |
26 | const values = ref({})
27 |
28 | watchEffect(() => {
29 | values.value = (unref(formExpose)?.getFormValue() as Recordable) ?? {}
30 | if (isRef(pearFormProps)) {
31 | formExpose.value?.updFormProps(unref(pearFormProps))
32 | }
33 | })
34 |
35 | onUnmounted(() => {
36 | formExpose.value = null
37 | })
38 |
39 | const methods: UseFormMethods = {
40 | values,
41 | getFormValue: () => {
42 | return values.value
43 | },
44 | restoreValidation: async () => {
45 | await nextTick()
46 | unref(formExpose)?.restoreValidation()
47 | },
48 | updFormProps: async (formProps?: Partial) => {
49 | await nextTick()
50 | unref(formExpose)?.updFormProps(formProps)
51 | },
52 | validate: async (args?: any) => {
53 | await nextTick()
54 | return unref(formExpose)?.validate(args)
55 | },
56 | reset: () => {
57 | unref(formExpose)?.restoreValidation()
58 | }
59 | }
60 |
61 | return makeDestructurable(
62 | {
63 | registerForm,
64 | methods
65 | } as const,
66 | [registerForm, methods]
67 | )
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/Form/composables/usePearFormModel.ts:
--------------------------------------------------------------------------------
1 | import type { PearFormProps, FormSchema } from '@/components/Form/components/PearForm.vue'
2 | import type { ComputedRef, Ref } from 'vue'
3 | import { ref, watchEffect } from 'vue'
4 | import { get } from '@vueuse/core'
5 |
6 | export interface FormModelMethods {
7 | restFormValue: () => void
8 | }
9 |
10 | export interface UsePearFormModelReturn {
11 | formModelRef: Ref
12 | methods: FormModelMethods
13 | }
14 |
15 | export function usePearFormModel(
16 | props: ComputedRef>
17 | ): UsePearFormModelReturn {
18 | const formModelRef = ref({})
19 |
20 | function handleInitModel(model: Recordable | undefined, formSchemas: FormSchema[] | undefined) {
21 | let formModel: Recordable = {}
22 | if (formSchemas && formSchemas.length > 0) {
23 | const schemaModel = formSchemas.reduce((modelObject, schema) => {
24 | return {
25 | ...modelObject,
26 | [schema.model]: null
27 | }
28 | }, {} as Recordable)
29 | if (model) {
30 | formModel = Object.assign(schemaModel, model)
31 | }
32 | formModelRef.value = formModel
33 | }
34 | }
35 |
36 | function restFormValue() {
37 | handleInitModel(props.value.model, props.value.schemas)
38 | }
39 |
40 | watchEffect(() => {
41 | const model = get(props, 'model')
42 | const schemas = get(props, 'schemas')
43 | if (model || schemas) {
44 | handleInitModel(model, schemas)
45 | }
46 | })
47 |
48 | return {
49 | formModelRef,
50 | methods: {
51 | restFormValue
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/Icon/Icon.vue:
--------------------------------------------------------------------------------
1 |
65 |
66 |
67 |
68 |
69 |
70 |
105 |
--------------------------------------------------------------------------------
/src/components/Modal/PModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/PageWrapper/Breadcrumb/Breadcrumb.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 | {{ m.meta.title }}
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/components/PageWrapper/Breadcrumb/useBreadcrumb.ts:
--------------------------------------------------------------------------------
1 | import { useRoute } from 'vue-router'
2 | import type { RouteLocationMatched } from 'vue-router'
3 | import { ref, watch } from 'vue'
4 |
5 | export default function useBreadcrumb() {
6 | const route = useRoute()
7 |
8 | const matches = ref([])
9 |
10 | watch(
11 | () => route.path,
12 | () => {
13 | matches.value = [...route.matched]
14 | },
15 | { immediate: true }
16 | )
17 |
18 | return {
19 | matches
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/PageWrapper/PageWrapper.vue:
--------------------------------------------------------------------------------
1 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | {{ route.meta.title }}
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
115 |
--------------------------------------------------------------------------------
/src/components/Table/components/ColumnSetting.vue:
--------------------------------------------------------------------------------
1 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | 列设置
92 |
93 |
94 |
95 |
96 |
101 | 列展示
102 |
103 | 重置
104 |
105 |
106 |
107 |
108 |
109 |
114 | {{ col?.title }}
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
141 |
--------------------------------------------------------------------------------
/src/components/Table/components/Reload.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 刷新
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/components/Table/components/ResizeHeight.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | 表格高度(当一页展示50条或更多条记录时,尝试增加高度,展示更多的数据.)
30 |
31 |
32 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/components/Table/components/SizeSetting.vue:
--------------------------------------------------------------------------------
1 |
35 |
36 |
37 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 密度
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/components/Table/components/TableTools.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
17 |
29 |
--------------------------------------------------------------------------------
/src/components/Table/composables/useColumns.ts:
--------------------------------------------------------------------------------
1 | import { computed, ref, watch } from 'vue'
2 | import type { TableBaseColumn } from 'naive-ui/es/data-table/src/interface'
3 |
4 | export interface PTableColumns extends TableBaseColumn {
5 | visible: boolean
6 | }
7 |
8 | export const NOT_RENDER_KEYS = ['expand', 'selection']
9 |
10 | export function useColumns(basicTableAttrs: Recordable) {
11 | const computedCol = computed(() => {
12 | return basicTableAttrs?.columns ? basicTableAttrs.columns : []
13 | })
14 |
15 | const columns = ref([])
16 | const caches = ref([])
17 |
18 | function updColumns(upd: PTableColumns[]) {
19 | columns.value = upd
20 | }
21 |
22 | watch(
23 | columns,
24 | (cols) => {
25 | caches.value = cols.filter((it) => {
26 | return it.visible || (it.type && NOT_RENDER_KEYS.includes(it.type))
27 | })
28 | },
29 | { deep: true }
30 | )
31 |
32 | watch(
33 | computedCol,
34 | (cols) => {
35 | columns.value = cols.map((col) => ({ ...col, visible: true }))
36 | },
37 | { immediate: true, deep: true }
38 | )
39 |
40 | return {
41 | columns: caches,
42 | updColumns
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/Table/composables/usePagination.ts:
--------------------------------------------------------------------------------
1 | import type { PaginationProps } from 'naive-ui'
2 | import type { Ref } from 'vue'
3 | import { ref } from 'vue'
4 |
5 | export interface UsePagination {
6 | paginationRef: Ref
7 | resetPagination: () => void
8 | }
9 |
10 | export default function usePagination(): UsePagination {
11 | const paginationRef = ref>({
12 | itemCount: 0,
13 | // pageCount: 100,
14 | page: 1,
15 | pageSize: 10,
16 | pageSlot: 9,
17 | showQuickJumper: true,
18 | showSizePicker: true,
19 | pageSizes: [
20 | {
21 | label: '10/页',
22 | value: 10
23 | },
24 | {
25 | label: '20/页',
26 | value: 20
27 | },
28 | {
29 | label: '30/页',
30 | value: 30
31 | },
32 | {
33 | label: '5000/页',
34 | value: 5000
35 | },
36 | {
37 | label: '50000/页',
38 | value: 50000
39 | }
40 | ],
41 | ['onUpdate:pageSize']: (pageSize: number) => {
42 | paginationRef.value.page = 1
43 | paginationRef.value.pageSize = pageSize
44 | },
45 | ['onUpdate:page']: (pageNo: number) => {
46 | paginationRef.value.page = pageNo
47 | },
48 | prefix: (pagination: PaginationProps) => {
49 | return `共${pagination.itemCount ?? 0}条数据`
50 | }
51 | // suffix: (pagination: PaginationProps) => {
52 | // return `${pagination.page} / ${pagination.pageCount}`
53 | // }
54 | })
55 |
56 | function resetPagination() {
57 | paginationRef.value.page = 1
58 | paginationRef.value.pageSize = 10
59 | }
60 |
61 | return {
62 | paginationRef,
63 | resetPagination
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/Table/composables/usePearTable.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | PearTableExpose,
3 | PearTableProps,
4 | TableFetch
5 | } from '@/components/Table/components/PearTable.vue'
6 | import { onUnmounted, ref } from 'vue'
7 | import type { MaybeRef } from '@vueuse/core'
8 | import { get, makeDestructurable } from '@vueuse/core'
9 |
10 | export type TableProps = PearTableProps & {
11 | fetch?: MaybeRef
12 | }
13 |
14 | export function usePearTable(options: MaybeRef>) {
15 | const tableExpose = ref>()
16 |
17 | function registerTable(expose?: PearTableExpose) {
18 | if (expose) {
19 | tableExpose.value = expose
20 | expose.updTableProps(get(options) as PearTableProps)
21 | }
22 | }
23 |
24 | onUnmounted(() => {
25 | tableExpose.value = null
26 | })
27 |
28 | const methods = {
29 | getFormValue: () => {
30 | return tableExpose.value?.searchFormValue
31 | }
32 | }
33 |
34 | return makeDestructurable({ registerTable, methods } as const, [registerTable, methods] as const)
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/Table/composables/useSearchFormExpand.ts:
--------------------------------------------------------------------------------
1 | import { ref, watch } from 'vue'
2 | import type { ComputedRef } from 'vue'
3 | import type { PearTableProps } from '@/components/Table/components/PearTable.vue'
4 |
5 | export function useSearchFormExpand(props: ComputedRef) {
6 | const gridCollapsed = ref(false)
7 |
8 | watch(
9 | () => props.value.searchFormProps?.gridProps?.collapsed,
10 | (val) => {
11 | gridCollapsed.value = !!val
12 | },
13 | { immediate: true }
14 | )
15 |
16 | function handleToggleFormExpand() {
17 | gridCollapsed.value = !gridCollapsed.value
18 | }
19 |
20 | return {
21 | gridCollapsed,
22 | handleToggleFormExpand
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/Table/composables/useTableBaseConfig.ts:
--------------------------------------------------------------------------------
1 | import { ref, watchEffect } from 'vue'
2 | import type { ComputedRef } from 'vue'
3 | import { DEFAULT_TABLE_HEIGHT, DEFAULT_TABLE_SIZE } from '@/config'
4 |
5 | import type { TableBaseColumn } from 'naive-ui/es/data-table/src/interface'
6 | import type { PearTableProps } from '@/components/Table/components/PearTable.vue'
7 | import { get } from '@vueuse/core'
8 |
9 | export interface PTableColumns extends TableBaseColumn {
10 | visible: boolean
11 | }
12 |
13 | export const NOT_RENDER_KEYS = ['expand', 'selection']
14 |
15 | export type TableSize = 'small' | 'medium' | 'large'
16 |
17 | export function useTableBaseConfig(props: ComputedRef>) {
18 | // table size
19 | const sizeRef = ref(DEFAULT_TABLE_SIZE)
20 |
21 | // table height
22 | const heightRef = ref(DEFAULT_TABLE_HEIGHT)
23 |
24 | // icon size
25 | const iconSizeRef = ref(18)
26 |
27 | // columns
28 | const columns = ref([])
29 |
30 | watchEffect(() => {
31 | if (get(props)?.size) {
32 | sizeRef.value = get(get(props)?.size) as TableSize
33 | }
34 | if (get(props)?.columns) {
35 | columns.value = get(props)?.columns?.map((col) => ({
36 | ...col,
37 | visible: true
38 | })) as PTableColumns[]
39 | }
40 | })
41 |
42 | return {
43 | tableSize: sizeRef,
44 | tableHeight: heightRef,
45 | iconSize: iconSizeRef,
46 | columns
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/Table/composables/useTableContext.ts:
--------------------------------------------------------------------------------
1 | import type { MaybeRef } from '@vueuse/core'
2 | import type { DataTableColumns } from 'naive-ui'
3 | import type { InjectionKey, Ref } from 'vue'
4 | import { createContext, useContext } from '@/composables/useContext'
5 | import type { UpdateProvideState } from '@/composables/useContext'
6 | import type { RequestOptionsInit } from 'umi-request'
7 |
8 | export type TableSize = 'small' | 'medium' | 'large'
9 |
10 | export interface TableContext {
11 | tableSize: MaybeRef
12 | tableHeight: MaybeRef
13 | iconSize: MaybeRef
14 | columns: MaybeRef
15 | fetchRunner: MaybeRef<(args?: RequestOptionsInit) => Promise>
16 | }
17 |
18 | const tableStateKey: InjectionKey = Symbol()
19 | const updTableStateKey: InjectionKey> = Symbol()
20 |
21 | export function createTableContext(payload: TableContext) {
22 | return createContext(tableStateKey, payload, updTableStateKey)
23 | }
24 |
25 | export function useTableContext() {
26 | const tableProvideState = useContext[>(tableStateKey)
27 | const updTableProvideState = useContext>(updTableStateKey)
28 |
29 | return {
30 | tableProvideState,
31 | updTableProvideState
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/Table/composables/useTableRequest.ts:
--------------------------------------------------------------------------------
1 | import { computed, nextTick, onMounted, ref, watch, watchEffect } from 'vue'
2 | import type { ComputedRef, Ref } from 'vue'
3 | import type { PaginationProps } from 'naive-ui'
4 | import type { Recordable } from 'vite-plugin-mock'
5 | import { DEFAULT_TABLE_FETCH, TABLE_FETCH_RESPONSE, TABLE_PAGINATION } from '@/config'
6 | import { get, set, useDebounceFn } from '@vueuse/core'
7 | import type { PearTableProps } from '@/components/Table/components/PearTable.vue'
8 | import { useApi } from '@/api/http'
9 | import type { FetchMethod } from '@/api/http/composables/useApi'
10 | import { isEqual, isFunction, merge } from 'lodash-es'
11 |
12 | export interface UseTableRequestOptions {
13 | pagination: Ref>
14 | fetchParams: Ref
15 | props: ComputedRef
16 | }
17 | export function useTableRequest(options: UseTableRequestOptions) {
18 | // 请求结果
19 | const result = ref([])
20 | // url
21 | const fetchUrl = computed((): string => {
22 | return (get(options.props).fetch?.fetchUrl as string) ?? ''
23 | })
24 | // fetch options
25 | const fetchOptions = ref({})
26 |
27 | // basic request options
28 | const basicParams = computed((): Recordable => {
29 | return {
30 | [TABLE_PAGINATION.pageNo]: get(options.pagination, 'page'),
31 | [TABLE_PAGINATION.pageSize]: get(options.pagination, 'pageSize'),
32 | ...get(options.fetchParams)
33 | }
34 | })
35 |
36 | watchEffect(() => {
37 | const before = get(options.props).fetch?.beforeFetch ?? null
38 | let userParams = {}
39 | if (before && isFunction(before)) {
40 | userParams = before.call(null, basicParams)
41 | }
42 | fetchOptions.value = merge({}, get(basicParams), get(userParams))
43 | })
44 |
45 | const { loading, executor, finished, data } = useApi(
46 | {
47 | url: fetchUrl,
48 | method: DEFAULT_TABLE_FETCH.method as FetchMethod,
49 | [DEFAULT_TABLE_FETCH.bodyType]: fetchOptions
50 | },
51 | {
52 | immediate: false,
53 | redo: false,
54 | debounce: get(options.props).fetch?.debounce ?? 0
55 | }
56 | )
57 |
58 | onMounted(async () => {
59 | await nextTick()
60 | const immediate = get(options.props).fetch?.immediate ?? true
61 | if (immediate) {
62 | await get(executor)()
63 | }
64 | })
65 |
66 | watch(
67 | [() => options.pagination.value.page, () => options.pagination.value.pageSize],
68 | ([cPage, cSize], [oPage, oSize]) => {
69 | if (cPage !== oPage || cSize !== oSize) {
70 | get(executor)()
71 | }
72 | }
73 | )
74 |
75 | watchEffect(() => {
76 | if (data.value) {
77 | result.value = []
78 | let cacheData = get(data)?.[TABLE_FETCH_RESPONSE.list] ?? []
79 | const after = get(options.props).fetch?.afterFetch ?? null
80 | if (after && isFunction(after)) {
81 | cacheData = after.call(null, cacheData) ?? []
82 | }
83 | result.value = cacheData
84 | // pagination
85 | set(options.pagination.value, 'itemCount', get(data)?.[TABLE_FETCH_RESPONSE.total])
86 | }
87 | })
88 |
89 | const debouncedFn = computed((): Nullable<() => Promise> => {
90 | const debounce = get(options.props)?.fetch?.debounce ?? 0
91 | if (debounce > 0) {
92 | return useDebounceFn(async () => {
93 | await get(executor)()
94 | }, debounce)
95 | }
96 | return null
97 | })
98 |
99 | // redo
100 | watch(
101 | basicParams,
102 | async (nV, oV) => {
103 | if (!isEqual(nV, oV) && options.props.value?.fetch?.redo) {
104 | const debounce = get(options.props)?.fetch?.debounce ?? 0
105 | if (debounce > 0) {
106 | await debouncedFn.value?.()
107 | } else {
108 | await get(executor)()
109 | }
110 | }
111 | },
112 | { deep: true }
113 | )
114 |
115 | return {
116 | loading,
117 | data: result,
118 | executor,
119 | finished
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/composables/useBreakPoint.ts:
--------------------------------------------------------------------------------
1 | import { watch, reactive } from 'vue'
2 | import { useEventListener } from '@vueuse/core'
3 | import { BREAK_POINT_NAME, BREAK_POINT_SIZE } from '@/enums/breakPointEnum'
4 |
5 | export interface Device {
6 | width: number
7 | screen: string
8 | }
9 |
10 | export function useBreakPoint() {
11 | const device: Device = reactive({
12 | width: getWindowInnerWith(),
13 | screen: BREAK_POINT_NAME.XXL
14 | })
15 |
16 | function getWindowInnerWith(): number {
17 | return document.body.clientWidth
18 | }
19 |
20 | useEventListener(
21 | 'resize',
22 | () => {
23 | device.width = getWindowInnerWith()
24 | },
25 | { passive: true }
26 | )
27 |
28 | watch(
29 | () => device.width,
30 | (w) => {
31 | const enumKeys = Object.keys(BREAK_POINT_SIZE)
32 | if (BREAK_POINT_SIZE.xxl < w) {
33 | device.screen = BREAK_POINT_NAME.XXL
34 | } else {
35 | const current = enumKeys.find((key) => w < BREAK_POINT_SIZE[key])
36 | device.screen = current as string
37 | }
38 | },
39 | { immediate: true }
40 | )
41 |
42 | return {
43 | device
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/composables/useContext.ts:
--------------------------------------------------------------------------------
1 | import { inject, provide, ref } from 'vue'
2 | import type { InjectionKey, Ref, UnwrapRef } from 'vue'
3 | import type { MaybeRef } from '@vueuse/core'
4 | import { get } from '@vueuse/core'
5 | import { merge } from 'lodash-es'
6 |
7 | export type UpdateProvideState = (payload: Partial>) => void
8 |
9 | export function createContext(
10 | injectKey: InjectionKey>,
11 | payload: MaybeRef,
12 | updStateInjectKey?: InjectionKey>
13 | ) {
14 | const innerState = ref({ ...get(payload) })
15 |
16 | provide][>>(injectKey, innerState)
17 |
18 | function updProvideState(payload: Partial>): void {
19 | merge(innerState.value, payload)
20 | }
21 |
22 | if (updStateInjectKey) {
23 | provide>(updStateInjectKey, updProvideState)
24 | }
25 |
26 | return {
27 | innerState,
28 | updProvideState
29 | }
30 | }
31 |
32 | export function useContext(key: InjectionKey): T {
33 | return inject(key) as T
34 | }
35 |
--------------------------------------------------------------------------------
/src/composables/usePromiseFn.ts:
--------------------------------------------------------------------------------
1 | import { computed, ref, watch } from 'vue'
2 | import type { Ref, UnwrapRef } from 'vue'
3 | import { get } from '@vueuse/core'
4 | import type { MaybeRef } from '@vueuse/core'
5 |
6 | export type FetchData = Recordable
7 |
8 | export interface UsePromiseConfig {
9 | immediate?: boolean
10 | redo?: boolean
11 | }
12 |
13 | export interface UsePromiseFnReturn {
14 | loading: Ref
15 | data: Ref>>
16 | finished: Ref
17 | error: Ref
18 | executor: () => void
19 | }
20 |
21 | export default function usePromiseFn(
22 | fn: (...args: any) => Promise>,
23 | fetchData?: Nullable>,
24 | config?: UsePromiseConfig
25 | ): UsePromiseFnReturn {
26 | const loading = ref(false)
27 | const data = ref>(null)
28 | const finished = ref(false)
29 | const error = ref(null)
30 |
31 | const fetchParams = computed((): Recordable | undefined => {
32 | if (fetchData) {
33 | return get(fetchData)
34 | }
35 | return undefined
36 | })
37 |
38 | const { immediate, redo } = config ? config : { immediate: true, redo: true }
39 |
40 | function runPromise() {
41 | if (loading.value) {
42 | return
43 | }
44 | loading.value = true
45 | finished.value = false
46 |
47 | const promiseFn = fetchParams.value ? fn(fetchParams.value) : fn()
48 | promiseFn
49 | .then((response: UnwrapRef) => {
50 | data.value = response
51 | error.value = null
52 | })
53 | .catch((err) => {
54 | error.value = err
55 | data.value = null
56 | })
57 | .finally(() => {
58 | loading.value = false
59 | finished.value = true
60 | })
61 | }
62 |
63 | watch(
64 | fetchParams,
65 | () => {
66 | if (redo) {
67 | if (!loading.value) {
68 | runPromise()
69 | }
70 | return
71 | }
72 | },
73 | { deep: true }
74 | )
75 |
76 | if (immediate) {
77 | if (!loading.value) {
78 | runPromise()
79 | }
80 | }
81 |
82 | return {
83 | loading,
84 | data,
85 | finished,
86 | error,
87 | executor: runPromise
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/composables/useRouterViewRefresh.ts:
--------------------------------------------------------------------------------
1 | import { onUnmounted, ref, watch } from 'vue'
2 | import { useTimeoutFn } from '@vueuse/core'
3 |
4 | export function useRouterViewRefresh() {
5 | const showView = ref(true)
6 |
7 | const { start, stop } = useTimeoutFn(
8 | () => {
9 | showView.value = true
10 | },
11 | 16,
12 | { immediate: false }
13 | )
14 |
15 | watch(showView, (val) => {
16 | if (!val) {
17 | start()
18 | }
19 | })
20 |
21 | function refreshRouterView() {
22 | showView.value = false
23 | }
24 |
25 | onUnmounted(() => {
26 | stop()
27 | })
28 |
29 | return {
30 | showView,
31 | refreshRouterView
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/composables/useUiConfig/useUiConfig.ts:
--------------------------------------------------------------------------------
1 | import type { ThemeName } from '@/store/modules/app'
2 | import { useAppStore } from '@/store/modules/app'
3 | import { computed, ref } from 'vue'
4 | import type { ComputedRef, Ref } from 'vue'
5 | import { zhCN, dateZhCN, useOsTheme, darkTheme } from 'naive-ui'
6 | import type { NLocale, GlobalThemeOverrides } from 'naive-ui'
7 | import { naiveUIConfig } from '@/config/theme.config'
8 | import type { NDateLocale } from 'naive-ui/lib/locales/date/enUS'
9 | import type { BuiltInGlobalTheme } from 'naive-ui/es/themes/interface'
10 |
11 | interface ProviderAttrs {
12 | dateLocale: Ref
13 | locale: Ref
14 | theme: ComputedRef>
15 | themeOverrides: GlobalThemeOverrides
16 | }
17 |
18 | export function useUiConfig() {
19 | const appStore = useAppStore()
20 | const osThemeRef = useOsTheme()
21 |
22 | const providerAttrs = ref({
23 | dateLocale: ref(dateZhCN),
24 | locale: ref(zhCN),
25 | theme: computed(() => {
26 | if (appStore.theme === 'auto') {
27 | return osThemeRef.value === 'dark' ? darkTheme : null
28 | }
29 | return appStore.theme === 'dark' ? darkTheme : null
30 | }),
31 | themeOverrides: naiveUIConfig
32 | })
33 |
34 | function toggleTheme(themeName: ThemeName) {
35 | return appStore.toggleTheme(themeName)
36 | }
37 |
38 | return {
39 | providerAttrs,
40 | toggleTheme
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_THEME = 'auto'
2 |
3 | export const DEFAULT_STORAGE = sessionStorage
4 |
5 | export const DEFAULT_TABLE_SIZE = 'medium'
6 |
7 | export const DEFAULT_TABLE_HEIGHT = 600
8 |
9 | export const DEFAULT_TABLE_FETCH = {
10 | method: 'post',
11 | bodyType: 'data'
12 | }
13 |
14 | export const TABLE_PAGINATION = {
15 | pageSize: 'pageSize',
16 | pageNo: 'pageNo',
17 | total: 'total'
18 | }
19 |
20 | export const TABLE_FETCH_RESPONSE = {
21 | pageSize: 'pageSize',
22 | pageNo: 'pageNo',
23 | total: 'total',
24 | list: 'list'
25 | }
26 |
--------------------------------------------------------------------------------
/src/config/menu.config.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pearadmin/pear-admin-naive/bba0ae576bd86e83d6581fb33b7cb9c16cce87ad/src/config/menu.config.ts
--------------------------------------------------------------------------------
/src/config/theme.config.ts:
--------------------------------------------------------------------------------
1 | import type { GlobalThemeOverrides } from 'naive-ui'
2 |
3 | export const naiveUIConfig: GlobalThemeOverrides = {
4 | common: {
5 | // primaryColor: '#36b368FF',
6 | // primaryColorHover: '#36b368FF',
7 | // primaryColorSuppl: '#36b368FF'
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/enums/breakPointEnum.ts:
--------------------------------------------------------------------------------
1 | export enum BREAK_POINT_SIZE {
2 | sm = 640,
3 | md = 768,
4 | lg = 1024,
5 | xl = 1280,
6 | xxl = 1536
7 | }
8 |
9 | export enum BREAK_POINT_NAME {
10 | SM = 'sm',
11 | MD = 'md',
12 | LG = 'lg',
13 | XL = 'xl',
14 | XXL = 'xxl'
15 | }
16 |
--------------------------------------------------------------------------------
/src/layouts/BasicLayout.vue:
--------------------------------------------------------------------------------
1 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/layouts/ParentLayout.tsx:
--------------------------------------------------------------------------------
1 | // parent route component
2 | import { defineComponent } from 'vue'
3 |
4 | export const getParentComponent = (name: string) => {
5 | return defineComponent({
6 | name,
7 | setup() {
8 | return () => {
9 | return
10 | }
11 | }
12 | })
13 | }
14 |
--------------------------------------------------------------------------------
/src/layouts/content/PearContent.vue:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
67 |
--------------------------------------------------------------------------------
/src/layouts/content/RouteTabs.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 | ]
28 |
39 |
40 |
41 | {{ tag?.title }}
42 |
43 |
44 |
45 |
46 |
56 |
57 |
58 |
59 |
105 |
--------------------------------------------------------------------------------
/src/layouts/content/TabRefresh.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | 刷新
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/layouts/content/TabsAction.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/layouts/content/useRouteTab.ts:
--------------------------------------------------------------------------------
1 | import { useRoute, useRouter } from 'vue-router'
2 | import { computed, watch } from 'vue'
3 | import type { ComputedRef } from 'vue'
4 | import type { RouteTag } from '@/store/modules/app'
5 | import { useAppStore } from '@/store/modules/app'
6 | import { ErrorPageNames } from '@/router/modules/errors'
7 |
8 | export interface ReturnUseRouteTab {
9 | tags: ComputedRef
10 | handleCloseTag: (tag: RouteTag) => void
11 | handleClickTag: (tag: RouteTag) => void
12 | handleCloseLeft: () => void
13 | handleCloseRight: () => void
14 | handleCloseOther: () => void
15 | }
16 |
17 | export default function useRouteTab(): ReturnUseRouteTab {
18 | const route = useRoute()
19 | const router = useRouter()
20 | const appStore = useAppStore()
21 |
22 | watch(
23 | () => route.path,
24 | () => {
25 | const { name } = route
26 | if (!ErrorPageNames.includes(name as string)) {
27 | appStore.addTag({
28 | path: route.path,
29 | fullPath: route.fullPath,
30 | name: route.name as string,
31 | title: route.meta.title
32 | })
33 | }
34 | },
35 | { immediate: true }
36 | )
37 |
38 | const tags = computed(() => appStore.tags)
39 | // const tags = ref([])
40 |
41 | // watch(
42 | // appStore.tags,
43 | // (val) => {
44 | // tags.value = val
45 | // },
46 | // { immediate: true }
47 | // )
48 |
49 | function handleClickTag(tag) {
50 | router.replace(tag.fullPath).catch((err) => console.error(err))
51 | }
52 |
53 | function handleCloseTag(tag) {
54 | // 最后一个不删除
55 | if (tags.value.length === 1) {
56 | return
57 | }
58 | // 删除操作
59 | appStore.removeTag(tag)
60 | // 删除的跟当前页面是同一个, 路由回退上一个
61 | if (tag.name === route.name) {
62 | const last = tags.value[tags.value.length - 1]
63 | router.replace(last.fullPath).catch((err) => console.error(err))
64 | }
65 | }
66 |
67 | function handleCloseLeft() {
68 | const currentName = route.name as string
69 | appStore.closeLeftTag(currentName)
70 | }
71 |
72 | function handleCloseRight() {
73 | const currentName = route.name as string
74 | appStore.closeRightTag(currentName)
75 | }
76 |
77 | function handleCloseOther() {
78 | const currentName = route.name as string
79 | appStore.closeOtherTag(currentName)
80 | }
81 |
82 | return {
83 | tags,
84 | handleCloseTag,
85 | handleClickTag,
86 | handleCloseLeft,
87 | handleCloseRight,
88 | handleCloseOther
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/layouts/createLayoutContextData.ts:
--------------------------------------------------------------------------------
1 | import type { MaybeRef } from '@vueuse/core'
2 | import { createContext, useContext } from '@/composables/useContext'
3 | import type { UpdateProvideState } from '@/composables/useContext'
4 | import type { InjectionKey, Ref } from 'vue'
5 |
6 | export interface AppTheme {
7 | inverted: boolean
8 | }
9 |
10 | export interface LayoutContextData {
11 | collapsed: MaybeRef
12 | isMobile: MaybeRef
13 | showView: MaybeRef
14 | refreshRouterView: () => void
15 | theme: AppTheme
16 | }
17 |
18 | const stateKey: InjectionKey = Symbol()
19 | const updateStateKey: InjectionKey> = Symbol()
20 |
21 | export function createLayoutContextData(payload: MaybeRef) {
22 | return createContext(stateKey, payload, updateStateKey)
23 | }
24 |
25 | export function useLayoutContextData(): {
26 | provideState: Ref
27 | } {
28 | const provideState = useContext[>(stateKey)
29 | return {
30 | provideState
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/layouts/footer/Footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
20 |
--------------------------------------------------------------------------------
/src/layouts/header/AppSetting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
19 |
--------------------------------------------------------------------------------
/src/layouts/header/FullScreen.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 全屏
20 |
21 |
22 |
23 |
33 |
--------------------------------------------------------------------------------
/src/layouts/header/PearHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
25 |
--------------------------------------------------------------------------------
/src/layouts/header/UserDropdown.vue:
--------------------------------------------------------------------------------
1 |
65 |
66 |
67 |
68 |
69 |
75 | 落梅听风雪
76 |
77 |
78 |
79 |
80 |
93 |
--------------------------------------------------------------------------------
/src/layouts/index.ts:
--------------------------------------------------------------------------------
1 | import BasicLayout from './BasicLayout.vue'
2 | import { getParentComponent } from './ParentLayout'
3 |
4 | export { BasicLayout, getParentComponent }
5 |
6 | export default BasicLayout
7 |
--------------------------------------------------------------------------------
/src/layouts/menu/PearMenu.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
27 |
28 |
29 |
30 |
35 |
--------------------------------------------------------------------------------
/src/layouts/menu/useMenu.ts:
--------------------------------------------------------------------------------
1 | import { getMenuOptions } from '@/router/util'
2 | import { computed, ref, watch } from 'vue'
3 | import type { Ref } from 'vue'
4 | import type { MenuOption } from 'naive-ui'
5 | import { useRoute } from 'vue-router'
6 | import { useUserStore } from '@/store/modules/userInfo'
7 |
8 | export interface ReturnUseMenu {
9 | menuRef: Ref
10 | expandKeys: Ref
11 | updateExpandKeys: (keys: string[]) => void
12 | currentMenu: Ref
13 | updateValue: (key: string) => void
14 | }
15 |
16 | export function useMenu(): ReturnUseMenu {
17 | const userStore = useUserStore()
18 | const menuRef = ref([])
19 |
20 | const routes = computed(() => {
21 | return userStore.menuRoutes
22 | })
23 |
24 | const menus = getMenuOptions(routes.value)
25 |
26 | // @ts-ignore
27 | menuRef.value = menus
28 |
29 | const route = useRoute()
30 |
31 | const expandKeys = ref([])
32 | const currentMenu = ref('')
33 |
34 | // 初始化加载
35 | watch(
36 | () => route.path,
37 | () => {
38 | setKeys()
39 | },
40 | { immediate: true }
41 | )
42 |
43 | function setKeys() {
44 | const matched = route.matched
45 | const matchedNames = matched.map((it) => it.name as string)
46 | const matchLen = matchedNames.length
47 | const matchExpandKeys = matchedNames.slice(0, matchLen - 1)
48 | const openKey = matchedNames[matchLen - 1]
49 | expandKeys.value = matchExpandKeys
50 | // 处理平级模式的菜单
51 | if (route?.meta?.activeMenuName) {
52 | currentMenu.value = route.meta.activeMenuName as string
53 | } else {
54 | currentMenu.value = openKey
55 | }
56 | }
57 |
58 | // 展开收起
59 | function updateExpandKeys(keys: string[]) {
60 | expandKeys.value = keys
61 | }
62 |
63 | // 选中的菜单
64 | function updateValue(key: string) {
65 | currentMenu.value = key
66 | }
67 |
68 | return {
69 | menuRef,
70 | expandKeys,
71 | updateExpandKeys,
72 | currentMenu,
73 | updateValue
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/layouts/sider/AppLogo.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | ]
9 |

10 |
Pear Admin Naive
11 |
12 |
13 |
14 |
36 |
--------------------------------------------------------------------------------
/src/layouts/sider/PearSider.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
26 |
27 |
28 |
29 |
30 |
31 |
36 |
--------------------------------------------------------------------------------
/src/layouts/useLayoutBreakPoint.ts:
--------------------------------------------------------------------------------
1 | import { ref, watch } from 'vue'
2 | import { useBreakPoint } from '@/composables/useBreakPoint'
3 | import { BREAK_POINT_SIZE } from '@/enums/breakPointEnum'
4 |
5 | export interface LayoutBreakPoint {
6 | collapsed: boolean
7 | isMobile: boolean
8 | }
9 |
10 | export function useLayoutBreakPoint() {
11 | const { device } = useBreakPoint()
12 |
13 | const config = ref({
14 | collapsed: device.width < BREAK_POINT_SIZE.lg,
15 | isMobile: device.width < BREAK_POINT_SIZE.lg
16 | })
17 |
18 | watch(
19 | () => device.width,
20 | (w) => {
21 | config.value = {
22 | collapsed: w < BREAK_POINT_SIZE.lg,
23 | isMobile: w < BREAK_POINT_SIZE.lg
24 | }
25 | },
26 | { immediate: true }
27 | )
28 |
29 | return {
30 | config
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import App from './App.vue'
3 | import '@purge-icons/generated'
4 | import 'uno.css'
5 | import '@/style/global.less'
6 | import { useAppRouter } from '@/router'
7 |
8 | import { useAppStore } from '@/store'
9 |
10 | const app = createApp(App)
11 |
12 | useAppRouter(app)
13 | useAppStore(app)
14 |
15 | app.mount('#app')
16 |
--------------------------------------------------------------------------------
/src/mock/createFetchSever.ts:
--------------------------------------------------------------------------------
1 | import Mock from 'mockjs'
2 | /**
3 | * interface Response extends Body {
4 | readonly headers: Headers;
5 | readonly ok: boolean;
6 | readonly redirected: boolean;
7 | readonly status: number;
8 | readonly statusText: string;
9 | readonly type: ResponseType;
10 | readonly url: string;
11 | clone(): Response;
12 | }
13 | * @param mockList
14 | */
15 | export function createFetchSever(mockList: any[]) {
16 | if (!window['originFetch']) {
17 | window['originFetch'] = window.fetch
18 | window.fetch = function (fetchUrl: string, init: RequestInit) {
19 | const currentMock = mockList.find((mi) => fetchUrl.includes(mi.url))
20 | if (currentMock) {
21 | const result = createFetchReturn(currentMock, init)
22 | return result
23 | } else {
24 | return window['originFetch'](fetchUrl, init)
25 | }
26 | }
27 | }
28 | }
29 |
30 | function __param2Obj__(url: string) {
31 | const search = url.split('?')[1]
32 | if (!search) {
33 | return {}
34 | }
35 | return JSON.parse(
36 | '{"' +
37 | decodeURIComponent(search)
38 | .replace(/"/g, '\\"')
39 | .replace(/&/g, '","')
40 | .replace(/=/g, '":"')
41 | .replace(/\+/g, ' ') +
42 | '"}'
43 | )
44 | }
45 |
46 | function __Fetch2ExpressReqWrapper__(handle: (d: any) => any) {
47 | return function (options: any) {
48 | let result = null
49 | if (typeof handle === 'function') {
50 | const { body, method, url, headers } = options
51 |
52 | let b = body
53 | try {
54 | b = JSON.parse(body)
55 | } catch {}
56 | result = handle({
57 | method,
58 | body: b,
59 | query: __param2Obj__(url),
60 | headers
61 | })
62 | } else {
63 | result = handle
64 | }
65 |
66 | return Mock.mock(result)
67 | }
68 | }
69 |
70 | function setupTimeOut(timeout = 0) {
71 | timeout &&
72 | Mock.setup({
73 | timeout
74 | })
75 | }
76 |
77 | function createFetchReturn(mock: Recordable, init: RequestInit) {
78 | const { timeout, response } = mock
79 | setupTimeOut(timeout)
80 | const mockFn = __Fetch2ExpressReqWrapper__(response)
81 | const data = mockFn(init)
82 | const result = {
83 | ok: true,
84 | status: 200,
85 | clone: () => {
86 | return result
87 | },
88 | text() {
89 | return Promise.resolve(data)
90 | },
91 | json() {
92 | return Promise.resolve(data)
93 | }
94 | }
95 | return result
96 | }
97 |
--------------------------------------------------------------------------------
/src/mock/mockUtil.ts:
--------------------------------------------------------------------------------
1 | interface ResponseData {
2 | code?: number
3 | success?: boolean
4 | msg?: string
5 | data?: unknown
6 | timestamp?: number
7 | }
8 | export function createResponseData(responseData: ResponseData): ResponseData {
9 | return Object.assign(
10 | {},
11 | {
12 | code: 0,
13 | success: true,
14 | msg: '成功',
15 | data: null,
16 | timestamp: new Date().getTime()
17 | },
18 | responseData
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/mock/modules/chartData.ts:
--------------------------------------------------------------------------------
1 | import type { MockMethod } from 'vite-plugin-mock'
2 | import { createResponseData } from '../mockUtil'
3 | import data from './gdp.json'
4 |
5 | export default [
6 | {
7 | url: '/dashboard/getGDP',
8 | method: 'get',
9 | response: () => {
10 | return createResponseData({
11 | data
12 | })
13 | }
14 | }
15 | ] as MockMethod[]
16 |
--------------------------------------------------------------------------------
/src/mock/modules/system.ts:
--------------------------------------------------------------------------------
1 | import type { MockMethod } from 'vite-plugin-mock'
2 | import Mock, { Random } from 'mockjs'
3 | import { createResponseData } from '../mockUtil'
4 |
5 | export default [
6 | {
7 | url: '/user/login',
8 | method: 'post',
9 | response: ({ body }) => {
10 | const data = {
11 | userInfo: {
12 | username: body.username,
13 | password: body.password
14 | },
15 | token: Math.random().toString(32).substr(3),
16 | routes: [],
17 | permissions: []
18 | }
19 | if (body.username !== 'admin' || body.password !== 'admin') {
20 | return createResponseData({
21 | code: -1,
22 | msg: '账号或密码不正确',
23 | success: false
24 | })
25 | }
26 | return createResponseData({
27 | data
28 | })
29 | }
30 | },
31 | {
32 | url: '/user/getCapture',
33 | method: 'get',
34 | response: (req) => {
35 | const value = Mock.mock({ regexp: /[a-zA-Z0-9]{4}/ }).regexp
36 | return {
37 | code: 0,
38 | data: {
39 | image: Random.image('100x38', Mock.mock('@color'), value),
40 | code: value
41 | }
42 | }
43 | }
44 | }
45 | ] as MockMethod[]
46 |
--------------------------------------------------------------------------------
/src/mock/modules/tableDemo.ts:
--------------------------------------------------------------------------------
1 | import type { MockMethod } from 'vite-plugin-mock'
2 | import Mock, { Random } from 'mockjs'
3 | import { createResponseData } from '../mockUtil'
4 |
5 | const TOTAL = 50000
6 |
7 | const getTableItem = (): Recordable => {
8 | return Mock.mock({
9 | 'age|1-100': 100,
10 | 'rate|1-5': '★',
11 | 'status|1-2': true,
12 | birthday: Mock.mock('@date("MM-dd")'),
13 | createTime: Random.datetime(),
14 | avatar: Random.image('200x200', '#894FC4', '#FFF', 'png', 'avatar'),
15 | email: Mock.mock('@email'),
16 | city: Mock.mock('@county(true)'),
17 | zip: Mock.mock('@zip()'),
18 | id: Mock.mock('@guid()'),
19 | name: Mock.mock('@cname()')
20 | })
21 | }
22 |
23 | const getTableData = (dataLength) => {
24 | const data: Recordable[] = []
25 | for (let i = 0; i < dataLength; i++) {
26 | data.push(getTableItem())
27 | }
28 | return data
29 | }
30 |
31 | // 100 / 30 = 3...10
32 | const getCurrentPage = (pageNo, pageSize) => {
33 | if (pageNo * pageSize <= TOTAL) {
34 | return getTableData(pageSize)
35 | } else {
36 | const dataLen = TOTAL % pageSize
37 | return getTableData(dataLen)
38 | }
39 | }
40 | export default [
41 | {
42 | url: '/demo/table/getTableRecords',
43 | method: 'post',
44 | response: ({ body }) => {
45 | const { pageSize, pageNo } = body
46 | const data = {
47 | list: getCurrentPage(pageNo, pageSize),
48 | total: TOTAL,
49 | pageSize,
50 | pageNo
51 | }
52 | return createResponseData({
53 | data
54 | })
55 | }
56 | }
57 | ] as MockMethod[]
58 |
--------------------------------------------------------------------------------
/src/mock/modules/useApiHooks.ts:
--------------------------------------------------------------------------------
1 | import type { MockMethod } from 'vite-plugin-mock'
2 | import Mock, { Random } from 'mockjs'
3 | import { createResponseData } from '../mockUtil'
4 |
5 | const getDemoData = (): Recordable => {
6 | return Mock.mock({
7 | 'age|1-100': 100,
8 | 'rate|1-5': '★',
9 | 'status|1-2': true,
10 | birthday: Mock.mock('@date("MM-dd")'),
11 | createTime: Random.datetime(),
12 | avatar: Random.image('200x200', '#894FC4', '#FFF', 'png', 'avatar'),
13 | email: Mock.mock('@email'),
14 | city: Mock.mock('@county(true)'),
15 | zip: Mock.mock('@zip()'),
16 | id: Mock.mock('@id()'),
17 | name: Mock.mock('@cname()')
18 | })
19 | }
20 |
21 | export default [
22 | {
23 | url: '/demo/use/api/getDemoData',
24 | method: 'post',
25 | response: ({ body }) => {
26 | const data = {
27 | ...getDemoData(),
28 | fetchData: body
29 | }
30 | return createResponseData({
31 | data
32 | })
33 | }
34 | }
35 | ] as MockMethod[]
36 |
--------------------------------------------------------------------------------
/src/mock/useMock.ts:
--------------------------------------------------------------------------------
1 | import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
2 |
3 | import system from './modules/system'
4 | import table from './modules/tableDemo'
5 | import useApi from './modules/useApiHooks'
6 | import chartData from './modules/chartData'
7 | import { createFetchSever } from '@/mock/createFetchSever'
8 |
9 | const modules = [...system, ...table, ...useApi, ...chartData]
10 |
11 | export function setupProdMockServer() {
12 | createProdMockServer(modules)
13 | createFetchSever(modules)
14 | }
15 |
--------------------------------------------------------------------------------
/src/router/guard/index.ts:
--------------------------------------------------------------------------------
1 | import { permissionGuard } from '@/router/guard/permissionGuard'
2 | import type { Router } from 'vue-router'
3 |
4 | export function useAppRouterGuard(router: Router) {
5 | permissionGuard(router)
6 | }
7 |
--------------------------------------------------------------------------------
/src/router/guard/permissionGuard.ts:
--------------------------------------------------------------------------------
1 | import type { Router } from 'vue-router'
2 | import { useUserStore } from '@/store/modules/userInfo'
3 | import { unref } from 'vue'
4 |
5 | export function permissionGuard(router: Router): void {
6 | router.beforeEach((to, from, next) => {
7 | const userStore = useUserStore()
8 | const token = unref(userStore.token)
9 | if (!token) {
10 | if (to.name === 'Login') {
11 | next()
12 | } else {
13 | next('/login')
14 | }
15 | } else {
16 | if (to.name === 'Login') {
17 | next('/dashboard')
18 | } else {
19 | next()
20 | }
21 | }
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import { createWebHashHistory, createRouter } from 'vue-router'
2 | import type { App } from 'vue'
3 | import routes from './routes'
4 | import { useAppRouterGuard } from '@/router/guard'
5 |
6 | const router = createRouter({
7 | history: createWebHashHistory('/'),
8 | routes,
9 | scrollBehavior() {
10 | return { top: 0 }
11 | }
12 | })
13 |
14 | useAppRouterGuard(router)
15 |
16 | export const useAppRouter = (app: App): void => {
17 | app.use(router)
18 | }
19 |
--------------------------------------------------------------------------------
/src/router/modules/components/index.ts:
--------------------------------------------------------------------------------
1 | import type { RouteRecordRaw } from 'vue-router'
2 | import { BasicLayout } from '@/layouts'
3 |
4 | const componentsRoutes: RouteRecordRaw[] = [
5 | {
6 | path: '/components',
7 | name: 'Components',
8 | component: BasicLayout,
9 | redirect: '/components/antVG2',
10 | meta: {
11 | title: '组件',
12 | icon: 'icon-park-outline:components'
13 | },
14 | children: [
15 | {
16 | path: 'antVG2',
17 | name: 'AntVG2',
18 | component: () => import('@/views/demo/components/antvG2/index.vue'),
19 | meta: {
20 | title: 'AntV/G2',
21 | icon: 'octicon:graph'
22 | }
23 | }
24 | ]
25 | }
26 | ]
27 |
28 | export default {
29 | sort: 4,
30 | routes: componentsRoutes
31 | }
32 |
--------------------------------------------------------------------------------
/src/router/modules/dashboard/index.ts:
--------------------------------------------------------------------------------
1 | import type { RouteRecordRaw } from 'vue-router'
2 | // import { BasicLayout } from '@/layouts'
3 |
4 | const dashboardRoutes: RouteRecordRaw[] = [
5 | {
6 | path: '/dashboard',
7 | name: 'Dashboard',
8 | component: () => import('@/layouts'),
9 | redirect: '/dashboard/analysis',
10 | meta: {
11 | title: '仪表盘',
12 | icon: 'icon-park-outline:dashboard-two'
13 | },
14 | children: [
15 | {
16 | path: 'analysis',
17 | name: 'Analysis',
18 | component: () => import('@/views/demo/dashboard/analysis/index.vue'),
19 | meta: {
20 | title: '分析页',
21 | icon: 'uim:analysis'
22 | }
23 | },
24 | {
25 | path: 'workspace',
26 | name: 'Workspace',
27 | component: () => import('@/views/demo/dashboard/workspace/index.vue'),
28 | meta: {
29 | title: '工作台',
30 | icon: 'carbon:workspace'
31 | }
32 | }
33 | ]
34 | }
35 | ]
36 |
37 | export default {
38 | sort: 1,
39 | routes: dashboardRoutes
40 | }
41 |
--------------------------------------------------------------------------------
/src/router/modules/errors.ts:
--------------------------------------------------------------------------------
1 | import type { RouteRecordRaw } from 'vue-router'
2 | import NotFound from '@/components/Error/Error404.vue'
3 |
4 | // 错误页面不在route tabs中展示
5 | export const ErrorPageNames = ['NotFound', 'NotPermission']
6 |
7 | const errorRoutes: RouteRecordRaw[] = [
8 | {
9 | path: '/:pathMatch(.*)*',
10 | name: 'NotFound',
11 | meta: {
12 | title: '404'
13 | },
14 | component: NotFound
15 | }
16 | ]
17 |
18 | export default errorRoutes
19 |
--------------------------------------------------------------------------------
/src/router/modules/errors/index.ts:
--------------------------------------------------------------------------------
1 | import type { RouteRecordRaw } from 'vue-router'
2 | import { BasicLayout } from '@/layouts/index'
3 |
4 | const errors: RouteRecordRaw[] = [
5 | {
6 | path: '/error',
7 | name: 'Error',
8 | meta: {
9 | title: '错误页面',
10 | icon: 'mi:circle-error'
11 | },
12 | component: BasicLayout,
13 | children: [
14 | {
15 | path: '/404',
16 | name: '404',
17 | meta: {
18 | title: '404'
19 | },
20 | component: () => import('@/views/error/404.vue')
21 | },
22 | {
23 | path: '/403',
24 | name: '403',
25 | meta: {
26 | title: '403'
27 | },
28 | component: () => import('@/views/error/403.vue')
29 | },
30 | {
31 | path: '/500',
32 | name: '500',
33 | meta: {
34 | title: '500'
35 | },
36 | component: () => import('@/views/error/500.vue')
37 | }
38 | ]
39 | }
40 | ]
41 |
42 | export default {
43 | sort: 8,
44 | routes: errors
45 | }
46 |
--------------------------------------------------------------------------------
/src/router/modules/feature/index.ts:
--------------------------------------------------------------------------------
1 | import type { RouteRecordRaw } from 'vue-router'
2 | import { BasicLayout } from '@/layouts'
3 |
4 | const componentsRoutes: RouteRecordRaw[] = [
5 | {
6 | path: '/feature',
7 | name: 'Feature',
8 | component: BasicLayout,
9 | redirect: '/feature/keepAlive',
10 | meta: {
11 | title: '功能',
12 | icon: 'typcn:feather'
13 | },
14 | children: [
15 | {
16 | path: 'keepAlive',
17 | name: 'KeepAlive',
18 | component: () => import('@/views/demo/feature/keep-alive/index.vue'),
19 | meta: {
20 | title: 'KeepAlive',
21 | icon: 'ic:round-insert-drive-file',
22 | keepAlive: true
23 | }
24 | }
25 | ]
26 | }
27 | ]
28 |
29 | export default {
30 | sort: 9,
31 | routes: componentsRoutes
32 | }
33 |
--------------------------------------------------------------------------------
/src/router/modules/form/index.ts:
--------------------------------------------------------------------------------
1 | import type { RouteRecordRaw } from 'vue-router'
2 |
3 | const formDemoRoutes: RouteRecordRaw[] = [
4 | {
5 | path: '/form',
6 | name: 'Form',
7 | // component: BasicLayout,
8 | component: () => import('@/layouts/index'),
9 | redirect: '/form/basicForm',
10 | meta: {
11 | title: '表单',
12 | icon: 'clarity:form-line'
13 | },
14 | children: [
15 | {
16 | path: 'basicForm',
17 | name: 'BasicForm',
18 | component: () => import('@/views/demo/form/BasicFormDemo.vue'),
19 | meta: {
20 | title: '基础表单',
21 | icon: 'ant-design:form-outlined'
22 | }
23 | },
24 | {
25 | path: 'useForm',
26 | name: 'SseForm',
27 | component: () => import('@/views/demo/form/UseFormDemo.vue'),
28 | meta: {
29 | title: 'UseForm',
30 | icon: 'ant-design:form-outlined'
31 | }
32 | },
33 | {
34 | path: 'useFormRef',
35 | name: 'UseFormRef',
36 | component: () => import('@/views/demo/form/UseFormRefDemo.vue'),
37 | meta: {
38 | title: 'UseFormRef',
39 | icon: 'ant-design:form-outlined'
40 | }
41 | }
42 | ]
43 | }
44 | ]
45 |
46 | export default {
47 | sort: 2,
48 | routes: formDemoRoutes
49 | }
50 |
--------------------------------------------------------------------------------
/src/router/modules/login.ts:
--------------------------------------------------------------------------------
1 | import type { RouteRecordRaw } from 'vue-router'
2 |
3 | const rootRoutes: RouteRecordRaw[] = [
4 | {
5 | path: '/login',
6 | name: 'Login',
7 | component: () => import('@/views/login/index.vue'),
8 | meta: {
9 | title: '登录'
10 | }
11 | }
12 | ]
13 |
14 | export default rootRoutes
15 |
--------------------------------------------------------------------------------
/src/router/modules/root.ts:
--------------------------------------------------------------------------------
1 | import type { RouteRecordRaw } from 'vue-router'
2 |
3 | const rootRoutes: RouteRecordRaw[] = [
4 | {
5 | path: '/',
6 | name: 'Root',
7 | redirect: '/dashboard/analysis'
8 | }
9 | ]
10 |
11 | export default rootRoutes
12 |
--------------------------------------------------------------------------------
/src/router/modules/system/index.ts:
--------------------------------------------------------------------------------
1 | import type { RouteRecordRaw } from 'vue-router'
2 |
3 | const systemRoutes: RouteRecordRaw[] = [
4 | {
5 | path: '/system',
6 | name: 'System',
7 | // component: BasicLayout,
8 | component: () => import('@/layouts'),
9 | redirect: '/system/user',
10 | meta: {
11 | title: '系统设置',
12 | icon: 'ri:settings-4-line'
13 | },
14 | children: [
15 | {
16 | path: 'user',
17 | name: 'User',
18 | component: () => import('@/views/demo/system/account/index.vue'),
19 | meta: {
20 | title: '用户管理',
21 | icon: 'ri:account-box-line'
22 | }
23 | },
24 | {
25 | path: 'hidePage',
26 | name: 'HidePage',
27 | component: () => import('@/views/demo/system/account/hidePage.vue'),
28 | meta: {
29 | title: 'HidePageManage',
30 | hidden: true,
31 | activeMenuName: 'User',
32 | icon: 'ri:account-box-line'
33 | }
34 | },
35 | {
36 | path: 'menus',
37 | name: 'Menus',
38 | component: () => import('@/views/demo/system/menus/index.vue'),
39 | meta: {
40 | title: '菜单管理',
41 | icon: 'system-uicons:side-menu'
42 | }
43 | },
44 | {
45 | path: 'role',
46 | name: 'Role',
47 | component: () => import('@/views/fast-api/role/index.vue'),
48 | meta: {
49 | title: '角色管理',
50 | icon: 'uil:user-square'
51 | }
52 | }
53 | ]
54 | }
55 | ]
56 |
57 | export default {
58 | sort: 6,
59 | routes: systemRoutes
60 | }
61 |
--------------------------------------------------------------------------------
/src/router/modules/table/index.ts:
--------------------------------------------------------------------------------
1 | import type { RouteRecordRaw } from 'vue-router'
2 |
3 | const tableDateRoutes: RouteRecordRaw[] = [
4 | {
5 | path: '/table',
6 | name: 'Table',
7 | component: () => import('@/layouts/index'),
8 | redirect: '/table/basicTable',
9 | meta: {
10 | title: '表格',
11 | icon: 'ph:table-light'
12 | },
13 | children: [
14 | {
15 | path: 'basicTable',
16 | name: 'basicTable',
17 | component: () => import('@/views/demo/table/BasicTableDemo.vue'),
18 | meta: {
19 | title: '基础表格',
20 | icon: 'la:table'
21 | }
22 | },
23 | {
24 | path: 'searchTable',
25 | name: 'SearchTable',
26 | component: () => import('@/views/demo/table/SearchTableDemo.vue'),
27 | meta: {
28 | title: '查询表格',
29 | icon: 'mdi:table-search'
30 | }
31 | },
32 | {
33 | path: 'customHeader',
34 | name: 'CustomHeader',
35 | component: () => import('@/views/demo/table/DefTableHead.vue'),
36 | meta: {
37 | title: '自定义表头',
38 | icon: 'system-uicons:table-header'
39 | }
40 | }
41 | ]
42 | }
43 | ]
44 |
45 | export default {
46 | sort: 3,
47 | routes: tableDateRoutes
48 | }
49 |
--------------------------------------------------------------------------------
/src/router/modules/top/index.ts:
--------------------------------------------------------------------------------
1 | import type { RouteRecordRaw } from 'vue-router'
2 | import { BasicLayout } from '@/layouts'
3 |
4 | const topRoutes: RouteRecordRaw[] = [
5 | {
6 | path: '/topLevel',
7 | name: 'TopLevel',
8 | component: BasicLayout,
9 | meta: {
10 | title: '顶级菜单',
11 | icon: 'entypo:tools',
12 | hiddenChildren: true
13 | },
14 | children: [
15 | {
16 | path: 'topIndex',
17 | name: 'TopIndex',
18 | component: () => import('@/views/demo/top-level/index.vue'),
19 | meta: {
20 | title: '顶级菜单1',
21 | icon: 'entypo:tools'
22 | }
23 | },
24 | {
25 | path: 'topIndex2',
26 | name: 'TopIndex2',
27 | component: () => import('@/views/demo/top-level/index2.vue'),
28 | meta: {
29 | title: '顶级菜单2',
30 | icon: 'entypo:tools'
31 | }
32 | }
33 | ]
34 | }
35 | ]
36 |
37 | export default {
38 | sort: 7,
39 | routes: topRoutes
40 | }
41 |
--------------------------------------------------------------------------------
/src/router/modules/util-demo/index.ts:
--------------------------------------------------------------------------------
1 | import type { RouteRecordRaw } from 'vue-router'
2 | import { getParentComponent, BasicLayout } from '@/layouts'
3 |
4 | const utilDemoRoutes: RouteRecordRaw[] = [
5 | {
6 | path: '/utils',
7 | name: 'Utils',
8 | component: BasicLayout,
9 | meta: {
10 | title: '工具案例',
11 | icon: 'ri:tools-line'
12 | },
13 | redirect: '/utils/http',
14 | children: [
15 | {
16 | path: 'http',
17 | name: 'Http',
18 | component: getParentComponent('parentHttp'),
19 | meta: {
20 | title: '请求工具',
21 | icon: 'ic:baseline-http'
22 | },
23 | redirect: '/utils/http/topAwait',
24 | children: [
25 | {
26 | path: 'topAwait',
27 | name: 'TopAwait',
28 | component: () => import('@/views/demo/utils-demo/http/topAwait.vue'),
29 | meta: {
30 | title: '顶层异步请求'
31 | }
32 | },
33 | {
34 | path: 'useApi',
35 | name: 'UseApi',
36 | meta: {
37 | title: 'useApi'
38 | },
39 | component: () => import('@/views/demo/utils-demo/http/useApiHooks.vue')
40 | }
41 | ]
42 | },
43 | {
44 | path: 'composable',
45 | name: 'Composable',
46 | component: getParentComponent('parentComposable'),
47 | redirect: '/utils/composable/usePromiseFn',
48 | meta: {
49 | title: 'Composable',
50 | icon: 'ic:baseline-webhook'
51 | },
52 | children: [
53 | {
54 | path: 'usePromiseFn',
55 | name: 'UsePromiseFn',
56 | meta: {
57 | title: 'usePromiseFn'
58 | },
59 | component: () => import('@/views/demo/utils-demo/composables/usePromiseFn.vue')
60 | }
61 | ]
62 | }
63 | ]
64 | }
65 | ]
66 |
67 | export default {
68 | sort: 5,
69 | routes: utilDemoRoutes
70 | }
71 |
--------------------------------------------------------------------------------
/src/router/routes.ts:
--------------------------------------------------------------------------------
1 | import type { RouteRecordRaw } from 'vue-router'
2 |
3 | const modules = import.meta.globEager('./modules/**/*.ts')
4 |
5 | const routes = Object.keys(modules).reduce((routes, key) => {
6 | const module = modules[key].default
7 | if (Array.isArray(module)) {
8 | return [...routes, ...module]
9 | } else {
10 | return [...routes, ...module.routes]
11 | }
12 | }, [] as RouteRecordRaw[])
13 |
14 | export default routes
15 |
--------------------------------------------------------------------------------
/src/router/util.tsx:
--------------------------------------------------------------------------------
1 | import type { MenuOption } from 'naive-ui'
2 | import type { RouteRecordRaw } from 'vue-router'
3 | import Icon from '@/components/Icon/Icon.vue'
4 |
5 | export function getMenuOptions(routes: RouteRecordRaw[]): MenuOption[] {
6 | let menuOptions: MenuOption[] = []
7 | routes.forEach((route) => {
8 | if (!route.meta?.hidden) {
9 | if (route.meta?.hiddenChildren) {
10 | const children = getMenuOptions(route.children || [])
11 | menuOptions = [...menuOptions, ...children]
12 | } else {
13 | const menuOption: MenuOption = {
14 | label: () => {
15 | if (route.children && Array.isArray(route.children)) {
16 | return route.meta?.title
17 | } else {
18 | return {route.meta?.title}
19 | }
20 | },
21 | icon: route.meta?.icon
22 | ? () => {
23 | return
24 | }
25 | : undefined,
26 | key: route.name as string
27 | }
28 | if (route.children && route.children.length > 0) {
29 | menuOption.children = getMenuOptions(route.children)
30 | }
31 | menuOptions.push(menuOption)
32 | }
33 | }
34 | })
35 | return menuOptions
36 | }
37 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import type { App } from 'vue'
2 | import { createPinia } from 'pinia'
3 |
4 | const store = createPinia()
5 |
6 | export function useAppStore(app: App): void {
7 | app.use(store)
8 | }
9 |
10 | export { store }
11 |
--------------------------------------------------------------------------------
/src/store/modules/app.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 | import { useLocalStorage, useSessionStorage } from '@vueuse/core'
3 | import { get } from '@vueuse/core'
4 | import type { RemoveableRef } from '@vueuse/core'
5 | import { unref } from 'vue'
6 |
7 | export type ThemeName = 'dark' | 'light' | 'auto'
8 |
9 | export interface RouteTag {
10 | name: string
11 | path: string
12 | fullPath: string
13 | title: string
14 | }
15 |
16 | export interface AppConfiguration {
17 | theme: RemoveableRef
18 | tags: RemoveableRef
19 | keepAliveNames: string[]
20 | }
21 |
22 | const useAppStore = defineStore({
23 | id: 'app',
24 | state: (): AppConfiguration => {
25 | return {
26 | theme: useLocalStorage('theme', 'dark'),
27 | tags: useSessionStorage('tags', []),
28 | keepAliveNames: []
29 | }
30 | },
31 | getters: {},
32 | actions: {
33 | toggleTheme(theme: ThemeName) {
34 | this.theme = theme
35 | localStorage.setItem('theme', theme)
36 | },
37 | addTag(tag: RouteTag) {
38 | const tagsRef: RemoveableRef = useSessionStorage('tags', [])
39 | if (tagsRef.value.findIndex((it) => get(it, 'name') === get(tag, 'name')) < 0) {
40 | tagsRef.value.push(tag)
41 | }
42 | this.tags = tagsRef.value
43 | },
44 | removeTag(tag: RouteTag) {
45 | const tagsRef: RemoveableRef = useSessionStorage('tags', [])
46 | const index = tagsRef.value.findIndex((it) => get(it, 'name') === get(tag, 'name'))
47 | if (index > -1) {
48 | tagsRef.value.splice(index, 1)
49 | this.tags = tagsRef.value
50 | }
51 | },
52 | closeLeftTag(currentName: string) {
53 | const tagsRef: RemoveableRef = useSessionStorage('tags', [])
54 | if (unref(tagsRef).length === 0) {
55 | return
56 | }
57 | const index = unref(tagsRef).findIndex((it) => it.name === currentName)
58 | this.tags = unref(tagsRef).filter((it, idx) => index <= idx)
59 | },
60 | closeRightTag(currentName: string) {
61 | const tagsRef: RemoveableRef = useSessionStorage('tags', [])
62 | if (unref(tagsRef).length === 0) {
63 | return
64 | }
65 | const index = unref(tagsRef).findIndex((it) => it.name === currentName)
66 | this.tags = unref(tagsRef).filter((it, idx) => idx <= index)
67 | },
68 | closeOtherTag(currentName: string) {
69 | const tagsRef: RemoveableRef = useSessionStorage('tags', [])
70 | if (unref(tagsRef).length === 0) {
71 | return
72 | }
73 | this.tags = unref(tagsRef).filter((it) => it.name === currentName)
74 | },
75 | setKeepAliveNames(keys: string[]) {
76 | this.keepAliveNames = keys
77 | }
78 | }
79 | })
80 |
81 | export { useAppStore }
82 |
--------------------------------------------------------------------------------
/src/store/modules/userInfo.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 | import { useStorage } from '@vueuse/core'
3 | import type { RouteRecordRaw } from 'vue-router'
4 |
5 | const menuRoutes = import.meta.globEager('../../router/modules/*/*.ts')
6 |
7 | interface UserState {
8 | userInfo: Nullable
9 | token: Nullable
10 | menuRoutes: RouteRecordRaw[]
11 | }
12 |
13 | const useUserStore = defineStore({
14 | id: 'user',
15 | state: (): UserState => {
16 | return {
17 | userInfo: useStorage('userInfo', null, sessionStorage).value,
18 | token: useStorage('token', null, sessionStorage).value,
19 | menuRoutes: useStorage('userRoutes', [], sessionStorage).value
20 | }
21 | },
22 | getters: {},
23 | actions: {
24 | setUserInfo(userInfo: Recordable) {
25 | const userInfoRef = useStorage('userInfo', userInfo, sessionStorage)
26 | userInfoRef.value = userInfo
27 | this.userInfo = userInfoRef.value
28 | },
29 | setToken(token: string) {
30 | const tokenRef = useStorage('token', token, sessionStorage)
31 | tokenRef.value = token
32 | this.token = tokenRef.value
33 | },
34 | setUserMenuRoutes() {
35 | const userRoutes = useStorage('userRoutes', [], sessionStorage)
36 | const routeModules = Object.keys(menuRoutes).reduce((routes, key) => {
37 | const module = menuRoutes[key]?.default || {}
38 | routes.push(module)
39 | return routes
40 | }, [] as any)
41 | routeModules.sort((p, n) => p.sort - n.sort)
42 | const routes = routeModules.map((it) => it.routes).flat()
43 | userRoutes.value = routes
44 | this.menuRoutes = routes
45 | }
46 | }
47 | })
48 |
49 | export { useUserStore }
50 |
--------------------------------------------------------------------------------
/src/style/global.less:
--------------------------------------------------------------------------------
1 | @import 'transition';
2 | [class^='pear-'] {
3 | box-sizing: border-box;
4 | }
5 |
--------------------------------------------------------------------------------
/src/style/transition.less:
--------------------------------------------------------------------------------
1 | .fade-right-enter-active {
2 | transition: all 0.9s;
3 | }
4 | .fade-right-leave-active {
5 | transition: all 0.9s;
6 | }
7 | .fade-right-enter-from {
8 | opacity: 0;
9 | transform: translateX(-35px);
10 | }
11 | .fade-right-leave-to {
12 | opacity: 0;
13 | transform: translateX(35px);
14 | display: none;
15 | }
16 |
17 | .fade-top-enter-active {
18 | transition: all 0.9s;
19 | }
20 | .fade-top-leave-active {
21 | transition: all 0.9s;
22 | }
23 | .fade-top-enter-from {
24 | opacity: 0;
25 | transform: translateY(35px);
26 | }
27 | .fade-top-leave-to {
28 | opacity: 0;
29 | transform: translateY(-35px);
30 | display: none;
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/componentUtil.ts:
--------------------------------------------------------------------------------
1 | export function getComponentProps(
2 | props: U,
3 | componentProps: T
4 | ): T {
5 | const cKeys = Object.keys(componentProps)
6 | return Object.keys(props).reduce((cProps: T, propKey: keyof T) => {
7 | if (cKeys.includes(propKey as string)) {
8 | return {
9 | ...cProps,
10 | [propKey]: props[propKey]
11 | }
12 | } else {
13 | return cProps
14 | }
15 | }, {} as T)
16 | }
17 |
--------------------------------------------------------------------------------
/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | export const isDevelopment = (): boolean => {
2 | return process.env.NODE_ENV === 'development'
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/demo/components/antvG2/index.vue:
--------------------------------------------------------------------------------
1 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | Reload
90 | Refresh
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | Reload
101 | Refresh
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/src/views/demo/components/antvG2/renderGameChart.ts:
--------------------------------------------------------------------------------
1 | import type { Chart } from '@antv/g2'
2 |
3 | function findMaxMin(data) {
4 | let maxValue = 0
5 | let minValue = 50000
6 | let maxObj = null
7 | let minObj = null
8 | for (const d of data) {
9 | if (d.Close > maxValue) {
10 | maxValue = d.Close
11 | maxObj = d
12 | }
13 | if (d.Close < minValue) {
14 | minValue = d.Close
15 | minObj = d
16 | }
17 | }
18 | return { max: maxObj, min: minObj } as any
19 | }
20 |
21 | export function renderGameChart(chart: Nullable, data: any) {
22 | chart?.data(data)
23 | chart?.scale({
24 | Date: {
25 | tickCount: 10
26 | },
27 | Close: {
28 | nice: true
29 | }
30 | })
31 | chart?.axis('Date', {
32 | label: {
33 | formatter: (text) => {
34 | const dataStrings = text.split('.')
35 | return dataStrings[2] + '-' + dataStrings[1] + '-' + dataStrings[0]
36 | }
37 | }
38 | })
39 |
40 | chart?.line().position('Date*Close')
41 | // annotation
42 | const { min, max } = findMaxMin(data)
43 | chart?.annotation().dataMarker({
44 | top: true,
45 | position: [max.Date, max.Close],
46 | text: {
47 | content: '全部峰值:' + max.Close
48 | },
49 | line: {
50 | length: 30
51 | }
52 | })
53 | chart?.annotation().dataMarker({
54 | top: true,
55 | position: [min.Date, min.Close],
56 | text: {
57 | content: '全部谷值:' + min.Close
58 | },
59 | line: {
60 | length: 50
61 | }
62 | })
63 | chart?.forceFit()
64 | chart?.render()
65 | }
66 |
--------------------------------------------------------------------------------
/src/views/demo/components/antvG2/service.ts:
--------------------------------------------------------------------------------
1 | export async function fetchGameChart() {
2 | const data = await fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/nintendo.json')
3 | .then((res) => res.json())
4 | .then((data) => data)
5 |
6 | return data
7 | }
8 |
--------------------------------------------------------------------------------
/src/views/demo/dashboard/analysis/index.vue:
--------------------------------------------------------------------------------
1 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | / 100
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | / 100
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | / 100
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | / 100
92 |
93 |
94 |
95 |
96 |
97 |
98 |
106 |
107 | Vite
108 |
109 |
110 |
111 |
119 |
120 | Vue
121 |
122 |
123 |
124 |
132 |
133 | Typescript
134 |
135 |
136 |
137 |
145 |
146 | Java
147 |
148 |
149 |
150 |
158 |
159 | Nginx
160 |
161 |
162 |
163 |
171 |
172 | ^_^
173 |
174 |
175 |
176 |
184 |
185 | o_o
186 |
187 |
188 |
189 |
197 |
198 | Good
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 | 重新加载数据
208 | 仅图表刷新
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
--------------------------------------------------------------------------------
/src/views/demo/dashboard/analysis/renderChart/renderDynamicChart.ts:
--------------------------------------------------------------------------------
1 | import { Chart, registerAnimation } from '@antv/g2'
2 |
3 | export function renderDynamicChart(chart: Chart, data: any) {
4 | chart.clear()
5 | clearChartInterval()
6 | function clearChartInterval() {
7 | if (window['interval']) {
8 | clearInterval(window['interval'])
9 | }
10 | }
11 | registerAnimation('label-appear', (element, animateCfg, cfg) => {
12 | let label
13 | if ('getChildren' in element) {
14 | label = element.getChildren()[0]
15 | }
16 | const coordinate = cfg.coordinate
17 | const startX = coordinate.start.x
18 | const finalX = label.attr('x')
19 | const labelContent = label.attr('text')
20 |
21 | label.attr('x', startX)
22 | label.attr('text', 0)
23 |
24 | const distance = finalX - startX
25 | label.animate((ratio) => {
26 | const position = startX + distance * ratio
27 | const text = (labelContent * ratio).toFixed(0)
28 |
29 | return {
30 | x: position,
31 | text
32 | }
33 | }, animateCfg)
34 | })
35 |
36 | registerAnimation('label-update', (element, animateCfg, cfg) => {
37 | const startX = element.attr('x')
38 | const startY = element.attr('y')
39 | // @ts-ignore
40 | const finalX = cfg.toAttrs.x
41 | // @ts-ignore
42 | const finalY = cfg.toAttrs.y
43 | const labelContent = element.attr('text')
44 | // @ts-ignore
45 | const finalContent = cfg.toAttrs.text
46 |
47 | const distanceX = finalX - startX
48 | const distanceY = finalY - startY
49 | const numberDiff = +finalContent - +labelContent
50 |
51 | element.animate((ratio) => {
52 | const positionX = startX + distanceX * ratio
53 | const positionY = startY + distanceY * ratio
54 | const text = (+labelContent + numberDiff * ratio).toFixed(0)
55 |
56 | return {
57 | x: positionX,
58 | y: positionY,
59 | text
60 | }
61 | }, animateCfg)
62 | })
63 |
64 | function handleData(source) {
65 | source.sort((a, b) => {
66 | return a.value - b.value
67 | })
68 |
69 | return source
70 | }
71 |
72 | let count = 0
73 | // let interval: any = undefined
74 | window['interval'] = undefined
75 |
76 | // if (interval) {
77 | // console.log('clear interval ')
78 | // clearInterval(interval)
79 | // }
80 |
81 | function countUp() {
82 | if (count === 0) {
83 | // @ts-ignore
84 | chart.data(handleData(Object.values(data)[count]))
85 | chart.coordinate('rect').transpose()
86 | chart.legend(false)
87 | chart.tooltip(false)
88 | // chart.axis('value', false);
89 | chart.axis('city', {
90 | animateOption: {
91 | update: {
92 | duration: 1000,
93 | easing: 'easeLinear'
94 | }
95 | }
96 | })
97 | chart.annotation().text({
98 | position: ['95%', '90%'],
99 | content: Object.keys(data)[count],
100 | style: {
101 | fontSize: 40,
102 | fontWeight: 'bold',
103 | fill: '#ddd',
104 | textAlign: 'end'
105 | },
106 | animate: false
107 | })
108 | chart
109 | .interval()
110 | .position('city*value')
111 | .color('city')
112 | .label('value', () => {
113 | // if (value !== 0) {
114 | return {
115 | animate: {
116 | appear: {
117 | animation: 'label-appear',
118 | delay: 0,
119 | duration: 1000,
120 | easing: 'easeLinear'
121 | },
122 | update: {
123 | animation: 'label-update',
124 | duration: 1000,
125 | easing: 'easeLinear'
126 | }
127 | },
128 | offset: 5
129 | }
130 | // }
131 | })
132 | .animate({
133 | appear: {
134 | duration: 1000,
135 | easing: 'easeLinear'
136 | },
137 | update: {
138 | duration: 1000,
139 | easing: 'easeLinear'
140 | }
141 | })
142 | chart.forceFit()
143 | chart.render()
144 | } else {
145 | chart.annotation().clear(true)
146 | chart.annotation().text({
147 | position: ['95%', '90%'],
148 | content: Object.keys(data)[count],
149 | style: {
150 | fontSize: 40,
151 | fontWeight: 'bold',
152 | fill: '#ddd',
153 | textAlign: 'end'
154 | },
155 | animate: false
156 | })
157 | // @ts-ignore
158 | chart.changeData(handleData(Object.values(data)[count]))
159 | }
160 |
161 | ++count
162 |
163 | if (count === Object.keys(data).length) {
164 | clearInterval(window['interval'])
165 | }
166 | }
167 |
168 | countUp()
169 | window['interval'] = setInterval(countUp, 1200)
170 | }
171 |
--------------------------------------------------------------------------------
/src/views/demo/dashboard/analysis/renderChart/renderLineChart.ts:
--------------------------------------------------------------------------------
1 | import type { Chart } from '@antv/g2'
2 | /**
3 | * 注意:不要用DataSet 目前DataSet打包后会访问会出现
4 | * vue-router.esm-bundler.js:3295 TypeError: Cannot read properties of undefined (reading 'Graph')
5 | * at greedy-fas.js:2
6 | * 的错误 : (
7 | */
8 | import DataSet from '@antv/data-set'
9 |
10 | export function renderLineChart(chart: Nullable, data: any) {
11 | if (!data) return
12 | const ds = new DataSet()
13 | chart?.scale({
14 | Deaths: {
15 | sync: true,
16 | nice: true
17 | },
18 | death: {
19 | sync: true,
20 | nice: true
21 | }
22 | })
23 |
24 | const dv1 = ds.createView().source(data)
25 | dv1.transform({
26 | type: 'map',
27 | callback: (row) => {
28 | if (typeof row.Deaths === 'string') {
29 | row.Deaths = row.Deaths.replace(',', '')
30 | }
31 | row.Deaths = parseInt(row.Deaths, 10)
32 | row.death = row.Deaths
33 | row.year = row.Year
34 | return row
35 | }
36 | })
37 | const view1 = chart?.createView()
38 | view1?.data(dv1.rows)
39 | view1?.axis('Year', {
40 | subTickLine: {
41 | count: 3,
42 | length: 3
43 | },
44 | tickLine: {
45 | length: 6
46 | }
47 | })
48 | view1?.axis('Deaths', {
49 | label: {
50 | formatter: (text) => {
51 | return text.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
52 | }
53 | }
54 | })
55 | view1?.line().position('Year*Deaths')
56 |
57 | const dv2 = ds.createView().source(dv1.rows)
58 | dv2.transform({
59 | type: 'regression',
60 | method: 'polynomial',
61 | fields: ['year', 'death'],
62 | bandwidth: 0.1,
63 | as: ['year', 'death']
64 | })
65 |
66 | const view2 = chart?.createView()
67 | view2?.axis(false)
68 | view2?.data(dv2.rows)
69 | view2
70 | ?.line()
71 | .position('year*death')
72 | .style({
73 | stroke: '#969696',
74 | lineDash: [3, 3]
75 | })
76 | .tooltip(false)
77 | view1?.annotation().text({
78 | content: '趋势线',
79 | position: ['1970', 2500],
80 | style: {
81 | fill: '#8c8c8c',
82 | fontSize: 14,
83 | fontWeight: 300
84 | },
85 | offsetY: -70
86 | })
87 | chart?.render()
88 | }
89 |
--------------------------------------------------------------------------------
/src/views/demo/dashboard/workspace/index.vue:
--------------------------------------------------------------------------------
1 |
54 |
55 |
56 |
57 |
58 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
73 | 早安! 落梅听风雪!
74 | 那日少年薄春衫,明月照银簪。
75 |
76 |
77 |
78 |
79 |
80 |
81 | 1/10
82 |
83 |
84 | 1/1
85 |
86 |
87 | 3/10
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | 更多
99 |
100 |
101 |
102 |
103 |
104 |
105 | {{ it.title }}
106 |
107 |
108 |
109 |
110 |
111 | {{ it.desc }}
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | {{ year }}-{{ month }}-{{ date }}
121 |
122 |
123 |
124 |
125 |
126 |
127 | 更多
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
--------------------------------------------------------------------------------
/src/views/demo/feature/keep-alive/index.vue:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
49 |
50 |
51 | {{ formValue }}
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/src/views/demo/form/BasicFormDemo.vue:
--------------------------------------------------------------------------------
1 |
235 |
236 |
237 |
238 |
239 |
240 |
249 |
250 | 获取model
251 | 改变值
252 | 重置
253 |
259 | 验证
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
--------------------------------------------------------------------------------
/src/views/demo/system/account/hidePage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | This is hidden Page
6 | Back
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/views/demo/system/account/index.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 | abcdefg account
15 | CLick
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/views/demo/system/menus/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | menus
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/views/demo/table/BasicTableDemo.vue:
--------------------------------------------------------------------------------
1 |
60 |
61 |
62 |
63 |
64 | 表头区域 slot
65 | 查询区域(设置openSearch=true后才显示哈)slot
66 | 标准表格
67 |
68 | 工具扩展区域 slot
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/src/views/demo/table/DefTableHead.vue:
--------------------------------------------------------------------------------
1 |
208 |
209 |
210 |
211 |
212 | 标准表格
213 |
214 |
215 |
216 | 我是自定义的
217 | 好巧,我也是
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
--------------------------------------------------------------------------------
/src/views/demo/table/SearchTableDemo.vue:
--------------------------------------------------------------------------------
1 |
207 |
208 |
209 |
210 |
211 | 标准表格
212 |
213 |
214 | {{ fetch.redo ? '禁用redo' : '启用redo' }}
215 | {{ fetch.redo ? '' : '(参数改变时会自动触发请求,慎用!)' }}
216 |
217 |
218 | 获取参数
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
--------------------------------------------------------------------------------
/src/views/demo/table/service.ts:
--------------------------------------------------------------------------------
1 | export enum TableDemoEnum {
2 | getTableRecords = '/demo/table/getTableRecords'
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/demo/top-level/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | TopLevel
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/views/demo/top-level/index2.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | TopLevel2
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/views/demo/utils-demo/composables/usePromiseFn.vue:
--------------------------------------------------------------------------------
1 |
43 |
44 |
45 |
46 |
47 | 需要注意的是: 只要设置了 redo为true
48 | 那么函数的参数可以为响应式的对象。在数据变化时,会自动再次请求。 usePromiseFn返回的 executor
49 | 是个执行器,可以手动触发请求
50 |
51 |
52 |
53 | FetchParams:
54 |
55 | {{ JSON.stringify(usePromiseFetchData, null, 2) }}
56 |
57 |
58 | Data: {{ JSON.stringify(promiseData) }}
59 | loading: {{ isLoading }}
60 | error: {{ JSON.stringify(promiseError) }}
61 | finished: {{ isFinished }}
62 |
63 | 手动触发
64 |
65 | 参数改变自动重发
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/src/views/demo/utils-demo/http/service.ts:
--------------------------------------------------------------------------------
1 | export enum HttpDemoEnums {
2 | getData = '/demo/use/api/getDemoData'
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/demo/utils-demo/http/topAwait.vue:
--------------------------------------------------------------------------------
1 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | top promise then
49 |
50 | {{ JSON.stringify(res, null, 2) }}
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/src/views/demo/utils-demo/http/useApiHooks.vue:
--------------------------------------------------------------------------------
1 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | 注意:改变参请后会马上触发请求,一般来说最好不要这么频繁。你懂的 : )
102 |
103 |
104 |
105 | 结果: => {{ JSON.stringify(data1, null, 2) }}
106 |
107 | 再次触发请求
108 |
109 |
110 |
111 |
112 |
113 | 结果: => {{ JSON.stringify(data2, null, 2) }}
114 |
115 | 手动换挡
116 |
117 |
118 |
119 |
120 |
121 |
--------------------------------------------------------------------------------
/src/views/error/403.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/views/error/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/views/error/500.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/views/fast-api/role/index.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 | role
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/views/login/index.vue:
--------------------------------------------------------------------------------
1 |
58 |
59 |
108 |
109 |
122 |
--------------------------------------------------------------------------------
/src/views/login/type.ts:
--------------------------------------------------------------------------------
1 | export interface FormState {
2 | username: string
3 | password: string
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "useDefineForClassFields": true,
5 | "module": "esnext",
6 | "moduleResolution": "node",
7 | "strict": true,
8 | "jsx": "preserve",
9 | "sourceMap": true,
10 | "resolveJsonModule": true,
11 | "esModuleInterop": true,
12 | "lib": ["esnext", "dom"],
13 | "noImplicitAny": false,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "noImplicitThis": true,
17 | "preserveValueImports": true,
18 | "forceConsistentCasingInFileNames": true,
19 | "paths": {
20 | "@/*": ["src/*"]
21 | },
22 | "strictFunctionTypes": false,
23 | "types": ["vite/client"],
24 | "typeRoots": ["./node_modules/@types/", "./types"],
25 | "instantiationDepthLimit": 100,
26 | "instantiationCountLimit": 10000000
27 | },
28 | "include": [
29 | "src/**/*.ts",
30 | "src/**/*.d.ts",
31 | "src/**/*.tsx",
32 | "src/**/*.vue",
33 | "types/**/*.d.ts",
34 | "vite.config.ts",
35 | "components.d.ts"
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/types/env.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import { DefineComponent } from 'vue'
3 | const Component: DefineComponent
4 | export default Component
5 | }
6 |
7 | import 'vue-router'
8 |
9 | declare module 'vue-router' {
10 | interface RouteMeta {
11 | title: string
12 | icon?: string
13 | /// 菜单是否显示 默认true
14 | hidden?: boolean
15 | /// 将所有的子级渲染为一级菜单 默认false
16 | hiddenChildren?: boolean
17 | /// 指定平级菜单的Name,用于渲染菜单的选中状态
18 | activeMenuName?: string
19 | /// keep-alive设置,如果路由需要设置keep-alive,则需要在对应的页面设置其页面组件的name属性方会生效。在script setup中
20 | /// 使用 defineOptions({ name: 'PageName' })来设置其name属性。注意: 缓存的是vue组件的name属性,而非vue-router的name属性
21 | keepAlive?: boolean
22 | }
23 | }
24 |
25 | declare interface ImportMetaEnv {
26 | VITE_FETCH_PREFIX_URL: string
27 | // 更多环境变量...
28 | }
29 |
--------------------------------------------------------------------------------
/types/global.d.ts:
--------------------------------------------------------------------------------
1 | declare interface Recordable {
2 | [key: string]: any
3 | }
4 |
5 | declare type Nullable = T | null
6 |
7 | declare type PromiseFn = (...args: T[]) => Promise
8 |
9 | declare type Writeable = { -readonly [P in keyof T]: T[P] }
10 |
--------------------------------------------------------------------------------
/types/window.d.ts:
--------------------------------------------------------------------------------
1 | import { MessageApiInjection } from 'naive-ui/lib/message/src/MessageProvider'
2 | import { NotificationApiInjection } from 'naive-ui/lib/notification/src/NotificationProvider'
3 | import { DialogApiInjection } from 'naive-ui/es/dialog/src/DialogProvider'
4 |
5 | declare global {
6 | interface Window {
7 | $message: MessageApiInjection
8 | $notification: NotificationApiInjection
9 | $dialog: DialogApiInjection
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import type { UserConfigExport } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 | import vueJsx from '@vitejs/plugin-vue-jsx'
4 | import path from 'path'
5 | import { viteMockServe } from 'vite-plugin-mock'
6 | import autoComponents from 'unplugin-vue-components/vite'
7 | import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
8 | import purgeIcons from 'vite-plugin-purge-icons'
9 | import eslintPlugin from '@nabla/vite-plugin-eslint'
10 | import legacy from '@vitejs/plugin-legacy'
11 | import defineOptions from 'unplugin-vue-define-options/vite'
12 | import unocss from 'unocss/vite'
13 |
14 | // https://vitejs.dev/config/
15 |
16 | function getPlugins(command: string) {
17 | return [
18 | vue(),
19 | unocss(),
20 | defineOptions(),
21 | vueJsx(),
22 | purgeIcons(),
23 | autoComponents({
24 | dirs: ['src/components', 'src/layouts'],
25 | resolvers: [NaiveUiResolver()],
26 | dts: true
27 | }),
28 | eslintPlugin({
29 | shouldLint: (path) => /\/src\/[^\?\r\n]*\.(vue|tsx?)$/.test(path),
30 | eslintOptions: {
31 | cache: false
32 | }
33 | }),
34 | viteMockServe({
35 | mockPath: 'src/mock',
36 | localEnabled: command === 'serve',
37 | prodEnabled: command !== 'serve',
38 | // 这样可以控制关闭mock的时候不让mock打包到最终代码内
39 | injectCode: `
40 | import { setupProdMockServer } from './mock/useMock'
41 | setupProdMockServer()
42 | `
43 | }),
44 | legacy({
45 | targets: ['defaults', 'not IE 11']
46 | })
47 | ]
48 | }
49 |
50 | export default ({ command }): UserConfigExport => ({
51 | plugins: getPlugins(command),
52 | resolve: {
53 | alias: [
54 | {
55 | find: '@',
56 | replacement: path.resolve(__dirname, './src/')
57 | }
58 | ]
59 | },
60 | server: {
61 | port: 8000,
62 | host: true,
63 | open: true
64 | },
65 | build: {
66 | sourcemap: false
67 | }
68 | })
69 |
--------------------------------------------------------------------------------