├── src ├── components │ ├── mt-dzr │ │ ├── composables │ │ │ └── mouse.ts │ │ ├── index.ts │ │ ├── utils │ │ │ └── types.ts │ │ ├── components │ │ │ ├── render-item.vue │ │ │ ├── rotate-handle.vue │ │ │ └── resize-handle.vue │ │ ├── __tests__ │ │ │ └── mt-dzr.spec.ts │ │ ├── store │ │ │ ├── types.ts │ │ │ └── index.ts │ │ └── types.ts │ ├── mt-edit │ │ ├── index.ts │ │ ├── components │ │ │ ├── selected-area │ │ │ │ ├── types.ts │ │ │ │ └── index.vue │ │ │ ├── render-core │ │ │ │ └── types.ts │ │ │ ├── layout │ │ │ │ ├── footer-panel │ │ │ │ │ └── index.vue │ │ │ │ └── right-aside │ │ │ │ │ ├── select-item-event-setting │ │ │ │ │ └── input-target-value.vue │ │ │ │ │ ├── json-edit.vue │ │ │ │ │ ├── index.vue │ │ │ │ │ ├── select-item-props-setting.vue │ │ │ │ │ └── select-item-animate-setting │ │ │ │ │ ├── index.vue │ │ │ │ │ └── common-animate.vue │ │ │ ├── types.ts │ │ │ ├── svg-analysis │ │ │ │ └── index.vue │ │ │ ├── custom-svg-render │ │ │ │ └── index.vue │ │ │ ├── svg-render │ │ │ │ └── index.vue │ │ │ ├── export-json │ │ │ │ └── index.vue │ │ │ ├── import-json │ │ │ │ └── index.vue │ │ │ ├── group-render │ │ │ │ └── index.vue │ │ │ ├── drag-canvas │ │ │ │ └── index.vue │ │ │ ├── done-tree │ │ │ │ └── index.vue │ │ │ ├── context-menu │ │ │ │ └── index.vue │ │ │ ├── pattern-grid │ │ │ │ └── index.vue │ │ │ ├── render-item │ │ │ │ └── index.vue │ │ │ └── draw-line-render │ │ │ │ └── index.vue │ │ ├── assets │ │ │ └── css │ │ │ │ └── custom_ani.css │ │ ├── store │ │ │ ├── cache.ts │ │ │ ├── left-aside.ts │ │ │ ├── context-menu.ts │ │ │ ├── global.ts │ │ │ └── types.ts │ │ ├── composables │ │ │ ├── index.ts │ │ │ ├── thumbnail.ts │ │ │ └── sys-line.ts │ │ └── ace-edit.ts │ ├── mt-preview │ │ ├── index.ts │ │ └── index.vue │ ├── test │ │ ├── my-button │ │ │ └── index.vue │ │ ├── my-input │ │ │ └── index.vue │ │ ├── pie-charts │ │ │ └── index.vue │ │ └── custom-demo │ │ │ └── index.vue │ └── custom-components │ │ ├── text-vue │ │ └── index.vue │ │ ├── card-vue │ │ └── index.vue │ │ ├── sys-button-vue │ │ └── index.vue │ │ ├── kv-vue │ │ └── index.vue │ │ └── now-time-vue │ │ └── index.vue ├── assets │ ├── main.css │ ├── logo.png │ ├── imgs │ │ └── test │ │ │ └── my-img.png │ ├── icons │ │ ├── menu-fold.svg │ │ ├── menu-unfold.svg │ │ ├── return.svg │ │ ├── view-show.svg │ │ ├── delete.svg │ │ ├── upload.svg │ │ ├── full-screen.svg │ │ ├── preview.svg │ │ ├── export-json.svg │ │ ├── import-json.svg │ │ ├── vertical-distribution.svg │ │ ├── horizontal-distribution.svg │ │ ├── tree-list.svg │ │ ├── thumbnail.svg │ │ ├── save.svg │ │ ├── view-hide.svg │ │ ├── align-left.svg │ │ ├── align-right.svg │ │ ├── align-vertical.svg │ │ ├── align-bottom.svg │ │ ├── align-horizontally.svg │ │ ├── align-top.svg │ │ ├── add.svg │ │ ├── help.svg │ │ ├── rotate.svg │ │ ├── redo.svg │ │ ├── unlock.svg │ │ ├── undo.svg │ │ ├── lock.svg │ │ ├── exit-full-screen.svg │ │ ├── align.svg │ │ ├── search.svg │ │ ├── pen-line.svg │ │ ├── dark.svg │ │ ├── line.svg │ │ ├── ungroup.svg │ │ ├── group.svg │ │ ├── question.svg │ │ ├── setting.svg │ │ └── light.svg │ ├── svgs │ │ └── electrical │ │ │ ├── 手车02.svg │ │ │ ├── stroke │ │ │ ├── 电动机.svg │ │ │ ├── 熔断器.svg │ │ │ ├── 三绕组自耦变压器.svg │ │ │ ├── 手车01.svg │ │ │ ├── 电容器.svg │ │ │ ├── 接地.svg │ │ │ ├── 电缆终端头.svg │ │ │ ├── 电抗器.svg │ │ │ ├── 隔离开关-刀闸.svg │ │ │ ├── 自动空气断路器.svg │ │ │ ├── 断路器-开关.svg │ │ │ ├── 跌落式熔断器.svg │ │ │ └── 消弧线圈.svg │ │ │ ├── fs │ │ │ ├── 电阻(阻抗).svg │ │ │ ├── 火花间隙.svg │ │ │ ├── 故障.svg │ │ │ └── 避雷器.svg │ │ │ ├── 双绕组变压器.svg │ │ │ ├── 交流发电机.svg │ │ │ └── 三绕组变压器.svg │ └── css-vars.css ├── export.ts ├── main.ts ├── views │ ├── preview │ │ └── index.vue │ ├── edit │ │ └── index.vue │ └── demo │ │ ├── index.vue │ │ ├── set-node-attr.vue │ │ ├── event-callback.vue │ │ ├── change-attr.vue │ │ └── edit-load.vue ├── router │ └── index.ts └── App.vue ├── env.d.ts ├── public ├── imgs │ ├── edit.png │ ├── change-attr.png │ ├── edit-load.png │ ├── set-node-attr.gif │ └── event-callback.png ├── svgs │ ├── my-button.svg │ ├── pie-charts.svg │ ├── my-input.svg │ └── demo.svg └── favicon.svg ├── .vscode ├── extensions.json └── settings.json ├── .prettierrc.json ├── tsconfig.vitest.json ├── tsconfig.json ├── tsconfig.app.json ├── .gitignore ├── index.html ├── global.d.ts ├── vitest.config.ts ├── tsconfig.node.json ├── .eslintrc.cjs ├── uno.config.ts ├── README.md ├── vite.config.ts ├── package.json └── LICENSE /src/components/mt-dzr/composables/mouse.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/assets/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaolunmao/maotu-webtopo/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/mt-dzr/index.ts: -------------------------------------------------------------------------------- 1 | import MtDzr from './index.vue'; 2 | 3 | export default MtDzr; 4 | -------------------------------------------------------------------------------- /src/components/mt-dzr/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type MouseTouchEvent = MouseEvent | TouchEvent; 2 | -------------------------------------------------------------------------------- /public/imgs/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaolunmao/maotu-webtopo/HEAD/public/imgs/edit.png -------------------------------------------------------------------------------- /src/components/mt-edit/index.ts: -------------------------------------------------------------------------------- 1 | import MtEdit from './index.vue'; 2 | 3 | export default MtEdit; 4 | -------------------------------------------------------------------------------- /public/imgs/change-attr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaolunmao/maotu-webtopo/HEAD/public/imgs/change-attr.png -------------------------------------------------------------------------------- /public/imgs/edit-load.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaolunmao/maotu-webtopo/HEAD/public/imgs/edit-load.png -------------------------------------------------------------------------------- /src/components/mt-preview/index.ts: -------------------------------------------------------------------------------- 1 | import MtPreview from './index.vue'; 2 | 3 | export default MtPreview; 4 | -------------------------------------------------------------------------------- /public/imgs/set-node-attr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaolunmao/maotu-webtopo/HEAD/public/imgs/set-node-attr.gif -------------------------------------------------------------------------------- /public/imgs/event-callback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaolunmao/maotu-webtopo/HEAD/public/imgs/event-callback.png -------------------------------------------------------------------------------- /src/assets/imgs/test/my-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaolunmao/maotu-webtopo/HEAD/src/assets/imgs/test/my-img.png -------------------------------------------------------------------------------- /src/components/mt-dzr/components/render-item.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/selected-area/types.ts: -------------------------------------------------------------------------------- 1 | export interface IAreaBinfo { 2 | width: number; 3 | height: number; 4 | top: number; 5 | left: number; 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "Vue.vscode-typescript-vue-plugin", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": true, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit", 4 | "eslint.autoFixOnSave": "explicit" 5 | }, 6 | "files.eol": "\n" 7 | } 8 | 9 | -------------------------------------------------------------------------------- /tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "exclude": [], 4 | "compilerOptions": { 5 | "composite": true, 6 | "lib": [], 7 | "types": ["node", "jsdom"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/icons/menu-fold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/menu-unfold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/return.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/手车02.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/view-show.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | }, 10 | { 11 | "path": "./tsconfig.vitest.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/icons/upload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/stroke/电动机.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/my-button.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svgs/pie-charts.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/full-screen.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/preview.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/export-json.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/import-json.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/vertical-distribution.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "@/*": ["./src/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/icons/horizontal-distribution.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/render-core/types.ts: -------------------------------------------------------------------------------- 1 | import type { CacheBoundingBox, IDoneJsonBinfo } from '../../store/types'; 2 | 3 | export interface onItemMoveParams { 4 | move_item_bounding_info: MoveItemBoundingInfo[]; 5 | move_binfo: IDoneJsonBinfo & { id: string }; 6 | } 7 | export type MoveItemBoundingInfo = CacheBoundingBox; 8 | -------------------------------------------------------------------------------- /src/assets/icons/tree-list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/thumbnail.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/save.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/view-hide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/fs/电阻(阻抗).svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | R 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .idea 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /src/assets/icons/align-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/align-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/layout/footer-panel/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MaoTu 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | $svgEventCallBack: (type: string, svg_item_id: string, ...args: any[]) => void; 4 | $setItemAttrByID: (id: string, key: string, val: any) => Promise; 5 | $getItemAttrByID: (id: string, key: string, val: any) => any; 6 | $previewCompareVal: (val1: any, operator: '>' | '<' | '=' | '!=', val2: any) => boolean; 7 | } 8 | } 9 | export {}; 10 | -------------------------------------------------------------------------------- /src/assets/icons/align-vertical.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/mt-dzr/__tests__/mt-dzr.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import { mount } from '@vue/test-utils'; 4 | import HelloWorld from '../index.vue'; 5 | 6 | describe('HelloWorld', () => { 7 | it('renders properly', () => { 8 | const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } }); 9 | expect(wrapper.text()).toContain('Hello Vitest'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/assets/icons/align-bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/align-horizontally.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/align-top.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/add.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/stroke/熔断器.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/mt-dzr/store/types.ts: -------------------------------------------------------------------------------- 1 | import type { IDzrPropsModelValue } from '../types'; 2 | 3 | export interface IDzrStore { 4 | dzr_copy_info: IDzrCopyInfo; 5 | setDzrCopyInfo: (value: IDzrCopyInfo) => void; 6 | showDzrCopy: (value: IDzrPropsModelValue, gen_id: string) => void; 7 | hideDzrCopy: () => void; 8 | } 9 | export interface IDzrCopyInfo { 10 | gen_id: string; 11 | show: boolean; 12 | value: IDzrPropsModelValue; 13 | } 14 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { mergeConfig, defineConfig, configDefaults } from 'vitest/config' 3 | import viteConfig from './vite.config' 4 | 5 | export default mergeConfig( 6 | viteConfig, 7 | defineConfig({ 8 | test: { 9 | environment: 'jsdom', 10 | exclude: [...configDefaults.exclude, 'e2e/*'], 11 | root: fileURLToPath(new URL('./', import.meta.url)) 12 | } 13 | }) 14 | ) 15 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Bundler", 14 | "types": ["node"] 15 | }, 16 | "paths":{ 17 | "@/*":["src/*"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/icons/help.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/rotate.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/types.ts: -------------------------------------------------------------------------------- 1 | import type { IDoneJson, IGlobalStoreCanvasCfg, IGlobalStoreGridCfg } from '../store/types'; 2 | 3 | export type MouseTouchEvent = MouseEvent | TouchEvent; 4 | export interface IExportDoneJson extends Omit { 5 | props: { 6 | [key: string]: any; 7 | }; 8 | } 9 | export interface IExportJson { 10 | canvasCfg: IGlobalStoreCanvasCfg; 11 | gridCfg: IGlobalStoreGridCfg; 12 | json: IExportDoneJson[]; 13 | } 14 | -------------------------------------------------------------------------------- /src/assets/icons/redo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/unlock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/undo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/svg-analysis/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 20 | -------------------------------------------------------------------------------- /src/export.ts: -------------------------------------------------------------------------------- 1 | import 'element-plus/dist/index.css'; 2 | import 'virtual:uno.css'; 3 | import '@/assets/css-vars.css'; 4 | import 'virtual:svg-icons-register'; 5 | import 'animate.css'; 6 | import '@/components/mt-edit/assets/css/custom_ani.css'; 7 | import MtDzr from '@/components/mt-dzr'; 8 | import MtEdit from '@/components/mt-edit'; 9 | import MtPreview from '@/components/mt-preview'; 10 | import { leftAsideStore } from '@/components/mt-edit/store/left-aside'; 11 | export { MtDzr, MtEdit, MtPreview, leftAsideStore }; 12 | -------------------------------------------------------------------------------- /src/assets/icons/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/test/my-button/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 13 | 19 | -------------------------------------------------------------------------------- /src/assets/icons/exit-full-screen.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/stroke/三绕组自耦变压器.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/stroke/手车01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/fs/火花间隙.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/test/my-input/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 18 | 19 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution'); 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | '@vue/eslint-config-prettier' 11 | // '@vue/eslint-config-prettier/skip-formatting' 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 'latest' 15 | }, 16 | rules: { 17 | 'vue/multi-word-component-names': [ 18 | 'error', 19 | { 20 | ignores: ['index', 'main'] 21 | } 22 | ] 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/assets/icons/align.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | import './assets/main.css'; 4 | import router from './router/index'; 5 | import MyButton from '@/components/test/my-button/index.vue'; 6 | import MyInput from '@/components/test/my-input/index.vue'; 7 | import CustomDemo from '@/components/test/custom-demo/index.vue'; 8 | import PieCharts from '@/components/test/pie-charts/index.vue'; 9 | const app = createApp(App); 10 | app.use(router); 11 | app.component('my-input', MyInput); 12 | app.component('my-button', MyButton); 13 | app.component('custom-demo', CustomDemo); 14 | app.component('pie-charts', PieCharts); 15 | app.mount('#app'); 16 | -------------------------------------------------------------------------------- /src/assets/icons/pen-line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/stroke/电容器.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/stroke/接地.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/fs/故障.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /src/views/preview/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 20 | -------------------------------------------------------------------------------- /public/svgs/my-input.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | // uno.config.ts 2 | import { defineConfig, presetAttributify, presetUno } from 'unocss'; 3 | 4 | export default defineConfig({ 5 | theme: { 6 | colors: { 7 | // ... 8 | myDarkBgColor: '#141414', 9 | myMainDarkBgColor: '#1c1c1c' 10 | } 11 | }, 12 | presets: [ 13 | presetAttributify({ 14 | /* preset options */ 15 | }), 16 | presetUno() 17 | ], 18 | shortcuts: { 19 | 'ct-border': 'border-t-1px border-t-solid border-t-gray-300', 20 | 'cr-border': 'border-r-1px border-r-solid border-r-gray-300 ', 21 | 'cl-border': 'border-l-1px border-l-solid border-l-gray-300', 22 | 'cb-border': 'border-b-1px border-b-solid border-b-gray-300' 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/custom-svg-render/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 27 | -------------------------------------------------------------------------------- /src/assets/icons/dark.svg: -------------------------------------------------------------------------------- 1 | 4 | 6 | -------------------------------------------------------------------------------- /src/components/mt-dzr/types.ts: -------------------------------------------------------------------------------- 1 | export interface IDzrProps { 2 | id: string; 3 | modelValue: IDzrPropsModelValue; //位置和大小 4 | scaleRatio?: number; //画布缩放倍数 5 | hide: boolean; //隐藏 6 | grid?: IDzrPropsGrid; //网格配置 7 | resize?: boolean; //开启缩放 8 | rotate?: boolean; //开启旋转 9 | lock?: boolean; //锁定 10 | active?: boolean; //激活 11 | useProportionalScaling?: boolean; //开启等比例缩放 12 | showGhostDom?: boolean; //是否显示幽灵dom 13 | class?: string; // 14 | disabled: boolean; //是否禁用 15 | adsorp_diff?: { 16 | x: number; 17 | y: number; 18 | }; 19 | } 20 | export interface IDzrPropsModelValue { 21 | left: number; 22 | top: number; 23 | width: number; 24 | height: number; 25 | angle: number; 26 | } 27 | export interface IDzrPropsGrid { 28 | enabled: boolean; //开启网格 29 | align: boolean; //对齐到网格 30 | size: number; //网格大小 31 | } 32 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/双绕组变压器.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/custom-components/text-vue/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 34 | 40 | -------------------------------------------------------------------------------- /src/assets/icons/line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/stroke/电缆终端头.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/mt-dzr/store/index.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue'; 2 | import type { IDzrCopyInfo, IDzrStore } from './types'; 3 | import type { IDzrPropsModelValue } from '../types'; 4 | 5 | export const dzrStore: IDzrStore = reactive({ 6 | dzr_copy_info: { 7 | gen_id: '', 8 | show: false, 9 | value: { 10 | left: 0, 11 | top: 0, 12 | width: 0, 13 | height: 0, 14 | angle: 0 15 | } 16 | }, 17 | setDzrCopyInfo: (value: IDzrCopyInfo) => { 18 | dzrStore.dzr_copy_info = value; 19 | }, 20 | showDzrCopy: (value: IDzrPropsModelValue, gen_id: string) => { 21 | dzrStore.setDzrCopyInfo({ 22 | ...dzrStore.dzr_copy_info, 23 | show: true, 24 | value, 25 | gen_id 26 | }); 27 | }, 28 | hideDzrCopy: () => { 29 | dzrStore.setDzrCopyInfo({ 30 | ...dzrStore.dzr_copy_info, 31 | show: false 32 | }); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/svg-render/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 28 | -------------------------------------------------------------------------------- /src/assets/icons/ungroup.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/stroke/电抗器.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/fs/避雷器.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/icons/group.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/stroke/隔离开关-刀闸.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/stroke/自动空气断路器.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/mt-edit/assets/css/custom_ani.css: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes rotate360 { 2 | 0% { 3 | -webkit-transform: rotate3d(0, 0, 1, 0deg); 4 | transform: rotate3d(0, 0, 1, 0deg); 5 | } 6 | 50% { 7 | -webkit-transform: rotate3d(0, 0, 1, 180deg); 8 | transform: rotate3d(0, 0, 1, 180deg); 9 | } 10 | to { 11 | -webkit-transform: rotate3d(0, 0, 1, 360deg); 12 | transform: rotate3d(0, 0, 1, 360deg); 13 | } 14 | } 15 | 16 | @keyframes rotate360 { 17 | 0% { 18 | -webkit-transform: rotate3d(0, 0, 1, 0deg); 19 | transform: rotate3d(0, 0, 1, 0deg); 20 | } 21 | 50% { 22 | -webkit-transform: rotate3d(0, 0, 1, 180deg); 23 | transform: rotate3d(0, 0, 1, 180deg); 24 | } 25 | to { 26 | -webkit-transform: rotate3d(0, 0, 1, 360deg); 27 | transform: rotate3d(0, 0, 1, 360deg); 28 | } 29 | } 30 | 31 | .animate__rotate360 { 32 | -webkit-animation-name: rotate360; 33 | animation-name: rotate360; 34 | animation-timing-function: linear; 35 | -webkit-transform-origin: center; 36 | transform-origin: center; 37 | } 38 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/stroke/断路器-开关.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/custom-components/card-vue/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 13 | 30 | 40 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/交流发电机.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | G 5 | ~ 6 | -------------------------------------------------------------------------------- /src/assets/icons/question.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/mt-edit/store/cache.ts: -------------------------------------------------------------------------------- 1 | import { nextTick, reactive } from 'vue'; 2 | import type { CacheBoundingBox, ICache, IDoneJson } from './types'; 3 | import { objectDeepClone } from '../utils'; 4 | 5 | export const cacheStore: ICache = reactive({ 6 | boundingBox: [], 7 | setBoundingBox: (val: CacheBoundingBox[]) => { 8 | cacheStore.boundingBox = val; 9 | }, 10 | adsorbPoint: [], 11 | setAdsorbPoint(val) { 12 | cacheStore.adsorbPoint = val; 13 | }, 14 | copy: [], 15 | setCopy(val) { 16 | cacheStore.copy = val; 17 | }, 18 | history: [[]], 19 | historyIndex: 0, 20 | addHistory(val: IDoneJson[]) { 21 | nextTick(() => { 22 | if (cacheStore.historyIndex + 1 < cacheStore.history.length) { 23 | cacheStore.history.splice(cacheStore.historyIndex + 1); 24 | } 25 | cacheStore.history.push(objectDeepClone(val)); 26 | cacheStore.historyIndex = cacheStore.history.length - 1; 27 | if (cacheStore.history.length > 20) { 28 | cacheStore.history.shift(); 29 | cacheStore.historyIndex = cacheStore.history.length - 1; 30 | } 31 | }); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/stroke/跌落式熔断器.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/views/edit/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/三绕组变压器.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/svgs/electrical/stroke/消弧线圈.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/export-json/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 41 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/import-json/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 43 | -------------------------------------------------------------------------------- /src/assets/icons/setting.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router'; 2 | export const constantRoutes = [ 3 | { 4 | path: '/', 5 | component: () => import('../views/edit/index.vue') 6 | }, 7 | { 8 | name: 'edit', 9 | path: '/edit', 10 | component: () => import('../views/edit/index.vue') 11 | }, 12 | { 13 | name: 'demo', 14 | path: '/demo', 15 | component: () => import('../views/demo/index.vue') 16 | }, 17 | { 18 | name: 'preview', 19 | path: '/preview', 20 | component: () => import('../views/preview/index.vue') 21 | }, 22 | { 23 | name: 'edit-load', 24 | path: '/edit-load', 25 | component: () => import('../views/demo/edit-load.vue') 26 | }, 27 | { 28 | name: 'set-node-attr', 29 | path: '/set-node-attr', 30 | component: () => import('../views/demo/set-node-attr.vue') 31 | }, 32 | { 33 | name: 'event-callback', 34 | path: '/event-callback', 35 | component: () => import('../views/demo/event-callback.vue') 36 | }, 37 | { 38 | name: 'change-attr', 39 | path: '/change-attr', 40 | component: () => import('../views/demo/change-attr.vue') 41 | } 42 | ]; 43 | const router = createRouter({ 44 | history: createWebHistory(), // hash模式:createWebHashHistory,history模式:createWebHistory 45 | routes: constantRoutes 46 | }); 47 | 48 | export default router; 49 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/layout/right-aside/select-item-event-setting/input-target-value.vue: -------------------------------------------------------------------------------- 1 | 2 | 16 | 32 | -------------------------------------------------------------------------------- /src/components/custom-components/sys-button-vue/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 49 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ao 31 | tu 32 | 33 | M A O T U 34 | 35 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/layout/right-aside/json-edit.vue: -------------------------------------------------------------------------------- 1 | 26 | 44 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/group-render/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 41 | 49 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/layout/right-aside/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 36 | 42 | -------------------------------------------------------------------------------- /src/components/mt-edit/store/left-aside.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance, reactive } from 'vue'; 2 | import type { ILeftAside, ILeftAsideConfigItemPublic, ILeftAsideConfigItem } from './types'; 3 | import { ElMessage } from 'element-plus'; 4 | import { svgToSymbol } from '../utils'; 5 | import { configStore } from './config'; 6 | 7 | export const leftAsideStore: ILeftAside = reactive({ 8 | config: new Map([ 9 | ['本地文件', []], 10 | ['系统组件', configStore.sysComponent] 11 | ]), 12 | registerConfig: (title: string, config: ILeftAsideConfigItemPublic[]) => { 13 | if (title == '本地文件' || title == '系统组件') { 14 | ElMessage.info(`title:${title}已被系统占用,请更换名称!`); 15 | return; 16 | } 17 | 18 | if (leftAsideStore.config.has(title)) { 19 | ElMessage.info(`title:${title}已存在,已经将其配置覆盖`); 20 | } 21 | const cfg: ILeftAsideConfigItem[] = config.map((m) => { 22 | if (m.type == 'svg') { 23 | const { symbol_str, width, height } = svgToSymbol(m.svg!, m.id); 24 | return { 25 | ...m, 26 | symbol: { 27 | symbol_id: m.id, 28 | symbol_str, 29 | width, 30 | height 31 | }, 32 | common_animations: { 33 | val: '', 34 | delay: 'delay-0s', 35 | speed: 'slow', 36 | repeat: 'infinite' 37 | } 38 | }; 39 | } 40 | return { 41 | ...m, 42 | common_animations: { 43 | val: '', 44 | delay: 'delay-0s', 45 | speed: 'slow', 46 | repeat: 'infinite' 47 | } 48 | }; 49 | }); 50 | leftAsideStore.config.set(title, cfg); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /src/components/custom-components/kv-vue/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 53 | 72 | -------------------------------------------------------------------------------- /src/views/demo/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # maotu-webtopo 2 | 3 | 基于 vue3 的 web 组态引擎库 4 | 5 | 探索将已有 svg 文件转为自由缩放图形库的解决方案,也可用作低代码大屏项目开发 6 | 7 | ## 说明 8 | 9 | 此开源版本的代码源自于 [maotu 插件版](https://www.npmjs.com/package/maotu) 0.3.1 版本,与插件版的差异请查阅插件版的 readme。 10 | 11 | ## 使用文档 12 | 13 | 请参考:[http://mt.yaolm.top](http://mt.yaolm.top) 14 | 15 | ## 声明 16 | 17 | `maotu-webtopo` 使用了 `LGPL-3.0` 协议。这意味着: 18 | * 您可以将 `maotu-webtopo` 作为库链接到您的商业项目,而无需开源您的整个项目。 19 | * 如果您修改了 `maotu-webtopo` 的**核心库**代码,并分发了修改后的版本,您必须按照 LGPL-3.0 协议的要求,开源您所做的修改。 20 | * 如果您仅仅是将`maotu-webtopo` 作为库链接到你的项目,而没有修改或分发它的源代码,那么你的项目无需开源。 21 | 22 | 详细的 LGPL-3.0 许可证文本请查阅 [https://www.gnu.org/licenses/lgpl-3.0.html](https://www.gnu.org/licenses/lgpl-3.0.html)。 23 | 24 | ## 如何构建插件并使用 25 | 26 | **构建库:** 27 | 28 | 1. 使用 `pnpm run lib` 命令构建 `maotu-webtopo`,生成 `dist` 文件夹。 29 | 30 | **使用库:** 31 | 32 | 1. **推荐使用 pnpm 安装:** 33 | 34 | ```bash 35 | pnpm i maotu # 前提是你已经发布到了 npm 36 | ``` 37 | 38 | 如果选择手动复制,请继续参考以下步骤 39 | 40 | 2. 将 `dist` 文件夹中的以下文件复制到你的项目: 41 | * `dist/maotu.es.js`:库的入口文件。 42 | * `dist/style.css`:库的样式文件。 43 | * `dist/src` : 库的ts类型定义。 44 | * 将这些文件放到你项目中的合适位置。例如,你可以创建一个 `src/lib/maotu` 目录,并将它们复制到这里。 45 | 46 | 3. 确保你的项目可以访问到 `style.css` 文件。可以通过在入口文件或组件中引入的方式来实现。 47 | 48 | **在项目中使用示例:** 49 | 50 | ```vue 51 | 55 | 56 | 61 | 62 | 63 | ``` 64 | 65 | ## 鸣谢 66 | 67 | maotu的部分逻辑实现参考了以下大佬的文章 68 | 69 | [幽月之格-可拖拽、缩放、旋转组件实现细节](https://juejin.cn/user/3597257779449165/posts) 70 | 71 | [woai3c-一个低代码(可视化拖拽)教学项目](https://github.com/woai3c/visual-drag-demo) 72 | 73 | -------------------------------------------------------------------------------- /src/components/test/pie-charts/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 65 | 66 | 72 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/drag-canvas/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 49 | 50 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/done-tree/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 52 | -------------------------------------------------------------------------------- /src/components/mt-edit/store/context-menu.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue'; 2 | import type { ContextMenuInfoType, IContextMenu } from './types'; 3 | 4 | export const contextMenuStore: IContextMenu = reactive({ 5 | menuInfo: { 6 | display: false, 7 | left: 0, 8 | top: 0, 9 | info: { 10 | selectAll: { 11 | title: '全选', 12 | hot_key: 'Ctrl + A', 13 | enable: false 14 | }, 15 | copy: { 16 | title: '复制', 17 | hot_key: 'Ctrl + C', 18 | enable: false 19 | }, 20 | paste: { 21 | title: '粘贴', 22 | hot_key: 'Ctrl + V', 23 | enable: false 24 | }, 25 | delete: { 26 | title: '删除', 27 | hot_key: 'Delete', 28 | enable: false 29 | }, 30 | group: { 31 | title: '组合', 32 | hot_key: 'Ctrl + G', 33 | enable: false 34 | }, 35 | ungroup: { 36 | title: '取消组合', 37 | hot_key: 'Ctrl + U', 38 | enable: false 39 | }, 40 | moveTop: { 41 | title: '置顶', 42 | hot_key: 'Ctrl + Right', 43 | enable: false 44 | }, 45 | moveUp: { 46 | title: '上移', 47 | hot_key: 'Ctrl + Up', 48 | enable: false 49 | }, 50 | moveDown: { 51 | title: '下移', 52 | hot_key: 'Ctrl + Down', 53 | enable: false 54 | }, 55 | moveBottom: { 56 | title: '置底', 57 | hot_key: 'Ctrl + Left', 58 | enable: false 59 | } 60 | } 61 | }, 62 | setMenuInfo: (val: IContextMenu['menuInfo']) => { 63 | contextMenuStore.menuInfo = val; 64 | }, 65 | setDisplayItem: (val: ContextMenuInfoType[]) => { 66 | for (const key in contextMenuStore.menuInfo.info) { 67 | contextMenuStore.menuInfo.info[key as ContextMenuInfoType].enable = false; 68 | } 69 | val.forEach((f) => { 70 | contextMenuStore.menuInfo.info[f].enable = true; 71 | }); 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /src/views/demo/set-node-attr.vue: -------------------------------------------------------------------------------- 1 | 2 | 9 | 84 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url'; 2 | 3 | import { defineConfig, UserConfigExport } from 'vite'; 4 | import vue from '@vitejs/plugin-vue'; 5 | import { resolve } from 'path'; 6 | import dts from 'vite-plugin-dts'; 7 | import UnoCSS from 'unocss/vite'; 8 | import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'; 9 | // https://vitejs.dev/config/ 10 | export default defineConfig(({ mode }) => { 11 | let config: UserConfigExport = { 12 | plugins: [ 13 | vue(), 14 | dts({ 15 | tsconfigPath: 'tsconfig.app.json' 16 | }), 17 | UnoCSS({ 18 | // 在低版本浏览器上开发时会报错 Unexpected reserved word 19 | // 如果在开发环境需要兼容不支持顶级await的低版本浏览器例如Chrome(v87),就将下面的配置打开 20 | // hmrTopLevelAwait: false 21 | }), 22 | createSvgIconsPlugin({ 23 | // 指定需要缓存的图标文件夹 24 | iconDirs: [resolve(process.cwd(), 'src/assets/icons')], 25 | // 指定symbolId格式 26 | symbolId: 'mt-edit-[name]', 27 | // 禁用压缩 否则想要修改无状态组件的stroke或者fill会影响到预设样式 例如stroke-width 28 | svgoOptions: false, 29 | customDomId: '___mt__edit__icons__dom__' 30 | }) 31 | ], 32 | 33 | resolve: { 34 | alias: { 35 | '@': fileURLToPath(new URL('./src', import.meta.url)) 36 | }, 37 | dedupe: ['vue'] 38 | } 39 | }; 40 | if (mode === 'lib' || mode === 'npm') { 41 | config = { 42 | ...config, 43 | ...{ 44 | build: { 45 | lib: { 46 | entry: resolve(__dirname, 'src/export.ts'), 47 | name: 'maotu', 48 | fileName: (format: any) => `maotu.${format}.js` 49 | }, 50 | rollupOptions: { 51 | // 确保外部化处理那些你不想打包进库的依赖 52 | external: ['vue', 'pinia'], 53 | output: { 54 | // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量 55 | globals: { 56 | vue: 'Vue', 57 | pinia: 'Pinia' 58 | }, 59 | inlineDynamicImports: true 60 | } 61 | } 62 | } 63 | } 64 | }; 65 | } 66 | return config; 67 | }); 68 | -------------------------------------------------------------------------------- /public/svgs/demo.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 12 | 16 | 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maotu", 3 | "version": "0.3.1", 4 | "private": false, 5 | "scripts": { 6 | "dev": "pnpm run format && vite", 7 | "build": "run-p type-check \"build-only {@}\" --", 8 | "preview": "vite preview", 9 | "test:unit": "vitest", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", 12 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 13 | "format": "prettier --write src/", 14 | "lib": "vite build --mode lib", 15 | "npm": "vite build --mode npm" 16 | }, 17 | "files": [ 18 | "dist" 19 | ], 20 | "main": "./dist/maotu.umd.js", 21 | "module": "./dist/maotu.es.js", 22 | "exports": { 23 | ".": { 24 | "import": "./dist/maotu.es.js", 25 | "require": "./dist/maotu.umd.js", 26 | "types": "./dist/src/export.d.ts" 27 | }, 28 | "./*": "./*" 29 | }, 30 | "typings": "dist/export.d.ts", 31 | "dependencies": { 32 | "@vueuse/core": "^10.6.1", 33 | "ace-builds": "^1.32.0", 34 | "animate.css": "^4.1.1", 35 | "canvg": "^4.0.1", 36 | "echarts": "^5.4.3", 37 | "element-plus": "^2.4.2", 38 | "html2canvas": "^1.4.1", 39 | "less": "^4.2.0", 40 | "vue": "^3.3.4", 41 | "vue-echarts": "^6.6.5", 42 | "vue-router": "^4.2.5", 43 | "vue3-ace-editor": "^2.2.4" 44 | }, 45 | "devDependencies": { 46 | "@rushstack/eslint-patch": "^1.3.3", 47 | "@tsconfig/node18": "^18.2.2", 48 | "@types/jsdom": "^21.1.3", 49 | "@types/node": "^18.18.5", 50 | "@vitejs/plugin-vue": "^4.4.0", 51 | "@vue/eslint-config-prettier": "^8.0.0", 52 | "@vue/eslint-config-typescript": "^12.0.0", 53 | "@vue/test-utils": "^2.4.1", 54 | "@vue/tsconfig": "^0.4.0", 55 | "eslint": "^8.49.0", 56 | "eslint-plugin-vue": "^9.17.0", 57 | "jsdom": "^22.1.0", 58 | "npm-run-all2": "^6.1.1", 59 | "prettier": "^3.0.3", 60 | "typescript": "~5.2.0", 61 | "unocss": "^0.57.4", 62 | "vite": "^4.4.11", 63 | "vite-plugin-dts": "^3.6.0", 64 | "vite-plugin-svg-icons": "^2.0.1", 65 | "vitest": "^0.34.6", 66 | "vue-tsc": "^1.8.19" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/components/mt-edit/composables/index.ts: -------------------------------------------------------------------------------- 1 | import type { IExportDoneJson, IExportJson } from '../components/types'; 2 | import { leftAsideStore } from '../store/left-aside'; 3 | import type { 4 | IDoneJson, 5 | IGlobalStoreCanvasCfg, 6 | IGlobalStoreGridCfg, 7 | ILeftAsideConfigItem, 8 | ILeftAsideConfigItemPublicProps 9 | } from '../store/types'; 10 | import { objectDeepClone } from '../utils'; 11 | 12 | export const genExportJson = ( 13 | canvasCfg: IGlobalStoreCanvasCfg, 14 | gridCfg: IGlobalStoreGridCfg, 15 | doneJson: IDoneJson[] 16 | ) => { 17 | let export_done_json: IExportDoneJson[] = []; 18 | export_done_json = objectDeepClone(doneJson).map((m) => { 19 | if (m.symbol) { 20 | delete m.symbol; 21 | } 22 | let new_props = {}; 23 | for (const key in m.props) { 24 | new_props = { ...new_props, ...{ [key]: m.props[key].val } }; 25 | } 26 | return { 27 | ...m, 28 | props: new_props, 29 | active: false 30 | }; 31 | }); 32 | const exportJson: IExportJson = { 33 | canvasCfg, 34 | gridCfg, 35 | json: export_done_json 36 | }; 37 | return { exportJson }; 38 | }; 39 | export const useExportJsonToDoneJson = (json: IExportJson) => { 40 | // 取出所有图形的初始配置 41 | let init_configs: ILeftAsideConfigItem[] = []; 42 | for (const iterator of leftAsideStore.config.values()) { 43 | if (iterator.length > 0) { 44 | init_configs = [...init_configs, ...iterator]; 45 | } 46 | } 47 | const importDoneJson: IDoneJson[] = json.json.map((m) => { 48 | let props: ILeftAsideConfigItemPublicProps = {}; 49 | let symbol = undefined; 50 | // 找到原始的props 51 | const find_item = init_configs.find((f) => f?.id == m.tag); 52 | const find_props = find_item?.props; 53 | if (find_props) { 54 | props = { ...props, ...objectDeepClone(find_props) }; 55 | } 56 | for (const key in m.props) { 57 | props[key].val = m.props[key]; 58 | } 59 | if (find_item?.symbol) { 60 | symbol = find_item.symbol; 61 | } 62 | return { 63 | ...m, 64 | props, 65 | symbol 66 | }; 67 | }); 68 | return { 69 | canvasCfg: json.canvasCfg, 70 | gridCfg: json.gridCfg, 71 | importDoneJson 72 | }; 73 | }; 74 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/context-menu/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 31 | 87 | -------------------------------------------------------------------------------- /src/assets/icons/light.svg: -------------------------------------------------------------------------------- 1 | 4 | 7 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/pattern-grid/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 74 | 75 | 83 | -------------------------------------------------------------------------------- /src/components/custom-components/now-time-vue/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 23 | 79 | -------------------------------------------------------------------------------- /src/components/mt-edit/composables/thumbnail.ts: -------------------------------------------------------------------------------- 1 | import { Canvg } from 'canvg'; 2 | import html2canvas from 'html2canvas'; 3 | import { ElMessage } from 'element-plus'; 4 | export const useGenThumbnail = async (canvas_id: string = 'mtCanvasArea') => { 5 | const el = document.querySelector(`#${canvas_id}`); 6 | if (!el) { 7 | ElMessage.error('没有找到canvas元素,请检查!'); 8 | return; 9 | } 10 | //记录要移除的svg元素 11 | const shouldRemoveSvgNodes = []; 12 | // 获取到所有的SVG 得到一个数组 目前只有自定义连线需要特殊处理 别的元素直接使用html2canvas就可以 13 | const svgElements: NodeListOf = document.body.querySelectorAll( 14 | `#${canvas_id} .mt-line-render` 15 | ); 16 | // 遍历这个数组 17 | for (const item of svgElements) { 18 | //去除空白字符 19 | const svg = item.outerHTML.trim(); 20 | // 创建一个 canvas DOM元素 21 | const canvas = document.createElement('canvas'); 22 | //设置 canvas 元素的宽高 23 | canvas.width = item.getBoundingClientRect().width; 24 | canvas.height = item.getBoundingClientRect().height; 25 | const ctx = canvas.getContext('2d'); 26 | // 将 SVG转化 成 canvas 27 | const v = Canvg.fromString(ctx!, svg); 28 | await v.render(); 29 | 30 | //设置生成 canvas 元素的坐标 保证与原SVG坐标保持一致 31 | if (item.style.position) { 32 | canvas.style.position += item.style.position; 33 | canvas.style.left += item.style.left; 34 | canvas.style.top += item.style.top; 35 | } 36 | 37 | //添加到需要截图的DOM节点中 38 | item.parentNode!.appendChild(canvas); 39 | // 删除这个元素 40 | shouldRemoveSvgNodes.push(canvas); 41 | } 42 | 43 | const width = el.offsetWidth; 44 | const height = el.offsetHeight; 45 | const canvas = await html2canvas(el, { 46 | useCORS: true, 47 | scale: 2, 48 | width, 49 | height, 50 | allowTaint: true, 51 | windowHeight: height, 52 | logging: false, 53 | ignoreElements: (element) => { 54 | if (element.classList.contains('mt-line-render')) { 55 | return true; 56 | } 57 | return false; 58 | } 59 | }); 60 | const img_link = document.createElement('a'); 61 | img_link.href = canvas.toDataURL('image/png'); // 转换后的图片地址 62 | img_link.download = Date.now().toString(); 63 | document.body.appendChild(img_link); 64 | // 触发点击 65 | img_link.click(); 66 | // 然后移除 67 | document.body.removeChild(img_link); 68 | // 移除需要移除掉的svg节点 69 | shouldRemoveSvgNodes.forEach((item) => { 70 | item.remove(); 71 | }); 72 | }; 73 | -------------------------------------------------------------------------------- /src/components/test/custom-demo/index.vue: -------------------------------------------------------------------------------- 1 | 34 | 44 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/layout/right-aside/select-item-props-setting.vue: -------------------------------------------------------------------------------- 1 | 50 | 78 | -------------------------------------------------------------------------------- /src/components/mt-edit/ace-edit.ts: -------------------------------------------------------------------------------- 1 | import ace from 'ace-builds'; 2 | 3 | import themeMonokaiUrl from 'ace-builds/src-noconflict/theme-monokai?url'; 4 | ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl); 5 | 6 | import workerBaseUrl from 'ace-builds/src-noconflict/worker-base?url'; 7 | ace.config.setModuleUrl('ace/mode/base', workerBaseUrl); 8 | 9 | import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url'; 10 | ace.config.setModuleUrl('ace/mode/json', modeJsonUrl); 11 | import workerJsonUrl from 'ace-builds/src-noconflict/worker-json?url'; 12 | ace.config.setModuleUrl('ace/mode/json_worker', workerJsonUrl); 13 | import snippetsJsonUrl from 'ace-builds/src-noconflict/snippets/json?url'; 14 | ace.config.setModuleUrl('ace/snippets/json', snippetsJsonUrl); 15 | 16 | import modeJavascriptUrl from 'ace-builds/src-noconflict/mode-javascript?url'; 17 | ace.config.setModuleUrl('ace/mode/javascript', modeJavascriptUrl); 18 | import workerJavascriptUrl from 'ace-builds/src-noconflict/worker-javascript?url'; 19 | ace.config.setModuleUrl('ace/mode/javascript_worker', workerJavascriptUrl); 20 | import snippetsJavascriptUrl from 'ace-builds/src-noconflict/snippets/javascript?url'; 21 | ace.config.setModuleUrl('ace/snippets/javascript', snippetsJavascriptUrl); 22 | 23 | import 'ace-builds/src-noconflict/ext-language_tools'; 24 | // ace.require('ace/ext/language_tools'); 25 | const langTools = ace.require('ace/ext/language_tools'); 26 | langTools.addCompleter({ 27 | getCompletions: function ( 28 | _editor: any, 29 | _session: any, 30 | _pos: any, 31 | prefix: string | any[], 32 | callback: ( 33 | arg0: null, 34 | arg1: { 35 | name: string; //显示的名称 36 | value: string; //插入的值, 37 | score: number; //分数 38 | meta: string; //描述 39 | }[] 40 | ) => void 41 | ) { 42 | if (prefix.length === 0) { 43 | callback(null, []); 44 | return; 45 | } 46 | callback(null, [ 47 | { 48 | name: '$mtEventCallBack', 49 | value: '$mtEventCallBack(type,$item_info.id)', 50 | score: 1000, 51 | meta: '执行订阅回调函数' 52 | }, 53 | { 54 | name: '$mtElMessage', 55 | value: '$mtElMessage.success("成功")', 56 | score: 1000, 57 | meta: '消息提示' 58 | }, 59 | { 60 | name: '$mtElMessageBox', 61 | value: `$mtElMessageBox.alert('This is a message', 'Title', { 62 | confirmButtonText: 'OK', 63 | callback: (action) => { 64 | console.log(action) 65 | }, 66 | })`, 67 | score: 1000, 68 | meta: '消息弹出框' 69 | }, 70 | { 71 | name: '$item_info', 72 | value: '$item_info', 73 | score: 1000, 74 | meta: '回调函数中获取当前触发事件图形的信息' 75 | } 76 | ]); 77 | } 78 | }); 79 | -------------------------------------------------------------------------------- /src/components/mt-edit/store/global.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue'; 2 | import type { 3 | GlobalStoreIntention, 4 | IDoneJson, 5 | IGlobalStore, 6 | IGlobalStoreCreateItemInfo, 7 | IRealTimeData 8 | } from './types'; 9 | export const globalStore: IGlobalStore = reactive({ 10 | intention: 'none', 11 | create_item_info: null, 12 | selected_items_id: [], 13 | done_json: [], 14 | canvasCfg: { 15 | width: 1920, 16 | height: 1080, 17 | scale: 1, 18 | color: '', 19 | img: '', 20 | guide: true, 21 | adsorp: true, 22 | adsorp_diff: 5, 23 | transform_origin: { 24 | x: 0, 25 | y: 0 26 | }, 27 | drag_offset: { 28 | x: 0, 29 | y: 0 30 | } 31 | }, 32 | gridCfg: { 33 | enabled: true, 34 | align: true, 35 | size: 10 36 | }, 37 | guideCfg: { 38 | x: { 39 | display: false, 40 | top: 0 41 | }, 42 | y: { 43 | display: false, 44 | left: 0 45 | } 46 | }, 47 | lock: false, 48 | real_time_data: { 49 | show: false, 50 | text: '' 51 | }, 52 | adsorp_diff: { 53 | x: 0, 54 | y: 0 55 | }, 56 | setIntention: (val: GlobalStoreIntention) => { 57 | globalStore.intention = val; 58 | }, 59 | setCreateItemInfo: (val: IGlobalStoreCreateItemInfo | null) => { 60 | globalStore.create_item_info = val; 61 | }, 62 | setGlobalStoreDoneJson: (val: IDoneJson[]) => { 63 | globalStore.done_json = val; 64 | }, 65 | //取消所有组件选中 66 | cancelAllSelect: () => { 67 | const done_json_temp = [...globalStore.done_json].map((m) => { 68 | if (m.active) { 69 | m.active = false; 70 | } 71 | return m; 72 | }); 73 | globalStore.setGlobalStoreDoneJson(done_json_temp); 74 | globalStore.selected_items_id = []; 75 | }, 76 | //刷新选中的id 77 | refreshSelectedItemsId: () => { 78 | globalStore.selected_items_id = globalStore.done_json.filter((m) => m.active).map((m) => m.id); 79 | }, 80 | //删除选中的组件 81 | deleteSelectedItems: () => { 82 | const done_json_temp = [...globalStore.done_json].filter( 83 | (m) => !globalStore.selected_items_id.includes(m.id) 84 | ); 85 | globalStore.setGlobalStoreDoneJson(done_json_temp); 86 | globalStore.selected_items_id = []; 87 | }, 88 | // 设置单个选中 89 | setSingleSelect: (id: string) => { 90 | globalStore.done_json.forEach((m) => { 91 | if (m.id === id) { 92 | m.active = true; 93 | } else { 94 | m.active = false; 95 | } 96 | }); 97 | globalStore.selected_items_id = [id]; 98 | }, 99 | setSelectItems: (ids: string[]) => { 100 | globalStore.done_json.forEach((m) => { 101 | if (ids.includes(m.id)) { 102 | m.active = true; 103 | } else { 104 | m.active = false; 105 | } 106 | }); 107 | globalStore.selected_items_id = ids; 108 | }, 109 | setRealTimeData: (val: IRealTimeData) => { 110 | globalStore.real_time_data = val; 111 | } 112 | }); 113 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/selected-area/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 83 | 95 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/layout/right-aside/select-item-animate-setting/index.vue: -------------------------------------------------------------------------------- 1 | 34 | 92 | -------------------------------------------------------------------------------- /src/views/demo/event-callback.vue: -------------------------------------------------------------------------------- 1 | 10 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/assets/css-vars.css: -------------------------------------------------------------------------------- 1 | #mt-edit.dark { 2 | color-scheme: dark; 3 | --el-color-primary: #409eff; 4 | --el-color-primary-light-3: #3375b9; 5 | --el-color-primary-light-5: #2a598a; 6 | --el-color-primary-light-7: #213d5b; 7 | --el-color-primary-light-8: #1d3043; 8 | --el-color-primary-light-9: #18222c; 9 | --el-color-primary-dark-2: #66b1ff; 10 | --el-color-success: #67c23a; 11 | --el-color-success-light-3: #4e8e2f; 12 | --el-color-success-light-5: #3e6b27; 13 | --el-color-success-light-7: #2d481f; 14 | --el-color-success-light-8: #25371c; 15 | --el-color-success-light-9: #1c2518; 16 | --el-color-success-dark-2: #85ce61; 17 | --el-color-warning: #e6a23c; 18 | --el-color-warning-light-3: #a77730; 19 | --el-color-warning-light-5: #7d5b28; 20 | --el-color-warning-light-7: #533f20; 21 | --el-color-warning-light-8: #3e301c; 22 | --el-color-warning-light-9: #292218; 23 | --el-color-warning-dark-2: #ebb563; 24 | --el-color-danger: #f56c6c; 25 | --el-color-danger-light-3: #b25252; 26 | --el-color-danger-light-5: #854040; 27 | --el-color-danger-light-7: #582e2e; 28 | --el-color-danger-light-8: #412626; 29 | --el-color-danger-light-9: #2b1d1d; 30 | --el-color-danger-dark-2: #f78989; 31 | --el-color-error: #f56c6c; 32 | --el-color-error-light-3: #b25252; 33 | --el-color-error-light-5: #854040; 34 | --el-color-error-light-7: #582e2e; 35 | --el-color-error-light-8: #412626; 36 | --el-color-error-light-9: #2b1d1d; 37 | --el-color-error-dark-2: #f78989; 38 | --el-color-info: #909399; 39 | --el-color-info-light-3: #6b6d71; 40 | --el-color-info-light-5: #525457; 41 | --el-color-info-light-7: #393a3c; 42 | --el-color-info-light-8: #2d2d2f; 43 | --el-color-info-light-9: #202121; 44 | --el-color-info-dark-2: #a6a9ad; 45 | --el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.36), 0px 8px 20px rgba(0, 0, 0, 0.72); 46 | --el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.72); 47 | --el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.72); 48 | --el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.72), 0px 12px 32px #000000, 49 | 0px 8px 16px -8px #000000; 50 | --el-bg-color-page: #0a0a0a; 51 | --el-bg-color: #141414; 52 | --el-bg-color-overlay: #1d1e1f; 53 | --el-text-color-primary: #e5eaf3; 54 | --el-text-color-regular: #cfd3dc; 55 | --el-text-color-secondary: #a3a6ad; 56 | --el-text-color-placeholder: #8d9095; 57 | --el-text-color-disabled: #6c6e72; 58 | --el-border-color-darker: #636466; 59 | --el-border-color-dark: #58585b; 60 | --el-border-color: #4c4d4f; 61 | --el-border-color-light: #414243; 62 | --el-border-color-lighter: #363637; 63 | --el-border-color-extra-light: #2b2b2c; 64 | --el-fill-color-darker: #424243; 65 | --el-fill-color-dark: #39393a; 66 | --el-fill-color: #303030; 67 | --el-fill-color-light: #262727; 68 | --el-fill-color-lighter: #1d1d1d; 69 | --el-fill-color-extra-light: #191919; 70 | --el-fill-color-blank: transparent; 71 | --el-mask-color: rgba(0, 0, 0, 0.8); 72 | --el-mask-color-extra-light: rgba(0, 0, 0, 0.3); 73 | transition: all 5s; 74 | } 75 | #mt-edit.dark .el-button { 76 | --el-button-disabled-text-color: rgba(255, 255, 255, 0.5); 77 | } 78 | #mt-edit.dark .el-card { 79 | --el-card-bg-color: var(--el-bg-color-overlay); 80 | } 81 | #mt-edit.dark .el-empty { 82 | --el-empty-fill-color-0: var(--el-color-black); 83 | --el-empty-fill-color-1: #4b4b52; 84 | --el-empty-fill-color-2: #36383d; 85 | --el-empty-fill-color-3: #1e1e20; 86 | --el-empty-fill-color-4: #262629; 87 | --el-empty-fill-color-5: #202124; 88 | --el-empty-fill-color-6: #212224; 89 | --el-empty-fill-color-7: #1b1c1f; 90 | --el-empty-fill-color-8: #1c1d1f; 91 | --el-empty-fill-color-9: #18181a; 92 | } 93 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/render-item/index.vue: -------------------------------------------------------------------------------- 1 | 53 | 101 | 102 | -------------------------------------------------------------------------------- /src/components/mt-dzr/components/rotate-handle.vue: -------------------------------------------------------------------------------- 1 | 11 | 77 | 89 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 191 | 192 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /src/views/demo/change-attr.vue: -------------------------------------------------------------------------------- 1 | 10 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /src/components/mt-preview/index.vue: -------------------------------------------------------------------------------- 1 | 36 | 172 | 183 | -------------------------------------------------------------------------------- /src/components/mt-dzr/components/resize-handle.vue: -------------------------------------------------------------------------------- 1 | 14 | 180 | 197 | -------------------------------------------------------------------------------- /src/views/demo/edit-load.vue: -------------------------------------------------------------------------------- 1 | 6 | 258 | 259 | 260 | -------------------------------------------------------------------------------- /src/components/mt-edit/store/types.ts: -------------------------------------------------------------------------------- 1 | export type ILeftAsideConfig = Map; 2 | export type ILeftAsideConfigItemPublicPropsType = 3 | | 'input' 4 | | 'color' 5 | | 'select' 6 | | 'switch' 7 | | 'number' 8 | | 'jsonEdit' 9 | | 'textArea'; 10 | // 开放注册配置 11 | export type ILeftAsideConfigItemPublicProps = Record< 12 | string, 13 | { 14 | title: string; //显示在属性面板的标题 15 | type: ILeftAsideConfigItemPublicPropsType; //属性的类型决定了修改属性的方式 16 | val: any; 17 | options?: any; //比如说修改属性的时候用到了下拉框,这里面就可以放下拉框的选项 18 | disabled?: boolean; //如果禁用了将不会显示到右侧属性面板里,但是仍然可以通过代码修改属性 19 | } 20 | >; 21 | export type ILeftAsideConfigItemPublicType = 'svg' | 'vue' | 'img' | 'custom-svg'; 22 | export type ILeftAsideConfigItemPrivateType = 'group' | 'sys-line'; 23 | export interface ILeftAsideConfigItemPublic { 24 | id: string; //图形的标识 值必须唯一 25 | title: string; //要显示的标题,一般用中文表示 26 | type: ILeftAsideConfigItemPublicType | ILeftAsideConfigItemPrivateType; //图形的类型 27 | thumbnail: string; //显示到左侧时候的缩略图 28 | svg?: string; //图形的svg代码 29 | props: ILeftAsideConfigItemPublicProps; 30 | } 31 | export interface ILeftAsideConfigItemPrivateSymbol { 32 | symbol_id: string; 33 | symbol_str: string; 34 | width: string; 35 | height: string; 36 | } 37 | export interface ICommonAnimations { 38 | val: string; 39 | delay: string; 40 | speed: string; 41 | repeat: string; 42 | } 43 | export interface ILeftAsideConfigItemPrivate { 44 | symbol?: ILeftAsideConfigItemPrivateSymbol; 45 | common_animations: ICommonAnimations; 46 | } 47 | export type ILeftAsideConfigItem = ILeftAsideConfigItemPublic & ILeftAsideConfigItemPrivate; 48 | 49 | export type GlobalStoreIntention = 50 | | 'none' 51 | | 'create' 52 | | 'beginMulSelect' 53 | | 'adsorbStart' 54 | | 'adsorbEnd' 55 | | 'beginDragCanvas' 56 | | 'runDragCanvas' 57 | | 'endDragCanvas' 58 | | 'showContextMenu' 59 | | 'drawSysLineStart'; 60 | export interface IGlobalStoreCreateItemInfo { 61 | config_key: string; //也就是折叠面板的值 62 | item_id: string; //要创建组件的id 63 | } 64 | 65 | export interface IGlobalStoreCanvasCfg { 66 | width: number; 67 | height: number; 68 | scale: number; 69 | color: string; 70 | img: string; 71 | guide: boolean; //参考线 72 | adsorp: boolean; //吸附 73 | adsorp_diff: number; 74 | // 缩放中心 75 | transform_origin: { 76 | x: number; 77 | y: number; 78 | }; 79 | // 拖动偏移量 80 | drag_offset: { 81 | x: number; 82 | y: number; 83 | }; 84 | } 85 | export interface IGlobalStoreGridCfg { 86 | enabled: boolean; 87 | align: boolean; 88 | size: number; 89 | } 90 | export type DoneJsonEventListType = 'click' | 'dblclick' | 'mouseover' | 'mouseout'; 91 | export type DoneJsonEventListAction = 'changeAttr' | 'customCode'; 92 | export interface IDoneJsonActionChangeAttr { 93 | id: string; 94 | target_id: string; 95 | target_attr: string | undefined; 96 | target_value: any; 97 | } 98 | export interface IDoneJsonEventList { 99 | id: string; 100 | type: DoneJsonEventListType; // 事件类型 101 | action: DoneJsonEventListAction; // 事件行为 102 | change_attr: IDoneJsonActionChangeAttr[]; //属性更改 103 | custom_code: string; 104 | trigger_rule: { 105 | trigger_id?: string; //触发图形的id 106 | trigger_attr?: string; //触发图形的属性 107 | operator?: string; //运算符 108 | value?: any; //期望值 109 | }; 110 | } 111 | export interface IDoneJson { 112 | id: string; //必须唯一 113 | title: string; //标题 114 | type: ILeftAsideConfigItemPublicType | ILeftAsideConfigItemPrivateType; //类型 由配置文件决定 115 | symbol?: ILeftAsideConfigItemPrivateSymbol; //类型是svg的时候需要用这个将svg转换成symbol 116 | binfo: IDoneJsonBinfo; 117 | props: ILeftAsideConfigItemPublicProps; 118 | resize: boolean; //开启缩放 119 | rotate: boolean; //开启旋转 120 | lock: boolean; //锁定 121 | active: boolean; //激活 122 | hide: boolean; //隐藏 123 | common_animations: ICommonAnimations; //通用动画 124 | use_proportional_scaling?: boolean; //使用等比缩放 125 | children?: IDoneJson[]; 126 | tag?: string; 127 | thumbnail?: string; 128 | events: IDoneJsonEventList[]; 129 | } 130 | //图形边界信息 131 | export interface IDoneJsonBinfo { 132 | left: number; 133 | top: number; 134 | width: number; 135 | height: number; 136 | angle: number; 137 | } 138 | export interface CacheBoundingBox { 139 | id: string; 140 | type: ILeftAsideConfigItemPublicType | ILeftAsideConfigItemPrivateType; 141 | left: number; 142 | top: number; 143 | width: number; 144 | height: number; 145 | bottom: number; 146 | right: number; 147 | } 148 | export type AdsorbPointType = 'tc' | 'bc' | 'lc' | 'rc'; 149 | export interface IContextMenuInfo { 150 | title: string; 151 | hot_key: string; 152 | enable: boolean; 153 | } 154 | export type ContextMenuInfoType = 155 | | 'copy' 156 | | 'paste' 157 | | 'delete' 158 | | 'group' 159 | | 'ungroup' 160 | | 'selectAll' 161 | | 'moveTop' 162 | | 'moveUp' 163 | | 'moveDown' 164 | | 'moveBottom'; 165 | export interface IContextMenuDetail { 166 | left: number; 167 | top: number; 168 | info: { 169 | [key in ContextMenuInfoType]: IContextMenuInfo; 170 | }; 171 | } 172 | export interface IRealTimeData { 173 | show: boolean; 174 | text: string; 175 | } 176 | // 全局状态 177 | export interface IGlobalStore { 178 | intention: GlobalStoreIntention; 179 | create_item_info: IGlobalStoreCreateItemInfo | null; 180 | done_json: IDoneJson[]; 181 | selected_items_id: string[]; 182 | canvasCfg: IGlobalStoreCanvasCfg; 183 | gridCfg: IGlobalStoreGridCfg; 184 | guideCfg: { 185 | x: { 186 | display: boolean; 187 | top: number; 188 | }; 189 | y: { 190 | display: boolean; 191 | left: number; 192 | }; 193 | }; 194 | lock: boolean; 195 | real_time_data: IRealTimeData; 196 | adsorp_diff: { 197 | x: number; 198 | y: number; 199 | }; 200 | setIntention: (val: GlobalStoreIntention) => void; 201 | setCreateItemInfo: (val: IGlobalStoreCreateItemInfo | null) => void; 202 | setGlobalStoreDoneJson: (val: IDoneJson[]) => void; 203 | cancelAllSelect: () => void; 204 | refreshSelectedItemsId: () => void; 205 | deleteSelectedItems: () => void; 206 | setSingleSelect: (id: string) => void; 207 | setSelectItems: (ids: string[]) => void; 208 | setRealTimeData: (val: IRealTimeData) => void; 209 | } 210 | // 左侧配置 211 | export interface ILeftAside { 212 | config: ILeftAsideConfig; 213 | registerConfig: (title: string, config: ILeftAsideConfigItemPublic[]) => void; 214 | } 215 | // 缓存配置 216 | export interface ICache { 217 | boundingBox: CacheBoundingBox[]; 218 | setBoundingBox: (val: CacheBoundingBox[]) => void; 219 | adsorbPoint: { type: AdsorbPointType; x: number; y: number; id: string }[]; 220 | setAdsorbPoint: (val: { type: AdsorbPointType; x: number; y: number; id: string }[]) => void; 221 | copy: IDoneJson[]; 222 | setCopy: (val: IDoneJson[]) => void; 223 | history: IDoneJson[][]; 224 | historyIndex: number; 225 | addHistory: (done_json: IDoneJson[]) => void; 226 | } 227 | // 杂项配置 228 | export interface IConfig { 229 | sysComponent: ILeftAsideConfigItem[]; 230 | lineRenderOffset: number; //因为连线是使用svg进行渲染的,所以需要一个偏移量和div的画布进行重叠 231 | } 232 | /** 233 | * 右键菜单 234 | */ 235 | export interface IContextMenu { 236 | menuInfo: IContextMenuDetail; 237 | setMenuInfo: (val: IContextMenuDetail) => void; 238 | setDisplayItem: (val: ContextMenuInfoType[]) => void; 239 | } 240 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/layout/right-aside/select-item-animate-setting/common-animate.vue: -------------------------------------------------------------------------------- 1 | 57 | 172 | 193 | -------------------------------------------------------------------------------- /src/components/mt-edit/components/draw-line-render/index.vue: -------------------------------------------------------------------------------- 1 | 100 | 204 | 205 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /src/components/mt-edit/composables/sys-line.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'vue'; 2 | import type { IDoneJson, IDoneJsonBinfo } from '../store/types'; 3 | import { getRectCenterCoordinate, getRectCoordinate, rotatePoint } from '../utils'; 4 | 5 | /** 6 | * 更新系统连线实际宽高 7 | * @param sys_lines 8 | * @param scale 9 | */ 10 | export const useUpdateSysLineRect = ( 11 | sys_lines: IDoneJson[], 12 | canvasDom: HTMLElement, 13 | scale: number 14 | ) => { 15 | sys_lines.forEach((f) => { 16 | const itemRect = document.querySelector(`#${f.id} g .real`)!.getBoundingClientRect(); 17 | const canvas_area_bounding_info = canvasDom!.getBoundingClientRect(); 18 | const new_left = (itemRect?.left - canvas_area_bounding_info?.left) / scale; 19 | const new_top = (itemRect?.top - canvas_area_bounding_info?.top) / scale; 20 | const move_x = new_left - f.binfo.left; 21 | const move_y = new_top - f.binfo.top; 22 | f.binfo.left = new_left; 23 | f.binfo.top = new_top; 24 | f.binfo.width = itemRect?.width / scale; 25 | f.binfo.height = itemRect?.height / scale; 26 | f.props.point_position = { 27 | ...f.props.point_position, 28 | val: f.props.point_position.val.map((m: { x: number; y: number }) => { 29 | return { 30 | x: m.x - move_x, 31 | y: m.y - move_y 32 | }; 33 | }) 34 | }; 35 | }); 36 | }; 37 | /** 38 | * 更新系统连线 39 | * @param sys_lines 要更新的连线列表 40 | * @param done_json 所有组件信息 41 | * @param canvasDom 画布dom 42 | * @param scale 画布缩放 43 | */ 44 | export const useUpdateSysLine = ( 45 | sys_lines: IDoneJson[], 46 | done_json: IDoneJson[], 47 | canvasDom: HTMLElement, 48 | scale: number, 49 | move_binfo?: IDoneJsonBinfo & { id: string } 50 | ) => { 51 | const temp_done_json = [...done_json]; 52 | sys_lines.forEach((f) => { 53 | if (!f.props.bind_anchors.val.start && !f.props.bind_anchors.val.end) { 54 | return; 55 | } 56 | const itemRect = document.querySelector(`#${f.id} g .real`)!.getBoundingClientRect(); 57 | const canvas_area_bounding_info = canvasDom!.getBoundingClientRect(); 58 | const new_left = (itemRect?.left - canvas_area_bounding_info?.left) / scale; 59 | const new_top = (itemRect?.top - canvas_area_bounding_info?.top) / scale; 60 | 61 | // 处理起点绑定 62 | if (f.props.bind_anchors.val.start) { 63 | // 根据id和类型找到锚点坐标 64 | const find_item = temp_done_json.find((m) => m.id === f.props.bind_anchors.val.start.id); 65 | if (find_item) { 66 | const b_info = find_item.id === move_binfo?.id ? move_binfo : find_item.binfo; 67 | // 四个角原始坐标 68 | const { topLeft, topRight, bottomLeft, bottomRight } = getRectCoordinate(b_info); 69 | // 四条边中点坐标 70 | const { topCenter, bottomCenter, leftCenter, rightCenter } = getRectCenterCoordinate( 71 | topLeft, 72 | topRight, 73 | bottomLeft, 74 | bottomRight 75 | ); 76 | // 旋转中心 77 | const centerX = topCenter.x; 78 | const centerY = leftCenter.y; 79 | 80 | // 旋转角度(弧度) 81 | const angleRad = (Math.PI / 180) * find_item.binfo.angle; 82 | 83 | if (f.props.bind_anchors.val.start.type === 'tc') { 84 | const new_tc = rotatePoint(topCenter.x, topCenter.y, centerX, centerY, angleRad); 85 | f.props.point_position.val[0] = { 86 | x: new_tc.x - f.binfo.left, 87 | y: new_tc.y - f.binfo.top 88 | }; 89 | } else if (f.props.bind_anchors.val.start.type === 'bc') { 90 | const new_bc = rotatePoint(bottomCenter.x, bottomCenter.y, centerX, centerY, angleRad); 91 | f.props.point_position.val[0] = { 92 | x: new_bc.x - f.binfo.left, 93 | y: new_bc.y - f.binfo.top 94 | }; 95 | } else if (f.props.bind_anchors.val.start.type === 'lc') { 96 | const new_lc = rotatePoint(leftCenter.x, leftCenter.y, centerX, centerY, angleRad); 97 | f.props.point_position.val[0] = { 98 | x: new_lc.x - f.binfo.left, 99 | y: new_lc.y - f.binfo.top 100 | }; 101 | } else if (f.props.bind_anchors.val.start.type === 'rc') { 102 | const new_rc = rotatePoint(rightCenter.x, rightCenter.y, centerX, centerY, angleRad); 103 | f.props.point_position.val[0] = { 104 | x: new_rc.x - f.binfo.left, 105 | y: new_rc.y - f.binfo.top 106 | }; 107 | } 108 | const move_x = new_left - f.binfo.left; 109 | const move_y = new_top - f.binfo.top; 110 | f.binfo = { 111 | ...f.binfo, 112 | left: new_left, 113 | top: new_top, 114 | width: itemRect?.width / scale, 115 | height: itemRect?.height / scale 116 | }; 117 | f.props.point_position = { 118 | ...f.props.point_position, 119 | val: f.props.point_position.val.map((m: { x: number; y: number }) => { 120 | return { 121 | x: m.x - move_x, 122 | y: m.y - move_y 123 | }; 124 | }) 125 | }; 126 | } else { 127 | f.props.bind_anchors.val.start = null; 128 | } 129 | } 130 | // 处理终点绑定 131 | if (f.props.bind_anchors.val.end) { 132 | // 根据id和类型找到锚点坐标 133 | const find_item = temp_done_json.find((m) => m.id === f.props.bind_anchors.val.end.id); 134 | if (find_item) { 135 | const b_info = find_item.id === move_binfo?.id ? move_binfo : find_item.binfo; 136 | // 四个角原始坐标 137 | const { topLeft, topRight, bottomLeft, bottomRight } = getRectCoordinate(b_info); 138 | // 四条边中点坐标 139 | const { topCenter, bottomCenter, leftCenter, rightCenter } = getRectCenterCoordinate( 140 | topLeft, 141 | topRight, 142 | bottomLeft, 143 | bottomRight 144 | ); 145 | // 旋转中心 146 | const centerX = topCenter.x; 147 | const centerY = leftCenter.y; 148 | 149 | // 旋转角度(弧度) 150 | const angleRad = (Math.PI / 180) * find_item.binfo.angle; 151 | 152 | if (f.props.bind_anchors.val.end.type === 'tc') { 153 | const new_tc = rotatePoint(topCenter.x, topCenter.y, centerX, centerY, angleRad); 154 | f.props.point_position.val[f.props.point_position.val.length - 1] = { 155 | x: new_tc.x - f.binfo.left, 156 | y: new_tc.y - f.binfo.top 157 | }; 158 | } else if (f.props.bind_anchors.val.end.type === 'bc') { 159 | const new_bc = rotatePoint(bottomCenter.x, bottomCenter.y, centerX, centerY, angleRad); 160 | f.props.point_position.val[f.props.point_position.val.length - 1] = { 161 | x: new_bc.x - f.binfo.left, 162 | y: new_bc.y - f.binfo.top 163 | }; 164 | } else if (f.props.bind_anchors.val.end.type === 'lc') { 165 | const new_lc = rotatePoint(leftCenter.x, leftCenter.y, centerX, centerY, angleRad); 166 | f.props.point_position.val[f.props.point_position.val.length - 1] = { 167 | x: new_lc.x - f.binfo.left, 168 | y: new_lc.y - f.binfo.top 169 | }; 170 | } else if (f.props.bind_anchors.val.end.type === 'rc') { 171 | const new_rc = rotatePoint(rightCenter.x, rightCenter.y, centerX, centerY, angleRad); 172 | f.props.point_position.val[f.props.point_position.val.length - 1] = { 173 | x: new_rc.x - f.binfo.left, 174 | y: new_rc.y - f.binfo.top 175 | }; 176 | } 177 | const move_x = new_left - f.binfo.left; 178 | const move_y = new_top - f.binfo.top; 179 | f.binfo = { 180 | ...f.binfo, 181 | left: new_left, 182 | top: new_top, 183 | width: itemRect?.width / scale, 184 | height: itemRect?.height / scale 185 | }; 186 | f.props.point_position = { 187 | ...f.props.point_position, 188 | val: f.props.point_position.val.map((m: { x: number; y: number }) => { 189 | return { 190 | x: m.x - move_x, 191 | y: m.y - move_y 192 | }; 193 | }) 194 | }; 195 | } else { 196 | f.props.bind_anchors.val.end = null; 197 | } 198 | } 199 | }); 200 | // 直接写在这里会损失一部分性能 也可以注释掉下面的 然后根据需求在useUpdateSysLine之后手动调用useUpdateSysLineRect 201 | nextTick(() => { 202 | useUpdateSysLineRect(sys_lines, canvasDom, scale); 203 | }); 204 | }; 205 | --------------------------------------------------------------------------------