feat: 资产总览新增所属公司筛选,支持按归属主体过滤全页数据
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

- 后端:新增 /api/vehicles/subjects 端点返回公司列表+台数预览;所有聚合端点接受 ?subject= 参数按 tab_truck.org_id 对应的主体公司过滤
- 前端:标题下方新增 Scope Chip 单选下拉,支持搜索+台数预览,选中后全页 KPI/汇总/库存统计按公司联动刷新

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-15 16:50:25 +08:00
parent d6c31dd2b6
commit 820fde5547
3 changed files with 260 additions and 58 deletions

View File

@@ -313,11 +313,21 @@ async function getVehicles(): Promise<Vehicle[]> {
async function getVehiclesForUser(c: Context): Promise<Vehicle[]> {
const all = await getVehicles();
const user = ((c as any).get?.('user') || (c as any).var?.user) as AuthUser | undefined;
if (user) {
const filtered = filterByPermission(all, user);
return maskCustomerNames(filtered);
}
return maskCustomerNames(all);
let list = user ? filterByPermission(all, user) : all;
list = applySubjectFilter(c, list);
return maskCustomerNames(list);
}
// 归属公司筛选(所属公司 = tab_truck.org_id → org_name, 即 Vehicle.subjectOrg
function getSubjectParam(c: Context): string | null {
const raw = (c.req.query('subject') || '').trim();
return raw ? raw : null;
}
function applySubjectFilter(c: Context, vehicles: Vehicle[]): Vehicle[] {
const subject = getSubjectParam(c);
if (!subject) return vehicles;
return vehicles.filter((v) => (v.subjectOrg || '') === subject);
}
function getRegionCounts(vehicles: Vehicle[], regions: readonly string[]): Record<string, number> {
@@ -611,7 +621,7 @@ app.get('/by-batch', async (c) => {
// GET /api/vehicles/inventory-analysis — 库存分析,不设数据权限,对所有人开放
app.get('/inventory-analysis', async (c) => {
const vehicles = await getVehicles();
const vehicles = applySubjectFilter(c, await getVehicles());
const typeFilters = [
{ name: '4.5T普货', filter: (v: Vehicle) => v.type === '4.5T' && !v.model.includes('冷链') },
@@ -978,7 +988,7 @@ app.get('/list', async (c) => {
// GET /api/vehicles/inventory-stats — 库存统计,不设数据权限,对所有人开放
app.get('/inventory-stats', async (c) => {
const vehicles = await getVehicles();
const vehicles = applySubjectFilter(c, await getVehicles());
const inventory = vehicles.filter((v) => v.status === 'Inventory' || v.status === 'Abnormal');
const TYPE_NAME_MAP: Record<string, string> = {
@@ -1037,6 +1047,30 @@ app.get('/weekly-detail', async (c) => {
return c.json(masked);
});
// GET /api/vehicles/subjects — 归属公司列表(含台数预览),用于顶部筛选下拉
app.get('/subjects', async (c) => {
const all = await getVehicles();
const user = ((c as any).get?.('user') || (c as any).var?.user) as AuthUser | undefined;
const visible = user ? filterByPermission(all, user) : all;
const map = new Map<string, { total: number; inventory: number; operating: number }>();
for (const v of visible) {
const name = (v.subjectOrg || '').trim();
if (!name) continue;
if (!map.has(name)) map.set(name, { total: 0, inventory: 0, operating: 0 });
const s = map.get(name)!;
s.total += 1;
if (v.status === 'Inventory' || v.status === 'Abnormal') s.inventory += 1;
if (v.status === 'Operating') s.operating += 1;
}
const result = Array.from(map.entries())
.map(([name, stats]) => ({ name, ...stats }))
.sort((a, b) => b.total - a.total);
return c.json(result);
});
// GET /api/vehicles/refresh — force cache refresh
app.get('/refresh', async (c) => {
lastFetchTime = 0;