├── .editorconfig ├── .env ├── .env.development ├── .env.production ├── .env.test ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── bug_report_zh.yml │ ├── config.yml │ ├── feature_request.yml │ └── feature_request_zh.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── create-release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .vscode └── extensions.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README.zh-CN.md ├── build ├── plugins │ ├── auto-import.ts │ ├── icons.ts │ ├── index.ts │ ├── mock.ts │ └── vue-components.ts ├── proxy.ts └── utils.ts ├── commitlint.config.js ├── index.html ├── mock ├── _createMockServer.ts ├── _util.ts └── service │ ├── menu.ts │ └── user.ts ├── netlify.toml ├── package.json ├── pnpm-lock.yaml ├── public └── favicon.ico ├── src ├── App.vue ├── api │ ├── config.ts │ ├── index.ts │ └── user │ │ ├── index.ts │ │ └── types.ts ├── assets │ ├── icons │ │ └── giftbox.svg │ ├── images │ │ ├── qrcode.png │ │ └── test.jpeg │ └── logo.png ├── components │ ├── CodeEditor │ │ ├── index.ts │ │ └── src │ │ │ └── CodeEditor.vue │ ├── CountTo │ │ ├── index.ts │ │ └── src │ │ │ └── CountTo.vue │ ├── Countdown │ │ ├── index.ts │ │ └── src │ │ │ ├── CountdownButton.vue │ │ │ ├── CountdownInput.vue │ │ │ └── useCountdown.ts │ ├── Cropper │ │ ├── index.ts │ │ └── src │ │ │ ├── Cropper.vue │ │ │ └── types.ts │ ├── Excel │ │ ├── index.ts │ │ └── src │ │ │ ├── ExportExcel.ts │ │ │ ├── ImportExcel.vue │ │ │ └── types.ts │ ├── Iframe │ │ ├── index.ts │ │ └── src │ │ │ └── Iframe.vue │ ├── Page │ │ ├── index.ts │ │ └── src │ │ │ └── PageWrapper.vue │ ├── Plum.vue │ ├── QrCode │ │ ├── index.ts │ │ └── src │ │ │ ├── QrCode.vue │ │ │ ├── drawCanvas.ts │ │ │ ├── drawLogo.ts │ │ │ ├── qrcodePlus.ts │ │ │ ├── toCanvas.ts │ │ │ └── typing.ts │ ├── StrengthMeter │ │ ├── index.ts │ │ └── src │ │ │ └── StrengthMeter.vue │ ├── TickForm │ │ ├── index.ts │ │ └── src │ │ │ ├── TickForm.vue │ │ │ └── props.ts │ └── Watermark │ │ ├── index.ts │ │ └── src │ │ ├── config.ts │ │ ├── types.ts │ │ ├── useProtectWatermark.ts │ │ └── useWatermark.ts ├── directives │ ├── index.ts │ └── lazyImg.ts ├── enums │ ├── cache.ts │ ├── index.ts │ ├── path.ts │ └── role.ts ├── hooks │ └── useRequest.ts ├── layouts │ ├── blank │ │ └── index.vue │ ├── default │ │ ├── content │ │ │ └── index.vue │ │ ├── features │ │ │ └── index.vue │ │ ├── footer │ │ │ └── index.vue │ │ ├── header │ │ │ ├── components │ │ │ │ ├── Breadcrumb.vue │ │ │ │ ├── FullScreen.vue │ │ │ │ ├── SettingDrawer.vue │ │ │ │ ├── SiderTrigger.vue │ │ │ │ └── UserDropdown.vue │ │ │ └── index.vue │ │ ├── index.vue │ │ ├── sidebar │ │ │ ├── components │ │ │ │ ├── Logo.vue │ │ │ │ ├── Menu.vue │ │ │ │ ├── MenuItem.vue │ │ │ │ └── MenuWithChildren.vue │ │ │ └── index.vue │ │ └── useCollapsed.ts │ └── index.ts ├── main.ts ├── router │ ├── guard │ │ ├── index.ts │ │ └── permissionGuard.ts │ ├── index.ts │ └── routes │ │ ├── basic.ts │ │ ├── index.ts │ │ ├── modules │ │ ├── about.ts │ │ ├── demo.ts │ │ ├── docs.ts │ │ └── page.ts │ │ ├── plugins │ │ └── dynamicRoutes.ts │ │ └── typings.ts ├── stores │ ├── index.ts │ └── modules │ │ ├── index.ts │ │ ├── routes.ts │ │ └── user.ts ├── styles │ ├── animation.less │ ├── index.less │ ├── main.less │ └── variable.less ├── utils │ ├── cache.ts │ ├── file │ │ └── download.ts │ ├── index.ts │ ├── is.ts │ ├── log.ts │ ├── map-menu.ts │ ├── request │ │ ├── index.ts │ │ └── types.ts │ ├── resize.ts │ └── useMutationObserver.ts └── views │ ├── about │ ├── components │ │ ├── Dependencies.vue │ │ ├── DevDependencies.vue │ │ └── ProjectInfo.vue │ └── index.vue │ ├── demo │ ├── code-editor │ │ └── index.vue │ ├── count-to │ │ └── index.vue │ ├── cropper │ │ └── index.vue │ ├── excel │ │ ├── data.ts │ │ ├── export.vue │ │ └── import.vue │ ├── fullscreen │ │ └── index.vue │ ├── md-editor │ │ └── index.vue │ ├── protect-element │ │ └── index.vue │ ├── rich-text │ │ └── index.vue │ ├── tick-form │ │ ├── demo │ │ │ ├── ant.vue │ │ │ ├── params.vue │ │ │ ├── reset.vue │ │ │ ├── update.vue │ │ │ ├── validator.vue │ │ │ └── watch.vue │ │ └── index.vue │ └── watermark │ │ └── index.vue │ ├── docs │ ├── antdv │ │ └── index.vue │ └── hbs-admin │ │ └── index.vue │ ├── login │ ├── components │ │ ├── ForgetPasswordForm.vue │ │ ├── LoginForm.vue │ │ ├── MobileForm.vue │ │ ├── QrCodeForm.vue │ │ └── RegisterForm.vue │ ├── index.vue │ └── useLogin.ts │ └── page │ └── not-found │ └── index.vue ├── tsconfig.json ├── types ├── env.d.ts ├── global.d.ts ├── index.d.ts ├── request.d.ts ├── requestHooks.d.ts ├── routes.d.ts ├── store.d.ts └── tickForm.d.ts ├── unocss.config.js └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | insert_final_newline = false 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_PORT = 8080 2 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # Whether to open mock 2 | VITE_USE_MOCK = true 3 | 4 | # public path 5 | VITE_PUBLIC_PATH = / 6 | 7 | # Cross-domain proxy, you can configure multiple 8 | # Please note that no line breaks 9 | VITE_PROXY = [['/api', 'http://localhost:8080']] 10 | 11 | # Delete console 12 | VITE_DROP_CONSOLE = false 13 | 14 | # Basic interface address SPA 15 | VITE_GLOB_API_URL = /api 16 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # Whether to open mock 2 | VITE_USE_MOCK = true 3 | 4 | # public path 5 | VITE_PUBLIC_PATH = / 6 | 7 | # Delete console 8 | VITE_DROP_CONSOLE = true 9 | 10 | # Basic interface address SPA 11 | VITE_GLOB_API_URL = /api 12 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | VITE_BASE_URL = 'api' 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41E Bug report" 2 | description: Report an issue 3 | labels: [pending triage] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this bug report! 9 | - type: textarea 10 | id: bug-description 11 | attributes: 12 | label: Describe the bug 13 | description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks! 14 | validations: 15 | required: true 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_zh.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41E 错误报告" 2 | description: 报告问题 3 | labels: [pending triage] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | 感谢您花时间填写此错误报告! 9 | - type: textarea 10 | id: bug-description 11 | attributes: 12 | label: 描述错误 13 | description: 清晰简洁地描述错误是什么。 如果您打算为此问题提交 PR,请在描述中告诉我们。 谢谢! 14 | validations: 15 | required: true 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 New feature proposal" 2 | description: Propose a new feature to be added 3 | labels: ["enhancement"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for your interest in the project and taking the time to fill out this feature report! 9 | - type: textarea 10 | id: feature-description 11 | attributes: 12 | label: Clear and concise description of the problem 13 | description: If you intend to submit a PR for this issue, tell us in the description. Thanks! 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: additional-context 18 | attributes: 19 | label: Additional context 20 | description: Any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request_zh.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 新功能提案" 2 | description: 提出要添加的新功能 3 | labels: ["enhancement"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | 感谢您对该项目的兴趣并花时间填写此功能报告! 9 | - type: textarea 10 | id: feature-description 11 | attributes: 12 | label: 清晰简洁的问题描述 13 | description: 如果您打算为此问题提交 PR,请在描述中告诉我们。 谢谢! 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: additional-context 18 | attributes: 19 | label: 附加上下文 20 | description: 此处有关功能请求的任何其他上下文或屏幕截图。 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description 4 | 5 | 6 | 7 | ### What is the purpose of this pull request? 8 | 9 | - [ ] Bug fix 10 | - [ ] New Feature 11 | - [ ] Documentation update 12 | - [ ] Other 13 | 14 | ### Before submitting the PR, please make sure you do the following 15 | 16 | - [ ] Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate. 17 | - [ ] Provide a description in this PR that addresses **what** the PR is solving, or reference the issue that it solves (e.g. `fixes #123`). 18 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' 5 | 6 | name: Create Release 7 | 8 | jobs: 9 | build: 10 | name: Create Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@master 15 | - name: Create Release for Tag 16 | id: create_release 17 | uses: Hongbusi/create-release@main 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | with: 21 | tag_name: ${{ github.ref }} 22 | -------------------------------------------------------------------------------- /.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 | dist 12 | auto-imports.d.ts 13 | components.d.ts 14 | dist-ssr 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm run lint 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "dbaeumer.vscode-eslint", 5 | "johnsoncodehk.volar", 6 | "johnsoncodehk.vscode-typescript-vue-plugin" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.2.2](https://github.com/developer-plus/vue-hbs-admin/compare/v1.2.1...v1.2.2) (2022-05-08) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * countTo import error ([#109](https://github.com/developer-plus/vue-hbs-admin/issues/109)) ([1ece073](https://github.com/developer-plus/vue-hbs-admin/commit/1ece07303b2e9bfe804c753660cb81175bc97634)) 7 | * **deps:** update dependency @unocss/reset to v0.33.1 ([#124](https://github.com/developer-plus/vue-hbs-admin/issues/124)) ([ebb5313](https://github.com/developer-plus/vue-hbs-admin/commit/ebb5313346115a5f5f4292b7f9e84d993ab29cb9)) 8 | * **deps:** update dependency @vueuse/core to v8.4.2 ([#122](https://github.com/developer-plus/vue-hbs-admin/issues/122)) ([070a4ff](https://github.com/developer-plus/vue-hbs-admin/commit/070a4ff21d531440850d62375e947d6242b9cd6d)) 9 | * **deps:** update dependency ant-design-vue to v3.2.3 ([#118](https://github.com/developer-plus/vue-hbs-admin/issues/118)) ([0dcf8b9](https://github.com/developer-plus/vue-hbs-admin/commit/0dcf8b9b3eea95fe57f79eef9c0d2b142ce026d0)) 10 | * **deps:** update dependency dayjs to v1.11.2 ([#125](https://github.com/developer-plus/vue-hbs-admin/issues/125)) ([9400d19](https://github.com/developer-plus/vue-hbs-admin/commit/9400d19988d88de37a04fa89fab56ef7cdac8b9c)) 11 | * **deps:** update dependency pinia to v2.0.14 ([#121](https://github.com/developer-plus/vue-hbs-admin/issues/121)) ([8a2b7d8](https://github.com/developer-plus/vue-hbs-admin/commit/8a2b7d8676eb533d0d17333da5a3b3061646d74d)) 12 | 13 | 14 | ### Features 15 | 16 | * user logout ([#91](https://github.com/developer-plus/vue-hbs-admin/issues/91)) ([884668b](https://github.com/developer-plus/vue-hbs-admin/commit/884668b7429bd8c470b33a88e58dde92d6ad96f4)) 17 | 18 | 19 | 20 | ## [1.2.1](https://github.com/developer-plus/vue-hbs-admin/compare/v1.2.0...v1.2.1) (2022-05-05) 21 | 22 | 23 | ### Bug Fixes 24 | 25 | * **deps:** add rollup ([#106](https://github.com/developer-plus/vue-hbs-admin/issues/106)) ([8dfab12](https://github.com/developer-plus/vue-hbs-admin/commit/8dfab1297e036cc03e9ba069f878e442454da12d)) 26 | * **deps:** pin dependencies ([#93](https://github.com/developer-plus/vue-hbs-admin/issues/93)) ([8a5609e](https://github.com/developer-plus/vue-hbs-admin/commit/8a5609e667bcf8a2b1520f43afc38c98bd9391d3)) 27 | * **deps:** update dependency @vueuse/core to v8.4.1 ([#114](https://github.com/developer-plus/vue-hbs-admin/issues/114)) ([1dcdace](https://github.com/developer-plus/vue-hbs-admin/commit/1dcdace088890b137120d572f63a68a6ea4f2183)) 28 | * **deps:** update dependency vue-router to v4.0.15 ([#113](https://github.com/developer-plus/vue-hbs-admin/issues/113)) ([2a69f62](https://github.com/developer-plus/vue-hbs-admin/commit/2a69f62e1dd3fbfe1941021532c09da3d5bad74d)) 29 | * element level optimization ([#87](https://github.com/developer-plus/vue-hbs-admin/issues/87)) ([0095cf0](https://github.com/developer-plus/vue-hbs-admin/commit/0095cf017e265e06c9b9a8f2a0bb2ba23fe16976)) 30 | * fix CountTo not working ([#96](https://github.com/developer-plus/vue-hbs-admin/issues/96)) ([0a65d71](https://github.com/developer-plus/vue-hbs-admin/commit/0a65d71c59587bd32d7aa51f58a9d5664c7c52ad)) 31 | * fix low dpi display horizontal page scrollbar ([#112](https://github.com/developer-plus/vue-hbs-admin/issues/112)) ([42e32d3](https://github.com/developer-plus/vue-hbs-admin/commit/42e32d3ee32e6b44d144beb1c9a7e20b2316803d)) 32 | * single breadcrumb item display separator ([#88](https://github.com/developer-plus/vue-hbs-admin/issues/88)) ([8faeedb](https://github.com/developer-plus/vue-hbs-admin/commit/8faeedb27f1b7a93f099a82d0cef177a1f2ff073)) 33 | 34 | 35 | 36 | # [1.2.0](https://github.com/developer-plus/vue-hbs-admin/compare/v1.0.0...v1.2.0) (2022-05-02) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * failure to create a custom watermark ([#68](https://github.com/developer-plus/vue-hbs-admin/issues/68)) ([4a6b22f](https://github.com/developer-plus/vue-hbs-admin/commit/4a6b22f1dbd2dd34002353ae18a9e8e12f443b4a)) 42 | * update README ([#73](https://github.com/developer-plus/vue-hbs-admin/issues/73)) ([d31ea7f](https://github.com/developer-plus/vue-hbs-admin/commit/d31ea7f6b650655c9079c316239d20bbefb8bb9e)) 43 | 44 | 45 | ### Features 46 | 47 | * add breadcrumb navigation ([#74](https://github.com/developer-plus/vue-hbs-admin/issues/74)) ([9de3f22](https://github.com/developer-plus/vue-hbs-admin/commit/9de3f22037570b35ec7ba10a00785c1d05e4337a)) 48 | * add code editor ([#79](https://github.com/developer-plus/vue-hbs-admin/issues/79)) ([f0c7d65](https://github.com/developer-plus/vue-hbs-admin/commit/f0c7d65537939fff1683652ca7de8e15a1cc26a8)) 49 | * add lazy-img directives ([#69](https://github.com/developer-plus/vue-hbs-admin/issues/69)) ([2cc91ea](https://github.com/developer-plus/vue-hbs-admin/commit/2cc91ea877f02ef855b87a770148934cc725e73a)) 50 | * add protect watermark && protect element page ([#77](https://github.com/developer-plus/vue-hbs-admin/issues/77)) ([ee60855](https://github.com/developer-plus/vue-hbs-admin/commit/ee60855700e577a7ce891ed6d38321cf18501270)) 51 | * docs pages ([#70](https://github.com/developer-plus/vue-hbs-admin/issues/70)) ([b7aef35](https://github.com/developer-plus/vue-hbs-admin/commit/b7aef35eeb2b29489ea370536382c5548987bc1c)) 52 | 53 | 54 | 55 | # [1.1.0](https://github.com/developer-plus/vue-hbs-admin/compare/v1.0.0...v1.1.0) (2022-05-02) 56 | 57 | 58 | ### Bug Fixes 59 | 60 | * failure to create a custom watermark ([#68](https://github.com/developer-plus/vue-hbs-admin/issues/68)) ([4a6b22f](https://github.com/developer-plus/vue-hbs-admin/commit/4a6b22f1dbd2dd34002353ae18a9e8e12f443b4a)) 61 | * update README ([#73](https://github.com/developer-plus/vue-hbs-admin/issues/73)) ([d31ea7f](https://github.com/developer-plus/vue-hbs-admin/commit/d31ea7f6b650655c9079c316239d20bbefb8bb9e)) 62 | 63 | 64 | ### Features 65 | 66 | * add breadcrumb navigation ([#74](https://github.com/developer-plus/vue-hbs-admin/issues/74)) ([9de3f22](https://github.com/developer-plus/vue-hbs-admin/commit/9de3f22037570b35ec7ba10a00785c1d05e4337a)) 67 | * add code editor ([#79](https://github.com/developer-plus/vue-hbs-admin/issues/79)) ([f0c7d65](https://github.com/developer-plus/vue-hbs-admin/commit/f0c7d65537939fff1683652ca7de8e15a1cc26a8)) 68 | * add lazy-img directives ([#69](https://github.com/developer-plus/vue-hbs-admin/issues/69)) ([2cc91ea](https://github.com/developer-plus/vue-hbs-admin/commit/2cc91ea877f02ef855b87a770148934cc725e73a)) 69 | * add protect watermark && protect element page ([#77](https://github.com/developer-plus/vue-hbs-admin/issues/77)) ([ee60855](https://github.com/developer-plus/vue-hbs-admin/commit/ee60855700e577a7ce891ed6d38321cf18501270)) 70 | * docs pages ([#70](https://github.com/developer-plus/vue-hbs-admin/issues/70)) ([b7aef35](https://github.com/developer-plus/vue-hbs-admin/commit/b7aef35eeb2b29489ea370536382c5548987bc1c)) 71 | 72 | 73 | 74 | # [1.0.0](https://github.com/developer-plus/vue-hbs-admin/compare/v0.0.7...v1.0.0) (2022-04-28) 75 | 76 | 77 | ### Features 78 | 79 | * **component:** page wrapper ([#61](https://github.com/developer-plus/vue-hbs-admin/issues/61)) ([fed3f70](https://github.com/developer-plus/vue-hbs-admin/commit/fed3f70eed106a26f694904a06d5183149bfc348)) 80 | * dynamic menu optimize ([#65](https://github.com/developer-plus/vue-hbs-admin/issues/65)) ([b916478](https://github.com/developer-plus/vue-hbs-admin/commit/b9164780d640abe521bf2088b0a60297549edc58)) 81 | * dynamic menus ([#60](https://github.com/developer-plus/vue-hbs-admin/issues/60)) ([768e7f8](https://github.com/developer-plus/vue-hbs-admin/commit/768e7f8b36e3c4a1d77042fb5bff8c50b3c702af)) 82 | * fullscreen ([#37](https://github.com/developer-plus/vue-hbs-admin/issues/37)) ([de6ee23](https://github.com/developer-plus/vue-hbs-admin/commit/de6ee23fa9dbf159b4ed83f36e9c9f84bcec8b14)) 83 | * loading ([#35](https://github.com/developer-plus/vue-hbs-admin/issues/35)) ([1af952d](https://github.com/developer-plus/vue-hbs-admin/commit/1af952d0b0ce9865eef08230f301940284702128)) 84 | 85 | 86 | 87 | ## [0.0.7](https://github.com/Hongbusi/vue-hbs-admin/compare/v0.0.6...v0.0.7) (2022-04-21) 88 | 89 | 90 | ### Features 91 | 92 | * **demo:** markdown-editor ([#28](https://github.com/Hongbusi/vue-hbs-admin/issues/28)) ([5e210e4](https://github.com/Hongbusi/vue-hbs-admin/commit/5e210e45625e55562899e142e3cfd962720a8d77)) 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hongbusi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-hbs-admin 2 | 3 | English / [简体中文](./README.zh-CN.md) 4 | 5 | [Live Demo](https://vue-hbs-admin.netlify.app) · [Develop Guide](https://vue-hbs-admin-docs.netlify.app) 6 | 7 | 8 | 9 | ## Introduction 10 | 11 | Provide ready-made out-of-the-box solutions and rich examples for backend management systems to improve development efficiency. 12 | 13 | ## Features 14 | 15 | - Vue3,Vite3 - Latest Frontend Technologies; 16 | - Pinia - State Mangement; 17 | - TypeScript - Type-safe JavaScript superset; 18 | - UnoCSS - Atomic CSS Engine; 19 | - Mock - Built-in Mock data solution; 20 | - unplugin - API, components auto import. 21 | 22 | ## Why 23 | 24 | - Learn Vue3 + Vite + TS in depth 25 | - Provide ready-to-use out-of-the-box solutions and rich examples to improve development efficiency. 26 | - Learn and master project development specifications, code specifications, etc. 27 | - Learn and master the process of maintaining open source projects. 28 | - Group progress, learning together, common maintenance, mutual review, step by step. 29 | - Set a small goal: get 300 star 30 | 31 | ## Participating Contributions 32 | 33 | Your contribution is very welcome, and you can build with us in the following ways 😄 : 34 | 35 | - Report bugs, raise new features or description problems via [Issue](https://github.com/Hongbusi/vue-hbs-admin/issues) 36 | - Submit [Pull Request](https://github.com/Hongbusi/vue-hbs-admin/pulls) to improve featuress 37 | 38 | Want to perfect this project together? You can add WeChat: `Hongbusi16530` to learn more. 39 | 40 | ## Git Commit Specification 41 | 42 | Refer to the [Vue]((https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) ) specification ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) 43 | 44 | - `feat` new feature 45 | - `fix` bug fix 46 | - `docs` modify documentation 47 | - `style` code style fix (white-space, formatting, missing semi colons, etc) 48 | - `refactor` refactor code 49 | - `perf` a code change that improves performance 50 | - `test` when adding missing tests 51 | - `build` changing project builds or external dependencies (such as webpack、gulp、npm and others) 52 | - `ci` change scripts commands in configuration files and packages of continuous integration software, e.g. Travis, Circle, etc. 53 | - `chore` changes to the build process or supporting tools (For example, changing the test environment) 54 | - `revert` code revert 55 | 56 | ## Changelog 57 | 58 | [CHANGELOG](./CHANGELOG.md) 59 | 60 | ## Contributors 61 | 62 |  63 | 64 | ## LICENSE 65 | 66 | MIT, Copyright (c) 2022 Hongbusi. 67 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # vue-hbs-admin 2 | 3 | [English](./README.md) / 简体中文 4 | 5 | [在线预览](https://vue-hbs-admin.netlify.app) · [开发文档](https://vue-hbs-admin-docs.netlify.app) 6 | 7 | 8 | 9 | ## 简介 10 | 11 | 为后台管理系统提供现成的开箱解决方案及丰富的示例,提高开发效率。 12 | 13 | ## 特性 14 | 15 | - Vue3,Vite3 - 前端最新技术; 16 | - Pinia - 状态管理; 17 | - TypeScript - 应用程序级 JavaScript 的语言; 18 | - UnoCSS - 即时按需原子 CSS 引擎; 19 | - Mock - 内置 Mock 数据方案; 20 | - unplugin - API,components 自动导入。 21 | 22 | ## 初心 23 | 24 | - 深入学习 Vue3 + Vite + TS; 25 | - 提供现成的开箱解决方案及丰富的示例,提高开发效率; 26 | - 学习并掌握项目开发规范、代码规范等; 27 | - 学习并掌握维护开源项目流程,为日后参加开源打好基础; 28 | - 抱团进步,一起学习,共同维护,相互 review,步步高升; 29 | - 定个小目标:300 star。 30 | 31 | ## 参与贡献 32 | 33 | 我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 😄 : 34 | 35 | - 通过 [Issue](https://github.com/Hongbusi/vue-hbs-admin/issues) 报告 bug、提新需求或进行咨询; 36 | - 提交 [Pull Request](https://github.com/Hongbusi/vue-hbs-admin/pulls) 改进代码。 37 | 38 | 想一起完善这个项目?你可以添加微信:`Hongbusi16530` 了解更多。 39 | 40 | ## Git 提交规范 41 | 42 | 参考 [Vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) 43 | 44 | - `feat` 新增特性 (feature) 45 | - `fix` 修复 Bug(bug fix) 46 | - `docs` 修改文档 (documentation) 47 | - `style` 代码格式修改(white-space, formatting, missing semi colons, etc) 48 | - `refactor` 代码重构(refactor) 49 | - `perf` 改善性能(A code change that improves performance) 50 | - `test` 测试(when adding missing tests) 51 | - `build` 变更项目构建或外部依赖(例如 scopes: webpack、gulp、npm 等) 52 | - `ci` 更改持续集成软件的配置文件和 package 中的 scripts 命令,例如 scopes: Travis, Circle 等 53 | - `chore` 变更构建流程或辅助工具(比如更改测试环境) 54 | - `revert` 代码回退 55 | 56 | ## 更新日志 57 | 58 | [CHANGELOG](./CHANGELOG.md) 59 | 60 | ## Contributors 61 | 62 |  63 | 64 | ## LICENSE 65 | 66 | MIT, Copyright (c) 2022 Hongbusi. 67 | -------------------------------------------------------------------------------- /build/plugins/auto-import.ts: -------------------------------------------------------------------------------- 1 | import AutoImport from 'unplugin-auto-import/vite' 2 | 3 | export default function setupAutoImport() { 4 | return AutoImport({ 5 | imports: [ 6 | 'vue', 7 | 'vue-router', 8 | '@vueuse/core' 9 | ], 10 | dts: 'types/auto-imports.d.ts' 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /build/plugins/icons.ts: -------------------------------------------------------------------------------- 1 | import Icons from 'unplugin-icons/vite' 2 | import { FileSystemIconLoader } from 'unplugin-icons/loaders' 3 | 4 | export default function setupIcons() { 5 | return Icons({ 6 | compiler: 'vue3', 7 | customCollections: { 8 | 'my-icons': FileSystemIconLoader( 9 | 'src/assets/icons', 10 | svg => svg.replace(/^ 11 | 12 | const httpsRE = /^https:\/\// 13 | 14 | /** 15 | * Generate proxy 16 | * @param list 17 | */ 18 | export function createProxy(list: ProxyList = []) { 19 | const ret: ProxyTargetList = {} 20 | for (const [prefix, target] of list) { 21 | const isHttps = httpsRE.test(target) 22 | 23 | // https://github.com/http-party/node-http-proxy#options 24 | ret[prefix] = { 25 | target, 26 | changeOrigin: true, 27 | ws: true, 28 | rewrite: path => path.replace(new RegExp(`^${prefix}`), ''), 29 | // https is require secure=false 30 | ...(isHttps ? { secure: false } : {}) 31 | } 32 | } 33 | return ret 34 | } 35 | -------------------------------------------------------------------------------- /build/utils.ts: -------------------------------------------------------------------------------- 1 | // Read all environment variable configuration files to process.env 2 | export function wrapperEnv(envConf: Recordable): ViteEnv { 3 | const ret: any = {} 4 | 5 | for (const envName of Object.keys(envConf)) { 6 | let realName = envConf[envName].replace(/\\n/g, '\n') 7 | realName = realName === 'true' ? true : realName === 'false' ? false : realName 8 | 9 | if (envName === 'VITE_PORT') { 10 | realName = Number(realName) 11 | } 12 | if (envName === 'VITE_PROXY' && realName) { 13 | try { 14 | realName = JSON.parse(realName.replace(/'/g, '"')) 15 | } 16 | catch (error) { 17 | realName = '' 18 | } 19 | } 20 | ret[envName] = realName 21 | if (typeof realName === 'string') { 22 | process.env[envName] = realName 23 | } 24 | else if (typeof realName === 'object') { 25 | process.env[envName] = JSON.stringify(realName) 26 | } 27 | } 28 | return ret 29 | } 30 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'] 3 | } 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-hbs-admin 9 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /mock/_createMockServer.ts: -------------------------------------------------------------------------------- 1 | import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer' 2 | 3 | const modules = import.meta.globEager('./**/*.ts') 4 | 5 | const mockModules: any[] = [] 6 | Object.keys(modules).forEach((key) => { 7 | if (key.includes('/_')) { 8 | return 9 | } 10 | mockModules.push(...modules[key].default) 11 | }) 12 | 13 | export function setupMockServer() { 14 | createProdMockServer(mockModules) 15 | } 16 | -------------------------------------------------------------------------------- /mock/_util.ts: -------------------------------------------------------------------------------- 1 | export interface requestParams { 2 | method: string 3 | body: any 4 | headers?: { authorization?: string } 5 | query: any 6 | } 7 | 8 | export function resultSuccess(data: T, { message = 'ok' } = {}) { 9 | return { 10 | code: 0, 11 | data, 12 | message, 13 | type: 'success' 14 | } 15 | } 16 | 17 | export function resultError(message = 'Request failed', { code = -1, result = null } = {}) { 18 | return { 19 | code, 20 | result, 21 | message, 22 | type: 'error' 23 | } 24 | } 25 | 26 | export function getRequestToken({ headers }: requestParams): string | undefined { 27 | return headers?.authorization 28 | } 29 | -------------------------------------------------------------------------------- /mock/service/menu.ts: -------------------------------------------------------------------------------- 1 | import type { MockMethod } from 'vite-plugin-mock' 2 | import type { requestParams } from '../_util' 3 | import { resultSuccess, resultError, getRequestToken } from '../_util' 4 | import { createFakeUserList } from './user' 5 | 6 | export default [ 7 | { 8 | url: '/api/menu/list', 9 | method: 'get', 10 | response: (request: requestParams) => { 11 | const token = getRequestToken(request) 12 | if (!token) { 13 | return resultError('Invalid token!') 14 | } 15 | const checkUser = createFakeUserList().find(item => item.token === token) 16 | if (!checkUser) { 17 | return resultError('Invalid user token!') 18 | } 19 | 20 | const menu: Object[] = [ 21 | { 22 | id: '1', 23 | name: '系统总览', 24 | type: 1, 25 | url: '/main/analysis', 26 | icon: 'dashboard', 27 | children: [ 28 | { 29 | id: '2', 30 | name: '核心技术', 31 | type: 2, 32 | url: '/main/analysis/overvie', 33 | children: null 34 | } 35 | ] 36 | }, 37 | { 38 | id: '2', 39 | name: '关于', 40 | type: 2, 41 | url: '/about', 42 | icon: 'dashboard', 43 | children: null 44 | } 45 | ] 46 | 47 | return resultSuccess(menu) 48 | } 49 | } 50 | ] as MockMethod[] 51 | -------------------------------------------------------------------------------- /mock/service/user.ts: -------------------------------------------------------------------------------- 1 | import type { MockMethod } from 'vite-plugin-mock' 2 | import type { requestParams } from '../_util' 3 | import { resultSuccess, resultError, getRequestToken } from '../_util' 4 | 5 | export function createFakeUserList() { 6 | return [ 7 | { 8 | id: '1', 9 | username: 'Hongbusi', 10 | realName: 'Hongbusi', 11 | avatar: 'https://q1.qlogo.cn/g?b=qq&nk=190848757&s=640', 12 | desc: 'zhe', 13 | password: 'admin', 14 | token: 'fakeToken1', 15 | homePath: '/dashboard/analysis', 16 | roles: [ 17 | { 18 | roleName: 'Super Admin', 19 | value: 'super' 20 | } 21 | ] 22 | }, 23 | { 24 | id: '2', 25 | username: 'test', 26 | password: 'admin', 27 | realName: 'test user', 28 | avatar: 'https://q1.qlogo.cn/g?b=qq&nk=339449197&s=640', 29 | desc: 'tester', 30 | token: 'fakeToken2', 31 | homePath: '/dashboard/workbench', 32 | roles: [ 33 | { 34 | roleName: 'Tester', 35 | value: 'test' 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | 42 | export default [ 43 | { 44 | url: '/api/login', 45 | timeout: 200, 46 | method: 'post', 47 | response: ({ body }: requestParams) => { 48 | const { username, password } = body 49 | const checkUser = createFakeUserList().find( 50 | item => item.username === username && password === item.password 51 | ) 52 | 53 | if (!checkUser) { 54 | return resultError('Incorrect account or password!') 55 | } 56 | 57 | const { id, username: _username, token, realName, desc, roles } = checkUser 58 | 59 | return resultSuccess({ 60 | roles, 61 | id, 62 | username: _username, 63 | token, 64 | realName, 65 | desc 66 | }) 67 | } 68 | }, 69 | { 70 | url: '/api/user/info', 71 | method: 'get', 72 | response: (request: requestParams) => { 73 | const token = getRequestToken(request) 74 | if (!token) return resultError('Invalid token') 75 | const checkUser = createFakeUserList().find(item => item.token === token) 76 | if (!checkUser) { 77 | return resultError('The corresponding user information was not obtained!') 78 | } 79 | return resultSuccess(checkUser) 80 | } 81 | } 82 | ] as MockMethod[] 83 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | # bypass npm auto install 3 | NPM_FLAGS = "--version" 4 | NODE_VERSION = "16" 5 | 6 | [build] 7 | publish = "dist" 8 | command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm run build" 9 | 10 | [[redirects]] 11 | from = "/*" 12 | to = "/index.html" 13 | status = 200 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-hbs-admin", 3 | "private": true, 4 | "version": "1.2.2", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "typecheck": "vue-tsc --noEmit", 9 | "preview": "vite preview", 10 | "lint": "lint-staged", 11 | "prepare": "husky install", 12 | "commit": "cz", 13 | "release": "release", 14 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s" 15 | }, 16 | "eslintConfig": { 17 | "extends": "@hongbusi" 18 | }, 19 | "dependencies": { 20 | "@ant-design/icons-vue": "6.1.0", 21 | "@unocss/reset": "0.44.1", 22 | "@vueuse/core": "8.9.2", 23 | "@wangeditor/editor": "5.1.7", 24 | "@wangeditor/editor-for-vue": "5.1.12", 25 | "@zxcvbn-ts/core": "2.0.1", 26 | "ant-design-vue": "3.2.10", 27 | "axios": "0.27.2", 28 | "codemirror": "5", 29 | "cropperjs": "1.5.12", 30 | "dayjs": "1.11.3", 31 | "lodash-es": "4.17.21", 32 | "nprogress": "0.2.0", 33 | "pinia": "2.0.16", 34 | "qrcode": "1.5.1", 35 | "vditor": "3.8.15", 36 | "vue": "3.2.37", 37 | "vue-router": "4.1.2", 38 | "xlsx": "0.18.5" 39 | }, 40 | "devDependencies": { 41 | "@commitlint/cli": "17.0.3", 42 | "@commitlint/config-conventional": "17.0.3", 43 | "@hongbusi/eslint-config": "0.3.4", 44 | "@hongbusi/release": "0.0.9", 45 | "@types/codemirror": "5.60.5", 46 | "@types/lodash-es": "4.17.6", 47 | "@types/node": "18.0.4", 48 | "@types/nprogress": "0.2.0", 49 | "@types/qrcode": "1.4.2", 50 | "@vitejs/plugin-vue": "3.0.0", 51 | "commitizen": "4.2.4", 52 | "conventional-changelog-cli": "2.2.2", 53 | "cz-conventional-changelog": "3.3.0", 54 | "eslint": "8.19.0", 55 | "husky": "8.0.1", 56 | "less": "4.1.3", 57 | "lint-staged": "13.0.3", 58 | "mockjs": "1.1.0", 59 | "rollup": "2.76.0", 60 | "taze": "^0.7.6", 61 | "typescript": "4.7.4", 62 | "unocss": "0.44.1", 63 | "unplugin-auto-import": "0.9.2", 64 | "unplugin-icons": "0.14.7", 65 | "unplugin-vue-components": "0.21.1", 66 | "vite": "3.0.0", 67 | "vite-plugin-mock": "2.9.6", 68 | "vue-tsc": "0.38.5" 69 | }, 70 | "config": { 71 | "commitizen": { 72 | "path": "cz-conventional-changelog" 73 | } 74 | }, 75 | "lint-staged": { 76 | "*.{vue,js,jsx,ts,tsx}": "eslint --fix" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developer-plus/vue-hbs-admin/c7277bf2f7939c18dabdcd996c17963984eb8886/public/favicon.ico -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/api/config.ts: -------------------------------------------------------------------------------- 1 | export const BASE_URL = import.meta.env.VITE_GLOB_API_URL 2 | 3 | export const TIME_OUT = 1000 * 5 4 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import Request from '../utils/request' 2 | import { BASE_URL, TIME_OUT } from './config' 3 | import localCache from '~/utils/cache' 4 | import { EnumCache } from '~/enums' 5 | 6 | const request = new Request({ 7 | baseURL: BASE_URL, 8 | timeout: TIME_OUT, 9 | interceptorHooks: { 10 | requestInterceptor: (config) => { 11 | const token = localCache.getCache(EnumCache.TOKEN_KEY) 12 | if (token) { 13 | config.headers.Authorization = token 14 | } 15 | return config 16 | }, 17 | requestInterceptorCatch: (error) => { 18 | return error 19 | }, 20 | responseInterceptor: (response) => { 21 | return response.data 22 | }, 23 | responseInterceptorCatch: (error) => { 24 | return error 25 | } 26 | } 27 | }) 28 | 29 | export default request 30 | -------------------------------------------------------------------------------- /src/api/user/index.ts: -------------------------------------------------------------------------------- 1 | import request from '../index' 2 | 3 | import type { Account, LoginInfo } from './types' 4 | 5 | enum API { 6 | Login = '/login', 7 | UserInfo = '/user/info', 8 | MenuList = '/menu/list' 9 | } 10 | 11 | export function loginRequest(account: Account) { 12 | return request.post({ 13 | url: API.Login, 14 | data: account 15 | }) 16 | } 17 | 18 | export function getUserInfo() { 19 | return request.get({ url: API.UserInfo }) 20 | } 21 | 22 | export function getMenuList() { 23 | return request.get({ url: API.MenuList }) 24 | } 25 | -------------------------------------------------------------------------------- /src/api/user/types.ts: -------------------------------------------------------------------------------- 1 | export interface Account { 2 | username: string 3 | password: string 4 | } 5 | 6 | export interface LoginInfo { 7 | id: number 8 | token: string 9 | username: string 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/icons/giftbox.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/images/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developer-plus/vue-hbs-admin/c7277bf2f7939c18dabdcd996c17963984eb8886/src/assets/images/qrcode.png -------------------------------------------------------------------------------- /src/assets/images/test.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developer-plus/vue-hbs-admin/c7277bf2f7939c18dabdcd996c17963984eb8886/src/assets/images/test.jpeg -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developer-plus/vue-hbs-admin/c7277bf2f7939c18dabdcd996c17963984eb8886/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/CodeEditor/index.ts: -------------------------------------------------------------------------------- 1 | import CodeEditor from './src/CodeEditor.vue' 2 | 3 | export { 4 | CodeEditor 5 | } 6 | -------------------------------------------------------------------------------- /src/components/CodeEditor/src/CodeEditor.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 115 | 116 | 126 | -------------------------------------------------------------------------------- /src/components/CountTo/index.ts: -------------------------------------------------------------------------------- 1 | import CountTo from './src/CountTo.vue' 2 | 3 | export { 4 | CountTo 5 | } 6 | -------------------------------------------------------------------------------- /src/components/CountTo/src/CountTo.vue: -------------------------------------------------------------------------------- 1 | 2 | {{ value }} 3 | 4 | 5 | 101 | -------------------------------------------------------------------------------- /src/components/Countdown/index.ts: -------------------------------------------------------------------------------- 1 | import CountdownButton from './src/CountdownButton.vue' 2 | import CountdownInput from './src/CountdownInput.vue' 3 | 4 | export { 5 | CountdownButton, 6 | CountdownInput 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Countdown/src/CountdownButton.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ getButtonText }} 4 | 5 | 6 | 7 | 44 | -------------------------------------------------------------------------------- /src/components/Countdown/src/CountdownInput.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 26 | 27 | 40 | -------------------------------------------------------------------------------- /src/components/Countdown/src/useCountdown.ts: -------------------------------------------------------------------------------- 1 | export function useCountdown(count: number) { 2 | const currentCount = ref(count) 3 | const isStart = ref(false) 4 | 5 | let timerId: ReturnType | null 6 | 7 | function clear() { 8 | timerId && window.clearInterval(timerId) 9 | } 10 | 11 | function stop() { 12 | isStart.value = false 13 | clear() 14 | timerId = null 15 | } 16 | 17 | function start() { 18 | if (unref(isStart) || !!timerId) { 19 | return 20 | } 21 | isStart.value = true 22 | timerId = setInterval(() => { 23 | if (unref(currentCount) === 1) { 24 | stop() 25 | currentCount.value = count 26 | } 27 | else { 28 | currentCount.value -= 1 29 | } 30 | }, 1000) 31 | } 32 | 33 | function reset() { 34 | currentCount.value = count 35 | stop() 36 | } 37 | 38 | function restart() { 39 | reset() 40 | start() 41 | } 42 | 43 | tryOnUnmounted(() => { 44 | reset() 45 | }) 46 | 47 | return { start, reset, restart, clear, stop, currentCount, isStart } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/Cropper/index.ts: -------------------------------------------------------------------------------- 1 | import Cropper from './src/Cropper.vue' 2 | 3 | export * from './src/types' 4 | export { 5 | Cropper 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Cropper/src/Cropper.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 169 | 170 | 180 | -------------------------------------------------------------------------------- /src/components/Cropper/src/types.ts: -------------------------------------------------------------------------------- 1 | import type Cropper from 'cropperjs' 2 | 3 | export interface CropendResult { 4 | imgBase64: string 5 | imgInfo: Cropper.Data 6 | } 7 | 8 | export type { Cropper } 9 | -------------------------------------------------------------------------------- /src/components/Excel/index.ts: -------------------------------------------------------------------------------- 1 | import ImportExcel from './src/ImportExcel.vue' 2 | 3 | export { ImportExcel } 4 | export * from './src/types' 5 | export { jsonToSheetXlsx, aoaToSheetXlsx } from './src/ExportExcel' 6 | -------------------------------------------------------------------------------- /src/components/Excel/src/ExportExcel.ts: -------------------------------------------------------------------------------- 1 | import * as XLSX from 'xlsx' 2 | import type { WorkBook } from 'xlsx' 3 | import type { JsonToSheet, AoAToSheet } from './types' 4 | 5 | const { utils, writeFile } = XLSX 6 | 7 | const DEFAULT_FILE_NAME = 'excel-list.xlsx' 8 | 9 | export function jsonToSheetXlsx(options: JsonToSheet) { 10 | const { 11 | data, 12 | header, 13 | filename = DEFAULT_FILE_NAME, 14 | json2sheetOpts = {}, 15 | write2excelOpts = { bookType: 'xlsx' } 16 | } = options 17 | 18 | const arrData = [...data] 19 | if (header) { 20 | arrData.unshift(header) 21 | json2sheetOpts.skipHeader = true 22 | } 23 | 24 | const worksheet = utils.json_to_sheet(arrData, json2sheetOpts) 25 | 26 | /* add worksheet to workbook */ 27 | const workbook: WorkBook = { 28 | SheetNames: [filename], 29 | Sheets: { 30 | [filename]: worksheet 31 | } 32 | } 33 | /* output format determined by filename */ 34 | writeFile(workbook, filename, write2excelOpts) 35 | /* at this point, out.xlsb will have been downloaded */ 36 | } 37 | 38 | export function aoaToSheetXlsx(options: AoAToSheet) { 39 | const { 40 | data, 41 | header, 42 | filename = DEFAULT_FILE_NAME, 43 | write2excelOpts = { bookType: 'xlsx' } 44 | } = options 45 | 46 | const arrData = [...data] 47 | if (header) { 48 | arrData.unshift(header) 49 | } 50 | 51 | const worksheet = utils.aoa_to_sheet(arrData) 52 | 53 | /* add worksheet to workbook */ 54 | const workbook: WorkBook = { 55 | SheetNames: [filename], 56 | Sheets: { 57 | [filename]: worksheet 58 | } 59 | } 60 | /* output format determined by filename */ 61 | writeFile(workbook, filename, write2excelOpts) 62 | /* at this point, out.xlsb will have been downloaded */ 63 | } 64 | -------------------------------------------------------------------------------- /src/components/Excel/src/ImportExcel.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 导入 Excel 12 | 13 | 14 | 15 | 16 | 97 | -------------------------------------------------------------------------------- /src/components/Excel/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { JSON2SheetOpts, WritingOptions } from 'xlsx' 2 | 3 | export interface ExcelData { 4 | header: string[] 5 | results: T[] 6 | meta: { sheetName: string } 7 | } 8 | 9 | export interface JsonToSheet { 10 | data: T[] 11 | header?: T 12 | filename?: string 13 | json2sheetOpts?: JSON2SheetOpts 14 | write2excelOpts?: WritingOptions 15 | } 16 | 17 | export interface AoAToSheet { 18 | data: T[][] 19 | header?: T[] 20 | filename?: string 21 | write2excelOpts?: WritingOptions 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Iframe/index.ts: -------------------------------------------------------------------------------- 1 | import Iframe from './src/Iframe.vue' 2 | 3 | export { 4 | Iframe 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Iframe/src/Iframe.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/components/Page/index.ts: -------------------------------------------------------------------------------- 1 | import PageWrapper from './src/PageWrapper.vue' 2 | 3 | export { 4 | PageWrapper 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Page/src/PageWrapper.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ content }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /src/components/Plum.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 122 | 123 | 133 | -------------------------------------------------------------------------------- /src/components/QrCode/index.ts: -------------------------------------------------------------------------------- 1 | import Qrcode from './src/Qrcode.vue' 2 | 3 | export { 4 | Qrcode 5 | } 6 | 7 | export * from './src/typing' 8 | -------------------------------------------------------------------------------- /src/components/QrCode/src/QrCode.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 111 | -------------------------------------------------------------------------------- /src/components/QrCode/src/drawCanvas.ts: -------------------------------------------------------------------------------- 1 | import { toCanvas } from 'qrcode' 2 | import type { QRCodeRenderersOptions } from 'qrcode' 3 | import { cloneDeep } from 'lodash-es' 4 | import type { RenderQrCodeParams, ContentType } from './typing' 5 | 6 | export const renderQrCode = ({ 7 | canvas, 8 | content, 9 | width = 0, 10 | options: params = {} 11 | }: RenderQrCodeParams) => { 12 | const options = cloneDeep(params) 13 | // 容错率,默认对内容少的二维码采用高容错率,内容多的二维码采用低容错率 14 | options.errorCorrectionLevel = options.errorCorrectionLevel || getErrorCorrectionLevel(content) 15 | 16 | return getOriginWidth(content, options).then((_width: number) => { 17 | options.scale = width === 0 ? undefined : (width / _width) * 4 18 | return toCanvas(canvas, content, options) 19 | }) 20 | } 21 | 22 | // 得到原 QrCode 的大小,以便缩放得到正确的 QrCode 大小 23 | function getOriginWidth(content: ContentType, options: QRCodeRenderersOptions) { 24 | const _canvas = document.createElement('canvas') 25 | return toCanvas(_canvas, content, options).then(() => _canvas.width) 26 | } 27 | 28 | // 对于内容少的 QrCode,增大容错率 29 | function getErrorCorrectionLevel(content: ContentType) { 30 | if (content.length > 36) { 31 | return 'M' 32 | } 33 | else if (content.length > 16) { 34 | return 'Q' 35 | } 36 | else { 37 | return 'H' 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/QrCode/src/drawLogo.ts: -------------------------------------------------------------------------------- 1 | import type { RenderQrCodeParams, LogoType } from './typing' 2 | import { isString } from '~/utils/is' 3 | 4 | export const drawLogo = ({ canvas, logo }: RenderQrCodeParams) => { 5 | if (!logo) { 6 | return new Promise((resolve) => { 7 | resolve((canvas as HTMLCanvasElement).toDataURL()) 8 | }) 9 | } 10 | const canvasWidth = (canvas as HTMLCanvasElement).width 11 | const { 12 | logoSize = 0.15, 13 | bgColor = '#ffffff', 14 | borderSize = 0.05, 15 | crossOrigin, 16 | borderRadius = 8, 17 | logoRadius = 0 18 | } = logo as LogoType 19 | 20 | const logoSrc: string = isString(logo) ? logo : logo.src 21 | const logoWidth = canvasWidth * logoSize 22 | const logoXY = (canvasWidth * (1 - logoSize)) / 2 23 | const logoBgWidth = canvasWidth * (logoSize + borderSize) 24 | const logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2 25 | 26 | const ctx = canvas.getContext('2d') 27 | if (!ctx) return 28 | 29 | // logo 底色 30 | canvasRoundRect(ctx)(logoBgXY, logoBgXY, logoBgWidth, logoBgWidth, borderRadius) 31 | ctx.fillStyle = bgColor 32 | ctx.fill() 33 | 34 | // logo 35 | const image = new Image() 36 | if (crossOrigin || logoRadius) { 37 | image.setAttribute('crossOrigin', crossOrigin || 'anonymous') 38 | } 39 | image.src = logoSrc 40 | 41 | // 使用 image 绘制可以避免某些跨域情况 42 | const drawLogoWithImage = (image: CanvasImageSource) => { 43 | ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth) 44 | } 45 | 46 | // 使用 canvas 绘制以获得更多的功能 47 | const drawLogoWithCanvas = (image: HTMLImageElement) => { 48 | const canvasImage = document.createElement('canvas') 49 | canvasImage.width = logoXY + logoWidth 50 | canvasImage.height = logoXY + logoWidth 51 | const imageCanvas = canvasImage.getContext('2d') 52 | if (!imageCanvas || !ctx) return 53 | imageCanvas.drawImage(image, logoXY, logoXY, logoWidth, logoWidth) 54 | 55 | canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius) 56 | if (!ctx) return 57 | const fillStyle = ctx.createPattern(canvasImage, 'no-repeat') 58 | if (fillStyle) { 59 | ctx.fillStyle = fillStyle 60 | ctx.fill() 61 | } 62 | } 63 | 64 | // 将 logo 绘制到 canvas上 65 | return new Promise((resolve) => { 66 | image.onload = () => { 67 | logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image) 68 | resolve((canvas as HTMLCanvasElement).toDataURL()) 69 | } 70 | }) 71 | } 72 | 73 | // copy 来的方法,用于绘制圆角 74 | function canvasRoundRect(ctx: CanvasRenderingContext2D) { 75 | return (x: number, y: number, w: number, h: number, r: number) => { 76 | const minSize = Math.min(w, h) 77 | if (r > minSize / 2) { 78 | r = minSize / 2 79 | } 80 | ctx.beginPath() 81 | ctx.moveTo(x + r, y) 82 | ctx.arcTo(x + w, y, x + w, y + h, r) 83 | ctx.arcTo(x + w, y + h, x, y + h, r) 84 | ctx.arcTo(x, y + h, x, y, r) 85 | ctx.arcTo(x, y, x + w, y, r) 86 | ctx.closePath() 87 | return ctx 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/components/QrCode/src/qrcodePlus.ts: -------------------------------------------------------------------------------- 1 | // 参考 qr-code-with-logo 进行ts版本修改 2 | import { toCanvas } from './toCanvas' 3 | export * from './typing' 4 | export { toCanvas } 5 | -------------------------------------------------------------------------------- /src/components/QrCode/src/toCanvas.ts: -------------------------------------------------------------------------------- 1 | import { renderQrCode } from './drawCanvas' 2 | import { drawLogo } from './drawLogo' 3 | import type { RenderQrCodeParams } from './typing' 4 | 5 | export const toCanvas = (options: RenderQrCodeParams) => { 6 | return renderQrCode(options) 7 | .then(() => { 8 | return options 9 | }) 10 | .then(drawLogo) as Promise 11 | } 12 | -------------------------------------------------------------------------------- /src/components/QrCode/src/typing.ts: -------------------------------------------------------------------------------- 1 | import type { QRCodeSegment, QRCodeRenderersOptions } from 'qrcode' 2 | 3 | export type ContentType = string | QRCodeSegment[] 4 | 5 | export type { QRCodeRenderersOptions } 6 | 7 | export interface LogoType { 8 | src: string 9 | logoSize: number 10 | borderColor: string 11 | bgColor: string 12 | borderSize: number 13 | crossOrigin: string 14 | borderRadius: number 15 | logoRadius: number 16 | } 17 | 18 | export interface RenderQrCodeParams { 19 | canvas: any 20 | content: ContentType 21 | width?: number 22 | options?: QRCodeRenderersOptions 23 | logo?: LogoType | string 24 | image?: HTMLImageElement 25 | downloadName?: string 26 | download?: boolean | Fn 27 | } 28 | 29 | export type ToCanvasFn = (options: RenderQrCodeParams) => Promise 30 | 31 | export interface QrCodeActionType { 32 | download: (fileName?: string) => void 33 | } 34 | 35 | export interface QrcodeDoneEventParams { 36 | url: string 37 | ctx?: CanvasRenderingContext2D | null 38 | } 39 | -------------------------------------------------------------------------------- /src/components/StrengthMeter/index.ts: -------------------------------------------------------------------------------- 1 | import StrengthMeter from './src/StrengthMeter.vue' 2 | 3 | export { 4 | StrengthMeter 5 | } 6 | -------------------------------------------------------------------------------- /src/components/StrengthMeter/src/StrengthMeter.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 67 | 68 | 140 | -------------------------------------------------------------------------------- /src/components/TickForm/index.ts: -------------------------------------------------------------------------------- 1 | export { default as TickForm } from './src/TickForm.vue' 2 | -------------------------------------------------------------------------------- /src/components/TickForm/src/TickForm.vue: -------------------------------------------------------------------------------- 1 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | {{ childItem.label }} 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | -------------------------------------------------------------------------------- /src/components/TickForm/src/props.ts: -------------------------------------------------------------------------------- 1 | import type { PropType } from 'vue' 2 | 3 | const DataFormPlusProps = { 4 | formConfig: { 5 | type: Object 6 | // default: { labelPlacement: 'left' } 7 | }, 8 | // preset: { 9 | // type: String, 10 | // default: 'form-item', 11 | // validator: (value: string) => { 12 | // if (!['form-item', 'grid-item'].includes(value)) { 13 | // console.error( 14 | // 'preset value must be `form-item` or `grid-item`, the default value is `form-item`' 15 | // ) 16 | // return false 17 | // } 18 | // return true 19 | // } 20 | // }, 21 | options: { 22 | type: Array as PropType> 23 | } 24 | // cols: { 25 | // type: String, 26 | // default: '1 450:2 600:3 900:4' 27 | // } 28 | } 29 | 30 | export default DataFormPlusProps 31 | -------------------------------------------------------------------------------- /src/components/Watermark/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/useWatermark' 2 | 3 | export * from './src/types' 4 | -------------------------------------------------------------------------------- /src/components/Watermark/src/config.ts: -------------------------------------------------------------------------------- 1 | import type { Attr, StyleConfig } from './types' 2 | 3 | export const domSymbol = Symbol('watermark-dom') 4 | 5 | export const initConfig: Required = { 6 | width: 300, 7 | height: 240, 8 | font: '15px Vedana', 9 | textAlign: 'center', 10 | textBaseline: 'middle', 11 | fillStyle: 'rgba(0, 0, 0, 0.3)', 12 | str: '防伪 ☆ 加密', 13 | str2: '' 14 | } 15 | 16 | export const styleConfig: StyleConfig = reactive({ 17 | 'id': domSymbol.toString(), 18 | 'pointer-events': 'none', 19 | 'width': '0px', 20 | 'height': '0px', 21 | 'top': '0px', 22 | 'left': '0px', 23 | 'position': 'absolute', 24 | 'z-index': '100000', 25 | 'background': 'rgba(0, 0, 0, 0.5)' 26 | }) 27 | -------------------------------------------------------------------------------- /src/components/Watermark/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Attr { 2 | str?: string 3 | str2?: string 4 | font?: string 5 | fillStyle?: string 6 | width?: number 7 | height?: number 8 | textAlign?: CanvasTextAlign 9 | textBaseline?: CanvasTextBaseline 10 | } 11 | 12 | export interface StyleConfig { 13 | id: string 14 | 'pointer-events': string 15 | top: string 16 | left: string 17 | width: string 18 | height: string 19 | background: string 20 | position: string 21 | 'z-index': string 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Watermark/src/useProtectWatermark.ts: -------------------------------------------------------------------------------- 1 | import { styleConfig } from './config' 2 | import type { StyleConfig } from './types' 3 | import type { TrackedElement } from '~/utils/useMutationObserver' 4 | import { addTrackedElement, ObserveType, removeTrackedElement } from '~/utils/useMutationObserver' 5 | 6 | const beTrackedWatermark = ref() 7 | const beProtected = ref(false) 8 | 9 | /** 10 | * 防破解水印 11 | * @param el 当前水印的 dom 元素 12 | */ 13 | export function setupProtectWatermark(el: HTMLElement): void { 14 | beProtected.value = true 15 | handleBeRemoved(el) 16 | handleStyleChanged(el, styleConfig) 17 | } 18 | 19 | /** 20 | * 处理水印被删除的情况,这里使用了 MutationObserver 21 | * @param el 当前水印的 dom 元素 22 | */ 23 | function handleBeRemoved(el: HTMLElement) { 24 | const element = beTrackedWatermark.value = { 25 | el, 26 | callback: () => { 27 | document.body.append(el) 28 | }, 29 | type: ObserveType.REMOVE 30 | } 31 | addTrackedElement(element) 32 | } 33 | 34 | /** 35 | * 处理水印的样式被改变,重置样式 36 | * @param el 37 | */ 38 | function handleStyleChanged(el: HTMLElement, config: StyleConfig) { 39 | useRafFn(async() => { 40 | if (beProtected.value) { 41 | await nextTick() 42 | resetStyle(el, config) 43 | } 44 | }) 45 | } 46 | 47 | function resetStyle(el: HTMLElement, config: StyleConfig) { 48 | const beObservedStyleName: (keyof StyleConfig)[] = ['id', 'pointer-events', 'background', 'position', 'z-index', 'width', 'height', 'top', 'left'] 49 | beObservedStyleName.forEach((styleName) => { 50 | if (el.style.getPropertyValue(styleName) !== config[styleName]) { 51 | el.style.setProperty(styleName, config[styleName]) 52 | } 53 | }) 54 | } 55 | 56 | export function stopProtectWatermark(): void { 57 | removeTrackedElement(beTrackedWatermark.value!) 58 | beTrackedWatermark.value = undefined 59 | beProtected.value = false 60 | } 61 | -------------------------------------------------------------------------------- /src/components/Watermark/src/useWatermark.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { throttle } from 'lodash-es' 3 | import type { Attr } from './types' 4 | import { stopProtectWatermark, setupProtectWatermark } from './useProtectWatermark' 5 | import { domSymbol, initConfig, styleConfig } from './config' 6 | import { isDef } from '~/utils/is' 7 | import { addResizeListener, removeResizeListener } from '~/utils/resize' 8 | 9 | export function useWatermark( 10 | appendEl: Ref = ref(document.body) as Ref 11 | ) { 12 | let curAttr: Attr 13 | const func = throttle(() => { 14 | const el = unref(appendEl) 15 | 16 | if (!el) return 17 | const { clientHeight: height, clientWidth: width } = el 18 | updateWatermark({ ...curAttr, height, width }) 19 | }) 20 | const id = domSymbol.toString() 21 | const watermarkEl = shallowRef() 22 | 23 | const clear = () => { 24 | const domId = unref(watermarkEl) 25 | watermarkEl.value = undefined 26 | const el = unref(appendEl) 27 | if (!el) return 28 | domId && el.removeChild(domId) 29 | removeResizeListener(el, func) 30 | stopProtectWatermark() 31 | } 32 | 33 | function createBase64(attr: Attr) { 34 | const can = document.createElement('canvas') 35 | const width = initConfig.width 36 | const height = initConfig.height 37 | Object.assign(can, { width, height }) 38 | 39 | const cans = can.getContext('2d') 40 | if (cans) { 41 | cans.rotate((-20 * Math.PI) / 120) 42 | cans.font = attr?.font || initConfig.font 43 | cans.fillStyle = attr?.fillStyle || initConfig.fillStyle 44 | cans.textAlign = attr?.textAlign || initConfig.textAlign 45 | cans.textBaseline = attr?.textBaseline || initConfig.textBaseline 46 | cans.fillText(attr?.str || initConfig.str, width / 20, height) 47 | attr?.str2 && cans.fillText(attr?.str2, width / 20, height + 20) 48 | } 49 | return can.toDataURL('image/png') 50 | } 51 | 52 | function updateWatermark(attr: Attr) { 53 | const el = unref(watermarkEl) 54 | curAttr = attr 55 | if (!el) return 56 | if (isDef(attr.width)) { 57 | el.style.width = styleConfig.width = `${attr.width}px` 58 | } 59 | if (isDef(attr.height)) { 60 | el.style.height = styleConfig.height = `${attr.height}px` 61 | } 62 | el.style.background = styleConfig.background = `url(${createBase64(attr)}) left top repeat` 63 | } 64 | 65 | const createWatermark = (attr: Attr) => { 66 | if (unref(watermarkEl)) { 67 | updateWatermark(attr) 68 | return id 69 | } 70 | const div = document.createElement('div') 71 | watermarkEl.value = div 72 | setupStyle(div) 73 | // 74 | const el = unref(appendEl) 75 | if (!el) return id 76 | const { clientHeight: height, clientWidth: width } = el 77 | attr.width = width 78 | attr.height = height 79 | updateWatermark(attr) 80 | el.appendChild(div) 81 | return id 82 | } 83 | 84 | function setupStyle(div: HTMLElement) { 85 | div.id = styleConfig.id 86 | div.style.pointerEvents = styleConfig['pointer-events'] 87 | div.style.top = `${styleConfig.top}px` 88 | div.style.left = `${styleConfig.left}px` 89 | div.style.position = styleConfig.position 90 | div.style.zIndex = styleConfig['z-index'] 91 | } 92 | 93 | function setWatermark(attr: Attr = {}) { 94 | if (!document.getElementById(id)) watermarkEl.value = undefined 95 | createWatermark(attr) 96 | setupProtectWatermark(watermarkEl.value!) 97 | addResizeListener(document.documentElement, func) 98 | const instance = getCurrentInstance() 99 | if (instance) { 100 | onBeforeUnmount(() => { 101 | clear() 102 | }) 103 | } 104 | } 105 | 106 | return { setWatermark, clear } 107 | } 108 | -------------------------------------------------------------------------------- /src/directives/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Coonfigure and register global directives 3 | */ 4 | 5 | import type { App } from 'vue' 6 | import { setupLazyImgDirectives } from './lazyImg' 7 | export function setupGlobDirectives(app: App) { 8 | setupLazyImgDirectives(app) 9 | } 10 | -------------------------------------------------------------------------------- /src/directives/lazyImg.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Global authority directive 3 | * Used for lazyLoading image 4 | * @Example v-lazyImg = "UserInfo.pictures" 5 | */ 6 | 7 | import type { App, Directive, DirectiveBinding } from 'vue' 8 | 9 | import { useIntersectionObserver } from '@vueuse/core' 10 | 11 | import defaultImg from '~/assets/images/test.jpeg' 12 | 13 | function lazyImg(el: HTMLImageElement, binding: any) { 14 | const { stop } = useIntersectionObserver(el, ([{ isIntersecting }]) => { 15 | if (isIntersecting) { 16 | // 一般在标签上接收接口传值(v-lazyImg="接口返回值.xxxURL") 17 | el.src = el.src || binding.value || defaultImg 18 | // 加载错误时的默认图片 19 | el.onerror = () => { 20 | el.src = binding.value || defaultImg 21 | } 22 | stop() 23 | } 24 | }) 25 | } 26 | 27 | const mounted = (el: HTMLImageElement, binding: DirectiveBinding) => { 28 | lazyImg(el, binding) 29 | } 30 | 31 | const LazyImgDirective: Directive = { 32 | mounted 33 | } 34 | 35 | export function setupLazyImgDirectives(app: App) { 36 | app.directive('lazyImg', LazyImgDirective) 37 | } 38 | 39 | export default LazyImgDirective 40 | -------------------------------------------------------------------------------- /src/enums/cache.ts: -------------------------------------------------------------------------------- 1 | export enum EnumCache { 2 | // token key 3 | TOKEN_KEY = 'TOKEN__', 4 | // user info key 5 | USER_INFO_KEY = 'USER__INFO__', 6 | // role info key 7 | ROLES_KEY = 'ROLES__KEY__' 8 | } 9 | -------------------------------------------------------------------------------- /src/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from './path' 2 | export * from './role' 3 | export * from './cache' 4 | -------------------------------------------------------------------------------- /src/enums/path.ts: -------------------------------------------------------------------------------- 1 | export enum EnumPath { 2 | // login path 3 | LOGIN = '/login', 4 | // home path 5 | HOME = '/about', 6 | // not found path 7 | NOT_FOUND = '/not-found' 8 | } 9 | -------------------------------------------------------------------------------- /src/enums/role.ts: -------------------------------------------------------------------------------- 1 | export enum EnumRole { 2 | // super admin 3 | SUPER = 'super', 4 | // tester 5 | TEST = 'test' 6 | } 7 | -------------------------------------------------------------------------------- /src/hooks/useRequest.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosResponse } from 'axios' 2 | import type { Ref } from 'vue' 3 | import { isArray } from '~/utils/is' 4 | import { noop } from '~/utils' 5 | 6 | /** 7 | * @author: tick 8 | * @description: 发起请求,判断请求回来的参数是否为空从而作出提示。请求结束后赋值变量 9 | * @param {UseLoadingRequestParameters} options 必须 函数参数配置 10 | * @param {Ref | undefined | Ref} [options.state] 非必需 请求时想要赋值的参数ref类型 11 | * @param {AxiosPromise>} options.axios 必须 axios请求 12 | * @return {AxiosResponse, any>} res 请求接口返回的数据 13 | */ 14 | const useIsNullRequest = async ({ 15 | axios, 16 | state 17 | }: UseIsNullRequestParameters) => { 18 | // todo 信息提示的来源 19 | // <-> 20 | const res = await axios 21 | const response = ( 22 | isArray(res.data.data) ? res.data.data : Object.keys(res.data.data) 23 | ) as string[] | any[] 24 | if (response.length === 0) { 25 | // todo 信息提示 26 | } 27 | if (state && res.data) { 28 | state.value = res.data.data 29 | } 30 | return res 31 | } 32 | 33 | const setArrayValue = (arr: Ref[], value: boolean) => 34 | arr.forEach(item => (item.value = value)) 35 | 36 | /** 37 | * @author: tick 38 | * @description: 拥有控制loading,赋值ref变量的请求工具 39 | * @param {UseLoadingRequestParameters} options 必须- 函数参数配置 40 | * @param {Ref | undefined | Ref} [options.state] 非必需- 请求时想要赋值的参数ref类型 41 | * @param {Ref | Ref[]} options.loadings 必须- ref设置的关于按钮loading的变量 42 | * @param {AxiosPromise>} options.axios 必须- axios请求 43 | * @return {T} 根据函数的泛型决定返回的类型 44 | */ 45 | const useLoadingRequest = async ({ 46 | state, 47 | axios, 48 | loadings = [], 49 | scheduler 50 | }: UseLoadingRequestParameters) => { 51 | const loadingStates: Ref[] = Array.isArray(loadings) 52 | ? loadings 53 | : [loadings] 54 | setArrayValue(loadingStates, true) 55 | const res = await axios 56 | 57 | if (!!scheduler && scheduler !== noop) { 58 | scheduler(res, state) 59 | } 60 | else if ((!scheduler || scheduler === noop) && state && res.data) { 61 | state.value = res.data.data 62 | } 63 | 64 | setArrayValue(loadingStates, false) 65 | 66 | return res 67 | } 68 | 69 | /** 70 | * @author: tick 71 | * @description: 拥有控制loading,赋值ref变量的请求工具,同时可以判断值数组是否为空 72 | * @param {UseLoadingRequestParameters} options 必须 函数参数配置 73 | * @param {Ref | undefined | Ref} [options.state] 非必需 请求时想要赋值的参数ref类型 74 | * @param {Ref | Ref[]} options.loadings 必须 ref设置的关于按钮loading的变量 75 | * @param {AxiosPromise>} options.axios 必须 axios请求 76 | * @return {T} 根据函数的泛型决定返回的类型 77 | */ 78 | const useLoadingRequestOrIsNull = async ({ 79 | state, 80 | axios, 81 | loadings, 82 | scheduler 83 | }: UseLoadingRequestParameters) => { 84 | // todo 信息提示的来源 85 | // <-> 86 | const res = await useLoadingRequest({ 87 | axios, 88 | state, 89 | loadings, 90 | scheduler 91 | }) 92 | if (!res.data.data) return res 93 | const dara = res.data.data 94 | const response = (isArray(dara || []) ? dara : Object.keys(dara)) as 95 | | string[] 96 | | T[] 97 | if (response.length === 0) { 98 | // todo 数据为空时 信息提示 99 | } 100 | 101 | return res 102 | } 103 | 104 | const useMessageRequest = async ({ 105 | state, 106 | axios, 107 | loadings = [], 108 | errorMessage, 109 | successCode = 0, 110 | errorCode = 500, 111 | scheduler 112 | }: UseMessageRequest) => { 113 | // todo 信息提示的来源 114 | // <-> 115 | 116 | try { 117 | const res = await useLoadingRequest({ state, axios, loadings, scheduler }) 118 | if (successCode === res.data.code) { 119 | // todo 成功的信息提示 120 | // message("success", successMessage || res.data.msg); 121 | } 122 | else if (errorCode === res.data.code) { 123 | // todo 失败的信息提示 124 | // message("error", errorMessage || res.data.msg); 125 | } 126 | return res 127 | } 128 | catch (error) { 129 | if (errorMessage) { 130 | // todo 失败的信息提示 131 | // message("error", errorMessage); 132 | } 133 | } 134 | } 135 | 136 | type RequestTools = 137 | | typeof useLoadingRequest 138 | | typeof useLoadingRequestOrIsNull 139 | | typeof useMessageRequest 140 | 141 | /** 142 | * @author: tick 143 | * @description: 使用具有分页功能的接口时调用的函数 144 | * @param {PaginationAxios} options.axios 封装好的axios请求函数 145 | * @param {Ref} options.state 要赋值的变量 146 | * @param {T} options.data 封装好的axios请求函数的参数 147 | * @param {Ref | Ref[]} [options.loadings] ref设置的关于按钮loading的变量 148 | * @param {boolean} [options.isNull] 请求完成后自动判断数据是否为kong并作出提示 149 | * @param {boolean} [options.pageSize] 每页的个数 150 | * @return {PaginationRequestReturn} prev 返回上一页 next 前往下一页 to 前往指定页 151 | */ 152 | const usePaginationRequest = ({ 153 | data, 154 | axios, 155 | state, 156 | loadings, 157 | isNull, 158 | pageSize = 10, 159 | isMessage, 160 | allCount, 161 | successMessage, 162 | errorMessage, 163 | successCode = '200', 164 | errorCode = '400', 165 | scheduler = noop, 166 | page 167 | }: PaginationRequestParams): PaginationRequestReturn => { 168 | const pagination = shallowReactive({ 169 | pageSize, 170 | pageNum: 1 171 | }) 172 | const _data = computed(() => { 173 | if (!data) { 174 | data = {} as T 175 | } 176 | return { 177 | ...data, 178 | pageSize: pagination.pageSize, 179 | pageNum: pagination.pageNum 180 | } 181 | }) 182 | const su = patchRequest(isNull, isMessage) 183 | const stopWatch = watch( 184 | pagination, 185 | async() => { 186 | if (!axios || !su) return 187 | if (page) { 188 | page.value = _data.value.pageNum 189 | } 190 | 191 | const res = (await su({ 192 | axios: axios(_data.value), 193 | state, 194 | loadings: loadings || [], 195 | scheduler, 196 | successMessage, 197 | errorMessage, 198 | successCode, 199 | errorCode 200 | })) as AxiosResponse> 201 | if (allCount && res && res.data && res.data.total) { 202 | allCount.value = res.data.total 203 | } 204 | }, 205 | { immediate: true } 206 | ) 207 | 208 | tryOnScopeDispose(() => { 209 | stopWatch() 210 | }) 211 | return { 212 | prev: () => (pagination.pageNum -= 1), 213 | next: () => (pagination.pageNum += 1), 214 | to: (num: number) => { 215 | return (pagination.pageNum = num) 216 | }, 217 | ...toRefs(pagination) 218 | } 219 | } 220 | function patchRequest( 221 | isNull: boolean | undefined, 222 | isMessage: boolean | undefined 223 | ) { 224 | let su: RequestTools | null = null 225 | if (isNull) { 226 | su = useLoadingRequestOrIsNull 227 | } 228 | else if (!isNull && isMessage) { 229 | su = useMessageRequest 230 | } 231 | else { 232 | su = useLoadingRequest 233 | } 234 | return su 235 | } 236 | 237 | const useAddPagination = ( 238 | key: string, 239 | pagination: Map, 240 | value: PaginationRequestReturn 241 | ) => { 242 | pagination.set(key, value) 243 | } 244 | 245 | /** 246 | * @description: 请求hooks的工具包 247 | * @return {Object} 返回所有请求的hooks函数 248 | */ 249 | function useRequest() { 250 | return { 251 | useLoadingRequest, 252 | useIsNullRequest, 253 | useLoadingRequestOrIsNull, 254 | usePaginationRequest, 255 | useAddPagination, 256 | useMessageRequest 257 | } 258 | } 259 | 260 | export default useRequest 261 | -------------------------------------------------------------------------------- /src/layouts/blank/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/layouts/default/content/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /src/layouts/default/features/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/layouts/default/footer/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | Copyright © 2022 Hongbusi 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/layouts/default/header/components/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ item.meta.title }} 5 | 6 | 7 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /src/layouts/default/header/components/FullScreen.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /src/layouts/default/header/components/SettingDrawer.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /src/layouts/default/header/components/SiderTrigger.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 15 | -------------------------------------------------------------------------------- /src/layouts/default/header/components/UserDropdown.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ user.getUserInfo.username }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 锁定屏幕 14 | 15 | 16 | 17 | 18 | 19 | 退出登录 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 39 | -------------------------------------------------------------------------------- /src/layouts/default/header/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 36 | 37 | 62 | -------------------------------------------------------------------------------- /src/layouts/default/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 33 | 34 | 46 | -------------------------------------------------------------------------------- /src/layouts/default/sidebar/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HbsAdmin 5 | 6 | 7 | 8 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /src/layouts/default/sidebar/components/Menu.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 19 | 20 | 21 | 22 | 74 | -------------------------------------------------------------------------------- /src/layouts/default/sidebar/components/MenuItem.vue: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | {{ menu.meta.title }} 10 | 11 | 12 | 48 | -------------------------------------------------------------------------------- /src/layouts/default/sidebar/components/MenuWithChildren.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ menu.meta?.title }} 8 | 9 | 13 | 19 | 24 | 25 | 26 | 27 | 40 | -------------------------------------------------------------------------------- /src/layouts/default/sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 26 | 27 | 34 | -------------------------------------------------------------------------------- /src/layouts/default/useCollapsed.ts: -------------------------------------------------------------------------------- 1 | const collapsed = ref(false) 2 | 3 | export function useCollapsed() { 4 | const getCollapsed = computed(() => collapsed.value) 5 | 6 | const toggleCollapsed = () => { 7 | collapsed.value = !collapsed.value 8 | } 9 | 10 | return { 11 | getCollapsed, 12 | toggleCollapsed 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/layouts/index.ts: -------------------------------------------------------------------------------- 1 | const DefaultLayout = () => import('./default/index.vue') 2 | const BlankLayout = () => import('./blank/index.vue') 3 | 4 | export { 5 | DefaultLayout, 6 | BlankLayout 7 | } 8 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | import '@unocss/reset/tailwind.css' 5 | import '~/styles/index.less' 6 | import 'uno.css' 7 | 8 | import { setupStore } from '~/stores' 9 | import { setupRouter } from '~/router' 10 | import { setupGlobDirectives } from '~/directives' 11 | import { setupMutationObserver } from '~/utils/useMutationObserver' 12 | 13 | function setupApp() { 14 | const app = createApp(App) 15 | 16 | // Configure store 17 | setupStore(app) 18 | 19 | // Configure router 20 | setupRouter(app) 21 | 22 | // Configure directives 23 | setupGlobDirectives(app) 24 | app.mount('#app') 25 | 26 | // setup mutation observer 27 | setupMutationObserver() 28 | } 29 | 30 | setupApp() 31 | -------------------------------------------------------------------------------- /src/router/guard/index.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'vue-router' 2 | import nProgress from 'nprogress' 3 | import { createPermissionGuard } from './permissionGuard' 4 | 5 | export function setupRouterGuard(router: Router) { 6 | createProgressGuard(router) 7 | createPermissionGuard(router) 8 | } 9 | 10 | function createProgressGuard(router: Router) { 11 | router.beforeEach(() => { 12 | nProgress.start() 13 | }) 14 | 15 | router.afterEach(() => { 16 | nProgress.done() 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /src/router/guard/permissionGuard.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'vue-router' 2 | import localCache from '~/utils/cache' 3 | import { EnumCache, EnumPath } from '~/enums' 4 | 5 | export function createPermissionGuard(router: Router) { 6 | router.beforeEach((to) => { 7 | if (to.path !== EnumPath.LOGIN) { 8 | const token = localCache.getCache(EnumCache.TOKEN_KEY) 9 | if (!token) { 10 | return EnumPath.LOGIN 11 | } 12 | } 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | 3 | import { createRouter, createWebHistory } from 'vue-router' 4 | import { basicRoutes, asyncRoutes } from './routes' 5 | import { setupRouterGuard } from './guard' 6 | import { setupDynamicRoutes } from './routes/plugins/dynamicRoutes' 7 | 8 | export const router = createRouter({ 9 | routes: [...basicRoutes, ...asyncRoutes], 10 | history: createWebHistory(), 11 | scrollBehavior: () => ({ left: 0, top: 0 }) 12 | }) 13 | 14 | export function setupRouter(app: App) { 15 | app.use(router) 16 | 17 | // Router guard 18 | setupRouterGuard(router) 19 | 20 | setupDynamicRoutes(asyncRoutes) 21 | } 22 | -------------------------------------------------------------------------------- /src/router/routes/basic.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { RouteRecordRaw } from 'vue-router' 3 | import { EnumPath } from '~/enums' 4 | import { BlankLayout } from '~/layouts' 5 | 6 | export const basicRoutes: RouteRecordRaw[] = [ 7 | { 8 | path: '/', 9 | name: 'root', 10 | redirect: EnumPath.HOME 11 | }, 12 | { 13 | path: '/login', 14 | name: 'login', 15 | component: BlankLayout, 16 | redirect: '/login', 17 | children: [ 18 | { 19 | path: '', 20 | name: 'login-page', 21 | component: () => import('~/views/login/index.vue') 22 | } 23 | ] 24 | }, 25 | { 26 | path: '/:pathMatch(.*)*', 27 | name: 'not-found', 28 | component: BlankLayout, 29 | children: [ 30 | { 31 | path: '/:pathMatch(.*)*', 32 | name: 'not-found-page', 33 | component: () => import('~/views/page/not-found/index.vue') 34 | } 35 | ] 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /src/router/routes/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteModuleList } from './typings' 2 | 3 | export * from './basic' 4 | 5 | const modules = import.meta.globEager('./modules/**/*.ts') 6 | 7 | const routeModuleList: RouteModuleList = [] 8 | 9 | Object.keys(modules).forEach((key) => { 10 | const mod = modules[key].default || {} 11 | const modList = Array.isArray(mod) ? [...mod] : [mod] 12 | routeModuleList.push(...modList) 13 | }) 14 | 15 | export const asyncRoutes = routeModuleList 16 | -------------------------------------------------------------------------------- /src/router/routes/modules/about.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | import { SettingOutlined } from '@ant-design/icons-vue' 3 | import { DefaultLayout } from '~/layouts' 4 | 5 | const route: RouteRecordRaw = { 6 | path: '/about', 7 | name: 'about', 8 | component: DefaultLayout, 9 | redirect: '/about', 10 | meta: { 11 | icon: SettingOutlined, 12 | single: true, 13 | title: '关于', 14 | sort: 4 15 | }, 16 | children: [ 17 | { 18 | path: '', 19 | name: 'about-page', 20 | component: () => import('~/views/about/index.vue') 21 | } 22 | ] 23 | } 24 | 25 | export default route 26 | -------------------------------------------------------------------------------- /src/router/routes/modules/demo.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | import { 3 | AppstoreOutlined 4 | } from '@ant-design/icons-vue' 5 | import { DefaultLayout } from '~/layouts' 6 | 7 | const route: RouteRecordRaw = { 8 | path: '/demo', 9 | name: 'demo', 10 | component: DefaultLayout, 11 | redirect: '/demo/excel/import', 12 | meta: { 13 | title: 'Demo', 14 | icon: AppstoreOutlined, 15 | sort: 1 16 | }, 17 | children: [ 18 | { 19 | path: 'excel/import', 20 | name: 'import-excel', 21 | component: () => import('~/views/demo/excel/import.vue'), 22 | meta: { 23 | title: 'Import Excel', 24 | sort: 1 25 | } 26 | }, 27 | { 28 | path: 'excel/export', 29 | name: 'export-excel', 30 | component: () => import('~/views/demo/excel/export.vue'), 31 | meta: { 32 | title: 'Export Excel', 33 | sort: 2 34 | } 35 | }, 36 | { 37 | path: 'watermark', 38 | name: 'watermark', 39 | component: () => import('~/views/demo/watermark/index.vue'), 40 | meta: { 41 | title: '水印', 42 | sort: 3 43 | } 44 | }, 45 | { 46 | path: 'count-to', 47 | name: 'count-to', 48 | component: () => import('~/views/demo/count-to/index.vue'), 49 | meta: { 50 | title: 'CountTo', 51 | sort: 4 52 | } 53 | }, 54 | { 55 | path: 'rich-text', 56 | name: 'rich-text', 57 | component: () => import('~/views/demo/rich-text/index.vue'), 58 | meta: { 59 | title: '富文本编辑器', 60 | sort: 5 61 | } 62 | }, 63 | { 64 | path: 'cropper', 65 | name: 'cropper', 66 | component: () => import('~/views/demo/cropper/index.vue'), 67 | meta: { 68 | title: '图片裁剪', 69 | sort: 6 70 | } 71 | }, 72 | { 73 | path: 'md-editor', 74 | name: 'md-editor', 75 | component: () => import('~/views/demo/md-editor/index.vue'), 76 | meta: { 77 | title: 'Markdown 编辑器', 78 | sort: 7 79 | } 80 | }, 81 | { 82 | path: 'fullscreen', 83 | name: 'fullscreen', 84 | component: () => import('~/views/demo/fullscreen/index.vue'), 85 | meta: { 86 | title: '全屏', 87 | sort: 8 88 | } 89 | }, 90 | { 91 | path: 'testLevel2', 92 | name: 'testLevel2', 93 | component: () => import('~/views/about/index.vue'), 94 | meta: { 95 | title: '测试二级菜单', 96 | sort: 9 97 | }, 98 | children: [ 99 | { 100 | path: 'testLevel3', 101 | name: 'testLevel3', 102 | component: () => import('~/views/about/index.vue'), 103 | meta: { 104 | title: '测试三级菜单', 105 | sort: 1 106 | }, 107 | children: [ 108 | { 109 | path: 'testLevel4', 110 | name: 'testLevel4', 111 | component: () => import('~/views/about/index.vue'), 112 | meta: { 113 | title: '测试四级菜单', 114 | sort: 1 115 | } 116 | } 117 | ] 118 | } 119 | ] 120 | }, 121 | { 122 | path: 'code-editor', 123 | name: 'code-editor', 124 | component: () => import('~/views/demo/code-editor/index.vue'), 125 | meta: { 126 | title: 'CodeEditor', 127 | sort: 10 128 | } 129 | }, 130 | { 131 | path: 'protect-element', 132 | name: 'protect-element', 133 | component: () => import('~/views/demo/protect-element/index.vue'), 134 | meta: { 135 | title: '保护元素', 136 | sort: 10 137 | } 138 | }, 139 | { 140 | path: 'tick-form', 141 | name: 'tick-form', 142 | component: () => import('~/views/demo/tick-form/index.vue'), 143 | meta: { 144 | title: 'tick表单', 145 | sort: 11 146 | } 147 | } 148 | ] 149 | } 150 | 151 | export default route 152 | -------------------------------------------------------------------------------- /src/router/routes/modules/docs.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | import { CompassOutlined } from '@ant-design/icons-vue' 3 | import { DefaultLayout } from '~/layouts' 4 | 5 | const route: RouteRecordRaw = { 6 | path: '/docs', 7 | name: 'docs', 8 | component: DefaultLayout, 9 | meta: { 10 | title: '外部页面', 11 | icon: CompassOutlined, 12 | sort: 3 13 | }, 14 | children: [ 15 | { 16 | path: 'inline/project/:src', 17 | name: 'InlineProject', 18 | component: () => import('~/views/docs/hbs-admin/index.vue'), 19 | props: true, 20 | meta: { 21 | title: '项目文档 (内嵌)', 22 | sort: 1, 23 | routeParams: { 24 | src: 'https://vue-hbs-admin-docs.netlify.app/' 25 | } 26 | } 27 | }, 28 | { 29 | path: 'inline/antdv/:src', 30 | name: 'InlineAntVue', 31 | component: () => import('~/views/docs/antdv/index.vue'), 32 | props: true, 33 | meta: { 34 | title: 'antVue文档 (内嵌)', 35 | sort: 2, 36 | routeParams: { 37 | src: 'https://antdv.com/docs/vue/introduce-cn' 38 | } 39 | } 40 | }, 41 | { 42 | path: 'https://vue-hbs-admin-docs.netlify.app/', 43 | name: 'projectExternalLink', 44 | component: () => import('~/views/about/index.vue'), 45 | meta: { 46 | title: '项目文档 (外链)', 47 | sort: 3 48 | } 49 | } 50 | ] 51 | } 52 | 53 | export default route 54 | -------------------------------------------------------------------------------- /src/router/routes/modules/page.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | import { 3 | ChromeOutlined 4 | } from '@ant-design/icons-vue' 5 | import { DefaultLayout } from '~/layouts' 6 | 7 | const route: RouteRecordRaw = { 8 | path: '/page', 9 | name: 'page', 10 | component: DefaultLayout, 11 | redirect: '/page/404', 12 | meta: { 13 | title: '页面', 14 | icon: ChromeOutlined, 15 | sort: 2 16 | }, 17 | children: [ 18 | { 19 | path: '404', 20 | name: '404', 21 | component: () => import('~/views/page/not-found/index.vue'), 22 | meta: { 23 | title: '404 页面', 24 | sort: 1 25 | } 26 | }, 27 | { 28 | path: 'testHide', 29 | name: 'testHide', 30 | component: () => import('~/views/about/index.vue'), 31 | meta: { 32 | title: '测试隐藏菜单', 33 | sort: 2, 34 | isHide: true 35 | } 36 | } 37 | ] 38 | } 39 | 40 | export default route 41 | -------------------------------------------------------------------------------- /src/router/routes/plugins/dynamicRoutes.ts: -------------------------------------------------------------------------------- 1 | import type { RouteModuleList } from '../typings' 2 | import { useRouteStore } from '~/stores' 3 | 4 | type RouteModule = GetArrayItemType 5 | 6 | function sortBySortKey(routerModuleList: RouteModuleList | RouteModule['children']) { 7 | return routerModuleList!.sort((a: RouteModule, b: RouteModule) => (a.meta?.sort || Number.MAX_VALUE) - (b.meta?.sort || Number.MAX_VALUE)) 8 | } 9 | 10 | function sortRoutesBySortKey(routerModuleList: RouteModuleList) { 11 | for (const routerModule of routerModuleList) { 12 | const single = routerModule.meta?.single || false 13 | if (!single) routerModule.children = sortBySortKey(routerModule.children) 14 | } 15 | return sortBySortKey(routerModuleList) 16 | } 17 | 18 | function filterHideRoute(routerModuleList: RouteModuleList | RouteModule['children']) { 19 | if (!routerModuleList) return [] 20 | const filteredModuleList = [] 21 | // for 循环提高执行效率 22 | for (let i = 0; i < routerModuleList.length; i++) { 23 | const routeModule = routerModuleList[i] 24 | if (routeModule.meta?.isHide) continue 25 | let { children } = routeModule 26 | if (children && children.length) { 27 | children = routeModule.children = filterHideRoute(routeModule.children) 28 | // 如果筛选出来子级没有需要展示的,那么就把 children 这个属性删除掉 29 | if (!children.length) Reflect.deleteProperty(routeModule, 'children') 30 | } 31 | filteredModuleList.push(routeModule) 32 | } 33 | return filteredModuleList 34 | } 35 | 36 | /** 37 | * 用于将本地静态路由 push 到路由表中 38 | * @param routerModuleList 路由 39 | */ 40 | export function setupDynamicRoutes(routerModuleList: RouteModuleList) { 41 | const store = useRouteStore() 42 | store.appendRoute(sortRoutesBySortKey(filterHideRoute(routerModuleList))) 43 | } 44 | -------------------------------------------------------------------------------- /src/router/routes/typings.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | export type RouteModuleList = RouteRecordRaw[] 4 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { createPinia } from 'pinia' 3 | 4 | const store = createPinia() 5 | 6 | export function setupStore(app: App) { 7 | app.use(store) 8 | } 9 | 10 | export * from './modules' 11 | -------------------------------------------------------------------------------- /src/stores/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './routes' 2 | export * from './user' 3 | -------------------------------------------------------------------------------- /src/stores/modules/routes.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { RouteModuleList } from '~/router/routes/typings' 3 | 4 | interface RouteState { 5 | routes: RouteModuleList 6 | } 7 | 8 | export const useRouteStore = defineStore('routes', { 9 | state: (): RouteState => ({ 10 | routes: [] 11 | }), 12 | getters: { 13 | getRoutes: (state) => { 14 | return state.routes 15 | } 16 | }, 17 | actions: { 18 | appendRoute(route: GetArrayItemType | RouteModuleList): void { 19 | if (Array.isArray(route)) { this.routes.push(...route) } 20 | else { this.routes.push(route) } 21 | } 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /src/stores/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { router } from '~/router' 3 | import { EnumCache, EnumPath } from '~/enums' 4 | import localCache from '~/utils/cache' 5 | import { loginRequest, getUserInfo, getMenuList } from '~/api/user' 6 | import { isArray } from '~/utils/is' 7 | import { mapMenuToRoutes } from '~/utils/map-menu' 8 | 9 | import type { EnumRole } from '~/enums' 10 | import type { UserInfo } from '#/store' 11 | 12 | interface UserState { 13 | token?: string 14 | userInfo: Nullable 15 | roleList: EnumRole[] 16 | menuList: any[] 17 | } 18 | 19 | export const useUserStore = defineStore('user', { 20 | state: (): UserState => ({ 21 | token: '', 22 | userInfo: null, 23 | roleList: [], 24 | menuList: [] 25 | }), 26 | 27 | getters: { 28 | getToken(): string { 29 | return this.token || localCache.getCache(EnumCache.TOKEN_KEY) 30 | }, 31 | 32 | getUserInfo(): UserInfo { 33 | return this.userInfo || localCache.getCache(EnumCache.USER_INFO_KEY) || {} 34 | }, 35 | 36 | getRoleList(): EnumRole[] { 37 | return this.roleList.length > 0 ? this.roleList : localCache.getCache(EnumCache.ROLES_KEY) 38 | }, 39 | 40 | getMenuList(): any[] { 41 | return this.menuList 42 | } 43 | }, 44 | 45 | actions: { 46 | setToken(token: string | undefined): void { 47 | this.token = token || '' 48 | localCache.setCache(EnumCache.TOKEN_KEY, this.token) 49 | }, 50 | 51 | setUserInfo(userInfo: UserInfo | null) { 52 | this.userInfo = userInfo 53 | localCache.setCache(EnumCache.USER_INFO_KEY, userInfo) 54 | }, 55 | 56 | setRoleList(roleList: EnumRole[]) { 57 | this.roleList = roleList 58 | localCache.setCache(EnumCache.ROLES_KEY, roleList) 59 | }, 60 | 61 | setMenuList(menuList: any[]) { 62 | this.menuList = menuList 63 | 64 | // map menu to routes 65 | const routes = mapMenuToRoutes(menuList) 66 | routes.forEach((route) => { 67 | router.addRoute(route) 68 | }) 69 | }, 70 | 71 | async loginAction(account: { username: string; password: string }) { 72 | try { 73 | const result = await loginRequest(account) 74 | const { token } = result 75 | 76 | // save token 77 | this.setToken(token) 78 | this.afterLoginAction() 79 | } 80 | catch (error) { 81 | return Promise.reject(error) 82 | } 83 | }, 84 | 85 | async afterLoginAction() { 86 | if (!this.getToken) return null 87 | 88 | // get user info 89 | await this.getUserInfoAction() 90 | 91 | // get menu list 92 | await this.getMenuListAction() 93 | 94 | router.push(EnumPath.HOME) 95 | }, 96 | 97 | logout() { 98 | this.setToken(undefined) 99 | this.setUserInfo(null) 100 | 101 | router.push(EnumPath.LOGIN) 102 | }, 103 | 104 | async getUserInfoAction(): Promise { 105 | if (!this.getToken) return null 106 | 107 | const userInfo = await getUserInfo() 108 | const { roles = [] } = userInfo 109 | if (isArray(roles)) { 110 | const roleList = roles.map(item => item.value) as EnumRole[] 111 | this.setRoleList(roleList) 112 | } 113 | else { 114 | userInfo.roles = [] 115 | this.setRoleList([]) 116 | } 117 | this.setUserInfo(userInfo) 118 | return userInfo 119 | }, 120 | 121 | async getMenuListAction(): Promise { 122 | if (!this.getToken) return null 123 | 124 | const menuList = await getMenuList() 125 | 126 | this.setMenuList(menuList) 127 | } 128 | } 129 | }) 130 | -------------------------------------------------------------------------------- /src/styles/animation.less: -------------------------------------------------------------------------------- 1 | each(range(7), { 2 | * > .enter-x:nth-child(@{value}) { 3 | transform: translateX(50px); 4 | } 5 | 6 | * > .-enter-x:nth-child(@{value}) { 7 | transform: translateX(-50px); 8 | } 9 | 10 | * > .enter-y:nth-child(@{value}) { 11 | transform: translateY(50px); 12 | } 13 | 14 | * > .-enter-y:nth-child(@{value}) { 15 | transform: translateY(-50px); 16 | } 17 | 18 | * > .enter-x:nth-child(@{value}), 19 | * > .-enter-x:nth-child(@{value}) { 20 | animation: enter-x-animation 0.4s ease-in-out 0.3s; 21 | } 22 | 23 | * > .enter-y:nth-child(@{value}), 24 | * > .-enter-y:nth-child(@{value}) { 25 | animation: enter-y-animation 0.4s ease-in-out 0.3s; 26 | } 27 | 28 | * > .enter-x:nth-child(@{value}), 29 | * > .-enter-x:nth-child(@{value}), 30 | * > .enter-y:nth-child(@{value}), 31 | * > .-enter-y:nth-child(@{value}) { 32 | z-index: (10 - @value); 33 | opacity: 0; 34 | animation-fill-mode: forwards; 35 | animation-delay: (@value * 1 / 10s); 36 | } 37 | }) 38 | 39 | @keyframes enter-x-animation { 40 | to { 41 | opacity: 1; 42 | transform: translateX(0); 43 | } 44 | } 45 | 46 | @keyframes enter-y-animation { 47 | to { 48 | opacity: 1; 49 | transform: translateY(0); 50 | } 51 | } 52 | 53 | .fade-enter-active, 54 | .fade-leave-active { 55 | transition: opacity 0.2s ease-in-out; 56 | } 57 | 58 | .fade-enter-from, 59 | .fade-leave-to { 60 | opacity: 0; 61 | } 62 | 63 | /* fade-slide */ 64 | .fade-slide-leave-active, 65 | .fade-slide-enter-active { 66 | transition: all 0.3s; 67 | } 68 | 69 | .fade-slide-enter-from { 70 | opacity: 0; 71 | transform: translateX(-30px); 72 | } 73 | 74 | .fade-slide-leave-to { 75 | opacity: 0; 76 | transform: translateX(30px); 77 | } 78 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import './variable.less'; 2 | @import './animation.less'; 3 | @import './main.less'; 4 | -------------------------------------------------------------------------------- /src/styles/main.less: -------------------------------------------------------------------------------- 1 | #nprogress { 2 | pointer-events: none; 3 | 4 | .bar { 5 | position: fixed; 6 | top: 0; 7 | left: 0; 8 | z-index: 99999; 9 | width: 100%; 10 | height: 2px; 11 | background-color: #1890ff; 12 | opacity: 0.75; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/variable.less: -------------------------------------------------------------------------------- 1 | :root { 2 | --header-height: 56px; 3 | --sidebar-width: 200px; 4 | --sidebar-collapsed-width: 56px; 5 | --footer-height: 70px; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/cache.ts: -------------------------------------------------------------------------------- 1 | class LocalCache { 2 | setCache(key: string, value: any) { 3 | if (value) { 4 | window.localStorage.setItem(key, JSON.stringify(value)) 5 | } 6 | } 7 | 8 | getCache(key: string) { 9 | const value = window.localStorage.getItem(key) 10 | if (value) { 11 | return JSON.parse(value) 12 | } 13 | } 14 | 15 | removeCache(key: string) { 16 | window.localStorage.removeItem(key) 17 | } 18 | 19 | clearLocal() { 20 | window.localStorage.clear() 21 | } 22 | } 23 | 24 | export default new LocalCache() 25 | -------------------------------------------------------------------------------- /src/utils/file/download.ts: -------------------------------------------------------------------------------- 1 | import { openWindow } from '..' 2 | 3 | interface DownloadInfo { 4 | url: string 5 | target?: TargetContext 6 | fileName?: string 7 | } 8 | 9 | /** 10 | * Download file according to file address 11 | * @param {*} sUrl 12 | */ 13 | export function downloadByUrl({ url, target = '_blank', fileName }: DownloadInfo): boolean { 14 | const isChrome = window.navigator.userAgent.toLowerCase().indexOf('chrome') > -1 15 | const isSafari = window.navigator.userAgent.toLowerCase().indexOf('safari') > -1 16 | 17 | if (/(iP)/g.test(window.navigator.userAgent)) { 18 | console.error('Your browser does not support download!') 19 | return false 20 | } 21 | if (isChrome || isSafari) { 22 | const link = document.createElement('a') 23 | link.href = url 24 | link.target = target 25 | 26 | if (link.download !== undefined) { 27 | link.download = fileName || url.substring(url.lastIndexOf('/') + 1, url.length) 28 | } 29 | 30 | if (document.createEvent) { 31 | const e = document.createEvent('MouseEvents') 32 | e.initEvent('click', true, true) 33 | link.dispatchEvent(e) 34 | return true 35 | } 36 | } 37 | if (url.indexOf('?') === -1) { 38 | url += '?download' 39 | } 40 | 41 | openWindow(url, { target }) 42 | return true 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentScope, onScopeDispose } from 'vue' 2 | 3 | export function openWindow( 4 | url: string, 5 | opt?: { 6 | target?: TargetContext | string 7 | noopener?: boolean 8 | noreferrer?: boolean 9 | } 10 | ) { 11 | const { target = '__blank', noopener = true, noreferrer = true } = opt || {} 12 | const feature: string[] = [] 13 | 14 | noopener && feature.push('noopener=yes') 15 | noreferrer && feature.push('noreferrer=yes') 16 | 17 | window.open(url, target, feature.join(',')) 18 | } 19 | 20 | export const noop = () => {} 21 | 22 | export function tryOnScopeDispose(fn: () => void) { 23 | if (getCurrentScope()) { 24 | onScopeDispose(fn) 25 | return true 26 | } 27 | return false 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | export function is(val: unknown, type: string) { 2 | return toString.call(val) === `[object ${type}]` 3 | } 4 | 5 | export function isArray(val: any): val is Array { 6 | return val && Array.isArray(val) 7 | } 8 | 9 | export function isFunction(val: unknown): val is Function { 10 | return typeof val === 'function' 11 | } 12 | 13 | export function isString(val: unknown): val is string { 14 | return is(val, 'String') 15 | } 16 | 17 | export function isNumber(val: unknown): val is number { 18 | return is(val, 'Number') 19 | } 20 | 21 | export function isDef(val?: T): val is T { 22 | return typeof val !== 'undefined' 23 | } 24 | 25 | export function isUrl(url: string): boolean { 26 | const reg 27 | = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i 28 | return reg.test(url) 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/log.ts: -------------------------------------------------------------------------------- 1 | export const logError = (error: any) => console.error(error) 2 | export const logWarning = (warning: any) => console.warn(warning) 3 | -------------------------------------------------------------------------------- /src/utils/map-menu.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { RouteRecordRaw } from 'vue-router' 3 | import { asyncRoutes } from '~/router/routes' 4 | 5 | export function mapMenuToRoutes(menuList: any[]): RouteRecordRaw[] { 6 | const routes: RouteRecordRaw[] = [] 7 | 8 | const _recurseGetRoute = (menus: any[]) => { 9 | for (const menu of menus) { 10 | if (menu.type === 2) { 11 | const route = asyncRoutes.find(route => route.path === menu.url) 12 | if (route) routes.push(route) 13 | } 14 | else { 15 | _recurseGetRoute(menu.children ?? []) 16 | } 17 | } 18 | } 19 | _recurseGetRoute(menuList) 20 | 21 | return routes 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/request/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import type { AxiosInstance } from 'axios' 3 | import type { InterceptorHooks, RequestConfig, Result } from './types' 4 | 5 | class Request { 6 | private instance: AxiosInstance 7 | private interceptorHooks?: InterceptorHooks 8 | 9 | constructor(config: RequestConfig) { 10 | this.instance = axios.create(config) 11 | this.interceptorHooks = config.interceptorHooks 12 | 13 | this.setupInterceptor() 14 | } 15 | 16 | private setupInterceptor(): void { 17 | // 添加对应实例独有的请求拦截器 18 | this.instance.interceptors.request.use( 19 | this.interceptorHooks?.requestInterceptor, 20 | this.interceptorHooks?.requestInterceptorCatch 21 | ) 22 | 23 | // 添加对应实例独有的响应拦截器 24 | this.instance.interceptors.response.use( 25 | this.interceptorHooks?.responseInterceptor, 26 | this.interceptorHooks?.responseInterceptorCatch 27 | ) 28 | 29 | // 添加所有实例都有的请求拦截器 30 | this.instance.interceptors.request.use( 31 | (config) => { 32 | return config 33 | }, 34 | (error) => { 35 | return error 36 | } 37 | ) 38 | 39 | // 添加所有实例都有的响应拦截器 40 | this.instance.interceptors.response.use( 41 | (response) => { 42 | return response 43 | }, 44 | (error) => { 45 | return error 46 | } 47 | ) 48 | } 49 | 50 | request(config: RequestConfig): Promise { 51 | return new Promise((resolve, reject) => { 52 | this.instance 53 | .request>(config) 54 | .then((response) => { 55 | resolve(response.data) 56 | }) 57 | .catch((error) => { 58 | reject(error) 59 | }) 60 | }) 61 | } 62 | 63 | get(config: RequestConfig): Promise { 64 | return this.request({ ...config, method: 'GET' }) 65 | } 66 | 67 | post(config: RequestConfig): Promise { 68 | return this.request({ ...config, method: 'POST' }) 69 | } 70 | 71 | delete(config: RequestConfig): Promise { 72 | return this.request({ ...config, method: 'DELETE' }) 73 | } 74 | 75 | patch(config: RequestConfig): Promise { 76 | return this.request({ ...config, method: 'PATCH' }) 77 | } 78 | 79 | put(config: RequestConfig): Promise { 80 | return this.request({ ...config, method: 'PUT' }) 81 | } 82 | } 83 | 84 | export default Request 85 | -------------------------------------------------------------------------------- /src/utils/request/types.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig, AxiosResponse } from 'axios' 2 | 3 | export interface InterceptorHooks { 4 | requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig 5 | requestInterceptorCatch?: (error: any) => any 6 | 7 | responseInterceptor?: (response: AxiosResponse) => AxiosResponse 8 | responseInterceptorCatch?: (error: any) => any 9 | } 10 | 11 | export interface RequestConfig extends AxiosRequestConfig { 12 | interceptorHooks?: InterceptorHooks 13 | } 14 | 15 | export interface Result { 16 | code: number 17 | data: T 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/resize.ts: -------------------------------------------------------------------------------- 1 | const isServer = typeof window === 'undefined' 2 | let disconnect: () => void 3 | 4 | function resizeHandler(entries: any) { 5 | for (const entry of entries) { 6 | const listeners = entry.target.__resizeListeners__ || [] 7 | if (listeners.length) { 8 | listeners.forEach((fn: () => any) => { 9 | fn() 10 | }) 11 | } 12 | } 13 | } 14 | 15 | export function addResizeListener(element: any, fn: () => any) { 16 | if (isServer) return 17 | if (!element.__resizeListeners__) { 18 | element.__resizeListeners__ = [] 19 | const { stop } = useResizeObserver(element, resizeHandler) 20 | disconnect = stop 21 | } 22 | element.__resizeListeners__.push(fn) 23 | } 24 | 25 | export function removeResizeListener(element: any, fn: () => any) { 26 | if (!element || !element.__resizeListeners__) return 27 | element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1) 28 | if (!element.__resizeListeners__.length) { 29 | disconnect() 30 | } 31 | } 32 | 33 | export function triggerWindowResize() { 34 | const event = document.createEvent('HTMLEvents') 35 | event.initEvent('resize', true, true); 36 | (event as any).eventType = 'message' 37 | window.dispatchEvent(event) 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/useMutationObserver.ts: -------------------------------------------------------------------------------- 1 | export enum ObserveType { 2 | REMOVE 3 | } 4 | 5 | export interface TrackedElement { 6 | el: HTMLElement | null 7 | callback: (el?: Node) => void 8 | type: ObserveType 9 | } 10 | 11 | const beTrackedElements = ref([]) 12 | 13 | /** 14 | * 添加被追踪变化的节点,节点的结构需要符合 `TrackedElement` 的结构 15 | * @param element 被追踪的节点 16 | */ 17 | export function addTrackedElement(element: TrackedElement) { 18 | beTrackedElements.value.push(element) 19 | } 20 | 21 | /** 22 | * 删除被追踪的节点,节点的结构需要符合 `TrackedElement` 的结构 23 | * @param element 被追踪的节点 24 | */ 25 | export function removeTrackedElement(element: TrackedElement) { 26 | beTrackedElements.value = beTrackedElements.value.filter(item => item.el !== element.el) 27 | } 28 | 29 | /** 30 | * 对删除节点的处理 31 | * @param mutation 32 | */ 33 | function observeRemovedNode(mutation: MutationRecord) { 34 | // 对删除的节点进行的处理 35 | mutation.removedNodes.forEach((item) => { 36 | const trackedElement = beTrackedElements.value.find(element => element.el === item && element.type === ObserveType.REMOVE) 37 | if (trackedElement) { 38 | trackedElement.callback(item) 39 | } 40 | }) 41 | } 42 | 43 | /** 44 | * 启动 MutationObserver 45 | * @param targetNode 被监听的节点,默认是 document.body 46 | */ 47 | export function setupMutationObserver(targetNode: HTMLElement = document.body) { 48 | if (!window.MutationObserver) return console.warn('sorry, your browser doesn\'t support MutationObserver') 49 | const config = { attributes: true, childList: true, subtree: true } 50 | const callback = function(mutationsList: MutationRecord[]) { 51 | if (!beTrackedElements.value.length) return 52 | for (const mutation of mutationsList) { 53 | observeRemovedNode(mutation) 54 | } 55 | } 56 | const observer = new MutationObserver(callback) 57 | observer.observe(targetNode, config) 58 | } 59 | -------------------------------------------------------------------------------- /src/views/about/components/Dependencies.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ value }} 6 | 7 | 8 | 9 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/views/about/components/DevDependencies.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ value }} 6 | 7 | 8 | 9 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/views/about/components/ProjectInfo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ version }} 7 | 8 | 9 | 10 | 11 | {{ lastBuildTime }} 12 | 13 | 14 | 15 | Github 16 | 17 | 18 | 预览地址 19 | 20 | 21 | 22 | 23 | 24 | 28 | -------------------------------------------------------------------------------- /src/views/about/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /src/views/demo/code-editor/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 主题: 6 | 12 | 13 | 14 | 语言: 15 | 21 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | 35 | 71 | -------------------------------------------------------------------------------- /src/views/demo/count-to/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | +1000 7 | 8 | 9 | -1000 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 开始 30 | 31 | 32 | 重置 33 | 34 | 35 | 36 | 37 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 61 | -------------------------------------------------------------------------------- /src/views/demo/cropper/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 26 | -------------------------------------------------------------------------------- /src/views/demo/excel/data.ts: -------------------------------------------------------------------------------- 1 | export const columns = [ 2 | { 3 | title: 'ID', 4 | dataIndex: 'id', 5 | width: 80 6 | }, 7 | { 8 | title: '姓名', 9 | dataIndex: 'name', 10 | width: 120 11 | }, 12 | { 13 | title: '年龄', 14 | dataIndex: 'age', 15 | width: 80 16 | }, 17 | { 18 | title: '编号', 19 | dataIndex: 'no', 20 | width: 80 21 | }, 22 | { 23 | title: '地址', 24 | dataIndex: 'address' 25 | }, 26 | { 27 | title: '开始时间', 28 | dataIndex: 'beginTime' 29 | }, 30 | { 31 | title: '结束时间', 32 | dataIndex: 'endTime' 33 | } 34 | ] 35 | 36 | export const data: any[] = (() => { 37 | const arr: any[] = [] 38 | for (let index = 0; index < 40; index++) { 39 | arr.push({ 40 | id: `${index}`, 41 | name: `${index} John Brown`, 42 | age: `${index + 10}`, 43 | no: `${index}98678`, 44 | address: 'New York No. 1 Lake ParkNew York No. 1 Lake Park', 45 | beginTime: new Date().toLocaleString(), 46 | endTime: new Date().toLocaleString() 47 | }) 48 | } 49 | return arr 50 | })() 51 | 52 | // ["ID", "姓名", "年龄", "编号", "地址", "开始时间", "结束时间"] 53 | export const arrHeader = columns.map(column => column.title) 54 | 55 | // [["ID", "姓名", "年龄", "编号", "地址", "开始时间", "结束时间"],["0", "0 John Brown", "10", "098678"]] 56 | export const arrData = data.map((item) => { 57 | return Object.keys(item).map(key => item[key]) 58 | }) 59 | -------------------------------------------------------------------------------- /src/views/demo/excel/export.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Json 数据导出 6 | 7 | 8 | Json 数据导出(自定义头部) 9 | 10 | 11 | Array 数据导出 12 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 59 | -------------------------------------------------------------------------------- /src/views/demo/excel/import.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 44 | -------------------------------------------------------------------------------- /src/views/demo/fullscreen/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 进入全屏 6 | 7 | 8 | 切换全屏 9 | 10 | 11 | 退出全屏 12 | 13 | 当前全屏状态:{{ isFullscreen }} 14 | 15 | 16 | 17 | 进入 DOM 元素全屏 18 | 19 | 20 | 21 | 22 | 23 | 退出 DOM 元素全屏 24 | 25 | 当前全屏状态:{{ isDomElFullscreen }} 26 | 27 | 28 | 29 | 30 | 31 | 49 | -------------------------------------------------------------------------------- /src/views/demo/md-editor/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 使用 Viditor 作为 Markdown 编辑器 10 | 11 | 12 | Github Repo: https://github.com/Vanessa219/vditor 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 98 | -------------------------------------------------------------------------------- /src/views/demo/protect-element/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 保护状态:{{ isProtected }} 5 | 6 | 7 | 8 | 开启保护 9 | 10 | 11 | 删除元素 12 | 13 | 14 | 关闭保护 15 | 16 | 17 | 重置元素 18 | 19 | 20 | 21 | 22 | 23 | 80 | -------------------------------------------------------------------------------- /src/views/demo/rich-text/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 插入文本 8 | 9 | 10 | 打印 HTML 11 | 12 | 13 | 禁止/启用 14 | 15 | 16 | 17 | 18 | 24 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 48 | 49 | 50 | 51 | 52 | 128 | -------------------------------------------------------------------------------- /src/views/demo/tick-form/demo/ant.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 | 39 | 40 | 41 | 获取参数 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/views/demo/tick-form/demo/params.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 56 | 57 | 58 | 59 | 获取参数 60 | 61 | 62 | 分割字段默认的key其实是tick,按照逻辑参数应该输出key字段,可是我在此配置项种加入了重构参数,所以tick被分割成了多个字段 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/views/demo/tick-form/demo/reset.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 39 | 40 | 41 | 42 | 表单重制 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/views/demo/tick-form/demo/update.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 46 | 47 | 48 | 49 | 更新 50 | 51 | 52 | 密码的key其实是password,但是我让他代理了tick当数据中存在tick字段时也会更新 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/views/demo/tick-form/demo/validator.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 36 | 37 | 38 | 39 | 获取参数 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/views/demo/tick-form/demo/watch.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 37 | 38 | 39 | 在账号内输入可获取log 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/views/demo/tick-form/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | -------------------------------------------------------------------------------- /src/views/demo/watermark/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 创建默认 6 | 7 | 18 | 创建自定义 19 | 20 | 21 | 清除 22 | 23 | 24 | 25 | 26 | 27 | 38 | -------------------------------------------------------------------------------- /src/views/docs/antdv/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/views/docs/hbs-admin/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/views/login/components/ForgetPasswordForm.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 重置 29 | 30 | 31 | 32 | 33 | 34 | 返回 35 | 36 | 37 | 38 | 39 | 40 | 72 | -------------------------------------------------------------------------------- /src/views/login/components/LoginForm.vue: -------------------------------------------------------------------------------- 1 | 2 | 9 | 13 | 18 | 19 | 20 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 记住密码 36 | 37 | 38 | 39 | 40 | 41 | 42 | 忘记密码? 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 登录 51 | 52 | 53 | 54 | 55 | 56 | 57 | 手机登录 58 | 59 | 60 | 61 | 62 | 二维码登录 63 | 64 | 65 | 66 | 67 | 注册 68 | 69 | 70 | 71 | 72 | 73 | 其他登录方式 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 135 | -------------------------------------------------------------------------------- /src/views/login/components/MobileForm.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 登录 22 | 23 | 24 | 25 | 26 | 27 | 返回 28 | 29 | 30 | 31 | 32 | 33 | 63 | -------------------------------------------------------------------------------- /src/views/login/components/QrCodeForm.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 扫码后点击"确认",即可完成登录 14 | 15 | 16 | 17 | 返回 18 | 19 | 20 | 21 | 22 | 33 | -------------------------------------------------------------------------------- /src/views/login/components/RegisterForm.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | 注册 43 | 44 | 45 | 46 | 47 | 48 | 返回 49 | 50 | 51 | 52 | 53 | 54 | 91 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | 24 | 48 | -------------------------------------------------------------------------------- /src/views/login/useLogin.ts: -------------------------------------------------------------------------------- 1 | export enum LoginStateEnum { 2 | LOGIN, 3 | REGISTER, 4 | RESET_PASSWORD, 5 | MOBILE, 6 | QR_CODE 7 | } 8 | 9 | const currentState = ref(LoginStateEnum.LOGIN) 10 | 11 | export function useLoginState() { 12 | function setLoginState(state: LoginStateEnum) { 13 | currentState.value = state 14 | } 15 | 16 | const getLoginState = computed(() => currentState.value) 17 | 18 | function handleBackLogin() { 19 | setLoginState(LoginStateEnum.LOGIN) 20 | } 21 | 22 | return { setLoginState, getLoginState, handleBackLogin } 23 | } 24 | -------------------------------------------------------------------------------- /src/views/page/not-found/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 20 | 27 | 34 | 41 | 48 | 55 | 56 | 57 | 58 | 404 59 | 抱歉,您访问的页面不存在 60 | 61 | 67 | 68 | 69 | 返回 70 | 71 | 72 | 回到首页 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 176 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "lib": ["esnext", "dom"], 15 | "incremental": false, 16 | "skipLibCheck": true, 17 | "types": ["vite/client"], 18 | "paths": { 19 | "~/*": ["src/*"], 20 | "#/*": ["types/*"] 21 | } 22 | }, 23 | "include": [ 24 | "src/**/*.ts", 25 | "src/**/*.d.ts", 26 | "src/**/*.tsx", 27 | "src/**/*.vue", 28 | "types/**/*.d.ts", 29 | "mock/**/*.ts", 30 | "build/**/*.ts", 31 | "vite.config.ts", 32 | "package.json" 33 | ], 34 | "exclude": ["dist", "node_modules"] 35 | } 36 | -------------------------------------------------------------------------------- /types/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | const __APP_INFO__: { 3 | pkg: { 4 | name: string 5 | version: string 6 | dependencies: Recordable 7 | devDependencies: Recordable 8 | } 9 | lastBuildTime: string 10 | } 11 | 12 | type Nullable = T | null 13 | type Recordable = Record 14 | 15 | interface ViteEnv { 16 | VITE_PORT: number 17 | VITE_USE_MOCK: boolean 18 | VITE_PUBLIC_PATH: string 19 | VITE_PROXY: [string, string][] 20 | VITE_DROP_CONSOLE: boolean 21 | } 22 | 23 | interface ChangeEvent extends Event { 24 | target: HTMLInputElement 25 | } 26 | } 27 | 28 | export {} 29 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Fn { 2 | (...arg: T[]): R 3 | } 4 | 5 | declare type GetArrayItemType = T extends Array ? S : never 6 | 7 | declare type TargetContext = '_self' | '_blank' 8 | -------------------------------------------------------------------------------- /types/request.d.ts: -------------------------------------------------------------------------------- 1 | declare interface ResponseData { 2 | code: string 3 | msg: string 4 | data: T 5 | total?: number 6 | } 7 | -------------------------------------------------------------------------------- /types/requestHooks.d.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosPromise, AxiosResponse } from 'axios' 2 | import type { Ref } from 'vue' 3 | declare global { 4 | interface BaseParams { 5 | state?: Ref 6 | axios: AxiosPromise> 7 | key?: string 8 | } 9 | 10 | interface UseLoadingRequestParameters extends BaseParams { 11 | loadings?: Ref | Ref[] 12 | scheduler?: ( 13 | res: AxiosResponse, any>, 14 | refSeta?: Ref 15 | ) => any 16 | } 17 | type UseIsNullRequestParameters = BaseParams 18 | 19 | interface UseMessageRequest extends UseLoadingRequestParameters { 20 | successMessage?: string 21 | errorMessage?: string 22 | successCode?: string | number 23 | errorCode?: string | number 24 | } 25 | 26 | interface UseOptionsRequestParameters { 27 | axios?: AxiosPromise> 28 | scheduler?: ( 29 | res: AxiosResponse, any>, 30 | refSeta?: Ref 31 | ) => void 32 | } 33 | 34 | type Pagination = T & { pageSize: number; pageNum: number } 35 | 36 | /** 37 | * @description usePaginationRequest函数的返回值 38 | */ 39 | interface PaginationRequestReturn { 40 | prev: () => number 41 | next: () => number 42 | to: (num: number) => number 43 | pageSize: Ref 44 | pageNum: Ref 45 | } 46 | type PaginationRequestReturnRes = Promise 47 | 48 | type PaginationTool = Record 49 | 50 | type PaginationAxios = ( 51 | data: Pagination 52 | ) => AxiosPromise> 53 | 54 | interface PaginationRequestParams { 55 | successMessage?: string 56 | errorMessage?: string 57 | successCode?: string 58 | errorCode?: string 59 | axios?: PaginationAxios 60 | state?: Ref 61 | data?: T 62 | loadings?: Ref | Ref[] 63 | isNull?: boolean 64 | isMessage?: boolean 65 | pageSize?: number 66 | allCount?: Ref 67 | scheduler?: (res: AxiosResponse>, refSeta?: Ref) => void 68 | page?: Ref 69 | } 70 | 71 | type PaginationAxiosCache = (data: any) => AxiosPromise> 72 | interface UseCacheRequestParameters { 73 | key: string 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /types/routes.d.ts: -------------------------------------------------------------------------------- 1 | import 'vue-router' 2 | import type { FunctionalComponent } from 'vue' 3 | 4 | declare module 'vue-router' { 5 | interface RouteMeta { 6 | // 标题 7 | title: string 8 | // 排序索引 9 | sort?: number 10 | // 是否隐藏菜单 11 | isHide?: boolean 12 | // icon,目前仅支持引入 antdv 中的 icon 组件 13 | icon?: FunctionalComponent 14 | // 是否是单独的菜单 15 | single?: boolean 16 | // 路由携带参数 17 | routeParams?: RouteLocationNormalized['params'] 18 | routeQuery?: RouteLocationNormalized['query'] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /types/store.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface UserInfo { 3 | userId: string | number 4 | username: string 5 | realName: string 6 | avatar: string 7 | desc?: string 8 | homePath?: string 9 | roles: any 10 | } 11 | -------------------------------------------------------------------------------- /types/tickForm.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare interface TickFormItem { 3 | label: string 4 | key: string 5 | type: string 6 | cops?: object 7 | name?: string 8 | message?: string 9 | trigger?: string 10 | required?: boolean 11 | rules?: object[] 12 | ant?: boolean 13 | proxyKey?: string | string[] 14 | watchKey?: string[] | string 15 | defaultValue?: any 16 | valueType?: string 17 | axiosOptions?: () => Promise 18 | reconfiguration?: (value: any) => { key: string; value: any }[] 19 | update?: (row: any) => any 20 | reset?: (formItem: TickFormData) => string | boolean | number | object 21 | // naiveValidator?: (rule: FormItemRule, value: any) => boolean | Error 22 | antValidator?: () => boolean | Error 23 | // validator?: (value: FormPlusData, message: MessageApi) => boolean 24 | validator?: () => boolean 25 | watchCallBack?: (params: TickFormData[], value: string[], self: TickFormData) => void 26 | } 27 | declare interface TickFormData extends TickFormItem { 28 | value: any | null 29 | options: { label: string; value: string }[] 30 | _watch?: WatchStopHandle 31 | _loading: boolean 32 | _isWatchUpdate: boolean 33 | } 34 | 35 | declare interface TickFormType { 36 | antValidator: (callBack: Function) => void 37 | generatorParams: () => object 38 | validator: () => boolean 39 | reset: () => void 40 | update: (value: object) => void 41 | } 42 | -------------------------------------------------------------------------------- /unocss.config.js: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | presetUno, 4 | presetAttributify, 5 | transformerDirectives, 6 | transformerVariantGroup 7 | } from 'unocss' 8 | 9 | export default defineConfig({ 10 | presets: [ 11 | presetUno(), 12 | presetAttributify() 13 | ], 14 | transformers: [ 15 | transformerDirectives(), 16 | transformerVariantGroup() 17 | ] 18 | }) 19 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import type { UserConfig, ConfigEnv } from 'vite' 3 | import { loadEnv } from 'vite' 4 | import dayjs from 'dayjs' 5 | import pkg from './package.json' 6 | import { wrapperEnv } from './build/utils' 7 | import setupVitePlugins from './build/plugins' 8 | import { createProxy } from './build/proxy' 9 | 10 | function pathResolve(dir: string) { 11 | return resolve(process.cwd(), '.', dir) 12 | } 13 | 14 | const { dependencies, devDependencies, name, version } = pkg 15 | const __APP_INFO__ = { 16 | pkg: { dependencies, devDependencies, name, version }, 17 | lastBuildTime: dayjs().format('YYYY-MM-DD HH:mm:ss') 18 | } 19 | 20 | export default ({ command, mode }: ConfigEnv): UserConfig => { 21 | const root = process.cwd() 22 | 23 | const env = loadEnv(mode, root) 24 | 25 | // The boolean type read by loadEnv is a string. This function can be converted to boolean type 26 | const viteEnv = wrapperEnv(env) 27 | 28 | const { VITE_PUBLIC_PATH, VITE_PORT, VITE_PROXY, VITE_DROP_CONSOLE } 29 | = viteEnv 30 | 31 | const isBuild = command === 'build' 32 | 33 | return { 34 | base: VITE_PUBLIC_PATH, 35 | root, 36 | resolve: { 37 | alias: [ 38 | { 39 | find: /~\//, 40 | replacement: `${pathResolve('src')}/` 41 | }, 42 | { 43 | find: /#\//, 44 | replacement: `${pathResolve('types')}/` 45 | } 46 | ] 47 | }, 48 | 49 | server: { 50 | // Listening on all local IPs 51 | host: true, 52 | open: true, 53 | port: VITE_PORT, 54 | // Load proxy configuration from .env 55 | proxy: createProxy(VITE_PROXY) 56 | }, 57 | 58 | esbuild: { 59 | pure: VITE_DROP_CONSOLE ? ['console.log', 'debugger'] : [] 60 | }, 61 | 62 | define: { 63 | __APP_INFO__: JSON.stringify(__APP_INFO__) 64 | }, 65 | 66 | // The vite plugin used by the project. The quantity is large, so it is separately extracted and managed 67 | plugins: setupVitePlugins(viteEnv, isBuild) 68 | } 69 | } 70 | --------------------------------------------------------------------------------
9 | 使用 Viditor 作为 Markdown 编辑器 10 |
12 | Github Repo: https://github.com/Vanessa219/vditor 13 |
62 | 分割字段默认的key其实是tick,按照逻辑参数应该输出key字段,可是我在此配置项种加入了重构参数,所以tick被分割成了多个字段 63 |
52 | 密码的key其实是password,但是我让他代理了tick当数据中存在tick字段时也会更新 53 |
在账号内输入可获取log
抱歉,您访问的页面不存在