This commit is contained in:
chndfang 2025-05-08 00:42:08 +08:00
parent f8b88edcad
commit c2c1d8eb5f
8 changed files with 306 additions and 43 deletions

View File

@ -113,14 +113,17 @@
.ant-checkbox-wrapper {
align-items: center;
font: 400 14px/22px Roboto;
vertical-align: top;
color: get-color(text2);
vertical-align: top;
}
.ant-collapse.ant-collapse-ghost {
.ant-collapse {
background-color: transparent;
border-radius: 0;
& > .ant-collapse-item {
border: none;
& > .ant-collapse-header {
gap: 8px;
align-items: center;
@ -128,7 +131,6 @@
padding: 0;
font: 500 14px/22px Roboto;
color: get-color(text1);
background: get-color(fill3);
border-radius: 2px;
& > .ant-collapse-header-text {
@ -155,6 +157,22 @@
padding: 0;
}
}
}
&.ant-collapse-borderless > .ant-collapse-item {
& > .ant-collapse-header {
background: get-color(neutral3);
}
& + .ant-collapse-item {
margin-block-start: 24px;
}
}
&.ant-collapse-ghost > .ant-collapse-item {
& > .ant-collapse-header {
background: get-color(fill3);
}
& + .ant-collapse-item {
margin-block-start: 8px;
@ -271,13 +289,13 @@
box-shadow: none !important;
&.ant-input-status-error {
outline-color: rgba($color: get-color(error), $alpha: 20%) !important;
border-color: get-color(error) !important;
outline-color: rgba($color: get-color(error), $alpha: 20%) !important;
}
&:not(:disabled):focus {
outline: 2px solid rgba($color: get-color(primary), $alpha: 20%);
border-color: get-color(primary);
outline: 2px solid rgba($color: get-color(primary), $alpha: 20%);
transition: none;
}
@ -291,13 +309,13 @@
box-shadow: none !important;
&.ant-input-affix-wrapper-status-error {
outline-color: rgba($color: get-color(error), $alpha: 20%) !important;
border-color: get-color(error) !important;
outline-color: rgba($color: get-color(error), $alpha: 20%) !important;
}
&:not(.ant-input-affix-wrapper-disabled).ant-input-affix-wrapper-focused {
outline: 2px solid rgba($color: get-color(primary), $alpha: 20%);
border-color: get-color(primary);
outline: 2px solid rgba($color: get-color(primary), $alpha: 20%);
transition: none;
}
@ -306,20 +324,24 @@
}
& > .ant-input {
outline: none;
background-color: transparent;
outline: none;
}
& > .ant-input-suffix {
margin-inline-start: 12px;
font-size: 16px;
color: get-color(icon-disabled);
color: get-color(icon);
}
&.search {
padding-inline-start: 7px;
background-color: get-color(fill3);
border-color: get-color(border2);
& > .ant-input-suffix {
color: get-color(icon-disabled);
}
}
}
@ -332,13 +354,13 @@
box-shadow: none !important;
&.ant-input-number-status-error {
outline-color: rgba($color: get-color(error), $alpha: 20%) !important;
border-color: get-color(error) !important;
outline-color: rgba($color: get-color(error), $alpha: 20%) !important;
}
&:not(:disabled).ant-input-number-focused {
outline: 2px solid rgba($color: get-color(primary), $alpha: 20%);
border-color: get-color(primary);
outline: 2px solid rgba($color: get-color(primary), $alpha: 20%);
transition: none;
}
}
@ -489,8 +511,8 @@
}
&.ant-select-status-error > .ant-select-selector {
outline-color: rgba($color: get-color(error), $alpha: 20%) !important;
border-color: get-color(error) !important;
outline-color: rgba($color: get-color(error), $alpha: 20%) !important;
}
&:not(.ant-select-disabled):hover > .ant-select-selector {
@ -498,8 +520,8 @@
}
&:not(.ant-select-disabled).ant-select-focused > .ant-select-selector {
outline: 2px solid rgba($color: get-color(primary), $alpha: 20%);
border-color: get-color(primary);
outline: 2px solid rgba($color: get-color(primary), $alpha: 20%);
transition: none;
}
@ -640,8 +662,8 @@
padding: 3px 5px;
margin: 0;
font: 500 14px/22px Roboto;
vertical-align: top;
color: get-color(text1);
vertical-align: top;
background-color: transparent;
border-color: get-color(border1);
border-radius: 4px;

View File

@ -2,20 +2,22 @@
@use 'asset/icons/icon' as *;
@include themed {
.icon-btn {
.ant-btn.icon-btn {
color: get-color(text1) !important;
&.panel-btn {
color: get-color(icon);
color: get-color(icon) !important;
&:disabled {
color: get-color(icon-disabled);
color: get-color(icon-disabled) !important;
}
}
&.tool-btn {
color: get-color(icon);
color: get-color(icon) !important;
&:disabled {
color: get-color(icon-disabled);
color: get-color(icon-disabled) !important;
}
&.active {
@ -23,7 +25,7 @@
}
&:not(:disabled):hover {
background-color: get-color(bg_layout);
background-color: get-color(bg_layout) !important;
}
}
}
@ -45,7 +47,7 @@
background-color: currentcolor;
&.primary {
color: get-color(icon-brand);
color: get-color(icon-brand) !important;
}
@each $icon in $icons {

View File

@ -40,3 +40,5 @@ export interface MapAreaInfo {
routes?: Array<string>; // 绑定线路id集合
}
//#endregion
export type MapRect = Record<'x' | 'y' | 'width' | 'height', number>;

View File

@ -36,14 +36,11 @@ const bindAction = computed<string>(
const mapAreas = (type: MapAreaType): string => {
const id = pen.value?.id;
if (!id) return '';
return (
editor.value
.find(`area-${type}`)
.filter(({ area }) => area?.points?.includes(id))
?.map(({ label }) => label)
.filter((v) => !!v)
.join('、') ?? ''
);
return editor.value
.getBoundAreas(id, 'point', type)
.map(({ label }) => label)
.filter((v) => !!v)
.join('、');
};
const coArea1 = computed<string>(() => mapAreas(MapAreaType.库区));
const coArea2 = computed<string>(() => mapAreas(MapAreaType.互斥区));

View File

@ -1,29 +1,57 @@
<script setup lang="ts">
import { MAP_POINT_TYPES, type MapPen, type MapPointInfo } from '@api/map';
import { MAP_POINT_TYPES, MapAreaType, type MapPen, type MapPointInfo, MapPointType, type MapRect } from '@api/map';
import type { RobotInfo } from '@api/robot';
import type { RobotBindModalRef } from '@common/modal/robot-bind-modal.vue';
import type { EditorService } from '@core/editor.service';
import sTheme from '@core/theme.service';
import { isNil } from 'lodash-es';
import { ref, shallowRef } from 'vue';
import { computed, inject, type InjectionKey, type ShallowRef } from 'vue';
type Props = {
token: InjectionKey<ShallowRef<EditorService>>;
id?: string;
};
const props = defineProps<Props>();
const editor = inject(props.token)!;
const pen = computed<MapPen | undefined>(() => editor.value.current.value);
const point = computed<MapPointInfo | null>(() => {
const point = pen.value?.point;
if (!point?.type) return null;
return point;
const pen = computed<MapPen | null>(() => {
const v = editor.value.current.value;
if (v?.id !== props.id) return null;
return v!;
});
const point = computed<MapPointInfo | null>(() => {
const v = pen.value?.point;
if (!v?.type) return null;
return v;
});
const rect = computed<MapRect | null>(() => {
if (isNil(pen.value)) return null;
return editor.value.getPenRect(pen.value);
});
const refBindRobot = shallowRef<RobotBindModalRef>();
const robotKeyword = ref<string>('');
const robots = computed<RobotInfo[]>(
() =>
<RobotInfo[]>(
point.value?.robots?.map((v) => editor.value.getRobotById(v)).filter((v) => v?.label.includes(robotKeyword.value))
) ?? [],
);
const coArea1 = computed<MapPen[]>(() => editor.value.getBoundAreas(props.id, 'point', MapAreaType.库区));
const coArea2 = computed<MapPen[]>(() => editor.value.getBoundAreas(props.id, 'point', MapAreaType.互斥区));
</script>
<template>
<RobotBindModal ref="refBindRobot" :token="token" />
<a-card :title="$t('属性')" :bordered="false">
<template v-if="pen && point">
<a-row :gutter="8">
<a-flex v-if="id && pen && point" :gap="24" vertical>
<a-row :gutter="[8, 8]">
<a-col :span="12">
<a-select :value="point.type" @change="editor.changePointType(pen.id!, <number>$event)">
<a-select :value="point.type" @change="editor.changePointType(id, <number>$event)">
<a-select-option v-for="[l, v] in MAP_POINT_TYPES" :key="v">{{ $t(l) }}</a-select-option>
</a-select>
</a-col>
@ -31,13 +59,139 @@ const point = computed<MapPointInfo | null>(() => {
<a-input
:maxlength="10"
:value="pen.label"
@change="editor.updatePen(pen.id!, { label: $event.target.value }, false)"
@change="editor.updatePen(id, { label: $event.target.value }, false)"
>
<template #suffix><EditOutlined /></template>
</a-input>
</a-col>
</a-row>
</template>
<a-row :gutter="[8, 8]">
<a-col :span="24">
<a-typography-text>{{ $t('描述') }}:</a-typography-text>
</a-col>
<a-col :span="24">
<a-textarea
class="prop"
:placeholder="$t('请输入描述内容')"
:maxlength="100"
:autoSize="{ minRows: 3, maxRows: 3 }"
:value="pen?.desc"
@change="editor.updatePen(id, { desc: $event.target.value }, false)"
/>
</a-col>
</a-row>
<a-row align="middle" :gutter="16">
<a-col flex="auto">
<a-typography-text>{{ $t('坐标') }}:</a-typography-text>
</a-col>
<a-col flex="none">
<a-space :size="8">
<a-typography-text code>X:</a-typography-text>
<a-input-number
style="width: 80px"
:placeholder="$t('请输入')"
:precision="0"
:controls="false"
:value="rect?.x?.toFixed()"
@change="editor.updatePen(id, { x: <number>$event })"
/>
</a-space>
</a-col>
<a-col flex="none">
<a-space :size="8">
<a-typography-text code>Y:</a-typography-text>
<a-input-number
style="width: 80px"
:placeholder="$t('请输入')"
:precision="0"
:controls="false"
:value="rect?.y?.toFixed()"
@change="editor.updatePen(id, { y: <number>$event })"
/>
</a-space>
</a-col>
</a-row>
<a-collapse expand-icon-position="end" :bordered="false">
<template #expandIcon="v">
<i class="icon dropdown" :class="{ active: v?.isActive }" />
</template>
<a-collapse-panel
v-if="[MapPointType.充电点, MapPointType.停靠点].includes(point.type)"
:header="$t('绑定机器人')"
>
<template #extra>
<a-button class="icon-btn" size="small" @click.stop="refBindRobot?.open(pen)">
<i class="mask plus" />
</a-button>
</template>
<a-input class="search mv-8" :placeholder="$t('请输入搜索关键字')" v-model:value="robotKeyword">
<template #suffix>
<i class="icon search size-16" />
</template>
</a-input>
<a-list rowKey="id" :data-source="robots">
<template #renderItem="{ item }">
<a-list-item class="ph-16" style="height: 36px">
<a-typography-text>{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
<a-collapse-panel v-if="MapPointType.等待点 === point.type" :header="$t('绑定动作点')">
<template #extra>
<a-button class="icon-btn" size="small" @click.stop>
<i class="mask plus" />
</a-button>
</template>
<a-list rowKey="id" :data-source="point.actions">
<template #renderItem="{ item }">
<a-list-item class="ph-16" style="height: 36px">
<a-typography-text>{{ editor.getPenById(item)?.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
<a-collapse-panel v-if="MapPointType.动作点 === point.type" :header="$t('关联库区')">
<a-list rowKey="id" :data-source="coArea1">
<template #renderItem="{ item }">
<a-list-item class="ph-16" style="height: 36px">
<a-typography-text>{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
<a-collapse-panel
v-if="
[
MapPointType.普通点,
MapPointType.电梯点,
MapPointType.自动门点,
MapPointType.等待点,
MapPointType.充电点,
MapPointType.停靠点,
MapPointType.动作点,
MapPointType.临时避让点,
].includes(point.type)
"
:header="$t('关联互斥区')"
>
<a-list rowKey="id" :data-source="coArea2">
<template #renderItem="{ item }">
<a-list-item class="ph-16" style="height: 36px">
<a-typography-text>{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
</a-collapse>
</a-flex>
<a-empty v-else :image="sTheme.empty" />
</a-card>
</template>

View File

@ -0,0 +1,70 @@
<script setup lang="ts">
import type { MapPen } from '@api/map';
import { type RobotInfo, RobotType } from '@api/robot';
import type { EditorService } from '@core/editor.service';
import { computed, inject, type InjectionKey, ref, type ShallowRef, toRaw } from 'vue';
type Props = {
token: InjectionKey<ShallowRef<EditorService>>;
};
const props = defineProps<Props>();
const editor = inject(props.token)!;
export type RobotBindModalRef = Ref;
type Ref = {
open: (pen: MapPen) => void;
};
const open: Ref['open'] = (pen) => {
if (!pen?.id) return;
keyword.value = '';
id.value = pen.id;
selected.value = pen.point?.robots ?? [];
show.value = true;
};
defineExpose<Ref>({ open });
const show = ref<boolean>(false);
const keyword = ref<string>('');
const robots = computed<RobotInfo[]>(() => editor.value.robots.filter(({ label }) => label.includes(keyword.value)));
const id = ref<string>('');
const selected = ref<string[]>([]);
const submit = () => {
editor.value.updatePoint(id.value, { robots: toRaw(selected.value) });
show.value = false;
};
</script>
<template>
<a-modal :width="420" :title="$t('绑定机器人')" v-model:open="show" :mask-closable="false" centered @ok="submit">
<a-input class="search" :placeholder="$t('请输入搜索关键字')" v-model:value="keyword">
<template #suffix>
<i class="icon search size-16" />
</template>
</a-input>
<a-table
class="mt-10"
rowKey="id"
:dataSource="robots"
:pagination="false"
:rowSelection="{
columnWidth: 32,
selectedRowKeys: selected,
onChange: (keys) => (selected = <string[]>keys),
}"
:scroll="{ y: 80 }"
bordered
>
<a-table-column dataIndex="label" :title="$t('机器人')" />
<a-table-column dataIndex="brand" :title="$t('品牌')" />
<a-table-column dataIndex="type" :title="$t('机器人类型')">
<template #default="{ text }">
{{ $t(RobotType[text]) }}
</template>
</a-table-column>
</a-table>
</a-modal>
</template>

View File

@ -138,7 +138,7 @@ const selectRobot = (id: string) => {
<RobotDetailCard v-if="isRobot" :token="EDITOR_KEY" :current="current.id" />
<template v-if="isPoint">
<PointEditCard v-if="editable" :token="EDITOR_KEY" :current="current.id" />
<PointEditCard v-if="editable" :token="EDITOR_KEY" :id="current.id" />
<PointDetailCard v-else :token="EDITOR_KEY" :current="current.id" />
</template>
<template v-if="isRoute">
@ -171,6 +171,7 @@ const selectRobot = (id: string) => {
right: 64px;
z-index: 100;
width: 320px;
max-height: calc(100% - 96px);
}
.ant-typography.title {

View File

@ -1,10 +1,10 @@
import { EDITOR_CONFIG, MapAreaType, type MapPen, MapPointType } from '@api/map';
import { EDITOR_CONFIG, MapAreaType, type MapPen, type MapPointInfo, MapPointType } from '@api/map';
import type { RobotGroup, RobotInfo } from '@api/robot';
import type { SceneData } from '@api/scene';
import sTheme from '@core/theme.service';
import { CanvasLayer, EditType, LockState, Meta2d, type Pen, s8 } from '@meta2d/core';
import { useObservable } from '@vueuse/rxjs';
import { clone, cloneDeep, get, isNil, isString, pick, remove, some } from 'lodash-es';
import { clone, cloneDeep, get, isNil, isString, mapKeys, pick, remove, some } from 'lodash-es';
import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs';
import { reactive, watch } from 'vue';
@ -195,6 +195,12 @@ export class EditorService extends Meta2d {
// this.pushHistory({ type: EditType.Add, pens: [cloneDeep(pen)] });
}
public updatePoint(id: string, info: Partial<MapPointInfo>): void {
const { point } = this.getPenById(id) ?? {};
if (!point?.type) return;
const p = { ...point, ...info };
this.setValue({ id, point: p }, { render: true, history: true, doEvent: true });
}
public changePointType(id: string, type: MapPointType): void {
this.setValue(
{ id, ...this.#mapPoint(type), ...this.#mapPointImage(type), point: { type } },
@ -241,6 +247,15 @@ export class EditorService extends Meta2d {
{ initialValue: new Array<MapPen>() },
);
public getBoundAreas(id: string = '', name: 'point' | 'line', type: MapAreaType): MapPen[] {
if (!id) return [];
return this.find(`area-${type}`).filter(({ area }) => {
if (name === 'point') return area?.points?.includes(id);
if (name === 'line') return area?.routes?.includes(id);
return false;
});
}
public async addArea(p1: Point, p2: Point, type = MapAreaType.) {
const scale = this.data().scale ?? 1;
const w = Math.abs(p1.x - p2.x);