first commit

This commit is contained in:
chndfang 2025-04-20 00:49:14 +08:00
commit d5a1b4ed47
45 changed files with 7228 additions and 0 deletions

3
.env Normal file
View File

@ -0,0 +1,3 @@
ENV_APP_TITLE=运输控制系统
ENV_HTTP_BASE=/api
ENV_WEBSOCKET_BASE=/ws

1
.env.development Normal file
View File

@ -0,0 +1 @@
ENV_APP_TITLE=运输控制系统(开发)

0
.env.production Normal file
View File

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
components.d.ts
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

13
.prettierrc.js Normal file
View File

@ -0,0 +1,13 @@
export default {
arrowParens: 'always',
bracketSpacing: true,
singleAttributePerLine: false,
bracketSameLine: false,
jsxSingleQuote: true,
printWidth: 120,
semi: true,
singleQuote: true,
tabWidth: 2,
useTabs: false,
endOfLine: 'auto',
};

13
.stylelintrc.json Normal file
View File

@ -0,0 +1,13 @@
{
"plugins": ["stylelint-order"],
"extends": [
"stylelint-config-standard",
"stylelint-config-standard-scss",
"stylelint-config-recommended-vue",
"stylelint-config-recess-order"
],
"rules": {
"font-family-name-quotes": "always-where-required",
"function-url-quotes": "never"
}
}

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

50
eslint.config.js Normal file
View File

@ -0,0 +1,50 @@
import pluginJs from '@eslint/js';
import configPrettier from 'eslint-config-prettier';
import pluginSimpleImportSort from 'eslint-plugin-simple-import-sort';
import pluginVue from 'eslint-plugin-vue';
import globals from 'globals';
import tseslint from 'typescript-eslint';
/** @type {import('eslint').Linter.Config[]} */
export default [
{ files: ['**/*.{js,mjs,cjs,ts,vue}'] },
{
languageOptions: {
globals: {
...globals.browser,
NodeJS: true,
},
},
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs['flat/essential'],
configPrettier,
{
files: ['**/*.vue'],
languageOptions: { parserOptions: { parser: tseslint.parser } },
rules: {
'vue/multi-word-component-names': ['warn', { ignores: ['index'] }],
},
},
{
plugins: { 'simple-import-sort': pluginSimpleImportSort },
rules: {
'simple-import-sort/imports': 'warn',
'simple-import-sort/exports': 'warn',
},
},
{
rules: {
indent: [
'error',
2,
{ SwitchCase: 1, CallExpression: { arguments: 'off' }, ignoredNodes: ['ConditionalExpression *'] },
],
quotes: ['error', 'single'],
semi: ['error', 'always'],
'max-len': ['warn', { code: 120, ignoreComments: true, ignoreUrls: true, ignoreTemplateLiterals: true }],
'max-lines': ['warn', { max: 1000, skipBlankLines: true, skipComments: true }],
},
},
];

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>%ENV_APP_TITLE%</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

6436
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "arm_system",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"start": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@meta2d/core": "^1.0.78",
"ant-design-vue": "^4.2.6",
"axios": "^1.8.4",
"lodash-es": "^4.17.21",
"vue": "^3.5.13",
"vue-i18n": "^11.1.3",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.14.1",
"@vitejs/plugin-vue": "^5.2.2",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.25.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-vue": "^10.0.0",
"globals": "^16.0.0",
"prettier": "^3.5.3",
"sass-embedded": "^1.86.3",
"stylelint": "^16.18.0",
"stylelint-config-recess-order": "^6.0.0",
"stylelint-config-recommended-vue": "^1.6.0",
"stylelint-config-standard": "^38.0.0",
"stylelint-config-standard-scss": "^14.0.0",
"stylelint-order": "^7.0.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.30.1",
"unplugin-vue-components": "^28.5.0",
"vite": "^6.3.1",
"vue-tsc": "^2.2.8"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

BIN
public/point/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

12
src/App.vue Normal file
View File

@ -0,0 +1,12 @@
<script setup lang="ts">
import sLocale from '@core/locale.service';
import sTheme from '@core/theme.service';
</script>
<template>
<a-config-provider :locale="sLocale.ant" :theme="sTheme.ant" :autoInsertSpaceInButton="false">
<a-app>
<router-view />
</a-app>
</a-config-provider>
</template>

4
src/ant.scss Normal file
View File

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

0
src/apis/map/api.ts Normal file
View File

63
src/apis/map/constant.ts Normal file
View File

@ -0,0 +1,63 @@
import { KeydownType, type Options } from '@meta2d/core';
//#region 点位
export enum MapPointType {
= 1,
,
,
,
= 11,
,
,
,
,
= 99,
}
export const MAP_POINT_TYPES = Object.freeze(
<[string, MapPointType][]>Object.entries(MapPointType).filter(([, v]) => typeof v === 'number'),
);
//#endregion
//#region 线路
export enum MapRouteType {
线 = 'line',
线 = 'bezier3',
}
export const MAP_ROUTE_TYPES = Object.freeze(<[string, MapRouteType][]>Object.entries(MapRouteType));
//#endregion
//#region 区域
export enum MapAreaType {
= 1,
= 11,
,
}
export const MAP_AREA_TYPES = Object.freeze(
<[string, MapAreaType][]>Object.entries(MapAreaType).filter(([, v]) => typeof v === 'number'),
);
//#endregion
export const EDITOR_CONFIG: Options = {
keydown: KeydownType.None,
strictScope: true,
moveConnectedLine: false,
textRotate: false,
textFlip: false,
disableInput: true,
disableRotate: true,
disableSize: true,
disableAnchor: true,
disableEmptyLine: true,
disableRepeatLine: true,
minScale: 0.19,
maxScale: 2.01,
scaleOff: 0.01,
defaultAnchors: [],
fontSize: 14,
lineHeight: 1.5,
textAlign: 'center',
textBaseline: 'top',
};

3
src/apis/map/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './api';
export * from './constant';
export type * from './type';

37
src/apis/map/type.ts Normal file
View File

@ -0,0 +1,37 @@
import type { RobotInfo } from '@api/robot';
import type { Pen } from '@meta2d/core';
import type { MapAreaType, MapPointType, MapRouteType } from './constant';
export interface MapPen extends Pen {
desc?: string; // 描述
point?: MapPointInfo; // 点位信息
route?: MapRouteInfo; // 线路信息
area?: MapAreaInfo; // 区域信息
attrs?: Record<string, unknown>; // 额外属性
activeAttrs?: string[]; // 已激活的额外属性
}
//#region 点位
export interface MapPointInfo {
type: MapPointType; // 点位类型
robots?: RobotInfo['id'][]; // 绑定机器人id集合
}
//#endregion
//#region 线路
export interface MapRouteInfo {
type: MapRouteType; // 线路类型
direction?: -1 | 1; // 线路方向
}
//#endregion
//#region 区域
export interface MapAreaInfo {
type: MapAreaType; // 区域类型
points?: string[]; // 绑定点位id集合
routes?: string[]; // 绑定线路id集合
}
//#endregion

0
src/apis/robot/api.ts Normal file
View File

View File

@ -0,0 +1,3 @@
export enum RobotType {
= 1,
}

3
src/apis/robot/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './api';
export * from './constant';
export type * from './type';

8
src/apis/robot/type.ts Normal file
View File

@ -0,0 +1,8 @@
import type { RobotType } from './constant';
export interface RobotInfo {
id: number; // 机器人id
name: string; // 机器人名称
brand?: string; // 机器人品牌
type: RobotType; // 机器人类型
}

0
src/apis/scene/api.ts Normal file
View File

View File

3
src/apis/scene/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './api';
export * from './constant';
export type * from './type';

0
src/apis/scene/type.ts Normal file
View File

View File

@ -0,0 +1,3 @@
{
"查询": "123"
}

View File

View File

View File

@ -0,0 +1,12 @@
{
"light": {
"background": "",
"color": "#000000"
},
"dark": {
"background": "",
"color": "#8C8C8C",
"activeColor": "#FCC947",
"textColor": "#BFBFBF"
}
}

9
src/main.ts Normal file
View File

@ -0,0 +1,9 @@
import './style.scss';
import { i18n } from '@core/locale.service';
import { router } from '@core/router';
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).use(router).use(i18n).mount('#app');

29
src/pages/home.vue Normal file
View File

@ -0,0 +1,29 @@
<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,90 @@
import { EDITOR_CONFIG, type MapPen, MapPointType } from '@api/map';
import sTheme from '@core/theme.service';
import { LockState, Meta2d } from '@meta2d/core';
import THEME from 'asset/themes/editor.json';
import { watch } from 'vue';
export class EditorService {
readonly #editor: Meta2d;
public destroy() {
this.#editor.destroy();
}
public resize(): void {
this.#editor.resize();
this.#editor.render();
}
public open(map?: string, readonly = false): void {
const data = map ? JSON.parse(map) : undefined;
this.#editor.open(data);
this.#editor.lock(readonly ? LockState.Disable : LockState.None);
}
public save(): string {
const data = this.#editor.data();
const map = JSON.stringify(data);
return map;
}
public export(): string {
const png = this.#editor.toPng(10);
return png;
}
//#region 点位
public async addPoint(x: number = 0, y: number = 0, type = MapPointType.): Promise<void> {
const pen: MapPen = {
name: 'point',
x,
y,
width: 24,
height: 24,
point: { type },
};
await this.#editor.addPen(pen);
}
//#endregion
//#region 线路
//#endregion
//#region 区域
//#endregion
constructor(container: HTMLDivElement) {
this.#editor = new Meta2d(container, EDITOR_CONFIG);
(<HTMLDivElement>container.children.item(5)).ondrop = null;
this.#editor.on('*', (e, v) => this.#listen(e, v));
this.#register();
watch(
() => sTheme.theme,
(v) => this.#editor.setTheme(v),
{ immediate: true },
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
#listen(e: unknown, v: any) {
switch (e) {
default:
// console.log(e, v);
break;
}
}
#register() {
this.#editor.store.theme = THEME;
this.#editor.registerCanvasDraw({ point: drawPoint });
}
}
//#region 绘制函数
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.stroke();
}
//#endregion

40
src/services/http.ts Normal file
View File

@ -0,0 +1,40 @@
import { message } from 'ant-design-vue';
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios';
// 创建HTTP实例
const http: HttpInstance = axios.create({
baseURL: import.meta.env.ENV_HTTP_BASE,
adapter: 'fetch',
timeout: 30_000,
});
export default http;
// 添加请求拦截器
http.interceptors.request.use(
(config) => config,
(error) => Promise.reject(error.message),
);
// 添加响应拦截器
http.interceptors.response.use(
(response) => {
const res = <CommonRes>response.data;
if (res?.success) return <never>res.data;
const hint = res?.message ?? '未知异常';
message.error(hint);
return Promise.reject(hint);
},
(error) => Promise.reject(error.message),
);
type HttpInstance = Omit<AxiosInstance, 'get' | 'post'> & {
get: <R = void>(url: string, config?: AxiosRequestConfig) => Promise<R | undefined>;
post: <R = void, D = unknown>(url: string, data?: D, config?: AxiosRequestConfig<D>) => Promise<R | undefined>;
};
type CommonRes<T = void> = {
code: number;
success: boolean;
data: T | undefined;
message: string;
};

View File

@ -0,0 +1,73 @@
import 'dayjs/locale/zh-cn';
import 'dayjs/locale/en';
import type { Locale as AntdLocale } from 'ant-design-vue/es/locale';
import enUS from 'ant-design-vue/es/locale/en_US';
import zhCN from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
import { chain } from 'lodash-es';
import { ref, watch } from 'vue';
import { createI18n } from 'vue-i18n';
const LOCALE_FILES = import.meta.glob('asset/locales/*.json', { eager: true, import: 'default' });
const LOCAL_MAP = chain(LOCALE_FILES)
.mapKeys((_, k) => k.match(/^.*[\\|\\/](.+?)\.[^\\.]+$/)?.[1])
.mapValues((v) => <Record<string, string>>v)
.value();
enum Locale {
= 'zh-CN',
English = 'en-US',
}
export const LOCALES = Object.freeze<[string, Locale][]>(Object.entries(Locale));
export const i18n = createI18n({
legacy: true,
silentTranslationWarn: true,
locale: Locale.简体中文,
messages: chain(Locale)
.invert()
.mapValues((_, k) => LOCAL_MAP[k] ?? {})
.value(),
});
const LOCALE_STORAGE_KEY = 'locale';
class LocaleService {
#locale = ref<Locale>(<Locale>localStorage.getItem(LOCALE_STORAGE_KEY) || Locale.);
public get locale(): Locale {
return this.#locale.value;
}
public set locale(v: Locale) {
this.#locale.value = v;
localStorage.setItem(LOCALE_STORAGE_KEY, v);
}
public get ant(): AntdLocale {
switch (this.#locale.value) {
case Locale.English:
return enUS;
case Locale.:
default:
return zhCN;
}
}
constructor() {
watch(this.#locale, (v) => this.#load(v), { immediate: true });
}
#load(locale: Locale): void {
i18n.global.locale = locale;
switch (locale) {
case Locale.English:
dayjs.locale('en');
break;
case Locale.:
default:
dayjs.locale('zh-cn');
break;
}
}
}
export default new LocaleService();

54
src/services/router.ts Normal file
View File

@ -0,0 +1,54 @@
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') },
// ],
// },
]);
export const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: ROUTES,
});

View File

@ -0,0 +1,40 @@
import { theme, type TokenType as AntdTheme } from 'ant-design-vue';
import { ref, watch } from 'vue';
enum Theme {
Light = 'light',
Dark = 'dark',
}
export const THEMES = Object.freeze<[string, Theme][]>(Object.entries(Theme));
const THEME_STORAGE_KEY = 'locale';
class ThemeService {
#theme = ref<Theme>(<Theme>localStorage.getItem(THEME_STORAGE_KEY) || Theme.Dark);
public get theme(): Theme {
return this.#theme.value;
}
public set theme(v: Theme) {
this.#theme.value = v;
localStorage.setItem(THEME_STORAGE_KEY, v);
}
public get ant(): AntdTheme {
switch (this.#theme.value) {
case Theme.Dark:
return { algorithm: theme.darkAlgorithm };
case Theme.Light:
default:
return { algorithm: theme.defaultAlgorithm };
}
}
constructor() {
watch(this.#theme, (v) => this.#load(v), { immediate: true });
}
#load(theme: Theme): void {
document.documentElement.setAttribute('theme', theme);
}
}
export default new ThemeService();

19
src/style.scss Normal file
View File

@ -0,0 +1,19 @@
@use './ant';
html,
body {
width: 100vw;
height: 100vh;
margin: 0;
}
#app {
width: 100%;
height: 100%;
}
.full {
width: 100%;
height: 100%;
overflow: hidden;
}

7
src/vite-env.d.ts vendored Normal file
View File

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

24
tsconfig.app.json Normal file
View File

@ -0,0 +1,24 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": "./",
"paths": {
"asset/*": ["src/assets/*"],
"@/*": ["src/pages/*"],
"@api/*": ["src/apis/*"],
"@common/*": ["src/components/*"],
"@core/*": ["src/services/*"],
"@layout/*": ["src/layouts/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

24
tsconfig.node.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

54
vite.config.ts Normal file
View File

@ -0,0 +1,54 @@
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
import Components from 'unplugin-vue-components/vite';
import { defineConfig } from 'vite';
export default ({ mode }: Record<string, unknown>) =>
defineConfig({
plugins: [vue(), Components({ resolvers: [AntDesignVueResolver({ importStyle: false, resolveIcons: true })] })],
base: '/',
envPrefix: 'ENV_',
resolve: {
alias: {
asset: resolve(__dirname, 'src/assets/'),
'@': resolve(__dirname, 'src/pages/'),
'@api': resolve(__dirname, 'src/apis/'),
'@common': resolve(__dirname, 'src/components/'),
'@core': resolve(__dirname, 'src/services/'),
'@layout': resolve(__dirname, 'src/layouts/'),
},
},
css: {
preprocessorOptions: {
scss: { api: 'modern-compiler' },
},
},
build: {
target: 'es2020',
outDir: 'dist',
sourcemap: false,
minify: 'esbuild',
chunkSizeWarningLimit: 2000,
},
esbuild: {
drop: mode === 'production' ? ['console'] : [],
},
server: {
port: 8888,
host: true,
proxy: {
// '/api/': {
// target: 'http://82.157.33.186:26981/jeecg-boot',
// rewrite: (path) => path.replace(/^\/api/, ''),
// changeOrigin: true,
// },
// '/ws/': {
// target: 'ws://82.157.33.186:26981/jeecg-boot',
// rewrite: (path) => path.replace(/^\/ws/, ''),
// changeOrigin: true,
// ws: true,
// },
},
},
});