diff --git a/frontend/components.d.ts b/frontend/components.d.ts index dead878e..312decf1 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -15,6 +15,7 @@ declare module 'vue' { BotSourceTag: typeof import('./src/components/BotSourceTag.vue')['default'] BudgetCollectPanel: typeof import('./src/components/BudgetCollectPanel.vue')['default'] BulletChart: typeof import('./src/components/charts/BulletChart.vue')['default'] + CaliberBadge: typeof import('./src/components/CaliberBadge.vue')['default'] CashClassifyPanel: typeof import('./src/components/CashClassifyPanel.vue')['default'] ConnectionLines: typeof import('./src/components/strategy/ConnectionLines.vue')['default'] ConnectionManagerDialog: typeof import('./src/components/map-canvas/ConnectionManagerDialog.vue')['default'] @@ -80,6 +81,7 @@ declare module 'vue' { KPIListView: typeof import('./src/components/KPIListView.vue')['default'] MapCanvasDialogs: typeof import('./src/components/map-canvas/MapCanvasDialogs.vue')['default'] MapCanvasToolbar: typeof import('./src/components/map-canvas/MapCanvasToolbar.vue')['default'] + MapDimensionFilter: typeof import('./src/components/map-canvas/MapDimensionFilter.vue')['default'] MapNode: typeof import('./src/components/strategy-map/MapNode.vue')['default'] MyDialog: typeof import('./src/components/MyDialog.vue')['default'] NodeEditDialog: typeof import('./src/components/strategy-map/NodeEditDialog.vue')['default'] diff --git a/frontend/src/components/CaliberBadge.vue b/frontend/src/components/CaliberBadge.vue new file mode 100644 index 00000000..6e892ec4 --- /dev/null +++ b/frontend/src/components/CaliberBadge.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/frontend/src/components/KPIListView.vue b/frontend/src/components/KPIListView.vue index 53627894..539f632b 100644 --- a/frontend/src/components/KPIListView.vue +++ b/frontend/src/components/KPIListView.vue @@ -7,6 +7,8 @@ {{ title }}
+ + 本月 本季 @@ -175,6 +177,13 @@ {{ row.actual_value != null ? row.actual_value : '—' }} + + @@ -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([]) +// 口径标识:只对登记过缺票事实的账套挂「科目口径 · 缺票」(当前 entity 2 陕西博海) +const caliberFacts = ref(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) => ({ diff --git a/frontend/src/constants/caliber.ts b/frontend/src/constants/caliber.ts new file mode 100644 index 00000000..8822d1f2 --- /dev/null +++ b/frontend/src/constants/caliber.ts @@ -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 = { + 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} 行。` + ) +} diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index 2b94dd57..baa7e647 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -32,6 +32,8 @@ />
+ + 📅 {{ dataRange }} {{ showSidebar ? '收起AI' : '🤖 AI分析' }} @@ -186,7 +188,7 @@
{{ finSummary.total_sales ? fmtValue(finSummary.total_sales, 'SALES_TOTAL') : '-' }}
本月销售额
{{ finSummary.gross_profit_rate != null ? finSummary.gross_profit_rate.toFixed(1) + '%' : '-' }}
-
毛利率
+
毛利率
{{ finSummary.receivable_turnover != null ? finSummary.receivable_turnover.toFixed(2) : '-' }}
应收周转率
@@ -194,6 +196,8 @@
{{ k.kpi_name }} + + {{ alertIcon(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([]) const finKpis = ref([]) const finSummary = ref({}) +// 缺票期标识:毛利率只显示现值 + 「测算/待票」,不按实账发票重算(仅登记账套挂载) +const caliberFacts = ref(caliberGapFor(currentCaliberEntityId())) // 预估区(单列,不计入实测汇总;财务裁定 2026-10-02) const forecastList = ref([]) 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') { diff --git a/frontend/src/views/MapCanvas.vue b/frontend/src/views/MapCanvas.vue index 06b16a27..af28cf66 100644 --- a/frontend/src/views/MapCanvas.vue +++ b/frontend/src/views/MapCanvas.vue @@ -25,6 +25,11 @@ @toggle-chain-mode="toggleChainMode" /> + +
+ +
+ @@ -206,6 +211,8 @@ import StrategyLayer from "../components/strategy-map/StrategyLayer.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" @@ -241,6 +248,8 @@ const kpiNameMap = reactive>({}) // 维度数据(对应backend的dimensions) const dimensions = reactive([]) const loading = ref(true) +// 口径标识:仅登记过缺票事实的账套(当前为 entity 2 陕西博海)才挂「科目口径 · 缺票」 +const caliberFacts = ref(caliberGapFor(currentCaliberEntityId())) // 连线 const connections = ref([]) @@ -540,6 +549,7 @@ watch(zoomLevel, () => { // ── 画布加载 ── function loadCanvas() { + caliberFacts.value = caliberGapFor(currentCaliberEntityId()) if (!selectedMap.value) return dimensions.splice(0, dimensions.length) connections.value = [] @@ -1621,6 +1631,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; diff --git a/frontend/src/views/StrategyMap.vue b/frontend/src/views/StrategyMap.vue index d9f915a8..8feaa14c 100644 --- a/frontend/src/views/StrategyMap.vue +++ b/frontend/src/views/StrategyMap.vue @@ -27,6 +27,8 @@
+ + 泳道自下而上:学习 → 流程 → 客户 → 财务 @@ -110,6 +112,8 @@ >
{{ n.name }} + + {{ n.unit || '—' }}
@@ -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(null) @@ -176,6 +182,12 @@ const edgePaths = ref([]) const MAP_ID = 65 // 陕西酣客三年战略规划(e1);map 列表为空时回退到「最新非归档地图」 +// 口径标识:以看板地图所属账套为准(接口返回 map.entity_id),缺省回退登录账套 +const caliberFacts = computed(() => { + const eid = board.value?.map?.entity_id + return caliberGapFor(eid != null ? Number(eid) : currentCaliberEntityId()) +}) + const displayLayers = computed(() => { const layers = (board.value?.layers || []).slice() // 后端已按 学习→流程→客户→财务 返回(自下而上);页面自上而下渲染需反转