# 会议出勤（Meeting Attendance）- UI 交互规范

> **版本**: v1.2
> **最后更新**: 2026-04-21
> **维护者**: 前端团队

> **参考标准**: [设计系统（权威）](../../../.agents/skills/frontend-main/references/design-system-standards.md)

---

## ✅ 机器读取区（必填）

### 页面清单

| 页面 | 路由 | 说明 | 状态 |
|------|------|------|------|
| 登录页 | `/login` | FFOA 统一登录 | ✅ |
| 仪表盘 | `/meetingattendance/dashboard` | 会议总览 | ✅ |
| 会议列表 | `/meetingattendance/meetings` | 会议管理 | ✅ |
| 会议创建 | `/meetingattendance/meetings/create` | 新建会议 | ✅ |
| 会议详情 | `/meetingattendance/meetings/:id` | 会议详情与签到情况 | ✅ |
| 会议二维码 | `/meetingattendance/meetings/:id/qr` | 二维码展示 | ✅ |
| 系列会议 | `/meetingattendance/series` | 系列会议管理 | ✅ |
| 模板管理 | `/meetingattendance/templates` | 会议模板管理 | ✅ |
| 用户管理 | `/meetingattendance/users` | 用户查看与跳转组织架构管理 | ✅ |
| 报表 | `/meetingattendance/reports` | 单场/系列报表 | ✅ |
| 审计日志 | `/meetingattendance/audit-logs` | 操作审计 | ✅ |
| 签到页（系统用户） | `/meetingattendance/checkin` | 登录后签到 | ✅ |
| 签到页（访客） | `/meetingattendance/checkin/guest` | 访客免登录签到 | ✅ |
| 修改密码 | `/meetingattendance/settings/change-password` | 修改密码 | ✅ |
| Teams-会议 | `/meetingattendance/teams/meetings` | Teams 入口 | ✅ |
| Teams-报表 | `/meetingattendance/teams/reports` | Teams 报表 | ✅ |
| Teams-配置 | `/meetingattendance/teams/config` | Teams 配置 | ✅ |
| Outlook 同步中心 | `/meetingattendance/integrations/outlook` | 候选会议筛选、纳管接管、同步历史 | ✅ |
| Outlook 已绑定会议总览（管理员） | `/meetingattendance/integrations/outlook/bindings-all` | 跨邮箱查看所有已绑定会议 | ✅ |
| 议程编辑页（v1.x） | `/meetingattendance/[meetingId]/agenda/edit` | 编辑会议议程（段 + 项 + 拖排序） | ✅ |
| 我的待办页（v1.x） | `/meetingattendance/my-tasks` | 员工查看议程项上传待办 | ✅ |

### 元素清单（最小）

| 元素 | 类型 | 说明 |
|------|------|------|
| 会议列表 | 表格 | 列表展示会议 |
| 会议表单 | 表单 | 创建/编辑会议 |
| 二维码展示 | 图片 | 线上/线下二维码 |
| 参会名单 | 列表 | 必需参会人员（仅系统用户） |
| 同步状态提示 | 提示条 | 展示 Outlook 纳管状态与“已转为本地维护，不再同步”说明 |
| 签到状态 | 标签 | ON_SITE/ONLINE/LATE 等 |
| 报表图表 | 图表 | 出勤率与状态分布 |
| 签到方式校验开关 | 开关（v1.2） | 会议/系列维度切换；系列切换时级联刷新下属 meeting；开启前 guard 要求 `city != null` |
| 会议/系列地点输入 | 输入+自动补全（v1.2） | 会议/系列创建编辑页；数据源 `GET /cities/suggestions` |
| 工作地输入 | 输入+自动补全（v1.2） | 组织架构用户管理页；数据源同上 |
| 允许签到方式标签 | 标签（v1.2） | 会议详情参会人列表每人一个：`线下 / 线上 / 已调整`（三种来源：城市派生 / 系列级覆盖 / 会议级覆盖） |
| Excel 导入预览弹窗 | 弹窗（v1.2） | 工作地批量导入分类展示（已有/相似警告/全新），管理员确认后落库 |
| 系列默认参会人弹窗签到方式下拉 | 下拉（v1.2） | 系列"管理默认参会人"弹窗每行加 `线下/线上/跟随工作地` 下拉 + 批量设置 + 顶部搜索 |

### 交互清单（最小）

| 交互 | 触发 | API | 结果 |
|------|------|-----|------|
| 进入会议列表 | 页面加载 | GET /meeting-attendance/meetings | 展示会议列表 |
| 创建会议 | 提交表单 | POST /meeting-attendance/meetings | 会议创建成功 + 生成二维码 |
| 删除单次会议参会人 | 点击删除按钮 | DELETE /meeting-attendance/meetings/:id/required-attendees/:userId | 删除未产生真实出勤记录的参会人；若会议为 Outlook 纳管会议，则同时标记“已转为本地维护” |
| 访客签到 | 提交表单 | POST /meeting-attendance/meetings/:id/guest-checkin | 显示签到结果 |
| 系统签到 | 扫码/按钮 | POST /meeting-attendance/meetings/:id/checkin | 更新出勤状态 |
| 生成报表 | 选择范围 | GET /meeting-attendance/reports/series | 渲染统计与排名 |
| 纳管候选会议 | 点击纳管 | POST /meeting-attendance/integrations/outlook/bindings | single 直接生成本地会议；seriesMaster 仅生成系列锚点，实例会议由 occurrence/exception 同步生成；已结束 seriesMaster 返回明确提示并拒绝纳管 |
| 强制接管纳管绑定 | 点击接管 | POST /meeting-attendance/integrations/outlook/bindings/:id/takeover | 绑定人和来源邮箱切换为当前管理员 |
| 系列纳管自动覆盖 | 纳管 seriesMaster 或其 occurrence | POST /meeting-attendance/integrations/outlook/bindings + Delta/Reconcile | seriesMaster 仅维护系列定义与绑定；系统自动维护该系列下 occurrence/exception 的本地绑定、会议与签到项 |
| 自动纳管当前管理员邮箱 | 首次进入同步页且未指定 mailboxId | GET /meeting-attendance/integrations/outlook/candidates | 若管理员邮箱未登记源邮箱，系统自动创建并启用个人邮箱后返回候选会议 |
| 首次进入快照初始化 | 首次进入同步页且邮箱尚无同步游标 | GET /meeting-attendance/integrations/outlook/candidates | 异步初始化该邮箱候选快照（含系列实例时间窗），不阻塞当前响应；接口返回 `snapshotInitializing=true` 时页面展示“数据初始化中”提示 |
| 候选按会议类型筛选 | 选择会议类型下拉 | 前端本地筛选 | 支持 ALL/seriesMaster/occurrence/exception 等类型过滤；“包含历史会议”未勾选时严格仅展示未来会议（`startTime >= now`）；已结束 seriesMaster 不展示为候选 |
| 系列子项展开 | 点击系列行展开按钮 | GET /meeting-attendance/integrations/outlook/candidates/:seriesMasterId/children | 优先读取本地快照子项；仅在本地无子项时回源 Outlook 拉取并回写快照；快照游标陈旧时后台异步触发同步，不阻塞展开；若系列已结束则返回空子项 |
| 排除单次 occurrence | 候选列表点击“排除此实例” | POST /meeting-attendance/integrations/outlook/bindings/:id/exclusions | 系列下指定 occurrence 不再同步到签到 |
| 查询已纳管会议 | 输入关键词/翻页 | GET /meeting-attendance/integrations/outlook/bindings | 展示已纳管会议维护列表 |
| 查看纳管详情 | 点击详情 | GET /meeting-attendance/integrations/outlook/bindings/:id | 展示绑定详情与来源信息 |
| 查看/撤销排除实例 | 详情弹窗操作 | GET /meeting-attendance/integrations/outlook/bindings/:id/exclusions + POST /meeting-attendance/integrations/outlook/exclusions/:id/remove | 管理系列单次排除列表 |
| 查看同步历史 | 详情弹窗加载/翻页 | GET /meeting-attendance/integrations/outlook/bindings/:id/history | 服务端分页展示绑定维度同步时间线 |
| 查看官方源版本差异 | 详情弹窗展开某次同步记录 | GET /meeting-attendance/integrations/outlook/bindings/:id/history | 时间线行内展示 Outlook 官方更新时间、获取时间、是否发生内容变化、变化字段与关键差异摘要；支持展开查看参会人增删清单 |
| 过滤失败事件 | 勾选“仅失败事件” | GET /meeting-attendance/integrations/outlook/bindings/:id/history | 服务端返回 resultStatus=ERROR 的同步日志 |
| 历史筛选与导出 | 选择事件/阶段/时间范围并导出 | GET /meeting-attendance/integrations/outlook/bindings/:id/history + GET /meeting-attendance/integrations/outlook/bindings/:id/history/export.csv | 支持服务端筛选并导出匹配结果 |
| 立即对账 | 点击立即对账 | POST /meeting-attendance/integrations/outlook/sync/reconcile | 触发当前上下文邮箱对账；先跑 Delta/对账，再按批次补齐当前候选窗口内缺少子快照的系列 |
| 调整同步参数 | 编辑并保存同步设置 | GET/PATCH /meeting-attendance/integrations/outlook/settings | 更新对账频率、拉取窗口、批次与续订提前量 |
| 查看所有已绑定会议（管理员） | 打开总览页/搜索/翻页 | GET /meeting-attendance/integrations/outlook/bindings/all | 返回跨邮箱已绑定会议分页列表 |
| 系列纳管进行中禁用子项 | 系列主会议纳管后（补齐进行中） | GET /meeting-attendance/integrations/outlook/candidates | 系列下 occurrence/exception 显示“纳管中（继承系列）”并禁用按钮，防止重复纳管 |
| 候选/已纳管请求去重 | 快速翻页/快速切筛选 | GET /meeting-attendance/integrations/outlook/candidates + GET /meeting-attendance/integrations/outlook/bindings | 取消旧请求，仅保留最新请求结果，避免空页和旧数据覆盖 |
| 组织者并入参会人开关 | 切换同步设置并保存 | PATCH /meeting-attendance/integrations/outlook/settings | 控制 organizer 是否写入参会人集合（默认关闭） |
| 切换会议签到方式校验（v1.2） | 会议详情页/创建编辑页 切换开关 | PATCH /meeting-attendance/meetings/:id/enforce-checkin-mode | 后端 guard 开启前要求 city 非空；不影响 Outlook 同步状态 |
| 切换系列签到方式校验（v1.2） | 系列管理页 切换开关 | PATCH /meeting-attendance/series/:id/enforce-checkin-mode | 级联刷新下属 meeting 的开关；city 为空时拒绝开启 |
| 编辑参会人签到方式 - 会议级（v1.2） | 会议详情参会人列表点击"允许方式" tag | PATCH /meeting-attendance/meetings/:id/required-attendees/:userId/checkin-mode | 本场级覆盖；支持恢复默认（传 null） |
| 按允许方式筛选参会人（v1.2） | 会议详情页点击 chips | 前端本地筛选 | 支持 `允许线下 / 允许线上 / 已调整` 切换（未打标用户工作地的以"未设置"单独一类） |
| Optional 状态卡（v1.4） | 会议详情页 Attendance Statistics 栏 Online 卡后（**恒定渲染**） | 前端本地筛选 | 紫色 indigo 卡：`optionalTotal > 0` 显示 `optionalAttended/optionalTotal`，`optionalTotal === 0` 显示单 `-`；可点击 toggle `statusFilter='OPTIONAL_ONLY'`，下方参会人列表筛到仅 OPTIONAL_ATTENDEE。其他 status 卡的列表筛选**排除 optional**，跟卡数字一致 |
| Optional 出勤率小标（v1.4） | 会议详情页 Attendance Rate 卡下方 + 报表页应到卡下方 | 仅展示 | `Optional: X/Y` 文字小字（`optionalTotal === 0` 时显示 `Optional: -`）；强调 optional 不计入主出勤率 |
| 线上/线下卡分母（v1.4） | 会议详情页 Attendance Statistics 栏 ON_SITE / ONLINE 卡 | 仅展示 | `enforceCheckinMode = true` → `X/Y`（Y 来自 `allowedMode` 为对应方式的常规参会人数）；`false` → `X/-`（横杠占位，表示分母不适用） |
| 设置系列级参会人签到方式（v1.2） | 系列"管理默认参会人"弹窗逐行下拉 or 批量设置 | PUT /meeting-attendance/series/:id/attendee-preferences | 写入 MeetingSeriesAttendeePreference；优先级高于城市派生、低于会议级覆盖 |
| 清除系列级参会人覆盖（v1.2） | 系列弹窗选"跟随工作地" | DELETE /meeting-attendance/series/:id/attendee-preferences/:userId | 恢复城市派生 |
| 系列默认参会人弹窗搜索（v1.2） | 弹窗顶部搜索框 | GET /meeting-attendance/series/:id/attendee-preferences?keyword= | 按姓名/邮箱模糊搜索参会人 |
| 城市自动补全（v1.2） | 工作地/会议地点/系列地点输入框聚焦或输入 | GET /meeting-attendance/cities/suggestions | 返回已有城市字符串列表，按拼音/字母排序 |
| 工作地 Excel 导入（v1.2） | 组织架构用户管理页"批量导入" | POST /organization/users/import-work-city（preview=true 预览，preview=false 提交） | 预览分类（已有/相似/全新）→ 管理员确认 → 落库 |
| 扫错码拒签（v1.2） | 扫码打开签到页 | POST /meeting-attendance/meetings/:id/guest-checkin | `MEETING_ATTENDANCE_033`，前端显示"请使用线下/线上专属二维码" |
| 用户工作地未配置拒签（v1.2） | 扫码打开签到页 | POST /meeting-attendance/meetings/:id/guest-checkin | `MEETING_ATTENDANCE_034`，前端显示"请联系管理员配置您的工作地" |
| 会议地点未配置拒签（v1.2） | 扫码打开签到页（边缘情况，guard 未生效时） | POST /meeting-attendance/meetings/:id/guest-checkin | `MEETING_ATTENDANCE_036`，前端显示"会议未配置地点" |

### 国际化与文案规范

- 所有用户可见文案必须使用 i18n key，不允许硬编码。
- 动态内容使用插值，不拼接字符串。
- 日期/数字必须本地化格式化。
- 会议时间展示统一规则：
  - 主展示：会议业务时区（`meeting.timezone`）的时间；
  - 次展示：当用户时区与会议时区不一致时，补充“用户时区时间”；
  - 格式统一：`YYYY-MM-DD HH:mm (IANA, TZ_SHORT)`，禁止混用 AM/PM 与裸 UTC。

---

## 🧭 人类阅读区（可选）

### 视觉与交互说明

- **兼容旧系统视觉**：允许保留原系统非 Lark 视觉以减少改动。
- **访客签到页**：公开访问，禁止依赖登录态；仅允许参会名单内用户签到。
- **二维码**：线上/线下二维码分开展示。
- **会议详情同步提示**：若单次会议已进入 `LOCKED_BY_LOCAL_EDIT`，详情页顶部显示黄色提示条，文案为“该会议已进行本地修改，已停止从 Outlook 自动同步”，并附锁定时间、操作人与原因。
- **用户管理**：用户新增/导入/删除由组织架构模块处理；本页提供跳转入口。
- **同步历史详情**：同步历史默认先展示可读摘要（如 `attendees 66 -> 56`、`internal matched 64 -> 54`）；用户显式展开后再显示邮箱级增删明细与 `rawPayload`，目的是降低时间线噪音和接口负载，不做额外角色权限限制。

### 签到方式校验交互细则（v1.2）

- **组织架构用户管理页**（由组织架构模块负责）：
  - 用户列表/详情新增"工作地"输入框，带自动补全（数据源 `GET /meeting-attendance/cities/suggestions`）
  - 页头新增"批量导入工作地"按钮，走组织架构模块接口 `POST /organization/users/import-work-city`
  - **Excel 导入预览弹窗**（v1.2 新增）：
    - 两列 Excel（`Email / 工作地`）上传后弹预览（不落库）
    - 分三类展示：🟢 已存在、🟡 相似警告（编辑距离 ≤ 2 且长度差 ≤ 2，例 `Los Angles → Los Angeles`）、🔵 全新
    - 🟡 每条让管理员选"用已有 / 保留本次填写"
    - 🔵 每条让管理员确认是否新增
    - 底部"确定导入"按钮，带管理员选择提交落库
    - 不匹配邮箱展示在预览底部列表
- **会议详情页（`/meetingattendance/meetings/[id]/`）**：
  - 会议信息区展示"会议地点"（只读）+ "签到方式校验"开关（管理员可切换，开启前后端校验 city）
  - 参会人列表新增一列"允许签到方式"：
    - 来源 = 城市派生 → 灰色 outline tag（`线下` / `线上`）
    - 来源 = 系列级覆盖 → 紫色 tag（`线上 · 系列默认`，hover 提示"此系列默认线上"）
    - 来源 = 会议级覆盖 → 蓝色实心 tag（`线下 ← 线上`），hover tooltip "原默认：X / 改为：Y / 操作人 · 时间"
    - 用户 workCity 未配置 = 红色警告 tag（`未设置 ⚠️`）
    - 开关 off = 不展示 tag 列
  - 点 tag 弹出操作菜单：`改为线下 / 改为线上 / 恢复默认`（清空会议级覆盖，回到系列级/城市派生）
  - 参会人列表上方筛选 chips 新增一组"允许方式"：`允许线下 / 允许线上 / 未设置 / 已调整`
  - **不**提供"Excel 导入会议白名单"入口
- **系列管理页（`/meetingattendance/series/`）**：
  - 系列表单加"系列地点"输入（自动补全）+ "签到方式校验"开关
  - 保存时后端级联刷新下属 meeting 的 `city` + `enforceCheckinMode`，前端提示"将同步刷新该系列下 N 场会议"
  - "管理默认参会人"弹窗改造（用户截图已有，v1.2 扩展）：
    - 顶部新增**搜索框**（按姓名/邮箱模糊）
    - 每行新增**签到方式下拉**：`线下 / 线上 / 跟随工作地`
    - 新增**批量设置**按钮：选中多行后统一设置签到方式
- **会议创建/编辑页（`/meetingattendance/meetings/create`）**：
  - 表单新增"会议地点"输入（自动补全）+ "签到方式校验"开关（schema 默认 false，页面默认也 false）
  - 绑定系列时从 series 继承两字段，编辑框 disabled + 提示"跟随系列"
- **签到页**（`/meetingattendance/checkin/guest`）：
  - `MEETING_ATTENDANCE_033` → "请使用线下/线上专属二维码"
  - `MEETING_ATTENDANCE_034` → "请联系管理员配置您的工作地"
  - `MEETING_ATTENDANCE_036` → "会议未配置地点"（边缘情况）
- **二维码页（`/meetingattendance/meetings/[id]/qr`）**：**不改动**（撤销 v1.1 的文字提示）
- **报表页（`/meetingattendance/reports/`）**：
  - 单场报表顶部新增 1 张卡片"签到方式校验：开/关"（与现有 3 张卡片并列）
  - 单场"个人出勤详情表"（现 5 列）末尾新增 1 列"允许方式"（tag，含 3 种来源）
  - 系列报表顶部新增 1 张卡片"签到方式校验：开/关"（与现有 4 张卡片并列）
  - 系列"低出勤率排名"和"个人出勤详情"两个表（现 14 列）末尾各新增 2 列："允许方式"、"调整次数"（只统计会议级临时调整；系列级默认不计）
  - CSV 导出对应列同步新增

### 流程图（示例：访客签到）

```mermaid
flowchart TD
  A[扫码进入访客页] --> B[输入姓名/邮箱]
  B --> C[校验参会名单（系统用户）]
  C -->|通过| D[写入出勤记录]
  C -->|失败| E[提示不在名单]
```

---

## 议程能力页面与组件（v1.x）

### 页面：议程编辑页

**基本信息**

| 项 | 内容 |
|---|---|
| 路由 | `/meetingattendance/[meetingId]/agenda/edit` |
| 权限 | `meeting:agenda:update`（MeetingManager / Administrator / 会议 creator） |
| 入口 | 会议详情页「编辑议程」按钮（仅有权用户可见） |

**页面元素清单**

| 元素 | 类型 | 说明 |
|---|---|---|
| 顶部栏 | 区域 | 会议标题 + 「保存」按钮（实时保存模式下作为状态指示器）+ 「返回」按钮 |
| 议程 tree（左列） | 嵌套列表 | 段 → 项；段可拖、项可拖；段折叠/展开 |
| 段卡片 | 卡片 | 段标题 + 项数 badge + 拖手柄 + 「加项」/「删段」操作 |
| 项卡片 | 卡片 | 标题、描述 prefix、`time`、`presenter` avatar、`category` badge、「上传任务」徽章、`编辑 / 删除 / 分配任务` 操作按钮 |
| 项编辑表单（右列） | 表单 | 选中项后编辑：`title` / `description` / `timeMinutes` / `presenter`（user picker）/ `categoryTag`（select） |
| 分配任务弹窗 | 弹窗 | 用户多选 + `dueAt` 日期选择（可空）+ 确认 / 取消 |
| 删除确认弹窗 | 弹窗 | 警告文案：「将连带删除该项的 N 个附件 + M 个待办任务，不可恢复」 |

**字段定义**

| 字段 | 类型 | 必填 | 校验 |
|---|---|---|---|
| section.title | string | 是 | 1–200 字符（与 schema 对齐） |
| item.title | string | 是 | 1–200 字符 |
| item.description | string | 否 | 建议 ≤ 2000 字符（后端 DTO `@MaxLength(2000)`） |
| item.code | string | 否 | ≤ 64 字符；用户自填业务编号；不 unique |
| item.timeMinutes | int | 否 | 如填则必须 > 0；累计可超会议时长（仅警告） |
| item.presenterUserId | userId | 否 | 如填则必须为有效系统用户 |
| item.categoryTag | enum | 否 | 4 选项之一：`FFAI_EAI_EV` / `EAI_ROBOTICS` / `MIXED` / `OTHER` |

**交互行为**

| 触发 | 行为 |
|---|---|
| 拖段 / 项 | 调用 reorder 接口；失败回滚顺序并 toast |
| 编辑字段失焦 | 自动 PATCH（实时保存）|
| 「加段」/「加项」 | 创建空白记录，自动聚焦到 title 输入框 |
| 「删项」 | 弹删除确认弹窗（含「30 天内可联系 admin 恢复」提示）→ 确认后软删（deletedAt 标记）+ 议程 tree 刷新 |
| 「分配任务」 | 弹用户多选弹窗 → 提交后议程项徽章刷新 |

**错误状态**

| 错误码 | 表现 |
|---|---|
| AGENDA_FORBIDDEN_NOT_MANAGER_OR_CREATOR | 全页面拦截，跳回详情页并 toast |
| 字段校验失败 | 表单字段下方红字提示 + 阻止保存 |
| 网络失败 | 操作 toast 报错 + 重试按钮 |

---

### 页面：我的待办页

**基本信息**

| 项 | 内容 |
|---|---|
| 路由 | `/meetingattendance/my-tasks` |
| 权限 | 已登录用户即可访问 |
| 入口 | 顶部菜单「我的待办」/ 用户菜单 |

**页面元素清单**

| 元素 | 类型 | 说明 |
|---|---|---|
| 筛选条 | 顶部工具栏 | 状态筛选：`PENDING / UPLOADED / CANCELLED / ALL`（CANCELLED 任务在 30 天内仍可见，灰色标记）；排序：`dueAt 升序 / assignedAt 降序`（对应 API query `sort=dueAt_asc | assignedAt_desc`） |
| 任务卡片 | 卡片列表 | 每条任务一张卡 |
| 空态 | 占位 | 「暂无待办上传任务」+ 插画 |

**任务卡字段定义（从 API 响应映射）**

| UI 标签 | API 响应字段路径 |
|---|---|
| 会议标题 | `agendaItem.meeting.title` |
| 议程项标题 | `agendaItem.title` |
| 段标题 | `agendaItem.sectionTitle` |
| 截止时间 | `dueAt`（可空，空时显示「无截止」） |
| 分配人 | `assignedBy.displayName` |
| status badge | `status`：`PENDING`（黄）/ `UPLOADED`（绿）/ `CANCELLED`（灰，30 天软删窗口内仍展示） |
| 「去上传」按钮 | 跳转 `/meetingattendance/meetings/{agendaItem.meeting.id}#agenda-item-{agendaItem.id}` |

**交互行为**

| 触发 | 行为 |
|---|---|
| 点卡片 / 「去上传」 | 跳到 `/meetingattendance/meetings/:meetingId#agenda-item-:itemId` |
| 切换状态筛选 | 重新拉取列表 |
| 上传完成回到该页 | 已上传任务从 PENDING 列表移除 |

**错误状态**

| 错误码 | 表现 |
|---|---|
| 列表加载失败 | 全页 toast + 重试 |
| 任务跳转目标议程项已被删除 | 跳转后 toast「议程项已删除」+ 自动刷新待办 |

---

### 段：会议详情页议程展示

> 在现有「会议详情」页（`/meetingattendance/meetings/:id`）中新增议程段。

**布局**

| 元素 | 类型 | 说明 |
|---|---|---|
| 议程段标题栏 | 区域 | 「议程」+「编辑议程」按钮（仅 MeetingManager / creator 可见）+「上传会议资料」按钮（仅 manager 可见） |
| 议程 tree（只读） | 嵌套列表 | 段 + 项，不可拖排序 |
| 项展开内容 | 折叠面板 | 议程项描述 + 附件列表 + 任务徽章 |
| 附件行 | 列表项 | 文件名（点击下载）/ 大小 / 上传人 / 上传时间 / 删除按钮（仅上传人或 manager 可见） |
| 任务徽章 | Badge | `PENDING N / UPLOADED M`，管理员可点击查看任务列表 |
| 空态 | 占位 | 议程为空：「尚无议程」+「添加议程」按钮（仅 manager 可见） |

**交互行为**

| 触发 | 行为 |
|---|---|
| 点议程项标题 | 展开 / 收起 |
| 点附件文件名 | 流式下载 |
| 点任务徽章 | 弹出任务列表查看（仅 manager） |
| 点「编辑议程」 | 跳议程编辑页 |
| 点议程项「上传资料」（被指派人才可见可操作） | 弹文件上传组件 |

**错误状态**

| 错误码 | 表现 |
|---|---|
| ATTACHMENT_NOT_FOUND | 附件行灰显并提示「附件已删除」 |
| 非参会人尝试下载 | 403 toast，返回详情 |

---

### 组件：文件上传组件（共享 component）

**基本信息**

| 项 | 内容 |
|---|---|
| 复用位置 | 议程项上传资料、会议级资料上传 |
| 形态 | 弹窗或抽屉，内嵌拖拽区域 |

**页面元素清单**

| 元素 | 类型 | 说明 |
|---|---|---|
| 拖拽区 | drop zone | 支持拖拽 + 点击选择 |
| 文件队列 | 列表 | 多文件并行上传，每行：文件名 / 大小 / 进度条 / 取消按钮 |
| 校验错误提示 | 内联 | mimeType 不允许 / 文件过大 |
| 总进度 | 进度条 | 已上传大小 / 总大小 |

**校验规则（前端先校验）**

| 项 | 规则 | 失败错误码 |
|---|---|---|
| 文件大小 | ≤ 200 MB / 个 | ATTACHMENT_TOO_LARGE |
| MIME 类型（前端） | 在 8 项白名单内：`application/pdf` / `application/vnd.openxmlformats-officedocument.wordprocessingml.document` (docx) / `application/vnd.openxmlformats-officedocument.presentationml.presentation` (pptx) / `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` (xlsx) / `image/jpeg` / `image/png` / `video/mp4` / `video/quicktime` (mov) | ATTACHMENT_MIME_NOT_ALLOWED |

> 后端额外用 magic bytes 二次校验（前端无法做），与 Content-Type 不一致 → `ATTACHMENT_MIME_MISMATCH` (415)。

**拖目录 / 多文件批量行为**

- 拖入目录 → 提示「不支持目录上传，请逐个选择文件」（不递归扁平化）
- 单次最多 10 文件，超出提示「单次最多上传 10 个文件」
- 并发数 3（同一时刻最多 3 个文件并行上传，其余排队）
- 进度条停在最后一次成功的字节比例 + toast 失败提示；用户重试整文件（**无 resume 断点续传**，重试 = 重头上传）

**交互行为**

| 触发 | 行为 |
|---|---|
| 拖入 / 点击选择 | 加入队列 → 校验 → 通过项开始上传 |
| 取消单文件 | 中断该项 + 移除 |
| 全部完成 | 自动关闭弹窗 + 父组件刷新议程项附件列表 / 任务徽章 |

**错误状态**

| 错误码 | 表现 |
|---|---|
| ATTACHMENT_TOO_LARGE | 文件行红色 + 文案「文件超过 200MB，请压缩后再上传」 |
| ATTACHMENT_MIME_NOT_ALLOWED | 文件行红色 + 文案「不支持的文件类型」 |
| ATTACHMENT_MIME_MISMATCH | 文件行红色 + 文案「文件内容与扩展名不一致，可能被篡改」 |
| UPLOAD_TASK_NOT_OWNED | 「上传资料」按钮置灰，hover 提示「该任务不属于你」 |
| 网络中断 | 当前文件行支持「重试」（重头上传，无断点续传） |

---

## 议程能力 i18n key 命名（v1.x）

> 命名风格沿用 `auth.sso.*` 嵌套 TS 对象结构，所有用户可见文案必须用 key，不允许硬编码。

| key | zh | en |
|---|---|---|
| `meeting.agenda.section.title` | 议程段标题 | Agenda Section Title |
| `meeting.agenda.section.add` | 新增议程段 | Add Section |
| `meeting.agenda.section.delete` | 删除议程段 | Delete Section |
| `meeting.agenda.item.title` | 议程项标题 | Agenda Item Title |
| `meeting.agenda.item.presenter` | 主讲人 | Presenter |
| `meeting.agenda.item.time` | 时长（分钟） | Duration (min) |
| `meeting.agenda.item.category` | 分类 | Category |
| `meeting.agenda.item.add` | 新增议程项 | Add Item |
| `meeting.agenda.item.delete` | 删除议程项 | Delete Item |
| `meeting.agenda.item.deleteWarning` | 将连带删除该项的 {n} 个附件 + {m} 个待办任务，不可恢复 | This will permanently delete {n} attachments and {m} pending tasks of this item. |
| `meeting.agenda.uploadTask.assign` | 分配上传任务 | Assign Upload Task |
| `meeting.agenda.uploadTask.status.pending` | 待上传 | Pending |
| `meeting.agenda.uploadTask.status.uploaded` | 已上传 | Uploaded |
| `meeting.agenda.uploadTask.status.cancelled` | 已取消 | Cancelled |
| `meeting.agenda.uploadTask.dueAt` | 截止时间 | Due At |
| `meeting.attachment.upload` | 上传资料 | Upload Files |
| `meeting.attachment.download` | 下载 | Download |
| `meeting.attachment.delete` | 删除附件 | Delete Attachment |
| `meeting.attachment.tooLarge` | 文件超过 200MB，请压缩后再上传 | File exceeds 200MB. Please compress and retry. |
| `meeting.attachment.mimeNotAllowed` | 不支持的文件类型 | Unsupported file type. |
| `meeting.myTasks.empty` | 暂无待办上传任务 | No pending upload tasks. |
| `meeting.myTasks.cardTitle` | 为「{meeting}」议程项「{item}」上传资料 | Upload material for {item} in {meeting} |
| `meeting.myTasks.cardDueAt` | 截止：{date} | Due: {date} |
| `meeting.myTasks.cardNoDueAt` | 无截止 | No due date |
| `meeting.agenda.editAfterEndWarning` | 该会议已结束/取消，是否仍要修改议程？ | This meeting has ended/cancelled. Are you sure you want to modify the agenda? |
| `meeting.agenda.categoryTag.ffaiEaiEv` | FFAI EAI EV | FFAI EAI EV |
| `meeting.agenda.categoryTag.eaiRobotics` | EAI Robotics | EAI Robotics |
| `meeting.agenda.categoryTag.mixed` | 混合 | Mixed |
| `meeting.agenda.categoryTag.other` | 其他 | Other |
| `meeting.agenda.uploadTask.skippedExisting` | 已有 {count} 人已分配，跳过 | {count} user(s) already assigned, skipped |

---

## 议程能力双语错误文案（v1.x）

> 错误码对外暴露由后端契约定义；前端文案统一走 i18n。所有错误均需 zh / en 双语。

| Code | zh | en |
|---|---|---|
| ATTACHMENT_TOO_LARGE | 文件超过 200MB，请压缩后再上传 | File exceeds 200MB. Please compress and retry. |
| ATTACHMENT_MIME_NOT_ALLOWED | 不支持的文件类型 | Unsupported file type. |
| ATTACHMENT_MIME_MISMATCH | 文件内容与扩展名不一致，可能被篡改 | File content does not match its declared type. |
| UPLOAD_TASK_NOT_OWNED | 该上传任务不属于你 | This upload task is not assigned to you. |
| UPLOAD_TASK_ALREADY_UPLOADED | 该任务已上传完成，无法重复操作 | This task has already been uploaded; cannot repeat. |
| AGENDA_FORBIDDEN_NOT_MANAGER_OR_CREATOR | 仅会议管理员可编辑议程 | Only meeting managers can edit agenda. |
| ATTACHMENT_DELETE_FORBIDDEN | 仅本人或管理员可删除 | Only the uploader or a manager can delete. |
| AGENDA_ITEM_NOT_FOUND | 议程项不存在或已删除 | Agenda item not found or has been deleted. |
| ATTACHMENT_NOT_FOUND | 附件不存在或已删除 | Attachment not found or has been deleted. |
