feat(strategy): P0 战略基座 — strategy_charter + GET/PUT /api/cma/charter + P组入口
ci/woodpecker/push/main Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
- 表 strategy_charter(一账套一行,entity_id 唯一):mission/vision/vision_year/vision_measurable/values_text/value_items(JSON) 全 NOT NULL
- 迁移脚本 backend/scripts/migrate_strategy_charter_20261021.py:--check/--apply/--down(回滚先 mysqldump,有数据需 --force),零 INSERT
- API:GET 无行 → filled=false 空态(禁示例数据);PUT 字段白名单 + 账套隔离三道 + vision_year 4位年份 + value_items [{name,meaning}] 结构校验
- 前端:P 组「战略章程」入口 + MyDialog 编辑(ceo/finance 可写,其余只读);战略地图页顶部只读章程条
- 用例 43 条 + auth-surface 58 条 = 101 passed;vite build rc=0
This commit is contained in:
@@ -561,4 +561,17 @@ export const arAgingApi = {
|
||||
},
|
||||
}
|
||||
|
||||
export default api
|
||||
// ── 战略基座:战略章程(P0 2026-10-21)──
|
||||
export const charterApi = {
|
||||
get: () => api.get('/charter'),
|
||||
save: (payload: {
|
||||
mission: string
|
||||
vision: string
|
||||
vision_year: string
|
||||
vision_measurable: string
|
||||
values_text: string
|
||||
value_items: Array<{ name: string; meaning: string }>
|
||||
}) => api.put('/charter', payload),
|
||||
}
|
||||
|
||||
export default api
|
||||
@@ -9,10 +9,10 @@ interface MenuItem {
|
||||
|
||||
// ── 角色路由映射 ──
|
||||
export const ROLE_ROUTES: Record<string, string[]> = {
|
||||
ceo: ['/my-dashboard', '/dashboard', '/ai-suggestions', '/kpis', '/maps', '/strategy-map', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/notifications', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/cost-intelligence', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/receivables', '/growth-quality', '/product-matrix', '/tax-compliance', '/data-classification', '/cashflow-structure', '/treasury-dashboard', '/treasury-payment'],
|
||||
finance: ['/my-dashboard', '/dashboard', '/ai-suggestions', '/kpis', '/maps', '/strategy-map', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/data', '/budget', '/deviations', '/cost', '/cost-intelligence', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/receivables', '/growth-quality', '/product-matrix', '/tax-compliance', '/data-classification', '/cashflow-structure', '/treasury-dashboard', '/treasury-payment'],
|
||||
business: ['/my-dashboard', '/dashboard', '/ai-suggestions', '/kpis', '/strategy-map', '/alerts', '/budget', '/deviations', '/action-plans', '/knowledge', '/guide', '/customer', '/expenses', '/cash-plan', '/growth-quality', '/product-matrix', '/tax-compliance', '/data-classification'],
|
||||
it: ['/my-dashboard', '/dashboard', '/ai-suggestions', '/kpis', '/strategy-map', '/alerts', '/data', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/cost-intelligence', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/analysis-confidence', '/expenses', '/cash-plan', '/product-matrix', '/tax-compliance', '/data-classification', '/treasury-payment'],
|
||||
ceo: ['/my-dashboard', '/strategy-charter', '/dashboard', '/ai-suggestions', '/kpis', '/maps', '/strategy-map', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/notifications', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/cost-intelligence', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/receivables', '/growth-quality', '/product-matrix', '/tax-compliance', '/data-classification', '/cashflow-structure', '/treasury-dashboard', '/treasury-payment'],
|
||||
finance: ['/my-dashboard', '/strategy-charter', '/dashboard', '/ai-suggestions', '/kpis', '/maps', '/strategy-map', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/data', '/budget', '/deviations', '/cost', '/cost-intelligence', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/receivables', '/growth-quality', '/product-matrix', '/tax-compliance', '/data-classification', '/cashflow-structure', '/treasury-dashboard', '/treasury-payment'],
|
||||
business: ['/my-dashboard', '/strategy-charter', '/dashboard', '/ai-suggestions', '/kpis', '/strategy-map', '/alerts', '/budget', '/deviations', '/action-plans', '/knowledge', '/guide', '/customer', '/expenses', '/cash-plan', '/growth-quality', '/product-matrix', '/tax-compliance', '/data-classification'],
|
||||
it: ['/my-dashboard', '/strategy-charter', '/dashboard', '/ai-suggestions', '/kpis', '/strategy-map', '/alerts', '/data', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/cost-intelligence', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/analysis-confidence', '/expenses', '/cash-plan', '/product-matrix', '/tax-compliance', '/data-classification', '/treasury-payment'],
|
||||
}
|
||||
|
||||
export const ROLE_ACTIONS: Record<string, string[]> = {
|
||||
@@ -26,6 +26,7 @@ export const MENU_ITEMS: MenuItem[] = [
|
||||
// ── GROUP 1: 规划与目标(Plan)──
|
||||
{ path: '/my-dashboard', label: '我的工作台', icon: 'Monitor', roles: ['ceo', 'finance', 'business', 'it'], group: '🔵 P 规划与目标' },
|
||||
{ path: '/maps', label: '战略地图', icon: 'DataBoard', roles: ['ceo', 'finance'], group: '🔵 P 规划与目标' },
|
||||
{ path: '/strategy-charter', label: '战略章程', icon: 'Opportunity', roles: ['ceo', 'finance', 'business', 'it'], group: '🔵 P 规划与目标' },
|
||||
{ path: '/strategy-map', label: '战略地图看板', icon: 'DataBoard', roles: ['ceo', 'finance', 'business', 'it'], group: '🔵 P 规划与目标' },
|
||||
{ path: '/budget', label: '预算管理', icon: 'Coin', roles: ['ceo', 'finance', 'business', 'it'], group: '🔵 P 规划与目标' },
|
||||
{ path: '/alignment', label: '战略执行看板', icon: 'Opportunity', roles: ['ceo', 'finance', 'business', 'it'], group: '🔵 P 规划与目标' },
|
||||
|
||||
@@ -19,6 +19,8 @@ const routes = [
|
||||
{ path: 'maps/canvas/:id', name: 'MapCanvas', component: () => import('@/views/MapCanvas.vue'), meta: { title: '战略地图画布', roles: ['ceo', 'finance'], zone: 'B', editable: true } },
|
||||
// /strategy-map: A区(只读活看板 —— 读库渲染,页内零写入口;2026-10-11 finance-20261011-strategy-map-frontend-fullstack)
|
||||
{ path: 'strategy-map', name: 'StrategyMap', component: () => import('@/views/StrategyMap.vue'), meta: { title: '战略地图看板', roles: ['ceo', 'finance', 'business', 'it'], zone: 'A' } },
|
||||
// /strategy-charter: P 组·战略基座入口(使命/愿景/价值观;页内含 MyDialog 写入口 ⇒ B区)
|
||||
{ path: 'strategy-charter', name: 'StrategyCharter', component: () => import('@/views/StrategyCharter.vue'), meta: { title: '战略章程', roles: ['ceo', 'finance', 'business', 'it'], zone: 'B', editable: true } },
|
||||
{ path: 'maps/review/:id', name: 'MapReview', component: () => import('@/views/MapReview.vue'), meta: { title: '战略回顾会', roles: ['ceo', 'finance'], zone: 'B' } },
|
||||
{ path: 'my-dashboard', name: 'MyDashboard', component: () => import('@/views/MyDashboard.vue'), meta: { title: '我的工作台', roles: ['ceo', 'finance', 'business', 'it'], zone: 'A' } },
|
||||
{ path: 'knowledge', name: 'CMAKnowledge', component: () => import('@/views/CMAKnowledge.vue'), meta: { title: 'CMA知识库', roles: ['ceo', 'finance', 'business', 'it'], zone: 'B', editable: true } },
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<div class="charter-page">
|
||||
<div class="charter-head">
|
||||
<div class="charter-title">
|
||||
<h2>战略章程</h2>
|
||||
<p>P 战略基座 · 使命 / 愿景(年度 + 可验证状态)/ 价值观 —— 全公司战略口径的唯一起点</p>
|
||||
</div>
|
||||
<div class="charter-actions">
|
||||
<el-button
|
||||
v-if="canEdit"
|
||||
type="primary"
|
||||
:icon="Edit"
|
||||
:disabled="loading"
|
||||
@click="openEdit"
|
||||
>{{ filled ? '修订章程' : '制定章程' }}</el-button>
|
||||
<el-button v-else :icon="Lock" disabled title="仅经营层(总经理 / 财务部)可修订章程">只读</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-skeleton v-if="loading" :rows="6" animated />
|
||||
|
||||
<!-- 空态:库里没有章程行 —— 不显示任何示例文本(派单红线) -->
|
||||
<div v-else-if="!filled" class="charter-empty">
|
||||
<el-icon :size="40"><Opportunity /></el-icon>
|
||||
<h3>尚未填写战略章程</h3>
|
||||
<p>本账套还没有使命 / 愿景 / 价值观记录。请由经营层录入第一条,系统不做任何预填。</p>
|
||||
<el-button v-if="canEdit" type="primary" :icon="Edit" @click="openEdit">录入章程</el-button>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="charter-cards">
|
||||
<section class="charter-card">
|
||||
<div class="card-label"><el-icon><Aim /></el-icon>使命<span>Mission</span></div>
|
||||
<p class="card-text">{{ data?.mission }}</p>
|
||||
</section>
|
||||
<section class="charter-card">
|
||||
<div class="card-label">
|
||||
<el-icon><Sunrise /></el-icon>愿景<span>Vision</span>
|
||||
<el-tag size="small" effect="plain" class="year-tag">{{ data?.vision_year }}</el-tag>
|
||||
</div>
|
||||
<p class="card-text">{{ data?.vision }}</p>
|
||||
<div class="measurable">
|
||||
<span class="measurable-k">可验证状态</span>
|
||||
<span class="measurable-v">{{ data?.vision_measurable }}</span>
|
||||
</div>
|
||||
</section>
|
||||
<section class="charter-card charter-card--wide">
|
||||
<div class="card-label"><el-icon><Star /></el-icon>价值观<span>Core Values</span></div>
|
||||
<p class="card-text">{{ data?.values_text }}</p>
|
||||
<ul class="value-list">
|
||||
<li v-for="(item, idx) in data?.value_items || []" :key="idx">
|
||||
<span class="value-name">{{ item.name }}</span>
|
||||
<span class="value-meaning">{{ item.meaning }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="charter-meta">
|
||||
<div class="meta-item">
|
||||
<span class="meta-k">最后修订</span>
|
||||
<span class="meta-v">{{ data?.updated_by_name || '—' }} · {{ data?.updated_at || '—' }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-k">创建时间</span>
|
||||
<span class="meta-v">{{ data?.created_at || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 项目铁律:业务弹窗一律 MyDialog(禁 el-dialog / ElMessageBox 确认) -->
|
||||
<MyDialog v-model="dialogVisible" :title="filled ? '修订战略章程' : '制定战略章程'" :width="760">
|
||||
<el-form :model="form" label-width="110px" label-position="left" class="charter-form">
|
||||
<el-form-item label="使命" required>
|
||||
<el-input
|
||||
v-model="form.mission"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="我们为何存在:做什么、为谁做"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="愿景" required>
|
||||
<el-input
|
||||
v-model="form.vision"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="要成为什么"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="愿景年度" required>
|
||||
<el-input v-model="form.vision_year" maxlength="4" placeholder="如 2030" class="year-input" />
|
||||
<span class="form-hint">不可空;4 位年份</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="可验证状态" required>
|
||||
<el-input
|
||||
v-model="form.vision_measurable"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="用可核对的规模/业务口径描述,如「2030 年服务 300 家付费企业、续约率 ≥85%」"
|
||||
/>
|
||||
<span class="form-hint">不可空:没有可核对口径的愿景无法验证</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="价值观总述" required>
|
||||
<el-input
|
||||
v-model="form.values_text"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
placeholder="不可交易的底线,一句话总述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="价值观条目" required>
|
||||
<div class="value-editor">
|
||||
<div v-for="(item, idx) in form.value_items" :key="idx" class="value-row">
|
||||
<el-input v-model="item.name" maxlength="50" placeholder="名称(如 客户成功)" class="value-row-name" />
|
||||
<el-input v-model="item.meaning" maxlength="300" placeholder="含义:决策冲突时如何裁决" class="value-row-meaning" />
|
||||
<el-button
|
||||
:icon="Delete"
|
||||
text
|
||||
type="danger"
|
||||
:disabled="form.value_items.length <= 1"
|
||||
title="删除该条"
|
||||
@click="form.value_items.splice(idx, 1)"
|
||||
/>
|
||||
</div>
|
||||
<el-button :icon="Plus" text type="primary" @click="addValueItem">新增条目</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false" :disabled="saving">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||||
</template>
|
||||
</MyDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Aim, Delete, Edit, Lock, Opportunity, Plus, Star, Sunrise } from '@element-plus/icons-vue'
|
||||
import MyDialog from '@/components/MyDialog.vue'
|
||||
import { charterApi } from '@/api'
|
||||
|
||||
interface ValueItem { name: string; meaning: string }
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const filled = ref(false)
|
||||
const data = ref<any>(null)
|
||||
const dialogVisible = ref(false)
|
||||
|
||||
const userRole = (() => {
|
||||
try { return JSON.parse(localStorage.getItem('cma_user') || '{}').role || '' } catch { return '' }
|
||||
})()
|
||||
// 与后端 PUT 角色白名单保持一致(ceo / finance)
|
||||
const canEdit = computed(() => ['ceo', 'finance'].includes(userRole))
|
||||
|
||||
const form = reactive({
|
||||
mission: '',
|
||||
vision: '',
|
||||
vision_year: '',
|
||||
vision_measurable: '',
|
||||
values_text: '',
|
||||
value_items: [{ name: '', meaning: '' }] as ValueItem[],
|
||||
})
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await charterApi.get()
|
||||
filled.value = !!res.filled
|
||||
data.value = res.data
|
||||
} catch (e) {
|
||||
ElMessage.error('章程加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openEdit() {
|
||||
form.mission = data.value?.mission || ''
|
||||
form.vision = data.value?.vision || ''
|
||||
form.vision_year = data.value?.vision_year || ''
|
||||
form.vision_measurable = data.value?.vision_measurable || ''
|
||||
form.values_text = data.value?.values_text || ''
|
||||
const items: ValueItem[] = (data.value?.value_items || []).map((i: ValueItem) => ({
|
||||
name: i.name, meaning: i.meaning,
|
||||
}))
|
||||
form.value_items = items.length ? items : [{ name: '', meaning: '' }]
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function addValueItem() {
|
||||
form.value_items.push({ name: '', meaning: '' })
|
||||
}
|
||||
|
||||
function validate(): string | null {
|
||||
if (!form.mission.trim()) return '使命不能为空'
|
||||
if (!form.vision.trim()) return '愿景不能为空'
|
||||
if (!/^\d{4}$/.test(form.vision_year.trim())) return '愿景年度须为 4 位年份(如 2030)'
|
||||
if (!form.vision_measurable.trim()) return '可验证状态不能为空'
|
||||
if (!form.values_text.trim()) return '价值观总述不能为空'
|
||||
const items = form.value_items.filter(i => i.name.trim() || i.meaning.trim())
|
||||
if (!items.length) return '价值观条目至少 1 条'
|
||||
if (items.some(i => !i.name.trim() || !i.meaning.trim())) return '价值观条目的名称与含义都要填'
|
||||
return null
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const err = validate()
|
||||
if (err) { ElMessage.warning(err); return }
|
||||
saving.value = true
|
||||
try {
|
||||
// 只提交后端字段白名单内的字段(entity_id 由登录账套决定,不手写)
|
||||
const payload = {
|
||||
mission: form.mission.trim(),
|
||||
vision: form.vision.trim(),
|
||||
vision_year: form.vision_year.trim(),
|
||||
vision_measurable: form.vision_measurable.trim(),
|
||||
values_text: form.values_text.trim(),
|
||||
value_items: form.value_items
|
||||
.filter(i => i.name.trim() && i.meaning.trim())
|
||||
.map(i => ({ name: i.name.trim(), meaning: i.meaning.trim() })),
|
||||
}
|
||||
const body: any = await charterApi.save(payload)
|
||||
ElMessage.success('战略章程已保存')
|
||||
dialogVisible.value = false
|
||||
filled.value = true
|
||||
data.value = body.data
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.detail || '保存失败,请稍后重试')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.charter-page { padding: var(--bh-space-xl, 20px); background: var(--bh-bg-main); min-height: 100%; }
|
||||
.charter-head { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--bh-space-lg, 16px); margin-bottom: var(--bh-space-xl, 20px); flex-wrap: wrap; }
|
||||
.charter-title h2 { margin: 0 0 6px; font-size: 20px; color: var(--bh-text-primary); font-weight: 600; }
|
||||
.charter-title p { margin: 0; font-size: 13px; color: var(--bh-text-secondary); }
|
||||
.charter-actions { display: flex; align-items: center; gap: var(--bh-space-md, 12px); }
|
||||
|
||||
.charter-empty { background: var(--bh-bg-card); border: 1px dashed var(--bh-border); border-radius: var(--bh-radius-md, 8px); padding: 56px 24px; text-align: center; color: var(--bh-text-secondary); }
|
||||
.charter-empty h3 { margin: 12px 0 6px; color: var(--bh-text-primary); font-size: 16px; }
|
||||
.charter-empty p { margin: 0 0 18px; font-size: 13px; }
|
||||
|
||||
.charter-cards { display: grid; grid-template-columns: 1fr 1fr; gap: var(--bh-space-lg, 16px); }
|
||||
.charter-card { background: var(--bh-bg-card); border-radius: var(--bh-radius-md, 8px); box-shadow: var(--bh-shadow-sm); padding: var(--bh-space-xl, 20px); border-left: 3px solid var(--bh-primary); }
|
||||
.charter-card--wide { grid-column: 1 / -1; border-left-color: var(--bh-success); }
|
||||
.card-label { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; color: var(--bh-text-regular); }
|
||||
.card-label span { font-weight: 400; font-size: 12px; color: var(--bh-text-placeholder); margin-left: 4px; letter-spacing: .3px; }
|
||||
.year-tag { margin-left: auto; }
|
||||
.card-text { margin: 12px 0 0; font-size: 15px; line-height: 1.75; color: var(--bh-text-primary); white-space: pre-wrap; word-break: break-word; }
|
||||
|
||||
.measurable { margin-top: 14px; padding-top: 12px; border-top: 1px dashed var(--bh-border-light); display: flex; flex-direction: column; gap: 4px; }
|
||||
.measurable-k { font-size: 12px; color: var(--bh-text-secondary); }
|
||||
.measurable-v { font-size: 13px; color: var(--bh-text-regular); line-height: 1.6; word-break: break-word; }
|
||||
|
||||
.value-list { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
|
||||
.value-list li { display: flex; gap: 10px; align-items: baseline; padding: 8px 12px; background: var(--bh-bg-main); border-radius: var(--bh-radius-sm, 4px); }
|
||||
.value-name { flex: 0 0 110px; font-size: 13px; font-weight: 600; color: var(--bh-text-primary); }
|
||||
.value-meaning { font-size: 13px; color: var(--bh-text-regular); line-height: 1.6; }
|
||||
|
||||
.charter-meta { margin-top: var(--bh-space-lg, 16px); background: var(--bh-bg-card); border-radius: var(--bh-radius-md, 8px); box-shadow: var(--bh-shadow-sm); padding: var(--bh-space-lg, 16px) var(--bh-space-xl, 20px); display: flex; flex-wrap: wrap; gap: var(--bh-space-xl, 20px); }
|
||||
.meta-item { display: flex; flex-direction: column; gap: 4px; min-width: 160px; }
|
||||
.meta-k { font-size: 12px; color: var(--bh-text-secondary); }
|
||||
.meta-v { font-size: 13px; color: var(--bh-text-primary); }
|
||||
|
||||
.charter-form :deep(.el-form-item) { margin-bottom: 16px; }
|
||||
.form-hint { margin-left: 10px; font-size: 12px; color: var(--bh-text-placeholder); }
|
||||
.year-input { width: 140px; }
|
||||
.value-editor { width: 100%; display: flex; flex-direction: column; gap: 8px; }
|
||||
.value-row { display: flex; gap: 8px; align-items: center; }
|
||||
.value-row-name { width: 150px; flex: 0 0 150px; }
|
||||
.value-row-meaning { flex: 1; }
|
||||
</style>
|
||||
@@ -25,6 +25,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── 战略章程(P 战略基座:地图最顶层输入,只读;空态不显示,禁示例文案)── -->
|
||||
<div v-if="charter" class="charter-strip">
|
||||
<div class="cs-item cs-mission">
|
||||
<span class="cs-k">使命</span>
|
||||
<span class="cs-v">{{ charter.mission }}</span>
|
||||
</div>
|
||||
<div class="cs-item">
|
||||
<span class="cs-k">愿景 · {{ charter.vision_year }}</span>
|
||||
<span class="cs-v">{{ charter.vision }}</span>
|
||||
</div>
|
||||
<div class="cs-item cs-measurable">
|
||||
<span class="cs-k">可验证状态</span>
|
||||
<span class="cs-v">{{ charter.vision_measurable }}</span>
|
||||
</div>
|
||||
<router-link class="cs-link" to="/strategy-charter">查看章程</router-link>
|
||||
</div>
|
||||
|
||||
<!-- ── 口径/统计条 ── -->
|
||||
<div class="meta-bar">
|
||||
<!-- 口径标识位(常驻,非 hover):与看板/KPI 列表共用同一组件与文案常量 -->
|
||||
@@ -167,6 +184,7 @@ import {
|
||||
Money, User, Setting, Reading, TrendCharts,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { mapApi } from '../api/index'
|
||||
import { charterApi } from '../api/index'
|
||||
import CaliberBadge from '../components/CaliberBadge.vue'
|
||||
import { caliberGapFor, currentCaliberEntityId, isCaliberKpiCode, type CaliberGapFacts } from '../constants/caliber'
|
||||
|
||||
@@ -175,6 +193,8 @@ const boardRef = ref<HTMLElement | null>(null)
|
||||
const loading = ref(false)
|
||||
const errorMsg = ref('')
|
||||
const board = ref<any>(null)
|
||||
// 战略章程(P 基座最顶层输入,只读;无行 → null,不显示任何文案)
|
||||
const charter = ref<any>(null)
|
||||
const period = ref<string>('')
|
||||
const periods = ref<string[]>([])
|
||||
const boardSize = ref({ w: 0, h: 0 })
|
||||
@@ -317,6 +337,10 @@ function onResize() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 章程与地图并行取;章程失败不阻断地图(只读装饰项)
|
||||
charterApi.get()
|
||||
.then((res: any) => { if (res?.filled) charter.value = res.data })
|
||||
.catch(() => { charter.value = null })
|
||||
await load()
|
||||
window.addEventListener('resize', onResize)
|
||||
})
|
||||
@@ -367,6 +391,16 @@ function doPrint() {
|
||||
<style scoped>
|
||||
.strategy-map-page { padding: 16px; background: #f5f7fa; min-height: 100%; }
|
||||
|
||||
/* 战略章程条(P 基座最顶层输入,只读) */
|
||||
.charter-strip { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; padding: 10px 14px; margin-bottom: 10px; background: var(--bh-bg-card, #fff); border-left: 3px solid var(--bh-primary, #409eff); border-radius: var(--bh-radius-sm, 4px); box-shadow: var(--bh-shadow-sm, 0 1px 2px rgba(0,0,0,.06)); }
|
||||
.cs-item { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
|
||||
.cs-k { flex: 0 0 auto; font-size: 12px; color: var(--bh-text-secondary, #909399); }
|
||||
.cs-v { font-size: 13px; color: var(--bh-text-primary, #303133); max-width: 420px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.cs-mission .cs-v { font-weight: 600; }
|
||||
.cs-measurable .cs-v { color: var(--bh-text-regular, #606266); }
|
||||
.cs-link { margin-left: auto; font-size: 12px; color: var(--bh-primary, #409eff); text-decoration: none; }
|
||||
.cs-link:hover { text-decoration: underline; }
|
||||
|
||||
.page-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }
|
||||
.page-subtitle { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 500; color: #64748B; margin: 0; }
|
||||
.title-icon { color: #64748B; font-size: 16px; }
|
||||
|
||||
Reference in New Issue
Block a user