├── .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 | [](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 | [](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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
28 |
29 |
54 |
--------------------------------------------------------------------------------
/src/components/layout/LayoutMainHeader.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
20 |
21 |
22 |
59 |
--------------------------------------------------------------------------------
/src/components/layout/LayoutMainMenu.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
32 |
33 |
34 |
50 |
--------------------------------------------------------------------------------
/src/components/layout/LayoutMainUser.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
30 |
31 |
32 |
33 |
34 | 退出登录
35 |
36 |
37 |
38 |
39 |
40 |
41 |
65 |
--------------------------------------------------------------------------------
/src/components/layout/LayoutPage.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
46 |
47 |
48 |
49 |
95 |
--------------------------------------------------------------------------------
/src/components/layout/LayoutUser.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
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 |
2 |
3 |
4 |
5 | 回到首页
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
24 |
--------------------------------------------------------------------------------
/src/views/LoginView/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
17 |
18 |
19 |
20 |
25 |
26 |
27 |
28 |
29 | Remember me
30 |
31 |
32 |
33 | Submit
34 |
35 |
36 |
37 |
38 |
39 |
73 |
74 |
89 |
--------------------------------------------------------------------------------
/src/views/MessageView/index.scss:
--------------------------------------------------------------------------------
1 | .message {
2 | min-height: 350px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/MessageView/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
14 | edit
15 | more
16 |
17 |
18 |
21 |
22 | {{ item.name.last }}
23 |
24 |
25 |
26 |
27 |
28 | content
29 |
30 |
31 |
32 |
33 |
34 |
76 |
79 |
--------------------------------------------------------------------------------
/src/views/WorkView/index.scss:
--------------------------------------------------------------------------------
1 | .work {
2 | width: 100%;
3 | height: 100%;
4 | }
5 |
--------------------------------------------------------------------------------
/src/views/WorkView/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 查询
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
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 |
--------------------------------------------------------------------------------