feat: OKR模板库改造 — 弹窗模板库联动+时间分解落库+制造业包6个

This commit is contained in:
Hermes CI Fix
2026-08-11 00:11:25 +08:00
parent 32fe3e4d50
commit 0b191d278e
7 changed files with 413 additions and 8 deletions
@@ -10,7 +10,8 @@
<div class="mc-dim-label">{{ dimensionName }}</div>
<!-- 模板列表 -->
<div class="mc-tmpl-list">
<div v-if="templateLoading" class="mc-tmpl-empty">模板库加载中...</div>
<div v-else class="mc-tmpl-list">
<div
v-for="(tmpl, idx) in filteredTemplates"
:key="idx"
@@ -20,7 +21,13 @@
>
<div class="mc-tmpl-header">
<span class="mc-tmpl-name">{{ tmpl.name }}</span>
<span v-if="selectedTemplateIdx === idx" class="mc-tmpl-check"></span>
<span class="mc-tmpl-tags">
<el-tag v-if="tmpl.source === 'industry_pack'" size="small" type="warning" style="margin-right:6px;">
{{ industryTagLabel(tmpl.industry_tag) }}
</el-tag>
<el-tag v-else-if="tmpl.source === 'user'" size="small" type="success" style="margin-right:6px;">用户</el-tag>
<span v-if="selectedTemplateIdx === idx" class="mc-tmpl-check"></span>
</span>
</div>
<div class="mc-tmpl-desc">{{ tmpl.description }}</div>
<div class="mc-tmpl-krs">
@@ -104,6 +111,7 @@
<div v-if="dialogStep === 1" class="mc-footer-buttons">
<el-button size="small" @click="onCancel">取消</el-button>
<el-button size="small" plain @click="skipTemplate"> 跳过模板自定义</el-button>
<el-button size="small" text type="primary" @click="goTemplateLibrary">📚 从模板库选择更多</el-button>
<el-button size="small" type="primary" @click="nextStep">
下一步 {{ selectedTemplateIdx !== null ? '(已选模板)' : '(直接跳过)' }}
</el-button>
@@ -230,8 +238,10 @@
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue"
import { ref, computed, watch, onMounted } from "vue"
import { useRouter } from "vue-router"
import { ElMessage } from "element-plus"
import { okrTemplateApi } from "../../api/index"
// ── Props ──
const props = defineProps<{
@@ -433,6 +443,9 @@ const DIMENSION_LABELS: Record<string, string> = {
const dialogStep = ref(1) // 1 = 模板选择, 2 = O+KR编辑
const selectedTemplateIdx = ref<number | null>(null)
const usedTemplate = ref<any | null>(null)
const backendTemplates = ref<any[]>([]) // 后端模板库(含行业包/用户自定义)
const templateLoading = ref(false)
const router = useRouter()
// 内部表单(O+KR结构)
const form = ref<{
@@ -453,13 +466,48 @@ const dimensionName = computed(() => {
const filteredTemplates = computed(() => {
const key = props.editingLayerKey || "finance"
return (CMA_TEMPLATES as any)[key] || []
const hardcoded = (CMA_TEMPLATES as any)[key] || []
// 后端模板库(14个CMA标准 + 行业包 + 用户自定义)按维度过滤,与硬编码去重合并
const fromBackend = backendTemplates.value
.filter((t: any) => t.dimension === key && t.is_active !== 0)
.map((t: any) => ({
id: t.id,
name: t.name,
description: t.description,
dimension: t.dimension,
source: t.source,
industry_tag: t.industry_tag,
preset_krs: (t.preset_krs || []).map((kr: any) => ({
name: kr.name,
target_value: kr.target_value,
weight: kr.weight,
kpi_code: kr.kpi_code || "",
})),
}))
const seen = new Set<string>()
const merged: any[] = []
for (const t of [...fromBackend, ...hardcoded]) {
if (seen.has(t.name)) continue
seen.add(t.name)
merged.push(t)
}
return merged
})
const dialogTitle = computed(() => {
return props.isEditing ? "编辑目标" : "添加目标"
})
const INDUSTRY_LABELS: Record<string, string> = {
trading: "贸易经销",
it_service: "IT服务",
manufacturing: "制造业",
general: "通用",
}
function industryTagLabel(tag?: string): string {
return (tag && INDUSTRY_LABELS[tag]) || tag || "行业包"
}
const kpiOptions = computed(() => {
return props.allKpis || []
})
@@ -518,6 +566,44 @@ const weightSum = computed(() => {
return form.value.krs.reduce((s, kr) => s + (parseInt(kr.weight) || 0), 0)
})
// ── 从后端加载模板库(失败时静默回退到硬编码模板) ──
async function loadBackendTemplates() {
templateLoading.value = true
try {
const res: any = await okrTemplateApi.list({})
const items = res?.items || []
// 后端模板需要标准化为前端结构
backendTemplates.value = items.map((t: any) => ({
id: t.id,
name: t.name,
description: t.description,
dimension: t.dimension,
source: t.source,
industry_tag: t.industry_tag,
is_active: t.is_active,
preset_krs: (t.preset_krs || []).map((kr: any) => ({
name: kr.name,
target_value: kr.target_value,
weight: kr.weight,
kpi_code: kr.kpi_code || "",
})),
}))
} catch (e) {
// 网络/权限失败:使用硬编码14个CMA模板兜底
backendTemplates.value = []
} finally {
templateLoading.value = false
}
}
onMounted(() => {
loadBackendTemplates()
})
function goTemplateLibrary() {
router.push('/okr-templates')
}
function addKr() {
if (form.value.krs.length >= 5) {
ElMessage.warning('每个目标最多5个关键结果')
@@ -576,6 +662,7 @@ function onSave() {
o_name: form.value.o_name.trim(),
o_description: form.value.o_description.trim(),
template_name: usedTemplate.value?.name || null,
template_id: usedTemplate.value?.id || null,
dimension: props.editingLayerKey,
krs: form.value.krs.map((kr, i) => ({
name: kr.name,
@@ -588,6 +675,11 @@ function onSave() {
name: form.value.o_name.trim(),
description: form.value.o_description.trim(),
})
// 模板使用次数 +1(仅后端来源的模板,静默失败不影响保存)
if (usedTemplate.value?.id) {
okrTemplateApi.incrementUse(usedTemplate.value.id).catch(() => {})
}
}
function onCancel() {