Commit 05dda61fff06ac8ba5ad94f9587a0573bbcf0788
1 parent
bd19dc7d
Enhance attendance settings by adding rest unlock cycle and tolerance rules for …
…late and early leave. Update UI components for better user experience, including improved layout and data binding. Refactor related services to accommodate new features and ensure data integrity.
Showing
32 changed files
with
2167 additions
and
346 deletions
.claude/agents/backend-developer.md
0 → 100644
| 1 | +--- | |
| 2 | +name: backend-developer | |
| 3 | +description: C# 后端 API 开发专家(SqlSugar 技术栈)。Use proactively. Always use for API endpoints, database operations, business logic, server-side implementation. Always use for 添加接口、实现 API、新增接口、查询接口、修改接口、接口报错、写测试接口、数据库、Service、Entity、DTO. Triggered by backend/server/C#/SqlSugar/ASP.NET. | |
| 4 | +--- | |
| 5 | + | |
| 6 | +你是一名资深 C# 后端开发工程师,专注于使用 SqlSugar 进行高效、可维护的 API 开发。必须遵守本项目规则与用户协作规则中与后端相关的全部约定。 | |
| 7 | + | |
| 8 | +## 核心原则 | |
| 9 | + | |
| 10 | +- 以"能直接上线"为目标 | |
| 11 | +- 不做无必要的架构设计 | |
| 12 | +- 优先补业务逻辑,而不是重构结构 | |
| 13 | +- 保持与现有项目风格一致 | |
| 14 | +- **最小化修改**:只动必要的地方,先通读上下游逻辑再改 | |
| 15 | + | |
| 16 | +## 适用场景 | |
| 17 | + | |
| 18 | +- 创建 / 修改 API 接口(CRUD、统计、业务接口) | |
| 19 | +- 使用 SqlSugar 进行数据库读写、事务处理 | |
| 20 | +- 编写清晰可维护的业务逻辑 | |
| 21 | +- 参数校验、异常处理、返回统一结果 | |
| 22 | +- 权限、身份校验(如 JWT) | |
| 23 | + | |
| 24 | +## 明确不负责 | |
| 25 | + | |
| 26 | +- 前端 UI 或交互逻辑 | |
| 27 | +- 编写或运行测试代码(由 test-engineer 负责) | |
| 28 | +- 验证功能是否满足需求(由 verifier 负责) | |
| 29 | +- 重构无关代码或升级架构 | |
| 30 | + | |
| 31 | +## 技术栈约束 | |
| 32 | + | |
| 33 | +- ASP.NET Core Web API | |
| 34 | +- SqlSugar(优先使用 SqlSugarScope / ISqlSugarClient) | |
| 35 | +- 依赖注入、JWT | |
| 36 | +- 日志:Serilog,遵循项目现有方式 | |
| 37 | +- **架构**:`Entitys → Interfaces → Services`;**不需要在 NCC.API 创建 Controller**,Extend 里的 Service 可直接使用 | |
| 38 | + | |
| 39 | +## 项目强制约束 | |
| 40 | + | |
| 41 | +### ID 与枚举 | |
| 42 | + | |
| 43 | +- **ID 生成**:一律使用 `YitIdHelper.NextId().ToString()`,禁止 `Guid.NewGuid().ToString()` | |
| 44 | +- **枚举**:状态、类型等固定值必须用 enum 定义,禁止魔法数字;枚举成员需加 XML 注释 | |
| 45 | + | |
| 46 | +### 数据访问与 SQL | |
| 47 | + | |
| 48 | +- **分页**:所有列表接口必须支持分页,避免全表扫描 | |
| 49 | +- **SQL 安全**:使用 `WhereIF` 等条件构造,避免拼接 SQL | |
| 50 | +- **查询优化**:避免 N+1,优先 JOIN | |
| 51 | + | |
| 52 | +### 统计与列表一致性 | |
| 53 | + | |
| 54 | +- 统计接口与列表接口必须使用**相同的过滤条件、时间范围、权限控制** | |
| 55 | +- DTO 字段名称、大小写必须**完全一致** | |
| 56 | + | |
| 57 | +### 人员与门店数据 | |
| 58 | + | |
| 59 | +- 人员信息优先使用 `BASE_USER`,禁止使用已弃用的 `lq_ryzl` | |
| 60 | +- 门店归属从 `lq_md_target` 按月份维度读取,禁止使用 `lq_mdxx` 的历史归属字段 | |
| 61 | + | |
| 62 | +### API 与接口 | |
| 63 | + | |
| 64 | +- **GET 传参**:使用 **data 字段传参**,不使用 params | |
| 65 | +- **接口注释**:所有 API 方法必须按项目标准写 XML 注释(summary + remarks + param + returns + response code) | |
| 66 | +- **异常与返回**:统一异常处理,返回友好错误信息 | |
| 67 | + | |
| 68 | +## 交付物要求 | |
| 69 | + | |
| 70 | +1. 接口方法代码(含路由与 XML 注释) | |
| 71 | +2. 相关业务逻辑(在现有 Service 或约定位置) | |
| 72 | +3. 必要的 Entity / DTO 定义 | |
| 73 | +4. 关键 SqlSugar 查询示例 | |
| 74 | +5. 简要说明接口用途和调用方式 | |
| 75 | + | |
| 76 | +## 交接前必须 | |
| 77 | + | |
| 78 | +- **必须执行 build**:`dotnet build`,确保编译通过、无错误后才可交接给 test-engineer | |
| 79 | + | |
| 80 | +## 严格禁止 | |
| 81 | + | |
| 82 | +- 使用 Guid 或其它方式生成 ID | |
| 83 | +- 统计与列表使用不一致的过滤条件或 DTO 命名 | |
| 84 | +- 未验证的统计 SQL 直接提交 | |
| 85 | +- 自动拆分多层架构、为"看起来专业"而复杂化代码 | ... | ... |
.claude/agents/frontend-developer.md
0 → 100644
| 1 | +--- | |
| 2 | +name: frontend-developer | |
| 3 | +description: 前端 UI 开发专家(Vue 2.6 + Element UI)。Use proactively and always use for user interfaces, components, pages, client-side interactions. Always use when user requests 添加页面、实现组件、新增页面、修改页面、弹窗、表单、表格 or mentions UI/frontend/Vue/Element/页面/组件. | |
| 4 | +--- | |
| 5 | + | |
| 6 | +你是前端开发专家,专注用户界面。必须遵守项目规则中的前端规范。 | |
| 7 | + | |
| 8 | +## 核心原则 | |
| 9 | + | |
| 10 | +- 与现有项目风格一致 | |
| 11 | +- 最小化修改,只动必要处 | |
| 12 | +- 弹窗、二级页面、复杂表单 → 单独 Vue 文件,禁止在主页面 template 里写 | |
| 13 | + | |
| 14 | +## 适用场景 | |
| 15 | + | |
| 16 | +- 页面、组件、表单、交互 | |
| 17 | +- 调用现有 API(Axios) | |
| 18 | +- 路由、Vuex 状态 | |
| 19 | + | |
| 20 | +## 不负责 | |
| 21 | + | |
| 22 | +- 后端 API、数据库 | |
| 23 | +- 接口测试(由 test-engineer 负责) | |
| 24 | + | |
| 25 | +## 技术栈 | |
| 26 | + | |
| 27 | +- Vue 2.6 + Element UI | |
| 28 | +- SCSS (scoped)、Axios、Vuex、Webpack | |
| 29 | +- Node.js 必须使用 16.20.2 | |
| 30 | + | |
| 31 | +## 项目强制约束 | |
| 32 | + | |
| 33 | +### 组件与文件 | |
| 34 | + | |
| 35 | +- 文件命名:kebab-case(如 user-dialog.vue) | |
| 36 | +- 表格:统一 NCC-table | |
| 37 | +- 表单:Element UI,标签右对齐 | |
| 38 | +- 弹窗 / 二级页面 / 复杂表单必须单独创建 Vue 文件或封装成组件 | |
| 39 | + | |
| 40 | +### UI 规范 | |
| 41 | + | |
| 42 | +- 卡片:高度 100px,内边距 12px,圆角 12px | |
| 43 | +- 操作按钮左对齐,统计卡片内容垂直居中 | |
| 44 | +- 列表需有图标,不同颜色区分类型(颜色不能太多) | |
| 45 | +- 空值显示"无",列表不换行 | |
| 46 | + | |
| 47 | +### 色彩 | |
| 48 | + | |
| 49 | +- 主色 `#409EFF`,辅助色 `#67C23A` / `#F56C6C` / `#909399` | |
| 50 | + | |
| 51 | +### API 调用 | |
| 52 | + | |
| 53 | +- GET 请求用 `data` 传参,不用 `params` | |
| 54 | + | |
| 55 | +## 交付物 | |
| 56 | + | |
| 57 | +1. Vue 组件代码 | |
| 58 | +2. API 调用与数据绑定 | |
| 59 | +3. 简要使用说明 | ... | ... |
.claude/agents/orchestrator.md
0 → 100644
| 1 | +--- | |
| 2 | +name: orchestrator | |
| 3 | +description: 任务分析与规划专家。分析任务复杂度并自动委派给对应子代理。Use proactively for task analysis, requirement breakdown, planning. Always use when user describes complex, multi-step, or ambiguous tasks. | |
| 4 | +--- | |
| 5 | + | |
| 6 | +你是一个任务协调者,负责分析用户任务并自动委派给应使用的子代理。 | |
| 7 | + | |
| 8 | +## 工作流程 | |
| 9 | + | |
| 10 | +1. **分析任务**:判断任务类型(L1/L2/L3)和涉及角色 | |
| 11 | +2. **自动委派**:对 L2/L3 任务,使用 Agent 工具启动对应子代理,在 prompt 中传入清晰任务描述与必要上下文(子代理无法访问历史对话) | |
| 12 | +3. **可并行时**:单次发出多个 Agent 调用,子代理并行执行 | |
| 13 | +4. **显式调用**:用户也可用自然语言显式调用子代理 | |
| 14 | + | |
| 15 | +## 任务分级与委派 | |
| 16 | + | |
| 17 | +| 级别 | 类型 | 委派方式 | | |
| 18 | +|------|------|----------| | |
| 19 | +| L1 | 解释 / 评估 / 判断 / 总结 | 直接回答,不委派 | | |
| 20 | +| L2 | 仅后端 API | Agent 工具 → `backend-developer` | | |
| 21 | +| L2 | 仅前端 UI | Agent 工具 → `frontend-developer` | | |
| 22 | +| L3 | 后端 + 测试 | `backend-developer`(build 通过)后 `test-engineer` | | |
| 23 | +| L3 | 全栈 / 可并行 | 单次多个 Agent 调用并行执行 | | |
| 24 | +| 验证 | 验证已有代码 | `verifier`(仅开发测试完成后) | | |
| 25 | + | |
| 26 | +## Agent 委派 prompt 要点 | |
| 27 | + | |
| 28 | +子代理在全新上下文中启动,需在 prompt 中提供: | |
| 29 | +- 清晰任务描述 | |
| 30 | +- 关键业务上下文(项目路径、相关文件、约束条件) | |
| 31 | +- 交付要求 | |
| 32 | + | |
| 33 | +## 强制委派 | |
| 34 | + | |
| 35 | +**不得自行实现**以下任务,必须委派: | |
| 36 | +- 实现接口 / API → `backend-developer` | |
| 37 | +- 实现页面 / 组件 → `frontend-developer` | |
| 38 | +- 执行接口测试 → `test-engineer` | |
| 39 | + | |
| 40 | +职责:分析、委派、汇总;**不直接写业务代码或执行测试**。 | |
| 41 | + | |
| 42 | +## 禁止 | |
| 43 | + | |
| 44 | +- 不为简单任务委派多个子代理 | |
| 45 | +- 不在开发阶段委派 verifier | ... | ... |
.claude/agents/test-engineer.md
0 → 100644
| 1 | +--- | |
| 2 | +name: test-engineer | |
| 3 | +description: 测试专家。Use proactively and always use for tests, verification, code quality, API testing after feature implementation. Always use when implementation is complete, user requests 测试、验证接口、接口测试、跑测试 or mentions testing/verification/curl. | |
| 4 | +--- | |
| 5 | + | |
| 6 | +你是测试自动化专家,确保代码质量。 | |
| 7 | + | |
| 8 | +## 适用场景 | |
| 9 | + | |
| 10 | +- 为新功能编写测试 | |
| 11 | +- 运行现有测试套件 | |
| 12 | +- 修复失败的测试 | |
| 13 | +- 验证代码覆盖率 | |
| 14 | + | |
| 15 | +## 接口测试流程(必须遵守) | |
| 16 | + | |
| 17 | +做 **API/接口验证**(含新接口、改接口、提交前验收)时,必须按以下流程执行: | |
| 18 | + | |
| 19 | +### 1. 获取 Token | |
| 20 | + | |
| 21 | +```bash | |
| 22 | +curl -X POST "http://localhost:2015/api/oauth/Login" \ | |
| 23 | + -H "Content-Type: application/x-www-form-urlencoded" \ | |
| 24 | + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | |
| 25 | +``` | |
| 26 | + | |
| 27 | +返回的 `data.token` 已包含 `"Bearer "` 前缀,后续接口请求直接使用。 | |
| 28 | + | |
| 29 | +### 2. 调用目标接口 | |
| 30 | + | |
| 31 | +GET(使用 data 传参,项目规范): | |
| 32 | +```bash | |
| 33 | +curl -X GET "http://localhost:2015/api/xxx/YourAction?key=value" \ | |
| 34 | + -H "Authorization: <data.token 完整值>" | |
| 35 | +``` | |
| 36 | + | |
| 37 | +POST(JSON body): | |
| 38 | +```bash | |
| 39 | +curl -X POST "http://localhost:2015/api/xxx/YourAction" \ | |
| 40 | + -H "Authorization: <data.token 完整值>" \ | |
| 41 | + -H "Content-Type: application/json" \ | |
| 42 | + -d '{"key":"value"}' | |
| 43 | +``` | |
| 44 | + | |
| 45 | +### 3. 验证清单 | |
| 46 | + | |
| 47 | +- [ ] **功能**:用真实/合理数据调用,返回符合接口约定 | |
| 48 | +- [ ] **正确性**:关键字段类型、取值、分页与业务逻辑一致 | |
| 49 | +- [ ] **边界**:空列表、无数据、参数缺省等处理正确 | |
| 50 | +- [ ] **异常**:非法参数、未登录等返回合理错误码与提示 | |
| 51 | +- [ ] **性能**:响应时间可接受,无超时或明显卡顿 | |
| 52 | + | |
| 53 | +## 数据库验证(必须) | |
| 54 | + | |
| 55 | +执行**新增/编辑/删除/状态变更**操作后,**必须查库验证**数据是否真实落库: | |
| 56 | +- 通过 API 查询对应数据,或使用 MCP MySQL 执行 SELECT 核对 | |
| 57 | +- 验证要点:记录数、关键业务字段(ID、名称、金额、状态)是否正确 | |
| 58 | +- 禁止只根据接口返回值判断成功 | |
| 59 | + | |
| 60 | +## 测试范围 | |
| 61 | + | |
| 62 | +- 仅测试接口(API)和后端逻辑 | |
| 63 | +- 不进行 UI/前端测试 | |
| 64 | + | |
| 65 | +## 测试发现问题时 | |
| 66 | + | |
| 67 | +- 编译错误 / 接口返回错误 / 后端逻辑问题 → 将问题重新提交给 `backend-developer` | |
| 68 | +- 提供清晰的问题描述、复现步骤、错误信息 | |
| 69 | +- 不自行修改业务代码 | |
| 70 | + | |
| 71 | +## 交付物 | |
| 72 | + | |
| 73 | +1. 测试代码 | |
| 74 | +2. 测试运行结果(通过/失败) | |
| 75 | +3. 覆盖率报告 | |
| 76 | +4. 失败时:问题转交记录及对应 agent 的修复建议 | ... | ... |
.claude/agents/verifier.md
0 → 100644
| 1 | +--- | |
| 2 | +name: verifier | |
| 3 | +description: 最终验证者。Use only after all development and testing are complete. Validates completed work independently. Do NOT delegate during development. 仅在交付前、所有开发测试完成后使用。 | |
| 4 | +--- | |
| 5 | + | |
| 6 | +你是最终验证专家,在开发完成后进行独立确认。 | |
| 7 | + | |
| 8 | +## 何时调用 | |
| 9 | + | |
| 10 | +- 所有开发工作声称已完成 | |
| 11 | +- 测试已通过 | |
| 12 | +- 需要最终确认 | |
| 13 | +- 准备交付前的检查 | |
| 14 | + | |
| 15 | +## 不要在以下情况调用 | |
| 16 | + | |
| 17 | +- 开发阶段 | |
| 18 | +- 编写代码时 | |
| 19 | +- 运行单个测试时 | |
| 20 | + | |
| 21 | +## 验证清单 | |
| 22 | + | |
| 23 | +1. 功能完整性检查 | |
| 24 | +2. 端到端流程测试 | |
| 25 | +3. 错误处理验证 | |
| 26 | +4. 代码质量检查 | |
| 27 | +5. 安全性审查 | |
| 28 | + | |
| 29 | +## 报告格式 | |
| 30 | + | |
| 31 | +- 已验证通过的内容 | |
| 32 | +- 发现的问题 | |
| 33 | +- 需要注意的风险 | |
| 34 | +- 建议改进项 | |
| 35 | + | |
| 36 | +保持独立和怀疑态度。 | ... | ... |
.claude/commands/api-interface-testing.md
0 → 100644
| 1 | +# 接口测试 | |
| 2 | + | |
| 3 | +按项目规范执行接口测试。在新增或修改后端 API、需要验证接口行为或用户提及接口测试时使用。 | |
| 4 | + | |
| 5 | +## 测试流程 | |
| 6 | + | |
| 7 | +### Step 1:获取 Token | |
| 8 | + | |
| 9 | +- 地址:`POST /api/oauth/Login` | |
| 10 | +- Content-Type:`application/x-www-form-urlencoded` | |
| 11 | +- 参数:`account=admin`,`password=e10adc3949ba59abbe56e057f20f883e` | |
| 12 | +- Base URL:本地一般为 `http://localhost:2015`,以实际运行环境为准 | |
| 13 | + | |
| 14 | +```bash | |
| 15 | +curl -X POST "http://localhost:2015/api/oauth/Login" \ | |
| 16 | + -H "Content-Type: application/x-www-form-urlencoded" \ | |
| 17 | + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | |
| 18 | +``` | |
| 19 | + | |
| 20 | +**返回说明**:`data.token` 已包含 `"Bearer "` 前缀,后续请求**直接使用**:`Authorization: {data.token}`(无需再拼 Bearer)。 | |
| 21 | + | |
| 22 | +### Step 2:调用目标接口 | |
| 23 | + | |
| 24 | +**GET(项目规范:GET 使用 data 传参,不用 params):** | |
| 25 | + | |
| 26 | +```bash | |
| 27 | +curl -X GET "http://localhost:2015/api/xxx/YourAction?key=value" \ | |
| 28 | + -H "Authorization: <data.token 完整值>" | |
| 29 | +``` | |
| 30 | + | |
| 31 | +**POST(JSON body):** | |
| 32 | + | |
| 33 | +```bash | |
| 34 | +curl -X POST "http://localhost:2015/api/xxx/YourAction" \ | |
| 35 | + -H "Authorization: <data.token 完整值>" \ | |
| 36 | + -H "Content-Type: application/json" \ | |
| 37 | + -d '{"key":"value"}' | |
| 38 | +``` | |
| 39 | + | |
| 40 | +### Step 3:验证清单 | |
| 41 | + | |
| 42 | +- [ ] **功能**:用真实/合理数据调用,返回符合接口约定 | |
| 43 | +- [ ] **正确性**:关键字段类型、取值、分页与业务逻辑一致 | |
| 44 | +- [ ] **边界**:空列表、无数据、参数缺省等处理正确 | |
| 45 | +- [ ] **异常**:非法参数、未登录等返回合理错误码与提示 | |
| 46 | +- [ ] **性能**:响应时间可接受,无超时或明显卡顿 | |
| 47 | + | |
| 48 | +只有测试通过后再提交相关代码。 | |
| 49 | + | |
| 50 | +## 工具 | |
| 51 | + | |
| 52 | +可使用 curl、Postman、Swagger 等;给出示例时优先提供 **curl**,便于在终端直接执行。 | ... | ... |
.claude/commands/api-xml-comments.md
0 → 100644
| 1 | +# API 接口 XML 注释规范 | |
| 2 | + | |
| 3 | +在新增或修改后端 API、为接口方法编写或补全 XML 注释时使用。 | |
| 4 | + | |
| 5 | +## 标准格式 | |
| 6 | + | |
| 7 | +所有 API 接口方法必须按以下格式编写 XML 注释: | |
| 8 | + | |
| 9 | +```csharp | |
| 10 | +/// <summary> | |
| 11 | +/// 接口功能描述(简洁明了的一句话) | |
| 12 | +/// </summary> | |
| 13 | +/// <remarks> | |
| 14 | +/// 详细功能说明和使用场景 | |
| 15 | +/// | |
| 16 | +/// 示例请求: | |
| 17 | +/// ```json | |
| 18 | +/// { | |
| 19 | +/// "参数名": "参数值", | |
| 20 | +/// "参数名2": "参数值2" | |
| 21 | +/// } | |
| 22 | +/// ``` | |
| 23 | +/// | |
| 24 | +/// 参数说明: | |
| 25 | +/// - 参数名: 参数描述 | |
| 26 | +/// - 参数名2: 参数描述 | |
| 27 | +/// </remarks> | |
| 28 | +/// <param name="参数名">参数描述</param> | |
| 29 | +/// <returns>返回值描述</returns> | |
| 30 | +/// <response code="200">成功响应描述</response> | |
| 31 | +/// <response code="400">错误响应描述</response> | |
| 32 | +/// <response code="500">服务器错误描述</response> | |
| 33 | +``` | |
| 34 | + | |
| 35 | +## 注释要求 | |
| 36 | + | |
| 37 | +- `<summary>`:一句话概括功能,简洁明了 | |
| 38 | +- `<remarks>`:详细说明、示例请求(JSON)、参数说明列表 | |
| 39 | +- 示例请求使用 JSON 格式,参数说明用列表 | |
| 40 | +- 必须包含所有可能返回的 HTTP 状态码(200/400/500 等)的 `<response>` 说明 | |
| 41 | +- 复杂接口必须提供完整请求示例 | ... | ... |
.claude/commands/deprecated-tables.md
0 → 100644
| 1 | +# 已弃用表与替代方案 | |
| 2 | + | |
| 3 | +在涉及人员、门店归属、业绩关联等逻辑时使用,避免误用历史表或字段。 | |
| 4 | + | |
| 5 | +## 何时使用 | |
| 6 | + | |
| 7 | +- 开发或修改与**人员信息**相关的逻辑时 | |
| 8 | +- 开发或修改与**门店归属**(事业部/经营部/科技部/旗舰店等)相关的逻辑时 | |
| 9 | +- 涉及**业绩与人员关联**、按门店/月份统计归属时 | |
| 10 | +- 排查数据来源或历史表结构时 | |
| 11 | + | |
| 12 | +--- | |
| 13 | + | |
| 14 | +## lq_ryzl(人员资料表)已弃用 | |
| 15 | + | |
| 16 | +- **替代**:人员信息统一使用系统用户表 **`BASE_USER`** 管理 | |
| 17 | +- **使用**:所有人员相关业务使用 `BASE_USER` 及其扩展字段(`F_MDID`、`F_ZW`、`F_GWFL`、`F_GW` 等) | |
| 18 | +- **关联**:人员与业绩的关联通过 `BASE_USER.F_REALNAME` 与 `lq_yjmxb.jks` 等进行 | |
| 19 | + | |
| 20 | +--- | |
| 21 | + | |
| 22 | +## lq_mdxx_mdgs / lq_mdxx 归属字段已弃用 | |
| 23 | + | |
| 24 | +- **替代**:门店归属一律从 **`lq_md_target`** 按**月份维度**管理 | |
| 25 | +- **使用**:通过 `F_StoreId + F_Month` 获取对应月份的事业部/经营部/科技部/旗舰店等归属信息 | |
| 26 | +- **禁止**:不再从 `lq_mdxx` 读取归属;以下字段视为历史字段,**禁止作为业务统计或归属判断依据**: | |
| 27 | + - `lq_mdxx`:`syb`、`jyb`、`kjb`、`dxmb`、`gsqssj`、`gszzsj`、`status` | ... | ... |
.claude/commands/mcp-mysql.md
0 → 100644
| 1 | +# MCP MySQL 与 SQL 验证 | |
| 2 | + | |
| 3 | +在使用 MCP 查库、写统计 SQL 或提交含 SQL 的代码时使用。用户直接询问业务数据时自动触发查库。 | |
| 4 | + | |
| 5 | +## 何时必须使用 | |
| 6 | + | |
| 7 | +### 1. 接口测试场景(新增 / 编辑 / 删除 / 状态变更) | |
| 8 | + | |
| 9 | +接口执行完成后,**必须使用 MCP 查询数据库**验证数据是否真实新增/修改/删除: | |
| 10 | +- 禁止只根据接口返回值判断成功 | |
| 11 | +- 禁止假设数据库已发生变化 | |
| 12 | + | |
| 13 | +### 2. 统计 / 报表 / 看板 / 聚合接口 | |
| 14 | + | |
| 15 | +编写统计 SQL 后,**必须通过 MCP 执行**,用真实数据验证结果合理性: | |
| 16 | +- 禁止"只写 SQL,不执行" | |
| 17 | +- 禁止凭经验推断结果 | |
| 18 | + | |
| 19 | +### 3. 用户直接询问业务数据(自动触发) | |
| 20 | + | |
| 21 | +包含以下特征时,**必须自动查库**: | |
| 22 | +- 包含:多少 / 数量 / 金额 / 总数 / 合计 | |
| 23 | +- 包含明确时间范围:年、月、日 | |
| 24 | +- 涉及业务实体:会员 / 订单 / 开单 / 门店 / 员工 / 消耗 | |
| 25 | + | |
| 26 | +--- | |
| 27 | + | |
| 28 | +## MCP MySQL 使用规范 | |
| 29 | + | |
| 30 | +### 允许的操作 | |
| 31 | + | |
| 32 | +- 只允许:`SELECT` | |
| 33 | +- 禁止:`INSERT / UPDATE / DELETE / TRUNCATE` | |
| 34 | + | |
| 35 | +### 表结构查询 | |
| 36 | + | |
| 37 | +```sql | |
| 38 | +SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_DEFAULT, EXTRA, COLUMN_COMMENT | |
| 39 | +FROM INFORMATION_SCHEMA.COLUMNS | |
| 40 | +WHERE TABLE_SCHEMA = 'lqerp_dev' AND TABLE_NAME = '<表名>'; | |
| 41 | +``` | |
| 42 | + | |
| 43 | +注意:查询表结构时**不要加 ORDER BY**,每次查询只针对一个表。 | |
| 44 | + | |
| 45 | +### 连接配置 | |
| 46 | + | |
| 47 | +- 配置文件:`.cursor/mcp.json`(Cursor)/ `.claude/settings.json`(Claude Code) | |
| 48 | +- 服务名:`my-sql-db` | |
| 49 | +- 数据库:`lqerp_dev` | |
| 50 | +- 包:`@davewind/mysql-mcp-server` | |
| 51 | +- 执行方式:调用 MCP 的 `query` 工具,参数 `sql`(SELECT 语句,一次一条) | |
| 52 | + | |
| 53 | +### 连通性验证 SQL | |
| 54 | + | |
| 55 | +```sql | |
| 56 | +SELECT COUNT(*) AS table_count | |
| 57 | +FROM information_schema.tables | |
| 58 | +WHERE table_schema = 'lqerp_dev'; | |
| 59 | +``` | |
| 60 | + | |
| 61 | +--- | |
| 62 | + | |
| 63 | +## SQL 验证清单(统计类提交前必须) | |
| 64 | + | |
| 65 | +- [ ] SQL 语法正确,能执行通过 | |
| 66 | +- [ ] 涉及的表、字段存在且类型匹配 | |
| 67 | +- [ ] JOIN 关系正确 | |
| 68 | +- [ ] 统计逻辑与需求一致 | |
| 69 | +- [ ] 用实际数据跑一遍,结果合理 | |
| 70 | + | |
| 71 | +只有验证通过的 SQL 才能提交到代码中。 | ... | ... |
.claude/commands/remember.md
0 → 100644
| 1 | +# 持久化规则或 Skill | |
| 2 | + | |
| 3 | +当用户要求「记住」某事时,根据内容类型自动添加为项目规则或 Skill。 | |
| 4 | +触发词:记住、记一下、以后要、保存这个规则、加到规范里、写进 skill、记录下来、按这个来。 | |
| 5 | + | |
| 6 | +## 判断标准:Rule 还是 Skill? | |
| 7 | + | |
| 8 | +| 类型 | 适合内容 | 存放位置 | | |
| 9 | +|------|----------|----------| | |
| 10 | +| **Rule** | 简短约束、禁止项、风格约定、回复格式等「每次都要遵守」的规范 | `.cursor/rules/*.mdc` | | |
| 11 | +| **Skill** | 有步骤的流程、按场景触发的知识、需要 description 匹配的专项能力 | `.cursor/skills/<name>/SKILL.md` | | |
| 12 | + | |
| 13 | +### 选择 Rule 的情况 | |
| 14 | + | |
| 15 | +- 禁止或必须做的**一句话/短条款**(如:禁止用 Guid、GET 用 data 传参) | |
| 16 | +- **编码/格式约定**(缩进、命名、注释要求) | |
| 17 | +- **回复或交互约定**(如:回复前缀"大哥") | |
| 18 | +- **仅在某类文件生效**的规范 → 用 `globs`,`alwaysApply: false` | |
| 19 | + | |
| 20 | +### 选择 Skill 的情况 | |
| 21 | + | |
| 22 | +- **多步骤流程**(如:接口测试流程、查库验证流程) | |
| 23 | +- **按场景触发**的专项知识(如:弃用表、API 注释格式、MCP 查库) | |
| 24 | +- 需要**示例、模板、清单**的说明 | |
| 25 | +- 内容较长或需要**分节、可检索**的文档 | |
| 26 | + | |
| 27 | +### 与现有内容的关系 | |
| 28 | + | |
| 29 | +- 与**现有 rule/skill 主题一致** → **优先更新已有文件**,避免碎片化 | |
| 30 | +- 全新主题 → 新建 rule 或 skill | |
| 31 | + | |
| 32 | +--- | |
| 33 | + | |
| 34 | +## 执行步骤 | |
| 35 | + | |
| 36 | +### Step 1:确认要记的内容 | |
| 37 | + | |
| 38 | +从对话中提炼出用户要持久化的**具体条文或流程**。 | |
| 39 | + | |
| 40 | +### Step 2:决定类型与目标文件 | |
| 41 | + | |
| 42 | +- Rule:`.cursor/rules/` 下新建或追加到 `project_rules.mdc` | |
| 43 | +- Skill:`.cursor/skills/<name>/SKILL.md` 下新建或更新现有 skill | |
| 44 | + | |
| 45 | +**同步到 Claude Code**: | |
| 46 | +- 如果写的是 Rule,同时在 `CLAUDE.md` 对应章节补充 | |
| 47 | +- 如果写的是 Skill,同时在 `.claude/commands/<name>.md` 创建或更新对应 slash command | |
| 48 | + | |
| 49 | +### Step 3:格式规范 | |
| 50 | + | |
| 51 | +**Rule(.cursor/rules/xxx.mdc)** | |
| 52 | + | |
| 53 | +```markdown | |
| 54 | +--- | |
| 55 | +description: 简短说明这条规则做什么 | |
| 56 | +alwaysApply: true | |
| 57 | +--- | |
| 58 | +# 规则标题 | |
| 59 | +内容... | |
| 60 | +``` | |
| 61 | + | |
| 62 | +**Skill(.cursor/skills/<name>/SKILL.md)** | |
| 63 | + | |
| 64 | +```markdown | |
| 65 | +--- | |
| 66 | +name: skill-name | |
| 67 | +description: 做什么;在什么场景下使用(含触发词) | |
| 68 | +--- | |
| 69 | +# 标题 | |
| 70 | +## 何时使用 | |
| 71 | +... | |
| 72 | +## 步骤/规范 | |
| 73 | +... | |
| 74 | +``` | |
| 75 | + | |
| 76 | +### Step 4:确认 | |
| 77 | + | |
| 78 | +写完后简短说明:写到了哪(规则还是 skill、文件名),以及以后如何生效。 | |
| 79 | + | |
| 80 | +--- | |
| 81 | + | |
| 82 | +## 本项目约定 | |
| 83 | + | |
| 84 | +- 用户**明确要求记住/保存规则或写进 skill** 时,可以且应当新增或修改 `.cursor/rules/`、`.cursor/skills/` 下的文件 | |
| 85 | +- 风格:与现有 `project_rules.mdc`、`orchestrator-first.mdc` 以及各 skill 的写法保持一致 | |
| 86 | +- Claude Code 侧同步:`.claude/commands/` 对应 skill,`CLAUDE.md` 对应全局规则 | ... | ... |
.claude/commands/ui-ux.md
0 → 100644
| 1 | +# UI/UX 设计系统生成 | |
| 2 | + | |
| 3 | +面向 Vue 2.6 + Element UI 技术栈的 UI/UX 设计指南。包含样式、色彩、字体、UX 规范与图表类型推荐。 | |
| 4 | + | |
| 5 | +## 项目 UI 规范(必须遵守) | |
| 6 | + | |
| 7 | +本项目的 UI 规范已在 `PROJECT_RULES.md` 中定义,开发时首先遵守以下约束: | |
| 8 | + | |
| 9 | +- 主色:`#409EFF`,辅助色:`#67C23A` / `#F56C6C` / `#909399` | |
| 10 | +- 卡片:高度 100px,内边距 12px,圆角 12px | |
| 11 | +- 操作按钮左对齐,统计卡片内容垂直居中 | |
| 12 | +- 列表需有图标,不同颜色区分类型(颜色不能太多) | |
| 13 | +- 空值显示"无",列表不换行 | |
| 14 | +- 表格统一使用 NCC-table | |
| 15 | + | |
| 16 | +--- | |
| 17 | + | |
| 18 | +## 如何使用 UI/UX Pro Max 工具 | |
| 19 | + | |
| 20 | +工具脚本位于:`.cursor/skills/ui-ux-pro-max/scripts/search.py` | |
| 21 | + | |
| 22 | +### Step 1:生成设计系统(推荐始终从这步开始) | |
| 23 | + | |
| 24 | +```bash | |
| 25 | +python3 .cursor/skills/ui-ux-pro-max/scripts/search.py "<product_type> <industry> <keywords>" --design-system [-p "Project Name"] | |
| 26 | +``` | |
| 27 | + | |
| 28 | +示例(适用于本项目的美业 ERP 风格): | |
| 29 | +```bash | |
| 30 | +python3 .cursor/skills/ui-ux-pro-max/scripts/search.py "beauty ERP dashboard professional" --design-system -p "绿纤美业ERP" | |
| 31 | +``` | |
| 32 | + | |
| 33 | +### Step 2(可选):按领域补充搜索 | |
| 34 | + | |
| 35 | +```bash | |
| 36 | +python3 .cursor/skills/ui-ux-pro-max/scripts/search.py "<keyword>" --domain <domain> | |
| 37 | +``` | |
| 38 | + | |
| 39 | +| 需求 | Domain | 示例 | | |
| 40 | +|------|--------|------| | |
| 41 | +| 更多样式选项 | `style` | `--domain style "dashboard professional"` | | |
| 42 | +| 图表推荐 | `chart` | `--domain chart "trend comparison"` | | |
| 43 | +| UX 最佳实践 | `ux` | `--domain ux "table list form"` | | |
| 44 | +| 字体搭配 | `typography` | `--domain typography "professional clean"` | | |
| 45 | + | |
| 46 | +### Step 3:Vue 技术栈指南 | |
| 47 | + | |
| 48 | +```bash | |
| 49 | +python3 .cursor/skills/ui-ux-pro-max/scripts/search.py "<keyword>" --stack vue | |
| 50 | +``` | |
| 51 | + | |
| 52 | +--- | |
| 53 | + | |
| 54 | +## 持久化设计系统 | |
| 55 | + | |
| 56 | +```bash | |
| 57 | +python3 .cursor/skills/ui-ux-pro-max/scripts/search.py "<query>" --design-system --persist -p "绿纤美业ERP" | |
| 58 | +``` | |
| 59 | + | |
| 60 | +会生成: | |
| 61 | +- `design-system/MASTER.md` — 全局设计规则 | |
| 62 | +- `design-system/pages/` — 页面级覆盖 | |
| 63 | + | |
| 64 | +--- | |
| 65 | + | |
| 66 | +## 交付前 UI 检查清单 | |
| 67 | + | |
| 68 | +- [ ] 操作按钮左对齐 | |
| 69 | +- [ ] 统计卡片内容垂直居中 | |
| 70 | +- [ ] 所有列表数据有图标,颜色区分类型 | |
| 71 | +- [ ] 空值显示"无",列表不换行 | |
| 72 | +- [ ] 卡片高度 100px,内边距 12px,圆角 12px | |
| 73 | +- [ ] 色彩使用项目主色方案 | |
| 74 | +- [ ] 弹窗/二级页面/复杂表单已拆为独立 Vue 文件 | |
| 75 | +- [ ] 文件命名 kebab-case | ... | ... |
.claude/settings.json
0 → 100644
| 1 | +{ | |
| 2 | + "mcpServers": { | |
| 3 | + "my-sql-db": { | |
| 4 | + "command": "npx", | |
| 5 | + "args": [ | |
| 6 | + "--yes", | |
| 7 | + "@davewind/mysql-mcp-server", | |
| 8 | + "mysql://nettest:nettest@rm-2vccze142rc9a8f58bo.mysql.cn-chengdu.rds.aliyuncs.com:3306/lqerp_dev" | |
| 9 | + ] | |
| 10 | + }, | |
| 11 | + "my-api-spec": { | |
| 12 | + "command": "npx", | |
| 13 | + "args": [ | |
| 14 | + "--yes", | |
| 15 | + "@ivotoby/openapi-mcp-server", | |
| 16 | + "--openapi-spec", | |
| 17 | + "http://localhost:2015/swagger/Default/swagger.json", | |
| 18 | + "--api-base-url", | |
| 19 | + "http://localhost:2015" | |
| 20 | + ] | |
| 21 | + }, | |
| 22 | + "filesystem": { | |
| 23 | + "command": "npx", | |
| 24 | + "args": [ | |
| 25 | + "@modelcontextprotocol/server-filesystem", | |
| 26 | + "." | |
| 27 | + ] | |
| 28 | + }, | |
| 29 | + "excel-reader": { | |
| 30 | + "command": "npx", | |
| 31 | + "args": [ | |
| 32 | + "--yes", | |
| 33 | + "@negokaz/excel-mcp-server" | |
| 34 | + ] | |
| 35 | + } | |
| 36 | + } | |
| 37 | +} | ... | ... |
.cursor/skills/sqlsugar-type-conversion-debug/SKILL.md
0 → 100644
| 1 | +--- | |
| 2 | +name: sqlsugar-type-conversion-debug | |
| 3 | +description: SqlSugar 字段类型转换报错(如 "can't convert string to datetime")的排查与修复流程。在遇到 SqlSugar 查询返回类型转换异常、接口 500 报 convert 错误、DateTime/int/decimal 字段映射失败时使用。 | |
| 4 | +--- | |
| 5 | + | |
| 6 | +# SqlSugar 字段类型转换报错排查规范 | |
| 7 | + | |
| 8 | +## 何时使用 | |
| 9 | + | |
| 10 | +- 接口报 `500: xxx can't convert string to datetime`(或其他类型转换错误) | |
| 11 | +- SqlSugar 查询结果映射到 DTO 时抛出类型异常 | |
| 12 | +- 修改了 Select / Subqueryable / MergeTable 后出现新的转换报错 | |
| 13 | + | |
| 14 | +--- | |
| 15 | + | |
| 16 | +## 核心教训(来自真实案例) | |
| 17 | + | |
| 18 | +**错误做法**:看到报错就直接猜测并大范围修改 `SqlFunc.ToDate`、Subqueryable 等写法,反复改动导致问题更复杂,原本可用的代码被破坏。 | |
| 19 | + | |
| 20 | +**正确做法**:**先用 MCP 查库确认数据异常,再决定是否改代码**。 | |
| 21 | + | |
| 22 | +--- | |
| 23 | + | |
| 24 | +## 排查步骤 | |
| 25 | + | |
| 26 | +### Step 1:先查数据,不动代码 | |
| 27 | + | |
| 28 | +用 MCP MySQL 工具查该字段是否存在异常数据: | |
| 29 | + | |
| 30 | +```sql | |
| 31 | +-- 1. 确认字段实际类型 | |
| 32 | +SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE | |
| 33 | +FROM INFORMATION_SCHEMA.COLUMNS | |
| 34 | +WHERE TABLE_SCHEMA = DATABASE() | |
| 35 | + AND TABLE_NAME = '表名' | |
| 36 | + AND COLUMN_NAME = '字段名'; | |
| 37 | + | |
| 38 | +-- 2. 检查是否有无法转换的异常值(针对 varchar 存日期的情况) | |
| 39 | +SELECT id, 字段名 FROM 表名 | |
| 40 | +WHERE 字段名 IS NOT NULL | |
| 41 | + AND TRIM(字段名) != '' | |
| 42 | + AND STR_TO_DATE(字段名, '%Y-%m-%d %H:%i:%s') IS NULL | |
| 43 | + AND STR_TO_DATE(字段名, '%Y-%m-%d') IS NULL | |
| 44 | +LIMIT 20; | |
| 45 | +``` | |
| 46 | + | |
| 47 | +- 若字段类型已是 `datetime`,`SqlFunc.ToDate(x.Yysj)` 在 Subqueryable 里**是可以正常工作的**,不要随意删除。 | |
| 48 | +- 若发现真有脏数据(空字符串、格式错误),先修数据,再看接口是否恢复正常。 | |
| 49 | + | |
| 50 | +### Step 2:确认 Entity 声明与 DB 类型一致 | |
| 51 | + | |
| 52 | +```csharp | |
| 53 | +// Entity 声明 DateTime? 对应 DB datetime 列 ✅ | |
| 54 | +[SugarColumn(ColumnName = "yysj")] | |
| 55 | +public DateTime? Yysj { get; set; } | |
| 56 | + | |
| 57 | +// Entity 声明 string 对应 DB datetime 列 ❌ 会触发转换错误 | |
| 58 | +public string Yysj { get; set; } | |
| 59 | +``` | |
| 60 | + | |
| 61 | +如果 Entity 类型声明与 DB 实际类型不一致,修改 Entity 类型即可,**不要改查询逻辑**。 | |
| 62 | + | |
| 63 | +### Step 3:若确认是 MergeTable 导致的类型丢失 | |
| 64 | + | |
| 65 | +`.MergeTable()` 会把查询包成中间子查询,某些版本的 SqlSugar 在此过程中会将 `DateTime` 类型推导为 `varchar`。此时才需要修改查询逻辑,正确做法是**分页后批量回填**: | |
| 66 | + | |
| 67 | +```csharp | |
| 68 | +// ❌ 错误:MergeTable 后 Subqueryable 返回 DateTime,中间表映射为 string | |
| 69 | +.Select(it => new OutputDto { | |
| 70 | + appointmentTime = SqlFunc.Subqueryable<LqYyjlEntity>() | |
| 71 | + .Where(x => x.Id == it.AppointmentId) | |
| 72 | + .Select(x => SqlFunc.ToDate(x.Yysj)), | |
| 73 | +}) | |
| 74 | +.MergeTable() | |
| 75 | +.ToPagedListAsync(...); | |
| 76 | + | |
| 77 | +// ✅ 正确:Select 里先置 null,分页后批量回填 | |
| 78 | +.Select(it => new OutputDto { | |
| 79 | + appointmentId = it.AppointmentId, | |
| 80 | + appointmentTime = (DateTime?)null, // 先置 null | |
| 81 | +}) | |
| 82 | +.MergeTable() | |
| 83 | +.ToPagedListAsync(...); | |
| 84 | + | |
| 85 | +// 分页后批量查询回填 | |
| 86 | +var apptIds = data.list | |
| 87 | + .Where(x => !string.IsNullOrEmpty(x.appointmentId)) | |
| 88 | + .Select(x => x.appointmentId).Distinct().ToList(); | |
| 89 | +if (apptIds.Any()) | |
| 90 | +{ | |
| 91 | + var apptMap = await _db.Queryable<LqYyjlEntity>() | |
| 92 | + .Where(x => apptIds.Contains(x.Id)) | |
| 93 | + .Select(x => new { x.Id, x.Yysj }) | |
| 94 | + .ToListAsync(); | |
| 95 | + var apptDict = apptMap.ToDictionary(x => x.Id); | |
| 96 | + foreach (var item in data.list) | |
| 97 | + { | |
| 98 | + if (!string.IsNullOrEmpty(item.appointmentId) | |
| 99 | + && apptDict.TryGetValue(item.appointmentId, out var appt)) | |
| 100 | + item.appointmentTime = appt.Yysj; | |
| 101 | + } | |
| 102 | +} | |
| 103 | +``` | |
| 104 | + | |
| 105 | +### Step 4:确认修改编译通过后再重启 | |
| 106 | + | |
| 107 | +```bash | |
| 108 | +cd netcore/src/Application/NCC.API && dotnet build --no-restore 2>&1 | tail -5 | |
| 109 | +``` | |
| 110 | + | |
| 111 | +必须 **0 Error** 后才重启 API,再测试接口。 | |
| 112 | + | |
| 113 | +--- | |
| 114 | + | |
| 115 | +## 优先级原则 | |
| 116 | + | |
| 117 | +| 顺序 | 操作 | 目的 | | |
| 118 | +|------|------|------| | |
| 119 | +| 1 | 用 MCP 查库确认数据 | 排除脏数据,80% 的情况到这里就解决了 | | |
| 120 | +| 2 | 检查 Entity 类型声明 | 确认 C# 类型与 DB 类型一致 | | |
| 121 | +| 3 | 检查是否有 MergeTable + Subqueryable DateTime | 确认是框架行为还是数据问题 | | |
| 122 | +| 4 | 改代码(改查询逻辑或 Entity) | 最后手段,改前先理解原代码为什么大多数情况能用 | | |
| 123 | + | |
| 124 | +--- | |
| 125 | + | |
| 126 | +## 禁止事项 | |
| 127 | + | |
| 128 | +- ❌ **不要看到类型转换报错就盲目删除 `SqlFunc.ToDate`**——它在大多数场景是正确的 | |
| 129 | +- ❌ **不要在没有查清原因前大范围修改 Select / Subqueryable**——会破坏原本正常的逻辑 | |
| 130 | +- ❌ **不要反复重启 API 来"试"是否生效**——每次改动前先确认 `dotnet build` 0 Error | |
| 131 | +- ❌ **原代码如果在 HEAD 中长期工作,优先怀疑是数据问题,而不是代码问题** | ... | ... |
.cursor/skills/workflow-form-development/SKILL.md
0 → 100644
| 1 | +--- | |
| 2 | +name: workflow-form-development | |
| 3 | +description: 工作流审批表单开发与流程配置规范。在开发新的审批表单页面、把表单配置到流程引擎、修改流程表单字段映射时使用。触发词:审批表单、流程表单、配置到流程、表单字段映射、FlowBox、formOperates。 | |
| 4 | +--- | |
| 5 | + | |
| 6 | +# 工作流审批表单开发与流程配置 | |
| 7 | + | |
| 8 | +## 一、何时使用 | |
| 9 | + | |
| 10 | +- 新增或修改审批申请表单页面(如请假、报销等) | |
| 11 | +- 用户要求"把表单配置到流程上" | |
| 12 | +- 表单在 FlowBox 中渲染报错(如 `init is not a function`、404 等) | |
| 13 | +- 配置流程设计器中的表单字段映射 | |
| 14 | + | |
| 15 | +--- | |
| 16 | + | |
| 17 | +## 二、开发审批表单页面 | |
| 18 | + | |
| 19 | +### 2.1 FlowBox 兼容要求(必须) | |
| 20 | + | |
| 21 | +框架组件 `FlowBox.vue` 通过 `ref="form"` 引用表单页面,并在生命周期中调用以下方法: | |
| 22 | + | |
| 23 | +| 方法 | 调用时机 | 说明 | | |
| 24 | +|------|----------|------| | |
| 25 | +| `init(data)` | 页面加载后 | 初始化表单,`data` 包含 `{id, flowId, enCode, readonly, formOperates, opType}` | | |
| 26 | +| `dataFormSubmit(eventType)` | 用户点击审批/驳回/提交等按钮 | 校验表单并 emit 数据 | | |
| 27 | + | |
| 28 | +FlowBox 监听的事件: | |
| 29 | + | |
| 30 | +| 事件 | 说明 | | |
| 31 | +|------|------| | |
| 32 | +| `eventReciver(formData, eventType)` | 提交表单数据,两个参数 | | |
| 33 | +| `setPageLoad` | 通知加载完成 | | |
| 34 | +| `setLoad` | 控制 loading 状态 | | |
| 35 | +| `close` | 关闭页面 | | |
| 36 | + | |
| 37 | +### 2.2 已有 mixin 方案 vs 自定义方案 | |
| 38 | + | |
| 39 | +**方案 A:使用框架 mixin(简单表单推荐)** | |
| 40 | + | |
| 41 | +```js | |
| 42 | +import comMixin from '../mixin' | |
| 43 | +export default { | |
| 44 | + mixins: [comMixin], | |
| 45 | + // mixin 自动提供 init、dataFormSubmit、judgeShow、judgeWrite | |
| 46 | +} | |
| 47 | +``` | |
| 48 | + | |
| 49 | +**方案 B:自定义实现(复杂表单,如请假申请)** | |
| 50 | + | |
| 51 | +当表单有自己的独立逻辑(额度查询、业务校验等),不能直接用 mixin 时: | |
| 52 | + | |
| 53 | +1. 在表单组件中自行实现 `init(data)` 和 `dataFormSubmit(eventType)` | |
| 54 | +2. `init` 中需完成:存 setting → 重置表单 → 加载数据/生成单据号 → emit `setPageLoad` | |
| 55 | +3. `dataFormSubmit` 中需完成:校验表单 → 校验业务规则 → emit `eventReciver(payload, eventType)` | |
| 56 | + | |
| 57 | +### 2.3 Wrapper 页面代理模式 | |
| 58 | + | |
| 59 | +如果使用「公共组件 + 场景 wrapper」的架构(如 `leave-apply-page.vue` + `rest-leave-apply/index.vue`),wrapper 必须代理方法和事件: | |
| 60 | + | |
| 61 | +```vue | |
| 62 | +<template> | |
| 63 | + <leave-apply-page ref="leaveForm" scene="rest" v-on="$listeners" /> | |
| 64 | +</template> | |
| 65 | + | |
| 66 | +<script> | |
| 67 | +import LeaveApplyPage from '../components/leave-apply-page' | |
| 68 | +export default { | |
| 69 | + components: { LeaveApplyPage }, | |
| 70 | + methods: { | |
| 71 | + init(data) { | |
| 72 | + this.$refs.leaveForm.init(data) | |
| 73 | + }, | |
| 74 | + dataFormSubmit(eventType) { | |
| 75 | + this.$refs.leaveForm.dataFormSubmit(eventType) | |
| 76 | + } | |
| 77 | + } | |
| 78 | +} | |
| 79 | +</script> | |
| 80 | +``` | |
| 81 | + | |
| 82 | +关键点: | |
| 83 | +- 用 `v-on="$listeners"` 自动转发所有事件给 FlowBox(`eventReciver` 有两个参数,手动转发会丢参数) | |
| 84 | +- 必须代理 `init` 和 `dataFormSubmit`(FlowBox 通过 `$refs.form` 直接调用) | |
| 85 | + | |
| 86 | +### 2.4 API Key 不等于流程 enCode(重要陷阱) | |
| 87 | + | |
| 88 | +前端 `Info(key, id)` 函数会把 key 首字母大写后拼 URL:`/api/workflow/Form/${Key}/${id}` | |
| 89 | + | |
| 90 | +**这里的 key 是后端控制器名,不是流程的 enCode!** | |
| 91 | + | |
| 92 | +| 后端控制器 | 路由 | 正确的 key | | |
| 93 | +|-----------|------|-----------| | |
| 94 | +| `LeaveApplyService` | `/api/workflow/Form/LeaveApply/` | `'leaveApply'` | | |
| 95 | + | |
| 96 | +三种请假流程(应休 `xjsq`、事假 `qjsq`、带薪 `ces`)都共用 `LeaveApplyService`,所以调用 `Info`、`Create`、`Update` 时必须固定传 `'leaveApply'`,而不是 `this.setting.enCode`。 | |
| 97 | + | |
| 98 | +```js | |
| 99 | +// ✅ 正确 | |
| 100 | +Info('leaveApply', data.id) | |
| 101 | + | |
| 102 | +// ❌ 错误 - enCode 是 xjsq,拼出 /api/workflow/Form/Xjsq/xxx → 404 | |
| 103 | +Info(this.setting.enCode, data.id) | |
| 104 | +``` | |
| 105 | + | |
| 106 | +### 2.5 FlowBox 模式下的 UI 适配 | |
| 107 | + | |
| 108 | +- 在 FlowBox 中渲染时,应隐藏页面自带的标题栏和按钮(FlowBox 有自己的操作按钮) | |
| 109 | +- 表单需支持 `setting.readonly` 只读模式(审批查看时) | |
| 110 | +- 可用 `flowBoxMode` 标记区分独立页面和 FlowBox 内嵌模式 | |
| 111 | + | |
| 112 | +### 2.6 前端语法限制 | |
| 113 | + | |
| 114 | +项目 Babel/webpack 配置不支持 ES2020+ 语法: | |
| 115 | +- ❌ `??`(空值合并) → 用 `a != null ? a : b` | |
| 116 | +- ❌ `?.`(可选链) → 用 `a && a.b` | |
| 117 | +- ✅ 箭头函数、展开运算符、模板字符串等 ES6 语法正常支持 | |
| 118 | + | |
| 119 | +--- | |
| 120 | + | |
| 121 | +## 三、把表单配置到流程上 | |
| 122 | + | |
| 123 | +当用户说"把表单配置到流程上"时,需要做以下 4 件事: | |
| 124 | + | |
| 125 | +### 3.1 配置 PC/App 页面路径 | |
| 126 | + | |
| 127 | +更新 `flow_engine` 表的 `F_FormUrl` 和 `F_AppFormUrl` 字段: | |
| 128 | + | |
| 129 | +```sql | |
| 130 | +UPDATE flow_engine | |
| 131 | +SET F_FormUrl = 'workFlow/leave-apply-pages/rest-leave-apply' | |
| 132 | +WHERE F_Id = '<流程ID>'; | |
| 133 | +``` | |
| 134 | + | |
| 135 | +- `F_FormUrl`:PC 端页面路径(不带前缀 `/`,对应 `antis-ncc-admin/src/views/` 下的路径) | |
| 136 | +- `F_AppFormUrl`:App 端页面路径(带前缀 `/`,对应 uni-app 的页面路径) | |
| 137 | + | |
| 138 | +### 3.2 配置表单字段映射(formOperates) | |
| 139 | + | |
| 140 | +更新 `flow_engine.F_FlowTemplateJson` 中每个节点的 `properties.formOperates` 数组。 | |
| 141 | + | |
| 142 | +**字段格式:** | |
| 143 | + | |
| 144 | +```json | |
| 145 | +{ | |
| 146 | + "id": "flowTitle", | |
| 147 | + "name": "流程标题", | |
| 148 | + "required": false, | |
| 149 | + "read": true, | |
| 150 | + "write": true | |
| 151 | +} | |
| 152 | +``` | |
| 153 | + | |
| 154 | +| 属性 | 说明 | | |
| 155 | +|------|------| | |
| 156 | +| `id` | 字段标识,对应 dataForm 的属性名 | | |
| 157 | +| `name` | 在流程设计器中显示的名称 | | |
| 158 | +| `required` | 是否必填 | | |
| 159 | +| `read` | 是否可见 | | |
| 160 | +| `write` | 是否可编辑 | | |
| 161 | + | |
| 162 | +**节点权限区别:** | |
| 163 | + | |
| 164 | +| 节点 | read | write | 说明 | | |
| 165 | +|------|------|-------|------| | |
| 166 | +| 发起节点(start) | true | true | 发起人可编辑 | | |
| 167 | +| 审批节点(approver) | true | false | 审批人只读查看 | | |
| 168 | + | |
| 169 | +### 3.3 配置表单字段定义(F_FormTemplateJson) | |
| 170 | + | |
| 171 | +**这一步容易遗漏!** `flow_engine` 表有两个不同的 JSON 字段,必须都配置: | |
| 172 | + | |
| 173 | +| 字段 | 用途 | 缺失后果 | | |
| 174 | +|------|------|----------| | |
| 175 | +| `F_FlowTemplateJson` → `formOperates` | 每个节点对字段的 read/write 权限控制 | 审批节点无法控制字段可见/可编辑 | | |
| 176 | +| `F_FormTemplateJson` | 流程设计器"表单字段"下拉列表定义 | **设计器里看不到字段列表,无法在界面上配置字段权限** | | |
| 177 | + | |
| 178 | +`F_FormTemplateJson` 是一个 JSON 数组,格式如下: | |
| 179 | + | |
| 180 | +```json | |
| 181 | +[ | |
| 182 | + {"filedName": "流程标题", "filedId": "flowTitle", "required": false}, | |
| 183 | + {"filedName": "单据号", "filedId": "billNo", "required": false}, | |
| 184 | + {"filedName": "休假类别", "filedId": "leaveType", "required": false} | |
| 185 | +] | |
| 186 | +``` | |
| 187 | + | |
| 188 | +| 属性 | 说明 | | |
| 189 | +|------|------| | |
| 190 | +| `filedName` | 在流程设计器中显示的字段名称(注意:原始字段名就是 `filedName`,不是 `fieldName`,这是框架的拼写) | | |
| 191 | +| `filedId` | 字段标识,必须与 `formOperates` 中的 `id` 以及 dataForm 的属性名一致 | | |
| 192 | +| `required` | 是否必填 | | |
| 193 | + | |
| 194 | +**示例 SQL:** | |
| 195 | + | |
| 196 | +```sql | |
| 197 | +UPDATE flow_engine | |
| 198 | +SET F_FormTemplateJson = '[{"filedName":"流程标题","filedId":"flowTitle","required":false},{"filedName":"单据号","filedId":"billNo","required":false},{"filedName":"紧急程度","filedId":"flowUrgent","required":false},{"filedName":"申请人员","filedId":"applyUser","required":false},{"filedName":"申请日期","filedId":"applyDate","required":false},{"filedName":"申请部门","filedId":"applyDept","required":false},{"filedName":"申请职位","filedId":"applyPost","required":false},{"filedName":"休假类别","filedId":"leaveType","required":false},{"filedName":"休假原因","filedId":"leaveReason","required":false},{"filedName":"开始时间","filedId":"leaveStartTime","required":false},{"filedName":"结束时间","filedId":"leaveEndTime","required":false},{"filedName":"请假天数","filedId":"leaveDayCount","required":false},{"filedName":"请假小时","filedId":"leaveHour","required":false},{"filedName":"相关附件","filedId":"fileJson","required":false}]' | |
| 199 | +WHERE F_Id = '<流程ID>'; | |
| 200 | +``` | |
| 201 | + | |
| 202 | +### 3.5 查询方法 | |
| 203 | + | |
| 204 | +```sql | |
| 205 | +-- 查看某流程的完整配置(含两个 JSON 字段) | |
| 206 | +SELECT F_Id, F_FullName, F_EnCode, F_FormUrl, F_AppFormUrl, | |
| 207 | + F_FormTemplateJson, F_FlowTemplateJson | |
| 208 | +FROM flow_engine | |
| 209 | +WHERE F_Id = '<流程ID>'; | |
| 210 | + | |
| 211 | +-- 参考已有流程的字段配置(如事假病假申请) | |
| 212 | +SELECT F_FormTemplateJson, F_FlowTemplateJson FROM flow_engine WHERE F_EnCode = 'qjsq'; | |
| 213 | +``` | |
| 214 | + | |
| 215 | +### 3.6 注意事项 | |
| 216 | + | |
| 217 | +- MCP 数据库是只读的,UPDATE 语句需要提供给用户手动执行 | |
| 218 | +- 修改 `F_FlowTemplateJson` 时要保留原有节点结构(nodeId、prevId、审批人配置等),只替换 `formOperates` 部分 | |
| 219 | +- 字段 ID 必须与表单页面的 dataForm model 属性名完全一致 | |
| 220 | +- 新增流程后必须确保前端路由能匹配到对应的 Vue 页面组件 | |
| 221 | + | |
| 222 | +--- | |
| 223 | + | |
| 224 | +## 四、完整检查清单 | |
| 225 | + | |
| 226 | +开发或修改审批表单后,按此清单逐项确认: | |
| 227 | + | |
| 228 | +- [ ] 表单组件有 `init(data)` 方法 | |
| 229 | +- [ ] 表单组件有 `dataFormSubmit(eventType)` 方法 | |
| 230 | +- [ ] wrapper 页面通过 `$refs` 代理了上述两个方法 | |
| 231 | +- [ ] wrapper 页面用 `v-on="$listeners"` 转发事件 | |
| 232 | +- [ ] API 调用使用正确的控制器名(如 `'leaveApply'`),不是 enCode | |
| 233 | +- [ ] 表单支持 `setting.readonly` 只读模式 | |
| 234 | +- [ ] 无 `??` 或 `?.` 语法 | |
| 235 | +- [ ] `flow_engine.F_FormUrl` 已配置 PC 页面路径 | |
| 236 | +- [ ] `flow_engine.F_AppFormUrl` 已配置 App 页面路径 | |
| 237 | +- [ ] `F_FormTemplateJson` 已配置表单字段定义(设计器字段列表) | |
| 238 | +- [ ] `F_FlowTemplateJson` 中 formOperates 字段映射完整(节点权限) | |
| 239 | +- [ ] `F_FormTemplateJson` 的 `filedId` 与 `formOperates` 的 `id` 与 dataForm 属性名三者一致 | ... | ... |
CLAUDE.md
0 → 100644
| 1 | +# 绿纤美业 ERP · Claude Code 工作指引 | |
| 2 | + | |
| 3 | +> 本文件供 Claude Code(CLI)自动读取。原始规则与技能仍保存在 `.cursor/` 目录下,不做任何改动。 | |
| 4 | +> Claude Code 专用的 agents 在 `.claude/agents/`,slash commands 在 `.claude/commands/`,MCP 配置在 `.claude/settings.json`。 | |
| 5 | + | |
| 6 | +--- | |
| 7 | + | |
| 8 | +## 强制约定 | |
| 9 | + | |
| 10 | +- **回复前缀**:每次回复必须以"大哥"开头。 | |
| 11 | +- 未被明确要求时,不要生成新的 Markdown 文档。 | |
| 12 | +- 单次改动最小化,先看上下游再改。 | |
| 13 | + | |
| 14 | +--- | |
| 15 | + | |
| 16 | +## 项目总览 | |
| 17 | + | |
| 18 | +| 端 | 目录 | 技术 | | |
| 19 | +|---|---|---| | |
| 20 | +| 后端 | `netcore/` | ASP.NET Core + SqlSugar + MySQL + JWT + Serilog | | |
| 21 | +| 管理后台 | `antis-ncc-admin/` | Vue 2.6 + Element UI + Vuex + Axios | | |
| 22 | +| 门店 PC | `store-pc/` | Vue 2.6 + Element UI + Axios | | |
| 23 | +| 移动端 | `绿纤uni-app/` | uni-app(微信小程序) | | |
| 24 | +| 文档/SQL/脚本 | `项目文档相关/` | Markdown + Shell + Python + SQL | | |
| 25 | + | |
| 26 | +## 关键目录 | |
| 27 | + | |
| 28 | +- 后端主入口:`netcore/src/Application/NCC.API/` | |
| 29 | +- 业务核心:`netcore/src/Modularity/Extend/`(Entitys → Interfaces → Services) | |
| 30 | +- 管理后台页面:`antis-ncc-admin/src/views/`,接口:`antis-ncc-admin/src/api/` | |
| 31 | +- 门店 PC 页面:`store-pc/src/views/` | |
| 32 | +- 数据库文档:`项目文档相关/docs/数据库说明.md` | |
| 33 | + | |
| 34 | +--- | |
| 35 | + | |
| 36 | +## 前端规范 | |
| 37 | + | |
| 38 | +- Node.js 必须使用 `16.20.2` | |
| 39 | +- GET 请求统一使用 `data` 传参,不使用 `params` | |
| 40 | +- 表格优先使用 `NCC-table` | |
| 41 | +- 弹窗、二级页、复杂表单必须拆为独立 `.vue` 文件(禁止在主页面 template 内直接写) | |
| 42 | +- 文件命名使用 `kebab-case` | |
| 43 | +- 操作按钮左对齐;列表内容不换行;空值显示"无" | |
| 44 | +- 卡片高度 100px、内边距 12px、圆角 12px | |
| 45 | +- 主色 `#409EFF`,辅助色 `#67C23A` / `#F56C6C` / `#909399` | |
| 46 | + | |
| 47 | +## 后端规范 | |
| 48 | + | |
| 49 | +- 不需要在 `NCC.API` 新建 Controller;`Extend` 中的 Service 直接暴露 | |
| 50 | +- 新实体 ID 必须使用 `YitIdHelper.NextId().ToString()`,禁止 `Guid.NewGuid()` | |
| 51 | +- 固定状态/类型必须使用 `enum`,并写 XML 注释 | |
| 52 | +- 列表接口必须分页 | |
| 53 | +- 查询条件优先 `WhereIF`,避免拼接 SQL | |
| 54 | +- 统计接口与列表接口必须使用完全一致的筛选条件、时间范围、权限控制与字段命名 | |
| 55 | +- 关键 API / 方法需要 XML 注释 | |
| 56 | + | |
| 57 | +## 数据口径 | |
| 58 | + | |
| 59 | +- 人员信息优先使用 `BASE_USER`,不要依赖 `lq_ryzl` | |
| 60 | +- 门店归属按月份从 `lq_md_target` 取,禁止使用 `lq_mdxx` 上的弃用归属字段 | |
| 61 | +- 表结构/字段说明变更后同步更新 `项目文档相关/docs/数据库说明.md` | |
| 62 | +- `base_organize.DeleteMark` 为 `null` 表示未删除 | |
| 63 | + | |
| 64 | +--- | |
| 65 | + | |
| 66 | +## Agents(子代理) | |
| 67 | + | |
| 68 | +Claude Code agents 配置在 `.claude/agents/`,对应 `.cursor/agents/` 中的角色: | |
| 69 | + | |
| 70 | +| Agent | 文件 | 职责 | | |
| 71 | +|---|---|---| | |
| 72 | +| orchestrator | `.claude/agents/orchestrator.md` | 任务分析与委派 | | |
| 73 | +| 后端 | `.claude/agents/backend-developer.md` | C# API / Service / DB | | |
| 74 | +| 前端 | `.claude/agents/frontend-developer.md` | Vue 2 页面 / 组件 | | |
| 75 | +| 测试 | `.claude/agents/test-engineer.md` | 接口测试与验证 | | |
| 76 | +| verifier | `.claude/agents/verifier.md` | 最终交付验收 | | |
| 77 | + | |
| 78 | +**任务分级委派原则**: | |
| 79 | +- L1(解释/评估/判断)→ 直接回答 | |
| 80 | +- L2(仅后端 / 仅前端)→ 启动对应 agent | |
| 81 | +- L3(跨角色)→ 并行启动多个 agent | |
| 82 | + | |
| 83 | +--- | |
| 84 | + | |
| 85 | +## Slash Commands(技能) | |
| 86 | + | |
| 87 | +Claude Code 命令配置在 `.claude/commands/`,对应 `.cursor/skills/` 中的 skill: | |
| 88 | + | |
| 89 | +| 命令 | 对应 Skill | 用途 | | |
| 90 | +|---|---|---| | |
| 91 | +| `/api-interface-testing` | `api-interface-testing` | 获取 Token、curl 接口测试流程 | | |
| 92 | +| `/api-xml-comments` | `api-xml-comments` | API XML 注释格式与模板 | | |
| 93 | +| `/deprecated-tables` | `deprecated-tables-context` | 已弃用表及替代方案 | | |
| 94 | +| `/mcp-mysql` | `mcp-mysql-and-sql-validation` | MCP MySQL 查库与 SQL 验证规范 | | |
| 95 | +| `/remember` | `remember-as-rule-or-skill` | 持久化规则或 Skill | | |
| 96 | +| `/ui-ux` | `ui-ux-pro-max` | UI/UX 设计系统生成 | | |
| 97 | + | |
| 98 | +--- | |
| 99 | + | |
| 100 | +## MCP 配置 | |
| 101 | + | |
| 102 | +MCP servers 配置在 `.claude/settings.json`(`mcpServers` 字段),与 `.cursor/mcp.json` 保持一致: | |
| 103 | + | |
| 104 | +| 服务 | 用途 | | |
| 105 | +|---|---| | |
| 106 | +| `my-sql-db` | MySQL 只读查询(lqerp_dev),使用 `@davewind/mysql-mcp-server` | | |
| 107 | +| `my-api-spec` | OpenAPI Spec 浏览与接口调用,使用 `@ivotoby/openapi-mcp-server` | | |
| 108 | +| `filesystem` | 文件系统访问,使用 `@modelcontextprotocol/server-filesystem` | | |
| 109 | +| `excel-reader` | Excel 文件读取,使用 `@negokaz/excel-mcp-server` | | |
| 110 | + | |
| 111 | +> 修改 MCP 时以 `.cursor/mcp.json` 为单一事实来源,再同步到 `.claude/settings.json`。 | |
| 112 | +> 同步脚本:`python3 项目文档相关/scripts/py/sync_cursor_mcp_to_codex.py` | |
| 113 | + | |
| 114 | +--- | |
| 115 | + | |
| 116 | +## 启动命令 | |
| 117 | + | |
| 118 | +```bash | |
| 119 | +# 后端 | |
| 120 | +cd netcore/src/Application/NCC.API && dotnet restore && dotnet run | |
| 121 | + | |
| 122 | +# 管理后台 | |
| 123 | +cd antis-ncc-admin && npm install && npm run dev # http://localhost:3000 | |
| 124 | + | |
| 125 | +# 门店 PC | |
| 126 | +cd store-pc && npm install && npm run dev # http://localhost:3100 | |
| 127 | +``` | |
| 128 | + | |
| 129 | +默认账号:`admin / 123456`,后端 API:`http://localhost:5000`,Swagger:`http://localhost:5000/antis.doc` | |
| 130 | + | |
| 131 | +--- | |
| 132 | + | |
| 133 | +## 参考资料(原始,保留在 .cursor/) | |
| 134 | + | |
| 135 | +- 项目总规则:`.cursor/rules/project_rules.mdc` | |
| 136 | +- Orchestrator 优先规则:`.cursor/rules/orchestrator-first.mdc` | |
| 137 | +- 接口测试:`.cursor/skills/api-interface-testing/SKILL.md` | |
| 138 | +- API 注释:`.cursor/skills/api-xml-comments/SKILL.md` | |
| 139 | +- 查库/SQL 验证:`.cursor/skills/mcp-mysql-and-sql-validation/SKILL.md` | |
| 140 | +- 已弃用表:`.cursor/skills/deprecated-tables-context/SKILL.md` | |
| 141 | +- 规则持久化:`.cursor/skills/remember-as-rule-or-skill/SKILL.md` | |
| 142 | +- UI/UX:`.cursor/skills/ui-ux-pro-max/SKILL.md` | ... | ... |
antis-ncc-admin/src/views/attendance-setting/components/attendance-config-item-dialog.vue
| 1 | 1 | <template> |
| 2 | - <el-dialog | |
| 3 | - :title="dialogTitle" | |
| 4 | - :visible.sync="visible" | |
| 5 | - width="720px" | |
| 6 | - append-to-body | |
| 7 | - :close-on-click-modal="false" | |
| 8 | - @close="handleClose" | |
| 9 | - > | |
| 2 | + <el-dialog :title="dialogTitle" :visible.sync="visible" width="720px" append-to-body :close-on-click-modal="false" | |
| 3 | + @close="handleClose"> | |
| 10 | 4 | <el-form ref="formRef" :model="form" :rules="rules" label-width="150px" size="small"> |
| 11 | 5 | <template v-if="moduleType === 'base'"> |
| 12 | 6 | <el-form-item label="请假扣款(日薪倍率)" prop="leaveDeductDailySalaryRate"> |
| 13 | - <el-input-number | |
| 14 | - v-model="form.leaveDeductDailySalaryRate" | |
| 15 | - :min="0" | |
| 16 | - :precision="2" | |
| 17 | - controls-position="right" | |
| 18 | - style="width: 100%" | |
| 19 | - /> | |
| 7 | + <el-input-number v-model="form.leaveDeductDailySalaryRate" :min="0" :precision="2" controls-position="right" | |
| 8 | + style="width: 100%" /> | |
| 20 | 9 | </el-form-item> |
| 21 | 10 | <el-form-item label="病假扣款(日薪倍率)" prop="sickLeaveDeductDailySalaryRate"> |
| 22 | - <el-input-number | |
| 23 | - v-model="form.sickLeaveDeductDailySalaryRate" | |
| 24 | - :min="0" | |
| 25 | - :precision="2" | |
| 26 | - controls-position="right" | |
| 27 | - style="width: 100%" | |
| 28 | - /> | |
| 11 | + <el-input-number v-model="form.sickLeaveDeductDailySalaryRate" :min="0" :precision="2" | |
| 12 | + controls-position="right" style="width: 100%" /> | |
| 29 | 13 | </el-form-item> |
| 30 | 14 | </template> |
| 31 | 15 | |
| 32 | 16 | <template v-else-if="moduleType === 'holiday'"> |
| 33 | 17 | <el-form-item label="公休日期" prop="holidayDate"> |
| 34 | - <el-date-picker | |
| 35 | - v-model="form.holidayDate" | |
| 36 | - type="date" | |
| 37 | - value-format="yyyy-MM-dd" | |
| 38 | - format="yyyy-MM-dd" | |
| 39 | - placeholder="选择日期" | |
| 40 | - style="width: 100%" | |
| 41 | - /> | |
| 18 | + <el-date-picker v-model="form.holidayDate" type="date" value-format="yyyy-MM-dd" format="yyyy-MM-dd" | |
| 19 | + placeholder="选择日期" style="width: 100%" /> | |
| 42 | 20 | </el-form-item> |
| 43 | 21 | <el-form-item label="节假日名称" prop="holidayName"> |
| 44 | 22 | <el-input v-model.trim="form.holidayName" maxlength="100" show-word-limit placeholder="例如:春节公休" /> |
| ... | ... | @@ -50,36 +28,59 @@ |
| 50 | 28 | <el-input v-model.trim="form.groupName" maxlength="100" show-word-limit placeholder="例如:门店组" /> |
| 51 | 29 | </el-form-item> |
| 52 | 30 | <el-form-item label="上班时间" prop="workStartTime"> |
| 53 | - <el-time-picker | |
| 54 | - v-model="form.workStartTime" | |
| 55 | - value-format="HH:mm" | |
| 56 | - format="HH:mm" | |
| 57 | - placeholder="09:00" | |
| 58 | - style="width: 100%" | |
| 59 | - /> | |
| 31 | + <el-time-picker v-model="form.workStartTime" value-format="HH:mm" format="HH:mm" placeholder="09:00" | |
| 32 | + style="width: 100%" /> | |
| 60 | 33 | </el-form-item> |
| 61 | 34 | <el-form-item label="下班时间" prop="workEndTime"> |
| 62 | - <el-time-picker | |
| 63 | - v-model="form.workEndTime" | |
| 64 | - value-format="HH:mm" | |
| 65 | - format="HH:mm" | |
| 66 | - placeholder="19:00" | |
| 67 | - style="width: 100%" | |
| 68 | - /> | |
| 35 | + <el-time-picker v-model="form.workEndTime" value-format="HH:mm" format="HH:mm" placeholder="19:00" | |
| 36 | + style="width: 100%" /> | |
| 69 | 37 | </el-form-item> |
| 70 | 38 | <el-form-item label="月应休天数" prop="monthlyRestDays"> |
| 71 | - <el-input-number v-model="form.monthlyRestDays" :min="0" :precision="0" controls-position="right" style="width: 100%" /> | |
| 39 | + <el-input-number v-model="form.monthlyRestDays" :min="0" :precision="0" controls-position="right" | |
| 40 | + style="width: 100%" /> | |
| 72 | 41 | </el-form-item> |
| 73 | 42 | <el-form-item label="可拆分半天休假天数" prop="halfDaySplitRestDays"> |
| 74 | - <el-input-number | |
| 75 | - v-model="form.halfDaySplitRestDays" | |
| 76 | - :min="0" | |
| 77 | - :precision="0" | |
| 78 | - :step="1" | |
| 79 | - controls-position="right" | |
| 80 | - style="width: 100%" | |
| 81 | - /> | |
| 82 | - <div style="margin-top: 6px; color: #909399; line-height: 1.6;">这里填写整数,表示本月有多少天允许拆成半天休假。例如月应休 4 天、此项填 1,则最多可拆成 2 次半天休假,其余 3 天只能整天休。</div> | |
| 43 | + <el-input-number v-model="form.halfDaySplitRestDays" :min="0" :precision="0" :step="1" | |
| 44 | + controls-position="right" style="width: 100%" /> | |
| 45 | + <div style="margin-top: 6px; color: #909399; line-height: 1.6;">这里填写整数,表示本月有多少天允许拆成半天休假。例如月应休 4 天、此项填 1,则最多可拆成 | |
| 46 | + 2 次半天休假,其余 3 天只能整天休。</div> | |
| 47 | + </el-form-item> | |
| 48 | + <el-form-item label="应休解锁规则"> | |
| 49 | + <el-switch :value="form.restUnlockCycle > 0" @change="handleRestUnlockToggle" style="margin-right: 8px" /> | |
| 50 | + <template v-if="form.restUnlockCycle > 0"> | |
| 51 | + <span style="color: #606266; margin-right: 6px;">每上满</span> | |
| 52 | + <el-input-number v-model="form.restUnlockCycle" :min="1" :max="365" :precision="0" controls-position="right" | |
| 53 | + style="width: 100px; margin-right: 6px" /> | |
| 54 | + <span style="color: #606266;">天,解锁 1 天应休</span> | |
| 55 | + </template> | |
| 56 | + <span v-else style="color: #909399; margin-left: 4px;">不启用</span> | |
| 57 | + <div style="margin-top: 6px; color: #909399; line-height: 1.6;">启用后,员工需上满指定天数才能逐步解锁当月应休额度。例如月应休 4 天、此项填 7,则上满 | |
| 58 | + 7 天解锁 1 | |
| 59 | + 天,上满 14 天解锁 2 天,以此类推。</div> | |
| 60 | + </el-form-item> | |
| 61 | + <el-form-item v-if="form.restUnlockCycle > 0" label="迟到容忍规则"> | |
| 62 | + <el-switch :value="form.lateToleranceMinutes != null" | |
| 63 | + @change="(v) => { form.lateToleranceMinutes = v ? 15 : null }" style="margin-right: 8px" /> | |
| 64 | + <template v-if="form.lateToleranceMinutes != null"> | |
| 65 | + <span style="color: #606266; margin-right: 6px;">迟到不超过</span> | |
| 66 | + <el-input-number v-model="form.lateToleranceMinutes" :min="0" :max="999" :precision="0" | |
| 67 | + controls-position="right" style="width: 100px; margin-right: 6px" /> | |
| 68 | + <span style="color: #606266;">分钟,计为有效上班</span> | |
| 69 | + </template> | |
| 70 | + <span v-else style="color: #909399; margin-left: 4px;">迟到不计为有效上班</span> | |
| 71 | + <div style="margin-top: 6px; color: #909399; line-height: 1.6;">用于解锁规则的有效上班天数统计。启用后,迟到在容忍时间内仍计为该天有效上班。</div> | |
| 72 | + </el-form-item> | |
| 73 | + <el-form-item v-if="form.restUnlockCycle > 0" label="早退容忍规则"> | |
| 74 | + <el-switch :value="form.earlyLeaveToleranceMinutes != null" | |
| 75 | + @change="(v) => { form.earlyLeaveToleranceMinutes = v ? 15 : null }" style="margin-right: 8px" /> | |
| 76 | + <template v-if="form.earlyLeaveToleranceMinutes != null"> | |
| 77 | + <span style="color: #606266; margin-right: 6px;">早退不超过</span> | |
| 78 | + <el-input-number v-model="form.earlyLeaveToleranceMinutes" :min="0" :max="999" :precision="0" | |
| 79 | + controls-position="right" style="width: 100px; margin-right: 6px" /> | |
| 80 | + <span style="color: #606266;">分钟,计为有效上班</span> | |
| 81 | + </template> | |
| 82 | + <span v-else style="color: #909399; margin-left: 4px;">早退不计为有效上班</span> | |
| 83 | + <div style="margin-top: 6px; color: #909399; line-height: 1.6;">用于解锁规则的有效上班天数统计。启用后,早退在容忍时间内仍计为该天有效上班。</div> | |
| 83 | 84 | </el-form-item> |
| 84 | 85 | <el-form-item label="是否启用" prop="isEnabled"> |
| 85 | 86 | <el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" /> |
| ... | ... | @@ -88,37 +89,46 @@ |
| 88 | 89 | |
| 89 | 90 | <template v-else-if="isYearRangeModule"> |
| 90 | 91 | <el-form-item label="最小司龄(含)" prop="minYears"> |
| 91 | - <el-input-number v-model="form.minYears" :min="0" :precision="0" controls-position="right" style="width: 100%" /> | |
| 92 | + <el-input-number v-model="form.minYears" :min="0" :precision="0" controls-position="right" | |
| 93 | + style="width: 100%" /> | |
| 92 | 94 | </el-form-item> |
| 93 | 95 | <el-form-item label="最大司龄(不含)" prop="maxYears"> |
| 94 | - <el-input-number v-model="form.maxYears" :min="0" :precision="0" controls-position="right" style="width: 100%" /> | |
| 96 | + <el-input-number v-model="form.maxYears" :min="0" :precision="0" controls-position="right" | |
| 97 | + style="width: 100%" /> | |
| 95 | 98 | </el-form-item> |
| 96 | 99 | <el-form-item :label="yearRangeLeaveDaysLabel" prop="leaveDays"> |
| 97 | - <el-input-number v-model="form.leaveDays" :min="0" :precision="0" controls-position="right" style="width: 100%" /> | |
| 100 | + <el-input-number v-model="form.leaveDays" :min="0" :precision="0" controls-position="right" | |
| 101 | + style="width: 100%" /> | |
| 98 | 102 | </el-form-item> |
| 99 | 103 | </template> |
| 100 | 104 | |
| 101 | 105 | <template v-else-if="moduleType === 'funeralLeaveRule'"> |
| 102 | 106 | <el-form-item label="最小司龄(含)" prop="minYears"> |
| 103 | - <el-input-number v-model="form.minYears" :min="0" :precision="0" controls-position="right" style="width: 100%" /> | |
| 107 | + <el-input-number v-model="form.minYears" :min="0" :precision="0" controls-position="right" | |
| 108 | + style="width: 100%" /> | |
| 104 | 109 | </el-form-item> |
| 105 | 110 | <el-form-item label="最大司龄(不含)" prop="maxYears"> |
| 106 | - <el-input-number v-model="form.maxYears" :min="0" :precision="0" controls-position="right" style="width: 100%" /> | |
| 111 | + <el-input-number v-model="form.maxYears" :min="0" :precision="0" controls-position="right" | |
| 112 | + style="width: 100%" /> | |
| 107 | 113 | </el-form-item> |
| 108 | 114 | <el-form-item label="直系亲属天数" prop="directRelativeDays"> |
| 109 | - <el-input-number v-model="form.directRelativeDays" :min="0" :precision="0" controls-position="right" style="width: 100%" /> | |
| 115 | + <el-input-number v-model="form.directRelativeDays" :min="0" :precision="0" controls-position="right" | |
| 116 | + style="width: 100%" /> | |
| 110 | 117 | </el-form-item> |
| 111 | 118 | <el-form-item label="非直系亲属天数" prop="indirectRelativeDays"> |
| 112 | - <el-input-number v-model="form.indirectRelativeDays" :min="0" :precision="0" controls-position="right" style="width: 100%" /> | |
| 119 | + <el-input-number v-model="form.indirectRelativeDays" :min="0" :precision="0" controls-position="right" | |
| 120 | + style="width: 100%" /> | |
| 113 | 121 | </el-form-item> |
| 114 | 122 | </template> |
| 115 | 123 | |
| 116 | 124 | <template v-else-if="moduleType === 'lateRule'"> |
| 117 | 125 | <el-form-item label="最小分钟(含)" prop="minMinutes"> |
| 118 | - <el-input-number v-model="form.minMinutes" :min="0" :precision="0" controls-position="right" style="width: 100%" /> | |
| 126 | + <el-input-number v-model="form.minMinutes" :min="0" :precision="0" controls-position="right" | |
| 127 | + style="width: 100%" /> | |
| 119 | 128 | </el-form-item> |
| 120 | 129 | <el-form-item label="最大分钟(不含)" prop="maxMinutes"> |
| 121 | - <el-input-number v-model="form.maxMinutes" :min="0" :precision="0" controls-position="right" style="width: 100%" /> | |
| 130 | + <el-input-number v-model="form.maxMinutes" :min="0" :precision="0" controls-position="right" | |
| 131 | + style="width: 100%" /> | |
| 122 | 132 | </el-form-item> |
| 123 | 133 | <el-form-item label="扣款方式" prop="deductMode"> |
| 124 | 134 | <el-select v-model="form.deductMode" placeholder="请选择" style="width: 100%"> |
| ... | ... | @@ -126,22 +136,27 @@ |
| 126 | 136 | </el-select> |
| 127 | 137 | </el-form-item> |
| 128 | 138 | <el-form-item label="扣款值" prop="deductValue"> |
| 129 | - <el-input-number v-model="form.deductValue" :min="0" :precision="2" controls-position="right" style="width: 100%" /> | |
| 139 | + <el-input-number v-model="form.deductValue" :min="0" :precision="2" controls-position="right" | |
| 140 | + style="width: 100%" /> | |
| 130 | 141 | </el-form-item> |
| 131 | 142 | <el-form-item label="展示说明" prop="expressionText"> |
| 132 | - <el-input v-model.trim="form.expressionText" maxlength="255" show-word-limit placeholder="例如:≥60分钟迟到/早退,扣日薪/2" /> | |
| 143 | + <el-input v-model.trim="form.expressionText" maxlength="255" show-word-limit | |
| 144 | + placeholder="例如:≥60分钟迟到/早退,扣日薪/2" /> | |
| 133 | 145 | </el-form-item> |
| 134 | 146 | </template> |
| 135 | 147 | |
| 136 | 148 | <template v-else-if="moduleType === 'missingCardRule'"> |
| 137 | 149 | <el-form-item label="最小次数(含)" prop="minCount"> |
| 138 | - <el-input-number v-model="form.minCount" :min="1" :precision="0" controls-position="right" style="width: 100%" /> | |
| 150 | + <el-input-number v-model="form.minCount" :min="1" :precision="0" controls-position="right" | |
| 151 | + style="width: 100%" /> | |
| 139 | 152 | </el-form-item> |
| 140 | 153 | <el-form-item label="最大次数(不含)" prop="maxCount"> |
| 141 | - <el-input-number v-model="form.maxCount" :min="1" :precision="0" controls-position="right" style="width: 100%" /> | |
| 154 | + <el-input-number v-model="form.maxCount" :min="1" :precision="0" controls-position="right" | |
| 155 | + style="width: 100%" /> | |
| 142 | 156 | </el-form-item> |
| 143 | 157 | <el-form-item label="每次扣款金额" prop="deductPerTime"> |
| 144 | - <el-input-number v-model="form.deductPerTime" :min="0" :precision="2" controls-position="right" style="width: 100%" /> | |
| 158 | + <el-input-number v-model="form.deductPerTime" :min="0" :precision="2" controls-position="right" | |
| 159 | + style="width: 100%" /> | |
| 145 | 160 | </el-form-item> |
| 146 | 161 | <el-form-item label="展示说明" prop="expressionText"> |
| 147 | 162 | <el-input v-model.trim="form.expressionText" maxlength="255" show-word-limit placeholder="例如:1-2次扣10元/次" /> |
| ... | ... | @@ -150,10 +165,12 @@ |
| 150 | 165 | |
| 151 | 166 | <template v-else-if="moduleType === 'absenteeismRule'"> |
| 152 | 167 | <el-form-item label="最小天数(含)" prop="minDays"> |
| 153 | - <el-input-number v-model="form.minDays" :min="0" :precision="2" :step="0.5" controls-position="right" style="width: 100%" /> | |
| 168 | + <el-input-number v-model="form.minDays" :min="0" :precision="2" :step="0.5" controls-position="right" | |
| 169 | + style="width: 100%" /> | |
| 154 | 170 | </el-form-item> |
| 155 | 171 | <el-form-item label="最大天数(不含)" prop="maxDays"> |
| 156 | - <el-input-number v-model="form.maxDays" :min="0" :precision="2" :step="0.5" controls-position="right" style="width: 100%" /> | |
| 172 | + <el-input-number v-model="form.maxDays" :min="0" :precision="2" :step="0.5" controls-position="right" | |
| 173 | + style="width: 100%" /> | |
| 157 | 174 | </el-form-item> |
| 158 | 175 | <el-form-item label="处理方式" prop="actionType"> |
| 159 | 176 | <el-select v-model="form.actionType" placeholder="请选择" style="width: 100%"> |
| ... | ... | @@ -161,19 +178,14 @@ |
| 161 | 178 | </el-select> |
| 162 | 179 | </el-form-item> |
| 163 | 180 | <el-form-item label="扣款方式" prop="deductMode"> |
| 164 | - <el-select v-model="form.deductMode" :disabled="Number(form.actionType) === 2" placeholder="请选择" style="width: 100%"> | |
| 181 | + <el-select v-model="form.deductMode" :disabled="Number(form.actionType) === 2" placeholder="请选择" | |
| 182 | + style="width: 100%"> | |
| 165 | 183 | <el-option v-for="item in deductModeOptions" :key="item.value" :label="item.label" :value="item.value" /> |
| 166 | 184 | </el-select> |
| 167 | 185 | </el-form-item> |
| 168 | 186 | <el-form-item label="扣款值" prop="deductValue"> |
| 169 | - <el-input-number | |
| 170 | - v-model="form.deductValue" | |
| 171 | - :disabled="Number(form.actionType) === 2" | |
| 172 | - :min="0" | |
| 173 | - :precision="2" | |
| 174 | - controls-position="right" | |
| 175 | - style="width: 100%" | |
| 176 | - /> | |
| 187 | + <el-input-number v-model="form.deductValue" :disabled="Number(form.actionType) === 2" :min="0" :precision="2" | |
| 188 | + controls-position="right" style="width: 100%" /> | |
| 177 | 189 | </el-form-item> |
| 178 | 190 | <el-form-item label="展示说明" prop="actionText"> |
| 179 | 191 | <el-input v-model.trim="form.actionText" maxlength="255" show-word-limit placeholder="例如:超过3天视为自离" /> |
| ... | ... | @@ -182,31 +194,15 @@ |
| 182 | 194 | |
| 183 | 195 | <template v-else-if="moduleType === 'exemptUser'"> |
| 184 | 196 | <el-form-item label="员工" prop="userId"> |
| 185 | - <user-select | |
| 186 | - v-model="form.userId" | |
| 187 | - placeholder="请选择员工" | |
| 188 | - @change="handleUserChange" | |
| 189 | - /> | |
| 197 | + <user-select v-model="form.userId" placeholder="请选择员工" @change="handleUserChange" /> | |
| 190 | 198 | </el-form-item> |
| 191 | 199 | <el-form-item label="开始日期" prop="startDate"> |
| 192 | - <el-date-picker | |
| 193 | - v-model="form.startDate" | |
| 194 | - type="date" | |
| 195 | - value-format="yyyy-MM-dd" | |
| 196 | - format="yyyy-MM-dd" | |
| 197 | - placeholder="开始日期" | |
| 198 | - style="width: 100%" | |
| 199 | - /> | |
| 200 | + <el-date-picker v-model="form.startDate" type="date" value-format="yyyy-MM-dd" format="yyyy-MM-dd" | |
| 201 | + placeholder="开始日期" style="width: 100%" /> | |
| 200 | 202 | </el-form-item> |
| 201 | 203 | <el-form-item label="结束日期" prop="endDate"> |
| 202 | - <el-date-picker | |
| 203 | - v-model="form.endDate" | |
| 204 | - type="date" | |
| 205 | - value-format="yyyy-MM-dd" | |
| 206 | - format="yyyy-MM-dd" | |
| 207 | - placeholder="结束日期" | |
| 208 | - style="width: 100%" | |
| 209 | - /> | |
| 204 | + <el-date-picker v-model="form.endDate" type="date" value-format="yyyy-MM-dd" format="yyyy-MM-dd" | |
| 205 | + placeholder="结束日期" style="width: 100%" /> | |
| 210 | 206 | </el-form-item> |
| 211 | 207 | <el-form-item label="是否启用" prop="isEnabled"> |
| 212 | 208 | <el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" /> |
| ... | ... | @@ -215,20 +211,18 @@ |
| 215 | 211 | |
| 216 | 212 | <template v-else-if="moduleType === 'extraLeave'"> |
| 217 | 213 | <el-form-item label="员工" prop="userId"> |
| 218 | - <user-select | |
| 219 | - v-model="form.userId" | |
| 220 | - placeholder="请选择员工" | |
| 221 | - @change="handleUserChange" | |
| 222 | - /> | |
| 214 | + <user-select v-model="form.userId" placeholder="请选择员工" @change="handleUserChange" /> | |
| 223 | 215 | </el-form-item> |
| 224 | 216 | <el-form-item label="假期名称" prop="leaveName"> |
| 225 | 217 | <el-input v-model.trim="form.leaveName" maxlength="100" show-word-limit placeholder="如:旅游奖励假" /> |
| 226 | 218 | </el-form-item> |
| 227 | 219 | <el-form-item label="归属年份" prop="grantYear"> |
| 228 | - <el-input-number v-model="form.grantYear" :min="2000" :max="2100" controls-position="right" style="width: 100%" /> | |
| 220 | + <el-input-number v-model="form.grantYear" :min="2000" :max="2100" controls-position="right" | |
| 221 | + style="width: 100%" /> | |
| 229 | 222 | </el-form-item> |
| 230 | 223 | <el-form-item label="额外天数" prop="extraDays"> |
| 231 | - <el-input-number v-model="form.extraDays" :min="0.01" :precision="2" :step="0.5" controls-position="right" style="width: 100%" /> | |
| 224 | + <el-input-number v-model="form.extraDays" :min="0.01" :precision="2" :step="0.5" controls-position="right" | |
| 225 | + style="width: 100%" /> | |
| 232 | 226 | </el-form-item> |
| 233 | 227 | <el-form-item label="是否启用" prop="isEnabled"> |
| 234 | 228 | <el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" /> |
| ... | ... | @@ -236,11 +230,13 @@ |
| 236 | 230 | </template> |
| 237 | 231 | |
| 238 | 232 | <el-form-item v-if="showRemark" label="备注" prop="remark"> |
| 239 | - <el-input v-model.trim="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="无" /> | |
| 233 | + <el-input v-model.trim="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit | |
| 234 | + placeholder="无" /> | |
| 240 | 235 | </el-form-item> |
| 241 | 236 | |
| 242 | 237 | <el-form-item label="变更原因" prop="changeReason"> |
| 243 | - <el-input v-model.trim="form.changeReason" type="textarea" :rows="2" maxlength="200" show-word-limit placeholder="可选,用于说明本次变更原因" /> | |
| 238 | + <el-input v-model.trim="form.changeReason" type="textarea" :rows="2" maxlength="200" show-word-limit | |
| 239 | + placeholder="可选,用于说明本次变更原因" /> | |
| 244 | 240 | </el-form-item> |
| 245 | 241 | </el-form> |
| 246 | 242 | |
| ... | ... | @@ -281,6 +277,9 @@ const createDefaultForm = moduleType => { |
| 281 | 277 | workEndTime: '19:00', |
| 282 | 278 | monthlyRestDays: 4, |
| 283 | 279 | halfDaySplitRestDays: 0, |
| 280 | + restUnlockCycle: 0, | |
| 281 | + lateToleranceMinutes: null, | |
| 282 | + earlyLeaveToleranceMinutes: null, | |
| 284 | 283 | isEnabled: 1, |
| 285 | 284 | remark: '', |
| 286 | 285 | changeReason: '' |
| ... | ... | @@ -515,6 +514,13 @@ export default { |
| 515 | 514 | setSubmitting(flag) { |
| 516 | 515 | this.submitLoading = !!flag |
| 517 | 516 | }, |
| 517 | + handleRestUnlockToggle(enabled) { | |
| 518 | + this.form.restUnlockCycle = enabled ? 7 : 0 | |
| 519 | + if (!enabled) { | |
| 520 | + this.form.lateToleranceMinutes = null | |
| 521 | + this.form.earlyLeaveToleranceMinutes = null | |
| 522 | + } | |
| 523 | + }, | |
| 518 | 524 | handleUserChange(ids, users) { |
| 519 | 525 | this.form.userId = ids |
| 520 | 526 | this.form.userName = users && users[0] ? users[0].fullName : '' | ... | ... |
antis-ncc-admin/src/views/attendance-setting/components/attendance-group-table.vue
| ... | ... | @@ -12,34 +12,76 @@ |
| 12 | 12 | </el-table-column> |
| 13 | 13 | <el-table-column label="上班时间" min-width="130" align="left"> |
| 14 | 14 | <template slot-scope="scope"> |
| 15 | - <el-time-picker | |
| 16 | - v-model="scope.row.workStartTime" | |
| 17 | - value-format="HH:mm" | |
| 18 | - format="HH:mm" | |
| 19 | - placeholder="09:00" | |
| 20 | - style="width: 100%" | |
| 21 | - /> | |
| 15 | + <el-time-picker v-model="scope.row.workStartTime" value-format="HH:mm" format="HH:mm" placeholder="09:00" | |
| 16 | + style="width: 100%" /> | |
| 22 | 17 | </template> |
| 23 | 18 | </el-table-column> |
| 24 | 19 | <el-table-column label="下班时间" min-width="130" align="left"> |
| 25 | 20 | <template slot-scope="scope"> |
| 26 | - <el-time-picker | |
| 27 | - v-model="scope.row.workEndTime" | |
| 28 | - value-format="HH:mm" | |
| 29 | - format="HH:mm" | |
| 30 | - placeholder="19:00" | |
| 31 | - style="width: 100%" | |
| 32 | - /> | |
| 21 | + <el-time-picker v-model="scope.row.workEndTime" value-format="HH:mm" format="HH:mm" placeholder="19:00" | |
| 22 | + style="width: 100%" /> | |
| 33 | 23 | </template> |
| 34 | 24 | </el-table-column> |
| 35 | 25 | <el-table-column label="月应休天数" min-width="120" align="left"> |
| 36 | 26 | <template slot-scope="scope"> |
| 37 | - <el-input-number v-model="scope.row.monthlyRestDays" :min="0" :precision="0" controls-position="right" style="width: 100%" /> | |
| 27 | + <el-input-number v-model="scope.row.monthlyRestDays" :min="0" :precision="0" controls-position="right" | |
| 28 | + style="width: 100%" /> | |
| 38 | 29 | </template> |
| 39 | 30 | </el-table-column> |
| 40 | 31 | <el-table-column label="可拆分半天休假天数" min-width="160" align="left"> |
| 41 | 32 | <template slot-scope="scope"> |
| 42 | - <el-input-number v-model="scope.row.halfDaySplitRestDays" :min="0" :precision="0" :step="1" controls-position="right" style="width: 100%" /> | |
| 33 | + <el-input-number v-model="scope.row.halfDaySplitRestDays" :min="0" :precision="0" :step="1" | |
| 34 | + controls-position="right" style="width: 100%" /> | |
| 35 | + </template> | |
| 36 | + </el-table-column> | |
| 37 | + <el-table-column label="应休解锁规则" min-width="200" align="left"> | |
| 38 | + <template slot-scope="scope"> | |
| 39 | + <div class="rule-cell"> | |
| 40 | + <el-switch :value="!!(scope.row.restUnlockCycle)" @change="(v) => toggleRestUnlock(scope.row, v)" /> | |
| 41 | + <template v-if="scope.row.restUnlockCycle"> | |
| 42 | + <span class="rule-label">每上满</span> | |
| 43 | + <el-input-number v-model="scope.row.restUnlockCycle" :min="1" :max="365" :precision="0" | |
| 44 | + controls-position="right" style="width: 100px" /> | |
| 45 | + <span class="rule-label">天解锁1天</span> | |
| 46 | + </template> | |
| 47 | + <span v-else class="rule-off">不启用</span> | |
| 48 | + </div> | |
| 49 | + </template> | |
| 50 | + </el-table-column> | |
| 51 | + <el-table-column v-if="hasAnyUnlockEnabled" label="迟到容忍" min-width="190" align="left"> | |
| 52 | + <template slot-scope="scope"> | |
| 53 | + <template v-if="scope.row.restUnlockCycle > 0"> | |
| 54 | + <div class="rule-cell"> | |
| 55 | + <el-switch :value="scope.row.lateToleranceMinutes != null" | |
| 56 | + @change="(v) => toggleLateTolerance(scope.row, v)" /> | |
| 57 | + <template v-if="scope.row.lateToleranceMinutes != null"> | |
| 58 | + <span class="rule-label">不超过</span> | |
| 59 | + <el-input-number v-model="scope.row.lateToleranceMinutes" :min="0" :max="999" :precision="0" | |
| 60 | + controls-position="right" style="width: 90px" /> | |
| 61 | + <span class="rule-label">分钟</span> | |
| 62 | + </template> | |
| 63 | + <span v-else class="rule-off">不计</span> | |
| 64 | + </div> | |
| 65 | + </template> | |
| 66 | + <span v-else class="rule-off">—</span> | |
| 67 | + </template> | |
| 68 | + </el-table-column> | |
| 69 | + <el-table-column v-if="hasAnyUnlockEnabled" label="早退容忍" min-width="190" align="left"> | |
| 70 | + <template slot-scope="scope"> | |
| 71 | + <template v-if="scope.row.restUnlockCycle > 0"> | |
| 72 | + <div class="rule-cell"> | |
| 73 | + <el-switch :value="scope.row.earlyLeaveToleranceMinutes != null" | |
| 74 | + @change="(v) => toggleEarlyLeaveTolerance(scope.row, v)" /> | |
| 75 | + <template v-if="scope.row.earlyLeaveToleranceMinutes != null"> | |
| 76 | + <span class="rule-label">不超过</span> | |
| 77 | + <el-input-number v-model="scope.row.earlyLeaveToleranceMinutes" :min="0" :max="999" :precision="0" | |
| 78 | + controls-position="right" style="width: 90px" /> | |
| 79 | + <span class="rule-label">分钟</span> | |
| 80 | + </template> | |
| 81 | + <span v-else class="rule-off">不计</span> | |
| 82 | + </div> | |
| 83 | + </template> | |
| 84 | + <span v-else class="rule-off">—</span> | |
| 43 | 85 | </template> |
| 44 | 86 | </el-table-column> |
| 45 | 87 | <el-table-column label="是否启用" width="100" align="left"> |
| ... | ... | @@ -55,12 +97,8 @@ |
| 55 | 97 | <el-table-column label="操作" width="180" align="left"> |
| 56 | 98 | <template slot-scope="scope"> |
| 57 | 99 | <el-button type="text" @click="showUsers(scope.row)">成员详情</el-button> |
| 58 | - <el-popconfirm | |
| 59 | - title="确认删除当前配置吗?" | |
| 60 | - confirm-button-text="确认删除" | |
| 61 | - cancel-button-text="取消" | |
| 62 | - @confirm="removeRow(scope.$index)" | |
| 63 | - > | |
| 100 | + <el-popconfirm title="确认删除当前配置吗?" confirm-button-text="确认删除" cancel-button-text="取消" | |
| 101 | + @confirm="removeRow(scope.$index)"> | |
| 64 | 102 | <el-button slot="reference" type="text" class="danger-text">删除</el-button> |
| 65 | 103 | </el-popconfirm> |
| 66 | 104 | </template> |
| ... | ... | @@ -86,6 +124,11 @@ export default { |
| 86 | 124 | default: () => [] |
| 87 | 125 | } |
| 88 | 126 | }, |
| 127 | + computed: { | |
| 128 | + hasAnyUnlockEnabled() { | |
| 129 | + return this.value.some(row => row.restUnlockCycle > 0) | |
| 130 | + } | |
| 131 | + }, | |
| 89 | 132 | methods: { |
| 90 | 133 | addRow() { |
| 91 | 134 | this.$emit('input', [ |
| ... | ... | @@ -96,6 +139,9 @@ export default { |
| 96 | 139 | workEndTime: '19:00', |
| 97 | 140 | monthlyRestDays: 4, |
| 98 | 141 | halfDaySplitRestDays: 0, |
| 142 | + restUnlockCycle: 0, | |
| 143 | + lateToleranceMinutes: null, | |
| 144 | + earlyLeaveToleranceMinutes: null, | |
| 99 | 145 | isEnabled: 1, |
| 100 | 146 | remark: '' |
| 101 | 147 | } |
| ... | ... | @@ -106,6 +152,19 @@ export default { |
| 106 | 152 | list.splice(index, 1) |
| 107 | 153 | this.$emit('input', list) |
| 108 | 154 | }, |
| 155 | + toggleRestUnlock(row, enabled) { | |
| 156 | + this.$set(row, 'restUnlockCycle', enabled ? 7 : 0) | |
| 157 | + if (!enabled) { | |
| 158 | + this.$set(row, 'lateToleranceMinutes', null) | |
| 159 | + this.$set(row, 'earlyLeaveToleranceMinutes', null) | |
| 160 | + } | |
| 161 | + }, | |
| 162 | + toggleLateTolerance(row, enabled) { | |
| 163 | + this.$set(row, 'lateToleranceMinutes', enabled ? 15 : null) | |
| 164 | + }, | |
| 165 | + toggleEarlyLeaveTolerance(row, enabled) { | |
| 166 | + this.$set(row, 'earlyLeaveToleranceMinutes', enabled ? 15 : null) | |
| 167 | + }, | |
| 109 | 168 | showUsers(row) { |
| 110 | 169 | if (!row.id) { |
| 111 | 170 | this.$message.warning('请先保存分组设置后再查看成员详情') |
| ... | ... | @@ -139,6 +198,24 @@ export default { |
| 139 | 198 | color: #f56c6c; |
| 140 | 199 | } |
| 141 | 200 | |
| 201 | +.rule-cell { | |
| 202 | + display: flex; | |
| 203 | + align-items: center; | |
| 204 | + gap: 6px; | |
| 205 | + flex-wrap: nowrap; | |
| 206 | +} | |
| 207 | + | |
| 208 | +.rule-label { | |
| 209 | + font-size: 12px; | |
| 210 | + color: #606266; | |
| 211 | + white-space: nowrap; | |
| 212 | +} | |
| 213 | + | |
| 214 | +.rule-off { | |
| 215 | + font-size: 12px; | |
| 216 | + color: #909399; | |
| 217 | +} | |
| 218 | + | |
| 142 | 219 | ::v-deep .el-card__header { |
| 143 | 220 | padding: 12px 16px; |
| 144 | 221 | } | ... | ... |
antis-ncc-admin/src/views/attendance-setting/index.vue
| ... | ... | @@ -62,7 +62,8 @@ |
| 62 | 62 | <div class="page-main__title"> |
| 63 | 63 | <i class="el-icon-s-operation page-main__title-icon" aria-hidden="true" /> |
| 64 | 64 | <span class="page-main__title-text">当前模块</span> |
| 65 | - <el-tag type="primary" effect="plain" size="small" class="page-main__tab-tag">{{ activeTabLabel }}</el-tag> | |
| 65 | + <el-tag type="primary" effect="plain" size="small" class="page-main__tab-tag">{{ activeTabLabel | |
| 66 | + }}</el-tag> | |
| 66 | 67 | </div> |
| 67 | 68 | <div class="page-main__actions"> |
| 68 | 69 | <el-button size="small" icon="el-icon-refresh-right" @click="initData">重新加载</el-button> |
| ... | ... | @@ -119,22 +120,21 @@ |
| 119 | 120 | </div> |
| 120 | 121 | |
| 121 | 122 | <div class="section-filter"> |
| 122 | - <el-select v-model="moduleState.holiday.query.year" size="small" class="section-filter__year" @change="handleModuleSearch('holiday')"> | |
| 123 | + <el-select v-model="moduleState.holiday.query.year" size="small" class="section-filter__year" | |
| 124 | + @change="handleModuleSearch('holiday')"> | |
| 123 | 125 | <el-option v-for="item in yearOptions" :key="item" :label="`${item}年`" :value="item" /> |
| 124 | 126 | </el-select> |
| 125 | - <el-input | |
| 126 | - v-model.trim="moduleState.holiday.query.keyword" | |
| 127 | - size="small" | |
| 128 | - class="section-filter__keyword" | |
| 129 | - clearable | |
| 130 | - placeholder="节假日名称 / 备注" | |
| 131 | - @keyup.enter.native="handleModuleSearch('holiday')" | |
| 132 | - /> | |
| 133 | - <el-button type="primary" size="small" icon="el-icon-search" @click="handleModuleSearch('holiday')">查询</el-button> | |
| 134 | - <el-button size="small" icon="el-icon-refresh-right" @click="handleModuleReset('holiday')">重置</el-button> | |
| 127 | + <el-input v-model.trim="moduleState.holiday.query.keyword" size="small" | |
| 128 | + class="section-filter__keyword" clearable placeholder="节假日名称 / 备注" | |
| 129 | + @keyup.enter.native="handleModuleSearch('holiday')" /> | |
| 130 | + <el-button type="primary" size="small" icon="el-icon-search" | |
| 131 | + @click="handleModuleSearch('holiday')">查询</el-button> | |
| 132 | + <el-button size="small" icon="el-icon-refresh-right" | |
| 133 | + @click="handleModuleReset('holiday')">重置</el-button> | |
| 135 | 134 | </div> |
| 136 | 135 | |
| 137 | - <NCC-table v-loading="moduleState.holiday.loading" :data="moduleState.holiday.list" border size="mini" height="460"> | |
| 136 | + <NCC-table v-loading="moduleState.holiday.loading" :data="moduleState.holiday.list" border | |
| 137 | + size="mini" height="460"> | |
| 138 | 138 | <el-table-column prop="holidayDate" label="公休日期" width="140" align="left"> |
| 139 | 139 | <template slot-scope="scope"> |
| 140 | 140 | <span class="cell-with-icon cell-with-icon--primary" :title="scope.row.holidayDate"> |
| ... | ... | @@ -159,21 +159,20 @@ |
| 159 | 159 | <el-table-column label="操作" width="210" align="left" fixed="right"> |
| 160 | 160 | <template slot-scope="scope"> |
| 161 | 161 | <el-button type="text" @click="openConfigDialog('holiday', scope.row)">编辑</el-button> |
| 162 | - <el-button type="text" @click="openHistory('holiday', scope.row, buildHolidayTitle(scope.row))">历史</el-button> | |
| 163 | - <el-button type="text" class="danger-text" @click="handleDelete('holiday', scope.row)">删除</el-button> | |
| 162 | + <el-button type="text" | |
| 163 | + @click="openHistory('holiday', scope.row, buildHolidayTitle(scope.row))">历史</el-button> | |
| 164 | + <el-button type="text" class="danger-text" | |
| 165 | + @click="handleDelete('holiday', scope.row)">删除</el-button> | |
| 164 | 166 | </template> |
| 165 | 167 | </el-table-column> |
| 166 | 168 | </NCC-table> |
| 167 | 169 | <div v-if="!moduleState.holiday.loading && !moduleState.holiday.list.length" class="section-empty"> |
| 168 | 170 | <el-empty :image-size="60" description="当前筛选条件下暂无公休时间配置" /> |
| 169 | 171 | </div> |
| 170 | - <pagination | |
| 171 | - :hidden="!moduleState.holiday.pagination.total" | |
| 172 | - :total="moduleState.holiday.pagination.total" | |
| 173 | - :page.sync="moduleState.holiday.query.currentPage" | |
| 172 | + <pagination :hidden="!moduleState.holiday.pagination.total" | |
| 173 | + :total="moduleState.holiday.pagination.total" :page.sync="moduleState.holiday.query.currentPage" | |
| 174 | 174 | :limit.sync="moduleState.holiday.query.pageSize" |
| 175 | - @pagination="() => loadModule('holiday', true)" | |
| 176 | - /> | |
| 175 | + @pagination="() => loadModule('holiday', true)" /> | |
| 177 | 176 | </el-card> |
| 178 | 177 | </div> |
| 179 | 178 | </el-tab-pane> |
| ... | ... | @@ -192,19 +191,17 @@ |
| 192 | 191 | </div> |
| 193 | 192 | |
| 194 | 193 | <div class="section-filter"> |
| 195 | - <el-input | |
| 196 | - v-model.trim="moduleState.group.query.keyword" | |
| 197 | - size="small" | |
| 198 | - class="section-filter__keyword" | |
| 199 | - clearable | |
| 200 | - placeholder="分组名称 / 备注" | |
| 201 | - @keyup.enter.native="handleModuleSearch('group')" | |
| 202 | - /> | |
| 203 | - <el-button type="primary" size="small" icon="el-icon-search" @click="handleModuleSearch('group')">查询</el-button> | |
| 204 | - <el-button size="small" icon="el-icon-refresh-right" @click="handleModuleReset('group')">重置</el-button> | |
| 194 | + <el-input v-model.trim="moduleState.group.query.keyword" size="small" | |
| 195 | + class="section-filter__keyword" clearable placeholder="分组名称 / 备注" | |
| 196 | + @keyup.enter.native="handleModuleSearch('group')" /> | |
| 197 | + <el-button type="primary" size="small" icon="el-icon-search" | |
| 198 | + @click="handleModuleSearch('group')">查询</el-button> | |
| 199 | + <el-button size="small" icon="el-icon-refresh-right" | |
| 200 | + @click="handleModuleReset('group')">重置</el-button> | |
| 205 | 201 | </div> |
| 206 | 202 | |
| 207 | - <NCC-table v-loading="moduleState.group.loading" :data="moduleState.group.list" border size="mini" height="460"> | |
| 203 | + <NCC-table v-loading="moduleState.group.loading" :data="moduleState.group.list" border size="mini" | |
| 204 | + height="460"> | |
| 208 | 205 | <el-table-column prop="groupName" label="分组名称" min-width="150" align="left"> |
| 209 | 206 | <template slot-scope="scope"> |
| 210 | 207 | <span class="cell-with-icon cell-with-icon--primary" :title="scope.row.groupName || '无'"> |
| ... | ... | @@ -217,6 +214,35 @@ |
| 217 | 214 | <el-table-column prop="workEndTime" label="下班时间" width="110" align="left" /> |
| 218 | 215 | <el-table-column prop="monthlyRestDays" label="月应休天数" width="110" align="left" /> |
| 219 | 216 | <el-table-column prop="halfDaySplitRestDays" label="可拆分半天休假天数" width="150" align="left" /> |
| 217 | + <el-table-column label="应休解锁规则" min-width="160" align="left"> | |
| 218 | + <template slot-scope="scope"> | |
| 219 | + <span v-if="scope.row.restUnlockCycle > 0" class="cell-with-icon cell-with-icon--primary"> | |
| 220 | + <i class="el-icon-unlock" /> | |
| 221 | + <span>每上满 {{ scope.row.restUnlockCycle }} 天解锁 1 天</span> | |
| 222 | + </span> | |
| 223 | + <span v-else style="color: #909399;">不启用</span> | |
| 224 | + </template> | |
| 225 | + </el-table-column> | |
| 226 | + <el-table-column label="迟到容忍" width="140" align="left"> | |
| 227 | + <template slot-scope="scope"> | |
| 228 | + <template v-if="scope.row.restUnlockCycle > 0"> | |
| 229 | + <span v-if="scope.row.lateToleranceMinutes != null">≤ {{ scope.row.lateToleranceMinutes }} | |
| 230 | + 分钟</span> | |
| 231 | + <span v-else style="color: #909399;">不计</span> | |
| 232 | + </template> | |
| 233 | + <span v-else style="color: #c0c4cc;">—</span> | |
| 234 | + </template> | |
| 235 | + </el-table-column> | |
| 236 | + <el-table-column label="早退容忍" width="140" align="left"> | |
| 237 | + <template slot-scope="scope"> | |
| 238 | + <template v-if="scope.row.restUnlockCycle > 0"> | |
| 239 | + <span v-if="scope.row.earlyLeaveToleranceMinutes != null">≤ {{ | |
| 240 | + scope.row.earlyLeaveToleranceMinutes }} 分钟</span> | |
| 241 | + <span v-else style="color: #909399;">不计</span> | |
| 242 | + </template> | |
| 243 | + <span v-else style="color: #c0c4cc;">—</span> | |
| 244 | + </template> | |
| 245 | + </el-table-column> | |
| 220 | 246 | <el-table-column label="状态" width="90" align="left"> |
| 221 | 247 | <template slot-scope="scope"> |
| 222 | 248 | <el-tag :type="Number(scope.row.isEnabled) === 1 ? 'success' : 'info'" size="mini"> |
| ... | ... | @@ -233,21 +259,19 @@ |
| 233 | 259 | <template slot-scope="scope"> |
| 234 | 260 | <el-button type="text" @click="showGroupUsers(scope.row)">成员</el-button> |
| 235 | 261 | <el-button type="text" @click="openConfigDialog('group', scope.row)">编辑</el-button> |
| 236 | - <el-button type="text" @click="openHistory('group', scope.row, scope.row.groupName)">历史</el-button> | |
| 237 | - <el-button type="text" class="danger-text" @click="handleDelete('group', scope.row)">删除</el-button> | |
| 262 | + <el-button type="text" | |
| 263 | + @click="openHistory('group', scope.row, scope.row.groupName)">历史</el-button> | |
| 264 | + <el-button type="text" class="danger-text" | |
| 265 | + @click="handleDelete('group', scope.row)">删除</el-button> | |
| 238 | 266 | </template> |
| 239 | 267 | </el-table-column> |
| 240 | 268 | </NCC-table> |
| 241 | 269 | <div v-if="!moduleState.group.loading && !moduleState.group.list.length" class="section-empty"> |
| 242 | 270 | <el-empty :image-size="60" description="暂无考勤分组配置" /> |
| 243 | 271 | </div> |
| 244 | - <pagination | |
| 245 | - :hidden="!moduleState.group.pagination.total" | |
| 246 | - :total="moduleState.group.pagination.total" | |
| 247 | - :page.sync="moduleState.group.query.currentPage" | |
| 248 | - :limit.sync="moduleState.group.query.pageSize" | |
| 249 | - @pagination="() => loadModule('group', true)" | |
| 250 | - /> | |
| 272 | + <pagination :hidden="!moduleState.group.pagination.total" | |
| 273 | + :total="moduleState.group.pagination.total" :page.sync="moduleState.group.query.currentPage" | |
| 274 | + :limit.sync="moduleState.group.query.pageSize" @pagination="() => loadModule('group', true)" /> | |
| 251 | 275 | </el-card> |
| 252 | 276 | </div> |
| 253 | 277 | </el-tab-pane> |
| ... | ... | @@ -261,10 +285,12 @@ |
| 261 | 285 | <div class="section-card__desc">按司龄区间单独维护婚假天数。</div> |
| 262 | 286 | </div> |
| 263 | 287 | <div class="section-card__actions"> |
| 264 | - <el-button type="primary" size="mini" @click="openConfigDialog('marriageLeaveRule')">新增规则</el-button> | |
| 288 | + <el-button type="primary" size="mini" | |
| 289 | + @click="openConfigDialog('marriageLeaveRule')">新增规则</el-button> | |
| 265 | 290 | </div> |
| 266 | 291 | </div> |
| 267 | - <NCC-table v-loading="moduleState.marriageLeaveRule.loading" :data="moduleState.marriageLeaveRule.list" border size="mini" height="320"> | |
| 292 | + <NCC-table v-loading="moduleState.marriageLeaveRule.loading" | |
| 293 | + :data="moduleState.marriageLeaveRule.list" border size="mini" height="320"> | |
| 268 | 294 | <el-table-column prop="minYears" label="最小司龄(含)" width="120" align="left"> |
| 269 | 295 | <template slot-scope="scope"> |
| 270 | 296 | <span class="cell-with-icon cell-with-icon--primary" :title="String(scope.row.minYears)"> |
| ... | ... | @@ -286,9 +312,12 @@ |
| 286 | 312 | </el-table-column> |
| 287 | 313 | <el-table-column label="操作" width="210" align="left" fixed="right"> |
| 288 | 314 | <template slot-scope="scope"> |
| 289 | - <el-button type="text" @click="openConfigDialog('marriageLeaveRule', scope.row)">编辑</el-button> | |
| 290 | - <el-button type="text" @click="openHistory('marriageLeaveRule', scope.row, buildYearRangeTitle(scope.row, '婚假规则'))">历史</el-button> | |
| 291 | - <el-button type="text" class="danger-text" @click="handleDelete('marriageLeaveRule', scope.row)">删除</el-button> | |
| 315 | + <el-button type="text" | |
| 316 | + @click="openConfigDialog('marriageLeaveRule', scope.row)">编辑</el-button> | |
| 317 | + <el-button type="text" | |
| 318 | + @click="openHistory('marriageLeaveRule', scope.row, buildYearRangeTitle(scope.row, '婚假规则'))">历史</el-button> | |
| 319 | + <el-button type="text" class="danger-text" | |
| 320 | + @click="handleDelete('marriageLeaveRule', scope.row)">删除</el-button> | |
| 292 | 321 | </template> |
| 293 | 322 | </el-table-column> |
| 294 | 323 | </NCC-table> |
| ... | ... | @@ -301,10 +330,12 @@ |
| 301 | 330 | <div class="section-card__desc">支持直系/非直系亲属丧假天数配置。</div> |
| 302 | 331 | </div> |
| 303 | 332 | <div class="section-card__actions"> |
| 304 | - <el-button type="primary" size="mini" @click="openConfigDialog('funeralLeaveRule')">新增规则</el-button> | |
| 333 | + <el-button type="primary" size="mini" | |
| 334 | + @click="openConfigDialog('funeralLeaveRule')">新增规则</el-button> | |
| 305 | 335 | </div> |
| 306 | 336 | </div> |
| 307 | - <NCC-table v-loading="moduleState.funeralLeaveRule.loading" :data="moduleState.funeralLeaveRule.list" border size="mini" height="320"> | |
| 337 | + <NCC-table v-loading="moduleState.funeralLeaveRule.loading" | |
| 338 | + :data="moduleState.funeralLeaveRule.list" border size="mini" height="320"> | |
| 308 | 339 | <el-table-column prop="minYears" label="最小司龄(含)" width="120" align="left"> |
| 309 | 340 | <template slot-scope="scope"> |
| 310 | 341 | <span class="cell-with-icon cell-with-icon--primary" :title="String(scope.row.minYears)"> |
| ... | ... | @@ -328,8 +359,10 @@ |
| 328 | 359 | <el-table-column label="操作" width="210" align="left" fixed="right"> |
| 329 | 360 | <template slot-scope="scope"> |
| 330 | 361 | <el-button type="text" @click="openConfigDialog('funeralLeaveRule', scope.row)">编辑</el-button> |
| 331 | - <el-button type="text" @click="openHistory('funeralLeaveRule', scope.row, buildYearRangeTitle(scope.row, '丧假规则'))">历史</el-button> | |
| 332 | - <el-button type="text" class="danger-text" @click="handleDelete('funeralLeaveRule', scope.row)">删除</el-button> | |
| 362 | + <el-button type="text" | |
| 363 | + @click="openHistory('funeralLeaveRule', scope.row, buildYearRangeTitle(scope.row, '丧假规则'))">历史</el-button> | |
| 364 | + <el-button type="text" class="danger-text" | |
| 365 | + @click="handleDelete('funeralLeaveRule', scope.row)">删除</el-button> | |
| 333 | 366 | </template> |
| 334 | 367 | </el-table-column> |
| 335 | 368 | </NCC-table> |
| ... | ... | @@ -342,10 +375,12 @@ |
| 342 | 375 | <div class="section-card__desc">按司龄区间维护年假额度,支持无上限区间。</div> |
| 343 | 376 | </div> |
| 344 | 377 | <div class="section-card__actions"> |
| 345 | - <el-button type="primary" size="mini" @click="openConfigDialog('annualLeaveRule')">新增规则</el-button> | |
| 378 | + <el-button type="primary" size="mini" | |
| 379 | + @click="openConfigDialog('annualLeaveRule')">新增规则</el-button> | |
| 346 | 380 | </div> |
| 347 | 381 | </div> |
| 348 | - <NCC-table v-loading="moduleState.annualLeaveRule.loading" :data="moduleState.annualLeaveRule.list" border size="mini" height="320"> | |
| 382 | + <NCC-table v-loading="moduleState.annualLeaveRule.loading" :data="moduleState.annualLeaveRule.list" | |
| 383 | + border size="mini" height="320"> | |
| 349 | 384 | <el-table-column prop="minYears" label="最小司龄(含)" width="120" align="left"> |
| 350 | 385 | <template slot-scope="scope"> |
| 351 | 386 | <span class="cell-with-icon cell-with-icon--success" :title="String(scope.row.minYears)"> |
| ... | ... | @@ -368,8 +403,10 @@ |
| 368 | 403 | <el-table-column label="操作" width="210" align="left" fixed="right"> |
| 369 | 404 | <template slot-scope="scope"> |
| 370 | 405 | <el-button type="text" @click="openConfigDialog('annualLeaveRule', scope.row)">编辑</el-button> |
| 371 | - <el-button type="text" @click="openHistory('annualLeaveRule', scope.row, buildYearRangeTitle(scope.row, '年假规则'))">历史</el-button> | |
| 372 | - <el-button type="text" class="danger-text" @click="handleDelete('annualLeaveRule', scope.row)">删除</el-button> | |
| 406 | + <el-button type="text" | |
| 407 | + @click="openHistory('annualLeaveRule', scope.row, buildYearRangeTitle(scope.row, '年假规则'))">历史</el-button> | |
| 408 | + <el-button type="text" class="danger-text" | |
| 409 | + @click="handleDelete('annualLeaveRule', scope.row)">删除</el-button> | |
| 373 | 410 | </template> |
| 374 | 411 | </el-table-column> |
| 375 | 412 | </NCC-table> |
| ... | ... | @@ -382,10 +419,12 @@ |
| 382 | 419 | <div class="section-card__desc">按司龄区间维护产假额度,支持无上限区间。</div> |
| 383 | 420 | </div> |
| 384 | 421 | <div class="section-card__actions"> |
| 385 | - <el-button type="primary" size="mini" @click="openConfigDialog('maternityLeaveRule')">新增规则</el-button> | |
| 422 | + <el-button type="primary" size="mini" | |
| 423 | + @click="openConfigDialog('maternityLeaveRule')">新增规则</el-button> | |
| 386 | 424 | </div> |
| 387 | 425 | </div> |
| 388 | - <NCC-table v-loading="moduleState.maternityLeaveRule.loading" :data="moduleState.maternityLeaveRule.list" border size="mini" height="320"> | |
| 426 | + <NCC-table v-loading="moduleState.maternityLeaveRule.loading" | |
| 427 | + :data="moduleState.maternityLeaveRule.list" border size="mini" height="320"> | |
| 389 | 428 | <el-table-column prop="minYears" label="最小司龄(含)" width="120" align="left"> |
| 390 | 429 | <template slot-scope="scope"> |
| 391 | 430 | <span class="cell-with-icon cell-with-icon--success" :title="String(scope.row.minYears)"> |
| ... | ... | @@ -407,9 +446,12 @@ |
| 407 | 446 | </el-table-column> |
| 408 | 447 | <el-table-column label="操作" width="210" align="left" fixed="right"> |
| 409 | 448 | <template slot-scope="scope"> |
| 410 | - <el-button type="text" @click="openConfigDialog('maternityLeaveRule', scope.row)">编辑</el-button> | |
| 411 | - <el-button type="text" @click="openHistory('maternityLeaveRule', scope.row, buildYearRangeTitle(scope.row, '产假规则'))">历史</el-button> | |
| 412 | - <el-button type="text" class="danger-text" @click="handleDelete('maternityLeaveRule', scope.row)">删除</el-button> | |
| 449 | + <el-button type="text" | |
| 450 | + @click="openConfigDialog('maternityLeaveRule', scope.row)">编辑</el-button> | |
| 451 | + <el-button type="text" | |
| 452 | + @click="openHistory('maternityLeaveRule', scope.row, buildYearRangeTitle(scope.row, '产假规则'))">历史</el-button> | |
| 453 | + <el-button type="text" class="danger-text" | |
| 454 | + @click="handleDelete('maternityLeaveRule', scope.row)">删除</el-button> | |
| 413 | 455 | </template> |
| 414 | 456 | </el-table-column> |
| 415 | 457 | </NCC-table> |
| ... | ... | @@ -429,7 +471,8 @@ |
| 429 | 471 | <el-button type="primary" size="mini" @click="openConfigDialog('lateRule')">新增规则</el-button> |
| 430 | 472 | </div> |
| 431 | 473 | </div> |
| 432 | - <NCC-table v-loading="moduleState.lateRule.loading" :data="moduleState.lateRule.list" border size="mini" height="320"> | |
| 474 | + <NCC-table v-loading="moduleState.lateRule.loading" :data="moduleState.lateRule.list" border | |
| 475 | + size="mini" height="320"> | |
| 433 | 476 | <el-table-column prop="minMinutes" label="最小分钟(含)" width="120" align="left"> |
| 434 | 477 | <template slot-scope="scope"> |
| 435 | 478 | <span class="cell-with-icon cell-with-icon--primary" :title="String(scope.row.minMinutes)"> |
| ... | ... | @@ -457,8 +500,10 @@ |
| 457 | 500 | <el-table-column label="操作" width="210" align="left" fixed="right"> |
| 458 | 501 | <template slot-scope="scope"> |
| 459 | 502 | <el-button type="text" @click="openConfigDialog('lateRule', scope.row)">编辑</el-button> |
| 460 | - <el-button type="text" @click="openHistory('lateRule', scope.row, buildMinuteRangeTitle(scope.row, '迟到/早退规则'))">历史</el-button> | |
| 461 | - <el-button type="text" class="danger-text" @click="handleDelete('lateRule', scope.row)">删除</el-button> | |
| 503 | + <el-button type="text" | |
| 504 | + @click="openHistory('lateRule', scope.row, buildMinuteRangeTitle(scope.row, '迟到/早退规则'))">历史</el-button> | |
| 505 | + <el-button type="text" class="danger-text" | |
| 506 | + @click="handleDelete('lateRule', scope.row)">删除</el-button> | |
| 462 | 507 | </template> |
| 463 | 508 | </el-table-column> |
| 464 | 509 | </NCC-table> |
| ... | ... | @@ -471,10 +516,12 @@ |
| 471 | 516 | <div class="section-card__desc">支持 1-2 次、3-6 次、7 次及以上等阶梯规则。</div> |
| 472 | 517 | </div> |
| 473 | 518 | <div class="section-card__actions"> |
| 474 | - <el-button type="primary" size="mini" @click="openConfigDialog('missingCardRule')">新增规则</el-button> | |
| 519 | + <el-button type="primary" size="mini" | |
| 520 | + @click="openConfigDialog('missingCardRule')">新增规则</el-button> | |
| 475 | 521 | </div> |
| 476 | 522 | </div> |
| 477 | - <NCC-table v-loading="moduleState.missingCardRule.loading" :data="moduleState.missingCardRule.list" border size="mini" height="320"> | |
| 523 | + <NCC-table v-loading="moduleState.missingCardRule.loading" :data="moduleState.missingCardRule.list" | |
| 524 | + border size="mini" height="320"> | |
| 478 | 525 | <el-table-column prop="minCount" label="最小次数(含)" width="120" align="left"> |
| 479 | 526 | <template slot-scope="scope"> |
| 480 | 527 | <span class="cell-with-icon cell-with-icon--primary" :title="String(scope.row.minCount)"> |
| ... | ... | @@ -497,8 +544,10 @@ |
| 497 | 544 | <el-table-column label="操作" width="210" align="left" fixed="right"> |
| 498 | 545 | <template slot-scope="scope"> |
| 499 | 546 | <el-button type="text" @click="openConfigDialog('missingCardRule', scope.row)">编辑</el-button> |
| 500 | - <el-button type="text" @click="openHistory('missingCardRule', scope.row, buildCountRangeTitle(scope.row, '缺卡规则'))">历史</el-button> | |
| 501 | - <el-button type="text" class="danger-text" @click="handleDelete('missingCardRule', scope.row)">删除</el-button> | |
| 547 | + <el-button type="text" | |
| 548 | + @click="openHistory('missingCardRule', scope.row, buildCountRangeTitle(scope.row, '缺卡规则'))">历史</el-button> | |
| 549 | + <el-button type="text" class="danger-text" | |
| 550 | + @click="handleDelete('missingCardRule', scope.row)">删除</el-button> | |
| 502 | 551 | </template> |
| 503 | 552 | </el-table-column> |
| 504 | 553 | </NCC-table> |
| ... | ... | @@ -511,10 +560,12 @@ |
| 511 | 560 | <div class="section-card__desc">支持扣款与视为自离两种处理方式。</div> |
| 512 | 561 | </div> |
| 513 | 562 | <div class="section-card__actions"> |
| 514 | - <el-button type="primary" size="mini" @click="openConfigDialog('absenteeismRule')">新增规则</el-button> | |
| 563 | + <el-button type="primary" size="mini" | |
| 564 | + @click="openConfigDialog('absenteeismRule')">新增规则</el-button> | |
| 515 | 565 | </div> |
| 516 | 566 | </div> |
| 517 | - <NCC-table v-loading="moduleState.absenteeismRule.loading" :data="moduleState.absenteeismRule.list" border size="mini" height="320"> | |
| 567 | + <NCC-table v-loading="moduleState.absenteeismRule.loading" :data="moduleState.absenteeismRule.list" | |
| 568 | + border size="mini" height="320"> | |
| 518 | 569 | <el-table-column prop="minDays" label="最小天数(含)" width="120" align="left"> |
| 519 | 570 | <template slot-scope="scope"> |
| 520 | 571 | <span class="cell-with-icon cell-with-icon--danger" :title="String(scope.row.minDays)"> |
| ... | ... | @@ -546,8 +597,10 @@ |
| 546 | 597 | <el-table-column label="操作" width="210" align="left" fixed="right"> |
| 547 | 598 | <template slot-scope="scope"> |
| 548 | 599 | <el-button type="text" @click="openConfigDialog('absenteeismRule', scope.row)">编辑</el-button> |
| 549 | - <el-button type="text" @click="openHistory('absenteeismRule', scope.row, buildDecimalRangeTitle(scope.row, '旷工规则'))">历史</el-button> | |
| 550 | - <el-button type="text" class="danger-text" @click="handleDelete('absenteeismRule', scope.row)">删除</el-button> | |
| 600 | + <el-button type="text" | |
| 601 | + @click="openHistory('absenteeismRule', scope.row, buildDecimalRangeTitle(scope.row, '旷工规则'))">历史</el-button> | |
| 602 | + <el-button type="text" class="danger-text" | |
| 603 | + @click="handleDelete('absenteeismRule', scope.row)">删除</el-button> | |
| 551 | 604 | </template> |
| 552 | 605 | </el-table-column> |
| 553 | 606 | </NCC-table> |
| ... | ... | @@ -569,19 +622,17 @@ |
| 569 | 622 | </div> |
| 570 | 623 | |
| 571 | 624 | <div class="section-filter"> |
| 572 | - <el-input | |
| 573 | - v-model.trim="moduleState.exemptUser.query.keyword" | |
| 574 | - size="small" | |
| 575 | - class="section-filter__keyword" | |
| 576 | - clearable | |
| 577 | - placeholder="员工姓名 / 备注" | |
| 578 | - @keyup.enter.native="handleModuleSearch('exemptUser')" | |
| 579 | - /> | |
| 580 | - <el-button type="primary" size="small" icon="el-icon-search" @click="handleModuleSearch('exemptUser')">查询</el-button> | |
| 581 | - <el-button size="small" icon="el-icon-refresh-right" @click="handleModuleReset('exemptUser')">重置</el-button> | |
| 625 | + <el-input v-model.trim="moduleState.exemptUser.query.keyword" size="small" | |
| 626 | + class="section-filter__keyword" clearable placeholder="员工姓名 / 备注" | |
| 627 | + @keyup.enter.native="handleModuleSearch('exemptUser')" /> | |
| 628 | + <el-button type="primary" size="small" icon="el-icon-search" | |
| 629 | + @click="handleModuleSearch('exemptUser')">查询</el-button> | |
| 630 | + <el-button size="small" icon="el-icon-refresh-right" | |
| 631 | + @click="handleModuleReset('exemptUser')">重置</el-button> | |
| 582 | 632 | </div> |
| 583 | 633 | |
| 584 | - <NCC-table v-loading="moduleState.exemptUser.loading" :data="moduleState.exemptUser.list" border size="mini" height="460"> | |
| 634 | + <NCC-table v-loading="moduleState.exemptUser.loading" :data="moduleState.exemptUser.list" border | |
| 635 | + size="mini" height="460"> | |
| 585 | 636 | <el-table-column prop="userName" label="员工" min-width="150" align="left"> |
| 586 | 637 | <template slot-scope="scope"> |
| 587 | 638 | <span class="cell-with-icon cell-with-icon--primary" :title="scope.row.userName || '无'"> |
| ... | ... | @@ -615,21 +666,22 @@ |
| 615 | 666 | <el-table-column label="操作" width="210" align="left" fixed="right"> |
| 616 | 667 | <template slot-scope="scope"> |
| 617 | 668 | <el-button type="text" @click="openConfigDialog('exemptUser', scope.row)">编辑</el-button> |
| 618 | - <el-button type="text" @click="openHistory('exemptUser', scope.row, scope.row.userName)">历史</el-button> | |
| 619 | - <el-button type="text" class="danger-text" @click="handleDelete('exemptUser', scope.row)">删除</el-button> | |
| 669 | + <el-button type="text" | |
| 670 | + @click="openHistory('exemptUser', scope.row, scope.row.userName)">历史</el-button> | |
| 671 | + <el-button type="text" class="danger-text" | |
| 672 | + @click="handleDelete('exemptUser', scope.row)">删除</el-button> | |
| 620 | 673 | </template> |
| 621 | 674 | </el-table-column> |
| 622 | 675 | </NCC-table> |
| 623 | - <div v-if="!moduleState.exemptUser.loading && !moduleState.exemptUser.list.length" class="section-empty"> | |
| 676 | + <div v-if="!moduleState.exemptUser.loading && !moduleState.exemptUser.list.length" | |
| 677 | + class="section-empty"> | |
| 624 | 678 | <el-empty :image-size="60" description="暂无免考勤人员配置" /> |
| 625 | 679 | </div> |
| 626 | - <pagination | |
| 627 | - :hidden="!moduleState.exemptUser.pagination.total" | |
| 680 | + <pagination :hidden="!moduleState.exemptUser.pagination.total" | |
| 628 | 681 | :total="moduleState.exemptUser.pagination.total" |
| 629 | 682 | :page.sync="moduleState.exemptUser.query.currentPage" |
| 630 | 683 | :limit.sync="moduleState.exemptUser.query.pageSize" |
| 631 | - @pagination="() => loadModule('exemptUser', true)" | |
| 632 | - /> | |
| 684 | + @pagination="() => loadModule('exemptUser', true)" /> | |
| 633 | 685 | </el-card> |
| 634 | 686 | </div> |
| 635 | 687 | </el-tab-pane> |
| ... | ... | @@ -648,22 +700,21 @@ |
| 648 | 700 | </div> |
| 649 | 701 | |
| 650 | 702 | <div class="section-filter"> |
| 651 | - <el-select v-model="moduleState.extraLeave.query.year" size="small" class="section-filter__year" @change="handleModuleSearch('extraLeave')"> | |
| 703 | + <el-select v-model="moduleState.extraLeave.query.year" size="small" class="section-filter__year" | |
| 704 | + @change="handleModuleSearch('extraLeave')"> | |
| 652 | 705 | <el-option v-for="item in yearOptions" :key="item" :label="`${item}年`" :value="item" /> |
| 653 | 706 | </el-select> |
| 654 | - <el-input | |
| 655 | - v-model.trim="moduleState.extraLeave.query.keyword" | |
| 656 | - size="small" | |
| 657 | - class="section-filter__keyword" | |
| 658 | - clearable | |
| 659 | - placeholder="员工姓名 / 假期名称 / 备注" | |
| 660 | - @keyup.enter.native="handleModuleSearch('extraLeave')" | |
| 661 | - /> | |
| 662 | - <el-button type="primary" size="small" icon="el-icon-search" @click="handleModuleSearch('extraLeave')">查询</el-button> | |
| 663 | - <el-button size="small" icon="el-icon-refresh-right" @click="handleModuleReset('extraLeave')">重置</el-button> | |
| 707 | + <el-input v-model.trim="moduleState.extraLeave.query.keyword" size="small" | |
| 708 | + class="section-filter__keyword" clearable placeholder="员工姓名 / 假期名称 / 备注" | |
| 709 | + @keyup.enter.native="handleModuleSearch('extraLeave')" /> | |
| 710 | + <el-button type="primary" size="small" icon="el-icon-search" | |
| 711 | + @click="handleModuleSearch('extraLeave')">查询</el-button> | |
| 712 | + <el-button size="small" icon="el-icon-refresh-right" | |
| 713 | + @click="handleModuleReset('extraLeave')">重置</el-button> | |
| 664 | 714 | </div> |
| 665 | 715 | |
| 666 | - <NCC-table v-loading="moduleState.extraLeave.loading" :data="moduleState.extraLeave.list" border size="mini" height="460"> | |
| 716 | + <NCC-table v-loading="moduleState.extraLeave.loading" :data="moduleState.extraLeave.list" border | |
| 717 | + size="mini" height="460"> | |
| 667 | 718 | <el-table-column prop="userName" label="员工" min-width="140" align="left"> |
| 668 | 719 | <template slot-scope="scope"> |
| 669 | 720 | <span class="cell-with-icon cell-with-icon--primary" :title="scope.row.userName || '无'"> |
| ... | ... | @@ -697,21 +748,22 @@ |
| 697 | 748 | <el-table-column label="操作" width="210" align="left" fixed="right"> |
| 698 | 749 | <template slot-scope="scope"> |
| 699 | 750 | <el-button type="text" @click="openConfigDialog('extraLeave', scope.row)">编辑</el-button> |
| 700 | - <el-button type="text" @click="openHistory('extraLeave', scope.row, buildExtraLeaveTitle(scope.row))">历史</el-button> | |
| 701 | - <el-button type="text" class="danger-text" @click="handleDelete('extraLeave', scope.row)">删除</el-button> | |
| 751 | + <el-button type="text" | |
| 752 | + @click="openHistory('extraLeave', scope.row, buildExtraLeaveTitle(scope.row))">历史</el-button> | |
| 753 | + <el-button type="text" class="danger-text" | |
| 754 | + @click="handleDelete('extraLeave', scope.row)">删除</el-button> | |
| 702 | 755 | </template> |
| 703 | 756 | </el-table-column> |
| 704 | 757 | </NCC-table> |
| 705 | - <div v-if="!moduleState.extraLeave.loading && !moduleState.extraLeave.list.length" class="section-empty"> | |
| 758 | + <div v-if="!moduleState.extraLeave.loading && !moduleState.extraLeave.list.length" | |
| 759 | + class="section-empty"> | |
| 706 | 760 | <el-empty :image-size="60" description="当前筛选条件下暂无额外假期配置" /> |
| 707 | 761 | </div> |
| 708 | - <pagination | |
| 709 | - :hidden="!moduleState.extraLeave.pagination.total" | |
| 762 | + <pagination :hidden="!moduleState.extraLeave.pagination.total" | |
| 710 | 763 | :total="moduleState.extraLeave.pagination.total" |
| 711 | 764 | :page.sync="moduleState.extraLeave.query.currentPage" |
| 712 | 765 | :limit.sync="moduleState.extraLeave.query.pageSize" |
| 713 | - @pagination="() => loadModule('extraLeave', true)" | |
| 714 | - /> | |
| 766 | + @pagination="() => loadModule('extraLeave', true)" /> | |
| 715 | 767 | </el-card> |
| 716 | 768 | </div> |
| 717 | 769 | </el-tab-pane> |
| ... | ... | @@ -721,12 +773,8 @@ |
| 721 | 773 | </div> |
| 722 | 774 | </div> |
| 723 | 775 | |
| 724 | - <attendance-config-item-dialog | |
| 725 | - ref="itemDialog" | |
| 726 | - :deduct-mode-options="deductModeOptions" | |
| 727 | - :action-type-options="actionTypeOptions" | |
| 728 | - @submit="handleDialogSubmit" | |
| 729 | - /> | |
| 776 | + <attendance-config-item-dialog ref="itemDialog" :deduct-mode-options="deductModeOptions" | |
| 777 | + :action-type-options="actionTypeOptions" @submit="handleDialogSubmit" /> | |
| 730 | 778 | <attendance-config-history-dialog ref="historyDialog" /> |
| 731 | 779 | <attendance-group-user-dialog ref="groupUserDialog" /> |
| 732 | 780 | </div> |
| ... | ... | @@ -1349,7 +1397,7 @@ export default { |
| 1349 | 1397 | flex-direction: column; |
| 1350 | 1398 | } |
| 1351 | 1399 | |
| 1352 | -::v-deep .setting-tabs > .el-tabs__header { | |
| 1400 | +::v-deep .setting-tabs>.el-tabs__header { | |
| 1353 | 1401 | flex-shrink: 0; |
| 1354 | 1402 | margin: 0 0 12px; |
| 1355 | 1403 | background: #fff; |
| ... | ... | @@ -1380,7 +1428,7 @@ export default { |
| 1380 | 1428 | background-color: #409eff; |
| 1381 | 1429 | } |
| 1382 | 1430 | |
| 1383 | -::v-deep .setting-tabs > .el-tabs__content { | |
| 1431 | +::v-deep .setting-tabs>.el-tabs__content { | |
| 1384 | 1432 | flex: 1; |
| 1385 | 1433 | min-height: 0; |
| 1386 | 1434 | overflow: auto !important; |
| ... | ... | @@ -1521,26 +1569,26 @@ export default { |
| 1521 | 1569 | white-space: nowrap; |
| 1522 | 1570 | } |
| 1523 | 1571 | |
| 1524 | -.cell-with-icon > i { | |
| 1572 | +.cell-with-icon>i { | |
| 1525 | 1573 | flex-shrink: 0; |
| 1526 | 1574 | margin-right: 6px; |
| 1527 | 1575 | font-size: 14px; |
| 1528 | 1576 | } |
| 1529 | 1577 | |
| 1530 | -.cell-with-icon > span:last-child { | |
| 1578 | +.cell-with-icon>span:last-child { | |
| 1531 | 1579 | overflow: hidden; |
| 1532 | 1580 | text-overflow: ellipsis; |
| 1533 | 1581 | } |
| 1534 | 1582 | |
| 1535 | -.cell-with-icon--primary > i { | |
| 1583 | +.cell-with-icon--primary>i { | |
| 1536 | 1584 | color: #409eff; |
| 1537 | 1585 | } |
| 1538 | 1586 | |
| 1539 | -.cell-with-icon--success > i { | |
| 1587 | +.cell-with-icon--success>i { | |
| 1540 | 1588 | color: #67c23a; |
| 1541 | 1589 | } |
| 1542 | 1590 | |
| 1543 | -.cell-with-icon--danger > i { | |
| 1591 | +.cell-with-icon--danger>i { | |
| 1544 | 1592 | color: #f56c6c; |
| 1545 | 1593 | } |
| 1546 | 1594 | ... | ... |
antis-ncc-admin/src/views/workFlow/leave-apply-pages/components/leave-apply-page.vue
| 1 | 1 | <template> |
| 2 | 2 | <div class="leave-apply-page" v-loading="loading || quotaLoading"> |
| 3 | - <div class="page-head"> | |
| 3 | + <div v-if="!flowBoxMode" class="page-head"> | |
| 4 | 4 | <div> |
| 5 | 5 | <div class="page-title">{{ currentSceneConfig.title }}</div> |
| 6 | 6 | <div class="page-tip">{{ currentSceneConfig.tip }}</div> |
| 7 | 7 | </div> |
| 8 | 8 | <div class="page-actions"> |
| 9 | 9 | <el-button @click="handleBack">返回</el-button> |
| 10 | - <el-button type="warning" :loading="submitLoading && submitType === 'save'" @click="handleSubmit('save')">保存草稿</el-button> | |
| 11 | - <el-button type="primary" :loading="submitLoading && submitType === 'submit'" @click="handleSubmit('submit')">提交申请</el-button> | |
| 10 | + <el-button type="warning" :loading="submitLoading && submitType === 'save'" | |
| 11 | + @click="handleSubmit('save')">保存草稿</el-button> | |
| 12 | + <el-button type="primary" :loading="submitLoading && submitType === 'submit'" | |
| 13 | + @click="handleSubmit('submit')">提交申请</el-button> | |
| 12 | 14 | </div> |
| 13 | 15 | </div> |
| 14 | 16 | |
| 15 | - <el-alert | |
| 16 | - v-if="!dataForm.flowId" | |
| 17 | - class="head-alert" | |
| 18 | - type="warning" | |
| 19 | - :closable="false" | |
| 20 | - title="当前页面未携带 flowId,请从流程入口进入,或在地址后追加 ?flowId=流程ID 再提交。" | |
| 21 | - show-icon | |
| 22 | - /> | |
| 17 | + <el-alert v-if="!flowBoxMode && !dataForm.flowId" class="head-alert" type="warning" :closable="false" | |
| 18 | + title="当前页面未携带 flowId,请从流程入口进入,或在地址后追加 ?flowId=流程ID 再提交。" show-icon /> | |
| 23 | 19 | |
| 24 | 20 | <div v-if="showSummaryPanel" class="summary-panel"> |
| 25 | 21 | <div class="summary-head"> |
| ... | ... | @@ -43,7 +39,7 @@ |
| 43 | 39 | <span>申请信息</span> |
| 44 | 40 | <span class="bill-no">单据号:{{ dataForm.billNo || '生成中...' }}</span> |
| 45 | 41 | </div> |
| 46 | - <el-form ref="dataForm" :model="dataForm" :rules="dataRule" label-width="100px"> | |
| 42 | + <el-form ref="dataForm" :model="dataForm" :rules="dataRule" label-width="100px" :disabled="setting.readonly"> | |
| 47 | 43 | <el-row :gutter="16"> |
| 48 | 44 | <el-col :span="12"> |
| 49 | 45 | <el-form-item label="流程标题" prop="flowTitle"> |
| ... | ... | @@ -53,7 +49,8 @@ |
| 53 | 49 | <el-col :span="12"> |
| 54 | 50 | <el-form-item label="紧急程度" prop="flowUrgent"> |
| 55 | 51 | <el-select v-model="dataForm.flowUrgent" placeholder="请选择紧急程度"> |
| 56 | - <el-option v-for="item in flowUrgentOptions" :key="item.value" :label="item.label" :value="item.value" /> | |
| 52 | + <el-option v-for="item in flowUrgentOptions" :key="item.value" :label="item.label" | |
| 53 | + :value="item.value" /> | |
| 57 | 54 | </el-select> |
| 58 | 55 | </el-form-item> |
| 59 | 56 | </el-col> |
| ... | ... | @@ -64,7 +61,8 @@ |
| 64 | 61 | </el-col> |
| 65 | 62 | <el-col :span="12"> |
| 66 | 63 | <el-form-item label="申请日期"> |
| 67 | - <el-date-picker v-model="dataForm.applyDate" type="date" value-format="timestamp" format="yyyy-MM-dd" readonly :editable="false" /> | |
| 64 | + <el-date-picker v-model="dataForm.applyDate" type="date" value-format="timestamp" format="yyyy-MM-dd" | |
| 65 | + readonly :editable="false" /> | |
| 68 | 66 | </el-form-item> |
| 69 | 67 | </el-col> |
| 70 | 68 | <el-col :span="12"> |
| ... | ... | @@ -80,7 +78,8 @@ |
| 80 | 78 | <el-col :span="24"> |
| 81 | 79 | <el-form-item :label="currentSceneConfig.leaveTypeLabel" prop="leaveType"> |
| 82 | 80 | <el-radio-group v-model="dataForm.leaveType" @change="handleLeaveTypeChange"> |
| 83 | - <el-radio v-for="item in leaveTypeOptions" :key="item.value" :label="item.value">{{ item.label }}</el-radio> | |
| 81 | + <el-radio v-for="item in leaveTypeOptions" :key="item.value" :label="item.value">{{ item.label | |
| 82 | + }}</el-radio> | |
| 84 | 83 | </el-radio-group> |
| 85 | 84 | </el-form-item> |
| 86 | 85 | </el-col> |
| ... | ... | @@ -94,17 +93,20 @@ |
| 94 | 93 | </el-col> |
| 95 | 94 | <el-col :span="24"> |
| 96 | 95 | <el-form-item label="请假原因" prop="leaveReason"> |
| 97 | - <el-input v-model.trim="dataForm.leaveReason" type="textarea" :rows="3" maxlength="300" show-word-limit :placeholder="currentSceneConfig.reasonPlaceholder" /> | |
| 96 | + <el-input v-model.trim="dataForm.leaveReason" type="textarea" :rows="3" maxlength="300" show-word-limit | |
| 97 | + :placeholder="currentSceneConfig.reasonPlaceholder" /> | |
| 98 | 98 | </el-form-item> |
| 99 | 99 | </el-col> |
| 100 | 100 | <el-col :span="12"> |
| 101 | 101 | <el-form-item label="开始时间" prop="leaveStartTime"> |
| 102 | - <el-date-picker v-model="dataForm.leaveStartTime" type="datetime" value-format="timestamp" format="yyyy-MM-dd HH:mm" :editable="false" placeholder="请选择开始时间" @change="computeDuration" /> | |
| 102 | + <el-date-picker v-model="dataForm.leaveStartTime" type="datetime" value-format="timestamp" | |
| 103 | + format="yyyy-MM-dd HH:mm" :editable="false" placeholder="请选择开始时间" @change="computeDuration" /> | |
| 103 | 104 | </el-form-item> |
| 104 | 105 | </el-col> |
| 105 | 106 | <el-col :span="12"> |
| 106 | 107 | <el-form-item label="结束时间" prop="leaveEndTime"> |
| 107 | - <el-date-picker v-model="dataForm.leaveEndTime" type="datetime" value-format="timestamp" format="yyyy-MM-dd HH:mm" :editable="false" placeholder="请选择结束时间" @change="computeDuration" /> | |
| 108 | + <el-date-picker v-model="dataForm.leaveEndTime" type="datetime" value-format="timestamp" | |
| 109 | + format="yyyy-MM-dd HH:mm" :editable="false" placeholder="请选择结束时间" @change="computeDuration" /> | |
| 108 | 110 | </el-form-item> |
| 109 | 111 | </el-col> |
| 110 | 112 | <el-col :span="24"> |
| ... | ... | @@ -203,6 +205,8 @@ export default { |
| 203 | 205 | applicantName: '', |
| 204 | 206 | fileList: [], |
| 205 | 207 | quotaSummary: null, |
| 208 | + flowBoxMode: false, | |
| 209 | + setting: {}, | |
| 206 | 210 | flowUrgentOptions: [ |
| 207 | 211 | { value: 1, label: '普通' }, |
| 208 | 212 | { value: 2, label: '重要' }, |
| ... | ... | @@ -270,59 +274,156 @@ export default { |
| 270 | 274 | currentSceneConfig() { |
| 271 | 275 | return this.sceneConfigMap[this.scene] || this.sceneConfigMap[REST_SCENE] |
| 272 | 276 | }, |
| 273 | - leaveTypeOptions() { | |
| 274 | - return this.currentSceneConfig.leaveTypeOptions || [] | |
| 275 | - }, | |
| 276 | 277 | isRestScene() { |
| 277 | 278 | return this.scene === REST_SCENE |
| 278 | 279 | }, |
| 280 | + isPersonalScene() { | |
| 281 | + return this.scene === PERSONAL_SCENE | |
| 282 | + }, | |
| 279 | 283 | isPaidScene() { |
| 280 | 284 | return this.scene === PAID_SCENE |
| 281 | 285 | }, |
| 282 | 286 | showSummaryPanel() { |
| 283 | - return true | |
| 287 | + return this.isRestScene || this.isPaidScene | |
| 284 | 288 | }, |
| 285 | 289 | restQuota() { |
| 286 | - return (this.quotaSummary && this.quotaSummary.rest) || {} | |
| 290 | + var q = this.quotaSummary || {} | |
| 291 | + var r = q.rest || q.Rest | |
| 292 | + if (!r || typeof r !== 'object') return {} | |
| 293 | + return { | |
| 294 | + ...r, | |
| 295 | + workedDaysThisMonth: r.workedDaysThisMonth != null ? r.workedDaysThisMonth : (r.WorkedDaysThisMonth != null ? r.WorkedDaysThisMonth : 0), | |
| 296 | + restUnlockCycle: r.restUnlockCycle != null ? r.restUnlockCycle : (r.RestUnlockCycle != null ? r.RestUnlockCycle : 0), | |
| 297 | + unlockedRestDays: r.unlockedRestDays != null ? r.unlockedRestDays : (r.UnlockedRestDays != null ? r.UnlockedRestDays : 0), | |
| 298 | + availableRestDays: r.availableRestDays != null ? r.availableRestDays : (r.AvailableRestDays != null ? r.AvailableRestDays : 0) | |
| 299 | + } | |
| 287 | 300 | }, |
| 288 | 301 | paidQuota() { |
| 289 | - return (this.quotaSummary && this.quotaSummary.paid) || {} | |
| 302 | + const q = this.quotaSummary || {} | |
| 303 | + const paid = q.paid || q.Paid | |
| 304 | + if (!paid || typeof paid !== 'object') { | |
| 305 | + return { marriage: {}, funeral: {}, annual: {}, maternity: {}, extraLeaves: [] } | |
| 306 | + } | |
| 307 | + const m = paid.marriage || paid.Marriage || {} | |
| 308 | + const f = paid.funeral || paid.Funeral || {} | |
| 309 | + const a = paid.annual || paid.Annual || {} | |
| 310 | + const mat = paid.maternity || paid.Maternity || {} | |
| 311 | + const exRaw = paid.extraLeaves || paid.ExtraLeaves | |
| 312 | + let exArr = [] | |
| 313 | + if (Array.isArray(exRaw)) exArr = exRaw | |
| 314 | + else if (exRaw && typeof exRaw === 'object') exArr = Object.values(exRaw) | |
| 315 | + var extraLeaves = exArr.map(function (x, idx) { | |
| 316 | + return { | |
| 317 | + id: x.id || x.Id || ('extra-' + idx), | |
| 318 | + leaveName: String(x.leaveName || x.LeaveName || '').trim(), | |
| 319 | + grantYear: x.grantYear != null ? x.grantYear : x.GrantYear, | |
| 320 | + extraDays: Number(x.extraDays != null ? x.extraDays : (x.ExtraDays != null ? x.ExtraDays : 0)) | |
| 321 | + } | |
| 322 | + }).filter(function (x) { return x.leaveName }) | |
| 323 | + return { | |
| 324 | + marriage: { | |
| 325 | + matched: m.matched != null ? m.matched : m.Matched, | |
| 326 | + maxDays: Number(m.maxDays != null ? m.maxDays : (m.MaxDays != null ? m.MaxDays : 0)) | |
| 327 | + }, | |
| 328 | + funeral: { | |
| 329 | + matched: f.matched != null ? f.matched : f.Matched, | |
| 330 | + directRelativeDays: Number(f.directRelativeDays != null ? f.directRelativeDays : (f.DirectRelativeDays != null ? f.DirectRelativeDays : 0)), | |
| 331 | + indirectRelativeDays: Number(f.indirectRelativeDays != null ? f.indirectRelativeDays : (f.IndirectRelativeDays != null ? f.IndirectRelativeDays : 0)) | |
| 332 | + }, | |
| 333 | + annual: { | |
| 334 | + matched: a.matched != null ? a.matched : a.Matched, | |
| 335 | + totalDays: Number(a.totalDays != null ? a.totalDays : (a.TotalDays != null ? a.TotalDays : 0)), | |
| 336 | + usedDays: Number(a.usedDays != null ? a.usedDays : (a.UsedDays != null ? a.UsedDays : 0)), | |
| 337 | + remainingDays: Number(a.remainingDays != null ? a.remainingDays : (a.RemainingDays != null ? a.RemainingDays : 0)) | |
| 338 | + }, | |
| 339 | + maternity: { | |
| 340 | + matched: mat.matched != null ? mat.matched : mat.Matched, | |
| 341 | + maxDays: Number(mat.maxDays != null ? mat.maxDays : (mat.MaxDays != null ? mat.MaxDays : 0)) | |
| 342 | + }, | |
| 343 | + extraLeaves: extraLeaves | |
| 344 | + } | |
| 345 | + }, | |
| 346 | + extraLeaveList() { | |
| 347 | + return Array.isArray(this.paidQuota.extraLeaves) ? this.paidQuota.extraLeaves : [] | |
| 348 | + }, | |
| 349 | + leaveTypeOptions() { | |
| 350 | + const baseOptions = this.currentSceneConfig.leaveTypeOptions || [] | |
| 351 | + if (!this.isPaidScene || !this.extraLeaveList.length) return baseOptions | |
| 352 | + const extraOptions = this.extraLeaveList.map(item => { | |
| 353 | + const nm = String(item.leaveName || '').trim() | |
| 354 | + const gy = item.grantYear | |
| 355 | + const label = nm && gy != null && gy !== '' ? `${nm}(${gy}年)` : nm | |
| 356 | + return { value: nm, label } | |
| 357 | + }) | |
| 358 | + return [...baseOptions, ...extraOptions] | |
| 290 | 359 | }, |
| 291 | 360 | summaryCards() { |
| 292 | 361 | if (this.isRestScene) { |
| 293 | - return [ | |
| 294 | - { key: 'total', label: '本月应休', value: this.formatNumber(this.restQuota.monthlyRestDays || 0), desc: '天' }, | |
| 295 | - { key: 'used', label: '已申请', value: this.formatNumber(this.restQuota.usedRestDays || 0), desc: '天' }, | |
| 296 | - { key: 'remain', label: '剩余可休', value: this.formatNumber(this.restQuota.remainingRestDays || 0), desc: '天', active: true }, | |
| 297 | - { key: 'split', label: '半天额度', value: this.formatNumber(this.restQuota.remainingHalfDaySplitDays || 0), desc: `还能拆 ${this.restQuota.remainingHalfDaySelections || 0} 次半天` } | |
| 362 | + const rest = this.restQuota | |
| 363 | + const unlockEnabled = rest.restUnlockCycle > 0 | |
| 364 | + const remainValue = unlockEnabled ? (rest.availableRestDays || 0) : (rest.remainingRestDays || 0) | |
| 365 | + const remainDesc = unlockEnabled ? `已解锁 ${rest.unlockedRestDays || 0} 天` : '天' | |
| 366 | + const cards = [ | |
| 367 | + { key: 'total', label: '本月应休', value: this.formatNumber(rest.monthlyRestDays || 0), desc: '天' }, | |
| 368 | + { key: 'used', label: '已申请', value: this.formatNumber(rest.usedRestDays || 0), desc: '天' }, | |
| 369 | + { key: 'remain', label: '剩余可休', value: this.formatNumber(remainValue), desc: remainDesc, active: true }, | |
| 370 | + { key: 'split', label: '半天额度', value: this.formatNumber(rest.remainingHalfDaySplitDays || 0), desc: `还能拆 ${rest.remainingHalfDaySelections || 0} 次半天` } | |
| 298 | 371 | ] |
| 372 | + if (unlockEnabled) { | |
| 373 | + cards.push({ | |
| 374 | + key: 'worked', | |
| 375 | + label: '本月已上班', | |
| 376 | + value: `${rest.workedDaysThisMonth || 0} 天`, | |
| 377 | + desc: `每${rest.restUnlockCycle}天解锁1天` | |
| 378 | + }) | |
| 379 | + } | |
| 380 | + return cards | |
| 299 | 381 | } |
| 300 | - if (this.scene === PERSONAL_SCENE) { | |
| 382 | + if (this.isPersonalScene) { | |
| 301 | 383 | return [ |
| 302 | 384 | { key: 'personal', label: '可申请类型', value: '事假 / 病假', desc: '按实际请假时长填写', active: true } |
| 303 | 385 | ] |
| 304 | 386 | } |
| 387 | + const extraTotalDays = this.extraLeaveList.reduce((sum, item) => sum + Number(item.extraDays || 0), 0) | |
| 388 | + const sel = String(this.dataForm.leaveType || '').trim() | |
| 389 | + const extraTypeSelected = this.extraLeaveList.some(item => String(item.leaveName || '').trim() === sel) | |
| 305 | 390 | return [ |
| 306 | 391 | { key: 'marriage', label: '婚假', value: `${this.formatNumber((this.paidQuota.marriage || {}).maxDays || 0)} 天`, active: this.dataForm.leaveType === '婚假' }, |
| 307 | 392 | { key: 'funeral', label: '丧假', value: `${this.formatNumber((this.paidQuota.funeral || {}).directRelativeDays || 0)} / ${this.formatNumber((this.paidQuota.funeral || {}).indirectRelativeDays || 0)} 天`, desc: '直系 / 非直系', active: this.dataForm.leaveType === '丧假' }, |
| 308 | 393 | { key: 'annual', label: '年假剩余', value: `${this.formatNumber((this.paidQuota.annual || {}).remainingDays || 0)} 天`, desc: `总额 ${this.formatNumber((this.paidQuota.annual || {}).totalDays || 0)} 天`, active: this.dataForm.leaveType === '年假' }, |
| 309 | - { key: 'maternity', label: '产假', value: `${this.formatNumber((this.paidQuota.maternity || {}).maxDays || 0)} 天`, desc: '按后台规则', active: this.dataForm.leaveType === '产假' } | |
| 394 | + { key: 'maternity', label: '产假', value: `${this.formatNumber((this.paidQuota.maternity || {}).maxDays || 0)} 天`, desc: '按后台规则', active: this.dataForm.leaveType === '产假' }, | |
| 395 | + { key: 'extra', label: '额外假期总额', value: `${this.formatNumber(extraTotalDays)} 天`, active: extraTypeSelected } | |
| 310 | 396 | ] |
| 311 | 397 | }, |
| 312 | 398 | leaveTypeSummaryText() { |
| 313 | 399 | if (this.isRestScene) { |
| 314 | - if (!this.restQuota.attendanceGroupBound) return '当前用户还未绑定考勤分组,暂时无法计算本月休假额度。' | |
| 315 | - return `当前分组【${this.restQuota.attendanceGroupName || '未命名分组'}】本月还可休 ${this.formatNumber(this.restQuota.remainingRestDays || 0)} 天;其中还能拆分半天 ${this.formatNumber(this.restQuota.remainingHalfDaySplitDays || 0)} 天。` | |
| 400 | + const rest = this.restQuota | |
| 401 | + if (!rest.attendanceGroupBound) return '当前用户还未绑定考勤分组,暂时无法计算本月休假额度。' | |
| 402 | + if (rest.restUnlockCycle > 0) { | |
| 403 | + const worked = rest.workedDaysThisMonth || 0 | |
| 404 | + const cycle = rest.restUnlockCycle | |
| 405 | + const unlocked = rest.unlockedRestDays || 0 | |
| 406 | + const available = rest.availableRestDays || 0 | |
| 407 | + const daysInCurrentCycle = worked % cycle | |
| 408 | + const nextNeed = daysInCurrentCycle === 0 ? cycle : cycle - daysInCurrentCycle | |
| 409 | + const canUnlockMore = unlocked < (rest.monthlyRestDays || 0) | |
| 410 | + let text = `本月已上班 ${worked} 天,已解锁 ${unlocked} 天应休,当前可申请 ${available} 天。` | |
| 411 | + if (canUnlockMore && nextNeed > 0) { | |
| 412 | + text += `再上满 ${nextNeed} 天可再解锁 1 天。` | |
| 413 | + } | |
| 414 | + return text | |
| 415 | + } | |
| 416 | + return `当前分组【${rest.attendanceGroupName || '未命名分组'}】本月还可休 ${this.formatNumber(rest.remainingRestDays || 0)} 天;其中还能拆分半天 ${this.formatNumber(rest.remainingHalfDaySplitDays || 0)} 天。` | |
| 316 | 417 | } |
| 317 | - if (this.scene === PERSONAL_SCENE) return '事假、病假页面不限制后台额度,按实际时长填写并走审批。' | |
| 418 | + if (this.isPersonalScene) return '事假、病假页面不限制后台额度,按实际时长填写并走审批。' | |
| 318 | 419 | if (this.dataForm.leaveType === '婚假') { |
| 319 | 420 | const maxDays = Number((this.paidQuota.marriage || {}).maxDays || 0) |
| 320 | 421 | return maxDays > 0 ? `按当前司龄规则,本次婚假最多可请 ${this.formatNumber(maxDays)} 天。` : '当前未匹配到婚假规则,请先联系管理员维护规则。' |
| 321 | 422 | } |
| 322 | 423 | if (this.dataForm.leaveType === '丧假') { |
| 323 | 424 | const funeral = this.paidQuota.funeral || {} |
| 324 | - const maxDays = Number(Number(this.dataForm.funeralRelationType) === 1 ? funeral.directRelativeDays || 0 : funeral.indirectRelativeDays || 0) | |
| 325 | - return `当前丧假规则:直系亲属 ${this.formatNumber(funeral.directRelativeDays || 0)} 天,非直系亲属 ${this.formatNumber(funeral.indirectRelativeDays || 0)} 天;当前选择最多可请 ${this.formatNumber(maxDays)} 天。` | |
| 425 | + const currentDays = Number(Number(this.dataForm.funeralRelationType) === 1 ? funeral.directRelativeDays : funeral.indirectRelativeDays) || 0 | |
| 426 | + return `当前丧假规则:直系亲属 ${this.formatNumber(funeral.directRelativeDays || 0)} 天,非直系亲属 ${this.formatNumber(funeral.indirectRelativeDays || 0)} 天;当前选择最多可请 ${this.formatNumber(currentDays)} 天。` | |
| 326 | 427 | } |
| 327 | 428 | if (this.dataForm.leaveType === '年假') { |
| 328 | 429 | const annual = this.paidQuota.annual || {} |
| ... | ... | @@ -332,15 +433,26 @@ export default { |
| 332 | 433 | const maternity = this.paidQuota.maternity || {} |
| 333 | 434 | return Number(maternity.maxDays || 0) > 0 ? `按当前司龄规则,本次产假最多可请 ${this.formatNumber(maternity.maxDays || 0)} 天。` : '当前未匹配到产假规则,请先联系管理员维护规则。' |
| 334 | 435 | } |
| 436 | + const matchedExtraLeave = this.extraLeaveList.find(item => String(item.leaveName || '').trim() === String(this.dataForm.leaveType || '').trim()) | |
| 437 | + if (matchedExtraLeave) { | |
| 438 | + return `当前额外假期【${matchedExtraLeave.leaveName}】最多可请 ${this.formatNumber(matchedExtraLeave.extraDays || 0)} 天。` | |
| 439 | + } | |
| 335 | 440 | return '' |
| 336 | 441 | }, |
| 337 | 442 | selectedLeaveMaxDays() { |
| 338 | - if (this.isRestScene) return Number(this.restQuota.remainingRestDays || 0) | |
| 443 | + if (this.isRestScene) { | |
| 444 | + const rest = this.restQuota | |
| 445 | + return rest.restUnlockCycle > 0 | |
| 446 | + ? Number(rest.availableRestDays || 0) | |
| 447 | + : Number(rest.remainingRestDays || 0) | |
| 448 | + } | |
| 339 | 449 | if (!this.isPaidScene) return null |
| 340 | 450 | if (this.dataForm.leaveType === '婚假') return Number((this.paidQuota.marriage || {}).maxDays || 0) |
| 341 | 451 | if (this.dataForm.leaveType === '丧假') return Number(Number(this.dataForm.funeralRelationType) === 1 ? (this.paidQuota.funeral || {}).directRelativeDays || 0 : (this.paidQuota.funeral || {}).indirectRelativeDays || 0) |
| 342 | 452 | if (this.dataForm.leaveType === '年假') return Number((this.paidQuota.annual || {}).remainingDays || 0) |
| 343 | 453 | if (this.dataForm.leaveType === '产假') return Number((this.paidQuota.maternity || {}).maxDays || 0) |
| 454 | + const matchedExtraLeave = this.extraLeaveList.find(item => String(item.leaveName || '').trim() === String(this.dataForm.leaveType || '').trim()) | |
| 455 | + if (matchedExtraLeave) return Number(matchedExtraLeave.extraDays || 0) | |
| 344 | 456 | return null |
| 345 | 457 | }, |
| 346 | 458 | canFillMaxDays() { |
| ... | ... | @@ -348,13 +460,13 @@ export default { |
| 348 | 460 | } |
| 349 | 461 | }, |
| 350 | 462 | created() { |
| 351 | - this.initializePage() | |
| 463 | + if (!this.flowBoxMode) this.initializePage() | |
| 352 | 464 | }, |
| 353 | 465 | watch: { |
| 354 | 466 | '$route.query': { |
| 355 | 467 | deep: true, |
| 356 | 468 | handler() { |
| 357 | - this.initializePage() | |
| 469 | + if (!this.flowBoxMode) this.initializePage() | |
| 358 | 470 | } |
| 359 | 471 | }, |
| 360 | 472 | 'dataForm.leaveType'(value) { |
| ... | ... | @@ -366,6 +478,57 @@ export default { |
| 366 | 478 | } |
| 367 | 479 | }, |
| 368 | 480 | methods: { |
| 481 | + init(data) { | |
| 482 | + this.flowBoxMode = true | |
| 483 | + this.setting = data || {} | |
| 484 | + this.dataForm = createDefaultForm() | |
| 485 | + this.dataForm.id = data.id || '' | |
| 486 | + this.dataForm.flowId = data.flowId || '' | |
| 487 | + this.dataForm.leaveType = this.currentSceneConfig.defaultLeaveType | |
| 488 | + this.$nextTick(() => { | |
| 489 | + if (this.$refs.dataForm) this.$refs.dataForm.resetFields() | |
| 490 | + this.fileList = [] | |
| 491 | + this.quotaSummary = null | |
| 492 | + this.fillApplicantInfo() | |
| 493 | + var done = () => { | |
| 494 | + this.loadQuotaSummary().then(() => { | |
| 495 | + this.$emit('setPageLoad') | |
| 496 | + }) | |
| 497 | + } | |
| 498 | + if (data.id) { | |
| 499 | + Info('leaveApply', data.id).then((res) => { | |
| 500 | + this.dataForm = Object.assign(createDefaultForm(), res.data || {}) | |
| 501 | + if (!this.dataForm.flowId && data.flowId) this.dataForm.flowId = data.flowId | |
| 502 | + if (this.dataForm.fileJson) { | |
| 503 | + try { this.fileList = JSON.parse(this.dataForm.fileJson) } catch (e) { this.fileList = [] } | |
| 504 | + } | |
| 505 | + done() | |
| 506 | + }).catch(() => { done() }) | |
| 507 | + } else { | |
| 508 | + BillNumber('WF_LeaveApplyNo').then((res) => { | |
| 509 | + this.dataForm.billNo = (res && res.data) || '' | |
| 510 | + done() | |
| 511 | + }).catch(() => { done() }) | |
| 512 | + } | |
| 513 | + }) | |
| 514 | + }, | |
| 515 | + dataFormSubmit(eventType) { | |
| 516 | + this.$refs.dataForm.validate((valid) => { | |
| 517 | + if (!valid) return | |
| 518 | + var error = this.validateBusinessRule() | |
| 519 | + if (error) { | |
| 520 | + this.$message.error(error) | |
| 521 | + return | |
| 522 | + } | |
| 523 | + var payload = Object.assign({}, this.dataForm, { | |
| 524 | + flowUrgent: Number(this.dataForm.flowUrgent || 1), | |
| 525 | + funeralRelationType: this.dataForm.leaveType === '丧假' ? Number(this.dataForm.funeralRelationType || 0) : null, | |
| 526 | + leaveReason: (this.dataForm.leaveReason || '').trim(), | |
| 527 | + fileJson: this.fileList && this.fileList.length ? JSON.stringify(this.fileList) : '' | |
| 528 | + }) | |
| 529 | + this.$emit('eventReciver', payload, eventType) | |
| 530 | + }) | |
| 531 | + }, | |
| 369 | 532 | async initializePage() { |
| 370 | 533 | const { id = '', flowId = '' } = this.$route.query || {} |
| 371 | 534 | const currentKey = `${this.scene}_${id}_${flowId}` |
| ... | ... | @@ -467,11 +630,14 @@ export default { |
| 467 | 630 | return value - Math.floor(value) |
| 468 | 631 | }, |
| 469 | 632 | validateRestScene(requestDays) { |
| 470 | - if (!this.restQuota.attendanceGroupBound) return '当前用户未绑定考勤分组,无法提交休假申请' | |
| 471 | - const remainDays = Number(this.restQuota.remainingRestDays || 0) | |
| 472 | - if (requestDays > remainDays) return `本月剩余可休 ${this.formatNumber(remainDays)} 天` | |
| 633 | + const rest = this.restQuota | |
| 634 | + if (!rest.attendanceGroupBound) return '当前用户未绑定考勤分组,无法提交休假申请' | |
| 635 | + const maxDays = rest.restUnlockCycle > 0 | |
| 636 | + ? Number(rest.availableRestDays || 0) | |
| 637 | + : Number(rest.remainingRestDays || 0) | |
| 638 | + if (requestDays > maxDays) return `当前可申请 ${this.formatNumber(maxDays)} 天` | |
| 473 | 639 | const splitDays = this.getHalfDaySplitDays(requestDays) |
| 474 | - const remainSplitDays = Number(this.restQuota.remainingHalfDaySplitDays || 0) | |
| 640 | + const remainSplitDays = Number(rest.remainingHalfDaySplitDays || 0) | |
| 475 | 641 | if (splitDays > remainSplitDays) return `本月可拆分半天额度剩余 ${this.formatNumber(remainSplitDays)} 天` |
| 476 | 642 | return '' |
| 477 | 643 | }, |
| ... | ... | @@ -497,6 +663,12 @@ export default { |
| 497 | 663 | if (maxDays <= 0) return '当前未匹配到产假规则,暂不能提交产假申请' |
| 498 | 664 | if (requestDays > maxDays) return `产假最多可请 ${this.formatNumber(maxDays)} 天` |
| 499 | 665 | } |
| 666 | + const matchedExtraLeave = this.extraLeaveList.find(item => String(item.leaveName || '').trim() === String(this.dataForm.leaveType || '').trim()) | |
| 667 | + if (matchedExtraLeave) { | |
| 668 | + const maxDays = Number(matchedExtraLeave.extraDays || 0) | |
| 669 | + if (maxDays <= 0) return `当前额外假期【${matchedExtraLeave.leaveName}】可用天数为 0` | |
| 670 | + if (requestDays > maxDays) return `额外假期【${matchedExtraLeave.leaveName}】最多可请 ${this.formatNumber(maxDays)} 天` | |
| 671 | + } | |
| 500 | 672 | return '' |
| 501 | 673 | }, |
| 502 | 674 | validateBusinessRule() { |
| ... | ... | @@ -520,7 +692,7 @@ export default { |
| 520 | 692 | if (type === 'submit') { |
| 521 | 693 | this.$confirm('确认提交当前申请吗?', '提示', { type: 'warning' }) |
| 522 | 694 | .then(() => this.handleRequest(type)) |
| 523 | - .catch(() => {}) | |
| 695 | + .catch(() => { }) | |
| 524 | 696 | return |
| 525 | 697 | } |
| 526 | 698 | this.handleRequest(type) | ... | ... |
antis-ncc-admin/src/views/workFlow/leave-apply-pages/paid-leave-apply/index.vue
| 1 | 1 | <template> |
| 2 | - <leave-apply-page scene="paid" /> | |
| 2 | + <leave-apply-page ref="leaveForm" scene="paid" v-on="$listeners" /> | |
| 3 | 3 | </template> |
| 4 | 4 | |
| 5 | 5 | <script> |
| ... | ... | @@ -7,6 +7,14 @@ import LeaveApplyPage from '../components/leave-apply-page' |
| 7 | 7 | |
| 8 | 8 | export default { |
| 9 | 9 | name: 'PaidLeaveApplyPage', |
| 10 | - components: { LeaveApplyPage } | |
| 10 | + components: { LeaveApplyPage }, | |
| 11 | + methods: { | |
| 12 | + init(data) { | |
| 13 | + this.$refs.leaveForm.init(data) | |
| 14 | + }, | |
| 15 | + dataFormSubmit(eventType) { | |
| 16 | + this.$refs.leaveForm.dataFormSubmit(eventType) | |
| 17 | + } | |
| 18 | + } | |
| 11 | 19 | } |
| 12 | 20 | </script> | ... | ... |
antis-ncc-admin/src/views/workFlow/leave-apply-pages/personal-leave-apply/index.vue
| 1 | 1 | <template> |
| 2 | - <leave-apply-page scene="personal" /> | |
| 2 | + <leave-apply-page ref="leaveForm" scene="personal" v-on="$listeners" /> | |
| 3 | 3 | </template> |
| 4 | 4 | |
| 5 | 5 | <script> |
| ... | ... | @@ -7,6 +7,14 @@ import LeaveApplyPage from '../components/leave-apply-page' |
| 7 | 7 | |
| 8 | 8 | export default { |
| 9 | 9 | name: 'PersonalLeaveApplyPage', |
| 10 | - components: { LeaveApplyPage } | |
| 10 | + components: { LeaveApplyPage }, | |
| 11 | + methods: { | |
| 12 | + init(data) { | |
| 13 | + this.$refs.leaveForm.init(data) | |
| 14 | + }, | |
| 15 | + dataFormSubmit(eventType) { | |
| 16 | + this.$refs.leaveForm.dataFormSubmit(eventType) | |
| 17 | + } | |
| 18 | + } | |
| 11 | 19 | } |
| 12 | 20 | </script> | ... | ... |
antis-ncc-admin/src/views/workFlow/leave-apply-pages/rest-leave-apply/index.vue
| 1 | 1 | <template> |
| 2 | - <leave-apply-page scene="rest" /> | |
| 2 | + <leave-apply-page ref="leaveForm" scene="rest" v-on="$listeners" /> | |
| 3 | 3 | </template> |
| 4 | 4 | |
| 5 | 5 | <script> |
| ... | ... | @@ -7,6 +7,14 @@ import LeaveApplyPage from '../components/leave-apply-page' |
| 7 | 7 | |
| 8 | 8 | export default { |
| 9 | 9 | name: 'RestLeaveApplyPage', |
| 10 | - components: { LeaveApplyPage } | |
| 10 | + components: { LeaveApplyPage }, | |
| 11 | + methods: { | |
| 12 | + init(data) { | |
| 13 | + this.$refs.leaveForm.init(data) | |
| 14 | + }, | |
| 15 | + dataFormSubmit(eventType) { | |
| 16 | + this.$refs.leaveForm.dataFormSubmit(eventType) | |
| 17 | + } | |
| 18 | + } | |
| 11 | 19 | } |
| 12 | 20 | </script> | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqAttendanceSetting/AttendanceRuleModels.cs
| ... | ... | @@ -73,6 +73,21 @@ namespace NCC.Extend.Entitys.Dto.LqAttendanceSetting |
| 73 | 73 | /// </summary> |
| 74 | 74 | public string remark { get; set; } |
| 75 | 75 | |
| 76 | + /// <summary> | |
| 77 | + /// 应休解锁周期:每上满N天解锁1天应休(0=不启用) | |
| 78 | + /// </summary> | |
| 79 | + public int restUnlockCycle { get; set; } | |
| 80 | + | |
| 81 | + /// <summary> | |
| 82 | + /// 迟到容忍分钟数:null=迟到不计为有效上班;N=迟到≤N分钟才计为有效上班 | |
| 83 | + /// </summary> | |
| 84 | + public int? lateToleranceMinutes { get; set; } | |
| 85 | + | |
| 86 | + /// <summary> | |
| 87 | + /// 早退容忍分钟数:null=早退不计为有效上班;N=早退≤N分钟才计为有效上班 | |
| 88 | + /// </summary> | |
| 89 | + public int? earlyLeaveToleranceMinutes { get; set; } | |
| 90 | + | |
| 76 | 91 | } |
| 77 | 92 | |
| 78 | 93 | /// <summary> | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_attendance_group/LqAttendanceGroupEntity.cs
| ... | ... | @@ -106,5 +106,23 @@ namespace NCC.Extend.Entitys.lq_attendance_group |
| 106 | 106 | /// </summary> |
| 107 | 107 | [SugarColumn(ColumnName = "F_CreateUserId")] |
| 108 | 108 | public string CreateUserId { get; set; } |
| 109 | + | |
| 110 | + /// <summary> | |
| 111 | + /// 应休解锁周期:每上满N天解锁1天应休(0=不启用) | |
| 112 | + /// </summary> | |
| 113 | + [SugarColumn(ColumnName = "F_RestUnlockCycle")] | |
| 114 | + public int RestUnlockCycle { get; set; } | |
| 115 | + | |
| 116 | + /// <summary> | |
| 117 | + /// 迟到容忍分钟数:null=迟到不计为有效上班;N=迟到≤N分钟才计为有效上班 | |
| 118 | + /// </summary> | |
| 119 | + [SugarColumn(ColumnName = "F_LateToleranceMinutes")] | |
| 120 | + public int? LateToleranceMinutes { get; set; } | |
| 121 | + | |
| 122 | + /// <summary> | |
| 123 | + /// 早退容忍分钟数:null=早退不计为有效上班;N=早退≤N分钟才计为有效上班 | |
| 124 | + /// </summary> | |
| 125 | + [SugarColumn(ColumnName = "F_EarlyLeaveToleranceMinutes")] | |
| 126 | + public int? EarlyLeaveToleranceMinutes { get; set; } | |
| 109 | 127 | } |
| 110 | 128 | } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqAttendanceSettingService.cs
| ... | ... | @@ -666,6 +666,9 @@ namespace NCC.Extend.LqAttendanceSetting |
| 666 | 666 | HalfDaySplitRestDays = model.halfDaySplitRestDays, |
| 667 | 667 | IsEnabled = model.isEnabled, |
| 668 | 668 | Remark = model.remark, |
| 669 | + RestUnlockCycle = model.restUnlockCycle, | |
| 670 | + LateToleranceMinutes = model.lateToleranceMinutes, | |
| 671 | + EarlyLeaveToleranceMinutes = model.earlyLeaveToleranceMinutes, | |
| 669 | 672 | SortCode = await GetNextSortCodeAsync(GetGroupQuery(), x => (int?)x.SortCode), |
| 670 | 673 | CreateTime = now, |
| 671 | 674 | CreateUserId = currentUser.UserId, |
| ... | ... | @@ -694,7 +697,10 @@ namespace NCC.Extend.LqAttendanceSetting |
| 694 | 697 | monthlyRestDays = model.monthlyRestDays, |
| 695 | 698 | halfDaySplitRestDays = model.halfDaySplitRestDays, |
| 696 | 699 | isEnabled = model.isEnabled, |
| 697 | - remark = model.remark | |
| 700 | + remark = model.remark, | |
| 701 | + restUnlockCycle = model.restUnlockCycle, | |
| 702 | + lateToleranceMinutes = model.lateToleranceMinutes, | |
| 703 | + earlyLeaveToleranceMinutes = model.earlyLeaveToleranceMinutes | |
| 698 | 704 | }; |
| 699 | 705 | if (IsSnapshotEqual(before, after)) |
| 700 | 706 | { |
| ... | ... | @@ -708,6 +714,9 @@ namespace NCC.Extend.LqAttendanceSetting |
| 708 | 714 | dbEntity.HalfDaySplitRestDays = model.halfDaySplitRestDays; |
| 709 | 715 | dbEntity.IsEnabled = model.isEnabled; |
| 710 | 716 | dbEntity.Remark = model.remark; |
| 717 | + dbEntity.RestUnlockCycle = model.restUnlockCycle; | |
| 718 | + dbEntity.LateToleranceMinutes = model.lateToleranceMinutes; | |
| 719 | + dbEntity.EarlyLeaveToleranceMinutes = model.earlyLeaveToleranceMinutes; | |
| 711 | 720 | ApplyModifyMeta(dbEntity, currentUser, now); |
| 712 | 721 | await _db.Updateable(dbEntity).ExecuteCommandAsync(); |
| 713 | 722 | after = MapGroupEntity(dbEntity); |
| ... | ... | @@ -1578,7 +1587,10 @@ namespace NCC.Extend.LqAttendanceSetting |
| 1578 | 1587 | monthlyRestDays = entity.MonthlyRestDays, |
| 1579 | 1588 | halfDaySplitRestDays = entity.HalfDaySplitRestDays, |
| 1580 | 1589 | isEnabled = entity.IsEnabled, |
| 1581 | - remark = entity.Remark | |
| 1590 | + remark = entity.Remark, | |
| 1591 | + restUnlockCycle = entity.RestUnlockCycle, | |
| 1592 | + lateToleranceMinutes = entity.LateToleranceMinutes, | |
| 1593 | + earlyLeaveToleranceMinutes = entity.EarlyLeaveToleranceMinutes | |
| 1582 | 1594 | }; |
| 1583 | 1595 | |
| 1584 | 1596 | private static AttendanceExemptUserModel MapExemptEntity(LqAttendanceExemptUserEntity entity) => new AttendanceExemptUserModel | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs
| ... | ... | @@ -746,15 +746,13 @@ namespace NCC.Extend.LqKdKdjlb |
| 746 | 746 | jj = it.Jj, |
| 747 | 747 | bz = it.Bz, |
| 748 | 748 | kdhy = it.Kdhy, |
| 749 | - kdhyc = SqlFunc.Subqueryable<LqKhxxEntity>().Where(x => x.Id == it.Kdhy).Select(x => x.Khmc), | |
| 750 | - kdhysjh = SqlFunc.Subqueryable<LqKhxxEntity>().Where(x => x.Id == it.Kdhy).Select(x => x.Sjh), | |
| 749 | + // 性能优化:kdhyc/kdhysjh/createUserName/activityName/appointmentTime | |
| 750 | + // 不在 SELECT 里赋值(否则经 MergeTable 后 NULL 被推断为 varchar,导致 DateTime 转换异常) | |
| 751 | + // 分页后仅对当页记录批量回填,查询数从 O(N×5) 降到 O(1) | |
| 751 | 752 | isEffective = it.IsEffective, |
| 752 | 753 | createUser = it.CreateUser, |
| 753 | - createUserName = SqlFunc.Subqueryable<UserEntity>().Where(x => x.Id == it.CreateUser).Select(x => x.RealName), | |
| 754 | 754 | activityId = it.ActivityId, |
| 755 | - activityName = SqlFunc.Subqueryable<LqPackageInfoEntity>().Where(x => x.Id == it.ActivityId).Select(x => x.ActivityName), | |
| 756 | 755 | appointmentId = it.AppointmentId, |
| 757 | - appointmentTime = it.AppointmentId == null ? (DateTime?)null : SqlFunc.Subqueryable<LqYyjlEntity>().Where(x => x.Id == it.AppointmentId).Select(x => SqlFunc.ToDate(x.Yysj)), | |
| 758 | 756 | upgradeMedicalBeauty = it.UpgradeMedicalBeauty, |
| 759 | 757 | upgradeTechBeauty = it.UpgradeTechBeauty, |
| 760 | 758 | upgradeLifeBeauty = it.UpgradeLifeBeauty, |
| ... | ... | @@ -765,6 +763,74 @@ namespace NCC.Extend.LqKdKdjlb |
| 765 | 763 | |
| 766 | 764 | // 获取当前页的开单记录ID列表 |
| 767 | 765 | var billingIds = data.list.Select(x => x.id).ToList(); |
| 766 | + | |
| 767 | + // 批量回填会员信息(kdhyc / kdhysjh) | |
| 768 | + var kdhyIds = data.list.Where(x => !string.IsNullOrEmpty(x.kdhy)).Select(x => x.kdhy).Distinct().ToList(); | |
| 769 | + if (kdhyIds.Any()) | |
| 770 | + { | |
| 771 | + var khxxMap = await _db.Queryable<LqKhxxEntity>() | |
| 772 | + .Where(x => kdhyIds.Contains(x.Id)) | |
| 773 | + .Select(x => new { x.Id, x.Khmc, x.Sjh }) | |
| 774 | + .ToListAsync(); | |
| 775 | + var khxxDict = khxxMap.ToDictionary(x => x.Id); | |
| 776 | + foreach (var item in data.list) | |
| 777 | + { | |
| 778 | + if (!string.IsNullOrEmpty(item.kdhy) && khxxDict.TryGetValue(item.kdhy, out var kh)) | |
| 779 | + { | |
| 780 | + item.kdhyc = kh.Khmc; | |
| 781 | + item.kdhysjh = kh.Sjh; | |
| 782 | + } | |
| 783 | + } | |
| 784 | + } | |
| 785 | + | |
| 786 | + // 批量回填创建人姓名 | |
| 787 | + var createUserIds = data.list.Where(x => !string.IsNullOrEmpty(x.createUser)).Select(x => x.createUser).Distinct().ToList(); | |
| 788 | + if (createUserIds.Any()) | |
| 789 | + { | |
| 790 | + var userMap = await _db.Queryable<UserEntity>() | |
| 791 | + .Where(x => createUserIds.Contains(x.Id)) | |
| 792 | + .Select(x => new { x.Id, x.RealName }) | |
| 793 | + .ToListAsync(); | |
| 794 | + var userDict = userMap.ToDictionary(x => x.Id); | |
| 795 | + foreach (var item in data.list) | |
| 796 | + { | |
| 797 | + if (!string.IsNullOrEmpty(item.createUser) && userDict.TryGetValue(item.createUser, out var u)) | |
| 798 | + item.createUserName = u.RealName; | |
| 799 | + } | |
| 800 | + } | |
| 801 | + | |
| 802 | + // 批量回填活动名称 | |
| 803 | + var activityIds = data.list.Where(x => !string.IsNullOrEmpty(x.activityId)).Select(x => x.activityId).Distinct().ToList(); | |
| 804 | + if (activityIds.Any()) | |
| 805 | + { | |
| 806 | + var actMap = await _db.Queryable<LqPackageInfoEntity>() | |
| 807 | + .Where(x => activityIds.Contains(x.Id)) | |
| 808 | + .Select(x => new { x.Id, x.ActivityName }) | |
| 809 | + .ToListAsync(); | |
| 810 | + var actDict = actMap.ToDictionary(x => x.Id); | |
| 811 | + foreach (var item in data.list) | |
| 812 | + { | |
| 813 | + if (!string.IsNullOrEmpty(item.activityId) && actDict.TryGetValue(item.activityId, out var act)) | |
| 814 | + item.activityName = act.ActivityName; | |
| 815 | + } | |
| 816 | + } | |
| 817 | + | |
| 818 | + // 批量回填预约时间 | |
| 819 | + var apptIds = data.list.Where(x => !string.IsNullOrEmpty(x.appointmentId)).Select(x => x.appointmentId).Distinct().ToList(); | |
| 820 | + if (apptIds.Any()) | |
| 821 | + { | |
| 822 | + var apptMap = await _db.Queryable<LqYyjlEntity>() | |
| 823 | + .Where(x => apptIds.Contains(x.Id)) | |
| 824 | + .Select(x => new { x.Id, x.Yysj }) | |
| 825 | + .ToListAsync(); | |
| 826 | + var apptDict = apptMap.ToDictionary(x => x.Id); | |
| 827 | + foreach (var item in data.list) | |
| 828 | + { | |
| 829 | + if (!string.IsNullOrEmpty(item.appointmentId) && apptDict.TryGetValue(item.appointmentId, out var appt)) | |
| 830 | + item.appointmentTime = appt.Yysj; | |
| 831 | + } | |
| 832 | + } | |
| 833 | + | |
| 768 | 834 | // 批量查询品项明细 |
| 769 | 835 | var itemDetails = new List<LqKdPxmxInfoOutput>(); |
| 770 | 836 | if (billingIds.Any()) |
| ... | ... | @@ -5094,4 +5160,4 @@ namespace NCC.Extend.LqKdKdjlb |
| 5094 | 5160 | } |
| 5095 | 5161 | #endregion |
| 5096 | 5162 | } |
| 5097 | 5163 | -} |
| 5164 | +} | |
| 5098 | 5165 | \ No newline at end of file | ... | ... |
netcore/src/Modularity/WorkFlow/NCC.WorkFlow/WorkFlowForm/LeaveApplyService.cs
| ... | ... | @@ -7,6 +7,8 @@ using NCC.Extend.Entitys.lq_attendance_annual_leave_rule; |
| 7 | 7 | using NCC.Extend.Entitys.lq_attendance_extra_leave; |
| 8 | 8 | using NCC.Extend.Entitys.lq_attendance_funeral_leave_rule; |
| 9 | 9 | using NCC.Extend.Entitys.lq_attendance_group; |
| 10 | +using NCC.Extend.Entitys.lq_attendance_record; | |
| 11 | +using NCC.Extend.Entitys.Enum; | |
| 10 | 12 | using NCC.Extend.Entitys.lq_attendance_marriage_leave_rule; |
| 11 | 13 | using NCC.Extend.Entitys.lq_attendance_maternity_leave_rule; |
| 12 | 14 | using NCC.FriendlyException; |
| ... | ... | @@ -156,6 +158,22 @@ namespace NCC.WorkFlow.WorkFlowForm |
| 156 | 158 | .Sum(); |
| 157 | 159 | var annualTotalDays = matchedAnnualRule?.LeaveDays ?? 0; |
| 158 | 160 | |
| 161 | + // 应休解锁规则计算 | |
| 162 | + var restUnlockCycle = group?.RestUnlockCycle ?? 0; | |
| 163 | + int workedDaysThisMonth = 0; | |
| 164 | + int unlockedRestDays = (int)monthlyRestDays; // 默认不启用时全额可用 | |
| 165 | + int availableRestDays = (int)Math.Max(0m, monthlyRestDays - usedRestDays); | |
| 166 | + | |
| 167 | + if (restUnlockCycle > 0 && group != null) | |
| 168 | + { | |
| 169 | + workedDaysThisMonth = await CountWorkedDaysThisMonthAsync(user.Id, monthStart, today, group); | |
| 170 | + unlockedRestDays = Math.Min( | |
| 171 | + (int)monthlyRestDays, | |
| 172 | + workedDaysThisMonth / restUnlockCycle | |
| 173 | + ); | |
| 174 | + availableRestDays = Math.Max(0, unlockedRestDays - (int)usedRestDays); | |
| 175 | + } | |
| 176 | + | |
| 159 | 177 | return new |
| 160 | 178 | { |
| 161 | 179 | serverDate = today.ToString("yyyy-MM-dd"), |
| ... | ... | @@ -185,7 +203,11 @@ namespace NCC.WorkFlow.WorkFlowForm |
| 185 | 203 | halfDaySplitRestDays = splitRestDays, |
| 186 | 204 | usedHalfDaySplitDays, |
| 187 | 205 | remainingHalfDaySplitDays = Math.Max(0m, splitRestDays - usedHalfDaySplitDays), |
| 188 | - remainingHalfDaySelections = (int)(Math.Max(0m, splitRestDays - usedHalfDaySplitDays) * 2) | |
| 206 | + remainingHalfDaySelections = (int)(Math.Max(0m, splitRestDays - usedHalfDaySplitDays) * 2), | |
| 207 | + workedDaysThisMonth, | |
| 208 | + restUnlockCycle, | |
| 209 | + unlockedRestDays, | |
| 210 | + availableRestDays | |
| 189 | 211 | }, |
| 190 | 212 | paid = new |
| 191 | 213 | { |
| ... | ... | @@ -420,6 +442,7 @@ namespace NCC.WorkFlow.WorkFlowForm |
| 420 | 442 | .FirstAsync(x => x.Id == user.AttendanceGroupId && SqlFunc.IsNull(x.DeleteMark, 0) == 0); |
| 421 | 443 | _ = group ?? throw NCCException.Oh("当前用户的考勤分组不存在,无法校验休假额度"); |
| 422 | 444 | |
| 445 | + var today = DateTime.Today; | |
| 423 | 446 | var monthStart = new DateTime(entity.LeaveStartTime.Value.Year, entity.LeaveStartTime.Value.Month, 1); |
| 424 | 447 | var monthEnd = monthStart.AddMonths(1); |
| 425 | 448 | var existingDayTexts = await _sqlSugarRepository.Context.Queryable<LeaveApplyEntity, FlowTaskEntity>((la, task) => new JoinQueryInfos( |
| ... | ... | @@ -451,6 +474,24 @@ namespace NCC.WorkFlow.WorkFlowForm |
| 451 | 474 | var remainHalfDaySplitDays = Math.Max(0m, group.HalfDaySplitRestDays - usedHalfDaySplitDays); |
| 452 | 475 | throw NCCException.Oh($"本月可拆分半天休假天数不足,当前分组允许拆分 {FormatDays(group.HalfDaySplitRestDays)} 天,已占用 {FormatDays(usedHalfDaySplitDays)} 天,剩余 {FormatDays(remainHalfDaySplitDays)} 天"); |
| 453 | 476 | } |
| 477 | + | |
| 478 | + // 解锁规则校验:启用后只能申请已解锁天数内的应休 | |
| 479 | + if (group.RestUnlockCycle > 0) | |
| 480 | + { | |
| 481 | + var workedDays = await CountWorkedDaysThisMonthAsync(user.Id, monthStart, today, group); | |
| 482 | + var unlocked = Math.Min(group.MonthlyRestDays, workedDays / group.RestUnlockCycle); | |
| 483 | + var available = Math.Max(0, unlocked - (int)usedDays); | |
| 484 | + if (requestDays > available) | |
| 485 | + { | |
| 486 | + var nextUnlockNeed = group.RestUnlockCycle - (workedDays % group.RestUnlockCycle); | |
| 487 | + var msg = $"本月已上班 {workedDays} 天,已解锁 {unlocked} 天应休,当前可申请 {available} 天。"; | |
| 488 | + if (nextUnlockNeed > 0 && unlocked < group.MonthlyRestDays) | |
| 489 | + { | |
| 490 | + msg += $"再上满 {nextUnlockNeed} 天可再解锁 1 天。"; | |
| 491 | + } | |
| 492 | + throw NCCException.Oh(msg); | |
| 493 | + } | |
| 494 | + } | |
| 454 | 495 | } |
| 455 | 496 | |
| 456 | 497 | private async Task ValidatePaidLeaveAsync(string currentId, LeaveApplyEntity entity) |
| ... | ... | @@ -733,6 +774,62 @@ LIMIT 1"; |
| 733 | 774 | { |
| 734 | 775 | return days % 1 == 0 ? days.ToString("0") : days.ToString("0.##"); |
| 735 | 776 | } |
| 777 | + | |
| 778 | + /// <summary> | |
| 779 | + /// 统计本月有效上班天数(用于应休解锁计算)。 | |
| 780 | + /// 旷工、休息、请假、病假、缺卡始终排除。 | |
| 781 | + /// 迟到/早退是否计入由考勤分组配置决定。 | |
| 782 | + /// </summary> | |
| 783 | + private async Task<int> CountWorkedDaysThisMonthAsync( | |
| 784 | + string userId, DateTime monthStart, DateTime today, | |
| 785 | + LqAttendanceGroupEntity group) | |
| 786 | + { | |
| 787 | + var records = await _sqlSugarRepository.Context | |
| 788 | + .Queryable<LqAttendanceRecordEntity>() | |
| 789 | + .Where(x => x.UserId == userId | |
| 790 | + && x.AttendanceDate >= monthStart | |
| 791 | + && x.AttendanceDate <= today | |
| 792 | + && x.IsEffective == 1 | |
| 793 | + && x.Status != (int)AttendanceRecordStatusEnum.旷工 | |
| 794 | + && x.Status != (int)AttendanceRecordStatusEnum.休息 | |
| 795 | + && x.Status != (int)AttendanceRecordStatusEnum.请假 | |
| 796 | + && x.Status != (int)AttendanceRecordStatusEnum.病假 | |
| 797 | + && x.Status != (int)AttendanceRecordStatusEnum.缺卡) | |
| 798 | + .Select(x => new | |
| 799 | + { | |
| 800 | + x.AttendanceDate, | |
| 801 | + x.Status, | |
| 802 | + x.LateMinutes, | |
| 803 | + x.EarlyLeaveMinutes | |
| 804 | + }) | |
| 805 | + .ToListAsync(); | |
| 806 | + | |
| 807 | + int count = 0; | |
| 808 | + var processedDates = new HashSet<DateTime>(); | |
| 809 | + foreach (var r in records) | |
| 810 | + { | |
| 811 | + var date = r.AttendanceDate.Date; | |
| 812 | + if (processedDates.Contains(date)) continue; | |
| 813 | + | |
| 814 | + // 迟到判断:未配置容忍时间则迟到不计为有效上班 | |
| 815 | + if (r.Status == (int)AttendanceRecordStatusEnum.迟到) | |
| 816 | + { | |
| 817 | + if (group?.LateToleranceMinutes == null) continue; | |
| 818 | + if (r.LateMinutes > group.LateToleranceMinutes) continue; | |
| 819 | + } | |
| 820 | + | |
| 821 | + // 早退判断:未配置容忍时间则早退不计为有效上班 | |
| 822 | + if (r.EarlyLeaveMinutes > 0) | |
| 823 | + { | |
| 824 | + if (group?.EarlyLeaveToleranceMinutes == null) continue; | |
| 825 | + if (r.EarlyLeaveMinutes > group.EarlyLeaveToleranceMinutes) continue; | |
| 826 | + } | |
| 827 | + | |
| 828 | + processedDates.Add(date); | |
| 829 | + count++; | |
| 830 | + } | |
| 831 | + return count; | |
| 832 | + } | |
| 736 | 833 | #endregion |
| 737 | 834 | |
| 738 | 835 | #region PublicMethod | ... | ... |
绿纤uni-app/pagesA/components/leave-apply-scene.vue
| ... | ... | @@ -271,12 +271,25 @@ export default { |
| 271 | 271 | }, |
| 272 | 272 | summaryCards() { |
| 273 | 273 | if (this.isRestScene) { |
| 274 | - return [ | |
| 275 | - { key: 'total', label: '本月应休', value: this.formatNumber(this.restQuota.monthlyRestDays || 0), desc: '天' }, | |
| 276 | - { key: 'used', label: '已申请', value: this.formatNumber(this.restQuota.usedRestDays || 0), desc: '天' }, | |
| 277 | - { key: 'remain', label: '剩余可休', value: this.formatNumber(this.restQuota.remainingRestDays || 0), desc: '天', active: true }, | |
| 278 | - { key: 'split', label: '半天额度', value: this.formatNumber(this.restQuota.remainingHalfDaySplitDays || 0), desc: `还能拆 ${this.restQuota.remainingHalfDaySelections || 0} 次半天` } | |
| 274 | + const rest = this.restQuota | |
| 275 | + const unlockEnabled = rest.restUnlockCycle > 0 | |
| 276 | + const remainValue = unlockEnabled ? (rest.availableRestDays || 0) : (rest.remainingRestDays || 0) | |
| 277 | + const remainDesc = unlockEnabled ? `已解锁 ${rest.unlockedRestDays || 0} 天` : '天' | |
| 278 | + const cards = [ | |
| 279 | + { key: 'total', label: '本月应休', value: this.formatNumber(rest.monthlyRestDays || 0), desc: '天' }, | |
| 280 | + { key: 'used', label: '已申请', value: this.formatNumber(rest.usedRestDays || 0), desc: '天' }, | |
| 281 | + { key: 'remain', label: '剩余可休', value: this.formatNumber(remainValue), desc: remainDesc, active: true }, | |
| 282 | + { key: 'split', label: '半天额度', value: this.formatNumber(rest.remainingHalfDaySplitDays || 0), desc: `还能拆 ${rest.remainingHalfDaySelections || 0} 次半天` } | |
| 279 | 283 | ] |
| 284 | + if (unlockEnabled) { | |
| 285 | + cards.push({ | |
| 286 | + key: 'worked', | |
| 287 | + label: '本月已上班', | |
| 288 | + value: `${rest.workedDaysThisMonth || 0} 天`, | |
| 289 | + desc: `每${rest.restUnlockCycle}天解锁1天` | |
| 290 | + }) | |
| 291 | + } | |
| 292 | + return cards | |
| 280 | 293 | } |
| 281 | 294 | const extraTotalDays = this.extraLeaveList.reduce((sum, item) => sum + Number(item.extraDays || 0), 0) |
| 282 | 295 | const sel = String(this.formData.leaveType || '').trim() |
| ... | ... | @@ -293,10 +306,25 @@ export default { |
| 293 | 306 | }, |
| 294 | 307 | leaveTypeSummaryText() { |
| 295 | 308 | if (this.isRestScene) { |
| 296 | - if (!this.restQuota.attendanceGroupBound) { | |
| 309 | + const rest = this.restQuota | |
| 310 | + if (!rest.attendanceGroupBound) { | |
| 297 | 311 | return '当前用户还未绑定考勤分组,暂时无法计算本月应休额度。' |
| 298 | 312 | } |
| 299 | - return `当前分组【${this.restQuota.attendanceGroupName || '未命名分组'}】本月还可休 ${this.formatNumber(this.restQuota.remainingRestDays || 0)} 天;其中还能拆分半天 ${this.formatNumber(this.restQuota.remainingHalfDaySplitDays || 0)} 天。` | |
| 313 | + if (rest.restUnlockCycle > 0) { | |
| 314 | + const worked = rest.workedDaysThisMonth || 0 | |
| 315 | + const cycle = rest.restUnlockCycle | |
| 316 | + const unlocked = rest.unlockedRestDays || 0 | |
| 317 | + const available = rest.availableRestDays || 0 | |
| 318 | + const daysInCurrentCycle = worked % cycle | |
| 319 | + const nextNeed = daysInCurrentCycle === 0 ? cycle : cycle - daysInCurrentCycle | |
| 320 | + const canUnlockMore = unlocked < (rest.monthlyRestDays || 0) | |
| 321 | + let text = `本月已上班 ${worked} 天,已解锁 ${unlocked} 天应休,当前可申请 ${available} 天。` | |
| 322 | + if (canUnlockMore && nextNeed > 0) { | |
| 323 | + text += `再上满 ${nextNeed} 天可再解锁 1 天。` | |
| 324 | + } | |
| 325 | + return text | |
| 326 | + } | |
| 327 | + return `当前分组【${rest.attendanceGroupName || '未命名分组'}】本月还可休 ${this.formatNumber(rest.remainingRestDays || 0)} 天;其中还能拆分半天 ${this.formatNumber(rest.remainingHalfDaySplitDays || 0)} 天。` | |
| 300 | 328 | } |
| 301 | 329 | if (this.formData.leaveType === '婚假') { |
| 302 | 330 | const maxDays = Number((this.paidQuota.marriage || {}).maxDays || 0) |
| ... | ... | @@ -325,7 +353,10 @@ export default { |
| 325 | 353 | }, |
| 326 | 354 | selectedLeaveMaxDays() { |
| 327 | 355 | if (this.isRestScene) { |
| 328 | - return Number(this.restQuota.remainingRestDays || 0) | |
| 356 | + const rest = this.restQuota | |
| 357 | + return rest.restUnlockCycle > 0 | |
| 358 | + ? Number(rest.availableRestDays || 0) | |
| 359 | + : Number(rest.remainingRestDays || 0) | |
| 329 | 360 | } |
| 330 | 361 | if (!this.isPaidScene) { |
| 331 | 362 | return null |
| ... | ... | @@ -565,11 +596,14 @@ export default { |
| 565 | 596 | if (!this.formData.leaveHour) return '请假小时计算失败,请重新填写' |
| 566 | 597 | |
| 567 | 598 | if (this.isRestScene) { |
| 568 | - if (!this.restQuota.attendanceGroupBound) return '当前用户未绑定考勤分组,无法提交应休申请' | |
| 569 | - const remainDays = Number(this.restQuota.remainingRestDays || 0) | |
| 570 | - if (requestDays > remainDays) return `本月剩余可休 ${this.formatNumber(remainDays)} 天` | |
| 599 | + const rest = this.restQuota | |
| 600 | + if (!rest.attendanceGroupBound) return '当前用户未绑定考勤分组,无法提交应休申请' | |
| 601 | + const maxDays = rest.restUnlockCycle > 0 | |
| 602 | + ? Number(rest.availableRestDays || 0) | |
| 603 | + : Number(rest.remainingRestDays || 0) | |
| 604 | + if (requestDays > maxDays) return `当前可申请 ${this.formatNumber(maxDays)} 天` | |
| 571 | 605 | const splitDays = this.getHalfDaySplitDays(requestDays) |
| 572 | - const remainSplitDays = Number(this.restQuota.remainingHalfDaySplitDays || 0) | |
| 606 | + const remainSplitDays = Number(rest.remainingHalfDaySplitDays || 0) | |
| 573 | 607 | if (splitDays > remainSplitDays) { |
| 574 | 608 | return `本月可拆分半天额度剩余 ${this.formatNumber(remainSplitDays)} 天` |
| 575 | 609 | } | ... | ... |
绿纤uni-app/service/quota-summary.js
| ... | ... | @@ -91,9 +91,16 @@ export function normalizePaidQuota(quotaSummary) { |
| 91 | 91 | } |
| 92 | 92 | } |
| 93 | 93 | |
| 94 | -/** rest 段 PascalCase 兼容 */ | |
| 94 | +/** rest 段 PascalCase 兼容,新增解锁规则相关字段 */ | |
| 95 | 95 | export function normalizeRestQuota(quotaSummary) { |
| 96 | 96 | const q = quotaSummary || {} |
| 97 | 97 | const r = q.rest || q.Rest |
| 98 | - return r && typeof r === 'object' ? r : {} | |
| 98 | + if (!r || typeof r !== 'object') return {} | |
| 99 | + return { | |
| 100 | + ...r, | |
| 101 | + workedDaysThisMonth: r.workedDaysThisMonth ?? r.WorkedDaysThisMonth ?? 0, | |
| 102 | + restUnlockCycle: r.restUnlockCycle ?? r.RestUnlockCycle ?? 0, | |
| 103 | + unlockedRestDays: r.unlockedRestDays ?? r.UnlockedRestDays ?? 0, | |
| 104 | + availableRestDays: r.availableRestDays ?? r.AvailableRestDays ?? 0 | |
| 105 | + } | |
| 99 | 106 | } | ... | ... |
绿纤uni-app/unpackage/dist/dev/mp-weixin/common/vendor.js
| ... | ... | @@ -840,9 +840,9 @@ function populateParameters(result) { |
| 840 | 840 | appVersion: "1.0.0", |
| 841 | 841 | appVersionCode: "100", |
| 842 | 842 | appLanguage: getAppLanguage(hostLanguage), |
| 843 | - uniCompileVersion: "5.05", | |
| 844 | - uniCompilerVersion: "5.05", | |
| 845 | - uniRuntimeVersion: "5.05", | |
| 843 | + uniCompileVersion: "5.06", | |
| 844 | + uniCompilerVersion: "5.06", | |
| 845 | + uniRuntimeVersion: "5.06", | |
| 846 | 846 | uniPlatform: undefined || "mp-weixin", |
| 847 | 847 | deviceBrand: deviceBrand, |
| 848 | 848 | deviceModel: model, |
| ... | ... | @@ -948,9 +948,9 @@ var getAppBaseInfo = { |
| 948 | 948 | hostTheme: theme, |
| 949 | 949 | isUniAppX: false, |
| 950 | 950 | uniPlatform: undefined || "mp-weixin", |
| 951 | - uniCompileVersion: "5.05", | |
| 952 | - uniCompilerVersion: "5.05", | |
| 953 | - uniRuntimeVersion: "5.05" | |
| 951 | + uniCompileVersion: "5.06", | |
| 952 | + uniCompilerVersion: "5.06", | |
| 953 | + uniRuntimeVersion: "5.06" | |
| 954 | 954 | })); |
| 955 | 955 | } |
| 956 | 956 | }; |
| ... | ... | @@ -40186,7 +40186,10 @@ Object.defineProperty(exports, "__esModule", { |
| 40186 | 40186 | exports.extractQuotaSummaryPayload = extractQuotaSummaryPayload; |
| 40187 | 40187 | exports.normalizePaidQuota = normalizePaidQuota; |
| 40188 | 40188 | exports.normalizeRestQuota = normalizeRestQuota; |
| 40189 | +var _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ 11)); | |
| 40189 | 40190 | var _typeof2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/typeof */ 13)); |
| 40191 | +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } | |
| 40192 | +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } | |
| 40190 | 40193 | /** |
| 40191 | 40194 | * 请假额度 QuotaSummary 接口解析(兼容 RESTful 包装、PascalCase、字符串 JSON) |
| 40192 | 40195 | */ |
| ... | ... | @@ -40289,11 +40292,18 @@ function normalizePaidQuota(quotaSummary) { |
| 40289 | 40292 | }; |
| 40290 | 40293 | } |
| 40291 | 40294 | |
| 40292 | -/** rest 段 PascalCase 兼容 */ | |
| 40295 | +/** rest 段 PascalCase 兼容,新增解锁规则相关字段 */ | |
| 40293 | 40296 | function normalizeRestQuota(quotaSummary) { |
| 40297 | + var _ref11, _r$workedDaysThisMont, _ref12, _r$restUnlockCycle, _ref13, _r$unlockedRestDays, _ref14, _r$availableRestDays; | |
| 40294 | 40298 | var q = quotaSummary || {}; |
| 40295 | 40299 | var r = q.rest || q.Rest; |
| 40296 | - return r && (0, _typeof2.default)(r) === 'object' ? r : {}; | |
| 40300 | + if (!r || (0, _typeof2.default)(r) !== 'object') return {}; | |
| 40301 | + return _objectSpread(_objectSpread({}, r), {}, { | |
| 40302 | + workedDaysThisMonth: (_ref11 = (_r$workedDaysThisMont = r.workedDaysThisMonth) !== null && _r$workedDaysThisMont !== void 0 ? _r$workedDaysThisMont : r.WorkedDaysThisMonth) !== null && _ref11 !== void 0 ? _ref11 : 0, | |
| 40303 | + restUnlockCycle: (_ref12 = (_r$restUnlockCycle = r.restUnlockCycle) !== null && _r$restUnlockCycle !== void 0 ? _r$restUnlockCycle : r.RestUnlockCycle) !== null && _ref12 !== void 0 ? _ref12 : 0, | |
| 40304 | + unlockedRestDays: (_ref13 = (_r$unlockedRestDays = r.unlockedRestDays) !== null && _r$unlockedRestDays !== void 0 ? _r$unlockedRestDays : r.UnlockedRestDays) !== null && _ref13 !== void 0 ? _ref13 : 0, | |
| 40305 | + availableRestDays: (_ref14 = (_r$availableRestDays = r.availableRestDays) !== null && _r$availableRestDays !== void 0 ? _r$availableRestDays : r.AvailableRestDays) !== null && _ref14 !== void 0 ? _ref14 : 0 | |
| 40306 | + }); | |
| 40297 | 40307 | } |
| 40298 | 40308 | |
| 40299 | 40309 | /***/ }), | ... | ... |
绿纤uni-app/unpackage/dist/dev/mp-weixin/pagesA/components/leave-apply-scene.js
| ... | ... | @@ -312,28 +312,41 @@ var _default2 = { |
| 312 | 312 | }, |
| 313 | 313 | summaryCards: function summaryCards() { |
| 314 | 314 | if (this.isRestScene) { |
| 315 | - return [{ | |
| 315 | + var rest = this.restQuota; | |
| 316 | + var unlockEnabled = rest.restUnlockCycle > 0; | |
| 317 | + var remainValue = unlockEnabled ? rest.availableRestDays || 0 : rest.remainingRestDays || 0; | |
| 318 | + var remainDesc = unlockEnabled ? "\u5DF2\u89E3\u9501 ".concat(rest.unlockedRestDays || 0, " \u5929") : '天'; | |
| 319 | + var cards = [{ | |
| 316 | 320 | key: 'total', |
| 317 | 321 | label: '本月应休', |
| 318 | - value: this.formatNumber(this.restQuota.monthlyRestDays || 0), | |
| 322 | + value: this.formatNumber(rest.monthlyRestDays || 0), | |
| 319 | 323 | desc: '天' |
| 320 | 324 | }, { |
| 321 | 325 | key: 'used', |
| 322 | 326 | label: '已申请', |
| 323 | - value: this.formatNumber(this.restQuota.usedRestDays || 0), | |
| 327 | + value: this.formatNumber(rest.usedRestDays || 0), | |
| 324 | 328 | desc: '天' |
| 325 | 329 | }, { |
| 326 | 330 | key: 'remain', |
| 327 | 331 | label: '剩余可休', |
| 328 | - value: this.formatNumber(this.restQuota.remainingRestDays || 0), | |
| 329 | - desc: '天', | |
| 332 | + value: this.formatNumber(remainValue), | |
| 333 | + desc: remainDesc, | |
| 330 | 334 | active: true |
| 331 | 335 | }, { |
| 332 | 336 | key: 'split', |
| 333 | 337 | label: '半天额度', |
| 334 | - value: this.formatNumber(this.restQuota.remainingHalfDaySplitDays || 0), | |
| 335 | - desc: "\u8FD8\u80FD\u62C6 ".concat(this.restQuota.remainingHalfDaySelections || 0, " \u6B21\u534A\u5929") | |
| 338 | + value: this.formatNumber(rest.remainingHalfDaySplitDays || 0), | |
| 339 | + desc: "\u8FD8\u80FD\u62C6 ".concat(rest.remainingHalfDaySelections || 0, " \u6B21\u534A\u5929") | |
| 336 | 340 | }]; |
| 341 | + if (unlockEnabled) { | |
| 342 | + cards.push({ | |
| 343 | + key: 'worked', | |
| 344 | + label: '本月已上班', | |
| 345 | + value: "".concat(rest.workedDaysThisMonth || 0, " \u5929"), | |
| 346 | + desc: "\u6BCF".concat(rest.restUnlockCycle, "\u5929\u89E3\u95011\u5929") | |
| 347 | + }); | |
| 348 | + } | |
| 349 | + return cards; | |
| 337 | 350 | } |
| 338 | 351 | var extraTotalDays = this.extraLeaveList.reduce(function (sum, item) { |
| 339 | 352 | return sum + Number(item.extraDays || 0); |
| ... | ... | @@ -375,10 +388,25 @@ var _default2 = { |
| 375 | 388 | leaveTypeSummaryText: function leaveTypeSummaryText() { |
| 376 | 389 | var _this = this; |
| 377 | 390 | if (this.isRestScene) { |
| 378 | - if (!this.restQuota.attendanceGroupBound) { | |
| 391 | + var rest = this.restQuota; | |
| 392 | + if (!rest.attendanceGroupBound) { | |
| 379 | 393 | return '当前用户还未绑定考勤分组,暂时无法计算本月应休额度。'; |
| 380 | 394 | } |
| 381 | - return "\u5F53\u524D\u5206\u7EC4\u3010".concat(this.restQuota.attendanceGroupName || '未命名分组', "\u3011\u672C\u6708\u8FD8\u53EF\u4F11 ").concat(this.formatNumber(this.restQuota.remainingRestDays || 0), " \u5929\uFF1B\u5176\u4E2D\u8FD8\u80FD\u62C6\u5206\u534A\u5929 ").concat(this.formatNumber(this.restQuota.remainingHalfDaySplitDays || 0), " \u5929\u3002"); | |
| 395 | + if (rest.restUnlockCycle > 0) { | |
| 396 | + var worked = rest.workedDaysThisMonth || 0; | |
| 397 | + var cycle = rest.restUnlockCycle; | |
| 398 | + var unlocked = rest.unlockedRestDays || 0; | |
| 399 | + var available = rest.availableRestDays || 0; | |
| 400 | + var daysInCurrentCycle = worked % cycle; | |
| 401 | + var nextNeed = daysInCurrentCycle === 0 ? cycle : cycle - daysInCurrentCycle; | |
| 402 | + var canUnlockMore = unlocked < (rest.monthlyRestDays || 0); | |
| 403 | + var text = "\u672C\u6708\u5DF2\u4E0A\u73ED ".concat(worked, " \u5929\uFF0C\u5DF2\u89E3\u9501 ").concat(unlocked, " \u5929\u5E94\u4F11\uFF0C\u5F53\u524D\u53EF\u7533\u8BF7 ").concat(available, " \u5929\u3002"); | |
| 404 | + if (canUnlockMore && nextNeed > 0) { | |
| 405 | + text += "\u518D\u4E0A\u6EE1 ".concat(nextNeed, " \u5929\u53EF\u518D\u89E3\u9501 1 \u5929\u3002"); | |
| 406 | + } | |
| 407 | + return text; | |
| 408 | + } | |
| 409 | + return "\u5F53\u524D\u5206\u7EC4\u3010".concat(rest.attendanceGroupName || '未命名分组', "\u3011\u672C\u6708\u8FD8\u53EF\u4F11 ").concat(this.formatNumber(rest.remainingRestDays || 0), " \u5929\uFF1B\u5176\u4E2D\u8FD8\u80FD\u62C6\u5206\u534A\u5929 ").concat(this.formatNumber(rest.remainingHalfDaySplitDays || 0), " \u5929\u3002"); | |
| 382 | 410 | } |
| 383 | 411 | if (this.formData.leaveType === '婚假') { |
| 384 | 412 | var maxDays = Number((this.paidQuota.marriage || {}).maxDays || 0); |
| ... | ... | @@ -408,7 +436,8 @@ var _default2 = { |
| 408 | 436 | selectedLeaveMaxDays: function selectedLeaveMaxDays() { |
| 409 | 437 | var _this2 = this; |
| 410 | 438 | if (this.isRestScene) { |
| 411 | - return Number(this.restQuota.remainingRestDays || 0); | |
| 439 | + var rest = this.restQuota; | |
| 440 | + return rest.restUnlockCycle > 0 ? Number(rest.availableRestDays || 0) : Number(rest.remainingRestDays || 0); | |
| 412 | 441 | } |
| 413 | 442 | if (!this.isPaidScene) { |
| 414 | 443 | return null; |
| ... | ... | @@ -743,11 +772,12 @@ var _default2 = { |
| 743 | 772 | if (!this.formData.leaveEndTime) return '请先选择开始时间并填写正确的请假天数'; |
| 744 | 773 | if (!this.formData.leaveHour) return '请假小时计算失败,请重新填写'; |
| 745 | 774 | if (this.isRestScene) { |
| 746 | - if (!this.restQuota.attendanceGroupBound) return '当前用户未绑定考勤分组,无法提交应休申请'; | |
| 747 | - var remainDays = Number(this.restQuota.remainingRestDays || 0); | |
| 748 | - if (requestDays > remainDays) return "\u672C\u6708\u5269\u4F59\u53EF\u4F11 ".concat(this.formatNumber(remainDays), " \u5929"); | |
| 775 | + var rest = this.restQuota; | |
| 776 | + if (!rest.attendanceGroupBound) return '当前用户未绑定考勤分组,无法提交应休申请'; | |
| 777 | + var maxDays = rest.restUnlockCycle > 0 ? Number(rest.availableRestDays || 0) : Number(rest.remainingRestDays || 0); | |
| 778 | + if (requestDays > maxDays) return "\u5F53\u524D\u53EF\u7533\u8BF7 ".concat(this.formatNumber(maxDays), " \u5929"); | |
| 749 | 779 | var splitDays = this.getHalfDaySplitDays(requestDays); |
| 750 | - var remainSplitDays = Number(this.restQuota.remainingHalfDaySplitDays || 0); | |
| 780 | + var remainSplitDays = Number(rest.remainingHalfDaySplitDays || 0); | |
| 751 | 781 | if (splitDays > remainSplitDays) { |
| 752 | 782 | return "\u672C\u6708\u53EF\u62C6\u5206\u534A\u5929\u989D\u5EA6\u5269\u4F59 ".concat(this.formatNumber(remainSplitDays), " \u5929"); |
| 753 | 783 | } | ... | ... |
项目文档相关/sql/2026-3-25/考勤相关表全量重建.sql
| ... | ... | @@ -96,6 +96,9 @@ CREATE TABLE `lq_attendance_group` ( |
| 96 | 96 | `F_WorkEndTime` varchar(10) NOT NULL COMMENT '下班打卡时间', |
| 97 | 97 | `F_MonthlyRestDays` int NOT NULL DEFAULT 0 COMMENT '月应休天数', |
| 98 | 98 | `F_HalfDaySplitRestDays` int NOT NULL DEFAULT 0 COMMENT '可拆分半天休假天数', |
| 99 | + `F_RestUnlockCycle` int NOT NULL DEFAULT 0 COMMENT '应休解锁周期:每上满N天解锁1天应休(0=不启用)', | |
| 100 | + `F_LateToleranceMinutes` int DEFAULT NULL COMMENT '迟到容忍分钟数:null=迟到不计为有效上班;N=迟到≤N分钟才计为有效上班', | |
| 101 | + `F_EarlyLeaveToleranceMinutes` int DEFAULT NULL COMMENT '早退容忍分钟数:null=早退不计为有效上班;N=早退≤N分钟才计为有效上班', | |
| 99 | 102 | `F_IsEnabled` int NOT NULL DEFAULT 1 COMMENT '是否启用(1-启用,0-禁用)', |
| 100 | 103 | `F_Remark` varchar(500) DEFAULT NULL COMMENT '备注', |
| 101 | 104 | `F_SortCode` int NOT NULL DEFAULT 0 COMMENT '排序', | ... | ... |