merge: origin/main 合入战略地图改造分支(解 MapCanvas 模板冲突:摘要条+编辑条+口径条并存)
ci/woodpecker/push/main Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<span
|
||||
v-if="facts"
|
||||
class="caliber-badge"
|
||||
:class="`caliber-badge--${kind}`"
|
||||
:data-caliber-badge="kind"
|
||||
:data-caliber-tip="tipText"
|
||||
:title="tipText"
|
||||
role="note"
|
||||
:aria-label="`${labelText}:${tipText}`"
|
||||
>
|
||||
{{ labelText }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 口径标识(常驻,非 hover 临时提示)
|
||||
*
|
||||
* - kind="account_invoice_gap":看板/战略地图首屏常驻「科目口径 · 缺票」
|
||||
* - kind="estimate":缺票期数值旁「测算/待票」(只标现值,不重算)
|
||||
*
|
||||
* 文案一律取自 constants/caliber.ts,禁止页内另写。
|
||||
* 使用原生 title 作为工具提示(与 ForecastTag 同惯例),常驻文本 + 长说明分离。
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
CALIBER_ACCOUNT_GAP_LABEL,
|
||||
CALIBER_ESTIMATE_LABEL,
|
||||
caliberEstimateTip,
|
||||
caliberGapTip,
|
||||
type CaliberGapFacts,
|
||||
type CaliberGapKind,
|
||||
} from '../constants/caliber'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
kind?: CaliberGapKind
|
||||
/** 账套缺票事实;缺省时不渲染(避免把博海事实套到其它账套) */
|
||||
facts?: CaliberGapFacts | null
|
||||
/** 允许覆盖标签文案(默认取常量真源) */
|
||||
label?: string
|
||||
}>(),
|
||||
{ kind: 'account_invoice_gap', facts: null, label: '' },
|
||||
)
|
||||
|
||||
const labelText = computed(() => {
|
||||
if (props.label) return props.label
|
||||
return props.kind === 'estimate' ? CALIBER_ESTIMATE_LABEL : CALIBER_ACCOUNT_GAP_LABEL
|
||||
})
|
||||
|
||||
const tipText = computed(() => {
|
||||
if (!props.facts) return ''
|
||||
return props.kind === 'estimate' ? caliberEstimateTip(props.facts) : caliberGapTip(props.facts)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.caliber-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
padding: 0 7px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
cursor: help;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.caliber-badge--account_invoice_gap {
|
||||
color: var(--el-color-warning);
|
||||
background: var(--el-color-warning-light-9);
|
||||
border: 1px solid var(--el-color-warning-light-5);
|
||||
}
|
||||
.caliber-badge--estimate {
|
||||
color: var(--el-text-color-secondary);
|
||||
background: var(--el-fill-color-lighter);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
</style>
|
||||
@@ -7,6 +7,8 @@
|
||||
{{ title }}
|
||||
</h3>
|
||||
<div class="header-actions">
|
||||
<!-- 口径标识(常驻):KPI 卡片看板首屏固定展示,非 hover 临时提示 -->
|
||||
<CaliberBadge v-if="caliberFacts" kind="account_invoice_gap" :facts="caliberFacts" />
|
||||
<el-radio-group v-model="periodType" size="small" @change="loadData">
|
||||
<el-radio-button value="month">本月</el-radio-button>
|
||||
<el-radio-button value="quarter">本季</el-radio-button>
|
||||
@@ -175,6 +177,13 @@
|
||||
<span :style="{ color: alertColor(row.alert_level), fontWeight: 600 }">
|
||||
{{ row.actual_value != null ? row.actual_value : '—' }}
|
||||
</span>
|
||||
<!-- 缺票期:只标现值 + 测算/待票,不重算 -->
|
||||
<CaliberBadge
|
||||
v-if="isCaliberKpiCode(row.kpi_code, caliberFacts)"
|
||||
kind="estimate"
|
||||
:facts="caliberFacts"
|
||||
style="margin-left: 4px"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="目标值" width="100">
|
||||
@@ -266,6 +275,8 @@ import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import api from '../api/index'
|
||||
import CaliberBadge from './CaliberBadge.vue'
|
||||
import { caliberGapFor, currentCaliberEntityId, isCaliberKpiCode, type CaliberGapFacts } from '../constants/caliber'
|
||||
|
||||
interface Category {
|
||||
key: string
|
||||
@@ -326,6 +337,8 @@ const formTitle = computed(() => {
|
||||
|
||||
const loading = ref(true)
|
||||
const kpis = ref<any[]>([])
|
||||
// 口径标识:只对登记过缺票事实的账套挂「科目口径 · 缺票」(当前 entity 2 陕西博海)
|
||||
const caliberFacts = ref<CaliberGapFacts | null>(caliberGapFor(currentCaliberEntityId()))
|
||||
const periodType = ref('month')
|
||||
const showForm = ref(false)
|
||||
const editMode = ref(false)
|
||||
@@ -381,6 +394,7 @@ function loadData() {
|
||||
loading.value = true
|
||||
kpis.value = []
|
||||
const eid = Number(localStorage.getItem('cma_entity_id') || 1)
|
||||
caliberFacts.value = caliberGapFor(eid)
|
||||
api.get('/kpis', { params: { dimension: props.dimension, page_size: 100, entity_id: eid } })
|
||||
.then((r: any) => {
|
||||
kpis.value = (r.data || []).map((k: any) => ({
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 口径标识文案单一真源(展示层)
|
||||
*
|
||||
* 依据(不得改写):
|
||||
* - A 口径 v1.1(任总 2026-10-19 21:43「放行6」):博海收入净额 = 6001 贷方发生额(剔 7 行结转
|
||||
* 期间损溢借方 4,366,638.63)= 6,312,815.78;采购折扣仅酒类 639,944.00;毛利率 24.0350%。
|
||||
* - 该口径来自会计科目取数,**不是发票级实账**:发票四件套 0/4、`invoice_check` 0 行
|
||||
* ⇒ 对外禁用「实账」类口径表述,必须以「科目口径 · 缺票」明示。
|
||||
* - 口径台账:rulings-ledger RUL-2026-10-19-03。
|
||||
*
|
||||
* 用法:所有标识位必须引用本文件常量 + `components/CaliberBadge.vue`,
|
||||
* 不得在页面里另写一套标签文案(战略地图 v4 §2/§4 与本卡共用同一组件)。
|
||||
*/
|
||||
|
||||
export type CaliberGapKind = 'account_invoice_gap' | 'estimate'
|
||||
|
||||
export interface CaliberGapFacts {
|
||||
/** 账套名(仅用于 tooltip/回执可读性) */
|
||||
entityLabel: string
|
||||
/** 发票四件套齐备度,如 '0/4' */
|
||||
fourPiece: string
|
||||
/** invoice_check 台账行数 */
|
||||
invoiceCheckRows: number
|
||||
/** 参与口径的会计科目 */
|
||||
subjects: string[]
|
||||
/** 该账套内载 A 口径(科目口径)的 KPI 编码:其数值须带「测算/待票」 */
|
||||
caliberKpiCodes: string[]
|
||||
}
|
||||
|
||||
/** 科目口径 · 缺票(常驻标识) */
|
||||
export const CALIBER_ACCOUNT_GAP_LABEL = '科目口径 · 缺票'
|
||||
|
||||
/** 缺票期数值标识(只显示现值,不按实账重算) */
|
||||
export const CALIBER_ESTIMATE_LABEL = '测算/待票'
|
||||
|
||||
/**
|
||||
* 各账套的缺票事实。未列出的账套不挂该标识(不得套用到酣客/demo 账套)。
|
||||
* 新增账套须先取到四件套/invoice_check 实测值再登记,禁止用默认值推断。
|
||||
*/
|
||||
export const CALIBER_GAP_BY_ENTITY: Record<number, CaliberGapFacts> = {
|
||||
2: {
|
||||
entityLabel: '陕西博海',
|
||||
fourPiece: '0/4',
|
||||
invoiceCheckRows: 0,
|
||||
subjects: ['6001', '530111', '571112', '6601', '6603'],
|
||||
// kpi_definitions 421/422/423/432(A 口径 v1.1 已落库):BH_REVENUE / BH_NET_PROFIT / BH_COST_RATIO / BH_GROSS_MARGIN
|
||||
caliberKpiCodes: ['BH_REVENUE', 'BH_NET_PROFIT', 'BH_COST_RATIO', 'BH_GROSS_MARGIN'],
|
||||
},
|
||||
}
|
||||
|
||||
/** 该行是否属缺票「科目口径」KPI(KPI 卡片/表格数值需带测算/待票) */
|
||||
export function isCaliberKpiCode(code: string | null | undefined, facts: CaliberGapFacts | null): boolean {
|
||||
if (!code || !facts) return false
|
||||
return facts.caliberKpiCodes.includes(code)
|
||||
}
|
||||
|
||||
/** 当前账套(与各页取数口径一致:localStorage.cma_entity_id,缺省 1) */
|
||||
export function currentCaliberEntityId(): number {
|
||||
return Number(localStorage.getItem('cma_entity_id') || 1)
|
||||
}
|
||||
|
||||
/** 取账套缺票事实;无登记 = 无该缺口(不挂标识) */
|
||||
export function caliberGapFor(entityId: number | null | undefined): CaliberGapFacts | null {
|
||||
if (entityId == null) return null
|
||||
return CALIBER_GAP_BY_ENTITY[Number(entityId)] || null
|
||||
}
|
||||
|
||||
/** 常驻标识 tooltip:口径来源 + 缺票证据(逐字可核) */
|
||||
export function caliberGapTip(facts: CaliberGapFacts): string {
|
||||
return (
|
||||
`口径来自会计科目(${facts.subjects.join('/')} 科目取数),非发票同票列明;` +
|
||||
`发票四件套 ${facts.fourPiece}、invoice_check ${facts.invoiceCheckRows} 行。` +
|
||||
`数值为科目口径现值,未按发票实账重算。`
|
||||
)
|
||||
}
|
||||
|
||||
/** 缺票期数值 tooltip:只显示现值 + 测算/待票,不静默重算 */
|
||||
export function caliberEstimateTip(facts: CaliberGapFacts): string {
|
||||
return (
|
||||
`缺票期只展示现值(科目口径测算),不按实账发票重算、不回填历史值;` +
|
||||
`四件套 ${facts.fourPiece}、invoice_check ${facts.invoiceCheckRows} 行。`
|
||||
)
|
||||
}
|
||||
@@ -32,6 +32,8 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<!-- 口径标识(常驻):A 口径 v1.1 来自会计科目、缺票未闭 → 首屏固定可见 -->
|
||||
<CaliberBadge v-if="caliberFacts" kind="account_invoice_gap" :facts="caliberFacts" style="margin-right: 8px" />
|
||||
<span v-if="dataRange" class="data-range">📅 {{ dataRange }}</span>
|
||||
<el-button text size="small" @click="showSidebar = !showSidebar; if(showSidebar) refreshAnalysis()">
|
||||
{{ showSidebar ? '收起AI' : '🤖 AI分析' }}
|
||||
@@ -186,7 +188,7 @@
|
||||
<div class="stat-card blue" @click="$router.push('/deviations')" style="cursor:pointer;"><div class="stat-val">{{ finSummary.total_sales ? fmtValue(finSummary.total_sales, 'SALES_TOTAL') : '-' }}</div><div class="stat-label">本月销售额</div></div>
|
||||
<div class="stat-card" :class="(finSummary.gross_profit_rate ?? 0) < 0 ? 'red' : 'green'" @click="$router.push('/deviations')" style="cursor:pointer;">
|
||||
<div class="stat-val">{{ finSummary.gross_profit_rate != null ? finSummary.gross_profit_rate.toFixed(1) + '%' : '-' }}</div>
|
||||
<div class="stat-label">毛利率</div>
|
||||
<div class="stat-label">毛利率 <CaliberBadge v-if="caliberFacts" kind="estimate" :facts="caliberFacts" /></div>
|
||||
</div>
|
||||
<div class="stat-card yellow" @click="$router.push('/deviations')" style="cursor:pointer;"><div class="stat-val">{{ finSummary.receivable_turnover != null ? finSummary.receivable_turnover.toFixed(2) : '-' }}</div><div class="stat-label">应收周转率</div></div>
|
||||
</div>
|
||||
@@ -194,6 +196,8 @@
|
||||
<div v-for="k in finKpis" :key="k.id" class="kpi-card-item wide" @click="showKPIDetail(k)">
|
||||
<div class="card-item-head">
|
||||
<span class="item-name">{{ k.kpi_name }}</span>
|
||||
<!-- 缺票期:现值旁标测算/待票(不重算、不回填) -->
|
||||
<CaliberBadge v-if="isCaliberKpiCode(k.kpi_code, caliberFacts)" kind="estimate" :facts="caliberFacts" />
|
||||
<el-tag size="small" :type="alertType(k.alert_level)">{{ alertIcon(k.alert_level) }}</el-tag>
|
||||
</div>
|
||||
<div class="card-item-val" :style="{ color: alertColor(k.alert_level) }">
|
||||
@@ -315,6 +319,8 @@ import BEPBreakEvenCard from '../components/BEPBreakEvenCard.vue'
|
||||
import MyDialog from '../components/MyDialog.vue'
|
||||
// 预估区标签(财务裁定 2026-10-02:预估行单列展示,不与实测相加/同线)
|
||||
import ForecastTag from '../components/ForecastTag.vue'
|
||||
import CaliberBadge from '../components/CaliberBadge.vue'
|
||||
import { caliberGapFor, currentCaliberEntityId, isCaliberKpiCode, type CaliberGapFacts } from '../constants/caliber'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -365,6 +371,8 @@ const yellowKpis = computed(() => kpis.value.filter((k: any) => k.alert_level ==
|
||||
const myKpis = ref<any[]>([])
|
||||
const finKpis = ref<any[]>([])
|
||||
const finSummary = ref<any>({})
|
||||
// 缺票期标识:毛利率只显示现值 + 「测算/待票」,不按实账发票重算(仅登记账套挂载)
|
||||
const caliberFacts = ref<CaliberGapFacts | null>(caliberGapFor(currentCaliberEntityId()))
|
||||
// 预估区(单列,不计入实测汇总;财务裁定 2026-10-02)
|
||||
const forecastList = ref<any[]>([])
|
||||
const enabledChannels = ref(0)
|
||||
@@ -495,6 +503,7 @@ function onCustomRangeChange(val: any) {
|
||||
async function loadData() {
|
||||
try {
|
||||
const entityId = Number(localStorage.getItem('cma_entity_id') || 1)
|
||||
caliberFacts.value = caliberGapFor(entityId)
|
||||
const params = { ...getPeriodParams(), entity_id: entityId }
|
||||
// include_forecast=true:预估行**单列返回**(forecast 字段),实测取值不受影响
|
||||
if (userRole.value === 'ceo') {
|
||||
|
||||
@@ -54,6 +54,11 @@
|
||||
@completeness="showCompleteness = true"
|
||||
/>
|
||||
|
||||
<!-- 口径标识(常驻,非 hover 临时提示):A 口径 v1.1 来自会计科目、非发票同票列明 -->
|
||||
<div v-if="caliberFacts" class="caliber-strip">
|
||||
<CaliberBadge kind="account_invoice_gap" :facts="caliberFacts" />
|
||||
</div>
|
||||
|
||||
<!-- A3 三维切片(时间/门店/产品):切换即重算;空态显式,不回退总额 -->
|
||||
<MapDimensionFilter :map-id="selectedMap" @slice="onDimensionSlice" />
|
||||
|
||||
@@ -316,6 +321,8 @@ import MapCompleteness from "../components/strategy-map/MapCompleteness.vue"
|
||||
import ConnectionLines from "../components/strategy/ConnectionLines.vue"
|
||||
import MapCanvasToolbar from "../components/map-canvas/MapCanvasToolbar.vue"
|
||||
import MapDimensionFilter from "../components/map-canvas/MapDimensionFilter.vue"
|
||||
import CaliberBadge from "../components/CaliberBadge.vue"
|
||||
import { caliberGapFor, currentCaliberEntityId } from "../constants/caliber"
|
||||
import MapCanvasDialogs from "../components/map-canvas/MapCanvasDialogs.vue"
|
||||
import ConnectionManagerDialog from "../components/map-canvas/ConnectionManagerDialog.vue"
|
||||
import MyDialog from "../components/MyDialog.vue"
|
||||
@@ -351,6 +358,8 @@ const kpiNameMap = reactive<Record<string, string>>({})
|
||||
// 维度数据(对应backend的dimensions)
|
||||
const dimensions = reactive<any[]>([])
|
||||
const loading = ref(true)
|
||||
// 口径标识:仅登记过缺票事实的账套(当前为 entity 2 陕西博海)才挂「科目口径 · 缺票」
|
||||
const caliberFacts = ref(caliberGapFor(currentCaliberEntityId()))
|
||||
|
||||
// ── 仪表盘视图状态(2026-10-18 V1+V3 合版) ──
|
||||
/** 四层视觉色(本页按设计规范呈现,不改动地图存储的颜色值) */
|
||||
@@ -790,6 +799,7 @@ watch(zoomLevel, () => {
|
||||
|
||||
// ── 画布加载 ──
|
||||
function loadCanvas() {
|
||||
caliberFacts.value = caliberGapFor(currentCaliberEntityId())
|
||||
if (!selectedMap.value) return
|
||||
dimensions.splice(0, dimensions.length)
|
||||
connections.value = []
|
||||
@@ -2053,6 +2063,9 @@ onUnmounted(() => {
|
||||
/* ── 页面布局 ── */
|
||||
.map-canvas-page { padding: 16px; height: 100vh; display: flex; flex-direction: column; position: relative; overflow: hidden; }
|
||||
|
||||
/* 口径标识条:常驻首屏,右侧对齐,不占画布高度(flex-shrink: 0) */
|
||||
.caliber-strip { display: flex; justify-content: flex-end; align-items: center; margin: -4px 0 8px; flex-shrink: 0; }
|
||||
|
||||
.linking-bar {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 8px 16px; background: #fdf6ec; border: 1px solid #f5dab1;
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
|
||||
<!-- ── 口径/统计条 ── -->
|
||||
<div class="meta-bar">
|
||||
<!-- 口径标识位(常驻,非 hover):与看板/KPI 列表共用同一组件与文案常量 -->
|
||||
<CaliberBadge v-if="caliberFacts" kind="account_invoice_gap" :facts="caliberFacts" />
|
||||
<span class="axis-hint">泳道自下而上:学习 → 流程 → 客户 → 财务</span>
|
||||
<span class="legend">
|
||||
<span v-for="lg in legend" :key="lg.level" class="legend-item">
|
||||
@@ -110,6 +112,8 @@
|
||||
>
|
||||
<div class="card-head">
|
||||
<span class="card-name" :title="n.name">{{ n.name }}</span>
|
||||
<!-- 缺票期:只标现值 + 测算/待票,不重算、不回填 -->
|
||||
<CaliberBadge v-if="isCaliberKpiCode(n.kpi_code, caliberFacts)" kind="estimate" :facts="caliberFacts" />
|
||||
<el-tag size="small" effect="plain" class="unit-tag">{{ n.unit || '—' }}</el-tag>
|
||||
</div>
|
||||
|
||||
@@ -163,6 +167,8 @@ import {
|
||||
Money, User, Setting, Reading, TrendCharts,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { mapApi } from '../api/index'
|
||||
import CaliberBadge from '../components/CaliberBadge.vue'
|
||||
import { caliberGapFor, currentCaliberEntityId, isCaliberKpiCode, type CaliberGapFacts } from '../constants/caliber'
|
||||
|
||||
// ── 状态 ──
|
||||
const boardRef = ref<HTMLElement | null>(null)
|
||||
@@ -176,6 +182,12 @@ const edgePaths = ref<any[]>([])
|
||||
|
||||
const MAP_ID = 65 // 陕西酣客三年战略规划(e1);map 列表为空时回退到「最新非归档地图」
|
||||
|
||||
// 口径标识:以看板地图所属账套为准(接口返回 map.entity_id),缺省回退登录账套
|
||||
const caliberFacts = computed<CaliberGapFacts | null>(() => {
|
||||
const eid = board.value?.map?.entity_id
|
||||
return caliberGapFor(eid != null ? Number(eid) : currentCaliberEntityId())
|
||||
})
|
||||
|
||||
const displayLayers = computed<any[]>(() => {
|
||||
const layers = (board.value?.layers || []).slice()
|
||||
// 后端已按 学习→流程→客户→财务 返回(自下而上);页面自上而下渲染需反转
|
||||
|
||||
Reference in New Issue
Block a user