rowan.wu 11 月之前
當前提交
14106013e8
共有 100 個文件被更改,包括 3239 次插入0 次删除
  1. 4 0
      .browserslistrc
  2. 47 0
      .chglog/CHANGELOG.tpl.md
  3. 27 0
      .chglog/config.yml
  4. 107 0
      .commitlintrc.cjs
  5. 7 0
      .dockerignore
  6. 19 0
      .editorconfig
  7. 3 0
      .env
  8. 23 0
      .env.analyze
  9. 19 0
      .env.development
  10. 20 0
      .env.production
  11. 21 0
      .env.test
  12. 16 0
      .eslintignore
  13. 7 0
      .eslintrc.cjs
  14. 11 0
      .gitattributes
  15. 42 0
      .github/workflows/eslint.yml
  16. 33 0
      .github/workflows/node.js.yml
  17. 35 0
      .gitignore
  18. 6 0
      .gitpod.yml
  19. 8 0
      .husky/commit-msg
  20. 9 0
      .husky/common.sh
  21. 10 0
      .husky/pre-commit
  22. 7 0
      .npmrc
  23. 21 0
      .originallicense/LICENSE
  24. 12 0
      .prettierignore
  25. 19 0
      .prettierrc.cjs
  26. 2 0
      .stylelintignore
  27. 4 0
      .stylelintrc.cjs
  28. 13 0
      .vscode/extensions.json
  29. 13 0
      .vscode/launch.json
  30. 173 0
      .vscode/settings.json
  31. 422 0
      CHANGELOG.md
  32. 8 0
      Dockerfile
  33. 9 0
      LICENSE
  34. 25 0
      Makefile
  35. 3 0
      README.En.md
  36. 3 0
      README.md
  37. 0 0
      apps/portal-view/.gitkeep
  38. 15 0
      apps/test-server/README.md
  39. 18 0
      apps/test-server/controller/FileController.ts
  40. 15 0
      apps/test-server/controller/UserController.ts
  41. 18 0
      apps/test-server/ecosystem.config.cjs
  42. 63 0
      apps/test-server/index.ts
  43. 8 0
      apps/test-server/nodemon.json
  44. 36 0
      apps/test-server/package.json
  45. 23 0
      apps/test-server/routes.ts
  46. 54 0
      apps/test-server/service/FileService.ts
  47. 25 0
      apps/test-server/service/UserService.ts
  48. 7 0
      apps/test-server/tsconfig.json
  49. 9 0
      apps/test-server/utils.ts
  50. 38 0
      deploy/default.conf
  51. 146 0
      index.html
  52. 9 0
      internal/eslint-config/.eslintignore
  53. 4 0
      internal/eslint-config/.eslintrc.cjs
  54. 10 0
      internal/eslint-config/build.config.ts
  55. 50 0
      internal/eslint-config/package.json
  56. 91 0
      internal/eslint-config/src/index.ts
  57. 57 0
      internal/eslint-config/src/strict.ts
  58. 5 0
      internal/eslint-config/tsconfig.json
  59. 9 0
      internal/stylelint-config/.eslintignore
  60. 4 0
      internal/stylelint-config/.eslintrc.cjs
  61. 10 0
      internal/stylelint-config/build.config.ts
  62. 49 0
      internal/stylelint-config/package.json
  63. 92 0
      internal/stylelint-config/src/index.ts
  64. 5 0
      internal/stylelint-config/tsconfig.json
  65. 4 0
      internal/ts-config/.eslintignore
  66. 27 0
      internal/ts-config/base.json
  67. 18 0
      internal/ts-config/node-server.json
  68. 12 0
      internal/ts-config/node.json
  69. 26 0
      internal/ts-config/package.json
  70. 10 0
      internal/ts-config/vue-app.json
  71. 9 0
      internal/vite-config/.eslintignore
  72. 4 0
      internal/vite-config/.eslintrc.cjs
  73. 10 0
      internal/vite-config/build.config.ts
  74. 59 0
      internal/vite-config/package.json
  75. 119 0
      internal/vite-config/src/config/application.ts
  76. 27 0
      internal/vite-config/src/config/common.ts
  77. 42 0
      internal/vite-config/src/config/package.ts
  78. 2 0
      internal/vite-config/src/index.ts
  79. 104 0
      internal/vite-config/src/plugins/appConfig.ts
  80. 38 0
      internal/vite-config/src/plugins/compress.ts
  81. 13 0
      internal/vite-config/src/plugins/html.ts
  82. 59 0
      internal/vite-config/src/plugins/index.ts
  83. 19 0
      internal/vite-config/src/plugins/mock.ts
  84. 17 0
      internal/vite-config/src/plugins/svgSprite.ts
  85. 14 0
      internal/vite-config/src/plugins/visualizer.ts
  86. 49 0
      internal/vite-config/src/utils/env.ts
  87. 16 0
      internal/vite-config/src/utils/hash.ts
  88. 49 0
      internal/vite-config/src/utils/modifyVars.ts
  89. 5 0
      internal/vite-config/tsconfig.json
  90. 20 0
      mock/_createProductionServer.ts
  91. 57 0
      mock/_util.ts
  92. 42 0
      mock/sys/user.ts
  93. 162 0
      package.json
  94. 0 0
      packages/.gitkeep
  95. 4 0
      packages/hooks/.eslintrc.cjs
  96. 10 0
      packages/hooks/build.config.ts
  97. 40 0
      packages/hooks/package.json
  98. 6 0
      packages/hooks/src/index.ts
  99. 25 0
      packages/hooks/src/onMountedOrActivated.ts
  100. 43 0
      packages/hooks/src/useAttrs.ts

+ 4 - 0
.browserslistrc

@@ -0,0 +1,4 @@
+> 1%
+last 2 versions
+not dead
+not ie 11

+ 47 - 0
.chglog/CHANGELOG.tpl.md

@@ -0,0 +1,47 @@
+{{ if .Versions -}} <a name="unreleased"></a>
+
+## [Unreleased]
+
+{{ if .Unreleased.CommitGroups -}} {{ range .Unreleased.CommitGroups -}}
+
+### {{ .Title }}
+
+{{ range .Commits -}}
+
+- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} {{ end }} {{ end -}} {{ end -}} {{ end -}}
+
+{{ range .Versions }} <a name="{{ .Tag.Name }}"></a>
+
+## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }}
+
+{{ range .CommitGroups -}}
+
+### {{ .Title }}
+
+{{ range .Commits -}}
+
+- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} {{ end }} {{ end -}}
+
+{{- if .RevertCommits -}}
+
+### Reverts
+
+{{ range .RevertCommits -}}
+
+- {{ .Revert.Header }} {{ end }} {{ end -}}
+
+{{- if .MergeCommits -}}
+
+### Pull Requests
+
+{{ range .MergeCommits -}}
+
+- {{ .Header }} {{ end }} {{ end -}}
+
+{{- if .NoteGroups -}} {{ range .NoteGroups -}}
+
+### {{ .Title }}
+
+{{ range .Notes }} {{ .Body }} {{ end }} {{ end -}} {{ end -}} {{ end -}}
+
+{{- if .Versions }} [Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD {{ range .Versions -}} {{ if .Tag.Previous -}} [{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }} {{ end -}} {{ end -}} {{ end -}}

+ 27 - 0
.chglog/config.yml

@@ -0,0 +1,27 @@
+style: github
+template: CHANGELOG.tpl.md
+info:
+  title: CHANGELOG
+  repository_url: https://github.com/suyuan32/simple-admin-backend-ui
+options:
+  commits:
+    # filters:
+    #   Type:
+    #     - feat
+    #     - fix
+    #     - perf
+    #     - refactor
+  commit_groups:
+    # title_maps:
+    #   feat: Features
+    #   fix: Bug Fixes
+    #   perf: Performance Improvements
+    #   refactor: Code Refactoring
+  header:
+    pattern: "^(\\w*)\\:\\s(.*)$"
+    pattern_maps:
+      - Type
+      - Subject
+  notes:
+    keywords:
+      - BREAKING CHANGE

+ 107 - 0
.commitlintrc.cjs

@@ -0,0 +1,107 @@
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const scopes = fs
+  .readdirSync(path.resolve(__dirname, 'src'), { withFileTypes: true })
+  .filter((dirent) => dirent.isDirectory())
+  .map((dirent) => dirent.name.replace(/s$/, ''));
+
+// precomputed scope
+const scopeComplete = execSync('git status --porcelain || true')
+  .toString()
+  .trim()
+  .split('\n')
+  .find((r) => ~r.indexOf('M  src'))
+  ?.replace(/(\/)/g, '%%')
+  ?.match(/src%%((\w|-)*)/)?.[1]
+  ?.replace(/s$/, '');
+
+/** @type {import('cz-git').UserConfig} */
+module.exports = {
+  ignores: [(commit) => commit.includes('init')],
+  extends: ['@commitlint/config-conventional'],
+  rules: {
+    'body-leading-blank': [2, 'always'],
+    'footer-leading-blank': [1, 'always'],
+    'header-max-length': [2, 'always', 108],
+    'subject-empty': [2, 'never'],
+    'type-empty': [2, 'never'],
+    'subject-case': [0],
+    'type-enum': [
+      2,
+      'always',
+      [
+        'feat',
+        'fix',
+        'perf',
+        'style',
+        'docs',
+        'test',
+        'refactor',
+        'build',
+        'ci',
+        'chore',
+        'revert',
+        'wip',
+        'workflow',
+        'types',
+        'release',
+      ],
+    ],
+  },
+  prompt: {
+    /** @use `yarn commit :f` */
+    alias: {
+      f: 'docs: fix typos',
+      r: 'docs: update README',
+      s: 'style: update code format',
+      b: 'build: bump dependencies',
+      c: 'chore: update config',
+    },
+    customScopesAlign: !scopeComplete ? 'top' : 'bottom',
+    defaultScope: scopeComplete,
+    scopes: [...scopes, 'mock'],
+    allowEmptyIssuePrefixs: false,
+    allowCustomIssuePrefixs: false,
+
+    // English
+    typesAppend: [
+      { value: 'wip', name: 'wip:      work in process' },
+      { value: 'workflow', name: 'workflow: workflow improvements' },
+      { value: 'types', name: 'types:    type definition file changes' },
+    ],
+
+    // 中英文对照版
+    // messages: {
+    //   type: '选择你要提交的类型 :',
+    //   scope: '选择一个提交范围 (可选):',
+    //   customScope: '请输入自定义的提交范围 :',
+    //   subject: '填写简短精炼的变更描述 :\n',
+    //   body: '填写更加详细的变更描述 (可选)。使用 "|" 换行 :\n',
+    //   breaking: '列举非兼容性重大的变更 (可选)。使用 "|" 换行 :\n',
+    //   footerPrefixsSelect: '选择关联issue前缀 (可选):',
+    //   customFooterPrefixs: '输入自定义issue前缀 :',
+    //   footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
+    //   confirmCommit: '是否提交或修改commit ?',
+    // },
+    // types: [
+    //   { value: 'feat', name: 'feat:     新增功能' },
+    //   { value: 'fix', name: 'fix:      修复缺陷' },
+    //   { value: 'docs', name: 'docs:     文档变更' },
+    //   { value: 'style', name: 'style:    代码格式' },
+    //   { value: 'refactor', name: 'refactor: 代码重构' },
+    //   { value: 'perf', name: 'perf:     性能优化' },
+    //   { value: 'test', name: 'test:     添加疏漏测试或已有测试改动' },
+    //   { value: 'build', name: 'build:    构建流程、外部依赖变更 (如升级 npm 包、修改打包配置等)' },
+    //   { value: 'ci', name: 'ci:       修改 CI 配置、脚本' },
+    //   { value: 'revert', name: 'revert:   回滚 commit' },
+    //   { value: 'chore', name: 'chore:    对构建过程或辅助工具和库的更改 (不影响源文件、测试用例)' },
+    //   { value: 'wip', name: 'wip:      正在开发中' },
+    //   { value: 'workflow', name: 'workflow: 工作流程改进' },
+    //   { value: 'types', name: 'types:    类型定义文件修改' },
+    // ],
+    // emptyScopesAlias: 'empty:      不填写',
+    // customScopesAlias: 'custom:     自定义',
+  },
+};

+ 7 - 0
.dockerignore

@@ -0,0 +1,7 @@
+node_modules/
+.husky/
+build/
+public/
+src/
+tests/
+types/

+ 19 - 0
.editorconfig

@@ -0,0 +1,19 @@
+root = true
+
+[*]
+charset=utf-8
+end_of_line=lf
+insert_final_newline=true
+indent_style=space
+indent_size=2
+max_line_length = 100
+
+[*.{yml,yaml,json}]
+indent_style = space
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
+
+[Makefile]
+indent_style = tab

+ 3 - 0
.env

@@ -0,0 +1,3 @@
+# spa-title
+VITE_GLOB_APP_TITLE = 冠客微信管理系统
+

+ 23 - 0
.env.analyze

@@ -0,0 +1,23 @@
+# Whether to open mock
+VITE_USE_MOCK = true
+
+# public path
+VITE_PUBLIC_PATH = /
+
+# Whether to enable gzip or brotli compression
+# Optional: gzip | brotli | none
+# If you need multiple forms, you can use `,` to separate
+VITE_BUILD_COMPRESS = 'none'
+
+
+# Basic interface address SPA
+VITE_GLOB_API_URL=/basic-api
+
+# File upload address, optional
+# It can be forwarded by nginx or write the actual address directly
+VITE_GLOB_UPLOAD_URL=/upload
+
+# Interface prefix
+VITE_GLOB_API_URL_PREFIX=
+
+VITE_ENABLE_ANALYZE = true

+ 19 - 0
.env.development

@@ -0,0 +1,19 @@
+# Whether to open mock
+VITE_USE_MOCK = false
+
+# public path
+VITE_PUBLIC_PATH = /
+
+VITE_BUILD_COMPRESS = 'none'
+
+# Delete console
+VITE_DROP_CONSOLE = false
+
+# Basic interface address SPA
+VITE_GLOB_API_URL=
+
+# File upload address, optional
+VITE_GLOB_UPLOAD_URL=/fms-api/upload
+
+# Interface prefix
+VITE_GLOB_API_URL_PREFIX=

+ 20 - 0
.env.production

@@ -0,0 +1,20 @@
+# Whether to open mock
+VITE_USE_MOCK = false
+
+# public path
+VITE_PUBLIC_PATH = /
+
+# Whether to enable gzip or brotli compression
+# Optional: gzip | brotli | none
+# If you need multiple forms, you can use `,` to separate
+VITE_BUILD_COMPRESS = 'none'
+
+
+# Basic interface address SPA
+VITE_GLOB_API_URL=
+
+# File upload address, optional
+VITE_GLOB_UPLOAD_URL=/fms-api/upload
+
+# Interface prefix
+VITE_GLOB_API_URL_PREFIX=

+ 21 - 0
.env.test

@@ -0,0 +1,21 @@
+NODE_ENV=production
+# Whether to open mock
+VITE_USE_MOCK = true
+
+# public path
+VITE_PUBLIC_PATH = /
+
+# Whether to enable gzip or brotli compression
+# Optional: gzip | brotli | none
+# If you need multiple forms, you can use `,` to separate
+VITE_BUILD_COMPRESS = 'none'
+
+# Basic interface address SPA
+VITE_GLOB_API_URL=/sys-api
+
+# File upload address, optional
+# It can be forwarded by nginx or write the actual address directly
+VITE_GLOB_UPLOAD_URL=/fms-api/upload
+
+# Interface prefix
+VITE_GLOB_API_URL_PREFIX=

+ 16 - 0
.eslintignore

@@ -0,0 +1,16 @@
+
+*.sh
+node_modules
+*.md
+*.woff
+*.ttf
+.vscode
+.idea
+dist
+/public
+/docs
+.husky
+.local
+/bin
+Dockerfile
+package.json

+ 7 - 0
.eslintrc.cjs

@@ -0,0 +1,7 @@
+module.exports = {
+  root: true,
+  extends: ['@vben'],
+  rules: {
+    'no-undef': 'off',
+  },
+};

+ 11 - 0
.gitattributes

@@ -0,0 +1,11 @@
+# https://docs.github.com/cn/get-started/getting-started-with-git/configuring-git-to-handle-line-endings
+
+# Automatically normalize line endings (to LF) for all text-based files.
+* text=auto eol=lf
+
+# Declare files that will always have CRLF line endings on checkout.
+*.{cmd,[cC][mM][dD]} text eol=crlf
+*.{bat,[bB][aA][tT]} text eol=crlf
+
+# Denote all files that are truly binary and should not be modified.
+*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary

+ 42 - 0
.github/workflows/eslint.yml

@@ -0,0 +1,42 @@
+name: ESLint
+
+on:
+  push:
+    branches: ['master', 'dev']
+  pull_request:
+    branches: ['master', 'dev']
+  schedule:
+    - cron: '0 0 * * Mon'
+
+jobs:
+  eslint:
+    name: Run eslint scanning
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      security-events: write
+      actions: read
+
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v3
+
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v3
+        with:
+          node-version: ${{ matrix.node-version }}
+
+      - name: Install pnpm
+        run: npm install -g pnpm
+
+      - name: Install ESLint
+        run: |
+          npm install -g eslint
+          npm install -g  @microsoft/eslint-formatter-sarif
+
+      - name: Install dependencies
+        run: pnpm install
+
+      - name: Run ESLint
+        run: |
+          pnpm lint

+ 33 - 0
.github/workflows/node.js.yml

@@ -0,0 +1,33 @@
+name: Node.js CI
+
+on:
+  push:
+    branches: [master, dev]
+  pull_request:
+    branches: [master, dev]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [16.x, 18.x]
+
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v3
+
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v3
+        with:
+          node-version: ${{ matrix.node-version }}
+
+      - name: Install pnpm
+        run: npm install -g pnpm
+
+      - name: Install dependencies
+        run: pnpm install
+
+      - name: Build
+        run: pnpm build

+ 35 - 0
.gitignore

@@ -0,0 +1,35 @@
+node_modules
+node-jiti
+.DS_Store
+dist
+.cache
+.turbo
+
+tests/server/static
+tests/server/static/upload
+
+.local
+# local env files
+.env.local
+.env.*.local
+.eslintcache
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+# .vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+package-lock.json
+pnpm-lock.yaml
+
+.history

+ 6 - 0
.gitpod.yml

@@ -0,0 +1,6 @@
+ports:
+  - port: 3344
+    onOpen: open-preview
+tasks:
+  - init: pnpm install
+    command: pnpm run dev

+ 8 - 0
.husky/commit-msg

@@ -0,0 +1,8 @@
+#!/bin/sh
+
+# shellcheck source=./_/husky.sh
+. "$(dirname "$0")/_/husky.sh"
+
+PATH="/usr/local/bin:$PATH"
+
+# npx --no-install commitlint --edit "$1"

+ 9 - 0
.husky/common.sh

@@ -0,0 +1,9 @@
+#!/bin/sh
+command_exists () {
+  command -v "$1" >/dev/null 2>&1
+}
+
+# Workaround for Windows 10, Git Bash and Yarn
+if command_exists winpty && test -t 1; then
+  exec < /dev/tty
+fi

+ 10 - 0
.husky/pre-commit

@@ -0,0 +1,10 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+. "$(dirname "$0")/common.sh"
+
+[ -n "$CI" ] && exit 0
+
+PATH="/usr/local/bin:$PATH"
+
+# Format and submit code according to lintstagedrc.js configuration
+# npm run lint

+ 7 - 0
.npmrc

@@ -0,0 +1,7 @@
+public-hoist-pattern[]=husky
+public-hoist-pattern[]=*eslint*
+public-hoist-pattern[]=*prettier*
+public-hoist-pattern[]=lint-staged
+public-hoist-pattern[]=*stylelint*
+public-hoist-pattern[]=@commitlint/cli
+public-hoist-pattern[]=@vben/eslint-config

+ 21 - 0
.originallicense/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020-present, Vben
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 12 - 0
.prettierignore

@@ -0,0 +1,12 @@
+dist
+.local
+.output.js
+node_modules
+
+**/*.svg
+**/*.sh
+
+public
+.npmrc
+
+*-lock.yaml

+ 19 - 0
.prettierrc.cjs

@@ -0,0 +1,19 @@
+module.exports = {
+  printWidth: 100,
+  semi: false,
+  vueIndentScriptAndStyle: true,
+  singleQuote: true,
+  trailingComma: 'all',
+  proseWrap: 'never',
+  htmlWhitespaceSensitivity: 'ignore',
+  endOfLine: 'auto',
+  plugins: ['prettier-plugin-packagejson'],
+  overrides: [
+    {
+      files: '.*rc',
+      options: {
+        parser: 'json',
+      },
+    },
+  ],
+};

+ 2 - 0
.stylelintignore

@@ -0,0 +1,2 @@
+dist
+public

+ 4 - 0
.stylelintrc.cjs

@@ -0,0 +1,4 @@
+module.exports = {
+  root: true,
+  extends: ['@vben/stylelint-config'],
+};

+ 13 - 0
.vscode/extensions.json

@@ -0,0 +1,13 @@
+{
+  "recommendations": [
+    "vue.volar",
+    "dbaeumer.vscode-eslint",
+    "stylelint.vscode-stylelint",
+    "esbenp.prettier-vscode",
+    "mrmlnc.vscode-less",
+    "lokalise.i18n-ally",
+    "antfu.iconify",
+    "antfu.unocss",
+    "mikestead.dotenv"
+  ]
+}

+ 13 - 0
.vscode/launch.json

@@ -0,0 +1,13 @@
+{
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "type": "chrome",
+      "request": "launch",
+      "name": "Launch Chrome",
+      "url": "http://localhost:5173",
+      "webRoot": "${workspaceFolder}/src",
+      "sourceMaps": true
+    }
+  ]
+}

+ 173 - 0
.vscode/settings.json

@@ -0,0 +1,173 @@
+{
+  "typescript.tsdk": "./node_modules/typescript/lib",
+  "volar.tsPlugin": true,
+  "volar.tsPluginStatus": false,
+  "npm.packageManager": "pnpm",
+  "editor.tabSize": 2,
+  "editor.defaultFormatter": "esbenp.prettier-vscode",
+  "files.eol": "\n",
+  "search.exclude": {
+    "**/node_modules": true,
+    "**/*.log": true,
+    "**/*.log*": true,
+    "**/bower_components": true,
+    "**/dist": true,
+    "**/elehukouben": true,
+    "**/.git": true,
+    "**/.gitignore": true,
+    "**/.svn": true,
+    "**/.DS_Store": true,
+    "**/.idea": true,
+    "**/.vscode": false,
+    "**/yarn.lock": true,
+    "**/tmp": true,
+    "out": true,
+    "dist": true,
+    "node_modules": true,
+    "CHANGELOG.md": true,
+    "examples": true,
+    "res": true,
+    "screenshots": true,
+    "yarn-error.log": true,
+    "**/.yarn": true
+  },
+  "files.exclude": {
+    "**/.cache": true,
+    "**/.editorconfig": true,
+    "**/.eslintcache": true,
+    "**/bower_components": true,
+    "**/.idea": true,
+    "**/tmp": true,
+    "**/.git": true,
+    "**/.svn": true,
+    "**/.hg": true,
+    "**/CVS": true,
+    "**/.DS_Store": true
+  },
+  "files.watcherExclude": {
+    "**/.git/objects/**": true,
+    "**/.git/subtree-cache/**": true,
+    "**/.vscode/**": true,
+    "**/node_modules/**": true,
+    "**/tmp/**": true,
+    "**/bower_components/**": true,
+    "**/dist/**": true,
+    "**/yarn.lock": true
+  },
+  "stylelint.enable": true,
+  "stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass"],
+  "path-intellisense.mappings": {
+    "/@/": "${workspaceRoot}/src"
+  },
+  "[javascriptreact]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[typescript]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[typescriptreact]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[html]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[css]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[less]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[scss]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[markdown]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "editor.codeActionsOnSave": {
+    "source.fixAll.eslint": "explicit",
+    "source.fixAll.stylelint": "explicit"
+  },
+  "[vue]": {
+    "editor.codeActionsOnSave": {
+      "source.fixAll.eslint": "explicit",
+      "source.fixAll.stylelint": "explicit"
+    },
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "i18n-ally.localesPaths": ["src/locales/lang"],
+  "i18n-ally.keystyle": "nested",
+  "i18n-ally.sortKeys": true,
+  "i18n-ally.namespace": true,
+  "i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
+  "i18n-ally.enabledParsers": ["json", "ts", "js"],
+  "i18n-ally.sourceLanguage": "en",
+  "i18n-ally.displayLanguage": "zh-CN",
+  "i18n-ally.enabledFrameworks": ["vue", "react"],
+  "cSpell.words": [
+    "antd",
+    "antv",
+    "brotli",
+    "browserslist",
+    "codemirror",
+    "commitlint",
+    "cropperjs",
+    "echarts",
+    "esnext",
+    "esno",
+    "iconify",
+    "INTLIFY",
+    "lintstagedrc",
+    "logicflow",
+    "mockjs",
+    "nprogress",
+    "pinia",
+    "pnpm",
+    "qrcode",
+    "sider",
+    "sortablejs",
+    "stylelint",
+    "tailwindcss",
+    "tinymce",
+    "unocss",
+    "unref",
+    "vben",
+    "vditor",
+    "Vite",
+    "vitejs",
+    "vueuse",
+    "zxcvbn"
+  ],
+  "vetur.format.scriptInitialIndent": true,
+  "vetur.format.styleInitialIndent": true,
+  "vetur.validation.script": false,
+  "MicroPython.executeButton": [
+    {
+      "text": "▶",
+      "tooltip": "运行",
+      "alignment": "left",
+      "command": "extension.executeFile",
+      "priority": 3.5
+    }
+  ],
+  "MicroPython.syncButton": [
+    {
+      "text": "$(sync)",
+      "tooltip": "同步",
+      "alignment": "left",
+      "command": "extension.execute",
+      "priority": 4
+    }
+  ],
+  "explorer.fileNesting.enabled": true,
+  "explorer.fileNesting.expand": false,
+  "explorer.fileNesting.patterns": {
+    "*.ts": "$(capture).test.ts, $(capture).test.tsx",
+    "*.tsx": "$(capture).test.ts, $(capture).test.tsx",
+    "*.env": "$(capture).env.*",
+    "CHANGELOG.md": "CHANGELOG*",
+    "package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,README*,.npmrc,.browserslistrc",
+    ".eslintrc.cjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,.stylelintrc.*"
+  },
+  "terminal.integrated.scrollback": 10000,
+  "nuxt.isNuxtApp": false
+}

文件差異過大導致無法顯示
+ 422 - 0
CHANGELOG.md


+ 8 - 0
Dockerfile

@@ -0,0 +1,8 @@
+FROM nginx:1.25.2-alpine
+
+COPY dist/ /usr/share/nginx/html/
+COPY deploy/default.conf /etc/nginx/conf.d/
+
+LABEL MAINTAINER="yuansu.china.work@gmail.com"
+
+EXPOSE 80

+ 9 - 0
LICENSE

@@ -0,0 +1,9 @@
+MIT License
+
+Copyright (c) 2022-present, Ryan Su
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 25 - 0
Makefile

@@ -0,0 +1,25 @@
+VERSION=$(shell git describe --tags --always)
+
+.PHONY: docker
+docker: # Compile and build the docker | 编译并构建 docker 镜像
+	pnpm install
+	pnpm build
+	docker build -f Dockerfile -t ${DOCKER_USERNAME}/backend-ui:${VERSION} .
+
+.PHONY: docker-not-build
+docker-not-build: # Build the docker without compiling | 不编译直接构建镜像
+	docker build -f Dockerfile -t ${DOCKER_USERNAME}/backend-ui:${VERSION} .
+
+.PHONY: publish-docker
+publish-docker: # Publish the docker | 发布镜像
+	echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin https://${REPO}
+	docker push ${DOCKER_USERNAME}/backend-ui:${VERSION}
+
+.PHONY: run-docker
+run-docker: # Run the docker image | 运行 docker 镜像
+	docker volume create backendui
+	docker run -d --name ${DOCKER_USERNAME}/backend-ui:${VERSION} -p 80:80 -v backendui:/etc/nginx --network docker-compose_simple-admin ${DOCKER_USERNAME}/backendui:${VERSION}
+
+.PHONY: help
+help: # Show help | 显示帮助
+	@grep -E '^[a-zA-Z0-9 -]+:.*#'  Makefile | sort | while read -r l; do printf "\033[1;32m$$(echo $$l | cut -f 1 -d':')\033[00m:$$(echo $$l | cut -f 2- -d'#')\n"; done

+ 3 - 0
README.En.md

@@ -0,0 +1,3 @@
+<div align="center">
+<h1>Gooki wechat admin</h1>
+</div>

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+<div align="center">
+<h1>冠客微信管理后台</h1>
+</div>

+ 0 - 0
apps/portal-view/.gitkeep


+ 15 - 0
apps/test-server/README.md

@@ -0,0 +1,15 @@
+# Test Server
+
+It is used to start the test interface service, which can test the upload, websocket, login and other interfaces.
+
+## Usage
+
+```bash
+
+cd ./test/server
+
+pnpm install
+
+pnpm run start
+
+```

+ 18 - 0
apps/test-server/controller/FileController.ts

@@ -0,0 +1,18 @@
+import FileService from '../service/FileService';
+
+class FileController {
+  private service: FileService = new FileService();
+
+  upload = async (ctx) => {
+    const files = ctx.request.files.file;
+    console.log(files);
+
+    if (files.length === undefined) {
+      this.service.upload(ctx, files, false);
+    } else {
+      this.service.upload(ctx, files, true);
+    }
+  };
+}
+
+export default new FileController();

+ 15 - 0
apps/test-server/controller/UserController.ts

@@ -0,0 +1,15 @@
+import UserService from '../service/UserService';
+
+class UserController {
+  private service: UserService = new UserService();
+
+  login = async (ctx) => {
+    ctx.body = await this.service.login();
+  };
+
+  getUserInfoById = async (ctx) => {
+    ctx.body = await this.service.getUserInfoById();
+  };
+}
+
+export default new UserController();

+ 18 - 0
apps/test-server/ecosystem.config.cjs

@@ -0,0 +1,18 @@
+const { name } = require('./package.json');
+const path = require('path');
+
+module.exports = {
+  apps: [
+    {
+      name,
+      script: path.resolve(__dirname, './dist/index.js'),
+      instances: require('os').cpus().length,
+      autorestart: true,
+      watch: true,
+      env_production: {
+        NODE_ENV: 'production',
+        PORT: 8080,
+      },
+    },
+  ],
+};

+ 63 - 0
apps/test-server/index.ts

@@ -0,0 +1,63 @@
+import Koa from 'koa';
+import path from 'path';
+import Router from 'koa-router';
+import body from 'koa-body';
+import cors from 'koa2-cors';
+import koaStatic from 'koa-static';
+import websockify from 'koa-websocket';
+import route from 'koa-route';
+
+import AppRoutes from './routes';
+
+const PORT = 3300;
+
+const app = websockify(new Koa());
+
+app.ws.use(function (ctx, next) {
+  ctx.websocket.send('connection succeeded!');
+  return next(ctx);
+});
+
+app.ws.use(
+  route.all('/test', function (ctx) {
+    // ctx.websocket.send('Hello World');
+    ctx.websocket.on('message', function (message) {
+      // do something with the message from client
+
+      if (message !== 'ping') {
+        const data = JSON.stringify({
+          id: Math.ceil(Math.random() * 1000),
+          time: new Date().getTime(),
+          res: `${message}`,
+        });
+        ctx.websocket.send(data);
+      }
+      console.log(message);
+    });
+  }),
+);
+
+const router = new Router();
+
+// router
+AppRoutes.forEach((route) => router[route.method](route.path, route.action));
+
+app.use(cors());
+app.use(
+  body({
+    encoding: 'gzip',
+    multipart: true,
+    formidable: {
+      // uploadDir: path.join(__dirname, '/upload/'), // 设置文件上传目录
+      keepExtensions: true,
+      maxFieldsSize: 20 * 1024 * 1024,
+    },
+  }),
+);
+app.use(router.routes());
+app.use(router.allowedMethods());
+app.use(koaStatic(path.join(__dirname)));
+
+app.listen(PORT, () => {
+  console.log(`Application started successfully: http://localhost:${PORT}`);
+});

+ 8 - 0
apps/test-server/nodemon.json

@@ -0,0 +1,8 @@
+{
+  "watch": ["src"],
+  "ext": "ts",
+  "exec": "ts-node -r tsconfig-paths/register index.ts",
+  "events": {
+    "restart": "clear"
+  }
+}

+ 36 - 0
apps/test-server/package.json

@@ -0,0 +1,36 @@
+{
+  "name": "server",
+  "version": "1.0.0",
+  "license": "MIT",
+  "scripts": {
+    "compile": "rimraf ./dist && tsup ./index.ts --dts --format cjs,esm  ",
+    "prod": "npx pm2 start ecosystem.config.cjs --env production",
+    "restart": "pm2 restart ecosystem.config.cjs --env production",
+    "start": "nodemon",
+    "stop": "npx pm2 stop ecosystem.config.cjs"
+  },
+  "dependencies": {
+    "fs-extra": "^11.1.1",
+    "koa": "^2.14.2",
+    "koa-body": "^6.0.1",
+    "koa-bodyparser": "^4.4.1",
+    "koa-route": "^3.2.0",
+    "koa-router": "^12.0.0",
+    "koa-static": "^5.0.0",
+    "koa-websocket": "^7.0.0",
+    "koa2-cors": "^2.0.6"
+  },
+  "devDependencies": {
+    "@types/koa": "^2.13.6",
+    "@types/koa-bodyparser": "^5.0.2",
+    "@types/koa-router": "^7.4.4",
+    "@types/node": "^20.4.0",
+    "nodemon": "^2.0.22",
+    "pm2": "^5.3.0",
+    "rimraf": "^5.0.1",
+    "ts-node": "^10.9.1",
+    "tsconfig-paths": "^4.2.0",
+    "tsup": "^7.1.0",
+    "typescript": "^5.1.6"
+  }
+}

+ 23 - 0
apps/test-server/routes.ts

@@ -0,0 +1,23 @@
+import UserController from './controller/UserController';
+import FileController from './controller/FileController';
+
+export default [
+  // user
+  {
+    path: '/login',
+    method: 'post',
+    action: UserController.login,
+  },
+  {
+    path: '/getUserInfoById',
+    method: 'get',
+    action: UserController.getUserInfoById,
+  },
+
+  // file
+  {
+    path: '/upload',
+    method: 'post',
+    action: FileController.upload,
+  },
+];

+ 54 - 0
apps/test-server/service/FileService.ts

@@ -0,0 +1,54 @@
+import path from 'path';
+import fs from 'fs-extra';
+
+const uploadUrl = 'http://localhost:3300/static/upload';
+const filePath = path.join(__dirname, '../static/upload/');
+
+fs.ensureDir(filePath);
+export default class UserService {
+  async upload(ctx, files, isMultiple) {
+    let fileReader, fileResource, writeStream;
+
+    const fileFunc = function (file) {
+      fileReader = fs.createReadStream(file.path);
+      fileResource = filePath + `/${file.name}`;
+      console.log(fileResource);
+
+      writeStream = fs.createWriteStream(fileResource);
+      fileReader.pipe(writeStream);
+    };
+
+    const returnFunc = function (flag) {
+      if (flag) {
+        let url = '';
+        for (let i = 0; i < files.length; i++) {
+          url += uploadUrl + `/${files[i].name},`;
+        }
+        url = url.replace(/,$/gi, '');
+        ctx.body = {
+          url: url,
+          code: 0,
+          message: 'upload Success!',
+        };
+      } else {
+        ctx.body = {
+          url: uploadUrl + `/${files.name}`,
+          code: 0,
+          message: 'upload Success!',
+        };
+      }
+    };
+    console.log(isMultiple, files.length);
+
+    if (isMultiple) {
+      for (let i = 0; i < files.length; i++) {
+        const f1 = files[i];
+        fileFunc(f1);
+      }
+    } else {
+      fileFunc(files);
+    }
+    fs.ensureDir(filePath);
+    returnFunc(isMultiple);
+  }
+}

+ 25 - 0
apps/test-server/service/UserService.ts

@@ -0,0 +1,25 @@
+import { Result } from '../utils';
+
+const fakeUserInfo = {
+  userId: '1',
+  username: 'vben',
+  realName: 'Vben Admin',
+  desc: 'manager',
+  password: '123456',
+  token: 'fakeToken1',
+  roles: [
+    {
+      roleName: 'Super Admin',
+      value: 'super',
+    },
+  ],
+};
+export default class UserService {
+  async login() {
+    return Result.success(fakeUserInfo);
+  }
+
+  async getUserInfoById() {
+    return Result.success(fakeUserInfo);
+  }
+}

+ 7 - 0
apps/test-server/tsconfig.json

@@ -0,0 +1,7 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "extends": "@vben/ts-config/node-server.json",
+  "compilerOptions": {
+    "noImplicitAny": false
+  }
+}

+ 9 - 0
apps/test-server/utils.ts

@@ -0,0 +1,9 @@
+export class Result {
+  static success(data: any) {
+    return {
+      code: 0,
+      success: true,
+      result: data,
+    };
+  }
+}

+ 38 - 0
deploy/default.conf

@@ -0,0 +1,38 @@
+server {
+    listen       80;
+    listen  [::]:80;
+    server_name  localhost;
+
+    location / {
+        root   /usr/share/nginx/html;
+        index  index.html index.htm;
+        try_files $uri $uri/ /index.html;
+    }
+
+    location /sys-api/ {
+        proxy_set_header Host $http_host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header REMOTE-HOST $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_pass  http://core-api-svc.default.svc.cluster.local:9100/;
+        # proxy_pass  http://core-api:9100/; # for docker
+    }
+    
+    # location /fms-api/ {
+    #     proxy_set_header Host $http_host;
+    #     proxy_set_header X-Real-IP $remote_addr;
+    #     proxy_set_header REMOTE-HOST $remote_addr;
+    #     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    #     proxy_pass  http://fms-api-svc.default.svc.cluster.local:9102/;
+    #     # proxy_pass  http://fms-api:9102/; # for docker
+    # }
+
+    # location /mms-api/ {
+    #     proxy_set_header Host $http_host;
+    #     proxy_set_header X-Real-IP $remote_addr;
+    #     proxy_set_header REMOTE-HOST $remote_addr;
+    #     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    #     proxy_pass  http://mms-api-svc.default.svc.cluster.local:9104/;
+    #     # proxy_pass  http://mms-api:9103/; # for docker
+    # }
+}

+ 146 - 0
index.html

@@ -0,0 +1,146 @@
+<!doctype html>
+<html lang="en" id="htmlRoot">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+    <meta name="renderer" content="webkit" />
+    <meta
+      name="viewport"
+      content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
+    />
+    <title><%= VITE_GLOB_APP_TITLE %></title>
+    <link rel="icon" href="/favicon.ico" />
+  </head>
+  <body>
+    <div id="app">
+      <style>
+        html {
+          /* same as ant-design-vue/dist/reset.css setting, avoid the title line-height changed */
+          line-height: 1.15;
+        }
+
+        html[data-theme='dark'] .app-loading {
+          background-color: #2c344a;
+        }
+
+        html[data-theme='dark'] .app-loading .app-loading-title {
+          color: rgb(255 255 255 / 85%);
+        }
+
+        .app-loading {
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          justify-content: center;
+          width: 100%;
+          height: 100%;
+          background-color: #f4f7f9;
+        }
+
+        .app-loading .app-loading-wrap {
+          display: flex;
+          position: absolute;
+          top: 50%;
+          left: 50%;
+          flex-direction: column;
+          align-items: center;
+          justify-content: center;
+          transform: translate3d(-50%, -50%, 0);
+        }
+
+        .app-loading .dots {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          padding: 98px;
+        }
+
+        .app-loading .app-loading-title {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          margin-top: 30px;
+          color: rgb(0 0 0 / 85%);
+          font-size: 30px;
+        }
+
+        .app-loading .app-loading-logo {
+          display: block;
+          width: 90px;
+          margin: 0 auto;
+          margin-bottom: 20px;
+        }
+
+        .dot {
+          display: inline-block;
+          position: relative;
+          box-sizing: border-box;
+          width: 48px;
+          height: 48px;
+          margin-top: 30px;
+          transform: rotate(45deg);
+          animation: ant-rotate 1.2s infinite linear;
+          font-size: 32px;
+        }
+
+        .dot i {
+          display: block;
+          position: absolute;
+          width: 20px;
+          height: 20px;
+          transform: scale(0.75);
+          transform-origin: 50% 50%;
+          animation: ant-spin-move 1s infinite linear alternate;
+          border-radius: 100%;
+          opacity: 0.3;
+          background-color: #0065cc;
+        }
+
+        .dot i:nth-child(1) {
+          top: 0;
+          left: 0;
+        }
+
+        .dot i:nth-child(2) {
+          top: 0;
+          right: 0;
+          animation-delay: 0.4s;
+        }
+
+        .dot i:nth-child(3) {
+          right: 0;
+          bottom: 0;
+          animation-delay: 0.8s;
+        }
+
+        .dot i:nth-child(4) {
+          bottom: 0;
+          left: 0;
+          animation-delay: 1.2s;
+        }
+
+        @keyframes ant-rotate {
+          to {
+            transform: rotate(405deg);
+          }
+        }
+
+        @keyframes ant-spin-move {
+          to {
+            opacity: 1;
+          }
+        }
+      </style>
+      <div class="app-loading">
+        <div class="app-loading-wrap">
+          <img src="/resource/img/logo.png" class="app-loading-logo" alt="Logo" />
+          <div class="app-loading-dots">
+            <span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
+          </div>
+          <div class="app-loading-title"><%= VITE_GLOB_APP_TITLE %></div>
+        </div>
+      </div>
+    </div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 9 - 0
internal/eslint-config/.eslintignore

@@ -0,0 +1,9 @@
+
+*.sh
+node_modules
+*.md
+*.woff
+*.ttf
+.turbo
+dist
+package.json

+ 4 - 0
internal/eslint-config/.eslintrc.cjs

@@ -0,0 +1,4 @@
+module.exports = {
+  root: true,
+  extends: ['@vben/eslint-config/strict'],
+};

+ 10 - 0
internal/eslint-config/build.config.ts

@@ -0,0 +1,10 @@
+import { defineBuildConfig } from 'unbuild';
+
+export default defineBuildConfig({
+  clean: true,
+  entries: ['src/index', 'src/strict'],
+  declaration: true,
+  rollup: {
+    emitCJS: true,
+  },
+});

+ 50 - 0
internal/eslint-config/package.json

@@ -0,0 +1,50 @@
+{
+  "name": "@vben/eslint-config",
+  "version": "1.0.0",
+  "private": true,
+  "homepage": "https://github.com/vbenjs/vue-vben-admin",
+  "bugs": {
+    "url": "https://github.com/vbenjs/vue-vben-admin/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+    "directory": "internal/eslint-config"
+  },
+  "license": "MIT",
+  "type": "module",
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.ts",
+      "import": "./dist/index.mjs",
+      "require": "./dist/index.cjs"
+    },
+    "./strict": {
+      "types": "./dist/strict.d.ts",
+      "import": "./dist/strict.mjs",
+      "require": "./dist/strict.cjs"
+    }
+  },
+  "main": "./dist/index.cjs",
+  "module": "./dist/index.mjs",
+  "types": "./dist/index.d.ts",
+  "files": [
+    "dist"
+  ],
+  "scripts": {
+    "clean": "pnpm rimraf .turbo node_modules dist",
+    "lint": "pnpm eslint .",
+    "stub": "pnpm unbuild --stub"
+  },
+  "devDependencies": {
+    "@typescript-eslint/eslint-plugin": "^7.0.2",
+    "@typescript-eslint/parser": "^7.0.2",
+    "eslint": "^8.56.0",
+    "eslint-config-prettier": "^9.1.0",
+    "eslint-plugin-import": "^2.29.1",
+    "eslint-plugin-prettier": "^5.1.3",
+    "eslint-plugin-simple-import-sort": "^12.0.0",
+    "eslint-plugin-vue": "^9.21.1",
+    "vue-eslint-parser": "^9.4.2"
+  }
+}

+ 91 - 0
internal/eslint-config/src/index.ts

@@ -0,0 +1,91 @@
+export default {
+  env: {
+    browser: true,
+    node: true,
+    es6: true,
+  },
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    parser: '@typescript-eslint/parser',
+    ecmaVersion: 2020,
+    sourceType: 'module',
+    jsxPragma: 'React',
+    ecmaFeatures: {
+      jsx: true,
+    },
+    project: './tsconfig.*?.json',
+    createDefaultProgram: false,
+    extraFileExtensions: ['.vue'],
+  },
+  plugins: ['vue', '@typescript-eslint', 'import'],
+  extends: [
+    'eslint:recommended',
+    'plugin:vue/vue3-recommended',
+    'plugin:@typescript-eslint/recommended',
+    'plugin:prettier/recommended',
+  ],
+  rules: {
+    'no-unused-vars': 'off',
+    'no-case-declarations': 'off',
+    'no-use-before-define': 'off',
+    'space-before-function-paren': 'off',
+
+    'import/first': 'error',
+    'import/newline-after-import': 'error',
+    'import/no-duplicates': 'error',
+
+    '@typescript-eslint/no-unused-vars': [
+      'error',
+      {
+        argsIgnorePattern: '^_',
+        varsIgnorePattern: '^_',
+      },
+    ],
+    '@typescript-eslint/ban-ts-ignore': 'off',
+    '@typescript-eslint/ban-ts-comment': 'off',
+    '@typescript-eslint/ban-types': 'off',
+    '@typescript-eslint/explicit-function-return-type': 'off',
+    '@typescript-eslint/no-explicit-any': 'off',
+    '@typescript-eslint/no-var-requires': 'off',
+    '@typescript-eslint/no-empty-function': 'off',
+    '@typescript-eslint/no-use-before-define': 'off',
+    '@typescript-eslint/no-non-null-assertion': 'off',
+    '@typescript-eslint/explicit-module-boundary-types': 'off',
+    'vue/script-setup-uses-vars': 'error',
+    'vue/no-reserved-component-names': 'off',
+    'vue/custom-event-name-casing': 'off',
+    'vue/attributes-order': 'off',
+    'vue/one-component-per-file': 'off',
+    'vue/html-closing-bracket-newline': 'off',
+    'vue/max-attributes-per-line': 'off',
+    'vue/multiline-html-element-content-newline': 'off',
+    'vue/singleline-html-element-content-newline': 'off',
+    'vue/attribute-hyphenation': 'off',
+    'vue/require-default-prop': 'off',
+    'vue/require-explicit-emits': 'off',
+    'vue/html-self-closing': [
+      'error',
+      {
+        html: {
+          void: 'always',
+          normal: 'never',
+          component: 'always',
+        },
+        svg: 'always',
+        math: 'always',
+      },
+    ],
+    'vue/multi-word-component-names': 'off',
+    // 'sort-imports': [
+    //   'error',
+    //   {
+    //     ignoreCase: true,
+    //     ignoreDeclarationSort: false,
+    //     ignoreMemberSort: false,
+    //     memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
+    //     allowSeparatedGroups: false,
+    //   },
+    // ],
+  },
+  globals: { defineOptions: 'readonly' },
+};

+ 57 - 0
internal/eslint-config/src/strict.ts

@@ -0,0 +1,57 @@
+export default {
+  extends: ['@vben'],
+  plugins: ['simple-import-sort'],
+  rules: {
+    'simple-import-sort/imports': 'error',
+    'simple-import-sort/exports': 'error',
+
+    '@typescript-eslint/ban-ts-comment': [
+      'error',
+      {
+        'ts-expect-error': 'allow-with-description',
+        'ts-ignore': 'allow-with-description',
+        'ts-nocheck': 'allow-with-description',
+        'ts-check': false,
+      },
+    ],
+
+    /**
+     * 【强制】关键字前后有一个空格
+     * @link https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/keyword-spacing.md
+     */
+    'keyword-spacing': 'off',
+    '@typescript-eslint/keyword-spacing': [
+      'error',
+      {
+        before: true,
+        after: true,
+        overrides: {
+          return: { after: true },
+          throw: { after: true },
+          case: { after: true },
+        },
+      },
+    ],
+
+    /**
+     * 禁止出现空函数,普通函数(非 async/await/generator)、箭头函数、类上的方法除外
+     * @link https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-empty-function.md
+     */
+    'no-empty-function': 'off',
+    '@typescript-eslint/no-empty-function': [
+      'error',
+      {
+        allow: ['arrowFunctions', 'functions', 'methods'],
+      },
+    ],
+
+    /**
+     * 优先使用 interface 而不是 type 定义对象类型
+     * @link https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-definitions.md
+     */
+    '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
+
+    'vue/attributes-order': 'error',
+    'vue/require-default-prop': 'error',
+  },
+};

+ 5 - 0
internal/eslint-config/tsconfig.json

@@ -0,0 +1,5 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "extends": "@vben/ts-config/node.json",
+  "include": ["src"]
+}

+ 9 - 0
internal/stylelint-config/.eslintignore

@@ -0,0 +1,9 @@
+
+*.sh
+node_modules
+*.md
+*.woff
+*.ttf
+.turbo
+dist
+package.json

+ 4 - 0
internal/stylelint-config/.eslintrc.cjs

@@ -0,0 +1,4 @@
+module.exports = {
+  root: true,
+  extends: ['@vben/eslint-config/strict'],
+};

+ 10 - 0
internal/stylelint-config/build.config.ts

@@ -0,0 +1,10 @@
+import { defineBuildConfig } from 'unbuild';
+
+export default defineBuildConfig({
+  clean: true,
+  entries: ['src/index'],
+  declaration: true,
+  rollup: {
+    emitCJS: true,
+  },
+});

+ 49 - 0
internal/stylelint-config/package.json

@@ -0,0 +1,49 @@
+{
+  "name": "@vben/stylelint-config",
+  "version": "1.0.0",
+  "private": true,
+  "homepage": "https://github.com/vbenjs/vue-vben-admin",
+  "bugs": {
+    "url": "https://github.com/vbenjs/vue-vben-admin/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+    "directory": "internal/stylelint-config"
+  },
+  "license": "MIT",
+  "type": "module",
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.ts",
+      "import": "./dist/index.mjs",
+      "require": "./dist/index.cjs"
+    }
+  },
+  "main": "./dist/index.cjs",
+  "module": "./dist/index.mjs",
+  "types": "./dist/index.d.ts",
+  "files": [
+    "dist"
+  ],
+  "scripts": {
+    "clean": "pnpm rimraf .turbo node_modules dist",
+    "lint": "pnpm eslint .",
+    "stub": "pnpm unbuild --stub"
+  },
+  "devDependencies": {
+    "postcss": "^8.4.35",
+    "postcss-html": "^1.6.0",
+    "postcss-less": "^6.0.0",
+    "postcss-scss": "^4.0.9",
+    "prettier": "^3.2.5",
+    "stylelint": "^16.2.1",
+    "stylelint-config-property-sort-order-smacss": "^10.0.0",
+    "stylelint-config-recommended-scss": "^14.0.0",
+    "stylelint-config-recommended-vue": "^1.5.0",
+    "stylelint-config-standard": "^36.0.0",
+    "stylelint-config-standard-scss": "^13.0.0",
+    "stylelint-order": "^6.0.4",
+    "stylelint-prettier": "^5.0.0"
+  }
+}

+ 92 - 0
internal/stylelint-config/src/index.ts

@@ -0,0 +1,92 @@
+export default {
+  extends: ['stylelint-config-standard', 'stylelint-config-property-sort-order-smacss'],
+  plugins: ['stylelint-order', 'stylelint-prettier'],
+  // customSyntax: 'postcss-html',
+  overrides: [
+    {
+      files: ['**/*.(css|html|vue)'],
+      customSyntax: 'postcss-html',
+    },
+    {
+      files: ['*.less', '**/*.less'],
+      customSyntax: 'postcss-less',
+      extends: ['stylelint-config-standard', 'stylelint-config-recommended-vue'],
+    },
+    {
+      files: ['*.scss', '**/*.scss'],
+      customSyntax: 'postcss-scss',
+      extends: ['stylelint-config-standard-scss', 'stylelint-config-recommended-vue/scss'],
+      rule: {
+        'scss/percent-placeholder-pattern': null,
+      },
+    },
+  ],
+  rules: {
+    'prettier/prettier': true,
+    'media-feature-range-notation': null,
+    'selector-not-notation': null,
+    'import-notation': null,
+    'function-no-unknown': null,
+    'selector-class-pattern': null,
+    'selector-pseudo-class-no-unknown': [
+      true,
+      {
+        ignorePseudoClasses: ['global', 'deep'],
+      },
+    ],
+    'selector-pseudo-element-no-unknown': [
+      true,
+      {
+        ignorePseudoElements: ['v-deep'],
+      },
+    ],
+    'at-rule-no-unknown': [
+      true,
+      {
+        ignoreAtRules: [
+          'tailwind',
+          'apply',
+          'variants',
+          'responsive',
+          'screen',
+          'function',
+          'if',
+          'each',
+          'include',
+          'mixin',
+          'extend',
+        ],
+      },
+    ],
+    'no-empty-source': null,
+    'named-grid-areas-no-invalid': null,
+    'no-descending-specificity': null,
+    'font-family-no-missing-generic-family-keyword': null,
+    'rule-empty-line-before': [
+      'always',
+      {
+        ignore: ['after-comment', 'first-nested'],
+      },
+    ],
+    'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }],
+    'order/order': [
+      [
+        'dollar-variables',
+        'custom-properties',
+        'at-rules',
+        'declarations',
+        {
+          type: 'at-rule',
+          name: 'supports',
+        },
+        {
+          type: 'at-rule',
+          name: 'media',
+        },
+        'rules',
+      ],
+      { severity: 'error' },
+    ],
+  },
+  ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'],
+};

+ 5 - 0
internal/stylelint-config/tsconfig.json

@@ -0,0 +1,5 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "extends": "@vben/ts-config/node.json",
+  "include": ["src"]
+}

+ 4 - 0
internal/ts-config/.eslintignore

@@ -0,0 +1,4 @@
+
+*.sh
+node_modules
+dist

+ 27 - 0
internal/ts-config/base.json

@@ -0,0 +1,27 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "display": "Base",
+  "compilerOptions": {
+    "target": "ESNext",
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "strict": true,
+    "declaration": true,
+    "noImplicitOverride": true,
+    "noUnusedLocals": true,
+    "esModuleInterop": true,
+    "useUnknownInCatchVariables": false,
+    "composite": false,
+    "declarationMap": false,
+    "forceConsistentCasingInFileNames": true,
+    "inlineSources": false,
+    "isolatedModules": true,
+    "skipLibCheck": true,
+    "noUnusedParameters": false,
+    "preserveWatchOutput": true,
+    "experimentalDecorators": true,
+    "resolveJsonModule": true,
+    "removeComments": true
+  },
+  "exclude": ["**/node_modules/**", "**/dist/**"]
+}

+ 18 - 0
internal/ts-config/node-server.json

@@ -0,0 +1,18 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "display": "Node Server  Config",
+  "extends": "./base.json",
+  "compilerOptions": {
+    "module": "commonjs",
+    "declaration": false,
+    "removeComments": true,
+    "emitDecoratorMetadata": true,
+    "experimentalDecorators": true,
+    "target": "es6",
+    "sourceMap": false,
+    "esModuleInterop": true,
+    "outDir": "./dist",
+    "baseUrl": "./"
+  },
+  "exclude": ["node_modules"]
+}

+ 12 - 0
internal/ts-config/node.json

@@ -0,0 +1,12 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "display": "Node Config",
+  "extends": "./base.json",
+  "compilerOptions": {
+    "lib": ["ESNext"],
+    "noImplicitAny": true,
+    "sourceMap": true,
+    "noEmit": true,
+    "baseUrl": "./"
+  }
+}

+ 26 - 0
internal/ts-config/package.json

@@ -0,0 +1,26 @@
+{
+  "name": "@vben/ts-config",
+  "version": "1.0.0",
+  "private": true,
+  "homepage": "https://github.com/vbenjs/vue-vben-admin",
+  "bugs": {
+    "url": "https://github.com/vbenjs/vue-vben-admin/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+    "directory": "internal/ts-config"
+  },
+  "license": "MIT",
+  "type": "module",
+  "files": [
+    "base.json",
+    "node.json",
+    "vue-app.json",
+    "node-server.json"
+  ],
+  "dependencies": {
+    "@types/node": "^20.11.28",
+    "vite": "^5.1.6"
+  }
+}

+ 10 - 0
internal/ts-config/vue-app.json

@@ -0,0 +1,10 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "display": "Vue Application",
+  "extends": "./base.json",
+  "compilerOptions": {
+    "jsx": "preserve",
+    "lib": ["ESNext", "DOM"],
+    "noImplicitAny": false
+  }
+}

+ 9 - 0
internal/vite-config/.eslintignore

@@ -0,0 +1,9 @@
+
+*.sh
+node_modules
+*.md
+*.woff
+*.ttf
+.turbo
+dist
+package.json

+ 4 - 0
internal/vite-config/.eslintrc.cjs

@@ -0,0 +1,4 @@
+module.exports = {
+  root: true,
+  extends: ['@vben/eslint-config/strict'],
+};

+ 10 - 0
internal/vite-config/build.config.ts

@@ -0,0 +1,10 @@
+import { defineBuildConfig } from 'unbuild';
+
+export default defineBuildConfig({
+  clean: true,
+  entries: ['src/index'],
+  declaration: true,
+  rollup: {
+    emitCJS: true,
+  },
+});

+ 59 - 0
internal/vite-config/package.json

@@ -0,0 +1,59 @@
+{
+  "name": "@vben/vite-config",
+  "version": "1.0.0",
+  "private": true,
+  "homepage": "https://github.com/vbenjs/vue-vben-admin",
+  "bugs": {
+    "url": "https://github.com/vbenjs/vue-vben-admin/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+    "directory": "internal/vite-config"
+  },
+  "license": "MIT",
+  "type": "module",
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.ts",
+      "import": "./dist/index.mjs",
+      "require": "./dist/index.cjs"
+    }
+  },
+  "main": "./dist/index.cjs",
+  "module": "./dist/index.mjs",
+  "types": "./dist/index.d.ts",
+  "files": [
+    "dist"
+  ],
+  "scripts": {
+    "clean": "pnpm rimraf .turbo node_modules dist",
+    "lint": "pnpm eslint .",
+    "stub": "pnpm unbuild --stub"
+  },
+  "dependencies": {
+    "@ant-design/colors": "^7.0.2",
+    "vite": "^5.1.6"
+  },
+  "devDependencies": {
+    "@types/fs-extra": "^11.0.4",
+    "@vitejs/plugin-vue": "^5.0.4",
+    "@vitejs/plugin-vue-jsx": "^3.1.0",
+    "ant-design-vue": "^4.1.2",
+    "dayjs": "^1.11.10",
+    "dotenv": "^16.4.5",
+    "fs-extra": "^11.2.0",
+    "less": "^4.2.0",
+    "picocolors": "^1.0.0",
+    "pkg-types": "^1.0.3",
+    "rollup-plugin-visualizer": "^5.12.0",
+    "sass": "^1.72.0",
+    "unocss": "0.58.5",
+    "vite-plugin-compression": "^0.5.1",
+    "vite-plugin-dts": "^3.7.3",
+    "vite-plugin-html": "^3.2.2",
+    "vite-plugin-mock": "^2.9.8",
+    "vite-plugin-purge-icons": "^0.10.0",
+    "vite-plugin-svg-icons": "^2.0.1"
+  }
+}

+ 119 - 0
internal/vite-config/src/config/application.ts

@@ -0,0 +1,119 @@
+import { resolve } from 'node:path';
+
+import dayjs from 'dayjs';
+import { readPackageJSON } from 'pkg-types';
+import { defineConfig, loadEnv, mergeConfig, type UserConfig } from 'vite';
+
+import { createPlugins } from '../plugins';
+import { generateModifyVars } from '../utils/modifyVars';
+import { commonConfig } from './common';
+
+interface DefineOptions {
+  overrides?: UserConfig;
+  options?: {
+    //
+  };
+}
+
+function defineApplicationConfig(defineOptions: DefineOptions = {}) {
+  const { overrides = {} } = defineOptions;
+
+  return defineConfig(async ({ command, mode }) => {
+    const root = process.cwd();
+    const isBuild = command === 'build';
+    const { VITE_PUBLIC_PATH, VITE_USE_MOCK, VITE_BUILD_COMPRESS, VITE_ENABLE_ANALYZE } = loadEnv(
+      mode,
+      root,
+    );
+
+    const defineData = await createDefineData(root);
+    const plugins = await createPlugins({
+      isBuild,
+      root,
+      enableAnalyze: VITE_ENABLE_ANALYZE === 'true',
+      enableMock: VITE_USE_MOCK === 'true',
+      compress: VITE_BUILD_COMPRESS,
+    });
+
+    const pathResolve = (pathname: string) => resolve(root, '.', pathname);
+    const timestamp = new Date().getTime();
+    const applicationConfig: UserConfig = {
+      base: VITE_PUBLIC_PATH,
+      resolve: {
+        alias: [
+          {
+            find: 'vue-i18n',
+            replacement: 'vue-i18n/dist/vue-i18n.cjs.js',
+          },
+          // @/xxxx => src/xxxx
+          {
+            find: /\/@\//,
+            replacement: pathResolve('src') + '/',
+          },
+          // /#/xxxx => types/xxxx
+          {
+            find: /\/#\//,
+            replacement: pathResolve('types') + '/',
+          },
+          // @/xxxx => src/xxxx
+          {
+            find: /@\//,
+            replacement: pathResolve('src') + '/',
+          },
+          // #/xxxx => types/xxxx
+          {
+            find: /#\//,
+            replacement: pathResolve('types') + '/',
+          },
+        ],
+      },
+      define: defineData,
+      build: {
+        target: 'es2015',
+        cssTarget: 'chrome80',
+        rollupOptions: {
+          output: {
+            // 入口文件名
+            entryFileNames: `assets/entry/[name]-[hash]-${timestamp}.js`,
+            manualChunks: {
+              vue: ['vue', 'pinia', 'vue-router'],
+              antd: ['ant-design-vue', '@ant-design/icons-vue'],
+            },
+          },
+        },
+      },
+      css: {
+        preprocessorOptions: {
+          less: {
+            modifyVars: generateModifyVars(),
+            javascriptEnabled: true,
+          },
+        },
+      },
+      plugins,
+    };
+
+    const mergedConfig = mergeConfig(commonConfig(mode), applicationConfig);
+
+    return mergeConfig(mergedConfig, overrides);
+  });
+}
+
+async function createDefineData(root: string) {
+  try {
+    const pkgJson = await readPackageJSON(root);
+    const { dependencies, devDependencies, name, version } = pkgJson;
+
+    const __APP_INFO__ = {
+      pkg: { dependencies, devDependencies, name, version },
+      lastBuildTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+    };
+    return {
+      __APP_INFO__: JSON.stringify(__APP_INFO__),
+    };
+  } catch (error) {
+    return {};
+  }
+}
+
+export { defineApplicationConfig };

+ 27 - 0
internal/vite-config/src/config/common.ts

@@ -0,0 +1,27 @@
+import { presetTypography, presetUno } from 'unocss';
+import UnoCSS from 'unocss/vite';
+import { type UserConfig } from 'vite';
+
+const commonConfig: (mode: string) => UserConfig = (mode) => ({
+  server: {
+    host: true,
+  },
+  esbuild: {
+    drop: mode === 'production' ? ['console', 'debugger'] : [],
+  },
+  build: {
+    reportCompressedSize: false,
+    chunkSizeWarningLimit: 1500,
+    rollupOptions: {
+      // TODO: Prevent memory overflow
+      maxParallelFileOps: 3,
+    },
+  },
+  plugins: [
+    UnoCSS({
+      presets: [presetUno(), presetTypography()],
+    }),
+  ],
+});
+
+export { commonConfig };

+ 42 - 0
internal/vite-config/src/config/package.ts

@@ -0,0 +1,42 @@
+import { readPackageJSON } from 'pkg-types';
+import { defineConfig, mergeConfig, type UserConfig } from 'vite';
+import dts from 'vite-plugin-dts';
+
+import { commonConfig } from './common';
+
+interface DefineOptions {
+  overrides?: UserConfig;
+  options?: {
+    //
+  };
+}
+
+function definePackageConfig(defineOptions: DefineOptions = {}) {
+  const { overrides = {} } = defineOptions;
+  const root = process.cwd();
+  return defineConfig(async ({ mode }) => {
+    const { dependencies = {}, peerDependencies = {} } = await readPackageJSON(root);
+    const packageConfig: UserConfig = {
+      build: {
+        lib: {
+          entry: 'src/index.ts',
+          formats: ['es'],
+          fileName: () => 'index.mjs',
+        },
+        rollupOptions: {
+          external: [...Object.keys(dependencies), ...Object.keys(peerDependencies)],
+        },
+      },
+      plugins: [
+        dts({
+          logLevel: 'error',
+        }),
+      ],
+    };
+    const mergedConfig = mergeConfig(commonConfig(mode), packageConfig);
+
+    return mergeConfig(mergedConfig, overrides);
+  });
+}
+
+export { definePackageConfig };

+ 2 - 0
internal/vite-config/src/index.ts

@@ -0,0 +1,2 @@
+export * from './config/application';
+export * from './config/package';

+ 104 - 0
internal/vite-config/src/plugins/appConfig.ts

@@ -0,0 +1,104 @@
+import colors from 'picocolors';
+import { readPackageJSON } from 'pkg-types';
+import { type PluginOption } from 'vite';
+
+import { getEnvConfig } from '../utils/env';
+import { createContentHash } from '../utils/hash';
+
+const GLOBAL_CONFIG_FILE_NAME = '_app.config.js';
+const PLUGIN_NAME = 'app-config';
+
+async function createAppConfigPlugin({
+  root,
+  isBuild,
+}: {
+  root: string;
+  isBuild: boolean;
+}): Promise<PluginOption> {
+  let publicPath: string;
+  let source: string;
+  if (!isBuild) {
+    return {
+      name: PLUGIN_NAME,
+    };
+  }
+  const { version = '' } = await readPackageJSON(root);
+
+  return {
+    name: PLUGIN_NAME,
+    async configResolved(_config) {
+      const appTitle = _config?.env?.VITE_GLOB_APP_TITLE ?? '';
+      // appTitle = appTitle.replace(/\s/g, '_').replace(/-/g, '_');
+      publicPath = _config.base;
+      source = await getConfigSource(appTitle);
+    },
+    async transformIndexHtml(html) {
+      publicPath = publicPath.endsWith('/') ? publicPath : `${publicPath}/`;
+
+      const appConfigSrc = `${
+        publicPath || '/'
+      }${GLOBAL_CONFIG_FILE_NAME}?v=${version}-${createContentHash(source)}`;
+
+      return {
+        html,
+        tags: [
+          {
+            tag: 'script',
+            attrs: {
+              src: appConfigSrc,
+            },
+          },
+        ],
+      };
+    },
+    async generateBundle() {
+      try {
+        this.emitFile({
+          type: 'asset',
+          fileName: GLOBAL_CONFIG_FILE_NAME,
+          source,
+        });
+
+        console.log(colors.cyan(`✨configuration file is build successfully!`));
+      } catch (error) {
+        console.log(
+          colors.red('configuration file configuration file failed to package:\n' + error),
+        );
+      }
+    },
+  };
+}
+
+/**
+ * Get the configuration file variable name
+ * @param env
+ */
+const getVariableName = (title: string) => {
+  function strToHex(str: string) {
+    const result: string[] = [];
+    for (let i = 0; i < str.length; ++i) {
+      const hex = str.charCodeAt(i).toString(16);
+      result.push(('000' + hex).slice(-4));
+    }
+    return result.join('').toUpperCase();
+  }
+  return `__PRODUCTION__${strToHex(title) || '__APP'}__CONF__`.toUpperCase().replace(/\s/g, '');
+};
+
+async function getConfigSource(appTitle: string) {
+  const config = await getEnvConfig();
+  const variableName = getVariableName(appTitle);
+  const windowVariable = `window.${variableName}`;
+  // Ensure that the variable will not be modified
+  let source = `${windowVariable}=${JSON.stringify(config)};`;
+  source += `
+    Object.freeze(${windowVariable});
+    Object.defineProperty(window, "${variableName}", {
+      configurable: false,
+      writable: false,
+    });
+  `.replace(/\s/g, '');
+  return source;
+}
+
+export { createAppConfigPlugin };

+ 38 - 0
internal/vite-config/src/plugins/compress.ts

@@ -0,0 +1,38 @@
+/**
+ * Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
+ * https://github.com/anncwb/vite-plugin-compression
+ */
+import type { PluginOption } from 'vite';
+import compressPlugin from 'vite-plugin-compression';
+
+export function configCompressPlugin({
+  compress,
+  deleteOriginFile = false,
+}: {
+  compress: string;
+  deleteOriginFile?: boolean;
+}): PluginOption[] {
+  const compressList = compress.split(',');
+
+  const plugins: PluginOption[] = [];
+
+  if (compressList.includes('gzip')) {
+    plugins.push(
+      compressPlugin({
+        ext: '.gz',
+        deleteOriginFile,
+      }),
+    );
+  }
+
+  if (compressList.includes('brotli')) {
+    plugins.push(
+      compressPlugin({
+        ext: '.br',
+        algorithm: 'brotliCompress',
+        deleteOriginFile,
+      }),
+    );
+  }
+  return plugins;
+}

+ 13 - 0
internal/vite-config/src/plugins/html.ts

@@ -0,0 +1,13 @@
+/**
+ * Plugin to minimize and use ejs template syntax in index.html.
+ * https://github.com/anncwb/vite-plugin-html
+ */
+import type { PluginOption } from 'vite';
+import { createHtmlPlugin } from 'vite-plugin-html';
+
+export function configHtmlPlugin({ isBuild }: { isBuild: boolean }) {
+  const htmlPlugin: PluginOption[] = createHtmlPlugin({
+    minify: isBuild,
+  });
+  return htmlPlugin;
+}

+ 59 - 0
internal/vite-config/src/plugins/index.ts

@@ -0,0 +1,59 @@
+import vue from '@vitejs/plugin-vue';
+import vueJsx from '@vitejs/plugin-vue-jsx';
+import { type PluginOption } from 'vite';
+import purgeIcons from 'vite-plugin-purge-icons';
+
+import { createAppConfigPlugin } from './appConfig';
+import { configCompressPlugin } from './compress';
+import { configHtmlPlugin } from './html';
+import { configMockPlugin } from './mock';
+import { configSvgIconsPlugin } from './svgSprite';
+import { configVisualizerConfig } from './visualizer';
+
+interface Options {
+  isBuild: boolean;
+  root: string;
+  compress: string;
+  enableMock?: boolean;
+  enableAnalyze?: boolean;
+}
+
+async function createPlugins({ isBuild, root, enableMock, compress, enableAnalyze }: Options) {
+  const vitePlugins: (PluginOption | PluginOption[])[] = [vue(), vueJsx()];
+
+  const appConfigPlugin = await createAppConfigPlugin({ root, isBuild });
+  vitePlugins.push(appConfigPlugin);
+
+  // vite-plugin-html
+  vitePlugins.push(configHtmlPlugin({ isBuild }));
+
+  // vite-plugin-svg-icons
+  vitePlugins.push(configSvgIconsPlugin({ isBuild }));
+
+  // vite-plugin-purge-icons
+  vitePlugins.push(purgeIcons());
+
+  // The following plugins only work in the production environment
+  if (isBuild) {
+    // rollup-plugin-gzip
+    vitePlugins.push(
+      configCompressPlugin({
+        compress,
+      }),
+    );
+  }
+
+  // rollup-plugin-visualizer
+  if (enableAnalyze) {
+    vitePlugins.push(configVisualizerConfig());
+  }
+
+  // vite-plugin-mock
+  if (enableMock) {
+    vitePlugins.push(configMockPlugin({ isBuild }));
+  }
+
+  return vitePlugins;
+}
+
+export { createPlugins };

+ 19 - 0
internal/vite-config/src/plugins/mock.ts

@@ -0,0 +1,19 @@
+/**
+ * Mock plugin for development and production.
+ * https://github.com/anncwb/vite-plugin-mock
+ */
+import { viteMockServe } from 'vite-plugin-mock';
+
+export function configMockPlugin({ isBuild }: { isBuild: boolean }) {
+  return viteMockServe({
+    ignore: /^_/,
+    mockPath: 'mock',
+    localEnabled: !isBuild,
+    prodEnabled: isBuild,
+    injectCode: `
+      import { setupProdMockServer } from '../mock/_createProductionServer';
+
+      setupProdMockServer();
+      `,
+  });
+}

+ 17 - 0
internal/vite-config/src/plugins/svgSprite.ts

@@ -0,0 +1,17 @@
+/**
+ *  Vite Plugin for fast creating SVG sprites.
+ * https://github.com/anncwb/vite-plugin-svg-icons
+ */
+
+import { resolve } from 'node:path';
+
+import type { PluginOption } from 'vite';
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
+
+export function configSvgIconsPlugin({ isBuild }: { isBuild: boolean }) {
+  const svgIconsPlugin = createSvgIconsPlugin({
+    iconDirs: [resolve(process.cwd(), 'src/assets/icons')],
+    svgoOptions: isBuild,
+  });
+  return svgIconsPlugin as PluginOption;
+}

+ 14 - 0
internal/vite-config/src/plugins/visualizer.ts

@@ -0,0 +1,14 @@
+/**
+ * Package file volume analysis
+ */
+import visualizer from 'rollup-plugin-visualizer';
+import { type PluginOption } from 'vite';
+
+export function configVisualizerConfig() {
+  return visualizer({
+    filename: './node_modules/.cache/visualizer/stats.html',
+    open: true,
+    gzipSize: true,
+    brotliSize: true,
+  }) as PluginOption;
+}

+ 49 - 0
internal/vite-config/src/utils/env.ts

@@ -0,0 +1,49 @@
+import { join } from 'node:path';
+
+import dotenv from 'dotenv';
+import { readFile } from 'fs-extra';
+
+/**
+ * 获取当前环境下生效的配置文件名
+ */
+function getConfFiles() {
+  const script = process.env.npm_lifecycle_script as string;
+  const reg = new RegExp('--mode ([a-z_\\d]+)');
+  const result = reg.exec(script);
+  if (result) {
+    const mode = result[1];
+    return ['.env', `.env.${mode}`];
+  }
+  return ['.env', '.env.production'];
+}
+
+/**
+ * Get the environment variables starting with the specified prefix
+ * @param match prefix
+ * @param confFiles ext
+ */
+export async function getEnvConfig(
+  match = 'VITE_GLOB_',
+  confFiles = getConfFiles(),
+): Promise<{
+  [key: string]: string;
+}> {
+  let envConfig = {};
+
+  for (const confFile of confFiles) {
+    try {
+      const envPath = await readFile(join(process.cwd(), confFile), { encoding: 'utf8' });
+      const env = dotenv.parse(envPath);
+      envConfig = { ...envConfig, ...env };
+    } catch (e) {
+      console.error(`Error in parsing ${confFile}`, e);
+    }
+  }
+  const reg = new RegExp(`^(${match})`);
+  Object.keys(envConfig).forEach((key) => {
+    if (!reg.test(key)) {
+      Reflect.deleteProperty(envConfig, key);
+    }
+  });
+  return envConfig;
+}

+ 16 - 0
internal/vite-config/src/utils/hash.ts

@@ -0,0 +1,16 @@
+import { createHash } from 'node:crypto';
+
+function createContentHash(content: string, hashLSize = 12) {
+  const hash = createHash('sha256').update(content);
+  return hash.digest('hex').slice(0, hashLSize);
+}
+function strToHex(str: string) {
+  const result: string[] = [];
+  for (let i = 0; i < str.length; ++i) {
+    const hex = str.charCodeAt(i).toString(16);
+    result.push(('000' + hex).slice(-4));
+  }
+  return result.join('').toUpperCase();
+}
+
+export { createContentHash, strToHex };

+ 49 - 0
internal/vite-config/src/utils/modifyVars.ts

@@ -0,0 +1,49 @@
+import { resolve } from 'node:path';
+
+import { generate } from '@ant-design/colors';
+// @ts-ignore: typo
+/* import { getThemeVariables } from 'ant-design-vue/dist/theme'; */
+import { theme } from 'ant-design-vue/lib';
+import convertLegacyToken from 'ant-design-vue/lib/theme/convertLegacyToken';
+
+const { defaultAlgorithm, defaultSeed } = theme;
+
+const primaryColor = '#1677ff';
+
+function generateAntColors(color: string, theme: 'default' | 'dark' = 'default') {
+  return generate(color, {
+    theme,
+  });
+}
+
+/**
+ * less global variable
+ */
+export function generateModifyVars() {
+  const palettes = generateAntColors(primaryColor);
+  const primary = palettes[5];
+  const primaryColorObj: Record<string, string> = {};
+
+  for (let index = 0; index < 10; index++) {
+    primaryColorObj[`primary-${index + 1}`] = palettes[index];
+  }
+  // const modifyVars = getThemeVariables();
+  const mapToken = defaultAlgorithm(defaultSeed);
+  const v3Token = convertLegacyToken(mapToken);
+  return {
+    ...v3Token,
+    // reference:  Avoid repeated references
+    hack: `true; @import (reference) "${resolve('src/design/config.less')}";`,
+    'primary-color': primary,
+    ...primaryColorObj,
+    'info-color': primary,
+    'processing-color': primary,
+    'success-color': '#55D187', //  Success color
+    'error-color': '#ED6F6F', //  False color
+    'warning-color': '#EFBD47', //   Warning color
+    'font-size-base': '14px', //  Main font size
+    'border-radius-base': '2px', //  Component/float fillet
+    'link-color': primary, //   Link color
+    'app-content-background': '#fafafa', //   Link color
+  };
+}

+ 5 - 0
internal/vite-config/tsconfig.json

@@ -0,0 +1,5 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "extends": "@vben/ts-config/node.json",
+  "include": ["src"]
+}

+ 20 - 0
mock/_createProductionServer.ts

@@ -0,0 +1,20 @@
+import { createProdMockServer } from 'vite-plugin-mock/dist/client';
+
+const modules: Record<string, any> = import.meta.glob('./**/*.ts', {
+  import: 'default',
+  eager: true,
+});
+
+const mockModules = Object.keys(modules).reduce((pre, key) => {
+  if (!key.includes('/_')) {
+    pre.push(...modules[key]);
+  }
+  return pre;
+}, [] as any[]);
+
+/**
+ * Used in a production environment. Need to manually import all modules
+ */
+export function setupProdMockServer() {
+  createProdMockServer(mockModules);
+}

+ 57 - 0
mock/_util.ts

@@ -0,0 +1,57 @@
+// Interface data format used to return a unified format
+import { ResultEnum } from '@/enums/httpEnum';
+
+export function resultSuccess<T = Recordable>(data: T, { msg = 'ok' } = {}) {
+  return {
+    code: ResultEnum.SUCCESS,
+    data,
+    msg,
+  };
+}
+
+export function resultPageSuccess<T = any>(
+  page: number,
+  pageSize: number,
+  list: T[],
+  { message = 'ok' } = {},
+) {
+  const pageData = pagination(page, pageSize, list);
+
+  return {
+    ...resultSuccess({
+      data: pageData,
+      total: list.length,
+    }),
+    message,
+  };
+}
+
+export function resultError(msg = 'Request failed', { code = ResultEnum.ERROR, data = null } = {}) {
+  return {
+    code,
+    data,
+    msg,
+  };
+}
+
+export function pagination<T = any>(pageNo: number, pageSize: number, array: T[]): T[] {
+  const offset = (pageNo - 1) * Number(pageSize);
+  return offset + Number(pageSize) >= array.length
+    ? array.slice(offset, array.length)
+    : array.slice(offset, offset + Number(pageSize));
+}
+
+export interface requestParams {
+  method: string;
+  body: any;
+  headers?: { authorization?: string };
+  query: any;
+}
+
+/**
+ * @description 本函数用于从request数据中获取token,请根据项目的实际情况修改
+ *
+ */
+export function getRequestToken({ headers }: requestParams): string | undefined {
+  return headers?.authorization;
+}

+ 42 - 0
mock/sys/user.ts

@@ -0,0 +1,42 @@
+import { MockMethod } from 'vite-plugin-mock';
+import { resultSuccess } from '../_util';
+
+const fakeCodeList: any = {
+  '1': ['1000', '3000', '5000'],
+
+  '2': ['2000', '4000', '6000'],
+};
+export default [
+  {
+    url: '/sys-api/user/perm',
+    timeout: 200,
+    method: 'get',
+    response: () => {
+      return resultSuccess(fakeCodeList['1'], { msg: 'success' });
+    },
+  },
+  // mock user login example
+  // {
+  //   url: '/sys-api/user/login',
+  //   timeout: 200,
+  //   method: 'post',
+  //   response: ({ body }) => {
+  //     const { username, password } = body;
+  //     const checkUser = createFakeUserList().find(
+  //       (item) => item.username === username && password === item.password,
+  //     );
+  //     if (!checkUser) {
+  //       return resultError('Incorrect account or password!');
+  //     }
+  //     const { userId, username: _username, token, realName, desc, roles } = checkUser;
+  //     return resultSuccess({
+  //       roles,
+  //       userId,
+  //       username: _username,
+  //       token,
+  //       realName,
+  //       desc,
+  //     });
+  //   },
+  // },
+] as MockMethod[];

+ 162 - 0
package.json

@@ -0,0 +1,162 @@
+{
+  "name": "simple-admin",
+  "version": "1.3.11",
+  "homepage": "https://github.com/suyuan32/simple-admin-backend-ui",
+  "bugs": {
+    "url": "https://github.com/suyuan32/simple-admin-backend-ui/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/suyuan32/simple-admin-backend-ui.git"
+  },
+  "license": "MIT",
+  "author": {
+    "name": "ryan",
+    "email": "yuansu.china.work@gmail.com",
+    "url": "https://github.com/suyuan32"
+  },
+  "type": "module",
+  "scripts": {
+    "bootstrap": "pnpm install",
+    "build": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 pnpm vite build",
+    "build:analyze": "cross-env NODE_OPTIONS=--max-old-space-size=8192 pnpm vite build --mode analyze",
+    "build:docker": "vite build --mode docker",
+    "build:no-cache": "pnpm store prune && npm run build",
+    "build:test": "cross-env NODE_OPTIONS=--max-old-space-size=8192 pnpm vite build --mode test",
+    "commit": "czg",
+    "dev": "pnpm vite",
+    "preinstall": "npx only-allow pnpm",
+    "postinstall": "turbo run stub",
+    "lint": "turbo run lint",
+    "lint:eslint": "eslint --cache --max-warnings 0  \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
+    "lint:prettier": "prettier --write .",
+    "lint:stylelint": "stylelint \"**/*.{vue,css,less,scss}\" --fix --cache --cache-location node_modules/.cache/stylelint/",
+    "prepare": "husky install",
+    "preview": "npm run build && vite preview",
+    "reinstall": "rimraf pnpm-lock.yaml && rimraf package.lock.json && rimraf node_modules && npm run bootstrap",
+    "serve": "npm run dev",
+    "test:gzip": "npx http-server dist --cors --gzip -c-1",
+    "type:check": "vue-tsc --noEmit --skipLibCheck"
+  },
+  "lint-staged": {
+    "*.{js,jsx,ts,tsx}": [
+      "prettier --write",
+      "eslint --fix"
+    ],
+    "{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": [
+      "prettier --write--parser json"
+    ],
+    "package.json": [
+      "prettier --write"
+    ],
+    "*.vue": [
+      "prettier --write",
+      "eslint --fix",
+      "stylelint --fix"
+    ],
+    "*.{scss,less,styl,html}": [
+      "prettier --write",
+      "stylelint --fix"
+    ],
+    "*.md": [
+      "prettier --write"
+    ]
+  },
+  "config": {
+    "commitizen": {
+      "path": "node_modules/cz-git"
+    }
+  },
+  "dependencies": {
+    "@ant-design/icons-vue": "^7.0.1",
+    "@axolo/tree-array": "^0.1.0",
+    "@codemirror/lang-json": "^6.0.1",
+    "@codemirror/language": "^6.10.1",
+    "@codemirror/legacy-modes": "^6.3.3",
+    "@iconify/iconify": "^3.1.1",
+    "@logicflow/core": "^1.2.22",
+    "@logicflow/extension": "^1.2.22",
+    "@uiw/codemirror-theme-github": "^4.21.24",
+    "@uponu/vuedraggable": "^4.1.3",
+    "@vben/hooks": "workspace:*",
+    "@vue/shared": "^3.4.21",
+    "@vueuse/core": "^10.9.0",
+    "@vueuse/shared": "^10.9.0",
+    "@zxcvbn-ts/core": "^3.0.4",
+    "ant-design-vue": "^4.1.2",
+    "axios": "^1.6.8",
+    "codemirror": "^6.0.1",
+    "cropperjs": "^1.6.1",
+    "crypto-js": "^4.2.0",
+    "dayjs": "^1.11.10",
+    "echarts": "^5.5.0",
+    "exceljs": "^4.4.0",
+    "file2md5": "^1.3.0",
+    "lodash-es": "^4.17.21",
+    "mockjs": "^1.1.0",
+    "nprogress": "^0.2.0",
+    "path-to-regexp": "^6.2.1",
+    "pinia": "2.1.7",
+    "pinia-plugin-persistedstate": "^3.2.1",
+    "print-js": "^1.6.0",
+    "qs": "^6.12.0",
+    "resize-observer-polyfill": "^1.5.1",
+    "showdown": "^2.1.0",
+    "sortablejs": "^1.15.2",
+    "tinymce": "^6.8.3",
+    "unocss": "^0.58.6",
+    "vditor": "^3.10.1",
+    "vue": "^3.4.21",
+    "vue-clipboard3": "^2.0.0",
+    "vue-codemirror": "^6.1.1",
+    "vue-i18n": "^9.10.2",
+    "vue-json-pretty": "^2.3.0",
+    "vue-router": "^4.3.0",
+    "vue-types": "^5.1.1",
+    "vuedraggable": "^4.1.0",
+    "vxe-table": "^4.5.21",
+    "vxe-table-plugin-export-xlsx": "^4.0.1",
+    "xe-utils": "^3.5.22",
+    "xlsx": "^0.18.5"
+  },
+  "devDependencies": {
+    "@commitlint/cli": "^18.6.1",
+    "@commitlint/config-conventional": "^18.6.3",
+    "@iconify/json": "^2.2.192",
+    "@purge-icons/generated": "^0.10.0",
+    "@types/codemirror": "^5.60.15",
+    "@types/crypto-js": "^4.2.2",
+    "@types/lodash-es": "^4.17.12",
+    "@types/mockjs": "^1.0.10",
+    "@types/nprogress": "^0.2.3",
+    "@types/qs": "^6.9.12",
+    "@types/showdown": "^2.0.6",
+    "@types/sortablejs": "^1.15.8",
+    "@vben/eslint-config": "workspace:*",
+    "@vben/stylelint-config": "workspace:*",
+    "@vben/ts-config": "workspace:*",
+    "@vben/types": "workspace:*",
+    "@vben/vite-config": "workspace:*",
+    "@vue/compiler-sfc": "^3.4.21",
+    "@vue/test-utils": "^2.4.5",
+    "cross-env": "^7.0.3",
+    "cz-git": "^1.9.0",
+    "czg": "^1.9.0",
+    "husky": "^9.0.11",
+    "lint-staged": "15.2.2",
+    "prettier": "^3.2.5",
+    "prettier-plugin-packagejson": "^2.4.12",
+    "rimraf": "^5.0.5",
+    "turbo": "^1.12.5",
+    "typescript": "^5.4.2",
+    "unbuild": "^2.0.0",
+    "vite": "^5.1.6",
+    "vite-plugin-mock": "^3.0.1",
+    "vue-tsc": "^1.8.27"
+  },
+  "packageManager": "pnpm@8.10.0",
+  "engines": {
+    "node": ">=18.12.0",
+    "pnpm": ">=8.10.0"
+  }
+}

+ 0 - 0
packages/.gitkeep


+ 4 - 0
packages/hooks/.eslintrc.cjs

@@ -0,0 +1,4 @@
+module.exports = {
+  root: true,
+  extends: ['@vben/eslint-config/strict'],
+};

+ 10 - 0
packages/hooks/build.config.ts

@@ -0,0 +1,10 @@
+import { defineBuildConfig } from 'unbuild';
+
+export default defineBuildConfig({
+  clean: true,
+  entries: ['src/index'],
+  declaration: true,
+  rollup: {
+    emitCJS: true,
+  },
+});

+ 40 - 0
packages/hooks/package.json

@@ -0,0 +1,40 @@
+{
+  "name": "@vben/hooks",
+  "version": "1.0.0",
+  "homepage": "https://github.com/vbenjs/vue-vben-admin",
+  "bugs": {
+    "url": "https://github.com/vbenjs/vue-vben-admin/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+    "directory": "packages/hooks"
+  },
+  "license": "MIT",
+  "sideEffects": false,
+  "type": "module",
+  "exports": {
+    ".": {
+      "default": "./src/index.ts"
+    }
+  },
+  "main": "./src/index.ts",
+  "module": "./src/index.ts",
+  "files": [
+    "dist"
+  ],
+  "scripts": {
+    "//build": "pnpm unbuild",
+    "//stub": "pnpm unbuild --stub",
+    "clean": "pnpm rimraf .turbo node_modules dist",
+    "lint": "pnpm eslint ."
+  },
+  "dependencies": {
+    "@vueuse/core": "^10.7.1",
+    "lodash-es": "^4.17.21",
+    "vue": "^3.4.5"
+  },
+  "devDependencies": {
+    "@vben/types": "workspace:*"
+  }
+}

+ 6 - 0
packages/hooks/src/index.ts

@@ -0,0 +1,6 @@
+export * from './onMountedOrActivated';
+export * from './useAttrs';
+export * from './useRefs';
+export * from './useScrollTo';
+export * from './useWindowSizeFn';
+export { useTimeoutFn } from '@vueuse/core';

+ 25 - 0
packages/hooks/src/onMountedOrActivated.ts

@@ -0,0 +1,25 @@
+import { type AnyFunction } from '@vben/types';
+import { nextTick, onActivated, onMounted } from 'vue';
+
+/**
+ * 在 OnMounted 或者 OnActivated 时触发
+ * @param hook 任何函数(包括异步函数)
+ */
+function onMountedOrActivated(hook: AnyFunction) {
+  let mounted: boolean;
+
+  onMounted(() => {
+    hook();
+    nextTick(() => {
+      mounted = true;
+    });
+  });
+
+  onActivated(() => {
+    if (mounted) {
+      hook();
+    }
+  });
+}
+
+export { onMountedOrActivated };

+ 43 - 0
packages/hooks/src/useAttrs.ts

@@ -0,0 +1,43 @@
+import { type Recordable } from '@vben/types';
+import { getCurrentInstance, reactive, shallowRef, watchEffect } from 'vue';
+
+interface UseAttrsOptions {
+  excludeListeners?: boolean;
+  excludeKeys?: string[];
+  excludeDefaultKeys?: boolean;
+}
+
+const DEFAULT_EXCLUDE_KEYS = ['class', 'style'];
+const LISTENER_PREFIX = /^on[A-Z]/;
+
+function entries<T>(obj: Recordable<T>): [string, T][] {
+  return Object.keys(obj).map((key: string) => [key, obj[key]]);
+}
+
+function useAttrs(options: UseAttrsOptions = {}): Recordable<any> {
+  const instance = getCurrentInstance();
+  if (!instance) return {};
+
+  const { excludeListeners = false, excludeKeys = [], excludeDefaultKeys = true } = options;
+  const attrs = shallowRef({});
+  const allExcludeKeys = excludeKeys.concat(excludeDefaultKeys ? DEFAULT_EXCLUDE_KEYS : []);
+
+  // Since attrs are not reactive, make it reactive instead of doing in `onUpdated` hook for better performance
+  instance.attrs = reactive(instance.attrs);
+
+  watchEffect(() => {
+    const res = entries(instance.attrs).reduce((acm, [key, val]) => {
+      if (!allExcludeKeys.includes(key) && !(excludeListeners && LISTENER_PREFIX.test(key))) {
+        acm[key] = val;
+      }
+
+      return acm;
+    }, {} as Recordable<any>);
+
+    attrs.value = res;
+  });
+
+  return attrs;
+}
+
+export { useAttrs, type UseAttrsOptions };

部分文件因文件數量過多而無法顯示