多角色督办任务详情页:从权限矩阵到组件拆分的完整实现

多角色督办任务详情页:从权限矩阵到组件拆分的完整实现

一个页面,五种角色,六个状态。怎么把"谁在什么状态下能做什么"这套逻辑写成可维护的代码?本文以 Vue 3 + TypeScript 项目中的督办催办模块为例,从架构设计到关键实现逐一拆解。


一、业务难题

IPO 项目管理中有一个"督办催办"场景:高管发起一个督办任务,指定拆解人(decomposer)将任务拆成多个子任务,再由负责人(owner)执行,审核人(reviewer)审批完成,最后由结单人(closer)做最终关闭。

五种角色访问同一个详情页,但各自看到的内容和能做的操作完全不同。子任务还有自己的状态流转:

objectivec 复制代码
DRAFT → WAIT_RECEIVE → IN_PROGRESS/OVERDUE → COMPLETION_REVIEW → COMPLETED → CLOSED

状态机有 6 种状态,角色有 5 种,这意味着潜在的 if-else 组合高达 30 种。如果用最朴素的做法------在模板里堆 v-if

html 复制代码
<el-button v-if="isOwner && status === 'WAIT_RECEIVE'">接收任务</el-button>
<el-button v-if="isReviewer && status === 'COMPLETION_REVIEW'">同意</el-button>
<el-button v-if="isOwner && (status === 'IN_PROGRESS' || status === 'OVERDUE')">提交进展</el-button>
<!-- 还有十几个按钮...... -->

不出两周,这段代码就会变成没人敢碰的"意大利面条"。核心矛盾很清晰:同一页面、同一接口返回的数据、不同用户看到不同 UI------怎么设计才能让复杂度可控?


二、权限模型:用配置表解出角色 × 状态矩阵

2.1 设计思路

面对 5×6 的矩阵,与其用条件判断堆叠,不如把它抽象成声明式的配置。核心原则就一句话:把"谁在什么状态下能干什么"变成数据,而不是逻辑

2.2 角色推导:determineModes

页面加载后,接口返回任务数据(含 responsibleUserIdcloseUserIddecomposerUserIdcreator),第一步是判断"当前用户是什么角色"。

ts 复制代码
function determineModes(raw: any, isSub: boolean, currentUserId: number): DetailMode[] {
  const uid = String(currentUserId)
  const responsible = String(raw.responsibleUserId)
  const closer = String(isSub ? raw.decomposerUserId : raw.closeUserId)
  const creator = String(raw.creator)
  const modes: DetailMode[] = []

  if (isSub) {
    if (closer === uid) modes.push('reviewer')
    if (responsible === uid) modes.push('owner')
    if (creator === uid) modes.push('senior')
    return modes
  }
  // 主任务
  if (closer === uid) modes.push('closer')
  if (responsible === uid) modes.push('decomposer')
  if (creator === uid) modes.push('senior')
  return modes
}

这里有一个关键设计决策:用户可能同时匹配多个角色,返回数组而非单个值 。比如发起人同时也是负责人时,两者按钮都展示。组件消费 modes[] 而非单一 mode,避免了"只能选一个"的信息丢失。

2.3 按钮定义:BUTTON_DEF 配置表

按钮的可见性不像传统项目那样在每个组件里写 v-if,而是集中在一张配置表里:

ts 复制代码
export const BUTTON_DEF: Record<string, {
  label: string
  type: 'default' | 'primary' | 'success' | 'danger' | 'warning' | ''
  show: (mode: DetailMode, subTaskStatus?: SubTaskStatus, progress?: number) => boolean
  disabled?: (subTaskStatus?: SubTaskStatus, progress?: number) => boolean
}> = {
  submit_progress: {
    label: '提交进展', type: 'primary',
    show: (m, s) => ['decomposer', 'owner'].includes(m)
      && (s === 'IN_PROGRESS' || s === 'OVERDUE'),
  },
  approve_review: {
    label: '同意', type: 'success',
    show: (m, s) => m === 'reviewer' && s === 'COMPLETION_REVIEW',
  },
  receive: {
    label: '接收任务', type: 'primary',
    show: (m, s) => m === 'owner' && s === 'WAIT_RECEIVE',
  },
  // ... 其余 9 个按钮定义
}

每个按钮的 show 是一个纯函数,接收角色和状态,返回是否可见。新增一个角色或按钮时,只需加一条配置,不用翻遍所有组件的 v-if

2.4 按钮过滤层:useTaskButtons

有了 modes[]BUTTON_DEF,最后一步是运行时的匹配计算:

ts 复制代码
const visibleButtons = computed(() => {
  if (!modes.value.length) return []
  const info = taskDetail.value?.taskInfo
  // 终态只展示返回按钮
  if (info?.status === 'CLOSED' || info?.status === 'COMPLETED') {
    return [{ key: 'back', label: '返回', type: '', disabled: false }]
  }
  const shown = new Set<string>()
  const buttons: { key: string; label: string; type: string; disabled: boolean }[] = []
  for (const m of modes.value) {
    for (const [key, def] of Object.entries(BUTTON_DEF)) {
      if (shown.has(key)) continue
      if (def.show(m, status, info?.progress)) {
        shown.add(key)
        buttons.push({ key, label: def.label, type: def.type, disabled: def.disabled?.(...) ?? false })
      }
    }
  }
  return buttons
})

Set 去重:同一个按钮对两种角色同时可见时只出现一次。整个权限模型的流程是:

scss 复制代码
接口数据 → determineModes() → modes[] → useTaskButtons → visibleButtons[] → ActionButtons 组件渲染

这一套下来,修改权限规则的代价从"改一堆组件"变成了"改一行配置"。


三、页面骨架:把复杂页面拆成可维护的零件

3.1 组件树

详情页(TaskDetailPage.vue)是核心,组件层级为:

bash 复制代码
index.vue
├── TaskDetailPage           ← 主容器:表单 + 按钮 + 子任务
│   ├── TaskHeader           ← 标题、状态、任务编号、创建人
│   ├── DataForm × 4         ← 信息区 / 总进度 / 进度填报 / 进展说明
│   ├── SubTaskTable         ← 子任务只读列表(decomposer/closer 可见)
│   ├── FlowRecordDrawer     ← 流转记录抽屉
│   └── ActionButtons        ← 底部操作栏(根据 visibleButtons 渲染)
└── DecomposeDrawer          ← 独立于详情页的拆解抽屉(顶层管理)

DecomposeDrawer 没有嵌套在 TaskDetailPage 内部,而是提升到 index.vue 层级与详情页平级。因为它的打开/关闭生命周期完全独立于详情的刷新------拆解完成后只需触发父级刷新,抽屉本身不需要知道详情内部的表单状态。

3.2 Hooks 分层

四个 hook 分别承担不同层级的职责:

css 复制代码
useTaskDetail    ------ 数据层:fetch 详情 + 子任务、角色推导、全局错误
useTaskActions   ------ 操作层:提交进展、接收、审批、驳回等所有 API 调用
useTaskButtons   ------ 表现层:modes[] → 可见按钮列表
useDecompose     ------ 独立场景:子任务拆解抽屉的状态机(增删改查 + 保存)

分层的好处:每个 hook 可以独立阅读和修改。比如要改"驳回时弹出的输入框文案",只需动 useTaskActions,不影响数据加载或按钮渲染逻辑。

3.3 数据流

单向向下,事件向上:

  • useTaskDetail.fetchDetail() 拉数据 → 写入 taskDetail ref → 组件 watch 后调用 reSetFormData 回填表单
  • 用户点击按钮 → ActionButtons emit action key → TaskDetailPageactionMap 分发到对应 useTaskActions 方法
  • 操作成功后 await fetchDetail() 原地刷新,页面状态更新

没有全局 store,所有状态都在组合式 API 的 ref 中闭环。因为详情页的数据不需要跨页面共享。


四、三个核心交互场景

4.1 拆解子任务

拆解是最高频的操作。拆解人在 DecomposeDrawer(80% 宽度的抽屉)中编辑子任务列表。看几个关键设计点:

防重复空白行addRow 前检查是否存在标题为空的行,有则 skip 并提示,避免用户狂点新增按钮导致列表中出现 N 行空白。

ts 复制代码
const addRow = () => {
  if (subTaskList.value.some((t) => !t.title)) {
    ElMessage.warning('请先填写当前空白行')
    return
  }
  subTaskList.value.push(EMPTY_SUB_TASK())
}

编辑保护 :非 DRAFT 状态的子任务所有字段只读。isNotDraft 一行判断搞定,不必列举哪些状态能编辑:

ts 复制代码
const isNotDraft = (row: { status: string }) => row.status !== 'DRAFT'

占比校验:保存时校验三项------必填项、每行占比 > 0、合计 = 100%。在抽屉底部用颜色提示(绿色 100%、红色超出、橙色不足)。

进度输入的小数支持ProgressInput 组件使用 parseFloat + 正则过滤非数字字符:

ts 复制代码
function handleInput(val: string) {
  const num = parseFloat(val.replace(/[^\d.]/g, ''))
  if (!isNaN(num)) {
    emit('update:value', Math.min(100, Math.max(0, num)))
  }
}

4.2 提交进展

负责人(或拆解人)可以填入当前进度百分比 + 进展描述 + 附件。

验证规则:进度必须 > 0;申请完成时进度必须 = 100%。表单数据走 DataForm.getFormData() 提取,经 actionMap 分发:

ts 复制代码
submit_progress: async () => {
  if (!(await validateProgressReport())) return
  const reportData = progressReportFormRef.value?.getFormData() || {}
  const descData = descriptionFormRef.value?.getFormData() || {}
  handleSubmitProgress(
    reportData.progress || 0,
    descData.description || '',
    fromMulFile(reportData.attachments || [])
  )
}

actionMap 是一个 key→函数 的映射表,按钮点击时只 emit key 字符串,由 handleAction 统一分发。这样做的好处是:ActionButtons 组件不需要知道任何业务逻辑,它只负责"渲染按钮列表 + 回传 key"。

4.3 审核流转

审核人和结单人的操作链路类似:确认弹窗 → 调接口 → 刷新。驳回比同意多一步------需要输入理由。

ts 复制代码
const handleRejectApprove = async () => {
  const result = await ElMessageBox.prompt('请输入驳回理由', {
    inputType: 'textarea',
  }).catch(() => null)
  if (!result) return
  // 调接口驳回,传入 reason
  await rejectComplete({ taskId: info.id, reason: result.value })
  ElMessage.success('已驳回')
  await fetchDetail()
}

关键决策:所有操作完成后统一"留在当前页刷新"。之前的实现是操作后跳转到上级页面,用户如果想连续审批多个任务需要重复进入,体验割裂。改进后操作成功只刷新当前数据,用户可以立即看到最新状态并继续操作。


五、优化与打磨:从"能用"到"好用"

完成核心功能后,还有一批 P0-P2 的细节让页面更健壮:

错误重试fetchDetail 失败后页面展示 el-result error 组件,但之前没有重试入口,用户只能手动刷新浏览器。加了一个"重试"按钮,调用 refreshAll() 同步刷新详情和子任务列表。

空状态提示 :子任务列表为空时,之前只展示空表头,用户不确定是加载失败还是确实没有数据。改为 <el-empty description="暂未拆解子任务" />,同时在接口报错时展示错误结果。

无权限角色提示 :用户未匹配任何角色(modes 为空数组)时,按钮区一片空白。加了一条 <el-alert type="info">:"您不是该任务的负责人/审核人/创建人,仅可查看"------让用户知道原因而不是疑惑。

未保存确认:拆解抽屉关闭时,如果用户编辑了子任务但没有点确认,数据会丢失。改进方案是打开抽屉时保存初始快照,关闭时对比当前数据,有差异则弹确认框。

防多次提交 :所有操作的 submitLoading 统一管理,按钮在请求进行中全部禁用,避免重复提交。


六、总结:可复用的四种模式

这个模块做下来,有四个模式可以迁移到其他复杂详情页:

1. 配置驱动的权限模型

角色 × 状态的矩阵用声明式配置管理。新增角色只需加条目,不需要翻遍组件的条件分支。核心公式:determineModes() + BUTTON_DEF + useTaskButtons = 可维护的按钮控制

2. Hooks 职责分层

数据层 / 操作层 / 表现层三层分离。每层只做一件事,每个 hook 的文件不超过 160 行。对比一个 500 行的"上帝 hook",四个 100 行的小 hook 更容易阅读和修改。

3. 以"操作场景"为边界的组件拆分

DecomposeDrawer 独立于 TaskDetailPage 不是因为视觉上它是抽屉,而是因为它代表一个完整的独立业务场景------有自己的 loading、错误、数据和行为。组件的边界应该跟业务操作对齐,而不是跟视觉区域对齐。

4. 防御性设计的清单

空状态、错误重试、未保存确认、防重复提交、输入容错------这些不是"锦上添花",而是前端质量的基本功。每一项都在几十行代码内解决,但每一项的缺失都会导致用户困惑或数据丢失。


模块源码位于 src/modules/project/pages/controlTower/superviseAndUrge/,基于 Vue 3 + TypeScript + Element Plus。

php 复制代码
> 一个页面,五种角色,六个状态。怎么把"谁在什么状态下能做什么"这套逻辑写成可维护的代码?本文以 Vue 3 + TypeScript 项目中的督办催办模块为例,从架构设计到关键实现逐一拆解。

---

## 一、业务难题

IPO 项目管理中有一个"督办催办"场景:高管发起一个督办任务,指定拆解人(decomposer)将任务拆成多个子任务,再由负责人(owner)执行,审核人(reviewer)审批完成,最后由结单人(closer)做最终关闭。

五种角色访问同一个详情页,但各自看到的内容和能做的操作完全不同。子任务还有自己的状态流转:

```
DRAFT → WAIT_RECEIVE → IN_PROGRESS/OVERDUE → COMPLETION_REVIEW → COMPLETED → CLOSED
```

状态机有 6 种状态,角色有 5 种,这意味着潜在的 `if-else` 组合高达 30 种。如果用最朴素的做法------在模板里堆 `v-if`:

```html
<el-button v-if="isOwner && status === 'WAIT_RECEIVE'">接收任务</el-button>
<el-button v-if="isReviewer && status === 'COMPLETION_REVIEW'">同意</el-button>
<el-button v-if="isOwner && (status === 'IN_PROGRESS' || status === 'OVERDUE')">提交进展</el-button>
<!-- 还有十几个按钮...... -->
```

不出两周,这段代码就会变成没人敢碰的"意大利面条"。核心矛盾很清晰:**同一页面、同一接口返回的数据、不同用户看到不同 UI------怎么设计才能让复杂度可控?**

---

## 二、权限模型:用配置表解出角色 × 状态矩阵

### 2.1 设计思路

面对 5×6 的矩阵,与其用条件判断堆叠,不如把它抽象成声明式的配置。核心原则就一句话:**把"谁在什么状态下能干什么"变成数据,而不是逻辑**。

### 2.2 角色推导:determineModes

页面加载后,接口返回任务数据(含 `responsibleUserId`、`closeUserId`、`decomposerUserId`、`creator`),第一步是判断"当前用户是什么角色"。

``` ts
function determineModes(raw: any, isSub: boolean, currentUserId: number): DetailMode[] {
  const uid = String(currentUserId)
  const responsible = String(raw.responsibleUserId)
  const closer = String(isSub ? raw.decomposerUserId : raw.closeUserId)
  const creator = String(raw.creator)
  const modes: DetailMode[] = []

  if (isSub) {
    if (closer === uid) modes.push('reviewer')
    if (responsible === uid) modes.push('owner')
    if (creator === uid) modes.push('senior')
    return modes
  }
  // 主任务
  if (closer === uid) modes.push('closer')
  if (responsible === uid) modes.push('decomposer')
  if (creator === uid) modes.push('senior')
  return modes
}
```

这里有一个关键设计决策:**用户可能同时匹配多个角色,返回数组而非单个值**。比如发起人同时也是负责人时,两者按钮都展示。组件消费 `modes[]` 而非单一 `mode`,避免了"只能选一个"的信息丢失。

### 2.3 按钮定义:BUTTON_DEF 配置表

按钮的可见性不像传统项目那样在每个组件里写 `v-if`,而是集中在一张配置表里:

```ts
export const BUTTON_DEF: Record<string, {
  label: string
  type: 'default' | 'primary' | 'success' | 'danger' | 'warning' | ''
  show: (mode: DetailMode, subTaskStatus?: SubTaskStatus, progress?: number) => boolean
  disabled?: (subTaskStatus?: SubTaskStatus, progress?: number) => boolean
}> = {
  submit_progress: {
    label: '提交进展', type: 'primary',
    show: (m, s) => ['decomposer', 'owner'].includes(m)
      && (s === 'IN_PROGRESS' || s === 'OVERDUE'),
  },
  approve_review: {
    label: '同意', type: 'success',
    show: (m, s) => m === 'reviewer' && s === 'COMPLETION_REVIEW',
  },
  receive: {
    label: '接收任务', type: 'primary',
    show: (m, s) => m === 'owner' && s === 'WAIT_RECEIVE',
  },
  // ... 其余 9 个按钮定义
}
```

每个按钮的 `show` 是一个纯函数,接收角色和状态,返回是否可见。新增一个角色或按钮时,只需加一条配置,不用翻遍所有组件的 `v-if`。

### 2.4 按钮过滤层:useTaskButtons

有了 `modes[]` 和 `BUTTON_DEF`,最后一步是运行时的匹配计算:

```ts
const visibleButtons = computed(() => {
  if (!modes.value.length) return []
  const info = taskDetail.value?.taskInfo
  // 终态只展示返回按钮
  if (info?.status === 'CLOSED' || info?.status === 'COMPLETED') {
    return [{ key: 'back', label: '返回', type: '', disabled: false }]
  }
  const shown = new Set<string>()
  const buttons: { key: string; label: string; type: string; disabled: boolean }[] = []
  for (const m of modes.value) {
    for (const [key, def] of Object.entries(BUTTON_DEF)) {
      if (shown.has(key)) continue
      if (def.show(m, status, info?.progress)) {
        shown.add(key)
        buttons.push({ key, label: def.label, type: def.type, disabled: def.disabled?.(...) ?? false })
      }
    }
  }
  return buttons
})
```

用 `Set` 去重:同一个按钮对两种角色同时可见时只出现一次。整个权限模型的流程是:

```
接口数据 → determineModes() → modes[] → useTaskButtons → visibleButtons[] → ActionButtons 组件渲染
```

这一套下来,修改权限规则的代价从"改一堆组件"变成了"改一行配置"。

---

## 三、页面骨架:把复杂页面拆成可维护的零件

### 3.1 组件树

详情页(`TaskDetailPage.vue`)是核心,组件层级为:

```
index.vue
├── TaskDetailPage           ← 主容器:表单 + 按钮 + 子任务
│   ├── TaskHeader           ← 标题、状态、任务编号、创建人
│   ├── DataForm × 4         ← 信息区 / 总进度 / 进度填报 / 进展说明
│   ├── SubTaskTable         ← 子任务只读列表(decomposer/closer 可见)
│   ├── FlowRecordDrawer     ← 流转记录抽屉
│   └── ActionButtons        ← 底部操作栏(根据 visibleButtons 渲染)
└── DecomposeDrawer          ← 独立于详情页的拆解抽屉(顶层管理)
```

`DecomposeDrawer` 没有嵌套在 `TaskDetailPage` 内部,而是提升到 `index.vue` 层级与详情页平级。因为它的打开/关闭生命周期完全独立于详情的刷新------拆解完成后只需触发父级刷新,抽屉本身不需要知道详情内部的表单状态。

### 3.2 Hooks 分层

四个 hook 分别承担不同层级的职责:

```
useTaskDetail    ------ 数据层:fetch 详情 + 子任务、角色推导、全局错误
useTaskActions   ------ 操作层:提交进展、接收、审批、驳回等所有 API 调用
useTaskButtons   ------ 表现层:modes[] → 可见按钮列表
useDecompose     ------ 独立场景:子任务拆解抽屉的状态机(增删改查 + 保存)
```

分层的好处:每个 hook 可以独立阅读和修改。比如要改"驳回时弹出的输入框文案",只需动 `useTaskActions`,不影响数据加载或按钮渲染逻辑。

### 3.3 数据流

单向向下,事件向上:

- `useTaskDetail.fetchDetail()` 拉数据 → 写入 `taskDetail` ref → 组件 watch 后调用 `reSetFormData` 回填表单
- 用户点击按钮 → `ActionButtons` emit `action` key → `TaskDetailPage` 的 `actionMap` 分发到对应 `useTaskActions` 方法
- 操作成功后 `await fetchDetail()` 原地刷新,页面状态更新

没有全局 store,所有状态都在组合式 API 的 `ref` 中闭环。因为详情页的数据不需要跨页面共享。

---

## 四、三个核心交互场景

### 4.1 拆解子任务

拆解是最高频的操作。拆解人在 `DecomposeDrawer`(80% 宽度的抽屉)中编辑子任务列表。看几个关键设计点:

**防重复空白行**:`addRow` 前检查是否存在标题为空的行,有则 skip 并提示,避免用户狂点新增按钮导致列表中出现 N 行空白。

```ts
const addRow = () => {
  if (subTaskList.value.some((t) => !t.title)) {
    ElMessage.warning('请先填写当前空白行')
    return
  }
  subTaskList.value.push(EMPTY_SUB_TASK())
}
```

**编辑保护**:非 DRAFT 状态的子任务所有字段只读。`isNotDraft` 一行判断搞定,不必列举哪些状态能编辑:

```ts
const isNotDraft = (row: { status: string }) => row.status !== 'DRAFT'
```

**占比校验**:保存时校验三项------必填项、每行占比 > 0、合计 = 100%。在抽屉底部用颜色提示(绿色 100%、红色超出、橙色不足)。

**进度输入的小数支持**:`ProgressInput` 组件使用 `parseFloat` + 正则过滤非数字字符:

```ts
function handleInput(val: string) {
  const num = parseFloat(val.replace(/[^\d.]/g, ''))
  if (!isNaN(num)) {
    emit('update:value', Math.min(100, Math.max(0, num)))
  }
}
```

### 4.2 提交进展

负责人(或拆解人)可以填入当前进度百分比 + 进展描述 + 附件。

验证规则:进度必须 > 0;申请完成时进度必须 = 100%。表单数据走 `DataForm.getFormData()` 提取,经 `actionMap` 分发:

```ts
submit_progress: async () => {
  if (!(await validateProgressReport())) return
  const reportData = progressReportFormRef.value?.getFormData() || {}
  const descData = descriptionFormRef.value?.getFormData() || {}
  handleSubmitProgress(
    reportData.progress || 0,
    descData.description || '',
    fromMulFile(reportData.attachments || [])
  )
}
```

`actionMap` 是一个 key→函数 的映射表,按钮点击时只 emit key 字符串,由 `handleAction` 统一分发。这样做的好处是:`ActionButtons` 组件不需要知道任何业务逻辑,它只负责"渲染按钮列表 + 回传 key"。

### 4.3 审核流转

审核人和结单人的操作链路类似:确认弹窗 → 调接口 → 刷新。驳回比同意多一步------需要输入理由。

```ts
const handleRejectApprove = async () => {
  const result = await ElMessageBox.prompt('请输入驳回理由', {
    inputType: 'textarea',
  }).catch(() => null)
  if (!result) return
  // 调接口驳回,传入 reason
  await rejectComplete({ taskId: info.id, reason: result.value })
  ElMessage.success('已驳回')
  await fetchDetail()
}
```

**关键决策:所有操作完成后统一"留在当前页刷新"**。之前的实现是操作后跳转到上级页面,用户如果想连续审批多个任务需要重复进入,体验割裂。改进后操作成功只刷新当前数据,用户可以立即看到最新状态并继续操作。

---

## 五、优化与打磨:从"能用"到"好用"

完成核心功能后,还有一批 P0-P2 的细节让页面更健壮:

**错误重试**:`fetchDetail` 失败后页面展示 `el-result error` 组件,但之前没有重试入口,用户只能手动刷新浏览器。加了一个"重试"按钮,调用 `refreshAll()` 同步刷新详情和子任务列表。

**空状态提示**:子任务列表为空时,之前只展示空表头,用户不确定是加载失败还是确实没有数据。改为 `<el-empty description="暂未拆解子任务" />`,同时在接口报错时展示错误结果。

**无权限角色提示**:用户未匹配任何角色(`modes` 为空数组)时,按钮区一片空白。加了一条 `<el-alert type="info">`:"您不是该任务的负责人/审核人/创建人,仅可查看"------让用户知道原因而不是疑惑。

**未保存确认**:拆解抽屉关闭时,如果用户编辑了子任务但没有点确认,数据会丢失。改进方案是打开抽屉时保存初始快照,关闭时对比当前数据,有差异则弹确认框。

**防多次提交**:所有操作的 `submitLoading` 统一管理,按钮在请求进行中全部禁用,避免重复提交。

---

## 六、总结:可复用的四种模式

这个模块做下来,有四个模式可以迁移到其他复杂详情页:

**1. 配置驱动的权限模型**  
角色 × 状态的矩阵用声明式配置管理。新增角色只需加条目,不需要翻遍组件的条件分支。核心公式:`determineModes() + BUTTON_DEF + useTaskButtons = 可维护的按钮控制`。

**2. Hooks 职责分层**  
数据层 / 操作层 / 表现层三层分离。每层只做一件事,每个 hook 的文件不超过 160 行。对比一个 500 行的"上帝 hook",四个 100 行的小 hook 更容易阅读和修改。

**3. 以"操作场景"为边界的组件拆分**  
`DecomposeDrawer` 独立于 `TaskDetailPage` 不是因为视觉上它是抽屉,而是因为它代表一个完整的独立业务场景------有自己的 loading、错误、数据和行为。组件的边界应该跟业务操作对齐,而不是跟视觉区域对齐。

**4. 防御性设计的清单**  
空状态、错误重试、未保存确认、防重复提交、输入容错------这些不是"锦上添花",而是前端质量的基本功。每一项都在几十行代码内解决,但每一项的缺失都会导致用户困惑或数据丢失。

---

*模块源码位于 `src/modules/project/pages/controlTower/superviseAndUrge/`,基于 Vue 3 + TypeScript + Element Plus。*
相关推荐
英勇无比的消炎药1 小时前
拆解内核:深入分析 TinyRobot 输入区组件设计与实现原理
vue.js
段一凡-华北理工大学1 小时前
LangChain框架在高炉炼铁智能化领域的应用~系列文章09:工具调用Tool — 让AI学会操作高炉仪表盘
网络·人工智能·架构·langchain·高炉炼铁·高炉智能化·高炉智能体
codexu_4612291871 小时前
NoteGen 里一条记录如何变成 Markdown
前端·笔记·rust·tauri
meilindehuzi_a1 小时前
从 Canvas 到 Vibe Coding:HTML5 游戏开发入门与 AI 飞机大战实战
前端·人工智能·html5
Cc_Debugger1 小时前
开发环境使用https配置
javascript·vue.js·https
lichenyang4531 小时前
鸿蒙 Web 容器(一):怎么把一个 H5 页面嵌进鸿蒙页面?
前端
Upsy-Daisy1 小时前
Hermes Agent 学习笔记 10:源码结构与整体架构总结,Hermes 到底是如何运转起来的?
笔记·学习·架构
触底反弹2 小时前
🎨 通义万相实战:用 Qwen 多模态 API 实现 AI 换装换姿势,10 行代码搞定!
vue.js·人工智能
风生8482 小时前
Agent Harness 工程详解:大模型之外,决定 Agent 生死的十一个组件
架构