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",
"dependencies": {
"@meta2d/core": "^1.0.78",
"@vueuse/rxjs": "^13.1.0",
"ant-design-vue": "^4.2.6",
"axios": "^1.8.4",
"lodash-es": "^4.17.21",
"rxjs": "^7.8.2",
"vue": "^3.5.13",
"vue-i18n": "^11.1.3",
"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": {
"version": "8.14.1",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.14.1.tgz",
@ -4706,7 +4736,6 @@
"version": "7.8.2",
"resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
@ -5942,7 +5971,6 @@
"version": "2.8.1",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/type-check": {

View File

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

View File

@ -5,8 +5,6 @@ import sTheme from '@core/theme.service';
<template>
<a-config-provider :locale="sLocale.ant" :theme="sTheme.ant" :autoInsertSpaceInButton="false">
<a-app>
<router-view />
</a-app>
<router-view />
</a-config-provider>
</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 { 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 { cloneDeep, pick } from 'lodash-es';
import { debounceTime, filter, map, Subject, switchMap } from 'rxjs';
import { watch } from 'vue';
export class EditorService {
readonly #editor: Meta2d;
export type Point = Record<'x' | 'y', number>;
public destroy() {
this.#editor.destroy();
}
public resize(): void {
this.#editor.resize();
this.#editor.render();
}
public open(map?: string, readonly = false): void {
export class EditorService extends Meta2d {
public load(map?: string, readonly = false): void {
const data = map ? JSON.parse(map) : undefined;
this.#editor.open(data);
this.#editor.lock(readonly ? LockState.Disable : LockState.None);
this.open(data);
this.lock(readonly ? LockState.Disable : LockState.None);
}
public save(): string {
const data = this.#editor.data();
const data = this.data();
const map = JSON.stringify(data);
return map;
}
public export(): string {
const png = this.#editor.toPng(10);
const png = this.toPng(10);
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 点位
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 = {
name: 'point',
x,
y,
x: p.x,
y: p.y,
width: 24,
height: 24,
point: { type },
};
await this.#editor.addPen(pen);
await this.addPen(pen, false, true, true);
this.pushHistory({ type: EditType.Add, pens: [cloneDeep(pen)] });
}
//#endregion
@ -49,18 +64,36 @@ export class EditorService {
//#endregion
//#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
constructor(container: HTMLDivElement) {
this.#editor = new Meta2d(container, EDITOR_CONFIG);
super(container, EDITOR_CONFIG);
(<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();
watch(
() => sTheme.theme,
(v) => this.#editor.setTheme(v),
(v) => this.setTheme(v),
{ immediate: true },
);
}
@ -68,6 +101,12 @@ export class EditorService {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
#listen(e: unknown, v: any) {
switch (e) {
case 'click':
case 'mousedown':
case 'mouseup':
this.#mouse$$.next({ type: e, value: pick(v, 'x', 'y') });
break;
default:
// console.log(e, v);
break;
@ -75,8 +114,9 @@ export class EditorService {
}
#register() {
this.#editor.store.theme = THEME;
this.#editor.registerCanvasDraw({ point: drawPoint });
this.store.theme = THEME;
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 {
const { x = 0, y = 0, width = 0, height = 0 } = pen.calculative?.worldRect ?? {};
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.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

View File

@ -1,51 +1,17 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
export const ROUTES = Object.freeze<RouteRecordRaw[]>([
{ path: '/:pathMatch(.*)*', redirect: '/home' },
{ path: '/home', component: () => import('@/home.vue') },
// {
// path: '/',
// component: () => import('@layout/main.vue'),
// children: [
// { path: '', redirect: 'home' },
// { name: '首页', path: 'home', component: () => import('@/home.vue') },
// { name: '任务管理', path: 'task-management', component: () => import('@/home.vue') },
// { name: '车辆管理', path: 'vehicle-management', component: () => import('@/home.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') },
// ],
// },
{ path: '/:pathMatch(.*)*', redirect: '/' },
{
path: '/scene-editor/:id',
props: true,
component: () => import('@/scene-editor.vue'),
},
{
path: '/group-editor/:id',
props: true,
component: () => import('@/group-editor.vue'),
},
]);
export const router = createRouter({

View File

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

View File

@ -5,6 +5,8 @@ body {
width: 100vw;
height: 100vh;
margin: 0;
user-select: none;
-webkit-user-drag: none;
}
#app {
@ -17,3 +19,57 @@ body {
height: 100%;
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" />
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/*"],
"@api/*": ["src/apis/*"],
"@common/*": ["src/components/*"],
"@core/*": ["src/services/*"],
"@layout/*": ["src/layouts/*"]
"@core/*": ["src/services/*"]
}
},
"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>) =>
defineConfig({
plugins: [vue(), Components({ resolvers: [AntDesignVueResolver({ importStyle: false, resolveIcons: true })] })],
plugins: [
vue(),
Components({
dts: true,
resolvers: [AntDesignVueResolver({ importStyle: false, resolveIcons: true })],
}),
],
base: '/',
envPrefix: 'ENV_',
resolve: {
@ -16,7 +22,6 @@ export default ({ mode }: Record<string, unknown>) =>
'@api': resolve(__dirname, 'src/apis/'),
'@common': resolve(__dirname, 'src/components/'),
'@core': resolve(__dirname, 'src/services/'),
'@layout': resolve(__dirname, 'src/layouts/'),
},
},
css: {