temp
@ -1,5 +1,9 @@
|
||||
{
|
||||
"code": 200,
|
||||
"success": false,
|
||||
"success": true,
|
||||
"data": [
|
||||
"mock-robot-1",
|
||||
"mock-robot-2"
|
||||
],
|
||||
"message": "模拟提示"
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
"data": {
|
||||
"id": "mock-scene-1",
|
||||
"label": "模拟场景A",
|
||||
"json": "{\"x\":0,\"y\":0,\"scale\":1,\"pens\":[],\"origin\":{\"x\":0,\"y\":0},\"center\":{\"x\":0,\"y\":0},\"paths\":{},\"template\":\"4c2a10f\",\"locked\":10,\"version\":\"1.0.78\",\"dataPoints\":[],\"robotGroups\":[{\"id\":\"mock-robot-group-1\",\"label\":\"模拟机器人组A\",\"robots\":[\"mock-robot-1\",\"mock-robot-2\",\"mock-robot-3\"]},{\"sid\":\"mock-scene-1\",\"id\":\"mock-robot-group-2\",\"label\":\"模拟机器人组B\",\"robots\":[\"mock-robot-4\"]}],\"robots\":[{\"gid\":\"mock-robot-group-1\",\"id\":\"mock-robot-1\",\"label\":\"模拟机器人A\",\"brand\":\"模拟品牌A\",\"type\":1},{\"gid\":\"mock-robot-group-1\",\"id\":\"mock-robot-2\",\"label\":\"模拟机器人B\",\"brand\":\"模拟品牌A\",\"type\":2},{\"gid\":\"mock-robot-group-1\",\"id\":\"mock-robot-3\",\"label\":\"模拟机器人C\",\"brand\":\"模拟品牌A\",\"type\":3},{\"gid\":\"mock-robot-group-2\",\"id\":\"mock-robot-4\",\"label\":\"模拟机器人D\",\"brand\":\"模拟品牌B\",\"type\":1}]}"
|
||||
"json": "{\"x\":0,\"y\":0,\"scale\":1,\"pens\":[],\"origin\":{\"x\":0,\"y\":0},\"center\":{\"x\":0,\"y\":0},\"paths\":{},\"template\":\"4c2a10f\",\"locked\":10,\"version\":\"1.0.78\",\"dataPoints\":[],\"robotGroups\":[{\"id\":\"mock-robot-group-1\",\"label\":\"模拟机器人组A\",\"robots\":[\"mock-robot-1\",\"mock-robot-2\",\"mock-robot-3\"]},{\"sid\":\"mock-scene-1\",\"id\":\"mock-robot-group-2\",\"label\":\"模拟机器人组B\",\"robots\":[\"mock-robot-4\"]}],\"robots\":[{\"gid\":\"mock-robot-group-1\",\"id\":\"mock-robot-1\",\"label\":\"模拟机器人A\",\"brand\":\"模拟品牌A\",\"type\":1,\"ip\":\"127.0.1.1\"},{\"gid\":\"mock-robot-group-1\",\"id\":\"mock-robot-2\",\"label\":\"模拟机器人B\",\"brand\":\"模拟品牌A\",\"type\":2,\"ip\":\"127.0.1.2\"},{\"gid\":\"mock-robot-group-1\",\"id\":\"mock-robot-3\",\"label\":\"模拟机器人C\",\"brand\":\"模拟品牌A\",\"type\":3,\"ip\":\"127.0.1.3\"},{\"gid\":\"mock-robot-group-2\",\"id\":\"mock-robot-4\",\"label\":\"模拟机器人D\",\"brand\":\"模拟品牌B\",\"type\":1,\"ip\":\"127.0.2.1\"}]}"
|
||||
},
|
||||
"message": "模拟提示"
|
||||
}
|
||||
|
BIN
public/point/16-dark.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
@ -1,9 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import sLocale from '@core/locale.service';
|
||||
import sTheme from '@core/theme.service';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const empty = computed<string>(() => new URL(`./assets/images/empty-${sTheme.theme}.png`, import.meta.url).href);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -16,7 +13,7 @@ const empty = computed<string>(() => new URL(`./assets/images/empty-${sTheme.the
|
||||
>
|
||||
<template #renderEmpty>
|
||||
<a-flex justify="center" align="center" :gap="8" vertical>
|
||||
<img height="40" :src="empty" />
|
||||
<img height="40" :src="sTheme.empty" />
|
||||
<a-typography-text disabled>{{ $t('暂无数据') }}</a-typography-text>
|
||||
</a-flex>
|
||||
</template>
|
||||
|
134
src/_ant.scss
@ -55,6 +55,21 @@
|
||||
&.ant-btn-sm {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.ant-btn-lg {
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
background-color: get-color(sider_bg2);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px 0 get-color(shadow1);
|
||||
|
||||
& > .ant-card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,6 +104,7 @@
|
||||
align-items: center;
|
||||
font: 400 14px/22px Roboto;
|
||||
color: get-color(text2);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.ant-collapse.ant-collapse-ghost {
|
||||
@ -136,6 +152,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-divider {
|
||||
border-color: get-color(border1);
|
||||
|
||||
&.ant-divider-vertical {
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-root {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
@ -155,12 +179,57 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
|
||||
& > .ant-empty-image {
|
||||
height: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& > .ant-empty-description {
|
||||
margin: 0;
|
||||
font: 400 14px/22px Roboto;
|
||||
color: get-color(text4);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-flex {
|
||||
& > * {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-float-btn {
|
||||
height: fit-content;
|
||||
overflow: hidden;
|
||||
background-color: transparent;
|
||||
box-shadow: none !important;
|
||||
|
||||
&.ant-float-btn-square > .ant-badge > .ant-float-btn-body {
|
||||
background-color: get-color(sider_bg2);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2px 0 get-color(shadow1);
|
||||
|
||||
& > .ant-float-btn-content {
|
||||
padding: 0;
|
||||
|
||||
& > .ant-float-btn-icon {
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover > .ant-badge > .ant-float-btn-body {
|
||||
background-color: get-color(fill1);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-form {
|
||||
& > .ant-form-item {
|
||||
margin-block-end: 8px;
|
||||
@ -297,6 +366,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-list.block .ant-list-item {
|
||||
padding: 12px;
|
||||
background-color: get-color(fill4);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-block-start: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal.ant-modal-confirm .ant-modal-content {
|
||||
padding: 32px 32px 24px;
|
||||
background-color: get-color(popover);
|
||||
@ -490,6 +570,8 @@
|
||||
}
|
||||
|
||||
.ant-tabs {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
|
||||
& > .ant-tabs-nav {
|
||||
@ -531,17 +613,61 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px 5px;
|
||||
margin: 0;
|
||||
font: 500 14px/22px Roboto;
|
||||
color: get-color(text1);
|
||||
vertical-align: top;
|
||||
background-color: transparent;
|
||||
border-color: get-color(border1);
|
||||
border-radius: 4px;
|
||||
box-shadow: none !important;
|
||||
|
||||
&.ant-tag-borderless {
|
||||
padding: 4px 8px;
|
||||
font: 400 14px/16px Roboto;
|
||||
color: get-color(text2);
|
||||
background-color: get-color(fill3);
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-typography {
|
||||
font: 400 14px/22px Roboto;
|
||||
color: get-color(text2);
|
||||
font: 500 16px/22px Roboto;
|
||||
color: get-color(text1);
|
||||
|
||||
&.ant-typography-secondary {
|
||||
font: 400 14px/22px Roboto;
|
||||
color: get-color(text2);
|
||||
}
|
||||
|
||||
&.ant-typography-disabled {
|
||||
font: 400 14px/22px Roboto;
|
||||
color: get-color(text4);
|
||||
}
|
||||
|
||||
& > strong {
|
||||
font: 500 16px/22px SourceHanSansSC;
|
||||
color: get-color(text1);
|
||||
font: 500 16px/22px Roboto;
|
||||
}
|
||||
|
||||
& > code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font: 400 14px/22px Roboto;
|
||||
color: get-color(text3);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
padding: 8px;
|
||||
background-color: get-color(sider_bg);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px 0 get-color(shadow2);
|
||||
}
|
||||
}
|
||||
|
@ -4,10 +4,26 @@
|
||||
@include themed {
|
||||
.icon-btn {
|
||||
&.panel-btn {
|
||||
color: get-color(ffffffd9);
|
||||
color: get-color(icon);
|
||||
|
||||
&:disabled {
|
||||
color: get-color(ffffff2e);
|
||||
color: get-color(icon-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
&.tool-btn {
|
||||
color: get-color(icon);
|
||||
|
||||
&:disabled {
|
||||
color: get-color(icon-disabled);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: get-color(primary) !important;
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background-color: get-color(bg_layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ export enum MapPointType {
|
||||
充电点,
|
||||
停靠点,
|
||||
动作点,
|
||||
禁行点,
|
||||
|
||||
障碍点 = 99,
|
||||
}
|
||||
|
@ -12,28 +12,38 @@ const enum API {
|
||||
|
||||
export async function getAllRobots(): Promise<Array<RobotInfo>> {
|
||||
type D = RobotInfo[];
|
||||
const data = await http.post<D>(API.获取所有机器人);
|
||||
return data ?? [];
|
||||
try {
|
||||
const data = await http.post<D>(API.获取所有机器人);
|
||||
return data ?? [];
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerRobot(robot: Omit<RobotDetail, 'id'>): Promise<RobotInfo | null> {
|
||||
type B = Omit<RobotDetail, 'id'>;
|
||||
type D = RobotInfo;
|
||||
const body = robot;
|
||||
const data = await http.post<D, B>(API.注册机器人, body);
|
||||
return data ?? null;
|
||||
}
|
||||
|
||||
export async function seizeRobotByIds(ids: Array<RobotInfo['id']>): Promise<boolean> {
|
||||
if (!ids.length) return false;
|
||||
type B = { ids: string[] };
|
||||
type D = void;
|
||||
try {
|
||||
const body = { ids };
|
||||
await http.post<D, B>(API.批量抢占控制权, body);
|
||||
return true;
|
||||
const body = robot;
|
||||
const data = await http.post<D, B>(API.注册机器人, body);
|
||||
return data ?? null;
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function seizeRobotByIds(ids: Array<RobotInfo['id']>): Promise<Array<RobotInfo['id']>> {
|
||||
if (!ids.length) return [];
|
||||
type B = { ids: string[] };
|
||||
type D = string[];
|
||||
try {
|
||||
const body = { ids };
|
||||
const data = await http.post<D, B>(API.批量抢占控制权, body);
|
||||
return data ?? [];
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -15,8 +15,8 @@ export enum RobotType {
|
||||
export const ROBOT_TYPE_OPTIONS = <Array<[string, RobotType]>>Object.entries(RobotType).filter(([, v]) => isNumber(v));
|
||||
|
||||
export enum RobotState {
|
||||
'任务执行中' = 1,
|
||||
'充电中',
|
||||
'停靠中',
|
||||
'故障中',
|
||||
任务执行中 = 1,
|
||||
充电中,
|
||||
停靠中,
|
||||
空闲中,
|
||||
}
|
||||
|
@ -1 +1,28 @@
|
||||
$icons: (control, dot, dropdown, edit, exit, pen, plus, register, trash_fill, trash);
|
||||
$icons: (
|
||||
area1-active,
|
||||
area1,
|
||||
area2-active,
|
||||
area2,
|
||||
area3-active,
|
||||
area3,
|
||||
battery_charge,
|
||||
battery,
|
||||
connect_off,
|
||||
connect_on,
|
||||
control,
|
||||
detail,
|
||||
dot,
|
||||
dropdown,
|
||||
edit,
|
||||
exit,
|
||||
pen,
|
||||
plus,
|
||||
redo,
|
||||
register,
|
||||
robot,
|
||||
save,
|
||||
search,
|
||||
trash_fill,
|
||||
trash,
|
||||
undo
|
||||
);
|
||||
|
BIN
src/assets/icons/dark/area1-active.png
Normal file
After Width: | Height: | Size: 660 B |
BIN
src/assets/icons/dark/area1.png
Normal file
After Width: | Height: | Size: 578 B |
BIN
src/assets/icons/dark/area2-active.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/icons/dark/area2.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/icons/dark/area3-active.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/icons/dark/area3.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
src/assets/icons/dark/battery.png
Normal file
After Width: | Height: | Size: 455 B |
BIN
src/assets/icons/dark/battery_charge.png
Normal file
After Width: | Height: | Size: 693 B |
BIN
src/assets/icons/dark/connect_off.png
Normal file
After Width: | Height: | Size: 930 B |
BIN
src/assets/icons/dark/connect_on.png
Normal file
After Width: | Height: | Size: 772 B |
BIN
src/assets/icons/dark/detail.png
Normal file
After Width: | Height: | Size: 613 B |
BIN
src/assets/icons/dark/redo.png
Normal file
After Width: | Height: | Size: 620 B |
BIN
src/assets/icons/dark/robot.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/icons/dark/save.png
Normal file
After Width: | Height: | Size: 689 B |
BIN
src/assets/icons/dark/search.png
Normal file
After Width: | Height: | Size: 701 B |
BIN
src/assets/icons/dark/undo.png
Normal file
After Width: | Height: | Size: 620 B |
112
src/components/card/robot-detail-card.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { type RobotInfo, RobotState, RobotType } from '@api/robot';
|
||||
import type { EditorService } from '@core/editor.service';
|
||||
import sTheme from '@core/theme.service';
|
||||
import { computed, inject, type InjectionKey, type ShallowRef } from 'vue';
|
||||
|
||||
type Props = {
|
||||
token: InjectionKey<ShallowRef<EditorService>>;
|
||||
current?: string;
|
||||
};
|
||||
const props = defineProps<Props>();
|
||||
const editor = inject(props.token)!;
|
||||
|
||||
const robot = computed<RobotInfo | null>(() => {
|
||||
const id = props.current;
|
||||
if (!id) return null;
|
||||
return editor.value.getRobotById(id) ?? null;
|
||||
});
|
||||
|
||||
const batteryIcon = computed<string>(() => (robot.value?.state === RobotState.充电中 ? 'battery_charge' : 'battery'));
|
||||
const stateDot = computed<string>(() => `state-${robot.value?.state}`);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-card class="card-container" :bordered="false">
|
||||
<template v-if="robot">
|
||||
<a-row :gutter="[8, 8]">
|
||||
<a-col :span="24">
|
||||
<a-flex align="center" :gap="8">
|
||||
<i class="icon robot" />
|
||||
<a-typography-text class="title" style="flex: auto" :content="robot.label" ellipsis />
|
||||
<a-tag :bordered="false">{{ $t(RobotType[robot.type]) }}</a-tag>
|
||||
</a-flex>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="24">
|
||||
<a-typography-text code>{{ robot.ip }}</a-typography-text>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="24">
|
||||
<a-space align="center" :size="8">
|
||||
<a-tag v-if="robot.isConnected">
|
||||
<i class="icon connect_on size-18 mr-4" />
|
||||
<span>{{ $t('已连接') }}</span>
|
||||
</a-tag>
|
||||
<a-tag v-else>
|
||||
<i class="icon connect_off size-18 mr-4" />
|
||||
<span>{{ $t('未连接') }}</span>
|
||||
</a-tag>
|
||||
|
||||
<a-tag>
|
||||
<i class="icon size-18 mr-4" :class="batteryIcon" />
|
||||
<span>{{ robot.battery ?? 0 }}%</span>
|
||||
</a-tag>
|
||||
|
||||
<a-tag v-if="robot.state">
|
||||
<i class="dot mr-4" :class="stateDot" />
|
||||
<span>{{ $t(RobotState[robot.state]) }}</span>
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</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>{{ $t(robot.canOrder ? '接单' : '不可接单') }}</a-typography-text>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-typography-text type="secondary">{{ $t('急停状态') }}</a-typography-text>
|
||||
<a-typography-text>{{ $t(robot.canStop ? '是' : '否') }}</a-typography-text>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-typography-text type="secondary">{{ $t('控制权') }}</a-typography-text>
|
||||
<a-typography-text>{{ $t(robot.canControl ? '已抢占' : '当前无控制权') }}</a-typography-text>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</template>
|
||||
<a-empty v-else :image="sTheme.empty" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ant-typography.title {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
&.state {
|
||||
&-1 {
|
||||
background-color: #19f390;
|
||||
}
|
||||
|
||||
&-2 {
|
||||
background-color: #fdc11c;
|
||||
}
|
||||
|
||||
&-3 {
|
||||
background-color: #1982f3;
|
||||
}
|
||||
|
||||
&-4 {
|
||||
background-color: #888;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
67
src/components/editor-toolbar.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { MapAreaType } from '@api/map';
|
||||
import type { EditorService } from '@core/editor.service';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { inject, type InjectionKey, ref, type ShallowRef, watch } from 'vue';
|
||||
|
||||
type Props = {
|
||||
token: InjectionKey<ShallowRef<EditorService>>;
|
||||
editable?: boolean;
|
||||
current?: string;
|
||||
};
|
||||
const props = defineProps<Props>();
|
||||
const editor = inject(props.token)!;
|
||||
|
||||
const mode = ref<MapAreaType>();
|
||||
watch(editor.value.mouseBrush, (v) => {
|
||||
if (!mode.value) return;
|
||||
const [p1, p2] = v ?? [];
|
||||
if (isEmpty(p1) || isEmpty(p2)) return;
|
||||
editor.value.addArea(p1, p2, mode.value);
|
||||
mode.value = undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-space class="toolbar" :size="0">
|
||||
<a-button
|
||||
class="icon-btn tool-btn"
|
||||
:class="{ active: mode === MapAreaType.库区 }"
|
||||
size="large"
|
||||
@click="mode = MapAreaType.库区"
|
||||
>
|
||||
<i class="icon" :class="mode === MapAreaType.库区 ? 'area1-active' : 'area1'" />
|
||||
</a-button>
|
||||
<a-button
|
||||
class="icon-btn tool-btn ml-12"
|
||||
:class="{ active: mode === MapAreaType.互斥区 }"
|
||||
size="large"
|
||||
@click="mode = MapAreaType.互斥区"
|
||||
>
|
||||
<i class="icon" :class="mode === MapAreaType.互斥区 ? 'area2-active' : 'area2'" />
|
||||
</a-button>
|
||||
<a-button
|
||||
class="icon-btn tool-btn ml-12"
|
||||
:class="{ active: mode === MapAreaType.非互斥区 }"
|
||||
size="large"
|
||||
@click="mode = MapAreaType.非互斥区"
|
||||
>
|
||||
<i class="icon" :class="mode === MapAreaType.非互斥区 ? 'area3-active' : 'area3'" />
|
||||
</a-button>
|
||||
|
||||
<a-divider class="size-24 mh-8" type="vertical" />
|
||||
|
||||
<a-button class="icon-btn tool-btn" size="large">
|
||||
<i class="mask save" />
|
||||
</a-button>
|
||||
<a-button class="icon-btn tool-btn ml-12" size="large">
|
||||
<i class="mask undo" />
|
||||
</a-button>
|
||||
<a-button class="icon-btn tool-btn ml-12" size="large">
|
||||
<i class="mask redo" />
|
||||
</a-button>
|
||||
<a-button class="icon-btn tool-btn ml-12" size="large" :disabled="!editor.selected.value.length">
|
||||
<i class="mask trash" />
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
@ -56,7 +56,9 @@ const submit = () => {
|
||||
<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><SearchOutlined /></template>
|
||||
<template #suffix>
|
||||
<i class="icon search size-16" />
|
||||
</template>
|
||||
</a-input>
|
||||
|
||||
<a-table
|
||||
|
110
src/components/pen-groups.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { type MapPen, MapPointType } from '@api/map';
|
||||
import type { EditorService } from '@core/editor.service';
|
||||
import { computed, inject, type InjectionKey, ref, type ShallowRef } from 'vue';
|
||||
|
||||
type Props = {
|
||||
token: InjectionKey<ShallowRef<EditorService>>;
|
||||
current?: string;
|
||||
};
|
||||
const props = defineProps<Props>();
|
||||
const editor = inject(props.token)!;
|
||||
|
||||
const keyword = ref<string>('');
|
||||
|
||||
//#region 点位列表
|
||||
const points = computed<MapPen[]>(() =>
|
||||
editor.value.points.value.filter(({ label }) => label?.includes(keyword.value)),
|
||||
);
|
||||
//#endregion
|
||||
|
||||
//#region 线路列表
|
||||
//#endregion
|
||||
|
||||
//#region 区域列表
|
||||
//#endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-input class="search mb-16" :placeholder="$t('请输入搜索关键字')" v-model:value="keyword">
|
||||
<template #suffix>
|
||||
<i class="icon search size-16" />
|
||||
</template>
|
||||
</a-input>
|
||||
|
||||
<a-collapse style="flex: auto; overflow-y: auto" expand-icon-position="end" ghost>
|
||||
<template #expandIcon="v">
|
||||
<i class="icon dropdown" :class="{ active: v?.isActive }" />
|
||||
</template>
|
||||
|
||||
<a-collapse-panel :header="$t('互斥区')"></a-collapse-panel>
|
||||
|
||||
<a-collapse-panel :header="$t('非互斥区')"></a-collapse-panel>
|
||||
|
||||
<a-collapse-panel :header="$t('仅载货可通行路线')"></a-collapse-panel>
|
||||
|
||||
<a-collapse-panel :header="$t('仅空载可通行路线')"></a-collapse-panel>
|
||||
|
||||
<a-collapse-panel :header="$t('禁行路线')"></a-collapse-panel>
|
||||
|
||||
<a-collapse-panel :header="$t('等待点')">
|
||||
<a-list rowKey="id" :data-source="points.filter(({ point }) => point?.type === MapPointType.等待点)">
|
||||
<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.充电点)">
|
||||
<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.停靠点)">
|
||||
<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.禁行点)">
|
||||
<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>
|
||||
</template>
|
@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { type RobotGroup, type RobotInfo } from '@api/robot';
|
||||
import { type RobotGroup, type RobotInfo, seizeRobotByIds } from '@api/robot';
|
||||
import type { RobotAddModalRef } from '@common/modal/robot-add-modal.vue';
|
||||
import type { RobotGroupRenameModalRef } from '@common/modal/robot-group-rename-modal.vue';
|
||||
import type { RobotRegisterModalRef } from '@common/modal/robot-register-modal.vue';
|
||||
import type { EditorService } from '@core/editor.service';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { map } from 'lodash-es';
|
||||
import { watch } from 'vue';
|
||||
import { reactive } from 'vue';
|
||||
import { computed, inject, type InjectionKey, ref, type ShallowRef, shallowRef } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@ -25,7 +26,10 @@ const emit = defineEmits<Events>();
|
||||
const { t } = useI18n();
|
||||
|
||||
//#region 接口
|
||||
|
||||
const seizeRobots = async () => {
|
||||
const res = await seizeRobotByIds([...selected.keys()]);
|
||||
editor.value.updateRobots(res, { canControl: true });
|
||||
};
|
||||
//#endregion
|
||||
|
||||
const keyword = ref<string>('');
|
||||
@ -33,15 +37,39 @@ const keyword = ref<string>('');
|
||||
//#region 机器人列表
|
||||
const groups = computed<RobotGroup[]>(() => editor.value.robotGroups.value ?? []);
|
||||
const robots = computed<RobotInfo[]>(() => editor.value.robots.filter(({ label }) => label.includes(keyword.value)));
|
||||
|
||||
const getGroupRobots = (ids: RobotGroup['robots']) =>
|
||||
ids?.map((id) => editor.value.getRobotById(id)).filter((robot) => robot?.label.includes(keyword.value));
|
||||
|
||||
const selected = ref<RobotInfo['id'][]>([]);
|
||||
const isAllSelected = computed<boolean>(() => robots.value.every(({ id }) => selected.value.includes(id)));
|
||||
const selected = reactive<Set<RobotInfo['id']>>(new Set());
|
||||
watch(
|
||||
() => props.editable,
|
||||
() => selected.clear(),
|
||||
);
|
||||
|
||||
const isAllSelected = computed<boolean>(() => robots.value.every(({ id }) => selected.has(id)));
|
||||
const selectAll = (checked: boolean) => {
|
||||
selected.value = checked ? map(robots.value, 'id') : [];
|
||||
if (checked) {
|
||||
robots.value.forEach(({ id }) => selected.add(id));
|
||||
} else {
|
||||
selected.clear();
|
||||
}
|
||||
};
|
||||
|
||||
const checkGroupSelected = (ids: RobotGroup['robots']) => !!ids?.length && ids.every((id) => selected.has(id));
|
||||
const selectGroup = (ids: RobotGroup['robots'], checked: boolean) => {
|
||||
if (checked) {
|
||||
ids?.forEach((id) => selected.add(id));
|
||||
} else {
|
||||
ids?.forEach((id) => selected.delete(id));
|
||||
}
|
||||
};
|
||||
|
||||
const selectRobot = (id: RobotInfo['id'], checked: boolean) => {
|
||||
if (checked) {
|
||||
selected.add(id);
|
||||
} else {
|
||||
selected.delete(id);
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
|
||||
@ -60,6 +88,21 @@ const toDeleteGroup = (id: RobotGroup['id']) =>
|
||||
onOk: () => editor.value.deleteRobotGroup(id),
|
||||
});
|
||||
//#endregion
|
||||
|
||||
//#region 机器人操作
|
||||
const toRemoveRobots = () =>
|
||||
Modal.confirm({
|
||||
class: 'confirm',
|
||||
title: t('您确定要从场景中移除已选机器人吗?'),
|
||||
centered: true,
|
||||
cancelText: t('返回'),
|
||||
okText: t('移除'),
|
||||
onOk: () => {
|
||||
editor.value.removeRobots([...selected.keys()]);
|
||||
selected.clear();
|
||||
},
|
||||
});
|
||||
//#endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -69,16 +112,30 @@ const toDeleteGroup = (id: RobotGroup['id']) =>
|
||||
|
||||
<a-flex class="full" vertical>
|
||||
<a-input class="search mb-16" :placeholder="$t('请输入搜索关键字')" v-model:value="keyword">
|
||||
<template #suffix><SearchOutlined /></template>
|
||||
<template #suffix>
|
||||
<i class="icon search size-16" />
|
||||
</template>
|
||||
</a-input>
|
||||
|
||||
<a-flex v-if="editable" class="mb-8" style="height: 32px" justify="space-between" align="center">
|
||||
<a-checkbox :checked="isAllSelected" @change="selectAll($event.target.checked)">{{ $t('全选') }}</a-checkbox>
|
||||
<a-space align="center">
|
||||
<a-button class="icon-btn panel-btn" size="small" :disabled="!selected.length">
|
||||
<a-button
|
||||
class="icon-btn panel-btn"
|
||||
:title="$t('抢占控制权')"
|
||||
size="small"
|
||||
@click="seizeRobots"
|
||||
:disabled="!selected.size"
|
||||
>
|
||||
<i class="mask control" />
|
||||
</a-button>
|
||||
<a-button class="icon-btn panel-btn" size="small" :disabled="!selected.length">
|
||||
<a-button
|
||||
class="icon-btn panel-btn"
|
||||
:title="$t('移除机器人')"
|
||||
size="small"
|
||||
@click="toRemoveRobots"
|
||||
:disabled="!selected.size"
|
||||
>
|
||||
<i class="mask trash_fill" />
|
||||
</a-button>
|
||||
</a-space>
|
||||
@ -88,10 +145,11 @@ const toDeleteGroup = (id: RobotGroup['id']) =>
|
||||
<template #expandIcon="v">
|
||||
<i class="icon dropdown" :class="{ active: v?.isActive }" />
|
||||
</template>
|
||||
<a-collapse-panel v-for="{ id, label, robots } in groups" :key="id" :header="label">
|
||||
|
||||
<a-collapse-panel v-for="{ id, label, robots } in groups" :key="id">
|
||||
<template v-if="editable" #extra>
|
||||
<a-dropdown placement="bottomRight">
|
||||
<a-button class="icon-btn" size="small">
|
||||
<a-button class="icon-btn" size="small" @click.stop>
|
||||
<i class="icon dot" />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
@ -124,7 +182,20 @@ const toDeleteGroup = (id: RobotGroup['id']) =>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<a-list :data-source="getGroupRobots(robots)">
|
||||
|
||||
<template #header>
|
||||
<a-space align="center" :size="8">
|
||||
<a-checkbox
|
||||
v-if="editable"
|
||||
:checked="checkGroupSelected(robots)"
|
||||
@change="selectGroup(robots, $event.target.checked)"
|
||||
@click.stop
|
||||
/>
|
||||
<span>{{ label }}</span>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-list rowKey="id" :data-source="getGroupRobots(robots)">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item
|
||||
class="ph-16"
|
||||
@ -132,8 +203,15 @@ const toDeleteGroup = (id: RobotGroup['id']) =>
|
||||
style="height: 36px"
|
||||
@click="emit('change', item.id)"
|
||||
>
|
||||
<template #actions> </template>
|
||||
<a-typography-text>{{ item.label }}</a-typography-text>
|
||||
<a-space align="center" :size="8">
|
||||
<a-checkbox
|
||||
v-if="editable"
|
||||
:checked="selected.has(item.id)"
|
||||
@change="selectRobot(item.id, $event.target.checked)"
|
||||
@click.stop
|
||||
/>
|
||||
<a-typography-text type="secondary">{{ item.label }}</a-typography-text>
|
||||
</a-space>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
@ -146,9 +224,3 @@ const toDeleteGroup = (id: RobotGroup['id']) =>
|
||||
</a-button>
|
||||
</a-flex>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.btn {
|
||||
justify-self: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
21
src/components/test-2.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<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> -->
|
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { getSceneById } from '@api/scene';
|
||||
import { EditorService } from '@core/editor.service';
|
||||
import { watch } from 'vue';
|
||||
import { computed, nextTick, watch } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { onMounted, provide, shallowRef } from 'vue';
|
||||
|
||||
@ -37,14 +37,38 @@ onMounted(() => {
|
||||
const editable = ref<boolean>(false);
|
||||
watch(editable, (v) => editor.value?.setState(v));
|
||||
|
||||
const current = ref<string>();
|
||||
const show = ref<boolean>(true);
|
||||
const current = ref<{ type: 'robot' | 'point' | 'line' | 'area'; id: string }>();
|
||||
watch(
|
||||
() => editor.value?.selected.value[0],
|
||||
(v) => {
|
||||
if (v) {
|
||||
const [pen] = editor.value?.find(v) ?? [];
|
||||
if (pen?.id) {
|
||||
current.value = { type: <'point' | 'line' | 'area'>pen.name, id: pen.id };
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (current.value?.type === 'robot') return;
|
||||
current.value = undefined;
|
||||
},
|
||||
);
|
||||
const isRobot = computed(() => current.value?.type === 'robot');
|
||||
const isPoint = computed(() => current.value?.type === 'point');
|
||||
const isRoute = computed(() => current.value?.type === 'line');
|
||||
const isArea = computed(() => current.value?.type === 'area');
|
||||
|
||||
const selectRobot = (id: string) => {
|
||||
current.value = { type: 'robot', id };
|
||||
editor.value?.inactive();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-layout class="full">
|
||||
<a-layout-header class="p-16" style="height: 64px">
|
||||
<a-flex justify="space-between" align="center">
|
||||
<a-typography-text strong>{{ title }}</a-typography-text>
|
||||
<a-typography-text class="title">{{ title }}</a-typography-text>
|
||||
<a-space align="center">
|
||||
<a-button v-if="editable" class="warning" @click="editable = false">
|
||||
<i class="icon exit size-18 mr-8" />
|
||||
@ -56,25 +80,27 @@ const current = ref<string>();
|
||||
</a-button>
|
||||
<a-button>{{ $t('推送') }}</a-button>
|
||||
<a-button>{{ $t('导入') }}</a-button>
|
||||
<a-button>{{ $t('导出') }}</a-button>
|
||||
<a-button @click="editor?.export()">{{ $t('导出') }}</a-button>
|
||||
</a-space>
|
||||
</a-flex>
|
||||
</a-layout-header>
|
||||
|
||||
<a-layout class="p-16">
|
||||
<a-layout-sider :width="320">
|
||||
<a-tabs class="full" type="card">
|
||||
<a-tabs type="card">
|
||||
<a-tab-pane key="1" :tab="$t('机器人')">
|
||||
<RobotGroups
|
||||
v-if="editor"
|
||||
:token="EDITOR_KEY"
|
||||
:editable="editable"
|
||||
:current="current"
|
||||
@change="current = $event"
|
||||
:current="current?.id"
|
||||
@change="selectRobot"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" :tab="$t('库区')">Content of Tab Pane 2</a-tab-pane>
|
||||
<a-tab-pane key="3" :tab="$t('高级组')">Content of Tab Pane 3</a-tab-pane>
|
||||
<a-tab-pane key="3" :tab="$t('高级组')">
|
||||
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-layout-sider>
|
||||
<a-layout-content>
|
||||
@ -82,10 +108,45 @@ const current = ref<string>();
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
|
||||
<div v-if="editable" class="toolbar-container">
|
||||
<EditorToolbar v-if="editor" :token="EDITOR_KEY" />
|
||||
</div>
|
||||
|
||||
<template v-if="current?.id">
|
||||
<a-float-button style="top: 80px; right: 16px" shape="square" @click="show = !show">
|
||||
<template #icon><i class="icon detail" /></template>
|
||||
</a-float-button>
|
||||
<div v-if="show" class="card-container">
|
||||
<RobotDetailCard v-if="isRobot" :token="EDITOR_KEY" :current="current.id" />
|
||||
|
||||
<template v-if="isPoint"> </template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.editor-container {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.toolbar-container {
|
||||
position: fixed;
|
||||
right: 50%;
|
||||
bottom: 40px;
|
||||
left: 50%;
|
||||
z-index: 8888;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: 64px;
|
||||
z-index: 8888;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.ant-typography.title {
|
||||
font: 500 16px/22px SourceHanSansSC;
|
||||
}
|
||||
</style>
|
||||
|
@ -2,28 +2,35 @@ import { EDITOR_CONFIG, MapAreaType, type MapPen, 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, s8 } from '@meta2d/core';
|
||||
import { CanvasLayer, EditType, LockState, Meta2d, type Pen, s8 } from '@meta2d/core';
|
||||
import { useObservable } from '@vueuse/rxjs';
|
||||
import { clone, cloneDeep, get, isNil, pick, remove, some } from 'lodash-es';
|
||||
import { clone, cloneDeep, get, isNil, isString, pick, remove, some } from 'lodash-es';
|
||||
import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs';
|
||||
import { watch } from 'vue';
|
||||
import { reactive, watch } from 'vue';
|
||||
|
||||
export type Point = Record<'x' | 'y', number>;
|
||||
|
||||
export class EditorService extends Meta2d {
|
||||
public load(map?: string, readonly = false): void {
|
||||
public async load(map?: string, editable = false): Promise<void> {
|
||||
const data = map ? JSON.parse(map) : undefined;
|
||||
this.open(data);
|
||||
this.setState(readonly);
|
||||
this.setState(editable);
|
||||
}
|
||||
public save(): string {
|
||||
const data = this.data();
|
||||
const map = JSON.stringify(data);
|
||||
return map;
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
public export(): string {
|
||||
const png = this.toPng(10);
|
||||
return png;
|
||||
public export(): void {
|
||||
const json = this.save();
|
||||
console.log(json);
|
||||
}
|
||||
|
||||
public setState(editable?: boolean): void {
|
||||
this.lock(editable ? LockState.None : LockState.Disable);
|
||||
}
|
||||
|
||||
public override data(): SceneData {
|
||||
return super.data();
|
||||
}
|
||||
|
||||
readonly #mouse$$ = new Subject<{ type: 'click' | 'mousedown' | 'mouseup'; value: Point }>();
|
||||
@ -46,19 +53,8 @@ export class EditorService extends Meta2d {
|
||||
),
|
||||
);
|
||||
|
||||
public override data(): SceneData {
|
||||
return super.data();
|
||||
}
|
||||
public override find(target: string): MapPen[] {
|
||||
return super.find(target);
|
||||
}
|
||||
|
||||
public setState(readonly?: boolean): void {
|
||||
this.lock(readonly ? LockState.Disable : LockState.None);
|
||||
}
|
||||
|
||||
//#region 机器人
|
||||
readonly #robotMap = new Map<RobotInfo['id'], RobotInfo>();
|
||||
readonly #robotMap = reactive<Map<RobotInfo['id'], RobotInfo>>(new Map());
|
||||
public get robots(): RobotInfo[] {
|
||||
return Array.from(this.#robotMap.values());
|
||||
}
|
||||
@ -81,6 +77,22 @@ export class EditorService extends Meta2d {
|
||||
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
|
||||
}
|
||||
public removeRobots(ids: RobotInfo['id'][]): void {
|
||||
ids?.forEach((v) => this.#robotMap.delete(v));
|
||||
const groups = clone(this.#robotGroups$$.value);
|
||||
groups.forEach(({ robots }) => remove(robots ?? [], (v) => !this.#robotMap.has(v)));
|
||||
this.#robotGroups$$.next(groups);
|
||||
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
|
||||
}
|
||||
public updateRobots(ids: RobotInfo['id'][], values: Partial<RobotInfo>): void {
|
||||
ids?.forEach((v) => {
|
||||
const robot = this.#robotMap.get(v);
|
||||
if (isNil(robot)) return;
|
||||
this.#robotMap.set(v, { ...robot, ...values });
|
||||
});
|
||||
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||
}
|
||||
|
||||
readonly #robotGroups$$ = new BehaviorSubject<RobotGroup[]>([]);
|
||||
public readonly robotGroups = useObservable<RobotGroup[]>(this.#robotGroups$$.pipe(debounceTime(300)));
|
||||
@ -113,7 +125,47 @@ export class EditorService extends Meta2d {
|
||||
}
|
||||
//#endregion
|
||||
|
||||
readonly #change$$ = new Subject<boolean>();
|
||||
public readonly selected = useObservable<string[], string[]>(
|
||||
this.#change$$.pipe(
|
||||
filter((v) => !v),
|
||||
debounceTime(100),
|
||||
map(() => this.store.active?.map(({ id }) => id).filter((v) => !isNil(v)) ?? []),
|
||||
),
|
||||
{ initialValue: new Array<string>() },
|
||||
);
|
||||
public readonly pens = useObservable<MapPen[]>(
|
||||
this.#change$$.pipe(
|
||||
filter((v) => v),
|
||||
debounceTime(100),
|
||||
map(() => this.data().pens),
|
||||
),
|
||||
);
|
||||
|
||||
public override find(target: string): MapPen[] {
|
||||
return super.find(target);
|
||||
}
|
||||
|
||||
public override active(target: string | Pen[], emit?: boolean): void {
|
||||
const pens = isString(target) ? this.find(target) : target;
|
||||
super.active(pens, emit);
|
||||
this.render();
|
||||
}
|
||||
public override inactive(): void {
|
||||
super.inactive();
|
||||
this.render();
|
||||
}
|
||||
|
||||
//#region 点位
|
||||
public readonly points = useObservable<MapPen[], MapPen[]>(
|
||||
this.#change$$.pipe(
|
||||
filter((v) => v),
|
||||
debounceTime(100),
|
||||
map(() => this.find('point')),
|
||||
),
|
||||
{ initialValue: new Array<MapPen>() },
|
||||
);
|
||||
|
||||
public async addPoint(p: Point, type = MapPointType.普通点): Promise<void> {
|
||||
const id = s8();
|
||||
const pen: MapPen = {
|
||||
@ -130,7 +182,7 @@ export class EditorService extends Meta2d {
|
||||
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)] });
|
||||
// this.pushHistory({ type: EditType.Add, pens: [cloneDeep(pen)] });
|
||||
}
|
||||
|
||||
#mapPoint(type: MapPointType): Required<Pick<MapPen, 'width' | 'height' | 'lineWidth' | 'iconSize'>> {
|
||||
@ -186,10 +238,8 @@ export class EditorService extends Meta2d {
|
||||
);
|
||||
}
|
||||
|
||||
#load(theme?: string): void {
|
||||
if (theme) {
|
||||
this.setTheme(theme);
|
||||
}
|
||||
#load(theme: string): void {
|
||||
this.setTheme(theme);
|
||||
|
||||
const { robots, robotGroups } = this.data();
|
||||
this.#robotMap.clear();
|
||||
@ -208,9 +258,50 @@ export class EditorService extends Meta2d {
|
||||
#listen(e: unknown, v: any) {
|
||||
switch (e) {
|
||||
case 'opened':
|
||||
this.#load();
|
||||
this.#load(sTheme.theme);
|
||||
this.#change$$.next(true);
|
||||
break;
|
||||
|
||||
case 'add':
|
||||
case 'delete':
|
||||
case 'update':
|
||||
case 'valueUpdate':
|
||||
this.#change$$.next(true);
|
||||
break;
|
||||
|
||||
case 'active':
|
||||
case 'inactive':
|
||||
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':
|
||||
@ -280,6 +371,7 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
||||
case MapPointType.充电点:
|
||||
case MapPointType.停靠点:
|
||||
case MapPointType.动作点:
|
||||
case MapPointType.禁行点:
|
||||
ctx.roundRect(x, y, w, h, r);
|
||||
ctx.strokeStyle = get(theme, active ? 'point-l.strokeActive' : 'point-l.stroke') ?? '';
|
||||
ctx.stroke();
|
||||
|
@ -41,6 +41,11 @@ class ThemeService {
|
||||
return THEME_MAP[`editor-${this.#theme.value}`] ?? {};
|
||||
}
|
||||
|
||||
public get empty(): string {
|
||||
const { href } = new URL(`../assets/images/empty-${this.#theme.value}.png`, import.meta.url);
|
||||
return href;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
watch(this.#theme, (v) => this.#load(v), { immediate: true });
|
||||
}
|
||||
|