commit
ee92fb5d6c
1108 changed files with 199645 additions and 0 deletions
@ -0,0 +1,4 @@ |
|||
> 1% |
|||
last 2 versions |
|||
not dead |
|||
not ie 11 |
@ -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: 自定义', |
|||
}, |
|||
}; |
@ -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 |
@ -0,0 +1,2 @@ |
|||
# spa-title |
|||
VITE_GLOB_APP_TITLE = Plus Admin |
@ -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 |
@ -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 |
@ -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 |
@ -0,0 +1,16 @@ |
|||
|
|||
*.sh |
|||
node_modules |
|||
*.md |
|||
*.woff |
|||
*.ttf |
|||
.vscode |
|||
.idea |
|||
dist |
|||
/public |
|||
/docs |
|||
.husky |
|||
.local |
|||
/bin |
|||
Dockerfile |
|||
package.json |
@ -0,0 +1,7 @@ |
|||
module.exports = { |
|||
root: true, |
|||
extends: ['@vben'], |
|||
rules: { |
|||
'no-undef': 'off', |
|||
}, |
|||
}; |
@ -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 |
@ -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: | |
|||
[在这里上传图片] |
@ -0,0 +1 @@ |
|||
blank_issues_enabled: true |
@ -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 |
@ -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 |
@ -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" |
@ -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 |
@ -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 |
@ -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 |
@ -0,0 +1,12 @@ |
|||
dist |
|||
.local |
|||
.output.js |
|||
node_modules |
|||
|
|||
**/*.svg |
|||
**/*.sh |
|||
|
|||
public |
|||
.npmrc |
|||
|
|||
*-lock.yaml |
@ -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', |
|||
}, |
|||
}, |
|||
], |
|||
}; |
@ -0,0 +1,2 @@ |
|||
dist |
|||
public |
@ -0,0 +1,4 @@ |
|||
module.exports = { |
|||
root: true, |
|||
extends: ['@vben/stylelint-config'], |
|||
}; |
@ -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" |
|||
] |
|||
} |
@ -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 |
|||
} |
|||
] |
|||
} |
@ -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" |
|||
} |
@ -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版本 |
@ -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. |
@ -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 /> |
@ -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> |
@ -0,0 +1,9 @@ |
|||
|
|||
*.sh |
|||
node_modules |
|||
*.md |
|||
*.woff |
|||
*.ttf |
|||
.turbo |
|||
dist |
|||
package.json |
@ -0,0 +1,4 @@ |
|||
module.exports = { |
|||
root: true, |
|||
extends: ['@vben/eslint-config/strict'], |
|||
}; |
@ -0,0 +1,10 @@ |
|||
import { defineBuildConfig } from 'unbuild'; |
|||
|
|||
export default defineBuildConfig({ |
|||
clean: true, |
|||
entries: ['src/index', 'src/strict'], |
|||
declaration: true, |
|||
rollup: { |
|||
emitCJS: true, |
|||
}, |
|||
}); |
@ -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" |
|||
} |
|||
} |
@ -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' }, |
|||
}; |
@ -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', |
|||
}, |
|||
}; |
@ -0,0 +1,5 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"extends": "@vben/ts-config/node.json", |
|||
"include": ["src"] |
|||
} |
@ -0,0 +1,9 @@ |
|||
|
|||
*.sh |
|||
node_modules |
|||
*.md |
|||
*.woff |
|||
*.ttf |
|||
.turbo |
|||
dist |
|||
package.json |
@ -0,0 +1,4 @@ |
|||
module.exports = { |
|||
root: true, |
|||
extends: ['@vben/eslint-config/strict'], |
|||
}; |
@ -0,0 +1,10 @@ |
|||
import { defineBuildConfig } from 'unbuild'; |
|||
|
|||
export default defineBuildConfig({ |
|||
clean: true, |
|||
entries: ['src/index'], |
|||
declaration: true, |
|||
rollup: { |
|||
emitCJS: true, |
|||
}, |
|||
}); |
@ -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" |
|||
} |
|||
} |
@ -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'], |
|||
}; |
@ -0,0 +1,5 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"extends": "@vben/ts-config/node.json", |
|||
"include": ["src"] |
|||
} |
@ -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/**"] |
|||
} |
@ -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"] |
|||
} |
@ -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": "./" |
|||
} |
|||
} |
@ -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" |
|||
} |
|||
} |
@ -0,0 +1,10 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"display": "Vue Application", |
|||
"extends": "./base.json", |
|||
"compilerOptions": { |
|||
"jsx": "preserve", |
|||
"lib": ["ESNext", "DOM"], |
|||
"noImplicitAny": false |
|||
} |
|||
} |
@ -0,0 +1,9 @@ |
|||
|
|||
*.sh |
|||
node_modules |
|||
*.md |
|||
*.woff |
|||
*.ttf |
|||
.turbo |
|||
dist |
|||
package.json |
@ -0,0 +1,4 @@ |
|||
module.exports = { |
|||
root: true, |
|||
extends: ['@vben/eslint-config/strict'], |
|||
}; |
@ -0,0 +1,10 @@ |
|||
import { defineBuildConfig } from 'unbuild'; |
|||
|
|||
export default defineBuildConfig({ |
|||
clean: true, |
|||
entries: ['src/index'], |
|||
declaration: true, |
|||
rollup: { |
|||
emitCJS: true, |
|||
}, |
|||
}); |
@ -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" |
|||
} |
|||
} |
@ -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 }; |
@ -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 }; |
@ -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 }; |
@ -0,0 +1,2 @@ |
|||
export * from './config/application'; |
|||
export * from './config/package'; |
@ -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 }; |
@ -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; |
|||
} |
@ -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; |
|||
} |
@ -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 }; |
@ -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; |
|||
} |
@ -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; |
|||
} |
@ -0,0 +1,7 @@ |
|||
/** |
|||
* 解决vite-plugin-purge-icons报错没有类型定义 |
|||
*/ |
|||
declare module 'vite-plugin-purge-icons' { |
|||
const type = any; |
|||
export default type; |
|||
} |
@ -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; |
|||
} |
@ -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 }; |
@ -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
|
|||
}; |
|||
} |
@ -0,0 +1,5 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"extends": "@vben/ts-config/node.json", |
|||
"include": ["src"] |
|||
} |
@ -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,0 +1,4 @@ |
|||
module.exports = { |
|||
root: true, |
|||
extends: ['@vben/eslint-config/strict'], |
|||
}; |
@ -0,0 +1,10 @@ |
|||
import { defineBuildConfig } from 'unbuild'; |
|||
|
|||
export default defineBuildConfig({ |
|||
clean: true, |
|||
entries: ['src/index'], |
|||
declaration: true, |
|||
rollup: { |
|||
emitCJS: true, |
|||
}, |
|||
}); |
@ -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:*" |
|||
} |
|||
} |
@ -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'; |
@ -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 }; |
@ -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 }; |
@ -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 }; |
@ -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 }); |
|||
} |
|||
} |
@ -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>[]); |
|||
} |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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>; |
@ -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>; |
|||
} |
@ -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(); |
|||
} |
|||
}; |
@ -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); |
|||
}); |
|||
}; |
@ -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); |
|||
}; |
|||
}; |
@ -0,0 +1,5 @@ |
|||
export const isBrowser = !!( |
|||
typeof window !== 'undefined' && |
|||
window.document && |
|||
window.document.createElement |
|||
); |
@ -0,0 +1,8 @@ |
|||
import { isBrowser } from './isBrowser'; |
|||
|
|||
export function isDocumentVisible(): boolean { |
|||
if (isBrowser) { |
|||
return document.visibilityState !== 'hidden'; |
|||
} |
|||
return true; |
|||
} |
@ -0,0 +1,2 @@ |
|||
export const isFunction = (value: unknown): value is (...args: any) => any => |
|||
typeof value === 'function'; |
@ -0,0 +1,8 @@ |
|||
import { isBrowser } from './isBrowser'; |
|||
|
|||
export function isOnline(): boolean { |
|||
if (isBrowser && typeof navigator.onLine !== 'undefined') { |
|||
return navigator.onLine; |
|||
} |
|||
return true; |
|||
} |
@ -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); |
|||
}; |
|||
} |
@ -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); |
|||
} |
|||
}; |
|||
} |
@ -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); |
|||
}; |
|||
} |
@ -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 }; |
@ -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 }; |
@ -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…
Reference in new issue