Compare commits

..

2 Commits

Author SHA1 Message Date
kkfluous
cfe79cace2 fix(assets): correct modal filtering for 待交车/库存-其他/本周X
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
三个弹窗筛选问题一起修:

1. 待交车 drill-in:Pending 原本错归入 weekly-detail(该接口不支持
   model/batch/location 过滤),改走 /list 并给 /list 的 category 分支
   补上 'Pending' 状态匹配。

2. 库存-其他:'其他' 同时存在于两个体系——资产表的"库存-其他"
   (mapRegion 结果) vs 区域统计的"其他"(mapMacroRegion 结果),
   过滤语义完全不同。引入 source 参数由前端传递,source==='asset'
   时按 v.location 匹配(库存语义),否则按 mapMacroRegion(宏观区域)。
   抽取 filterByLocation 辅助函数供 /list 与 /weekly-detail 共用。

3. 本周交车/还车/替换:/weekly-detail 接口新增 model/batch/location/source
   过滤;前端 fetchWeeklyDetail 签名扩容。实现方式:SQL 结果与缓存
   车辆集(按过滤条件筛)按 truck_id 取交集。

4. BIGINT 精度丢失:DELIVERED_SQL / RETURNED_SQL / REPLACED_SQL 及
   pending/new 子查询原本使用裸 truck.id,mysql2 驱动把 BIGINT 当
   JS Number 返回,大 id (>2^53) 尾部被截,导致 truck_id 交集永远
   为空。全部改为 CAST(truck.id AS CHAR),与 MAIN_SQL 保持一致。

5. fetchVehicleList 类型补上 source,避免前端传的 source 被 URLSearchParams
   构造时静默丢弃。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:00:45 +08:00
kkfluous
9ea2f306c4 feat(dev): env-gated local auth bypass for development
.env 里设 DEV_BYPASS_AUTH=1 + VITE_DEV_BYPASS_AUTH=1 即可本地免登录调试。
前端判定强制要求 import.meta.env.DEV,避免生产构建误启用。
后端塞入 dev 身份(含 所有权限 / BI-SCHEDULE-OPT 角色),保证 c.get('user')
下游依赖不会 crash。

新增 src/vite-env.d.ts 引入 vite/client 类型以访问 import.meta.env。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:00:30 +08:00
7 changed files with 104 additions and 26 deletions

View File

@@ -36,6 +36,23 @@ export default function AuthProvider({ children }: { children: ReactNode }) {
}, []); }, []);
async function authenticate() { async function authenticate() {
// 本地开发免登录开关:.env 里设 VITE_DEV_BYPASS_AUTH=1 启用,仅 dev 生效
if (import.meta.env.DEV && import.meta.env.VITE_DEV_BYPASS_AUTH === '1') {
setState({
isLoading: false,
isAuthenticated: true,
user: {
userId: 'dev-local',
userName: '本地开发',
permissionLevel: 'full',
depName: '',
roles: ['所有权限', 'BI-SCHEDULE-OPT'],
},
error: null,
});
return;
}
// 1. 检查 sessionStorage 中是否有 JWT // 1. 检查 sessionStorage 中是否有 JWT
const savedToken = sessionStorage.getItem('bi_jwt'); const savedToken = sessionStorage.getItem('bi_jwt');
if (savedToken) { if (savedToken) {
@@ -65,8 +82,7 @@ export default function AuthProvider({ children }: { children: ReactNode }) {
const jumpToken = params.get('jumpToken'); const jumpToken = params.get('jumpToken');
if (!jumpToken) { if (!jumpToken) {
// 演示模式:无 token 时直接放行 setState({ isLoading: false, isAuthenticated: false, user: null, error: '请从业务系统跳转访问' });
setState({ isLoading: false, isAuthenticated: true, user: null, error: null });
return; return;
} }

View File

@@ -71,8 +71,7 @@ export function Shell({ modules }: { modules: ModuleConfig[] }) {
}, [user]); }, [user]);
return ( return (
<DemoModeProvider enabled={true}> <DemoModeProvider enabled={false}>
<div className="flex min-h-screen"> <div className="flex min-h-screen">
{/* 全局水印 */} {/* 全局水印 */}
<div className="fixed inset-0 pointer-events-none z-[9999] overflow-hidden" style={{ opacity: 0.06 }}> <div className="fixed inset-0 pointer-events-none z-[9999] overflow-hidden" style={{ opacity: 0.06 }}>

View File

@@ -223,11 +223,18 @@ export default function AssetsModule() {
setModalLoading(true); setModalLoading(true);
const cat = showPlateNumbers.category; const cat = showPlateNumbers.category;
// Weekly categories use the dedicated weekly-detail endpoint // Weekly categories use the dedicated weekly-detail endpoint.
const weeklyTypes: Record<string, string> = { Delivered: 'delivered', Returned: 'returned', Replaced: 'replaced', Pending: 'pending' }; // Pending 不属于 weeklyweekly-detail 不支持 model/batch/location 过滤,
// 走下面的 /list 路径才能按型号/区域等维度过滤。
const weeklyTypes: Record<string, string> = { Delivered: 'delivered', Returned: 'returned', Replaced: 'replaced' };
if (cat && weeklyTypes[cat]) { if (cat && weeklyTypes[cat]) {
setModalVehicles([]); setModalVehicles([]);
fetchWeeklyDetail(weeklyTypes[cat]) fetchWeeklyDetail(weeklyTypes[cat], {
model: showPlateNumbers.model,
batch: showPlateNumbers.batch,
location: showPlateNumbers.location,
source: showPlateNumbers.source,
})
.then(setModalWeeklyDetail) .then(setModalWeeklyDetail)
.catch(() => setModalWeeklyDetail([])) .catch(() => setModalWeeklyDetail([]))
.finally(() => setModalLoading(false)); .finally(() => setModalLoading(false));
@@ -241,8 +248,10 @@ export default function AssetsModule() {
if (showPlateNumbers.batch !== 'All') params.batch = showPlateNumbers.batch; if (showPlateNumbers.batch !== 'All') params.batch = showPlateNumbers.batch;
if (showPlateNumbers.model !== 'All') params.model = showPlateNumbers.model; if (showPlateNumbers.model !== 'All') params.model = showPlateNumbers.model;
if (showPlateNumbers.location !== 'All') params.location = showPlateNumbers.location; if (showPlateNumbers.location !== 'All') params.location = showPlateNumbers.location;
if (showPlateNumbers.source) params.source = showPlateNumbers.source;
if (cat === 'Inventory') params.category = 'Inventory'; if (cat === 'Inventory') params.category = 'Inventory';
if (cat === 'Operating') params.category = 'Operating'; if (cat === 'Operating') params.category = 'Operating';
if (cat === 'Pending') params.category = 'Pending';
if (showPlateNumbers.manager) params.manager = showPlateNumbers.manager; if (showPlateNumbers.manager) params.manager = showPlateNumbers.manager;
if (showPlateNumbers.customer) params.customer = showPlateNumbers.customer; if (showPlateNumbers.customer) params.customer = showPlateNumbers.customer;
if (showPlateNumbers.department) params.department = showPlateNumbers.department; if (showPlateNumbers.department) params.department = showPlateNumbers.department;

View File

@@ -50,6 +50,7 @@ export async function fetchVehicleList(params: {
department?: string; department?: string;
attendance?: string; attendance?: string;
subject?: string | null; subject?: string | null;
source?: string;
}): Promise<VehicleListItem[]> { }): Promise<VehicleListItem[]> {
const query = new URLSearchParams(); const query = new URLSearchParams();
if (params.batch) query.set('batch', params.batch); if (params.batch) query.set('batch', params.batch);
@@ -65,6 +66,7 @@ export async function fetchVehicleList(params: {
if (params.department) query.set('department', params.department); if (params.department) query.set('department', params.department);
if (params.attendance) query.set('attendance', params.attendance); if (params.attendance) query.set('attendance', params.attendance);
if (params.subject) query.set('subject', params.subject); if (params.subject) query.set('subject', params.subject);
if (params.source) query.set('source', params.source);
return fetchJson<VehicleListItem[]>(`${BASE}/list?${query.toString()}`); return fetchJson<VehicleListItem[]>(`${BASE}/list?${query.toString()}`);
} }
@@ -112,6 +114,14 @@ export async function fetchRegionChart(
); );
} }
export async function fetchWeeklyDetail(type: string): Promise<WeeklyDetailItem[]> { export async function fetchWeeklyDetail(
return fetchJson<WeeklyDetailItem[]>(`${BASE}/weekly-detail?type=${type}`); type: string,
filters?: { model?: string; batch?: string; location?: string; source?: string },
): Promise<WeeklyDetailItem[]> {
const params = new URLSearchParams({ type });
if (filters?.model && filters.model !== 'All') params.set('model', filters.model);
if (filters?.batch && filters.batch !== 'All') params.set('batch', filters.batch);
if (filters?.location && filters.location !== 'All') params.set('location', filters.location);
if (filters?.source) params.set('source', filters.source);
return fetchJson<WeeklyDetailItem[]>(`${BASE}/weekly-detail?${params.toString()}`);
} }

View File

@@ -4,8 +4,8 @@ import type { JwtPayload, AuthUser } from './types.js';
const JWT_SECRET = process.env.JWT_SECRET || 'ln-bi-default-secret'; const JWT_SECRET = process.env.JWT_SECRET || 'ln-bi-default-secret';
// 演示分支:跳过所有认证(保留完整逻辑便于快速恢复) // 临时:跳过所有认证(保留完整逻辑便于快速恢复)
const BYPASS_AUTH = true; const BYPASS_AUTH = false;
export async function authMiddleware(c: Context, next: Next) { export async function authMiddleware(c: Context, next: Next) {
const path = c.req.path; const path = c.req.path;
@@ -14,6 +14,21 @@ export async function authMiddleware(c: Context, next: Next) {
return next(); return next();
} }
// 本地开发免登录开关:.env 里设 DEV_BYPASS_AUTH=1 启用
if (process.env.DEV_BYPASS_AUTH === '1') {
const devUser: AuthUser = {
userId: 'dev-local',
userName: '本地开发',
loginName: 'dev-local',
depCode: '',
depName: '',
permissionLevel: 'full',
roles: ['所有权限', 'BI-SCHEDULE-OPT'],
};
c.set('user', devUser);
return next();
}
// 跳过不需要认证的路径 // 跳过不需要认证的路径
if (path === '/api/health' || path.startsWith('/api/auth/')) { if (path === '/api/health' || path.startsWith('/api/auth/')) {
return next(); return next();

View File

@@ -420,7 +420,7 @@ interface WeeklyStats {
// 交车单 SQL // 交车单 SQL
const DELIVERED_SQL = `SELECT const DELIVERED_SQL = `SELECT
take.id, DATE(take.handover_date) AS handover_date, take.id, DATE(take.handover_date) AS handover_date,
truck.id AS truck_id, truck.plate_number, CAST(truck.id AS CHAR) AS truck_id, truck.plate_number,
dic_contract_type.dic_name AS contract_type, dic_contract_type.dic_name AS contract_type,
customer.customer_name customer.customer_name
FROM tab_truck_rent_take take FROM tab_truck_rent_take take
@@ -439,7 +439,7 @@ WHERE take.is_deleted = 0 AND take.take_name IS NOT NULL
// 还车单 SQL // 还车单 SQL
const RETURNED_SQL = `SELECT const RETURNED_SQL = `SELECT
r.id, DATE(r.return_date) AS handover_date, r.id, DATE(r.return_date) AS handover_date,
truck.id AS truck_id, truck.plate_number, CAST(truck.id AS CHAR) AS truck_id, truck.plate_number,
dic_contract_type.dic_name AS contract_type, dic_contract_type.dic_name AS contract_type,
customer.customer_name customer.customer_name
FROM tab_truck_rent_return r FROM tab_truck_rent_return r
@@ -457,7 +457,7 @@ WHERE r.is_deleted = 0 AND r.return_date IS NOT NULL`;
// 替换车单 SQL // 替换车单 SQL
const REPLACED_SQL = `SELECT const REPLACED_SQL = `SELECT
take.id, DATE(take.handover_date) AS handover_date, take.id, DATE(take.handover_date) AS handover_date,
truck.id AS truck_id, truck.plate_number, CAST(truck.id AS CHAR) AS truck_id, truck.plate_number,
dic_contract_type.dic_name AS contract_type, dic_contract_type.dic_name AS contract_type,
customer.customer_name customer.customer_name
FROM tab_truck_rent_take take FROM tab_truck_rent_take take
@@ -880,6 +880,21 @@ app.get('/customer-stats', async (c) => {
return c.json(result); return c.json(result);
}); });
// Location 过滤器:支持展示区域(嘉兴/广东/北京/新疆/其他)、库存区域(江浙沪/其它)、
// 城市(嘉兴市)、宏观区域(华东/华南/...)。
// '其他' 在两个体系里都存在(资产表的"库存-其他" vs 区域表的"其他"宏观区域),
// 用 source 区分source==='asset' 时按 v.location 匹配,其它情况按宏观区域匹配。
function filterByLocation(vehicles: Vehicle[], location: string, source?: string): Vehicle[] {
const macroRegions = ['华东', '华南', '华北', '华中', '西南', '西北'];
const isMacro = macroRegions.includes(location) || (location === '其他' && source !== 'asset');
if (isMacro) {
return vehicles.filter((v) => mapMacroRegion(v.province, v.city) === location);
}
const inventoryRegionMap: Record<string, string> = { '江浙沪': '嘉兴', '其它': '其他' };
const mappedLocation = inventoryRegionMap[location] || location;
return vehicles.filter((v) => v.location === mappedLocation || v.city === location || resolveCity(v.city, v.province) === location);
}
// Vehicle type filter map (same logic as /by-type) // Vehicle type filter map (same logic as /by-type)
const VEHICLE_TYPE_FILTERS: Record<string, (v: Vehicle) => boolean> = { const VEHICLE_TYPE_FILTERS: Record<string, (v: Vehicle) => boolean> = {
'4.5T普货': (v) => v.type === '4.5T' && !v.model.includes('冷链'), '4.5T普货': (v) => v.type === '4.5T' && !v.model.includes('冷链'),
@@ -925,15 +940,7 @@ app.get('/list', async (c) => {
filtered = filtered.filter((v) => v.model === model); filtered = filtered.filter((v) => v.model === model);
} }
if (location && location !== 'All') { if (location && location !== 'All') {
// Support: display regions (嘉兴/广东), inventory regions (江浙沪), cities (嘉兴市), macro regions (华东/华南) filtered = filterByLocation(filtered, location, c.req.query('source'));
const macroRegions = ['华东', '华南', '华北', '华中', '西南', '西北'];
if (macroRegions.includes(location) || location === '其他') {
filtered = filtered.filter((v) => mapMacroRegion(v.province, v.city) === location);
} else {
const inventoryRegionMap: Record<string, string> = { '江浙沪': '嘉兴', '其它': '其他' };
const mappedLocation = inventoryRegionMap[location] || location;
filtered = filtered.filter((v) => v.location === mappedLocation || v.city === location || resolveCity(v.city, v.province) === location);
}
} }
if (status && status !== 'All') { if (status && status !== 'All') {
filtered = filtered.filter((v) => v.status === status); filtered = filtered.filter((v) => v.status === status);
@@ -943,6 +950,8 @@ app.get('/list', async (c) => {
filtered = filtered.filter((v) => v.status === 'Inventory' || v.status === 'Abnormal'); filtered = filtered.filter((v) => v.status === 'Inventory' || v.status === 'Abnormal');
} else if (category === 'Operating') { } else if (category === 'Operating') {
filtered = filtered.filter((v) => v.status === 'Operating'); filtered = filtered.filter((v) => v.status === 'Operating');
} else if (category === 'Pending') {
filtered = filtered.filter((v) => v.status === 'Pending');
} }
} }
if (manager) { if (manager) {
@@ -1023,8 +1032,11 @@ app.get('/inventory-stats', async (c) => {
}); });
// GET /api/vehicles/weekly-detail?type=delivered|returned|replaced|pending // GET /api/vehicles/weekly-detail?type=delivered|returned|replaced|pending
// Optional filters: model, batch, location, source — 按缓存车辆集合的 truck_id 交集过滤
app.get('/weekly-detail', async (c) => { app.get('/weekly-detail', async (c) => {
const type = c.req.query('type'); const type = c.req.query('type');
const { model, batch, location } = c.req.query();
const source = c.req.query('source');
let sql: string; let sql: string;
if (type === 'delivered') { if (type === 'delivered') {
sql = `${DELIVERED_SQL} AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL} ORDER BY take.handover_date DESC`; sql = `${DELIVERED_SQL} AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL} ORDER BY take.handover_date DESC`;
@@ -1033,17 +1045,33 @@ app.get('/weekly-detail', async (c) => {
} else if (type === 'replaced') { } else if (type === 'replaced') {
sql = `${REPLACED_SQL} AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL} ORDER BY take.handover_date DESC`; sql = `${REPLACED_SQL} AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL} ORDER BY take.handover_date DESC`;
} else if (type === 'pending') { } else if (type === 'pending') {
sql = `SELECT truck.id AS truck_id, truck.plate_number, NULL AS handover_date, NULL AS contract_type, NULL AS customer_name sql = `SELECT CAST(truck.id AS CHAR) AS truck_id, truck.plate_number, NULL AS handover_date, NULL AS contract_type, NULL AS customer_name
FROM tab_truck truck WHERE truck.is_deleted=0 AND truck.is_operation=1 AND truck.truck_rent_status=7`; FROM tab_truck truck WHERE truck.is_deleted=0 AND truck.is_operation=1 AND truck.truck_rent_status=7`;
} else if (type === 'new') { } else if (type === 'new') {
sql = `SELECT truck.id AS truck_id, truck.plate_number, truck.create_time AS handover_date, NULL AS contract_type, NULL AS customer_name sql = `SELECT CAST(truck.id AS CHAR) AS truck_id, truck.plate_number, truck.create_time AS handover_date, NULL AS contract_type, NULL AS customer_name
FROM tab_truck truck WHERE truck.is_deleted=0 AND truck.is_operation=1 FROM tab_truck truck WHERE truck.is_deleted=0 AND truck.is_operation=1
AND truck.create_time >= ${WEEK_START_SQL} AND truck.create_time < ${WEEK_END_SQL} ORDER BY truck.create_time DESC`; AND truck.create_time >= ${WEEK_START_SQL} AND truck.create_time < ${WEEK_END_SQL} ORDER BY truck.create_time DESC`;
} else { } else {
return c.json([]); return c.json([]);
} }
const [rows] = await pool.query<any[]>(sql); const [rows] = await pool.query<any[]>(sql);
const masked = (rows as any[]).map(r => ({ ...r, customer_name: maskCustomerName(r.customer_name) })); let result = rows as any[];
// 按型号/批次/区域过滤:借助缓存车辆集,取 truck_id 交集
const hasModelFilter = model && model !== 'All';
const hasBatchFilter = batch && batch !== 'All';
const hasLocationFilter = location && location !== 'All';
if (hasModelFilter || hasBatchFilter || hasLocationFilter) {
const vehicles = await getVehiclesForUser(c);
let pool2 = vehicles;
if (hasModelFilter) pool2 = pool2.filter((v) => v.model === model);
if (hasBatchFilter) pool2 = pool2.filter((v) => (v.contractNo || '未知') === batch);
if (hasLocationFilter) pool2 = filterByLocation(pool2, location, source);
const truckSet = new Set(pool2.map((v) => String(v.id)));
result = result.filter((r: any) => truckSet.has(String(r.truck_id)));
}
const masked = result.map(r => ({ ...r, customer_name: maskCustomerName(r.customer_name) }));
return c.json(masked); return c.json(masked);
}); });

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />