12 KiB
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 中弹窗相关部分) -
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 -
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: 手工冒烟(发起页) — 待本地手测
- 打开
/#/system-manage/oa-approval/start,选模板 → 弹窗与改造前一致。 - 暂存草稿 → 关闭 → 草稿箱可见。
- 深链带
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 -
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) — 待本地手测
- 草稿箱「编辑」:URL 仍为 drafts;弹窗回显。
- 弹窗「保存草稿」:列表刷新,不跳转。
- 弹窗「提交」:草稿消失;可进详情。
- 列表「提交审批」失败:打开弹窗而非跳 start。
- 发起页选模板发起仍正常。
- 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:
- Subagent-Driven (recommended) — 每任务派生子代理,任务间复查
- Inline Execution — 本会话按 executing-plans 连续执行并设检查点
Which approach?