Browse Source

first commit

master
zhouhaibin 3 months ago
commit
ee92fb5d6c
  1. 4
      .browserslistrc
  2. 107
      .commitlintrc.cjs
  3. 19
      .editorconfig
  4. 2
      .env
  5. 17
      .env.development
  6. 27
      .env.production
  7. 17
      .env.test
  8. 16
      .eslintignore
  9. 7
      .eslintrc.cjs
  10. 11
      .gitattributes
  11. 38
      .gitee/ISSUE_TEMPLATE/bug.yml
  12. 1
      .gitee/ISSUE_TEMPLATE/config.yml
  13. 43
      .gitee/ISSUE_TEMPLATE/feature.yml
  14. 34
      .gitignore
  15. 8
      .husky/commit-msg
  16. 9
      .husky/common.sh
  17. 10
      .husky/pre-commit
  18. 8
      .npmrc
  19. 12
      .prettierignore
  20. 19
      .prettierrc.cjs
  21. 2
      .stylelintignore
  22. 4
      .stylelintrc.cjs
  23. 14
      .vscode/extensions.json
  24. 13
      .vscode/launch.json
  25. 192
      .vscode/settings.json
  26. 479
      CHANGELOG.md
  27. 21
      LICENSE
  28. 177
      README.md
  29. 157
      index.html
  30. 9
      internal/eslint-config/.eslintignore
  31. 4
      internal/eslint-config/.eslintrc.cjs
  32. 10
      internal/eslint-config/build.config.ts
  33. 50
      internal/eslint-config/package.json
  34. 91
      internal/eslint-config/src/index.ts
  35. 57
      internal/eslint-config/src/strict.ts
  36. 5
      internal/eslint-config/tsconfig.json
  37. 9
      internal/stylelint-config/.eslintignore
  38. 4
      internal/stylelint-config/.eslintrc.cjs
  39. 10
      internal/stylelint-config/build.config.ts
  40. 49
      internal/stylelint-config/package.json
  41. 92
      internal/stylelint-config/src/index.ts
  42. 5
      internal/stylelint-config/tsconfig.json
  43. 27
      internal/ts-config/base.json
  44. 18
      internal/ts-config/node-server.json
  45. 12
      internal/ts-config/node.json
  46. 26
      internal/ts-config/package.json
  47. 10
      internal/ts-config/vue-app.json
  48. 9
      internal/vite-config/.eslintignore
  49. 4
      internal/vite-config/.eslintrc.cjs
  50. 10
      internal/vite-config/build.config.ts
  51. 59
      internal/vite-config/package.json
  52. 105
      internal/vite-config/src/config/application.ts
  53. 26
      internal/vite-config/src/config/common.ts
  54. 42
      internal/vite-config/src/config/package.ts
  55. 2
      internal/vite-config/src/index.ts
  56. 104
      internal/vite-config/src/plugins/appConfig.ts
  57. 38
      internal/vite-config/src/plugins/compress.ts
  58. 13
      internal/vite-config/src/plugins/html.ts
  59. 55
      internal/vite-config/src/plugins/index.ts
  60. 17
      internal/vite-config/src/plugins/svgSprite.ts
  61. 14
      internal/vite-config/src/plugins/visualizer.ts
  62. 7
      internal/vite-config/src/types/index.d.ts
  63. 49
      internal/vite-config/src/utils/env.ts
  64. 16
      internal/vite-config/src/utils/hash.ts
  65. 48
      internal/vite-config/src/utils/modifyVars.ts
  66. 5
      internal/vite-config/tsconfig.json
  67. 161
      package.json
  68. 0
      packages/.gitkeep
  69. 4
      packages/hooks/.eslintrc.cjs
  70. 10
      packages/hooks/build.config.ts
  71. 40
      packages/hooks/package.json
  72. 7
      packages/hooks/src/index.ts
  73. 25
      packages/hooks/src/onMountedOrActivated.ts
  74. 43
      packages/hooks/src/useAttrs.ts
  75. 24
      packages/hooks/src/useRefs.ts
  76. 147
      packages/hooks/src/useRequest/Fetch.ts
  77. 30
      packages/hooks/src/useRequest/index.ts
  78. 52
      packages/hooks/src/useRequest/plugins/useAutoRunPlugin.ts
  79. 127
      packages/hooks/src/useRequest/plugins/useCachePlugin.ts
  80. 71
      packages/hooks/src/useRequest/plugins/useDebouncePlugin.ts
  81. 45
      packages/hooks/src/useRequest/plugins/useLoadingDelayPlugin.ts
  82. 71
      packages/hooks/src/useRequest/plugins/usePollingPlugin.ts
  83. 37
      packages/hooks/src/useRequest/plugins/useRefreshOnWindowFocusPlugin.ts
  84. 54
      packages/hooks/src/useRequest/plugins/useRetryPlugin.ts
  85. 63
      packages/hooks/src/useRequest/plugins/useThrottlePlugin.ts
  86. 124
      packages/hooks/src/useRequest/types.ts
  87. 49
      packages/hooks/src/useRequest/useRequestImplement.ts
  88. 48
      packages/hooks/src/useRequest/utils/cache.ts
  89. 23
      packages/hooks/src/useRequest/utils/cachePromise.ts
  90. 22
      packages/hooks/src/useRequest/utils/cacheSubscribe.ts
  91. 5
      packages/hooks/src/useRequest/utils/isBrowser.ts
  92. 8
      packages/hooks/src/useRequest/utils/isDocumentVisible.ts
  93. 2
      packages/hooks/src/useRequest/utils/isFunction.ts
  94. 8
      packages/hooks/src/useRequest/utils/isOnline.ts
  95. 12
      packages/hooks/src/useRequest/utils/limit.ts
  96. 30
      packages/hooks/src/useRequest/utils/subscribeFocus.ts
  97. 25
      packages/hooks/src/useRequest/utils/subscribeReVisible.ts
  98. 60
      packages/hooks/src/useScrollTo.ts
  99. 40
      packages/hooks/src/useWindowSizeFn.ts
  100. 5
      packages/hooks/tsconfig.json

4
.browserslistrc

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

107
.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 `pnpm 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: 自定义',
},
};

19
.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

2
.env

@ -0,0 +1,2 @@
# spa-title
VITE_GLOB_APP_TITLE = Plus Admin

17
.env.development

@ -0,0 +1,17 @@
# public path
VITE_PUBLIC_PATH = /
# 后台请求路径 具体在vite.config.ts配置代理
VITE_GLOB_API_URL=/basic-api
# 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)
VITE_GLOB_ENABLE_ENCRYPT=true
# RSA公钥 请求加密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
# RSA私钥 响应解密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
# 客户端id
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 开启WEBSOCKET
VITE_GLOB_WEBSOCKET_ENABLE=true

27
.env.production

@ -0,0 +1,27 @@
# public path
VITE_PUBLIC_PATH = /
# 是否开启压缩 需要nginx支持
# 可选gzip brotli 也可共存 'brotli,gzip'
VITE_BUILD_COMPRESS = 'gzip'
# 是否删除所有的console.xx 和 debugger production模式才会生效
VITE_DROP_CONSOLE = true
# 后端路径
VITE_GLOB_API_URL=/prod-api
# 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)
VITE_GLOB_ENABLE_ENCRYPT=true
# RSA公钥 请求加密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
# RSA私钥 响应解密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
# 客户端id
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 开启WEBSOCKET
VITE_GLOB_WEBSOCKET_ENABLE=true

17
.env.test

@ -0,0 +1,17 @@
# public path
VITE_PUBLIC_PATH = /
# 后台请求路径 具体在vite.config.ts配置代理
VITE_GLOB_API_URL=/basic-api
# 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)
VITE_GLOB_ENABLE_ENCRYPT=true
# RSA公钥 请求加密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
# RSA私钥 响应解密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
# 客户端id
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 开启WEBSOCKET
VITE_GLOB_WEBSOCKET_ENABLE=true

16
.eslintignore

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

7
.eslintrc.cjs

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

11
.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

38
.gitee/ISSUE_TEMPLATE/bug.yml

@ -0,0 +1,38 @@
name: Bug 反馈
description: 当你在代码中发现了一个 Bug,导致应用崩溃或抛出异常,或者有一个组件存在问题,或者某些地方看起来不对劲。
title: '[Bug]: '
labels: ['bug']
body:
- type: markdown
attributes:
value: |
感谢对项目的支持与关注。在提出问题之前,请确保你已查看相关开发或使用文档:
- https://...
- type: checkboxes
attributes:
label: 这个问题是否已经存在?
options:
- label: 我已经搜索过现有的问题 (https://gitee.com/../../issues)
required: true
- type: textarea
attributes:
label: 如何复现
description: 请详细告诉我们如何复现你遇到的问题,如涉及代码,可提供一个最小代码示例,并使用反引号```附上它
placeholder: |
1. ...
2. ...
3. ...
validations:
required: true
- type: textarea
attributes:
label: 预期结果
description: 请告诉我们你预期会发生什么。
validations:
required: true
- type: textarea
attributes:
label: 截图或视频
description: 如果可以的话,上传任何关于 bug 的截图。
value: |
[在这里上传图片]

1
.gitee/ISSUE_TEMPLATE/config.yml

@ -0,0 +1 @@
blank_issues_enabled: true

43
.gitee/ISSUE_TEMPLATE/feature.yml

@ -0,0 +1,43 @@
name: 功能建议
description: 对本项目提出一个功能建议
title: '[功能建议]: '
labels: ['enhancement']
body:
- type: markdown
attributes:
value: |
感谢提出功能建议,我们将仔细考虑!
- type: textarea
id: related-problem
attributes:
label: 你的功能建议是否和某个问题相关?
description: 清晰并简洁地描述问题是什么,例如,当我...时,我总是感到困扰。
validations:
required: false
- type: textarea
id: desired-solution
attributes:
label: 你希望看到什么解决方案?
description: 清晰并简洁地描述你希望发生的事情。
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: 你考虑过哪些替代方案?
description: 清晰并简洁地描述你考虑过的任何替代解决方案或功能。
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: 你有其他上下文或截图吗?
description: 在此处添加有关功能请求的任何其他上下文或截图。
validations:
required: false
- type: checkboxes
attributes:
label: 意向参与贡献
options:
- label: 我有意向参与具体功能的开发实现并将代码贡献回到上游社区
required: false

34
.gitignore

@ -0,0 +1,34 @@
node_modules
.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

8
.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
.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
.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
pnpm exec lint-staged

8
.npmrc

@ -0,0 +1,8 @@
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
package-manager-strict=false

12
.prettierignore

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

19
.prettierrc.cjs

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

2
.stylelintignore

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

4
.stylelintrc.cjs

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

14
.vscode/extensions.json

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

13
.vscode/launch.json

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

192
.vscode/settings.json

@ -0,0 +1,192 @@
{
"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",
"Cascader",
"clientid",
"codemirror",
"colorpicker",
"commitlint",
"cropperjs",
"Datas",
"didi",
"echarts",
"esnext",
"esno",
"exceljs",
"iconify",
"INNERLINK",
"INTLIFY",
"jsencrypt",
"lintstagedrc",
"logicflow",
"mockjs",
"nprogress",
"packagejson",
"PARENTVIEW",
"persistedstate",
"phonenumber",
"pinia",
"pnpm",
"Popconfirm",
"preinstall",
"qrcode",
"ruoyi",
"sider",
"sortablejs",
"stylelint",
"tailwindcss",
"tinymce",
"unocss",
"unref",
"vben",
"vditor",
"Vite",
"vitejs",
"vuedraggable",
"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,
"vscodeCustomCodeColor.highlightValue": "v-auth",
"vscodeCustomCodeColor.highlightValueColor": "#6366f1"
}

479
CHANGELOG.md

@ -0,0 +1,479 @@
# 1.3.5
**Feature**
新增导出功能(带查询参数)
duplicateRoutesChecker 路由name重复检查器
**Refactor**
重构用户选择`UserSelect`组件, 不用在父页面多次引入, 增加对应type的emit即可
**Bug Fixed**
**其他**
修改全局名称为`Ruoyi Plus` -> `Plus Admin`
eslint/typescript扫描 修复全局报错 现在不会有任何报错(vscode)
因Vxe-Table 4.7版本类型定义与4.6不同 回滚为4.6版本
导出取消`PopConfirmButton`二次确认 改为`Button`
更新`update_icon.sql`文件
# 1.3.4
**Refactor**
合并最新vben Upload组件 添加demo
根据后端重构oauth相关功能
**Bug Fixed**
**其他**
通知提醒的分页样式
修复 [vue-router 4.3.2 -> 4.3.3](https://github.com/vuejs/router/compare/v4.3.2...v4.3.3#diff-bd14e588139087e2c8f184c61b6978ffb473a2d63261ce07b19c8ffd9882d955R551) warn `permissionGuard.ts:82 [Vue Router warn]: Finding ancestor route "/:path(.*)*" failed for "/:path(.*)*"`
Vxe-Table 升级 4.7.25 需要重新`pnpm install`
# 1.3.3
**Refactor**
`TableSwitch`组件去除二次确认Modal, 可直接切换
**Bug Fixed**
流程设计器 单选 在id为undefined下打开窗口会错误赋值id-0
**其他**
锁定vxe-table版本号 "vxe-table": "~4.6.3", 防止版本冲突
# 1.3.2
**Bug Fixed**
强制指定vite-config模块的postcss版本 解决高版本unocss报错问题(unocss版本暂时锁死0.60.4)
修复消息通知点击通知一直弹窗不会关闭的问题
# 1.3.1
**Feature**
系统主题切换功能(重磅登场!)
**Bug Fixed**
暂时降级@Unocss到0.58.9版本 更高版本报错无法运行
# 1.3.0
合并最新vben依赖
**Feature**
加签/减签功能
# 1.2.0(大版本更新)
**Bug Fixed**
当存在`菜单根目录为菜单`形式(比如演示站的微信群) & login?redirect=%252F(即/) 会错误调转到`对应的第一个根目录菜单的页面`(空页面) 解决: 判断为/跳转到首页
官方的element使用ParentView来处理二级菜单 使用ParentView的菜单不应被添加到路由(添加到侧边菜单就够了)
ps: 相同name的路由 后一个会覆盖前一个(https://www.cnblogs.com/mochenxiya/p/16732793.html)
OSS 图片拓展名大写无法预览 -> toLowerCase()比较
代码生成 编辑 下拉框组件在展开状态下滚动不会跟随 -> 官方方案: [注意,如果发现下拉菜单跟随页面滚动,或者需要在其他弹层中触发 Select,请尝试使用 getPopupContainer={triggerNode => triggerNode.parentNode} 将下拉弹层渲染节点固定在触发器的父元素中。](https://www.antdv.com/components/select-cn#api)
**Refactor**
主题色(primaryColor)由`#0960bd`修改为`#1677FF`[AntDesign品牌色](https://ant-design.antgroup.com/docs/spec/colors-cn#%E4%BA%A7%E5%93%81%E7%BA%A7%E8%89%B2%E5%BD%A9%E4%BD%93%E7%B3%BB)
使用Ant Design推荐的设计规范 将表格按钮从title栏(左边)移动到toolbar(右边)
详见:
- [有很多按钮组,如何确定顺序](https://ant-design.antgroup.com/docs/spec/buttons-cn#%E6%9C%89%E5%BE%88%E5%A4%9A%E6%8C%89%E9%92%AE%E7%BB%84%E5%A6%82%E4%BD%95%E7%A1%AE%E5%AE%9A%E9%A1%BA%E5%BA%8F)
- [按钮区](https://ant-design.antgroup.com/docs/spec/buttons-cn#%E6%8C%89%E9%92%AE%E5%8C%BA)
修改部分按钮颜色(比如导出)改为次要按钮
部分需要二次确认的操作(删除) 由modal改为popConfirm
- [覆盖层](https://ant-design.antgroup.com/docs/spec/stay-cn#%E8%A6%86%E7%9B%96%E5%B1%82)
代码生成-编辑 Table编辑组件改为`VxeTable` 性能对比原来的`AntTable`性能大幅提升
BasicTable 使用`reload`代替`reloadWithCallback` 官方已经修复modal会有关闭两次动画问题
**Feature**
`VITE_GLOB_ENABLE_ENCRYPT`全局请求加解密开关
适配官方workflow版本(预览版)
TableAction的dropdown按钮支持绑定按钮样式
代码生成-代码预览 支持根据文件的不同类型切换不同的预览显示
BasicTable reload添加`doNotClearSelectRows`参数 默认reload会清空当前表格所有选中行
租户管理 `未添加任何租户套餐时`不允许打开`新增租户抽屉`并弹出提示(所有东西填完了发现没法选择租户套餐所以也没办法提交😅)
**其他**
客户端管理 id为1(默认PC客户端)不可进行禁用
Oss配置 新增添加提示信息
默认登录页 登录中disabled第三方登录
上传文件/头像上传 超时时间设置为30s
**依赖更新**
npm依赖升级至目前最新
新增依赖 bpmn-js bpmn-js-token-simulation diagram-js diagram-js-minimap didi tiny-svg
| 依赖 | 功能 |
| ------------------------ | --------------------- |
| bpmn-js | bpmn核心依赖 |
| diagram-js | 创建/管理bpmn图形界面 |
| bpmn-js-token-simulation | 流程图执行模拟 |
| diagram-js-minimap | 流程图小地图 |
| didi | 依赖注入 |
| tiny-svg | svg依赖 |
# 1.1.10
租户套餐 增加预览菜单功能
去除表格上部表单 重置/搜索的前置图标
默认DictTag的primary色由`blue`改为`primary`
字典标签重构 支持`自定义颜色`+`css属性(前置/后置小点效果)`
unocss支持iconify图标 不用再导入Icon组件
- 即`<div class="i-material-symbols-light:13mp-outline-sharp"></div>`写法
~~用户管理 新增/修改 部门选择改为`级联选择器`组件~~ (由于修改了岗位和部门联动逻辑, 恢复成下拉)
# 1.1.9
**Bug Fixed**
当存在`菜单根目录为菜单`形式(比如演示站的微信群) & login?redirect=%252F(即/) 会错误调转到`对应的第一个根目录菜单的页面`(空页面) 解决: 判断为/跳转到首页
官方的element使用ParentView来处理二级菜单 使用ParentView的菜单不应被添加到路由(添加到侧边菜单就够了)
ps: 相同name的路由 后一个会覆盖前一个(https://www.cnblogs.com/mochenxiya/p/16732793.html)
**Refactor**
主题色(primaryColor)由`#0960bd`修改为`#1677FF`[AntDesign品牌色](https://ant-design.antgroup.com/docs/spec/colors-cn#%E4%BA%A7%E5%93%81%E7%BA%A7%E8%89%B2%E5%BD%A9%E4%BD%93%E7%B3%BB)
使用Ant Design推荐的设计规范 将表格按钮从title栏(左边)移动到toolbar(右边)
详见:
- [有很多按钮组,如何确定顺序](https://ant-design.antgroup.com/docs/spec/buttons-cn#%E6%9C%89%E5%BE%88%E5%A4%9A%E6%8C%89%E9%92%AE%E7%BB%84%E5%A6%82%E4%BD%95%E7%A1%AE%E5%AE%9A%E9%A1%BA%E5%BA%8F)
- [按钮区](https://ant-design.antgroup.com/docs/spec/buttons-cn#%E6%8C%89%E9%92%AE%E5%8C%BA)
修改部分按钮颜色(比如导出)改为次要按钮
部分需要二次确认的操作(删除) 由modal改为popConfirm
- [覆盖层](https://ant-design.antgroup.com/docs/spec/stay-cn#%E8%A6%86%E7%9B%96%E5%B1%82)
代码生成-编辑 Table编辑组件改为`VxeTable` 性能对比原来的`AntTable`性能大幅提升
BasicTable 使用`reload`代替`reloadWithCallback` 官方已经修复modal会有关闭两次动画问题
**Feature**
`VITE_GLOB_ENABLE_ENCRYPT`全局请求加解密开关
TableAction的dropdown按钮支持绑定按钮样式
代码生成-代码预览 支持根据文件的不同类型切换不同的预览显示
BasicTable reload添加`doNotClearSelectRows`参数 默认reload会清空当前表格所有选中行
租户管理 `未添加任何租户套餐时`不允许打开`新增租户抽屉`并弹出提示(所有东西填完了发现没法选择租户套餐所以也没办法提交)
**其他**
客户端管理 id为1(默认PC客户端)不可进行禁用
Oss配置 新增添加提示信息
默认登录页 登录中disabled第三方登录
**依赖更新**
npm依赖升级至目前最新
# 1.1.8
**Bug Fixed**
使用filter方法替代findNodeAll(用于排除节点) (findNodeAll由于children拼写错误导致运行成功--!)
用户管理 用户导入 下载模板modal添加z-index(设置过小会有bug) 否则下载模板时会遮挡loading效果
角色管理 分配角色 导入由modal改为抽屉 修复表格翻页会重置勾选状态
**Refactor**
逻辑更新 去除websocket相关**VITE_GLOB_WEBSOCKET_URL** 兼容apiUrl为http链接/非链接形式 即使用http://aaa.com/bbb或/bbb都行
**Feature**
登录重定向 即登录页login?redirect=重定向地址(即1.1.6有bug被移除 vben官方已经修复)
- 在登录超时/踢下线/修改角色下线(即403)等一些会携带redirect /login?redirect=xxx地址
- 如果是正常登出则不带redirect /login
- redirect如果是不存在的地址(手动地址栏输入/更改角色权限导致菜单不存在)则跳转到默认首页
增加`手机号登录`的支持
**其他**
oss的图片预览组件TableImg改为异步导入 解决table加载时间过长(loading会在图片加载完成结束 改为异步则不会有限制)
大量的拼写错误(还是建议安装一个拼写检查工具 vscode没有自带 --!)
去除接口前缀相关**VITE_GLOB_API_URL_PREFIX** 直接拼接在**VITE_GLOB_API_URL**即可
# 1.1.7
**Bug Fixed**
租户套餐 在未操作菜单情况下(比如直接点确定/改备注后点确定) transformIdStr函数转number导致丢失精度 -> id直接用string
src/router/guard/permissionGuard.ts 外链不能被添加到路由(漏了)
**Feature**
租户套餐 隐藏租户相关菜单 只有superadmin可以操作 分配了也没法用
角色管理 隐藏租户相关菜单 只有superadmin可以操作 分配了也没法用
角色管理 小管理员(admin)不可操作(防止误操作把自己权限玩没)
**其他**
登录 有验证码和无验证码时input宽度(404-400px)统一 原版本在有验证码情况下input宽度太长了
用户管理 部门树选择 ->改为/
菜单管理 菜单树选择 ->改为/
部门管理 部门树选择 ->改为/
用户管理 左侧部门树增加图标
角色管理 仅超级管理员可修改小管理员菜单
# 1.1.6-fix
~~登录重定向 即登录页login?redirect=重定向地址~~ **功能有严重BUG 回滚版本**
# 1.1.6
**Bug Fixed**
修复同一个字典多次请求api(页面/modal/drawer会加载三次), 现在只会请求一次
角色管理 分配用户 Number(roleId)导致精度丢失 -> 改为string
/@/ => @/(新版vben已不支持/@/路径)
pinia-plugin-persistedstate插件无法持久化(key的问题)
**Feature**
登录页面 租户和验证码都加载完成登录按钮才可用(enable)
**有严重BUG 于1.1.6-fix版本删除** ~~登录重定向 即登录页login?redirect=重定向地址~~
- ~~在登录超时/踢下线/修改角色下线(即403)等一些会携带redirect~~
- ~~如果是正常登出则不带redirect~~
- ~~redirect如果是不存在的地址则跳转到默认首页~~
**其他**
控制台warning: [DOM] Found 2 elements with non-unique id #form_item_configKey: (More info: https://goo.gl/9p2vKq) 主要是由于id冲突(即搜索和更新用的同一个id) 表单添加上name参数即可
改了一些代码(oxlint warning) 主要是代码风格
# 1.1.5
**Bug Fixed**
**Feature**
**其他**
用户管理 默认头像
userStore 默认头像
操作日志 添加id
客户端管理 pc不允许禁用(编辑里仍然可以修改)
消息通知 style
# 1.1.4
**Bug Fixed**
夜间模式通过刷新加载会有短暂白屏问题
回滚部门管理代码 逻辑有问题 --!
**Feature**
用户管理 部门树skeleton加载
**其他**
用户管理 用户信息modal使用skeleton加载
oss配置 v-auth
代码生成 v-auth
# 1.1.3
**Bug Fixed**
**Feature**
用户管理 用户信息预览
添加使用modal/drawer页面打开时的loading
代码生成 - 代码预览样式优化
代码生成 - 树表
**其他**
个人中心 绑定item间距
# 1.1.2
**Bug Fixed**
通知公告 - 删除(变量写错了导致无法删除)
**Feature**
通知公告 - 预览
富文本编辑器(TinyMce)支持图片大小修改(右键修改)
富文本编辑器(TinyMce)支持图片粘贴 (base64格式非上传)
**大部分组件新增抽屉 极少数表单只有几行的没加**
部门管理 切换上级部门时 对应的负责人列表也会变化
登录日志 根据浏览器/系统名称显示对应图标
在线用户 根据浏览器/系统名称显示对应图标
**其他**
手机端不显示租户切换(有遮挡)
租户下拉框样式优化
- 用户管理-用户导入 样式更新
- 用户管理-密码修改 样式更新
- 操作日志 添加'部门'
**大部分页面支持移动端(聊胜于无)**
# 1.1.1
**Bug Fixed**
(样式)按钮点击后任然有焦点
(样式)TableSetting居中
**Feature**
操作日志支持模态框/抽屉打开 可自行选择
操作日志清空添加等待时间5S 等待完成才能点击 防止误操作
登录日志清空添加等待时间5S 等待完成才能点击 防止误操作
个人中心添加loading效果
**其他**
菜单管理 为按钮时不再显示"新增按钮"(不合逻辑)
树表(如菜单管理/部门管理)去除空children 这样前面就不会有展开/关闭图标了
代码生成-多选删除 按钮样式
CollapseContainer border-radius 2px -> 6px
缓存监控 添加图标
表格圆角 2px -> 6px
# 1.1.0 (2024年1月16日)
**依赖更新**
- 升级目前最新最新vben
**Bug Fixed**
- 修复表格在开启border情况下 左边有一条竖线的样式问题
- 修复tab的关闭按钮"X"没有居中样式问题
**Feature**
- 树表支持双击展开/折叠 -菜单管理/部门管理
**其他**
- 登录后右上角昵称默认加粗显示
# 1.0.0 (2023-11-1)
**依赖更新**
- 升级目前最新最新vben 使用antv4版本
# 0.1.0
没写 初始完成版本 使用antv3版本

21
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.

177
README.md

@ -0,0 +1,177 @@
<div align="center"> <a href="https://github.com/anncwb/vue-vben-admin"> <img alt="VbenAdmin Logo" width="200" height="200" src="https://anncwb.github.io/anncwb/images/logo.png"> </a> <br> <br>
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE)
<h1>RuoYi Plus Vben</h1>
</div>
## 提示
已经将 antv4 版本的代码提交到该仓库 即默认 main 分支
关于运行警告: `The CJS build of Vite's Node API is deprecated. See https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.` 是由于升级vite5 官方还未解决但是不影响使用 详见[vben issue](https://github.com/vbenjs/vue-vben-admin/pull/3508)
## 简介
基于 [vben(ant-design-vue)](https://github.com/vbenjs/vue-vben-admin) 的 RuoYi-Vue-Plus 前端项目
| 组件/框架 | 版本 |
| :------------- | :----- |
| vben | 2.11.4 |
| ant-design-vue | 4.2.1 |
| vue | 3.4.25 |
对应后端项目: **(分布式 5.X 分支 微服务 2.分支)**
分布式 [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus/tree/5.X/)
微服务 [RuoYi-Cloud-Plus](https://gitee.com/dromara/RuoYi-Cloud-Plus/tree/2.X/)
## 预览
admin 账号: admin admin123
[预览地址点这里](http://117.72.10.31)
## WX Group
广告佬g远点, 正常人从演示站加
## 文档
[vben 文档地址](https://doc.vvbin.cn/)
[RuoYi-Plus 文档地址](https://plus-doc.dromara.org/#/)
[本框架文档(必看)](https://117.72.10.31:6060/)
## 预览图
![图片](https://gitee.com/dapppp/ruoyi-plus-vben/raw/main/preview/1.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben/raw/main/preview/2.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben/raw/main/preview/3.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben/raw/main/preview/4.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben/raw/main/preview/5.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben/raw/main/preview/6.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben/raw/main/preview/7.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben/raw/main/preview/8.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben/raw/main/preview/9.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben/raw/main/preview/10.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben/raw/main/preview/11.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben/raw/main/preview/12.png)
## 安装使用
前置准备环境(只能用pnpm)
```json
"packageManager": "pnpm@9.0.4",
"engines": {
"node": ">=18.12.0",
"pnpm": ">=9.0.4"
}
```
- 获取项目代码
```bash
git clone https://gitee.com/dapppp/ruoyi-plus-vben.git
```
- 安装依赖
```bash
cd ruoyi-plus-vben
pnpm install
```
- 关于代码生成(非必选)
**系统工具-代码生成 表格右上角有详细操作怎么改后端!**
**系统工具-代码生成 表格右上角有详细操作怎么改后端!**
**系统工具-代码生成 表格右上角有详细操作怎么改后端!**
- 关于一些监控的地址配置(微服务版本可以跳过这一小节)
使用[RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus/tree/5.X/)注意 `已经去除 admin/powerjob/easyretry 的.env 配置` 可自行修改 有两种方式
1. 修改源码`/views/monitor/powerjob` `/views/monitor/admin` `views/monitor/easyretry`
```ts
// 修改地址
const url = ref<string>('http://127.0.0.1:7700/#/oms/home');
```
2. **推荐** 使用菜单自行配置 (跟 cloud 版本打开方式一致)
![图片](https://gitee.com/dapppp/ruoyi-plus-vben/raw/main/preview/菜单修改.png)
使用内嵌 iframe 方式需要解决跨域问题 可参考[nginx.conf](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/5.X/script/docker/nginx/conf/nginx.conf#LC87)配置
- 修改.env.development 配置文件
- **注意 RSA 公私钥一定要修改和后端匹配**
- RSA 公私钥为两对 `前端请求加密-后端解密是一对` `后端响应加密 前端解密是一对`
```properties
# 后台请求路径 具体在vite.config.ts配置代理
VITE_GLOB_API_URL=/basic-api
# 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)
VITE_GLOB_ENABLE_ENCRYPT=true
# RSA公钥 请求加密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PUBLIC_KEY=xxx
# RSA私钥 响应解密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PRIVATE_KEY=xx
# 客户端id 必填
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 开启WEBSOCKET
VITE_GLOB_WEBSOCKET_ENABLE=true
```
- 运行
```bash
pnpm serve
```
- 打包
```bash
pnpm build
```
## 项目地址
- [vue-vben-admin](https://github.com/anncwb/vue-vben-admin) - vben
- [ruoyi-plus-vben](https://gitee.com/dapppp/ruoyi-plus-vben/tree/master)
- ~~[ruoyi-plus-vben-antv4](https://gitee.com/dapppp/ruoyi-plus-vben-antv4)~~
## Git 贡献提交规范
- 参考 [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))
- `feat` 增加新功能
- `fix` 修复问题/BUG
- `style` 代码风格相关无影响运行结果的
- `perf` 优化/性能提升
- `refactor` 重构
- `revert` 撤销修改
- `test` 测试相关
- `docs` 文档/注释
- `chore` 依赖更新/脚手架配置修改等
- `workflow` 工作流改进
- `ci` 持续集成
- `types` 类型定义文件更改
- `wip` 开发中
## 浏览器支持
本地开发推荐使用`Chrome 85+` 浏览器
支持现代浏览器, 不支持 IE
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: | :-: |
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## 捐赠
如果项目帮助到您 可以考虑请作者喝杯咖啡 万分感谢您对开源的支持!
<img src=https://117.72.10.31/minio-server/plus/2024/03/16/98a9d704eb0c4c04b721bf7799217571.jpg height=360px />

157
index.html

@ -0,0 +1,157 @@
<!doctype html>
<html lang="zh" 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>
<script>
// 从localStorage中获取主题 第一次打开页面未null 也就是默认的是light
const mode = localStorage.getItem('__APP__DARK__MODE__');
// data-theme设置之后 html[data-theme='dark'] 才会生效
// 主要解决夜间模式加载会白屏的问题
if (mode === 'dark') {
htmlRoot.setAttribute('data-theme', 'dark');
} else {
htmlRoot.setAttribute('data-theme', 'light');
}
</script>
<div class="app-loading">
<div class="app-loading-wrap">
<img src="/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
internal/eslint-config/.eslintignore

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

4
internal/eslint-config/.eslintrc.cjs

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

10
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
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.1",
"@typescript-eslint/parser": "^7.0.1",
"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
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
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
internal/eslint-config/tsconfig.json

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

9
internal/stylelint-config/.eslintignore

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

4
internal/stylelint-config/.eslintrc.cjs

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

10
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
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.38",
"postcss-html": "^1.6.0",
"postcss-less": "^6.0.0",
"postcss-scss": "^4.0.9",
"prettier": "^3.2.5",
"stylelint": "^16.4.0",
"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.1.0",
"stylelint-order": "^6.0.4",
"stylelint-prettier": "^5.0.0"
}
}

92
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
internal/stylelint-config/tsconfig.json

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

27
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
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
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
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.12.7",
"vite": "^5.2.10"
}
}

10
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
internal/vite-config/.eslintignore

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

4
internal/vite-config/.eslintrc.cjs

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

10
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
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.2.10"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"ant-design-vue": "^4.2.1",
"dayjs": "^1.11.10",
"dotenv": "^16.4.5",
"fs-extra": "^11.2.0",
"less": "^4.2.0",
"picocolors": "^1.0.0",
"pkg-types": "^1.1.0",
"postcss": "^8.4.38",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.75.0",
"unocss": "0.60.4",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-dts": "^3.9.0",
"vite-plugin-html": "^3.2.2",
"vite-plugin-purge-icons": "^0.10.0",
"vite-plugin-svg-icons": "^2.0.1"
}
}

105
internal/vite-config/src/config/application.ts

@ -0,0 +1,105 @@
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_BUILD_COMPRESS, VITE_ENABLE_ANALYZE } = loadEnv(mode, root);
const defineData = await createDefineData(root);
const plugins = await createPlugins({
isBuild,
root,
enableAnalyze: VITE_ENABLE_ANALYZE === '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') + '/',
},
],
},
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 };

26
internal/vite-config/src/config/common.ts

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

42
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
internal/vite-config/src/index.ts

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

104
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
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
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;
}

55
internal/vite-config/src/plugins/index.ts

@ -0,0 +1,55 @@
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 { configSvgIconsPlugin } from './svgSprite';
import { configVisualizerConfig } from './visualizer';
// import DevTools from 'vite-plugin-vue-devtools';
interface Options {
isBuild: boolean;
root: string;
compress: string;
enableAnalyze?: boolean;
}
async function createPlugins({ isBuild, root, compress, enableAnalyze }: Options) {
const vitePlugins: (PluginOption | PluginOption[])[] = [vue(), vueJsx()];
const appConfigPlugin = await createAppConfigPlugin({ root, isBuild });
vitePlugins.push(appConfigPlugin);
// vitePlugins.push(DevTools());
// 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());
}
return vitePlugins;
}
export { createPlugins };

17
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
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;
}

7
internal/vite-config/src/types/index.d.ts

@ -0,0 +1,7 @@
/**
* vite-plugin-purge-icons报错没有类型定义
*/
declare module 'vite-plugin-purge-icons' {
const type = any;
export default type;
}

49
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
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 };

48
internal/vite-config/src/utils/modifyVars.ts

@ -0,0 +1,48 @@
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 = '#0960bd';
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
};
}

5
internal/vite-config/tsconfig.json

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

161
package.json

@ -0,0 +1,161 @@
{
"name": "ruoyi-plus-vben",
"version": "1.3.5",
"homepage": "https://gitee.com/dapppp/ruoyi-plus-vben.git",
"bugs": {
"url": "https://gitee.com/dapppp/ruoyi-plus-vben/issues"
},
"repository": {
"type": "git",
"url": "git+https://gitee.com/dapppp/ruoyi-plus-vben.git"
},
"license": "MIT",
"author": {
"name": "vben",
"email": "anncwb@126.com",
"url": "https://github.com/anncwb"
},
"type": "module",
"scripts": {
"bootstrap": "pnpm install",
"build": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 pnpm vite build",
"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/**/*.{vue,ts,tsx}\" --fix",
"lint:prettier": "prettier --write .",
"lint:stylelint": "stylelint \"**/*.{vue,css,less,scss}\" --fix --cache --cache-location node_modules/.cache/stylelint/",
"log": "conventional-changelog -p angular -i CHANGELOG.md -s",
"prepare": "husky install",
"preview": "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",
"@iconify/iconify": "^3.1.1",
"@logicflow/core": "^1.2.26",
"@logicflow/extension": "^1.2.26",
"@vben/hooks": "workspace:*",
"@vue/shared": "^3.4.25",
"@vueuse/core": "^10.9.0",
"@zxcvbn-ts/core": "^3.0.4",
"ant-design-vue": "^4.2.3",
"axios": "^1.6.8",
"bpmn-js": "17.5.0",
"bpmn-js-token-simulation": "^0.34.1",
"codemirror": "^5.65.16",
"cropperjs": "^1.6.2",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
"diagram-js": "^14.6.0",
"diagram-js-minimap": "^4.1.0",
"didi": "^10.2.2",
"driver.js": "^1.3.1",
"echarts": "^5.5.0",
"exceljs": "^4.4.0",
"html2canvas": "^1.4.1",
"jsencrypt": "^3.3.2",
"lodash-es": "^4.17.21",
"nprogress": "^0.2.0",
"path-to-regexp": "^6.2.2",
"pinia": "2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"print-js": "^1.6.0",
"qrcode": "^1.5.3",
"qs": "^6.12.1",
"resize-observer-polyfill": "^1.5.1",
"showdown": "^2.1.0",
"sortablejs": "^1.15.2",
"tiny-svg": "^4.0.0",
"tinymce": "^5.10.9",
"unocss": "0.60.4",
"vditor": "^3.10.4",
"vue": "^3.4.25",
"vue-i18n": "^9.13.1",
"vue-json-pretty": "^2.4.0",
"vue-router": "^4.3.3",
"vue-types": "^5.1.1",
"vue3-colorpicker": "^2.3.0",
"vuedraggable": "^4.1.0",
"vxe-table": "4.6.17",
"vxe-table-plugin-export-xlsx": "^4.0.1",
"xe-utils": "^3.5.25",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@iconify/json": "^2.2.203",
"@purge-icons/generated": "^0.10.0",
"@types/codemirror": "^5.60.15",
"@types/crypto-js": "^4.2.2",
"@types/lodash-es": "^4.17.12",
"@types/nprogress": "^0.2.3",
"@types/qrcode": "^1.5.5",
"@types/qs": "^6.9.15",
"@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.25",
"@vue/test-utils": "^2.4.5",
"conventional-changelog-cli": "^4.1.0",
"cross-env": "^7.0.3",
"cz-git": "^1.9.1",
"czg": "^1.9.1",
"husky": "^9.0.11",
"lint-staged": "15.2.2",
"prettier": "^3.2.5",
"prettier-plugin-packagejson": "^2.5.0",
"rimraf": "^5.0.5",
"turbo": "^1.13.2",
"typescript": "^5.4.5",
"unbuild": "^2.0.0",
"vite": "^5.2.10",
"vite-plugin-vue-devtools": "^7.2.0",
"vue-tsc": "^2.0.14"
},
"engines": {
"node": ">=18.12.0",
"pnpm": ">=9.0.4"
}
}

0
packages/.gitkeep

4
packages/hooks/.eslintrc.cjs

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

10
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
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.9.0",
"lodash-es": "^4.17.21",
"vue": "^3.4.25"
},
"devDependencies": {
"@vben/types": "workspace:*"
}
}

7
packages/hooks/src/index.ts

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

25
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
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 };

24
packages/hooks/src/useRefs.ts

@ -0,0 +1,24 @@
import type { ComponentPublicInstance, Ref } from 'vue';
import { onBeforeUpdate, shallowRef } from 'vue';
function useRefs<T = HTMLElement>(): {
refs: Ref<T[]>;
setRefs: (index: number) => (el: Element | ComponentPublicInstance | null) => void;
} {
const refs = shallowRef([]) as Ref<T[]>;
onBeforeUpdate(() => {
refs.value = [];
});
const setRefs = (index: number) => (el: Element | ComponentPublicInstance | null) => {
refs.value[index] = el as T;
};
return {
refs,
setRefs,
};
}
export { useRefs };

147
packages/hooks/src/useRequest/Fetch.ts

@ -0,0 +1,147 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { reactive } from 'vue';
import type { FetchState, PluginReturn, Service, Subscribe, UseRequestOptions } from './types';
import { isFunction } from './utils/isFunction';
export default class Fetch<TData, TParams extends any[]> {
pluginImpls: PluginReturn<TData, TParams>[] = [];
count: number = 0;
state: FetchState<TData, TParams> = reactive({
loading: false,
params: undefined,
data: undefined,
error: undefined,
});
constructor(
public serviceRef: Service<TData, TParams>,
public options: UseRequestOptions<TData, TParams>,
public subscribe: Subscribe,
public initState: Partial<FetchState<TData, TParams>> = {},
) {
this.setState({ loading: !options.manual, ...initState });
}
setState(s: Partial<FetchState<TData, TParams>> = {}) {
Object.assign(this.state, s);
this.subscribe();
}
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
// @ts-ignore
const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
return Object.assign({}, ...r);
}
async runAsync(...params: TParams): Promise<TData> {
this.count += 1;
const currentCount = this.count;
const {
stopNow = false,
returnNow = false,
...state
} = this.runPluginHandler('onBefore', params);
// stop request
if (stopNow) {
return new Promise(() => {});
}
this.setState({
loading: true,
params,
...state,
});
// return now
if (returnNow) {
return Promise.resolve(state.data);
}
this.options.onBefore?.(params);
try {
// replace service
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef, params);
if (!servicePromise) {
servicePromise = this.serviceRef(...params);
}
const res = await servicePromise;
if (currentCount !== this.count) {
// prevent run.then when request is canceled
return new Promise(() => {});
}
// const formattedResult = this.options.formatResultRef.current ? this.options.formatResultRef.current(res) : res;
this.setState({ data: res, error: undefined, loading: false });
this.options.onSuccess?.(res, params);
this.runPluginHandler('onSuccess', res, params);
this.options.onFinally?.(params, res, undefined);
if (currentCount === this.count) {
this.runPluginHandler('onFinally', params, res, undefined);
}
return res;
} catch (error) {
if (currentCount !== this.count) {
// prevent run.then when request is canceled
return new Promise(() => {});
}
this.setState({ error, loading: false });
this.options.onError?.(error, params);
this.runPluginHandler('onError', error, params);
this.options.onFinally?.(params, undefined, error);
if (currentCount === this.count) {
this.runPluginHandler('onFinally', params, undefined, error);
}
throw error;
}
}
run(...params: TParams) {
this.runAsync(...params).catch((error) => {
if (!this.options.onError) {
console.error(error);
}
});
}
cancel() {
this.count += 1;
this.setState({ loading: false });
this.runPluginHandler('onCancel');
}
refresh() {
// @ts-ignore
this.run(...(this.state.params || []));
}
refreshAsync() {
// @ts-ignore
return this.runAsync(...(this.state.params || []));
}
mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
const targetData = isFunction(data) ? data(this.state.data) : data;
this.runPluginHandler('onMutate', targetData);
this.setState({ data: targetData });
}
}

30
packages/hooks/src/useRequest/index.ts

@ -0,0 +1,30 @@
import useAutoRunPlugin from './plugins/useAutoRunPlugin';
import useCachePlugin from './plugins/useCachePlugin';
import useDebouncePlugin from './plugins/useDebouncePlugin';
import useLoadingDelayPlugin from './plugins/useLoadingDelayPlugin';
import usePollingPlugin from './plugins/usePollingPlugin';
import useRefreshOnWindowFocusPlugin from './plugins/useRefreshOnWindowFocusPlugin';
import useRetryPlugin from './plugins/useRetryPlugin';
import useThrottlePlugin from './plugins/useThrottlePlugin';
import type { Service, UseRequestOptions, UseRequestPlugin } from './types';
import { useRequestImplement } from './useRequestImplement';
export { clearCache } from './utils/cache';
export function useRequest<TData, TParams extends any[]>(
service: Service<TData, TParams>,
options?: UseRequestOptions<TData, TParams>,
plugins?: UseRequestPlugin<TData, TParams>[],
) {
return useRequestImplement<TData, TParams>(service, options, [
...(plugins || []),
useDebouncePlugin,
useLoadingDelayPlugin,
usePollingPlugin,
useRefreshOnWindowFocusPlugin,
useThrottlePlugin,
useAutoRunPlugin,
useCachePlugin,
useRetryPlugin,
] as UseRequestPlugin<TData, TParams>[]);
}

52
packages/hooks/src/useRequest/plugins/useAutoRunPlugin.ts

@ -0,0 +1,52 @@
import { ref, unref, watch } from 'vue';
import type { UseRequestPlugin } from '../types';
// support refreshDeps & ready
const useAutoRunPlugin: UseRequestPlugin<any, any[]> = (
fetchInstance,
{ manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction },
) => {
const hasAutoRun = ref(false);
watch(
() => unref(ready),
(readyVal) => {
if (!unref(manual) && readyVal) {
hasAutoRun.value = true;
fetchInstance.run(...defaultParams);
}
},
);
if (refreshDeps.length) {
watch(refreshDeps, () => {
if (hasAutoRun.value) {
return;
}
if (!manual) {
if (refreshDepsAction) {
refreshDepsAction();
} else {
fetchInstance.refresh();
}
}
});
}
return {
onBefore: () => {
if (!unref(ready)) {
return { stopNow: true };
}
},
};
};
useAutoRunPlugin.onInit = ({ ready = true, manual }) => {
return {
loading: !unref(manual) && unref(ready),
};
};
export default useAutoRunPlugin;

127
packages/hooks/src/useRequest/plugins/useCachePlugin.ts

@ -0,0 +1,127 @@
import { onUnmounted, ref, watchEffect } from 'vue';
import type { UseRequestPlugin } from '../types';
import type { CachedData } from '../utils/cache';
import { getCache, setCache } from '../utils/cache';
import { getCachePromise, setCachePromise } from '../utils/cachePromise';
import { subscribe, trigger } from '../utils/cacheSubscribe';
const useCachePlugin: UseRequestPlugin<any, any[]> = (
fetchInstance,
{
cacheKey,
cacheTime = 5 * 60 * 1000,
staleTime = 0,
setCache: customSetCache,
getCache: customGetCache,
},
) => {
const unSubscribeRef = ref<() => void>();
const currentPromiseRef = ref<Promise<any>>();
const _setCache = (key: string, cachedData: CachedData) => {
customSetCache ? customSetCache(cachedData) : setCache(key, cacheTime, cachedData);
trigger(key, cachedData.data);
};
const _getCache = (key: string, params: any[] = []) => {
return customGetCache ? customGetCache(params) : getCache(key);
};
watchEffect(() => {
if (!cacheKey) return;
// get data from cache when init
const cacheData = _getCache(cacheKey);
if (cacheData && Object.hasOwnProperty.call(cacheData, 'data')) {
fetchInstance.state.data = cacheData.data;
fetchInstance.state.params = cacheData.params;
if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
fetchInstance.state.loading = false;
}
}
// subscribe same cachekey update, trigger update
unSubscribeRef.value = subscribe(cacheKey, (data) => {
fetchInstance.setState({ data });
});
});
onUnmounted(() => {
unSubscribeRef.value?.();
});
if (!cacheKey) {
return {};
}
return {
onBefore: (params) => {
const cacheData = _getCache(cacheKey, params);
if (!cacheData || !Object.hasOwnProperty.call(cacheData, 'data')) {
return {};
}
// If the data is fresh, stop request
if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
return {
loading: false,
data: cacheData?.data,
error: undefined,
returnNow: true,
};
} else {
// If the data is stale, return data, and request continue
return { data: cacheData?.data, error: undefined };
}
},
onRequest: (service, args) => {
let servicePromise = getCachePromise(cacheKey);
// If has servicePromise, and is not trigger by self, then use it
if (servicePromise && servicePromise !== currentPromiseRef.value) {
return { servicePromise };
}
servicePromise = service(...args);
currentPromiseRef.value = servicePromise;
setCachePromise(cacheKey, servicePromise);
return { servicePromise };
},
onSuccess: (data, params) => {
if (cacheKey) {
// cancel subscribe, avoid trgger self
unSubscribeRef.value?.();
_setCache(cacheKey, { data, params, time: new Date().getTime() });
// resubscribe
unSubscribeRef.value = subscribe(cacheKey, (d) => {
fetchInstance.setState({ data: d });
});
}
},
onMutate: (data) => {
if (cacheKey) {
// cancel subscribe, avoid trigger self
unSubscribeRef.value?.();
_setCache(cacheKey, {
data,
params: fetchInstance.state.params,
time: new Date().getTime(),
});
// resubscribe
unSubscribeRef.value = subscribe(cacheKey, (d) => {
fetchInstance.setState({ data: d });
});
}
},
};
};
export default useCachePlugin;

71
packages/hooks/src/useRequest/plugins/useDebouncePlugin.ts

@ -0,0 +1,71 @@
import type { DebouncedFunc, DebounceSettings } from 'lodash-es';
import { debounce } from 'lodash-es';
import { computed, ref, watchEffect } from 'vue';
import type { UseRequestPlugin } from '../types';
const useDebouncePlugin: UseRequestPlugin<any, any[]> = (
fetchInstance,
{ debounceWait, debounceLeading, debounceTrailing, debounceMaxWait },
) => {
const debouncedRef = ref<DebouncedFunc<any>>();
const options = computed(() => {
const ret: DebounceSettings = {};
if (debounceLeading !== undefined) {
ret.leading = debounceLeading;
}
if (debounceTrailing !== undefined) {
ret.trailing = debounceTrailing;
}
if (debounceMaxWait !== undefined) {
ret.maxWait = debounceMaxWait;
}
return ret;
});
watchEffect(() => {
if (debounceWait) {
const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
debouncedRef.value = debounce(
(callback) => {
callback();
},
debounceWait,
options.value,
);
// debounce runAsync should be promise
// https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
fetchInstance.runAsync = (...args) => {
return new Promise((resolve, reject) => {
debouncedRef.value?.(() => {
_originRunAsync(...args)
.then(resolve)
.catch(reject);
});
});
};
return () => {
debouncedRef.value?.cancel();
fetchInstance.runAsync = _originRunAsync;
};
}
});
if (!debounceWait) {
return {};
}
return {
onCancel: () => {
debouncedRef.value?.cancel();
},
};
};
export default useDebouncePlugin;

45
packages/hooks/src/useRequest/plugins/useLoadingDelayPlugin.ts

@ -0,0 +1,45 @@
import { ref, unref } from 'vue';
import type { UseRequestPlugin, UseRequestTimeout } from '../types';
const useLoadingDelayPlugin: UseRequestPlugin<any, any[]> = (
fetchInstance,
{ loadingDelay, ready },
) => {
const timerRef = ref<UseRequestTimeout>();
if (!loadingDelay) {
return {};
}
const cancelTimeout = () => {
if (timerRef.value) {
clearTimeout(timerRef.value);
}
};
return {
onBefore: () => {
cancelTimeout();
// Two cases:
// 1. ready === undefined
// 2. ready === true
if (unref(ready) !== false) {
timerRef.value = setTimeout(() => {
fetchInstance.setState({ loading: true });
}, loadingDelay);
}
return { loading: false };
},
onFinally: () => {
cancelTimeout();
},
onCancel: () => {
cancelTimeout();
},
};
};
export default useLoadingDelayPlugin;

71
packages/hooks/src/useRequest/plugins/usePollingPlugin.ts

@ -0,0 +1,71 @@
import { ref, watch } from 'vue';
import type { UseRequestPlugin, UseRequestTimeout } from '../types';
import { isDocumentVisible } from '../utils/isDocumentVisible';
import subscribeReVisible from '../utils/subscribeReVisible';
const usePollingPlugin: UseRequestPlugin<any, any[]> = (
fetchInstance,
{ pollingInterval, pollingWhenHidden = true, pollingErrorRetryCount = -1 },
) => {
const timerRef = ref<UseRequestTimeout>();
const unsubscribeRef = ref<() => void>();
const countRef = ref<number>(0);
const stopPolling = () => {
if (timerRef.value) {
clearTimeout(timerRef.value);
}
unsubscribeRef.value?.();
};
watch(
() => pollingInterval,
() => {
if (!pollingInterval) {
stopPolling();
}
},
);
if (!pollingInterval) {
return {};
}
return {
onBefore: () => {
stopPolling();
},
onError: () => {
countRef.value += 1;
},
onSuccess: () => {
countRef.value = 0;
},
onFinally: () => {
if (
pollingErrorRetryCount === -1 ||
// When an error occurs, the request is not repeated after pollingErrorRetryCount retries
(pollingErrorRetryCount !== -1 && countRef.value <= pollingErrorRetryCount)
) {
timerRef.value = setTimeout(() => {
// if pollingWhenHidden = false && document is hidden, then stop polling and subscribe revisible
if (!pollingWhenHidden && !isDocumentVisible()) {
unsubscribeRef.value = subscribeReVisible(() => {
fetchInstance.refresh();
});
} else {
fetchInstance.refresh();
}
}, pollingInterval);
} else {
countRef.value = 0;
}
},
onCancel: () => {
stopPolling();
},
};
};
export default usePollingPlugin;

37
packages/hooks/src/useRequest/plugins/useRefreshOnWindowFocusPlugin.ts

@ -0,0 +1,37 @@
import { onUnmounted, ref, watchEffect } from 'vue';
import type { UseRequestPlugin } from '../types';
import { limit } from '../utils/limit';
import subscribeFocus from '../utils/subscribeFocus';
const useRefreshOnWindowFocusPlugin: UseRequestPlugin<any, any[]> = (
fetchInstance,
{ refreshOnWindowFocus, focusTimespan = 5000 },
) => {
const unsubscribeRef = ref<() => void>();
const stopSubscribe = () => {
unsubscribeRef.value?.();
};
watchEffect(() => {
if (refreshOnWindowFocus) {
const limitRefresh = limit(fetchInstance.refresh.bind(fetchInstance), focusTimespan);
unsubscribeRef.value = subscribeFocus(() => {
limitRefresh();
});
}
return () => {
stopSubscribe();
};
});
onUnmounted(() => {
stopSubscribe();
});
return {};
};
export default useRefreshOnWindowFocusPlugin;

54
packages/hooks/src/useRequest/plugins/useRetryPlugin.ts

@ -0,0 +1,54 @@
import { ref } from 'vue';
import type { UseRequestPlugin, UseRequestTimeout } from '../types';
const useRetryPlugin: UseRequestPlugin<any, any[]> = (
fetchInstance,
{ retryInterval, retryCount },
) => {
const timerRef = ref<UseRequestTimeout>();
const countRef = ref(0);
const triggerByRetry = ref(false);
if (!retryCount) {
return {};
}
return {
onBefore: () => {
if (!triggerByRetry.value) {
countRef.value = 0;
}
triggerByRetry.value = false;
if (timerRef.value) {
clearTimeout(timerRef.value);
}
},
onSuccess: () => {
countRef.value = 0;
},
onError: () => {
countRef.value += 1;
if (retryCount === -1 || countRef.value <= retryCount) {
// Exponential backoff
const timeout = retryInterval ?? Math.min(1000 * 2 ** countRef.value, 30000);
timerRef.value = setTimeout(() => {
triggerByRetry.value = true;
fetchInstance.refresh();
}, timeout);
} else {
countRef.value = 0;
}
},
onCancel: () => {
countRef.value = 0;
if (timerRef.value) {
clearTimeout(timerRef.value);
}
},
};
};
export default useRetryPlugin;

63
packages/hooks/src/useRequest/plugins/useThrottlePlugin.ts

@ -0,0 +1,63 @@
import type { DebouncedFunc, ThrottleSettings } from 'lodash-es';
import { throttle } from 'lodash-es';
import { ref, watchEffect } from 'vue';
import type { UseRequestPlugin } from '../types';
const useThrottlePlugin: UseRequestPlugin<any, any[]> = (
fetchInstance,
{ throttleWait, throttleLeading, throttleTrailing },
) => {
const throttledRef = ref<DebouncedFunc<any>>();
const options: ThrottleSettings = {};
if (throttleLeading !== undefined) {
options.leading = throttleLeading;
}
if (throttleTrailing !== undefined) {
options.trailing = throttleTrailing;
}
watchEffect(() => {
if (throttleWait) {
const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
throttledRef.value = throttle(
(callback) => {
callback();
},
throttleWait,
options,
);
// throttle runAsync should be promise
// https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
fetchInstance.runAsync = (...args) => {
return new Promise((resolve, reject) => {
throttledRef.value?.(() => {
_originRunAsync(...args)
.then(resolve)
.catch(reject);
});
});
};
return () => {
fetchInstance.runAsync = _originRunAsync;
throttledRef.value?.cancel();
};
}
});
if (!throttleWait) {
return {};
}
return {
onCancel: () => {
throttledRef.value?.cancel();
},
};
};
export default useThrottlePlugin;

124
packages/hooks/src/useRequest/types.ts

@ -0,0 +1,124 @@
import type { MaybeRef, Ref, WatchSource } from 'vue';
import type Fetch from './Fetch';
import type { CachedData } from './utils/cache';
export type Service<TData, TParams extends any[]> = (...args: TParams) => Promise<TData>;
export type Subscribe = () => void;
// for Fetch
export interface FetchState<TData, TParams extends any[]> {
loading: boolean;
params?: TParams;
data?: TData;
error?: Error;
}
export interface PluginReturn<TData, TParams extends any[]> {
onBefore?: (params: TParams) =>
| ({
stopNow?: boolean;
returnNow?: boolean;
} & Partial<FetchState<TData, TParams>>)
| void;
onRequest?: (
service: Service<TData, TParams>,
params: TParams,
) => {
servicePromise?: Promise<TData>;
};
onSuccess?: (data: TData, params: TParams) => void;
onError?: (e: Error, params: TParams) => void;
onFinally?: (params: TParams, data?: TData, e?: Error) => void;
onCancel?: () => void;
onMutate?: (data: TData) => void;
}
// for useRequestImplement
export interface UseRequestOptions<TData, TParams extends any[]> {
manual?: MaybeRef<boolean>;
onBefore?: (params: TParams) => void;
onSuccess?: (data: TData, params: TParams) => void;
onError?: (e: Error, params: TParams) => void;
// formatResult?: (res: any) => TData;
onFinally?: (params: TParams, data?: TData, e?: Error) => void;
defaultParams?: TParams;
// refreshDeps
refreshDeps?: WatchSource<any>[];
refreshDepsAction?: () => void;
// loading delay
loadingDelay?: number;
// polling
pollingInterval?: number;
pollingWhenHidden?: boolean;
pollingErrorRetryCount?: number;
// refresh on window focus
refreshOnWindowFocus?: boolean;
focusTimespan?: number;
// debounce
debounceWait?: number;
debounceLeading?: boolean;
debounceTrailing?: boolean;
debounceMaxWait?: number;
// throttle
throttleWait?: number;
throttleLeading?: boolean;
throttleTrailing?: boolean;
// cache
cacheKey?: string;
cacheTime?: number;
staleTime?: number;
setCache?: (data: CachedData<TData, TParams>) => void;
getCache?: (params: TParams) => CachedData<TData, TParams> | undefined;
// retry
retryCount?: number;
retryInterval?: number;
// ready
ready?: MaybeRef<boolean>;
// [key: string]: any;
}
export interface UseRequestPlugin<TData, TParams extends any[]> {
// eslint-disable-next-line prettier/prettier
(
fetchInstance: Fetch<TData, TParams>,
options: UseRequestOptions<TData, TParams>,
): PluginReturn<TData, TParams>;
onInit?: (options: UseRequestOptions<TData, TParams>) => Partial<FetchState<TData, TParams>>;
}
// for index
// export type OptionsWithoutFormat<TData, TParams extends any[]> = Omit<Options<TData, TParams>, 'formatResult'>;
// export interface OptionsWithFormat<TData, TParams extends any[], TFormated, TTFormated extends TFormated = any> extends Omit<Options<TTFormated, TParams>, 'formatResult'> {
// formatResult: (res: TData) => TFormated;
// };
export interface UseRequestResult<TData, TParams extends any[]> {
loading: Ref<boolean>;
data: Ref<TData>;
error: Ref<Error>;
params: Ref<TParams | []>;
cancel: Fetch<TData, TParams>['cancel'];
refresh: Fetch<TData, TParams>['refresh'];
refreshAsync: Fetch<TData, TParams>['refreshAsync'];
run: Fetch<TData, TParams>['run'];
runAsync: Fetch<TData, TParams>['runAsync'];
mutate: Fetch<TData, TParams>['mutate'];
}
export type UseRequestTimeout = ReturnType<typeof setTimeout>;

49
packages/hooks/src/useRequest/useRequestImplement.ts

@ -0,0 +1,49 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { onMounted, onUnmounted, toRefs } from 'vue';
import Fetch from './Fetch';
import type { Service, UseRequestOptions, UseRequestPlugin, UseRequestResult } from './types';
export function useRequestImplement<TData, TParams extends any[]>(
service: Service<TData, TParams>,
options: UseRequestOptions<TData, TParams> = {},
plugins: UseRequestPlugin<TData, TParams>[] = [],
) {
const { manual = false, ...rest } = options;
const fetchOptions = { manual, ...rest };
const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
const fetchInstance = new Fetch<TData, TParams>(
service,
fetchOptions,
() => {},
Object.assign({}, ...initState),
);
fetchInstance.options = fetchOptions;
// run all plugins hooks
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
onMounted(() => {
if (!manual) {
const params = fetchInstance.state.params || options.defaultParams || [];
// @ts-ignore
fetchInstance.run(...params);
}
});
onUnmounted(() => {
fetchInstance.cancel();
});
return {
...toRefs(fetchInstance.state),
cancel: fetchInstance.cancel.bind(fetchInstance),
mutate: fetchInstance.mutate.bind(fetchInstance),
refresh: fetchInstance.refresh.bind(fetchInstance),
refreshAsync: fetchInstance.refreshAsync.bind(fetchInstance),
run: fetchInstance.run.bind(fetchInstance),
runAsync: fetchInstance.runAsync.bind(fetchInstance),
} as UseRequestResult<TData, TParams>;
}

48
packages/hooks/src/useRequest/utils/cache.ts

@ -0,0 +1,48 @@
type Timer = ReturnType<typeof setTimeout>;
type CachedKey = string | number;
export interface CachedData<TData = any, TParams = any> {
data: TData;
params: TParams;
time: number;
}
interface RecordData extends CachedData {
timer: Timer | undefined;
}
const cache = new Map<CachedKey, RecordData>();
export const setCache = (key: CachedKey, cacheTime: number, cachedData: CachedData) => {
const currentCache = cache.get(key);
if (currentCache?.timer) {
clearTimeout(currentCache.timer);
}
let timer: Timer | undefined = undefined;
if (cacheTime > -1) {
// if cache out, clear it
timer = setTimeout(() => {
cache.delete(key);
}, cacheTime);
}
cache.set(key, {
...cachedData,
timer,
});
};
export const getCache = (key: CachedKey) => {
return cache.get(key);
};
export const clearCache = (key?: string | string[]) => {
if (key) {
const cacheKeys = Array.isArray(key) ? key : [key];
cacheKeys.forEach((cacheKey) => cache.delete(cacheKey));
} else {
cache.clear();
}
};

23
packages/hooks/src/useRequest/utils/cachePromise.ts

@ -0,0 +1,23 @@
type CachedKey = string | number;
const cachePromise = new Map<CachedKey, Promise<any>>();
export const getCachePromise = (cacheKey: CachedKey) => {
return cachePromise.get(cacheKey);
};
export const setCachePromise = (cacheKey: CachedKey, promise: Promise<any>) => {
// Should cache the same promise, cannot be promise.finally
// Because the promise.finally will change the reference of the promise
cachePromise.set(cacheKey, promise);
// no use promise.finally for compatibility
promise
.then((res) => {
cachePromise.delete(cacheKey);
return res;
})
.catch(() => {
cachePromise.delete(cacheKey);
});
};

22
packages/hooks/src/useRequest/utils/cacheSubscribe.ts

@ -0,0 +1,22 @@
type Listener = (data: any) => void;
const listeners: Record<string, Listener[]> = {};
export const trigger = (key: string, data: any) => {
if (listeners[key]) {
listeners[key].forEach((item) => item(data));
}
};
export const subscribe = (key: string, listener: Listener) => {
if (!listeners[key]) {
listeners[key] = [];
}
listeners[key].push(listener);
return function unsubscribe() {
const index = listeners[key].indexOf(listener);
listeners[key].splice(index, 1);
};
};

5
packages/hooks/src/useRequest/utils/isBrowser.ts

@ -0,0 +1,5 @@
export const isBrowser = !!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);

8
packages/hooks/src/useRequest/utils/isDocumentVisible.ts

@ -0,0 +1,8 @@
import { isBrowser } from './isBrowser';
export function isDocumentVisible(): boolean {
if (isBrowser) {
return document.visibilityState !== 'hidden';
}
return true;
}

2
packages/hooks/src/useRequest/utils/isFunction.ts

@ -0,0 +1,2 @@
export const isFunction = (value: unknown): value is (...args: any) => any =>
typeof value === 'function';

8
packages/hooks/src/useRequest/utils/isOnline.ts

@ -0,0 +1,8 @@
import { isBrowser } from './isBrowser';
export function isOnline(): boolean {
if (isBrowser && typeof navigator.onLine !== 'undefined') {
return navigator.onLine;
}
return true;
}

12
packages/hooks/src/useRequest/utils/limit.ts

@ -0,0 +1,12 @@
export function limit(fn: any, timespan: number) {
let pending = false;
return (...args: any[]) => {
if (pending) return;
pending = true;
fn(...args);
setTimeout(() => {
pending = false;
}, timespan);
};
}

30
packages/hooks/src/useRequest/utils/subscribeFocus.ts

@ -0,0 +1,30 @@
import { isBrowser } from './isBrowser';
import { isDocumentVisible } from './isDocumentVisible';
import { isOnline } from './isOnline';
type Listener = () => void;
const listeners: Listener[] = [];
if (isBrowser) {
const revalidate = () => {
if (!isDocumentVisible() || !isOnline()) return;
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
};
window.addEventListener('visibilitychange', revalidate, false);
window.addEventListener('focus', revalidate, false);
}
export default function subscribe(listener: Listener) {
listeners.push(listener);
return function unsubscribe() {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
};
}

25
packages/hooks/src/useRequest/utils/subscribeReVisible.ts

@ -0,0 +1,25 @@
import { isBrowser } from './isBrowser';
import { isDocumentVisible } from './isDocumentVisible';
type Listener = () => void;
const listeners: Listener[] = [];
if (isBrowser) {
const revalidate = () => {
if (!isDocumentVisible()) return;
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
};
window.addEventListener('visibilitychange', revalidate, false);
}
export default function subscribe(listener: Listener) {
listeners.push(listener);
return function unsubscribe() {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}

60
packages/hooks/src/useScrollTo.ts

@ -0,0 +1,60 @@
import { shallowRef, unref } from 'vue';
interface UseScrollToOptions {
el: any;
to: number;
duration?: number;
callback?: () => any;
}
function easeInOutQuad(t: number, b: number, c: number, d: number) {
t /= d / 2;
if (t < 1) {
return (c / 2) * t * t + b;
}
t--;
return (-c / 2) * (t * (t - 2) - 1) + b;
}
function move(el: HTMLElement, amount: number) {
el.scrollTop = amount;
}
const position = (el: HTMLElement) => {
return el.scrollTop;
};
function useScrollTo({ el, to, duration = 500, callback }: UseScrollToOptions) {
const isActiveRef = shallowRef(false);
const start = position(el);
const change = to - start;
const increment = 20;
let currentTime = 0;
const animateScroll = function () {
if (!unref(isActiveRef)) {
return;
}
currentTime += increment;
const val = easeInOutQuad(currentTime, start, change, duration);
move(el, val);
if (currentTime < duration && unref(isActiveRef)) {
requestAnimationFrame(animateScroll);
} else {
if (callback && typeof callback === 'function') {
callback();
}
}
};
const run = () => {
isActiveRef.value = true;
animateScroll();
};
const stop = () => {
isActiveRef.value = false;
};
return { start: run, stop };
}
export { useScrollTo, type UseScrollToOptions };

40
packages/hooks/src/useWindowSizeFn.ts

@ -0,0 +1,40 @@
import { type AnyFunction } from '@vben/types';
import { tryOnMounted, tryOnUnmounted, useDebounceFn } from '@vueuse/core';
interface UseWindowSizeOptions {
wait?: number;
once?: boolean;
immediate?: boolean;
listenerOptions?: AddEventListenerOptions | boolean;
}
function useWindowSizeFn(fn: AnyFunction, options: UseWindowSizeOptions = {}) {
const { wait = 150, immediate } = options;
let handler = () => {
fn();
};
const handleSize = useDebounceFn(handler, wait);
handler = handleSize;
const start = () => {
if (immediate) {
handler();
}
window.addEventListener('resize', handler);
};
const stop = () => {
window.removeEventListener('resize', handler);
};
tryOnMounted(() => {
start();
});
tryOnUnmounted(() => {
stop();
});
return { start, stop };
}
export { useWindowSizeFn, type UseWindowSizeOptions };

5
packages/hooks/tsconfig.json

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

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save