fix: align asset customer data with OneOS and refine statistics display
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
shishengliang
2026-09-20 09:51:31 +08:00
parent 746edeb72c
commit 8fb574599b
8 changed files with 56 additions and 86 deletions
+3 -3
View File
@@ -34,7 +34,7 @@ import {
deriveInventoryView, deriveInventoryView,
deriveModalVehicleView, deriveModalVehicleView,
formatLocalDateTime, formatLocalDateTime,
getWeeklyFlowRange, getRecentFlowRange,
selectFlowDetails, selectFlowDetails,
type VehicleModalSelection, type VehicleModalSelection,
type FlowSelection, type FlowSelection,
@@ -84,10 +84,10 @@ export default function AssetsModule() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [lastUpdate, setLastUpdate] = useState<string>(() => formatLocalDateTime(new Date())); const [lastUpdate, setLastUpdate] = useState<string>(() => formatLocalDateTime(new Date()));
const [modalLoading, setModalLoading] = useState(false); const [modalLoading, setModalLoading] = useState(false);
const [flowRange, setFlowRange] = useState(() => getWeeklyFlowRange()); const [flowRange, setFlowRange] = useState(() => getRecentFlowRange());
const [flowStats, setFlowStats] = useState<FlowStatsResponse | null>(null); const [flowStats, setFlowStats] = useState<FlowStatsResponse | null>(null);
const [flowLoading, setFlowLoading] = useState(false); const [flowLoading, setFlowLoading] = useState(false);
const [flowDailyExpanded, setFlowDailyExpanded] = useState(false); const [flowDailyExpanded, setFlowDailyExpanded] = useState(true);
const [selectedFlow, setSelectedFlow] = useState<FlowSelection | null>(null); const [selectedFlow, setSelectedFlow] = useState<FlowSelection | null>(null);
// Dept/Region/Customer data // Dept/Region/Customer data
+5 -41
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react'; import React from 'react';
import { ChevronDown, Filter, Loader2, Search } from 'lucide-react'; import { ChevronDown, Filter, Loader2, Search } from 'lucide-react';
import { motion } from 'motion/react'; import { motion } from 'motion/react';
import type { SubjectOption } from '../api'; import type { SubjectOption } from '../api';
@@ -30,42 +30,6 @@ const TABS = [
{ id: 'customer', label: '按客户' }, { id: 'customer', label: '按客户' },
]; ];
function MarqueeBanner() {
const trackRef = useRef<HTMLDivElement>(null);
const innerRef = useRef<HTMLDivElement>(null);
const [overflow, setOverflow] = useState(false);
useEffect(() => {
const check = () => {
if (!trackRef.current || !innerRef.current) return;
setOverflow(innerRef.current.scrollWidth > trackRef.current.clientWidth);
};
check();
const ro = new ResizeObserver(check);
ro.observe(trackRef.current!);
return () => ro.disconnect();
}, []);
const text = '车辆资产已于 2026 年 6 月 18 日完成“运营状态”与“业务关联”校验';
return (
<div className="relative -mx-6 mb-4 bg-green-50 border-y border-green-200">
<div ref={trackRef} className="overflow-hidden">
<div className={`flex w-max py-2 ${overflow ? 'animate-marquee' : 'w-full justify-center'}`}>
<span ref={innerRef} className="inline-block whitespace-nowrap px-6 text-xs text-green-700 font-medium">
{text}
</span>
{overflow && (
<span className="inline-block whitespace-nowrap px-6 text-xs text-green-700 font-medium">
{text}
</span>
)}
</div>
</div>
</div>
);
}
export function AssetsHeader({ export function AssetsHeader({
activeTab, activeTab,
setActiveTab, setActiveTab,
@@ -246,16 +210,16 @@ export function AssetsHeader({
))} ))}
</div> </div>
{/* Status row */} {/* Status row */}
<div className="flex items-center justify-center gap-4 py-1.5 text-[10px] text-gray-400"> <div className="flex flex-wrap items-center justify-center gap-x-1 gap-y-0.5 px-3 py-1.5 text-[10px] text-gray-400">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="w-1 h-1 rounded-full bg-blue-400 inline-block" /> <span className="w-1 h-1 rounded-full bg-blue-400 inline-block" />
: {lastUpdate} {lastUpdate.slice(0, 16)}
</div> </div>
<span aria-hidden="true"></span>
<span className="whitespace-nowrap">当日17:00</span>
</div> </div>
</div> </div>
{/* OneOS 迁移提示滚动条 */}
<MarqueeBanner />
</> </>
); );
} }
@@ -1,6 +1,6 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { ArrowRight, CalendarDays, Check, ChevronDown, ChevronLeft, ChevronRight, X } from 'lucide-react'; import { ArrowRight, CalendarDays, Check, ChevronDown, ChevronLeft, ChevronRight, X } from 'lucide-react';
import { getWeeklyFlowRange, type DateRange } from '../../model'; import { getRecentFlowRange, getWeeklyFlowRange, type DateRange } from '../../model';
const weekdays = ['一', '二', '三', '四', '五', '六', '日']; const weekdays = ['一', '二', '三', '四', '五', '六', '日'];
const ymd = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; const ymd = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
@@ -25,7 +25,7 @@ export function FlowDateRangePicker({ value, onChange }: { value: DateRange; onC
{ label: '今天', range: { start: today, end: today } }, { label: '今天', range: { start: today, end: today } },
{ label: '昨天', range: { start: ymd(shiftDay(new Date(), -1)), end: ymd(shiftDay(new Date(), -1)) } }, { label: '昨天', range: { start: ymd(shiftDay(new Date(), -1)), end: ymd(shiftDay(new Date(), -1)) } },
{ label: '业务周', range: getWeeklyFlowRange() }, { label: '业务周', range: getWeeklyFlowRange() },
{ label: '近 7 天', range: { start: ymd(shiftDay(new Date(), -6)), end: today } }, { label: '近 7 天', range: getRecentFlowRange() },
{ label: '近 30 天', range: { start: ymd(shiftDay(new Date(), -29)), end: today } }, { label: '近 30 天', range: { start: ymd(shiftDay(new Date(), -29)), end: today } },
]; ];
const open = () => { const open = () => {
@@ -86,7 +86,7 @@ export function FlowStatisticsCard({
{flowLoading && ( {flowLoading && (
<div className="rounded-xl bg-slate-50 px-3 py-4 text-center text-[11px] font-bold text-slate-400">...</div> <div className="rounded-xl bg-slate-50 px-3 py-4 text-center text-[11px] font-bold text-slate-400">...</div>
)} )}
{!flowLoading && flowStats?.daily.map((day) => ( {!flowLoading && flowStats?.daily.slice().sort((a, b) => b.date.localeCompare(a.date)).map((day) => (
<div key={day.date} className="rounded-2xl border border-slate-100 bg-white px-3 py-3 shadow-sm"> <div key={day.date} className="rounded-2xl border border-slate-100 bg-white px-3 py-3 shadow-sm">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-[11px] font-black text-slate-400">{day.date}</div> <div className="text-[11px] font-black text-slate-400">{day.date}</div>
+7
View File
@@ -126,6 +126,13 @@ function addDays(date: Date, days: number): Date {
} }
// 资产流转周报按周六至周五统计;周末查看时仍停留在刚结束的周五。 // 资产流转周报按周六至周五统计;周末查看时仍停留在刚结束的周五。
export function getRecentFlowRange(referenceDate = new Date()): DateRange {
return {
start: formatLocalDate(addDays(referenceDate, -6)),
end: formatLocalDate(referenceDate),
};
}
export function getWeeklyFlowRange(referenceDate = new Date()): DateRange { export function getWeeklyFlowRange(referenceDate = new Date()): DateRange {
const day = referenceDate.getDay(); const day = referenceDate.getDay();
const end = day === 6 const end = day === 6
+6 -19
View File
@@ -1,13 +1,14 @@
import pool from '../../db.js'; import pool from '../../db.js';
import { LATEST_VEHICLE_LEASE_JOIN_SQL } from '../../vehicle-lease-sql.js';
import type { VehicleInfoRow } from './types.js'; import type { VehicleInfoRow } from './types.js';
/** 车辆关联信息 SQL(客户名、部门、经理、租赁状态、主体、项目、品牌) */ /** 车辆关联信息 SQL(客户名、部门、经理、租赁状态、主体、项目、品牌) */
export const VEHICLE_INFO_SQL = `SELECT export const VEHICLE_INFO_SQL = `SELECT
vi.plate_number AS plate, vi.plate_number AS plate,
vi.vin AS vin, vi.vin AS vin,
COALESCE(c.customer_name, vor.customer_name, ci.customer_name) AS customer, vor.customer_name AS customer,
COALESCE(c.business_department_name, vor.business_dept) AS department, vor.business_dept AS department,
COALESCE(c.business_manager_name, vor.business_manager) AS manager, vor.business_manager AS manager,
CAST(COALESCE(c.business_manager_id, vi.business_id) AS CHAR) AS manager_id, CAST(COALESCE(c.business_manager_id, vi.business_id) AS CHAR) AS manager_id,
CASE vs.operation_status CASE vs.operation_status
WHEN '1' THEN '租赁' WHEN '1' THEN '租赁'
@@ -18,7 +19,7 @@ export const VEHICLE_INFO_SQL = `SELECT
ELSE vs.operation_status ELSE vs.operation_status
END AS rent_status, END AS rent_status,
NULLIF(vi.registered_ownership, '') AS entity, NULLIF(vi.registered_ownership, '') AS entity,
COALESCE(c.project_name, vor.project_name) AS project, vor.project_name AS project,
COALESCE(vi_operation_city.NAME, NULLIF(vi.operation_city, '')) AS inventory_location, COALESCE(vi_operation_city.NAME, NULLIF(vi.operation_city, '')) AS inventory_location,
CASE vm.brand CASE vm.brand
WHEN 'hyundai' THEN CASE WHEN vm.model LIKE '%帕力安%' OR vm.model LIKE '%冷链%' OR vm.model LIKE '%双飞翼%' THEN '帕力安牌' ELSE '现代' END WHEN 'hyundai' THEN CASE WHEN vm.model LIKE '%帕力安%' OR vm.model LIKE '%冷链%' OR vm.model LIKE '%双飞翼%' THEN '帕力安牌' ELSE '现代' END
@@ -50,21 +51,7 @@ LEFT JOIN vehicle_model vm
LEFT JOIN common_district vi_operation_city LEFT JOIN common_district vi_operation_city
ON vi_operation_city.CODE = vi.operation_city COLLATE utf8mb4_unicode_ci ON vi_operation_city.CODE = vi.operation_city COLLATE utf8mb4_unicode_ci
AND vi_operation_city.STATUS = 'VALID' AND vi_operation_city.STATUS = 'VALID'
LEFT JOIN vehicle_lease_order_record vor ${LATEST_VEHICLE_LEASE_JOIN_SQL}
ON vor.vehicle_id = vi.id
AND vor.del_flag = '0'
AND vor.id = (
SELECT MAX(vor2.id)
FROM vehicle_lease_order_record vor2
WHERE vor2.vehicle_id = vi.id
AND vor2.del_flag = '0'
)
LEFT JOIN vehicle_lease_contract_info c
ON c.order_id = vor.contract_id
AND c.del_flag = '0'
LEFT JOIN customer_info ci
ON ci.id = vi.customer_id
AND ci.del_flag = '0'
WHERE vi.del_flag = '0' WHERE vi.del_flag = '0'
AND COALESCE(vs.operation_status, '') <> '5'`; AND COALESCE(vs.operation_status, '') <> '5'`;
+7 -20
View File
@@ -1,4 +1,5 @@
import pool from '../../db.js'; import pool from '../../db.js';
import { LATEST_VEHICLE_LEASE_JOIN_SQL } from '../../vehicle-lease-sql.js';
import type { Vehicle, VehicleRow } from '../../types.js'; import type { Vehicle, VehicleRow } from '../../types.js';
import { transformRow } from './model.js'; import { transformRow } from './model.js';
import type { WeeklyTruckIds } from './types.js'; import type { WeeklyTruckIds } from './types.js';
@@ -80,13 +81,13 @@ export const MAIN_SQL = `SELECT
ELSE vm.brand ELSE vm.brand
END AS 车辆品牌Label, END AS 车辆品牌Label,
c.id AS 合同ID, c.id AS 合同ID,
COALESCE(c.contract_code, vor.contract_code, vi.contract_code) AS 合同编码, vor.contract_code AS 合同编码,
COALESCE(c.customer_name, vor.customer_name, ci.customer_name) AS 客户名称, vor.customer_name AS 客户名称,
c.signing_company AS 合同归属公司, c.signing_company AS 合同归属公司,
COALESCE(c.business_department_name, vor.business_dept) AS 合同归属部门, vor.business_dept AS 合同归属部门,
NULLIF(vi.registered_ownership, '') AS 主体, NULLIF(vi.registered_ownership, '') AS 主体,
COALESCE(c.project_name, vor.project_name) AS 项目名称, vor.project_name AS 项目名称,
COALESCE(c.business_manager_name, vor.business_manager) AS 客户经理, vor.business_manager AS 客户经理,
CAST(COALESCE(c.business_manager_id, vi.business_id) AS CHAR) AS 经理ID CAST(COALESCE(c.business_manager_id, vi.business_id) AS CHAR) AS 经理ID
FROM vehicle_info vi FROM vehicle_info vi
LEFT JOIN vehicle_status vs LEFT JOIN vehicle_status vs
@@ -113,21 +114,7 @@ LEFT JOIN common_district vi_city
LEFT JOIN common_district vi_operation_city LEFT JOIN common_district vi_operation_city
ON vi_operation_city.CODE = vi.operation_city COLLATE utf8mb4_unicode_ci ON vi_operation_city.CODE = vi.operation_city COLLATE utf8mb4_unicode_ci
AND vi_operation_city.STATUS = 'VALID' AND vi_operation_city.STATUS = 'VALID'
LEFT JOIN vehicle_lease_order_record vor ${LATEST_VEHICLE_LEASE_JOIN_SQL}
ON vor.vehicle_id = vi.id
AND vor.del_flag = '0'
AND vor.id = (
SELECT MAX(vor2.id)
FROM vehicle_lease_order_record vor2
WHERE vor2.vehicle_id = vi.id
AND vor2.del_flag = '0'
)
LEFT JOIN vehicle_lease_contract_info c
ON c.order_id = vor.contract_id
AND c.del_flag = '0'
LEFT JOIN customer_info ci
ON ci.id = vi.customer_id
AND ci.del_flag = '0'
WHERE vi.del_flag = '0' WHERE vi.del_flag = '0'
AND COALESCE(vs.operation_status, '') <> '5'`; AND COALESCE(vs.operation_status, '') <> '5'`;
+25
View File
@@ -0,0 +1,25 @@
/**
* 与 OneOS VehicleInfoMapper.LatestVehicleLeaseJoin 保持一致。
* vi 为车辆表;vor 为当前业务记录;c 为合同。
* 还车会清空 vor 的客户与合同信息,调用方不得用历史档案补回空值。
*/
export const LATEST_VEHICLE_LEASE_JOIN_SQL = `LEFT JOIN (
SELECT ranked_record.*
FROM (
SELECT source_record.*,
ROW_NUMBER() OVER (
PARTITION BY source_record.vehicle_id
ORDER BY COALESCE(
source_record.last_delivery_time,
source_record.last_return_time,
source_record.update_time
) DESC, source_record.id DESC
) AS lease_rn
FROM vehicle_lease_order_record source_record
WHERE source_record.del_flag = '0'
) ranked_record
WHERE ranked_record.lease_rn = 1
) vor ON vor.vehicle_id = vi.id
LEFT JOIN vehicle_lease_contract_info c
ON c.id = vor.contract_id
AND c.del_flag = '0'`;