├── .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 | Pear Admin Naive Version 21 | 22 | 23 | Vue Version 24 | 25 | 26 | Naive UI Version 27 | 28 |
29 | 30 | Node Version 31 | 32 |

33 | 34 |
35 | 36 | 37 | 38 | 39 |
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 | [//]: # (| ![输入图片说明](https://images.gitee.com/uploads/images/2021/0505/223456_0ae4c5ef_4835367.png '屏幕截图.png') | ![输入图片说明](https://images.gitee.com/uploads/images/2021/0505/223516_74b7d454_4835367.png '屏幕截图.png') |) 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 | [![Giteye chart](https://chart.giteye.net/gitee/jobin_jia/pear-admin-naive-min/GVB5WBKG.png)](https://giteye.net/chart/GVB5WBKG) 126 | 127 | ### 贡献列表 128 | 129 | [![Giteye chart](https://chart.giteye.net/gitee/jobin_jia/pear-admin-naive-min/8EQS6NZQ.png)](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 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/components/AntVG2/G2Chart.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 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 | 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 | 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 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/Error/Error403.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | -------------------------------------------------------------------------------- /src/components/Error/Error404.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | -------------------------------------------------------------------------------- /src/components/Error/Error500.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 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 | 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 | 69 | 70 | 105 | -------------------------------------------------------------------------------- /src/components/Modal/PModal.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/PageWrapper/Breadcrumb/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 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 | 95 | 96 | 115 | -------------------------------------------------------------------------------- /src/components/Table/components/ColumnSetting.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 121 | 122 | 141 | -------------------------------------------------------------------------------- /src/components/Table/components/Reload.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/Table/components/ResizeHeight.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 43 | -------------------------------------------------------------------------------- /src/components/Table/components/SizeSetting.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/components/Table/components/TableTools.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 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 | 55 | 56 | 67 | -------------------------------------------------------------------------------- /src/layouts/content/RouteTabs.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 58 | 59 | 105 | -------------------------------------------------------------------------------- /src/layouts/content/TabRefresh.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /src/layouts/content/TabsAction.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 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 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /src/layouts/header/AppSetting.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /src/layouts/header/FullScreen.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | 23 | 33 | -------------------------------------------------------------------------------- /src/layouts/header/PearHeader.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /src/layouts/header/UserDropdown.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 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 | 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 | 13 | 14 | 36 | -------------------------------------------------------------------------------- /src/layouts/sider/PearSider.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 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 | 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 | 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 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /src/views/demo/feature/keep-alive/index.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/views/demo/form/BasicFormDemo.vue: -------------------------------------------------------------------------------- 1 | 235 | 236 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /src/views/demo/system/account/hidePage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/views/demo/system/account/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/views/demo/system/menus/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/demo/table/BasicTableDemo.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/views/demo/table/DefTableHead.vue: -------------------------------------------------------------------------------- 1 | 208 | 209 | 224 | 225 | 226 | -------------------------------------------------------------------------------- /src/views/demo/table/SearchTableDemo.vue: -------------------------------------------------------------------------------- 1 | 207 | 208 | 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 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/demo/top-level/index2.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/demo/utils-demo/composables/usePromiseFn.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 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 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/views/demo/utils-demo/http/useApiHooks.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/views/error/403.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/error/404.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/error/500.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/fast-api/role/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 58 | 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 | --------------------------------------------------------------------------------