feat(assets): refine inventory presentation and abnormal vehicle details
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import type { InventoryFilters, VehicleModalSelection } from '../../model';
|
||||
import type { RegionalInventoryStats } from '../../types';
|
||||
|
||||
const BRAND_ORDER = ['现代', '帕力安', '宇通', '飞驰', '跃进'];
|
||||
const normalizeBrand = (brand: string) => brand.trim().replace(/牌$/, '');
|
||||
|
||||
function compareInventoryBrand(left: string, right: string) {
|
||||
const rank = (brand: string) => {
|
||||
const normalized = normalizeBrand(brand);
|
||||
if (!normalized || normalized === '其他') return BRAND_ORDER.length + 1;
|
||||
const index = BRAND_ORDER.indexOf(normalized);
|
||||
return index < 0 ? BRAND_ORDER.length : index;
|
||||
};
|
||||
return rank(left) - rank(right);
|
||||
}
|
||||
|
||||
export function inventoryTotal(rows: RegionalInventoryStats[]) {
|
||||
return rows.reduce((total, row) => total + row.quantity, 0);
|
||||
}
|
||||
|
||||
/** Decompose display text only; never change the model used for drilldown. */
|
||||
export function inventoryModelTags(row: Pick<RegionalInventoryStats, 'type' | 'model' | 'batch'> & Partial<Pick<RegionalInventoryStats, 'brand'>>) {
|
||||
const tags: { kind: 'type' | 'model' | 'detail' | 'batch'; label: string; value: string }[] = [];
|
||||
const type = row.type.trim();
|
||||
const model = row.model.trim();
|
||||
const details = Array.from(model.matchAll(/[((]([^()()]+)[))]/g), match => match[1].trim()).filter(Boolean);
|
||||
const base = model.replace(/[((]([^()()]+)[))]/g, '').trim();
|
||||
// The type already has its own tag, so avoid repeating it in the model tag.
|
||||
let compactModel = (type ? base.replace(type, '') : base).trim();
|
||||
// Alias model names can say “现代” even when the separate brand is “帕力安牌”.
|
||||
// Strip only a leading brand, preserving body style, color and qualifiers.
|
||||
const brands = [...new Set([row.brand || '', normalizeBrand(row.brand || ''), ...BRAND_ORDER, '帕力安牌'])]
|
||||
.filter(brand => brand && brand !== '其他').sort((a, b) => b.length - a.length);
|
||||
const prefix = brands.find(brand => compactModel.startsWith(brand));
|
||||
if (prefix) compactModel = compactModel.slice(prefix.length).replace(/^[\s-]+/, '').trim();
|
||||
if (type) tags.push({ kind: 'type', label: '车型', value: type });
|
||||
if (compactModel) tags.push({ kind: 'model', label: '型号', value: compactModel });
|
||||
for (const value of new Set(details)) tags.push({ kind: 'detail', label: '型号说明', value });
|
||||
const batch = row.batch?.trim();
|
||||
// The current API aliases batch to model; only render genuinely distinct values.
|
||||
if (batch && batch !== model) tags.push({ kind: 'batch', label: '批次', value: batch });
|
||||
return tags;
|
||||
}
|
||||
|
||||
/** Preserve the displayed scope when opening a filtered total or group. */
|
||||
export function inventorySelection(filters: InventoryFilters, title: string, overrides: Partial<VehicleModalSelection> = {}): VehicleModalSelection {
|
||||
return {
|
||||
batch: 'All', model: filters.model || 'All',
|
||||
location: filters.city || filters.region || 'All',
|
||||
brand: filters.brand || undefined, vehicleType: filters.type || undefined,
|
||||
category: 'Inventory', source: 'asset', title, ...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export interface InventoryDisplayGroup {
|
||||
name: string;
|
||||
sections: [string, RegionalInventoryStats[]][];
|
||||
rows: RegionalInventoryStats[];
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
/** Group presentation only: keep each original record for quantity and drilldown. */
|
||||
export function inventoryBrandGroups(rows: RegionalInventoryStats[], groupBy: 'type' | 'model') {
|
||||
const brands = new Map<string, { brand: string; rows: RegionalInventoryStats[]; variants: Map<string, RegionalInventoryStats[]> }>();
|
||||
for (const row of rows) {
|
||||
const key = normalizeBrand(row.brand);
|
||||
let brand = brands.get(key);
|
||||
if (!brand) {
|
||||
brand = { brand: row.brand, rows: [], variants: new Map() };
|
||||
brands.set(key, brand);
|
||||
}
|
||||
brand.rows.push(row);
|
||||
const variantKey = groupBy === 'type' ? row.type : JSON.stringify([row.type, row.model, row.batch]);
|
||||
const variant = brand.variants.get(variantKey) || [];
|
||||
variant.push(row);
|
||||
brand.variants.set(variantKey, variant);
|
||||
}
|
||||
return [...brands.values()].sort((a, b) => compareInventoryBrand(a.brand, b.brand))
|
||||
.map(brand => ({ ...brand, variants: [...brand.variants.values()] }));
|
||||
}
|
||||
|
||||
export function inventoryGroups(source: Record<string, Record<string, RegionalInventoryStats[]>>, sort: 'default' | 'quantity', groupBy: 'region' | 'model' = 'region'): InventoryDisplayGroup[] {
|
||||
const groups = Object.entries(source).map(([name, children]) => {
|
||||
const sections: InventoryDisplayGroup['sections'] = Object.entries(children).map(([section, records]) => [
|
||||
section, [...records].sort((a, b) => compareInventoryBrand(a.brand, b.brand)),
|
||||
]);
|
||||
// Keep city blocks intact; in the type view order whole model blocks by brand.
|
||||
if (groupBy === 'model') sections.sort(([, a], [, b]) => compareInventoryBrand(a[0]?.brand || '', b[0]?.brand || ''));
|
||||
const rows = sections.flatMap(([, records]) => records);
|
||||
return { name, sections, rows, quantity: inventoryTotal(rows) };
|
||||
});
|
||||
return sort === 'quantity' ? groups.sort((a, b) => b.quantity - a.quantity) : groups;
|
||||
}
|
||||
Reference in New Issue
Block a user