feat: route

This commit is contained in:
chndfang 2025-05-10 00:49:45 +08:00
parent 43f16303d8
commit 597967497d
18 changed files with 188 additions and 157 deletions

File diff suppressed because one or more lines are too long

5
mocks/scene/saveById Normal file
View File

@ -0,0 +1,5 @@
{
"code": 200,
"success": false,
"message": "模拟提示"
}

View File

@ -428,6 +428,25 @@
}
}
.ant-message-notice-content {
padding: 0;
background-color: none;
box-shadow: none !important;
& > .ant-message-error {
padding: 8px 15px;
font: 400 14px/22px Roboto;
color: get-color(text1);
background-color: get-color(error_bg);
border: 1px solid get-color(error_border);
border-radius: 2px;
& > .anticon {
margin-inline-end: 10px;
}
}
}
.ant-modal.ant-modal-confirm .ant-modal-content {
padding: 32px 32px 24px;
background-color: get-color(popover);
@ -601,8 +620,11 @@
& > table {
& > .ant-table-tbody > .ant-table-row > .ant-table-cell {
padding: 9px 16px 8px;
overflow: hidden;
font: 400 14px/22px Roboto;
color: get-color(text1);
text-overflow: ellipsis;
white-space: nowrap;
background-color: transparent;
border-color: get-color(divider);
}

View File

@ -45,3 +45,4 @@ export interface MapAreaInfo {
export type Point = Record<'x' | 'y', number>;
export type Rect = Record<'x' | 'y' | 'width' | 'height', number>;
export type AnchorPosition = 't' | 'b' | 'l' | 'r';

View File

@ -4,6 +4,7 @@ import type { SceneDetail, SceneInfo } from './type';
const enum API {
= '/scene/getById',
= '/scene/saveById',
}
export async function getSceneById(id: SceneInfo['id']): Promise<SceneDetail | null> {
@ -19,3 +20,17 @@ export async function getSceneById(id: SceneInfo['id']): Promise<SceneDetail | n
return null;
}
}
export async function saveSceneById(id: SceneInfo['id'], json: string): Promise<boolean> {
if (!id) return false;
type B = { id: string; json: string };
type D = void;
try {
const body = { id, json };
await http.post<D, B>(API., body);
return true;
} catch (error) {
console.debug(error);
return false;
}
}

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { type MapAreaInfo, MapAreaType, type MapPen } from '@api/map';
import { type MapAreaInfo, MapAreaType, type MapPen, MapPointType } from '@api/map';
import type { EditorService } from '@core/editor.service';
import sTheme from '@core/theme.service';
import { computed, inject, type InjectionKey, type ShallowRef } from 'vue';
@ -20,6 +20,15 @@ const area = computed<MapAreaInfo | null>(() => {
const icon = computed<string>(() => `area${area.value?.type}-detail`);
const bindAction = computed<string>(
() =>
area.value?.points
?.map((v) => editor.value.getPenById(v))
.filter((v) => v?.point?.type === MapPointType.动作点)
.map((v) => v?.label)
.filter((v) => !!v)
.join('、') ?? '',
);
const bindPoint = computed<string>(
() =>
area.value?.points
@ -57,7 +66,7 @@ const bindRoute = computed<string>(
<a-list-item v-if="MapAreaType.库区 === area.type">
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('绑定动作点') }}</a-typography-text>
<a-typography-text>{{ bindPoint || $t('暂无') }}</a-typography-text>
<a-typography-text>{{ bindAction || $t('暂无') }}</a-typography-text>
</a-flex>
</a-list-item>
<a-list-item v-if="[MapAreaType.互斥区, MapAreaType.非互斥区].includes(area.type)">

View File

@ -33,13 +33,14 @@ const points = computed<MapPen[]>(
area.value?.points?.map((v) => editor.value.getPenById(v)).filter((v) => v?.label?.includes(pointKeyword.value))
),
);
const actions = computed<MapPen[]>(() => points.value?.filter(({ point }) => point?.type === MapPointType.动作点));
const refBindRoute = shallowRef<RouteBindModalRef>();
const routeKeyword = ref<string>('');
const routes = computed<MapPen[]>(
const routes = computed<string[]>(
() =>
<MapPen[]>(
area.value?.routes?.map((v) => editor.value.getPenById(v)).filter((v) => v?.label?.includes(routeKeyword.value))
<string[]>(
area.value?.routes?.map((v) => editor.value.getRouteLabel(v)).filter((v) => v?.includes(routeKeyword.value))
),
);
</script>
@ -88,7 +89,7 @@ const routes = computed<MapPen[]>(
<a-collapse-panel v-if="MapAreaType.库区 === area.type" :header="$t('绑定动作点')">
<template #extra>
<a-button class="icon-btn" size="small" @click.stop="refBindPoint?.open(pen, [MapPointType.动作点])">
<a-button class="icon-btn" size="small" @click.stop="refBindPoint?.open(pen, MapPointType.动作点)">
<i class="mask plus" />
</a-button>
</template>
@ -97,7 +98,7 @@ const routes = computed<MapPen[]>(
<i class="icon search size-16" />
</template>
</a-input>
<a-list rowKey="id" :data-source="points">
<a-list rowKey="id" :data-source="actions">
<template #renderItem="{ item }">
<a-list-item class="ph-16" style="height: 36px">
<a-typography-text>{{ item.label }}</a-typography-text>
@ -143,7 +144,7 @@ const routes = computed<MapPen[]>(
<a-list rowKey="id" :data-source="routes">
<template #renderItem="{ item }">
<a-list-item class="ph-16" style="height: 36px">
<a-typography-text>{{ item.label }}</a-typography-text>
<a-typography-text>{{ item }}</a-typography-text>
</a-list-item>
</template>
</a-list>

View File

@ -28,7 +28,9 @@ const bindRobot = computed<string>(
const bindAction = computed<string>(
() =>
point.value?.actions
?.map((v) => editor.value.getPenById(v)?.label)
?.map((v) => editor.value.getPenById(v))
.filter((v) => v?.point?.type === MapPointType.动作点)
.map((v) => v?.label)
.filter((v) => !!v)
.join('、') ?? '',
);

View File

@ -110,7 +110,7 @@ const coArea2 = computed<MapPen[]>(() => editor.value.getBoundAreas(props.id, 'p
:precision="0"
:controls="false"
:value="rect?.x?.toFixed()"
@change="editor.updatePen(id, { x: <number>$event })"
@change="editor.updatePen(id, { x: +$event })"
/>
</a-space>
</a-col>
@ -123,7 +123,7 @@ const coArea2 = computed<MapPen[]>(() => editor.value.getBoundAreas(props.id, 'p
:precision="0"
:controls="false"
:value="rect?.y?.toFixed()"
@change="editor.updatePen(id, { y: <number>$event })"
@change="editor.updatePen(id, { y: +$event })"
/>
</a-space>
</a-col>
@ -159,7 +159,7 @@ const coArea2 = computed<MapPen[]>(() => editor.value.getBoundAreas(props.id, 'p
<a-collapse-panel v-if="MapPointType.等待点 === point.type" :header="$t('绑定动作点')">
<template #extra>
<a-button class="icon-btn" size="small" @click.stop="refBindPoint?.open(pen, [MapPointType.动作点])">
<a-button class="icon-btn" size="small" @click.stop="refBindPoint?.open(pen, MapPointType.动作点)">
<i class="mask plus" />
</a-button>
</template>

View File

@ -106,7 +106,7 @@ const route = computed<MapRouteInfo | null>(() => {
:precision="0"
:controls="false"
:value="route?.c1?.x.toFixed()"
@change="editor.updateRoute(id, { c1: { x: <number>$event, y: route?.c1?.y ?? 0 } })"
@change="editor.updateRoute(id, { c1: { x: +$event, y: route?.c1?.y ?? 0 } })"
/>
</a-space>
</a-col>
@ -119,7 +119,7 @@ const route = computed<MapRouteInfo | null>(() => {
:precision="0"
:controls="false"
:value="route?.c1?.y.toFixed()"
@change="editor.updateRoute(id, { c1: { x: route?.c1?.x ?? 0, y: <number>$event } })"
@change="editor.updateRoute(id, { c1: { x: route?.c1?.x ?? 0, y: +$event } })"
/>
</a-space>
</a-col>
@ -138,7 +138,7 @@ const route = computed<MapRouteInfo | null>(() => {
:precision="0"
:controls="false"
:value="route?.c2?.x.toFixed()"
@change="editor.updateRoute(id, { c2: { x: <number>$event, y: route?.c2?.y ?? 0 } })"
@change="editor.updateRoute(id, { c2: { x: +$event, y: route?.c2?.y ?? 0 } })"
/>
</a-space>
</a-col>
@ -151,7 +151,7 @@ const route = computed<MapRouteInfo | null>(() => {
:precision="0"
:controls="false"
:value="route?.c2?.y.toFixed()"
@change="editor.updateRoute(id, { c2: { x: route?.c2?.x ?? 0, y: <number>$event } })"
@change="editor.updateRoute(id, { c2: { x: route?.c2?.x ?? 0, y: +$event } })"
/>
</a-space>
</a-col>

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { MapAreaType } from '@api/map';
import { saveSceneById } from '@api/scene';
import type { EditorService } from '@core/editor.service';
import { isEmpty } from 'lodash-es';
import { inject, type InjectionKey, ref, type ShallowRef, watch } from 'vue';
@ -12,6 +13,14 @@ type Props = {
const props = defineProps<Props>();
const editor = inject(props.token)!;
//#region
const updateScene = async () => {
const json = editor.value.save();
if (!json) return;
await saveSceneById(props.id, json);
};
//#endregion
const mode = ref<MapAreaType>();
watch(editor.value.mouseBrush, (v) => {
if (!mode.value) return;
@ -51,7 +60,7 @@ watch(editor.value.mouseBrush, (v) => {
<a-divider class="size-24 mh-8" type="vertical" />
<a-button class="icon-btn tool-btn" size="large">
<a-button class="icon-btn tool-btn" size="large" @click="updateScene">
<i class="mask save" />
</a-button>
<a-button class="icon-btn tool-btn ml-12" size="large" @click="editor.undo()">

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { type MapPen, MapPointType } from '@api/map';
import type { EditorService } from '@core/editor.service';
import { isNil } from 'lodash-es';
import { computed, inject, type InjectionKey, ref, type ShallowRef, toRaw } from 'vue';
type Props = {
@ -11,12 +12,12 @@ const editor = inject(props.token)!;
export type PointBindModalRef = Ref;
type Ref = {
open: (pen: MapPen, filter?: MapPointType[]) => void;
open: (pen: MapPen, filter?: MapPointType) => void;
};
const open: Ref['open'] = (pen, filter = []) => {
const open: Ref['open'] = (pen, type) => {
if (!pen?.id) return;
keyword.value = '';
types.value = [MapPointType.禁行点, ...filter];
filter.value = type;
id.value = pen.id;
sn.value = pen.name;
switch (sn.value) {
@ -37,10 +38,13 @@ defineExpose<Ref>({ open });
const show = ref<boolean>(false);
const keyword = ref<string>('');
const types = ref<MapPointType[]>([MapPointType.禁行点]);
const filter = ref<MapPointType>();
const points = computed<MapPen[]>(() =>
editor.value.points.value
.filter(({ point }) => point?.type && !types.value.includes(point.type))
.filter(
({ point }) =>
point?.type && point.type !== MapPointType.禁行点 && (isNil(filter.value) || point.type === filter.value),
)
.filter(({ label }) => label?.includes(keyword.value)),
);

View File

@ -60,14 +60,14 @@ const submit = () => {
bordered
>
<a-table-column dataIndex="label" :title="$t('路段')" />
<a-table-column :dataIndex="['route', 'type']" :title="$t('路段类型')">
<a-table-column :width="140" :dataIndex="['route', 'type']" :title="$t('路段类型')">
<template #default="{ text }">
{{ $t(MAP_ROUTE_TYPE[text]) }}
</template>
</a-table-column>
<a-table-column :dataIndex="['route', 'pass']" :title="$t('可通行类型')">
<a-table-column :width="128" :dataIndex="['route', 'pass']" :title="$t('可通行类型')">
<template #default="{ text }">
{{ $t(MapRoutePassType[text]) }}
{{ $t(MapRoutePassType[text ?? MapRoutePassType.]) }}
</template>
</a-table-column>
</a-table>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { MapAreaType, type MapPen, MapPointType } from '@api/map';
import { MapAreaType, type MapPen, MapPointType, MapRoutePassType } from '@api/map';
import type { EditorService } from '@core/editor.service';
import { computed, inject, type InjectionKey, ref, type ShallowRef } from 'vue';
@ -20,6 +20,13 @@ const points = computed<MapPen[]>(() =>
//#endregion
//#region 线
const routes = computed<MapPen[]>(() =>
editor.value.routes.value.filter(({ label }) => {
console.log(label);
return label?.includes(keyword.value);
}),
);
//#endregion
//#region
@ -85,11 +92,56 @@ const areas = computed<MapPen[]>(() => editor.value.areas.value.filter(({ label
</a-list>
</a-collapse-panel>
<a-collapse-panel :header="$t('仅载货可通行路线')"></a-collapse-panel>
<a-collapse-panel :header="$t('仅载货可通行路线')">
<a-list
rowKey="id"
:data-source="routes.filter(({ route }) => route?.pass === MapRoutePassType.仅载货可通行)"
>
<template #renderItem="{ item }">
<a-list-item
class="ph-16"
:class="{ selected: item.id === current }"
style="height: 36px"
@click="editor.active(item.id)"
>
<a-typography-text type="secondary">{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
<a-collapse-panel :header="$t('仅空载可通行路线')"></a-collapse-panel>
<a-collapse-panel :header="$t('仅空载可通行路线')">
<a-list
rowKey="id"
:data-source="routes.filter(({ route }) => route?.pass === MapRoutePassType.仅空载可通行)"
>
<template #renderItem="{ item }">
<a-list-item
class="ph-16"
:class="{ selected: item.id === current }"
style="height: 36px"
@click="editor.active(item.id)"
>
<a-typography-text type="secondary">{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
<a-collapse-panel :header="$t('禁行路线')"></a-collapse-panel>
<a-collapse-panel :header="$t('禁行路线')">
<a-list rowKey="id" :data-source="routes.filter(({ route }) => route?.pass === MapRoutePassType.禁行)">
<template #renderItem="{ item }">
<a-list-item
class="ph-16"
:class="{ selected: item.id === current }"
style="height: 36px"
@click="editor.active(item.id)"
>
<a-typography-text type="secondary">{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
<a-collapse-panel :header="$t('等待点')">
<a-list rowKey="id" :data-source="points.filter(({ point }) => point?.type === MapPointType.等待点)">

View File

@ -1,21 +0,0 @@
<script setup lang="ts">
import type { EditorService } from '@core/editor.service';
import sTheme from '@core/theme.service';
import { inject, type InjectionKey, type ShallowRef } from 'vue';
type Props = {
token: InjectionKey<ShallowRef<EditorService>>;
editable?: boolean;
current?: string;
};
const props = defineProps<Props>();
const editor = inject(props.token)!;
</script>
<template>
<a-row v-if="true"></a-row>
<a-empty v-else :image="sTheme.empty" />
</template>
<!-- <style scoped lang="scss"></style> -->
<!-- <style scoped lang="scss"></style> -->

View File

@ -1,65 +0,0 @@
<script setup lang="ts">
import type { EditorService } from '@core/editor.service';
import { isEmpty } from 'lodash-es';
import { inject, type InjectionKey, ref, type ShallowRef, watch } from 'vue';
const enum Mode {
常规,
添加点位 = 0x10,
添加库区 = 0x31,
添加互斥区,
添加非互斥区,
}
type Props = {
editor: InjectionKey<ShallowRef<EditorService>>;
editable?: boolean;
};
const props = defineProps<Props>();
const editor = inject(props.editor)!;
const mode = ref<Mode>(Mode.常规);
watch(editor.value.mouseClick, (v) => {
if (mode.value !== Mode.添加点位) return;
if (isEmpty(v)) return;
editor.value.addPoint(v, 1);
editor.value.addPoint(v, 11);
});
watch(editor.value.mouseBrush, (v) => {
if (![Mode.添加库区, Mode.添加互斥区, Mode.添加非互斥区].includes(mode.value)) return;
const [p1, p2] = v ?? [];
if (isEmpty(p1) || isEmpty(p2)) return;
editor.value.addArea(p1, p2, mode.value & 0xf);
mode.value = Mode.常规;
});
</script>
<template>
<a-space class="scroll" direction="vertical">
<a-button @click="mode = Mode.常规">常规</a-button>
<a-button @click="mode = Mode.添加点位">点位</a-button>
<!-- <a-button @click="editor.addRoute(PenRouteType.直线)">直线</a-button> -->
<!-- <a-button @click="editor.addRoute(PenRouteType.三阶贝塞尔曲线)">曲线</a-button> -->
<a-button @click="mode = Mode.添加库区">库区</a-button>
<a-button @click="mode = Mode.添加互斥区">互斥区</a-button>
<a-button @click="mode = Mode.添加非互斥区">非互斥区</a-button>
<a-button @click="mode = Mode.常规">常规</a-button>
<a-button @click="mode = Mode.添加点位">点位</a-button>
<!-- <a-button @click="editor.addRoute(PenRouteType.直线)">直线</a-button> -->
<!-- <a-button @click="editor.addRoute(PenRouteType.三阶贝塞尔曲线)">曲线</a-button> -->
<a-button @click="mode = Mode.添加库区">库区</a-button>
<a-button @click="mode = Mode.添加互斥区">互斥区</a-button>
<a-button @click="mode = Mode.添加非互斥区">非互斥区</a-button>
<a-button @click="mode = Mode.常规">常规</a-button>
<a-button @click="mode = Mode.添加点位">点位</a-button>
<!-- <a-button @click="editor.addRoute(PenRouteType.直线)">直线</a-button> -->
<!-- <a-button @click="editor.addRoute(PenRouteType.三阶贝塞尔曲线)">曲线</a-button> -->
<a-button @click="mode = Mode.添加库区">库区</a-button>
<a-button @click="mode = Mode.添加互斥区">互斥区</a-button>
<a-button @click="mode = Mode.添加非互斥区">非互斥区</a-button>
</a-space>
</template>
<!-- <style scoped lang="scss"></style> -->

View File

@ -47,7 +47,6 @@ const importScene = async () => {
const exportScene = () => {
const json = editor.value?.save();
if (!json) return;
getSceneById(json);
const blob = textToBlob(json);
if (!blob?.size) return;
const url = URL.createObjectURL(blob);

View File

@ -1,4 +1,5 @@
import {
type AnchorPosition,
EDITOR_CONFIG,
type MapAreaInfo,
MapAreaType,
@ -26,12 +27,26 @@ export class EditorService extends Meta2d {
this.setState(editable);
setTimeout(() => {
this.addRoute(['21b74c90', '00f32a0']);
this.addRoute(['21b74c90', '7f201a25'], 'bezier3');
const pens = this.data().pens;
this.addRoute([pens[4].id, pens[9].id]);
// this.addRoute(['21b74c90', '7f201a25'], 'bezier3');
}, 1000);
}
public save(): string {
const data = this.data();
data.pens.forEach((pen: MapPen) => {
remove(pen.point?.robots ?? [], (v) => !this.#robotMap.has(v));
remove(pen.point?.actions ?? [], (v) => this.getPenById(v)?.point?.type !== MapPointType.);
remove(pen.area?.points ?? [], (v) => {
const { point } = this.getPenById(v) ?? {};
if (isNil(point)) return true;
if (point.type === MapPointType.) return true;
if (pen.area?.type === MapAreaType. && point.type !== MapPointType.) return true;
return false;
});
remove(pen.area?.routes ?? [], (v) => isNil(this.getPenById(v)));
});
return JSON.stringify(data);
}
@ -177,7 +192,7 @@ export class EditorService extends Meta2d {
}
public updatePen(id: string, pen: Partial<MapPen>, record = true): void {
this.setValue({ ...pen, id }, { render: record, history: record, doEvent: true });
this.setValue({ ...pen, id }, { render: true, history: record, doEvent: true });
}
//#region 点位
@ -202,9 +217,6 @@ export class EditorService extends Meta2d {
label: `P${id}`,
point: { type },
};
const { x, y, width, height } = this.getPenRect(pen);
pen.x = x - width / 2;
pen.y = y - height / 2;
await this.addPen(pen, false, true, true);
// this.pushHistory({ type: EditType.Add, pens: [cloneDeep(pen)] });
}
@ -241,7 +253,7 @@ export class EditorService extends Meta2d {
this.#change$$.pipe(
filter((v) => v),
debounceTime(100),
map(() => this.find('route').map((v) => (v.label = this.getRouteLabel(v.id)))),
map(() => this.find('route').map((v) => ({ ...v, label: this.getRouteLabel(v.id) }))),
),
{ initialValue: new Array<MapPen>() },
);
@ -259,12 +271,13 @@ export class EditorService extends Meta2d {
return `${p1.label}${(d ?? direction) > 0 ? '→' : '←'}${p2.label}`;
}
public addRoute(p: [string, string], type = MapRouteType.线): void {
public addRoute(p: [string, string], type = MapRouteType.线, from?: AnchorPosition, to?: AnchorPosition): void {
const [p1, p2] = p.map((v) => this.getPenById(v));
if (!p1?.anchors?.length || !p2?.anchors?.length) return;
const line = this.connectLine(p1, p2, p1.anchors[0], p2.anchors[0], false);
const a1 = p1.anchors.find(({ id }) => id === from);
const a2 = p2.anchors.find(({ id }) => id === to);
const line = this.connectLine(p1, p2, a1, a2, false);
const pen: MapPen = { tags: ['route'], route: { type }, lineWidth: 2, iconSize: 10 };
// this.bottom(line);
this.setValue({ id: line.id, ...pen }, { render: false, history: false, doEvent: false });
this.updateLineType(line, type);
// this.pushHistory({ type: EditType.Add, pens: [cloneDeep(line)] });
@ -384,7 +397,20 @@ export class EditorService extends Meta2d {
this.render();
}
#onDelete(): void {}
#onDelete(pens?: MapPen[]): void {
pens?.forEach((pen) => {
switch (pen.name) {
case 'point':
{
const lines = this.getLines(pen);
this.delete(lines, true, false);
}
break;
default:
break;
}
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
#listen(e: unknown, v: any) {
@ -398,7 +424,7 @@ export class EditorService extends Meta2d {
this.#change$$.next(true);
break;
case 'delete':
this.#onDelete();
this.#onDelete(v);
this.#change$$.next(true);
break;
case 'update':
@ -413,34 +439,6 @@ export class EditorService extends Meta2d {
this.#change$$.next(false);
break;
// case 'undo':
// case 'redo':
// {
// const { type, pens = [], initPens = [] } = data ?? {};
// switch (type) {
// case EditType.Add:
// pens?.forEach((pen: SceneMapPen) => {
// if (pen.name === 'point') {
// const { image } = this.#mapPointType(pen.point!.type);
// const rect = this.#editor?.getPenRect(pen);
// this.#editor?.setValue({ id: pen.id, image, ...rect }, { render: true, history: false });
// }
// });
// break;
// case EditType.Update:
// (event === 'undo' ? initPens : pens)?.forEach((pen: SceneMapPen) => {
// if (pen.name === 'point') {
// const { image } = this.#mapPointType(pen.point!.type);
// this.#editor?.setValue({ id: pen.id, image }, { render: true, history: false });
// }
// });
// break;
// default:
// break;
// }
// }
// break;
case 'click':
case 'mousedown':
case 'mouseup':