80 Commits

Author SHA1 Message Date
7ddf1cf5ae chore(projects): release v1.1.0 2025-07-01 10:46:39 +08:00
814b291c58 chore(projects): release v1.1.0 2025-07-01 10:42:40 +08:00
f7c7fc41da fix(other): 修复代码生成类型定义文件重复问题 2025-07-01 10:37:34 +08:00
53fa87dae2 Merge remote-tracking branch 'origin/dev' into dev 2025-07-01 10:09:53 +08:00
99675cbc0e docs(readme): 更新 README.md 文件 2025-07-01 10:08:09 +08:00
ad9386eb58 !11 fix(projects): 修复部门字典 sys_normal_disable 重复获取
Merge pull request !11 from 素还真/N/A
2025-07-01 02:05:34 +00:00
440fd836e2 update src/views/system/dept/modules/dept-search.vue.
部门字典 sys_normal_disable 重复获取

Signed-off-by: 素还真 <11555891+metabytes@user.noreply.gitee.com>
2025-07-01 01:59:02 +00:00
AN
6fc7b11b18 fix(projects): 修复代码生成逻辑判断问题 2025-06-30 20:44:20 +08:00
7c6ca91ef2 chore(projects): update pnpm-lock.yaml 2025-06-30 17:21:05 +08:00
c789867de3 Merge remote-tracking branch 'soybean/main' into dev
# Conflicts:
#	README.en_US.md
#	README.md
#	package.json
#	pnpm-lock.yaml
2025-06-30 17:17:43 +08:00
90145fa53b fix(styles): 修复登录页平板界面滚动问题 2025-06-30 11:55:32 +08:00
a31994dc98 docs(projects): 优化 cursor 规则及 mcp 2025-06-30 11:44:32 +08:00
9a480d2245 !8 优化模板:index[-tree].vue页面引用API接口路径遇到驼峰报错问题
Merge pull request !8 from 唐振超/dev
2025-06-30 01:15:55 +00:00
7b18c0c210 !7 fix 修复api层级目录不正确
Merge pull request !7 from 这夏天依然平凡/dev
2025-06-29 12:32:03 +00:00
d13beac046 update docs/template/index-tree.vue.vm.
Signed-off-by: 唐振超 <imtzc@qq.com>
2025-06-29 01:07:22 +00:00
3122cf4df5 update docs/template/index.vue.vm.
Signed-off-by: 唐振超 <imtzc@qq.com>
2025-06-29 01:07:04 +00:00
2f1733bae1 fix 修复api层级目录不正确
Signed-off-by: 这夏天依然平凡 <1822213252@qq.com>
2025-06-28 10:01:20 +00:00
AN
dbcf8d422a fix(projects): 修改强退在线设备接口 2025-06-27 21:26:47 +08:00
5cb1cebd88 chore(deps): update deps 2025-06-27 18:17:05 +08:00
3628c2496a fix(projects): 修复字典数据重复获取问题 2025-06-26 17:12:51 +08:00
650673e2db feat(hooks): 重构下载方法,支持流式下载 2025-06-26 14:14:02 +08:00
a5c4b4e3b7 chore(deps): update NodeJS and pnpm version requirements in package.json and documentation 2025-06-25 18:12:25 +08:00
fec0563ef7 chore(deps): update deps 2025-06-25 11:20:18 +08:00
1680ce4e26 Merge remote-tracking branch 'soybean/main' into dev
# Conflicts:
#	CHANGELOG.md
#	README.en_US.md
#	README.md
#	package.json
#	pnpm-lock.yaml
#	src/App.vue
#	src/theme/settings.ts
#	src/views/_builtin/iframe-page/[url].vue
2025-06-25 11:17:46 +08:00
87a675bf62 chore(projects): release v1.3.15 2025-06-24 21:40:19 +08:00
4d42dcbea8 docs(readme): add warning about upcoming V2 version and link to plan list 2025-06-24 21:39:21 +08:00
276d836c87 refactor(iframe-page): remove unused lifecycle hooks and clean up script setup 2025-06-24 21:31:57 +08:00
7d84062e2c fix(app): replace console.error with window.console.error for consistency 2025-06-24 21:31:17 +08:00
afd604212b fix(projects): ensure proper text color when themes are inverted 2025-06-24 21:20:24 +08:00
fcb89883fa optimize(components): optimize spacing for lang-switch dropdown options 2025-06-24 21:20:20 +08:00
dc674ce870 chore(deps): update deps 2025-06-24 21:11:08 +08:00
dbd995c12c chore(projects): update deps & fix moduleResolution 2025-06-24 19:29:12 +08:00
7b2e510a2f docs(other): update docs with video tutorial link. 2025-06-24 10:17:50 +08:00
AN
62e6c7763c fix(projects): 调整属性名 2025-06-22 14:41:05 +08:00
AN
8b3151b8ce fix(projects): 修复个人信息-修改密码未加密且参数错误问题 2025-06-22 12:49:51 +08:00
AN
f36ac9abc6 refactor(projects): 补充formTip信息 2025-06-20 21:47:43 +08:00
8b4e41ce1b style(projects): 优化移动端字体大小 2025-06-20 00:02:43 +08:00
742e3858ab feat(styles): 修复登录页移动端显示问题 2025-06-19 23:52:39 +08:00
907f043969 style(projects): 修改按钮文本颜色 2025-06-19 23:30:54 +08:00
adf3d87e5c revert(projects): 单点登录回调页面回滚 2025-06-19 23:20:06 +08:00
7846b2cb1f typo(projects): 修改标题内容 2025-06-19 23:06:41 +08:00
406800de59 style(projects): 重构登录页样式 2025-06-19 23:04:09 +08:00
7e4ecae6cb style(projects): 更换 logo 与加载样式 2025-06-19 21:28:19 +08:00
471912e17f optimize(projects): 优化接口请求异常拦截代码 2025-06-19 09:33:46 +08:00
031d071af1 fix(projects): 修复接口请求异常拦截问题 2025-06-19 09:26:46 +08:00
6e6cc4d91f fix(components): 修复菜单选择标签渲染问题 2025-06-18 23:48:49 +08:00
8c84063ad1 feat(projects): 新增字典多语言适配 2025-06-18 23:20:36 +08:00
0f33f4a301 feat(other): 新增菜单字典多语言适配 SQL 2025-06-18 22:58:48 +08:00
27f061957e fix(projects): 修复切换用户或登录过期部分问题 2025-06-18 21:07:59 +08:00
AN
72b8f56e32 fix(projects): 目录类型禁用iframe选项 2025-06-18 17:06:13 +08:00
AN
0ac0a093a4 fix(projects): 修复权限字符显示逻辑错误问题 2025-06-18 14:02:50 +08:00
94d1863ef3 fix(other): 修复代码生成字典相关问题 2025-06-17 14:03:47 +08:00
ffa47c37fa fix(projects): 修复导出查询参数问题 2025-06-17 14:01:41 +08:00
031b7f698a fix(projects): 修复首页未从环境变量获取问题 2025-06-16 17:56:25 +08:00
da149e5bbd fix(types): The environment variable VITE_ICON_LOCAL_PREFIX has the wrong type. 2025-06-14 22:01:01 +08:00
f0810bce4c fix(other): 代码生成模板 dateRangeTime 错误 2025-06-14 13:42:47 +08:00
1ec1099179 fix(other): 修复代码生成问题 2025-06-14 13:40:15 +08:00
AN
cafee1dbd9 fix(projects): 修复登录过期后,重复弹窗问题 2025-06-12 09:45:50 +08:00
39dd9acca9 feat(projects): 菜单字典适配 i18n 2025-06-11 20:36:53 +08:00
AN
d141ed5bef chore(projects): 移除未使用代码 2025-06-11 11:23:51 +08:00
AN
e16a0fa6ed fix(components): 修复上传组件回显问题,修改accept参数逻辑 2025-06-11 11:09:34 +08:00
03c8a7f5b7 feat(components): 新增表单上传组件 2025-06-10 22:00:28 +08:00
AN
da1c16e023 fix(components): 修复部门选择组件非树结构,默认展开失败问题 2025-06-10 20:33:11 +08:00
7c3dac4212 feat(projects): add configurable user name watermark option 2025-06-10 19:05:08 +08:00
aeb736ebf1 fix(components): 修复树选择组件再次勾选父子联动导致全选问题 2025-06-10 11:19:04 +08:00
bbda803e90 fix(components): 修复菜单树选择组件 2025-06-10 11:05:50 +08:00
94f183e7b5 merge(project): merge soybean 1.3.14 2025-06-10 09:46:43 +08:00
39b89a1234 chore(projects): release v1.3.14 2025-06-09 22:40:03 +08:00
c57f88aad2 fix(auth): remove redundant authStore declaration in resetStore function 2025-06-09 22:34:57 +08:00
3e4e17abd8 chore(deps): update deps 2025-06-09 22:10:30 +08:00
AN
858c318002 optimize(projects): optimize tab deletion logic 2025-06-09 21:28:03 +08:00
e6044d0fc7 optimize(projects): optimize tab deletion logic. closed #755 2025-06-09 14:19:44 +08:00
AN
a0f33664ec fix(projects): 修复自定义数据权限没有保存角色部门bug 2025-06-09 13:20:28 +08:00
AN
8bb31b1c36 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into dev 2025-06-08 22:51:46 +08:00
AN
64bd119c29 fix(utils): 修复 删除当前tab为最后一个时,tab切换错误bug. 2025-06-08 22:49:41 +08:00
2ed0b6484c feat(docs): add DartNode sponsorship badge to README files 2025-06-06 22:50:17 +08:00
e75c552551 docs(projects): 新增赞助列表 2025-06-06 14:34:14 +08:00
AN
d37adc362d fix(styles): 添加滚动条,去除页码 2025-06-06 13:07:21 +08:00
9ff15feee4 docs(projects): 更换微信群聊二维码 2025-06-06 09:49:25 +08:00
7bec9d1476 docs(projects): 修改部署工作流与项目说明文件 2025-06-06 09:36:29 +08:00
126 changed files with 3587 additions and 4274 deletions

View File

@ -1,106 +0,0 @@
## Development Guidelines
### Framework and Language
> This project uses Vue 3 with TypeScript, focusing on modern development practices and type safety.
**Framework Considerations:**
- Version Compatibility: Ensure all dependencies are compatible with Vue 3.4+ and TypeScript 5.3+
- Feature Usage: Leverage Vue 3 Composition API and TypeScript features
- Performance Patterns: Follow Vue 3 best practices for optimal performance
- Upgrade Strategy: Keep dependencies up to date while maintaining compatibility
- Importance Notes for Framework:
* Use Composition API for better code organization and reusability
* Implement proper TypeScript types for better development experience
* Follow Vue 3's reactivity system best practices
**Language Best Practices:**
- Type Safety: Use TypeScript's type system to prevent runtime errors
- Modern Features: Utilize TypeScript's latest features while maintaining compatibility
- Consistency: Apply consistent coding patterns throughout the codebase
- Documentation: Document complex TypeScript implementations and workarounds
### Code Abstraction and Reusability
> The project follows a modular architecture with clear separation of concerns. Key reusable components and utilities are organized in specific directories.
**Modular Design Principles:**
- Single Responsibility: Each module is responsible for only one functionality
- High Cohesion, Low Coupling: Related functions are centralized, reducing dependencies between modules
- Stable Interfaces: Expose stable interfaces externally while internal implementations can vary
**Reusable Component Library:**
```
root
- src
- components // Reusable Vue components
- advanced // Advanced UI components
- common // Common UI components
- custom // Custom business components
- hooks // Reusable Vue composition functions
- business // Business-specific hooks
- common // Common utility hooks
- utils // Utility functions
- common.ts // Common utility functions
- crypto.ts // Cryptographic utilities
- format.ts // Formatting utilities
- storage.ts // Storage utilities
```
### Coding Standards and Tools
**Code Formatting Tools:**
- ESLint: JavaScript/TypeScript code checking
- Prettier: Code formatting
- StyleLint: CSS/SCSS code checking
**Naming and Structure Conventions:**
- Semantic Naming: Variable/function names should clearly express their purpose
- Consistent Naming Style:
* Vue components: PascalCase (e.g., UserProfile.vue)
* TypeScript files: camelCase (e.g., userService.ts)
* CSS/SCSS: kebab-case (e.g., user-profile.scss)
- Directory Structure follows functional responsibility division
### Frontend-Backend Collaboration Standards
**API Design and Documentation:**
- RESTful design principles
* Use HTTP methods (GET, POST, PUT, DELETE) to represent operations
* Follow RESTful resource naming conventions
- Timely interface documentation updates
* Document API endpoints, parameters, and responses
* Keep API documentation in sync with implementation
- Unified error handling specifications
* Implement consistent error handling across the application
* Use appropriate HTTP status codes
**Data Flow:**
- Clear frontend state management
* Use Pinia for state management
* Implement proper state persistence strategies
- Data validation on both frontend and backend
* Validate data types and constraints
* Implement proper error handling
- Standardized asynchronous operation handling
* Use consistent API call patterns
* Implement proper loading and error states
### Performance and Security
**Performance Optimization Focus:**
- Resource loading optimization
* Use code splitting and lazy loading
* Implement proper caching strategies
- Rendering performance optimization
* Use virtualization for large lists
* Implement proper pagination
- Appropriate use of caching
* Implement caching strategies for API responses
* Use browser storage effectively
**Security Measures:**
- Input validation and filtering
* Validate user inputs and sanitize data
* Implement proper XSS protection
- Protection of sensitive information
* Use secure authentication and authorization mechanisms
* Implement proper token management
- Access control mechanisms
* Implement role-based access control
* Use proper permission checking

View File

@ -1,36 +0,0 @@
# Changelog
## [Unreleased]
### Added
- Added UnoCSS usage guidelines
- Added API request pattern documentation
- Added hooks usage guidelines for boolean and loading states
- Added table component guidelines with implementation examples
- Added code cleanliness guidelines for unused imports and variables
- Added comprehensive README.md file with project overview, installation instructions, development guidelines, and feature descriptions
### Changed
- Updated development guidelines with new sections
- Enhanced code documentation with specific usage patterns
- Updated UnoCSS documentation to emphasize its priority over custom CSS/SCSS
- Added guidance on choosing between useBoolean and useLoading based on business requirements
## [1.0.1] - 2024-06-28
### Added
- Enhanced project documentation with detailed component descriptions
- Added key project components section to documentation
- Added detailed descriptions of build system, monorepo structure, frontend architecture, API integration, and theming system
### Changed
- Improved project structure documentation with more detailed explanations
- Reorganized documentation to better highlight important architecture components
## [1.0.0] - 2024-03-20
### Added
- Initial project setup
- Basic project structure
- Core functionality implementation
- Documentation framework

File diff suppressed because it is too large Load Diff

39
.cursor/mcp.json Normal file
View File

@ -0,0 +1,39 @@
{
"mcpServers": {
"context7": {
"command": "npx",
"args": ["-y", "@upstash/context7-mcp@latest"]
},
"sequential-thinking": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"]
},
"mcp-feedback-enhanced": {
"command": "uvx",
"args": ["mcp-feedback-enhanced@latest"],
"timeout": 600,
"autoApprove": ["interactive_feedback"]
},
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"]
},
"mcp-server-time": {
"command": "uvx",
"args": ["mcp-server-time", "--local-timezone=Asia/Shanghai"]
},
"shrimp-task-manager": {
"command": "npx",
"args": ["-y", "mcp-shrimp-task-manager"],
"env": {
"DATA_DIR": "D:/workspace/Aother/mcp-shrimp-task-manager/data",
"TEMPLATES_USE": "en",
"ENABLE_GUI": "false"
}
},
"mcp-deepwiki": {
"command": "npx",
"args": ["-y", "mcp-deepwiki@latest"]
}
}
}

171
.cursor/rules/riper-5.mdc Normal file
View File

@ -0,0 +1,171 @@
---
description:
globs:
alwaysApply: false
---
**# RIPER-5 + 多维度思维 + 代理执行协议 (v4.9.1 - MCP工具驱动版)**
**元指令:** 此协议旨在最大化你的战略规划与执行效率。你的核心任务是**指挥和利用MCP工具集**来驱动项目进展。严格遵守核心原则,利用 `mcp-shrimp-task-manager` 进行项目规划与追踪,使用 `deepwiki-mcp` 进行深度研究。主动管理 `/project_document` 作为知识库。**每轮主要响应后,调用 `mcp.feedback_enhanced` 进行交互或通知。**
**目录**
* 核心理念与角色
* MCP工具集详解
* RIPER-5 模式:工具驱动的工作流
* 关键执行指南
* 产出核心要求 (文档与代码)
* 任务文件模板 (精简)
* 性能与自动化期望
## 1. 核心理念与角色
**1.1. AI设定与理念**
你是超智能AI项目指挥官代号齐天大圣你的职责不是手动完成每一步而是**高效地指挥MCP工具集**来自动化和管理整个项目生命周期。所有产出和关键文档存储在 `/project_document` 中。你将整合以下专家视角进行决策:
* **PM (项目经理):** 定义总体目标和风险,监控由 `mcp-shrimp-task-manager` 报告的进度。
* **PDM (产品经理):** 提供用户价值和需求,作为 `mcp-shrimp-task-manager` 规划任务的输入。
* **AR (架构师):** 负责系统和安全设计,其产出的架构将作为 `mcp-shrimp-task-manager` 任务分解的依据。
* **LD (首席开发):** 作为主要的**任务执行者**,从 `mcp-shrimp-task-manager` 接收任务,进行编码和测试(包括 `mcp.playwright`)。
* **DW (文档编写者):** 审计所有由AI或MCP工具生成的文档确保存储在 `/project_document` 的信息符合规范。
**1.2. `/project_document` 与文档管理:**
* `/project_document` 是项目的**最终知识库和产出存档**。
* `mcp-shrimp-task-manager` 负责过程中的任务记忆和状态追踪。
* AI负责将关键的、总结性的信息如最终架构、审查报告、自动生成的任务摘要等从MCP同步归档至 `/project_document`。
* **文档原则:** 最新内容优先、保留完整历史、精确时间戳(通过 `mcp.server_time`)、更新原因明确。
**1.3. 核心思维与编码原则 (AI内化执行)**
* **思维原则:** 系统思维、风险防范、工程卓越。AI应利用 `mcp.sequential_thinking` 进行深度思考,但将常规规划交给 `mcp-shrimp-task-manager`。
* **编码原则:** KISS, YAGNI, SOLID, DRY, 高内聚低耦合, 可读性, 可测试性, 安全编码。
## 2. MCP工具集详解
* **`mcp.feedback_enhanced` (用户交互核心):**
* 在每轮主要响应后**必须调用**,用于反馈、确认和流程控制。
* **AUTO模式自动化:** 若用户短时无交互AI自动按 `mcp-shrimp-task-manager` 的计划推进。
* **`mcp-shrimp-task-manager` (核心任务管理器):**
* **功能:** 项目规划、任务分解、依赖管理、状态追踪、复杂度评估、自动摘要、历史记忆。
* **AI交互** AI通过此MCP初始化项目、输入需求/架构、审查生成的计划、获取任务、报告结果。
* **激活声明:** `[INTERNAL_ACTION: Initializing/Interacting with mcp-shrimp-task-manager for X.]` (AI指明X的具体操作)
* **`deepwiki-mcp` (深度知识库):**
* **功能:** 抓取 `deepwiki.com` 的页面转换为干净的Markdown。
* **AI交互** 在研究阶段使用,以获取特定主题或库的深度信息。
* **激活声明:** `[INTERNAL_ACTION: Researching 'X' via deepwiki-mcp.]`
* **`mcp.context7` & `mcp.sequential_thinking` (AI认知增强):**
* 在需要超越标准流程的深度分析或复杂上下文理解时激活。
* **`mcp.playwright` & `mcp.server_time` (基础执行与服务):**
* `playwright` 由LD在执行E2E测试任务时使用。
* `server_time` 为所有记录提供标准时间戳。
## 3. RIPER-5 模式:工具驱动的工作流
**通用指令:** AI的核心工作是为每个阶段选择合适的MCP工具并有效指挥它。
### 模式1: 研究 (RESEARCH)
* **目的:** 快速形成对任务的全面理解。
* **核心工具与活动:**
1. 使用 `deepwiki-mcp` 抓取特定技术文档。
2. 对于系统性的技术研究,激活 `mcp-shrimp-task-manager` 的**研究模式**,它将提供引导式流程来探索和比较解决方案。
3. 分析现有项目文件(若有)。
* **产出:** 形成研究报告,存入 `/project_document/research/`,并在主任务文件 `任务文件名.md` 中进行摘要。
### 模式2: 创新 (INNOVATE)
* **目的:** 提出高层次的解决方案。此阶段侧重于人类与AI的创造性思维较少依赖自动化工具。
* **核心活动:** 基于研究成果进行头脑风暴提出2-3个候选方案。AR主导架构草图设计。
* **产出:** 形成包含各方案优劣对比的文档,存入 `/project_document/proposals/`。主任务文件中记录最终选择的方案方向。
### 模式3: 计划 (PLAN)
* **目的:** 将选定的方案转化为一个完整的、结构化的、可追踪的执行计划。
* **核心工具与活动:**
1. **激活 `mcp-shrimp-task-manager`**。
2. 向其输入选定的解决方案、架构设计来自AR、关键需求来自PDM
3. 指挥任务管理器进行**智能任务拆分、依赖关系管理和复杂度评估**。
4. PM和AR审查并批准由任务管理器生成的计划。
* **产出:**
* 一个由 `mcp-shrimp-task-manager` 管理的完整项目计划。
* 在主任务文件中记录**计划已生成**并附上访问计划的Web GUI链接如果启用或高级别计划摘要。**不再手动罗列详细清单。**
### 模式4: 执行 (EXECUTE)
* **目的:** 高效、准确地完成由任务管理器分派的任务。
* **核心工具与活动 (执行循环)**
1. LD向 `mcp-shrimp-task-manager` **请求下一个可执行任务**。
2. AI对当前任务进行必要的**预执行分析 (`EXECUTE-PREP`)**。
3. LD执行任务编码、使用`mcp.playwright`进行测试等)。
4. 完成后,向 `mcp-shrimp-task-manager` **报告任务完成状态和结果**。
5. 任务管理器**自动更新状态、处理依赖关系并生成任务摘要**。
* **产出:**
* 所有代码和测试产出按规范提交。
* 主任务文件的“任务进度”部分,通过引用 `mcp-shrimp-task-manager` 自动生成的摘要来**动态更新**,而非手动填写长篇报告。
### 模式5: 审查 (REVIEW)
* **目的:** 验证整个项目的成果是否符合预期。
* **核心工具与活动:**
1. 使用 `mcp-shrimp-task-manager` 的**任务完整性验证**功能,检查所有任务是否已关闭且符合其定义的完成标准。
2. 审查 `/project_document` 中归档的所有关键产出(最终架构、代码、测试报告摘要等)。
3. AR和LD进行代码和架构的最终审查。
* **产出:** 在主任务文件中撰写最终的审查报告,包括与 `mcp-shrimp-task-manager` 记录的对比、综合结论和改进建议。
## 4. 关键执行指南
* **指挥官角色:** 你的主要价值在于正确地使用和指挥MCP工具而不是手动执行本可自动化的任务。
* **信任工具:** 信任 `mcp-shrimp-task-manager` 进行详细的计划和追踪。你的任务是提供高质量的输入,并审查其输出。
* **自动化反馈环:** 利用 `mcp.feedback_enhanced` 和 `mcp-shrimp-task-manager` 的状态更新,与用户保持高效同步。
* **文档归档:** AI负责在项目关键节点如模式结束将 `mcp-shrimp-task-manager` 中的重要信息(如阶段性摘要、最终计划概览)固化并归档到 `/project_document`。
## 5. 产出核心要求 (文档与代码)
* **代码块结构 (`{{CHENGQI:...}}`):** 保持简洁,核心是 `Action`, `Timestamp`, `Reason`。
```language
// [INTERNAL_ACTION: Fetching current time via mcp.server_time.]
// {{CHENGQI:
// Action: [Added/Modified/Removed]; Timestamp: [...]; Reason: [Shrimp Task ID: #123, brief why];
// }}
// {{START MODIFICATIONS}} ... {{END MODIFICATIONS}}
```
* **文档质量 (DW审计):** 归档到 `/project_document` 的文档必须清晰、准确、完整。
## 6. 任务文件模板 (`任务文件名.md` - 精简)
# 上下文
项目ID: [...] 任务文件名:[...] 创建于:(`mcp.server_time`) [YYYY-MM-DD HH:MM:SS +08:00]
关联协议RIPER-5 v5.0
# 任务描述
[...]
# 1. 研究成果摘要 (RESEARCH)
* (如有) Deepwiki研究报告链接: /project_document/research/deepwiki_summary.md
* (如有) `mcp-shrimp-task-manager` 研究模式产出链接: /project_document/research/tech_comparison.md
# 2. 选定方案 (INNOVATE)
* **最终方案方向:** [方案描述例如采用微服务架构使用React前端...]
* **高层架构图链接:** /project_document/proposals/solution_arch_sketch.png
# 3. 项目计划 (PLAN)
* **状态:** 项目计划已通过 `mcp-shrimp-task-manager` 生成并最终确定。
* **计划访问:** [可选的Web GUI链接] 或 [高级别里程碑列表]
* **DW确认:** 计划生成过程已记录,符合规范。
# 4. 任务进度 (EXECUTE)
> 本部分由 `mcp-shrimp-task-manager` 的自动摘要驱动。将定期更新。
---
* **最近更新:** (`mcp.server_time`) [YYYY-MM-DD HH:MM:SS +08:00]
* **已完成任务摘要:**
* **[#123] 实现用户登录API:** 完成于 [...], 链接到代码提交和测试报告。
* **[#124] 创建登录页面UI:** 完成于 [...], 链接到代码提交和Playwright测试结果。
* ...
* **当前进行中任务:** [#125] 用户个人资料页面后端逻辑
---
# 5. 最终审查 (REVIEW)
* **符合性评估:** 项目成果已对照 `mcp-shrimp-task-manager` 的计划进行验证,所有任务均已关闭。
* **(AR)架构与安全评估:** 最终架构与设计一致,未发现重大安全疏漏。
* **(LD)测试与质量总结:** 单元测试覆盖率达到[X%]所有关键路径的E2E测试已通过。
* **综合结论:** 项目成功完成/有以下偏差...
* **改进建议:** [...]
## 7. 性能与自动化期望
* **极致效率:** AI应最大限度地减少手动干预让MCP工具处理所有可以自动化的工作。
* **战略聚焦:** 将AI的“思考”集中在无法被工具替代的领域战略决策、创新构想、复杂问题诊断 (`mcp.sequential_thinking`) 和最终质量把关。
* **无缝集成:** 期望AI能流畅地在不同MCP工具之间传递信息形成一个高度整合的自动化工作流。

4
.env
View File

@ -2,9 +2,9 @@
# if use a sub directory, it must be end with "/", like "/admin/" but not "/admin"
VITE_BASE_URL=/
VITE_APP_TITLE=RuoYi-Vue-Plus
VITE_APP_TITLE=RuoYi Plus Soybean
VITE_APP_DESC=RuoYi-Vue-Plus多租户管理系统
VITE_APP_DESC=RuoYi Plus Soybean 后台管理系统
# the prefix of the icon name
VITE_ICON_PREFIX=icon

View File

@ -3,6 +3,14 @@ name: Build and Deploy
on:
push:
branches: [ "master" ]
paths-ignore:
- 'README.md'
- 'LICENSE'
- 'docs/**'
- '.vscode/**'
- '.github/**'
- '.gitee/**'
- '.codeif/**'
jobs:
build-and-deploy:

View File

@ -1,5 +1,99 @@
# 更新日志
## [v1.1.0](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.0.0...v1.1.0) (2025-07-01)
### &nbsp;&nbsp;&nbsp;🚀 新功能
- **components**:
- 新增表单上传组件 &nbsp;-&nbsp; by @m-xlsea [<samp>(03c8a)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/03c8a7f5)
- **other**:
- 新增菜单字典多语言适配 SQL &nbsp;-&nbsp; by @m-xlsea [<samp>(0f33f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/0f33f4a3)
- **projects**:
- add configurable user name watermark option &nbsp;-&nbsp; by @wenyuanw [<samp>(7c3da)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7c3dac42)
- 菜单字典适配 i18n &nbsp;-&nbsp; by @m-xlsea [<samp>(39dd9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/39dd9acc)
- 新增字典多语言适配 &nbsp;-&nbsp; by @m-xlsea [<samp>(8c840)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8c84063a)
- **styles**:
- 修复登录页移动端显示问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(742e3)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/742e3858)
### &nbsp;&nbsp;&nbsp;🐞 Bug 修复
- **app**:
- replace console.error with window.console.error for consistency &nbsp;-&nbsp; by @soybeanjs [<samp>(7d840)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7d84062e)
- **auth**:
- remove redundant authStore declaration in resetStore function &nbsp;-&nbsp; by @soybeanjs [<samp>(c57f8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/c57f88aa)
- **components**:
- 修复菜单树选择组件 &nbsp;-&nbsp; by @m-xlsea [<samp>(bbda8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/bbda803e)
- 修复树选择组件再次勾选父子联动导致全选问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(aeb73)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/aeb736eb)
- 修复部门选择组件非树结构,默认展开失败问题 &nbsp;-&nbsp; by **AN** [<samp>(da1c1)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/da1c16e0)
- 修复上传组件回显问题修改accept参数逻辑 &nbsp;-&nbsp; by **AN** [<samp>(e16a0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e16a0fa6)
- 修复菜单选择标签渲染问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(6e6cc)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6e6cc4d9)
- **other**:
- 修复代码生成问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(1ec10)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/1ec10991)
- 代码生成模板 dateRangeTime 错误 &nbsp;-&nbsp; by @m-xlsea [<samp>(f0810)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f0810bce)
- 修复代码生成字典相关问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(94d18)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/94d1863e)
- 修复代码生成类型定义文件重复问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(f7c7fc41)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f7c7fc41)
- **projects**:
- 修复自定义数据权限没有保存角色部门bug &nbsp;-&nbsp; by **AN** [<samp>(a0f33)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a0f33664)
- 修复登录过期后,重复弹窗问题 &nbsp;-&nbsp; by **AN** [<samp>(cafee)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/cafee1db)
- 修复首页未从环境变量获取问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(031b7)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/031b7f69)
- 修复导出查询参数问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(ffa47)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ffa47c37)
- 修复权限字符显示逻辑错误问题 &nbsp;-&nbsp; by **AN** [<samp>(0ac0a)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/0ac0a093)
- 目录类型禁用iframe选项 &nbsp;-&nbsp; by **AN** [<samp>(72b8f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/72b8f56e)
- 修复切换用户或登录过期部分问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(27f06)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/27f06195)
- 修复接口请求异常拦截问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(031d0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/031d071a)
- 修复个人信息-修改密码未加密且参数错误问题 &nbsp;-&nbsp; by **AN** [<samp>(8b315)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8b3151b8)
- 调整属性名 &nbsp;-&nbsp; by **AN** [<samp>(62e6c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/62e6c776)
- ensure proper text color when themes are inverted &nbsp;-&nbsp; by @wenyuanw [<samp>(afd60)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/afd60421)
- **styles**:
- 添加滚动条,去除页码 &nbsp;-&nbsp; by **AN** [<samp>(d37ad)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d37adc36)
- **types**:
- The environment variable VITE_ICON_LOCAL_PREFIX has the wrong type. &nbsp;-&nbsp; by **chenziwen** [<samp>(da149)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/da149e5b)
- **utils**:
- 修复 删除当前tab为最后一个时tab切换错误bug. &nbsp;-&nbsp; by **AN** [<samp>(64bd1)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/64bd119c)
### &nbsp;&nbsp;&nbsp;🛠 优化
- **components**:
- optimize spacing for lang-switch dropdown options &nbsp;-&nbsp; by @wenyuanw [<samp>(fcb89)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/fcb89883)
- **projects**:
- optimize tab deletion logic. closed #755 &nbsp;-&nbsp; by @wenyuanw in https://gitee.com/xlsea/ruoyi-plus-soybean/issues/755 [<samp>(e6044)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e6044d0f)
- optimize tab deletion logic &nbsp;-&nbsp; by **AN** [<samp>(858c3)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/858c3180)
- 优化接口请求异常拦截代码 &nbsp;-&nbsp; by @m-xlsea [<samp>(47191)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/471912e1)
### &nbsp;&nbsp;&nbsp;💅 重构
- **iframe-page**: remove unused lifecycle hooks and clean up script setup &nbsp;-&nbsp; by @soybeanjs [<samp>(276d8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/276d836c)
- **projects**: 补充formTip信息 &nbsp;-&nbsp; by **AN** [<samp>(f36ac)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f36ac9ab)
### &nbsp;&nbsp;&nbsp;📖 文档
- **readme**:
- 更新 README.md 文件 &nbsp;-&nbsp; by @m-xlsea [<samp>(99675cb)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/99675cb)
### &nbsp;&nbsp;&nbsp;🏡 杂项
- **deps**:
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(3e4e1)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3e4e17ab)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(dc674)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/dc674ce8)
- update deps &nbsp;-&nbsp; by @m-xlsea [<samp>(fec05)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/fec0563e)
- **projects**:
- 移除未使用代码 &nbsp;-&nbsp; by **AN** [<samp>(d141e)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d141ed5b)
- update deps & fix `moduleResolution` &nbsp;-&nbsp; by @soybeanjs [<samp>(dbd99)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/dbd995c1)
### &nbsp;&nbsp;&nbsp;🎨 样式
- **projects**:
- 更换 logo 与加载样式 &nbsp;-&nbsp; by @m-xlsea [<samp>(7e4ec)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7e4ecae6)
- 重构登录页样式 &nbsp;-&nbsp; by @m-xlsea [<samp>(40680)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/406800de)
- 修改按钮文本颜色 &nbsp;-&nbsp; by @m-xlsea [<samp>(907f0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/907f0439)
- 优化移动端字体大小 &nbsp;-&nbsp; by @m-xlsea [<samp>(8b4e4)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8b4e41ce)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![xlsea](https://github.com/m-xlsea.png?size=48)](https://gitee.com/xlsea)&nbsp;&nbsp;[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![wenyuanw](https://github.com/wenyuanw.png?size=48)](https://github.com/wenyuanw)&nbsp;&nbsp;[![Elio-An](https://github.com/Elio-An.png?size=48)](https://gitee.com/elio-an)&nbsp;&nbsp;[![wenyuanw](https://github.com/chen-ziwen.png?size=48)](https://github.com/chen-ziwen)&nbsp;&nbsp;
[![wangzhongqi0917](https://gitee.com/wangzhongqi0917.png?width=48)](https://gitee.com/wangzhongqi0917)&nbsp;&nbsp;[![qq1822213252](https://gitee.com/qq1822213252.png?width=48)](https://gitee.com/qq1822213252)&nbsp;&nbsp;[![tangzc](https://gitee.com/tangzc.png?width=48)](https://gitee.com/tangzc),&nbsp;[metabytes](https://gitee.com/metabytes)
## [v1.0.0](https://gitee.com/xlsea/ruoyi-plus-soybean/releases/tag/v1.0.0) (2025-06-05)
### &nbsp;&nbsp;&nbsp;🚀 新功能

View File

@ -1,5 +1,6 @@
<div align="center">
<img src="https://foruda.gitee.com/images/1679673773341074847/178e8451_1766278.png" width="50%" height="50%">
<div align="center">
<img src="https://docs.ruoyi.xlsea.cn/logo.svg" width="160">
<h1>RuoYi-Plus-Soybean</h1>
</div>
<div style="height: 10px; clear: both;"></div>
@ -7,21 +8,29 @@
<div align="center">
<p>一个基于 <a href="https://gitee.com/dromara/RuoYi-Vue-Plus" target="_blank">RuoYi-Vue-Plus</a> 的后端能力和 <a href="https://github.com/soybeanjs/soybean-admin" target="_blank">Soybean Admin</a> 前端特性的现代化多租户管理系统</p>
<p>
<img src="https://gitee.com/xlsea/ruoyi-plus-soybean/badge/star.svg" alt="Gitee">
<img src="https://img.shields.io/github/stars/m-xlsea/ruoyi-plus-soybean" alt="Github">
<img src="https://img.shields.io/badge/Vue-3.5-brightgreen" alt="vue">
<img src="https://img.shields.io/badge/TypeScript-5.8-blue" alt="typescript">
<img src="https://img.shields.io/badge/Vite-6.2-orange" alt="vite">
<img src="https://img.shields.io/badge/NaiveUI-2.41-purple" alt="naive-ui">
<img src="https://img.shields.io/badge/License-MIT-yellow" alt="license">
<a href="https://gitee.com/xlsea/ruoyi-plus-soybean" target="_blank"><img src="https://gitee.com/xlsea/ruoyi-plus-soybean/badge/star.svg" alt="Gitee"></a>
<a href="https://gitee.com/dromara/RuoYi-Vue-Plus" target="_blank"><img src="https://gitee.com/xlsea/ruoyi-plus-soybean/badge/fork.svg" alt="Gitee-forks"></a>
<a href="https://github.com/m-xlsea/ruoyi-plus-soybean" target="_blank"><img src="https://img.shields.io/github/stars/m-xlsea/ruoyi-plus-soybean" alt="Github"></a>
<a href="https://github.com/m-xlsea/ruoyi-plus-soybean" target="_blank"><img src="https://img.shields.io/github/forks/m-xlsea/ruoyi-plus-soybean" alt="Github-forks"></a>
<a href="https://vuejs.org" target="_blank"><img src="https://img.shields.io/badge/Vue-3.5-brightgreen" alt="vue"></a>
<a href="https://www.typescriptlang.org" target="_blank"><img src="https://img.shields.io/badge/TypeScript-5.8-blue" alt="typescript"></a>
<a href="https://vite.dev" target="_blank"><img src="https://img.shields.io/badge/Vite-6.2-orange" alt="vite"></a>
<a href="https://www.naiveui.com" target="_blank"><img src="https://img.shields.io/badge/NaiveUI-2.41-purple" alt="naive-ui"></a>
<a href="./LICENSE" target="_blank"><img src="https://img.shields.io/badge/License-MIT-yellow" alt="license"></a>
</p>
</div>
# 📢 重要通知
<p style="color: red; font-weight: bold; font-size: 24px;">该项目未首发公测版本,请谨慎在生产环境使用!!!</p>
<p style="color: red; font-weight: bold; font-size: 24px;">该项目未首发公测版本,请谨慎在生产环境使用!!!</p>
<p style="color: red; font-weight: bold; font-size: 24px;">该项目未首发公测版本,请谨慎在生产环境使用!!!</p>
1.1.0 版本已发布,但仍然建议:
- 在生产环境使用前进行充分测试
- 关注项目更新,及时获取最新版本
- 积极反馈问题,帮助我们快速迭代
**后续规划**
- 工作流引擎集成
- 多语言国际化完善
- 性能优化和稳定性提升
> 如果对该项目感兴趣,可以给一个 Star 支持一下,谢谢!
> 请大家踊跃提交 PR 和 Issue一起完善这个项目
@ -340,6 +349,8 @@ console.log(t('common.confirm'));
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - 后端基础框架
- [Soybean Admin](https://github.com/soybeanjs/soybean-admin) - 前端基础框架
- [RuoYi-Plus-Soybean](https://ruoyi.xlsea.cn) - 官方演示站点
- [RuoYi-Plus-Soybean-Docs](https://docs.ruoyi.xlsea.cn) - 项目文档
- [Open Hives](https://openhives.com/questions) - OpenHives 问答社区
## 📮 联系方式
@ -354,7 +365,15 @@ console.log(t('common.confirm'));
## 💬 交流群
<img src="https://foruda.gitee.com/images/1748404753216665472/3d8b1a0b_5601833.png" width="300px" />
**加群前请先阅读一下内容:**
- 禁止内容:黄腔、暴力言论、政治话题,违者直接飞机票(踢出群)
- 遇到问题请先阅读 [项目文档](https://docs.ruoyi.xlsea.cn) 和 [Soybean 文档](https://docs.soybeanjs.cn/),某些简单问题不予理睬
- 蜡笔小新头像为机器人助手,私聊不保证回复,问题请在群内讨论
<img src="https://foruda.gitee.com/images/1749174520085305975/ad1b54fe_5601833.png" width="300px" />
添加作者微信备注:加群
## 🧧 捐献作者
@ -364,56 +383,4 @@ console.log(t('common.confirm'));
## 🫡 捐赠列表
感谢下方各位老板的捐赠 🫡
> 如果不想出现在下方捐赠列表,请在备注中说明,我会匿名处理
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747030503230216352/6879cbe5_5601833.jpeg" width="24px" >
<span>酷酷冬天 20元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747105640574377313/6d259bba_5601833.jpeg" width="24px" >
<span>Selfish Altruism(JackSue) 200元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747190127964232140/5ffa5ac4_5601833.jpeg" width="24px" >
<span>匿名用户 50元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747190127964232140/5ffa5ac4_5601833.jpeg" width="24px" >
<span>匿名用户 10元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747280244120267391/6d719481_5601833.jpeg" width="24px" >
<span>DAS 20元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747739790990961771/57f4b208_5601833.jpeg" width="24px" >
<span>大山 100元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747742188121789563/646dff1c_5601833.jpeg" width="24px" >
<span>依依 20元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747742593880284419/5b56043d_5601833.jpeg" width="24px" >
<span>沙海 20元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747789196227712891/00c37bdf_5601833.jpeg" width="24px" >
<span>xxl 50元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747796468040874363/1faa75ce_5601833.jpeg" width="24px" >
<span>莫离支🤴 10元</span>
</div>
**捐赠列表已移至 [捐赠列表](https://docs.ruoyi.xlsea.cn/other/donate.html)**

View File

@ -73,8 +73,9 @@ public class VelocityUtils {
velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName));
velocityContext.put("dicts", getDicts(genTable));
velocityContext.put("dictList", getDictList(genTable));
velocityContext.put("columns", getColumns(genTable));
velocityContext.put("columns", genTable.getColumns());
velocityContext.put("table", genTable);
velocityContext.put("StrUtil", new StrUtil());
setMenuVelocityContext(velocityContext, genTable);
if (GenConstants.TPL_TREE.equals(tplCategory)) {
setTreeVelocityContext(velocityContext, genTable);
@ -189,9 +190,9 @@ public class VelocityUtils {
} else if (template.contains("index-tree.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("api.d.ts.vm")) {
fileName = StringUtils.format("{}/typings/api/{}.api.d.ts", soybeanPath, moduleName);
fileName = StringUtils.format("{}/typings/api/{}.{}.api.d.ts", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("api.ts.vm")) {
fileName = StringUtils.format("{}/api/{}/{}.ts", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/service/api/{}/{}.ts", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("search.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-search.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("operate-drawer.vue.vm")) {
@ -296,23 +297,6 @@ public class VelocityUtils {
}
}
/**
* 根据列类型获取字典组
*
* @param genTable 业务表对象
* @return 返回字典组
*/
public static List<GenTableColumn> getColumns(GenTable genTable) {
List<GenTableColumn> columns = genTable.getColumns();
for (GenTableColumn column : columns) {
if (StringUtils.isNotBlank(column.getDictType())) {
column.setDictType(StringUtils.toCamelCase(column.getDictType()));
}
}
return columns;
}
/**
* 获取权限前缀
*

View File

@ -0,0 +1,59 @@
-- 修改字典数据表的 list_class 字段,将 danger 改为 error
UPDATE `sys_dict_data` SET `list_class` = 'error' WHERE `list_class` = 'danger';
-- 字典适配多语言
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_user_sex.male', `dict_type` = 'sys_user_sex' WHERE `dict_code` = 1;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_user_sex.female', `dict_type` = 'sys_user_sex' WHERE `dict_code` = 2;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_user_sex.unknown', `dict_type` = 'sys_user_sex' WHERE `dict_code` = 3;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_show_hide.show', `dict_type` = 'sys_show_hide' WHERE `dict_code` = 4;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_show_hide.hide', `dict_type` = 'sys_show_hide' WHERE `dict_code` = 5;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_normal_disable.normal', `dict_type` = 'sys_normal_disable' WHERE `dict_code` = 6;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_normal_disable.disable', `dict_type` = 'sys_normal_disable' WHERE `dict_code` = 7;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_yes_no.yes', `dict_type` = 'sys_yes_no' WHERE `dict_code` = 12;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_yes_no.no', `dict_type` = 'sys_yes_no' WHERE `dict_code` = 13;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_notice_type.notice', `dict_type` = 'sys_notice_type' WHERE `dict_code` = 14;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_notice_type.announcement', `dict_type` = 'sys_notice_type' WHERE `dict_code` = 15;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_notice_status.normal', `dict_type` = 'sys_notice_status' WHERE `dict_code` = 16;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_notice_status.close', `dict_type` = 'sys_notice_status' WHERE `dict_code` = 17;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.insert', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 18;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.update', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 19;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.delete', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 20;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.grant', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 21;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.export', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 22;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.import', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 23;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.force', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 24;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.gencode', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 25;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.clean', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 26;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_common_status.success', `dict_type` = 'sys_common_status' WHERE `dict_code` = 27;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_common_status.fail', `dict_type` = 'sys_common_status' WHERE `dict_code` = 28;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.other', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 29;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_grant_type.password', `dict_type` = 'sys_grant_type' WHERE `dict_code` = 30;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_grant_type.sms', `dict_type` = 'sys_grant_type' WHERE `dict_code` = 31;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_grant_type.email', `dict_type` = 'sys_grant_type' WHERE `dict_code` = 32;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_grant_type.miniapp', `dict_type` = 'sys_grant_type' WHERE `dict_code` = 33;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_grant_type.social', `dict_type` = 'sys_grant_type' WHERE `dict_code` = 34;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_device_type.pc', `dict_type` = 'sys_device_type' WHERE `dict_code` = 35;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_device_type.android', `dict_type` = 'sys_device_type' WHERE `dict_code` = 36;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_device_type.ios', `dict_type` = 'sys_device_type' WHERE `dict_code` = 37;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_device_type.miniapp', `dict_type` = 'sys_device_type' WHERE `dict_code` = 38;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.revoked', `dict_type` = 'wf_business_status' WHERE `dict_code` = 39;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.draft', `dict_type` = 'wf_business_status' WHERE `dict_code` = 40;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.pending', `dict_type` = 'wf_business_status' WHERE `dict_code` = 41;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.completed', `dict_type` = 'wf_business_status' WHERE `dict_code` = 42;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.cancelled', `dict_type` = 'wf_business_status' WHERE `dict_code` = 43;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.returned', `dict_type` = 'wf_business_status' WHERE `dict_code` = 44;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.terminated', `dict_type` = 'wf_business_status' WHERE `dict_code` = 45;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_form_type.custom_form', `dict_type` = 'wf_form_type' WHERE `dict_code` = 46;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_form_type.dynamic_form', `dict_type` = 'wf_form_type' WHERE `dict_code` = 47;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.revoke', `dict_type` = 'wf_task_status' WHERE `dict_code` = 48;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.pass', `dict_type` = 'wf_task_status' WHERE `dict_code` = 49;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.pending_review', `dict_type` = 'wf_task_status' WHERE `dict_code` = 50;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.cancel', `dict_type` = 'wf_task_status' WHERE `dict_code` = 51;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.return', `dict_type` = 'wf_task_status' WHERE `dict_code` = 52;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.terminate', `dict_type` = 'wf_task_status' WHERE `dict_code` = 53;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.transfer', `dict_type` = 'wf_task_status' WHERE `dict_code` = 54;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.delegate', `dict_type` = 'wf_task_status' WHERE `dict_code` = 55;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.copy', `dict_type` = 'wf_task_status' WHERE `dict_code` = 56;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.add_sign', `dict_type` = 'wf_task_status' WHERE `dict_code` = 57;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.minus_sign', `dict_type` = 'wf_task_status' WHERE `dict_code` = 58;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.timeout', `dict_type` = 'wf_task_status' WHERE `dict_code` = 59;

View File

@ -1,38 +1,38 @@
-- 目录类型菜单
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'carbon:cloud-service-management' WHERE `menu_id` = 1;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'stash:dashboard' WHERE `menu_id` = 2;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:tools' WHERE `menu_id` = 3;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'material-symbols:kid-star-outline' WHERE `menu_id` = 5;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:building-cog' WHERE `menu_id` = 6;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:logs' WHERE `menu_id` = 108;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'carbon:cloud-service-management', `menu_name` = 'route.system' WHERE `menu_id` = 1;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'stash:dashboard', `menu_name` = 'route.monitor' WHERE `menu_id` = 2;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:tools', `menu_name` = 'route.tool' WHERE `menu_id` = 3;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'material-symbols:kid-star-outline', `menu_name` = 'route.demo' WHERE `menu_id` = 5;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:building-cog', `menu_name` = 'menu.system_tenant' WHERE `menu_id` = 6;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:logs', `menu_name` = 'menu.system_log' WHERE `menu_id` = 108;
-- 页面类型
UPDATE `sys_menu` SET `icon` = 'ic:round-manage-accounts' WHERE `menu_id` = 100;
UPDATE `sys_menu` SET `icon` = 'carbon:user-role' WHERE `menu_id` = 101;
UPDATE `sys_menu` SET `icon` = 'material-symbols:route' WHERE `menu_id` = 102;
UPDATE `sys_menu` SET `icon` = 'mingcute:department-line' WHERE `menu_id` = 103;
UPDATE `sys_menu` SET `icon` = 'hugeicons:permanent-job' WHERE `menu_id` = 104;
UPDATE `sys_menu` SET `icon` = 'qlementine-icons:dictionary-16' WHERE `menu_id` = 105;
UPDATE `sys_menu` SET `icon` = 'carbon:parameter' WHERE `menu_id` = 106;
UPDATE `sys_menu` SET `icon` = 'solar:chat-line-outline' WHERE `menu_id` = 107;
UPDATE `sys_menu` SET `icon` = 'majesticons:status-online-line' WHERE `menu_id` = 109;
UPDATE `sys_menu` SET `icon` = 'simple-icons:redis' WHERE `menu_id` = 113;
UPDATE `sys_menu` SET `icon` = 'material-symbols:code-blocks-outline' WHERE `menu_id` = 115;
UPDATE `sys_menu` SET `icon` = 'material-symbols:attach-file' WHERE `menu_id` = 118;
UPDATE `sys_menu` SET `icon` = 'tabler:building-skyscraper' WHERE `menu_id` = 121;
UPDATE `sys_menu` SET `icon` = 'lets-icons:package-box-alt' WHERE `menu_id` = 122;
UPDATE `sys_menu` SET `icon` = 'tabler:device-imac-cog' WHERE `menu_id` = 123;
UPDATE `sys_menu` SET `icon` = 'carbon:operations-record' WHERE `menu_id` = 500;
UPDATE `sys_menu` SET `icon` = 'tabler:login-2' WHERE `menu_id` = 501;
UPDATE `sys_menu` SET `icon` = 'gg:debug' WHERE `menu_id` = 1500;
UPDATE `sys_menu` SET `icon` = 'gg:debug' WHERE `menu_id` = 1506;
UPDATE `sys_menu` SET `path` = 'oss/config', `component` = 'system/oss-config/index', `icon` = 'hugeicons:configuration-01' WHERE `menu_id` = 133;
UPDATE `sys_menu` SET `icon` = 'ic:round-manage-accounts', `menu_name` = 'route.system_user' WHERE `menu_id` = 100;
UPDATE `sys_menu` SET `icon` = 'carbon:user-role', `menu_name` = 'route.system_role' WHERE `menu_id` = 101;
UPDATE `sys_menu` SET `icon` = 'material-symbols:route', `menu_name` = 'route.system_menu' WHERE `menu_id` = 102;
UPDATE `sys_menu` SET `icon` = 'mingcute:department-line', `menu_name` = 'route.system_dept' WHERE `menu_id` = 103;
UPDATE `sys_menu` SET `icon` = 'hugeicons:permanent-job', `menu_name` = 'route.system_post' WHERE `menu_id` = 104;
UPDATE `sys_menu` SET `icon` = 'qlementine-icons:dictionary-16', `menu_name` = 'route.system_dict' WHERE `menu_id` = 105;
UPDATE `sys_menu` SET `icon` = 'carbon:parameter', `menu_name` = 'route.system_config' WHERE `menu_id` = 106;
UPDATE `sys_menu` SET `icon` = 'solar:chat-line-outline', `menu_name` = 'route.system_notice' WHERE `menu_id` = 107;
UPDATE `sys_menu` SET `icon` = 'majesticons:status-online-line', `menu_name` = 'route.monitor_online' WHERE `menu_id` = 109;
UPDATE `sys_menu` SET `icon` = 'simple-icons:redis', `menu_name` = 'route.monitor_cache' WHERE `menu_id` = 113;
UPDATE `sys_menu` SET `icon` = 'material-symbols:code-blocks-outline', `menu_name` = 'route.tool_gen' WHERE `menu_id` = 115;
UPDATE `sys_menu` SET `icon` = 'material-symbols:attach-file', `menu_name` = 'route.system_oss' WHERE `menu_id` = 118;
UPDATE `sys_menu` SET `icon` = 'tabler:building-skyscraper', `menu_name` = 'route.system_tenant' WHERE `menu_id` = 121;
UPDATE `sys_menu` SET `icon` = 'lets-icons:package-box-alt', `menu_name` = 'route.system_tenant-package' WHERE `menu_id` = 122;
UPDATE `sys_menu` SET `icon` = 'tabler:device-imac-cog', `menu_name` = 'route.system_client' WHERE `menu_id` = 123;
UPDATE `sys_menu` SET `icon` = 'carbon:operations-record', `menu_name` = 'route.monitor_operlog' WHERE `menu_id` = 500;
UPDATE `sys_menu` SET `icon` = 'tabler:login-2', `menu_name` = 'route.monitor_logininfor' WHERE `menu_id` = 501;
UPDATE `sys_menu` SET `icon` = 'gg:debug', `menu_name` = 'route.demo_demo' WHERE `menu_id` = 1500;
UPDATE `sys_menu` SET `icon` = 'gg:debug', `menu_name` = 'route.demo_tree' WHERE `menu_id` = 1506;
UPDATE `sys_menu` SET `path` = 'oss/config', `component` = 'system/oss-config/index', `icon` = 'hugeicons:configuration-01', `menu_name` = 'route.system_oss-config' WHERE `menu_id` = 133;
-- IFrame 类型
UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = 'https://ruoyi.xlsea.cn/admin/', `is_frame` = 2, `icon` = 'bx:bxl-spring-boot' WHERE `menu_id` = 117;
UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = 'https://preview.snailjob.opensnail.com/', `is_frame` = 2, `icon` = 'gridicons:scheduled' WHERE `menu_id` = 120;
UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = 'https://ruoyi.xlsea.cn/admin/', `is_frame` = 2, `icon` = 'bx:bxl-spring-boot', `menu_name` = 'menu.monitor_admin' WHERE `menu_id` = 117;
UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = 'https://preview.snailjob.opensnail.com/', `is_frame` = 2, `icon` = 'gridicons:scheduled', `menu_name` = 'menu.monitor_snail-job' WHERE `menu_id` = 120;
-- 外链类型
UPDATE `sys_menu` SET `path` = 'https://gitee.com/xlsea/ruoyi-plus-soybean', `component` = 'FrameView', `icon` = 'local-icon-gitee' WHERE `menu_id` = 4;
UPDATE `sys_menu` SET `path` = 'https://gitee.com/xlsea/ruoyi-plus-soybean', `component` = 'FrameView', `icon` = 'local-icon-gitee', `menu_name` = 'RuoYi-Plus-Soybean' WHERE `menu_id` = 4;
-- plus-ui 需要禁用的页面
UPDATE `sys_menu` SET `status` = '1' WHERE `menu_id` IN ( '116', '130', '131', '132', '11700', '11701' );

View File

@ -2,7 +2,7 @@
import { NDivider } from 'naive-ui';
import { jsonClone } from '@sa/utils';
import { type TableDataWithIndex } from '@sa/hooks';
import { fetchBatchDelete${BusinessName}, fetchGet${BusinessName}List } from '@/service/api/${moduleName}/${businessName}';
import { fetchBatchDelete${BusinessName}, fetchGet${BusinessName}List } from '@/service/api/${moduleName}/${business__name}';
import { useAppStore } from '@/store/modules/app';
import { useAuth } from '@/hooks/business/auth';
import { useTreeTable, useTreeTableOperate } from '@/hooks/common/tree-table';

View File

@ -1,6 +1,6 @@
<script setup lang="tsx">
import { NDivider } from 'naive-ui';
import { fetchBatchDelete${BusinessName}, fetchGet${BusinessName}List } from '@/service/api/${moduleName}/${businessName}';
import { fetchBatchDelete${BusinessName}, fetchGet${BusinessName}List } from '@/service/api/${moduleName}/${business__name}';
import { useAppStore } from '@/store/modules/app';
import { useAuth } from '@/hooks/business/auth';
import { useDownload } from '@/hooks/business/download';

View File

@ -52,7 +52,7 @@ function createDefaultModel(): Model {
return {
#foreach($column in $columns)
#if($column.insert)
${column.javaField}:#if($column.javaType == 'String') ''#else null#end#if($foreach.hasNext),#end
${column.javaField}:#if($column.javaType == 'String' || ($!column.dictType && $column.dictType != '')) ''#else undefined#end#if($foreach.hasNext),#end
#end
#end
};
@ -91,16 +91,22 @@ function closeDrawer() {
async function handleSubmit() {
await validate();
#set($operateColumns = [])
#foreach($column in $columns)#if($column.insert || $column.edit)#set($dummy = $operateColumns.add($column))#end#end
const { #foreach($column in $operateColumns)$column.javaField#if($foreach.hasNext), #end#end } = model;
// request
if (props.operateType === 'add') {
const { #foreach($column in $columns)#if($column.insert)$column.javaField#if($foreach.hasNext), #end#end#end } = model;
const { error } = await fetchCreate${BusinessName}({ #foreach($column in $columns)#if($column.insert)$column.javaField#if($foreach.hasNext), #end#end#end });
#set($addFields = [])
#foreach($column in $columns)#if($column.insert)#set($dummy = $addFields.add($column.javaField))#end#end
const { error } = await fetchCreate${BusinessName}({ #foreach($field in $addFields)$field#if($foreach.hasNext), #end#end });
if (error) return;
}
if (props.operateType === 'edit') {
const { #foreach($column in $columns)#if($column.edit)$column.javaField#if($foreach.hasNext), #end#end#end } = model;
const { error } = await fetchUpdate${BusinessName}({ #foreach($column in $columns)#if($column.edit)$column.javaField#if($foreach.hasNext), #end#end#end });
#set($editFields = [])
#foreach($column in $columns)#if($column.edit)#set($dummy = $editFields.add($column.javaField))#end#end
const { error } = await fetchUpdate${BusinessName}({ #foreach($field in $editFields)$field#if($foreach.hasNext), #end#end });
if (error) return;
}
@ -130,7 +136,7 @@ watch(visible, () => {
#else
#set($comment=$column.columnComment)
#end
#set($dictType=$column.dictType)
#set($dictType=$!StrUtil.toCamelCase($column.dictType))
<NFormItem label="$column.columnComment" path="$column.javaField">
#if($column.htmlType == "textarea" || $column.htmlType == "editor")
<NInput
@ -143,7 +149,7 @@ watch(visible, () => {
<NSelect
v-model:value="model.$column.javaField"
placeholder="请选择$column.columnComment"
:options="${column.dictType}Options"
:options="${dictType}Options"
clearable
/>
#elseif($column.htmlType == "select" && $dictType)
@ -157,7 +163,7 @@ watch(visible, () => {
<NRadioGroup v-model:value="model.$column.javaField">
<NSpace>
<NRadio
v-for="option in ${column.dictType}Options"
v-for="option in ${dictType}Options"
:key="option.value"
:value="option.value"
:label="option.label"
@ -170,13 +176,36 @@ watch(visible, () => {
<NRadio value="0" label="请选择字典生成" />
</NSpace>
</NRadioGroup>
#elseif($column.htmlType == "datetime")
#elseif($column.htmlType == "checkbox" && "" != $dictType)
<NCheckboxGroup v-model:value="model.$column.javaField">
<NSpace>
<NCheckbox
v-for="option in ${dictType}Options"
:key="option.value"
:value="option.value"
:label="option.label"
/>
</NSpace>
</NCheckboxGroup>
#elseif($column.htmlType == "checkbox" && $dictType)
<NCheckboxGroup v-model:value="model.$column.javaField">
<NSpace>
<NCheckbox value="0" label="请选择字典生成" />
</NSpace>
</NCheckboxGroup>
#elseif($column.htmlType == 'datetime')
<NDatePicker
v-model:formatted-value="model.$column.javaField"
type="date"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
clearable
/>
#elseif($column.htmlType == "imageUpload")
<OssUpload v-model:value="model.$column.javaField" upload-type="image" />
#elseif($column.htmlType == "fileUpload")
<OssUpload v-model:value="model.$column.javaField" upload-type="file" />
#elseif($column.htmlType == "editor")
<TinymceEditor v-model:value="model.$column.javaField" />
#else <NInput v-model:value="model.$column.javaField" placeholder="请输入$column.columnComment" />
#end
</NFormItem>

View File

@ -50,7 +50,7 @@ async function search() {
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
if (dateRange${AttrName}.value?.length) {
model.value.params!.begin${AttrName} = dateRange${AttrName}.value[0];
model.value.params!.end${AttrName} = dateRange${AttrName}.value[0];
model.value.params!.end${AttrName} = dateRange${AttrName}.value[1];
}
#end
#end
@ -66,7 +66,7 @@ async function search() {
<NGrid responsive="screen" item-responsive>
#foreach($column in $columns)
#if($column.query)
#set($dictType=$column.dictType)
#set($dictType=$!StrUtil.toCamelCase($column.dictType))
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
#set($parentheseIndex=$column.columnComment.indexOf(""))
#if($parentheseIndex != -1)
@ -75,35 +75,28 @@ async function search() {
#set($comment=$column.columnComment)
#end
<NFormItemGi span="24 s:12 m:6" label="$column.columnComment" path="$column.javaField" class="pr-24px">
#if(($column.htmlType == "select" || $column.htmlType == "radio") && "" != $dictType)
#if($!StrUtil.contains("select, radio, checkbox", $column.htmlType) && $dictType && "" != $dictType)
<NSelect
v-model:value="model.$column.javaField"
placeholder="请选择$column.columnComment"
:options="${column.dictType}Options"
:options="${dictType}Options"
clearable
/>
#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && $dictType)
#elseif($!StrUtil.contains("select, radio, checkbox", $column.htmlType))
<NSelect
v-model:value="model.$column.javaField"
placeholder="请选择$column.columnComment"
:options="[]"
clearable
/>
#elseif($column.htmlType.equals('select') || $column.htmlType.equals('radio'))
<NSelect
v-model:value="model.$column.javaField"
placeholder="请选择$column.columnComment"
:options="[]"
clearable
/>
#elseif($column.htmlType == "datetime" && $column.queryType != "BETWEEN")
#elseif($column.htmlType == 'datetime' && $column.queryType != "BETWEEN")
<NDatePicker
v-model:formatted-value="model.$column.javaField"
type="date"
value-format="yyyy-MM-dd"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
clearable
/>
#elseif($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#elseif($column.htmlType == 'datetime' && $column.queryType == "BETWEEN")
<NDatePicker
v-model:formatted-value="dateRange${AttrName}"
type="datetimerange"

View File

@ -2,7 +2,7 @@
<html lang="zh-cmn-Hans">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.png" />
<link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<title>%VITE_APP_TITLE%</title>

View File

@ -1,15 +1,15 @@
{
"name": "ruoyi-vue-plus",
"type": "module",
"version": "1.0.0",
"description": "RuoYi-Vue-Plus多租户管理系统",
"version": "1.1.0",
"description": "结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。",
"author": {
"name": "xlsea",
"email": "xlsea@linux.do",
"url": "https://gitee.com/xlsea/ruoyi-plus-soybean"
"email": "m@xlsea.cn",
"url": "https://gitee.com/xlsea"
},
"license": "MIT",
"homepage": "https://ruoyi.xlsea.cn",
"homepage": "https://docs.ruoyi.xlsea.cn",
"repository": {
"url": "https://gitee.com/xlsea/ruoyi-plus-soybean.git"
},
@ -18,6 +18,7 @@
},
"keywords": [
"RuoYi-Vue-Plus",
"Soybean Admin",
"Vue3 admin ",
"vue-admin-template",
"Vite6",
@ -26,9 +27,16 @@
"naive-ui-admin",
"UnoCSS"
],
"contributors": [
{
"name": "Elio",
"email": "1983933789@qq.com",
"url": "https://gitee.com/elio-an"
}
],
"engines": {
"node": ">=18.20.0",
"pnpm": ">=8.7.0"
"node": ">=20.19.0",
"pnpm": ">=10.5.0"
},
"scripts": {
"build": "vite build --mode prod",
@ -57,7 +65,8 @@
"@sa/materials": "workspace:*",
"@sa/tinymce": "workspace:*",
"@sa/utils": "workspace:*",
"@vueuse/core": "13.1.0",
"@types/streamsaver": "^2.0.5",
"@vueuse/core": "13.4.0",
"clipboard": "2.0.11",
"dayjs": "1.11.13",
"defu": "6.1.4",
@ -65,50 +74,51 @@
"highlight.js": "^11.11.1",
"jsencrypt": "^3.3.2",
"json5": "2.2.3",
"monaco-editor": "^0.52.0",
"naive-ui": "2.41.0",
"monaco-editor": "^0.52.2",
"naive-ui": "2.42.0",
"nprogress": "0.2.0",
"pinia": "3.0.2",
"tailwind-merge": "3.2.0",
"vue": "3.5.13",
"pinia": "3.0.3",
"streamsaver": "^2.0.6",
"tailwind-merge": "3.3.1",
"vue": "3.5.17",
"vue-advanced-cropper": "^2.8.9",
"vue-draggable-plus": "0.6.0",
"vue-i18n": "11.1.3",
"vue-i18n": "11.1.7",
"vue-router": "4.5.1"
},
"devDependencies": {
"@elegant-router/vue": "0.3.8",
"@iconify/json": "2.2.337",
"@iconify/json": "2.2.353",
"@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.6.0",
"@types/node": "22.15.17",
"@soybeanjs/eslint-config": "1.7.0",
"@types/node": "24.0.4",
"@types/nprogress": "0.2.3",
"@unocss/eslint-config": "66.1.1",
"@unocss/preset-icons": "66.1.1",
"@unocss/preset-uno": "66.1.1",
"@unocss/transformer-directives": "66.1.1",
"@unocss/transformer-variant-group": "66.1.1",
"@unocss/vite": "66.1.1",
"@vitejs/plugin-vue": "5.2.4",
"@vitejs/plugin-vue-jsx": "4.1.2",
"@unocss/eslint-config": "66.3.2",
"@unocss/preset-icons": "66.3.2",
"@unocss/preset-uno": "66.3.2",
"@unocss/transformer-directives": "66.3.2",
"@unocss/transformer-variant-group": "66.3.2",
"@unocss/vite": "66.3.2",
"@vitejs/plugin-vue": "6.0.0",
"@vitejs/plugin-vue-jsx": "5.0.0",
"consola": "3.4.2",
"eslint": "9.26.0",
"eslint-plugin-vue": "10.1.0",
"eslint": "9.29.0",
"eslint-plugin-vue": "10.2.0",
"kolorist": "1.8.0",
"sass": "1.88.0",
"sass": "1.89.2",
"simple-git-hooks": "2.13.0",
"tsx": "4.19.4",
"tsx": "4.20.3",
"typescript": "5.8.3",
"unplugin-icons": "22.1.0",
"unplugin-vue-components": "28.5.0",
"vite": "6.3.5",
"unplugin-vue-components": "28.7.0",
"vite": "7.0.0",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-progress": "0.0.7",
"vite-plugin-static-copy": "^3.0.0",
"vite-plugin-static-copy": "^3.0.2",
"vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "7.7.6",
"vue-eslint-parser": "10.1.3",
"vite-plugin-vue-devtools": "7.7.7",
"vue-eslint-parser": "10.1.4",
"vue-tsc": "2.2.10"
},
"simple-git-hooks": {

View File

@ -1,6 +1,6 @@
{
"name": "@sa/alova",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts",
"./fetch": "./src/fetch.ts",
@ -13,8 +13,8 @@
}
},
"dependencies": {
"@alova/mock": "2.0.14",
"@alova/mock": "2.0.17",
"@sa/utils": "workspace:*",
"alova": "3.2.10"
"alova": "3.3.3"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@sa/axios",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},
@ -11,11 +11,11 @@
},
"dependencies": {
"@sa/utils": "workspace:*",
"axios": "1.9.0",
"axios": "1.10.0",
"axios-retry": "4.5.0",
"qs": "6.14.0"
},
"devDependencies": {
"@types/qs": "6.9.18"
"@types/qs": "6.14.0"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@sa/color",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},

View File

@ -1,6 +1,6 @@
{
"name": "@sa/hooks",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},

View File

@ -1,6 +1,6 @@
{
"name": "@sa/materials",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},

View File

@ -1,6 +1,6 @@
{
"name": "@sa/fetch",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},

View File

@ -1,6 +1,6 @@
{
"name": "@sa/scripts",
"version": "1.3.13",
"version": "1.3.15",
"bin": {
"sa": "./bin.ts"
},
@ -14,12 +14,12 @@
},
"devDependencies": {
"@soybeanjs/changelog": "0.3.24",
"bumpp": "10.1.0",
"c12": "3.0.3",
"bumpp": "10.2.0",
"c12": "3.0.4",
"cac": "6.7.14",
"consola": "3.4.2",
"enquirer": "2.4.1",
"execa": "9.5.3",
"execa": "9.6.0",
"kolorist": "1.8.0",
"npm-check-updates": "18.0.1",
"rimraf": "6.0.1"

View File

@ -10,9 +10,9 @@
}
},
"dependencies": {
"tinymce": "7.8.0"
"tinymce": "7.9.1"
},
"devDependencies": {
"@tinymce/tinymce-vue": "6.1.0"
"@tinymce/tinymce-vue": "6.2.0"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@sa/uno-preset",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},

View File

@ -1,6 +1,6 @@
{
"name": "@sa/utils",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},

3997
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

4
public/favicon.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 476.22 476.22">
<path fill="#0e42d2" d="M389.98,400.6c-64.58,76.59-176.66,93.98-261.1,38.54-57.75-37.91-92.29-103.74-87.69-173.54,1.28-19.35,9.54-58.77,33.33-61.44,20.49-2.29,43.2,14.41,62.99,21.14,64.93,22.1,150.88,21.39,214.51-4.81,17.24-7.1,39.23-23.9,58.05-13.31,25.61,14.41,27.13,67.07,24.38,92.75-3.94,36.78-20.9,72.7-44.47,100.65ZM150.91,269c-17.4-4.01-34.19-9.87-50.5-17.04-2.56-1.12-15.17-8.19-16.64-6.83-23.51,92.25,48.81,179.87,140.64,188.01,3.68.33,9,.72,12.64.79,3.76.07,7.18-.41,10.86-.78-.24-3.7-1.05-7.05-.77-10.85,1.3-17.71,28.61-26.36,26.88-43.25-.89-8.76-14.85-11.03-22.02-11.55-8.23-.6-17.94,1.77-25.24-3.11-5.39-3.61-5.08-10.32-7.37-15.76-4.74-11.26-15.86-17.54-27.79-18.47-22.55-1.77-68.52,7.5-81.87-16.63-11.77-21.27,12.24-35.1,29.25-40.42,3.38-1.06,7.52-1.31,10.88-2.55,1.03-.38,1.28.27,1.04-1.56ZM391.83,245.21c-1.03-.58-12.51,5.5-14.65,6.42-20.82,8.92-42.01,16.02-64.2,20.87-10.31,2.25-21.7,3.15-31.66,5.65-19.17,4.82-17.59,22.78-.42,29.11,10.27,3.78,25.34,3.94,36.56,5.97,23.56,4.26,56.74,11.28,53.56,41.93-.31,2.98-2.13,5.86-2.26,8.2-.08,1.48.18,1.21,1.5,1.13,5.18-9.87,11.46-19.09,15.7-29.46,7.55-18.44,12.19-42.75,11.17-62.68-.19-3.67-2.95-25.81-5.31-27.15Z"/>
<path fill="#0e42d2" d="M278.22,21.54c4.79,5.69,9.27,14.39,11.84,21.37.67,1.8,1.49,7.86,2.59,8.61,1.49,1.01,17.78-3.63,21.39-4.05,71.76-8.37,101.88,65.14,44.46,110.3-41.92,32.98-84.43,32.67-135.62,31.41-47.74-1.18-111.25-13.52-129.97-64.78-19.66-53.81,31.01-83.19,77.15-58.5,2,1.07,11.58,7.79,12.54,7.59,1.57-.32,1.05-4.62,1.34-6.14,3.75-19.76,11.37-39.47,27.8-52.04,20.82-15.93,49.31-14.16,66.47,6.23ZM256.71,71.1c1.07-24.31-18.03-42.71-30.8-12.52-5.88,13.9-7.77,32.56-3.36,47,1.73.46,1.24-.41,1.7-.95,10.15-11.8,19.02-25.11,32.46-33.52ZM340.18,123.96c10.12-10.37,17.92-27.48.94-35.85-29-14.31-83.74,17.89-87.91,49.24-1.6,11.97,2.2,14.36,13.71,14.53,21.84.32,57.86-12.13,73.26-27.92ZM173.64,117.92c-4.77-6.36-9.81-11.55-16.53-15.93-7.07-4.62-24.6-13.3-29.47-2.16-3.94,9.02,6.8,23.05,13.43,28.86,12.36,10.83,29.63,16.76,45.66,19.42-.28-11.02-6.62-21.58-13.08-30.19Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -4,8 +4,8 @@ import { NConfigProvider, darkTheme } from 'naive-ui';
import type { WatermarkProps } from 'naive-ui';
import { useAppStore } from './store/modules/app';
import { useThemeStore } from './store/modules/theme';
import { naiveDateLocales, naiveLocales } from './locales/naive';
import { useAuthStore } from './store/modules/auth';
import { naiveDateLocales, naiveLocales } from './locales/naive';
defineOptions({
name: 'App'
@ -27,8 +27,12 @@ const naiveDateLocale = computed(() => {
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 {
content: userInfo.user?.userName ? `${userInfo.user?.nickName}@${appTitle} ${userInfo.user?.userName}` : appTitle,
content,
cross: true,
fullscreen: true,
fontSize: 14,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 192 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 476.22 476.22">
<path fill="#0e42d2" d="M389.98,400.6c-64.58,76.59-176.66,93.98-261.1,38.54-57.75-37.91-92.29-103.74-87.69-173.54,1.28-19.35,9.54-58.77,33.33-61.44,20.49-2.29,43.2,14.41,62.99,21.14,64.93,22.1,150.88,21.39,214.51-4.81,17.24-7.1,39.23-23.9,58.05-13.31,25.61,14.41,27.13,67.07,24.38,92.75-3.94,36.78-20.9,72.7-44.47,100.65ZM150.91,269c-17.4-4.01-34.19-9.87-50.5-17.04-2.56-1.12-15.17-8.19-16.64-6.83-23.51,92.25,48.81,179.87,140.64,188.01,3.68.33,9,.72,12.64.79,3.76.07,7.18-.41,10.86-.78-.24-3.7-1.05-7.05-.77-10.85,1.3-17.71,28.61-26.36,26.88-43.25-.89-8.76-14.85-11.03-22.02-11.55-8.23-.6-17.94,1.77-25.24-3.11-5.39-3.61-5.08-10.32-7.37-15.76-4.74-11.26-15.86-17.54-27.79-18.47-22.55-1.77-68.52,7.5-81.87-16.63-11.77-21.27,12.24-35.1,29.25-40.42,3.38-1.06,7.52-1.31,10.88-2.55,1.03-.38,1.28.27,1.04-1.56ZM391.83,245.21c-1.03-.58-12.51,5.5-14.65,6.42-20.82,8.92-42.01,16.02-64.2,20.87-10.31,2.25-21.7,3.15-31.66,5.65-19.17,4.82-17.59,22.78-.42,29.11,10.27,3.78,25.34,3.94,36.56,5.97,23.56,4.26,56.74,11.28,53.56,41.93-.31,2.98-2.13,5.86-2.26,8.2-.08,1.48.18,1.21,1.5,1.13,5.18-9.87,11.46-19.09,15.7-29.46,7.55-18.44,12.19-42.75,11.17-62.68-.19-3.67-2.95-25.81-5.31-27.15Z"/>
<path fill="#0e42d2" d="M278.22,21.54c4.79,5.69,9.27,14.39,11.84,21.37.67,1.8,1.49,7.86,2.59,8.61,1.49,1.01,17.78-3.63,21.39-4.05,71.76-8.37,101.88,65.14,44.46,110.3-41.92,32.98-84.43,32.67-135.62,31.41-47.74-1.18-111.25-13.52-129.97-64.78-19.66-53.81,31.01-83.19,77.15-58.5,2,1.07,11.58,7.79,12.54,7.59,1.57-.32,1.05-4.62,1.34-6.14,3.75-19.76,11.37-39.47,27.8-52.04,20.82-15.93,49.31-14.16,66.47,6.23ZM256.71,71.1c1.07-24.31-18.03-42.71-30.8-12.52-5.88,13.9-7.77,32.56-3.36,47,1.73.46,1.24-.41,1.7-.95,10.15-11.8,19.02-25.11,32.46-33.52ZM340.18,123.96c10.12-10.37,17.92-27.48.94-35.85-29-14.31-83.74,17.89-87.91,49.24-1.6,11.97,2.2,14.36,13.71,14.53,21.84.32,57.86-12.13,73.26-27.92ZM173.64,117.92c-4.77-6.36-9.81-11.55-16.53-15.93-7.07-4.62-24.6-13.3-29.47-2.16-3.94,9.02,6.8,23.05,13.43,28.86,12.36,10.83,29.63,16.76,45.66,19.42-.28-11.02-6.62-21.58-13.08-30.19Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -31,13 +31,25 @@ const tooltipContent = computed(() => {
return $t('icon.lang');
});
/** Add bottom margin to all options except the last one for proper visual separation */
const dropdownOptions = computed(() => {
const lastIndex = props.langOptions.length - 1;
return props.langOptions.map((option, index) => ({
...option,
props: {
class: index < lastIndex ? 'mb-1' : undefined
}
}));
});
function changeLang(lang: App.I18n.LangType) {
emit('changeLang', lang);
}
</script>
<template>
<NDropdown :value="lang" :options="langOptions" trigger="hover" @select="changeLang">
<NDropdown :value="lang" :options="dropdownOptions" trigger="hover" @select="changeLang">
<div>
<ButtonIcon :tooltip-content="tooltipContent" tooltip-placement="left">
<SvgIcon icon="heroicons:language" />

View File

@ -3,7 +3,7 @@ defineOptions({ name: 'SystemLogo' });
</script>
<template>
<img src="@/assets/imgs/logo.png" />
<icon-local-logo />
</template>
<style scoped></style>

View File

@ -2,7 +2,7 @@
import { useAttrs } from 'vue';
import type { TreeSelectProps } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { fetchGetDeptSelect } from '@/service/api/system';
import { fetchGetDeptTree } from '@/service/api/system';
defineOptions({ name: 'DeptTreeSelect' });
@ -13,16 +13,21 @@ interface Props {
defineProps<Props>();
const value = defineModel<CommonType.IdType | null>('value', { required: false });
const options = defineModel<Api.System.Dept[]>('options', { required: false, default: [] });
const options = defineModel<Api.Common.CommonTreeRecord>('options', { required: false, default: [] });
const expandedKeys = defineModel<CommonType.IdType[]>('expandedKeys', { required: false, default: [] });
const attrs: TreeSelectProps = useAttrs();
const { loading, startLoading, endLoading } = useLoading();
async function getDeptList() {
startLoading();
const { error, data } = await fetchGetDeptSelect();
const { error, data } = await fetchGetDeptTree();
if (error) return;
options.value = data;
// 设置默认展开的节点
if (data?.length && !expandedKeys.value.length) {
expandedKeys.value = [data[0].id];
}
endLoading();
}
@ -32,13 +37,13 @@ getDeptList();
<template>
<NTreeSelect
v-model:value="value"
v-model:expanded-keys="expandedKeys"
filterable
class="h-full"
:loading="loading"
key-field="deptId"
label-field="deptName"
:options="options"
:default-expanded-keys="[0]"
key-field="id"
label-field="label"
:options="options as []"
v-bind="attrs"
/>
</template>

View File

@ -3,6 +3,7 @@ import { computed, useAttrs } from 'vue';
import type { TagProps } from 'naive-ui';
import { useDict } from '@/hooks/business/dict';
import { isNotNull } from '@/utils/common';
import { $t } from '@/locales';
defineOptions({ name: 'DictTag' });
@ -23,13 +24,18 @@ const props = withDefaults(defineProps<Props>(), {
const attrs = useAttrs() as TagProps;
const { transformDictData } = useDict(props.dictCode, props.immediate);
const dictTagData = computed<Api.System.DictData[]>(() => {
if (props.dictData) {
return [props.dictData];
const dictData = props.dictData;
if (dictData.dictLabel?.startsWith(`dict.${dictData.dictType}.`)) {
dictData.dictLabel = $t(dictData.dictLabel as App.I18n.I18nKey);
}
return [dictData];
}
// 避免 props.value 为 0 时,无法触发
if (props.dictCode && isNotNull(props.value)) {
const { transformDictData } = useDict(props.dictCode, props.immediate);
return transformDictData(props.value) || [];
}

View File

@ -1,9 +1,10 @@
<script setup lang="ts">
import { ref, useAttrs, watch } from 'vue';
import { computed, useAttrs } from 'vue';
import type { UploadFileInfo, UploadProps } from 'naive-ui';
import { fetchBatchDeleteOss } from '@/service/api/system/oss';
import { getToken } from '@/store/modules/auth/shared';
import { getServiceBaseURL } from '@/utils/service';
import { AcceptType } from '@/enum/business';
defineOptions({
name: 'FileUpload'
@ -26,27 +27,24 @@ const props = withDefaults(defineProps<Props>(), {
defaultUpload: true,
showTip: true,
max: 5,
accept: '.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.pdf',
accept: undefined,
fileSize: 5,
uploadType: 'file'
});
const accept = computed(() => {
if (props.accept) {
return props.accept;
}
return props.uploadType === 'file' ? AcceptType.File : AcceptType.Image;
});
const attrs: UploadProps = useAttrs();
let fileNum = 0;
const fileList = ref<UploadFileInfo[]>([]);
const needRelaodData = ref(false);
defineExpose({
needRelaodData
const fileList = defineModel<UploadFileInfo[]>('fileList', {
default: () => []
});
watch(
() => fileList.value,
newValue => {
needRelaodData.value = newValue.length > 0;
}
);
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
@ -61,12 +59,12 @@ function beforeUpload(options: { file: UploadFileInfo; fileList: UploadFileInfo[
const { file } = options;
// 校检文件类型
if (props.accept) {
if (accept.value) {
const fileName = file.name.split('.');
const fileExt = `.${fileName[fileName.length - 1]}`;
const isTypeOk = props.accept.split(',')?.includes(fileExt);
const isTypeOk = accept.value.split(',')?.includes(fileExt);
if (!isTypeOk) {
window.$message?.error(`文件格式不正确, 请上传 ${props.accept} 格式文件!`);
window.$message?.error(`文件格式不正确, 请上传 ${accept.value} 格式文件!`);
return false;
}
}
@ -119,64 +117,67 @@ function handleError(options: { file: UploadFileInfo; event?: ProgressEvent }) {
async function handleRemove(file: UploadFileInfo) {
if (file.status !== 'finished') {
return;
return false;
}
const { error } = await fetchBatchDeleteOss([file.id]);
if (error) return;
if (error) return false;
window.$message?.success('删除成功');
return true;
}
</script>
<template>
<NUpload
v-bind="attrs"
v-model:file-list="fileList"
:action="`${baseURL}${action}`"
:data="data"
:headers="headers"
:max="max"
:accept="accept"
:multiple="max > 1"
directory-dnd
:default-upload="defaultUpload"
:list-type="uploadType === 'image' ? 'image-card' : 'text'"
:is-error-state="isErrorState"
@finish="handleFinish"
@error="handleError"
@before-upload="beforeUpload"
@remove="({ file }) => handleRemove(file)"
>
<NUploadDragger v-if="uploadType === 'file'">
<div class="mb-12px flex-center">
<SvgIcon icon="material-symbols:unarchive-outline" class="text-58px color-#d8d8db dark:color-#a1a1a2" />
</div>
<NText class="text-16px">点击或者拖动文件到该区域来上传</NText>
<NP v-if="showTip" depth="3" class="mt-8px text-center">
请上传
<template v-if="fileSize">
大小不超过
<b class="text-red-500">{{ fileSize }}MB</b>
</template>
<template v-if="accept">
且格式为
<b class="text-red-500">{{ accept.replaceAll(',', '/') }}</b>
</template>
的文件
</NP>
</NUploadDragger>
</NUpload>
<NP v-if="showTip && uploadType === 'image'" depth="3" class="mt-12px">
请上传
<template v-if="fileSize">
大小不超过
<b class="text-red-500">{{ fileSize }}MB</b>
</template>
<template v-if="accept">
且格式为
<b class="text-red-500">{{ accept.replaceAll(',', '/') }}</b>
</template>
的文件
</NP>
<div class="w-full flex-col">
<NUpload
v-bind="attrs"
v-model:file-list="fileList"
:action="`${baseURL}${action}`"
:data="data"
:headers="headers"
:max="max"
:accept="accept"
:multiple="max > 1"
directory-dnd
:default-upload="defaultUpload"
:list-type="uploadType === 'image' ? 'image-card' : 'text'"
:is-error-state="isErrorState"
@finish="handleFinish"
@error="handleError"
@before-upload="beforeUpload"
@remove="({ file }) => handleRemove(file)"
>
<NUploadDragger v-if="uploadType === 'file'">
<div class="mb-12px flex-center">
<SvgIcon icon="material-symbols:unarchive-outline" class="text-58px color-#d8d8db dark:color-#a1a1a2" />
</div>
<NText class="text-16px">点击或者拖动文件到该区域来上传</NText>
<NP v-if="showTip" depth="3" class="mt-8px text-center">
请上传
<template v-if="fileSize">
大小不超过
<b class="text-red-500">{{ fileSize }}MB</b>
</template>
<template v-if="accept">
且格式为
<b class="text-red-500">{{ accept.replaceAll(',', '/') }}</b>
</template>
的文件
</NP>
</NUploadDragger>
</NUpload>
<NP v-if="showTip && uploadType === 'image'" depth="3" class="mt-12px">
请上传
<template v-if="fileSize">
大小不超过
<b class="text-red-500">{{ fileSize }}MB</b>
</template>
<template v-if="accept">
且格式为
<b class="text-red-500">{{ accept.replaceAll(',', '/') }}</b>
</template>
的文件
</NP>
</div>
</template>
<style scoped></style>

View File

@ -1,18 +1,22 @@
<script setup lang="tsx">
import { useAttrs } from 'vue';
import { onMounted, useAttrs } from 'vue';
import type { TreeOption, TreeSelectProps } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { fetchGetMenuList } from '@/service/api/system';
import { handleTree } from '@/utils/common';
import SvgIcon from '@/components/custom/svg-icon.vue';
import { $t } from '@/locales';
defineOptions({ name: 'MenuTreeSelect' });
interface Props {
immediate?: boolean;
[key: string]: any;
}
defineProps<Props>();
const props = withDefaults(defineProps<Props>(), {
immediate: true
});
const value = defineModel<CommonType.IdType | null>('value', { required: false });
const options = defineModel<Api.System.MenuList>('options', { required: false, default: [] });
@ -35,7 +39,19 @@ async function getMenuList() {
endLoading();
}
getMenuList();
onMounted(() => {
if (props.immediate) {
getMenuList();
}
});
function renderLabel({ option }: { option: TreeOption }) {
let label = String(option.menuName);
if (label?.startsWith('route.') || label?.startsWith('menu.')) {
label = $t(label as App.I18n.I18nKey);
}
return <div>{label}</div>;
}
function renderPrefix({ option }: { option: TreeOption }) {
const renderLocalIcon = String(option.icon).startsWith('local-icon-');
@ -55,6 +71,8 @@ function renderPrefix({ option }: { option: TreeOption }) {
label-field="menuName"
:options="options"
:default-expanded-keys="[0]"
:render-tag="renderLabel"
:render-label="renderLabel"
:render-prefix="renderPrefix"
v-bind="attrs"
/>

View File

@ -4,6 +4,7 @@ import type { TreeOption, TreeSelectInst, TreeSelectProps } from 'naive-ui';
import { useBoolean } from '@sa/hooks';
import { fetchGetMenuTreeSelect } from '@/service/api/system';
import SvgIcon from '@/components/custom/svg-icon.vue';
import { $t } from '@/locales';
defineOptions({ name: 'MenuTree' });
@ -51,7 +52,6 @@ onMounted(() => {
}
});
// 添加 watch 监听 expandAll 的变化,options有值后计算expandedKeys
watch([expandAll, options], ([newVal]) => {
if (newVal) {
// 展开所有节点
@ -61,6 +61,14 @@ watch([expandAll, options], ([newVal]) => {
}
});
function renderLabel({ option }: { option: TreeOption }) {
let label = option.label;
if (label?.startsWith('route.') || label?.startsWith('menu.')) {
label = $t(label as App.I18n.I18nKey);
}
return <div>{label}</div>;
}
function renderPrefix({ option }: { option: TreeOption }) {
const renderLocalIcon = String(option.icon).startsWith('local-icon-');
let icon = renderLocalIcon ? undefined : String(option.icon ?? 'material-symbols:buttons-alt-outline-rounded');
@ -82,6 +90,21 @@ function getAllMenuIds(menu: Api.System.MenuList) {
return menuIds;
}
/** 获取所有叶子节点的 ID没有子节点的节点 */
function getLeafMenuIds(menu: Api.System.MenuList): CommonType.IdType[] {
const leafIds: CommonType.IdType[] = [];
menu.forEach(item => {
if (!item.children || item.children.length === 0) {
// 是叶子节点
leafIds.push(item.id!);
} else {
// 有子节点,递归获取子节点中的叶子节点
leafIds.push(...getLeafMenuIds(item.children));
}
});
return leafIds;
}
function handleCheckedTreeNodeAll(checked: boolean) {
if (checked) {
checkedKeys.value = getAllMenuIds(options.value);
@ -90,16 +113,30 @@ function handleCheckedTreeNodeAll(checked: boolean) {
checkedKeys.value = [];
}
function getCheckedMenuIds() {
function getCheckedMenuIds(isCascade: boolean = false) {
const menuIds = menuTreeRef.value?.getCheckedData()?.keys as string[];
const indeterminateData = menuTreeRef.value?.getIndeterminateData();
if (cascade.value) {
if (cascade.value || isCascade) {
const parentIds: string[] = indeterminateData?.keys.filter(item => !menuIds?.includes(String(item))) as string[];
menuIds?.push(...parentIds);
}
return menuIds;
}
watch(cascade, () => {
if (cascade.value) {
// 获取当前菜单树中的所有叶子节点ID
const allLeafIds = getLeafMenuIds(options.value);
// 筛选出当前选中项中的叶子节点
const selectedLeafIds = checkedKeys.value.filter(id => allLeafIds.includes(id));
// 重新设置选中状态为只包含叶子节点,让组件基于父子联动规则重新计算父节点状态
checkedKeys.value = selectedLeafIds;
return;
}
// 禁用父子联动时,将半选中的父节点也加入到选中列表
checkedKeys.value = getCheckedMenuIds(true);
});
defineExpose({
getCheckedMenuIds,
refresh: getMenuList
@ -135,6 +172,7 @@ defineExpose({
:loading="loading"
virtual-scroll
check-strategy="all"
:render-label="renderLabel"
:render-prefix="renderPrefix"
v-bind="attrs"
/>

View File

@ -0,0 +1,68 @@
<script setup lang="ts">
import { ref, useAttrs, watch } from 'vue';
import type { UploadFileInfo } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { fetchGetOssListByIds } from '@/service/api/system/oss';
import { isNotNull } from '@/utils/common';
import FileUpload from '@/components/custom/file-upload.vue';
defineOptions({
name: 'OssUpload'
});
const attrs = useAttrs();
const value = defineModel<string>('value', { default: '' });
const { loading, startLoading, endLoading } = useLoading();
const fileList = ref<UploadFileInfo[]>([]);
async function handleFetchOssList(ossIds: string[]) {
startLoading();
const { error, data } = await fetchGetOssListByIds(ossIds);
if (error) return;
fileList.value = data.map(item => ({
id: String(item.ossId),
url: item.url,
name: item.originalName,
status: 'finished'
}));
endLoading();
}
watch(
value,
async val => {
const ossIds = val?.split(',')?.filter(item => isNotNull(item)) || [];
const fileIds = new Set(fileList.value.filter(item => item.status === 'finished').map(item => item.id));
if (ossIds.every(item => fileIds.has(item))) {
return;
}
if (ossIds.length === 0) {
fileList.value = [];
return;
}
await handleFetchOssList(ossIds);
},
{ immediate: true }
);
watch(
fileList,
val => {
value.value = val
.filter(item => item.status === 'finished')
.map(item => item.id)
.join(',');
},
{ deep: true }
);
</script>
<template>
<NSpin v-if="loading" />
<FileUpload v-else v-bind="attrs" v-model:file-list="fileList" />
</template>
<style scoped></style>

View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import { Tinymce } from '@sa/tinymce';
import { getToken } from '@/store/modules/auth/shared';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { getServiceBaseURL } from '@/utils/service';
defineOptions({
name: 'TinymceEditor'
});
const value = defineModel<string | null>('value', { required: false, default: '' });
const appStore = useAppStore();
const themeStore = useThemeStore();
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
const headers: Record<string, string> = {
Authorization: `Bearer ${getToken()}`,
clientid: import.meta.env.VITE_APP_CLIENT_ID!
};
</script>
<template>
<Tinymce
v-model="value"
:lang="appStore.locale"
:is-dark="themeStore.darkMode"
:upload-url="`${baseURL}/resource/oss/upload`"
:upload-headers="headers"
/>
</template>
<style scoped></style>

View File

@ -76,7 +76,7 @@ export const genHtmlTypeRecord: Record<Api.Tool.HtmlType, string> = {
select: '下拉框',
radio: '单选框',
checkbox: '复选框',
datetime: '日期控件',
datetime: '日期时间控件',
imageUpload: '图片上传',
fileUpload: '文件上传',
editor: '富文本控件'

4
src/enum/business.ts Normal file
View File

@ -0,0 +1,4 @@
export enum AcceptType {
Image = '.jpg,.jpeg,.png,.gif,.bmp,.webp',
File = '.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.pdf,.zip,.rar,.7z'
}

View File

@ -3,6 +3,8 @@ import { storeToRefs } from 'pinia';
import { fetchGetDictDataByType } from '@/service/api/system';
import { useDictStore } from '@/store/modules/dict';
import { isNull } from '@/utils/common';
import { $t } from '@/locales';
export function useDict(dictType: string, immediate: boolean = true) {
const dictStore = useDictStore();
const { dictData: dictList } = storeToRefs(dictStore);
@ -19,6 +21,11 @@ export function useDict(dictType: string, immediate: boolean = true) {
}
const { data: dictData, error } = await fetchGetDictDataByType(dictType);
if (error) return;
dictData.forEach(dict => {
if (dict.dictLabel?.startsWith(`dict.${dictType}.`)) {
dict.dictLabel = $t(dict.dictLabel as App.I18n.I18nKey);
}
});
dictStore.setDict(dictType, dictData);
data.value = dictData;
}

View File

@ -1,114 +1,151 @@
import StreamSaver from 'streamsaver';
import { errorCodeRecord } from '@/constants/common';
import { localStg } from '@/utils/storage';
import { getServiceBaseURL } from '@/utils/service';
import { transformToURLSearchParams } from '@/utils/common';
interface RequestConfig {
method: 'GET' | 'POST';
url: string;
params?: Record<string, any>;
filename?: string;
contentType?: string;
}
export function useDownload() {
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
function downloadByData(data: BlobPart, filename: string, type: string = 'application/octet-stream') {
const blobData = [data];
const blob = new Blob(blobData, { type });
/** 获取通用请求头 */
const getCommonHeaders = (contentType = 'application/octet-stream') => ({
Authorization: `Bearer ${localStg.get('token')}`,
Clientid: import.meta.env.VITE_APP_CLIENT_ID!,
'Content-Type': contentType
});
/** 通用下载方法 */
function downloadByData(data: BlobPart, filename: string, type = 'application/octet-stream') {
const blob = new Blob([data], { type });
const blobURL = window.URL.createObjectURL(blob);
const tempLink = document.createElement('a');
tempLink.style.display = 'none';
tempLink.href = blobURL;
tempLink.setAttribute('download', filename);
const tempLink = Object.assign(document.createElement('a'), {
style: { display: 'none' },
href: blobURL,
download: filename
});
if (typeof tempLink.download === 'undefined') {
tempLink.setAttribute('target', '_blank');
}
document.body.appendChild(tempLink);
tempLink.click();
document.body.removeChild(tempLink);
window.URL.revokeObjectURL(blobURL);
}
function download(url: string, params: any, fileName: string) {
window.$loading?.startLoading('正在下载数据,请稍候...');
const token = localStg.get('token');
const clientId = import.meta.env.VITE_APP_CLIENT_ID;
const now = Date.now();
const searchParams = new FormData();
if (params) {
Object.keys(params).forEach(key => {
if (params[key] !== null && params[key] !== undefined) {
searchParams.append(key, params[key]);
}
});
/** 流式下载 */
async function downloadByStream(
readableStream: ReadableStream<Uint8Array>,
filename: string,
contentLength?: number
): Promise<void> {
window.$loading?.endLoading();
const fileStream = StreamSaver.createWriteStream(filename, { size: contentLength });
if (window.WritableStream && readableStream?.pipeTo) {
await readableStream.pipeTo(fileStream);
window.$message?.success('下载完成');
return;
}
fetch(`${baseURL}${url}?t=${now}`, {
method: 'post',
body: searchParams,
headers: {
Authorization: `Bearer ${token}`,
Clientid: clientId!,
'Content-Type': 'application/octet-stream'
}
})
.then(async response => {
if (response.headers.get('Content-Type')?.includes('application/json')) {
const res = await response.json();
const code = res.code as CommonType.ErrorCode;
throw new Error(errorCodeRecord[code] || res.msg || errorCodeRecord.default);
}
return response.blob();
})
.then(data => downloadByData(data, fileName, 'application/zip'))
.catch(err => window.$message?.error(err.message))
.finally(() => window.$loading?.endLoading());
// 降级处理
const writer = fileStream.getWriter();
const reader = readableStream.getReader();
const pump = async (): Promise<void> => {
const { done, value } = await reader.read();
if (done) return writer.close();
await writer.write(value);
return pump();
};
await pump();
}
function oss(ossId: CommonType.IdType) {
window.$loading?.startLoading('正在下载数据,请稍候...');
const token = localStg.get('token');
const clientId = import.meta.env.VITE_APP_CLIENT_ID;
const url = `/resource/oss/download/${ossId}`;
const now = Date.now();
let fileName = String(`${ossId}-${now}`);
fetch(`${baseURL}${url}?t=${now}`, {
method: 'get',
headers: {
Authorization: `Bearer ${token}`,
Clientid: clientId!,
'Content-Type': 'application/octet-stream'
}
})
.then(async response => {
fileName = String(response.headers.get('Download-Filename'));
return response.blob();
})
.then(data => downloadByData(data, fileName))
.catch(err => window.$message?.error(err.message))
.finally(() => window.$loading?.endLoading());
/** 处理响应 */
async function handleResponse(response: Response) {
if (response.headers.get('Content-Type')?.includes('application/json')) {
const res = await response.json();
const code = res.code as CommonType.ErrorCode;
throw new Error(errorCodeRecord[code] || res.msg || errorCodeRecord.default);
}
}
function zip(url: string, fileName: string) {
/** 核心下载逻辑 */
async function executeDownload(config: RequestConfig): Promise<void> {
const { method, url, params, filename, contentType } = config;
const timestamp = Date.now();
const fullUrl = `${baseURL}${url}${url.includes('?') ? '&' : '?'}t=${timestamp}`;
window.$loading?.startLoading('正在下载数据,请稍候...');
const token = localStg.get('token');
const clientId = import.meta.env.VITE_APP_CLIENT_ID;
const now = Date.now();
fetch(`${baseURL}${url}${url.includes('?') ? '&' : '?'}t=${now}`, {
method: 'get',
headers: {
Authorization: `Bearer ${token}`,
Clientid: clientId!,
'Content-Type': 'application/octet-stream'
try {
const requestOptions: RequestInit = {
method,
headers: getCommonHeaders(contentType)
};
if (method === 'POST' && params) {
requestOptions.body = transformToURLSearchParams(params);
requestOptions.headers = {
...requestOptions.headers,
'Content-Type': 'application/x-www-form-urlencoded'
};
}
})
.then(async response => {
if (response.headers.get('Content-Type')?.includes('application/json')) {
const res = await response.json();
const code = res.code as CommonType.ErrorCode;
throw new Error(errorCodeRecord[code] || res.msg || errorCodeRecord.default);
}
return response.blob();
})
.then(data => downloadByData(data, fileName, 'application/zip'))
.catch(err => window.$message?.error(err.message))
.finally(() => window.$loading?.endLoading());
const response = await fetch(fullUrl, requestOptions);
await handleResponse(response);
const finalFilename = filename || response.headers.get('Download-Filename') || `download-${timestamp}`;
if (response.body) {
const contentLength = Number(response.headers.get('Content-Length'));
await downloadByStream(response.body, finalFilename, contentLength);
return;
}
const responseContentType = response.headers.get('Content-Type');
const mainType = responseContentType?.split(';')[0]?.trim() || 'application/octet-stream';
downloadByData(await response.blob(), finalFilename, mainType);
} catch (error: any) {
window.$message?.error(error.message);
} finally {
window.$loading?.endLoading();
}
}
/** 公共下载接口 */
const download = (url: string, params: Record<string, any>, filename: string) =>
executeDownload({ method: 'POST', url, params, filename });
/** OSS文件下载 */
const oss = (ossId: CommonType.IdType) =>
executeDownload({
method: 'GET',
url: `/resource/oss/download/${ossId}`
});
/** ZIP文件下载 */
const zip = (url: string, filename: string) =>
executeDownload({
method: 'GET',
url,
filename,
contentType: 'application/octet-stream'
});
return {
oss,
zip,

View File

@ -39,7 +39,7 @@ const toGitee = () => {
<NCard
size="small"
:bordered="false"
class="w-340px"
class="w-345px"
header-class="p-0"
:segmented="{ content: true, footer: 'soft' }"
>
@ -63,7 +63,7 @@ const toGitee = () => {
一键已读
</NTooltip>
</template>
<div>
<NScrollbar class="h-260px">
<template v-if="state?.notices?.length">
<template v-for="(message, index) in state?.notices" :key="index">
<NDivider v-show="index !== 0" />
@ -81,7 +81,7 @@ const toGitee = () => {
</template>
</template>
<NEmpty v-else class="h-180px flex-center" />
</div>
</NScrollbar>
<template #footer>
<div class="flex items-center justify-end">
<NButton type="primary" size="small" @click="toGitee">前往 Gitee</NButton>

View File

@ -45,7 +45,7 @@ const tenantId = ref<CommonType.IdType>(authStore.userInfo?.user?.tenantId || '0
</div>
<div class="h-full flex-y-center justify-end">
<TenantSelect v-if="!appStore.isMobile" v-model:value="tenantId" class="mr-12px w-150px" />
<GlobalSearch v-if="themeStore.header.globalSearch.visible" />
<GlobalSearch v-if="themeStore.header.globalSearch.visible && !appStore.isMobile" />
<MessageButton />
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />
<LangSwitch
@ -59,7 +59,7 @@ const tenantId = ref<CommonType.IdType>(authStore.userInfo?.user?.tenantId || '0
:is-dark="themeStore.darkMode"
@switch="themeStore.toggleThemeScheme"
/>
<ThemeButton />
<ThemeButton v-if="!appStore.isMobile" />
<UserAvatar />
</div>
</DarkModeContainer>

View File

@ -17,8 +17,8 @@ withDefaults(defineProps<Props>(), {
<template>
<RouterLink to="/" class="w-full flex-center nowrap-hidden">
<SystemLogo class="w-32px text-primary" />
<h2 v-show="showTitle" class="pl-8px text-16px text-primary font-bold transition duration-300 ease-in-out">
<SystemLogo class="text-30px text-primary" />
<h2 v-show="showTitle" class="pl-12px text-16px text-primary font-bold transition duration-300 ease-in-out">
{{ $t('system.title') }}
</h2>
</RouterLink>

View File

@ -117,7 +117,10 @@ const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wra
<SettingItem key="8" :label="$t('theme.watermark.visible')">
<NSwitch v-model:value="themeStore.watermark.visible" />
</SettingItem>
<SettingItem v-if="themeStore.watermark.visible" key="8-1" :label="$t('theme.watermark.text')">
<SettingItem v-if="themeStore.watermark.visible" key="8-1" :label="$t('theme.watermark.enableUserName')">
<NSwitch v-model:value="themeStore.watermark.enableUserName" />
</SettingItem>
<SettingItem v-if="themeStore.watermark.visible" key="8-2" :label="$t('theme.watermark.text')">
<NInput
v-model:value="themeStore.watermark.text"
autosize

View File

@ -1,6 +1,6 @@
const local: App.I18n.Schema = {
system: {
title: 'RuoYi Vue Plus',
title: 'RuoYi Plus Soybean',
updateTitle: 'System Version Update Notification',
updateContent: 'A new version of the system has been detected. Do you want to refresh the page immediately?',
updateConfirm: 'Refresh immediately',
@ -166,7 +166,8 @@ const local: App.I18n.Schema = {
},
watermark: {
visible: 'Watermark Full Screen Visible',
text: 'Watermark Text'
text: 'Watermark Text',
enableUserName: 'Enable User Name Watermark'
},
tablePropsTitle: 'Table Props',
table: {
@ -234,6 +235,95 @@ const local: App.I18n.Schema = {
exception_404: '404',
exception_500: '500'
},
menu: {
system_tenant: 'Tenant Management',
system_log: 'Log Management',
'monitor_snail-job': 'Job Management',
monitor_admin: 'Admin Monitor'
},
dict: {
sys_user_sex: {
male: 'Male',
female: 'Female',
unknown: 'Unknown'
},
sys_show_hide: {
show: 'Show',
hide: 'Hide'
},
sys_normal_disable: {
normal: 'Normal',
disable: 'Disable'
},
sys_yes_no: {
yes: 'Yes',
no: 'No'
},
sys_notice_type: {
notice: 'Notice',
announcement: 'Announcement'
},
sys_notice_status: {
normal: 'Normal',
close: 'Close'
},
sys_oper_type: {
insert: 'Insert',
update: 'Update',
delete: 'Delete',
grant: 'Grant',
export: 'Export',
import: 'Import',
force: 'Force',
gencode: 'Generate Code',
clean: 'Clean Data',
other: 'Other'
},
sys_common_status: {
success: 'Success',
fail: 'Fail'
},
sys_grant_type: {
password: 'Password Auth',
sms: 'SMS Auth',
email: 'Email Auth',
miniapp: 'Mini App Auth',
social: 'Social Auth'
},
sys_device_type: {
pc: 'PC',
android: 'Android',
ios: 'iOS',
miniapp: 'Mini App'
},
wf_business_status: {
revoked: 'Revoked',
draft: 'Draft',
pending: 'Pending',
completed: 'Completed',
cancelled: 'Cancelled',
returned: 'Returned',
terminated: 'Terminated'
},
wf_form_type: {
custom_form: 'Custom Form',
dynamic_form: 'Dynamic Form'
},
wf_task_status: {
revoke: 'Revoke',
pass: 'Pass',
pending_review: 'Pending Review',
cancel: 'Cancel',
return: 'Return',
terminate: 'Terminate',
transfer: 'Transfer',
delegate: 'Delegate',
copy: 'Copy',
add_sign: 'Add Sign',
minus_sign: 'Minus Sign',
timeout: 'Timeout'
}
},
page: {
login: {
common: {

View File

@ -1,6 +1,6 @@
const local: App.I18n.Schema = {
system: {
title: 'RuoYi Vue Plus',
title: 'RuoYi Plus Soybean',
updateTitle: '系统版本更新通知',
updateContent: '检测到系统有新版本发布,是否立即刷新页面?',
updateConfirm: '立即刷新',
@ -166,7 +166,8 @@ const local: App.I18n.Schema = {
},
watermark: {
visible: '显示全屏水印',
text: '水印文本'
text: '水印文本',
enableUserName: '启用用户名水印'
},
tablePropsTitle: '表格配置',
table: {
@ -221,7 +222,7 @@ const local: App.I18n.Schema = {
system_notice: '通知公告',
'social-callback': '单点登录回调',
system_oss: '文件管理',
'system_oss-config': 'OSS配置',
'system_oss-config': 'OSS 配置',
monitor_cache: '缓存监控',
monitor_online: '在线用户',
'user-center': '个人中心',
@ -234,6 +235,95 @@ const local: App.I18n.Schema = {
exception_404: '404',
exception_500: '500'
},
menu: {
system_tenant: '租户管理',
system_log: '日志管理',
'monitor_snail-job': '任务调度中心',
monitor_admin: 'Admin 监控'
},
dict: {
sys_user_sex: {
male: '男',
female: '女',
unknown: '未知'
},
sys_show_hide: {
show: '显示',
hide: '隐藏'
},
sys_normal_disable: {
normal: '正常',
disable: '停用'
},
sys_yes_no: {
yes: '是',
no: '否'
},
sys_notice_type: {
notice: '通知',
announcement: '公告'
},
sys_notice_status: {
normal: '正常',
close: '关闭'
},
sys_oper_type: {
insert: '新增',
update: '修改',
delete: '删除',
grant: '授权',
export: '导出',
import: '导入',
force: '强退',
gencode: '生成代码',
clean: '清空数据',
other: '其他'
},
sys_common_status: {
success: '成功',
fail: '失败'
},
sys_grant_type: {
password: '密码认证',
sms: '短信认证',
email: '邮件认证',
miniapp: '小程序认证',
social: '三方登录认证'
},
sys_device_type: {
pc: 'PC',
android: '安卓',
ios: 'iOS',
miniapp: '小程序'
},
wf_business_status: {
revoked: '已撤销',
draft: '草稿',
pending: '待审核',
completed: '已完成',
cancelled: '已作废',
returned: '已退回',
terminated: '已终止'
},
wf_form_type: {
custom_form: '自定义表单',
dynamic_form: '动态表单'
},
wf_task_status: {
revoke: '撤销',
pass: '通过',
pending_review: '待审核',
cancel: '作废',
return: '退回',
terminate: '终止',
transfer: '转办',
delegate: '委托',
copy: '抄送',
add_sign: '加签',
minus_sign: '减签',
timeout: '超时'
}
},
page: {
login: {
common: {

View File

@ -95,7 +95,6 @@ async function getHtmlBuildTime(): Promise<string | null> {
const res = await fetch(`${baseUrl}index.html?time=${Date.now()}`);
if (!res.ok) {
console.error('getHtmlBuildTime error:', res.status, res.statusText);
return null;
}
@ -103,7 +102,7 @@ async function getHtmlBuildTime(): Promise<string | null> {
const match = html.match(/<meta name="buildTime" content="(.*)">/);
return match?.[1] || null;
} catch (error) {
console.error('getHtmlBuildTime error:', error);
window.console.error('getHtmlBuildTime error:', error);
return null;
}
}

View File

@ -3,8 +3,8 @@ import { getRgb } from '@sa/color';
import { DARK_CLASS } from '@/constants/app';
import { localStg } from '@/utils/storage';
import { toggleHtmlClass } from '@/utils/common';
import systemLogo from '@/assets/imgs/logo.png';
import { $t } from '@/locales';
import '@/styles/scss/loading.scss';
export function setupLoading() {
const app = document.getElementById('app');
@ -21,12 +21,11 @@ export function setupLoading() {
const loading = `
<div class="fixed-center flex-col bg-layout" style="${primaryColor}">
<div class="w-120px h-120px my-36px">
<div class="relative h-full animate-spin">
<img src="${systemLogo}" width="120" />
</div>
<div class="my-52px h-120px w-120px">
<!-- From Uiverse.io by SchawnnahJ -->
<div class="loader"></div>
</div>
<h2 class="text-28px font-500 text-primary">${$t('system.title')}</h2>
<h2 class="text-30px text-primary-400 font-500">${$t('system.title')}</h2>
</div>`;
if (app) {

View File

@ -1,49 +0,0 @@
/* eslint-disable no-console */
/** 后台返回的路由动态生成name 解决缓存问题 感谢 @fourteendp 详见 https://github.com/vbenjs/vue-vben-admin/issues/3927 */
import type { Component } from 'vue';
import { defineComponent, h } from 'vue';
interface Options {
name?: string;
}
export type RouteComponentLoader = () => Promise<any>;
/**
* 作用相当于给组件包了一层 & 设置name 解决keepAlive问题
*
* @param loader 导入的路由
* @param options options
* @returns components
*/
export function createCustomNameComponent(
loader: RouteComponentLoader,
options: Options = {}
): () => Promise<Component> {
const { name } = options;
let component: Component | null = null;
const load = async () => {
try {
const { default: loadedComponent } = await loader();
component = loadedComponent;
} catch (error) {
console.error(`Cannot resolve component ${name}, error:`, error);
}
};
return async () => {
if (!component) {
await load();
}
return Promise.resolve(
defineComponent({
name,
render() {
return h(component as Component);
}
})
);
};
}

View File

@ -20,6 +20,17 @@ export function fetchForceLogout(tokenId: string) {
method: 'delete'
});
}
/**
* 强退当前在线设备
*
* @param tokenId - 令牌ID
*/
export function fetchKickOutCurrentDevice(tokenId: string) {
return request<boolean>({
url: `/monitor/online/myself/${tokenId}`,
method: 'delete'
});
}
/** 获取在线设备列表 */
export function fetchGetOnlineDeviceList(params?: Api.Monitor.OnlineUserSearchParams) {

View File

@ -1,4 +1,3 @@
import type { AxiosRequestConfig, GenericAbortSignal } from 'axios';
import { request } from '@/service/request';
/** 获取文件管理列表 */
@ -18,36 +17,10 @@ export function fetchBatchDeleteOss(ossIds: CommonType.IdType[]) {
});
}
/** Axios上传进度事件 */
export type AxiosProgressEvent = AxiosRequestConfig['onUploadProgress'];
/** 默认上传结果 */
export interface UploadResult {
url: string;
fileName: string;
ossId: string;
}
export interface UploadApiOptions {
onUploadProgress?: AxiosProgressEvent;
signal?: GenericAbortSignal;
}
/** 上传文件接口 */
export function uploadApi(file: File | Blob, options?: UploadApiOptions) {
const { onUploadProgress, signal } = options ?? {};
const formData = new FormData();
formData.append('file', file);
return request<UploadResult>({
url: '/resource/oss/upload',
method: 'post',
data: formData,
onUploadProgress,
headers: {
'Content-Type': 'multipart/form-data'
},
signal
// 查询OSS对象基于id串
export function fetchGetOssListByIds(ossIds: CommonType.IdType[]) {
return request<Api.System.Oss[]>({
url: `/resource/oss/listByIds/${ossIds.join(',')}`,
method: 'get'
});
}

View File

@ -36,6 +36,15 @@ export function fetchUpdateRoleStatus(data: Api.System.RoleOperateParams) {
});
}
/** 修改角色数据权限 */
export function fetchUpdateRoleDataScope(data: Api.System.RoleOperateParams) {
return request<boolean>({
url: '/system/role/dataScope',
method: 'put',
data
});
}
/** 批量删除角色信息 */
export function fetchBatchDeleteRole(roleIds: CommonType.IdType[]) {
return request<boolean>({

View File

@ -16,7 +16,7 @@ export function fetchCreateTenant(data: Api.System.TenantOperateParams) {
method: 'post',
headers: {
isEncrypt: true,
repeatSubmit: true
repeatSubmit: false
},
data
});

View File

@ -81,7 +81,8 @@ export function fetchResetUserPassword(userId: CommonType.IdType, password: stri
url: '/system/user/resetPwd',
method: 'put',
headers: {
isEncrypt: true
isEncrypt: true,
repeatSubmit: false
},
data: { userId, password }
});
@ -118,6 +119,9 @@ export function fetchUpdateUserPassword(data: Api.System.UserPasswordOperatePara
return request<boolean>({
url: '/system/user/profile/updatePwd',
method: 'put',
headers: {
isEncrypt: true
},
data
});
}

View File

@ -80,17 +80,21 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
// prevent the user from refreshing the page
window.addEventListener('beforeunload', handleLogout);
window.$dialog?.warning({
title: '系统提示',
content: '登录状态已过期,您可以继续留在该页面,或者重新登录',
positiveText: '重新登录',
negativeText: '取消',
maskClosable: false,
closeOnEsc: false,
onPositiveClick() {
logoutAndCleanup();
}
});
if (!window.location.pathname?.startsWith('/login')) {
window.$dialog?.warning({
title: '系统提示',
content: '登录状态已过期,您可以继续留在该页面,或者重新登录',
positiveText: '重新登录',
negativeText: '取消',
maskClosable: false,
closeOnEsc: false,
onPositiveClick() {
logoutAndCleanup();
}
});
request.cancelAllRequest();
}
return null;
}

View File

@ -12,6 +12,7 @@ import { clearAuthStorage, getToken } from './shared';
export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
const route = useRoute();
const authStore = useAuthStore();
const routeStore = useRouteStore();
const tabStore = useTabStore();
const { toLogin, redirectFromLogin } = useRouterPush(false);
@ -37,8 +38,6 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
/** Reset auth store */
async function resetStore() {
const authStore = useAuthStore();
recordUserId();
clearAuthStorage();

View File

@ -1,11 +1,17 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
import { $t } from '@/locales';
export const useDictStore = defineStore('dict', () => {
const dictData = ref<{ [key: string]: Api.System.DictData[] }>({});
const getDict = (key: string) => {
return dictData.value[key];
return dictData.value[key]?.map(item => ({
...item,
dictLabel: item.dictLabel?.startsWith(`dict.${item.dictType}.`)
? $t(item.dictLabel as App.I18n.I18nKey)
: item.dictLabel
}));
};
const setDict = (key: string, dict: Api.System.DictData[]) => {

View File

@ -42,16 +42,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
const authRouteMode = ref(import.meta.env.VITE_AUTH_ROUTE_MODE);
/** Home route key */
const routeHome = ref(import.meta.env.VITE_ROUTE_HOME);
/**
* Set route home
*
* @param routeKey Route key
*/
function setRouteHome(routeKey: LastLevelRouteKey) {
routeHome.value = routeKey;
}
const routeHome = ref(import.meta.env.VITE_ROUTE_HOME || 'home');
/** constant routes */
const constantRoutes = shallowRef<ElegantConstRoute[]>([]);
@ -103,6 +94,9 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
// eslint-disable-next-line complexity
function parseRouter(route: ElegantConstRoute, parent?: ElegantConstRoute) {
route.meta = route.meta ? route.meta : { title: route.name };
if (route.meta.title.startsWith('route.') || route.meta.title.startsWith('menu.')) {
route.meta.i18nKey = route.meta.title as App.I18n.I18nKey;
}
const isLayout = route.component === 'Layout';
const isFramePage = route.component === 'FrameView';
const isParentLayout = route.component === 'ParentView';
@ -300,9 +294,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
handleConstantAndAuthRoutes();
setRouteHome('home');
handleUpdateRootRouteRedirect('home');
handleUpdateRootRouteRedirect(routeHome.value);
setIsInitAuthRoute(true);
} else {

View File

@ -100,7 +100,9 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
const removedTabRouteKey = tabs.value[removeTabIndex].routeKey;
const isRemoveActiveTab = activeTabId.value === tabId;
const nextTab = tabs.value[removeTabIndex + 1] || homeTab.value;
// if remove the last tab, then switch to the second last tab
const nextTab = tabs.value[removeTabIndex + 1] || tabs.value[removeTabIndex - 1] || homeTab.value;
// remove tab
tabs.value.splice(removeTabIndex, 1);

View File

@ -252,6 +252,9 @@ export function getNaiveTheme(colors: App.Theme.ThemeColor, recommended = false)
},
Tag: {
borderRadius: '6px'
},
Button: {
textColorPrimary: '#ffffff'
}
};

View File

@ -10,4 +10,5 @@ body,
html {
overflow-x: hidden;
color: rgb(var(--base-text-color));
}

View File

@ -0,0 +1,105 @@
@use 'sass:math';
$base-size: 100px;
$em-to-px: math.div($base-size, 2.5);
$loader-color-1: rgb(var(--error-color) / 75%);
$loader-color-2: rgb(var(--primary-color) / 75%);
$loader-color-3: rgb(var(--success-color) / 75%);
$loader-color-4: rgb(var(--warning-color) / 75%);
@function loader-size($em-value) {
@return $em-to-px * $em-value;
}
/* From Uiverse.io by SchawnnahJ */
.loader {
position: relative;
width: loader-size(2.5);
height: loader-size(2.5);
transform: rotate(165deg);
}
.loader:before,
.loader:after {
content: '';
position: absolute;
top: 50%;
left: 50%;
display: block;
width: loader-size(0.5);
height: loader-size(0.5);
border-radius: loader-size(0.25);
transform: translate(-50%, -50%);
}
.loader:before {
animation: before8 2s infinite;
}
.loader:after {
animation: after6 2s infinite;
}
@keyframes before8 {
0% {
width: loader-size(0.5);
box-shadow:
loader-size(1) loader-size(-0.5) $loader-color-1,
loader-size(-1) loader-size(0.5) $loader-color-2;
}
35% {
width: loader-size(2.5);
box-shadow:
0 loader-size(-0.5) $loader-color-1,
0 loader-size(0.5) $loader-color-2;
}
70% {
width: loader-size(0.5);
box-shadow:
loader-size(-1) loader-size(-0.5) $loader-color-1,
loader-size(1) loader-size(0.5) $loader-color-2;
}
100% {
box-shadow:
loader-size(1) loader-size(-0.5) $loader-color-1,
loader-size(-1) loader-size(0.5) $loader-color-2;
}
}
@keyframes after6 {
0% {
height: loader-size(0.5);
box-shadow:
loader-size(0.5) loader-size(1) $loader-color-3,
loader-size(-0.5) loader-size(-1) $loader-color-4;
}
35% {
height: loader-size(2.5);
box-shadow:
loader-size(0.5) 0 $loader-color-3,
loader-size(-0.5) 0 $loader-color-4;
}
70% {
height: loader-size(0.5);
box-shadow:
loader-size(0.5) loader-size(-1) $loader-color-3,
loader-size(-0.5) loader-size(1) $loader-color-4;
}
100% {
box-shadow:
loader-size(0.5) loader-size(1) $loader-color-3,
loader-size(-0.5) loader-size(-1) $loader-color-4;
}
}
.loader {
position: absolute;
top: calc(50% - #{math.div(loader-size(2.5), 2)});
left: calc(50% - #{math.div(loader-size(2.5), 2)});
}

View File

@ -58,7 +58,8 @@ export const themeSettings: App.Theme.ThemeSetting = {
},
watermark: {
visible: import.meta.env.VITE_WATERMARK === 'Y',
text: 'RuoYi-Vue-Plus'
text: 'RuoYi-Vue-Plus',
enableUserName: false
},
table: {
bordered: true,

View File

@ -152,9 +152,10 @@ declare namespace Api {
type UserProfileOperateParams = CommonType.RecordNullable<Pick<User, 'nickName' | 'email' | 'phonenumber' | 'sex'>>;
/** user password operate params */
type UserPasswordOperateParams = CommonType.RecordNullable<
Pick<User, 'userId' | 'password'> & { newPassword: string }
>;
type UserPasswordOperateParams = CommonType.RecordNullable<{
oldPassword: string;
newPassword: string;
}>;
/** user info */
type UserInfo = {
@ -372,7 +373,15 @@ declare namespace Api {
type DictDataOperateParams = CommonType.RecordNullable<
Pick<
Api.System.DictData,
'dictCode' | 'dictSort' | 'dictLabel' | 'dictValue' | 'dictType' | 'cssClass' | 'listClass' | 'remark'
| 'dictCode'
| 'dictSort'
| 'dictLabel'
| 'dictValue'
| 'dictType'
| 'cssClass'
| 'listClass'
| 'isDefault'
| 'remark'
>
>;

View File

@ -112,6 +112,8 @@ declare namespace App {
visible: boolean;
/** Watermark text */
text: string;
/** Whether to use user name as watermark text */
enableUserName: boolean;
};
table: {
/** Whether to show the table border */
@ -446,6 +448,7 @@ declare namespace App {
watermark: {
visible: string;
text: string;
enableUserName: string;
};
tablePropsTitle: string;
table: {
@ -467,6 +470,8 @@ declare namespace App {
};
};
route: Record<I18nRouteKey, string>;
menu: Record<string, string>;
dict: Record<string, Record<string, string>>;
page: {
common: {
id: string;

View File

@ -34,6 +34,7 @@ declare module 'vue' {
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
IconLocalBanner: typeof import('~icons/local/banner')['default']
IconLocalLogo: typeof import('~icons/local/logo')['default']
'IconMaterialSymbols:add': typeof import('~icons/material-symbols/add')['default']
'IconMaterialSymbols:deleteOutline': typeof import('~icons/material-symbols/delete-outline')['default']
'IconMaterialSymbols:downloadRounded': typeof import('~icons/material-symbols/download-rounded')['default']
@ -45,6 +46,7 @@ declare module 'vue' {
IconMaterialSymbolsAddRounded: typeof import('~icons/material-symbols/add-rounded')['default']
IconMaterialSymbolsDeleteOutline: typeof import('~icons/material-symbols/delete-outline')['default']
IconMaterialSymbolsDriveFileRenameOutlineOutline: typeof import('~icons/material-symbols/drive-file-rename-outline-outline')['default']
'IconMdi:github': typeof import('~icons/mdi/github')['default']
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
@ -52,6 +54,7 @@ declare module 'vue' {
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
'IconQuill:collapse': typeof import('~icons/quill/collapse')['default']
'IconQuill:expand': typeof import('~icons/quill/expand')['default']
'IconSimpleIcons:gitee': typeof import('~icons/simple-icons/gitee')['default']
IconUilSearch: typeof import('~icons/uil/search')['default']
JsonPreview: typeof import('./../components/custom/json-preview.vue')['default']
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
@ -129,6 +132,7 @@ declare module 'vue' {
NUpload: typeof import('naive-ui')['NUpload']
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
NWatermark: typeof import('naive-ui')['NWatermark']
OssUpload: typeof import('./../components/custom/oss-upload.vue')['default']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
PostSelect: typeof import('./../components/custom/post-select.vue')['default']
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
@ -145,6 +149,7 @@ declare module 'vue' {
TableSiderLayout: typeof import('./../components/advanced/table-sider-layout.vue')['default']
TenantSelect: typeof import('./../components/custom/tenant-select.vue')['default']
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
TinymceEditor: typeof import('./../components/custom/tinymce-editor.vue')['default']
UserSelect: typeof import('./../components/custom/user-select.vue')['default']
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
}

View File

@ -25,7 +25,7 @@ declare namespace Env {
*
* This prefix is start with the icon prefix
*/
readonly VITE_ICON_LOCAL_PREFIX: 'local-icon';
readonly VITE_ICON_LOCAL_PREFIX: 'icon-local';
/** backend service base url */
readonly VITE_SERVICE_BASE_URL: string;
/**

View File

@ -1,3 +1,4 @@
import { AcceptType } from '@/enum/business';
import { $t } from '@/locales';
/**
* Transform record to option
@ -87,8 +88,7 @@ export function isNull(value: any) {
/** 判断是否为图片类型 */
export function isImage(suffix: string) {
const imgSuffixList = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
return imgSuffixList.includes(suffix.toLowerCase());
return AcceptType.Image.split(',').includes(suffix.toLowerCase());
}
/**
@ -163,3 +163,31 @@ export const handleTree = <T extends Record<string, any>>(data: T[], config: Com
return tree;
};
/**
* 将对象转换为 URLSearchParams
*
* @param obj
*/
export function transformToURLSearchParams(obj: Record<string, any>, excludeKeys: string[] = []) {
const searchParams = new URLSearchParams();
if (!isNotNull(obj)) {
return searchParams;
}
Object.entries(obj).forEach(([key, value]) => {
if (excludeKeys.includes(key)) {
return;
}
if (typeof value === 'object') {
transformToURLSearchParams(value).forEach((v, k) => {
searchParams.append(`${key}[${k}]`, v);
});
return;
}
if (!isNotNull(value)) {
return;
}
searchParams.append(key, value);
});
return searchParams;
}

View File

@ -1,21 +1,9 @@
<script setup lang="ts">
import { onActivated, onMounted } from 'vue';
interface Props {
url: string;
}
defineProps<Props>();
onMounted(() => {
// eslint-disable-next-line no-console
console.log('mounted');
});
onActivated(() => {
// eslint-disable-next-line no-console
console.log('activated');
});
</script>
<template>

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { Component } from 'vue';
import { getPaletteColorByNumber, mixColor } from '@sa/color';
import { loginModuleRecord } from '@/constants/app';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import loginBackground from '@/assets/svg-icon/login-background.svg';
import { $t } from '@/locales';
import PwdLogin from './modules/pwd-login.vue';
import CodeLogin from './modules/code-login.vue';
@ -36,29 +36,23 @@ const moduleMap: Record<UnionKey.LoginModule, LoginModule> = {
};
const activeModule = computed(() => moduleMap[props.module || 'pwd-login']);
const bgThemeColor = computed(() =>
themeStore.darkMode ? getPaletteColorByNumber(themeStore.themeColor, 600) : themeStore.themeColor
);
const bgColor = computed(() => {
const COLOR_WHITE = '#ffffff';
const ratio = themeStore.darkMode ? 0.5 : 0.2;
return mixColor(COLOR_WHITE, themeStore.themeColor, ratio);
});
</script>
<template>
<div class="relative size-full flex-center overflow-hidden" :style="{ backgroundColor: bgColor }">
<WaveBg :theme-color="bgThemeColor" />
<NCard :bordered="false" class="relative z-4 w-auto rd-12px">
<div class="w-400px lt-sm:w-300px">
<div class="relative min-h-screen w-full flex flex-wrap">
<div class="hidden min-h-screen w-50% bg-primary-100 lg:block dark:bg-primary-800">
<div class="size-full flex-center">
<img class="w-60% sm:w-80%" :src="loginBackground" />
</div>
</div>
<div class="w-full flex-col-center px-24px py-32px lg:w-50%">
<div class="mx-auto max-w-464px w-full">
<header class="flex-y-center justify-between">
<SystemLogo class="w-64px text-primary lt-sm:text-48px" />
<h3 class="text-28px text-primary font-500 lt-sm:text-22px">{{ $t('system.title') }}</h3>
<div class="i-flex-col">
<div class="flex-y-center gap-16px">
<SystemLogo class="text-30px text-primary sm:text-42px" />
<h3 class="text-24px text-primary font-500 sm:text-32px">{{ $t('system.title') }}</h3>
</div>
<div class="flex-y-center">
<ThemeSchemaSwitch
:theme-schema="themeStore.themeScheme"
:show-tooltip="false"
@ -70,20 +64,20 @@ const bgColor = computed(() => {
:lang="appStore.locale"
:lang-options="appStore.localeOptions"
:show-tooltip="false"
class="text-20px lt-sm:text-18px"
@change-lang="appStore.changeLocale"
/>
</div>
</header>
<main class="pt-24px">
<h3 class="text-18px text-primary font-medium">{{ $t(activeModule.label) }}</h3>
<div class="pt-24px">
<div>
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
<component :is="activeModule.component" />
</Transition>
</div>
</main>
</div>
</NCard>
</div>
</div>
</template>

View File

@ -121,63 +121,89 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
</script>
<template>
<NForm
ref="formRef"
:model="model"
:rules="rules"
size="large"
:show-label="false"
@keyup.enter="() => !authStore.loginLoading && handleSubmit()"
>
<NFormItem v-if="tenantEnabled" path="tenantId">
<NSelect
v-model:value="model.tenantId"
placeholder="请选择租户"
:options="tenantOption"
:loading="tenantLoading"
/>
</NFormItem>
<NFormItem path="username">
<NInput v-model:value="model.username" :placeholder="$t('page.login.common.userNamePlaceholder')" />
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
</NFormItem>
<NFormItem v-if="captchaEnabled" path="code">
<div class="w-full flex-y-center gap-16px">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
<NSpin :show="codeLoading" :size="28" class="h-42px">
<NButton :focusable="false" class="login-code h-42px w-116px" @click="handleFetchCaptchaCode">
<img v-if="codeUrl" :src="codeUrl" />
<NEmpty v-else :show-icon="false" description="暂无验证码" />
</NButton>
</NSpin>
</div>
</NFormItem>
<NSpace vertical :size="16" class="mb-8px">
<div class="mx-6px flex-y-center justify-between">
<NCheckbox v-model:checked="remberMe">{{ $t('page.login.pwdLogin.rememberMe') }}</NCheckbox>
<NSpace :size="1">
<ButtonIcon class="color-#44b549" icon="ic:outline-wechat" @click="handleSocialLogin('wechat_open')" />
<ButtonIcon local-icon="topiam" @click="handleSocialLogin('topiam')" />
<ButtonIcon local-icon="maxkey" @click="handleSocialLogin('maxkey')" />
<ButtonIcon class="color-#c71d23" icon="simple-icons:gitee" @click="handleSocialLogin('gitee')" />
<ButtonIcon class="color-#010409" icon="mdi:github" @click="handleSocialLogin('github')" />
</NSpace>
</div>
<NButton type="primary" size="large" block :loading="authStore.loginLoading" @click="handleSubmit">
{{ $t('common.login') }}
<div>
<div class="mb-12px text-24px text-black font-500 sm:text-30px dark:text-white">登录到您的账户</div>
<div class="pb-24px text-18px text-#858585">欢迎回来请输入您的账户信息</div>
<NForm
ref="formRef"
:model="model"
:rules="rules"
size="large"
:show-label="false"
@keyup.enter="() => !authStore.loginLoading && handleSubmit()"
>
<NFormItem v-if="tenantEnabled" path="tenantId">
<NSelect
v-model:value="model.tenantId"
placeholder="请选择租户"
:options="tenantOption"
:loading="tenantLoading"
/>
</NFormItem>
<NFormItem path="username">
<NInput v-model:value="model.username" :placeholder="$t('page.login.common.userNamePlaceholder')" />
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
</NFormItem>
<NFormItem v-if="captchaEnabled" path="code">
<div class="w-full flex-y-center gap-16px">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
<NSpin :show="codeLoading" :size="28" class="h-52px">
<NButton :focusable="false" class="login-code h-52px w-136px" @click="handleFetchCaptchaCode">
<img v-if="codeUrl" :src="codeUrl" />
<NEmpty v-else :show-icon="false" description="暂无验证码" />
</NButton>
</NSpin>
</div>
</NFormItem>
<NSpace vertical :size="16" class="mb-8px">
<div class="mx-6px mb-10px flex-y-center justify-between">
<NCheckbox v-model:checked="remberMe" size="large">{{ $t('page.login.pwdLogin.rememberMe') }}</NCheckbox>
<NA type="primary" class="text-18px" @click="toggleLoginModule('reset-pwd')">
{{ $t('page.login.pwdLogin.forgetPassword') }}
</NA>
</div>
<NButton type="primary" size="large" block :loading="authStore.loginLoading" @click="handleSubmit">
{{ $t('common.login') }}
</NButton>
<NButton v-if="registerEnabled" size="large" block @click="toggleLoginModule('register')">
{{ $t('page.login.common.register') }}
</NButton>
</NSpace>
</NForm>
<NDivider>
<div class="color-#858585">{{ $t('page.login.pwdLogin.otherAccountLogin') }}</div>
</NDivider>
<div class="w-full flex-y-center gap-16px">
<NButton class="flex-1" @click="handleSocialLogin('gitee')">
<template #icon>
<icon-simple-icons:gitee class="color-#c71d23" />
</template>
<span class="ml-6px">Gitee</span>
</NButton>
<NButton v-if="registerEnabled" size="large" block @click="toggleLoginModule('register')">
<NButton class="flex-1" @click="handleSocialLogin('github')">
<template #icon>
<icon-mdi:github class="color-#010409" />
</template>
<span class="ml-6px">GitHub</span>
</NButton>
</div>
<div class="mt-32px w-full text-center text-18px text-#858585">
您还没有账户
<NA type="primary" class="text-18px" @click="toggleLoginModule('register')">
{{ $t('page.login.common.register') }}
</NButton>
</NSpace>
</NForm>
</NA>
</div>
</div>
</template>
<style scoped>
@ -188,7 +214,34 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
}
img {
height: 40px;
height: 52px;
}
}
:deep(.n-base-selection),
:deep(.n-input) {
--n-height: 52px !important;
--n-font-size: 16px !important;
--n-border-radius: 8px !important;
}
:deep(.n-base-selection-label) {
padding: 0 6px !important;
}
:deep(.n-checkbox) {
--n-size: 18px !important;
--n-font-size: 16px !important;
}
:deep(.n-button) {
--n-height: 52px !important;
--n-font-size: 18px !important;
--n-border-radius: 8px !important;
}
:deep(.n-divider) {
--n-font-size: 16px !important;
--n-font-weight: 400 !important;
}
</style>

View File

@ -103,56 +103,64 @@ handleFetchCaptchaCode();
</script>
<template>
<NForm
ref="formRef"
:model="model"
:rules="rules"
size="large"
:show-label="false"
@keyup.enter="() => !registerLoading && handleSubmit()"
>
<NFormItem v-if="tenantEnabled" path="tenantId">
<NSelect v-model:value="model.tenantId" :options="tenantOption" :enabled="tenantEnabled" />
</NFormItem>
<NFormItem path="username">
<NInput v-model:value="model.username" :placeholder="$t('page.login.common.userNamePlaceholder')" />
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
</NFormItem>
<NFormItem path="confirmPassword">
<NInput
v-model:value="model.confirmPassword"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
/>
</NFormItem>
<NFormItem v-if="captchaEnabled" path="code">
<div class="w-full flex-y-center gap-16px">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
<NSpin :show="codeLoading" :size="28" class="h-42px">
<NButton :focusable="false" class="login-code h-42px w-116px" @click="handleFetchCaptchaCode">
<img v-if="codeUrl" :src="codeUrl" />
<NEmpty v-else :show-icon="false" description="暂无验证码" />
</NButton>
</NSpin>
</div>
</NFormItem>
<NSpace vertical :size="18" class="w-full">
<NButton type="primary" size="large" block :loading="registerLoading" @click="handleSubmit">
{{ $t('page.login.common.register') }}
</NButton>
<NButton size="large" block @click="toggleLoginModule('pwd-login')">
{{ $t('page.login.common.back') }}
</NButton>
</NSpace>
</NForm>
<div>
<div class="mb-12px text-24px text-black font-500 sm:text-30px dark:text-white">注册新账户</div>
<div class="pb-24px text-18px text-#858585">欢迎注册请输入您的账户信息</div>
<NForm
ref="formRef"
:model="model"
:rules="rules"
size="large"
:show-label="false"
@keyup.enter="() => !registerLoading && handleSubmit()"
>
<NFormItem v-if="tenantEnabled" path="tenantId">
<NSelect v-model:value="model.tenantId" :options="tenantOption" :enabled="tenantEnabled" />
</NFormItem>
<NFormItem path="username">
<NInput v-model:value="model.username" :placeholder="$t('page.login.common.userNamePlaceholder')" />
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
</NFormItem>
<NFormItem path="confirmPassword">
<NInput
v-model:value="model.confirmPassword"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
/>
</NFormItem>
<NFormItem v-if="captchaEnabled" path="code">
<div class="w-full flex-y-center gap-16px">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
<NSpin :show="codeLoading" :size="28" class="h-52px">
<NButton :focusable="false" class="login-code h-52px w-136px" @click="handleFetchCaptchaCode">
<img v-if="codeUrl" :src="codeUrl" />
<NEmpty v-else :show-icon="false" description="暂无验证码" />
</NButton>
</NSpin>
</div>
</NFormItem>
<NSpace vertical :size="18" class="w-full pt-6px">
<NButton type="primary" size="large" block :loading="registerLoading" @click="handleSubmit">
{{ $t('page.login.common.register') }}
</NButton>
</NSpace>
</NForm>
<div class="mt-32px w-full text-center text-18px text-#858585">
您已有账户
<NA type="primary" class="text-18px" @click="toggleLoginModule('pwd-login')">
{{ $t('common.login') }}
</NA>
</div>
</div>
</template>
<style scoped lang="scss">
@ -166,4 +174,21 @@ handleFetchCaptchaCode();
height: 40px;
}
}
:deep(.n-base-selection),
:deep(.n-input) {
--n-height: 52px !important;
--n-font-size: 16px !important;
--n-border-radius: 8px !important;
}
:deep(.n-base-selection-label) {
padding: 0 6px !important;
}
:deep(.n-button) {
--n-height: 52px !important;
--n-font-size: 18px !important;
--n-border-radius: 8px !important;
}
</style>

View File

@ -45,38 +45,61 @@ async function handleSubmit() {
</script>
<template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
<NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
</NFormItem>
<NFormItem path="code">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
</NFormItem>
<NFormItem path="confirmPassword">
<NInput
v-model:value="model.confirmPassword"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
/>
</NFormItem>
<NSpace vertical :size="18" class="w-full">
<NButton type="primary" size="large" round block @click="handleSubmit">
{{ $t('common.confirm') }}
</NButton>
<NButton size="large" round block @click="toggleLoginModule('pwd-login')">
{{ $t('page.login.common.back') }}
</NButton>
</NSpace>
</NForm>
<div>
<div class="mb-12px text-24px text-black font-500 sm:text-30px dark:text-white">
{{ $t('page.login.resetPwd.title') }}
</div>
<div class="pb-24px text-18px text-#858585">请输入您的手机号我们将发送验证码到您的手机</div>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
<NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
</NFormItem>
<NFormItem path="code">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
</NFormItem>
<NFormItem path="confirmPassword">
<NInput
v-model:value="model.confirmPassword"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
/>
</NFormItem>
<NSpace vertical :size="18" class="w-full">
<NButton type="primary" size="large" block @click="handleSubmit">
{{ $t('page.login.resetPwd.title') }}
</NButton>
<NButton size="large" block @click="toggleLoginModule('pwd-login')">
{{ $t('page.login.common.back') }}
</NButton>
</NSpace>
</NForm>
</div>
</template>
<style scoped></style>
<style scoped>
:deep(.n-base-selection),
:deep(.n-input) {
--n-height: 52px !important;
--n-font-size: 16px !important;
--n-border-radius: 8px !important;
}
:deep(.n-base-selection-label) {
padding: 0 6px !important;
}
:deep(.n-button) {
--n-height: 52px !important;
--n-font-size: 18px !important;
--n-border-radius: 8px !important;
}
</style>

View File

@ -1,15 +1,17 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useLoading } from '@sa/hooks';
import { getRgb } from '@sa/color';
import { DARK_CLASS } from '@/constants/app';
import { fetchSocialLoginCallback } from '@/service/api';
import { useAuthStore } from '@/store/modules/auth';
import { useRouterPush } from '@/hooks/common/router';
import { localStg } from '@/utils/storage';
import { toggleHtmlClass } from '@/utils/common';
const route = useRoute();
const authStore = useAuthStore();
const { routerPushByKey } = useRouterPush();
const { loading, startLoading, endLoading } = useLoading(true);
/**
* 接收Route传递的参数
@ -31,7 +33,7 @@ const processResponse = async () => {
msg.value = '登录成功1s 后即将跳转至首页';
}, 1000);
setTimeout(() => {
routerPushByKey('home');
routerPushByKey(import.meta.env.VITE_ROUTE_HOME || 'home');
}, 1000);
};
@ -56,21 +58,19 @@ const callbackByCode = async (data: Api.Auth.SocialLoginForm) => {
return;
}
await processResponse();
endLoading();
};
const loginByCode = async (data: Api.Auth.SocialLoginForm) => {
try {
await authStore.logout();
await authStore.login(data);
await processResponse();
} catch {
handleError();
}
endLoading();
};
const init = async () => {
startLoading();
// 如果域名不相等 则重定向处理
const host = window.location.host;
if (domain !== host) {
@ -98,17 +98,28 @@ const init = async () => {
onMounted(async () => {
await init();
});
const themeColor = localStg.get('themeColor') || '#2080f0';
const darkMode = localStg.get('darkMode') || false;
const { r, g, b } = getRgb(themeColor);
if (darkMode) {
toggleHtmlClass(DARK_CLASS).add();
}
const primaryColor = `--primary-color: ${r} ${g} ${b}`;
</script>
<template>
<div class="fixed-center flex-col bg-layout">
<div class="my-36px h-120px w-120px">
<div class="relative h-full" :class="{ 'animate-spin': loading }">
<img src="@/assets/imgs/logo.png" width="120" />
</div>
<div class="fixed-center flex-col bg-layout" :style="primaryColor">
<div class="my-52px h-120px w-120px">
<!-- From Uiverse.io by SchawnnahJ -->
<div class="loader"></div>
</div>
<h2 class="text-28px text-primary font-500">{{ msg }}</h2>
<h2 class="text-30px text-primary-400 font-500">{{ msg }}</h2>
</div>
</template>
<style scoped></style>
<style lang="scss" scoped>
@use '@/styles/scss/loading.scss';
</style>

View File

@ -46,14 +46,14 @@ function createDefaultProfileModel(): ProfileModel {
function createDefaultPasswordModel(): PasswordModel {
return {
password: '',
oldPassword: '',
confirmPassword: '',
newPassword: ''
};
}
type ProfileRuleKey = Extract<keyof ProfileModel, 'nickName' | 'email' | 'phonenumber' | 'sex'>;
type PasswordRuleKey = Extract<keyof PasswordModel, 'password' | 'confirmPassword' | 'newPassword'>;
type PasswordRuleKey = Extract<keyof PasswordModel, 'oldPassword' | 'newPassword' | 'confirmPassword'>;
const profileRules: Record<ProfileRuleKey, App.Global.FormRule> = {
nickName: createRequiredRule('昵称不能为空'),
@ -63,7 +63,7 @@ const profileRules: Record<ProfileRuleKey, App.Global.FormRule> = {
};
const passwordRules: Record<PasswordRuleKey, App.Global.FormRule> = {
password: createRequiredRule('密码不能为空'),
oldPassword: createRequiredRule('密码不能为空'),
confirmPassword: createRequiredRule('确认密码不能为空'),
newPassword: createRequiredRule('新密码不能为空')
};
@ -90,7 +90,8 @@ async function updatePassword() {
return;
}
startBtnLoading();
const { error } = await fetchUpdateUserPassword(passwordModel);
const { oldPassword, newPassword } = passwordModel;
const { error } = await fetchUpdateUserPassword({ oldPassword, newPassword });
if (!error) {
window.$message?.success('密码修改成功');
// 清空表单
@ -183,9 +184,9 @@ async function updatePassword() {
label-width="100px"
class="mt-16px max-w-520px"
>
<NFormItem label="旧密码" path="password">
<NFormItem label="旧密码" path="oldPassword">
<NInput
v-model:value="passwordModel.password"
v-model:value="passwordModel.oldPassword"
type="password"
placeholder="请输入旧密码"
show-password-on="click"

View File

@ -1,11 +1,13 @@
<script setup lang="tsx">
import { NTime } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { fetchForceLogout, fetchGetOnlineDeviceList } from '@/service/api/monitor';
import { fetchGetOnlineDeviceList, fetchKickOutCurrentDevice } from '@/service/api/monitor';
import { useAppStore } from '@/store/modules/app';
import { useTable } from '@/hooks/common/table';
import { useDict } from '@/hooks/business/dict';
import { getBrowserIcon, getOsIcon } from '@/utils/icon-tag-format';
import { $t } from '@/locales';
import DictTag from '@/components/custom/dict-tag.vue';
import ButtonIcon from '@/components/custom/button-icon.vue';
import SvgIcon from '@/components/custom/svg-icon.vue';
@ -13,17 +15,23 @@ defineOptions({
name: 'OnlineTable'
});
useDict('sys_device_type');
const appStore = useAppStore();
const { loading: btnLoading, startLoading: startBtnLoading, endLoading: endBtnLoading } = useLoading(false);
const { columns, data, loading, mobilePagination, getData } = useTable({
const { columns, data, loading, getData } = useTable({
apiFn: fetchGetOnlineDeviceList,
apiParams: {
pageNum: 1,
pageSize: 15
},
columns: () => [
{ title: '用户名', key: 'userName', align: 'center', minWidth: 120 },
{
title: '设备类型',
key: 'deviceType',
align: 'center',
minWidth: 120,
render: row => {
return <DictTag size="small" value={row.deviceType} dict-code="sys_device_type" />;
}
},
{ title: 'IP地址', key: 'ipaddr', align: 'center', minWidth: 120 },
{ title: '登录地点', key: 'loginLocation', align: 'center', minWidth: 120 },
{
@ -90,7 +98,7 @@ const { columns, data, loading, mobilePagination, getData } = useTable({
/** 强制下线 */
async function forceLogout(tokenId: string) {
startBtnLoading();
const { error } = await fetchForceLogout(tokenId);
const { error } = await fetchKickOutCurrentDevice(tokenId);
if (!error) {
window.$message?.success('强制下线成功');
await getData();
@ -109,7 +117,6 @@ async function forceLogout(tokenId: string) {
:loading="loading"
remote
:row-key="row => row.noticeId"
:pagination="mobilePagination"
class="h-full"
/>
</template>

View File

@ -169,7 +169,7 @@ async function handleExport() {
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<DemoSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
<NCard title="测试单表列表" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<NCard title="测试单表列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"

View File

@ -53,12 +53,13 @@ function createDefaultModel(): Model {
};
}
type RuleKey = Extract<keyof Model, 'id' | 'deptId' | 'userId' | 'testKey' | 'value'>;
type RuleKey = Extract<keyof Model, 'id' | 'deptId' | 'userId' | 'orderNum' | 'testKey' | 'value'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
id: createRequiredRule('主键不能为空'),
deptId: createRequiredRule('部门不能为空'),
userId: createRequiredRule('用户不能为空'),
orderNum: createRequiredRule('排序号不能为空'),
testKey: createRequiredRule('key 键不能为空'),
value: createRequiredRule('值不能为空')
};
@ -124,7 +125,7 @@ watch(visible, () => {
<NInput v-model:value="model.testKey" placeholder="请输入 key 键" />
</NFormItem>
<NFormItem label="值" path="value">
<NInput v-model:value="model.value" placeholder="请输入值" />
<OssUpload v-model:value="model.value as string" upload-type="image" placeholder="请输入值" />
</NFormItem>
<NFormItem label="备注" path="remark">
<NInput v-model:value="model.remark" type="textarea" placeholder="请输入备注" />

View File

@ -18,7 +18,6 @@ const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.Demo.DemoSearchParams>('model', { required: true });
async function reset() {
Object.assign(model.value.params!, {});
await restoreValidation();
emit('reset');
}

View File

@ -179,7 +179,7 @@ function handleExport() {
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<TreeSearch v-model:model="searchParams" :tree-list="data" @reset="resetSearchParams" @search="getData" />
<NCard title="测试树列表" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<NCard title="测试树列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"

View File

@ -25,7 +25,6 @@ const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.Demo.TreeSearchParams>('model', { required: true });
async function reset() {
Object.assign(model.value.params!, {});
await restoreValidation();
emit('reset');
}

View File

@ -228,7 +228,7 @@ async function handleUnlockLoginInfor(username: string) {
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<LoginInforSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
<NCard title="登录日志列表" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<NCard title="登录日志列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"

View File

@ -20,6 +20,13 @@ const dateRangeLoginTime = ref<[string, string] | null>(null);
const model = defineModel<Api.Monitor.LoginInforSearchParams>('model', { required: true });
function onDateRangeLoginTimeUpdate(value: [string, string] | null) {
if (value?.length) {
model.value.params!.beginTime = value[0];
model.value.params!.endTime = value[1];
}
}
async function reset() {
dateRangeLoginTime.value = null;
await restoreValidation();
@ -28,10 +35,6 @@ async function reset() {
async function search() {
await validate();
if (dateRangeLoginTime.value?.length) {
model.value.params!.beginTime = dateRangeLoginTime.value[0];
model.value.params!.endTime = dateRangeLoginTime.value[1];
}
emit('search');
}
</script>
@ -62,6 +65,7 @@ async function search() {
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
clearable
@update:formatted-value="onDateRangeLoginTimeUpdate"
/>
</NFormItemGi>
<NFormItemGi span="24" class="pr-24px">

View File

@ -141,7 +141,7 @@ async function handleForceLogout(tokenId: string) {
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<OnlineSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" />
<NCard title="在线用户列表" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<NCard title="在线用户列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"

View File

@ -174,7 +174,7 @@ async function handleCleanOperLog() {
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<OperLogSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
<NCard title="操作日志列表" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<NCard title="操作日志列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"

View File

@ -20,6 +20,13 @@ const dateRangeOperTime = ref<[string, string] | null>(null);
const model = defineModel<Api.Monitor.OperLogSearchParams>('model', { required: true });
function onDateRangeOperTimeUpdate(value: [string, string] | null) {
if (value?.length) {
model.value.params!.beginTime = value[0];
model.value.params!.endTime = value[1];
}
}
async function reset() {
dateRangeOperTime.value = null;
await restoreValidation();
@ -28,10 +35,6 @@ async function reset() {
async function search() {
await validate();
if (dateRangeOperTime.value?.length) {
model.value.params!.beginTime = dateRangeOperTime.value[0];
model.value.params!.endTime = dateRangeOperTime.value[1];
}
emit('search');
}
</script>
@ -73,6 +76,7 @@ async function search() {
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
clearable
@update:formatted-value="onDateRangeOperTimeUpdate"
/>
</NFormItemGi>
<NFormItemGi span="24 s:12 m:8" class="pr-24px">

View File

@ -207,7 +207,7 @@ async function handleExport() {
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<ClientSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
<NCard :title="$t('page.system.client.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<NCard :title="$t('page.system.client.title')" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"

View File

@ -193,7 +193,7 @@ async function handleRefreshCache() {
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<ConfigSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
<NCard :title="$t('page.system.config.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<NCard :title="$t('page.system.config.title')" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"

View File

@ -20,6 +20,13 @@ const dateRangeCreateTime = ref<[string, string] | null>(null);
const model = defineModel<Api.System.ConfigSearchParams>('model', { required: true });
function onDateRangeCreateTimeUpdate(value: [string, string] | null) {
if (value?.length) {
model.value.params!.beginTime = value[0];
model.value.params!.endTime = value[1];
}
}
async function reset() {
dateRangeCreateTime.value = null;
await restoreValidation();
@ -28,10 +35,6 @@ async function reset() {
async function search() {
await validate();
if (dateRangeCreateTime.value?.length) {
model.value.params!.beginTime = dateRangeCreateTime.value[0];
model.value.params!.endTime = dateRangeCreateTime.value[1];
}
emit('search');
}
</script>
@ -80,6 +83,7 @@ async function search() {
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
clearable
@update:formatted-value="onDateRangeCreateTimeUpdate"
/>
</NFormItemGi>
<NFormItemGi span="24 s:12 m:12" class="pr-24px">

View File

@ -168,7 +168,7 @@ async function handleAddOperate() {
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<DeptSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" />
<NCard :title="$t('page.system.dept.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<NCard :title="$t('page.system.dept.title')" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"

View File

@ -17,7 +17,7 @@ const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.System.DeptSearchParams>('model', { required: true });
const { options: sysNormalDisableOptions } = useDict('sys_normal_disable');
const { options: sysNormalDisableOptions } = useDict('sys_normal_disable', false);
async function reset() {
await restoreValidation();

View File

@ -362,7 +362,7 @@ const selectable = computed(() => {
<div class="h-full flex-col-stretch gap-12px overflow-hidden lt-sm:overflow-auto">
<DictDataSearch v-model:model="searchParams" @reset="handleReset" @search="getDataByPage" />
<TableRowCheckAlert v-model:checked-row-keys="checkedRowKeys" />
<NCard :title="$t('page.system.dict.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<NCard :title="$t('page.system.dict.title')" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"

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