Author SHA1 Message Date
shishengliang d8871964a4 chore(release): bump version to 1.1.21
ci/woodpecker/push/woodpecker Pipeline failed
2026-10-24 13:16:59 +08:00
shishengliang 1e57586a35 Merge asset history trends into feature/asset-statistics 2026-10-24 13:16:53 +08:00
shishengliang 187a72d6b0 chore(release): bump version to 1.1.20
ci/woodpecker/push/woodpecker Pipeline was successful
2026-10-22 11:06:43 +08:00
shishengliang 3657661fa7 Merge fault_1013_improve into feature/asset-statistics 2026-10-22 11:06:41 +08:00
shishengliang 00c09d7452 fix(mileage): skip missing daily mileage in interval totals
ci/woodpecker/push/woodpecker Pipeline was successful
2026-10-13 13:20:14 +08:00
shishengliang 5751d1af15 fix(assets): emphasize daily flow details and sort newest first
ci/woodpecker/push/woodpecker Pipeline was successful
2026-10-10 21:56:06 +08:00
shishengliang 24984fe033 fix(assets): show inventory operations managers and bump v1.1.19 2026-10-10 21:56:06 +08:00
shishengliang 7dd7e67602 fix(energy): connect Foshan external vehicle hydrogen orders
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-29 10:03:45 +08:00
7 changed files with 51 additions and 42 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ln-bi", "name": "ln-bi",
"version": "1.1.19", "version": "1.1.21",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ln-bi", "name": "ln-bi",
"version": "1.1.19", "version": "1.1.21",
"dependencies": { "dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1", "@amap/amap-jsapi-loader": "^1.0.1",
"@hono/node-server": "^1.13.0", "@hono/node-server": "^1.13.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "ln-bi", "name": "ln-bi",
"private": true, "private": true,
"version": "1.1.19", "version": "1.1.21",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "concurrently -n server,client -c blue,green \"npm run dev:server\" \"npm run dev:client\"", "dev": "concurrently -n server,client -c blue,green \"npm run dev:server\" \"npm run dev:client\"",
+7 -29
View File
@@ -28,6 +28,7 @@ export default function HydrogenDaily() {
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
setError(null); setError(null);
setRows(null);
const query = pick === 'custom' const query = pick === 'custom'
? { startDate: effectiveRange.start, endDate: effectiveRange.end } ? { startDate: effectiveRange.start, endDate: effectiveRange.end }
: { range: pick }; : { range: pick };
@@ -81,34 +82,13 @@ export default function HydrogenDaily() {
<div className="grid grid-cols-2 gap-3 md:grid-cols-4"> <div className="grid grid-cols-2 gap-3 md:grid-cols-4">
<MetricTile icon={Fuel} label={`${scopeLabel}加氢量`} value={totalKg.toLocaleString('zh-CN', { maximumFractionDigits: 1 })} unit="Kg" helper={rangeText} /> <MetricTile icon={Fuel} label={`${scopeLabel}加氢量`} value={totalKg.toLocaleString('zh-CN', { maximumFractionDigits: 1 })} unit="Kg" helper={rangeText} />
<MetricTile icon={Truck} label="车辆归属" value={customer === 'external' ? '外部' : '羚牛'} helper="当前筛选口径" tone="emerald" /> <MetricTile icon={Truck} label="车辆归属" value={customer === 'external' ? '外部' : '羚牛'} helper={customer === 'external' ? '佛山南海羚牛加氢站 · 未关联车辆的订单' : '当前筛选口径'} tone="emerald" />
<MetricTile icon={TrendingUp} label="有效天数" value={`${activeDays}/${rows?.length ?? 0}`} helper={`日均 ${avgKg.toLocaleString('zh-CN', { maximumFractionDigits: 1 })} Kg`} tone="amber" /> <MetricTile icon={TrendingUp} label="有效天数" value={`${activeDays}/${rows?.length ?? 0}`} helper={`日均 ${avgKg.toLocaleString('zh-CN', { maximumFractionDigits: 1 })} Kg`} tone="amber" />
<MetricTile icon={Plug} label="涉及加氢站" value={stationCount} unit="站" helper="按明细站点去重" tone="slate" /> <MetricTile icon={Plug} label="涉及加氢站" value={stationCount} unit="站" helper="按明细站点去重" tone="slate" />
</div> </div>
{/* 外部车辆:新系统数据还没准备好 */} {/* 时段加氢量柱图 */}
{customer === 'external' && rows !== null && totalKg === 0 && ( {!error && trendData.length > 0 && totalKg > 0 && (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl border border-slate-100 shadow-sm px-6 py-14 flex flex-col items-center text-center"
>
<div className="w-14 h-14 rounded-2xl bg-blue-50 flex items-center justify-center mb-3 relative">
<Plug size={22} className="text-blue-500" />
<span className="absolute -top-0.5 -right-0.5 w-3 h-3 rounded-full bg-blue-400 animate-ping" />
<span className="absolute -top-0.5 -right-0.5 w-3 h-3 rounded-full bg-blue-500" />
</div>
<div className="text-sm font-bold text-slate-700 mb-1">外部车辆 · 数据未就绪</div>
<div className="text-[11px] text-slate-400 max-w-[280px] leading-relaxed">
新系统的外部车辆加氢数据还在准备中
<br />
上线后此处将展示完整明细
</div>
</motion.div>
)}
{/* 时段加氢量柱图(外部车辆无数据时不渲染) */}
{!(customer === 'external' && totalKg === 0) && trendData.length > 0 && (
<SurfaceCard> <SurfaceCard>
<div className="flex items-center justify-between px-4 pt-4 mb-2"> <div className="flex items-center justify-between px-4 pt-4 mb-2">
<span className="text-sm font-bold text-slate-700">每日加氢量</span> <span className="text-sm font-bold text-slate-700">每日加氢量</span>
@@ -184,8 +164,7 @@ export default function HydrogenDaily() {
</SurfaceCard> </SurfaceCard>
)} )}
{/* 表格(外部车辆 + 全 0 时不渲染,由上方友好空状态替代) */} {/* 日明细:加载失败、无订单与正常数据分别展示。 */}
{!(customer === 'external' && rows !== null && totalKg === 0) && (
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden"> <div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
{/* 表头 */} {/* 表头 */}
<div className="grid grid-cols-[minmax(0,1fr)_84px_80px] md:grid-cols-[minmax(0,1fr)_140px_120px_104px] gap-2 md:gap-3 px-3 py-2 bg-slate-50 text-[11px] font-bold text-slate-500"> <div className="grid grid-cols-[minmax(0,1fr)_84px_80px] md:grid-cols-[minmax(0,1fr)_140px_120px_104px] gap-2 md:gap-3 px-3 py-2 bg-slate-50 text-[11px] font-bold text-slate-500">
@@ -206,8 +185,8 @@ export default function HydrogenDaily() {
<div className="p-3"><ErrorState message={error} /></div> <div className="p-3"><ErrorState message={error} /></div>
) : rows === null ? ( ) : rows === null ? (
<div className="p-3"><LoadingState label="正在加载加氢明细" /></div> <div className="p-3"><LoadingState label="正在加载加氢明细" /></div>
) : rows.length === 0 ? ( ) : rows.length === 0 || totalKg === 0 ? (
<div className="p-3"><EmptyState title="暂无加氢数据" description="请切换时间范围或车辆归属" /></div> <div className="p-3"><EmptyState title="所选时段暂无加氢数据" description={customer === 'external' ? '佛山南海羚牛加氢站在该时段没有未关联车辆的加氢订单,请切换时间范围' : '请切换时间范围或车辆归属'} /></div>
) : rows.map(r => { ) : rows.map(r => {
const open = expanded.has(r.date); const open = expanded.has(r.date);
const isAbnormal = Math.abs(r.chainPct) >= 0.3; const isAbnormal = Math.abs(r.chainPct) >= 0.3;
@@ -270,7 +249,6 @@ export default function HydrogenDaily() {
); );
})} })}
</div> </div>
)}
</div> </div>
); );
+1 -1
View File
@@ -39,7 +39,7 @@ export function registerHydrogenDailyRoute(
HYDROGEN_BASE_WHERE_B, HYDROGEN_BASE_WHERE_B,
`b.${HYDROGEN_LOCAL} >= '${HYDROGEN_MIN_DATE}'`, `b.${HYDROGEN_LOCAL} >= '${HYDROGEN_MIN_DATE}'`,
dateRangeClause(`b.${HYDROGEN_LOCAL}`), dateRangeClause(`b.${HYDROGEN_LOCAL}`),
customerClause(customer).replaceAll('customer_price', 'b.customer_price').replaceAll('fee_total', 'b.fee_total'), customerClause(customer),
].join(' AND '); ].join(' AND ');
// 站点级聚合(每日 × 每站)。前端组装成 day → stations // 站点级聚合(每日 × 每站)。前端组装成 day → stations
+26 -4
View File
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { DatabaseSync } from 'node:sqlite';
import { import {
customerClause, customerClause,
dateRangeClause, dateRangeClause,
@@ -7,10 +8,31 @@ import {
resolveDateRange, resolveDateRange,
} from './query-model.js'; } from './query-model.js';
test('客户范围继续让外部车辆返回空数据', () => { test('外部车辆只取佛山羚牛站未关联车辆的订单,车牌文字和收费不影响分类', () => {
assert.equal(customerClause('external'), '1=0'); const db = new DatabaseSync(':memory:');
assert.equal(customerClause('lingniu'), '1=1'); try {
assert.equal(customerClause('all'), '1=1'); db.exec(`CREATE TABLE orders (
id INTEGER, station_id INTEGER, vehicle_id INTEGER,
license_plate TEXT, customer_price REAL, del_flag TEXT
);
INSERT INTO orders VALUES
(1, 341, NULL, '粤A外部', 38, '0'),
(2, 341, NULL, '', 0, '0'),
(3, 341, 123, '粤A羚牛', 38, '0'),
(4, 52, NULL, '粤A其他站', 38, '0'),
(5, NULL, NULL, '', 0, '0'),
(6, NULL, 456, '粤A未关联站点', 0, '0'),
(7, 341, NULL, '粤A已删除', 38, '1');`);
const ids = (kind: 'external' | 'lingniu' | 'all') => db
.prepare(`SELECT b.id FROM orders b WHERE b.del_flag = '0' AND ${customerClause(kind)} ORDER BY b.id`)
.all().map(row => row.id);
assert.deepEqual(ids('external'), [1, 2]);
assert.deepEqual(ids('lingniu'), [3, 4, 5, 6]);
assert.deepEqual(ids('all'), [1, 2, 3, 4, 5, 6]);
assert.deepEqual([...ids('external'), ...ids('lingniu')].sort(), ids('all'));
} finally {
db.close();
}
}); });
test('能源日期快捷区间继续按本地自然日计算', () => { test('能源日期快捷区间继续按本地自然日计算', () => {
+7 -3
View File
@@ -8,10 +8,14 @@ export interface EnergyDateRange {
const YMD_RE = /^\d{4}-\d{2}-\d{2}$/; const YMD_RE = /^\d{4}-\d{2}-\d{2}$/;
// 新账本目前只承载羚牛车辆订单;外部车辆仍需返回空集合而不是混用羚牛数据。 // 主数据 ID 341:佛山南海羚牛加氢站。车牌文字存在不代表已关联车辆。
const FOSHAN_LINGNIU_STATION_ID = 341;
// 日明细账本使用别名 b;两类互斥,其他订单继续保留在原羚牛范围内。
export function customerClause(customer: CustomerKind): string { export function customerClause(customer: CustomerKind): string {
if (customer === 'external') return '1=0'; const external = `(COALESCE(b.station_id, 0) = ${FOSHAN_LINGNIU_STATION_ID} AND b.vehicle_id IS NULL)`;
if (customer === 'lingniu') return '1=1'; if (customer === 'external') return external;
if (customer === 'lingniu') return `NOT ${external}`;
return '1=1'; return '1=1';
} }
+7 -2
View File
@@ -105,6 +105,11 @@ test('四个处理器保持缓存和参数契约,加氢站点使用新主数
'/hydrogen/daily?startDate=2025-01-01&endDate=2025-01-02&customer=external&force=1', '/hydrogen/daily?startDate=2025-01-01&endDate=2025-01-02&customer=external&force=1',
); );
assert.equal(dailyResponse.status, 200); assert.equal(dailyResponse.status, 200);
assert.deepEqual(await dailyResponse.json(), [
{ date: '2025-01-02', totalKg: 10, chainPct: 0, customerType: 'external',
stations: [{ name: '测试站', pricePerKg: 35, kg: 10, chainPct: 0 }] },
{ date: '2025-01-01', totalKg: 0, chainPct: 0, customerType: 'external', stations: [] },
]);
const electricApp = new Hono(); const electricApp = new Hono();
const electricDependencies = { const electricDependencies = {
@@ -127,7 +132,7 @@ test('四个处理器保持缓存和参数契约,加氢站点使用新主数
{ key: 'electric/monthly?customer=external&start=2025-01-01&end=2025-01-02', force: true }, { key: 'electric/monthly?customer=external&start=2025-01-01&end=2025-01-02', force: true },
]); ]);
// 指纹来自拆分前的 13 条 SQL,连空白符变化也会使测试失败。 // 保留查询契约指纹;日明细已更新为佛山羚牛站未关联车辆的外部订单筛选。
assert.deepEqual( assert.deepEqual(
[...hydrogenCalls, ...electricCalls].map(call => sqlHash(call.sql)), [...hydrogenCalls, ...electricCalls].map(call => sqlHash(call.sql)),
[ [
@@ -138,7 +143,7 @@ test('四个处理器保持缓存和参数契约,加氢站点使用新主数
'256ac8309d5a', '256ac8309d5a',
'dac9e86fc527', 'dac9e86fc527',
'a71da8f05765', 'a71da8f05765',
'24b23780a259', 'd1620280f520',
'226fd8f950a1', '226fd8f950a1',
'6e4031b10926', '6e4031b10926',
'4fc77463294c', '4fc77463294c',