feat:新增多库位渲染,已经库位状态的显示

This commit is contained in:
xudan 2025-09-03 18:47:45 +08:00
parent 27b611c941
commit 510fb7e5d0
4 changed files with 105 additions and 17 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

View File

@ -1,13 +1,10 @@
import type { MapPen } from '@api/map';
function roundedRectPath(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number,
r: number,
) {
// 预加载锁定图标(仅一次)
const lockedIcon = new Image();
lockedIcon.src = new URL('../../assets/icons/png/locked.png', import.meta.url).toString();
function roundedRectPath(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
const rr = Math.max(0, Math.min(r, Math.min(w, h) / 2));
ctx.moveTo(x + rr, y);
ctx.lineTo(x + w - rr, y);
@ -27,7 +24,10 @@ function roundedRectPath(
export function drawStorageGrid(
ctx: CanvasRenderingContext2D,
pen: MapPen,
opts?: { fontFamily?: string },
opts?: {
fontFamily?: string;
stateResolver?: (penId: string, layerName: string) => { occupied?: boolean; locked?: boolean } | undefined;
},
): void {
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
const assoc: string[] = pen.point?.associatedStorageLocations || [];
@ -58,17 +58,41 @@ export function drawStorageGrid(
// 前 5 个格子 + 最后一格可能显示 +N
const slots = assoc.slice(0, gridCols * gridRows);
slots.forEach((_, i) => {
slots.forEach((layerName, i) => {
const r = Math.floor(i / gridCols);
const c = i % gridCols;
const cx = gx + c * (cell + gap);
const cy = gy + r * (cell + gap);
ctx.beginPath();
roundedRectPath(ctx, cx, cy, cell, cell, Math.min(4, cell * 0.3));
ctx.fillStyle = '#f5f5f5';
// 解析状态
const state = opts?.stateResolver?.(pen.id!, layerName) ?? {};
const occupied = !!state.occupied;
const locked = !!state.locked;
// 填充颜色:占用为红色,否则默认灰底
ctx.fillStyle = occupied ? '#ff4d4f' : '#f5f5f5';
ctx.fill();
ctx.strokeStyle = '#999999';
ctx.stroke();
// 若锁定,绘制锁图标角标(尽量不阻塞:未加载完成则跳过)
if (locked && lockedIcon.complete && lockedIcon.naturalWidth > 0) {
const pad = Math.max(1, Math.floor(cell * 0.08));
// 图标尺寸不超过 cell 内可用空间,且有最小可见阈值
const maxIcon = Math.max(0, cell - 2 * pad);
const iconSize = Math.min(14, Math.floor(cell * 0.7), maxIcon);
// 将绘制范围裁剪到当前格子,确保不会溢出
ctx.save();
ctx.beginPath();
roundedRectPath(ctx, cx, cy, cell, cell, Math.min(4, cell * 0.3));
ctx.clip();
// 放在格子左上角
ctx.drawImage(lockedIcon, cx + pad, cy + pad, iconSize, iconSize);
ctx.restore();
}
});
const overflow = Math.max(0, assoc.length - gridCols * gridRows);

View File

@ -32,6 +32,7 @@ import { reactive, watch } from 'vue';
import { AreaOperationService } from './area-operation.service';
import { drawStorageGrid } from './draw/storage-location-drawer';
import { LayerManagerService } from './layer-manager.service';
import { storageStateMap } from './storage-location.service';
/**
*
@ -1547,7 +1548,18 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
// 库位2x3栅格动作点在画布上直接绘制静态数据
if (type === MapPointType.) {
drawStorageGrid(ctx, pen, { fontFamily });
drawStorageGrid(ctx, pen, {
fontFamily,
stateResolver: (penId: string, layerName: string) => {
const inner = storageStateMap.get(penId);
if (!inner) {
return {};
}
const state = inner.get(layerName);
return state ?? {};
},
});
}
ctx.restore();
}

View File

@ -4,6 +4,10 @@ import type { EditorService } from '@core/editor.service';
import { isNil } from 'lodash-es';
import { type Ref, ref } from 'vue';
// 提供给绘制层快速查询的全局状态映射pointId -> (layerName -> { occupied, locked })
export type StorageState = { occupied?: boolean; locked?: boolean };
export const storageStateMap = new Map<string, Map<string, StorageState>>();
/**
*
* WebSocket连接
@ -13,12 +17,26 @@ export class StorageLocationService {
private storageLocations: Ref<Map<string, StorageLocationInfo[]>> = ref(new Map());
private editor: EditorService | null = null;
private sceneId: string = '';
// 渲染调度标记,避免在高频事件中重复 render
private renderScheduled = false;
constructor(editor: EditorService, sceneId: string) {
this.editor = editor;
this.sceneId = sceneId;
}
/**
* render()
*/
private scheduleRender() {
if (this.renderScheduled) return;
this.renderScheduled = true;
requestAnimationFrame(() => {
this.renderScheduled = false;
this.editor?.render();
});
}
/**
*
*/
@ -123,9 +141,9 @@ export class StorageLocationService {
// 按画布点ID组织库位数据
const locationsByPointId = new Map<string, StorageLocationInfo[]>();
message.data.storage_locations.forEach((location) => {
const byOperateId = location.operate_point_id; // 直接对应动作点ID
const byStationName = stationToPointIdMap.get(location.station_name);
const pointId = byOperateId || byStationName;
const byStationName = stationToPointIdMap.get(location.station_name); // 优先使用画布上的 pen.id
const byOperateId = location.operate_point_id; // 后端UUID仅作兜底
const pointId = byStationName || byOperateId;
if (pointId) {
if (!locationsByPointId.has(pointId)) {
@ -137,8 +155,20 @@ export class StorageLocationService {
this.storageLocations.value = locationsByPointId;
// 同步维护 storageStateMap
storageStateMap.clear();
for (const [pointId, list] of locationsByPointId.entries()) {
const inner = new Map<string, StorageState>();
list.forEach((loc) => {
inner.set(loc.layer_name, { occupied: loc.is_occupied, locked: loc.is_locked });
});
storageStateMap.set(pointId, inner);
}
// 更新动作点的边框颜色
this.updatePointBorderColors();
// 批量更新后触发一次重绘
this.scheduleRender();
} else if (message.type === 'storage_location_status_change') {
// 处理单个库位状态变化
const { new_status } = message;
@ -146,6 +176,28 @@ export class StorageLocationService {
if (pointId) {
this.updateStorageLocationInMap(pointId, new_status.id, new_status);
this.updatePointBorderColor(pointId);
// 更新 storageStateMap 对应项
const inner = storageStateMap.get(pointId) ?? new Map<string, StorageState>();
// 兼容不同消息结构:优先消息顶层 layer_name其次从缓存反查
let layerName: string | undefined;
const msgLoose = message as unknown as { layer_name?: string };
if (typeof msgLoose.layer_name === 'string') {
layerName = msgLoose.layer_name;
}
if (!layerName) {
const cached = this.storageLocations.value.get(pointId)?.find((loc) => loc.id === new_status.id);
layerName = cached?.layer_name;
}
if (layerName) {
inner.set(layerName, {
occupied: new_status.is_occupied,
locked: new_status.is_locked,
});
storageStateMap.set(pointId, inner);
}
// 单条状态更新后触发重绘
this.scheduleRender();
}
}
}
@ -165,8 +217,8 @@ export class StorageLocationService {
try {
const message = <StorageLocationMessage>JSON.parse(e.data || '{}');
this.handleStorageLocationUpdate(message);
} catch (error) {
console.debug('处理库位状态消息失败:', error);
} catch {
// 忽略解析错误,避免打断消息循环
}
};