Files
ln-bi/src/server/routes/vehicles/model.ts
T

255 lines
13 KiB
TypeScript

import type { Vehicle, VehicleRow } from '../../types.js';
import type { VehicleTypeCategory } from './types.js';
export const REGIONS = ['嘉兴', '广东', '北京', '新疆', '其他'] as const;
export const INVENTORY_REGIONS = ['江浙沪', '广东', '新疆', '其它'] as const;
export function mapRegion(province: string | null, city: string | null): string {
if (!province && !city) return '其他';
const loc = (city || province || '').trim();
if (loc.includes('嘉兴') || loc.includes('浙江') || loc.includes('上海') || loc.includes('江苏')) return '嘉兴';
if (loc.includes('广东') || loc.includes('广州') || loc.includes('深圳') || loc.includes('佛山') || loc.includes('东莞')) return '广东';
if (loc.includes('北京')) return '北京';
if (loc.includes('新疆') || loc.includes('乌鲁木齐')) return '新疆';
const prov = (province || '').trim();
if (prov.includes('浙江') || prov.includes('上海') || prov.includes('江苏')) return '嘉兴';
if (prov.includes('广东')) return '广东';
if (prov.includes('北京')) return '北京';
if (prov.includes('新疆')) return '新疆';
return '其他';
}
export function mapInventoryRegion(region: string): string {
if (region === '嘉兴') return '江浙沪';
if (region === '广东') return '广东';
if (region === '新疆') return '新疆';
return '其它';
}
export function mapMacroRegion(province: string | null, city: string | null): string {
const prov = (province || '').trim();
const cityName = (city || '').trim();
const location = prov + cityName;
if (/上海|江苏|浙江|安徽|福建|江西|山东|南京|杭州|合肥|济南|青岛|苏州|宁波|厦门|嘉兴|无锡/.test(location)) return '华东';
if (/广东|广西|海南|广州|深圳|佛山|东莞|珠海|惠州|中山|南宁/.test(location)) return '华南';
if (/北京|天津|河北|山西|内蒙古|石家庄|太原|呼和浩特/.test(location)) return '华北';
if (/河南|湖北|湖南|郑州|武汉|长沙/.test(location)) return '华中';
if (/重庆|四川|贵州|云南|西藏|成都|昆明|贵阳/.test(location)) return '西南';
if (/陕西|甘肃|青海|宁夏|新疆|西安|兰州|乌鲁木齐/.test(location)) return '西北';
return '其他';
}
export function classifyVehicleType(vehicle: Vehicle): VehicleTypeCategory {
if (vehicle.type === '4.5T' && !vehicle.model.includes('冷链')) return 't4_5';
if (vehicle.type === '4.5T' && vehicle.model.includes('冷链')) return 't4_5c';
if (vehicle.type === '18T') return 't18';
if (vehicle.type === '49T') return 't49';
if (vehicle.type === '挂车' || vehicle.model.includes('挂车')) return 'trailer';
return 'other';
}
// vehicle_status has priority because pending (4) and abnormal (14) override operation_status.
// operation_status: 1=租赁, 2=自营, 3=可运营, 4=待运营, 5=退出运营.
export function mapStatus(operationStatus: string | null, vehicleStatus: string | null): Vehicle['status'] {
const operation = (operationStatus || '').trim();
const vehicle = (vehicleStatus || '').trim();
if (vehicle === '4') return 'Pending';
if (vehicle === '14') return 'Abnormal';
if (operation === '1' || operation === '2') return 'Operating';
if (operation === '3' || operation === '4') return 'Inventory';
if (operation === '租赁' || operation === '自营') return 'Operating';
if (operation === '可运营' || operation === '待运营' || operation === '在库') return 'Inventory';
if (operation === '异动') return 'Abnormal';
return 'Inventory';
}
export function mapOwnership(rentStatusLabel: string | null): string {
if (!rentStatusLabel) return 'Unknown';
const status = rentStatusLabel.trim();
if (status === '0') return 'Self';
if (status === '1') return 'Hanging';
if (status === '2') return 'Leased';
if (status === '3') return 'Self';
if (status === '自有') return 'Self';
if (status === '外租') return 'Leased';
if (status === '自营') return 'Self';
if (status === '租赁') return 'Leased';
if (status === '挂靠') return 'Hanging';
return 'Unknown';
}
export function resolveCity(city: string | null, province: string | null): string {
const cityName = (city || '').replace(/[\[\]]/g, '').trim();
if (cityName) return cityName;
const provinceName = (province || '').replace(/[\[\]]/g, '').trim();
return provinceName || '其他';
}
// vehicle_type is the new-system category code: 1=4.5T, 3=牵引车头, 5/6=挂车.
export function deriveType(modelLabel: string | null, vehicleTypeCode: string | null): string {
const label = (modelLabel || '').trim();
const code = (vehicleTypeCode || '').trim();
if (label.includes('半挂车') || code === '5' || code === '6') return '挂车';
if (label.includes('4.5吨')) return '4.5T';
if (label.includes('18吨')) return '18T';
if (label.includes('49吨')) return '49T';
if (label.includes('35吨')) return '35T';
if (code === '1') return '4.5T';
if (code === '3') return '49T';
if (label.includes('叉车')) return '叉车';
return '其他车型';
}
export function normalizeModelLabel(modelLabel: string | null): string | null {
const label = (modelLabel || '').trim();
if (label === '帕力安牌4.5吨冷链车') return '4.5吨冷链车';
if (label === '帕力安牌18吨双飞翼货车') return '18吨双飞翼货车';
if (label === '海格牌18吨双飞翼货车') return '18吨双飞翼货车';
return label || null;
}
// Key format: brand-model-color[+rentCompany when ownership is 外租].
// Multiple source tags intentionally share aliases and order values to merge dirty or equivalent data.
const MODEL_ALIAS_MAP: Record<string, { alias: string; order: number }> = {
// 4.5T 普货
'现代-4.5吨货车-白色广州开发区交投氢能运营管理有限公司': { alias: '现代4.5T普货(交投)', order: 101 },
'现代-4.5吨货车-白恒运': { alias: '现代4.5T普货(恒运)', order: 102 },
'现代-4.5吨货车-白色恒运': { alias: '现代4.5T普货(恒运)', order: 102 },
'现代-4.5吨货车-白色': { alias: '现代4.5T普货', order: 101 },
'现代-4.5吨货车-白': { alias: '现代4.5T普货', order: 101 },
// 4.5T 冷链
'帕力安牌-4.5吨冷链车-白色广州开发区交投氢能运营管理有限公司': { alias: '现代4.5T冷链(交投)', order: 201 },
'帕力安牌-4.5吨冷链车-白色': { alias: '现代4.5T冷链(羚牛)', order: 202 },
'跃进-4.5吨冷链车-白/绿/灰': { alias: '跃进4.5T冷链', order: 203 },
// 18T
'飞驰-18吨厢式货车-红': { alias: '飞驰18T(红车)', order: 301 },
'飞驰-18吨厢式货车-白/绿': { alias: '飞驰18T(白车)', order: 302 },
'楚风-18吨厢式货车-白': { alias: '楚风18T厢货', order: 303 },
'苏龙-18吨双飞翼货车-白': { alias: '苏龙18T飞翼', order: 304 },
'苏龙-18吨双飞翼货车-白色': { alias: '苏龙18T飞翼', order: 304 },
'苏龙-18吨双飞翼货车-白安吉天地物流科技有限公司': { alias: '苏龙18T飞翼(安吉)', order: 305 },
'帕力安牌-18吨双飞翼货车-白': { alias: '现代18T双飞翼(羚牛)', order: 306 },
'帕力安牌-18吨双飞翼货车-白/绿': { alias: '现代18T双飞翼(羚牛)', order: 306 },
// 49T
'宇通-49吨牵引车头-白': { alias: '49T宇通', order: 401 },
'飞驰-49吨牵引车头-白/蓝/绿': { alias: '49T飞驰', order: 402 },
'飞驰-49吨牵引车头-白/蓝/绿嘉兴氢能产业发展股份有限公司': { alias: '49T飞驰(嘉氢)', order: 403 },
'飞驰-49吨牵引车头-红': { alias: '49T飞驰(红)', order: 402 },
'飞驰-49吨牵引车头-红嘉兴氢能产业发展股份有限公司': { alias: '49T飞驰(嘉氢)', order: 403 },
'飞驰-49吨牵引车头-红浙江氢能产业发展有限公司': { alias: '49T飞驰(浙氢-红)', order: 404 },
'飞驰-49吨牵引车头-白/蓝/绿浙江氢能产业发展有限公司': { alias: '49T飞驰(浙氢-蓝白绿)', order: 405 },
'楚风-49吨牵引车头-蓝/黑': { alias: '49T楚风', order: 406 },
'楚风-49吨牵引车头-蓝/黑海珀特科技(北京)有限公司': { alias: '49T楚风(海珀特)', order: 406 },
// 其他
'红岩-35吨牵引车头-红色': { alias: '35T油车', order: 501 },
'其他-氢能叉车-蓝白绿': { alias: '氢能叉车', order: 502 },
'通华-重型集装箱半挂车-红色浙江锦昌仓储有限公司': { alias: '挂车', order: 503 },
'通华-重型集装箱半挂车-红色嘉兴市鼎义物流有限公司': { alias: '挂车', order: 503 },
'通华-重型集装箱半挂车-红色': { alias: '挂车', order: 503 },
'大通-重型集装箱半挂车-红色': { alias: '挂车', order: 503 },
'明威-重型集装箱半挂车-红色': { alias: '挂车', order: 503 },
'明威-重型集装箱半挂车-红': { alias: '挂车', order: 503 },
'万风-重型平板半挂车-红': { alias: '挂车', order: 503 },
'舒捷-SJ型蓄电池观光车-蓝白': { alias: '观光车', order: 504 },
'东风-挂靠油车-白色': { alias: '公务车/挂靠车', order: 505 },
'东风-挂靠油车-白': { alias: '公务车/挂靠车', order: 505 },
'腾势-公务用车/小客车-黑': { alias: '公务车/挂靠车', order: 505 },
'腾势-公务用车/小客车-白': { alias: '公务车/挂靠车', order: 505 },
'其他-公务用车/小客车-蓝色': { alias: '公务车/挂靠车', order: 505 },
'其他-公务用车/小客车-灰': { alias: '公务车/挂靠车', order: 505 },
'远程牌-公务用车/小客车-白': { alias: '公务车/挂靠车', order: 505 },
'大通-公务用车/小客车-灰': { alias: '公务车/挂靠车', order: 505 },
};
export function deriveModelTag(
brandLabel: string | null,
modelLabel: string | null,
color: string | null,
ownershipLabel: string | null,
rentCompany: string | null,
): string {
const brand = (brandLabel || '').trim();
const model = (normalizeModelLabel(modelLabel) || '').trim();
const vehicleColor = (color || '').trim();
if (model === '公务用车/小客车') return '公务车/挂靠车';
const company = ownershipLabel?.trim() === '外租' ? (rentCompany || '').trim() : '';
if (!brand && !model) return '未知车型';
const tag = `${brand}-${model}-${vehicleColor}${company}`;
return MODEL_ALIAS_MAP[tag]?.alias || tag;
}
export function getModelOrder(model: string): number {
for (const entry of Object.values(MODEL_ALIAS_MAP)) {
if (entry.alias === model) return entry.order;
}
return 999;
}
export function compareModelNames(left: string, right: string): number {
return getModelOrder(left) - getModelOrder(right);
}
export function transformRow(row: VehicleRow): Vehicle {
const region = mapRegion(row., row.);
return {
id: row.id,
plateNumber: row.车牌号 || '',
vin: row.vin || '',
type: deriveType(row.车辆型号Label, row.车辆类型参数),
model: deriveModelTag(row.车辆品牌Label, row.车辆型号Label, row.车辆颜色, row.车辆归属状态Label, row.租赁公司),
color: row.车辆颜色 || '',
location: region,
region,
province: row.省,
city: row.市,
status: mapStatus(row.车辆租赁状态Label, row.车辆租赁状态),
operationStatus: row.车辆租赁状态Label,
ownership: mapOwnership(row.车辆归属状态Label),
rentCompany: row.租赁公司 || '',
contractNo: row.合同编码,
customerName: row.客户名称,
orgName: row.合同归属公司,
departmentName: row.合同归属部门,
subjectOrg: row.主体,
projectName: row.项目名称,
customerManager: row.客户经理,
managerId: row.经理ID || null,
brandLabel: row.车辆品牌Label,
};
}
export const SUMMARY_TYPE_FILTERS: ReadonlyArray<{ name: string; filter: (vehicle: Vehicle) => boolean }> = [
{ name: '4.5T普货', filter: (vehicle) => vehicle.type === '4.5T' && !vehicle.model.includes('冷链') },
{ name: '4.5T冷链', filter: (vehicle) => vehicle.type === '4.5T' && vehicle.model.includes('冷链') },
{ name: '18T', filter: (vehicle) => vehicle.type === '18T' },
{ name: '49T', filter: (vehicle) => vehicle.type === '49T' },
{ name: '其他', filter: (vehicle) => !['4.5T', '18T', '49T'].includes(vehicle.type) },
];
// List filtering keeps trailers separate, while summary grouping intentionally includes them in “其他”.
export const VEHICLE_TYPE_FILTERS: Record<string, (vehicle: Vehicle) => boolean> = {
'4.5T普货': (vehicle) => vehicle.type === '4.5T' && !vehicle.model.includes('冷链'),
'4.5T冷链': (vehicle) => vehicle.type === '4.5T' && vehicle.model.includes('冷链'),
'18T': (vehicle) => vehicle.type === '18T',
'49T': (vehicle) => vehicle.type === '49T',
'挂车': (vehicle) => vehicle.type === '挂车' || vehicle.model.includes('挂车'),
'其他': (vehicle) => classifyVehicleType(vehicle) === 'other',
};
export function filterByLocation(vehicles: Vehicle[], location: string, source?: string): Vehicle[] {
const macroRegions = ['华东', '华南', '华北', '华中', '西南', '西北'];
const isMacro = macroRegions.includes(location) || (location === '其他' && source !== 'asset');
if (isMacro) {
return vehicles.filter((vehicle) => mapMacroRegion(vehicle.province, vehicle.city) === location);
}
const inventoryRegionMap: Record<string, string> = { '江浙沪': '嘉兴', '其它': '其他' };
const mappedLocation = inventoryRegionMap[location] || location;
return vehicles.filter(
(vehicle) => vehicle.location === mappedLocation
|| vehicle.city === location
|| resolveCity(vehicle.city, vehicle.province) === location,
);
}