Compare commits
2 Commits
a472e543ce
...
cfe79cace2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfe79cace2 | ||
|
|
9ea2f306c4 |
@@ -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) {
|
||||||
|
|||||||
@@ -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 不属于 weekly:weekly-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;
|
||||||
|
|||||||
@@ -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()}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
Reference in New Issue
Block a user