web-map/src/components/card/point-detail-card.vue

455 lines
14 KiB
Vue
Raw Normal View History

2025-05-06 23:48:21 +08:00
<script setup lang="ts">
import { MapAreaType, type MapPen, type MapPointInfo, MapPointType, type Rect } from '@api/map';
import type { StorageLocationInfo } from '@api/scene';
2025-05-06 23:48:21 +08:00
import type { EditorService } from '@core/editor.service';
import sTheme from '@core/theme.service';
import { isNil } from 'lodash-es';
2025-05-06 23:48:21 +08:00
import { computed, inject, type InjectionKey, type ShallowRef } from 'vue';
// 库位任务相关类型定义
interface BinTaskOperation {
operation: string;
recfile?: string;
recognize?: boolean;
use_down_pgv?: boolean;
use_pgv?: boolean;
pgv_x_adjust?: boolean;
}
interface BinTaskItem {
Load?: BinTaskOperation;
Unload?: BinTaskOperation;
}
interface BinLocationProperty {
key: string;
type: string;
stringValue: string;
}
interface BinLocationItem {
className: string;
instanceName: string;
pointName: string;
pos: {
x: number;
y: number;
};
property: BinLocationProperty[];
}
interface BinLocationGroup {
binLocationList: BinLocationItem[];
}
interface BinLocationsList {
binLocationsList: BinLocationGroup[];
}
2025-05-06 23:48:21 +08:00
type Props = {
token: InjectionKey<ShallowRef<EditorService>>;
current?: string;
storageLocations?: StorageLocationInfo[]; // 当前动作点绑定的库位信息
2025-05-06 23:48:21 +08:00
};
const props = defineProps<Props>();
const editor = inject(props.token)!;
const pen = computed<MapPen | undefined>(() => editor.value.getPenById(props.current));
const point = computed<MapPointInfo | null>(() => {
2025-05-09 20:15:04 +08:00
const v = pen.value?.point;
if (!v?.type) return null;
return v;
2025-05-06 23:48:21 +08:00
});
const rect = computed<Partial<Rect>>(() => {
if (isNil(point.value)) return {};
const { x, y } = editor.value.getPointRect(pen.value!) ?? {};
return { x, y };
});
2025-05-06 23:48:21 +08:00
const bindRobot = computed<string>(
() =>
point.value?.robots
?.map((v) => editor.value.getRobotById(v)?.label)
.filter((v) => !!v)
.join('、') ?? '',
);
const bindAction = computed<string>(
() =>
point.value?.actions
2025-05-10 00:49:45 +08:00
?.map((v) => editor.value.getPenById(v))
.filter((v) => v?.point?.type === MapPointType.动作点)
.map((v) => v?.label)
2025-05-06 23:48:21 +08:00
.filter((v) => !!v)
.join('、') ?? '',
);
const mapAreas = (type: MapAreaType): string => {
const id = pen.value?.id;
if (!id) return '';
2025-05-08 00:42:08 +08:00
return editor.value
.getBoundAreas(id, 'point', type)
.map(({ label }) => label)
.filter((v) => !!v)
.join('、');
2025-05-06 23:48:21 +08:00
};
const coArea1 = computed<string>(() => mapAreas(MapAreaType.库区));
const coArea2 = computed<string>(() => mapAreas(MapAreaType.互斥区));
// 库位状态标签样式映射
const getStorageStatusTag = (location: StorageLocationInfo) => {
const tags = [];
if (location.is_occupied) {
tags.push({ text: '已占用', color: 'error' });
} else {
tags.push({ text: '未占用', color: 'success' });
}
if (location.is_locked) {
tags.push({ text: '已锁定', color: 'warning' });
} else {
tags.push({ text: '未锁定', color: 'success' });
}
if (location.is_disabled) {
tags.push({ text: '已禁用', color: 'error' });
} else {
tags.push({ text: '未禁用', color: 'success' });
}
if (location.is_empty_tray) {
tags.push({ text: '空托盘', color: 'success' });
} else {
tags.push({ text: '非空托盘', color: 'warning' });
}
return tags;
};
// 解析库位任务数据
const parseBinTask = (binTaskString: string): BinTaskItem[] => {
try {
return JSON.parse(binTaskString.replace(/\n/g, '').trim());
} catch (error) {
console.error('解析库位任务数据失败:', error);
return [];
}
};
// 获取当前动作点对应的库位任务数据
const binTaskData = computed(() => {
const rawData = editor.value.getBinLocationsList();
if (!rawData || !point.value) return [];
const currentPointName = pen.value?.label || pen.value?.id;
if (!currentPointName) return [];
// 获取库位组数据
const binLocationGroups = Array.isArray(rawData)
? (rawData as BinLocationGroup[])
: (rawData as BinLocationsList)?.binLocationsList;
if (!binLocationGroups) return [];
const allBinLocations = binLocationGroups.flatMap((group) => group.binLocationList);
return allBinLocations
.filter((item) => item.pointName === currentPointName)
.map((item) => {
const binTaskProperty = item.property.find((prop) => prop.key === 'binTask');
return {
instanceName: item.instanceName,
binTasks: binTaskProperty ? parseBinTask(binTaskProperty.stringValue) : [],
};
})
.filter((item) => item.binTasks.length > 0);
});
2025-05-06 23:48:21 +08:00
</script>
<template>
<a-card :bordered="false">
<template v-if="pen && point">
<a-row :gutter="[8, 8]">
<a-col :span="24">
<a-flex align="center" :gap="8">
<i class="icon point" />
<a-typography-text class="card-title" style="flex: auto" :content="pen.label" ellipsis />
<a-tag :bordered="false">{{ $t(MapPointType[point.type]) }}</a-tag>
</a-flex>
</a-col>
<a-col :span="24">
<a-typography-text code>{{ pen.desc || $t('暂无描述') }}</a-typography-text>
</a-col>
</a-row>
<a-list class="block mt-16">
<a-list-item>
<a-typography-text type="secondary">{{ $t('站点坐标') }}</a-typography-text>
<a-typography-text>({{ rect.x?.toFixed() }},{{ rect.y?.toFixed() }})</a-typography-text>
2025-05-06 23:48:21 +08:00
</a-list-item>
<a-list-item v-if="point.type === MapPointType.自动门点 && point.deviceId">
<a-typography-text type="secondary">{{ $t('设备ID') }}</a-typography-text>
<a-typography-text>{{ point.deviceId }}</a-typography-text>
</a-list-item>
<a-list-item v-if="point.extensionType">
<a-typography-text type="secondary">{{ $t('扩展类型') }}</a-typography-text>
<a-typography-text>{{ $t(MapPointType[point.extensionType]) }}</a-typography-text>
</a-list-item>
2025-05-06 23:48:21 +08:00
<a-list-item v-if="[MapPointType.充电点, MapPointType.停靠点].includes(point.type)">
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('绑定机器人') }}</a-typography-text>
<a-typography-text>{{ bindRobot || $t('暂无') }}</a-typography-text>
</a-flex>
</a-list-item>
<a-list-item v-if="point.type === MapPointType.停靠点">
<a-typography-text type="secondary">{{ $t('启用状态') }}</a-typography-text>
<a-typography-text>{{ point.enabled === 1 ? $t('已启用') : $t('已禁用') }}</a-typography-text>
</a-list-item>
2025-05-06 23:48:21 +08:00
<a-list-item v-if="MapPointType.等待点 === point.type">
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('绑定动作点') }}</a-typography-text>
<a-typography-text>{{ bindAction || $t('暂无') }}</a-typography-text>
</a-flex>
</a-list-item>
<a-list-item v-if="MapPointType.动作点 === point.type">
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('关联库区') }}</a-typography-text>
<a-typography-text>{{ coArea1 || $t('暂无') }}</a-typography-text>
</a-flex>
</a-list-item>
<a-list-item v-if="MapPointType.动作点 === point.type && point.associatedStorageLocations?.length">
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('库位') }}</a-typography-text>
<a-typography-text>{{ point.associatedStorageLocations.join('、') }}</a-typography-text>
</a-flex>
</a-list-item>
2025-05-06 23:48:21 +08:00
<a-list-item
v-if="
[
MapPointType.普通点,
MapPointType.电梯点,
MapPointType.自动门点,
MapPointType.等待点,
MapPointType.充电点,
MapPointType.停靠点,
MapPointType.动作点,
MapPointType.临时避让点,
].includes(point.type)
"
>
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('关联互斥区') }}</a-typography-text>
<a-typography-text>{{ coArea2 || $t('暂无') }}</a-typography-text>
</a-flex>
</a-list-item>
<a-list-item v-if="MapPointType.动作点 === point.type && storageLocations?.length">
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('库位状态') }}</a-typography-text>
<div class="storage-locations">
<div v-for="location in storageLocations" :key="location.id" class="storage-item">
<a-typography-text class="storage-name">{{ location.layer_name }}</a-typography-text>
<div class="storage-tags">
<div
v-for="tag in getStorageStatusTag(location)"
:key="tag.text"
:class="['storage-tag', `storage-tag-${tag.color}`]"
>
{{ tag.text }}
</div>
</div>
</div>
</div>
</a-flex>
</a-list-item>
<a-list-item v-if="MapPointType.动作点 === point.type && binTaskData.length">
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('库位任务配置') }}</a-typography-text>
<div class="bin-task-container">
<div v-for="binLocation in binTaskData" :key="binLocation.instanceName" class="bin-task-group">
<a-typography-text class="bin-location-name">{{ binLocation.instanceName }}</a-typography-text>
<div class="bin-tasks">
<div v-for="(task, taskIndex) in binLocation.binTasks" :key="taskIndex" class="bin-task-item">
<div v-if="task.Load" class="task-operation">
<a-typography-text class="operation-title">装载</a-typography-text>
<div class="operation-details">
<div v-for="(value, key) in task.Load" :key="key" class="detail-item">
<span class="detail-key">{{ key }}:</span>
<span class="detail-value">
{{ typeof value === 'boolean' ? (value ? '是' : '否') : value }}
</span>
</div>
</div>
</div>
<div v-if="task.Unload" class="task-operation">
<a-typography-text class="operation-title">卸载</a-typography-text>
<div class="operation-details">
<div v-for="(value, key) in task.Unload" :key="key" class="detail-item">
<span class="detail-key">{{ key }}:</span>
<span class="detail-value">
{{ typeof value === 'boolean' ? (value ? '是' : '否') : value }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</a-flex>
</a-list-item>
2025-05-06 23:48:21 +08:00
</a-list>
</template>
<a-empty v-else :image="sTheme.empty" />
</a-card>
</template>
<style scoped lang="scss">
@use '/src/assets/themes/theme' as *;
@include themed {
.storage-locations {
.storage-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
padding: 16px;
border-radius: 4px;
&:last-child {
margin-bottom: 0;
}
.storage-name {
font-weight: 500;
flex-shrink: 0;
margin-right: 8px;
color: get-color(text1);
}
.storage-tags {
flex: 1;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 4px;
}
.storage-tag {
padding: 2px 6px;
font-size: 12px;
border-radius: 4px;
border: 1px solid;
&-error {
color: get-color(error_text);
background-color: get-color(error_bg);
border-color: get-color(error_border);
}
&-success {
color: get-color(success_text);
background-color: get-color(success_bg);
border-color: get-color(success_border);
}
&-warning {
color: get-color(warning_text);
background-color: get-color(warning_bg);
border-color: get-color(warning_border);
}
&-processing {
color: get-color(primary_text);
background-color: get-color(primary_bg);
border-color: get-color(primary_border);
}
&-default {
color: get-color(text2);
background-color: get-color(fill2);
border-color: get-color(border1);
}
}
}
}
.bin-task-container {
.bin-task-group {
margin-bottom: 16px;
padding: 12px;
border-radius: 4px;
background-color: get-color(fill1);
border: 1px solid get-color(border1);
&:last-child {
margin-bottom: 0;
}
.bin-location-name {
font-weight: 600;
color: get-color(text1);
margin-bottom: 8px;
display: block;
}
.bin-tasks {
.bin-task-item {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
.task-operation {
margin-bottom: 8px;
padding: 8px;
border-radius: 4px;
background-color: get-color(fill2);
&:last-child {
margin-bottom: 0;
}
.operation-title {
font-weight: 500;
color: get-color(primary);
margin-bottom: 6px;
display: block;
}
.operation-details {
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
font-size: 12px;
&:last-child {
margin-bottom: 0;
}
.detail-key {
color: get-color(text2);
font-weight: 500;
flex-shrink: 0;
margin-right: 8px;
}
.detail-value {
color: get-color(text1);
text-align: right;
word-break: break-all;
}
}
}
}
}
}
}
}
}
</style>