Files
zkxw/docs/superpowers/plans/2026-08-06-oa-drafts-inline-edit-dialog.md
T
2026-08-06 16:54:32 +08:00

390 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 暴露:
```ts
/** 选模板后新建发起(清空草稿态) */
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:
```ts
// 保存草稿成功(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` 实现:
```ts
async function openForApp(appRow: any) {
await onPick(appRow)
}
```
`openForDraft` 实现要点(相对现 `openDraft`):
```ts
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` 成功路径:
```ts
// 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 + 列表。
- [x] **Step 2: `defineExpose` + emits**
```ts
const emit = defineEmits<{
saved: []
submitted: [instance: any]
closed: []
}>()
defineExpose({ openForApp, openForDraft, openForAppWithCopy })
```
`openForAppWithCopy`:`await openForApp(appRow)` 后按现 start `onMounted` 中 `qCopyFrom` 逻辑预填 `formData`。
- [x] **Step 3: 迁入样式**
仅迁与弹窗相关的 `.oa-start__tip`(及确认日期 relink 若有);卡片列表样式留在 start。
- [x] **Step 4: 静态检查**
Run: `cd web-admin && pnpm exec vue-tsc --noEmit 2>&1 | head -80`
(若项目过慢可用 `pnpm lint` 仅针对改动文件;至少保证新组件无未解析 import。)
- [x] **Step 5: Commit** — `0dcc05dc`
---
### Task 2: 发起页改用共用弹窗
**Files:**
- Modify: `web-admin/src/views/oa-approval/start/index.vue`
- [x] **Step 1: 替换模板中的 `el-dialog`**
删除原 `el-dialog` 整块,改为:
```vue
<OaStartFormDialog
ref="startDialogRef"
@saved="onDialogSaved"
@submitted="onDialogSubmitted"
/>
```
- [x] **Step 2: 精简 script**
保留:模板列表、分组、搜索、`displayGroups`、`onPick` 改为调用 dialog、`onMounted` 深链。
删除已迁入 dialog 的状态与函数。
```ts
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
}
})
```
- [x] **Step 3: 手工冒烟(发起页)** — 待本地手测
1. 打开 `/#/system-manage/oa-approval/start`,选模板 → 弹窗与改造前一致。
2. 暂存草稿 → 关闭 → 草稿箱可见。
3. 深链带 `draftId` 仍能打开回显(兼容)。
- [x] **Step 4: Commit** — `cf009de5`
```bash
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>` 前增加:
```vue
<OaStartFormDialog
ref="draftDialogRef"
@saved="onDraftDialogSaved"
@submitted="onDraftDialogSubmitted"
/>
```
- [x] **Step 2: 改 script**
```ts
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 则勿重复)。
- [x] **Step 3: 手工验收(对照 spec §7)** — 待本地手测
1. 草稿箱「编辑」:URL 仍为 drafts;弹窗回显。
2. 弹窗「保存草稿」:列表刷新,不跳转。
3. 弹窗「提交」:草稿消失;可进详情。
4. 列表「提交审批」失败:打开弹窗而非跳 start。
5. 发起页选模板发起仍正常。
- [x] **Step 4: Commit** — `3cf2f68b`
```bash
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
)"
```
- [x] **Step 5: 更新知识图谱** — `e93edc77`
```bash
./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?