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:
Vendored
+1
@@ -16,6 +16,7 @@ declare module 'vue' {
|
||||
DriverFactorBudget: typeof import('./src/components/DriverFactorBudget.vue')['default']
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElAside: typeof import('element-plus/es')['ElAside']
|
||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user