feat:新增多库位渲染,已经库位状态的显示
This commit is contained in:
parent
27b611c941
commit
510fb7e5d0
BIN
src/assets/icons/png/locked.png
Normal file
BIN
src/assets/icons/png/locked.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 573 B |
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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 {
|
||||
// 忽略解析错误,避免打断消息循环
|
||||
}
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user