diff --git a/docs/02-功能清单-医院/01-合同管理.md b/docs/02-功能清单-医院/01-合同管理.md index cb23965..6c2ca38 100644 --- a/docs/02-功能清单-医院/01-合同管理.md +++ b/docs/02-功能清单-医院/01-合同管理.md @@ -3,6 +3,7 @@ > 模块编码:contract > 端侧:Web专属(仅医院账号) > 关联文档:01-模块划分 §4.1 / 02-功能清单-医院 §1 / 03-业务流转逻辑-医院 §1 / 05-接口规范 §9.2 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -85,6 +86,59 @@ | 列表查询 | /api/v1/contracts | GET | 分页查询 | | 导出 | /api/v1/contracts/export | GET | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用合同列表API(默认分页20条/页)→ 渲染列表,到期日≤30天的行标记橙色 +2. **查询/筛选交互流程**:填写筛选条件 → 点击"查询"按钮 → 调用API携带筛选参数 → 刷新列表;点击"重置"清空所有条件并重新加载 +3. **表单填写与提交流程**:点击"新增合同" → 跳转合同录入页;点击"导出Excel" → 调用导出API → 下载文件 +4. **弹窗/抽屉交互流程**:无弹窗交互,操作均跳转页面 +5. **行内操作流程**:点击"查看"跳转合同详情页;点击"审批"(状态=审批中时显示)跳转合同详情页审批区域 +6. **异常与错误处理**:API请求失败显示el-message错误提示;列表为空显示el-empty;导出失败提示"导出失败,请重试" +7. **联动/级联交互**:状态筛选与列表数据联动,筛选后分页重置为第1页 +8. **权限控制交互表现**:无新增权限时隐藏"新增合同"按钮;无导出权限时隐藏"导出Excel"按钮;无审批权限时行操作不显示"审批" + +### 前端硬性约束 + +- **H1 防重复提交(强制)**: "查询"/"导出Excel"按钮请求期间置 loading+disabled 状态;行操作(查看/审批)点击后禁用该行所有操作按钮直至请求完成;翻页/切换每页条数时 abort 未完成的列表请求 +- **H2 超时控制(强制)**: 列表查询(GET)超时 15s;导出(GET,大文件场景)超时 60s;超过阈值提示"请求超时,请稍后重试" +- **H3 操作确认(强制)**: 本页面无危险操作需二次确认 +- **H5 权限隔离(建议)**: 当后端返回空列表且 code=200 时展示"暂无数据";当返回 code=403 时展示"无权限访问该模块",引导联系管理员 +- **H6 批量限制(建议)**: 导出Excel限制单次≤500条,超出时提示"数据量过大,请缩小筛选范围后导出" +- **H8 反馈机制(建议)**: 查询成功静默刷新(无 success 提示);查询/导出失败显示 error 提示(duration=0);网络异常时显示重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 合同名称输入 | el-input | placeholder="请输入合同名称",clearable,maxlength=100 | +| 合同类型选择 | el-select | placeholder="请选择合同类型",clearable,options: 保洁/维修/安保/综合/其他 | +| 状态选择 | el-select | placeholder="请选择状态",clearable,multiple=false | +| 物业公司选择 | el-select | placeholder="请选择物业公司",clearable,remote-method动态加载 | +| 签约日期范围 | el-date-picker | type="daterange",start-placeholder="开始日期",end-placeholder="结束日期" | +| 查询按钮 | el-button | type="primary",icon="Search" | +| 重置按钮 | el-button | type="default",icon="Refresh" | +| 新增合同按钮 | el-button | type="primary",icon="Plus" | +| 导出Excel按钮 | el-button | type="success",icon="Download" | +| 数据列表 | el-table | stripe,border,row-key="id",@row-click查看详情 | +| 合同类型列 | el-tag | :type根据合同类型配色 | +| 状态列 | el-tag | :type根据状态配色(审批中=warning,履约中=success,即将到期=danger) | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next",:page-sizes="[10,20,50]" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 合同名称 | maxlength=100 | 合同名称不能超过100个字符 | +| 签约日期范围 | 结束日期≥开始日期 | 结束日期不能早于开始日期 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区单行排列,列表全字段展示,操作栏水平排列 | +| 1024-1279px(Pad横屏) | 查询条件区双行排列,隐藏"序号""操作"列,操作收入"更多"下拉 | +| 768-1023px(Pad竖屏) | 查询条件区纵向堆叠,仅显示合同名称/类型/状态筛选,列表显示核心字段(合同名称/物业公司/状态/到期日),操作列固定右侧 | + --- ## 页面2:合同录入页 @@ -137,6 +191,68 @@ | 新增 | /api/v1/contracts | POST | — | | 提交审批 | /api/v1/contracts/{id}/submit | POST | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 渲染空表单 → 加载下拉选项数据(物业公司、院区、合同类型) +2. **查询/筛选交互流程**:不适用 +3. **表单填写与提交流程**:逐项填写表单 → 上传合同附件(支持拖拽上传,上传后显示文件列表含预览/删除)→ 付款方式选"分期"时展开付款节点子表单(动态增删行)→ 点击"保存草稿"保存为草稿状态 / 点击"提交审批"触发校验后提交 +4. **弹窗/抽屉交互流程**:合同附件上传使用el-upload,点击已上传文件可弹窗预览(PDF使用iframe预览,图片使用el-image-viewer预览) +5. **行内操作流程**:付款节点子表单支持行内添加/删除节点 +6. **异常与错误处理**:必填项未填时表单校验不通过,高亮未填字段并滚动到第一个错误字段;文件上传失败提示"上传失败";提交审批失败显示el-message错误提示 +7. **联动/级联交互**:付款方式选择"分期"→展开付款节点子表单;付款方式选择"一次性"→隐藏付款节点;服务期限(止)自动校验需晚于(起) +8. **权限控制交互表现**:无创建权限时跳转回列表页 + +### 前端硬性约束 + +- **H1 防重复提交(强制)**: "保存草稿"/"提交审批"按钮请求期间置 loading+disabled 状态;提交后禁用所有表单交互直至请求完成 +- **H2 超时控制(强制)**: 表单提交(POST)超时 30s;文件上传超时 60s;超过阈值提示"请求超时,请稍后重试" +- **H3 操作确认(强制)**: "提交审批"操作前弹出 confirm("确定要提交该合同审批?提交后将进入审批流程,不可自行修改。", type='warning') +- **H4 脏数据检测(强制)**: 页面加载时 deep clone 初始表单数据为 _originData;监听表单变化维护 isDirty 标志;用户未保存尝试离开(路由切换/关闭标签)时通过 beforeRouteLeave 拦截并弹窗确认 +- **H7 文件上传(建议)**: 合同附件上传限制单个文件≤10MB、总数≤9个;超出时提示"单文件不超过10MB,附件总数不超过9个";支持 .pdf/.doc/.docx/.jpg/.png +- **H8 反馈机制(建议)**: 保存草稿成功显示 success 提示(duration=2s)+ 静默刷新页面状态;提交审批成功显示 success 提示(duration=2s)+ 延迟跳转;失败显示 error 提示(duration=0);网络异常时显示重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 合同名称输入 | el-input | placeholder="请输入合同名称",maxlength=100,show-word-limit | +| 合同类型选择 | el-select | placeholder="请选择合同类型",options: 字典管理-合同类型 | +| 关联物业公司 | el-select | placeholder="请选择物业公司",filterable,remote,:remote-method搜索 | +| 关联院区 | el-select | placeholder="请选择院区",multiple,collapse-tags,collapse-tags-tooltip | +| 合同金额 | el-input-number | :min=0.01,:precision=2,controls-position="right" | +| 服务期限 | el-date-picker | type="daterange",start-placeholder="起始日期",end-placeholder="结束日期" | +| 付款方式 | el-select | placeholder="请选择付款方式",@change联动付款节点显示/隐藏 | +| 合同描述 | el-input | type="textarea",:rows=4,maxlength=500,show-word-limit | +| 合同附件 | el-upload | action="/api/v1/files/upload",:limit=10,accept=".pdf,.doc,.docx,.jpg,.png",:file-list.sync,:on-preview预览,:on-remove删除 | +| 付款节点表格 | el-table + el-button | 动态增删行,每行:节点名称/金额/日期/条件 | +| 保存草稿按钮 | el-button | type="default",@click保存草稿 | +| 提交审批按钮 | el-button | type="primary",@click提交审批,:loading提交中 | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 合同名称 | required,maxlength=100 | 请输入合同名称 / 合同名称不能超过100个字符 | +| 合同类型 | required | 请选择合同类型 | +| 关联物业公司 | required | 请选择关联物业公司 | +| 关联院区 | required | 请选择关联院区 | +| 合同金额 | required,>0 | 请输入合同金额 / 合同金额必须大于0 | +| 服务期限(起) | required | 请选择服务起始日期 | +| 服务期限(止) | required,晚于起始日期 | 请选择服务结束日期 / 结束日期必须晚于起始日期 | +| 付款方式 | required | 请选择付款方式 | +| 合同附件 | required,≤10个文件 | 请上传合同附件 / 附件数量不能超过10个 | +| 节点名称 | required(分期时) | 请输入节点名称 | +| 节点金额 | required(分期时),>0 | 请输入节点金额 / 节点金额必须大于0 | +| 预计付款日期 | required(分期时) | 请选择预计付款日期 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 表单双列布局,付款节点子表单全宽展示 | +| 1024-1279px(Pad横屏) | 表单单列布局,付款节点子表单全宽展示 | +| 768-1023px(Pad竖屏) | 表单单列布局,合同附件上传区域宽度100%,按钮组纵向堆叠 | + --- ## 页面3:合同详情页 @@ -164,6 +280,61 @@ | 申请变更 | contract:change:create | 底部 | 状态=履约中 | — | | 终止合同 | contract:list:update | 底部 | 状态=履约中 | 二次确认 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 根据合同ID调用详情API → 渲染基本信息及各标签页数据,默认展示"基本信息"标签页 +2. **查询/筛选交互流程**:不适用(详情页无查询) +3. **表单填写与提交流程**:审批通过/驳回 → 点击按钮 → 弹出审批确认弹窗(通过直接确认,驳回需填写驳回原因)→ 确认后调用API;申请变更 → 跳转变更申请页;终止合同 → 弹出二次确认弹窗 +4. **弹窗/抽屉交互流程**:审批驳回弹窗(el-dialog,含驳回原因多行文本输入);终止合同确认弹窗(el-message-box确认) +5. **行内操作流程**:标签页切换展示不同内容区域;附件标签页点击文件可预览/下载 +6. **异常与错误处理**:合同ID无效跳转404;审批/终止操作失败显示el-message错误提示 +7. **联动/级联交互**:合同状态=审批中时显示审批按钮;状态=履约中时显示申请变更和终止按钮 +8. **权限控制交互表现**:无审批权限隐藏审批按钮;无变更权限隐藏申请变更按钮;无终止权限隐藏终止合同按钮 + +### 前端硬性约束 + +- **H1 防重复提交(强制)**: "审批通过"/"审批驳回"/"申请变更"/"终止合同"按钮请求期间置 loading+disabled 状态;操作期间禁用同组其他按钮 +- **H2 超时控制(强制)**: 详情加载(GET)超时 15s;操作提交(POST)超时 30s;超过阈值提示"请求超时,请稍后重试" +- **H3 操作确认(强制)**: "终止合同"前弹出 confirm(`确定要终止合同${contractNo}?终止后该合同将无法恢复,相关付款节点将作废。`, type='error');"审批驳回"前弹出 confirm + 原因输入框(必填) +- **H8 反馈机制(建议)**: 操作成功显示 success 提示(duration=2s)+ 静默刷新详情数据;失败显示 error 提示(duration=0);网络异常时显示重试按钮 +- **弹窗表单(审批驳回)额外约束**: + - **H1 防重复提交(强制)**: 弹窗内确认按钮 loading+disabled + - **H2 超时控制(强制)**: 审批提交(POST)超时 30s + - **H3 操作确认(强制)**: 已由外层 H3 覆盖(驳回需填写原因) + - **H4 脏数据检测(强制)**: 弹窗打开时记录初始状态,有修改未提交尝试关闭弹窗时拦截确认 + - **H8 反馈机制(建议)**: 成功后关闭弹窗 + 刷新列表 + success(2s);失败 error(0) + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 标签页 | el-tabs | type="border-card",:tab-click切换标签页 | +| 基本信息 | el-descriptions | :column=2,border,label-class-name="desc-label" | +| 付款节点列表 | el-table | :data=付款节点数据,stripe,show-summary | +| 变更记录列表 | el-timeline | 展示变更历史时间线 | +| 关联项目 | el-link | :underline=false,type="primary",点击跳转招标管理 | +| 附件列表 | el-upload | :file-list.sync,disabled,:on-preview预览下载 | +| 审批通过按钮 | el-button | type="success",icon="Check" | +| 审批驳回按钮 | el-button | type="danger",icon="Close" | +| 申请变更按钮 | el-button | type="warning",icon="Edit" | +| 终止合同按钮 | el-button | type="danger",icon="SwitchButton" | +| 审批驳回弹窗 | el-dialog | title="审批驳回",width="500px",含el-input textarea | +| 终止确认弹窗 | el-message-box | type="warning",confirm-button-text="确认终止" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 驳回原因 | required(审批驳回时) | 请输入驳回原因 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 标签页内容区双列描述列表,底部按钮水平居右排列 | +| 1024-1279px(Pad横屏) | 标签页内容区单列描述列表,底部按钮水平居右排列 | +| 768-1023px(Pad竖屏) | 标签页内容区单列描述列表,底部按钮纵向堆叠,附件列表自适应宽度 | + --- ## 页面4:付款管理页 @@ -196,6 +367,58 @@ |----------|----------|----------|----------|----------| | 付款节点到期提醒 | 医院账号 | Web提醒 | 付款节点即将到期 | 03-医院 §1.2 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用付款列表API → 渲染列表,逾期状态行标红 +2. **查询/筛选交互流程**:不适用(由合同筛选进入,可按合同名称/状态筛选) +3. **表单填写与提交流程**:点击"确认付款" → 弹出付款确认弹窗(填写实际付款日期、付款金额、付款备注)→ 确认后调用API → 刷新列表 +4. **弹窗/抽屉交互流程**:付款确认弹窗(el-dialog,含实际付款日期、付款金额、备注字段) +5. **行内操作流程**:点击"确认付款"弹出付款确认弹窗 +6. **异常与错误处理**:付款确认失败显示el-message错误提示;逾期状态高亮显示 +7. **联动/级联交互**:付款状态与行操作按钮联动(仅待付款状态显示确认付款按钮) +8. **权限控制交互表现**:无付款确认权限时隐藏"确认付款"按钮 + +### 前端硬性约束 + +- **H1 防重复提交(强制)**: "确认付款"行操作按钮点击后置 loading+disabled 状态;弹窗内确认按钮同样 loading+disabled;操作期间禁用该行其他操作 +- **H2 超时控制(强制)**: 列表查询(GET)超时 15s;付款确认(POST)超时 30s;超过阈值提示"请求超时,请稍后重试" +- **H3 操作确认(强制)**: "确认付款"前弹出 confirm("确定要确认该付款节点已付款?确认后状态将变更为\"已付款\"。", type='warning') +- **H5 权限隔离(建议)**: 当返回空列表展示"暂无付款记录";code=403 时展示"无权限访问" +- **H8 反馈机制(建议)**: 付款成功显示 success(duration=2s)+ 静默刷新列表;失败显示 error(duration=0);网络异常显示重试按钮 +- **弹窗表单(付款确认)额外约束**: + - **H1 防重复提交(强制)**: 弹窗确认按钮 loading+disabled + - **H2 超时控制(强制)**: 提交(POST)超时 30s + - **H4 脏数据检测(强制)**: 弹窗内表单修改后尝试关闭时拦截确认 + - **H8 反馈机制(建议)**: 成功关闭弹窗 + 刷新 + success(2s);失败 error(0) + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 付款列表 | el-table | stripe,border,:default-sort="{prop: 'expectedDate', order: 'ascending'}" | +| 状态列 | el-tag | :type根据状态配色(待付款=warning,已付款=success,逾期=danger) | +| 确认付款按钮 | el-button | type="primary",size="small",link样式 | +| 付款确认弹窗 | el-dialog | title="确认付款",width="500px" | +| 实际付款日期 | el-date-picker | type="date",placeholder="请选择实际付款日期" | +| 付款金额 | el-input-number | :min=0.01,:precision=2 | +| 付款备注 | el-input | type="textarea",:rows=3 | +| 确认按钮 | el-button | type="primary",:loading提交中 | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 实际付款日期 | required | 请选择实际付款日期 | +| 付款金额 | required,>0 | 请输入付款金额 / 付款金额必须大于0 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 列表全字段展示,操作列右对齐 | +| 1024-1279px(Pad横屏) | 隐藏"节点名称"列,操作列固定右侧 | +| 768-1023px(Pad竖屏) | 仅显示合同名称/节点金额/状态/操作核心字段,付款确认弹窗全屏宽度 | + --- ## 页面5:变更管理页 @@ -222,6 +445,59 @@ |------|----------|------|----------|------| | 审批 | contract:change:approve | 行操作 | 审批状态=待审批 | 通过/驳回 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用变更列表API → 渲染列表,默认按申请时间倒序 +2. **查询/筛选交互流程**:不适用(由合同详情进入,可按合同名称/审批状态筛选) +3. **表单填写与提交流程**:不适用(变更申请由合同详情页发起) +4. **弹窗/抽屉交互流程**:点击"审批" → 弹出审批弹窗(选择通过/驳回,驳回需填写原因)→ 确认后调用API → 刷新列表;点击"查看" → 跳转变更详情页 +5. **行内操作流程**:审批状态=待审批时显示"审批"按钮;始终显示"查看"按钮 +6. **异常与错误处理**:审批操作失败显示el-message错误提示;列表为空显示el-empty +7. **联动/级联交互**:审批状态与操作按钮联动(待审批显示审批,其余仅查看) +8. **权限控制交互表现**:无审批权限时行操作仅显示"查看" + +### 前端硬性约束 + +- **H1 防重复提交(强制)**: "审批"行操作按钮点击后置 loading+disabled 状态;弹窗内确认按钮同样 loading+disabled +- **H2 超时控制(强制)**: 列表查询(GET)超时 15s;变更审批(POST)超时 30s;超过阈值提示"请求超时,请稍后重试" +- **H3 操作确认(强制)**: 审批驳回前弹出 confirm + 驳回原因输入框(必填);审批通过弹出 confirm("确定要通过该变更申请?通过后变更将立即生效。", type='warning') +- **H5 权限隔离(建议)**: 返回空列表展示"暂无变更记录";code=403 时展示"无权限访问" +- **H8 反馈机制(建议)**: 操作成功 success(duration=2s)+ 静默刷新列表;失败 error(duration=0);网络异常显示重试按钮 +- **弹窗表单(变更审批)额外约束**: + - **H1 防重复提交(强制)**: 弹窗确认按钮 loading+disabled + - **H2 超时控制(强制)**: 提交(POST)超时 30s + - **H3 操作确认(强制)**: 已由外层 H3 覆盖 + - **H4 脏数据检测(强制)**: 弹窗内审批结果/原因修改后尝试关闭时拦截确认 + - **H8 反馈机制(建议)**: 成功关闭弹窗 + 刷新 + success(2s);失败 error(0) + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 变更列表 | el-table | stripe,border,:default-sort="{prop: 'applyTime', order: 'descending'}" | +| 变更类型列 | el-tag | :type根据类型配色(金额变更=warning,期限变更=info,条款变更=primary) | +| 审批状态列 | el-tag | :type根据状态配色(待审批=warning,已通过=success,已驳回=danger) | +| 审批按钮 | el-button | type="primary",size="small",link样式 | +| 查看按钮 | el-button | type="default",size="small",link样式 | +| 审批弹窗 | el-dialog | title="变更审批",width="500px" | +| 审批结果 | el-radio-group | 通过/驳回单选 | +| 驳回原因 | el-input | type="textarea",:rows=3,v-if="审批结果=驳回" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 审批结果 | required | 请选择审批结果 | +| 驳回原因 | required(审批结果=驳回时) | 请输入驳回原因 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 列表全字段展示,操作列右对齐 | +| 1024-1279px(Pad横屏) | 隐藏"变更原因"列(点击查看详情),操作列固定右侧 | +| 768-1023px(Pad竖屏) | 仅显示合同名称/变更类型/审批状态/操作核心字段,审批弹窗全屏宽度 | + --- ## 页面6:到期预警页 @@ -248,6 +524,50 @@ | 合同到期前15天 | 医院账号 | Web提醒 | 合同即将到期 | — | | 合同到期前7天 | 医院账号 | Web提醒 | 合同即将到期 | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用到期预警列表API → 渲染列表,剩余天数≤7天行标红,≤30天行标橙色 +2. **查询/筛选交互流程**:不适用(仅展示即将到期合同,可按物业公司/剩余天数筛选) +3. **表单填写与提交流程**:不适用 +4. **弹窗/抽屉交互流程**:点击"续签" → 弹出续签确认弹窗(确认续签后跳转合同录入页,自动填充原合同信息);点击"终止" → 弹出终止确认弹窗(el-message-box二次确认) +5. **行内操作流程**:点击"续签"或"终止"执行对应操作 +6. **异常与错误处理**:续签/终止操作失败显示el-message错误提示;列表为空显示el-empty(提示"暂无即将到期的合同") +7. **联动/级联交互**:合同状态与操作按钮联动(仅履约中状态显示续签/终止按钮) +8. **权限控制交互表现**:无续签权限隐藏"续签"按钮;无终止权限隐藏"终止"按钮 + +### 前端硬性约束 + +- **H1 防重复提交(强制)**: "续签"/"终止"行操作按钮点击后置 loading+disabled 状态;操作期间禁用该行其他操作 +- **H2 超时控制(强制)**: 列表查询(GET)超时 15s;操作提交(POST)超时 30s;超过阈值提示"请求超时,请稍后重试" +- **H3 操作确认(强制)**: "终止合同"前弹出 confirm(`确定要终止合同${contractNo}?终止后该合同将作废且无法恢复。`, type='error');"续签"前弹出 confirm("确定要续签该合同?将跳转至合同录入页并自动填充原合同信息。", type='warning') +- **H5 权限隔离(建议)**: 返回空列表展示"暂无即将到期的合同";code=403 时展示"无权限访问" +- **H8 反馈机制(建议)**: 操作成功 success(duration=2s)+ 静默刷新列表;失败 error(duration=0);网络异常显示重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 预警列表 | el-table | stripe,border,:row-class-name根据剩余天数设置行样式 | +| 剩余天数列 | el-tag | :type根据天数配色(≤7天=danger,≤15天=warning,≤30天=info) | +| 续签按钮 | el-button | type="primary",size="small",link样式,icon="RefreshRight" | +| 终止按钮 | el-button | type="danger",size="small",link样式,icon="SwitchButton" | +| 续签确认弹窗 | el-message-box | title="确认续签",type="info",confirm-button-text="确认续签" | +| 终止确认弹窗 | el-message-box | title="确认终止",type="warning",confirm-button-text="确认终止" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| (无表单校验) | — | — | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 列表全字段展示,操作列右对齐 | +| 1024-1279px(Pad横屏) | 隐藏"物业公司"列,操作列固定右侧 | +| 768-1023px(Pad竖屏) | 仅显示合同名称/到期日期/剩余天数/操作核心字段,操作按钮改为图标按钮 | + --- ## 需求追溯 diff --git a/docs/02-功能清单-医院/02-分段招标管理.md b/docs/02-功能清单-医院/02-分段招标管理.md index cf6ae23..db0a16d 100644 --- a/docs/02-功能清单-医院/02-分段招标管理.md +++ b/docs/02-功能清单-医院/02-分段招标管理.md @@ -3,6 +3,7 @@ > 模块编码:bidding > 端侧:Web专属(仅医院账号) > 关联文档:01-模块划分 §4.2 / 02-功能清单-医院 §2 / 03-业务流转逻辑-医院 §2 / 05-接口规范 §9.2 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -70,6 +71,69 @@ | 新增 | /api/v1/bidding-plans | POST | — | | 编辑 | /api/v1/bidding-plans/{id} | PUT | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用招标计划列表API → 渲染列表 +2. **查询/筛选交互流程**:填写筛选条件 → 点击"查询"按钮 → 调用API携带筛选参数 → 刷新列表;点击"重置"清空条件重新加载 +3. **表单填写与提交流程**:点击"新增招标计划" → 弹出新增弹窗(或跳转新增页)→ 填写表单 → 提交保存;点击"编辑"(状态=计划中)→ 弹出编辑弹窗 → 修改后保存 +4. **弹窗/抽屉交互流程**:新增/编辑弹窗(el-dialog,含招标计划表单字段) +5. **行内操作流程**:点击"查看"跳转计划详情页;点击"编辑"弹出编辑弹窗 +6. **异常与错误处理**:API请求失败显示el-message错误提示;投标截止日期早于当前时间提交时提示错误 +7. **联动/级联交互**:状态筛选与列表数据联动,筛选后分页重置为第1页 +8. **权限控制交互表现**:无创建权限隐藏"新增招标计划"按钮;状态非"计划中"时隐藏"编辑"按钮 + +### 前端硬性约束 + +- **H1 防重复提交(强制)**: "查询"/"新增招标计划"按钮请求期间置 loading+disabled 状态;行操作(编辑/查看)点击后禁用该行操作直至请求完成;翻页时 abort 未完成请求 +- **H2 超时控制(强制)**: 列表查询(GET)超时 15s;表单提交(POST)超时 30s;超过阈值提示"请求超时,请稍后重试" +- **H3 操作确认(强制)**: 本页面无危险操作需二次确认 +- **H5 权限隔离(建议)**: 返回空列表展示"暂无招标计划";code=403 时展示"无权限访问" +- **H6 批量限制(建议)**: 本页面无批量导出操作 +- **H8 反馈机制(建议)**: 操作成功 success(duration=2s)+ 静默刷新;失败 error(duration=0);网络异常显示重试按钮 +- **弹窗表单(新增/编辑招标计划)额外约束**: + - **H1 防重复提交(强制)**: 弹窗内提交按钮 loading+disabled + - **H2 超时控制(强制)**: 提交(POST/PUT)超时 30s + - **H4 脏数据检测(强制)**: 弹窗内表单修改后尝试关闭时拦截确认 + - **H8 反馈机制(建议)**: 成功关闭弹窗 + 刷新 + success(2s);失败 error(0) + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 计划名称输入 | el-input | placeholder="请输入计划名称",clearable,maxlength=100 | +| 状态选择 | el-select | placeholder="请选择状态",clearable | +| 招标方式选择 | el-select | placeholder="请选择招标方式",clearable,options: 公开招标/邀请招标/竞争性谈判 | +| 查询按钮 | el-button | type="primary",icon="Search" | +| 重置按钮 | el-button | type="default",icon="Refresh" | +| 新增招标计划按钮 | el-button | type="primary",icon="Plus" | +| 计划列表 | el-table | stripe,border,row-key="id" | +| 状态列 | el-tag | :type根据状态配色(计划中=info,投标中=warning,已完成=success) | +| 编辑按钮 | el-button | type="primary",size="small",link样式 | +| 查看按钮 | el-button | type="default",size="small",link样式 | +| 新增/编辑弹窗 | el-dialog | title="新增招标计划/编辑招标计划",width="600px" | +| 关联项目选择 | el-select | placeholder="请选择关联项目",filterable | +| 预算金额 | el-input-number | :min=0.01,:precision=2,controls-position="right" | +| 投标截止日期 | el-date-picker | type="datetime",placeholder="请选择投标截止日期" | +| 计划描述 | el-input | type="textarea",:rows=4,maxlength=500,show-word-limit | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 计划名称 | required,maxlength=100 | 请输入计划名称 / 计划名称不能超过100个字符 | +| 关联项目 | required | 请选择关联项目 | +| 招标方式 | required | 请选择招标方式 | +| 预算金额 | required,>0 | 请输入预算金额 / 预算金额必须大于0 | +| 投标截止日期 | required,晚于当前时间 | 请选择投标截止日期 / 截止日期必须晚于当前时间 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区单行排列,列表全字段展示 | +| 1024-1279px(Pad横屏) | 查询条件区双行排列,隐藏"创建时间"列,编辑弹窗宽度90% | +| 768-1023px(Pad竖屏) | 查询条件区纵向堆叠,仅显示计划名称/状态筛选,列表显示核心字段(计划名称/招标方式/状态/操作),弹窗全屏宽度 | + --- ## 页面2:标段管理页 @@ -100,6 +164,61 @@ | 资质要求 | 多行文本 | 否 | — | 自填 | — | | 评标标准 | 多行文本 | 否 | — | 自填 | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面(从招标计划详情进入)→ 调用标段列表API → 渲染列表 +2. **查询/筛选交互流程**:不适用(按所属计划自动过滤) +3. **表单填写与提交流程**:点击"新增标段"(操作栏)→ 弹出新增弹窗 → 填写标段信息 → 保存;点击"编辑" → 弹出编辑弹窗 → 修改后保存 +4. **弹窗/抽屉交互流程**:新增/编辑标段弹窗(el-dialog,含标段名称、范围、预算金额、资质要求、评标标准字段) +5. **行内操作流程**:点击"编辑"弹出编辑弹窗;点击"查看"跳转标段详情 +6. **异常与错误处理**:保存失败显示el-message错误提示;预算金额为0时阻止提交 +7. **联动/级联交互**:所属计划与标段数据联动,切换计划自动加载对应标段 +8. **权限控制交互表现**:无编辑权限时行操作仅显示"查看" + +### 前端硬性约束 + +- **H1 防重复提交(强制)**: "新增标段"/行操作(编辑/查看)按钮请求期间置 loading+disabled 状态;操作期间禁用该行其他操作 +- **H2 超时控制(强制)**: 列表查询(GET)超时 15s;标段保存(POST)超时 30s;超过阈值提示"请求超时,请稍后重试" +- **H3 操作确认(强制)**: 本页面无危险操作需二次确认 +- **H5 权限隔离(建议)**: 返回空列表展示"暂无标段数据";code=403 时展示"无权限访问" +- **H8 反馈机制(建议)**: 操作成功 success(duration=2s)+ 静默刷新列表;失败 error(duration=0);网络异常显示重试按钮 +- **弹窗表单(新增/编辑标段)额外约束**: + - **H1 防重复提交(强制)**: 弹窗内提交按钮 loading+disabled + - **H2 超时控制(强制)**: 提交(POST)超时 30s + - **H4 脏数据检测(强制)**: 弹窗内表单修改后尝试关闭时拦截确认 + - **H8 反馈机制(建议)**: 成功关闭弹窗 + 刷新 + success(2s);失败 error(0) + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 标段列表 | el-table | stripe,border,row-key="id" | +| 状态列 | el-tag | :type根据状态配色 | +| 编辑按钮 | el-button | type="primary",size="small",link样式 | +| 查看按钮 | el-button | type="default",size="small",link样式 | +| 新增/编辑弹窗 | el-dialog | title="新增标段/编辑标段",width="600px" | +| 标段名称输入 | el-input | placeholder="请输入标段名称",maxlength=50 | +| 标段范围 | el-input | type="textarea",:rows=3,maxlength=500,show-word-limit | +| 预算金额 | el-input-number | :min=0.01,:precision=2,controls-position="right" | +| 资质要求 | el-input | type="textarea",:rows=3 | +| 评标标准 | el-input | type="textarea",:rows=3 | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 标段名称 | required,maxlength=50 | 请输入标段名称 / 标段名称不能超过50个字符 | +| 标段范围 | required,maxlength=500 | 请输入标段范围 / 标段范围不能超过500个字符 | +| 预算金额 | required,>0 | 请输入预算金额 / 预算金额必须大于0 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 列表全字段展示,操作列右对齐 | +| 1024-1279px(Pad横屏) | 隐藏"标段范围"列,编辑弹窗宽度90% | +| 768-1023px(Pad竖屏) | 仅显示标段名称/预算金额/状态/操作核心字段,弹窗全屏宽度 | + --- ## 页面3:供应商管理页 @@ -149,6 +268,74 @@ | 资质审核 | /api/v1/suppliers/{id}/audit | POST | — | | 拉黑 | /api/v1/suppliers/{id}/blacklist | PUT | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用供应商列表API → 渲染列表,黑名单供应商行灰色标记 +2. **查询/筛选交互流程**:支持按供应商名称、资质状态筛选,查询后刷新列表 +3. **表单填写与提交流程**:点击"新增供应商" → 弹出新增弹窗 → 填写供应商信息 → 保存;点击"编辑" → 弹出编辑弹窗 → 修改后保存 +4. **弹窗/抽屉交互流程**:新增/编辑供应商弹窗(el-dialog);资质审核弹窗(选择通过/驳回,驳回需填写原因);拉黑确认弹窗(el-message-box) +5. **行内操作流程**:点击"资质审核"弹出审核弹窗;点击"拉黑"弹出确认弹窗,确认后状态变更;点击"移出黑名单"恢复正常状态 +6. **异常与错误处理**:统一社会信用代码重复时提示"该供应商已存在";拉黑后该供应商不可投标,操作时二次确认提醒 +7. **联动/级联交互**:资质状态与操作按钮联动(待审核显示审核按钮,黑名单显示移出黑名单按钮) +8. **权限控制交互表现**:无审核权限隐藏"资质审核"按钮;无拉黑权限隐藏"拉黑/移出黑名单"按钮 + +### 前端硬性约束 + +- **H1 防重复提交(强制)**: "新增供应商"/行操作(编辑/资质审核/拉黑)按钮请求期间置 loading+disabled 状态;操作期间禁用该行其他操作 +- **H2 超时控制(强制)**: 列表查询(GET)超时 15s;提交/审核(POST)超时 30s;超过阈值提示"请求超时,请稍后重试" +- **H3 操作确认(强制)**: "拉黑"前弹出 confirm(`确定要将供应商"${supplierName}"加入黑名单?加入后该供应商将无法参与任何投标。`, type='error');"移出黑名单"前弹出 confirm("确定要将该供应商移出黑名单?", type='warning');"资质审核-驳回"前弹出 confirm + 驳回原因输入框(必填) +- **H5 权限隔离(建议)**: 返回空列表展示"暂无供应商数据";code=403 时展示"无权限访问" +- **H7 文件上传(建议)**: 资质文件上传限制单个文件≤10MB、总数≤9个;超出时提示"单文件不超过10MB,附件总数不超过9个" +- **H8 反馈机制(建议)**: 操作成功 success(duration=2s)+ 静默刷新列表;失败 error(duration=0);网络异常显示重试按钮 +- **弹窗表单(新增/编辑供应商)额外约束**: + - **H1 防重复提交(强制)**: 弹窗内提交按钮 loading+disabled + - **H2 超时控制(强制)**: 提交(POST)超时 30s + - **H4 脏数据检测(强制)**: 弹窗内表单修改后尝试关闭时拦截确认 + - **H8 反馈机制(建议)**: 成功关闭弹窗 + 刷新 + success(2s);失败 error(0) +- **弹窗表单(资质审核)额外约束**: + - **H1 防重复提交(强制)**: 弹窗内确认按钮 loading+disabled + - **H2 超时控制(强制)**: 审核(POST)超时 30s + - **H3 操作确认(强制)**: 已由外层 H3 覆盖(驳回需原因) + - **H4 脏数据检测(强制)**: 审核结果/驳回原因修改后尝试关闭时拦截 + - **H8 反馈机制(建议)**: 成功关闭弹窗 + 刷新 + success(2s);失败 error(0) + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 供应商列表 | el-table | stripe,border,:row-class-name黑名单行灰色 | +| 资质状态列 | el-tag | :type根据状态配色(已审核=success,待审核=warning,黑名单=danger) | +| 新增供应商按钮 | el-button | type="primary",icon="Plus" | +| 编辑按钮 | el-button | type="primary",size="small",link样式 | +| 资质审核按钮 | el-button | type="warning",size="small",link样式 | +| 拉黑按钮 | el-button | type="danger",size="small",link样式 | +| 新增/编辑弹窗 | el-dialog | title="新增供应商/编辑供应商",width="600px" | +| 统一社会信用代码 | el-input | placeholder="请输入统一社会信用代码",maxlength=18 | +| 联系电话 | el-input | placeholder="请输入联系电话" | +| 资质文件上传 | el-upload | action="/api/v1/files/upload",:limit=5,:file-list.sync | +| 资质审核弹窗 | el-dialog | title="资质审核",width="500px" | +| 审核结果 | el-radio-group | 通过/驳回单选 | +| 审核驳回原因 | el-input | type="textarea",:rows=3,v-if="审核结果=驳回" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 供应商名称 | required,maxlength=100 | 请输入供应商名称 / 供应商名称不能超过100个字符 | +| 统一社会信用代码 | required,pattern=/^[0-9A-Z]{18}$/ | 请输入统一社会信用代码 / 统一社会信用代码格式不正确(18位字母数字) | +| 联系人 | required | 请输入联系人 | +| 联系电话 | required,pattern=/^1[3-9]\d{9}$/ | 请输入联系电话 / 联系电话格式不正确 | +| 审核结果 | required(审核时) | 请选择审核结果 | +| 审核驳回原因 | required(审核驳回时) | 请输入驳回原因 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 列表全字段展示,操作列右对齐 | +| 1024-1279px(Pad横屏) | 隐藏"合作次数"列,操作收入"更多"下拉 | +| 768-1023px(Pad竖屏) | 仅显示供应商名称/资质状态/操作核心字段,弹窗全屏宽度 | + --- ## 页面4:招标发布页 @@ -172,6 +359,54 @@ | 招标文件 | 文件上传 | 是 | — | 上传 | ≤10个文件 | | 邀请供应商 | 下拉多选 | 条件 | — | 供应商列表(已审核) | 邀请招标时必填 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 加载可选招标计划(状态=待发布)→ 渲染发布表单 +2. **查询/筛选交互流程**:不适用 +3. **表单填写与提交流程**:选择招标计划 → 自动加载关联标段信息 → 确认标段 → 上传招标文件 → 填写招标公告 → (邀请招标时)选择邀请供应商 → 点击"发布" → 二次确认后调用发布API → 发布成功跳转招标计划页 +4. **弹窗/抽屉交互流程**:发布确认弹窗(el-message-box,提示"确认发布招标?发布后不可撤回");招标文件上传使用el-upload,支持预览已上传文件 +5. **行内操作流程**:不适用(发布页为表单交互) +6. **异常与错误处理**:招标文件未上传时阻止发布;邀请招标未选择供应商时提示"请选择邀请供应商";发布失败显示el-message错误提示 +7. **联动/级联交互**:招标方式为"邀请招标"时显示邀请供应商选择(必填);招标方式为"公开招标"时隐藏;选择招标计划后自动加载标段信息 +8. **权限控制交互表现**:无发布权限时隐藏"发布"按钮 + +### 前端硬性约束 + +- **H1 防重复提交(强制)**: "发布"按钮请求期间置 loading+disabled 状态;提交后禁用所有表单交互直至请求完成 +- **H2 超时控制(强制)**: 表单提交(POST)超时 30s;文件上传超时 60s;超过阈值提示"请求超时,请稍后重试" +- **H3 操作确认(强制)**: "发布"前弹出 confirm("确定要发布该招标?发布后不可撤回,供应商将可查看并投标。", type='warning') +- **H4 脏数据检测(强制)**: 页面加载时 deep clone 初始表单数据为 _originData;监听表单变化维护 isDirty 标志;用户未保存尝试离开时通过 beforeRouteLeave 拦截并弹窗确认 +- **H7 文件上传(建议)**: 招标文件上传限制单个文件≤10MB、总数≤9个;超出时提示"单文件不超过10MB,文件总数不超过9个";支持 .pdf/.doc/.docx +- **H8 反馈机制(建议)**: 发布成功显示 success(duration=2s)+ 延迟跳转招标计划页;失败显示 error(duration=0);网络异常时显示重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 招标计划选择 | el-select | placeholder="请选择招标计划",filterable,@change加载标段信息 | +| 标段确认区 | el-table | :data=关联标段,stripe,border,disabled不可编辑 | +| 招标公告 | el-input | type="textarea",:rows=6,placeholder="请输入招标公告" | +| 招标文件上传 | el-upload | action="/api/v1/files/upload",:limit=10,accept=".pdf,.doc,.docx",drag拖拽上传,:on-preview预览 | +| 邀请供应商 | el-select | placeholder="请选择供应商",multiple,filterable,collapse-tags,v-if="招标方式=邀请招标" | +| 发布按钮 | el-button | type="primary",icon="Promotion",:loading提交中 | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 招标计划 | required | 请选择招标计划 | +| 招标公告 | required | 请输入招标公告 | +| 招标文件 | required,≤10个 | 请上传招标文件 / 招标文件不能超过10个 | +| 邀请供应商 | required(邀请招标时) | 请选择邀请供应商 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 表单双列布局,标段确认区全宽展示 | +| 1024-1279px(Pad横屏) | 表单单列布局,标段确认区全宽展示 | +| 768-1023px(Pad竖屏) | 表单单列布局,文件上传区域宽度100%,邀请供应商选择全宽展示 | + --- ## 页面5:投标管理页 @@ -191,6 +426,48 @@ | 5 | 投标文件 | 100px | 否 | 下载查看 | | 6 | 状态 | 80px | 否 | 已投标/已开标 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用投标列表API → 渲染列表,默认按投标时间倒序 +2. **查询/筛选交互流程**:支持按标段名称、供应商、状态筛选 +3. **表单填写与提交流程**:不适用(医院端仅查看投标数据,投标由供应商提交) +4. **弹窗/抽屉交互流程**:点击投标文件"下载查看" → 新窗口打开/下载投标文件 +5. **行内操作流程**:点击投标文件列链接下载查看 +6. **异常与错误处理**:投标文件下载失败提示"文件下载失败,请重试";列表为空显示el-empty +7. **联动/级联交互**:标段选择后自动过滤该标段下的投标数据 +8. **权限控制交互表现**:医院账号仅查看,无操作按钮 + +### 前端硬性约束 + +- **H1 防重复提交(强制)**: 投标文件下载按钮点击后置 loading+disabled 状态直至下载完成 +- **H2 超时控制(强制)**: 列表查询(GET)超时 15s;文件下载超时 60s;超过阈值提示"请求超时,请稍后重试" +- **H8 反馈机制(建议)**: 文件下载失败显示 error(duration=0);网络异常时显示重试按钮;列表加载成功静默刷新 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 投标列表 | el-table | stripe,border,:default-sort="{prop: 'bidTime', order: 'descending'}" | +| 状态列 | el-tag | :type根据状态配色(已投标=info,已开标=success) | +| 投标文件 | el-link | type="primary",:underline=false,icon="Download",@click下载 | +| 标段筛选 | el-select | placeholder="请选择标段",clearable,filterable | +| 供应商筛选 | el-input | placeholder="请输入供应商名称",clearable | +| 状态筛选 | el-select | placeholder="请选择状态",clearable | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| (无表单校验) | — | — | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 列表全字段展示 | +| 1024-1279px(Pad横屏) | 隐藏"投标文件"列(改为行操作下载按钮) | +| 768-1023px(Pad竖屏) | 仅显示标段名称/供应商/投标金额/状态核心字段 | + --- ## 页面6:评标管理页 @@ -227,6 +504,69 @@ | 设置评分标准 | bidding:award:create | 操作栏 | 始终 | — | | 提交评标结果 | bidding:award:update | 操作栏 | 始终 | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用评标数据API → 渲染评标委员会信息、评分标准、评分录入表格 +2. **查询/筛选交互流程**:不适用 +3. **表单填写与提交流程**:流程步骤:组建评标委员会 → 设置评分标准 → 录入各供应商评分 → 提交评标结果。组建委员会:点击按钮 → 弹窗选择评标人员 → 保存;设置评分标准:点击按钮 → 弹窗设置各维度权重 → 保存(权重之和必须=100%);录入评分:在评分表格中逐行填写各供应商的商务分/技术分/价格分 → 系统自动计算总分和排名 → 点击"提交评标结果" +4. **弹窗/抽屉交互流程**:组建评标委员会弹窗(el-dialog,含评标人员多选);设置评分标准弹窗(el-dialog,含各维度权重滑块/输入) +5. **行内操作流程**:评分录入表格中行内输入评分数据,总分和排名自动计算 +6. **异常与错误处理**:评分权重之和不等于100%时阻止保存;单项评分超出范围提示错误;提交评标结果时校验所有供应商评分是否填写完整 +7. **联动/级联交互**:评分标准设置后,评分录入表头动态显示对应权重;各维度评分输入后自动计算总分和排名 +8. **权限控制交互表现**:无评标权限时仅查看评分结果,不可编辑 + +### 前端硬性约束 + +- **H1 防重复提交(强制)**: "组建评标委员会"/"设置评分标准"/"提交评标结果"按钮请求期间置 loading+disabled 状态;评分录入期间锁定已填写行 +- **H2 超时控制(强制)**: 数据加载(GET)超时 15s;提交(POST)超时 30s;超过阈值提示"请求超时,请稍后重试" +- **H3 操作确认(强制)**: "提交评标结果"前弹出 confirm("确定要提交评标结果?提交后将进入定标审批流程,不可修改评分。", type='warning') +- **H4 脏数据检测(强制)**: 页面加载时 deep clone 初始评分数据为 _originData;监听评分变化维护 isDirty 标志;用户有未保存评分尝试离开时通过 beforeRouteLeave 拦截确认 +- **H8 反馈机制(建议)**: 操作成功显示 success(duration=2s)+ 静默刷新页面状态;失败显示 error(duration=0);网络异常显示重试按钮 +- **弹窗表单(组建委员会/设置标准)额外约束**: + - **H1 防重复提交(强制)**: 弹窗内确认按钮 loading+disabled + - **H2 超时控制(强制)**: 提交(POST)超时 30s + - **H4 脏数据检测(强制)**: 弹窗内数据修改后尝试关闭时拦截确认 + - **H8 反馈机制(建议)**: 成功关闭弹窗 + 刷新 + success(2s);失败 error(0) + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 评标步骤指示 | el-steps | :active=当前步骤,:space=200,finish-status="success" | +| 评标委员会区 | el-card | shadow="hover",含评标人员列表 | +| 组建评标委员会按钮 | el-button | type="primary",icon="User",size="small" | +| 评标人员展示 | el-tag | v-for遍历评标人员,closable | +| 评分标准区 | el-card | shadow="hover",含各维度权重展示 | +| 设置评分标准按钮 | el-button | type="primary",icon="Setting",size="small" | +| 评分录入表格 | el-table | :data=供应商评分数据,stripe,border,show-summary | +| 商务分/技术分/价格分 | el-input-number | :min=0,:max=100,:precision=1,controls-position="right",size="small" | +| 总分列 | 自动计算 | :formatter根据权重加权计算 | +| 排名列 | 自动计算 | 根据总分降序排列 | +| 提交评标结果按钮 | el-button | type="primary",icon="Check",:loading提交中 | +| 组建评标委员会弹窗 | el-dialog | title="组建评标委员会",width="600px" | +| 评标人员选择 | el-select | multiple,filterable,placeholder="请选择评标人员" | +| 设置评分标准弹窗 | el-dialog | title="设置评分标准",width="500px" | +| 权重输入 | el-slider | :min=0,:max=100,show-input,:marks="{0:'0%', 50:'50%', 100:'100%'}" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 评标人员 | required,≥3人 | 请选择评标人员 / 评标委员会至少3人 | +| 商务评分权重 | required,0-100 | 请设置商务评分权重 | +| 技术评分权重 | required,0-100 | 请设置技术评分权重 | +| 价格评分权重 | required,0-100 | 请设置价格评分权重 | +| 权重总和 | =100% | 评分权重之和必须等于100% | +| 商务分/技术分/价格分 | required,0-100 | 请输入评分 / 评分范围为0-100 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 评标委员会与评分标准左右并排,评分录入表格全宽 | +| 1024-1279px(Pad横屏) | 评标委员会与评分标准上下排列,评分录入表格全宽,评分输入列宽缩小 | +| 768-1023px(Pad竖屏) | 全部区域纵向堆叠,评分录入表格横向滚动,弹窗全屏宽度 | + --- ## 页面7:定标审批页 @@ -241,6 +581,58 @@ |------|----------|------|----------|------| | 定标审批 | bidding:award:approve | 行操作 | 状态=待定标 | 通过/驳回 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用待定标项目列表API → 渲染列表 +2. **查询/筛选交互流程**:支持按标段名称、状态筛选 +3. **表单填写与提交流程**:点击"定标审批" → 弹出审批弹窗(查看评标结果摘要,选择通过/驳回)→ 驳回需填写原因 → 确认后调用API → 刷新列表 +4. **弹窗/抽屉交互流程**:定标审批弹窗(el-dialog,含评标结果摘要展示、通过/驳回选择、驳回原因输入) +5. **行内操作流程**:仅状态=待定标时显示"定标审批"按钮 +6. **异常与错误处理**:审批操作失败显示el-message错误提示 +7. **联动/级联交互**:审批结果与列表状态联动(通过→公示中,驳回→评标中) +8. **权限控制交互表现**:无审批权限隐藏"定标审批"按钮 + +### 前端硬性约束 + +- **H1 防重复提交(强制)**: "定标审批"行操作按钮点击后置 loading+disabled 状态;弹窗内确认按钮同样 loading+disabled +- **H2 超时控制(强制)**: 列表查询(GET)超时 15s;定标审批(POST)超时 30s;超过阈值提示"请求超时,请稍后重试" +- **H3 操作确认(强制)**: "定标审批-通过"前弹出 confirm("确定要通过该定标审批?通过后将进入公示阶段。", type='warning');"定标审批-驳回"前弹出 confirm + 驳回原因输入框(必填),含影响说明:"驳回后需重新评标" +- **H5 权限隔离(建议)**: 返回空列表展示"暂无待定标项目";code=403 时展示"无权限访问" +- **H8 反馈机制(建议)**: 操作成功 success(duration=2s)+ 静默刷新列表;失败 error(duration=0);网络异常显示重试按钮 +- **弹窗表单(定标审批)额外约束**: + - **H1 防重复提交(强制)**: 弹窗内确认按钮 loading+disabled + - **H2 超时控制(强制)**: 提交(POST)超时 30s + - **H3 操作确认(强制)**: 已由外层 H3 覆盖 + - **H4 脏数据检测(强制)**: 审批结果/驳回原因修改后尝试关闭弹窗时拦截确认 + - **H8 反馈机制(建议)**: 成功关闭弹窗 + 刷新 + success(2s);失败 error(0) + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 定标列表 | el-table | stripe,border | +| 状态列 | el-tag | :type根据状态配色(待定标=warning,公示中=success,评标中=info) | +| 定标审批按钮 | el-button | type="primary",size="small",link样式 | +| 定标审批弹窗 | el-dialog | title="定标审批",width="600px" | +| 评标结果摘要 | el-descriptions | :column=1,border,展示中标候选人、评分等 | +| 审批结果 | el-radio-group | 通过/驳回单选 | +| 驳回原因 | el-input | type="textarea",:rows=3,v-if="审批结果=驳回" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 审批结果 | required | 请选择审批结果 | +| 驳回原因 | required(审批驳回时) | 请输入驳回原因 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 列表全字段展示,审批弹窗600px宽度 | +| 1024-1279px(Pad横屏) | 列表隐藏次要字段,审批弹窗80%宽度 | +| 768-1023px(Pad竖屏) | 列表仅显示核心字段,审批弹窗全屏宽度 | + --- ## 页面8:中标公示页 @@ -267,6 +659,48 @@ |------|----------|------|----------|------| | 生成合同 | contract:list:create | 行操作 | 状态=已生效 | 跳转合同管理创建合同 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用中标公示列表API → 渲染列表 +2. **查询/筛选交互流程**:支持按标段名称、状态筛选 +3. **表单填写与提交流程**:点击"生成合同"(状态=已生效)→ 二次确认后跳转合同录入页,自动填充标段信息和中标供应商 +4. **弹窗/抽屉交互流程**:生成合同确认弹窗(el-message-box,提示"确认生成合同?将跳转合同管理创建合同") +5. **行内操作流程**:仅状态=已生效时显示"生成合同"按钮 +6. **异常与错误处理**:生成合同跳转失败显示el-message错误提示;列表为空显示el-empty +7. **联动/级联交互**:公示状态与操作按钮联动(仅已生效显示生成合同按钮) +8. **权限控制交互表现**:无合同创建权限隐藏"生成合同"按钮 + +### 前端硬性约束 + +- **H1 防重复提交(强制)**: "生成合同"行操作按钮点击后置 loading+disabled 状态;操作期间禁用该行其他操作 +- **H2 超时控制(强制)**: 列表查询(GET)超时 15s;超过阈值提示"请求超时,请稍后重试" +- **H3 操作确认(强制)**: "生成合同"前弹出 confirm("确认生成合同?将跳转至合同管理并自动填充中标信息。", type='info') +- **H5 权限隔离(建议)**: 返回空列表展示"暂无中标公示记录";code=403 时展示"无权限访问" +- **H8 反馈机制(建议)**: 跳转成功无需提示(已跳转);跳转失败显示 error(duration=0);网络异常显示重试按钮;列表加载静默刷新 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 公示列表 | el-table | stripe,border | +| 状态列 | el-tag | :type根据状态配色(公示中=warning,已生效=success) | +| 生成合同按钮 | el-button | type="primary",size="small",link样式,icon="Document" | +| 生成合同确认弹窗 | el-message-box | title="确认生成合同",type="info",confirm-button-text="确认生成" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| (无表单校验) | — | — | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 列表全字段展示,操作列右对齐 | +| 1024-1279px(Pad横屏) | 隐藏"公示开始日/公示结束日"列,操作列固定右侧 | +| 768-1023px(Pad竖屏) | 仅显示标段名称/中标供应商/状态/操作核心字段 | + --- ## 需求追溯 diff --git a/docs/02-功能清单-医院/03-服务监督.md b/docs/02-功能清单-医院/03-服务监督.md index cdbea38..ab299f8 100644 --- a/docs/02-功能清单-医院/03-服务监督.md +++ b/docs/02-功能清单-医院/03-服务监督.md @@ -3,6 +3,7 @@ > 模块编码:service-supervision > 端侧:Web专属(仅医院账号) > 关联文档:01-模块划分 §4.3 / 02-功能清单-医院 §3 / 03-业务流转逻辑-医院 §3 / 05-接口规范 §9.2 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -99,6 +100,67 @@ | 列表查询 | /api/v1/repair-orders | GET | 只读,自动过滤关联物业公司 | | 详情查询 | /api/v1/repair-orders/{id} | GET | 只读 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用报修工单列表API(自动过滤关联物业公司)→ 渲染统计卡片 + 列表 +2. **查询/筛选交互流程**:填写筛选条件 → 点击"查询"按钮 → 调用API携带筛选参数 → 刷新列表和统计卡片;点击"重置"清空条件重新加载 +3. **表单填写与提交流程**:不适用(只读页面,无任何操作按钮) +4. **弹窗/抽屉交互流程**:不适用(查看详情跳转只读详情页) +5. **行内操作流程**:点击工单号或"查看详情" → 跳转只读工单详情页(标签页:基本信息/流转记录/照片附件,底部无操作按钮) +6. **异常与错误处理**:API请求失败显示el-message错误提示;列表为空显示el-empty;无权限时提示"您无权查看此数据" +7. **联动/级联交互**:筛选条件变化后统计卡片数据同步刷新;所属区域级联选择(楼栋→楼层→房间) +8. **权限控制交互表现**:医院账号仅查看,所有操作按钮隐藏;仅显示关联物业公司的数据 + +### 前端硬性约束 + +> 以下为本页面必须遵守的前端交互与体验硬性约束(H=Hard Constraint)。 + +| 编号 | 约束项 | 级别 | 本页要求 | +|------|--------|------|----------| +| **H1** | 防重复提交 | **强制** | 列表查询/翻页期间显示loading状态并禁用查询/分页控件;切换分页时abort上一页未完成请求;"查看详情"操作防重复点击 | +| **H2** | 请求超时 | **强制** | GET列表请求设置15s超时;GET详情请求设置15s超时 | +| **H3** | 操作确认 | 强制(不适用) | 本页为只读查看页面,无删除/停用等危险操作,不涉及 | +| **H4** | 脏数据保护 | 强制(不适用) | 本页无编辑表单,不涉及脏数据检测 | +| **H5** | 权限隔离表现 | 建议 | 医院账号仅可见关联物业公司数据范围;无`repair:list:view`权限时隐藏列表及查询区域;权限不足时展示空状态或无权提示而非报错 | +| **H6** | 批量操作限制 | 建议(不适用) | 本页为只读页面,无批量操作,不涉及 | +| **H7** | 文件上传约束 | 建议(不适用) | 本页无文件上传功能,不涉及 | +| **H8** | 反馈规范 | 建议 | 成功提示自动2s后消失(silent刷新);错误提示需手动关闭(duration=0);网络异常时展示重试按钮 | + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 工单号输入 | el-input | placeholder="请输入工单号",clearable | +| 状态选择 | el-select | placeholder="请选择状态",clearable,multiple | +| 报修类型选择 | el-select | placeholder="请选择报修类型",clearable | +| 紧急程度选择 | el-select | placeholder="请选择紧急程度",clearable | +| 提交日期范围 | el-date-picker | type="daterange",start-placeholder="开始日期",end-placeholder="结束日期" | +| 所属班组选择 | el-select | placeholder="请选择班组",clearable | +| 所属区域 | el-cascader | placeholder="请选择区域",:props="{checkStrictly: true}",级联:楼栋/楼层/房间 | +| 查询按钮 | el-button | type="primary",icon="Search" | +| 重置按钮 | el-button | type="default",icon="Refresh" | +| 统计卡片 | el-card | v-for遍历4个指标,shadow="hover",含数字和标签 | +| 工单列表 | el-table | stripe,border,:default-sort="{prop: 'submitTime', order: 'descending'}" | +| 紧急程度列 | el-tag | :type根据程度配色(紧急=danger,较急=warning,一般=info) | +| 状态列 | el-tag | :type根据状态配色 | +| 工单号列 | el-link | type="primary",:underline=false,@click跳转详情 | +| 查看详情按钮 | el-button | type="default",size="small",link样式,icon="View" | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next",:page-sizes="[10,20,50]" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| (无表单校验,仅查询条件) | — | — | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区双行排列,统计卡片横排4列,列表全字段展示 | +| 1024-1279px(Pad横屏) | 查询条件区三行排列,统计卡片2×2网格,列表隐藏"报修人""维修人员"列 | +| 768-1023px(Pad竖屏) | 查询条件区纵向堆叠(仅保留工单号/状态/报修类型),统计卡片2×2网格,列表仅显示工单号/类型/状态/提交时间/操作 | + --- ## 页面2:巡检数据查看页 @@ -126,6 +188,56 @@ |------|----------|------|----------|------| | 查看详情 | inspection:task:view(只读) | 行操作 | 始终 | 只读 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用巡检数据列表API(自动过滤关联物业公司)→ 渲染列表 +2. **查询/筛选交互流程**:支持按巡检人员、计划名称、打卡方式、状态筛选 +3. **表单填写与提交流程**:不适用(只读页面) +4. **弹窗/抽屉交互流程**:点击"查看详情" → 跳转只读巡检详情页 +5. **行内操作流程**:点击"查看详情"跳转只读详情页;异常数>0时该行标记橙色 +6. **异常与错误处理**:API请求失败显示el-message错误提示;列表为空显示el-empty +7. **联动/级联交互**:状态筛选与列表数据联动 +8. **权限控制交互表现**:医院账号仅查看,无操作按钮 + +### 前端硬性约束 + +> 以下为本页面必须遵守的前端交互与体验硬性约束(H=Hard Constraint)。 + +| 编号 | 约束项 | 级别 | 本页要求 | +|------|--------|------|----------| +| **H1** | 防重复提交 | **强制** | 列表查询/筛选/翻页期间显示loading状态并禁用相关控件;切换分页时abort上一页未完成请求;"查看详情"操作防重复点击 | +| **H2** | 请求超时 | **强制** | GET列表请求设置15s超时;GET详情请求设置15s超时 | +| **H3** | 操作确认 | 强制(不适用) | 本页为只读查看页面,无删除/停用等危险操作,不涉及 | +| **H4** | 脏数据保护 | 强制(不适用) | 本页无编辑表单,不涉及脏数据检测 | +| **H5** | 权限隔离表现 | 建议 | 仅展示本医院关联物业公司的巡检数据;无`inspection:task:view`权限时隐藏整个列表区域 | +| **H6** | 批量操作限制 | 建议(不适用) | 本页为只读页面,无批量操作,不涉及 | +| **H7** | 文件上传约束 | 建议(不适用) | 本页无文件上传功能,不涉及 | +| **H8** | 反馈规范 | 建议 | 成功提示自动2s后消失(silent刷新);错误提示需手动关闭(duration=0);网络异常时展示重试按钮 | + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 巡检数据列表 | el-table | stripe,border,:row-class-name异常行高亮 | +| 打卡方式列 | el-tag | :type根据方式配色(蓝牙=success,手动=warning,补录=info) | +| 状态列 | el-tag | :type根据状态配色(正常=success,异常=danger) | +| 异常数列 | el-tag | v-if="异常数>0" type="danger",否则显示"0" | +| 查看详情按钮 | el-button | type="default",size="small",link样式,icon="View" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| (无表单校验,仅查询条件) | — | — | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 列表全字段展示 | +| 1024-1279px(Pad横屏) | 隐藏"打卡方式"列 | +| 768-1023px(Pad竖屏) | 仅显示巡检人员/计划名称/状态/异常数/操作核心字段 | + --- ## 页面3:保洁数据查看页 @@ -153,6 +265,55 @@ |------|----------|------|----------|------| | 查看详情 | cleaning:task:view(只读) | 行操作 | 始终 | 只读 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用保洁数据列表API(自动过滤关联物业公司)→ 渲染列表 +2. **查询/筛选交互流程**:支持按保洁人员、保洁区域、任务日期、完成状态筛选 +3. **表单填写与提交流程**:不适用(只读页面) +4. **弹窗/抽屉交互流程**:点击"查看详情" → 跳转只读保洁详情页 +5. **行内操作流程**:点击"查看详情"跳转只读详情页;超时/未执行状态行标记橙色;抽查不合格行标记红色 +6. **异常与错误处理**:API请求失败显示el-message错误提示;列表为空显示el-empty +7. **联动/级联交互**:完成状态筛选与列表数据联动 +8. **权限控制交互表现**:医院账号仅查看,无操作按钮 + +### 前端硬性约束 + +> 以下为本页面必须遵守的前端交互与体验硬性约束(H=Hard Constraint)。 + +| 编号 | 约束项 | 级别 | 本页要求 | +|------|--------|------|----------| +| **H1** | 防重复提交 | **强制** | 列表查询/筛选/翻页期间显示loading状态并禁用相关控件;切换分页时abort上一页未完成请求;"查看详情"操作防重复点击 | +| **H2** | 请求超时 | **强制** | GET列表请求设置15s超时;GET详情请求设置15s超时 | +| **H3** | 操作确认 | 强制(不适用) | 本页为只读查看页面,无删除/停用等危险操作,不涉及 | +| **H4** | 脏数据保护 | 强制(不适用) | 本页无编辑表单,不涉及脏数据检测 | +| **H5** | 权限隔离表现 | 建议 | 仅展示本医院关联物业公司的保洁数据;无`cleaning:task:view`权限时隐藏整个列表区域 | +| **H6** | 批量操作限制 | 建议(不适用) | 本页为只读页面,无批量操作,不涉及 | +| **H7** | 文件上传约束 | 建议(不适用) | 本页无文件上传功能,不涉及 | +| **H8** | 反馈规范 | 建议 | 成功提示自动2s后消失(silent刷新);错误提示需手动关闭(duration=0);网络异常时展示重试按钮 | + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 保洁数据列表 | el-table | stripe,border,:row-class-name超时/不合格行高亮 | +| 完成状态列 | el-tag | :type根据状态配色(已完成=success,超时=warning,未执行=danger) | +| 抽查结果列 | el-tag | :type根据结果配色(合格=success,不合格=danger,未抽查=info) | +| 查看详情按钮 | el-button | type="default",size="small",link样式,icon="View" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| (无表单校验,仅查询条件) | — | — | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 列表全字段展示 | +| 1024-1279px(Pad横屏) | 隐藏"打卡方式"列 | +| 768-1023px(Pad竖屏) | 仅显示保洁人员/保洁区域/完成状态/抽查结果/操作核心字段 | + --- ## 需求追溯 diff --git a/docs/02-功能清单-医院/04-服务评价.md b/docs/02-功能清单-医院/04-服务评价.md index 67bc7af..6dd439e 100644 --- a/docs/02-功能清单-医院/04-服务评价.md +++ b/docs/02-功能清单-医院/04-服务评价.md @@ -3,6 +3,7 @@ > 模块编码:evaluation > 端侧:Web专属(仅医院账号) > 关联文档:01-模块划分 §4.4 / 02-功能清单-医院 §4 / 03-业务流转逻辑-医院 §4 / 05-接口规范 §9.2 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -85,6 +86,49 @@ |----------|---------|------|------| | 发起评价 | /api/v1/evaluations | POST | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 渲染空评价表单 → 加载下拉选项(物业公司列表、服务类型) +2. **查询/筛选交互流程**:不适用 +3. **表单填写与提交流程**:选择评价类型 → 选择物业公司 → 选择关联服务(综合评价时跳过)→ 评分(1-5星)→ 填写留言(可选)→ 上传图片(可选,≤5张)→ 点击"提交评价" → 校验通过后调用API → 提交成功提示"评价提交成功",低评分自动触发通知 +4. **弹窗/抽屉交互流程**:不适用(表单在页面内直接展示) +5. **行内操作流程**:选择评价类型后联动显示对应的服务选择项;评分选择后显示对应文字描述 +6. **异常与错误处理**:必填项未填时表单校验不通过;提交失败显示el-message错误提示;图片上传失败提示"图片上传失败" +7. **联动/级联交互**:评价类型选择→关联物业公司选择→关联服务选择(级联);评价类型为"综合评价"时隐藏关联服务选择;物业公司选择后动态加载该物业的服务数据 +8. **权限控制交互表现**:仅医院账号可发起评价;无评价权限时隐藏"提交评价"按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 评价类型 | el-radio-group | v-for遍历4种类型,@change联动关联服务显示 | +| 关联物业公司 | el-select | placeholder="请选择物业公司",filterable,@change加载服务数据 | +| 关联服务 | el-select | placeholder="请选择关联服务",filterable,v-if="非综合评价" | +| 评分 | el-rate | :max=5,show-text,:texts="['非常不满意','不满意','一般','满意','非常满意']",allow-half=false | +| 留言 | el-input | type="textarea",:rows=4,maxlength=500,show-word-limit,placeholder="请输入评价内容" | +| 图片上传 | el-upload | action="/api/v1/files/upload",:limit=5,accept=".jpg,.jpeg,.png",list-type="picture-card",:on-preview预览 | +| 取消按钮 | el-button | type="default",@click返回上一页 | +| 提交评价按钮 | el-button | type="primary",:loading提交中 | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 评价类型 | required | 请选择评价类型 | +| 关联物业公司 | required | 请选择关联物业公司 | +| 关联服务 | required(非综合评价时) | 请选择关联服务 | +| 评分 | required | 请选择评分 | +| 留言 | maxlength=500 | 评价内容不能超过500个字符 | +| 图片 | ≤5张 | 图片数量不能超过5张 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 表单居中展示,宽度800px,评分与文字描述水平排列 | +| 1024-1279px(Pad横屏) | 表单宽度90%,评分与文字描述水平排列 | +| 768-1023px(Pad竖屏) | 表单宽度100%,评分与文字描述纵向堆叠,图片上传区全宽 | + --- ## 页面2:评价汇总查看页 @@ -123,6 +167,42 @@ |----------|---------|------|------| | 汇总数据 | /api/v1/evaluations/summary | GET | 本医院发起的评价汇总 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认加载本月数据 → 调用评价汇总API → 渲染统计卡片 + 图表 +2. **查询/筛选交互流程**:选择时间范围(本月/本季度/本年/自定义)→ 刷新汇总数据和图表 +3. **表单填写与提交流程**:不适用(只读页面) +4. **弹窗/抽屉交互流程**:不适用 +5. **行内操作流程**:图表支持交互(折线图hover显示具体数据,柱状图点击可查看物业公司详情) +6. **异常与错误处理**:API请求失败显示el-message错误提示;无数据时图表显示"暂无数据" +7. **联动/级联交互**:时间范围选择后所有统计卡片和图表同步刷新 +8. **权限控制交互表现**:仅医院账号可查看本医院发起的评价汇总 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 时间选择 | el-radio-group | 本月/本季度/本年/自定义,@change刷新数据 | +| 自定义日期 | el-date-picker | type="daterange",v-if="自定义" | +| 统计卡片 | el-card | shadow="hover",v-for遍历3个指标,含数字和标签 | +| 评分趋势图 | echarts | 折线图,:xAxis=时间,:yAxis=平均分,tooltip,dataZoom | +| 各物业公司评分对比图 | echarts | 柱状图,:xAxis=物业公司,:yAxis=平均分,tooltip | +| 星级分布图 | echarts | 饼图,:data=各星级占比,tooltip,legend | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 自定义日期范围 | 结束日期≥开始日期 | 结束日期不能早于开始日期 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 统计卡片横排3列,折线图与柱状图左右并排,饼图全宽居中 | +| 1024-1279px(Pad横屏) | 统计卡片横排3列,折线图与柱状图上下排列,饼图全宽 | +| 768-1023px(Pad竖屏) | 统计卡片纵向堆叠,所有图表纵向堆叠,ECharts图表宽度100%自适应,高度300px | + --- ## 页面3:评价列表查看页 @@ -173,6 +253,66 @@ | 列表查询 | /api/v1/evaluations | GET | 本医院发起的评价 | | 详情 | /api/v1/evaluations/{id} | GET | 含物业回复 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用评价列表API → 渲染列表,默认按评价时间倒序 +2. **查询/筛选交互流程**:选择评价类型/评分/物业公司/日期范围 → 点击"查询"按钮 → 刷新列表 +3. **表单填写与提交流程**:不适用(只读页面) +4. **弹窗/抽屉交互流程**:点击"查看详情" → 弹出评价详情弹窗(含评价内容、图片、物业回复) +5. **行内操作流程**:点击"查看详情"弹出详情弹窗;评分列显示星级;回复状态列标签化 +6. **异常与错误处理**:API请求失败显示el-message错误提示;列表为空显示el-empty +7. **联动/级联交互**:筛选条件变化后分页重置为第1页 +8. **权限控制交互表现**:医院账号仅查看本医院发起的评价 + +### 前端硬性约束 + +> 以下为本页面必须遵守的前端交互与体验硬性约束(H=Hard Constraint)。 + +| 编号 | 约束项 | 级别 | 本页要求 | +|------|--------|------|----------| +| **H1** | 防重复提交 | **强制** | 列表查询/翻页期间显示loading状态并禁用查询/分页控件;切换分页时abort上一页未完成请求;"查看详情"弹窗防重复弹出 | +| **H2** | 请求超时 | **强制** | GET列表请求设置15s超时;GET详情请求设置15s超时 | +| **H3** | 操作确认 | 强制(不适用) | 本页为只读列表查看页面,无删除/停用等危险操作,不涉及 | +| **H4** | 脏数据保护 | 强制(不适用) | 本页无编辑表单,不涉及脏数据检测 | +| **H5** | 权限隔离表现 | 建议 | 仅展示本医院发起的评价数据;无`evaluation:list:view`权限时隐藏列表区域;不同角色可见的数据范围通过后端过滤体现 | +| **H6** | 批量操作限制 | 建议(不适用) | 本页为只读列表页面,无批量操作,不涉及 | +| **H7** | 文件上传约束 | 建议(不适用) | 本页无文件上传功能,不涉及 | +| **H8** | 反馈规范 | 建议 | 成功提示自动2s后消失(silent刷新);错误提示需手动关闭(duration=0);详情弹窗内图片预览成功/失败分别处理反馈 | + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 评价类型选择 | el-select | placeholder="请选择评价类型",clearable | +| 评分选择 | el-select | placeholder="请选择评分",clearable,options: 1-5分 | +| 物业公司选择 | el-select | placeholder="请选择物业公司",clearable,filterable | +| 日期范围 | el-date-picker | type="daterange",start-placeholder="开始日期",end-placeholder="结束日期" | +| 查询按钮 | el-button | type="primary",icon="Search" | +| 重置按钮 | el-button | type="default",icon="Refresh" | +| 评价列表 | el-table | stripe,border,:default-sort="{prop: 'evaluationTime', order: 'descending'}" | +| 评分列 | el-rate | :max=5,disabled,show-score | +| 回复状态列 | el-tag | :type根据状态配色(已回复=success,未回复=warning) | +| 查看详情按钮 | el-button | type="default",size="small",link样式,icon="View" | +| 评价详情弹窗 | el-dialog | title="评价详情",width="600px" | +| 评价内容展示 | el-descriptions | :column=1,border | +| 评价图片 | el-image | v-for遍历,:preview-src-list预览 | +| 物业回复展示 | el-card | shadow="never",含回复内容和回复时间 | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next",:page-sizes="[10,20,50]" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| (无表单校验,仅查询条件) | — | — | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区单行排列,列表全字段展示,详情弹窗600px | +| 1024-1279px(Pad横屏) | 查询条件区双行排列,隐藏"评价内容""物业回复"列(查看详情),详情弹窗80%宽度 | +| 768-1023px(Pad竖屏) | 查询条件区纵向堆叠(仅保留评价类型/评分/物业公司),列表显示核心字段(评价类型/物业公司/评分/回复状态/操作),详情弹窗全屏宽度 | + --- ## 需求追溯 diff --git a/docs/02-功能清单-医院/05-统计报表.md b/docs/02-功能清单-医院/05-统计报表.md index b0db996..7701f5d 100644 --- a/docs/02-功能清单-医院/05-统计报表.md +++ b/docs/02-功能清单-医院/05-统计报表.md @@ -3,6 +3,7 @@ > 模块编码:statistics > 端侧:Web专属(仅医院账号) > 关联文档:01-模块划分 §4.5 / 02-功能清单-医院 §5 / 03-业务流转逻辑-医院 §5 / 05-接口规范 §9.2 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -39,12 +40,66 @@ | 查看 | statistics:repair:view | 仅查看,不可导出 | | 导出 | ❌ 不支持 | 业务数据报表仅查看(来源:03-医院 §5) | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认加载本月数据 → 调用报修统计API → 渲染统计卡片 + 图表 +2. **查询/筛选交互流程**:选择时间范围(本月/本季度/本年/自定义)→ 刷新统计数据和图表;支持按物业公司筛选 +3. **表单填写与提交流程**:不适用(只读页面,不支持导出) +4. **弹窗/抽屉交互流程**:不适用 +5. **行内操作流程**:图表支持交互(饼图hover显示各状态数量,柱状图hover显示各班组工单量,折线图hover显示趋势数据) +6. **异常与错误处理**:API请求失败显示el-message错误提示;无数据时图表显示"暂无数据" +7. **联动/级联交互**:时间范围/物业公司筛选后所有统计卡片和图表同步刷新 +8. **权限控制交互表现**:仅查看权限,无导出按钮;无查看权限时隐藏整个页面内容 + +### 前端硬性约束 + +> 以下为本页面必须遵守的前端交互与体验硬性约束(H=Hard Constraint)。 + +| 编号 | 约束项 | 级别 | 本页要求 | +|------|--------|------|----------| +| **H1** | 防重复提交 | **强制** | 切换时间范围/物业公司筛选加载数据期间显示loading状态(skeleton)并禁用筛选控件;防止重复请求 | +| **H2** | 请求超时 | **强制** | GET统计请求设置**30s超时**(统计报表类接口数据量大,使用30s档位) | +| **H3** | 操作确认 | 强制(不适用) | 本页为只读统计页面,无删除/停用等危险操作,不涉及 | +| **H4** | 脏数据保护 | 强制(不适用) | 本页无编辑表单,不涉及脏数据检测 | +| **H5** | 权限隔离表现 | 建议 | 无`statistics:repair:view`权限时隐藏整个页面或展示无权提示;物业公司筛选下拉仅展示有权限的数据范围 | +| **H6** | 批量操作限制 | 建议(不适用) | 本页为只读统计页面,无批量操作,不涉及 | +| **H7** | 文件上传约束 | 建议(不适用) | 本页不支持导出且无文件上传功能,不涉及 | +| **H8** | 反馈规范 | 建议 | 数据加载成功后silent刷新图表和卡片;API失败时在对应区域展示错误状态而非全局toast;网络异常时展示重试按钮 | + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 时间选择 | el-radio-group | 本月/本季度/本年/自定义,@change刷新数据 | +| 自定义日期 | el-date-picker | type="daterange",v-if="自定义" | +| 物业公司筛选 | el-select | placeholder="请选择物业公司",clearable,filterable | +| 统计卡片区 | el-row + el-col | :gutter=20,v-for遍历6个指标,每个el-card shadow="hover" | +| 各状态分布图 | echarts | 饼图,:data=各状态数量,tooltip,legend,color预设配色 | +| 各班组工单量图 | echarts | 柱状图,:xAxis=班组,:yAxis=工单数,tooltip | +| 完成率/紧急占比 | echarts | 仪表盘图/环形图,tooltip | +| 平均处理时长 | echarts | 折线图(时间趋势),:xAxis=时间,:yAxis=小时 | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 自定义日期范围 | 结束日期≥开始日期 | 结束日期不能早于开始日期 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 统计卡片3×2网格,图表区2列布局(饼图+柱状图并排),下方折线图全宽 | +| 1024-1279px(Pad横屏) | 统计卡片3×2网格,图表区上下排列,ECharts图表宽度100% | +| 768-1023px(Pad竖屏) | 统计卡片2×3网格,所有图表纵向堆叠,ECharts图表宽度100%,高度250px,支持手势缩放 | + --- ## 页面2:巡检统计页(只读) **页面编号**:HO-ST-02-P01 **端侧归属**:Web专属 +**页面路径**:/statistics/inspection ### 统计指标 @@ -56,12 +111,49 @@ | 蓝牙打卡率 | — | | 补录率 | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认加载本月数据 → 调用巡检统计API → 渲染统计卡片 + 图表 +2. **查询/筛选交互流程**:选择时间范围(本月/本季度/本年/自定义)→ 刷新统计数据和图表 +3. **表单填写与提交流程**:不适用(只读页面,不支持导出) +4. **弹窗/抽屉交互流程**:不适用 +5. **行内操作流程**:图表支持交互(饼图hover显示完成/异常分布,柱状图hover显示各区域巡检量) +6. **异常与错误处理**:API请求失败显示el-message错误提示;无数据时图表显示"暂无数据" +7. **联动/级联交互**:时间范围筛选后所有统计卡片和图表同步刷新 +8. **权限控制交互表现**:仅查看权限,无导出按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 时间选择 | el-radio-group | 本月/本季度/本年/自定义,@change刷新数据 | +| 自定义日期 | el-date-picker | type="daterange",v-if="自定义" | +| 统计卡片区 | el-row + el-col | :gutter=20,v-for遍历5个指标 | +| 完成率/异常率图 | echarts | 饼图/环形图,:data=完成/异常分布 | +| 蓝牙打卡率/补录率图 | echarts | 柱状图,:xAxis=时间段,:yAxis=百分比 | +| 巡检趋势图 | echarts | 折线图,:xAxis=时间,:yAxis=任务数 | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 自定义日期范围 | 结束日期≥开始日期 | 结束日期不能早于开始日期 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 统计卡片横排5列,图表区2列布局 | +| 1024-1279px(Pad横屏) | 统计卡片3+2排列,图表区上下排列 | +| 768-1023px(Pad竖屏) | 统计卡片2×3网格,所有图表纵向堆叠,ECharts图表宽度100%,高度250px,支持手势缩放 | + --- ## 页面3:保洁统计页(只读) **页面编号**:HO-ST-03-P01 **端侧归属**:Web专属 +**页面路径**:/statistics/cleaning ### 统计指标 @@ -72,12 +164,49 @@ | 超时率 | — | | 抽查合格率 | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认加载本月数据 → 调用保洁统计API → 渲染统计卡片 + 图表 +2. **查询/筛选交互流程**:选择时间范围(本月/本季度/本年/自定义)→ 刷新统计数据和图表 +3. **表单填写与提交流程**:不适用(只读页面,不支持导出) +4. **弹窗/抽屉交互流程**:不适用 +5. **行内操作流程**:图表支持交互(柱状图hover显示各区域保洁完成量,折线图hover显示超时率趋势) +6. **异常与错误处理**:API请求失败显示el-message错误提示;无数据时图表显示"暂无数据" +7. **联动/级联交互**:时间范围筛选后所有统计卡片和图表同步刷新 +8. **权限控制交互表现**:仅查看权限,无导出按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 时间选择 | el-radio-group | 本月/本季度/本年/自定义,@change刷新数据 | +| 自定义日期 | el-date-picker | type="daterange",v-if="自定义" | +| 统计卡片区 | el-row + el-col | :gutter=20,v-for遍历4个指标 | +| 完成率/超时率图 | echarts | 饼图/环形图,:data=完成/超时分布 | +| 抽查合格率图 | echarts | 仪表盘图,:min=0,:max=100 | +| 各区域保洁量图 | echarts | 柱状图,:xAxis=区域,:yAxis=任务数 | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 自定义日期范围 | 结束日期≥开始日期 | 结束日期不能早于开始日期 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 统计卡片横排4列,图表区2列布局 | +| 1024-1279px(Pad横屏) | 统计卡片2×2排列,图表区上下排列 | +| 768-1023px(Pad竖屏) | 统计卡片2×2网格,所有图表纵向堆叠,ECharts图表宽度100%,高度250px,支持手势缩放 | + --- ## 页面4:评价统计页(只读) **页面编号**:HO-ST-04-P01 **端侧归属**:Web专属 +**页面路径**:/statistics/evaluation ### 统计指标 @@ -89,6 +218,59 @@ | 低评分占比 | — | | 物业回复率 | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认加载本月数据 → 调用评价统计API → 渲染统计卡片 + 图表 +2. **查询/筛选交互流程**:选择时间范围(本月/本季度/本年/自定义)→ 刷新统计数据和图表;支持按物业公司筛选 +3. **表单填写与提交流程**:不适用(只读页面,不支持导出) +4. **弹窗/抽屉交互流程**:不适用 +5. **行内操作流程**:图表支持交互(柱状图hover显示各物业公司评分,饼图hover显示星级分布,折线图hover显示评分趋势) +6. **异常与错误处理**:API请求失败显示el-message错误提示;无数据时图表显示"暂无数据" +7. **联动/级联交互**:时间范围/物业公司筛选后所有统计卡片和图表同步刷新 +8. **权限控制交互表现**:仅查看权限,无导出按钮 + +### 前端硬性约束 + +> 以下为本页面必须遵守的前端交互与体验硬性约束(H=Hard Constraint)。 + +| 编号 | 约束项 | 级别 | 本页要求 | +|------|--------|------|----------| +| **H1** | 防重复提交 | **强制** | 切换时间范围/物业公司筛选加载数据期间显示loading状态(skeleton)并禁用筛选控件;防止重复请求 | +| **H2** | 请求超时 | **强制** | GET统计请求设置**30s超时**(统计报表类接口数据量大,使用30s档位) | +| **H3** | 操作确认 | 强制(不适用) | 本页为只读统计页面,无删除/停用等危险操作,不涉及 | +| **H4** | 脏数据保护 | 强制(不适用) | 本页无编辑表单,不涉及脏数据检测 | +| **H5** | 权限隔离表现 | 建议 | 无`statistics:evaluation:view`权限时隐藏整个页面或展示无权提示;物业公司筛选仅展示有权限范围 | +| **H6** | 批量操作限制 | 建议(不适用) | 本页为只读统计页面,无批量操作,不涉及 | +| **H7** | 文件上传约束 | 建议(不适用) | 本页不支持导出且无文件上传功能,不涉及 | +| **H8** | 反馈规范 | 建议 | 数据加载成功后silent刷新图表和卡片;API失败时在对应区域展示错误状态;网络异常时展示重试按钮 | + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 时间选择 | el-radio-group | 本月/本季度/本年/自定义,@change刷新数据 | +| 自定义日期 | el-date-picker | type="daterange",v-if="自定义" | +| 物业公司筛选 | el-select | placeholder="请选择物业公司",clearable,filterable | +| 统计卡片区 | el-row + el-col | :gutter=20,v-for遍历5个指标 | +| 各物业公司评分对比图 | echarts | 柱状图,:xAxis=物业公司,:yAxis=评分,color预设配色 | +| 星级分布图 | echarts | 饼图,:data=各星级占比,tooltip,legend | +| 评分趋势图 | echarts | 折线图,:xAxis=时间,:yAxis=平均分,dataZoom | +| 低评分占比图 | echarts | 环形图,:data=低评分/正常占比 | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 自定义日期范围 | 结束日期≥开始日期 | 结束日期不能早于开始日期 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 统计卡片横排5列,柱状图与饼图左右并排,折线图全宽 | +| 1024-1279px(Pad横屏) | 统计卡片3+2排列,所有图表上下排列 | +| 768-1023px(Pad竖屏) | 统计卡片2×3网格,所有图表纵向堆叠,ECharts图表宽度100%,高度250px,支持手势缩放 | + --- ## 页面5:合同统计页(可导出) @@ -115,12 +297,68 @@ | 查看 | statistics:contract:view | — | | 导出 | statistics:contract:export | 合同统计可导出(来源:03-医院 §5) | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认加载本月数据 → 调用合同统计API → 渲染统计卡片 + 图表 +2. **查询/筛选交互流程**:选择时间范围(本月/本季度/本年/自定义)→ 刷新统计数据和图表;支持按物业公司筛选 +3. **表单填写与提交流程**:不适用(只读查看,但支持导出) +4. **弹窗/抽屉交互流程**:点击"导出" → 选择导出格式(Excel/PDF)→ 调用导出API → 下载文件 +5. **行内操作流程**:图表支持交互;点击"导出"按钮下载报表 +6. **异常与错误处理**:API请求失败显示el-message错误提示;导出失败提示"导出失败,请重试";无数据时图表显示"暂无数据" +7. **联动/级联交互**:时间范围/物业公司筛选后所有统计卡片和图表同步刷新 +8. **权限控制交互表现**:有导出权限显示"导出"按钮;无导出权限隐藏 + +### 前端硬性约束 + +> 以下为本页面必须遵守的前端交互与体验硬性约束(H=Hard Constraint)。 + +| 编号 | 约束项 | 级别 | 本页要求 | +|------|--------|------|----------| +| **H1** | 防重复提交 | **强制** | 切换时间范围/物业公司筛选加载数据期间显示loading状态(skeleton)并禁用筛选控件;**导出期间显示loading并禁用"导出"按钮**,防止重复点击重复下载 | +| **H2** | 请求超时 | **强制** | GET统计请求设置**30s超时**(统计报表类接口数据量大);**POST导出请求设置60s超时**(导出文件可能较大) | +| **H3** | 操作确认 | 强制(不适用) | 本页为只读统计+导出页面,无删除/停用等危险操作,不涉及 | +| **H4** | 脏数据保护 | 强制(不适用) | 本页无编辑表单,不涉及脏数据检测 | +| **H5** | 权限隔离表现 | 建议 | 无`statistics:contract:view`权限时隐藏整个页面或展示无权提示;有`statistics:contract:export`权限才显示"导出"按钮及格式选择下拉 | +| **H6** | 批量操作限制 | 建议(不适用) | 本页为只读统计页面,无批量操作,不涉及 | +| **H7** | 文件上传约束 | 建议(不适用) | 本页为下载导出场景,无文件上传功能,不涉及 | +| **H8** | 反馈规范 | 建议 | 数据加载成功后silent刷新图表和卡片;**导出成功后显示success提示(2s)并触发浏览器下载**;导出失败显示error提示(duration=0)含重试选项;网络异常时展示重试按钮 | + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 时间选择 | el-radio-group | 本月/本季度/本年/自定义,@change刷新数据 | +| 自定义日期 | el-date-picker | type="daterange",v-if="自定义" | +| 物业公司筛选 | el-select | placeholder="请选择物业公司",clearable,filterable | +| 统计卡片区 | el-row + el-col | :gutter=20,v-for遍历6个指标 | +| 各物业合同分布图 | echarts | 柱状图,:xAxis=物业公司,:yAxis=合同数/金额,tooltip | +| 合同金额趋势图 | echarts | 折线图,:xAxis=时间,:yAxis=金额,dataZoom | +| 付款完成率图 | echarts | 环形图/仪表盘图 | +| 到期预警列表 | el-table | stripe,border,:data=即将到期合同 | +| 导出按钮 | el-button | type="success",icon="Download",@click导出 | +| 导出格式选择 | el-dropdown | 触发导出,:options="[{label: 'Excel'}, {label: 'PDF'}]" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 自定义日期范围 | 结束日期≥开始日期 | 结束日期不能早于开始日期 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 统计卡片3×2网格,柱状图与折线图左右并排,到期预警列表全宽 | +| 1024-1279px(Pad横屏) | 统计卡片3×2网格,图表上下排列,到期预警列表全宽 | +| 768-1023px(Pad竖屏) | 统计卡片2×3网格,所有图表纵向堆叠,到期预警列表隐藏次要列,ECharts图表宽度100%,高度250px,支持手势缩放 | + --- ## 页面6:招标统计页(可导出) **页面编号**:HO-ST-06-P01 **端侧归属**:Web专属 +**页面路径**:/statistics/bidding ### 统计指标 @@ -139,6 +377,59 @@ | 查看 | statistics:bidding:view | — | | 导出 | statistics:bidding:export | 招标统计可导出 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认加载本月数据 → 调用招标统计API → 渲染统计卡片 + 图表 +2. **查询/筛选交互流程**:选择时间范围(本月/本季度/本年/自定义)→ 刷新统计数据和图表 +3. **表单填写与提交流程**:不适用(只读查看,但支持导出) +4. **弹窗/抽屉交互流程**:点击"导出" → 选择导出格式(Excel/PDF)→ 调用导出API → 下载文件 +5. **行内操作流程**:图表支持交互;成本对比图hover显示预算vs中标金额详情 +6. **异常与错误处理**:API请求失败显示el-message错误提示;导出失败提示"导出失败,请重试";无数据时图表显示"暂无数据" +7. **联动/级联交互**:时间范围筛选后所有统计卡片和图表同步刷新 +8. **权限控制交互表现**:有导出权限显示"导出"按钮;无导出权限隐藏 + +### 前端硬性约束 + +> 以下为本页面必须遵守的前端交互与体验硬性约束(H=Hard Constraint)。 + +| 编号 | 约束项 | 级别 | 本页要求 | +|------|--------|------|----------| +| **H1** | 防重复提交 | **强制** | 切换时间范围加载数据期间显示loading状态(skeleton)并禁用时间选择控件;**导出期间显示loading并禁用"导出"按钮**,防止重复点击重复下载 | +| **H2** | 请求超时 | **强制** | GET统计请求设置**30s超时**(统计报表类接口数据量大);**POST导出请求设置60s超时**(导出文件可能较大) | +| **H3** | 操作确认 | 强制(不适用) | 本页为只读统计+导出页面,无删除/停用等危险操作,不涉及 | +| **H4** | 脏数据保护 | 强制(不适用) | 本页无编辑表单,不涉及脏数据检测 | +| **H5** | 权限隔离表现 | 建议 | 无`statistics:bidding:view`权限时隐藏整个页面或展示无权提示;有`statistics:bidding:export`权限才显示"导出"按钮及格式选择下拉 | +| **H6** | 批量操作限制 | 建议(不适用) | 本页为只读统计页面,无批量操作,不涉及 | +| **H7** | 文件上传约束 | 建议(不适用) | 本页为下载导出场景,无文件上传功能,不涉及 | +| **H8** | 反馈规范 | 建议 | 数据加载成功后silent刷新图表和卡片;**导出成功后显示success提示(2s)并触发浏览器下载**;导出失败显示error提示(duration=0)含重试选项;网络异常时展示重试按钮 | + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 时间选择 | el-radio-group | 本月/本季度/本年/自定义,@change刷新数据 | +| 自定义日期 | el-date-picker | type="daterange",v-if="自定义" | +| 统计卡片区 | el-row + el-col | :gutter=20,v-for遍历5个指标 | +| 成本对比图 | echarts | 柱状图(分组),:xAxis=标段,两组数据:预算/中标金额,tooltip | +| 各标段中标情况图 | echarts | 饼图/柱状图,:data=各标段中标状态 | +| 招标周期趋势图 | echarts | 折线图,:xAxis=项目,:yAxis=天数,tooltip | +| 导出按钮 | el-button | type="success",icon="Download",@click导出 | +| 导出格式选择 | el-dropdown | 触发导出,:options="[{label: 'Excel'}, {label: 'PDF'}]" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 自定义日期范围 | 结束日期≥开始日期 | 结束日期不能早于开始日期 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 统计卡片横排5列,成本对比图与中标情况图左右并排,周期趋势图全宽 | +| 1024-1279px(Pad横屏) | 统计卡片3+2排列,所有图表上下排列 | +| 768-1023px(Pad竖屏) | 统计卡片2×3网格,所有图表纵向堆叠,ECharts图表宽度100%,高度250px,支持手势缩放 | + --- ## 页面7:综合看板页 @@ -171,6 +462,57 @@ - 服务数据:物业公司业务数据(只读) - 合同/招标数据:本系统数据 +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认加载"今天"数据 → 并行调用各模块统计API → 渲染统计卡片 +2. **查询/筛选交互流程**:选择时间范围(今天/本周/本月/自定义)→ 刷新所有看板数据 +3. **表单填写与提交流程**:不适用(只读看板) +4. **弹窗/抽屉交互流程**:不适用 +5. **行内操作流程**:点击各统计卡片可跳转对应统计详情页(如点击"报修完成率"跳转报修统计页) +6. **异常与错误处理**:部分模块API失败时仅该模块显示"数据加载失败",其他模块正常展示;全部失败显示"看板数据加载失败" +7. **联动/级联交互**:时间范围切换后所有模块数据同步刷新 +8. **权限控制交互表现**:无查看权限的模块对应卡片灰显或隐藏 + +### 前端硬性约束 + +> 以下为本页面必须遵守的前端交互与体验硬性约束(H=Hard Constraint)。 + +| 编号 | 约束项 | 级别 | 本页要求 | +|------|--------|------|----------| +| **H1** | 防重复提交 | **强制** | 切换时间范围刷新数据期间显示loading状态(skeleton)并禁用时间选择控件;各模块并行加载时防止重复请求;卡片点击跳转防抖处理 | +| **H2** | 请求超时 | **强制** | 各模块GET统计请求均设置**30s超时**(综合看板涉及多模块并行调用,使用30s档位) | +| **H3** | 操作确认 | 强制(不适用) | 本页为只读综合看板页面,无删除/停用等危险操作,不涉及 | +| **H4** | 脏数据保护 | 强制(不适用) | 本页无编辑表单,不涉及脏数据检测 | +| **H5** | 权限隔离表现 | 建议 | 各统计卡片根据用户权限动态显隐或灰显;无某模块权限时该区域展示"暂无数据访问权";整体无看板权限时隐藏页面内容 | +| **H6** | 批量操作限制 | 建议(不适用) | 本页为只读看板页面,无批量操作,不涉及 | +| **H7** | 文件上传约束 | 建议(不适用) | 本页无文件上传功能,不涉及 | +| **H8** | 反馈规范 | 建议 | 各模块独立加载反馈,成功则silent渲染;**部分模块失败仅在该卡片区域展示错误状态,不影响其他正常模块**;全部失败时展示全局错误提示+重试按钮 | + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 时间选择 | el-radio-group | 今天/本周/本月/自定义,@change刷新数据 | +| 自定义日期 | el-date-picker | type="daterange",v-if="自定义" | +| 服务数据卡片组 | el-card | shadow="hover",:gutter=20,4个指标(报修完成率/巡检完成率/保洁完成率/评价均分),@click跳转详情 | +| 合同/招标数据卡片组 | el-card | shadow="hover",:gutter=20,4个指标(活跃合同/付款完成率/招标项目/即将到期合同),@click跳转详情 | +| 数值展示 | el-statistic | :precision=1(均分)/ 0(其他),prefix/suffix图标 | +| 加载状态 | el-skeleton | :loading=数据加载中,animated | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 自定义日期范围 | 结束日期≥开始日期 | 结束日期不能早于开始日期 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 两组卡片各横排4列,整体2行布局 | +| 1024-1279px(Pad横屏) | 两组卡片各2×2网格,整体2行布局 | +| 768-1023px(Pad竖屏) | 所有卡片纵向堆叠(8行1列),支持手势下拉刷新,卡片宽度100%,高度自适应 | + --- ## 页面8:自定义报表页 @@ -195,6 +537,63 @@ | 生成报表 | statistics:custom:view | — | | 导出 | statistics:custom:export | 合同/招标相关数据可导出,业务数据仅查看 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 渲染报表配置表单(数据源/维度/指标/时间范围选择) +2. **查询/筛选交互流程**:选择数据源 → 联动加载可选维度和指标 → 选择维度和指标 → 选择时间范围 +3. **表单填写与提交流程**:配置报表参数 → 点击"生成报表" → 调用自定义报表API → 渲染图表/表格 → (合同/招标数据)点击"导出"下载报表 +4. **弹窗/抽屉交互流程**:不适用 +5. **行内操作流程**:报表结果支持图表交互(hover查看数据点);表格数据支持排序 +6. **异常与错误处理**:未选择数据源时"生成报表"按钮禁用;生成失败显示el-message错误提示;业务数据导出时提示"业务数据仅查看,不支持导出" +7. **联动/级联交互**:数据源选择→联动可选维度和指标;选择"报修/巡检/保洁/评价"数据源时隐藏导出按钮;选择"合同/招标"数据源时显示导出按钮 +8. **权限控制交互表现**:有导出权限且选择合同/招标数据源时显示"导出"按钮;业务数据源始终不显示导出 + +### 前端硬性约束 + +> 以下为本页面必须遵守的前端交互与体验硬性约束(H=Hard Constraint)。 + +| 编号 | 约束项 | 级别 | 本页要求 | +|------|--------|------|----------| +| **H1** | 防重复提交 | **强制** | "生成报表"期间显示loading状态并禁用"生成报表"按钮;切换数据源/维度/指标联动加载期间禁用相关控件;**导出期间显示loading并禁用"导出"按钮** | +| **H2** | 请求超时 | **强制** | GET加载维度/指标选项15s超时;GET/POST生成报表请求**30s超时**;**POST导出请求设置60s超时** | +| **H3** | 操作确认 | 强制(不适用) | 本页为报表配置+查看页面,无删除/停用等危险操作,不涉及 | +| **H4** | 脏数据保护 | **强制** | 用户配置了报表参数但未点"生成报表"即离开页面时,若已有已生成的报表结果则标记isDirty=true,触发beforeunload离开拦截确认 | +| **H5** | 权限隔离表现 | 建议 | 无`statistics:custom:view`权限时隐藏整个配置表单区;有`statistics:custom:export`权限且选择合同/招标数据源才显示导出按钮;业务数据源始终不显示导出入口 | +| **H6** | 批量操作限制 | 建议(不适用) | 本页为单次报表生成页面,无批量操作,不涉及 | +| **H7** | 文件上传约束 | 建议(不适用) | 本页为报表生成+导出下载场景,无文件上传功能,不涉及 | +| **H8** | 反馈规范 | 建议 | 生成报表成功后silent渲染图表/表格;**业务数据源尝试导出时显示warning提示"业务数据仅查看,不支持导出"(duration=0)**;合同/招标导出成功显示success(2s)并触发下载;失败显示error(duration=0)+重试 | + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 数据源选择 | el-select | placeholder="请选择数据源",@change联动维度/指标,options: 报修/巡检/保洁/评价/合同/招标 | +| 维度选择 | el-checkbox-group | v-for遍历可选维度,@change联动指标 | +| 指标选择 | el-checkbox-group | v-for遍历可选指标 | +| 时间范围 | el-date-picker | type="daterange",start-placeholder="开始日期",end-placeholder="结束日期" | +| 生成报表按钮 | el-button | type="primary",icon="DataAnalysis",:disabled="未选择数据源" | +| 导出按钮 | el-button | type="success",icon="Download",v-if="数据源=合同/招标" | +| 报表图表区 | echarts | 根据配置动态渲染,:resize自适应容器 | +| 报表表格区 | el-table | :data=报表数据,stripe,border,sortable | +| 空状态 | el-empty | description="请选择数据源和指标后生成报表" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 数据源 | required | 请选择数据源 | +| 维度 | required,至少选1项 | 请至少选择一个维度 | +| 指标 | required,至少选1项 | 请至少选择一个指标 | +| 时间范围 | required | 请选择时间范围 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 配置区左侧,报表结果区右侧,宽度比3:7 | +| 1024-1279px(Pad横屏) | 配置区上方,报表结果区下方,全宽展示 | +| 768-1023px(Pad竖屏) | 配置区上方折叠为el-collapse,报表结果区下方全宽,ECharts图表宽度100%,高度250px,表格横向滚动 | + --- ## 需求追溯 diff --git a/docs/02-功能清单-小程序端/01-通用功能.md b/docs/02-功能清单-小程序端/01-通用功能.md index daeaf88..65424d8 100644 --- a/docs/02-功能清单-小程序端/01-通用功能.md +++ b/docs/02-功能清单-小程序端/01-通用功能.md @@ -3,6 +3,7 @@ > 模块编码:common > 端侧:小程序 > 关联文档:01-模块划分.md §5.2、02-功能清单-小程序端.md §1、03-业务流转逻辑-小程序端.md §1&10、05-接口规范.md §2、06-项目技术要求.md §4.1 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -33,6 +34,44 @@ | 微信一键登录 | — | 触发wx.login()获取code,后端换openid | | 获取手机号 | — | 微信获取手机号组件,绑定手机号 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时检查本地token是否有效→有效则直接跳转工作台→无效则显示登录页;展示应用Logo和系统名称,显示骨架屏占位 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:点击「微信一键登录」→ 触发wx.login()获取code → 后端换取openid → 若未绑定手机号则显示获取手机号按钮 → 点击获取手机号调用微信获取手机号组件 → 绑定成功后跳转工作台 +4. **弹窗/弹层交互流程**:首次使用弹出用户协议与隐私政策确认弹窗→用户点击同意后继续登录流程;匹配失败时弹出提示"未找到关联信息,请联系管理员" +5. **行内操作流程**:点击协议链接→跳转协议详情页 +6. **异常与错误处理**:微信授权失败显示"授权失败,请重试";网络异常显示重试按钮;openid匹配失败提示联系管理员;离线状态下提示"网络不可用,请检查网络连接" +7. **联动/级联交互**:登录成功后自动加载权限集,根据权限渲染工作台功能入口 +8. **权限控制交互表现**:登录前无法访问其他页面,所有页面跳转均需验证token有效性 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 应用Logo | `image` | mode="aspectFit",宽度120px | +| 微信一键登录按钮 | `button` | open-type="getPhoneNumber",type="primary",size="default" | +| 获取手机号按钮 | `button` | open-type="getPhoneNumber",type="default" | +| 协议链接 | `navigator` | url="/pages/agreement/index" | +| 版本号 | `text` | font-size="12px",color="#999" | +| 确认弹窗 | `uni-popup` | type="dialog",:mask-click="false" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 微信授权 | wx.login()必须返回有效code | "微信授权失败,请重试" | +| 手机号绑定 | 必须获取微信手机号组件返回的有效手机号 | "获取手机号失败,请重试" | +| 用户协议 | 首次登录必须同意协议 | "请先同意用户协议与隐私政策" | +| 人员匹配 | openid必须关联系统人员信息 | "未找到关联信息,请联系管理员" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),按钮宽度max-width=320px居中 +- **横竖屏适配策略**:竖屏为默认布局;横屏时Logo缩小,按钮宽度自适应 +- **手势交互规范**:登录按钮可点击区域≥44px;协议链接可点击区域≥44px +- **安全区域**:底部按钮适配底部安全区,padding-bottom=env(safe-area-inset-bottom) + ### 页面2:工作台首页 - **页面路径**:`/pages/workbench/index` @@ -57,6 +96,48 @@ |------|------|------| | 各功能入口 | 按角色 | 根据权限集动态显示/隐藏 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取用户信息及权限集→根据权限动态渲染功能入口网格→显示骨架屏占位→数据返回后渲染用户信息栏和功能入口 +2. **查询/筛选交互流程**:无手动筛选,功能入口根据角色权限自动过滤显示 +3. **表单填写与提交流程**:无表单提交操作 +4. **弹窗/弹层交互流程**:点击消息入口→跳转消息通知列表页;功能入口红点提示为系统推送自动标记 +5. **行内操作流程**:点击功能入口→跳转对应功能页面;点击消息入口→跳转消息列表;底部Tab切换工作台/消息/我的 +6. **异常与错误处理**:权限加载失败显示重试按钮;网络异常显示错误提示;离线时显示缓存的功能入口但提示"数据可能不是最新" +7. **联动/级联交互**:功能入口数量与权限集联动,角色变更后重新渲染入口网格 +8. **权限控制交互表现**:无权限的功能入口不显示;不同角色看到不同的功能入口集合 + +9. **H1 防重复请求(强制)**:功能入口点击后做轻量防抖(300ms),避免重复跳转;权限集加载请求pending期间不重复发;下拉刷新时abort前一次请求后重发 +10. **H2 超时配置(强制)**:获取用户信息及权限集GET请求超时15s;>3s显示wx.showLoading("加载中...");超时后提示"加载超时,请下拉刷新"+重试按钮 +11. **H8 反馈(建议)**:数据加载成功无额外toast(静默渲染);加载失败wx.showToast({icon:'none', title:'数据加载失败'}); 禺线模式wx.showToast({icon:'none', title:'数据可能不是最新'}) + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 用户信息栏 | `view` + `image` + `text` | flex布局,头像40px圆形 | +| 功能入口网格 | `uni-grid` | :column="2",:showBorder="false" | +| 功能入口项 | `uni-grid-item` | 点击跳转对应页面 | +| 红点提示 | `uni-badge` | :content="count",type="error" | +| 消息入口 | `uni-icons` | type="notification",size="24" | +| 底部Tab栏 | `uni-tabbar` | 3个Tab:工作台/消息/我的 | +| 今日指标卡片 | `uni-card` | mode="center",简洁数字展示 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 用户信息 | 必须返回有效的用户信息 | "获取用户信息失败,请下拉刷新" | +| 权限集 | 必须返回有效的权限数据 | "权限加载失败,请重新登录" | +| 功能入口 | 至少有一个可访问的功能入口 | "暂无可用功能,请联系管理员" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),功能入口网格宽度自适应 +- **横竖屏适配策略**:竖屏2列网格;横屏改为3~4列网格,用户信息栏水平展开 +- **手势交互规范**:功能入口可点击区域≥44px;底部Tab可点击区域≥48px;支持下拉刷新 +- **安全区域**:底部Tab栏适配底部安全区,padding-bottom=env(safe-area-inset-bottom) + ### 页面3:个人信息页 - **页面路径**:`/pages/profile/index` @@ -80,6 +161,50 @@ | 修改手机号 | — | 弹出微信获取手机号组件 | | 退出登录 | — | 清除本地token,跳转登录页 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取当前用户个人信息→渲染头像、姓名、手机号、所属公司/班组/角色 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:点击修改手机号→弹出微信获取手机号组件→获取新手机号→调用接口更新→更新成功提示"手机号修改成功" +4. **弹窗/弹层交互流程**:点击头像→弹出大图预览;退出登录→弹出确认弹窗"确定要退出登录吗?" +5. **行内操作流程**:点击修改手机号→调用微信手机号组件;点击退出登录→确认后清除token跳转登录页 +6. **异常与错误处理**:信息加载失败显示重试;手机号修改失败提示错误信息;退出登录清除本地缓存失败则强制跳转 +7. **联动/级联交互**:手机号修改成功后同步更新页面显示和本地缓存 +8. **权限控制交互表现**:所有登录用户均可访问;部分信息只读不可编辑 + +9. **H1 防重复请求(强制)**:修改手机号按钮点击后loading态+disabled;退出登录按钮点击后立即disabled防止重复触发;手机号更新请求pending期间禁止重复调用微信组件 +10. **H2 超时配置(强制)**:手机号更新接口POST请求超时30s;>3s显示wx.showLoading("正在更新...");超时后提示"请求超时,请重试"+重试按钮 +11. **H3 操作确认(强制)**:退出登录必须使用uni-popup(type="dialog")弹出确认弹窗"确定要退出登录吗?退出后需重新登录",含后果说明;用户确认后才执行清除token和跳转 +12. **H4 脏数据检测(强制)**:修改手机号操作前对当前用户信息做深拷贝快照(JSON.parse(JSON.stringify(userInfo)));页面卸载onUnload时检测isDirty,若有未保存的修改弹出提示 +13. **H7 文件上传(建议)**:本页面不涉及文件上传 +14. **H8 反馈(建议)**:手机号修改成功wx.showToast({icon:'success', title:'手机号修改成功'});修改失败wx.showToast({icon:'none', title:'修改失败原因'}); 退出登录成功无需toast静默跳转 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 头像 | `image` | mode="aspectFill",80px圆形,@click预览大图 | +| 信息列表 | `uni-list` + `uni-list-item` | :showArrow="false",右侧为值 | +| 修改手机号 | `button` | open-type="getPhoneNumber",type="default" | +| 退出登录 | `button` | type="warn",size="default" | +| 确认弹窗 | `uni-popup` | type="dialog",确认退出/取消 | +| 大图预览 | `uni-popup` | type="center",图片全屏预览 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 手机号 | 必须为11位有效手机号 | "手机号格式不正确" | +| 头像 | 点击预览需有效URL | — | +| 退出确认 | 必须用户二次确认 | — | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),信息列表宽度自适应 +- **横竖屏适配策略**:竖屏垂直布局;横屏信息列表水平分组展示 +- **手势交互规范**:头像可点击区域≥44px;修改手机号按钮≥44px;退出登录按钮≥44px +- **安全区域**:底部退出按钮适配底部安全区 + ### 页面4:消息通知列表 - **页面路径**:`/pages/message/list` @@ -110,6 +235,50 @@ | 全部标记已读 | — | 将所有未读消息标为已读 | | 点击消息 | — | 跳转到对应业务详情页 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时请求消息列表→显示骨架屏→数据返回后渲染分类Tab和消息列表;默认显示"全部"分类 +2. **查询/筛选交互流程**:点击分类Tab切换消息类别→重新加载对应分类的消息列表;支持滚动加载更多 +3. **表单填写与提交流程**:无表单提交操作 +4. **弹窗/弹层交互流程**:无弹窗 +5. **行内操作流程**:点击消息→标记该消息已读→跳转对应业务详情页;点击「全部标记已读」→批量标记所有未读消息 +6. **异常与错误处理**:消息加载失败显示重试;网络异常显示错误提示;离线时显示缓存消息并提示"离线模式" +7. **联动/级联交互**:Tab切换与列表数据联动;标记已读后红点数量实时更新 +8. **权限控制交互表现**:所有登录用户均可访问;消息内容根据权限控制跳转详情页的可见性 + +9. **H1 防重复请求(强制)**:「全部标记已读」按钮点击后loading态+disabled;Tab切换请求pending时不重复发;上拉加载更多时分页请求使用requestTask,新请求时abort前一次;下拉刷新时abort进行中的列表请求 +10. **H2 超时配置(强制)**:消息列表GET请求超时15s;标记已读POST请求超时30s;列表加载>3s保持骨架屏;超时后提示"请求超时,请下拉刷新"+重试按钮 +11. **H3 操作确认(强制)**:本页面无删除等危险操作,「全部标记已读」为低风险操作可不加确认 +12. **H6 批量限制(建议)**:「全部标记已读」为批量操作,操作前检查未读数量,>50条时提示"未读消息较多,确认全部标记?"再执行 +13. **H8 反馈(建议)**:标记已读成功wx.showToast({icon:'success', title:'已全部标记已读'});消息加载失败wx.showToast({icon:'none', title:'消息加载失败'}); 无未读消息时提示"暂无未读消息" + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 分类Tab | `uni-segmented-control` | :current="currentTab",:values="['全部','工单','审批','系统']" | +| 消息列表 | `uni-list` + `uni-list-item` | clickable=true,showArrow=true | +| 未读红点 | `uni-badge` | :content="1",type="error",size="small" | +| 消息类型图标 | `uni-icons` | type="notification/chat/settings",size="22" | +| 全部标记已读 | `button` | type="default",size="mini" | +| 下拉刷新 | `uni-refresher` | @onRefresh回调,threshold=45px | +| 上拉加载 | `uni-load-more` | :status="loadingStatus" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 消息列表 | 加载失败时允许重试 | "消息加载失败,请下拉刷新重试" | +| 分类Tab | 切换时重新请求数据 | — | +| 全部标记已读 | 须有未读消息才可操作 | "暂无未读消息" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),消息列表宽度自适应 +- **横竖屏适配策略**:竖屏垂直列表;横屏可双列展示消息卡片 +- **手势交互规范**:消息项可点击区域≥44px;Tab切换区域≥44px;支持下拉刷新 +- **安全区域**:底部标记已读按钮适配底部安全区 + ### 页面5:数据补录申请页 - **页面路径**:`/pages/supplement/apply` @@ -131,6 +300,54 @@ |------|------|------| | 提交申请 | — | 提交补录申请,等待主管审核 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取补录模块字典→渲染模块选择器 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:选择补录模块→渲染对应的动态表单→选择补录原因→填写补录说明→上传证明材料→点击提交申请→确认弹窗→提交成功提示"补录申请已提交,等待主管审核" +4. **弹窗/弹层交互流程**:提交前弹出确认弹窗"确认提交补录申请?";上传照片时可选择拍照或从相册选择 +5. **行内操作流程**:选择补录模块→动态渲染对应表单字段;点击上传区域→调起相机/相册 +6. **异常与错误处理**:网络异常提交失败提示"提交失败,请重试";图片上传失败提示重传;离线时数据暂存本地,联网后自动提交 +7. **联动/级联交互**:补录模块选择与动态表单联动,不同模块显示不同字段 +8. **权限控制交互表现**:所有登录用户均可提交补录申请;提交后状态为待审核,不可重复提交同一条 + +9. **H1 防重复请求(强制)**:提交申请按钮点击后:loading="true"且disabled;表单提交请求pending期间禁用提交按钮和返回操作;使用requestTask管理提交请求 +10. **H2 超时配置(强制)**:补录模块字典GET请求超时15s;提交补录POST请求超时30s;图片上传超时60s;>3s wx.showLoading("正在提交..."); 超时后提示"请求超时,请重试"+重试按钮 +11. **H3 操作确认(强制)**:提交前必须弹出uni-popup确认弹窗"确认提交补录申请?提交后将等待主管审核",含后果说明 +12. **H4 脏数据检测(强制)**:页面进入时对表单初始值做深拷贝快照;离开页面onUnload/onHide时检测表单是否有填写内容(isDirty),若有已填写内容但未提交则弹出提示"有未提交的内容,确定离开吗?" +13. **H6 批量限制(建议)**:证明材料最多3张(已在校验规则限制),超出时提示"证明材料最多上传3张" +14. **H7 文件上传(建议)**:证明材料使用wx.chooseMedia选择图片,限制最多3张;单张≤10MB,格式白名单jpg/jpeg/png;上传时显示进度回调(uni-file-picker内置);上传失败提示具体错误原因 +15. **H8 反馈(建议)**:提交成功wx.showToast({icon:'success', title:'补录申请已提交,等待主管审核'}); 提交失败wx.showToast({icon:'none', title:'提交失败原因'}可显示多文字); 网络异常wx.getNetworkType检测后提示+重试按钮 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 补录模块选择 | `uni-data-select` | v-model="module",:localdata="moduleList" | +| 补录原因选择 | `uni-data-select` | v-model="reason",:localdata="reasonList" | +| 补录说明 | `uni-easyinput` | type="textarea",maxlength="200",:showWordLimit="true" | +| 证明材料上传 | `uni-file-picker` | limit="3",file-mediatype="image",:auto-upload="true" | +| 动态表单 | `uni-forms` | :modelValue="formData",:rules="rules" | +| 提交按钮 | `button` | type="primary",:loading="submitting" | +| 确认弹窗 | `uni-popup` | type="dialog",确认提交/取消 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 补录模块 | 必选 | "请选择补录模块" | +| 补录原因 | 必选 | "请选择补录原因" | +| 补录说明 | 必填,最多200字 | "请填写补录说明" / "补录说明不能超过200字" | +| 证明材料 | 最多3张 | "证明材料最多上传3张" | +| 动态表单 | 根据模块动态校验 | 根据具体字段提示 | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),表单宽度自适应 +- **横竖屏适配策略**:竖屏垂直布局;横屏表单水平分组展示 +- **手势交互规范**:选择器可点击区域≥44px;提交按钮≥44px;上传区域≥44px +- **安全区域**:底部提交按钮适配底部安全区 + ### 页面6:通讯录页 - **页面路径**:`/pages/contacts/index` @@ -160,6 +377,48 @@ |------|------|------| | 拨打电话 | — | 点击手机号直接拨打 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取联系人数据→按班组分组→渲染分组列表 +2. **查询/筛选交互流程**:输入搜索关键词→实时过滤联系人(按姓名/手机号匹配)→无匹配结果显示"未找到联系人" +3. **表单填写与提交流程**:无表单提交操作 +4. **弹窗/弹层交互流程**:无弹窗 +5. **行内操作流程**:点击手机号图标→调用wx.makePhoneCall拨打电话;滚动列表浏览不同班组的联系人 +6. **异常与错误处理**:联系人加载失败显示重试;拨号失败提示"拨号失败";离线时显示缓存数据 +7. **联动/级联交互**:搜索关键词与列表过滤联动,输入即过滤 +8. **权限控制交互表现**:所有登录用户均可访问;只能查看本公司/班组人员 + +9. **H1 防重复请求(强制)**:搜索输入做防抖处理(300ms)避免频繁过滤;联系人列表加载请求pending期间不重复发;拨打电话按钮点击后短暂disabled防止连击 +10. **H2 超时配置(强制)**:联系人列表GET请求超时15s;>3s显示加载骨架屏;超时后提示"加载超时,请重试"+重试按钮 +11. **H6 批量限制(建议)**:本页面无批量操作 +12. **H8 反馈(建议)**:联系人加载成功静默渲染;加载失败wx.showToast({icon:'none', title:'联系人加载失败'}); 拨号失败wx.showToast({icon:'none', title:'拨号失败,请重试'}) + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 搜索框 | `uni-search-bar` | placeholder="搜索姓名/手机号",@confirm="search" | +| 班组分组 | `uni-section` | :title="groupName",type="line" | +| 联系人列表 | `uni-list` + `uni-list-item` | clickable=true,showArrow=false | +| 头像 | `image` | mode="aspectFill",40px圆形 | +| 手机号图标 | `uni-icons` | type="phone",size="22",color="#007AFF" | +| 拨打电话 | `button` | @click="makePhoneCall",open-type="contact" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 搜索关键词 | 最少2个字符才触发搜索 | "请输入至少2个字符" | +| 手机号 | 脱敏显示中间4位 | — | +| 联系人列表 | 加载失败允许重试 | "加载失败,请重试" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),联系人列表宽度自适应 +- **横竖屏适配策略**:竖屏垂直列表;横屏可双列展示联系人卡片 +- **手势交互规范**:联系人项可点击区域≥44px;拨打电话图标≥44px;搜索框≥44px +- **安全区域**:底部适配底部安全区 + ### 页面7:版本更新提示弹窗 - **页面路径**:弹窗组件(非独立页面) @@ -174,6 +433,47 @@ - 中部:更新说明 - 底部:操作按钮 +#### 交互流程要求 + +1. **页面加载流程**:应用启动时调用后台接口获取最新版本号+最低兼容版本→对比当前版本→小版本更新弹窗提示→大版本更新强制弹窗 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:无表单提交操作 +4. **弹窗/弹层交互流程**:小版本更新→弹窗显示更新内容+更新/取消按钮,用户可关闭;大版本更新→弹窗仅显示更新按钮,不可关闭 +5. **行内操作流程**:点击更新→跳转微信小程序更新页面;点击取消→关闭弹窗继续使用(小版本) +6. **异常与错误处理**:版本检查接口失败静默处理,不影响正常使用;SDK依赖校验失败提示"应用需要更新" +7. **联动/级联交互**:版本号与更新内容联动展示 +8. **权限控制交互表现**:所有用户均可见更新提示 + +9. **H1 防重复请求(强制)**:更新按钮点击后loading态+disabled防止重复下载;版本检查请求pending期间不重复发送;使用requestTask管理版本检查接口 +10. **H2 超时配置(强制)**:版本检查GET接口超时15s;>3s显示wx.showLoading("检查更新中..."); 接口超时或失败静默处理不影响正常使用 +11. **H3 操作确认(强制)**:小版本更新弹窗需明确展示更新内容+更新/取消选项;大版本更新弹窗仅显示更新按钮不可关闭,强制用户更新 +12. **H8 反馈(建议)**:版本检查成功且有新版本正常展示弹窗;已是最新版本无需提示;SDK校验失败wx.showToast({icon:'none', title:'应用需要更新'}) + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 弹窗容器 | `uni-popup` | type="center",:mask-click="false"(大版本)/ :mask-click="true"(小版本) | +| 版本号 | `text` | font-size="18px",font-weight="bold" | +| 更新说明 | `scroll-view` | scroll-y=true,max-height="300rpx" | +| 更新按钮 | `button` | type="primary",@click="updateApp" | +| 取消按钮 | `button` | type="default",@click="closePopup"(仅小版本显示) | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 版本号 | 必须为有效版本格式(x.y.z) | — | +| 更新内容 | 不能为空 | — | +| 最低兼容版本 | 必须为有效版本格式 | — | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),弹窗宽度max-width=300px居中 +- **横竖屏适配策略**:竖屏居中弹窗;横屏弹窗宽度适当增大 +- **手势交互规范**:更新按钮≥44px;取消按钮≥44px;更新说明区域可滚动 +- **安全区域**:弹窗内容不受安全区影响 + ## 需求追溯 | 功能点编号 | 功能名称 | 文档来源 | 后续服务 | 关联功能 | diff --git a/docs/02-功能清单-小程序端/02-报修相关功能.md b/docs/02-功能清单-小程序端/02-报修相关功能.md index be3d1ff..8c519ca 100644 --- a/docs/02-功能清单-小程序端/02-报修相关功能.md +++ b/docs/02-功能清单-小程序端/02-报修相关功能.md @@ -3,6 +3,7 @@ > 模块编码:repair > 端侧:小程序 > 关联文档:01-模块划分.md §3.1、02-功能清单-小程序端.md §2、03-业务流转逻辑-小程序端.md §2、05-接口规范.md §9.2、06-项目技术要求.md §4.4 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -32,6 +33,41 @@ | 扫描二维码 | — | 调用wx.scanCode扫描项目二维码 | | 确认绑定 | — | 绑定身份到对应项目 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时自动调起wx.scanCode扫码→扫码成功后显示项目信息→等待用户确认绑定 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:点击扫描二维码→调用wx.scanCode→获取项目编码→查询项目信息→展示项目名称、地址→点击确认绑定→绑定成功跳转工作台 +4. **弹窗/弹层交互流程**:扫码失败弹出提示"二维码无效,请重新扫描";绑定成功弹出成功提示 +5. **行内操作流程**:点击扫码区域→调起扫码;点击确认绑定→提交绑定请求 +6. **异常与错误处理**:扫码超时提示"扫码超时,请重试";二维码无效提示重扫;网络异常显示重试;相机权限被拒提示"请开启相机权限" +7. **联动/级联交互**:扫码结果与项目信息联动展示 +8. **权限控制交互表现**:所有用户均可扫码注册;已绑定用户不显示此页面 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 扫码区域 | `button` | @click="scanCode",type="primary",text="扫描二维码" | +| 项目信息展示 | `uni-card` | mode="basic",展示项目名称、地址 | +| 确认绑定 | `button` | type="primary",:loading="binding" | +| 提示弹窗 | `uni-popup` | type="dialog",确认/取消 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 二维码 | 必须为系统有效项目二维码 | "二维码无效,请重新扫描" | +| 项目信息 | 扫码后必须返回有效项目数据 | "未找到项目信息" | +| 绑定状态 | 未绑定才可操作 | "您已绑定项目" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),扫码区域居中 +- **横竖屏适配策略**:竖屏垂直布局;横屏扫码区域与信息并排展示 +- **手势交互规范**:扫码按钮≥44px;确认绑定按钮≥44px +- **安全区域**:底部按钮适配底部安全区 + ### 页面2:一键报修页 - **页面路径**:`/pages/repair/create` @@ -53,6 +89,53 @@ | 从相册选择 | — | 选择相册照片,自动添加水印 | | 提交报修 | — | 生成工单,等待分配 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取报修类型字典→渲染类型选择器;获取当前位置信息预填位置 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:选择报修类型→输入报修描述→拍照/选择照片(自动添加时间+位置水印)→选择/确认报修位置→确认联系方式→点击提交报修→确认弹窗→提交成功跳转工单列表 +4. **弹窗/弹层交互流程**:点击拍照/相册弹出选择弹窗(拍照/从相册选择);提交前弹出确认弹窗 +5. **行内操作流程**:点击照片上传区域→调起相机/相册→选择照片→自动添加水印→显示缩略图;点击位置→打开地图选择位置 +6. **异常与错误处理**:定位失败提示手动选择位置;图片上传失败提示重传;提交失败显示重试;离线时数据暂存本地草稿 +7. **联动/级联交互**:报修类型与描述提示联动;位置自动获取与手动选择联动 +8. **权限控制交互表现**:所有登录用户均可报修;相机/定位权限被拒时引导开启 + +9. **H1 防重复请求(强制)**:提交报修按钮点击后:loading="submitting"且disabled;表单提交请求pending期间禁用提交和返回操作;使用requestTask管理提交请求生命周期 +10. **H2 超时配置(强制)**:报修类型字典GET请求超时15s;提交报修POST请求超时30s;图片上传超时60s;定位接口超时10s;>3s wx.showLoading("正在提交..."); 超时后提示"请求超时,请重试"+重试按钮 +11. **H3 操作确认(强制)**:提交前必须弹出uni-popup确认弹窗"确认提交报修单?提交后将生成工单等待分配",含后果说明 +12. **H4 脏数据检测(强制)**:进入页面对空表单做深拷贝快照;填写过程中任何字段变更标记isDirty=true;页面卸载/onBackPress时检测isDirty,若有已填写内容(描述/照片等)弹出uni-popup确认"有未提交的报修内容,确定离开吗?" +13. **H7 文件上传(建议)**:照片上传使用wx.chooseMedia({count:9, mediaType:['image']})选择,单张≤10MB,格式白名单jpg/jpeg/png;自动添加水印(时间+位置)后再上传;上传过程显示进度回调(uni-file-picker);上传失败提示具体原因支持重传 +14. **H8 反馈(建议)**:报修提交成功wx.showToast({icon:'success', title:'报修成功'}); 提交失败wx.showToast({icon:'none', title:'提交失败原因'})可显示多文字;定位失败wx.showToast({icon:'none', title:'定位失败,请手动选择位置'}); 离线模式提示数据已存草稿 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 报修类型选择 | `uni-data-select` | v-model="type",:localdata="typeList" | +| 报修描述 | `uni-easyinput` | type="textarea",maxlength="500",:showWordLimit="true" | +| 照片上传 | `uni-file-picker` | limit="9",file-mediatype="image",:auto-upload="true" | +| 报修位置 | `uni-list-item` | :showArrow="true",@click="chooseLocation" | +| 联系方式 | `uni-easyinput` | type="number",maxlength="11" | +| 提交按钮 | `button` | type="primary",:loading="submitting" | +| 确认弹窗 | `uni-popup` | type="dialog" | +| 位置选择 | `map` + `button` | 调用wx.chooseLocation | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 报修类型 | 必选 | "请选择报修类型" | +| 报修描述 | 必填,最多500字 | "请填写报修描述" / "描述不能超过500字" | +| 照片 | 最多9张,必须含水印 | "照片最多上传9张" | +| 联系方式 | 必填,11位手机号 | "请输入联系方式" / "手机号格式不正确" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),表单宽度自适应 +- **横竖屏适配策略**:竖屏垂直布局;横屏照片上传区3~4列网格 +- **手势交互规范**:照片上传区域≥44px;提交按钮≥44px;位置选择≥44px +- **安全区域**:底部提交按钮适配底部安全区 + ### 页面3:我的工单列表 - **页面路径**:`/pages/repair/my-orders` @@ -88,6 +171,51 @@ | 查看详情 | — | 跳转工单详情页 | | 催单 | repair:list:view | 处理中工单可催单 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时请求工单列表→显示骨架屏→渲染Tab和工单卡片;默认显示"全部"状态 +2. **查询/筛选交互流程**:点击状态Tab切换筛选条件→重新加载列表;输入关键词搜索工单号/描述;下拉刷新重新加载;上拉加载更多 +3. **表单填写与提交流程**:无表单提交操作 +4. **弹窗/弹层交互流程**:催单时弹出确认弹窗"确认催单?催单后维修人员和主管将收到通知" +5. **行内操作流程**:点击工单卡片→跳转工单详情;点击催单→确认催单→发送催单通知 +6. **异常与错误处理**:列表加载失败显示重试;催单超过每日3次限制提示"今日已催单3次,请明日再试";网络异常显示错误提示 +7. **联动/级联交互**:Tab切换与列表数据联动;催单后催单标记实时更新 +8. **权限控制交互表现**:催单按钮仅处理中工单显示;无repair:list:view权限时隐藏催单按钮 + +9. **H1 防重复请求(强制)**:催单按钮点击后loading态+disabled;Tab切换请求pending时不重复发列表请求;搜索输入防抖300ms;上拉加载更多时分页用requestTask管理,新请求abort前一次;下拉刷新abort进行中请求 +10. **H2 超时配置(强制)**:工单列表GET请求超时15s;催单POST请求超时30s;>3s保持骨架屏;超时后提示"请求超时,请下拉刷新"+重试按钮 +11. **H3 操作确认(强制)**:催单操作必须弹出uni-popup确认弹窗"确认催单?催单后维修人员和主管将收到通知(今日剩余X次)",含后果说明和次数限制提示 +12. **H6 批量限制(建议)**:催单每天最多3次(服务端校验),前端达到限制时直接禁用催单按钮并提示"今日已催单3次,请明日再试" +13. **H8 反馈(建议)**:催单成功wx.showToast({icon:'success', title:'催单成功'}); 列表加载失败wx.showToast({icon:'none', title:'加载失败,请下拉刷新'}); 催单次数不足wx.showToast({icon:'none', title:'今日已催单3次,请明日再试'}) + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 状态Tab | `uni-segmented-control` | :values="['全部','待分配','处理中','待验收','已完成','已关闭']" | +| 搜索框 | `uni-search-bar` | placeholder="搜索工单号/描述" | +| 工单列表 | `uni-list` + `uni-list-item` | clickable=true | +| 状态标签 | `uni-tag` | type: success(已完成)/warning(处理中)/error(异常)/default(待分配) | +| 催单标记 | `uni-badge` | content="催",type="error" | +| 下拉刷新 | `uni-refresher` | @onRefresh回调 | +| 上拉加载 | `uni-load-more` | :status="loadingStatus" | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 工单列表 | 加载失败允许重试 | "加载失败,请下拉刷新" | +| 搜索关键词 | 最少2个字符 | "请输入至少2个字符" | +| 催单 | 每天最多3次 | "今日已催单3次,请明日再试" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),卡片宽度自适应,padding 16px +- **横竖屏适配策略**:竖屏单列卡片;横屏双列卡片展示 +- **手势交互规范**:工单卡片可点击区域≥44px;催单按钮≥44px;支持下拉刷新和上拉加载 +- **安全区域**:底部列表适配底部安全区 + ### 页面4:待处理工单列表(维修人员) - **页面路径**:`/pages/repair/pending-orders` @@ -116,6 +244,49 @@ |------|------|------| | 接单 | repair:list:update | 接单后工单状态变为"处理中" | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时请求分配给当前维修人员的待处理工单→显示骨架屏→渲染待处理数量统计和工单列表 +2. **查询/筛选交互流程**:无手动筛选,自动加载当前人员的待处理工单;下拉刷新重新加载 +3. **表单填写与提交流程**:无表单提交操作 +4. **弹窗/弹层交互流程**:点击接单→弹出确认弹窗"确认接单?" +5. **行内操作流程**:点击工单卡片→查看工单详情;点击接单→确认后工单状态变为处理中→通知报修人 +6. **异常与错误处理**:列表加载失败显示重试;接单失败提示错误信息;工单已被他人接单提示"工单已被接单" +7. **联动/级联交互**:接单成功后列表数据更新,待处理数量减少 +8. **权限控制交互表现**:仅维修人员角色可见此页面;无repair:list:update权限时接单按钮置灰 + +9. **H1 防重复请求(强制)**:接单按钮点击后loading态+disabled;列表加载pending不重复发;上拉分页用requestTask管理,新请求abort旧请求;下拉刷新abort进行中请求 +10. **H2 超时配置(强制)**:待处理工单列表GET请求超时15s;接单POST请求超时30s;>3s保持骨架屏;超时后提示"请求超时,请下拉刷新"+重试按钮 +11. **H3 操作确认(强制)**:接单操作必须弹出uni-popup确认弹窗"确认接单?接单后请在规定时间内完成维修",含后果说明 +12. **H6 批量限制(建议)**:本页面主要为单个接单操作,无批量操作场景 +13. **H8 反馈(建议)**:接单成功wx.showToast({icon:'success', title:'接单成功'}); 工单已被他人接单wx.showToast({icon:'none', title:'工单已被接单,请刷新列表'}); 列表加载失败wx.showToast({icon:'none', title:'加载失败,请重试'}) + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 待处理数量 | `uni-badge` | :content="count",type="warning",size="large" | +| 工单列表 | `uni-list` + `uni-list-item` | clickable=true | +| 优先级标签 | `uni-tag` | type: error(高)/warning(中)/default(低) | +| 照片缩略图 | `image` | mode="aspectFill",60px×60px圆角 | +| 接单按钮 | `button` | type="primary",size="mini" | +| 下拉刷新 | `uni-refresher` | @onRefresh回调 | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 工单列表 | 加载失败允许重试 | "加载失败,请下拉刷新" | +| 接单操作 | 工单必须为待分配/已分配状态 | "工单状态已变更,请刷新" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),卡片宽度自适应 +- **横竖屏适配策略**:竖屏单列卡片;横屏双列卡片展示 +- **手势交互规范**:工单卡片可点击区域≥44px;接单按钮≥44px;支持下拉刷新 +- **安全区域**:底部列表适配底部安全区 + ### 页面5:维修完工页 - **页面路径**:`/pages/repair/complete` @@ -135,6 +306,43 @@ | 拍照上传 | — | 拍摄维修后照片 | | 提交完工 | repair:list:update | 工单状态变为"待验收" | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取工单基本信息→渲染工单信息摘要 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:输入维修说明→拍照上传维修后照片(自动添加水印)→可选填材料使用记录→点击提交完工→确认弹窗→提交成功跳转工单列表 +4. **弹窗/弹层交互流程**:点击拍照弹出选择弹窗(拍照/从相册选择);提交前弹出确认弹窗 +5. **行内操作流程**:点击照片上传区域→调起相机/相册;点击提交完工→确认提交 +6. **异常与错误处理**:图片上传失败提示重传;提交失败显示重试;离线时数据暂存本地草稿 +7. **联动/级联交互**:照片上传数量与限制联动(最多9张) +8. **权限控制交互表现**:仅维修人员可操作;无repair:list:update权限时提交按钮置灰 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 工单信息摘要 | `uni-card` | mode="basic",只读展示 | +| 维修说明 | `uni-easyinput` | type="textarea",maxlength="500",:showWordLimit="true" | +| 照片上传 | `uni-file-picker` | limit="9",file-mediatype="image",:auto-upload="true" | +| 材料使用记录 | `uni-list` + `uni-list-item` | 可动态添加行 | +| 提交按钮 | `button` | type="primary",:loading="submitting" | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 维修说明 | 必填,最多500字 | "请填写维修说明" / "维修说明不能超过500字" | +| 维修后照片 | 至少1张 | "请上传维修后照片" | +| 照片水印 | 自动添加时间+位置水印 | — | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),表单宽度自适应 +- **横竖屏适配策略**:竖屏垂直布局;横屏照片区3~4列网格 +- **手势交互规范**:照片上传区域≥44px;提交按钮≥44px +- **安全区域**:底部提交按钮适配底部安全区 + ### 页面6:延期申请页 - **页面路径**:`/pages/repair/extension-apply` @@ -151,6 +359,41 @@ |------|------|------| | 提交延期申请 | repair:list:update | 工单状态变为"延期中",等待主管审批 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取工单基本信息→渲染工单信息摘要 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:输入延期原因→选择预计完成时间→点击提交延期申请→确认弹窗→提交成功跳转工单详情 +4. **弹窗/弹层交互流程**:提交前弹出确认弹窗"确认申请延期?" +5. **行内操作流程**:点击预计完成时间→弹出日期时间选择器→选择时间;点击提交延期申请→确认提交 +6. **异常与错误处理**:提交失败显示重试;预计完成时间不能早于当前时间 +7. **联动/级联交互**:延期申请提交后工单状态变为"延期中" +8. **权限控制交互表现**:仅维修人员可操作;无repair:list:update权限时提交按钮置灰 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 工单信息摘要 | `uni-card` | mode="basic",只读展示 | +| 延期原因 | `uni-easyinput` | type="textarea",maxlength="500",:showWordLimit="true" | +| 预计完成时间 | `uni-datetime-picker` | type="datetime",:start="minDate" | +| 提交按钮 | `button` | type="primary",:loading="submitting" | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 延期原因 | 必填,最多500字 | "请填写延期原因" / "延期原因不能超过500字" | +| 预计完成时间 | 必选,必须晚于当前时间 | "请选择预计完成时间" / "预计完成时间不能早于当前时间" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),表单宽度自适应 +- **横竖屏适配策略**:竖屏垂直布局;横屏表单水平分组展示 +- **手势交互规范**:时间选择器≥44px;提交按钮≥44px +- **安全区域**:底部提交按钮适配底部安全区 + ### 页面7:延期审批页(主管) - **页面路径**:`/pages/repair/extension-approve` @@ -169,6 +412,42 @@ | 通过 | repair:detail:approve | 延期生效,工单继续处理 | | 驳回 | repair:detail:approve | 通知维修人员,工单恢复原状态 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取工单基本信息和延期申请详情→渲染工单信息摘要和申请详情 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:查看延期申请详情→输入审批意见→点击通过/驳回→确认弹窗→操作成功跳转列表 +4. **弹窗/弹层交互流程**:通过时弹出确认弹窗"确认通过延期申请?";驳回时弹出确认弹窗+必填驳回原因 +5. **行内操作流程**:点击通过→确认→延期生效;点击驳回→填写驳回原因→确认→通知维修人员 +6. **异常与错误处理**:审批操作失败显示重试;工单状态已变更提示"工单状态已更新,请刷新" +7. **联动/级联交互**:审批结果与工单状态联动更新 +8. **权限控制交互表现**:仅主管可操作;无repair:detail:approve权限时按钮置灰 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 工单信息摘要 | `uni-card` | mode="basic",只读展示 | +| 延期申请详情 | `uni-list` + `uni-list-item` | :showArrow="false",展示申请人、原因、预计时间 | +| 审批意见 | `uni-easyinput` | type="textarea",maxlength="200" | +| 通过按钮 | `button` | type="primary",@click="approve" | +| 驳回按钮 | `button` | type="warn",@click="reject" | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 审批意见 | 驳回时必填 | "请填写驳回原因" | +| 审批意见 | 最多200字 | "审批意见不能超过200字" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),表单宽度自适应 +- **横竖屏适配策略**:竖屏垂直布局;横屏按钮并排展示更宽 +- **手势交互规范**:通过/驳回按钮≥44px,间距≥16px +- **安全区域**:底部按钮适配底部安全区 + ### 页面8:协助维修申请页 - **页面路径**:`/pages/repair/assist-apply` @@ -185,6 +464,41 @@ |------|------|------| | 提交协助申请 | repair:list:update | 生成协助子工单,等待协助完成 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取工单基本信息和班组列表→渲染工单信息摘要 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:选择协助班组→输入协助原因→点击提交协助申请→确认弹窗→提交成功跳转工单详情 +4. **弹窗/弹层交互流程**:提交前弹出确认弹窗"确认申请协助维修?" +5. **行内操作流程**:选择协助班组→输入协助原因→确认提交 +6. **异常与错误处理**:提交失败显示重试;无可用协助班组提示"暂无可协助的班组" +7. **联动/级联交互**:协助申请提交后生成协助子工单 +8. **权限控制交互表现**:仅维修人员可操作;无repair:list:update权限时提交按钮置灰 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 工单信息摘要 | `uni-card` | mode="basic",只读展示 | +| 协助班组选择 | `uni-data-select` | v-model="team",:localdata="teamList" | +| 协助原因 | `uni-easyinput` | type="textarea",maxlength="500",:showWordLimit="true" | +| 提交按钮 | `button` | type="primary",:loading="submitting" | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 协助班组 | 必选 | "请选择协助班组" | +| 协助原因 | 必填,最多500字 | "请填写协助原因" / "协助原因不能超过500字" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),表单宽度自适应 +- **横竖屏适配策略**:竖屏垂直布局;横屏表单水平分组展示 +- **手势交互规范**:班组选择≥44px;提交按钮≥44px +- **安全区域**:底部提交按钮适配底部安全区 + ### 页面9:工单分配页(主管) - **页面路径**:`/pages/repair/assign` @@ -202,6 +516,43 @@ |------|------|------| | 确认分配 | repair:list:assign | 分配成功后通知维修人员 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取工单基本信息和班组列表→渲染工单信息摘要 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:选择分配班组→联动加载班组下人员列表→选择分配人员→可选填分配备注→点击确认分配→确认弹窗→分配成功通知维修人员 +4. **弹窗/弹层交互流程**:提交前弹出确认弹窗"确认分配给XXX?" +5. **行内操作流程**:选择班组→人员列表联动刷新→选择人员→确认分配 +6. **异常与错误处理**:班组下无人员提示"该班组暂无可用人员";分配失败显示重试 +7. **联动/级联交互**:班组选择与人员列表联动,选择班组后动态加载该班组人员 +8. **权限控制交互表现**:仅主管可操作;无repair:list:assign权限时提交按钮置灰 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 工单信息摘要 | `uni-card` | mode="basic",只读展示 | +| 班组选择 | `uni-data-select` | v-model="teamId",@change="loadMembers" | +| 人员选择 | `uni-data-select` | v-model="memberId",:localdata="memberList" | +| 分配备注 | `uni-easyinput` | type="textarea",maxlength="200" | +| 确认分配 | `button` | type="primary",:loading="submitting" | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 分配班组 | 必选 | "请选择分配班组" | +| 分配人员 | 必选 | "请选择分配人员" | +| 分配备注 | 最多200字 | "备注不能超过200字" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),表单宽度自适应 +- **横竖屏适配策略**:竖屏垂直布局;横屏班组人员选择并排展示 +- **手势交互规范**:选择器≥44px;确认按钮≥44px +- **安全区域**:底部按钮适配底部安全区 + ### 页面10:工单验收页 - **页面路径**:`/pages/repair/accept` @@ -221,6 +572,43 @@ |------|------|------| | 提交验收 | repair:detail:approve | 通过→工单完成;不通过→退回维修 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取工单信息→渲染工单摘要、维修说明、前后照片对比 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:查看维修前后照片对比→选择验收结果(通过/不通过)→不通过时填写验收意见→点击提交验收→确认弹窗→验收通过工单完成/不通过退回维修 +4. **弹窗/弹层交互流程**:提交前弹出确认弹窗"确认验收结果?";点击照片弹出大图预览 +5. **行内操作流程**:选择通过/不通过→不通过时展开验收意见输入→提交验收 +6. **异常与错误处理**:验收操作失败显示重试;工单状态已变更提示刷新 +7. **联动/级联交互**:验收结果选择与验收意见联动,不通过时验收意见必填 +8. **权限控制交互表现**:仅主管/报修人可操作;无repair:detail:approve权限时按钮置灰 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 工单信息摘要 | `uni-card` | mode="basic",只读展示 | +| 照片对比 | `swiper` | 左右滑动对比,:indicator-dots="true" | +| 照片大图 | `uni-popup` | type="center",图片全屏预览 | +| 验收结果 | `uni-data-checkbox` | :localdata="[{value:'pass',text:'通过'},{value:'fail',text:'不通过'}]" | +| 验收意见 | `uni-easyinput` | type="textarea",maxlength="500" | +| 提交按钮 | `button` | type="primary",:loading="submitting" | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 验收结果 | 必选 | "请选择验收结果" | +| 验收意见 | 不通过时必填,最多500字 | "不通过时请填写验收意见" / "验收意见不能超过500字" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),照片区域宽度自适应 +- **横竖屏适配策略**:竖屏照片上下对比;横屏照片左右对比 +- **手势交互规范**:照片可点击放大≥44px;提交按钮≥44px;左右滑动切换照片 +- **安全区域**:底部按钮适配底部安全区 + ### 页面11:工单评价页 - **页面路径**:`/pages/repair/evaluate` @@ -238,6 +626,43 @@ |------|------|------| | 提交评价 | — | 评价提交后不可修改 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取工单基本信息→渲染工单信息摘要 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:点击星级选择评分→输入评价内容→可选上传图片→点击提交评价→确认弹窗→评价提交成功跳转工单详情 +4. **弹窗/弹层交互流程**:提交前弹出确认弹窗"确认提交评价?提交后不可修改";上传图片弹出选择(拍照/相册) +5. **行内操作流程**:点击星星评分→输入评价→上传图片→提交评价 +6. **异常与错误处理**:图片上传失败提示重传;提交失败显示重试;评价已提交提示"已评价" +7. **联动/级联交互**:评分与评价提交联动,必须选择评分才能提交 +8. **权限控制交互表现**:仅报修人可评价;已评价工单不显示评价入口 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 工单信息摘要 | `uni-card` | mode="basic",只读展示 | +| 星级评分 | `uni-rate` | :max="5",:value="score",:size="28" | +| 评价内容 | `uni-easyinput` | type="textarea",maxlength="200",:showWordLimit="true" | +| 图片上传 | `uni-file-picker` | limit="3",file-mediatype="image" | +| 提交按钮 | `button` | type="primary",:loading="submitting" | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 评分 | 必选,1~5分 | "请选择评分" | +| 评价内容 | 最多200字 | "评价内容不能超过200字" | +| 图片 | 最多3张 | "评价图片最多上传3张" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),表单宽度自适应 +- **横竖屏适配策略**:竖屏垂直布局;横屏评分与输入并排展示 +- **手势交互规范**:星级评分每颗星≥44px;提交按钮≥44px +- **安全区域**:底部提交按钮适配底部安全区 + ### 页面12:工单详情页 - **页面路径**:`/pages/repair/detail` @@ -268,6 +693,50 @@ | 验收 | 主管/报修人 | 待验收 | 验收工单 | | 评价 | 报修人 | 已完成 | 评价工单 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取工单详细信息→渲染工单编号、状态、基本信息、照片、维修记录、时间轴 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:本页面无表单提交操作,操作按钮跳转对应子页面 +4. **弹窗/弹层交互流程**:点击照片→弹出大图预览;点击催单→弹出确认弹窗 +5. **行内操作流程**:点击操作按钮→根据角色和状态跳转对应操作页面(接单→完工页、延期→延期申请页等);查看照片轮播;滚动查看时间轴 +6. **异常与错误处理**:工单加载失败显示重试;照片加载失败显示占位图;网络异常显示错误提示 +7. **联动/级联交互**:操作按钮根据工单状态和用户角色动态显示/隐藏 +8. **权限控制交互表现**:不同角色看到不同操作按钮;无权限操作按钮不显示;催单按钮仅报修人可见 + +9. **H1 防重复请求(强制)**:各操作按钮(催单/接单/完工等)点击后分别做loading+disabled防连击;操作跳转子页面由子页面负责防重;照片预览大图操作不做loading拦截;工单详情GET请求pending期间不重复发 +10. **H2 超时配置(强制)**:工单详情GET请求超时15s;>3s显示骨架屏;详情加载超时后提示"加载超时,请重试"+重试按钮 +11. **H3 操作确认(强制)**:催单操作在详情页弹出确认弹窗"确认催单?(今日剩余X次)";其他危险操作由各子页面确认 +12. **H7 文件上传(建议)**:本页面照片均为只读展示,不涉及上传操作 +13. **H8 反馈(建议)**:详情加载成功静默渲染无需toast;加载失败wx.showToast({icon:'none', title:'工单信息加载失败,请重试'}); 照片加载失败显示占位图;网络异常wx.getNetworkType检测+重试按钮 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 状态标签 | `uni-tag` | type: success(已完成)/warning(处理中)/error(异常)/default(待分配) | +| 照片轮播 | `swiper` | :indicator-dots="true",:autoplay="false" | +| 照片大图 | `uni-popup` | type="center",图片全屏预览 | +| 维修记录 | `uni-steps` | :options="timeLineData",direction="column" | +| 信息列表 | `uni-list` + `uni-list-item` | :showArrow="false",展示各项信息 | +| 操作按钮区 | `view` + `button` | 固定底部,按钮根据角色动态渲染 | +| 催单确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 工单信息 | 必须返回有效工单数据 | "工单信息加载失败,请重试" | +| 照片 | 加载失败显示占位图 | — | +| 操作权限 | 根据角色和状态校验 | "您暂无此操作权限" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),信息区域宽度自适应 +- **横竖屏适配策略**:竖屏垂直布局;横屏信息区和照片区可并排展示 +- **手势交互规范**:操作按钮≥44px;照片可点击放大≥44px;左右滑动切换照片 +- **安全区域**:底部操作按钮区适配底部安全区,padding-bottom=env(safe-area-inset-bottom) + ## 需求追溯 | 功能点编号 | 功能名称 | 文档来源 | 后续服务 | 关联功能 | diff --git a/docs/02-功能清单-小程序端/03-巡检相关功能.md b/docs/02-功能清单-小程序端/03-巡检相关功能.md index 6328ec3..df9f428 100644 --- a/docs/02-功能清单-小程序端/03-巡检相关功能.md +++ b/docs/02-功能清单-小程序端/03-巡检相关功能.md @@ -3,6 +3,7 @@ > 模块编码:inspection > 端侧:小程序 > 关联文档:01-模块划分.md §3.2、02-功能清单-小程序端.md §3、03-业务流转逻辑-小程序端.md §3、05-接口规范.md §9.2、06-项目技术要求.md §4.4&5.4 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -45,6 +46,45 @@ | 开始巡检 | inspection:task:view | 跳转巡检执行页 | | 查看详情 | inspection:task:view | 已完成的任务查看详情 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时自动请求当日巡检任务列表,显示骨架屏占位;获取任务后渲染统计卡片与任务列表;无任务时显示空状态插图与提示文案 +2. **查询/筛选交互流程**:无手动筛选条件,自动按当日+本人过滤;支持下拉刷新重新加载任务数据 +3. **表单填写与提交流程**:本页面无表单提交操作 +4. **弹窗/弹层交互流程**:无弹窗 +5. **行内操作流程**:点击「开始巡检」→ 校验蓝牙打卡状态 → 跳转巡检执行页;点击「查看详情」→ 跳转巡检详情页 +6. **异常与错误处理**:网络异常显示重试提示;蓝牙状态异常在卡片上以图标提示;任务加载失败显示错误页与重试按钮 +7. **联动/级联交互**:统计卡片数字与列表数据联动实时更新 +8. **权限控制交互表现**:无权限时隐藏「开始巡检」和「查看详情」按钮;按钮置灰且不可点击 +9. **H1 防重复提交**:「开始巡检」「查看详情」按钮点击时须设置 `:loading` + `:disabled` 双重锁定;下拉刷新期间禁止重复触发;使用 pending Promise 去重;分页请求切换月份时 abort 上一次请求 +10. **H2 超时控制**:GET 请求超时 15s,页面加载 >3s 时须调用 `wx.showLoading({title:'加载中...', mask:true})`;请求超时时提示"网络请求超时,请重试"并显示重试按钮 +11. **H3 操作确认**:点击「开始巡检」前若存在未完成巡检任务,须通过 `wx.showModal` 二次确认,内容含后果说明(如"当前有未完成任务,是否继续?");跳转操作无需二次确认 +12. **H8 反馈规范**:任务加载成功后使用 `wx.showToast({icon:'success'})` 反馈;加载失败使用 `wx.showToast({icon:'none', title:'具体错误信息'})` 显示多文字提示;网络异常(`wx.getNetworkType` 判断非 wifi/4g)时提示弱网络并提供重试入口 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 统计卡片 | `uni-card` | mode="center",三列等宽布局 | +| 任务列表 | `uni-list` + `uni-list-item` | clickable=true,showArrow=true | +| 状态标签 | `uni-tag` | type: success(已完成)/warning(进行中)/error(异常)/default(待执行) | +| 蓝牙状态图标 | `uni-icons` | type="bluetooth",size="22" | +| 下拉刷新 | `uni-refresher` | @onRefresh 回调,threshold=45px | +| 空状态 | `uni-section` | 插槽自定义空状态插图与文案 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 任务列表 | 加载失败时允许重试 | "加载失败,请下拉刷新重试" | +| 统计数据 | 数据为空时显示0 | — | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),卡片宽度自适应屏幕宽度,左右padding 16px +- **横竖屏适配策略**:竖屏为默认布局;横屏时统计卡片改为横向平铺,任务列表改为双列卡片 +- **手势交互规范**:所有可点击区域≥44px;下拉刷新触发区域≥50px;卡片内按钮间距≥8px + ### 页面2:蓝牙打卡页 - **页面路径**:`/pages/inspection/bluetooth-checkin` @@ -68,6 +108,50 @@ | 重新扫描 | — | 蓝牙扫描超时后可重试(最多3次) | | 进入补录模式 | — | 蓝牙无法连接时,进入补录模式 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时查询蓝牙策略→策略=REQUIRED自动启动蓝牙扫描→显示扫描动画;策略=OPTIONAL显示打卡方式选择 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:自动扫描蓝牙Beacon→检测到Beacon→自动连接→连接成功打卡→显示打卡结果;扫描超时→提示重新扫描或进入补录模式 +4. **弹窗/弹层交互流程**:打卡成功弹出成功提示;扫描超时弹出重试提示;策略=OPTIONAL弹出打卡方式选择弹窗 +5. **行内操作流程**:点击重新扫描→重新搜索蓝牙;点击进入补录模式→跳转补录页 +6. **异常与错误处理**:蓝牙未开启提示"请开启手机蓝牙";扫描超时3次后自动显示补录入口;蓝牙连接失败提示重试;离线时提示"网络不可用" +7. **联动/级联交互**:蓝牙策略与扫描行为联动,策略=REQUIRED自动扫描,策略=OPTIONAL手动选择 +8. **权限控制交互表现**:所有巡检人员可访问;蓝牙权限被拒时引导开启 +9. **H1 防重复提交**:「重新扫描」按钮点击时须设置 `:loading="scanning"` + `:disabled` 双重锁定,扫描期间禁止重复点击;「进入补录模式」按钮同样需要 loading+disabled 锁定;使用 pending 标志位防止重复发起扫描 +10. **H2 超时控制**:蓝牙扫描超时 3s,策略查询 GET 请求超时 15s;扫描等待 >3s 时须显示 `wx.showLoading({title:'正在扫描...', mask:true})`;扫描超时或请求超时均需提示用户并提供重试入口 +11. **H3 操作确认**:点击「进入补录模式」前须通过 `wx.showModal` 确认,内容说明"进入补录模式后将记录为手动打卡,是否继续?" +12. **H4 脏数据检测**:本页面以自动扫描为主,若用户手动修改过任何数据则标记 isDirty;`onUnload` 生命周期中检测到 isDirty 且未提交时弹出 `wx.showModal` 提示"未完成的打卡将丢失,确定离开?" +13. **H7 上传约束**:本页面无上传功能(拍照在巡检执行页完成),此项不适用 +14. **H8 反馈规范**:蓝牙连接成功使用 `wx.showToast({icon:'success', title:'打卡成功'})`;扫描失败使用 `wx.showToast({icon:'none', title:'未检测到蓝牙信号,请重试或补录'})`;网络异常通过 `wx.getNetworkType` 判断后提供重试 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 扫描动画 | `view` + CSS动画 | 旋转动画,size=80px | +| 状态提示 | `text` | font-size="14px",动态文案 | +| Beacon信息 | `uni-list` + `uni-list-item` | :showArrow="false",显示UUID/RSSI | +| 重新扫描 | `button` | type="default",:loading="scanning" | +| 进入补录模式 | `button` | type="warn",@click="goSupplement" | +| 打卡结果 | `uni-popup` | type="dialog",打卡成功/失败提示 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 蓝牙状态 | 必须开启蓝牙 | "请开启手机蓝牙" | +| Beacon信号 | RSSI > -70dBm | "蓝牙信号弱,请靠近打卡点" | +| 扫描超时 | 3秒超时,最多重试3次 | "蓝牙扫描超时,请重试或进入补录模式" | +| 打卡点验证 | Beacon必须属于本人班组打卡点 | "未检测到有效打卡点" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),扫描动画居中 +- **横竖屏适配策略**:竖屏垂直布局;横屏动画与信息并排展示 +- **手势交互规范**:按钮可点击区域≥44px;动画区域≥80px +- **安全区域**:底部按钮适配底部安全区 + ### 页面3:巡检执行页 - **页面路径**:`/pages/inspection/execute` @@ -100,6 +184,55 @@ | 一键生成报修 | — | 异常项可直接生成报修工单(数据写入报修模块) | | 提交巡检结果 | inspection:task:view | 提交巡检记录 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取巡检任务清单→渲染检查项列表和进度条;查询蓝牙策略→蓝牙策略=REQUIRED时显示蓝牙连接状态指示 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:逐项标记正常/异常→异常项展开附加信息→选择严重等级→输入描述→拍照上传(蓝牙策略=REQUIRED须蓝牙连接下拍照)→点击提交巡检结果→确认弹窗→提交成功 +4. **弹窗/弹层交互流程**:点击拍照弹出选择(拍照/相册);提交前弹出确认弹窗"确认提交巡检结果?";点击一键生成报修弹出确认弹窗 +5. **行内操作流程**:点击标记正常→该项标记为正常;点击标记异常→展开附加信息区;点击拍照→调起相机;点击一键生成报修→创建报修工单 +6. **异常与错误处理**:蓝牙断连时拍照提示"请连接蓝牙后拍照";提交失败显示重试;未完成所有检查项时提示"请完成所有检查项" +7. **联动/级联交互**:检查进度与进度条联动;异常项标记与附加信息展开联动 +8. **权限控制交互表现**:仅巡检人员可操作;无inspection:task:view权限时提交按钮置灰 +9. **H1 防重复提交**:「标记正常」「标记异常」「提交巡检结果」按钮点击时须设置 `:loading` + `:disabled` 双重锁定;「一键生成报修」按钮同样需要防重复;提交期间禁止重复操作 +10. **H2 超时控制**:获取巡检清单 GET 请求超时 15s;提交巡检结果 POST 请求超时 30s;请求耗时 >3s 时须调用 `wx.showLoading({mask:true})`;超时提示"提交超时,请检查网络后重试" +11. **H3 操作确认**:点击「提交巡检结果」前须通过 `wx.showModal` 二次确认,内容包括已检查项数、正常/异常统计及后果说明;点击「一键生成报修」前须确认"确认为此异常项生成报修工单?" +12. **H4 脏数据检测**:页面加载时对初始巡检清单数据进行快照(JSON.stringify 深拷贝);每次标记正常/异常或填写附加信息时对比快照检测 isDirty;`onUnload` 中检测到 isDirty 且未提交时弹出 `wx.showModal({content:"巡检数据尚未保存,确定离开?"})` 拦截离开 +13. **H7 上传约束**:异常项照片上传单个文件 ≤10MB,每项最多 ≤9 张;使用 `uni-file-picker` 配合进度回调 `@progress` 实时展示上传百分比;超出限制时提示"照片大小不能超过10MB"或"最多上传9张照片" +14. **H8 反馈规范**:提交成功使用 `wx.showToast({icon:'success', title:'巡检结果已提交'})`;校验失败(如未完成所有检查项)使用 `wx.showToast({icon:'none', title:'请完成所有检查项后再提交'})`;网络异常时通过 `wx.getNetworkType` 判断并提供重试按钮 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 进度条 | `progress` | :percent="progress",show-info=true | +| 蓝牙状态指示 | `uni-icons` | type="bluetooth",size="18",color动态 | +| 检查项列表 | `uni-list` + `uni-list-item` | :showArrow="false",可展开 | +| 正常/异常切换 | `uni-data-checkbox` | :localdata="[{value:'normal',text:'正常'},{value:'abnormal',text:'异常'}]" | +| 严重等级选择 | `uni-data-select` | :localdata="levelList" | +| 异常描述 | `uni-easyinput` | type="textarea",maxlength="500" | +| 照片上传 | `uni-file-picker` | limit="9",file-mediatype="image" | +| 一键生成报修 | `button` | type="warn",size="mini" | +| 提交按钮 | `button` | type="primary",:loading="submitting" | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 检查项 | 必须逐项完成 | "请完成所有检查项" | +| 异常项严重等级 | 异常时必选 | "请选择严重等级" | +| 异常项描述 | 异常时必填 | "请填写异常描述" | +| 异常项照片 | 异常时至少1张 | "请上传异常照片" | +| 蓝牙状态 | 策略=REQUIRED拍照时须蓝牙连接 | "请连接蓝牙后拍照" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),检查项列表宽度自适应 +- **横竖屏适配策略**:竖屏垂直列表;横屏异常项展开区域更宽 +- **手势交互规范**:检查项可点击区域≥44px;按钮≥44px;照片上传区域≥44px +- **安全区域**:底部提交按钮适配底部安全区 + ### 页面4:异常上报页 - **页面路径**:`/pages/inspection/report-abnormal` @@ -120,6 +253,45 @@ | 拍照上传 | — | 拍摄异常照片 | | 提交上报 | — | 上报异常,如选择生成工单则同时创建报修工单 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时初始化表单,获取报修类型字典 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:选择严重等级→输入异常描述→拍照上传(含水印)→可选开启"生成报修工单"开关→选择报修类型→点击提交上报→确认弹窗→上报成功 +4. **弹窗/弹层交互流程**:点击拍照弹出选择(拍照/相册);提交前弹出确认弹窗 +5. **行内操作流程**:选择严重等级→输入描述→上传照片→开启生成报修工单→选择报修类型→提交 +6. **异常与错误处理**:图片上传失败提示重传;提交失败显示重试 +7. **联动/级联交互**:生成报修工单开关与报修类型选择联动,开启后显示报修类型选择器 +8. **权限控制交互表现**:所有巡检人员可操作 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 严重等级 | `uni-data-select` | :localdata="[{value:'minor',text:'一般'},{value:'major',text:'较重'},{value:'critical',text:'严重'}]" | +| 异常描述 | `uni-easyinput` | type="textarea",maxlength="500",:showWordLimit="true" | +| 照片上传 | `uni-file-picker` | limit="9",file-mediatype="image",:auto-upload="true" | +| 生成报修工单 | `switch` | @change="onSwitchChange" | +| 报修类型 | `uni-data-select` | v-model="repairType",:localdata="repairTypeList",v-if="generateRepair" | +| 提交按钮 | `button` | type="primary",:loading="submitting" | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 严重等级 | 必选 | "请选择严重等级" | +| 异常描述 | 必填,最多500字 | "请填写异常描述" / "描述不能超过500字" | +| 照片 | 至少1张,最多9张 | "请上传异常照片" / "照片最多上传9张" | +| 报修类型 | 开启生成报修工单时必选 | "请选择报修类型" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),表单宽度自适应 +- **横竖屏适配策略**:竖屏垂直布局;横屏照片区3~4列网格 +- **手势交互规范**:选择器≥44px;提交按钮≥44px;照片上传区域≥44px +- **安全区域**:底部提交按钮适配底部安全区 + ### 页面5:巡检历史列表 - **页面路径**:`/pages/inspection/history` @@ -151,6 +323,42 @@ |------|------|------| | 查看详情 | inspection:task:view | 查看巡检记录详情 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时默认加载当月巡检记录→显示骨架屏→渲染月份选择器和记录列表 +2. **查询/筛选交互流程**:切换月份→重新加载对应月份记录;下拉刷新重新加载;上拉加载更多 +3. **表单填写与提交流程**:无表单提交操作 +4. **弹窗/弹层交互流程**:无弹窗 +5. **行内操作流程**:点击查看详情→跳转巡检记录详情页;左右滑动切换月份 +6. **异常与错误处理**:记录加载失败显示重试;无记录显示空状态插图和文案 +7. **联动/级联交互**:月份切换与列表数据联动 +8. **权限控制交互表现**:仅巡检人员可访问本人数据;主管可查看本班组数据;无inspection:task:view权限时隐藏查看详情 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 月份选择器 | `uni-datetime-picker` | type="date",fields="month" | +| 记录列表 | `uni-list` + `uni-list-item` | clickable=true,showArrow=true | +| 状态标签 | `uni-tag` | type: success(正常)/error(异常) | +| 下拉刷新 | `uni-refresher` | @onRefresh回调 | +| 上拉加载 | `uni-load-more` | :status="loadingStatus" | +| 空状态 | `uni-section` | 插槽自定义空状态插图与文案 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 月份 | 必须选择有效月份 | — | +| 记录列表 | 加载失败允许重试 | "加载失败,请下拉刷新重试" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),卡片宽度自适应,padding 16px +- **横竖屏适配策略**:竖屏单列卡片;横屏双列卡片展示 +- **手势交互规范**:记录项可点击区域≥44px;月份选择器≥44px;支持下拉刷新和上拉加载 +- **安全区域**:底部列表适配底部安全区 + ### 页面6:异常数据补录页 - **页面路径**:`/pages/inspection/supplement` @@ -172,6 +380,54 @@ |------|------|------| | 提交补录申请 | — | 提交后等待主管审核 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时显示补录模式提示(红色警示条)→获取巡检区域和检查项数据 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:选择补录原因→填写补录说明→上传证明材料→手动选择巡检时间→选择巡检区域→逐项填写检查项结果→点击提交补录申请→确认弹窗→提交成功等待主管审核 +4. **弹窗/弹层交互流程**:点击上传弹出选择(拍照/相册);提交前弹出确认弹窗"确认提交补录申请?" +5. **行内操作流程**:选择原因→填写说明→上传照片→选择时间和区域→填写检查项→提交 +6. **异常与错误处理**:图片上传失败提示重传;提交失败显示重试;离线时数据暂存本地 +7. **联动/级联交互**:补录原因与其他字段无强联动;巡检区域与检查项联动 +8. **权限控制交互表现**:仅巡检人员可操作;补录数据标记is_supplement=true +9. **H1 防重复提交**:「提交补录申请」按钮点击时须设置 `:loading="submitting"` + `:disabled` 双重锁定;提交期间禁止重复点击 +10. **H2 超时控制**:获取区域和检查项数据 GET 请求超时 15s;提交补录 POST 请求超时 30s;证明材料上传超时 60s;>3s 时须 `wx.showLoading({mask:true})`;超时后提示并提供重试 +11. **H3 操作确认**:点击「提交补录申请」前须通过 `wx.showModal` 二次确认,内容包括补录原因摘要、说明字数及"提交后需等待主管审核"的后果说明 +12. **H4 脏数据检测**:页面初始化时保存空白表单快照;选择补录原因、填写说明、上传材料、选择时间/区域、填写检查项等任一操作触发 isDirty=true;`onUnload` 中检测 isDirty 时弹出 `wx.showModal({content:"补录信息尚未提交,确定离开?"})` 拦截 +13. **H7 上传约束**:证明材料上传单个文件 ≤10MB,最多 ≤3 张(业务限制);使用 `uni-file-picker` 的 `@progress` 回调显示上传进度;超出限制时前端拦截并提示 +14. **H8 反馈规范**:提交成功使用 `wx.showToast({icon:'success', title:'补录申请已提交,待审核'})`;校验失败使用 `wx.showToast({icon:'none', title:'具体错误信息'})`;离线暂存本地时提示"已暂存至本地,联网后将自动提交" + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 补录原因 | `uni-data-select` | :localdata="[{value:'bluetooth',text:'蓝牙失灵'},{value:'location',text:'定位失败'},{value:'system',text:'系统异常'},{value:'other',text:'其他'}]" | +| 补录说明 | `uni-easyinput` | type="textarea",maxlength="200",:showWordLimit="true" | +| 证明材料上传 | `uni-file-picker` | limit="3",file-mediatype="image" | +| 巡检时间 | `uni-datetime-picker` | type="datetime" | +| 巡检区域 | `uni-data-select` | v-model="areaId",:localdata="areaList" | +| 检查项结果 | `uni-data-checkbox` | 每项动态渲染,正常/异常 | +| 提交按钮 | `button` | type="primary",:loading="submitting" | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 补录原因 | 必选 | "请选择补录原因" | +| 补录说明 | 必填,最多200字 | "请填写补录说明" / "补录说明不能超过200字" | +| 证明材料 | 最多3张 | "证明材料最多上传3张" | +| 巡检时间 | 必选 | "请选择巡检时间" | +| 巡检区域 | 必选 | "请选择巡检区域" | +| 检查项结果 | 必须逐项填写 | "请完成所有检查项" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),表单宽度自适应 +- **横竖屏适配策略**:竖屏垂直布局;横屏表单水平分组展示 +- **手势交互规范**:选择器≥44px;提交按钮≥44px;上传区域≥44px +- **安全区域**:底部提交按钮适配底部安全区 + ## 需求追溯 | 功能点编号 | 功能名称 | 文档来源 | 后续服务 | 关联功能 | diff --git a/docs/02-功能清单-小程序端/04-保洁相关功能.md b/docs/02-功能清单-小程序端/04-保洁相关功能.md index 8625eec..679cc3f 100644 --- a/docs/02-功能清单-小程序端/04-保洁相关功能.md +++ b/docs/02-功能清单-小程序端/04-保洁相关功能.md @@ -3,6 +3,7 @@ > 模块编码:cleaning > 端侧:微信小程序 > 关联文档:01-模块划分.md(v4.0)、02-功能清单-小程序端.md(§4)、03-业务流转逻辑-小程序端.md(§4)、05-接口规范.md(§9)、06-项目技术要求.md +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -42,6 +43,46 @@ - 「开始执行」→ 进入保洁执行页(权限:cleaning:task:update) - 「查看详情」→ 查看任务详情 +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取当日保洁任务列表→显示骨架屏→渲染统计卡片和任务列表 +2. **查询/筛选交互流程**:无手动筛选,自动按当天+本人过滤;支持下拉刷新 +3. **表单填写与提交流程**:无表单提交操作 +4. **弹窗/弹层交互流程**:无弹窗 +5. **行内操作流程**:点击「开始执行」→校验蓝牙打卡状态→跳转保洁执行页;点击「查看详情」→查看任务详情 +6. **异常与错误处理**:任务加载失败显示重试;无任务显示空状态插图和文案 +7. **联动/级联交互**:统计卡片数字与列表数据联动更新 +8. **权限控制交互表现**:无cleaning:task:update权限时隐藏「开始执行」按钮 +9. **H1 防重复提交**:「开始执行」「查看详情」按钮点击时须设置 `:loading` + `:disabled` 双重锁定;下拉刷新期间禁止重复触发;使用 pending Promise 去重 +10. **H2 超时控制**:获取保洁任务列表 GET 请求超时 15s;页面加载 >3s 时须调用 `wx.showLoading({title:'加载中...', mask:true})`;请求超时时提示"网络请求超时,请下拉刷新重试" +11. **H3 操作确认**:点击「开始执行」前若存在进行中任务,须通过 `wx.showModal` 二次确认,内容含后果说明 +12. **H8 反馈规范**:任务加载成功使用静默渲染;加载失败使用 `wx.showToast({icon:'none', title:'加载失败,请重试'})`;网络异常通过 `wx.getNetworkType` 判断后提供重试入口 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 统计卡片 | `uni-card` | mode="center",三列等宽布局 | +| 任务列表 | `uni-list` + `uni-list-item` | clickable=true,showArrow=true | +| 状态标签 | `uni-tag` | type: success(已完成)/warning(进行中)/error(超时)/default(待执行) | +| 蓝牙状态图标 | `uni-icons` | type="bluetooth",size="22" | +| 下拉刷新 | `uni-refresher` | @onRefresh回调,threshold=45px | +| 空状态 | `uni-section` | 插槽自定义空状态插图与文案 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 任务列表 | 加载失败时允许重试 | "加载失败,请下拉刷新重试" | +| 统计数据 | 数据为空时显示0 | — | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),卡片宽度自适应,padding 16px +- **横竖屏适配策略**:竖屏单列卡片;横屏统计卡片横向平铺,任务列表双列卡片 +- **手势交互规范**:任务项可点击区域≥44px;下拉刷新触发区域≥50px +- **安全区域**:底部适配底部安全区 + **需求追溯**: | 功能点编号 | 功能名称 | 文档来源 | 后续服务 | 关联功能 | @@ -82,6 +123,51 @@ |------------|----------|----------|----------|----------| | MP-CL-02 | 蓝牙强制打卡确认 | 02-小程序端 §4 | 保洁执行 | 保洁补录、系统蓝牙配置 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时查询蓝牙策略→策略=REQUIRED自动启动蓝牙扫描→显示扫描动画;策略=OPTIONAL显示打卡方式选择 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:自动扫描蓝牙Beacon→检测到→连接→打卡成功(check_type=BLUETOOTH);扫描失败→重试/进入补录模式 +4. **弹窗/弹层交互流程**:打卡成功弹出成功提示;扫描超时弹出重试提示 +5. **行内操作流程**:点击重新扫描→重新搜索蓝牙;点击进入补录模式→跳转补录申请页;点击确认打卡→记录蓝牙打卡 +6. **异常与错误处理**:蓝牙未开启提示"请开启手机蓝牙";扫描超时3次显示补录入口;连接失败提示重试 +7. **联动/级联交互**:蓝牙策略与扫描行为联动 +8. **权限控制交互表现**:所有保洁人员可访问 +9. **H1 防重复提交**:「重新扫描」「确认打卡」「进入补录模式」按钮点击时均须设置 `:loading` + `:disabled` 双重锁定;扫描期间使用 pending 标志位防止重复发起蓝牙扫描 +10. **H2 超时控制**:蓝牙策略查询 GET 请求超时 15s;蓝牙扫描超时 3s;>3s 时须显示 `wx.showLoading({title:'正在扫描...', mask:true})`;超时或连接失败均提示并提供重试/补录选项 +11. **H3 操作确认**:点击「进入补录模式」前须通过 `wx.showModal` 确认,内容说明"进入补录后将记录为手动打卡,是否继续?";点击「确认打卡」前提示"确认在此位置打卡?" +12. **H4 脏数据检测**:本页面以自动操作为主,若用户手动修改过任何信息则标记 isDirty;`onUnload` 中检测到 isDirty 且未完成打卡时弹出 `wx.showModal({content:"未完成的打卡将丢失,确定离开?"})` +13. **H7 上传约束**:本页面无上传功能,此项不适用 +14. **H8 反馈规范**:打卡成功使用 `wx.showToast({icon:'success', title:'打卡成功'})`;扫描失败使用 `wx.showToast({icon:'none', title:'未检测到蓝牙信号'})`;网络异常提供重试入口 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 扫描动画 | `view` + CSS动画 | 旋转动画,size=80px | +| 蓝牙状态图标 | `uni-icons` | type="bluetooth",size=40 | +| 打卡点名称 | `text` | font-size="18px",font-weight="bold" | +| 信号强度 | `text` | font-size="14px",动态显示RSSI | +| 重新扫描 | `button` | type="default",:loading="scanning" | +| 进入补录模式 | `button` | type="warn" | +| 确认打卡 | `button` | type="primary" | +| 打卡结果 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 蓝牙状态 | 必须开启蓝牙 | "请开启手机蓝牙" | +| Beacon信号 | RSSI > -70dBm | "蓝牙信号弱,请靠近打卡点" | +| 扫描超时 | 3秒超时,最多重试3次 | "蓝牙扫描超时" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),扫描动画居中 +- **横竖屏适配策略**:竖屏垂直布局;横屏动画与信息并排展示 +- **手势交互规范**:按钮可点击区域≥44px +- **安全区域**:底部按钮适配底部安全区 + --- ### 页面3:保洁执行 @@ -119,6 +205,50 @@ |------------|----------|----------|----------|----------| | MP-CL-03 | 保洁执行 | 02-小程序端 §4 | 主管抽查 | 保洁异常反馈 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取保洁任务详情和清单→渲染任务信息和检查项列表;查询蓝牙拍照策略 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:逐项点击「完成该项」→标记异常项点击「标记异常」→跳转异常反馈页→完成后拍照上传→点击「提交完成」→确认弹窗→提交成功 +4. **弹窗/弹层交互流程**:点击拍照弹出选择(拍照/相册);提交前弹出确认弹窗 +5. **行内操作流程**:点击完成该项→该项标记完成;点击标记异常→跳转异常反馈页;点击拍照→调起相机(蓝牙策略=REQUIRED须蓝牙连接下拍照) +6. **异常与错误处理**:蓝牙断连时拍照提示"请连接蓝牙后拍照";提交失败显示重试 +7. **联动/级联交互**:检查项完成进度与任务状态联动 +8. **权限控制交互表现**:无cleaning:task:update权限时按钮置灰 +9. **H1 防重复提交**:「完成该项」「标记异常」「提交完成」按钮点击时均须设置 `:loading` + `:disabled` 双重锁定;提交期间禁止重复操作;拍照调用同样需要防抖处理 +10. **H2 超时控制**:获取任务详情和清单 GET 请求超时 15s;提交完成 POST 请求超时 30s;照片上传超时 60s;>3s 时须 `wx.showLoading({mask:true})`;超时后提示具体原因及重试选项 +11. **H3 操作确认**:点击「提交完成」前须通过 `wx.showModal` 二次确认,内容包括已完成项数、异常项数及"确认提交后将进入待抽查状态"的后果说明 +12. **H4 脏数据检测**:页面加载时对初始清单数据进行快照(深拷贝);每次点击完成/标记异常或上传照片时对比快照检测 isDirty;`onUnload` 中检测到 isDirty 且未提交时弹出 `wx.showModal({content:"保洁数据尚未保存,确定离开?"})` +13. **H7 上传约束**:完成照片上传单个文件 ≤10MB,最多 ≤9 张;使用 `uni-file-picker` 的 `@progress` 回调实时展示上传进度百分比;蓝牙策略=REQUIRED 时拍照前校验蓝牙连接状态 +14. **H8 反馈规范**:提交成功使用 `wx.showToast({icon:'success', title:'保洁任务已提交'})`;校验失败使用 `wx.showToast({icon:'none', title:'请完成所有保洁项目'})` 多文字提示;网络异常判断并提供重试 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 任务信息卡片 | `uni-card` | mode="basic",只读展示 | +| 保洁清单列表 | `uni-list` + `uni-list-item` | :showArrow="false",可勾选 | +| 完成该项 | `button` | type="default",size="mini",@click="completeItem" | +| 标记异常 | `button` | type="warn",size="mini",@click="reportAbnormal" | +| 照片上传 | `uni-file-picker` | limit="9",file-mediatype="image" | +| 提交完成 | `button` | type="primary",:loading="submitting" | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 保洁清单 | 必须逐项完成或标记异常 | "请完成所有保洁项目" | +| 完成照片 | 至少1张 | "请上传完成照片" | +| 蓝牙状态 | 策略=REQUIRED拍照时须蓝牙连接 | "请连接蓝牙后拍照" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),列表宽度自适应 +- **横竖屏适配策略**:竖屏垂直列表;横屏清单与照片区并排 +- **手势交互规范**:列表项可点击区域≥44px;按钮≥44px +- **安全区域**:底部提交按钮适配底部安全区 + --- ### 页面4:异常反馈 @@ -146,6 +276,42 @@ |------------|----------|----------|----------|----------| | MP-CL-04 | 异常反馈 | 02-小程序端 §4 | 通知主管 | 保洁管理(Web端) | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时初始化表单 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:输入异常描述→选择严重程度→拍照上传→点击提交反馈→确认弹窗→提交成功通知主管 +4. **弹窗/弹层交互流程**:点击拍照弹出选择(拍照/相册);提交前弹出确认弹窗 +5. **行内操作流程**:输入描述→选择严重程度→上传照片→提交 +6. **异常与错误处理**:图片上传失败提示重传;提交失败显示重试 +7. **联动/级联交互**:无强联动 +8. **权限控制交互表现**:所有保洁人员可操作 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 异常描述 | `uni-easyinput` | type="textarea",maxlength="500",:showWordLimit="true" | +| 严重程度 | `uni-data-select` | :localdata="[{value:'minor',text:'一般'},{value:'major',text:'严重'},{value:'critical',text:'紧急'}]" | +| 照片上传 | `uni-file-picker` | limit="9",file-mediatype="image",:auto-upload="true" | +| 提交按钮 | `button` | type="primary",:loading="submitting" | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 异常描述 | 必填,最多500字 | "请填写异常描述" / "描述不能超过500字" | +| 严重程度 | 必选 | "请选择严重程度" | +| 照片 | 最多9张,含水印 | "照片最多上传9张" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),表单宽度自适应 +- **横竖屏适配策略**:竖屏垂直布局;横屏照片区3~4列网格 +- **手势交互规范**:选择器≥44px;提交按钮≥44px;上传区域≥44px +- **安全区域**:底部提交按钮适配底部安全区 + --- ### 页面5:保洁抽查 @@ -185,6 +351,45 @@ |------------|----------|----------|----------|----------| | MP-CL-05 | 保洁抽查 | 02-小程序端 §4 | 不合格时重新生成任务 | 保洁管理(Web端) | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取待抽查任务列表→显示骨架屏→渲染筛选条件和任务列表 +2. **查询/筛选交互流程**:日期/区域/状态筛选条件→切换筛选重新加载列表;下拉刷新 +3. **表单填写与提交流程**:点击任务→弹出抽查结果标记弹窗→选择合格/不合格→不合格时填写原因+上传照片→确认提交 +4. **弹窗/弹层交互流程**:点击抽查→弹出抽查标记弹窗(合格/不合格选择+原因输入) +5. **行内操作流程**:点击合格→确认完成;点击不合格→填写原因→确认→系统自动重新生成任务 +6. **异常与错误处理**:列表加载失败显示重试;抽查操作失败显示重试 +7. **联动/级联交互**:不合格标记与任务重新生成联动 +8. **权限控制交互表现**:仅主管可操作;无cleaning:spot-check:approve权限时按钮置灰 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 筛选条件 | `uni-data-select` | 日期/区域/状态三个选择器 | +| 任务列表 | `uni-list` + `uni-list-item` | clickable=true | +| 抽查标记弹窗 | `uni-popup` | type="bottom",弹出抽查操作面板 | +| 合格按钮 | `button` | type="primary",@click="markQualified" | +| 不合格按钮 | `button` | type="warn",@click="markUnqualified" | +| 不合格原因 | `uni-easyinput` | type="textarea",maxlength="200" | +| 不合格照片 | `uni-file-picker` | limit="9",file-mediatype="image" | +| 下拉刷新 | `uni-refresher` | @onRefresh回调 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 抽查结果 | 必选(合格/不合格) | "请选择抽查结果" | +| 不合格原因 | 不合格时必填 | "请填写不合格原因" | +| 不合格照片 | 不合格时至少1张 | "请上传不合格照片" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),列表宽度自适应 +- **横竖屏适配策略**:竖屏单列卡片;横屏双列卡片+底部弹窗更宽 +- **手势交互规范**:任务项可点击区域≥44px;按钮≥44px +- **安全区域**:底部弹窗适配底部安全区 + --- ### 页面6:保洁历史 @@ -220,6 +425,45 @@ |------------|----------|----------|----------|----------| | MP-CL-06 | 保洁历史 | 02-小程序端 §4 | 无 | 保洁执行 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时默认加载当月保洁记录→显示骨架屏→渲染月份选择器和记录列表 +2. **查询/筛选交互流程**:切换月份→重新加载;状态筛选(全部/正常/异常/补录)→过滤记录;下拉刷新 +3. **表单填写与提交流程**:无表单提交操作 +4. **弹窗/弹层交互流程**:无弹窗 +5. **行内操作流程**:点击查看详情→查看历史任务详情 +6. **异常与错误处理**:记录加载失败显示重试;无记录显示空状态 +7. **联动/级联交互**:月份和状态筛选与列表数据联动 +8. **权限控制交互表现**:仅保洁人员可访问本人数据 +9. **H1 防重复提交**:「查看详情」按钮设置防重复标记;下拉刷新期间禁止重复触发;切换月份/状态筛选时 abort 未完成的上一次请求 +10. **H2 超时控制**:获取历史记录 GET 请求超时 15s;切换月份加载 >3s 时须 `wx.showLoading({mask:true})`;超时提示"加载超时,请重试" +11. **H3 操作确认**:本页面主要为列表查看,无需操作确认 +12. **H8 反馈规范**:记录加载成功静默渲染;加载失败使用 `wx.showToast({icon:'none', title:'记录加载失败,请重试'})`;空数据显示空状态插图;网络异常提供重试 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 月份选择器 | `uni-datetime-picker` | type="date",fields="month" | +| 状态筛选 | `uni-segmented-control` | :values="['全部','正常','异常','补录']" | +| 记录列表 | `uni-list` + `uni-list-item` | clickable=true,showArrow=true | +| 状态标签 | `uni-tag` | type: success(正常)/error(异常)/primary(补录) | +| 下拉刷新 | `uni-refresher` | @onRefresh回调 | +| 空状态 | `uni-section` | 插槽自定义空状态插图与文案 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 记录列表 | 加载失败允许重试 | "加载失败,请下拉刷新重试" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),卡片宽度自适应 +- **横竖屏适配策略**:竖屏单列卡片;横屏双列卡片展示 +- **手势交互规范**:记录项可点击区域≥44px;支持下拉刷新 +- **安全区域**:底部适配底部安全区 + --- ### 页面7:异常数据补录 @@ -252,6 +496,53 @@ |------------|----------|----------|----------|----------| | MP-CL-07 | 异常数据补录 | 02-小程序端 §4 | 主管审核补录 | 操作日志、考勤补录审核 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时显示补录模式提示(红色警示条)→获取保洁区域和检查项数据 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:选择补录原因→填写补录说明→上传证明材料→手动填写保洁记录(时间/区域/检查项)→点击提交补录→确认弹窗→提交成功等待审核 +4. **弹窗/弹层交互流程**:点击上传弹出选择(拍照/相册);提交前弹出确认弹窗 +5. **行内操作流程**:选择原因→填写说明→上传照片→填写保洁记录→提交 +6. **异常与错误处理**:图片上传失败提示重传;提交失败显示重试;离线暂存本地 +7. **联动/级联交互**:补录原因与表单无强联动 +8. **权限控制交互表现**:仅保洁人员可操作;补录数据标记is_supplement=true +9. **H1 防重复提交**:「提交补录」按钮点击时须设置 `:loading="submitting"` + `:disabled` 双重锁定;提交期间禁止重复点击 +10. **H2 超时控制**:获取区域和检查项 GET 请求超时 15s;提交补录 POST 请求超时 30s;证明材料上传超时 60s;>3s 时须 `wx.showLoading({mask:true})`;超时后提示并提供重试 +11. **H3 操作确认**:点击「提交补录」前须通过 `wx.showModal` 二次确认,内容包括补录原因摘要及"提交后需等待主管审核"的后果说明 +12. **H4 脏数据检测**:页面初始化时保存空白表单快照;选择原因、填写说明、上传材料、填写记录等任一操作触发 isDirty=true;`onUnload` 中检测 isDirty 时弹出 `wx.showModal({content:"补录信息尚未提交,确定离开?"})` +13. **H7 上传约束**:证明材料上传单个文件 ≤10MB,最多 ≤3 张(业务限制);使用 `uni-file-picker` 的 `@progress` 回调显示上传进度;超出限制时前端拦截并提示 +14. **H8 反馈规范**:提交成功使用 `wx.showToast({icon:'success', title:'补录申请已提交,待审核'})`;校验失败使用 `wx.showToast({icon:'none', title:'具体错误信息'})`;离线暂存本地时提示"已暂存至本地" + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 补录原因 | `uni-data-select` | :localdata="[{value:'bluetooth',text:'蓝牙故障'},{value:'phone',text:'手机异常'},{value:'system',text:'系统异常'},{value:'other',text:'其他'}]" | +| 补录说明 | `uni-easyinput` | type="textarea",maxlength="200",:showWordLimit="true" | +| 证明材料上传 | `uni-file-picker` | limit="3",file-mediatype="image" | +| 保洁时间 | `uni-datetime-picker` | type="datetime" | +| 保洁区域 | `uni-data-select` | v-model="areaId",:localdata="areaList" | +| 检查项结果 | `uni-data-checkbox` | 逐项渲染 | +| 提交按钮 | `button` | type="primary",:loading="submitting" | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 补录原因 | 必选 | "请选择补录原因" | +| 补录说明 | 必填,最多200字 | "请填写补录说明" | +| 证明材料 | 最多3张 | "证明材料最多上传3张" | +| 保洁时间 | 必选 | "请选择保洁时间" | +| 保洁区域 | 必选 | "请选择保洁区域" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),表单宽度自适应 +- **横竖屏适配策略**:竖屏垂直布局;横屏表单水平分组 +- **手势交互规范**:选择器≥44px;提交按钮≥44px +- **安全区域**:底部提交按钮适配底部安全区 + --- ## 业务规则 diff --git a/docs/02-功能清单-小程序端/05-考勤相关功能.md b/docs/02-功能清单-小程序端/05-考勤相关功能.md index 548c7cb..4135d08 100644 --- a/docs/02-功能清单-小程序端/05-考勤相关功能.md +++ b/docs/02-功能清单-小程序端/05-考勤相关功能.md @@ -3,6 +3,7 @@ > 模块编码:attendance > 端侧:微信小程序 > 关联文档:01-模块划分.md(v4.0)、02-功能清单-小程序端.md(§5)、03-业务流转逻辑-小程序端.md(§5)、05-接口规范.md(§9)、06-项目技术要求.md +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -51,6 +52,55 @@ |------------|----------|----------|----------|----------| | MP-AT-01 | 上班打卡 | 02-小程序端 §5 | 记录操作日志 | 考勤记录、打卡点管理 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取当前时间→查询蓝牙策略→策略=REQUIRED自动启动蓝牙扫描;显示打卡状态(已打卡/未打卡) +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:点击「上班打卡」→策略=REQUIRED自动扫描蓝牙→检测到打卡点Beacon→打卡成功(check_method=BLUETOOTH);策略=OPTIONAL选择打卡方式→蓝牙/手动打卡 +4. **弹窗/弹层交互流程**:打卡成功弹出成功提示(显示打卡时间);蓝牙未检测到弹出提示"未检测到打卡点蓝牙" +5. **行内操作流程**:点击打卡按钮→执行打卡流程→显示结果;点击异常申诉→跳转异常申诉页 +6. **异常与错误处理**:蓝牙未开启提示"请开启手机蓝牙";打卡失败显示重试;已打卡提示"今日已打卡";离线时数据暂存本地联网后同步 +7. **联动/级联交互**:蓝牙策略与打卡流程联动;打卡状态实时更新 +8. **权限控制交互表现**:所有登录用户可打卡;蓝牙权限被拒引导开启 + +#### 前端硬性约束 + +- **H1 防重复提交**:打卡按钮点击后立即 `loading=true` + `disabled=true` 防止重复点击;使用 pending 请求去重机制(同一请求未完成前不发送新请求);支持 `wx.requestTask.abort()` 取消前一次未完成的请求 +- **H2 超时控制**:GET 请求超时 15s、POST 请求超时 30s;请求耗时 >3s 时自动调用 `wx.showLoading({ title: '加载中...', mask: true })` 提示用户等待 +- **H3 二次确认**:无危险操作,无需二次确认 +- **H4 脏数据检测**:本页面为操作型页面,无需表单脏数据检测 +- **H7 文件上传**:无上传操作 +- **H8 操作反馈**:打卡成功调用 `wx.showToast({ title: '打卡成功', icon: 'success' })`;网络异常时提示"网络异常,请检查网络后重试",提供重试按钮;离线暂存成功提示"已暂存本地,联网后将自动同步" +- **通用约束**:使用 uni-ui 组件库;所有可点击元素触控区域 ≥44px;下拉刷新操作需防重复触发 + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 当前时间 | `text` | font-size="48px",font-weight="bold",动态更新 | +| 打卡状态 | `uni-tag` | type: success(已打卡)/warning(未打卡) | +| 打卡按钮 | `button` | type="primary",圆形大按钮,size=120px | +| 蓝牙状态指示 | `uni-icons` | type="bluetooth",size="22" | +| 打卡点名称 | `text` | font-size="14px" | +| 异常申诉 | `button` | type="default",size="mini" | +| 打卡结果 | `uni-popup` | type="dialog",显示打卡时间 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 蓝牙状态 | 策略=REQUIRED须开启蓝牙 | "请开启手机蓝牙" | +| Beacon验证 | 须属于本人班组打卡点 | "未检测到有效打卡点" | +| 打卡状态 | 未打卡才可打卡 | "今日已打卡" | +| 打卡时间 | 自动记录精确到秒 | — | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),打卡按钮居中 +- **横竖屏适配策略**:竖屏垂直居中布局;横屏信息与按钮并排 +- **手势交互规范**:打卡按钮≥120px可点击区域;申诉按钮≥44px +- **安全区域**:底部按钮适配底部安全区 + --- ### 页面2:下班打卡 @@ -81,6 +131,54 @@ |------------|----------|----------|----------|----------| | MP-AT-02 | 下班打卡 | 02-小程序端 §5 | 记录操作日志 | 考勤记录 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取当前时间和上班打卡时间→查询蓝牙策略→自动扫描蓝牙(策略=REQUIRED);显示打卡状态 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:点击「下班打卡」→策略=REQUIRED自动扫描蓝牙→打卡成功;策略=OPTIONAL选择打卡方式→打卡 +4. **弹窗/弹层交互流程**:打卡成功弹出成功提示(显示上下班时间);蓝牙未检测到弹出提示 +5. **行内操作流程**:点击打卡按钮→执行打卡→显示结果;点击异常申诉→跳转异常申诉页 +6. **异常与错误处理**:蓝牙未开启提示;下班未打上班卡提示"请先完成上班打卡";离线暂存 +7. **联动/级联交互**:上班打卡时间与下班打卡联动展示 +8. **权限控制交互表现**:所有登录用户可打卡 + +#### 前端硬性约束 + +- **H1 防重复提交**:打卡按钮点击后立即 `loading=true` + `disabled=true` 防止重复点击;使用 pending 请求去重机制(同一请求未完成前不发送新请求);支持 `wx.requestTask.abort()` 取消前一次未完成的请求 +- **H2 超时控制**:GET 请求超时 15s、POST 请求超时 30s;请求耗时 >3s 时自动调用 `wx.showLoading({ title: '加载中...', mask: true })` 提示用户等待 +- **H3 二次确认**:无危险操作,无需二次确认 +- **H4 脏数据检测**:本页面为操作型页面,无需表单脏数据检测 +- **H7 文件上传**:无上传操作 +- **H8 操作反馈**:打卡成功调用 `wx.showToast({ title: '打卡成功', icon: 'success' })`;网络异常时提示"网络异常,请检查网络后重试",提供重试按钮;离线暂存成功提示"已暂存本地,联网后将自动同步" +- **通用约束**:使用 uni-ui 组件库;所有可点击元素触控区域 ≥44px + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 当前时间 | `text` | font-size="48px",font-weight="bold" | +| 上班打卡时间 | `text` | font-size="14px",color="#666" | +| 打卡状态 | `uni-tag` | type: success(已打卡)/warning(未打卡) | +| 打卡按钮 | `button` | type="primary",圆形大按钮,size=120px | +| 蓝牙状态指示 | `uni-icons` | type="bluetooth",size="22" | +| 异常申诉 | `button` | type="default",size="mini" | +| 打卡结果 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 上班打卡 | 须先完成上班打卡 | "请先完成上班打卡" | +| 蓝牙状态 | 策略=REQUIRED须蓝牙连接 | "请开启手机蓝牙" | +| 打卡状态 | 未打卡才可打卡 | "今日已打卡" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),打卡按钮居中 +- **横竖屏适配策略**:竖屏垂直居中布局;横屏信息与按钮并排 +- **手势交互规范**:打卡按钮≥120px可点击区域 +- **安全区域**:底部按钮适配底部安全区 + --- ### 页面3:打卡记录 @@ -119,6 +217,52 @@ |------------|----------|----------|----------|----------| | MP-AT-03 | 打卡记录 | 02-小程序端 §5 | 无 | 考勤日历 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取当月打卡记录和统计→渲染月份选择器和记录列表 +2. **查询/筛选交互流程**:切换月份→重新加载;状态筛选(全部/正常/迟到/早退/缺卡/补录)→过滤记录 +3. **表单填写与提交流程**:无表单提交操作 +4. **弹窗/弹层交互流程**:点击查看详情→弹出打卡详情弹窗(含打卡点、蓝牙信息) +5. **行内操作流程**:点击查看详情→查看详情弹窗;点击异常申诉→跳转异常申诉页 +6. **异常与错误处理**:记录加载失败显示重试;无记录显示空状态 +7. **联动/级联交互**:月份和状态筛选与列表数据联动 +8. **权限控制交互表现**:仅可查看本人打卡记录 + +#### 前端硬性约束 + +- **H1 防重复提交**:月份切换/状态筛选时防重复请求;列表加载期间禁用筛选操作;使用 pending 请求去重 +- **H2 超时控制**:GET 请求超时 15s;列表加载 >3s 时调用 `wx.showLoading({ title: '加载中...', mask: true })` +- **H3 二次确认**:无危险操作,无需二次确认 +- **H4 脏数据检测**:本页面为列表查看页,无需表单脏数据检测 +- **H7 文件上传**:无上传操作 +- **H8 操作反馈**:加载成功无提示(静默刷新);加载失败调用 `wx.showToast({ title: '加载失败', icon: 'none' })` 并显示重试按钮;网络异常时提示"网络异常,请检查网络后重试" +- **通用约束**:使用 uni-ui 组件库;所有可点击元素触控区域 ≥44px;下拉刷新需防重复触发(refreshing 标志位) + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 月份选择器 | `uni-datetime-picker` | type="date",fields="month" | +| 统计卡片 | `uni-card` | mode="center",四列等宽 | +| 状态筛选 | `uni-segmented-control` | :values="['全部','正常','迟到','早退','缺卡','补录']" | +| 记录列表 | `uni-list` + `uni-list-item` | clickable=true | +| 状态标签 | `uni-tag` | type: success(正常)/warning(迟到/早退)/error(缺卡)/primary(补录) | +| 详情弹窗 | `uni-popup` | type="bottom",显示打卡详情 | +| 打卡方式标签 | `uni-tag` | type: primary(蓝牙)/default(手动)/warning(补录),size="mini" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 记录列表 | 加载失败允许重试 | "加载失败,请重试" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),卡片宽度自适应 +- **横竖屏适配策略**:竖屏单列列表;横屏双列卡片 +- **手势交互规范**:记录项可点击区域≥44px +- **安全区域**:底部适配底部安全区 + --- ### 页面4:异常申诉 @@ -148,6 +292,56 @@ |------------|----------|----------|----------|----------| | MP-AT-04 | 异常申诉 | 02-小程序端 §5 | 主管审核 | 考勤审核 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时初始化表单,获取异常类型字典 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:选择异常日期→选择异常类型(上班/下班/全天)→选择异常原因→填写补充说明→上传照片→点击提交申诉→确认弹窗→提交成功等待审核 +4. **弹窗/弹层交互流程**:点击上传弹出选择(拍照/相册);提交前弹出确认弹窗 +5. **行内操作流程**:选择日期→选择类型→选择原因→填写说明→上传照片→提交 +6. **异常与错误处理**:图片上传失败提示重传;提交失败显示重试;离线暂存本地 +7. **联动/级联交互**:异常类型与日期选择联动 +8. **权限控制交互表现**:所有用户可提交异常申诉 + +#### 前端硬性约束 + +- **H1 防重复提交**:提交按钮点击后立即 `loading=true` + `disabled=true` 防止重复提交;使用 pending 请求去重机制(提交请求未返回前禁用按钮);支持 `wx.requestTask.abort()` 取消前一次未完成的提交 +- **H2 超时控制**:GET 请求超时 15s、POST 请求超时 30s、文件上传超时 60s;上传耗时 >3s 时调用 `wx.showLoading({ title: '上传中...', mask: true })` +- **H3 二次确认**:提交申诉前必须调用 `wx.showModal({ title: '确认提交', content: '提交后将等待主管审核,确认提交申诉?', confirmText: '确认', cancelText: '取消' })` 进行二次确认,明确告知操作后果 +- **H4 脏数据检测**:页面进入时对表单数据做 deep clone 快照;用户编辑过程中维护 isDirty 状态;`onUnload` / `onBackPress` 生命周期中检测到 isDirty 时弹出 `wx.showModal` 提示"未保存的修改将丢失,确定离开吗?",用户确认后才允许离开 +- **H7 文件上传**:单张图片 ≤10MB,超出时提示"图片大小不能超过10MB"并阻止上传;使用 `uni-file-picker` 的 `onProgress` 回调显示上传进度条;支持多图队列上传 +- **H8 操作反馈**:提交成功调用 `wx.showToast({ title: '申诉已提交,请等待审核', icon: 'success' })`;网络异常时调用 `wx.showToast({ title: '网络异常,请检查网络后重试', icon: 'none' })` 并提供重试按钮;离线暂存提示"已暂存本地,联网后将自动同步" +- **通用约束**:使用 uni-ui 组件库;所有可点击元素触控区域 ≥44px;选择器/输入框/按钮均 ≥44px + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 异常日期 | `uni-datetime-picker` | type="date" | +| 异常类型 | `uni-data-select` | :localdata="[{value:'clock_in',text:'上班'},{value:'clock_out',text:'下班'},{value:'all_day',text:'全天'}]" | +| 异常原因 | `uni-data-select` | :localdata="[{value:'bluetooth',text:'蓝牙故障'},{value:'phone',text:'手机异常'},{value:'system',text:'系统异常'},{value:'forget',text:'忘记打卡'},{value:'other',text:'其他'}]" | +| 补充说明 | `uni-easyinput` | type="textarea",maxlength="200",:showWordLimit="true" | +| 照片上传 | `uni-file-picker` | limit="9",file-mediatype="image" | +| 提交按钮 | `button` | type="primary",:loading="submitting" | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 异常日期 | 必选 | "请选择异常日期" | +| 异常类型 | 必选 | "请选择异常类型" | +| 异常原因 | 必选 | "请选择异常原因" | +| 补充说明 | 最多200字 | "补充说明不能超过200字" | +| 照片 | 最多9张 | "照片最多上传9张" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),表单宽度自适应 +- **横竖屏适配策略**:竖屏垂直布局;横屏表单水平分组 +- **手势交互规范**:选择器≥44px;提交按钮≥44px +- **安全区域**:底部提交按钮适配底部安全区 + --- ### 页面5:考勤日历 @@ -180,6 +374,51 @@ |------------|----------|----------|----------|----------| | MP-AT-05 | 考勤日历 | 02-小程序端 §5 | 无 | 打卡记录 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取当月考勤日历数据→渲染月历视图和每日状态标记 +2. **查询/筛选交互流程**:左右滑动切换月份→重新加载月历数据 +3. **表单填写与提交流程**:无表单提交操作 +4. **弹窗/弹层交互流程**:点击日期→弹出当日打卡详情弹窗(上班/下班时间、打卡方式、状态) +5. **行内操作流程**:点击日期→查看详情弹窗;左右滑动切换月份 +6. **异常与错误处理**:日历数据加载失败显示重试 +7. **联动/级联交互**:月份切换与日历数据联动 +8. **权限控制交互表现**:仅可查看本人考勤日历 + +#### 前端硬性约束 + +- **H1 防重复提交**:月份切换(左右滑动)时防重复请求;滑动切换期间锁定请求,避免连续滑动触发多次请求 +- **H2 超时控制**:GET 请求超时 15s;日历数据加载 >3s 时调用 `wx.showLoading({ title: '加载中...', mask: true })` +- **H3 二次确认**:无危险操作,无需二次确认 +- **H4 脏数据检测**:本页面为只读详情页,无需表单脏数据检测 +- **H7 文件上传**:无上传操作 +- **H8 操作反馈**:加载成功无提示(静默刷新);加载失败调用 `wx.showToast({ title: '日历加载失败', icon: 'none' })` 并显示重试按钮;网络异常时提示"网络异常,请检查网络后重试" +- **通用约束**:使用 uni-ui 组件库;日期格子可点击区域 ≥44px + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 月历视图 | 自定义日历组件 | 7列网格,日期格子44px | +| 状态标记点 | `view` | 圆形8px,颜色动态(绿/黄/红/蓝/灰) | +| 月份切换 | `uni-icons` | type="left"/"right",size="20",@click切换 | +| 详情弹窗 | `uni-popup` | type="bottom",显示当日打卡详情 | +| 图例说明 | `view` + `text` | 水平排列,颜色点+文字 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 日历数据 | 加载失败允许重试 | "日历加载失败,请重试" | +| 日期选择 | 只能查看,不可选择未来日期 | — | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),日历网格宽度自适应 +- **横竖屏适配策略**:竖屏7列日历网格;横屏日历宽度适当增大,详情弹窗更宽 +- **手势交互规范**:日期格子≥44px;左右滑动切换月份;点击日期≥44px +- **安全区域**:底部图例适配底部安全区 + --- ### 页面6:考勤审核 @@ -233,6 +472,56 @@ |------------|----------|----------|----------|----------| | MP-AT-06 | 考勤审核 | 02-小程序端 §5 | 补录打卡记录 | 操作日志、考勤管理(Web端) | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取待审核申诉列表→显示骨架屏→渲染筛选条件和申诉列表 +2. **查询/筛选交互流程**:状态/日期/人员筛选→切换筛选重新加载列表;下拉刷新 +3. **表单填写与提交流程**:查看申诉详情→点击通过→系统自动补录打卡记录;点击驳回→填写驳回原因→确认驳回→通知员工 +4. **弹窗/弹层交互流程**:点击通过弹出确认弹窗"确认通过申诉?";点击驳回弹出驳回原因输入弹窗 +5. **行内操作流程**:点击通过→确认→补录生效;点击驳回→填写原因→确认→通知员工 +6. **异常与错误处理**:审核操作失败显示重试;申诉已处理提示"该申诉已处理" +7. **联动/级联交互**:审核通过后自动补录打卡记录,标记is_supplement=true +8. **权限控制交互表现**:仅主管可操作;无attendance:appeal:approve权限时按钮置灰 + +#### 前端硬性约束 + +- **H1 防重复提交**:通过/驳回按钮点击后立即 `loading=true` + `disabled=true` 防止重复操作;使用 pending 请求去重机制(审核请求未返回前禁用按钮);筛选切换时防重复请求 +- **H2 超时控制**:GET 请求超时 15s、POST 请求超时 30s;列表加载/审核操作 >3s 时调用 `wx.showLoading({ title: '处理中...', mask: true })` +- **H3 二次确认**:审核通过前必须调用 `wx.showModal({ title: '确认通过', content: '确认通过该申诉?通过后系统将自动补录打卡记录。', confirmText: '通过', cancelText: '取消' })`;驳回前必须调用 `wx.showModal({ title: '确认驳回', content: '确认驳回该申诉?驳回后将通知员工重新处理。', confirmText: '驳回', cancelText: '取消' })` +- **H4 脏数据检测**:驳回原因输入框维护 isDirty 状态;用户输入驳回原因但未提交就离开时,在 `onUnload` / `onBackPress` 中弹出提示 +- **H7 文件上传**:无上传操作 +- **H8 操作反馈**:审核通过成功调用 `wx.showToast({ title: '已通过,已自动补录打卡记录', icon: 'success' })`;审核驳回成功调用 `wx.showToast({ title: '已驳回', icon: 'success' })`;网络异常时调用 `wx.showToast({ title: '网络异常,请检查网络后重试', icon: 'none' })` 并提供重试按钮 +- **通用约束**:使用 uni-ui 组件库;所有可点击元素触控区域 ≥44px;通过/驳回按钮 ≥44px;下拉刷新需防重复触发(refreshing 标志位) + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 筛选条件 | `uni-data-select` | 状态/日期/人员三个选择器 | +| 申诉列表 | `uni-list` + `uni-list-item` | clickable=true | +| 审核状态标签 | `uni-tag` | type: warning(待审核)/success(已通过)/error(已驳回) | +| 通过按钮 | `button` | type="primary",size="mini",@click="approve" | +| 驳回按钮 | `button` | type="warn",size="mini",@click="reject" | +| 驳回原因弹窗 | `uni-popup` | type="bottom",含文本输入和确认按钮 | +| 驳回原因输入 | `uni-easyinput` | type="textarea",maxlength="200" | +| 确认弹窗 | `uni-popup` | type="dialog" | +| 下拉刷新 | `uni-refresher` | @onRefresh回调 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 驳回原因 | 驳回时必填 | "请填写驳回原因" | +| 驳回原因 | 最多200字 | "驳回原因不能超过200字" | +| 审核状态 | 须为待审核状态 | "该申诉已处理" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),列表宽度自适应 +- **横竖屏适配策略**:竖屏单列卡片;横屏双列卡片+底部弹窗更宽 +- **手势交互规范**:申诉项可点击区域≥44px;通过/驳回按钮≥44px +- **安全区域**:底部操作区域适配底部安全区 + --- ## 业务规则 diff --git a/docs/02-功能清单-小程序端/06-组织架构相关功能.md b/docs/02-功能清单-小程序端/06-组织架构相关功能.md index 283a06c..fb5a88f 100644 --- a/docs/02-功能清单-小程序端/06-组织架构相关功能.md +++ b/docs/02-功能清单-小程序端/06-组织架构相关功能.md @@ -3,6 +3,7 @@ > 模块编码:org > 端侧:微信小程序 > 关联文档:01-模块划分.md(v4.0)、02-功能清单-小程序端.md(§6)、03-业务流转逻辑-小程序端.md(§6)、05-接口规范.md(§9)、06-项目技术要求.md +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -49,6 +50,52 @@ |------------|----------|----------|----------|----------| | MP-OR-01 | 我的班组 | 02-小程序端 §6 | 无 | 组织架构(Web端)、通讯录 | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取本人所在班组信息→渲染班组卡片和成员列表 +2. **查询/筛选交互流程**:无筛选操作,自动加载本人班组 +3. **表单填写与提交流程**:无表单提交操作 +4. **弹窗/弹层交互流程**:点击查看详情→弹出成员详情弹窗(完整信息展示) +5. **行内操作流程**:点击拨打电话→调用wx.makePhoneCall;点击查看详情→弹出详情 +6. **异常与错误处理**:班组信息加载失败显示重试;拨号失败提示"拨号失败" +7. **联动/级联交互**:无强联动 +8. **权限控制交互表现**:所有用户可查看本人班组信息;数据为只读 + +#### 前端硬性约束 + +- **H1 防重复提交**:页面加载时防重复请求;使用 pending 请求去重机制避免重复加载班组数据 +- **H2 超时控制**:GET 请求超时 15s;数据加载 >3s 时调用 `wx.showLoading({ title: '加载中...', mask: true })` +- **H3 二次确认**:无危险操作,无需二次确认 +- **H4 脏数据检测**:本页面为只读详情页,无需表单脏数据检测 +- **H7 文件上传**:无上传操作 +- **H8 操作反馈**:加载成功无提示(静默刷新);加载失败调用 `wx.showToast({ title: '班组信息加载失败', icon: 'none' })` 并显示重试按钮;网络异常时提示"网络异常,请检查网络后重试" +- **通用约束**:使用 uni-ui 组件库;成员项/拨号图标可点击区域 ≥44px + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 班组信息卡片 | `uni-card` | mode="basic",展示班组名、部门、班组长 | +| 成员列表 | `uni-list` + `uni-list-item` | clickable=true,showArrow=true | +| 头像 | `image` | mode="aspectFill",40px圆形 | +| 技能标签 | `uni-tag` | type="primary",size="mini",v-for渲染 | +| 拨打电话 | `uni-icons` | type="phone",size="22",color="#007AFF" | +| 成员详情弹窗 | `uni-popup` | type="bottom",展示完整信息 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 班组信息 | 必须返回有效数据 | "班组信息加载失败,请重试" | +| 电话号码 | 脱敏显示,拨号时显示完整号码 | — | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),列表宽度自适应 +- **横竖屏适配策略**:竖屏垂直列表;横屏双列展示成员 +- **手势交互规范**:成员项可点击区域≥44px;拨号图标≥44px +- **安全区域**:底部适配底部安全区 + --- ### 页面2:我的排班 @@ -86,6 +133,50 @@ |------------|----------|----------|----------|----------| | MP-OR-02 | 我的排班 | 02-小程序端 §6 | 无 | 考勤打卡、组织架构排班(Web端) | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取当月排班数据→渲染月历视图和排班标记 +2. **查询/筛选交互流程**:左右滑动切换月份→重新加载排班数据 +3. **表单填写与提交流程**:无表单提交操作(只读查看) +4. **弹窗/弹层交互流程**:点击日期→弹出当日排班详情弹窗(排班类型、上下班时间、打卡点) +5. **行内操作流程**:点击日期→查看排班详情;左右滑动切换月份 +6. **异常与错误处理**:排班数据加载失败显示重试;无排班数据显示"暂无排班信息" +7. **联动/级联交互**:月份切换与排班数据联动 +8. **权限控制交互表现**:所有用户可查看本人排班;数据只读 + +#### 前端硬性约束 + +- **H1 防重复提交**:月份切换(左右滑动)时防重复请求;滑动切换期间锁定请求,避免连续滑动触发多次加载 +- **H2 超时控制**:GET 请求超时 15s;排班数据加载 >3s 时调用 `wx.showLoading({ title: '加载中...', mask: true })` +- **H3 二次确认**:无危险操作,无需二次确认 +- **H4 脏数据检测**:本页面为只读详情页,无需表单脏数据检测 +- **H7 文件上传**:无上传操作 +- **H8 操作反馈**:加载成功无提示(静默刷新);加载失败调用 `wx.showToast({ title: '排班数据加载失败', icon: 'none' })` 并显示重试按钮;网络异常时提示"网络异常,请检查网络后重试" +- **通用约束**:使用 uni-ui 组件库;日期格子可点击区域 ≥44px + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 月历视图 | 自定义日历组件 | 7列网格,日期格子44px | +| 排班标记条 | `view` | 颜色条3px,颜色动态(蓝/绿/橙/灰) | +| 月份切换 | `uni-icons` | type="left"/"right",size="20" | +| 排班详情弹窗 | `uni-popup` | type="bottom",展示排班类型、时间、打卡点 | +| 图例说明 | `view` + `text` | 水平排列,颜色条+文字 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 排班数据 | 加载失败允许重试 | "排班数据加载失败,请重试" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),日历网格宽度自适应 +- **横竖屏适配策略**:竖屏7列日历网格;横屏日历宽度适当增大 +- **手势交互规范**:日期格子≥44px;左右滑动切换月份 +- **安全区域**:底部图例适配底部安全区 + --- ## 业务规则 diff --git a/docs/02-功能清单-小程序端/07-服务评价相关功能.md b/docs/02-功能清单-小程序端/07-服务评价相关功能.md index 705c856..6d1a779 100644 --- a/docs/02-功能清单-小程序端/07-服务评价相关功能.md +++ b/docs/02-功能清单-小程序端/07-服务评价相关功能.md @@ -3,6 +3,7 @@ > 模块编码:evaluation > 端侧:微信小程序 > 关联文档:01-模块划分.md(v4.0)、02-功能清单-小程序端.md(§7)、03-业务流转逻辑-小程序端.md(§7)、05-接口规范.md(§9)、06-项目技术要求.md +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -53,6 +54,51 @@ |------------|----------|----------|----------|----------| | MP-EV-01 | 待评价列表 | 02-小程序端 §7 | 评分留言 | 在线报修(验收完成后触发) | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取待评价工单列表→显示骨架屏→渲染待评价数量和工单列表 +2. **查询/筛选交互流程**:无手动筛选,自动过滤待评价工单 +3. **表单填写与提交流程**:无表单提交操作 +4. **弹窗/弹层交互流程**:无弹窗 +5. **行内操作流程**:点击「去评价」→跳转评分留言页 +6. **异常与错误处理**:列表加载失败显示重试;无待评价工单显示空状态 +7. **联动/级联交互**:评价完成后列表自动移除已评价项 +8. **权限控制交互表现**:仅报修人可访问;待评价工单自动过滤展示 + +#### 前端硬性约束 + +- **H1 防重复提交**:列表加载时防重复请求;使用 pending 请求去重机制;点击「去评价」跳转时防重复跳转 +- **H2 超时控制**:GET 请求超时 15s;列表加载 >3s 时调用 `wx.showLoading({ title: '加载中...', mask: true })` +- **H3 二次确认**:无危险操作,无需二次确认 +- **H4 脏数据检测**:本页面为列表查看页,无需表单脏数据检测 +- **H7 文件上传**:无上传操作 +- **H8 操作反馈**:加载成功无提示(静默刷新);加载失败调用 `wx.showToast({ title: '加载失败', icon: 'none' })` 并显示重试按钮;网络异常时提示"网络异常,请检查网络后重试" +- **通用约束**:使用 uni-ui 组件库;所有可点击元素触控区域 ≥44px;去评价按钮 ≥44px + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 待评价数量 | `uni-badge` | :content="count",type="warning" | +| 工单列表 | `uni-list` + `uni-list-item` | clickable=true,showArrow=true | +| 评价状态标签 | `uni-tag` | type: warning(待评价)/success(已评价) | +| 去评价按钮 | `button` | type="primary",size="mini" | +| 空状态 | `uni-section` | 插槽自定义空状态插图与文案 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 工单列表 | 加载失败允许重试 | "加载失败,请重试" | +| 评价时效 | 验收后7天内可评价 | "评价已超期" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),卡片宽度自适应 +- **横竖屏适配策略**:竖屏单列卡片;横屏双列卡片展示 +- **手势交互规范**:工单项可点击区域≥44px;去评价按钮≥44px +- **安全区域**:底部适配底部安全区 + --- ### 页面2:评分留言 @@ -90,6 +136,56 @@ |------------|----------|----------|----------|----------| | MP-EV-02 | 评分留言 | 02-小程序端 §7 | 物业公司查看回复 | 服务评价(Web端) | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取工单信息→渲染工单摘要卡片 +2. **查询/筛选交互流程**:无筛选操作 +3. **表单填写与提交流程**:点击星星选择评分→可选点击评分维度标签→输入留言→可选上传图片→开关匿名评价→点击提交评价→确认弹窗→提交成功 +4. **弹窗/弹层交互流程**:点击拍照弹出选择(拍照/相册);提交前弹出确认弹窗"确认提交评价?提交后不可修改" +5. **行内操作流程**:选择评分→输入留言→上传图片→设置匿名→提交 +6. **异常与错误处理**:图片上传失败提示重传;提交失败显示重试;评分≤3分触发低分预警事件 +7. **联动/级联交互**:评分与提交按钮联动(必须选择评分);匿名开关与提交数据联动 +8. **权限控制交互表现**:仅报修人可评价;评价不可修改 + +#### 前端硬性约束 + +- **H1 防重复提交**:提交按钮点击后立即 `loading=true` + `disabled=true` 防止重复提交;使用 pending 请求去重机制(提交请求未返回前禁用按钮);支持 `wx.requestTask.abort()` 取消前一次未完成的提交 +- **H2 超时控制**:GET 请求超时 15s、POST 请求超时 30s、文件上传超时 60s;上传耗时 >3s 时调用 `wx.showLoading({ title: '上传中...', mask: true })` +- **H3 二次确认**:提交评价前必须调用 `wx.showModal({ title: '确认提交评价', content: '确认提交评价?提交后不可修改。', confirmText: '确认', cancelText: '取消' })` 进行二次确认,明确告知操作后果(不可修改) +- **H4 脏数据检测**:页面进入时对表单数据做 deep clone 快照(评分、留言、图片等);用户编辑过程中维护 isDirty 状态;`onUnload` / `onBackPress` 生命周期中检测到 isDirty 时弹出 `wx.showModal` 提示"未保存的评价内容将丢失,确定离开吗?",用户确认后才允许离开 +- **H7 文件上传**:单张图片 ≤10MB,超出时提示"图片大小不能超过10MB"并阻止上传;使用 `uni-file-picker` 的 `onProgress` 回调显示上传进度条;支持多图队列上传 +- **H8 操作反馈**:提交成功调用 `wx.showToast({ title: '评价提交成功', icon: 'success' })`;网络异常时调用 `wx.showToast({ title: '网络异常,请检查网络后重试', icon: 'none' })` 并提供重试按钮;低分预警(≤3分)提示"已通知主管关注" +- **通用约束**:使用 uni-ui 组件库;星级评分每颗星 ≥44px;提交按钮 ≥44px + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 工单信息摘要 | `uni-card` | mode="basic",只读展示 | +| 星级评分 | `uni-rate` | :max="5",:value="score",:size="28" | +| 评分维度标签 | `uni-data-checkbox` | :localdata="dimensionList",multiple=true | +| 留言输入 | `uni-easyinput` | type="textarea",maxlength="200",:showWordLimit="true" | +| 图片上传 | `uni-file-picker` | limit="9",file-mediatype="image" | +| 匿名开关 | `switch` | @change="onAnonymousChange" | +| 提交按钮 | `button` | type="primary",:loading="submitting" | +| 确认弹窗 | `uni-popup` | type="dialog" | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 评分 | 必选,1~5分 | "请选择评分" | +| 留言 | 最多200字 | "留言不能超过200字" | +| 图片 | 最多9张 | "评价图片最多上传9张" | +| 评价时效 | 验收后7天内 | "评价已超期" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),表单宽度自适应 +- **横竖屏适配策略**:竖屏垂直布局;横屏评分与输入并排 +- **手势交互规范**:星级评分每颗星≥44px;提交按钮≥44px +- **安全区域**:底部提交按钮适配底部安全区 + --- ### 页面3:历史评价 @@ -126,6 +222,52 @@ |------------|----------|----------|----------|----------| | MP-EV-03 | 历史评价 | 02-小程序端 §7 | 无 | 服务评价(Web端) | +#### 交互流程要求 + +1. **页面加载流程**:页面加载时获取历史评价列表→渲染筛选条件和评价卡片 +2. **查询/筛选交互流程**:评分筛选(全部/好评/中评/差评)→过滤列表;时间筛选→过滤列表;下拉刷新 +3. **表单填写与提交流程**:无表单提交操作 +4. **弹窗/弹层交互流程**:点击查看详情→弹出评价详情弹窗(含物业回复内容) +5. **行内操作流程**:点击查看详情→查看评价详情及物业回复 +6. **异常与错误处理**:列表加载失败显示重试;无评价显示空状态 +7. **联动/级联交互**:筛选条件与列表数据联动 +8. **权限控制交互表现**:仅报修人可查看自己的历史评价 + +#### 前端硬性约束 + +- **H1 防重复提交**:评分筛选/时间筛选切换时防重复请求;列表加载期间禁用筛选操作;使用 pending 请求去重;下拉刷新需防重复触发(refreshing 标志位) +- **H2 超时控制**:GET 请求超时 15s;列表加载 >3s 时调用 `wx.showLoading({ title: '加载中...', mask: true })` +- **H3 二次确认**:无危险操作,无需二次确认 +- **H4 脏数据检测**:本页面为列表查看页,无需表单脏数据检测 +- **H7 文件上传**:无上传操作 +- **H8 操作反馈**:加载成功无提示(静默刷新);加载失败调用 `wx.showToast({ title: '加载失败', icon: 'none' })` 并显示重试按钮;网络异常时提示"网络异常,请检查网络后重试" +- **通用约束**:使用 uni-ui 组件库;所有可点击元素触控区域 ≥44px;下拉刷新需防重复触发(refreshing 标志位) + +#### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 评分筛选 | `uni-segmented-control` | :values="['全部','好评','中评','差评']" | +| 时间筛选 | `uni-datetime-picker` | type="date",fields="month" | +| 评价列表 | `uni-list` + `uni-list-item` | clickable=true | +| 评分显示 | `uni-rate` | :size="14",:readonly="true" | +| 回复状态标签 | `uni-tag` | type: success(已回复)/warning(未回复) | +| 详情弹窗 | `uni-popup` | type="bottom",展示评价详情+物业回复 | +| 下拉刷新 | `uni-refresher` | @onRefresh回调 | + +#### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 评价列表 | 加载失败允许重试 | "加载失败,请重试" | + +#### 响应式布局 + +- **适配机型**:iPhone SE(375px)~ iPad mini(768px),卡片宽度自适应 +- **横竖屏适配策略**:竖屏单列卡片;横屏双列卡片展示 +- **手势交互规范**:评价项可点击区域≥44px +- **安全区域**:底部适配底部安全区 + --- ## 业务规则 diff --git a/docs/02-功能清单-小程序端/08-统计概览功能.md b/docs/02-功能清单-小程序端/08-统计概览功能.md index 68d5340..b5771fc 100644 --- a/docs/02-功能清单-小程序端/08-统计概览功能.md +++ b/docs/02-功能清单-小程序端/08-统计概览功能.md @@ -3,6 +3,7 @@ > 模块编码:statistics > 端侧:微信小程序 > 关联文档:01-模块划分.md(v4.0)、02-功能清单-小程序端.md(§8)、03-业务流转逻辑-小程序端.md(§8)、05-接口规范.md(§9)、06-项目技术要求.md +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -49,6 +50,49 @@ |------------|----------|----------|----------|----------| | MP-ST-01 | 简版统计概览 | 02-小程序端 §8 | 无 | 统计报表(Web端) | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用API加载今日统计数据 → 渲染指标卡片 + 趋势图 +2. **时间切换**:点击"今日/本周"切换标签 → 重新请求对应时间范围数据 → 刷新卡片和图表 +3. **按角色显示**:管理员看到全部指标卡片;主管仅看到管辖模块指标 +4. **异常处理**:数据加载失败 → 显示"数据加载失败,点击重试";网络离线 → 显示上次缓存数据+离线标识 +5. **权限控制**:无统计权限 → 不显示对应指标卡片 + +6. **[H1]防重复请求** + - 今日/本周时间切换按钮点击后立即 disabled + wx.showLoading({title:'加载中'});API返回后恢复 + - 切换期间再次点击无效(pending标志位去重) + - 页面卸载时 onUnload 中 abort 未完成的请求 + +7. **[H2]超时与加载反馈** + - 统计数据查询 API timeout=30秒(统计报表档位) + - 超过3秒未响应时显示 wx.showLoading 提示 + - 超时后提示"请求超时,请检查网络后重试"+ 提供"重试"按钮 + +8. **[H8]操作结果反馈** + - 数据加载成功:wx.showToast({ title: '刷新成功', icon: 'success', duration: 2000 }) + - 加载失败:wx.showToast({ title: errMsg, icon: 'none', duration: 3000 }) + - 网络离线:显示缓存数据 + uni-icons type="warning" + "当前为离线模式"提示条 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 页面容器 | scroll-view | scroll-y, :refresher-enabled, @refresherrefresh | +| 切换标签 | uni-segmented-control | :current, :values="['今日','本周']" | +| 指标卡片 | view | class="stat-card", v-for | +| 指标数值 | text | class="stat-value", font-size=28px | +| 趋势图 | qiun-data-charts | type="line", :chartData | +| 下拉刷新 | scroll-view | refresher-enabled, @refresherrefresh | + +### 校验规则 + +无表单校验(纯展示页面) + +### 响应式布局 + +- 适配机型:iPhone SE ~ iPad mini +- 指标卡片:竖屏2列,横屏4列 +- 趋势图:全宽自适应 +- 触摸区域:卡片点击区域≥44px + --- ### 页面2:个人绩效统计 @@ -83,6 +127,45 @@ |------------|----------|----------|----------|----------| | MP-ST-02 | 个人绩效统计 | 02-小程序端 §8 | 无 | 统计报表(Web端) | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用API加载个人本周绩效数据 → 渲染指标卡片 + 趋势图 +2. **时间切换**:点击"本周/本月"切换 → 重新请求 → 刷新 +3. **异常处理**:数据加载失败 → 显示重试提示;无数据 → 显示"暂无绩效数据" + +### 前端硬性约束 + +> - **H1 防重复提交**: +> - 时间切换操作在请求期间必须设置 `loading` 状态 + 切换标签 `disabled`,防止重复点击触发多次请求。 +> - 使用 pending 标志位对同一请求进行去重,确保请求完成前不发出相同新请求。 +> - 页面卸载时(`onUnload`)必须 `abort` 未完成的请求,避免无效回调。 +> - **H2 超时处理**: +> - 绩效统计数据接口请求超时时间设置为 **30秒**;超过 3 秒未响应时必须展示 loading 动画提示。 +> - 超时后给出明确错误提示并提供"重试"按钮。 +> - **H8 操作反馈**: +> - 加载成功使用 `uni.showToast({ title: '加载成功', icon: 'success', duration: 2000 })`,2 秒自动消失。 +> - 错误反馈使用 `icon: 'none'`,需手动关闭或短时自动消失。 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 页面容器 | scroll-view | scroll-y, refresher-enabled | +| 时间切换 | uni-segmented-control | :values="['本周','本月']" | +| 绩效卡片 | view | class="stat-card" | +| 评分展示 | uni-rate | :size=20, :readonly=true | +| 趋势图 | qiun-data-charts | type="line" | + +### 校验规则 + +无表单校验(纯展示页面) + +### 响应式布局 + +- 适配机型:iPhone SE ~ iPad mini +- 绩效卡片:竖屏2列,横屏3列 +- 触摸区域:≥44px + --- ## 业务规则 diff --git a/docs/02-功能清单-小程序端/09-管理员小程序功能.md b/docs/02-功能清单-小程序端/09-管理员小程序功能.md index c24efbd..2de0f99 100644 --- a/docs/02-功能清单-小程序端/09-管理员小程序功能.md +++ b/docs/02-功能清单-小程序端/09-管理员小程序功能.md @@ -3,6 +3,7 @@ > 模块编码:property-manager > 端侧:微信小程序 > 关联文档:01-模块划分.md(v4.0)、02-功能清单-小程序端.md(§9)、03-业务流转逻辑-小程序端.md(§9)、05-接口规范.md(§9)、06-项目技术要求.md +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -57,6 +58,62 @@ |------------|----------|----------|----------|----------| | MP-PM-01 | 报修管理 | 02-小程序端 §9 | 通知维修人员 | 在线报修(Web端) | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 并行调用:统计卡片数据 + 工单列表(默认全部状态) +2. **筛选交互**:选择状态/日期/类型 → 触发列表刷新;搜索框输入 → 回车或点击搜索 +3. **分配操作**:点击"分配"→ 弹出人员选择弹窗(按班组筛选)→ 选择人员 → 确认 → 调用API → 成功提示 → 推送通知维修人员 +4. **查看详情**:点击工单卡片 → 跳转工单详情页 +5. **审批延期**:点击"审批延期"→ 弹窗展示延期原因 → 同意/拒绝 → API调用 → 通知申请人 +6. **异常处理**:分配失败提示;网络离线提示 +7. **权限控制**:无分配权限 → "分配"按钮不显示;无审批权限 → "审批延期"不显示 + +8. **[H1]防重复请求** + - 分配操作:点击"分配"后按钮 disabled + loading态,API返回后恢复 + - 审批延期操作:点击后弹窗底部确认按钮 disabled + loading + - 筛选切换:状态/日期切换时 abort 上一次列表请求再发新的 + - 下拉刷新:refreshing 标志位防止重复触发 + +9. **[H2]超时与加载反馈** + - 统计卡片+列表查询 timeout=15秒 + - 分配/审批操作 timeout=30秒 + - 超过3秒 wx.showLoading({ title: '加载中', mask: true }) + - 超时 → wx.showToast({ icon: 'none', title: '请求超时' }) + 重试按钮 + +10. **[H3]操作确认机制** + - 分配确认:选择人员后 uni.showModal({ content: '确定将工单分配给{人员姓名}?', success: res => { if(res.confirm) ... } }) + - 审批延期同意:uni.showModal({ content: '确定同意该延期申请?', ... }) + - 审批延期拒绝:必须填写拒绝原因(弹窗内输入框) + +11. **[H8]操作结果反馈** + - 分配成功:wx.showToast({ icon: 'success', title: '分配成功' }) + 刷新列表 + - 操作失败:wx.showToast({ icon: 'none', title: errMsg }) + - 网络异常:wx.showModal({ title: '提示', content: '网络连接异常', confirmText: '重试' }) + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 页面容器 | scroll-view | scroll-y, refresher-enabled, @scrolltolower | +| 统计卡片 | view | class="stat-card", v-for, horizontal scroll | +| 状态筛选 | uni-segmented-control | :values="['全部','待分配','处理中','待验收','已完成']" | +| 工单列表 | view | v-for, class="order-card" | +| 状态标签 | uni-tag | :type按状态变化 | +| 分配按钮 | button | size="mini", type="primary" | +| 人员选择弹窗 | uni-popup | type="bottom" | +| 人员列表 | radio-group | v-for | + +### 校验规则 + +无表单校验 + +### 响应式布局 + +- 适配机型:iPhone SE ~ iPad mini +- 统计卡片:横向滚动 +- 工单卡片:全宽 +- 触摸区域:按钮≥44px + --- ### 页面2:巡检管理 @@ -96,6 +153,51 @@ |------------|----------|----------|----------|----------| | MP-PM-02 | 巡检管理 | 02-小程序端 §9 | 无 | 巡检管理(Web端) | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 并行加载统计卡片 + 巡检记录列表 +2. **筛选交互**:选择日期/状态/人员 → 触发列表刷新 +3. **查看详情**:点击记录卡片 → 跳转巡检详情页 +4. **审核补录**:点击"审核补录"→ 弹窗展示补录原因+证据 → 同意/拒绝 → API调用 → 通知申请人 +5. **异常处理**:数据加载失败显示重试;网络离线提示 +6. **权限控制**:无补录审核权限 → "审核补录"按钮不显示 +7. **[H1]防重复请求** + - 筛选操作点击后 disabled + loading态,API返回后恢复 + - 审核补录操作点击后 disabled + loading态 + - 使用 pending 标志位去重,页面卸载时 onUnload 必须 abort 未完成请求(wx.requestTask.abort()) +8. **[H2]超时与加载反馈** + - GET列表查询 timeout=15秒;POST/PUT/DELETE写操作 timeout=30秒 + - 超时 → wx.showToast({icon:'none', title:'请求超时,请检查网络后重试'}) + - 加载>2秒显示 wx.showLoading +9. **[H3]操作确认机制**(有不可逆操作时) + - 审核补录: wx.showModal 确认 +10. **[H8]操作结果反馈** + - 成功: wx.showToast({icon:'success', duration:2000}) + - 失败: wx.showToast({icon:'none'}) 需手动关闭 + - 网络/离线: 异常提示+重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 页面容器 | scroll-view | scroll-y, refresher-enabled | +| 统计卡片 | view | class="stat-card", v-for | +| 巡检记录列表 | view | v-for, class="inspection-card" | +| 状态标签 | uni-tag | 待执行:type="info", 进行中:type="primary", 已完成:type="success" | +| 审核补录弹窗 | uni-popup | type="bottom" | +| 同意/拒绝按钮 | button | 同意:type="primary", 拒绝:type="warn" | + +### 校验规则 + +无 + +### 响应式布局 + +- 适配机型:iPhone SE ~ iPad mini +- 统计卡片横向排列 +- 记录卡片全宽 +- 按钮触摸区≥44px + --- ### 页面3:保洁管理 @@ -136,6 +238,47 @@ |------------|----------|----------|----------|----------| | MP-PM-03 | 保洁管理 | 02-小程序端 §9 | 不合格时重新生成任务 | 保洁管理(Web端) | +### 交互流程要求 + +1. **页面加载**:进入页面 → 加载统计卡片 + 保洁任务列表 +2. **抽查操作**:点击"抽查"→ 弹窗选择合格/不合格 → 提交 → 不合格时系统自动重新生成任务 +3. **审核补录**:同巡检补录审核流程 +4. **异常处理**:抽查提交失败提示;不合格重新生成任务失败提示 + +5. **[H1]防重复请求** + - 抽查操作:点击"抽查"后按钮 disabled + 弹窗确认按钮 loading + - 审核补录:同上 + - 筛选切换时 abort 上次请求 + +6. **[H2]超时与加载反馈** + - 列表查询 timeout=15秒;抽查/审核操作 timeout=30秒 + - >3秒 wx.showLoading;超时提示+重试 + +7. **[H3]操作确认机制** + - 抽查不合格:uni.showModal({ content: '确定标记为不合格?系统将自动重新生成任务', ... }) + - 抽查合格:uni.showModal({ content: '确定标记为合格?', ... }) + +8. **[H8]操作结果反馈** + - 成功:wx.showToast({ icon: 'success' }) + 刷新 + - 失败:wx.showToast({ icon: 'none', title: errMsg }) + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 抽查弹窗 | uni-popup | type="bottom" | +| 合格/不合格 | radio-group | 合格/不合格 | +| 抽查结果标签 | uni-tag | 合格:type="success", 不合格:type="error", 待抽查:type="info" | + +### 校验规则 + +无 + +### 响应式布局 + +- 适配机型:iPhone SE ~ iPad mini +- 触摸区域≥44px + --- ### 页面4:考勤管理 @@ -175,6 +318,47 @@ |------------|----------|----------|----------|----------| | MP-PM-04 | 考勤管理 | 02-小程序端 §9 | 补录打卡记录 | 考勤打卡(Web端) | +### 交互流程要求 + +1. **页面加载**:进入页面 → 加载今日出勤统计 + 考勤记录列表 +2. **审核申诉**:点击"审核申诉"→ 弹窗展示申诉原因+证据 → 同意/拒绝 → API调用 +3. **审核补录**:同上补录审核流程 +4. **异常处理**:审核失败提示 + +5. **[H1]防重复请求** + - 审核申诉/补录:操作按钮点击后 disabled + loading + - 筛选切换 abort 上次请求 + +6. **[H2]超时与加载反馈** + - 列表查询 timeout=15秒;审核操作 timeout=30秒 + - >3秒 wx.showLoading;超时提示+重试 + +7. **[H3]操作确认机制** + - 审核通过:uni.showModal({ content: '确定同意该申诉/补录?', ... }) + - 审核拒绝:uni.showModal + 必填拒绝原因 + +8. **[H8]操作结果反馈** + - 成功:wx.showToast({ icon: 'success' }) + 刷新 + - 失败:wx.showToast({ icon: 'none', title: errMsg }) + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 统计卡片 | view | class="stat-card" | +| 状态标签 | uni-tag | 正常:type="success", 迟到:type="warning", 早退:type="warning", 缺卡:type="error" | +| 打卡方式标签 | uni-tag | 蓝牙:type="primary", 手动:type="info", 补录:type="warning" | +| 审核弹窗 | uni-popup | type="bottom" | + +### 校验规则 + +无 + +### 响应式布局 + +- 适配机型:iPhone SE ~ iPad mini +- 触摸区域≥44px + --- ### 页面5:评价管理 @@ -213,6 +397,45 @@ |------------|----------|----------|----------|----------| | MP-PM-05 | 评价管理 | 02-小程序端 §9 | 通知报修人 | 服务评价(Web端) | +### 交互流程要求 + +1. **页面加载**:进入页面 → 加载评价统计 + 评价列表 +2. **回复操作**:点击"回复"→ 弹窗输入回复内容 → 提交 → API调用 → 成功后通知报修人 +3. **查看详情**:点击评价卡片 → 跳转评价详情页 +4. **异常处理**:回复失败提示 + +5. **[H1]防重复请求** + - 回复操作:点击"回复"后按钮 disabled + 弹窗提交按钮 loading + - 筛选切换 abort 上次请求 + +6. **[H2]超时与加载反馈** + - 列表查询 timeout=15秒;回复提交 timeout=30秒 + - >3秒 wx.showLoading;超时提示+重试 + +7. **[H8]操作结果反馈** + - 回复成功:wx.showToast({ icon: 'success', title: '回复成功' }) + 刷新 + - 回复失败:wx.showToast({ icon: 'none', title: errMsg }) + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 评分展示 | uni-rate | :size=16, :readonly=true | +| 回复状态标签 | uni-tag | 已回复:type="success", 未回复:type="warning" | +| 回复弹窗 | uni-popup | type="bottom" | +| 回复输入 | uni-easyinput | type="textarea", maxlength=200 | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 回复内容 | 必填, 最大200字符 | "请输入回复内容" | + +### 响应式布局 + +- 适配机型:iPhone SE ~ iPad mini +- 触摸区域≥44px + --- ### 页面6:组织架构查看 @@ -252,6 +475,48 @@ |------------|----------|----------|----------|----------| | MP-PM-06 | 组织架构查看 | 02-小程序端 §9 | 无 | 组织架构(Web端) | +### 交互流程要求 + +1. **页面加载**:进入页面 → 调用API加载班组列表 +2. **查看班组**:点击班组卡片 → 展开班组成员列表(手风琴模式,同时只展开一个) +3. **查看人员**:点击人员 → 跳转人员详情页(姓名、职位、电话、技能标签) +4. **拨打电话**:点击"拨打电话"→ 调用 `uni.makePhoneCall` → 拨号 +5. **搜索**:输入关键词 → 实时筛选班组/人员 +6. **异常处理**:拨号失败(无电话号码)→ 提示"该人员未设置联系电话" + +### 前端硬性约束 + +> - **H1 防重复提交**: +> - 搜索/筛选请求使用 pending 标志位去重,避免频繁触发 API 调用。 +> - 页面卸载时(`onUnload`)必须 `abort` 未完成的请求。 +> - **H2 超时处理**: +> - 班组列表(GET)接口超时 **15秒**;超过 3 秒未响应展示 loading 动画。 +> - 搜索防抖处理(300ms),避免高频输入触发过多请求。 +> - **H8 操作反馈**: +> - 数据加载成功 `uni.showToast({ icon: 'success', duration: 2000 })`,拨号失败 `icon: 'none'` 提示具体原因。 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 搜索框 | uni-easyinput | placeholder="搜索姓名/职位", clearable | +| 班组卡片 | uni-card | :isFull=true, @click="toggleExpand" | +| 展开收起 | uni-icons | type="bottom"/"top" | +| 成员列表 | view | v-for, v-show="expanded" | +| 技能标签 | uni-tag | v-for, type="primary", size="mini" | +| 拨打电话按钮 | button | size="mini", type="primary", @click="makePhoneCall" | + +### 校验规则 + +无 + +### 响应式布局 + +- 适配机型:iPhone SE ~ iPad mini +- 班组卡片全宽 +- 成员列表全宽 +- 触摸区域≥44px + --- ## 业务规则 diff --git a/docs/02-功能清单-物业公司/01-在线报修.md b/docs/02-功能清单-物业公司/01-在线报修.md index 046d1a2..ed9eef8 100644 --- a/docs/02-功能清单-物业公司/01-在线报修.md +++ b/docs/02-功能清单-物业公司/01-在线报修.md @@ -3,6 +3,7 @@ > 模块编码:repair > 端侧:Web + 小程序(双端) > 关联文档:01-模块划分 §3.1 / 02-功能清单-物业公司 §1 / 03-业务流转逻辑-物业公司 §1 / 05-接口规范 §9.2 / 06-项目技术要求 §4.4 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -108,6 +109,67 @@ | 批量关闭 | /api/v1/repair-orders/batch-close | POST | 需填关闭原因 | | 导出 | /api/v1/repair-orders/export | GET | 导出Excel | +### 交互流程要求 + +1. **页面加载流程**:进入页面后调用列表查询API,默认按提交时间倒序排列,加载第一页数据(每页20条);同时并行加载状态下拉选项、报修类型字典、班组列表、区域级联数据;页面顶部面包屑显示"在线报修 > 工单列表"。 +2. **查询/筛选交互流程**:用户填写筛选条件后点击"查询"按钮,携带筛选参数重新请求第1页数据;点击"重置"清空所有筛选条件并重新加载默认列表;输入工单号支持回车触发查询;日期范围选择后自动关闭弹窗。 +3. **表单填写与提交流程**:点击"新增工单"打开弹窗,必填项标红星;报修类型从字典加载,紧急程度默认"普通",报修区域级联选择至少选到楼层;照片上传支持多选,单张≤20MB,最多9张;提交前校验必填项,校验通过后调用新增API,成功则关闭弹窗并刷新列表。 +4. **弹窗/抽屉交互流程**:批量分配弹窗——勾选工单后点击"批量分配",弹窗中选择目标班组和维修人员,确认后批量提交;批量关闭弹窗——勾选后点击"批量关闭",二次确认弹窗中填写关闭原因,确认后批量提交;新增工单弹窗宽度600px,点击遮罩层不关闭。 +5. **行内操作流程**:点击工单号跳转详情页(新标签页打开);状态=待分配时显示"分配"和"关闭"操作按钮;点击"分配"弹出分配弹窗选择班组和人员;点击"关闭"弹出二次确认输入关闭原因;点击"查看"跳转详情页。 +6. **异常与错误处理**:API请求失败时顶部显示el-notification错误提示,3秒后自动关闭;网络超时提示"请求超时,请稍后重试";批量操作部分失败时显示部分成功提示并列出失败工单号;导出超时提示"数据量较大,导出中请勿关闭页面"。 +7. **联动/级联交互**:所属区域级联选择——选择项目后加载区域列表,选择区域后加载楼栋,选择楼栋后加载楼层;报修类型筛选与新增弹窗共用字典数据源;状态多选筛选支持全选/反选。 +8. **权限控制交互表现**:无权限按钮不渲染(v-if控制,非disabled);维修人员角色操作列仅显示"查看"按钮;主管角色不显示"新增工单"按钮;数据范围由后端控制,前端仅展示返回数据。 + +9. **【H1 防重复请求】**:查询按钮点击后立即 disabled 并显示 loading 态,API返回后恢复;行内"分配"/"关闭"操作按钮点击后该行禁用 + loading态,避免重复提交;分页切换时必须 abort 上一次未完成的列表请求再发新请求;批量分配/批量关闭/新增工单的提交按钮绑定 `:loading` 属性同时 `disabled`;页面加载时并行请求列表、字典、班组、区域等独立数据,互不阻塞。 +10. **【H2 统一超时配置】**:GET 列表查询超时15秒;POST 批量分配/批量关闭/新增工单超时30秒;GET 导出Excel超时60秒;所有请求超时自动中断并提示"请求超时,请稍后重试",同时自动恢复按钮状态;请求耗时超过3秒时显示全局 loading(ElLoading.service)。 +11. **【H3 操作确认机制】**:"关闭工单"操作前弹出 `ElMessageBox.confirm("确定要关闭「{工单号}」吗?关闭后将无法恢复处理", {type: 'warning'})`;"批量关闭"操作前弹出 `ElMessageBox.confirm("确定对 {N} 条数据进行批量关闭吗?", {type: 'warning'})` 需填写关闭原因;删除类操作统一使用 type='error' 级别确认。 +12. **【H5 数据权限隔离】**:区分"暂无数据"(ElEmpty)与"无权限"场景;当接口返回403状态码时,前端拦截器统一展示"您没有权限访问此数据",不展示具体数据内容。 +13. **【H6 批量操作限制】**:批量分配/批量关闭单次最多选择50条记录,超过限制时提示"批量操作最多支持50条,当前已选{N}条"并在操作栏角色标处显示警告色角标;导出Excel单次最多导出500条记录,超出提示"数据量过大,请缩小筛选范围后重试"。 +14. **【H8 操作结果反馈】**:操作成功使用 `ElMessage.success` 提示(duration=2000ms),成功后 silent 刷新列表(无loading动画);操作失败使用 `ElMessage.error` 提示(duration=0需手动关闭)并展示服务端返回的具体错误信息;网络异常/断网时提示中包含"重试"按钮可重新触发上次操作。 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 工单号输入 | el-input | placeholder="请输入工单号", clearable, maxlength=20 | +| 状态筛选 | el-select | multiple, collapse-tags, collapse-tags-tooltip, placeholder="请选择状态" | +| 报修类型筛选 | el-select | placeholder="请选择报修类型", clearable, filterable | +| 紧急程度筛选 | el-select | placeholder="请选择紧急程度", clearable | +| 日期范围 | el-date-picker | type="daterange", range-separator="至", start-placeholder="开始日期", end-placeholder="结束日期", value-format="YYYY-MM-DD" | +| 所属班组 | el-select | placeholder="请选择班组", clearable, filterable | +| 所属区域 | el-cascader | :props="{checkStrictly: true, emitPath: false}", placeholder="请选择区域", clearable, filterable | +| 列表 | el-table | stripe, border, :max-height="calc(100vh - 320px)", @selection-change | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next, jumper", :page-sizes="[10,20,50,100]", :page-size="20" | +| 紧急程度标签 | el-tag | :type="danger/warning/info" 紧急=danger/普通=warning/低优先=info | +| 状态标签 | el-tag | :type根据状态映射 启用色系区分 | +| 批量分配弹窗 | el-dialog | title="批量分配", width="500px", :close-on-click-modal="false" | +| 新增工单弹窗 | el-dialog | title="新增工单", width="600px", :close-on-click-modal="false" | +| 操作按钮组 | el-button | type="primary/danger/text", size="default/small" | +| 二次确认 | el-message-box | type="warning", confirmButtonText="确定", cancelButtonText="取消" | +| 勾选列 | el-table-column | type="selection", width="40" | +| 补录标记 | el-tag | type="warning", size="small" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 工单号(筛选) | maxlength=20,允许字母数字 | — | +| 状态(筛选) | 数组,可为空 | — | +| 提交日期范围 | 结束日期≥开始日期 | "结束日期不能早于开始日期" | +| 勾选工单(批量分配) | 至少勾选1条,且所有勾选状态=待分配 | "请选择待分配状态的工单" | +| 勾选工单(批量关闭) | 至少勾选1条,且所有勾选状态=待分配 | "请选择待分配状态的工单" | +| 关闭原因 | 必填,maxlength=200 | "请填写关闭原因" | +| 分配班组 | 必选 | "请选择负责班组" | +| 分配人员 | 必选 | "请选择维修人员" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区单行展示全部筛选项;操作栏与表格等宽;列表展示全部12列;分页完整显示 | +| 1024-1279px(Pad横屏) | 查询条件区折行为两行,日期范围缩窄;列表隐藏"补录标记""预约时间"列,其余列宽自适应;操作按钮缩小为small尺寸 | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,每行一个筛选项;操作栏仅保留"新增工单"和"导出"按钮,批量操作收入"更多"下拉菜单(el-dropdown);列表隐藏"补录标记""预约时间""维修人员""负责班组"列,紧急程度改为图标显示;分页简化为prev/next | + --- ## 页面2:工单详情页 @@ -186,6 +248,63 @@ | 延期审批 | /api/v1/repair-orders/{id}/delay-approve | POST | 通过/驳回 | | 验收 | /api/v1/repair-orders/{id}/accept | POST | 通过/不通过 | +### 交互流程要求 + +1. **页面加载流程**:根据路由参数id调用详情查询API,加载工单基本信息、流转记录、照片附件、评价信息;页面顶部状态栏显示工单号和当前状态;默认展示"基本信息"标签页;根据当前状态和用户角色动态渲染底部操作按钮。 +2. **查询/筛选交互流程**:切换标签页时无额外查询(数据一次性加载),仅切换展示内容;流转记录按时间轴倒序展示。 +3. **表单填写与提交流程**:点击"分配工单"弹出分配弹窗,选择班组和维修人员后提交;点击"关闭工单"弹出关闭原因输入弹窗,填写后提交;点击"验收不通过"弹出原因输入弹窗,填写后提交退回;延期审批弹窗选择"通过"或"驳回",驳回需填写原因。 +4. **弹窗/抽屉交互流程**:分配弹窗——选择班组后联动加载班组内人员列表,选择人员后确认提交;关闭弹窗——必填关闭原因,支持最多200字;验收不通过弹窗——必填不通过原因,支持最多200字;延期审批弹窗——radio选择通过/驳回,驳回时显示原因输入框。所有弹窗点击遮罩层不关闭。 +5. **行内操作流程**:照片缩略图点击后打开el-image-viewer大图预览,支持左右切换;评价信息中图片同样支持点击预览;流转记录时间轴展示操作人、时间、操作类型、备注。 +6. **异常与错误处理**:工单不存在时显示404占位页并提示"工单不存在或已被删除";操作失败显示el-notification错误提示;网络异常时底部按钮置灰并提示"网络异常,请刷新重试";照片加载失败显示占位图。 +7. **联动/级联交互**:分配弹窗中班组选择后联动加载该班组下的人员列表;标签页切换无延迟,数据预加载;补录工单在基本信息区额外显示补录原因和审核状态区块。 +8. **权限控制交互表现**:维修人员角色底部操作栏完全隐藏;主管仅可见分配、审批、验收按钮;状态不匹配的按钮隐藏(如待分配时不显示验收按钮);操作按钮根据当前工单状态动态显示/隐藏。 + +9. **【H1 防重复请求(轻量)】**:详情加载时防止重复请求(路由参数不变时不重复调用);底部操作栏各按钮(分配/关闭/审批/验收)点击后立即 disabled + loading态,API返回后恢复;标签页切换无需请求(数据一次性预加载完成)。 +10. **【H2 统一超时配置】**:GET 详情查询超时15秒;POST 分配/关闭/延期审批/验收操作超时30秒;超时时中断请求 + 提示"请求超时,请稍后重试" + 按钮恢复可用;工单不存在(404)直接展示占位页,不做超时二次请求。 +11. **【H3 操作确认机制】**:"分配工单"前确认选择的目标班组和人员正确(可选轻提示或弹窗确认);"关闭工单"前必须弹出 `ElMessageBox.confirm("确定要关闭工单「{工单号}」?关闭后将无法恢复处理", {type: 'warning'})`;"延期审批-驳回"前提示含后果说明"驳回后将退回处理中状态";"验收不通过"前提示"将退回返修流程,确认?"。 +12. **【H8 操作结果反馈】**:分配/关闭/审批/验收等操作成功后 `ElMessage.success`(2s)+ 刷新详情数据和流转记录;操作失败 `ElMessage.error`(手动关闭)+ 保留用户已填写的表单内容(如原因输入框);照片加载失败显示占位图而非错误toast。 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 状态栏 | el-descriptions | :column="2", border, title="工单信息" | +| 标签页 | el-tabs | v-model="activeTab", type="card" | +| 基本信息展示 | el-descriptions | :column="2", border | +| 流转记录 | el-timeline | — | +| 流转记录项 | el-timeline-item | :timestamp="item.time", placement="top" | +| 照片附件 | el-image | :preview-src-list="photoList", fit="cover", :lazy="true" | +| 图片预览 | el-image-viewer | :url-list="photoList" | +| 评价信息-评分 | el-rate | disabled, :colors="['#99A9BF', '#F7BA2A', '#FF9900']" | +| 底部操作栏 | el-affix | position="bottom", :offset="0" | +| 分配弹窗 | el-dialog | title="分配工单", width="500px", :close-on-click-modal="false" | +| 关闭弹窗 | el-dialog | title="关闭工单", width="500px", :close-on-click-modal="false" | +| 验收不通过弹窗 | el-dialog | title="验收不通过", width="500px", :close-on-click-modal="false" | +| 延期审批弹窗 | el-dialog | title="延期审批", width="500px", :close-on-click-modal="false" | +| 班组选择 | el-select | placeholder="请选择班组", filterable, @change="loadStaff" | +| 人员选择 | el-select | placeholder="请选择维修人员", filterable | +| 关闭原因 | el-input | type="textarea", :rows="3", maxlength=200, show-word-limit | +| 状态标签 | el-tag | :type根据状态映射,size="large" | +| 补录标记区块 | el-alert | type="warning", :closable="false", show-icon | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 分配班组 | 必选 | "请选择负责班组" | +| 分配维修人员 | 必选 | "请选择维修人员" | +| 关闭原因 | 必填,maxlength=200 | "请填写关闭原因" | +| 验收不通过原因 | 必填,maxlength=200 | "请填写不通过原因" | +| 延期审批-驳回原因 | 驳回时必填,maxlength=200 | "请填写驳回原因" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 基本信息el-descriptions两列展示;底部操作栏固定在页面底部;标签页完整展示4个标签;照片缩略图4列网格排列 | +| 1024-1279px(Pad横屏) | 基本信息el-descriptions两列展示但标签列缩窄;照片缩略图3列网格排列;底部操作栏按钮缩小为small尺寸 | +| 768-1023px(Pad竖屏) | 基本信息el-descriptions改为单列展示;标签页改为el-dropdown切换(节省空间);照片缩略图2列网格排列;底部操作栏按钮组改为堆叠排列,主操作按钮full-width | + --- ## 页面3:新增工单弹窗 @@ -228,6 +347,62 @@ | 报修描述 | 多行文本 | 是 | — | 自填 | 最大500字 | | 照片 | 图片上传 | 否 | — | 拍照/相册 | ≤9张,单张≤20MB,自动加水印 | +### 交互流程要求 + +1. **页面加载流程**:点击工单列表页"新增工单"按钮后打开弹窗;并行加载报修类型字典选项、区域级联数据、紧急程度选项;紧急程度默认选中"普通";弹窗打开后聚焦第一个必填字段(报修类型)。 +2. **查询/筛选交互流程**:不适用(弹窗无筛选功能)。 +3. **表单填写与提交流程**:用户依次填写报修类型、紧急程度、报修区域、报修人、联系电话、预约时间、报修描述、照片上传;点击"提交"按钮触发表单校验,校验通过后调用新增API;提交成功关闭弹窗并刷新列表,显示成功提示;提交失败显示具体错误信息。点击"取消"关闭弹窗,不保存数据。 +4. **弹窗/抽屉交互流程**:弹窗宽度600px,标题"新增工单";点击右上角×号或"取消"按钮关闭弹窗;点击遮罩层不关闭(防止误操作);照片上传区域点击"+"触发文件选择,支持多选;已上传照片显示缩略图,右上角有删除按钮。 +5. **行内操作流程**:报修区域级联选择——选择项目后加载区域,逐级联动;预约时间选择日期时间后自动关闭选择器;照片上传后显示进度条,上传失败显示重试按钮。 +6. **异常与错误处理**:必填项未填时提交按钮触发el-form校验,对应字段下方显示红色错误提示;联系电话格式不正确实时提示;照片上传失败显示上传失败标识,可点击重试;上传超过9张时提示"最多上传9张照片";单张超过20MB时提示"单张照片不能超过20MB";提交API失败显示el-notification错误信息。 +7. **联动/级联交互**:报修区域级联选择——选择项目→加载区域→选择区域→加载楼栋→选择楼栋→加载楼层,至少选到楼层;照片上传自动添加水印(时间+位置+蓝牙标记)。 +8. **权限控制交互表现**:仅物业管理员角色可点击"新增工单"按钮打开此弹窗;主管和维修人员角色无此按钮。 + +9. **【H1 防重复请求】**:提交按钮点击后立即 `:loading="true"` + `disabled`,防止重复提交;弹窗内级联选择/下拉选项并行加载,互不阻塞表单填写;照片上传每个文件独立的进度跟踪,互不影响。 +10. **【H2 统一超时配置】**:POST 新增工单API超时30秒;照片上传单个文件超时60秒;超时自动中断 + ElMessage.error("上传超时,请检查网络后重试") + 按钮恢复;>3秒的请求在弹窗内显示局部loading指示器(非全局遮罩)。 +11. **【H4 脏数据检测】**:进入弹窗时初始化空表单作为快照基准;用户修改任意字段后标记 isDirty=true;点击"取消"按钮时若 isDirty 为 true,弹出 `ElMessageBox.confirm("表单已有未保存的修改,确定关闭吗?", {type: 'warning'})`;点击遮罩层和×号同样触发脏数据检测拦截。 +12. **【H7 文件上传约束】**:照片单张≤20MB(按原文档要求),每次最多9张,格式白名单 image/jpeg,image/png,image/gif,image/webp;超出限制时 el-upload 触发 on-exceed 回调提示;每个文件上传进度条实时展示(el-progress 或 upload 内置进度);上传失败的单张显示错误标识并提供重试按钮。 +13. **【H8 操作结果反馈】**:提交成功 `ElMessage.success("工单创建成功")`(2s)→ 自动关闭弹窗 → silent 刷新父级列表;提交失败 `ElMessage.error`(手动关闭)保留表单内容便于修改重试;校验失败滚动到首个错误字段并聚焦。 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 弹窗容器 | el-dialog | title="新增工单", width="600px", :close-on-click-modal="false", destroy-on-close | +| 表单 | el-form | :model="form", :rules="rules", ref="formRef", label-width="90px", label-position="right" | +| 报修类型 | el-select | placeholder="请选择报修类型", filterable, clearable | +| 紧急程度 | el-select | placeholder="请选择紧急程度" | +| 报修区域 | el-cascader | :props="{checkStrictly: false, emitPath: false}", placeholder="请选择报修区域", filterable, clearable | +| 报修人 | el-input | placeholder="请输入报修人姓名", maxlength=20, clearable | +| 联系电话 | el-input | placeholder="请输入联系电话", maxlength=11, clearable | +| 预约时间 | el-date-picker | type="datetime", placeholder="请选择预约时间", value-format="YYYY-MM-DD HH:mm", :disabled-date="disablePastDate" | +| 报修描述 | el-input | type="textarea", :rows="4", maxlength=500, show-word-limit, placeholder="请描述报修问题" | +| 照片上传 | el-upload | action="/api/v1/files/upload", list-type="picture-card", :limit=9, :file-size=20, accept="image/*", :on-exceed="handleExceed" | +| 提交按钮 | el-button | type="primary", :loading="submitting" | +| 取消按钮 | el-button | @click="dialogVisible=false" | +| 表单项 | el-form-item | :label="字段名", :prop="字段key" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 报修类型 | 必选 | "请选择报修类型" | +| 紧急程度 | 必选 | "请选择紧急程度" | +| 报修区域 | 必选,且必须选到楼层级(level≥4) | "请选择完整的报修区域,至少选到楼层" | +| 报修人 | 必填,maxlength=20,不允许纯空格 | "请输入报修人姓名" | +| 联系电话 | 必填,正则/^1[3-9]\d{9}$/ | "请输入正确的手机号码" | +| 预约时间 | 可选,但不早于当前时间 | "预约时间不能早于当前时间" | +| 报修描述 | 必填,maxlength=500,不允许纯空格 | "请描述报修问题" | +| 照片 | 可选,≤9张,单张≤20MB | "最多上传9张照片" / "单张照片不能超过20MB" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 弹窗宽度600px,表单两列布局(报修类型+紧急程度同行,报修人+联系电话同行);照片上传区4列网格 | +| 1024-1279px(Pad横屏) | 弹窗宽度560px,表单两列布局不变,字段标签缩窄至80px;照片上传区3列网格 | +| 768-1023px(Pad竖屏) | 弹窗宽度90vw,表单改为单列布局,所有字段垂直堆叠;照片上传区3列网格;底部按钮组full-width堆叠排列 | + --- ## 页面4:报修类型管理页 @@ -277,6 +452,55 @@ | 编辑 | /api/v1/repair-types/{id} | PUT | — | | 启用/停用 | /api/v1/repair-types/{id}/toggle-status | PUT | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用列表查询API GET /api/v1/repair-types → 渲染类型列表表格,默认按序号升序排列;列表无分页(数据量小) +2. **查询/筛选交互流程**:本页无独立筛选条件,所有类型数据一次加载展示 +3. **表单填写与提交流程**:点击[新增类型] → 弹窗表单 → 填写类型名称、选择关联班组(多选)、填写描述 → 点击确定 → 前端校验通过 → 调用新增API → 成功后关闭弹窗并刷新列表 +4. **弹窗/抽屉交互流程**:新增/编辑弹窗宽度480px,标题根据操作切换"新增报修类型"/"编辑报修类型";点击遮罩层不关闭;关联班组多选时使用collapse-tags折叠展示 +5. **行内操作流程**:点击[编辑] → 弹窗回填当前类型数据 → 修改后提交 → 刷新当前行;点击[停用] → 二次确认弹窗"停用后该类型将不再出现在报修选项中,确认停用?" → 确认后调用toggle-status API → 刷新列表;有工单关联的不可停用,按钮置灰并Tooltip提示"该类型下存在关联工单,不可停用" +6. **异常与错误处理**:类型名称重复提示"该报修类型名称已存在";API请求失败显示ElMessage.error;停用关联工单的类型时后端返回400,前端提示"该类型下存在关联工单,请先迁移" +7. **联动/级联交互**:关联班组下拉选择时数据来源于组织架构-班组列表;类型列表中"工单数量"为实时统计值 +8. **权限控制交互表现**:无 repair:type:create 权限时[新增类型]按钮隐藏;无 repair:type:update 权限时[编辑][停用]按钮隐藏 + +9. **【H1 防重复请求】**:查询/刷新列表时按钮 disabled + loading态;[新增]/[编辑]弹窗的提交按钮 :loading + disabled 防重复提交;[停用]操作点击后该行禁用 + loading态;列表无分页但仍需防止并发重复请求。 +10. **【H2 统一超时配置】**:GET 列表查询超时15秒;POST 新增超时30秒;PUT 编辑/启用停用超时30秒;超时中断 + 提示"请求超时..." + 按钮恢复。 +11. **【H3 操作确认机制】**:[停用]操作前弹出 `ElMessageBox.confirm("确定停用「{类型名称}」?停用后将不再出现在报修选项中", {type: 'warning'})` 含操作后果说明;[编辑]保存前若检测到名称变更可加轻量提示。 +12. **【H4 脏数据检测】**:[编辑]弹窗打开时对当前行数据做 deep clone 作为原始快照;用户修改后 isDirty 检测;取消/关闭弹窗时若 isDirty 弹出"修改未保存,确定关闭?"确认框;[新增]弹窗同理检测是否有任何输入内容。 +13. **【H8 操作结果反馈】**:新增/编辑成功 `ElMessage.success`(2s)→ 关闭弹窗 → 刷新列表;停用成功更新该行状态标签为"停用";失败 `ElMessage.error`(手动关闭)+ 保留表单内容。 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 列表 | el-table | stripe, border, :data="typeList", size="default" | +| 新增类型按钮 | el-button | type="primary", icon="Plus", v-hasPermission="['repair:type:create']" | +| 编辑按钮 | el-button | type="primary", link, v-hasPermission="['repair:type:update']" | +| 启用/停用按钮 | el-button | type="warning/danger", link, :disabled="hasOrderBind", v-hasPermission="['repair:type:update']" | +| 新增/编辑弹窗 | el-dialog | :title="dialogTitle", width="480px", :close-on-click-modal="false", destroy-on-close | +| 类型名称 | el-input | maxlength=20, show-word-limit, placeholder="请输入类型名称" | +| 关联班组 | el-select | v-model="form.teamIds", multiple, collapse-tags, collapse-tags-tooltip, filterable, placeholder="请选择关联班组" | +| 描述 | el-input | type="textarea", :rows="3", maxlength=200, show-word-limit, placeholder="请输入描述" | +| 状态标签 | el-tag | 启用=success / 停用=info | +| 工单数量 | el-link | type="primary", @click="viewOrders" | +| 二次确认 | el-message-box | type="warning", confirmButtonText="确定", cancelButtonText="取消" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 类型名称 | 必填,maxlength=20,同租户内唯一 | "请输入类型名称" / "该报修类型名称已存在" | +| 关联班组 | 必填,至少选择1个 | "请选择关联班组" | +| 描述 | 非必填,maxlength=200 | — | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 表格完整展示全部列,弹窗宽度480px | +| 1024-1279px(Pad横屏) | 表格隐藏"描述"列,弹窗宽度440px | +| 768-1023px(Pad竖屏) | 表格仅显示类型名称、关联班组、状态、操作列,弹窗宽度90vw | + --- ## 页面5:数据补录页 @@ -333,6 +557,58 @@ | 列表查询 | /api/v1/repair-orders/supplements | GET | 分页查询 | | 审核 | /api/v1/repair-orders/supplements/{id}/approve | POST | 通过/驳回 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用补录列表API GET /api/v1/repair-orders/supplements → 渲染补录数据表格,默认按补录时间倒序排列,每页20条 +2. **查询/筛选交互流程**:选择补录状态/输入补录人/选择日期范围 → 点击[查询] → 携带筛选参数重新请求第1页数据 → 点击[重置]清空条件并重新加载 +3. **表单填写与提交流程**:本页无新增表单,补录数据由小程序端提交 +4. **弹窗/抽屉交互流程**:点击[审核] → 弹出审核弹窗,选择"通过"或"驳回",驳回时必填驳回原因 → 确认后调用审核API → 成功后关闭弹窗并刷新列表;点击[详情] → 弹出补录详情弹窗,展示补录原因、详细说明、补录人工单信息等 +5. **行内操作流程**:审核状态=待审核时显示[审核]按钮;点击[审核] → 弹窗选择通过/驳回 → 提交 → 刷新该行状态;点击[详情] → 弹窗展示完整补录信息 +6. **异常与错误处理**:API请求失败显示ElMessage.error;审核已审核过的记录时提示"该记录已审核";列表无数据时显示ElEmpty +7. **联动/级联交互**:审核状态筛选影响列表展示;补录原因枚举来源于系统配置 +8. **权限控制交互表现**:无 repair:supplement:approve 权限时[审核]按钮隐藏;无 repair:supplement:view 权限时[详情]按钮隐藏 + +9. **【H1 防重复请求】**:查询按钮点击后 disabled + loading态,API返回后恢复;行内[审核]按钮点击后该行禁用 + loading态;分页切换 abort上一请求再发新请求。 +10. **【H2 统一超时配置】**:GET 补录列表查询超时15秒;POST 审核操作超时30秒;超时自动中断 + "请求超时,请稍后重试" + 按钮恢复;>3秒显示全局 ElLoading。 +11. **【H3 操作确认机制】**:审核操作前弹出 `ElMessageBox.confirm("确定要「{通过/驳回}」该条补录记录吗?驳回后将通知补录人重新执行巡检", {type: 通过?'info':'warning'})`,通过/驳回均需含操作后果说明。 +12. **【H5 数据权限隔离】**:区分"暂无补录记录"(ElEmpty)与"无权限访问"(403);403时前端拦截器统一展示"您没有权限查看补录数据"。 +13. **【H8 操作结果反馈】**:审核成功 `ElMessage.success("审核成功")`(2s)→ 关闭弹窗 → silent 刷新该行状态;审核失败 `ElMessage.error`(手动关闭);网络异常提示含重试按钮。 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 补录状态下拉 | el-select | v-model="query.auditStatus", clearable, placeholder="请选择审核状态" | +| 补录人输入 | el-input | v-model="query.supplementUser", clearable, maxlength=20, placeholder="请输入补录人" | +| 日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD", range-separator="至" | +| 列表 | el-table | stripe, border, :data="tableData" | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next", :page-sizes="[10,20,50]" | +| 审核按钮 | el-button | type="primary", link, v-hasPermission="['repair:supplement:approve']" | +| 详情按钮 | el-button | type="primary", link, v-hasPermission="['repair:supplement:view']" | +| 审核状态标签 | el-tag | 待审核=warning / 已通过=success / 已驳回=danger | +| 补录标记标签 | el-tag | type="warning", size="small" | +| 审核弹窗 | el-dialog | title="审核补录", width="500px", :close-on-click-modal="false" | +| 审核结果 | el-radio-group | v-model="auditForm.result" | +| 驳回原因 | el-input | type="textarea", :rows="3", maxlength=200, show-word-limit, v-if="auditForm.result==='reject'" | +| 详情弹窗 | el-dialog | title="补录详情", width="600px", :close-on-click-modal="true" | +| 详情展示 | el-descriptions | :column="2", border | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 审核结果 | 必选 | "请选择审核结果" | +| 驳回原因 | 驳回时必填,maxlength=200 | "请填写驳回原因" | +| 日期范围 | 结束日期≥开始日期 | "结束日期不能早于开始日期" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区水平排列,表格完整展示全部列,弹窗宽度500px/600px | +| 1024-1279px(Pad横屏) | 查询条件区换行排列,表格隐藏"补录时间"列,弹窗宽度460px/560px | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,表格仅显示工单号、补录人、审核状态、操作列,弹窗宽度90vw | + --- ## 需求追溯 diff --git a/docs/02-功能清单-物业公司/02-巡检管理.md b/docs/02-功能清单-物业公司/02-巡检管理.md index 730543c..0d0bd1d 100644 --- a/docs/02-功能清单-物业公司/02-巡检管理.md +++ b/docs/02-功能清单-物业公司/02-巡检管理.md @@ -3,6 +3,7 @@ > 模块编码:inspection > 端侧:Web + 小程序(双端) > 关联文档:01-模块划分 §3.2 / 02-功能清单-物业公司 §2 / 03-业务流转逻辑-物业公司 §2 / 05-接口规范 §9.2 / 06-项目技术要求 §4.4 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -96,6 +97,73 @@ | 编辑 | /api/v1/inspection-plans/{id} | PUT | — | | 启用/停用 | /api/v1/inspection-plans/{id}/toggle-status | PUT | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 并行调用列表查询API GET /api/v1/inspection-plans 和下拉选项数据(巡检类型字典、班组列表)→ 渲染表格和筛选条件,默认按序号升序排列,加载第一页数据(每页20条) +2. **查询/筛选交互流程**:填写计划名称/选择状态/选择巡检类型/选择负责班组 → 点击[查询] → 携带筛选参数重新请求第1页 → 点击[重置]清空条件并重新加载;计划名称输入框支持回车触发查询 +3. **表单填写与提交流程**:点击[新增计划] → 跳转新增页面或打开弹窗 → 填写计划名称、巡检类型、巡检区域(级联多选)、巡检设备、频次(自定义时填cron表达式)、负责班组、巡检人员(班组内多选)、巡检清单、生效日期 → 前端校验通过 → 调用新增API → 成功后跳回列表并刷新 +4. **弹窗/抽屉交互流程**:编辑弹窗宽度600px,回填当前计划数据;选择负责班组后联动加载班组内人员列表;频次选择"自定义"时显示cron表达式输入框 +5. **行内操作流程**:点击[编辑] → 弹窗回填计划数据 → 修改后提交 → 刷新列表;点击[停用] → 二次确认"停用后将不再自动生成巡检任务,确认停用?" → 确认后调用toggle-status API → 刷新该行状态 +6. **异常与错误处理**:计划名称重复提示"该计划名称已存在";cron表达式格式错误提示"请输入有效的cron表达式";生效日期早于当前日期提示"生效日期不能早于今天";API请求失败显示ElMessage.error +7. **联动/级联交互**:选择负责班组后联动加载班组内人员下拉选项;巡检区域级联多选——选择项目→加载区域→选择区域→加载楼栋,逐级展开;频次=自定义时显示cron表达式输入框 +8. **权限控制交互表现**:无 inspection:plan:create 权限时[新增计划]按钮隐藏;无 inspection:plan:update 权限时[编辑][停用]按钮隐藏 + +9. **【H1 防重复请求】**:查询按钮点击后立即 disabled + loading态,API返回后恢复;分页切换时 abort 上一次未完成的列表请求再发新请求;[新增]/[编辑]弹窗提交按钮 :loading + disabled 防重复提交;[停用]操作点击后该行禁用 + loading态;页面加载时并行请求列表和字典数据,互不阻塞。 +10. **【H2 统一超时配置】**:GET 列表查询超时15秒;POST 新增计划超时30秒;PUT 编辑/启用停用超时30秒;超时自动中断 + 提示"请求超时,请稍后重试" + 按钮恢复可用;请求>3秒显示全局 ElLoading。 +11. **【H3 操作确认机制】**:[停用]操作前弹出 `ElMessageBox.confirm("确定停用「{计划名称}」?停用后将不再自动生成巡检任务", {type: 'warning'})` 含操作后果说明。 +12. **【H4 脏数据检测】**:[编辑]弹窗打开时对当前计划数据做 deep clone 作为原始快照;用户修改后 isDirty 检测;取消/关闭弹窗时若 isDirty 弹出"修改未保存,确定关闭?"确认框(含 type:'warning');[新增]弹窗同理检测是否有任何输入内容。 +13. **【H8 操作结果反馈】**:新增/编辑成功 `ElMessage.success`(2s)→ 关闭弹窗 → silent 刷新列表;停用成功更新该行状态标签为"停用";失败 `ElMessage.error`(手动关闭)保留表单内容便于修改重试。 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 计划名称输入 | el-input | placeholder="请输入计划名称", clearable, maxlength=50 | +| 状态筛选 | el-select | placeholder="请选择状态", clearable | +| 巡检类型筛选 | el-select | placeholder="请选择巡检类型", clearable, filterable | +| 负责班组筛选 | el-select | placeholder="请选择负责班组", clearable, filterable | +| 列表 | el-table | stripe, border, :data="tableData", :max-height="calc(100vh - 280px)" | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next", :page-sizes="[10,20,50]" | +| 新增计划按钮 | el-button | type="primary", icon="Plus" | +| 编辑按钮 | el-button | type="primary", link | +| 停用按钮 | el-button | type="warning", link | +| 状态标签 | el-tag | 启用=success / 停用=info | +| 巡检类型标签 | el-tag | type="primary", size="small" | +| 编辑弹窗 | el-dialog | title="编辑巡检计划", width="600px", :close-on-click-modal="false" | +| 计划名称 | el-input | maxlength=50, show-word-limit, placeholder="请输入计划名称" | +| 巡检类型 | el-select | placeholder="请选择巡检类型", filterable | +| 巡检区域 | el-cascader | :props="{multiple:true, checkStrictly:true, emitPath:false}", placeholder="请选择巡检区域", filterable, clearable | +| 巡检设备 | el-select | v-model="form.deviceIds", multiple, collapse-tags, placeholder="请选择巡检设备" | +| 频次 | el-select | placeholder="请选择频次" | +| 自定义cron | el-input | v-if="form.frequency==='custom'", placeholder="请输入cron表达式" | +| 负责班组 | el-select | placeholder="请选择负责班组", filterable, @change="loadStaff" | +| 巡检人员 | el-select | v-model="form.staffIds", multiple, collapse-tags, placeholder="请选择巡检人员" | +| 巡检清单 | el-select | v-model="form.checklistIds", multiple, collapse-tags, placeholder="请选择巡检清单" | +| 生效日期 | el-date-picker | type="date", value-format="YYYY-MM-DD", :disabled-date="disablePastDate" | +| 二次确认 | el-message-box | type="warning", confirmButtonText="确定", cancelButtonText="取消" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 计划名称 | 必填,maxlength=50,同租户内唯一 | "请输入计划名称" / "该计划名称已存在" | +| 巡检类型 | 必选 | "请选择巡检类型" | +| 巡检区域 | 必选,至少选一个 | "请选择巡检区域" | +| 频次 | 必选 | "请选择频次" | +| 自定义cron | 频次=自定义时必填,cron格式校验 | "请输入cron表达式" / "cron表达式格式不正确" | +| 负责班组 | 必选 | "请选择负责班组" | +| 巡检人员 | 必选,至少选一个 | "请选择巡检人员" | +| 巡检清单 | 必选,至少选一个 | "请选择巡检清单" | +| 生效日期 | 必选,不早于当前日期 | "请选择生效日期" / "生效日期不能早于今天" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区单行展示;表格完整展示全部9列;弹窗宽度600px | +| 1024-1279px(Pad横屏) | 查询条件区折行两行;表格隐藏"巡检区域""巡检人员"列;弹窗宽度560px | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠;表格隐藏"巡检区域""巡检人员""频次"列;弹窗宽度90vw,表单改为单列布局 | + --- ## 页面2:巡检任务看板页 @@ -157,6 +225,54 @@ | 任务列表 | /api/v1/inspection-tasks | GET | 分页查询 | | 日历数据 | /api/v1/inspection-tasks/calendar | GET | 按月返回日历数据 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认日期为今天,视图为日历视图 → 并行调用任务列表API和日历数据API → 渲染日历视图(按月展示每日上午/下午巡检状态),列表视图渲染任务表格 +2. **查询/筛选交互流程**:切换日期(点击左右箭头)→ 重新请求日历/列表数据;选择计划名称/状态/输入巡检人员 → 点击[查询] → 重新加载数据;切换日历/列表视图时无需重新请求(数据共用) +3. **表单填写与提交流程**:本页无表单提交操作,仅查询展示 +4. **弹窗/抽屉交互流程**:点击日历某日某时段 → 弹出该时段巡检任务列表弹窗,展示具体任务详情;弹窗宽度500px +5. **行内操作流程**:列表视图点击[查看] → 跳转巡检记录详情页;异常数>0时点击异常数 → 跳转异常处理跟踪页(带筛选条件) +6. **异常与错误处理**:日历数据加载失败时日历区显示"加载失败,点击重试";列表数据为空时显示ElEmpty"暂无巡检任务";网络异常提示ElMessage.error +7. **联动/级联交互**:日历视图与列表视图数据联动,切换视图不重新请求;日期切换后日历和列表同步刷新 +8. **权限控制交互表现**:无 inspection:task:view 权限时[查看]按钮隐藏,仅展示基本信息 + +9. **【H1 防重复请求(轻量)】**:日期切换(左右箭头)时防抖300ms后发起请求,避免快速连续点击产生冗余请求;查询按钮点击后 disabled + loading态;日历数据和列表数据并行加载互不阻塞;视图切换(日历/列表)无需重新请求。 +10. **【H2 统一超时配置】**:GET 任务列表/日历数据API超时15秒;超时中断 + 日历区显示"加载失败,点击重试" + 列表区显示 ElEmpty 占位;>3秒的请求在各自区域内显示局部 loading 而非全屏遮罩。 +11. **【H8 操作结果反馈】**:数据加载完成无额外toast提示(静默渲染);加载失败 ElMessage.error("数据加载失败,请检查网络")(手动关闭);日历某日点击弹出详情弹窗失败时弹窗内展示错误占位。 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 视图切换 | el-radio-group + el-radio-button | v-model="viewMode", size="default" | +| 日期导航 | 自定义组件 | 左右箭头+日期显示,@prev/@next事件 | +| 日历视图 | 自定义日历组件 | 按月渲染,每日分上午/下午格子,颜色标识状态 | +| 列表视图 | el-table | stripe, border, :data="taskList" | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next", :page-sizes="[10,20,50]" | +| 状态标签 | el-tag | 正常=success / 异常=danger / 未执行=warning | +| 日期筛选 | el-date-picker | type="date", value-format="YYYY-MM-DD" | +| 计划名称 | el-select | clearable, placeholder="全部计划" | +| 状态筛选 | el-select | clearable, placeholder="全部状态" | +| 巡检人员 | el-input | clearable, maxlength=20, placeholder="输入巡检人员" | +| 查看按钮 | el-button | type="primary", link | +| 异常数链接 | el-link | type="danger", @click="viewAbnormals" | +| 日历详情弹窗 | el-dialog | title="巡检任务详情", width="500px" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 巡检人员 | 非必填,maxlength=20 | — | +| 状态 | 非必填,枚举值 | — | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 日历视图完整展示,列表视图全部列展示 | +| 1024-1279px(Pad横屏) | 日历视图字号略减,列表视图隐藏"打卡方式"列 | +| 768-1023px(Pad竖屏) | 日历视图改为简化周视图(仅显示本周7天);列表视图隐藏"打卡方式""计划名称"列,查询条件垂直堆叠 | + --- ## 页面3:巡检记录查询页 @@ -203,6 +319,58 @@ | 记录列表 | /api/v1/inspection-records | GET | 分页查询 | | 记录详情 | /api/v1/inspection-records/{id} | GET | 含打卡信息+检查项结果+照片 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认查询条件为空 → 调用记录列表API GET /api/v1/inspection-records → 渲染表格,默认按打卡时间倒序排列,每页20条 +2. **查询/筛选交互流程**:输入巡检人员/选择日期范围/选择打卡方式/选择结果状态/选择补录标记 → 点击[查询] → 携带筛选参数重新请求第1页 → 点击[重置]清空条件并重新加载 +3. **表单填写与提交流程**:本页无表单提交操作,仅查询展示 +4. **弹窗/抽屉交互流程**:点击[查看详情] → 调用记录详情API → 弹窗展示完整巡检记录(打卡信息、检查项结果、照片),弹窗宽度700px,使用el-tabs分为"打卡信息""检查项""照片"三个标签页 +5. **行内操作流程**:点击[查看异常](异常数>0时显示)→ 跳转异常处理跟踪页,自动带入该记录的异常筛选条件;补录标记列点击"补录"标签 → 弹出补录原因浮层 +6. **异常与错误处理**:列表数据为空时显示ElEmpty"暂无巡检记录";详情弹窗加载失败提示"记录详情加载失败";API请求失败显示ElMessage.error +7. **联动/级联交互**:打卡方式列根据值显示不同颜色标签(蓝牙-蓝/手动-灰/补录-橙);补录标记与打卡方式联动,补录方式=补录时显示补录标签 +8. **权限控制交互表现**:无 inspection:task:view 权限时[查看详情][查看异常]按钮隐藏 + +9. **【H1 防重复请求】**:查询按钮点击后立即 disabled + loading态,API返回后恢复;行内[查看详情]按钮点击后该行禁用 + loading态(防止重复打开弹窗);分页切换 abort 上一请求再发新请求;详情弹窗打开时防止同一记录重复请求详情API。 +10. **【H2 统一超时配置】**:GET 记录列表查询超时15秒;GET 记录详情查询超时15秒;超时自动中断 + 提示"请求超时..." + 按钮/弹窗恢复;>3秒显示全局 ElLoading。 +11. **【H5 数据权限隔离】**:区分"暂无巡检记录"(ElEmpty)与"无权限访问"(403);403时前端拦截器统一提示"您没有权限查看巡检记录",不暴露具体数据结构。 +12. **【H8 操作结果反馈】**:列表刷新静默无提示;详情弹窗加载失败弹窗内展示"记录详情加载失败,请重试";ElMessage.error 仅在主动操作失败时使用(duration=0手动关闭)。 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 巡检人员输入 | el-input | placeholder="请输入巡检人员", clearable, maxlength=20 | +| 日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD", range-separator="至" | +| 打卡方式 | el-select | clearable, placeholder="全部方式" | +| 结果状态 | el-select | clearable, placeholder="全部状态" | +| 补录标记 | el-select | clearable, placeholder="全部" | +| 列表 | el-table | stripe, border, :data="tableData" | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next", :page-sizes="[10,20,50]" | +| 打卡方式标签 | el-tag | 蓝牙=primary / 手动=info / 补录=warning | +| 补录标记标签 | el-tag | type="warning", size="small" | +| 查看详情按钮 | el-button | type="primary", link | +| 查看异常按钮 | el-button | type="danger", link | +| 详情弹窗 | el-dialog | title="巡检记录详情", width="700px", :close-on-click-modal="true" | +| 详情标签页 | el-tabs | v-model="detailTab", type="card" | +| 打卡信息 | el-descriptions | :column="2", border | +| 检查项列表 | el-table | :data="checkItems", size="small" | +| 照片展示 | el-image | :preview-src-list="photoList", fit="cover", :lazy="true" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 巡检人员 | 非必填,maxlength=20 | — | +| 日期范围 | 结束日期≥开始日期 | "结束日期不能早于开始日期" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区水平排列,表格完整展示全部9列,详情弹窗宽度700px | +| 1024-1279px(Pad横屏) | 查询条件区换行排列,表格隐藏"检查项数""补录标记"列,详情弹窗宽度650px | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,表格仅显示巡检人员、打卡时间、状态、操作列,详情弹窗宽度95vw | + --- ## 页面4:异常处理跟踪页 @@ -254,6 +422,50 @@ | 异常列表 | /api/v1/inspection-abnormals | GET | 分页查询 | | 生成工单 | /api/v1/inspection-abnormals/{id}/create-order | POST | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用异常列表API GET /api/v1/inspection-abnormals → 渲染表格,默认按上报时间倒序排列,每页20条;同时加载严重等级、处理状态等下拉选项 +2. **查询/筛选交互流程**:选择严重等级/选择处理状态/选择日期范围 → 点击[查询] → 携带筛选参数重新请求第1页 → 点击[重置]清空条件并重新加载 +3. **表单填写与提交流程**:点击[生成报修工单](处理状态=待处理时显示)→ 弹窗确认"将为此异常生成报修工单,确认?" → 确认后调用生成工单API → 成功后刷新该行状态为"已生成工单",并显示工单号 +4. **弹窗/抽屉交互流程**:生成工单确认弹窗宽度420px;点击[查看] → 弹出异常详情弹窗,展示异常描述、照片、关联巡检记录、处理历史 +5. **行内操作流程**:点击[生成报修工单] → 二次确认 → 调用API → 刷新行数据;点击[查看] → 弹窗展示详情;关联工单号可点击跳转工单详情页(新标签页) +6. **异常与错误处理**:重复生成工单提示"该异常已生成工单,不可重复操作";API请求失败显示ElMessage.error;列表无数据时显示ElEmpty"暂无异常记录" +7. **联动/级联交互**:处理状态=待处理时才显示"生成报修工单"按钮;生成工单后状态自动变为"已生成工单",关联工单号可点击跳转 +8. **权限控制交互表现**:无 repair:list:create 权限时"生成报修工单"按钮隐藏;无 inspection:task:view 权限时"查看"按钮隐藏 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 严重等级筛选 | el-select | clearable, placeholder="全部等级" | +| 处理状态筛选 | el-select | clearable, placeholder="全部状态" | +| 日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD", range-separator="至" | +| 列表 | el-table | stripe, border, :data="tableData" | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next", :page-sizes="[10,20,50]" | +| 严重等级标签 | el-tag | 一般=warning / 较重=danger / 严重=danger, effect="dark" | +| 处理状态标签 | el-tag | 待处理=warning / 已生成工单=success / 已关闭=info | +| 生成工单按钮 | el-button | type="primary", link, v-if="row.status==='pending'" | +| 查看按钮 | el-button | type="primary", link | +| 关联工单链接 | el-link | type="primary", @click="openOrderDetail" | +| 确认弹窗 | el-message-box | type="warning", confirmButtonText="确认生成", cancelButtonText="取消" | +| 详情弹窗 | el-dialog | title="异常详情", width="600px", :close-on-click-modal="true" | +| 详情展示 | el-descriptions | :column="2", border | +| 异常照片 | el-image | :preview-src-list="photoList", fit="cover" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 日期范围 | 结束日期≥开始日期 | "结束日期不能早于开始日期" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区水平排列,表格完整展示全部9列,详情弹窗宽度600px | +| 1024-1279px(Pad横屏) | 查询条件区换行排列,表格隐藏"巡检记录""异常描述"列,详情弹窗宽度560px | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,表格仅显示异常编号、严重等级、处理状态、操作列,详情弹窗宽度95vw | + --- ## 页面5:巡检区域管理页 @@ -308,6 +520,56 @@ | 编辑 | /api/v1/inspection-areas/{id} | PUT | — | | 删除 | /api/v1/inspection-areas/{id} | DELETE | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用区域树API GET /api/v1/inspection-areas/tree → 渲染左侧树形结构 → 默认选中第一个项目节点 → 右侧展示该节点详情(蓝牙Beacon信息、关联设备、巡检计划) +2. **查询/筛选交互流程**:点击树形节点 → 右侧实时刷新展示选中区域详情;树形节点支持搜索过滤 +3. **表单填写与提交流程**:点击[新增区域] → 弹窗表单 → 填写区域名称、选择上级区域(级联)、选择蓝牙Beacon、选择关联设备 → 点击确定 → 前端校验通过 → 调用新增API → 成功后关闭弹窗并刷新树形结构 +4. **弹窗/抽屉交互流程**:新增/编辑弹窗宽度520px,上级区域级联选择器懒加载展开;点击遮罩层不关闭 +5. **行内操作流程**:点击[编辑] → 弹窗回填当前区域数据 → 修改后提交 → 刷新右侧详情;点击[删除] → 二次确认"删除后不可恢复,确认删除?" → 确认后调用DELETE API → 刷新树形;有关联计划的区域不可删除,删除按钮置灰并Tooltip提示 +6. **异常与错误处理**:区域名称重复提示"该区域名称已存在";有关联计划时删除按钮置灰并提示"该区域存在关联巡检计划,不可删除";网络异常显示全局错误提示 +7. **联动/级联交互**:上级区域级联选择器联动下级选项;选择项目后区域选项刷新,选择区域后楼栋选项刷新,逐级展开;选择蓝牙Beacon后展示设备在线状态 +8. **权限控制交互表现**:无 inspection:area:create 权限时[新增区域]按钮隐藏;无 inspection:area:update 权限时[编辑]按钮隐藏;无 inspection:area:delete 权限时[删除]按钮隐藏 + +9. **【H1 防重复请求】**:树形节点点击防抖200ms避免频繁切换导致右侧反复请求;[新增]/[编辑]弹窗提交按钮 :loading + disabled;[删除]操作点击后该节点禁用 + loading态;区域树初始化加载时显示 skeleton 骨架屏。 +10. **【H2 统一超时配置】**:GET 区域树查询超时15秒;POST 新增超时30秒;PUT 编辑超时30秒;DELETE 删除超时30秒;超时中断 + "请求超时..." + 按钮恢复。 +11. **【H3 操作确认机制】**:[删除]操作前必须弹出 `ElMessageBox.confirm("确定要删除「{区域名称}」吗?删除后将不可恢复", {type: 'error'})` 使用 error 级别强调不可逆;有关联计划的区域删除按钮已置灰(由业务逻辑控制),不触发此确认框。 +12. **【H4 脏数据检测】**:[编辑]弹窗打开时 deep clone 当前区域数据作为快照基准;isDirty 检测;取消/关闭/遮罩层点击时若 isDirty 弹出"修改未保存,确定关闭?"确认框(type:'warning');[新增]弹窗同理。 +13. **【H8 操作结果反馈】**:新增/编辑成功 `ElMessage.success`(2s)→ 关闭弹窗 → 刷新树形结构+右侧详情;删除成功 `ElMessage.success("删除成功")`(2s)→ 刷新树;失败 `ElMessage.error`(手动关闭)。 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 区域树 | el-tree | :data="areaTree", node-key="id", :props="{label:'name',children:'children'}", highlight-current, :expand-on-click-node="false", @node-click="handleNodeClick" | +| 新增区域按钮 | el-button | type="primary", icon="Plus" | +| 编辑按钮 | el-button | type="primary", link | +| 删除按钮 | el-button | type="danger", link, :disabled="hasPlanBind" | +| 新增/编辑弹窗 | el-dialog | :title="dialogTitle", width="520px", :close-on-click-modal="false" | +| 区域名称 | el-input | maxlength=30, show-word-limit, placeholder="请输入区域名称" | +| 上级区域 | el-cascader | :props="{checkStrictly:true, emitPath:false, value:'id', label:'name'}", placeholder="请选择上级区域", clearable, filterable | +| 蓝牙Beacon | el-select | filterable, placeholder="请选择蓝牙Beacon", clearable | +| 关联设备 | el-select | v-model="form.deviceIds", multiple, collapse-tags, placeholder="请选择关联设备" | +| 详情展示 | el-descriptions | :column="2", border | +| Beacon状态标签 | el-tag | 在线=success / 离线=danger | +| 删除确认 | el-message-box | type="warning", confirmButtonText="确认", cancelButtonText="取消" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 区域名称 | 必填,maxlength=30 | "请输入区域名称" / "区域名称不能超过30个字符" | +| 上级区域 | 必填 | "请选择上级区域" | +| 蓝牙Beacon | 必填 | "请选择蓝牙Beacon" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 左侧树形面板固定宽度280px,右侧详情区自适应,弹窗宽度520px | +| 1024-1279px(Pad横屏) | 左侧树形面板宽度缩减至220px,右侧详情区自适应,弹窗宽度480px | +| 768-1023px(Pad竖屏) | 树形面板折叠为顶部下拉选择器,详情区全宽展示,弹窗宽度90vw | + --- ## 页面6:数据补录与补录审核页 @@ -357,6 +619,58 @@ | 补录列表 | /api/v1/inspection-records/supplements | GET | 分页查询 | | 审核 | /api/v1/inspection-records/supplements/{id}/approve | POST | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用补录列表API GET /api/v1/inspection-records/supplements → 渲染补录数据表格,默认按补录时间倒序排列,每页20条 +2. **查询/筛选交互流程**:选择审核状态/输入补录人/选择补录原因/选择日期范围 → 点击[查询] → 携带筛选参数重新请求第1页 → 点击[重置]清空条件并重新加载 +3. **表单填写与提交流程**:本页无新增表单,补录数据由小程序端提交 +4. **弹窗/抽屉交互流程**:点击[审核](审核状态=待审核时显示)→ 弹出审核弹窗,选择"通过"或"驳回",驳回时必填驳回原因 → 确认后调用审核API → 成功后关闭弹窗并刷新列表;点击[查看] → 弹出补录详情弹窗,展示补录原因、详细说明、巡检记录信息等 +5. **行内操作流程**:点击[审核] → 弹窗选择通过/驳回 → 提交 → 刷新该行审核状态和审核人/审核时间;点击[查看] → 弹窗展示完整补录信息 +6. **异常与错误处理**:审核已审核过的记录时提示"该记录已审核";API请求失败显示ElMessage.error;列表无数据时显示ElEmpty"暂无补录记录" +7. **联动/级联交互**:审核状态筛选影响列表展示;补录原因枚举来源于系统配置(蓝牙故障/系统异常/定位失败/其他) +8. **权限控制交互表现**:无 inspection:supplement:approve 权限时[审核]按钮隐藏;无 inspection:supplement:view 权限时[查看]按钮隐藏 + +9. **【H1 防重复请求】**:查询按钮点击后立即 disabled + loading态,API返回后恢复;行内[审核]按钮点击后该行禁用 + loading态防止重复审核;分页切换 abort 上一请求再发新请求。 +10. **【H2 统一超时配置】**:GET 补录列表查询超时15秒;POST 审核操作超时30秒;超时自动中断 + "请求超时,请稍后重试" + 按钮恢复可用;>3秒显示全局 ElLoading。 +11. **【H3 操作确认机制】**:审核操作前弹出 `ElMessageBox.confirm("确定要「{通过/驳回}」该条补录记录吗?驳回后将通知补录人重新执行巡检", {type: 通过?'info':'warning'})`,含操作后果说明;驳回原因必填校验在确认前执行。 +12. **【H5 数据权限隔离】**:区分"暂无补录记录"(ElEmpty)与"无权限访问"(403);403时前端拦截器统一提示"您没有权限查看补录数据",不展示任何记录信息。 +13. **【H8 操作结果反馈】**:审核成功 `ElMessage.success("审核成功")`(2s)→ 关闭弹窗 → silent 刷新该行审核状态/审核人/审核时间;审核失败 `ElMessage.error`(手动关闭);网络异常提示含"重试"按钮可重新触发上次操作。 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 审核状态筛选 | el-select | clearable, placeholder="全部状态" | +| 补录人输入 | el-input | clearable, maxlength=20, placeholder="请输入补录人" | +| 补录原因筛选 | el-select | clearable, placeholder="全部原因" | +| 日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD", range-separator="至" | +| 列表 | el-table | stripe, border, :data="tableData" | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next", :page-sizes="[10,20,50]" | +| 审核状态标签 | el-tag | 待审核=warning / 已通过=success / 已驳回=danger | +| 审核按钮 | el-button | type="primary", link, v-if="row.auditStatus==='pending'" | +| 查看按钮 | el-button | type="primary", link | +| 审核弹窗 | el-dialog | title="审核补录", width="500px", :close-on-click-modal="false" | +| 审核结果 | el-radio-group | v-model="auditForm.result" | +| 驳回原因 | el-input | type="textarea", :rows="3", maxlength=200, show-word-limit, v-if="auditForm.result==='reject'" | +| 详情弹窗 | el-dialog | title="补录详情", width="600px" | +| 详情展示 | el-descriptions | :column="2", border | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 审核结果 | 必选 | "请选择审核结果" | +| 驳回原因 | 驳回时必填,maxlength=200 | "请填写驳回原因" | +| 日期范围 | 结束日期≥开始日期 | "结束日期不能早于开始日期" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区水平排列,表格完整展示全部列,弹窗宽度500px/600px | +| 1024-1279px(Pad横屏) | 查询条件区换行排列,表格隐藏"补录时间"列,弹窗宽度460px/560px | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,表格仅显示补录人、补录原因、审核状态、操作列,弹窗宽度90vw | + --- ## 需求追溯 diff --git a/docs/02-功能清单-物业公司/03-保洁管理.md b/docs/02-功能清单-物业公司/03-保洁管理.md index 72e67c4..32cf52c 100644 --- a/docs/02-功能清单-物业公司/03-保洁管理.md +++ b/docs/02-功能清单-物业公司/03-保洁管理.md @@ -3,6 +3,7 @@ > 模块编码:cleaning > 端侧:Web + 小程序(双端) > 关联文档:01-模块划分 §3.3 / 02-功能清单-物业公司 §3 / 03-业务流转逻辑-物业公司 §3 / 05-接口规范 §9.2 / 06-项目技术要求 §4.4 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -80,6 +81,72 @@ | 编辑 | /api/v1/cleaning-areas/{id} | PUT | — | | 删除 | /api/v1/cleaning-areas/{id} | DELETE | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用区域树接口 GET /api/v1/cleaning-areas/tree → 渲染左侧五级树形结构 → 默认选中第一个项目节点 → 右侧展示该节点详情信息 +2. **查询/筛选交互流程**:点击树形节点 → 右侧实时刷新展示选中区域的详情(区域路径、责任人、蓝牙Beacon、当前排班) +3. **表单填写与提交流程**:点击[新增区域] → 弹窗表单 → 填写区域名称、选择上级区域(级联)、选择责任人、选择蓝牙Beacon、选择保洁清单 → 点击确认 → 提交 POST /api/v1/cleaning-areas → 成功后刷新树形结构并选中新节点 +4. **弹窗/抽屉交互流程**:新增/编辑弹窗宽度520px,上级区域级联选择器懒加载展开,提交后自动关闭弹窗 +5. **行内操作流程**:点击[编辑] → 弹窗回填当前区域数据 → 修改后提交 PUT /api/v1/cleaning-areas/{id} → 刷新右侧详情;点击[删除] → 二次确认弹窗("删除后不可恢复,确认删除?") → 确认后调用 DELETE → 刷新树形 +6. **异常与错误处理**:区域名称重复提示"该区域名称已存在";存在排班关联时删除按钮置灰并Tooltip提示"该区域存在排班关联,不可删除";网络异常显示全局错误提示 +7. **联动/级联交互**:上级区域级联选择器联动下级选项;选择项目后区域选项刷新,选择区域后楼栋选项刷新,依次类推 +8. **权限控制交互表现**:无 cleaning:area:create 权限时[新增区域]按钮隐藏;无 cleaning:area:update 权限时[编辑]按钮隐藏;无 cleaning:area:delete 权限时[删除]按钮隐藏 +9. **【H1 防重复请求】**: + - 区域树加载:进入页面时调用 GET /api/v1/cleaning-areas/tree,loading 状态禁用树形节点点击;切换节点时右侧详情区展示 skeleton 加载占位 + - 新增/提交:弹窗确认按钮 `:loading` + `:disabled` 防重复提交 + - 行内操作:[编辑][删除]按钮操作中 disabled+loading 图标,防止连续点击 +10. **【H2 统一超时】**: + - GET(区域树)15s 超时;POST(新增)/PUT(编辑)/DELETE(删除)30s 超时 + - 超时后中断请求、提示"请求超时,请检查网络后重试"、按钮状态恢复 + - 响应时间 >3s 时自动显示全局 ElLoading 全屏遮罩提示"数据加载中..." +11. **【H3 操作确认(不可逆操作)】**: + - 删除操作必须调用 confirm("确定要删除区域「{区域名称}」?删除后将不可恢复且关联排班将被影响", { type: "error", confirmButtonText: "确认删除", cancelButtonText: "取消" }) + - 无排班关联时可执行删除,关联存在时按钮置灰并 Tooltip 提示 +12. **【H4 脏数据检测(弹窗编辑)】**: + - 编辑模式进入时对当前区域数据进行 deep clone 快照(JSON.parse(JSON.stringify(row))) + - 弹窗内通过 watch/deepWatch 监测 isDirty 状态变化 + - 关闭弹窗时若 isDirty 为 true,拦截并弹出 confirm("修改未保存,确定要关闭吗?") +13. **【H8 操作结果反馈】**: + - 新增成功:ElMessage.success("区域创建成功", duration=2000),2秒后 silent 刷新树形结构并选中新节点 + - 编辑成功:ElMessage.success("区域更新成功"),silent 刷新右侧详情 + - 删除成功:ElMessage.success("删除成功"),刷新树形结构 + - 失败:ElMessage.error(接口返回错误信息, duration=0) + - 网络异常:ElMessage.error("网络连接异常,请检查网络") + 显示重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 区域树 | el-tree | :data="areaTree" node-key="id" :props="{label:'name',children:'children'}" highlight-current :expand-on-click-node="false" @node-click="handleNodeClick" | +| 新增区域按钮 | el-button | type="primary" @click="showAddDialog" v-hasPermission="['cleaning:area:create']" | +| 编辑按钮 | el-button | type="primary" link @click="showEditDialog" v-hasPermission="['cleaning:area:update']" | +| 删除按钮 | el-button | type="danger" link @click="handleDelete" :disabled="hasScheduleBind" v-hasPermission="['cleaning:area:delete']" | +| 新增/编辑弹窗 | el-dialog | :title="dialogTitle" v-model="dialogVisible" width="520px" :close-on-click-modal="false" | +| 区域名称 | el-input | v-model="form.name" maxlength="30" show-word-limit placeholder="请输入区域名称" | +| 上级区域 | el-cascader | v-model="form.parentId" :options="areaOptions" :props="{checkStrictly:true,emitPath:false,value:'id',label:'name'}" placeholder="请选择上级区域" clearable | +| 区域责任人 | el-select | v-model="form.responsibleId" filterable placeholder="请选择责任人" | +| 蓝牙Beacon | el-select | v-model="form.beaconId" filterable placeholder="请选择蓝牙Beacon" | +| 保洁清单 | el-select | v-model="form.checklistIds" multiple collapse-tags collapse-tags-tooltip placeholder="请选择保洁清单" | +| 删除确认 | el-message-box | confirmButtonText="确认" cancelButtonText="取消" type="warning" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 区域名称 | 必填,长度1-30字符 | 请输入区域名称 / 区域名称不能超过30个字符 | +| 上级区域 | 必填 | 请选择上级区域 | +| 区域责任人 | 必填 | 请选择区域责任人 | +| 蓝牙Beacon | 必填 | 请选择蓝牙Beacon | +| 保洁清单 | 必填,至少选择1项 | 请至少选择一个保洁清单 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 左侧树形面板固定宽度280px,右侧详情区自适应剩余宽度,弹窗宽度520px | +| 1024-1279px(Pad横屏) | 左侧树形面板宽度缩减至220px,右侧详情区自适应,弹窗宽度480px | +| 768-1023px(Pad竖屏) | 树形面板折叠为顶部下拉选择器,详情区全宽展示,弹窗宽度90% | + --- ## 页面2:保洁任务看板页 @@ -130,6 +197,62 @@ |----------|---------|------|------| | 任务列表 | /api/v1/cleaning-tasks | GET | 分页查询+看板数据 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认查询条件为"今天+全部班组+全部状态" → 调用 GET /api/v1/cleaning-tasks → 渲染看板三列(待执行/执行中/已完成)及任务卡片 +2. **查询/筛选交互流程**:修改日期/班组/人员/状态筛选条件 → 点击[查询] → 重新请求接口刷新看板数据;点击[重置] → 恢复默认条件并刷新 +3. **表单填写与提交流程**:本页面无表单提交操作,仅查询展示 +4. **弹窗/抽屉交互流程**:点击任务卡片[查看详情] → 跳转至任务详情页(新页面路由) +5. **行内操作流程**:点击任务卡片 → 展开卡片详情浮层(区域、人员、时间、清单);点击[查看详情]按钮 → 路由跳转至详情页 +6. **异常与错误处理**:接口超时时显示"数据加载失败,请重试";无数据时看板列显示空状态el-empty;超时任务卡片边框红色高亮 +7. **联动/级联交互**:筛选条件中班组与人员联动,选择班组后人员下拉仅显示该班组人员 +8. **权限控制交互表现**:无 cleaning:task:view 权限时[查看详情]按钮隐藏,卡片仅展示基本信息 +9. **【H1 防重复请求】**: + - 查询按钮:点击后 `:loading` + `:disabled` 防止重复点击 + - 看板加载:数据请求期间看板三列展示 skeleton 占位卡片 + - 分页/翻页:切换日期或筛选条件时 abort 上一个未完成请求再发起新请求 +10. **【H2 统一超时】**: + - GET 任务列表 15s 超时 + - 超时后中断请求、提示"数据加载超时"、查询按钮恢复可用 + - 响应时间 >3s 时显示全局 ElLoading 提示"任务数据加载中..." +11. **【H8 操作结果反馈】**: + - 数据加载成功:正常渲染看板,无需额外提示 + - 加载失败:ElMessage.error("任务数据加载失败", duration=0) + - 空数据:各列展示 el-empty "暂无任务数据" + - 超时卡片边框红色高亮,配合 ElMessage.warning("存在超时任务,请及时处理") + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 看板容器 | 自定义看板组件 | 三列布局,每列header显示状态名+数量,列内任务卡片纵向排列 | +| 任务卡片 | el-card | shadow="hover" :body-style="{padding:'12px'}" @click="viewDetail" | +| 状态标签 | el-tag | :type="statusType" 待执行=info 执行中=warning 已完成=success 超时=danger | +| 日期筛选 | el-date-picker | v-model="query.date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" | +| 班组筛选 | el-select | v-model="query.teamId" clearable placeholder="全部班组" | +| 人员筛选 | el-input | v-model="query.staffName" clearable placeholder="输入人员姓名" | +| 状态筛选 | el-select | v-model="query.status" clearable placeholder="全部状态" | +| 查询按钮 | el-button | type="primary" @click="handleQuery" | +| 重置按钮 | el-button | @click="handleReset" | +| 查看详情按钮 | el-button | type="primary" link @click="viewDetail" v-hasPermission="['cleaning:task:view']" | +| 空状态 | el-empty | description="暂无任务数据" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 日期 | 非必填,格式YYYY-MM-DD | 日期格式不正确 | +| 人员 | 非必填,最大20字符 | — | +| 状态 | 非必填,枚举值:pending/ongoing/completed/timeout | — | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 看板三列横向排列,每列宽度均分,查询条件区水平排列 | +| 1024-1279px(Pad横屏) | 看板三列横向排列,卡片内容字号略减,查询条件区水平排列 | +| 768-1023px(Pad竖屏) | 看板改为纵向堆叠,每列占满宽度;查询条件区换行排列,每行2个条件 | + --- ## 页面3:人员排班页 @@ -182,6 +305,81 @@ | 复制排班 | /api/v1/cleaning-schedules/copy | POST | 从指定周复制 | | 导出 | /api/v1/cleaning-schedules/export | GET | 导出Excel | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认显示本周周视图 → 调用排班查询API GET /api/v1/cleaning-schedules → 渲染周视图排班表格(区域×日期矩阵),同时加载班组下拉选项 +2. **查询/筛选交互流程**:切换周视图/月视图 → 重新请求对应维度数据;点击左右箭头切换周/月 → 重新加载;选择班组 → 过滤显示该班组的排班数据 +3. **表单填写与提交流程**:点击排班单元格 → 弹出编辑弹窗 → 选择保洁人员、班次、时间范围 → 确认后暂存到前端;点击[保存排班] → 批量提交所有变更 → 成功后刷新排班视图;点击[复制上周排班] → 调用复制API → 成功后刷新本周视图 +4. **弹窗/抽屉交互流程**:排班编辑弹窗宽度420px,点击单元格打开;点击[清空本周] → 二次确认"确认清空本周所有排班数据?" → 确认后调用清空API +5. **行内操作流程**:点击排班单元格 → 弹出编辑弹窗 → 修改后暂存;已有排班数据的单元格显示人员和班次信息;空白单元格可点击新增 +6. **异常与错误处理**:同一人员同一时段排班冲突时提示"该人员此时段已有排班";保存失败显示ElMessage.error;复制上周无数据时提示"上周无排班数据" +7. **联动/级联交互**:班次选择后自动填充时间范围(早班06:00-14:00/晚班14:00-22:00/全天08:00-17:00);周视图/月视图切换联动数据刷新 +8. **权限控制交互表现**:无 cleaning:schedule:create 权限时[复制上周排班]按钮隐藏;无 cleaning:schedule:update 权限时排班单元格不可点击编辑;无 cleaning:schedule:delete 权限时[清空本周]按钮隐藏 + +9. **[H1]防重复请求** + - [保存排班]按钮点击后::loading=true + 文案"保存中..." + disabled + 排班表格区域半透明遮罩;API返回后恢复 + - [复制上周排班]按钮点击后立即 disabled + loading态,API返回后恢复 + - [清空本周]按钮点击后 disabled + loading态(确认弹窗通过后执行) + - 单元格编辑弹窗保存后:弹窗内保存按钮 :loading + 弹窗关闭期间父页面不可操作 + - 周视图/月视图切换、左右箭头切换:abort上一请求再发新请求 + +10. **[H2]超时与加载反馈** + - 排班数据查询(GET列表/周/月视图):timeout=15秒 + - 保存排班/复制上周/清空本周(POST):timeout=30秒 + - 超时 → ElMessage.error("请求超时,请检查网络后重试") + 按钮恢复 + - 加载>2秒显示全局ElLoading + +11. **[H3]操作确认机制** + - 清空本周:ElMessageBox.confirm("确认清空本周所有排班数据?清开后数据无法恢复", "清空确认", { type: 'warning', confirmButtonText: '确认清空', cancelButtonText: '取消' }) + - 复制上周:ElMessageBox.confirm("确定复制上周排班数据到本周?将覆盖本周已有排班", { type: 'info' }) + +12. **[H4]脏数据检测** + - 用户编辑任一单元格后排班表标记 isDirty=true + - 切换周/月视图时:若 isDirty → ElMessageBox.confirm("当前有未保存的排班变更,切换视图将丢失未保存内容,是否继续?") + - 离开页面时:beforeRouteLeave 导航守卫拦截,isDirty 则弹出确认提示 + - 保存成功后将当前状态设为新快照,重置 isDirty=false + +13. **[H8]操作结果反馈** + - 保存成功:ElMessage.success("排班保存成功", duration=2000) + silent刷新排班视图 + - 复制成功:ElMessage.success("已复制上周排班到本周") + 刷新 + - 清空成功:ElMessage.success("已清空本周排班") + 刷新 + - 冲突提示:ElMessage.warning("该人员此时段已有排班") + 标红冲突单元格 + - 失败:ElMessage.error(错误信息, duration=0) + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 视图切换 | el-radio-group + el-radio-button | v-model="viewMode", size="default" | +| 周导航 | 自定义组件 | 左右箭头+周显示,@prev/@next | +| 班组筛选 | el-select | clearable, filterable, placeholder="全部班组" | +| 周视图表格 | 自定义排班表格 | 行=区域,列=周一~周日,单元格可点击 | +| 排班编辑弹窗 | el-dialog | title="编辑排班", width="420px", :close-on-click-modal="false" | +| 保洁人员 | el-select | v-model="form.staffId", filterable, placeholder="请选择保洁人员" | +| 班次 | el-select | v-model="form.shift", placeholder="请选择班次", @change="fillTimeRange" | +| 时间范围 | el-time-picker | is-range, format="HH:mm", range-separator="至" | +| 保存排班按钮 | el-button | type="primary", :loading="saving" | +| 清空本周按钮 | el-button | type="danger" | +| 复制上周按钮 | el-button | type="success" | +| 导出按钮 | el-button | type="success", icon="Download" | +| 二次确认 | el-message-box | type="warning", confirmButtonText="确认", cancelButtonText="取消" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 保洁人员 | 必选 | "请选择保洁人员" | +| 班次 | 必选 | "请选择班次" | +| 时间范围 | 必选,结束时间>开始时间 | "请选择时间范围" / "结束时间必须晚于开始时间" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 周视图完整7列展示,排班编辑弹窗420px | +| 1024-1279px(Pad横屏) | 周视图列宽缩窄,单元格字号略减,弹窗宽度380px | +| 768-1023px(Pad竖屏) | 周视图改为3天一组展示(今天+后2天),左右滑动切换日期范围;弹窗宽度90vw | + --- ## 页面4:蓝牙点位管理页 @@ -232,6 +430,64 @@ | 绑定 | /api/v1/cleaning-beacon-points/{id}/bind | PUT | 绑定区域 | | 解绑 | /api/v1/cleaning-beacon-points/{id}/unbind | PUT | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用列表查询API GET /api/v1/cleaning-beacon-points → 渲染Beacon列表表格,同时加载区域级联数据;默认按Beacon名称排列 +2. **查询/筛选交互流程**:选择区域/选择Beacon状态/选择电量状态 → 筛选条件变更后自动触发查询(无需手动点击查询按钮);区域级联选择逐级展开 +3. **表单填写与提交流程**:点击[编辑绑定] → 弹窗选择要绑定的区域 → 确认后调用绑定API → 成功后刷新列表;点击[解绑] → 二次确认"确认解绑该Beacon与区域的绑定关系?" → 确认后调用解绑API +4. **弹窗/抽屉交互流程**:编辑绑定弹窗宽度480px,展示当前Beacon信息+区域级联选择器;点击遮罩层可关闭 +5. **行内操作流程**:点击[编辑绑定] → 弹窗选择新区域 → 提交 → 刷新行数据;点击[解绑](已绑定时显示)→ 二次确认 → 解绑 → 刷新行数据 +6. **异常与错误处理**:解绑未绑定的Beacon时提示"该Beacon未绑定区域";API请求失败显示ElMessage.error;Beacon离线状态实时刷新(每30秒轮询心跳数据) +7. **联动/级联交互**:区域筛选级联选择——选择项目→加载区域→选择区域→加载楼栋;Beacon状态根据心跳数据实时更新;电量<20%显示红色低电量标签 +8. **权限控制交互表现**:无 cleaning:area:update 权限时[编辑绑定][解绑]按钮隐藏 +9. **[H1]防重复请求** + - 行内操作点击后该行禁用 + loading态 + - 筛选条件变更自动触发查询时 abort 上一个未完成请求再发新请求 +10. **[H2]超时与加载反馈** + - GET列表查询 timeout=15秒;POST/PUT写操作 timeout=30秒 + - 超时 → 提示"请求超时,请检查网络后重试" + 按钮恢复 + - 加载>2秒显示全局loading +11. **[H3]操作确认机制**(有不可逆操作时) + - 解绑: ElMessageBox.confirm("确认解绑该Beacon与区域的绑定关系?", { type: 'warning' }) +12. **[H4]脏数据检测** + - 编辑模式进入时deep clone快照 + - isDirty检测 + 取消/离开拦截 +13. **[H8]操作结果反馈** + - 成功: success(2s) + silent刷新 + - 失败: error(0手动关闭) + - 网络: 异常提示+重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 区域筛选 | el-cascader | :props="{checkStrictly:true, emitPath:false}", clearable, filterable, placeholder="选择区域" | +| Beacon状态筛选 | el-select | clearable, placeholder="全部状态" | +| 电量筛选 | el-select | clearable, placeholder="全部" | +| 列表 | el-table | stripe, border, :data="tableData" | +| Beacon状态标签 | el-tag | 在线=success / 离线=danger | +| 电量显示 | el-progress | :percentage="row.battery", :color="batteryColor", :stroke-width="8", :text-inside="true" | +| 绑定区域 | el-link | type="primary", @click="viewArea" | +| 编辑绑定按钮 | el-button | type="primary", link | +| 解绑按钮 | el-button | type="danger", link, v-if="row.bindAreaId" | +| 编辑绑定弹窗 | el-dialog | title="编辑Beacon绑定", width="480px", :close-on-click-modal="true" | +| 区域选择 | el-cascader | :props="{checkStrictly:true, emitPath:false}", filterable, placeholder="请选择绑定区域" | +| 解绑确认 | el-message-box | type="warning", confirmButtonText="确认解绑", cancelButtonText="取消" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 绑定区域 | 编辑绑定时必选 | "请选择绑定区域" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 筛选条件区水平排列,表格完整展示全部7列 | +| 1024-1279px(Pad横屏) | 筛选条件区换行排列,表格隐藏"UUID"列 | +| 768-1023px(Pad竖屏) | 筛选条件区垂直堆叠,表格仅显示Beacon名称、绑定区域、状态、操作列 | + --- ## 页面5:超时预警页 @@ -263,6 +519,55 @@ |----------|---------|------|------| | 超时列表 | /api/v1/cleaning-tasks/timeouts | GET | 仅显示超时任务 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用超时列表API GET /api/v1/cleaning-tasks/timeouts → 渲染超时任务表格,默认按超时时长降序排列 +2. **查询/筛选交互流程**:本页无独立筛选条件,仅展示超时任务列表;数据由后端自动筛选超时任务 +3. **表单填写与提交流程**:点击[催办] → 调用催办API → 发送小程序推送通知对应保洁人员 → 成功后刷新该行状态;点击[查看] → 跳转任务详情页 +4. **弹窗/抽屉交互流程**:催办操作弹出确认弹窗"将向保洁人员发送催办通知,确认?" → 确认后发送催办 +5. **行内操作流程**:点击[催办] → 二次确认 → 发送催办通知 → 刷新行数据;点击[查看] → 跳转任务详情页 +6. **异常与错误处理**:催办失败提示"催办通知发送失败,请重试";列表无数据时显示ElEmpty"暂无超时任务,太棒了!" +7. **联动/级联交互**:超时时长列红色标记,时长越长颜色越深;催办后更新催办状态 +8. **权限控制交互表现**:本页仅物业管理员和主管可见;主管仅看本班组超时数据 +9. **[H1]防重复请求** + - 催办按钮点击后 disabled + loading态,API返回后恢复 +10. **[H2]超时与加载反馈** + - GET列表查询 timeout=15秒;POST催办 timeout=30秒 + - 超时 → 提示"请求超时,请检查网络后重试" + 按钮恢复 + - 加载>2秒显示全局loading +11. **[H3]操作确认机制**(有不可逆操作时) + - 催办: ElMessageBox.confirm("将向保洁人员发送催办通知,确认?", { type: 'warning' }) +12. **[H8]操作结果反馈** + - 成功: success(2s) + silent刷新 + - 失败: error(0手动关闭) + - 网络: 异常提示+重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 列表 | el-table | stripe, border, :data="timeoutList", :row-class-name="timeoutRowClass" | +| 超时时长 | el-tag | type="danger", 超时越长字体越大 | +| 当前状态标签 | el-tag | 超时未开始=danger / 超时未完成=danger, effect="dark" | +| 催办按钮 | el-button | type="warning", link, icon="Bell" | +| 查看按钮 | el-button | type="primary", link | +| 催办确认 | el-message-box | type="warning", confirmButtonText="确认催办", cancelButtonText="取消" | +| 空状态 | el-empty | description="暂无超时任务,太棒了!" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| — | — | — | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 表格完整展示全部6列 | +| 1024-1279px(Pad横屏) | 表格隐藏"计划完成时间"列 | +| 768-1023px(Pad竖屏) | 表格仅显示任务区域、保洁人员、超时时长、操作列 | + --- ## 页面6:保洁抽查页 @@ -305,6 +610,66 @@ | 抽查列表 | /api/v1/cleaning-spot-checks | GET | — | | 标记抽查 | /api/v1/cleaning-spot-checks | POST | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用抽查列表API GET /api/v1/cleaning-spot-checks → 渲染抽查记录表格,默认按抽查时间倒序排列 +2. **查询/筛选交互流程**:选择抽查结果/输入抽查人/选择日期范围 → 点击[查询] → 重新加载数据 → 点击[重置]清空条件并重新加载 +3. **表单填写与提交流程**:点击[标记抽查] → 弹窗选择合格/不合格,不合格时必填原因 → 确认后调用标记抽查API → 成功后刷新列表 +4. **弹窗/抽屉交互流程**:标记抽查弹窗宽度480px,选择结果后不合格显示原因输入框;点击遮罩层不关闭 +5. **行内操作流程**:不合格记录点击[重新生成] → 二次确认"将为该区域重新生成保洁任务,确认?" → 确认后调用重新生成API → 刷新行数据 +6. **异常与错误处理**:重复标记提示"该任务已抽查过";重新生成失败提示ElMessage.error;列表无数据时显示ElEmpty +7. **联动/级联交互**:选择"不合格"时自动显示原因输入框;不合格记录自动触发重新生成按钮 +8. **权限控制交互表现**:无 cleaning:spot-check:approve 权限时[标记抽查][重新生成]按钮隐藏 +9. **[H1]防重复请求** + - 查询按钮点击后 disabled + loading态,API返回后恢复 + - 行内操作点击后该行禁用 + loading态 + - 分页切换 abort上一请求再发新请求 +10. **[H2]超时与加载反馈** + - GET列表查询 timeout=15秒;POST/PUT写操作 timeout=30秒 + - 超时 → 提示"请求超时,请检查网络后重试" + 按钮恢复 + - 加载>2秒显示全局loading +11. **[H3]操作确认机制**(有不可逆操作时) + - 重新生成: ElMessageBox.confirm("将为该区域重新生成保洁任务,确认?", { type: 'warning' }) +12. **[H4]脏数据检测** + - 编辑模式进入时deep clone快照 + - isDirty检测 + 取消/离开拦截 +13. **[H8]操作结果反馈** + - 成功: success(2s) + silent刷新 + - 失败: error(0手动关闭) + - 网络: 异常提示+重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 抽查结果筛选 | el-select | clearable, placeholder="全部结果" | +| 抽查人输入 | el-input | clearable, maxlength=20, placeholder="请输入抽查人" | +| 日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD", range-separator="至" | +| 列表 | el-table | stripe, border, :data="tableData" | +| 抽查结果标签 | el-tag | 合格=success / 不合格=danger | +| 标记抽查按钮 | el-button | type="primary", icon="Check" | +| 重新生成按钮 | el-button | type="warning", link, v-if="row.result==='unqualified'" | +| 标记抽查弹窗 | el-dialog | title="标记抽查", width="480px", :close-on-click-modal="false" | +| 抽查结果 | el-radio-group | v-model="checkForm.result" | +| 不合格原因 | el-input | type="textarea", :rows="3", maxlength=200, show-word-limit, v-if="checkForm.result==='unqualified'" | +| 二次确认 | el-message-box | type="warning" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 抽查结果 | 必选 | "请选择抽查结果" | +| 不合格原因 | 不合格时必填,maxlength=200 | "请填写不合格原因" | +| 日期范围 | 结束日期≥开始日期 | "结束日期不能早于开始日期" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区水平排列,表格完整展示全部7列 | +| 1024-1279px(Pad横屏) | 查询条件区换行排列,表格隐藏"不合格原因"列 | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,表格仅显示任务区域、保洁人员、抽查结果、操作列 | + --- ## 页面7:数据补录与补录审核页 @@ -328,6 +693,68 @@ |------|----------|------|----------|------| | 审核 | cleaning:supplement:approve | 行操作 | 审核状态=待审核 | 通过/驳回 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用补录列表API → 渲染补录数据表格,默认按补录时间倒序排列,每页20条;同时加载审核状态下拉选项 +2. **查询/筛选交互流程**:选择审核状态/输入补录人/选择日期范围 → 点击[查询] → 重新加载数据 → 点击[重置]清空条件 +3. **表单填写与提交流程**:本页无新增表单,补录数据由小程序端提交 +4. **弹窗/抽屉交互流程**:点击[审核](审核状态=待审核时显示)→ 弹出审核弹窗,选择"通过"或"驳回",驳回时必填驳回原因 → 确认后调用审核API → 成功后关闭弹窗并刷新列表 +5. **行内操作流程**:点击[审核] → 弹窗选择通过/驳回 → 提交 → 刷新该行审核状态和审核人/审核时间 +6. **异常与错误处理**:审核已审核过的记录时提示"该记录已审核";API请求失败显示ElMessage.error;列表无数据时显示ElEmpty +7. **联动/级联交互**:审核状态筛选影响列表展示;补录数据标记规范中is_supplement=true的数据在此页面展示 +8. **权限控制交互表现**:无 cleaning:supplement:approve 权限时[审核]按钮隐藏 +9. **【H1 防重复请求】**: + - 查询按钮 `:loading` + `:disabled` 防重复点击 + - 分页切换时 abort 上一个未完成请求再发新请求 + - [审核]按钮及弹窗确认按钮操作中 disabled+loading +10. **【H2 统一超时】**: + - GET(补录列表)15s 超时;PUT(审核)30s 超时 + - 超时后中断请求、提示"请求超时" + - 响应时间 >3s 时全局 ElLoading +11. **【H3 操作确认(不可逆操作)**: + - 审核通过:confirm("确认通过该补录数据?通过后数据将纳入正常统计", { type: "info", confirmButtonText: "确认通过", cancelButtonText: "取消" }) + - 审核驳回:confirm("确认驳回该补录数据?驳回后需补录人重新提交", { type: "warning", confirmButtonText: "确认驳回", cancelButtonText: "取消",含后果说明 }) +12. **【H4 脏数据检测(审核弹窗)】**: + - 审核弹窗打开时初始化空审核表单快照 + - 选择通过/驳回或填写驳回原因后检测 isDirty + - 关闭弹窗若 isDirty 为 true 拦截并 confirm("审核结果未提交,确定要关闭?") +13. **【H8 操作结果反馈】**: + - 审核成功:ElMessage.success(审核结果 === 'pass' ? "审核通过" : "已驳回"),silent 刷新列表行状态 + - 重复审核:ElMessage.warning("该记录已审核") + - 操作失败:ElMessage.error(错误信息, duration=0) + - 网络异常:ElMessage.error("网络连接异常") + 重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 审核状态筛选 | el-select | clearable, placeholder="全部状态" | +| 补录人输入 | el-input | clearable, maxlength=20, placeholder="请输入补录人" | +| 日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD", range-separator="至" | +| 列表 | el-table | stripe, border, :data="tableData" | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next", :page-sizes="[10,20,50]" | +| 审核状态标签 | el-tag | 待审核=warning / 已通过=success / 已驳回=danger | +| 审核按钮 | el-button | type="primary", link, v-if="row.auditStatus==='pending'" | +| 审核弹窗 | el-dialog | title="审核补录", width="500px", :close-on-click-modal="false" | +| 审核结果 | el-radio-group | v-model="auditForm.result" | +| 驳回原因 | el-input | type="textarea", :rows="3", maxlength=200, show-word-limit, v-if="auditForm.result==='reject'" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 审核结果 | 必选 | "请选择审核结果" | +| 驳回原因 | 驳回时必填,maxlength=200 | "请填写驳回原因" | +| 日期范围 | 结束日期≥开始日期 | "结束日期不能早于开始日期" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区水平排列,表格完整展示,弹窗宽度500px | +| 1024-1279px(Pad横屏) | 查询条件区换行排列,表格隐藏次要列,弹窗宽度460px | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,表格仅显示关键列,弹窗宽度90vw | + --- ## 需求追溯 diff --git a/docs/02-功能清单-物业公司/04-组织架构.md b/docs/02-功能清单-物业公司/04-组织架构.md index 1d3bbe5..306f500 100644 --- a/docs/02-功能清单-物业公司/04-组织架构.md +++ b/docs/02-功能清单-物业公司/04-组织架构.md @@ -3,6 +3,7 @@ > 模块编码:org > 端侧:Web + 小程序(双端) > 关联文档:01-模块划分 §3.4 / 02-功能清单-物业公司 §4 / 03-业务流转逻辑-物业公司 §4 / 05-接口规范 §9.2 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -68,6 +69,70 @@ | 新增 | /api/v1/teams | POST | — | | 编辑 | /api/v1/teams/{id} | PUT | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 并行调用列表查询API GET /api/v1/teams 和下拉选项数据(班组类型字典)→ 渲染表格和筛选条件,默认加载全部班组数据 +2. **查询/筛选交互流程**:输入班组名称/选择班组类型/选择状态 → 点击[查询] → 携带筛选参数重新请求 → 点击[重置]清空条件并重新加载;班组名称支持回车触发查询 +3. **表单填写与提交流程**:点击[新增班组] → 弹窗表单 → 填写班组名称、班组类型、班组长、负责区域、描述 → 前端校验通过 → 调用新增API → 成功后关闭弹窗并刷新列表 +4. **弹窗/抽屉交互流程**:新增/编辑弹窗宽度520px;选择班组长时从人员列表中选择(支持搜索);负责区域使用级联多选;点击遮罩层不关闭 +5. **行内操作流程**:点击[编辑] → 弹窗回填当前班组数据 → 修改后提交 → 刷新列表;点击[停用] → 二次确认"停用后该班组下人员将无法接收新任务,确认停用?" → 有未完成任务时按钮置灰并提示"该班组存在未完成任务,不可停用" +6. **异常与错误处理**:班组名称重复提示"该班组名称已存在";停用有未完成任务的班组时提示"该班组存在未完成任务,不可停用";API请求失败显示ElMessage.error +7. **联动/级联交互**:选择班组类型后班组长下拉可按类型过滤;负责区域级联选择逐级展开 +8. **权限控制交互表现**:无 org:team:create 权限时[新增班组]按钮隐藏;无 org:team:update 权限时[编辑][停用]按钮隐藏 +9. **[H1]防重复请求** + - 查询按钮点击后 disabled + loading态,API返回后恢复 + - 行内操作点击后该行禁用 + loading态 + - 分页切换 abort上一请求再发新请求 +10. **[H2]超时与加载反馈** + - GET列表查询 timeout=15秒;POST/PUT/DELETE写操作 timeout=30秒 + - 超时 → 提示"请求超时,请检查网络后重试" + 按钮恢复 + - 加载>2秒显示全局loading +11. **[H3]操作确认机制**(有不可逆操作时) + - 删除/停用: ElMessageBox.confirm/Web端或wx.showModal/小程序端 +12. **[H4]脏数据检测** + - 编辑模式进入时deep clone快照 + - isDirty检测 + 取消/离开拦截 +13. **[H8]操作结果反馈** + - 成功: success(2s) + silent刷新 + - 失败: error(0手动关闭) + - 网络: 异常提示+重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 班组名称输入 | el-input | placeholder="请输入班组名称", clearable, maxlength=30 | +| 班组类型筛选 | el-select | clearable, placeholder="全部类型" | +| 状态筛选 | el-select | clearable, placeholder="全部状态" | +| 列表 | el-table | stripe, border, :data="tableData" | +| 新增班组按钮 | el-button | type="primary", icon="Plus" | +| 编辑按钮 | el-button | type="primary", link | +| 停用按钮 | el-button | type="warning", link, :disabled="hasPendingTasks" | +| 状态标签 | el-tag | 启用=success / 停用=info | +| 新增/编辑弹窗 | el-dialog | :title="dialogTitle", width="520px", :close-on-click-modal="false" | +| 班组名称 | el-input | maxlength=30, show-word-limit, placeholder="请输入班组名称" | +| 班组类型 | el-select | placeholder="请选择班组类型", filterable | +| 班组长 | el-select | placeholder="请选择班组长", filterable | +| 负责区域 | el-cascader | :props="{multiple:true, checkStrictly:true, emitPath:false}", filterable, clearable | +| 描述 | el-input | type="textarea", :rows="3", maxlength=200, show-word-limit | +| 二次确认 | el-message-box | type="warning" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 班组名称 | 必填,maxlength=30,同租户唯一 | "请输入班组名称" / "该班组名称已存在" | +| 班组类型 | 必选 | "请选择班组类型" | +| 班组长 | 必选 | "请选择班组长" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区水平排列,表格完整展示全部列,弹窗宽度520px | +| 1024-1279px(Pad横屏) | 查询条件区换行排列,表格隐藏"负责区域"列,弹窗宽度480px | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,表格仅显示班组名称、类型、班组长、状态、操作列,弹窗宽度90vw | + --- ## 页面2:人员管理页 @@ -123,6 +188,70 @@ | 新增 | /api/v1/staffs | POST | — | | 编辑 | /api/v1/staffs/{id} | PUT | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 并行调用人员列表API和下拉选项(班组列表、技能标签列表)→ 渲染表格和筛选条件 +2. **查询/筛选交互流程**:输入姓名/选择所属班组/选择技能标签/选择状态 → 点击[查询] → 重新加载数据 → 点击[重置]清空条件 +3. **表单填写与提交流程**:点击[新增人员] → 弹窗表单 → 填写姓名、手机号、所属班组(多选)、技能标签、入职日期 → 前端校验通过 → 调用新增API → 成功后关闭弹窗并刷新列表 +4. **弹窗/抽屉交互流程**:新增/编辑弹窗宽度520px;所属班组支持多选(一人多班组),使用collapse-tags折叠展示;点击遮罩层不关闭 +5. **行内操作流程**:点击[编辑] → 弹窗回填当前人员数据 → 修改后提交 → 刷新列表;点击[分配] → 弹窗分配到班组/区域;点击[查看排班] → 跳转排班管理页(带该人员筛选条件) +6. **异常与错误处理**:手机号已存在提示"该手机号已被注册";API请求失败显示ElMessage.error;技能标签无数据时显示"暂无技能标签" +7. **联动/级联交互**:所属班组多选后技能标签可根据班组关联过滤;手机号脱敏显示(138****1234) +8. **权限控制交互表现**:无 org:staff:create 权限时[新增人员]按钮隐藏;无 org:staff:update 权限时[编辑][分配]按钮隐藏 +9. **[H1]防重复请求** + - 查询按钮点击后 disabled + loading态,API返回后恢复 + - 行内操作点击后该行禁用 + loading态 + - 分页切换 abort上一请求再发新请求 +10. **[H2]超时与加载反馈** + - GET列表查询 timeout=15秒;POST/PUT/DELETE写操作 timeout=30秒 + - 超时 → 提示"请求超时,请检查网络后重试" + 按钮恢复 + - 加载>2秒显示全局loading +11. **[H4]脏数据检测** + - 编辑模式进入时deep clone快照 + - isDirty检测 + 取消/离开拦截 +12. **[H8]操作结果反馈** + - 成功: success(2s) + silent刷新 + - 失败: error(0手动关闭) + - 网络: 异常提示+重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 姓名输入 | el-input | clearable, maxlength=20, placeholder="请输入姓名" | +| 所属班组筛选 | el-select | clearable, filterable, placeholder="全部班组" | +| 技能标签筛选 | el-select | clearable, multiple, collapse-tags, placeholder="全部标签" | +| 状态筛选 | el-select | clearable, placeholder="全部状态" | +| 列表 | el-table | stripe, border, :data="tableData" | +| 新增人员按钮 | el-button | type="primary", icon="Plus" | +| 编辑按钮 | el-button | type="primary", link | +| 分配按钮 | el-button | type="success", link | +| 查看排班按钮 | el-button | type="primary", link | +| 技能标签列 | el-tag | v-for, type="primary", size="small", style="margin:2px" | +| 状态标签 | el-tag | 在职=success / 离职=info | +| 新增/编辑弹窗 | el-dialog | :title="dialogTitle", width="520px", :close-on-click-modal="false" | +| 姓名 | el-input | maxlength=20, placeholder="请输入姓名" | +| 手机号 | el-input | maxlength=11, placeholder="请输入手机号" | +| 所属班组 | el-select | v-model="form.teamIds", multiple, collapse-tags, filterable, placeholder="请选择所属班组" | +| 技能标签 | el-select | v-model="form.skillIds", multiple, collapse-tags, placeholder="请选择技能标签" | +| 入职日期 | el-date-picker | type="date", value-format="YYYY-MM-DD" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 姓名 | 必填,maxlength=20 | "请输入姓名" | +| 手机号 | 必填,正则/^1[3-9]\d{9}$/ | "请输入正确的手机号码" | +| 所属班组 | 必选,至少1个 | "请选择所属班组" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区水平排列,表格完整展示全部7列,弹窗宽度520px | +| 1024-1279px(Pad横屏) | 查询条件区换行排列,表格隐藏"技能标签"列,弹窗宽度480px | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,表格仅显示姓名、班组、状态、操作列,弹窗宽度90vw | + --- ## 页面3:排班管理页 @@ -164,6 +293,66 @@ | 排班查询 | /api/v1/team-schedules | GET | — | | 保存 | /api/v1/team-schedules/batch | POST | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认显示本周周视图 → 调用排班查询API GET /api/v1/team-schedules → 渲染排班表格(人员×日期矩阵),同时加载班组下拉选项 +2. **查询/筛选交互流程**:切换周视图/月视图 → 重新请求对应数据;点击左右箭头切换周 → 重新加载;选择班组 → 过滤显示该班组人员排班 +3. **表单填写与提交流程**:点击排班单元格 → 弹出编辑弹窗选择班次 → 暂存前端;点击[保存] → 批量提交所有变更 → 成功后刷新排班视图;点击[批量排班] → 选择多人+统一班次 → 批量填充 → 保存;点击[模板排班] → 从已有模板加载 +4. **弹窗/抽屉交互流程**:排班编辑弹窗宽度400px,选择班次;批量排班弹窗宽度500px,选择人员和班次;模板排班弹窗宽度480px,选择模板 +5. **行内操作流程**:点击排班单元格 → 弹出班次选择 → 确认后暂存;点击已有班次可修改或清除 +6. **异常与错误处理**:同一人员同一时段排班冲突时提示"排班冲突";保存失败显示ElMessage.error;模板无数据时提示"暂无排班模板" +7. **联动/级联交互**:批量排班选择班次后自动填充时间范围;班组筛选联动人员列表刷新 +8. **权限控制交互表现**:无 org:team:create 权限时[批量排班][模板排班]按钮隐藏;无 org:team:update 权限时排班单元格不可编辑;无 org:team:export 权限时[导出]按钮隐藏 +9. **[H1]防重复请求** + - 查询/视图切换点击后 disabled + loading态,API返回后恢复 + - 保存操作点击后 disabled + loading态 + - 分页切换 abort上一请求再发新请求 +10. **[H2]超时与加载反馈** + - GET列表查询 timeout=15秒;POST/PUT/DELETE写操作 timeout=30秒 + - 超时 → 提示"请求超时,请检查网络后重试" + 按钮恢复 + - 加载>2秒显示全局loading +11. **[H8]操作结果反馈** + - 成功: success(2s) + silent刷新 + - 失败: error(0手动关闭) + - 网络: 异常提示+重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 视图切换 | el-radio-group + el-radio-button | v-model="viewMode" | +| 周导航 | 自定义组件 | 左右箭头+周显示 | +| 班组筛选 | el-select | clearable, filterable, placeholder="全部班组" | +| 排班表格 | 自定义排班表格 | 行=人员,列=周一~周日,单元格可点击 | +| 排班编辑弹窗 | el-dialog | title="编辑排班", width="400px", :close-on-click-modal="false" | +| 班次选择 | el-select | placeholder="请选择班次" | +| 批量排班弹窗 | el-dialog | title="批量排班", width="500px", :close-on-click-modal="false" | +| 人员多选 | el-select | v-model="batchForm.staffIds", multiple, collapse-tags, filterable | +| 批量班次 | el-select | v-model="batchForm.shift" | +| 模板排班弹窗 | el-dialog | title="选择排班模板", width="480px" | +| 模板列表 | el-select | placeholder="请选择模板" | +| 保存按钮 | el-button | type="primary", :loading="saving" | +| 批量排班按钮 | el-button | type="success" | +| 模板排班按钮 | el-button | type="warning" | +| 导出按钮 | el-button | type="success", icon="Download" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 班次 | 必选 | "请选择班次" | +| 批量排班-人员 | 至少选1人 | "请选择人员" | +| 批量排班-班次 | 必选 | "请选择班次" | +| 模板排班-模板 | 必选 | "请选择排班模板" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 排班表格完整7列展示,操作按钮水平排列 | +| 1024-1279px(Pad横屏) | 排班表格列宽缩窄,单元格字号略减 | +| 768-1023px(Pad竖屏) | 排班表格改为3天一组展示,左右滑动切换;操作按钮折叠为下拉菜单 | + --- ## 页面4:技能管理页 @@ -189,6 +378,59 @@ | 关联班组 | 下拉多选 | 否 | — | 班组列表 | — | | 描述 | 多行文本 | 否 | — | 自填 | 最大200字 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用技能列表API → 渲染技能表格,默认按持有人员数降序排列 +2. **查询/筛选交互流程**:本页无独立筛选条件,所有技能数据一次加载 +3. **表单填写与提交流程**:点击[新增技能](如有操作栏按钮)→ 弹窗表单 → 填写技能名称、关联班组、描述 → 前端校验通过 → 调用新增API → 成功后关闭弹窗并刷新列表 +4. **弹窗/抽屉交互流程**:新增/编辑弹窗宽度480px;点击遮罩层不关闭 +5. **行内操作流程**:点击[编辑] → 弹窗回填技能数据 → 修改后提交 → 刷新列表;点击[删除] → 二次确认"删除后持有该技能的人员将失去此标签,确认删除?" → 确认后调用删除API → 刷新列表 +6. **异常与错误处理**:技能名称重复提示"该技能名称已存在";删除被引用的技能时提示"该技能正在使用中,不可删除";API请求失败显示ElMessage.error +7. **联动/级联交互**:关联班组多选后自动关联到该班组人员;持有人员数为实时统计值 +8. **权限控制交互表现**:无新增/编辑/删除权限时对应按钮隐藏 +9. **[H1]防重复请求** + - 行内操作点击后该行禁用 + loading态 +10. **[H2]超时与加载反馈** + - GET列表查询 timeout=15秒;POST/PUT/DELETE写操作 timeout=30秒 + - 超时 → 提示"请求超时,请检查网络后重试" + 按钮恢复 +11. **[H3]操作确认机制**(有不可逆操作时) + - 删除: ElMessageBox.confirm +12. **[H4]脏数据检测** + - 编辑模式进入时deep clone快照 + - isDirty检测 + 取消/离开拦截 +13. **[H8]操作结果反馈** + - 成功: success(2s) + silent刷新 + - 失败: error(0手动关闭) + - 网络: 异常提示+重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 列表 | el-table | stripe, border, :data="skillList" | +| 新增按钮 | el-button | type="primary", icon="Plus" | +| 编辑按钮 | el-button | type="primary", link | +| 删除按钮 | el-button | type="danger", link | +| 新增/编辑弹窗 | el-dialog | :title="dialogTitle", width="480px", :close-on-click-modal="false" | +| 技能名称 | el-input | maxlength=30, show-word-limit, placeholder="请输入技能名称" | +| 关联班组 | el-select | v-model="form.teamIds", multiple, collapse-tags, filterable, placeholder="请选择关联班组" | +| 描述 | el-input | type="textarea", :rows="3", maxlength=200, show-word-limit | +| 删除确认 | el-message-box | type="warning" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 技能名称 | 必填,maxlength=30,同租户唯一 | "请输入技能名称" / "该技能名称已存在" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 表格完整展示全部4列,弹窗宽度480px | +| 1024-1279px(Pad横屏) | 表格隐藏"关联班组"列,弹窗宽度440px | +| 768-1023px(Pad竖屏) | 表格仅显示技能名称、持有人员数、操作列,弹窗宽度90vw | + --- ## 页面5:打卡点分配页 @@ -218,6 +460,65 @@ | 蓝牙Beacon | 下拉单选 | 是 | — | 蓝牙设备管理 | — | | 适用角色 | 下拉多选 | 是 | 全部 | 固定选项 | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用打卡点列表API → 渲染打卡点表格,同时加载班组下拉、蓝牙Beacon下拉选项 +2. **查询/筛选交互流程**:本页无独立筛选条件,所有打卡点数据一次加载展示;可按班组筛选 +3. **表单填写与提交流程**:点击[新增打卡点] → 弹窗表单 → 填写打卡点名称、适用班组、位置描述、蓝牙Beacon、适用角色 → 前端校验通过 → 调用新增API → 成功后关闭弹窗并刷新列表 +4. **弹窗/抽屉交互流程**:新增/编辑弹窗宽度480px;选择蓝牙Beacon后展示设备在线状态;点击遮罩层不关闭 +5. **行内操作流程**:点击[编辑] → 弹窗回填数据 → 修改后提交 → 刷新列表;点击[删除] → 二次确认 → 调用删除API → 刷新列表 +6. **异常与错误处理**:打卡点名称重复提示"该打卡点名称已存在";删除被引用的打卡点提示"该打卡点正在使用中,不可删除";API请求失败显示ElMessage.error +7. **联动/级联交互**:选择适用班组后蓝牙Beacon下拉可按班组区域过滤;适用角色多选支持全选/反选 +8. **权限控制交互表现**:无新增/编辑/删除权限时对应按钮隐藏 +9. **[H1]防重复请求** + - 行内操作点击后该行禁用 + loading态 +10. **[H2]超时与加载反馈** + - GET列表查询 timeout=15秒;POST/PUT/DELETE写操作 timeout=30秒 + - 超时 → 提示"请求超时,请检查网络后重试" + 按钮恢复 +11. **[H3]操作确认机制**(有不可逆操作时) + - 删除: ElMessageBox.confirm +12. **[H4]脏数据检测** + - 编辑模式进入时deep clone快照 + - isDirty检测 + 取消/离开拦截 +13. **[H8]操作结果反馈** + - 成功: success(2s) + silent刷新 + - 失败: error(0手动关闭) + - 网络: 异常提示+重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 列表 | el-table | stripe, border, :data="pointList" | +| 新增打卡点按钮 | el-button | type="primary", icon="Plus" | +| 编辑按钮 | el-button | type="primary", link | +| 删除按钮 | el-button | type="danger", link | +| 新增/编辑弹窗 | el-dialog | :title="dialogTitle", width="480px", :close-on-click-modal="false" | +| 打卡点名称 | el-input | maxlength=30, show-word-limit, placeholder="请输入打卡点名称" | +| 适用班组 | el-select | filterable, placeholder="请选择适用班组" | +| 位置描述 | el-input | placeholder="请输入位置描述" | +| 蓝牙Beacon | el-select | filterable, placeholder="请选择蓝牙Beacon" | +| 适用角色 | el-select | v-model="form.roles", multiple, collapse-tags, placeholder="请选择适用角色" | +| 删除确认 | el-message-box | type="warning" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 打卡点名称 | 必填,maxlength=30 | "请输入打卡点名称" | +| 适用班组 | 必选 | "请选择适用班组" | +| 位置描述 | 必填 | "请输入位置描述" | +| 蓝牙Beacon | 必选 | "请选择蓝牙Beacon" | +| 适用角色 | 必选,至少1项 | "请选择适用角色" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 表格完整展示全部6列,弹窗宽度480px | +| 1024-1279px(Pad横屏) | 表格隐藏"位置"列,弹窗宽度440px | +| 768-1023px(Pad竖屏) | 表格仅显示打卡点名称、班组、操作列,弹窗宽度90vw | + --- ## 页面6:下属账号管理页 @@ -309,6 +610,84 @@ | 权限覆盖 | /api/v1/subordinates/{id}/override-permissions | PUT | 四级树形权限 | | 批量操作 | /api/v1/subordinates/batch | POST | 批量启停/分配角色 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 并行调用下属列表API、班组列表、角色列表 → 渲染表格和筛选条件,默认加载第一页数据(每页20条) +2. **查询/筛选交互流程**:输入姓名/选择班组/选择角色/选择状态 → 点击[查询] → 重新加载数据 → 点击[重置]清空条件 +3. **表单填写与提交流程**:点击[新增下属] → 弹窗填写姓名、手机号、所属班组、分配角色、是否自定义权限、数据权限范围 → 前端校验通过 → 调用新增API → 成功后关闭弹窗并刷新列表;开启"自定义权限"后展示四级权限树形勾选 +4. **弹窗/抽屉交互流程**:新增弹窗宽度600px;编辑弹窗宽度520px;分配角色弹窗宽度480px;权限覆盖弹窗宽度700px,展示四级权限树形结构;点击遮罩层不关闭 +5. **行内操作流程**:点击[编辑] → 弹窗回填数据 → 修改后提交 → 刷新列表;点击[分配角色] → 选择角色 → 提交 → 角色实时生效;点击[权限覆盖] → 四级权限树形勾选 → 提交 → 权限实时生效;点击[启停] → 二次确认 → 提交 → 刷新行状态 +6. **异常与错误处理**:手机号已注册提示"该手机号已被注册";角色变更后毫秒级生效(Redis Pub/Sub);账号禁用后session失效+小程序下线;API请求失败显示ElMessage.error +7. **联动/级联交互**:选择所属班组后角色下拉可按班组类型过滤;开启"自定义权限"后动态展示四级权限树;批量分配角色时勾选人员后统一选择角色 +8. **权限控制交互表现**:无 org:staff:create 权限时[新增下属]按钮隐藏;无 permission:user:update 权限时[分配角色][权限覆盖]按钮隐藏;无 org:staff:update 权限时[编辑][启停]按钮隐藏;批量操作按钮需勾选后才可点击 +9. **[H1]防重复请求** + - 查询按钮点击后 disabled + loading态,API返回后恢复 + - 行内操作点击后该行禁用 + loading态 + - 分页切换 abort上一请求再发新请求 +10. **[H2]超时与加载反馈** + - GET列表查询 timeout=15秒;POST/PUT/DELETE写操作 timeout=30秒 + - 超时 → 提示"请求超时,请检查网络后重试" + 按钮恢复 + - 加载>2秒显示全局loading +11. **[H3]操作确认机制**(有不可逆操作时) + - 启停: ElMessageBox.confirm +12. **[H4]脏数据检测** + - 编辑模式进入时deep clone快照 + - isDirty检测 + 取消/离开拦截 +13. **[H8]操作结果反馈** + - 成功: success(2s) + silent刷新 + - 失败: error(0手动关闭) + - 网络: 异常提示+重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 姓名输入 | el-input | clearable, maxlength=20, placeholder="请输入姓名" | +| 班组筛选 | el-select | clearable, filterable, placeholder="全部班组" | +| 角色筛选 | el-select | clearable, placeholder="全部角色" | +| 状态筛选 | el-select | clearable, placeholder="全部状态" | +| 列表 | el-table | stripe, border, :data="tableData", @selection-change | +| 勾选列 | el-table-column | type="selection", width="40" | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next", :page-sizes="[10,20,50]" | +| 新增下属按钮 | el-button | type="primary", icon="Plus" | +| 批量分配角色按钮 | el-button | type="success", :disabled="!selectedRows.length" | +| 批量启停按钮 | el-button | type="warning", :disabled="!selectedRows.length" | +| 编辑按钮 | el-button | type="primary", link | +| 分配角色按钮 | el-button | type="success", link | +| 权限覆盖按钮 | el-button | type="warning", link | +| 启停按钮 | el-button | type="warning", link | +| 状态标签 | el-tag | 启用=success / 禁用=danger | +| 新增弹窗 | el-dialog | title="新增下属", width="600px", :close-on-click-modal="false" | +| 姓名 | el-input | maxlength=20, placeholder="请输入姓名" | +| 手机号 | el-input | maxlength=11, placeholder="请输入手机号" | +| 所属班组 | el-select | v-model="form.teamIds", multiple, collapse-tags, filterable | +| 分配角色 | el-select | placeholder="请选择角色" | +| 自定义权限开关 | el-switch | v-model="form.customPermission" | +| 数据权限范围 | el-select | placeholder="请选择数据权限范围" | +| 四级权限树 | el-tree | :data="permissionTree", show-checkbox, node-key="id", :props="{label:'name',children:'children'}" | +| 分配角色弹窗 | el-dialog | title="分配角色", width="480px", :close-on-click-modal="false" | +| 权限覆盖弹窗 | el-dialog | title="权限覆盖", width="700px", :close-on-click-modal="false" | +| 二次确认 | el-message-box | type="warning" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 姓名 | 必填,maxlength=20 | "请输入姓名" | +| 手机号 | 必填,正则/^1[3-9]\d{9}$/ | "请输入正确的手机号码" | +| 所属班组 | 必选,至少1个 | "请选择所属班组" | +| 分配角色 | 必选 | "请选择分配角色" | +| 数据权限范围 | 必选 | "请选择数据权限范围" | +| 批量操作-勾选 | 至少勾选1条 | "请至少选择一条记录" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区水平排列,表格完整展示全部8列,新增弹窗600px,权限弹窗700px | +| 1024-1279px(Pad横屏) | 查询条件区换行排列,表格隐藏"数据权限""手机号"列,弹窗宽度缩减10% | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,表格仅显示姓名、班组、角色、状态、操作列;批量操作收入"更多"下拉菜单;弹窗宽度90vw | + --- ## 需求追溯 diff --git a/docs/02-功能清单-物业公司/05-考勤打卡.md b/docs/02-功能清单-物业公司/05-考勤打卡.md index 41fcaac..3267ac9 100644 --- a/docs/02-功能清单-物业公司/05-考勤打卡.md +++ b/docs/02-功能清单-物业公司/05-考勤打卡.md @@ -3,6 +3,7 @@ > 模块编码:attendance > 端侧:Web + 小程序(双端) > 关联文档:01-模块划分 §3.5 / 02-功能清单-物业公司 §5 / 03-业务流转逻辑-物业公司 §5 / 05-接口规范 §9.2 / 06-项目技术要求 §4.4 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -67,6 +68,58 @@ | 新增 | /api/v1/attendance-points | POST | — | | 编辑 | /api/v1/attendance-points/{id} | PUT | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 请求打卡点列表API → 渲染表格数据 → 默认无选中状态 +2. **查询/筛选交互流程**:填写筛选条件(打卡点名称/所属班组/状态)→ 点击"查询" → 调用列表API并刷新表格 → 点击"重置"清空条件并重新加载 +3. **表单填写与提交流程**:点击"新增打卡点" → 弹出新增弹窗 → 填写表单字段 → 点击确定 → 前端校验通过 → 调用新增API → 成功后关闭弹窗并刷新列表 +4. **弹窗/抽屉交互流程**:新增/编辑弹窗居中展示,宽度480px;点击遮罩层或右上角关闭按钮可关闭;未保存变更时提示确认 +5. **行内操作流程**:点击"编辑" → 弹出编辑弹窗(预填数据)→ 修改后提交 → 刷新当前行;点击"删除" → 弹出二次确认弹窗"确定删除该打卡点?" → 确认后调用删除API → 刷新列表 +6. **异常与错误处理**:API请求失败显示ElMessage.error提示;删除有关联人员的打卡点时提示"该打卡点下存在关联人员,无法删除" +7. **联动/级联交互**:选择所属班组后,蓝牙Beacon下拉选项可按班组过滤;Beacon状态根据设备管理模块实时数据展示在线/离线 +8. **权限控制交互表现**:无`attendance:point:create`权限时隐藏"新增打卡点"按钮;无`attendance:point:update`权限时隐藏行内"编辑"按钮;无`attendance:point:delete`权限时隐藏行内"删除"按钮 +9. **H1 防重复请求(强制)**:"查询""新增打卡点"按钮点击后立即disabled并显示loading态;行内"编辑""删除"操作时整行添加半透明遮罩禁用重复点击;翻页/切换筛选条件时自动abort前一个未完成请求再发新请求;列表查询与下拉选项加载等并行请求互不阻塞 +10. **H2 超时配置(强制)**:列表查询GET请求设置15s超时,新增/编辑POST请求设置30s超时;任何请求耗时>3s时在页面顶部展示全局ElLoading("数据加载中...")直到响应返回 +11. **H3 操作确认(强制)**:"删除打卡点"操作弹出ElMessageBox.confirm二次确认,type="error",提示文案"确定删除该打卡点?删除后将不可恢复" +12. **H6 批量限制(建议)**:当前页面暂无批量操作;若后续扩展批量删除功能,单次批量删除上限50条记录,超出时提示"单次最多选择50条记录进行操作" +13. **H8 反馈规范(建议)**:新增/编辑成功后ElMessage.success显示2秒后自动消失,同时silent方式刷新列表(不显示loading遮罩);失败提示ElMessage.error需用户手动关闭;网络异常时显示"网络连接异常,请检查网络后重试"并提供重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 打卡点名称输入框 | ElInput | maxlength=30, showWordLimit=true, placeholder="请输入打卡点名称" | +| 所属班组下拉 | ElSelect | filterable=true, clearable=true, placeholder="请选择所属班组" | +| 位置描述输入框 | ElInput | maxlength=100, showWordLimit=true, type="textarea", :rows=2 | +| 蓝牙Beacon下拉 | ElSelect | filterable=true, clearable=true, placeholder="请选择蓝牙Beacon" | +| 适用角色多选 | ElSelect | multiple=true, collapseTags=true, placeholder="请选择适用角色" | +| 状态下拉 | ElSelect | clearable=true, :options=[{label:"在线",value:"online"},{label:"离线",value:"offline"}] | +| 查询按钮 | ElButton | type="primary", icon="Search" | +| 重置按钮 | ElButton | icon="Refresh" | +| 新增打卡点按钮 | ElButton | type="primary", icon="Plus" | +| 列表表格 | ElTable | stripe=true, border=true, :data=tableData | +| 分页 | ElPagination | layout="total, sizes, prev, pager, next", :page-sizes=[10,20,50] | +| 新增/编辑弹窗 | ElDialog | width="480px", :close-on-click-modal=false, :destroy-on-close=true | +| 删除确认弹窗 | ElMessageBox | type="warning", confirmButtonText="确定", cancelButtonText="取消" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 打卡点名称 | required, maxLength=30 | 请输入打卡点名称;打卡点名称不能超过30个字符 | +| 所属班组 | required | 请选择所属班组 | +| 位置描述 | required, maxLength=100 | 请输入位置描述;位置描述不能超过100个字符 | +| 蓝牙Beacon | required | 请选择蓝牙Beacon设备 | +| 适用角色 | required | 请选择适用角色 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区水平排列,表格完整展示所有列,弹窗宽度480px | +| 1024-1279px(Pad横屏) | 查询条件区换行排列(名称独占一行,班组+状态一行),表格隐藏位置描述列,弹窗宽度420px | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,表格仅显示打卡点名称、所属班组、操作列,弹窗宽度90vw | + --- ## 页面2:打卡规则页 @@ -125,6 +178,62 @@ | 规则查询 | /api/v1/attendance-rules | GET | 按班组查询 | | 保存 | /api/v1/attendance-rules | POST | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 请求规则列表API(按班组分组)→ 渲染规则卡片列表 → 默认展示全部班组卡片 +2. **查询/筛选交互流程**:本页无独立筛选条件,数据按班组以卡片形式展示,所有班组规则一次加载 +3. **表单填写与提交流程**:点击卡片"编辑"按钮 → 弹出编辑弹窗(预填当前规则数据)→ 修改时间与阈值 → 点击确定 → 前端校验通过 → 调用保存API → 成功后关闭弹窗并刷新对应卡片数据 +4. **弹窗/抽屉交互流程**:编辑弹窗居中展示,宽度520px;弹窗内时间选择器联动显示打卡窗口;点击遮罩层或关闭按钮可退出,未保存时提示确认 +5. **行内操作流程**:无行内操作,仅在卡片上提供"编辑"按钮 +6. **异常与错误处理**:严重迟到阈值≤迟到阈值时提示"严重迟到阈值必须大于迟到阈值";严重早退阈值≤早退阈值时提示"严重早退阈值必须大于早退阈值";打卡窗口起止时间逻辑冲突时提示错误 +7. **联动/级联交互**:修改上班时间时自动调整上班打卡窗口默认值(上班前30分~上班后30分);修改下班时间同理自动调整下班打卡窗口 +8. **权限控制交互表现**:无`attendance:rule:update`权限时隐藏卡片"编辑"按钮,卡片仅展示只读信息 +9. **H1 防重复请求(强制)**:卡片"编辑"按钮点击后立即disabled并显示loading态;编辑弹窗提交按钮点击后disabled防重复提交 +10. **H2 超时配置(强制)**:规则查询GET请求设置15s超时,保存POST请求设置30s超时;请求耗时>3s时展示全局ElLoading +11. **H4 脏数据检测(强制)**:打开编辑弹窗时对当前规则数据进行deepClone快照;弹窗内监听表单变更计算isDirty状态;点击弹窗关闭按钮或遮罩层且isDirty为true时,弹出ElMessageBox.confirm提示"修改尚未保存,确定要关闭吗?";路由离开前通过beforeRouteLeave守卫拦截未保存的变更 +12. **H8 反馈规范(建议)**:保存成功后ElMessage.success("规则保存成功")显示2秒后自动消失并silent刷新对应卡片数据;失败提示需手动关闭;网络异常提供重试 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 上班时间选择 | ElTimePicker | format="HH:mm", placeholder="请选择上班时间" | +| 上班打卡窗口(起) | ElTimePicker | format="HH:mm", placeholder="上班打卡开始时间" | +| 上班打卡窗口(止) | ElTimePicker | format="HH:mm", placeholder="上班打卡结束时间" | +| 下班时间选择 | ElTimePicker | format="HH:mm", placeholder="请选择下班时间" | +| 下班打卡窗口(起) | ElTimePicker | format="HH:mm", placeholder="下班打卡开始时间" | +| 下班打卡窗口(止) | ElTimePicker | format="HH:mm", placeholder="下班打卡结束时间" | +| 迟到阈值 | ElInputNumber | :min=0, :max=180, :step=5, controls-position="right" | +| 严重迟到阈值 | ElInputNumber | :min=1, :max=180, :step=5, controls-position="right" | +| 早退阈值 | ElInputNumber | :min=0, :max=180, :step=5, controls-position="right" | +| 严重早退阈值 | ElInputNumber | :min=1, :max=180, :step=5, controls-position="right" | +| 规则卡片 | ElCard | shadow="hover", :body-style="{ padding: '20px' }" | +| 编辑按钮 | ElButton | type="primary", link=true, icon="Edit" | +| 编辑弹窗 | ElDialog | width="520px", :close-on-click-modal=false, :destroy-on-close=true | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 上班时间 | required | 请选择上班时间 | +| 上班打卡窗口(起) | required, 必须早于上班时间 | 请选择上班打卡开始时间;上班打卡开始时间必须早于上班时间 | +| 上班打卡窗口(止) | required, 必须晚于上班时间 | 请选择上班打卡结束时间;上班打卡结束时间必须晚于上班时间 | +| 下班时间 | required | 请选择下班时间 | +| 下班打卡窗口(起) | required, 必须早于下班时间 | 请选择下班打卡开始时间;下班打卡开始时间必须早于下班时间 | +| 下班打卡窗口(止) | required, 必须晚于下班时间 | 请选择下班打卡结束时间;下班打卡结束时间必须晚于下班时间 | +| 迟到阈值(分钟) | required, ≥0 | 请输入迟到阈值;迟到阈值不能为负数 | +| 严重迟到阈值(分钟) | required, >迟到阈值 | 请输入严重迟到阈值;严重迟到阈值必须大于迟到阈值 | +| 早退阈值(分钟) | required, ≥0 | 请输入早退阈值;早退阈值不能为负数 | +| 严重早退阈值(分钟) | required, >早退阈值 | 请输入严重早退阈值;严重早退阈值必须大于早退阈值 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 规则卡片双列排列,卡片内信息水平展示,弹窗宽度520px | +| 1024-1279px(Pad横屏) | 规则卡片单列排列,卡片内信息水平展示,弹窗宽度480px | +| 768-1023px(Pad竖屏) | 规则卡片单列排列,卡片内信息垂直堆叠(标签与值分行显示),弹窗宽度90vw | + --- ## 页面3:考勤记录页 @@ -169,6 +278,53 @@ |----------|---------|------|------| | 记录查询 | /api/v1/attendance-records | GET | 分页查询 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认日期为今天 → 请求考勤记录API → 渲染表格数据 +2. **查询/筛选交互流程**:选择日期/班组/人员/打卡状态 → 点击"查询" → 调用API刷新表格 → 点击"重置"恢复默认条件(今天/全部/全部/全部)并重新加载 +3. **表单填写与提交流程**:本页无表单提交操作 +4. **弹窗/抽屉交互流程**:点击"查看详情" → 弹出详情弹窗展示完整打卡信息(打卡时间、方式、状态、打卡点、蓝牙设备信息等),宽度500px +5. **行内操作流程**:点击"查看详情" → 调用详情API → 弹窗展示完整考勤记录详情 +6. **异常与错误处理**:API请求失败显示ElMessage.error提示;列表数据为空时展示ElEmpty空状态;补录标记列显示ElTag标签"补录" +7. **联动/级联交互**:选择班组后人员筛选可按班组过滤;上班/下班状态字段根据状态值显示不同颜色Tag(正常-绿/迟到-橙/严重迟到-红/早退-橙/严重早退-红) +8. **权限控制交互表现**:无`attendance:record:view`权限时隐藏"查看详情"按钮;主管仅看到本班组数据,员工仅看本人数据(后端数据权限控制) +9. **H1 防重复请求(强制-轻量)**:"查询"按钮点击后disabled+loading态;行内"查看详情"按钮点击后整行半透明遮罩防重复;分页切换时abort前一次未完成请求;本页以查看为主,采用轻量级防重复策略 +10. **H2 超时配置(强制)**:记录查询GET请求设置15s超时;请求耗时>3s时在表格区域展示ElLoading局部加载动画 +11. **H8 反馈规范(建议)**:详情弹窗加载完成后正常展示,无需额外成功反馈;API请求失败时ElMessage.error提示需手动关闭;网络异常时显示重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 日期选择 | ElDatePicker | type="date", value-format="YYYY-MM-DD", placeholder="选择日期" | +| 班组下拉 | ElSelect | filterable=true, clearable=true, placeholder="请选择班组" | +| 人员输入框 | ElInput | placeholder="请输入人员姓名", clearable=true | +| 打卡状态下拉 | ElSelect | clearable=true, :options=[正常,迟到,早退,缺卡,补录] | +| 查询按钮 | ElButton | type="primary", icon="Search" | +| 重置按钮 | ElButton | icon="Refresh" | +| 列表表格 | ElTable | stripe=true, border=true, :data=tableData, :default-sort={prop:"上班打卡时间",order:"ascending"} | +| 上班状态列 | ElTag | :type=状态映射(正常→success/迟到→warning/严重迟到→danger) | +| 下班状态列 | ElTag | :type=状态映射(正常→success/早退→warning/严重早退→danger) | +| 补录标记列 | ElTag | type="info", v-if="is_supplement" | +| 分页 | ElPagination | layout="total, sizes, prev, pager, next", :page-sizes=[10,20,50] | +| 详情弹窗 | ElDialog | width="500px", :close-on-click-modal=true, :destroy-on-close=true | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 日期 | 非必填,格式校验 | 日期格式不正确 | +| 人员 | 最大20字 | 人员姓名不能超过20个字符 | +| 打卡状态 | 枚举值校验 | 请选择有效的打卡状态 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区水平排列,表格完整展示所有10列,分页右对齐 | +| 1024-1279px(Pad横屏) | 查询条件区换行排列,表格隐藏上班打卡方式、下班打卡方式列,保留8列 | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,表格仅显示姓名、班组、上班状态、下班状态、操作列,分页居中 | + --- ## 页面4:异常审核页 @@ -226,6 +382,55 @@ | 申诉列表 | /api/v1/attendance-appeals | GET | — | | 审核 | /api/v1/attendance-appeals/{id}/approve | POST | 通过后自动补录打卡 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用申诉列表API GET /api/v1/attendance-appeals → 渲染异常审核表格,默认按申诉日期倒序排列,每页20条;同时加载审核状态下拉选项 +2. **查询/筛选交互流程**:选择审核状态/输入申诉人/选择日期范围 → 点击[查询] → 携带筛选参数重新请求第1页 → 点击[重置]清空条件并重新加载 +3. **表单填写与提交流程**:本页无新增表单,申诉数据由小程序端提交 +4. **弹窗/抽屉交互流程**:点击[审核](审核状态=待审核时显示)→ 弹出审核弹窗,选择"通过"或"驳回",驳回时必填驳回原因 → 确认后调用审核API → 审核通过后系统自动补录打卡记录 → 成功后关闭弹窗并刷新列表 +5. **行内操作流程**:点击[审核] → 弹窗选择通过/驳回 → 提交 → 刷新该行审核状态;点击[查看](如有)→ 弹窗展示申诉详情 +6. **异常与错误处理**:审核已审核过的记录提示"该申诉已审核";审核通过后自动补录打卡失败时提示"自动补录失败,请手动处理";API请求失败显示ElMessage.error;列表无数据时显示ElEmpty +7. **联动/级联交互**:审核状态筛选影响列表展示;蓝牙双模式下,策略=REQUIRED时异常申诉必须审核,策略=OPTIONAL时无需申诉(手动打卡) +8. **权限控制交互表现**:无 attendance:appeal:approve 权限时[审核]按钮隐藏;主管仅审核本班组人员的申诉 +9. **H1 防重复请求(强制)**:"查询"按钮点击后disabled+loading态;行内[审核]按钮点击后整行禁用+半透明遮罩;分页切换时abort前一次未完成请求 +10. **H2 超时配置(强制)**:申诉列表GET请求设置15s超时,审核POST请求设置30s超时;请求耗时>3s时展示全局ElLoading +11. **H3 操作确认(强制)**:审核操作(通过/驳回)为不可逆操作,提交前弹出ElMessageBox.confirm二次确认,通过操作type="info"提示"确定通过该申诉?通过后将自动补录打卡记录",驳回操作type="warning"提示"确定驳回该申诉?" +12. **H6 批量限制(建议)**:当前页面暂无批量操作;若后续扩展批量审核功能,单次批量审核上限100条记录,超出时提示"单次最多选择100条进行批量审核" +13. **H8 反馈规范(建议)**:审核成功后ElMessage.success显示2秒后自动消失,silent刷新列表状态;失败提示需手动关闭;网络异常提供重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 审核状态筛选 | el-select | clearable, placeholder="全部状态" | +| 申诉人输入 | el-input | clearable, maxlength=20, placeholder="请输入申诉人" | +| 日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD", range-separator="至" | +| 列表 | el-table | stripe, border, :data="tableData" | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next", :page-sizes="[10,20,50]" | +| 审核状态标签 | el-tag | 待审核=warning / 已通过=success / 已驳回=danger | +| 异常类型标签 | el-tag | type="info", size="small" | +| 审核按钮 | el-button | type="primary", link, v-if="row.auditStatus==='pending'" | +| 审核弹窗 | el-dialog | title="审核异常申诉", width="500px", :close-on-click-modal="false" | +| 审核结果 | el-radio-group | v-model="auditForm.result" | +| 驳回原因 | el-input | type="textarea", :rows="3", maxlength=200, show-word-limit, v-if="auditForm.result==='reject'" | +| 蓝牙双模式差异展示 | el-descriptions | :column="2", border, title="蓝牙策略差异" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 审核结果 | 必选 | "请选择审核结果" | +| 驳回原因 | 驳回时必填,maxlength=200 | "请填写驳回原因" | +| 日期范围 | 结束日期≥开始日期 | "结束日期不能早于开始日期" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区水平排列,表格完整展示全部7列,弹窗宽度500px | +| 1024-1279px(Pad横屏) | 查询条件区换行排列,表格隐藏"申诉说明"列,弹窗宽度460px | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,表格仅显示申诉人、异常类型、审核状态、操作列,弹窗宽度90vw | + --- ## 页面5:数据补录页 @@ -256,6 +461,54 @@ | 补录列表 | /api/v1/attendance-records/supplements | GET | — | | 审核 | /api/v1/attendance-records/supplements/{id}/approve | POST | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用补录列表API GET /api/v1/attendance-records/supplements → 渲染补录数据表格,默认按补录时间倒序排列,每页20条 +2. **查询/筛选交互流程**:选择审核状态/输入补录人/选择日期范围 → 点击[查询] → 重新加载数据 → 点击[重置]清空条件 +3. **表单填写与提交流程**:本页无新增表单,补录数据来源:①小程序端蓝牙失败后手动补录 ②异常申诉审核通过后系统自动补录 +4. **弹窗/抽屉交互流程**:点击[审核](审核状态=待审核时显示)→ 弹出审核弹窗,选择"通过"或"驳回",驳回时必填驳回原因 → 确认后调用审核API → 成功后关闭弹窗并刷新列表 +5. **行内操作流程**:点击[审核] → 弹窗选择通过/驳回 → 提交 → 刷新该行审核状态;自动补录的记录(异常申诉审核通过)标记为"自动补录",无需二次审核 +6. **异常与错误处理**:审核已审核过的记录提示"该记录已审核";API请求失败显示ElMessage.error;列表无数据时显示ElEmpty +7. **联动/级联交互**:审核状态筛选影响列表展示;补录原因枚举来源于系统配置;自动补录记录与手动补录记录通过标签区分 +8. **权限控制交互表现**:无 attendance:supplement:approve 权限时[审核]按钮隐藏 +9. **H1 防重复请求(强制)**:"查询"按钮点击后disabled+loading态;行内[审核]按钮点击后整行禁用+半透明遮罩;分页切换时abort前一次未完成请求 +10. **H2 超时配置(强制)**:补录列表GET请求设置15s超时,审核POST请求设置30s超时;请求耗时>3s时展示全局ElLoading +11. **H3 操作确认(强制)**:审核操作(通过/驳回)为不可逆操作,提交前弹出ElMessageBox.confirm二次确认,通过type="info"提示"确定通过该补录记录?",驳回type="warning"提示"确定驳回该补录记录?" +12. **H6 批量限制(建议)**:当前页面暂无批量操作;若后续扩展批量审核功能,单次批量审核上限100条记录 +13. **H8 反馈规范(建议)**:审核成功ElMessage.success 2秒自动消失+silent刷新列表;失败手动关闭;网络异常提供重试 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 审核状态筛选 | el-select | clearable, placeholder="全部状态" | +| 补录人输入 | el-input | clearable, maxlength=20, placeholder="请输入补录人" | +| 日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD", range-separator="至" | +| 列表 | el-table | stripe, border, :data="tableData" | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next", :page-sizes="[10,20,50]" | +| 审核状态标签 | el-tag | 待审核=warning / 已通过=success / 已驳回=danger | +| 补录类型标签 | el-tag | 自动补录=primary / 手动补录=warning, size="small" | +| 审核按钮 | el-button | type="primary", link, v-if="row.auditStatus==='pending'" | +| 审核弹窗 | el-dialog | title="审核补录", width="500px", :close-on-click-modal="false" | +| 审核结果 | el-radio-group | v-model="auditForm.result" | +| 驳回原因 | el-input | type="textarea", :rows="3", maxlength=200, show-word-limit, v-if="auditForm.result==='reject'" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 审核结果 | 必选 | "请选择审核结果" | +| 驳回原因 | 驳回时必填,maxlength=200 | "请填写驳回原因" | +| 日期范围 | 结束日期≥开始日期 | "结束日期不能早于开始日期" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区水平排列,表格完整展示全部列,弹窗宽度500px | +| 1024-1279px(Pad横屏) | 查询条件区换行排列,表格隐藏次要列,弹窗宽度460px | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,表格仅显示关键列,弹窗宽度90vw | + --- ## 需求追溯 diff --git a/docs/02-功能清单-物业公司/06-服务评价.md b/docs/02-功能清单-物业公司/06-服务评价.md index 137e85c..3544b2b 100644 --- a/docs/02-功能清单-物业公司/06-服务评价.md +++ b/docs/02-功能清单-物业公司/06-服务评价.md @@ -3,6 +3,7 @@ > 模块编码:evaluation > 端侧:Web + 小程序(双端) > 关联文档:01-模块划分 §3.6 / 02-功能清单-物业公司 §6 / 03-业务流转逻辑-物业公司 §6 / 05-接口规范 §9.2 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -61,6 +62,51 @@ |----------|---------|------|------| | 汇总数据 | /api/v1/evaluations/summary | GET | 含统计卡片+图表数据 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认查询"本月"时间范围 → 调用汇总数据API GET /api/v1/evaluations/summary → 依次渲染统计卡片区(平均评分/评价总数/待回复数/低评分数)→ 图表区(柱状图+折线图+饼图),加载中显示骨架屏 +2. **查询/筛选交互流程**:点击快捷时间按钮(今日/本周/本月/自定义)→ 立即切换并重新请求汇总数据;选择自定义日期范围 → 确认后触发查询 +3. **表单填写与提交流程**:本页面无表单提交;点击[导出] → 导出汇总数据Excel +4. **弹窗/抽屉交互流程**:点击低评分数卡片 → 弹出低评分评价列表弹窗(宽度700px);点击待回复数卡片 → 跳转评价列表页(自动筛选未回复) +5. **行内操作流程**:点击图表数据点 → 弹出该数据点的详细评价列表 +6. **异常与错误处理**:汇总数据加载失败时各区域显示"加载失败,点击重试";无评价数据时统计卡片显示"—",图表区显示空状态插图;导出失败提示"导出失败,请重试" +7. **联动/级联交互**:时间范围变更后统计卡片、图表区全区域联动刷新;各模块评分柱状图点击可下钻到该模块的评价列表 +8. **权限控制交互表现**:无 evaluation:summary:view 权限时页面显示403提示;导出按钮需对应权限 +9. **H1 防重复请求(强制)**:快捷时间按钮切换时立即disabled并loading;导出按钮点击后disabled+loading态直到下载完成或失败;时间范围/筛选条件变更触发查询时abort前一次未完成请求 +10. **H2 超时配置(强制)**:汇总数据GET请求设置30s超时(统计档);导出操作GET请求设置60s超时(上传导出档);任何请求耗时>3s时展示全局ElLoading +11. **H8 反馈规范(建议)**:数据加载完成后各区域正常渲染无需额外成功提示;导出成功后ElMessage.success("导出成功")2秒自动消失;加载失败区域显示"点击重试"按钮;网络异常提供重试 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 时间快捷按钮 | el-radio-group + el-radio-button | v-model="timeRange" | +| 自定义日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD", range-separator="至" | +| 统计卡片 | el-card | shadow="hover", body-style="{padding:'20px'}" | +| 平均评分 | 自定义评分展示 | 大字号数字 + el-rate(disabled) | +| 各模块平均评分图 | echarts (柱状图) | tooltip.trigger="axis", series.type="bar" | +| 评分趋势图 | echarts (折线图) | smooth=true, tooltip.trigger="axis" | +| 星级分布图 | echarts (饼图) | tooltip.trigger="item", radius=["40%","70%"] | +| 导出按钮 | el-button | type="success", icon="Download" | +| 低评卡片点击 | el-link | type="danger", @click="showLowScoreList" | +| 待回复卡片点击 | el-link | type="warning", @click="goUnrepliedList" | +| 低评分列表弹窗 | el-dialog | title="低评分评价列表", width="700px" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 自定义日期范围-起始日期 | 必须早于或等于结束日期 | "开始日期不能晚于结束日期" | +| 自定义日期范围-跨度 | 时间跨度不超过1年 | "查询时间范围不能超过1年" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 统计卡片4列横排;图表区2列布局(柱状图+折线图),下方饼图居中;弹窗宽度700px | +| 1024-1279px(Pad横屏) | 统计卡片2列×2行排列;图表区单列堆叠,ECharts图表宽度100%,高度最小350px;弹窗宽度650px | +| 768-1023px(Pad竖屏) | 统计卡片2列×2行排列;图表区单列堆叠,ECharts图表宽度100%,高度最小300px;饼图改为环形图节省空间;弹窗宽度95vw | + --- ## 页面2:评价列表页 @@ -129,6 +175,60 @@ | 列表查询 | /api/v1/evaluations | GET | 分页查询 | | 回复 | /api/v1/evaluations/{id}/reply | POST | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用评价列表API GET /api/v1/evaluations → 渲染表格,默认按评价时间倒序排列,每页20条;同时加载评价类型、评分等级等下拉选项 +2. **查询/筛选交互流程**:选择评价类型/选择评分/选择回复状态/选择日期范围 → 点击[查询] → 重新加载数据 → 点击[重置]清空条件 +3. **表单填写与提交流程**:点击[回复](未回复或低分评价时显示)→ 弹出回复弹窗 → 填写回复内容 → 前端校验通过 → 调用回复API → 成功后关闭弹窗并刷新该行回复状态 +4. **弹窗/抽屉交互流程**:点击[查看详情] → 弹窗展示评价详情(评分、评价内容、图片、回复记录),宽度600px;回复弹窗宽度500px,展示评价信息+回复输入框 +5. **行内操作流程**:点击[查看详情] → 弹窗展示完整评价信息;点击[回复] → 弹窗填写回复内容 → 提交 → 刷新该行回复状态为"已回复";低分评价(≤2分)行红色背景高亮 +6. **异常与错误处理**:重复回复提示"该评价已回复";API请求失败显示ElMessage.error;列表无数据时显示ElEmpty"暂无评价数据" +7. **联动/级联交互**:评分列使用星级展示,低分(≤2分)红色标记;回复状态标签已回复=绿色/未回复=红色;低评分评价自动通知主管 +8. **权限控制交互表现**:无 evaluation:list:view 权限时[查看详情]按钮隐藏;无 evaluation:list:update 权限时[回复]按钮隐藏;主管仅看本班组相关评价 +9. **H1 防重复请求(强制)**:"查询"按钮点击后disabled+loading态;行内[查看详情][回复]按钮点击后整行禁用+半透明遮罩;分页切换时abort前一次未完成请求 +10. **H2 超时配置(强制)**:列表查询GET请求15s超时,回复POST请求30s超时;>3s展示全局ElLoading +11. **H3 操作确认(强制)**:"回复"操作为不可逆操作,提交前弹出ElMessageBox.confirm二次确认,type="info",提示"确定提交该回复?提交后将不可修改" +12. **H6 批量限制(建议)**:当前页面暂无批量操作;若后续扩展批量回复功能,单次批量回复上限100条记录 +13. **H8 反馈规范(建议)**:回复成功ElMessage.success 2秒自动消失+silent刷新该行状态;重复回复提示需手动关闭;网络异常提供重试 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 评价类型筛选 | el-select | clearable, placeholder="全部类型" | +| 评分筛选 | el-select | clearable, placeholder="全部评分" | +| 回复状态筛选 | el-select | clearable, placeholder="全部状态" | +| 日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD", range-separator="至" | +| 列表 | el-table | stripe, border, :data="tableData", :row-class-name="lowScoreRowClass" | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next", :page-sizes="[10,20,50]" | +| 评分展示 | el-rate | disabled, :colors="['#99A9BF','#F7BA2A','#FF9900']" | +| 评价类型标签 | el-tag | 报修服务=primary / 巡检服务=success / 保洁服务=warning / 综合评价=info | +| 回复状态标签 | el-tag | 已回复=success / 未回复=danger | +| 低分标记 | el-tag | type="danger", size="small", v-if="score<=2" | +| 查看详情按钮 | el-button | type="primary", link | +| 回复按钮 | el-button | type="success", link, v-if="!row.replied \|\| row.score<=2" | +| 详情弹窗 | el-dialog | title="评价详情", width="600px" | +| 评价详情展示 | el-descriptions | :column="2", border | +| 评价图片 | el-image | :preview-src-list="photoList", fit="cover" | +| 回复弹窗 | el-dialog | title="回复评价", width="500px", :close-on-click-modal="false" | +| 回复内容 | el-input | type="textarea", :rows="4", maxlength=500, show-word-limit, placeholder="请输入回复内容" | +| 提交回复按钮 | el-button | type="primary", :loading="replying" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 回复内容 | 必填,maxlength=500 | "请输入回复内容" | +| 日期范围 | 结束日期≥开始日期 | "结束日期不能早于开始日期" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区水平排列,表格完整展示全部8列,弹窗宽度500px/600px | +| 1024-1279px(Pad横屏) | 查询条件区换行排列,表格隐藏"评价内容"列,弹窗宽度460px/560px | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,表格仅显示评价类型、评分、评价人、回复状态、操作列,弹窗宽度95vw | + --- ## 页面3:绩效报表页 @@ -169,6 +269,49 @@ | 绩效数据 | /api/v1/evaluations/performance | GET | — | | 导出 | /api/v1/evaluations/performance/export | GET | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认时间维度为"本月" → 调用绩效数据API GET /api/v1/evaluations/performance → 渲染绩效表格,加载中显示骨架屏 +2. **查询/筛选交互流程**:选择时间维度(天/周/月/年/自定义)→ 重新请求;选择班组 → 过滤该班组绩效数据;维度变更后自动刷新 +3. **表单填写与提交流程**:本页无表单提交;点击[导出Excel] → 弹出确认弹窗 → 确认后触发下载 +4. **弹窗/抽屉交互流程**:导出确认弹窗宽度400px;点击遮罩层可关闭 +5. **行内操作流程**:点击人员行 → 展开该人员的评价详情(评价数、平均分、低分数等);低分数列红色标记 +6. **异常与错误处理**:数据加载失败显示ElMessage.error"数据加载失败,请稍后重试";无数据时表格显示ElEmpty;导出失败提示"导出失败,请重试" +7. **联动/级联交互**:时间维度变更后表格和导出按钮联动刷新;班组筛选联动数据过滤 +8. **权限控制交互表现**:无 evaluation:summary:view 权限时导出按钮隐藏;主管仅看本班组数据 +9. **H1 防重复请求(强制)**:时间维度/班组筛选变更时立即disabled+loading;导出按钮点击后disabled+loading态直到下载完成;翻页/排序时abort前一次未完成请求 +10. **H2 超时配置(强制)**:绩效数据GET请求30s超时(统计档),导出GET请求60s超时(上传导出档);>3s展示全局ElLoading +11. **H6 批量限制(建议)**:导出操作单次最多导出500条记录,超出时提示"导出数据量过大(>500条),请缩小筛选范围后再试" +12. **H8 反馈规范(建议)**:数据加载完成正常渲染无额外提示;导出成功ElMessage.success 2秒自动消失;导出失败手动关闭错误提示;网络异常提供重试 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 时间维度 | el-select | v-model="query.dimension", placeholder="请选择时间维度" | +| 自定义日期 | el-date-picker | type="daterange", v-if="query.dimension==='custom'" | +| 班组筛选 | el-select | clearable, filterable, placeholder="全部班组" | +| 列表 | el-table | stripe, border, :data="performanceData" | +| 导出按钮 | el-button | type="success", icon="Download", :loading="exportLoading" | +| 低分标记 | el-tag | type="danger", v-if="row.lowScoreCount>0" | +| 5星占比 | el-progress | :percentage="row.fiveStarRate", :stroke-width="10" | +| 导出确认弹窗 | el-dialog | title="确认导出", width="400px" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 时间维度 | 必选 | "请选择时间维度" | +| 自定义日期范围 | 维度=自定义时必填,结束日期≥开始日期 | "请选择日期范围" / "结束日期不能早于开始日期" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区水平排列,表格完整展示全部7列 | +| 1024-1279px(Pad横屏) | 查询条件区换行排列,表格隐藏"5星占比"列 | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,表格仅显示班组、人员、评价数、平均评分、低分数列,启用横向滚动 | + --- ## 页面4:评价配置页 @@ -200,6 +343,49 @@ | 查询配置 | /api/v1/evaluation-configs | GET | — | | 保存 | /api/v1/evaluation-configs | PUT | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用查询配置API GET /api/v1/evaluation-configs → 回填配置表单数据 +2. **查询/筛选交互流程**:本页无筛选功能,直接展示配置表单 +3. **表单填写与提交流程**:修改开关/阈值 → 点击[保存] → 前端校验通过 → 调用保存API PUT /api/v1/evaluation-configs → 成功后显示ElMessage.success"配置保存成功" +4. **弹窗/抽屉交互流程**:本页无弹窗交互 +5. **行内操作流程**:无行内操作,表单为页面级 +6. **异常与错误处理**:低评分阈值必须1-5之间,否则提示"低评分阈值必须在1-5之间";回复时限必须>0,否则提示"回复时限必须大于0";保存失败显示ElMessage.error +7. **联动/级联交互**:开启"工单完成后自动触发评价"后,工单验收通过时系统自动发送评价邀请;低评分阈值变更后影响通知触发规则 +8. **权限控制交互表现**:无 evaluation:config:update 权限时[保存]按钮隐藏,表单字段全部禁用(只读模式) +9. **H1 防重复请求(强制)**:"保存"按钮点击后立即disabled+loading态防重复提交 +10. **H2 超时配置(强制)**:查询配置GET请求15s超时,保存PUT请求30s超时;>3s展示全局ElLoading +11. **H3 操作确认(强制)**:保存配置为影响全局的操作(低评分阈值变更将影响通知触发规则、时限变更将影响24h强回复规则),提交前弹出ElMessageBox.confirm二次确认,type="warning",提示"确定保存配置?变更将立即生效并影响相关业务规则" +12. **H4 脏数据检测(强制)**:进入页面加载配置数据时进行deepClone快照作为原始值;监听表单变更计算isDirty状态;当用户尝试路由离开且isDirty为true时,通过beforeRouteLeave守卫弹出确认框提示"配置尚未保存,确定要离开吗?" +13. **H8 反馈规范(建议)**:保存成功ElMessage.success("配置保存成功")2秒自动消失;校验失败/保存失败提示需手动关闭;网络异常提供重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 配置表单 | el-form | :model="configForm", label-width="240px", label-position="right" | +| 工单完成后自动触发评价 | el-switch | v-model="configForm.orderTrigger" | +| 巡检完成后自动触发评价 | el-switch | v-model="configForm.inspectionTrigger" | +| 保洁完成后自动触发评价 | el-switch | v-model="configForm.cleaningTrigger" | +| 低评分阈值 | el-input-number | v-model="configForm.lowScoreThreshold", :min=1, :max=5, :step=1 | +| 低评分必须回复时限 | el-input-number | v-model="configForm.replyDeadline", :min=1, :max=168, :step=1, suffix="小时" | +| 保存按钮 | el-button | type="primary", :loading="saving" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 低评分阈值 | 必填,1-5整数 | "低评分阈值必须在1-5之间" | +| 低评分必须回复时限 | 必填,>0,单位小时 | "回复时限必须大于0" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 表单label-width=240px,表单区域最大宽度800px居中展示 | +| 1024-1279px(Pad横屏) | 表单label-width=200px,表单区域最大宽度700px | +| 768-1023px(Pad竖屏) | 表单label-position改为top,表单区域宽度95%,保存按钮full-width | + --- ## 需求追溯 diff --git a/docs/02-功能清单-物业公司/07-统计报表.md b/docs/02-功能清单-物业公司/07-统计报表.md index f24e654..64a2ac5 100644 --- a/docs/02-功能清单-物业公司/07-统计报表.md +++ b/docs/02-功能清单-物业公司/07-统计报表.md @@ -3,6 +3,7 @@ > 模块编码:statistics > 端侧:Web专属 > 关联文档:01-模块划分 §3.7 / 02-功能清单-物业公司 §7 / 03-业务流转逻辑-物业公司 §7 / 05-接口规范 §9.2 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -67,6 +68,55 @@ | 统计数据 | /api/v1/statistics/repair | GET | 含卡片+图表+表格 | | 导出 | /api/v1/statistics/repair/export | GET | 支持Excel/PDF | +### 交互流程要求 + +1. **页面加载流程**:页面加载时默认查询"本月"时间范围的报修统计数据,依次渲染统计卡片区→图表区→数据表格,加载期间各区域显示骨架屏(Skeleton),全部数据加载完成后隐藏骨架屏 +2. **查询/筛选交互流程**:用户点击快捷时间按钮(今天/本周/本月/本年)立即切换并重新请求;选择"自定义"时弹出el-date-picker日期范围选择器,确认后触发查询;班组、区域、紧急程度下拉筛选变更后自动触发查询,多个筛选条件组合生效 +3. **表单填写与提交流程**:本页面无表单提交,仅筛选查询;导出操作点击后弹出确认弹窗选择导出格式(Excel/PDF),确认后触发下载 +4. **弹窗/抽屉交互流程**:导出格式选择弹窗居中展示,宽度400px;点击弹窗外部或按ESC键关闭弹窗,不触发导出 +5. **行内操作流程**:数据表格行暂无行内操作,鼠标悬停行高亮背景色 +6. **异常与错误处理**:接口请求失败时页面顶部显示el-notification错误提示"数据加载失败,请稍后重试";无数据时图表区显示空状态插图+提示文字"暂无统计数据";导出失败时弹窗提示"导出失败,请重试" +7. **联动/级联交互**:时间范围变更后统计卡片、图表、表格三区联动刷新;筛选条件变更后同样全区域联动 +8. **权限控制交互表现**:无`statistics:repair:export`权限时,导出Excel和导出PDF按钮隐藏;无`statistics:repair:view`权限时页面显示403提示 +9. **H1 防重复请求(强制)**:快捷时间按钮/筛选条件变更时按钮组disabled+loading态;导出按钮点击后disabled+loading直到下载完成;筛选变更触发查询时abort前一次未完成请求;统计卡片与图表区并发请求互不阻塞 +10. **H2 超时配置(强制)**:统计数据GET请求设置30s超时(统计档);导出操作GET请求设置60s超时(上传导出档);任何请求耗时>3s时展示全局ElLoading +11. **H6 批量限制(建议)**:数据表格行数超过500条时提示"数据量过大,建议缩小时间范围后查看或导出";单次导出上限500条记录 +12. **H8 反馈规范(建议)**:数据加载完成后各区域正常渲染无额外成功提示;导出成功ElMessage.success 2秒自动消失;加载失败区域显示"点击重试"及重试按钮;网络异常提供全局重试入口 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 时间快捷按钮 | el-radio-group + el-radio-button | v-model="timeRange", size="default" | +| 自定义日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD", range-separator="至" | +| 班组筛选 | el-select | v-model="filters.team", clearable, placeholder="选择班组" | +| 区域筛选 | el-select | v-model="filters.area", clearable, placeholder="选择区域" | +| 紧急程度筛选 | el-select | v-model="filters.urgency", clearable, placeholder="选择紧急程度" | +| 统计卡片 | el-card | shadow="hover", body-style="{padding: '20px'}" | +| 工单趋势图 | echarts (折线图) | smooth=true, tooltip.trigger="axis", grid.containLabel=true | +| 类型分布图 | echarts (饼图) | roseType="radius", tooltip.trigger="item" | +| 班组工单排名 | echarts (柱状图) | tooltip.trigger="axis", series.type="bar" | +| 数据表格 | el-table | stripe, border, size="default", :data="tableData" | +| 导出Excel按钮 | el-button | type="success", icon="Download", :loading="exportLoading" | +| 导出PDF按钮 | el-button | type="danger", icon="Document", :loading="exportLoading" | +| 导出格式弹窗 | el-dialog | title="选择导出格式", width="400px", :close-on-click-modal="false" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 自定义日期范围-起始日期 | 必须早于或等于结束日期 | 开始日期不能晚于结束日期 | +| 自定义日期范围-结束日期 | 必须晚于或等于起始日期 | 结束日期不能早于开始日期 | +| 自定义日期范围-跨度 | 时间跨度不超过1年 | 查询时间范围不能超过1年 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 统计卡片6列横排;图表区2列布局(趋势图+饼图),下方柱状图全宽;表格全宽展示 | +| 1024-1279px(Pad横屏) | 统计卡片3列×2行排列;图表区改为单列堆叠,ECharts图表宽度100%,高度自适应最小350px;表格列宽自适应 | +| 768-1023px(Pad竖屏) | 统计卡片2列×3行排列;图表区单列堆叠,ECharts图表宽度100%,高度最小300px,饼图改为环形图节省空间;表格启用横向滚动;筛选条件区折叠为展开/收起模式 | + --- ## 页面2:巡检统计页 @@ -91,6 +141,51 @@ |------|----------|------|----------|------| | 导出 | statistics:inspection:export | 操作栏 | 始终 | — | +### 交互流程要求 + +1. **页面加载流程**:页面加载时默认查询"本月"巡检统计数据,统计卡片区与图表区并发请求,渲染顺序:统计卡片→趋势折线图→异常率分布饼图→数据表格,加载中显示骨架屏 +2. **查询/筛选交互流程**:时间快捷按钮(今天/本周/本月/本年)点击即刷新;自定义日期范围确认后触发;班组、区域筛选变更自动刷新;补录数据"包含/排除"切换后立即重新查询 +3. **表单填写与提交流程**:无表单提交;导出操作点击后直接触发文件下载,按钮显示loading状态直到下载完成 +4. **弹窗/抽屉交互流程**:本页面无弹窗交互 +5. **行内操作流程**:数据表格行无行内操作,悬停高亮 +6. **异常与错误处理**:数据加载失败显示el-notification错误提示;无数据时展示空状态插图;导出失败提示"导出失败,请重试" +7. **联动/级联交互**:时间范围或筛选条件变更后统计卡片、图表、表格全区域联动刷新 +8. **权限控制交互表现**:无`statistics:inspection:export`权限时导出按钮隐藏 +9. **H1 防重复请求(强制)**:快捷时间按钮/筛选条件变更时立即disabled+loading;导出按钮点击后disabled+loading直到下载完成;查询触发时abort前一次未完成请求;卡片与图表区并发请求互不阻塞 +10. **H2 超时配置(强制)**:统计数据GET请求设置30s超时(统计档);导出操作设置60s超时(上传导出档);>3s展示全局ElLoading +11. **H6 批量限制(建议)**:单次导出上限500条记录,超出时提示"数据量过大,请缩小筛选范围" +12. **H8 反馈规范(建议)**:加载完成正常渲染无额外提示;导出成功ElMessage.success 2秒自动消失;加载失败区域提供"点击重试"按钮;网络异常提供重试 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 时间快捷按钮 | el-radio-group + el-radio-button | v-model="timeRange", size="default" | +| 自定义日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD" | +| 班组筛选 | el-select | v-model="filters.team", clearable | +| 区域筛选 | el-select | v-model="filters.area", clearable | +| 补录数据筛选 | el-switch | v-model="filters.includeSupplement", active-text="包含", inactive-text="排除" | +| 统计卡片 | el-card | shadow="hover", body-style="{padding: '20px'}" | +| 完成率趋势图 | echarts (折线图) | smooth=true, tooltip.trigger="axis" | +| 异常率分布图 | echarts (饼图) | tooltip.trigger="item" | +| 数据表格 | el-table | stripe, border, size="default" | +| 导出按钮 | el-button | type="success", icon="Download", :loading="exportLoading" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 自定义日期范围-起始日期 | 必须早于或等于结束日期 | 开始日期不能晚于结束日期 | +| 自定义日期范围-跨度 | 时间跨度不超过1年 | 查询时间范围不能超过1年 | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 统计卡片5列横排;图表区2列布局(趋势图+饼图),下方表格全宽 | +| 1024-1279px(Pad横屏) | 统计卡片3列+2列排列;图表区单列堆叠,ECharts图表宽度100%,高度最小350px;表格列宽自适应 | +| 768-1023px(Pad竖屏) | 统计卡片2列+3列排列;图表区单列堆叠,ECharts图表宽度100%,高度最小300px;表格启用横向滚动;筛选区折叠 | + --- ## 页面3:保洁统计页 @@ -109,6 +204,46 @@ | 抽查合格率 | 抽查合格数/抽查总数 | | 补录率 | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认查询"本月"时间范围 → 调用保洁统计API → 渲染统计卡片区(总任务数/完成率/超时率/抽查合格率/补录率)→ 图表区(趋势图+分布图)→ 数据表格 +2. **查询/筛选交互流程**:点击快捷时间按钮立即切换并重新请求;选择自定义日期范围确认后触发查询;班组/区域筛选变更后自动触发查询 +3. **表单填写与提交流程**:无表单提交;导出操作点击后触发下载 +4. **弹窗/抽屉交互流程**:本页面无弹窗交互 +5. **行内操作流程**:数据表格行无行内操作,悬停高亮;超时率列红色标记 +6. **异常与错误处理**:数据加载失败显示el-notification错误提示;无数据时展示空状态插图;导出失败提示"导出失败,请重试" +7. **联动/级联交互**:时间范围或筛选条件变更后全区域联动刷新 +8. **权限控制交互表现**:无 statistics:cleaning:export 权限时导出按钮隐藏 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 时间快捷按钮 | el-radio-group + el-radio-button | v-model="timeRange" | +| 自定义日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD" | +| 班组筛选 | el-select | clearable, placeholder="选择班组" | +| 区域筛选 | el-select | clearable, placeholder="选择区域" | +| 统计卡片 | el-card | shadow="hover", body-style="{padding:'20px'}" | +| 超时率趋势图 | echarts (折线图) | smooth=true, tooltip.trigger="axis" | +| 任务状态分布图 | echarts (饼图) | tooltip.trigger="item" | +| 数据表格 | el-table | stripe, border | +| 导出按钮 | el-button | type="success", icon="Download", :loading="exportLoading" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 自定义日期范围-起始日期 | 必须早于或等于结束日期 | "开始日期不能晚于结束日期" | +| 自定义日期范围-跨度 | 时间跨度不超过1年 | "查询时间范围不能超过1年" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 统计卡片5列横排;图表区2列布局;表格全宽展示 | +| 1024-1279px(Pad横屏) | 统计卡片3列+2列排列;图表区单列堆叠;表格列宽自适应 | +| 768-1023px(Pad竖屏) | 统计卡片2列+3列排列;图表区单列堆叠,高度最小300px;表格启用横向滚动;筛选区折叠 | + --- ## 页面4:评价统计页 @@ -127,6 +262,50 @@ | 低评分占比 | ≤2分评价占比 | | 回复率 | 已回复/总评价数 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认查询"本月"时间范围 → 调用评价统计API → 渲染统计卡片区(总评价数/平均评分/低评分占比/回复率)→ 图表区(各模块评分对比柱状图+星级分布饼图+评分趋势折线图)→ 数据表格 +2. **查询/筛选交互流程**:点击快捷时间按钮立即切换;自定义日期范围确认后触发;班组筛选变更后自动触发查询 +3. **表单填写与提交流程**:无表单提交;导出操作点击后触发下载 +4. **弹窗/抽屉交互流程**:本页面无弹窗交互 +5. **行内操作流程**:数据表格行无行内操作,悬停高亮;低分占比列红色标记 +6. **异常与错误处理**:数据加载失败显示el-notification错误提示;无数据时展示空状态插图;导出失败提示"导出失败,请重试" +7. **联动/级联交互**:时间范围或筛选条件变更后全区域联动刷新 +8. **权限控制交互表现**:无 statistics:evaluation:export 权限时导出按钮隐藏 +9. **H1 防重复请求(强制)**:快捷时间/筛选变更时disabled+loading态;导出按钮点击后disabled+loading;查询变更时abort前次未完成请求;各区域并发请求互不阻塞 +10. **H2 超时配置(强制)**:统计GET请求30s超时(统计档),导出60s超时(上传导出档);>3s展示全局ElLoading +11. **H6 批量限制(建议)**:单次导出上限500条,超出提示缩小范围 +12. **H8 反馈规范(建议)**:正常渲染无额外提示;导出成功2秒自动消失;加载失败区域可重试;网络异常提供重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 时间快捷按钮 | el-radio-group + el-radio-button | v-model="timeRange" | +| 自定义日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD" | +| 班组筛选 | el-select | clearable, placeholder="选择班组" | +| 统计卡片 | el-card | shadow="hover", body-style="{padding:'20px'}" | +| 各模块评分对比 | echarts (柱状图) | tooltip.trigger="axis", series.type="bar" | +| 星级分布图 | echarts (饼图) | tooltip.trigger="item", radius=["40%","70%"] | +| 评分趋势图 | echarts (折线图) | smooth=true, tooltip.trigger="axis" | +| 数据表格 | el-table | stripe, border | +| 导出按钮 | el-button | type="success", icon="Download", :loading="exportLoading" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 自定义日期范围-起始日期 | 必须早于或等于结束日期 | "开始日期不能晚于结束日期" | +| 自定义日期范围-跨度 | 时间跨度不超过1年 | "查询时间范围不能超过1年" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 统计卡片5列横排;图表区2列+1列布局;表格全宽展示 | +| 1024-1279px(Pad横屏) | 统计卡片3列+2列排列;图表区单列堆叠;表格列宽自适应 | +| 768-1023px(Pad竖屏) | 统计卡片2列+3列排列;图表区单列堆叠,高度最小300px;表格启用横向滚动;筛选区折叠 | + --- ## 页面5:考勤统计页 @@ -145,6 +324,49 @@ | 异常申诉率 | 申诉数/总打卡次数 | | 蓝牙打卡率 | 蓝牙打卡数/总打卡数 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认查询"本月"时间范围 → 调用考勤统计API → 渲染统计卡片区(出勤率/迟到率/早退率/异常申诉率/蓝牙打卡率)→ 图表区(出勤率趋势折线图+迟到早退分布图)→ 数据表格 +2. **查询/筛选交互流程**:点击快捷时间按钮立即切换;自定义日期范围确认后触发查询;班组筛选变更后自动触发查询 +3. **表单填写与提交流程**:无表单提交;导出操作点击后触发下载 +4. **弹窗/抽屉交互流程**:本页面无弹窗交互 +5. **行内操作流程**:数据表格行无行内操作,悬停高亮;迟到率/早退率列橙色标记 +6. **异常与错误处理**:数据加载失败显示el-notification错误提示;无数据时展示空状态插图;导出失败提示"导出失败,请重试" +7. **联动/级联交互**:时间范围或筛选条件变更后全区域联动刷新 +8. **权限控制交互表现**:无 statistics:attendance:export 权限时导出按钮隐藏 +9. **H1 防重复请求(强制)**:快捷时间/筛选变更时disabled+loading态;导出按钮点击后disabled+loading;查询变更时abort前次未完成请求;各区域并发请求互不阻塞 +10. **H2 超时配置(强制)**:统计GET请求30s超时(统计档),导出60s超时(上传导出档);>3s展示全局ElLoading +11. **H6 批量限制(建议)**:单次导出上限500条,超出提示缩小范围 +12. **H8 反馈规范(建议)**:正常渲染无额外提示;导出成功2秒自动消失;加载失败区域可重试;网络异常提供重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 时间快捷按钮 | el-radio-group + el-radio-button | v-model="timeRange" | +| 自定义日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD" | +| 班组筛选 | el-select | clearable, placeholder="选择班组" | +| 统计卡片 | el-card | shadow="hover", body-style="{padding:'20px'}" | +| 出勤率趋势图 | echarts (折线图) | smooth=true, tooltip.trigger="axis" | +| 迟到早退分布图 | echarts (柱状图) | tooltip.trigger="axis", series.type="bar" | +| 数据表格 | el-table | stripe, border | +| 导出按钮 | el-button | type="success", icon="Download", :loading="exportLoading" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 自定义日期范围-起始日期 | 必须早于或等于结束日期 | "开始日期不能晚于结束日期" | +| 自定义日期范围-跨度 | 时间跨度不超过1年 | "查询时间范围不能超过1年" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 统计卡片5列横排;图表区2列布局;表格全宽展示 | +| 1024-1279px(Pad横屏) | 统计卡片3列+2列排列;图表区单列堆叠;表格列宽自适应 | +| 768-1023px(Pad竖屏) | 统计卡片2列+3列排列;图表区单列堆叠,高度最小300px;表格启用横向滚动;筛选区折叠 | + --- ## 页面6:综合看板页 @@ -176,6 +398,46 @@ └──────────────────────────────────────────────────────────────────┘ ``` +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认查询"本月"时间范围 → 并行调用各模块汇总API → 渲染6个统计卡片(报修完成率/巡检完成率/保洁完成率/考勤出勤率/评价均分/待处理工单)→ 趋势图(各模块指标趋势对比折线图) +2. **查询/筛选交互流程**:点击快捷时间按钮(今天/本周/本月/自定义)→ 立即切换并重新请求所有数据;自定义日期范围确认后触发查询 +3. **表单填写与提交流程**:本页无表单提交 +4. **弹窗/抽屉交互流程**:点击统计卡片 → 跳转到对应模块的统计详情页 +5. **行内操作流程**:点击卡片底部"查看详情"链接 → 跳转对应模块统计页 +6. **异常与错误处理**:各卡片数据独立加载,某模块加载失败仅该卡片显示"加载失败";趋势图加载失败显示"加载失败,点击重试";全部加载失败显示整页错误提示 +7. **联动/级联交互**:时间范围变更后所有卡片和趋势图联动刷新 +8. **权限控制交互表现**:无某模块统计查看权限时对应卡片显示"无权限查看" +9. **H1 防重复请求(强制)**:快捷时间切换/自定义日期确认后立即disabled+loading态;时间变更触发全量刷新时abort前一次所有未完成并行请求;各模块API独立并发加载,单模块失败不阻塞其他模块渲染 +10. **H2 超时配置(强制)**:各模块汇总GET请求设置30s超时(统计档);>3s时对尚未返回的卡片区域展示局部ElLoading;全部超时时显示全局重试入口 +11. **H8 反馈规范(建议)**:各卡片数据正常加载完成后直接渲染数值和趋势箭头,无需额外成功提示;单卡片加载失败显示"点击重试";趋势图加载失败显示"加载失败,点击重试"及重试按钮;网络异常时提供全局重试入口 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 时间快捷按钮 | el-radio-group + el-radio-button | v-model="timeRange" | +| 自定义日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD" | +| 统计卡片 | el-card | shadow="hover", body-style="{padding:'20px'}", @click="goDetail" | +| 卡片数值 | 自定义组件 | 大字号数字+趋势箭头(↑↓)+百分比变化 | +| 趋势图 | echarts (多折线图) | 多条折线对比各模块,smooth=true, tooltip.trigger="axis", legend.data=[报修,巡检,保洁,考勤,评价] | +| 查看详情链接 | el-link | type="primary", :underline="false" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 自定义日期范围-起始日期 | 必须早于或等于结束日期 | "开始日期不能晚于结束日期" | +| 自定义日期范围-跨度 | 时间跨度不超过1年 | "查询时间范围不能超过1年" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 统计卡片3列×2行排列;趋势图全宽展示 | +| 1024-1279px(Pad横屏) | 统计卡片3列×2行排列,卡片字号略减;趋势图全宽展示 | +| 768-1023px(Pad竖屏) | 统计卡片2列×3行排列;趋势图全宽展示,高度最小300px;时间选择区折叠为下拉 | + --- ## 页面7:自定义报表页 @@ -220,6 +482,57 @@ | 保存模板 | /api/v1/statistics/custom/templates | POST | — | | 导出 | /api/v1/statistics/custom/export | GET | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 加载配置区下拉选项(数据源/维度/指标列表);若存在已保存的模板,在配置区下方展示"最近使用模板"快捷入口 +2. **查询/筛选交互流程**:选择数据源、维度、指标、时间范围、添加筛选条件 → 点击[生成报表] → 调用自定义报表API POST /api/v1/statistics/custom → 渲染结果展示区(图表+数据表格) +3. **表单填写与提交流程**:配置报表参数 → 点击[生成报表] → 加载中显示骨架屏 → 渲染结果;点击[保存模板] → 弹窗输入模板名称 → 保存当前配置;点击[导出] → 触发下载 +4. **弹窗/抽屉交互流程**:保存模板弹窗宽度420px,输入模板名称;点击遮罩层可关闭 +5. **行内操作流程**:动态添加筛选条件行 → 选择字段+运算符+值 → 可删除已有条件行;生成报表后图表区可交互(缩放、筛选、下钻) +6. **异常与错误处理**:数据源和维度至少选一个,否则提示"请选择数据源和维度";指标至少选一个,否则提示"请至少选择一个指标";生成失败显示ElMessage.error;导出失败提示"导出失败,请重试" +7. **联动/级联交互**:数据源选择后联动可用维度和指标列表;维度选择后联动图表类型推荐;筛选条件中字段选择后联动运算符和值选项 +8. **权限控制交互表现**:无 statistics:custom:view 权限时[生成报表]按钮隐藏;无 statistics:custom:create 权限时[保存模板]按钮隐藏;无 statistics:custom:export 权限时[导出]按钮隐藏 +9. **H1 防重复请求(强制)**:"生成报表"按钮点击后立即disabled+loading直到结果返回或超时;"保存模板""导出"按钮点击后disabled+loading防重复;筛选条件行动态添加/删除不影响已生成的报表结果 +10. **H2 超时配置(强制)**:生成报表POST请求设置30s超时(统计档);保存模板POST请求30s超时;导出GET请求60s超时(上传导出档);>3s时展示全局ElLoading +11. **H6 批量限制(建议)**:导出操作上限500条记录,超出提示"数据量过大,请缩小维度范围后重新生成" +12. **H8 反馈规范(建议)**:生成报表成功后图表+表格正常渲染无额外成功提示;保存模板成功ElMessage.success("模板保存成功")2秒自动消失;导出成功2秒自动消失;校验错误/生成失败需手动关闭;网络异常提供重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 数据源选择 | el-select | v-model="config.dataSources", multiple, collapse-tags, placeholder="请选择数据源" | +| 维度选择 | el-select | v-model="config.dimensions", multiple, collapse-tags, placeholder="请选择维度" | +| 指标选择 | el-select | v-model="config.metrics", multiple, collapse-tags, placeholder="请选择指标" | +| 时间范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD", range-separator="至" | +| 筛选条件添加 | el-button | type="primary", link, icon="Plus", @click="addFilter" | +| 筛选条件行 | 自定义组件 | 字段el-select + 运算符el-select + 值el-input/el-select + 删除按钮 | +| 生成报表按钮 | el-button | type="primary", :loading="generating" | +| 保存模板按钮 | el-button | type="success" | +| 导出按钮 | el-button | type="success", icon="Download", :disabled="!hasResult" | +| 结果图表区 | echarts | 动态渲染,根据维度/指标配置图表类型 | +| 结果表格 | el-table | stripe, border, :data="resultData" | +| 保存模板弹窗 | el-dialog | title="保存模板", width="420px" | +| 模板名称 | el-input | maxlength=30, placeholder="请输入模板名称" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 数据源 | 至少选1个 | "请选择数据源" | +| 维度 | 至少选1个 | "请选择维度" | +| 指标 | 至少选1个 | "请至少选择一个指标" | +| 时间范围 | 必选,跨度不超过1年 | "请选择时间范围" / "查询时间范围不能超过1年" | +| 模板名称 | 保存时必填,maxlength=30 | "请输入模板名称" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 配置区水平排列,结果区2列布局(图表+表格) | +| 1024-1279px(Pad横屏) | 配置区换行排列,结果区单列堆叠 | +| 768-1023px(Pad竖屏) | 配置区垂直堆叠,使用el-collapse折叠面板组织配置项;结果区单列堆叠;操作按钮full-width堆叠 | + --- ## 需求追溯 diff --git a/docs/02-功能清单-物业公司/08-操作日志.md b/docs/02-功能清单-物业公司/08-操作日志.md index d8aef56..5650920 100644 --- a/docs/02-功能清单-物业公司/08-操作日志.md +++ b/docs/02-功能清单-物业公司/08-操作日志.md @@ -3,6 +3,7 @@ > 模块编码:audit-log > 端侧:Web专属 > 关联文档:01-模块划分 §3.8 / 02-功能清单-物业公司 §8 / 03-业务流转逻辑-物业公司 §8 / 05-接口规范 §9.2 / 06-项目技术要求 §4.5 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -62,6 +63,54 @@ |----------|---------|------|------| | 时间轴查询 | /api/v1/audit-logs/timeline | GET | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认日期范围为今天 → 调用时间轴查询API GET /api/v1/audit-logs/timeline → 渲染时间轴视图,按时间倒序排列;同时加载模块下拉选项 +2. **查询/筛选交互流程**:选择模块/输入操作人/选择日期范围 → 点击[查询] → 重新加载时间轴数据 → 点击[重置]恢复默认条件(今天/全部模块);支持快捷日期(今天/昨天/近7天) +3. **表单填写与提交流程**:本页无表单提交操作,仅查询展示 +4. **弹窗/抽屉交互流程**:点击[查看详情] → 调用详情API GET /api/v1/audit-logs/{id} → 弹窗展示完整日志信息(操作人、时间、IP、模块路径、操作内容、变更前后数据、请求参数、响应状态),宽度700px +5. **行内操作流程**:点击时间轴节点的[查看详情] → 弹窗展示;向下滚动到底部自动加载更多(无限滚动v-infinite-scroll) +6. **异常与错误处理**:时间轴数据加载失败显示"加载失败,点击重试";无数据时显示ElEmpty"暂无日志记录";日期范围超过90天提示"查询时间范围不能超过90天";网络异常提示ElMessage.error +7. **联动/级联交互**:模块筛选影响时间轴数据过滤;日期范围变更后时间轴重新渲染;快捷日期按钮联动日期范围选择器 +8. **权限控制交互表现**:无 audit-log:list:view 权限时页面不可见;物业管理员仅查看本公司绑定医院的操作日志 +9. **[H1] 防重复请求**:查询按钮点击后立即 disabled + loading 状态,防止重复提交;时间轴无限滚动触发 loadMore 时节流处理(500ms 内不重复请求);查看详情弹窗打开时,当前行操作按钮禁用 +10. **[H2] 超时控制**:时间轴查询 API GET /api/v1/audit-logs/timeline 设置超时 15s,超过时提示"查询超时,请稍后重试";详情 API GET /api/v1/audit-logs/{id} 设置超时 15s;请求耗时 >3s 时显示全局 Loading 指示器 +11. **[H3] 操作确认**:本页主要为只读展示,暂无删除/停用等危险操作需二次确认 +12. **[H8] 反馈机制**:查询成功后静默刷新数据(silent),不弹成功提示;加载失败显示 ElMessage.error("加载失败,请重试",持续显示需手动关闭);详情加载成功 2s 后自动消失的 ElMessage.success 提示 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 模块筛选 | el-select | clearable, placeholder="全部模块" | +| 操作人输入 | el-input | clearable, maxlength=20, placeholder="请输入操作人" | +| 日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD", range-separator="至" | +| 快捷日期 | el-radio-group + el-radio-button | v-model="quickDate" | +| 时间轴 | el-timeline | :reverse=false, v-infinite-scroll="loadMore" | +| 时间轴节点 | el-timeline-item | :timestamp="item.time", placement="top" | +| 模块标签 | el-tag | type="primary", size="small" | +| 查看详情按钮 | el-button | type="primary", link, size="small" | +| 详情弹窗 | el-dialog | title="操作日志详情", width="700px" | +| 详情展示 | el-descriptions | :column="2", border | +| 变更前数据 | el-input | type="textarea", :rows="4", readonly, v-if="beforeData" | +| 变更后数据 | el-input | type="textarea", :rows="4", readonly, v-if="afterData" | +| 响应状态标签 | el-tag | 成功=success / 失败=danger | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 操作人 | 非必填,maxlength=20 | — | +| 日期范围 | 最多90天 | "查询时间范围不能超过90天" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件一行;时间轴居中展示(最大宽度900px);详情弹窗700px | +| 1024-1279px(Pad横屏) | 查询条件两行;时间轴宽度100%;详情弹窗650px | +| 768-1023px(Pad竖屏) | 查询条件竖向堆叠;时间轴全宽;详情弹窗95vw | + --- ## 页面2:日志列表页 @@ -124,6 +173,55 @@ | 列表查询 | /api/v1/audit-logs | GET | 分页查询 | | 详情 | /api/v1/audit-logs/{id} | GET | 含完整信息 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 默认日期范围为今天 → 调用列表查询API GET /api/v1/audit-logs → 渲染表格,默认按操作时间倒序排列,每页20条;同时加载模块下拉、操作类型下拉选项 +2. **查询/筛选交互流程**:选择模块/输入操作人/选择操作类型(多选,支持全选/反选)/选择日期范围 → 点击[查询] → 重新加载数据 → 点击[重置]恢复默认条件 +3. **表单填写与提交流程**:本页无表单提交操作,仅查询展示 +4. **弹窗/抽屉交互流程**:点击[查看详情] → 调用详情API GET /api/v1/audit-logs/{id} → 弹窗展示完整日志信息(操作人+班组、操作时间、操作IP、操作模块路径、操作内容、变更前/后数据JSON、请求参数、响应状态),宽度700px +5. **行内操作流程**:点击[查看详情] → 弹窗展示;变更前后数据使用JSON格式化高亮展示 +6. **异常与错误处理**:列表数据为空时显示ElEmpty"暂无操作日志";详情加载失败提示"详情加载失败";日期范围超过90天提示"查询时间范围不能超过90天";API请求失败显示ElMessage.error +7. **联动/级联交互**:操作类型多选筛选与模块筛选可组合使用;分页切换保持筛选条件 +8. **权限控制交互表现**:无 audit-log:list:view 权限时页面不可见;物业管理员仅查看本公司绑定医院的操作日志 +9. **[H1] 防重复请求**:查询按钮点击后 disabled + loading,防止重复提交;分页切换时 abort 前一次未完成的列表请求再发新请求;行内"查看详情"按钮点击后禁用当前行操作,弹窗关闭后恢复 +10. **[H2] 超时控制**:列表查询 GET /api/v1/audit-logs 超时 15s;详情 GET /api/v1/audit-logs/{id} 超时 15s;请求 >3s 显示全局 v-loading 全屏遮罩 +11. **[H3] 操作确认**:本页为只读列表,暂无删除等危险操作需确认 +12. **[H8] 反馈机制**:查询成功静默刷新(silent);失败 ElMessage.error 持续显示手动关闭;详情加载成功 ElMessage.success 2s 自动消失 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 模块筛选 | el-select | clearable, placeholder="全部模块" | +| 操作人输入 | el-input | clearable, maxlength=20, placeholder="请输入操作人" | +| 操作类型筛选 | el-select | v-model="query.types", multiple, collapse-tags, collapse-tags-tooltip, filterable, placeholder="全部类型" | +| 日期范围 | el-date-picker | type="daterange", value-format="YYYY-MM-DD", range-separator="至" | +| 列表 | el-table | stripe, border, :data="tableData", v-loading | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next", :page-sizes="[10,20,50,100]" | +| 操作类型标签 | el-tag | 查看=info / 新增=success / 编辑=primary / 删除=danger / 审批=warning / 导出=info / 分配=primary | +| 响应状态标签 | el-tag | 成功=success / 失败=danger | +| 查看详情按钮 | el-button | type="primary", link | +| 详情弹窗 | el-dialog | title="操作日志详情", width="700px", :close-on-click-modal="true" | +| 详情展示 | el-descriptions | :column="2", border | +| 变更前数据 | el-input | type="textarea", :rows="6", readonly, class="json-viewer" | +| 变更后数据 | el-input | type="textarea", :rows="6", readonly, class="json-viewer" | +| 请求参数 | el-input | type="textarea", :rows="4", readonly | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 操作人 | 非必填,maxlength=20 | — | +| 日期范围 | 最多90天 | "查询时间范围不能超过90天" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区水平排列,表格完整展示全部7列,详情弹窗700px | +| 1024-1279px(Pad横屏) | 查询条件区换行排列,表格隐藏"模块"列,详情弹窗650px | +| 768-1023px(Pad竖屏) | 查询条件区垂直堆叠,表格仅显示操作时间、操作人、操作类型、操作内容、操作列,详情弹窗95vw | + --- ## 页面3:日志导出 @@ -138,6 +236,45 @@ |------|----------|------|----------|------| | 导出Excel | audit-log:list:export | 列表页操作栏 | 始终 | 导出当前筛选结果 | +### 交互流程要求 + +1. **页面加载流程**:日志导出为日志列表页的操作功能,非独立页面;进入日志列表页时加载导出按钮 +2. **查询/筛选交互流程**:导出数据范围跟随列表页当前筛选条件 +3. **表单填写与提交流程**:在日志列表页点击[导出Excel] → 弹出导出确认弹窗(确认导出当前筛选条件下的数据)→ 确认 → 后端异步生成Excel → 生成完毕自动下载 +4. **弹窗/抽屉交互流程**:导出确认弹窗宽度400px,展示当前筛选条件和预估数据量;点击遮罩层可关闭 +5. **行内操作流程**:大量数据导出时显示进度条,支持取消操作 +6. **异常与错误处理**:导出失败提示"导出失败,请稍后重试";数据量超10万条提示"数据量过大,请缩小查询范围";导出超时提示"导出超时,请缩小查询范围后重试" +7. **联动/级联交互**:导出数据范围与列表页筛选条件联动 +8. **权限控制交互表现**:无 audit-log:list:export 权限时导出按钮隐藏 +9. **[H1] 防重复请求**:导出按钮点击后 disabled + loading 状态,防止重复点击;导出进行中时禁用查询/筛选等可能改变数据范围的操作 +10. **[H2] 超时控制**:导出异步任务超时设置 60s(大文件生成);导出确认弹窗 API 调用超时 15s;>3s 显示全局 Loading +11. **[H3] 操作确认**:导出前必须通过确认弹窗(type=warning),弹窗内容须包含当前筛选条件和预估数据量,用户明确确认后才开始导出 +12. **[H8] 反馈机制**:导出成功 ElMessage.success("导出成功",2s 自动消失);导出失败 ElMessage.error 持续显示手动关闭;取消导出不提示 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 导出按钮 | el-button | type="success", icon="Download", plain, :loading="exportLoading" | +| 确认弹窗 | el-message-box | type="warning", confirmButtonText="确认导出", cancelButtonText="取消" | +| 导出进度弹窗 | el-dialog | title="导出中", width="400px", :close-on-click-modal="false", :show-close="false" | +| 进度条 | el-progress | :percentage="exportProgress", :stroke-width="6" | +| 取消导出按钮 | el-button | type="danger", @click="cancelExport" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| — | — | — | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 导出按钮在列表页操作栏展示,弹窗宽度400px | +| 1024-1279px(Pad横屏) | 导出按钮缩小为small尺寸 | +| 768-1023px(Pad竖屏) | 导出按钮收入"更多"下拉菜单,弹窗宽度90vw | + --- ## 页面4:数据补录日志页 @@ -179,6 +316,41 @@ |----------|---------|------|------| | 补录日志 | /api/v1/audit-logs/supplement | GET | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用API加载补录日志列表 +2. **查询交互**:填写条件 → 查询 → 重置到第1页 +3. **行内操作**:点击"查看详情"→ 弹窗展示补录详情(补录原因、原始数据、补录后数据、审核信息) +4. **异常处理**:无数据时显示空状态 +5. **权限控制**:无 `audit-log:supplement:view` → 页面不可见 +6. **[H1] 防重复请求**:查询按钮点击后 disabled + loading,防止重复提交;分页切换时 abort 前次未完成请求;行内"查看详情"点击后禁用当前行,弹窗关闭后恢复 +7. **[H2] 超时控制**:列表查询 GET /api/v1/audit-logs/supplement 超时 15s;详情加载超时 15s;>3s 全局 Loading +8. **[H3] 操作确认**:本页为只读查看,暂无删除/停用等危险操作需二次确认 +9. **[H8] 反馈机制**:查询成功静默刷新(silent);失败 ElMessage.error 手动关闭;详情加载成功 2s 自动消失提示 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 列表 | el-table | stripe, border, v-loading | +| 审核状态标签 | el-tag | 待审核:type="warning", 通过:type="success", 驳回:type="danger" | +| 补录原因标签 | el-tag | type="info", size="small" | +| 详情弹窗 | el-dialog | width="600px" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 日期范围 | 最多90天 | "查询时间范围不能超过90天" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px | 查询条件一行;列表完整展示 | +| 1024-1279px | 查询条件两行;"审核人""审核时间"列隐藏 | +| 768-1023px | 查询条件竖向堆叠;列表仅显示:补录时间、补录人、模块、审核状态、操作 | + --- ## 需求追溯 diff --git a/docs/02-功能清单-物业公司/09-系统配置.md b/docs/02-功能清单-物业公司/09-系统配置.md index 6dc74bc..e0a928f 100644 --- a/docs/02-功能清单-物业公司/09-系统配置.md +++ b/docs/02-功能清单-物业公司/09-系统配置.md @@ -3,6 +3,7 @@ > 模块编码:system > 端侧:Web专属 > 关联文档:01-模块划分 §3.9 / 02-功能清单-物业公司 §9 / 03-业务流转逻辑-物业公司 §9 / 05-接口规范 §9.2 / 06-项目技术要求 §4.4 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -98,6 +99,50 @@ | 解绑 | /api/v1/beacons/{id}/unbind | PUT | — | | 删除 | /api/v1/beacons/{id} | DELETE | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用API加载Beacon列表 + 绑定位置级联数据 +2. **查询交互**:填写条件 → 查询 → 重置到第1页 +3. **新增/编辑Beacon**:点击"新增Beacon"→ 弹窗 → 填写表单 → 保存 → 刷新列表 +4. **解绑操作**:点击"解绑"→ 二次确认 → API调用 → 刷新列表 +5. **删除操作**:仅未绑定位置时可删除 → 二次确认 → API调用 → 刷新列表 +6. **联动交互**:绑定位置使用级联选择器(楼栋→楼层→区域→点位) +7. **异常处理**:UUID格式错误即时提示;删除已绑定Beacon提示"请先解绑" +8. **权限控制**:无对应权限 → 操作按钮不渲染 +9. **[H1] 防重复请求**:查询/新增/编辑/解绑按钮点击后 disabled + loading;分页切换 abort 前次未完成请求;行内操作(编辑/解绑/删除)点击后禁用当前行,弹窗/确认关闭后恢复 +10. **[H2] 超时控制**:列表 GET /api/v1/beacons 超时 15s;新增 POST /api/v1/beacons 超时 30s;编辑 PUT 超时 30s;删除 DELETE 超时 30s;>3s 全局 Loading +11. **[H3] 操作确认**:删除操作必须二次确认 ElMessageBox.confirm(type=error),文案含"确定删除该Beacon设备吗?此操作不可恢复";解绑操作二次确认(type=warning),文案含"确定解绑该Beacon的绑定位置吗?" +12. **[H4] 脏数据检测**:新增/编辑弹窗打开时 deep clone 表单初始值;表单字段变更后计算 isDirty;用户尝试关闭弹窗时若 isDirty 为 true 则拦截并提示"修改尚未保存,确定离开吗?" +13. **[H8] 反馈机制**:新增/编辑成功 ElMessage.success 2s 自动消失 + silent 刷新列表;失败 ElMessage.error 手动关闭;删除/解绑成功 2s 提示后刷新 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 列表 | el-table | stripe, border, v-loading | +| 状态标签 | el-tag | 在线:type="success", 离线:type="danger" | +| 电量 | el-progress | :percentage, :stroke-width=10, <20%:color="red" | +| 新增/编辑弹窗 | el-dialog | width="500px" | +| 绑定位置级联 | el-cascader | :options="locationTree", :props="{checkStrictly:true}" | +| 绑定模块下拉 | el-select | :options="[{label:'巡检',value:'inspection'},{label:'保洁',value:'cleaning'},{label:'考勤',value:'attendance'}]" | +| 电量预警阈值 | el-input-number | :min=0, :max=100, :step=5 | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| Beacon名称 | 必填, 最大30字符 | "请输入Beacon名称" / "名称不能超过30字符" | +| UUID | 必填, UUID格式 | "请输入UUID" / "UUID格式不正确" | +| 电量预警阈值 | 0-100整数 | "阈值范围为0-100" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px | 查询条件一行;列表完整展示 | +| 1024-1279px | 查询条件两行;"绑定模块""电量"列隐藏 | +| 768-1023px | 查询条件竖向堆叠;列表仅显示:名称、绑定位置、状态、操作 | + --- ## 页面2:字典管理页 @@ -142,6 +187,42 @@ | 新增类型 | /api/v1/dict-types | POST | — | | 新增值 | /api/v1/dict-types/{typeCode}/values | POST | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用API加载字典类型列表 → 默认选中第一个类型 → 加载对应字典值 +2. **左侧类型交互**:点击类型 → 右侧加载对应字典值列表 +3. **新增类型**:点击"新增类型"→ 弹窗输入类型名称和编码 → 保存 → 刷新左侧列表 +4. **新增字典值**:点击"新增字典值"→ 弹窗输入字典值和排序 → 保存 → 刷新右侧列表 +5. **启用/停用**:被业务数据引用的字典值不可停用 → 提示"该字典值正在使用中,无法停用" +6. **联动交互**:字典值变更后,引用该字典的下拉选项实时刷新 +7. **异常处理**:类型编码重复 → 提示"该编码已存在" + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 左右分栏 | el-container | 左侧宽度250px | +| 字典类型列表 | el-menu | :default-active, @select | +| 右侧字典值列表 | el-table | stripe, border | +| 状态标签 | el-tag | 启用:type="success", 停用:type="danger" | +| 新增弹窗 | el-dialog | width="400px" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 类型名称 | 必填, 最大30字符 | "请输入类型名称" | +| 类型编码 | 必填, 字母数字下划线, 唯一 | "请输入类型编码" / "该编码已存在" | +| 字典值 | 必填, 最大50字符 | "请输入字典值" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px | 左右分栏,左侧250px | +| 1024-1279px | 左右分栏,左侧200px | +| 768-1023px | 上下分栏,类型列表改为横向tab切换 + --- ## 页面3:微信配置页 @@ -175,6 +256,45 @@ | 保存 | /api/v1/system/wechat-config | PUT | — | | 测试连接 | /api/v1/system/wechat-config/test | POST | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用API加载当前微信配置 → 回填表单(Secret字段脱敏显示) +2. **表单提交流程**:修改配置 → 点击"保存"→ 前端校验 → API调用 → 成功提示 +3. **测试连接**:点击"测试连接"→ 调用API → 成功提示"连接成功" / 失败提示具体错误 +4. **安全交互**:AppSecret字段默认脱敏,点击"显示"按钮后明文显示 +5. **异常处理**:配置校验失败即时提示;测试连接失败提示具体原因 +6. **[H1] 防重复请求**:"保存"按钮点击后 disabled + loading,防止重复提交;"测试连接"按钮同样 disabled + loading +7. **[H2] 超时控制**:查询 GET /api/v1/system/wechat-config 超时 15s;保存 PUT 超时 30s;测试连接 POST 超时 30s;>3s 全局 Loading +8. **[H4] 脏数据检测**:页面加载时 deep clone 原始配置数据为原始快照;表单字段变更后与快照对比计算 isDirty;用户尝试离开路由时若 isDirty 通过 beforeRouteLeave 拦截并提示"配置尚未保存,确定离开吗?" +9. **[H8] 反馈机制**:保存成功 ElMessage.success("保存成功",2s 自动消失);测试连接成功/失败均 2s 提示自动消失;失败 ElMessage.error 手动关闭 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 表单 | el-form | label-width="140px", :model, :rules | +| AppID | el-input | clearable | +| AppSecret | el-input | show-password, type="password" | +| 服务器地址 | el-input | clearable | +| 保存按钮 | el-button | type="primary", :loading | +| 测试连接按钮 | el-button | type="success", plain, :loading | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 小程序AppID | 必填 | "请输入小程序AppID" | +| 小程序AppSecret | 必填 | "请输入小程序AppSecret" | +| 服务器地址 | 必填, URL格式 | "请输入服务器地址" / "地址格式不正确" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px | 表单标签140px,输入区400px | +| 1024-1279px | 表单标签120px,输入区350px | +| 768-1023px | 表单标签100px,输入区100% | + --- ## 页面4:消息模板管理页 @@ -211,6 +331,40 @@ | 列表查询 | /api/v1/message-templates | GET | — | | 编辑 | /api/v1/message-templates/{id} | PUT | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用API加载模板列表 +2. **编辑模板**:点击"编辑"→ 弹窗回填模板内容 → 修改 → 保存 → 刷新列表 +3. **模板变量**:编辑时显示可用变量列表,点击变量自动插入到模板内容中 +4. **异常处理**:模板ID重复 → 提示"该模板ID已存在" + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 列表 | el-table | stripe, border | +| 状态标签 | el-tag | 启用:type="success", 停用:type="danger" | +| 编辑弹窗 | el-dialog | width="600px" | +| 模板内容 | el-input | type="textarea", :rows=6, maxlength=500, show-word-limit | +| 变量列表 | el-tag | v-for, type="info", size="small", @click="insertVariable" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 模板名称 | 必填, 最大50字符 | "请输入模板名称" | +| 模板ID | 必填 | "请输入模板ID" | +| 适用场景 | 必填 | "请选择适用场景" | +| 模板内容 | 必填, 最大500字符 | "请输入模板内容" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px | 列表完整展示;编辑弹窗600px | +| 1024-1279px | "模板变量"列隐藏;编辑弹窗550px | +| 768-1023px | 列表仅显示:模板名称、适用场景、状态、操作;编辑弹窗90vw | + --- ## 页面5:蓝牙策略配置页 @@ -282,6 +436,46 @@ | 保存 | /api/v1/system/bluetooth-policy | PUT | — | | 提交审核 | /api/v1/system/bluetooth-policy/submit | POST | — | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用API加载当前蓝牙策略 → 回填各场景配置 +2. **策略配置交互**:每个场景行内下拉选择策略(REQUIRED/OPTIONAL)→ 实时保存为草稿 +3. **保存草稿**:点击"保存"→ 调用API → 成功提示"草稿已保存" +4. **提交审核**:点击"提交审核"→ 二次确认("提交后需医院账号审核,确认提交?")→ 调用API → 成功提示"已提交审核"→ 按钮变为"审核中"不可再次点击 +5. **审核状态展示**:待审核时显示"审核中"标签;审核通过显示"已生效";审核拒绝显示拒绝原因+重新编辑按钮 +6. **联动交互**:选择"强制蓝牙"策略时,显示补录说明"蓝牙连接失败时自动进入补录流程" +7. **异常处理**:保存失败提示错误;提交审核时策略未变更提示"策略未修改" +8. **[H1] 防重复请求**:"保存"按钮点击后 disabled + loading;"提交审核"按钮点击后 disabled + loading 且提交中状态不可重复点击 +9. **[H2] 超时控制**:查询 GET /api/v1/system/bluetooth-policy 超时 15s;保存 PUT 超时 30s;提交审核 POST 超时 30s;>3s 全局 Loading +10. **[H4] 脏数据检测**:页面加载时 deep clone 当前策略配置为原始快照;任一场景策略下拉变更后计算 isDirty;用户尝试离开路由时若 isDirty 通过 beforeRouteLeave 拦截并提示"策略配置有未保存的修改,确定离开吗?" +11. **[H8] 反馈机制**:保存成功 ElMessage.success("草稿已保存",2s);提交审核成功 2s 后按钮变为"审核中";失败 ElMessage.error 手动关闭 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 策略表格 | el-table | :data="policies", border | +| 策略下拉 | el-select | :options="[{label:'强制蓝牙',value:'REQUIRED'},{label:'非强制',value:'OPTIONAL'}]" | +| 补录说明 | el-alert | type="info", :closable=false, v-if="policy==='REQUIRED'" | +| 审核状态标签 | el-tag | 审核中:type="warning", 已生效:type="success", 已拒绝:type="danger" | +| 保存按钮 | el-button | type="primary", :loading | +| 提交审核按钮 | el-button | type="success", :loading, :disabled="审核中" | +| 拒绝原因 | el-alert | type="error", :closable=false, v-if="rejected" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 策略配置 | 每个场景必须选择 | "请完成所有场景的策略配置" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px | 策略表格全宽 | +| 1024-1279px | 策略表格"说明"列隐藏 | +| 768-1023px | 策略表格改为卡片列表模式,每场景一个卡片 | + --- ## 需求追溯 diff --git a/docs/02-功能清单-超级管理员/01-账号管理.md b/docs/02-功能清单-超级管理员/01-账号管理.md index 5658bba..bca9fdf 100644 --- a/docs/02-功能清单-超级管理员/01-账号管理.md +++ b/docs/02-功能清单-超级管理员/01-账号管理.md @@ -3,6 +3,7 @@ > 模块编码:account > 端侧:Web专属(仅超级管理员) > 关联文档:01-模块划分 §1.1~1.4 / 02-功能清单-超级管理员 §1 / 03-业务流转逻辑-超级管理员 §1~3 / 05-接口规范 §9.2 / 06-项目技术要求 §4.1~4.3 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -76,12 +77,83 @@ |------|----------|----------|------| | 超级管理员 | 全部按钮 | 全部医院 | — | -### API端点 - -| 页面操作 | API路径 | 方法 | 说明 | -|----------|---------|------|------| -| 列表查询 | /api/v1/hospitals | GET | 分页查询 | -| 启用/停用 | /api/v1/hospitals/{id}/toggle-status | PUT | 切换状态 | +### 交互流程要求 + +1. **页面加载流程** + - 进入页面 → 调用 `GET /api/v1/hospitals` 加载列表数据(默认第1页,每页20条,创建时间倒序) + - 并行加载下拉选项:状态下拉(启用/停用) + - 列表为空时显示空状态插图 + "暂无医院信息" 提示文字 + - 加载中显示 `el-table` 骨架屏(skeleton)效果 + +2. **查询交互流程** + - 用户填写查询条件 → 点击"查询"按钮 → 前端校验无特殊限制 → 调用API携带筛选参数 → 重新渲染列表(重置到第1页) + - 点击"重置"→ 清空所有查询条件 → 重新加载默认列表 + - 支持回车键触发查询 + +3. **行内操作流程** + - **编辑**:点击"编辑"→ 路由跳转至 `/account/hospitals/:id/edit` + - **启用/停用**:点击"启用"或"停用"→ 弹出 `el-message-box` 二次确认("确认停用XX医院?停用后该医院下所有账号将无法登录")→ 确认后调用 `PUT /api/v1/hospitals/{id}/toggle-status` → 成功后 `el-message` 提示"操作成功" → 刷新当前列表 + +4. **异常处理** + - API请求失败 → `el-message.error` 提示错误信息,列表保持原数据 + - 网络超时 → 提示"网络异常,请稍后重试" + - 启停操作失败 → 提示具体错误原因(如"该医院下存在进行中的工单,无法停用") + +5. **权限控制交互** + - 无 `permission:user:create` 权限 → "新增医院"按钮不渲染 + - 无 `permission:user:update` 权限 → 行操作列不渲染"编辑"和"启用/停用"按钮 + +6. **[H1]防重复请求** + - 查询/筛选:点击"查询"按钮后立即 disabled=true + 显示loading态,API返回后恢复;查询期间再次点击无效 + - 行内操作(编辑/启停):点击操作按钮后该行所有操作按钮禁用 + 按钮显示loading旋转图标;操作完成后恢复 + - 分页切换:切换页码时取消上一页未完成请求(abortController)后再发起新请求 + - 页面初始化并行请求(列表+下拉数据)之间互不阻塞 + +7. **[H2]超时与加载反馈** + - 列表查询API:timeout=15秒;加载中表格区显示v-loading骨架屏遮罩 + - 启停等写操作API:timeout=30秒;操作按钮:loading态 + - 超时处理:自动中断 → ElMessage.error("请求超时,请检查网络后重试") → 按钮恢复可用 + - 列表加载超过2秒时显示全局ElLoading进度提示 + +8. **[H3]操作确认机制** + - 启用/停用操作:ElMessageBox.confirm("确定要{启用/停用}「{对象名称}」吗?停用后该账号将无法登录", "操作确认", { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) + - 确认弹窗期间底层页面不可点击(modal遮罩) + +9. **[H8]操作结果反馈** + - 成功操作:ElMessage.success("{操作}成功",duration=2000ms) → silent方式刷新列表数据(不带loading闪烁) + - 失败:ElMessage.error(后端message或"操作失败,请稍后重试",duration=0手动关闭) + - 网络异常:提示"网络连接异常,请检查网络后重试"+ 提供"重试"文字按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 查询条件区 | el-form | inline, label-width="auto" | +| 医院名称/联系人 | el-input | clearable, maxlength=50, placeholder="请输入" | +| 状态下拉 | el-select | clearable, placeholder="请选择" | +| 查询按钮 | el-button | type="primary", icon="Search" | +| 重置按钮 | el-button | icon="Refresh" | +| 列表 | el-table | stripe, border, size="default", v-loading | +| 状态标签 | el-tag | 启用:type="success", 停用:type="danger" | +| 院区数(可点击) | el-link | type="primary", underline=false | +| 操作按钮 | el-button | type="primary", link, size="small" | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next", :page-sizes="[10,20,50]" | +| 新增医院 | el-button | type="primary", icon="Plus" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 医院名称(查询) | 最大50字符 | — | +| 联系人(查询) | 最大20字符 | — | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件区一行排列;列表完整展示所有列 | +| 1024-1279px(Pad横屏) | 查询条件区一行排列;"联系人"列隐藏,"创建时间"列隐藏 | +| 768-1023px(Pad竖屏) | 查询条件区两行排列;列表仅显示:序号、医院名称、状态、操作;其余列折叠,点击行展开详情 | --- @@ -145,6 +217,100 @@ | 编辑医院 | /api/v1/hospitals/{id} | PUT | 含院区列表 | | 查询详情 | /api/v1/hospitals/{id} | GET | 编辑时回填 | +### 交互流程要求 + +1. **页面加载流程** + - 新增模式:页面空白表单,状态默认"启用" + - 编辑模式:根据路由参数 `:id` 调用 `GET /api/v1/hospitals/{id}` 回填表单数据 + - 加载中显示表单骨架屏 + +2. **表单填写与提交流程** + - 用户逐项填写基本信息 → 动态添加/删除院区行 → 点击"保存"→ 前端校验 → 调用API → 成功后 `el-message.success` → 路由跳转回列表页 + - 编辑模式下保存 → 调用 `PUT /api/v1/hospitals/{id}` → 成功后返回列表页 + +3. **院区动态表单交互** + - 点击"+添加院区"→ 在院区列表末尾新增一行空表单 + - 点击"删除"→ 二次确认后移除该院区行(至少保留一个院区) + - 院区行内字段实时校验 + +4. **联动交互** + - 无特殊联动逻辑 + +5. **异常处理** + - 唯一性校验失败(医院名称重复)→ 对应字段下方显示红色错误提示"该医院名称已存在" + - API失败 → `el-message.error` 提示错误信息,表单保持当前数据不丢失 + - 编辑模式下详情加载失败 → 提示"数据加载失败"并提供"返回列表"按钮 + +6. **权限控制交互** + - 无保存权限时 → "保存"按钮禁用 + +7. **[H1]防重复请求** + - 点击"保存"按钮后:按钮 :loading=true + 文案变为"保存中..." + 按钮disabled;API返回(成功/失败/超时)后恢复 + - 保存期间不允许再次点击保存或关闭页面 + - 院区行动态增删操作:删除院区确认弹窗pending期间禁用其他删除按钮 + +8. **[H2]超时与加载反馈** + - 新增/编辑提交API(POST/PUT):timeout=30秒 + - 编辑模式详情回填API(GET):timeout=15秒 + - 提交中保存按钮保持:loading态 + 表单区域不可编辑(半透明遮罩可选) + - 超时处理:中断请求 → 提示"保存超时,请检查网络后重试" → 按钮恢复 + +9. **[H3]操作确认机制** + - 删除院区行前 ElMessageBox.confirm("确定要删除该院区吗?删除后数据无法恢复", "删除确认", { type: 'warning' }) + +10. **[H4]脏数据检测** + - 编辑模式进入时deep clone表单初始数据作为快照 + - 用户修改任意字段后标记isDirty=true + - 点击"取消"按钮或浏览器后退时: + * isDirty=true → ElMessageBox.confirm("当前修改尚未保存,离开后将丢失未保存的内容,是否确认离开?") → 确认则离开/取消则停留 + * isDirty=false → 直接执行离开操作 + - 使用vue-router的beforeRouteLeave导航守卫拦截路由切换 + - 保存成功后将当前数据设为新快照,重置isDirty=false + +11. **[H8]操作结果反馈** + - 保存成功:ElMessage.success("保存成功", duration=2000) → 延迟300ms后router.back()返回列表页 + - 保存失败:ElMessage.error(错误信息, duration=0);表单数据保持不丢失 + - 唯一性校验失败:对应字段下方红色文字提示,不弹message + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 基本信息表单 | el-form | label-width="100px", :model, :rules | +| 医院名称 | el-input | maxlength=50, show-word-limit, clearable | +| 状态 | el-radio-group | el-radio label="启用"/"停用" | +| 医院地址 | el-input | maxlength=200, show-word-limit | +| 联系人 | el-input | maxlength=20 | +| 联系电话 | el-input | maxlength=11, type="tel" | +| 院区列表 | 动态表单行 | v-for 渲染, 动态增删 | +| 院区名称 | el-input | maxlength=30, show-word-limit | +| 院区地址 | el-input | maxlength=200 | +| 保存按钮 | el-button | type="primary", :loading | +| 取消按钮 | el-button | @click="router.back()" | +| 添加院区 | el-button | type="primary", plain, icon="Plus" | +| 删除院区 | el-button | type="danger", link, icon="Delete" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 医院名称 | 必填, 2-50字符, 全局唯一 | "请输入医院名称" / "医院名称长度2-50字符" / "该医院名称已存在" | +| 状态 | 必填 | "请选择状态" | +| 医院地址 | 最大200字符 | "医院地址不能超过200字符" | +| 联系人 | 最大20字符 | "联系人不能超过20字符" | +| 联系电话 | 手机号格式(/^1[3-9]\d{9}$/) | "请输入正确的手机号" | +| 院区名称 | 必填, 2-30字符, 同医院内唯一 | "请输入院区名称" / "院区名称长度2-30字符" / "该院区名称已存在" | +| 院区地址 | 必填, 最大200字符 | "请输入院区地址" / "院区地址不能超过200字符" | +| 院区列表 | 至少1个院区 | "请至少添加一个院区" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 基本信息两列排列(医院名称+状态一行,地址独占一行,联系人+电话一行);院区信息卡片宽度100% | +| 1024-1279px(Pad横屏) | 基本信息两列排列;院区信息卡片宽度100% | +| 768-1023px(Pad竖屏) | 基本信息单列排列(每行一个字段);院区信息卡片宽度100%,院内行单列 | + --- ## 页面3:物业公司信息管理列表页 @@ -208,6 +374,74 @@ | 列表查询 | /api/v1/property-companies | GET | 分页查询 | | 启用/停用 | /api/v1/property-companies/{id}/toggle-status | PUT | — | +### 交互流程要求 + +1. **页面加载流程** + - 进入页面 → 调用 `GET /api/v1/property-companies` 加载列表(默认第1页,每页20条,创建时间倒序) + - 并行加载下拉选项:状态下拉 + - 列表为空时显示空状态插图 + "暂无物业公司信息" + +2. **查询交互流程** + - 填写查询条件 → 点击"查询"→ 调用API → 重置到第1页渲染 + - 点击"重置"→ 清空条件 → 重新加载 + +3. **行内操作流程** + - **编辑**:点击"编辑"→ 路由跳转至 `/account/property-companies/:id/edit` + - **启用/停用**:二次确认弹窗("确认停用XX物业?停用后该物业下所有账号将无法登录")→ 确认后调用API → 成功提示 → 刷新列表 + +4. **异常处理** + - 同页面1通用异常处理规则 + +5. **权限控制交互** + - 无 `permission:user:create` → "新增物业公司"按钮不渲染 + - 无 `permission:user:update` → 行操作列不渲染 + +6. **[H1]防重复请求** + - 查询/筛选:点击"查询"按钮后立即 disabled=true + 显示loading态,API返回后恢复;查询期间再次点击无效 + - 行内操作(编辑/启停):点击操作按钮后该行所有操作按钮禁用 + 按钮显示loading旋转图标;操作完成后恢复 + - 分页切换:切换页码时取消上一页未完成请求(abortController)后再发起新请求 + - 页面初始化并行请求(列表+下拉数据)之间互不阻塞 + +7. **[H2]超时与加载反馈** + - 列表查询API:timeout=15秒;加载中表格区显示v-loading骨架屏遮罩 + - 启停等写操作API:timeout=30秒;操作按钮:loading态 + - 超时处理:自动中断 → ElMessage.error("请求超时,请检查网络后重试") → 按钮恢复可用 + - 列表加载超过2秒时显示全局ElLoading进度提示 + +8. **[H3]操作确认机制** + - 启用/停用操作:ElMessageBox.confirm("确定要{启用/停用}「{对象名称}」吗?停用后该账号将无法登录", "操作确认", { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) + - 确认弹窗期间底层页面不可点击(modal遮罩) + +9. **[H8]操作结果反馈** + - 成功操作:ElMessage.success("{操作}成功",duration=2000ms) → silent方式刷新列表数据(不带loading闪烁) + - 失败:ElMessage.error(后端message或"操作失败,请稍后重试",duration=0手动关闭) + - 网络异常:提示"网络连接异常,请检查网络后重试"+ 提供"重试"文字按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 列表 | el-table | stripe, border, v-loading | +| 服务医院(可点击) | el-link | type="primary" | +| 状态标签 | el-tag | 启用:type="success", 停用:type="danger" | +| 操作按钮 | el-button | type="primary", link, size="small" | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 公司名称(查询) | 最大50字符 | — | +| 联系人(查询) | 最大20字符 | — | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件一行排列;列表完整展示所有列 | +| 1024-1279px(Pad横屏) | 查询条件一行排列;"联系人""联系电话"列隐藏 | +| 768-1023px(Pad竖屏) | 查询条件两行排列;列表仅显示:序号、公司名称、状态、操作 | + --- ## 页面4:物业公司信息新增/编辑页 @@ -232,6 +466,72 @@ | 新增 | /api/v1/property-companies | POST | — | | 编辑 | /api/v1/property-companies/{id} | PUT | — | +### 交互流程要求 + +1. **页面加载流程** + - 新增模式:空白表单 + - 编辑模式:根据路由参数 `:id` 调用 `GET /api/v1/property-companies/{id}` 回填数据 + +2. **表单填写与提交流程** + - 填写表单 → 点击"保存"→ 前端校验 → 调用API → 成功提示 → 返回列表页 + - 校验失败 → 定位到第一个错误字段,滚动到可见区域 + +3. **异常处理** + - 唯一性校验失败 → 字段下方红色提示"该公司名称已存在" + - API失败 → 提示错误信息,表单数据不丢失 + +4. **[H1]防重复请求** + - 点击"保存"按钮后:按钮 :loading=true + 文案变为"保存中..." + 按钮disabled;API返回(成功/失败/超时)后恢复 + - 保存期间不允许再次点击保存或关闭页面 + +5. **[H2]超时与加载反馈** + - 新增/编辑提交API(POST/PUT):timeout=30秒 + - 编辑模式详情回填API(GET):timeout=15秒 + - 提交中保存按钮保持:loading态 + 表单区域不可编辑(半透明遮罩可选) + - 超时处理:中断请求 → 提示"保存超时,请检查网络后重试" → 按钮恢复 + +6. **[H4]脏数据检测** + - 编辑模式进入时deep clone表单初始数据作为快照 + - 用户修改任意字段后标记isDirty=true + - 点击"取消"按钮或浏览器后退时: + * isDirty=true → ElMessageBox.confirm("当前修改尚未保存,离开后将丢失未保存的内容,是否确认离开?") → 确认则离开/取消则停留 + * isDirty=false → 直接执行离开操作 + - 使用vue-router的beforeRouteLeave导航守卫拦截路由切换 + - 保存成功后将当前数据设为新快照,重置isDirty=false + +7. **[H8]操作结果反馈** + - 保存成功:ElMessage.success("保存成功", duration=2000) → 延迟300ms后router.back()返回列表页 + - 保存失败:ElMessage.error(错误信息, duration=0);表单数据保持不丢失 + - 唯一性校验失败:对应字段下方红色文字提示,不弹message + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 表单 | el-form | label-width="100px", :model, :rules | +| 公司名称 | el-input | maxlength=50, show-word-limit | +| 公司地址 | el-input | maxlength=200 | +| 联系人 | el-input | maxlength=20 | +| 联系电话 | el-input | maxlength=11, type="tel" | +| 保存按钮 | el-button | type="primary", :loading | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 公司名称 | 必填, 2-50字符, 全局唯一 | "请输入公司名称" / "公司名称长度2-50字符" / "该公司名称已存在" | +| 公司地址 | 最大200字符 | "公司地址不能超过200字符" | +| 联系人 | 必填, 最大20字符 | "请输入联系人" / "联系人不能超过20字符" | +| 联系电话 | 必填, 手机号格式 | "请输入联系电话" / "请输入正确的手机号" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 表单两列排列(公司名称+联系人一行,地址独占一行) | +| 1024-1279px(Pad横屏) | 表单两列排列 | +| 768-1023px(Pad竖屏) | 表单单列排列 | + --- ## 页面5:医院账号管理列表页 @@ -308,6 +608,80 @@ | 续期 | /api/v1/accounts/{id}/renew | PUT | 修改有效期 | | 重置密码 | /api/v1/accounts/{id}/reset-password | PUT | — | +### 交互流程要求 + +1. **页面加载流程** + - 进入页面 → 并行调用:列表查询API + 绑定医院下拉数据 + 状态下拉 + 有效期下拉 + - 加载中骨架屏 → 数据渲染列表 + +2. **查询交互流程** + - 填写查询条件 → 点击"查询"→ 调用API → 重置到第1页 + - "绑定医院"下拉支持搜索过滤(filterable) + +3. **行内操作流程** + - **编辑**:点击"编辑"→ 路由跳转编辑页(如存在独立编辑页) + - **续期**:点击"续期"→ 弹出续期弹窗(`el-dialog`,选择新有效期日期)→ 确认后调用 `PUT /api/v1/accounts/{id}/renew` → 成功提示 → 刷新列表 + - **启用/禁用**:二次确认("确认禁用该账号?禁用后该账号将立即无法登录")→ 确认后调用API → 成功提示 → 刷新列表 + - **重置密码**:二次确认("确认重置密码?重置后密码将恢复为默认密码")→ 确认后调用API → 成功后弹窗展示新密码,支持一键复制 + +4. **联动交互** + - 绑定医院下拉数据来源于医院信息管理中的启用状态医院 + +5. **异常处理** + - 禁用失败(账号已禁用等)→ 提示具体原因 + - 续期时新有效期早于当前日期 → 前端校验拦截,提示"有效期不能早于当前日期" + - 重置密码失败 → 提示错误信息 + +6. **权限控制交互** + - 无对应权限 → 行操作按钮不渲染 + +7. **[H1]防重复请求** + - 查询/筛选:点击"查询"按钮后立即 disabled=true + 显示loading态,API返回后恢复;查询期间再次点击无效 + - 行内操作(编辑/续期/启停/重置密码):点击操作按钮后该行所有操作按钮禁用 + 按钮显示loading旋转图标;操作完成后恢复 + - 分页切换:切换页码时取消上一页未完成请求(abortController)后再发起新请求 + - 页面初始化并行请求(列表+下拉数据)之间互不阻塞 + +8. **[H2]超时与加载反馈** + - 列表查询API:timeout=15秒;加载中表格区显示v-loading骨架屏遮罩 + - 启停/续期/重置密码等写操作API:timeout=30秒;操作按钮:loading态 + - 超时处理:自动中断 → ElMessage.error("请求超时,请检查网络后重试") → 按钮恢复可用 + - 列表加载超过2秒时显示全局ElLoading进度提示 + +9. **[H3]操作确认机制** + - 启用/禁用操作:ElMessageBox.confirm("确定要{启用/禁用}「{对象名称}」吗?禁用后该账号将无法登录", "操作确认", { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) + - 重置密码:ElMessageBox.confirm("确定要重置「{账号}」的密码吗?重置后将恢复为默认密码", "操作确认", { type: 'warning' }) + - 确认弹窗期间底层页面不可点击(modal遮罩) + +10. **[H8]操作结果反馈** + - 成功操作:ElMessage.success("{操作}成功",duration=2000ms) → silent方式刷新列表数据(不带loading闪烁) + - 失败:ElMessage.error(后端message或"操作失败,请稍后重试",duration=0手动关闭) + - 网络异常:提示"网络连接异常,请检查网络后重试"+ 提供"重试"文字按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 绑定医院下拉 | el-select | filterable, clearable, remote, placeholder="请选择医院" | +| 有效期至 | el-tag | 正常:type="success", 即将到期:type="warning", 已过期:type="danger" | +| 状态标签 | el-tag | 正常:type="success", 即将到期:type="warning", 已过期:type="danger", 已停用:type="info" | +| 续期弹窗 | el-dialog | width="400px", :close-on-click-modal=false | +| 续期日期选择 | el-date-picker | type="date", :disabled-date="禁用过去日期" | +| 重置密码结果弹窗 | el-dialog | 展示新密码 + 复制按钮 | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 续期日期 | 必填, 不早于当前日期 | "请选择有效期" / "有效期不能早于当前日期" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件一行排列;列表完整展示所有列 | +| 1024-1279px(Pad横屏) | 查询条件一行排列;"角色"列隐藏 | +| 768-1023px(Pad竖屏) | 查询条件两行排列;列表仅显示:序号、登录账号、绑定医院、状态、操作 | + --- ## 页面6:新增医院账号页 @@ -347,6 +721,83 @@ |----------|---------|------|------| | 新增 | /api/v1/accounts/hospital | POST | — | +### 交互流程要求 + +1. **页面加载流程** + - 进入页面 → 并行加载:绑定医院下拉(仅启用状态医院)+ 角色下拉(仅医院适用范围角色) + - 自动生成初始密码(8位随机字母数字),显示在密码输入框中 + - 有效期默认为当前日期+1年 + +2. **表单填写与提交流程** + - 填写登录账号 → 实时校验唯一性(失焦时调用后端校验接口) + - 选择绑定医院 → 选择有效期 → 分配角色 → 点击"保存"→ 前端校验全部通过 → 调用API → 成功提示 → 返回列表页 + +3. **联动交互** + - 绑定医院下拉:数据来源于医院信息管理中启用状态的医院 + - 分配角色下拉:数据来源于权限管理中"适用范围=医院账号"的启用角色 + +4. **异常处理** + - 登录账号重复 → 失焦校验后即时提示"该登录账号已存在" + - 保存失败 → 提示错误信息,表单数据不丢失 + +5. **权限控制交互** + - 无保存权限时 → "保存"按钮禁用 + +6. **[H1]防重复请求** + - 点击"保存"按钮后:按钮 :loading=true + 文案变为"保存中..." + 按钮disabled;API返回(成功/失败/超时)后恢复 + - 保存期间不允许再次点击保存或关闭页面 + +7. **[H2]超时与加载反馈** + - 新增提交API(POST):timeout=30秒 + - 提交中保存按钮保持:loading态 + 表单区域不可编辑(半透明遮罩可选) + - 超时处理:中断请求 → 提示"保存超时,请检查网络后重试" → 按钮恢复 + +8. **[H4]脏数据检测** + - 页面进入时deep clone表单初始数据作为快照 + - 用户修改任意字段后标记isDirty=true + - 点击"取消"按钮或浏览器后退时: + * isDirty=true → ElMessageBox.confirm("当前修改尚未保存,离开后将丢失未保存的内容,是否确认离开?") → 确认则离开/取消则停留 + * isDirty=false → 直接执行离开操作 + - 使用vue-router的beforeRouteLeave导航守卫拦截路由切换 + - 保存成功后将当前数据设为新快照,重置isDirty=false + +9. **[H8]操作结果反馈** + - 保存成功:ElMessage.success("保存成功", duration=2000) → 延迟300ms后router.back()返回列表页 + - 保存失败:ElMessage.error(错误信息, duration=0);表单数据保持不丢失 + - 唯一性校验失败:对应字段下方红色文字提示,不弹message + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 表单 | el-form | label-width="100px", :model, :rules | +| 登录账号 | el-input | maxlength=20, clearable, placeholder="4-20位字母数字" | +| 初始密码 | el-input | maxlength=20, show-password, readonly=false | +| 绑定医院 | el-select | filterable, clearable, placeholder="请选择医院" | +| 有效期至 | el-date-picker | type="date", :disabled-date="禁用过去日期", value-format="YYYY-MM-DD" | +| 分配角色 | el-select | multiple, filterable, collapse-tags, placeholder="请选择角色" | +| 保存按钮 | el-button | type="primary", :loading | +| 取消按钮 | el-button | @click="router.back()" | +| 刷新密码按钮 | el-button | icon="Refresh", circle, @click="generatePassword" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 登录账号 | 必填, 4-20位字母数字(/^[a-zA-Z0-9]{4,20}$/), 全局唯一 | "请输入登录账号" / "登录账号为4-20位字母数字" / "该登录账号已存在" | +| 初始密码 | 必填, 6-20位 | "请输入初始密码" / "密码长度6-20位" | +| 绑定医院 | 必填 | "请选择绑定医院" | +| 有效期至 | 必填, 不早于当前日期 | "请选择有效期" / "有效期不能早于当前日期" | +| 分配角色 | 必填, 至少选一个 | "请选择至少一个角色" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 表单两列排列(登录账号+初始密码一行,绑定医院+有效期一行,角色独占一行) | +| 1024-1279px(Pad横屏) | 表单两列排列 | +| 768-1023px(Pad竖屏) | 表单单列排列,每个字段独占一行 | + --- ## 页面7:物业公司管理员账号管理列表页 @@ -421,6 +872,81 @@ | 启用/禁用 | /api/v1/accounts/{id}/toggle-status | PUT | 禁用同步下线下属 | | 续期 | /api/v1/accounts/{id}/renew | PUT | — | +### 交互流程要求 + +1. **页面加载流程** + - 进入页面 → 并行加载:列表数据 + 绑定物业公司下拉 + 服务医院下拉 + 状态下拉 + - 加载中骨架屏 + +2. **查询交互流程** + - 填写条件 → 点击"查询"→ 调用API → 重置到第1页 + - "绑定物业公司"和"服务医院"下拉均支持搜索过滤 + +3. **行内操作流程** + - **编辑**:跳转编辑页 + - **续期**:弹出续期弹窗 → 选择新有效期 → 确认 → API调用 → 刷新列表 + - **启用/禁用**:二次确认("确认禁用?禁用后该管理员及所有下属账号将同步失效")→ 确认 → API调用 → 刷新列表 + - **重置密码**:二次确认 → API调用 → 弹窗展示新密码 + +4. **联动交互** + - 物业公司下拉数据来源于物业公司信息管理中的启用状态公司 + - 选择物业公司后,服务医院下拉可联动过滤该物业关联的医院 + +5. **异常处理** + - 禁用操作提示级联影响范围:"该操作将同步禁用X个下属账号" + - 其他同页面5 + +6. **权限控制交互** + - 同页面5 + +7. **[H1]防重复请求** + - 查询/筛选:点击"查询"按钮后立即 disabled=true + 显示loading态,API返回后恢复;查询期间再次点击无效 + - 行内操作(编辑/续期/启停/重置密码):点击操作按钮后该行所有操作按钮禁用 + 按钮显示loading旋转图标;操作完成后恢复 + - 分页切换:切换页码时取消上一页未完成请求(abortController)后再发起新请求 + - 页面初始化并行请求(列表+下拉数据)之间互不阻塞 + +8. **[H2]超时与加载反馈** + - 列表查询API:timeout=15秒;加载中表格区显示v-loading骨架屏遮罩 + - 启停/续期/重置密码等写操作API:timeout=30秒;操作按钮:loading态 + - 超时处理:自动中断 → ElMessage.error("请求超时,请检查网络后重试") → 按钮恢复可用 + - 列表加载超过2秒时显示全局ElLoading进度提示 + +9. **[H3]操作确认机制** + - 启用/禁用操作:ElMessageBox.confirm("确定要{启用/禁用}「{对象名称}」吗?禁用后将同步禁用所有下属账号", "操作确认", { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) + - 重置密码:ElMessageBox.confirm("确定要重置「{账号}」的密码吗?重置后将恢复为默认密码", "操作确认", { type: 'warning' }) + - 确认弹窗期间底层页面不可点击(modal遮罩) + +10. **[H8]操作结果反馈** + - 成功操作:ElMessage.success("{操作}成功",duration=2000ms) → silent方式刷新列表数据(不带loading闪烁) + - 失败:ElMessage.error(后端message或"操作失败,请稍后重试",duration=0手动关闭) + - 网络异常:提示"网络连接异常,请检查网络后重试"+ 提供"重试"文字按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 绑定物业公司下拉 | el-select | filterable, clearable | +| 服务医院下拉 | el-select | filterable, clearable | +| 状态标签 | el-tag | 同页面5颜色规则 | +| 操作按钮 | el-button | type="primary", link, size="small" | +| 续期弹窗 | el-dialog | width="400px" | +| 续期日期选择 | el-date-picker | type="date", :disabled-date | +| 重置密码结果弹窗 | el-dialog | 展示新密码 + 复制按钮 | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 续期日期 | 必填, 不早于当前日期 | "请选择有效期" / "有效期不能早于当前日期" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件一行排列;列表完整展示所有列 | +| 1024-1279px(Pad横屏) | 查询条件两行排列;"角色"列隐藏 | +| 768-1023px(Pad竖屏) | 查询条件两行排列;列表仅显示:序号、登录账号、绑定物业、状态、操作 | + --- ## 页面8:新增物业管理员账号页 @@ -446,6 +972,84 @@ |----------|---------|------|------| | 新增 | /api/v1/accounts/property-admin | POST | — | +### 交互流程要求 + +1. **页面加载流程** + - 进入页面 → 并行加载:绑定物业公司下拉 + 服务医院下拉 + 角色下拉(物业适用范围) + - 自动生成初始密码,有效期默认当前日期+1年 + +2. **表单填写与提交流程** + - 填写登录账号 → 失焦时校验唯一性 + - 选择物业公司 → 选择服务医院 → 选择有效期 → 分配角色 → 点击"保存"→ 校验 → API → 成功返回列表页 + +3. **联动交互** + - 绑定物业公司下拉:来源于物业公司信息管理中启用状态的公司 + - 选择物业公司后,服务医院下拉联动过滤该物业关联的医院 + - 分配角色下拉:仅显示"适用范围=物业管理员"的启用角色 + +4. **异常处理** + - 登录账号重复 → 失焦时即时提示 + - API失败 → 提示错误,表单不丢失 + +5. **权限控制交互** + - 无保存权限时 → "保存"按钮禁用 + +6. **[H1]防重复请求** + - 点击"保存"按钮后:按钮 :loading=true + 文案变为"保存中..." + 按钮disabled;API返回(成功/失败/超时)后恢复 + - 保存期间不允许再次点击保存或关闭页面 + +7. **[H2]超时与加载反馈** + - 新增提交API(POST):timeout=30秒 + - 提交中保存按钮保持:loading态 + 表单区域不可编辑(半透明遮罩可选) + - 超时处理:中断请求 → 提示"保存超时,请检查网络后重试" → 按钮恢复 + +8. **[H4]脏数据检测** + - 页面进入时deep clone表单初始数据作为快照 + - 用户修改任意字段后标记isDirty=true + - 点击"取消"按钮或浏览器后退时: + * isDirty=true → ElMessageBox.confirm("当前修改尚未保存,离开后将丢失未保存的内容,是否确认离开?") → 确认则离开/取消则停留 + * isDirty=false → 直接执行离开操作 + - 使用vue-router的beforeRouteLeave导航守卫拦截路由切换 + - 保存成功后将当前数据设为新快照,重置isDirty=false + +9. **[H8]操作结果反馈** + - 保存成功:ElMessage.success("保存成功", duration=2000) → 延迟300ms后router.back()返回列表页 + - 保存失败:ElMessage.error(错误信息, duration=0);表单数据保持不丢失 + - 唯一性校验失败:对应字段下方红色文字提示,不弹message + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 表单 | el-form | label-width="120px", :model, :rules | +| 登录账号 | el-input | maxlength=20, clearable | +| 初始密码 | el-input | maxlength=20, show-password | +| 绑定物业公司 | el-select | filterable, clearable | +| 服务医院 | el-select | filterable, clearable | +| 有效期至 | el-date-picker | type="date", :disabled-date, value-format="YYYY-MM-DD" | +| 分配角色 | el-select | multiple, filterable, collapse-tags | +| 保存按钮 | el-button | type="primary", :loading | +| 刷新密码按钮 | el-button | icon="Refresh", circle | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 登录账号 | 必填, 4-20位字母数字, 全局唯一 | "请输入登录账号" / "登录账号为4-20位字母数字" / "该登录账号已存在" | +| 初始密码 | 必填, 6-20位 | "请输入初始密码" / "密码长度6-20位" | +| 绑定物业公司 | 必填 | "请选择绑定物业公司" | +| 服务医院 | 必填 | "请选择服务医院" | +| 有效期至 | 必填, 不早于当前日期 | "请选择有效期" / "有效期不能早于当前日期" | +| 分配角色 | 必填, 至少选一个 | "请选择至少一个角色" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 表单两列排列 | +| 1024-1279px(Pad横屏) | 表单两列排列 | +| 768-1023px(Pad竖屏) | 表单单列排列 | + --- ## 页面9:到期账号管理页 @@ -516,6 +1120,79 @@ | 列表查询 | /api/v1/accounts/expiring | GET | 筛选到期账号 | | 续期 | /api/v1/accounts/{id}/renew | PUT | — | +### 交互流程要求 + +1. **页面加载流程** + - 进入页面 → 并行调用:统计卡片数据 + 列表数据 + 下拉选项 + - 统计卡片数据优先渲染,列表加载中显示骨架屏 + - 点击统计卡片可快速筛选(点击"已过期"卡片 → 自动填充到期状态=已过期 → 触发查询) + +2. **查询交互流程** + - 填写条件 → 点击"查询"→ 调用API → 重置到第1页 + - 统计卡片数据随查询条件联动刷新 + +3. **行内操作流程** + - **续期**:点击"续期"→ 弹出续期弹窗(选择新有效期)→ 确认 → API调用 → 成功提示"续期成功"→ 刷新列表和统计卡片 + +4. **联动交互** + - 统计卡片点击 → 联动查询条件 → 触发筛选 + +5. **异常处理** + - 续期失败 → 提示错误信息 + - 统计数据加载失败 → 卡片显示"--"占位,不影响列表操作 + +6. **权限控制交互** + - 无 `permission:user:update` → 行"续期"按钮不渲染 + +7. **[H1]防重复请求** + - 查询/筛选:点击"查询"按钮后立即 disabled=true + 显示loading态,API返回后恢复;查询期间再次点击无效 + - 行内操作(续期):点击操作按钮后该行所有操作按钮禁用 + 按钮显示loading旋转图标;操作完成后恢复 + - 分页切换:切换页码时取消上一页未完成请求(abortController)后再发起新请求 + - 页面初始化并行请求(统计卡片+列表+下拉数据)之间互不阻塞 + +8. **[H2]超时与加载反馈** + - 列表查询API:timeout=15秒;加载中表格区显示v-loading骨架屏遮罩 + - 续期写操作API:timeout=30秒;操作按钮:loading态 + - 统计卡片数据加载API:timeout=15秒 + - 超时处理:自动中断 → ElMessage.error("请求超时,请检查网络后重试") → 按钮恢复可用 + - 列表加载超过2秒时显示全局ElLoading进度提示 + +9. **[H3]操作确认机制** + - 续期弹窗本身即为操作确认机制:选择新有效期后需用户点击"确定"才提交 + - 确认弹窗期间底层页面不可点击(modal遮罩) + +10. **[H8]操作结果反馈** + - 成功操作:ElMessage.success("{操作}成功",duration=2000ms) → silent方式刷新列表数据和统计卡片(不带loading闪烁) + - 失败:ElMessage.error(后端message或"操作失败,请稍后重试",duration=0手动关闭) + - 网络异常:提示"网络连接异常,请检查网络后重试"+ 提供"重试"文字按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 统计卡片 | el-card | shadow="hover", :body-style="{padding:'20px',cursor:'pointer'}" | +| 卡片数字 | div | font-size=28px, font-weight=bold | +| 已过期数字 | — | color=var(--el-color-danger) | +| 7天内到期数字 | — | color=var(--el-color-warning) | +| 30天内到期数字 | — | color=var(--el-color-primary) | +| 剩余天数 | el-tag | 已过期:type="danger", ≤7天:type="warning", 其他:type="info" | +| 续期弹窗 | el-dialog | width="400px", :close-on-click-modal=false | +| 续期日期选择 | el-date-picker | type="date", :disabled-date | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 续期日期 | 必填, 不早于当前日期 | "请选择有效期" / "有效期不能早于当前日期" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 统计卡片一行三个;列表完整展示所有列 | +| 1024-1279px(Pad横屏) | 统计卡片一行三个;"账号类型"列隐藏 | +| 768-1023px(Pad竖屏) | 统计卡片一行三个(缩小间距);列表仅显示:序号、登录账号、绑定单位、剩余天数、操作 | + --- ## 页面10:到期提醒规则配置页 @@ -561,6 +1238,84 @@ | 查询配置 | /api/v1/system/configs/expiry-reminder | GET | — | | 保存配置 | /api/v1/system/configs/expiry-reminder | PUT | — | +### 交互流程要求 + +1. **页面加载流程** + - 进入页面 → 调用 `GET /api/v1/system/configs/expiry-reminder` 加载当前配置 → 回填表单 + - 加载中表单显示骨架屏 + +2. **表单填写与提交流程** + - 修改配置项 → 点击"保存"→ 前端校验(至少选一个提醒天数)→ 调用 `PUT` API → 成功提示"配置保存成功" + - 无需返回列表页,停留在当前页 + +3. **标签多选交互** + - "提前提醒天数"使用标签多选(预设7/15/30/60/90天选项),点击标签切换选中/取消 + - 支持自定义天数输入(输入框+添加按钮) + - 至少保留一个选中项 + +4. **联动交互** + - "用户登录弹窗"关闭时 → "弹窗关闭后行为"选项才可编辑 + - "过期后行为"选择"限制部分功能"时 → 显示附加配置(限制哪些功能) + +5. **异常处理** + - 保存失败 → 提示错误信息,表单数据不丢失 + - 配置加载失败 → 提示"配置加载失败",提供"重试"按钮 + +6. **权限控制交互** + - 无 `permission:config:update` → 表单所有控件禁用,"保存"按钮不渲染 + +7. **[H1]防重复请求** + - 点击"保存"按钮后:按钮 :loading=true + 文案变为"保存中..." + 按钮disabled;API返回(成功/失败/超时)后恢复 + - 保存期间不允许再次点击保存或关闭页面 + +8. **[H2]超时与加载反馈** + - 保存配置提交API(PUT):timeout=30秒 + - 查询配置回填API(GET):timeout=15秒 + - 提交中保存按钮保持:loading态 + 表单区域不可编辑(半透明遮罩可选) + - 超时处理:中断请求 → 提示"保存超时,请检查网络后重试" → 按钮恢复 + +9. **[H4]脏数据检测** + - 页面进入时deep clone表单初始数据作为快照 + - 用户修改任意字段后标记isDirty=true + - 点击"取消"按钮或浏览器后退时: + * isDirty=true → ElMessageBox.confirm("当前修改尚未保存,离开后将丢失未保存的内容,是否确认离开?") → 确认则离开/取消则停留 + * isDirty=false → 直接执行离开操作 + - 使用vue-router的beforeRouteLeave导航守卫拦截路由切换 + - 保存成功后将当前数据设为新快照,重置isDirty=false + +10. **[H8]操作结果反馈** + - 保存成功:ElMessage.success("配置保存成功", duration=2000);停留在当前页不跳转 + - 保存失败:ElMessage.error(错误信息, duration=0);表单数据保持不丢失 + - 唯一性校验失败:对应字段下方红色文字提示,不弹message(本页面无唯一性字段) + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 表单 | el-form | label-width="140px", :model, :rules | +| 提前提醒天数 | el-check-tag | 多选模式,预设7/15/30/60/90 | +| 自定义天数输入 | el-input-number | :min=1, :max=365, :step=1 | +| 用户登录弹窗 | el-switch | active-text="开启", inactive-text="关闭" | +| 弹窗关闭后行为 | el-radio-group | el-radio label="可正常使用"/"限制部分功能" | +| 过期后行为 | el-radio-group | el-radio label="禁止登录"/"仅提醒" | +| 保存按钮 | el-button | type="primary", :loading | +| 取消按钮 | el-button | @click="resetForm" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 提前提醒天数 | 必填, 至少选一个 | "请至少选择一个提醒天数" | +| 自定义天数 | 1-365整数 | "天数范围1-365" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 表单标签宽度140px,输入区域宽度400px | +| 1024-1279px(Pad横屏) | 表单标签宽度120px,输入区域宽度350px | +| 768-1023px(Pad竖屏) | 表单标签宽度100px,输入区域宽度100% | + --- ## 需求追溯 diff --git a/docs/02-功能清单-超级管理员/02-权限管理.md b/docs/02-功能清单-超级管理员/02-权限管理.md index 504d0a8..d06d6da 100644 --- a/docs/02-功能清单-超级管理员/02-权限管理.md +++ b/docs/02-功能清单-超级管理员/02-权限管理.md @@ -3,6 +3,7 @@ > 模块编码:permission > 端侧:Web专属(仅超级管理员) > 关联文档:01-模块划分 §8 / 02-功能清单-超级管理员 §2 / 03-业务流转逻辑-超级管理员 §4~6 / 05-接口规范 §4.2~4.3 / 06-项目技术要求 §4.2 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -86,6 +87,86 @@ | 停用角色 | /api/v1/roles/{id}/disable | PUT | — | | 删除角色 | /api/v1/roles/{id} | DELETE | 仅无关联账号时可删 | +### 交互流程要求 + +1. **页面加载流程** + - 进入页面 → 调用 `GET /api/v1/roles` 加载列表(默认第1页,创建时间倒序) + - 并行加载下拉选项:适用范围、状态 + - 列表为空时显示空状态插图 + "暂无角色" + +2. **查询交互流程** + - 填写查询条件 → 点击"查询"→ 调用API → 重置到第1页 + - 点击"重置"→ 清空条件 → 重新加载 + +3. **行内操作流程** + - **编辑**:点击"编辑"→ 路由跳转 `/permission/roles/:id/edit` + - **权限预览**:点击"权限预览"→ 弹窗展示该角色的完整权限清单(调用 `GET /api/v1/roles/{id}/permissions`) + - **停用**:二次确认("确认停用角色XX?停用后关联账号将失去该角色的权限")→ 调用 `PUT /api/v1/roles/{id}/disable` → 成功提示 → 刷新列表 + - **删除**:仅关联账号数=0时显示 → 二次确认("确认删除角色XX?此操作不可恢复")→ 调用 `DELETE /api/v1/roles/{id}` → 成功提示 → 刷新列表 + +4. **联动交互** + - "关联账号数"可点击 → 弹窗展示使用该角色的账号列表 + +5. **异常处理** + - 删除角色失败(存在关联账号)→ 提示"该角色下存在关联账号,无法删除" + - 停用角色失败 → 提示具体原因 + - API失败 → 通用错误提示 + +6. **权限控制交互** + - 无 `permission:role:create` → "新增角色"按钮不渲染 + - 无 `permission:role:update` → "编辑""停用"按钮不渲染 + - 无 `permission:role:delete` → "删除"按钮不渲染 + - 无 `permission:role:view` → "权限预览"按钮不渲染 + +7. **[H1]防重复请求** + - 查询/筛选:点击"查询"按钮后立即 disabled=true + 显示loading态,API返回后恢复;查询期间再次点击无效 + - 行内操作(停用/删除等):点击操作按钮后该行所有操作按钮禁用 + 按钮显示loading旋转图标;操作完成后恢复 + - 分页切换:切换页码时取消上一页未完成请求(abortController)后再发起新请求 + - 页面初始化并行请求之间互不阻塞 + +8. **[H2]超时与加载反馈** + - 列表查询API:timeout=15秒;加载中表格区显示v-loading骨架屏遮罩 + - 停用/删除等写操作API:timeout=30秒;操作按钮:loading态 + - 超时处理:自动中断 → ElMessage.error("请求超时,请检查网络后重试") → 按钮恢复可用 + - 列表加载超过2秒时显示全局ElLoading进度提示 + +9. **[H3]操作确认机制** + - 停用角色:ElMessageBox.confirm("确定要停用「{角色名}」吗?停用后关联账号将失去该角色的权限", "操作确认", { type: 'warning' }) + - 删除角色:ElMessageBox.confirm("确定要删除「{角色名}」吗?此操作不可恢复", "删除确认", { type: 'error' }) + - 确认弹窗期间底层页面不可点击 + +10. **[H8]操作结果反馈** + - 成功:ElMessage.success(duration=2000ms) → silent刷新列表 + - 失败:ElMessage.error(duration=0手动关闭) + - 网络异常:提示"网络连接异常"+ 重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 查询条件区 | el-form | inline, label-width="auto" | +| 适用范围下拉 | el-select | clearable, placeholder="请选择" | +| 状态标签 | el-tag | 启用:type="success", 停用:type="danger" | +| 预设模板标签 | el-tag | 是:type="primary", 否:type="info" | +| 关联账号数 | el-link | type="primary", underline=false, @click="showRelatedAccounts" | +| 操作按钮 | el-button | type="primary", link, size="small" | +| 删除按钮 | el-button | type="danger", link, size="small" | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 角色名称(查询) | 最大30字符 | — | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件一行排列;列表完整展示所有列 | +| 1024-1279px(Pad横屏) | 查询条件一行排列;"预设模板"列隐藏 | +| 768-1023px(Pad竖屏) | 查询条件两行排列;列表仅显示:序号、角色名称、适用范围、状态、操作 | + --- ## 页面2:角色新增/编辑页 @@ -175,6 +256,93 @@ | 获取权限树 | /api/v1/permissions/tree | GET | 四级权限树结构 | | 权限预览 | /api/v1/roles/{id}/permissions | GET | 已分配的完整权限列表 | +### 交互流程要求 + +1. **页面加载流程** + - 新增模式:空白表单 + 调用 `GET /api/v1/permissions/tree` 加载权限树 + - 编辑模式:并行调用权限树 + 角色详情(`GET /api/v1/roles/{id}`)→ 回填表单 + 勾选已有权限 + - 权限树加载中显示加载动画 + +2. **表单填写与提交流程** + - 填写基本信息 → 选择预设模板(可选)→ 自动填充权限树勾选 → 微调权限 → 点击"保存" + - 保存前校验:角色名称必填、适用范围必选、至少勾选一个权限项 + - 保存成功 → 记录权限审计日志 → 权限实时生效(Redis Pub/Sub)→ 返回列表页 + +3. **权限树交互** + - 四级展开:功能菜单→页面→功能点→动作 + - 勾选父节点 → 自动勾选所有子节点 + - 取消父节点 → 自动取消所有子节点 + - 子节点全选 → 父节点自动勾选 + - 子节点部分选 → 父节点半选(indeterminate) + - 支持搜索过滤权限名称 + +4. **预设模板联动** + - 选择预设模板 → 弹出确认("选择模板将覆盖当前权限配置,是否继续?")→ 确认后自动勾选模板权限 + - 模板填充后仍可手动微调 + +5. **权限预览交互** + - 点击"权限预览"→ 弹窗展示当前已勾选的权限清单(非保存后的,而是实时的) + - 权限预览弹窗同页面3格式 + +6. **异常处理** + - 角色名称重复 → 提示"该角色名称已存在" + - 权限树加载失败 → 提示"权限配置加载失败",提供"重试"按钮 + - 保存失败 → 提示错误信息,表单数据不丢失 + +7. **权限控制交互** + - 无保存权限 → "保存"按钮禁用 + +8. **[H1]防重复请求** + - 点击"保存"按钮后::loading=true + 文案"保存中..." + disabled;API返回后恢复 + +9. **[H2]超时与加载反馈** + - 提交API(POST/PUT):timeout=30秒;回填API(GET):timeout=15秒 + - 超时处理:中断 → 提示"保存超时..." → 按钮恢复 + +10. **[H3]操作确认机制** + - 选择预设模板时:ElMessageBox.confirm("选择模板将覆盖当前权限配置,是否继续?", { type: 'warning' }) + +11. **[H4]脏数据检测** + - 编辑模式进入时deep clone初始数据为快照 + - 修改任意字段→isDirty=true + - 取消/路由离开时:isDirty则ElMessageBox.confirm("当前修改尚未保存...") + - beforeRouteLeave导航守卫拦截 + - 保存成功后重置快照和isDirty + +12. **[H8]操作结果反馈** + - 成功:ElMessage.success("保存成功") → 延迟300ms返回列表页 + - 失败:ElMessage.error(duration=0);表单数据保持不丢失 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 表单 | el-form | label-width="100px", :model, :rules | +| 角色名称 | el-input | maxlength=30, show-word-limit, clearable | +| 角色描述 | el-input | type="textarea", maxlength=200, show-word-limit, :rows=3 | +| 适用范围 | el-select | clearable, placeholder="请选择适用范围" | +| 预设模板 | el-select | clearable, placeholder="可选,选择后自动填充权限" | +| 权限树 | el-tree | show-checkbox, check-strictly=false, default-expand-all=false, :data="permissionTree", node-key="code", :filter-node-method | +| 权限搜索 | el-input | placeholder="搜索权限名称", clearable, prefix-icon="Search" | +| 权限预览按钮 | el-button | type="info", plain | +| 保存按钮 | el-button | type="primary", :loading | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 角色名称 | 必填, 2-30字符, 全局唯一 | "请输入角色名称" / "角色名称长度2-30字符" / "该角色名称已存在" | +| 适用范围 | 必填 | "请选择适用范围" | +| 权限勾选 | 至少勾选一个 | "请至少分配一项权限" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 基本信息两列排列;权限树区域宽度100%,高度500px可滚动 | +| 1024-1279px(Pad横屏) | 基本信息两列排列;权限树区域高度400px | +| 768-1023px(Pad竖屏) | 基本信息单列排列;权限树区域高度350px,默认折叠到第二级 | + --- ## 页面3:权限预览弹窗 @@ -212,6 +380,54 @@ - 每个动作用 ✓/✗ 标识是否有权限 - 支持搜索功能菜单/页面名称 +### 交互流程要求 + +1. **弹窗打开流程** + - 从角色列表页或角色编辑页触发 → 调用 `GET /api/v1/roles/{id}/permissions` 加载权限数据 → 渲染权限树 + - 加载中弹窗内显示loading + +2. **弹窗内交互** + - 搜索框输入关键词 → 实时过滤权限树,高亮匹配项 + - 点击展开/折叠节点 + - 只读模式,不可修改勾选状态 + +3. **弹窗关闭** + - 点击"关闭"按钮或右上角× → 关闭弹窗 + - 支持ESC键关闭 + +4. **[H1]防重复请求** + - 弹窗打开时:调用API期间"关闭"按钮禁用 + 弹窗内显示v-loading遮罩;数据返回后恢复 + +5. **[H2]超时与加载反馈** + - 权限查询API(GET):timeout=15秒;加载中弹窗内显示loading骨架屏 + - 超时处理:中断 → ElMessage.error("权限数据加载超时") → 提供"重试"按钮 + +6. **[H8]操作结果反馈** + - 加载失败:ElMessage.error(duration=0手动关闭) + 弹窗内展示重试按钮 + - 网络异常:提示"网络连接异常"+ 重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 弹窗 | el-dialog | width="600px", :close-on-click-modal=true, title="权限预览" | +| 权限树 | el-tree | :data, default-expand-all, :props, :filter-node-method | +| 搜索框 | el-input | placeholder="搜索权限名称", clearable, prefix-icon="Search" | +| 权限标识 | el-tag | 有权限:type="success", ✓; 无权限:type="info", ✗ | +| 关闭按钮 | el-button | @click="dialogVisible=false" | + +### 校验规则 + +无表单校验(只读展示页) + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 弹窗宽度600px | +| 1024-1279px(Pad横屏) | 弹窗宽度550px | +| 768-1023px(Pad竖屏) | 弹窗宽度90vw,最大500px | + --- ## 页面4:权限配置注册页 @@ -274,6 +490,68 @@ | 列表查询 | /api/v1/permissions/registry | GET | 查看已注册的权限配置 | | 刷新 | /api/v1/permissions/registry/refresh | POST | 手动触发权限重新扫描 | +### 交互流程要求 + +1. **页面加载流程** + - 进入页面 → 调用 `GET /api/v1/permissions/registry` 加载权限配置列表 + - 页面顶部显示说明提示条(el-alert),说明此页面为只读 + +2. **查询交互流程** + - 填写查询条件 → 点击"查询"→ 筛选列表 → 重置到第1页 + +3. **操作流程** + - **刷新**:点击"刷新"按钮 → 二次确认("确认重新扫描权限配置?可能需要数秒")→ 调用 `POST /api/v1/permissions/registry/refresh` → 成功提示 → 重新加载列表 + - 无其他操作按钮(只读页面) + +4. **异常处理** + - 刷新失败 → 提示"权限扫描失败,请稍后重试" + +5. **权限控制交互** + - 此页面仅超级管理员可见 + +6. **[H1]防重复请求** + - 查询/筛选:点击"查询"按钮后立即 disabled=true + 显示loading态,API返回后恢复;查询期间再次点击无效 + - 刷新操作:点击"刷新"按钮后:loading=true + disabled;API返回后恢复 + - 分页切换:切换页码时取消上一页未完成请求(abortController)后再发起新请求 + - 页面初始化并行请求之间互不阻塞 + +7. **[H2]超时与加载反馈** + - 列表查询API:timeout=15秒;加载中表格区显示v-loading骨架屏遮罩 + - 刷新操作API(POST):timeout=30秒;操作按钮:loading态 + - 超时处理:自动中断 → ElMessage.error("请求超时,请检查网络后重试") → 按钮恢复可用 + - 列表加载超过2秒时显示全局ElLoading进度提示 + +8. **[H3]操作确认机制** + - 刷新权限配置:ElMessageBox.confirm("确认重新扫描权限配置?可能需要数秒", "操作确认", { type: 'warning' }) + - 确认弹窗期间底层页面不可点击 + +9. **[H8]操作结果反馈** + - 成功:ElMessage.success(duration=2000ms) → silent刷新列表 + - 失败:ElMessage.error(duration=0手动关闭) + - 网络异常:提示"网络连接异常"+ 重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 说明提示条 | el-alert | type="info", :closable=false, show-icon | +| 列表 | el-table | stripe, border, v-loading | +| 可用动作 | el-tag | v-for 循环渲染, type="info", size="small" | +| 刷新按钮 | el-button | type="warning", icon="Refresh", plain | +| 分页 | el-pagination | layout="total, sizes, prev, pager, next" | + +### 校验规则 + +无表单校验(只读页面) + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 列表完整展示所有列 | +| 1024-1279px(Pad横屏) | "功能点编码"列隐藏 | +| 768-1023px(Pad竖屏) | 列表仅显示:模块名称、页面名称、功能点名称、可用动作 | + --- ## 页面5:权限审计日志页 @@ -354,6 +632,71 @@ | 列表查询 | /api/v1/audit-logs/permission | GET | 分页查询 | | 详情查询 | /api/v1/audit-logs/permission/{id} | GET | 含变更前后对比 | +### 交互流程要求 + +1. **页面加载流程** + - 进入页面 → 调用 `GET /api/v1/audit-logs/permission` 加载列表(默认倒序) + - 并行加载下拉选项:操作类型 + - 列表为空时显示空状态 + +2. **查询交互流程** + - 填写条件 → 点击"查询"→ 调用API → 重置到第1页 + - 日期范围选择器限制最多查询90天 + +3. **行内操作流程** + - **查看详情**:点击"查看"→ 弹窗展示变更前后对比(调用 `GET /api/v1/audit-logs/permission/{id}`) + - 新增权限用绿色 `[+新增]` 标记,移除权限用红色 `[-移除]` 标记 + +4. **异常处理** + - 详情加载失败 → 提示错误信息 + - 日期范围超限 → 提示"查询时间范围不能超过90天" + +5. **权限控制交互** + - 无 `audit-log:permission:view` → 页面不可见 + +6. **[H1]防重复请求** + - 查询/筛选:点击"查询"按钮后立即 disabled=true + 显示loading态,API返回后恢复;查询期间再次点击无效 + - 行内操作(查看详情):点击"查看"按钮后该行操作按钮禁用 + 按钮显示loading旋转图标;弹窗打开后恢复 + - 分页切换:切换页码时取消上一页未完成请求(abortController)后再发起新请求 + - 页面初始化并行请求之间互不阻塞 + +7. **[H2]超时与加载反馈** + - 列表查询API:timeout=15秒;加载中表格区显示v-loading骨架屏遮罩 + - 详情查询API:timeout=15秒;加载中弹窗内显示v-loading遮罩 + - 超时处理:自动中断 → ElMessage.error("请求超时,请检查网络后重试") → 按钮恢复可用 + - 列表加载超过2秒时显示全局ElLoading进度提示 + +8. **[H8]操作结果反馈** + - 成功:ElMessage.success(duration=2000ms) → silent刷新列表 + - 失败:ElMessage.error(duration=0手动关闭) + - 网络异常:提示"网络连接异常"+ 重试按钮 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 日期范围选择 | el-date-picker | type="daterange", value-format="YYYY-MM-DD", :disabled-date | +| 操作类型标签 | el-tag | 角色创建:type="success", 权限修改:type="warning", 角色分配:type="primary", 角色移除:type="danger" | +| 变更摘要 | span | [+N项] color=green, [-M项] color=red | +| 详情弹窗 | el-dialog | width="650px", :close-on-click-modal=false | +| 新增权限 | div | color=var(--el-color-success), 前缀"[+新增]" | +| 移除权限 | div | color=var(--el-color-danger), 前缀"[-移除]" | +| 关闭按钮 | el-button | @click="dialogVisible=false" | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 日期范围 | 最多90天 | "查询时间范围不能超过90天" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件一行排列;列表完整展示所有列;详情弹窗650px | +| 1024-1279px(Pad横屏) | 查询条件两行排列;"目标角色"列隐藏;详情弹窗600px | +| 768-1023px(Pad竖屏) | 查询条件两行排列;列表仅显示:序号、操作时间、操作人、操作类型、操作;详情弹窗90vw | + --- ## 需求追溯 diff --git a/docs/02-功能清单-超级管理员/03-系统配置.md b/docs/02-功能清单-超级管理员/03-系统配置.md index 04a2100..497d4be 100644 --- a/docs/02-功能清单-超级管理员/03-系统配置.md +++ b/docs/02-功能清单-超级管理员/03-系统配置.md @@ -3,6 +3,7 @@ > 模块编码:system > 端侧:Web专属(仅超级管理员) > 关联文档:01-模块划分 §2.3 / 02-功能清单-超级管理员 §3 / 03-业务流转逻辑-超级管理员 §9 / 05-接口规范 §9.2 / 06-项目技术要求 §8.3~8.4 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -98,6 +99,91 @@ | 编辑 | /api/v1/system/versions/{id} | PUT | — | | 获取最新版本 | /api/v1/system/versions/latest | GET | 小程序启动时调用 | +### 交互流程要求 + +1. **页面加载流程** + - 进入页面 → 并行调用:当前版本信息卡片数据 + 版本列表数据 + - 卡片数据优先渲染 + +2. **操作流程** + - **新增版本**:点击"新增版本"→ 弹出表单弹窗 → 填写版本信息 → 保存 → 刷新列表和卡片 + - **编辑**:点击行"编辑"→ 弹出表单弹窗(回填数据)→ 修改 → 保存 → 刷新列表和卡片 + +3. **弹窗交互** + - 新增/编辑共用一个 `el-dialog` 弹窗 + - 保存成功后弹窗关闭,列表刷新 + +4. **异常处理** + - 版本号格式校验失败 → 字段下方红色提示 + - 版本号已存在 → 提示"该版本号已存在" + - 新增版本后小程序端自动检测更新 + +5. **权限控制交互** + - 无 `system:version:create` → "新增版本"按钮不渲染 + - 无 `system:version:update` → 行"编辑"按钮不渲染 + +6. **[H1]防重复请求**(列表页) + - 行内操作按钮点击后整行操作禁用 + loading态 + - 分页切换 abort上一请求后再发新请求 + +7. **[H2]超时与加载反馈**(列表页) + - 列表查询 timeout=15秒;写操作(新增/编辑) timeout=30秒 + - 超时 → ElMessage.error("请求超时,请稍后重试") + 按钮恢复 + - 加载>2秒显示全局ElLoading + +8. **[H3]操作确认机制** + - 版本发布无需额外确认(弹窗内已有保存按钮) + +9. **[H8]操作结果反馈** + - 成功 ElMessage.success(2s) → silent刷新 + - 失败 ElMessage.error(0手动关闭) + +10. **[H1]防重复请求**(弹窗内保存按钮) + - 保存按钮 :loading + disabled,API返回后恢复 + +11. **[H2]超时与加载反馈**(弹窗内提交) + - 弹窗内提交 timeout=30秒 + +12. **[H4]脏数据检测**(编辑模式弹窗) + - 弹窗打开时记录快照 + - 关闭弹窗/点击取消时 isDirty 检测 → 确认提示"修改尚未保存,确定关闭吗?" + +13. **[H8]操作结果反馈**(弹窗) + - 保存成功 → 关闭弹窗 → 刷新列表 + - 保存失败 → 弹窗保持打开,错误提示 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 版本信息卡片 | el-card | shadow="hover", :body-style="{padding:'20px'}" | +| 当前版本号 | div | font-size=20px, font-weight=bold | +| 版本列表 | el-table | stripe, border, v-loading | +| 端侧标签 | el-tag | Web:type="primary", 小程序:type="success" | +| 强制更新标签 | el-tag | 是:type="danger", 否:type="success" | +| 新增/编辑弹窗 | el-dialog | width="500px", :close-on-click-modal=false | +| 版本号输入 | el-input | placeholder="x.y.z格式", clearable | +| 端侧下拉 | el-select | :options="[{label:'Web',value:'web'},{label:'小程序',value:'miniapp'}]" | +| 强制更新开关 | el-switch | active-text="是", inactive-text="否" | +| 更新说明 | el-input | type="textarea", :rows=4, maxlength=500, show-word-limit | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 版本号 | 必填, 语义化格式(/^\d+\.\d+\.\d+$/) | "请输入版本号" / "版本号格式为x.y.z" | +| 端侧 | 必填 | "请选择端侧" | +| 最低兼容版本 | 必填, 语义化格式 | "请输入最低兼容版本" / "版本号格式为x.y.z" | +| 更新说明 | 必填, 最大500字符 | "请输入更新说明" / "更新说明不能超过500字符" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 版本卡片一行两个;列表完整展示所有列 | +| 1024-1279px(Pad横屏) | 版本卡片一行两个;"更新说明"列隐藏 | +| 768-1023px(Pad竖屏) | 版本卡片一行一个(全宽);列表仅显示:序号、版本号、端侧、是否强制更新、操作 + --- ## 页面2:缓存管理页 @@ -159,6 +245,68 @@ | 清理单模块 | /api/v1/system/cache/clear/{module} | POST | 清理指定模块缓存 | | 全部清理 | /api/v1/system/cache/clear-all | POST | 清理全部缓存 | +### 交互流程要求 + +1. **页面加载流程** + - 进入页面 → 并行调用:缓存状态概览(`GET /api/v1/system/cache/status`)+ 缓存模块列表 + - 概览数据优先渲染 + +2. **操作流程** + - **清理单模块**:点击行"清理"→ 二次确认("确认清理XX缓存?清理后相关数据将从数据库重新加载")→ 调用API → 成功提示 → 刷新缓存状态 + - **全部清理**:点击"全部清理"→ 二次确认("确认清理全部缓存?所有在线用户将收到刷新提示")→ 调用API → 成功提示 → 刷新缓存状态 + +3. **缓存状态实时更新** + - 清理操作完成后自动刷新状态概览和模块列表 + - 支持手动刷新(页面顶部刷新按钮) + +4. **异常处理** + - Redis连接断开 → 状态概览显示"● 未连接"红色标识,清理按钮禁用 + - 清理失败 → 提示错误信息 + +5. **权限控制交互** + - 无 `system:cache:update` → 所有清理按钮不渲染 + +6. **[H1]防重复请求** + - 行内清理按钮点击后整行操作禁用 + loading态 + - "全部清理"按钮点击后 disabled + loading态,API返回后恢复 + +7. **[H2]超时与加载反馈** + - 写操作(缓存清理) timeout=30秒 + - 超时 → ElMessage.error("请求超时,请稍后重试") + 按钮恢复 + - 加载>2秒显示全局ElLoading + +8. **[H3]操作确认机制** + - 缓存清理:ElMessageBox.confirm("确认清理XX缓存?清理后相关数据将从数据库重新加载", { type: 'warning' }) + - 全部清理:ElMessageBox.confirm("确认清理全部缓存?所有在线用户将收到刷新提示", { type: 'error' }) + +9. **[H8]操作结果反馈** + - 成功 ElMessage.success(2s) → silent刷新 + - 失败 ElMessage.error(0手动关闭) + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| Redis状态指示 | el-badge | 已连接:type="success", 未连接:type="danger" | +| 内存使用 | el-progress | :percentage, :stroke-width=10, :color | +| 缓存模块列表 | el-table | stripe, :show-header=true | +| 缓存条数 | el-statistic | :value, 千分位格式化 | +| 清理按钮(行) | el-button | type="danger", link, size="small", icon="Delete" | +| 全部清理按钮 | el-button | type="danger", icon="Delete", plain | +| 刷新按钮 | el-button | icon="Refresh", circle | + +### 校验规则 + +无表单校验(操作类页面) + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 状态概览一行展示;缓存模块列表完整展示 | +| 1024-1279px(Pad横屏) | 状态概览一行展示;"最后更新时间"列隐藏 | +| 768-1023px(Pad竖屏) | 状态概览竖向堆叠;缓存模块列表仅显示:模块名称、缓存条数、操作 + --- ## 需求追溯 diff --git a/docs/02-功能清单-超级管理员/04-操作日志.md b/docs/02-功能清单-超级管理员/04-操作日志.md index 8c86670..979bac9 100644 --- a/docs/02-功能清单-超级管理员/04-操作日志.md +++ b/docs/02-功能清单-超级管理员/04-操作日志.md @@ -3,6 +3,7 @@ > 模块编码:audit-log > 端侧:Web专属(仅超级管理员) > 关联文档:01-模块划分 §2.3 / 02-功能清单-超级管理员 §4 / 03-业务流转逻辑-超级管理员 §8 / 05-接口规范 §9.2 / 06-项目技术要求 §4.5 +> 强制规范遵循 `07-前端界面开发规范.md` ## 功能概览 @@ -95,6 +96,73 @@ | 列表查询 | /api/v1/audit-logs/permission | GET | 分页查询 | | 详情查询 | /api/v1/audit-logs/permission/{id} | GET | 含变更前后对比 | +### 交互流程要求 + +1. **页面加载流程** + - 进入页面 → 调用 `GET /api/v1/audit-logs/permission` 加载列表(默认倒序) + - 并行加载操作类型下拉 + - 列表为空时显示空状态 + +2. **查询交互流程** + - 填写条件 → 点击"查询"→ 调用API → 重置到第1页 + - 日期范围最多查询90天 + +3. **行内操作流程** + - **查看详情**:点击"查看"→ 弹窗展示变更前后对比 + - 新增权限绿色 `[+新增]` 标记,移除权限红色 `[-移除]` 标记 + +4. **异常处理** + - 日期范围超限 → 提示"查询时间范围不能超过90天" + - 详情加载失败 → 提示错误信息 + +5. **权限控制交互** + - 无 `audit-log:permission:view` → 页面不可见 + +6. **[H1]防重复请求**(列表页) + - 查询按钮点击后 disabled + loading态,API返回后恢复 + - 行内"查看"按钮点击后整行操作禁用 + loading态 + - 分页切换 abort上一请求后再发新请求 + +7. **[H2]超时与加载反馈**(列表页) + - 列表查询 timeout=15秒 + - 超时 → ElMessage.error("请求超时,请稍后重试") + 按钮恢复 + - 加载>2秒显示全局ElLoading + +8. **[H8]操作结果反馈** + - 查询成功 → 正常渲染;查询失败 → ElMessage.error(0手动关闭) + +9. **[H1]防重复请求**(详情弹窗) + - 详情查看:点击"查看"后该行按钮disabled,弹窗加载完成后恢复 + +10. **[H2]超时与加载反馈**(详情弹窗) + - 详情查询 timeout=15秒 + - 弹窗内 v-loading 展示加载态 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 日期范围选择 | el-date-picker | type="daterange", value-format="YYYY-MM-DD" | +| 操作类型标签 | el-tag | 角色创建:type="success", 权限修改:type="warning", 角色分配:type="primary", 角色移除:type="danger" | +| 变更摘要 | span | [+N项] color=green, [-M项] color=red | +| 详情弹窗 | el-dialog | width="650px" | +| 新增权限 | div | color=var(--el-color-success) | +| 移除权限 | div | color=var(--el-color-danger) | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 日期范围 | 最多90天 | "查询时间范围不能超过90天" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px(桌面端) | 查询条件一行排列;列表完整展示所有列 | +| 1024-1279px(Pad横屏) | 查询条件两行排列;"目标角色"列隐藏 | +| 768-1023px(Pad竖屏) | 查询条件两行排列;列表仅显示:序号、操作时间、操作人、操作类型、操作 | + --- ## 页面2:账号操作日志页 @@ -182,6 +250,58 @@ | 列表查询 | /api/v1/audit-logs/account | GET | 分页查询 | | 详情查询 | /api/v1/audit-logs/account/{id} | GET | 含变更前后数据 | +### 交互流程要求 + +1. **页面加载流程**:进入页面 → 调用列表API加载(默认倒序)→ 并行加载操作类型和账号类型下拉 +2. **查询交互**:填写条件 → 点击"查询"→ API调用 → 重置到第1页;日期范围最多90天 +3. **行内操作**:点击"查看"→ 弹窗展示完整操作信息(操作人、时间、IP、变更前后数据对比) +4. **异常处理**:日期超限提示;详情加载失败提示错误 +5. **权限控制**:无 `audit-log:list:view` → 页面不可见 + +6. **[H1]防重复请求**(列表页) + - 查询按钮点击后 disabled + loading态,API返回后恢复 + - 行内"查看"按钮点击后整行操作禁用 + loading态 + - 分页切换 abort上一请求后再发新请求 + +7. **[H2]超时与加载反馈**(列表页) + - 列表查询 timeout=15秒 + - 超时 → ElMessage.error("请求超时,请稍后重试") + 按钮恢复 + - 加载>2秒显示全局ElLoading + +8. **[H8]操作结果反馈** + - 查询成功 → 正常渲染;查询失败 → ElMessage.error(0手动关闭) + +9. **[H1]防重复请求**(详情弹窗) + - 详情查看:点击"查看"后该行按钮disabled,弹窗加载完成后恢复 + +10. **[H2]超时与加载反馈**(详情弹窗) + - 详情查询 timeout=15秒 + - 弹窗内 v-loading 展示加载态 + +### 组件规范 + +| 元素 | 组件 | 配置参数 | +|------|------|----------| +| 日期范围选择 | el-date-picker | type="daterange", value-format="YYYY-MM-DD" | +| 操作类型标签 | el-tag | 创建:type="success", 编辑:type="primary", 启用:type="success", 禁用:type="danger", 续期:type="warning", 重置密码:type="info" | +| 详情弹窗 | el-dialog | width="600px" | +| 变更前数据 | el-descriptions | title="变更前", :column=1, border | +| 变更后数据 | el-descriptions | title="变更后", :column=1, border | + +### 校验规则 + +| 字段 | 规则 | 错误提示 | +|------|------|----------| +| 日期范围 | 最多90天 | "查询时间范围不能超过90天" | + +### 响应式布局 + +| 断点 | 布局调整 | +|------|----------| +| ≥1280px | 查询条件一行;列表完整展示 | +| 1024-1279px | 查询条件两行;"绑定单位"列隐藏 | +| 768-1023px | 查询条件两行;列表仅显示:序号、操作时间、操作人、操作类型、详情 | + --- ## 需求追溯 diff --git a/docs/04-开发与测试规范.md b/docs/04-开发与测试规范.md index 1306819..2e1e01b 100644 --- a/docs/04-开发与测试规范.md +++ b/docs/04-开发与测试规范.md @@ -122,6 +122,8 @@ Repository层:数据访问(MyBatis-Plus Mapper) - 接口函数命名:`get{Resource}List`、`create{Resource}`、`update{Resource}`、`delete{Resource}` - 禁止在组件中直接调用 Axios +> **前端界面开发规范**(开发流程、Mock数据、组件标准、H约束实现、共享组件管理、自测标准)→ 详见 `07-前端界面开发规范.md` + --- ## 三、小程序开发规范(uni-app) diff --git a/docs/05-接口规范.md b/docs/05-接口规范.md index 720394f..d5a713f 100644 --- a/docs/05-接口规范.md +++ b/docs/05-接口规范.md @@ -151,8 +151,24 @@ POST /api/v1/files/upload - 请求格式:`multipart/form-data` - 参数:`file`(文件)、`module`(所属模块:REPAIR/INSPECTION/CLEANING/EVALUATION/CONTRACT)、`add_watermark`(是否添加水印,默认true) -- 存储方式:腾讯云 COS +- **存储方式:独立文件存储站点**(详见 `06-项目技术要求.md` 第十一章) +- 存储路径规则:`{module}/{YYYY/MM}/{uuid}.{ext}` +- **图片自动压缩**:上传时服务端自动按配置参数压缩(质量75、最大1920px),并生成缩略图(300px) - 大小限制:20MB +- 响应示例: +```json +{ + "code": 200, + "data": { + "fileId": "550e8400-e29b-41d4-a716-446655440000", + "originalName": "photo.jpg", + "url": "/files/repair/2026/04/550e8400-e29b-41d4-a716-446655440000.jpg", + "thumbnailUrl": "/files/thumbnail/repair/2026/04/550e8400-e29b-41d4-a716-446655440000.jpg", + "size": 256000, + "mimeType": "image/jpeg" + } +} +``` ### 3.2 图片水印规范 diff --git a/docs/06-项目技术要求.md b/docs/06-项目技术要求.md index ef0c7aa..2df392c 100644 --- a/docs/06-项目技术要求.md +++ b/docs/06-项目技术要求.md @@ -1,8 +1,8 @@ # 医院物业SaaS管理后台 — 项目技术要求 -> 版本:v1.1 -> 定位:内部团队开发标准,所有开发人员必须严格按照此标准执行 -> 日期:2026-04-16 +> 版本:v1.2 +> 定位:内部团队开发标准,所有开发人员必须严格按照此标准执行 +> 日期:2026-04-17 --- @@ -19,8 +19,6 @@ | MyBatis-Plus | 3.5+ | ORM框架 | | Spring Security | 6.x | 认证与授权 | | JWT (jjwt) | 0.12+ | Token生成与校验 | -| 腾讯云COS SDK | 最新稳定版 | 文件存储 | -| ShedLock | 5.x+ | 分布式定时任务锁 | ### 1.2 前端Web技术栈 @@ -41,14 +39,12 @@ |------|----------|------| | uni-app | 3.x | 跨端框架(Vue 3模式) | | uni-ui | 最新稳定版 | UI组件库 | -| uni-ble | 兼容最新版 | 蓝牙低功耗(BLE)插件 | ### 1.4 基础设施 | 技术 | 用途 | |------|------| -| Docker + Docker Compose | 私有云容器化部署 | -| Nginx | 反向代理 + 静态资源 | +| Nginx | 反向代理 + 静态资源 + 文件存储映射 | | Jenkins / GitLab CI | CI/CD流水线 | | Git | 版本管理 | @@ -83,7 +79,7 @@ │ 数据层 │ │ MariaDB主库 ──复制──▶ MariaDB从库(读写分离) │ │ Redis 缓存(权限/字典/菜单) │ -│ 腾讯云COS(照片/附件/合同文件) │ +│ 独立文件存储站点(照片/附件/合同文件) │ └──────────────────────────────────────────────────┘ ``` @@ -168,10 +164,10 @@ ### 3.4 定时任务 -- **替代方案**:Spring `@Scheduled` + 数据库分布式锁 -- **分布式锁实现**:ShedLock 或数据库行锁(`SELECT ... FOR UPDATE`) +- **实现方案**:Spring `@Scheduled`(当前为单实例部署,无需分布式锁) - **适用场景**:巡检任务自动生成、保洁超时预警、合同到期提醒、Beacon心跳检测 -- **实现要点**:定时方法加 `@Scheduled` 注解声明cron表达式,配合 `@ShedLock` 注解声明锁名称、最少/最多持有时间,确保多实例部署时同一时刻仅一个实例执行 +- **实现要点**:定时方法加 `@Scheduled` 注解声明cron表达式即可 +- **扩展说明**:如未来需要多实例部署,可引入 ShedLock 或数据库行锁(`SELECT ... FOR UPDATE`)防止重复执行 --- @@ -358,6 +354,7 @@ - **前后端协作规范** → 详见 `04-开发与测试规范.md` 第五章 - **代码审查要求** → 详见 `04-开发与测试规范.md` 第六章 - **测试规范**(单元测试、集成测试、蓝牙测试、性能测试、安全测试)→ 详见 `04-开发与测试规范.md` 第七章 +- **前端界面开发规范**(开发流程、Mock数据、组件标准、H约束实现、共享组件管理、自测标准)→ 详见 `07-前端界面开发规范.md` --- @@ -366,41 +363,67 @@ ### 8.1 部署架构(私有云) ``` -┌──────────────────────────────────────────┐ -│ 私有云服务器 │ -│ │ -│ ┌─────────────┐ ┌─────────────────┐ │ -│ │ Nginx │ │ Docker Compose │ │ -│ │ 反向代理 │──│ ┌────────────┐ │ │ -│ │ 静态资源 │ │ │ Spring Boot│ │ │ -│ └─────────────┘ │ │ 应用容器 │ │ │ -│ │ └────────────┘ │ │ -│ │ ┌────────────┐ │ │ -│ │ │ Redis │ │ │ -│ │ │ 缓存容器 │ │ │ -│ │ └────────────┘ │ │ -│ └─────────────────┘ │ -│ │ -│ ┌─────────────┐ ┌─────────────────┐ │ -│ │ MariaDB主库 │ │ MariaDB从库 │ │ -│ │ 写操作 │──│ 读操作 │ │ -│ └─────────────┘ └─────────────────┘ │ -└──────────────────────────────────────────┘ +┌──────────────────────────────────────────────────┐ +│ 私有云服务器 │ +│ │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ Nginx │ │ +│ │ :80/:443 │ │ +│ │ ┌──────────┐ ┌───────────┐ ┌────────────┐ │ │ +│ │ │ API反向代理│ │ 前端静态 │ │ 文件存储 │ │ │ +│ │ │ → :8080 │ │ dist目录 │ │ 映射 │ │ │ +│ │ └──────────┘ └───────────┘ └────────────┘ │ │ +│ └─────────────────────┬───────────────────────┘ │ +│ │ │ +│ ┌─────────────────────▼─────────────────────┐ │ +│ │ Spring Boot 应用 (:8080) │ │ +│ └─────────────────────┬─────────────────────┘ │ +│ │ │ +│ ┌─────────────────────▼──┐ ┌──────────────┐ │ +│ │ Redis (:6379) │ │ 文件存储目录 │ │ +│ │ │ │ /data/files/ │ │ +│ └────────────────────────┘ └──────────────┘ │ +│ │ +│ ┌────────────┐ ┌────────────┐ │ +│ │ MariaDB主库 │ │ MariaDB从库 │ │ +│ └────────────┘ └────────────┘ │ +└──────────────────────────────────────────────────┘ ``` -### 8.2 Docker Compose部署要求 +### 8.2 传统部署要求 + +#### 8.2.1 应用部署 + +| 组件 | 部署方式 | 说明 | +|------|----------|------| +| Spring Boot应用 | JAR包直接运行(`java -jar`) | 端口8080,建议使用 systemd 管理进程 | +| Redis | 直接安装(yum/apt 或官方二进制包) | 版本7.x,端口6379,开启持久化 | +| MariaDB主库 | 直接安装 | 版本10.6+,负责写操作 | +| MariaDB从库 | 直接安装 | 版本10.6+,配置主从复制,负责读操作 | +| Nginx | 直接安装 | 反向代理API + 前端静态资源 + 文件存储映射 | + +- **环境变量注入**:通过 `SPRING_PROFILES_ACTIVE`、`SPRING_DATASOURCE_URL`、`SPRING_DATASOURCE_SLAVE_URL`、`SPRING_REDIS_HOST` 等环境变量配置连接信息,**禁止硬编码** +- **JVM参数建议**:`-Xms512m -Xmx1024 -XX:+UseG1GC` +- **应用启动用户**:使用非root用户运行Spring Boot应用 -部署包含以下服务容器: +#### 8.2.2 文件存储站点部署 -| 服务 | 镜像 | 说明 | -|------|------|------| -| app | hospital-mgmt:latest | Spring Boot应用,端口8080,依赖Redis和MariaDB主库 | -| redis | redis:7-alpine | 缓存服务,持久化存储 | -| mariadb-master | mariadb:10.6 | 主库(写),持久化存储 | -| mariadb-slave | mariadb:10.6 | 从库(读),依赖主库,持久化存储 | -| nginx | nginx:alpine | 反向代理+静态资源,端口80/443,挂载自定义nginx.conf和前端dist目录 | +文件存储作为**独立存储目录**部署,通过 Nginx 提供静态文件访问服务: -- **环境变量注入**:通过 `SPRING_PROFILES_ACTIVE`、`SPRING_DATASOURCE_URL`、`SPRING_DATASOURCE_SLAVE_URL`、`SPRING_REDIS_HOST` 等环境变量配置连接信息,禁止硬编码 +``` +/data/file-storage/ +├── repair/ # 报修模块 +├── inspection/ # 巡检模块 +├── cleaning/ # 保洁模块 +├── evaluation/ # 评价模块 +├── contract/ # 合同模块 +└── temp/ # 临时目录 +``` + +- **Nginx 配置**:在 Nginx 中添加 location 映射到 `/data/file-storage/` 目录(如 `/files/` 路径或独立子域名) +- **权限设置**:应用启动用户对存储目录有读写权限,Nginx worker 用户有读权限 +- **磁盘要求**:使用独立磁盘或独立分区,与系统盘分离 +- **备份覆盖**:第六章的数据库备份策略需同步覆盖此目录 ### 8.3 小程序版本更新策略 @@ -466,8 +489,8 @@ ### 10.2 敏感配置管理 -- 数据库密码、Redis密码、JWT密钥、腾讯云SecretKey等 **禁止硬编码** -- 使用环境变量注入:`SPRING_DATASOURCE_PASSWORD`, `JWT_SECRET`, `COS_SECRET_KEY` +- 数据库密码、Redis密码、JWT密钥等 **禁止硬编码** +- 使用环境变量注入:`SPRING_DATASOURCE_PASSWORD`, `JWT_SECRET` - 生产环境密码定期轮换(每季度) ### 10.3 配置项清单 @@ -483,10 +506,130 @@ | `async.pool.core-size` | 异步线程池核心线程数 | CPU核心数 | | `async.pool.max-size` | 异步线程池最大线程数 | CPU核心数*2 | | `upload.max-size` | 文件上传大小限制(MB) | 20 | -| `cos.bucket-name` | 腾讯云COS存储桶名称 | — | +| `upload.local-path` | 本地文件存储根路径 | ./uploads/ | +| `upload.url-prefix` | 文件访问URL前缀(Nginx映射路径) | /files/ | +| `image.compress.enabled` | 是否启用图片自动压缩 | true | +| `image.compress.max-width` | 图片最大宽度(px),超过等比缩放 | 1920 | +| `image.compress.max-height` | 图片最大高度(px),超过等比缩放 | 1920 | +| `image.compress.quality` | JPEG压缩质量(0-100) | 75 | +| `image.compress.thumbnail-enabled` | 是否生成缩略图 | true | +| `image.compress.thumbnail-size` | 缩略图尺寸(px) | 300 | | `backup.full-cron` | 全量备份cron | 0 2 * * ? | | `backup.incr-interval-hours` | 增量备份间隔(小时) | 4 | --- +## 十一、文件存储站点规范 + +### 11.1 存储架构 + +文件存储作为**独立存储目录**部署,与 Spring Boot 主应用分离,通过 Nginx 提供静态文件 HTTP 访问服务: + +``` +{upload.local-path}/ +├── repair/ # 报修模块文件 +├── inspection/ # 巡检模块文件 +├── cleaning/ # 保洁模块文件 +├── evaluation/ # 评价模块文件 +├── contract/ # 合同模块文件 +├── temp/ # 临时目录(自动清理) +└── thumbnail/ # 缩略图目录 + ├── repair/ + ├── inspection/ + └── ... +``` + +### 11.2 文件存储规则 + +| 规则项 | 说明 | +|--------|------| +| 文件路径格式 | `{module}/{YYYY/MM}/{uuid}.{ext}` | +| 原始文件名 | 记录在数据库中,URL 使用 UUID 防冲突和遍历 | +| 模块取值 | REPAIR / INSPECTION / CLEANING / EVALUATION / CONTRACT | +| URL访问方式 | 通过 Nginx location 映射(如 `{url-prefix}{module}/YYYY/MM/uuid.ext`) | +| 允许的文件类型 | 图片:jpg/jpeg/png/gif/webp;文档:pdf/doc/docx/xlsx/xls/ppt/pptx;其他:txt/zip/rar(超管可配置白名单) | + +### 11.3 图片压缩规范 + +#### 压缩时机与方式 +- **服务端自动压缩**:文件上传时后端自动处理,不依赖客户端 +- **技术选型**:使用 Thumbnailator 或 Java ImageIO,无额外第三方依赖 +- **支持格式**:JPG / PNG / WebP 输入;输出为 JPEG(透明 PNG 保持 PNG 格式) + +#### 压缩参数(可在超管后台配置) + +| 配置项 | 说明 | 默认值 | +|--------|------|--------| +| `image.compress.enabled` | 是否启用图片自动压缩 | true | +| `image.compress.max-width` | 超过此宽度则等比缩放(px) | 1920 | +| `image.compress.max-height` | 超过此高度则等比缩放(px) | 1920 | +| `image.compress.quality` | JPEG 输出质量(0-100) | 75 | +| `image.compress.thumbnail-enabled` | 是否同时生成缩略图 | true | +| `image.compress.thumbnail-size` | 缩略图宽高(px) | 300 | + +#### 压缩率目标 + +| 场景 | 原始大小 | 压缩后目标 | 压缩率 | +|------|----------|-----------|--------| +| 普通照片(手机拍摄) | 5-15 MB | 200-500 KB | ~3%-5% | +| 截图等小图片 | <2 MB | <100 KB | ~5% | +| 高清图片(>15MB) | >15 MB | ≤1 MB | <1% | + +#### EXIF 处理规则 +- **保留**:拍摄时间、设备型号等信息 +- **去除**:GPS 经纬度坐标等敏感位置信息 +- **透明图片**:PNG 格式且含透明通道的图片保持原格式,不做转 JPEG 处理 + +### 11.4 超管后台 — 文件存储设置 + +#### 页面位置 +超级管理员后台 → **系统管理 → 文件存储设置** + +#### 可配置项 + +| 配置项 | 说明 | 默认值 | +|--------|------|--------| +| 存储根路径 | 文件存储基础路径(需重启生效) | ./uploads/ | +| URL访问前缀 | 文件访问的URL前缀路径 | /files/ | +| 最大文件大小 | 单文件上传上限(MB) | 20 | +| 允许的文件类型 | 文件扩展名白名单,逗号分隔 | jpg,jpeg,png,gif,webp,pdf,doc,docx,xlsx,xls | +| 图片压缩开关 | 全局开启/关闭图片自动压缩 | 开启 | +| 图片最大宽度 | 超过则等比缩放(px) | 1920 | +| 图片最大高度 | 超过则等比缩放(px) | 1920 | +| 图片压缩质量 | JPEG质量(0-100) | 75 | +| 缩略图开关 | 是否生成缩略图 | 开启 | +| 缩略图尺寸 | 缩略图宽高(px) | 300 | +| 磁盘告警阈值 | 使用率超过此值触发告警(%) | 85 | +| 临时文件清理天数 | temp目录文件自动清理周期(天) | 7 | + +#### 操作功能 + +**存储概览面板:** +- 总文件数、总占用空间、今日上传量 +- 各模块文件数及空间占比(饼图) +- 近30天上传量趋势折线图 +- 磁盘使用率仪表盘 + +**手动管理:** +- 按模块 / 日期范围筛选查看已上传文件列表 +- 支持批量删除文件(二次确认) +- 一键清理超过N天未引用的孤立文件(通过数据库关联检查) + +**监控告警:** +- 磁盘使用率超阈值时触发系统内通知 + 企业微信告警 +- 单日异常大量上传时预警(防恶意上传) + +### 11.5 安全要求 + +| 安全项 | 要求 | +|--------|------| +| 文件名安全 | 使用 UUID 重命名,禁止原始文件名直接写入路径,防止路径遍历攻击 | +| 文件类型校验 | 服务端不仅校验扩展名,还需校验文件头(Magic Number),防止伪装类型上传 | +| 文件内容扫描 | 可选集成杀毒引擎对上传文件进行病毒扫描 | +| 访问控制 | 文件访问接口需鉴权(公开资源如头像除外),支持带签名的临时访问链接 | +| 防盗链 | Nginx 配置 Referer 白名单,防止外部网站直接引用 | +| 敏感文件 | 合同文件、身份证件等建议加密存储或设置独立访问权限 | + +--- + > **本文档为内部开发标准,所有开发人员必须严格按照此标准执行。如有疑问或需要调整,需经技术负责人审批。** diff --git a/docs/07-前端界面开发规范.md b/docs/07-前端界面开发规范.md new file mode 100644 index 0000000..e00581d --- /dev/null +++ b/docs/07-前端界面开发规范.md @@ -0,0 +1,1646 @@ +# 医院物业SaaS管理后台 — 前端界面开发规范 + +> 版本:v1.0 +> 定位:前端界面开发强制规范,所有开发工作100%遵循 +> 日期:2026-04-17 +> 级别:**强制** — 除非经技术负责人审批,否则不得偏离 + +--- + +## 一、总则 + +### 1.1 适用范围 + +本规范适用于以下四套前端界面开发: + +| 端侧 | 技术栈 | UI组件库 | +|------|--------|----------| +| 超级管理员后台(Web) | Vue 3 + TypeScript + Vite | Element Plus | +| 物业公司后台(Web) | Vue 3 + TypeScript + Vite | Element Plus | +| 医院后台(Web) | Vue 3 + TypeScript + Vite | Element Plus | +| 微信小程序端 | uni-app 3.x | uni-ui | + +### 1.2 文档体系关系 + +``` +01-模块划分.md → 模块定义、角色体系 +02-功能清单-*/*.md → 页面布局、字段、按钮、弹窗、业务逻辑(页面特有) +03-业务流转逻辑-*.md → 状态机、审批流 +04-开发与测试规范.md → 代码规范(命名、分层、Git、测试) +05-接口规范.md → API定义、IModulePlugin、权限矩阵 +06-项目技术要求.md → 技术栈、架构、安全、性能 +07-前端界面开发规范.md → ★ 界面开发流程、交付标准、组件规范(本文档) +``` + +**分层原则**: +- 本文档定义**通用强制规范**(开发流程、组件标准、H约束实现、自测标准) +- 功能清单文件定义**页面特有内容**(字段、按钮、弹窗、业务交互) +- 本文档不重复功能清单中已有的H1-H8约束描述,但定义其**落地实现标准** + +### 1.3 强制级别 + +| 标记 | 含义 | 违反后果 | +|------|------|----------| +| **[强制]** | 必须遵循,无例外 | 代码审查打回 | +| **[建议]** | 推荐遵循,可申明理由偏离 | 记录偏离原因 | +| **[禁止]** | 绝对不允许 | 代码审查打回 + 问题追踪 | + +--- + +## 二、开发流程规范 + +### 2.1 五步开发流程 + +``` +用户描述功能需求 + │ + ▼ +┌─────────────────┐ +│ ① 确认修改计划 │ 输出:修改计划文档(含影响范围) +└────────┬────────┘ + ▼ +┌─────────────────┐ +│ ② 更新功能清单 │ 输出:02-功能清单-*/*.md 已更新 +└────────┬────────┘ + ▼ +┌─────────────────┐ +│ ③ 静态界面开发 │ 输出:Mock数据驱动的完整界面 +└────────┬────────┘ + ▼ +┌─────────────────┐ +│ ④ 全面自测 │ 输出:自测通过报告(见第九章Checklist) +└────────┬────────┘ + ┌────┴────┐ + ▼ 不通过 ▼ 通过 + 回到③ ┌─────────────────┐ + │ ⑤ 真实API界面 │ 输出:API联调完成的界面 + └─────────────────┘ +``` + +### 2.2 每步详解 + +#### ① 确认修改计划 + +| 项目 | 要求 | +|------|------| +| 输入 | 用户的功能需求描述 | +| 输出 | 修改计划文档,包含:涉及的功能清单文件、新增/修改的页面、共享组件影响分析 | +| 责任人 | 开发者 | +| 通过标准 | 用户确认修改计划,无遗漏 | + +**[强制]** 修改计划必须包含**共享组件影响分析**:列出本次修改涉及的共享组件,以及这些组件在哪些功能模块中被引用。详见第七章。 + +#### ② 更新功能清单 + +| 项目 | 要求 | +|------|------| +| 输入 | 已确认的修改计划 | +| 输出 | `02-功能清单-*/*.md` 中对应页面描述已更新 | +| 责任人 | 开发者 | +| 通过标准 | 功能清单与修改计划一致,用户确认 | + +**[强制]** 功能清单更新后方可开始编码。功能清单是界面开发的唯一权威依据。 + +#### ③ 静态界面开发 + +| 项目 | 要求 | +|------|------| +| 输入 | 已更新的功能清单 | +| 输出 | 使用Mock数据、无API调用的完整界面 | +| 责任人 | 开发者 | +| 通过标准 | 界面布局、交互逻辑、页面跳转全部可操作 | + +**[强制]** 静态界面必须满足: +- 所有列表页有至少20条逼真模拟数据,支持分页 +- 所有表单页可填写提交(数据写入本地Mock状态) +- 所有页面跳转和路由正常工作 +- 编辑页回填选中行的数据 +- H1-H8硬性约束全部实现 +- 错误场景(空列表/403/超时/500/校验失败)可演示 + +#### ④ 全面自测 + +| 项目 | 要求 | +|------|------| +| 输入 | 静态界面代码 | +| 输出 | 自测通过报告(按第九章Checklist逐项验证) | +| 责任人 | 开发者 | +| 通过标准 | Checklist全部通过 | + +**[强制]** 自测未通过不得进入下一步。自测范围包括共享组件引用方的回归验证。 + +#### ⑤ 真实API界面开发 + +| 项目 | 要求 | +|------|------| +| 输入 | 自测通过的静态界面 | +| 输出 | API联调完成的界面 | +| 责任人 | 开发者 + 后端联调 | +| 通过标准 | 所有接口调用正确,数据展示与接口响应一致 | + +**[强制]** API迁移支持逐模块渐进式切换(见第十章),无需一次性全部切换。 + +### 2.3 [禁止] 跳过流程 + +以下行为严格禁止: +- **[禁止]** 不更新功能清单直接编码 +- **[禁止]** 跳过自测直接进入API联调 +- **[禁止]** 静态界面未通过自测就切换到真实API +- **[禁止]** 修改共享组件不执行影响分析和覆盖测试 + +--- + +## 三、功能清单更新规范 + +### 3.1 页面描述结构 + +每个页面的功能清单描述必须包含以下章节(与现有格式一致): + +```markdown +## 页面N:{页面名称} + +**页面编号**:{模块编码}-{页面序号}-P{序号} +**端侧归属**:Web专属 / 小程序专属 / 双端 +**页面路径**:/{module}/{page} + +### 界面布局 +(ASCII布局图) + +### 查询条件 +(字段名、控件类型、必填、默认值、说明) + +### 列表字段 +(序号、字段名、列宽、支持排序、说明) + +### 操作按钮 +(按钮、权限编码、位置、显示条件、说明) + +### 弹窗定义 +(弹窗名称、触发条件、表单字段、校验规则) + +### 交互流程要求 +(步骤编号 + 描述 + H约束标记) + +### 组件规范 +(组件名称、使用场景、规范要求) + +### 前端硬性约束 +(H1~H8页面级具体实现,如确认弹窗文案等) +``` + +### 3.2 H约束补充原则 + +- 功能清单中的H1-H8约束是**页面级具体实现**(如确认弹窗的文案、超时的具体秒数) +- 通用H约束实现标准在本规范第六章定义 +- 功能清单只写**与该页面业务相关的差异化内容**,不重复通用规则 + +### 3.3 更新审批流程 + +1. 开发者根据修改计划更新功能清单 +2. 用户确认功能清单更新内容 +3. 确认后方可开始编码 + +--- + +## 四、前端项目架构规范 + +### 4.1 整体目录结构 + +``` +src/ +├── api/ # API适配器层 +│ ├── index.ts # 适配器工厂(环境变量切换) +│ ├── types/ # API类型定义 +│ │ ├── repair.ts # 报修模块 Request/Response 类型 +│ │ ├── inspection.ts +│ │ └── ... +│ ├── mock/ # Mock适配器实现 +│ │ ├── index.ts # Mock注册入口 +│ │ ├── data/ # Mock数据文件 +│ │ │ ├── repair.ts # 报修模块模拟数据(≥20条) +│ │ │ ├── inspection.ts +│ │ │ └── ... +│ │ └── adapters/ +│ │ ├── repair.ts # 报修模块MockAdapter +│ │ ├── inspection.ts +│ │ └── ... +│ └── real/ # 真实API适配器实现 +│ ├── index.ts # Axios实例 + 拦截器 +│ └── adapters/ +│ ├── repair.ts # 报修模块ApiAdapter +│ ├── inspection.ts +│ └── ... +├── components/ # 组件 +│ ├── shared/ # [共享组件] — 修改须遵守第七章规范 +│ │ ├── registry.ts # 共享组件注册表 +│ │ ├── Breadcrumb/ +│ │ ├── QueryPanel/ +│ │ ├── ActionBar/ +│ │ ├── DataTable/ +│ │ ├── Pagination/ +│ │ ├── FormDialog/ +│ │ ├── DetailDrawer/ +│ │ └── ... +│ └── business/ # 业务组件(模块独有) +│ ├── repair/ +│ ├── inspection/ +│ └── ... +├── layouts/ # 布局组件 +│ ├── AdminLayout.vue # 管理后台统一布局 +│ ├── sidebar/ # 侧边栏 +│ ├── header/ # 顶栏 +│ └── tags/ # 标签页 +├── views/ # 页面视图(按后台+模块划分) +│ ├── super-admin/ # 超级管理员后台 +│ │ ├── account/ +│ │ ├── permission/ +│ │ ├── system/ +│ │ └── log/ +│ ├── property/ # 物业公司后台 +│ │ ├── repair/ +│ │ ├── inspection/ +│ │ ├── cleaning/ +│ │ ├── org/ +│ │ ├── attendance/ +│ │ ├── evaluation/ +│ │ ├── stats/ +│ │ ├── log/ +│ │ └── system/ +│ ├── hospital/ # 医院后台 +│ │ ├── contract/ +│ │ ├── bidding/ +│ │ ├── supervise/ +│ │ ├── evaluation/ +│ │ └── stats/ +│ └── miniprogram/ # 小程序(独立项目,此处仅做参考) +├── router/ # 路由 +│ ├── index.ts # 路由入口 +│ ├── guards.ts # 路由守卫 +│ └── modules/ # 按后台拆分路由模块 +│ ├── super-admin.ts +│ ├── property.ts +│ └── hospital.ts +├── stores/ # Pinia状态管理 +│ ├── global/ # 全局状态 +│ │ ├── useUserStore.ts # 用户信息+权限 +│ │ ├── useDictStore.ts # 字典数据 +│ │ └── useAppStore.ts # 应用配置 +│ └── modules/ # 模块状态 +│ ├── useRepairStore.ts +│ └── ... +├── styles/ # 样式 +│ ├── variables.scss # Element Plus主题变量覆盖 +│ ├── mixins.scss # 公共mixin +│ └── reset.scss # 样式重置 +├── utils/ # 工具函数 +│ ├── request.ts # Axios封装(拦截器、Token、错误处理) +│ ├── permission.ts # 权限校验工具 +│ ├── dirty-check.ts # H4脏数据检测工具 +│ ├── debounce-submit.ts # H1防重复请求工具 +│ ├── result-feedback.ts # H8操作结果反馈工具 +│ └── shared-impact.ts # 共享组件影响分析工具 +├── hooks/ # 组合式函数 +│ ├── useListPage.ts # 列表页通用逻辑 +│ ├── useFormPage.ts # 表单页通用逻辑 +│ └── usePermission.ts # 权限控制Hook +└── types/ # 全局类型 + ├── enums/ # 业务枚举 + ├── api.d.ts # API通用类型 + └── global.d.ts # 全局类型声明 +``` + +### 4.2 多后台差异化规范 + +**[强制]** 三个Web后台遵循以下目录划分原则: + +| 规则 | 说明 | +|------|------| +| 独立路由入口 | 每个后台有独立路由模块(`router/modules/`) | +| 独立页面目录 | 页面视图按后台分目录(`views/super-admin/`、`views/property/`、`views/hospital/`) | +| 共享组件目录 | 跨后台复用的组件放在 `components/shared/` | +| 业务组件目录 | 模块独有组件放在 `components/business/{module}/` | +| **[禁止]跨后台复制粘贴** | 同一功能在不同后台必须复用共享组件,不得复制代码 | + +### 4.3 环境变量与API模式切换 + +**`.env` 配置**: + +```bash +# API模式:mock | real | 渐进式 +VITE_API_MODE=mock + +# 渐进式迁移示例:报修用真实API,其余用Mock +# VITE_API_MODE=mock:repair=real,inspection=mock,cleaning=mock + +# API基础路径 +VITE_API_BASE_URL=/api/v1 + +# 超时配置(与H2约束对齐) +VITE_API_TIMEOUT_GET=15000 +VITE_API_TIMEOUT_POST=30000 +VITE_API_TIMEOUT_UPLOAD=60000 +VITE_API_TIMEOUT_STATS=30000 +``` + +**适配器工厂**: + +```typescript +// api/index.ts +import type { IRepairApi } from './types/repair' +// ... 其他模块类型 + +type ApiMode = 'mock' | 'real' + +function getModuleMode(moduleCode: string): ApiMode { + const mode = import.meta.env.VITE_API_MODE + if (mode === 'mock' || mode === 'real') return mode + + // 渐进式解析:mock:repair=real,inspection=mock + const overrides = mode.split(':').slice(1)[0]?.split(',') || [] + const override = overrides.find(o => o.startsWith(`${moduleCode}=`)) + return override ? override.split('=')[1] as ApiMode : mode.split(':')[0] as ApiMode +} + +// 懒加载适配器 +export function getRepairApi(): IRepairApi { + const mode = getModuleMode('repair') + return mode === 'real' + ? import('./real/adapters/repair').then(m => new m.RepairApiAdapter()) + : import('./mock/adapters/repair').then(m => new m.RepairApiAdapter()) +} +``` + +### 4.4 路由组织规范 + +```typescript +// router/modules/property.ts +export default [ + { + path: '/repair', + component: AdminLayout, + meta: { platform: 'property' }, + children: [ + { + path: 'orders', + name: 'RepairOrderList', + component: () => import('@/views/property/repair/OrderList.vue'), + meta: { + title: '工单列表', + permissions: ['repair:list:view'], + keepAlive: true + } + }, + { + path: 'orders/:id', + name: 'RepairOrderDetail', + component: () => import('@/views/property/repair/OrderDetail.vue'), + meta: { + title: '工单详情', + permissions: ['repair:detail:view'] + } + } + ] + } +] +``` + +**[强制]** 路由meta必须包含: +- `title`:页面标题(面包屑/标签页使用) +- `permissions`:权限编码数组(路由守卫判断) +- `keepAlive`:列表页设为true(缓存查询状态) + +### 4.5 布局框架规范 + +所有Web后台使用统一 `AdminLayout`,结构如下: + +``` +┌──────────────────────────────────────────────┐ +│ Header(顶栏:Logo/用户信息/退出) │ +├────────┬─────────────────────────────────────┤ +│ │ TagsView(标签页导航) │ +│ Side ├─────────────────────────────────────┤ +│ bar │ │ +│ (侧 │ Main Content(页面内容区) │ +│ 边 │ │ +│ 栏) │ │ +│ │ │ +├────────┴─────────────────────────────────────┤ +│ Footer(可选:版权信息) │ +└──────────────────────────────────────────────┘ +``` + +--- + +## 五、静态界面开发规范 + +### 5.1 Mock数据规范 + +#### 5.1.1 类型定义 + +**[强制]** 每个模块的Mock数据必须有对应的TypeScript类型定义,类型定义与功能清单中的列表字段、表单字段一一对应: + +```typescript +// api/types/repair.ts + +/** 工单列表项 — 对应功能清单"列表字段"表格 */ +export interface RepairOrderItem { + id: string + orderNo: string // 工单号 + repairType: string // 报修类型 + urgency: 'URGENT' | 'NORMAL' | 'LOW' // 紧急程度 + status: RepairOrderStatus // 状态 + reporterName: string // 报修人 + teamName: string // 负责班组 + handlerName: string // 维修人员 + submitTime: string // 提交时间 + appointmentTime: string // 预约时间 + isSupplement: boolean // 补录标记 +} + +/** 工单列表查询参数 — 对应功能清单"查询条件"表格 */ +export interface RepairOrderQuery { + orderNo?: string + status?: RepairOrderStatus[] + repairType?: string + urgency?: string + submitDateRange?: [string, string] + teamId?: string + areaId?: string + page: number + pageSize: number +} + +/** 工单列表响应 */ +export interface RepairOrderListResponse { + list: RepairOrderItem[] + pagination: { + page: number + pageSize: number + total: number + totalPages: number + } +} + +/** API接口定义 — Mock和Real共同实现 */ +export interface IRepairApi { + getOrderList(query: RepairOrderQuery): Promise + getOrderDetail(id: string): Promise + createOrder(data: CreateRepairOrderRequest): Promise<{ id: string }> + updateOrder(id: string, data: UpdateRepairOrderRequest): Promise + deleteOrder(id: string): Promise +} +``` + +#### 5.1.2 数据量要求 + +| 场景 | 最少数据量 | 说明 | +|------|-----------|------| +| 列表页 | 20条 | 支撑分页演示(默认每页20条,至少2页) | +| 下拉选项 | 5-10个 | 班组/区域/类型等 | +| 树形结构 | 3级×5个 | 区域/组织等层级 | +| 级联选择 | 3级×5个 | 项目→区域→楼栋→楼层 | + +#### 5.1.3 数据逼真度要求 + +**[强制]** Mock数据必须符合真实业务场景: + +| 字段类型 | 逼真度要求 | 示例 | +|----------|-----------|------| +| 姓名 | 中文常见姓名 | 张伟、李明、王芳 | +| 地址 | 医院建筑格式 | 1号楼3层301室 | +| 工单号 | 带前缀+日期+序号 | WX202604170001 | +| 时间 | 合理日期范围 | 近3个月内 | +| 状态 | 符合业务状态分布 | 待分配20%、处理中40%、已完成30%、已关闭10% | +| 金额 | 合理范围 | 合同金额50万-500万 | +| 手机号 | 脱敏格式 | 138****1234 | + +#### 5.1.4 边界值覆盖 + +**[强制]** Mock数据必须包含以下边界场景: + +| 场景 | 数据要求 | +|------|----------| +| 空列表 | 返回 `list: [], pagination: { total: 0 }` | +| 长文本 | 备注/描述字段包含50+字的文本 | +| 特殊字符 | 工单描述包含换行、引号、尖括号 | +| 补录数据 | 至少2条 `isSupplement: true` 的记录 | +| 未分配 | 至少3条无负责人/班组的记录 | +| 紧急工单 | 至少2条 `urgency: 'URGENT'` 的记录 | + +#### 5.1.5 错误场景Mock + +**[强制]** MockAdapter必须内置以下错误场景触发机制: + +```typescript +// api/mock/adapters/repair.ts +export class RepairApiAdapter implements IRepairApi { + + async getOrderList(query: RepairOrderQuery): Promise { + await this.delay() + + // 错误场景触发(通过特殊query参数) + if (query._mockError === 'empty') { + return { list: [], pagination: { page: 1, pageSize: 20, total: 0, totalPages: 0 } } + } + if (query._mockError === '403') { + throw new ApiError(40300, '无数据权限(越权访问)') + } + if (query._mockError === 'timeout') { + await this.delay(16000) // 超过GET 15s阈值 + return normalData + } + if (query._mockError === '500') { + throw new ApiError(50000, '服务器内部错误') + } + + // 正常数据返回 + return this.filterAndPaginate(mockRepairOrders, query) + } + + /** 模拟网络延时 200-800ms */ + private delay(ms?: number): Promise { + const duration = ms ?? Math.floor(Math.random() * 600) + 200 + return new Promise(resolve => setTimeout(resolve, duration)) + } +} +``` + +**错误场景测试入口**:在开发环境的查询面板中提供"模拟错误"下拉框,可切换空列表/403/超时/500场景。 + +### 5.2 组件使用标准 + +**[强制]** 界面组件严格按功能清单"组件规范"表格选用Element Plus组件: + +| 功能清单组件规范 | Element Plus组件 | 说明 | +|----------------|-----------------|------| +| 文本输入 | `el-input` | — | +| 下拉单选 | `el-select` | — | +| 下拉多选 | `el-select multiple` | — | +| 日期选择 | `el-date-picker` | — | +| 日期范围 | `el-date-picker type="daterange"` | — | +| 级联选择 | `el-cascader` | 区域/组织树 | +| 表格 | `el-table` | 列表数据展示 | +| 分页 | `el-pagination` | — | +| 对话框 | `el-dialog` | 弹窗表单 | +| 抽屉 | `el-drawer` | 详情侧滑 | +| 确认弹窗 | `ElMessageBox.confirm` | H3操作确认 | +| 消息提示 | `ElMessage.success/error` | H8结果反馈 | +| 加载 | `ElLoading.service` | H2加载反馈 | +| 标签 | `el-tag` | 状态/类型标记 | +| 树控件 | `el-tree` | 组织架构/区域树 | +| 文件上传 | `el-upload` | H7文件约束 | + +### 5.3 交互实现标准 + +#### 5.3.1 列表页交互 + +| 交互 | 实现要求 | +|------|----------| +| 查询 | 点击查询按钮 → 触发查询 → 列表刷新到第1页 | +| 重置 | 点击重置 → 清空所有筛选条件 → 重新查询 | +| 分页 | 切换页码/每页条数 → 刷新列表 → 保留筛选条件 | +| 排序 | 点击列头排序 → 触发查询 → 保留筛选条件 | +| 新增 | 点击新增 → 打开弹窗/跳转新增页 → 提交后刷新列表 | +| 编辑 | 点击编辑/行内编辑 → 打开弹窗回填数据 → 提交后刷新列表 | +| 删除 | 点击删除 → H3确认弹窗 → 确认后删除 → H8结果反馈 → 刷新列表 | +| 批量操作 | 勾选行 → 按钮启用 → 点击 → H3确认 → 执行 → H8反馈 | +| 导出 | 点击导出 → H3确认 → 调用导出API → 下载文件 → H8反馈 | + +#### 5.3.2 表单页交互 + +| 交互 | 实现要求 | +|------|----------| +| 表单校验 | 提交前 `el-form.validate()` — 全部通过才提交 | +| 取消/返回 | H4脏数据检测 — 有修改弹确认框,无修改直接返回 | +| 保存 | H1防重复 → 提交 → H8反馈 → 成功后跳转/关闭 | +| 字段联动 | 上级字段变更 → 清空下级字段 → 重新加载选项 | + +#### 5.3.3 页面跳转与数据传递 + +**[强制]** 页面间数据传递遵循以下规则: + +| 场景 | 传递方式 | 实现要求 | +|------|----------|----------| +| 列表→详情 | 路由参数 `/:id` | 详情页通过ID重新查询完整数据 | +| 列表→编辑 | 路由参数 `/:id/edit` | 编辑页通过ID查询数据并回填 | +| 新增→列表 | 路由跳转 | 新增成功后跳转列表并刷新 | +| 弹窗编辑 | 组件Props | 传入行数据,弹窗内回填 | + +**[强制]** 编辑页必须回填选中行的完整数据: + +```typescript +// views/property/repair/OrderEdit.vue +const route = useRoute() +const router = useRouter() + +onMounted(async () => { + const id = route.params.id as string + if (id) { + // 通过ID查询完整数据,回填表单 + const detail = await repairApi.getOrderDetail(id) + Object.assign(formData, detail) + // H4: 保存初始快照 + initialSnapshot = deepClone(formData) + } +}) +``` + +--- + +## 六、H1-H8硬性约束实现标准 + +### 6.1 H1 防重复请求 [强制] + +```typescript +// utils/debounce-submit.ts + +interface PendingRequest { + key: string + timestamp: number +} + +const pendingRequests = new Map() + +/** 防重复提交装饰器 */ +export function useDebounceSubmit() { + const submitLoading = ref(false) + + async function debounceSubmit( + key: string, + fn: () => Promise, + options?: { cooldown?: number } + ): Promise { + if (submitLoading.value) return null + + const pending = pendingRequests.get(key) + const cooldown = options?.cooldown ?? 1000 + if (pending && Date.now() - pending.timestamp < cooldown) { + return null + } + + submitLoading.value = true + pendingRequests.set(key, { key, timestamp: Date.now() }) + + try { + return await fn() + } finally { + submitLoading.value = false + pendingRequests.delete(key) + } + } + + return { submitLoading, debounceSubmit } +} +``` + +**实现要求**: + +| 要求 | 实现 | +|------|------| +| pending去重 | Map结构记录进行中的请求key | +| 按钮disabled | `submitLoading` 绑定按钮 `:disabled` | +| loading态 | `submitLoading` 绑定按钮 `:loading` | +| abort重发 | 列表查询使用 `AbortController`,新查询abort上一个 | + +### 6.2 H2 超时与加载反馈 [强制] + +```typescript +// utils/request.ts 中的超时配置 + +const service = axios.create({ + timeout: 15000, // 默认GET 15s +}) + +// 按请求类型动态超时 +service.interceptors.request.use(config => { + if (config.method === 'post' || config.method === 'put') { + config.timeout = 30000 // POST/PUT 30s + } + if (config.url?.includes('/upload')) { + config.timeout = 60000 // 上传 60s + } + if (config.url?.includes('/statistics') || config.url?.includes('/report')) { + config.timeout = 30000 // 统计报表 30s + } + return config +}) +``` + +**实现要求**: + +| 要求 | 实现 | +|------|------| +| GET 15s | Axios默认timeout | +| POST 30s | 请求拦截器动态设置 | +| 上传导出 60s | URL匹配设置 | +| 统计 30s | URL匹配设置 | +| 超时提示 | 响应拦截器捕获ECONNABORTED | +| 加载>2秒 | 全局loading(`ElLoading.service`) | + +### 6.3 H3 操作确认机制 [强制] + +```typescript +// 通用确认弹窗 +import { ElMessageBox } from 'element-plus' + +/** 操作确认 — 具体文案由功能清单页面级H3定义 */ +export async function confirmAction(message: string, title = '操作确认'): Promise { + try { + await ElMessageBox.confirm(message, title, { + confirmButtonText: '确认', + cancelButtonText: '取消', + type: 'warning', + }) + return true + } catch { + return false + } +} + +// 使用示例(具体文案来自功能清单) +async function handleDelete(row: RepairOrderItem) { + const confirmed = await confirmAction( + `确认删除工单 ${row.orderNo}?删除后不可恢复。` + ) + if (!confirmed) return + // ... 执行删除 +} +``` + +**实现要求**: +- 不可逆操作(删除、关闭、终止、批量操作)必须二次确认 +- 确认弹窗文案在功能清单页面级H3中定义 +- 小程序端使用 `wx.showModal` 替代 `ElMessageBox.confirm` + +### 6.4 H4 脏数据检测 [强制] + +```typescript +// utils/dirty-check.ts +import { ref, watch } from 'vue' +import { ElMessageBox } from 'element-plus' +import type { Router } from 'vue-router' + +/** 脏数据检测 */ +export function useDirtyCheck>( + formData: Ref, + router: Router +) { + const initialSnapshot = ref('') + const isDirty = ref(false) + + // 保存初始快照 + function saveSnapshot() { + initialSnapshot.value = JSON.stringify(formData.value) + } + + // 监听变化 + watch(formData, () => { + isDirty.value = JSON.stringify(formData.value) !== initialSnapshot.value + }, { deep: true }) + + // 路由离开拦截 + router.beforeEach(async (to, from, next) => { + if (isDirty.value && from.name?.toString().includes('Edit')) { + try { + await ElMessageBox.confirm('当前页面有未保存的修改,确认离开?', '提示', { + confirmButtonText: '离开', + cancelButtonText: '留在此页', + type: 'warning', + }) + next() + } catch { + next(false) + } + } else { + next() + } + }) + + // 浏览器关闭拦截 + onMounted(() => { + window.addEventListener('beforeunload', handleBeforeUnload) + }) + onUnmounted(() => { + window.removeEventListener('beforeunload', handleBeforeUnload) + }) + + function handleBeforeUnload(e: BeforeUnloadEvent) { + if (isDirty.value) { + e.preventDefault() + e.returnValue = '' + } + } + + return { isDirty, saveSnapshot } +} +``` + +**实现要求**: + +| 要求 | 实现 | +|------|------| +| deep clone快照 | `JSON.stringify` 对比 | +| isDirty检测 | `watch` deep监听 | +| 取消拦截 | 弹确认框 | +| 离开拦截 | `router.beforeEach` + `beforeunload` | +| 仅编辑型表单 | 新增页不检测 | + +### 6.5 H5 数据权限隔离 [建议] + +```typescript +// 403错误处理 — 在Axios响应拦截器中 +service.interceptors.response.use( + response => response, + error => { + if (error.response?.data?.code === 40300) { + ElMessage.error('您没有权限访问该数据') + } else if (error.response?.data?.code === 40301) { + // 数据权限越权 — 显示"暂无数据"而非"无权限" + // 由页面组件根据上下文判断显示"暂无数据" + } + return Promise.reject(error) + } +) +``` + +### 6.6 H6 批量操作限制 [建议] + +| 操作 | 上限 | 超限提示 | +|------|------|----------| +| 批量删除 | 50条 | "单次最多删除50条,请分批操作" | +| 批量导出 | 500条 | "导出数据量较大,请缩小查询范围" | +| 批量审批 | 100条 | "单次最多审批100条" | + +### 6.7 H7 文件上传约束 [建议] + +```typescript +// 文件上传前校验 +export function validateFileUpload(file: File): string | null { + const MAX_SIZE = 10 * 1024 * 1024 // 10MB + const MAX_COUNT = 9 + const ALLOWED_TYPES = [ + 'image/jpeg', 'image/png', 'image/gif', 'image/webp', + 'application/pdf', + 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ] + + if (file.size > MAX_SIZE) return '文件大小不能超过10MB' + if (!ALLOWED_TYPES.includes(file.type)) return '不支持该文件类型' + return null +} +``` + +### 6.8 H8 操作结果反馈 [强制] + +```typescript +// utils/result-feedback.ts +import { ElMessage } from 'element-plus' + +/** 操作成功反馈 */ +export function showSuccess(message = '操作成功', duration = 2000) { + ElMessage.success({ message, duration }) +} + +/** 操作失败反馈 */ +export function showError(message = '操作失败', duration = 0) { + ElMessage.error({ message, duration }) // duration=0 需手动关闭 +} + +/** 网络异常反馈 */ +export function showNetworkError(retryCallback?: () => void) { + ElMessage.error({ + message: '网络异常,请检查网络后重试', + duration: 0, + showClose: true, + }) +} +``` + +**实现要求**: + +| 场景 | 反馈方式 | 持续时间 | +|------|----------|----------| +| 操作成功 | `ElMessage.success` + silent刷新列表 | 2秒自动关闭 | +| 操作失败 | `ElMessage.error` | 0(手动关闭) | +| 网络异常 | `ElMessage.error` + 重试按钮 | 0(手动关闭) | +| 小程序成功 | `wx.showToast({ icon: 'success' })` | 2秒 | +| 小程序失败 | `wx.showToast({ icon: 'none' })` | 0(手动关闭) | + +--- + +## 七、共享组件管理规范 + +### 7.1 共享组件定义 + +**共享组件**:被2个及以上功能模块或2个及以上后台引用的组件。 + +### 7.2 共享组件标记 [强制] + +**[强制]** 所有共享组件必须在文件头部标记 `@shared` 注释,并在 `components/shared/registry.ts` 中注册: + +```typescript +// components/shared/registry.ts + +/** + * 共享组件注册表 + * 修改任何共享组件前,必须查阅此注册表确认影响范围 + * 修改后必须执行100%覆盖测试,并向用户发出变更通知 + */ +export const SharedComponentRegistry = { + Breadcrumb: { + path: 'components/shared/Breadcrumb/index.vue', + description: '面包屑导航', + referencedBy: ['所有页面'] as string[], + }, + QueryPanel: { + path: 'components/shared/QueryPanel/index.vue', + description: '查询条件面板(折叠/展开)', + referencedBy: ['repair:OrderList', 'inspection:TaskList', 'cleaning:AreaList', 'attendance:RecordList'] as string[], + }, + ActionBar: { + path: 'components/shared/ActionBar/index.vue', + description: '操作按钮栏(权限控制+批量操作)', + referencedBy: ['repair:OrderList', 'inspection:TaskList', 'org:StaffList', 'contract:ContractList'] as string[], + }, + DataTable: { + path: 'components/shared/DataTable/index.vue', + description: '数据表格(排序/选择/行操作)', + referencedBy: ['所有列表页'] as string[], + }, + Pagination: { + path: 'components/shared/Pagination/index.vue', + description: '分页组件', + referencedBy: ['所有列表页'] as string[], + }, + FormDialog: { + path: 'components/shared/FormDialog/index.vue', + description: '表单弹窗(新增/编辑)', + referencedBy: ['repair:OrderList', 'inspection:PlanList', 'cleaning:AreaList', 'org:TeamList'] as string[], + }, + DetailDrawer: { + path: 'components/shared/DetailDrawer/index.vue', + description: '详情侧滑抽屉', + referencedBy: ['repair:OrderList', 'inspection:TaskList', 'contract:ContractList'] as string[], + }, + StatusTag: { + path: 'components/shared/StatusTag/index.vue', + description: '状态标签(彩色区分)', + referencedBy: ['repair:OrderList', 'inspection:TaskList', 'cleaning:TaskBoard', 'contract:ContractList'] as string[], + }, +} as const + +/** 获取组件被哪些模块引用 */ +export function getComponentReferences(componentName: string): string[] { + return SharedComponentRegistry[componentName]?.referencedBy ?? [] +} +``` + +### 7.3 共享组件修改流程 [强制] + +**[强制]** 修改共享组件必须执行以下流程: + +``` +识别修改涉及共享组件 + │ + ▼ +查询 registry.ts 获取引用模块列表 + │ + ▼ +制定修改方案(必须向后兼容或同步更新所有引用方) + │ + ▼ +执行修改 + │ + ▼ +执行100%覆盖测试(所有引用模块) + │ + ▼ +向用户发出变更通知 +``` + +### 7.4 共享组件100%覆盖测试 [强制] + +**[强制]** 修改共享组件后,必须对所有引用模块执行回归测试: + +| 测试项 | 验证方法 | +|--------|----------| +| 功能正常 | 每个引用模块的核心功能正常运行 | +| 样式一致 | 每个引用模块的组件显示效果与修改前一致 | +| 交互完整 | 每个引用模块的组件交互行为正常 | +| 无副作用 | 未直接引用该组件的模块不受影响 | +| 类型安全 | TypeScript类型检查通过 | + +### 7.5 共享组件变更通知 [强制] + +**[强制]** 当修改涉及共享组件时,开发者(含AI助手)自测通过后,必须向用户输出以下**变更影响报告**: + +```markdown +## 共享组件变更通知 + +### 变更内容 +- 修改的共享组件:{组件名} +- 修改内容:{具体改了什么} +- 修改原因:{为什么改} + +### 影响范围 +- 引用该组件的功能模块: + - {模块1}:{页面1}、{页面2} + - {模块2}:{页面3} +- 受影响的后台:{超级管理员/物业公司/医院} + +### 自测结果 +- ✅ {模块1}-{页面1}:功能正常 +- ✅ {模块1}-{页面2}:功能正常 +- ✅ {模块2}-{页面3}:功能正常 + +### 需要人工测试 +请重点验证以上引用模块的显示效果和交互行为。 +``` + +### 7.6 共享组件修改原则 [强制] + +| 原则 | 说明 | +|------|------| +| **[强制] 向后兼容优先** | 新增Props/Slot,不删除已有Props/Slot | +| **[强制] 废弃标记** | 如需废弃Props,先用`@deprecated`标记,至少保留1个版本周期 | +| **[强制] 禁止破坏性修改** | 不得修改已有Props的默认值、类型、语义 | +| **[建议] 版本化** | 重大变更时创建V2组件(如`FormDialogV2`),并行存在 | + +--- + +## 八、跨后台风格统一规范 + +### 8.1 主题变量统一 + +```scss +// styles/variables.scss — Element Plus主题变量覆盖 + +// 品牌色 +$--color-primary: #409EFF; +$--color-success: #67C23A; +$--color-warning: #E6A23C; +$--color-danger: #F56C6C; +$--color-info: #909399; + +// 布局 +$--layout-sidebar-width: 220px; +$--layout-sidebar-collapsed-width: 64px; +$--layout-header-height: 56px; +$--layout-tags-height: 36px; +$--layout-footer-height: 48px; + +// 字体 +$--font-size-base: 14px; +$--font-size-small: 12px; +$--font-size-large: 16px; + +// 间距 +$--spacing-page: 20px; +$--spacing-section: 16px; +$--spacing-item: 12px; +``` + +### 8.2 布局结构统一 + +**[强制]** 三个Web后台使用完全一致的布局框架: + +| 区域 | 规范 | 实现 | +|------|------|------| +| 侧边栏 | 固定宽度220px,可折叠至64px | `AdminLayout` + `el-menu` | +| 顶栏 | 固定高度56px,含Logo/面包屑/用户信息 | `LayoutHeader` | +| 标签页 | 固定高度36px,支持右键关闭 | `TagsView` | +| 内容区 | 自适应宽度,内边距20px | `` | +| 全局Loading | 固定在内容区中央 | `ElLoading.service` | + +### 8.3 公共组件统一 + +**[强制]** 以下公共组件跨后台必须使用共享组件,不得各后台自行实现: + +| 组件 | 用途 | 规范 | +|------|------|------| +| `Breadcrumb` | 面包屑导航 | 自动从路由meta生成 | +| `QueryPanel` | 查询条件面板 | 支持折叠/展开,默认收起超过3行 | +| `ActionBar` | 操作按钮栏 | 权限控制 + 批量操作启用/禁用 | +| `DataTable` | 数据表格 | 统一空数据文案、行高、斑马纹 | +| `Pagination` | 分页 | 统一布局:总数 + 每页条数 + 页码 | +| `FormDialog` | 表单弹窗 | 统一宽度/确认取消按钮/H4脏数据检测 | +| `DetailDrawer` | 详情侧滑 | 统一宽度/关闭确认 | +| `StatusTag` | 状态标签 | 统一颜色方案:待处理=warning/进行中=primary/完成=success/关闭=info | + +--- + +## 九、自测检查清单 + +### 9.1 功能清单符合度 + +| # | 检查项 | 验证方法 | 通过标准 | +|---|--------|----------|----------| +| 1 | 界面布局与功能清单ASCII图一致 | 人工对比 | 布局区域、排列顺序一致 | +| 2 | 查询条件字段完整 | 逐一核对 | 字段名、控件类型、必填性一致 | +| 3 | 列表字段完整 | 逐一核对 | 列名、列宽、排序、说明一致 | +| 4 | 操作按钮完整 | 逐一核对 | 按钮名、权限编码、显示条件一致 | +| 5 | 弹窗定义完整 | 逐一核对 | 弹窗名称、字段、校验规则一致 | +| 6 | 页面路径正确 | 浏览器访问 | 路由可正常跳转 | +| 7 | 权限编码正确 | 模拟不同权限 | 按钮显示/隐藏符合预期 | + +### 9.2 交互完整性 + +| # | 检查项 | 验证方法 | 通过标准 | +|---|--------|----------|----------| +| 1 | 查询/重置功能 | 输入条件→查询→重置 | 列表数据正确筛选和重置 | +| 2 | 分页功能 | 翻页→改每页条数 | 数据正确切换 | +| 3 | 排序功能 | 点击列头 | 排序方向和数据正确 | +| 4 | 新增功能 | 点击新增→填写→提交 | 数据添加成功,列表刷新 | +| 5 | 编辑功能 | 点击编辑→修改→提交 | 数据更新成功,列表刷新 | +| 6 | 删除功能 | 点击删除→确认 | 数据删除成功,H3确认弹窗出现 | +| 7 | 批量操作 | 勾选→批量操作 | 操作成功,未勾选时按钮禁用 | +| 8 | 页面跳转 | 列表→详情→返回 | 路由正常,数据正确 | +| 9 | 编辑回填 | 列表→编辑 | 选中行数据完整回填到表单 | +| 10 | 表单校验 | 提交空表单 | 必填项校验提示正确 | + +### 9.3 H约束合规性 + +| # | 检查项 | 验证方法 | 通过标准 | +|---|--------|----------|----------| +| 1 | H1防重复 | 快速双击提交按钮 | 第二次点击无效,按钮loading态 | +| 2 | H2超时 | 触发超时Mock | 超时提示出现,按钮恢复可点击 | +| 3 | H2加载反馈 | 查询大量数据 | 加载>2秒时全局loading显示 | +| 4 | H3操作确认 | 点击删除/批量操作 | 确认弹窗出现,文案与功能清单一致 | +| 5 | H4脏数据 | 编辑表单→不保存→返回 | 确认弹窗出现"有未保存修改" | +| 6 | H4浏览器关闭 | 编辑表单→关闭浏览器 | 浏览器提示"有未保存修改" | +| 7 | H5权限隔离 | 无权限账号访问 | 显示"暂无数据"而非报错 | +| 8 | H6批量限制 | 勾选超过限制数量 | 提示"单次最多N条" | +| 9 | H7上传约束 | 上传超限文件 | 提示大小/类型/数量限制 | +| 10 | H8成功反馈 | 提交成功 | 成功提示2秒自动消失,列表刷新 | +| 11 | H8失败反馈 | 模拟500错误 | 错误提示需手动关闭 | +| 12 | H8网络异常 | 断开网络操作 | 网络异常提示 + 重试 | + +### 9.4 组件规范合规性 + +| # | 检查项 | 验证方法 | 通过标准 | +|---|--------|----------|----------| +| 1 | 使用Element Plus指定组件 | 代码审查 | 无自行实现的表格/分页/弹窗等 | +| 2 | ` +``` + +### 11.2 标准表单弹窗模板 + +```vue + + + + +``` + +--- + +## 十二、接口文档对齐机制 + +### 12.1 前后端类型对齐流程 + +``` +功能清单确定字段 + │ + ▼ +前端定义TypeScript类型(api/types/) + │ + ├──────────────────────┐ + ▼ ▼ +前端MockAdapter 后端定义DTO/VO +基于TS类型生成Mock数据 基于Swagger生成接口文档 + │ │ + ▼ ▼ + 静态界面开发 后端接口开发 + │ │ + └──────────┬───────────┘ + ▼ + 联调阶段 + 对比TS类型 vs Swagger Schema + │ + ┌──────┴──────┐ + ▼ 一致 ▼ 不一致 + 直接联调 协商修改 → 更新类型 → 重新联调 +``` + +### 12.2 对齐要求 + +| 对齐项 | 要求 | +|--------|------| +| 字段名 | 前端TS类型的字段名必须与后端DTO/VO的JSON字段名一致 | +| 数据类型 | TS类型与后端Java类型对应(String↔string, Long↔number, List↔Array, Enum↔union type) | +| 分页结构 | 统一使用 `{ list, pagination }` 格式 | +| 枚举值 | 前端枚举值必须与后端枚举的name()一致 | +| 必填性 | TS类型可选字段与后端@Nullable对应 | + +### 12.3 类型同步工具(建议) + +- **方式一**:后端Swagger → `openapi-typescript` 自动生成前端TS类型 +- **方式二**:前端TS类型 → 手动维护,联调时与Swagger文档对照 +- **推荐**:联调初期用手动维护(灵活),稳定后切换到自动生成(防漂移) + +--- + +> **本文档为前端界面开发强制规范,所有开发人员必须严格遵守。如有疑问或需要调整,需经技术负责人审批。** diff --git a/frontend/super-admin/.env.development b/frontend/super-admin/.env.development new file mode 100644 index 0000000..62770b1 --- /dev/null +++ b/frontend/super-admin/.env.development @@ -0,0 +1,2 @@ +VITE_API_MODE=mock +VITE_API_BASE_URL=/api/v1 diff --git a/frontend/super-admin/.env.production b/frontend/super-admin/.env.production new file mode 100644 index 0000000..59ba15f --- /dev/null +++ b/frontend/super-admin/.env.production @@ -0,0 +1,2 @@ +VITE_API_MODE=real +VITE_API_BASE_URL=/api/v1 diff --git a/frontend/super-admin/.gitignore b/frontend/super-admin/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/super-admin/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/super-admin/README.md b/frontend/super-admin/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/frontend/super-admin/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/frontend/super-admin/package-lock.json b/frontend/super-admin/package-lock.json new file mode 100644 index 0000000..02ffdd1 --- /dev/null +++ b/frontend/super-admin/package-lock.json @@ -0,0 +1,2253 @@ +{ + "name": "super-admin", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "super-admin", + "version": "0.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "axios": "^1.15.0", + "echarts": "^6.0.0", + "element-plus": "^2.13.7", + "pinia": "^2.3.1", + "sass": "^1.99.0", + "vue": "^3.5.32", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@types/node": "^24.12.2", + "@vitejs/plugin-vue": "^6.0.5", + "@vue/tsconfig": "^0.9.1", + "typescript": "~6.0.2", + "vite": "^8.0.4", + "vue-tsc": "^3.2.6" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz", + "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz", + "integrity": "sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.13" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", + "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.32", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", + "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", + "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.32", + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", + "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.6.tgz", + "integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", + "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", + "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", + "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.32", + "@vue/runtime-core": "3.5.32", + "@vue/shared": "3.5.32", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", + "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32" + }, + "peerDependencies": { + "vue": "3.5.32" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", + "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.1.tgz", + "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 5.8", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/core": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz", + "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "12.0.0", + "@vueuse/shared": "12.0.0", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.0.0.tgz", + "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.0.0.tgz", + "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==", + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/alien-signals": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz", + "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/element-plus": { + "version": "2.13.7", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.7.tgz", + "integrity": "sha512-XdHATFZOyzVFL1DaHQ90IOJQSg9UnSAV+bhDW+YB5UoZ0Hxs50mwqjqfwXkuwpSag+VXXizVcErBR6Movo5daw==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "12.0.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.19", + "lodash": "^4.17.23", + "lodash-es": "^4.17.23", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.2.4" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz", + "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.15", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", + "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-sfc": "3.5.32", + "@vue/runtime-dom": "3.5.32", + "@vue/server-renderer": "3.5.32", + "@vue/shared": "3.5.32" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.2.6.tgz", + "integrity": "sha512-O02tnvIfOQVmnvoWwuSydwRoHjZVt8UEBR+2p4rT35p8GAy5VTlWP8o5qXfJR/GWCN0nVZoYWsVUvx2jwgdBmQ==", + "license": "MIT" + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-tsc": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.6.tgz", + "integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.2.6" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + } + } +} diff --git a/frontend/super-admin/package.json b/frontend/super-admin/package.json new file mode 100644 index 0000000..fe4b79a --- /dev/null +++ b/frontend/super-admin/package.json @@ -0,0 +1,29 @@ +{ + "name": "super-admin", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "axios": "^1.15.0", + "echarts": "^6.0.0", + "element-plus": "^2.13.7", + "pinia": "^2.3.1", + "sass": "^1.99.0", + "vue": "^3.5.32", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@types/node": "^24.12.2", + "@vitejs/plugin-vue": "^6.0.5", + "@vue/tsconfig": "^0.9.1", + "typescript": "~6.0.2", + "vite": "^8.0.4", + "vue-tsc": "^3.2.6" + } +} diff --git a/frontend/super-admin/public/favicon.svg b/frontend/super-admin/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/super-admin/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/super-admin/public/icons.svg b/frontend/super-admin/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/super-admin/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/super-admin/src/App.vue b/frontend/super-admin/src/App.vue new file mode 100644 index 0000000..7054172 --- /dev/null +++ b/frontend/super-admin/src/App.vue @@ -0,0 +1,9 @@ + + + + + diff --git a/frontend/super-admin/src/api/index.ts b/frontend/super-admin/src/api/index.ts new file mode 100644 index 0000000..18b1a85 --- /dev/null +++ b/frontend/super-admin/src/api/index.ts @@ -0,0 +1,39 @@ +import type { IAccountApi } from '../types/account' +import { mockAccountApi } from './mock/account' +import type { IPermissionApi } from '../types/permission' +import { mockPermissionApi } from './mock/permission' +import type { ISystemApi, IAuditLogApi } from '../types/system' +import { mockSystemApi, mockAuditLogApi } from './mock/system' + +export type ApiMode = 'mock' | 'real' + +class ApiFactory { + private mode: ApiMode = (import.meta.env.VITE_API_MODE as ApiMode) || 'mock' + + get accountApi(): IAccountApi { + if (this.mode === 'mock') return mockAccountApi as unknown as IAccountApi + // TODO: real API adapter + return mockAccountApi as unknown as IAccountApi + } + + get permissionApi(): IPermissionApi { + if (this.mode === 'mock') return mockPermissionApi as unknown as IPermissionApi + return mockPermissionApi as unknown as IPermissionApi + } + + get systemApi(): ISystemApi { + if (this.mode === 'mock') return mockSystemApi as unknown as ISystemApi + return mockSystemApi as unknown as ISystemApi + } + + get auditLogApi(): IAuditLogApi { + if (this.mode === 'mock') return mockAuditLogApi as unknown as IAuditLogApi + return mockAuditLogApi as unknown as IAuditLogApi + } +} + +export const apiFactory = new ApiFactory() +export const accountApi = apiFactory.accountApi +export const permissionApi = apiFactory.permissionApi +export const systemApi = apiFactory.systemApi +export const auditLogApi = apiFactory.auditLogApi diff --git a/frontend/super-admin/src/api/mock/account.ts b/frontend/super-admin/src/api/mock/account.ts new file mode 100644 index 0000000..d93eca4 --- /dev/null +++ b/frontend/super-admin/src/api/mock/account.ts @@ -0,0 +1,357 @@ +import type { PageResponse } from '../types/common' +import type { + Hospital, HospitalQuery, HospitalFormData, + PropertyCompany, PropertyCompanyQuery, PropertyCompanyFormData, + HospitalAccount, HospitalAccountQuery, HospitalAccountFormData, + PropertyAccount, PropertyAccountQuery, PropertyAccountFormData, + ExpiringAccount, ExpiringAccountQuery, ExpiringStats, + ExpiryReminderConfig +} from '../types/account' + +// ===== Mock数据 ===== + +const hospitalList: Hospital[] = Array.from({ length: 25 }, (_, i) => ({ + id: `hosp-${i + 1}`, + name: ['北京协和医院', '上海瑞金医院', '广州中山大学附属医院', '四川华西医院', '武汉同济医院', '南京鼓楼医院', '浙江大学附属医院', '湘雅医院', '山东省立医院', '天津医科大学总医院', '西安交大一附院', '重庆医科大学附属医院', '郑州大学一附院', '哈尔滨医科大学附属医院', '吉林大学一院', '安徽省立医院', '福建省立医院', '南昌大学一附院', '河北医科大学二院', '广西医科大学一附院', '昆明医科大学一附院', '兰州大学一院', '贵州医科大学附属医院', '内蒙古医科大学附属医院', '新疆医科大学一附院'][i], + creditCode: `91110000${String(10000000 + i * 1357911).slice(0, 8)}${String(i + 10).padStart(2, '0')}`, + address: `${['北京市', '上海市', '广州市', '成都市', '武汉市', '南京市', '杭州市', '长沙市', '济南市', '天津市', '西安市', '重庆市', '郑州市', '哈尔滨市', '长春市', '合肥市', '福州市', '南昌市', '石家庄市', '南宁市', '昆明市', '兰州市', '贵阳市', '呼和浩特市', '乌鲁木齐市'][i]}XX路${i + 1}号`, + contactPerson: ['张伟', '王芳', '李强', '赵敏', '刘洋', '陈静', '杨光', '周丽', '吴明', '郑华', '孙磊', '马超', '朱俊', '胡波', '林燕', '何勇', '高红', '罗杰', '谢军', '韩冰', '唐亮', '曹敏', '邓涛', '许峰', '范晶'][i], + contactPhone: `1${3 + (i % 7)}${String(10000000 + i * 1234567).slice(0, 8)}`, + status: i === 5 || i === 12 ? 'disabled' as const : 'enabled' as const, + campusCount: i < 5 ? 3 : i < 10 ? 2 : 1, + campuses: [ + { id: `campus-${i + 1}-1`, name: '主院区', address: `主院区地址${i + 1}号`, contactPerson: ['张伟', '王芳', '李强'][i % 3] }, + ...(i < 10 ? [{ id: `campus-${i + 1}-2`, name: '东院区', address: `东院区地址${i + 1}号`, contactPerson: ['赵敏', '刘洋'][i % 2] }] : []), + ...(i < 5 ? [{ id: `campus-${i + 1}-3`, name: '西院区', address: `西院区地址${i + 1}号`, contactPerson: '陈静' }] : []) + ], + createdAt: `2025-${String((i % 12) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')} 09:00:00`, + updatedAt: `2026-${String((i % 3) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')} 10:00:00` +})) + +const propertyCompanyList: PropertyCompany[] = Array.from({ length: 20 }, (_, i) => ({ + id: `prop-co-${i + 1}`, + name: ['万科物业', '绿城服务', '保利物业', '碧桂园服务', '中海物业', '龙湖智创', '金地物业', '融创服务', '招商积余', '华润万象', '新城悦服务', '永升服务', '雅生活', '时代邻里', '卓越商企', '彩生活', '中奥到家', '建业新生活', '弘阳服务', '鑫苑服务'][i], + address: `${['北京市', '上海市', '广州市', '深圳市', '杭州市', '成都市', '南京市', '武汉市', '重庆市', '西安市'][i % 10]}XX大道${i + 100}号`, + contactPerson: ['王建国', '李美玲', '张志远', '陈晓峰', '刘德明', '赵丽华', '周大伟', '吴秀英', '郑强', '马丽', '孙国强', '朱美凤', '胡志刚', '林翠花', '何建明', '高小红', '罗建军', '谢美玲', '韩志伟', '曹丽华'][i], + contactPhone: `1${5 + (i % 5)}${String(20000000 + i * 2345678).slice(0, 8)}`, + status: i === 3 || i === 8 ? 'disabled' as const : 'enabled' as const, + serviceHospitals: hospitalList.slice(i % 5, (i % 5) + 2 + (i % 3)).map(h => h.name), + createdAt: `2025-${String((i % 12) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')} 08:00:00`, + updatedAt: `2026-${String((i % 4) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')} 14:00:00` +})) + +const hospitalAccountList: HospitalAccount[] = Array.from({ length: 25 }, (_, i) => { + const expireDate = new Date() + expireDate.setDate(expireDate.getDate() + (i < 5 ? -30 + i * 5 : i < 15 ? 7 + i * 2 : 60 + i * 5)) + return { + id: `hacc-${i + 1}`, + username: `hospital${String(i + 1).padStart(2, '0')}`, + hospitalId: hospitalList[i % hospitalList.length].id, + hospitalName: hospitalList[i % hospitalList.length].name, + roles: [`role-hosp-${(i % 3) + 1}`], + roleNames: [['医院查看模板', '医院管理模板', '医院审批模板'][i % 3]], + expireDate: expireDate.toISOString().split('T')[0], + status: i < 3 ? 'expired' as const : i < 8 ? 'expiring' as const : i === 20 ? 'stopped' as const : 'normal' as const, + createdAt: `2025-${String((i % 12) + 1).padStart(2, '0')}-15 10:00:00` + } +}) + +const propertyAccountList: PropertyAccount[] = Array.from({ length: 25 }, (_, i) => { + const expireDate = new Date() + expireDate.setDate(expireDate.getDate() + (i < 4 ? -20 + i * 5 : i < 12 ? 10 + i * 3 : 90 + i * 5)) + return { + id: `pacc-${i + 1}`, + username: `propadmin${String(i + 1).padStart(2, '0')}`, + propertyCompanyId: propertyCompanyList[i % propertyCompanyList.length].id, + propertyName: propertyCompanyList[i % propertyCompanyList.length].name, + hospitalId: hospitalList[i % hospitalList.length].id, + hospitalName: hospitalList[i % hospitalList.length].name, + roles: [`role-prop-${(i % 4) + 1}`], + roleNames: [['物业管理员模板', '主管模板', '班组长模板', '维修员模板'][i % 4]], + expireDate: expireDate.toISOString().split('T')[0], + status: i < 2 ? 'expired' as const : i < 6 ? 'expiring' as const : i === 18 ? 'stopped' as const : 'normal' as const, + createdAt: `2025-${String((i % 12) + 1).padStart(2, '0')}-20 11:00:00` + } +}) + +const expiringAccountList: ExpiringAccount[] = [ + ...hospitalAccountList.filter(a => a.status === 'expired' || a.status === 'expiring').map(a => ({ + id: a.id, + username: a.username, + accountType: 'hospital' as const, + bindUnit: a.hospitalName, + expireDate: a.expireDate, + remainDays: Math.ceil((new Date(a.expireDate).getTime() - Date.now()) / 86400000), + status: a.status + })), + ...propertyAccountList.filter(a => a.status === 'expired' || a.status === 'expiring').map(a => ({ + id: a.id, + username: a.username, + accountType: 'property_admin' as const, + bindUnit: a.propertyName, + expireDate: a.expireDate, + remainDays: Math.ceil((new Date(a.expireDate).getTime() - Date.now()) / 86400000), + status: a.status + })) +] + +let reminderConfig: ExpiryReminderConfig = { + reminderDays: [7, 15, 30], + loginPopup: true, + popupCloseAction: 'normal', + expiredAction: 'block' +} + +// ===== 工具函数 ===== + +function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +function filterList(list: T[], query: Record, page: number, pageSize: number): PageResponse { + let filtered = [...list] + if (query.name) filtered = filtered.filter((item: any) => item.name?.includes(query.name)) + if (query.username) filtered = filtered.filter((item: any) => item.username?.includes(query.username)) + if (query.contactPerson) filtered = filtered.filter((item: any) => item.contactPerson?.includes(query.contactPerson)) + if (query.status && query.status !== '') filtered = filtered.filter((item: any) => item.status === query.status) + if (query.hospitalId) filtered = filtered.filter((item: any) => item.hospitalId === query.hospitalId) + if (query.propertyCompanyId) filtered = filtered.filter((item: any) => item.propertyCompanyId === query.propertyCompanyId) + if (query.accountType && query.accountType !== '') filtered = filtered.filter((item: any) => item.accountType === query.accountType) + if (query.expireFilter) { + const now = new Date() + filtered = filtered.filter((item: any) => { + const exp = new Date(item.expireDate) + const diff = (exp.getTime() - now.getTime()) / 86400000 + if (query.expireFilter === 'expired') return diff < 0 + if (query.expireFilter === '7days') return diff >= 0 && diff <= 7 + if (query.expireFilter === '30days') return diff >= 0 && diff <= 30 + if (query.expireFilter === 'normal') return diff > 30 + return true + }) + } + if (query.expireStatus) { + const now = new Date() + filtered = filtered.filter((item: any) => { + const diff = (new Date(item.expireDate).getTime() - now.getTime()) / 86400000 + if (query.expireStatus === 'expired') return diff < 0 + if (query.expireStatus === '7days') return diff >= 0 && diff <= 7 + if (query.expireStatus === '30days') return diff >= 0 && diff <= 30 + return true + }) + } + if (query.operator) filtered = filtered.filter((item: any) => item.operator?.includes(query.operator)) + if (query.operationType && query.operationType !== '') filtered = filtered.filter((item: any) => item.operationType === query.operationType) + + const total = filtered.length + const totalPages = Math.ceil(total / pageSize) + const startIdx = (page - 1) * pageSize + const pagedList = filtered.slice(startIdx, startIdx + pageSize) + + return { list: pagedList, pagination: { page, pageSize, total, totalPages } } +} + +// ===== Mock API实现 ===== + +export const mockAccountApi = { + async getHospitalList(query: HospitalQuery): Promise> { + await delay(300) + if (query._mockError === 'empty') return { list: [], pagination: { page: 1, pageSize: 20, total: 0, totalPages: 0 } } + if (query._mockError === 'timeout') { await delay(16000); throw new Error('timeout') } + if (query._mockError === '500') throw new Error('服务器内部错误') + return filterList(hospitalList, query, query.page, query.pageSize) + }, + + async getHospitalDetail(id: string): Promise { + await delay(200) + const hospital = hospitalList.find(h => h.id === id) + if (!hospital) throw new Error('医院不存在') + return { ...hospital } + }, + + async createHospital(data: HospitalFormData): Promise<{ id: string }> { + await delay(500) + const id = `hosp-${Date.now()}` + hospitalList.unshift({ + id, + name: data.name, + creditCode: data.creditCode, + address: data.address, + contactPerson: data.contactPerson, + contactPhone: data.contactPhone, + status: data.status, + campusCount: data.campuses.length, + campuses: data.campuses, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }) + return { id } + }, + + async updateHospital(id: string, data: HospitalFormData): Promise { + await delay(500) + const idx = hospitalList.findIndex(h => h.id === id) + if (idx === -1) throw new Error('医院不存在') + hospitalList[idx] = { + ...hospitalList[idx], + ...data, + campusCount: data.campuses.length, + campuses: data.campuses, + updatedAt: new Date().toISOString() + } + }, + + async toggleHospitalStatus(id: string): Promise { + await delay(400) + const hospital = hospitalList.find(h => h.id === id) + if (!hospital) throw new Error('医院不存在') + hospital.status = hospital.status === 'enabled' ? 'disabled' : 'enabled' + }, + + async getPropertyCompanyList(query: PropertyCompanyQuery): Promise> { + await delay(300) + if (query._mockError === 'empty') return { list: [], pagination: { page: 1, pageSize: 20, total: 0, totalPages: 0 } } + return filterList(propertyCompanyList, query, query.page, query.pageSize) + }, + + async getPropertyCompanyDetail(id: string): Promise { + await delay(200) + const company = propertyCompanyList.find(c => c.id === id) + if (!company) throw new Error('物业公司不存在') + return { ...company } + }, + + async createPropertyCompany(data: PropertyCompanyFormData): Promise<{ id: string }> { + await delay(500) + const id = `prop-co-${Date.now()}` + propertyCompanyList.unshift({ + id, + name: data.name, + address: data.address, + contactPerson: data.contactPerson, + contactPhone: data.contactPhone, + status: 'enabled', + serviceHospitals: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }) + return { id } + }, + + async updatePropertyCompany(id: string, data: PropertyCompanyFormData): Promise { + await delay(500) + const idx = propertyCompanyList.findIndex(c => c.id === id) + if (idx === -1) throw new Error('物业公司不存在') + propertyCompanyList[idx] = { ...propertyCompanyList[idx], ...data, updatedAt: new Date().toISOString() } + }, + + async togglePropertyCompanyStatus(id: string): Promise { + await delay(400) + const company = propertyCompanyList.find(c => c.id === id) + if (!company) throw new Error('物业公司不存在') + company.status = company.status === 'enabled' ? 'disabled' : 'enabled' + }, + + async getHospitalAccountList(query: HospitalAccountQuery): Promise> { + await delay(300) + return filterList(hospitalAccountList, query, query.page, query.pageSize) + }, + + async createHospitalAccount(data: HospitalAccountFormData): Promise<{ id: string }> { + await delay(500) + const id = `hacc-${Date.now()}` + const hospital = hospitalList.find(h => h.id === data.hospitalId) + hospitalAccountList.unshift({ + id, + username: data.username, + hospitalId: data.hospitalId, + hospitalName: hospital?.name || '', + roles: data.roleIds, + roleNames: data.roleIds.map(() => '医院查看模板'), + expireDate: data.expireDate, + status: 'normal', + createdAt: new Date().toISOString() + }) + return { id } + }, + + async toggleAccountStatus(id: string): Promise { + await delay(400) + const account = [...hospitalAccountList, ...propertyAccountList].find(a => a.id === id) + if (account) account.status = account.status === 'normal' ? 'stopped' : 'normal' + }, + + async renewAccount(id: string, expireDate: string): Promise { + await delay(400) + const account = [...hospitalAccountList, ...propertyAccountList].find(a => a.id === id) + if (account) { + account.expireDate = expireDate + account.status = 'normal' + } + }, + + async resetPassword(id: string): Promise<{ newPassword: string }> { + await delay(400) + const pwd = 'Abc' + Math.random().toString(36).slice(2, 8) + return { newPassword: pwd } + }, + + async getPropertyAccountList(query: PropertyAccountQuery): Promise> { + await delay(300) + return filterList(propertyAccountList, query, query.page, query.pageSize) + }, + + async createPropertyAccount(data: PropertyAccountFormData): Promise<{ id: string }> { + await delay(500) + const id = `pacc-${Date.now()}` + const company = propertyCompanyList.find(c => c.id === data.propertyCompanyId) + const hospital = hospitalList.find(h => h.id === data.hospitalId) + propertyAccountList.unshift({ + id, + username: data.username, + propertyCompanyId: data.propertyCompanyId, + propertyName: company?.name || '', + hospitalId: data.hospitalId, + hospitalName: hospital?.name || '', + roles: data.roleIds, + roleNames: data.roleIds.map(() => '物业管理员模板'), + expireDate: data.expireDate, + status: 'normal', + createdAt: new Date().toISOString() + }) + return { id } + }, + + async getExpiringAccountList(query: ExpiringAccountQuery): Promise> { + await delay(300) + return filterList(expiringAccountList, query, query.page, query.pageSize) + }, + + async getExpiringStats(): Promise { + await delay(200) + return { + expired: expiringAccountList.filter(a => a.remainDays < 0).length, + expiringIn7Days: expiringAccountList.filter(a => a.remainDays >= 0 && a.remainDays <= 7).length, + expiringIn30Days: expiringAccountList.filter(a => a.remainDays >= 0 && a.remainDays <= 30).length + } + }, + + async getExpiryReminderConfig(): Promise { + await delay(200) + return { ...reminderConfig } + }, + + async saveExpiryReminderConfig(data: ExpiryReminderConfig): Promise { + await delay(500) + reminderConfig = { ...data } + }, + + async getHospitalOptions(): Promise<{ id: string; name: string }[]> { + await delay(100) + return hospitalList.filter(h => h.status === 'enabled').map(h => ({ id: h.id, name: h.name })) + }, + + async getPropertyCompanyOptions(): Promise<{ id: string; name: string }[]> { + await delay(100) + return propertyCompanyList.filter(c => c.status === 'enabled').map(c => ({ id: c.id, name: c.name })) + } +} diff --git a/frontend/super-admin/src/api/mock/permission.ts b/frontend/super-admin/src/api/mock/permission.ts new file mode 100644 index 0000000..622fcd4 --- /dev/null +++ b/frontend/super-admin/src/api/mock/permission.ts @@ -0,0 +1,320 @@ +import type { PageResponse } from '../types/common' +import type { + Role, RoleQuery, RoleFormData, + PermissionNode, + PermissionRegistryItem, PermissionRegistryQuery, + PermissionAuditLog, PermissionAuditLogQuery, PermissionAuditDetail +} from '../types/permission' + +// ===== Mock数据 ===== + +const permissionTree: PermissionNode[] = [ + { + code: 'menu:repair', name: '在线报修', type: 'menu', + children: [ + { + code: 'page:repair:list', name: '工单列表', type: 'page', + children: [ + { + code: 'feature:repair:order-manage', name: '工单管理', type: 'feature', + children: [ + { code: 'action:repair:order:view', name: '查看', type: 'action' }, + { code: 'action:repair:order:create', name: '新增', type: 'action' }, + { code: 'action:repair:order:update', name: '编辑', type: 'action' }, + { code: 'action:repair:order:delete', name: '删除', type: 'action' }, + { code: 'action:repair:order:approve', name: '审批', type: 'action' }, + { code: 'action:repair:order:export', name: '导出', type: 'action' }, + { code: 'action:repair:order:assign', name: '分配', type: 'action' } + ] + }, + { + code: 'feature:repair:batch', name: '批量操作', type: 'feature', + children: [ + { code: 'action:repair:batch:view', name: '查看', type: 'action' }, + { code: 'action:repair:batch:export', name: '导出', type: 'action' } + ] + } + ] + }, + { + code: 'page:repair:detail', name: '工单详情', type: 'page', + children: [ + { + code: 'feature:repair:delay-approve', name: '延期审批', type: 'feature', + children: [ + { code: 'action:repair:delay:view', name: '查看', type: 'action' }, + { code: 'action:repair:delay:approve', name: '审批', type: 'action' } + ] + }, + { + code: 'feature:repair:acceptance', name: '工单验收', type: 'feature', + children: [ + { code: 'action:repair:acceptance:view', name: '查看', type: 'action' }, + { code: 'action:repair:acceptance:approve', name: '审批', type: 'action' } + ] + } + ] + }, + { + code: 'page:repair:type-manage', name: '报修类型管理', type: 'page', + children: [ + { + code: 'feature:repair:type', name: '类型管理', type: 'feature', + children: [ + { code: 'action:repair:type:view', name: '查看', type: 'action' }, + { code: 'action:repair:type:create', name: '新增', type: 'action' }, + { code: 'action:repair:type:update', name: '编辑', type: 'action' }, + { code: 'action:repair:type:delete', name: '删除', type: 'action' } + ] + } + ] + } + ] + }, + { + code: 'menu:inspection', name: '巡检管理', type: 'menu', + children: [ + { + code: 'page:inspection:plan', name: '巡检计划', type: 'page', + children: [ + { + code: 'feature:inspection:plan-manage', name: '计划管理', type: 'feature', + children: [ + { code: 'action:inspection:plan:view', name: '查看', type: 'action' }, + { code: 'action:inspection:plan:create', name: '新增', type: 'action' }, + { code: 'action:inspection:plan:update', name: '编辑', type: 'action' }, + { code: 'action:inspection:plan:delete', name: '删除', type: 'action' }, + { code: 'action:inspection:plan:export', name: '导出', type: 'action' } + ] + } + ] + }, + { + code: 'page:inspection:task', name: '巡检任务', type: 'page', + children: [ + { + code: 'feature:inspection:task-manage', name: '任务管理', type: 'feature', + children: [ + { code: 'action:inspection:task:view', name: '查看', type: 'action' }, + { code: 'action:inspection:task:execute', name: '执行', type: 'action' }, + { code: 'action:inspection:task:export', name: '导出', type: 'action' } + ] + } + ] + } + ] + }, + { + code: 'menu:contract', name: '合同管理', type: 'menu', + children: [ + { + code: 'page:contract:list', name: '合同列表', type: 'page', + children: [ + { + code: 'feature:contract:manage', name: '合同管理', type: 'feature', + children: [ + { code: 'action:contract:view', name: '查看', type: 'action' }, + { code: 'action:contract:create', name: '新增', type: 'action' }, + { code: 'action:contract:update', name: '编辑', type: 'action' }, + { code: 'action:contract:delete', name: '删除', type: 'action' }, + { code: 'action:contract:approve', name: '审批', type: 'action' }, + { code: 'action:contract:export', name: '导出', type: 'action' } + ] + } + ] + } + ] + }, + { + code: 'menu:cleaning', name: '保洁管理', type: 'menu', + children: [ + { + code: 'page:cleaning:task', name: '保洁任务', type: 'page', + children: [ + { + code: 'feature:cleaning:manage', name: '保洁管理', type: 'feature', + children: [ + { code: 'action:cleaning:view', name: '查看', type: 'action' }, + { code: 'action:cleaning:execute', name: '执行', type: 'action' }, + { code: 'action:cleaning:export', name: '导出', type: 'action' } + ] + } + ] + } + ] + } +] + +const roleList: Role[] = [ + { id: 'role-1', name: '医院查看模板', description: '医院账号默认查看权限', scope: 'hospital', isPreset: true, accountCount: 15, status: 'enabled', createdAt: '2025-01-15 10:00:00' }, + { id: 'role-2', name: '医院管理模板', description: '医院账号管理权限', scope: 'hospital', isPreset: true, accountCount: 5, status: 'enabled', createdAt: '2025-01-15 10:00:00' }, + { id: 'role-3', name: '医院审批模板', description: '医院账号审批权限', scope: 'hospital', isPreset: true, accountCount: 3, status: 'enabled', createdAt: '2025-01-15 10:00:00' }, + { id: 'role-4', name: '物业管理员模板', description: '物业公司管理员默认权限', scope: 'property_admin', isPreset: true, accountCount: 20, status: 'enabled', createdAt: '2025-01-15 10:00:00' }, + { id: 'role-5', name: '主管模板', description: '主管级权限', scope: 'property_staff', isPreset: true, accountCount: 8, status: 'enabled', createdAt: '2025-01-15 10:00:00' }, + { id: 'role-6', name: '班组长模板', description: '班组长权限', scope: 'property_staff', isPreset: true, accountCount: 12, status: 'enabled', createdAt: '2025-01-15 10:00:00' }, + { id: 'role-7', name: '维修员模板', description: '维修人员默认权限', scope: 'property_staff', isPreset: true, accountCount: 23, status: 'enabled', createdAt: '2025-01-15 10:00:00' }, + { id: 'role-8', name: '巡检员模板', description: '巡检人员默认权限', scope: 'property_staff', isPreset: true, accountCount: 10, status: 'enabled', createdAt: '2025-01-15 10:00:00' }, + { id: 'role-9', name: '保洁员模板', description: '保洁人员默认权限', scope: 'property_staff', isPreset: true, accountCount: 15, status: 'enabled', createdAt: '2025-01-15 10:00:00' }, + { id: 'role-10', name: '巡检主管', description: '巡检主管自定义权限', scope: 'property_staff', isPreset: false, accountCount: 3, status: 'enabled', createdAt: '2025-06-01 14:00:00' }, + { id: 'role-11', name: '高级维修员', description: '高级维修员扩展权限', scope: 'property_staff', isPreset: false, accountCount: 5, status: 'enabled', createdAt: '2025-07-20 09:00:00' }, + { id: 'role-12', name: '保洁主管', description: '保洁主管自定义权限', scope: 'property_staff', isPreset: false, accountCount: 2, status: 'enabled', createdAt: '2025-08-10 11:00:00' }, + { id: 'role-13', name: '合同管理员', description: '合同管理专用角色', scope: 'property_admin', isPreset: false, accountCount: 0, status: 'enabled', createdAt: '2025-09-05 16:00:00' }, + { id: 'role-14', name: '已停用测试角色', description: '测试用停用角色', scope: 'hospital', isPreset: false, accountCount: 0, status: 'disabled', createdAt: '2025-10-01 08:00:00' }, + { id: 'role-15', name: '只读审计角色', description: '仅查看审计日志', scope: 'hospital', isPreset: false, accountCount: 1, status: 'enabled', createdAt: '2025-11-15 13:00:00' } +] + +const rolePermissionsMap: Record = { + 'role-1': ['menu:repair', 'page:repair:list', 'feature:repair:order-manage', 'action:repair:order:view', 'menu:contract', 'page:contract:list', 'feature:contract:manage', 'action:contract:view'], + 'role-4': ['menu:repair', 'page:repair:list', 'feature:repair:order-manage', 'action:repair:order:view', 'action:repair:order:create', 'action:repair:order:update', 'action:repair:order:assign', 'action:repair:order:export', 'page:repair:detail', 'feature:repair:delay-approve', 'action:repair:delay:view', 'feature:repair:acceptance', 'action:repair:acceptance:view', 'menu:inspection', 'page:inspection:plan', 'feature:inspection:plan-manage', 'action:inspection:plan:view'], + 'role-5': ['menu:repair', 'page:repair:list', 'feature:repair:order-manage', 'action:repair:order:view', 'action:repair:order:approve', 'action:repair:order:export', 'menu:inspection', 'page:inspection:plan', 'feature:inspection:plan-manage', 'action:inspection:plan:view', 'action:inspection:plan:export'], + 'role-7': ['menu:repair', 'page:repair:list', 'feature:repair:order-manage', 'action:repair:order:view', 'page:repair:detail', 'feature:repair:delay-approve', 'action:repair:delay:view'], + 'role-8': ['menu:inspection', 'page:inspection:plan', 'feature:inspection:plan-manage', 'action:inspection:plan:view', 'page:inspection:task', 'feature:inspection:task-manage', 'action:inspection:task:view', 'action:inspection:task:execute'], + 'role-10': ['menu:repair', 'page:repair:list', 'feature:repair:order-manage', 'action:repair:order:view', 'action:repair:order:approve', 'action:repair:order:export', 'menu:inspection', 'page:inspection:plan', 'feature:inspection:plan-manage', 'action:inspection:plan:view', 'action:inspection:plan:create', 'action:inspection:plan:update', 'action:inspection:plan:export', 'page:inspection:task', 'feature:inspection:task-manage', 'action:inspection:task:view', 'action:inspection:task:export'] +} + +const permissionRegistryList: PermissionRegistryItem[] = [ + { moduleCode: 'repair', moduleName: '在线报修', pageCode: 'repair_list', pageName: '工单列表', featureCode: 'order_manage', featureName: '工单管理', actions: ['view', 'create', 'update', 'delete', 'approve', 'export', 'assign'] }, + { moduleCode: 'repair', moduleName: '在线报修', pageCode: 'repair_list', pageName: '工单列表', featureCode: 'batch_operate', featureName: '批量操作', actions: ['view', 'export'] }, + { moduleCode: 'repair', moduleName: '在线报修', pageCode: 'repair_detail', pageName: '工单详情', featureCode: 'delay_approve', featureName: '延期审批', actions: ['view', 'approve'] }, + { moduleCode: 'repair', moduleName: '在线报修', pageCode: 'repair_detail', pageName: '工单详情', featureCode: 'order_acceptance', featureName: '工单验收', actions: ['view', 'approve'] }, + { moduleCode: 'repair', moduleName: '在线报修', pageCode: 'repair_type', pageName: '报修类型管理', featureCode: 'type_manage', featureName: '类型管理', actions: ['view', 'create', 'update', 'delete'] }, + { moduleCode: 'inspection', moduleName: '巡检管理', pageCode: 'insp_plan', pageName: '巡检计划', featureCode: 'plan_manage', featureName: '计划管理', actions: ['view', 'create', 'update', 'delete', 'export'] }, + { moduleCode: 'inspection', moduleName: '巡检管理', pageCode: 'insp_task', pageName: '巡检任务', featureCode: 'task_manage', featureName: '任务管理', actions: ['view', 'execute', 'export'] }, + { moduleCode: 'contract', moduleName: '合同管理', pageCode: 'contract_list', pageName: '合同列表', featureCode: 'contract_manage', featureName: '合同管理', actions: ['view', 'create', 'update', 'delete', 'approve', 'export'] }, + { moduleCode: 'cleaning', moduleName: '保洁管理', pageCode: 'cleaning_task', pageName: '保洁任务', featureCode: 'cleaning_manage', featureName: '保洁管理', actions: ['view', 'execute', 'export'] }, + { moduleCode: 'bidding', moduleName: '分段招标', pageCode: 'bidding_list', pageName: '招标列表', featureCode: 'bidding_manage', featureName: '招标管理', actions: ['view', 'create', 'update', 'delete', 'approve', 'export'] }, + { moduleCode: 'service', moduleName: '服务监督', pageCode: 'service_monitor', pageName: '服务监督', featureCode: 'monitor_manage', featureName: '监督管理', actions: ['view', 'create', 'update', 'export'] }, + { moduleCode: 'evaluation', moduleName: '服务评价', pageCode: 'eval_list', pageName: '评价列表', featureCode: 'eval_manage', featureName: '评价管理', actions: ['view', 'export'] } +] + +const permissionAuditLogList: PermissionAuditLog[] = Array.from({ length: 30 }, (_, i) => ({ + id: `paudit-${i + 1}`, + operationTime: `2026-04-${String(17 - Math.floor(i / 5)).padStart(2, '0')} ${String(9 + (i % 8)).padStart(2, '0')}:${String((i * 17) % 60).padStart(2, '0')}:00`, + operator: i % 3 === 0 ? 'admin' : i % 3 === 1 ? 'super_admin' : 'system', + operationType: (['role_create', 'permission_modify', 'role_assign', 'role_remove'] as const)[i % 4], + targetRole: roleList[i % roleList.length].name, + changeSummary: i % 4 === 0 ? '初始权限' : `+${2 + (i % 3)}项 -${i % 2}项`, + addCount: i % 4 === 0 ? 0 : 2 + (i % 3), + removeCount: i % 4 === 0 ? 0 : i % 2 +})) + +function delay(ms: number) { return new Promise(r => setTimeout(r, ms)) } + +function filterList(list: T[], query: Record, page: number, pageSize: number): PageResponse { + let filtered = [...list] + if (query.name) filtered = filtered.filter((item: any) => item.name?.includes(query.name)) + if (query.scope && query.scope !== '') filtered = filtered.filter((item: any) => item.scope === query.scope) + if (query.status && query.status !== '') filtered = filtered.filter((item: any) => item.status === query.status) + if (query.moduleName) filtered = filtered.filter((item: any) => item.moduleName?.includes(query.moduleName)) + if (query.pageName) filtered = filtered.filter((item: any) => item.pageName?.includes(query.pageName)) + if (query.operator) filtered = filtered.filter((item: any) => item.operator?.includes(query.operator)) + if (query.operationType && query.operationType !== '') filtered = filtered.filter((item: any) => item.operationType === query.operationType) + if (query.role) filtered = filtered.filter((item: any) => item.targetRole?.includes(query.role)) + const total = filtered.length + const totalPages = Math.ceil(total / pageSize) + return { list: filtered.slice((page - 1) * pageSize, page * pageSize), pagination: { page, pageSize, total, totalPages } } +} + +export const mockPermissionApi = { + async getRoleList(query: RoleQuery): Promise> { + await delay(300) + return filterList(roleList, query, query.page, query.pageSize) + }, + + async getRoleDetail(id: string): Promise { + await delay(200) + const role = roleList.find(r => r.id === id) + if (!role) throw new Error('角色不存在') + return { ...role } + }, + + async createRole(data: RoleFormData): Promise<{ id: string }> { + await delay(500) + const id = `role-${Date.now()}` + roleList.unshift({ + id, name: data.name, description: data.description, scope: data.scope, + isPreset: false, accountCount: 0, status: 'enabled', createdAt: new Date().toISOString() + }) + rolePermissionsMap[id] = data.permissions + return { id } + }, + + async updateRole(id: string, data: RoleFormData): Promise { + await delay(500) + const idx = roleList.findIndex(r => r.id === id) + if (idx === -1) throw new Error('角色不存在') + roleList[idx] = { ...roleList[idx], name: data.name, description: data.description, scope: data.scope } + rolePermissionsMap[id] = data.permissions + }, + + async disableRole(id: string): Promise { + await delay(400) + const role = roleList.find(r => r.id === id) + if (!role) throw new Error('角色不存在') + if (role.accountCount > 0) throw new Error('该角色下存在关联账号,无法停用') + role.status = 'disabled' + }, + + async deleteRole(id: string): Promise { + await delay(400) + const idx = roleList.findIndex(r => r.id === id) + if (idx === -1) throw new Error('角色不存在') + if (roleList[idx].accountCount > 0) throw new Error('该角色下存在关联账号,无法删除') + roleList.splice(idx, 1) + }, + + async getPermissionTree(): Promise { + await delay(300) + return JSON.parse(JSON.stringify(permissionTree)) + }, + + async getRolePermissions(id: string): Promise { + await delay(300) + const codes = rolePermissionsMap[id] || [] + // Return full tree with checked state based on codes + function markChecked(nodes: PermissionNode[]): PermissionNode[] { + return nodes.map(n => ({ + ...n, + children: n.children ? markChecked(n.children) : undefined + })) + } + return markChecked(permissionTree) + }, + + async getPermissionRegistryList(query: PermissionRegistryQuery): Promise> { + await delay(300) + return filterList(permissionRegistryList, query, query.page, query.pageSize) + }, + + async refreshPermissionRegistry(): Promise { + await delay(1000) + }, + + async getPermissionAuditLogList(query: PermissionAuditLogQuery): Promise> { + await delay(300) + return filterList(permissionAuditLogList, query, query.page, query.pageSize) + }, + + async getPermissionAuditDetail(id: string): Promise { + await delay(200) + const log = permissionAuditLogList.find(l => l.id === id) + if (!log) throw new Error('日志不存在') + return { + id: log.id, + operationTime: log.operationTime, + operator: log.operator, + operationType: log.operationType, + targetRole: log.targetRole, + operationIp: '192.168.1.100', + addedPermissions: log.addCount > 0 ? [ + '在线报修 → 工单详情 → 延期审批 → 审批', + '在线报修 → 工单列表 → 工单管理 → 导出', + ...Array.from({ length: Math.max(0, log.addCount - 2) }, (_, i) => `权限项${i + 3}`) + ] : [], + removedPermissions: log.removeCount > 0 ? [ + '巡检管理 → 巡检计划 → 计划管理 → 删除', + ...Array.from({ length: Math.max(0, log.removeCount - 1) }, (_, i) => `移除权限${i + 2}`) + ] : [] + } + } +} diff --git a/frontend/super-admin/src/api/mock/system.ts b/frontend/super-admin/src/api/mock/system.ts new file mode 100644 index 0000000..7f97e2c --- /dev/null +++ b/frontend/super-admin/src/api/mock/system.ts @@ -0,0 +1,131 @@ +import type { SystemVersion, SystemVersionFormData, CacheStatus, CacheModule, AccountAuditLog, AccountAuditLogQuery, AccountAuditDetail } from '../types/system' +import type { PageResponse } from '../types/common' + +// ===== Mock数据 ===== + +const versionList: SystemVersion[] = [ + { id: 'v-1', version: '1.2.0', platform: 'web', minCompatibleVersion: '1.0.0', forceUpdate: false, releaseDate: '2026-04-01', description: '新增权限管理模块、优化系统性能、修复已知问题' }, + { id: 'v-2', version: '1.1.0', platform: 'miniapp', minCompatibleVersion: '1.0.0', forceUpdate: false, releaseDate: '2026-03-15', description: '新增巡检打卡功能、优化报修流程、修复蓝牙连接问题' }, + { id: 'v-3', version: '1.0.0', platform: 'web', minCompatibleVersion: '1.0.0', forceUpdate: false, releaseDate: '2026-01-01', description: '系统初始版本,包含基础功能模块' }, + { id: 'v-4', version: '1.0.0', platform: 'miniapp', minCompatibleVersion: '1.0.0', forceUpdate: true, releaseDate: '2026-01-01', description: '小程序初始版本' }, + { id: 'v-5', version: '0.9.0', platform: 'miniapp', minCompatibleVersion: '0.9.0', forceUpdate: true, releaseDate: '2025-12-01', description: '内测版本' } +] + +const cacheStatus: CacheStatus = { + redisStatus: 'connected', + memoryUsed: 128, + memoryTotal: 512, + connections: 24 +} + +const cacheModules: CacheModule[] = [ + { name: '权限缓存', keyPrefix: 'perm:*', count: 1234, lastUpdateTime: '2026-04-17 10:30:25' }, + { name: '字典缓存', keyPrefix: 'dict:*', count: 56, lastUpdateTime: '2026-04-17 09:00:00' }, + { name: '菜单缓存', keyPrefix: 'menu:*', count: 120, lastUpdateTime: '2026-04-17 09:00:00' }, + { name: '业务缓存', keyPrefix: 'biz:*', count: 0, lastUpdateTime: '—' } +] + +const accountAuditLogList: AccountAuditLog[] = Array.from({ length: 30 }, (_, i) => ({ + id: `alog-${i + 1}`, + operationTime: `2026-04-${String(17 - Math.floor(i / 4)).padStart(2, '0')} ${String(8 + (i % 9)).padStart(2, '0')}:${String((i * 23) % 60).padStart(2, '0')}:00`, + operator: i % 2 === 0 ? 'admin' : 'super_admin', + operationType: (['create', 'edit', 'enable', 'disable', 'renew', 'reset_password'] as const)[i % 6], + targetAccount: i % 2 === 0 ? `hospital${String(i + 1).padStart(2, '0')}` : `propadmin${String(i + 1).padStart(2, '0')}`, + accountType: i % 2 === 0 ? 'hospital' : 'property_admin', + bindUnit: i % 2 === 0 ? '北京协和医院' : '万科物业' +})) + +function delay(ms: number) { return new Promise(r => setTimeout(r, ms)) } + +function filterList(list: T[], query: Record, page: number, pageSize: number): PageResponse { + let filtered = [...list] + if (query.operator) filtered = filtered.filter((item: any) => item.operator?.includes(query.operator)) + if (query.operationType && query.operationType !== '') filtered = filtered.filter((item: any) => item.operationType === query.operationType) + if (query.accountType && query.accountType !== '') filtered = filtered.filter((item: any) => item.accountType === query.accountType) + const total = filtered.length + const totalPages = Math.ceil(total / pageSize) + return { list: filtered.slice((page - 1) * pageSize, page * pageSize), pagination: { page, pageSize, total, totalPages } } +} + +export const mockSystemApi = { + async getSystemVersionList(): Promise { + await delay(200) + return [...versionList] + }, + + async createSystemVersion(data: SystemVersionFormData): Promise<{ id: string }> { + await delay(500) + const id = `v-${Date.now()}` + versionList.unshift({ id, ...data, releaseDate: new Date().toISOString().split('T')[0] }) + return { id } + }, + + async updateSystemVersion(id: string, data: SystemVersionFormData): Promise { + await delay(500) + const idx = versionList.findIndex(v => v.id === id) + if (idx === -1) throw new Error('版本不存在') + versionList[idx] = { ...versionList[idx], ...data } + }, + + async getLatestVersions(): Promise<{ web: SystemVersion | null; miniapp: SystemVersion | null }> { + await delay(200) + const web = versionList.find(v => v.platform === 'web') || null + const miniapp = versionList.find(v => v.platform === 'miniapp') || null + return { web, miniapp } + }, + + async getCacheStatus(): Promise { + await delay(200) + return { ...cacheStatus } + }, + + async getCacheModules(): Promise { + await delay(200) + return [...cacheModules] + }, + + async clearCacheModule(module: string): Promise { + await delay(800) + const mod = cacheModules.find(m => m.keyPrefix.startsWith(module.split(':')[0])) + if (mod) { mod.count = 0; mod.lastUpdateTime = new Date().toISOString() } + }, + + async clearAllCache(): Promise { + await delay(1500) + cacheModules.forEach(m => { m.count = 0; m.lastUpdateTime = new Date().toISOString() }) + } +} + +export const mockAuditLogApi = { + async getAccountAuditLogList(query: AccountAuditLogQuery): Promise> { + await delay(300) + return filterList(accountAuditLogList, query, query.page, query.pageSize) + }, + + async getAccountAuditDetail(id: string): Promise { + await delay(200) + const log = accountAuditLogList.find(l => l.id === id) + if (!log) throw new Error('日志不存在') + return { + id: log.id, + operationTime: log.operationTime, + operator: log.operator, + operationIp: '192.168.1.100', + operationType: log.operationType, + targetAccount: log.targetAccount, + accountType: log.accountType, + bindUnit: log.bindUnit, + beforeData: log.operationType === 'create' ? null : { + username: log.targetAccount, + status: log.operationType === 'disable' ? 'normal' : undefined, + expireDate: log.operationType === 'renew' ? '2026-03-01' : undefined + }, + afterData: { + username: log.targetAccount, + status: log.operationType === 'disable' ? 'stopped' : log.operationType === 'enable' ? 'normal' : undefined, + expireDate: log.operationType === 'renew' ? '2027-04-16' : undefined, + roles: log.operationType === 'create' ? ['医院查看模板'] : undefined + } + } + } +} diff --git a/frontend/super-admin/src/api/types/account.ts b/frontend/super-admin/src/api/types/account.ts new file mode 100644 index 0000000..e4b8bb7 --- /dev/null +++ b/frontend/super-admin/src/api/types/account.ts @@ -0,0 +1,194 @@ +import type { PageQuery, PageResponse, CommonStatus, AccountStatus } from './common' + +// ===== 医院信息 ===== +export interface Hospital { + id: string + name: string + creditCode: string + address: string + contactPerson: string + contactPhone: string + status: CommonStatus + campusCount: number + campuses: HospitalCampus[] + createdAt: string + updatedAt: string +} + +export interface HospitalCampus { + id?: string + name: string + address: string + contactPerson: string +} + +export interface HospitalQuery extends PageQuery { + name?: string + status?: CommonStatus | '' + contactPerson?: string +} + +export interface HospitalFormData { + name: string + creditCode: string + address: string + contactPerson: string + contactPhone: string + status: CommonStatus + campuses: HospitalCampus[] +} + +// ===== 物业公司信息 ===== +export interface PropertyCompany { + id: string + name: string + address: string + contactPerson: string + contactPhone: string + status: CommonStatus + serviceHospitals: string[] + createdAt: string + updatedAt: string +} + +export interface PropertyCompanyQuery extends PageQuery { + name?: string + status?: CommonStatus | '' + contactPerson?: string +} + +export interface PropertyCompanyFormData { + name: string + address: string + contactPerson: string + contactPhone: string +} + +// ===== 医院账号 ===== +export interface HospitalAccount { + id: string + username: string + hospitalId: string + hospitalName: string + roles: string[] + roleNames: string[] + expireDate: string + status: AccountStatus + createdAt: string +} + +export interface HospitalAccountQuery extends PageQuery { + username?: string + hospitalId?: string + status?: AccountStatus | '' + expireFilter?: string +} + +export interface HospitalAccountFormData { + username: string + password: string + hospitalId: string + expireDate: string + roleIds: string[] +} + +// ===== 物业管理员账号 ===== +export interface PropertyAccount { + id: string + username: string + propertyCompanyId: string + propertyName: string + hospitalId: string + hospitalName: string + roles: string[] + roleNames: string[] + expireDate: string + status: AccountStatus + createdAt: string +} + +export interface PropertyAccountQuery extends PageQuery { + username?: string + propertyCompanyId?: string + hospitalId?: string + status?: AccountStatus | '' +} + +export interface PropertyAccountFormData { + username: string + password: string + propertyCompanyId: string + hospitalId: string + expireDate: string + roleIds: string[] +} + +// ===== 到期账号 ===== +export interface ExpiringAccount { + id: string + username: string + accountType: 'hospital' | 'property_admin' + bindUnit: string + expireDate: string + remainDays: number + status: AccountStatus +} + +export interface ExpiringAccountQuery extends PageQuery { + accountType?: 'hospital' | 'property_admin' | '' + expireStatus?: string +} + +export interface ExpiringStats { + expired: number + expiringIn7Days: number + expiringIn30Days: number +} + +// ===== 到期提醒配置 ===== +export interface ExpiryReminderConfig { + reminderDays: number[] + loginPopup: boolean + popupCloseAction: 'normal' | 'restrict' + expiredAction: 'block' | 'remind' +} + +// ===== 账号API接口 ===== +export interface IAccountApi { + // 医院 + getHospitalList(query: HospitalQuery): Promise> + getHospitalDetail(id: string): Promise + createHospital(data: HospitalFormData): Promise<{ id: string }> + updateHospital(id: string, data: HospitalFormData): Promise + toggleHospitalStatus(id: string): Promise + + // 物业公司 + getPropertyCompanyList(query: PropertyCompanyQuery): Promise> + getPropertyCompanyDetail(id: string): Promise + createPropertyCompany(data: PropertyCompanyFormData): Promise<{ id: string }> + updatePropertyCompany(id: string, data: PropertyCompanyFormData): Promise + togglePropertyCompanyStatus(id: string): Promise + + // 医院账号 + getHospitalAccountList(query: HospitalAccountQuery): Promise> + createHospitalAccount(data: HospitalAccountFormData): Promise<{ id: string }> + toggleAccountStatus(id: string): Promise + renewAccount(id: string, expireDate: string): Promise + resetPassword(id: string): Promise<{ newPassword: string }> + + // 物业管理员账号 + getPropertyAccountList(query: PropertyAccountQuery): Promise> + createPropertyAccount(data: PropertyAccountFormData): Promise<{ id: string }> + + // 到期账号 + getExpiringAccountList(query: ExpiringAccountQuery): Promise> + getExpiringStats(): Promise + + // 到期提醒配置 + getExpiryReminderConfig(): Promise + saveExpiryReminderConfig(data: ExpiryReminderConfig): Promise + + // 下拉数据 + getHospitalOptions(): Promise<{ id: string; name: string }[]> + getPropertyCompanyOptions(): Promise<{ id: string; name: string }[]> +} diff --git a/frontend/super-admin/src/api/types/common.ts b/frontend/super-admin/src/api/types/common.ts new file mode 100644 index 0000000..9a43564 --- /dev/null +++ b/frontend/super-admin/src/api/types/common.ts @@ -0,0 +1,37 @@ +// 超级管理员 - 通用类型定义 + +/** 分页请求参数 */ +export interface PageQuery { + page: number + pageSize: number + _mockError?: 'empty' | '403' | 'timeout' | '500' +} + +/** 分页响应 */ +export interface PageResponse { + list: T[] + pagination: { + page: number + pageSize: number + total: number + totalPages: number + } +} + +/** API错误 */ +export class ApiError extends Error { + code: number + constructor(code: number, message: string) { + super(message) + this.code = code + } +} + +/** 通用状态 */ +export type CommonStatus = 'enabled' | 'disabled' + +/** 账号状态 */ +export type AccountStatus = 'normal' | 'expiring' | 'expired' | 'stopped' + +/** 适用范围 */ +export type RoleScope = 'hospital' | 'property_admin' | 'property_staff' diff --git a/frontend/super-admin/src/api/types/permission.ts b/frontend/super-admin/src/api/types/permission.ts new file mode 100644 index 0000000..b75386a --- /dev/null +++ b/frontend/super-admin/src/api/types/permission.ts @@ -0,0 +1,108 @@ +import type { PageQuery, PageResponse, CommonStatus, RoleScope } from './common' + +// ===== 角色 ===== +export interface Role { + id: string + name: string + description: string + scope: RoleScope + isPreset: boolean + accountCount: number + status: CommonStatus + createdAt: string +} + +export interface RoleQuery extends PageQuery { + name?: string + scope?: RoleScope | '' + status?: CommonStatus | '' +} + +export interface RoleFormData { + name: string + description: string + scope: RoleScope + presetTemplate?: string + permissions: string[] +} + +// ===== 权限树 ===== +export interface PermissionNode { + code: string + name: string + type: 'menu' | 'page' | 'feature' | 'action' + children?: PermissionNode[] +} + +// ===== 权限预览 ===== +export interface RolePermission { + menu: string + page: string + feature: string + actions: string[] +} + +// ===== 权限配置注册 ===== +export interface PermissionRegistryItem { + moduleCode: string + moduleName: string + pageCode: string + pageName: string + featureCode: string + featureName: string + actions: string[] +} + +export interface PermissionRegistryQuery extends PageQuery { + moduleName?: string + pageName?: string +} + +// ===== 权限审计日志 ===== +export interface PermissionAuditLog { + id: string + operationTime: string + operator: string + operationType: 'role_create' | 'permission_modify' | 'role_assign' | 'role_remove' + targetRole: string + changeSummary: string + addCount: number + removeCount: number +} + +export interface PermissionAuditLogQuery extends PageQuery { + operator?: string + operationType?: string + role?: string + dateRange?: [string, string] +} + +export interface PermissionAuditDetail { + id: string + operationTime: string + operator: string + operationType: string + targetRole: string + operationIp: string + addedPermissions: string[] + removedPermissions: string[] +} + +// ===== 权限API接口 ===== +export interface IPermissionApi { + getRoleList(query: RoleQuery): Promise> + getRoleDetail(id: string): Promise + createRole(data: RoleFormData): Promise<{ id: string }> + updateRole(id: string, data: RoleFormData): Promise + disableRole(id: string): Promise + deleteRole(id: string): Promise + + getPermissionTree(): Promise + getRolePermissions(id: string): Promise + + getPermissionRegistryList(query: PermissionRegistryQuery): Promise> + refreshPermissionRegistry(): Promise + + getPermissionAuditLogList(query: PermissionAuditLogQuery): Promise> + getPermissionAuditDetail(id: string): Promise +} diff --git a/frontend/super-admin/src/api/types/system.ts b/frontend/super-admin/src/api/types/system.ts new file mode 100644 index 0000000..63651a0 --- /dev/null +++ b/frontend/super-admin/src/api/types/system.ts @@ -0,0 +1,84 @@ +import type { PageQuery, PageResponse } from './common' + +// ===== 系统版本 ===== +export interface SystemVersion { + id: string + version: string + platform: 'web' | 'miniapp' + minCompatibleVersion: string + forceUpdate: boolean + releaseDate: string + description: string +} + +export interface SystemVersionFormData { + version: string + platform: 'web' | 'miniapp' + minCompatibleVersion: string + forceUpdate: boolean + description: string +} + +// ===== 缓存管理 ===== +export interface CacheStatus { + redisStatus: 'connected' | 'disconnected' + memoryUsed: number + memoryTotal: number + connections: number +} + +export interface CacheModule { + name: string + keyPrefix: string + count: number + lastUpdateTime: string +} + +// ===== 账号操作日志 ===== +export interface AccountAuditLog { + id: string + operationTime: string + operator: string + operationType: 'create' | 'edit' | 'enable' | 'disable' | 'renew' | 'reset_password' + targetAccount: string + accountType: 'hospital' | 'property_admin' + bindUnit: string +} + +export interface AccountAuditLogQuery extends PageQuery { + operator?: string + operationType?: string + accountType?: 'hospital' | 'property_admin' | '' + dateRange?: [string, string] +} + +export interface AccountAuditDetail { + id: string + operationTime: string + operator: string + operationIp: string + operationType: string + targetAccount: string + accountType: string + bindUnit: string + beforeData: Record | null + afterData: Record | null +} + +// ===== 系统API接口 ===== +export interface ISystemApi { + getSystemVersionList(): Promise + createSystemVersion(data: SystemVersionFormData): Promise<{ id: string }> + updateSystemVersion(id: string, data: SystemVersionFormData): Promise + getLatestVersions(): Promise<{ web: SystemVersion | null; miniapp: SystemVersion | null }> + + getCacheStatus(): Promise + getCacheModules(): Promise + clearCacheModule(module: string): Promise + clearAllCache(): Promise +} + +export interface IAuditLogApi { + getAccountAuditLogList(query: AccountAuditLogQuery): Promise> + getAccountAuditDetail(id: string): Promise +} diff --git a/frontend/super-admin/src/assets/hero.png b/frontend/super-admin/src/assets/hero.png new file mode 100644 index 0000000..cc51a3d Binary files /dev/null and b/frontend/super-admin/src/assets/hero.png differ diff --git a/frontend/super-admin/src/assets/vite.svg b/frontend/super-admin/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/frontend/super-admin/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/super-admin/src/assets/vue.svg b/frontend/super-admin/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/frontend/super-admin/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/super-admin/src/components/HelloWorld.vue b/frontend/super-admin/src/components/HelloWorld.vue new file mode 100644 index 0000000..5917e16 --- /dev/null +++ b/frontend/super-admin/src/components/HelloWorld.vue @@ -0,0 +1,93 @@ + + + diff --git a/frontend/super-admin/src/components/shared/ActionBar/index.vue b/frontend/super-admin/src/components/shared/ActionBar/index.vue new file mode 100644 index 0000000..d27550c --- /dev/null +++ b/frontend/super-admin/src/components/shared/ActionBar/index.vue @@ -0,0 +1,21 @@ + + + + + + diff --git a/frontend/super-admin/src/components/shared/Breadcrumb/index.vue b/frontend/super-admin/src/components/shared/Breadcrumb/index.vue new file mode 100644 index 0000000..935534e --- /dev/null +++ b/frontend/super-admin/src/components/shared/Breadcrumb/index.vue @@ -0,0 +1,30 @@ + + + + + + diff --git a/frontend/super-admin/src/components/shared/DataTable/index.vue b/frontend/super-admin/src/components/shared/DataTable/index.vue new file mode 100644 index 0000000..4363470 --- /dev/null +++ b/frontend/super-admin/src/components/shared/DataTable/index.vue @@ -0,0 +1,76 @@ + + + + + + diff --git a/frontend/super-admin/src/components/shared/DetailDrawer/index.vue b/frontend/super-admin/src/components/shared/DetailDrawer/index.vue new file mode 100644 index 0000000..3f5a399 --- /dev/null +++ b/frontend/super-admin/src/components/shared/DetailDrawer/index.vue @@ -0,0 +1,30 @@ + + + + + + diff --git a/frontend/super-admin/src/components/shared/FormDialog/index.vue b/frontend/super-admin/src/components/shared/FormDialog/index.vue new file mode 100644 index 0000000..dc1319e --- /dev/null +++ b/frontend/super-admin/src/components/shared/FormDialog/index.vue @@ -0,0 +1,57 @@ + + + + + + diff --git a/frontend/super-admin/src/components/shared/Pagination/index.vue b/frontend/super-admin/src/components/shared/Pagination/index.vue new file mode 100644 index 0000000..d0da400 --- /dev/null +++ b/frontend/super-admin/src/components/shared/Pagination/index.vue @@ -0,0 +1,61 @@ + + + + + + diff --git a/frontend/super-admin/src/components/shared/QueryPanel/index.vue b/frontend/super-admin/src/components/shared/QueryPanel/index.vue new file mode 100644 index 0000000..edc1f0b --- /dev/null +++ b/frontend/super-admin/src/components/shared/QueryPanel/index.vue @@ -0,0 +1,51 @@ + + + + + + diff --git a/frontend/super-admin/src/components/shared/StatusTag/index.vue b/frontend/super-admin/src/components/shared/StatusTag/index.vue new file mode 100644 index 0000000..b0bfd26 --- /dev/null +++ b/frontend/super-admin/src/components/shared/StatusTag/index.vue @@ -0,0 +1,29 @@ + + + + diff --git a/frontend/super-admin/src/components/shared/registry.ts b/frontend/super-admin/src/components/shared/registry.ts new file mode 100644 index 0000000..bdf8e4f --- /dev/null +++ b/frontend/super-admin/src/components/shared/registry.ts @@ -0,0 +1,50 @@ +// @shared 共享组件注册表 +// 修改任何共享组件前,必须查阅此注册表确认影响范围 +// 修改后必须执行100%覆盖测试,并向用户发出变更通知 + +export const SharedComponentRegistry = { + Breadcrumb: { + path: 'components/shared/Breadcrumb/index.vue', + description: '面包屑导航', + referencedBy: ['所有页面'] as string[], + }, + QueryPanel: { + path: 'components/shared/QueryPanel/index.vue', + description: '查询条件面板(折叠/展开)', + referencedBy: ['account:HospitalList', 'account:PropertyCompanyList', 'account:HospitalAccountList', 'account:PropertyAccountList', 'account:ExpiringAccountList', 'permission:RoleList', 'permission:PermissionRegistry', 'permission:PermissionAuditLog', 'audit-log:PermissionLog', 'audit-log:AccountLog'] as string[], + }, + ActionBar: { + path: 'components/shared/ActionBar/index.vue', + description: '操作按钮栏(权限控制+批量操作)', + referencedBy: ['account:HospitalList', 'account:PropertyCompanyList', 'account:HospitalAccountList', 'account:PropertyAccountList', 'permission:RoleList', 'system:VersionList', 'system:CacheManagement'] as string[], + }, + DataTable: { + path: 'components/shared/DataTable/index.vue', + description: '数据表格(排序/选择/行操作)', + referencedBy: ['所有列表页'] as string[], + }, + Pagination: { + path: 'components/shared/Pagination/index.vue', + description: '分页组件', + referencedBy: ['所有列表页'] as string[], + }, + FormDialog: { + path: 'components/shared/FormDialog/index.vue', + description: '表单弹窗(新增/编辑)', + referencedBy: ['account:HospitalAccountList', 'account:PropertyAccountList', 'account:ExpiringAccountList', 'permission:RoleList', 'system:VersionList'] as string[], + }, + DetailDrawer: { + path: 'components/shared/DetailDrawer/index.vue', + description: '详情侧滑抽屉', + referencedBy: ['account:HospitalList', 'account:PropertyCompanyList'] as string[], + }, + StatusTag: { + path: 'components/shared/StatusTag/index.vue', + description: '状态标签(彩色区分)', + referencedBy: ['account:HospitalList', 'account:PropertyCompanyList', 'account:HospitalAccountList', 'account:PropertyAccountList', 'account:ExpiringAccountList', 'permission:RoleList', 'audit-log:PermissionLog', 'audit-log:AccountLog'] as string[], + }, +} as const + +export function getComponentReferences(componentName: string): string[] { + return SharedComponentRegistry[componentName]?.referencedBy ?? [] +} diff --git a/frontend/super-admin/src/env.d.ts b/frontend/super-admin/src/env.d.ts new file mode 100644 index 0000000..15eff1e --- /dev/null +++ b/frontend/super-admin/src/env.d.ts @@ -0,0 +1,21 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} + +interface ImportMetaEnv { + readonly VITE_API_MODE: string + readonly VITE_API_BASE_URL: string + readonly VITE_API_TIMEOUT_GET: string + readonly VITE_API_TIMEOUT_POST: string + readonly VITE_API_TIMEOUT_UPLOAD: string + readonly VITE_API_TIMEOUT_STATS: string + readonly VITE_APP_TITLE: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/frontend/super-admin/src/layouts/AdminLayout.vue b/frontend/super-admin/src/layouts/AdminLayout.vue new file mode 100644 index 0000000..f4fa452 --- /dev/null +++ b/frontend/super-admin/src/layouts/AdminLayout.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/frontend/super-admin/src/layouts/header/HeaderBar.vue b/frontend/super-admin/src/layouts/header/HeaderBar.vue new file mode 100644 index 0000000..1b37f66 --- /dev/null +++ b/frontend/super-admin/src/layouts/header/HeaderBar.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/frontend/super-admin/src/layouts/sidebar/Sidebar.vue b/frontend/super-admin/src/layouts/sidebar/Sidebar.vue new file mode 100644 index 0000000..73445e1 --- /dev/null +++ b/frontend/super-admin/src/layouts/sidebar/Sidebar.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/frontend/super-admin/src/layouts/tags/TagsView.vue b/frontend/super-admin/src/layouts/tags/TagsView.vue new file mode 100644 index 0000000..e5f6867 --- /dev/null +++ b/frontend/super-admin/src/layouts/tags/TagsView.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/frontend/super-admin/src/main.ts b/frontend/super-admin/src/main.ts new file mode 100644 index 0000000..72d5f82 --- /dev/null +++ b/frontend/super-admin/src/main.ts @@ -0,0 +1,36 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import 'element-plus/dist/index.css' +import zhCn from 'element-plus/es/locale/lang/zh-cn' +import App from './App.vue' +import router from './router' +import { createPinia } from 'pinia' +import './styles/reset.scss' + +const app = createApp(App) + +// 注册所有Element Plus图标 +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +// 注册 v-hasPermission 自定义指令 +app.directive('hasPermission', { + mounted(el: HTMLElement, binding) { + const permissionCode = binding.value as string + if (!permissionCode) return + // 延迟检查,确保 store 已初始化 + import('./utils/permission').then(({ hasPermission }) => { + if (!hasPermission(permissionCode)) { + el.parentNode?.removeChild(el) + } + }) + } +}) + +app.use(ElementPlus, { locale: zhCn }) +app.use(createPinia()) +app.use(router) + +app.mount('#app') diff --git a/frontend/super-admin/src/router/index.ts b/frontend/super-admin/src/router/index.ts new file mode 100644 index 0000000..5ce436d --- /dev/null +++ b/frontend/super-admin/src/router/index.ts @@ -0,0 +1,26 @@ +import { createRouter, createWebHistory } from 'vue-router' +import type { RouteRecordRaw } from 'vue-router' +import superAdminRoutes from './modules/super-admin' + +const routes: RouteRecordRaw[] = [ + { + path: '/login', + name: 'Login', + component: () => import('@/views/login/index.vue'), + meta: { title: '登录' }, + }, + ...superAdminRoutes, + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: () => import('@/views/error/404.vue'), + meta: { title: '页面不存在' }, + }, +] + +const router = createRouter({ + history: createWebHistory(), + routes, +}) + +export default router diff --git a/frontend/super-admin/src/router/modules/super-admin.ts b/frontend/super-admin/src/router/modules/super-admin.ts new file mode 100644 index 0000000..7c50349 --- /dev/null +++ b/frontend/super-admin/src/router/modules/super-admin.ts @@ -0,0 +1,173 @@ +import type { RouteRecordRaw } from 'vue-router' +import AdminLayout from '@/layouts/AdminLayout.vue' + +const superAdminRoutes: RouteRecordRaw[] = [ + // === 账号管理 === + { + path: '/account', + name: 'Account', + component: AdminLayout, + meta: { title: '账号管理', icon: 'User' }, + redirect: '/account/hospitals', + children: [ + { + path: 'hospitals', + name: 'HospitalList', + component: () => import('@/views/super-admin/account/HospitalList.vue'), + meta: { title: '医院信息管理', permissions: ['permission:user:view'], keepAlive: true }, + }, + { + path: 'hospitals/create', + name: 'HospitalCreate', + component: () => import('@/views/super-admin/account/HospitalForm.vue'), + meta: { title: '新增医院', permissions: ['permission:user:create'] }, + }, + { + path: 'hospitals/:id/edit', + name: 'HospitalEdit', + component: () => import('@/views/super-admin/account/HospitalForm.vue'), + meta: { title: '编辑医院', permissions: ['permission:user:update'] }, + }, + { + path: 'property-companies', + name: 'PropertyCompanyList', + component: () => import('@/views/super-admin/account/PropertyCompanyList.vue'), + meta: { title: '物业公司信息管理', permissions: ['permission:user:view'], keepAlive: true }, + }, + { + path: 'property-companies/create', + name: 'PropertyCompanyCreate', + component: () => import('@/views/super-admin/account/PropertyCompanyForm.vue'), + meta: { title: '新增物业公司', permissions: ['permission:user:create'] }, + }, + { + path: 'property-companies/:id/edit', + name: 'PropertyCompanyEdit', + component: () => import('@/views/super-admin/account/PropertyCompanyForm.vue'), + meta: { title: '编辑物业公司', permissions: ['permission:user:update'] }, + }, + { + path: 'hospital-accounts', + name: 'HospitalAccountList', + component: () => import('@/views/super-admin/account/HospitalAccountList.vue'), + meta: { title: '医院账号管理', permissions: ['permission:user:view'], keepAlive: true }, + }, + { + path: 'hospital-accounts/create', + name: 'HospitalAccountCreate', + component: () => import('@/views/super-admin/account/HospitalAccountCreate.vue'), + meta: { title: '新增医院账号', permissions: ['permission:user:create'] }, + }, + { + path: 'property-accounts', + name: 'PropertyAccountList', + component: () => import('@/views/super-admin/account/PropertyAccountList.vue'), + meta: { title: '物业管理员账号管理', permissions: ['permission:user:view'], keepAlive: true }, + }, + { + path: 'property-accounts/create', + name: 'PropertyAccountCreate', + component: () => import('@/views/super-admin/account/PropertyAccountCreate.vue'), + meta: { title: '新增物业管理员账号', permissions: ['permission:user:create'] }, + }, + { + path: 'expiring', + name: 'ExpiringAccountList', + component: () => import('@/views/super-admin/account/ExpiringAccountList.vue'), + meta: { title: '到期账号管理', permissions: ['permission:user:view'], keepAlive: true }, + }, + { + path: 'expiry-settings', + name: 'ExpirySettings', + component: () => import('@/views/super-admin/account/ExpirySettings.vue'), + meta: { title: '到期提醒规则配置', permissions: ['permission:config:view'] }, + }, + ], + }, + // === 权限管理 === + { + path: '/permission', + name: 'Permission', + component: AdminLayout, + meta: { title: '权限管理', icon: 'Lock' }, + redirect: '/permission/roles', + children: [ + { + path: 'roles', + name: 'RoleList', + component: () => import('@/views/super-admin/permission/RoleList.vue'), + meta: { title: '角色管理', permissions: ['permission:role:view'], keepAlive: true }, + }, + { + path: 'roles/create', + name: 'RoleCreate', + component: () => import('@/views/super-admin/permission/RoleForm.vue'), + meta: { title: '新增角色', permissions: ['permission:role:create'] }, + }, + { + path: 'roles/:id/edit', + name: 'RoleEdit', + component: () => import('@/views/super-admin/permission/RoleForm.vue'), + meta: { title: '编辑角色', permissions: ['permission:role:update'] }, + }, + { + path: 'registry', + name: 'PermissionRegistry', + component: () => import('@/views/super-admin/permission/PermissionRegistry.vue'), + meta: { title: '权限配置注册', permissions: ['permission:config:view'], keepAlive: true }, + }, + { + path: 'audit-log', + name: 'PermissionAuditLog', + component: () => import('@/views/super-admin/permission/PermissionAuditLog.vue'), + meta: { title: '权限审计日志', permissions: ['audit-log:permission:view'], keepAlive: true }, + }, + ], + }, + // === 系统配置 === + { + path: '/system', + name: 'System', + component: AdminLayout, + meta: { title: '系统配置', icon: 'Setting' }, + redirect: '/system/versions', + children: [ + { + path: 'versions', + name: 'VersionList', + component: () => import('@/views/super-admin/system/VersionManagement.vue'), + meta: { title: '系统版本管理', permissions: ['system:version:view'], keepAlive: true }, + }, + { + path: 'cache', + name: 'CacheManagement', + component: () => import('@/views/super-admin/system/CacheManagement.vue'), + meta: { title: '缓存管理', permissions: ['system:cache:view'] }, + }, + ], + }, + // === 操作日志 === + { + path: '/audit-log', + name: 'AuditLog', + component: AdminLayout, + meta: { title: '操作日志', icon: 'Document' }, + redirect: '/audit-log/permission', + children: [ + { + path: 'permission', + name: 'AuditLogPermission', + component: () => import('@/views/super-admin/audit-log/PermissionChangeLog.vue'), + meta: { title: '权限变更日志', permissions: ['audit-log:permission:view'], keepAlive: true }, + }, + { + path: 'account', + name: 'AuditLogAccount', + component: () => import('@/views/super-admin/audit-log/AccountOperationLog.vue'), + meta: { title: '账号操作日志', permissions: ['audit-log:list:view'], keepAlive: true }, + }, + ], + }, +] + +export default superAdminRoutes diff --git a/frontend/super-admin/src/stores/global/useAppStore.ts b/frontend/super-admin/src/stores/global/useAppStore.ts new file mode 100644 index 0000000..1566c0e --- /dev/null +++ b/frontend/super-admin/src/stores/global/useAppStore.ts @@ -0,0 +1,48 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export interface TagView { + name: string + path: string + title: string + icon?: string + keepAlive?: boolean +} + +export const useAppStore = defineStore('app', () => { + const visitedViews = ref([]) + const cachedViews = ref([]) + + function addView(view: TagView) { + if (visitedViews.value.some(v => v.path === view.path)) return + visitedViews.value.push(view) + if (view.keepAlive && !cachedViews.value.includes(view.name)) { + cachedViews.value.push(view.name) + } + } + + function removeView(path: string) { + const idx = visitedViews.value.findIndex(v => v.path === path) + if (idx > -1) visitedViews.value.splice(idx, 1) + const name = visitedViews.value[idx]?.name + if (name) { + const cacheIdx = cachedViews.value.indexOf(name) + if (cacheIdx > -1) cachedViews.value.splice(cacheIdx, 1) + } + } + + function removeOtherViews(path: string) { + visitedViews.value = visitedViews.value.filter(v => v.path === path) + cachedViews.value = cachedViews.value.filter(name => + visitedViews.value.some(v => v.name === name) + ) + } + + return { + visitedViews, + cachedViews, + addView, + removeView, + removeOtherViews, + } +}) diff --git a/frontend/super-admin/src/stores/global/useUserStore.ts b/frontend/super-admin/src/stores/global/useUserStore.ts new file mode 100644 index 0000000..77a01eb --- /dev/null +++ b/frontend/super-admin/src/stores/global/useUserStore.ts @@ -0,0 +1,53 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const useUserStore = defineStore('user', () => { + const userInfo = ref({ + id: '1', + username: 'admin', + realName: '超级管理员', + avatar: '', + }) + + const permissions = ref([ + // 账号管理权限 + 'permission:user:view', + 'permission:user:create', + 'permission:user:update', + 'permission:user:delete', + // 权限管理权限 + 'permission:role:view', + 'permission:role:create', + 'permission:role:update', + 'permission:role:delete', + 'permission:config:view', + 'permission:config:update', + // 系统配置权限 + 'system:version:view', + 'system:version:create', + 'system:version:update', + 'system:cache:view', + 'system:cache:update', + // 操作日志权限 + 'audit-log:permission:view', + 'audit-log:list:view', + ]) + + const sidebarCollapsed = ref(false) + + function hasPermission(code: string): boolean { + return permissions.value.includes(code) + } + + function toggleSidebar() { + sidebarCollapsed.value = !sidebarCollapsed.value + } + + return { + userInfo, + permissions, + sidebarCollapsed, + hasPermission, + toggleSidebar, + } +}) diff --git a/frontend/super-admin/src/style.css b/frontend/super-admin/src/style.css new file mode 100644 index 0000000..527d4fb --- /dev/null +++ b/frontend/super-admin/src/style.css @@ -0,0 +1,296 @@ +:root { + --text: #6b6375; + --text-h: #08060d; + --bg: #fff; + --border: #e5e4e7; + --code-bg: #f4f3ec; + --accent: #aa3bff; + --accent-bg: rgba(170, 59, 255, 0.1); + --accent-border: rgba(170, 59, 255, 0.5); + --social-bg: rgba(244, 243, 236, 0.5); + --shadow: + rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; + + --sans: system-ui, 'Segoe UI', Roboto, sans-serif; + --heading: system-ui, 'Segoe UI', Roboto, sans-serif; + --mono: ui-monospace, Consolas, monospace; + + font: 18px/145% var(--sans); + letter-spacing: 0.18px; + color-scheme: light dark; + color: var(--text); + background: var(--bg); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + @media (max-width: 1024px) { + font-size: 16px; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --text: #9ca3af; + --text-h: #f3f4f6; + --bg: #16171d; + --border: #2e303a; + --code-bg: #1f2028; + --accent: #c084fc; + --accent-bg: rgba(192, 132, 252, 0.15); + --accent-border: rgba(192, 132, 252, 0.5); + --social-bg: rgba(47, 48, 58, 0.5); + --shadow: + rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; + } + + #social .button-icon { + filter: invert(1) brightness(2); + } +} + +body { + margin: 0; +} + +h1, +h2 { + font-family: var(--heading); + font-weight: 500; + color: var(--text-h); +} + +h1 { + font-size: 56px; + letter-spacing: -1.68px; + margin: 32px 0; + @media (max-width: 1024px) { + font-size: 36px; + margin: 20px 0; + } +} +h2 { + font-size: 24px; + line-height: 118%; + letter-spacing: -0.24px; + margin: 0 0 8px; + @media (max-width: 1024px) { + font-size: 20px; + } +} +p { + margin: 0; +} + +code, +.counter { + font-family: var(--mono); + display: inline-flex; + border-radius: 4px; + color: var(--text-h); +} + +code { + font-size: 15px; + line-height: 135%; + padding: 4px 8px; + background: var(--code-bg); +} + +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#app { + width: 1126px; + max-width: 100%; + margin: 0 auto; + text-align: center; + border-inline: 1px solid var(--border); + min-height: 100svh; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/frontend/super-admin/src/styles/reset.scss b/frontend/super-admin/src/styles/reset.scss new file mode 100644 index 0000000..c0f3234 --- /dev/null +++ b/frontend/super-admin/src/styles/reset.scss @@ -0,0 +1,68 @@ +// 样式重置 +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body, #app { + width: 100%; + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-size: $--font-size-base; + color: #333; + background-color: #f5f7fa; +} + +// 滚动条 +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-thumb { + background: #c0c4cc; + border-radius: 3px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +// 页面容器 +.page-container { + padding: $--spacing-page; +} + +// 查询条件区域 +.query-panel { + background: #fff; + padding: $--spacing-section; + border-radius: 4px; + margin-bottom: $--spacing-item; +} + +// 操作栏区域 +.action-bar { + background: #fff; + padding: $--spacing-item $--spacing-section; + border-radius: 4px; + margin-bottom: $--spacing-item; +} + +// 表格区域 +.data-table-wrapper { + background: #fff; + padding: $--spacing-section; + border-radius: 4px; + margin-bottom: $--spacing-item; +} + +// 分页 +.pagination-wrapper { + background: #fff; + padding: $--spacing-item $--spacing-section; + border-radius: 4px; + display: flex; + justify-content: flex-end; +} diff --git a/frontend/super-admin/src/styles/variables.scss b/frontend/super-admin/src/styles/variables.scss new file mode 100644 index 0000000..3ce910b --- /dev/null +++ b/frontend/super-admin/src/styles/variables.scss @@ -0,0 +1,25 @@ +// Element Plus主题变量覆盖 + +// 品牌色 +$--color-primary: #409EFF; +$--color-success: #67C23A; +$--color-warning: #E6A23C; +$--color-danger: #F56C6C; +$--color-info: #909399; + +// 布局 +$--layout-sidebar-width: 220px; +$--layout-sidebar-collapsed-width: 64px; +$--layout-header-height: 56px; +$--layout-tags-height: 36px; +$--layout-footer-height: 48px; + +// 字体 +$--font-size-base: 14px; +$--font-size-small: 12px; +$--font-size-large: 16px; + +// 间距 +$--spacing-page: 20px; +$--spacing-section: 16px; +$--spacing-item: 12px; diff --git a/frontend/super-admin/src/utils/debounce-submit.ts b/frontend/super-admin/src/utils/debounce-submit.ts new file mode 100644 index 0000000..151e59f --- /dev/null +++ b/frontend/super-admin/src/utils/debounce-submit.ts @@ -0,0 +1,39 @@ +import { ref } from 'vue' + +interface PendingRequest { + key: string + timestamp: number +} + +const pendingRequests = new Map() + +/** H1 防重复提交 */ +export function useDebounceSubmit() { + const submitLoading = ref(false) + + async function debounceSubmit( + key: string, + fn: () => Promise, + options?: { cooldown?: number } + ): Promise { + if (submitLoading.value) return null + + const pending = pendingRequests.get(key) + const cooldown = options?.cooldown ?? 1000 + if (pending && Date.now() - pending.timestamp < cooldown) { + return null + } + + submitLoading.value = true + pendingRequests.set(key, { key, timestamp: Date.now() }) + + try { + return await fn() + } finally { + submitLoading.value = false + pendingRequests.delete(key) + } + } + + return { submitLoading, debounceSubmit } +} diff --git a/frontend/super-admin/src/utils/dirty-check.ts b/frontend/super-admin/src/utils/dirty-check.ts new file mode 100644 index 0000000..0590dc0 --- /dev/null +++ b/frontend/super-admin/src/utils/dirty-check.ts @@ -0,0 +1,57 @@ +import { ref, watch, onMounted, onUnmounted } from 'vue' +import type { Ref } from 'vue' +import { ElMessageBox } from 'element-plus' + +/** H4 脏数据检测 */ +export function useDirtyCheck>( + formData: Ref, +) { + const initialSnapshot = ref('') + const isDirty = ref(false) + + function saveSnapshot() { + initialSnapshot.value = JSON.stringify(formData.value) + isDirty.value = false + } + + watch(formData, () => { + if (initialSnapshot.value) { + isDirty.value = JSON.stringify(formData.value) !== initialSnapshot.value + } + }, { deep: true }) + + function handleBeforeUnload(e: BeforeUnloadEvent) { + if (isDirty.value) { + e.preventDefault() + e.returnValue = '' + } + } + + onMounted(() => { + window.addEventListener('beforeunload', handleBeforeUnload) + }) + + onUnmounted(() => { + window.removeEventListener('beforeunload', handleBeforeUnload) + }) + + async function checkBeforeLeave(): Promise { + if (!isDirty.value) return true + try { + await ElMessageBox.confirm( + '当前修改尚未保存,离开后将丢失未保存的内容,是否确认离开?', + '提示', + { + confirmButtonText: '离开', + cancelButtonText: '留在此页', + type: 'warning', + } + ) + return true + } catch { + return false + } + } + + return { isDirty, saveSnapshot, checkBeforeLeave } +} diff --git a/frontend/super-admin/src/utils/permission.ts b/frontend/super-admin/src/utils/permission.ts new file mode 100644 index 0000000..954b336 --- /dev/null +++ b/frontend/super-admin/src/utils/permission.ts @@ -0,0 +1,13 @@ +import { useUserStore } from '@/stores/global/useUserStore' + +/** 权限校验 */ +export function hasPermission(code: string): boolean { + const userStore = useUserStore() + return userStore.hasPermission(code) +} + +/** 权限指令辅助 */ +export function checkPermissions(codes: string[]): boolean { + if (!codes || codes.length === 0) return true + return codes.every(code => hasPermission(code)) +} diff --git a/frontend/super-admin/src/utils/request.ts b/frontend/super-admin/src/utils/request.ts new file mode 100644 index 0000000..2a47cd6 --- /dev/null +++ b/frontend/super-admin/src/utils/request.ts @@ -0,0 +1,97 @@ +import axios from 'axios' +import { ElMessage, ElLoading } from 'element-plus' +import { showTimeoutError, showNetworkError } from './result-feedback' + +const service = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || '/api/v1', + timeout: Number(import.meta.env.VITE_API_TIMEOUT_GET) || 15000, +}) + +let loadingInstance: ReturnType | null = null +let loadingCount = 0 + +// 请求拦截器 +service.interceptors.request.use( + (config) => { + // 动态超时 + if (config.method === 'post' || config.method === 'put') { + config.timeout = Number(import.meta.env.VITE_API_TIMEOUT_POST) || 30000 + } + if (config.url?.includes('/upload')) { + config.timeout = 60000 + } + + // 模拟Token + const token = localStorage.getItem('token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + + return config + }, + (error) => Promise.reject(error) +) + +// 响应拦截器 +service.interceptors.response.use( + (response) => { + closeLoading() + const { code, message } = response.data + if (code && code !== 0 && code !== 200) { + ElMessage.error(message || '请求失败') + return Promise.reject(new Error(message)) + } + return response.data + }, + (error) => { + closeLoading() + if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) { + showTimeoutError() + } else if (!window.navigator.onLine) { + showNetworkError() + } else if (error.response) { + const { status, data } = error.response + if (status === 401) { + ElMessage.error('登录已过期,请重新登录') + } else if (status === 403) { + ElMessage.error('无权限访问') + } else if (status === 500) { + ElMessage.error(data?.message || '服务器内部错误') + } else { + ElMessage.error(data?.message || '请求失败') + } + } else { + showNetworkError() + } + return Promise.reject(error) + } +) + +function closeLoading() { + loadingCount-- + if (loadingCount <= 0) { + loadingCount = 0 + if (loadingInstance) { + loadingInstance.close() + loadingInstance = null + } + } +} + +/** 显示全局Loading(>2秒时自动触发) */ +export function showGlobalLoading() { + loadingCount++ + if (!loadingInstance) { + setTimeout(() => { + if (loadingCount > 0 && !loadingInstance) { + loadingInstance = ElLoading.service({ + lock: true, + text: '加载中...', + background: 'rgba(255, 255, 255, 0.7)', + }) + } + }, 2000) + } +} + +export default service diff --git a/frontend/super-admin/src/utils/result-feedback.ts b/frontend/super-admin/src/utils/result-feedback.ts new file mode 100644 index 0000000..da50d6b --- /dev/null +++ b/frontend/super-admin/src/utils/result-feedback.ts @@ -0,0 +1,56 @@ +import { ElMessage, ElMessageBox } from 'element-plus' + +/** H3 操作确认 */ +export async function confirmAction( + message: string, + title = '操作确认', + type: 'warning' | 'error' = 'warning' +): Promise { + try { + await ElMessageBox.confirm(message, title, { + confirmButtonText: '确认', + cancelButtonText: '取消', + type, + }) + return true + } catch { + return false + } +} + +/** H8 操作成功反馈 */ +export function showSuccess(message = '操作成功', duration = 2000) { + ElMessage.success({ message, duration }) +} + +/** H8 操作失败反馈 */ +export function showError(message = '操作失败,请稍后重试', duration = 0) { + ElMessage.error({ message, duration }) +} + +/** H8 网络异常反馈 */ +export function showNetworkError() { + ElMessage.error({ + message: '网络连接异常,请检查网络后重试', + duration: 0, + showClose: true, + }) +} + +/** H8 超时反馈 */ +export function showTimeoutError() { + ElMessage.error({ + message: '请求超时,请检查网络后重试', + duration: 0, + showClose: true, + }) +} + +/** 统一操作结果反馈 */ +export function showResultFeedback(type: 'success' | 'error', message: string) { + if (type === 'success') { + showSuccess(message) + } else { + showError(message) + } +} diff --git a/frontend/super-admin/src/views/error/403.vue b/frontend/super-admin/src/views/error/403.vue new file mode 100644 index 0000000..a7c3b56 --- /dev/null +++ b/frontend/super-admin/src/views/error/403.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/frontend/super-admin/src/views/error/404.vue b/frontend/super-admin/src/views/error/404.vue new file mode 100644 index 0000000..56568ba --- /dev/null +++ b/frontend/super-admin/src/views/error/404.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/frontend/super-admin/src/views/error/500.vue b/frontend/super-admin/src/views/error/500.vue new file mode 100644 index 0000000..f1f4ce4 --- /dev/null +++ b/frontend/super-admin/src/views/error/500.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/frontend/super-admin/src/views/login/index.vue b/frontend/super-admin/src/views/login/index.vue new file mode 100644 index 0000000..b5e37bb --- /dev/null +++ b/frontend/super-admin/src/views/login/index.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/frontend/super-admin/src/views/super-admin/account/ExpiringAccountList.vue b/frontend/super-admin/src/views/super-admin/account/ExpiringAccountList.vue new file mode 100644 index 0000000..8d5dd03 --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/account/ExpiringAccountList.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/frontend/super-admin/src/views/super-admin/account/ExpirySettings.vue b/frontend/super-admin/src/views/super-admin/account/ExpirySettings.vue new file mode 100644 index 0000000..888361f --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/account/ExpirySettings.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/frontend/super-admin/src/views/super-admin/account/HospitalAccountCreate.vue b/frontend/super-admin/src/views/super-admin/account/HospitalAccountCreate.vue new file mode 100644 index 0000000..095faee --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/account/HospitalAccountCreate.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/frontend/super-admin/src/views/super-admin/account/HospitalAccountList.vue b/frontend/super-admin/src/views/super-admin/account/HospitalAccountList.vue new file mode 100644 index 0000000..f037828 --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/account/HospitalAccountList.vue @@ -0,0 +1,232 @@ + + + diff --git a/frontend/super-admin/src/views/super-admin/account/HospitalForm.vue b/frontend/super-admin/src/views/super-admin/account/HospitalForm.vue new file mode 100644 index 0000000..62cdc58 --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/account/HospitalForm.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/frontend/super-admin/src/views/super-admin/account/HospitalList.vue b/frontend/super-admin/src/views/super-admin/account/HospitalList.vue new file mode 100644 index 0000000..77c0090 --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/account/HospitalList.vue @@ -0,0 +1,202 @@ + + + diff --git a/frontend/super-admin/src/views/super-admin/account/PropertyAccountCreate.vue b/frontend/super-admin/src/views/super-admin/account/PropertyAccountCreate.vue new file mode 100644 index 0000000..a48362e --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/account/PropertyAccountCreate.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/frontend/super-admin/src/views/super-admin/account/PropertyAccountList.vue b/frontend/super-admin/src/views/super-admin/account/PropertyAccountList.vue new file mode 100644 index 0000000..d424066 --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/account/PropertyAccountList.vue @@ -0,0 +1,218 @@ + + + diff --git a/frontend/super-admin/src/views/super-admin/account/PropertyCompanyForm.vue b/frontend/super-admin/src/views/super-admin/account/PropertyCompanyForm.vue new file mode 100644 index 0000000..5ec142e --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/account/PropertyCompanyForm.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/frontend/super-admin/src/views/super-admin/account/PropertyCompanyList.vue b/frontend/super-admin/src/views/super-admin/account/PropertyCompanyList.vue new file mode 100644 index 0000000..3e56487 --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/account/PropertyCompanyList.vue @@ -0,0 +1,145 @@ + + + diff --git a/frontend/super-admin/src/views/super-admin/audit-log/AccountOperationLog.vue b/frontend/super-admin/src/views/super-admin/audit-log/AccountOperationLog.vue new file mode 100644 index 0000000..2e89425 --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/audit-log/AccountOperationLog.vue @@ -0,0 +1,152 @@ + + + diff --git a/frontend/super-admin/src/views/super-admin/audit-log/PermissionChangeLog.vue b/frontend/super-admin/src/views/super-admin/audit-log/PermissionChangeLog.vue new file mode 100644 index 0000000..f960b2b --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/audit-log/PermissionChangeLog.vue @@ -0,0 +1,133 @@ + + + diff --git a/frontend/super-admin/src/views/super-admin/permission/PermissionAuditLog.vue b/frontend/super-admin/src/views/super-admin/permission/PermissionAuditLog.vue new file mode 100644 index 0000000..63b8e66 --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/permission/PermissionAuditLog.vue @@ -0,0 +1,100 @@ + + + diff --git a/frontend/super-admin/src/views/super-admin/permission/PermissionRegistry.vue b/frontend/super-admin/src/views/super-admin/permission/PermissionRegistry.vue new file mode 100644 index 0000000..c274359 --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/permission/PermissionRegistry.vue @@ -0,0 +1,69 @@ + + + diff --git a/frontend/super-admin/src/views/super-admin/permission/RoleForm.vue b/frontend/super-admin/src/views/super-admin/permission/RoleForm.vue new file mode 100644 index 0000000..00a4987 --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/permission/RoleForm.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/frontend/super-admin/src/views/super-admin/permission/RoleList.vue b/frontend/super-admin/src/views/super-admin/permission/RoleList.vue new file mode 100644 index 0000000..46af25e --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/permission/RoleList.vue @@ -0,0 +1,159 @@ + + + diff --git a/frontend/super-admin/src/views/super-admin/system/CacheManagement.vue b/frontend/super-admin/src/views/super-admin/system/CacheManagement.vue new file mode 100644 index 0000000..6ecfcc3 --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/system/CacheManagement.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/frontend/super-admin/src/views/super-admin/system/VersionManagement.vue b/frontend/super-admin/src/views/super-admin/system/VersionManagement.vue new file mode 100644 index 0000000..1a80122 --- /dev/null +++ b/frontend/super-admin/src/views/super-admin/system/VersionManagement.vue @@ -0,0 +1,201 @@ + + + diff --git a/frontend/super-admin/tsconfig.app.json b/frontend/super-admin/tsconfig.app.json new file mode 100644 index 0000000..5c750c5 --- /dev/null +++ b/frontend/super-admin/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client"], + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/frontend/super-admin/tsconfig.json b/frontend/super-admin/tsconfig.json new file mode 100644 index 0000000..4b0bc2e --- /dev/null +++ b/frontend/super-admin/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/super-admin/tsconfig.node.json b/frontend/super-admin/tsconfig.node.json new file mode 100644 index 0000000..d3c52ea --- /dev/null +++ b/frontend/super-admin/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/super-admin/vite.config.ts b/frontend/super-admin/vite.config.ts new file mode 100644 index 0000000..2ed7789 --- /dev/null +++ b/frontend/super-admin/vite.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import path from 'path' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, + }, + css: { + preprocessorOptions: { + scss: { + additionalData: `@import "@/styles/variables.scss";`, + api: 'modern-compiler', + }, + }, + }, + server: { + port: 3000, + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + }, + }, + }, +})