71 Commits

Author SHA1 Message Date
3496852be1 Merge remote-tracking branch 'origin/dev' into v2.0
# Conflicts:
#	src/service/request/index.ts
#	src/views/_builtin/login/index.vue
#	src/views/system/dept/modules/dept-operate-drawer.vue
#	src/views/system/menu/modules/menu-operate-drawer.vue
#	src/views/system/post/index.vue
#	src/views/system/user/index.vue
#	src/views/tool/gen/modules/gen-table-operate-drawer.vue
2025-09-16 15:32:18 +08:00
4e27f3b5a5 fix(projects): 修复登录页面样式问题 2025-09-15 17:07:38 +08:00
e623b560e4 docs(other): 更新 cursor 规则 2025-09-15 11:40:20 +08:00
8aeb73627a fix(projects): 修复登录页面跳转问题 2025-09-13 13:59:51 +08:00
3f148a4e62 fix(projects): 修复消息通知字典值未处理问题 2025-09-12 10:04:36 +08:00
34ab7d5da2 fix(projects): 修复菜单默认图标问题 2025-09-11 11:45:52 +08:00
513dc31eaa feat(styles): 优化左侧树形结构样式 2025-09-11 10:28:43 +08:00
dc2fbbd556 fix(projects): 修复退出登录未清空消息列表问题 2025-09-11 10:21:11 +08:00
56fd5434ca optimize(projects): 字典状态使用枚举值 2025-09-11 10:16:20 +08:00
ad207255bb fix(projects): 修复菜单弹窗打开未清空默认值问题 2025-09-11 10:14:45 +08:00
3146c039f0 feat(projects): 用户列表新增头像展示 2025-09-11 10:09:35 +08:00
AN
d5bbc37dec fix(projects): 修复新增部门时不显示上级部门问题 2025-09-10 11:48:13 +08:00
AN
2f794c4b73 fix(projects): 修改代码生成功能模块名为驼峰时,路由错误问题 2025-09-10 10:40:23 +08:00
AN
378aa869bf style(components): 修改json预览组件样式问题 2025-09-08 13:51:28 +08:00
AN
4a4244b5c4 style(styles): 修复字体样式导致下划线不可见问题 2025-09-05 17:20:55 +08:00
AN
ecad1c3e78 optimize(components): 补充国际化 2025-09-05 14:31:23 +08:00
9ef0bd416e fix(utils): 修复请求工具响应解密问题 2025-09-04 14:57:12 +08:00
25ee32074a feat(projects): 路由兼容 activeMenu 选项 2025-09-02 15:10:59 +08:00
8412a8db16 feat(projects): 重构登录页面样式 2025-09-02 14:49:58 +08:00
9c51f75404 feat(projects): 登录页新增装饰点缀 2025-09-01 17:49:51 +08:00
75cc0e4592 feat(projects): 登陆页面样式重构 2025-09-01 17:05:42 +08:00
f9afd27f4b Merge remote-tracking branch 'Soybean/v2.0' into v2.0
# Conflicts:
#	build/config/proxy.ts
#	package.json
#	packages/scripts/package.json
#	pnpm-lock.yaml
#	src/typings/components.d.ts
2025-09-01 15:28:16 +08:00
6dcc8c349a Merge branch 'dev' into v2.0
# Conflicts:
#	package.json
#	packages/scripts/package.json
#	pnpm-lock.yaml
#	src/views/system/user/modules/user-operate-drawer.vue
2025-09-01 15:18:26 +08:00
e230b0da81 Merge remote-tracking branch 'Soybean/main' into dev
# Conflicts:
#	build/config/proxy.ts
#	package.json
#	pnpm-lock.yaml
2025-09-01 15:06:44 +08:00
12b25e0d58 fix(types): fix proxy types 2025-08-28 00:26:29 +08:00
e33f944a74 chore(deps): update deps 2025-08-28 00:26:12 +08:00
3d72f954ed fix(types): fix proxy types 2025-08-28 00:11:13 +08:00
1213531bef chore(deps): update deps 2025-08-28 00:09:07 +08:00
805c338141 chore(packages): add picomatch to fix scripts 2025-08-28 00:08:44 +08:00
3c0a52825d feat(projects): modify the default value of the reset cache policy to 'refresh'. 2025-08-27 18:17:55 +08:00
100e0ea55d style(projects): format code. 2025-08-24 14:14:50 +08:00
257f1183fc feat(projects): support theme preset function. 2025-08-24 14:14:50 +08:00
2504498eb5 fix(hooks): 修复树表全部展开问题(临时) 2025-08-19 11:00:19 +08:00
7f2f3bd088 feat(utils): 新增本地 Excel 导出工具类 2025-08-18 16:14:29 +08:00
5ef1c5de98 fix(hooks): 修复下载 hooks 错误未处理 2025-08-18 15:24:25 +08:00
90a14e338a fix(components): 修复字典标签会修改字典数据值问题 2025-08-18 14:10:12 +08:00
5581a4a59f Merge remote-tracking branch 'Soybean/v2.0' into v2.0
# Conflicts:
#	src/typings/components.d.ts
2025-08-14 18:14:31 +08:00
7c83ce7937 Merge branch 'dev' into v2.0
# Conflicts:
#	src/views/system/user/modules/user-import-modal.vue
2025-08-14 18:12:03 +08:00
d73111116a refactor(menu): optimize the margin on the menu 2025-08-14 17:41:57 +08:00
29a2a5c66a feat(projects): add prompt information for scrolling mode and tab bar caching. 2025-08-13 15:54:15 +08:00
4005763c00 feat(components): replace NTooltip with IconTooltip and optimize the layout of related components. 2025-08-13 15:54:15 +08:00
a55b4dc073 feat(components): add the IconTooltip component. 2025-08-13 15:54:15 +08:00
be8f915a0c chore(other): update the ESLint validation configuration to support more file types. 2025-08-13 15:54:15 +08:00
358e129765 feat(hooks): add scrollX computation for total table width in useNaiveTable 2025-08-11 10:42:17 +08:00
2af57caf44 Merge branch 'dev' into v2.0
# Conflicts:
#	src/views/system/user/modules/user-import-modal.vue
#	src/views/system/user/modules/user-operate-drawer.vue
2025-08-08 17:21:41 +08:00
4699654fc1 feat(hooks): 完成表格 Hooks 改造 2025-07-31 17:08:06 +08:00
01116c9ffa Merge remote-tracking branch 'Soybean/v2.0' into v2.0 2025-07-31 14:53:35 +08:00
9ea56c9b82 fix(packages): fix the parsing logic for stored data to ensure correct return of boolean values 2025-07-31 11:24:42 +08:00
87adc35f2e refactor(hooks): remove useSignal hook and update exports 2025-07-20 00:31:03 +08:00
ee4341457a refactor(hooks): streamline column visibility handling in useTable and table components 2025-07-20 00:30:16 +08:00
8a7cd5934b fix(hooks): correct chart rendering logic in useEcharts 2025-07-19 20:09:20 +08:00
c962f7b2c5 chore(deps): update deps 2025-07-19 19:45:49 +08:00
8cc5177cda refactor(hooks)!: refactor useTable and enhance type definitions 2025-07-19 19:43:58 +08:00
3a343eea33 optimize(projects): optimize api type file 2025-07-19 18:25:59 +08:00
f83eefbc3e refactor(request): unify response transformation methods and deprecate transformBackendResponse 2025-07-19 12:04:00 +08:00
936b834e62 optimize(hooks): optimize useEcharts 2025-07-19 02:53:46 +08:00
c965140b87 refactor(hooks): optimize useContext and update useMixMenuContext 2025-07-19 02:40:25 +08:00
32b8f99071 fix(table): add type annotations for records in useTable hook 2025-07-19 02:28:05 +08:00
abaaa4a068 optimize(packages): remove ofetch package 2025-07-19 02:27:54 +08:00
b4e125300e refactor(request)!: remove cancelRequest method and related logic from request instances 2025-07-19 02:24:14 +08:00
50a5cba088 optimize(request): enhance request options and response handling with generic types 2025-07-19 02:17:50 +08:00
d6c8142bb4 refactor(projects): remove unnecessary logic in onRouteSwitchWhenLoggedIn 2025-07-15 22:04:18 +08:00
8146858b96 optimize(projects): optimize theme drawer width 2025-07-14 00:48:17 +08:00
8b8a2083bb optimize(projects): improve robustness of second-level menu key logic 2025-07-14 00:48:16 +08:00
6207292d81 typo(projects): update description of vertical-hybrid-header-first layout mode 2025-07-14 00:48:16 +08:00
b4e5c6d990 feat(projects): add 'vertical-hybrid-header-first' layout mode 2025-07-14 00:48:16 +08:00
b6ac3106ce feat(projects)!: optimize layout mode, split horizontal mix component into two layouts, and rename the component. 2025-07-14 00:48:16 +08:00
d37ce04606 refactor(types): move Auth and Route namespaces to separate files and clean up api.d.ts 2025-07-14 00:48:16 +08:00
8439a60070 optimize(projects): improve theme drawer responsive width for mobile devices 2025-07-14 00:48:16 +08:00
f238fcbd47 feat(projects): Add current time display option for watermark (#772)
* feat(projects): Add current time display option for watermark

* perf(projects): add watermark timer controls
2025-07-14 00:48:16 +08:00
8ba71a0857 feat(projects): refactor theme drawer with tabbed layout for better UX. 2025-07-14 00:48:16 +08:00
234 changed files with 5273 additions and 27566 deletions

View File

@ -16,7 +16,7 @@
}, },
"playwright": { "playwright": {
"command": "npx", "command": "npx",
"args": ["@playwright/mcp@0.0.29"] "args": ["@playwright/mcp@latest"]
}, },
"mcp-server-time": { "mcp-server-time": {
"command": "uvx", "command": "uvx",
@ -26,14 +26,21 @@
"command": "npx", "command": "npx",
"args": ["-y", "mcp-shrimp-task-manager"], "args": ["-y", "mcp-shrimp-task-manager"],
"env": { "env": {
"DATA_DIR": "D:/workspace/mcp-shrimp-task-manager/data", "DATA_DIR": "D:/workspace/tools/mcp-shrimp-task-manager/data",
"TEMPLATES_USE": "en", "TEMPLATES_USE": "en",
"ENABLE_GUI": "false" "ENABLE_GUI": "true"
} }
}, },
"mcp-deepwiki": { "mcp-deepwiki": {
"command": "npx", "command": "npx",
"args": ["-y", "mcp-deepwiki@latest"] "args": ["-y", "mcp-deepwiki@latest"]
},
"memory": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-memory"],
"env": {
"MEMORY_FILE_PATH": "D:/workspace/tools/server-memory/memory.json"
}
} }
} }
} }

View File

@ -1,9 +1,6 @@
--- ---
description:
globs:
alwaysApply: false alwaysApply: false
--- ---
**# RIPER-5 + 多维度思维 + 代理执行协议 (v4.9.1 - MCP工具驱动版)** **# RIPER-5 + 多维度思维 + 代理执行协议 (v4.9.1 - MCP工具驱动版)**
**元指令:** 此协议旨在最大化你的战略规划与执行效率。你的核心任务是**指挥和利用MCP工具集**来驱动项目进展。严格遵守核心原则,利用 `mcp-shrimp-task-manager` 进行项目规划与追踪,使用 `deepwiki-mcp` 进行深度研究。主动管理 `/project_document` 作为知识库。**每轮主要响应后,调用 `mcp.feedback_enhanced` 进行交互或通知。** **元指令:** 此协议旨在最大化你的战略规划与执行效率。你的核心任务是**指挥和利用MCP工具集**来驱动项目进展。严格遵守核心原则,利用 `mcp-shrimp-task-manager` 进行项目规划与追踪,使用 `deepwiki-mcp` 进行深度研究。主动管理 `/project_document` 作为知识库。**每轮主要响应后,调用 `mcp.feedback_enhanced` 进行交互或通知。**
@ -167,5 +164,4 @@ alwaysApply: false
* **极致效率:** AI应最大限度地减少手动干预让MCP工具处理所有可以自动化的工作。 * **极致效率:** AI应最大限度地减少手动干预让MCP工具处理所有可以自动化的工作。
* **战略聚焦:** 将AI的“思考”集中在无法被工具替代的领域战略决策、创新构想、复杂问题诊断 (`mcp.sequential_thinking`) 和最终质量把关。 * **战略聚焦:** 将AI的“思考”集中在无法被工具替代的领域战略决策、创新构想、复杂问题诊断 (`mcp.sequential_thinking`) 和最终质量把关。
* **无缝集成:** 期望AI能流畅地在不同MCP工具之间传递信息形成一个高度整合的自动化工作流。 * **无缝集成:** 期望AI能流畅地在不同MCP工具之间传递信息形成一个高度整合的自动化工作流。

View File

@ -15,6 +15,8 @@ VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 接口加密功能开关(如需关闭 后端也必须对应关闭) # 接口加密功能开关(如需关闭 后端也必须对应关闭)
VITE_APP_ENCRYPT=Y VITE_APP_ENCRYPT=Y
# AES 加密头标识
VITE_HEADER_FLAG=encrypt-key
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换 # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换 # 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换

View File

@ -12,6 +12,8 @@ VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 接口加密功能开关(如需关闭 后端也必须对应关闭) # 接口加密功能开关(如需关闭 后端也必须对应关闭)
VITE_APP_ENCRYPT=Y VITE_APP_ENCRYPT=Y
# AES 加密头标识
VITE_HEADER_FLAG=encrypt-key
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换 # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换 # 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换

View File

@ -12,6 +12,8 @@ VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 接口加密功能开关(如需关闭 后端也必须对应关闭) # 接口加密功能开关(如需关闭 后端也必须对应关闭)
VITE_APP_ENCRYPT=Y VITE_APP_ENCRYPT=Y
# AES 加密头标识
VITE_HEADER_FLAG=encrypt-key
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换 # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换 # 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换

View File

@ -31,7 +31,8 @@
"vue.server.hybridMode": true, "vue.server.hybridMode": true,
"files.exclude": { "/docs": true }, "files.exclude": { "/docs": true },
"search.exclude": { "search.exclude": {
"/docs": true "/docs": true,
"**/dist/**": true
}, },
"cSpell.words": ["Axios", "tinymce"] "cSpell.words": ["Axios", "tinymce"]
} }

View File

@ -1,4 +1,4 @@
import type { HttpProxy, ProxyOptions } from 'vite'; import type { ProxyOptions } from 'vite';
import { bgRed, bgYellow, green, lightBlue } from 'kolorist'; import { bgRed, bgYellow, green, lightBlue } from 'kolorist';
import { consola } from 'consola'; import { consola } from 'consola';
import { createServiceConfig } from '../../src/utils/service'; import { createServiceConfig } from '../../src/utils/service';
@ -34,7 +34,7 @@ function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean
target: item.baseURL, target: item.baseURL,
changeOrigin: true, changeOrigin: true,
ws: item.ws, ws: item.ws,
configure: (_proxy: HttpProxy.Server, options: ProxyOptions) => { configure: (_proxy, options) => {
_proxy.on('proxyReq', (_proxyReq, req, _res) => { _proxy.on('proxyReq', (_proxyReq, req, _res) => {
if (!enableLog) return; if (!enableLog) return;

View File

@ -10,6 +10,7 @@ import org.apache.velocity.VelocityContext;
import org.dromara.common.core.utils.DateUtils; import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils; import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.enums.DataBaseType;
import org.dromara.common.mybatis.helper.DataBaseHelper; import org.dromara.common.mybatis.helper.DataBaseHelper;
import org.dromara.generator.constant.GenConstants; import org.dromara.generator.constant.GenConstants;
import org.dromara.generator.domain.GenTable; import org.dromara.generator.domain.GenTable;
@ -58,7 +59,7 @@ public class VelocityUtils {
velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】"); velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】");
velocityContext.put("ClassName", genTable.getClassName()); velocityContext.put("ClassName", genTable.getClassName());
velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName())); velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName()));
velocityContext.put("moduleName", genTable.getModuleName()); velocityContext.put("moduleName", StrUtil.toSymbolCase(genTable.getModuleName(), '-'));
velocityContext.put("BusinessName", StringUtils.capitalize(genTable.getBusinessName())); velocityContext.put("BusinessName", StringUtils.capitalize(genTable.getBusinessName()));
velocityContext.put("businessName", genTable.getBusinessName()); velocityContext.put("businessName", genTable.getBusinessName());
velocityContext.put("business_name", StrUtil.toUnderlineCase(genTable.getBusinessName())); velocityContext.put("business_name", StrUtil.toUnderlineCase(genTable.getBusinessName()));
@ -124,11 +125,12 @@ public class VelocityUtils {
templates.add("vm/java/serviceImpl.java.vm"); templates.add("vm/java/serviceImpl.java.vm");
templates.add("vm/java/controller.java.vm"); templates.add("vm/java/controller.java.vm");
templates.add("vm/xml/mapper.xml.vm"); templates.add("vm/xml/mapper.xml.vm");
if (DataBaseHelper.isOracle()) { DataBaseType dataBaseType = DataBaseHelper.getDataBaseType();
if (dataBaseType.isOracle()) {
templates.add("vm/sql/oracle/sql.vm"); templates.add("vm/sql/oracle/sql.vm");
} else if (DataBaseHelper.isPostgerSql()) { } else if (dataBaseType.isPostgreSql()) {
templates.add("vm/sql/postgres/sql.vm"); templates.add("vm/sql/postgres/sql.vm");
} else if (DataBaseHelper.isSqlServer()) { } else if (dataBaseType.isSqlServer()) {
templates.add("vm/sql/sqlserver/sql.vm"); templates.add("vm/sql/sqlserver/sql.vm");
} else { } else {
templates.add("vm/sql/sql.vm"); templates.add("vm/sql/sql.vm");
@ -163,7 +165,7 @@ public class VelocityUtils {
String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/"); String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/");
String mybatisPath = MYBATIS_PATH + "/" + moduleName; String mybatisPath = MYBATIS_PATH + "/" + moduleName;
String soybeanPath = "soy"; String soybeanPath = "soy";
String soybeanModuleName = StrUtil.toSymbolCase(moduleName, '-');
if (template.contains("domain.java.vm")) { if (template.contains("domain.java.vm")) {
fileName = StringUtils.format("{}/domain/{}.java", javaPath, className); fileName = StringUtils.format("{}/domain/{}.java", javaPath, className);
} }
@ -186,17 +188,17 @@ public class VelocityUtils {
} else if (template.contains("sql.vm")) { } else if (template.contains("sql.vm")) {
fileName = businessName + "Menu.sql"; fileName = businessName + "Menu.sql";
} else if (template.contains("index.vue.vm")) { } else if (template.contains("index.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-')); fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("index-tree.vue.vm")) { } else if (template.contains("index-tree.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-')); fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("api.d.ts.vm")) { } else if (template.contains("api.d.ts.vm")) {
fileName = StringUtils.format("{}/typings/api/{}.{}.api.d.ts", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-')); fileName = StringUtils.format("{}/typings/api/{}.{}.api.d.ts", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("api.ts.vm")) { } else if (template.contains("api.ts.vm")) {
fileName = StringUtils.format("{}/service/api/{}/{}.ts", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-')); fileName = StringUtils.format("{}/service/api/{}/{}.ts", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("search.vue.vm")) { } else if (template.contains("search.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-search.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-')); fileName = StringUtils.format("{}/views/{}/{}/modules/{}-search.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("operate-drawer.vue.vm")) { } else if (template.contains("operate-drawer.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-operate-drawer.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-')); fileName = StringUtils.format("{}/views/{}/{}/modules/{}-operate-drawer.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
} }
return fileName; return fileName;
} }

View File

@ -41,21 +41,18 @@
"scripts": { "scripts": {
"build": "vite build --mode prod", "build": "vite build --mode prod",
"build:dev": "vite build --mode dev", "build:dev": "vite build --mode dev",
"build:tauri": "pnpm tauri build",
"build:test": "vite build --mode test", "build:test": "vite build --mode test",
"cleanup": "sa cleanup", "cleanup": "sa cleanup",
"commit": "sa git-commit", "commit": "sa git-commit",
"commit:zh": "sa git-commit -l=zh-cn", "commit:zh": "sa git-commit -l=zh-cn",
"dev": "vite --mode dev", "dev": "vite --mode dev",
"dev:prod": "vite --mode prod", "dev:prod": "vite --mode prod",
"dev:tauri": "pnpm tauri dev",
"dev:test": "vite --mode test", "dev:test": "vite --mode test",
"gen-route": "sa gen-route", "gen-route": "sa gen-route",
"lint": "eslint . --fix", "lint": "eslint . --fix",
"prepare": "simple-git-hooks", "prepare": "simple-git-hooks",
"preview": "vite preview", "preview": "vite preview",
"release": "sa release", "release": "sa release",
"tauri-icon": "pnpm tauri icon ./public/logo.png",
"typecheck": "vue-tsc --noEmit --skipLibCheck", "typecheck": "vue-tsc --noEmit --skipLibCheck",
"update-pkg": "sa update-pkg" "update-pkg": "sa update-pkg"
}, },
@ -68,13 +65,12 @@
"@sa/materials": "workspace:*", "@sa/materials": "workspace:*",
"@sa/tinymce": "workspace:*", "@sa/tinymce": "workspace:*",
"@sa/utils": "workspace:*", "@sa/utils": "workspace:*",
"@tauri-apps/api": "2.5.0",
"@types/streamsaver": "^2.0.5", "@types/streamsaver": "^2.0.5",
"@vueuse/core": "13.5.0", "@vueuse/core": "13.8.0",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"dayjs": "1.11.13", "dayjs": "1.11.14",
"defu": "6.1.4", "defu": "6.1.4",
"echarts": "5.6.0", "echarts": "6.0.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"json5": "2.2.3", "json5": "2.2.3",
@ -84,47 +80,47 @@
"pinia": "3.0.3", "pinia": "3.0.3",
"streamsaver": "^2.0.6", "streamsaver": "^2.0.6",
"tailwind-merge": "3.3.1", "tailwind-merge": "3.3.1",
"vue": "3.5.17", "vue": "3.5.20",
"vue-advanced-cropper": "^2.8.9", "vue-advanced-cropper": "^2.8.9",
"vue-draggable-plus": "0.6.0", "vue-draggable-plus": "0.6.0",
"vue-i18n": "11.1.9", "vue-i18n": "11.1.11",
"vue-router": "4.5.1" "vue-router": "4.5.1",
"xlsx": "0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@elegant-router/vue": "0.3.8", "@elegant-router/vue": "0.3.8",
"@iconify/json": "2.2.357", "@iconify/json": "2.2.378",
"@sa/scripts": "workspace:*", "@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*", "@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.7.1", "@soybeanjs/eslint-config": "1.7.1",
"@tauri-apps/cli": "2.5.0", "@types/node": "24.3.0",
"@types/node": "24.0.13",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@unocss/eslint-config": "66.3.3", "@unocss/eslint-config": "66.4.2",
"@unocss/preset-icons": "66.3.3", "@unocss/preset-icons": "66.4.2",
"@unocss/preset-uno": "66.3.3", "@unocss/preset-uno": "66.4.2",
"@unocss/transformer-directives": "66.3.3", "@unocss/transformer-directives": "66.4.2",
"@unocss/transformer-variant-group": "66.3.3", "@unocss/transformer-variant-group": "66.4.2",
"@unocss/vite": "66.3.3", "@unocss/vite": "66.4.2",
"@vitejs/plugin-vue": "6.0.0", "@vitejs/plugin-vue": "6.0.1",
"@vitejs/plugin-vue-jsx": "5.0.1", "@vitejs/plugin-vue-jsx": "5.1.0",
"consola": "3.4.2", "consola": "3.4.2",
"eslint": "9.31.0", "eslint": "9.34.0",
"eslint-plugin-vue": "10.3.0", "eslint-plugin-vue": "10.4.0",
"kolorist": "1.8.0", "kolorist": "1.8.0",
"sass": "1.89.2", "sass": "1.91.0",
"simple-git-hooks": "2.13.0", "simple-git-hooks": "2.13.1",
"tsx": "4.20.3", "tsx": "4.20.5",
"typescript": "5.8.3", "typescript": "5.9.2",
"unplugin-icons": "22.1.0", "unplugin-icons": "22.2.0",
"unplugin-vue-components": "28.8.0", "unplugin-vue-components": "29.0.0",
"vite": "7.0.4", "vite": "7.1.3",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-progress": "0.0.7", "vite-plugin-progress": "0.0.7",
"vite-plugin-static-copy": "^3.1.0", "vite-plugin-static-copy": "^3.1.0",
"vite-plugin-svg-icons": "2.0.1", "vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "7.7.7", "vite-plugin-vue-devtools": "8.0.1",
"vue-eslint-parser": "10.2.0", "vue-eslint-parser": "10.2.0",
"vue-tsc": "3.0.1" "vue-tsc": "3.0.6"
}, },
"simple-git-hooks": { "simple-git-hooks": {
"commit-msg": "pnpm sa git-commit-verify", "commit-msg": "pnpm sa git-commit-verify",

View File

@ -11,7 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@sa/utils": "workspace:*", "@sa/utils": "workspace:*",
"axios": "1.10.0", "axios": "1.11.0",
"axios-retry": "4.5.0", "axios-retry": "4.5.0",
"qs": "6.14.0" "qs": "6.14.0"
}, },

View File

@ -13,11 +13,12 @@ import type {
ResponseType ResponseType
} from './type'; } from './type';
function createCommonRequest<ResponseData = any>( function createCommonRequest<
axiosConfig?: CreateAxiosDefaults, ResponseData,
options?: Partial<RequestOption<ResponseData>> ApiData = ResponseData,
) { State extends Record<string, unknown> = Record<string, unknown>
const opts = createDefaultOptions<ResponseData>(options); >(axiosConfig?: CreateAxiosDefaults, options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
const opts = createDefaultOptions<ResponseData, ApiData, State>(options);
const axiosConf = createAxiosConfig(axiosConfig); const axiosConf = createAxiosConfig(axiosConfig);
const instance = axios.create(axiosConf); const instance = axios.create(axiosConf);
@ -80,14 +81,6 @@ function createCommonRequest<ResponseData = any>(
} }
); );
function cancelRequest(requestId: string) {
const abortController = abortControllerMap.get(requestId);
if (abortController) {
abortController.abort();
abortControllerMap.delete(requestId);
}
}
function cancelAllRequest() { function cancelAllRequest() {
abortControllerMap.forEach(abortController => { abortControllerMap.forEach(abortController => {
abortController.abort(); abortController.abort();
@ -98,7 +91,6 @@ function createCommonRequest<ResponseData = any>(
return { return {
instance, instance,
opts, opts,
cancelRequest,
cancelAllRequest cancelAllRequest
}; };
} }
@ -109,27 +101,27 @@ function createCommonRequest<ResponseData = any>(
* @param axiosConfig axios config * @param axiosConfig axios config
* @param options request options * @param options request options
*/ */
export function createRequest<ResponseData = any, State = Record<string, unknown>>( export function createRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
axiosConfig?: CreateAxiosDefaults, axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>> options?: Partial<RequestOption<ResponseData, ApiData, State>>
) { ) {
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options); const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
const request: RequestInstance<State> = async function request<T = any, R extends ResponseType = 'json'>( const request: RequestInstance<ApiData, State> = async function request<
config: CustomAxiosRequestConfig T extends ApiData = ApiData,
) { R extends ResponseType = 'json'
>(config: CustomAxiosRequestConfig) {
const response: AxiosResponse<ResponseData> = await instance(config); const response: AxiosResponse<ResponseData> = await instance(config);
const responseType = response.config?.responseType || 'json'; const responseType = response.config?.responseType || 'json';
if (responseType === 'json') { if (responseType === 'json') {
return opts.transformBackendResponse(response); return opts.transform(response);
} }
return response.data as MappedType<R, T>; return response.data as MappedType<R, T>;
} as RequestInstance<State>; } as RequestInstance<ApiData, State>;
request.cancelRequest = cancelRequest;
request.cancelAllRequest = cancelAllRequest; request.cancelAllRequest = cancelAllRequest;
request.state = {} as State; request.state = {} as State;
@ -144,14 +136,14 @@ export function createRequest<ResponseData = any, State = Record<string, unknown
* @param axiosConfig axios config * @param axiosConfig axios config
* @param options request options * @param options request options
*/ */
export function createFlatRequest<ResponseData = any, State = Record<string, unknown>>( export function createFlatRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
axiosConfig?: CreateAxiosDefaults, axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>> options?: Partial<RequestOption<ResponseData, ApiData, State>>
) { ) {
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options); const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
const flatRequest: FlatRequestInstance<State, ResponseData> = async function flatRequest< const flatRequest: FlatRequestInstance<ResponseData, ApiData, State> = async function flatRequest<
T = any, T extends ApiData = ApiData,
R extends ResponseType = 'json' R extends ResponseType = 'json'
>(config: CustomAxiosRequestConfig) { >(config: CustomAxiosRequestConfig) {
try { try {
@ -160,20 +152,21 @@ export function createFlatRequest<ResponseData = any, State = Record<string, unk
const responseType = response.config?.responseType || 'json'; const responseType = response.config?.responseType || 'json';
if (responseType === 'json') { if (responseType === 'json') {
const data = opts.transformBackendResponse(response); const data = await opts.transform(response);
return { data, error: null, response }; return { data, error: null, response };
} }
return { data: response.data as MappedType<R, T>, error: null }; return { data: response.data as MappedType<R, T>, error: null, response };
} catch (error) { } catch (error) {
return { data: null, error, response: (error as AxiosError<ResponseData>).response }; return { data: null, error, response: (error as AxiosError<ResponseData>).response };
} }
} as FlatRequestInstance<State, ResponseData>; } as FlatRequestInstance<ResponseData, ApiData, State>;
flatRequest.cancelRequest = cancelRequest;
flatRequest.cancelAllRequest = cancelAllRequest; flatRequest.cancelAllRequest = cancelAllRequest;
flatRequest.state = {} as State; flatRequest.state = {
...opts.defaultState
} as State;
return flatRequest; return flatRequest;
} }

View File

@ -4,15 +4,27 @@ import { stringify } from 'qs';
import { isHttpSuccess } from './shared'; import { isHttpSuccess } from './shared';
import type { RequestOption } from './type'; import type { RequestOption } from './type';
export function createDefaultOptions<ResponseData = any>(options?: Partial<RequestOption<ResponseData>>) { export function createDefaultOptions<
const opts: RequestOption<ResponseData> = { 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, onRequest: async config => config,
isBackendSuccess: _response => true, isBackendSuccess: _response => true,
onBackendFail: async () => {}, onBackendFail: async () => {},
transformBackendResponse: async response => response.data,
onError: async () => {} onError: async () => {}
}; };
if (options?.transform) {
opts.transform = options.transform;
} else {
opts.transform = options?.transformBackendResponse || opts.transform;
}
Object.assign(opts, options); Object.assign(opts, options);
return opts; return opts;

View File

@ -8,7 +8,30 @@ export type ContentType =
| 'application/x-www-form-urlencoded' | 'application/x-www-form-urlencoded'
| 'application/octet-stream'; | 'application/octet-stream';
export interface RequestOption<ResponseData = any> { 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 * The hook before request
* *
@ -35,12 +58,6 @@ export interface RequestOption<ResponseData = any> {
response: AxiosResponse<ResponseData>, response: AxiosResponse<ResponseData>,
instance: AxiosInstance instance: AxiosInstance
) => Promise<AxiosResponse | null> | Promise<void>; ) => Promise<AxiosResponse | null> | Promise<void>;
/**
* transform backend response when the responseType is json
*
* @param response Axios response
*/
transformBackendResponse(response: AxiosResponse<ResponseData>): any | Promise<any>;
/** /**
* The hook to handle error * The hook to handle error
* *
@ -68,15 +85,7 @@ export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<Axi
responseType?: R; responseType?: R;
}; };
export interface RequestInstanceCommon<T> { export interface RequestInstanceCommon<State extends Record<string, unknown>> {
/**
* cancel the request by request id
*
* if the request provide abort controller sign from config, it will not collect in the abort controller map
*
* @param requestId
*/
cancelRequest: (requestId: string) => void;
/** /**
* cancel all request * cancel all request
* *
@ -84,32 +93,35 @@ export interface RequestInstanceCommon<T> {
*/ */
cancelAllRequest: () => void; cancelAllRequest: () => void;
/** you can set custom state in the request instance */ /** you can set custom state in the request instance */
state: T; state: State;
} }
/** The request instance */ /** The request instance */
export interface RequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> { export interface RequestInstance<ApiData, State extends Record<string, unknown>> extends RequestInstanceCommon<State> {
<T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>; <T extends ApiData = ApiData, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig<R>
): Promise<MappedType<R, T>>;
} }
export type FlatResponseSuccessData<T = any, ResponseData = any> = { export type FlatResponseSuccessData<ResponseData, ApiData> = {
data: T; data: ApiData;
error: null; error: null;
response: AxiosResponse<ResponseData>; response: AxiosResponse<ResponseData>;
}; };
export type FlatResponseFailData<ResponseData = any> = { export type FlatResponseFailData<ResponseData> = {
data: null; data: null;
error: AxiosError<ResponseData>; error: AxiosError<ResponseData>;
response: AxiosResponse<ResponseData>; response: AxiosResponse<ResponseData>;
}; };
export type FlatResponseData<T = any, ResponseData = any> = export type FlatResponseData<ResponseData, ApiData> =
| FlatResponseSuccessData<T, ResponseData> | FlatResponseSuccessData<ResponseData, ApiData>
| FlatResponseFailData<ResponseData>; | FlatResponseFailData<ResponseData>;
export interface FlatRequestInstance<S = Record<string, unknown>, ResponseData = any> extends RequestInstanceCommon<S> { export interface FlatRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
<T = any, R extends ResponseType = 'json'>( extends RequestInstanceCommon<State> {
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig<R> config: CustomAxiosRequestConfig<R>
): Promise<FlatResponseData<MappedType<R, T>, ResponseData>>; ): Promise<FlatResponseData<ResponseData, MappedType<R, T>>>;
} }

View File

@ -3,9 +3,7 @@ import useLoading from './use-loading';
import useCountDown from './use-count-down'; import useCountDown from './use-count-down';
import useContext from './use-context'; import useContext from './use-context';
import useSvgIconRender from './use-svg-icon-render'; import useSvgIconRender from './use-svg-icon-render';
import useHookTable from './use-table'; import useTable from './use-table';
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable }; export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useTable };
export type * from './use-table';
export * from './use-signal';
export * from './use-table';

View File

@ -1,5 +1,4 @@
import { inject, provide } from 'vue'; import { inject, provide } from 'vue';
import type { InjectionKey } from 'vue';
/** /**
* Use context * Use context
@ -12,7 +11,7 @@ import type { InjectionKey } from 'vue';
* import { ref } from 'vue'; * import { ref } from 'vue';
* import { useContext } from '@sa/hooks'; * import { useContext } from '@sa/hooks';
* *
* export const { setupStore, useStore } = useContext('demo', () => { * export const [provideDemoContext, useDemoContext] = useContext('demo', () => {
* const count = ref(0); * const count = ref(0);
* *
* function increment() { * function increment() {
@ -35,10 +34,10 @@ import type { InjectionKey } from 'vue';
* <div>A</div> * <div>A</div>
* </template> * </template>
* <script setup lang="ts"> * <script setup lang="ts">
* import { setupStore } from './context'; * import { provideDemoContext } from './context';
* *
* setupStore(); * provideDemoContext();
* // const { increment } = setupStore(); // also can control the store in the parent component * // const { increment } = provideDemoContext(); // also can control the store in the parent component
* </script> * </script>
* ``` // B.vue * ``` // B.vue
* ```vue * ```vue
@ -46,9 +45,9 @@ import type { InjectionKey } from 'vue';
* <div>B</div> * <div>B</div>
* </template> * </template>
* <script setup lang="ts"> * <script setup lang="ts">
* import { useStore } from './context'; * import { useDemoContext } from './context';
* *
* const { count, increment } = useStore(); * const { count, increment } = useDemoContext();
* </script> * </script>
* ```; * ```;
* *
@ -57,40 +56,41 @@ import type { InjectionKey } from 'vue';
* @param contextName Context name * @param contextName Context name
* @param fn Context function * @param fn Context function
*/ */
export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) { export default function useContext<Arguments extends Array<any>, T>(
type Context = ReturnType<T>; contextName: string,
composable: (...args: Arguments) => T
) {
const key = Symbol(contextName);
const { useProvide, useInject: useStore } = createContext<Context>(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);
function setupStore(...args: Parameters<T>) { if (consumerName && !value) {
const context: Context = fn(...args); throw new Error(`\`${consumerName}\` must be used within \`${contextName}\``);
return useProvide(context); }
}
return { // @ts-expect-error - we want to return null if the value is undefined or null
/** Setup store in the parent component */ return value || null;
setupStore,
/** Use store in the child component */
useStore
}; };
}
/** Create context */ const useProvide = (...args: Arguments) => {
function createContext<T>(contextName: string) { const value = composable(...args);
const injectKey: InjectionKey<T> = Symbol(contextName);
function useProvide(context: T) { provide(key, value);
provide(injectKey, context);
return context; return value;
}
function useInject() {
return inject(injectKey) as T;
}
return {
useProvide,
useInject
}; };
return [useProvide, useInject] as const;
} }

View File

@ -6,31 +6,31 @@ import type {
CreateAxiosDefaults, CreateAxiosDefaults,
CustomAxiosRequestConfig, CustomAxiosRequestConfig,
MappedType, MappedType,
RequestInstanceCommon,
RequestOption, RequestOption,
ResponseType ResponseType
} from '@sa/axios'; } from '@sa/axios';
import useLoading from './use-loading'; import useLoading from './use-loading';
export type HookRequestInstanceResponseSuccessData<T = any> = { export type HookRequestInstanceResponseSuccessData<ApiData> = {
data: Ref<T>; data: Ref<ApiData>;
error: Ref<null>; error: Ref<null>;
}; };
export type HookRequestInstanceResponseFailData<ResponseData = any> = { export type HookRequestInstanceResponseFailData<ResponseData> = {
data: Ref<null>; data: Ref<null>;
error: Ref<AxiosError<ResponseData>>; error: Ref<AxiosError<ResponseData>>;
}; };
export type HookRequestInstanceResponseData<T = any, ResponseData = any> = { export type HookRequestInstanceResponseData<ResponseData, ApiData> = {
loading: Ref<boolean>; loading: Ref<boolean>;
} & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<ResponseData>); } & (HookRequestInstanceResponseSuccessData<ApiData> | HookRequestInstanceResponseFailData<ResponseData>);
export interface HookRequestInstance<ResponseData = any> { export interface HookRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
<T = any, R extends ResponseType = 'json'>( extends RequestInstanceCommon<State> {
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig config: CustomAxiosRequestConfig
): HookRequestInstanceResponseData<MappedType<R, T>, ResponseData>; ): HookRequestInstanceResponseData<ResponseData, MappedType<R, T>>;
cancelRequest: (requestId: string) => void;
cancelAllRequest: () => void;
} }
/** /**
@ -39,25 +39,26 @@ export interface HookRequestInstance<ResponseData = any> {
* @param axiosConfig * @param axiosConfig
* @param options * @param options
*/ */
export default function createHookRequest<ResponseData = any>( export default function createHookRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
axiosConfig?: CreateAxiosDefaults, axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>> options?: Partial<RequestOption<ResponseData, ApiData, State>>
) { ) {
const request = createFlatRequest<ResponseData>(axiosConfig, options); const request = createFlatRequest<ResponseData, ApiData, State>(axiosConfig, options);
const hookRequest: HookRequestInstance<ResponseData> = function hookRequest<T = any, R extends ResponseType = 'json'>( const hookRequest: HookRequestInstance<ResponseData, ApiData, State> = function hookRequest<
config: CustomAxiosRequestConfig T extends ApiData = ApiData,
) { R extends ResponseType = 'json'
>(config: CustomAxiosRequestConfig) {
const { loading, startLoading, endLoading } = useLoading(); const { loading, startLoading, endLoading } = useLoading();
const data = ref<MappedType<R, T> | null>(null) as Ref<MappedType<R, T>>; const data = ref(null) as Ref<MappedType<R, T>>;
const error = ref<AxiosError<ResponseData> | null>(null) as Ref<AxiosError<ResponseData> | null>; const error = ref(null) as Ref<AxiosError<ResponseData> | null>;
startLoading(); startLoading();
request(config).then(res => { request(config).then(res => {
if (res.data) { if (res.data) {
data.value = res.data; data.value = res.data as MappedType<R, T>;
} else { } else {
error.value = res.error; error.value = res.error;
} }
@ -70,9 +71,8 @@ export default function createHookRequest<ResponseData = any>(
data, data,
error error
}; };
} as HookRequestInstance<ResponseData>; } as HookRequestInstance<ResponseData, ApiData, State>;
hookRequest.cancelRequest = request.cancelRequest;
hookRequest.cancelAllRequest = request.cancelAllRequest; hookRequest.cancelAllRequest = request.cancelAllRequest;
return hookRequest; return hookRequest;

View File

@ -1,144 +0,0 @@
import { computed, ref, shallowRef, triggerRef } from 'vue';
import type {
ComputedGetter,
DebuggerOptions,
Ref,
ShallowRef,
WritableComputedOptions,
WritableComputedRef
} from 'vue';
type Updater<T> = (value: T) => T;
type Mutator<T> = (value: T) => void;
/**
* Signal is a reactive value that can be set, updated or mutated
*
* @example
* ```ts
* const count = useSignal(0);
*
* // `watchEffect`
* watchEffect(() => {
* console.log(count());
* });
*
* // watch
* watch(count, value => {
* console.log(value);
* });
*
* // useComputed
* const double = useComputed(() => count() * 2);
* const writeableDouble = useComputed({
* get: () => count() * 2,
* set: value => count.set(value / 2)
* });
* ```
*/
export interface Signal<T> {
(): Readonly<T>;
/**
* Set the value of the signal
*
* It recommend use `set` for primitive values
*
* @param value
*/
set(value: T): void;
/**
* Update the value of the signal using an updater function
*
* It recommend use `update` for non-primitive values, only the first level of the object will be reactive.
*
* @param updater
*/
update(updater: Updater<T>): void;
/**
* Mutate the value of the signal using a mutator function
*
* this action will call `triggerRef`, so the value will be tracked on `watchEffect`.
*
* It recommend use `mutate` for non-primitive values, all levels of the object will be reactive.
*
* @param mutator
*/
mutate(mutator: Mutator<T>): void;
/**
* Get the reference of the signal
*
* Sometimes it can be useful to make `v-model` work with the signal
*
* ```vue
* <template>
* <input v-model="model.count" />
* </template>;
*
* <script setup lang="ts">
* const state = useSignal({ count: 0 }, { useRef: true });
*
* const model = state.getRef();
* </script>
* ```
*/
getRef(): Readonly<ShallowRef<Readonly<T>>>;
}
export interface ReadonlySignal<T> {
(): Readonly<T>;
}
export interface SignalOptions {
/**
* Whether to use `ref` to store the value
*
* @default false use `sharedRef` to store the value
*/
useRef?: boolean;
}
export function useSignal<T>(initialValue: T, options?: SignalOptions): Signal<T> {
const { useRef } = options || {};
const state = useRef ? (ref(initialValue) as Ref<T>) : shallowRef(initialValue);
return createSignal(state);
}
export function useComputed<T>(getter: ComputedGetter<T>, debugOptions?: DebuggerOptions): ReadonlySignal<T>;
export function useComputed<T>(options: WritableComputedOptions<T>, debugOptions?: DebuggerOptions): Signal<T>;
export function useComputed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions
) {
const isGetter = typeof getterOrOptions === 'function';
const computedValue = computed(getterOrOptions as any, debugOptions);
if (isGetter) {
return () => computedValue.value as ReadonlySignal<T>;
}
return createSignal(computedValue);
}
function createSignal<T>(state: ShallowRef<T> | WritableComputedRef<T>): Signal<T> {
const signal = () => state.value;
signal.set = (value: T) => {
state.value = value;
};
signal.update = (updater: Updater<T>) => {
state.value = updater(state.value);
};
signal.mutate = (mutator: Mutator<T>) => {
mutator(state.value);
triggerRef(state);
};
signal.getRef = () => state as Readonly<ShallowRef<Readonly<T>>>;
return signal;
}

View File

@ -1,12 +1,20 @@
import { computed, reactive, ref } from 'vue'; import { computed, ref } from 'vue';
import type { Ref, VNodeChild } from 'vue'; import type { Ref, VNodeChild } from 'vue';
import { jsonClone } from '@sa/utils';
import useBoolean from './use-boolean'; import useBoolean from './use-boolean';
import useLoading from './use-loading'; import useLoading from './use-loading';
export type MaybePromise<T> = T | Promise<T>; export interface PaginationData<T> {
data: T[];
pageNum: number;
pageSize: number;
total: number;
}
export type ApiFn = (args: any) => Promise<unknown>; 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 TableColumnCheckTitle = string | ((...args: any) => VNodeChild);
@ -14,74 +22,64 @@ export type TableColumnCheck = {
key: string; key: string;
title: TableColumnCheckTitle; title: TableColumnCheckTitle;
checked: boolean; checked: boolean;
visible: boolean;
}; };
export type TableDataWithIndex<T> = T & { index: number }; export interface UseTableOptions<ResponseData, ApiData, Column, Pagination extends boolean> {
/**
export type TransformedData<T> = { * api function to get table data
data: TableDataWithIndex<T>[]; */
total?: number; api: () => Promise<ResponseData>;
}; /**
* whether to enable pagination
export type Transformer<T, Response> = (response: Response) => TransformedData<T>; */
pagination?: Pagination;
export type TableConfig<A extends ApiFn, T, C> = { /**
/** api function to get table data */ * transform api response to table data
apiFn: A; */
/** api params */ transform: Transform<ResponseData, ApiData, Pagination>;
apiParams?: Parameters<A>[0]; /**
/** transform api response to table data */ * columns factory
transformer: Transformer<T, Awaited<ReturnType<A>>>; */
/** columns factory */ columns: () => Column[];
columns: () => C[];
/** /**
* get column checks * get column checks
*
* @param columns
*/ */
getColumnChecks: (columns: C[]) => TableColumnCheck[]; getColumnChecks: (columns: Column[]) => TableColumnCheck[];
/** /**
* get columns * get columns
*
* @param columns
*/ */
getColumns: (columns: C[], checks: TableColumnCheck[]) => C[]; getColumns: (columns: Column[], checks: TableColumnCheck[]) => Column[];
/** /**
* callback when response fetched * callback when response fetched
*
* @param transformed transformed data
*/ */
onFetched?: (transformed: TransformedData<T>) => MaybePromise<void>; onFetched?: (data: GetApiData<ApiData, Pagination>) => void | Promise<void>;
/** /**
* whether to get data immediately * whether to get data immediately
* *
* @default true * @default true
*/ */
immediate?: boolean; immediate?: boolean;
}; }
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) { export default function useTable<ResponseData, ApiData, Column, Pagination extends boolean>(
options: UseTableOptions<ResponseData, ApiData, Column, Pagination>
) {
const { loading, startLoading, endLoading } = useLoading(); const { loading, startLoading, endLoading } = useLoading();
const { bool: empty, setBool: setEmpty } = useBoolean(); const { bool: empty, setBool: setEmpty } = useBoolean();
const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config; const { api, pagination, transform, columns, getColumnChecks, getColumns, onFetched, immediate = true } = options;
const searchParams: NonNullable<Parameters<A>[0]> = reactive(jsonClone({ ...apiParams })); const data = ref([]) as Ref<ApiData[]>;
const allColumns = ref(config.columns()) as Ref<C[]>; const columnChecks = ref(getColumnChecks(columns())) as Ref<TableColumnCheck[]>;
const data: Ref<TableDataWithIndex<T>[]> = ref([]); const $columns = computed(() => getColumns(columns(), columnChecks.value));
const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
const columns = computed(() => getColumns(allColumns.value, columnChecks.value));
function reloadColumns() { function reloadColumns() {
allColumns.value = config.columns();
const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked])); const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
const defaultChecks = getColumnChecks(allColumns.value); const defaultChecks = getColumnChecks(columns());
columnChecks.value = defaultChecks.map(col => ({ columnChecks.value = defaultChecks.map(col => ({
...col, ...col,
@ -90,48 +88,21 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
} }
async function getData() { async function getData() {
startLoading(); try {
startLoading();
const formattedParams = formatSearchParams(searchParams); const response = await api();
const response = await apiFn(formattedParams); const transformed = transform(response);
const transformed = transformer(response as Awaited<ReturnType<A>>); data.value = getTableData(transformed, pagination);
data.value = transformed.data; setEmpty(data.value.length === 0);
setEmpty(transformed.data.length === 0); await onFetched?.(transformed);
} finally {
await config.onFetched?.(transformed); endLoading();
}
endLoading();
}
function formatSearchParams(params: Record<string, unknown>) {
const formattedParams: Record<string, unknown> = {};
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
formattedParams[key] = value;
}
});
return formattedParams;
}
/**
* update search params
*
* @param params
*/
function updateSearchParams(params: Partial<Parameters<A>[0]>) {
Object.assign(searchParams, params);
}
/** reset search params */
function resetSearchParams() {
Object.assign(searchParams, jsonClone(apiParams));
getData();
} }
if (immediate) { if (immediate) {
@ -142,12 +113,20 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
loading, loading,
empty, empty,
data, data,
columns, columns: $columns,
columnChecks, columnChecks,
reloadColumns, reloadColumns,
getData, getData
searchParams,
updateSearchParams,
resetSearchParams
}; };
} }
function getTableData<ApiData, Pagination extends boolean>(
data: GetApiData<ApiData, Pagination>,
pagination?: Pagination
) {
if (pagination) {
return (data as PaginationData<ApiData>).data;
}
return data as ApiData[];
}

View File

@ -127,7 +127,6 @@ function handleClickMask() {
:class="[ :class="[
style['layout-header'], style['layout-header'],
commonClass, commonClass,
headerClass,
headerLeftGapClass, headerLeftGapClass,
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab } { 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
]" ]"

View File

@ -6,12 +6,6 @@ interface AdminLayoutHeaderConfig {
* @default true * @default true
*/ */
headerVisible?: boolean; headerVisible?: boolean;
/**
* Header class
*
* @default ''
*/
headerClass?: string;
/** /**
* Header height * Header height
* *

View File

@ -1,15 +0,0 @@
{
"name": "@sa/fetch",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"ofetch": "1.4.1"
}
}

View File

@ -1,10 +0,0 @@
import { ofetch } from 'ofetch';
import type { FetchOptions } from 'ofetch';
export function createRequest(options: FetchOptions) {
const request = ofetch.create(options);
return request;
}
export default createRequest;

View File

@ -1,20 +0,0 @@
{
"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"]
}

View File

@ -14,14 +14,15 @@
}, },
"devDependencies": { "devDependencies": {
"@soybeanjs/changelog": "0.3.24", "@soybeanjs/changelog": "0.3.24",
"bumpp": "10.2.0", "bumpp": "10.2.3",
"c12": "3.0.4", "c12": "3.2.0",
"cac": "6.7.14", "cac": "6.7.14",
"consola": "3.4.2", "consola": "3.4.2",
"enquirer": "2.4.1", "enquirer": "2.4.1",
"execa": "9.6.0", "execa": "9.6.0",
"kolorist": "1.8.0", "kolorist": "1.8.0",
"npm-check-updates": "18.0.1", "npm-check-updates": "18.0.3",
"picomatch": "4.0.3",
"rimraf": "6.0.1" "rimraf": "6.0.1"
} }
} }

View File

@ -32,7 +32,8 @@ export function createStorage<T extends object>(type: StorageType, storagePrefix
storageData = JSON.parse(json); storageData = JSON.parse(json);
} catch {} } catch {}
if (storageData) { // storageData may be `false` if it is boolean type
if (storageData !== null) {
return storageData as T[K]; return storageData as T[K];
} }
} }

2657
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

View File

@ -1,3 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/

4580
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +0,0 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
default-run = "app"
edition = "2021"
rust-version = "1.60"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "2", features = [] }
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
# DO NOT REMOVE!!
custom-protocol = [ "tauri/custom-protocol" ]

View File

@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

View File

@ -1,7 +0,0 @@
{
"identifier": "migrated",
"description": "permissions that were migrated from v1",
"local": true,
"windows": ["main"],
"permissions": ["core:default"]
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
{
"migrated": {
"identifier": "migrated",
"description": "permissions that were migrated from v1",
"local": true,
"windows": ["main"],
"permissions": ["core:default"]
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,8 +0,0 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -1,57 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm run dev",
"frontendDist": "../dist",
"devUrl": "http://localhost:9527"
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "RuoYi-Plus-Soybean",
"targets": "all",
"externalBin": [],
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
},
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"linux": {
"deb": {
"depends": []
}
}
},
"productName": "RuoYi-Plus-Soybean",
"mainBinaryName": "RuoYi-Plus-Soybean",
"version": "1.0.0",
"identifier": "org.dromara.admin",
"plugins": {},
"app": {
"windows": [
{
"fullscreen": false,
"height": 768,
"resizable": true,
"title": "RuoYi-Plus-Soybean",
"width": 1366,
"useHttpsScheme": true
}
],
"security": {
"csp": null
}
}
}

View File

@ -4,7 +4,6 @@ import { NConfigProvider, darkTheme } from 'naive-ui';
import type { WatermarkProps } from 'naive-ui'; import type { WatermarkProps } from 'naive-ui';
import { useAppStore } from './store/modules/app'; import { useAppStore } from './store/modules/app';
import { useThemeStore } from './store/modules/theme'; import { useThemeStore } from './store/modules/theme';
import { useAuthStore } from './store/modules/auth';
import { naiveDateLocales, naiveLocales } from './locales/naive'; import { naiveDateLocales, naiveLocales } from './locales/naive';
defineOptions({ defineOptions({
@ -13,7 +12,6 @@ defineOptions({
const appStore = useAppStore(); const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const { userInfo } = useAuthStore();
const naiveDarkTheme = computed(() => (themeStore.darkMode ? darkTheme : undefined)); const naiveDarkTheme = computed(() => (themeStore.darkMode ? darkTheme : undefined));
@ -26,24 +24,19 @@ const naiveDateLocale = computed(() => {
}); });
const watermarkProps = computed<WatermarkProps>(() => { const watermarkProps = computed<WatermarkProps>(() => {
const appTitle = import.meta.env.VITE_APP_TITLE || 'RuoYi-Vue-Plus';
const content =
themeStore.watermark.enableUserName && userInfo.user?.userName
? `${userInfo.user?.nickName}@${appTitle} ${userInfo.user?.userName}`
: appTitle;
return { return {
content, content: themeStore.watermarkContent,
cross: true, cross: true,
fullscreen: true, fullscreen: true,
fontSize: 14, fontSize: 14,
fontColor: themeStore.darkMode ? 'rgba(200, 200, 200, 0.03)' : 'rgba(200, 200, 200, 0.2)',
lineHeight: 14, lineHeight: 14,
width: 200, width: 384,
height: 300, height: 384,
xOffset: 12, xOffset: 12,
yOffset: 60, yOffset: 60,
rotate: -18, rotate: -13,
zIndex: 9999 zIndex: 9999,
fontColor: themeStore.darkMode ? 'rgba(200, 200, 200, 0.03)' : 'rgba(200, 200, 200, 0.2)'
}; };
}); });
</script> </script>

View File

@ -22,7 +22,12 @@ const columns = defineModel<NaiveUI.TableColumnCheck[]>('columns', {
</NButton> </NButton>
</template> </template>
<VueDraggable v-model="columns" :animation="150" filter=".none_draggable"> <VueDraggable v-model="columns" :animation="150" filter=".none_draggable">
<div v-for="item in columns" :key="item.key" class="h-36px flex-y-center rd-4px hover:(bg-primary bg-opacity-20)"> <div
v-for="item in columns"
:key="item.key"
class="h-36px flex-y-center rd-4px hover:(bg-primary bg-opacity-20)"
:class="{ hidden: !item.visible }"
>
<icon-mdi-drag class="mr-8px h-full cursor-move text-icon" /> <icon-mdi-drag class="mr-8px h-full cursor-move text-icon" />
<NCheckbox v-model:checked="item.checked" class="none_draggable flex-1"> <NCheckbox v-model:checked="item.checked" class="none_draggable flex-1">
<template v-if="typeof item.title === 'function'"> <template v-if="typeof item.title === 'function'">

View File

@ -0,0 +1,42 @@
<script lang="ts" setup>
import { computed, useSlots } from 'vue';
import type { PopoverPlacement } from 'naive-ui';
defineOptions({ name: 'IconTooltip' });
interface Props {
icon?: string;
localIcon?: string;
desc?: string;
placement?: PopoverPlacement;
}
const props = withDefaults(defineProps<Props>(), {
icon: 'mdi-help-circle',
localIcon: '',
desc: '',
placement: 'top'
});
const slots = useSlots();
const hasCustomTrigger = computed(() => Boolean(slots.trigger));
if (!hasCustomTrigger.value && !props.icon && !props.localIcon) {
throw new Error('icon or localIcon is required when no custom trigger slot is provided');
}
</script>
<template>
<NTooltip :placement="placement">
<template #trigger>
<slot name="trigger">
<div class="cursor-pointer">
<SvgIcon :icon="icon" :local-icon="localIcon" />
</div>
</slot>
</template>
<slot>
<span>{{ desc }}</span>
</slot>
</NTooltip>
</template>

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, useAttrs } from 'vue'; import { computed, useAttrs } from 'vue';
import type { TagProps } from 'naive-ui'; import type { TagProps } from 'naive-ui';
import { jsonClone } from '@sa/utils';
import { useDict } from '@/hooks/business/dict'; import { useDict } from '@/hooks/business/dict';
import { isNotNull } from '@/utils/common'; import { isNotNull } from '@/utils/common';
import { $t } from '@/locales'; import { $t } from '@/locales';
@ -28,7 +29,7 @@ const { transformDictData } = useDict(props.dictCode, props.immediate);
const dictTagData = computed<Api.System.DictData[]>(() => { const dictTagData = computed<Api.System.DictData[]>(() => {
if (props.dictData) { if (props.dictData) {
const dictData = props.dictData; const dictData = jsonClone(props.dictData);
if (dictData.dictLabel?.startsWith(`dict.${dictData.dictType}.`)) { if (dictData.dictLabel?.startsWith(`dict.${dictData.dictType}.`)) {
dictData.dictLabel = $t(dictData.dictLabel as App.I18n.I18nKey); dictData.dictLabel = $t(dictData.dictLabel as App.I18n.I18nKey);
} }

View File

@ -2,6 +2,7 @@
import { computed } from 'vue'; import { computed } from 'vue';
import hljs from 'highlight.js/lib/core'; import hljs from 'highlight.js/lib/core';
import json from 'highlight.js/lib/languages/json'; import json from 'highlight.js/lib/languages/json';
import { twMerge } from 'tailwind-merge';
hljs.registerLanguage('json', json); hljs.registerLanguage('json', json);
@ -10,15 +11,19 @@ defineOptions({
}); });
interface Props { interface Props {
class?: string;
code?: string; code?: string;
showLineNumbers?: boolean; showLineNumbers?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
class: '',
code: '', code: '',
showLineNumbers: false showLineNumbers: false
}); });
const DEFAULT_CLASS = 'max-h-500px';
/** 格式化JSON数据 */ /** 格式化JSON数据 */
const jsonData = computed<string>(() => { const jsonData = computed<string>(() => {
if (!props.code) return ''; if (!props.code) return '';
@ -33,9 +38,9 @@ const jsonData = computed<string>(() => {
</script> </script>
<template> <template>
<div class="json-preview"> <NScrollbar :class="twMerge(DEFAULT_CLASS, props.class)">
<NCode :code="jsonData" :hljs="hljs" language="json" :show-line-numbers="showLineNumbers" /> <NCode :code="jsonData" :hljs="hljs" language="json" :show-line-numbers="showLineNumbers" :word-wrap="true" />
</div> </NScrollbar>
</template> </template>
<style lang="scss"> <style lang="scss">
@ -44,18 +49,4 @@ html[class='dark'] {
background-color: #7c7777; background-color: #7c7777;
} }
} }
.json-preview {
width: 100%;
max-height: 500px;
overflow-y: auto;
@include scrollbar();
.empty-data {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #999;
font-size: 14px;
}
}
</style> </style>

View File

@ -1,61 +1,524 @@
<!-- Copyright By https://github.com/Daymychen/art-design-pro/blob/main/src/components/core/views/login/LoginLeftView.vue -->
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { useThemeStore } from '@/store/modules/theme';
import { getPaletteColorByNumber } from '@sa/color';
defineOptions({ name: 'WaveBg' }); defineOptions({ name: 'WaveBg' });
interface Props { const themeStore = useThemeStore();
/** Theme color */
themeColor: string; function toggleThemeScheme() {
if (themeStore.darkMode) {
themeStore.setThemeScheme('light');
return;
}
themeStore.setThemeScheme('dark');
} }
const props = defineProps<Props>();
const lightColor = computed(() => getPaletteColorByNumber(props.themeColor, 200));
const darkColor = computed(() => getPaletteColorByNumber(props.themeColor, 500));
</script> </script>
<template> <template>
<div class="absolute-lt z-1 size-full overflow-hidden"> <div class="wave-bg">
<div class="absolute -right-300px -top-900px lt-sm:(-right-100px -top-1170px)"> <!-- 几何装饰元素 -->
<svg height="1337" width="1337"> <div class="geometric-decorations">
<defs> <!-- 基础几何形状 -->
<path <div class="geo-element circle-outline animate-fade-in-up animate-delay-0s"></div>
id="path-1" <div class="geo-element square-rotated animate-fade-in-left animate-delay-0s"></div>
opacity="1" <div class="geo-element circle-small animate-fade-in-up animate-delay-0.3s"></div>
fill-rule="evenodd"
d="M1337,668.5 C1337,1037.455193874239 1037.455193874239,1337 668.5,1337 C523.6725684305388,1337 337,1236 370.50000000000006,1094 C434.03835568300906,824.6732385973953 6.906089672974592e-14,892.6277623047779 0,668.5000000000001 C0,299.5448061257611 299.5448061257609,1.1368683772161603e-13 668.4999999999999,0 C1037.455193874239,0 1337,299.544806125761 1337,668.5Z" <div class="geo-element square-bottom-right animate-fade-in-right animate-delay-0s"></div>
/>
<linearGradient id="linearGradient-2" x1="0.79" y1="0.62" x2="0.21" y2="0.86"> <!-- 背景泡泡 -->
<stop offset="0" :stop-color="lightColor" stop-opacity="1" /> <div class="geo-element bg-bubble animate-scale-in animate-delay-0.5s"></div>
<stop offset="1" :stop-color="darkColor" stop-opacity="1" />
</linearGradient> <!-- 太阳/月亮 -->
</defs> <div
<g opacity="1"> class="geo-element circle-top-right animate-fade-in-down animate-delay-0.5s"
<use xlink:href="#path-1" fill="url(#linearGradient-2)" fill-opacity="1" /> @click="toggleThemeScheme"
</g> ></div>
</svg>
</div> <!-- 装饰点 -->
<div class="absolute -bottom-400px -left-200px lt-sm:(-bottom-760px -left-100px)"> <div class="geo-element dot dot-top-left animate-bounce-in animate-delay-0s"></div>
<svg height="896" width="967.8852157128662"> <div class="geo-element dot dot-top-right animate-bounce-in animate-delay-0s"></div>
<defs> <div class="geo-element dot dot-center-right animate-bounce-in animate-delay-0s"></div>
<path
id="path-2" <!-- 叠加方块组 -->
opacity="1" <div class="squares-group">
fill-rule="evenodd" <i class="geo-element square square-blue animate-fade-in-left-rotated-blue animate-delay-0.2s"></i>
d="M896,448 C1142.6325445712241,465.5747656464056 695.2579309733121,896 448,896 C200.74206902668806,896 5.684341886080802e-14,695.2579309733121 0,448.0000000000001 C0,200.74206902668806 200.74206902668791,5.684341886080802e-14 447.99999999999994,0 C695.2579309733121,0 475,418 896,448Z" <i class="geo-element square square-pink animate-fade-in-left-rotated-pink animate-delay-0.4s"></i>
/> <i class="geo-element square square-purple animate-fade-in-left-no-rotation animate-delay-0.6s"></i>
<linearGradient id="linearGradient-3" x1="0.5" y1="0" x2="0.5" y2="1"> </div>
<stop offset="0" :stop-color="darkColor" stop-opacity="1" />
<stop offset="1" :stop-color="lightColor" stop-opacity="1" />
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-2" fill="url(#linearGradient-3)" fill-opacity="1" />
</g>
</svg>
</div> </div>
</div> </div>
</template> </template>
<style scoped></style> <style lang="scss" scoped>
// 颜色变量定义
$primary-light-7: rgb(var(--primary-50-color));
$primary-light-8: rgb(var(--primary-100-color));
$primary-light-9: rgb(var(--primary-200-color));
$primary-base: rgb(var(--primary-color));
$main-bg: rgb(var(--primary-50-color));
// 混合颜色函数
$bg-mix-light-9: color-mix(in srgb, $primary-light-9 100%, $main-bg);
$bg-mix-light-8: color-mix(in srgb, $primary-light-8 80%, $main-bg);
$bg-mix-light-7: color-mix(in srgb, $primary-light-7 80%, $main-bg);
.wave-bg {
.geometric-decorations {
.geo-element {
position: absolute;
opacity: 0;
animation-fill-mode: forwards;
animation-duration: 0.8s;
animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
// 动画 mixin
@mixin fadeAnimation($direction: '', $rotation: 0deg) {
from {
opacity: 0;
@if $direction == 'up' {
transform: translateY(30px) rotate($rotation);
} @else if $direction == 'down' {
transform: translateY(-30px) rotate($rotation);
} @else if $direction == 'left' {
transform: translateX(-30px) rotate($rotation);
} @else if $direction == 'right' {
transform: translateX(30px) rotate($rotation);
}
}
to {
opacity: 1;
@if $direction == 'up' or $direction == 'down' {
transform: translateY(0) rotate($rotation);
} @else {
transform: translateX(0) rotate($rotation);
}
}
}
// 动画定义
@keyframes fadeInUp {
@include fadeAnimation('up');
}
@keyframes fadeInDown {
@include fadeAnimation('down');
}
@keyframes fadeInLeft {
@include fadeAnimation('left');
}
@keyframes fadeInLeftRotated {
@include fadeAnimation('left', -25deg);
}
@keyframes fadeInRight {
@include fadeAnimation('right');
}
@keyframes fadeInRightRotated {
@include fadeAnimation('right', 45deg);
}
@keyframes fadeInLeftRotatedBlue {
@include fadeAnimation('left', -10deg);
}
@keyframes fadeInLeftRotatedPink {
@include fadeAnimation('left', 10deg);
}
@keyframes fadeInLeftNoRotation {
@include fadeAnimation('left');
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(0.3);
}
50% {
opacity: 1;
transform: scale(1.05);
}
70% {
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes lineGrow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
// 动画类
.animate-fade-in-up {
animation-name: fadeInUp;
}
.animate-fade-in-down {
animation-name: fadeInDown;
}
.animate-fade-in-left {
animation-name: fadeInLeft;
}
.animate-fade-in-right {
animation-name: fadeInRight;
}
.animate-scale-in {
animation-name: scaleIn;
animation-duration: 1.2s;
}
.animate-bounce-in {
animation-name: bounceIn;
animation-duration: 0.6s;
}
.animate-fade-in-left-rotated-blue {
animation-name: fadeInLeftRotatedBlue;
}
.animate-fade-in-left-rotated-pink {
animation-name: fadeInLeftRotatedPink;
}
.animate-fade-in-left-no-rotation {
animation-name: fadeInLeftNoRotation;
}
// 基础几何形状
.circle-outline {
top: 10%;
left: 25%;
width: 42px;
height: 42px;
border: 2px solid $primary-light-8;
border-radius: 50%;
}
.square-rotated {
top: 50%;
left: 16%;
width: 60px;
height: 60px;
background-color: $bg-mix-light-8;
&.animate-fade-in-left {
animation-name: fadeInLeftRotated;
}
}
.circle-small {
bottom: 26%;
left: 30%;
width: 18px;
height: 18px;
background-color: $primary-light-8;
border-radius: 50%;
}
// 太阳/月亮效果
.circle-top-right {
top: 3%;
right: 3%;
z-index: 100;
width: 50px;
height: 50px;
cursor: pointer;
background: $bg-mix-light-7;
border-radius: 50%;
transition: all 0.3s;
&::after {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
content: '';
background: linear-gradient(to right, #fcbb04, #fffc00);
border-radius: 50%;
opacity: 0;
transition: all 0.5s;
transform: translate(-50%, -50%);
}
&:hover {
box-shadow: 0 0 36px #fffc00;
&::after {
opacity: 1;
}
}
}
.square-bottom-right {
right: 10%;
bottom: 10%;
width: 50px;
height: 50px;
background-color: $primary-light-8;
&.animate-fade-in-right {
animation-name: fadeInRightRotated;
}
}
// 背景泡泡
.bg-bubble {
top: -120px;
right: -120px;
width: 360px;
height: 360px;
background-color: $bg-mix-light-8;
border-radius: 50%;
}
// 装饰点
.dot {
width: 14px;
height: 14px;
background-color: $primary-light-7;
border-radius: 50%;
&.dot-top-left {
top: 140px;
left: 100px;
}
&.dot-top-right {
top: 140px;
right: 120px;
}
&.dot-center-right {
top: 46%;
right: 22%;
background-color: $primary-light-8;
}
}
// 叠加方块组
.squares-group {
position: absolute;
bottom: 18px;
left: 20px;
width: 140px;
height: 140px;
pointer-events: none;
.square {
position: absolute;
display: block;
border-radius: 8px;
box-shadow: 0 8px 24px rgb(64 87 167 / 12%);
&.square-blue {
top: 12px;
left: 30px;
z-index: 2;
width: 50px;
height: 50px;
background-color: rgb(from $primary-base r g b / 30%);
}
&.square-pink {
top: 30px;
left: 48px;
z-index: 1;
width: 70px;
height: 70px;
background-color: rgb(from $primary-base r g b / 15%);
}
&.square-purple {
top: 66px;
left: 86px;
z-index: 3;
width: 32px;
height: 32px;
background-color: rgb(from $primary-base r g b / 45%);
}
}
// 装饰线条
&::after {
position: absolute;
top: 86px;
left: 72px;
width: 80px;
height: 1px;
content: '';
background: linear-gradient(90deg, var(--el-color-primary-light-6), transparent);
opacity: 0;
transform: rotate(50deg);
animation: lineGrow 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-delay: 1.2s;
}
}
}
@media only screen and (max-width: 1600px) {
width: 60vw;
.text-wrap {
bottom: 40px;
}
}
@media only screen and (max-width: 1280px) {
width: auto;
height: auto;
padding: 0;
// 隐藏背景和其他内容,只保留 logo
background: transparent;
.left-img,
.text-wrap,
.geometric-decorations {
display: none;
}
.logo {
position: fixed;
top: 15px;
left: 25px;
z-index: 1000;
}
}
}
// 暗色主题
.dark .wave-bg {
background-color: color-mix(in srgb, $primary-light-9 60%, #070707);
@media only screen and (max-width: 1280px) {
background: transparent;
}
.geometric-decorations {
// 月亮效果
.circle-top-right {
background-color: $bg-mix-light-8;
box-shadow: 0 0 25px #333 inset;
transition: all 0.3s ease-in-out 0.1s;
rotate: -48deg;
&::before {
position: absolute;
top: 0;
left: 15px;
width: 50px;
height: 50px;
content: '';
background-color: $bg-mix-light-9;
border-radius: 50%;
transition: all 0.3s ease-in-out;
}
&:hover {
background-color: transparent;
box-shadow: 0 40px 25px #ddd inset;
&::before {
left: 18px;
}
&::after {
opacity: 0;
}
}
}
.bg-bubble {
background-color: $bg-mix-light-9;
}
// 其他元素颜色调整
.square-rotated {
background-color: $bg-mix-light-9;
}
.circle-small,
.dot {
background-color: $primary-light-8;
}
.square-bottom-right {
background-color: $primary-light-9;
}
.dot.dot-top-right {
background-color: $primary-light-8;
}
}
// 方块组暗色调整
.squares-group {
.square {
box-shadow: none;
&.square-blue {
background-color: rgb(from $primary-base r g b / 18%);
}
&.square-pink {
background-color: rgb(from $primary-base r g b / 10%);
}
&.square-purple {
background-color: rgb(from $primary-base r g b / 20%);
}
}
&::after {
background: linear-gradient(90deg, $primary-light-8, transparent);
}
}
}
</style>

View File

@ -5,9 +5,9 @@ export const GLOBAL_HEADER_MENU_ID = '__GLOBAL_HEADER_MENU__';
export const GLOBAL_SIDER_MENU_ID = '__GLOBAL_SIDER_MENU__'; export const GLOBAL_SIDER_MENU_ID = '__GLOBAL_SIDER_MENU__';
export const themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = { export const themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = {
light: 'theme.themeSchema.light', light: 'theme.appearance.themeSchema.light',
dark: 'theme.themeSchema.dark', dark: 'theme.appearance.themeSchema.dark',
auto: 'theme.themeSchema.auto' auto: 'theme.appearance.themeSchema.auto'
}; };
export const themeSchemaOptions = transformRecordToOption(themeSchemaRecord); export const themeSchemaOptions = transformRecordToOption(themeSchemaRecord);
@ -21,49 +21,61 @@ export const loginModuleRecord: Record<UnionKey.LoginModule, App.I18n.I18nKey> =
}; };
export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = { export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = {
vertical: 'theme.layoutMode.vertical', vertical: 'theme.layout.layoutMode.vertical',
'vertical-mix': 'theme.layoutMode.vertical-mix', 'vertical-mix': 'theme.layout.layoutMode.vertical-mix',
horizontal: 'theme.layoutMode.horizontal', 'vertical-hybrid-header-first': 'theme.layout.layoutMode.vertical-hybrid-header-first',
'horizontal-mix': 'theme.layoutMode.horizontal-mix' horizontal: 'theme.layout.layoutMode.horizontal',
'top-hybrid-sidebar-first': 'theme.layout.layoutMode.top-hybrid-sidebar-first',
'top-hybrid-header-first': 'theme.layout.layoutMode.top-hybrid-header-first'
}; };
export const themeLayoutModeOptions = transformRecordToOption(themeLayoutModeRecord); export const themeLayoutModeOptions = transformRecordToOption(themeLayoutModeRecord);
export const themeScrollModeRecord: Record<UnionKey.ThemeScrollMode, App.I18n.I18nKey> = { export const themeScrollModeRecord: Record<UnionKey.ThemeScrollMode, App.I18n.I18nKey> = {
wrapper: 'theme.scrollMode.wrapper', wrapper: 'theme.layout.content.scrollMode.wrapper',
content: 'theme.scrollMode.content' content: 'theme.layout.content.scrollMode.content'
}; };
export const themeScrollModeOptions = transformRecordToOption(themeScrollModeRecord); export const themeScrollModeOptions = transformRecordToOption(themeScrollModeRecord);
export const themeTabModeRecord: Record<UnionKey.ThemeTabMode, App.I18n.I18nKey> = { export const themeTabModeRecord: Record<UnionKey.ThemeTabMode, App.I18n.I18nKey> = {
chrome: 'theme.tab.mode.chrome', chrome: 'theme.layout.tab.mode.chrome',
button: 'theme.tab.mode.button' button: 'theme.layout.tab.mode.button'
}; };
export const themeTabModeOptions = transformRecordToOption(themeTabModeRecord); export const themeTabModeOptions = transformRecordToOption(themeTabModeRecord);
export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode, App.I18n.I18nKey> = { export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode, App.I18n.I18nKey> = {
'fade-slide': 'theme.page.mode.fade-slide', 'fade-slide': 'theme.layout.content.page.mode.fade-slide',
fade: 'theme.page.mode.fade', fade: 'theme.layout.content.page.mode.fade',
'fade-bottom': 'theme.page.mode.fade-bottom', 'fade-bottom': 'theme.layout.content.page.mode.fade-bottom',
'fade-scale': 'theme.page.mode.fade-scale', 'fade-scale': 'theme.layout.content.page.mode.fade-scale',
'zoom-fade': 'theme.page.mode.zoom-fade', 'zoom-fade': 'theme.layout.content.page.mode.zoom-fade',
'zoom-out': 'theme.page.mode.zoom-out', 'zoom-out': 'theme.layout.content.page.mode.zoom-out',
none: 'theme.page.mode.none' none: 'theme.layout.content.page.mode.none'
}; };
export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord); export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord);
export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = { export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = {
close: 'theme.resetCacheStrategy.close', refresh: 'theme.layout.resetCacheStrategy.refresh',
refresh: 'theme.resetCacheStrategy.refresh' close: 'theme.layout.resetCacheStrategy.close'
}; };
export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord); export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord);
export const DARK_CLASS = 'dark'; export const DARK_CLASS = 'dark';
export const watermarkTimeFormatOptions = [
{ label: 'YYYY-MM-DD HH:mm', value: 'YYYY-MM-DD HH:mm' },
{ label: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss' },
{ label: 'YYYY/MM/DD HH:mm', value: 'YYYY/MM/DD HH:mm' },
{ label: 'YYYY/MM/DD HH:mm:ss', value: 'YYYY/MM/DD HH:mm:ss' },
{ label: 'HH:mm', value: 'HH:mm' },
{ label: 'HH:mm:ss', value: 'HH:mm:ss' },
{ label: 'MM-DD HH:mm', value: 'MM-DD HH:mm' }
];
export const themeTableSizeRecord: Record<UnionKey.ThemeTableSize, App.I18n.I18nKey> = { export const themeTableSizeRecord: Record<UnionKey.ThemeTableSize, App.I18n.I18nKey> = {
small: 'theme.table.size.small', small: 'theme.table.size.small',
medium: 'theme.table.size.medium', medium: 'theme.table.size.medium',

View File

@ -4,5 +4,6 @@ export enum SetupStoreId {
Auth = 'auth-store', Auth = 'auth-store',
Route = 'route-store', Route = 'route-store',
Tab = 'tab-store', Tab = 'tab-store',
Notice = 'notice-store' Notice = 'notice-store',
Dict = 'dict-store'
} }

View File

@ -113,6 +113,10 @@ export function useDownload() {
const response = await fetch(fullUrl, requestOptions); const response = await fetch(fullUrl, requestOptions);
if (response.status !== 200) {
throw new Error(errorCodeRecord.default);
}
await handleResponse(response); await handleResponse(response);
const rawHeader = response.headers.get('Download-Filename'); const rawHeader = response.headers.get('Download-Filename');

View File

@ -1,4 +1,4 @@
import { computed, effectScope, nextTick, onScopeDispose, ref, watch } from 'vue'; import { computed, effectScope, nextTick, onScopeDispose, shallowRef, watch } from 'vue';
import { useElementSize } from '@vueuse/core'; import { useElementSize } from '@vueuse/core';
import * as echarts from 'echarts/core'; import * as echarts from 'echarts/core';
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts'; import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
@ -86,11 +86,11 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const darkMode = computed(() => themeStore.darkMode); const darkMode = computed(() => themeStore.darkMode);
const domRef = ref<HTMLElement | null>(null); const domRef = shallowRef<HTMLElement | null>(null);
const initialSize = { width: 0, height: 0 }; const initialSize = { width: 0, height: 0 };
const { width, height } = useElementSize(domRef, initialSize); const { width, height } = useElementSize(domRef, initialSize);
let chart: echarts.ECharts | null = null; const chart = shallowRef<echarts.ECharts | null>(null);
const chartOptions: T = optionsFactory(); const chartOptions: T = optionsFactory();
const { const {
@ -111,18 +111,9 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
onDestroy onDestroy
} = hooks; } = hooks;
/**
* whether can render chart
*
* when domRef is ready and initialSize is valid
*/
function canRender() {
return domRef.value && initialSize.width > 0 && initialSize.height > 0;
}
/** is chart rendered */ /** is chart rendered */
function isRendered() { function isRendered() {
return Boolean(domRef.value && chart); return Boolean(domRef.value && chart.value);
} }
/** /**
@ -131,59 +122,59 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
* @param callback callback function * @param callback callback function
*/ */
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) { async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
if (!isRendered()) return;
const updatedOpts = callback(chartOptions, optionsFactory); const updatedOpts = callback(chartOptions, optionsFactory);
Object.assign(chartOptions, updatedOpts); Object.assign(chartOptions, updatedOpts);
await nextTick();
if (!isRendered()) return;
if (isRendered()) { if (isRendered()) {
chart?.clear(); chart.value?.clear();
} }
chart?.setOption({ ...updatedOpts, backgroundColor: 'transparent' }); chart.value?.setOption({ ...updatedOpts, backgroundColor: 'transparent' });
await onUpdated?.(chart!); await onUpdated?.(chart.value!);
} }
function setOptions(options: T) { function setOptions(options: T) {
chart?.setOption(options); chart.value?.setOption(options);
} }
/** render chart */ /** render chart */
async function render() { async function render() {
if (!isRendered()) { if (isRendered()) return;
const chartTheme = darkMode.value ? 'dark' : 'light';
await nextTick(); const chartTheme = darkMode.value ? 'dark' : 'light';
chart = echarts.init(domRef.value, chartTheme); chart.value = echarts.init(domRef.value, chartTheme);
chart.setOption({ ...chartOptions, backgroundColor: 'transparent' }); chart.value?.setOption({ ...chartOptions, backgroundColor: 'transparent' });
await onRender?.(chart); await onRender?.(chart.value!);
}
} }
/** resize chart */ /** resize chart */
function resize() { function resize() {
chart?.resize(); chart.value?.resize();
} }
/** destroy chart */ /** destroy chart */
async function destroy() { async function destroy() {
if (!chart) return; if (!chart.value) return;
await onDestroy?.(chart); await onDestroy?.(chart.value);
chart?.dispose(); chart.value?.dispose();
chart = null; chart.value = null;
} }
/** change chart theme */ /** change chart theme */
async function changeTheme() { async function changeTheme() {
await destroy(); await destroy();
await render(); await render();
await onUpdated?.(chart!); await onUpdated?.(chart.value!);
} }
/** /**
@ -196,30 +187,29 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
initialSize.width = w; initialSize.width = w;
initialSize.height = h; initialSize.height = h;
// size is abnormal, destroy chart
if (!canRender()) {
await destroy();
return;
}
// resize chart // resize chart
if (isRendered()) { if (isRendered()) {
resize(); resize();
return;
} }
// render chart // render chart
await render(); await render();
if (chart) { if (chart.value) {
await onUpdated?.(chart); await onUpdated?.(chart.value);
} }
} }
scope.run(() => { scope.run(() => {
watch([width, height], ([newWidth, newHeight]) => { watch(
renderChartBySize(newWidth, newHeight); [width, height],
}); ([newWidth, newHeight]) => {
renderChartBySize(newWidth, newHeight);
},
{ flush: 'post' }
);
watch(darkMode, () => { watch(darkMode, () => {
changeTheme(); changeTheme();
@ -233,6 +223,7 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
return { return {
domRef, domRef,
chart,
updateOptions, updateOptions,
setOptions setOptions
}; };

View File

@ -1,195 +1,55 @@
import { computed, effectScope, onScopeDispose, reactive, ref, watch } from 'vue'; import { computed, effectScope, onScopeDispose, reactive, ref, shallowRef, watch } from 'vue';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import type { PaginationProps } from 'naive-ui'; import type { PaginationProps } from 'naive-ui';
import { useBoolean, useTable } from '@sa/hooks';
import type { PaginationData, TableColumnCheck, UseTableOptions } from '@sa/hooks';
import type { FlatResponseData } from '@sa/axios';
import { jsonClone } from '@sa/utils'; import { jsonClone } from '@sa/utils';
import { useBoolean, useHookTable } from '@sa/hooks';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { $t } from '@/locales'; import { $t } from '@/locales';
type TableData = NaiveUI.TableData; export type UseNaiveTableOptions<ResponseData, ApiData, Pagination extends boolean> = Omit<
type GetTableData<A extends NaiveUI.TableApiFn> = NaiveUI.GetTableData<A>; UseTableOptions<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, Pagination>,
type TableColumn<T> = NaiveUI.TableColumn<T>; 'pagination' | 'getColumnChecks' | 'getColumns'
> & {
/**
* get column visible
*
* @param column
*
* @default true
*
* @returns true if the column is visible, false otherwise
*/
getColumnVisible?: (column: NaiveUI.TableColumn<ApiData>) => boolean;
};
export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTableConfig<A>) { const SELECTION_KEY = '__selection__';
const EXPAND_KEY = '__expand__';
export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptions<ResponseData, ApiData, false>) {
const scope = effectScope(); const scope = effectScope();
const appStore = useAppStore(); const appStore = useAppStore();
const isMobile = computed(() => appStore.isMobile); const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, false>({
...options,
const { apiFn, apiParams, immediate, showTotal = true } = config; getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
getColumns
const SELECTION_KEY = '__selection__';
const EXPAND_KEY = '__expand__';
const {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
getData,
searchParams,
updateSearchParams,
resetSearchParams
} = useHookTable<A, GetTableData<A>, TableColumn<NaiveUI.TableDataWithIndex<GetTableData<A>>>>({
apiFn,
apiParams,
columns: config.columns,
transformer: res => {
const { rows: records = [], total = 0 } = res.data || {};
const current = searchParams.pageNum as number;
const size = (searchParams.pageSize || 0) as number;
// Ensure that the size is greater than 0, If it is less than 0, it will cause paging calculation errors.
const pageSize = size <= 0 ? 10 : size;
const recordsWithIndex = records.map((item, index) => {
return {
...item,
index: (current - 1) * pageSize + index + 1
};
});
return {
data: recordsWithIndex,
pageNum: current,
pageSize,
total
};
},
getColumnChecks: cols => {
const checks: NaiveUI.TableColumnCheck[] = [];
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
checks.push({
key: column.key as string,
title: column.title!,
checked: true
});
} else if (column.type === 'selection') {
checks.push({
key: SELECTION_KEY,
title: $t('common.check'),
checked: true
});
} else if (column.type === 'expand') {
checks.push({
key: EXPAND_KEY,
title: $t('common.expandColumn'),
checked: true
});
}
});
return checks;
},
getColumns: (cols, checks) => {
const columnMap = new Map<string, TableColumn<GetTableData<A>>>();
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
columnMap.set(column.key as string, column);
} else if (column.type === 'selection') {
columnMap.set(SELECTION_KEY, column);
} else if (column.type === 'expand') {
columnMap.set(EXPAND_KEY, column);
}
});
const filteredColumns = checks
.filter(item => item.checked)
.map(check => columnMap.get(check.key) as TableColumn<GetTableData<A>>);
return filteredColumns;
},
onFetched: async transformed => {
const { total } = transformed;
updatePagination({
page: searchParams.pageNum,
pageSize: searchParams.pageSize,
itemCount: total
});
},
immediate
}); });
const pagination: PaginationProps = reactive({ // calculate the total width of the table this is used for horizontal scrolling
page: 1, const scrollX = computed(() => {
pageSize: 10, return result.columns.value.reduce((acc, column) => {
showSizePicker: true, return acc + Number(column.width ?? column.minWidth ?? 120);
itemCount: 0, }, 0);
pageSizes: [10, 15, 20, 25, 30],
onUpdatePage: async (page: number) => {
pagination.page = page;
updateSearchParams({
pageNum: page,
pageSize: pagination.pageSize!
});
getData();
},
onUpdatePageSize: async (pageSize: number) => {
pagination.pageSize = pageSize;
pagination.page = 1;
updateSearchParams({
pageNum: pagination.page,
pageSize
});
getData();
},
...(showTotal
? {
prefix: page => $t('datatable.itemCount', { total: page.itemCount })
}
: {})
}); });
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
const mobilePagination = computed(() => {
const p: PaginationProps = {
...pagination,
pageSlot: isMobile.value ? 3 : 9,
prefix: !isMobile.value && showTotal ? pagination.prefix : undefined
};
return p;
});
function updatePagination(update: Partial<PaginationProps>) {
Object.assign(pagination, update);
}
/**
* get data by page number
*
* @param pageNum the page number. default is 1
*/
async function getDataByPage(pageNum: number = 1) {
updatePagination({
page: pageNum
});
updateSearchParams({
pageNum,
pageSize: pagination.pageSize!
});
await getData();
}
scope.run(() => { scope.run(() => {
watch( watch(
() => appStore.locale, () => appStore.locale,
() => { () => {
reloadColumns(); result.reloadColumns();
} }
); );
}); });
@ -199,27 +59,126 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
}); });
return { return {
loading, ...result,
empty, scrollX
data,
columns,
columnChecks,
reloadColumns,
pagination,
mobilePagination,
updatePagination,
getData,
getDataByPage,
searchParams,
updateSearchParams,
resetSearchParams
}; };
} }
export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>, getData: () => Promise<void>) { type PaginationParams = Pick<PaginationProps, 'page' | 'pageSize'>;
type UseNaivePaginatedTableOptions<ResponseData, ApiData> = UseNaiveTableOptions<ResponseData, ApiData, true> & {
paginationProps?: Omit<PaginationProps, 'page' | 'pageSize' | 'itemCount'>;
/**
* whether to show the total count of the table
*
* @default true
*/
showTotal?: boolean;
onPaginationParamsChange?: (params: PaginationParams) => void | Promise<void>;
};
export function useNaivePaginatedTable<ResponseData, ApiData>(
options: UseNaivePaginatedTableOptions<ResponseData, ApiData>
) {
const scope = effectScope();
const appStore = useAppStore();
const isMobile = computed(() => appStore.isMobile);
const showTotal = computed(() => options.showTotal ?? true);
const pagination = reactive({
page: 1,
pageSize: 10,
itemCount: 0,
showSizePicker: true,
pageSizes: [10, 15, 20, 25, 30],
prefix: showTotal.value ? page => $t('datatable.itemCount', { total: page.itemCount }) : undefined,
onUpdatePage(page) {
pagination.page = page;
},
onUpdatePageSize(pageSize) {
pagination.pageSize = pageSize;
pagination.page = 1;
},
...options.paginationProps
}) as PaginationProps;
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
const mobilePagination = computed(() => {
const p: PaginationProps = {
...pagination,
pageSlot: isMobile.value ? 3 : 9,
prefix: !isMobile.value && showTotal.value ? pagination.prefix : undefined
};
return p;
});
const paginationParams = computed(() => {
const { page, pageSize } = pagination;
return {
page,
pageSize
};
});
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, true>({
...options,
pagination: true,
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
getColumns,
onFetched: data => {
pagination.itemCount = data.total;
}
});
async function getDataByPage(page: number = 1) {
if (page !== pagination.page) {
pagination.page = page;
return;
}
await result.getData();
}
scope.run(() => {
watch(
() => appStore.locale,
() => {
result.reloadColumns();
}
);
watch(paginationParams, async newVal => {
await options.onPaginationParamsChange?.(newVal);
await result.getData();
});
});
onScopeDispose(() => {
scope.stop();
});
return {
...result,
getDataByPage,
pagination,
mobilePagination
};
}
export function useTableOperate<TableData>(
data: Ref<TableData[]>,
idKey: keyof TableData,
getData: () => Promise<void>
) {
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean(); const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
const operateType = ref<NaiveUI.TableOperateType>('add'); const operateType = shallowRef<NaiveUI.TableOperateType>('add');
function handleAdd() { function handleAdd() {
operateType.value = 'add'; operateType.value = 'add';
@ -227,18 +186,18 @@ export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>,
} }
/** the editing row data */ /** the editing row data */
const editingData: Ref<T | null> = ref(null); const editingData = shallowRef<TableData | null>(null);
function handleEdit(field: keyof T, id: CommonType.IdType) { function handleEdit(id: TableData[keyof TableData]) {
operateType.value = 'edit'; operateType.value = 'edit';
const findItem = data.value.find(item => item[field] === id) || null; const findItem = data.value.find(item => item[idKey] === id) || null;
editingData.value = jsonClone(findItem); editingData.value = jsonClone(findItem);
openDrawer(); openDrawer();
} }
/** the checked row keys of table */ /** the checked row keys of table */
const checkedRowKeys = ref<CommonType.IdType[]>([]); const checkedRowKeys = shallowRef<string[]>([]);
/** the hook after the batch delete operation is completed */ /** the hook after the batch delete operation is completed */
async function onBatchDeleted() { async function onBatchDeleted() {
@ -270,6 +229,285 @@ export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>,
}; };
} }
function isTableColumnHasKey<T>(column: TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> { export function defaultTransform<ApiData>(
response: FlatResponseData<any, Api.Common.PaginatingQueryRecord<ApiData>>
): PaginationData<ApiData> {
const { data, error } = response;
if (error) {
return {
data: [],
pageNum: 1,
pageSize: 10,
total: 0
};
}
const { rows: records, pageSize: current, pageNum: size, total } = data;
return {
data: records,
pageNum: current,
pageSize: size,
total
};
}
type UseNaiveTreeTableOptions<ResponseData, ApiData> = UseNaiveTableOptions<ResponseData, ApiData, false> & {
keyField: keyof ApiData;
defaultExpandAll?: boolean;
};
export function useNaiveTreeTable<ResponseData, ApiData>(options: UseNaiveTreeTableOptions<ResponseData, ApiData>) {
const scope = effectScope();
const appStore = useAppStore();
const rows: Ref<ApiData[]> = ref([]);
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, false>({
...options,
pagination: false,
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
getColumns,
onFetched: transformData => {
const data: ApiData[] = [];
const collect = (nodes: any[]) => {
nodes.forEach(node => {
data.push(node);
if (node?.children?.length) {
collect(node.children);
}
});
};
collect(transformData);
rows.value = data;
}
});
const { keyField = 'id', defaultExpandAll = false } = options;
const expandedRowKeys = ref<ApiData[keyof ApiData][]>([]);
const { bool: isCollapse, toggle: toggleCollapse } = useBoolean(defaultExpandAll);
/** expand all nodes */
function expandAll() {
toggleCollapse();
expandedRowKeys.value = rows.value.map(item => item[keyField as keyof ApiData]);
}
/** collapse all nodes */
function collapseAll() {
toggleCollapse();
expandedRowKeys.value = [];
}
scope.run(() => {
watch(
() => appStore.locale,
() => {
result.reloadColumns();
}
);
});
onScopeDispose(() => {
scope.stop();
});
return {
...result,
rows,
isCollapse,
expandedRowKeys,
expandAll,
collapseAll
};
}
export function useTreeTableOperate<ApiData>(data: Ref<ApiData[]>, idKey: keyof ApiData, getData: () => Promise<void>) {
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
const operateType = shallowRef<NaiveUI.TableOperateType>('add');
function handleAdd() {
operateType.value = 'add';
openDrawer();
}
/** the editing row data */
const editingData = shallowRef<ApiData | null>(null);
function handleEdit(id: ApiData[keyof ApiData]) {
operateType.value = 'edit';
const findItem = data.value.find(item => item[idKey] === id) || null;
editingData.value = jsonClone(findItem);
openDrawer();
}
/** the checked row keys of table */
const checkedRowKeys = shallowRef<string[]>([]);
/** the hook after the batch delete operation is completed */
async function onBatchDeleted() {
window.$message?.success($t('common.deleteSuccess'));
checkedRowKeys.value = [];
await getData();
}
/** the hook after the delete operation is completed */
async function onDeleted() {
window.$message?.success($t('common.deleteSuccess'));
await getData();
}
return {
drawerVisible,
openDrawer,
closeDrawer,
operateType,
handleAdd,
editingData,
handleEdit,
checkedRowKeys,
onBatchDeleted,
onDeleted
};
}
type TreeTableOptions<ApiData> = {
/** id field name */
idField?: keyof ApiData;
/** parent id field name */
parentIdField?: keyof ApiData;
/** children field name */
childrenField?: keyof ApiData;
/** filter function */
filterFn?: (node: ApiData) => boolean;
};
export function treeTransform<ApiData>(
response: FlatResponseData<any, ApiData[]>,
options: TreeTableOptions<ApiData>
): ApiData[] {
const { data, error } = response;
if (error || !data.length) {
return [];
}
const { idField = 'id', parentIdField = 'parentId', childrenField = 'children', filterFn = () => true } = options;
// 使用 Map 替代普通对象,提高性能
const childrenMap = new Map<ApiData[keyof ApiData], ApiData[]>();
const nodeMap = new Map<ApiData[keyof ApiData], ApiData>();
const tree: ApiData[] = [];
// 第一遍遍历:构建节点映射
for (const item of data) {
const id = item[idField as keyof ApiData];
const parentId = item[parentIdField as keyof ApiData];
nodeMap.set(id, item);
if (!childrenMap.has(parentId)) {
childrenMap.set(parentId, []);
}
// 应用过滤函数
if (filterFn(item)) {
childrenMap.get(parentId)!.push(item);
}
}
// 第二遍遍历:找出根节点
for (const item of data) {
const parentId = item[parentIdField as keyof ApiData];
if (!nodeMap.has(parentId) && filterFn(item)) {
tree.push(item);
}
}
// 递归构建树形结构
const buildTree = (node: ApiData) => {
const id = node[idField as keyof ApiData];
const children = childrenMap.get(id);
if (children?.length) {
// 使用类型断言确保类型安全
(node as any)[childrenField] = children;
for (const child of children) {
buildTree(child);
}
} else {
// 如果没有子节点,设置为 undefined
(node as any)[childrenField] = undefined;
}
};
// 从根节点开始构建树
for (const root of tree) {
buildTree(root);
}
return tree;
}
function getColumnChecks<Column extends NaiveUI.TableColumn<any>>(
cols: Column[],
getColumnVisible?: (column: Column) => boolean
) {
const checks: TableColumnCheck[] = [];
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
checks.push({
key: column.key as string,
title: column.title!,
checked: true,
visible: getColumnVisible?.(column) ?? true
});
} else if (column.type === 'selection') {
checks.push({
key: SELECTION_KEY,
title: $t('common.check'),
checked: true,
visible: getColumnVisible?.(column) ?? false
});
} else if (column.type === 'expand') {
checks.push({
key: EXPAND_KEY,
title: $t('common.expandColumn'),
checked: true,
visible: getColumnVisible?.(column) ?? false
});
}
});
return checks;
}
function getColumns<Column extends NaiveUI.TableColumn<any>>(cols: Column[], checks: TableColumnCheck[]) {
const columnMap = new Map<string, Column>();
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
columnMap.set(column.key as string, column);
} else if (column.type === 'selection') {
columnMap.set(SELECTION_KEY, column);
} else if (column.type === 'expand') {
columnMap.set(EXPAND_KEY, column);
}
});
const filteredColumns = checks.filter(item => item.checked).map(check => columnMap.get(check.key) as Column);
return filteredColumns;
}
export function isTableColumnHasKey<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key); return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
} }

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