Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8871964a4 | ||
|
|
1e57586a35 | ||
|
|
187a72d6b0 | ||
|
|
3657661fa7 | ||
|
|
00c09d7452 | ||
|
|
5751d1af15 | ||
|
|
24984fe033 | ||
|
|
7dd7e67602 |
Generated
+2
-2
@@ -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
@@ -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\"",
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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('能源日期快捷区间继续按本地自然日计算', () => {
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user