2026-08-06-oa-drafts-inline-edit-dialog.md 12 KB

OA 草稿箱就地弹窗编辑 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 草稿箱内用与「发起审批」相同的弹窗完成草稿回显、保存与提交,不再跳转到发起页。

Architecture: 从 start/index.vue 抽出 OaStartFormDialog.vue(表单 + Kit + 流程预览 + 保存/提交);发起页与草稿箱共用。草稿箱 openForDraft(draftId) 回显;列表快捷提交失败时打开同一弹窗。

Tech Stack: Vue 3 + Element Plus + form-create;既有 @/api/oaApproval 草稿 API(无后端改动)。

Spec: docs/superpowers/specs/2026-08-06-oa-drafts-inline-edit-dialog-design.md


File map

文件 职责
web-admin/src/components/oa-approval/OaStartFormDialog.vue 新建:发起/编辑草稿共用弹窗(从 start 抽出)
web-admin/src/views/oa-approval/start/index.vue 保留模板卡片列表;弹窗改为挂载共用组件;深链仍打开弹窗
web-admin/src/views/oa-approval/drafts/index.vue 去掉跳转;挂载共用组件;编辑/提交失败走弹窗

后端与 oaApproval.ts API:不改。


Public API(OaStartFormDialog)

组件通过 defineExpose 暴露:

/** 选模板后新建发起(清空草稿态) */
function openForApp(appRow: { id: string; appName?: string; [k: string]: any }): Promise<void>

/**
 * 打开并回显草稿。
 * @param draftId 草稿 id
 * @param templatesOptional 发起页可传当前模板列表,用于补全 app 行;草稿箱可省略
 */
function openForDraft(
  draftId: string,
  templatesOptional?: Array<{ id: string; appName?: string }>
): Promise<void>

/**
 * 复制旧实例表单后打开(仅发起页深链 copyFromInstanceId 使用)。
 * 内部先 openForApp,再预填 formData。
 */
function openForAppWithCopy(
  appRow: { id: string; appName?: string; [k: string]: any },
  copyFromInstanceId: string
): Promise<void>

Events:

// 保存草稿成功(create 或 update)后关闭前触发,便于父页刷新列表
emit('saved')
// 提交成功;payload 为后端返回的实例(至少含 id)
emit('submitted', instance: { id?: string; [k: string]: any })
// 弹窗关闭(取消或保存/提交后)
emit('closed')

Props:无必填 props(可见性由组件内部 dlgVisible 控制)。

弹窗模板须与现 start/index.vue 中 el-dialog 块一致(dialog-form-layout、protocol-dialog-mask、form-create、OaFlowPreview、底栏取消/保存草稿/提交)。


Task 1: 抽出 OaStartFormDialog.vue

Files:

  • Create: web-admin/src/components/oa-approval/OaStartFormDialog.vue
  • Reference (move from): web-admin/src/views/oa-approval/start/index.vue(dialog 模板 + 相关 script,约 L87–142 与 L214–714 中弹窗相关部分)

  • [x] Step 1: 新建组件骨架

创建文件:含 <el-dialog> 模板(从 start 原样搬入)、<script setup>、defineOptions({ name: 'OaStartFormDialog' })。

将 start 中下列状态与函数迁入组件(保持行为不变):

  • 状态:dlgVisible、detailLoading、picked、formData、formRule、formOption、fapi、submitting、savingDraft、editingDraftId、needInitiatorSelect、initiatorSelectByNode、userOptions、flowSteps、flowResolved、flowPendingMessage、previewLoading、Kit sync 相关局部变量、confirmationDateManualFields
  • 函数:flowNeedsInitiatorSelect、loadUsers、loadFlowPreview、scheduleFlowPreview、onPick(内部供 openForApp 调用)、closeDialog、openDraft(改名为 openForDraft 并增加 templatesOptional 参数)、saveDraft、submit、Kit sync / restoreConfirmationDateAuto、formData 的 watch

openForApp 实现:

async function openForApp(appRow: any) {
  await onPick(appRow)
}

openForDraft 实现要点(相对现 openDraft):

async function openForDraft(draftId: string, templatesOptional?: any[]) {
  const detail = await fetchOaInstanceDraftDetail(draftId)
  const appId = String(detail?.appId || '').trim()
  if (!appId) {
    ElMessage.error('草稿缺少审批模板')
    return
  }
  const list = templatesOptional || []
  let row = list.find((t: any) => t.id === appId)
  if (!row) {
    row = { id: appId, appName: detail?.appName || '发起审批' }
  }
  await onPick(row)
  editingDraftId.value = draftId
  // … 与现 openDraft 相同的 formData / initiatorSelect / loadFlowPreview 回填
}

saveDraft / submit 成功路径:

// saveDraft finally 成功后:
emit('saved')
closeDialog()
emit('closed')

// submit 成功后:
emit('submitted', data)
closeDialog()
emit('closed')
// 注意:跳转实例详情由父页处理(start 与 drafts 都可 router.push),组件内不要写死 drafts 以外的路由假设;
// 为兼容现网,组件内可保留 refreshOaMenuBadges;跳转由父监听 submitted 完成。

推荐: 组件内 不 router.push 详情;由父页 @submitted 跳转(start / drafts 行为一致)。refreshOaMenuBadges 可在组件成功路径调用,或父页 saved/submitted 里调用——选一种并两端统一,建议父页统一刷新 badge + 列表。

  • Step 2: defineExpose + emits
const emit = defineEmits<{
  saved: []
  submitted: [instance: any]
  closed: []
}>()

defineExpose({ openForApp, openForDraft, openForAppWithCopy })

openForAppWithCopy:await openForApp(appRow) 后按现 start onMounted 中 qCopyFrom 逻辑预填 formData。

  • Step 3: 迁入样式

仅迁与弹窗相关的 .oa-start__tip(及确认日期 relink 若有);卡片列表样式留在 start。

  • Step 4: 静态检查

Run: cd web-admin && pnpm exec vue-tsc --noEmit 2>&1 | head -80
(若项目过慢可用 pnpm lint 仅针对改动文件;至少保证新组件无未解析 import。)

  • Step 5: Commit — 0dcc05dc

Task 2: 发起页改用共用弹窗

Files:

  • Modify: web-admin/src/views/oa-approval/start/index.vue

  • [x] Step 1: 替换模板中的 el-dialog

删除原 el-dialog 整块,改为:

<OaStartFormDialog
  ref="startDialogRef"
  @saved="onDialogSaved"
  @submitted="onDialogSubmitted"
/>
  • Step 2: 精简 script

保留:模板列表、分组、搜索、displayGroups、onPick 改为调用 dialog、onMounted 深链。

删除已迁入 dialog 的状态与函数。

import OaStartFormDialog from '@/components/oa-approval/OaStartFormDialog.vue'
import { refreshOaMenuBadges } from '@/utils/oa-menu-badge'

const startDialogRef = ref<InstanceType<typeof OaStartFormDialog> | null>(null)

async function onPick(row: any) {
  await startDialogRef.value?.openForApp(row)
}

function onDialogSaved() {
  void refreshOaMenuBadges()
}

function onDialogSubmitted(data: any) {
  void refreshOaMenuBadges()
  if (data?.id) {
    router.push({ path: `/system-manage/oa-approval/instances/${data.id}` })
  }
}

onMounted(async () => {
  loading.value = true
  try {
    const [tpl, groupsRes] = await Promise.all([fetchOaTemplates(), fetchOaGroupList()])
    templates.value = tpl?.list || []
    groupOptions.value = groupsRes?.list || []
    const qDraftId = String(route.query.draftId || '').trim()
    const qAppId = String(route.query.appId || '').trim()
    const qCopyFrom = String(route.query.copyFromInstanceId || '').trim()
    if (qDraftId) {
      try {
        await startDialogRef.value?.openForDraft(qDraftId, templates.value)
      } catch {
        ElMessage.error('无法打开草稿')
      }
    } else if (qAppId) {
      const row = templates.value.find((t: any) => t.id === qAppId)
      if (row) {
        if (qCopyFrom) {
          await startDialogRef.value?.openForAppWithCopy(row, qCopyFrom)
        } else {
          await startDialogRef.value?.openForApp(row)
        }
      }
    }
  } finally {
    loading.value = false
  }
})
  • Step 3: 手工冒烟(发起页) — 待本地手测
  1. 打开 /#/system-manage/oa-approval/start,选模板 → 弹窗与改造前一致。
  2. 暂存草稿 → 关闭 → 草稿箱可见。
  3. 深链带 draftId 仍能打开回显(兼容)。
  • Step 4: Commit — cf009de5
git add web-admin/src/views/oa-approval/start/index.vue
git commit -m "$(cat <<'EOF'
refactor(web-admin): wire start page to OaStartFormDialog.

EOF
)"

Task 3: 草稿箱就地编辑 / 提交

Files:

  • Modify: web-admin/src/views/oa-approval/drafts/index.vue

  • [x] Step 1: 模板挂载弹窗

在 </section> 后、</div> 前增加:

<OaStartFormDialog
  ref="draftDialogRef"
  @saved="onDraftDialogSaved"
  @submitted="onDraftDialogSubmitted"
/>
  • Step 2: 改 script
import OaStartFormDialog from '@/components/oa-approval/OaStartFormDialog.vue'

const draftDialogRef = ref<InstanceType<typeof OaStartFormDialog> | null>(null)

async function openEdit(row: any) {
  if (!row?.id) return
  try {
    await draftDialogRef.value?.openForDraft(row.id)
  } catch {
    ElMessage.error('无法打开草稿')
  }
}

function onDraftDialogSaved() {
  void loadList()
  void refreshOaMenuBadges()
}

function onDraftDialogSubmitted(data: any) {
  void loadList()
  void refreshOaMenuBadges()
  if (data?.id) {
    router.push({ path: `/system-manage/oa-approval/instances/${data.id}` })
  }
}

async function handleSubmit(row: any) {
  if (!row?.id) return
  try {
    await ElMessageBox.confirm('确认提交该草稿并发起审批吗?', '提交审批', {
      type: 'warning',
      confirmButtonText: '提交',
      cancelButtonText: '取消'
    })
  } catch {
    return
  }
  submittingId.value = row.id
  try {
    const data: any = await fetchSubmitOaInstanceDraft(row.id)
    await loadList()
    void refreshOaMenuBadges()
    if (data?.id) {
      router.push({ path: `/system-manage/oa-approval/instances/${data.id}` })
    }
  } catch {
    // 快捷提交失败:打开同一弹窗回显,不跳发起页
    await openEdit(row)
  } finally {
    submittingId.value = ''
  }
}

删除 openEdit 中的 router.push 到 start。若不再需要仅因跳转而引入的依赖则清理;useRouter 仍用于提交成功跳详情。

补充:import { ElMessage } from 'element-plus'(若打开失败提示需要;项目若 auto-import 则勿重复)。

  • Step 3: 手工验收(对照 spec §7) — 待本地手测
  1. 草稿箱「编辑」:URL 仍为 drafts;弹窗回显。
  2. 弹窗「保存草稿」:列表刷新,不跳转。
  3. 弹窗「提交」:草稿消失;可进详情。
  4. 列表「提交审批」失败:打开弹窗而非跳 start。
  5. 发起页选模板发起仍正常。
  • Step 4: Commit — 3cf2f68b
git add web-admin/src/views/oa-approval/drafts/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): edit and submit OA drafts in-place via shared dialog.

EOF
)"
  • Step 5: 更新知识图谱 — e93edc77
./scripts/update-graph.sh frontend

若有图谱文件变更一并 commit:chore: update graphify after OaStartFormDialog.


Spec coverage check

Spec 要求 Task
编辑不跳转、弹窗回显 Task 3
弹窗与发起审批一致(共用组件) Task 1–2
保存刷新列表 Task 3 @saved
提交跳详情 Task 3 @submitted
快捷提交失败开弹窗 Task 3 handleSubmit catch
发起页行为等价 + 深链 Task 2
无后端改动 全计划遵守

Placeholder scan

无 TBD/TODO;API 与文件路径已写死。


Execution handoff

Plan complete and saved to docs/superpowers/plans/2026-08-06-oa-drafts-inline-edit-dialog.md.

Two execution options:

  1. Subagent-Driven (recommended) — 每任务派生子代理,任务间复查
  2. Inline Execution — 本会话按 executing-plans 连续执行并设检查点

Which approach?