feat: style

This commit is contained in:
chndfang 2025-05-05 01:06:09 +08:00
parent 07f7a378c0
commit 46710c9134
34 changed files with 1265 additions and 238 deletions

View File

@ -3,44 +3,46 @@
"success": true,
"data": [
{
"gid": "mock-robot-group-1",
"id": "mock-robot-1",
"label": "模拟机器人A",
"brand": "模拟品牌A",
"type": 1
"type": 1,
"ip": "127.0.1.1"
},
{
"gid": "mock-robot-group-1",
"id": "mock-robot-2",
"label": "模拟机器人B",
"brand": "模拟品牌A",
"type": 2
"type": 2,
"ip": "127.0.1.2"
},
{
"gid": "mock-robot-group-1",
"id": "mock-robot-3",
"label": "模拟机器人C",
"brand": "模拟品牌A",
"type": 3
"type": 3,
"ip": "127.0.1.3"
},
{
"gid": "mock-robot-group-2",
"id": "mock-robot-4",
"label": "模拟机器人D",
"brand": "模拟品牌B",
"type": 1
"type": 1,
"ip": "127.0.2.1"
},
{
"id": "mock-robot-5",
"label": "模拟机器人E",
"brand": "模拟品牌B",
"type": 2
"type": 2,
"ip": "127.0.2.2"
},
{
"id": "mock-robot-6",
"label": "模拟机器人F",
"brand": "模拟品牌B",
"type": 3
"type": 3,
"ip": "127.0.2.3"
}
],
"message": "模拟提示"

12
mocks/robot/register Normal file
View File

@ -0,0 +1,12 @@
{
"code": 200,
"success": true,
"data": {
"id": "mock-robot-0",
"label": "模拟机器人-注册",
"brand": "模拟品牌A",
"type": 1,
"ip": "127.0.0.0"
},
"message": "模拟提示"
}

5
mocks/robot/seizeByIds Normal file
View File

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

View File

@ -4,49 +4,7 @@
"data": {
"id": "mock-scene-1",
"label": "模拟场景A",
"robotGroups": [
{
"id": "mock-robot-group-1",
"label": "模拟机器人组A",
"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
}
]
},
{
"id": "mock-robot-group-2",
"label": "模拟机器人组B",
"robots": [
{
"gid": "mock-robot-group-2",
"id": "mock-robot-4",
"label": "模拟机器人D",
"brand": "模拟品牌B",
"type": 1
}
]
}
],
"map": ""
"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}]}"
},
"message": "模拟提示"
}

View File

@ -1,6 +1,9 @@
<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>
@ -9,7 +12,14 @@ import sTheme from '@core/theme.service';
:theme="sTheme.ant"
:autoInsertSpaceInButton="false"
:wave="{ disabled: true }"
:input="{ autocomplete: 'off' }"
>
<template #renderEmpty>
<a-flex justify="center" align="center" :gap="8" vertical>
<img height="40" :src="empty" />
<a-typography-text disabled>{{ $t('暂无数据') }}</a-typography-text>
</a-flex>
</template>
<a-app @contextmenu.prevent>
<router-view />
</a-app>

View File

@ -10,36 +10,47 @@
.ant-btn {
display: inline-flex;
align-items: center;
justify-content: center;
font: 400 14px/22px Roboto;
vertical-align: top;
border-radius: 2px;
box-shadow: none !important;
&.ant-btn-lg {
padding-block: 8px;
border-radius: 4px;
}
&.ant-btn-default {
color: get-color(primary);
background-color: get-color(141414);
background-color: get-color(neutral1);
border-color: get-color(primary);
box-shadow: 0 2px 0 0 get-color(00000005);
}
&.ant-btn-dashed {
font-weight: 500;
color: get-color(primary_text-active);
background-color: get-color(info_bg);
border-color: get-color(primary_border);
}
&.ant-btn-primary {
font-weight: 500;
color: get-color(141414);
color: get-color(text1-inverse);
background-color: get-color(primary);
border-color: get-color(primary);
box-shadow: 0 2px 0 0 get-color(0000000a);
}
&.warning {
font-weight: 500;
color: get-color(141414);
color: get-color(text1-inverse);
background-color: get-color(warning);
border-color: get-color(warning);
box-shadow: 0 2px 0 0 get-color(0000000a);
}
&.icon-btn {
background-color: transparent;
border: none;
box-shadow: none;
&.ant-btn-sm {
padding: 0;
@ -53,8 +64,8 @@
}
& > .ant-checkbox-inner {
background-color: get-color(141414);
border-color: get-color(434343);
background-color: get-color(neutral1);
border-color: get-color(neutral5);
border-radius: 2px;
}
@ -63,7 +74,13 @@
border-color: get-color(primary);
&::after {
border-color: get-color(141414);
border-color: get-color(neutral1);
}
}
&.ant-checkbox-indeterminate:not(.ant-checkbox-disabled) > .ant-checkbox-inner {
&::after {
background-color: get-color(primary);
}
}
}
@ -71,7 +88,7 @@
.ant-checkbox-wrapper {
align-items: center;
font: 400 14px/22px Roboto;
color: get-color(ffffffa6);
color: get-color(text2);
}
.ant-collapse.ant-collapse-ghost {
@ -79,14 +96,19 @@
& > .ant-collapse-item {
& > .ant-collapse-header {
gap: 8px;
align-items: center;
height: 36px;
padding: 0 0 0 8px;
padding: 0;
font: 500 14px/22px Roboto;
color: get-color(ffffffd9);
background: get-color(ffffff14);
color: get-color(text1);
background: get-color(fill3);
border-radius: 2px;
& > .ant-collapse-header-text {
padding-inline: 8px;
}
& > .ant-collapse-expand-icon {
width: 24px;
height: 24px;
@ -114,59 +136,355 @@
}
}
.ant-input {
color: get-color(ffffffd9);
background-color: transparent;
.ant-dropdown-menu-root {
padding: 0;
overflow: hidden;
background-color: get-color(neutral9);
border-radius: 4px;
box-shadow: 0 2px 8px 0 get-color(shadow2);
&::placeholder {
color: get-color(ffffff40);
& > .ant-dropdown-menu-item {
padding: 8px 16px;
font: 400 14px/16px Roboto;
color: get-color(text1-inverse);
border-radius: 0;
&:hover {
background-color: get-color(neutral8);
}
}
}
.ant-input-affix-wrapper {
box-shadow: none;
.ant-flex {
& > * {
flex: none;
}
}
.ant-form {
& > .ant-form-item {
margin-block-end: 8px;
& > .ant-form-item-row {
justify-content: flex-end;
}
.ant-form-item-label {
font: 400 14px/22px Roboto;
color: get-color(text1);
& > .ant-form-item-required::before {
color: get-color(error);
}
}
.ant-form-item-control {
max-width: 292px;
}
}
}
.ant-input {
color: get-color(text1);
background-color: get-color(neutral1);
border-color: get-color(neutral5);
border-radius: 2px;
box-shadow: none !important;
&.ant-input-status-error {
border-color: get-color(error) !important;
outline-color: rgba($color: get-color(error), $alpha: 20%) !important;
}
&:not(:disabled):focus {
border-color: get-color(primary);
outline: 2px solid rgba($color: get-color(primary), $alpha: 20%);
transition: none;
}
&::placeholder {
color: get-color(text4);
}
}
.ant-input-affix-wrapper:not(.ant-input-affix-wrapper-borderless) {
border-radius: 2px;
box-shadow: none !important;
&.ant-input-affix-wrapper-status-error {
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 {
border-color: get-color(primary);
outline: 2px solid rgba($color: get-color(primary), $alpha: 20%);
transition: none;
}
&::before {
display: none;
}
& > .ant-input {
background-color: transparent;
outline: none;
}
& > .ant-input-suffix {
margin-inline-start: 12px;
font-size: 16px;
color: get-color(ffffffd9);
color: get-color(icon-disabled);
}
&.search {
padding-inline-start: 7px;
background-color: transparent;
border-color: get-color(595959);
border-radius: 4px;
background-color: get-color(fill3);
border-color: get-color(border2);
}
}
.ant-input-number {
width: 100%;
color: get-color(text1);
background-color: get-color(neutral1);
border-color: get-color(neutral5);
border-radius: 2px;
box-shadow: none !important;
&.ant-input-number-status-error {
border-color: get-color(error) !important;
outline-color: rgba($color: get-color(error), $alpha: 20%) !important;
}
&:not(:disabled).ant-input-number-focused {
border-color: get-color(primary);
outline: 2px solid rgba($color: get-color(primary), $alpha: 20%);
transition: none;
}
}
.ant-layout {
background-color: get-color(2a2c2c);
background-color: get-color(page_bg);
& > .ant-layout-header {
z-index: 10;
line-height: unset;
background-color: get-color(242525);
box-shadow: 0 2px 8px 0 get-color(00000026);
background-color: get-color(sider_bg);
box-shadow: 0 2px 8px 0 get-color(shadow2);
}
& > .ant-layout-sider {
background-color: get-color(36393a);
z-index: 20;
background-color: get-color(sider_bg2);
border-radius: 8px;
box-shadow: 0 2px 8px 0 get-color(ffffff14);
box-shadow: 0 2px 8px 0 get-color(shadow1);
}
}
.ant-list-items {
& > .ant-list-item {
padding: 0;
border-color: get-color(f9f9f914);
border-color: get-color(item_bg-hover);
&:hover {
background-color: get-color(f9f9f914);
background-color: get-color(item_bg-hover);
}
&.selected {
background-color: get-color(primary_bg);
}
}
}
.ant-modal.ant-modal-confirm .ant-modal-content {
padding: 32px 32px 24px;
background-color: get-color(popover);
border-radius: 2px;
box-shadow: none !important;
& > .ant-modal-body > .ant-modal-confirm-body-wrapper {
& > .ant-modal-confirm-body {
& > .anticon {
margin-inline-end: 16px;
color: get-color(warning);
}
& > .ant-modal-confirm-title {
font: 500 16px/24px Roboto;
color: get-color(text1);
}
& > .ant-modal-confirm-content {
margin: 0;
}
}
& > .ant-modal-confirm-btns {
margin-block-start: 24px;
}
}
}
.ant-modal:not(.ant-modal-confirm) .ant-modal-content {
padding: 0;
background-color: get-color(popover);
border-radius: 2px;
box-shadow: none !important;
& > .ant-modal-close {
inset: 17px 16px 17px auto;
width: 1em;
height: 1em;
font-size: 16px;
color: get-color(text3);
background-color: transparent;
& > .ant-modal-close-x {
width: 1em;
height: 1em;
line-height: 1;
}
}
& > .ant-modal-header {
padding: 14px 16px;
margin: 0;
background-color: get-color(bg_layout);
border-radius: 2px 2px 0 0;
& > .ant-modal-title {
font: 500 16px/22px Roboto;
color: get-color(text1);
}
}
& > .ant-modal-body {
padding: 24px 16px;
}
& > .ant-modal-footer {
padding: 16px;
margin: 0;
border-block-start: 1px solid get-color(border2);
}
}
.ant-modal-mask {
background-color: get-color(mask);
}
.ant-select:not(.ant-select-borderless) {
& > .ant-select-selector {
color: get-color(text1);
background-color: get-color(neutral1);
border-color: get-color(neutral5);
border-radius: 2px;
box-shadow: none !important;
& > .ant-select-selection-item {
color: inherit;
}
& > .ant-select-selection-placeholder {
color: get-color(text4);
}
}
&.ant-select-disabled > .ant-select-selector {
color: get-color(text4);
background-color: get-color(fill3);
}
&.ant-select-status-error > .ant-select-selector {
border-color: get-color(error) !important;
outline-color: rgba($color: get-color(error), $alpha: 20%) !important;
}
&:not(.ant-select-disabled):hover > .ant-select-selector {
border-color: get-color(neutral5);
}
&:not(.ant-select-disabled).ant-select-focused > .ant-select-selector {
border-color: get-color(primary);
outline: 2px solid rgba($color: get-color(primary), $alpha: 20%);
transition: none;
}
& > .ant-select-arrow {
color: get-color(icon-disabled);
}
}
.ant-select-dropdown {
padding: 4px 0;
background-color: get-color(popover);
border-radius: 2px;
box-shadow: 0 0 4px 0 get-color(shadow1);
.ant-select-item {
font: 400 14px/22px Roboto;
color: get-color(text1);
background-color: transparent;
border-radius: 0;
&.ant-select-item-option-selected {
background-color: get-color(fill1);
}
&:not(.ant-select-item-option-disabled):hover {
background-color: get-color(item_bg-hover);
}
}
}
.ant-table.ant-table-bordered {
background-color: transparent;
border-radius: 0;
& > .ant-table-container {
border-color: get-color(divider);
border-radius: 0;
& > .ant-table-header {
border-radius: 0;
& > table {
border-color: get-color(divider);
border-radius: 0;
& > .ant-table-thead > tr > .ant-table-cell {
padding: 9px 16px 8px;
font: 500 14px/22px Roboto;
color: get-color(text3);
background-color: get-color(neutral3);
border-color: get-color(divider);
border-radius: 0;
}
}
}
& > .ant-table-body {
& > table {
& > .ant-table-tbody > .ant-table-row > .ant-table-cell {
padding: 9px 16px 8px;
font: 400 14px/22px Roboto;
color: get-color(text1);
background-color: transparent;
border-color: get-color(divider);
}
}
}
& > .ant-table-header > table > .ant-table-thead > tr > .ant-table-cell.ant-table-selection-column,
& > .ant-table-body > table > .ant-table-tbody > .ant-table-row > .ant-table-cell.ant-table-selection-column {
padding: 0;
}
& > .ant-table-body > table > .ant-table-tbody > .ant-table-placeholder > .ant-table-cell {
padding: 8px 0 0;
background-color: transparent;
border-color: get-color(divider);
}
}
}
@ -176,14 +494,14 @@
& > .ant-tabs-nav {
margin: 0;
background-color: get-color(ffffff2e);
background-color: get-color(fill1);
& > .ant-tabs-nav-wrap > .ant-tabs-nav-list {
& > .ant-tabs-tab {
padding: 14px 32px;
margin: 0;
font: 400 16px/20px Roboto;
color: get-color(ffffff73);
color: get-color(text3);
background-color: transparent;
border: none;
border-radius: 0;
@ -196,7 +514,7 @@
&.ant-tabs-tab-active {
font-weight: 600;
color: get-color(primary);
background-color: get-color(36393a);
background-color: get-color(sider_bg2);
}
}
@ -215,11 +533,15 @@
.ant-typography {
font: 400 14px/22px Roboto;
color: get-color(ffffffa6);
color: get-color(text2);
&.ant-typography-disabled {
color: get-color(text4);
}
& > strong {
font: 500 16px/22px SourceHanSansSC;
color: get-color(ffffffd9);
color: get-color(text1);
}
}
}

View File

@ -2,9 +2,13 @@
@use 'asset/icons/icon' as *;
@include themed {
i {
&.bg-1 {
background-color: get-color(ffffffd9);
.icon-btn {
&.panel-btn {
color: get-color(ffffffd9);
&:disabled {
color: get-color(ffffff2e);
}
}
}
@ -21,6 +25,13 @@
.mask {
@extend %icon;
color: inherit;
background-color: currentcolor;
&.primary {
color: get-color(icon-brand);
}
@each $icon in $icons {
&.#{$icon} {
mask: get-icon($icon);

View File

@ -12,13 +12,13 @@ export interface MapPen extends Pen {
area?: MapAreaInfo; // 区域信息
attrs?: Record<string, unknown>; // 额外属性
activeAttrs?: string[]; // 已激活的额外属性
activeAttrs?: Array<string>; // 已激活的额外属性
}
//#region 点位
export interface MapPointInfo {
type: MapPointType; // 点位类型
robots?: RobotInfo['id'][]; // 绑定机器人id集合
robots?: Array<RobotInfo['id']>; // 绑定机器人id集合
}
//#endregion
@ -32,7 +32,7 @@ export interface MapRouteInfo {
//#region 区域
export interface MapAreaInfo {
type: MapAreaType; // 区域类型
points?: string[]; // 绑定点位id集合
routes?: string[]; // 绑定线路id集合
points?: Array<string>; // 绑定点位id集合
routes?: Array<string>; // 绑定线路id集合
}
//#endregion

View File

@ -1,13 +1,39 @@
import http from '@core/http';
import type { RobotInfo } from './type';
import type { RobotDetail, RobotInfo } from './type';
const enum API {
= '/robot/getAll',
= '/robot/register',
= '/robot/seizeByIds',
}
export async function getAllRobot(): Promise<RobotInfo[]> {
export async function getAllRobots(): Promise<Array<RobotInfo>> {
type D = RobotInfo[];
const data = await http.get<D>(API.);
const data = await http.post<D>(API.);
return data ?? [];
}
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;
} catch (error) {
console.debug(error);
return false;
}
}

View File

@ -1,12 +1,18 @@
import { isNumber } from 'lodash-es';
export enum RobotBrand {
'先工' = 1,
}
export const ROBOT_BRAND_OPTIONS = <Array<[string, RobotBrand]>>(
Object.entries(RobotBrand).filter(([, v]) => isNumber(v))
);
export enum RobotType {
= 1,
AMR机器人,
,
}
export const ROBOT_TYPE_OPTIONS = <Array<[string, RobotType]>>Object.entries(RobotType).filter(([, v]) => isNumber(v));
export enum RobotState {
'任务执行中' = 1,

View File

@ -3,7 +3,7 @@ import type { RobotBrand, RobotState, RobotType } from './constant';
export interface RobotGroup {
id: string; // 机器人组id
label: string; // 机器人组名称
robots: RobotInfo[]; // 机器人列表
robots?: Array<string>; // 机器人列表
}
export interface RobotInfo {
@ -12,19 +12,20 @@ export interface RobotInfo {
label: string; // 机器人名称
brand: RobotBrand; // 机器人品牌
type: RobotType; // 机器人类型
}
export interface RobotDetail extends RobotInfo {
ip: string; // 机器人ip
isSimulative?: boolean; // 是否仿真机器人
battery?: number; // 机器人电量
minBattery?: number; // 最小电量
maxBattery?: number; // 最大电量
chargeBattery?: number; // 充电电量
swapBattery?: number; // 交换电量
isConnected?: boolean; // 机器人连接状态
state?: RobotState; // 机器人状态
canOrder?: boolean; // 接单状态
canStop?: boolean; // 急停状态
canControl?: boolean; // 控制状态
}
export interface RobotDetail extends RobotInfo {
isSimulative?: 0 | 1; // 是否仿真机器人
minBattery?: number; // 最小电量
maxBattery?: number; // 最大电量
chargeBattery?: number; // 充电电量
taskBattery?: number; // 任务电量
swapBattery?: number; // 交换电量
}

View File

@ -10,7 +10,12 @@ export async function getSceneById(id: SceneInfo['id']): Promise<SceneDetail | n
if (!id) return null;
type B = { id: string };
type D = SceneDetail;
const body = { id };
const data = await http.post<D, B>(API., body);
return data ?? null;
try {
const body = { id };
const data = await http.post<D, B>(API., body);
return data ?? null;
} catch (error) {
console.debug(error);
return null;
}
}

View File

@ -1,10 +1,15 @@
import type { RobotGroup } from '@api/robot';
import type { RobotGroup, RobotInfo } from '@api/robot';
import type { Meta2dData } from '@meta2d/core';
export interface SceneInfo {
id: string; // 场景id
label: string; // 场景名称
}
export interface SceneDetail extends SceneInfo {
robotGroups?: RobotGroup[]; // 机器人组列表
map?: string; // 地图JSON
json?: string; // 场景JSON
}
export interface SceneData extends Meta2dData {
robots?: Array<RobotInfo>;
robotGroups?: Array<RobotGroup>;
}

Binary file not shown.

Binary file not shown.

View File

@ -2,19 +2,19 @@
@font-face {
font-family: Roboto;
font-weight: 400;
src: url(./Roboto-Regular.otf) format('truetype');
src: url(./Roboto-Regular.ttf) format('truetype');
}
@font-face {
font-family: Roboto;
font-weight: 500;
src: url(./Roboto-Medium.otf) format('truetype');
src: url(./Roboto-Medium.ttf) format('truetype');
}
@font-face {
font-family: Roboto;
font-weight: 600;
src: url(./Roboto-Bold.ttf) format('truetype');
src: url(./Roboto-SemiBold.ttf) format('truetype');
}
// 思源黑体

View File

@ -1 +1 @@
$icons: (control, dropdown, edit, exit, trash_fill);
$icons: (control, dot, dropdown, edit, exit, pen, plus, register, trash_fill, trash);

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,29 +1,130 @@
$colors: (
// 品牌色
primary: #0dbb8a,
primary-hover: #2bb589,
primary-active: #0f8161,
primary_bg: #11221e,
primary_bg-hover: #10342b,
primary_border: #124637,
primary_border-hover: #115f49,
primary_text: #0ea278,
primary_text-hover: #2bb589,
primary_text-active: #0f8161,
// 成功色
success: #49aa19,
success-hover: #306317,
success-active: #3c8618,
success_bg: #162312,
success_bg-hover: #1d3712,
success_border: #274916,
success_border-hover: #306317,
success_text: #49aa19,
success_text-hover: #6abe39,
success_text-active: #3c8618,
// 警戒色
warning: #d89614,
111e1b: #111e1b,
warning-hover: #7c5914,
warning-active: #aa7714,
warning_bg: #2b2111,
warning_bg-hover: #443111,
warning_border: #594214,
warning_border-hover: #7c5914,
warning_text: #d89614,
warning_text-hover: #e8b339,
warning_text-active: #aa7714,
141414: #141414,
595959: #595959,
00000005: #00000005,
0000000a: #0000000a,
00000026: #00000026,
f9f9f914: #f9f9f914,
// 错误色
error: #dc4446,
error-hover: #e86e6b,
error-active: #ad393a,
error_bg: #2c1618,
error_bg-hover: #451d1f,
error_border: #5b2526,
error_border-hover: #7e2e2f,
error_text: #dc4446,
error_text-hover: #e86e6b,
error_text-active: #ad393a,
000000: #000,
2a2c2c: #2a2c2c,
242525: #242525,
36393a: #36393a,
// 信息色
info: #0ea278,
info-hover: #115441,
info-active: #107055,
info_bg: #111e1b,
info_bg-hover: #102e27,
info_border: #123f32,
info_border-hover: #115441,
info_text: #0f8d69,
info_text-hover: #2aa07b,
info_text-active: #107055,
424242: #424242,
434343: #434343,
// 中性色-文本
text1: #ffffffd9,
text1-inverse: #141414,
text2: #ffffffa6,
text3: #ffffff73,
text4: #ffffff40,
ffffff2e: #ffffff2e,
ffffff14: #ffffff14,
ffffff0a: #ffffff0a,
// 中性色-描边
border1: #424242,
border2: #303030,
ffffffd9: #ffffffd9,
ffffffa6: #ffffffa6,
ffffff73: #ffffff73,
ffffff40: #ffffff40,
// 中性色-填充
fill1: #ffffff2e,
fill2: #ffffff1f,
fill3: #ffffff14,
fill4: #ffffff0a,
// 中性色-背景
bg_component: #141414,
bg_modal: #1f1f1f,
bg_layout: #000,
bg_attention: #424242,
bg_mask: #00000073,
bg_title: #525f74,
// 中性色-阴影
shadow1: #212121,
shadow2: #141414,
shadow3: #0dbb8a,
// 图标
icon: #fff,
icon-inverse: #141414,
icon-hover: #434343,
icon-disabled: #595959,
icon-selected: #0dbb8a,
icon-brand: #0dbb8a,
// Neutral
neutral1: #141414,
neutral2: #212121,
neutral3: #1f1f1f,
neutral4: #262626,
neutral5: #434343,
neutral6: #595959,
neutral7: #8c8c8c,
neutral8: #bfbfbf,
neutral9: #f0f0f0,
neutral10: #f5f5f5,
neutral11: #fafafa,
neutral12: #fff,
neutral13: #fff,
// Conditional
divider: #303030,
header_bg: #0f1c29,
item_bg-hover: #f9f9f914,
mask: #000c,
menu_item_bg-selected: #177ddc,
menu_item_text-selected: #fff,
side_icon: #a0a9c0,
sider_bg: #242525,
sider_bg2: #36393a,
table_column: #ffffff05,
table_filter_trigger-hover: #0000000a,
page_bg: #2a2c2c,
popover: #232525,
popover_header_bg: #454647
);

View File

@ -1,3 +1,130 @@
$colors: (
text-1: #000,
// 品牌色
primary: #0dbb8a,
primary-hover: #2ec796,
primary-active: #039470,
primary_bg: #e1faef,
primary_bg-hover: #a8edd1,
primary_border: #7be0ba,
primary_border-hover: #53d4a6,
primary_text: #0dbb8a,
primary_text-hover: #2ec796,
primary_text-active: #039470,
// 成功色
success: #52c41a,
success-hover: #95de64,
success-active: #389e0d,
success_bg: #f6ffed,
success_bg-hover: #d9f7be,
success_border: #b7eb8f,
success_border-hover: #95de64,
success_text: #52c41a,
success_text-hover: #73d13d,
success_text-active: #389e0d,
// 警戒色
warning: #faad14,
warning-hover: #ffd666,
warning-active: #d48806,
warning_bg: #fffbe6,
warning_bg-hover: #fff1b8,
warning_border: #ffe58f,
warning_border-hover: #ffd666,
warning_text: #faad14,
warning_text-hover: #ffc53d,
warning_text-active: #d48806,
// 错误色
error: #ff4d4f,
error-hover: #ff7875,
error-active: #d9363e,
error_bg: #fff2f0,
error_bg-hover: #fff1f0,
error_border: #ffccc7,
error_border-hover: #ffa39e,
error_text: #ff4d4f,
error_text-hover: #ff7875,
error_text-active: #d9363e,
// 信息色
info: #0dbb8a,
info-hover: #53d4a6,
info-active: #039470,
info_bg: #e1faef,
info_bg-hover: #a8edd1,
info_border: #7be0ba,
info_border-hover: #53d4a6,
info_text: #0dbb8a,
info_text-hover: #2ec796,
info_text-active: #039470,
// 中性色-文本
text1: #000000e0,
text1-inverse: #fff,
text2: #000000a6,
text3: #00000073,
text4: #00000040,
// 中性色-描边
border1: #d9d9d9,
border2: #f0f0f0,
// 中性色-填充
fill1: #00000026,
fill2: #0000000f,
fill3: #0000000a,
fill4: #00000005,
// 中性色-背景
bg_component: #fff,
bg_modal: #fff,
bg_layout: #f5f5f5,
bg_attention: #000000d9,
bg_mask: #00000073,
bg_title: #c0d4da,
// 中性色-阴影
shadow1: #0000000a,
shadow2: #00000026,
shadow3: #0dbb8a,
// 图标
icon: #000,
icon-inverse: #fff,
icon-hover: #d9d9d9,
icon-disabled: #bfbfbf,
icon-selected: #0dbb8a,
icon-brand: #0dbb8a,
// Neutral
neutral1: #fff,
neutral2: #fafafa,
neutral3: #f5f5f5,
neutral4: #f0f0f0,
neutral5: #d9d9d9,
neutral6: #bfbfbf,
neutral7: #8c8c8c,
neutral8: #595959,
neutral9: #434343,
neutral10: #262626,
neutral11: #1f1f1f,
neutral12: #141414,
neutral13: #000,
// Conditional
divider: #0000000f,
header_bg: #fff,
item_bg-hover: #29386014,
mask: #0000004d,
menu_item_bg-selected: #e6f7ff,
menu_item_text-selected: #1890ff,
side_icon: #798582,
sider_bg: #fff,
sider_bg2: #fcfcfc,
table_column: #fafafa,
table_filter_trigger-hover: #0000000a,
page_bg: #f0f2f5,
popover: #fff,
popover_header_bg: #ebebeb
);

View File

@ -0,0 +1,84 @@
<script setup lang="ts">
import { getAllRobots, type RobotInfo, RobotType } from '@api/robot';
import type { EditorService } from '@core/editor.service';
import { message } from 'ant-design-vue';
import { isError, isNil } from 'lodash-es';
import { computed, inject, type InjectionKey, ref, type ShallowRef } from 'vue';
import { useI18n } from 'vue-i18n';
type Props = {
token: InjectionKey<ShallowRef<EditorService>>;
};
const props = defineProps<Props>();
const editor = inject(props.token)!;
export type RobotAddModalRef = Ref;
type Ref = {
open: (gid: string) => void;
};
const open: Ref['open'] = async (id) => {
const res = await getAllRobots();
robots.value = res.filter(({ id }) => isNil(editor.value.getRobotById(id)));
gid.value = id;
keyword.value = '';
selected.value = [];
show.value = true;
};
defineExpose<Ref>({ open });
const { t } = useI18n();
const gid = ref<RobotInfo['gid']>();
const show = ref<boolean>(false);
const keyword = ref<string>('');
const robots = ref<RobotInfo[]>([]);
const filtered = computed<RobotInfo[]>(() => robots.value.filter(({ label }) => label.includes(keyword.value)));
const selected = ref<RobotInfo['id'][]>([]);
const submit = () => {
try {
const temp = selected.value.map((v) => robots.value.find(({ id }) => id === v)).filter((v) => !isNil(v));
editor.value.addRobots(gid.value, temp);
show.value = false;
} catch (error) {
if (isError(error)) {
message.error(t(error?.message));
} else {
console.debug(error);
}
}
};
</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><SearchOutlined /></template>
</a-input>
<a-table
class="mt-10"
rowKey="id"
:dataSource="filtered"
: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

@ -0,0 +1,56 @@
<script setup lang="ts">
import type { EditorService } from '@core/editor.service';
import { type FormInstance, message } from 'ant-design-vue';
import { isError } from 'lodash-es';
import { inject, type InjectionKey, reactive, ref, type ShallowRef, shallowRef } from 'vue';
import { useI18n } from 'vue-i18n';
type Props = {
token: InjectionKey<ShallowRef<EditorService>>;
};
const props = defineProps<Props>();
const editor = inject(props.token)!;
export type RobotGroupRenameModalRef = Ref;
type Ref = {
open: (id: string, label: string) => void;
};
const open: Ref['open'] = (id, label) => {
form.value?.resetFields();
data.id = id;
data.label = label;
show.value = true;
};
defineExpose<Ref>({ open });
const { t } = useI18n();
const show = ref<boolean>(false);
const form = shallowRef<FormInstance>();
const data = reactive<{ id: string; label: string }>({ id: '', label: '' });
const submit = async () => {
try {
await form.value?.validate();
editor.value.updateRobotGroupLabel(data.id, data.label);
show.value = false;
} catch (error) {
if (isError(error)) {
message.error(t(error?.message));
} else {
console.debug(error);
}
}
};
</script>
<template>
<a-modal :width="460" :title="$t('修改组名称')" v-model:open="show" :mask-closable="false" centered @ok="submit">
<a-form ref="form" class="mr-14" :model="data">
<a-form-item :label="$t('机器人组名称')" name="label" :rules="[{ required: true, message: '' }]">
<a-input :placeholder="$t('请输入')" :maxlength="16" v-model:value="data.label" />
</a-form-item>
</a-form>
</a-modal>
</template>

View File

@ -0,0 +1,140 @@
<script setup lang="ts">
import { registerRobot, ROBOT_BRAND_OPTIONS, ROBOT_TYPE_OPTIONS, type RobotDetail, type RobotGroup } from '@api/robot';
import type { EditorService } from '@core/editor.service';
import { type FormInstance, message } from 'ant-design-vue';
import { isError, isNil } from 'lodash-es';
import { computed, inject, type InjectionKey, reactive, ref, type ShallowRef, shallowRef } from 'vue';
import { useI18n } from 'vue-i18n';
const IP_REGEX = /^((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}$/;
type Props = {
token: InjectionKey<ShallowRef<EditorService>>;
};
const props = defineProps<Props>();
const editor = inject(props.token)!;
export type RobotRegisterModalRef = Ref;
type Ref = {
open: (gid: string) => void;
};
const open: Ref['open'] = (id) => {
form.value?.resetFields();
data.gid = id;
show.value = true;
};
defineExpose<Ref>({ open });
const { t } = useI18n();
const show = ref<boolean>(false);
const groups = computed<RobotGroup[]>(() => editor.value.robotGroups.value ?? []);
const form = shallowRef<FormInstance>();
const data = reactive<Partial<RobotDetail>>({ isSimulative: 0 });
const submit = async () => {
try {
await form.value?.validate();
const robot = await registerRobot(<RobotDetail>data);
if (isNil(robot)) throw Error('机器人注册失败');
editor.value.addRobots(data.gid, [robot]);
show.value = false;
} catch (error) {
if (isError(error)) {
message.error(t(error?.message));
} else {
console.debug(error);
}
}
};
</script>
<template>
<a-modal :width="572" :title="$t('机器人注册')" v-model:open="show" :mask-closable="false" centered @ok="submit">
<a-form ref="form" class="mr-70" :model="data" :validate-messages="{ required: '' }">
<a-form-item :label="$t('机器人名称')" name="label" :rules="[{ required: true }]">
<a-input :placeholder="$t('请输入')" :maxlength="16" v-model:value="data.label" />
</a-form-item>
<a-form-item :label="$t('是否仿真机器人')" name="isSimulative" :rules="[{ required: true }]">
<a-select v-model:value="data.isSimulative">
<a-select-option :value="1">{{ $t('是') }}</a-select-option>
<a-select-option :value="0">{{ $t('否') }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('品牌')" name="brand" :rules="[{ required: true }]">
<a-select :placeholder="$t('请选择')" v-model:value="data.brand">
<a-select-option v-for="[l, v] in ROBOT_BRAND_OPTIONS" :key="v">{{ $t(l) }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('机器人类型')" name="type" :rules="[{ required: true }]">
<a-select :placeholder="$t('请选择')" v-model:value="data.type">
<a-select-option v-for="[l, v] in ROBOT_TYPE_OPTIONS" :key="v">{{ $t(l) }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item
:label="$t('输入属性IP')"
name="ip"
:rules="[{ required: true }, { pattern: IP_REGEX, message: '' }]"
>
<a-input :placeholder="$t('请输入')" :maxlength="16" v-model:value="data.ip" />
</a-form-item>
<a-form-item :label="$t('最小电量')" name="minBattery">
<a-input-number
:placeholder="$t('请输入')"
:min="0"
:max="100"
:precision="0"
v-model:value="data.minBattery"
:controls="false"
/>
</a-form-item>
<a-form-item :label="$t('充电电量')" name="chargeBattery">
<a-input-number
:placeholder="$t('请输入')"
:min="0"
:max="100"
:precision="0"
v-model:value="data.chargeBattery"
:controls="false"
/>
</a-form-item>
<a-form-item :label="$t('任务电量')" name="taskBattery">
<a-input-number
:placeholder="$t('请输入')"
:min="0"
:max="100"
:precision="0"
v-model:value="data.taskBattery"
:controls="false"
/>
</a-form-item>
<a-form-item :label="$t('可交换电量')" name="swapBattery">
<a-input-number
:placeholder="$t('请输入')"
:min="0"
:max="100"
:precision="0"
v-model:value="data.swapBattery"
:controls="false"
/>
</a-form-item>
<a-form-item :label="$t('最大电量')" name="maxBattery">
<a-input-number
:placeholder="$t('请输入')"
:min="0"
:max="100"
:precision="0"
v-model:value="data.maxBattery"
:controls="false"
/>
</a-form-item>
<a-form-item :label="$t('机器人组')" name="gid" :rules="[{ required: true }]">
<a-select :placeholder="$t('请选择')" v-model:value="data.gid" disabled>
<a-select-option v-for="{ id, label } in groups" :key="id">{{ label }}</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</template>

View File

@ -1,69 +0,0 @@
<script setup lang="ts">
import type { RobotGroup, RobotInfo } from '@api/robot';
import type { EditorService } from '@core/editor.service';
import { chain, groupBy, map } from 'lodash-es';
import { computed, inject, type InjectionKey, ref, type ShallowRef } from 'vue';
type Props = {
editor: InjectionKey<ShallowRef<EditorService>>;
editable?: boolean;
groups?: RobotGroup[];
};
const props = defineProps<Props>();
const editor = inject(props.editor)!;
const keyword = ref<string>('');
//#region
const robotList = computed<RobotInfo[]>(() =>
chain(props.groups)
.map('robots')
.flatten()
.filter(({ label }) => label.includes(keyword.value))
.value(),
);
const robotGroupMap = computed<Record<RobotGroup['id'], RobotInfo[]>>(() => groupBy(robotList.value, 'gid'));
const selected = ref<RobotInfo['id'][]>([]);
const isAllSelected = computed<boolean>(() => robotList.value.every(({ id }) => selected.value.includes(id)));
const selectAll = (checked: boolean) => {
selected.value = checked ? map(robotList.value, 'id') : [];
};
//#endregion
</script>
<template>
<a-flex class="full" vertical>
<a-input class="search mb-16" :placeholder="$t('请输入搜索关键字')" v-model:value="keyword">
<template #suffix><SearchOutlined /></template>
</a-input>
<a-flex v-if="editable" class="mb-8" style="height: 32px" flex="none" justify="space-between" align="center">
<a-checkbox :checked="isAllSelected" @change="selectAll($event.target.checked)">{{ $t('全选') }}</a-checkbox>
<a-space>
<a-button class="icon-btn" size="small" :disabled="!selected.length">
<i class="mask control bg-1" />
</a-button>
</a-space>
</a-flex>
<a-collapse class="scroll" expand-icon-position="end" ghost>
<template #expandIcon="v">
<i class="icon dropdown" :class="{ active: v?.isActive }" />
</template>
<a-collapse-panel v-for="{ id, label } in groups" :key="id" :header="label">
<a-list :data-source="robotGroupMap[id]">
<template #renderItem="{ item }">
<a-list-item class="ph-16" style="height: 36px">
<template #actions> </template>
<a-typography-text>{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
</a-collapse>
</a-flex>
</template>
<!-- <style scoped lang="scss"></style> -->

View File

@ -0,0 +1,154 @@
<script setup lang="ts">
import { type RobotGroup, type RobotInfo } 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 { computed, inject, type InjectionKey, ref, type ShallowRef, shallowRef } from 'vue';
import { useI18n } from 'vue-i18n';
type Props = {
token: InjectionKey<ShallowRef<EditorService>>;
editable?: boolean;
current?: string;
};
const props = defineProps<Props>();
const editor = inject(props.token)!;
type Events = {
(e: 'change', id: string): void;
};
const emit = defineEmits<Events>();
const { t } = useI18n();
//#region
//#endregion
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 selectAll = (checked: boolean) => {
selected.value = checked ? map(robots.value, 'id') : [];
};
//#endregion
//#region
const refAddRobot = shallowRef<RobotAddModalRef>();
const refRegisterRobot = shallowRef<RobotRegisterModalRef>();
const refRenameGroup = shallowRef<RobotGroupRenameModalRef>();
const toDeleteGroup = (id: RobotGroup['id']) =>
Modal.confirm({
class: 'confirm',
title: t('您确定要删除该机器人组吗?'),
centered: true,
cancelText: t('返回'),
okText: t('删除'),
onOk: () => editor.value.deleteRobotGroup(id),
});
//#endregion
</script>
<template>
<RobotAddModal ref="refAddRobot" :token="token" />
<RobotRegisterModal ref="refRegisterRobot" :token="token" />
<RobotGroupRenameModal ref="refRenameGroup" :token="token" />
<a-flex class="full" vertical>
<a-input class="search mb-16" :placeholder="$t('请输入搜索关键字')" v-model:value="keyword">
<template #suffix><SearchOutlined /></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">
<i class="mask control" />
</a-button>
<a-button class="icon-btn panel-btn" size="small" :disabled="!selected.length">
<i class="mask trash_fill" />
</a-button>
</a-space>
</a-flex>
<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 v-for="{ id, label, robots } in groups" :key="id" :header="label">
<template v-if="editable" #extra>
<a-dropdown placement="bottomRight">
<a-button class="icon-btn" size="small">
<i class="icon dot" />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item>
<a-space align="center" :size="4" @click="refAddRobot?.open(id)">
<i class="icon plus size-20" />
<span>{{ $t('添加机器人') }}</span>
</a-space>
</a-menu-item>
<a-menu-item>
<a-space align="center" :size="4" @click="refRegisterRobot?.open(id)">
<i class="icon register size-20" />
<span>{{ $t('注册机器人') }}</span>
</a-space>
</a-menu-item>
<a-menu-item @click="refRenameGroup?.open(id, label)">
<a-space align="center" :size="4">
<i class="icon pen size-20" />
<span>{{ $t('修改组名称') }}</span>
</a-space>
</a-menu-item>
<a-menu-item @click="toDeleteGroup(id)">
<a-space align="center" :size="4">
<i class="icon trash size-20" />
<span>{{ $t('删除组') }}</span>
</a-space>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<a-list :data-source="getGroupRobots(robots)">
<template #renderItem="{ item }">
<a-list-item
class="ph-16"
:class="{ selected: item.id === current }"
style="height: 36px"
@click="emit('change', item.id)"
>
<template #actions> </template>
<a-typography-text>{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
</a-collapse>
<a-button class="mt-8" v-if="editable" type="dashed" size="large" block @click="editor.createRobotGroup()">
<i class="mask plus size-20 primary" />
<span>{{ $t('添加机器人组') }}</span>
</a-button>
</a-flex>
</template>
<style scoped lang="scss">
.btn {
justify-self: flex-end;
}
</style>

View File

@ -1,5 +1,4 @@
<script setup lang="ts">
import type { RobotGroup } from '@api/robot';
import { getSceneById } from '@api/scene';
import { EditorService } from '@core/editor.service';
import { watch } from 'vue';
@ -13,19 +12,20 @@ type Props = {
};
const props = defineProps<Props>();
//#region
const readScene = async () => {
const res = await getSceneById(props.id);
title.value = res?.label ?? '';
editor.value?.load(res?.json, !editable.value);
};
//#endregion
const title = ref<string>('');
const groups = ref<RobotGroup[]>([]);
const map = ref<string>('');
onMounted(() => {
getSceneById(props.id).then((res) => {
title.value = res?.label ?? '';
groups.value = res?.robotGroups ?? [];
map.value = res?.map ?? '';
});
});
watch(map, (v) => {
editor.value?.load(v, !editable.value);
});
watch(
() => props.id,
() => readScene(),
{ immediate: true, flush: 'post' },
);
const container = shallowRef<HTMLDivElement>();
const editor = shallowRef<EditorService>();
@ -35,9 +35,9 @@ onMounted(() => {
});
const editable = ref<boolean>(false);
watch(editable, (v) => {
editor.value?.setState(v);
});
watch(editable, (v) => editor.value?.setState(v));
const current = ref<string>();
</script>
<template>
@ -65,7 +65,13 @@ watch(editable, (v) => {
<a-layout-sider :width="320">
<a-tabs class="full" type="card">
<a-tab-pane key="1" :tab="$t('机器人')">
<RobotGroup v-if="editor" :editor="EDITOR_KEY" :editable="editable" :groups="groups" />
<RobotGroups
v-if="editor"
:token="EDITOR_KEY"
:editable="editable"
:current="current"
@change="current = $event"
/>
</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>

View File

@ -1,9 +1,11 @@
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 } from '@meta2d/core';
import { CanvasLayer, EditType, LockState, Meta2d, s8 } from '@meta2d/core';
import { useObservable } from '@vueuse/rxjs';
import { cloneDeep, get, pick } from 'lodash-es';
import { debounceTime, filter, map, Subject, switchMap } from 'rxjs';
import { clone, cloneDeep, get, isNil, pick, remove, some } from 'lodash-es';
import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs';
import { watch } from 'vue';
export type Point = Record<'x' | 'y', number>;
@ -44,6 +46,9 @@ export class EditorService extends Meta2d {
),
);
public override data(): SceneData {
return super.data();
}
public override find(target: string): MapPen[] {
return super.find(target);
}
@ -52,15 +57,73 @@ export class EditorService extends Meta2d {
this.lock(readonly ? LockState.Disable : LockState.None);
}
//#region 机器人
readonly #robotMap = new Map<RobotInfo['id'], RobotInfo>();
public get robots(): RobotInfo[] {
return Array.from(this.#robotMap.values());
}
public getRobotById(id: RobotInfo['id']): RobotInfo | undefined {
return this.#robotMap.get(id);
}
public addRobots(gid: RobotInfo['gid'], robots: RobotInfo[]): void {
const groups = clone(this.#robotGroups$$.value);
const group = groups.find((v) => v.id === gid);
if (isNil(group)) throw Error('未找到目标机器人组');
group.robots ??= [];
robots.forEach((v) => {
if (this.#robotMap.has(v.id)) return;
this.#robotMap.set(v.id, { ...v, gid });
group.robots?.push(v.id);
});
this.#robotGroups$$.next(groups);
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
}
readonly #robotGroups$$ = new BehaviorSubject<RobotGroup[]>([]);
public readonly robotGroups = useObservable<RobotGroup[]>(this.#robotGroups$$.pipe(debounceTime(300)));
public createRobotGroup(): void {
const id = s8();
const label = `RG-${id}`;
const groups = clone(this.#robotGroups$$.value);
groups.push({ id, label });
this.#robotGroups$$.next(groups);
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
}
public deleteRobotGroup(id: RobotGroup['id']): void {
const groups = clone(this.#robotGroups$$.value);
const group = groups.find((v) => v.id === id);
group?.robots?.forEach((v) => this.#robotMap.delete(v));
remove(groups, group);
this.#robotGroups$$.next(groups);
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
}
public updateRobotGroupLabel(id: RobotGroup['id'], label: RobotGroup['label']): void {
const groups = this.#robotGroups$$.value;
const group = groups.find((v) => v.id === id);
if (isNil(group)) throw Error('未找到目标机器人组');
if (some(groups, ['label', label])) throw Error('机器人组名称已经存在');
group.label = label;
this.#robotGroups$$.next([...groups]);
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
}
//#endregion
//#region 点位
public async addPoint(p: Point, type = MapPointType.): Promise<void> {
const id = s8();
const pen: MapPen = {
...p,
...this.#mapPoint(type),
...this.#mapPointImage(type),
id,
name: 'point',
tags: ['point', `point-${type}`],
label: 'POINT',
label: `P-${id}`,
point: { type },
};
const { x, y, width, height } = this.getPenRect(pen);
@ -128,6 +191,11 @@ export class EditorService extends Meta2d {
this.setTheme(theme);
}
const { robots, robotGroups } = this.data();
this.#robotMap.clear();
robots?.forEach((r) => this.#robotMap.set(r.id, r));
this.#robotGroups$$.next(robotGroups ?? []);
this.find('point').forEach((pen) => {
if (!pen.point?.type) return;
if (pen.point.type < 10) return;

View File

@ -29,10 +29,11 @@ class ThemeService {
public get ant(): AntdTheme {
switch (this.#theme.value) {
case Theme.Dark:
return THEME_MAP[`antd-${this.#theme.value}`] ?? {};
return { algorithm: theme.darkAlgorithm };
case Theme.Light:
default:
return { algorithm: theme.defaultAlgorithm };
default:
return {};
}
}

View File

@ -21,14 +21,10 @@ body {
.full {
width: 100%;
height: 100%;
overflow: hidden;
overflow: visible;
}
.scroll {
overflow-y: auto;
}
@for $i from 0 through 20 {
@for $i from 0 through 50 {
.size-#{$i * 2} {
font-size: $i * 2px !important;
}