├── .prettierignore ├── .node-version ├── apps ├── nuxt3-app │ ├── env │ │ ├── .env.qa │ │ ├── .env.development │ │ ├── .env.production │ │ ├── .env.github │ │ └── .env │ ├── .prettierignore │ ├── server │ │ └── tsconfig.json │ ├── plugins │ │ ├── mock.client.ts │ │ ├── external.ts │ │ └── error.client.ts │ ├── api │ │ ├── index.ts │ │ ├── data.ts │ │ ├── common.ts │ │ ├── base.ts │ │ └── user.ts │ ├── vite-env.d.ts │ ├── constant │ │ ├── color.ts │ │ └── index.ts │ ├── assets │ │ ├── avatar.png │ │ └── logo.png │ ├── public │ │ └── favicon.ico │ ├── components │ │ ├── layout │ │ │ ├── Router.vue │ │ │ ├── Breadcrumb.vue │ │ │ ├── Logo.vue │ │ │ ├── Nav.vue │ │ │ └── index.vue │ │ ├── exception │ │ │ ├── index.less │ │ │ ├── buttons.vue │ │ │ ├── 403.vue │ │ │ ├── 404.vue │ │ │ ├── 500.vue │ │ │ └── error-403.vue │ │ └── HelloWorld.vue │ ├── middleware │ │ ├── lastName.global.ts │ │ ├── 01.home.global.ts │ │ └── 02.router.global.ts │ ├── mock │ │ ├── utils.ts │ │ ├── index.ts │ │ ├── data.ts │ │ └── user.ts │ ├── styles │ │ └── index.less │ ├── app │ │ └── router.options.ts │ ├── layouts │ │ └── default.vue │ ├── router │ │ ├── routes │ │ │ ├── index.ts │ │ │ ├── others.ts │ │ │ └── components.ts │ │ ├── home.ts │ │ ├── index.ts │ │ └── common.ts │ ├── .editorconfig │ ├── views │ │ └── home │ │ │ └── index.vue │ ├── scripts │ │ ├── gh-pages.sh │ │ └── verifyCommit.ts │ ├── tsconfig.json │ ├── .prettierrc │ ├── store │ │ ├── store.ts │ │ ├── user.ts │ │ └── cos.ts │ ├── modules │ │ └── antd.ts │ ├── upload.js │ ├── README.md │ ├── package.json │ ├── uno.config.ts │ ├── app.vue │ └── nuxt.config.ts └── vue3-app │ ├── env │ ├── .env.qa │ ├── .env.production │ ├── .env.development │ ├── .env.github │ └── .env │ ├── .prettierignore │ ├── src │ ├── api │ │ ├── index.ts │ │ ├── data.ts │ │ ├── common.ts │ │ ├── base.ts │ │ └── user.ts │ ├── constant │ │ ├── color.ts │ │ └── index.ts │ ├── vite-env.d.ts │ ├── assets │ │ ├── logo.png │ │ └── avatar.png │ ├── components │ │ ├── layout │ │ │ ├── Router.vue │ │ │ ├── Breadcrumb.vue │ │ │ ├── Logo.vue │ │ │ ├── Nav.vue │ │ │ └── index.vue │ │ ├── exception │ │ │ ├── index.less │ │ │ ├── buttons.vue │ │ │ ├── 500.vue │ │ │ ├── 403.vue │ │ │ ├── 404.vue │ │ │ └── error-403.vue │ │ ├── HelloWorld.vue │ │ └── search │ │ │ └── demo │ │ │ └── index.vue │ ├── shims-vue.d.ts │ ├── mock │ │ ├── utils.ts │ │ ├── index.ts │ │ ├── data.ts │ │ └── user.ts │ ├── styles │ │ └── index.less │ ├── router │ │ ├── routes │ │ │ ├── index.ts │ │ │ ├── others.ts │ │ │ └── components.ts │ │ ├── common.ts │ │ ├── home.ts │ │ └── index.ts │ ├── views │ │ └── home │ │ │ └── index.vue │ ├── store │ │ ├── store.ts │ │ ├── user.ts │ │ └── cos.ts │ ├── App.vue │ └── main.ts │ ├── public │ └── favicon.ico │ ├── postcss.config.mjs │ ├── .editorconfig │ ├── index.html │ ├── scripts │ ├── gh-pages.sh │ └── verifyCommit.ts │ ├── .prettierrc │ ├── README.md │ ├── tsconfig.json │ ├── upload.js │ ├── package.json │ ├── components.d.ts │ ├── uno.config.ts │ └── vite.config.ts ├── base └── README.md ├── packages ├── components │ ├── index.ts │ ├── layout │ │ ├── Router.vue │ │ ├── Breadcrumb.vue │ │ ├── Logo.vue │ │ ├── Nav.vue │ │ └── index.vue │ ├── exception │ │ ├── index.less │ │ ├── buttons.vue │ │ ├── 403.vue │ │ ├── 404.vue │ │ └── 500.vue │ ├── phone │ │ ├── api │ │ │ └── index.ts │ │ └── index.vue │ ├── package.json │ ├── upload │ │ └── Preview.vue │ ├── search │ │ ├── props.ts │ │ ├── index.vue │ │ └── demo │ │ │ ├── columns.ts │ │ │ └── index.vue │ ├── btn-group │ │ └── index.vue │ └── select │ │ └── SelectDep.vue ├── token │ ├── index.ts │ ├── package.json │ ├── TokenContextHolder.vue │ └── utils.ts ├── lib │ ├── index.ts │ └── package.json ├── styles │ ├── theme.less │ ├── package.json │ ├── sortable.less │ ├── antd.less │ ├── reset.less │ └── index.less ├── types │ ├── enums │ │ ├── index.ts │ │ ├── websocketMsg.ts │ │ └── httpMsg.ts │ ├── package.json │ ├── index.ts │ └── requests.ts ├── directives │ ├── index.ts │ ├── package.json │ └── title.ts ├── plugins │ ├── echarts.ts │ ├── bus.ts │ ├── auth.ts │ ├── components.ts │ ├── package.json │ ├── icon.ts │ └── index.ts ├── utils │ ├── loadingBar.ts │ ├── index.ts │ ├── package.json │ ├── vue.ts │ ├── auth.ts │ ├── is │ │ └── index.ts │ ├── internal.ts │ ├── array │ │ └── index.ts │ ├── audio.ts │ ├── assist.ts │ ├── sm3 │ │ └── index.js │ ├── file.ts │ ├── datetime.ts │ └── excel.ts └── hooks │ ├── package.json │ ├── index.ts │ ├── timer.ts │ ├── permissions.ts │ ├── excel.ts │ ├── tabs.ts │ ├── router.ts │ ├── theme.ts │ ├── select.ts │ ├── cosUpload.ts │ ├── table.ts │ ├── upload.ts │ └── sortable.ts ├── .eslintignore ├── pnpm-workspace.yaml ├── .editorconfig ├── features └── components │ ├── provide │ ├── Child.vue │ └── index.vue │ ├── bus │ ├── index.vue │ ├── A.vue │ └── B.vue │ ├── router │ └── index.vue │ ├── sortable │ └── index.vue │ ├── store │ └── index.vue │ └── table │ ├── fixed.vue │ └── index.vue ├── .gitignore ├── .prettierrc ├── tsconfig.json ├── scripts └── verifyCommit.ts ├── README.md └── .eslintrc.js /.prettierignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | system 2 | -------------------------------------------------------------------------------- /apps/nuxt3-app/env/.env.qa: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/vue3-app/env/.env.qa: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /base/README.md: -------------------------------------------------------------------------------- 1 | # base 2 | -------------------------------------------------------------------------------- /packages/components/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/nuxt3-app/.prettierignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/vue3-app/.prettierignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/vue3-app/env/.env.production: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/nuxt3-app/env/.env.development: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/nuxt3-app/env/.env.production: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/vue3-app/env/.env.development: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/token/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils' 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | iconfont.js 3 | lame.min.js 4 | -------------------------------------------------------------------------------- /apps/nuxt3-app/env/.env.github: -------------------------------------------------------------------------------- 1 | VITE_APP_BASE=/vite-vue-template/ 2 | -------------------------------------------------------------------------------- /apps/vue3-app/env/.env.github: -------------------------------------------------------------------------------- 1 | VITE_APP_BASE=/vite-vue-template/ 2 | -------------------------------------------------------------------------------- /packages/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './requests' 2 | export * from './prometheus' 3 | -------------------------------------------------------------------------------- /packages/styles/theme.less: -------------------------------------------------------------------------------- 1 | :root { 2 | --dark-layout-header-background: #001529; 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/**' 3 | - 'apps/**' 4 | - 'base/**' 5 | -------------------------------------------------------------------------------- /apps/nuxt3-app/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/types/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from './httpMsg' 2 | export * from './websocketMsg' 3 | -------------------------------------------------------------------------------- /apps/nuxt3-app/plugins/mock.client.ts: -------------------------------------------------------------------------------- 1 | import '@/mock' 2 | 3 | export default defineNuxtPlugin(() => {}) 4 | -------------------------------------------------------------------------------- /apps/nuxt3-app/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data' 2 | export * from './user' 3 | export * from './common' 4 | -------------------------------------------------------------------------------- /apps/nuxt3-app/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | declare const __APP_TITLE__: string 2 | declare const __DYNAMIC_MENU__: boolean 3 | -------------------------------------------------------------------------------- /apps/vue3-app/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data' 2 | export * from './user' 3 | export * from './common' 4 | -------------------------------------------------------------------------------- /apps/nuxt3-app/constant/color.ts: -------------------------------------------------------------------------------- 1 | export const COLORS = { 2 | 1: 'red', 3 | 2: 'blue', 4 | 3: 'blue' 5 | } 6 | -------------------------------------------------------------------------------- /apps/nuxt3-app/constant/index.ts: -------------------------------------------------------------------------------- 1 | export * from './color' 2 | export const pageSizeOptions = ['10', '20', '50'] 3 | -------------------------------------------------------------------------------- /apps/vue3-app/src/constant/color.ts: -------------------------------------------------------------------------------- 1 | export const COLORS = { 2 | 1: 'red', 3 | 2: 'blue', 4 | 3: 'blue' 5 | } 6 | -------------------------------------------------------------------------------- /apps/vue3-app/src/constant/index.ts: -------------------------------------------------------------------------------- 1 | export * from './color' 2 | export const pageSizeOptions = ['10', '20', '50'] 3 | -------------------------------------------------------------------------------- /apps/vue3-app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | declare const __APP_TITLE__: string 2 | declare const __DYNAMIC_MENU__: boolean 3 | -------------------------------------------------------------------------------- /apps/nuxt3-app/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinlei3166/vite-vue-template/HEAD/apps/nuxt3-app/assets/avatar.png -------------------------------------------------------------------------------- /apps/nuxt3-app/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinlei3166/vite-vue-template/HEAD/apps/nuxt3-app/assets/logo.png -------------------------------------------------------------------------------- /apps/vue3-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinlei3166/vite-vue-template/HEAD/apps/vue3-app/public/favicon.ico -------------------------------------------------------------------------------- /apps/nuxt3-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinlei3166/vite-vue-template/HEAD/apps/nuxt3-app/public/favicon.ico -------------------------------------------------------------------------------- /apps/vue3-app/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinlei3166/vite-vue-template/HEAD/apps/vue3-app/src/assets/logo.png -------------------------------------------------------------------------------- /apps/vue3-app/src/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinlei3166/vite-vue-template/HEAD/apps/vue3-app/src/assets/avatar.png -------------------------------------------------------------------------------- /apps/vue3-app/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | import UnoCSS from '@unocss/postcss' 2 | 3 | export default { 4 | plugins: [ 5 | UnoCSS() 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /apps/nuxt3-app/components/layout/Router.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/components/layout/Router.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /apps/vue3-app/src/components/layout/Router.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/components/exception/index.less: -------------------------------------------------------------------------------- 1 | .exception-wrap { 2 | height: 80vh; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | } 7 | -------------------------------------------------------------------------------- /apps/nuxt3-app/components/exception/index.less: -------------------------------------------------------------------------------- 1 | .exception-wrap { 2 | height: 80vh; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | } 7 | -------------------------------------------------------------------------------- /apps/nuxt3-app/plugins/external.ts: -------------------------------------------------------------------------------- 1 | import plugins from '@packages/plugins' 2 | 3 | export default defineNuxtPlugin(nuxtApp => { 4 | nuxtApp.vueApp.use(plugins) 5 | }) 6 | -------------------------------------------------------------------------------- /apps/vue3-app/src/components/exception/index.less: -------------------------------------------------------------------------------- 1 | .exception-wrap { 2 | height: 80vh; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | } 7 | -------------------------------------------------------------------------------- /apps/nuxt3-app/middleware/lastName.global.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtRouteMiddleware(async (to, from) => { 2 | if (from) { 3 | to.meta.lastName = from.name 4 | } 5 | }) 6 | -------------------------------------------------------------------------------- /apps/vue3-app/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } 6 | -------------------------------------------------------------------------------- /packages/directives/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import title from './title' 3 | 4 | export default { 5 | install: (app: App) => { 6 | app.directive('title', title) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/nuxt3-app/mock/utils.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | export const genRegexpValue = (regexp: RegExp) => Mock.mock({ regexp }).regexp 4 | 5 | export const genPhone = () => genRegexpValue(/1\d{10}/) 6 | -------------------------------------------------------------------------------- /packages/plugins/echarts.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import ECharts from 'vue-echarts' 3 | 4 | export default { 5 | install: (app: App) => { 6 | app.component('VChart', ECharts) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/vue3-app/src/mock/utils.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | export const genRegexpValue = (regexp: RegExp) => Mock.mock({ regexp }).regexp 4 | 5 | export const genPhone = () => genRegexpValue(/1\d{10}/) 6 | -------------------------------------------------------------------------------- /apps/nuxt3-app/styles/index.less: -------------------------------------------------------------------------------- 1 | // demo 样式,项目删除即可。 2 | .card { 3 | min-height: 100%; 4 | } 5 | 6 | .title { 7 | font-size: 24px; 8 | color: deeppink; 9 | margin-left: 40px; 10 | padding: 24px; 11 | } 12 | -------------------------------------------------------------------------------- /apps/vue3-app/src/styles/index.less: -------------------------------------------------------------------------------- 1 | // demo 样式,项目删除即可。 2 | .card { 3 | min-height: 100%; 4 | } 5 | 6 | .title { 7 | font-size: 24px; 8 | color: deeppink; 9 | margin-left: 40px; 10 | padding: 24px; 11 | } 12 | -------------------------------------------------------------------------------- /apps/nuxt3-app/app/router.options.ts: -------------------------------------------------------------------------------- 1 | import { routes as manualRoutes } from '../router' 2 | 3 | import type { RouterConfig } from '@nuxt/schema' 4 | 5 | export default { 6 | routes: _routes => manualRoutes 7 | } 8 | -------------------------------------------------------------------------------- /apps/nuxt3-app/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@packages/types", 3 | "version": "1.0.0", 4 | "description": "@packages/types", 5 | "main": "index.ts", 6 | "keywords": [], 7 | "author": "君惜", 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /packages/styles/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@packages/styles", 3 | "version": "1.0.0", 4 | "description": "@packages/styles", 5 | "main": "index.ts", 6 | "keywords": [], 7 | "license": "MIT", 8 | "dependencies": {} 9 | } 10 | -------------------------------------------------------------------------------- /packages/directives/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@packages/directives", 3 | "version": "1.0.0", 4 | "description": "@packages/directives", 5 | "main": "index.ts", 6 | "keywords": [], 7 | "author": "君惜", 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /packages/utils/loadingBar.ts: -------------------------------------------------------------------------------- 1 | import NProgress from 'nprogress' 2 | import 'nprogress/nprogress.css' 3 | 4 | export function loadingStart() { 5 | NProgress.start() 6 | } 7 | 8 | export function loadingEnd() { 9 | NProgress.done() 10 | } 11 | -------------------------------------------------------------------------------- /apps/nuxt3-app/api/data.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, Config } from '@packages/types' 2 | import { requests } from './base' 3 | 4 | export function getList(config?: Config): Promise { 5 | return requests.get('/api/mock/data/list', config) 6 | } 7 | -------------------------------------------------------------------------------- /apps/nuxt3-app/router/routes/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | import components from './components' 4 | import others from './others' 5 | 6 | const routes: RouteRecordRaw[] = [...components, ...others] 7 | 8 | export default routes 9 | -------------------------------------------------------------------------------- /apps/vue3-app/src/api/data.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, Config } from '@packages/types' 2 | import { requests } from './base' 3 | 4 | export function getList(config?: Config): Promise { 5 | return requests.get('/api/mock/data/list', config) 6 | } 7 | -------------------------------------------------------------------------------- /apps/vue3-app/src/router/routes/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | import components from './components' 4 | import others from './others' 5 | 6 | const routes: RouteRecordRaw[] = [...components, ...others] 7 | 8 | export default routes 9 | -------------------------------------------------------------------------------- /packages/plugins/bus.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import mitt from 'mitt' 3 | 4 | export default { 5 | install: (app: App) => { 6 | const bus = mitt() 7 | app.provide('bus', bus) 8 | app.config.globalProperties.bus = mitt() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /packages/directives/title.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectDirective } from 'vue' 2 | 3 | // usage: v-title="'v-title'" 4 | export default { 5 | mounted(el, binding, vnode, prevNnode) { 6 | console.log(binding) 7 | document.title = binding.value 8 | } 9 | } as ObjectDirective 10 | -------------------------------------------------------------------------------- /packages/types/enums/websocketMsg.ts: -------------------------------------------------------------------------------- 1 | export enum websocketMsg { 2 | errTitle = '提示', 3 | notSupport = '当前浏览器 Not support websocket', 4 | deviceOut = 'Ukey设备已拔出,请重新登录', 5 | timeOut = '设备超时,请重新登录', 6 | socketCloseErr = '未检测到驱动,请启动UKey客户端', 7 | VersionErr = 'UKey客户端版本过低,请安装新版本' 8 | } 9 | -------------------------------------------------------------------------------- /apps/nuxt3-app/api/common.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, Config } from '@packages/types' 2 | import { requests } from './base' 3 | 4 | export function getQcloudTmpkeys(data: Request, config?: Config): Promise { 5 | return requests.post('/api/getQcloudTmpkeys', data, config) 6 | } 7 | -------------------------------------------------------------------------------- /packages/components/phone/api/index.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Config } from '@packages/types' 2 | // @ts-ignore 3 | import { requests } from '@/api/base' 4 | 5 | // 查看手机号 6 | export const getPhone = (config?: Config): Promise => 7 | requests.get('/user/user/getUserIdCartAndPhone', config) 8 | -------------------------------------------------------------------------------- /apps/nuxt3-app/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /apps/vue3-app/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /apps/vue3-app/src/api/common.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, Config } from '@packages/types' 2 | import { requests } from './base' 3 | 4 | export function getQcloudTmpkeys(data: Request, config?: Config): Promise { 5 | return requests.post('/api/getQcloudTmpkeys', data, config) 6 | } 7 | -------------------------------------------------------------------------------- /apps/nuxt3-app/env/.env: -------------------------------------------------------------------------------- 1 | VITE_APP_TITLE='Vue App' 2 | VITE_LOGIN_TITLE='Vue App' 3 | VITE_DYNAMIC_MENU=false 4 | VITE_APP_BASE=/ 5 | VITE_API_URL='' 6 | VITE_APP_STORAGE_KEY_PREFIX="vmt" 7 | VITE_PROMETHEUS_SERVER='http://192.168.1.1:9090' 8 | VITE_PROXY_TARGET='http://192.168.1.1:5080' 9 | #VITE_OUTDIR=.nuxt 10 | -------------------------------------------------------------------------------- /apps/vue3-app/env/.env: -------------------------------------------------------------------------------- 1 | VITE_APP_TITLE='Vue App' 2 | VITE_LOGIN_TITLE='Vue App' 3 | VITE_DYNAMIC_MENU=false 4 | VITE_APP_BASE=/ 5 | VITE_API_URL='' 6 | VITE_APP_STORAGE_KEY_PREFIX="vmt" 7 | VITE_PROMETHEUS_SERVER='http://192.168.1.1:9090' 8 | VITE_PROXY_TARGET='http://192.168.1.1:5080' 9 | VITE_OUTDIR=dist 10 | -------------------------------------------------------------------------------- /packages/components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@packages/components", 3 | "version": "1.0.0", 4 | "description": "@packages/components", 5 | "main": "index.ts", 6 | "keywords": [], 7 | "author": "君惜", 8 | "license": "MIT", 9 | "dependencies": { 10 | "@packages/utils": "workspace:*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/plugins/auth.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import type { App } from 'vue' 3 | import { auth, pageAuth } from '@packages/utils' 4 | 5 | export { auth, pageAuth } 6 | 7 | export default { 8 | install: (app: App) => { 9 | app.provide('auth', auth) 10 | app.config.globalProperties.auth = auth 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/nuxt3-app/components/exception/buttons.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/nuxt3-app/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/vue3-app/src/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/plugins/components.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import Search from '@packages/components/search/index.vue' 3 | import Phone from '@packages/components/phone/index.vue' 4 | 5 | export default { 6 | install: (app: App) => { 7 | app.component('Search', Search) 8 | app.component('Phone', Phone) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@packages/hooks", 3 | "version": "1.0.0", 4 | "description": "@packages/hooks", 5 | "main": "index.ts", 6 | "keywords": [], 7 | "author": "君惜", 8 | "license": "MIT", 9 | "dependencies": { 10 | "@packages/types": "workspace:*", 11 | "@packages/utils": "workspace:*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/types/enums/httpMsg.ts: -------------------------------------------------------------------------------- 1 | export enum httpMsg { 2 | errorTitle = '提示', 3 | errorMsg = '系统错误', 4 | '请求错误' = 400, 5 | '未授权,请重新登录' = 401, 6 | '拒绝访问' = 403, 7 | '未找到该资源' = 404, 8 | '请求超时' = 408, 9 | '服务器错误' = 500, 10 | '服务未实现' = 501, 11 | '网络错误' = 502, 12 | '服务不可用' = 503, 13 | '网络超时' = 504, 14 | 'HTTP版本不受支持' = 505 15 | } 16 | -------------------------------------------------------------------------------- /packages/plugins/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@packages/plugins", 3 | "version": "1.0.0", 4 | "description": "@packages/plugins", 5 | "main": "index.ts", 6 | "keywords": [], 7 | "author": "君惜", 8 | "license": "MIT", 9 | "dependencies": { 10 | "@packages/components": "workspace:*", 11 | "@packages/utils": "workspace:*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/components/exception/buttons.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data' 2 | export * from './excel' 3 | export * from './permissions' 4 | export * from './router' 5 | export * from './select' 6 | export * from './table' 7 | export * from './tabs' 8 | export * from './sortable' 9 | export * from './theme' 10 | export * from './timer' 11 | export * from './upload' 12 | export * from './cosUpload' 13 | -------------------------------------------------------------------------------- /apps/vue3-app/src/components/exception/buttons.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/token/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@packages/token", 3 | "version": "1.0.0", 4 | "description": "@packages/token", 5 | "main": "index.ts", 6 | "keywords": [], 7 | "license": "MIT", 8 | "peerDependencies": { 9 | "ant-design-vue": ">=4.0.0" 10 | }, 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "ant-design-vue": "^4.1.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@packages/lib", 3 | "version": "1.0.0", 4 | "description": "@packages/lib", 5 | "main": "index.ts", 6 | "keywords": [], 7 | "author": "君惜", 8 | "license": "MIT", 9 | "dependencies": { 10 | "@packages/types": "workspace:*", 11 | "@packages/utils": "workspace:*", 12 | "axios": "^1.6.8", 13 | "dayjs": "^1.11.10" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/plugins/icon.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { createFromIconfontCN } from '@ant-design/icons-vue' 3 | 4 | const Icon = createFromIconfontCN({ 5 | // 如果在iconfont.cn里新增了icon,记得更新项目public中的iconfont.js文件 6 | scriptUrl: `${import.meta.env.VITE_APP_BASE || '/'}iconfont.js` 7 | }) 8 | 9 | export default { 10 | install: (app: App) => { 11 | app.component('Icon', Icon) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/hooks/timer.ts: -------------------------------------------------------------------------------- 1 | export function useTimer() { 2 | let timer: any 3 | 4 | // time: second 5 | const initTimer = (cb?: any, time = 0) => { 6 | clearTimer() 7 | timer = setInterval(() => { 8 | cb?.() 9 | }, time * 1000) 10 | } 11 | 12 | const clearTimer = () => { 13 | clearInterval(timer) 14 | timer = null 15 | } 16 | 17 | return { timer, initTimer, clearTimer } 18 | } 19 | -------------------------------------------------------------------------------- /packages/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import icon from './icon' 3 | import auth from './auth' 4 | import bus from './bus' 5 | import components from './components' 6 | import echarts from './echarts' 7 | 8 | export default { 9 | install: (app: App) => { 10 | app.use(icon) 11 | app.use(auth) 12 | app.use(bus) 13 | app.use(components) 14 | app.use(echarts) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './array' 2 | export * from './is' 3 | export * from './patterns' 4 | export * from './sm3' 5 | export * from './assist' 6 | export * from './auth' 7 | export * from './datetime' 8 | export * from './excel' 9 | export * from './file' 10 | export * from './internal' 11 | export * from './audio' 12 | export * from './loadingBar' 13 | export * from './util' 14 | export * from './vue' 15 | -------------------------------------------------------------------------------- /apps/vue3-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= VITE_APP_TITLE %> 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /features/components/provide/Child.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/components/exception/403.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /packages/components/exception/404.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /packages/components/exception/500.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /apps/nuxt3-app/components/exception/403.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /apps/nuxt3-app/components/exception/404.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /apps/nuxt3-app/components/exception/500.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /apps/vue3-app/src/components/exception/500.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /packages/hooks/permissions.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@packages/utils' 2 | 3 | // 权限,适用于 btn-group 和 单个按钮 4 | export function usePermissions(permissions: string[]) { 5 | const perms = permissions.reduce((prev: any, permission: string) => { 6 | prev[permission] = !!auth(permission) 7 | return prev 8 | }, {}) 9 | 10 | // hasPermission 表示至少拥有一个权限 11 | return { permissions: perms, hasPermission: Object.values(perms).some(p => p) } 12 | } 13 | -------------------------------------------------------------------------------- /apps/vue3-app/src/components/exception/403.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /apps/vue3-app/src/components/exception/404.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /apps/vue3-app/scripts/gh-pages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # 确保脚本抛出遇到的错误 4 | set -e 5 | 6 | # 查看当前目录 7 | pwd 8 | 9 | # 生成静态文件 10 | pnpm run build:github 11 | 12 | # 进入生成的文件夹 13 | cd dist 14 | 15 | # 如果是发布到自定义域名 16 | # echo 'www.example.com' > CNAME 17 | 18 | git init 19 | git add -A 20 | git commit -m 'deploy to the gh-pages' 21 | 22 | # 如果发布到 https://.github.io 23 | git push -f git@github.com:xinlei3166/vite-vue-template.git master:gh-pages 24 | -------------------------------------------------------------------------------- /apps/nuxt3-app/scripts/gh-pages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # 确保脚本抛出遇到的错误 4 | set -e 5 | 6 | # 查看当前目录 7 | pwd 8 | 9 | # 生成静态文件 10 | pnpm run build:github 11 | 12 | # 进入生成的文件夹 13 | cd dist 14 | 15 | # 如果是发布到自定义域名 16 | # echo 'www.example.com' > CNAME 17 | 18 | git init 19 | git add -A 20 | git commit -m 'deploy to the gh-pages' 21 | 22 | # 如果发布到 https://.github.io 23 | git push -f git@github.com:xinlei3166/vite-vue-template.git master:gh-pages 24 | -------------------------------------------------------------------------------- /apps/nuxt3-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "lib": ["esnext", "dom"], 7 | "types": ["vite/client", "node"], 8 | // "baseUrl": ".", 9 | // "paths": { 10 | // "features/*": ["../../features/*"] 11 | // }, 12 | // "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 13 | // "exclude": ["node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | 7 | # Nuxt dev/build outputs 8 | .output 9 | .data 10 | .nuxt 11 | .nitro 12 | .cache 13 | 14 | # local env files 15 | .env.local 16 | .env.*.local 17 | 18 | # Log files 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | pnpm-debug.log* 23 | *.log 24 | logs 25 | 26 | # Editor directories and files 27 | .idea 28 | .fleet 29 | .vscode 30 | *.suo 31 | *.ntvs* 32 | *.njsproj 33 | *.sln 34 | *.sw? 35 | -------------------------------------------------------------------------------- /packages/styles/sortable.less: -------------------------------------------------------------------------------- 1 | .sortable-wrap { 2 | margin-left: 64px; 3 | 4 | .row { 5 | display: block; 6 | width: 600px; 7 | padding: 12px 20px; 8 | background-color: #fff; 9 | border: 1px solid rgba(0, 0, 0, 0.125); 10 | } 11 | 12 | .sortable-handle { 13 | cursor: move; 14 | } 15 | 16 | .sortable-ghost { 17 | opacity: 0.5; 18 | background: #c8ebfb; 19 | } 20 | 21 | .flip-list-move { 22 | transition: transform 0.5s; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@packages/utils", 3 | "version": "1.0.0", 4 | "description": "@packages/utils", 5 | "main": "index.ts", 6 | "keywords": [], 7 | "license": "MIT", 8 | "dependencies": { 9 | "dayjs": "^1.11.10", 10 | "exceljs": "^4.4.0", 11 | "js-cookie": "^3.0.5", 12 | "nprogress": "^0.2.0", 13 | "qs": "^6.12.0" 14 | }, 15 | "devDependencies": { 16 | "@types/js-cookie": "^3.0.6", 17 | "@types/nprogress": "^0.2.3", 18 | "@types/qs": "^6.9.14" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 100 2 | semi: false 3 | singleQuote: true 4 | proseWrap: "never" 5 | arrowParens: "avoid" 6 | bracketSpacing: true 7 | #disableLanguages": ["custom"] 8 | endOfLine: "auto" 9 | eslintIntegration: true 10 | htmlWhitespaceSensitivity: "ignore" # 属性标签格式化符合预期 11 | ignorePath: ".prettierignore" 12 | jsxBracketSameLine: false 13 | jsxSingleQuote: false 14 | #parser: vue 15 | stylelintIntegration: true 16 | trailingComma: "none" 17 | tslintIntegration: false 18 | vueIndentScriptAndStyle: false 19 | embeddedLanguageFormatting: "auto" 20 | -------------------------------------------------------------------------------- /apps/nuxt3-app/.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 100 2 | semi: false 3 | singleQuote: true 4 | proseWrap: "never" 5 | arrowParens: "avoid" 6 | bracketSpacing: true 7 | #disableLanguages": ["custom"] 8 | endOfLine: "auto" 9 | eslintIntegration: true 10 | htmlWhitespaceSensitivity: "ignore" # 属性标签格式化符合预期 11 | ignorePath: ".prettierignore" 12 | jsxBracketSameLine: false 13 | jsxSingleQuote: false 14 | #parser: vue 15 | stylelintIntegration: true 16 | trailingComma: "none" 17 | tslintIntegration: false 18 | vueIndentScriptAndStyle: false 19 | embeddedLanguageFormatting: "auto" 20 | -------------------------------------------------------------------------------- /apps/vue3-app/.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 100 2 | semi: false 3 | singleQuote: true 4 | proseWrap: "never" 5 | arrowParens: "avoid" 6 | bracketSpacing: true 7 | #disableLanguages": ["custom"] 8 | endOfLine: "auto" 9 | eslintIntegration: true 10 | htmlWhitespaceSensitivity: "ignore" # 属性标签格式化符合预期 11 | ignorePath: ".prettierignore" 12 | jsxBracketSameLine: false 13 | jsxSingleQuote: false 14 | #parser: vue 15 | stylelintIntegration: true 16 | trailingComma: "none" 17 | tslintIntegration: false 18 | vueIndentScriptAndStyle: false 19 | embeddedLanguageFormatting: "auto" 20 | -------------------------------------------------------------------------------- /packages/utils/vue.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'vue' 2 | 3 | // 滚动到页面底部 4 | export const scroll = (el = '.layout-content-wrap', scrollHeight?: number) => { 5 | nextTick(() => { 6 | const element = document.querySelector(el) as HTMLElement 7 | element.scrollTop = typeof scrollHeight === 'number' ? scrollHeight : element.scrollHeight 8 | }) 9 | } 10 | 11 | export function onbeforeunload(e: any) { 12 | const confirmationMessage = '系统可能不会保存您所做的更改。' 13 | 14 | ;(e || window.event).returnValue = confirmationMessage 15 | 16 | return confirmationMessage 17 | } 18 | -------------------------------------------------------------------------------- /apps/nuxt3-app/plugins/error.client.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtPlugin(nuxtApp => { 2 | if (import.meta.env.PROD) { 3 | nuxtApp.vueApp.config.errorHandler = (err, vm, info) => { 4 | console.group('vue_global_error') 5 | console.log('捕获到异常:', { err, vm, info }) 6 | console.groupEnd() 7 | } 8 | 9 | window.onerror = function (message, source, lineno, colno, error) { 10 | console.group('window_global_error') 11 | console.log('捕获到异常:', { message, source, lineno, colno, error }) 12 | console.groupEnd() 13 | } 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /features/components/bus/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/components/upload/Preview.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | -------------------------------------------------------------------------------- /features/components/router/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "allowJs": true, 12 | "noImplicitAny": true, 13 | "lib": ["esnext", "dom"], 14 | "types": ["vite/client", "node"], 15 | // "baseUrl": ".", 16 | // "paths": { 17 | // "@/*": ["./src/*"] 18 | // } 19 | }, 20 | "include": ["packages/**/*.ts", "packages/**/*.d.ts", "packages/**/*.tsx", "packages/**/*.vue"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/utils/auth.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import { useUserStore } from '@/store/user' 3 | 4 | export const auth = (permission: string) => { 5 | const userStore = useUserStore() 6 | const arr = userStore.permissions 7 | const permissions = arr.map(x => x.menuCode) 8 | return permissions.includes(permission) 9 | } 10 | 11 | export const pageAuth = async (permission: string) => { 12 | const userStore = useUserStore() 13 | let arr = userStore.permissions 14 | if (!arr.length) { 15 | await userStore.setPermissions() 16 | arr = userStore.permissions 17 | } 18 | const permissions = arr.map(x => x.menuCode) 19 | return permissions.includes(permission) 20 | } 21 | -------------------------------------------------------------------------------- /apps/vue3-app/README.md: -------------------------------------------------------------------------------- 1 | # vue3-app 2 | 3 | vue3 app 4 | 5 | ## Framework packages 6 | 7 | Vite5 + Vue3 + Pinia + Vue Router + TypeScript + Ant Design Vue 8 | 9 | ## Package Manager 10 | pnpm 11 | 12 | ## Quick start 13 | ``` 14 | git clone https://github.com/xinlei3166/vite-vue-template.git 15 | ``` 16 | 17 | ## Project setup 18 | ``` 19 | pnpm install 20 | ``` 21 | 22 | ### Compiles and hot-reloads for development 23 | ``` 24 | pnpm run dev 25 | ``` 26 | 27 | ### Compiles and minifies for production 28 | ``` 29 | pnpm run build 30 | ``` 31 | 32 | ### Run your tests 33 | ``` 34 | pnpm run test 35 | ``` 36 | 37 | ### Lints and fixes files 38 | ``` 39 | pnpm run lint 40 | ``` 41 | 42 | -------------------------------------------------------------------------------- /apps/nuxt3-app/mock/index.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import qs from 'qs' 3 | import { getUserMenu, getPermissions, getUserinfo, login, logout } from './user' 4 | import { getList } from './data' 5 | 6 | Mock.setup({ 7 | timeout: 300 8 | }) 9 | 10 | // 拦截ajax请求,配置mock的数据 11 | Mock.mock(RegExp('/api/mock/data/list' + '.*'), 'get', getList()) 12 | Mock.mock(RegExp('/api/mock/user/menu' + '.*'), 'get', getUserMenu()) 13 | Mock.mock(RegExp('/api/mock/user/permissions' + '.*'), 'get', getPermissions()) 14 | Mock.mock(RegExp('/api/mock/user/info' + '.*'), 'get', getUserinfo()) 15 | Mock.mock('/api/mock/user/login', 'post', login()) 16 | Mock.mock('/api/mock/user/logout', 'post', logout()) 17 | -------------------------------------------------------------------------------- /apps/vue3-app/src/mock/index.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import qs from 'qs' 3 | import { getUserMenu, getPermissions, getUserinfo, login, logout } from './user' 4 | import { getList } from './data' 5 | 6 | Mock.setup({ 7 | timeout: 300 8 | }) 9 | 10 | // 拦截ajax请求,配置mock的数据 11 | Mock.mock(RegExp('/api/mock/data/list' + '.*'), 'get', getList()) 12 | Mock.mock(RegExp('/api/mock/user/menu' + '.*'), 'get', getUserMenu()) 13 | Mock.mock(RegExp('/api/mock/user/permissions' + '.*'), 'get', getPermissions()) 14 | Mock.mock(RegExp('/api/mock/user/info' + '.*'), 'get', getUserinfo()) 15 | Mock.mock('/api/mock/user/login', 'post', login()) 16 | Mock.mock('/api/mock/user/logout', 'post', logout()) 17 | -------------------------------------------------------------------------------- /apps/vue3-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "allowJs": true, 12 | "noImplicitAny": true, 13 | "lib": ["esnext", "dom"], 14 | "types": ["vite/client", "node"], 15 | "baseUrl": ".", 16 | "paths": { 17 | "@/*": ["./src/*"], 18 | "features/*": ["../../features/*"] 19 | } 20 | }, 21 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/utils/is/index.ts: -------------------------------------------------------------------------------- 1 | type typeName = 'Object' | 'String' | 'Function' | 'Array' 2 | 3 | export function is(val: unknown, type: typeName) { 4 | return toString.call(val) === `[object ${type}]` 5 | } 6 | 7 | export function isObject(val: any): val is Record { 8 | return val !== null && is(val, 'Object') 9 | } 10 | 11 | export function isString(val: any): val is Record { 12 | return val !== null && is(val, 'String') 13 | } 14 | 15 | export function isFunction(val: any): val is Record { 16 | return val !== null && is(val, 'Function') 17 | } 18 | 19 | export function isArray(val: any): val is Record { 20 | return val !== null && is(val, 'Array') 21 | } 22 | -------------------------------------------------------------------------------- /apps/vue3-app/src/store/store.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export interface StoreState { 4 | userinfo: { 5 | name: string 6 | } 7 | } 8 | 9 | export const useStore = defineStore('store', { 10 | state: (): StoreState => ({ userinfo: { name: '' } }), 11 | getters: { 12 | Userinfo(state) { 13 | return state.userinfo 14 | } 15 | }, 16 | actions: { 17 | setUserinfo(userinfo: Partial) { 18 | this.userinfo = { ...this.userinfo, ...userinfo } 19 | } 20 | }, 21 | persist: { 22 | enabled: true, 23 | strategies: [ 24 | { 25 | key: 'store', 26 | storage: localStorage 27 | } 28 | ] 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /apps/nuxt3-app/router/home.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | const Layout = () => import('@/components/layout/index.vue') 4 | 5 | export default [ 6 | { 7 | path: '/', 8 | component: Layout, 9 | meta: { 10 | hidden: true 11 | }, 12 | children: [ 13 | { 14 | path: '', 15 | name: 'home', 16 | meta: { 17 | title: '首页' 18 | }, 19 | component: () => import('@/views/home/index.vue') 20 | } 21 | ] 22 | }, 23 | { 24 | path: '/login', 25 | name: 'login', 26 | meta: { 27 | title: '登录' 28 | }, 29 | component: () => import('@/views/login/index.vue') 30 | } 31 | ] as any as RouteRecordRaw[] 32 | -------------------------------------------------------------------------------- /apps/nuxt3-app/store/store.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export interface StoreState { 4 | userinfo: { 5 | name: string 6 | } 7 | } 8 | 9 | export const useStore = defineStore('store', { 10 | state: (): StoreState => ({ userinfo: { name: '' } }), 11 | getters: { 12 | Userinfo(state) { 13 | return state.userinfo 14 | } 15 | }, 16 | actions: { 17 | setUserinfo(userinfo: Partial) { 18 | this.userinfo = { ...this.userinfo, ...userinfo } 19 | } 20 | }, 21 | persist: { 22 | enabled: true, 23 | strategies: [ 24 | { 25 | key: 'store', 26 | storage: persistedState.localStorage 27 | } 28 | ] 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /packages/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import type { TablePaginationConfig } from 'ant-design-vue' 3 | 4 | export * from './requests' 5 | 6 | export interface Pagination extends TablePaginationConfig { 7 | [key: string]: any 8 | } 9 | 10 | // tab list 11 | export interface TabList { 12 | key: string // component name 13 | permission?: string 14 | name?: string // cn name 15 | tab?: string 16 | slots?: Object 17 | } 18 | 19 | export type TabListRef = Ref 20 | 21 | // visible 22 | export interface Visible extends Record { 23 | visible: boolean 24 | title?: string 25 | id?: number 26 | mode?: 'new' | 'edit' | string 27 | data?: any[] | Record 28 | } 29 | -------------------------------------------------------------------------------- /apps/nuxt3-app/router/index.ts: -------------------------------------------------------------------------------- 1 | import home from './home' 2 | import common from './common' 3 | import _routes from './routes' 4 | 5 | export const checkExternalWhiteRoute = (routePath: string) => { 6 | let hasExternalWhiteRoute = false 7 | for (const w of externalWhiteList) { 8 | if (routePath.startsWith(w)) { 9 | hasExternalWhiteRoute = true 10 | break 11 | } 12 | } 13 | return hasExternalWhiteRoute 14 | } 15 | 16 | export const externalWhiteList = ['/visualScreen'] 17 | 18 | export const whiteList = ['/login'] 19 | 20 | export const routes = __DYNAMIC_MENU__ ? [...home, ...common] : [...home, ..._routes, ...common] 21 | 22 | // 兼容 import router from '@/router' 23 | export default window?.$nuxt?.$router 24 | -------------------------------------------------------------------------------- /features/components/bus/A.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /apps/nuxt3-app/api/base.ts: -------------------------------------------------------------------------------- 1 | import { message } from 'ant-design-vue' 2 | import router from '@/router' 3 | import { useRequests } from '@packages/lib' 4 | import { removeToken } from '@packages/utils' 5 | import { useUserStore } from '@/store/user' 6 | import { useMenuStore } from '@/store/menu' 7 | 8 | const errorHandler = (msg: string) => { 9 | const userStore = useUserStore() 10 | const menuStore = useMenuStore() 11 | removeToken() 12 | userStore.cleanup() 13 | menuStore.cleanup() 14 | message.destroy() 15 | message.error(msg) 16 | setTimeout(() => { 17 | router.push('/login') 18 | }, 50) 19 | } 20 | 21 | const baseURL = import.meta.env.VITE_API_URL 22 | export const requests = useRequests({ baseURL, errorHandler }) 23 | -------------------------------------------------------------------------------- /apps/vue3-app/src/api/base.ts: -------------------------------------------------------------------------------- 1 | import { message } from 'ant-design-vue' 2 | import router from '@/router' 3 | import { useRequests } from '@packages/lib' 4 | import { removeToken } from '@packages/utils' 5 | import { useUserStore } from '@/store/user' 6 | import { useMenuStore } from '@/store/menu' 7 | 8 | const errorHandler = (msg: string) => { 9 | const userStore = useUserStore() 10 | const menuStore = useMenuStore() 11 | removeToken() 12 | userStore.cleanup() 13 | menuStore.cleanup() 14 | message.destroy() 15 | message.error(msg) 16 | setTimeout(() => { 17 | router.push('/login') 18 | }, 50) 19 | } 20 | 21 | const baseURL = import.meta.env.VITE_API_URL 22 | export const requests = useRequests({ baseURL, errorHandler }) 23 | -------------------------------------------------------------------------------- /apps/nuxt3-app/api/user.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, Config } from '@packages/types' 2 | import { requests } from './base' 3 | 4 | // 用户当前菜单列表 5 | export const getUserMenu = (config?: Config): Promise => 6 | requests.get('/api/mock/user/menu', config) 7 | 8 | // 权限列表-扁平化 9 | export const getPermissions = (config?: Config): Promise => 10 | requests.get('/api/mock/user/permissions', config) 11 | 12 | // 用户当前菜单列表 13 | export const getUserinfo = (config?: Config): Promise => 14 | requests.get('/api/mock/user/info', config) 15 | 16 | // 普通用户登录 17 | export const login = (data?: Request) => requests.post('/api/mock/user/login', data) 18 | 19 | // 退出登录 20 | export const logout = () => requests.post('/api/mock/user/logout') 21 | -------------------------------------------------------------------------------- /apps/vue3-app/src/api/user.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, Config } from '@packages/types' 2 | import { requests } from './base' 3 | 4 | // 用户当前菜单列表 5 | export const getUserMenu = (config?: Config): Promise => 6 | requests.get('/api/mock/user/menu', config) 7 | 8 | // 权限列表-扁平化 9 | export const getPermissions = (config?: Config): Promise => 10 | requests.get('/api/mock/user/permissions', config) 11 | 12 | // 用户当前菜单列表 13 | export const getUserinfo = (config?: Config): Promise => 14 | requests.get('/api/mock/user/info', config) 15 | 16 | // 普通用户登录 17 | export const login = (data?: Request) => requests.post('/api/mock/user/login', data) 18 | 19 | // 退出登录 20 | export const logout = () => requests.post('/api/mock/user/logout') 21 | -------------------------------------------------------------------------------- /apps/vue3-app/src/router/routes/others.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | const Layout = () => import('@/components/layout/index.vue') 4 | 5 | const routes: RouteRecordRaw[] = [ 6 | { 7 | path: '/others', 8 | name: 'others', 9 | meta: { 10 | title: '其他组件', 11 | link: true, 12 | icon: 'icon-appstore' 13 | }, 14 | component: Layout, 15 | redirect: { name: 'vue' }, 16 | children: [ 17 | { 18 | path: '', 19 | name: 'vue', 20 | meta: { 21 | title: 'vue', 22 | icon: 'icon-setting' 23 | }, 24 | component: () => import('@/components/HelloWorld.vue') 25 | } 26 | ] 27 | } 28 | ] 29 | 30 | export default routes 31 | -------------------------------------------------------------------------------- /packages/hooks/excel.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import type { ExcelColumn } from '@packages/utils' 3 | import { exportExcel as _exportExcel } from '@packages/utils' 4 | 5 | interface Options { 6 | excelFields: ExcelColumn[] 7 | getExcelData: Function 8 | } 9 | 10 | export function useExcel({ excelFields, getExcelData }: Options) { 11 | const downloadLoading = ref(false) 12 | const excelData = ref([]) 13 | 14 | const exportExcel = async (filename: string) => { 15 | excelData.value = await getExcelData(downloadLoading) 16 | if (!excelData.value) return 17 | await _exportExcel({ filename, columns: excelFields, data: excelData.value }) 18 | return true 19 | } 20 | 21 | return { downloadLoading, excelData, exportExcel } 22 | } 23 | -------------------------------------------------------------------------------- /packages/components/search/props.ts: -------------------------------------------------------------------------------- 1 | export const props = { 2 | inline: { type: Boolean, default: false }, 3 | columns: { type: Array, default: () => [] }, 4 | model: { type: Object, default: () => ({}) }, 5 | labelAlign: { type: String, default: 'right' }, // left | right 6 | labelWidth: { type: String, default: 'auto' }, 7 | showSearchBtn: { type: Boolean, default: true }, 8 | showResetBtn: { type: Boolean, default: true }, 9 | showBtn: { type: Boolean, default: true }, 10 | searchBtnLabel: { type: String, default: '查询' }, // 查询, 搜索 11 | resetBtnLabel: { type: String, default: '重置' }, 12 | btnAlign: { type: String, default: 'left' }, // left/right 13 | btnClass: { type: String, default: '' }, 14 | btnStyle: { type: Object, default: () => ({}) } 15 | } 16 | -------------------------------------------------------------------------------- /apps/nuxt3-app/router/routes/others.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | // const Layout = () => import('@/components/layout/index.vue') 4 | const Layout = () => import('@/components/layout/Router.vue') 5 | 6 | const routes: RouteRecordRaw[] = [ 7 | { 8 | path: '/others', 9 | name: 'others', 10 | meta: { 11 | title: '其他组件', 12 | link: true, 13 | icon: 'icon-appstore' 14 | }, 15 | component: Layout, 16 | redirect: { name: 'vue' }, 17 | children: [ 18 | { 19 | path: '', 20 | name: 'vue', 21 | meta: { 22 | title: 'vue', 23 | icon: 'icon-setting' 24 | }, 25 | component: () => import('@/components/HelloWorld.vue') 26 | } 27 | ] 28 | } 29 | ] 30 | 31 | export default routes 32 | -------------------------------------------------------------------------------- /features/components/bus/B.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /packages/styles/antd.less: -------------------------------------------------------------------------------- 1 | .ant-dropdown-menu-title-content > a { 2 | color: inherit; 3 | transition: all 0.3s; 4 | } 5 | 6 | // card tabs style 7 | .tabs-card .ant-tabs-tab { 8 | font-size: 14px !important; 9 | } 10 | 11 | .tabs-card .ant-tabs-tab { 12 | padding: 16px 0 !important; 13 | } 14 | 15 | // tree style 16 | @tree-bg-color: #e6f1ff; 17 | //.ant-tree { 18 | // .ant-tree-node-content-wrapper.ant-tree-node-selected { 19 | // background-color: @tree-bg-color; 20 | // } 21 | //} 22 | //.ant-select-tree { 23 | // .ant-select-tree-node-content-wrapper.ant-select-tree-node-selected { 24 | // background-color: @tree-bg-color; 25 | // } 26 | //} 27 | 28 | // text btn style 29 | .a-text-btn { 30 | padding: 0; 31 | border: none; 32 | } 33 | .a-text-btn + .a-text-btn { 34 | margin-left: 8px; 35 | } 36 | -------------------------------------------------------------------------------- /apps/nuxt3-app/modules/antd.ts: -------------------------------------------------------------------------------- 1 | import { extractStyle } from 'ant-design-vue/lib/_util/static-style-extract' 2 | import { defineNuxtModule } from 'nuxt/kit' 3 | 4 | export default defineNuxtModule({ 5 | // meta: { 6 | // name: 'nuxt-antd-css' 7 | // }, 8 | // setup(_, nuxt) { 9 | // // spa 时并不需要注入 css 10 | // if (!nuxt.options.ssr) { 11 | // return 12 | // } 13 | // 14 | // // generate 时,通过 replace 修改 process.env.NODE_ENV 为 production (默认为 prerender) 15 | // // 而 antd 生成 css 前缀时依赖 process.env.NODE_ENV 16 | // if (nuxt.options.dev === false && nuxt.options.nitro.static) { 17 | // nuxt.options.nitro.replace ??= {} 18 | // nuxt.options.nitro.replace['process.env.NODE_ENV'] = "'production'" 19 | // } 20 | // nuxt.options.app.head?.style?.push(extractStyle()) 21 | // } 22 | }) 23 | -------------------------------------------------------------------------------- /packages/utils/internal.ts: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | // @ts-ignore 4 | import router from '@/router' 5 | // @ts-ignore 6 | import { useUserStore } from '@/store/user' 7 | // @ts-ignore 8 | import { useMenuStore } from '@/store/menu' 9 | 10 | const storageKeyPrefix = import.meta.env.VITE_APP_STORAGE_KEY_PREFIX 11 | export const TokenKey = `${storageKeyPrefix}Token` 12 | 13 | export const setToken = (token: string) => Cookies.set(TokenKey, token) // { expires: 1 } 14 | export const getToken = () => Cookies.get(TokenKey) 15 | export const removeToken = () => { 16 | Cookies.remove(TokenKey) 17 | } 18 | 19 | // logout 20 | export const logoutCleanup = () => { 21 | const userStore = useUserStore() 22 | const menuStore = useMenuStore() 23 | removeToken() 24 | router.push('/login') 25 | userStore.cleanup() 26 | menuStore.cleanup() 27 | } 28 | -------------------------------------------------------------------------------- /apps/nuxt3-app/components/exception/error-403.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 36 | -------------------------------------------------------------------------------- /apps/vue3-app/src/components/exception/error-403.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 36 | -------------------------------------------------------------------------------- /apps/nuxt3-app/middleware/01.home.global.ts: -------------------------------------------------------------------------------- 1 | import { useMenuStore } from '@/store/menu' 2 | 3 | export default defineNuxtRouteMiddleware(async (to, from) => { 4 | // 在服务器端跳过中间件 5 | if (process.server) return 6 | // 完全在客户端跳过中间件 7 | // if (process.client) return 8 | // 或仅在初始客户端加载时跳过中间件 9 | // const nuxtApp = useNuxtApp() 10 | // if (process.client && nuxtApp.isHydrating && nuxtApp.payload.serverRendered) return 11 | if (to.path !== '/') return 12 | if (!__DYNAMIC_MENU__) return navigateTo('/components') 13 | const menuStore = useMenuStore() 14 | const routeMenus = menuStore.routeMenus.slice(0, -1) 15 | if (!routeMenus.length) { 16 | return navigateTo({ 17 | path: '/403' 18 | }) 19 | } 20 | const path = routeMenus[0].children?.length ? routeMenus[0].children[0].path : routeMenus[0].path 21 | return navigateTo({ path }) 22 | }) 23 | -------------------------------------------------------------------------------- /apps/nuxt3-app/upload.js: -------------------------------------------------------------------------------- 1 | const to = require('await-to-js').default 2 | const Client = require('ssh2-sftp-client') 3 | const sftp = new Client() 4 | const srcDir = `${__dirname}/dist` 5 | const dstDir = `/data/nginx/html/vite-vue-template` 6 | 7 | const config = { 8 | host: '192.168.1.1', 9 | username: '', 10 | password: '' 11 | } 12 | 13 | async function run() { 14 | let err 15 | ;[err] = await to(sftp.connect(config)) 16 | if (err) { 17 | console.log(err, `连接失败`) 18 | return 19 | } 20 | ;[err] = await to(sftp.rmdir(dstDir, true)) 21 | ;[err] = await to(sftp.uploadDir(srcDir, dstDir)) 22 | if (err) { 23 | console.log(err, `上传失败`) 24 | sftp.end() 25 | return 26 | } 27 | console.log(`Source Dir: ${srcDir}`) 28 | console.log(`Dest Dir: ${dstDir}`) 29 | console.log(`Host: ${config.host}`) 30 | console.log(`上传成功`) 31 | sftp.end() 32 | } 33 | 34 | run() 35 | -------------------------------------------------------------------------------- /apps/vue3-app/upload.js: -------------------------------------------------------------------------------- 1 | const to = require('await-to-js').default 2 | const Client = require('ssh2-sftp-client') 3 | const sftp = new Client() 4 | const srcDir = `${__dirname}/dist` 5 | const dstDir = `/data/nginx/html/vite-vue-template` 6 | 7 | const config = { 8 | host: '192.168.1.1', 9 | username: '', 10 | password: '' 11 | } 12 | 13 | async function run() { 14 | let err 15 | ;[err] = await to(sftp.connect(config)) 16 | if (err) { 17 | console.log(err, `连接失败`) 18 | return 19 | } 20 | ;[err] = await to(sftp.rmdir(dstDir, true)) 21 | ;[err] = await to(sftp.uploadDir(srcDir, dstDir)) 22 | if (err) { 23 | console.log(err, `上传失败`) 24 | sftp.end() 25 | return 26 | } 27 | console.log(`Source Dir: ${srcDir}`) 28 | console.log(`Dest Dir: ${dstDir}`) 29 | console.log(`Host: ${config.host}`) 30 | console.log(`上传成功`) 31 | sftp.end() 32 | } 33 | 34 | run() 35 | -------------------------------------------------------------------------------- /apps/nuxt3-app/README.md: -------------------------------------------------------------------------------- 1 | # nuxt3-app 2 | 3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Framework packages 6 | 7 | Vite5 + Nuxt3 + Pinia + Vue Router + TypeScript + Ant Design Vue 8 | 9 | ## Package Manager 10 | pnpm 11 | 12 | ## Quick start 13 | ``` 14 | git clone https://github.com/xinlei3166/vite-vue-template.git 15 | ``` 16 | 17 | ## Project setup 18 | ``` 19 | pnpm install 20 | ``` 21 | 22 | ### Compiles and hot-reloads for development 23 | 24 | Start the development server on `http://localhost:3000`: 25 | 26 | ``` 27 | pnpm run dev 28 | ``` 29 | 30 | ### Compiles and minifies for production 31 | ``` 32 | pnpm run build 33 | ``` 34 | 35 | ### Locally preview production build 36 | ``` 37 | pnpm run preview 38 | ``` 39 | 40 | ### Run your tests 41 | ``` 42 | pnpm run test 43 | ``` 44 | 45 | ### Lints and fixes files 46 | ``` 47 | pnpm run lint 48 | ``` 49 | -------------------------------------------------------------------------------- /packages/utils/array/index.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * 并集 3 | * a: [1,2,3], b: [3,5,2] 4 | * unionSet(a,b) = [1,2,3,5] 5 | * */ 6 | export function unionSet(a: [], b: []) { 7 | const a1 = new Set(a) 8 | const b1 = new Set(b) 9 | return new Set([...a1, ...b1]) 10 | } 11 | 12 | /*** 13 | * 交集 14 | * a: [1,2,3], b: [3,5,2] 15 | * intersectionSet(a,b) = [2,3] 16 | * */ 17 | export function intersectionSet(a: [], b: []) { 18 | const a1 = new Set(a) 19 | const b1 = new Set(b) 20 | return new Set([...a1].filter(x => b1.has(x))) 21 | } 22 | 23 | /*** 24 | * 差集 25 | * a: [1,2,3], b: [3,5,2] 26 | * differenceABSet(a,b) = [1] 27 | * */ 28 | export function differenceABSet(a: [], b: []) { 29 | const a1 = new Set(a) 30 | const b1 = new Set(b) 31 | return new Set([...a1].filter(x => !b1.has(x))) 32 | } 33 | 34 | // 去重 35 | export function arrayDedupe(array: any[]) { 36 | const a1 = new Set(array) 37 | return Array.from(a1) 38 | } 39 | -------------------------------------------------------------------------------- /features/components/provide/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /packages/hooks/tabs.ts: -------------------------------------------------------------------------------- 1 | import { ref, isRef } from 'vue' 2 | import { useRouter } from 'vue-router' 3 | import type { TabList, TabListRef } from '@packages/types' 4 | import { auth, typeOf } from '@packages/utils' 5 | 6 | export function useTabs( 7 | tabs: TabList[] | TabListRef, 8 | verify = true, 9 | emptyHandle: Boolean | Function = true 10 | ) { 11 | const _tabs = isRef(tabs) ? tabs.value : tabs 12 | const tabList = ref(_tabs.filter(tab => !verify || auth(tab.permission!))) 13 | 14 | const activeTabKey = ref(tabList.value[0]?.key) 15 | 16 | const onTabChange = (key: any) => { 17 | activeTabKey.value = key 18 | } 19 | 20 | const to403 = () => { 21 | const router = useRouter() 22 | router.push('/403') 23 | } 24 | 25 | if (verify && emptyHandle && !tabList.value.length) { 26 | const cb = typeOf(emptyHandle) === 'function' ? emptyHandle : to403 27 | // @ts-ignore 28 | cb() 29 | } 30 | 31 | return { activeTabKey, tabList, onTabChange } 32 | } 33 | -------------------------------------------------------------------------------- /packages/token/TokenContextHolder.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 30 | 31 | 36 | -------------------------------------------------------------------------------- /features/components/sortable/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 33 | 34 | 37 | -------------------------------------------------------------------------------- /packages/hooks/router.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'vue-router' 2 | 3 | export type NavigationType = 'push' | 'replace' 4 | 5 | export function useOpenRoute(type: NavigationType = 'push') { 6 | const router = useRouter() 7 | 8 | const openRoute = ( 9 | name: string, 10 | params: Record = {}, 11 | query: Record = {} 12 | ) => { 13 | ;(router as any)[type]({ name, params, query }) 14 | } 15 | 16 | const openTab = ( 17 | name: string, 18 | params: Record = {}, 19 | query: Record = {} 20 | ) => { 21 | const route = router.resolve({ name, params, query }) 22 | window.open(route.href) 23 | } 24 | 25 | const resolveRoute = ( 26 | name: string, 27 | params: Record = {}, 28 | query: Record = {} 29 | ) => { 30 | const route = router.resolve({ name, params, query }) 31 | return route.href 32 | } 33 | 34 | return { router, openRoute, openTab, resolveRoute } 35 | } 36 | -------------------------------------------------------------------------------- /apps/nuxt3-app/router/common.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | const Layout = () => import('@/components/layout/index.vue') 4 | 5 | const routes: RouteRecordRaw[] = [ 6 | { 7 | path: '/403', 8 | meta: { 9 | title: '403', 10 | auth: false, 11 | hidden: true 12 | }, 13 | component: () => import('@/components/exception/403.vue') 14 | }, 15 | { 16 | name: '404', 17 | path: '/404', 18 | meta: { 19 | title: '404', 20 | auth: false, 21 | hidden: true 22 | }, 23 | component: () => import('@/components/exception/404.vue') 24 | }, 25 | { 26 | name: '500', 27 | path: '/500', 28 | meta: { 29 | title: '500', 30 | auth: false, 31 | hidden: true 32 | }, 33 | component: () => import('@/components/exception/500.vue') 34 | } 35 | ] 36 | 37 | export const errorRoute: RouteRecordRaw = { 38 | path: '/:pathMatch(.*)*', 39 | redirect: { name: '404' } 40 | } 41 | 42 | export default routes 43 | -------------------------------------------------------------------------------- /apps/vue3-app/src/router/common.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | const Layout = () => import('@/components/layout/index.vue') 4 | 5 | const routes: RouteRecordRaw[] = [ 6 | { 7 | path: '/403', 8 | meta: { 9 | title: '403', 10 | auth: false, 11 | hidden: true 12 | }, 13 | component: () => import('@/components/exception/403.vue') 14 | }, 15 | { 16 | name: '404', 17 | path: '/404', 18 | meta: { 19 | title: '404', 20 | auth: false, 21 | hidden: true 22 | }, 23 | component: () => import('@/components/exception/404.vue') 24 | }, 25 | { 26 | name: '500', 27 | path: '/500', 28 | meta: { 29 | title: '500', 30 | auth: false, 31 | hidden: true 32 | }, 33 | component: () => import('@/components/exception/500.vue') 34 | } 35 | ] 36 | 37 | export const errorRoute: RouteRecordRaw = { 38 | path: '/:pathMatch(.*)*', 39 | redirect: { name: '404' } 40 | } 41 | 42 | export default routes 43 | -------------------------------------------------------------------------------- /packages/hooks/theme.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import type { Ref } from 'vue' 3 | import type { GlobalToken } from 'ant-design-vue/es/theme' 4 | 5 | export interface Theme { 6 | theme: 'dark' | 'light' 7 | layout: 'side' | 'mix' 8 | mode: 'vertical' | 'inline' | 'horizontal' 9 | width: string 10 | height: string 11 | collapsed: boolean 12 | collapsedWidth: string 13 | headerTheme: boolean 14 | showBreadcrumb: boolean 15 | token: Partial 16 | algorithm: string 17 | } 18 | 19 | const theme = ref({ 20 | theme: 'light', // light, dark 21 | layout: 'mix', // side, mix 22 | mode: 'inline', 23 | width: '240px', 24 | height: '64px', 25 | collapsed: false, 26 | collapsedWidth: '80px', 27 | headerTheme: false, 28 | showBreadcrumb: true, // 是否显示面包屑 29 | token: { 30 | colorPrimary: '#0077fa', 31 | colorInfo: '#0077fa' 32 | }, 33 | algorithm: 'defaultAlgorithm' 34 | }) 35 | 36 | export const useTheme = function (): Ref { 37 | return theme 38 | } 39 | -------------------------------------------------------------------------------- /scripts/verifyCommit.ts: -------------------------------------------------------------------------------- 1 | // Invoked on the commit-msg git hook by simple-git-hooks. 2 | 3 | import { readFileSync } from 'node:fs' 4 | import colors from 'picocolors' 5 | 6 | // get $1 from commit-msg script 7 | const msgPath = process.argv[2] 8 | const msg = readFileSync(msgPath, 'utf-8').trim() 9 | 10 | // Merge branch 11 | const mergeRE = /^Merge branch.*/ 12 | const commitRE = 13 | /^(revert: )?(feat|fix|docs|style|refactor|perf|test|chore|revert|release)(\(.+\))?: .{1,50}/ 14 | 15 | if (!mergeRE.test(msg) && !commitRE.test(msg)) { 16 | console.log() 17 | console.error( 18 | ` ${colors.bgRed(colors.white(' ERROR '))} ${colors.red( 19 | `invalid commit message format.` 20 | )}\n\n` + 21 | colors.red( 22 | ` Proper commit message format is required for automated changelog generation. Examples:\n\n` 23 | ) + 24 | ` ${colors.green(`feat: add 'comments' option`)}\n` + 25 | ` ${colors.green(`fix: handle events on blur (close #28)`)}\n` 26 | ) 27 | process.exit(1) 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vite-vue-template 2 | 3 | 这个模板可以帮助你快速使用 `Vue3`, `TypeScript` 和 `Vite` 进行项目开发。 4 | 5 | **安装pnpm** 6 | 7 | ```javascript 8 | npm i -g pnpm 9 | ``` 10 | 11 | **起步** 12 | 13 | 执行这个命令安装完依赖,apps目录下的应用可以正常启动。 14 | 15 | ```javascript 16 | pnpm i 17 | ``` 18 | 19 | **安装依赖** 20 | 21 | ```javascript 22 | pnpm i -w pkgName 23 | ``` 24 | 25 | 递归每个package安装 26 | ```javascript 27 | pnpm i -w -r pkgName 28 | ``` 29 | 30 | **给子包安装指定依赖** 31 | ```javascript 32 | pnpm -F @packages/components add lodash 33 | ``` 34 | 35 | **package间的相互引用** 36 | ```javascript 37 | pnpm -F @packages/components add @packages/utils 38 | ``` 39 | 40 | 41 | **项目结构** 42 | ```javascript 43 | ├── base 44 | ├── apps // web项目 45 | ├── features // 可复用的页面组件代码 46 | ├── packages // 常用工具和库 47 | │   ├── components 48 | │   ├── hooks 49 | │   ├── lib 50 | │   ├── plugins 51 | │   ├── styles 52 | │   ├── types 53 | │   └── utils 54 | ├── .gitignore 55 | ├── package.json 56 | ├── pnpm-lock.yaml 57 | ├── pnpm-workspace.yaml 58 | ├── README.md 59 | └── tsconfig.json 60 | ``` 61 | -------------------------------------------------------------------------------- /apps/nuxt3-app/scripts/verifyCommit.ts: -------------------------------------------------------------------------------- 1 | // Invoked on the commit-msg git hook by simple-git-hooks. 2 | 3 | import { readFileSync } from 'node:fs' 4 | import colors from 'picocolors' 5 | 6 | // get $1 from commit-msg script 7 | const msgPath = process.argv[2] 8 | const msg = readFileSync(msgPath, 'utf-8').trim() 9 | 10 | // Merge branch 11 | const mergeRE = /^Merge branch.*/ 12 | const commitRE = 13 | /^(revert: )?(feat|fix|docs|style|refactor|perf|test|chore|revert|release)(\(.+\))?: .{1,50}/ 14 | 15 | if (!mergeRE.test(msg) && !commitRE.test(msg)) { 16 | console.log() 17 | console.error( 18 | ` ${colors.bgRed(colors.white(' ERROR '))} ${colors.red( 19 | `invalid commit message format.` 20 | )}\n\n` + 21 | colors.red( 22 | ` Proper commit message format is required for automated changelog generation. Examples:\n\n` 23 | ) + 24 | ` ${colors.green(`feat: add 'comments' option`)}\n` + 25 | ` ${colors.green(`fix: handle events on blur (close #28)`)}\n` 26 | ) 27 | process.exit(1) 28 | } 29 | -------------------------------------------------------------------------------- /apps/vue3-app/scripts/verifyCommit.ts: -------------------------------------------------------------------------------- 1 | // Invoked on the commit-msg git hook by simple-git-hooks. 2 | 3 | import { readFileSync } from 'node:fs' 4 | import colors from 'picocolors' 5 | 6 | // get $1 from commit-msg script 7 | const msgPath = process.argv[2] 8 | const msg = readFileSync(msgPath, 'utf-8').trim() 9 | 10 | // Merge branch 11 | const mergeRE = /^Merge branch.*/ 12 | const commitRE = 13 | /^(revert: )?(feat|fix|docs|style|refactor|perf|test|chore|revert|release)(\(.+\))?: .{1,50}/ 14 | 15 | if (!mergeRE.test(msg) && !commitRE.test(msg)) { 16 | console.log() 17 | console.error( 18 | ` ${colors.bgRed(colors.white(' ERROR '))} ${colors.red( 19 | `invalid commit message format.` 20 | )}\n\n` + 21 | colors.red( 22 | ` Proper commit message format is required for automated changelog generation. Examples:\n\n` 23 | ) + 24 | ` ${colors.green(`feat: add 'comments' option`)}\n` + 25 | ` ${colors.green(`fix: handle events on blur (close #28)`)}\n` 26 | ) 27 | process.exit(1) 28 | } 29 | -------------------------------------------------------------------------------- /packages/styles/reset.less: -------------------------------------------------------------------------------- 1 | /* css reset */ 2 | body, 3 | p, 4 | h1, 5 | h2, 6 | h3, 7 | h4, 8 | h5, 9 | h6, 10 | dl, 11 | dd { 12 | margin: 0; 13 | -webkit-text-size-adjust: 100%; // 移动端横竖屏切换后字体可能会有不同比例的缩放 14 | } 15 | 16 | body { 17 | font-size: 14px; 18 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 19 | font-variant-numeric: tabular-nums; // 将数字设置为等宽字体 20 | } 21 | 22 | *, *::before, *::after { 23 | box-sizing: border-box; 24 | } 25 | 26 | ol, 27 | ul { 28 | list-style: none; 29 | padding: 0; 30 | margin: 0; 31 | } 32 | 33 | input, textarea { 34 | padding: 0; 35 | margin: 0; 36 | } 37 | 38 | img { 39 | border: none; 40 | vertical-align: top; 41 | } 42 | 43 | a { 44 | text-decoration: none; 45 | } 46 | 47 | a, 48 | input, 49 | button { 50 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); // 点击后的阴影 51 | -webkit-appearance: none; // ios下input圆角 52 | } 53 | -------------------------------------------------------------------------------- /features/components/store/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /apps/nuxt3-app/mock/data.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import qs from 'qs' 3 | import { genPhone } from './utils' 4 | 5 | // 获取 mock.Random 对象 6 | const Random = Mock.Random 7 | 8 | export const getList = function (options: Record = {}) { 9 | const body: Record = qs.parse(options.body) 10 | const arr = [] 11 | for (let i = 0; i < (body.pageSize || 10); i++) { 12 | const obj = { 13 | id: i + 1, 14 | name: Random.cname(), // Random.cname() 随机生成一个常见的中文姓名 15 | phone: genPhone(), // Random.cname() 随机生成一个手机号码 16 | email: Random.email(), // Random.cname() 随机生成一个邮箱地址 17 | age: Random.integer(1, 99), // Random.integer( min, max ) 18 | hobby: Random.csentence(5, 30), // Random.csentence( min, max ) 19 | updateTime: Random.date() + ' ' + Random.time(), // Random.date()指示生成的日期字符串的格式, 默认为yyyy-MM-dd;Random.time() 返回一个随机的时间字符串 20 | img: Random.dataImage('300x250', 'mock的图片') // Random.dataImage( size, text ) 生成一段随机的 Base64 图片编码 21 | } 22 | arr.push(obj) 23 | } 24 | 25 | return { 26 | code: 0, 27 | data: { 28 | list: arr, 29 | total: 100 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/vue3-app/src/mock/data.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import qs from 'qs' 3 | import { genPhone } from './utils' 4 | 5 | // 获取 mock.Random 对象 6 | const Random = Mock.Random 7 | 8 | export const getList = function (options: Record = {}) { 9 | const body: Record = qs.parse(options.body) 10 | const arr = [] 11 | for (let i = 0; i < (body.pageSize || 10); i++) { 12 | const obj = { 13 | id: i + 1, 14 | name: Random.cname(), // Random.cname() 随机生成一个常见的中文姓名 15 | phone: genPhone(), // Random.cname() 随机生成一个手机号码 16 | email: Random.email(), // Random.cname() 随机生成一个邮箱地址 17 | age: Random.integer(1, 99), // Random.integer( min, max ) 18 | hobby: Random.csentence(5, 30), // Random.csentence( min, max ) 19 | updateTime: Random.date() + ' ' + Random.time(), // Random.date()指示生成的日期字符串的格式, 默认为yyyy-MM-dd;Random.time() 返回一个随机的时间字符串 20 | img: Random.dataImage('300x250', 'mock的图片') // Random.dataImage( size, text ) 生成一段随机的 Base64 图片编码 21 | } 22 | arr.push(obj) 23 | } 24 | 25 | return { 26 | code: 0, 27 | data: { 28 | list: arr, 29 | total: 100 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/components/layout/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /apps/nuxt3-app/components/layout/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /apps/vue3-app/src/components/layout/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /apps/vue3-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/xinlei3166/vite-vue-template.git", 9 | "directory": "apps/vue3-app" 10 | }, 11 | "keywords": [ 12 | "vue", 13 | "vite", 14 | "template" 15 | ], 16 | "author": "君惜", 17 | "license": "MIT", 18 | "scripts": { 19 | "preinstall": "npx only-allow pnpm", 20 | "dev": "vite --host --port 6001", 21 | "build:qa": "vite build --mode qa", 22 | "build:github": "vite build --mode github", 23 | "build": "vite build", 24 | "serve": "vite preview --host", 25 | "deploy": "zsh scripts/gh-pages.sh", 26 | "upload": "node upload", 27 | "test": "pnpm run build:qa && pnpm run upload" 28 | }, 29 | "dependencies": { 30 | "@packages/components": "workspace:*", 31 | "@packages/hooks": "workspace:*", 32 | "@packages/lib": "workspace:*", 33 | "@packages/plugins": "workspace:*", 34 | "@packages/styles": "workspace:*", 35 | "@packages/token": "workspace:*", 36 | "@packages/types": "workspace:*", 37 | "@packages/utils": "workspace:*" 38 | }, 39 | "devDependencies": {} 40 | } 41 | -------------------------------------------------------------------------------- /apps/nuxt3-app/middleware/02.router.global.ts: -------------------------------------------------------------------------------- 1 | import { getToken, loadingEnd, loadingStart, pageAuth } from '@packages/utils' 2 | import { useMenuStore } from '@/store/menu' 3 | import { checkExternalWhiteRoute, whiteList } from '@/router' 4 | 5 | export default defineNuxtRouteMiddleware(async (to, from) => { 6 | // 在服务器端跳过中间件 7 | if (process.server) return 8 | // 完全在客户端跳过中间件 9 | // if (process.client) return 10 | // 或仅在初始客户端加载时跳过中间件 11 | // const nuxtApp = useNuxtApp() 12 | // if (process.client && nuxtApp.isHydrating && nuxtApp.payload.serverRendered) return 13 | if (checkExternalWhiteRoute(to.path)) return 14 | const token = getToken() 15 | if (whiteList.includes(to.path)) { 16 | if (token && to.path === '/login') return navigateTo({ path: '/' }) 17 | return 18 | } 19 | if (!token && to.path !== '/login') return navigateTo({ path: '/login' }) 20 | if (!__DYNAMIC_MENU__) return 21 | const menuStore = useMenuStore() 22 | if (menuStore.hasSetRoutes) { 23 | // const auth = await pageAuth(to.name) 24 | // if (!auth && to.meta.auth !== false && to.path !== '/403') { 25 | // navigateTo('/403') 26 | // } 27 | return 28 | } else { 29 | await menuStore.setMenus() 30 | navigateTo(to, { replace: true }) 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /apps/vue3-app/src/store/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { getPermissions, getUserinfo } from '@/api' 3 | 4 | const storageKeyPrefix = import.meta.env.VITE_APP_STORAGE_KEY_PREFIX 5 | 6 | export interface UserState { 7 | userinfo: Record 8 | permissions: Record[] 9 | } 10 | 11 | export const useUserStore = defineStore('user', { 12 | state: (): UserState => ({ userinfo: {}, permissions: [] }), 13 | getters: { 14 | Userinfo(state) { 15 | return state.userinfo 16 | }, 17 | Permissions(state) { 18 | return state.permissions 19 | } 20 | }, 21 | actions: { 22 | async setUserinfo() { 23 | const res: any = await getUserinfo() 24 | if (!res || res.code !== 0) return 25 | this.userinfo = res.data 26 | }, 27 | async setPermissions() { 28 | const res: any = await getPermissions() 29 | if (!res || res.code !== 0) return 30 | this.permissions = res.data 31 | }, 32 | async cleanup() { 33 | this.userinfo = {} 34 | this.permissions = [] 35 | } 36 | }, 37 | persist: { 38 | enabled: true, 39 | strategies: [ 40 | { 41 | key: storageKeyPrefix + 'User', 42 | storage: localStorage 43 | } 44 | ] 45 | } 46 | }) 47 | -------------------------------------------------------------------------------- /apps/nuxt3-app/store/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { getPermissions, getUserinfo } from '@/api' 3 | 4 | const storageKeyPrefix = import.meta.env.VITE_APP_STORAGE_KEY_PREFIX 5 | 6 | export interface UserState { 7 | userinfo: Record 8 | permissions: Record[] 9 | } 10 | 11 | export const useUserStore = defineStore('user', { 12 | state: (): UserState => ({ userinfo: {}, permissions: [] }), 13 | getters: { 14 | Userinfo(state) { 15 | return state.userinfo 16 | }, 17 | Permissions(state) { 18 | return state.permissions 19 | } 20 | }, 21 | actions: { 22 | async setUserinfo() { 23 | const res: any = await getUserinfo() 24 | if (!res || res.code !== 0) return 25 | this.userinfo = res.data 26 | }, 27 | async setPermissions() { 28 | const res: any = await getPermissions() 29 | if (!res || res.code !== 0) return 30 | this.permissions = res.data 31 | }, 32 | async cleanup() { 33 | this.userinfo = {} 34 | this.permissions = [] 35 | } 36 | }, 37 | persist: { 38 | enabled: true, 39 | strategies: [ 40 | { 41 | key: storageKeyPrefix + 'User', 42 | storage: persistedState.localStorage 43 | } 44 | ] 45 | } 46 | }) 47 | -------------------------------------------------------------------------------- /packages/types/requests.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios' 2 | 3 | export interface RequestsConfig { 4 | baseURL?: string 5 | AuthorizationKey?: string 6 | errorCodes?: Array 7 | codeKey?: string 8 | messageKey?: string 9 | successCode?: string | number 10 | errorHandler?: Function 11 | } 12 | 13 | export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete' 14 | 15 | export interface RequestOptions { 16 | withRequestId?: boolean 17 | responseType?: 'json' | 'blob' | 'base64' | 'raw' 18 | stringify?: boolean 19 | cb?: Function 20 | fileKey?: string 21 | fileName?: string 22 | blobOptions?: Record 23 | } 24 | 25 | export interface RequestConfig { 26 | requestOptions?: RequestOptions 27 | [key: string]: any 28 | } 29 | export type Config = RequestConfig & AxiosRequestConfig 30 | export type InternalConfig = RequestConfig & InternalAxiosRequestConfig 31 | 32 | export interface Request { 33 | [key: string]: any 34 | } 35 | 36 | export type RequestMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' 37 | export type ResponseData = null | Array | Record 38 | 39 | export interface Response { 40 | code: number | string 41 | data: ResponseData 42 | 43 | [key: string]: any 44 | } 45 | -------------------------------------------------------------------------------- /apps/vue3-app/src/router/home.ts: -------------------------------------------------------------------------------- 1 | import { useMenuStore } from '@/store/menu' 2 | import type { RouteRecordRaw } from 'vue-router' 3 | 4 | const Layout = () => import('@/components/layout/index.vue') 5 | 6 | const beforeEnter = async (to: any, from: any, next: any) => { 7 | // 非动态路由下跳转的路径,如果不需要跳转路由对象删除beforeEnter方法 8 | if (!__DYNAMIC_MENU__) return next('/components') 9 | const menuStore = useMenuStore() 10 | const routeMenus = menuStore.routeMenus.slice(0, -1) 11 | if (!routeMenus.length) { 12 | next({ 13 | path: '/403' 14 | }) 15 | return 16 | } 17 | const path = routeMenus[0].children?.length ? routeMenus[0].children[0].path : routeMenus[0].path 18 | next({ path }) 19 | } 20 | 21 | export default [ 22 | { 23 | path: '/', 24 | component: Layout, 25 | meta: { 26 | hidden: true 27 | }, 28 | children: [ 29 | { 30 | path: '', 31 | name: 'home', 32 | meta: { 33 | title: '首页' 34 | }, 35 | beforeEnter, 36 | component: () => import('@/views/home/index.vue') 37 | } 38 | ] 39 | }, 40 | { 41 | path: '/login', 42 | name: 'login', 43 | meta: { 44 | title: '登录' 45 | }, 46 | component: () => import('@/views/login/index.vue') 47 | } 48 | ] as any as RouteRecordRaw[] 49 | -------------------------------------------------------------------------------- /packages/utils/audio.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | // convertWavToMp3(recorder.getWAV()) 3 | export function convertWavToMp3(wavDataView) { 4 | const wav = window.lamejs.WavHeader.readHeader(wavDataView) 5 | const samples = new Int16Array(wavDataView.buffer, wav.dataOffset, wav.dataLen / 2) 6 | const { channels, sampleRate } = wav 7 | const buffer = [] 8 | const mp3enc = new window.lamejs.Mp3Encoder(channels, sampleRate, 128) 9 | let remaining = samples.length 10 | const maxSamples = 1152 11 | for (let i = 0; remaining >= maxSamples; i += maxSamples) { 12 | const mono = samples.subarray(i, i + maxSamples) 13 | const mp3buf = mp3enc.encodeBuffer(mono) 14 | if (mp3buf.length > 0) { 15 | buffer.push(new Int8Array(mp3buf)) 16 | } 17 | remaining -= maxSamples 18 | } 19 | const d = mp3enc.flush() 20 | if (d.length > 0) { 21 | buffer.push(new Int8Array(d)) 22 | } 23 | 24 | return new Blob(buffer, { type: 'audio/mp3' }) 25 | } 26 | 27 | // convertDurationToHMS(3000) 28 | export function convertDurationToHMS(duration) { 29 | let hour = parseInt(duration / 3600) 30 | hour = hour > 0 && hour < 10 ? '0' + hour : null 31 | let minute = parseInt((duration % 3600) / 60) 32 | minute = minute < 10 ? '0' + minute : minute 33 | let second = parseInt(duration % 60) 34 | second = second < 10 ? '0' + second : second 35 | return [hour, minute, second].filter(x => x !== null).join(':') 36 | } 37 | -------------------------------------------------------------------------------- /apps/nuxt3-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt3-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/xinlei3166/vite-vue-template.git", 9 | "directory": "apps/nuxt3-app" 10 | }, 11 | "author": "君惜", 12 | "license": "MIT", 13 | "scripts": { 14 | "preinstall": "npx only-allow pnpm", 15 | "postinstall": "nuxi prepare", 16 | "dev": "nuxi dev", 17 | "build:qa": "nuxi build --mode qa", 18 | "build:github": "nuxi build --mode github", 19 | "build": "nuxi build", 20 | "generate": "nuxi generate", 21 | "preview": "nuxi preview", 22 | "deploy": "zsh scripts/gh-pages.sh", 23 | "upload": "node upload", 24 | "test": "pnpm run build:qa && pnpm run upload" 25 | }, 26 | "dependencies": { 27 | "nuxt": "^3.11.2", 28 | "@ant-design-vue/nuxt": "^1.4.1", 29 | "@unocss/nuxt": "^0.59.0", 30 | "@pinia/nuxt": "^0.5.1", 31 | "@pinia-plugin-persistedstate/nuxt": "^1.2.0", 32 | "vue": "^3.4.21", 33 | "vue-router": "^4.3.0", 34 | "exceljs": "^4.4.0", 35 | "@packages/components": "workspace:*", 36 | "@packages/hooks": "workspace:*", 37 | "@packages/lib": "workspace:*", 38 | "@packages/plugins": "workspace:*", 39 | "@packages/styles": "workspace:*", 40 | "@packages/token": "workspace:*", 41 | "@packages/types": "workspace:*", 42 | "@packages/utils": "workspace:*" 43 | }, 44 | "devDependencies": {} 45 | } 46 | -------------------------------------------------------------------------------- /apps/vue3-app/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | 403: typeof import('./src/components/exception/403.vue')['default'] 11 | 404: typeof import('./src/components/exception/404.vue')['default'] 12 | 500: typeof import('./src/components/exception/500.vue')['default'] 13 | Breadcrumb: typeof import('./src/components/layout/Breadcrumb.vue')['default'] 14 | Buttons: typeof import('./src/components/exception/buttons.vue')['default'] 15 | Demo: typeof import('./src/components/search/demo/index.vue')['default'] 16 | Error403: typeof import('./src/components/exception/error-403.vue')['default'] 17 | HelloWorld: typeof import('./src/components/HelloWorld.vue')['default'] 18 | Layout: typeof import('./src/components/layout/index.vue')['default'] 19 | Logo: typeof import('./src/components/layout/Logo.vue')['default'] 20 | Nav: typeof import('./src/components/layout/Nav.vue')['default'] 21 | Router: typeof import('./src/components/layout/Router.vue')['default'] 22 | RouterLink: typeof import('vue-router')['RouterLink'] 23 | RouterView: typeof import('vue-router')['RouterView'] 24 | Setting: typeof import('./src/components/layout/Setting.vue')['default'] 25 | Siderbar: typeof import('./src/components/layout/Siderbar.vue')['default'] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/utils/assist.ts: -------------------------------------------------------------------------------- 1 | import { message } from 'ant-design-vue' 2 | 3 | export const delay = (ms: any) => new Promise(resolve => setTimeout(resolve, ms)) 4 | 5 | export function typeOf(obj: any) { 6 | const toString = Object.prototype.toString 7 | const map = { 8 | '[object Boolean]': 'boolean', 9 | '[object Number]': 'number', 10 | '[object String]': 'string', 11 | '[object Function]': 'function', 12 | '[object Array]': 'array', 13 | '[object Date]': 'date', 14 | '[object RegExp]': 'regExp', 15 | '[object Undefined]': 'undefined', 16 | '[object Null]': 'null', 17 | '[object Object]': 'object' 18 | } 19 | // @ts-ignore 20 | return map[toString.call(obj)] 21 | } 22 | 23 | export function validateFields(form: Record, messages: Record) { 24 | for (const [k, v] of Object.entries(form)) { 25 | if (!v) { 26 | messages[k] && message.error(messages[k]) 27 | return 28 | } 29 | } 30 | return true 31 | } 32 | 33 | export function validateSelectedRowKeys(arr: Array, msg = '请选择一条数据') { 34 | if (!arr.length) { 35 | message.error(msg) 36 | return 37 | } 38 | return true 39 | } 40 | 41 | export function validateSelectedRowKeysWithRowCount( 42 | arr: Array, 43 | rowCount = 1, 44 | msg1 = '请选择一条数据', 45 | msg2 = '只能同时编辑一条数据' 46 | ) { 47 | if (!arr.length) { 48 | message.error(msg1) 49 | return 50 | } 51 | if (rowCount && arr.length > rowCount) { 52 | message.error(msg2) 53 | return 54 | } 55 | return true 56 | } 57 | -------------------------------------------------------------------------------- /packages/hooks/select.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | interface Config { 4 | method?: 'get' | 'post' 5 | params?: {} 6 | data?: {} 7 | } 8 | 9 | /** 10 | * select remote search hook 11 | * @param api 搜索接口。 12 | * @param dataKey 数据key。 13 | * @param config 额外的参数对象。 14 | * @param config.method 请求方式。 15 | * @param config.method 请求方式。 16 | * @param config.params url参数。 17 | * @param config.data 请求数据主体。 18 | */ 19 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 20 | export function useSelectSearch(api: Function, dataKey?: any, config?: Config) { 21 | const options = ref([]) 22 | 23 | /** 24 | * select 主动搜索方法, 参数可为空。 25 | * 1.进入页面调用此方法搜索数据 26 | * 2.例如改变另外一个筛选项的值,主动调用此方法搜索数据 27 | * @param params 参数 28 | */ 29 | const onTrigger = async (...params: any[]) => { 30 | const res = await api(...params) 31 | if (!res || res.code !== 0) return (options.value = []) 32 | if (dataKey) { 33 | options.value = res.data?.[dataKey] || [] 34 | } else { 35 | options.value = res.data || [] 36 | } 37 | return options.value 38 | } 39 | 40 | // 本地搜索 41 | const filter = (input: string, option: any) => { 42 | // console.log(input, option) 43 | // return option.props.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 44 | } 45 | 46 | // cascader本地搜索 47 | const cascaderFilter = (inputValue: string, path: Record[]) => { 48 | return path.some(option => option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1) 49 | } 50 | 51 | return { options, onTrigger, filter, cascaderFilter } 52 | } 53 | -------------------------------------------------------------------------------- /apps/vue3-app/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import home from './home' 3 | import common from './common' 4 | import routes from './routes' 5 | import { loadingStart, loadingEnd, getToken } from '@packages/utils' 6 | import { useMenuStore } from '@/store/menu' 7 | 8 | const router = createRouter({ 9 | history: createWebHistory(import.meta.env.BASE_URL), 10 | routes: __DYNAMIC_MENU__ ? [...home, ...common] : [...home, ...routes, ...common] 11 | }) 12 | 13 | export const checkExternalWhiteRoute = (routePath: string) => { 14 | let hasExternalWhiteRoute = false 15 | for (const w of externalWhiteList) { 16 | if (routePath.startsWith(w)) { 17 | hasExternalWhiteRoute = true 18 | break 19 | } 20 | } 21 | return hasExternalWhiteRoute 22 | } 23 | 24 | export const externalWhiteList = ['/visualScreen'] 25 | 26 | const whiteList = ['/login', '/initLogin'] 27 | router.beforeEach(async (to, from, next) => { 28 | loadingStart() 29 | if (checkExternalWhiteRoute(to.path)) return next() 30 | const token = getToken() 31 | if (whiteList.includes(to.path)) { 32 | if (token && to.path === '/login') return next({ path: '/' }) 33 | return next() 34 | } 35 | if (!token) return next({ path: '/login' }) 36 | if (!__DYNAMIC_MENU__) return next() 37 | const menuStore = useMenuStore() 38 | if (menuStore.hasSetRoutes) { 39 | next() 40 | } else { 41 | await menuStore.setMenus() 42 | next({ ...to, replace: true }) 43 | } 44 | }) 45 | 46 | router.afterEach(() => { 47 | loadingEnd() 48 | }) 49 | 50 | export default router 51 | -------------------------------------------------------------------------------- /packages/hooks/cosUpload.ts: -------------------------------------------------------------------------------- 1 | import { message } from 'ant-design-vue' 2 | import { useCosStore } from '@/store/cos' 3 | 4 | export interface UploadRes { 5 | url: string 6 | key: string 7 | } 8 | 9 | export function useCosUpload(type: string) { 10 | const store = useCosStore() 11 | // 使用之前先获取一次临时密钥 getTmpKeys 12 | store.getTmpKeys() 13 | const COS: any = store.COS 14 | const COS_KEYS = store.COS_KEYS 15 | return function cosUpload(data: any, name: string) { 16 | let file: any 17 | if (type === 'blob') { 18 | file = data 19 | } else { 20 | file = data.file 21 | } 22 | const fileName = file.name || name 23 | const fileType = file.type.split('/')[0] 24 | const types = ['image', 'video', 'audio'] 25 | return new Promise(function (resolve) { 26 | COS.sliceUploadFile( 27 | { 28 | ...COS_KEYS, 29 | Key: `avatar/${ 30 | types.includes(fileType) ? fileType : 'file' 31 | }/${new Date().getTime()}/${fileName}`, 32 | Body: file, 33 | onTaskReady() {}, 34 | onProgress() {} 35 | }, 36 | (err: { statusCode: number }, data: { Location: any; Key: any }) => { 37 | if (err) { 38 | if (err.statusCode === 403) { 39 | store.getTmpKeys() 40 | message.error('Access Key失效,请重新上传') 41 | } else { 42 | console.log(err) 43 | } 44 | } else { 45 | const { Location, Key: key } = data 46 | const url = 'https://' + Location 47 | resolve({ url, key }) 48 | } 49 | } 50 | ) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /apps/vue3-app/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /apps/vue3-app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp as _createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import piniaPersist from 'pinia-plugin-persist' 4 | import App from './App.vue' 5 | import router from './router' 6 | import plugins from '@packages/plugins' 7 | import '@packages/styles/index.less' 8 | import '@packages/styles/reset.less' 9 | import '@/styles/index.less' 10 | import 'animate.css' 11 | import 'virtual:uno.css' 12 | import './mock' 13 | 14 | function createApp() { 15 | const app = _createApp(App) 16 | const pinia = createPinia() 17 | pinia.use(piniaPersist) 18 | app.use(pinia) 19 | app.use(router) 20 | app.use(plugins) 21 | 22 | app.mount('#app') 23 | 24 | if (import.meta.env.PROD) { 25 | app.config.errorHandler = (err, vm, info) => { 26 | console.group('vue_global_error') 27 | console.log('捕获到异常:', { err, vm, info }) 28 | console.groupEnd() 29 | } 30 | 31 | window.onerror = function (message, source, lineno, colno, error) { 32 | console.group('window_global_error') 33 | console.log('捕获到异常:', { message, source, lineno, colno, error }) 34 | console.groupEnd() 35 | } 36 | } 37 | 38 | return app 39 | } 40 | 41 | declare global { 42 | interface Window { 43 | // 是否存在无界 44 | __POWERED_BY_WUJIE__?: boolean 45 | // 子应用mount函数 46 | __WUJIE_MOUNT: () => void 47 | // 子应用unmount函数 48 | __WUJIE_UNMOUNT: () => void 49 | // 子应用无界实例 50 | __WUJIE: { mount: () => void } 51 | } 52 | } 53 | 54 | if (window.__POWERED_BY_WUJIE__) { 55 | let app: any 56 | window.__WUJIE_MOUNT = () => { 57 | app = createApp() 58 | } 59 | window.__WUJIE_UNMOUNT = () => { 60 | app.unmount() 61 | } 62 | // module脚本异步加载,应用主动调用生命周期 63 | window.__WUJIE.mount() 64 | } else { 65 | createApp() 66 | } 67 | -------------------------------------------------------------------------------- /apps/nuxt3-app/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 59 | 60 | 77 | -------------------------------------------------------------------------------- /apps/vue3-app/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 59 | 60 | 77 | -------------------------------------------------------------------------------- /packages/utils/sm3/index.js: -------------------------------------------------------------------------------- 1 | import sm3 from './sm3' 2 | 3 | /** 4 | * 字节数组转 16 进制串 5 | */ 6 | function ArrayToHex(arr) { 7 | return arr 8 | .map(item => { 9 | item = item.toString(16) 10 | return item.length === 1 ? '0' + item : item 11 | }) 12 | .join('') 13 | } 14 | 15 | /** 16 | * utf8 串转字节数组 17 | */ 18 | function utf8ToArray(str) { 19 | const arr = [] 20 | 21 | for (let i = 0, len = str.length; i < len; i++) { 22 | const point = str.codePointAt(i) 23 | 24 | if (point <= 0x007f) { 25 | // 单字节,标量值:00000000 00000000 0zzzzzzz 26 | arr.push(point) 27 | } else if (point <= 0x07ff) { 28 | // 双字节,标量值:00000000 00000yyy yyzzzzzz 29 | arr.push(0xc0 | (point >>> 6)) // 110yyyyy(0xc0-0xdf) 30 | arr.push(0x80 | (point & 0x3f)) // 10zzzzzz(0x80-0xbf) 31 | } else if (point <= 0xd7ff || (point >= 0xe000 && point <= 0xffff)) { 32 | // 三字节:标量值:00000000 xxxxyyyy yyzzzzzz 33 | arr.push(0xe0 | (point >>> 12)) // 1110xxxx(0xe0-0xef) 34 | arr.push(0x80 | ((point >>> 6) & 0x3f)) // 10yyyyyy(0x80-0xbf) 35 | arr.push(0x80 | (point & 0x3f)) // 10zzzzzz(0x80-0xbf) 36 | } else if (point >= 0x010000 && point <= 0x10ffff) { 37 | // 四字节:标量值:000wwwxx xxxxyyyy yyzzzzzz 38 | i++ 39 | arr.push(0xf0 | ((point >>> 18) & 0x1c)) // 11110www(0xf0-0xf7) 40 | arr.push(0x80 | ((point >>> 12) & 0x3f)) // 10xxxxxx(0x80-0xbf) 41 | arr.push(0x80 | ((point >>> 6) & 0x3f)) // 10yyyyyy(0x80-0xbf) 42 | arr.push(0x80 | (point & 0x3f)) // 10zzzzzz(0x80-0xbf) 43 | } else { 44 | // 五、六字节,暂时不支持 45 | arr.push(point) 46 | throw new Error('input is not supported') 47 | } 48 | } 49 | 50 | return arr 51 | } 52 | 53 | export default function (input) { 54 | input = typeof input === 'string' ? utf8ToArray(input) : Array.prototype.slice.call(input) 55 | return ArrayToHex(sm3(input)) 56 | } 57 | -------------------------------------------------------------------------------- /features/components/table/fixed.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /apps/nuxt3-app/components/layout/Logo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 83 | -------------------------------------------------------------------------------- /packages/components/layout/Logo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 83 | -------------------------------------------------------------------------------- /apps/vue3-app/src/components/layout/Logo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 83 | -------------------------------------------------------------------------------- /packages/components/btn-group/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 38 | 39 | 82 | -------------------------------------------------------------------------------- /apps/nuxt3-app/mock/user.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import qs from 'qs' 3 | import { genPhone } from './utils' 4 | 5 | // 获取 mock.Random 对象 6 | const Random = Mock.Random 7 | 8 | export const getUserMenu = function (options: Record = {}) { 9 | const body: Record = qs.parse(options.body) 10 | const arr: any[] = [] 11 | return { 12 | code: 0, 13 | data: { 14 | list: arr, 15 | total: 0 16 | } 17 | } 18 | } 19 | 20 | export const getPermissions = function (options: Record = {}) { 21 | const body: Record = qs.parse(options.body) 22 | const arr: any[] = [] 23 | return { 24 | code: 0, 25 | data: { 26 | list: arr, 27 | total: 0 28 | } 29 | } 30 | } 31 | 32 | export const getUserinfo = function (options: Record = {}) { 33 | const body: Record = qs.parse(options.body) 34 | const data = { 35 | id: Random.integer(1, 99), 36 | name: Random.cname(), // Random.cname() 随机生成一个常见的中文姓名 37 | phone: genPhone(), // Random.cname() 随机生成一个手机号码 38 | email: Random.email(), // Random.cname() 随机生成一个邮箱地址 39 | age: Random.integer(1, 99), // Random.integer( min, max ) 40 | hobby: Random.csentence(5, 30), // Random.csentence( min, max ) 41 | updateTime: Random.date() + ' ' + Random.time(), // Random.date()指示生成的日期字符串的格式, 默认为yyyy-MM-dd;Random.time() 返回一个随机的时间字符串 42 | img: Random.dataImage('300x250', 'mock的图片') // Random.dataImage( size, text ) 生成一段随机的 Base64 图片编码 43 | } 44 | 45 | return { 46 | code: 0, 47 | data 48 | } 49 | } 50 | 51 | export const login = function (options: Record = {}) { 52 | const body: Record = qs.parse(options.body) 53 | return { 54 | code: 0, 55 | data: { 56 | accessToken: Random.guid() 57 | } 58 | } 59 | } 60 | 61 | export const logout = function (options: Record = {}) { 62 | const body: Record = qs.parse(options.body) 63 | return { 64 | code: 0 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /apps/vue3-app/src/mock/user.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import qs from 'qs' 3 | import { genPhone } from './utils' 4 | 5 | // 获取 mock.Random 对象 6 | const Random = Mock.Random 7 | 8 | export const getUserMenu = function (options: Record = {}) { 9 | const body: Record = qs.parse(options.body) 10 | const arr: any[] = [] 11 | return { 12 | code: 0, 13 | data: { 14 | list: arr, 15 | total: 0 16 | } 17 | } 18 | } 19 | 20 | export const getPermissions = function (options: Record = {}) { 21 | const body: Record = qs.parse(options.body) 22 | const arr: any[] = [] 23 | return { 24 | code: 0, 25 | data: { 26 | list: arr, 27 | total: 0 28 | } 29 | } 30 | } 31 | 32 | export const getUserinfo = function (options: Record = {}) { 33 | const body: Record = qs.parse(options.body) 34 | const data = { 35 | id: Random.integer(1, 99), 36 | name: Random.cname(), // Random.cname() 随机生成一个常见的中文姓名 37 | phone: genPhone(), // Random.cname() 随机生成一个手机号码 38 | email: Random.email(), // Random.cname() 随机生成一个邮箱地址 39 | age: Random.integer(1, 99), // Random.integer( min, max ) 40 | hobby: Random.csentence(5, 30), // Random.csentence( min, max ) 41 | updateTime: Random.date() + ' ' + Random.time(), // Random.date()指示生成的日期字符串的格式, 默认为yyyy-MM-dd;Random.time() 返回一个随机的时间字符串 42 | img: Random.dataImage('300x250', 'mock的图片') // Random.dataImage( size, text ) 生成一段随机的 Base64 图片编码 43 | } 44 | 45 | return { 46 | code: 0, 47 | data 48 | } 49 | } 50 | 51 | export const login = function (options: Record = {}) { 52 | const body: Record = qs.parse(options.body) 53 | return { 54 | code: 0, 55 | data: { 56 | accessToken: Random.guid() 57 | } 58 | } 59 | } 60 | 61 | export const logout = function (options: Record = {}) { 62 | const body: Record = qs.parse(options.body) 63 | return { 64 | code: 0 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/utils/file.ts: -------------------------------------------------------------------------------- 1 | import type { UploadFile } from 'ant-design-vue' 2 | import { is } from './is' 3 | 4 | export function base64ToURL(base64String: string, type = 'application/pdf;chartset=UTF-8') { 5 | const bstr = window.atob(base64String) 6 | let n = bstr.length 7 | const u8arr = new Uint8Array(n) 8 | while (n--) { 9 | u8arr[n] = bstr.charCodeAt(n) 10 | } 11 | const blob = new Blob([u8arr], { type }) 12 | const url = window.URL.createObjectURL(blob) 13 | return url 14 | } 15 | 16 | export function downFile( 17 | base64String: string, 18 | fileName: string, 19 | type = 'application/pdf;chartset=UTF-8' 20 | ) { 21 | const url = base64ToURL(base64String, type) 22 | const elink = document.createElement('a') 23 | elink.download = fileName 24 | elink.style.display = 'none' 25 | elink.href = url 26 | document.body.appendChild(elink) 27 | elink.click() 28 | document.body.removeChild(elink) 29 | } 30 | 31 | export function openFile(base64String: string, type = 'application/pdf;chartset=UTF-8') { 32 | if (base64String) { 33 | const url = base64ToURL(base64String, type) 34 | const elink = document.createElement('a') 35 | elink.style.display = 'none' 36 | elink.href = url 37 | elink.target = '_blank' 38 | document.body.appendChild(elink) 39 | elink.click() 40 | document.body.removeChild(elink) 41 | } 42 | } 43 | 44 | export function hasFileType(file: UploadFile, type: string | string[]): boolean { 45 | // @ts-ignore 46 | const mime = file.name.split('.').pop().toLowerCase() 47 | if (is(type, 'Array')) { 48 | return type.includes(mime) 49 | } else if (is(type, 'String')) { 50 | return type === mime 51 | } else { 52 | return false 53 | } 54 | } 55 | 56 | export function hasExcelFile(file: UploadFile): boolean { 57 | const mimes = [ 58 | 'xlsx', 59 | 'xlsm', 60 | 'xlsb', 61 | 'xltx', 62 | 'xltm', 63 | 'xls', 64 | 'xlt', 65 | 'xml', 66 | 'xlam', 67 | 'xlw', 68 | 'xlr', 69 | 'csv' 70 | ] 71 | return hasFileType(file, mimes) 72 | } 73 | -------------------------------------------------------------------------------- /packages/components/select/SelectDep.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /apps/nuxt3-app/store/cos.ts: -------------------------------------------------------------------------------- 1 | import COS from 'cos-js-sdk-v5' 2 | import { defineStore } from 'pinia' 3 | import { getQcloudTmpkeys } from '@/api' 4 | import type { Response } from '@packages/types' 5 | 6 | export interface CosState { 7 | cosKeys: object 8 | cos: object 9 | expires: number | string 10 | } 11 | 12 | export const useCosStore = defineStore('cos', { 13 | state: (): CosState => ({ cosKeys: {}, cos: {}, expires: '' }), 14 | getters: { 15 | COS_KEYS(state: CosState) { 16 | return state.cosKeys 17 | }, 18 | COS(state: CosState) { 19 | return state.cos 20 | } 21 | }, 22 | actions: { 23 | setCosKeys(data: any) { 24 | const options = { 25 | Bucket: data.bucket, 26 | Region: data.region 27 | } 28 | this.cosKeys = options 29 | sessionStorage.setItem('cosKeys', JSON.stringify(options)) 30 | }, 31 | createCOS(data: any) { 32 | const cosInstance = new COS({ 33 | getAuthorization(options, callback) { 34 | callback({ 35 | TmpSecretId: data.tmpSecretId, 36 | TmpSecretKey: data.tmpSecretKey, 37 | XCosSecurityToken: data.sessionToken, 38 | ExpiredTime: data.expiredTime, 39 | StartTime: data.StartTime 40 | }) 41 | } 42 | }) 43 | this.cos = cosInstance 44 | sessionStorage.setItem('cos', JSON.stringify(cosInstance)) 45 | }, 46 | setExpires(data: any) { 47 | const timeDiff = data.expiredTime - data.startTime - 600 48 | const date = (new Date().getTime() / 1000) as any 49 | const dateNow = parseInt(date) 50 | this.expires = dateNow + timeDiff 51 | }, 52 | async getTmpKeys() { 53 | const date = (new Date().getTime() / 1000) as any 54 | const dateNow = parseInt(date) // 当前时间戳 55 | if (dateNow > this.expires) { 56 | // 初次和失效后调取 57 | const res = (await getQcloudTmpkeys()) as Response 58 | if (!res || res.code !== 0) return 59 | const data = res.data 60 | this.setExpires(data) 61 | this.setCosKeys(data) 62 | this.createCOS(data) 63 | } 64 | } 65 | } 66 | }) 67 | -------------------------------------------------------------------------------- /apps/vue3-app/src/store/cos.ts: -------------------------------------------------------------------------------- 1 | import COS from 'cos-js-sdk-v5' 2 | import { defineStore } from 'pinia' 3 | import { getQcloudTmpkeys } from '@/api' 4 | import type { Response } from '@packages/types' 5 | 6 | export interface CosState { 7 | cosKeys: object 8 | cos: object 9 | expires: number | string 10 | } 11 | 12 | export const useCosStore = defineStore('cos', { 13 | state: (): CosState => ({ cosKeys: {}, cos: {}, expires: '' }), 14 | getters: { 15 | COS_KEYS(state: CosState) { 16 | return state.cosKeys 17 | }, 18 | COS(state: CosState) { 19 | return state.cos 20 | } 21 | }, 22 | actions: { 23 | setCosKeys(data: any) { 24 | const options = { 25 | Bucket: data.bucket, 26 | Region: data.region 27 | } 28 | this.cosKeys = options 29 | sessionStorage.setItem('cosKeys', JSON.stringify(options)) 30 | }, 31 | createCOS(data: any) { 32 | const cosInstance = new COS({ 33 | getAuthorization(options, callback) { 34 | callback({ 35 | TmpSecretId: data.tmpSecretId, 36 | TmpSecretKey: data.tmpSecretKey, 37 | XCosSecurityToken: data.sessionToken, 38 | ExpiredTime: data.expiredTime, 39 | StartTime: data.StartTime 40 | }) 41 | } 42 | }) 43 | this.cos = cosInstance 44 | sessionStorage.setItem('cos', JSON.stringify(cosInstance)) 45 | }, 46 | setExpires(data: any) { 47 | const timeDiff = data.expiredTime - data.startTime - 600 48 | const date = (new Date().getTime() / 1000) as any 49 | const dateNow = parseInt(date) 50 | this.expires = dateNow + timeDiff 51 | }, 52 | async getTmpKeys() { 53 | const date = (new Date().getTime() / 1000) as any 54 | const dateNow = parseInt(date) // 当前时间戳 55 | if (dateNow > this.expires) { 56 | // 初次和失效后调取 57 | const res = (await getQcloudTmpkeys()) as Response 58 | if (!res || res.code !== 0) return 59 | const data = res.data 60 | this.setExpires(data) 61 | this.setCosKeys(data) 62 | this.createCOS(data) 63 | } 64 | } 65 | } 66 | }) 67 | -------------------------------------------------------------------------------- /packages/components/search/index.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 67 | 68 | 75 | -------------------------------------------------------------------------------- /apps/vue3-app/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, presetAttributify, presetUno } from 'unocss' 2 | import presetRemToPx from '@unocss/preset-rem-to-px' 3 | 4 | import { getColorCssVars } from '@packages/token' 5 | import { theme } from 'ant-design-vue' 6 | const { defaultAlgorithm, defaultSeed } = theme 7 | const mapToken = defaultAlgorithm(defaultSeed) 8 | 9 | // https://unocss.dev/interactive/ 10 | export default defineConfig({ 11 | content: { 12 | filesystem: ['**/*.{html,js,ts,jsx,tsx,vue,svelte,astro}'] 13 | }, 14 | presets: [ 15 | presetUno(), 16 | presetAttributify({ 17 | prefix: 'un-', 18 | prefixedOnly: false 19 | // ignoreAttributes: [] 20 | }), 21 | presetRemToPx() 22 | ], 23 | // rules: [[/^opacity-brand-(\w+)$/, ([, d]) => ({ opacity: `var(--${d}-opacity)` })]], 24 | theme: { 25 | colors: { 26 | // 使用 antd token 生成的 css 变量,从这里导出。查看所有变量,查看文档 unocss.md。 27 | ...getColorCssVars(mapToken), 28 | // 下面这些颜色变量在 getColorVars(mapToken) 方法中都有,简单列举一些。结尾带数字的是自定义变量。 29 | primary: 'var(--ant-color-primary)', // class="text-primary" 30 | primaryHover: 'var(--ant-color-primary-hover)', 31 | primaryActive: 'var(--ant-color-primary-active)', 32 | primaryTextHover: 'var(--ant-color-primary-text-hover)', 33 | primaryText: 'var(--ant-color-primary-text)', 34 | primaryTextActive: 'var(--ant-color-primary-text-active)', 35 | primaryBg: 'var(--ant-color-primary-bg)', 36 | primaryBgHover: 'var(--ant-color-primary-bg-hover)', 37 | primaryBorder: 'var(--ant-color-primary-border)', 38 | primaryBorderHover: 'var(--ant-color-primary-border-hover)', 39 | text: 'var(--ant-color-text)', 40 | text2: 'var(--ant-color-text-secondary)', 41 | text3: 'var(--ant-color-text-tertiary)', 42 | text4: 'var(--ant-color-text-quaternary)', 43 | disabled: 'var(--ant-color-text-secondary)', 44 | border: 'var(--ant-color-border)', 45 | border2: 'var(--ant-color-border-secondary)', 46 | fill: 'var(--ant-color-fill)', 47 | fill2: 'var(--ant-color-fill-secondary)', 48 | fill3: 'var(--ant-color-fill-tertiary)', 49 | fill4: 'var(--ant-color-fill-quaternary)' 50 | } 51 | } 52 | }) 53 | -------------------------------------------------------------------------------- /apps/nuxt3-app/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, presetAttributify, presetUno } from 'unocss' 2 | import presetRemToPx from '@unocss/preset-rem-to-px' 3 | 4 | import { getColorCssVars } from '@packages/token' 5 | import { theme } from 'ant-design-vue' 6 | const { defaultAlgorithm, defaultSeed } = theme 7 | const mapToken = defaultAlgorithm(defaultSeed) 8 | 9 | // https://unocss.dev/interactive/ 10 | export default defineConfig({ 11 | content: { 12 | filesystem: ['**/*.{html,js,ts,jsx,tsx,vue,svelte,astro}'] 13 | }, 14 | presets: [ 15 | presetUno(), 16 | presetAttributify({ 17 | prefix: 'un-', 18 | prefixedOnly: false 19 | // ignoreAttributes: [] 20 | }), 21 | presetRemToPx() 22 | ], 23 | // rules: [[/^opacity-brand-(\w+)$/, ([, d]) => ({ opacity: `var(--${d}-opacity)` })]], 24 | theme: { 25 | colors: { 26 | // 使用 antd token 生成的 css 变量,从这里导出。查看所有变量,查看文档 unocss.md。 27 | ...getColorCssVars(mapToken), 28 | // 下面这些颜色变量在 getColorVars(mapToken) 方法中都有,简单列举一些。结尾带数字的是自定义变量。 29 | primary: 'var(--ant-color-primary)', // class="text-primary" 30 | primaryHover: 'var(--ant-color-primary-hover)', 31 | primaryActive: 'var(--ant-color-primary-active)', 32 | primaryTextHover: 'var(--ant-color-primary-text-hover)', 33 | primaryText: 'var(--ant-color-primary-text)', 34 | primaryTextActive: 'var(--ant-color-primary-text-active)', 35 | primaryBg: 'var(--ant-color-primary-bg)', 36 | primaryBgHover: 'var(--ant-color-primary-bg-hover)', 37 | primaryBorder: 'var(--ant-color-primary-border)', 38 | primaryBorderHover: 'var(--ant-color-primary-border-hover)', 39 | text: 'var(--ant-color-text)', 40 | text2: 'var(--ant-color-text-secondary)', 41 | text3: 'var(--ant-color-text-tertiary)', 42 | text4: 'var(--ant-color-text-quaternary)', 43 | disabled: 'var(--ant-color-text-secondary)', 44 | border: 'var(--ant-color-border)', 45 | border2: 'var(--ant-color-border-secondary)', 46 | fill: 'var(--ant-color-fill)', 47 | fill2: 'var(--ant-color-fill-secondary)', 48 | fill3: 'var(--ant-color-fill-tertiary)', 49 | fill4: 'var(--ant-color-fill-quaternary)' 50 | } 51 | } 52 | }) 53 | -------------------------------------------------------------------------------- /packages/hooks/table.ts: -------------------------------------------------------------------------------- 1 | import { ref, reactive } from 'vue' 2 | import type { Pagination } from '@packages/types' 3 | 4 | const defaultPagination: Pagination = { 5 | size: 'small', 6 | current: 1, 7 | defaultCurrent: 1, 8 | pageSize: 10, 9 | total: 0, 10 | showTotal: (total: number | string) => `共${total}条`, 11 | showLessItems: true, 12 | showQuickJumper: true, 13 | showSizeChanger: true, 14 | pageSizeOptions: ['10', '20', '30', '50', '100'] 15 | } 16 | 17 | export function usePagination(pagination?: Record) { 18 | const pag = pagination || {} 19 | const _pagination: Pagination | false = reactive({ ...defaultPagination, ...pag }) 20 | 21 | const loading = ref(false) 22 | const data = ref>>([]) 23 | 24 | return { 25 | loading, 26 | data, 27 | pagination: _pagination 28 | } 29 | } 30 | 31 | export function useRowSelection() { 32 | const selectedRowKeys = ref([]) 33 | const selectedRows = ref([]) 34 | 35 | const cleanup = () => { 36 | selectedRowKeys.value = [] 37 | selectedRows.value = [] 38 | } 39 | 40 | const onChange = (_selectedRowKeys: (string | number)[], _selectedRows: any[]) => { 41 | selectedRowKeys.value = _selectedRowKeys 42 | selectedRows.value = _selectedRows 43 | } 44 | 45 | const onSelect = (record: any, selected: boolean, selectedRows: any[]) => { 46 | // 47 | } 48 | 49 | const onSelectAll = (selected: boolean, selectedRows: any[], changeRows: any[]) => { 50 | // 51 | } 52 | 53 | return { selectedRowKeys, selectedRows, cleanup, onChange } 54 | } 55 | 56 | export function useCustomRow({ rowKey = 'id' }: { rowKey: string }) { 57 | const currentRowKey = ref(-1) 58 | const lastRowKey = ref(-1) 59 | const rowKeys = ref([]) 60 | 61 | const customRow = (record: Record) => ({ 62 | onClick() { 63 | lastRowKey.value = currentRowKey.value 64 | currentRowKey.value = record[rowKey] 65 | rowKeys.value.push(record[rowKey]) 66 | } 67 | }) 68 | 69 | const rowClassName = (record: Record) => { 70 | return rowKeys.value.includes(record[rowKey]) ? 'clicked' : '' 71 | } 72 | 73 | return { currentRowKey, lastRowKey, rowKeys, customRow, rowClassName } 74 | } 75 | -------------------------------------------------------------------------------- /packages/hooks/upload.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { message } from 'ant-design-vue' 3 | import type { UploadProps, UploadFile } from 'ant-design-vue' 4 | 5 | export interface UploadConfig { 6 | upload?: boolean 7 | maxCount?: number 8 | maxSize?: number // MB 9 | accept?: string // .zip,.jpg 10 | } 11 | 12 | export function useUpload({ maxCount, maxSize, accept, upload = true }: UploadConfig) { 13 | const fileList = ref([]) 14 | const uploading = ref(false) 15 | 16 | const beforeUpload: UploadProps['beforeUpload'] = file => { 17 | if (maxCount && fileList.value.length === maxCount) { 18 | message.error(`最多只能上传${maxCount}个文件`) 19 | return false 20 | } 21 | if (maxSize && file.size > maxSize * 1024 * 1024) { 22 | message.error(`上传文件大小不能超过${maxSize}MB`) 23 | fileList.value.pop() 24 | return false 25 | } 26 | const suffixes = accept?.split(',') || [] 27 | const sIndex = file.name.lastIndexOf('.') 28 | const suffix = file.name.slice(sIndex).toLowerCase() 29 | if (suffixes?.length && !suffixes.includes(suffix)) { 30 | message.error(`文件格式错误,请上传${accept}格式的文件`) 31 | return false 32 | } 33 | if (upload) return true 34 | fileList.value = [...fileList.value, file] 35 | return false 36 | } 37 | 38 | const onRemove: UploadProps['onRemove'] = file => { 39 | const index = fileList.value?.indexOf(file) 40 | const newFileList = fileList.value?.slice() 41 | newFileList?.splice(index!, 1) 42 | fileList.value = newFileList 43 | } 44 | 45 | const onUpload = () => { 46 | const formData = new FormData() 47 | fileList.value?.forEach((file: UploadFile) => { 48 | formData.append('files[]', file as any) 49 | }) 50 | uploading.value = true 51 | 52 | // request('https://www.mocky.io/v2/5cc8019d300000980a055e76', { 53 | // method: 'post', 54 | // data: formData 55 | // }) 56 | // .then(() => { 57 | // fileList.value = [] 58 | // uploading.value = false 59 | // message.success('upload successfully.') 60 | // }) 61 | // .catch(() => { 62 | // uploading.value = false 63 | // message.error('upload failed.') 64 | // }) 65 | } 66 | 67 | return { fileList, uploading, onRemove, beforeUpload, onUpload } 68 | } 69 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'vue-eslint-parser', 3 | parserOptions: { 4 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 5 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features 6 | sourceType: 'module', // Allows for the use of imports 7 | ecmaFeatures: { 8 | tsx: true, 9 | jsx: true 10 | } 11 | }, 12 | env: { 13 | node: true, 14 | browser: true 15 | }, 16 | plugins: ['@typescript-eslint'], 17 | extends: [ 18 | // 'eslint:recommended', 19 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 20 | 'plugin:vue/vue3-recommended', 21 | 'plugin:prettier/recommended' // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 22 | ], 23 | rules: { 24 | // js 25 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 26 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 27 | 'no-alert': process.env.NODE_ENV === 'production' ? 'error' : 'off', 28 | 'no-tabs': 2, // 禁止使用tabs 29 | 'no-useless-escape': 0, 30 | 'no-var': 2, // 使用let和const代替var 31 | 'no-mixed-spaces-and-tabs': 2, // 不允许混用tab和空格 32 | 'no-useless-return': 0, 33 | 'arrow-parens': 0, 34 | camelcase: 0, // 禁用驼峰命名检测 35 | // ts 36 | '@typescript-eslint/no-explicit-any': 0, 37 | '@typescript-eslint/ban-types': 0, 38 | '@typescript-eslint/no-empty-interface': 0, 39 | '@typescript-eslint/no-empty-function': 0, 40 | '@typescript-eslint/explicit-module-boundary-types': 0, 41 | '@typescript-eslint/ban-ts-comment': 0, 42 | '@typescript-eslint/no-non-null-assertion': 0, 43 | '@typescript-eslint/no-unused-vars': 1, 44 | '@typescript-eslint/no-var-requires': 0, 45 | '@typescript-eslint/consistent-type-imports': 1, 46 | // vue 47 | 'vue/no-parsing-error': [2, { 'x-invalid-end-tag': false }], 48 | 'vue/html-self-closing': 0, 49 | 'vue/no-use-v-if-with-v-for': 2, 50 | 'vue/html-closing-bracket-newline': 0, 51 | 'vue/max-attributes-per-line': 0, 52 | 'vue/no-unused-components': 1, 53 | 'vue/no-mutating-props': 0, 54 | 'vue/multi-word-component-names': 0 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /apps/vue3-app/src/router/routes/components.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | const Layout = () => import('@/components/layout/index.vue') 4 | 5 | const routes: RouteRecordRaw[] = [ 6 | { 7 | path: '/components', 8 | name: 'components', 9 | meta: { 10 | title: '常用组件', 11 | icon: 'icon-reloadtime' 12 | }, 13 | redirect: { name: 'provide-inject' }, 14 | component: Layout, 15 | children: [ 16 | { 17 | path: 'router', 18 | name: 'router', 19 | meta: { 20 | title: 'router', 21 | icon: 'icon-reloadtime' 22 | }, 23 | component: () => import('features/components/router/index.vue') 24 | }, 25 | { 26 | path: 'store', 27 | name: 'store', 28 | meta: { 29 | title: 'store', 30 | icon: 'icon-unorderedlist' 31 | }, 32 | component: () => import('features/components/store/index.vue') 33 | }, 34 | { 35 | path: 'provide-inject', 36 | name: 'provide-inject', 37 | meta: { 38 | title: 'provide', 39 | icon: 'icon-appstoreadd' 40 | }, 41 | component: () => import('features/components/provide/index.vue') 42 | }, 43 | { 44 | path: 'bus', 45 | name: 'bus', 46 | meta: { 47 | title: 'bus', 48 | icon: 'icon-user' 49 | }, 50 | component: () => import('features/components/bus/index.vue') 51 | }, 52 | { 53 | path: 'table', 54 | name: 'table', 55 | meta: { 56 | title: 'table', 57 | icon: 'icon-appstore' 58 | }, 59 | component: () => import('features/components/table/index.vue') 60 | }, 61 | { 62 | path: 'fixed-table', 63 | name: 'fixed-table', 64 | meta: { 65 | title: 'fixed-table', 66 | icon: 'icon-appstore' 67 | }, 68 | component: () => import('features/components/table/fixed.vue') 69 | }, 70 | { 71 | path: 'sortable', 72 | name: 'sortable', 73 | meta: { 74 | title: 'sortable', 75 | icon: 'icon-setting' 76 | }, 77 | component: () => import('features/components/sortable/index.vue') 78 | } 79 | ] 80 | } 81 | ] 82 | 83 | export default routes 84 | -------------------------------------------------------------------------------- /packages/token/utils.ts: -------------------------------------------------------------------------------- 1 | export const kebabCase = (str: string) => str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`) 2 | 3 | // /^(color|font|line|link|motion|border|margin|padding(?!con)|screen((?!min|max).)*$|boxShadow(?!pop|dra|tab|car))/ 4 | // /^(blue|purple|cyan|green|magenta|pink|red|orange|yellow|volcano|geekblue|gold|lime)/ 5 | export function generateCssVars(token: any, prefix = 'ant', keys?: Array) { 6 | const pattern = 7 | /^(blue|purple|cyan|green|magenta|pink|red|orange|yellow|volcano|geekblue|gold|lime)/i 8 | const pxPattern = /(size|width|radius|margin|screen|padding)/i 9 | keys = keys || Object.keys(token).filter(([key]) => !pattern.test(key)) 10 | const cssVars = keys.map( 11 | key => `--${prefix}-${kebabCase(key)}: ${token[key]}${pxPattern.test(key) ? 'px' : ''}` 12 | ) 13 | return cssVars.join(';\n') 14 | } 15 | 16 | export function getCssVars(token: any, prefix = 'ant') { 17 | const pattern = 18 | /^(blue|purple|cyan|green|magenta|pink|red|orange|yellow|volcano|geekblue|gold|lime)/i 19 | const keys = Object.keys(token).filter(([key]) => !pattern.test(key)) 20 | return keys.map(key => ({ [key]: `--${prefix}-${kebabCase(key)}` })) 21 | } 22 | 23 | const excludedKeys = /^(colorWhite|colorDark)/ 24 | export function generateColorCssVars(token: any, prefix = 'ant') { 25 | const pattern = 26 | /^(blue|purple|cyan|green|magenta|pink|red|orange|yellow|volcano|geekblue|gold|lime)/i 27 | const pxPattern = /(size|width|radius|margin|screen|padding)/i 28 | const keys = Object.keys(token).filter( 29 | key => !pattern.test(key) && key.startsWith('color') && !excludedKeys.test(key) 30 | ) 31 | const cssVars = keys.map( 32 | key => `--${prefix}-${kebabCase(key)}: ${token[key]}${pxPattern.test(key) ? 'px' : ''}` 33 | ) 34 | return cssVars.join(';\n') 35 | } 36 | 37 | // 获取 antd token 生成的 css 变量(color开头的token), colorPrimary -> { primary: var(--ant-color-primary) } 38 | export function getColorCssVars(token: any, prefix = 'ant') { 39 | const keys = Object.keys(token).filter(k => k.startsWith('color') && !excludedKeys.test(k)) 40 | return keys.reduce((prev: Record, value: string) => { 41 | let shortKey = value.replace('color', '') 42 | shortKey = shortKey.replace(shortKey[0], shortKey[0].toLowerCase()) 43 | prev[shortKey] = `var(--${prefix}-${kebabCase(value)})` 44 | return prev 45 | }, {}) 46 | } 47 | -------------------------------------------------------------------------------- /apps/nuxt3-app/router/routes/components.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | // const Layout = () => import('@/components/layout/index.vue') 4 | const Layout = () => import('@/components/layout/Router.vue') 5 | 6 | const routes: RouteRecordRaw[] = [ 7 | { 8 | path: '/components', 9 | name: 'components', 10 | meta: { 11 | title: '常用组件', 12 | icon: 'icon-reloadtime' 13 | }, 14 | redirect: { name: 'provide-inject' }, 15 | component: Layout, 16 | children: [ 17 | { 18 | path: 'router', 19 | name: 'router', 20 | meta: { 21 | title: 'router', 22 | icon: 'icon-reloadtime' 23 | }, 24 | component: () => import('features/components/router/index.vue') 25 | }, 26 | { 27 | path: 'store', 28 | name: 'store', 29 | meta: { 30 | title: 'store', 31 | icon: 'icon-unorderedlist' 32 | }, 33 | component: () => import('features/components/store/index.vue') 34 | }, 35 | { 36 | path: 'provide-inject', 37 | name: 'provide-inject', 38 | meta: { 39 | title: 'provide', 40 | icon: 'icon-appstoreadd' 41 | }, 42 | component: () => import('features/components/provide/index.vue') 43 | }, 44 | { 45 | path: 'bus', 46 | name: 'bus', 47 | meta: { 48 | title: 'bus', 49 | icon: 'icon-user' 50 | }, 51 | component: () => import('features/components/bus/index.vue') 52 | }, 53 | { 54 | path: 'table', 55 | name: 'table', 56 | meta: { 57 | title: 'table', 58 | icon: 'icon-appstore' 59 | }, 60 | component: () => import('features/components/table/index.vue') 61 | }, 62 | { 63 | path: 'fixed-table', 64 | name: 'fixed-table', 65 | meta: { 66 | title: 'fixed-table', 67 | icon: 'icon-appstore' 68 | }, 69 | component: () => import('features/components/table/fixed.vue') 70 | }, 71 | { 72 | path: 'sortable', 73 | name: 'sortable', 74 | meta: { 75 | title: 'sortable', 76 | icon: 'icon-setting' 77 | }, 78 | component: () => import('features/components/sortable/index.vue') 79 | } 80 | ] 81 | } 82 | ] 83 | 84 | export default routes 85 | -------------------------------------------------------------------------------- /packages/components/phone/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 66 | 67 | 87 | -------------------------------------------------------------------------------- /apps/nuxt3-app/app.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /features/components/table/index.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /apps/vue3-app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { loadEnv, defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import jsx from '@vitejs/plugin-vue-jsx' 4 | import UnoCSS from 'unocss/vite' 5 | import { createHtmlPlugin } from 'vite-plugin-html' 6 | import AutoImport from 'unplugin-auto-import/vite' 7 | import Components from 'unplugin-vue-components/vite' 8 | import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers' 9 | import path from 'path' 10 | 11 | // @ts-ignore 12 | export default ({ mode, command }) => { 13 | console.log('mode', mode) 14 | // const env = loadEnv(mode, process.cwd()) 15 | const envDir = path.resolve(process.cwd(), 'env') 16 | const env = loadEnv(mode, envDir) 17 | console.log('env', env) 18 | 19 | return defineConfig({ 20 | define: { 21 | __APP_TITLE__: JSON.stringify(env.VITE_APP_TITLE), 22 | __DYNAMIC_MENU__: env.VITE_DYNAMIC_MENU 23 | }, 24 | envDir, 25 | build: { 26 | outDir: env.VITE_OUTDIR || 'dist' 27 | }, 28 | css: { 29 | preprocessorOptions: { 30 | less: { 31 | javascriptEnabled: true, 32 | // modifyVars: {}, 33 | additionalData: `@import "@packages/styles/theme.less";` 34 | } 35 | } 36 | }, 37 | plugins: [ 38 | vue(), 39 | jsx(), 40 | UnoCSS(), 41 | AutoImport({ 42 | imports: ['vue', 'vue-router'], 43 | dts: false 44 | }), 45 | Components({ 46 | resolvers: [ 47 | AntDesignVueResolver({ 48 | importStyle: false // css in js 49 | }) 50 | ] 51 | }), 52 | createHtmlPlugin({ 53 | inject: { 54 | data: { 55 | // title: env.VITE_APP_TITLE, 56 | // injectScript: `` 57 | } 58 | } 59 | }) 60 | ], 61 | base: env.VITE_APP_BASE || '/', 62 | resolve: { 63 | alias: { 64 | '@': path.resolve(__dirname, 'src'), 65 | vue: 'vue/dist/vue.esm-bundler.js', 66 | features: path.resolve(__dirname, '../../features') 67 | }, 68 | extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.vue', '.json', '.less', '.scss', '.css'] 69 | }, 70 | esbuild: { 71 | drop: command === 'build' ? ['console', 'debugger'] : [] 72 | }, 73 | server: { 74 | // proxy: { 75 | // [env.VITE_API_URL]: { 76 | // target: env.VITE_PROXY_TARGET, 77 | // changeOrigin: true, 78 | // secure: false 79 | // // rewrite: path => path.replace(new RegExp(`^${env.VITE_API_URL}`), '') 80 | // } 81 | // } 82 | } 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /apps/nuxt3-app/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | // 环境变量配置 5 | import { loadEnv } from 'vite' 6 | const isBuildCommand = command => ['build', 'generate'].includes(command) 7 | const command = process.env.npm_lifecycle_script.match(/nuxi\s(\S+)/)?.[1] 8 | const defaultMode = isBuildCommand(command) ? 'production' : 'development' 9 | const mode = process.env.npm_lifecycle_script.match(/--mode\s(.*)/)?.[1] || defaultMode 10 | const env = loadEnv(mode, path.resolve(process.cwd(), 'env')) 11 | const baseURL = env.VITE_APP_BASE || '/' 12 | Object.assign(process.env, env, { 13 | MODE: mode, 14 | BASE_URL: baseURL, 15 | PROD: isBuildCommand(command), 16 | DEV: command === 'dev' 17 | // SSR: false 18 | }) 19 | console.log('mode', mode) 20 | console.log('env', env) 21 | 22 | // https://nuxt.com/docs/api/configuration/nuxt-config 23 | export default defineNuxtConfig({ 24 | // ssr: false, 25 | ssr: process.env.NODE_ENV !== 'development', // fix antdv ssr warning 26 | alias: { 27 | features: fileURLToPath(new URL('../../features', import.meta.url)) 28 | }, 29 | baseURL, 30 | // buildDir: env.VITE_OUTDIR || '.nuxt', 31 | extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.vue', '.json', '.less', '.scss', '.css'], 32 | devtools: { enabled: true }, 33 | modules: [ 34 | '@ant-design-vue/nuxt', 35 | '@unocss/nuxt', 36 | '@pinia/nuxt', 37 | '@pinia-plugin-persistedstate/nuxt' 38 | ], 39 | // plugins: ['~/plugins/external'], 40 | nitro: { 41 | devProxy: { 42 | [env.VITE_API_URL]: { 43 | target: env.VITE_PROXY_TARGET, 44 | changeOrigin: true, 45 | secure: false 46 | // rewrite: path => path.replace(new RegExp(`^${env.VITE_API_URL}`), '') 47 | } 48 | }, 49 | esbuild: { 50 | // drop: command === 'build' ? ['console', 'debugger'] : [] 51 | drop: [] 52 | } 53 | // prerender: { 54 | // failOnError: false 55 | // } 56 | }, 57 | // routeRules: { 58 | // '/': { prerender: false } 59 | // }, 60 | // appConfig: {}, 61 | // runtimeConfig: {}, 62 | vite: { 63 | // optimizeDeps: { include: [] }, 64 | ssr: { noExternal: ['vue-echarts'] }, 65 | define: { 66 | __APP_TITLE__: JSON.stringify(env.VITE_APP_TITLE), 67 | __DYNAMIC_MENU__: env.VITE_DYNAMIC_MENU 68 | }, 69 | css: { 70 | preprocessorOptions: { 71 | less: { 72 | javascriptEnabled: true, 73 | // modifyVars: {}, 74 | additionalData: `@import "@packages/styles/theme.less";` 75 | } 76 | } 77 | } 78 | }, 79 | postcss: { 80 | plugins: { 81 | '@unocss/postcss': {} 82 | } 83 | } 84 | }) 85 | -------------------------------------------------------------------------------- /apps/vue3-app/src/components/search/demo/index.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 88 | 89 | 100 | -------------------------------------------------------------------------------- /packages/hooks/sortable.ts: -------------------------------------------------------------------------------- 1 | import type { Options, SortableEvent } from 'sortablejs' 2 | import Sortable from 'sortablejs' 3 | import type { Ref } from 'vue' 4 | import { ref, onMounted } from 'vue' 5 | 6 | export interface SortableOptions extends Options { 7 | callInternalMethods?: boolean // true: 调用内部一些方法后再执行 options 中的方法 8 | } 9 | 10 | const defaultOptions = { 11 | scroll: true, 12 | animation: 0, 13 | group: 'sortableGroup', 14 | ghostClass: 'sortable-ghost', 15 | dragClass: 'sortable-ghost', 16 | handle: '.sortable-handle', 17 | filter: '.sortable-ignore-elements', 18 | preventOnFilter: false, 19 | draggable: '.sortable-draggable', 20 | fallbackTolerance: 5 21 | } 22 | 23 | const removeNode = (node: HTMLElement) => { 24 | if (node.parentElement !== null) { 25 | node.parentElement.removeChild(node) 26 | } 27 | } 28 | 29 | const insertNodeAt = (fatherNode: HTMLElement, node: HTMLElement, position: number) => { 30 | const refNode = 31 | position === 0 ? fatherNode.children[0] : fatherNode.children[position - 1].nextSibling 32 | fatherNode.insertBefore(node, refNode) 33 | } 34 | 35 | const updatePosition = (list: Ref, oldIndex: number, newIndex: number) => { 36 | const item = list.value.splice(oldIndex!, 1)[0] 37 | list.value.splice(newIndex!, 0, { ...item }) 38 | } 39 | 40 | export function useSortable(list: Ref, selector: string, options: SortableOptions = {}) { 41 | const sortable = ref() 42 | const drag = ref(false) 43 | 44 | const _internalMethods = { 45 | onStart(event: SortableEvent) { 46 | // console.log('onStart', event) 47 | drag.value = true 48 | // window.scrollTo(0, 0) 49 | options.onStart?.(event) 50 | }, 51 | onEnd(event: SortableEvent) { 52 | // console.log('onEnd', event) 53 | drag.value = false 54 | // updatePosition(list, event.oldIndex!, event.newIndex!) 55 | options.onEnd?.(event) 56 | }, 57 | onUpdate(event: SortableEvent) { 58 | removeNode(event.item) 59 | insertNodeAt(event.from, event.item, event.oldIndex!) 60 | updatePosition(list, event.oldIndex!, event.newIndex!) 61 | options.onUpdate?.(event) 62 | } 63 | } 64 | let internalMethods = {} 65 | const callInternalMethods = 66 | options.callInternalMethods !== undefined ? options.callInternalMethods : true 67 | if (callInternalMethods) { 68 | internalMethods = _internalMethods 69 | } 70 | 71 | const mergedOptions = { 72 | ...defaultOptions, 73 | ...options, 74 | ...internalMethods 75 | } 76 | 77 | onMounted(() => { 78 | const el = document.querySelector(selector) as HTMLElement 79 | sortable.value = Sortable.create(el, mergedOptions) 80 | }) 81 | 82 | return { sortable, drag, list } 83 | } 84 | -------------------------------------------------------------------------------- /apps/nuxt3-app/components/layout/Nav.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 57 | 58 | 114 | -------------------------------------------------------------------------------- /packages/components/layout/Nav.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 58 | 59 | 115 | -------------------------------------------------------------------------------- /apps/vue3-app/src/components/layout/Nav.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 58 | 59 | 115 | -------------------------------------------------------------------------------- /packages/utils/datetime.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import weekday from 'dayjs/plugin/weekday' 3 | 4 | dayjs.extend(weekday) 5 | 6 | // 时间戳格式化 7 | export const formatDatetime = (datetime: dayjs.ConfigType, format?: string) => { 8 | return datetime ? dayjs(datetime).format(format || 'YYYY-MM-DD HH:mm:ss') : '' 9 | } 10 | 11 | // 10位或者13位时间戳格式化成字符串 12 | export const unifiedTimeStamp = (datetime: dayjs.ConfigType, format?: string) => { 13 | const str = datetime + '' 14 | const time = str.length === 10 ? +str * 1000 : +str 15 | return formatDatetime(time, format) 16 | } 17 | 18 | // 时间转时间戳(秒) 19 | export const formatTimeStamp = (datetime: dayjs.ConfigType) => { 20 | return dayjs(datetime).unix() 21 | } 22 | 23 | // 获取星期 24 | export const getWeekday = (datetime: dayjs.ConfigType, weekdays?: []) => { 25 | const cnWeekdays = weekdays || ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] 26 | const weekday = dayjs(datetime).weekday() 27 | return cnWeekdays[weekday] 28 | } 29 | 30 | // 时分转为分 12:06 31 | export function hourToMinute(time: string): number { 32 | if (!time || !time.includes(':')) return 0 33 | const hour = Number(time.split(':')[0]) 34 | const minute = Number(time.split(':')[1]) 35 | return hour * 60 + minute 36 | } 37 | 38 | // 秒数转为小时数 39 | export function convertSecondToHour(second: number) { 40 | return Math.floor(second / 3600) 41 | } 42 | 43 | // 秒数转为分钟数 44 | export function convertSecondToMinute(second: number) { 45 | return Math.floor(second / 60) 46 | } 47 | 48 | // 秒数转为时分秒 49 | export function convertSecondToHMS(second: number, ignoreZero = true) { 50 | if (!second && ignoreZero) return '' 51 | let hour: string | number = Math.floor(second / 3600) 52 | hour = hour < 10 ? '0' + hour : hour 53 | let minute: string | number = Math.floor((second % 3600) / 60) 54 | minute = minute < 10 ? '0' + minute : minute 55 | let _second: string | number = Math.floor(second % 60) 56 | _second = _second < 10 ? '0' + _second : _second 57 | return [hour, minute, _second].filter(x => x !== null).join(':') 58 | } 59 | 60 | // 秒数转为天时分 61 | export function convertSecondToDHM(second: number, ignoreZero = true) { 62 | if (!second && ignoreZero) return '' 63 | const daySeconds = 24 * 3600 64 | const negative = second < 0 65 | second = Math.abs(second) 66 | const day: string | number = Math.floor(second / daySeconds) 67 | const hour: string | number = Math.floor((second % daySeconds) / 3600) 68 | const minute: string | number = Math.floor(((second % daySeconds) % 3600) / 60) 69 | return `${negative ? '-' : ''}${day}天${hour}小时${minute}分` 70 | } 71 | 72 | // 获取几天后的时间数组 73 | export const getAddDayDatetime = (day: number, useCurrTime = false) => { 74 | const valueFormatWithTime = 'YYYY-MM-DD HH:mm:ss' 75 | const valueFormat = 'YYYY-MM-DD' 76 | const startTime = dayjs() 77 | const endTime = startTime.add(day, 'day') 78 | let startTimeStr, endTimeStr 79 | if (useCurrTime) { 80 | startTimeStr = startTime.format(valueFormatWithTime) 81 | endTimeStr = endTime.format(valueFormatWithTime) 82 | } else { 83 | startTimeStr = `${startTime.format(valueFormat)} 00:00:00` 84 | endTimeStr = `${endTime.format(valueFormat)} 23:59:59` 85 | } 86 | return [startTimeStr, endTimeStr] 87 | } 88 | -------------------------------------------------------------------------------- /packages/styles/index.less: -------------------------------------------------------------------------------- 1 | @import './antd'; 2 | 3 | // required 4 | .label-required::before { 5 | content: '*'; 6 | color: theme('colors.error'); 7 | margin-right: 4px; 8 | } 9 | 10 | // overflow 11 | .overflow-ellipsis { 12 | overflow: hidden; 13 | white-space: nowrap; 14 | text-overflow: ellipsis; 15 | } 16 | 17 | // column-wrap 18 | .column-wrap { 19 | display: flex; 20 | align-items: center; 21 | } 22 | 23 | // column-mark 24 | .column-tooltip { 25 | .ivu-tooltip-content { 26 | max-height: 400px; 27 | overflow-y: auto; 28 | } 29 | } 30 | 31 | .column-mark { 32 | display: inline-block; 33 | width: 70px; 34 | overflow: hidden; 35 | white-space: nowrap; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | // dropdown-icon 40 | .dropdown-icon { 41 | font-size: 12px; 42 | vertical-align: baseline; 43 | } 44 | 45 | // card 46 | .g-card { 47 | display: flex; 48 | background: #fff; 49 | border-radius: 4px; 50 | font-size: 14px; 51 | transition: all 0.2s ease-in-out; 52 | } 53 | 54 | // readonly-text 55 | .readonly-text { 56 | .ivu-input[disabled] { 57 | background-color: transparent; 58 | opacity: 1; 59 | cursor: default; 60 | color: theme('colors.text'); 61 | } 62 | } 63 | 64 | // empty-style 65 | .empty-style { 66 | text-align: center; 67 | color: theme('colors.text2'); 68 | 69 | .ant-empty-image { 70 | margin-bottom: 8px; 71 | } 72 | } 73 | 74 | // nprogress style 75 | #nprogress .bar { 76 | background: theme('colors.primary'); 77 | } 78 | 79 | // reset-btn 80 | .reset-btn { 81 | color: #fff !important; 82 | background-color: theme('colors.warning'); 83 | border-color: theme('colors.warning'); 84 | text-shadow: 0 -1px 0 rgb(0 0 0 / 12%); 85 | box-shadow: 0 2px 0 rgb(0 0 0 / 5%); 86 | 87 | &:hover, &:focus, &:active { 88 | text-decoration: none; 89 | } 90 | 91 | &:hover, &:focus { 92 | background-color: theme('colors.warningHover'); 93 | border-color: theme('colors.warningHover'); 94 | } 95 | 96 | &:focus, &:active { 97 | outline: 0; 98 | } 99 | 100 | &:active { 101 | background-color: theme('colors.warningActive'); 102 | border-color: theme('colors.warningActive'); 103 | box-shadow: none; 104 | outline: 0; 105 | } 106 | } 107 | 108 | // text-btn 109 | .text-btn-success { 110 | cursor: pointer; 111 | color: theme('colors.success'); 112 | 113 | &:hover { 114 | color: theme('colors.successHover'); 115 | } 116 | } 117 | 118 | .text-btn-error { 119 | cursor: pointer; 120 | color: theme('colors.error'); 121 | 122 | &:hover { 123 | color: theme('colors.errorHover'); 124 | } 125 | } 126 | 127 | .text-btn-disabled { 128 | cursor: not-allowed; 129 | color: theme('colors.text2'); 130 | } 131 | 132 | .text-btn, .phone-icon, .im-icon { 133 | color: theme('colors.primary'); 134 | cursor: pointer; 135 | } 136 | 137 | .text-btn:hover, .phone-icon:hover { 138 | color: theme('colors.primaryHover'); 139 | } 140 | 141 | .text-btn + .text-btn { 142 | margin-left: 8px; 143 | } 144 | 145 | .hover-btn { 146 | cursor: pointer; 147 | 148 | &:hover { 149 | color: theme('colors.primary') !important; 150 | } 151 | } 152 | 153 | .phone-icon { 154 | font-size: 20px; 155 | margin-left: 8px; 156 | } 157 | -------------------------------------------------------------------------------- /packages/components/search/demo/columns.ts: -------------------------------------------------------------------------------- 1 | import { ref, reactive } from 'vue' 2 | 3 | export interface Options { 4 | label: string 5 | value: any 6 | } 7 | 8 | export const columns = [ 9 | { 10 | label: '姓名', 11 | searchType: 'input', 12 | type: 'text', 13 | key: 'name1', 14 | allowClear: true, 15 | placeholder: '请输入姓名' 16 | }, 17 | { 18 | label: '年龄', 19 | searchType: 'input', 20 | type: 'number', 21 | key: 'name2', 22 | allowClear: true, 23 | placeholder: '请输入年龄' 24 | }, 25 | { 26 | label: '爱好', 27 | searchType: 'select', 28 | mode: undefined, // multiple | tags 29 | options: { 30 | 1: '玩游戏', 31 | 2: '听音乐' 32 | }, 33 | key: 'name3', 34 | allowClear: true, 35 | placeholder: '请选择爱好' 36 | }, 37 | { 38 | label: '国籍', 39 | searchType: 'select', 40 | mode: undefined, // multiple | tags 41 | options: [{ label: '中国', value: 'china' }], 42 | key: 'name4', 43 | allowClear: true, 44 | placeholder: '请选择城市' 45 | }, 46 | { 47 | label: '省市区', 48 | searchType: 'tree-select', 49 | key: 'name5', 50 | placeholder: '请选择省市区', 51 | searchPlaceholder: '', 52 | treeData: [ 53 | { 54 | title: '北京', 55 | value: 'bj', 56 | key: 'bj', 57 | children: [ 58 | { 59 | title: '北京市', 60 | value: 'bjs', 61 | key: 'bjs', 62 | children: [ 63 | { 64 | title: '东城区', 65 | value: 'dcq', 66 | key: 'dcq' 67 | }, 68 | { 69 | title: '西城区', 70 | value: 'xcq', 71 | key: 'xcq' 72 | } 73 | ] 74 | } 75 | ] 76 | } 77 | ], 78 | treeCheckable: false, 79 | multiple: false, 80 | allowClear: true, 81 | showSearch: false, 82 | showCheckedStrategy: 'SHOW_CHILD', // SHOW_ALL, SHOW_PARENT, SHOW_CHILD 83 | treeDefaultExpandAll: false 84 | }, 85 | { 86 | label: '性别', 87 | key: 'name6', 88 | slot: 'name6' 89 | }, 90 | { 91 | label: '食物', 92 | searchType: 'cascader', 93 | options: [ 94 | { 95 | value: 'chaocai', 96 | label: '炒菜', 97 | children: [ 98 | { 99 | value: 'gaifan', 100 | label: '盖饭', 101 | children: [ 102 | { 103 | value: 'hongshaoroufan', 104 | label: '红烧肉饭' 105 | } 106 | ] 107 | } 108 | ] 109 | } 110 | ], 111 | key: 'name7', 112 | allowClear: true, 113 | placeholder: '请选择食物' 114 | }, 115 | { 116 | class: 'search-item-name5', 117 | searchType: 'select', 118 | labelWidth: 0, 119 | mode: undefined, // multiple | tags 120 | labelKey: 'name', 121 | valueKey: 'id', 122 | options: [ 123 | { name: '微辣', id: 1 }, 124 | { name: '中辣', id: 2 }, 125 | { name: '特辣', id: 3 } 126 | ], 127 | key: 'name8', 128 | allowClear: true, 129 | placeholder: '请选择辣度' 130 | }, 131 | { 132 | label: '生日', 133 | searchType: 'date-picker', 134 | key: 'name9', 135 | allowClear: true, 136 | placeholder: '请选择生日' 137 | }, 138 | { 139 | label: '日期', 140 | searchType: 'range-picker', 141 | key: 'name10', 142 | allowClear: true, 143 | placeholder: ['开始日期', '结束日期'] 144 | } 145 | ] 146 | -------------------------------------------------------------------------------- /packages/components/search/demo/index.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 111 | 112 | 123 | -------------------------------------------------------------------------------- /packages/utils/excel.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import Excel from 'exceljs' 3 | 4 | // 具体 api 参考 exceljs 官网文档 5 | export type ExcelColumn = { header: string; key: string; style: Object; [key: string]: any } 6 | 7 | export interface ExcelOptions { 8 | filename: string 9 | columns: ExcelColumn[] 10 | data: Record[] 11 | firstRowStyle?: Record 12 | } 13 | 14 | const defaultOptions = { 15 | filename: 'excel.xlsx', 16 | columns: [], 17 | data: [], 18 | firstRowStyle: { font: { size: 10, bold: true } } 19 | } 20 | 21 | export async function exportExcel(options: ExcelOptions) { 22 | options = { ...defaultOptions, ...(options || {}) } 23 | const { filename, columns, firstRowStyle, data: rows } = options 24 | 25 | const workbook = new Excel.Workbook() 26 | const worksheet = workbook.addWorksheet() 27 | 28 | worksheet.columns = columns 29 | worksheet.addRows(rows) 30 | 31 | autoWidth(worksheet, 10, columns) 32 | setFirstRowStyle(worksheet, columns, firstRowStyle) 33 | 34 | const buffer = await workbook.xlsx.writeBuffer() 35 | await writeFile(filename, buffer) 36 | 37 | // workbook.xlsx.writeFile(filename).then(function () { 38 | // console.log('file is written') 39 | // res.sendFile(tempFilePath, function (err) { 40 | // console.log('---------- error downloading file: ' + err) 41 | // }) 42 | // }) 43 | } 44 | 45 | const setFirstRowStyle = (worksheet, columns, style) => { 46 | Object.keys(style).forEach(key => { 47 | const row = worksheet.getRow(1) 48 | columns.forEach((col, index) => { 49 | row.getCell(index + 1)[key] = style[key] 50 | row.getCell(index + 1).alignment = { horizontal: 'center' } 51 | }) 52 | }) 53 | } 54 | 55 | const autoWidth = (worksheet, minimalWidth = 10, columns = []) => { 56 | worksheet.columns.forEach((column, index) => { 57 | if (columns[index]?.width) { 58 | column.width = columns[index].width 59 | } else { 60 | let maxColumnLength = 0 61 | column.eachCell({ includeEmpty: true }, cell => { 62 | maxColumnLength = Math.max( 63 | maxColumnLength, 64 | minimalWidth, 65 | cell.value ? cell.value.toString().length : 0 66 | ) 67 | }) 68 | column.width = maxColumnLength + 2 69 | } 70 | }) 71 | } 72 | 73 | const autoHeight = worksheet => { 74 | const lineHeight = 12 // height per line is roughly 12 75 | worksheet.eachRow(row => { 76 | let maxLine = 1 77 | row.eachCell(cell => { 78 | maxLine = Math.max(cell.value.split('\n').length - 1, maxLine) 79 | }) 80 | row.height = lineHeight * maxLine 81 | }) 82 | } 83 | 84 | const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) 85 | 86 | export const writeFile = async ( 87 | filename: string, 88 | content: any, 89 | options: Record = {} 90 | ) => { 91 | const a = document.createElement('a') 92 | const blob = new Blob([content], { type: 'text/plain', ...options }) 93 | a.download = filename 94 | a.href = URL.createObjectURL(blob) 95 | a.click() 96 | await delay(100) 97 | a.remove() 98 | } 99 | 100 | export const writeBase64File = async ( 101 | filename: string, 102 | base64Str: string, 103 | options?: Record = {} 104 | ) => { 105 | const bStr = atob(base64Str) 106 | let n = bStr.length 107 | const u8arr = new Uint8Array(n) 108 | while (n--) { 109 | u8arr[n] = bStr.charCodeAt(n) 110 | } 111 | await writeFile(filename, u8arr, options) 112 | } 113 | -------------------------------------------------------------------------------- /apps/vue3-app/src/components/layout/index.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 52 | 53 | 129 | -------------------------------------------------------------------------------- /packages/components/layout/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 53 | 54 | 136 | -------------------------------------------------------------------------------- /apps/nuxt3-app/components/layout/index.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 58 | 59 | 135 | --------------------------------------------------------------------------------