Files
cma-management/frontend/src/components/map-canvas/MapCanvasDialogs.vue
T

799 lines
27 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!-- ==========================================================
第一步选择模板弹窗
========================================================== -->
<MyDialog :model-value="showNodeDialog" @update:model-value="$emit('update:showNodeDialog', $event)" :title="dialogTitle" :width="620">
<div v-if="dialogStep === 1" class="mc-template-step">
<div class="mc-step-hint">步骤 1/2 选择目标模板或自定义</div>
<!-- 维度名称显示 -->
<div class="mc-dim-label">{{ dimensionName }}</div>
<!-- 模板列表 -->
<div class="mc-tmpl-list">
<div
v-for="(tmpl, idx) in filteredTemplates"
:key="idx"
class="mc-tmpl-card"
:class="{ selected: selectedTemplateIdx === idx }"
@click="selectedTemplateIdx = idx"
>
<div class="mc-tmpl-header">
<span class="mc-tmpl-name">{{ tmpl.name }}</span>
<span v-if="selectedTemplateIdx === idx" class="mc-tmpl-check"></span>
</div>
<div class="mc-tmpl-desc">{{ tmpl.description }}</div>
<div class="mc-tmpl-krs">
<span v-for="(kr, ki) in tmpl.preset_krs" :key="ki" class="mc-tmpl-kr-tag">
{{ kr.name }}
</span>
</div>
</div>
</div>
<!-- 无模板提示理论上不会出现留防御 -->
<div v-if="filteredTemplates.length === 0" class="mc-tmpl-empty">
当前维度没有预设模板请直接自定义
</div>
</div>
<!-- ==========================================================
第二步编辑 O + KR
========================================================== -->
<div v-if="dialogStep === 2" class="mc-okr-step">
<div class="mc-step-hint">步骤 2/2 编辑目标与关键结果</div>
<div class="mc-form-item">
<label>🎯 目标名称 <span class="mc-required">*</span></label>
<input v-model="form.o_name" class="mc-input" placeholder="请输入目标名称" />
</div>
<div class="mc-form-item">
<label>📝 目标描述</label>
<textarea v-model="form.o_description" class="mc-input mc-textarea" rows="2" placeholder="目标描述(可选)"></textarea>
</div>
<div class="mc-kr-section">
<div class="mc-kr-header">
<span>关键结果{{ form.krs.length }}</span>
<el-button size="small" type="primary" plain @click="addKr">+ 添加KR</el-button>
</div>
<div v-for="(kr, ki) in form.krs" :key="ki" class="mc-kr-row">
<div class="mc-kr-row-top">
<span class="mc-kr-index">#{{ ki + 1 }}</span>
<input v-model="kr.name" class="mc-input mc-kr-name" placeholder="KR名称" />
<el-button v-if="form.krs.length > 1" size="small" text type="danger" @click="removeKr(ki)">×</el-button>
</div>
<div class="mc-kr-row-bottom">
<div class="mc-kr-field">
<label>目标值</label>
<input v-model="kr.target_value" class="mc-input mc-kr-input" placeholder="输入数字,回车自动加%" @keydown.enter.prevent="formatTargetValue(ki)" />
</div>
<div class="mc-kr-field">
<label>权重</label>
<select v-model="kr.weight" class="mc-input mc-kr-input" @change="checkWeightSum">
<option value="10">10%</option>
<option value="20">20%</option>
<option value="25">25%</option>
<option value="30">30%</option>
<option value="33">33%</option>
<option value="40">40%</option>
<option value="50">50%</option>
<option value="60">60%</option>
<option value="70">70%</option>
<option value="80">80%</option>
<option value="100">100%</option>
</select>
</div>
</div>
<div v-if="weightSum !== null && weightSum !== 100" class="mc-kr-warning"> 权重合计 {{ weightSum }}%应为100%</div>
</div>
</div>
</div>
<template #footer>
<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" type="primary" @click="nextStep">
下一步 {{ selectedTemplateIdx !== null ? '(已选模板)' : '(直接跳过)' }}
</el-button>
</div>
<div v-if="dialogStep === 2" class="mc-footer-buttons">
<el-button size="small" @click="prevStep">上一步</el-button>
<el-button size="small" @click="onCancel">取消</el-button>
<el-button size="small" type="primary" @click="onSave">保存</el-button>
</div>
</template>
</MyDialog>
<!-- ==========================================================
KPI 因果链推荐弹窗不变
========================================================== -->
<MyDialog :model-value="showCausalityDialog" @update:model-value="$emit('update:showCausalityDialog', $event)" title="KPI因果链推荐" :width="680">
<div style="max-height:400px;overflow-y:auto;">
<div v-if="causalityLoading" style="text-align:center;padding:20px;color:#999;">加载中...</div>
<div v-else-if="causalityRecommendations.length === 0" style="text-align:center;padding:20px;color:#999;">暂无因果链推荐</div>
<div v-else>
<div v-for="(rec,idx) in causalityRecommendations" :key="idx" style="padding:10px 0;border-bottom:1px solid #f0f0f0;display:flex;justify-content:space-between;align-items:center;">
<div>
<strong>{{ rec.source_kpi_code || rec.source_code }}</strong>
<span style="color:#999;margin:0 8px;"></span>
<strong>{{ rec.target_kpi_code || rec.target_code }}</strong>
<span style="margin-left:8px;font-size:12px;color:#666;">强度: {{ rec.strength || '-' }}</span>
</div>
<el-button size="small" @click="$emit('apply-causality', rec)">应用</el-button>
</div>
</div>
</div>
<template #footer>
<el-button size="small" @click="$emit('update:showCausalityDialog', false)">取消</el-button>
<el-button size="small" type="primary" @click="$emit('apply-all-causality')">一键全部应用</el-button>
</template>
</MyDialog>
<!-- ==========================================================
版本历史弹窗不变
========================================================== -->
<MyDialog :model-value="showVersions" @update:model-value="$emit('update:showVersions', $event)" title="版本历史" :width="620">
<div style="max-height:400px;overflow-y:auto;">
<table class="mc-table" v-if="versions.length > 0">
<thead><tr><th>说明</th><th>创建时间</th><th>操作</th></tr></thead>
<tbody>
<tr v-for="v in versions" :key="v.id">
<td>{{ v.comment || '-' }}</td>
<td>{{ v.created_at }}</td>
<td><el-button size="small" type="warning" @click="$emit('rollback-version', v.id)">回滚</el-button></td>
</tr>
</tbody>
</table>
<div v-else style="text-align:center;color:#999;padding:20px;">暂无版本记录</div>
</div>
</MyDialog>
<!-- ==========================================================
KPI 详情浮层不变
========================================================== -->
<MyDialog :model-value="showKpiDetail" @update:model-value="$emit('update:showKpiDetail', $event)" :title="'KPI详情'" :width="560">
<div style="max-height:360px;overflow-y:auto;">
<div v-if="kpiDetailList.length === 0" style="text-align:center;color:#999;padding:20px;">
该目标暂未关联KPI
</div>
<div v-for="(item, idx) in kpiDetailList" :key="idx" class="kpi-detail-card" :class="'kpi-detail-' + getKpiLevel(item)">
<div class="kpi-detail-top">
<span class="kpi-detail-badge">{{ badgeIcon(getKpiLevel(item)) }}</span>
<span class="kpi-detail-code">{{ item.code }}</span>
<span class="kpi-detail-name">{{ item.name }}</span>
</div>
<div class="kpi-detail-values">
<div class="kdv-item">
<span class="kdv-label">实际值</span>
<span class="kdv-val">{{ item.actual != null ? fmtKpiVal(item.actual) : '—' }}</span>
</div>
<div class="kdv-item">
<span class="kdv-label">目标值</span>
<span class="kdv-val">{{ item.target != null ? fmtKpiVal(item.target) : '—' }}</span>
</div>
<div v-if="item.actual != null && item.target" class="kdv-item">
<span class="kdv-label">达成率</span>
<span class="kdv-val" :style="{ color: getKpiLevel(item) === 'red' ? '#f56c6c' : getKpiLevel(item) === 'yellow' ? '#e6a23c' : '#67c23a' }">
{{ (item.actual / item.target * 100).toFixed(1) }}%
</span>
</div>
<div v-if="item.unit" class="kdv-item">
<span class="kdv-label">单位</span>
<span class="kdv-val">{{ item.unit }}</span>
</div>
</div>
</div>
</div>
<template #footer>
<el-button size="small" @click="$emit('update:showKpiDetail', false)">关闭</el-button>
<el-button size="small" type="primary" @click="$emit('edit-kpi-obj')">编辑目标</el-button>
</template>
</MyDialog>
<!-- ==========================================================
行动方案列表弹窗不变
========================================================== -->
<MyDialog :model-value="showPlanPopup" @update:model-value="$emit('update:showPlanPopup', $event)" :title="'行动方案'" :width="600">
<div style="max-height:400px;overflow-y:auto;">
<div v-if="planPopupList.length === 0" style="text-align:center;color:#999;padding:20px;">
暂无关联的行动方案
</div>
<div v-for="p in planPopupList" :key="p.id" class="plan-popup-card">
<div class="plan-popup-top">
<span class="plan-popup-status" :class="'pps-' + p.status">{{ planStatusLabel(p.status) }}</span>
<span class="plan-popup-title">{{ p.title }}</span>
</div>
<div class="plan-popup-meta">
<span v-if="p.assignee">👤 {{ p.assignee }}</span>
<span v-if="p.due_date">📅 {{ p.due_date.slice(0, 10) }}</span>
<span v-if="p.progress > 0">进度 {{ p.progress }}%</span>
</div>
</div>
</div>
<template #footer>
<el-button size="small" @click="$emit('update:showPlanPopup', false)">关闭</el-button>
<el-button size="small" type="primary" @click="$emit('quick-create-plan')">+ 新建行动方案</el-button>
</template>
</MyDialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue"
// ── Props ──
const props = defineProps<{
showNodeDialog: boolean
showCausalityDialog: boolean
showVersions: boolean
showKpiDetail: boolean
showPlanPopup: boolean
isEditing: boolean
form: any // 保持兼容
causalityLoading: boolean
causalityRecommendations: any[]
versions: any[]
kpiDetailObjName: string
kpiDetailList: any[]
planPopupObjName: string
planPopupList: any[]
editingLayerKey?: string // 当前编辑的维度key
}>()
// ── Emits ──
const emit = defineEmits<{
(e: 'update:showNodeDialog', v: boolean): void
(e: 'update:showCausalityDialog', v: boolean): void
(e: 'update:showVersions', v: boolean): void
(e: 'update:showKpiDetail', v: boolean): void
(e: 'update:showPlanPopup', v: boolean): void
(e: 'save-node-dialog', data: any): void
(e: 'apply-causality', rec: any): void
(e: 'apply-all-causality'): void
(e: 'rollback-version', id: number): void
(e: 'edit-kpi-obj'): void
(e: 'quick-create-plan'): void
}>()
// ── 14 个 CMA 标准模板(硬编码) ──
const CMA_TEMPLATES = {
finance: [
{
name: "扩大收入规模",
description: "通过新市场开拓、新产品线、新客户群实现收入增长",
dimension: "finance",
preset_krs: [
{ name: "营收增长率≥20%", target_value: "≥20%", weight: 40 },
{ name: "新客户收入占比≥15%", target_value: "≥15%", weight: 30 },
{ name: "客单价提升≥10%", target_value: "≥10%", weight: 30 },
],
},
{
name: "优化成本结构",
description: "通过成本降低和费用管控提升盈利能力",
dimension: "finance",
preset_krs: [
{ name: "毛利率提升≥3%", target_value: "≥3%", weight: 40 },
{ name: "管理费用率≤15%", target_value: "≤15%", weight: 30 },
{ name: "销售费用率≤10%", target_value: "≤10%", weight: 30 },
],
},
{
name: "提升资产效率",
description: "提高资产周转速度,释放沉淀资金",
dimension: "finance",
preset_krs: [
{ name: "库存周转天数≤45天", target_value: "≤45天", weight: 34 },
{ name: "应收账款周转天数≤45天", target_value: "≤45天", weight: 33 },
{ name: "总资产周转率≥1.0", target_value: "≥1.0", weight: 33 },
],
},
{
name: "保障现金流安全",
description: "确保经营现金流为正,防范流动性风险",
dimension: "finance",
preset_krs: [
{ name: "现金比率≥20%", target_value: "≥20%", weight: 34 },
{ name: "经营现金流为正", target_value: "为正", weight: 33 },
{ name: "自由现金流≥0", target_value: "≥0", weight: 33 },
],
},
],
customer: [
{
name: "产品领先",
description: "提供行业最优的产品质量和功能",
dimension: "customer",
preset_krs: [
{ name: "产品合格率≥99%", target_value: "≥99%", weight: 34 },
{ name: "NPS净推荐值≥60", target_value: "≥60", weight: 33 },
{ name: "市场份额提升≥2%", target_value: "≥2%", weight: 33 },
],
},
{
name: "客户亲密",
description: "建立深度客户关系,提升客户粘性",
dimension: "customer",
preset_krs: [
{ name: "客户满意度≥90%", target_value: "≥90%", weight: 34 },
{ name: "大客户留存率≥95%", target_value: "≥95%", weight: 33 },
{ name: "服务响应时间≤2小时", target_value: "≤2小时", weight: 33 },
],
},
{
name: "卓越运营",
description: "以最优的价格和最高的便利性服务客户",
dimension: "customer",
preset_krs: [
{ name: "价格竞争力行业TOP3", target_value: "TOP3", weight: 34 },
{ name: "按时交付率≥98%", target_value: "≥98%", weight: 33 },
{ name: "客户投诉率≤1%", target_value: "≤1%", weight: 33 },
],
},
],
process: [
{
name: "运营高效顺畅",
description: "优化供应链和交付流程,提升运营效率",
dimension: "process",
preset_krs: [
{ name: "交付及时率≥95%", target_value: "≥95%", weight: 34 },
{ name: "库存周转≤45天", target_value: "≤45天", weight: 33 },
{ name: "流程自动化率≥30%", target_value: "≥30%", weight: 33 },
],
},
{
name: "客户管理有序",
description: "建立标准化客户管理流程",
dimension: "process",
preset_krs: [
{ name: "新客获取成本下降≥10%", target_value: "≥10%", weight: 34 },
{ name: "客户流失率≤5%", target_value: "≤5%", weight: 33 },
{ name: "客户线索转化率≥20%", target_value: "≥20%", weight: 33 },
],
},
{
name: "创新驱动增长",
description: "通过产品和服务创新驱动业务增长",
dimension: "process",
preset_krs: [
{ name: "新产品收入占比≥15%", target_value: "≥15%", weight: 34 },
{ name: "研发投入占比≥5%", target_value: "≥5%", weight: 33 },
{ name: "年度创新提案≥12项", target_value: "≥12项", weight: 33 },
],
},
{
name: "合规稳健运营",
description: "确保合规运营,防范法律和税务风险",
dimension: "process",
preset_krs: [
{ name: "合规事件0起", target_value: "0起", weight: 34 },
{ name: "审计通过率100%", target_value: "100%", weight: 33 },
{ name: "税务申报及时率100%", target_value: "100%", weight: 33 },
],
},
],
learning: [
{
name: "团队能力升级",
description: "提升员工核心技能和关键岗位胜任度",
dimension: "learning",
preset_krs: [
{ name: "关键岗位培训完成率100%", target_value: "100%", weight: 34 },
{ name: "认证持有率≥80%", target_value: "≥80%", weight: 33 },
{ name: "人均培训时长≥40小时/年", target_value: "≥40小时/年", weight: 33 },
],
},
{
name: "数字系统赋能",
description: "通过数字化系统提升工作效率",
dimension: "learning",
preset_krs: [
{ name: "系统覆盖率≥90%", target_value: "≥90%", weight: 34 },
{ name: "数据自动化率≥70%", target_value: "≥70%", weight: 33 },
{ name: "报表自动生成率≥80%", target_value: "≥80%", weight: 33 },
],
},
{
name: "组织协同对齐",
description: "建立高绩效文化和战略共识",
dimension: "learning",
preset_krs: [
{ name: "员工满意度≥85%", target_value: "≥85%", weight: 34 },
{ name: "战略认知度≥80%", target_value: "≥80%", weight: 33 },
{ name: "跨部门协作评分≥4分", target_value: "≥4分", weight: 33 },
],
},
],
}
// ── 维度中文名映射 ──
const DIMENSION_LABELS: Record<string, string> = {
finance: "💰 财务层",
customer: "👥 客户层",
process: "⚙️ 内部流程层",
learning: "📚 学习成长层",
}
// ── 状态 ──
const dialogStep = ref(1) // 1 = 模板选择, 2 = O+KR编辑
const selectedTemplateIdx = ref<number | null>(null)
const usedTemplate = ref<any | null>(null)
// 内部表单(O+KR结构)
const form = ref<{
o_name: string
o_description: string
krs: { name: string; target_value: string; weight: string }[]
}>({
o_name: "",
o_description: "",
krs: [],
})
// ── 计算属性 ──
const dimensionName = computed(() => {
const key = props.editingLayerKey || "finance"
return DIMENSION_LABELS[key] || key
})
const filteredTemplates = computed(() => {
const key = props.editingLayerKey || "finance"
return (CMA_TEMPLATES as any)[key] || []
})
const dialogTitle = computed(() => {
return props.isEditing ? "编辑目标" : "添加目标"
})
// ── 方法 ──
function resetForm() {
dialogStep.value = 1
selectedTemplateIdx.value = null
usedTemplate.value = null
form.value = {
o_name: "",
o_description: "",
krs: [],
}
}
function skipTemplate() {
dialogStep.value = 2
selectedTemplateIdx.value = null
usedTemplate.value = null
form.value = {
o_name: "",
o_description: "",
krs: [
{ name: "", target_value: "", weight: "33" },
{ name: "", target_value: "", weight: "33" },
{ name: "", target_value: "", weight: "34" },
],
}
}
function nextStep() {
if (selectedTemplateIdx.value === null) return
const tmpl = filteredTemplates.value[selectedTemplateIdx.value]
if (!tmpl) return
usedTemplate.value = tmpl
dialogStep.value = 2
form.value = {
o_name: tmpl.name,
o_description: tmpl.description,
krs: tmpl.preset_krs.map((kr: any) => ({
name: kr.name,
target_value: kr.target_value,
weight: String(kr.weight),
})),
}
}
function prevStep() {
dialogStep.value = 1
selectedTemplateIdx.value = null
}
const weightSum = computed(() => {
return form.value.krs.reduce((s, kr) => s + (parseInt(kr.weight) || 0), 0)
})
function addKr() {
form.value.krs.push({ name: "", target_value: "", weight: "33" })
}
function removeKr(idx: number) {
form.value.krs.splice(idx, 1)
}
function formatTargetValue(idx: number) {
const kr = form.value.krs[idx]
if (!kr) return
const val = kr.target_value
if (val && !val.includes('%') && !isNaN(parseFloat(val))) {
const pct = parseFloat(val).toFixed(2) + '%'
kr.target_value = pct
// 自动同步到KR名称:如果名称是纯文字无数字,追加≥目标值
if (kr.name && !/\d/.test(kr.name)) {
kr.name = kr.name + '≥' + pct
}
}
}
function checkWeightSum() {
// 触发computed重新计算,warning自动显示
}
function onSave() {
if (!form.value.o_name?.trim()) {
// 简单提示 — 父组件会检测
return
}
// 构造完整数据发给父组件
emit("save-node-dialog", {
o_name: form.value.o_name.trim(),
o_description: form.value.o_description.trim(),
template_name: usedTemplate.value?.name || null,
dimension: props.editingLayerKey,
krs: form.value.krs.map((kr, i) => ({
name: kr.name,
target_value: kr.target_value,
weight: parseInt(kr.weight) || 33,
sort_order: i + 1,
})),
// 兼容旧字段(父组件 onNodeSave 需要 name
name: form.value.o_name.trim(),
description: form.value.o_description.trim(),
})
}
function onCancel() {
emit("update:showNodeDialog", false)
resetForm()
}
// 监听弹窗打开 → 重置状态
watch(
() => props.showNodeDialog,
(val) => {
if (!val) {
resetForm()
}
}
)
// ── 以下方法保持向后兼容(其他弹窗使用) ──
function getKpiLevel(item: any): string {
if (!item.actual || !item.target) return "gray"
const ratio = item.actual / item.target
if (ratio >= 0.9) return "green"
if (ratio >= 0.7) return "yellow"
return "red"
}
function badgeIcon(level: string): string {
return level === "green" ? "🟢" : level === "yellow" ? "🟡" : level === "red" ? "🔴" : "⚪"
}
function fmtKpiVal(v: any): string {
if (v == null) return "—"
return typeof v === "number" ? v.toFixed(2) : String(v)
}
function planStatusLabel(s: string): string {
const m: Record<string, string> = { pending: "待处理", in_progress: "进行中", completed: "已完成", cancelled: "已取消" }
return m[s] || s
}
</script>
<style scoped>
/* ── 步骤提示 ── */
.mc-step-hint {
font-size: 13px;
color: #909399;
margin-bottom: 14px;
padding-bottom: 8px;
border-bottom: 1px dashed #e4e7ed;
}
.mc-dim-label {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
/* ── 模板选择 ── */
.mc-tmpl-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 360px;
overflow-y: auto;
}
.mc-tmpl-card {
border: 1px solid #e4e7ed;
border-radius: 6px;
padding: 10px 14px;
cursor: pointer;
transition: all 0.2s;
}
.mc-tmpl-card:hover {
border-color: #409eff;
background: #ecf5ff;
}
.mc-tmpl-card.selected {
border-color: #409eff;
background: #ecf5ff;
}
.mc-tmpl-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.mc-tmpl-name {
font-weight: 600;
font-size: 14px;
color: #303133;
}
.mc-tmpl-check {
color: #409eff;
font-weight: bold;
font-size: 16px;
}
.mc-tmpl-desc {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.mc-tmpl-krs {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 6px;
}
.mc-tmpl-kr-tag {
font-size: 11px;
background: #f0f9eb;
color: #67c23a;
padding: 2px 6px;
border-radius: 3px;
}
.mc-tmpl-empty {
text-align: center;
color: #999;
padding: 30px 0;
}
/* ── O+KR 编辑 ── */
.mc-form-item {
margin-bottom: 12px;
}
.mc-form-item label {
display: block;
font-size: 13px;
font-weight: 500;
color: #606266;
margin-bottom: 4px;
}
.mc-required {
color: #f56c6c;
}
.mc-input {
width: 100%;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 6px 10px;
font-size: 13px;
outline: none;
transition: border-color 0.2s;
box-sizing: border-box;
}
.mc-input:focus {
border-color: #409eff;
}
.mc-textarea {
resize: vertical;
font-family: inherit;
}
/* ── KR 区域 ── */
.mc-kr-section {
margin-top: 8px;
}
.mc-kr-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
color: #606266;
}
.mc-kr-row {
border: 1px solid #e4e7ed;
border-radius: 6px;
padding: 10px 12px;
margin-bottom: 8px;
}
.mc-kr-row-top {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.mc-kr-index {
font-weight: 600;
color: #409eff;
font-size: 13px;
min-width: 22px;
}
.mc-kr-name {
flex: 1;
}
.mc-kr-row-bottom {
display: flex;
gap: 12px;
}
.mc-kr-field {
flex: 1;
}
.mc-kr-field label {
display: block;
font-size: 11px;
color: #909399;
margin-bottom: 2px;
}
.mc-kr-input {
font-size: 12px;
}
/* ── 底部按钮 ── */
.mc-footer-buttons {
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* ── 以下保持旧样式(其他弹窗使用) ── */
.mc-dialog-body { padding: 0; }
.mc-row { display: flex; gap: 8px; margin-bottom: 12px; }
.mc-input-half { flex: 1; }
.mc-icon-picker { display: flex; gap: 6px; }
.mc-icon-btn {
width: 32px; height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #fff;
cursor: pointer;
font-size: 16px;
}
.mc-icon-btn.active { border-color: #409eff; background: #ecf5ff; }
.mc-radio-label { margin-right: 12px; font-size: 13px; color: #606266; cursor: pointer; }
.mc-table { width: 100%; border-collapse: collapse; }
.mc-table th, .mc-table td { padding: 8px 10px; border-bottom: 1px solid #ebeef5; text-align: left; font-size: 13px; }
.mc-table th { background: #f5f7fa; font-weight: 500; color: #606266; }
/* KPI详情卡片 */
.kpi-detail-card { padding: 10px 0; border-bottom: 1px solid #f0f0f0; }
.kpi-detail-top { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.kpi-detail-badge { font-size: 14px; }
.kpi-detail-code { font-size: 12px; color: #909399; }
.kpi-detail-name { font-size: 14px; font-weight: 500; }
.kpi-detail-values { display: flex; gap: 16px; }
.kdv-item { font-size: 12px; color: #606266; }
.kdv-label { color: #909399; margin-right: 4px; }
.kdv-val { font-weight: 500; }
/* 行动方案卡片 */
.plan-popup-card { padding: 10px 0; border-bottom: 1px solid #f0f0f0; }
.plan-popup-top { display: flex; align-items: center; gap: 8px; }
.plan-popup-status { font-size: 11px; padding: 1px 6px; border-radius: 3px; }
.pps-pending { background: #fdf6ec; color: #e6a23c; }
.pps-in_progress { background: #ecf5ff; color: #409eff; }
.pps-completed { background: #f0f9eb; color: #67c23a; }
.pps-cancelled { background: #fef0f0; color: #f56c6c; }
.plan-popup-title { font-size: 13px; font-weight: 500; }
.plan-popup-meta { display: flex; gap: 12px; margin-top: 4px; font-size: 12px; color: #909399; }
</style>
.mc-kr-warning { color: #e6a23c; font-size: 12px; margin-top: 4px; }