diff --git a/frontend/src/views/KPIList.vue b/frontend/src/views/KPIList.vue
index 7d781ea6..23f928bd 100644
--- a/frontend/src/views/KPIList.vue
+++ b/frontend/src/views/KPIList.vue
@@ -5,7 +5,7 @@
KPI字典
@@ -170,6 +170,34 @@
+
+
+
+
+
+ 📐 KPI设计引导(五步法 · 五规则)
+
+
+
+
{{ i + 1 }}
+
+
{{ s.title }}
+
{{ s.desc }}
+
+
+
+
+
+
五规则实时校验
+
+ {{ c.pass ? '✅' : '❌' }}
+ {{ c.label }}
+ {{ c.note }}
+
+
+
+
+
@@ -432,6 +460,41 @@ const editMode = ref(false)
const form = ref({})
const saving = ref(false)
+// 新建入口:重置表单 + 设计引导草稿
+function openCreate() {
+ editMode.value = false
+ form.value = {}
+ designDraft.value = {}
+ guideOpen.value = ['guide']
+ showForm.value = true
+}
+
+// ── KPI设计引导(五步法·五规则,对应 docs/kpi-design-rule.md)──
+const guideOpen = ref(['guide'])
+const guideSteps = [
+ { field: 'duty', title: '① 明确岗位职责', desc: '这个KPI考核谁的什么岗位?岗位存在的价值是什么?', placeholder: '如:销售岗——负责新客户开拓与回款' },
+ { field: 'customer', title: '② 分析客户是谁', desc: '服务对象:内部客户(上级/下游部门)+ 外部客户(终端/渠道)', placeholder: '如:外部客户(新签/续约)+ 内部(交付/财务)' },
+ { field: 'requirement', title: '③ 分析客户需求', desc: '客户对这个岗位的期望标准是什么?', placeholder: '如:快速响应、按时交付、回款可控' },
+ { field: 'output', title: '④ 确定成果输出', desc: '岗位需要产出什么可交付成果?(即本KPI,填入上方表单)', placeholder: '如:新签客户数、签约金额' },
+ { field: 'factors', title: '⑤ 五因素设计指标', desc: '时间/数量/质量/成本/风险——对应下方表单字段', placeholder: '如:数量=新签数(目标值)、时间=频率(月度)、质量=满意度(阈值)' },
+]
+const designDraft = ref>({})
+
+const designChecks = computed(() => {
+ const f = form.value || {}
+ const dimCount = kpis.value.filter((k: any) => k.dimension === f.dimension).length
+ const hasTarget = f.target_monthly != null || f.target_quarterly != null || f.target_yearly != null || f.target_value != null
+ return [
+ { label: 'KPI名称已填(SMART·具体)', pass: !!f.kpi_name },
+ { label: '已选维度(BSC四维平衡)', pass: !!f.dimension },
+ { label: '已填单位(可衡量)', pass: !!f.unit },
+ { label: '有目标值(可衡量·五因素数量)', pass: hasTarget },
+ { label: '已选频率(SMART·有时限)', pass: !!f.frequency },
+ { label: '已选负责人(责任到人)', pass: !!f.responsible_user },
+ { label: '本维度KPI数量≤6(规则①)', pass: dimCount < 6, note: dimCount ? `当前${DIM_MAP[f.dimension] || ''}维度已有${dimCount}个,建议聚焦` : '' },
+ ]
+})
+
// ── 编辑入口:列表行 → 弹窗编辑(与新建一致)──
function openEdit(row: any) {
editMode.value = true
@@ -667,6 +730,17 @@ function onDimensionChange(val: string) {
async function saveKPI() {
if (saving.value) return
+ // KPI设计规范·基础门禁(仅新建模式;五步法成果输出 + SMART可衡量)
+ if (!editMode.value) {
+ if (!form.value.kpi_name || !form.value.dimension) {
+ ElMessage.warning('请先填写 KPI名称 和 维度(KPI设计规范·五步法)')
+ return
+ }
+ if (!form.value.unit || (form.value.target_monthly == null && form.value.target_quarterly == null && form.value.target_yearly == null && form.value.target_value == null)) {
+ ElMessage.warning('请补充 单位 和 至少一个目标值(KPI设计规范·SMART可衡量)')
+ return
+ }
+ }
saving.value = true
try {
// 多粒度目标同步:按frequency将对应周期目标写入target_value(兼容评分/阈值等旧逻辑)
@@ -801,6 +875,83 @@ onMounted(() => {
display: flex;
flex-direction: column;
}
+
+/* ── KPI设计引导(五步法·五规则)── */
+.design-guide {
+ margin-bottom: 12px;
+ border: 1px solid #d9e8ff;
+ border-radius: 8px;
+ background: #f8fbff;
+ overflow: hidden;
+}
+.guide-title {
+ font-weight: 600;
+ font-size: 13px;
+ color: #1f4e9c;
+}
+.guide-steps {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 4px 8px 10px;
+}
+.guide-step {
+ display: flex;
+ gap: 10px;
+ align-items: flex-start;
+}
+.step-no {
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ background: #409EFF;
+ color: #fff;
+ font-size: 12px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ margin-top: 2px;
+}
+.step-body {
+ flex: 1;
+}
+.step-title {
+ font-size: 13px;
+ font-weight: 600;
+ color: #303133;
+}
+.step-desc {
+ font-size: 12px;
+ color: #909399;
+ margin: 2px 0 4px;
+}
+.rule-checks {
+ border-top: 1px dashed #d9e8ff;
+ padding: 10px 8px 4px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+.rule-title {
+ font-size: 13px;
+ font-weight: 600;
+ color: #1f4e9c;
+}
+.rule-check {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+ color: #303133;
+}
+.rule-check .ok { color: #67C23A; }
+.rule-check .fail { color: #F56C6C; }
+.rule-note {
+ color: #E6A23C;
+ font-size: 12px;
+}
.page-header {
display: flex;
justify-content: space-between;