Files
cma-management/frontend/src/views/MapVersions.vue
T
wecom-fullstack f51fc72ac7
ci/woodpecker/push/main Pipeline failed
style(page-title): B 全站规范——页面内同名标题清零 + 增量副标题弱化 + 标题 emoji 换 Lucide
- 39+ 页删除与布局标题同名/基本同名的页面内 h1-h4(同屏三标题→两处)
- 增量副标题弱化为 14px #64748B 小字:收付款计划/战略回顾/地图版本/会计科目打标
- 标题 emoji 清零,保留标题处改 Lucide 内联 SVG(bar-chart-3/video/scale/clipboard-list/alert-circle/map/history)
- KPIListView 增加 hideTitle(仅展示),客户/学习看板隐藏组件内重复页头
- 不动 MainLayout、不改接口/业务逻辑、不碰 MapCanvas.vue
2026-10-18 19:33:11 +08:00

204 lines
8.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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>
<div class="mv-wrap">
<div class="mv-header">
<div class="mv-title-row">
<el-button size="small" @click="goBack">← 返回地图列表</el-button>
<span style="margin:0 0 0 12px; display:inline-flex; align-items:center; font-size:14px; font-weight:400; color:#64748B;">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="#64748B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex:none;vertical-align:-3px;"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></svg>{{ mapTitle }}
<el-tag v-if="mapInfo" size="small" style="margin-left:10px;" :type="mapInfo.status === 'published' ? 'success' : 'info'">
{{ mapInfo.status === 'published' ? '已发布' : '草稿' }}
</el-tag>
<el-tag v-if="mapInfo" size="small" type="warning" style="margin-left:6px;">当前 {{ mapInfo.version }}</el-tag>
</span>
<div style="flex:1"></div>
<el-button v-if="mapId" size="small" type="primary" plain @click="goCanvas">打开画布</el-button>
<el-button v-if="mapId" size="small" @click="load">刷新</el-button>
</div>
<div v-if="loading" class="mv-empty">版本加载中...</div>
<el-table v-else :data="versions" style="width:100%" :empty-text="'暂无版本记录 —— 发布地图后自动生成快照'">
<el-table-column prop="version" label="版本号" width="120">
<template #default="{ row }">
<span :class="{ 'mv-cur-ver': row.is_current }">{{ row.version }}</span>
<el-tag v-if="row.is_current" size="small" type="success" style="margin-left:6px;">当前</el-tag>
</template>
</el-table-column>
<el-table-column prop="comment" label="备注" min-width="180">
<template #default="{ row }">{{ row.comment || '—' }}</template>
</el-table-column>
<el-table-column label="内容摘要" width="200">
<template #default="{ row }">
<span class="mv-dim">{{ row.dimension_count ?? 0 }}维度</span>
<span class="mv-sep">·</span>
<span class="mv-dim">{{ row.objective_count ?? 0 }}目标</span>
<span class="mv-sep">·</span>
<span class="mv-dim">{{ row.kpi_count ?? 0 }}KPI</span>
<span class="mv-sep">·</span>
<span class="mv-dim">{{ row.connection_count ?? 0 }}连线</span>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180" />
<el-table-column label="操作" width="280">
<template #default="{ row }">
<el-button size="small" @click="viewDetail(row)">查看</el-button>
<el-button size="small" @click="editComment(row)">备注</el-button>
<el-button size="small" type="warning" :disabled="row.is_current" @click="rollback(row)">回滚</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 查看版本详情:只读展示该版本画布快照的维度树 + 连线 -->
<MyDialog v-model="showDetail" title="版本快照详情" :width="720">
<div v-if="detail" class="mv-detail">
<div class="mv-detail-meta">
<el-tag size="small" type="warning">{{ detail.version }}</el-tag>
<span style="margin-left:8px;color:#999;font-size:13px;">{{ detail.comment || '无备注' }} · {{ detail.created_at }}</span>
</div>
<el-collapse v-model="openDims">
<el-collapse-item v-for="dim in detail.dimensions || []" :key="dim.key || dim.name" :name="dim.key || dim.name">
<template #title>
<span class="mv-dim-title">{{ dim.icon || '🎯' }} {{ dim.name || dim.key }}({{ (dim.objectives || []).length }}目标)</span>
</template>
<div v-for="obj in dim.objectives || []" :key="obj.name" class="mv-obj">
<div class="mv-obj-name">{{ obj.icon || '▪' }} {{ obj.name }}</div>
<div v-if="(obj.kpis || []).length" class="mv-obj-kpis">KPI:{{ (obj.kpis || []).join('、') }}</div>
</div>
</el-collapse-item>
</el-collapse>
<div class="mv-conn-title">画布连线({{ (detail.canvas_data?.connections || []).length }} 条)</div>
<div v-if="(detail.canvas_data?.connections || []).length" class="mv-conns">
<el-tag v-for="(c, i) in detail.canvas_data.connections" :key="i" size="small" style="margin:0 6px 6px 0;">
{{ c.from }} → {{ c.to }}
</el-tag>
</div>
<div v-else style="color:#999;font-size:13px;">无连线</div>
</div>
</MyDialog>
<!-- 编辑备注 -->
<MyDialog v-model="showComment" title="编辑版本备注" :width="480">
<el-input v-model="commentText" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入版本备注(如:本版调整了营收目标的权重)" />
<template #footer>
<el-button size="small" @click="showComment = false">取消</el-button>
<el-button size="small" type="primary" :loading="savingComment" @click="saveComment">保存</el-button>
</template>
</MyDialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue"
import { useRoute, useRouter } from "vue-router"
import { ElMessage, ElMessageBox } from "element-plus"
import { mapApi } from "../api/index"
const route = useRoute()
const router = useRouter()
const mapId = computed(() => Number(route.params.id))
const versions = ref<any[]>([])
const mapsAll = ref<any[]>([])
const loading = ref(true)
const showDetail = ref(false)
const detail = ref<any>(null)
const openDims = ref<string[]>([])
const showComment = ref(false)
const commentText = ref("")
const savingComment = ref(false)
const editingRow = ref<any>(null)
const mapInfo = computed(() => mapsAll.value.find((m: any) => m.id === mapId.value) || null)
const mapTitle = computed(() => mapInfo.value?.title || `地图 #${mapId.value}`)
function goBack() { router.push('/maps') }
function goCanvas() { router.push('/maps/canvas/' + mapId.value) }
async function loadMapInfo() {
try {
const r: any = await mapApi.list()
mapsAll.value = r.data || []
} catch { /* 静默 */ }
}
async function load() {
loading.value = true
try {
const r: any = await mapApi.versions(mapId.value)
versions.value = Array.isArray(r) ? r : (r.data || [])
} catch (e: any) {
ElMessage.error("加载版本失败: " + (e?.response?.data?.detail || e?.message || "未知错误"))
versions.value = []
} finally {
loading.value = false
}
}
async function viewDetail(row: any) {
try {
const r: any = await mapApi.versionDetail(mapId.value, row.id)
detail.value = r.data || r
openDims.value = (detail.value?.dimensions || []).map((d: any) => d.key || d.name)
showDetail.value = true
} catch (e: any) {
ElMessage.error("加载版本详情失败: " + (e?.response?.data?.detail || e?.message || "未知错误"))
}
}
function editComment(row: any) {
editingRow.value = row
commentText.value = row.comment || ""
showComment.value = true
}
async function saveComment() {
if (!editingRow.value) return
savingComment.value = true
try {
await mapApi.versionUpdateComment(mapId.value, editingRow.value.id, commentText.value.trim())
ElMessage.success("备注已更新")
showComment.value = false
load()
} catch (e: any) {
ElMessage.error("更新失败: " + (e?.response?.data?.detail || e?.message || "未知错误"))
} finally {
savingComment.value = false
}
}
async function rollback(row: any) {
try {
await ElMessageBox.confirm(
`确定回滚到版本 ${row.version}?\n当前画布内容将自动备份为新版本,不会被覆盖丢失。`,
"回滚确认",
{ type: "warning", confirmButtonText: "确认回滚", cancelButtonText: "取消" }
)
} catch { return }
try {
const r: any = await mapApi.versionRollback(mapId.value, row.id)
ElMessage.success(r?.message || "回滚成功")
load()
loadMapInfo()
} catch (e: any) {
ElMessage.error("回滚失败: " + (e?.response?.data?.detail || e?.message || "未知错误"))
}
}
onMounted(() => { load(); loadMapInfo() })
</script>
<style scoped>
.mv-wrap { padding: 4px 2px; }
.mv-header { }
.mv-title-row { display:flex; align-items:center; margin-bottom:16px; }
.mv-empty { text-align:center; color:#999; padding:40px 0; }
.mv-cur-ver { font-weight:700; color:#409eff; }
.mv-dim { color:#666; font-size:13px; }
.mv-sep { color:#ddd; margin:0 4px; }
.mv-detail-meta { margin-bottom:12px; }
.mv-dim-title { font-weight:600; font-size:14px; }
.mv-obj { padding:8px 12px; margin-bottom:6px; background:#f8fafc; border-radius:8px; border:1px solid #eef2f7; }
.mv-obj-name { font-weight:500; color:#303133; }
.mv-obj-kpis { margin-top:4px; font-size:12px; color:#909399; }
.mv-conn-title { margin:16px 0 8px; font-weight:600; font-size:13px; color:#606266; }
</style>