ci/woodpecker/push/main Pipeline failed
- 39+ 页删除与布局标题同名/基本同名的页面内 h1-h4(同屏三标题→两处) - 增量副标题弱化为 14px #64748B 小字:收付款计划/战略回顾/地图版本/会计科目打标 - 标题 emoji 清零,保留标题处改 Lucide 内联 SVG(bar-chart-3/video/scale/clipboard-list/alert-circle/map/history) - KPIListView 增加 hideTitle(仅展示),客户/学习看板隐藏组件内重复页头 - 不动 MainLayout、不改接口/业务逻辑、不碰 MapCanvas.vue
204 lines
8.9 KiB
Vue
204 lines
8.9 KiB
Vue
<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>
|