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';
|
import type { MapPen } from '@api/map';
|
||||||
|
|
||||||
function roundedRectPath(
|
// 预加载锁定图标(仅一次)
|
||||||
ctx: CanvasRenderingContext2D,
|
const lockedIcon = new Image();
|
||||||
x: number,
|
lockedIcon.src = new URL('../../assets/icons/png/locked.png', import.meta.url).toString();
|
||||||
y: number,
|
|
||||||
w: number,
|
function roundedRectPath(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
|
||||||
h: number,
|
|
||||||
r: number,
|
|
||||||
) {
|
|
||||||
const rr = Math.max(0, Math.min(r, Math.min(w, h) / 2));
|
const rr = Math.max(0, Math.min(r, Math.min(w, h) / 2));
|
||||||
ctx.moveTo(x + rr, y);
|
ctx.moveTo(x + rr, y);
|
||||||
ctx.lineTo(x + w - rr, y);
|
ctx.lineTo(x + w - rr, y);
|
||||||
@ -27,7 +24,10 @@ function roundedRectPath(
|
|||||||
export function drawStorageGrid(
|
export function drawStorageGrid(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
pen: MapPen,
|
pen: MapPen,
|
||||||
opts?: { fontFamily?: string },
|
opts?: {
|
||||||
|
fontFamily?: string;
|
||||||
|
stateResolver?: (penId: string, layerName: string) => { occupied?: boolean; locked?: boolean } | undefined;
|
||||||
|
},
|
||||||
): void {
|
): void {
|
||||||
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
|
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
|
||||||
const assoc: string[] = pen.point?.associatedStorageLocations || [];
|
const assoc: string[] = pen.point?.associatedStorageLocations || [];
|
||||||
@ -58,17 +58,41 @@ export function drawStorageGrid(
|
|||||||
|
|
||||||
// 前 5 个格子 + 最后一格可能显示 +N
|
// 前 5 个格子 + 最后一格可能显示 +N
|
||||||
const slots = assoc.slice(0, gridCols * gridRows);
|
const slots = assoc.slice(0, gridCols * gridRows);
|
||||||
slots.forEach((_, i) => {
|
slots.forEach((layerName, i) => {
|
||||||
const r = Math.floor(i / gridCols);
|
const r = Math.floor(i / gridCols);
|
||||||
const c = i % gridCols;
|
const c = i % gridCols;
|
||||||
const cx = gx + c * (cell + gap);
|
const cx = gx + c * (cell + gap);
|
||||||
const cy = gy + r * (cell + gap);
|
const cy = gy + r * (cell + gap);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
roundedRectPath(ctx, cx, cy, cell, cell, Math.min(4, cell * 0.3));
|
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.fill();
|
||||||
ctx.strokeStyle = '#999999';
|
ctx.strokeStyle = '#999999';
|
||||||
ctx.stroke();
|
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);
|
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 { AreaOperationService } from './area-operation.service';
|
||||||
import { drawStorageGrid } from './draw/storage-location-drawer';
|
import { drawStorageGrid } from './draw/storage-location-drawer';
|
||||||
import { LayerManagerService } from './layer-manager.service';
|
import { LayerManagerService } from './layer-manager.service';
|
||||||
|
import { storageStateMap } from './storage-location.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 场景编辑器服务类
|
* 场景编辑器服务类
|
||||||
@ -1547,7 +1548,18 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
|||||||
|
|
||||||
// 库位2x3栅格:动作点在画布上直接绘制(静态数据)
|
// 库位2x3栅格:动作点在画布上直接绘制(静态数据)
|
||||||
if (type === MapPointType.动作点) {
|
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();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,10 @@ import type { EditorService } from '@core/editor.service';
|
|||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
import { type Ref, ref } from 'vue';
|
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连接、状态更新和画布点颜色管理
|
* 负责管理库位数据、WebSocket连接、状态更新和画布点颜色管理
|
||||||
@ -13,12 +17,26 @@ export class StorageLocationService {
|
|||||||
private storageLocations: Ref<Map<string, StorageLocationInfo[]>> = ref(new Map());
|
private storageLocations: Ref<Map<string, StorageLocationInfo[]>> = ref(new Map());
|
||||||
private editor: EditorService | null = null;
|
private editor: EditorService | null = null;
|
||||||
private sceneId: string = '';
|
private sceneId: string = '';
|
||||||
|
// 渲染调度标记,避免在高频事件中重复 render
|
||||||
|
private renderScheduled = false;
|
||||||
|
|
||||||
constructor(editor: EditorService, sceneId: string) {
|
constructor(editor: EditorService, sceneId: string) {
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
this.sceneId = sceneId;
|
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组织库位数据
|
// 按画布点ID组织库位数据
|
||||||
const locationsByPointId = new Map<string, StorageLocationInfo[]>();
|
const locationsByPointId = new Map<string, StorageLocationInfo[]>();
|
||||||
message.data.storage_locations.forEach((location) => {
|
message.data.storage_locations.forEach((location) => {
|
||||||
const byOperateId = location.operate_point_id; // 直接对应动作点ID
|
const byStationName = stationToPointIdMap.get(location.station_name); // 优先使用画布上的 pen.id
|
||||||
const byStationName = stationToPointIdMap.get(location.station_name);
|
const byOperateId = location.operate_point_id; // 后端UUID,仅作兜底
|
||||||
const pointId = byOperateId || byStationName;
|
const pointId = byStationName || byOperateId;
|
||||||
|
|
||||||
if (pointId) {
|
if (pointId) {
|
||||||
if (!locationsByPointId.has(pointId)) {
|
if (!locationsByPointId.has(pointId)) {
|
||||||
@ -137,8 +155,20 @@ export class StorageLocationService {
|
|||||||
|
|
||||||
this.storageLocations.value = locationsByPointId;
|
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.updatePointBorderColors();
|
||||||
|
// 批量更新后触发一次重绘
|
||||||
|
this.scheduleRender();
|
||||||
} else if (message.type === 'storage_location_status_change') {
|
} else if (message.type === 'storage_location_status_change') {
|
||||||
// 处理单个库位状态变化
|
// 处理单个库位状态变化
|
||||||
const { new_status } = message;
|
const { new_status } = message;
|
||||||
@ -146,6 +176,28 @@ export class StorageLocationService {
|
|||||||
if (pointId) {
|
if (pointId) {
|
||||||
this.updateStorageLocationInMap(pointId, new_status.id, new_status);
|
this.updateStorageLocationInMap(pointId, new_status.id, new_status);
|
||||||
this.updatePointBorderColor(pointId);
|
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 {
|
try {
|
||||||
const message = <StorageLocationMessage>JSON.parse(e.data || '{}');
|
const message = <StorageLocationMessage>JSON.parse(e.data || '{}');
|
||||||
this.handleStorageLocationUpdate(message);
|
this.handleStorageLocationUpdate(message);
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.debug('处理库位状态消息失败:', error);
|
// 忽略解析错误,避免打断消息循环
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user