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
| 文件 | 职责 |
|---|---|
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:不改。
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、底栏取消/保存草稿/提交)。
OaStartFormDialog.vueFiles:
web-admin/src/components/oa-approval/OaStartFormDialog.vueReference (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 相关局部变量、confirmationDateManualFieldsflowNeedsInitiatorSelect、loadUsers、loadFlowPreview、scheduleFlowPreview、onPick(内部供 openForApp 调用)、closeDialog、openDraft(改名为 openForDraft 并增加 templatesOptional 参数)、saveDraft、submit、Kit sync / restoreConfirmationDateAuto、formData 的 watchopenForApp 实现:
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 + 列表。
defineExpose + emitsconst emit = defineEmits<{
saved: []
submitted: [instance: any]
closed: []
}>()
defineExpose({ openForApp, openForDraft, openForAppWithCopy })
openForAppWithCopy:await openForApp(appRow) 后按现 start onMounted 中 qCopyFrom 逻辑预填 formData。
仅迁与弹窗相关的 .oa-start__tip(及确认日期 relink 若有);卡片列表样式留在 start。
Run: cd web-admin && pnpm exec vue-tsc --noEmit 2>&1 | head -80
(若项目过慢可用 pnpm lint 仅针对改动文件;至少保证新组件无未解析 import。)
0dcc05dcFiles:
Modify: web-admin/src/views/oa-approval/start/index.vue
[x] Step 1: 替换模板中的 el-dialog
删除原 el-dialog 整块,改为:
<OaStartFormDialog
ref="startDialogRef"
@saved="onDialogSaved"
@submitted="onDialogSubmitted"
/>
保留:模板列表、分组、搜索、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
}
})
/#/system-manage/oa-approval/start,选模板 → 弹窗与改造前一致。draftId 仍能打开回显(兼容)。cf009de5git add web-admin/src/views/oa-approval/start/index.vue
git commit -m "$(cat <<'EOF'
refactor(web-admin): wire start page to OaStartFormDialog.
EOF
)"
Files:
Modify: web-admin/src/views/oa-approval/drafts/index.vue
[x] Step 1: 模板挂载弹窗
在 </section> 后、</div> 前增加:
<OaStartFormDialog
ref="draftDialogRef"
@saved="onDraftDialogSaved"
@submitted="onDraftDialogSubmitted"
/>
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 则勿重复)。
3cf2f68bgit 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
)"
e93edc77./scripts/update-graph.sh frontend
若有图谱文件变更一并 commit:chore: update graphify after OaStartFormDialog.
| Spec 要求 | Task |
|---|---|
| 编辑不跳转、弹窗回显 | Task 3 |
| 弹窗与发起审批一致(共用组件) | Task 1–2 |
| 保存刷新列表 | Task 3 @saved |
| 提交跳详情 | Task 3 @submitted |
| 快捷提交失败开弹窗 | Task 3 handleSubmit catch |
| 发起页行为等价 + 深链 | Task 2 |
| 无后端改动 | 全计划遵守 |
无 TBD/TODO;API 与文件路径已写死。
Plan complete and saved to docs/superpowers/plans/2026-08-06-oa-drafts-inline-edit-dialog.md.
Two execution options:
Which approach?