Browse Source

first commit

main
betaqi 2 months ago
commit
fd6efc86ef
  1. 3
      .env
  2. 3
      .env.cloud
  3. 3
      .env.local
  4. 29
      .gitignore
  5. 0
      .idea/.gitignore
  6. 12
      .idea/EDFS-MCU.iml
  7. 8
      .idea/GitCommitMessageStorage.xml
  8. 6
      .idea/copilot.data.migration.agent.xml
  9. 6
      .idea/copilot.data.migration.ask.xml
  10. 6
      .idea/copilot.data.migration.ask2agent.xml
  11. 6
      .idea/copilot.data.migration.edit.xml
  12. 8
      .idea/dictionaries/project.xml
  13. 15
      .idea/git_toolbox_prj.xml
  14. 6
      .idea/inspectionProfiles/Project_Default.xml
  15. 12
      .idea/material_theme_project_new.xml
  16. 8
      .idea/modules.xml
  17. 6
      .idea/vcs.xml
  18. 477
      .idea/workspace.xml
  19. 33
      README.md
  20. 78
      global.types/auto-imports.d.ts
  21. 32
      global.types/components.d.ts
  22. 8
      global.types/env.d.ts
  23. 60
      global.types/global.d.ts
  24. 16
      index.html
  25. 8238
      package-lock.json
  26. 57
      package.json
  27. BIN
      public/favicon.ico
  28. 11
      src/App.vue
  29. 14
      src/api/Keys.ts
  30. 5
      src/api/basic/errorCode.ts
  31. 6
      src/api/basic/httpTypes.ts
  32. 27
      src/api/basic/utils.ts
  33. 0
      src/api/modkJs/engineering/index.ts
  34. 5
      src/api/module/engineering/index.d.ts
  35. 0
      src/api/module/engineering/index.ts
  36. 6
      src/api/module/index.ts
  37. 238
      src/api/server/axiosInstance.ts
  38. 18
      src/api/server/config.ts
  39. BIN
      src/assets/images/device/bms.png
  40. BIN
      src/assets/images/device/cac.png
  41. BIN
      src/assets/images/device/dgs.png
  42. BIN
      src/assets/images/device/ecg.png
  43. BIN
      src/assets/images/device/ecu.png
  44. BIN
      src/assets/images/device/em.png
  45. BIN
      src/assets/images/device/ems.png
  46. BIN
      src/assets/images/device/ffs.png
  47. BIN
      src/assets/images/device/group.png
  48. BIN
      src/assets/images/device/mppt.png
  49. BIN
      src/assets/images/device/pcs.png
  50. BIN
      src/assets/images/device/ths.png
  51. BIN
      src/assets/images/device/tms.png
  52. BIN
      src/assets/images/device/unit.png
  53. BIN
      src/assets/images/device/wpp.png
  54. 5
      src/assets/styles/global.module.scss
  55. 15
      src/assets/styles/main.css
  56. 77
      src/assets/styles/mixins.scss
  57. 74
      src/assets/styles/theme-variables.css
  58. 154
      src/components/Edfs-Input.vue
  59. 67
      src/components/Edfs-button.vue
  60. 108
      src/components/Edfs-dialog.vue
  61. 151
      src/components/Edfs-number-input.vue
  62. 11
      src/components/Edfs-table/defaults.ts
  63. 213
      src/components/Edfs-table/index.vue
  64. 224
      src/components/Edfs-wrap.vue
  65. 140
      src/composables/useMessage.ts
  66. 41
      src/composables/useTheme.ts
  67. 175
      src/composables/useZMQJsonWorker.ts
  68. 103
      src/hooks/useG6/index.ts
  69. 9
      src/hooks/useG6/useG6.d.ts
  70. 49
      src/hooks/useG6/utils/GenerateGraphData.ts
  71. 64
      src/hooks/useG6/utils/MyLineEdge.ts
  72. 4
      src/hooks/useG6/utils/index.ts
  73. 1
      src/lib/awesome-qr.js
  74. 14
      src/lib/zmq/zmqClient.d.ts
  75. 10
      src/lib/zmq/zmqClient.d.ts.map
  76. 118
      src/lib/zmq/zmqClient.js
  77. 10
      src/lib/zmq/zmqClient.js.map
  78. 14
      src/main.ts
  79. 34
      src/router/index.ts
  80. 270
      src/stores/transferData.ts
  81. 5
      src/types/device.ts
  82. 53
      src/uno-preset/src/index.ts
  83. 117
      src/utils/is.ts
  84. 174
      src/utils/zmq.ts
  85. 166
      src/utils/zmqJsonWorker.ts
  86. 19
      src/views/engineering/index.vue
  87. 189
      src/views/layout/index.vue
  88. 26
      tsconfig.app.json
  89. 20
      tsconfig.json
  90. 21
      tsconfig.node.json
  91. 1
      tsconfig.tsbuildinfo
  92. 43
      uno.config.js
  93. 51
      uno.config.ts
  94. 82
      vite.config.ts

3
.env

@ -0,0 +1,3 @@
VITE_BASE_API = '/remoteServer'
VITE_SHOW_ONLINE_DEVICE = true
VITE_APP_ENV = local

3
.env.cloud

@ -0,0 +1,3 @@
VITE_APP_ENV = cloud
VITE_BASE_URL = 'http://192.168.1.3:8080'
VITE_ZMQ_BASE_URL = '192.168.1.99'

3
.env.local

@ -0,0 +1,3 @@
VITE_APP_ENV = local
VITE_BASE_URL = 'http://192.168.1.210:8080'
VITE_ZMQ_BASE_URL = '192.168.1.210'

29
.gitignore vendored

@ -0,0 +1,29 @@
.DS_Store
.vscode
# package manager log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.npm
# eslint cache
.eslintcache
# stylelint cache
.stylelintcache
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/dist
/dist-local
/dist-cloud
/docker_output/
/node_modules/
/*.tar

0
.idea/.gitignore vendored

12
.idea/EDFS-MCU.iml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/GitCommitMessageStorage.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitCommitMessageStorage">
<option name="messageStorage">
<MessageStorage />
</option>
</component>
</project>

6
.idea/copilot.data.migration.agent.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.ask.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.ask2agent.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.edit.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EditMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

8
.idea/dictionaries/project.xml

@ -0,0 +1,8 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>antv</w>
<w>mingcute</w>
</words>
</dictionary>
</component>

15
.idea/git_toolbox_prj.xml

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>

6
.idea/inspectionProfiles/Project_Default.xml

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="D" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
</profile>
</component>

12
.idea/material_theme_project_new.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-6d6b07ab:1980cbf3ff4:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
</project>

8
.idea/modules.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/EDFS-MCU.iml" filepath="$PROJECT_DIR$/.idea/EDFS-MCU.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

477
.idea/workspace.xml

@ -0,0 +1,477 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="1a5a317b-2539-4ca3-a13a-6db181725745" name="Changes" comment="feat: 样式优化" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="TypeScript File" />
<option value="Vue Composition API Component" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="main-testG6" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitToolBoxStore">
<option name="recentBranches">
<RecentBranches>
<option name="branchesForRepo">
<list>
<RecentBranchesForRepo>
<option name="branches">
<list>
<RecentBranch>
<option name="branchName" value="main" />
<option name="lastUsedInstant" value="1762340990" />
</RecentBranch>
<RecentBranch>
<option name="branchName" value="main-testG6" />
<option name="lastUsedInstant" value="1762333705" />
</RecentBranch>
</list>
</option>
<option name="repositoryRootUrl" value="file://$PROJECT_DIR$" />
</RecentBranchesForRepo>
</list>
</option>
</RecentBranches>
</option>
</component>
<component name="HighlightingSettingsPerFile">
<setting file="file://$USER_HOME$/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/wxid_0wujhfg3eau322_5d75/msg/file/2025-11/1.json" root0="FORCE_HIGHLIGHTING" />
</component>
<component name="ProblemsViewState">
<option name="selectedTabId" value="CurrentFile" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 1
}</component>
<component name="ProjectId" id="33Pn17EFffQH46OlqwOKF3K4cpA" />
<component name="ProjectViewState">
<option name="autoscrollFromSource" value="true" />
<option name="autoscrollToSource" value="true" />
<option name="hideEmptyMiddlePackages" value="true" />
<option name="openDirectoriesWithSingleClick" value="true" />
<option name="showLibraryContents" value="true" />
<option name="showMembers" value="true" />
<option name="showModules" value="false" />
<option name="showScratchesAndConsoles" value="false" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ASKED_MARK_IGNORED_FILES_AS_EXCLUDED": "true",
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultAutoModeForALLUsers.v1": "true",
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
"git-widget-placeholder": "main",
"junie.onboarding.icon.badge.shown": "true",
"last_opened_file_path": "/Users/taqibe/worker/EDFS-EPM",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "editing.templates",
"to.speed.mode.migration.done": "true",
"ts.external.directory.path": "/Users/taqibe/worker/EDFS-EPM/node_modules/typescript/lib",
"vue.rearranger.settings.migration": "true"
},
"keyToStringList": {
"vue.recent.templates": [
"Vue Composition API Component"
]
}
}]]></component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/src/views" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/src/components" />
</key>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-3aa1da707db6-JavaScript-WS-252.27397.92" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="1a5a317b-2539-4ca3-a13a-6db181725745" name="Changes" comment="" />
<created>1759227356966</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1759227356966</updated>
<workItem from="1759227358304" duration="1023000" />
<workItem from="1759228390368" duration="40000" />
<workItem from="1759228549846" duration="11000" />
<workItem from="1759230772741" duration="89000" />
<workItem from="1759989220548" duration="176000" />
<workItem from="1759989551899" duration="170000" />
<workItem from="1759989746585" duration="292000" />
<workItem from="1759990048708" duration="78000" />
<workItem from="1759990136521" duration="3232000" />
<workItem from="1759993446079" duration="487000" />
<workItem from="1759994018535" duration="3043000" />
<workItem from="1759997133848" duration="14412000" />
<workItem from="1760336647412" duration="244000" />
<workItem from="1760431007053" duration="3049000" />
<workItem from="1760509810280" duration="10929000" />
<workItem from="1761284911195" duration="9696000" />
<workItem from="1761530640193" duration="3291000" />
<workItem from="1761558310679" duration="2000" />
<workItem from="1761632345080" duration="225000" />
<workItem from="1761632681106" duration="1485000" />
<workItem from="1761643503658" duration="19000" />
<workItem from="1761644243506" duration="709000" />
<workItem from="1761649848303" duration="1426000" />
<workItem from="1761710435976" duration="607000" />
<workItem from="1761789282293" duration="5005000" />
<workItem from="1761817047786" duration="316000" />
<workItem from="1761817569420" duration="353000" />
<workItem from="1761817939264" duration="2782000" />
<workItem from="1762136843058" duration="1324000" />
<workItem from="1762138186028" duration="1264000" />
<workItem from="1762139581166" duration="11368000" />
<workItem from="1762155421774" duration="11124000" />
<workItem from="1762227571451" duration="42233000" />
<workItem from="1762506557322" duration="7100000" />
<workItem from="1762840881699" duration="7799000" />
<workItem from="1762876079079" duration="617000" />
<workItem from="1762935965088" duration="57732000" />
<workItem from="1763346319714" duration="475000" />
<workItem from="1763347210197" duration="7708000" />
<workItem from="1763520819502" duration="1260000" />
<workItem from="1763537899229" duration="583000" />
<workItem from="1763539836768" duration="598000" />
<workItem from="1763543600806" duration="4000" />
<workItem from="1763543655623" duration="1880000" />
<workItem from="1763610355529" duration="670000" />
<workItem from="1763611329249" duration="1960000" />
</task>
<task id="LOCAL-00001" summary="fix: 一些调整">
<option name="closed" value="true" />
<created>1759227435333</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1759227435333</updated>
</task>
<task id="LOCAL-00002" summary="fix: 一些调整">
<option name="closed" value="true" />
<created>1759227945246</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1759227945246</updated>
</task>
<task id="LOCAL-00003" summary="feat: 云端导入增加导入方式">
<option name="closed" value="true" />
<created>1759989710232</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1759989710232</updated>
</task>
<task id="LOCAL-00004" summary="feat: 在线设备根据站点区分">
<option name="closed" value="true" />
<created>1759998954540</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1759998954540</updated>
</task>
<task id="LOCAL-00005" summary="feat: 迁移drawer loading 逻辑修改">
<option name="closed" value="true" />
<created>1760178915059</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1760178915059</updated>
</task>
<task id="LOCAL-00006" summary="feat: 增加设备类型并增加icon">
<option name="closed" value="true" />
<created>1761531297313</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1761531297313</updated>
</task>
<task id="LOCAL-00007" summary="feat: emu 设备数据点位值保留小数修改">
<option name="closed" value="true" />
<created>1761632856036</created>
<option name="number" value="00007" />
<option name="presentableId" value="LOCAL-00007" />
<option name="project" value="LOCAL" />
<updated>1761632856036</updated>
</task>
<task id="LOCAL-00008" summary="feat: emu 设备批量选择增加全选">
<option name="closed" value="true" />
<created>1761792036623</created>
<option name="number" value="00008" />
<option name="presentableId" value="LOCAL-00008" />
<option name="project" value="LOCAL" />
<updated>1761792036623</updated>
</task>
<task id="LOCAL-00009" summary="feat: 点位数据不显示数据为空的">
<option name="closed" value="true" />
<created>1761817754637</created>
<option name="number" value="00009" />
<option name="presentableId" value="LOCAL-00009" />
<option name="project" value="LOCAL" />
<updated>1761817754637</updated>
</task>
<task id="LOCAL-00010" summary="feat: 使用时间对齐作为折线图x轴">
<option name="closed" value="true" />
<created>1762136923706</created>
<option name="number" value="00010" />
<option name="presentableId" value="LOCAL-00010" />
<option name="project" value="LOCAL" />
<updated>1762136923706</updated>
</task>
<task id="LOCAL-00011" summary="fix: 导入导出名称修正">
<option name="closed" value="true" />
<created>1762141831374</created>
<option name="number" value="00011" />
<option name="presentableId" value="LOCAL-00011" />
<option name="project" value="LOCAL" />
<updated>1762141831374</updated>
</task>
<task id="LOCAL-00012" summary="feat: wrap组件收缩功能">
<option name="closed" value="true" />
<created>1762154304120</created>
<option name="number" value="00012" />
<option name="presentableId" value="LOCAL-00012" />
<option name="project" value="LOCAL" />
<updated>1762154304120</updated>
</task>
<task id="LOCAL-00013" summary="feat: 设备数据侧边设备点位收缩功能">
<option name="closed" value="true" />
<created>1762154365707</created>
<option name="number" value="00013" />
<option name="presentableId" value="LOCAL-00013" />
<option name="project" value="LOCAL" />
<updated>1762154365707</updated>
</task>
<task id="LOCAL-00014" summary="feat: 样式修改">
<option name="closed" value="true" />
<created>1762156981920</created>
<option name="number" value="00014" />
<option name="presentableId" value="LOCAL-00014" />
<option name="project" value="LOCAL" />
<updated>1762156981920</updated>
</task>
<task id="LOCAL-00015" summary="feat: 任务列表状态实时更新逻辑">
<option name="closed" value="true" />
<created>1762162249095</created>
<option name="number" value="00015" />
<option name="presentableId" value="LOCAL-00015" />
<option name="project" value="LOCAL" />
<updated>1762162249095</updated>
</task>
<task id="LOCAL-00016" summary="fix: 导入导出名称修正">
<option name="closed" value="true" />
<created>1762227756908</created>
<option name="number" value="00016" />
<option name="presentableId" value="LOCAL-00016" />
<option name="project" value="LOCAL" />
<updated>1762227756908</updated>
</task>
<task id="LOCAL-00017" summary="fix: 平台名称修改">
<option name="closed" value="true" />
<created>1762321409388</created>
<option name="number" value="00017" />
<option name="presentableId" value="LOCAL-00017" />
<option name="project" value="LOCAL" />
<updated>1762321409388</updated>
</task>
<task id="LOCAL-00018" summary="fix: ZMQ 多个消息订阅统一个主题时回调函数被覆盖问题">
<option name="closed" value="true" />
<created>1762321502585</created>
<option name="number" value="00018" />
<option name="presentableId" value="LOCAL-00018" />
<option name="project" value="LOCAL" />
<updated>1762321502585</updated>
</task>
<task id="LOCAL-00019" summary="fix: 任务状态更新修正">
<option name="closed" value="true" />
<created>1762321564102</created>
<option name="number" value="00019" />
<option name="presentableId" value="LOCAL-00019" />
<option name="project" value="LOCAL" />
<updated>1762321564102</updated>
</task>
<task id="LOCAL-00020" summary="feat: 点位数据tooltip 增加单位">
<option name="closed" value="true" />
<created>1762325331961</created>
<option name="number" value="00020" />
<option name="presentableId" value="LOCAL-00020" />
<option name="project" value="LOCAL" />
<updated>1762325331961</updated>
</task>
<task id="LOCAL-00021" summary="feat: 数据精确到毫秒">
<option name="closed" value="true" />
<created>1762329140928</created>
<option name="number" value="00021" />
<option name="presentableId" value="LOCAL-00021" />
<option name="project" value="LOCAL" />
<updated>1762329140928</updated>
</task>
<task id="LOCAL-00022" summary="feat: 任务zmq回显调整">
<option name="closed" value="true" />
<created>1762481889492</created>
<option name="number" value="00022" />
<option name="presentableId" value="LOCAL-00022" />
<option name="project" value="LOCAL" />
<updated>1762481889492</updated>
</task>
<task id="LOCAL-00023" summary="feat: 任务zmq回显调整">
<option name="closed" value="true" />
<created>1762484638850</created>
<option name="number" value="00023" />
<option name="presentableId" value="LOCAL-00023" />
<option name="project" value="LOCAL" />
<updated>1762484638850</updated>
</task>
<task id="LOCAL-00024" summary="feat: 一些调整">
<option name="closed" value="true" />
<created>1762508208321</created>
<option name="number" value="00024" />
<option name="presentableId" value="LOCAL-00024" />
<option name="project" value="LOCAL" />
<updated>1762508208321</updated>
</task>
<task id="LOCAL-00025" summary="feat: 优化折线图性能">
<option name="closed" value="true" />
<created>1762942198582</created>
<option name="number" value="00025" />
<option name="presentableId" value="LOCAL-00025" />
<option name="project" value="LOCAL" />
<updated>1762942198582</updated>
</task>
<task id="LOCAL-00026" summary="feat: 增加查询最近">
<option name="closed" value="true" />
<created>1763013063694</created>
<option name="number" value="00026" />
<option name="presentableId" value="LOCAL-00026" />
<option name="project" value="LOCAL" />
<updated>1763013063694</updated>
</task>
<task id="LOCAL-00027" summary="feat: 点位数据增加并发请求">
<option name="closed" value="true" />
<created>1763110405491</created>
<option name="number" value="00027" />
<option name="presentableId" value="LOCAL-00027" />
<option name="project" value="LOCAL" />
<updated>1763110405491</updated>
</task>
<task id="LOCAL-00028" summary="feat: 请求进度增加滚动">
<option name="closed" value="true" />
<created>1763347443465</created>
<option name="number" value="00028" />
<option name="presentableId" value="LOCAL-00028" />
<option name="project" value="LOCAL" />
<updated>1763347443465</updated>
</task>
<task id="LOCAL-00029" summary="feat: 点位数据空值不不处理">
<option name="closed" value="true" />
<created>1763349111621</created>
<option name="number" value="00029" />
<option name="presentableId" value="LOCAL-00029" />
<option name="project" value="LOCAL" />
<updated>1763349111621</updated>
</task>
<task id="LOCAL-00030" summary="feat: topo 点位Total 调整">
<option name="closed" value="true" />
<created>1763349274662</created>
<option name="number" value="00030" />
<option name="presentableId" value="LOCAL-00030" />
<option name="project" value="LOCAL" />
<updated>1763349274662</updated>
</task>
<task id="LOCAL-00031" summary="feat: 样式优化">
<option name="closed" value="true" />
<created>1763358207970</created>
<option name="number" value="00031" />
<option name="presentableId" value="LOCAL-00031" />
<option name="project" value="LOCAL" />
<updated>1763358207970</updated>
</task>
<option name="localTasksCounter" value="32" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
<option name="exactExcludedFiles">
<list>
<option value="$PROJECT_DIR$/uno.config.js" />
</list>
</option>
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="feat: 迁移drawer loading 逻辑修改" />
<MESSAGE value="feat: 增加设备类型并增加icon" />
<MESSAGE value="feat: emu 设备数据点位值保留小数修改" />
<MESSAGE value="feat: emu 设备批量选择增加全选" />
<MESSAGE value="feat: 点位数据不显示数据为空的" />
<MESSAGE value="feat: 使用时间对齐作为折线图x轴" />
<MESSAGE value="feat: wrap组件收缩功能" />
<MESSAGE value="feat: 设备数据侧边设备点位收缩功能" />
<MESSAGE value="feat: 样式修改" />
<MESSAGE value="feat: 任务列表状态实时更新逻辑" />
<MESSAGE value="fix: 导入导出名称修正" />
<MESSAGE value="fix: 平台名称修改" />
<MESSAGE value="fix: ZMQ 多个消息订阅统一个主题时回调函数被覆盖问题" />
<MESSAGE value="fix: 任务状态更新修正" />
<MESSAGE value="feat: 点位数据tooltip 增加单位" />
<MESSAGE value="feat: 数据精确到毫秒" />
<MESSAGE value="feat: 任务zmq回显调整" />
<MESSAGE value="feat: 一些调整" />
<MESSAGE value="feat: 优化折线图性能" />
<MESSAGE value="feat: 增加查询最近" />
<MESSAGE value="feat: 点位数据增加并发请求" />
<MESSAGE value="feat: 请求进度增加滚动" />
<MESSAGE value="feat: 点位数据空值不不处理" />
<MESSAGE value="feat: topo 点位Total 调整" />
<MESSAGE value="feat: 样式优化" />
<option name="LAST_COMMIT_MESSAGE" value="feat: 样式优化" />
</component>
<component name="github-copilot-workspace">
<instructionFileLocations>
<option value=".github/instructions" />
</instructionFileLocations>
<promptFileLocations>
<option value=".github/prompts" />
</promptFileLocations>
</component>
</project>

33
README.md

@ -0,0 +1,33 @@
# vue-project
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```

78
global.types/auto-imports.d.ts vendored

@ -0,0 +1,78 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const ElLoading: typeof import('element-plus/es')['ElLoading']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const effectScope: typeof import('vue')['effectScope']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useModel: typeof import('vue')['useModel']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

32
global.types/components.d.ts vendored

@ -0,0 +1,32 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
EdfsButton: typeof import('./../src/components/Edfs-button.vue')['default']
EdfsDialog: typeof import('./../src/components/Edfs-dialog.vue')['default']
EdfsInput: typeof import('./../src/components/Edfs-Input.vue')['default']
EdfsNumberInput: typeof import('./../src/components/Edfs-number-input.vue')['default']
EdfsTable: typeof import('./../src/components/Edfs-table/index.vue')['default']
EdfsWrap: typeof import('./../src/components/Edfs-wrap.vue')['default']
ElAside: typeof import('element-plus/es')['ElAside']
ElButton: typeof import('element-plus/es')['ElButton']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface GlobalDirectives {
vInfiniteScroll: typeof import('element-plus/es')['ElInfiniteScroll']
}
}

8
global.types/env.d.ts vendored

@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
declare module "element-plus/dist/locale/zh-cn.mjs";

60
global.types/global.d.ts vendored

@ -0,0 +1,60 @@
export { }
declare global {
interface Fn<T = any> {
(...arg: T[]): T
}
type Nullable<T> = T | null
type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T>
type Recordable<T = any> = Record<string, T>
type ComponentRef<T extends abstract new (...args: any) => any> = InstanceType<T>
type LocaleType = 'zh-CN' | 'en'
type AxiosHeaders =
| 'application/json'
| 'application/x-www-form-urlencoded'
| 'multipart/form-data'
type AxiosMethod = 'get' | 'post' | 'delete' | 'put' | 'GET' | 'POST' | 'DELETE' | 'PUT'
type AxiosResponseType =
| 'arraybuffer'
| 'blob'
| 'document'
| 'json'
| 'text'
| 'stream'
interface AxiosConfig {
params?: any
data?: any
url?: string
method?: AxiosMethod
headersType?: string
responseType?: AxiosResponseType
}
interface IResponse<T = any> {
code: string
data: T extends any ? T : T & any
}
interface PageParam {
pageSize?: number
pageNo?: number
}
interface Tree {
id: number
name: string
children?: Tree[] | any[]
}
// 分页数据公共返回
interface PageResult<T> {
list: T // 数据
total: number // 总量
}
}

16
index.html

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

8238
package-lock.json generated

File diff suppressed because it is too large Load Diff

57
package.json

@ -0,0 +1,57 @@
{
"name": "vue-project",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"preview": "vite preview",
"build-only": "vite build",
"build": "vite build",
"type-check": "vue-tsc --build"
},
"dependencies": {
"@antv/g": "^6.1.28",
"@antv/g-svg": "^2.0.42",
"@antv/g6": "^5.0.49",
"@types/qs": "^6.9.18",
"@unocss/reset": "^66.0.0",
"axios": "^1.8.4",
"dayjs": "^1.11.13",
"dexie": "^4.0.11",
"echarts": "^5.6.0",
"element-plus": "^2.9.5",
"g6-extension-vue": "^0.1.0",
"jszmq": "^0.1.2",
"lodash-es": "^4.17.21",
"mockjs": "^1.1.0",
"p-limit": "^7.2.0",
"pinia": "^3.0.1",
"qs": "^6.14.0",
"uuid": "^11.1.0",
"vue": "^3.5.13",
"vue-echarts": "^7.0.3",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@iconify/json": "^2.2.310",
"@tsconfig/node22": "^22.0.0",
"@types/node": "^24.2.1",
"@unocss/preset-icons": "^66.0.0",
"@unocss/preset-rem-to-px": "^66.0.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^4.2.0",
"@vue/tsconfig": "^0.7.0",
"npm-run-all2": "^7.0.2",
"sass": "^1.85.0",
"typescript": "~5.7.3",
"unocss": "^66.0.0",
"unocss-preset-scalpel": "^1.2.7",
"unplugin-auto-import": "^19.1.0",
"unplugin-icons": "^22.1.0",
"unplugin-vue-components": "^28.4.0",
"vite": "^6.3.5",
"vite-plugin-vue-devtools": "^7.7.2",
"vue-tsc": "^2.2.2"
}
}

BIN
public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

11
src/App.vue

@ -0,0 +1,11 @@
<script setup lang="ts">
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
</script>
<template>
<el-config-provider :locale="zhCn">
<RouterView/>
</el-config-provider>
</template>
<style scoped lang="scss"></style>

14
src/api/Keys.ts

@ -0,0 +1,14 @@
export default class Keys {
public static STORAGE_UUID = 'uuid'
public static STORAGE_TENANT_ID = 'tenant'
public static STORAGE_TOKEN = 'token'
public static STORAGE_REFRESH_TOKEN = 'refreshToken'
public static STORAGE_USER_INFO = 'userInfo'
public static STORAGE_ROLE_ROUTERS = 'roleRouters'
public static STORAGE_DICT_CACHE = 'dict'
public static STORAGE_LANG = 'lang'
public static STORAGE_STATIONID = 'stationId'
public static STORAGE_THEME = 'EDFS-THEME'
public static CODE_SUCCEED = 0
}

5
src/api/basic/errorCode.ts

@ -0,0 +1,5 @@
export const errorCode: any = {
401: '认证失败,无法访问系统资源',
403: '当前操作没有权限',
404: '访问资源不存在',
}

6
src/api/basic/httpTypes.ts

@ -0,0 +1,6 @@
export interface Result<T> {
code: number
msg: string
status?: number
data: T
}

27
src/api/basic/utils.ts

@ -0,0 +1,27 @@
const authErrMap: any = {
'401': '认证失败,无法访问系统资源',
'403': '当前操作没有权限',
'404': '访问资源不存在',
default: '系统未知错误,请反馈给管理员',
}
const networkErrMap: { [key: number]: string } = {
400: '请求错误(400)',
401: '未授权,请重新登录(401)',
403: '拒绝访问(403)',
404: '请求出错(404)',
408: '请求超时(408)',
500: '服务器错误(500)',
501: '服务未实现(501)',
502: '网络错误(502)',
503: '服务不可用(503)',
504: '网络超时(504)',
505: 'HTTP版本不受支持(505)',
}
const ignoreMsgs = [
'无效的刷新令牌', // 刷新令牌被删除时,不用提示
'刷新令牌已过期', // 使用刷新令牌,刷新获取新的访问令牌时,结果因为过期失败,此时需要忽略。否则,会导致继续 401,无法跳转到登出界面
]
export { authErrMap, networkErrMap, ignoreMsgs }

0
src/api/modkJs/engineering/index.ts

5
src/api/module/engineering/index.d.ts vendored

@ -0,0 +1,5 @@
export interface IEngineeringList {
id: string,
name: string,
description: string,
}

0
src/api/module/engineering/index.ts

6
src/api/module/index.ts

@ -0,0 +1,6 @@
import axiosInstance from '@/api/server/axiosInstance'
import { API_Config } from '@/api/server/config'
const globalServer = axiosInstance('global', API_Config.global)
export { globalServer }

238
src/api/server/axiosInstance.ts

@ -0,0 +1,238 @@
import type {
AxiosInstance,
AxiosRequestConfig,
AxiosRequestHeaders,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios'
import qs from 'qs'
import Keys from '../Keys'
import axios, { AxiosError, isCancel } from 'axios'
// import {
// getRefreshToken,
// getTenantId,
// getToken,
// removeToken,
// setToken,
// } from '@/utils/auth'
import { authErrMap, ignoreMsgs, networkErrMap } from '../basic/utils'
import type { Result } from '../basic/httpTypes'
import type { APIConfigKeys } from './config'
import { useRouter } from 'vue-router'
import { useMessage } from '@/composables/useMessage'
const router = useRouter()
const message = useMessage()
const basicHeader = {
tenant() {
// const tenant = getTenantId()
// if (tenant) return tenant
return ''
},
token() {
// const token = getToken()
// if (token) return 'Bearer ' + token
return ''
},
}
interface Config {
baseURL?: string
baseAPI: string
}
const instances: Record<string, AxiosInstance> = {}
let isRefreshToken = false // 是否正在刷新中
let requestList: any[] = [] // 请求队列
const createAxiosInstance = (module: APIConfigKeys, config: Config) => {
if (!config || !config.baseAPI) {
throw new Error(`Invalid configuration for module: ${module}`)
}
const { baseAPI } = config
if (!instances[module]) {
const instance = axios.create({
baseURL: `${baseAPI}`,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
instances[module] = instance
}
const service = instances[module]
service.interceptors.request.use(
(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
// config.headers.Authorization = basicHeader.token()
// config.headers['tenant-id'] = basicHeader.tenant()
const params = config.params || {}
const data = config.data || false
if (
config.method?.toUpperCase() === 'POST' &&
(config.headers as AxiosRequestHeaders)['Content-Type'] ===
'application/x-www-form-urlencoded'
) {
config.data = qs.stringify(data)
}
if (config.method?.toUpperCase() === 'GET' && params) {
config.params = {}
const paramsStr = qs.stringify(params, { allowDots: true })
if (paramsStr) {
config.url = config.url + '?' + paramsStr
}
}
return config
},
(error: AxiosError): Promise<AxiosError> => {
return Promise.reject(error)
}
)
service.interceptors.response.use(
async (res: AxiosResponse) => {
const config = res.config
let { data } = res
if (!res.data) {
throw new Error('返回“[HTTP]请求没有返回值”')
}
if (
res.request.responseType === 'blob' ||
res.request.responseType === 'arraybuffer'
) {
// 注意:如果导出的响应为 json,说明可能失败了,不直接返回进行下载
if (res.data.type !== 'application/json') {
return Promise.resolve({ code: res.status, data: res.data })
}
data = await new Response(res.data).json()
}
const code = data.code || 200
const msg = data.msg || authErrMap[code] || authErrMap['default']
if (ignoreMsgs.indexOf(msg) !== -1) {
// 如果是忽略的错误码,直接返回 msg 异常
return Promise.reject({ code: null, msg })
} else
// if (code === 401) {
// if (!isRefreshToken) {
// isRefreshToken = true
// if (!getRefreshToken()) logout()
// try {
// const refreshTokenRes = await refreshToken()
// if (refreshTokenRes.data.code !== 0) {
// logout()
// return Promise.reject({ code, msg })
// }
// setToken(refreshTokenRes.data.data.accessToken)
// config.headers!.Authorization = 'Bearer ' + getToken()
// requestList.forEach((cb: any) => {
// cb()
// })
// requestList = []
// // 重连接socket
// // openSocket()
// return service(config)
// } catch (e) {
// logout()
// return Promise.reject({ code, msg })
// } finally {
// requestList = []
// isRefreshToken = false
// }
// } else {
// return new Promise(resolve => {
// requestList.push(() => {
// config.headers!.Authorization = 'Bearer ' + getToken()
// resolve(service(config))
// })
// })
// }
// } else
if (code === 500) {
return Promise.reject({ code, msg })
} else if (code !== 200) {
// if (msg === '无效的刷新令牌') {
// // hard coding:忽略这个提示,直接登出
// console.log(msg)
// } else {
// ElMessage.error(msg)
// }
return Promise.reject({ code, msg })
}
return data
},
(error: any): Promise<any> => {
if (isCancel(error)) {
return Promise.resolve()
}
const message = '请求错误'
if (error.code === 'ECONNABORTED') {
return Promise.reject({ code: null, msg: '服务器响应超时' })
}
if (!error.response) {
return Promise.reject({ code: null, msg: message })
}
const status = error.response?.status
const unKnowError = `连接出错(${error.response.status})!`
if (status === 401) {
localStorage.removeItem(Keys.STORAGE_TOKEN)
router.push('/login')
}
const msg = networkErrMap[status] ? networkErrMap[status] : unKnowError
return Promise.reject({ code: status || null, msg: msg })
}
)
if (!service) {
throw new Error(`Failed to create Axios instance for module: ${module}`)
}
// const refreshToken = async () => {
// // axios.defaults.headers.common['tenant-id'] = getTenantId()
// return await axios.post(
// `${VITE_BASE_API_SYSTEM}/auth/refresh-token?refreshToken=` + getRefreshToken()
// )
// }
async function request<T = any>(config: AxiosRequestConfig): Promise<Result<T>> {
try {
const result = (await service(config)) as Result<T>
return result
} catch (err: any) {
console.log(err)
const result: Result<T> = {
code: err?.code || -1,
msg: err.msg || err.message,
data: err.data || null,
}
return result
}
}
return request
}
// let isShowLogout = false
// async function logout() {
// if (isShowLogout) return
// isShowLogout = true
// await message.forceConfirm('登录状态已失效,请重新登录', '系统提示', '重新登录')
// removeToken()
// isShowLogout = false
// window.location.href = '/login'
// }
export default createAxiosInstance

18
src/api/server/config.ts

@ -0,0 +1,18 @@
export interface APIConfig {
global: Config
}
interface Config {
baseURL?: string
baseAPI: string
}
const API_Config: APIConfig = {
global: {
baseAPI: import.meta.env.VITE_BASE_API,
},
}
export type APIConfigKeys = keyof typeof API_Config
export { API_Config }

BIN
src/assets/images/device/bms.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src/assets/images/device/cac.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
src/assets/images/device/dgs.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/assets/images/device/ecg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

BIN
src/assets/images/device/ecu.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/images/device/em.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
src/assets/images/device/ems.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/assets/images/device/ffs.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/images/device/group.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

BIN
src/assets/images/device/mppt.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src/assets/images/device/pcs.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/assets/images/device/ths.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
src/assets/images/device/tms.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
src/assets/images/device/unit.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/images/device/wpp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

5
src/assets/styles/global.module.scss

@ -0,0 +1,5 @@
// 导出变量
:export {
namespace: $namespace;
elNamespace: $elNamespace;
}

15
src/assets/styles/main.css

@ -0,0 +1,15 @@
@import 'element-plus/dist/index.css';
@import './theme-variables.css';
@import '@unocss/reset/tailwind.css';
html,
body {
@apply w-full h-full;
}
#app {
@apply wh-full;
}
.el-progress-bar__innerText {
@apply w-full text-center;
}

77
src/assets/styles/mixins.scss

@ -0,0 +1,77 @@
@use 'sass:math';
// 命名空间
$namespace: v;
// el命名空间
$elNamespace: el;
$W-scale-ratio-1440: math.div(1440, 1920);
$H-scale-ratio-900: math.div(900, 1080);
$W-scale-ratio-2560: math.div(2560, 1920);
$H-scale-ratio-1440: math.div(1440, 1080);
@mixin ResponsiveW($base-width) {
width: $base-width + px !important;
@media only screen and (max-width: 1440px) {
width: calc($W-scale-ratio-1440 * #{$base-width}px) !important;
}
@media only screen and (min-width: 2560px) {
width: calc($W-scale-ratio-2560 * #{$base-width}px) !important;
}
}
@mixin ResponsiveH($base-height) {
height: $base-height + px;
@media only screen and (max-width: 1440px) {
height: calc($H-scale-ratio-900 * #{$base-height}px);
}
@media only screen and (min-width: 2560px) {
height: calc($H-scale-ratio-1440 * #{$base-height}px);
}
}
@mixin ResponsiveFz($base-font-size) {
font-size: $base-font-size + px;
@media only screen and (max-width: 1440px) {
font-size: calc($W-scale-ratio-1440 * #{$base-font-size}px);
}
@media only screen and (min-width: 2560px) {
font-size: calc($W-scale-ratio-2560 * #{$base-font-size}px);
}
}
@mixin ResponsivePadding($top, $right, $bottom, $left) {
padding: $top + px $right + px $bottom + px $left + px;
@media only screen and (max-width: 1440px) {
padding: calc($H-scale-ratio-900 * #{$top}px) calc($W-scale-ratio-1440 * #{$right}px)
calc($H-scale-ratio-900 * #{$bottom}px) calc($W-scale-ratio-1440 * #{$left}px);
}
@media only screen and (min-width: 2560px) {
padding: calc($H-scale-ratio-1440 * #{$top}px) calc($W-scale-ratio-2560 * #{$right}px)
calc($H-scale-ratio-1440 * #{$bottom}px),
calc($W-scale-ratio-2560 * #{$left}px);
}
}
@mixin ResponsiveMargin($top, $right, $bottom, $left) {
margin: $top + px $right + px $bottom + px $left + px;
@media only screen and (max-width: 1440px) {
margin: calc($H-scale-ratio-900 * #{$top}px) calc($W-scale-ratio-1440 * #{$right}px)
calc($H-scale-ratio-900 * #{$bottom}px) calc($W-scale-ratio-1440 * #{$left}px);
}
@media only screen and (min-width: 2560px) {
margin: calc($H-scale-ratio-1440 * #{$top}px) calc($W-scale-ratio-2560 * #{$right}px)
calc($H-scale-ratio-1440 * #{$bottom}px),
calc($W-scale-ratio-2560 * #{$left}px);
}
}

74
src/assets/styles/theme-variables.css

@ -0,0 +1,74 @@
html[data-theme='dark'] {
--bg: #0d0d0d;
--text-color: #ffffff;
--layout-header-bg: #1b1d23;
--layout-menu-bg: #1b1d23;
--menu-active-bg: #2c342c;
--menu-hover-bg: #272b35;
--warp-bg: #212327;
--table-header-bg: #3b3d40;
--table-header-text-color: #cdcecf;
--pagination-bg: #101115;
--pagination-border-color: rgba(255, 255, 255, 0.13);
--select-header--text-color: #ffffff;
--station-card-bg: #23272f;
--station-card-border-color: #4b5361;
--station-header-bg: #363a40;
--station-header-text-color: #fff;
--station-info-val-text: #fff;
--label-color: #666;
--text-desc: #8d9095;
--mask-bg: #333;
--icon-color: #aaa;
--icon-hover-color: #606266;
}
html[data-theme='light'] {
--bg: #f1f2f6;
--text-color: #4d4d4d;
--layout-header-bg: #ffffff;
--layout-menu-bg: #fff;
--menu-active-bg: #f5fcee;
--menu-hover-bg: #f5f5f5;
--warp-bg: #ffffff;
--table-header-bg: #e8e9ee;
--table-header-text-color: #030303;
--pagination-bg: transparent;
--pagination-border-color: rgba(217, 217, 217, 1);
--select-header--text-color: #666;
--station-card-bg: #ffffff;
--station-card-border-color: #e9e9e9;
--station-header-bg: #f1f1f1;
--station-header-text-color: #030303;
--station-info-val-text: #4d4d4d;
--label-color: #666;
--text-desc: #a8abb2;
--mask-bg: #f5f5f5;
--icon-color: #aaa;
--icon-hover-color: #606266;
}
html.dark {
--el-bg-color: #212327;
--el-border-color: rgba(255, 255, 255, 0.13);
--el-button-divide-border-color: rgba(255, 255, 255, 0.13);
--el-bg-color-overlay: #212327;
--el-text-color-regular: #c7c8cb;
--el-color-primary: #619925;
--el-color-primary-light-3: rgb(78.1, 141.8, 46.6);
--el-color-primary-light-5: rgb(61.5, 107, 39);
--el-color-primary-light-7: rgb(44.9, 72.2, 31.4);
--el-color-primary-light-8: rgb(36.6, 54.8, 27.6);
--el-color-primary-light-9: rgb(28.3, 37.4, 23.8);
--el-color-primary-dark-2: rgb(133.4, 206.2, 97.4);
}
html.light {
--el-color-primary: #619925;
--el-color-primary-light-3: rgb(148.6, 212.3, 117.1);
--el-color-primary-light-5: rgb(179, 224.5, 156.5);
--el-color-primary-light-7: rgb(209.4, 236.7, 195.9);
--el-color-primary-light-8: rgb(224.6, 242.8, 215.6);
--el-color-primary-light-9: rgb(239.8, 248.9, 235.3);
--el-color-primary-dark-2: rgb(82.4, 155.2, 46.4);
}

154
src/components/Edfs-Input.vue

@ -0,0 +1,154 @@
<template>
<div class="additem-item-input-root">
<div class="label" :class="labelWidth">
<span v-if="props.require" class="require">*</span>
{{ label }}:
</div>
<el-input
v-model="localValue"
:type="type"
:disabled="!canEdit"
:show-password="showPassword"
class="input"
:placeholder="placeholder"
@input="inputChange"
resize="none"
:rows="rows"
@blur="handleBlur"
:formatter="handleFormatter"
></el-input>
</div>
</template>
<script setup lang="ts">
export interface Props {
type?: string
require?: boolean
label?: string
modelValue?: string | number
canEdit?: boolean
widthType?: number
stringNum?: boolean //stringnumbervaluestringid
placeholder?: string
showPassword?: boolean
rows?: number
}
const props = withDefaults(defineProps<Props>(), {
type: 'text',
require: false,
label: '',
modelValue: '',
canEdit: true,
widthType: 0,
stringNum: false,
cmd: false,
placeholder: '',
showPassword: false,
rows: 5,
})
const emit = defineEmits(['update:modelValue'])
const localValue = ref<string | number>('')
const labelWidth = computed(() => {
switch (props.widthType) {
case 0:
return 'width-normal'
case 1:
return ''
default:
return 'width-normal'
}
})
const inputChange = (val: string | number) => {
emit('update:modelValue', props.type === 'number' ? Number(val) : val)
}
const handleBlur = () => {
if (props.type === 'number') {
if (
String(localValue.value).length === 0 ||
String(localValue.value).match(/^[0]+$/) !== null
) {
localValue.value = 0
}
}
}
const handleFormatter = (value: string | number) => {
if (props.stringNum && typeof value === 'string') {
return value.replace(/([^\d])+/g, '')
}
return value
}
watch(
() => props.modelValue,
modelValue => {
localValue.value = modelValue
},
{ immediate: true }
)
</script>
<style lang="scss" scoped>
.additem-item-input-root {
display: flex;
align-items: center;
width: 100%;
height: 100%;
box-sizing: border-box;
column-gap: 8px;
// font-size: 0.18rem;
// padding-right: 42px;
.label {
// font-size: 14px;
color: var(--label-color);
line-height: 33px;
text-align: right;
// margin-right: 24px;
.require {
color: red;
}
}
.input {
flex-grow: 1;
width: 0;
height: 100%;
line-height: 100%;
}
.width-normal {
width: 110px;
}
:deep(input::-webkit-outer-spin-button),
:deep(input::-webkit-inner-spin-button) {
-webkit-appearance: none;
}
:deep(input[type='number']) {
-moz-appearance: textfield;
}
:deep(.el-input) {
font-size: inherit;
}
:deep(.el-input__inner) {
// background-color: transparent;
height: 100%;
line-height: 100%;
// @include border_color("ws_dialog_input");
}
:deep(.el-input__inner::-webkit-input-placeholder) {
}
}
</style>

67
src/components/Edfs-button.vue

@ -0,0 +1,67 @@
<template>
<el-button
:type="type"
:circle="circle"
:icon="icon"
:loading="loading"
:auto-insert-space="autoInsertSpace"
:round="round"
:size="size"
:plain="plain"
:disabled="disabled"
:autofocus="autofocus"
:color="color"
:class="className"
@click="onClick"
:link="link"
:text="text"
class="edfs-button"
>
{{ innerText }}
</el-button>
</template>
<script setup lang="ts">
import { type ButtonProps } from 'element-plus'
type Props = ButtonProps & {
innerText: string
}
const props = defineProps<Partial<Props>>()
const emit = defineEmits<{
click: [e: MouseEvent]
}>()
const className = computed(() => {
let name = 'edfs-button-default'
switch (props.size) {
case 'default':
name = 'edfs-button-default'
break
case 'large':
name = 'edfs-button-large'
break
case 'small':
name = 'edfs-button-small'
break
}
return name
})
function onClick(e: MouseEvent) {
emit('click', e)
}
</script>
<style scoped lang="scss">
.edfs-button-default {
height: 32px;
font-size: 14px;
padding: 8px 15px;
}
.edfs-button-small {
height: 24px;
font-size: 14px;
padding: 6px 11px;
}
</style>

108
src/components/Edfs-dialog.vue

@ -0,0 +1,108 @@
<template>
<div class="base-dialog">
<el-dialog
:title="title"
v-model="visible"
:draggable="draggable"
:overflow="overflow"
:width="width"
:close-on-click-modal="false"
@close="onClose"
>
<slot></slot>
<template #footer>
<slot name="footer"></slot>
</template>
<template v-if="isShowFooter">
<div class="dialog-footer">
<edfs-button
type="primary"
@click="onSave"
:loading="btnLoading"
inner-text="确定"
/>
<edfs-button v-if="useDelBtn" type="danger" @click="onDel" inner-text="删除" />
<edfs-button @click="onClose" inner-text="关闭" />
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import edfsButton from './Edfs-button.vue'
interface Props {
title: string
isShow: boolean
isShowFooter?: boolean
draggable?: boolean
overflow?: boolean
width?: string
btnLoading?: boolean
useDelBtn?: boolean
}
const emits = defineEmits(['on-save', 'on-close', 'on-del'])
const visible = ref(false)
const props = withDefaults(defineProps<Props>(), {
title: '提示',
isShow: false,
isShowFooter: true,
draggable: true,
overflow: true,
btnLoading: false,
useDelBtn: false,
})
watch(
() => props.isShow,
val => {
visible.value = val
}
)
function onSave() {
emits('on-save')
}
function onClose() {
emits('on-close')
}
function onDel() {
emits('on-del')
}
</script>
<style lang="scss" scoped>
.base-dialog {
:deep(.el-dialog__header) {
display: flex;
padding-bottom: 16px;
padding-right: 20px;
}
:deep(.el-dialog__title) {
font-size: 18px;
}
:deep(.el-dialog) {
padding: 20px;
}
:deep(.el-dialog__footer) {
padding: 0;
}
.dialog-footer {
display: flex;
justify-content: center;
align-items: center;
column-gap: 4px;
height: 60px;
margin-top: 20px;
}
}
</style>

151
src/components/Edfs-number-input.vue

@ -0,0 +1,151 @@
<template>
<div class="edfs-number-input">
<div class="label" v-if="labelName">
<span v-if="props.require" class="require">*</span>
{{ labelName }}
</div>
<el-tooltip
popper-class="number-tips"
:content="tipText"
placement="top"
:visible="isShowTip"
:disabled="!isUseTip"
>
<el-input
class="number-input"
v-model="inputValue"
type="text"
@input="handleInput"
@wheel="onWheel"
:disabled="disabled"
:placeholder="placeholder"
@focus="onFocus"
@blur="onBlur"
/>
</el-tooltip>
</div>
</template>
<script setup lang="ts">
interface Props {
tipText?: string
isUseTip?: boolean
labelName?: string
require?: boolean
placeholder?: string
modelValue: number | string
numMax?: number | undefined
numMin?: number | undefined
useWheel?: boolean
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
labelName: '',
modelValue: '',
require: false,
placeholder: '请输入',
numMax: undefined,
numMin: undefined,
isUseTip: false,
tipText: '',
disabled: false,
useWheel: false,
})
const emit = defineEmits(['update:modelValue', 'change'])
const inputValue = ref()
watch(
() => props.modelValue,
newValue => {
inputValue.value = newValue
},
{ immediate: true }
)
const handleInput = (value: string) => {
let newValue = value.replace(/[^0-9.-]/g, '').replace(/(\..*)\./g, '$1')
if (newValue.indexOf('-') > 0) {
newValue = newValue.replace(/-/g, '')
}
if (newValue.length === 0) {
isShowTip.value = true
} else {
isShowTip.value = false
}
inputValue.value = newValue
}
const onWheel = (event: WheelEvent) => {
event.preventDefault()
if (!props.useWheel) return
let currentValue = Number(inputValue.value) || 0
if (event.deltaY < 0) {
currentValue++
} else {
currentValue--
}
inputValue.value = currentValue.toString()
}
watch(inputValue, newValue => {
if (!newValue) {
emit('update:modelValue', undefined)
return
}
let numericValue = newValue
// if (props.numMax !== undefined && numericValue > props.numMax) {
// inputValue.value = props.numMax.toString()
// numericValue = props.numMax
// }
// if (props.numMin !== undefined && numericValue < props.numMin) {
// inputValue.value = props.numMin.toString()
// numericValue = props.numMin
// }
emit('update:modelValue', numericValue)
})
const isShowTip = ref(false)
function onBlur() {
onChangeTip(false)
}
function onFocus() {
onChangeTip(true)
}
function onChangeTip(visible: boolean) {
isShowTip.value = visible
}
</script>
<style lang="scss" scoped>
.edfs-number-input {
display: flex;
align-items: center;
width: 100%;
height: 100%;
box-sizing: border-box;
.label {
color: var(--label-color);
white-space: nowrap;
text-align: right;
line-height: 33px;
width: 110px;
.require {
color: red;
}
}
.number-input {
flex: 1;
}
}
</style>

11
src/components/Edfs-table/defaults.ts

@ -0,0 +1,11 @@
import type { TableProps } from 'element-plus';
interface IPaging {
currentPage?: number,
pageSize?: number,
pageTotal?: number,
usePaging?: boolean
loading?: boolean
}
export type Props<T> = TableProps<T> & IPaging

213
src/components/Edfs-table/index.vue

@ -0,0 +1,213 @@
<template>
<div class="edfs-table-components">
<el-table :data="data" :fit="fit" :stripe="stripe" :border="border" v-loading="loading"
:show-header="showHeader"
:max-height="maxHeight" :highlight-current-row="highlightCurrentRow"
:row-class-name="rowClassName"
@current-change="onCurrentChange" ref="ELTableRef" @row-click="onRowClick"
@row-dblclick="onRowDblclick"
@selection-change="handleSelectionChange" @expand-change="expandChange"
class="edfs-table"
:span-method="spanMethod">
<slot></slot>
</el-table>
<template v-if="usePaging">
<div class="pagination-block">
<el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize"
:width-type="1"
layout="prev, pager, next,total, jumper" :total="pageTotal" background
@current-change="onPageCurrentChange"/>
<!-- @size-change="onPageSizeChange" -->
</div>
</template>
</div>
</template>
<script setup lang="ts" generic="T">
import { ElTable } from 'element-plus'
import { type Props } from './defaults'
const props = withDefaults(defineProps<Props<T>>(), {
fit: true,
showHeader: true,
usePaging: true,
currentPage: 1,
pageTotal: 15,
loading: false,
})
const emit = defineEmits<{
'current-change': [currentRow: any, oldCurrentRow: any]
'row-click': [row: any, column: any, event: Event]
'row-dblclick': [row: any, column: any, event: Event]
// 'page-size-change': [pageSize: number]
'page-current-change': [currentPage: number]
'selection-change': [selection: any[]]
'expand-change': [row: any, expandedRows: any[]]
}>()
function onCurrentChange(currentRow: any, oldCurrentRow: any) {
emit('current-change', currentRow, oldCurrentRow)
}
function onRowClick(row: any, column: any, event: Event) {
emit('row-click', row, column, event)
}
function onRowDblclick(row: any, column: any, event: Event) {
emit('row-dblclick', row, column, event)
}
function handleSelectionChange(selection: any[]) {
emit('selection-change', selection)
}
function expandChange(row: any, expandedRows: any[]) {
emit('expand-change', row, expandedRows)
}
//
const pageSize = ref()
watch(
() => props.pageSize,
val => {
pageSize.value = val
}
)
const currentPage = ref()
watch(
() => props.currentPage,
val => {
currentPage.value = val
}
)
// function onPageSizeChange(pageSize: number) {
// emit('page-size-change', pageSize)
// }
function onPageCurrentChange(currentPage: number) {
emit('page-current-change', currentPage)
}
function getSize() {
const el = document.querySelector('.edfs-table-components')
if (!el) return 18
const height = Math.floor(el.getBoundingClientRect().height)
return Math.ceil(height / 40)
}
const ELTableRef = ref<InstanceType<typeof ElTable>>()
function clearSelection() {
ELTableRef.value?.clearSelection()
}
defineExpose({
getSize,
clearSelection,
})
</script>
<style lang="scss" scoped>
.edfs-table-components {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
// background-color: #fff;
.edfs-table {
width: 100%;
height: 100%;
font-size: 14px;
}
:deep(.el-table th.el-table__cell) {
background-color: var(--table-header-bg);
font-family: Alibaba-PuHuiTi-M;
font-size: 14px;
color: var(--table-header-text-color);
font-weight: 500;
}
:deep(.el-table__row .cell) {
font-family: Alibaba-PuHuiTi-R;
line-height: 16px;
min-height: 20px;
font-weight: 400;
color: var(--text-color);
}
.pagination-block {
user-select: none;
display: flex;
// place-content: center;
align-items: center;
justify-content: end;
height: 60px;
:deep(.el-pagination__editor.el-input) {
width: 66px;
}
:deep(.el-input__wrapper) {
height: 30px;
margin-left: 8px;
padding: 0 10px;
.el-input__inner {
font-size: 14px;
height: 100%;
}
}
:deep(.el-pagination) {
// width: cvw(200);
}
:deep(.el-pagination),
:deep(.el-pagination .el-icon),
:deep(.el-pagination li) {
font-size: 14px;
}
:deep(.el-pagination .btn-next),
:deep(.el-pagination .btn-prev),
:deep(.el-pagination li) {
background-color: transparent;
}
:deep(.el-pager) {
height: 28px;
}
:deep(.el-pagination li) {
min-width: 28px;
height: 28px;
border-radius: 2px;
border: 1px solid var(--pagination-border-color);
background-color: var(--pagination-bg);
&:hover {
color: #619925;
}
}
:deep(.is-active) {
color: #619925 !important;
background: rgba(97, 153, 37, 0.06) !important;
border: 1px solid rgba(97, 153, 37, 1) !important;
border-radius: 2px;
}
:deep(.el-input__wrapper) {
background-color: var(--pagination-bg);
}
}
}
</style>

224
src/components/Edfs-wrap.vue

@ -0,0 +1,224 @@
<template>
<div class="edfs-layout-wrap">
<div class="edfs-wrap">
<div class="wrap-title" v-if="title || customLeft">
<div class="title-left" v-if="!customLeft">
<template v-if="shape === 'rect'">
<div
class="title-rect"
v-if="!useLeftColorBar"
:style="{ backgroundColor: shapeColor }"
></div>
</template>
<template v-else-if="shape === 'circle'">
<div
class="title-circle"
v-if="!useLeftColorBar"
:style="{ backgroundColor: shapeColor }"
></div>
</template>
<div class="title-text">
{{ title }}
</div>
</div>
<div class="title-left" v-else>
<div class="title-rect" v-if="!useLeftColorBar"></div>
<slot name="title-left"></slot>
</div>
<div class="title-right">
<slot name="title-right"></slot>
</div>
</div>
<el-scrollbar class="wrap-body" :class="{ 'is-title': title }" v-if="useScrollBar">
<slot></slot>
</el-scrollbar>
<div class="wrap-body" :class="{ 'is-title': title }" v-else>
<slot></slot>
</div>
</div>
<div
class="toggle-bar"
v-if="isCollapse"
:class="{ collapsed: !collapsed }"
@click="onCollapse"
>
<div class="top"></div>
<div class="bottom"></div>
</div>
</div>
</template>
<script setup lang="ts">
const emit = defineEmits<{
collapse: []
}>()
interface Props {
title?: string
useLeftColorBar?: boolean
barColor?: string
customLeft?: boolean
shape?: 'circle' | 'rect'
shapeColor?: string
useScrollBar?: boolean
isCollapse?: boolean
collapsed?: boolean
}
const props = withDefaults(defineProps<Props>(), {
title: '',
useLeftColorBar: false,
barColor: '#fff',
customLeft: false,
shape: 'rect',
shapeColor: '#619925',
useScrollBar: false,
isCollapse: false,
collapsed: false,
})
function onCollapse() {
emit('collapse')
}
</script>
<style lang="scss" scoped>
.edfs-layout-wrap {
width: 100%;
transition: width 0.3s;
position: relative;
.edfs-wrap {
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
height: 100%;
box-sizing: border-box;
background-color: var(--warp-bg);
border-radius: 4px;
.wrap-title {
height: 46px;
width: 100%;
display: flex;
justify-content: space-between;
padding: 0 16px;
box-sizing: border-box;
background-color: var(--warp-bg);
white-space: nowrap;
.title-left {
height: 100%;
display: flex;
gap: 4px;
align-items: center;
.title-rect {
width: 3px;
height: 14px;
}
.title-circle {
width: 8px;
height: 8px;
border-radius: 50%;
}
.title-text {
font-size: 16px;
color: var(--text-color);
font-weight: 700;
}
}
.title-right {
// flex: 1;
height: 100%;
display: flex;
align-items: center;
box-sizing: border-box;
}
:deep(.el-button) {
height: 26px;
font-size: 14px;
}
:deep(.el-icon) {
font-size: 14px;
}
:deep(.icon) {
font-size: 14px;
}
}
.wrap-body {
width: 100%;
padding: 16px;
height: 100%;
box-sizing: border-box;
background-color: var(--warp-bg);
}
.is-title {
height: calc(100% - 46px);
}
}
.toggle-bar.collapsed {
&:hover {
.top {
transform: rotate(12deg) scale(1.15) translateY(-2px);
}
.bottom {
transform: rotate(-12deg) scale(1.15) translateY(2px);
}
}
}
.toggle-bar {
cursor: pointer;
height: 72px;
width: 32px;
position: absolute;
top: calc(50% - 36px);
z-index: 888;
right: -20px;
&:hover {
.top {
transform: rotate(-12deg) scale(1.15) translateY(-2px);
}
.bottom {
transform: rotate(12deg) scale(1.15) translateY(2px);
}
}
.top {
background-color: #c7bebe;
position: absolute;
width: 4px;
border-radius: 2px;
height: 38px;
left: 14px;
transition: all 0.3s;
}
.bottom {
position: absolute;
top: 34px;
background-color: #c7bebe;
width: 4px;
border-radius: 2px;
height: 38px;
left: 14px;
transition: all 0.3s;
}
}
}
</style>

140
src/composables/useMessage.ts

@ -0,0 +1,140 @@
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
export const useMessage = () => {
return {
// 消息提示
info(content: string) {
ElMessage.info(content)
},
// 错误消息
error(content: string) {
ElMessage.error(content)
},
// 成功消息
success(content: string) {
ElMessage.success(content)
},
// 警告消息
warning(content: string) {
ElMessage.warning(content)
},
// 弹出提示
alert(content: string) {
ElMessageBox.alert(content, '系统提示')
},
// 错误提示
alertError(content: string) {
ElMessageBox.alert(content, '系统提示', { type: 'error' })
},
// 成功提示
alertSuccess(content: string) {
ElMessageBox.alert(content, '系统提示', { type: 'success' })
},
// 警告提示
alertWarning(content: string) {
ElMessageBox.alert(content, '系统提示', { type: 'warning' })
},
// 通知提示
notify(content: string) {
ElNotification.info(content)
},
// 错误通知
notifyError(content: string, tip?: string) {
ElNotification.error({
title: tip ? tip : '系统提示',
message: content,
})
},
// 成功通知
notifySuccess(content: string) {
ElNotification.success(content)
},
// 警告通知
notifyWarning(content: string) {
ElNotification.warning(content)
},
// 确认窗体
confirm(content: string, tip?: string) {
return ElMessageBox.confirm(content, tip ? tip : '系统提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--success',
cancelButtonClass: 'el-button--default',
type: 'warning',
})
},
forceConfirm(content: string, tip?: string, buttonText?: string) {
return ElMessageBox.confirm(content, tip ? tip : '系统提示', {
confirmButtonText: buttonText ?? '确定',
showCancelButton: false,
closeOnClickModal: false,
closeOnPressEscape: false,
confirmButtonClass: 'el-button--success',
cancelButtonClass: 'el-button--default',
showClose: false,
type: 'warning',
})
},
// 删除窗体
delConfirm(content?: string, tip?: string) {
return new Promise((resolve, reject) => {
ElMessageBox.confirm(
content ? content : '是否确认删除数据项?',
tip ? tip : '系统提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
confirmButtonClass: 'el-button--success',
cancelButtonClass: 'el-button--default',
}
)
.then(() => {
resolve('')
})
.catch(() => {
reject('')
})
})
},
// 导出窗体
exportConfirm(content?: string, tip?: string) {
return ElMessageBox.confirm(
content ? content : '是否确认导出数据项?',
tip ? tip : '系统提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--success',
cancelButtonClass: 'el-button--default',
type: 'warning',
}
)
},
// 提交内容
prompt(content: string, tip: string) {
return ElMessageBox.prompt(content, tip, {
confirmButtonText: '确定',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--success',
cancelButtonClass: 'el-button--default',
type: 'warning',
})
},
promptVerify(content: string, tip: string, pattern: string, inputErrorMessage = '') {
const PatternRegExp = new RegExp(
`^${pattern.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}$`
)
return ElMessageBox.prompt(content, tip, {
confirmButtonText: '确定',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--success',
cancelButtonClass: 'el-button--default',
inputPattern: PatternRegExp,
inputErrorMessage: inputErrorMessage,
type: 'warning',
})
},
}
}

41
src/composables/useTheme.ts

@ -0,0 +1,41 @@
const STORAGE_THEME = 'theme'
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
const localTheme = 'light'
// localStorage.getItem(STORAGE_THEME) || (prefersDark ? 'dark' : 'light')
const theme = ref<'light' | 'dark'>(localTheme as 'light' | 'dark')
const chartGraphicTextColor = computed(() =>
theme.value === 'dark' ? '#ccc' : '#4d4d4d'
)
watchEffect(() => {
const html = document.querySelector('html')
if (theme.value === 'dark') {
html?.classList.remove('light')
html?.classList.add('dark')
} else {
html?.classList.remove('dark')
html?.classList.add('light')
}
document.documentElement.setAttribute('data-theme', theme.value)
localStorage.setItem(STORAGE_THEME, theme.value)
})
function toggle() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
function setTheme(value: 'light' | 'dark') {
theme.value = value
}
export function useTheme() {
return {
theme,
toggle,
setTheme,
chartGraphicTextColor,
}
}

175
src/composables/useZMQJsonWorker.ts

@ -0,0 +1,175 @@
import type {
ManualAction,
PublishMsg,
PubMsgData,
SubMsgData,
TimeoutMsg,
ZmqMessage
} from '@/utils/zmq'
import { WorkerCMD, ZmqCMD, } from '@/utils/zmq'
import webWorker from '@/utils/zmqJsonWorker?worker'
let defaultHost = env.VITE_ZMQ_BASE_URL
const SUBDEFAULTKEY = 'default'
type Handler = (msg: SubMsgData | PubMsgData) => void
class ZMQJsonWorker {
private static instance: ZMQJsonWorker | null = null; // ➤ 单例实例
private worker: Worker;
private scribeHandlers: Map<string, Map<string, Handler>> = new Map();
private pubTimeoutHandlers: Map<string, (msg: TimeoutMsg) => void> = new Map();
private readonly host: string;
private statusCallback: ((status: string) => void) | null = null;
private isAlwaysListenMsgMap: Map<string, PublishMsg<any>> = new Map();
private constructor(host: string = defaultHost) {
this.host = host;
this.worker = new webWorker();
this.worker.onmessage = this.handleMessage.bind(this);
}
public static getInstance(host: string = defaultHost): ZMQJsonWorker {
if (!ZMQJsonWorker.instance) {
ZMQJsonWorker.instance = new ZMQJsonWorker(host);
}
return ZMQJsonWorker.instance;
}
start() {
this.worker.postMessage({ cmd: WorkerCMD.START, msg: this.host });
}
stop() {
this.worker.postMessage({ cmd: WorkerCMD.STOP });
this.worker.terminate();
ZMQJsonWorker.instance = null; // ➤ 释放实例,允许重新创建
}
subscribe(topic: string, handler: (msg: any) => void, id?: string) {
const key = id ?? SUBDEFAULTKEY;
let topicMap = this.scribeHandlers.get(topic);
if (!topicMap) {
topicMap = new Map<string, Handler>();
this.scribeHandlers.set(topic, topicMap);
}
// 添加 handler,不会覆盖其他 id 的 handler
topicMap.set(key, handler);
this.worker.postMessage({ cmd: WorkerCMD.SUBSCRIBE, topic });
}
unsubscribe(topic: string, id?: string) {
const topicMap = this.scribeHandlers.get(topic);
if (!topicMap) return;
if (id) {
topicMap.delete(id);
} else {
topicMap.delete(SUBDEFAULTKEY);
}
if (topicMap.size === 0) {
this.scribeHandlers.delete(topic);
this.worker.postMessage({ cmd: WorkerCMD.UNSUBSCRIBE, topic });
}
}
publish<T extends ManualAction>(topic: string, msg: PublishMsg<T>, isTimeout: boolean = false, handler?: (msg: TimeoutMsg) => void, isAlwaysListen: boolean = false) {
if (isTimeout) {
const timeoutId = msg.id
if (typeof handler !== 'function') {
console.warn(`发布主题${topic}失败, 回调函数handler为空`)
return
}
this.pubTimeoutHandlers.set(timeoutId, handler)
}
if (isAlwaysListen) {
this.isAlwaysListenMsgMap.set(`${msg.id}`, msg)
}
this.worker.postMessage({
cmd: WorkerCMD.PUBLISH,
topic,
msg: JSON.stringify(msg),
isTimeout,
isAlwaysListen
});
}
setStatusCallback(callback: (status: string) => void) {
this.statusCallback = callback;
}
private handleSubscribeMessage(topic: string, json: PubMsgData & SubMsgData) {
const topicMap = this.scribeHandlers.get(topic);
if (!topicMap) return;
topicMap.forEach((handler, id) => {
try {
handler(json);
} catch (error) {
console.error(`主题: ${topic} 的 handler ${id} 执行失败:`, error);
}
});
}
private handleTimeoutMessage(timeoutTopic: string, timeoutId: string) {
const handler = this.pubTimeoutHandlers.get(timeoutId);
if (handler) {
handler({
timeoutId,
timeoutTopic
});
}
}
// 回收发布后订阅的回调
private GC_pubReleaseSub(key: string) {
this.scribeHandlers.delete(key);
}
// 回收超时消息的回调
private GC_pubReleaseTimeout(key: string) {
this.pubTimeoutHandlers.delete(key);
}
private handleMessage(e: MessageEvent<ZmqMessage>) {
const { cmd, msg, topic, community } = e.data;
// const now = dayjs().format('YYYY-MM-DD HH:mm:ss')
// console.log(now, e.data)
if (cmd === ZmqCMD.STATUS) {
const status = community ? 'disconnected' : 'connected';
if (this.statusCallback) {
this.statusCallback(status);
}
} else if (cmd === ZmqCMD.JSON_MSG) {
const json = JSON.parse(msg) as PubMsgData & SubMsgData;
if (json.action) {
// 处理订阅消息
this.handleSubscribeMessage(topic, json);
} else {
// 处理需要发布消息并有返回的订阅消息
this.handleSubscribeMessage(`${topic}-${json.id}`, json);
// 删除发布消息的回调
if (Object.keys(json).includes('result') && json.result !== 'progress') {
if (!this.isAlwaysListenMsgMap.has(`${json.id}`)) {
this.GC_pubReleaseSub(`${topic}-${json.id}`)
this.GC_pubReleaseTimeout(`${json.id}`)
}
}
}
} else if (cmd === ZmqCMD.TIMEOUT) {
this.handleTimeoutMessage(topic, msg);
this.GC_pubReleaseTimeout(msg)
// console.log('pubTimeoutHandlers=>', this.pubTimeoutHandlers)
}
}
}
export default ZMQJsonWorker;

103
src/hooks/useG6/index.ts

@ -0,0 +1,103 @@
import { merge } from "lodash-es";
import type { IUseG6Options } from "./useG6";
import { VueNode } from "g6-extension-vue";
import { GenerateGraphData, MyLineEdge } from "./utils";
import {
ExtensionCategory,
Graph,
type GraphOptions,
register,
type PluginOptions
} from "@antv/g6";
export function useG6<T = any>({
tree,
nodeComponent,
nodeSize,
layoutType = 'TB'
}: IUseG6Options<T>,
plugins?: PluginOptions
) {
register(ExtensionCategory.NODE, 'vue-node', VueNode);
register(ExtensionCategory.EDGE, 'my-line-edge', MyLineEdge);
const container = ref<HTMLElement>()
const graphData = new GenerateGraphData(tree)
const LR = () => ({
layout: {
type: 'compact-box',
direction: 'LR',
getHeight: function getHeight() {
return 32;
},
getWidth: function getWidth() {
return 32;
},
getVGap: function getVGap() {
return 10;
},
getHGap: function getHGap() {
return 100;
},
},
node: {
style: {
ports: [{ placement: 'right' }, { placement: 'left' }],
}
}
})
const TB = () => ({
layout: {
type: 'antv-dagre',
},
node: {
style: {
size: nodeSize,
dx: -nodeSize[0] / 2,
dy: -nodeSize[1] / 2,
ports: [{ placement: 'top' }, { placement: 'bottom' }],
}
}
})
const options: GraphOptions = {
container: container.value!,
padding: 50,
autoFit: 'view',
node: {
type: 'vue-node',
style: {
component: (data: any) => {
return h(nodeComponent, { data: data })
},
},
},
edge: {
type: 'my-line-edge',
style: {
radius: 10,
router: {
type: 'orth',
},
},
},
data: {
nodes: graphData.getNodes(),
edges: graphData.getEdges()
},
plugins,
behaviors: ['drag-canvas', 'zoom-canvas'],
}
const mergeOption = merge({}, options, layoutType === 'LR' ? LR() : TB())
const graph = new Graph(Object.assign(options, mergeOption));
return {
container,
graph
}
}

9
src/hooks/useG6/useG6.d.ts vendored

@ -0,0 +1,9 @@
import type { Component } from "vue";
interface IUseG6Options<T> {
tree: T[],
nodeComponent: Component,
nodeSize: [number, number]
layoutType?: 'LR' | 'TB'
}

49
src/hooks/useG6/utils/GenerateGraphData.ts

@ -0,0 +1,49 @@
import type { EdgeData, NodeData } from "@antv/g6";
import type { Device, MyNodeData } from "@/types/device";
export class GenerateGraphData {
private readonly devices: Device[] = [];
private nodes: MyNodeData[] = [];
private edges: EdgeData[] = [];
constructor(devices: Device[]) {
this.devices = devices;
this.generateNodeData()
this.generateEdgeData();
}
private generateNodeData() {
for (const device of this.devices) {
this.nodes.push({
id: String(device.name),
label: device.name,
data: Object.assign(device, {
// typeString: DeviceType[device.type],
}),
})
}
}
private generateEdgeData() {
for (const node of this.nodes) {
const customData: Device = node.data
const findParentNode: Device = this.nodes.some((n: MyNodeData) => n.id === String(customData.parentName));
if (findParentNode) {
this.edges.push({
source: String(customData.parentName),
target: String(node.id),
data: customData,
})
}
}
}
getNodes(): NodeData[] {
return this.nodes as NodeData[];
}
getEdges(): EdgeData[] {
return this.edges;
}
}

64
src/hooks/useG6/utils/MyLineEdge.ts

@ -0,0 +1,64 @@
import { Polyline } from '@antv/g6';
import { Circle } from '@antv/g';
import { subStyleProps } from '@antv/g6';
import type { Device } from "@/types/device";
export class MyLineEdge extends Polyline {
getMarkerStyle(attributes: object) {
return {
r: 4,
fill: '#58C448',
offsetPath: this.shapeMap.key, ...subStyleProps(attributes, 'marker')
};
}
onCreate() {
const marker = this.upsert('marker', Circle, this.getMarkerStyle(this.attributes), this)!;
const prev = this.context.model.getRelatedEdgesData(this.sourceNode.id) // 获取源节点的相关边数据
.find((edge) => edge.target === this.targetNode.id) as Device;
const depth = prev!.data!.depth;
const delay = depth * 3000; // 每级延迟 3 秒
marker.animate(
[{ offsetDistance: 0 }, { offsetDistance: 1 }],
{
duration: 3000,
iterations: Infinity,
delay,
}
);
// 涟漪效果:在 marker 外套一个圆
const ripple = this.upsert(
'ripple',
Circle,
{
r: 4, // 初始半径和 marker 一样
stroke: '#58C448',
lineWidth: 2,
fill: 'none',
offsetPath: this.shapeMap.key,
},
this
)!;
// 涟漪动画:半径变大 + 透明度变小
ripple.animate(
[
{ offsetDistance: 0, r: 4, opacity: 0.7 },
{ offsetDistance: 1, r: 6, opacity: 0 },
],
{
duration: 3000,
iterations: Infinity,
delay,
}
);
// marker.animate([{ offsetDistance: 0 }, { offsetDistance: 1 }], {
// duration: 3000,
// iterations: Infinity,
// });
}
}

4
src/hooks/useG6/utils/index.ts

@ -0,0 +1,4 @@
export * from './GenerateGraphData'
export * from './MyLineEdge'

1
src/lib/awesome-qr.js

File diff suppressed because one or more lines are too long

14
src/lib/zmq/zmqClient.d.ts vendored

@ -0,0 +1,14 @@
import * as zmq from "jszmq";
export default class ZmqClient {
sock: zmq.Dealer | zmq.Router | zmq.XSub | zmq.XPub | zmq.Pull | zmq.Push | zmq.Pair;
constructor(type: string);
zmqReq(host: string, request_type: string, dataStr: string, timeout: number): Promise<string>;
zmqSub(host: string, callback: (...args: any[]) => void): void;
zmqPub(host: string): void;
subscribe(topic: string): void;
unsubscribe(topic: string): void;
publishHex(topic: string, dataStr: string): Promise<void>;
publishStr(topic: string, dataStr: string): Promise<void>;
close(host: string, callback?: (...args: any[]) => void): void;
}
//# sourceMappingURL=zmqClient.d.ts.map

10
src/lib/zmq/zmqClient.d.ts.map

@ -0,0 +1,10 @@
{
"version": 3,
"file": "zmqClient.d.ts",
"sourceRoot": "",
"sources": [
"../src/zmqClient.ts"
],
"names": [],
"mappings": "AAAA,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAG7B,MAAM,CAAC,OAAO,OAAO,SAAS;IAC1B,IAAI,EACE,GAAG,CAAC,MAAM,GACV,GAAG,CAAC,MAAM,GACV,GAAG,CAAC,IAAI,GACR,GAAG,CAAC,IAAI,GACR,GAAG,CAAC,IAAI,GACR,GAAG,CAAC,IAAI,GACR,GAAG,CAAC,IAAI,CAAC;gBACH,IAAI,EAAE,MAAM;IAgBlB,MAAM,CACR,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC;IA6BlB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI;IAKvD,MAAM,CAAC,IAAI,EAAE,MAAM;IAInB,SAAS,CAAC,KAAK,EAAE,MAAM;IAIvB,WAAW,CAAC,KAAK,EAAE,MAAM;IAInB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAOzC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAO/C,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI;CAO1D"
}

118
src/lib/zmq/zmqClient.js

@ -0,0 +1,118 @@
var __awaiter =
(this && this.__awaiter) ||
function (thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P
? value
: new P(function (resolve) {
resolve(value)
})
}
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value))
} catch (e) {
reject(e)
}
}
function rejected(value) {
try {
step(generator['throw'](value))
} catch (e) {
reject(e)
}
}
function step(result) {
result.done
? resolve(result.value)
: adopt(result.value).then(fulfilled, rejected)
}
step((generator = generator.apply(thisArg, _arguments || [])).next())
})
}
import * as zmq from 'jszmq'
import { Buffer } from 'buffer'
export default class ZmqClient {
constructor(type) {
switch (type) {
case 'req':
this.sock = zmq.socket('req')
break
case 'sub':
this.sock = zmq.socket('sub')
break
case 'pub':
this.sock = zmq.socket('pub')
break
default:
throw new Error('unsupported client type')
}
}
zmqReq(host, request_type, dataStr, timeout) {
return __awaiter(this, void 0, void 0, function* () {
this.sock.connect(host)
var request = new Array()
request[0] = request_type
request[1] = dataStr
this.sock.send(request)
return Promise.race([
new Promise((resolve, reject) => {
this.sock.on('message', function (message) {
resolve(message.toString())
})
}),
new Promise((resolve, reject) => {
setTimeout(() => {
reject('timeout error')
}, timeout)
}),
])
// var isMessageArrived = false;
// const result: string = "";
// var msgReceiveTimer = setTimeout(() => {
// if (!isMessageArrived) {
// console.log("msg receive timed out");
// }
// }, timeout); // 设置超时时间为5秒
})
}
zmqSub(host, callback) {
this.sock.connect(host)
this.sock.on('message', callback)
}
zmqPub(host) {
this.sock.connect(host)
}
subscribe(topic) {
this.sock.subscribe(topic)
}
unsubscribe(topic) {
this.sock.unsubscribe(topic)
}
publishHex(topic, dataStr) {
return __awaiter(this, void 0, void 0, function* () {
var request = new Array()
request[0] = topic
const buf = Buffer.from(dataStr, 'hex')
request[1] = buf
this.sock.send(request)
})
}
publishStr(topic, dataStr) {
return __awaiter(this, void 0, void 0, function* () {
var request = new Array()
request[0] = topic
request[1] = dataStr
this.sock.send(request)
})
}
close(host, callback) {
if (callback) {
this.sock.removeListener('message', callback)
}
this.sock.disconnect(host)
this.sock.close()
}
}
//# sourceMappingURL=zmqClient.js.map

10
src/lib/zmq/zmqClient.js.map

@ -0,0 +1,10 @@
{
"version": 3,
"file": "zmqClient.js",
"sourceRoot": "",
"sources": [
"../src/zmqClient.ts"
],
"names": [],
"mappings": ";;;;;;;;;AAAA,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,CAAC,OAAO,OAAO,SAAS;IAS1B,YAAY,IAAY;QACpB,QAAQ,IAAI,EAAE;YACV,KAAK,KAAK;gBACN,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC9B,MAAM;YACV,KAAK,KAAK;gBACN,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC9B,MAAM;YACV,KAAK,KAAK;gBACN,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC9B,MAAM;YACV;gBACI,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;SAClD;IACL,CAAC;IAEK,MAAM,CACR,IAAY,EACZ,YAAoB,EACpB,OAAe,EACf,OAAe;;YAEf,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACxB,IAAI,OAAO,GAAG,IAAI,KAAK,EAAE,CAAC;YAC1B,OAAO,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC;YAC1B,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAExB,OAAO,OAAO,CAAC,IAAI,CAAC;gBAChB,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;oBACpC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,UAAU,OAAO;wBACrC,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;oBAChC,CAAC,CAAC,CAAC;gBACP,CAAC,CAAC;gBACF,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;oBACpC,UAAU,CAAC,GAAG,EAAE;wBACZ,MAAM,CAAC,eAAe,CAAC,CAAC;oBAC5B,CAAC,EAAE,OAAO,CAAC,CAAC;gBAChB,CAAC,CAAC;aACL,CAAC,CAAC;YAEH,gCAAgC;YAChC,6BAA6B;YAC7B,2CAA2C;YAC3C,+BAA+B;YAC/B,gDAAgD;YAChD,QAAQ;YACR,4BAA4B;QAChC,CAAC;KAAA;IAED,MAAM,CAAC,IAAY,EAAE,QAAkC;QACnD,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,CAAC,IAAY;QACf,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED,SAAS,CAAC,KAAa;QACnB,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAED,WAAW,CAAC,KAAa;QACrB,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;IAEK,UAAU,CAAC,KAAa,EAAE,OAAe;;YAC3C,IAAI,OAAO,GAAG,IAAI,KAAK,EAAE,CAAC;YAC1B,OAAO,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;YACnB,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YACxC,OAAO,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;YACjB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5B,CAAC;KAAA;IACK,UAAU,CAAC,KAAa,EAAE,OAAe;;YAC3C,IAAI,OAAO,GAAG,IAAI,KAAK,EAAE,CAAC;YAC1B,OAAO,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;YACnB,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5B,CAAC;KAAA;IAED,KAAK,CAAC,IAAY,EAAE,QAAmC;QACnD,IAAI,QAAQ,EAAE;YACV,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;SACjD;QACD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;CACJ"
}

14
src/main.ts

@ -0,0 +1,14 @@
import './assets/styles/main.css'
import 'virtual:uno.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

34
src/router/index.ts

@ -0,0 +1,34 @@
import { createRouter, createWebHistory } from 'vue-router'
export const defaultRouter = [
{
path: '/',
name: 'dashboard',
redirect: '/engineering',
component: () => import('@/views/layout/index.vue'),
meta: {
title: '首页',
isShow: true,
icon: 'i-mage:file-2',
},
children: [
{
path: '/engineering',
name: 'engineering',
component: () => import('@/views/engineering/index.vue'),
meta: {
title: '工程管理',
isShow: true,
icon: 'i-icon-park-outline:data',
},
},
],
},
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: defaultRouter,
})
export default router

270
src/stores/transferData.ts

@ -0,0 +1,270 @@
import { ref, computed, reactive } from 'vue'
import { defineStore } from 'pinia'
import type { IOnlineDevice, IUpFirmwareStatus } from '@/views/stationData/type'
import ZMQWorker from '@/composables/useZMQJsonWorker'
import { getSubTopic, type SubMsgData } from '@/utils/zmq'
import { getDeviceTopic } from '@/views/stationData/utils'
export interface SiteInfo {
id: string
onlineCount: number
offlineCount: number
devices: Map<string, IOnlineDevice>
}
export const useTransferDataStore = defineStore('transfer', () => {
const subDevices = getSubTopic('client', 'status', 'transfer')
const worker = ZMQWorker.getInstance()
const isConnected = ref(false)
const connectSite = ref<any>(null)
const checkDeviceStatusInterval = ref<NodeJS.Timeout>()
const siteMap = reactive(new Map<string, SiteInfo>())
const devicesMap = reactive(new Map<string, IOnlineDevice>())
// =========== mock =================
// let i = 0
//
// function mockSubDeviceMsg() {
// // 随机生成 SN
// const sn = `SN-${Math.floor(Math.random() * 1000)}`
// // const siteId = `${i % 2 === 0 ? 'site1' : 'site2'}`
// const siteId = `siteId-${Math.floor(Math.random() * 1000)}`
// const clientIp = `192.168.0.${Math.floor(Math.random() * 255)}`
// const version = `v${(Math.random() * 2 + 1).toFixed(2)}`
// const footprint = (Math.random() * 50000).toFixed(2) // KB
//
// // const sn = 123
// // const siteId = 123123
// // const clientIp = `192.168.0.${Math.floor(Math.random() * 255)}`
// // const version = `v${(Math.random() * 2 + 1).toFixed(2)}`
// // const footprint = (Math.random() * 50000).toFixed(2) // KB
//
// const msg: any = {
// feedback: [clientIp, sn, siteId, version, footprint],
// }
//
// getSubDevicesCb(msg)
// }
//
//
// let a = setInterval(() => {
// i++
// mockSubDeviceMsg()
// }, 1000)
// setTimeout(() => {
// clearInterval(a)
// }, 8000)
// =========== mock end =================
function startCheckStatus() {
clearInterval(checkDeviceStatusInterval.value)
checkDeviceStatusInterval.value = setInterval(checkDeviceStatusFn, 1000);
}
const checkDeviceStatusFn = () => {
const now = Date.now();
devicesMap.forEach((device: IOnlineDevice, sn) => {
// console.log(device, dayjs(now).format('HH:mm:ss'), dayjs(device.lastUpdated).format('HH:mm:ss'), now - device.lastUpdated)
if (now - device.lastUpdated > 5500) {
device.status = '离线';
}
});
reassignDevicesToSites()
}
function formatSizeFromKB(num: number): string {
const sizeKB = Number(num)
const units = ['KB', 'MB', 'GB', 'TB', 'PB']
let size = sizeKB
let unitIndex = 0
while (size >= 1024 && unitIndex < units.length - 1) {
size = size / 1024
unitIndex++
}
return `${size.toFixed(2)} ${units[unitIndex]}`
}
function getSubDevicesCb(msg: SubMsgData) {
const { feedback } = msg
const sn = feedback[1]
const hasDevice = devicesMap.get(sn)
if (hasDevice) {
hasDevice.lastUpdated = Date.now()
hasDevice.status = '在线'
hasDevice.footprint = formatSizeFromKB(Number(feedback[4] || 0))
hasDevice.clientIp = feedback[0]
hasDevice.versions = feedback[3] ?? '--'
} else {
const num = feedback[4] || 0
const device: IOnlineDevice = {
clientIp: feedback[0],
sn: sn,
site_id: feedback[2],
versions: feedback[3] ?? '--',
footprint: formatSizeFromKB(Number(num)),
lastUpdated: Date.now(),
status: '在线', // 初始状态为在线
isChecked: false,
}
devicesMap.set(sn, device)
}
}
function reassignDevicesToSites() {
devicesMap.forEach((device) => {
const siteId = device.site_id
if (!siteId) return
// 若该站点还未创建,则初始化
if (!siteMap.has(siteId)) {
siteMap.set(siteId, {
id: siteId,
onlineCount: 0,
offlineCount: 0,
devices: reactive(new Map<string, IOnlineDevice>()),
})
}
// 获取站点信息并更新
const siteInfo = siteMap.get(siteId)!
siteInfo.devices.set(device.sn, device)
})
// 更新site 在线数量和离线数量
siteMap.forEach((siteInfo) => {
const devices = Array.from(siteInfo.devices.values())
siteInfo.onlineCount = devices.filter(item => item.status === '在线').length
siteInfo.offlineCount = devices.filter(item => item.status === '离线').length
})
}
const onlineCount = computed(() => {
return Array.from(devicesMap.values()).filter(item => item.status === '在线')
.length
})
const offlineCount = computed(() => {
return Array.from(devicesMap.values()).filter(item => item.status === '离线')
.length
})
onMounted(() => {
worker.subscribe(getDeviceTopic, getSubDevicesCb)
startCheckStatus()
document.addEventListener('visibilitychange', handleVisibilityChange)
})
const route = useRoute()
watch(() => route.path, (val) => {
if (!['/station/data-transfer', '/station'].includes(val)) {
clearInterval(checkDeviceStatusInterval.value)
document.removeEventListener('visibilitychange', handleVisibilityChange)
worker.unsubscribe(subDevices)
}
})
function handleVisibilityChange() {
if (document.hidden) {
clearInterval(checkDeviceStatusInterval.value)
} else {
startCheckStatus()
}
}
function upFirmwareStatus(sn: string, feedback: any[]) {
const device = devicesMap.get(sn)
if (device) {
device.upFirmware = 'updating'
const step = feedback[1]
const progress = feedback[2] || undefined
const errMsg = feedback[3] || undefined
if (step < (device.upFirmwareStatus?.step ?? -100)) return
device.upFirmwareStatus = {
step,
progress: progress === -1 ? 100 : progress,
errMsg,
}
}
}
function upFirmwareStatusReject(sn: string, feedback: any[]) {
const device = devicesMap.get(sn)
if (device) {
device.upFirmware = 'rejected'
const step = feedback[1]
const progress = feedback[2] || undefined
const errMsg = feedback[3] || undefined
if (step < (device.upFirmwareStatus?.step ?? -100)) return
device.upFirmwareStatus = {
step,
progress: progress === -1 ? 100 : progress,
errMsg,
}
}
}
function upFirmwarePending(snList: string[]) {
for (const sn of snList) {
const device = devicesMap.get(sn)
if (device) {
device.upFirmware = 'pending'
device.upFirmwareStatus = {
step: 0,
progress: undefined,
errMsg: undefined,
}
}
}
}
function upFirmwareReset(snList: string[]) {
for (const sn of snList) {
const device = devicesMap.get(sn)
if (device) {
upFirmwareSucceed(sn)
}
}
}
function upFirmwareSucceed(sn: string) {
const device = devicesMap.get(sn)
if (device) {
device.upFirmware = undefined
device.upFirmwareStatus = undefined
}
}
function upFirmwareTimeout(sn: string) {
const device = devicesMap.get(sn)
if (device) {
device.upFirmware = 'timeout'
device.upFirmwareStatus = undefined
}
}
return {
siteMap,
isConnected,
devicesMap,
connectSite,
checkDeviceStatusInterval,
onlineCount,
offlineCount,
upFirmwarePending,
upFirmwareReset,
upFirmwareStatus,
upFirmwareSucceed,
upFirmwareStatusReject,
upFirmwareTimeout
}
})

5
src/types/device.ts

@ -0,0 +1,5 @@
import type { NodeData } from "@antv/g6";
export type Device = any
export interface MyNodeData extends Omit<NodeData, 'data'> {
data?: Device;
}

53
src/uno-preset/src/index.ts

@ -0,0 +1,53 @@
// @unocss-include
import type { Preset, PresetUnoTheme } from 'unocss'
export function presetSoybeanAdmin(): Preset<PresetUnoTheme> {
const preset: Preset<PresetUnoTheme> = {
name: 'preset-soybean-admin',
shortcuts: [
{
'flex-center': 'flex justify-center items-center',
'flex-x-center': 'flex justify-center',
'flex-y-center': 'flex items-center',
'flex-col': 'flex flex-col',
'flex-col-center': 'flex-center flex-col',
'flex-col-stretch': 'flex-col items-stretch',
'i-flex-center': 'inline-flex justify-center items-center',
'i-flex-x-center': 'inline-flex justify-center',
'i-flex-y-center': 'inline-flex items-center',
'i-flex-col': 'flex-col inline-flex',
'i-flex-col-center': 'flex-col i-flex-center',
'i-flex-col-stretch': 'i-flex-col items-stretch',
'flex-1-hidden': 'flex-1 overflow-hidden',
},
{
'absolute-lt': 'absolute left-0 top-0',
'absolute-lb': 'absolute left-0 bottom-0',
'absolute-rt': 'absolute right-0 top-0',
'absolute-rb': 'absolute right-0 bottom-0',
'absolute-tl': 'absolute-lt',
'absolute-tr': 'absolute-rt',
'absolute-bl': 'absolute-lb',
'absolute-br': 'absolute-rb',
'absolute-center': 'absolute-lt flex-center size-full',
'fixed-lt': 'fixed left-0 top-0',
'fixed-lb': 'fixed left-0 bottom-0',
'fixed-rt': 'fixed right-0 top-0',
'fixed-rb': 'fixed right-0 bottom-0',
'fixed-tl': 'fixed-lt',
'fixed-tr': 'fixed-rt',
'fixed-bl': 'fixed-lb',
'fixed-br': 'fixed-rb',
'fixed-center': 'fixed-lt flex-center size-full',
},
{
'nowrap-hidden': 'overflow-hidden whitespace-nowrap',
'ellipsis-text': 'nowrap-hidden text-ellipsis',
},
],
}
return preset
}
export default presetSoybeanAdmin

117
src/utils/is.ts

@ -0,0 +1,117 @@
// copy to vben-admin
const toString = Object.prototype.toString
export const is = (val: unknown, type: string) => {
return toString.call(val) === `[object ${type}]`
}
export const isDef = <T = unknown>(val?: T): val is T => {
return typeof val !== 'undefined'
}
export const isUnDef = <T = unknown>(val?: T): val is T => {
return !isDef(val)
}
export const isObject = (val: any): val is Record<any, any> => {
return val !== null && is(val, 'Object')
}
export const isEmpty = <T = unknown>(val: T): val is T => {
if (val === null) {
return true
}
if (isArray(val) || isString(val)) {
return val.length === 0
}
if (val instanceof Map || val instanceof Set) {
return val.size === 0
}
if (isObject(val)) {
return Object.keys(val).length === 0
}
return false
}
export const isDate = (val: unknown): val is Date => {
return is(val, 'Date')
}
export const isNull = (val: unknown): val is null => {
return val === null
}
export const isNullAndUnDef = (val: unknown): val is null | undefined => {
return isUnDef(val) && isNull(val)
}
export const isNullOrUnDef = (val: unknown): val is null | undefined => {
return isUnDef(val) || isNull(val)
}
export const isNumber = (val: unknown): val is number => {
return is(val, 'Number')
}
export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch)
}
export const isString = (val: unknown): val is string => {
return is(val, 'String')
}
export const isFunction = (val: unknown): val is Function => {
return typeof val === 'function'
}
export const isBoolean = (val: unknown): val is boolean => {
return is(val, 'Boolean')
}
export const isRegExp = (val: unknown): val is RegExp => {
return is(val, 'RegExp')
}
export const isArray = (val: any): val is Array<any> => {
return val && Array.isArray(val)
}
export const isWindow = (val: any): val is Window => {
return typeof window !== 'undefined' && is(val, 'Window')
}
export const isElement = (val: unknown): val is Element => {
return isObject(val) && !!val.tagName
}
export const isMap = (val: unknown): val is Map<any, any> => {
return is(val, 'Map')
}
export const isServer = typeof window === 'undefined'
export const isClient = !isServer
export const isUrl = (path: string): boolean => {
const reg =
/(((^https?:(?:\/\/)?)(?:[-:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&%@.\w_]*)#?(?:[\w]*))?)$/
return reg.test(path)
}
export const isDark = (): boolean => {
return window.matchMedia('(prefers-color-scheme: dark)').matches
}
// 是否是图片链接
export const isImgPath = (path: string): boolean => {
return /(https?:\/\/|data:image\/).*?\.(png|jpg|jpeg|gif|svg|webp|ico)/gi.test(path)
}
export const isEmptyVal = (val: any): boolean => {
return val === '' || val === null || val === undefined
}

174
src/utils/zmq.ts

@ -0,0 +1,174 @@
import { v4 as uuidv4 } from 'uuid';
export type ManualAction =
'init' | 'release' | 'write' | 'report' | 'lock' | 'unlock' |
'export' | 'cancel' | 'import' | 'upgrade'
export type ZmqStatus = 'disconnected' | 'connected'
export enum WorkerCMD {
START,
SUBSCRIBE,
UNSUBSCRIBE,
PUBLISH,
STOP,
SET_TIMEOUT
}
export enum ZmqMsgResultType {
SUCCESS = 200,
PROGRESS = 1002,
ERROR = 1003,
}
export enum ZmqCMD {
MSG,
JSON_MSG,
STATUS,
TIMEOUT
}
export interface TimeoutMsg {
timeoutId: string
timeoutTopic: string
}
export interface ZmqMessage {
cmd: ZmqCMD
msg: string
community: boolean
topic: string
}
type TopicType = 'event' | 'status'
// web端发布消息类型
export interface PublishMsg<A> {
id: string // 每条消息的唯一标识
action: A // 消息的动作
reply: 'yes' | 'no' // 是否需要回复
params: any // 消息的参数
}
// web端发布消息服务端返回的数据类型
export interface PubMsgData {
code: number
feedback: any,
id: string
result: 'success' | 'progress' | 'failure' | 'refuse' | 'error'
}
// 订阅消息服务端返回的数据类型
export interface SubMsgData {
id: string
action: 'report'
feedback: any,
reply: 'yes' | 'no' // 是否需要回复
}
/*
* @Description:
* type:
* module: 模块名称
* userid: 用户唯一标识
*/
export function getPubTopic(type: TopicType, module: string,) {
return `web/${type}/${module}/`
}
export function getSubTopic(server: string, type: TopicType, module: string,) {
return `${server}/${type}/${module}/`
}
// 获取随机id
export function getRandomId() {
const uniqueId = `${Date.now()}-${uuidv4()}`;
return uniqueId
}
export function generatePubMsg<A>(massage: Omit<PublishMsg<A>, 'id'>): PublishMsg<A> {
return {
id: getRandomId(),
action: massage.action,
reply: massage.reply,
params: massage.params
}
}
export function getLockPubMsg(action: 'lock' | 'unlock'): PublishMsg<'lock' | 'unlock'> {
return {
id: getRandomId(),
action: action,
params: [],
reply: 'yes'
}
}
export function pollingWithCallback(intervalInSeconds: number, callback: Function) {
let counter = 0
const fps = 60
function poll() {
counter++
if (counter >= intervalInSeconds * fps) {
callback()
counter = 0
}
requestAnimationFrame(poll)
}
poll()
}
export function getPubInitData<T extends ManualAction>(action: T, params: any, reply: 'yes' | 'no' = 'yes') {
return generatePubMsg<T>({
action,
reply,
params
})
}
export function isHexadecimal(text: string) {
const hexRegex = /^[0-9A-Fa-f]+$/
return hexRegex.test(text)
}
export function stringToHex(str: string) {
return Array.from(str)
.map(char => char.charCodeAt(0).toString(16).toUpperCase())
.join('')
}
export function stringToDecimalismNumbers(str: string): number[] {
return Array.from(str).map(char => char.charCodeAt(0))
}
export function hexToArray(num: string) {
const numStr = num.toString()
let paddedNumStr = numStr
if (numStr.length % 2 !== 0) {
paddedNumStr = numStr.slice(0, -1) + '0' + numStr.slice(-1)
}
const result = []
for (let i = 0; i < paddedNumStr.length; i += 2) {
result.push(paddedNumStr.slice(i, i + 2))
}
return result
}
export function hexToDecimal(hex: string[]) {
return hex.map(item => parseInt(item, 16))
}
export function decimalToHexArray(decimalArray: number[]) {
return decimalArray.map(num => num.toString(16).toUpperCase()).join('')
}
export function decimalToString(arr: number[]) {
return arr.map(num => String.fromCharCode(num)).join('')
}

166
src/utils/zmqJsonWorker.ts

@ -0,0 +1,166 @@
import ZmqClient from '@/lib/zmq/zmqClient'
import { type PublishMsg, type PubMsgData, WorkerCMD, ZmqCMD, } from './zmq'
const HEARTBEAT_TOPIC = 'HEARTBEAT'
const HEARTBEAT_INTERVAL = 3000
const STATUS_CHECK_INTERVAL = 1000
let messageTimeout = 20000
let heartClient: ZmqClient | null, subClient: ZmqClient | null, pubClient: ZmqClient | null
let subHost = '', pubHost = ''
let lastHeartbeatTime = 0
let statusTimerId: ReturnType<typeof setInterval> | null
let isConnectionError = false
function decodeMessage(data: Uint8Array) {
return new TextDecoder().decode(data)
}
function updateHeartbeat() {
lastHeartbeatTime = Date.now()
}
function changeConnectionStatus(hasError: boolean) {
if (isConnectionError !== hasError) {
isConnectionError = hasError
console.log('ZMQ连接状态更新:', hasError ? '断开' : '连接')
postMessage({ cmd: ZmqCMD.STATUS, community: hasError })
}
}
function monitorConnection() {
if (statusTimerId) return
lastHeartbeatTime = Date.now()
statusTimerId = setInterval(() => {
const currentTime = Date.now()
const hasError = currentTime - lastHeartbeatTime > HEARTBEAT_INTERVAL
changeConnectionStatus(hasError)
}, STATUS_CHECK_INTERVAL)
}
function stopMonitoringConnection() {
if (statusTimerId) {
clearInterval(statusTimerId)
statusTimerId = null
}
changeConnectionStatus(false)
}
function disconnect() {
if (subClient) {
subClient.close(subHost, handleZmqMessage)
subClient = null
}
if (heartClient) {
heartClient.unsubscribe(HEARTBEAT_TOPIC)
heartClient.close(subHost, updateHeartbeat)
heartClient = null
}
if (pubClient) {
pubClient.close(pubHost)
pubClient = null
}
stopMonitoringConnection()
}
function handleZmqMessage(topic: Uint8Array, msg: Uint8Array) {
try {
if (msg instanceof Uint8Array) {
const jsonMessage = decodeMessage(msg) as string
postMessage({
topic: decodeMessage(topic),
cmd: ZmqCMD.JSON_MSG,
msg: jsonMessage
})
const parsedMessage = JSON.parse(jsonMessage) as PubMsgData
// traceMessages.get(parsedMessage.id)
const curTraceMessages = traceMessages.get(parsedMessage.id)
if (parsedMessage.id && !!curTraceMessages) {
if (curTraceMessages.isAlwaysListen || parsedMessage.result === 'progress') {
// 重置消息超时时间
const val = traceMessages.get(parsedMessage.id)
if (val) {
val.timestamp = Date.now()
}
traceMessages.set(parsedMessage.id, val)
} else {
traceMessages.delete(parsedMessage.id)
}
}
}
} catch (e) {
console.error('handleZmqMessage error:', e)
}
}
function connect(host: string) {
disconnect()
subHost = `ws://${host}:15555`
subClient = new ZmqClient('sub')
subClient.zmqSub(subHost, handleZmqMessage)
heartClient = new ZmqClient('sub')
heartClient.zmqSub(subHost, updateHeartbeat)
heartClient.subscribe(HEARTBEAT_TOPIC)
monitorConnection()
pubHost = `ws://${host}:15556`
pubClient = new ZmqClient('pub')
pubClient.zmqPub(pubHost)
setInterval(() => {
const now = Date.now()
traceMessages.forEach((val, id) => {
if (now - val.timestamp > messageTimeout) {
console.warn(`Message ${id} timed out.`)
postMessage({
cmd: ZmqCMD.TIMEOUT,
topic: val.topic.replace(/^web\//, 'server/'),
msg: id
})
traceMessages.delete(id)
}
})
}, 1000)
}
const traceMessages = new Map<string, any>()
self.onmessage = function (event) {
const { cmd, topic, msg, isTimeout = false, isAlwaysListen } = event.data
switch (cmd) {
case WorkerCMD.START:
connect(msg)
break
case WorkerCMD.SET_TIMEOUT:
messageTimeout = msg
break
case WorkerCMD.SUBSCRIBE:
subClient?.subscribe(topic)
break
case WorkerCMD.UNSUBSCRIBE:
subClient?.unsubscribe(topic)
break
case WorkerCMD.PUBLISH:
if (!msg) throw new Error('msg is required')
if (isTimeout) {
const parseMsg = JSON.parse(msg) as PublishMsg<string>
traceMessages.set(parseMsg.id, {
timestamp: Date.now(),
topic: topic,
isAlwaysListen,
})
}
pubClient?.publishStr(topic, msg)
break
case WorkerCMD.STOP:
disconnect()
break
}
}

19
src/views/engineering/index.vue

@ -0,0 +1,19 @@
<template>
<EdfsWrap class="wh-full" :title="'工程列表'">
<template #title-right>
<EdfsButton
inner-text="新增工程"
type="primary"
size="small"
plain
@click="addEngineering"
/>
</template>
</EdfsWrap>
</template>
<script setup lang="ts">
import EdfsWrap from '@/components/Edfs-wrap.vue'
function addEngineering() {}
</script>

189
src/views/layout/index.vue

@ -0,0 +1,189 @@
<template>
<div class="common-layout">
<el-container>
<el-aside class="aside-wrap">
<RouterLink to="/" class="layout-logo" :class="{ 'layout-logo-collapse': isCollapse }">
<svg class="inline-block text-32px" width="1em" height="1em" viewBox="0 0 160 160"
xmlns="http://www.w3.org/2000/svg">
<path
d="M81.28 55.9c-.1-11.67-2.93-22.55-9.37-32.38-1-1.5-2.14-2.86-2.5-4.71a8.1 8.1 0 014-8.61 7.89 7.89 0 019.3 1.23 35.999 35.999 0 015.9 8.83 75.18 75.18 0 018.44 28.58 83.211 83.211 0 01-5.23 36.74 102.983 102.983 0 01-3 7.28 1.2 1.2 0 000 1.41c9.58 13.3 21.76 23 37.85 27.24a54.37 54.37 0 0019.68 1.57 7.72 7.72 0 018.36 6.9 7.903 7.903 0 01-6.7 9 64.744 64.744 0 01-23-1.33 77.68 77.68 0 01-36.93-19.88 93.628 93.628 0 01-11.91-13.71 2.18 2.18 0 00-2.3-1.06 72.744 72.744 0 00-27.38 7.55c-11.6 6-20.67 14.58-26.4 26.45a10.134 10.134 0 01-3.7 4.7 8 8 0 01-9.19-.7 7.86 7.86 0 01-2.36-9.28 60.324 60.324 0 018.72-14.52c12.2-15.43 28.21-24.59 47.32-28.57A85.085 85.085 0 0173.07 87c.524.015 1-.307 1.18-.8a76.06 76.06 0 006.53-22.3c.351-2.652.518-5.325.5-8z"
fill="currentColor"></path>
<path
d="M136.26 108.34a44.742 44.742 0 01-11.13-2.87 46.108 46.108 0 01-19.66-13.76 8 8 0 015.72-13.22 7.93 7.93 0 016.54 2.93 33.27 33.27 0 0018.87 10.75c1.546.155 3.058.553 4.48 1.18a8.08 8.08 0 013.84 9.21c-.92 3.52-4.13 5.81-8.66 5.78zm-80.6-75.02a7.61 7.61 0 016.64 5 49.139 49.139 0 013.64 17 46.33 46.33 0 01-2.46 17.28c-2 5.77-8.24 7.79-12.89 4.15a8.1 8.1 0 01-2.39-9 31.679 31.679 0 001.68-12.36 35.77 35.77 0 00-2.43-11c-2.1-5.45 1.75-11.07 8.21-11.07zm22.26 93.25a8 8 0 01-6.68 7.86 32.88 32.88 0 00-19.7 12.19 8.13 8.13 0 01-11.21 1.62 8 8 0 01-1.41-11.58A51.043 51.043 0 0154 123.81a45.842 45.842 0 0114-5.1c5.35-1.04 9.91 2.56 9.92 7.86z"
fill="currentColor"></path>
</svg>
<h2 class="pl-8px text-16px font-bold" v-show="!isCollapse">
EPM-工程管理平台
</h2>
</RouterLink>
<el-menu class="layout-menu" :default-active="activeMenu" @select="menuSelect" router
:collapse="isCollapse">
<template v-for="router in menuList">
<template v-if="router.meta?.isShow">
<el-sub-menu v-if="router?.children?.filter((item: any) => item.meta?.isShow).length"
:index="router.path"
:key="router.path">
<template #title>
<div :class="router.meta.icon" class="menu-icon"></div>
<span>{{ router.meta.title }}</span>
</template>
<template v-for="child in router?.children">
<el-menu-item v-if="child?.meta?.isShow" :key="child.path"
:index="`${router.path}/${child.path}`">
<div :class="child.meta.icon" class="menu-icon"></div>
<span>{{ child.meta.title }}</span>
</el-menu-item>
</template>
</el-sub-menu>
<el-menu-item v-else :index="router.path" :key="router?.path">
<div :class="router.meta.icon" class="menu-icon"></div>
<span>{{ router.meta.title }}</span>
</el-menu-item>
</template>
</template>
</el-menu>
</el-aside>
<el-container>
<el-header>
<div class="flex items-center gap-col-2">
<el-button class="collapes-btn" @click="isCollapse = !isCollapse">
<div :class="isCollapse ? unfold : fold"></div>
</el-button>
<div>
{{ currentTime }}
</div>
</div>
</el-header>
<main class="main-wrap">
<RouterView/>
</main>
</el-container>
</el-container>
</div>
</template>
<script setup lang="ts">
import { useTheme } from '@/composables/useTheme'
import { defaultRouter } from '@/router'
import dayjs from 'dayjs'
const unfold = 'i-icon-park-outline:menu-unfold'
const fold = 'i-icon-park-outline:menu-fold'
const { theme } = useTheme()
const menuList = computed<any[]>(() => {
let data = defaultRouter[0].children
return data
}
)
const circleUrl = ref(
'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
)
const isCollapse = ref(false)
const getIconClass = (icon: string) => {
return icon
}
const { push, currentRoute } = useRouter()
const activeMenu = computed(() => {
const { meta, path } = unref(currentRoute)
return path
})
function menuSelect(path: string) {
push(path)
}
const currentTime = ref('123')
const updateTime = () => {
currentTime.value = dayjs().format('YYYY-MM-DD HH:mm:ss')
requestAnimationFrame(updateTime)
}
onMounted(() => {
updateTime()
})
</script>
<style lang="scss" scoped>
.common-layout {
@apply w-full h-full bg-[#F2F3F5];
:deep(.el-container) {
@apply w-full h-full;
}
:deep(.el-header) {
@apply h-56 w-full p0 flex items-center justify-between bg-white;
}
:deep(.el-main) {
@apply p0;
}
:deep(.el-aside) {
@apply w-auto;
}
:deep(.el-menu) {
@apply h-full;
}
.aside-wrap {
@apply flex-col;
border-radius: 4px;
height: calc(100vh - 16px);
}
.layout-logo {
@apply w-full flex-center nowrap-hidden h-56 bg-white;
}
.layout-menu {
height: calc(100vh - 56px);
@apply border-r-none;
}
.layout-menu:not(.el-menu--collapse) {
width: 220px;
height: calc(100vh - 56px);
}
.layout-logo {
transition: width 0.3s ease-in-out;
width: 220px;
}
.layout-logo-collapse {
width: 64px;
}
:deep(.el-header) {
@apply p-x-12;
}
.collapes-btn {
@apply border-none;
&:hover {
background-color: rgba(46, 51, 56, 0.09);
color: rgb(51, 54, 57);
}
}
.avatar {
@apply w-24 h-24;
}
.menu-icon {
@apply m-r-8;
}
.main-wrap {
height: calc(100vh - 56px);
@apply p-16;
}
}
</style>

26
tsconfig.app.json

@ -0,0 +1,26 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [
"env.d.ts",
"src/**/*",
"src/**/*.ts",
"src/**/*.vue",
"src/**/**/*.vue",
"src/**/**/*.ts",
"global.types/**/*.d.ts",
"*/*.d.ts",
],
"exclude": [
"src/**/__tests__/*"
],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"paths": {
"@/*": [
"./src/*"
]
}
}
}

20
tsconfig.json

@ -0,0 +1,20 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
],
"compilerOptions": {
"types": [
"node"
],
"moduleResolution": "node",
"typeRoots": [
"node_modules/@types"
]
},
}

21
tsconfig.node.json

@ -0,0 +1,21 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"composite": true, //
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": [
"node"
]
}
}

1
tsconfig.tsbuildinfo

@ -0,0 +1 @@
{"root":["./src/app.vue","./src/main.ts","./src/api/keys.ts","./src/api/basic/errorcode.ts","./src/api/basic/httptypes.ts","./src/api/basic/utils.ts","./src/api/module/index.ts","./src/api/module/firmware/index.ts","./src/api/module/taks/index.ts","./src/api/module/transfer/index.ts","./src/api/server/axiosinstance.ts","./src/api/server/config.ts","./src/components/edfs-input.vue","./src/components/edfs-button.vue","./src/components/edfs-dialog.vue","./src/components/edfs-number-input.vue","./src/components/edfs-wrap.vue","./src/components/edfs-table/defaults.ts","./src/components/edfs-table/index.vue","./src/composables/usemessage.ts","./src/composables/usetheme.ts","./src/composables/usezmqjsonworker.ts","./src/hooks/useg6/index.ts","./src/hooks/useg6/useg6.d.ts","./src/hooks/useg6/utils/generategraphdata.ts","./src/hooks/useg6/utils/mylineedge.ts","./src/hooks/useg6/utils/index.ts","./src/lib/zmq/zmqclient.d.ts","./src/router/index.ts","./src/stores/transferdata.ts","./src/types/device.ts","./src/uno-preset/src/index.ts","./src/utils/is.ts","./src/utils/zmq.ts","./src/utils/zmqjsonworker.ts","./src/views/engineering/index.vue","./src/views/layout/index.vue"],"errors":true,"version":"5.7.3"}

43
uno.config.js

@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var preset_rem_to_px_1 = require("@unocss/preset-rem-to-px");
var unocss_preset_scalpel_1 = require("unocss-preset-scalpel");
var index_1 = require("./src/uno-preset/src/index");
var unocss_1 = require("unocss");
exports.default = (0, unocss_1.defineConfig)({
shortcuts: [],
theme: {
colors: {},
},
content: {
pipeline: {
include: [
//参考:https://unocss.dev/guide/extracting#extracting-from-build-tools-pipeline
/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/,
'src/**/*.{js,ts}',
'src/router/index.ts',
'src/views/home/utils/menuConfig.ts',
],
},
},
rules: [['wh-full', { width: '100%', height: '100%' }]],
presets: [
(0, preset_rem_to_px_1.default)(),
(0, unocss_preset_scalpel_1.presetScalpel)(),
(0, unocss_1.presetWind3)(),
(0, index_1.presetSoybeanAdmin)(),
(0, unocss_1.presetAttributify)({
prefix: 'uno-',
prefixedOnly: true,
}),
(0, unocss_1.presetIcons)({
scale: 1.2,
warn: true,
}),
(0, unocss_1.presetTypography)(),
(0, unocss_1.presetWebFonts)({
fonts: {},
}),
],
transformers: [(0, unocss_1.transformerDirectives)(), (0, unocss_1.transformerVariantGroup)()],
});

51
uno.config.ts

@ -0,0 +1,51 @@
import presetRemToPx from '@unocss/preset-rem-to-px'
import { presetScalpel } from 'unocss-preset-scalpel'
import { presetSoybeanAdmin } from './src/uno-preset/src/index'
import {
defineConfig,
presetAttributify,
presetIcons,
presetTypography,
presetWind3,
presetWebFonts,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
export default defineConfig({
shortcuts: [],
theme: {
colors: {},
},
content: {
pipeline: {
include: [
//参考:https://unocss.dev/guide/extracting#extracting-from-build-tools-pipeline
/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/,
'src/**/*.{js,ts}',
'src/router/index.ts',
'src/views/home/utils/menuConfig.ts',
],
},
},
rules: [['wh-full', { width: '100%', height: '100%' }]],
presets: [
presetRemToPx(),
presetScalpel(),
presetWind3(),
presetSoybeanAdmin(),
presetAttributify({
prefix: 'uno-',
prefixedOnly: true,
}),
presetIcons({
scale: 1.2,
warn: true,
}),
presetTypography(),
presetWebFonts({
fonts: {},
}),
],
transformers: [transformerDirectives(), transformerVariantGroup()],
})

82
vite.config.ts

@ -0,0 +1,82 @@
import { fileURLToPath, URL } from 'node:url'
import UnoCSS from 'unocss/vite'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import vueJsx from '@vitejs/plugin-vue-jsx'
import Icons from 'unplugin-icons/vite'
// Vite 配置文件
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
console.log(env)
console.log(env.PROD)
return {
plugins: [
vue(),
vueJsx(),
vueDevTools(),
UnoCSS(),
Icons({
autoInstall: true,
}),
AutoImport({
imports: ['vue', 'vue-router'],
resolvers: [ElementPlusResolver()],
dts: 'global.types/auto-imports.d.ts',
}),
Components({
dirs: ['src/components'],
extensions: ['vue'],
dts: 'global.types/components.d.ts',
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
build: {
outDir: env.VITE_APP_ENV === 'cloud' ? 'dist-cloud' : 'dist-local',
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
},
css: {
preprocessorOptions: {
scss: {
additionalData: '@use "@/assets/styles/mixins.scss" as *;',
},
},
},
define: {
'process.env': {}
},
// 开发服务器配置
server: {
// 启动时自动打开浏览器
// 开发服务器端口
port: 3000,
// 允许局域网访问
host: '0.0.0.0',
proxy: {
'/remoteServer': {
target: env.VITE_BASE_URL,
changeOrigin: true,
secure: false,
ws: true,
rewrite: path => path.replace(/^\/remoteServer/, ''),
},
}
},
}
})
Loading…
Cancel
Save