This commit is contained in:
chndfang 2025-04-27 00:05:18 +08:00
parent d5a1b4ed47
commit 87db6f358c
15 changed files with 299 additions and 123 deletions

32
package-lock.json generated
View File

@ -9,9 +9,11 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@meta2d/core": "^1.0.78", "@meta2d/core": "^1.0.78",
"@vueuse/rxjs": "^13.1.0",
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
"axios": "^1.8.4", "axios": "^1.8.4",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"rxjs": "^7.8.2",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^11.1.3", "vue-i18n": "^11.1.3",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
@ -1769,6 +1771,34 @@
} }
} }
}, },
"node_modules/@vueuse/rxjs": {
"version": "13.1.0",
"resolved": "https://registry.npmmirror.com/@vueuse/rxjs/-/rxjs-13.1.0.tgz",
"integrity": "sha512-/ItzpO8yky6K+frthSlUbBvmy4WSrKSHze/SyYPMgGGLh+xMYFUvmXQnO/daVC4fdWJN8Phb2dfa7d9WZTDQhQ==",
"license": "MIT",
"dependencies": {
"@vueuse/shared": "13.1.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"rxjs": ">=6.0.0",
"vue": "^3.5.0"
}
},
"node_modules/@vueuse/shared": {
"version": "13.1.0",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-13.1.0.tgz",
"integrity": "sha512-IVS/qRRjhPTZ6C2/AM3jieqXACGwFZwWTdw5sNTSKk2m/ZpkuuN+ri+WCVUP8TqaKwJYt/KuMwmXspMAw8E6ew==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.14.1", "version": "8.14.1",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.14.1.tgz", "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.14.1.tgz",
@ -4706,7 +4736,6 @@
"version": "7.8.2", "version": "7.8.2",
"resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz", "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
@ -5942,7 +5971,6 @@
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/type-check": { "node_modules/type-check": {

View File

@ -10,9 +10,11 @@
}, },
"dependencies": { "dependencies": {
"@meta2d/core": "^1.0.78", "@meta2d/core": "^1.0.78",
"@vueuse/rxjs": "^13.1.0",
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
"axios": "^1.8.4", "axios": "^1.8.4",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"rxjs": "^7.8.2",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^11.1.3", "vue-i18n": "^11.1.3",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"

View File

@ -5,8 +5,6 @@ import sTheme from '@core/theme.service';
<template> <template>
<a-config-provider :locale="sLocale.ant" :theme="sTheme.ant" :autoInsertSpaceInButton="false"> <a-config-provider :locale="sLocale.ant" :theme="sTheme.ant" :autoInsertSpaceInButton="false">
<a-app> <router-view />
<router-view />
</a-app>
</a-config-provider> </a-config-provider>
</template> </template>

View File

@ -1,4 +0,0 @@
.ant-app {
width: 100%;
height: 100%;
}

View File

@ -0,0 +1,47 @@
<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>>;
};
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);
});
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 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-space>
</template>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,10 @@
<script setup lang="ts">
type Props = {
id: string;
};
const props = defineProps<Props>();
</script>
<template>组编辑器</template>
<!-- <style scoped lang="scss"></style> -->

View File

@ -1,29 +0,0 @@
<script setup lang="ts">
import { EditorService } from '@core/editor.service';
import { onMounted, shallowRef } from 'vue';
const elContainer = shallowRef<HTMLDivElement>();
const editor = shallowRef<EditorService>();
onMounted(() => {
editor.value = new EditorService(elContainer.value!);
});
</script>
<template>
<div ref="elContainer" class="full"></div>
<div class="test">
<a-button @click="editor?.addPoint(50, 50)"></a-button>
</div>
</template>
<style scoped lang="scss">
.test {
position: fixed;
top: 20px;
left: 20px;
z-index: 2333;
display: flex;
flex-direction: column;
}
</style>

View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import { EditorService } from '@core/editor.service';
import { onMounted, provide, shallowRef } from 'vue';
const EDITOR_KEY = Symbol('editor-key');
type Props = {
id: string;
};
const props = defineProps<Props>();
const container = shallowRef<HTMLDivElement>();
const editor = shallowRef<EditorService>();
provide(EDITOR_KEY, editor);
onMounted(() => {
editor.value = new EditorService(container.value!);
});
</script>
<template>
<a-layout class="full">
<a-layout-header>header</a-layout-header>
<a-layout>
<a-layout-sider>
<RobotList v-if="editor" :editor="EDITOR_KEY" />
</a-layout-sider>
<a-layout-content>
<div ref="container" class="full"></div>
</a-layout-content>
<a-layout-sider>sider</a-layout-sider>
</a-layout>
</a-layout>
</template>
<!-- <style scoped lang="scss"></style> -->

View File

@ -1,47 +1,62 @@
import { EDITOR_CONFIG, type MapPen, MapPointType } from '@api/map'; import { EDITOR_CONFIG, MapAreaType, type MapPen, MapPointType } from '@api/map';
import sTheme from '@core/theme.service'; import sTheme from '@core/theme.service';
import { LockState, Meta2d } from '@meta2d/core'; import { EditType, LockState, Meta2d } from '@meta2d/core';
import { useObservable } from '@vueuse/rxjs';
import THEME from 'asset/themes/editor.json'; import THEME from 'asset/themes/editor.json';
import { cloneDeep, pick } from 'lodash-es';
import { debounceTime, filter, map, Subject, switchMap } from 'rxjs';
import { watch } from 'vue'; import { watch } from 'vue';
export class EditorService { export type Point = Record<'x' | 'y', number>;
readonly #editor: Meta2d;
public destroy() { export class EditorService extends Meta2d {
this.#editor.destroy(); public load(map?: string, readonly = false): void {
}
public resize(): void {
this.#editor.resize();
this.#editor.render();
}
public open(map?: string, readonly = false): void {
const data = map ? JSON.parse(map) : undefined; const data = map ? JSON.parse(map) : undefined;
this.#editor.open(data); this.open(data);
this.#editor.lock(readonly ? LockState.Disable : LockState.None); this.lock(readonly ? LockState.Disable : LockState.None);
} }
public save(): string { public save(): string {
const data = this.#editor.data(); const data = this.data();
const map = JSON.stringify(data); const map = JSON.stringify(data);
return map; return map;
} }
public export(): string { public export(): string {
const png = this.#editor.toPng(10); const png = this.toPng(10);
return png; return png;
} }
readonly #mouse$$ = new Subject<{ type: 'click' | 'mousedown' | 'mouseup'; value: Point }>();
public readonly mouseClick = useObservable<Point>(
this.#mouse$$.pipe(
filter(({ type }) => type === 'click'),
debounceTime(100),
map(({ value }) => value),
),
);
public readonly mouseBrush = useObservable<[Point, Point]>(
this.#mouse$$.pipe(
filter(({ type }) => type === 'mousedown'),
switchMap(({ value: s }) =>
this.#mouse$$.pipe(
filter(({ type }) => type === 'mouseup'),
map(({ value: e }) => <[Point, Point]>[s, e]),
),
),
),
);
//#region 点位 //#region 点位
public async addPoint(x: number = 0, y: number = 0, type = MapPointType.): Promise<void> { public async addPoint(p: Point, type = MapPointType.): Promise<void> {
const pen: MapPen = { const pen: MapPen = {
name: 'point', name: 'point',
x, x: p.x,
y, y: p.y,
width: 24, width: 24,
height: 24, height: 24,
point: { type }, point: { type },
}; };
await this.#editor.addPen(pen); await this.addPen(pen, false, true, true);
this.pushHistory({ type: EditType.Add, pens: [cloneDeep(pen)] });
} }
//#endregion //#endregion
@ -49,18 +64,36 @@ export class EditorService {
//#endregion //#endregion
//#region 区域 //#region 区域
public async addArea(p1: Point, p2: Point, type = MapAreaType.) {
const scale = this.data().scale ?? 1;
const w = Math.abs(p1.x - p2.x);
const h = Math.abs(p1.y - p2.y);
if (w * scale < 50 || h * scale < 60) return;
const pen: MapPen = {
name: 'area',
x: Math.min(p1.x, p2.x),
y: Math.min(p1.y, p2.y),
width: w,
height: h,
area: { type },
locked: LockState.DisableMoveScale,
};
const area = await this.addPen(pen, false, true, true);
this.bottom(area);
this.pushHistory({ type: EditType.Add, pens: [cloneDeep(pen)] });
}
//#endregion //#endregion
constructor(container: HTMLDivElement) { constructor(container: HTMLDivElement) {
this.#editor = new Meta2d(container, EDITOR_CONFIG); super(container, EDITOR_CONFIG);
(<HTMLDivElement>container.children.item(5)).ondrop = null; (<HTMLDivElement>container.children.item(5)).ondrop = null;
this.#editor.on('*', (e, v) => this.#listen(e, v)); this.on('*', (e, v) => this.#listen(e, v));
this.#register(); this.#register();
watch( watch(
() => sTheme.theme, () => sTheme.theme,
(v) => this.#editor.setTheme(v), (v) => this.setTheme(v),
{ immediate: true }, { immediate: true },
); );
} }
@ -68,6 +101,12 @@ export class EditorService {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
#listen(e: unknown, v: any) { #listen(e: unknown, v: any) {
switch (e) { switch (e) {
case 'click':
case 'mousedown':
case 'mouseup':
this.#mouse$$.next({ type: e, value: pick(v, 'x', 'y') });
break;
default: default:
// console.log(e, v); // console.log(e, v);
break; break;
@ -75,8 +114,9 @@ export class EditorService {
} }
#register() { #register() {
this.#editor.store.theme = THEME; this.store.theme = THEME;
this.#editor.registerCanvasDraw({ point: drawPoint }); this.registerCanvasDraw({ point: drawPoint, line: drawLine, area: drawArea });
this.registerAnchors({ point: anchorPoint });
} }
} }
@ -84,7 +124,34 @@ export class EditorService {
function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void { function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
const { x = 0, y = 0, width = 0, height = 0 } = pen.calculative?.worldRect ?? {}; const { x = 0, y = 0, width = 0, height = 0 } = pen.calculative?.worldRect ?? {};
const { type } = pen.point ?? {}; const { type } = pen.point ?? {};
ctx.rect(x, y, width, height); ctx.save();
ctx.lineWidth = 2;
ctx.roundRect(x, y, width, height, 4);
ctx.stroke(); ctx.stroke();
ctx.fillStyle = '#fff';
ctx.fillText(String(type), x + width / 2, y + height / 2);
ctx.restore();
}
function anchorPoint(pen: MapPen): void {
pen.anchors = [{ x: 0.5, y: 0.5 }];
}
function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
const [p1, p2] = pen.calculative?.worldAnchors ?? [];
const { direction } = pen.route ?? {};
ctx.save();
ctx.lineWidth = 2;
ctx.restore();
}
function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void {
const { x = 0, y = 0, width = 0, height = 0 } = pen.calculative?.worldRect ?? {};
const { type } = pen.area ?? {};
ctx.save();
ctx.lineWidth = 1;
ctx.strokeRect(x, y, width, height);
ctx.fillStyle = '#fff';
ctx.fillText(String(type), x + width / 2, y);
ctx.restore();
} }
//#endregion //#endregion

View File

@ -1,51 +1,17 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'; import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
export const ROUTES = Object.freeze<RouteRecordRaw[]>([ export const ROUTES = Object.freeze<RouteRecordRaw[]>([
{ path: '/:pathMatch(.*)*', redirect: '/home' }, { path: '/:pathMatch(.*)*', redirect: '/' },
{ path: '/home', component: () => import('@/home.vue') }, {
// { path: '/scene-editor/:id',
// path: '/', props: true,
// component: () => import('@layout/main.vue'), component: () => import('@/scene-editor.vue'),
// children: [ },
// { path: '', redirect: 'home' }, {
// { name: '首页', path: 'home', component: () => import('@/home.vue') }, path: '/group-editor/:id',
// { name: '任务管理', path: 'task-management', component: () => import('@/home.vue') }, props: true,
// { name: '车辆管理', path: 'vehicle-management', component: () => import('@/home.vue') }, component: () => import('@/group-editor.vue'),
// { },
// name: '地图管理',
// path: 'map-management',
// meta: { standalone: true },
// component: () => import('@/map-management/scene-edit.vue'),
// },
// { name: '参数设置', path: 'parameter-setting', component: import('@/parameter-setting/index.vue') },
// {
// name: '调度仿真',
// path: 'scheduling-emulation',
// children: [
// {
// name: '调度模拟',
// path: 'scheduling-simulation',
// components: {
// default: () => import('@/scheduling-emulation/scheduling-simulation.vue'),
// toolbar: () => import('@common/map-switcher.vue'),
// },
// },
// {
// name: '车辆调试',
// path: 'vehicle-commissioning',
// component: () => import('@/scheduling-emulation/vehicle-commissioning.vue'),
// },
// {
// name: '场景测试',
// path: 'scenario-testing',
// component: () => import('@/scheduling-emulation/scenario-testing.vue'),
// },
// ],
// },
// { name: '告警管理', path: 'alarm-management', component: () => import('@/home.vue') },
// { name: '日志管理', path: 'log-management', component: () => import('@/home.vue') },
// ],
// },
]); ]);
export const router = createRouter({ export const router = createRouter({

View File

@ -7,7 +7,7 @@ enum Theme {
} }
export const THEMES = Object.freeze<[string, Theme][]>(Object.entries(Theme)); export const THEMES = Object.freeze<[string, Theme][]>(Object.entries(Theme));
const THEME_STORAGE_KEY = 'locale'; const THEME_STORAGE_KEY = 'theme';
class ThemeService { class ThemeService {
#theme = ref<Theme>(<Theme>localStorage.getItem(THEME_STORAGE_KEY) || Theme.Dark); #theme = ref<Theme>(<Theme>localStorage.getItem(THEME_STORAGE_KEY) || Theme.Dark);

View File

@ -5,6 +5,8 @@ body {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
margin: 0; margin: 0;
user-select: none;
-webkit-user-drag: none;
} }
#app { #app {
@ -17,3 +19,57 @@ body {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
@for $i from 0 through 100 {
.size-#{$i * 2} {
font-size: $i * 2px !important;
}
.w-#{$i * 4} {
width: $i * 4px !important;
}
.p-#{$i * 2} {
padding: $i * 2px !important;
}
.ph-#{$i * 2} {
padding-inline: $i * 2px !important;
}
.pv-#{$i * 2} {
padding-block: $i * 2px !important;
}
.pl-#{$i * 2} {
padding-left: $i * 2px !important;
}
.pr-#{$i * 2} {
padding-right: $i * 2px !important;
}
.pt-#{$i * 2} {
padding-top: $i * 2px !important;
}
.pb-#{$i * 2} {
padding-bottom: $i * 2px !important;
}
.m-#{$i * 2} {
margin: $i * 2px !important;
}
.mh-#{$i * 2} {
margin-inline: $i * 2px !important;
}
.mv-#{$i * 2} {
margin-block: $i * 2px !important;
}
.ml-#{$i * 2} {
margin-left: $i * 2px !important;
}
.mr-#{$i * 2} {
margin-right: $i * 2px !important;
}
.mt-#{$i * 2} {
margin-top: $i * 2px !important;
}
.mb-#{$i * 2} {
margin-bottom: $i * 2px !important;
}
}

6
src/vite-env.d.ts vendored
View File

@ -1,7 +1 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const vueComponent: DefineComponent<object, object, unknown>;
export default vueComponent;
}

View File

@ -16,9 +16,9 @@
"@/*": ["src/pages/*"], "@/*": ["src/pages/*"],
"@api/*": ["src/apis/*"], "@api/*": ["src/apis/*"],
"@common/*": ["src/components/*"], "@common/*": ["src/components/*"],
"@core/*": ["src/services/*"], "@core/*": ["src/services/*"]
"@layout/*": ["src/layouts/*"]
} }
}, },
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"files": ["components.d.ts"]
} }

View File

@ -6,7 +6,13 @@ import { defineConfig } from 'vite';
export default ({ mode }: Record<string, unknown>) => export default ({ mode }: Record<string, unknown>) =>
defineConfig({ defineConfig({
plugins: [vue(), Components({ resolvers: [AntDesignVueResolver({ importStyle: false, resolveIcons: true })] })], plugins: [
vue(),
Components({
dts: true,
resolvers: [AntDesignVueResolver({ importStyle: false, resolveIcons: true })],
}),
],
base: '/', base: '/',
envPrefix: 'ENV_', envPrefix: 'ENV_',
resolve: { resolve: {
@ -16,7 +22,6 @@ export default ({ mode }: Record<string, unknown>) =>
'@api': resolve(__dirname, 'src/apis/'), '@api': resolve(__dirname, 'src/apis/'),
'@common': resolve(__dirname, 'src/components/'), '@common': resolve(__dirname, 'src/components/'),
'@core': resolve(__dirname, 'src/services/'), '@core': resolve(__dirname, 'src/services/'),
'@layout': resolve(__dirname, 'src/layouts/'),
}, },
}, },
css: { css: {