├── .env.production ├── .env.development ├── src ├── styles │ ├── antd.less │ ├── variables.less │ ├── index.less │ ├── public.less │ ├── common.less │ └── transition.less ├── layouts │ ├── BlankLayout.vue │ └── BasicLayout │ │ ├── components │ │ ├── index.less │ │ ├── constant.ts │ │ ├── Header.vue │ │ ├── RightContent.vue │ │ └── SideMenu.tsx │ │ ├── utils │ │ ├── typings.ts │ │ └── index.ts │ │ └── index.vue ├── assets │ └── images │ │ ├── logo.png │ │ ├── avatar.png │ │ ├── login_bg.png │ │ ├── Icon_Block.png │ │ ├── Icon_node.png │ │ ├── Icon _Search.png │ │ ├── Icon_contract.png │ │ └── Icon_trading.png ├── api │ ├── global.d.ts │ ├── common │ │ ├── model.d.ts │ │ └── index.ts │ ├── user │ │ ├── model.d.ts │ │ └── index.ts │ ├── sys │ │ └── account │ │ │ ├── model.d.ts │ │ │ └── index.ts │ └── home │ │ ├── index.ts │ │ └── model.d.ts ├── shims-vue.d.ts ├── views │ ├── 404.vue │ ├── others │ │ ├── antdv │ │ │ └── index.vue │ │ └── about │ │ │ └── index.vue │ ├── redirect │ │ └── index.tsx │ ├── account │ │ ├── constant.ts │ │ └── index.vue │ ├── website │ │ ├── constant.tsx │ │ └── index.vue │ ├── home │ │ ├── constant.tsx │ │ ├── index.vue │ │ └── components │ │ │ ├── DataOverview.vue │ │ │ └── TradingHistory.vue │ ├── login │ │ ├── index.vue │ │ └── Form.vue │ └── table-demo │ │ ├── constant.tsx │ │ └── index.vue ├── store │ ├── index.ts │ └── modules │ │ ├── home.ts │ │ ├── sysAccount.ts │ │ ├── user.ts │ │ └── permission.ts ├── utils │ ├── auth.ts │ ├── dateUtil.ts │ ├── echarts.ts │ ├── crypto.ts │ ├── validate.ts │ ├── permission.ts │ ├── index.ts │ ├── is.ts │ ├── http.ts │ └── mitt.ts ├── directives │ ├── index.ts │ ├── permission.ts │ └── role.ts ├── router │ ├── index.ts │ ├── permission.ts │ └── router.config.ts ├── components │ ├── Alert │ │ └── index.vue │ ├── Modal │ │ └── index.vue │ ├── Breadcrumb │ │ └── index.vue │ ├── Icon │ │ └── index.vue │ ├── Upload │ │ └── index.vue │ ├── TableFilter │ │ └── index.vue │ └── Table │ │ └── index.vue ├── enums │ └── authEnum.ts ├── hooks │ ├── useRole.ts │ ├── useTitle.ts │ ├── usePermission.ts │ ├── useBreadcrumbTitle.ts │ ├── useTimeout.ts │ ├── useEventListener.ts │ ├── useECharts.ts │ └── useMessage.tsx ├── App.vue ├── main.ts └── plugin.ts ├── .vscode └── extensions.json ├── public └── favicon.ico ├── postcss.config.js ├── .env ├── .husky ├── commit-msg ├── pre-commit ├── common.sh └── lintstagedrc.js ├── .eslintignore ├── .npmrc ├── prettier.config.js ├── .prettierignore ├── config ├── vite │ ├── plugin │ │ ├── autoImport.ts │ │ ├── visualizer.ts │ │ ├── svgIcons.ts │ │ ├── compress.ts │ │ ├── mock.ts │ │ ├── index.ts │ │ ├── component.ts │ │ └── styleImport.ts │ ├── proxy.ts │ └── optimizer.ts ├── constant.ts └── themeConfig.ts ├── types ├── common.d.ts ├── index.d.ts └── global.d.ts ├── index.html ├── .gitignore ├── mock ├── _createProdMockServer.ts ├── common.ts ├── _util.ts ├── home.ts ├── user.ts └── table.ts ├── .github └── workflows │ └── deploy.yml ├── LICENSE ├── commitlint.config.js ├── tsconfig.json ├── CHANGELOG.md ├── vite.config.ts ├── .eslintrc.js ├── package.json └── README.md /.env.production: -------------------------------------------------------------------------------- 1 | VITE_APP_ENV = production -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | VITE_APP_ENV = dev 2 | VITE_APP_TITLE = 我是标题 -------------------------------------------------------------------------------- /src/styles/antd.less: -------------------------------------------------------------------------------- 1 | // 2 | 3 | // .ant-layout-content{ 4 | 5 | // } 6 | -------------------------------------------------------------------------------- /src/layouts/BlankLayout.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["johnsoncodehk.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JS-banana/vite-vue3-ts/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | VUE_APP_PREVIEW=false 3 | VUE_APP_API_BASE_URL=/api/hto-mp/v1 4 | PORT=8998 5 | -------------------------------------------------------------------------------- /src/styles/variables.less: -------------------------------------------------------------------------------- 1 | // 2 | 3 | @primary-color: '#3860F4'; 4 | 5 | @border-color-dark: #b6b7b9; 6 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JS-banana/vite-vue3-ts/HEAD/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JS-banana/vite-vue3-ts/HEAD/src/assets/images/avatar.png -------------------------------------------------------------------------------- /src/assets/images/login_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JS-banana/vite-vue3-ts/HEAD/src/assets/images/login_bg.png -------------------------------------------------------------------------------- /src/assets/images/Icon_Block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JS-banana/vite-vue3-ts/HEAD/src/assets/images/Icon_Block.png -------------------------------------------------------------------------------- /src/assets/images/Icon_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JS-banana/vite-vue3-ts/HEAD/src/assets/images/Icon_node.png -------------------------------------------------------------------------------- /src/assets/images/Icon _Search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JS-banana/vite-vue3-ts/HEAD/src/assets/images/Icon _Search.png -------------------------------------------------------------------------------- /src/assets/images/Icon_contract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JS-banana/vite-vue3-ts/HEAD/src/assets/images/Icon_contract.png -------------------------------------------------------------------------------- /src/assets/images/Icon_trading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JS-banana/vite-vue3-ts/HEAD/src/assets/images/Icon_trading.png -------------------------------------------------------------------------------- /src/api/global.d.ts: -------------------------------------------------------------------------------- 1 | // 接口返回 形状 2 | export interface ResData { 3 | code: number; 4 | message: string; 5 | result: T; 6 | } 7 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # shellcheck source=./_/husky.sh 4 | . "$(dirname "$0")/_/husky.sh" 5 | 6 | npx --no-install commitlint --edit "$1" 7 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import './transition.less'; 2 | @import './variables.less'; 3 | @import './common.less'; 4 | @import './public.less'; 5 | @import './antd.less'; 6 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { defineComponent } from 'vue'; 3 | const Component: ReturnType; 4 | export default Component; 5 | } 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | 2 | *.sh 3 | node_modules 4 | *.md 5 | *.woff 6 | *.ttf 7 | .vscode 8 | .idea 9 | dist 10 | /public 11 | /docs 12 | .husky 13 | .local 14 | /bin 15 | Dockerfile 16 | .npmrc -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | . "$(dirname "$0")/common.sh" 4 | 5 | [ -n "$CI" ] && exit 0 6 | 7 | # Format and submit code according to lintstagedrc.js configuration 8 | npm run lint:lint-staged 9 | -------------------------------------------------------------------------------- /.husky/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | command_exists () { 3 | command -v "$1" >/dev/null 2>&1 4 | } 5 | 6 | # Workaround for Windows 10, Git Bash and Yarn 7 | if command_exists winpty && test -t 1; then 8 | exec < /dev/tty 9 | fi 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # 提升一些依赖包至 node_modules 2 | # 解决部分包模块not found的问题 3 | # 用于配合 pnpm 4 | shamefully-hoist = true 5 | 6 | # node-sass 下载问题 7 | # sass_binary_site="https://npm.taobao.org/mirrors/node-sass/" 8 | 9 | # peers 10 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /src/views/404.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/views/others/antdv/index.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | semi: true, 4 | vueIndentScriptAndStyle: true, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | proseWrap: 'never', 8 | htmlWhitespaceSensitivity: 'strict', 9 | endOfLine: 'auto', 10 | }; 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | **/*.ico 3 | package.json 4 | /dist 5 | .DS_Store 6 | .eslintignore 7 | *.png 8 | *.toml 9 | .editorconfig 10 | .gitignore 11 | .prettierignore 12 | LICENSE 13 | .eslintcache 14 | *.lock 15 | yarn-error.log 16 | /public 17 | **/node_modules/** 18 | .npmrc -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | 3 | const store = createPinia(); 4 | 5 | export { store }; 6 | 7 | // 因为 pinia的实现也是通过vue的各种api(ref/reactive/computed等) 8 | // 所以,不要求一定要在Vue上挂载注册,可以随便在组件中使用,组件外使用也有对应方案 9 | // 不过,app.use(store) 可以把store实例挂载到Vue上使用 10 | -------------------------------------------------------------------------------- /src/api/common/model.d.ts: -------------------------------------------------------------------------------- 1 | export interface ReqParams { 2 | limit: number; 3 | page: number; 4 | } 5 | 6 | export interface ResResult { 7 | id: number; 8 | url: string; 9 | ip: string; 10 | protocol: string; 11 | host: number; 12 | domain: string; 13 | email: string; 14 | } 15 | -------------------------------------------------------------------------------- /config/vite/plugin/autoImport.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name AutoImportDeps 3 | * @description 按需加载,自动引入依赖 4 | */ 5 | import AutoImport from 'unplugin-auto-import/vite'; 6 | 7 | export const AutoImportDeps = () => 8 | AutoImport({ 9 | imports: ['vue', 'vue-router'], 10 | dts: 'src/auto-imports.d.ts', 11 | }); 12 | -------------------------------------------------------------------------------- /types/common.d.ts: -------------------------------------------------------------------------------- 1 | /*类型命名建议以Ty结尾*/ 2 | /* 3 | * 4 | 枚举 类,接口 都是大驼峰 WangMeng 5 | 方法,变量,常量 小驼峰 wangMeng 6 | * */ 7 | /*通用对象*/ 8 | interface ObjTy { 9 | [propName: string]: any; 10 | } 11 | 12 | /* ant select组件 options 参数类型 */ 13 | type IOptions = Array<{ 14 | label: string; 15 | value: T; 16 | }>; 17 | -------------------------------------------------------------------------------- /src/api/user/model.d.ts: -------------------------------------------------------------------------------- 1 | export interface ReqParams { 2 | mobile: 'string'; 3 | password: 'string'; 4 | } 5 | 6 | export interface ReqAuth { 7 | auths: string[]; 8 | modules: string[]; 9 | is_admin?: 0 | 1; 10 | } 11 | 12 | export interface ResResult { 13 | login_status: number; 14 | st: string; 15 | token: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | const TokenKey = 'x-auth-token'; 2 | 3 | export function getToken() { 4 | return localStorage.getItem(TokenKey) || ''; 5 | } 6 | 7 | export function setToken(token: string) { 8 | localStorage.setItem(TokenKey, token); 9 | } 10 | 11 | export function removeToken() { 12 | localStorage.setItem(TokenKey, ''); 13 | } 14 | -------------------------------------------------------------------------------- /.husky/lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'], 3 | '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'], 4 | 'package.json': ['prettier --write'], 5 | '*.vue': ['eslint --fix', 'prettier --write'], 6 | '*.{scss,less,styl,html}': ['prettier --write'], 7 | }; 8 | -------------------------------------------------------------------------------- /src/directives/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configure and register global directives 3 | */ 4 | import type { App } from 'vue'; 5 | import { setupPermissionDirective } from './permission'; 6 | import { setupRoleDirective } from './role'; 7 | 8 | export function setupGlobDirectives(app: App) { 9 | setupPermissionDirective(app); 10 | setupRoleDirective(app); 11 | } 12 | -------------------------------------------------------------------------------- /src/api/sys/account/model.d.ts: -------------------------------------------------------------------------------- 1 | export interface ReqAccount { 2 | id: number; 3 | account?: string; 4 | password?: string; 5 | } 6 | 7 | export interface ResAccount { 8 | account: string; 9 | last_login: string; 10 | mobile: string; 11 | role_name: string; 12 | true_name: string; 13 | user_id: number; 14 | } 15 | 16 | export type ResPermission = { auths: Array }; 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite-Vue3-Admin 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/api/home/index.ts: -------------------------------------------------------------------------------- 1 | import { ReqParams, ResInfoList, ResResult } from './model'; 2 | import { get } from '/@/utils/http'; 3 | 4 | enum URL { 5 | list = '/v1/home/list', 6 | info = '/v1/home/info', 7 | } 8 | 9 | const list = async (data: ReqParams) => get({ url: URL.list, data }); 10 | 11 | const info = async () => get({ url: URL.info }); 12 | 13 | export default { list, info }; 14 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router'; 2 | import routes from './router.config'; 3 | 4 | // app router 5 | export const router = createRouter({ 6 | // 解决 二级路径存在时,路径地址路由不匹配的问题 7 | // https://juejin.cn/post/7051826951463370760#heading-27 8 | history: createWebHistory(import.meta.env.BASE_URL), 9 | routes, 10 | strict: true, 11 | scrollBehavior: () => ({ left: 0, top: 0 }), 12 | }); 13 | -------------------------------------------------------------------------------- /src/api/user/index.ts: -------------------------------------------------------------------------------- 1 | import { ReqAuth, ReqParams, ResResult } from './model'; 2 | import { get, post } from '/@/utils/http'; 3 | 4 | enum URL { 5 | login = '/v1/user/login', 6 | permission = '/v1/user/permission', 7 | } 8 | 9 | const login = async (data: ReqParams) => post({ url: URL.login, data }); 10 | 11 | const permission = async () => get({ url: URL.permission }); 12 | 13 | export default { login, permission }; 14 | -------------------------------------------------------------------------------- /src/api/common/index.ts: -------------------------------------------------------------------------------- 1 | import { ReqParams, ResResult } from './model'; 2 | import { get } from '/@/utils/http'; 3 | 4 | enum URL { 5 | page_one_list = '/v1/common/page_one/list', 6 | list = '/v1/node/nodelist', 7 | } 8 | 9 | const page_one_list = async (data: ReqParams) => get({ url: URL.page_one_list, data }); 10 | 11 | const node_list = async (data: ReqParams) => get({ url: URL.list, data }); 12 | 13 | export default { page_one_list, node_list }; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | .cache 5 | 6 | package-lock.json 7 | yarn.lock 8 | 9 | .local 10 | # local env files 11 | .env.local 12 | .env.*.local 13 | .eslintcache 14 | 15 | # Log files 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | pnpm-debug.log* 20 | 21 | # Editor directories and files 22 | .idea 23 | # .vscode 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | **/src/auto-imports.d.ts 31 | **/src/components.d.ts 32 | -------------------------------------------------------------------------------- /config/vite/plugin/visualizer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Package file volume analysis 3 | */ 4 | import visualizer from 'rollup-plugin-visualizer'; 5 | import { ANALYSIS } from '../../constant'; 6 | 7 | export function configVisualizerConfig() { 8 | if (ANALYSIS) { 9 | return visualizer({ 10 | filename: './node_modules/.cache/visualizer/stats.html', 11 | open: true, 12 | gzipSize: true, 13 | brotliSize: true, 14 | }) as Plugin; 15 | } 16 | return []; 17 | } 18 | -------------------------------------------------------------------------------- /src/views/redirect/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue'; 2 | import { useRoute, useRouter } from 'vue-router'; 3 | import { onBeforeMount } from 'vue'; 4 | 5 | export default defineComponent({ 6 | setup() { 7 | const route = useRoute(); 8 | const router = useRouter(); 9 | onBeforeMount(() => { 10 | const { params, query } = route; 11 | const { path } = params; 12 | router.replace({ path: '/' + path, query }); 13 | }); 14 | return () => ''; 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /src/api/sys/account/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name account 3 | * @description 系统设置 - 账户模块 4 | */ 5 | 6 | import { ReqAccount, ResAccount } from './model'; 7 | import { get, post } from '/@/utils/http'; 8 | 9 | enum URL { 10 | update = '/v1/account/edit', 11 | account = '/v1/account/info', 12 | } 13 | 14 | const account = async () => get({ url: URL.account }); 15 | 16 | const update = async (data: ReqAccount) => post({ url: URL.update, data }); 17 | 18 | export default { account, update }; 19 | -------------------------------------------------------------------------------- /src/components/Alert/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 21 | -------------------------------------------------------------------------------- /src/views/account/constant.ts: -------------------------------------------------------------------------------- 1 | export const KeyValue = [ 2 | { 3 | label: '用户名 :', 4 | value: '', 5 | key: 'username', 6 | isEdit: true, 7 | }, 8 | { 9 | label: '用户角色 :', 10 | key: 'role_name', 11 | value: '', 12 | }, 13 | { 14 | label: '手机号 :', 15 | key: 'mobile', 16 | value: '', 17 | }, 18 | { 19 | label: '登录密码 :', 20 | value: '********', 21 | key: 'password', 22 | isEdit: true, 23 | }, 24 | { 25 | label: '上次登录 :', 26 | key: 'last_login', 27 | value: '', 28 | }, 29 | ]; 30 | -------------------------------------------------------------------------------- /mock/_createProdMockServer.ts: -------------------------------------------------------------------------------- 1 | import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'; 2 | 3 | const modules = import.meta.globEager('./**/*.ts'); 4 | 5 | const mockModules: any[] = []; 6 | Object.keys(modules).forEach((key) => { 7 | if (key.includes('/_')) { 8 | return; 9 | } 10 | mockModules.push(...modules[key].default); 11 | }); 12 | 13 | /** 14 | * Used in a production environment. Need to manually import all modules 15 | */ 16 | export function setupProdMockServer() { 17 | createProdMockServer(mockModules); 18 | } 19 | -------------------------------------------------------------------------------- /config/vite/plugin/svgIcons.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Vite Plugin for fast creating SVG sprites. 3 | * https://github.com/anncwb/vite-plugin-svg-icons 4 | */ 5 | 6 | import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'; 7 | import path from 'path'; 8 | 9 | export function configSvgIconsPlugin(isBuild: boolean) { 10 | const svgIconsPlugin = createSvgIconsPlugin({ 11 | iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')], 12 | svgoOptions: isBuild, 13 | // default 14 | symbolId: 'icon-[dir]-[name]', 15 | }); 16 | return svgIconsPlugin; 17 | } 18 | -------------------------------------------------------------------------------- /src/enums/authEnum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name AuthEnum 3 | * @description 权限,配合指令 v-auth 使用 4 | * @Example v-auth="AuthEnum.user_create" 5 | */ 6 | 7 | export enum AuthEnum { 8 | /** 9 | * 用户 10 | */ 11 | // 新增用户 12 | user_create = '/v1/user/create', 13 | // 编辑用户 14 | user_update = '/v1/user/update', 15 | // 删除用户 16 | user_delete = '/v1/user/delete', 17 | 18 | /** 19 | * 角色 20 | */ 21 | // 新增角色 22 | role_create = '/v1/role/create', 23 | // 修改角色 24 | role_update = '/v1/role/update', 25 | // 删除角色 26 | role_delete = '/v1/role/delete', 27 | } 28 | -------------------------------------------------------------------------------- /config/vite/plugin/compress.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated 3 | * https://github.com/anncwb/vite-plugin-compression 4 | */ 5 | import type { Plugin } from 'vite'; 6 | import compressPlugin from 'vite-plugin-compression'; 7 | import { COMPRESSION } from '../../constant'; 8 | 9 | export function configCompressPlugin(): Plugin | Plugin[] { 10 | if (COMPRESSION) { 11 | return compressPlugin({ 12 | ext: '.gz', 13 | deleteOriginFile: false, 14 | }) as Plugin; 15 | } 16 | return []; 17 | } 18 | -------------------------------------------------------------------------------- /src/hooks/useRole.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name useRole 3 | * @description 处理角色权限 4 | */ 5 | 6 | import { usePermissioStore } from '/@/store/modules/permission'; 7 | 8 | export function useRole() { 9 | const permissioStore = usePermissioStore(); 10 | 11 | function hasRole(value?: string | string[], def = true): boolean { 12 | if (value == null) { 13 | return def; 14 | } 15 | 16 | if (typeof value === 'boolean') { 17 | return value; 18 | } 19 | 20 | if (typeof value === 'number') { 21 | return permissioStore.getRole === value; 22 | } 23 | return def; 24 | } 25 | 26 | return { hasRole }; 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/useTitle.ts: -------------------------------------------------------------------------------- 1 | import { watch, unref } from 'vue'; 2 | import { useTitle as usePageTitle } from '@vueuse/core'; 3 | import { useRouter } from 'vue-router'; 4 | 5 | /** 6 | * Listening to page changes and dynamically changing site titles 7 | */ 8 | export function useTitle() { 9 | const { currentRoute } = useRouter(); 10 | 11 | const pageTitle = usePageTitle(); 12 | 13 | watch( 14 | [() => currentRoute.value.path], 15 | () => { 16 | const route = unref(currentRoute); 17 | 18 | const tTitle = route?.meta?.title as string; 19 | pageTitle.value = tTitle; 20 | }, 21 | { immediate: true }, 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/dateUtil.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Independent time operation tool to facilitate subsequent switch to dayjs 3 | */ 4 | // import moment from 'moment'; 5 | import dayjs from 'dayjs'; 6 | 7 | const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; 8 | const DATE_FORMAT = 'YYYY-MM-DD '; 9 | 10 | export function formatToDateTime( 11 | date: dayjs.ConfigType = undefined, 12 | format = DATE_TIME_FORMAT, 13 | ): string { 14 | return dayjs(date).format(format); 15 | } 16 | 17 | export function formatToDate(date: dayjs.ConfigType = undefined, format = DATE_FORMAT): string { 18 | return dayjs(date).format(format); 19 | } 20 | 21 | export const dateUtil = dayjs; 22 | -------------------------------------------------------------------------------- /mock/common.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs'; 2 | 3 | const list = Mock.mock({ 4 | 'items|60': [ 5 | { 6 | id: '@id', 7 | url: '@url', 8 | ip: '@ip', 9 | protocol: '@protocol', 10 | 'host|1': [80, 443], 11 | domain: '@domain', 12 | email: '@email', 13 | }, 14 | ], 15 | }); 16 | 17 | export default [ 18 | { 19 | url: '/v1/common/page_one/list', 20 | method: 'get', 21 | response: () => { 22 | const items = list.items; 23 | return { 24 | code: 0, 25 | result: { 26 | total: items.length, 27 | list: items, 28 | }, 29 | }; 30 | }, 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /src/api/home/model.d.ts: -------------------------------------------------------------------------------- 1 | export interface ReqParams { 2 | limit: number; 3 | page: number; 4 | } 5 | 6 | export interface ResResult { 7 | id: string; 8 | title: string; 9 | name: string; 10 | description: string; 11 | created_at: string; 12 | updated_at: string; 13 | age: number; 14 | color: string; 15 | email: string; 16 | } 17 | 18 | interface ResInfoListItem { 19 | id: number; 20 | num: number; 21 | time: string; 22 | } 23 | 24 | export interface ResInfoList { 25 | hu_num: number; 26 | yun_num: number; 27 | ce_num: number; 28 | create_time: number; 29 | online_num: number; 30 | total_num: number; 31 | seven_days: ResInfoListItem[]; 32 | } 33 | -------------------------------------------------------------------------------- /src/layouts/BasicLayout/components/index.less: -------------------------------------------------------------------------------- 1 | .my-sideMenu-sider { 2 | &_logo { 3 | margin: 16px 0; 4 | padding: 0 16px; 5 | height: 23px; 6 | overflow: hidden; 7 | } 8 | &_menu { 9 | flex: 1 1 0%; 10 | height: calc(100% - 55px); 11 | overflow: hidden auto; 12 | } 13 | &_footer { 14 | border-top: 1px solid #f0f0f0; 15 | width: 100%; 16 | position: absolute; 17 | left: 0; 18 | bottom: 0; 19 | height: 40px; 20 | padding: 0 16px; 21 | display: flex; 22 | align-items: center; 23 | .trigger:hover { 24 | color: #3860f4; 25 | } 26 | } 27 | .sideMenu-icon { 28 | vertical-align: 1px; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/echarts.ts: -------------------------------------------------------------------------------- 1 | // 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。 2 | import * as echarts from 'echarts/core'; 3 | // 引入柱状图图表,图表后缀都为 Chart 4 | import { LineChart } from 'echarts/charts'; 5 | // 引入提示框,标题,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component 6 | import { 7 | // TitleComponent, 8 | TooltipComponent, 9 | GridComponent, 10 | } from 'echarts/components'; 11 | // 标签自动布局,全局过渡动画等特性 12 | import { UniversalTransition } from 'echarts/features'; 13 | // 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步 14 | import { CanvasRenderer } from 'echarts/renderers'; 15 | 16 | // 注册必须的组件 17 | echarts.use([CanvasRenderer, LineChart, GridComponent, TooltipComponent, UniversalTransition]); 18 | 19 | export default echarts; 20 | -------------------------------------------------------------------------------- /src/layouts/BasicLayout/components/constant.ts: -------------------------------------------------------------------------------- 1 | export const navs = [ 2 | // { 3 | // icon: 'yonghuguanli', 4 | // name: '用户管理', 5 | // path: '/sys/user', 6 | // auth: 'user', 7 | // }, 8 | // { 9 | // icon: 'jiaoseguanli', 10 | // name: '角色管理', 11 | // path: '/sys/role', 12 | // auth: 'role', 13 | // }, 14 | // { 15 | // icon: 'xitongrizhi', 16 | // name: '系统日志', 17 | // path: '/sys/logs', 18 | // auth: 'log', 19 | // }, 20 | { 21 | icon: 'zhanghaozhongxin', 22 | name: '账号中心', 23 | path: '/sys/account', 24 | auth: '', 25 | }, 26 | { 27 | icon: 'tuichudenglu_huaban1fuben17', 28 | name: '退出登录', 29 | auth: '', 30 | }, 31 | ]; 32 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | -------------------------------------------------------------------------------- /config/vite/plugin/mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Mock plugin for development and production. 3 | * https://github.com/anncwb/vite-plugin-mock 4 | */ 5 | import { viteMockServe } from 'vite-plugin-mock'; 6 | 7 | export function configMockPlugin(isBuild: boolean) { 8 | return viteMockServe({ 9 | ignore: /^\_/, 10 | mockPath: 'mock', 11 | localEnabled: !isBuild, 12 | prodEnabled: isBuild, // 为了演示,线上开启 mock,实际开发请关闭,会影响打包体积 13 | // 开发环境无需关心 14 | // injectCode 只受prodEnabled影响 15 | // https://github.com/anncwb/vite-plugin-mock/issues/9 16 | // 下面这段代码会被注入 main.ts 17 | injectCode: ` 18 | import { setupProdMockServer } from '../mock/_createProdMockServer'; 19 | 20 | setupProdMockServer(); 21 | `, 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /config/vite/proxy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate proxy 3 | */ 4 | 5 | import { 6 | API_BASE_URL, 7 | API_TARGET_URL, 8 | MOCK_API_BASE_URL, 9 | MOCK_API_TARGET_URL, 10 | } from '../../config/constant'; 11 | import { ProxyOptions } from 'vite'; 12 | 13 | type ProxyTargetList = Record; 14 | 15 | const ret: ProxyTargetList = { 16 | // test 17 | [API_BASE_URL]: { 18 | target: API_TARGET_URL, 19 | changeOrigin: true, 20 | rewrite: (path) => path.replace(new RegExp(`^${API_BASE_URL}`), ''), 21 | }, 22 | // mock 23 | [MOCK_API_BASE_URL]: { 24 | target: MOCK_API_TARGET_URL, 25 | changeOrigin: true, 26 | rewrite: (path) => path.replace(new RegExp(`^${MOCK_API_BASE_URL}`), '/api'), 27 | }, 28 | }; 29 | 30 | export default ret; 31 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Fn { 2 | (...arg: T[]): R; 3 | } 4 | 5 | declare interface PromiseFn { 6 | (...arg: T[]): Promise; 7 | } 8 | 9 | declare type RefType = T | null; 10 | 11 | declare type LabelValueOptions = { 12 | label: string; 13 | value: any; 14 | [key: string]: string | number | boolean; 15 | }[]; 16 | 17 | declare type EmitType = (event: string, ...args: any[]) => void; 18 | 19 | declare type TargetContext = '_self' | '_blank'; 20 | 21 | declare interface ComponentElRef { 22 | $el: T; 23 | } 24 | 25 | declare type ComponentRef = ComponentElRef | null; 26 | 27 | declare type ElRef = Nullable; 28 | -------------------------------------------------------------------------------- /config/constant.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Config 3 | * @description 项目配置 4 | */ 5 | 6 | // 应用名 7 | export const APP_TITLE = 'Vite-Vue3-Admin'; 8 | 9 | // 本地服务端口 10 | export const VITE_PORT = 3000; 11 | 12 | // prefix 13 | export const API_PREFIX = '/api'; 14 | 15 | // serve 16 | export const API_BASE_URL = '/api'; 17 | export const API_TARGET_URL = 'http://localhost:3000'; 18 | 19 | // mock 20 | export const MOCK_API_BASE_URL = '/mock/api'; 21 | export const MOCK_API_TARGET_URL = 'http://localhost:3000'; 22 | 23 | // iconfontUrl 24 | export const ICONFONTURL = '//at.alicdn.com/t/font_3004192_9jmc1z9neiw.js'; // 去色版 25 | 26 | // 包依赖分析 27 | export const ANALYSIS = true; 28 | 29 | // 代码压缩 30 | export const COMPRESSION = true; 31 | 32 | // 删除 console 33 | export const VITE_DROP_CONSOLE = true; 34 | -------------------------------------------------------------------------------- /src/styles/public.less: -------------------------------------------------------------------------------- 1 | #app { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | // ================================= 7 | // ==============scrollbar========== 8 | // ================================= 9 | 10 | ::-webkit-scrollbar { 11 | width: 7px; 12 | height: 8px; 13 | } 14 | 15 | // ::-webkit-scrollbar-track { 16 | // background: transparent; 17 | // } 18 | 19 | ::-webkit-scrollbar-track { 20 | background-color: rgb(0 0 0 / 5%); 21 | } 22 | 23 | ::-webkit-scrollbar-thumb { 24 | // background: rgba(0, 0, 0, 0.6); 25 | background-color: rgb(144 147 153 / 30%); 26 | // background-color: rgba(144, 147, 153, 0.3); 27 | border-radius: 2px; 28 | box-shadow: inset 0 0 6px rgb(0 0 0 / 20%); 29 | } 30 | 31 | ::-webkit-scrollbar-thumb:hover { 32 | background-color: @border-color-dark; 33 | } 34 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | // import 'ant-design-vue/dist/antd.css'; 2 | import 'sanitize.css'; 3 | // import 'sanitize.css/forms.css'; 4 | // import 'sanitize.css/typography.css'; 5 | import '/@/styles/index.less'; 6 | 7 | import { createApp } from 'vue'; 8 | import App from './App.vue'; 9 | import { router } from './router'; 10 | import { store } from './store'; 11 | import { setupGlobDirectives } from './directives'; 12 | import './router/permission'; 13 | // import { setupComponents } from './plugin'; 14 | 15 | const app = createApp(App); 16 | 17 | app.use(store); 18 | 19 | app.use(router); 20 | 21 | // Register global directive 22 | setupGlobDirectives(app); 23 | 24 | // Register UI components 25 | // setupComponents(app); 26 | 27 | // 全局属性 28 | // app.config.globalProperties.AuthEnum = AuthEnum; 29 | 30 | app.mount('#app'); 31 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # 1. checkout 仓库 13 | - uses: actions/checkout@v3 14 | 15 | # 2. 设置pnpm包管理器 16 | - name: Install pnpm 17 | uses: pnpm/action-setup@v2 18 | with: 19 | version: 8 20 | 21 | # 3. 设置pnpm包管理器 22 | - uses: actions/setup-node@v3 23 | with: 24 | node-version: 16 25 | 26 | - name: build 27 | run: pnpm install --no-frozen-lockfile && pnpm run build:github # 部署至GitHub需要配置,base路径 28 | 29 | - name: Deploy 30 | uses: peaceiris/actions-gh-pages@v3 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: ./dist 34 | -------------------------------------------------------------------------------- /config/vite/optimizer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name configManualChunk 3 | * @description chunk 拆包优化 4 | */ 5 | 6 | const vendorLibs: { match: string[]; output: string }[] = [ 7 | { 8 | match: ['ant-design-vue'], 9 | output: 'antdv', 10 | }, 11 | { 12 | match: ['echarts'], 13 | output: 'echarts', 14 | }, 15 | ]; 16 | 17 | // pnpm安装的依赖,获取到的路径名称是拼接而成且比较长的 18 | // vite-vue3-ts/node_modules/.pnpm/registry.npmmirror.com+ant-design-vue@3.2.7_vue@3.2.23/node_modules/ant-design-vue/es/card/style/index.js 19 | export const configManualChunk = (id: string) => { 20 | if (/[\\/]node_modules[\\/]/.test(id)) { 21 | const matchItem = vendorLibs.find((item) => { 22 | const reg = new RegExp(`[\\/]node_modules[\\/]_?(${item.match.join('|')})(.*)`, 'ig'); 23 | return reg.test(id); 24 | }); 25 | return matchItem ? matchItem.output : null; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/directives/permission.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Global authority directive 3 | * Used for fine-grained control of component permissions 4 | * @Example v-auth="RoleEnum.TEST" 5 | */ 6 | import type { App, Directive, DirectiveBinding } from 'vue'; 7 | 8 | import { usePermission } from '/@/hooks/usePermission'; 9 | 10 | function isAuth(el: Element, binding: any) { 11 | const { hasPermission } = usePermission(); 12 | 13 | const value = binding.value; 14 | if (!value) return; 15 | if (!hasPermission(value)) { 16 | el.parentNode?.removeChild(el); 17 | } 18 | } 19 | 20 | const mounted = (el: Element, binding: DirectiveBinding) => { 21 | isAuth(el, binding); 22 | }; 23 | 24 | const authDirective: Directive = { 25 | mounted, 26 | }; 27 | 28 | export function setupPermissionDirective(app: App) { 29 | app.directive('auth', authDirective); 30 | } 31 | 32 | export default authDirective; 33 | -------------------------------------------------------------------------------- /src/hooks/usePermission.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name usePermission 3 | * @description 处理权限 4 | */ 5 | 6 | import intersection from 'lodash-es/intersection'; 7 | import { isArray } from '../utils/is'; 8 | import { usePermissioStore } from '/@/store/modules/permission'; 9 | 10 | export function usePermission() { 11 | const permissioStore = usePermissioStore(); 12 | 13 | function hasPermission(value?: string | string[], def = true): boolean { 14 | // Visible by default 15 | if (!value) { 16 | return def; 17 | } 18 | 19 | if (permissioStore.getIsAdmin === 1) { 20 | return true; 21 | } 22 | 23 | if (!isArray(value)) { 24 | return permissioStore.getAuths?.includes(value); 25 | } 26 | 27 | if (isArray(value)) { 28 | return intersection(value, permissioStore.getAuths).length > 0; 29 | } 30 | 31 | return true; 32 | } 33 | 34 | return { hasPermission }; 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/crypto.ts: -------------------------------------------------------------------------------- 1 | // // 加密 2 | // // 采用 DES 3 | 4 | // import CryptoJS from 'crypto-js'; 5 | 6 | // const DES_KEY = 'xxxxx'; // 定义的 key 7 | 8 | // const keyHex = CryptoJS.enc.Utf8.parse(DES_KEY); 9 | 10 | // //DES加密 11 | // export function encryptByDES(message: string) { 12 | // const encrypted = CryptoJS.DES.encrypt(message, keyHex, { 13 | // mode: CryptoJS.mode.ECB, 14 | // padding: CryptoJS.pad.ZeroPadding, 15 | // }); 16 | // return encrypted.ciphertext.toString(); 17 | // } 18 | 19 | // //DES加密 20 | // export function decryptByDES(ciphertext: string) { 21 | // const decrypted = CryptoJS.DES.decrypt( 22 | // { 23 | // ciphertext: CryptoJS.enc.Hex.parse(ciphertext), 24 | // }, 25 | // keyHex, 26 | // { 27 | // mode: CryptoJS.mode.ECB, 28 | // padding: CryptoJS.pad.ZeroPadding, 29 | // }, 30 | // ); 31 | // return decrypted.toString(CryptoJS.enc.Utf8); 32 | // } 33 | -------------------------------------------------------------------------------- /src/utils/validate.ts: -------------------------------------------------------------------------------- 1 | import { RuleObject } from 'ant-design-vue/es/form/interface'; 2 | 3 | const RegRules = { 4 | phone: /^1[3456789]\d{9}$/, 5 | password: /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,20}$/, 6 | pass: /^[a-zA-Z0-9_,.-]{6,20}$/, 7 | name: /^[a-z][a-z0-9]{1,17}$/, 8 | }; 9 | 10 | // 密码 11 | export function validatePassword(_: RuleObject, value: string) { 12 | if (value === '') { 13 | return Promise.reject('密码不能为空!'); 14 | } else if (!RegRules.pass.test(value)) { 15 | return Promise.reject('6~20位数字、字母、下划线等非特殊字符'); 16 | } else { 17 | return Promise.resolve(); 18 | } 19 | } 20 | 21 | // 电话 22 | export async function validatePhone(_: RuleObject, value: string) { 23 | if (!value) { 24 | return Promise.reject('手机号不能为空!'); 25 | } else if (!RegRules.phone.test(value)) { 26 | return Promise.reject('请输入正确的手机号!'); 27 | } else { 28 | return Promise.resolve(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/hooks/useBreadcrumbTitle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name useBreadcrumbTitle 3 | * @description 修改面包屑Title 4 | * @param isAddOn 是否为添加 on注册 5 | */ 6 | 7 | import mitt from '/@/utils/mitt'; 8 | import { onMounted, onUnmounted, ref } from 'vue'; 9 | import { useRoute } from 'vue-router'; 10 | 11 | const emitter = mitt(); 12 | 13 | const key = Symbol(); 14 | 15 | export const useBreadcrumbTitle = (isAddOn = true) => { 16 | const route = useRoute(); 17 | const title = ref(route.meta.title); 18 | 19 | watch( 20 | () => route.meta.title, 21 | (val) => { 22 | title.value = val; 23 | }, 24 | ); 25 | 26 | const changeTitle = (val: string) => (title.value = val); 27 | 28 | onMounted(() => isAddOn && emitter.on(key, changeTitle)); 29 | 30 | onUnmounted(() => isAddOn && emitter.off(key, changeTitle)); 31 | 32 | const setBreadcrumbTitle = (title: string) => emitter.emit(key, title); 33 | 34 | return { 35 | title, 36 | setBreadcrumbTitle, 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /src/views/website/constant.tsx: -------------------------------------------------------------------------------- 1 | import { Tag } from 'ant-design-vue'; 2 | import { ColumnProps } from 'ant-design-vue/es/table'; 3 | 4 | export const columns: ColumnProps[] = [ 5 | { 6 | title: 'IP地址', 7 | dataIndex: 'ip', 8 | width: 150, 9 | }, 10 | { 11 | title: '端口', 12 | dataIndex: 'host', 13 | width: 80, 14 | customRender: ({ text }) => {text}, 15 | }, 16 | { 17 | title: '协议', 18 | dataIndex: 'protocol', 19 | width: 100, 20 | }, 21 | { 22 | title: '域名', 23 | dataIndex: 'domain', 24 | width: 100, 25 | }, 26 | { 27 | title: '邮箱', 28 | dataIndex: 'email', 29 | width: 150, 30 | }, 31 | { 32 | title: '地址', 33 | dataIndex: 'url', 34 | width: 200, 35 | customRender: ({ text }) => ( 36 | 37 | {text} 38 | 39 | ), 40 | }, 41 | { 42 | title: '操作', 43 | key: 'action', 44 | width: 120, 45 | // slots: { customRender: 'action' }, // 该用法已废弃 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /src/store/modules/home.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { store } from '/@/store'; 3 | import fetchApi from '/@/api/home'; 4 | import { ResInfoList } from '/@/api/home/model'; 5 | 6 | interface HomeState { 7 | info: Nullable; 8 | } 9 | 10 | export const useHomeStore = defineStore({ 11 | id: 'app-home', 12 | state: (): HomeState => ({ 13 | // info 14 | info: null, 15 | }), 16 | getters: { 17 | getInfo(): Nullable { 18 | return this.info || null; 19 | }, 20 | }, 21 | actions: { 22 | setInfo(info: Nullable) { 23 | this.info = info; 24 | }, 25 | resetState() { 26 | this.info = null; 27 | }, 28 | /** 29 | * @description: login 30 | */ 31 | async fetchInfo() { 32 | const res = await fetchApi.info(); 33 | if (res) { 34 | // save token 35 | this.setInfo(res); 36 | } 37 | return res; 38 | }, 39 | }, 40 | }); 41 | 42 | // Need to be used outside the setup 43 | export function useHomeStoreWithOut() { 44 | return useHomeStore(store); 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present, JS-banana 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 | -------------------------------------------------------------------------------- /src/hooks/useTimeout.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch } from 'vue'; 2 | import { Fn, tryOnUnmounted } from '@vueuse/core'; 3 | import { isFunction } from '/@/utils/is'; 4 | 5 | export function useTimeoutFn(handle: Fn, wait: number, native = false) { 6 | if (!isFunction(handle)) { 7 | throw new Error('handle is not Function!'); 8 | } 9 | 10 | const { readyRef, stop, start } = useTimeoutRef(wait); 11 | if (native) { 12 | handle(); 13 | } else { 14 | watch( 15 | readyRef, 16 | (maturity) => { 17 | maturity && handle(); 18 | }, 19 | { immediate: false }, 20 | ); 21 | } 22 | return { readyRef, stop, start }; 23 | } 24 | 25 | export function useTimeoutRef(wait: number) { 26 | const readyRef = ref(false); 27 | 28 | let timer: TimeoutHandle; 29 | function stop(): void { 30 | readyRef.value = false; 31 | timer && window.clearTimeout(timer); 32 | } 33 | function start(): void { 34 | stop(); 35 | timer = setTimeout(() => { 36 | readyRef.value = true; 37 | }, wait); 38 | } 39 | 40 | start(); 41 | 42 | tryOnUnmounted(stop); 43 | 44 | return { readyRef, stop, start }; 45 | } 46 | -------------------------------------------------------------------------------- /src/views/home/constant.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip, Tag } from 'ant-design-vue'; 2 | import { ColumnProps } from 'ant-design-vue/es/table'; 3 | 4 | export const columns: ColumnProps[] = [ 5 | { 6 | title: '姓名', 7 | dataIndex: 'name', 8 | width: 100, 9 | }, 10 | { 11 | title: '年龄', 12 | dataIndex: 'age', 13 | width: 80, 14 | }, 15 | { 16 | title: '创建日期', 17 | dataIndex: 'created_at', 18 | // slots: { customRender: 'toDateTime' }, // 该用法已废弃 19 | key: 'toDateTime', 20 | width: 150, 21 | }, 22 | { 23 | title: '更新日期', 24 | dataIndex: 'updated_at', 25 | width: 170, 26 | }, 27 | { 28 | title: '邮箱', 29 | dataIndex: 'email', 30 | width: 150, 31 | }, 32 | { 33 | title: '描述', 34 | dataIndex: 'description', 35 | // width: 180, 36 | ellipsis: true, 37 | customRender: ({ text }) => ( 38 | 39 | {text} 40 | 41 | ), 42 | }, 43 | { 44 | title: '颜色', 45 | dataIndex: 'color', 46 | width: 100, 47 | customRender: ({ text }) => {text}, 48 | fixed: 'right', 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /config/themeConfig.ts: -------------------------------------------------------------------------------- 1 | import { getThemeVariables } from 'ant-design-vue/dist/theme'; 2 | 3 | // @primary-color: #1890ff; // 全局主色 4 | // @link-color: #1890ff; // 链接色 5 | // @success-color: #52c41a; // 成功色 6 | // @warning-color: #faad14; // 警告色 7 | // @error-color: #f5222d; // 错误色 8 | // @font-size-base: 14px; // 主字号 9 | // @heading-color: rgba(0, 0, 0, 0.85); // 标题色 10 | // @text-color: rgba(0, 0, 0, 0.65); // 主文本色 11 | // @text-color-secondary: rgba(0, 0, 0, 0.45); // 次文本色 12 | // @disabled-color: rgba(0, 0, 0, 0.25); // 失效色 13 | // @border-radius-base: 4px; // 组件/浮层圆角 14 | // @border-color-base: #d9d9d9; // 边框色 15 | // @box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.15); // 浮层阴影 16 | 17 | /** 18 | * less global variable 19 | */ 20 | export function generateModifyVars(dark = false) { 21 | const modifyVars = getThemeVariables({ dark }); 22 | return { 23 | ...modifyVars, 24 | // Used for global import to avoid the need to import each style file separately 25 | // reference: Avoid repeated references 26 | // hack: `${modifyVars.hack} @import (reference) "${resolve('src/design/config.less')}";`, 27 | 'primary-color': '#3860F4', 28 | 'link-color': '#3860F4', 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // feat:新增功能 2 | // fix:bug 修复 3 | // docs:文档更新 4 | // style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑) 5 | // refactor:重构代码(既没有新增功能,也没有修复 bug) 6 | // perf:性能, 体验优化 7 | // test:新增测试用例或是更新现有测试 8 | // build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交 9 | // ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交 10 | // chore:不属于以上类型的其他类,比如构建流程, 依赖管理 11 | // revert:回滚某个更早之前的提交 12 | 13 | module.exports = { 14 | ignores: [(commit) => commit.includes('init')], 15 | extends: ['@commitlint/config-conventional'], 16 | rules: { 17 | 'body-leading-blank': [2, 'always'], 18 | 'footer-leading-blank': [1, 'always'], 19 | 'header-max-length': [2, 'always', 108], 20 | 'subject-empty': [2, 'never'], 21 | 'type-empty': [2, 'never'], 22 | 'subject-case': [0], 23 | 'type-enum': [ 24 | 2, 25 | 'always', 26 | [ 27 | 'feat', 28 | 'fix', 29 | 'perf', 30 | 'style', 31 | 'docs', 32 | 'test', 33 | 'refactor', 34 | 'build', 35 | 'ci', 36 | 'chore', 37 | 'revert', 38 | 'wip', 39 | 'workflow', 40 | 'types', 41 | 'release', 42 | ], 43 | ], 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strictFunctionTypes": false, 10 | "jsx": "preserve", 11 | "baseUrl": ".", 12 | "allowJs": true, 13 | "sourceMap": true, 14 | "esModuleInterop": true, 15 | "resolveJsonModule": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "experimentalDecorators": true, 19 | "lib": ["dom", "esnext"], 20 | "types": ["vite/client"], 21 | "typeRoots": ["./node_modules/@types/", "./types"], 22 | "noImplicitAny": false, 23 | "skipLibCheck": true, 24 | "paths": { 25 | "/@/*": ["src/*"], 26 | "/#/*": ["types/*"] 27 | } 28 | }, 29 | "include": [ 30 | "src/**/*.ts", 31 | "src/**/*.d.ts", 32 | "src/**/*.tsx", 33 | "src/**/*.vue", 34 | "types/**/*.d.ts", 35 | "types/**/*.ts", 36 | "config/**/*.ts", 37 | "config/**/*.d.ts", 38 | "mock/**/*.ts", 39 | "vite.config.ts" 40 | ], 41 | "exclude": ["node_modules", "tests/server/**/*.ts", "dist", "**/*.js"] 42 | } 43 | -------------------------------------------------------------------------------- /src/layouts/BasicLayout/utils/typings.ts: -------------------------------------------------------------------------------- 1 | import type { VNode } from 'vue'; 2 | 3 | export type WithFalse = T | false; 4 | 5 | export type TargetType = '_blank' | '_self' | unknown; 6 | 7 | export interface MetaRecord { 8 | /** 9 | * @name 菜单的icon 10 | */ 11 | icon?: string | VNode; 12 | /** 13 | * @type 有 children 的菜单的组件类型 可选值 'group' 14 | */ 15 | type?: string; 16 | /** 17 | * @name 自定义菜单的国际化 key,如果没有则返回自身 18 | */ 19 | title?: string; 20 | /** 21 | * @name 内建授权信息 22 | */ 23 | authority?: string | string[]; 24 | /** 25 | * @name 打开目标位置 '_blank' | '_self' | null | undefined 26 | */ 27 | target?: TargetType; 28 | /** 29 | * @name 在菜单中隐藏子节点 30 | */ 31 | hideChildInMenu?: boolean; 32 | /** 33 | * @name 在菜单中隐藏自己和子节点 34 | */ 35 | hideInMenu?: boolean; 36 | /** 37 | * @name disable 菜单选项 38 | */ 39 | disabled?: boolean; 40 | /** 41 | * @name 隐藏自己,并且将子节点提升到与自己平级 42 | */ 43 | flatMenu?: boolean; 44 | 45 | [key: string]: any; 46 | } 47 | 48 | export interface MenuDataItem { 49 | /** 50 | * @name 用于标定选中的值,默认是 path 51 | */ 52 | path: string; 53 | name?: string | symbol; 54 | meta?: MetaRecord; 55 | /** 56 | * @name 子菜单 57 | */ 58 | children?: MenuDataItem[]; 59 | } 60 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 18 | 44 | -------------------------------------------------------------------------------- /src/components/Modal/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 50 | -------------------------------------------------------------------------------- /src/layouts/BasicLayout/components/Header.vue: -------------------------------------------------------------------------------- 1 | 15 | 20 | 51 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 42 | 47 | -------------------------------------------------------------------------------- /src/router/permission.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name permission 3 | * @description 全局路由过滤、权限过滤 4 | */ 5 | 6 | import { router } from '.'; 7 | import { getToken } from '../utils/auth'; 8 | import { usePermissioStoreWithOut } from '/@/store/modules/permission'; 9 | 10 | const permissioStore = usePermissioStoreWithOut(); 11 | const whiteList = ['/login']; // no redirect whitelist 12 | 13 | router.beforeEach(async (to: any, _, next) => { 14 | const hasToken = getToken(); 15 | if (hasToken) { 16 | // 已登录 17 | if (to.path === '/login') { 18 | next({ path: '/' }); 19 | } else { 20 | //是否获取过用户信息 21 | const isGetUserInfo = permissioStore.getIsGetUserInfo; 22 | if (isGetUserInfo) { 23 | next(); 24 | } else { 25 | // 没有获取,请求数据 26 | await permissioStore.fetchAuths(); 27 | // 过滤权限路由 28 | const routes = await permissioStore.buildRoutesAction(); 29 | // 404 路由一定要放在 权限路由后面 30 | routes.forEach((route) => { 31 | router.addRoute(route); 32 | }); 33 | // hack 方法 34 | // 不使用 next() 是因为,在执行完 router.addRoute 后, 35 | // 原本的路由表内还没有添加进去的路由,会 No match 36 | // replace 使路由从新进入一遍,进行匹配即可 37 | next({ ...to, replace: true }); 38 | } 39 | } 40 | } else { 41 | // 未登录 42 | if (whiteList.indexOf(to.path) !== -1) { 43 | next(); 44 | } else { 45 | next('/login'); 46 | } 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | // import type { ComponentPublicInstance, FunctionalComponent } from 'vue'; 2 | declare global { 3 | export type Writable = { 4 | -readonly [P in keyof T]: T[P]; 5 | }; 6 | 7 | namespace JSX { 8 | // tslint:disable no-empty-interface 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | interface IntrinsicAttributes { 13 | [elem: string]: any; 14 | } 15 | } 16 | } 17 | 18 | // declare module 'vue' { 19 | // export type JSXComponent = 20 | // | { new (): ComponentPublicInstance } 21 | // | FunctionalComponent; 22 | // } 23 | 24 | interface ImportMetaEnv extends ViteEnv { 25 | __: unknown; 26 | } 27 | 28 | declare interface ViteEnv { 29 | API_BASE_URL: string; 30 | } 31 | 32 | declare type Nullable = T | null; 33 | declare type NonNullable = T extends null | undefined ? never : T; 34 | declare type Recordable = Record; 35 | declare type ReadonlyRecordable = { 36 | readonly [key: string]: T; 37 | }; 38 | declare type Indexable = { 39 | [key: string]: T; 40 | }; 41 | declare type DeepPartial = { 42 | [P in keyof T]?: DeepPartial; 43 | }; 44 | declare type TimeoutHandle = ReturnType; 45 | declare type IntervalHandle = ReturnType; 46 | 47 | declare interface ChangeEvent extends Event { 48 | target: HTMLInputElement; 49 | } 50 | -------------------------------------------------------------------------------- /config/vite/plugin/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import vueJsx from '@vitejs/plugin-vue-jsx'; 4 | // import legacy from '@vitejs/plugin-legacy'; 5 | import { configStyleImportPlugin } from './styleImport'; 6 | import { configSvgIconsPlugin } from './svgIcons'; 7 | import { autoRegistryComponents } from './component'; 8 | import { AutoImportDeps } from './autoImport'; 9 | import { configMockPlugin } from './mock'; 10 | import { configVisualizerConfig } from './visualizer'; 11 | import { configCompressPlugin } from './compress'; 12 | 13 | export function createVitePlugins(isBuild: boolean) { 14 | const vitePlugins: (Plugin | Plugin[])[] = [ 15 | // vue支持 16 | vue(), 17 | // JSX支持 18 | vueJsx(), 19 | // 自动按需引入组件 20 | autoRegistryComponents(), 21 | // 自动按需引入依赖 22 | AutoImportDeps(), 23 | ]; 24 | 25 | // @vitejs/plugin-legacy 26 | // isBuild && vitePlugins.push(legacy()); 27 | 28 | // rollup-plugin-gzip 29 | isBuild && vitePlugins.push(configCompressPlugin()); 30 | 31 | // vite-plugin-svg-icons 32 | vitePlugins.push(configSvgIconsPlugin(isBuild)); 33 | 34 | // vite-plugin-mock 35 | vitePlugins.push(configMockPlugin(isBuild)); 36 | 37 | // rollup-plugin-visualizer 38 | vitePlugins.push(configVisualizerConfig()); 39 | 40 | // vite-plugin-style-import 41 | vitePlugins.push(configStyleImportPlugin(isBuild)); 42 | 43 | return vitePlugins; 44 | } 45 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 手动引入组件注册 3 | * 如果在意unplugin-vue-components插件的自动引入性能问题,可以考虑该方式 4 | */ 5 | import { 6 | Alert, 7 | Avatar, 8 | Breadcrumb, 9 | Button, 10 | Card, 11 | Col, 12 | DatePicker, 13 | Divider, 14 | Dropdown, 15 | Form, 16 | Input, 17 | Layout, 18 | Menu, 19 | Popconfirm, 20 | Row, 21 | Select, 22 | Space, 23 | Spin, 24 | Table as AntdTable, 25 | } from 'ant-design-vue'; 26 | 27 | import type { App } from 'vue'; 28 | 29 | import Icon from '/@/components/Icon/index.vue'; 30 | import Modal from '/@/components/Modal/index.vue'; 31 | import Table from '/@/components/Table/index.vue'; 32 | import TableFilter from '/@/components/TableFilter/index.vue'; 33 | import Upload from '/@/components/Upload/index.vue'; 34 | 35 | export function setupComponents(app: App) { 36 | app.component('Icon', Icon); 37 | app.component('Modal', Modal); 38 | app.component('Table', Table); 39 | app.component('TableFilter', TableFilter); 40 | app.component('Upload', Upload); 41 | 42 | app 43 | .use(Alert) 44 | .use(Avatar) 45 | .use(Breadcrumb) 46 | .use(Button) 47 | .use(Card) 48 | .use(Col) 49 | .use(DatePicker) 50 | .use(Divider) 51 | .use(Dropdown) 52 | .use(Form) 53 | .use(Input) 54 | .use(Layout) 55 | .use(Menu) 56 | .use(Popconfirm) 57 | .use(Row) 58 | .use(Select) 59 | .use(Space) 60 | .use(Spin) 61 | .use(AntdTable); 62 | } 63 | -------------------------------------------------------------------------------- /src/components/Icon/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 48 | 55 | -------------------------------------------------------------------------------- /src/directives/role.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Global authority directive 3 | * 角色控制: 银行 0 / 监管 1 4 | * @Example v-role="role" 5 | */ 6 | import type { App, Directive, DirectiveBinding } from 'vue'; 7 | import { useRole } from '/@/hooks/useRole'; 8 | import intersection from 'lodash-es/intersection'; 9 | 10 | // 操作按钮无权限时,替换展示内容 11 | function replaceHtml(parentNode: HTMLElement | null) { 12 | if (!parentNode) return; 13 | 14 | const child = document.createElement('span'); 15 | // 只过滤 Table里的操作按钮 16 | const classNames = ['ant-space-item', 'ant-table-row-cell-break-word']; 17 | const parentNodeText = 18 | intersection(classNames, parentNode?.className?.split(' ')).length > 0 ? '——' : ''; 19 | // console.dir(parentNode); 20 | child.innerHTML = parentNodeText; 21 | child.style.color = 'rgba(0,0,0,.08)'; 22 | parentNode?.appendChild(child); 23 | } 24 | 25 | function isAuth(el: Element, binding: any) { 26 | const { hasRole } = useRole(); 27 | const value = binding.value; 28 | // 过滤 undefined、null 29 | if (value == null) return; 30 | // 权限验证 31 | 32 | if (!hasRole(value)) { 33 | const parentNode = el.parentNode; 34 | el.parentNode?.removeChild(el); 35 | replaceHtml(parentNode as any); 36 | } 37 | } 38 | 39 | const mounted = (el: Element, binding: DirectiveBinding) => { 40 | isAuth(el, binding); 41 | }; 42 | 43 | const authDirective: Directive = { 44 | mounted, 45 | }; 46 | 47 | export function setupRoleDirective(app: App) { 48 | app.directive('role', authDirective); 49 | } 50 | 51 | export default authDirective; 52 | -------------------------------------------------------------------------------- /mock/_util.ts: -------------------------------------------------------------------------------- 1 | // Interface data format used to return a unified format 2 | 3 | export function resultSuccess(result: T, { message = 'ok' } = {}) { 4 | return { 5 | code: 0, 6 | result, 7 | message, 8 | type: 'success', 9 | }; 10 | } 11 | 12 | export function resultPageSuccess( 13 | page: number, 14 | pageSize: number, 15 | list: T[], 16 | { message = 'ok' } = {}, 17 | ) { 18 | const pageData = pagination(page, pageSize, list); 19 | 20 | return { 21 | ...resultSuccess({ 22 | items: pageData, 23 | total: list.length, 24 | }), 25 | message, 26 | }; 27 | } 28 | 29 | export function resultError(message = 'Request failed', { code = -1, result = null } = {}) { 30 | return { 31 | code, 32 | result, 33 | message, 34 | type: 'error', 35 | }; 36 | } 37 | 38 | export function pagination(pageNo: number, pageSize: number, array: T[]): T[] { 39 | const offset = (pageNo - 1) * Number(pageSize); 40 | const ret = 41 | offset + Number(pageSize) >= array.length 42 | ? array.slice(offset, array.length) 43 | : array.slice(offset, offset + Number(pageSize)); 44 | return ret; 45 | } 46 | 47 | export interface requestParams { 48 | method: string; 49 | body: any; 50 | headers?: { authorization?: string }; 51 | query: any; 52 | } 53 | 54 | /** 55 | * @description 本函数用于从request数据中获取token,请根据项目的实际情况修改 56 | * 57 | */ 58 | export function getRequestToken({ headers }: requestParams): string | undefined { 59 | return headers?.authorization; 60 | } 61 | -------------------------------------------------------------------------------- /src/views/table-demo/constant.tsx: -------------------------------------------------------------------------------- 1 | import { ColumnProps } from 'ant-design-vue/es/table'; 2 | import { Tag, Tooltip, Space } from 'ant-design-vue'; 3 | import { QuestionCircleOutlined } from '@ant-design/icons-vue'; 4 | 5 | export const columns: ColumnProps[] = [ 6 | { 7 | title: '序号', 8 | dataIndex: 'index', 9 | key: 'toIndex', 10 | width: 80, 11 | }, 12 | { 13 | title: '节点', 14 | dataIndex: 'node_name', 15 | width: 150, 16 | }, 17 | { 18 | title: ( 19 | 20 | 21 | 机构 22 | 23 | 24 | 25 | ), 26 | dataIndex: 'institutions_name', 27 | width: 80, 28 | }, 29 | { 30 | title: 'IP', 31 | dataIndex: 'ip', 32 | width: 100, 33 | customRender: ({ record }: any) => ( 34 | {record.ip ? `${record.ip}:${record.port}` : ''} 35 | ), 36 | }, 37 | { 38 | title: '角色', 39 | dataIndex: 'nodeRole', 40 | width: 100, 41 | }, 42 | { 43 | title: '是否管理员', 44 | dataIndex: 'is_consensus', 45 | width: 150, 46 | }, 47 | { 48 | title: '创建时间', 49 | dataIndex: 'create_time', 50 | width: 150, 51 | }, 52 | { 53 | title: '状态', 54 | dataIndex: 'status', 55 | width: 150, 56 | customRender: ({ text }) => {text}, 57 | }, 58 | { 59 | title: '操作', 60 | key: 'action', 61 | width: 120, 62 | // slots: { customRender: 'action' }, 63 | }, 64 | ]; 65 | -------------------------------------------------------------------------------- /src/layouts/BasicLayout/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecord, RouteRecordRaw } from 'vue-router'; 2 | 3 | type IRouteRecordRaw = RouteRecordRaw & { childrenPaths?: string[] }; 4 | 5 | // 过滤路由属性 hideInMenu hideChildInMenu 6 | export function clearMenuItem(menusData: RouteRecord[] | RouteRecordRaw[]): RouteRecordRaw[] { 7 | const filterHideMenus = menusData 8 | .map((item: RouteRecord | RouteRecordRaw) => { 9 | const finalItem = { ...item }; 10 | if (!finalItem.name || finalItem.meta?.hideInMenu) { 11 | return null; 12 | } 13 | 14 | if (finalItem && finalItem?.children) { 15 | if ( 16 | !finalItem.meta?.hideChildInMenu && 17 | finalItem.children.some( 18 | (child: RouteRecord | RouteRecordRaw) => child && child.name && !child.meta?.hideInMenu, 19 | ) 20 | ) { 21 | return { 22 | ...item, 23 | children: clearMenuItem(finalItem.children), 24 | }; 25 | } 26 | delete finalItem.children; 27 | } 28 | return finalItem; 29 | }) 30 | .filter((item) => item) as IRouteRecordRaw[]; 31 | 32 | // 33 | 34 | return filterHideMenus; 35 | } 36 | 37 | // 存在二级菜单时,过滤掉重复的并在一级菜单显示的 item 38 | export const filterRoutes = (menusData: RouteRecordRaw[]): RouteRecordRaw[] => { 39 | const filterRoutes: string[] = []; 40 | menusData.forEach((n) => { 41 | if (n.children) { 42 | n.children.forEach(({ path }) => filterRoutes.push(path)); 43 | } 44 | }); 45 | 46 | return menusData.filter(({ path }) => !filterRoutes.includes(path)); 47 | }; 48 | -------------------------------------------------------------------------------- /config/vite/plugin/component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name autoRegistryComponents 3 | * @description 按需加载,自动引入组件 4 | */ 5 | import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'; 6 | import Components from 'unplugin-vue-components/vite'; 7 | 8 | export const autoRegistryComponents = () => { 9 | return Components({ 10 | // relative paths to the directory to search for components. 11 | dirs: ['src/components'], 12 | 13 | // valid file extensions for components. 14 | extensions: ['vue'], 15 | // search for subdirectories 16 | deep: true, 17 | // resolvers for custom components 18 | resolvers: [AntDesignVueResolver({ importStyle: 'less' })], 19 | 20 | // generate `components.d.ts` global declarations, 21 | // also accepts a path for custom filename 22 | // dts: false, 23 | dts: 'src/components.d.ts', 24 | 25 | // Allow subdirectories as namespace prefix for components. 26 | directoryAsNamespace: false, 27 | // Subdirectory paths for ignoring namespace prefixes 28 | // works when `directoryAsNamespace: true` 29 | globalNamespaces: [], 30 | 31 | // auto import for directives 32 | // default: `true` for Vue 3, `false` for Vue 2 33 | // Babel is needed to do the transformation for Vue 2, it's disabled by default for performance concerns. 34 | // To install Babel, run: `npm install -D @babel/parser @babel/traverse` 35 | directives: true, 36 | 37 | // filters for transforming targets 38 | include: [/\.vue$/, /\.vue\?vue/], 39 | exclude: [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/, /[\\/]\.nuxt[\\/]/], 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /src/styles/common.less: -------------------------------------------------------------------------------- 1 | /* 2 | flex布局 第一个字母为主轴 3 | */ 4 | .rowSC { 5 | display: flex; 6 | flex-direction: row; 7 | justify-content: flex-start; 8 | align-items: center; 9 | } 10 | .rowCC { 11 | display: flex; 12 | flex-direction: row; 13 | justify-content: center; 14 | align-items: center; 15 | } 16 | .rowBC { 17 | display: flex; 18 | flex-direction: row; 19 | justify-content: space-between; 20 | align-items: center; 21 | } 22 | .rowE { 23 | display: flex !important; 24 | justify-content: flex-end; 25 | } 26 | .marT13 { 27 | margin-top: 13px; 28 | } 29 | .marT20 { 30 | margin-bottom: 20px; 31 | } 32 | .marL10 { 33 | margin-left: 10px; 34 | } 35 | .link { 36 | cursor: pointer; 37 | } 38 | 39 | /* 40 | * 字体 41 | */ 42 | .font18 { 43 | font-size: 18px; 44 | font-weight: 600; 45 | line-height: 28px; 46 | color: #000000; 47 | } 48 | 49 | .font16 { 50 | font-size: 16px; 51 | font-weight: 600; 52 | line-height: 22px; 53 | color: rgba(0, 0, 0, 0.85); 54 | } 55 | 56 | .font14 { 57 | font-size: 14px; 58 | font-weight: 500; 59 | line-height: 22px; 60 | color: #000000; 61 | } 62 | .font14_blue { 63 | font-family: Segoe UI; 64 | font-size: 14px; 65 | font-weight: bold; 66 | color: #3860f4; 67 | } 68 | 69 | .color-666 { 70 | font-weight: 300; 71 | line-height: 20px; 72 | color: #666666; 73 | } 74 | 75 | .text-center { 76 | text-align: center; 77 | } 78 | .text-left { 79 | text-align: left; 80 | } 81 | .text-right { 82 | text-align: right; 83 | } 84 | .line { 85 | margin: 0 !important; 86 | height: 24px !important; 87 | border: none !important; 88 | } 89 | -------------------------------------------------------------------------------- /src/store/modules/sysAccount.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name sysAccount 3 | * @description 系统设置-账户模块 4 | */ 5 | import { defineStore } from 'pinia'; 6 | import { store } from '/@/store'; 7 | import fetchApi from '/@/api/sys/account'; 8 | import { ReqAccount, ResAccount } from '/@/api/sys/account/model'; 9 | // import { encryptByDES } from '/@/utils/crypto'; 10 | 11 | type AccountInfoTy = ResAccount | null; 12 | 13 | interface UserState { 14 | info: AccountInfoTy; 15 | } 16 | 17 | export const useSysAccountStore = defineStore({ 18 | id: 'sys-account', 19 | state: (): UserState => ({ 20 | info: null, 21 | }), 22 | getters: { 23 | getAccount(): AccountInfoTy { 24 | return this.info; 25 | }, 26 | }, 27 | actions: { 28 | setAccount(info: AccountInfoTy) { 29 | this.info = info; 30 | }, 31 | resetState() { 32 | this.info = null; 33 | }, 34 | 35 | /** 36 | * @description: fetchRole 37 | */ 38 | async fetchAccount() { 39 | const res = await fetchApi.account(); 40 | if (res) { 41 | this.setAccount(res); 42 | } 43 | return res !== undefined; 44 | }, 45 | 46 | /** 47 | * @description: fetchAccountUpdate 48 | */ 49 | async fetchAccountUpdate(params: ReqAccount) { 50 | // if (params.password) { 51 | // params.password = encryptByDES(params.password); 52 | // } 53 | const res = await fetchApi.update(params); 54 | return res !== undefined; 55 | }, 56 | }, 57 | }); 58 | 59 | // Need to be used outside the setup 60 | export function useSysAccountStoreWithOut() { 61 | return useSysAccountStore(store); 62 | } 63 | -------------------------------------------------------------------------------- /src/styles/transition.less: -------------------------------------------------------------------------------- 1 | // fade 2 | .fade-enter-active, 3 | .fade-leave-active { 4 | transition: opacity 0.2s ease-in-out; 5 | } 6 | 7 | .fade-enter-from, 8 | .fade-leave-to { 9 | opacity: 0; 10 | } 11 | 12 | /* fade-slide */ 13 | .fade-slide-leave-active, 14 | .fade-slide-enter-active { 15 | transition: all 0.3s; 16 | } 17 | 18 | .fade-slide-enter-from { 19 | opacity: 0; 20 | transform: translateX(-30px); 21 | } 22 | 23 | .fade-slide-leave-to { 24 | opacity: 0; 25 | transform: translateX(30px); 26 | } 27 | 28 | // /////////////////////////////////////////////// 29 | // Fade Bottom 30 | // /////////////////////////////////////////////// 31 | 32 | // Speed: 1x 33 | .fade-bottom-enter-active, 34 | .fade-bottom-leave-active { 35 | transition: opacity 0.25s, transform 0.3s; 36 | } 37 | 38 | .fade-bottom-enter-from { 39 | opacity: 0; 40 | transform: translateY(-10%); 41 | } 42 | 43 | .fade-bottom-leave-to { 44 | opacity: 0; 45 | transform: translateY(10%); 46 | } 47 | 48 | // fade-scale 49 | .fade-scale-leave-active, 50 | .fade-scale-enter-active { 51 | transition: all 0.28s; 52 | } 53 | 54 | .fade-scale-enter-from { 55 | opacity: 0; 56 | transform: scale(1.2); 57 | } 58 | 59 | .fade-scale-leave-to { 60 | opacity: 0; 61 | transform: scale(0.8); 62 | } 63 | 64 | // /////////////////////////////////////////////// 65 | // Fade Top 66 | // /////////////////////////////////////////////// 67 | 68 | // Speed: 1x 69 | .fade-top-enter-active, 70 | .fade-top-leave-active { 71 | transition: opacity 0.2s, transform 0.25s; 72 | } 73 | 74 | .fade-top-enter-from { 75 | opacity: 0; 76 | transform: translateY(8%); 77 | } 78 | 79 | .fade-top-leave-to { 80 | opacity: 0; 81 | transform: translateY(-8%); 82 | } 83 | -------------------------------------------------------------------------------- /mock/home.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs'; 2 | import { resultSuccess } from './_util'; 3 | 4 | const list = Mock.mock({ 5 | 'items|30': [ 6 | { 7 | id: '@id', 8 | title: '@ctitle', 9 | mobile: '@phone', 10 | name: '@cname', 11 | description: '@cparagraph', 12 | created_at: '@datetime', 13 | updated_at: '@datetime', 14 | age: '@natural(10,50)', 15 | color: '@color', 16 | email: '@email', 17 | }, 18 | ], 19 | }); 20 | 21 | const data = { 22 | hu_num: 42, 23 | yun_num: 87755, 24 | ce_num: 3, 25 | create_time: 1636352741, 26 | online_num: 101, 27 | total_num: 110, 28 | seven_days: [ 29 | { 30 | id: 9, 31 | num: 7, 32 | time: '20211130', 33 | }, 34 | { 35 | id: 8, 36 | num: 80, 37 | time: '20211129', 38 | }, 39 | { 40 | id: 0, 41 | num: 280, 42 | time: '20211128', 43 | }, 44 | { 45 | id: 0, 46 | num: 0, 47 | time: '20211127', 48 | }, 49 | { 50 | id: 7, 51 | num: 5, 52 | time: '20211126', 53 | }, 54 | { 55 | id: 6, 56 | num: 20, 57 | time: '20211125', 58 | }, 59 | { 60 | id: 5, 61 | num: 5, 62 | time: '20211124', 63 | }, 64 | ], 65 | }; 66 | 67 | export default [ 68 | { 69 | url: '/v1/home/info', 70 | method: 'get', 71 | response: () => { 72 | return resultSuccess(data); 73 | }, 74 | }, 75 | { 76 | url: '/v1/home/list', 77 | method: 'get', 78 | response: () => { 79 | const items = list.items; 80 | return { 81 | code: 0, 82 | result: { 83 | total: items.length, 84 | list: items, 85 | }, 86 | }; 87 | }, 88 | }, 89 | ]; 90 | -------------------------------------------------------------------------------- /src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { store } from '/@/store'; 3 | import { ReqParams } from '/@/api/user/model'; 4 | import fetchApi from '/@/api/user'; 5 | // import { encryptByDES } from '/@/utils/crypto'; 6 | import { getToken, setToken, removeToken } from '/@/utils/auth'; 7 | import { router } from '/@/router'; 8 | 9 | interface UserState { 10 | token: string; 11 | auths: string[]; 12 | } 13 | 14 | export const useUserStore = defineStore({ 15 | id: 'app-user', 16 | state: (): UserState => ({ 17 | // token 18 | token: '', 19 | // auths 20 | auths: [], 21 | }), 22 | getters: { 23 | getToken(): string { 24 | return this.token || getToken(); 25 | }, 26 | }, 27 | actions: { 28 | setToken(info: string) { 29 | this.token = info ?? ''; // for null or undefined value 30 | setToken(info); 31 | }, 32 | setAuth(auths: string[]) { 33 | this.auths = auths; 34 | }, 35 | resetState() { 36 | this.token = ''; 37 | this.auths = []; 38 | }, 39 | /** 40 | * @description: login 41 | */ 42 | async login(params: ReqParams) { 43 | // 密码加密 44 | // params.password = encryptByDES(params.password); 45 | const res = await fetchApi.login(params); 46 | if (res) { 47 | // save token 48 | this.setToken(res.token); 49 | } 50 | return res; 51 | }, 52 | 53 | /** 54 | * @description: logout 55 | */ 56 | async logout() { 57 | this.resetState(); 58 | removeToken(); 59 | router.replace('/login'); 60 | // 路由表重置 61 | location.reload(); 62 | }, 63 | }, 64 | }); 65 | 66 | // Need to be used outside the setup 67 | export function useUserStoreWithOut() { 68 | return useUserStore(store); 69 | } 70 | -------------------------------------------------------------------------------- /src/hooks/useEventListener.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue'; 2 | import { ref, watch, unref } from 'vue'; 3 | import { useThrottleFn, useDebounceFn } from '@vueuse/core'; 4 | 5 | export type RemoveEventFn = () => void; 6 | export interface UseEventParams { 7 | el?: Element | Ref | Window | any; 8 | name: string; 9 | listener: EventListener; 10 | options?: boolean | AddEventListenerOptions; 11 | autoRemove?: boolean; 12 | isDebounce?: boolean; 13 | wait?: number; 14 | } 15 | export function useEventListener({ 16 | el = window, 17 | name, 18 | listener, 19 | options, 20 | autoRemove = true, 21 | isDebounce = true, 22 | wait = 80, 23 | }: UseEventParams): { removeEvent: RemoveEventFn } { 24 | /* eslint-disable-next-line */ 25 | let remove: RemoveEventFn = () => {}; 26 | const isAddRef = ref(false); 27 | 28 | if (el) { 29 | const element = ref(el as Element) as Ref; 30 | 31 | const handler = isDebounce ? useDebounceFn(listener, wait) : useThrottleFn(listener, wait); 32 | const realHandler = wait ? handler : listener; 33 | const removeEventListener = (e: Element) => { 34 | isAddRef.value = true; 35 | e.removeEventListener(name, realHandler, options); 36 | }; 37 | const addEventListener = (e: Element) => e.addEventListener(name, realHandler, options); 38 | 39 | const removeWatch = watch( 40 | element, 41 | (v, _ov, cleanUp) => { 42 | if (v) { 43 | !unref(isAddRef) && addEventListener(v); 44 | cleanUp(() => { 45 | autoRemove && removeEventListener(v); 46 | }); 47 | } 48 | }, 49 | { immediate: true }, 50 | ); 51 | 52 | remove = () => { 53 | removeEventListener(element.value); 54 | removeWatch(); 55 | }; 56 | } 57 | return { removeEvent: remove }; 58 | } 59 | -------------------------------------------------------------------------------- /src/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 54 | 61 | -------------------------------------------------------------------------------- /src/utils/permission.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name permission 3 | * @description 路由处理工具 4 | */ 5 | 6 | import { RouteRecordRaw } from 'vue-router'; 7 | import intersection from 'lodash-es/intersection'; 8 | 9 | const Reg_Module = /^\/v1\/(.+)\/.+$/; 10 | 11 | // 根据 auths 过滤module 12 | export const filterModuleByAuths = (auths: string[]): string[] => { 13 | // const reg=new RegExp(Reg_Module) 14 | return auths.filter(Boolean).map((auth) => { 15 | auth.match(Reg_Module); 16 | const moduleName = RegExp.$1; 17 | // console.log(authMatch, moduleName); 18 | return moduleName; 19 | }); 20 | }; 21 | 22 | // 不需要权限过滤的 白名单 23 | export const WhiteList = ['/v1/user/login', '/v1/user/permission', '/v1/account/info']; 24 | 25 | type IAuth = { auth?: string[]; role?: number }; 26 | 27 | export const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]): RouteRecordRaw[] => { 28 | const res: RouteRecordRaw[] = []; 29 | routes.forEach((route) => { 30 | const { auth } = (route.meta as IAuth) || {}; 31 | if (!auth) { 32 | if (route.children) { 33 | route.children = filterAsyncRoutes(route.children, roles); 34 | } 35 | res.push(route); 36 | } else { 37 | if (intersection(roles, auth).length > 0) { 38 | if (route.children) { 39 | route.children = filterAsyncRoutes(route.children, roles); 40 | } 41 | res.push(route); 42 | } 43 | } 44 | }); 45 | return res; 46 | }; 47 | 48 | export const filterRouteByRole = (routes: RouteRecordRaw[], ROLE: number) => { 49 | const filterChildrenByRole = (currentRoutes: RouteRecordRaw[]): RouteRecordRaw[] => { 50 | const result: RouteRecordRaw[] = []; 51 | 52 | currentRoutes.forEach((route) => { 53 | const { role } = (route.meta as IAuth) || {}; 54 | 55 | if (role == undefined || role == ROLE) { 56 | if (route.children) { 57 | route.children = filterChildrenByRole(route.children); 58 | } 59 | result.push(route); 60 | } 61 | }); 62 | 63 | return result; 64 | }; 65 | 66 | return filterChildrenByRole(routes); 67 | }; 68 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.0.0](https://github.com/js-banana/vite-vue3-ts/compare/v1.1.0...v1.0.0) (2022-08-09) 2 | 3 | # [1.0.0](https://github.com/js-banana/vite-vue3-ts/compare/v1.1.0...v1.0.0) (2022-08-09) 4 | 5 | # [1.0.0](https://github.com/js-banana/vite-vue3-ts/compare/v1.1.0...v1.0.0) (2022-08-09) 6 | 7 | # [1.0.0](https://github.com/js-banana/vite-vue3-ts/compare/v1.1.0...v1.0.0) (2022-08-09) 8 | 9 | # [1.0.0](https://github.com/js-banana/vite-vue3-ts/compare/v1.1.0...v1.0.0) (2022-08-09) 10 | 11 | ### Bug Fixes 12 | 13 | - nav ([a66af47](https://github.com/js-banana/vite-vue3-ts/commit/a66af4704c00fac48fcd6d30f1fa05f3c883aad9)) 14 | - **router:** 域名二级目录的路由配置优化 ([9b8c887](https://github.com/js-banana/vite-vue3-ts/commit/9b8c8876d61ebbdda7b16cc10aa19083517eceb2)) 15 | - **SideMenu:** 修复菜单文本与图标居中 ([60bafa0](https://github.com/js-banana/vite-vue3-ts/commit/60bafa0711b2f44df76f2979399ac95998576d67)) 16 | 17 | ### Features 18 | 19 | - **.env:** 增加环境变量配置文件 ([403029c](https://github.com/js-banana/vite-vue3-ts/commit/403029cb0ad703f4ea464a81876987a64b570f37)) 20 | - 调整权限逻辑,补充 v-role 指令 ([9a9598b](https://github.com/js-banana/vite-vue3-ts/commit/9a9598b2bb85a5c8baf5a08c56efd0e308514b96)) 21 | - 添加路由动效,抽离 Breadcrumb 组件 ([d32087c](https://github.com/js-banana/vite-vue3-ts/commit/d32087c9f9490f6245589fce42342b19e3068b5e)) 22 | - 增加 Table 使用 demo,完善文档说明,优化 Table API,保持与官方 antv 一致 ([159e0da](https://github.com/js-banana/vite-vue3-ts/commit/159e0da34c2897d4d2a3433ac793cfa3c4521cf2)) 23 | - vite2.x => vite3.x 工具链生态相关升级更新 ([820c02e](https://github.com/js-banana/vite-vue3-ts/commit/820c02e0f1eec256bf4fc9884cb25f2631fa803e)) 24 | 25 | ### Performance Improvements 26 | 27 | - 路由模式由 hash 调整为 history ([e37f2f6](https://github.com/js-banana/vite-vue3-ts/commit/e37f2f60291241ec4ab30134afd46b0dd83815a8)) 28 | 29 | ## [0.0.1](https://github.com/js-banana/vite-vue3-ts/compare/219fe493bd2623c0abfab0e4ef48a2a12838ccdf...v0.0.1) (2021-12-13) 30 | 31 | ### Features 32 | 33 | - **app:** 生产环境 mock、功能组件、路由完善 ([74b1983](https://github.com/js-banana/vite-vue3-ts/commit/74b1983c7a946b2fb1a95afcd3870a13db96fa9b)) 34 | - **mock:** mock 数据编写 ([219fe49](https://github.com/js-banana/vite-vue3-ts/commit/219fe493bd2623c0abfab0e4ef48a2a12838ccdf)) 35 | - update app and docs ([b61d9ce](https://github.com/js-banana/vite-vue3-ts/commit/b61d9cea26c522850eca74cb0833d5efd90c52c1)) 36 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { UserConfig, ConfigEnv } from 'vite'; 2 | import { resolve } from 'path'; 3 | import { createVitePlugins } from './config/vite/plugin'; 4 | import proxy from './config/vite/proxy'; 5 | import { VITE_DROP_CONSOLE, VITE_PORT } from './config/constant'; 6 | import { generateModifyVars } from './config/themeConfig'; 7 | import { configManualChunk } from './config/vite/optimizer'; 8 | 9 | function pathResolve(dir: string) { 10 | return resolve(process.cwd(), '.', dir); 11 | } 12 | 13 | // https://vitejs.dev/config/ 14 | export default ({ command, mode }: ConfigEnv): UserConfig => { 15 | const isBuild = command === 'build'; 16 | console.log(command, mode); 17 | 18 | return { 19 | resolve: { 20 | alias: [ 21 | // /@/xxxx => src/xxxx 22 | { find: /^~/, replacement: resolve(__dirname, '') }, 23 | { 24 | find: /\/@\//, 25 | replacement: pathResolve('src') + '/', 26 | }, 27 | ], 28 | }, 29 | 30 | // plugins 31 | plugins: createVitePlugins(isBuild), 32 | 33 | css: { 34 | preprocessorOptions: { 35 | less: { 36 | modifyVars: generateModifyVars(), 37 | javascriptEnabled: true, 38 | }, 39 | }, 40 | }, 41 | 42 | // server 43 | server: { 44 | hmr: { overlay: false }, // 禁用或配置 HMR 连接 设置 server.hmr.overlay 为 false 可以禁用服务器错误遮罩层 45 | // 服务配置 46 | port: VITE_PORT, // 类型: number 指定服务器端口; 47 | open: false, // 类型: boolean | string在服务器启动时自动在浏览器中打开应用程序; 48 | cors: false, // 类型: boolean | CorsOptions 为开发服务器配置 CORS。默认启用并允许任何源 49 | // host: '0.0.0.0', // IP配置,支持从IP启动 50 | proxy, 51 | }, 52 | 53 | // build 54 | build: { 55 | target: 'es2015', 56 | terserOptions: { 57 | compress: { 58 | keep_infinity: true, 59 | drop_console: VITE_DROP_CONSOLE, 60 | }, 61 | }, 62 | rollupOptions: { 63 | output: { 64 | manualChunks: configManualChunk, 65 | }, 66 | }, 67 | // Turning off brotliSize display can slightly reduce packaging time 68 | reportCompressedSize: false, 69 | chunkSizeWarningLimit: 2000, 70 | }, 71 | 72 | //optimizeDeps 73 | // optimizeDeps: { 74 | // include: ['ant-design-vue/es/locale/zh_CN', 'moment/dist/locale/zh-cn'], 75 | // }, 76 | }; 77 | }; 78 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { defineConfig } = require('eslint-define-config'); 3 | module.exports = defineConfig({ 4 | root: true, 5 | env: { 6 | browser: true, 7 | node: true, 8 | es6: true, 9 | }, 10 | parser: 'vue-eslint-parser', 11 | parserOptions: { 12 | parser: '@typescript-eslint/parser', 13 | ecmaVersion: 2020, 14 | sourceType: 'module', 15 | jsxPragma: 'React', 16 | ecmaFeatures: { 17 | jsx: true, 18 | }, 19 | }, 20 | extends: [ 21 | 'plugin:vue/vue3-recommended', 22 | 'plugin:@typescript-eslint/recommended', 23 | 'prettier', 24 | 'plugin:prettier/recommended', 25 | 'plugin:jest/recommended', 26 | ], 27 | rules: { 28 | 'vue/script-setup-uses-vars': 'error', 29 | '@typescript-eslint/ban-ts-ignore': 'off', 30 | '@typescript-eslint/explicit-function-return-type': 'off', 31 | '@typescript-eslint/no-explicit-any': 'off', 32 | '@typescript-eslint/no-var-requires': 'off', 33 | '@typescript-eslint/no-empty-function': 'off', 34 | 'vue/custom-event-name-casing': 'off', 35 | 'no-use-before-define': 'off', 36 | '@typescript-eslint/no-use-before-define': 'off', 37 | '@typescript-eslint/ban-ts-comment': 'off', 38 | '@typescript-eslint/ban-types': 'off', 39 | '@typescript-eslint/no-non-null-assertion': 'off', 40 | '@typescript-eslint/explicit-module-boundary-types': 'off', 41 | '@typescript-eslint/no-unused-vars': [ 42 | 'error', 43 | { 44 | argsIgnorePattern: '^_', 45 | varsIgnorePattern: '^_', 46 | }, 47 | ], 48 | 'no-unused-vars': [ 49 | 'error', 50 | { 51 | argsIgnorePattern: '^_', 52 | varsIgnorePattern: '^_', 53 | }, 54 | ], 55 | 'space-before-function-paren': 'off', 56 | 57 | 'vue/attributes-order': 'off', 58 | 'vue/one-component-per-file': 'off', 59 | 'vue/html-closing-bracket-newline': 'off', 60 | 'vue/max-attributes-per-line': 'off', 61 | 'vue/multiline-html-element-content-newline': 'off', 62 | 'vue/singleline-html-element-content-newline': 'off', 63 | 'vue/attribute-hyphenation': 'off', 64 | 'vue/require-default-prop': 'off', 65 | 'vue/html-self-closing': [ 66 | 'error', 67 | { 68 | html: { 69 | void: 'always', 70 | normal: 'never', 71 | component: 'always', 72 | }, 73 | svg: 'always', 74 | math: 'always', 75 | }, 76 | ], 77 | }, 78 | }); 79 | -------------------------------------------------------------------------------- /config/vite/plugin/styleImport.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Introduces component library styles on demand. 3 | * https://github.com/anncwb/vite-plugin-style-import 4 | */ 5 | import { createStyleImportPlugin } from 'vite-plugin-style-import'; 6 | 7 | export function configStyleImportPlugin(isBuild: boolean) { 8 | if (!isBuild) { 9 | return []; 10 | } 11 | const styleImportPlugin = createStyleImportPlugin({ 12 | libs: [ 13 | { 14 | libraryName: 'ant-design-vue', 15 | esModule: true, 16 | resolveStyle: (name) => { 17 | // 这里是无需额外引入样式文件的“子组件”列表 18 | const ignoreList = [ 19 | 'anchor-link', 20 | 'sub-menu', 21 | 'menu-item', 22 | 'menu-item-group', 23 | 'breadcrumb-item', 24 | 'breadcrumb-separator', 25 | 'form-item', 26 | 'step', 27 | 'select-option', 28 | 'select-opt-group', 29 | 'card-grid', 30 | 'card-meta', 31 | 'collapse-panel', 32 | 'descriptions-item', 33 | 'list-item', 34 | 'list-item-meta', 35 | 'table-column', 36 | 'table-column-group', 37 | 'tab-pane', 38 | 'tab-content', 39 | 'timeline-item', 40 | 'tree-node', 41 | 'skeleton-input', 42 | 'skeleton-avatar', 43 | 'skeleton-title', 44 | 'skeleton-paragraph', 45 | 'skeleton-image', 46 | 'skeleton-button', 47 | ]; 48 | // 这里是需要额外引入样式的子组件列表 49 | // 单独引入子组件时需引入组件样式,否则会在打包后导致子组件样式丢失 50 | const replaceList = { 51 | 'typography-text': 'typography', 52 | 'typography-title': 'typography', 53 | 'typography-paragraph': 'typography', 54 | 'typography-link': 'typography', 55 | 'dropdown-button': 'dropdown', 56 | 'input-password': 'input', 57 | 'input-search': 'input', 58 | 'input-group': 'input', 59 | 'radio-group': 'radio', 60 | 'checkbox-group': 'checkbox', 61 | 'layout-sider': 'layout', 62 | 'layout-content': 'layout', 63 | 'layout-footer': 'layout', 64 | 'layout-header': 'layout', 65 | 'month-picker': 'date-picker', 66 | }; 67 | 68 | return ignoreList.includes(name) 69 | ? '' 70 | : replaceList.hasOwnProperty(name) 71 | ? `ant-design-vue/es/${replaceList[name]}/style/index` 72 | : `ant-design-vue/es/${name}/style/index`; 73 | }, 74 | }, 75 | ], 76 | }); 77 | return styleImportPlugin; 78 | } 79 | -------------------------------------------------------------------------------- /src/layouts/BasicLayout/components/RightContent.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 56 | 57 | 89 | -------------------------------------------------------------------------------- /src/components/Upload/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 97 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalized, RouteRecordNormalized } from 'vue-router'; 2 | import type { App, Plugin } from 'vue'; 3 | 4 | import { unref } from 'vue'; 5 | import { isObject } from '/@/utils/is'; 6 | 7 | export const noop = () => {}; 8 | 9 | /** 10 | * @description: Set ui mount node 11 | */ 12 | export function getPopupContainer(node?: HTMLElement): HTMLElement { 13 | return (node?.parentNode as HTMLElement) ?? document.body; 14 | } 15 | 16 | /** 17 | * Add the object as a parameter to the URL 18 | * @param baseUrl url 19 | * @param obj 20 | * @returns {string} 21 | * eg: 22 | * let obj = {a: '3', b: '4'} 23 | * setObjToUrlParams('www.baidu.com', obj) 24 | * ==>www.baidu.com?a=3&b=4 25 | */ 26 | export function setObjToUrlParams(baseUrl: string, obj: any): string { 27 | let parameters = ''; 28 | for (const key in obj) { 29 | parameters += key + '=' + encodeURIComponent(obj[key]) + '&'; 30 | } 31 | parameters = parameters.replace(/&$/, ''); 32 | return /\?$/.test(baseUrl) ? baseUrl + parameters : baseUrl.replace(/\/?$/, '?') + parameters; 33 | } 34 | 35 | export function deepMerge(src: any = {}, target: any = {}): T { 36 | let key: string; 37 | for (key in target) { 38 | src[key] = isObject(src[key]) ? deepMerge(src[key], target[key]) : (src[key] = target[key]); 39 | } 40 | return src; 41 | } 42 | 43 | export function openWindow( 44 | url: string, 45 | opt?: { target?: TargetContext | string; noopener?: boolean; noreferrer?: boolean }, 46 | ) { 47 | const { target = '__blank', noopener = true, noreferrer = true } = opt || {}; 48 | const feature: string[] = []; 49 | 50 | noopener && feature.push('noopener=yes'); 51 | noreferrer && feature.push('noreferrer=yes'); 52 | 53 | window.open(url, target, feature.join(',')); 54 | } 55 | 56 | // dynamic use hook props 57 | export function getDynamicProps(props: T): Partial { 58 | const ret: Recordable = {}; 59 | 60 | Object.keys(props).map((key) => { 61 | ret[key] = unref((props as Recordable)[key]); 62 | }); 63 | 64 | return ret as Partial; 65 | } 66 | 67 | export function getRawRoute(route: RouteLocationNormalized): RouteLocationNormalized { 68 | if (!route) return route; 69 | const { matched, ...opt } = route; 70 | return { 71 | ...opt, 72 | matched: (matched 73 | ? matched.map((item) => ({ 74 | meta: item.meta, 75 | name: item.name, 76 | path: item.path, 77 | })) 78 | : undefined) as RouteRecordNormalized[], 79 | }; 80 | } 81 | 82 | export const withInstall = (component: T, alias?: string) => { 83 | const comp = component as any; 84 | comp.install = (app: App) => { 85 | app.component(comp.name || comp.displayName, component); 86 | if (alias) { 87 | app.config.globalProperties[alias] = component; 88 | } 89 | }; 90 | return component as T & Plugin; 91 | }; 92 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | const toString = Object.prototype.toString; 2 | 3 | export function is(val: unknown, type: string) { 4 | return toString.call(val) === `[object ${type}]`; 5 | } 6 | 7 | export function isDef(val?: T): val is T { 8 | return typeof val !== 'undefined'; 9 | } 10 | 11 | export function isUnDef(val?: T): val is T { 12 | return !isDef(val); 13 | } 14 | 15 | export function isObject(val: any): val is Record { 16 | return val !== null && is(val, 'Object'); 17 | } 18 | 19 | export function isEmpty(val: T): val is T { 20 | if (isArray(val) || isString(val)) { 21 | return val.length === 0; 22 | } 23 | 24 | if (val instanceof Map || val instanceof Set) { 25 | return val.size === 0; 26 | } 27 | 28 | if (isObject(val)) { 29 | return Object.keys(val).length === 0; 30 | } 31 | 32 | return false; 33 | } 34 | 35 | export function isDate(val: unknown): val is Date { 36 | return is(val, 'Date'); 37 | } 38 | 39 | export function isNull(val: unknown): val is null { 40 | return val === null; 41 | } 42 | 43 | export function isNullAndUnDef(val: unknown): val is null | undefined { 44 | return isUnDef(val) && isNull(val); 45 | } 46 | 47 | export function isNullOrUnDef(val: unknown): val is null | undefined { 48 | return isUnDef(val) || isNull(val); 49 | } 50 | 51 | export function isNumber(val: unknown): val is number { 52 | return is(val, 'Number'); 53 | } 54 | 55 | export function isPromise(val: unknown): val is Promise { 56 | return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch); 57 | } 58 | 59 | export function isString(val: unknown): val is string { 60 | return is(val, 'String'); 61 | } 62 | 63 | export function isFunction(val: unknown): val is Function { 64 | return typeof val === 'function'; 65 | } 66 | 67 | export function isBoolean(val: unknown): val is boolean { 68 | return is(val, 'Boolean'); 69 | } 70 | 71 | export function isRegExp(val: unknown): val is RegExp { 72 | return is(val, 'RegExp'); 73 | } 74 | 75 | export function isArray(val: any): val is Array { 76 | return val && Array.isArray(val); 77 | } 78 | 79 | export function isWindow(val: any): val is Window { 80 | return typeof window !== 'undefined' && is(val, 'Window'); 81 | } 82 | 83 | export function isElement(val: unknown): val is Element { 84 | return isObject(val) && !!val.tagName; 85 | } 86 | 87 | export function isMap(val: unknown): val is Map { 88 | return is(val, 'Map'); 89 | } 90 | 91 | export const isServer = typeof window === 'undefined'; 92 | 93 | export const isClient = !isServer; 94 | 95 | export function isUrl(path: string): boolean { 96 | const reg = 97 | /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/; 98 | return reg.test(path); 99 | } 100 | -------------------------------------------------------------------------------- /src/hooks/useECharts.ts: -------------------------------------------------------------------------------- 1 | import type { EChartsOption } from 'echarts'; 2 | import type { Ref } from 'vue'; 3 | import { useTimeoutFn } from '/@/hooks/useTimeout'; 4 | import { Fn, tryOnUnmounted } from '@vueuse/core'; 5 | import { unref, nextTick, computed, ref } from 'vue'; 6 | import { useDebounceFn } from '@vueuse/core'; 7 | import { useEventListener } from '/@/hooks/useEventListener'; 8 | import echarts from '/@/utils/echarts'; 9 | 10 | export function useECharts( 11 | elRef: Ref, 12 | theme: 'light' | 'dark' | 'default' = 'default', 13 | ) { 14 | const getDarkMode = computed(() => { 15 | return theme; 16 | }); 17 | let chartInstance: echarts.ECharts | null = null; 18 | let resizeFn: Fn = resize; 19 | const cacheOptions = ref({}) as Ref; 20 | let removeResizeFn: Fn = () => {}; 21 | 22 | resizeFn = useDebounceFn(resize, 200); 23 | 24 | const getOptions = computed(() => { 25 | return { 26 | backgroundColor: 'transparent', 27 | ...cacheOptions.value, 28 | } as EChartsOption; 29 | }); 30 | 31 | function initCharts() { 32 | const el = unref(elRef); 33 | if (!el || !unref(el)) { 34 | return; 35 | } 36 | 37 | chartInstance = echarts.init(el); 38 | const { removeEvent } = useEventListener({ 39 | el: window, 40 | name: 'resize', 41 | listener: resizeFn, 42 | }); 43 | removeResizeFn = removeEvent; 44 | if (el.offsetHeight === 0) { 45 | useTimeoutFn(() => { 46 | resizeFn(); 47 | }, 30); 48 | } 49 | } 50 | 51 | function setOptions(options: EChartsOption, clear = true) { 52 | cacheOptions.value = options; 53 | if (unref(elRef)?.offsetHeight === 0) { 54 | useTimeoutFn(() => { 55 | setOptions(unref(getOptions)); 56 | }, 30); 57 | return; 58 | } 59 | nextTick(() => { 60 | useTimeoutFn(() => { 61 | if (!chartInstance) { 62 | initCharts(); 63 | 64 | if (!chartInstance) return; 65 | } 66 | clear && chartInstance?.clear(); 67 | 68 | chartInstance?.setOption(unref(getOptions)); 69 | }, 30); 70 | }); 71 | } 72 | 73 | function resize() { 74 | chartInstance?.resize(); 75 | } 76 | 77 | watch( 78 | () => getDarkMode.value, 79 | () => { 80 | if (chartInstance) { 81 | chartInstance.dispose(); 82 | initCharts(); 83 | setOptions(cacheOptions.value); 84 | } 85 | }, 86 | ); 87 | 88 | tryOnUnmounted(() => { 89 | if (!chartInstance) return; 90 | removeResizeFn(); 91 | chartInstance.dispose(); 92 | chartInstance = null; 93 | }); 94 | 95 | function getInstance(): echarts.ECharts | null { 96 | if (!chartInstance) { 97 | initCharts(); 98 | } 99 | return chartInstance; 100 | } 101 | 102 | return { 103 | setOptions, 104 | resize, 105 | echarts, 106 | getInstance, 107 | }; 108 | } 109 | -------------------------------------------------------------------------------- /src/layouts/BasicLayout/index.vue: -------------------------------------------------------------------------------- 1 | 41 | 77 | 90 | -------------------------------------------------------------------------------- /src/views/home/components/DataOverview.vue: -------------------------------------------------------------------------------- 1 | 21 | 79 | 109 | -------------------------------------------------------------------------------- /mock/user.ts: -------------------------------------------------------------------------------- 1 | import { MockMethod } from 'vite-plugin-mock'; 2 | import { resultError, resultSuccess, getRequestToken, requestParams } from './_util'; 3 | 4 | export function createFakeUserList() { 5 | return [ 6 | { 7 | userId: '1', 8 | username: 'admin', 9 | realName: 'sssgoEasy Admin', 10 | avatar: '', 11 | desc: 'manager', 12 | password: '123456', 13 | token: 'fakeToken1', 14 | auths: [], 15 | modules: [], 16 | is_admin: 1, 17 | role_name: '管理员角色', 18 | mobile: 13000000000, 19 | last_login: '2021-11-11 12:00', 20 | role: 1, // 管理 21 | }, 22 | { 23 | userId: '2', 24 | username: 'test', 25 | password: '123456', 26 | realName: 'test user', 27 | avatar: '', 28 | desc: 'tester', 29 | token: 'fakeToken2', 30 | auths: [], 31 | modules: ['home', 'website'], 32 | is_admin: 0, 33 | role_name: '普通用户角色', 34 | mobile: 18000000000, 35 | last_login: '2021-11-11 12:12', 36 | role: 0, // 普通 37 | }, 38 | ]; 39 | } 40 | 41 | export default [ 42 | // mock user login 43 | { 44 | url: '/v1/user/login', 45 | timeout: 200, 46 | method: 'post', 47 | response: ({ body }) => { 48 | const { username, password } = body; 49 | const checkUser = createFakeUserList().find( 50 | (item) => item.username === username && password === item.password, 51 | ); 52 | if (!checkUser) { 53 | return resultError('Incorrect account or password!'); 54 | } 55 | return resultSuccess(checkUser); 56 | }, 57 | }, 58 | { 59 | url: '/v1/user/permission', 60 | method: 'get', 61 | response: (request: requestParams) => { 62 | const token = getRequestToken(request); 63 | if (!token) return resultError('Invalid token'); 64 | const checkUser = createFakeUserList().find((item) => item.token === token); 65 | if (!checkUser) { 66 | return resultError('The corresponding user information was not obtained!'); 67 | } 68 | return resultSuccess(checkUser); 69 | }, 70 | }, 71 | { 72 | url: '/v1/user/logout', 73 | timeout: 200, 74 | method: 'get', 75 | response: (request: requestParams) => { 76 | const token = getRequestToken(request); 77 | if (!token) return resultError('Invalid token'); 78 | const checkUser = createFakeUserList().find((item) => item.token === token); 79 | if (!checkUser) { 80 | return resultError('Invalid token!'); 81 | } 82 | return resultSuccess(undefined, { message: 'Token has been destroyed' }); 83 | }, 84 | }, 85 | { 86 | url: '/v1/account/info', 87 | method: 'get', 88 | response: (request: requestParams) => { 89 | const token = getRequestToken(request); 90 | if (!token) return resultError('Invalid token'); 91 | const checkUser = createFakeUserList().find((item) => item.token === token); 92 | if (!checkUser) { 93 | return resultError('The corresponding user information was not obtained!'); 94 | } 95 | return resultSuccess(checkUser); 96 | }, 97 | }, 98 | ] as MockMethod[]; 99 | -------------------------------------------------------------------------------- /src/views/home/components/TradingHistory.vue: -------------------------------------------------------------------------------- 1 | 24 | 99 | 110 | -------------------------------------------------------------------------------- /src/utils/http.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; 2 | import { API_PREFIX } from '../../config/constant'; 3 | import { ResData } from '../api/global'; 4 | import { getToken } from './auth'; 5 | import { useUserStoreWithOut } from '../store/modules/user'; 6 | import { useMessage } from '../hooks/useMessage'; 7 | // import { WhiteList } from './permission'; 8 | // import { usePermissioStoreWithOut } from '/@/store/modules/permission'; 9 | 10 | const { createMessage } = useMessage(); 11 | // baseURL 12 | const BASE_URL = import.meta.env.MODE === 'development' ? API_PREFIX : ''; 13 | 14 | const instance = axios.create({ 15 | baseURL: BASE_URL, 16 | withCredentials: true, 17 | timeout: 10000, 18 | }); 19 | 20 | instance.interceptors.request.use( 21 | (config) => { 22 | // 接口权限拦截 23 | // const store = usePermissioStoreWithOut(); 24 | // const { url = '' } = config; 25 | // if (!WhiteList.includes(url) && store.getIsAdmin === 0) { 26 | // if (!store.getAuths.includes(url)) { 27 | // console.log('url', url, store.getIsAdmin); 28 | // return Promise.reject('没有操作权限'); 29 | // } 30 | // } 31 | 32 | // 请求头 token配置 33 | const token = getToken(); 34 | 35 | if (token) { 36 | config.headers = { 37 | ...config.headers, 38 | Authorization: token, 39 | }; 40 | // config.headers['Authorization'] = token; 41 | } 42 | return config; 43 | }, 44 | (error) => { 45 | return Promise.reject(error); 46 | }, 47 | ); 48 | 49 | instance.interceptors.response.use( 50 | (response) => { 51 | const res = response.data as ResData; 52 | // 正确状态 53 | if (res.code === 0) { 54 | return res.result || true; 55 | } 56 | 57 | // 登录失效 58 | if (res.code === -1) { 59 | useUserStoreWithOut().logout(); 60 | } 61 | 62 | // 异常 63 | createMessage.error(res.message); 64 | return undefined; 65 | }, 66 | (error) => { 67 | console.log('err' + error); // for debug 68 | // 没权限时,不再重复提示 69 | if (error === '没有操作权限') return; 70 | createMessage.error('网络超时,稍后再试吧'); 71 | }, 72 | ); 73 | 74 | const request = ( 75 | config: AxiosRequestConfig | string, 76 | options?: AxiosRequestConfig, 77 | ): Promise => { 78 | if (typeof config === 'string') { 79 | if (!options) { 80 | return instance.request({ 81 | url: config, 82 | }); 83 | // throw new Error('请配置正确的请求参数'); 84 | } else { 85 | return instance.request({ 86 | url: config, 87 | ...options, 88 | }); 89 | } 90 | } else { 91 | return instance.request(config); 92 | } 93 | }; 94 | export function get(config: AxiosRequestConfig, options?: AxiosRequestConfig): Promise { 95 | return request({ ...config, method: 'GET' }, options); 96 | } 97 | 98 | export function post( 99 | config: AxiosRequestConfig, 100 | options?: AxiosRequestConfig, 101 | ): Promise { 102 | return request({ ...config, method: 'POST' }, options); 103 | } 104 | 105 | export default request; 106 | export type { AxiosInstance, AxiosResponse }; 107 | -------------------------------------------------------------------------------- /src/utils/mitt.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * copy to https://github.com/developit/mitt 3 | * Expand clear method 4 | */ 5 | 6 | export type EventType = string | symbol; 7 | 8 | // An event handler can take an optional event argument 9 | // and should not return a value 10 | export type Handler = (event?: T) => void; 11 | export type WildcardHandler = (type: EventType, event?: any) => void; 12 | 13 | // An array of all currently registered event handlers for a type 14 | export type EventHandlerList = Array; 15 | export type WildCardEventHandlerList = Array; 16 | 17 | // A map of event types and their corresponding event handlers. 18 | export type EventHandlerMap = Map; 19 | 20 | export interface Emitter { 21 | all: EventHandlerMap; 22 | 23 | on(type: EventType, handler: Handler): void; 24 | on(type: '*', handler: WildcardHandler): void; 25 | 26 | off(type: EventType, handler: Handler): void; 27 | off(type: '*', handler: WildcardHandler): void; 28 | 29 | emit(type: EventType, event?: T): void; 30 | emit(type: '*', event?: any): void; 31 | clear(): void; 32 | } 33 | 34 | /** 35 | * Mitt: Tiny (~200b) functional event emitter / pubsub. 36 | * @name mitt 37 | * @returns {Mitt} 38 | */ 39 | export default function mitt(all?: EventHandlerMap): Emitter { 40 | all = all || new Map(); 41 | 42 | return { 43 | /** 44 | * A Map of event names to registered handler functions. 45 | */ 46 | all, 47 | 48 | /** 49 | * Register an event handler for the given type. 50 | * @param {string|symbol} type Type of event to listen for, or `"*"` for all events 51 | * @param {Function} handler Function to call in response to given event 52 | * @memberOf mitt 53 | */ 54 | on(type: EventType, handler: Handler) { 55 | const handlers = all?.get(type); 56 | const added = handlers && handlers.push(handler); 57 | if (!added) { 58 | all?.set(type, [handler]); 59 | } 60 | }, 61 | 62 | /** 63 | * Remove an event handler for the given type. 64 | * @param {string|symbol} type Type of event to unregister `handler` from, or `"*"` 65 | * @param {Function} handler Handler function to remove 66 | * @memberOf mitt 67 | */ 68 | off(type: EventType, handler: Handler) { 69 | const handlers = all?.get(type); 70 | if (handlers) { 71 | handlers.splice(handlers.indexOf(handler) >>> 0, 1); 72 | } 73 | }, 74 | 75 | /** 76 | * Invoke all handlers for the given type. 77 | * If present, `"*"` handlers are invoked after type-matched handlers. 78 | * 79 | * Note: Manually firing "*" handlers is not supported. 80 | * 81 | * @param {string|symbol} type The event type to invoke 82 | * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler 83 | * @memberOf mitt 84 | */ 85 | emit(type: EventType, evt: T) { 86 | ((all?.get(type) || []) as EventHandlerList).slice().map((handler) => { 87 | handler(evt); 88 | }); 89 | ((all?.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { 90 | handler(type, evt); 91 | }); 92 | }, 93 | 94 | /** 95 | * Clear all 96 | */ 97 | clear() { 98 | this.all.clear(); 99 | }, 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /src/router/router.config.ts: -------------------------------------------------------------------------------- 1 | import BasicLayout from '/@/layouts/BasicLayout/index.vue'; 2 | import BlankLayout from '/@/layouts/BlankLayout.vue'; 3 | import type { RouteRecordRaw } from 'vue-router'; 4 | 5 | export const accessRoutes: RouteRecordRaw[] = [ 6 | { 7 | path: '/app', 8 | name: 'app', 9 | component: BasicLayout, 10 | redirect: '/app/home', 11 | meta: { title: '管理平台' }, 12 | children: [ 13 | { 14 | path: '/app/home', 15 | component: () => import('/@/views/home/index.vue'), 16 | name: 'home', 17 | meta: { 18 | title: '首页', 19 | icon: 'liulanqi', 20 | auth: ['home'], 21 | }, 22 | }, 23 | { 24 | path: '/app/website', 25 | name: 'website', 26 | component: () => import('/@/views/website/index.vue'), 27 | meta: { 28 | title: '网站管理', 29 | keepAlive: true, 30 | icon: 'jiedianguanli', 31 | auth: ['website'], 32 | }, 33 | }, 34 | { 35 | path: '/app/table-demo', 36 | name: 'table-demo', 37 | component: () => import('/@/views/table-demo/index.vue'), 38 | meta: { 39 | title: '表格用法', 40 | keepAlive: true, 41 | icon: 'rili', 42 | }, 43 | }, 44 | { 45 | path: '/app/others', 46 | name: 'others', 47 | component: BlankLayout, 48 | redirect: '/app/others/about', 49 | meta: { 50 | title: '其他菜单', 51 | icon: 'shurumimadenglu', 52 | auth: ['others'], 53 | }, 54 | children: [ 55 | { 56 | path: '/app/others/about', 57 | name: 'about', 58 | component: () => import('/@/views/others/about/index.vue'), 59 | meta: { title: '关于', keepAlive: true, hiddenWrap: true }, 60 | }, 61 | { 62 | path: '/app/others/antdv', 63 | name: 'antdv', 64 | component: () => import('/@/views/others/antdv/index.vue'), 65 | meta: { title: '组件', keepAlive: true, breadcrumb: true }, 66 | }, 67 | ], 68 | }, 69 | { 70 | path: '/sys/account', 71 | name: 'account', 72 | component: () => import('/@/views/account/index.vue'), 73 | meta: { title: '用户管理', keepAlive: true, breadcrumb: true }, 74 | }, 75 | ], 76 | }, 77 | ]; 78 | 79 | const constantRoutes: RouteRecordRaw[] = [ 80 | { 81 | path: '/login', 82 | component: () => import('/@/views/login/index.vue'), 83 | name: 'login', 84 | meta: { title: '登录' }, 85 | }, 86 | { 87 | path: '/', 88 | name: 'Root', 89 | redirect: '/app', 90 | meta: { 91 | title: 'Root', 92 | }, 93 | }, 94 | // ...accessRoutes, 95 | ]; 96 | 97 | export const publicRoutes = [ 98 | { 99 | path: '/redirect', 100 | component: BlankLayout, 101 | children: [ 102 | { 103 | path: '/redirect/:path(.*)', 104 | component: () => import('/@/views/redirect/index'), 105 | }, 106 | ], 107 | }, 108 | { 109 | path: '/:pathMatch(.*)', 110 | redirect: '/404', 111 | }, 112 | { 113 | path: '/404', 114 | component: () => import('/@/views/404.vue'), 115 | }, 116 | ]; 117 | 118 | // /** 119 | // * 基础路由 120 | // * @type { *[] } 121 | // */ 122 | // export const constantRouterMap = []; 123 | 124 | export default constantRoutes; 125 | -------------------------------------------------------------------------------- /mock/table.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs'; 2 | import { faker } from '@faker-js/faker'; 3 | import { getRequestToken, resultError } from './_util'; 4 | import { createFakeUserList } from './user'; 5 | 6 | enum URL { 7 | table = '/table/list', 8 | list = '/v1/node/nodelist', 9 | } 10 | 11 | const data = Mock.mock({ 12 | 'items|30': [ 13 | { 14 | id: '@id', 15 | title: '@sentence(10, 20)', 16 | account: '@phone', 17 | true_name: '@name', 18 | created_at: '@datetime', 19 | role_name: '@name', 20 | }, 21 | ], 22 | }); 23 | 24 | const NAMES = ['节点1', '节点2', '节点3', '节点4']; 25 | const DATA_names = ['机构1', '机构2', '机构3']; 26 | 27 | const DATA_blockList = Mock.mock({ 28 | 'items|23': [ 29 | { 30 | 'id|+1': 1, 31 | node_name: () => faker.helpers.arrayElement(NAMES), 32 | institutions_name: () => faker.helpers.arrayElement(DATA_names), 33 | ip: () => faker.internet.ip(), 34 | port: () => faker.internet.port(), 35 | nodeRole: () => faker.helpers.arrayElement(['普通节点', '管理节点']), 36 | is_consensus: () => faker.helpers.arrayElement(['是', '否']), 37 | create_time: () => faker.date.past(2, new Date().toISOString()), 38 | status: () => faker.helpers.arrayElement(['正常', '异常']), 39 | isSelf: () => faker.datatype.boolean(), 40 | }, 41 | ], 42 | }); 43 | 44 | export default [ 45 | { 46 | url: URL.table, 47 | method: 'get', 48 | response: () => { 49 | const items = data.items; 50 | return { 51 | code: 0, 52 | result: { 53 | total: items.length, 54 | list: items, 55 | }, 56 | }; 57 | }, 58 | }, 59 | { 60 | url: URL.list, 61 | method: 'get', 62 | response: (request) => { 63 | let items = DATA_blockList.items.map((n) => { 64 | return { 65 | ...n, 66 | node_name: n.nodeRole === '创世节点' ? '创世节点' : n.node_name, 67 | institutions_name: 68 | n.nodeRole === '管理节点' 69 | ? '管理' 70 | : ['节点1', '节点2'].includes(n.node_name) 71 | ? n.node_name.replace('节点', '机构') 72 | : n.institutions_name, 73 | }; 74 | }); 75 | 76 | const token = getRequestToken(request); 77 | if (!token) return resultError('Invalid token'); 78 | const checkUser = createFakeUserList().find((item) => item.token === token); 79 | if (checkUser?.role === 0) { 80 | items = [ 81 | { 82 | id: 14, 83 | node_name: '节点1', 84 | institutions_name: '机构1', 85 | ip: '147.174.206.1', 86 | port: 26042, 87 | nodeRole: '普通节点', 88 | is_consensus: '否', 89 | create_time: '2021-02-25T02:27:18.151Z', 90 | status: '正常', 91 | isSelf: true, 92 | isUpgrade: true, 93 | }, 94 | { 95 | id: 15, 96 | node_name: '节点2', 97 | institutions_name: '机构2', 98 | ip: '147.174.6.190', 99 | port: 26042, 100 | nodeRole: '普通节点', 101 | is_consensus: '是', 102 | create_time: '2021-03-25T02:25:18.151Z', 103 | status: '正常', 104 | isSelf: false, 105 | isUpgrade: false, 106 | }, 107 | ]; 108 | } 109 | 110 | return { 111 | code: 0, 112 | result: { 113 | total: items.length, 114 | list: items, 115 | }, 116 | }; 117 | }, 118 | }, 119 | ]; 120 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-vue3-ts", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "description": "a vite + vue3 + pinia + typescript + ant-design-vue template", 6 | "keywords": [ 7 | "vite", 8 | "vue3", 9 | "vue-router", 10 | "setup", 11 | "typescript", 12 | "pinia", 13 | "ant-design-vue", 14 | "template" 15 | ], 16 | "homepage": "https://github.com/js-banana/vite-vue3-ts", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/js-banana/vite-vue3-ts.git" 20 | }, 21 | "engines": { 22 | "node": ">=14" 23 | }, 24 | "scripts": { 25 | "bootstrap": "pnpm install", 26 | "check": "vue-tsc --noEmit", 27 | "serve": "npm run dev", 28 | "dev": "vite", 29 | "dev:development": "vite --mode development", 30 | "build:production": "vue-tsc --noEmit && vite build --mode production", 31 | "build": "vite build", 32 | "build:check": "vue-tsc --noEmit && vite build", 33 | "build:github": "vite build --base=/vite-vue3-ts/", 34 | "build:no-cache": "pnpm clean:cache && npm run build", 35 | "preview": "vite preview", 36 | "clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite", 37 | "clean:lib": "rimraf node_modules", 38 | "lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock}/**/*.{vue,ts,tsx}\" --fix", 39 | "lint:prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"", 40 | "lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js", 41 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s && npx prettier CHANGELOG.md --write && git add CHANGELOG.md", 42 | "prepare": "husky install" 43 | }, 44 | "dependencies": { 45 | "@ant-design/icons-vue": "^6.0.1", 46 | "@vueuse/core": "^6.8.0", 47 | "ant-design-vue": "^3.2.7", 48 | "axios": "^0.24.0", 49 | "dayjs": "^1.11.3", 50 | "echarts": "^5.2.2", 51 | "lodash-es": "^4.17.21", 52 | "pinia": "^2.0.3", 53 | "sanitize.css": "^13.0.0", 54 | "vue": "^3.2.16", 55 | "vue-request": "^1.2.3", 56 | "vue-router": "^4.0.12" 57 | }, 58 | "devDependencies": { 59 | "@commitlint/cli": "^15.0.0", 60 | "@commitlint/config-conventional": "^15.0.0", 61 | "@faker-js/faker": "^7.3.0", 62 | "@types/lodash-es": "^4.17.5", 63 | "@typescript-eslint/eslint-plugin": "^6.7.2", 64 | "@typescript-eslint/parser": "^6.7.2", 65 | "@vitejs/plugin-legacy": "^4.1.1", 66 | "@vitejs/plugin-vue": "^4.3.4", 67 | "@vitejs/plugin-vue-jsx": "^3.0.2", 68 | "autoprefixer": "^10.4.0", 69 | "consola": "^3.2.3", 70 | "conventional-changelog-cli": "^2.2.2", 71 | "eslint": "^7.32.0", 72 | "eslint-config-prettier": "^8.3.0", 73 | "eslint-define-config": "~1.0.9", 74 | "eslint-plugin-jest": "^25.3.0", 75 | "eslint-plugin-prettier": "^4.0.0", 76 | "eslint-plugin-vue": "^7.17.0", 77 | "husky": "^7.0.4", 78 | "jest": "^27.4.2", 79 | "less": "^4.1.2", 80 | "lint-staged": "^11.2.6", 81 | "mockjs": "^1.1.0", 82 | "postcss": "^8.3.11", 83 | "postcss-html": "^1.2.0", 84 | "postcss-less": "^5.0.0", 85 | "prettier": "^2.4.1", 86 | "rimraf": "^3.0.2", 87 | "rollup-plugin-visualizer": "^5.9.2", 88 | "typescript": "^5.2.2", 89 | "unplugin-auto-import": "^0.4.20", 90 | "unplugin-vue-components": "^0.17.21", 91 | "vite": "^4.4.9", 92 | "vite-plugin-compression": "^0.5.1", 93 | "vite-plugin-mock": "^2.9.6", 94 | "vite-plugin-style-import": "^2.0.0", 95 | "vite-plugin-svg-icons": "^2.0.1", 96 | "vue-eslint-parser": "^7.11.0", 97 | "vue-tsc": "^1.8.13" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/views/others/about/index.vue: -------------------------------------------------------------------------------- 1 | 57 | 129 | -------------------------------------------------------------------------------- /src/views/account/index.vue: -------------------------------------------------------------------------------- 1 | 34 | 88 | 129 | -------------------------------------------------------------------------------- /src/store/modules/permission.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { store } from '/@/store'; 3 | import fetchApi from '/@/api/user'; 4 | import { RouteRecordRaw } from 'vue-router'; 5 | import constantRoutes, { accessRoutes, publicRoutes } from '/@/router/router.config'; 6 | import { filterAsyncRoutes } from '/@/utils/permission'; 7 | 8 | interface PermissioState { 9 | isGetUserInfo: boolean; // 是否获取过用户信息 10 | isAdmin: 0 | 1; // 是否为管理员 11 | auths: string[]; // 当前用户权限 12 | modules: string[]; // 模块权限 13 | role: 0 | 1; 14 | } 15 | 16 | export const usePermissioStore = defineStore({ 17 | id: 'app-permission', 18 | state: (): PermissioState => ({ 19 | // isGetUserInfo 20 | isGetUserInfo: false, 21 | // isAdmin 22 | isAdmin: 0, 23 | // auths 24 | auths: [], 25 | // modules 26 | modules: [], 27 | // role 0-银行 1-银保监 28 | role: 0, 29 | }), 30 | getters: { 31 | getAuths(): string[] { 32 | return this.auths; 33 | }, 34 | getRole(): 0 | 1 { 35 | return this.role; 36 | }, 37 | getModules(): string[] { 38 | return this.modules; 39 | }, 40 | getIsAdmin(): 0 | 1 { 41 | return this.isAdmin; 42 | }, 43 | getIsGetUserInfo(): boolean { 44 | return this.isGetUserInfo; 45 | }, 46 | }, 47 | actions: { 48 | setAuth(auths: string[], modules: string[]) { 49 | this.auths = auths; 50 | this.isGetUserInfo = true; 51 | this.modules = modules; 52 | }, 53 | setIsAdmin(isAdmin: 0 | 1) { 54 | this.isAdmin = isAdmin; 55 | }, 56 | resetState() { 57 | this.isGetUserInfo = false; 58 | this.isAdmin = 0; 59 | this.auths = []; 60 | this.modules = []; 61 | this.role = 0; 62 | }, 63 | 64 | /** 65 | * @name fetchAuths 66 | * @description 获取当前用户权限 67 | */ 68 | async fetchAuths() { 69 | const res = await fetchApi.permission(); 70 | if (res) { 71 | this.setAuth(res.auths, res.modules); 72 | this.setIsAdmin(res.is_admin || 0); 73 | } 74 | return res; 75 | }, 76 | 77 | /** 78 | * @name buildRoutesAction 79 | * @description: 获取路由 80 | */ 81 | async buildRoutesAction(): Promise { 82 | // 404 路由一定要放在 权限路由后面 83 | let routes: RouteRecordRaw[] = [...constantRoutes, ...accessRoutes, ...publicRoutes]; 84 | 85 | if (this.getIsAdmin !== 1) { 86 | // 普通用户 87 | // 1. 方案一:过滤每个路由模块涉及的接口权限,判断是否展示该路由 88 | // 2. 方案二:直接检索接口权限列表是否包含该路由模块,不做细分,axios同一拦截 89 | routes = [ 90 | ...constantRoutes, 91 | ...filterAsyncRoutes(accessRoutes, this.modules), 92 | ...publicRoutes, 93 | ]; 94 | } 95 | 96 | return routes; 97 | }, 98 | 99 | // /** 100 | // * @name buildRoutesAction 101 | // * @description: 获取路由 102 | // */ 103 | // buildRoutesAction(): RouteRecordRaw[] { 104 | // // this.isGetUserInfo = true; 105 | // this.setIsGetUserInfo(true); 106 | 107 | // // 404 路由一定要放在 权限路由后面 108 | // let routes: RouteRecordRaw[] = [...constantRoutes, ...accessRoutes, ...publicRoutes]; 109 | 110 | // // 1. 角色权限过滤:0-银行 1-银保监 111 | // let filterRoutes = filterRouteByRole(cloneDeep(accessRoutes), this.role); 112 | // // let filterRoutes = routes; 113 | 114 | // // 2. 菜单权限过滤: 115 | // // 管理员直接跳过 116 | // if (this.getIsAdmin === 0) { 117 | // const filterRoutesByAuth = filterAsyncRoutes(cloneDeep(filterRoutes), this.modules); 118 | // filterRoutes = filterRoutesByAuth; 119 | // } 120 | 121 | // // 普通用户 122 | // // 1. 方案一:过滤每个路由模块涉及的接口权限,判断是否展示该路由 123 | // // 2. 方案二:直接检索接口权限列表是否包含该路由模块,不做细分,axios同一拦截 124 | // routes = [...constantRoutes, ...filterRoutes, ...publicRoutes]; 125 | 126 | // return routes; 127 | // }, 128 | }, 129 | }); 130 | 131 | // Need to be used outside the setup 132 | export function usePermissioStoreWithOut() { 133 | return usePermissioStore(store); 134 | } 135 | -------------------------------------------------------------------------------- /src/hooks/useMessage.tsx: -------------------------------------------------------------------------------- 1 | import type { ModalFunc, ModalFuncProps } from 'ant-design-vue/lib/modal/Modal'; 2 | 3 | import { Modal, message as Message, notification } from 'ant-design-vue'; 4 | import { InfoCircleFilled, CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons-vue'; 5 | 6 | import { NotificationArgsProps, ConfigProps } from 'ant-design-vue/lib/notification'; 7 | import { isString } from '/@/utils/is'; 8 | 9 | // 手动引入 message样式 10 | import 'ant-design-vue/es/message/style'; 11 | import 'ant-design-vue/es/notification/style'; 12 | 13 | export interface NotifyApi { 14 | info(config: NotificationArgsProps): void; 15 | success(config: NotificationArgsProps): void; 16 | error(config: NotificationArgsProps): void; 17 | warn(config: NotificationArgsProps): void; 18 | warning(config: NotificationArgsProps): void; 19 | open(args: NotificationArgsProps): void; 20 | close(key: String): void; 21 | config(options: ConfigProps): void; 22 | destroy(): void; 23 | } 24 | 25 | export declare type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'; 26 | export declare type IconType = 'success' | 'info' | 'error' | 'warning'; 27 | export interface ModalOptionsEx extends Omit { 28 | iconType: 'warning' | 'success' | 'error' | 'info'; 29 | } 30 | export type ModalOptionsPartial = Partial & Pick; 31 | 32 | interface ConfirmOptions { 33 | info: ModalFunc; 34 | success: ModalFunc; 35 | error: ModalFunc; 36 | warn: ModalFunc; 37 | warning: ModalFunc; 38 | } 39 | 40 | function getIcon(iconType: string) { 41 | if (iconType === 'warning') { 42 | return ; 43 | } else if (iconType === 'success') { 44 | return ; 45 | } else if (iconType === 'info') { 46 | return ; 47 | } else { 48 | return ; 49 | } 50 | } 51 | 52 | function renderContent({ content }: Pick) { 53 | if (isString(content)) { 54 | return
${content as string}
`}>; 55 | } else { 56 | return content; 57 | } 58 | } 59 | 60 | /** 61 | * @description: Create confirmation box 62 | */ 63 | function createConfirm(options: ModalOptionsEx): ConfirmOptions { 64 | const iconType = options.iconType || 'warning'; 65 | Reflect.deleteProperty(options, 'iconType'); 66 | const opt: ModalFuncProps = { 67 | centered: true, 68 | icon: getIcon(iconType), 69 | ...options, 70 | content: renderContent(options), 71 | }; 72 | return Modal.confirm(opt) as unknown as ConfirmOptions; 73 | } 74 | 75 | const getBaseOptions = () => { 76 | return { 77 | okText: '确定', 78 | centered: true, 79 | }; 80 | }; 81 | 82 | function createModalOptions(options: ModalOptionsPartial, icon: string): ModalOptionsPartial { 83 | return { 84 | ...getBaseOptions(), 85 | ...options, 86 | content: renderContent(options), 87 | icon: getIcon(icon), 88 | }; 89 | } 90 | 91 | function createSuccessModal(options: ModalOptionsPartial) { 92 | return Modal.success(createModalOptions(options, 'success')); 93 | } 94 | 95 | function createErrorModal(options: ModalOptionsPartial) { 96 | return Modal.error(createModalOptions(options, 'close')); 97 | } 98 | 99 | function createInfoModal(options: ModalOptionsPartial) { 100 | return Modal.info(createModalOptions(options, 'info')); 101 | } 102 | 103 | function createWarningModal(options: ModalOptionsPartial) { 104 | return Modal.warning(createModalOptions(options, 'warning')); 105 | } 106 | 107 | notification.config({ 108 | placement: 'topRight', 109 | duration: 3, 110 | }); 111 | 112 | /** 113 | * @description: message 114 | */ 115 | export function useMessage() { 116 | return { 117 | createMessage: Message, 118 | notification: notification as NotifyApi, 119 | createConfirm: createConfirm, 120 | createSuccessModal, 121 | createErrorModal, 122 | createInfoModal, 123 | createWarningModal, 124 | }; 125 | } 126 | -------------------------------------------------------------------------------- /src/layouts/BasicLayout/components/SideMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Layout, Menu, Space } from 'ant-design-vue'; 2 | import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue'; 3 | import Icon from '/@/components/Icon/index.vue'; 4 | import { PropType, h, Transition } from 'vue'; 5 | import { MenuDataItem } from '../utils/typings'; 6 | import { router } from '/@/router'; 7 | import './index.less'; 8 | 9 | export default defineComponent({ 10 | name: 'BaseMenu', 11 | props: { 12 | theme: { 13 | type: String, 14 | default: 'light', 15 | }, 16 | menuWidth: { 17 | type: Number, 18 | default: 208, 19 | }, 20 | menuData: { 21 | type: Array as PropType, 22 | default: () => [], 23 | }, 24 | }, 25 | setup(props) { 26 | const state = reactive({ 27 | collapsed: false, // default value 28 | openKeys: [], 29 | selectedKeys: [], 30 | }); 31 | 32 | watchEffect(() => { 33 | if (router.currentRoute) { 34 | const matched = router.currentRoute.value.matched.concat(); 35 | state.selectedKeys = matched.filter((r) => r.name !== 'index').map((r) => r.path); 36 | state.openKeys = matched 37 | .filter((r) => r.path !== router.currentRoute.value.path) 38 | .map((r) => r.path); 39 | } 40 | }); 41 | 42 | const onSelect = (e: { key: string; item: { props: { routeid: number } } } | any) => { 43 | router.push(e.key); 44 | }; 45 | 46 | const getIcon = (type?: string) => 47 | type ? : null; 48 | 49 | // 构建树结构 50 | const makeTreeDom = (data: MenuDataItem[]): JSX.Element[] => { 51 | return data.map((item: MenuDataItem) => { 52 | if (item.children) { 53 | return ( 54 | 58 | {getIcon(item.meta?.icon as string)} 59 | {item.meta?.title} 60 | 61 | } 62 | > 63 | {makeTreeDom(item.children)} 64 | 65 | ); 66 | } 67 | return ( 68 | 69 | {getIcon(item.meta?.icon as string)} 70 | {item.meta?.title} 71 | 72 | ); 73 | }); 74 | }; 75 | 76 | return () => { 77 | return ( 78 | (state.collapsed = val)} 86 | collapsible 87 | collapsed={state.collapsed} 88 | // collapsedWidth={48} 89 | > 90 | {/* logo */} 91 | 92 | {!state.collapsed && ( 93 | 99 | )} 100 | 101 | {/* menu */} 102 | (state.openKeys = keys)} 108 | onSelect={onSelect} 109 | class="my-sideMenu-sider_menu" 110 | > 111 | {makeTreeDom(props.menuData)} 112 | 113 | {/* footer */} 114 | 121 | 122 | ); 123 | }; 124 | }, 125 | }); 126 | -------------------------------------------------------------------------------- /src/views/website/index.vue: -------------------------------------------------------------------------------- 1 | 34 | 131 | 139 | -------------------------------------------------------------------------------- /src/components/TableFilter/index.vue: -------------------------------------------------------------------------------- 1 | 52 | 108 | 133 | -------------------------------------------------------------------------------- /src/views/login/Form.vue: -------------------------------------------------------------------------------- 1 | 48 | 107 | 159 | -------------------------------------------------------------------------------- /src/views/table-demo/index.vue: -------------------------------------------------------------------------------- 1 | 39 | 164 | 172 | -------------------------------------------------------------------------------- /src/components/Table/index.vue: -------------------------------------------------------------------------------- 1 | 61 | 196 | 221 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vite-vue3-ts 2 | 3 | [![ci](https://github.com/JS-banana/vite-vue3-ts/actions/workflows/deploy.yml/badge.svg)](https://github.com/JS-banana/vite-vue3-ts/actions/workflows/deploy.yml) 4 | 5 | ## 介绍 6 | 7 | 一个使用 `vite` + `vue3` + `pinia` + `ant-design-vue` + `typescript` 完整技术路线开发的项目,秒级开发更新启动、新的`vue3 composition api` 结合 `setup`纵享丝滑般的开发体验、全新的 `pinia`状态管理器和优秀的设计体验(`1k`的size)、`antd`无障碍过渡使用UI组件库 `ant-design-vue`、安全高效的 `typescript`类型支持、代码规范验证、多级别的权限管理~ 8 | 9 | 相关文章: 10 | 11 | 本项目相关改动及更新见【[更新记录](#更新记录)↓↓↓】 12 | 13 | ## 特性 14 | 15 | - ✨脚手架工具:高效、快速的 **Vite** 16 | - 🔥前端框架:眼下最时髦的 **Vue3** 17 | - 🍍状态管理器:`vue3`新秀 **Pinia**,犹如 `react zustand`般的体验,友好的api和异步处理 18 | - 🏆开发语言:政治正确 **TypeScript** 19 | - 🎉UI组件:`antd`开发者无障碍过渡使用 **ant-design-vue**,熟悉的配方熟悉的味道 20 | - 🎨css样式:**less** 、`postcss` 21 | - 📖代码规范:**Eslint**、**Prettier**、**Commitlint** 22 | - 🔒权限管理:页面级、菜单级、按钮级、接口级 23 | - ✊依赖按需加载:**unplugin-auto-import**,可自动导入使用到的`vue`、`vue-router`等依赖 24 | - 💪组件按需导入:**unplugin-vue-components**,无论是第三方UI组件还是自定义组件都可实现自动按需导入以及`TS`语法提示 25 | 26 | ## 项目目录 27 | 28 | ```js 29 | ├── .husky // husky git hooks配置目录 30 | ├── _ // husky 脚本生成的目录文件 31 | ├── commit-msg // commit-msg钩子,用于验证 message格式 32 | ├── pre-commit // pre-commit钩子,主要是和eslint配合 33 | ├── config // 全局配置文件 34 | ├── vite // vite 相关配置 35 | ├── constant.ts // 项目配置 36 | ├── themeConfig.ts // 主题配置 37 | ├── dist // 默认的 build 输出目录 38 | ├── mock // 前端数据mock 39 | ├── public // vite项目下的静态目录 40 | └── src // 源码目录 41 | ├── api // 接口相关 42 | ├── assets // 公共的文件(如image、css、font等) 43 | ├── components // 项目组件 44 | ├── directives // 自定义 指令 45 | ├── enums // 自定义 常量(枚举写法) 46 | ├── hooks // 自定义 hooks 47 | ├── layout // 全局布局 48 | ├── router // 路由 49 | ├── store // 状态管理器 50 | ├── utils // 工具库 51 | ├── views // 页面模块目录 52 | ├── login // login页面模块 53 | ├── ... 54 | ├── App.vue // vue顶层文件 55 | ├── auto-imports.d.ts // unplugin-auto-import 插件生成 56 | ├── components.d.d.ts // unplugin-vue-components 插件生成 57 | ├── main.ts // 项目入口文件 58 | ├── shimes-vue.d.ts // vite默认ts类型文件 59 | ├── types // 项目type类型定义文件夹 60 | ├── .editorconfig // IDE格式规范 61 | ├── .env // 环境变量 62 | ├── .eslintignore // eslint忽略 63 | ├── .eslintrc // eslint配置文件 64 | ├── .gitignore // git忽略 65 | ├── .npmrc // npm配置文件 66 | ├── .prettierignore // prettierc忽略 67 | ├── .prettierrc // prettierc配置文件 68 | ├── index.html // 入口文件 69 | ├── LICENSE.md // LICENSE 70 | ├── package.json // package 71 | ├── pnpm-lock.yaml // pnpm-lock 72 | ├── postcss.config.js // postcss 73 | ├── README.md // README 74 | ├── tsconfig.json // typescript配置文件 75 | └── vite.config.ts // vite 76 | ``` 77 | 78 | ## 使用说明 79 | 80 | > 简要说明: 81 | > 82 | > 随着vite3.x的发布,本项目针对该依赖的相关生态做了升级,详情见分支 [feat-vite3.x](https://github.com/JS-banana/vite-vue3-ts/tree/feat-vite3.x) 83 | > 84 | > 需要指出的是vite3.x要求node14.18及以上,详情见 [从 v2 迁移](https://cn.vitejs.dev/guide/migration.html) 85 | 86 | 1. 克隆本项目 87 | 88 | ```sh 89 | git clone https://github.com/JS-banana/vite-vue3-ts.git 90 | ``` 91 | 92 | 2. 安装依赖 93 | 94 | ```sh 95 | # 推荐使用 pnpm 96 | pnpm install 97 | # 没有安装的直接安装 98 | npm install -g pnpm 99 | ``` 100 | 101 | 3. 启动项目 102 | 103 | ```sh 104 | pnpm serve 105 | # or 106 | pnpm dev 107 | ``` 108 | 109 | 4. 部署 110 | 111 | ```sh 112 | # 检查TS类型然后构建打包 113 | pnpm build:check 114 | # 跳过检查直接构建打包 115 | pnpm build 116 | # 预览 117 | pnpm preview 118 | ``` 119 | 120 | ### 数据模拟 121 | 122 | 为了实现更多元化和真实数据展示,使用了Mock+fakerjs进行数据模拟,fakerjs的功能极其强大,几乎可以定制任何类型数据,本项目里做了部分演示,源码见`mock/table.ts` 123 | 124 | ### ant-design-vue 2.x升级到3.x的说明 125 | 126 | Table组件: 127 | 128 | 在2.x版本的时候,Table组件主要通过 `columns`属性,配置字段 `slots: { customRender: 'action' }`进行自定义插槽,做到制定内容的内容,基于此,本项目Table组件封装的内部实现为`