Compare commits

...
2 Commits
2 changed files with 94 additions and 13 deletions
@@ -58,14 +58,14 @@
<!-- 空状态占位提示 --> <!-- 空状态占位提示 -->
<div v-if="objectives.length === 0" class="layer-empty"> <div v-if="objectives.length === 0" class="layer-empty">
<el-empty description="暂无目标" :image-size="60"> <el-empty description="暂无目标" :image-size="60">
<el-button text type="primary" size="small" @click="$emit('add-objective', layerKey)"> <el-button type="primary" size="small" @click="$emit('add-objective', layerKey)">
点击 + 添加目标 点击 + 添加目标
</el-button> </el-button>
</el-empty> </el-empty>
</div> </div>
<!-- 底部添加按钮 --> <!-- 底部添加按钮 -->
<el-button text type="primary" size="small" class="layer-add-btn" @click="$emit('add-objective', layerKey)"> <el-button type="primary" size="small" class="layer-add-btn" @click="$emit('add-objective', layerKey)">
+ 添加目标 + 添加目标
</el-button> </el-button>
</div> </div>
+92 -11
View File
@@ -77,7 +77,7 @@
:all-kpis="allKpis" :all-kpis="allKpis"
:get-node-level="getNodeLevel" :get-node-level="getNodeLevel"
:get-node-progress="getNodeProgress" :get-node-progress="getNodeProgress"
@add-objective="(k: string) => openAddDialog(k)" @add-objective="openAddDialog"
@edit-objective="(p: any) => openEditDialog(p.dimKey, p.idx, p.obj)" @edit-objective="(p: any) => openEditDialog(p.dimKey, p.idx, p.obj)"
@delete-objective="(p: any) => deleteNode(p.dimKey, p.idx)" @delete-objective="(p: any) => deleteNode(p.dimKey, p.idx)"
@start-link="(p: any) => { const ps = p.key.split('-'); startLink(ps[0], parseInt(ps[ps.length-1]), p.obj) }" @start-link="(p: any) => { const ps = p.key.split('-'); startLink(ps[0], parseInt(ps[ps.length-1]), p.obj) }"
@@ -112,17 +112,56 @@
@delete-connection="onDeleteConnection" @delete-connection="onDeleteConnection"
/> />
<!-- 节点编辑弹窗 --> <!-- 节点编辑弹窗(内联,v-show替代v-if -->
<NodeEditDialog <div v-show="showNodeDialog" class="mc-dialog-overlay-fix" @click.self="showNodeDialog=false">
:visible="showNodeDialog" <div class="mc-dialog-box">
:form="editingNodeData || { name: '', description: '', icon: 'target', kpis: [] }" <div class="mc-dialog-header">
:dim-key="editingLayerKey" <span>{{ editingNodeData?._index != null ? '编辑目标' : '添加目标' }}</span>
@close="showNodeDialog = false" <button class="mc-dialog-close" @click="showNodeDialog=false">×</button>
@save="onNodeSave" </div>
/> <div class="mc-dialog-body">
<div class="mc-form-item">
<label>目标名称 <span class="mc-required">*</span></label>
<input v-model="dialogForm.name" class="mc-input" placeholder="请输入目标名称" ref="dialogNameRef" />
</div>
<div class="mc-form-item">
<label>描述</label>
<textarea v-model="dialogForm.description" class="mc-input mc-textarea" rows="3" placeholder="目标描述可选"></textarea>
</div>
<div class="mc-form-item">
<label>图标</label>
<div class="mc-icon-picker">
<button v-for="(ico, key) in iconOptions" :key="key"
class="mc-icon-btn" :class="{ active: dialogForm.icon === key }"
@click="dialogForm.icon = key" :title="ico">{{ iconEmoji(key) }}</button>
</div>
</div>
<div class="mc-form-item">
<label>目标值 <span class="mc-hint">CMA字段)</span></label>
<input v-model="dialogForm.targetValue" class="mc-input" type="number" placeholder="目标值" />
</div>
<div class="mc-row">
<input v-model="dialogForm.currentValue" class="mc-input mc-input-half" type="number" placeholder="当前值" />
<input v-model="dialogForm.unit" class="mc-input mc-input-half" placeholder="单位%万元" />
</div>
<div class="mc-row">
<input v-model="dialogForm.owner" class="mc-input" placeholder="责任人" />
</div>
<div class="mc-form-item">
<label>指标类型</label>
<label class="mc-radio-label"><input type="radio" v-model="dialogForm.isLeading" :value="false" /> 滞后指标(结果)</label>
<label class="mc-radio-label"><input type="radio" v-model="dialogForm.isLeading" :value="true" /> 领先指标(驱动)</label>
</div>
</div>
<div class="mc-dialog-footer">
<button class="mc-btn" @click="showNodeDialog=false">取消</button>
<button class="mc-btn mc-btn-primary" @click="onSaveDialog">保存</button>
</div>
</div>
</div>
<!-- 版本历史弹窗 --> <!-- 版本历史弹窗 -->
<div v-if="showVersions" class="mc-dialog-overlay" @click.self="showVersions=false"> <div v-show="showVersions" class="mc-dialog-overlay" @click.self="showVersions=false">
<div class="mc-dialog-box" style="width:620px;"> <div class="mc-dialog-box" style="width:620px;">
<div class="mc-dialog-header"> <div class="mc-dialog-header">
<span>版本历史</span> <span>版本历史</span>
@@ -234,7 +273,6 @@ import KnowledgePanel from '../components/KnowledgePanel.vue'
import api from "../api/index" import api from "../api/index"
import { LAYER_CONFIG, LAYER_KEYS, getLayerList } from "../config/layers" import { LAYER_CONFIG, LAYER_KEYS, getLayerList } from "../config/layers"
import StrategyLayer from "../components/strategy-map/StrategyLayer.vue" import StrategyLayer from "../components/strategy-map/StrategyLayer.vue"
import NodeEditDialog from "../components/strategy-map/NodeEditDialog.vue"
import ConnectionLines from "../components/strategy/ConnectionLines.vue" import ConnectionLines from "../components/strategy/ConnectionLines.vue"
// ── 四层泳道配置 ── // ── 四层泳道配置 ──
@@ -278,6 +316,18 @@ const kpiProgressMap = reactive<Record<string, KpiProgressItem>>({})
const showNodeDialog = ref(false) const showNodeDialog = ref(false)
const editingNodeData = ref<any>(null) const editingNodeData = ref<any>(null)
const editingLayerKey = ref('') const editingLayerKey = ref('')
const dialogRefreshKey = ref(0)
const dialogForm = reactive({ name: '', description: '', icon: 'target', targetValue: null, currentValue: null, unit: '%', owner: '', isLeading: false, kpis: [] as string[] })
const iconOptions: Record<string, string> = { target: '目标', star: '星级', rocket: '火箭', chart: '图表', team: '团队', light: '灯泡', shield: '盾牌', gear: '齿轮', handshake: '握手', medal: '奖牌' }
function iconEmoji(key: string): string {
const m: Record<string, string> = { target: '🎯', star: '⭐', rocket: '🚀', chart: '📊', team: '👥', light: '💡', shield: '🛡️', gear: '⚙️', handshake: '🤝', medal: '🏅' }
return m[key] || '📌'
}
function onSaveDialog() {
if (!dialogForm.name?.trim()) { ElMessage.warning('请输入目标名称'); return }
onNodeSave({ ...dialogForm, name: dialogForm.name.trim(), layer: editingLayerKey.value })
showNodeDialog.value = false
}
const showVersions = ref(false) const showVersions = ref(false)
const versions = ref<any[]>([]) const versions = ref<any[]>([])
@@ -559,6 +609,16 @@ function onDeleteConnection(idx: number) {
function openAddDialog(layerKey: string) { function openAddDialog(layerKey: string) {
editingLayerKey.value = layerKey editingLayerKey.value = layerKey
editingNodeData.value = null editingNodeData.value = null
dialogForm.name = ''
dialogForm.description = ''
dialogForm.icon = 'target'
dialogForm.targetValue = null
dialogForm.currentValue = null
dialogForm.unit = '%'
dialogForm.owner = ''
dialogForm.isLeading = false
dialogForm.kpis = []
dialogRefreshKey.value++
showNodeDialog.value = true showNodeDialog.value = true
} }
@@ -990,4 +1050,25 @@ onUnmounted(() => {
.mc-btn-warn { color:#e6a23c; border-color:#e6a23c; } .mc-btn-warn { color:#e6a23c; border-color:#e6a23c; }
.mc-btn-warn:hover { background:#e6a23c; color:#fff; } .mc-btn-warn:hover { background:#e6a23c; color:#fff; }
.mc-btn:hover { opacity:0.85; } .mc-btn:hover { opacity:0.85; }
/* 内联dialog样式 */
.mc-dialog-overlay-fix {
position: fixed; top:0; left:0; right:0; bottom:0;
background:rgba(0,0,0,0.5); z-index:9999;
display:flex; align-items:center; justify-content:center;
}
.mc-form-item { margin-bottom:16px; }
.mc-form-item label { display:block; font-size:13px; color:#666; margin-bottom:6px; font-weight:500; }
.mc-required { color:#f56c6c; margin-left:2px; }
.mc-hint { font-weight:400; color:#999; font-size:12px; }
.mc-input { width:100%; padding:8px 12px; border:1px solid #dcdfe6; border-radius:6px; font-size:14px; outline:none; box-sizing:border-box; }
.mc-input:focus { border-color:#409eff; }
.mc-input-half { width:48%; }
.mc-row { display:flex; gap:4%; margin-bottom:16px; }
.mc-icon-picker { display:flex; flex-wrap:wrap; gap:6px; }
.mc-icon-btn { width:40px; height:40px; display:flex; align-items:center; justify-content:center; font-size:20px; border:2px solid #e8e8e8; border-radius:8px; background:#fff; cursor:pointer; }
.mc-icon-btn:hover { border-color:#409eff; transform:scale(1.1); }
.mc-icon-btn.active { border-color:#409eff; background:#ecf5ff; }
.mc-radio-label { display:inline-flex; align-items:center; gap:4px; margin-right:16px; font-size:13px; color:#666; cursor:pointer; }
.mc-radio-label input { margin:0; }
</style> </style>