|
@@ -5,9 +5,8 @@
|
|
|
<h1>仓储管理总览</h1>
|
|
<h1>仓储管理总览</h1>
|
|
|
<div class="meta"><span>统计月份:<b>{{ monthLabel }}</b></span><span>服务端查询时间:<b>{{ summary.capturedAt || '尚未更新' }}</b></span></div>
|
|
<div class="meta"><span>统计月份:<b>{{ monthLabel }}</b></span><span>服务端查询时间:<b>{{ summary.capturedAt || '尚未更新' }}</b></span></div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="greet"><div><h2>{{ greeting }}{{ userName ? ',' + userName : '' }}</h2><p>今天是 {{ todayLabel }} · 数据更新于 {{ updatedAt || '尚未更新' }}(统计每 15 分钟、预警每 5 分钟、动态每 30 秒刷新)</p></div></div>
|
|
|
|
|
|
|
+ <div class="greet"><div><h2>{{ greeting }}{{ userName ? ',' + userName : '' }}</h2><p>今天是 {{ todayLabel }} · 数据更新于 {{ updatedAt || '尚未更新' }}(统计每 15 分钟、预警每 5 分钟、动态每 30 秒刷新)</p></div><div class="home-filters"><label>仓库 <select :value="warehouseId" @change="$emit('warehouse', $event.target.value)"><option value="">全部授权仓库</option><option v-for="item in options.warehouses || []" :key="item.id" :value="item.id">{{ item.name }}</option></select></label><label>基本单位 <select :value="unit" @change="$emit('unit', $event.target.value)"><option value="">全部单位</option><option v-for="item in options.units || []" :key="item.unit" :value="item.unit">{{ item.unit }}</option></select></label></div></div>
|
|
|
<div v-if="error" class="data-note bad" role="alert">{{ error }} <button type="button" @click="$emit('refresh')">重试</button></div>
|
|
<div v-if="error" class="data-note bad" role="alert">{{ error }} <button type="button" @click="$emit('refresh')">重试</button></div>
|
|
|
- <div class="data-note">估值覆盖率 {{ display(summary.valuationCoverage) }}% · 库龄来源覆盖率 {{ display(summary.ageCoverage) }}% · {{ summary.comparisonReason || '日末采集历史待核实' }}。批次估值按可核实入库价计算。</div>
|
|
|
|
|
<div class="todo-bar">
|
|
<div class="todo-bar">
|
|
|
<div v-for="(item, i) in todoCards" :key="item.key" class="todo-item" role="button" tabindex="0" @click="$emit('open', item.target, item.filters || {})" @keydown.enter="$emit('open', item.target, item.filters || {})">
|
|
<div v-for="(item, i) in todoCards" :key="item.key" class="todo-item" role="button" tabindex="0" @click="$emit('open', item.target, item.filters || {})" @keydown.enter="$emit('open', item.target, item.filters || {})">
|
|
|
<div class="todo-icon" :class="'icon-' + item.icon"><span>{{ item.symbol }}</span></div>
|
|
<div class="todo-icon" :class="'icon-' + item.icon"><span>{{ item.symbol }}</span></div>
|
|
@@ -16,35 +15,48 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="kpi-row">
|
|
<div class="kpi-row">
|
|
|
- <div v-for="(item, i) in kpis" :key="item.key" class="card kpi corner" role="button" tabindex="0" @click="$emit('open', item.target, item.filters || {})" @keydown.enter="$emit('open', item.target, item.filters || {})">
|
|
|
|
|
- <span class="idx">{{ ('0' + (i + 1)).slice(-2) }}</span><span class="kpi-go">查看明细 ›</span>
|
|
|
|
|
|
|
+ <div v-for="item in kpis" :key="item.key" class="card kpi corner" role="button" tabindex="0" @click="$emit('open', item.target, item.filters || {})" @keydown.enter="$emit('open', item.target, item.filters || {})">
|
|
|
|
|
+ <span class="idx">{{ item.code }}</span><span class="kpi-go">查看明细 ›</span>
|
|
|
<div class="lb">{{ item.label }} <span class="q" :title="item.info">?</span></div>
|
|
<div class="lb">{{ item.label }} <span class="q" :title="item.info">?</span></div>
|
|
|
- <div class="val" :style="{ color: item.color }">{{ display(item.value) }}<small v-if="item.value != null">{{ item.unit }}</small></div>
|
|
|
|
|
- <div class="delta dim" :title="item.reason">{{ item.reason || '数据待核实' }}</div>
|
|
|
|
|
|
|
+ <div class="val" :style="{ color: item.color }">{{ displayKpi(item) }}<small v-if="item.value != null">{{ item.unit }}</small></div>
|
|
|
|
|
+ <div class="delta dim" :title="item.deltaTitle || item.reason">{{ item.delta || item.status || item.reason || '数据待核实' }}</div>
|
|
|
|
|
+ <div v-if="item.subtotal" class="delta dim" :title="item.subtotal.title">{{ item.subtotal.text }}</div>
|
|
|
|
|
+ <div v-if="item.locked" class="delta dim" :title="item.locked.title">{{ item.locked.text }}</div>
|
|
|
<div v-if="item.progress != null" class="bar-mini"><i :style="{ width: item.progress + '%', background: item.color, color: item.color }"></i></div>
|
|
<div v-if="item.progress != null" class="bar-mini"><i :style="{ width: item.progress + '%', background: item.color, color: item.color }"></i></div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="grid-2-1">
|
|
<div class="grid-2-1">
|
|
|
- <section class="card corner"><div class="card-hd"><div><h3>出入库动态</h3><div class="sub">近 30 天滚动 · 净趋势=当日入库−当日出库</div></div><span class="link-group"><a class="link" href="#" @click.prevent="$emit('open', 'stockManagement')">进入入库管理 ›</a><a class="link" href="#" @click.prevent="$emit('open', 'outgoingManagement')">进入出库管理 ›</a></span></div><div class="chart-body"><div class="legend"><span><i style="background:#38C8F5"></i>入库量</span><span><i style="background:#F0A03A"></i>出库量</span><span><i style="background:#9B7DF7;height:2px"></i>净趋势</span></div><dashboard-chart v-if="trend.length" kind="trend" :rows="trend" :unit="unit" @select="trendSelect" /><div v-else class="empty-chart">{{ trendReason || '暂无已完成出入库流水' }}</div></div></section>
|
|
|
|
|
- <section class="card corner"><div class="card-hd"><div><h3>库存分类分布</h3><div class="sub">按物料分类 TOP8(前 7 + 其他兜底)· 按基本单位</div></div><a class="link" href="#" @click.prevent="$emit('open', 'ledger')">查看台账 ›</a></div><div class="chart-body"><dashboard-chart v-if="categories.length" kind="category" :rows="categories" :unit="unit" @select="categorySelect" /><div v-else class="empty-chart">{{ categoryReason || '当前筛选无可追溯库存' }}</div></div></section>
|
|
|
|
|
|
|
+ <section class="card corner"><div class="card-hd"><div><h3>出入库动态</h3><div class="sub">近 30 天滚动 · 净趋势=当日入库−当日出库</div></div><a class="link" href="#" @click.prevent="$emit('open', 'io')">查看动态 ›</a></div><div class="chart-body"><div class="legend"><span><i style="background:#38C8F5"></i>入库量</span><span><i style="background:#F0A03A"></i>出库量</span><span><i style="background:#9B7DF7;height:2px"></i>净趋势</span></div><dashboard-chart v-if="trend.length" kind="trend" :rows="trend" :unit="unit" @select="trendSelect" /><div v-else class="empty-chart">{{ trendReason || '暂无已完成出入库流水' }}</div></div></section>
|
|
|
|
|
+ <section class="card corner"><div class="card-hd"><div><h3>库存分类分布</h3><div class="sub">按物料分类 TOP8(前 7 + 其他兜底)· 按基本单位</div></div><a class="link" href="#" @click.prevent="$emit('open', 'stockLedger')">查看台账 ›</a></div><div class="chart-body"><dashboard-chart v-if="categories.length" kind="category" :rows="categories" :unit="unit" @select="categorySelect" /><div v-else class="empty-chart">{{ categoryReason || '当前筛选无可追溯库存' }}</div></div></section>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="grid-3">
|
|
<div class="grid-3">
|
|
|
- <section class="card corner"><div class="card-hd"><div><h3>库龄结构</h3><div class="sub">在库批次按库龄分段 · 与呆滞口径联动</div></div><a class="link" href="#" @click.prevent="$emit('open', 'age')">明细 ›</a></div><div class="chart-body"><dashboard-chart v-if="aging.length" kind="aging" :rows="aging" :unit="unit" @select="ageSelect" /><div v-else class="empty-panel">{{ agingReason || '日期待核实,暂无完整库龄结构' }}</div></div></section>
|
|
|
|
|
- <section class="card corner"><div class="card-hd"><div><h3>安全库存预警</h3><div class="sub">低于安全库存 / 超储 · 准实时(≤5 分钟)</div></div><a class="link" href="#" @click.prevent="$emit('open', 'alerts')">全部预警 ›</a></div><div v-if="alerts.length"><div v-for="item in alerts" :key="item.id" class="alert-item" role="button" tabindex="0" @click="$emit('open', 'alerts', { categoryId: item.categoryId, warehouseId: item.warehouseId })" @keydown.enter="$emit('open', 'alerts', { categoryId: item.categoryId, warehouseId: item.warehouseId })"><span class="dot" :style="{ background: item.color || '#F0A03A', color: item.color || '#F0A03A' }"></span><b>{{ item.name }}</b><span class="meta">{{ item.message }}</span></div></div><div v-else class="empty-panel">{{ alertReason || '暂无可核对的预警数据' }}</div></section>
|
|
|
|
|
- <section class="card corner"><div class="card-hd"><div><h3>最近作业动态</h3><div class="sub">出入库 / 调拨 / 盘点流水</div></div><a class="link" href="#" @click.prevent="$emit('open', 'io')">全部动态 ›</a></div><div v-if="activities.length" class="feed"><div v-for="item in activities" :key="item.id" class="feed-item" role="button" tabindex="0" @click="$emit('business', 'io', item)" @keydown.enter="$emit('business', 'io', item)"><span class="tm">{{ item.time }}</span><span><b>{{ item.title }}</b> · {{ item.description }}</span></div></div><div v-else class="empty-panel">{{ activityReason || '暂无可核对的作业动态' }}</div></section>
|
|
|
|
|
|
|
+ <section class="card corner"><div class="card-hd"><div><h3>库龄结构</h3><div class="sub">在库批次按库龄分段 · 与呆滞口径联动 · 平均库龄 {{ summary.averageAgeDays == null ? '—' : summary.averageAgeDays + '天' }}</div></div><a class="link" href="#" @click.prevent="$emit('open', 'age')">明细 ›</a></div><div class="chart-body"><dashboard-chart v-if="aging.length" kind="aging" :rows="aging" :unit="unit" @select="ageSelect" /><div v-else class="empty-panel">{{ agingReason || '日期待核实,暂无完整库龄结构' }}</div></div></section>
|
|
|
|
|
+ <section class="card corner"><div class="card-hd"><div><h3>安全库存预警</h3><div class="sub">低于安全库存 / 超储 · 准实时(≤5 分钟)</div></div><a class="link" href="#" @click.prevent="$emit('open', 'alerts')">全部 {{ summary.alertCount == null ? '—' : summary.alertCount }} 项 ›</a></div><div v-if="alerts.length"><div v-for="item in alerts" :key="item.id" class="alert-item" role="button" tabindex="0" @click="$emit('open', 'alerts', { categoryId: item.categoryId, warehouseId: item.warehouseId })" @keydown.enter="$emit('open', 'alerts', { categoryId: item.categoryId, warehouseId: item.warehouseId })"><span class="dot" :style="{ background: item.color || '#F0A03A', color: item.color || '#F0A03A' }"></span><b>{{ item.name }}</b><span class="meta">{{ item.message }}</span></div></div><div v-else class="empty-panel">{{ alertReason || '暂无可核对的预警数据' }}</div></section>
|
|
|
|
|
+ <section class="card corner"><div class="card-hd"><div><h3>最近作业动态</h3><div class="sub">出入库 / 调拨 / 盘点流水</div></div><a class="link" href="#" @click.prevent="$emit('open', 'io')">全部动态 ›</a></div><div v-if="activities.length" class="feed"><div v-for="item in activities" :key="item.id" class="feed-item" role="button" tabindex="0" @click="$emit('business', 'io', item)" @keydown.enter="$emit('business', 'io', item)"><span class="tm" :title="item.time">{{ activityTime(item.time) }}</span><span><b>{{ item.title }}</b> · {{ item.description }}</span><span class="feed-status" :class="{ active: item.activityType === 'inventory' && Number(item.status) === 1, waiting: item.activityType === 'inventory' && Number(item.status) === 0 }">{{ item.activityType === 'inventory' ? Number(item.status) === 2 ? '已完成' : Number(item.status) === 1 ? '进行中' : '待处理' : '已完成' }}</span></div></div><div v-else class="empty-panel">{{ activityReason || '暂无可核对的作业动态' }}</div></section>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <details class="data-note"><summary>统计口径与数据覆盖</summary>估值覆盖率 {{ display(summary.valuationCoverage) }}% · 库龄来源覆盖率 {{ display(summary.ageCoverage) }}% · {{ summary.comparisonReason || '日末采集历史待核实' }}。批次估值按可核实入库价计算。</details>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
<script>
|
|
<script>
|
|
|
import DashboardChart from './DashboardChart.vue';
|
|
import DashboardChart from './DashboardChart.vue';
|
|
|
-import { displayNumber } from '../dashboard-utils';
|
|
|
|
|
|
|
+import { displayNumber, formatSignedNumber, formatComparisonBase, formatUnitSubtotals, homeLog } from '../dashboard-utils';
|
|
|
|
|
|
|
|
export default {
|
|
export default {
|
|
|
name: 'WarehouseHomeOverview',
|
|
name: 'WarehouseHomeOverview',
|
|
|
components: { DashboardChart },
|
|
components: { DashboardChart },
|
|
|
- props: { summary: { type: Object, default: () => ({}) }, unit: { type: String, default: '' }, updatedAt: { type: String, default: '' }, error: { type: String, default: '' } },
|
|
|
|
|
|
|
+ props: { summary: { type: Object, default: () => ({}) }, options: { type: Object, default: () => ({}) }, warehouseId: { type: String, default: '' }, unit: { type: String, default: '' }, updatedAt: { type: String, default: '' }, error: { type: String, default: '' } },
|
|
|
computed: {
|
|
computed: {
|
|
|
- trend() { return this.summary.trend || []; },
|
|
|
|
|
|
|
+ trend() {
|
|
|
|
|
+ const rows = this.summary.trend || [];
|
|
|
|
|
+ if (!rows.length) return [];
|
|
|
|
|
+ const byDate = new Map(rows.map((row) => [row.date, row]));
|
|
|
|
|
+ const today = new Date();
|
|
|
|
|
+ return Array.from({ length: 30 }, (_, index) => {
|
|
|
|
|
+ const day = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29 + index);
|
|
|
|
|
+ const date = day.getFullYear() + '-' + String(day.getMonth() + 1).padStart(2, '0') + '-' + String(day.getDate()).padStart(2, '0');
|
|
|
|
|
+ return byDate.get(date) || { date, inbound: null, outbound: null, net: null };
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
categories() { return this.summary.categories || []; },
|
|
categories() { return this.summary.categories || []; },
|
|
|
aging() { return this.summary.aging || []; },
|
|
aging() { return this.summary.aging || []; },
|
|
|
alerts() { return this.summary.alerts || []; },
|
|
alerts() { return this.summary.alerts || []; },
|
|
@@ -55,34 +67,90 @@ export default {
|
|
|
alertReason() { return this.summary.alertReason || ''; },
|
|
alertReason() { return this.summary.alertReason || ''; },
|
|
|
activityReason() { return this.summary.activityReason || ''; },
|
|
activityReason() { return this.summary.activityReason || ''; },
|
|
|
|
|
|
|
|
|
|
+ // 库存总量「较上周」:口径为本期数量 − 7 天前日末同刻快照,仅单一基本单位且两侧快照齐全时后端才给值。
|
|
|
|
|
+ // weekOverWeekQuantity 为 null/缺失/非数字一律按「不可比」处理,显示比较原因兜底,绝不显示 0。
|
|
|
|
|
+ stockWeekOverWeek() {
|
|
|
|
|
+ const s = this.summary;
|
|
|
|
|
+ const diff = s.weekOverWeekQuantity;
|
|
|
|
|
+ const base = formatComparisonBase(s.comparisonBaseAt);
|
|
|
|
|
+ const comparable = diff != null && diff !== '' && Number.isFinite(Number(diff));
|
|
|
|
|
+ if (!comparable) {
|
|
|
|
|
+ const reason = s.comparisonReason || '暂无上周同时点可比快照';
|
|
|
|
|
+ homeLog('库存总量周环比不可比', { weekOverWeekQuantity: diff, comparisonBaseAt: s.comparisonBaseAt, comparisonReason: s.comparisonReason });
|
|
|
|
|
+ return { text: reason, title: s.comparisonReason || reason };
|
|
|
|
|
+ }
|
|
|
|
|
+ const unit = String(s.weekOverWeekUnit || this.unit || '').trim();
|
|
|
|
|
+ const signed = formatSignedNumber(diff);
|
|
|
|
|
+ // 0 只显示「持平」,不再拼数值;基准时点缺失时省略括号。
|
|
|
|
|
+ const text = '较上周' + (signed === '持平' ? '持平' : ' ' + signed + (unit ? ' ' + unit : '')) + (base ? '(基准 ' + base + ')' : '');
|
|
|
|
|
+ homeLog('库存总量周环比已解析', { weekOverWeekQuantity: diff, weekOverWeekUnit: unit, comparisonBaseAt: s.comparisonBaseAt, text });
|
|
|
|
|
+ return { text, title: '本期数量 − 7 天前日末同刻快照' + (base ? '(基准 ' + base + ')' : '') + ';不跨基本单位加总' };
|
|
|
|
|
+ },
|
|
|
|
|
+ // 多基本单位小计:units 多于 1 个时按「单位 数量(锁定 X)」平铺,不同单位绝不加总;
|
|
|
|
|
+ // 超过 3 个单位折叠为前 3 个 + 其余 N 个单位;units 缺省或只有 1 个单位时不渲染小计行。
|
|
|
|
|
+ stockUnitSubtotal() {
|
|
|
|
|
+ const { items, rest, total } = formatUnitSubtotals(this.summary.units, 3);
|
|
|
|
|
+ if (total <= 1) {
|
|
|
|
|
+ if (total === 0) homeLog('库存总量无分单位数据,不渲染小计行', { unitsCount: 0 });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ const text = items.map((item) => item.label).join(' · ') + (rest > 0 ? ' · 其余 ' + rest + ' 个单位' : '');
|
|
|
|
|
+ homeLog('库存总量分单位小计已解析', { unitsCount: total, shownUnits: items.map((item) => item.unit), collapsedUnits: rest, text });
|
|
|
|
|
+ return { text, title: items.map((item) => item.title).join(';') + (rest > 0 ? ';另有 ' + rest + ' 个单位未展开' : '') };
|
|
|
|
|
+ },
|
|
|
|
|
+ // 锁定数量单列:单一基本单位时显示「其中锁定 X 个单位名」;0 是真实零显示「0」,null/缺失显示「—」。
|
|
|
|
|
+ // 多基本单位时锁定数随小计行逐单位展示,这里返回 null 避免重复。
|
|
|
|
|
+ stockLocked() {
|
|
|
|
|
+ const rows = (Array.isArray(this.summary.units) ? this.summary.units : []).filter((row) => row && row.unit != null && String(row.unit).trim() !== '');
|
|
|
|
|
+ if (rows.length !== 1) return null;
|
|
|
|
|
+ const unit = String(rows[0].unit).trim();
|
|
|
|
|
+ const locked = displayNumber(rows[0].lockedQuantity);
|
|
|
|
|
+ const text = '其中锁定 ' + locked + (unit ? ' ' + unit : '');
|
|
|
|
|
+ homeLog('库存总量锁定数量已解析', { unit, lockedQuantity: rows[0].lockedQuantity, text });
|
|
|
|
|
+ return { text, title: unit + ':锁定数量 ' + locked + ';0 为真实零,缺失显示「—」' };
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
userName() { return (this.$store.state.user.info || {}).nickname || (this.$store.state.user.info || {}).name || ''; },
|
|
userName() { return (this.$store.state.user.info || {}).nickname || (this.$store.state.user.info || {}).name || ''; },
|
|
|
monthLabel() { return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit' }).format(new Date()); },
|
|
monthLabel() { return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit' }).format(new Date()); },
|
|
|
todayLabel() { return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long' }).format(new Date()); },
|
|
todayLabel() { return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long' }).format(new Date()); },
|
|
|
greeting() { const hour = new Date().getHours(); return hour < 11 ? '上午好' : hour < 18 ? '下午好' : '晚上好'; },
|
|
greeting() { const hour = new Date().getHours(); return hour < 11 ? '上午好' : hour < 18 ? '下午好' : '晚上好'; },
|
|
|
todoCards() {
|
|
todoCards() {
|
|
|
return [
|
|
return [
|
|
|
- { key: 'in', icon: 'in', symbol: '↓', label: '待入库单', value: this.summary.pendingInbound, target: 'stockManagement', overdue: this.summary.overdueInbound },
|
|
|
|
|
- { key: 'out', icon: 'out', symbol: '↑', label: '待出库单', value: this.summary.pendingOutbound, target: 'outgoingManagement', overdue: this.summary.overdueOutbound },
|
|
|
|
|
- { key: 'check', icon: 'check', symbol: '✓', label: '盘点任务进行中', value: this.summary.pendingInventory, target: 'todo', overdue: this.summary.overdueInventory },
|
|
|
|
|
|
|
+ { key: 'in', icon: 'in', symbol: '↓', label: '待入库单', value: this.summary.pendingInbound, target: 'ioapp', overdue: this.summary.overdueInbound },
|
|
|
|
|
+ { key: 'out', icon: 'out', symbol: '↑', label: '待出库单', value: this.summary.pendingOutbound, target: 'inapp', overdue: this.summary.overdueOutbound },
|
|
|
|
|
+ { key: 'check', icon: 'check', symbol: '✓', label: '盘点任务进行中', value: this.summary.pendingInventory, target: 'todo' },
|
|
|
{ key: 'approve', icon: 'approve', symbol: '▣', label: '待我审批', value: this.summary.pendingApproval, overdue: this.summary.overdueApproval, target: 'flow' }
|
|
{ key: 'approve', icon: 'approve', symbol: '▣', label: '待我审批', value: this.summary.pendingApproval, overdue: this.summary.overdueApproval, target: 'flow' }
|
|
|
];
|
|
];
|
|
|
},
|
|
},
|
|
|
kpis() {
|
|
kpis() {
|
|
|
const s = this.summary;
|
|
const s = this.summary;
|
|
|
return [
|
|
return [
|
|
|
- { key: 'stock', label: '库存总量', value: s.inventoryQuantity, unit: this.unit, reason: s.inventoryReason, info: '当前在库数量,含待检及锁定;不同基本单位分别展示', color: '#38C8F5', target: 'stockLedger' },
|
|
|
|
|
- { key: 'value', label: '库存金额', value: s.inventoryValue, unit: '元', reason: s.valuationReason, info: '批次实际计量数量或重量乘可核实入库单价;未定价不作为零金额', color: '#9B7DF7', target: 'stockLedger' },
|
|
|
|
|
- { key: 'turnover', label: '库存周转天数', value: s.turnoverDays, unit: '天', reason: s.turnoverReason, info: '本月截至昨日的平均库存与实际消耗估算,历史不足时无法计算', color: '#3BD48A', target: 'turnover' },
|
|
|
|
|
- { key: 'stagnant', label: '呆滞品占比', value: s.stagnantRate, unit: '%', reason: s.stagnantReason, info: '首次入库及最近实际消耗均超过 180 天的批次估值除以全部库存估值;估值或日期不完整时无法核实', color: '#F0A03A', progress: s.stagnantRate, target: 'dead' },
|
|
|
|
|
- { key: 'location', label: '库位利用率', value: s.locationUtilization, unit: '%', reason: s.locationReason, info: '已占用的有效库位数量除以启用的普通库位数量', color: '#38C8F5', progress: s.locationUtilization, target: 'locations' },
|
|
|
|
|
- { key: 'quality', label: '质检通过率', value: s.qualityPassRate, unit: '%', reason: s.qualityReason, info: '本月已检批次中合格批次占比,让步计分母但不计合格', color: '#3BD48A', progress: s.qualityPassRate, target: 'qc' }
|
|
|
|
|
|
|
+ { key: 'stock', code: '01', label: '库存总量', value: s.inventoryQuantity, unit: this.unit, reason: s.inventoryReason, delta: this.stockWeekOverWeek.text, deltaTitle: this.stockWeekOverWeek.title, subtotal: this.stockUnitSubtotal, locked: this.stockLocked, info: '当前在库数量含待检、剔除锁定;不同基本单位分别展示,不跨单位加总', color: '#38C8F5', target: 'stockLedger' },
|
|
|
|
|
+ { key: 'value', code: '02', label: '库存金额', value: s.inventoryValue, unit: '元', reason: s.valuationReason, info: '按移动加权、不含税成本价计算;当前缺少可核实成本数据源', color: '#9B7DF7', target: 'stockLedger' },
|
|
|
|
|
+ { key: 'turnover', code: '03', label: '库存周转天数', value: s.turnoverDays, unit: '天', reason: s.turnoverReason, info: '本月截至昨日的平均库存与实际消耗估算,历史不足时无法计算', color: s.turnoverDays == null ? '#9FB8D4' : s.turnoverDays <= 45 ? '#3BD48A' : s.turnoverDays <= 60 ? '#F0A03A' : '#F25E5E', status: s.turnoverDays == null ? '' : s.turnoverDays <= 45 ? '达标' : s.turnoverDays <= 60 ? '超目标' : '周转偏慢', target: 'turnover' },
|
|
|
|
|
+ { key: 'stagnant', code: '04', label: '呆滞品占比', value: s.stagnantRate, unit: '%', reason: s.stagnantReason, info: '首次入库及最近实际消耗均超过 180 天的批次数占当前在库批次总数的比例;日期不完整时无法核实', color: s.stagnantRate == null ? '#9FB8D4' : s.stagnantRate >= 10 ? '#F25E5E' : s.stagnantRate >= 8 ? '#F0A03A' : '#3BD48A', status: s.stagnantRate == null ? '' : s.stagnantRate >= 10 ? '呆滞预警' : s.stagnantRate >= 8 ? '接近阈值' : '正常', progress: s.stagnantRate == null ? null : Math.min(100, Number(s.stagnantRate) * 10), target: 'dead' },
|
|
|
|
|
+ { key: 'quality', code: '06', label: '质检通过率', value: s.qualityPassRate, unit: '%', reason: s.qualityReason, info: '本月已检批次中合格批次占比,让步计分母但不计合格', color: s.qualityPassRate == null ? '#9FB8D4' : s.qualityPassRate >= 98 ? '#3BD48A' : s.qualityPassRate >= 95 ? '#F0A03A' : '#F25E5E', status: s.qualityPassRate == null ? '' : s.qualityPassRate >= 98 ? '达标' : s.qualityPassRate >= 95 ? '接近阈值' : '低于目标', progress: s.qualityPassRate, target: 'qc' }
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
methods: {
|
|
methods: {
|
|
|
display: displayNumber,
|
|
display: displayNumber,
|
|
|
|
|
+ displayKpi(item) {
|
|
|
|
|
+ if (item.value == null) return '—';
|
|
|
|
|
+ const value = Number(item.value);
|
|
|
|
|
+ return Number.isFinite(value) ? value.toLocaleString('zh-CN', item.unit === '%' ? { minimumFractionDigits: 1, maximumFractionDigits: 1 } : { maximumFractionDigits: 0 }) : String(item.value);
|
|
|
|
|
+ },
|
|
|
|
|
+ activityTime(value) {
|
|
|
|
|
+ if (!value) return '—';
|
|
|
|
|
+ const date = new Date(value);
|
|
|
|
|
+ if (Number.isNaN(date.getTime())) return String(value).slice(0, 16);
|
|
|
|
|
+ const today = new Date();
|
|
|
|
|
+ const day = date.toDateString();
|
|
|
|
|
+ return day === today.toDateString() ? String(date.getHours()).padStart(2, '0') + ':' + String(date.getMinutes()).padStart(2, '0')
|
|
|
|
|
+ : day === new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1).toDateString() ? '昨日' : String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0');
|
|
|
|
|
+ },
|
|
|
trendSelect(row) { this.$emit('open', 'io', { startTime: row.date + ' 00:00:00', endTime: row.date + ' 23:59:59' }); },
|
|
trendSelect(row) { this.$emit('open', 'io', { startTime: row.date + ' 00:00:00', endTime: row.date + ' 23:59:59' }); },
|
|
|
- categorySelect(row) { if (row.id === 'other' || !row.id && !row.categoryId) return this.$message.info('其他物料为合并分组,请在库存台账选择具体物料'); this.$emit('open', 'ledger', { categoryId: row.id || row.categoryId }); },
|
|
|
|
|
|
|
+ categorySelect() { this.$emit('open', 'stockLedger'); },
|
|
|
ageSelect(row) { this.$emit('open', 'age', { age: row.key }); }
|
|
ageSelect(row) { this.$emit('open', 'age', { age: row.key }); }
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|