390 lines
12 KiB
Markdown
390 lines
12 KiB
Markdown
# 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?
|