├── env.d.ts ├── docs └── example.png ├── public ├── favicon.ico └── profile.xml ├── .prettierrc.json ├── .editorconfig ├── .vscode └── extensions.json ├── src ├── assets │ ├── main.css │ └── base.css ├── main.ts ├── ProfileEditor │ ├── node │ │ ├── index.ts │ │ ├── operationNode.ts │ │ ├── instrumentNode.ts │ │ ├── modelNode.ts │ │ ├── configNode.ts │ │ ├── OperationNode.vue │ │ ├── InstrumentNode.vue │ │ ├── ModelNode.vue │ │ └── ConfigNode.vue │ ├── assets │ │ ├── rotate-ccw.svg │ │ ├── database.svg │ │ ├── zoom-out.svg │ │ ├── download.svg │ │ ├── import.svg │ │ ├── computer.svg │ │ ├── maximize.svg │ │ ├── minimize.svg │ │ ├── zoom-in.svg │ │ ├── bolt.svg │ │ └── zap.svg │ ├── icons │ │ ├── ArrowLeft.vue │ │ └── CirclePlus.vue │ ├── components │ │ ├── drawer │ │ │ ├── InstrumentNodeForm.vue │ │ │ ├── CustomOperationNodeForm.vue │ │ │ ├── ModelNodeForm.vue │ │ │ ├── FunctionOperationNodeForm.vue │ │ │ ├── NiVisaOperationNodeForm.vue │ │ │ ├── MethodEditor │ │ │ │ ├── ParameterForm.vue │ │ │ │ └── MethodEditor.vue │ │ │ ├── MeasureModeEditor │ │ │ │ ├── ByteStreamForm.vue │ │ │ │ ├── WorkPlaceList.vue │ │ │ │ └── MeasureModeEditor.vue │ │ │ ├── ConfigNodeForm.vue │ │ │ └── NodeEditDrawer.vue │ │ └── ControlPanel.vue │ ├── utils │ │ ├── inital.ts │ │ └── adaptor.ts │ ├── common.ts │ ├── types.ts │ └── ProfileEditor.vue └── App.vue ├── tsconfig.json ├── index.html ├── tsconfig.app.json ├── README.md ├── .gitignore ├── vite.config.ts ├── tsconfig.node.json ├── eslint.config.js └── package.json /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /docs/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeSuger/instrument-profile-editor/HEAD/docs/example.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeSuger/instrument-profile-editor/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "$schema": "https://json.schemastore.org/prettierrc", 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}] 2 | charset = utf-8 3 | indent_size = 2 4 | indent_style = space 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "dbaeumer.vscode-eslint", 5 | "EditorConfig.EditorConfig", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | #app { 4 | width: 100vw; 5 | height: 100vh; 6 | margin: 0; 7 | padding: 0; 8 | overflow: hidden; 9 | font-weight: normal; 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './assets/main.css'; 2 | 3 | import { createApp } from 'vue'; 4 | 5 | import ArcoVue from '@arco-design/web-vue'; 6 | import App from './App.vue'; 7 | 8 | import '@arco-design/web-vue/dist/arco.css'; 9 | 10 | createApp(App).use(ArcoVue).mount('#app'); 11 | -------------------------------------------------------------------------------- /src/ProfileEditor/node/index.ts: -------------------------------------------------------------------------------- 1 | import InstrumentNode from "./instrumentNode"; 2 | import ModelNode from "./modelNode"; 3 | import ConfigNode from "./configNode"; 4 | import OperationNode from "./operationNode"; 5 | 6 | export { 7 | InstrumentNode, 8 | ModelNode, 9 | ConfigNode, 10 | OperationNode 11 | }; -------------------------------------------------------------------------------- /src/ProfileEditor/assets/rotate-ccw.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ProfileEditor/assets/database.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ProfileEditor/assets/zoom-out.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ProfileEditor/assets/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ProfileEditor/assets/import.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ProfileEditor/assets/computer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ProfileEditor/assets/maximize.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ProfileEditor/assets/minimize.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ProfileEditor/assets/zoom-in.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ProfileEditor/assets/bolt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ProfileEditor/assets/zap.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Instrument Profile Editor 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 8 | 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": ["./src/*"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # INSTRUMENT-PROFILE-EDITOR 2 | 3 | 基于 Vue3 + [LogicFlow](https://github.com/didi/LogicFlow) 实现的仪器配置文件编辑器。 4 | 5 | ![example.png](./docs/example.png) 6 | 7 | ## 本地开发 8 | 9 | ```bash 10 | # 安装依赖 11 | npm install 12 | 13 | # 启动本地服务 14 | npm run dev 15 | ``` 16 | 17 | ## 物料来源 18 | 19 | | 物料 | 来源 | 20 | | --- | --- | 21 | | Icon | [Lucide](https://lucide.dev/) | 22 | | 组件库 | [Arco Vue](https://arco.design/vue/docs/start) | 23 | -------------------------------------------------------------------------------- /src/ProfileEditor/icons/ArrowLeft.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.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 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | *.tsbuildinfo 31 | -------------------------------------------------------------------------------- /src/ProfileEditor/icons/CirclePlus.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import vueDevTools from 'vite-plugin-vue-devtools' 6 | 7 | // https://vite.dev/config/ 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | vueDevTools(), 12 | ], 13 | resolve: { 14 | alias: { 15 | '@': fileURLToPath(new URL('./src', import.meta.url)) 16 | }, 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/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 | "noEmit": true, 13 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 14 | 15 | "module": "ESNext", 16 | "moduleResolution": "Bundler", 17 | "types": ["node"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import pluginVue from 'eslint-plugin-vue'; 2 | import vueTsEslintConfig from '@vue/eslint-config-typescript'; 3 | import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'; 4 | 5 | export default [ 6 | { 7 | name: 'app/files-to-lint', 8 | files: ['**/*.{ts,mts,tsx,vue}'], 9 | }, 10 | 11 | { 12 | name: 'app/files-to-ignore', 13 | ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'], 14 | }, 15 | 16 | ...pluginVue.configs['flat/essential'], 17 | ...vueTsEslintConfig(), 18 | skipFormatting, 19 | { 20 | rules: { 21 | 'semi': 2, 22 | 'vue/no-mutating-props': 0, 23 | } 24 | } 25 | ]; 26 | -------------------------------------------------------------------------------- /src/ProfileEditor/components/drawer/InstrumentNodeForm.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/ProfileEditor/components/drawer/CustomOperationNodeForm.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 40 | 41 | 48 | -------------------------------------------------------------------------------- /src/ProfileEditor/utils/inital.ts: -------------------------------------------------------------------------------- 1 | import type { InstrumentNodeData, ModelNodeData, ConfigNodeData, OperationNodeData } from '../types'; 2 | 3 | type FlodedData = { isFloded: boolean }; 4 | 5 | export function initInstrumentNodeData(): InstrumentNodeData { 6 | return { 7 | id: '', 8 | }; 9 | } 10 | 11 | export function initModelNodeData(): ModelNodeData & FlodedData { 12 | return { 13 | isFloded: false, 14 | id: '', 15 | configType: 'NI-VISA', 16 | }; 17 | } 18 | 19 | export function initConfigNodeData(): ConfigNodeData & FlodedData { 20 | return { 21 | isFloded: false, 22 | id: 'NI-VISA', 23 | spaceName: '', 24 | className: '', 25 | dllTemplate: '', 26 | isVisa: 'false', 27 | communicationType: 'RS232', 28 | communicationConfig: { 29 | baudRate: '9600', 30 | dataBits: '8', 31 | stopBits: '1', 32 | parity: '0', 33 | bufferBytes: '0', 34 | handShake: '0', 35 | timeout: '2000', 36 | ip: '', 37 | port: '', 38 | }, 39 | }; 40 | } 41 | 42 | export function initOperationNodeData(): OperationNodeData { 43 | return { 44 | id: '', 45 | parameter: '', 46 | hasReturn: 'false', 47 | command: '', 48 | methods: [], 49 | measureModes: [], 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/ProfileEditor/components/drawer/ModelNodeForm.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "profile-editor", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "run-p type-check \"build-only {@}\" --", 9 | "preview": "vite preview", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --build --force", 12 | "lint": "eslint . --fix", 13 | "format": "prettier --write src/" 14 | }, 15 | "dependencies": { 16 | "@logicflow/core": "^2.0.7", 17 | "@logicflow/extension": "^2.0.11", 18 | "events": "^3.3.0", 19 | "lodash-es": "^4.17.21", 20 | "sass": "^1.69.7", 21 | "uuid": "^11.0.3", 22 | "vue": "^3.5.12", 23 | "xml2js": "^0.6.2" 24 | }, 25 | "devDependencies": { 26 | "@arco-design/web-vue": "^2.54.5", 27 | "@tsconfig/node22": "^22.0.0", 28 | "@types/lodash-es": "^4.17.12", 29 | "@types/node": "^22.9.0", 30 | "@types/xml2js": "^0.4.14", 31 | "@vitejs/plugin-vue": "^5.1.4", 32 | "@vue/eslint-config-prettier": "^10.1.0", 33 | "@vue/eslint-config-typescript": "^14.1.3", 34 | "@vue/tsconfig": "^0.5.1", 35 | "eslint": "^9.14.0", 36 | "eslint-plugin-vue": "^9.30.0", 37 | "npm-run-all2": "^7.0.1", 38 | "prettier": "^3.3.3", 39 | "typescript": "~5.6.3", 40 | "vite": "^5.4.10", 41 | "vite-plugin-vue-devtools": "^7.5.4", 42 | "vue-tsc": "^2.1.10" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ProfileEditor/components/drawer/FunctionOperationNodeForm.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/ProfileEditor/components/drawer/NiVisaOperationNodeForm.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/ProfileEditor/components/drawer/MethodEditor/ParameterForm.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 69 | 70 | -------------------------------------------------------------------------------- /src/ProfileEditor/components/drawer/MeasureModeEditor/ByteStreamForm.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 73 | 74 | -------------------------------------------------------------------------------- /src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | font-weight: normal; 59 | } 60 | 61 | body { 62 | min-height: 100vh; 63 | color: var(--color-text); 64 | background: var(--color-background); 65 | transition: 66 | color 0.5s, 67 | background-color 0.5s; 68 | line-height: 1.6; 69 | font-family: 70 | Inter, 71 | -apple-system, 72 | BlinkMacSystemFont, 73 | 'Segoe UI', 74 | Roboto, 75 | Oxygen, 76 | Ubuntu, 77 | Cantarell, 78 | 'Fira Sans', 79 | 'Droid Sans', 80 | 'Helvetica Neue', 81 | sans-serif; 82 | font-size: 15px; 83 | text-rendering: optimizeLegibility; 84 | -webkit-font-smoothing: antialiased; 85 | -moz-osx-font-smoothing: grayscale; 86 | } 87 | -------------------------------------------------------------------------------- /src/ProfileEditor/common.ts: -------------------------------------------------------------------------------- 1 | export enum NodeType { 2 | Instrument = 'Instrument', 3 | Model = 'Model', 4 | Config = 'Config', 5 | NI_VISA_OPERATION = 'NI-VISA_OPERATION', 6 | FUNCTION_OPERATION = 'FUNCTION_OPERATION', 7 | CUSTOM_OPERATION = 'CUSTOM_OPERATION', 8 | } 9 | 10 | export const OPERATION_NODE_TYPE_MAP = { 11 | 'NI-VISA': NodeType.NI_VISA_OPERATION, 12 | 'FUNCTION': NodeType.FUNCTION_OPERATION, 13 | 'CUSTOM': NodeType.CUSTOM_OPERATION, 14 | } as const; 15 | 16 | export const POSITION_X = { 17 | 'instrument-node': 100, 18 | 'model-node': 350, 19 | 'config-node': 600, 20 | 'operation-node': 850, 21 | } as const; 22 | 23 | export const NODE_WIDTH = 150; 24 | export const NODE_HEIGHT = 30; 25 | export const NODE_WIDTH_HALF = NODE_WIDTH / 2; 26 | 27 | /** 28 | * @description 配置类型 29 | */ 30 | export const CONFIG_TYPE_OPTIONS = [ 31 | { value: 'NI-VISA', label: 'NI-VISA' }, 32 | { value: 'FUNCTION', label: 'FUNCTION' }, 33 | { value: 'CUSTOM', label: 'CUSTOM' }, 34 | ]; 35 | 36 | export const BOOLEAN_STRING_OPTIONS = [ 37 | { value: 'true', label: '是' }, 38 | { value: 'false', label: '否' }, 39 | ]; 40 | 41 | /** 42 | * @description 通讯配置-通讯类型 43 | */ 44 | export const COMMUNICATION_TYPE_OPTIONS = [ 45 | { value: 'RS232', label: 'RS232' }, 46 | { value: 'RS485', label: 'RS485' }, 47 | { value: 'TCP', label: 'TCP' }, 48 | ]; 49 | 50 | /** 51 | * @description 方法参数类型 52 | */ 53 | export const PARAMETER_TYPE_OPTIONS = [ 54 | { value: 'System.Boolean', label: 'Boolean' }, 55 | { value: 'System.Char', label: 'Char' }, 56 | { value: 'System.Double', label: 'Double' }, 57 | { value: 'System.Int32', label: 'Int32' }, 58 | { value: 'System.String', label: 'String' }, 59 | { value: 'System.Single', label: 'Single' }, 60 | ]; 61 | 62 | /** 63 | * @description 通讯配置-停止位 64 | */ 65 | export const STOP_BITS_OPTIONS = [ 66 | { value: '0', label: 'None' }, 67 | { value: '1', label: 'One' }, 68 | { value: '2', label: 'Two' }, 69 | { value: '3', label: 'OnePointFive' }, 70 | ]; 71 | 72 | /** 73 | * @description 通讯配置-奇偶校验 74 | */ 75 | export const PARITY_OPTIONS = [ 76 | { value: '0', label: 'None' }, 77 | { value: '1', label: 'Odd' }, 78 | { value: '2', label: 'Even' }, 79 | { value: '3', label: 'Mark' }, 80 | { value: '4', label: 'Space' }, 81 | ]; 82 | 83 | /** 84 | * @description 通讯配置-握手协议 85 | */ 86 | export const HAND_SHAKE_OPTIONS = [ 87 | { value: '0', label: 'None' }, 88 | { value: '1', label: 'Xon/Xoff' }, 89 | { value: '2', label: 'RequestToSend' }, 90 | { value: '3', label: 'RequestToSendXonXoff' }, 91 | ]; 92 | -------------------------------------------------------------------------------- /src/ProfileEditor/node/operationNode.ts: -------------------------------------------------------------------------------- 1 | import { HtmlNode, HtmlNodeModel, type IHtmlNodeProps, type Model, BaseNodeModel } from '@logicflow/core'; 2 | import OperationNode from './OperationNode.vue'; 3 | import { createApp, h, ref } from 'vue'; 4 | import { NODE_WIDTH, NODE_HEIGHT } from '../common'; 5 | 6 | class OperationNodeView extends HtmlNode { 7 | root: HTMLDivElement; 8 | vueComponent: typeof OperationNode; 9 | vm: ReturnType | null = null; 10 | vmProps: ReturnType> | null = null; 16 | 17 | constructor(props: IHtmlNodeProps) { 18 | super(props); 19 | this.root = document.createElement('div'); 20 | this.root.style.width = '100%'; 21 | this.root.style.height = '100%'; 22 | this.vueComponent = OperationNode; 23 | } 24 | 25 | shouldUpdate() { 26 | const data = { 27 | id: this.props.model.properties.id, 28 | isSelected: this.props.model.isSelected, 29 | isHovered: this.props.model.isHovered 30 | }; 31 | if (this.preProperties && this.preProperties === JSON.stringify(data)) { 32 | return false; 33 | } 34 | this.preProperties = JSON.stringify(data); 35 | return true; 36 | } 37 | 38 | setHtml(rootEl: SVGForeignObjectElement): void { 39 | rootEl.appendChild(this.root); 40 | if (this.vm && this.vmProps) { 41 | this.vmProps.value = { 42 | properties: this.props.model.properties, 43 | isSelected: this.props.model.isSelected, 44 | isHovered: this.props.model.isHovered, 45 | model: this.props.model 46 | }; 47 | } else { 48 | // @ts-expect-error 暂时不解决 49 | this.vmProps = ref({ 50 | properties: this.props.model.properties, 51 | isSelected: this.props.model.isSelected, 52 | isHovered: this.props.model.isHovered, 53 | model: this.props.model, 54 | }); 55 | this.vm = createApp({ 56 | render: () => h(this.vueComponent, this.vmProps!.value) 57 | }); 58 | this.vm.mount(this.root); 59 | } 60 | } 61 | } 62 | 63 | class OperationNodeModel extends HtmlNodeModel { 64 | setAttributes() { 65 | this.width = NODE_WIDTH; 66 | this.height = NODE_HEIGHT; 67 | } 68 | 69 | getDefaultAnchor(): Model.AnchorConfig[] { 70 | const { width, x, y, id } = this; 71 | return [ 72 | { 73 | x: x - width / 2, 74 | y, 75 | name: 'left', 76 | id: `${id}_0` 77 | }, 78 | { 79 | x: x + width / 2, 80 | y, 81 | name: 'right', 82 | id: `${id}_1`, 83 | }, 84 | ]; 85 | } 86 | } 87 | 88 | export default { 89 | type: 'operation-node', 90 | model: OperationNodeModel, 91 | view: OperationNodeView 92 | }; -------------------------------------------------------------------------------- /src/ProfileEditor/node/instrumentNode.ts: -------------------------------------------------------------------------------- 1 | import { HtmlNode, HtmlNodeModel, type IHtmlNodeProps, type Model } from '@logicflow/core'; 2 | import InstrumentNode from './InstrumentNode.vue'; 3 | import { createApp, h, ref } from 'vue'; 4 | import { NODE_WIDTH, NODE_HEIGHT } from '../common'; 5 | 6 | class InstrumentNodeView extends HtmlNode { 7 | root: HTMLDivElement; 8 | vueComponent: typeof InstrumentNode; 9 | vm: ReturnType | null = null; 10 | vmProps: ReturnType> | null = null; 16 | 17 | constructor(props: IHtmlNodeProps) { 18 | super(props); 19 | this.root = document.createElement('div'); 20 | this.root.style.width = '100%'; 21 | this.root.style.height = '100%'; 22 | this.vueComponent = InstrumentNode; 23 | } 24 | 25 | shouldUpdate() { 26 | const data = { 27 | id: this.props.model.properties.id, 28 | isSelected: this.props.model.isSelected, 29 | isHovered: this.props.model.isHovered 30 | }; 31 | if (this.preProperties && this.preProperties === JSON.stringify(data)) { 32 | return false; 33 | } 34 | this.preProperties = JSON.stringify(data); 35 | return true; 36 | } 37 | 38 | setHtml(rootEl: SVGForeignObjectElement): void { 39 | rootEl.appendChild(this.root); 40 | if (this.vm && this.vmProps) { 41 | this.vmProps.value = { 42 | properties: this.props.model.properties, 43 | isSelected: this.props.model.isSelected, 44 | isHovered: this.props.model.isHovered, 45 | model: this.props.model 46 | }; 47 | } else { 48 | // @ts-expect-error 暂时不解决 49 | this.vmProps = ref({ 50 | properties: this.props.model.properties, 51 | isSelected: this.props.model.isSelected, 52 | isHovered: this.props.model.isHovered, 53 | model: this.props.model, 54 | }); 55 | this.vm = createApp({ 56 | render: () => h(this.vueComponent, this.vmProps!.value) 57 | }); 58 | this.vm.mount(this.root); 59 | } 60 | } 61 | } 62 | 63 | class InstrumentNodeModel extends HtmlNodeModel { 64 | setAttributes() { 65 | this.width = NODE_WIDTH; 66 | this.height = NODE_HEIGHT; 67 | } 68 | 69 | getDefaultAnchor(): Model.AnchorConfig[] { 70 | const { width, x, y, id } = this; 71 | return [ 72 | { 73 | x: x - width / 2, 74 | y, 75 | name: 'left', 76 | id: `${id}_0` 77 | }, 78 | { 79 | x: x + width / 2, 80 | y, 81 | name: 'right', 82 | id: `${id}_1`, 83 | }, 84 | ]; 85 | } 86 | } 87 | 88 | export default { 89 | type: 'instrument-node', 90 | model: InstrumentNodeModel, 91 | view: InstrumentNodeView 92 | }; 93 | -------------------------------------------------------------------------------- /src/ProfileEditor/node/modelNode.ts: -------------------------------------------------------------------------------- 1 | import { HtmlNode, HtmlNodeModel, type IHtmlNodeProps, type Model } from '@logicflow/core'; 2 | import ModelNode from './ModelNode.vue'; 3 | import { createApp, h, ref } from 'vue'; 4 | import { NODE_WIDTH, NODE_HEIGHT } from '../common'; 5 | 6 | class ModelNodeView extends HtmlNode { 7 | root: HTMLDivElement; 8 | vueComponent: typeof ModelNode; 9 | vm: ReturnType | null = null; 10 | vmProps: ReturnType> | null = null; 17 | 18 | constructor(props: IHtmlNodeProps) { 19 | super(props); 20 | this.root = document.createElement('div'); 21 | this.root.style.width = '100%'; 22 | this.root.style.height = '100%'; 23 | this.vueComponent = ModelNode; 24 | } 25 | 26 | shouldUpdate() { 27 | const data = { 28 | id: this.props.model.properties.id, 29 | isFloded: this.props.model.properties.isFloded, 30 | isSelected: this.props.model.isSelected, 31 | isHovered: this.props.model.isHovered 32 | }; 33 | if (this.preProperties && this.preProperties === JSON.stringify(data)) { 34 | return false; 35 | } 36 | this.preProperties = JSON.stringify(data); 37 | return true; 38 | } 39 | 40 | setHtml(rootEl: SVGForeignObjectElement): void { 41 | rootEl.appendChild(this.root); 42 | if (this.vm && this.vmProps) { 43 | this.vmProps.value = { 44 | properties: this.props.model.properties, 45 | isSelected: this.props.model.isSelected, 46 | isHovered: this.props.model.isHovered, 47 | isFloded: this.props.model.properties.isFloded as boolean, 48 | model: this.props.model 49 | }; 50 | } else { 51 | // @ts-expect-error 暂时不解决 52 | this.vmProps = ref({ 53 | properties: this.props.model.properties, 54 | isSelected: this.props.model.isSelected, 55 | isHovered: this.props.model.isHovered, 56 | isFloded: this.props.model.properties.isFloded, 57 | model: this.props.model, 58 | }); 59 | this.vm = createApp({ 60 | render: () => h(this.vueComponent, this.vmProps!.value) 61 | }); 62 | this.vm.mount(this.root); 63 | } 64 | } 65 | } 66 | 67 | class ModelNodeModel extends HtmlNodeModel { 68 | setAttributes() { 69 | this.width = NODE_WIDTH; 70 | this.height = NODE_HEIGHT; 71 | } 72 | 73 | getDefaultAnchor(): Model.AnchorConfig[] { 74 | const { width, x, y, id } = this; 75 | return [ 76 | { 77 | x: x - width / 2, 78 | y, 79 | name: 'left', 80 | id: `${id}_0` 81 | }, 82 | { 83 | x: x + width / 2, 84 | y, 85 | name: 'right', 86 | id: `${id}_1`, 87 | }, 88 | ]; 89 | } 90 | } 91 | 92 | export default { 93 | type: 'model-node', 94 | model: ModelNodeModel, 95 | view: ModelNodeView 96 | }; 97 | -------------------------------------------------------------------------------- /src/ProfileEditor/node/configNode.ts: -------------------------------------------------------------------------------- 1 | import { HtmlNode, HtmlNodeModel, type IHtmlNodeProps, type Model } from '@logicflow/core'; 2 | import ConfigNode from './ConfigNode.vue'; 3 | import { createApp, h, ref } from 'vue'; 4 | import { NODE_WIDTH, NODE_HEIGHT } from '../common'; 5 | 6 | class ConfigNodeView extends HtmlNode { 7 | root: HTMLDivElement; 8 | vueComponent: typeof ConfigNode; 9 | vm: ReturnType | null = null; 10 | vmProps: ReturnType> | null = null; 17 | 18 | constructor(props: IHtmlNodeProps) { 19 | super(props); 20 | this.root = document.createElement('div'); 21 | this.root.style.width = '100%'; 22 | this.root.style.height = '100%'; 23 | this.vueComponent = ConfigNode; 24 | } 25 | 26 | shouldUpdate() { 27 | const data = { 28 | id: this.props.model.properties.id, 29 | isFloded: this.props.model.properties.isFloded, 30 | isSelected: this.props.model.isSelected, 31 | isHovered: this.props.model.isHovered, 32 | }; 33 | if (this.preProperties && this.preProperties === JSON.stringify(data)) { 34 | return false; 35 | } 36 | this.preProperties = JSON.stringify(data); 37 | return true; 38 | } 39 | 40 | setHtml(rootEl: SVGForeignObjectElement): void { 41 | rootEl.appendChild(this.root); 42 | if (this.vm && this.vmProps) { 43 | this.vmProps.value = { 44 | properties: this.props.model.properties, 45 | isSelected: this.props.model.isSelected, 46 | isHovered: this.props.model.isHovered, 47 | isFloded: this.props.model.properties.isFloded as boolean, 48 | model: this.props.model 49 | }; 50 | } else { 51 | // @ts-expect-error 暂时不解决 52 | this.vmProps = ref({ 53 | properties: this.props.model.properties, 54 | isSelected: this.props.model.isSelected, 55 | isHovered: this.props.model.isHovered, 56 | isFloded: this.props.model.properties.isFloded, 57 | model: this.props.model, 58 | }); 59 | this.vm = createApp({ 60 | render: () => h(this.vueComponent, this.vmProps!.value) 61 | }); 62 | this.vm.mount(this.root); 63 | } 64 | } 65 | } 66 | 67 | class ConfigNodeModel extends HtmlNodeModel { 68 | setAttributes() { 69 | this.width = NODE_WIDTH; 70 | this.height = NODE_HEIGHT; 71 | } 72 | 73 | getDefaultAnchor(): Model.AnchorConfig[] { 74 | const { width, x, y, id } = this; 75 | return [ 76 | { 77 | x: x - width / 2, 78 | y, 79 | name: 'left', 80 | id: `${id}_0` 81 | }, 82 | { 83 | x: x + width / 2, 84 | y, 85 | name: 'right', 86 | id: `${id}_1`, 87 | }, 88 | ]; 89 | } 90 | } 91 | 92 | export default { 93 | type: 'config-node', 94 | model: ConfigNodeModel, 95 | view: ConfigNodeView 96 | }; 97 | -------------------------------------------------------------------------------- /src/ProfileEditor/node/OperationNode.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 57 | 58 | 117 | -------------------------------------------------------------------------------- /src/ProfileEditor/components/drawer/MeasureModeEditor/WorkPlaceList.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | -------------------------------------------------------------------------------- /src/ProfileEditor/components/drawer/MethodEditor/MethodEditor.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 100 | 101 | -------------------------------------------------------------------------------- /public/profile.xml: -------------------------------------------------------------------------------- 1 | 2 | <设备类别 id="万用表"> 3 | <型号 id="DMM6500-0"> 4 | <配置方式>NI-VISA 5 | <配置 id="NI-VISA"> 6 | <操作 id="采集直流电压" Parameter="" HasReturn="true"> 7 | <指令>abort;MEASure:VOLTage:DC? 8 | 9 | <操作 id="采集交流电压" Parameter="" HasReturn="true"> 10 | <指令>abort;MEASure:CURRent:AC? 11 | 12 | <操作 id="采集直流电流" Parameter="" HasReturn="true"> 13 | <指令>abort;MEASure:CURRent:DC? 14 | 15 | <操作 id="采集电阻" Parameter="" HasReturn="true"> 16 | <指令>abort;MEAS:RES? 17 | 18 | 19 | 20 | <型号 id="DMM6500-1"> 21 | <配置方式>FUNCTION 22 | <配置 id="FUNCTION"> 23 | <类 Type="命名空间.类名, DLL模板" IsVISA="true"/> 24 | <操作 id="连接检测" Parameter="" HasReturn="true"> 25 | <方法 Name="CheckConnect"/> 26 | 27 | <操作 id="PICO设置" Parameter="" HasReturn="true"> 28 | <方法 Name="setAmplitude"> 29 | <参数 Type="System.Int32" Value="5"/> 30 | <参数 Type="System.String" Value="V"/> 31 | <参数 Type="System.String" Value="AC"/> 32 | <参数 Type="System.Int32" Value="1"/> 33 | 34 | <方法 Name="setAmplitude"> 35 | <参数 Type="System.Int32" Value="5"/> 36 | <参数 Type="System.String" Value="V"/> 37 | <参数 Type="System.String" Value="AC"/> 38 | <参数 Type="System.Int32" Value="2"/> 39 | 40 | <方法 Name="setTime"> 41 | <参数 Type="System.Int32" Value="40000"/> 42 | <参数 Type="System.Int32" Value="500"/> 43 | <参数 Type="System.String" Value="us"/> 44 | 45 | <方法 Name="setBuffer"> 46 | <参数 Type="System.Int32" Value="1"/> 47 | 48 | <方法 Name="setBuffer"> 49 | <参数 Type="System.Int32" Value="2"/> 50 | 51 | 52 | 53 | 54 | <型号 id="DMM6500-2"> 55 | <配置方式>CUSTOM 56 | <配置 id="CUSTOM"> 57 | <通信方法>TCP 58 | 59 | 192.172.0.1 60 | <端口>80 61 | 62 | <操作 id="切换"> 63 | <测量模式 id="S01"> 64 | <工位 id="1"> 65 | <字节流 send="0x00" receive="0x00"/> 66 | <字节流 send="0x00" receive="0x00"/> 67 | 68 | <工位 id="2"> 69 | <字节流 send="0x00" receive="0x00"/> 70 | <字节流 send="0x00" receive="0x00"/> 71 | 72 | <工位 id="3"> 73 | <字节流 send="0x00" receive="0x00"/> 74 | <字节流 send="0x00" receive="0x00"/> 75 | 76 | <工位 id="4"> 77 | <字节流 send="0x00" receive="0x00"/> 78 | <字节流 send="0x00" receive="0x00"/> 79 | 80 | 81 | <测量模式 id="S02"> 82 | <工位 id="1"> 83 | <字节流 send="0x00" receive="0x00"/> 84 | <字节流 send="0x00" receive="0x00"/> 85 | 86 | <工位 id="2"> 87 | <字节流 send="0x00" receive="0x00"/> 88 | <字节流 send="0x00" receive="0x00"/> 89 | 90 | <工位 id="3"> 91 | <字节流 send="0x00" receive="0x00"/> 92 | <字节流 send="0x00" receive="0x00"/> 93 | 94 | <工位 id="4"> 95 | <字节流 send="0x00" receive="0x00"/> 96 | <字节流 send="0x00" receive="0x00"/> 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/ProfileEditor/components/drawer/MeasureModeEditor/MeasureModeEditor.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 100 | 101 | -------------------------------------------------------------------------------- /src/ProfileEditor/node/InstrumentNode.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 77 | 78 | 162 | -------------------------------------------------------------------------------- /src/ProfileEditor/components/drawer/ConfigNodeForm.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 139 | 140 | -------------------------------------------------------------------------------- /src/ProfileEditor/components/ControlPanel.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 137 | 138 | 165 | -------------------------------------------------------------------------------- /src/ProfileEditor/types.ts: -------------------------------------------------------------------------------- 1 | // 用于数据转换的配置文件定义 2 | export type ProfileData = InstrumentNodeData & { 3 | models: ModelData[]; 4 | } 5 | 6 | export type ModelData = ModelNodeData & { 7 | 'NI-VISA'?: ConfigNodeDataMap['NI-VISA'] & { operations: OperationNodeDataMap['NI-VISA'][] }; 8 | 'FUNCTION'?: ConfigNodeDataMap['FUNCTION'] & { operations: OperationNodeDataMap['FUNCTION'][] }; 9 | 'CUSTOM'?: ConfigNodeDataMap['CUSTOM'] & { operations: OperationNodeDataMap['CUSTOM'][] }; 10 | }; 11 | 12 | // 流程图节点数据梳理 13 | 14 | export type ConfigType = 'NI-VISA' | 'FUNCTION' | 'CUSTOM'; 15 | export type CommunicationType = 'RS232' | 'RS485' | 'TCP'; 16 | 17 | export type InstrumentNodeData = { 18 | /** 19 | * 设备类别 20 | */ 21 | id: string; 22 | } 23 | 24 | export type ModelNodeData = { 25 | /** 26 | * 设备型号 27 | */ 28 | id: string; 29 | /** 30 | * 配置方式 31 | */ 32 | configType: ConfigType; 33 | } 34 | 35 | export type ConfigNodeData = 36 | Omit & 37 | Omit & 38 | Omit & { 39 | id: ConfigType; 40 | }; 41 | 42 | export type ConfigNodeDataMap = { 43 | 'NI-VISA': { 44 | id: 'NI-VISA'; 45 | }; 46 | 'FUNCTION': { 47 | id: 'FUNCTION'; 48 | spaceName: string; 49 | className: string; 50 | dllTemplate: string; 51 | isVisa: BooleanString; 52 | } 53 | 'CUSTOM': { 54 | id: 'CUSTOM'; 55 | } & { 56 | communicationType: 'RS232' | 'RS485' | 'TCP'; 57 | communicationConfig: { 58 | /** 59 | * 波特率 60 | */ 61 | baudRate: string; 62 | /** 63 | * 数据位 64 | */ 65 | dataBits: string; 66 | /** 67 | * 停止位 68 | * - `0`: None 69 | * - `1`: One 70 | * - `2`: Two 71 | * - `3`: OnePointFive 72 | */ 73 | stopBits: string; 74 | /** 75 | * 奇偶校验 76 | * - `0`: None 77 | * - `1`: Odd 78 | * - `2`: Even 79 | * - `3`: Mark 80 | * - `4`: Space 81 | */ 82 | parity: string; 83 | /** 84 | * 缓冲区字节数 85 | */ 86 | bufferBytes: string; 87 | /** 88 | * 握手协议 89 | * - `0`: None 90 | * - `1`: Xon/Xoff 91 | * - `2`: RequestToSend 92 | * - `3`: RequestToSendXonXoff 93 | */ 94 | handShake: string; 95 | /** 96 | * 超时时间 97 | */ 98 | timeout: string; 99 | /** 100 | * IP 地址 101 | */ 102 | ip: string; 103 | /** 104 | * 端口号 105 | */ 106 | port: string; 107 | } 108 | } 109 | } 110 | 111 | export type OperationNodeData = OperationNodeDataMap['NI-VISA'] & OperationNodeDataMap['FUNCTION'] & OperationNodeDataMap['CUSTOM']; 112 | 113 | export type OperationNodeDataMap = { 114 | 'NI-VISA': { 115 | id: string; 116 | parameter: string; 117 | hasReturn: BooleanString; 118 | command: string; 119 | } 120 | 'FUNCTION': { 121 | id: string; 122 | parameter: string; 123 | hasReturn: BooleanString; 124 | methods: MethodData[]; 125 | } 126 | 'CUSTOM': { 127 | id: string; 128 | measureModes: MeasureMode[]; 129 | } 130 | } 131 | 132 | export type MethodData = { 133 | name: string; 134 | parameters: { 135 | type: string; 136 | value: string; 137 | }[]; 138 | } 139 | 140 | export type MeasureMode = { 141 | id: string; 142 | workplaces: Workplace[]; 143 | } 144 | 145 | export type Workplace = { 146 | id: string; 147 | byteStreams: ByteStream[]; 148 | } 149 | 150 | export type ByteStream = { 151 | send: string; 152 | receive: string; 153 | } 154 | 155 | // xml2js 解析 xml 得到的配置文件数据定义 156 | 157 | export type XMLProfileData = { 158 | '设备类别': { 159 | $: { id: string }; 160 | '型号': XMLModelData[]; 161 | } 162 | } 163 | 164 | export type XMLModelData = { 165 | $: { id: string }; 166 | '配置方式': [ConfigType]; 167 | '配置': XMLConfigData[]; 168 | } 169 | 170 | export type XMLConfigData = XMLConfigDataMap[keyof XMLConfigDataMap]; 171 | 172 | export type XMLConfigDataMap = { 173 | 'NI-VISA': { 174 | $: { id: 'NI-VISA' }; 175 | '操作': XMLOperationDataMap['NI-VISA'][]; 176 | } 177 | 'FUNCTION': { 178 | $: { id: 'FUNCTION' }; 179 | '操作': XMLOperationDataMap['FUNCTION'][]; 180 | '类': [{ $: { 181 | Type: string; 182 | IsVISA: BooleanString; 183 | }}] 184 | } 185 | 'CUSTOM': { 186 | $: { id: 'CUSTOM' }; 187 | '操作': XMLOperationDataMap['CUSTOM'][]; 188 | } & (XMLCommunicationConfigDataMap[keyof XMLCommunicationConfigDataMap]) 189 | } 190 | 191 | export type XMLCommunicationConfigDataMap = { 192 | 'RS232': { 193 | '通信方法': ['RS232']; 194 | RS232: [{ 195 | '波特率': [string]; 196 | '数据位': [string]; 197 | '停止位': [string]; 198 | '奇偶校验': [string]; 199 | '缓冲区字节数': [string]; 200 | '握手协议': [string]; 201 | '超时时间': [string]; 202 | }] 203 | } 204 | 'RS485': { 205 | '通信方法': ['RS485']; 206 | RS485: [{ 207 | '波特率': [string]; 208 | '数据位': [string]; 209 | '停止位': [string]; 210 | '奇偶校验': [string]; 211 | '缓冲区字节数': [string]; 212 | '握手协议': [string]; 213 | '超时时间': [string]; 214 | }] 215 | } 216 | 'TCP': { 217 | '通信方法': ['TCP']; 218 | TCP: [{ 219 | IP: [string]; 220 | '端口': [string]; 221 | }] 222 | } 223 | } 224 | 225 | export type XMLOperationDataMap = { 226 | 'NI-VISA': { 227 | $: { id: string; Parameter: string; HasReturn: BooleanString }; 228 | '指令': [string]; 229 | } 230 | 'FUNCTION': { 231 | $: { id: string; Parameter: string; HasReturn: BooleanString }; 232 | '方法': XMLMethodData[]; 233 | } 234 | 'CUSTOM': { 235 | $: { id: string }; 236 | '测量模式': XMLMeasureModeData[]; 237 | } 238 | } 239 | 240 | export type XMLMethodData = { 241 | $: { Name: string }; 242 | '参数': XMLParameterData[]; 243 | } 244 | 245 | export type XMLParameterData = { 246 | $: { Type: string; Value: string }; 247 | } 248 | 249 | export type XMLMeasureModeData = { 250 | $: { id: string }; 251 | '工位': XMLWorkplaceData[]; 252 | } 253 | 254 | export type XMLWorkplaceData = { 255 | $: { id: string }; 256 | '字节流': XMLByteStreamData[]; 257 | } 258 | 259 | export type XMLByteStreamData = { 260 | $: { send: string; receive: string }; 261 | } 262 | 263 | export type BooleanString = 'true' | 'false'; 264 | -------------------------------------------------------------------------------- /src/ProfileEditor/components/drawer/NodeEditDrawer.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 207 | 208 | 253 | -------------------------------------------------------------------------------- /src/ProfileEditor/node/ModelNode.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 166 | 167 | 289 | -------------------------------------------------------------------------------- /src/ProfileEditor/node/ConfigNode.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 179 | 180 | 302 | -------------------------------------------------------------------------------- /src/ProfileEditor/ProfileEditor.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 251 | 252 | 275 | 276 | 314 | -------------------------------------------------------------------------------- /src/ProfileEditor/utils/adaptor.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ProfileData, 3 | ModelData, 4 | XMLProfileData, 5 | XMLModelData, 6 | XMLConfigDataMap, 7 | XMLCommunicationConfigDataMap, 8 | } from '../types'; 9 | import { v4 as uuidv4 } from 'uuid'; 10 | import type LogicFlow from '@logicflow/core'; 11 | import { NodeType, NODE_WIDTH_HALF } from '../common'; 12 | import { initConfigNodeData, initOperationNodeData } from './inital'; 13 | 14 | /** 15 | * 将 xml2js 解析 xml 得到的数据适配为配置文件数据 16 | */ 17 | export function xmlData2ProfileData(xmlProfileData: XMLProfileData): ProfileData { 18 | const profileData: ProfileData = { 19 | id: xmlProfileData['设备类别'].$.id, 20 | models: [], 21 | }; 22 | 23 | for (const xmlModelData of xmlProfileData['设备类别']['型号'] ?? []) { 24 | const modelData: ModelData = { 25 | id: xmlModelData.$.id, 26 | configType: xmlModelData['配置方式'][0], 27 | }; 28 | for (const xmlConfigData of xmlModelData['配置'] ?? []) { 29 | if (xmlConfigData.$.id === 'NI-VISA') { 30 | const configData: ModelData['NI-VISA'] = { 31 | id: xmlConfigData.$.id, 32 | operations: (xmlConfigData as XMLConfigDataMap['NI-VISA'])['操作']?.map((xmlOperationData) => { 33 | return { 34 | id: xmlOperationData.$.id, 35 | parameter: xmlOperationData.$.Parameter, 36 | hasReturn: xmlOperationData.$.HasReturn, 37 | command: xmlOperationData['指令'][0], 38 | }; 39 | }) || [], 40 | }; 41 | if (!modelData['NI-VISA']) { 42 | modelData['NI-VISA'] = configData; 43 | } 44 | } else if (xmlConfigData.$.id === 'FUNCTION') { 45 | const [spaceName = '', nextString = ''] = (xmlConfigData as XMLConfigDataMap['FUNCTION'])['类'][0].$.Type.split('.'); 46 | const [className = '', dllTemplate = ''] = nextString.split(', '); 47 | const configData: ModelData['FUNCTION'] = { 48 | id: xmlConfigData.$.id, 49 | spaceName, 50 | className, 51 | dllTemplate, 52 | isVisa: (xmlConfigData as XMLConfigDataMap['FUNCTION'])['类'][0].$.IsVISA, 53 | operations: (xmlConfigData as XMLConfigDataMap['FUNCTION'])['操作']?.map((xmlOperationData) => { 54 | return { 55 | id: xmlOperationData.$.id, 56 | parameter: xmlOperationData.$.Parameter, 57 | hasReturn: xmlOperationData.$.HasReturn, 58 | methods: xmlOperationData['方法']?.map(xmlMethodData => { 59 | return { 60 | name: xmlMethodData.$.Name, 61 | parameters: xmlMethodData['参数']?.map(xmlParameterData => { 62 | return { 63 | type: xmlParameterData.$.Type, 64 | value: xmlParameterData.$.Value, 65 | }; 66 | }) || [], 67 | }; 68 | }) || [], 69 | }; 70 | }) || [], 71 | }; 72 | if (!modelData.FUNCTION) { 73 | modelData.FUNCTION = configData; 74 | } 75 | } else if (xmlConfigData.$.id === 'CUSTOM') { 76 | const communicationType = (xmlConfigData as XMLConfigDataMap['CUSTOM'])['通信方法'][0]; 77 | if (communicationType === 'RS232' || communicationType === 'RS485') { 78 | // @ts-expect-error 这个类型定义太麻烦了 79 | const communicationConfig: XMLCommunicationConfigDataMap['RS232']['RS232'][0] = xmlConfigData[communicationType][0]; 80 | const configData: ModelData['CUSTOM'] = { 81 | id: xmlConfigData.$.id, 82 | communicationType, 83 | communicationConfig: { 84 | baudRate: communicationConfig['波特率'][0], 85 | dataBits: communicationConfig['数据位'][0], 86 | stopBits: communicationConfig['停止位'][0], 87 | parity: communicationConfig['奇偶校验'][0], 88 | bufferBytes: communicationConfig['缓冲区字节数'][0], 89 | handShake: communicationConfig['握手协议'][0], 90 | timeout: communicationConfig['超时时间'][0], 91 | ip: '', 92 | port: '', 93 | }, 94 | operations: (xmlConfigData as XMLConfigDataMap['CUSTOM'])['操作']?.map((xmlOperationData) => { 95 | return { 96 | id: xmlOperationData.$.id, 97 | measureModes: xmlOperationData['测量模式']?.map(xmlMeasureModeData => { 98 | return { 99 | id: xmlMeasureModeData.$.id, 100 | workplaces: xmlMeasureModeData['工位']?.map(xmlWorkplaceData => { 101 | return { 102 | id: xmlWorkplaceData.$.id, 103 | byteStreams: xmlWorkplaceData['字节流']?.map(xmlByteStreamData => { 104 | return { 105 | send: xmlByteStreamData.$.send, 106 | receive: xmlByteStreamData.$.receive, 107 | }; 108 | }) || [], 109 | }; 110 | }) || [], 111 | }; 112 | }) || [], 113 | }; 114 | }) || [], 115 | }; 116 | if (!modelData.CUSTOM) { 117 | modelData.CUSTOM = configData; 118 | } 119 | } else if (communicationType === 'TCP') { 120 | // @ts-expect-error 这个类型定义太麻烦了 121 | const communicationConfig: XMLCommunicationConfigDataMap['TCP']['TCP'][0] = xmlConfigData[communicationType][0]; 122 | const configData: ModelData['CUSTOM'] = { 123 | id: xmlConfigData.$.id, 124 | communicationType, 125 | communicationConfig: { 126 | baudRate: '', 127 | dataBits: '', 128 | stopBits: '', 129 | parity: '', 130 | bufferBytes: '', 131 | handShake: '', 132 | timeout: '', 133 | ip: communicationConfig['IP'][0], 134 | port: communicationConfig['端口'][0], 135 | }, 136 | operations: (xmlConfigData as XMLConfigDataMap['CUSTOM'])['操作']?.map((xmlOperationData) => { 137 | return { 138 | id: xmlOperationData.$.id, 139 | measureModes: xmlOperationData['测量模式']?.map(xmlMeasureModeData => { 140 | return { 141 | id: xmlMeasureModeData.$.id, 142 | workplaces: xmlMeasureModeData['工位']?.map(xmlWorkplaceData => { 143 | return { 144 | id: xmlWorkplaceData.$.id, 145 | byteStreams: xmlWorkplaceData['字节流']?.map(xmlByteStreamData => { 146 | return { 147 | send: xmlByteStreamData.$.send, 148 | receive: xmlByteStreamData.$.receive, 149 | }; 150 | }) || [], 151 | }; 152 | }) || [], 153 | }; 154 | }) || [], 155 | }; 156 | }) || [], 157 | }; 158 | if (!modelData.CUSTOM) { 159 | modelData.CUSTOM = configData; 160 | } 161 | } 162 | } 163 | } 164 | profileData.models.push(modelData); 165 | } 166 | 167 | return profileData; 168 | } 169 | 170 | 171 | /** 172 | * 将配置文件数据适配为 xml 数据,使得 xml2js 可以将其转换为 xml 文件 173 | */ 174 | export function profileData2XmlData(profileData: ProfileData): XMLProfileData { 175 | const xmlProfileData: XMLProfileData = { 176 | '设备类别' : { 177 | $: { id: profileData.id }, 178 | '型号': [], 179 | } 180 | }; 181 | 182 | for (const modelData of profileData.models ?? []) { 183 | const xmlModelData: XMLModelData = { 184 | $: { id: modelData.id }, 185 | '配置方式': [modelData.configType], 186 | '配置': [], 187 | }; 188 | if (modelData['NI-VISA']) { 189 | const xmlConfigData: XMLConfigDataMap['NI-VISA'] = { 190 | $: { id: 'NI-VISA' }, 191 | '操作': modelData['NI-VISA'].operations?.map(operationData => { 192 | return { 193 | $: { 194 | id: operationData.id, 195 | Parameter: operationData.parameter, 196 | HasReturn: operationData.hasReturn, 197 | }, 198 | '指令': [operationData.command], 199 | }; 200 | }), 201 | }; 202 | xmlModelData['配置'].push(xmlConfigData); 203 | } 204 | if (modelData.FUNCTION) { 205 | const xmlConfigData: XMLConfigDataMap['FUNCTION'] = { 206 | $: { id: 'FUNCTION' }, 207 | '类': [{ 208 | $: { 209 | Type: `${modelData.FUNCTION.spaceName}.${modelData.FUNCTION.className}, ${modelData.FUNCTION.dllTemplate}`, 210 | IsVISA: modelData.FUNCTION.isVisa, 211 | } 212 | }], 213 | '操作': modelData.FUNCTION.operations?.map(operationData => { 214 | return { 215 | $: { 216 | id: operationData.id, 217 | Parameter: operationData.parameter, 218 | HasReturn: operationData.hasReturn, 219 | }, 220 | '方法': operationData.methods?.map(methodData => { 221 | return { 222 | $: { Name: methodData.name }, 223 | '参数': methodData.parameters?.map(parameterData => { 224 | return { 225 | $: { 226 | Type: parameterData.type, 227 | Value: parameterData.value, 228 | } 229 | }; 230 | }), 231 | }; 232 | }), 233 | }; 234 | }), 235 | }; 236 | xmlModelData['配置'].push(xmlConfigData); 237 | } 238 | if (modelData.CUSTOM) { 239 | // @ts-expect-error 这个类型定义太麻烦了 240 | const xmlConfigData: XMLConfigDataMap['CUSTOM'] = { 241 | $: { id: 'CUSTOM' }, 242 | '通信方法': [modelData.CUSTOM.communicationType], 243 | [modelData.CUSTOM.communicationType]: [{ 244 | ...( 245 | modelData.CUSTOM.communicationType === 'RS232' || modelData.CUSTOM.communicationType === 'RS485' 246 | ? { 247 | '波特率': [modelData.CUSTOM.communicationConfig.baudRate], 248 | '数据位': [modelData.CUSTOM.communicationConfig.dataBits], 249 | '停止位': [modelData.CUSTOM.communicationConfig.stopBits], 250 | '奇偶校验': [modelData.CUSTOM.communicationConfig.parity], 251 | '缓冲区字节数': [modelData.CUSTOM.communicationConfig.bufferBytes], 252 | '握手协议': [modelData.CUSTOM.communicationConfig.handShake], 253 | '超时时间': [modelData.CUSTOM.communicationConfig.timeout], 254 | } 255 | : { 256 | 'IP': [modelData.CUSTOM.communicationConfig.ip], 257 | '端口': [modelData.CUSTOM.communicationConfig.port], 258 | } 259 | ), 260 | }], 261 | '操作': modelData.CUSTOM.operations?.map(operationData => { 262 | return { 263 | $: { id: operationData.id }, 264 | '测量模式': operationData.measureModes?.map(measureModeData => { 265 | return { 266 | $: { id: measureModeData.id }, 267 | '工位': measureModeData.workplaces?.map(workplaceData => { 268 | return { 269 | $: { id: workplaceData.id }, 270 | '字节流': workplaceData.byteStreams?.map(byteStreamData => { 271 | return { 272 | $: { 273 | send: byteStreamData.send, 274 | receive: byteStreamData.receive, 275 | } 276 | }; 277 | }), 278 | }; 279 | }), 280 | }; 281 | }), 282 | }; 283 | }), 284 | }; 285 | xmlModelData['配置'].push(xmlConfigData); 286 | } 287 | xmlProfileData['设备类别']['型号'].push(xmlModelData); 288 | } 289 | 290 | return xmlProfileData; 291 | } 292 | 293 | /** 294 | * 将配置文件数据适配为流程图数据 295 | */ 296 | export function adaptorIn(profileData: ProfileData): LogicFlow.GraphData { 297 | const graphData: LogicFlow.GraphData = { 298 | nodes: [], 299 | edges: [], 300 | }; 301 | 302 | const instrumentNode = { 303 | id: uuidv4(), 304 | type: 'instrument-node', 305 | x: 0, 306 | y: 0, 307 | properties: { 308 | type: NodeType.Instrument, 309 | id: profileData.id, 310 | } 311 | }; 312 | 313 | graphData.nodes.push(instrumentNode); 314 | 315 | for (const modelData of profileData.models ?? []) { 316 | const modelNode = { 317 | id: uuidv4(), 318 | type: 'model-node', 319 | x: 0, 320 | y: 0, 321 | properties: { 322 | type: NodeType.Model, 323 | parentId: instrumentNode.id, 324 | id: modelData.id, 325 | configType: modelData.configType, 326 | } 327 | }; 328 | 329 | graphData.nodes.push(modelNode); 330 | graphData.edges.push(initEdgeData(instrumentNode.id, modelNode.id)); 331 | 332 | if (modelData['NI-VISA']) { 333 | const configNode = { 334 | id: uuidv4(), 335 | type: 'config-node', 336 | x: 0, 337 | y: 0, 338 | properties: { 339 | type: NodeType.Config, 340 | parentId: modelNode.id, 341 | ...initConfigNodeData(), 342 | id: modelData['NI-VISA'].id, 343 | } 344 | }; 345 | 346 | graphData.nodes.push(configNode); 347 | graphData.edges.push(initEdgeData(modelNode.id, configNode.id)); 348 | 349 | for (const operationData of modelData['NI-VISA'].operations ?? []) { 350 | const operationNode = { 351 | id: uuidv4(), 352 | type: 'operation-node', 353 | x: 0, 354 | y: 0, 355 | properties: { 356 | type: NodeType.NI_VISA_OPERATION, 357 | parentId: configNode.id, 358 | ...initOperationNodeData(), 359 | id: operationData.id, 360 | parameter: operationData.parameter, 361 | hasReturn: operationData.hasReturn, 362 | command: operationData.command, 363 | } 364 | }; 365 | 366 | graphData.nodes.push(operationNode); 367 | graphData.edges.push(initEdgeData(configNode.id, operationNode.id)); 368 | } 369 | } 370 | 371 | if (modelData['FUNCTION']) { 372 | const configNode = { 373 | id: uuidv4(), 374 | type: 'config-node', 375 | x: 0, 376 | y: 0, 377 | properties: { 378 | type: NodeType.Config, 379 | parentId: modelNode.id, 380 | ...initConfigNodeData(), 381 | id: modelData.FUNCTION.id, 382 | spaceName: modelData.FUNCTION.spaceName, 383 | className: modelData.FUNCTION.className, 384 | dllTemplate: modelData.FUNCTION.dllTemplate, 385 | isVisa: modelData.FUNCTION.isVisa, 386 | } 387 | }; 388 | 389 | graphData.nodes.push(configNode); 390 | graphData.edges.push(initEdgeData(modelNode.id, configNode.id)); 391 | 392 | for (const operationData of modelData.FUNCTION.operations ?? []) { 393 | const operationNode = { 394 | id: uuidv4(), 395 | type: 'operation-node', 396 | x: 0, 397 | y: 0, 398 | properties: { 399 | type: NodeType.FUNCTION_OPERATION, 400 | parentId: configNode.id, 401 | ...initOperationNodeData(), 402 | id: operationData.id, 403 | parameter: operationData.parameter, 404 | hasReturn: operationData.hasReturn, 405 | methods: operationData.methods, 406 | } 407 | }; 408 | 409 | graphData.nodes.push(operationNode); 410 | graphData.edges.push(initEdgeData(configNode.id, operationNode.id)); 411 | } 412 | } 413 | 414 | if (modelData['CUSTOM']) { 415 | const configNode = { 416 | id: uuidv4(), 417 | type: 'config-node', 418 | x: 0, 419 | y: 0, 420 | properties: { 421 | type: NodeType.Config, 422 | parentId: modelNode.id, 423 | ...initConfigNodeData(), 424 | id: modelData.CUSTOM.id, 425 | communicationType: modelData.CUSTOM.communicationType, 426 | communicationConfig: modelData.CUSTOM.communicationConfig, 427 | } 428 | }; 429 | 430 | graphData.nodes.push(configNode); 431 | graphData.edges.push(initEdgeData(modelNode.id, configNode.id)); 432 | 433 | for (const operationData of modelData.CUSTOM.operations ?? []) { 434 | const operationNode = { 435 | id: uuidv4(), 436 | type: 'operation-node', 437 | x: 0, 438 | y: 0, 439 | properties: { 440 | type: NodeType.CUSTOM_OPERATION, 441 | parentId: configNode.id, 442 | ...initOperationNodeData(), 443 | id: operationData.id, 444 | measureModes: operationData.measureModes, 445 | } 446 | }; 447 | 448 | graphData.nodes.push(operationNode); 449 | graphData.edges.push(initEdgeData(configNode.id, operationNode.id)); 450 | } 451 | } 452 | } 453 | 454 | return graphData; 455 | } 456 | 457 | function initEdgeData(sourceNodeId: string, targetNodeId: string): LogicFlow.EdgeData { 458 | return { 459 | id: uuidv4(), 460 | type: 'polyline', 461 | sourceNodeId, 462 | targetNodeId, 463 | startPoint: { x: NODE_WIDTH_HALF, y: 0 }, 464 | endPoint: { x: -NODE_WIDTH_HALF, y: 0 }, 465 | }; 466 | } 467 | 468 | /** 469 | * 将流程图数据适配为配置文件数据 470 | */ 471 | export function adaptorOut(graphData: LogicFlow.GraphData): ProfileData { 472 | const nodeMap = new Map(); 473 | let startNode = graphData.nodes[0]; 474 | graphData.nodes.forEach((node) => { 475 | const parentId = node.properties!.parentId; 476 | if (parentId) { 477 | if (nodeMap.has(parentId)) { 478 | nodeMap.get(parentId)!.push(node); 479 | } else { 480 | nodeMap.set(parentId, [node]); 481 | } 482 | } 483 | if (node.type === 'instrument-node') { 484 | startNode = node; 485 | } 486 | }); 487 | 488 | const profileData: ProfileData = { 489 | id: startNode.properties!.id, 490 | models: [], 491 | }; 492 | 493 | nodeMap.get(startNode.id)?.forEach(modelNode => { 494 | const modelData: ModelData = { 495 | id: modelNode.properties!.id, 496 | configType: modelNode.properties!.configType, 497 | }; 498 | 499 | nodeMap.get(modelNode.id)?.forEach(configNode => { 500 | if (configNode.properties!.id === 'NI-VISA' && !modelData['NI-VISA']) { 501 | const configData: ModelData['NI-VISA'] = { 502 | id: 'NI-VISA', 503 | operations: [], 504 | }; 505 | 506 | nodeMap.get(configNode.id)?.forEach(operationNode => { 507 | configData.operations.push({ 508 | id: operationNode.properties!.id, 509 | parameter: operationNode.properties!.parameter, 510 | hasReturn: operationNode.properties!.hasReturn, 511 | command: operationNode.properties!.command, 512 | }); 513 | }); 514 | 515 | modelData['NI-VISA'] = configData; 516 | } else if (configNode.properties!.id === 'FUNCTION' && !modelData.FUNCTION) { 517 | const configData: ModelData['FUNCTION'] = { 518 | id: 'FUNCTION', 519 | spaceName: configNode.properties!.spaceName, 520 | className: configNode.properties!.className, 521 | dllTemplate: configNode.properties!.dllTemplate, 522 | isVisa: configNode.properties!.isVisa, 523 | operations: [], 524 | }; 525 | 526 | nodeMap.get(configNode.id)?.forEach(operationNode => { 527 | configData.operations.push({ 528 | id: operationNode.properties!.id, 529 | parameter: operationNode.properties!.parameter, 530 | hasReturn: operationNode.properties!.hasReturn, 531 | methods: operationNode.properties!.methods, 532 | }); 533 | }); 534 | 535 | modelData.FUNCTION = configData; 536 | } else if (configNode.properties!.id === 'CUSTOM' && !modelData.CUSTOM) { 537 | const configData: ModelData['CUSTOM'] = { 538 | id: 'CUSTOM', 539 | communicationType: configNode.properties!.communicationType, 540 | communicationConfig: configNode.properties!.communicationConfig, 541 | operations: [], 542 | }; 543 | 544 | nodeMap.get(configNode.id)?.forEach(operationNode => { 545 | const operationData = { 546 | id: operationNode.properties!.id, 547 | measureModes: operationNode.properties!.measureModes, 548 | }; 549 | configData.operations.push(operationData); 550 | }); 551 | 552 | modelData.CUSTOM = configData; 553 | } 554 | }); 555 | 556 | profileData.models.push(modelData); 557 | }); 558 | 559 | return profileData; 560 | } 561 | --------------------------------------------------------------------------------