├── .editorconfig ├── .env.development ├── .env.production ├── .eslintignore ├── .eslintrc-auto-import.json ├── .eslintrc.cjs ├── .gitattributes ├── .github └── workflows │ ├── deploy-dockerhub.yml │ ├── deploy-githubpages.yml │ └── deploy-tcb.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .prettierrc.js ├── .release-it.json ├── Dockerfile ├── auto-imports.d.ts ├── cloudbaserc.json ├── cypress.config.ts ├── cypress ├── e2e │ ├── example.cy.ts │ └── tsconfig.json ├── fixtures │ └── example.json └── support │ ├── commands.ts │ └── e2e.ts ├── env.d.ts ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public └── favicon.png ├── readme.md ├── readme.zh-CN.md ├── src ├── App.vue ├── assets │ ├── images │ │ └── logo.png │ └── style │ │ ├── atomic.scss │ │ ├── main.scss │ │ ├── reset.scss │ │ └── variables │ │ ├── color.scss │ │ └── default.scss ├── components │ └── layout │ │ ├── LayoutMain.vue │ │ ├── LayoutMainHeader.vue │ │ ├── LayoutMainMenu.vue │ │ ├── LayoutMainUser.vue │ │ ├── LayoutPage.vue │ │ ├── LayoutUser.vue │ │ └── config.ts ├── hooks │ ├── index.ts │ ├── useLoading.ts │ └── usePageTableSize.ts ├── libs │ ├── index.ts │ ├── log.ts │ ├── request.ts │ └── utils.ts ├── main.ts ├── plugins │ ├── directives │ │ ├── index.ts │ │ └── loading │ │ │ ├── createLoading.ts │ │ │ ├── index.ts │ │ │ ├── readme.md │ │ │ └── types.ts │ ├── index.ts │ └── readme.md ├── router │ └── index.ts ├── services │ └── user.ts ├── stores │ ├── index.ts │ └── useUserStore.ts └── views │ ├── ErrorView │ └── NotFound.vue │ ├── LoginView │ └── index.vue │ ├── MessageView │ ├── index.scss │ └── index.vue │ └── WorkView │ ├── index.scss │ └── index.vue ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.vitest.json ├── vite.config.ts └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 只在开发模式中被载入 2 | 3 | # API - 本地通过/api代理 4 | VITE_API_URL = /apis/ 5 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 只在生产模式中被载入 2 | 3 | # API 4 | VITE_API_URL = https://forguo.cn/api 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .editorconfig 4 | pnpm-lock.yaml 5 | .npmrc 6 | 7 | .tmp 8 | .cache/ 9 | coverage/ 10 | .nyc_output/ 11 | **/.yarn/** 12 | **/.pnp.* 13 | /dist*/ 14 | node_modules/ 15 | **/node_modules/ 16 | 17 | ## Local 18 | .husky 19 | .local 20 | 21 | !.prettierrc.js 22 | components.d.ts 23 | auto-imports.d.ts 24 | -------------------------------------------------------------------------------- /.eslintrc-auto-import.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "Component": true, 4 | "ComponentPublicInstance": true, 5 | "ComputedRef": true, 6 | "EffectScope": true, 7 | "ExtractDefaultPropTypes": true, 8 | "ExtractPropTypes": true, 9 | "ExtractPublicPropTypes": true, 10 | "InjectionKey": true, 11 | "PropType": true, 12 | "Ref": true, 13 | "VNode": true, 14 | "WritableComputedRef": true, 15 | "computed": true, 16 | "createApp": true, 17 | "customRef": true, 18 | "defineAsyncComponent": true, 19 | "defineComponent": true, 20 | "effectScope": true, 21 | "getCurrentInstance": true, 22 | "getCurrentScope": true, 23 | "h": true, 24 | "inject": true, 25 | "isProxy": true, 26 | "isReactive": true, 27 | "isReadonly": true, 28 | "isRef": true, 29 | "markRaw": true, 30 | "nextTick": true, 31 | "onActivated": true, 32 | "onBeforeMount": true, 33 | "onBeforeRouteLeave": true, 34 | "onBeforeRouteUpdate": true, 35 | "onBeforeUnmount": true, 36 | "onBeforeUpdate": true, 37 | "onDeactivated": true, 38 | "onErrorCaptured": true, 39 | "onMounted": true, 40 | "onRenderTracked": true, 41 | "onRenderTriggered": true, 42 | "onScopeDispose": true, 43 | "onServerPrefetch": true, 44 | "onUnmounted": true, 45 | "onUpdated": true, 46 | "provide": true, 47 | "reactive": true, 48 | "readonly": true, 49 | "ref": true, 50 | "resolveComponent": true, 51 | "shallowReactive": true, 52 | "shallowReadonly": true, 53 | "shallowRef": true, 54 | "toRaw": true, 55 | "toRef": true, 56 | "toRefs": true, 57 | "toValue": true, 58 | "triggerRef": true, 59 | "unref": true, 60 | "useAttrs": true, 61 | "useCssModule": true, 62 | "useCssVars": true, 63 | "useLink": true, 64 | "useRoute": true, 65 | "useRouter": true, 66 | "useSlots": true, 67 | "watch": true, 68 | "watchEffect": true, 69 | "watchPostEffect": true, 70 | "watchSyncEffect": true, 71 | "acceptHMRUpdate": true, 72 | "createPinia": true, 73 | "defineStore": true, 74 | "getActivePinia": true, 75 | "mapActions": true, 76 | "mapGetters": true, 77 | "mapState": true, 78 | "mapStores": true, 79 | "mapWritableState": true, 80 | "setActivePinia": true, 81 | "setMapStoreSuffix": true, 82 | "storeToRefs": true, 83 | "DirectiveBinding": true, 84 | "MaybeRef": true, 85 | "MaybeRefOrGetter": true, 86 | "onWatcherCleanup": true, 87 | "useId": true, 88 | "useModel": true, 89 | "useTemplateRef": true 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | '@vue/eslint-config-prettier', 11 | './.eslintrc-auto-import.json' 12 | ], 13 | overrides: [ 14 | { 15 | files: ['cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}'], 16 | extends: ['plugin:cypress/recommended'] 17 | } 18 | ], 19 | parserOptions: { 20 | ecmaVersion: 'latest' 21 | }, 22 | rules: { 23 | // 在数组方法的回调中强制执行 return 语句 24 | 'array-callback-return': [ 25 | 'error', 26 | { 27 | // 允许隐式 28 | allowImplicit: true 29 | } 30 | ], 31 | 'space-before-function-paren': 0, // 函数定义时括号前面要有空格 32 | 'no-console': 'off', 33 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 34 | 'no-unused-vars': 'off', // 不能有声明后未被使用的变量或参数 35 | '@typescript-eslint/no-unused-vars': 'off', 36 | // 组件名 37 | 'vue/multi-word-component-names': [ 38 | 'warn', 39 | { 40 | ignores: ['index'] // 需要忽略的组件名 41 | } 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/deploy-dockerhub.yml: -------------------------------------------------------------------------------- 1 | name: DockerHub Deploy 2 | 3 | # 触发构建动作 4 | # push: 5 | # # 触发构建分支[默认分支] 6 | # branches: [ $default-branch ] 7 | # pull_request: 8 | on: 9 | push: 10 | # 以下 分支有 push 时触发 11 | branches: 12 | - main 13 | 14 | env: # 设置环境变量 15 | TZ: Asia/Shanghai # 时区 16 | 17 | # 作业是在同一运行服务器上执行的一组步骤 18 | jobs: 19 | # 作业名称 20 | deploy: 21 | # 运行器(这里是Ubuntu系统) 22 | runs-on: ubuntu-latest 23 | # 步骤是可以在作业中运行命令的单个任务 24 | # 步骤可以是操作或 shell 命令 25 | steps: 26 | # 检出推送的代码 27 | - name: Checkout - 检出代码 28 | uses: actions/checkout@v3 29 | 30 | # 使用pnpm 31 | - name: Setup pnpm 32 | uses: pnpm/action-setup@v2 33 | 34 | # Node版本 35 | - name: Setup Node.js v16 36 | uses: actions/setup-node@v3 37 | with: 38 | node-version: 16 39 | cache: 'pnpm' 40 | # 安装依赖 41 | - name: Install NodeModules - 安装依赖 42 | run: pnpm install 43 | 44 | # 打包 45 | - name: Build - 打包 46 | run: pnpm run build 47 | 48 | # 打包结果 49 | - name: Build Status - 打包结果 50 | run: cd dist && ls -ll 51 | 52 | # 打包Docker镜像并push 53 | # - name: Docker Image Build - 打包Docker镜像 54 | # uses: elgohr/Publish-Docker-Github-Action@master 55 | # with: 56 | # name: forguo/forguo.cn 57 | # username: ${{ secrets.DOCKER_USERNAME }} 58 | # password: ${{ secrets.DOCKER_PASSWORD }} 59 | 60 | # 登录远程服务器并部署容器 61 | # - name: Docker Image Deploy - 部署 62 | # uses: appleboy/ssh-action@master 63 | # with: 64 | # host: ${{ secrets.HOST }} 65 | # username: ${{ secrets.USERNAME }} 66 | # password: ${{ secrets.PASSWORD }} 67 | # port: 22 68 | # script: cd ~/deploy/ && sh deploy-forguo.cn-dockehub.sh ${{ secrets.DOCKER_USERNAME }} ${{ secrets.DOCKER_PASSWORD }} 69 | -------------------------------------------------------------------------------- /.github/workflows/deploy-githubpages.yml: -------------------------------------------------------------------------------- 1 | name: Github Pages Deploy 2 | 3 | # 触发构建动作 4 | # push: 5 | # # 触发构建分支[默认分支] 6 | # branches: [ $default-branch ] 7 | # pull_request: 8 | on: 9 | push: 10 | # 以下 分支有 push 时触发 11 | branches: 12 | - master 13 | - main 14 | 15 | env: # 设置环境变量 16 | TZ: Asia/Shanghai # 时区 17 | 18 | # 作业是在同一运行服务器上执行的一组步骤 19 | jobs: 20 | # 作业名称 21 | deploy: 22 | # 运行器(这里是Ubuntu系统) 23 | runs-on: ubuntu-latest 24 | # 步骤是可以在作业中运行命令的单个任务 25 | # 步骤可以是操作或 shell 命令 26 | steps: 27 | # 检出推送的代码 28 | - name: Checkout - 检出代码 29 | uses: actions/checkout@v3 30 | 31 | # 使用pnpm 32 | - name: Setup pnpm 33 | uses: pnpm/action-setup@v2 34 | 35 | # Node版本 36 | - name: Setup Node.js v16 37 | uses: actions/setup-node@v3 38 | with: 39 | node-version: 16 40 | cache: 'pnpm' 41 | 42 | # 安装依赖 43 | - name: Install NodeModules/vue3-quick-start - 安装依赖 44 | run: pnpm install 45 | 46 | # 打包 47 | - name: Build - 打包 48 | run: pnpm run build # 打包 49 | 50 | # 打包结果 51 | - name: Dir - 打包结果 52 | run: cd dist && ls -ll # 打包结果 53 | 54 | # 部署 55 | - name: Deploy - 部署 56 | uses: peaceiris/actions-gh-pages@v3 # 使用部署到 GitHub pages 的 action 57 | with: 58 | github_token: ${{ secrets.CL_TOKEN }} # github_token,仓库secrets配置 59 | publish_dir: dist # 部署打包后的 dist 目录 60 | -------------------------------------------------------------------------------- /.github/workflows/deploy-tcb.yml: -------------------------------------------------------------------------------- 1 | # 工作流程名称 2 | name: Tcb Deploy 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | # 触发构建动作 10 | # push: 11 | # # 触发构建分支[默认分支] 12 | # branches: [ $default-branch ] 13 | # pull_request: 14 | 15 | env: # 设置环境变量 16 | TZ: Asia/Shanghai # 时区 17 | 18 | # 作业是在同一运行服务器上执行的一组步骤 19 | jobs: 20 | # 作业名称 21 | deploy: 22 | # 运行器(这里是Ubuntu系统) 23 | runs-on: ubuntu-latest 24 | # 作业名称(同deploy) 25 | name: Deploy 26 | # 步骤是可以在作业中运行命令的单个任务 27 | # 步骤可以是操作或 shell 命令 28 | steps: 29 | # 检出推送的代码 30 | - name: Checkout 31 | uses: actions/checkout@v2 32 | 33 | # 使用pnpm 34 | - name: Setup pnpm 35 | uses: pnpm/action-setup@v2 36 | 37 | # Node版本 38 | - name: Setup Node.js v16 39 | uses: actions/setup-node@v3 40 | with: 41 | node-version: 16 42 | cache: 'pnpm' 43 | 44 | # 安装依赖 45 | - name: Install NodeModules - 安装依赖 46 | run: pnpm install 47 | 48 | # 打包 49 | - name: Build - 打包 50 | run: pnpm run build 51 | 52 | # 打包结果 53 | - name: Build Status - 打包结果 54 | run: cd dist && ls -ll 55 | 56 | # 云开发部署 57 | # - name: Deploy to Tencent CloudBase 58 | # uses: TencentCloudBase/cloudbase-action@v2 59 | # with: 60 | # 以下参数配置于 github secrets 61 | # secretId: ${{secrets.SECRETID}} 62 | # secretKey: ${{secrets.SECRETKEY}} 63 | # envId: ${{secrets.ENV_ID}} 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | components.d.ts 17 | 18 | /cypress/videos/ 19 | /cypress/screenshots/ 20 | 21 | # Editor directories and files 22 | .vscode/* 23 | !.vscode/extensions.json 24 | .idea 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .editorconfig 4 | pnpm-lock.yaml 5 | .npmrc 6 | 7 | .tmp 8 | .cache/ 9 | coverage/ 10 | .nyc_output/ 11 | **/.yarn/** 12 | **/.pnp.* 13 | /dist*/ 14 | node_modules/ 15 | **/node_modules/ 16 | 17 | ## Local 18 | .husky 19 | .local 20 | 21 | **/*.svg 22 | **/*.sh 23 | components.d.ts 24 | auto-imports.d.ts 25 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | $schema: 'https://json.schemastore.org/prettierrc', 3 | printWidth: 120, // 单行输出(不折行)的(最大)长度 4 | semi: false, // 结尾使用分号, 默认true 5 | useTabs: false, // 使用tab缩进,默认false 6 | tabWidth: 4, // tab缩进大小,默认为2 7 | singleQuote: true, // 使用单引号, 默认false(在jsx中配置无效, 默认都是双引号) 8 | jsxSingleQuote: true, // jsx 不使用单引号,而使用双引号 9 | trailingComma: 'none', // 行尾逗号,默认none,可选 none|es5|all es5 包括es5中的数组、对象,all 包括函数对象等所有可选 10 | bracketSpacing: true, // 对象中的空格 默认true,true: { foo: bar },false: {foo: bar} 11 | htmlWhitespaceSensitivity: 'ignore', // 指定 HTML 文件的全局空白区域敏感度, "ignore" - 空格被认为是不敏感的 12 | jsxBracketSameLine: false, 13 | arrowParens: 'avoid', // 箭头函数参数括号 默认avoid 可选 avoid| always, avoid 能省略括号的时候就省略 例如x => x,always 总是有括号 14 | proseWrap: 'always' // 当超出print width(上面有这个参数)时就折行 15 | } 16 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "@release-it/conventional-changelog": { 4 | "preset": "angular", 5 | "infile": "CHANGELOG.md" 6 | } 7 | }, 8 | "git": { 9 | "commitMessage": "chore: vue3-quick-start release v${version}" 10 | }, 11 | "github": { 12 | "release": false, 13 | "draft": false 14 | }, 15 | "npm": { 16 | "publish": false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 指定基础镜像 2 | FROM nginx 3 | 4 | ## 项目代码 5 | COPY ./dist/ /usr/share/nginx/html/vue3-quick-start/ 6 | 7 | # nginx配置 8 | #COPY ./nginx/ /etc/nginx/ 9 | 10 | ## nginx目录都是放在/usr/local/nginx下面的,docker安装是和老版本nginx一样的目录 11 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | const EffectScope: typeof import('vue')['EffectScope'] 9 | const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] 10 | const computed: typeof import('vue')['computed'] 11 | const createApp: typeof import('vue')['createApp'] 12 | const createPinia: typeof import('pinia')['createPinia'] 13 | const customRef: typeof import('vue')['customRef'] 14 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 15 | const defineComponent: typeof import('vue')['defineComponent'] 16 | const defineStore: typeof import('pinia')['defineStore'] 17 | const effectScope: typeof import('vue')['effectScope'] 18 | const getActivePinia: typeof import('pinia')['getActivePinia'] 19 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 20 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 21 | const h: typeof import('vue')['h'] 22 | const inject: typeof import('vue')['inject'] 23 | const isProxy: typeof import('vue')['isProxy'] 24 | const isReactive: typeof import('vue')['isReactive'] 25 | const isReadonly: typeof import('vue')['isReadonly'] 26 | const isRef: typeof import('vue')['isRef'] 27 | const mapActions: typeof import('pinia')['mapActions'] 28 | const mapGetters: typeof import('pinia')['mapGetters'] 29 | const mapState: typeof import('pinia')['mapState'] 30 | const mapStores: typeof import('pinia')['mapStores'] 31 | const mapWritableState: typeof import('pinia')['mapWritableState'] 32 | const markRaw: typeof import('vue')['markRaw'] 33 | const nextTick: typeof import('vue')['nextTick'] 34 | const onActivated: typeof import('vue')['onActivated'] 35 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 36 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] 37 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 38 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 39 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 40 | const onDeactivated: typeof import('vue')['onDeactivated'] 41 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 42 | const onMounted: typeof import('vue')['onMounted'] 43 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 44 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 45 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 46 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 47 | const onUnmounted: typeof import('vue')['onUnmounted'] 48 | const onUpdated: typeof import('vue')['onUpdated'] 49 | const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] 50 | const provide: typeof import('vue')['provide'] 51 | const reactive: typeof import('vue')['reactive'] 52 | const readonly: typeof import('vue')['readonly'] 53 | const ref: typeof import('vue')['ref'] 54 | const resolveComponent: typeof import('vue')['resolveComponent'] 55 | const setActivePinia: typeof import('pinia')['setActivePinia'] 56 | const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] 57 | const shallowReactive: typeof import('vue')['shallowReactive'] 58 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 59 | const shallowRef: typeof import('vue')['shallowRef'] 60 | const storeToRefs: typeof import('pinia')['storeToRefs'] 61 | const toRaw: typeof import('vue')['toRaw'] 62 | const toRef: typeof import('vue')['toRef'] 63 | const toRefs: typeof import('vue')['toRefs'] 64 | const toValue: typeof import('vue')['toValue'] 65 | const triggerRef: typeof import('vue')['triggerRef'] 66 | const unref: typeof import('vue')['unref'] 67 | const useAttrs: typeof import('vue')['useAttrs'] 68 | const useCssModule: typeof import('vue')['useCssModule'] 69 | const useCssVars: typeof import('vue')['useCssVars'] 70 | const useId: typeof import('vue')['useId'] 71 | const useLink: typeof import('vue-router')['useLink'] 72 | const useModel: typeof import('vue')['useModel'] 73 | const useRoute: typeof import('vue-router')['useRoute'] 74 | const useRouter: typeof import('vue-router')['useRouter'] 75 | const useSlots: typeof import('vue')['useSlots'] 76 | const useTemplateRef: typeof import('vue')['useTemplateRef'] 77 | const watch: typeof import('vue')['watch'] 78 | const watchEffect: typeof import('vue')['watchEffect'] 79 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 80 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 81 | } 82 | // for type re-export 83 | declare global { 84 | // @ts-ignore 85 | export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' 86 | import('vue') 87 | } 88 | -------------------------------------------------------------------------------- /cloudbaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "envId": "{{env.ENV_ID}}", 3 | "$schema": "https://framework-1258016615.tcloudbaseapp.com/schema/latest.json", 4 | "framework": { 5 | "name": "vue3-quick-start", 6 | "plugins": { 7 | "client": { 8 | "use": "@cloudbase/framework-plugin-website", 9 | "inputs": { 10 | "buildCommand": "npm run build", 11 | "outputPath": "/dist", 12 | "cloudPath": "/vue3-quick-start" 13 | } 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}', 6 | baseUrl: 'http://localhost:4173' 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /cypress/e2e/example.cy.ts: -------------------------------------------------------------------------------- 1 | // https://on.cypress.io/api 2 | 3 | describe('My First Test', () => { 4 | it('visits the app root url', () => { 5 | cy.visit('/') 6 | cy.contains('h1', 'You did it!') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /cypress/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["./**/*", "../support/**/*"], 4 | "compilerOptions": { 5 | "isolatedModules": false, 6 | "target": "es5", 7 | "lib": ["es5", "dom"], 8 | "types": ["cypress"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } 38 | 39 | export {} 40 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface ImportMetaEnv { 3 | readonly BASE_URL: string 4 | readonly MODE: string 5 | readonly APP_VERSION: string 6 | readonly APP_NAME: string 7 | readonly APP_BUILD_TIME: string 8 | readonly VITE_BASE_URL: string 9 | readonly VITE_API_URL: string 10 | } 11 | 12 | interface ImportMeta { 13 | readonly env: ImportMetaEnv 14 | readonly glob: (path: string, config: object) => Record Promise<{ default: any }>> 15 | } 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %APP_NAME% 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-quick-start", 3 | "version": "0.1.0", 4 | "private": true, 5 | "packageManager": "pnpm@7.6.0", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "pnpm type-check && pnpm build-only", 9 | "deploy": "cloudbase framework:deploy", 10 | "preview": "vite preview", 11 | "test:unit": "vitest", 12 | "test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'", 13 | "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'", 14 | "build-only": "vite build", 15 | "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", 16 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 17 | "format": "prettier --write .", 18 | "prepare": "husky install", 19 | "release": "release-it" 20 | }, 21 | "dependencies": { 22 | "@ant-design/icons-vue": "^6.1.0", 23 | "@vueuse/core": "^10.9.0", 24 | "@wei-design/web-vue": "^1.0.2", 25 | "ant-design-vue": "^4.2.1", 26 | "axios": "^1.4.0", 27 | "dayjs": "^1.11.9", 28 | "pinia": "^2.1.3", 29 | "vue": "^3.3.4", 30 | "vue-router": "^4.2.2" 31 | }, 32 | "devDependencies": { 33 | "@commitlint/cli": "^17.6.6", 34 | "@commitlint/config-conventional": "^17.6.6", 35 | "@release-it/conventional-changelog": "^7.0.0", 36 | "@rushstack/eslint-patch": "^1.2.0", 37 | "@tsconfig/node18": "^2.0.1", 38 | "@types/jsdom": "^21.1.1", 39 | "@types/node": "^18.16.17", 40 | "@vitejs/plugin-vue": "^4.4.0", 41 | "@vitejs/plugin-vue-jsx": "^3.0.2", 42 | "@vue/eslint-config-prettier": "^7.1.0", 43 | "@vue/eslint-config-typescript": "^11.0.3", 44 | "@vue/test-utils": "^2.4.1", 45 | "@vue/tsconfig": "^0.4.0", 46 | "cypress": "^12.14.0", 47 | "eslint": "^8.50.0", 48 | "eslint-plugin-cypress": "^2.13.3", 49 | "eslint-plugin-vue": "^9.17.0", 50 | "husky": "^8.0.0", 51 | "jsdom": "^22.1.0", 52 | "lint-staged": "^13.2.3", 53 | "prettier": "^2.8.8", 54 | "release-it": "^16.1.2", 55 | "sass": "^1.63.6", 56 | "start-server-and-test": "^2.0.1", 57 | "typescript": "^4.9.5", 58 | "unplugin-auto-import": "^0.16.6", 59 | "unplugin-vue-components": "^0.25.2", 60 | "vite": "^4.3.9", 61 | "vite-plugin-compression": "^0.5.1", 62 | "vite-plugin-meta-env": "^1.0.2", 63 | "vitest": "^0.34.6", 64 | "vue-tsc": "^1.6.5" 65 | }, 66 | "engines": { 67 | "node": ">=16.0.0" 68 | }, 69 | "lint-staged": { 70 | "*.{js,jsx,ts,tsx,vue}": [ 71 | "npm run lint", 72 | "git add ." 73 | ] 74 | }, 75 | "commitlint": { 76 | "extends": [ 77 | "@commitlint/config-conventional" 78 | ] 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wei-zone/vue3-quick-start/82cafa32a5334e6ce4969802fa480fd00740c193/public/favicon.png -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # vue3-quick-start 2 | 3 | English | [简体中文](https://github.com/wforguo/vue3-quick-start/blob/master/readme.zh-CN.md) 4 | 5 | This template should help get you started developing with Vue 3 in Vite. 6 | 7 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 8 | 9 | [start guide](https://forguo.cn/f2e/%E5%B7%A5%E7%A8%8B%E5%8C%96/Vue3%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96%E9%A1%B9%E7%9B%AE%E6%90%AD%E5%BB%BA.html) 10 | 11 | ## vite config 12 | 13 | See [Vite Configuration Reference](https://vitejs.dev/config/). 14 | 15 | ## start 16 | 17 | ```sh 18 | pnpm install 19 | ``` 20 | 21 | ### Compile and Hot-Reload for Development 22 | 23 | ```sh 24 | pnpm dev 25 | ``` 26 | 27 | ### Type-Check, Compile and Minify for Production 28 | 29 | ```sh 30 | pnpm build 31 | ``` 32 | 33 | ### Run Unit Tests with [Vitest](https://vitest.dev/) 34 | 35 | ```sh 36 | pnpm test:unit 37 | ``` 38 | 39 | ### Run End-to-End Tests with [Cypress](https://www.cypress.io/) 40 | 41 | ```sh 42 | pnpm test:e2e:dev 43 | ``` 44 | 45 | This runs the end-to-end tests against the Vite development server. It is much faster than the production build. 46 | 47 | But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments): 48 | 49 | ```sh 50 | pnpm build 51 | pnpm test:e2e 52 | ``` 53 | 54 | ### Lint with [ESLint](https://eslint.org/) 55 | 56 | ```sh 57 | pnpm lint 58 | ``` 59 | 60 | ## debug 61 | 62 | [eruda](https://github.com/liriliri/eruda) 63 | 64 | Opening method [modify variables or opening method as needed] 65 | 66 | Add after link: `?debug=true` 67 | 68 | such as: [http://127.0.0.1:5000/?debug=true](http://127.0.0.1:5000/?debug=true) 69 | -------------------------------------------------------------------------------- /readme.zh-CN.md: -------------------------------------------------------------------------------- 1 | # vue3-quick-start 2 | 3 | 简体中文 | [English](https:github.com/wforguo/vue3-quick-start/blob/main/readme.md) 4 | 5 | This template should help get you started developing with Vue 3 in Vite. 6 | 7 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 8 | 9 | [Vue3 工程化项目搭建指北](https://forguo.cn/f2e/%E5%B7%A5%E7%A8%8B%E5%8C%96/Vue3%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96%E9%A1%B9%E7%9B%AE%E6%90%AD%E5%BB%BA.html) 10 | 11 | ## vite 配置 12 | 13 | See [Vite Configuration Reference](https://vitejs.dev/config/). 14 | 15 | ## 运行 16 | 17 | ```sh 18 | pnpm install 19 | ``` 20 | 21 | ### Compile and Hot-Reload for Development 22 | 23 | ```sh 24 | pnpm dev 25 | ``` 26 | 27 | ### Type-Check, Compile and Minify for Production 28 | 29 | ```sh 30 | pnpm build 31 | ``` 32 | 33 | ### Run Unit Tests with [Vitest](https://vitest.dev/) 34 | 35 | ```sh 36 | pnpm test:unit 37 | ``` 38 | 39 | ### Run End-to-End Tests with [Cypress](https://www.cypress.io/) 40 | 41 | ```sh 42 | pnpm test:e2e:dev 43 | ``` 44 | 45 | This runs the end-to-end tests against the Vite development server. It is much faster than the production build. 46 | 47 | But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments): 48 | 49 | ```sh 50 | pnpm build 51 | pnpm test:e2e 52 | ``` 53 | 54 | ### Lint with [ESLint](https://eslint.org/) 55 | 56 | ```sh 57 | pnpm lint 58 | ``` 59 | 60 | ## 移动端调试 61 | 62 | [eruda](https://github.com/liriliri/eruda) 63 | 64 | 开启方式【根据需要自行修改变量或开启方式】 65 | 66 | 链接后添加`?debug=true` 67 | 68 | 如:[http://127.0.0.1:5000/?debug=true](http://127.0.0.1:5000/?debug=true) 69 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wei-zone/vue3-quick-start/82cafa32a5334e6ce4969802fa480fd00740c193/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/style/atomic.scss: -------------------------------------------------------------------------------- 1 | .flex { 2 | display: flex; 3 | } 4 | .flex-1 { 5 | flex: 1; 6 | } 7 | .flex-column { 8 | flex-direction: column; 9 | } 10 | .flex-center { 11 | align-items: center; 12 | } 13 | .flex-wrap { 14 | flex-wrap: wrap; 15 | } 16 | .flex-js-center { 17 | justify-content: center; 18 | } 19 | 20 | .is--ellipsis { 21 | overflow: hidden; 22 | text-overflow: ellipsis; 23 | white-space: nowrap; 24 | } 25 | 26 | /** 27 | 无滚动条 28 | */ 29 | .no-scrollbar { 30 | /*无滚动条*/ 31 | &::-webkit-scrollbar { 32 | /*滚动条整体样式*/ 33 | display: none !important; 34 | } 35 | &::-webkit-scrollbar-track { 36 | /* 滚动条的滑轨背景颜色 */ 37 | display: none !important; 38 | } 39 | &::-webkit-scrollbar-thumb { 40 | /*滚动条里面小方块*/ 41 | display: none !important; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/assets/style/main.scss: -------------------------------------------------------------------------------- 1 | @import './variables/default'; 2 | @import './reset.scss'; 3 | @import './atomic.scss'; 4 | -------------------------------------------------------------------------------- /src/assets/style/reset.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100%; 4 | height: 100%; 5 | overflow: hidden; 6 | min-width: 1200px; 7 | overflow-x: auto; 8 | } 9 | input::-ms-clear, 10 | input::-ms-reveal { 11 | display: none; 12 | } 13 | *, 14 | *::before, 15 | *::after { 16 | box-sizing: border-box; 17 | } 18 | html { 19 | font-family: sans-serif; 20 | line-height: 1.15; 21 | -webkit-text-size-adjust: 100%; 22 | -ms-text-size-adjust: 100%; 23 | -ms-overflow-style: scrollbar; 24 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 25 | } 26 | body { 27 | margin: 0; 28 | } 29 | #app { 30 | width: 100%; 31 | height: 100%; 32 | } 33 | [tabindex='-1']:focus { 34 | outline: none; 35 | } 36 | hr { 37 | box-sizing: content-box; 38 | height: 0; 39 | overflow: visible; 40 | } 41 | h1, 42 | h2, 43 | h3, 44 | h4, 45 | h5, 46 | h6 { 47 | margin-top: 0; 48 | margin-bottom: 0.5em; 49 | font-weight: 500; 50 | } 51 | p { 52 | margin-top: 0; 53 | margin-bottom: 1em; 54 | } 55 | abbr[title], 56 | abbr[data-original-title] { 57 | -webkit-text-decoration: underline dotted; 58 | text-decoration: underline; 59 | text-decoration: underline dotted; 60 | border-bottom: 0; 61 | cursor: help; 62 | } 63 | address { 64 | margin-bottom: 1em; 65 | font-style: normal; 66 | line-height: inherit; 67 | } 68 | input[type='text'], 69 | input[type='password'], 70 | input[type='number'], 71 | textarea { 72 | -webkit-appearance: none; 73 | } 74 | ol, 75 | ul, 76 | dl { 77 | margin-top: 0; 78 | margin-bottom: 1em; 79 | } 80 | ol ol, 81 | ul ul, 82 | ol ul, 83 | ul ol { 84 | margin-bottom: 0; 85 | } 86 | dt { 87 | font-weight: 500; 88 | } 89 | dd { 90 | margin-bottom: 0.5em; 91 | margin-left: 0; 92 | } 93 | blockquote { 94 | margin: 0 0 1em; 95 | } 96 | dfn { 97 | font-style: italic; 98 | } 99 | b, 100 | strong { 101 | font-weight: bolder; 102 | } 103 | small { 104 | font-size: 80%; 105 | } 106 | sub, 107 | sup { 108 | position: relative; 109 | font-size: 75%; 110 | line-height: 0; 111 | vertical-align: baseline; 112 | } 113 | sub { 114 | bottom: -0.25em; 115 | } 116 | sup { 117 | top: -0.5em; 118 | } 119 | pre, 120 | code, 121 | kbd, 122 | samp { 123 | font-size: 1em; 124 | font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; 125 | } 126 | pre { 127 | margin-top: 0; 128 | margin-bottom: 1em; 129 | overflow: auto; 130 | } 131 | figure { 132 | margin: 0 0 1em; 133 | } 134 | img { 135 | vertical-align: middle; 136 | border-style: none; 137 | } 138 | a, 139 | area, 140 | button, 141 | [role='button'], 142 | input:not([type='range']), 143 | label, 144 | select, 145 | summary, 146 | textarea { 147 | touch-action: manipulation; 148 | } 149 | table { 150 | border-collapse: collapse; 151 | } 152 | caption { 153 | padding-top: 0.75em; 154 | padding-bottom: 0.3em; 155 | text-align: left; 156 | caption-side: bottom; 157 | } 158 | input, 159 | button, 160 | select, 161 | optgroup, 162 | textarea { 163 | margin: 0; 164 | color: inherit; 165 | font-size: inherit; 166 | font-family: inherit; 167 | line-height: inherit; 168 | } 169 | button, 170 | input { 171 | overflow: visible; 172 | } 173 | button, 174 | select { 175 | text-transform: none; 176 | } 177 | button, 178 | html [type='button'], 179 | [type='reset'], 180 | [type='submit'] { 181 | -webkit-appearance: button; 182 | } 183 | button::-moz-focus-inner, 184 | [type='button']::-moz-focus-inner, 185 | [type='reset']::-moz-focus-inner, 186 | [type='submit']::-moz-focus-inner { 187 | padding: 0; 188 | border-style: none; 189 | } 190 | input[type='radio'], 191 | input[type='checkbox'] { 192 | box-sizing: border-box; 193 | padding: 0; 194 | } 195 | input[type='date'], 196 | input[type='time'], 197 | input[type='datetime-local'], 198 | input[type='month'] { 199 | -webkit-appearance: listbox; 200 | } 201 | input { 202 | outline: none; 203 | background: none; 204 | border: 0; 205 | } 206 | textarea { 207 | overflow: auto; 208 | resize: vertical; 209 | } 210 | fieldset { 211 | min-width: 0; 212 | margin: 0; 213 | padding: 0; 214 | border: 0; 215 | } 216 | legend { 217 | display: block; 218 | width: 100%; 219 | max-width: 100%; 220 | margin-bottom: 0.5em; 221 | padding: 0; 222 | color: inherit; 223 | font-size: 1.5em; 224 | line-height: inherit; 225 | white-space: normal; 226 | } 227 | progress { 228 | vertical-align: baseline; 229 | } 230 | [type='number']::-webkit-inner-spin-button, 231 | [type='number']::-webkit-outer-spin-button { 232 | height: auto; 233 | } 234 | [type='search'] { 235 | outline-offset: -2px; 236 | -webkit-appearance: none; 237 | } 238 | [type='search']::-webkit-search-cancel-button, 239 | [type='search']::-webkit-search-decoration { 240 | -webkit-appearance: none; 241 | } 242 | ::-webkit-file-upload-button { 243 | font: inherit; 244 | -webkit-appearance: button; 245 | } 246 | output { 247 | display: inline-block; 248 | } 249 | summary { 250 | display: list-item; 251 | } 252 | template { 253 | display: none; 254 | } 255 | [hidden] { 256 | display: none !important; 257 | } 258 | mark { 259 | padding: 0.2em; 260 | } 261 | 262 | /*去除自动填充的白色背景*/ 263 | input:-webkit-autofill, 264 | textarea:-webkit-autofill, 265 | select:-webkit-autofill { 266 | -webkit-text-fill-color: #fff !important; 267 | -webkit-box-shadow: 0 0 0px 1000px transparent inset !important; 268 | background-color: transparent; 269 | background-image: none; 270 | transition: background-color 50000s ease-in-out 0s; 271 | } 272 | 273 | body { 274 | min-height: 100vh; 275 | color: var(--text-important); 276 | font-size: 16px; 277 | font-weight: 400; 278 | background: #fff; 279 | transition: color 0.5s, background-color 0.5s; 280 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, 281 | Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol; 282 | text-rendering: optimizeLegibility; 283 | -webkit-font-smoothing: antialiased; 284 | -moz-osx-font-smoothing: grayscale; 285 | } 286 | 287 | /*重置滚动条样式*/ 288 | *::-webkit-scrollbar { 289 | /*滚动条整体样式*/ 290 | width: 7px; 291 | /*高宽分别对应横竖滚动条的尺寸*/ 292 | height: 7px; 293 | } 294 | 295 | ::-webkit-scrollbar-track { 296 | /* 滚动条的滑轨背景颜色 */ 297 | border-radius: 10px; 298 | } 299 | 300 | ::-webkit-scrollbar-thumb { 301 | /*滚动条里面小方块*/ 302 | border-radius: 26px; 303 | background: var(--gray-7); 304 | } 305 | 306 | ::-webkit-scrollbar-button { 307 | /* 滑轨两头的监听按钮颜色 */ 308 | } 309 | 310 | ::-webkit-scrollbar-corner { 311 | /* 横向滚动条和纵向滚动条相交处尖角的颜色 */ 312 | } 313 | ::-webkit-scrollbar-thumb:hover { 314 | background: var(--gray-3) 315 | } 316 | 317 | ::-webkit-scrollbar-thumb:active { 318 | background: var(--gray-3) 319 | } 320 | -------------------------------------------------------------------------------- /src/assets/style/variables/color.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --gray-1: #fff; 3 | --gray-2: #979aa7; 4 | --gray-3: #6d7480; 5 | --gray-4: #424551; 6 | --gray-5: #06080c; 7 | --gray-6: #1b1d22; 8 | --gray-7: #363a44; 9 | --gray-8: #202227; 10 | --gray-9: #252931; 11 | --gray-10: #00000040; 12 | --gray-11: #2f323b; 13 | --gray-12: #2e3138; 14 | --gray-13: #3b404b; 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/style/variables/default.scss: -------------------------------------------------------------------------------- 1 | @import './color'; 2 | 3 | :root { 4 | --transition-ease: ease; 5 | --transition-time: 0.3s; 6 | --transition-all: all var(--transition-ease) var(--transition-time); 7 | } 8 | 9 | :root { 10 | --text-important: var(--gray-1); 11 | --text-primary: var(--gray-1); 12 | --background-medium: var(--gray-7); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/layout/LayoutMain.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 28 | 29 | 54 | -------------------------------------------------------------------------------- /src/components/layout/LayoutMainHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 59 | -------------------------------------------------------------------------------- /src/components/layout/LayoutMainMenu.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 33 | 34 | 50 | -------------------------------------------------------------------------------- /src/components/layout/LayoutMainUser.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 40 | 41 | 65 | -------------------------------------------------------------------------------- /src/components/layout/LayoutPage.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 48 | 49 | 95 | -------------------------------------------------------------------------------- /src/components/layout/LayoutUser.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /src/components/layout/config.ts: -------------------------------------------------------------------------------- 1 | export const menus = [ 2 | { 3 | key: 'WorkView', 4 | label: '工作台' 5 | }, 6 | { 7 | key: 'MessageView', 8 | label: '消息中心' 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useLoading' 2 | export * from './usePageTableSize' 3 | -------------------------------------------------------------------------------- /src/hooks/useLoading.ts: -------------------------------------------------------------------------------- 1 | export const useLoading = () => { 2 | const loading = ref(false) 3 | const showLoading = () => { 4 | loading.value = true 5 | } 6 | const hideLoading = () => { 7 | loading.value = false 8 | } 9 | return { 10 | loading, 11 | showLoading, 12 | hideLoading 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/hooks/usePageTableSize.ts: -------------------------------------------------------------------------------- 1 | import { onBeforeUnmount, watch, type Ref, nextTick } from 'vue' 2 | 3 | export const usePageTableSize = ( 4 | target: Ref, 5 | callback: (args: { width: number; height: number }) => void 6 | ) => { 7 | // 当页面resize时,执行以下代码 8 | const handleResize = () => { 9 | const { width = 0, height = 0 } = target.value?.getBoundingClientRect() || {} 10 | callback({ 11 | width: Math.ceil(width), 12 | // 需要减去头部高度,55px 13 | height: Math.ceil(height - 55) 14 | }) 15 | } 16 | 17 | const stopWatch = watch( 18 | target, 19 | async el => { 20 | if (el) { 21 | await nextTick(() => { 22 | handleResize() 23 | }) 24 | // 监听窗口resize事件 25 | window.addEventListener('resize', handleResize) 26 | } 27 | }, 28 | { immediate: true, deep: true } 29 | ) 30 | 31 | // 在组件卸载前移除resize事件监听 32 | onBeforeUnmount(() => { 33 | stopWatch() 34 | window.removeEventListener('resize', handleResize) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/libs/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: forguo 3 | * @Date: 2023/7/25 19:16 4 | * @Description: index.ts 5 | */ 6 | 7 | export * from '@/libs/log' 8 | export * from '@/libs/utils' 9 | export * from '@/libs/request' 10 | -------------------------------------------------------------------------------- /src/libs/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: forguo 3 | * @Date: 2023/7/9 11:44 4 | * @Description: console控制栏调试 5 | */ 6 | 7 | import { loadRemoteJs } from '@/libs/utils' 8 | 9 | export const appInfo = () => { 10 | // app info 11 | console.log( 12 | `%c APP_VERSION %c ${import.meta.env.APP_NAME} %c`, 13 | 'background:#35495E; padding: 1px; border-radius: 3px 0 0 3px; color: #fff;', 14 | `background:#e6a23c; padding: 1px; border-radius: 0 3px 3px 0; color: #fff;`, 15 | 'background:transparent' 16 | ) 17 | console.log( 18 | `%c APP_BUILD_TIME %c ${import.meta.env.APP_BUILD_TIME} %c`, 19 | 'background:#35495E; padding: 1px; border-radius: 3px 0 0 3px; color: #fff;', 20 | `background:#409eff; padding: 1px; border-radius: 0 3px 3px 0; color: #fff;`, 21 | 'background:transparent' 22 | ) 23 | console.log( 24 | `%c APP_VERSION %c ${import.meta.env.APP_VERSION} %c`, 25 | 'background:#35495E; padding: 1px; border-radius: 3px 0 0 3px; color: #fff;', 26 | `background:#67c23a; padding: 1px; border-radius: 0 3px 3px 0; color: #fff;`, 27 | 'background:transparent' 28 | ) 29 | console.groupEnd() 30 | 31 | // 线上版本判断 32 | // const APP_VERSION = import.meta.env.APP_VERSION 33 | // const onlineVersion = String(Math.random()) 34 | // if (APP_VERSION != onlineVersion) { 35 | // console.log('APP_VERSION', APP_VERSION) 36 | // } 37 | 38 | // 开启移动端debug 39 | const src = '//cdn.jsdelivr.net/npm/eruda' 40 | if (!/debug=true/.test(window.location.href)) { 41 | return 42 | } 43 | loadRemoteJs(src).then(() => { 44 | // @ts-ignore 45 | eruda.init() 46 | }) 47 | } 48 | 49 | export const logger = { 50 | info: (message: string) => { 51 | console.log(`%c${message}`, `background:#35495E; padding: 1px 6px; border-radius: 3px; color: #fff;`) 52 | }, 53 | progress: (message: string) => { 54 | console.log(`%c${message}`, `background:#409eff; padding: 1px 6px; border-radius: 3px; color: #fff;`) 55 | }, 56 | success: (message: string) => { 57 | console.log(`%c${message}`, `background:#67c23a; padding: 1px 6px; border-radius: 3px; color: #fff;`) 58 | }, 59 | warning: (message: string) => { 60 | console.log(`%c${message}`, `background:#e6a23c; padding: 1px 6px; border-radius: 3px; color: #fff;`) 61 | }, 62 | error: (message: string) => { 63 | console.log(`%c${message}`, `background:#f56c6c; padding: 1px 6px; border-radius: 3px; color: #fff;`) 64 | } 65 | } 66 | 67 | // logger.progress('progress') 68 | // logger.info('info') 69 | // logger.success('success') 70 | // logger.warning('warning') 71 | // logger.error('error') 72 | -------------------------------------------------------------------------------- /src/libs/request.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: forguo 3 | * @Date: 2022/5/16 21:29 4 | * @Description: request.ts.js 5 | */ 6 | 7 | import axios from 'axios' 8 | import type { AxiosResponse, AxiosInstance, AxiosRequestConfig, CreateAxiosDefaults } from 'axios' 9 | import { useUserStore } from '@/stores' 10 | import { Modal, message } from 'ant-design-vue' 11 | 12 | /** 13 | * @Description: 请求响应接口 14 | */ 15 | export interface ApiResponse { 16 | code?: number 17 | message?: string 18 | time?: string | number 19 | data?: D 20 | } 21 | 22 | /** 23 | * 接口返回 Promise 类型 24 | */ 25 | export type ApiPromise = Promise> 26 | 27 | class Request { 28 | // axios实例 29 | instance: AxiosInstance 30 | // 用于存储控制器对象 31 | abortControllerMap: Map 32 | // 构造函数 33 | constructor(config?: CreateAxiosDefaults) { 34 | // 创建axios实例 35 | this.instance = axios.create(config) 36 | // 用于存储控制器对象 37 | this.abortControllerMap = new Map() 38 | // 设置拦截器 39 | this.setInterceptors(this.instance) 40 | } 41 | // 请求 42 | request(config: AxiosRequestConfig): ApiPromise { 43 | return this.instance(config) 44 | } 45 | 46 | /** 47 | * 处理响应 48 | * @param res 49 | */ 50 | handleResponse(res: AxiosResponse & any) { 51 | const { userLogout } = useUserStore() 52 | const url = res.config.url || '' 53 | // 请求完成后,将控制器实例从Map中移除 54 | this.abortControllerMap.delete(url) 55 | if (axios.isCancel(res)) { 56 | console.log('Request canceled', res) 57 | return Promise.reject(res) 58 | } 59 | if (res.status === 200) { 60 | const { showError = true } = res.config 61 | const { code } = res.data 62 | if (code === 200) { 63 | return Promise.resolve(res.data) 64 | } else if (code === 401) { 65 | Modal.confirm({ 66 | title: '确认', 67 | content: '登录已过期,请重新登录~', 68 | onOk() { 69 | userLogout() 70 | return Promise.reject(res.data) 71 | } 72 | }) 73 | } else { 74 | if (showError) { 75 | message.warning(res.data.message || '服务繁忙,请重试~') 76 | } 77 | return Promise.reject(res.data) 78 | } 79 | } else { 80 | const { showError = true } = res.config 81 | if (showError) { 82 | message.warning(res.message || res.data.message || res.response.statusText || '服务繁忙,请重试~') 83 | } 84 | } 85 | return Promise.reject(res) 86 | } 87 | // 拦截器 88 | setInterceptors(request: AxiosInstance) { 89 | // 请求拦截器 90 | request.interceptors.request.use(config => { 91 | const { token } = useUserStore() 92 | // toDo 也可以在这里做一个重复请求的拦截 93 | // https://github.com/axios/axios/tree/main#abortcontroller 94 | // 请求url为key 95 | const url = config.url || '' 96 | // 实例化控制器 97 | const controller = new AbortController() 98 | // 将控制器实例与请求绑定 99 | config.signal = controller.signal 100 | // 将控制器实例存储到Map中 101 | this.abortControllerMap.set(url, controller) 102 | // 设置请求头 103 | if (config && config.headers && token) { 104 | config.headers.set('Authorization', `Bearer ${token}`) 105 | } 106 | return config 107 | }) 108 | // 响应拦截器 109 | request.interceptors.response.use( 110 | res => { 111 | return this.handleResponse(res) 112 | }, 113 | res => { 114 | return this.handleResponse(res) 115 | } 116 | ) 117 | } 118 | /** 119 | * 取消全部请求 120 | */ 121 | cancelAllRequest() { 122 | for (const [, controller] of this.abortControllerMap) { 123 | // 取消请求 124 | controller.abort() 125 | } 126 | this.abortControllerMap.clear() 127 | } 128 | /** 129 | * 取消指定的请求 130 | * @param url 待取消的请求URL 131 | */ 132 | cancelRequest(url: string | string[]) { 133 | const urlList = Array.isArray(url) ? url : [url] 134 | for (const _url of urlList) { 135 | // 取消请求 136 | this.abortControllerMap.get(_url)?.abort() 137 | this.abortControllerMap.delete(_url) 138 | } 139 | } 140 | } 141 | 142 | const instance = new Request({ 143 | baseURL: `${import.meta.env.VITE_API_URL}` 144 | } as CreateAxiosDefaults) 145 | 146 | // 请求 147 | export const request = (config: AxiosRequestConfig & any): ApiPromise => { 148 | return instance.request(config) 149 | } 150 | 151 | // 取消请求 152 | export const cancelRequest = (url: string) => { 153 | instance.cancelRequest(url) 154 | } 155 | 156 | export default Request 157 | -------------------------------------------------------------------------------- /src/libs/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: forguo 3 | * @Date: 2023/7/9 12:14 4 | * @Description: 工具库 5 | */ 6 | 7 | /** 8 | * @desc 动态加载远程的js 9 | * @param {*} src js链接 10 | */ 11 | export function loadRemoteJs(src: string) { 12 | return new Promise((resolve, reject) => { 13 | const scriptNode = document.createElement('script') 14 | scriptNode.setAttribute('type', 'text/javascript') 15 | scriptNode.setAttribute('charset', 'utf-8') 16 | scriptNode.setAttribute('src', src) 17 | document.body.appendChild(scriptNode) 18 | scriptNode.onload = res => { 19 | console.log(`${src} is loaded`) 20 | resolve(res) 21 | } 22 | scriptNode.onerror = e => { 23 | console.warn(`${src} is load failed`) 24 | reject(e) 25 | } 26 | }) 27 | } 28 | 29 | /** 30 | * @desc 获取url参数 31 | * @param name 32 | */ 33 | export function getSearchParam(name: string) { 34 | const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i') 35 | const r = window.location.search.substring(1).match(reg) 36 | if (r != null) { 37 | return decodeURIComponent(r[2]) 38 | } 39 | return null 40 | } 41 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './assets/style/main.scss' 2 | import { createApp } from 'vue' 3 | import { createPinia } from 'pinia' 4 | import App from '@/App.vue' 5 | import router from '@/router' 6 | import { appInfo } from '@/libs' 7 | import plugins from '@/plugins' 8 | appInfo() 9 | // 完整引入组件库 10 | import WeDesign from '@wei-design/web-vue' 11 | import '@wei-design/web-vue/lib/style.css' 12 | 13 | const app = createApp(App) 14 | 15 | app.use(createPinia()).use(router).use(plugins).use(WeDesign).mount('#app') 16 | -------------------------------------------------------------------------------- /src/plugins/directives/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 导出所有的指令 3 | */ 4 | import type { App } from 'vue' 5 | import loading from './loading' 6 | import type { directiveInstance } from '@/plugins/directives/loading/types' 7 | 8 | export const directives: directiveInstance = { 9 | loading 10 | } 11 | 12 | export default { 13 | install: (app: App) => { 14 | for (const key in directives) { 15 | app.directive(key, directives[key]) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/plugins/directives/loading/createLoading.ts: -------------------------------------------------------------------------------- 1 | import { createVNode, render } from 'vue' 2 | import type { VNode } from 'vue' 3 | import { Spin as Loading } from 'ant-design-vue' 4 | 5 | import type { LoadingOptionsResolved } from './types' 6 | import { LoadingOutlined } from '@ant-design/icons-vue' 7 | 8 | export function createLoading(options: LoadingOptionsResolved & { style: Partial }, wait = false) { 9 | const target = options.target as HTMLElement 10 | const originOptions = { 11 | ...options 12 | } 13 | delete originOptions.target 14 | delete originOptions.parent 15 | const data = reactive({ 16 | ...originOptions 17 | }) 18 | 19 | // 自定义加载器 20 | const indicator = h(LoadingOutlined, { 21 | style: { 22 | fontSize: '24px' 23 | }, 24 | spin: true 25 | }) 26 | 27 | // 生成 Loading vNode 28 | const vm: VNode = createVNode( 29 | Loading, 30 | // options 31 | { 32 | delay: data.delay, 33 | indicator: indicator, 34 | size: data.size, 35 | spinning: data.spinning, 36 | tip: data.tip || '加载中...', 37 | wrapperClassName: data.wrapperClassName, 38 | style: data.style 39 | }, 40 | { 41 | // default: () => data.tip 42 | } 43 | // children,允许字符串或数组 44 | ) 45 | 46 | // 输出虚拟DOM 47 | if (wait) { 48 | setTimeout(() => { 49 | render(vm, document.createElement('div')) 50 | }, 0) 51 | } else { 52 | render(vm, document.createElement('div')) 53 | } 54 | 55 | // 卸载 56 | function close() { 57 | if (vm?.el && vm.el.parentNode) { 58 | vm.el.parentNode.removeChild(vm.el) 59 | } 60 | target?.classList.remove('v-loading-target') 61 | } 62 | 63 | // 挂载 64 | function open(target: HTMLElement = document.body) { 65 | if (!vm || !vm.el) { 66 | return 67 | } 68 | target.appendChild(vm.el as HTMLElement) 69 | target.classList.add('v-loading-target') 70 | } 71 | 72 | if (target) { 73 | open(target as HTMLElement) 74 | } 75 | 76 | return { 77 | ...toRefs(data), 78 | vm, 79 | close, 80 | open, 81 | get loading() { 82 | return data.spinning 83 | }, 84 | get $el(): HTMLElement { 85 | return vm?.el as HTMLElement 86 | } 87 | } 88 | } 89 | 90 | export type LoadingInstance = ReturnType 91 | -------------------------------------------------------------------------------- /src/plugins/directives/loading/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: weiguo02 3 | * @Date: 2023/11/21 15:41 4 | * @Description: v-loading 指令 5 | */ 6 | import type { Directive, DirectiveBinding, UnwrapRef } from 'vue' 7 | 8 | import { createLoading } from './createLoading' 9 | import type { LoadingOptions, ElementLoading, LoadingBinding, LoadingOptionsResolved } from './types' 10 | import { INSTANCE_KEY } from './types' 11 | 12 | const isString = (target: any): boolean => typeof target === 'string' 13 | const isObject = (val: any): boolean => toString.call(val) === '[object Object]' 14 | 15 | /** 16 | * 参数处理 17 | * @param options 18 | */ 19 | const resolveOptions = (options: LoadingOptions): LoadingOptionsResolved => { 20 | let target: HTMLElement 21 | if (isString(options.target)) { 22 | target = document.querySelector(options.target as string) ?? document.body 23 | } else { 24 | target = (options.target || document.body) as HTMLElement 25 | } 26 | return { 27 | delay: Number(options.delay || 0), 28 | indicator: options.indicator || false, 29 | size: options.size || 'default', 30 | spinning: options.spinning || false, 31 | tip: options.tip || '', 32 | wrapperClassName: options.wrapperClassName || '', 33 | parent: target === document.body ? document.body : target, 34 | background: options.background || '', 35 | fullscreen: target === document.body && (options.fullscreen ?? true), 36 | target 37 | } 38 | } 39 | 40 | /** 41 | * 创建实例 42 | * @param el 43 | * @param binding 44 | */ 45 | const createInstance = (el: ElementLoading, binding: DirectiveBinding) => { 46 | // 获取对应属性,HTML属性,绑定为 loading-xxx 47 | const getProp = (name: K) => el.getAttribute(`loading-${name}`) 48 | // 全屏展示 49 | const fullscreen = getProp('fullscreen') ?? binding.modifiers.fullscreen 50 | 51 | // Spin组件参数 52 | const options: LoadingOptions = resolveOptions({ 53 | delay: getProp('delay'), 54 | indicator: getProp('indicator'), 55 | size: getProp('size'), 56 | spinning: !!binding.value, 57 | tip: getProp('tip'), 58 | wrapperClassName: getProp('wrapperClassName'), 59 | background: getProp('background'), 60 | // 默认为el,全屏为body,否则为自定义 61 | target: getProp('target') ?? (fullscreen ? undefined : el), 62 | fullscreen: !!fullscreen 63 | }) 64 | 65 | // 背景色 66 | const backgroundColor = options.background || 'rgba(255, 255, 255, 0.55)' 67 | 68 | // 实例样式 69 | const instanceStyle: Partial = { 70 | display: 'flex', 71 | alignItems: 'center', 72 | justifyContent: 'center', 73 | position: 'absolute', 74 | flexDirection: 'column', 75 | gap: '12px', 76 | backgroundColor, 77 | opacity: '1', 78 | zIndex: '998', 79 | top: '0', 80 | right: '0', 81 | left: '0', 82 | bottom: '0', 83 | transition: 'opacity 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86)' 84 | } 85 | 86 | // 保存实例 87 | el[INSTANCE_KEY] = { 88 | options, 89 | instance: createLoading({ 90 | ...options, 91 | style: instanceStyle 92 | }) 93 | } 94 | } 95 | 96 | /** 97 | * 更新实例 98 | * @param newOptions 99 | * @param originalOptions 100 | */ 101 | const updateOptions = (newOptions: UnwrapRef, originalOptions: LoadingOptions) => { 102 | for (const key of Object.keys(originalOptions)) { 103 | const optionKey = key as keyof LoadingOptions // 类型断言 104 | if (isRef(originalOptions[optionKey])) originalOptions[optionKey].value = newOptions[optionKey] 105 | } 106 | } 107 | 108 | /** 109 | * v-loading 指令 110 | */ 111 | const vLoading: Directive = { 112 | // 及他自己的所有子节点都挂载完成后调用 113 | mounted(el, binding) { 114 | // 如果绑定的值为true,则创建实例 115 | if (binding.value) { 116 | createInstance(el, binding) 117 | } 118 | }, 119 | // 在绑定元素的父组件 120 | // 及他自己的所有子节点都更新后调用 121 | updated(el, binding: any) { 122 | const instance = el[INSTANCE_KEY] 123 | if (binding.oldValue !== binding.value) { 124 | if (binding.value && !binding.oldValue) { 125 | createInstance(el, binding) 126 | } else if (binding.value && binding.oldValue) { 127 | if (isObject(binding.value)) updateOptions(binding.value, instance!.options) 128 | } else { 129 | instance?.instance.close() 130 | } 131 | } 132 | }, 133 | // 绑定元素的父组件卸载后调用 134 | unmounted(el) { 135 | el[INSTANCE_KEY]?.instance.close() 136 | } 137 | } 138 | 139 | // 添加基础样式 140 | const style = document.createElement('style') 141 | style.innerHTML = ` 142 | .v-loading-target { 143 | position: relative; 144 | pointer-events: none; 145 | } 146 | .v-loading-target > :not(.ant-spin) { 147 | pointer-events: none; 148 | user-select: none; 149 | opacity: 0.5; 150 | transition: opacity 0.3s; 151 | } 152 | .v-loading-target > .ant-spin { 153 | opacity: 0; 154 | transition: opacity 0.3s; 155 | color: #1677ff 156 | } 157 | .v-loading-target > .ant-spin .ant-spin-dot-item { 158 | background-color: var(--text-important) 159 | } 160 | ` 161 | 162 | document.head.appendChild(style) 163 | 164 | export default vLoading 165 | -------------------------------------------------------------------------------- /src/plugins/directives/loading/readme.md: -------------------------------------------------------------------------------- 1 | # v-loading 2 | 3 | ## Usage 4 | 5 | ```vue 6 |
7 |
内容
8 |
9 | ``` 10 | 11 | ## Modifiers 12 | 13 | | 参数 | 说明 | 可选值 | 默认值 | 14 | | ---------- | -------- | ------ | ------ | 15 | | fullscreen | 是否全屏 | 可选 | | 16 | 17 | ## Attributes 18 | 19 | 用于设置 Loading 组件的参数,通过 HTML 属性传递,参数名为 `loading-` 开头,如 `loading-tip`。 20 | 21 | 具体参数同渲染的 [spin](https://next.antdv.com/components/spin-cn) 组件 22 | 23 | | 参数 | 说明 | 类型 | 可选值 | 默认值 | 24 | | ---- | ---- | ------ | ------ | ------ | 25 | | tip | 文本 | string | 可选 | | 26 | -------------------------------------------------------------------------------- /src/plugins/directives/loading/types.ts: -------------------------------------------------------------------------------- 1 | import type { Directive, UnwrapRef } from 'vue' 2 | import type { LoadingInstance } from './createLoading' 3 | 4 | export type LoadingOptionsResolved = { 5 | delay?: number | string | null 6 | indicator?: any 7 | size?: string | null 8 | spinning?: boolean 9 | tip?: string | null 10 | wrapperClassName?: string | null 11 | background?: string | null 12 | fullscreen?: boolean 13 | // 父级元素 14 | parent?: HTMLElement 15 | // 目标元素 16 | target?: HTMLElement | string 17 | } 18 | 19 | export type LoadingOptions = Partial 20 | 21 | export interface directiveInstance { 22 | [propName: string]: Directive 23 | } 24 | 25 | export const INSTANCE_KEY = Symbol('ElLoading') 26 | export type LoadingBinding = boolean | UnwrapRef 27 | 28 | export interface ElementLoading extends HTMLElement { 29 | [INSTANCE_KEY]?: { 30 | instance: LoadingInstance 31 | options: LoadingOptions 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 导出所有的插件 3 | */ 4 | import type { App } from 'vue' 5 | import directives from './directives' 6 | export default { 7 | install: (app: App) => { 8 | app.use(directives) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/plugins/readme.md: -------------------------------------------------------------------------------- 1 | # 插件 2 | 3 | 所有的插件都在这里 4 | 5 | [writing-a-plugin](https://vuejs.org/guide/reusability/plugins.html#writing-a-plugin) 6 | 7 | ## 自定义指令 8 | 9 | [自定义指令](https://cn.vuejs.org/v2/guide/custom-directive.html) 10 | 11 | 目前包含 `v-loading` 自定义指令 12 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import LayoutMain from '@/components/layout/LayoutMain.vue' 2 | import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router' 3 | import { type RouterOptions } from 'vue-router' 4 | 5 | // 路由集合 6 | const routes: (RouteRecordRaw | any)[] = [ 7 | { 8 | path: '/', 9 | redirect: { path: '/WorkView' }, 10 | component: LayoutMain, 11 | children: [ 12 | { 13 | path: '/:pathMatch(.*)*', 14 | component: () => import('@/views/ErrorView/NotFound.vue') 15 | } 16 | ] 17 | }, 18 | { 19 | path: '/login', 20 | name: 'login', 21 | meta: { title: '登录' }, 22 | component: () => import('@/views/LoginView/index.vue') 23 | } 24 | ] 25 | 26 | // 匹配到的文件默认是懒加载的,通过动态导入实现,并会在构建时分离为独立的 chunk。如果你倾向于直接引入所有的模块(例如依赖于这些模块中的副作用首先被应用),你可以传入 { eager: true } 作为第二个参数: 27 | const views: any = import.meta.glob(`@/views/*/index.vue`, { eager: true }) 28 | 29 | // 动态加载路由 30 | for (const componentPath in views) { 31 | // 找到example的组件,并加载 32 | const $component = views[componentPath].default 33 | // 默认首页必须得 34 | if ($component && !$component.hidden) { 35 | if ($component.name !== 'LoginView') { 36 | const { name, title } = $component 37 | routes[0].children.push({ 38 | path: name === 'WorkView' ? '/' : `/${name}`, 39 | name: name, 40 | title, 41 | component: $component, 42 | meta: { 43 | name, 44 | title 45 | } 46 | }) 47 | } 48 | } 49 | } 50 | 51 | const router = createRouter({ 52 | history: createWebHashHistory(import.meta.env.BASE_URL), 53 | routes: [...routes] 54 | } as RouterOptions) 55 | console.log(routes) 56 | export default router 57 | -------------------------------------------------------------------------------- /src/services/user.ts: -------------------------------------------------------------------------------- 1 | import { type ApiPromise, request } from '@/libs' 2 | 3 | /** 4 | * 登录 5 | */ 6 | export const login = (data: { username: string; password: string }): ApiPromise => { 7 | return request({ 8 | url: '/v1/login', 9 | method: 'post', 10 | data 11 | }) 12 | } 13 | 14 | /** 15 | * 退出登录 16 | */ 17 | export const logout = () => { 18 | return request({ 19 | url: '/v1/login', 20 | method: 'post' 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useUserStore' 2 | -------------------------------------------------------------------------------- /src/stores/useUserStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | import { login } from '@/services/user' 4 | import { useRouter } from 'vue-router' 5 | import { message } from 'ant-design-vue' 6 | export interface ILoginParams { 7 | username: string 8 | password: string 9 | } 10 | 11 | export const useUserStore = defineStore('userStore', () => { 12 | const router = useRouter() 13 | const token = ref('') 14 | const userInfo = ref({ 15 | username: '' 16 | }) 17 | const clearToken = () => { 18 | token.value = '' 19 | } 20 | 21 | const clearUserInfo = () => { 22 | userInfo.value = { 23 | username: '' 24 | } 25 | } 26 | const userLogin = async (params: ILoginParams) => { 27 | const { data } = await login(params) 28 | setToken(data.token) 29 | setUserInfo({ 30 | ...data 31 | }) 32 | await router.push({ 33 | path: '/' 34 | }) 35 | message.success('登录成功~') 36 | } 37 | 38 | const userLogout = async () => { 39 | console.log('userLogout ---> clearToken') 40 | localStorage.clear() 41 | clearToken() 42 | clearUserInfo() 43 | await router.push({ 44 | path: '/login' 45 | }) 46 | } 47 | 48 | const setToken = (value: string) => { 49 | // 保存token 50 | localStorage.setItem('token', value) 51 | token.value = value 52 | } 53 | const setUserInfo = (value: any) => { 54 | // 保存token 55 | localStorage.setItem('userInfo', JSON.stringify(value)) 56 | userInfo.value = value 57 | } 58 | 59 | const initToken = () => { 60 | const value = localStorage.getItem('token') 61 | if (value) { 62 | token.value = value 63 | } 64 | } 65 | const initUserInfo = () => { 66 | try { 67 | const value = localStorage.getItem('userInfo') 68 | if (value) { 69 | userInfo.value = JSON.parse(value) 70 | } 71 | } catch (e) { 72 | userInfo.value = { 73 | username: '' 74 | } 75 | } 76 | } 77 | initToken() 78 | initUserInfo() 79 | 80 | return { 81 | token, 82 | userInfo, 83 | userLogout, 84 | userLogin 85 | } 86 | }) 87 | -------------------------------------------------------------------------------- /src/views/ErrorView/NotFound.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /src/views/LoginView/index.vue: -------------------------------------------------------------------------------- 1 | 39 | 73 | 74 | 89 | -------------------------------------------------------------------------------- /src/views/MessageView/index.scss: -------------------------------------------------------------------------------- 1 | .message { 2 | min-height: 350px; 3 | } 4 | -------------------------------------------------------------------------------- /src/views/MessageView/index.vue: -------------------------------------------------------------------------------- 1 | 34 | 76 | 79 | -------------------------------------------------------------------------------- /src/views/WorkView/index.scss: -------------------------------------------------------------------------------- 1 | .work { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /src/views/WorkView/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 72 | 75 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "./auto-imports.d.ts"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "@/*": ["./src/*"] 10 | }, 11 | /* Bundler mode */ 12 | "moduleResolution": "node", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "preserve", 18 | "experimentalDecorators": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | }, 10 | { 11 | "path": "./tsconfig.vitest.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "module": "ESNext", 7 | "types": ["node"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "exclude": [], 4 | "compilerOptions": { 5 | "moduleResolution": "node", 6 | "composite": true, 7 | "lib": [], 8 | "types": ["node", "jsdom"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | import { ConfigEnv, defineConfig, loadEnv } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import vueJsx from '@vitejs/plugin-vue-jsx' 5 | import AutoImport from 'unplugin-auto-import/vite' 6 | import Components from 'unplugin-vue-components/vite' 7 | import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers' 8 | import dayjs from 'dayjs' 9 | // 引入插件 10 | import VitePluginMetaEnv from 'vite-plugin-meta-env' 11 | // gzip压缩 12 | // import { visualizer } from 'rollup-plugin-visualizer' 13 | // import viteCompression from 'vite-plugin-compression' 14 | // import viteImagemin from 'vite-plugin-imagemin' 15 | // @ts-ignore 16 | import pkg from './package.json' 17 | const { name: title, version: APP_VERSION } = pkg 18 | 19 | // https://vitejs.dev/config/ 20 | export default (configEnv: ConfigEnv) => { 21 | const { mode } = configEnv 22 | const env = loadEnv(mode, process.cwd()) 23 | // 增加环境变量 24 | const metaEnv = { 25 | APP_VERSION, 26 | APP_NAME: title, 27 | APP_BUILD_TIME: dayjs().format('YYYY-MM-DD HH:mm:ss') 28 | } 29 | 30 | return defineConfig({ 31 | // 设置打包路径 32 | base: mode === 'development' ? '/' : `/${title}/`, 33 | // 插件 34 | plugins: [ 35 | vue({ 36 | script: { 37 | defineModel: true 38 | }, 39 | template: { 40 | compilerOptions: { 41 | // iconpark- 视为自定义元素 42 | isCustomElement: tag => tag.includes('iconpark-') 43 | } 44 | } 45 | }), 46 | vueJsx(), 47 | // 按需导入 48 | AutoImport({ 49 | resolvers: [AntDesignVueResolver()], 50 | // targets to transform 51 | include: [ 52 | /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx 53 | /\.vue$/, 54 | /\.vue\?vue/, // .vue 55 | /\.md$/ // .md 56 | ], 57 | // global imports to register 58 | imports: ['vue', 'vue-router', 'pinia'], 59 | 60 | // Filepath to generate corresponding .d.ts file. 61 | // Defaults to './auto-imports.d.ts' when `typescript` is installed locally. 62 | // Set `false` to disable. 63 | dts: './auto-imports.d.ts', 64 | 65 | // Inject the imports at the end of other imports 66 | injectAtEnd: true, 67 | 68 | // Generate corresponding .eslintrc-auto-import.json file. 69 | // eslint globals Docs - https://eslint.org/docs/user-guide/configuring/language-options#specifying-globals 70 | eslintrc: { 71 | enabled: true, // Default `false` 72 | filepath: './.eslintrc-auto-import.json' // Default `./.eslintrc-auto-import.json` 73 | } 74 | }), 75 | Components({ 76 | resolvers: [ 77 | AntDesignVueResolver({ 78 | importStyle: false // css in js 79 | }) 80 | ] 81 | }), 82 | // 环境变量 83 | VitePluginMetaEnv(metaEnv, 'import.meta.env'), 84 | VitePluginMetaEnv(metaEnv, 'process.env') 85 | ], 86 | // 别名 87 | resolve: { 88 | alias: { 89 | '@': fileURLToPath(new URL('./src', import.meta.url)) 90 | } 91 | }, 92 | // 打包配置 93 | build: { 94 | sourcemap: false, 95 | rollupOptions: { 96 | output: { 97 | chunkFileNames: 'js/[name]-[hash].js', // 引入文件名的名称 98 | entryFileNames: 'js/[name]-[hash].js', // 包的入口文件名称 99 | assetFileNames: '[ext]/[name]-[hash].[ext]', // 资源文件像 字体,图片等 100 | // entryFileNames: 'main-app.js', 101 | manualChunks(id, { getModuleInfo }) { 102 | // 打包依赖 103 | if (id.includes('node_modules')) { 104 | return 'vendor' 105 | } 106 | const reg = /(.*)\/src\/components\/(.*)/ 107 | if (reg.test(id)) { 108 | // @ts-ignore 109 | const importersLen = getModuleInfo(id).importers.length 110 | // 被多处引用 111 | if (importersLen > 1) { 112 | return 'common' 113 | } 114 | } 115 | } 116 | }, 117 | plugins: [ 118 | // build.rollupOptions.plugins[] 119 | // viteCompression({ 120 | // verbose: true, // 是否在控制台中输出压缩结果 121 | // disable: false, 122 | // threshold: 10240, // 如果体积大于阈值,将被压缩,单位为b,体积过小时请不要压缩,以免适得其反 123 | // algorithm: 'gzip', // 压缩算法,可选['gzip',' brotliccompress ','deflate ','deflateRaw'] 124 | // ext: '.gz', 125 | // deleteOriginFile: true // 源文件压缩后是否删除(我为了看压缩后的效果,先选择了true) 126 | // }) 127 | // 参数及配置:https://github.com/vbenjs/vite-plugin-imagemin/blob/main/README.zh_CN.md 128 | // viteImagemin({ 129 | // gifsicle: { 130 | // optimizationLevel: 7, 131 | // interlaced: false 132 | // }, 133 | // optipng: { 134 | // optimizationLevel: 7 135 | // }, 136 | // mozjpeg: { 137 | // quality: 20 138 | // }, 139 | // pngquant: { 140 | // quality: [0.8, 0.9], 141 | // speed: 4 142 | // }, 143 | // svgo: { 144 | // plugins: [ 145 | // { 146 | // name: 'removeViewBox' 147 | // }, 148 | // { 149 | // name: 'removeEmptyAttrs', 150 | // active: false 151 | // } 152 | // ] 153 | // } 154 | // }) 155 | ] 156 | } 157 | }, 158 | // 本地服务配置 159 | server: { 160 | headers: { 161 | 'Access-Control-Allow-Origin': '*' 162 | }, 163 | cors: true, 164 | open: false, 165 | port: 5000, 166 | host: true, 167 | proxy: { 168 | '/apis': { 169 | target: 'https://forguo.cn/api/', 170 | changeOrigin: true, 171 | rewrite: path => path.replace(/^\/apis/, '') 172 | }, 173 | '/amap': { 174 | target: 'https://restapi.amap.com/', 175 | changeOrigin: true, 176 | rewrite: path => path.replace(/^\/amap/, '') 177 | } 178 | } 179 | } 180 | }) 181 | } 182 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { mergeConfig } from 'vite' 3 | import { configDefaults, defineConfig } from 'vitest/config' 4 | import viteConfig from './vite.config' 5 | 6 | export default mergeConfig( 7 | viteConfig, 8 | defineConfig({ 9 | test: { 10 | environment: 'jsdom', 11 | exclude: [...configDefaults.exclude, 'e2e/*'], 12 | root: fileURLToPath(new URL('./', import.meta.url)), 13 | transformMode: { 14 | web: [/\.[jt]sx$/] 15 | } 16 | } 17 | }) 18 | ) 19 | --------------------------------------------------------------------------------