Commit 05dda61fff06ac8ba5ad94f9587a0573bbcf0788

Authored by “wangming”
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 =&gt; {
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 &#39;../components/leave-apply-page&#39;
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 &#39;../components/leave-apply-page&#39;
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 &#39;../components/leave-apply-page&#39;
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&quot;;
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, &quot;__esModule&quot;, {
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 '排序',
... ...