updates
This commit is contained in:
commit
a11329c502
|
|
@ -0,0 +1,73 @@
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: Build and Deploy
|
||||||
|
|
||||||
|
clone:
|
||||||
|
depth: 10
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: go_cache
|
||||||
|
host:
|
||||||
|
path: /data/drone_cache/go_cache
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: restore-cache
|
||||||
|
image: drillster/drone-volume-cache
|
||||||
|
volumes:
|
||||||
|
- name: go_cache
|
||||||
|
path: /cache
|
||||||
|
settings:
|
||||||
|
restore: true
|
||||||
|
mount:
|
||||||
|
- ./.npm-cache
|
||||||
|
- ./node_modules
|
||||||
|
|
||||||
|
- name: build
|
||||||
|
image: node:alpine
|
||||||
|
pull: if-not-exists
|
||||||
|
commands:
|
||||||
|
- export NODE_OPTIONS=--max_old_space_size=6144
|
||||||
|
- echo ${DRONE_BRANCH}
|
||||||
|
- echo ${DRONE_TAG}
|
||||||
|
- echo ${DRONE_COMMIT}
|
||||||
|
- echo ${DRONE_COMMIT:0-7}
|
||||||
|
- npm config set registry https://registry.npmmirror.com
|
||||||
|
- npm install -g pnpm
|
||||||
|
- pnpm config set registry https://registry.npmmirror.com
|
||||||
|
- pnpm i
|
||||||
|
- pnpm build
|
||||||
|
|
||||||
|
- name: rebuild-cache
|
||||||
|
image: drillster/drone-volume-cache
|
||||||
|
volumes:
|
||||||
|
- name: go_cache
|
||||||
|
path: /cache
|
||||||
|
settings:
|
||||||
|
rebuild: true
|
||||||
|
mount:
|
||||||
|
- ./.npm-cache
|
||||||
|
- ./node_modules
|
||||||
|
|
||||||
|
- name: scp files
|
||||||
|
image: appleboy/drone-scp
|
||||||
|
pull: if-not-exists
|
||||||
|
settings:
|
||||||
|
host:
|
||||||
|
from_secret: HOST
|
||||||
|
username:
|
||||||
|
from_secret: USERNAME
|
||||||
|
password:
|
||||||
|
from_secret: PASSWORD
|
||||||
|
port:
|
||||||
|
from_secret: PORT
|
||||||
|
target:
|
||||||
|
from_secret: TARGET_PATH
|
||||||
|
source: dist/*
|
||||||
|
overwrite: true
|
||||||
|
rm: true
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- master
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Editor configuration, see http://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
# the base url of the application, the default is "/"
|
||||||
|
# if use a sub directory, it must be end with "/", like "/admin/" but not "/admin"
|
||||||
|
VITE_BASE_URL=/
|
||||||
|
|
||||||
|
VITE_APP_TITLE=RuoYi Plus Soybean
|
||||||
|
|
||||||
|
VITE_APP_DESC=RuoYi Plus Soybean 后台管理系统
|
||||||
|
|
||||||
|
# the prefix of the icon name
|
||||||
|
VITE_ICON_PREFIX=icon
|
||||||
|
|
||||||
|
# the prefix of the local svg icon component, must include VITE_ICON_PREFIX
|
||||||
|
# format {VITE_ICON_PREFIX}-{local icon name}
|
||||||
|
VITE_ICON_LOCAL_PREFIX=icon-local
|
||||||
|
|
||||||
|
# auth route mode: static | dynamic
|
||||||
|
VITE_AUTH_ROUTE_MODE=dynamic
|
||||||
|
|
||||||
|
# static auth route home
|
||||||
|
VITE_ROUTE_HOME=home
|
||||||
|
|
||||||
|
# default menu icon
|
||||||
|
VITE_MENU_ICON=mdi:menu
|
||||||
|
|
||||||
|
# whether to enable http proxy when is dev mode
|
||||||
|
VITE_HTTP_PROXY=Y
|
||||||
|
|
||||||
|
# vue-router mode: hash | history | memory
|
||||||
|
VITE_ROUTER_HISTORY_MODE=history
|
||||||
|
|
||||||
|
# success code of backend service, when the code is received, the request is successful
|
||||||
|
VITE_SERVICE_SUCCESS_CODE=200
|
||||||
|
|
||||||
|
# logout codes of backend service, when the code is received, the user will be logged out and redirected to login page
|
||||||
|
VITE_SERVICE_LOGOUT_CODES=401
|
||||||
|
|
||||||
|
# modal logout codes of backend service, when the code is received, the user will be logged out by displaying a modal
|
||||||
|
VITE_SERVICE_MODAL_LOGOUT_CODES=401
|
||||||
|
|
||||||
|
# token expired codes of backend service, when the code is received, it will refresh the token and resend the request
|
||||||
|
VITE_SERVICE_EXPIRED_TOKEN_CODES=9999,9998,3333
|
||||||
|
|
||||||
|
# when the route mode is static, the defined super role
|
||||||
|
VITE_STATIC_SUPER_ROLE=R_SUPER
|
||||||
|
|
||||||
|
# sourcemap
|
||||||
|
VITE_SOURCE_MAP=N
|
||||||
|
|
||||||
|
# Used to differentiate storage across different domains
|
||||||
|
VITE_STORAGE_PREFIX=RY_
|
||||||
|
|
||||||
|
# used to control whether the program automatically detects updates
|
||||||
|
VITE_AUTOMATICALLY_DETECT_UPDATE=Y
|
||||||
|
|
||||||
|
# watermark
|
||||||
|
VITE_WATERMARK=N
|
||||||
|
|
||||||
|
# show proxy url log in terminal
|
||||||
|
VITE_PROXY_LOG=Y
|
||||||
|
|
||||||
|
# used to control whether to launch editor
|
||||||
|
# by the way, this plugin is only available in dev mode, not in build mode
|
||||||
|
VITE_DEVTOOLS_LAUNCH_EDITOR=code
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
# backend service base url, test environment
|
||||||
|
VITE_SERVICE_BASE_URL=http://localhost:8080
|
||||||
|
|
||||||
|
VITE_APP_BASE_API=/dev-api
|
||||||
|
|
||||||
|
# watermark
|
||||||
|
VITE_WATERMARK=N
|
||||||
|
# 是否开启 SSE 功能
|
||||||
|
VITE_APP_SSE=Y
|
||||||
|
# 是否开启 websocket 功能
|
||||||
|
VITE_APP_WEBSOCKET=N
|
||||||
|
|
||||||
|
# app client id
|
||||||
|
VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
||||||
|
|
||||||
|
# 记住密码 AES 加密密钥
|
||||||
|
VITE_REMEMBER_ME_AES_KEY=pC4aO6cD2uU7hA0bK6iD4vE1mV8sU8xG
|
||||||
|
|
||||||
|
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
|
||||||
|
VITE_APP_ENCRYPT=Y
|
||||||
|
# AES 加密头标识
|
||||||
|
VITE_HEADER_FLAG=encrypt-key
|
||||||
|
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
|
||||||
|
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
|
||||||
|
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
|
||||||
|
VITE_APP_RSA_PRIVATE_KEY='MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
VITE_APP_BASE_API=/prod-api
|
||||||
|
|
||||||
|
# watermark
|
||||||
|
VITE_WATERMARK=Y
|
||||||
|
# 是否开启 SSE 功能
|
||||||
|
VITE_APP_SSE=Y
|
||||||
|
# 是否开启 websocket 功能
|
||||||
|
VITE_APP_WEBSOCKET=N
|
||||||
|
|
||||||
|
# app client id
|
||||||
|
VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
||||||
|
|
||||||
|
# 记住密码 AES 加密密钥
|
||||||
|
VITE_REMEMBER_ME_AES_KEY=pC4aO6cD2uU7hA0bK6iD4vE1mV8sU8xG
|
||||||
|
|
||||||
|
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
|
||||||
|
VITE_APP_ENCRYPT=Y
|
||||||
|
# AES 加密头标识
|
||||||
|
VITE_HEADER_FLAG=encrypt-key
|
||||||
|
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
|
||||||
|
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
|
||||||
|
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
|
||||||
|
VITE_APP_RSA_PRIVATE_KEY='MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
VITE_APP_BASE_API=/test-api
|
||||||
|
|
||||||
|
# watermark
|
||||||
|
VITE_WATERMARK=Y
|
||||||
|
# 是否开启 SSE 功能
|
||||||
|
VITE_APP_SSE=Y
|
||||||
|
# 是否开启 websocket 功能
|
||||||
|
VITE_APP_WEBSOCKET=N
|
||||||
|
|
||||||
|
# app client id
|
||||||
|
VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
||||||
|
|
||||||
|
# 记住密码 AES 加密密钥
|
||||||
|
VITE_REMEMBER_ME_AES_KEY=pC4aO6cD2uU7hA0bK6iD4vE1mV8sU8xG
|
||||||
|
|
||||||
|
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
|
||||||
|
VITE_APP_ENCRYPT=Y
|
||||||
|
# AES 加密头标识
|
||||||
|
VITE_HEADER_FLAG=encrypt-key
|
||||||
|
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
|
||||||
|
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
|
||||||
|
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
|
||||||
|
VITE_APP_RSA_PRIVATE_KEY='MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
"*.vue" eol=lf
|
||||||
|
"*.js" eol=lf
|
||||||
|
"*.ts" eol=lf
|
||||||
|
"*.jsx" eol=lf
|
||||||
|
"*.tsx" eol=lf
|
||||||
|
"*.mjs" eol=lf
|
||||||
|
"*.json" eol=lf
|
||||||
|
"*.html" eol=lf
|
||||||
|
"*.css" eol=lf
|
||||||
|
"*.scss" eol=lf
|
||||||
|
"*.md" eol=lf
|
||||||
|
"*.yaml" eol=lf
|
||||||
|
"*.yml" eol=lf
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
.VSCodeCounter
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
registry=https://registry.npmmirror.com/
|
||||||
|
shamefully-hoist=true
|
||||||
|
ignore-workspace-root-check=true
|
||||||
|
link-workspace-packages=true
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"afzalsayed96.icones",
|
||||||
|
"antfu.iconify",
|
||||||
|
"antfu.unocss",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"lokalise.i18n-ally",
|
||||||
|
"mhutchie.git-graph",
|
||||||
|
"mikestead.dotenv",
|
||||||
|
"naumovs.color-highlight",
|
||||||
|
"pkief.material-icon-theme",
|
||||||
|
"sdras.vue-vscode-snippets",
|
||||||
|
"vue.volar",
|
||||||
|
"whtouche.vscode-js-console-utils",
|
||||||
|
"zhuangtongfa.material-theme",
|
||||||
|
"tu6ge.naive-ui-intelligence"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Vue Debugger",
|
||||||
|
"url": "http://localhost:9527",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "TS Debugger",
|
||||||
|
"runtimeExecutable": "tsx",
|
||||||
|
"skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"],
|
||||||
|
"program": "${file}",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "never"
|
||||||
|
},
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
"eslint.validate": [
|
||||||
|
"html",
|
||||||
|
"css",
|
||||||
|
"scss",
|
||||||
|
"json",
|
||||||
|
"jsonc",
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact",
|
||||||
|
"vue"
|
||||||
|
],
|
||||||
|
"i18n-ally.displayLanguage": "zh-cn",
|
||||||
|
"i18n-ally.enabledParsers": ["ts"],
|
||||||
|
"i18n-ally.enabledFrameworks": ["vue"],
|
||||||
|
"i18n-ally.editor.preferEditor": true,
|
||||||
|
"i18n-ally.keystyle": "nested",
|
||||||
|
"i18n-ally.localesPaths": ["src/locales/langs"],
|
||||||
|
"i18n-ally.parsers.typescript.compilerOptions": {
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
|
"prettier.enable": false,
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"unocss.root": ["./"],
|
||||||
|
"vue.server.hybridMode": true,
|
||||||
|
"files.exclude": { "/docs": true },
|
||||||
|
"search.exclude": {
|
||||||
|
"/docs": true,
|
||||||
|
"**/dist/**": true,
|
||||||
|
"**/node_modules": true,
|
||||||
|
"node_modules/**": true,
|
||||||
|
},
|
||||||
|
"cSpell.words": ["Axios", "tinymce"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,501 @@
|
||||||
|
# 更新日志
|
||||||
|
|
||||||
|
## [v2.0.0](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.2.1...v2.0.0) (2025-12-25)
|
||||||
|
|
||||||
|
### 🚀 新功能
|
||||||
|
|
||||||
|
- **components**:
|
||||||
|
- 列设置新增滚动条处理 - by @m-xlsea [<samp>(6696d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6696da52)
|
||||||
|
- 优化文件上传组件提示内容 - by @m-xlsea [<samp>(7bd11)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7bd115bf)
|
||||||
|
- 新增预设主题支持 - by @m-xlsea [<samp>(c1063)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/c1063e3e)
|
||||||
|
- **docs**:
|
||||||
|
- 新增 GitCode star 徽章 - by @m-xlsea [<samp>(5310d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/5310d352)
|
||||||
|
- **hooks**:
|
||||||
|
- 优化表格响应数据处理 - by @m-xlsea [<samp>(7d7f2)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7d7f28c4)
|
||||||
|
- 完成表格 Hooks 改造 - by @m-xlsea [<samp>(46996)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4699654f)
|
||||||
|
- 优化树形表格 hooks 封装 - by @m-xlsea [<samp>(ccbb7)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ccbb72c0)
|
||||||
|
- **project**:
|
||||||
|
- 优化业务代码语法格式 - by @m-xlsea [<samp>(7f04b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7f04b119)
|
||||||
|
- **projects**:
|
||||||
|
- 项目适配 Soybean 2.0 - by @m-xlsea
|
||||||
|
- 客户端管理新增状态修改开关 - by @m-xlsea [<samp>(ea6a9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ea6a92cd)
|
||||||
|
- Iframe 类型菜单传参更改 - by @m-xlsea [<samp>(bf3d5)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/bf3d5cb3)
|
||||||
|
- 新增同步租户参数配置功能 - by @m-xlsea [<samp>(901a6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/901a65ad)
|
||||||
|
- 新增关于页面 - by @m-xlsea [<samp>(7d851)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7d85127c)
|
||||||
|
- 菜单新增布局选择支持 - by @m-xlsea [<samp>(13de6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/13de6fbb)
|
||||||
|
- 优化控制台输出和 sql 导入文件 - by @m-xlsea [<samp>(bfb71)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/bfb7169e)
|
||||||
|
- 登录记住密码加密保存 - by @m-xlsea [<samp>(90c52)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/90c52d97)
|
||||||
|
- 使用 highlight.js 替换 monaco-editor - by @m-xlsea [<samp>(7dd7a)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7dd7a936)
|
||||||
|
- 演示页面新增字段排序 demo - by @m-xlsea [<samp>(41c25)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/41c25dcd)
|
||||||
|
- support pinning and unpinning of tabs - by @PChening [<samp>(b8a76)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b8a767d7)
|
||||||
|
- hybrid layout mode auto select first deepest child menu - by @paynezhuang [<samp>(94019)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9401925f)
|
||||||
|
|
||||||
|
### 🐞 Bug 修复
|
||||||
|
|
||||||
|
- **hooks**:
|
||||||
|
- 修复 useTable 获取字段列表问题 - by @m-xlsea [<samp>(0f83c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/0f83cf5f)
|
||||||
|
- update pagination pageSize after data fetch. - by **Azir-11** [<samp>(64226)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/64226d9b)
|
||||||
|
- **projects**:
|
||||||
|
- 修复表单校验问题 - by @m-xlsea [<samp>(62fb9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/62fb9d90)
|
||||||
|
- 修复登录页面 logo 颜色问题 - by @m-xlsea [<samp>(27cae)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/27cae756)
|
||||||
|
- 修复路由 name 与 path 不一致激活菜单异常问题 - by @m-xlsea [<samp>(789a6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/789a6bb9)
|
||||||
|
- fix the incorrect judgment of home by pin tab. - by **Azir-11** [<samp>(62a43)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/62a43c39)
|
||||||
|
- **project**:
|
||||||
|
- 修复导出时查询参数错误问题 - by @m-xlsea [<samp>(52ad9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/52ad93b2)
|
||||||
|
- **table**:
|
||||||
|
- 修复分页数据处理逻辑 - by @imtzc [<samp>(a59fd)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a59fdc58)
|
||||||
|
- **template**:
|
||||||
|
- 调整搜索模块的属性定义位置 - by @imtzc [<samp>(bb039)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/bb039eff)
|
||||||
|
|
||||||
|
### 🛠 优化
|
||||||
|
|
||||||
|
- **projects**:
|
||||||
|
- 修复菜单代码质量问题 - by @m-xlsea [<samp>(6f349)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6f34956e)
|
||||||
|
- 优化注释规范 - by @m-xlsea [<samp>(4139a)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4139a729)
|
||||||
|
- **styles**:
|
||||||
|
- 优化属性表格展开列样式 - by @m-xlsea [<samp>(e40c3)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e40c37a0)
|
||||||
|
|
||||||
|
### 📖 文档
|
||||||
|
|
||||||
|
- **other**:
|
||||||
|
- 优化 sql 插入语句 - by @m-xlsea [<samp>(f7d8d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f7d8d189)
|
||||||
|
- 优化工作流相关菜单 - by @m-xlsea [<samp>(33155)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3315552d)
|
||||||
|
- 移除 cursor 文件夹 - by @m-xlsea [<samp>(5f950)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/5f950b46)
|
||||||
|
- 修复模板处理工具类内容错误 - by @m-xlsea [<samp>(f6dcd)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f6dcded8)
|
||||||
|
- **projects**:
|
||||||
|
- 更新 cursor 规则 - by @m-xlsea [<samp>(e63fe)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e63fee59)
|
||||||
|
|
||||||
|
### 🏡 杂项
|
||||||
|
|
||||||
|
- **deps**:
|
||||||
|
- update deps - by @soybeanjs [<samp>(ec9f9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ec9f9af9)
|
||||||
|
- update umo-editor deps - by @m-xlsea [<samp>(39f8d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/39f8d13b)
|
||||||
|
- **styles**:
|
||||||
|
- format code - by @soybeanjs [<samp>(098cd)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/098cd50e)
|
||||||
|
|
||||||
|
### ❤️ 贡献者
|
||||||
|
|
||||||
|
[](https://github.com/m-xlsea) [](https://github.com/soybeanjs) [](https://github.com/imtzc) [](https://github.com/paynezhuang) [](https://github.com/PChening) [](https://github.com/Azir-11) [](https://github.com/wenyuanw) [](https://github.com/CyberShen) [](https://github.com/Lruihao)
|
||||||
|
[刘璐](mailto:hi.alue@qq.com), [CyberShen123](mailto:s.lijun@qq.com), [whyang](mailto:whyang9701@gmail.com), [HongxuanG](mailto:1359774872@qq.com), [NicholasLD](mailto:878639947@qq.com),
|
||||||
|
|
||||||
|
## [v2.0.0-beta.2](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v2.0.0-beta.1...v2.0.0-beta.2) (2025-12-17)
|
||||||
|
|
||||||
|
### 🚀 新功能
|
||||||
|
|
||||||
|
- **components**:
|
||||||
|
- 列设置新增滚动条处理 - by @m-xlsea [<samp>(6696d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6696da52)
|
||||||
|
- **docs**:
|
||||||
|
- 新增 GitCode star 徽章 - by @m-xlsea [<samp>(5310d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/5310d352)
|
||||||
|
- **hooks**:
|
||||||
|
- 优化表格响应数据处理 - by @m-xlsea [<samp>(7d7f2)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7d7f28c4)
|
||||||
|
- **project**:
|
||||||
|
- 优化业务代码语法格式 - by @m-xlsea [<samp>(7f04b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7f04b119)
|
||||||
|
- **projects**:
|
||||||
|
- 客户端管理新增状态修改开关 - by @m-xlsea [<samp>(ea6a9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ea6a92cd)
|
||||||
|
- Iframe 类型菜单传参更改 - by @m-xlsea [<samp>(bf3d5)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/bf3d5cb3)
|
||||||
|
- 新增同步租户参数配置功能 - by @m-xlsea [<samp>(901a6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/901a65ad)
|
||||||
|
- support pinning and unpinning of tabs - by @PChening [<samp>(b8a76)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b8a767d7)
|
||||||
|
- hybrid layout mode auto select first deepest child menu - by @paynezhuang [<samp>(94019)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9401925f)
|
||||||
|
- 新增关于页面 - by @m-xlsea [<samp>(7d851)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7d85127c)
|
||||||
|
- 菜单新增布局选择支持 - by @m-xlsea [<samp>(13de6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/13de6fbb)
|
||||||
|
- 优化控制台输出和 sql 导入文件 - by @m-xlsea [<samp>(bfb71)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/bfb7169e)
|
||||||
|
- 登录记住密码加密保存 - by @m-xlsea [<samp>(90c52)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/90c52d97)
|
||||||
|
- 使用 highlight.js 替换 monaco-editor - by @m-xlsea [<samp>(7dd7a)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7dd7a936)
|
||||||
|
- 演示页面新增字段排序 demo - by @m-xlsea [<samp>(41c25)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/41c25dcd)
|
||||||
|
|
||||||
|
### 🐞 Bug 修复
|
||||||
|
|
||||||
|
- **hooks**:
|
||||||
|
- update pagination pageSize after data fetch. - by **Azir-11** [<samp>(64226)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/64226d9b)
|
||||||
|
- **project**:
|
||||||
|
- 修复导出时查询参数错误问题 - by @m-xlsea [<samp>(52ad9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/52ad93b2)
|
||||||
|
- **projects**:
|
||||||
|
- fix the incorrect judgment of home by pin tab. - by **Azir-11** [<samp>(62a43)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/62a43c39)
|
||||||
|
- **table**:
|
||||||
|
- 修复分页数据处理逻辑 - by @imtzc [<samp>(a59fd)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a59fdc58)
|
||||||
|
- **template**:
|
||||||
|
- 调整搜索模块的属性定义位置 - by @imtzc [<samp>(bb039)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/bb039eff)
|
||||||
|
|
||||||
|
### 🛠 优化
|
||||||
|
|
||||||
|
- **projects**:
|
||||||
|
- 修复菜单代码质量问题 - by @m-xlsea [<samp>(6f349)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6f34956e)
|
||||||
|
- 优化注释规范 - by @m-xlsea [<samp>(4139a)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4139a729)
|
||||||
|
|
||||||
|
### 📖 文档
|
||||||
|
|
||||||
|
- **other**:
|
||||||
|
- 更新 cursor 规则 - by @m-xlsea [<samp>(1d6af)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/1d6af984)
|
||||||
|
- 优化 sql 插入语句 - by @m-xlsea [<samp>(f7d8d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f7d8d189)
|
||||||
|
- 优化工作流相关菜单 - by @m-xlsea [<samp>(33155)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3315552d)
|
||||||
|
- 移除 cursor 文件夹 - by @m-xlsea [<samp>(5f950)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/5f950b46)
|
||||||
|
- 修复模板处理工具类内容错误 - by @m-xlsea [<samp>(f6dcd)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f6dcded8)
|
||||||
|
|
||||||
|
### 🏡 重构
|
||||||
|
|
||||||
|
- **deps**:
|
||||||
|
- update deps - by @soybeanjs [<samp>(7cf40)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7cf4083b)
|
||||||
|
- update umo-editor deps - by @m-xlsea [<samp>(39f8d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/39f8d13b)
|
||||||
|
- **styles**:
|
||||||
|
- format code - by @soybeanjs [<samp>(098cd)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/098cd50e)
|
||||||
|
|
||||||
|
### ❤️ 贡献者
|
||||||
|
|
||||||
|
[](https://github.com/m-xlsea) [](https://github.com/imtzc) [](https://github.com/soybeanjs) [](https://github.com/paynezhuang) [](https://github.com/PChening) [](https://github.com/Azir-11)
|
||||||
|
|
||||||
|
## [v2.0.0-beta.1](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.2.1...v2.0.0-beta.1) (2025-12-04)
|
||||||
|
|
||||||
|
### 🚀 新功能
|
||||||
|
|
||||||
|
- **projects**:
|
||||||
|
- 项目适配 Soybean 2.0 - by @m-xlsea
|
||||||
|
- **components**:
|
||||||
|
- 新增预设主题支持 - by @m-xlsea [<samp>(c1063)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/c1063e3e)
|
||||||
|
- **hooks**:
|
||||||
|
- 完成表格 Hooks 改造 - by @m-xlsea [<samp>(46996)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4699654f)
|
||||||
|
- 优化树形表格 hooks 封装 - by @m-xlsea [<samp>(ccbb7)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ccbb72c0)
|
||||||
|
|
||||||
|
### 🐞 Bug 修复
|
||||||
|
|
||||||
|
- **projects**:
|
||||||
|
- 修复登录页面 logo 颜色问题 - by @m-xlsea [<samp>(27cae)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/27cae756)
|
||||||
|
- 修复路由 name 与 path 不一致激活菜单异常问题 - by @m-xlsea [<samp>(789a6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/789a6bb9)
|
||||||
|
|
||||||
|
### ❤️ 贡献者
|
||||||
|
|
||||||
|
[](https://github.com/m-xlsea) [](https://github.com/soybeanjs) [](https://github.com/Azir-11) [](https://github.com/wenyuanw) [](https://github.com/CyberShen) [](https://github.com/Lruihao)
|
||||||
|
[刘璐](mailto:hi.alue@qq.com), [CyberShen123](mailto:s.lijun@qq.com), [whyang](mailto:whyang9701@gmail.com), [HongxuanG](mailto:1359774872@qq.com), [NicholasLD](mailto:878639947@qq.com),
|
||||||
|
|
||||||
|
## [v1.2.1](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.2.0...v1.2.1) (2025-10-29)
|
||||||
|
|
||||||
|
### 🚀 新功能
|
||||||
|
|
||||||
|
- **components**:
|
||||||
|
- 菜单树选择组件新增隐藏禁用标识 - by @m-xlsea [<samp>(08cfa)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/08cfa167)
|
||||||
|
- 列设置新增滚动条处理 - by @m-xlsea [<samp>(6696d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6696da52)
|
||||||
|
- **docs**:
|
||||||
|
- 新增 GitCode star 徽章 - by @m-xlsea [<samp>(5310d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/5310d352)
|
||||||
|
- **projects**:
|
||||||
|
- 优化字典操作 - by @m-xlsea [<samp>(2400b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2400bf8c)
|
||||||
|
- 客户端管理新增状态修改开关 - by @m-xlsea [<samp>(ea6a9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ea6a92cd)
|
||||||
|
- Iframe 类型菜单传参更改 - by @m-xlsea [<samp>(bf3d5)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/bf3d5cb3)
|
||||||
|
- 新增同步租户参数配置功能 - by @m-xlsea [<samp>(901a6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/901a65ad)
|
||||||
|
|
||||||
|
### 🐞 Bug 修复
|
||||||
|
|
||||||
|
- **projects**: 修复代码生成树模板问题 - by **AN** [<samp>(fa7bc)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/fa7bc434)
|
||||||
|
|
||||||
|
### 🛠 优化
|
||||||
|
|
||||||
|
- **projects**:
|
||||||
|
- 优化代码内容 - by @m-xlsea [<samp>(9edbd)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9edbd8e6)
|
||||||
|
- 修复菜单代码质量问题 - by @m-xlsea [<samp>(6f349)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6f34956e)
|
||||||
|
|
||||||
|
### 📖 文档
|
||||||
|
|
||||||
|
- **other**: 更新 cursor 规则 - by @m-xlsea [<samp>(1d6af)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/1d6af984)
|
||||||
|
- **projects**: 更新 cursor 规则 - by @m-xlsea [<samp>(e63fe)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e63fee59)
|
||||||
|
|
||||||
|
### 🎨 样式
|
||||||
|
|
||||||
|
- **projects**: 优化注释规范 - by @m-xlsea [<samp>(4139a)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4139a729)
|
||||||
|
|
||||||
|
### ❤️ 贡献值
|
||||||
|
|
||||||
|
[](https://gitee.com/xlsea) [](https://gitee.com/elio-an)
|
||||||
|
|
||||||
|
## [v1.2.0](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.3...v1.2.0) (2025-09-26)
|
||||||
|
|
||||||
|
### 🚀 新功能
|
||||||
|
|
||||||
|
- **components**:
|
||||||
|
- 新增 umodoc 编辑器集成 - by @m-xlsea [<samp>(f182d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f182def5)
|
||||||
|
- **projects**:
|
||||||
|
- 重构登录页面样式 - by @m-xlsea [<samp>(8412a)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8412a8db)
|
||||||
|
- 路由兼容 activeMenu 选项 - by @m-xlsea [<samp>(25ee3)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/25ee3207)
|
||||||
|
- 用户列表新增头像展示 - by @m-xlsea [<samp>(3146c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3146c039)
|
||||||
|
- 新增岗位部门树接口 - by **AN** [<samp>(28101)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/28101cb2)
|
||||||
|
- **styles**:
|
||||||
|
- 优化左侧树形结构样式 - by @m-xlsea [<samp>(513dc)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/513dc31e)
|
||||||
|
- **utils**:
|
||||||
|
- 新增本地 Excel 导出工具类 - by @m-xlsea [<samp>(7f2f3)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7f2f3bd0)
|
||||||
|
|
||||||
|
### 🐞 Bug 修复
|
||||||
|
|
||||||
|
- **components**:
|
||||||
|
- 修复字典标签会修改字典数据值问题 - by @m-xlsea [<samp>(90a14)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/90a14e33)
|
||||||
|
- **hooks**:
|
||||||
|
- 修复下载 hooks 错误未处理 - by @m-xlsea [<samp>(5ef1c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/5ef1c5de)
|
||||||
|
- **packages**:
|
||||||
|
- axios: fix json response. fixed #815 - by @soybeanjs in https://gitee.com/xlsea/ruoyi-plus-soybean/issues/815 [<samp>(fd087)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/fd087f59)
|
||||||
|
- 修复tinymce层级问题 - by **AN** [<samp>(2c248)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2c248d82)
|
||||||
|
- **projects**:
|
||||||
|
- 修改代码生成功能模块名为驼峰时,路由错误问题 - by **AN** [<samp>(2f794)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2f794c4b)
|
||||||
|
- 修复新增部门时不显示上级部门问题 - by **AN** [<samp>(d5bbc)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d5bbc37d)
|
||||||
|
- 修复菜单弹窗打开未清空默认值问题 - by @m-xlsea [<samp>(ad207)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ad207255)
|
||||||
|
- 修复退出登录未清空消息列表问题 - by @m-xlsea [<samp>(dc2fb)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/dc2fbbd5)
|
||||||
|
- 修复菜单默认图标问题 - by @m-xlsea [<samp>(34ab7)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/34ab7d5d)
|
||||||
|
- 修复消息通知字典值未处理问题 - by @m-xlsea [<samp>(3f148)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3f148a4e)
|
||||||
|
- 修复登录页面跳转问题 - by @m-xlsea [<samp>(8aeb7)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8aeb7362)
|
||||||
|
- 修复登录页面样式问题 - by @m-xlsea [<samp>(4e27f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4e27f3b5)
|
||||||
|
- **types**:
|
||||||
|
- fix proxy types - by @soybeanjs [<samp>(12b25)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/12b25e0d)
|
||||||
|
- **utils**:
|
||||||
|
- 修复请求工具响应解密问题 - by @m-xlsea [<samp>(9ef0b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9ef0bd41)
|
||||||
|
|
||||||
|
### 🛠 优化
|
||||||
|
|
||||||
|
- **components**: 补充国际化 - by **AN** [<samp>(ecad1)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ecad1c3e)
|
||||||
|
- **projects**: 字典状态使用枚举值 - by @m-xlsea [<samp>(56fd5)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/56fd5434)
|
||||||
|
|
||||||
|
### 📖 文档
|
||||||
|
|
||||||
|
- **other**: 更新 cursor 规则 - by @m-xlsea [<samp>(e623b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e623b560)
|
||||||
|
|
||||||
|
### 🏡 重构
|
||||||
|
|
||||||
|
- **deps**:
|
||||||
|
- update deps - by @soybeanjs [<samp>(e33f9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e33f944a)
|
||||||
|
- update deps - by @soybeanjs [<samp>(9fa95)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9fa951aa)
|
||||||
|
|
||||||
|
### 🎨 样式
|
||||||
|
|
||||||
|
- **components**: 修改json预览组件样式问题 - by **AN** [<samp>(378aa)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/378aa869)
|
||||||
|
- **styles**: 修复字体样式导致下划线不可见问题 - by **AN** [<samp>(4a424)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4a4244b5)
|
||||||
|
|
||||||
|
### ❤️ 贡献者
|
||||||
|
|
||||||
|
[](https://github.com/m-xlsea) [](https://github.com/soybeanjs) [](https://gitee.com/elio-an)
|
||||||
|
|
||||||
|
## [v1.1.3](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.2...v1.1.3) (2025-08-16)
|
||||||
|
|
||||||
|
### 🐞 Bug 修复
|
||||||
|
|
||||||
|
- **hooks**:
|
||||||
|
- 非安全环境下不使用流式下载 - by @m-xlsea [<samp>(f8983)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f8983557)
|
||||||
|
- 修复oss下载时未转码问题 - by **AN** [<samp>(2d31d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2d31d7dc)
|
||||||
|
- **project**:
|
||||||
|
- 关闭多租户功能后仍然遍历租户列表导致控制台报错的问题 - by **wang_rui** [<samp>(b96c4)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b96c46ba)
|
||||||
|
- 关闭多租户功能后仍然遍历租户列表导致控制台报错的问题 Merge pull request !25 from littleghost2016/dev - by **不寻俗** [<samp>(90276)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9027632b)
|
||||||
|
- **projects**:
|
||||||
|
- 修复一级菜单隐藏失效问题 - by **AN** [<samp>(8fcc7)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8fcc70d7)
|
||||||
|
- 修复日期搜索条件清除问题 - by **AN** [<samp>(52318)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/52318c10)
|
||||||
|
- 修复登录过期事件监听未被重置 - by @m-xlsea [<samp>(71037)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/71037439)
|
||||||
|
- 修复用户新增时角色下拉包含超级管理员问题 - by **AN** [<samp>(a15b6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a15b683b)
|
||||||
|
- 修复用户导入功能无法更新问题 - by **AN** [<samp>(4e983)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4e9839bd)
|
||||||
|
- Fix the icon size in the image preview toolbar - by @m-xlsea [<samp>(4539f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4539fe01)
|
||||||
|
- 修复新增用户未查询角色列表问题 - by **AN** [<samp>(d6ae8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d6ae85d2)
|
||||||
|
- **readme**:
|
||||||
|
- update GitHub stars and forks links for gitee - by @soybeanjs [<samp>(923eb)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/923eb98a)
|
||||||
|
|
||||||
|
### 💅 重构
|
||||||
|
|
||||||
|
- **menu**:
|
||||||
|
- 菜单管理中隐藏的菜单显示灰色 - by **NicholasLD** [<samp>(adca2)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/adca2e26)
|
||||||
|
- 菜单管理中隐藏的菜单显示灰色 Merge pull request !24 from NicholasLD/N/A - by **不寻俗** [<samp>(4eb77)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4eb77eac)
|
||||||
|
- **projects**:
|
||||||
|
- 菜单列表新增禁用菜单样式 - by @m-xlsea [<samp>(e5383)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e538355f)
|
||||||
|
|
||||||
|
### 🏡 杂项
|
||||||
|
|
||||||
|
- **other**: update the ESLint validation configuration to support more file types. - by **Azir-11** [<samp>(8d7f9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8d7f91dc)
|
||||||
|
- **readme**: remove DartNode sponsorship badge from README files - by @soybeanjs [<samp>(33ade)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/33ade539)
|
||||||
|
|
||||||
|
### ❤️ 贡献者
|
||||||
|
|
||||||
|
[](https://github.com/soybeanjs) [](https://github.com/m-xlsea) [](https://gitee.com/elio-an) [](https://github.com/Azir-11) [](https://github.com/NicholasLD)
|
||||||
|
[wang_rui](mailto:wrr1996@163.com)
|
||||||
|
|
||||||
|
## [v1.1.2](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.1...v1.1.2) (2025-07-24)
|
||||||
|
|
||||||
|
### 🐞 Bug 修复
|
||||||
|
|
||||||
|
- 修复 api.d.ts.vm 代码生成模板bug - by **zygalaxy** [<samp>(4e8c8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4e8c8715)
|
||||||
|
- **projects**:
|
||||||
|
- 修复刷新时跳转至登录页问题 - by **AN** [<samp>(2587f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2587f8cb)
|
||||||
|
- 修复登录过期不弹窗问题 - by **AN** [<samp>(e485f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e485f680)
|
||||||
|
- 修复菜单结构变动后路由无法进入问题 - by @m-xlsea [<samp>(f4038)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f4038a2d)
|
||||||
|
|
||||||
|
### 🛠 优化
|
||||||
|
|
||||||
|
- **projects**: 优化搜索框FormItem - by **AN** [<samp>(a1336)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a1336d15)
|
||||||
|
|
||||||
|
### 🏡 杂项
|
||||||
|
|
||||||
|
- **deps**: update deps - by @soybeanjs [<samp>(e89b8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e89b86ce)
|
||||||
|
|
||||||
|
### 🎨 样式
|
||||||
|
|
||||||
|
- **projects**: 搜索FormItem占比调整 - by **AN** [<samp>(cc29e)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/cc29ea85)
|
||||||
|
|
||||||
|
### ❤️ 贡献者
|
||||||
|
|
||||||
|
[](https://github.com/m-xlsea) [](https://gitee.com/elio-an) [](https://github.com/soybeanjs)
|
||||||
|
[zygalaxy](mailto:zygalaxy@qq.com)
|
||||||
|
|
||||||
|
## [v1.1.1](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.0...v1.1.1) (2025-07-11)
|
||||||
|
|
||||||
|
### 🚀 新功能
|
||||||
|
|
||||||
|
- **hooks**:
|
||||||
|
- 重构下载方法,支持流式下载 - by @m-xlsea [<samp>(65067)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/650673e2)
|
||||||
|
- **projects**:
|
||||||
|
- 角色分配用户新增部门与时间查询条件 - by @m-xlsea [<samp>(ad48d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ad48d8e8)
|
||||||
|
- 修改操作后列表查询方式 - by @m-xlsea [<samp>(d8542)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d85424ee)
|
||||||
|
|
||||||
|
### 🐞 Bug 修复
|
||||||
|
|
||||||
|
- **hooks**:
|
||||||
|
- 解决 streamsaver 访问不到 Github 资源问题 - by @m-xlsea [<samp>(566b2)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/566b2c2d)
|
||||||
|
- **other**:
|
||||||
|
- 修复代码生成类型定义文件重复问题 - by @m-xlsea [<samp>(f7c7f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f7c7fc41)
|
||||||
|
- **packages**:
|
||||||
|
- 修复 cleanup 会删除富文本编辑器资源问题 - by @m-xlsea [<samp>(9ca7c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9ca7ca8f)
|
||||||
|
- **projects**:
|
||||||
|
- 修复字典数据重复获取问题 - by @m-xlsea [<samp>(3628c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3628c249)
|
||||||
|
- 修改强退在线设备接口 - by **AN** [<samp>(dbcf8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/dbcf8d42)
|
||||||
|
- 修复代码生成逻辑判断问题 - by **AN** [<samp>(6fc7b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6fc7b11b)
|
||||||
|
- 修复部门字典 sys_normal_disable 重复获取 Merge pull request !11 from 素还真/N/A - by @m-xlsea [<samp>(ad938)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ad9386eb)
|
||||||
|
- 修复未清空文件列表,上传回显问题 - by **AN** [<samp>(229e0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/229e0044)
|
||||||
|
- Fix i18n-ally not working when setting moduleResolution to bundler. fixed #780 - by @xiaobao0505 in https://gitee.com/xlsea/ruoyi-plus-soybean/issues/780 [<samp>(41191)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/41191d54)
|
||||||
|
- 修复角色列表操作栏展示不全问题 - by @m-xlsea [<samp>(62f2c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/62f2c6d5)
|
||||||
|
- 修复用户导入结果信息未渲染标签问题 - by **AN** [<samp>(efc95)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/efc953c0)
|
||||||
|
- 修复角色用户分配未调用接口问题 - by @m-xlsea [<samp>(ff874)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ff87415d)
|
||||||
|
- **styles**:
|
||||||
|
- 修复登录页平板界面滚动问题 - by @m-xlsea [<samp>(90145)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/90145fa5)
|
||||||
|
- **utils**:
|
||||||
|
- 修复isNull和IsNotNull判断方法潜在问题 - by **AN** [<samp>(90d32)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/90d32ee2)
|
||||||
|
|
||||||
|
### 💅 重构
|
||||||
|
|
||||||
|
- **projects**: 调整租户套餐菜单接口 - by **AN** [<samp>(b9999)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b9999935)
|
||||||
|
|
||||||
|
### 📖 文档
|
||||||
|
|
||||||
|
- **other**: 修改文档内容 - by @m-xlsea [<samp>(3ae99)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3ae9922d)
|
||||||
|
- **projects**: 优化 cursor 规则及 mcp - by @m-xlsea [<samp>(a3199)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a31994dc)
|
||||||
|
- **readme**: 更新 README.md 文件 - by @m-xlsea [<samp>(99675)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/99675cbc)
|
||||||
|
|
||||||
|
### 🏡 杂项
|
||||||
|
|
||||||
|
- **deps**:
|
||||||
|
- update NodeJS and pnpm version requirements in package.json and documentation - by **Junior25306** [<samp>(a5c4b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a5c4b4e3)
|
||||||
|
- update deps - by @soybeanjs [<samp>(5cb1c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/5cb1cebd)
|
||||||
|
- update deps - by @soybeanjs [<samp>(aeb63)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/aeb63690)
|
||||||
|
- update deps - by @m-xlsea [<samp>(89c71)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/89c716e1)
|
||||||
|
- **packages**:
|
||||||
|
- update Vite version to 7 in package.json and documentation. - by **Azir** [<samp>(03dd6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/03dd64c5)
|
||||||
|
- **projects**:
|
||||||
|
- update pnpm-lock.yaml - by @m-xlsea [<samp>(7c6ca)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7c6ca91e)
|
||||||
|
- **vscode**:
|
||||||
|
- remove unused vue.server.hybridMode setting from .vscode/settings.json - by @soybeanjs [<samp>(13319)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/133196f3)
|
||||||
|
|
||||||
|
### ❤️ 贡献值
|
||||||
|
|
||||||
|
[](https://github.com/m-xlsea) [](https://github.com/soybeanjs) [](https://github.com/xiaobao0505) [](https://gitee.com/elio-an) [](https://github.com/Azir-11) [Junior25306](mailto:dayu429@qq.com)
|
||||||
|
|
||||||
|
## [v1.1.0](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.0.0...v1.1.0) (2025-07-01)
|
||||||
|
|
||||||
|
### 🚀 新功能
|
||||||
|
|
||||||
|
- **components**:
|
||||||
|
- 新增表单上传组件 - by @m-xlsea [<samp>(03c8a)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/03c8a7f5)
|
||||||
|
- **other**:
|
||||||
|
- 新增菜单字典多语言适配 SQL - by @m-xlsea [<samp>(0f33f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/0f33f4a3)
|
||||||
|
- **projects**:
|
||||||
|
- add configurable user name watermark option - by @wenyuanw [<samp>(7c3da)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7c3dac42)
|
||||||
|
- 菜单字典适配 i18n - by @m-xlsea [<samp>(39dd9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/39dd9acc)
|
||||||
|
- 新增字典多语言适配 - by @m-xlsea [<samp>(8c840)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8c84063a)
|
||||||
|
- **styles**:
|
||||||
|
- 修复登录页移动端显示问题 - by @m-xlsea [<samp>(742e3)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/742e3858)
|
||||||
|
|
||||||
|
### 🐞 Bug 修复
|
||||||
|
|
||||||
|
- **app**:
|
||||||
|
- replace console.error with window.console.error for consistency - by @soybeanjs [<samp>(7d840)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7d84062e)
|
||||||
|
- **auth**:
|
||||||
|
- remove redundant authStore declaration in resetStore function - by @soybeanjs [<samp>(c57f8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/c57f88aa)
|
||||||
|
- **components**:
|
||||||
|
- 修复菜单树选择组件 - by @m-xlsea [<samp>(bbda8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/bbda803e)
|
||||||
|
- 修复树选择组件再次勾选父子联动导致全选问题 - by @m-xlsea [<samp>(aeb73)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/aeb736eb)
|
||||||
|
- 修复部门选择组件非树结构,默认展开失败问题 - by **AN** [<samp>(da1c1)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/da1c16e0)
|
||||||
|
- 修复上传组件回显问题,修改accept参数逻辑 - by **AN** [<samp>(e16a0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e16a0fa6)
|
||||||
|
- 修复菜单选择标签渲染问题 - by @m-xlsea [<samp>(6e6cc)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6e6cc4d9)
|
||||||
|
- **other**:
|
||||||
|
- 修复代码生成问题 - by @m-xlsea [<samp>(1ec10)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/1ec10991)
|
||||||
|
- 代码生成模板 dateRangeTime 错误 - by @m-xlsea [<samp>(f0810)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f0810bce)
|
||||||
|
- 修复代码生成字典相关问题 - by @m-xlsea [<samp>(94d18)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/94d1863e)
|
||||||
|
- 修复代码生成类型定义文件重复问题 - by @m-xlsea [<samp>(f7c7fc41)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f7c7fc41)
|
||||||
|
- **projects**:
|
||||||
|
- 修复自定义数据权限没有保存角色部门bug - by **AN** [<samp>(a0f33)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a0f33664)
|
||||||
|
- 修复登录过期后,重复弹窗问题 - by **AN** [<samp>(cafee)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/cafee1db)
|
||||||
|
- 修复首页未从环境变量获取问题 - by @m-xlsea [<samp>(031b7)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/031b7f69)
|
||||||
|
- 修复导出查询参数问题 - by @m-xlsea [<samp>(ffa47)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ffa47c37)
|
||||||
|
- 修复权限字符显示逻辑错误问题 - by **AN** [<samp>(0ac0a)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/0ac0a093)
|
||||||
|
- 目录类型禁用iframe选项 - by **AN** [<samp>(72b8f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/72b8f56e)
|
||||||
|
- 修复切换用户或登录过期部分问题 - by @m-xlsea [<samp>(27f06)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/27f06195)
|
||||||
|
- 修复接口请求异常拦截问题 - by @m-xlsea [<samp>(031d0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/031d071a)
|
||||||
|
- 修复个人信息-修改密码未加密且参数错误问题 - by **AN** [<samp>(8b315)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8b3151b8)
|
||||||
|
- 调整属性名 - by **AN** [<samp>(62e6c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/62e6c776)
|
||||||
|
- ensure proper text color when themes are inverted - by @wenyuanw [<samp>(afd60)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/afd60421)
|
||||||
|
- **styles**:
|
||||||
|
- 添加滚动条,去除页码 - by **AN** [<samp>(d37ad)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d37adc36)
|
||||||
|
- **types**:
|
||||||
|
- The environment variable VITE_ICON_LOCAL_PREFIX has the wrong type. - by **chenziwen** [<samp>(da149)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/da149e5b)
|
||||||
|
- **utils**:
|
||||||
|
- 修复 删除当前tab为最后一个时,tab切换错误bug. - by **AN** [<samp>(64bd1)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/64bd119c)
|
||||||
|
|
||||||
|
### 🛠 优化
|
||||||
|
|
||||||
|
- **components**:
|
||||||
|
- optimize spacing for lang-switch dropdown options - by @wenyuanw [<samp>(fcb89)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/fcb89883)
|
||||||
|
- **projects**:
|
||||||
|
- optimize tab deletion logic. closed #755 - by @wenyuanw in https://gitee.com/xlsea/ruoyi-plus-soybean/issues/755 [<samp>(e6044)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e6044d0f)
|
||||||
|
- optimize tab deletion logic - by **AN** [<samp>(858c3)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/858c3180)
|
||||||
|
- 优化接口请求异常拦截代码 - by @m-xlsea [<samp>(47191)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/471912e1)
|
||||||
|
|
||||||
|
### 💅 重构
|
||||||
|
|
||||||
|
- **iframe-page**: remove unused lifecycle hooks and clean up script setup - by @soybeanjs [<samp>(276d8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/276d836c)
|
||||||
|
- **projects**: 补充formTip信息 - by **AN** [<samp>(f36ac)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f36ac9ab)
|
||||||
|
|
||||||
|
### 📖 文档
|
||||||
|
|
||||||
|
- **readme**:
|
||||||
|
- 更新 README.md 文件 - by @m-xlsea [<samp>(99675cb)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/99675cb)
|
||||||
|
|
||||||
|
### 🏡 杂项
|
||||||
|
|
||||||
|
- **deps**:
|
||||||
|
- update deps - by @soybeanjs [<samp>(3e4e1)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3e4e17ab)
|
||||||
|
- update deps - by @soybeanjs [<samp>(dc674)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/dc674ce8)
|
||||||
|
- update deps - by @m-xlsea [<samp>(fec05)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/fec0563e)
|
||||||
|
- **projects**:
|
||||||
|
- 移除未使用代码 - by **AN** [<samp>(d141e)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d141ed5b)
|
||||||
|
- update deps & fix `moduleResolution` - by @soybeanjs [<samp>(dbd99)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/dbd995c1)
|
||||||
|
|
||||||
|
### 🎨 样式
|
||||||
|
|
||||||
|
- **projects**:
|
||||||
|
- 更换 logo 与加载样式 - by @m-xlsea [<samp>(7e4ec)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7e4ecae6)
|
||||||
|
- 重构登录页样式 - by @m-xlsea [<samp>(40680)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/406800de)
|
||||||
|
- 修改按钮文本颜色 - by @m-xlsea [<samp>(907f0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/907f0439)
|
||||||
|
- 优化移动端字体大小 - by @m-xlsea [<samp>(8b4e4)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8b4e41ce)
|
||||||
|
|
||||||
|
### ❤️ 贡献者
|
||||||
|
|
||||||
|
[](https://gitee.com/xlsea) [](https://github.com/soybeanjs) [](https://github.com/wenyuanw) [](https://gitee.com/elio-an) [](https://github.com/chen-ziwen)
|
||||||
|
[](https://gitee.com/wangzhongqi0917) [](https://gitee.com/qq1822213252) [](https://gitee.com/tangzc), [metabytes](https://gitee.com/metabytes)
|
||||||
|
|
||||||
|
|
||||||
|
## [v1.0.0](https://gitee.com/xlsea/ruoyi-plus-soybean/releases/tag/v1.0.0) (2025-06-05)
|
||||||
|
|
||||||
|
### 🚀 新功能
|
||||||
|
|
||||||
|
1.0.0 版本正式发布,此版本不包含工作流与多语言,请期待后续版本发布。
|
||||||
|
|
||||||
|
### ❤️ 贡献者
|
||||||
|
|
||||||
|
首次发版不展示过多贡献者,敬请谅解
|
||||||
|
|
||||||
|
[](https://github.com/honghuangdc) [](https://gitee.com/xlsea) [](https://gitee.com/elio-an) [](https://github.com/wangqiqi95)
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 xlsea
|
||||||
|
|
||||||
|
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,391 @@
|
||||||
|
<div align="center">
|
||||||
|
<img src="https://docs.ruoyi.xlsea.cn/logo.svg" width="160">
|
||||||
|
<h1>RuoYi-Plus-Soybean</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="height: 10px; clear: both;"></div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<p>一个基于 <a href="https://gitee.com/dromara/RuoYi-Vue-Plus" target="_blank">RuoYi-Vue-Plus</a> 的后端能力和 <a href="https://github.com/soybeanjs/soybean-admin" target="_blank">Soybean Admin</a> 前端特性的现代化多租户管理系统</p>
|
||||||
|
<p>
|
||||||
|
<a href="https://gitcode.com/xlsea/ruoyi-plus-soybean" target="_blank"><img src="https://gitcode.com/xlsea/ruoyi-plus-soybean/star/badge.svg" alt="GitCode"></a>
|
||||||
|
<a href="https://github.com/m-xlsea/ruoyi-plus-soybean" target="_blank"><img src="https://img.shields.io/github/stars/m-xlsea/ruoyi-plus-soybean" alt="Github"></a>
|
||||||
|
<a href="https://gitee.com/xlsea/ruoyi-plus-soybean" target="_blank"><img src="https://gitee.com/xlsea/ruoyi-plus-soybean/badge/star.svg" alt="Gitee"></a>
|
||||||
|
<a href="https://vuejs.org" target="_blank"><img src="https://img.shields.io/badge/Vue-3.5-brightgreen" alt="vue"></a>
|
||||||
|
<a href="https://www.typescriptlang.org" target="_blank"><img src="https://img.shields.io/badge/TypeScript-5.8-blue" alt="typescript"></a>
|
||||||
|
<a href="https://vite.dev" target="_blank"><img src="https://img.shields.io/badge/Vite-6.2-orange" alt="vite"></a>
|
||||||
|
<a href="https://www.naiveui.com" target="_blank"><img src="https://img.shields.io/badge/NaiveUI-2.41-purple" alt="naive-ui"></a>
|
||||||
|
<a href="./LICENSE" target="_blank"><img src="https://img.shields.io/badge/License-MIT-yellow" alt="license"></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
# 📢 重要通知
|
||||||
|
|
||||||
|
2.0.0 版本已经正式发布(工作流版本请切换 [flow](https://gitee.com/xlsea/ruoyi-plus-soybean/tree/flow/) 分支查看),但仍然建议:
|
||||||
|
- 在生产环境使用前进行充分测试
|
||||||
|
- 关注项目更新,及时获取最新版本
|
||||||
|
- 积极反馈问题,帮助我们快速迭代
|
||||||
|
|
||||||
|
**后续规划**
|
||||||
|
- 多语言国际化完善
|
||||||
|
- 性能优化和稳定性提升
|
||||||
|
|
||||||
|
> 如果对该项目感兴趣,可以给一个 Star 支持一下,谢谢!
|
||||||
|
> 请大家踊跃提交 PR 和 Issue,一起完善这个项目
|
||||||
|
|
||||||
|
# ❗开发前必看
|
||||||
|
|
||||||
|
<p style="font-weight: bold; font-size: 24px;">本项目强制使用 pnpm 构建,详细请看 <a href="#安装步骤及说明">安装步骤及说明</a></p>
|
||||||
|
|
||||||
|
<p style="font-weight: bold; font-size: 24px;">后端需要替换代码生成模板与菜单 SQL,详细请看 <a href="#代码生成与菜单更新">代码生成与菜单更新</a></p>
|
||||||
|
|
||||||
|
# 💎 友情链接
|
||||||
|
|
||||||
|
- [Snail Job Pro](https://pro.snailjob.opensnail.com/home) - 灵活,可靠和快速的分布式任务重试和分布式任务调度平台
|
||||||
|
- [AiZuDa - 爱组搭(飞龙工作流企业版)](https://naiveui.aizuda.com) - 像搭积木一样进行低代码甚至零代码快速构建应用
|
||||||
|
|
||||||
|
## 📋 项目概述
|
||||||
|
|
||||||
|
RuoYi-Plus-Soybean 是一个现代化的企业级多租户管理系统,它结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。
|
||||||
|
|
||||||
|
### 🌟 项目特点
|
||||||
|
|
||||||
|
- **多租户架构**:完整支持SaaS多租户模式,灵活的租户管理能力
|
||||||
|
- **现代前端技术栈**:基于Vue 3、TypeScript、Vite和Naive UI构建
|
||||||
|
- **Monorepo工程管理**:使用pnpm workspaces管理多包结构
|
||||||
|
- **丰富的组件库**:内置大量业务组件和布局选项
|
||||||
|
- **主题定制**:支持多种布局模式和主题配色
|
||||||
|
- **国际化**:内置多语言支持
|
||||||
|
- **权限管理**:精细的基于角色的权限控制
|
||||||
|
|
||||||
|
## 🛠️ 技术栈
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- **核心框架**:Vue 3.5.x
|
||||||
|
- **开发语言**:TypeScript 5.8.x
|
||||||
|
- **构建工具**:Vite 6.2.x
|
||||||
|
- **UI组件库**:Naive UI 2.41.x
|
||||||
|
- **状态管理**:Pinia 3.0.x
|
||||||
|
- **路由**:Vue Router 4.5.x
|
||||||
|
- **HTTP客户端**:Axios/Alova
|
||||||
|
- **CSS**:UnoCSS
|
||||||
|
- **包管理器**:pnpm 8.x+
|
||||||
|
|
||||||
|
### 后端(与RuoYi-Vue-Plus兼容)
|
||||||
|
- **核心框架**:Spring Boot
|
||||||
|
- **安全框架**:Spring Security
|
||||||
|
- **权限认证**:Sa-Token
|
||||||
|
- **数据操作**:MyBatis-Plus
|
||||||
|
- **数据库**:MySQL
|
||||||
|
|
||||||
|
## 🏗️ 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
root
|
||||||
|
├── build # 构建配置和插件
|
||||||
|
│ ├── config # 构建配置文件
|
||||||
|
│ └── plugins # Vite 插件
|
||||||
|
├── docs # 文档和模板
|
||||||
|
│ ├── java # 代码生成工具类
|
||||||
|
│ └── template # 代码生成模板
|
||||||
|
├── packages # Monorepo包
|
||||||
|
│ ├── alova # 使用Alova的HTTP客户端实现
|
||||||
|
│ ├── axios # 使用Axios的HTTP客户端实现
|
||||||
|
│ ├── color # 颜色管理工具
|
||||||
|
│ ├── hooks # 可复用的Vue组合函数
|
||||||
|
│ ├── materials # UI组件和材料
|
||||||
|
│ ├── ofetch # 使用ofetch的HTTP客户端实现
|
||||||
|
│ ├── scripts # 构建和开发脚本
|
||||||
|
│ ├── uno-preset # UnoCSS预设配置
|
||||||
|
│ └── utils # 通用工具函数
|
||||||
|
├── public # 静态资源
|
||||||
|
├── src # 主应用源代码
|
||||||
|
│ ├── assets # 静态资源(图片、图标)
|
||||||
|
│ ├── components # 可复用的 Vue 组件
|
||||||
|
│ ├── constants # 应用常量
|
||||||
|
│ ├── enum # TypeScript 枚举
|
||||||
|
│ ├── hooks # Vue 组合函数
|
||||||
|
│ ├── layouts # 页面布局
|
||||||
|
│ ├── locales # 国际化
|
||||||
|
│ ├── plugins # Vue 插件
|
||||||
|
│ ├── router # Vue Router 配置
|
||||||
|
│ ├── service # API 服务
|
||||||
|
│ ├── store # Pinia 存储模块
|
||||||
|
│ ├── styles # 全局样式
|
||||||
|
│ ├── theme # 主题配置
|
||||||
|
│ ├── typings # TypeScript 类型定义
|
||||||
|
│ ├── utils # 工具函数
|
||||||
|
│ └── views # 页面组件
|
||||||
|
└── vite.config.ts # Vite 配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 环境要求与安装
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
- Node.js >= 20.19.0
|
||||||
|
- pnpm >= 10.5.0
|
||||||
|
- Git
|
||||||
|
|
||||||
|
### 安装步骤及说明
|
||||||
|
|
||||||
|
1. 克隆仓库
|
||||||
|
```bash
|
||||||
|
git clone https://gitee.com/xlsea/ruoyi-plus-soybean.git
|
||||||
|
cd ruoyi-plus-soybean
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 安装 pnpm (如果未安装)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install pnpm -g
|
||||||
|
```
|
||||||
|
|
||||||
|
设置淘宝镜像
|
||||||
|
```bash
|
||||||
|
pnpm config set registry https://registry.npmmirror.com
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 安装依赖
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 运行开发服务器
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 构建生产版本
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码生成与菜单更新
|
||||||
|
|
||||||
|
项目提供了代码生成工具和菜单SQL更新文件,在 <a href="https://gitee.com/xlsea/ruoyi-plus-soybean/tree/master/docs" target="_blank">docs</a> 目录下:
|
||||||
|
|
||||||
|
- **代码生成工具**
|
||||||
|
- 代码生成工具类位于 `docs/java` 目录,如果没有修改过VelocityUtils.java文件,直接替换即可
|
||||||
|
- 代码生成模板位于 `docs/template` 目录,请在ruoyi-generator模块的`resource/vm`下新建 `soy`文件夹,并将所有模板拷贝至`soy`文件夹中
|
||||||
|
|
||||||
|
- **菜单SQL更新**
|
||||||
|
- 菜单数据更新SQL文件位于 `docs/sql` 目录
|
||||||
|
- 在系统初始化或更新时,需要执行相应的SQL文件来更新菜单数据
|
||||||
|
|
||||||
|
## 📝 开发指南
|
||||||
|
|
||||||
|
### 可用的脚本命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发环境
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# 测试环境
|
||||||
|
pnpm dev:test
|
||||||
|
|
||||||
|
# 生产环境
|
||||||
|
pnpm dev:prod
|
||||||
|
|
||||||
|
# 构建生产版本
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# 构建开发版本
|
||||||
|
pnpm build:dev
|
||||||
|
|
||||||
|
# 构建测试版本
|
||||||
|
pnpm build:test
|
||||||
|
|
||||||
|
# 预览构建
|
||||||
|
pnpm preview
|
||||||
|
|
||||||
|
# 类型检查
|
||||||
|
pnpm typecheck
|
||||||
|
|
||||||
|
# 代码规范检查并修复
|
||||||
|
pnpm lint
|
||||||
|
|
||||||
|
# 路由生成
|
||||||
|
pnpm gen-route
|
||||||
|
|
||||||
|
# 提交代码
|
||||||
|
pnpm commit
|
||||||
|
|
||||||
|
# 中文提交信息
|
||||||
|
pnpm commit:zh
|
||||||
|
|
||||||
|
# 依赖包更新
|
||||||
|
pnpm update-pkg
|
||||||
|
|
||||||
|
# 清理项目
|
||||||
|
pnpm cleanup
|
||||||
|
|
||||||
|
# 发布新版本
|
||||||
|
pnpm release
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码规范与风格
|
||||||
|
|
||||||
|
项目使用ESLint进行代码检查,遵循以下规范:
|
||||||
|
|
||||||
|
- **命名规范**:
|
||||||
|
- Vue组件: PascalCase (如 UserProfile.vue)
|
||||||
|
- TypeScript文件: camelCase (如 userService.ts)
|
||||||
|
- CSS/SCSS: kebab-case (如 user-profile.scss)
|
||||||
|
|
||||||
|
- **代码风格**:
|
||||||
|
- 使用Vue 3 Composition API
|
||||||
|
- 使用TypeScript类型系统
|
||||||
|
- 遵循单一职责原则
|
||||||
|
|
||||||
|
### 核心开发模式
|
||||||
|
|
||||||
|
#### 状态管理
|
||||||
|
使用Pinia进行状态管理,模块位于`src/store/modules`目录:
|
||||||
|
- **app**: 应用全局状态
|
||||||
|
- **theme**: 主题配置
|
||||||
|
- **route**: 路由信息
|
||||||
|
- **tab**: 标签页管理
|
||||||
|
- **auth**: 认证信息
|
||||||
|
- **dict**: 字典管理
|
||||||
|
- **notice**: 通知管理
|
||||||
|
|
||||||
|
#### API交互
|
||||||
|
项目支持多种HTTP客户端实现:
|
||||||
|
|
||||||
|
- **Axios**:
|
||||||
|
```typescript
|
||||||
|
import { useRequest } from '@/hooks/common/request';
|
||||||
|
|
||||||
|
const { data, loading, error } = useRequest(() => api.getData(params));
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Hooks使用**:
|
||||||
|
```typescript
|
||||||
|
// 布尔值管理
|
||||||
|
import { useBoolean } from '@sa/hooks';
|
||||||
|
const { bool, setTrue, setFalse } = useBoolean();
|
||||||
|
|
||||||
|
// 加载状态管理
|
||||||
|
import { useLoading } from '@sa/hooks';
|
||||||
|
const { loading, startLoading, endLoading } = useLoading();
|
||||||
|
|
||||||
|
// 表格管理
|
||||||
|
import { useTable } from '@/hooks/common/table';
|
||||||
|
const { tableData, loading, getPaginationData } = useTable(fetchTableData);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 组件使用
|
||||||
|
项目包含多种业务组件:
|
||||||
|
|
||||||
|
- **表格组件**:支持列设置、搜索区域和高级操作
|
||||||
|
- **表单组件**:集成验证和表单布局
|
||||||
|
- **字典组件**:字典选择、标签和单选
|
||||||
|
- **布局组件**:支持多种布局模式和主题
|
||||||
|
|
||||||
|
### UnoCSS使用指南
|
||||||
|
项目优先使用 UnoCSS 来实现样式:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="flex flex-col items-center justify-center p-4 m-2 bg-blue-100 dark:bg-blue-800 rounded-md">
|
||||||
|
<span class="text-lg font-bold text-center">内容</span>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 国际化
|
||||||
|
项目使用vue-i18n实现国际化支持:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在组件中使用
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
console.log(t('common.confirm'));
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💎 特性与功能
|
||||||
|
|
||||||
|
### 前端特性
|
||||||
|
- **多种布局模式**:支持垂直、水平、混合等多种布局
|
||||||
|
- **可配置的主题**:明暗模式、主题色定制
|
||||||
|
- **标签页管理**:多种标签风格、右键菜单
|
||||||
|
- **组件封装**:进度条、图标、加载动画等
|
||||||
|
- **路由生成**:基于目录结构的路由生成
|
||||||
|
- **权限管理**:菜单和按钮级别的权限控制
|
||||||
|
|
||||||
|
### 业务功能
|
||||||
|
- **用户管理**:用户信息维护、角色分配
|
||||||
|
- **角色管理**:角色权限配置
|
||||||
|
- **菜单管理**:系统功能配置
|
||||||
|
- **部门管理**:组织架构维护
|
||||||
|
- **字典管理**:数据字典配置
|
||||||
|
- **租户管理**:多租户配置
|
||||||
|
- **系统监控**:登录日志、操作日志、在线用户、缓存监控
|
||||||
|
- **代码生成**:生成前后端代码,提升开发效率
|
||||||
|
|
||||||
|
## 🤝 贡献指南
|
||||||
|
|
||||||
|
### 开发流程
|
||||||
|
1. Fork项目
|
||||||
|
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. 提交更改 (`git commit -m 'feat: add amazing feature'`)
|
||||||
|
4. 推送到分支 (`git push origin feature/amazing-feature`)
|
||||||
|
5. 提交Pull Request
|
||||||
|
|
||||||
|
### 提交规范
|
||||||
|
项目使用约定式提交规范:
|
||||||
|
|
||||||
|
- `feat`: 新功能
|
||||||
|
- `fix`: 修复Bug
|
||||||
|
- `docs`: 文档更新
|
||||||
|
- `style`: 代码风格调整
|
||||||
|
- `refactor`: 代码重构
|
||||||
|
- `perf`: 性能优化
|
||||||
|
- `test`: 测试代码
|
||||||
|
- `chore`: 构建或工具变动
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
[MIT License](./LICENSE)
|
||||||
|
|
||||||
|
## 🔗 相关链接
|
||||||
|
|
||||||
|
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - 后端基础框架
|
||||||
|
- [Soybean Admin](https://github.com/soybeanjs/soybean-admin) - 前端基础框架
|
||||||
|
- [RuoYi-Plus-Soybean](https://ruoyi.xlsea.cn) - 官方演示站点
|
||||||
|
- [RuoYi-Plus-Soybean-Docs](https://docs.ruoyi.xlsea.cn) - 项目文档
|
||||||
|
- [Open Hives](https://openhives.com/questions) - OpenHives 问答社区
|
||||||
|
|
||||||
|
## 📮 联系方式
|
||||||
|
|
||||||
|
- **作者**: xlsea
|
||||||
|
- **邮箱**: m@xlsea.cn
|
||||||
|
- **作者主页**: https://gitee.com/xlsea
|
||||||
|
|
||||||
|
更多周边生态请翻阅 [周边生态](https://docs.soybeanjs.cn/zh/awesome) 文档。
|
||||||
|
|
||||||
|
|
||||||
|
- **作者**: Elio
|
||||||
|
- **邮箱**: 1983933789@qq.com
|
||||||
|
- **作者主页**: https://gitee.com/ahcode
|
||||||
|
|
||||||
|
## 💬 交流群
|
||||||
|
|
||||||
|
**加群前请先阅读一下内容:**
|
||||||
|
|
||||||
|
- 禁止内容:黄腔、暴力言论、政治话题,违者直接飞机票(踢出群)
|
||||||
|
- 遇到问题请先阅读 [项目文档](https://docs.ruoyi.xlsea.cn) 和 [Soybean 文档](https://docs.soybeanjs.cn/),某些简单问题不予理睬
|
||||||
|
- 蜡笔小新头像为机器人助手,私聊不保证回复,问题请在群内讨论
|
||||||
|
|
||||||
|
<img src="https://foruda.gitee.com/images/1749174520085305975/ad1b54fe_5601833.png" width="300px" />
|
||||||
|
|
||||||
|
添加作者微信备注:加群
|
||||||
|
|
||||||
|
## 🧧 捐献作者
|
||||||
|
|
||||||
|
作者为兼职做开源,平时还需要工作,如果帮到了您可以请作者吃个盒饭
|
||||||
|
|
||||||
|
<img src="https://foruda.gitee.com/images/1746840166037207866/f8c6f06b_5601833.png" width="300px" height="300px" />
|
||||||
|
|
||||||
|
## 🫡 捐赠列表
|
||||||
|
|
||||||
|
**捐赠列表已移至 [捐赠列表](https://docs.ruoyi.xlsea.cn/other/donate.html)**
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
【开发需求】
|
||||||
|
1. 功能扩展:在前端页面 `@src/views/art/school/index.vue` 的院校列表操作列中,新增子表操作按钮(院校名称管理、校区管理、专业管理等),按钮需绑定弹窗触发事件。
|
||||||
|
2. 组件适配:基于已完成的子模块基础代码(路径:`src/views/art/school/modules/` 下 school-campus、school-college 等所有子组件),统一调整组件渲染形态为弹窗样式,需符合现有项目的弹窗UI规范。
|
||||||
|
3. 数据关联:实现列表按钮点击时,将当前行的院校ID作为入参传递至对应子模块弹窗,确保子模块接口调用时携带该关联ID,完成子表数据与主表院校的关联管理。
|
||||||
|
4. 接口对接:子模块需对接指定的TS接口文件(路径:`src/service/api/art/` 下 school-campus.ts、school-college.ts 等),并引用对应的TS类型定义文件(路径:`src/typings/api/` 下 art.school-campus.api.d.ts、art.school-college.api.d.ts 等),保证类型校验与接口调用的规范性。
|
||||||
|
|
||||||
|
【文件路径清单】
|
||||||
|
- 主列表页面:@src/views/art/school/index.vue
|
||||||
|
- 子模块组件:src/views/art/school/modules/(school-campus/college/detail/dorm/enroll-plan/major/major-tag/media/name/tag)
|
||||||
|
- 接口层文件:src/service/api/art/(school-campus.ts/college.ts/detail.ts/dorm.ts/enroll-plan.ts/major-tag.ts/major.ts/media.ts/name.ts/tag.ts)
|
||||||
|
- 类型定义文件:src/typings/api/(art.school-campus.api.d.ts/college.api.d.ts/detail.api.d.ts/dorm.api.d.ts/enroll-plan.api.d.ts/major-tag.api.d.ts/major.api.d.ts/media.api.d.ts/name.api.d.ts/tag.api.d.ts/art.school.api.d.ts)
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './proxy';
|
||||||
|
export * from './time';
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import type { ProxyOptions } from 'vite';
|
||||||
|
import { bgRed, bgYellow, green, lightBlue } from 'kolorist';
|
||||||
|
import { consola } from 'consola';
|
||||||
|
import { createServiceConfig } from '../../src/utils/service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set http proxy
|
||||||
|
*
|
||||||
|
* @param env - The current env
|
||||||
|
* @param enable - If enable http proxy
|
||||||
|
*/
|
||||||
|
export function createViteProxy(env: Env.ImportMeta, enable: boolean) {
|
||||||
|
const isEnableHttpProxy = enable && env.VITE_HTTP_PROXY === 'Y';
|
||||||
|
|
||||||
|
if (!isEnableHttpProxy) return undefined;
|
||||||
|
|
||||||
|
const isEnableProxyLog = env.VITE_PROXY_LOG === 'Y';
|
||||||
|
|
||||||
|
const { baseURL, proxyPattern, ws, other } = createServiceConfig(env);
|
||||||
|
|
||||||
|
const proxy: Record<string, ProxyOptions> = createProxyItem({ baseURL, ws, proxyPattern }, isEnableProxyLog);
|
||||||
|
|
||||||
|
other.forEach(item => {
|
||||||
|
Object.assign(proxy, createProxyItem(item, isEnableProxyLog));
|
||||||
|
});
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean) {
|
||||||
|
const proxy: Record<string, ProxyOptions> = {};
|
||||||
|
|
||||||
|
proxy[item.proxyPattern] = {
|
||||||
|
target: item.baseURL,
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: item.ws,
|
||||||
|
configure: (_proxy, options) => {
|
||||||
|
_proxy.on('proxyReq', (_proxyReq, req, _res) => {
|
||||||
|
if (!enableLog) return;
|
||||||
|
|
||||||
|
const requestUrl = `${lightBlue('[proxy url]')}: ${bgYellow(` ${req.method} `)} ${green(`${item.proxyPattern}${req.url}`)}`;
|
||||||
|
|
||||||
|
const proxyUrl = `${lightBlue('[real request url]')}: ${green(`${options.target}${req.url}`)}`;
|
||||||
|
|
||||||
|
consola.log(`\n${requestUrl}\n${proxyUrl}`);
|
||||||
|
});
|
||||||
|
_proxy.on('error', (_err, req, _res) => {
|
||||||
|
if (!enableLog) return;
|
||||||
|
consola.log(bgRed(`Error: ${req.method} `), green(`${options.target}${req.url}`));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
rewrite: path => path.replace(new RegExp(`^${item.proxyPattern}`), '')
|
||||||
|
};
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import utc from 'dayjs/plugin/utc';
|
||||||
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
|
|
||||||
|
export function getBuildTime() {
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
|
const buildTime = dayjs.tz(Date.now(), 'Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
|
||||||
|
return buildTime;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import VueDevtools from 'vite-plugin-vue-devtools';
|
||||||
|
|
||||||
|
export function setupDevtoolsPlugin(viteEnv: Env.ImportMeta) {
|
||||||
|
const { VITE_DEVTOOLS_LAUNCH_EDITOR } = viteEnv;
|
||||||
|
|
||||||
|
return VueDevtools({
|
||||||
|
launchEditor: VITE_DEVTOOLS_LAUNCH_EDITOR
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import type { Plugin } from 'vite';
|
||||||
|
|
||||||
|
export function setupHtmlPlugin(buildTime: string) {
|
||||||
|
const plugin: Plugin = {
|
||||||
|
name: 'html-plugin',
|
||||||
|
apply: 'build',
|
||||||
|
transformIndexHtml(html) {
|
||||||
|
return html.replace('<head>', `<head>\n <meta name="buildTime" content="${buildTime}">`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import type { PluginOption } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||||
|
import progress from 'vite-plugin-progress';
|
||||||
|
import { setupElegantRouter } from './router';
|
||||||
|
import { setupUnocss } from './unocss';
|
||||||
|
import { setupUnplugin } from './unplugin';
|
||||||
|
import { setupHtmlPlugin } from './html';
|
||||||
|
import { setupDevtoolsPlugin } from './devtools';
|
||||||
|
|
||||||
|
export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) {
|
||||||
|
const plugins: PluginOption = [
|
||||||
|
vue(),
|
||||||
|
vueJsx(),
|
||||||
|
setupDevtoolsPlugin(viteEnv),
|
||||||
|
setupElegantRouter(),
|
||||||
|
setupUnocss(viteEnv),
|
||||||
|
...setupUnplugin(viteEnv),
|
||||||
|
progress(),
|
||||||
|
setupHtmlPlugin(buildTime)
|
||||||
|
];
|
||||||
|
|
||||||
|
return plugins;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import type { RouteMeta } from 'vue-router';
|
||||||
|
import ElegantVueRouter from '@elegant-router/vue/vite';
|
||||||
|
import type { RouteKey } from '@elegant-router/types';
|
||||||
|
|
||||||
|
export function setupElegantRouter() {
|
||||||
|
return ElegantVueRouter({
|
||||||
|
layouts: {
|
||||||
|
base: 'src/layouts/base-layout/index.vue',
|
||||||
|
blank: 'src/layouts/blank-layout/index.vue'
|
||||||
|
},
|
||||||
|
customRoutes: {
|
||||||
|
names: ['exception_403', 'exception_404', 'exception_500']
|
||||||
|
},
|
||||||
|
routePathTransformer(routeName, routePath) {
|
||||||
|
const key = routeName as RouteKey;
|
||||||
|
|
||||||
|
if (key === 'login') {
|
||||||
|
const modules: UnionKey.LoginModule[] = ['pwd-login', 'code-login', 'register', 'reset-pwd', 'bind-wechat'];
|
||||||
|
|
||||||
|
const moduleReg = modules.join('|');
|
||||||
|
|
||||||
|
return `/login/:module(${moduleReg})?`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return routePath;
|
||||||
|
},
|
||||||
|
onRouteMetaGen(routeName) {
|
||||||
|
const key = routeName as RouteKey;
|
||||||
|
|
||||||
|
const constantRoutes: RouteKey[] = ['login', '403', '404', '500'];
|
||||||
|
|
||||||
|
const meta: Partial<RouteMeta> = {
|
||||||
|
title: key,
|
||||||
|
i18nKey: `route.${key}` as App.I18n.I18nKey
|
||||||
|
};
|
||||||
|
|
||||||
|
if (constantRoutes.includes(key)) {
|
||||||
|
meta.constant = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import process from 'node:process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import unocss from '@unocss/vite';
|
||||||
|
import presetIcons from '@unocss/preset-icons';
|
||||||
|
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders';
|
||||||
|
|
||||||
|
export function setupUnocss(viteEnv: Env.ImportMeta) {
|
||||||
|
const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
|
||||||
|
|
||||||
|
const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon');
|
||||||
|
|
||||||
|
/** The name of the local icon collection */
|
||||||
|
const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '');
|
||||||
|
|
||||||
|
return unocss({
|
||||||
|
presets: [
|
||||||
|
presetIcons({
|
||||||
|
prefix: `${VITE_ICON_PREFIX}-`,
|
||||||
|
scale: 1,
|
||||||
|
extraProperties: {
|
||||||
|
display: 'inline-block'
|
||||||
|
},
|
||||||
|
collections: {
|
||||||
|
[collectionName]: FileSystemIconLoader(localIconPath, svg =>
|
||||||
|
svg.replace(/^<svg\s/, '<svg width="1em" height="1em" ')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
warn: true
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import process from 'node:process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import type { PluginOption } from 'vite';
|
||||||
|
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
||||||
|
import Icons from 'unplugin-icons/vite';
|
||||||
|
import IconsResolver from 'unplugin-icons/resolver';
|
||||||
|
import Components from 'unplugin-vue-components/vite';
|
||||||
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
||||||
|
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
|
||||||
|
|
||||||
|
export function setupUnplugin(viteEnv: Env.ImportMeta) {
|
||||||
|
const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
|
||||||
|
|
||||||
|
const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon');
|
||||||
|
|
||||||
|
/** The name of the local icon collection */
|
||||||
|
const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '');
|
||||||
|
|
||||||
|
const plugins: PluginOption[] = [
|
||||||
|
Icons({
|
||||||
|
compiler: 'vue3',
|
||||||
|
customCollections: {
|
||||||
|
[collectionName]: FileSystemIconLoader(localIconPath, svg =>
|
||||||
|
svg.replace(/^<svg\s/, '<svg width="1em" height="1em" ')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
scale: 1,
|
||||||
|
defaultClass: 'inline-block'
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
dts: 'src/typings/components.d.ts',
|
||||||
|
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
|
||||||
|
resolvers: [
|
||||||
|
NaiveUiResolver(),
|
||||||
|
IconsResolver({ customCollections: [collectionName], componentPrefix: VITE_ICON_PREFIX })
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
createSvgIconsPlugin({
|
||||||
|
iconDirs: [localIconPath],
|
||||||
|
symbolId: `${VITE_ICON_LOCAL_PREFIX}-[dir]-[name]`,
|
||||||
|
inject: 'body-last',
|
||||||
|
customDomId: '__SVG_ICON_LOCAL__'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
return plugins;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
## java
|
||||||
|
|
||||||
|
后端代码生成工具类替换文件
|
||||||
|
|
||||||
|
## template
|
||||||
|
|
||||||
|
代码生成模板文件
|
||||||
|
|
||||||
|
## sql
|
||||||
|
|
||||||
|
菜单数据替换 SQL
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
# 院校主子表导入导出接口文档(art/school)
|
||||||
|
|
||||||
|
## 1. 目标
|
||||||
|
|
||||||
|
面向前端提供“院校主表 + 子表(详情/校区/学院/专业/招生计划)”的一体化导出与导入能力,支持:
|
||||||
|
|
||||||
|
- 多 Sheet 标准模板导出
|
||||||
|
- 导入前冲突预检
|
||||||
|
- 按院校编码选择性替换(原子事务)
|
||||||
|
- 导入明细反馈(成功/失败/跳过与原因)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 接口清单
|
||||||
|
|
||||||
|
### 2.1 导出模板数据(含业务数据)
|
||||||
|
|
||||||
|
- **URL**: `POST /art/school/export`
|
||||||
|
- **权限**: `art:school:export`
|
||||||
|
- **Content-Type**: `application/x-www-form-urlencoded`
|
||||||
|
- **响应**: Excel 文件下载(`.xlsx`)
|
||||||
|
|
||||||
|
#### 查询参数(同院校列表)
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| mainCode | string | 否 | 院校编码 |
|
||||||
|
| mainName | string | 否 | 院校名称(模糊) |
|
||||||
|
| shortName | string | 否 | 院校简称(模糊) |
|
||||||
|
| province/city/district | string | 否 | 行政区 |
|
||||||
|
| universityType | string | 否 | 大学类型 |
|
||||||
|
| educationLevel | string | 否 | 学历层次 |
|
||||||
|
| schoolNature | string | 否 | 办学性质 |
|
||||||
|
|
||||||
|
#### 导出 Sheet 结构
|
||||||
|
|
||||||
|
1. `院校基本信息`
|
||||||
|
2. `院校详情信息`
|
||||||
|
3. `校区信息`
|
||||||
|
4. `学院信息`
|
||||||
|
5. `专业信息`
|
||||||
|
6. `招生计划信息`
|
||||||
|
7. `模板说明`
|
||||||
|
|
||||||
|
> 子表均包含 `院校编码*`、`院校名称*` 用于与主表关联。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 导出空模板
|
||||||
|
|
||||||
|
- **URL**: `POST /art/school/importTemplate`
|
||||||
|
- **权限**: `art:school:export`
|
||||||
|
- **响应**: 空模板 Excel(仅表头 + 模板说明)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 导入预检
|
||||||
|
|
||||||
|
- **URL**: `POST /art/school/importPreview`
|
||||||
|
- **权限**: `art:school:import`
|
||||||
|
- **Content-Type**: `multipart/form-data`
|
||||||
|
- **入参**:
|
||||||
|
- `file`(Excel 文件,必填)
|
||||||
|
|
||||||
|
#### 返回示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "操作成功",
|
||||||
|
"data": {
|
||||||
|
"totalSchoolCount": 10,
|
||||||
|
"conflictCount": 3,
|
||||||
|
"invalidCount": 1,
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"mainCode": "1001",
|
||||||
|
"mainName": "测试院校01",
|
||||||
|
"status": "CONFLICT",
|
||||||
|
"message": "系统已存在该院校数据,导入时需确认是否替换"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mainCode": "",
|
||||||
|
"mainName": "",
|
||||||
|
"status": "INVALID",
|
||||||
|
"message": "必填字段缺失:院校编码/院校名称不能为空"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 状态说明
|
||||||
|
|
||||||
|
- `CONFLICT`: 系统已存在(按院校编码/名称命中)
|
||||||
|
- `INVALID`: 模板数据无效(缺失、重复、子表关联不到主表)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 导入执行
|
||||||
|
|
||||||
|
- **URL**: `POST /art/school/importData`
|
||||||
|
- **权限**: `art:school:import`
|
||||||
|
- **Content-Type**: `multipart/form-data`
|
||||||
|
- **入参**:
|
||||||
|
- `file`(Excel 文件,必填)
|
||||||
|
- `replaceAll`(boolean,选填,默认 `false`)
|
||||||
|
- `replaceMainCodes`(string[],选填,可多值传参)
|
||||||
|
|
||||||
|
#### 前端推荐调用逻辑
|
||||||
|
|
||||||
|
1. 先调 `importPreview`。
|
||||||
|
2. 对 `CONFLICT` 院校弹窗询问“是否替换”。
|
||||||
|
3. 将用户同意替换的院校编码组装为 `replaceMainCodes`,再调 `importData`。
|
||||||
|
4. 用户全量替换时可直接传 `replaceAll=true`。
|
||||||
|
|
||||||
|
#### 返回示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "操作成功",
|
||||||
|
"data": {
|
||||||
|
"totalSchoolCount": 10,
|
||||||
|
"successCount": 8,
|
||||||
|
"failCount": 1,
|
||||||
|
"skippedCount": 1,
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"mainCode": "1001",
|
||||||
|
"mainName": "测试院校01",
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"message": "替换成功"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mainCode": "1002",
|
||||||
|
"mainName": "测试院校02",
|
||||||
|
"status": "SKIPPED",
|
||||||
|
"message": "院校名称重复且用户取消替换"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mainCode": "1003",
|
||||||
|
"mainName": "测试院校03",
|
||||||
|
"status": "FAILED",
|
||||||
|
"message": "学院保存失败"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 状态说明
|
||||||
|
|
||||||
|
- `SUCCESS`: 导入/替换成功
|
||||||
|
- `SKIPPED`: 冲突且用户未选择替换
|
||||||
|
- `FAILED`: 导入失败(事务回滚)
|
||||||
|
- `INVALID`: 文件预校验失败
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 原子性与一致性
|
||||||
|
|
||||||
|
- 导入按“单院校”为最小事务单元:
|
||||||
|
- 该院校主表 + 子表(详情/校区/学院/专业/招生计划)要么全部成功,要么全部回滚。
|
||||||
|
- 替换逻辑:
|
||||||
|
1. 删除该院校历史主表及上述子表数据
|
||||||
|
2. 插入新主表数据
|
||||||
|
3. 插入新子表数据
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 模板必填规则(简表)
|
||||||
|
|
||||||
|
- `院校基本信息`: `院校编码*`、`院校名称*`
|
||||||
|
- `院校详情信息`: `院校编码*`、`院校名称*`
|
||||||
|
- `校区信息`: `院校编码*`、`院校名称*`、`校区名称*`
|
||||||
|
- `学院信息`: `院校编码*`、`院校名称*`、`学院名称*`
|
||||||
|
- `专业信息`: `院校编码*`、`院校名称*`、`学院ID*`、`专业名称*`
|
||||||
|
- `招生计划信息`: `院校编码*`、`院校名称*`、`年份*`、`招生省份*`、`专业名称*`、`计划数*`
|
||||||
|
|
||||||
|
> `*` 表示模板必填字段。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 兼容说明
|
||||||
|
|
||||||
|
- 现有 `POST /art/school` 与 `PUT /art/school` 仍用于单院校编辑(`school + detail` 统一结构)。
|
||||||
|
- 本文档新增导入导出接口可与现有列表/编辑功能并行使用。
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
# 学校与专业标签联动接口补充文档
|
||||||
|
|
||||||
|
本文档仅说明本次新增字段:
|
||||||
|
|
||||||
|
- `/art/school`:新增 `schoolTags`
|
||||||
|
- `/art/schoolMajor`:新增 `majorTags`
|
||||||
|
|
||||||
|
## 1. /art/school 接口补充
|
||||||
|
|
||||||
|
### 1.1 GET `/art/school/{schoolId}`
|
||||||
|
|
||||||
|
返回对象 `data` 中新增:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| schoolTags | string[] | 学校标签列表 |
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "操作成功",
|
||||||
|
"data": {
|
||||||
|
"schoolId": 1001,
|
||||||
|
"mainCode": "10531",
|
||||||
|
"mainName": "某某大学",
|
||||||
|
"enrollCodes": ["10531", "A10531"],
|
||||||
|
"schoolTags": ["985", "211", "双一流"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 POST `/art/school`
|
||||||
|
|
||||||
|
请求体 `ArtSchoolSubmitBo` 新增:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| schoolTags | string[] | 否 | 学校标签列表(全量替换语义) |
|
||||||
|
|
||||||
|
### 1.3 PUT `/art/school`
|
||||||
|
|
||||||
|
请求体 `ArtSchoolSubmitBo` 新增:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| schoolTags | string[] | 否 | 学校标签列表(全量替换语义) |
|
||||||
|
|
||||||
|
### 1.4 `schoolTags` 处理语义
|
||||||
|
|
||||||
|
- `schoolTags = null`:不修改已有标签
|
||||||
|
- `schoolTags = []`:清空该学校全部标签
|
||||||
|
- `schoolTags = ["985","211"]`:按传入值全量覆盖(去重后保存)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. /art/schoolMajor 接口补充
|
||||||
|
|
||||||
|
### 2.1 GET `/art/schoolMajor/{majorId}`
|
||||||
|
|
||||||
|
返回对象 `data` 中新增:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| majorTags | string[] | 专业标签列表 |
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "操作成功",
|
||||||
|
"data": {
|
||||||
|
"majorId": 9001,
|
||||||
|
"majorName": "视觉传达设计",
|
||||||
|
"majorTags": ["国家级特色专业", "一流本科专业"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 GET `/art/schoolMajor/list`
|
||||||
|
|
||||||
|
列表项中新增字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| majorTags | string[] | 专业标签列表 |
|
||||||
|
|
||||||
|
### 2.3 POST `/art/schoolMajor`
|
||||||
|
|
||||||
|
请求体 `ArtSchoolMajorBo` 新增:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| majorTags | string[] | 否 | 专业标签列表(全量替换语义) |
|
||||||
|
|
||||||
|
### 2.4 PUT `/art/schoolMajor`
|
||||||
|
|
||||||
|
请求体 `ArtSchoolMajorBo` 新增:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| majorTags | string[] | 否 | 专业标签列表(全量替换语义) |
|
||||||
|
|
||||||
|
### 2.5 `majorTags` 处理语义
|
||||||
|
|
||||||
|
- `majorTags = null`:不修改已有标签
|
||||||
|
- `majorTags = []`:清空该专业全部标签
|
||||||
|
- `majorTags = ["A","B"]`:按传入值全量覆盖(去重后保存)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 与旧字段兼容说明
|
||||||
|
|
||||||
|
- `ArtSchoolMajorBo` / `ArtSchoolMajorVo` 中旧字段 `tags` 仍保留,便于兼容历史前端。
|
||||||
|
- 新增推荐字段为 `majorTags`(数组),后续前端优先使用该字段。
|
||||||
|
|
@ -0,0 +1,389 @@
|
||||||
|
package org.dromara.generator.util;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.core.convert.Convert;
|
||||||
|
import cn.hutool.core.lang.Dict;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.apache.velocity.VelocityContext;
|
||||||
|
import org.dromara.common.core.utils.DateUtils;
|
||||||
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.dromara.common.json.utils.JsonUtils;
|
||||||
|
import org.dromara.common.mybatis.enums.DataBaseType;
|
||||||
|
import org.dromara.common.mybatis.helper.DataBaseHelper;
|
||||||
|
import org.dromara.generator.constant.GenConstants;
|
||||||
|
import org.dromara.generator.domain.GenTable;
|
||||||
|
import org.dromara.generator.domain.GenTableColumn;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模板处理工具类
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class VelocityUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目空间路径
|
||||||
|
*/
|
||||||
|
private static final String PROJECT_PATH = "main/java";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mybatis空间路径
|
||||||
|
*/
|
||||||
|
private static final String MYBATIS_PATH = "main/resources/mapper";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认上级菜单,系统工具
|
||||||
|
*/
|
||||||
|
private static final String DEFAULT_PARENT_MENU_ID = "3";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置模板变量信息
|
||||||
|
*
|
||||||
|
* @return 模板列表
|
||||||
|
*/
|
||||||
|
public static VelocityContext prepareContext(GenTable genTable) {
|
||||||
|
String moduleName = genTable.getModuleName();
|
||||||
|
String businessName = genTable.getBusinessName();
|
||||||
|
String packageName = genTable.getPackageName();
|
||||||
|
String tplCategory = genTable.getTplCategory();
|
||||||
|
String functionName = genTable.getFunctionName();
|
||||||
|
|
||||||
|
VelocityContext velocityContext = new VelocityContext();
|
||||||
|
velocityContext.put("tplCategory", genTable.getTplCategory());
|
||||||
|
velocityContext.put("tableName", genTable.getTableName());
|
||||||
|
velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】");
|
||||||
|
velocityContext.put("ClassName", genTable.getClassName());
|
||||||
|
velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName()));
|
||||||
|
velocityContext.put("moduleName", StrUtil.toSymbolCase(genTable.getModuleName(), '-'));
|
||||||
|
velocityContext.put("BusinessName", StringUtils.capitalize(genTable.getBusinessName()));
|
||||||
|
velocityContext.put("businessName", genTable.getBusinessName());
|
||||||
|
velocityContext.put("business_name", StrUtil.toUnderlineCase(genTable.getBusinessName()));
|
||||||
|
velocityContext.put("business__name", StrUtil.toSymbolCase(genTable.getBusinessName(), '-'));
|
||||||
|
velocityContext.put("businessname", StrUtil.toSymbolCase(genTable.getBusinessName(), ' '));
|
||||||
|
velocityContext.put("basePackage", getPackagePrefix(packageName));
|
||||||
|
velocityContext.put("packageName", packageName);
|
||||||
|
velocityContext.put("author", genTable.getFunctionAuthor());
|
||||||
|
velocityContext.put("datetime", DateUtils.getDate());
|
||||||
|
velocityContext.put("pkColumn", genTable.getPkColumn());
|
||||||
|
velocityContext.put("importList", getImportList(genTable));
|
||||||
|
velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName));
|
||||||
|
velocityContext.put("dicts", getDicts(genTable));
|
||||||
|
velocityContext.put("dictList", getDictList(genTable));
|
||||||
|
velocityContext.put("pkColumn", genTable.getPkColumn());
|
||||||
|
velocityContext.put("columns", genTable.getColumns());
|
||||||
|
velocityContext.put("table", genTable);
|
||||||
|
velocityContext.put("StrUtil", new StrUtil());
|
||||||
|
setMenuVelocityContext(velocityContext, genTable);
|
||||||
|
if (GenConstants.TPL_TREE.equals(tplCategory)) {
|
||||||
|
setTreeVelocityContext(velocityContext, genTable);
|
||||||
|
}
|
||||||
|
return velocityContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setMenuVelocityContext(VelocityContext context, GenTable genTable) {
|
||||||
|
String options = genTable.getOptions();
|
||||||
|
Dict paramsObj = JsonUtils.parseMap(options);
|
||||||
|
String parentMenuId = getParentMenuId(paramsObj);
|
||||||
|
context.put("parentMenuId", parentMenuId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setTreeVelocityContext(VelocityContext context, GenTable genTable) {
|
||||||
|
String options = genTable.getOptions();
|
||||||
|
Dict paramsObj = JsonUtils.parseMap(options);
|
||||||
|
String treeCode = getTreecode(paramsObj);
|
||||||
|
String treeParentCode = getTreeParentCode(paramsObj);
|
||||||
|
String treeName = getTreeName(paramsObj);
|
||||||
|
|
||||||
|
context.put("treeCode", treeCode);
|
||||||
|
context.put("treeParentCode", treeParentCode);
|
||||||
|
context.put("treeName", treeName);
|
||||||
|
context.put("expandColumn", getExpandColumn(genTable));
|
||||||
|
if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) {
|
||||||
|
context.put("tree_parent_code", paramsObj.get(GenConstants.TREE_PARENT_CODE));
|
||||||
|
}
|
||||||
|
if (paramsObj.containsKey(GenConstants.TREE_NAME)) {
|
||||||
|
context.put("tree_name", paramsObj.get(GenConstants.TREE_NAME));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模板信息
|
||||||
|
*
|
||||||
|
* @return 模板列表
|
||||||
|
*/
|
||||||
|
public static List<String> getTemplateList(String tplCategory) {
|
||||||
|
List<String> templates = new ArrayList<>();
|
||||||
|
templates.add("vm/java/domain.java.vm");
|
||||||
|
templates.add("vm/java/vo.java.vm");
|
||||||
|
templates.add("vm/java/bo.java.vm");
|
||||||
|
templates.add("vm/java/mapper.java.vm");
|
||||||
|
templates.add("vm/java/service.java.vm");
|
||||||
|
templates.add("vm/java/serviceImpl.java.vm");
|
||||||
|
templates.add("vm/java/controller.java.vm");
|
||||||
|
templates.add("vm/xml/mapper.xml.vm");
|
||||||
|
DataBaseType dataBaseType = DataBaseHelper.getDataBaseType();
|
||||||
|
if (dataBaseType.isOracle()) {
|
||||||
|
templates.add("vm/sql/oracle/sql.vm");
|
||||||
|
} else if (dataBaseType.isPostgreSql()) {
|
||||||
|
templates.add("vm/sql/postgres/sql.vm");
|
||||||
|
} else if (dataBaseType.isSqlServer()) {
|
||||||
|
templates.add("vm/sql/sqlserver/sql.vm");
|
||||||
|
} else {
|
||||||
|
templates.add("vm/sql/sql.vm");
|
||||||
|
}
|
||||||
|
templates.add("vm/soy/typings/api.d.ts.vm");
|
||||||
|
templates.add("vm/soy/api/api.ts.vm");
|
||||||
|
templates.add("vm/soy/modules/search.vue.vm");
|
||||||
|
templates.add("vm/soy/modules/operate-drawer.vue.vm");
|
||||||
|
if (GenConstants.TPL_CRUD.equals(tplCategory)) {
|
||||||
|
templates.add("vm/soy/index.vue.vm");
|
||||||
|
} else if (GenConstants.TPL_TREE.equals(tplCategory)) {
|
||||||
|
templates.add("vm/soy/index-tree.vue.vm");
|
||||||
|
}
|
||||||
|
return templates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件名
|
||||||
|
*/
|
||||||
|
public static String getFileName(String template, GenTable genTable) {
|
||||||
|
// 文件名称
|
||||||
|
String fileName = "";
|
||||||
|
// 包路径
|
||||||
|
String packageName = genTable.getPackageName();
|
||||||
|
// 模块名
|
||||||
|
String moduleName = genTable.getModuleName();
|
||||||
|
// 大写类名
|
||||||
|
String className = genTable.getClassName();
|
||||||
|
// 业务名称
|
||||||
|
String businessName = genTable.getBusinessName();
|
||||||
|
|
||||||
|
String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/");
|
||||||
|
String mybatisPath = MYBATIS_PATH + "/" + moduleName;
|
||||||
|
String soybeanPath = "soy";
|
||||||
|
String soybeanModuleName = StrUtil.toSymbolCase(moduleName, '-');
|
||||||
|
if (template.contains("domain.java.vm")) {
|
||||||
|
fileName = StringUtils.format("{}/domain/{}.java", javaPath, className);
|
||||||
|
}
|
||||||
|
if (template.contains("vo.java.vm")) {
|
||||||
|
fileName = StringUtils.format("{}/domain/vo/{}Vo.java", javaPath, className);
|
||||||
|
}
|
||||||
|
if (template.contains("bo.java.vm")) {
|
||||||
|
fileName = StringUtils.format("{}/domain/bo/{}Bo.java", javaPath, className);
|
||||||
|
}
|
||||||
|
if (template.contains("mapper.java.vm")) {
|
||||||
|
fileName = StringUtils.format("{}/mapper/{}Mapper.java", javaPath, className);
|
||||||
|
} else if (template.contains("service.java.vm")) {
|
||||||
|
fileName = StringUtils.format("{}/service/I{}Service.java", javaPath, className);
|
||||||
|
} else if (template.contains("serviceImpl.java.vm")) {
|
||||||
|
fileName = StringUtils.format("{}/service/impl/{}ServiceImpl.java", javaPath, className);
|
||||||
|
} else if (template.contains("controller.java.vm")) {
|
||||||
|
fileName = StringUtils.format("{}/controller/{}Controller.java", javaPath, className);
|
||||||
|
} else if (template.contains("mapper.xml.vm")) {
|
||||||
|
fileName = StringUtils.format("{}/{}Mapper.xml", mybatisPath, className);
|
||||||
|
} else if (template.contains("sql.vm")) {
|
||||||
|
fileName = businessName + "Menu.sql";
|
||||||
|
} else if (template.contains("index.vue.vm")) {
|
||||||
|
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
|
||||||
|
} else if (template.contains("index-tree.vue.vm")) {
|
||||||
|
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
|
||||||
|
} else if (template.contains("api.d.ts.vm")) {
|
||||||
|
fileName = StringUtils.format("{}/typings/api/{}.{}.api.d.ts", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
|
||||||
|
} else if (template.contains("api.ts.vm")) {
|
||||||
|
fileName = StringUtils.format("{}/service/api/{}/{}.ts", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
|
||||||
|
} else if (template.contains("search.vue.vm")) {
|
||||||
|
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-search.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
|
||||||
|
} else if (template.contains("operate-drawer.vue.vm")) {
|
||||||
|
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-operate-drawer.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
|
||||||
|
}
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取包前缀
|
||||||
|
*
|
||||||
|
* @param packageName 包名称
|
||||||
|
* @return 包前缀名称
|
||||||
|
*/
|
||||||
|
public static String getPackagePrefix(String packageName) {
|
||||||
|
int lastIndex = packageName.lastIndexOf(".");
|
||||||
|
return StringUtils.substring(packageName, 0, lastIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据列类型获取导入包
|
||||||
|
*
|
||||||
|
* @param genTable 业务表对象
|
||||||
|
* @return 返回需要导入的包列表
|
||||||
|
*/
|
||||||
|
public static HashSet<String> getImportList(GenTable genTable) {
|
||||||
|
List<GenTableColumn> columns = genTable.getColumns();
|
||||||
|
HashSet<String> importList = new HashSet<>();
|
||||||
|
for (GenTableColumn column : columns) {
|
||||||
|
if (!column.isSuperColumn() && GenConstants.TYPE_DATE.equals(column.getJavaType())) {
|
||||||
|
importList.add("java.util.Date");
|
||||||
|
importList.add("com.fasterxml.jackson.annotation.JsonFormat");
|
||||||
|
} else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) {
|
||||||
|
importList.add("java.math.BigDecimal");
|
||||||
|
} else if (!column.isSuperColumn() && "imageUpload".equals(column.getHtmlType())) {
|
||||||
|
importList.add("org.dromara.common.translation.annotation.Translation");
|
||||||
|
importList.add("org.dromara.common.translation.constant.TransConstant");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return importList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据列类型获取字典组
|
||||||
|
*
|
||||||
|
* @param genTable 业务表对象
|
||||||
|
* @return 返回字典组
|
||||||
|
*/
|
||||||
|
public static String getDicts(GenTable genTable) {
|
||||||
|
List<GenTableColumn> columns = genTable.getColumns();
|
||||||
|
Set<String> dicts = new HashSet<>();
|
||||||
|
addDicts(dicts, columns);
|
||||||
|
return StringUtils.join(dicts, ", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加字典列表
|
||||||
|
*
|
||||||
|
* @param dicts 字典列表
|
||||||
|
* @param columns 列集合
|
||||||
|
*/
|
||||||
|
public static void addDicts(Set<String> dicts, List<GenTableColumn> columns) {
|
||||||
|
for (GenTableColumn column : columns) {
|
||||||
|
if (!column.isSuperColumn() && StringUtils.isNotEmpty(column.getDictType()) && StringUtils.equalsAny(
|
||||||
|
column.getHtmlType(),
|
||||||
|
GenConstants.HTML_SELECT, GenConstants.HTML_RADIO, GenConstants.HTML_CHECKBOX)) {
|
||||||
|
dicts.add("'" + column.getDictType() + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据列类型获取字典组
|
||||||
|
*
|
||||||
|
* @param genTable 业务表对象
|
||||||
|
* @return 返回字典组
|
||||||
|
*/
|
||||||
|
public static Set<Map<String, Object>> getDictList(GenTable genTable) {
|
||||||
|
List<GenTableColumn> columns = genTable.getColumns();
|
||||||
|
Set<Map<String, Object>> dicts = new HashSet<>();
|
||||||
|
addDictList(dicts, columns);
|
||||||
|
return dicts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加字典列表
|
||||||
|
*
|
||||||
|
* @param dicts 字典列表
|
||||||
|
* @param columns 列集合
|
||||||
|
*/
|
||||||
|
public static void addDictList(Set<Map<String, Object>> dicts, List<GenTableColumn> columns) {
|
||||||
|
for (GenTableColumn column : columns) {
|
||||||
|
if (!column.isSuperColumn() && StringUtils.isNotEmpty(column.getDictType()) && StringUtils.equalsAny(
|
||||||
|
column.getHtmlType(),
|
||||||
|
GenConstants.HTML_SELECT, GenConstants.HTML_RADIO, GenConstants.HTML_CHECKBOX)) {
|
||||||
|
Map<String, Object> dict = new HashMap<>();
|
||||||
|
dict.put("type", column.getDictType());
|
||||||
|
dict.put("name", StringUtils.toCamelCase(column.getDictType()));
|
||||||
|
dict.put("immediate", column.isList());
|
||||||
|
dicts.add(dict);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取权限前缀
|
||||||
|
*
|
||||||
|
* @param moduleName 模块名称
|
||||||
|
* @param businessName 业务名称
|
||||||
|
* @return 返回权限前缀
|
||||||
|
*/
|
||||||
|
public static String getPermissionPrefix(String moduleName, String businessName) {
|
||||||
|
return StringUtils.format("{}:{}", moduleName, businessName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取上级菜单ID字段
|
||||||
|
*
|
||||||
|
* @param paramsObj 生成其他选项
|
||||||
|
* @return 上级菜单ID字段
|
||||||
|
*/
|
||||||
|
public static String getParentMenuId(Dict paramsObj) {
|
||||||
|
if (CollUtil.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.PARENT_MENU_ID)
|
||||||
|
&& StringUtils.isNotEmpty(paramsObj.getStr(GenConstants.PARENT_MENU_ID))) {
|
||||||
|
return paramsObj.getStr(GenConstants.PARENT_MENU_ID);
|
||||||
|
}
|
||||||
|
return DEFAULT_PARENT_MENU_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取树编码
|
||||||
|
*
|
||||||
|
* @param paramsObj 生成其他选项
|
||||||
|
* @return 树编码
|
||||||
|
*/
|
||||||
|
public static String getTreecode(Map<String, Object> paramsObj) {
|
||||||
|
if (CollUtil.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.TREE_CODE)) {
|
||||||
|
return StringUtils.toCamelCase(Convert.toStr(paramsObj.get(GenConstants.TREE_CODE)));
|
||||||
|
}
|
||||||
|
return StringUtils.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取树父编码
|
||||||
|
*
|
||||||
|
* @param paramsObj 生成其他选项
|
||||||
|
* @return 树父编码
|
||||||
|
*/
|
||||||
|
public static String getTreeParentCode(Dict paramsObj) {
|
||||||
|
if (CollUtil.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) {
|
||||||
|
return StringUtils.toCamelCase(paramsObj.getStr(GenConstants.TREE_PARENT_CODE));
|
||||||
|
}
|
||||||
|
return StringUtils.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取树名称
|
||||||
|
*
|
||||||
|
* @param paramsObj 生成其他选项
|
||||||
|
* @return 树名称
|
||||||
|
*/
|
||||||
|
public static String getTreeName(Dict paramsObj) {
|
||||||
|
if (CollUtil.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.TREE_NAME)) {
|
||||||
|
return StringUtils.toCamelCase(paramsObj.getStr(GenConstants.TREE_NAME));
|
||||||
|
}
|
||||||
|
return StringUtils.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取需要在哪一列上面显示展开按钮
|
||||||
|
*
|
||||||
|
* @param genTable 业务表对象
|
||||||
|
* @return 展开按钮列序号
|
||||||
|
*/
|
||||||
|
public static int getExpandColumn(GenTable genTable) {
|
||||||
|
String options = genTable.getOptions();
|
||||||
|
Dict paramsObj = JsonUtils.parseMap(options);
|
||||||
|
String treeName = paramsObj.getStr(GenConstants.TREE_NAME);
|
||||||
|
int num = 0;
|
||||||
|
for (GenTableColumn column : genTable.getColumns()) {
|
||||||
|
if (column.isList()) {
|
||||||
|
num++;
|
||||||
|
String columnName = column.getColumnName();
|
||||||
|
if (columnName.equals(treeName)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
# 专业/宿舍关联校区接口评估与需求
|
||||||
|
|
||||||
|
## 1. 当前可用能力评估
|
||||||
|
|
||||||
|
### 1.1 宿舍管理(可支撑)
|
||||||
|
- 已有校区列表接口:`GET /art/schoolCampus/list`
|
||||||
|
- 支持入参 `schoolId`,可按学校查询校区。
|
||||||
|
- 已有宿舍新增/修改接口:
|
||||||
|
- `POST /art/schoolDorm`
|
||||||
|
- `PUT /art/schoolDorm`
|
||||||
|
- 入参已包含 `campusId`,可直接做“校区下拉选择后提交关联”。
|
||||||
|
|
||||||
|
### 1.2 专业管理(当前不可完全支撑)
|
||||||
|
- 专业接口当前为:
|
||||||
|
- `GET /art/schoolMajor/list`
|
||||||
|
- `POST /art/schoolMajor`
|
||||||
|
- `PUT /art/schoolMajor`
|
||||||
|
- 现有专业模型/类型无 `campusId` 字段,仅有 `schoolId`、`collegeId` 等。
|
||||||
|
- 结论:前端可加载校区列表,但无法通过现有专业接口保存“专业 -> 校区”关联。
|
||||||
|
|
||||||
|
## 2. 专业管理所需接口改造(建议)
|
||||||
|
|
||||||
|
## 2.1 方案:在专业主模型中新增 `campusId`(单校区关联)
|
||||||
|
|
||||||
|
### 专业列表查询
|
||||||
|
- URL: `GET /art/schoolMajor/list`
|
||||||
|
- 新增查询入参:
|
||||||
|
- `campusId` `number|string` 可选
|
||||||
|
- 返回字段新增:
|
||||||
|
- `campusId` `number|string`
|
||||||
|
- `campusName` `string`(建议返回,便于表格展示)
|
||||||
|
|
||||||
|
#### 查询入参示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pageNum": 1,
|
||||||
|
"pageSize": 10,
|
||||||
|
"schoolId": 1001,
|
||||||
|
"campusId": 2001,
|
||||||
|
"majorName": "视觉传达设计"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 查询返回 rows 单项示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"majorId": 3001,
|
||||||
|
"schoolId": 1001,
|
||||||
|
"campusId": 2001,
|
||||||
|
"campusName": "主校区",
|
||||||
|
"collegeId": 5001,
|
||||||
|
"majorName": "视觉传达设计"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 专业新增
|
||||||
|
- URL: `POST /art/schoolMajor`
|
||||||
|
- 新增必填入参:
|
||||||
|
- `campusId` `number|string`
|
||||||
|
|
||||||
|
#### 新增入参示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schoolId": 1001,
|
||||||
|
"campusId": 2001,
|
||||||
|
"collegeId": 5001,
|
||||||
|
"majorCode": "130502",
|
||||||
|
"majorName": "视觉传达设计",
|
||||||
|
"educationLevel": "本科",
|
||||||
|
"durationYears": 4
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 专业修改
|
||||||
|
- URL: `PUT /art/schoolMajor`
|
||||||
|
- 新增必填入参:
|
||||||
|
- `campusId` `number|string`
|
||||||
|
|
||||||
|
#### 修改入参示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"majorId": 3001,
|
||||||
|
"schoolId": 1001,
|
||||||
|
"campusId": 2002,
|
||||||
|
"collegeId": 5001,
|
||||||
|
"majorName": "视觉传达设计"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 统一返回格式建议
|
||||||
|
|
||||||
|
### 列表接口返回(保持现有 RuoYi 分页结构)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "操作成功",
|
||||||
|
"rows": [],
|
||||||
|
"total": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 新增/修改接口返回
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "操作成功",
|
||||||
|
"data": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
-- 修改字典数据表的 list_class 字段,将 danger 改为 error
|
||||||
|
UPDATE `sys_dict_data` SET `list_class` = 'error' WHERE `list_class` = 'danger';
|
||||||
|
|
||||||
|
-- 字典适配多语言
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_user_sex.male', `dict_type` = 'sys_user_sex' WHERE `dict_code` = 1;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_user_sex.female', `dict_type` = 'sys_user_sex' WHERE `dict_code` = 2;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_user_sex.unknown', `dict_type` = 'sys_user_sex' WHERE `dict_code` = 3;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_show_hide.show', `dict_type` = 'sys_show_hide' WHERE `dict_code` = 4;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_show_hide.hide', `dict_type` = 'sys_show_hide' WHERE `dict_code` = 5;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_normal_disable.normal', `dict_type` = 'sys_normal_disable' WHERE `dict_code` = 6;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_normal_disable.disable', `dict_type` = 'sys_normal_disable' WHERE `dict_code` = 7;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_yes_no.yes', `dict_type` = 'sys_yes_no' WHERE `dict_code` = 12;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_yes_no.no', `dict_type` = 'sys_yes_no' WHERE `dict_code` = 13;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_notice_type.notice', `dict_type` = 'sys_notice_type' WHERE `dict_code` = 14;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_notice_type.announcement', `dict_type` = 'sys_notice_type' WHERE `dict_code` = 15;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_notice_status.normal', `dict_type` = 'sys_notice_status' WHERE `dict_code` = 16;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_notice_status.close', `dict_type` = 'sys_notice_status' WHERE `dict_code` = 17;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.insert', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 18;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.update', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 19;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.delete', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 20;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.grant', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 21;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.export', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 22;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.import', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 23;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.force', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 24;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.gencode', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 25;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.clean', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 26;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_common_status.success', `dict_type` = 'sys_common_status' WHERE `dict_code` = 27;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_common_status.fail', `dict_type` = 'sys_common_status' WHERE `dict_code` = 28;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.other', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 29;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_grant_type.password', `dict_type` = 'sys_grant_type' WHERE `dict_code` = 30;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_grant_type.sms', `dict_type` = 'sys_grant_type' WHERE `dict_code` = 31;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_grant_type.email', `dict_type` = 'sys_grant_type' WHERE `dict_code` = 32;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_grant_type.miniapp', `dict_type` = 'sys_grant_type' WHERE `dict_code` = 33;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_grant_type.social', `dict_type` = 'sys_grant_type' WHERE `dict_code` = 34;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_device_type.pc', `dict_type` = 'sys_device_type' WHERE `dict_code` = 35;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_device_type.android', `dict_type` = 'sys_device_type' WHERE `dict_code` = 36;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_device_type.ios', `dict_type` = 'sys_device_type' WHERE `dict_code` = 37;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_device_type.miniapp', `dict_type` = 'sys_device_type' WHERE `dict_code` = 38;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.revoked', `dict_type` = 'wf_business_status' WHERE `dict_code` = 39;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.draft', `dict_type` = 'wf_business_status' WHERE `dict_code` = 40;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.pending', `dict_type` = 'wf_business_status' WHERE `dict_code` = 41;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.completed', `dict_type` = 'wf_business_status' WHERE `dict_code` = 42;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.cancelled', `dict_type` = 'wf_business_status' WHERE `dict_code` = 43;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.returned', `dict_type` = 'wf_business_status' WHERE `dict_code` = 44;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.terminated', `dict_type` = 'wf_business_status' WHERE `dict_code` = 45;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_form_type.custom_form', `dict_type` = 'wf_form_type' WHERE `dict_code` = 46;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_form_type.dynamic_form', `dict_type` = 'wf_form_type' WHERE `dict_code` = 47;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.revoke', `dict_type` = 'wf_task_status' WHERE `dict_code` = 48;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.pass', `dict_type` = 'wf_task_status' WHERE `dict_code` = 49;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.pending_review', `dict_type` = 'wf_task_status' WHERE `dict_code` = 50;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.cancel', `dict_type` = 'wf_task_status' WHERE `dict_code` = 51;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.return', `dict_type` = 'wf_task_status' WHERE `dict_code` = 52;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.terminate', `dict_type` = 'wf_task_status' WHERE `dict_code` = 53;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.transfer', `dict_type` = 'wf_task_status' WHERE `dict_code` = 54;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.delegate', `dict_type` = 'wf_task_status' WHERE `dict_code` = 55;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.copy', `dict_type` = 'wf_task_status' WHERE `dict_code` = 56;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.add_sign', `dict_type` = 'wf_task_status' WHERE `dict_code` = 57;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.minus_sign', `dict_type` = 'wf_task_status' WHERE `dict_code` = 58;
|
||||||
|
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.timeout', `dict_type` = 'wf_task_status' WHERE `dict_code` = 59;
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
-- 目录类型菜单
|
||||||
|
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'carbon:cloud-service-management', `menu_name` = 'route.system' WHERE `menu_id` = 1;
|
||||||
|
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'stash:dashboard', `menu_name` = 'route.monitor' WHERE `menu_id` = 2;
|
||||||
|
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:tools', `menu_name` = 'route.tool' WHERE `menu_id` = 3;
|
||||||
|
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'material-symbols:kid-star-outline', `menu_name` = 'route.demo' WHERE `menu_id` = 5;
|
||||||
|
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:building-cog', `menu_name` = 'menu.system_tenant' WHERE `menu_id` = 6;
|
||||||
|
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:logs', `menu_name` = 'menu.system_log' WHERE `menu_id` = 108;
|
||||||
|
|
||||||
|
-- 页面类型
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'ic:round-manage-accounts', `menu_name` = 'route.system_user' WHERE `menu_id` = 100;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'carbon:user-role', `menu_name` = 'route.system_role' WHERE `menu_id` = 101;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'material-symbols:route', `menu_name` = 'route.system_menu' WHERE `menu_id` = 102;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'mingcute:department-line', `menu_name` = 'route.system_dept' WHERE `menu_id` = 103;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'hugeicons:permanent-job', `menu_name` = 'route.system_post' WHERE `menu_id` = 104;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'qlementine-icons:dictionary-16', `menu_name` = 'route.system_dict' WHERE `menu_id` = 105;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'carbon:parameter', `menu_name` = 'route.system_config' WHERE `menu_id` = 106;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'solar:chat-line-outline', `menu_name` = 'route.system_notice' WHERE `menu_id` = 107;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'majesticons:status-online-line', `menu_name` = 'route.monitor_online' WHERE `menu_id` = 109;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'simple-icons:redis', `menu_name` = 'route.monitor_cache' WHERE `menu_id` = 113;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'material-symbols:code-blocks-outline', `menu_name` = 'route.tool_gen' WHERE `menu_id` = 115;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'material-symbols:attach-file', `menu_name` = 'route.system_oss' WHERE `menu_id` = 118;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'tabler:building-skyscraper', `menu_name` = 'route.system_tenant' WHERE `menu_id` = 121;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'lets-icons:package-box-alt', `menu_name` = 'route.system_tenant-package' WHERE `menu_id` = 122;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'tabler:device-imac-cog', `menu_name` = 'route.system_client' WHERE `menu_id` = 123;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'carbon:operations-record', `menu_name` = 'route.monitor_operlog' WHERE `menu_id` = 500;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'tabler:login-2', `menu_name` = 'route.monitor_logininfor' WHERE `menu_id` = 501;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'gg:debug', `menu_name` = 'route.demo_demo' WHERE `menu_id` = 1500;
|
||||||
|
UPDATE `sys_menu` SET `icon` = 'gg:debug', `menu_name` = 'route.demo_tree' WHERE `menu_id` = 1506;
|
||||||
|
UPDATE `sys_menu` SET `path` = 'oss/config', `component` = 'system/oss-config/index', `icon` = 'hugeicons:configuration-01', `menu_name` = 'route.system_oss-config' WHERE `menu_id` = 133;
|
||||||
|
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (9, 'route.about', 0, 99, 'about', 'about/index', '', 1, 1, 'C', '0', '0', '', 'fluent:book-information-24-regular', 103, 1, sysdate(), null, null, '关于页面') ON DUPLICATE KEY UPDATE `update_time` = sysdate();
|
||||||
|
|
||||||
|
-- IFrame 类型
|
||||||
|
UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = '{"url": "https://ruoyi.xlsea.cn/admin/"}', `is_frame` = 2, `icon` = 'bx:bxl-spring-boot', `menu_name` = 'menu.monitor_admin' WHERE `menu_id` = 117;
|
||||||
|
UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = '{"url": "https://preview.snailjob.opensnail.com/"}', `is_frame` = 2, `icon` = 'gridicons:scheduled', `menu_name` = 'menu.monitor_snail-job' WHERE `menu_id` = 120;
|
||||||
|
-- 外链类型
|
||||||
|
UPDATE `sys_menu` SET `menu_name` = 'RuoYi-Vue-Plus', `order_num` = 100, `path` = 'https://gitee.com/dromara/RuoYi-Vue-Plus', `component` = 'FrameView', `icon` = 'local-icon-gitee', `remark` = 'RuoYi-Vue-Plus 仓库地址' WHERE `menu_id` = 4;
|
||||||
|
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (7, 'Soybean Admin', 0, 100, 'https://github.com/soybeanjs', 'FrameView', '', 0, 0, 'M', '0', '0', '', 'mdi:github', 103, 1, sysdate(), null, null, 'Soybean Admin 仓库地址') ON DUPLICATE KEY UPDATE `update_time` = sysdate();
|
||||||
|
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (8, 'RuoYi-Plus-Soybean', 0, 100, 'https://gitee.com/xlsea/ruoyi-plus-soybean', 'FrameView', '', 0, 0, 'M', '0', '0', '', 'local-icon-gitee', 103, 1, sysdate(), null, null, 'RuoYi-Plus-Soybean 仓库地址') ON DUPLICATE KEY UPDATE `update_time` = sysdate();
|
||||||
|
|
||||||
|
-- plus-ui 需要禁用的页面
|
||||||
|
UPDATE `sys_menu` SET `status` = '1' WHERE `menu_id` IN ( '116', '130', '131', '132' );
|
||||||
|
-- 工作流需要禁用的页面
|
||||||
|
UPDATE `sys_menu` SET `status` = '1' WHERE `menu_id` IN ( '11616', '11618', '11638', '11700', '11701' );
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { request } from '@/service/request';
|
||||||
|
#set($responseType = "")
|
||||||
|
#if($tplCategory == "tree")
|
||||||
|
#set($responseType = "Api.${ModuleName}.${BusinessName}[]")
|
||||||
|
#else
|
||||||
|
#set($responseType = "Api.${ModuleName}.${BusinessName}List")
|
||||||
|
#end
|
||||||
|
|
||||||
|
/** 获取${functionName}列表 */
|
||||||
|
export function fetchGet${BusinessName}List (params?: Api.${ModuleName}.${BusinessName}SearchParams) {
|
||||||
|
return request<$responseType>({
|
||||||
|
url: '/${moduleName}/${businessName}/list',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/** 新增${functionName} */
|
||||||
|
export function fetchCreate${BusinessName} (data: Api.${ModuleName}.${BusinessName}OperateParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
url: '/${moduleName}/${businessName}',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改${functionName} */
|
||||||
|
export function fetchUpdate${BusinessName} (data: Api.${ModuleName}.${BusinessName}OperateParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
url: '/${moduleName}/${businessName}',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除${functionName} */
|
||||||
|
export function fetchBatchDelete${BusinessName} (${pkColumn.javaField}s: CommonType.IdType[]) {
|
||||||
|
return request<boolean>({
|
||||||
|
url: `/${moduleName}/${businessName}/${${pkColumn.javaField}s.join(',')}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { NDivider } from 'naive-ui';
|
||||||
|
import { jsonClone } from '@sa/utils';
|
||||||
|
import { fetchBatchDelete${BusinessName}, fetchGet${BusinessName}List } from '@/service/api/${moduleName}/${business__name}';
|
||||||
|
import { useAppStore } from '@/store/modules/app';
|
||||||
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
|
import { treeTransform, useNaiveTreeTable, useTableOperate } from '@/hooks/common/table';
|
||||||
|
import { useDownload } from '@/hooks/business/download';
|
||||||
|
import { $t } from '@/locales';
|
||||||
|
import ButtonIcon from '@/components/custom/button-icon.vue';
|
||||||
|
import ${BusinessName}OperateDrawer from './modules/${business__name}-operate-drawer.vue';
|
||||||
|
import ${BusinessName}Search from './modules/${business__name}-search.vue';
|
||||||
|
#if($dictList && $dictList.size() > 0)
|
||||||
|
import { useDict } from '@/hooks/business/dict';
|
||||||
|
#end
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: '${BusinessName}List'
|
||||||
|
});
|
||||||
|
|
||||||
|
#if($dictList && $dictList.size() > 0)
|
||||||
|
#foreach($dict in $dictList)
|
||||||
|
useDict('${dict.type}'#if(!$dict.immediate), false#end);
|
||||||
|
#end#end
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { download } = useDownload();
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
|
||||||
|
const searchParams = ref<Api.$ModuleName.${BusinessName}SearchParams>({
|
||||||
|
#foreach ($column in $columns)
|
||||||
|
#if($column.query)
|
||||||
|
$column.javaField: null#if($foreach.hasNext),#end
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
params: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
columns,
|
||||||
|
columnChecks,
|
||||||
|
data,
|
||||||
|
rows,
|
||||||
|
getData,
|
||||||
|
loading,
|
||||||
|
expandedRowKeys,
|
||||||
|
isCollapse,
|
||||||
|
expandAll,
|
||||||
|
collapseAll,
|
||||||
|
scrollX
|
||||||
|
} = useNaiveTreeTable({
|
||||||
|
keyField: '$pkColumn.javaField',
|
||||||
|
api: () => fetchGet${BusinessName}List(searchParams.value),
|
||||||
|
transform: response => treeTransform(response, { idField: '$pkColumn.javaField' }),
|
||||||
|
columns: () => [
|
||||||
|
{
|
||||||
|
type: 'selection',
|
||||||
|
align: 'center',
|
||||||
|
width: 48
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'index',
|
||||||
|
title: $t('common.index'),
|
||||||
|
align: 'center',
|
||||||
|
width: 64,
|
||||||
|
render: (_, index) => index + 1
|
||||||
|
},
|
||||||
|
#foreach ($column in $columns)
|
||||||
|
#if($column.list)
|
||||||
|
{
|
||||||
|
key: '$column.javaField',
|
||||||
|
title: '$column.columnComment',
|
||||||
|
align: 'center',
|
||||||
|
minWidth: 120#if($column.dictType),
|
||||||
|
render(row) {
|
||||||
|
return <DictTag value={row.$column.javaField} dictCode="$column.dictType" />;
|
||||||
|
}#end
|
||||||
|
},
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
{
|
||||||
|
key: 'operate',
|
||||||
|
title: $t('common.operate'),
|
||||||
|
align: 'center',
|
||||||
|
width: 130,
|
||||||
|
render: row => {
|
||||||
|
const addBtn = () => {
|
||||||
|
return (
|
||||||
|
<ButtonIcon
|
||||||
|
text
|
||||||
|
type="primary"
|
||||||
|
icon="material-symbols:add-2-rounded"
|
||||||
|
tooltipContent={$t('common.add')}
|
||||||
|
onClick={() => addInRow(row)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editBtn = () => {
|
||||||
|
return (
|
||||||
|
<ButtonIcon
|
||||||
|
text
|
||||||
|
type="primary"
|
||||||
|
icon="material-symbols:drive-file-rename-outline-outline"
|
||||||
|
tooltipContent={$t('common.edit')}
|
||||||
|
onClick={() => edit(row.$pkColumn.javaField)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteBtn = () => {
|
||||||
|
return (
|
||||||
|
<ButtonIcon
|
||||||
|
text
|
||||||
|
type="error"
|
||||||
|
icon="material-symbols:delete-outline"
|
||||||
|
tooltipContent={$t('common.delete')}
|
||||||
|
popconfirmContent={$t('common.confirmDelete')}
|
||||||
|
onPositiveClick={() => handleDelete(row.$pkColumn.javaField)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttons = [];
|
||||||
|
if (hasAuth('${moduleName}:${businessName}:add')) buttons.push(addBtn());
|
||||||
|
if (hasAuth('${moduleName}:${businessName}:edit')) buttons.push(editBtn());
|
||||||
|
if (hasAuth('${moduleName}:${businessName}:remove')) buttons.push(deleteBtn());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex-center gap-8px">
|
||||||
|
{buttons.map((btn, index) => (
|
||||||
|
<>
|
||||||
|
{index !== 0 && <NDivider vertical />}
|
||||||
|
{btn}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
|
||||||
|
useTableOperate(rows, '$pkColumn.javaField', getData);
|
||||||
|
|
||||||
|
async function handleBatchDelete() {
|
||||||
|
// request
|
||||||
|
const { error } = await fetchBatchDelete${BusinessName}(checkedRowKeys.value);
|
||||||
|
if (error) return;
|
||||||
|
onBatchDeleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete($pkColumn.javaField: CommonType.IdType) {
|
||||||
|
// request
|
||||||
|
const { error } = await fetchBatchDelete${BusinessName}([$pkColumn.javaField]);
|
||||||
|
if (error) return;
|
||||||
|
onDeleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit($pkColumn.javaField: CommonType.IdType) {
|
||||||
|
handleEdit($pkColumn.javaField);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addInRow(row: Api.$ModuleName.${BusinessName}) {
|
||||||
|
editingData.value = jsonClone(row);
|
||||||
|
handleAdd();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExport() {
|
||||||
|
download('/${moduleName}/${businessName}/export', searchParams.value, `${functionName}_#[[${new Date().getTime()}]]#.xlsx`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||||
|
<${BusinessName}Search v-model:model="searchParams" @search="getData" />
|
||||||
|
<NCard title="${functionName}列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
|
||||||
|
<template #header-extra>
|
||||||
|
<TableHeaderOperation
|
||||||
|
v-model:columns="columnChecks"
|
||||||
|
:disabled-delete="checkedRowKeys.length === 0"
|
||||||
|
:loading="loading"
|
||||||
|
:show-add="hasAuth('${moduleName}:${businessName}:add')"
|
||||||
|
:show-delete="hasAuth('${moduleName}:${businessName}:remove')"
|
||||||
|
:show-export="false"
|
||||||
|
@add="handleAdd"
|
||||||
|
@delete="handleBatchDelete"
|
||||||
|
@export="handleExport"
|
||||||
|
@refresh="getData"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<NButton v-if="!isCollapse" :disabled="!data.length" size="small" @click="expandAll">
|
||||||
|
<template #icon>
|
||||||
|
<icon-quill-expand />
|
||||||
|
</template>
|
||||||
|
全部展开
|
||||||
|
</NButton>
|
||||||
|
<NButton v-if="isCollapse" :disabled="!data.length" size="small" @click="collapseAll">
|
||||||
|
<template #icon>
|
||||||
|
<icon-quill-collapse />
|
||||||
|
</template>
|
||||||
|
全部收起
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
</TableHeaderOperation>
|
||||||
|
</template>
|
||||||
|
<NDataTable
|
||||||
|
v-model:checked-row-keys="checkedRowKeys"
|
||||||
|
v-model:expanded-row-keys="expandedRowKeys"
|
||||||
|
:columns="columns"
|
||||||
|
:data="data"
|
||||||
|
size="small"
|
||||||
|
:flex-height="!appStore.isMobile"
|
||||||
|
:scroll-x="scrollX"
|
||||||
|
:loading="loading"
|
||||||
|
remote
|
||||||
|
:row-key="row => row.#foreach($column in $columns)#if($column.isPk == '1')$column.javaField#end#end"
|
||||||
|
class="sm:h-full"
|
||||||
|
/>
|
||||||
|
<${BusinessName}OperateDrawer
|
||||||
|
v-model:visible="drawerVisible"
|
||||||
|
:operate-type="operateType"
|
||||||
|
:row-data="editingData"
|
||||||
|
@submitted="getData"
|
||||||
|
/>
|
||||||
|
</NCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { NDivider } from 'naive-ui';
|
||||||
|
import { fetchBatchDelete${BusinessName}, fetchGet${BusinessName}List } from '@/service/api/${moduleName}/${business__name}';
|
||||||
|
import { useAppStore } from '@/store/modules/app';
|
||||||
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
|
import { useDownload } from '@/hooks/business/download';
|
||||||
|
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
|
||||||
|
import { $t } from '@/locales';
|
||||||
|
import ButtonIcon from '@/components/custom/button-icon.vue';
|
||||||
|
import ${BusinessName}OperateDrawer from './modules/${business__name}-operate-drawer.vue';
|
||||||
|
import ${BusinessName}Search from './modules/${business__name}-search.vue';
|
||||||
|
#if($dictList && $dictList.size() > 0)
|
||||||
|
import { useDict } from '@/hooks/business/dict';
|
||||||
|
#end
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: '${BusinessName}List'
|
||||||
|
});
|
||||||
|
|
||||||
|
#if($dictList && $dictList.size() > 0)
|
||||||
|
#foreach($dict in $dictList)
|
||||||
|
useDict('${dict.type}'#if(!$dict.immediate), false#end);
|
||||||
|
#end#end
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { download } = useDownload();
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
|
||||||
|
const searchParams = ref<Api.$ModuleName.${BusinessName}SearchParams>({
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
#foreach ($column in $columns)
|
||||||
|
#if($column.query)
|
||||||
|
$column.javaField: null#if($foreach.hasNext),#end
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
params: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination, scrollX } =
|
||||||
|
useNaivePaginatedTable({
|
||||||
|
api: () => fetchGet${BusinessName}List(searchParams.value),
|
||||||
|
transform: response => defaultTransform(response),
|
||||||
|
onPaginationParamsChange: params => {
|
||||||
|
searchParams.value.pageNum = params.page;
|
||||||
|
searchParams.value.pageSize = params.pageSize;
|
||||||
|
},
|
||||||
|
columns: () => [
|
||||||
|
{
|
||||||
|
type: 'selection',
|
||||||
|
align: 'center',
|
||||||
|
width: 48
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'index',
|
||||||
|
title: $t('common.index'),
|
||||||
|
align: 'center',
|
||||||
|
width: 64,
|
||||||
|
render: (_, index) => index + 1
|
||||||
|
},
|
||||||
|
#foreach ($column in $columns)
|
||||||
|
#if($column.list)
|
||||||
|
{
|
||||||
|
key: '$column.javaField',
|
||||||
|
title: '$column.columnComment',
|
||||||
|
align: 'center',
|
||||||
|
minWidth: 120#if($column.dictType),
|
||||||
|
render(row) {
|
||||||
|
return <DictTag value={row.$column.javaField} dictCode="$column.dictType" />;
|
||||||
|
}#end
|
||||||
|
},
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
{
|
||||||
|
key: 'operate',
|
||||||
|
title: $t('common.operate'),
|
||||||
|
align: 'center',
|
||||||
|
width: 130,
|
||||||
|
render: row => {
|
||||||
|
const divider = () => {
|
||||||
|
if (!hasAuth('${moduleName}:${businessName}:edit') || !hasAuth('${moduleName}:${businessName}:remove')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <NDivider vertical />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const editBtn = () => {
|
||||||
|
if (!hasAuth('${moduleName}:${businessName}:edit')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ButtonIcon
|
||||||
|
text
|
||||||
|
type="primary"
|
||||||
|
icon="material-symbols:drive-file-rename-outline-outline"
|
||||||
|
tooltipContent={$t('common.edit')}
|
||||||
|
onClick={() => edit(row.$pkColumn.javaField)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteBtn = () => {
|
||||||
|
if (!hasAuth('${moduleName}:${businessName}:remove')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ButtonIcon
|
||||||
|
text
|
||||||
|
type="error"
|
||||||
|
icon="material-symbols:delete-outline"
|
||||||
|
tooltipContent={$t('common.delete')}
|
||||||
|
popconfirmContent={$t('common.confirmDelete')}
|
||||||
|
onPositiveClick={() => handleDelete(row.$pkColumn.javaField)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex-center gap-8px">
|
||||||
|
{editBtn()}
|
||||||
|
{divider()}
|
||||||
|
{deleteBtn()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
|
||||||
|
useTableOperate(data, '$pkColumn.javaField', getData);
|
||||||
|
|
||||||
|
async function handleBatchDelete() {
|
||||||
|
// request
|
||||||
|
const { error } = await fetchBatchDelete${BusinessName}(checkedRowKeys.value);
|
||||||
|
if (error) return;
|
||||||
|
onBatchDeleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete($pkColumn.javaField: CommonType.IdType) {
|
||||||
|
// request
|
||||||
|
const { error } = await fetchBatchDelete${BusinessName}([$pkColumn.javaField]);
|
||||||
|
if (error) return;
|
||||||
|
onDeleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit($pkColumn.javaField: CommonType.IdType) {
|
||||||
|
handleEdit($pkColumn.javaField);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExport() {
|
||||||
|
download('/${moduleName}/${businessName}/export', searchParams.value, `${functionName}_#[[${new Date().getTime()}]]#.xlsx`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||||
|
<${BusinessName}Search v-model:model="searchParams" @search="getDataByPage" />
|
||||||
|
<NCard title="${functionName}列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
|
||||||
|
<template #header-extra>
|
||||||
|
<TableHeaderOperation
|
||||||
|
v-model:columns="columnChecks"
|
||||||
|
:disabled-delete="checkedRowKeys.length === 0"
|
||||||
|
:loading="loading"
|
||||||
|
:show-add="hasAuth('${moduleName}:${businessName}:add')"
|
||||||
|
:show-delete="hasAuth('${moduleName}:${businessName}:remove')"
|
||||||
|
:show-export="hasAuth('${moduleName}:${businessName}:export')"
|
||||||
|
@add="handleAdd"
|
||||||
|
@delete="handleBatchDelete"
|
||||||
|
@export="handleExport"
|
||||||
|
@refresh="getData"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<NDataTable
|
||||||
|
v-model:checked-row-keys="checkedRowKeys"
|
||||||
|
:columns="columns"
|
||||||
|
:data="data"
|
||||||
|
size="small"
|
||||||
|
:flex-height="!appStore.isMobile"
|
||||||
|
:scroll-x="scrollX"
|
||||||
|
:loading="loading"
|
||||||
|
remote
|
||||||
|
:row-key="row => row.$pkColumn.javaField"
|
||||||
|
:pagination="mobilePagination"
|
||||||
|
class="sm:h-full"
|
||||||
|
/>
|
||||||
|
<${BusinessName}OperateDrawer
|
||||||
|
v-model:visible="drawerVisible"
|
||||||
|
:operate-type="operateType"
|
||||||
|
:row-data="editingData"
|
||||||
|
@submitted="getDataByPage"
|
||||||
|
/>
|
||||||
|
</NCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { jsonClone } from '@sa/utils';
|
||||||
|
import { fetchCreate${BusinessName}, fetchUpdate${BusinessName}#if($tplCategory == 'tree'), fetchGet${BusinessName}List#end } from '@/service/api/${moduleName}/${business__name}';
|
||||||
|
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||||
|
#if($tplCategory == 'tree')
|
||||||
|
import { handleTree } from '@/utils/common';
|
||||||
|
#end
|
||||||
|
import { $t } from '@/locales';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: '${BusinessName}OperateDrawer'
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** the type of operation */
|
||||||
|
operateType: NaiveUI.TableOperateType;
|
||||||
|
/** the edit row data */
|
||||||
|
rowData?: Api.$ModuleName.${BusinessName} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'submitted'): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', {
|
||||||
|
default: false
|
||||||
|
});
|
||||||
|
#if($tplCategory == 'tree')
|
||||||
|
|
||||||
|
const treeList = ref<Api.Demo.Tree[]>([]);
|
||||||
|
#end
|
||||||
|
|
||||||
|
const { formRef, validate, restoreValidation } = useNaiveForm();
|
||||||
|
const { createRequiredRule } = useFormRules();
|
||||||
|
|
||||||
|
const title = computed(() => {
|
||||||
|
const titles: Record<NaiveUI.TableOperateType, string> = {
|
||||||
|
add: '新增${functionName}',
|
||||||
|
edit: '编辑${functionName}'
|
||||||
|
};
|
||||||
|
return titles[props.operateType];
|
||||||
|
});
|
||||||
|
|
||||||
|
type Model = Api.$ModuleName.${BusinessName}OperateParams;
|
||||||
|
|
||||||
|
const model = ref<Model>(createDefaultModel());
|
||||||
|
|
||||||
|
function createDefaultModel(): Model {
|
||||||
|
return {
|
||||||
|
#foreach($column in $columns)
|
||||||
|
#if($column.insert() || $column.isEdit())
|
||||||
|
${column.javaField}:#if($column.javaType == 'String' || ($!column.dictType && $column.dictType != '')) ''#else null#end#if($foreach.hasNext),#end
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleKey = Extract<
|
||||||
|
keyof Model,
|
||||||
|
#foreach($column in $columns)
|
||||||
|
#if($column.required)
|
||||||
|
| '$column.javaField'#if($foreach.hasNext)#end
|
||||||
|
#end#end>;
|
||||||
|
|
||||||
|
const rules: Record<RuleKey, App.Global.FormRule> = {
|
||||||
|
#foreach($column in $columns)
|
||||||
|
#if($column.required)
|
||||||
|
$column.javaField: createRequiredRule('${column.columnComment}不能为空')#if($foreach.hasNext),#end
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleUpdateModelWhenEdit() {
|
||||||
|
model.value = createDefaultModel();
|
||||||
|
#if($tplCategory == 'tree')
|
||||||
|
model.value.$treeParentCode = props.rowData?.$treeCode || 0;
|
||||||
|
#end
|
||||||
|
|
||||||
|
if (props.operateType === 'edit' && props.rowData) {
|
||||||
|
Object.assign(model.value, jsonClone(props.rowData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDrawer() {
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
await validate();
|
||||||
|
|
||||||
|
#set($operateColumns = [])
|
||||||
|
#foreach($column in $columns)#if($column.insert || $column.edit)#set($dummy = $operateColumns.add($column))#end#end
|
||||||
|
const { #foreach($column in $operateColumns)$column.javaField#if($foreach.hasNext), #end#end } = model.value;
|
||||||
|
|
||||||
|
// request
|
||||||
|
if (props.operateType === 'add') {
|
||||||
|
#set($addFields = [])
|
||||||
|
#foreach($column in $columns)#if($column.insert)#set($dummy = $addFields.add($column.javaField))#end#end
|
||||||
|
const { error } = await fetchCreate${BusinessName}({ #foreach($field in $addFields)$field#if($foreach.hasNext), #end#end });
|
||||||
|
if (error) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.operateType === 'edit') {
|
||||||
|
#set($editFields = [])
|
||||||
|
#foreach($column in $columns)#if($column.edit)#set($dummy = $editFields.add($column.javaField))#end#end
|
||||||
|
const { error } = await fetchUpdate${BusinessName}({ #foreach($field in $editFields)$field#if($foreach.hasNext), #end#end });
|
||||||
|
if (error) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.$message?.success($t('common.updateSuccess'));
|
||||||
|
closeDrawer();
|
||||||
|
emit('submitted');
|
||||||
|
}
|
||||||
|
#if($tplCategory == 'tree')
|
||||||
|
|
||||||
|
async function getTreeList() {
|
||||||
|
const { data, error } = await fetchGet${BusinessName}List();
|
||||||
|
if (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { tree } = handleTree(data);
|
||||||
|
treeList.value = tree;
|
||||||
|
}
|
||||||
|
#end
|
||||||
|
|
||||||
|
watch(visible, () => {
|
||||||
|
if (visible.value) {
|
||||||
|
handleUpdateModelWhenEdit();
|
||||||
|
restoreValidation();
|
||||||
|
getTreeList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
#if($tplCategory == 'tree')
|
||||||
|
|
||||||
|
const treeOptions = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
treeName: '顶级节点',
|
||||||
|
children: treeList.value
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
#end
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="800" class="max-w-90%">
|
||||||
|
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||||
|
<NForm ref="formRef" :model="model" :rules="rules">
|
||||||
|
#set($immediateDictList = [])
|
||||||
|
#foreach($column in $columns)
|
||||||
|
#set($field=$column.javaField)
|
||||||
|
#if(($column.insert || $column.edit) && !$column.pk)
|
||||||
|
#set($isImmediate = !$column.isList() && !$column.isQuery() && !$immediateDictList.contains($column.dictType))
|
||||||
|
<NFormItem label="$column.columnComment" path="$column.javaField">
|
||||||
|
#if($tplCategory == 'tree' && $column.javaField == $treeParentCode)
|
||||||
|
<NTreeSelect
|
||||||
|
v-model:value="model.$treeParentCode"
|
||||||
|
filterable
|
||||||
|
class="h-full"
|
||||||
|
key-field="$treeCode"
|
||||||
|
label-field="$treeName"
|
||||||
|
:options="treeOptions"
|
||||||
|
:default-expanded-keys="[0]"
|
||||||
|
/>
|
||||||
|
#elseif($column.htmlType == "textarea" || $column.htmlType == "editor")
|
||||||
|
<NInput
|
||||||
|
v-model:value="model.$column.javaField"
|
||||||
|
:rows="3"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入$column.columnComment"
|
||||||
|
/>
|
||||||
|
#elseif($column.htmlType == "select" && $column.dictType)
|
||||||
|
<DictSelect
|
||||||
|
v-model:value="model.$column.javaField"
|
||||||
|
placeholder="请选择$column.columnComment"
|
||||||
|
dict-code="$column.dictType"
|
||||||
|
clearable
|
||||||
|
#if($isImmediate)#set($void = $immediateDictList.add($column.dictType))
|
||||||
|
immediate
|
||||||
|
#end
|
||||||
|
/>
|
||||||
|
#elseif($column.htmlType == "select" && !$column.dictType)
|
||||||
|
<NSelect
|
||||||
|
v-model:value="model.$column.javaField"
|
||||||
|
placeholder="请选择$column.columnComment"
|
||||||
|
:options="[{ value: '0', label: '请选择字典生成' }]"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
#elseif($column.htmlType == "radio" && $column.dictType)
|
||||||
|
<DictRadio
|
||||||
|
v-model:value="model.$column.javaField"
|
||||||
|
placeholder="请选择$column.columnComment"
|
||||||
|
dict-code="$column.dictType"
|
||||||
|
clearable
|
||||||
|
#if($isImmediate)#set($void = $immediateDictList.add($column.dictType))
|
||||||
|
immediate
|
||||||
|
#end
|
||||||
|
/>
|
||||||
|
#elseif($column.htmlType == "radio" && !$column.dictType)
|
||||||
|
<NRadioGroup v-model:value="model.$column.javaField">
|
||||||
|
<NSpace>
|
||||||
|
<NRadio value="0" label="请选择字典生成" />
|
||||||
|
</NSpace>
|
||||||
|
</NRadioGroup>
|
||||||
|
#elseif($column.htmlType == "checkbox" && $column.dictType)
|
||||||
|
<DictCheckbox
|
||||||
|
v-model:value="model.$column.javaField"
|
||||||
|
placeholder="请选择$column.columnComment"
|
||||||
|
dict-code="$column.dictType"
|
||||||
|
clearable
|
||||||
|
#if($isImmediate)#set($void = $immediateDictList.add($column.dictType))
|
||||||
|
immediate
|
||||||
|
#end
|
||||||
|
/>
|
||||||
|
#elseif($column.htmlType == "checkbox" && $column.dictType)
|
||||||
|
<NCheckboxGroup v-model:value="model.$column.javaField">
|
||||||
|
<NSpace>
|
||||||
|
<NCheckbox value="0" label="请选择字典生成" />
|
||||||
|
</NSpace>
|
||||||
|
</NCheckboxGroup>
|
||||||
|
#elseif($column.htmlType == 'datetime')
|
||||||
|
<NDatePicker
|
||||||
|
v-model:formatted-value="model.$column.javaField"
|
||||||
|
type="datetime"
|
||||||
|
value-format="yyyy-MM-dd HH:mm:ss"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
#elseif($column.htmlType == "imageUpload")
|
||||||
|
<OssUpload v-model:value="model.$column.javaField" upload-type="image" />
|
||||||
|
#elseif($column.htmlType == "fileUpload")
|
||||||
|
<OssUpload v-model:value="model.$column.javaField" upload-type="file" />
|
||||||
|
#elseif($column.htmlType == "editor")
|
||||||
|
<TinymceEditor v-model:value="model.$column.javaField" />
|
||||||
|
#else <NInput v-model:value="model.$column.javaField" placeholder="请输入$column.columnComment" />
|
||||||
|
#end
|
||||||
|
</NFormItem>
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
</NForm>
|
||||||
|
<template #footer>
|
||||||
|
<NSpace :size="16">
|
||||||
|
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
|
||||||
|
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
|
||||||
|
</NSpace>
|
||||||
|
</template>
|
||||||
|
</NDrawerContent>
|
||||||
|
</NDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
#set($ModuleName=$moduleName.substring(0, 1).toUpperCase() + $moduleName.substring(1))
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { toRaw } from 'vue';
|
||||||
|
import { jsonClone } from '@sa/utils';
|
||||||
|
import { useNaiveForm } from '@/hooks/common/form';
|
||||||
|
import { $t } from '@/locales';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: '${BusinessName}Search'
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'search'): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const { formRef, validate, restoreValidation } = useNaiveForm();
|
||||||
|
|
||||||
|
#foreach ($column in $columns)
|
||||||
|
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
|
||||||
|
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
|
||||||
|
const dateRange${AttrName} = ref<[string, string] | null>(null);
|
||||||
|
#end#end
|
||||||
|
const model = defineModel<Api.$ModuleName.${BusinessName}SearchParams>('model', { required: true });
|
||||||
|
|
||||||
|
const defaultModel = jsonClone(toRaw(model.value));
|
||||||
|
|
||||||
|
function resetModel() {
|
||||||
|
#foreach ($column in $columns)
|
||||||
|
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
|
||||||
|
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
|
||||||
|
dateRange${AttrName}.value = null;
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
Object.assign(model.value, defaultModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reset() {
|
||||||
|
await restoreValidation();
|
||||||
|
resetModel();
|
||||||
|
emit('search');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function search() {
|
||||||
|
await validate();
|
||||||
|
#foreach ($column in $columns)
|
||||||
|
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
|
||||||
|
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
|
||||||
|
if (dateRange${AttrName}.value?.length) {
|
||||||
|
model.value.params!.begin${AttrName} = dateRange${AttrName}.value[0];
|
||||||
|
model.value.params!.end${AttrName} = dateRange${AttrName}.value[1];
|
||||||
|
}
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
emit('search');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NCard :bordered="false" size="small" class="card-wrapper">
|
||||||
|
<NCollapse>
|
||||||
|
<NCollapseItem :title="$t('common.search')" name="$moduleName-${business__name}-search">
|
||||||
|
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
|
||||||
|
<NGrid responsive="screen" item-responsive>
|
||||||
|
#set($immediateDictList = [])
|
||||||
|
#foreach($column in $columns)
|
||||||
|
#if($column.query)
|
||||||
|
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
|
||||||
|
<NFormItemGi span="24 s:12 m:6" label="$column.columnComment" label-width="auto" path="$column.javaField" class="pr-24px">
|
||||||
|
#if($!StrUtil.contains("select, radio, checkbox", $column.htmlType) && $column.dictType)
|
||||||
|
<DictSelect
|
||||||
|
v-model:value="model.$column.javaField"
|
||||||
|
placeholder="请选择$column.columnComment"
|
||||||
|
dict-code="$column.dictType"
|
||||||
|
clearable
|
||||||
|
#if(!$column.isList() && !$immediateDictList.contains($column.dictType))#set($void = $immediateDictList.add($column.dictType))
|
||||||
|
immediate
|
||||||
|
#end
|
||||||
|
/>
|
||||||
|
#elseif($!StrUtil.contains("select, radio, checkbox", $column.htmlType))
|
||||||
|
<NSelect
|
||||||
|
v-model:value="model.$column.javaField"
|
||||||
|
placeholder="请选择$column.columnComment"
|
||||||
|
:options="[]"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
#elseif($column.htmlType == 'datetime' && $column.queryType != "BETWEEN")
|
||||||
|
<NDatePicker
|
||||||
|
v-model:formatted-value="model.$column.javaField"
|
||||||
|
type="datetime"
|
||||||
|
value-format="yyyy-MM-dd HH:mm:ss"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
#elseif($column.htmlType == 'datetime' && $column.queryType == "BETWEEN")
|
||||||
|
<NDatePicker
|
||||||
|
v-model:formatted-value="dateRange${AttrName}"
|
||||||
|
type="datetimerange"
|
||||||
|
value-format="yyyy-MM-dd HH:mm:ss"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
#else <NInput v-model:value="model.$column.javaField" placeholder="请输入$column.columnComment" />
|
||||||
|
#end
|
||||||
|
</NFormItemGi>
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
<NFormItemGi :show-feedback="false" span="24" class="pr-24px">
|
||||||
|
<NSpace class="w-full" justify="end">
|
||||||
|
<NButton @click="reset">
|
||||||
|
<template #icon>
|
||||||
|
<icon-ic-round-refresh class="text-icon" />
|
||||||
|
</template>
|
||||||
|
{{ $t('common.reset') }}
|
||||||
|
</NButton>
|
||||||
|
<NButton type="primary" ghost @click="search">
|
||||||
|
<template #icon>
|
||||||
|
<icon-ic-round-search class="text-icon" />
|
||||||
|
</template>
|
||||||
|
{{ $t('common.search') }}
|
||||||
|
</NButton>
|
||||||
|
</NSpace>
|
||||||
|
</NFormItemGi>
|
||||||
|
</NGrid>
|
||||||
|
</NForm>
|
||||||
|
</NCollapseItem>
|
||||||
|
</NCollapse>
|
||||||
|
</NCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
#set($BaseEntity = ['createDept', 'createBy', 'createTime', 'updateBy', 'updateTime'])
|
||||||
|
#set($ModuleName = $moduleName.substring(0, 1).toUpperCase() + $moduleName.substring(1))
|
||||||
|
/**
|
||||||
|
* Namespace Api
|
||||||
|
*
|
||||||
|
* All backend api type
|
||||||
|
*/
|
||||||
|
declare namespace Api {
|
||||||
|
/**
|
||||||
|
* namespace ${ModuleName}
|
||||||
|
*
|
||||||
|
* backend api module: "${ModuleName}"
|
||||||
|
*/
|
||||||
|
namespace ${ModuleName} {
|
||||||
|
/** ${businessname} */
|
||||||
|
type ${BusinessName} = Common.CommonRecord<{
|
||||||
|
#foreach($column in $columns)#if(!$BaseEntity.contains($column.javaField))
|
||||||
|
/** $column.columnComment */
|
||||||
|
$column.javaField:#if($column.javaField.indexOf("id") != -1 || $column.javaField.indexOf("Id") != -1) CommonType.IdType; #elseif($column.javaType == 'Long' || $column.javaType == 'Integer' || $column.javaType == 'Double' || $column.javaType == 'Float' || $column.javaType == 'BigDecimal') number; #elseif($column.javaType == 'Boolean') boolean; #else string; #end
|
||||||
|
#end#end
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/** ${businessname} search params */
|
||||||
|
type ${BusinessName}SearchParams = CommonType.RecordNullable<
|
||||||
|
Pick<
|
||||||
|
Api.${ModuleName}.${BusinessName},
|
||||||
|
#foreach($column in $columns)
|
||||||
|
#if($column.query && $column.queryType != 'BETWEEN')
|
||||||
|
| '${column.javaField}'
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
> &
|
||||||
|
Api.Common.CommonSearchParams
|
||||||
|
>;
|
||||||
|
|
||||||
|
/** ${businessname} operate params */
|
||||||
|
type ${BusinessName}OperateParams = CommonType.RecordNullable<
|
||||||
|
Pick<
|
||||||
|
Api.${ModuleName}.${BusinessName},
|
||||||
|
#foreach($column in $columns)
|
||||||
|
#if($column.insert || $column.edit)
|
||||||
|
| '${column.javaField}'
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
|
/** ${businessname} list */
|
||||||
|
type ${BusinessName}List = Api.Common.PaginatingQueryRecord<${BusinessName}>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { defineConfig } from '@soybeanjs/eslint-config';
|
||||||
|
|
||||||
|
export default defineConfig(
|
||||||
|
{ vue: true, unocss: true },
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'vue/multi-word-component-names': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
ignores: ['index', 'App', 'Register', '[id]', '[url]']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'vue/component-name-in-template-casing': [
|
||||||
|
'warn',
|
||||||
|
'PascalCase',
|
||||||
|
{
|
||||||
|
registeredComponentsOnly: false,
|
||||||
|
ignores: ['/^icon-/']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'unocss/order-attributify': 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-cmn-Hans">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="color-scheme" content="light dark" />
|
||||||
|
<title>%VITE_APP_TITLE%</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
{
|
||||||
|
"name": "ruoyi-vue-plus",
|
||||||
|
"type": "module",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。",
|
||||||
|
"author": {
|
||||||
|
"name": "xlsea",
|
||||||
|
"email": "m@xlsea.cn",
|
||||||
|
"url": "https://gitee.com/xlsea"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://docs.ruoyi.xlsea.cn",
|
||||||
|
"repository": {
|
||||||
|
"url": "https://gitee.com/xlsea/ruoyi-plus-soybean.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://gitee.com/xlsea/ruoyi-plus-soybean/issues"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"RuoYi-Vue-Plus",
|
||||||
|
"Soybean Admin",
|
||||||
|
"Vue3 admin ",
|
||||||
|
"vue-admin-template",
|
||||||
|
"Vite7",
|
||||||
|
"TypeScript",
|
||||||
|
"naive-ui",
|
||||||
|
"naive-ui-admin",
|
||||||
|
"ant-design-vue v4",
|
||||||
|
"UnoCSS"
|
||||||
|
],
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Elio",
|
||||||
|
"email": "1983933789@qq.com",
|
||||||
|
"url": "https://gitee.com/elio-an"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0",
|
||||||
|
"pnpm": ">=10.5.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build --mode prod",
|
||||||
|
"build:dev": "vite build --mode dev",
|
||||||
|
"build:test": "vite build --mode test",
|
||||||
|
"cleanup": "sa cleanup",
|
||||||
|
"commit": "sa git-commit",
|
||||||
|
"commit:zh": "sa git-commit -l=zh-cn",
|
||||||
|
"dev": "vite --mode dev",
|
||||||
|
"dev:prod": "vite --mode prod",
|
||||||
|
"dev:test": "vite --mode test",
|
||||||
|
"gen-route": "sa gen-route",
|
||||||
|
"lint": "eslint . --fix",
|
||||||
|
"prepare": "simple-git-hooks",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"release": "sa release",
|
||||||
|
"typecheck": "vue-tsc --noEmit --skipLibCheck",
|
||||||
|
"update-pkg": "sa update-pkg"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@better-scroll/core": "2.5.1",
|
||||||
|
"@iconify/vue": "5.0.0",
|
||||||
|
"@sa/axios": "workspace:*",
|
||||||
|
"@sa/color": "workspace:*",
|
||||||
|
"@sa/hooks": "workspace:*",
|
||||||
|
"@sa/materials": "workspace:*",
|
||||||
|
"@sa/utils": "workspace:*",
|
||||||
|
"@types/streamsaver": "^2.0.5",
|
||||||
|
"@umoteam/editor": "^9.0.1",
|
||||||
|
"@vueuse/core": "14.1.0",
|
||||||
|
"clipboard": "2.0.11",
|
||||||
|
"dayjs": "1.11.19",
|
||||||
|
"defu": "6.1.4",
|
||||||
|
"echarts": "6.0.0",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
|
"jsencrypt": "^3.5.4",
|
||||||
|
"json5": "2.2.3",
|
||||||
|
"naive-ui": "2.43.2",
|
||||||
|
"nprogress": "0.2.0",
|
||||||
|
"pinia": "3.0.4",
|
||||||
|
"streamsaver": "^2.0.6",
|
||||||
|
"tailwind-merge": "3.4.0",
|
||||||
|
"vue": "3.5.26",
|
||||||
|
"vue-advanced-cropper": "^2.8.9",
|
||||||
|
"vue-draggable-plus": "0.6.0",
|
||||||
|
"vue-i18n": "11.2.7",
|
||||||
|
"vue-router": "4.6.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@elegant-router/vue": "0.3.8",
|
||||||
|
"@iconify/json": "2.2.417",
|
||||||
|
"@sa/scripts": "workspace:*",
|
||||||
|
"@sa/uno-preset": "workspace:*",
|
||||||
|
"@soybeanjs/eslint-config": "1.7.4",
|
||||||
|
"@types/node": "25.0.3",
|
||||||
|
"@types/nprogress": "0.2.3",
|
||||||
|
"@unocss/eslint-config": "66.5.10",
|
||||||
|
"@unocss/preset-icons": "66.5.10",
|
||||||
|
"@unocss/preset-uno": "66.5.10",
|
||||||
|
"@unocss/transformer-directives": "66.5.10",
|
||||||
|
"@unocss/transformer-variant-group": "66.5.10",
|
||||||
|
"@unocss/vite": "66.5.10",
|
||||||
|
"@vitejs/plugin-vue": "6.0.3",
|
||||||
|
"@vitejs/plugin-vue-jsx": "5.1.2",
|
||||||
|
"consola": "3.4.2",
|
||||||
|
"eslint": "9.39.2",
|
||||||
|
"eslint-plugin-vue": "10.6.2",
|
||||||
|
"kolorist": "1.8.0",
|
||||||
|
"sass": "1.97.1",
|
||||||
|
"simple-git-hooks": "2.13.1",
|
||||||
|
"tsx": "4.21.0",
|
||||||
|
"typescript": "5.9.3",
|
||||||
|
"unplugin-icons": "22.5.0",
|
||||||
|
"unplugin-vue-components": "30.0.0",
|
||||||
|
"vite": "7.3.0",
|
||||||
|
"vite-plugin-progress": "0.0.7",
|
||||||
|
"vite-plugin-svg-icons": "2.0.1",
|
||||||
|
"vite-plugin-vue-devtools": "8.0.5",
|
||||||
|
"vue-eslint-parser": "10.2.0",
|
||||||
|
"vue-tsc": "3.2.1"
|
||||||
|
},
|
||||||
|
"simple-git-hooks": {
|
||||||
|
"commit-msg": "pnpm sa git-commit-verify",
|
||||||
|
"pre-commit": "pnpm typecheck && pnpm lint && git diff --exit-code"
|
||||||
|
},
|
||||||
|
"website": "https://ruoyi.xlsea.cn"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "@sa/alova",
|
||||||
|
"version": "2.0.2",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./fetch": "./src/fetch.ts",
|
||||||
|
"./client": "./src/client.ts",
|
||||||
|
"./mock": "./src/mock.ts"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@alova/mock": "2.0.18",
|
||||||
|
"@sa/utils": "workspace:*",
|
||||||
|
"alova": "3.4.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from 'alova/client';
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
/** the backend error code key */
|
||||||
|
export const BACKEND_ERROR_CODE = 'BACKEND_ERROR';
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
import adapterFetch from 'alova/fetch';
|
||||||
|
export default adapterFetch;
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { createAlova } from 'alova';
|
||||||
|
import type { AlovaDefaultCacheAdapter, AlovaGenerics, AlovaGlobalCacheAdapter, AlovaRequestAdapter } from 'alova';
|
||||||
|
import VueHook from 'alova/vue';
|
||||||
|
import type { VueHookType } from 'alova/vue';
|
||||||
|
import adapterFetch from 'alova/fetch';
|
||||||
|
import { createServerTokenAuthentication } from 'alova/client';
|
||||||
|
import type { FetchRequestInit } from 'alova/fetch';
|
||||||
|
import { BACKEND_ERROR_CODE } from './constant';
|
||||||
|
import type { CustomAlovaConfig, RequestOptions } from './type';
|
||||||
|
|
||||||
|
export const createAlovaRequest = <
|
||||||
|
RequestConfig = FetchRequestInit,
|
||||||
|
ResponseType = Response,
|
||||||
|
ResponseHeader = Headers,
|
||||||
|
L1Cache extends AlovaGlobalCacheAdapter = AlovaDefaultCacheAdapter,
|
||||||
|
L2Cache extends AlovaGlobalCacheAdapter = AlovaDefaultCacheAdapter
|
||||||
|
>(
|
||||||
|
customConfig: CustomAlovaConfig<
|
||||||
|
AlovaGenerics<any, any, RequestConfig, ResponseType, ResponseHeader, L1Cache, L2Cache, any>
|
||||||
|
>,
|
||||||
|
options: RequestOptions<AlovaGenerics<any, any, RequestConfig, ResponseType, ResponseHeader, L1Cache, L2Cache, any>>
|
||||||
|
) => {
|
||||||
|
const { tokenRefresher } = options;
|
||||||
|
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication<
|
||||||
|
VueHookType,
|
||||||
|
AlovaRequestAdapter<RequestConfig, ResponseType, ResponseHeader>
|
||||||
|
>({
|
||||||
|
refreshTokenOnSuccess: {
|
||||||
|
isExpired: (response, method) => tokenRefresher?.isExpired(response, method) || false,
|
||||||
|
handler: async (response, method) => tokenRefresher?.handler(response, method)
|
||||||
|
},
|
||||||
|
refreshTokenOnError: {
|
||||||
|
isExpired: (response, method) => tokenRefresher?.isExpired(response, method) || false,
|
||||||
|
handler: async (response, method) => tokenRefresher?.handler(response, method)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const instance = createAlova({
|
||||||
|
...customConfig,
|
||||||
|
timeout: customConfig.timeout ?? 10 * 1000,
|
||||||
|
requestAdapter: (customConfig.requestAdapter as any) ?? adapterFetch(),
|
||||||
|
statesHook: VueHook,
|
||||||
|
beforeRequest: onAuthRequired(options.onRequest as any),
|
||||||
|
responded: onResponseRefreshToken({
|
||||||
|
onSuccess: async (response, method) => {
|
||||||
|
// check if http status is success
|
||||||
|
let error: any = null;
|
||||||
|
let transformedData: any = null;
|
||||||
|
try {
|
||||||
|
if (await options.isBackendSuccess(response)) {
|
||||||
|
transformedData = await options.transformBackendResponse(response);
|
||||||
|
} else {
|
||||||
|
error = new Error('the backend request error');
|
||||||
|
error.code = BACKEND_ERROR_CODE;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
await options.onError?.(error, response, method);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformedData;
|
||||||
|
},
|
||||||
|
onComplete: options.onComplete,
|
||||||
|
onError: (error, method) => options.onError?.(error, null, method)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { BACKEND_ERROR_CODE };
|
||||||
|
export type * from './type';
|
||||||
|
export type * from 'alova';
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from '@alova/mock';
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import type { AlovaGenerics, AlovaOptions, AlovaRequestAdapter, Method, ResponseCompleteHandler } from 'alova';
|
||||||
|
|
||||||
|
export type CustomAlovaConfig<AG extends AlovaGenerics> = Omit<
|
||||||
|
AlovaOptions<AG>,
|
||||||
|
'statesHook' | 'beforeRequest' | 'responded' | 'requestAdapter'
|
||||||
|
> & {
|
||||||
|
/** request adapter. all request of alova will be sent by it. */
|
||||||
|
requestAdapter?: AlovaRequestAdapter<AG['RequestConfig'], AG['Response'], AG['ResponseHeader']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface RequestOptions<AG extends AlovaGenerics> {
|
||||||
|
/**
|
||||||
|
* The hook before request
|
||||||
|
*
|
||||||
|
* For example: You can add header token in this hook
|
||||||
|
*
|
||||||
|
* @param method alova Method Instance
|
||||||
|
*/
|
||||||
|
onRequest?: AlovaOptions<AG>['beforeRequest'];
|
||||||
|
/**
|
||||||
|
* The hook to check backend response is success or not
|
||||||
|
*
|
||||||
|
* @param response alova response
|
||||||
|
*/
|
||||||
|
isBackendSuccess: (response: AG['Response']) => Promise<boolean>;
|
||||||
|
|
||||||
|
/** The config to refresh token */
|
||||||
|
tokenRefresher?: {
|
||||||
|
/** detect the token is expired */
|
||||||
|
isExpired(response: AG['Response'], Method: Method<AG>): Promise<boolean> | boolean;
|
||||||
|
/** refresh token handler */
|
||||||
|
handler(response: AG['Response'], Method: Method<AG>): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The hook after backend request complete */
|
||||||
|
onComplete?: ResponseCompleteHandler<AG>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hook to handle error
|
||||||
|
*
|
||||||
|
* For example: You can show error message in this hook
|
||||||
|
*
|
||||||
|
* @param error
|
||||||
|
*/
|
||||||
|
onError?: (error: any, response: AG['Response'] | null, methodInstance: Method<AG>) => any | Promise<any>;
|
||||||
|
/**
|
||||||
|
* transform backend response when the responseType is json
|
||||||
|
*
|
||||||
|
* @param response alova response
|
||||||
|
*/
|
||||||
|
transformBackendResponse: (response: AG['Response']) => any;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "@sa/axios",
|
||||||
|
"version": "2.0.2",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sa/utils": "workspace:*",
|
||||||
|
"axios": "1.13.2",
|
||||||
|
"axios-retry": "4.5.0",
|
||||||
|
"qs": "6.14.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/qs": "6.14.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
/** request id key */
|
||||||
|
export const REQUEST_ID_KEY = 'X-Request-Id';
|
||||||
|
|
||||||
|
/** the backend error code key */
|
||||||
|
export const BACKEND_ERROR_CODE = 'BACKEND_ERROR';
|
||||||
|
|
||||||
|
/** the request canceled code */
|
||||||
|
export const REQUEST_CANCELED_CODE = 'ERR_CANCELED';
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
import axios, { AxiosError } from 'axios';
|
||||||
|
import type { AxiosResponse, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
|
||||||
|
import axiosRetry from 'axios-retry';
|
||||||
|
import { nanoid } from '@sa/utils';
|
||||||
|
import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options';
|
||||||
|
import { transformResponse } from './shared';
|
||||||
|
import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant';
|
||||||
|
import type {
|
||||||
|
CustomAxiosRequestConfig,
|
||||||
|
FlatRequestInstance,
|
||||||
|
MappedType,
|
||||||
|
RequestInstance,
|
||||||
|
RequestOption,
|
||||||
|
ResponseType
|
||||||
|
} from './type';
|
||||||
|
|
||||||
|
function createCommonRequest<
|
||||||
|
ResponseData,
|
||||||
|
ApiData = ResponseData,
|
||||||
|
State extends Record<string, unknown> = Record<string, unknown>
|
||||||
|
>(axiosConfig?: CreateAxiosDefaults, options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
|
||||||
|
const opts = createDefaultOptions<ResponseData, ApiData, State>(options);
|
||||||
|
|
||||||
|
const axiosConf = createAxiosConfig(axiosConfig);
|
||||||
|
const instance = axios.create(axiosConf);
|
||||||
|
|
||||||
|
const abortControllerMap = new Map<string, AbortController>();
|
||||||
|
|
||||||
|
// config axios retry
|
||||||
|
const retryOptions = createRetryOptions(axiosConf);
|
||||||
|
axiosRetry(instance, retryOptions);
|
||||||
|
|
||||||
|
instance.interceptors.request.use(conf => {
|
||||||
|
const config: InternalAxiosRequestConfig = { ...conf };
|
||||||
|
|
||||||
|
// set request id
|
||||||
|
const requestId = nanoid();
|
||||||
|
config.headers.set(REQUEST_ID_KEY, requestId);
|
||||||
|
|
||||||
|
// config abort controller
|
||||||
|
if (!config.signal) {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
config.signal = abortController.signal;
|
||||||
|
abortControllerMap.set(requestId, abortController);
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle config by hook
|
||||||
|
const handledConfig = opts.onRequest?.(config) || config;
|
||||||
|
|
||||||
|
return handledConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.interceptors.response.use(
|
||||||
|
async response => {
|
||||||
|
const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json';
|
||||||
|
|
||||||
|
await transformResponse(response);
|
||||||
|
|
||||||
|
if (responseType !== 'json' || opts.isBackendSuccess(response)) {
|
||||||
|
return Promise.resolve(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fail = await opts.onBackendFail(response, instance);
|
||||||
|
if (fail) {
|
||||||
|
return fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backendError = new AxiosError<ResponseData>(
|
||||||
|
'the backend request error',
|
||||||
|
BACKEND_ERROR_CODE,
|
||||||
|
response.config,
|
||||||
|
response.request,
|
||||||
|
response
|
||||||
|
);
|
||||||
|
|
||||||
|
await opts.onError(backendError);
|
||||||
|
|
||||||
|
return Promise.reject(backendError);
|
||||||
|
},
|
||||||
|
async (error: AxiosError<ResponseData>) => {
|
||||||
|
await opts.onError(error);
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function cancelAllRequest() {
|
||||||
|
abortControllerMap.forEach(abortController => {
|
||||||
|
abortController.abort();
|
||||||
|
});
|
||||||
|
abortControllerMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
instance,
|
||||||
|
opts,
|
||||||
|
cancelAllRequest
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a request instance
|
||||||
|
*
|
||||||
|
* @param axiosConfig axios config
|
||||||
|
* @param options request options
|
||||||
|
*/
|
||||||
|
export function createRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||||
|
axiosConfig?: CreateAxiosDefaults,
|
||||||
|
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||||
|
) {
|
||||||
|
const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
|
||||||
|
|
||||||
|
const request: RequestInstance<ApiData, State> = async function request<
|
||||||
|
T extends ApiData = ApiData,
|
||||||
|
R extends ResponseType = 'json'
|
||||||
|
>(config: CustomAxiosRequestConfig) {
|
||||||
|
const response: AxiosResponse<ResponseData> = await instance(config);
|
||||||
|
|
||||||
|
const responseType = response.config?.responseType || 'json';
|
||||||
|
|
||||||
|
if (responseType === 'json') {
|
||||||
|
return opts.transform(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as MappedType<R, T>;
|
||||||
|
} as RequestInstance<ApiData, State>;
|
||||||
|
|
||||||
|
request.cancelAllRequest = cancelAllRequest;
|
||||||
|
request.state = {} as State;
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a flat request instance
|
||||||
|
*
|
||||||
|
* The response data is a flat object: { data: any, error: AxiosError }
|
||||||
|
*
|
||||||
|
* @param axiosConfig axios config
|
||||||
|
* @param options request options
|
||||||
|
*/
|
||||||
|
export function createFlatRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||||
|
axiosConfig?: CreateAxiosDefaults,
|
||||||
|
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||||
|
) {
|
||||||
|
const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
|
||||||
|
|
||||||
|
const flatRequest: FlatRequestInstance<ResponseData, ApiData, State> = async function flatRequest<
|
||||||
|
T extends ApiData = ApiData,
|
||||||
|
R extends ResponseType = 'json'
|
||||||
|
>(config: CustomAxiosRequestConfig) {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<ResponseData> = await instance(config);
|
||||||
|
|
||||||
|
const responseType = response.config?.responseType || 'json';
|
||||||
|
|
||||||
|
if (responseType === 'json') {
|
||||||
|
const data = await opts.transform(response);
|
||||||
|
|
||||||
|
return { data, error: null, response };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: response.data as MappedType<R, T>, error: null, response };
|
||||||
|
} catch (error) {
|
||||||
|
return { data: null, error, response: (error as AxiosError<ResponseData>).response };
|
||||||
|
}
|
||||||
|
} as FlatRequestInstance<ResponseData, ApiData, State>;
|
||||||
|
|
||||||
|
flatRequest.cancelAllRequest = cancelAllRequest;
|
||||||
|
flatRequest.state = {
|
||||||
|
...opts.defaultState
|
||||||
|
} as State;
|
||||||
|
|
||||||
|
return flatRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { BACKEND_ERROR_CODE, REQUEST_ID_KEY };
|
||||||
|
export type * from './type';
|
||||||
|
export type { CreateAxiosDefaults, AxiosError };
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import type { CreateAxiosDefaults } from 'axios';
|
||||||
|
import type { IAxiosRetryConfig } from 'axios-retry';
|
||||||
|
import { stringify } from 'qs';
|
||||||
|
import { isHttpSuccess } from './shared';
|
||||||
|
import type { RequestOption } from './type';
|
||||||
|
|
||||||
|
export function createDefaultOptions<
|
||||||
|
ResponseData,
|
||||||
|
ApiData = ResponseData,
|
||||||
|
State extends Record<string, unknown> = Record<string, unknown>
|
||||||
|
>(options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
|
||||||
|
const opts: RequestOption<ResponseData, ApiData, State> = {
|
||||||
|
defaultState: {} as State,
|
||||||
|
transform: async response => response.data as unknown as ApiData,
|
||||||
|
transformBackendResponse: async response => response.data as unknown as ApiData,
|
||||||
|
onRequest: async config => config,
|
||||||
|
isBackendSuccess: _response => true,
|
||||||
|
onBackendFail: async () => {},
|
||||||
|
onError: async () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options?.transform) {
|
||||||
|
opts.transform = options.transform;
|
||||||
|
} else {
|
||||||
|
opts.transform = options?.transformBackendResponse || opts.transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(opts, options);
|
||||||
|
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRetryOptions(config?: Partial<CreateAxiosDefaults>) {
|
||||||
|
const retryConfig: IAxiosRetryConfig = {
|
||||||
|
retries: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(retryConfig, config);
|
||||||
|
|
||||||
|
return retryConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAxiosConfig(config?: Partial<CreateAxiosDefaults>) {
|
||||||
|
const TEN_SECONDS = 10 * 1000;
|
||||||
|
|
||||||
|
const axiosConfig: CreateAxiosDefaults = {
|
||||||
|
timeout: TEN_SECONDS,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
validateStatus: isHttpSuccess,
|
||||||
|
paramsSerializer: params => {
|
||||||
|
return stringify(params);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(axiosConfig, config);
|
||||||
|
|
||||||
|
return axiosConfig;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import type { AxiosHeaderValue, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||||
|
import type { ResponseType } from './type';
|
||||||
|
|
||||||
|
export function getContentType(config: InternalAxiosRequestConfig) {
|
||||||
|
const contentType: AxiosHeaderValue = config.headers?.['Content-Type'] || 'application/json';
|
||||||
|
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if http status is success
|
||||||
|
*
|
||||||
|
* @param status
|
||||||
|
*/
|
||||||
|
export function isHttpSuccess(status: number) {
|
||||||
|
const isSuccessCode = status >= 200 && status < 300;
|
||||||
|
return isSuccessCode || status === 304;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* is response json
|
||||||
|
*
|
||||||
|
* @param response axios response
|
||||||
|
*/
|
||||||
|
export function isResponseJson(response: AxiosResponse) {
|
||||||
|
const { responseType } = response.config;
|
||||||
|
|
||||||
|
return responseType === 'json' || responseType === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function transformResponse(response: AxiosResponse) {
|
||||||
|
const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json';
|
||||||
|
if (responseType === 'json') return;
|
||||||
|
|
||||||
|
const isJson = response.headers['content-type']?.includes('application/json');
|
||||||
|
if (!isJson) return;
|
||||||
|
|
||||||
|
if (responseType === 'blob') {
|
||||||
|
await transformBlobToJson(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseType === 'arrayBuffer') {
|
||||||
|
await transformArrayBufferToJson(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function transformBlobToJson(response: AxiosResponse) {
|
||||||
|
try {
|
||||||
|
let data = response.data;
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.toString.call(data) === '[object Blob]') {
|
||||||
|
const json = await data.text();
|
||||||
|
data = JSON.parse(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.data = data;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function transformArrayBufferToJson(response: AxiosResponse) {
|
||||||
|
try {
|
||||||
|
let data = response.data;
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.toString.call(data) === '[object ArrayBuffer]') {
|
||||||
|
const json = new TextDecoder().decode(data);
|
||||||
|
data = JSON.parse(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.data = data;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||||
|
|
||||||
|
export type ContentType =
|
||||||
|
| 'text/html'
|
||||||
|
| 'text/plain'
|
||||||
|
| 'multipart/form-data'
|
||||||
|
| 'application/json'
|
||||||
|
| 'application/x-www-form-urlencoded'
|
||||||
|
| 'application/octet-stream';
|
||||||
|
|
||||||
|
export type ResponseTransform<Input = any, Output = any> = (input: Input) => Output | Promise<Output>;
|
||||||
|
|
||||||
|
export interface RequestOption<
|
||||||
|
ResponseData,
|
||||||
|
ApiData = ResponseData,
|
||||||
|
State extends Record<string, unknown> = Record<string, unknown>
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* The default state
|
||||||
|
*/
|
||||||
|
defaultState?: State;
|
||||||
|
/**
|
||||||
|
* transform the response data to the api data
|
||||||
|
*
|
||||||
|
* @param response Axios response
|
||||||
|
*/
|
||||||
|
transform: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
|
||||||
|
/**
|
||||||
|
* transform the response data to the api data
|
||||||
|
*
|
||||||
|
* @deprecated use `transform` instead, will be removed in the next major version v3
|
||||||
|
* @param response Axios response
|
||||||
|
*/
|
||||||
|
transformBackendResponse: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
|
||||||
|
/**
|
||||||
|
* The hook before request
|
||||||
|
*
|
||||||
|
* For example: You can add header token in this hook
|
||||||
|
*
|
||||||
|
* @param config Axios config
|
||||||
|
*/
|
||||||
|
onRequest: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
|
||||||
|
/**
|
||||||
|
* The hook to check backend response is success or not
|
||||||
|
*
|
||||||
|
* @param response Axios response
|
||||||
|
*/
|
||||||
|
isBackendSuccess: (response: AxiosResponse<ResponseData>) => boolean;
|
||||||
|
/**
|
||||||
|
* The hook after backend request fail
|
||||||
|
*
|
||||||
|
* For example: You can handle the expired token in this hook
|
||||||
|
*
|
||||||
|
* @param response Axios response
|
||||||
|
* @param instance Axios instance
|
||||||
|
*/
|
||||||
|
onBackendFail: (
|
||||||
|
response: AxiosResponse<ResponseData>,
|
||||||
|
instance: AxiosInstance
|
||||||
|
) => Promise<AxiosResponse | null> | Promise<void>;
|
||||||
|
/**
|
||||||
|
* The hook to handle error
|
||||||
|
*
|
||||||
|
* For example: You can show error message in this hook
|
||||||
|
*
|
||||||
|
* @param error
|
||||||
|
*/
|
||||||
|
onError: (error: AxiosError<ResponseData>) => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResponseMap {
|
||||||
|
blob: Blob;
|
||||||
|
text: string;
|
||||||
|
arrayBuffer: ArrayBuffer;
|
||||||
|
stream: ReadableStream<Uint8Array>;
|
||||||
|
document: Document;
|
||||||
|
}
|
||||||
|
export type ResponseType = keyof ResponseMap | 'json';
|
||||||
|
|
||||||
|
export type MappedType<R extends ResponseType, JsonType = any> = R extends keyof ResponseMap
|
||||||
|
? ResponseMap[R]
|
||||||
|
: JsonType;
|
||||||
|
|
||||||
|
export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<AxiosRequestConfig, 'responseType'> & {
|
||||||
|
responseType?: R;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface RequestInstanceCommon<State extends Record<string, unknown>> {
|
||||||
|
/**
|
||||||
|
* cancel all request
|
||||||
|
*
|
||||||
|
* if the request provide abort controller sign from config, it will not collect in the abort controller map
|
||||||
|
*/
|
||||||
|
cancelAllRequest: () => void;
|
||||||
|
/** you can set custom state in the request instance */
|
||||||
|
state: State;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The request instance */
|
||||||
|
export interface RequestInstance<ApiData, State extends Record<string, unknown>> extends RequestInstanceCommon<State> {
|
||||||
|
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||||
|
config: CustomAxiosRequestConfig<R>
|
||||||
|
): Promise<MappedType<R, T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FlatResponseSuccessData<ResponseData, ApiData> = {
|
||||||
|
data: ApiData;
|
||||||
|
error: null;
|
||||||
|
response: AxiosResponse<ResponseData>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FlatResponseFailData<ResponseData> = {
|
||||||
|
data: null;
|
||||||
|
error: AxiosError<ResponseData>;
|
||||||
|
response: AxiosResponse<ResponseData>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FlatResponseData<ResponseData, ApiData> =
|
||||||
|
| FlatResponseSuccessData<ResponseData, ApiData>
|
||||||
|
| FlatResponseFailData<ResponseData>;
|
||||||
|
|
||||||
|
export interface FlatRequestInstance<
|
||||||
|
ResponseData,
|
||||||
|
ApiData,
|
||||||
|
State extends Record<string, unknown>
|
||||||
|
> extends RequestInstanceCommon<State> {
|
||||||
|
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||||
|
config: CustomAxiosRequestConfig<R>
|
||||||
|
): Promise<FlatResponseData<ResponseData, MappedType<R, T>>>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "@sa/color",
|
||||||
|
"version": "2.0.2",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sa/utils": "workspace:*",
|
||||||
|
"colord": "2.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './name';
|
||||||
|
export * from './palette';
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,356 @@
|
||||||
|
import type { ColorPaletteFamily } from '../types';
|
||||||
|
|
||||||
|
export const colorPalettes: ColorPaletteFamily[] = [
|
||||||
|
{
|
||||||
|
name: 'Slate',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#f8fafc', number: 50 },
|
||||||
|
{ hex: '#f1f5f9', number: 100 },
|
||||||
|
{ hex: '#e2e8f0', number: 200 },
|
||||||
|
{ hex: '#cbd5e1', number: 300 },
|
||||||
|
{ hex: '#94a3b8', number: 400 },
|
||||||
|
{ hex: '#64748b', number: 500 },
|
||||||
|
{ hex: '#475569', number: 600 },
|
||||||
|
{ hex: '#334155', number: 700 },
|
||||||
|
{ hex: '#1e293b', number: 800 },
|
||||||
|
{ hex: '#0f172a', number: 900 },
|
||||||
|
{ hex: '#020617', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Gray',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#f9fafb', number: 50 },
|
||||||
|
{ hex: '#f3f4f6', number: 100 },
|
||||||
|
{ hex: '#e5e7eb', number: 200 },
|
||||||
|
{ hex: '#d1d5db', number: 300 },
|
||||||
|
{ hex: '#9ca3af', number: 400 },
|
||||||
|
{ hex: '#6b7280', number: 500 },
|
||||||
|
{ hex: '#4b5563', number: 600 },
|
||||||
|
{ hex: '#374151', number: 700 },
|
||||||
|
{ hex: '#1f2937', number: 800 },
|
||||||
|
{ hex: '#111827', number: 900 },
|
||||||
|
{ hex: '#030712', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Zinc',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fafafa', number: 50 },
|
||||||
|
{ hex: '#f4f4f5', number: 100 },
|
||||||
|
{ hex: '#e4e4e7', number: 200 },
|
||||||
|
{ hex: '#d4d4d8', number: 300 },
|
||||||
|
{ hex: '#a1a1aa', number: 400 },
|
||||||
|
{ hex: '#71717a', number: 500 },
|
||||||
|
{ hex: '#52525b', number: 600 },
|
||||||
|
{ hex: '#3f3f46', number: 700 },
|
||||||
|
{ hex: '#27272a', number: 800 },
|
||||||
|
{ hex: '#18181b', number: 900 },
|
||||||
|
{ hex: '#09090b', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Neutral',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fafafa', number: 50 },
|
||||||
|
{ hex: '#f5f5f5', number: 100 },
|
||||||
|
{ hex: '#e5e5e5', number: 200 },
|
||||||
|
{ hex: '#d4d4d4', number: 300 },
|
||||||
|
{ hex: '#a3a3a3', number: 400 },
|
||||||
|
{ hex: '#737373', number: 500 },
|
||||||
|
{ hex: '#525252', number: 600 },
|
||||||
|
{ hex: '#404040', number: 700 },
|
||||||
|
{ hex: '#262626', number: 800 },
|
||||||
|
{ hex: '#171717', number: 900 },
|
||||||
|
{ hex: '#0a0a0a', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Stone',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fafaf9', number: 50 },
|
||||||
|
{ hex: '#f5f5f4', number: 100 },
|
||||||
|
{ hex: '#e7e5e4', number: 200 },
|
||||||
|
{ hex: '#d6d3d1', number: 300 },
|
||||||
|
{ hex: '#a8a29e', number: 400 },
|
||||||
|
{ hex: '#78716c', number: 500 },
|
||||||
|
{ hex: '#57534e', number: 600 },
|
||||||
|
{ hex: '#44403c', number: 700 },
|
||||||
|
{ hex: '#292524', number: 800 },
|
||||||
|
{ hex: '#1c1917', number: 900 },
|
||||||
|
{ hex: '#0c0a09', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Red',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fef2f2', number: 50 },
|
||||||
|
{ hex: '#fee2e2', number: 100 },
|
||||||
|
{ hex: '#fecaca', number: 200 },
|
||||||
|
{ hex: '#fca5a5', number: 300 },
|
||||||
|
{ hex: '#f87171', number: 400 },
|
||||||
|
{ hex: '#ef4444', number: 500 },
|
||||||
|
{ hex: '#dc2626', number: 600 },
|
||||||
|
{ hex: '#b91c1c', number: 700 },
|
||||||
|
{ hex: '#991b1b', number: 800 },
|
||||||
|
{ hex: '#7f1d1d', number: 900 },
|
||||||
|
{ hex: '#450a0a', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Orange',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fff7ed', number: 50 },
|
||||||
|
{ hex: '#ffedd5', number: 100 },
|
||||||
|
{ hex: '#fed7aa', number: 200 },
|
||||||
|
{ hex: '#fdba74', number: 300 },
|
||||||
|
{ hex: '#fb923c', number: 400 },
|
||||||
|
{ hex: '#f97316', number: 500 },
|
||||||
|
{ hex: '#ea580c', number: 600 },
|
||||||
|
{ hex: '#c2410c', number: 700 },
|
||||||
|
{ hex: '#9a3412', number: 800 },
|
||||||
|
{ hex: '#7c2d12', number: 900 },
|
||||||
|
{ hex: '#431407', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Amber',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fffbeb', number: 50 },
|
||||||
|
{ hex: '#fef3c7', number: 100 },
|
||||||
|
{ hex: '#fde68a', number: 200 },
|
||||||
|
{ hex: '#fcd34d', number: 300 },
|
||||||
|
{ hex: '#fbbf24', number: 400 },
|
||||||
|
{ hex: '#f59e0b', number: 500 },
|
||||||
|
{ hex: '#d97706', number: 600 },
|
||||||
|
{ hex: '#b45309', number: 700 },
|
||||||
|
{ hex: '#92400e', number: 800 },
|
||||||
|
{ hex: '#78350f', number: 900 },
|
||||||
|
{ hex: '#451a03', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Yellow',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fefce8', number: 50 },
|
||||||
|
{ hex: '#fef9c3', number: 100 },
|
||||||
|
{ hex: '#fef08a', number: 200 },
|
||||||
|
{ hex: '#fde047', number: 300 },
|
||||||
|
{ hex: '#facc15', number: 400 },
|
||||||
|
{ hex: '#eab308', number: 500 },
|
||||||
|
{ hex: '#ca8a04', number: 600 },
|
||||||
|
{ hex: '#a16207', number: 700 },
|
||||||
|
{ hex: '#854d0e', number: 800 },
|
||||||
|
{ hex: '#713f12', number: 900 },
|
||||||
|
{ hex: '#422006', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Lime',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#f7fee7', number: 50 },
|
||||||
|
{ hex: '#ecfccb', number: 100 },
|
||||||
|
{ hex: '#d9f99d', number: 200 },
|
||||||
|
{ hex: '#bef264', number: 300 },
|
||||||
|
{ hex: '#a3e635', number: 400 },
|
||||||
|
{ hex: '#84cc16', number: 500 },
|
||||||
|
{ hex: '#65a30d', number: 600 },
|
||||||
|
{ hex: '#4d7c0f', number: 700 },
|
||||||
|
{ hex: '#3f6212', number: 800 },
|
||||||
|
{ hex: '#365314', number: 900 },
|
||||||
|
{ hex: '#1a2e05', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Green',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#f0fdf4', number: 50 },
|
||||||
|
{ hex: '#dcfce7', number: 100 },
|
||||||
|
{ hex: '#bbf7d0', number: 200 },
|
||||||
|
{ hex: '#86efac', number: 300 },
|
||||||
|
{ hex: '#4ade80', number: 400 },
|
||||||
|
{ hex: '#22c55e', number: 500 },
|
||||||
|
{ hex: '#16a34a', number: 600 },
|
||||||
|
{ hex: '#15803d', number: 700 },
|
||||||
|
{ hex: '#166534', number: 800 },
|
||||||
|
{ hex: '#14532d', number: 900 },
|
||||||
|
{ hex: '#052e16', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Emerald',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#ecfdf5', number: 50 },
|
||||||
|
{ hex: '#d1fae5', number: 100 },
|
||||||
|
{ hex: '#a7f3d0', number: 200 },
|
||||||
|
{ hex: '#6ee7b7', number: 300 },
|
||||||
|
{ hex: '#34d399', number: 400 },
|
||||||
|
{ hex: '#10b981', number: 500 },
|
||||||
|
{ hex: '#059669', number: 600 },
|
||||||
|
{ hex: '#047857', number: 700 },
|
||||||
|
{ hex: '#065f46', number: 800 },
|
||||||
|
{ hex: '#064e3b', number: 900 },
|
||||||
|
{ hex: '#022c22', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Teal',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#f0fdfa', number: 50 },
|
||||||
|
{ hex: '#ccfbf1', number: 100 },
|
||||||
|
{ hex: '#99f6e4', number: 200 },
|
||||||
|
{ hex: '#5eead4', number: 300 },
|
||||||
|
{ hex: '#2dd4bf', number: 400 },
|
||||||
|
{ hex: '#14b8a6', number: 500 },
|
||||||
|
{ hex: '#0d9488', number: 600 },
|
||||||
|
{ hex: '#0f766e', number: 700 },
|
||||||
|
{ hex: '#115e59', number: 800 },
|
||||||
|
{ hex: '#134e4a', number: 900 },
|
||||||
|
{ hex: '#042f2e', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cyan',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#ecfeff', number: 50 },
|
||||||
|
{ hex: '#cffafe', number: 100 },
|
||||||
|
{ hex: '#a5f3fc', number: 200 },
|
||||||
|
{ hex: '#67e8f9', number: 300 },
|
||||||
|
{ hex: '#22d3ee', number: 400 },
|
||||||
|
{ hex: '#06b6d4', number: 500 },
|
||||||
|
{ hex: '#0891b2', number: 600 },
|
||||||
|
{ hex: '#0e7490', number: 700 },
|
||||||
|
{ hex: '#155e75', number: 800 },
|
||||||
|
{ hex: '#164e63', number: 900 },
|
||||||
|
{ hex: '#083344', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sky',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#f0f9ff', number: 50 },
|
||||||
|
{ hex: '#e0f2fe', number: 100 },
|
||||||
|
{ hex: '#bae6fd', number: 200 },
|
||||||
|
{ hex: '#7dd3fc', number: 300 },
|
||||||
|
{ hex: '#38bdf8', number: 400 },
|
||||||
|
{ hex: '#0ea5e9', number: 500 },
|
||||||
|
{ hex: '#0284c7', number: 600 },
|
||||||
|
{ hex: '#0369a1', number: 700 },
|
||||||
|
{ hex: '#075985', number: 800 },
|
||||||
|
{ hex: '#0c4a6e', number: 900 },
|
||||||
|
{ hex: '#082f49', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Blue',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#eff6ff', number: 50 },
|
||||||
|
{ hex: '#dbeafe', number: 100 },
|
||||||
|
{ hex: '#bfdbfe', number: 200 },
|
||||||
|
{ hex: '#93c5fd', number: 300 },
|
||||||
|
{ hex: '#60a5fa', number: 400 },
|
||||||
|
{ hex: '#3b82f6', number: 500 },
|
||||||
|
{ hex: '#2563eb', number: 600 },
|
||||||
|
{ hex: '#1d4ed8', number: 700 },
|
||||||
|
{ hex: '#1e40af', number: 800 },
|
||||||
|
{ hex: '#1e3a8a', number: 900 },
|
||||||
|
{ hex: '#172554', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Indigo',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#eef2ff', number: 50 },
|
||||||
|
{ hex: '#e0e7ff', number: 100 },
|
||||||
|
{ hex: '#c7d2fe', number: 200 },
|
||||||
|
{ hex: '#a5b4fc', number: 300 },
|
||||||
|
{ hex: '#818cf8', number: 400 },
|
||||||
|
{ hex: '#6366f1', number: 500 },
|
||||||
|
{ hex: '#4f46e5', number: 600 },
|
||||||
|
{ hex: '#4338ca', number: 700 },
|
||||||
|
{ hex: '#3730a3', number: 800 },
|
||||||
|
{ hex: '#312e81', number: 900 },
|
||||||
|
{ hex: '#1e1b4b', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Violet',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#f5f3ff', number: 50 },
|
||||||
|
{ hex: '#ede9fe', number: 100 },
|
||||||
|
{ hex: '#ddd6fe', number: 200 },
|
||||||
|
{ hex: '#c4b5fd', number: 300 },
|
||||||
|
{ hex: '#a78bfa', number: 400 },
|
||||||
|
{ hex: '#8b5cf6', number: 500 },
|
||||||
|
{ hex: '#7c3aed', number: 600 },
|
||||||
|
{ hex: '#6d28d9', number: 700 },
|
||||||
|
{ hex: '#5b21b6', number: 800 },
|
||||||
|
{ hex: '#4c1d95', number: 900 },
|
||||||
|
{ hex: '#2e1065', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Purple',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#faf5ff', number: 50 },
|
||||||
|
{ hex: '#f3e8ff', number: 100 },
|
||||||
|
{ hex: '#e9d5ff', number: 200 },
|
||||||
|
{ hex: '#d8b4fe', number: 300 },
|
||||||
|
{ hex: '#c084fc', number: 400 },
|
||||||
|
{ hex: '#a855f7', number: 500 },
|
||||||
|
{ hex: '#9333ea', number: 600 },
|
||||||
|
{ hex: '#7e22ce', number: 700 },
|
||||||
|
{ hex: '#6b21a8', number: 800 },
|
||||||
|
{ hex: '#581c87', number: 900 },
|
||||||
|
{ hex: '#3b0764', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Fuchsia',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fdf4ff', number: 50 },
|
||||||
|
{ hex: '#fae8ff', number: 100 },
|
||||||
|
{ hex: '#f5d0fe', number: 200 },
|
||||||
|
{ hex: '#f0abfc', number: 300 },
|
||||||
|
{ hex: '#e879f9', number: 400 },
|
||||||
|
{ hex: '#d946ef', number: 500 },
|
||||||
|
{ hex: '#c026d3', number: 600 },
|
||||||
|
{ hex: '#a21caf', number: 700 },
|
||||||
|
{ hex: '#86198f', number: 800 },
|
||||||
|
{ hex: '#701a75', number: 900 },
|
||||||
|
{ hex: '#4a044e', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pink',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fdf2f8', number: 50 },
|
||||||
|
{ hex: '#fce7f3', number: 100 },
|
||||||
|
{ hex: '#fbcfe8', number: 200 },
|
||||||
|
{ hex: '#f9a8d4', number: 300 },
|
||||||
|
{ hex: '#f472b6', number: 400 },
|
||||||
|
{ hex: '#ec4899', number: 500 },
|
||||||
|
{ hex: '#db2777', number: 600 },
|
||||||
|
{ hex: '#be185d', number: 700 },
|
||||||
|
{ hex: '#9d174d', number: 800 },
|
||||||
|
{ hex: '#831843', number: 900 },
|
||||||
|
{ hex: '#500724', number: 950 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rose',
|
||||||
|
palettes: [
|
||||||
|
{ hex: '#fff1f2', number: 50 },
|
||||||
|
{ hex: '#ffe4e6', number: 100 },
|
||||||
|
{ hex: '#fecdd3', number: 200 },
|
||||||
|
{ hex: '#fda4af', number: 300 },
|
||||||
|
{ hex: '#fb7185', number: 400 },
|
||||||
|
{ hex: '#f43f5e', number: 500 },
|
||||||
|
{ hex: '#e11d48', number: 600 },
|
||||||
|
{ hex: '#be123c', number: 700 },
|
||||||
|
{ hex: '#9f1239', number: 800 },
|
||||||
|
{ hex: '#881337', number: 900 },
|
||||||
|
{ hex: '#4c0519', number: 950 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { colorPalettes } from './constant';
|
||||||
|
|
||||||
|
export * from './palette';
|
||||||
|
export * from './shared';
|
||||||
|
export { colorPalettes };
|
||||||
|
|
||||||
|
export * from './types';
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
import type { AnyColor, HsvColor } from 'colord';
|
||||||
|
import { getHex, getHsv, isValidColor, mixColor } from '../shared';
|
||||||
|
import type { ColorIndex } from '../types';
|
||||||
|
|
||||||
|
/** Hue step */
|
||||||
|
const hueStep = 2;
|
||||||
|
/** Saturation step, light color part */
|
||||||
|
const saturationStep = 16;
|
||||||
|
/** Saturation step, dark color part */
|
||||||
|
const saturationStep2 = 5;
|
||||||
|
/** Brightness step, light color part */
|
||||||
|
const brightnessStep1 = 5;
|
||||||
|
/** Brightness step, dark color part */
|
||||||
|
const brightnessStep2 = 15;
|
||||||
|
/** Light color count, main color up */
|
||||||
|
const lightColorCount = 5;
|
||||||
|
/** Dark color count, main color down */
|
||||||
|
const darkColorCount = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get AntD palette color by index
|
||||||
|
*
|
||||||
|
* @param color - Color
|
||||||
|
* @param index - The color index of color palette (the main color index is 6)
|
||||||
|
* @returns Hex color
|
||||||
|
*/
|
||||||
|
export function getAntDPaletteColorByIndex(color: AnyColor, index: ColorIndex): string {
|
||||||
|
if (!isValidColor(color)) {
|
||||||
|
throw new Error('invalid input color value');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === 6) {
|
||||||
|
return getHex(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLight = index < 6;
|
||||||
|
const hsv = getHsv(color);
|
||||||
|
const i = isLight ? lightColorCount + 1 - index : index - lightColorCount - 1;
|
||||||
|
|
||||||
|
const newHsv: HsvColor = {
|
||||||
|
h: getHue(hsv, i, isLight),
|
||||||
|
s: getSaturation(hsv, i, isLight),
|
||||||
|
v: getValue(hsv, i, isLight)
|
||||||
|
};
|
||||||
|
|
||||||
|
return getHex(newHsv);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map of dark color index and opacity */
|
||||||
|
const darkColorMap = [
|
||||||
|
{ index: 7, opacity: 0.15 },
|
||||||
|
{ index: 6, opacity: 0.25 },
|
||||||
|
{ index: 5, opacity: 0.3 },
|
||||||
|
{ index: 5, opacity: 0.45 },
|
||||||
|
{ index: 5, opacity: 0.65 },
|
||||||
|
{ index: 5, opacity: 0.85 },
|
||||||
|
{ index: 5, opacity: 0.9 },
|
||||||
|
{ index: 4, opacity: 0.93 },
|
||||||
|
{ index: 3, opacity: 0.95 },
|
||||||
|
{ index: 2, opacity: 0.97 },
|
||||||
|
{ index: 1, opacity: 0.98 }
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get AntD color palette
|
||||||
|
*
|
||||||
|
* @param color - Color
|
||||||
|
* @param darkTheme - Dark theme
|
||||||
|
* @param darkThemeMixColor - Dark theme mix color (default: #141414)
|
||||||
|
*/
|
||||||
|
export function getAntDColorPalette(color: AnyColor, darkTheme = false, darkThemeMixColor = '#141414'): string[] {
|
||||||
|
const indexes: ColorIndex[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||||
|
|
||||||
|
const patterns = indexes.map(index => getAntDPaletteColorByIndex(color, index));
|
||||||
|
|
||||||
|
if (darkTheme) {
|
||||||
|
const darkPatterns = darkColorMap.map(({ index, opacity }) => {
|
||||||
|
const darkColor = mixColor(darkThemeMixColor, patterns[index], opacity);
|
||||||
|
|
||||||
|
return darkColor;
|
||||||
|
});
|
||||||
|
|
||||||
|
return darkPatterns.map(item => getHex(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
return patterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get hue
|
||||||
|
*
|
||||||
|
* @param hsv - Hsv format color
|
||||||
|
* @param i - The relative distance from 6
|
||||||
|
* @param isLight - Is light color
|
||||||
|
*/
|
||||||
|
function getHue(hsv: HsvColor, i: number, isLight: boolean) {
|
||||||
|
let hue: number;
|
||||||
|
|
||||||
|
const hsvH = Math.round(hsv.h);
|
||||||
|
|
||||||
|
if (hsvH >= 60 && hsvH <= 240) {
|
||||||
|
hue = isLight ? hsvH - hueStep * i : hsvH + hueStep * i;
|
||||||
|
} else {
|
||||||
|
hue = isLight ? hsvH + hueStep * i : hsvH - hueStep * i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hue < 0) {
|
||||||
|
hue += 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hue >= 360) {
|
||||||
|
hue -= 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get saturation
|
||||||
|
*
|
||||||
|
* @param hsv - Hsv format color
|
||||||
|
* @param i - The relative distance from 6
|
||||||
|
* @param isLight - Is light color
|
||||||
|
*/
|
||||||
|
function getSaturation(hsv: HsvColor, i: number, isLight: boolean) {
|
||||||
|
if (hsv.h === 0 && hsv.s === 0) {
|
||||||
|
return hsv.s;
|
||||||
|
}
|
||||||
|
|
||||||
|
let saturation: number;
|
||||||
|
|
||||||
|
if (isLight) {
|
||||||
|
saturation = hsv.s - saturationStep * i;
|
||||||
|
} else if (i === darkColorCount) {
|
||||||
|
saturation = hsv.s + saturationStep;
|
||||||
|
} else {
|
||||||
|
saturation = hsv.s + saturationStep2 * i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saturation > 100) {
|
||||||
|
saturation = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLight && i === lightColorCount && saturation > 10) {
|
||||||
|
saturation = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saturation < 6) {
|
||||||
|
saturation = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
return saturation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value of hsv
|
||||||
|
*
|
||||||
|
* @param hsv - Hsv format color
|
||||||
|
* @param i - The relative distance from 6
|
||||||
|
* @param isLight - Is light color
|
||||||
|
*/
|
||||||
|
function getValue(hsv: HsvColor, i: number, isLight: boolean) {
|
||||||
|
let value: number;
|
||||||
|
|
||||||
|
if (isLight) {
|
||||||
|
value = hsv.v + brightnessStep1 * i;
|
||||||
|
} else {
|
||||||
|
value = hsv.v - brightnessStep2 * i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value > 100) {
|
||||||
|
value = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import type { AnyColor } from 'colord';
|
||||||
|
import { getHex } from '../shared';
|
||||||
|
import type { ColorPaletteNumber } from '../types';
|
||||||
|
import { getRecommendedColorPalette } from './recommend';
|
||||||
|
import { getAntDColorPalette } from './antd';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get color palette by provided color
|
||||||
|
*
|
||||||
|
* @param color
|
||||||
|
* @param recommended whether to get recommended color palette (the provided color may not be the main color)
|
||||||
|
*/
|
||||||
|
export function getColorPalette(color: AnyColor, recommended = false) {
|
||||||
|
const colorMap = new Map<ColorPaletteNumber, string>();
|
||||||
|
|
||||||
|
if (recommended) {
|
||||||
|
const colorPalette = getRecommendedColorPalette(getHex(color));
|
||||||
|
colorPalette.palettes.forEach(palette => {
|
||||||
|
colorMap.set(palette.number, palette.hex);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const colors = getAntDColorPalette(color);
|
||||||
|
|
||||||
|
const colorNumbers: ColorPaletteNumber[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
|
||||||
|
|
||||||
|
colorNumbers.forEach((number, index) => {
|
||||||
|
colorMap.set(number, colors[index]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return colorMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get color palette color by number
|
||||||
|
*
|
||||||
|
* @param color the provided color
|
||||||
|
* @param number the color palette number
|
||||||
|
* @param recommended whether to get recommended color palette (the provided color may not be the main color)
|
||||||
|
*/
|
||||||
|
export function getPaletteColorByNumber(color: AnyColor, number: ColorPaletteNumber, recommended = false) {
|
||||||
|
const colorMap = getColorPalette(color, recommended);
|
||||||
|
|
||||||
|
return colorMap.get(number as ColorPaletteNumber)!;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
import { getColorName, getDeltaE, getHsl, isValidColor, transformHslToHex } from '../shared';
|
||||||
|
import { colorPalettes } from '../constant';
|
||||||
|
import type {
|
||||||
|
ColorPalette,
|
||||||
|
ColorPaletteFamily,
|
||||||
|
ColorPaletteFamilyWithNearestPalette,
|
||||||
|
ColorPaletteMatch,
|
||||||
|
ColorPaletteNumber
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get recommended color palette by provided color
|
||||||
|
*
|
||||||
|
* @param color the provided color
|
||||||
|
*/
|
||||||
|
export function getRecommendedColorPalette(color: string) {
|
||||||
|
const colorPaletteFamily = getRecommendedColorPaletteFamily(color);
|
||||||
|
|
||||||
|
const colorMap = new Map<ColorPaletteNumber, ColorPalette>();
|
||||||
|
|
||||||
|
colorPaletteFamily.palettes.forEach(palette => {
|
||||||
|
colorMap.set(palette.number, palette);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mainColor = colorMap.get(500)!;
|
||||||
|
const matchColor = colorPaletteFamily.palettes.find(palette => palette.hex === color)!;
|
||||||
|
|
||||||
|
const colorPalette: ColorPaletteMatch = {
|
||||||
|
...colorPaletteFamily,
|
||||||
|
colorMap,
|
||||||
|
main: mainColor,
|
||||||
|
match: matchColor
|
||||||
|
};
|
||||||
|
|
||||||
|
return colorPalette;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get recommended palette color by provided color
|
||||||
|
*
|
||||||
|
* @param color the provided color
|
||||||
|
* @param number the color palette number
|
||||||
|
*/
|
||||||
|
export function getRecommendedPaletteColorByNumber(color: string, number: ColorPaletteNumber) {
|
||||||
|
const colorPalette = getRecommendedColorPalette(color);
|
||||||
|
|
||||||
|
const { hex } = colorPalette.colorMap.get(number)!;
|
||||||
|
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get color palette family by provided color and color name
|
||||||
|
*
|
||||||
|
* @param color the provided color
|
||||||
|
*/
|
||||||
|
export function getRecommendedColorPaletteFamily(color: string) {
|
||||||
|
if (!isValidColor(color)) {
|
||||||
|
throw new Error('Invalid color, please check color value!');
|
||||||
|
}
|
||||||
|
|
||||||
|
let colorName = getColorName(color);
|
||||||
|
|
||||||
|
colorName = colorName.toLowerCase().replace(/\s/g, '-');
|
||||||
|
|
||||||
|
const { h: h1, s: s1 } = getHsl(color);
|
||||||
|
|
||||||
|
const { nearestLightnessPalette, palettes } = getNearestColorPaletteFamily(color, colorPalettes);
|
||||||
|
|
||||||
|
const { number, hex } = nearestLightnessPalette;
|
||||||
|
|
||||||
|
const { h: h2, s: s2 } = getHsl(hex);
|
||||||
|
|
||||||
|
const deltaH = h1 - h2;
|
||||||
|
|
||||||
|
const sRatio = s1 / s2;
|
||||||
|
|
||||||
|
const colorPaletteFamily: ColorPaletteFamily = {
|
||||||
|
name: colorName,
|
||||||
|
palettes: palettes.map(palette => {
|
||||||
|
let hexValue = color;
|
||||||
|
|
||||||
|
const isSame = number === palette.number;
|
||||||
|
|
||||||
|
if (!isSame) {
|
||||||
|
const { h: h3, s: s3, l } = getHsl(palette.hex);
|
||||||
|
|
||||||
|
const newH = deltaH < 0 ? h3 + deltaH : h3 - deltaH;
|
||||||
|
const newS = s3 * sRatio;
|
||||||
|
|
||||||
|
hexValue = transformHslToHex({
|
||||||
|
h: newH,
|
||||||
|
s: newS,
|
||||||
|
l
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hex: hexValue,
|
||||||
|
number: palette.number
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return colorPaletteFamily;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get nearest color palette family
|
||||||
|
*
|
||||||
|
* @param color color
|
||||||
|
* @param families color palette families
|
||||||
|
*/
|
||||||
|
function getNearestColorPaletteFamily(color: string, families: ColorPaletteFamily[]) {
|
||||||
|
const familyWithConfig = families.map(family => {
|
||||||
|
const palettes = family.palettes.map(palette => {
|
||||||
|
return {
|
||||||
|
...palette,
|
||||||
|
delta: getDeltaE(color, palette.hex)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const nearestPalette = palettes.reduce((prev, curr) => (prev.delta < curr.delta ? prev : curr));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...family,
|
||||||
|
palettes,
|
||||||
|
nearestPalette
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const nearestPaletteFamily = familyWithConfig.reduce((prev, curr) =>
|
||||||
|
prev.nearestPalette.delta < curr.nearestPalette.delta ? prev : curr
|
||||||
|
);
|
||||||
|
|
||||||
|
const { l } = getHsl(color);
|
||||||
|
|
||||||
|
const paletteFamily: ColorPaletteFamilyWithNearestPalette = {
|
||||||
|
...nearestPaletteFamily,
|
||||||
|
nearestLightnessPalette: nearestPaletteFamily.palettes.reduce((prev, curr) => {
|
||||||
|
const { l: prevLightness } = getHsl(prev.hex);
|
||||||
|
const { l: currLightness } = getHsl(curr.hex);
|
||||||
|
|
||||||
|
const deltaPrev = Math.abs(prevLightness - l);
|
||||||
|
const deltaCurr = Math.abs(currLightness - l);
|
||||||
|
|
||||||
|
return deltaPrev < deltaCurr ? prev : curr;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return paletteFamily;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { colord, extend } from 'colord';
|
||||||
|
import namesPlugin from 'colord/plugins/names';
|
||||||
|
import mixPlugin from 'colord/plugins/mix';
|
||||||
|
import labPlugin from 'colord/plugins/lab';
|
||||||
|
import type { AnyColor, HslColor, RgbColor } from 'colord';
|
||||||
|
|
||||||
|
extend([namesPlugin, mixPlugin, labPlugin]);
|
||||||
|
|
||||||
|
export function isValidColor(color: AnyColor) {
|
||||||
|
return colord(color).isValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHex(color: AnyColor) {
|
||||||
|
return colord(color).toHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRgb(color: AnyColor) {
|
||||||
|
return colord(color).toRgb();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHsl(color: AnyColor) {
|
||||||
|
return colord(color).toHsl();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHsv(color: AnyColor) {
|
||||||
|
return colord(color).toHsv();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeltaE(color1: AnyColor, color2: AnyColor) {
|
||||||
|
return colord(color1).delta(color2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformHslToHex(color: HslColor) {
|
||||||
|
return colord(color).toHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add color alpha
|
||||||
|
*
|
||||||
|
* @param color - Color
|
||||||
|
* @param alpha - Alpha (0 - 1)
|
||||||
|
*/
|
||||||
|
export function addColorAlpha(color: AnyColor, alpha: number) {
|
||||||
|
return colord(color).alpha(alpha).toHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mix color
|
||||||
|
*
|
||||||
|
* @param firstColor - First color
|
||||||
|
* @param secondColor - Second color
|
||||||
|
* @param ratio - The ratio of the second color (0 - 1)
|
||||||
|
*/
|
||||||
|
export function mixColor(firstColor: AnyColor, secondColor: AnyColor, ratio: number) {
|
||||||
|
return colord(firstColor).mix(secondColor, ratio).toHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform color with opacity to similar color without opacity
|
||||||
|
*
|
||||||
|
* @param color - Color
|
||||||
|
* @param alpha - Alpha (0 - 1)
|
||||||
|
* @param bgColor Background color (usually white or black)
|
||||||
|
*/
|
||||||
|
export function transformColorWithOpacity(color: AnyColor, alpha: number, bgColor = '#ffffff') {
|
||||||
|
const originColor = addColorAlpha(color, alpha);
|
||||||
|
const { r: oR, g: oG, b: oB } = colord(originColor).toRgb();
|
||||||
|
|
||||||
|
const { r: bgR, g: bgG, b: bgB } = colord(bgColor).toRgb();
|
||||||
|
|
||||||
|
function calRgb(or: number, bg: number, al: number) {
|
||||||
|
return bg + (or - bg) * al;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultRgb: RgbColor = {
|
||||||
|
r: calRgb(oR, bgR, alpha),
|
||||||
|
g: calRgb(oG, bgG, alpha),
|
||||||
|
b: calRgb(oB, bgB, alpha)
|
||||||
|
};
|
||||||
|
|
||||||
|
return colord(resultRgb).toHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is white color
|
||||||
|
*
|
||||||
|
* @param color - Color
|
||||||
|
*/
|
||||||
|
export function isWhiteColor(color: AnyColor) {
|
||||||
|
return colord(color).isEqual('#ffffff');
|
||||||
|
}
|
||||||
|
|
||||||
|
export { colord };
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './colord';
|
||||||
|
export * from './name';
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { colorNames } from '../constant';
|
||||||
|
import { getHex, getHsl, getRgb } from './colord';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color name
|
||||||
|
*
|
||||||
|
* @param color
|
||||||
|
*/
|
||||||
|
export function getColorName(color: string) {
|
||||||
|
const hex = getHex(color);
|
||||||
|
const rgb = getRgb(color);
|
||||||
|
const hsl = getHsl(color);
|
||||||
|
|
||||||
|
let ndf = 0;
|
||||||
|
let ndf1 = 0;
|
||||||
|
let ndf2 = 0;
|
||||||
|
let cl = -1;
|
||||||
|
let df = -1;
|
||||||
|
|
||||||
|
let name = '';
|
||||||
|
|
||||||
|
colorNames.some((item, index) => {
|
||||||
|
const [hexValue, colorName] = item;
|
||||||
|
|
||||||
|
const match = hex === hexValue;
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
name = colorName;
|
||||||
|
} else {
|
||||||
|
const { r, g, b } = getRgb(hexValue);
|
||||||
|
const { h, s, l } = getHsl(hexValue);
|
||||||
|
|
||||||
|
ndf1 = (rgb.r - r) ** 2 + (rgb.g - g) ** 2 + (rgb.b - b) ** 2;
|
||||||
|
ndf2 = (hsl.h - h) ** 2 + (hsl.s - s) ** 2 + (hsl.l - l) ** 2;
|
||||||
|
|
||||||
|
ndf = ndf1 + ndf2 * 2;
|
||||||
|
if (df < 0 || df > ndf) {
|
||||||
|
df = ndf;
|
||||||
|
cl = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
|
||||||
|
name = colorNames[cl][1];
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
/**
|
||||||
|
* the color palette number
|
||||||
|
*
|
||||||
|
* the main color number is 500
|
||||||
|
*/
|
||||||
|
export type ColorPaletteNumber = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
|
||||||
|
|
||||||
|
/** the color palette */
|
||||||
|
export type ColorPalette = {
|
||||||
|
/** the color hex value */
|
||||||
|
hex: string;
|
||||||
|
/**
|
||||||
|
* the color number
|
||||||
|
*
|
||||||
|
* - 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950
|
||||||
|
*/
|
||||||
|
number: ColorPaletteNumber;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** the color palette family */
|
||||||
|
export type ColorPaletteFamily = {
|
||||||
|
/** the color palette family name */
|
||||||
|
name: string;
|
||||||
|
/** the color palettes */
|
||||||
|
palettes: ColorPalette[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** the color palette with delta */
|
||||||
|
export type ColorPaletteWithDelta = ColorPalette & {
|
||||||
|
delta: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** the color palette family with nearest palette */
|
||||||
|
export type ColorPaletteFamilyWithNearestPalette = ColorPaletteFamily & {
|
||||||
|
nearestPalette: ColorPaletteWithDelta;
|
||||||
|
nearestLightnessPalette: ColorPaletteWithDelta;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** the color palette match */
|
||||||
|
export type ColorPaletteMatch = ColorPaletteFamily & {
|
||||||
|
/** the color map of the palette */
|
||||||
|
colorMap: Map<ColorPaletteNumber, ColorPalette>;
|
||||||
|
/**
|
||||||
|
* the main color of the palette
|
||||||
|
*
|
||||||
|
* which number is 500
|
||||||
|
*/
|
||||||
|
main: ColorPalette;
|
||||||
|
/** the match color of the palette */
|
||||||
|
match: ColorPalette;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The color index of color palette
|
||||||
|
*
|
||||||
|
* From left to right, the color is from light to dark, 6 is main color
|
||||||
|
*/
|
||||||
|
export type ColorIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "@sa/hooks",
|
||||||
|
"version": "2.0.2",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sa/axios": "workspace:*",
|
||||||
|
"@sa/utils": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import useBoolean from './use-boolean';
|
||||||
|
import useLoading from './use-loading';
|
||||||
|
import useCountDown from './use-count-down';
|
||||||
|
import useContext from './use-context';
|
||||||
|
import useSvgIconRender from './use-svg-icon-render';
|
||||||
|
import useTable from './use-table';
|
||||||
|
|
||||||
|
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useTable };
|
||||||
|
export type * from './use-table';
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean
|
||||||
|
*
|
||||||
|
* @param initValue Init value
|
||||||
|
*/
|
||||||
|
export default function useBoolean(initValue = false) {
|
||||||
|
const bool = ref(initValue);
|
||||||
|
|
||||||
|
function setBool(value: boolean) {
|
||||||
|
bool.value = value;
|
||||||
|
}
|
||||||
|
function setTrue() {
|
||||||
|
setBool(true);
|
||||||
|
}
|
||||||
|
function setFalse() {
|
||||||
|
setBool(false);
|
||||||
|
}
|
||||||
|
function toggle() {
|
||||||
|
setBool(!bool.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bool,
|
||||||
|
setBool,
|
||||||
|
setTrue,
|
||||||
|
setFalse,
|
||||||
|
toggle
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { inject, provide } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use context
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // there are three vue files: A.vue, B.vue, C.vue, and A.vue is the parent component of B.vue and C.vue
|
||||||
|
*
|
||||||
|
* // context.ts
|
||||||
|
* import { ref } from 'vue';
|
||||||
|
* import { useContext } from '@sa/hooks';
|
||||||
|
*
|
||||||
|
* export const [provideDemoContext, useDemoContext] = useContext('demo', () => {
|
||||||
|
* const count = ref(0);
|
||||||
|
*
|
||||||
|
* function increment() {
|
||||||
|
* count.value++;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* function decrement() {
|
||||||
|
* count.value--;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* return {
|
||||||
|
* count,
|
||||||
|
* increment,
|
||||||
|
* decrement
|
||||||
|
* };
|
||||||
|
* })
|
||||||
|
* ``` // A.vue
|
||||||
|
* ```vue
|
||||||
|
* <template>
|
||||||
|
* <div>A</div>
|
||||||
|
* </template>
|
||||||
|
* <script setup lang="ts">
|
||||||
|
* import { provideDemoContext } from './context';
|
||||||
|
*
|
||||||
|
* provideDemoContext();
|
||||||
|
* // const { increment } = provideDemoContext(); // also can control the store in the parent component
|
||||||
|
* </script>
|
||||||
|
* ``` // B.vue
|
||||||
|
* ```vue
|
||||||
|
* <template>
|
||||||
|
* <div>B</div>
|
||||||
|
* </template>
|
||||||
|
* <script setup lang="ts">
|
||||||
|
* import { useDemoContext } from './context';
|
||||||
|
*
|
||||||
|
* const { count, increment } = useDemoContext();
|
||||||
|
* </script>
|
||||||
|
* ```;
|
||||||
|
*
|
||||||
|
* // C.vue is same as B.vue
|
||||||
|
*
|
||||||
|
* @param contextName Context name
|
||||||
|
* @param fn Context function
|
||||||
|
*/
|
||||||
|
export default function useContext<Arguments extends Array<any>, T>(
|
||||||
|
contextName: string,
|
||||||
|
composable: (...args: Arguments) => T
|
||||||
|
) {
|
||||||
|
const key = Symbol(contextName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects the context value.
|
||||||
|
*
|
||||||
|
* @param consumerName - The name of the component that is consuming the context. If provided, the component must be
|
||||||
|
* used within the context provider.
|
||||||
|
* @param defaultValue - The default value to return if the context is not provided.
|
||||||
|
* @returns The context value.
|
||||||
|
*/
|
||||||
|
const useInject = <N extends string | null | undefined = undefined>(
|
||||||
|
consumerName?: N,
|
||||||
|
defaultValue?: T
|
||||||
|
): N extends null | undefined ? T | null : T => {
|
||||||
|
const value = inject(key, defaultValue);
|
||||||
|
|
||||||
|
if (consumerName && !value) {
|
||||||
|
throw new Error(`\`${consumerName}\` must be used within \`${contextName}\``);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error - we want to return null if the value is undefined or null
|
||||||
|
return value || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useProvide = (...args: Arguments) => {
|
||||||
|
const value = composable(...args);
|
||||||
|
|
||||||
|
provide(key, value);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return [useProvide, useInject] as const;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { computed, onScopeDispose, ref } from 'vue';
|
||||||
|
import { useRafFn } from '@vueuse/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook for implementing a countdown timer. It uses `requestAnimationFrame` for smooth and accurate timing,
|
||||||
|
* independent of the screen refresh rate.
|
||||||
|
*
|
||||||
|
* @param initialSeconds - The total number of seconds for the countdown.
|
||||||
|
*/
|
||||||
|
export default function useCountDown(initialSeconds: number) {
|
||||||
|
const remainingSeconds = ref(0);
|
||||||
|
|
||||||
|
const count = computed(() => Math.ceil(remainingSeconds.value));
|
||||||
|
|
||||||
|
const isCounting = computed(() => remainingSeconds.value > 0);
|
||||||
|
|
||||||
|
const { pause, resume } = useRafFn(
|
||||||
|
({ delta }) => {
|
||||||
|
// delta: milliseconds elapsed since the last frame.
|
||||||
|
|
||||||
|
// If countdown already reached zero or below, ensure it's 0 and stop.
|
||||||
|
if (remainingSeconds.value <= 0) {
|
||||||
|
remainingSeconds.value = 0;
|
||||||
|
pause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate seconds passed since the last frame.
|
||||||
|
const secondsPassed = delta / 1000;
|
||||||
|
remainingSeconds.value -= secondsPassed;
|
||||||
|
|
||||||
|
// If countdown has finished after decrementing.
|
||||||
|
if (remainingSeconds.value <= 0) {
|
||||||
|
remainingSeconds.value = 0;
|
||||||
|
pause();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: false } // The timer does not start automatically.
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the countdown.
|
||||||
|
*
|
||||||
|
* @param [updatedSeconds=initialSeconds] - Optionally, start with a new duration. Default is `initialSeconds`
|
||||||
|
*/
|
||||||
|
function start(updatedSeconds: number = initialSeconds) {
|
||||||
|
remainingSeconds.value = updatedSeconds;
|
||||||
|
resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stops the countdown and resets the remaining time to 0. */
|
||||||
|
function stop() {
|
||||||
|
remainingSeconds.value = 0;
|
||||||
|
pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the rAF loop is cleaned up when the component is unmounted.
|
||||||
|
onScopeDispose(() => {
|
||||||
|
pause();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
count,
|
||||||
|
isCounting,
|
||||||
|
start,
|
||||||
|
stop
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import useBoolean from './use-boolean';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading
|
||||||
|
*
|
||||||
|
* @param initValue Init value
|
||||||
|
*/
|
||||||
|
export default function useLoading(initValue = false) {
|
||||||
|
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initValue);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
startLoading,
|
||||||
|
endLoading
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { createFlatRequest } from '@sa/axios';
|
||||||
|
import type {
|
||||||
|
AxiosError,
|
||||||
|
CreateAxiosDefaults,
|
||||||
|
CustomAxiosRequestConfig,
|
||||||
|
MappedType,
|
||||||
|
RequestInstanceCommon,
|
||||||
|
RequestOption,
|
||||||
|
ResponseType
|
||||||
|
} from '@sa/axios';
|
||||||
|
import useLoading from './use-loading';
|
||||||
|
|
||||||
|
export type HookRequestInstanceResponseSuccessData<ApiData> = {
|
||||||
|
data: Ref<ApiData>;
|
||||||
|
error: Ref<null>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HookRequestInstanceResponseFailData<ResponseData> = {
|
||||||
|
data: Ref<null>;
|
||||||
|
error: Ref<AxiosError<ResponseData>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HookRequestInstanceResponseData<ResponseData, ApiData> = {
|
||||||
|
loading: Ref<boolean>;
|
||||||
|
} & (HookRequestInstanceResponseSuccessData<ApiData> | HookRequestInstanceResponseFailData<ResponseData>);
|
||||||
|
|
||||||
|
export interface HookRequestInstance<
|
||||||
|
ResponseData,
|
||||||
|
ApiData,
|
||||||
|
State extends Record<string, unknown>
|
||||||
|
> extends RequestInstanceCommon<State> {
|
||||||
|
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||||
|
config: CustomAxiosRequestConfig
|
||||||
|
): HookRequestInstanceResponseData<ResponseData, MappedType<R, T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a hook request instance
|
||||||
|
*
|
||||||
|
* @param axiosConfig
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
export default function createHookRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||||
|
axiosConfig?: CreateAxiosDefaults,
|
||||||
|
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||||
|
) {
|
||||||
|
const request = createFlatRequest<ResponseData, ApiData, State>(axiosConfig, options);
|
||||||
|
|
||||||
|
const hookRequest: HookRequestInstance<ResponseData, ApiData, State> = function hookRequest<
|
||||||
|
T extends ApiData = ApiData,
|
||||||
|
R extends ResponseType = 'json'
|
||||||
|
>(config: CustomAxiosRequestConfig) {
|
||||||
|
const { loading, startLoading, endLoading } = useLoading();
|
||||||
|
|
||||||
|
const data = ref(null) as Ref<MappedType<R, T>>;
|
||||||
|
const error = ref(null) as Ref<AxiosError<ResponseData> | null>;
|
||||||
|
|
||||||
|
startLoading();
|
||||||
|
|
||||||
|
request(config).then(res => {
|
||||||
|
if (res.data) {
|
||||||
|
data.value = res.data as MappedType<R, T>;
|
||||||
|
} else {
|
||||||
|
error.value = res.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
endLoading();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
data,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
} as HookRequestInstance<ResponseData, ApiData, State>;
|
||||||
|
|
||||||
|
hookRequest.cancelAllRequest = request.cancelAllRequest;
|
||||||
|
|
||||||
|
return hookRequest;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { h } from 'vue';
|
||||||
|
import type { Component } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Svg icon render hook
|
||||||
|
*
|
||||||
|
* @param SvgIcon Svg icon component
|
||||||
|
*/
|
||||||
|
export default function useSvgIconRender(SvgIcon: Component) {
|
||||||
|
interface IconConfig {
|
||||||
|
/** Iconify icon name */
|
||||||
|
icon?: string;
|
||||||
|
/** Local icon name */
|
||||||
|
localIcon?: string;
|
||||||
|
/** Icon color */
|
||||||
|
color?: string;
|
||||||
|
/** Icon size */
|
||||||
|
fontSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type IconStyle = Partial<Pick<CSSStyleDeclaration, 'color' | 'fontSize'>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Svg icon VNode
|
||||||
|
*
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
const SvgIconVNode = (config: IconConfig) => {
|
||||||
|
const { color, fontSize, icon, localIcon } = config;
|
||||||
|
|
||||||
|
const style: IconStyle = {};
|
||||||
|
|
||||||
|
if (color) {
|
||||||
|
style.color = color;
|
||||||
|
}
|
||||||
|
if (fontSize) {
|
||||||
|
style.fontSize = `${fontSize}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!icon && !localIcon) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => h(SvgIcon, { icon, localIcon, style });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
SvgIconVNode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import type { Ref, VNodeChild } from 'vue';
|
||||||
|
import useBoolean from './use-boolean';
|
||||||
|
import useLoading from './use-loading';
|
||||||
|
|
||||||
|
export interface PaginationData<T> {
|
||||||
|
data: T[];
|
||||||
|
pageNum: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetApiData<ApiData, Pagination extends boolean> = Pagination extends true ? PaginationData<ApiData> : ApiData[];
|
||||||
|
|
||||||
|
type Transform<ResponseData, ApiData, Pagination extends boolean> = (
|
||||||
|
response: ResponseData
|
||||||
|
) => GetApiData<ApiData, Pagination>;
|
||||||
|
|
||||||
|
export type TableColumnCheckTitle = string | ((...args: any) => VNodeChild);
|
||||||
|
|
||||||
|
export type TableColumnCheck = {
|
||||||
|
key: string;
|
||||||
|
title: TableColumnCheckTitle;
|
||||||
|
checked: boolean;
|
||||||
|
visible: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface UseTableOptions<ResponseData, ApiData, Column, Pagination extends boolean> {
|
||||||
|
/**
|
||||||
|
* api function to get table data
|
||||||
|
*/
|
||||||
|
api: () => Promise<ResponseData>;
|
||||||
|
/**
|
||||||
|
* whether to enable pagination
|
||||||
|
*/
|
||||||
|
pagination?: Pagination;
|
||||||
|
/**
|
||||||
|
* transform api response to table data
|
||||||
|
*/
|
||||||
|
transform: Transform<ResponseData, ApiData, Pagination>;
|
||||||
|
/**
|
||||||
|
* columns factory
|
||||||
|
*/
|
||||||
|
columns: () => Column[];
|
||||||
|
/**
|
||||||
|
* get column checks
|
||||||
|
*/
|
||||||
|
getColumnChecks: (columns: Column[]) => TableColumnCheck[];
|
||||||
|
/**
|
||||||
|
* get columns
|
||||||
|
*/
|
||||||
|
getColumns: (columns: Column[], checks: TableColumnCheck[]) => Column[];
|
||||||
|
/**
|
||||||
|
* callback when response fetched
|
||||||
|
*/
|
||||||
|
onFetched?: (data: GetApiData<ApiData, Pagination>) => void | Promise<void>;
|
||||||
|
/**
|
||||||
|
* whether to get data immediately
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
immediate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useTable<ResponseData, ApiData, Column, Pagination extends boolean>(
|
||||||
|
options: UseTableOptions<ResponseData, ApiData, Column, Pagination>
|
||||||
|
) {
|
||||||
|
const { loading, startLoading, endLoading } = useLoading();
|
||||||
|
const { bool: empty, setBool: setEmpty } = useBoolean();
|
||||||
|
|
||||||
|
const { api, pagination, transform, columns, getColumnChecks, getColumns, onFetched, immediate = true } = options;
|
||||||
|
|
||||||
|
const data = ref([]) as Ref<ApiData[]>;
|
||||||
|
|
||||||
|
const columnChecks = ref(getColumnChecks(columns())) as Ref<TableColumnCheck[]>;
|
||||||
|
|
||||||
|
const $columns = computed(() => getColumns(columns(), columnChecks.value));
|
||||||
|
|
||||||
|
function reloadColumns() {
|
||||||
|
const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
|
||||||
|
|
||||||
|
const defaultChecks = getColumnChecks(columns());
|
||||||
|
|
||||||
|
columnChecks.value = defaultChecks.map(col => ({
|
||||||
|
...col,
|
||||||
|
checked: checkMap.get(col.key) ?? col.checked
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getData() {
|
||||||
|
try {
|
||||||
|
startLoading();
|
||||||
|
|
||||||
|
const response = await api();
|
||||||
|
|
||||||
|
const transformed = transform(response);
|
||||||
|
|
||||||
|
data.value = getTableData(transformed, pagination);
|
||||||
|
|
||||||
|
setEmpty(data.value.length === 0);
|
||||||
|
|
||||||
|
await onFetched?.(transformed);
|
||||||
|
} finally {
|
||||||
|
endLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (immediate) {
|
||||||
|
getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
empty,
|
||||||
|
data,
|
||||||
|
columns: $columns,
|
||||||
|
columnChecks,
|
||||||
|
reloadColumns,
|
||||||
|
getData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTableData<ApiData, Pagination extends boolean>(
|
||||||
|
data: GetApiData<ApiData, Pagination>,
|
||||||
|
pagination?: Pagination
|
||||||
|
) {
|
||||||
|
if (pagination) {
|
||||||
|
return (data as PaginationData<ApiData>).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as ApiData[];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "@sa/materials",
|
||||||
|
"version": "2.0.2",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sa/utils": "workspace:*",
|
||||||
|
"simplebar-vue": "2.4.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typed-css-modules": "0.9.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import AdminLayout, { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './libs/admin-layout';
|
||||||
|
import PageTab from './libs/page-tab';
|
||||||
|
import SimpleScrollbar from './libs/simple-scrollbar';
|
||||||
|
|
||||||
|
export { AdminLayout, LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX, PageTab, SimpleScrollbar };
|
||||||
|
export * from './types';
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
/* @type */
|
||||||
|
|
||||||
|
.layout-header,
|
||||||
|
.layout-header-placement {
|
||||||
|
height: var(--soy-header-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-header {
|
||||||
|
z-index: var(--soy-header-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-tab {
|
||||||
|
top: var(--soy-header-height);
|
||||||
|
height: var(--soy-tab-height);
|
||||||
|
z-index: var(--soy-tab-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-tab-placement {
|
||||||
|
height: var(--soy-tab-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-sider {
|
||||||
|
width: var(--soy-sider-width);
|
||||||
|
z-index: var(--soy-sider-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-mobile-sider {
|
||||||
|
z-index: var(--soy-sider-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-mobile-sider-mask {
|
||||||
|
z-index: var(--soy-mobile-sider-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-sider_collapsed {
|
||||||
|
width: var(--soy-sider-collapsed-width);
|
||||||
|
z-index: var(--soy-sider-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-footer,
|
||||||
|
.layout-footer-placement {
|
||||||
|
height: var(--soy-footer-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-footer {
|
||||||
|
z-index: var(--soy-footer-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-gap {
|
||||||
|
padding-left: var(--soy-sider-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-gap_collapsed {
|
||||||
|
padding-left: var(--soy-sider-collapsed-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sider-padding-top {
|
||||||
|
padding-top: var(--soy-header-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sider-padding-bottom {
|
||||||
|
padding-bottom: var(--soy-footer-height);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
declare const styles: {
|
||||||
|
readonly 'layout-header': string;
|
||||||
|
readonly 'layout-header-placement': string;
|
||||||
|
readonly 'layout-tab': string;
|
||||||
|
readonly 'layout-tab-placement': string;
|
||||||
|
readonly 'layout-sider': string;
|
||||||
|
readonly 'layout-mobile-sider': string;
|
||||||
|
readonly 'layout-mobile-sider-mask': string;
|
||||||
|
readonly 'layout-sider_collapsed': string;
|
||||||
|
readonly 'layout-footer': string;
|
||||||
|
readonly 'layout-footer-placement': string;
|
||||||
|
readonly 'left-gap': string;
|
||||||
|
readonly 'left-gap_collapsed': string;
|
||||||
|
readonly 'sider-padding-top': string;
|
||||||
|
readonly 'sider-padding-bottom': string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default styles;
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import AdminLayout from './index.vue';
|
||||||
|
import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './shared';
|
||||||
|
|
||||||
|
export default AdminLayout;
|
||||||
|
export { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX };
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { AdminLayoutProps } from '../../types';
|
||||||
|
import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID, createLayoutCssVars } from './shared';
|
||||||
|
import style from './index.module.css';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'AdminLayout'
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<AdminLayoutProps>(), {
|
||||||
|
mode: 'vertical',
|
||||||
|
scrollMode: 'content',
|
||||||
|
scrollElId: LAYOUT_SCROLL_EL_ID,
|
||||||
|
commonClass: 'transition-all-300',
|
||||||
|
fixedTop: true,
|
||||||
|
maxZIndex: LAYOUT_MAX_Z_INDEX,
|
||||||
|
headerVisible: true,
|
||||||
|
headerHeight: 56,
|
||||||
|
tabVisible: true,
|
||||||
|
tabHeight: 48,
|
||||||
|
siderVisible: true,
|
||||||
|
siderCollapse: false,
|
||||||
|
siderWidth: 220,
|
||||||
|
siderCollapsedWidth: 64,
|
||||||
|
footerVisible: true,
|
||||||
|
footerHeight: 48,
|
||||||
|
rightFooter: false
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
/** Update siderCollapse */
|
||||||
|
(e: 'update:siderCollapse', collapse: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||||
|
|
||||||
|
type Slots = {
|
||||||
|
/** Main */
|
||||||
|
default?: SlotFn;
|
||||||
|
/** Header */
|
||||||
|
header?: SlotFn;
|
||||||
|
/** Tab */
|
||||||
|
tab?: SlotFn;
|
||||||
|
/** Sider */
|
||||||
|
sider?: SlotFn;
|
||||||
|
/** Footer */
|
||||||
|
footer?: SlotFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
const slots = defineSlots<Slots>();
|
||||||
|
|
||||||
|
const cssVars = computed(() => createLayoutCssVars(props));
|
||||||
|
|
||||||
|
// config visible
|
||||||
|
const showHeader = computed(() => Boolean(slots.header) && props.headerVisible);
|
||||||
|
const showTab = computed(() => Boolean(slots.tab) && props.tabVisible);
|
||||||
|
const showSider = computed(() => !props.isMobile && Boolean(slots.sider) && props.siderVisible);
|
||||||
|
const showMobileSider = computed(() => props.isMobile && Boolean(slots.sider) && props.siderVisible);
|
||||||
|
const showFooter = computed(() => Boolean(slots.footer) && props.footerVisible);
|
||||||
|
|
||||||
|
// scroll mode
|
||||||
|
const isWrapperScroll = computed(() => props.scrollMode === 'wrapper');
|
||||||
|
const isContentScroll = computed(() => props.scrollMode === 'content');
|
||||||
|
|
||||||
|
// layout direction
|
||||||
|
const isVertical = computed(() => props.mode === 'vertical');
|
||||||
|
const isHorizontal = computed(() => props.mode === 'horizontal');
|
||||||
|
|
||||||
|
const fixedHeaderAndTab = computed(() => props.fixedTop || (isHorizontal.value && isWrapperScroll.value));
|
||||||
|
|
||||||
|
// css
|
||||||
|
const leftGapClass = computed(() => {
|
||||||
|
if (!props.fullContent && showSider.value) {
|
||||||
|
return props.siderCollapse ? style['left-gap_collapsed'] : style['left-gap'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const headerLeftGapClass = computed(() => (isVertical.value ? leftGapClass.value : ''));
|
||||||
|
|
||||||
|
const footerLeftGapClass = computed(() => {
|
||||||
|
const condition1 = isVertical.value;
|
||||||
|
const condition2 = isHorizontal.value && isWrapperScroll.value && !props.fixedFooter;
|
||||||
|
const condition3 = Boolean(isHorizontal.value && props.rightFooter);
|
||||||
|
|
||||||
|
if (condition1 || condition2 || condition3) {
|
||||||
|
return leftGapClass.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const siderPaddingClass = computed(() => {
|
||||||
|
let cls = '';
|
||||||
|
|
||||||
|
if (showHeader.value && !headerLeftGapClass.value) {
|
||||||
|
cls += style['sider-padding-top'];
|
||||||
|
}
|
||||||
|
if (showFooter.value && !footerLeftGapClass.value) {
|
||||||
|
cls += ` ${style['sider-padding-bottom']}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cls;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleClickMask() {
|
||||||
|
emit('update:siderCollapse', true);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative h-full" :class="[commonClass]" :style="cssVars">
|
||||||
|
<div
|
||||||
|
:id="isWrapperScroll ? scrollElId : undefined"
|
||||||
|
class="h-full flex flex-col"
|
||||||
|
:class="[commonClass, scrollWrapperClass, { 'overflow-y-auto': isWrapperScroll }]"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<template v-if="showHeader">
|
||||||
|
<header
|
||||||
|
v-show="!fullContent"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
:class="[
|
||||||
|
style['layout-header'],
|
||||||
|
commonClass,
|
||||||
|
headerLeftGapClass,
|
||||||
|
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
v-show="!fullContent && fixedHeaderAndTab"
|
||||||
|
class="flex-shrink-0 overflow-hidden"
|
||||||
|
:class="[style['layout-header-placement']]"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Tab -->
|
||||||
|
<template v-if="showTab">
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0"
|
||||||
|
:class="[
|
||||||
|
style['layout-tab'],
|
||||||
|
commonClass,
|
||||||
|
tabClass,
|
||||||
|
{ 'top-0!': fullContent || !showHeader },
|
||||||
|
leftGapClass,
|
||||||
|
{ 'absolute left-0 w-full': fixedHeaderAndTab }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot name="tab"></slot>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-show="fullContent || fixedHeaderAndTab"
|
||||||
|
class="flex-shrink-0 overflow-hidden"
|
||||||
|
:class="[style['layout-tab-placement']]"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Sider -->
|
||||||
|
<template v-if="showSider">
|
||||||
|
<aside
|
||||||
|
v-show="!fullContent"
|
||||||
|
class="absolute left-0 top-0 h-full"
|
||||||
|
:class="[
|
||||||
|
commonClass,
|
||||||
|
siderClass,
|
||||||
|
siderPaddingClass,
|
||||||
|
siderCollapse ? style['layout-sider_collapsed'] : style['layout-sider']
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot name="sider"></slot>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Mobile Sider -->
|
||||||
|
<template v-if="showMobileSider">
|
||||||
|
<aside
|
||||||
|
class="absolute left-0 top-0 h-full w-0 bg-white"
|
||||||
|
:class="[
|
||||||
|
commonClass,
|
||||||
|
mobileSiderClass,
|
||||||
|
style['layout-mobile-sider'],
|
||||||
|
siderCollapse ? 'overflow-hidden' : style['layout-sider']
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot name="sider"></slot>
|
||||||
|
</aside>
|
||||||
|
<div
|
||||||
|
v-show="!siderCollapse"
|
||||||
|
class="absolute left-0 top-0 h-full w-full bg-[rgba(0,0,0,0.2)]"
|
||||||
|
:class="[style['layout-mobile-sider-mask']]"
|
||||||
|
@click="handleClickMask"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main
|
||||||
|
:id="isContentScroll ? scrollElId : undefined"
|
||||||
|
class="flex flex-col flex-grow"
|
||||||
|
:class="[commonClass, contentClass, leftGapClass, { 'overflow-y-auto': isContentScroll }]"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<template v-if="showFooter">
|
||||||
|
<footer
|
||||||
|
v-show="!fullContent"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
:class="[
|
||||||
|
style['layout-footer'],
|
||||||
|
commonClass,
|
||||||
|
footerClass,
|
||||||
|
footerLeftGapClass,
|
||||||
|
{ 'absolute left-0 bottom-0 w-full': fixedFooter }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</footer>
|
||||||
|
<div
|
||||||
|
v-show="!fullContent && fixedFooter"
|
||||||
|
class="flex-shrink-0 overflow-hidden"
|
||||||
|
:class="[style['layout-footer-placement']]"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import type { AdminLayoutProps, LayoutCssVars, LayoutCssVarsProps } from '../../types';
|
||||||
|
|
||||||
|
/** The id of the scroll element of the layout */
|
||||||
|
export const LAYOUT_SCROLL_EL_ID = '__SCROLL_EL_ID__';
|
||||||
|
|
||||||
|
/** The max z-index of the layout */
|
||||||
|
export const LAYOUT_MAX_Z_INDEX = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create layout css vars by css vars props
|
||||||
|
*
|
||||||
|
* @param props Css vars props
|
||||||
|
*/
|
||||||
|
function createLayoutCssVarsByCssVarsProps(props: LayoutCssVarsProps) {
|
||||||
|
const cssVars: LayoutCssVars = {
|
||||||
|
'--soy-header-height': `${props.headerHeight}px`,
|
||||||
|
'--soy-header-z-index': props.headerZIndex,
|
||||||
|
'--soy-tab-height': `${props.tabHeight}px`,
|
||||||
|
'--soy-tab-z-index': props.tabZIndex,
|
||||||
|
'--soy-sider-width': `${props.siderWidth}px`,
|
||||||
|
'--soy-sider-collapsed-width': `${props.siderCollapsedWidth}px`,
|
||||||
|
'--soy-sider-z-index': props.siderZIndex,
|
||||||
|
'--soy-mobile-sider-z-index': props.mobileSiderZIndex,
|
||||||
|
'--soy-footer-height': `${props.footerHeight}px`,
|
||||||
|
'--soy-footer-z-index': props.footerZIndex
|
||||||
|
};
|
||||||
|
|
||||||
|
return cssVars;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create layout css vars
|
||||||
|
*
|
||||||
|
* @param props
|
||||||
|
*/
|
||||||
|
export function createLayoutCssVars(props: AdminLayoutProps) {
|
||||||
|
const {
|
||||||
|
mode,
|
||||||
|
isMobile,
|
||||||
|
maxZIndex = LAYOUT_MAX_Z_INDEX,
|
||||||
|
headerHeight,
|
||||||
|
tabHeight,
|
||||||
|
siderWidth,
|
||||||
|
siderCollapsedWidth,
|
||||||
|
footerHeight
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const headerZIndex = maxZIndex - 3;
|
||||||
|
const tabZIndex = maxZIndex - 5;
|
||||||
|
const siderZIndex = mode === 'vertical' || isMobile ? maxZIndex - 1 : maxZIndex - 4;
|
||||||
|
const mobileSiderZIndex = isMobile ? maxZIndex - 2 : 0;
|
||||||
|
const footerZIndex = maxZIndex - 5;
|
||||||
|
|
||||||
|
const cssProps: LayoutCssVarsProps = {
|
||||||
|
headerHeight,
|
||||||
|
headerZIndex,
|
||||||
|
tabHeight,
|
||||||
|
tabZIndex,
|
||||||
|
siderWidth,
|
||||||
|
siderZIndex,
|
||||||
|
mobileSiderZIndex,
|
||||||
|
siderCollapsedWidth,
|
||||||
|
footerHeight,
|
||||||
|
footerZIndex
|
||||||
|
};
|
||||||
|
|
||||||
|
return createLayoutCssVarsByCssVarsProps(cssProps);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PageTabProps } from '../../types';
|
||||||
|
import style from './index.module.css';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ButtonTab'
|
||||||
|
});
|
||||||
|
|
||||||
|
defineProps<PageTabProps>();
|
||||||
|
|
||||||
|
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||||
|
|
||||||
|
type Slots = {
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The center content of the tab
|
||||||
|
*/
|
||||||
|
default?: SlotFn;
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The left content of the tab
|
||||||
|
*/
|
||||||
|
prefix?: SlotFn;
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The right content of the tab
|
||||||
|
*/
|
||||||
|
suffix?: SlotFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineSlots<Slots>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-12px whitespace-nowrap border-(1px solid) rounded-4px px-12px py-4px"
|
||||||
|
:class="[
|
||||||
|
style['button-tab'],
|
||||||
|
{ [style['button-tab_dark']]: darkMode },
|
||||||
|
{ [style['button-tab_active']]: active },
|
||||||
|
{ [style['button-tab_active_dark']]: active && darkMode }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot name="prefix"></slot>
|
||||||
|
<slot></slot>
|
||||||
|
<slot name="suffix"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({
|
||||||
|
name: 'ChromeTabBg'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg class="size-full">
|
||||||
|
<defs>
|
||||||
|
<symbol id="geometry-left" viewBox="0 0 214 36">
|
||||||
|
<path d="M17 0h197v36H0v-2c4.5 0 9-3.5 9-8V8c0-4.5 3.5-8 8-8z" />
|
||||||
|
</symbol>
|
||||||
|
<symbol id="geometry-right" viewBox="0 0 214 36">
|
||||||
|
<use xlink:href="#geometry-left" />
|
||||||
|
</symbol>
|
||||||
|
<clipPath>
|
||||||
|
<rect width="100%" height="100%" x="0" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<svg width="51%" height="100%">
|
||||||
|
<use xlink:href="#geometry-left" width="214" height="36" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
<g transform="scale(-1, 1)">
|
||||||
|
<svg width="51%" height="100%" x="-100%" y="0">
|
||||||
|
<use xlink:href="#geometry-right" width="214" height="36" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PageTabProps } from '../../types';
|
||||||
|
import ChromeTabBg from './chrome-tab-bg.vue';
|
||||||
|
import style from './index.module.css';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ChromeTab'
|
||||||
|
});
|
||||||
|
|
||||||
|
defineProps<PageTabProps>();
|
||||||
|
|
||||||
|
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||||
|
|
||||||
|
type Slots = {
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The center content of the tab
|
||||||
|
*/
|
||||||
|
default?: SlotFn;
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The left content of the tab
|
||||||
|
*/
|
||||||
|
prefix?: SlotFn;
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The right content of the tab
|
||||||
|
*/
|
||||||
|
suffix?: SlotFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineSlots<Slots>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-16px whitespace-nowrap px-24px py-6px -mr-18px"
|
||||||
|
:class="[
|
||||||
|
style['chrome-tab'],
|
||||||
|
{ [style['chrome-tab_dark']]: darkMode },
|
||||||
|
{ [style['chrome-tab_active']]: active },
|
||||||
|
{ [style['chrome-tab_active_dark']]: active && darkMode }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class=":soy: pointer-events-none absolute left-0 top-0 h-full w-full -z-1" :class="[style['chrome-tab__bg']]">
|
||||||
|
<ChromeTabBg />
|
||||||
|
</div>
|
||||||
|
<slot name="prefix"></slot>
|
||||||
|
<slot></slot>
|
||||||
|
<slot name="suffix"></slot>
|
||||||
|
<div class=":soy: absolute right-7px h-16px w-1px bg-#1f2225" :class="[style['chrome-tab-divider']]"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
/* @type */
|
||||||
|
|
||||||
|
.button-tab {
|
||||||
|
border-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-tab_dark {
|
||||||
|
border-color: #ffffff3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-tab:hover {
|
||||||
|
color: var(--soy-primary-color);
|
||||||
|
border-color: var(--soy-primary-color-opacity3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-tab_active {
|
||||||
|
color: var(--soy-primary-color);
|
||||||
|
border-color: var(--soy-primary-color-opacity3);
|
||||||
|
background-color: var(--soy-primary-color-opacity1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-tab_active_dark {
|
||||||
|
background-color: var(--soy-primary-color-opacity2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-tab .svg-close:hover {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: var(--soy-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-tab_dark .svg-close:hover {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab:hover {
|
||||||
|
z-index: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_active {
|
||||||
|
z-index: 10;
|
||||||
|
color: var(--soy-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab__bg {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_active .chrome-tab__bg {
|
||||||
|
color: var(--soy-primary-color1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_active_dark .chrome-tab__bg {
|
||||||
|
color: var(--soy-primary-color2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab:hover .chrome-tab__bg {
|
||||||
|
color: #dee1e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_active:hover .chrome-tab__bg {
|
||||||
|
color: var(--soy-primary-color1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_dark:hover .chrome-tab__bg {
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_active_dark:hover .chrome-tab__bg {
|
||||||
|
color: var(--soy-primary-color2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab .svg-close:hover {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_active .svg-close:hover {
|
||||||
|
background-color: var(--soy-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_dark .svg-close:hover {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_active .chrome-tab-divider {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab:hover .chrome-tab-divider {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-tab_dark .chrome-tab-divider {
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-tab {
|
||||||
|
background-color: transparent;
|
||||||
|
height: 100%;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-tab_dark {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-tab:hover {
|
||||||
|
color: var(--soy-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-tab_active {
|
||||||
|
color: var(--soy-primary-color);
|
||||||
|
background-color: var(--soy-primary-color-opacity1);
|
||||||
|
border-bottom-color: var(--soy-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-tab_active_dark {
|
||||||
|
background-color: var(--soy-primary-color-opacity2);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
declare const styles: {
|
||||||
|
readonly 'button-tab': string;
|
||||||
|
readonly 'button-tab_dark': string;
|
||||||
|
readonly 'button-tab_active': string;
|
||||||
|
readonly 'button-tab_active_dark': string;
|
||||||
|
readonly 'chrome-tab': string;
|
||||||
|
readonly 'chrome-tab_active': string;
|
||||||
|
readonly 'chrome-tab__bg': string;
|
||||||
|
readonly 'chrome-tab_active_dark': string;
|
||||||
|
readonly 'chrome-tab_dark': string;
|
||||||
|
readonly 'chrome-tab-divider': string;
|
||||||
|
readonly 'svg-close': string;
|
||||||
|
readonly 'slider-tab': string;
|
||||||
|
readonly 'slider-tab_active': string;
|
||||||
|
readonly 'slider-tab_active_dark': string;
|
||||||
|
readonly 'slider-tab_dark': string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default styles;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import PageTab from './index.vue';
|
||||||
|
|
||||||
|
export default PageTab;
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { Component } from 'vue';
|
||||||
|
import type { PageTabMode, PageTabProps } from '../../types';
|
||||||
|
import { ACTIVE_COLOR, createTabCssVars } from './shared';
|
||||||
|
import ChromeTab from './chrome-tab.vue';
|
||||||
|
import ButtonTab from './button-tab.vue';
|
||||||
|
import SliderTab from './slider-tab.vue';
|
||||||
|
import SvgClose from './svg-close.vue';
|
||||||
|
import style from './index.module.css';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'PageTab'
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<PageTabProps>(), {
|
||||||
|
mode: 'chrome',
|
||||||
|
commonClass: 'transition-all-300',
|
||||||
|
activeColor: ACTIVE_COLOR,
|
||||||
|
closable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'close'): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const activeTabComponent = computed(() => {
|
||||||
|
const { mode, chromeClass, buttonClass, sliderClass } = props;
|
||||||
|
|
||||||
|
const tabComponentMap = {
|
||||||
|
chrome: {
|
||||||
|
component: ChromeTab,
|
||||||
|
class: chromeClass
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
component: ButtonTab,
|
||||||
|
class: buttonClass
|
||||||
|
},
|
||||||
|
slider: {
|
||||||
|
component: SliderTab,
|
||||||
|
class: sliderClass
|
||||||
|
}
|
||||||
|
} satisfies Record<PageTabMode, { component: Component; class?: string }>;
|
||||||
|
|
||||||
|
return tabComponentMap[mode];
|
||||||
|
});
|
||||||
|
|
||||||
|
const cssVars = computed(() => createTabCssVars(props.activeColor));
|
||||||
|
|
||||||
|
const bindProps = computed(() => {
|
||||||
|
const { chromeClass: _chromeCls, buttonClass: _btnCls, sliderClass: _sliderCls, ...rest } = props;
|
||||||
|
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="activeTabComponent.component" :class="activeTabComponent.class" :style="cssVars" v-bind="bindProps">
|
||||||
|
<template #prefix>
|
||||||
|
<slot name="prefix"></slot>
|
||||||
|
</template>
|
||||||
|
<slot></slot>
|
||||||
|
<template #suffix>
|
||||||
|
<slot name="suffix">
|
||||||
|
<SvgClose v-if="closable" :class="[style['svg-close']]" @pointerdown.stop="handleClose" />
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { addColorAlpha, transformColorWithOpacity } from '@sa/color';
|
||||||
|
import type { PageTabCssVars, PageTabCssVarsProps } from '../../types';
|
||||||
|
|
||||||
|
/** The active color of the tab */
|
||||||
|
export const ACTIVE_COLOR = '#1890ff';
|
||||||
|
|
||||||
|
function createCssVars(props: PageTabCssVarsProps) {
|
||||||
|
const cssVars: PageTabCssVars = {
|
||||||
|
'--soy-primary-color': props.primaryColor,
|
||||||
|
'--soy-primary-color1': props.primaryColor1,
|
||||||
|
'--soy-primary-color2': props.primaryColor2,
|
||||||
|
'--soy-primary-color-opacity1': props.primaryColorOpacity1,
|
||||||
|
'--soy-primary-color-opacity2': props.primaryColorOpacity2,
|
||||||
|
'--soy-primary-color-opacity3': props.primaryColorOpacity3
|
||||||
|
};
|
||||||
|
|
||||||
|
return cssVars;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTabCssVars(primaryColor: string) {
|
||||||
|
const cssProps: PageTabCssVarsProps = {
|
||||||
|
primaryColor,
|
||||||
|
primaryColor1: transformColorWithOpacity(primaryColor, 0.1, '#ffffff'),
|
||||||
|
primaryColor2: transformColorWithOpacity(primaryColor, 0.3, '#000000'),
|
||||||
|
primaryColorOpacity1: addColorAlpha(primaryColor, 0.1),
|
||||||
|
primaryColorOpacity2: addColorAlpha(primaryColor, 0.15),
|
||||||
|
primaryColorOpacity3: addColorAlpha(primaryColor, 0.3)
|
||||||
|
};
|
||||||
|
|
||||||
|
return createCssVars(cssProps);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PageTabProps } from '../../types';
|
||||||
|
import style from './index.module.css';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'SliderTab'
|
||||||
|
});
|
||||||
|
|
||||||
|
defineProps<PageTabProps>();
|
||||||
|
|
||||||
|
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||||
|
|
||||||
|
type Slots = {
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The center content of the tab
|
||||||
|
*/
|
||||||
|
default?: SlotFn;
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The left content of the tab
|
||||||
|
*/
|
||||||
|
prefix?: SlotFn;
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The right content of the tab
|
||||||
|
*/
|
||||||
|
suffix?: SlotFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineSlots<Slots>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-6px whitespace-nowrap px-12px py-4px"
|
||||||
|
:class="[
|
||||||
|
style['slider-tab'],
|
||||||
|
{ [style['slider-tab_dark']]: darkMode },
|
||||||
|
{ [style['slider-tab_active']]: active },
|
||||||
|
{ [style['slider-tab_active_dark']]: active && darkMode }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot name="prefix"></slot>
|
||||||
|
<slot></slot>
|
||||||
|
<slot name="suffix"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({
|
||||||
|
name: 'SvgClose'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class=":soy: relative h-16px w-16px inline-flex items-center justify-center rd-50% text-14px">
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 1024 1024">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="m563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8L295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512L196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1l216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import SimpleScrollbar from './index.vue';
|
||||||
|
|
||||||
|
export default SimpleScrollbar;
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Simplebar from 'simplebar-vue';
|
||||||
|
import 'simplebar-vue/dist/simplebar.min.css';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'SimpleScrollbar'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full flex-1-hidden">
|
||||||
|
<Simplebar class="h-full">
|
||||||
|
<slot />
|
||||||
|
</Simplebar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,291 @@
|
||||||
|
/** Header config */
|
||||||
|
interface AdminLayoutHeaderConfig {
|
||||||
|
/**
|
||||||
|
* Whether header is visible
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
headerVisible?: boolean;
|
||||||
|
/**
|
||||||
|
* Header height
|
||||||
|
*
|
||||||
|
* @default 56px
|
||||||
|
*/
|
||||||
|
headerHeight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tab config */
|
||||||
|
interface AdminLayoutTabConfig {
|
||||||
|
/**
|
||||||
|
* Whether tab is visible
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
tabVisible?: boolean;
|
||||||
|
/**
|
||||||
|
* Tab class
|
||||||
|
*
|
||||||
|
* @default ''
|
||||||
|
*/
|
||||||
|
tabClass?: string;
|
||||||
|
/**
|
||||||
|
* Tab height
|
||||||
|
*
|
||||||
|
* @default 48px
|
||||||
|
*/
|
||||||
|
tabHeight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sider config */
|
||||||
|
interface AdminLayoutSiderConfig {
|
||||||
|
/**
|
||||||
|
* Whether sider is visible
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
siderVisible?: boolean;
|
||||||
|
/**
|
||||||
|
* Sider class
|
||||||
|
*
|
||||||
|
* @default ''
|
||||||
|
*/
|
||||||
|
siderClass?: string;
|
||||||
|
/**
|
||||||
|
* Mobile sider class
|
||||||
|
*
|
||||||
|
* @default ''
|
||||||
|
*/
|
||||||
|
mobileSiderClass?: string;
|
||||||
|
/**
|
||||||
|
* Sider collapse status
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
siderCollapse?: boolean;
|
||||||
|
/**
|
||||||
|
* Sider width when collapse is false
|
||||||
|
*
|
||||||
|
* @default '220px'
|
||||||
|
*/
|
||||||
|
siderWidth?: number;
|
||||||
|
/**
|
||||||
|
* Sider width when collapse is true
|
||||||
|
*
|
||||||
|
* @default '64px'
|
||||||
|
*/
|
||||||
|
siderCollapsedWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Content config */
|
||||||
|
export interface AdminLayoutContentConfig {
|
||||||
|
/**
|
||||||
|
* Content class
|
||||||
|
*
|
||||||
|
* @default ''
|
||||||
|
*/
|
||||||
|
contentClass?: string;
|
||||||
|
/**
|
||||||
|
* Whether content is full the page
|
||||||
|
*
|
||||||
|
* If true, other elements will be hidden by `display: none`
|
||||||
|
*/
|
||||||
|
fullContent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Footer config */
|
||||||
|
export interface AdminLayoutFooterConfig {
|
||||||
|
/**
|
||||||
|
* Whether footer is visible
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
footerVisible?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether footer is fixed
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
fixedFooter?: boolean;
|
||||||
|
/**
|
||||||
|
* Footer class
|
||||||
|
*
|
||||||
|
* @default ''
|
||||||
|
*/
|
||||||
|
footerClass?: string;
|
||||||
|
/**
|
||||||
|
* Footer height
|
||||||
|
*
|
||||||
|
* @default 48px
|
||||||
|
*/
|
||||||
|
footerHeight?: number;
|
||||||
|
/**
|
||||||
|
* Whether footer is on the right side
|
||||||
|
*
|
||||||
|
* When the layout is vertical, the footer is on the right side
|
||||||
|
*/
|
||||||
|
rightFooter?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout mode
|
||||||
|
*
|
||||||
|
* - Horizontal
|
||||||
|
* - Vertical
|
||||||
|
*/
|
||||||
|
export type LayoutMode = 'horizontal' | 'vertical';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scroll mode when content overflow
|
||||||
|
*
|
||||||
|
* - Wrapper: the layout component's wrapper element has a scrollbar
|
||||||
|
* - Content: the layout component's content element has a scrollbar
|
||||||
|
*
|
||||||
|
* @default 'wrapper'
|
||||||
|
*/
|
||||||
|
export type LayoutScrollMode = 'wrapper' | 'content';
|
||||||
|
|
||||||
|
/** Admin layout props */
|
||||||
|
export interface AdminLayoutProps
|
||||||
|
extends
|
||||||
|
AdminLayoutHeaderConfig,
|
||||||
|
AdminLayoutTabConfig,
|
||||||
|
AdminLayoutSiderConfig,
|
||||||
|
AdminLayoutContentConfig,
|
||||||
|
AdminLayoutFooterConfig {
|
||||||
|
/**
|
||||||
|
* Layout mode
|
||||||
|
*
|
||||||
|
* - {@link LayoutMode}
|
||||||
|
*/
|
||||||
|
mode?: LayoutMode;
|
||||||
|
/** Is mobile layout */
|
||||||
|
isMobile?: boolean;
|
||||||
|
/**
|
||||||
|
* Scroll mode
|
||||||
|
*
|
||||||
|
* - {@link ScrollMode}
|
||||||
|
*/
|
||||||
|
scrollMode?: LayoutScrollMode;
|
||||||
|
/**
|
||||||
|
* The id of the scroll element of the layout
|
||||||
|
*
|
||||||
|
* It can be used to get the corresponding Dom and scroll it
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* use the default id by import
|
||||||
|
* ```ts
|
||||||
|
* import { adminLayoutScrollElId } from '@sa/vue-materials';
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @default
|
||||||
|
* ```ts
|
||||||
|
* const adminLayoutScrollElId = '__ADMIN_LAYOUT_SCROLL_EL_ID__'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
scrollElId?: string;
|
||||||
|
/** The class of the scroll element */
|
||||||
|
scrollElClass?: string;
|
||||||
|
/** The class of the scroll wrapper element */
|
||||||
|
scrollWrapperClass?: string;
|
||||||
|
/**
|
||||||
|
* The common class of the layout
|
||||||
|
*
|
||||||
|
* Is can be used to configure the transition animation
|
||||||
|
*
|
||||||
|
* @default 'transition-all-300'
|
||||||
|
*/
|
||||||
|
commonClass?: string;
|
||||||
|
/**
|
||||||
|
* Whether fix the header and tab
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
fixedTop?: boolean;
|
||||||
|
/**
|
||||||
|
* The max z-index of the layout
|
||||||
|
*
|
||||||
|
* The z-index of Header,Tab,Sider and Footer will not exceed this value
|
||||||
|
*/
|
||||||
|
maxZIndex?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Kebab<S extends string> = S extends Uncapitalize<S> ? S : `-${Uncapitalize<S>}`;
|
||||||
|
|
||||||
|
type KebabCase<S extends string> = S extends `${infer Start}${infer End}`
|
||||||
|
? `${Uncapitalize<Start>}${KebabCase<Kebab<End>>}`
|
||||||
|
: S;
|
||||||
|
|
||||||
|
type Prefix = '--soy-';
|
||||||
|
|
||||||
|
export type LayoutCssVarsProps = Pick<
|
||||||
|
AdminLayoutProps,
|
||||||
|
'headerHeight' | 'tabHeight' | 'siderWidth' | 'siderCollapsedWidth' | 'footerHeight'
|
||||||
|
> & {
|
||||||
|
headerZIndex?: number;
|
||||||
|
tabZIndex?: number;
|
||||||
|
siderZIndex?: number;
|
||||||
|
mobileSiderZIndex?: number;
|
||||||
|
footerZIndex?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LayoutCssVars = {
|
||||||
|
[K in keyof LayoutCssVarsProps as `${Prefix}${KebabCase<K>}`]: string | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mode of the tab
|
||||||
|
*
|
||||||
|
* - Button: button style
|
||||||
|
* - Chrome: chrome style
|
||||||
|
*
|
||||||
|
* @default chrome
|
||||||
|
*/
|
||||||
|
export type PageTabMode = 'button' | 'chrome' | 'slider';
|
||||||
|
|
||||||
|
export interface PageTabProps {
|
||||||
|
/** Whether is dark mode */
|
||||||
|
darkMode?: boolean;
|
||||||
|
/**
|
||||||
|
* The mode of the tab
|
||||||
|
*
|
||||||
|
* - {@link TabMode}
|
||||||
|
*/
|
||||||
|
mode?: PageTabMode;
|
||||||
|
/**
|
||||||
|
* The common class of the layout
|
||||||
|
*
|
||||||
|
* Is can be used to configure the transition animation
|
||||||
|
*
|
||||||
|
* @default 'transition-all-300'
|
||||||
|
*/
|
||||||
|
commonClass?: string;
|
||||||
|
/** The class of the button tab */
|
||||||
|
buttonClass?: string;
|
||||||
|
/** The class of the chrome tab */
|
||||||
|
chromeClass?: string;
|
||||||
|
/** The class of the title tab */
|
||||||
|
sliderClass?: string;
|
||||||
|
/** Whether the tab is active */
|
||||||
|
active?: boolean;
|
||||||
|
/** The color of the active tab */
|
||||||
|
activeColor?: string;
|
||||||
|
/**
|
||||||
|
* Whether the tab is closable
|
||||||
|
*
|
||||||
|
* Show the close icon when true
|
||||||
|
*/
|
||||||
|
closable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PageTabCssVarsProps = {
|
||||||
|
primaryColor: string;
|
||||||
|
primaryColor1: string;
|
||||||
|
primaryColor2: string;
|
||||||
|
primaryColorOpacity1: string;
|
||||||
|
primaryColorOpacity2: string;
|
||||||
|
primaryColorOpacity3: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PageTabCssVars = {
|
||||||
|
[K in keyof PageTabCssVarsProps as `${Prefix}${KebabCase<K>}`]: string | number;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue