feat: 复盘结果持久化 — 大PDCA Act闭环(P1, yanxue-review-persistence)

- review_records表+模型(entity_id多租户)
- API: POST/GET/GET:id /maps/{id}/review-records
- 前端: MapReview 保存复盘结论弹窗+历史记录时间线(并发agent完成主体, 本项目补齐script逻辑)
- 验证: POST 200保存成功/GET列表/跨企业404/pytest 11 passed/构建部署
This commit is contained in:
Hermes CI Fix
2026-08-27 15:59:55 +08:00
parent fdc42d443d
commit 9f787e628b
5 changed files with 251 additions and 2 deletions
+7
View File
@@ -58,6 +58,13 @@ export const mapApi = {
createWithTemplate: (data: any) => api.post('/maps/create-with-template', data),
}
// 复盘记录持久化(大PDCA Act闭环 2026-08-27
export const reviewRecordApi = {
list: (mapId: number, params?: any) => api.get(`/maps/${mapId}/review-records`, { params }),
get: (mapId: number, id: number) => api.get(`/maps/${mapId}/review-records/${id}`),
create: (mapId: number, data: any) => api.post(`/maps/${mapId}/review-records`, data),
}
export const dashboardApi = {
summary: (role: string) => api.get('/dashboard/summary', { params: { role } }),
kpis: (params: any) => api.get('/dashboard/kpis', { params }),
+118
View File
@@ -37,6 +37,17 @@
</el-tag>
</div>
<!-- 复盘结论持久化工具条大PDCA Act闭环 2026-08-27 -->
<div class="review-toolbar">
<el-button type="primary" :disabled="!selectedMap || !summary" @click="openSaveDialog">
📝 保存本次复盘结论
</el-button>
<el-button :disabled="!selectedMap" @click="showHistoryDialog = true">
📚 历史复盘记录
<el-badge v-if="reviewRecords.length > 0" :value="reviewRecords.length" type="primary" style="margin-left:6px;" />
</el-button>
</div>
<!-- 运营复盘会特殊内容 -->
<template v-if="meetingType === 'operational' && !loading && summary">
<div class="op-agenda-cards">
@@ -283,6 +294,52 @@
</div>
</div>
</el-dialog>
<!-- 保存复盘结论弹窗(大PDCA Act闭环 2026-08-27 -->
<el-dialog v-model="saveDialogVisible" title="📝 保存复盘结论" width="560">
<el-form label-width="110px">
<el-form-item label="复盘类型">
<el-select v-model="reviewForm.review_type" style="width: 200px;">
<el-option label="月度运营复盘" value="monthly" />
<el-option label="季度战略回顾" value="quarterly" />
</el-select>
</el-form-item>
<el-form-item label="复盘日期">
<el-date-picker v-model="reviewForm.review_date" type="date" value-format="YYYY-MM-DD" style="width: 200px;" />
</el-form-item>
<el-form-item label="复盘结论">
<el-input v-model="reviewForm.summary" type="textarea" :rows="3"
placeholder="为什么没达成 / 关键偏差原因 / 需要怎么调整" />
</el-form-item>
<el-form-item label="调整项(JSON)">
<el-input v-model="reviewForm.adjustments_text" type="textarea" :rows="2"
placeholder='[{"type":"kpi","target":"F_REVENUE","action":"调高目标"}]' />
</el-form-item>
<el-form-item label="下期优先级">
<el-input v-model="reviewForm.next_priorities_text" type="textarea" :rows="2"
placeholder='["聚焦营收增长", "改善现金流"]' />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="saveDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="saveReview">保存</el-button>
</template>
</el-dialog>
<!-- 历史复盘记录弹窗 -->
<el-dialog v-model="showHistoryDialog" title="📚 历史复盘记录" width="600">
<div v-if="reviewRecords.length === 0" style="color:#909399;padding:20px;text-align:center;">暂无历史复盘记录</div>
<el-timeline v-else>
<el-timeline-item v-for="rec in reviewRecords" :key="rec.id" :timestamp="rec.review_date + ' · ' + (rec.review_type === 'quarterly' ? '季度战略回顾' : '月度运营复盘')" placement="top">
<el-card shadow="never" style="border:1px solid #e2e8f0;">
<div style="font-size:13px;color:#334155;white-space:pre-wrap;">{{ rec.summary }}</div>
<div v-if="rec.next_priorities?.length" style="margin-top:8px;font-size:12px;color:#64748b;">
下期优先级:{{ rec.next_priorities.join(' / ') }}
</div>
</el-card>
</el-timeline-item>
</el-timeline>
</el-dialog>
</div>
</template>
@@ -306,6 +363,18 @@ const dimensions = ref<any[]>([])
const focusItems = ref<any[]>([])
const actionPlans = ref<any[]>([])
const showKpiDialog = ref(false)
// ── 复盘持久化(大PDCA Act闭环 2026-08-27 ──
const reviewRecords = ref<any[]>([])
const saveDialogVisible = ref(false)
const showHistoryDialog = ref(false)
const saving = ref(false)
const reviewForm = reactive({
review_type: "quarterly",
review_date: "",
summary: "",
adjustments_text: "",
next_priorities_text: "",
})
const selectedObjName = ref("")
const selectedKpis = ref<any[]>([])
@@ -335,6 +404,7 @@ async function loadReview() {
// 联动加载偏差数据
await loadDeviations()
await loadKrs()
await loadReviewRecords()
} catch (e: any) {
ElMessage.error("加载战略回顾数据失败")
} finally {
@@ -366,6 +436,54 @@ async function loadKrs() {
}
}
// ── 复盘记录加载/保存(大PDCA Act闭环 2026-08-27 ──
async function loadReviewRecords() {
if (!selectedMap.value) return
try {
const r: any = await api.get(`/maps/${selectedMap.value}/review-records`)
reviewRecords.value = r.data || []
} catch (_) {
reviewRecords.value = []
}
}
function openSaveDialog() {
reviewForm.review_type = "quarterly"
reviewForm.review_date = new Date().toISOString().slice(0, 10)
reviewForm.summary = ""
reviewForm.adjustments_text = ""
reviewForm.next_priorities_text = ""
saveDialogVisible.value = true
}
async function saveReview() {
if (!selectedMap.value || !reviewForm.summary.trim()) {
ElMessage.warning("请填写复盘结论")
return
}
saving.value = true
try {
let adjustments: any[] = []
let nextPriorities: string[] = []
try { adjustments = reviewForm.adjustments_text ? JSON.parse(reviewForm.adjustments_text) : [] } catch { ElMessage.warning("调整项JSON格式错误"); return }
try { nextPriorities = reviewForm.next_priorities_text ? JSON.parse(reviewForm.next_priorities_text) : [] } catch { ElMessage.warning("优先级JSON格式错误"); return }
await api.post(`/maps/${selectedMap.value}/review-records`, {
review_type: reviewForm.review_type,
review_date: reviewForm.review_date || new Date().toISOString().slice(0, 10),
summary: reviewForm.summary,
adjustments,
next_priorities: nextPriorities,
})
ElMessage.success("复盘结论已保存")
saveDialogVisible.value = false
await loadReviewRecords()
} catch (e: any) {
ElMessage.error("保存失败: " + (e?.message || ""))
} finally {
saving.value = false
}
}
// ── 会议类型切换 ──
function onMeetingTypeChange() {
if (selectedMap.value) {