xY{?$6~)KZ?w;a~!WdJV&Rk=eVA%ALfaDR1V9A{e@15@4lXewQ>K@R560{TfC>1?
vov(g2KlqgXzW`iD`^V7FCrP?=KT-Msu{*)N(g0`000000NkvXXu0mjfPng`l
literal 0
HcmV?d00001
diff --git a/src/assets/images/device/dgs.png b/src/assets/images/device/dgs.png
new file mode 100644
index 0000000000000000000000000000000000000000..683b0ac27153952f499ec74360cce7d8758b0578
GIT binary patch
literal 1260
zcmVPx(rb$FWRCr$PT3M(aMG!sb$A}sc1Y^X2A3;S#+(--rH6pIK;PN9XCN3DBVuBz6
zKQ!QvUxp}8MNuPyAc`m$K@cNEK_x+45F;VM-6(Zl0Bq}XByZ1z!kGZB%_P~4BOJOQ0JRN(W8a^(
zMxOw%;Rnz+cgn2FJ18k-$~Ka#YIz=nqX7uUc!y4GvQUjV`CUvyp=fvgeg3A^c2apZ2=h9c^_l&xMeDHLkt53
z0LFCV8~5(U==}~<@VuaBa*+g=EaN75d6#$M;YK9Nfvt7~y03NN-*C_x?N$w6#xOCnQU}*#Z
zJ4l`z=8ft%_;Rx*0rxXWcLR8`L}zCJ98i->+6>^{2;kZoqC`wBp609X(KfJNRVd2xt!hk?T(JKR6q
zkjQMW*nQ9GEC+aTJH0M^*6ku?Z-UP9p6^)yp5zY!oO=K~UIA*S0Nfc8+1ZghpBI}~
z3k1h~$GE@ZlC(b=SM>@f)eqPcGWewpozG8mHRxZVP6Kdc4V|4a;3WVz*D!QaqMZRS
zDTnIn9RTPm2h`oENy(3e0cRx_lAV*XaKd`GlDx8PjhIv52ms#zIAOxZPC|XF3dGE+j{4;$?_MR|Ch(5%_5hOWG!jq
zaN>=kVC(Puat*m{KM&pF`Z=Y}!zFRw6h_`pntvyB9IDY|mUj7pPPlnb~v5F0y
zxozBYQ>ShI6Abmy@A+svvFrA!*ZgzjBPZINQtTHx-Dm#y?7gKMb`|`T&xwzmenMXE
zq495}dF&Nww@*+sW)Ltd=q
zs#3Ie*@%Ra$(cddMDoW}>w
z#XD`7A>`hd*ZU7u21g7w!JmyA#Hxy4n?f9}uku4|lXLlN_l4gNl^wTgtJO?Z=7o4Q
ytnYX0@p)ezK6py?+|ymN`q?)IAn{86fxhO%(0E1H=hK1th{4m<&t;ucLK6Vm%ctuA
literal 0
HcmV?d00001
diff --git a/src/assets/images/device/ecu.png b/src/assets/images/device/ecu.png
new file mode 100644
index 0000000000000000000000000000000000000000..a8efdfac56d2a3b8008e99bad84b77d5410b3003
GIT binary patch
literal 1122
zcmV-o1fBbdP)Px(97#k$RCr$PTTO^gQ5b%njR<2gEQnGlAsIg<7FhYol!z>bgov0}$dcJB3qPe;
zO;ScFWHK8wiy<iA7{BfcpUIT*u@@_lo2a*SVbl
z5RrNS-&~m%fUbk&Mi)6;0k{QVhYQ03(X_a12Uh@o#+ZH^0R8)#v(obboGi(*I3mXb
zlACO@Spg7{6#$+^7`2hyWn*5eei2y&;ARAbUnHm5WN-mMPy2@vMhzr;ZOm)gFCxbP
zTnNv}(jKtMU?mWd5dgR_woROMe_A;`0JfD#i>!@r*8OSa%)|mjWG8?v0On?`SQ&nA
zNFE9If&Aypk8%0s?(H08A*8JoyZu9YC7>KV|?(v9QiV
z#)JhpYm>6_
z0SttcoJI0esQ)#9g$Y3@GytkgJxMiljrgKoS~JPP9KMK5Hr1s`;|+ip0QLjWgoow}
zqvt>)fS2PrBb0DK6O$Ky3nbPk;ef&cur8qj)slCG%ilg0$TJqGb}6$M?=8SvK)N!1
zCK(|U4gSA}pbv+AJ|H$B;)|ocIQlPV?~9|pI6BhZ^Tp9BUEo4RgS!FTs8XS%!8)C{0g`XS6Zv4s2SYUshQb-&TG+eG95FZ1;Bu)uRWumApyVRp
zXw#J54Cs3Sy|O#hvI1ZVfWiBIk=6vDo8*VlR<06%5m^RcTTE@TljH`QY*q_U1|rf8
zV2zDg!TQgUJY|!~6@WtkuGp9rtbf>L%yP0h0br~^-wbXpn3Cnh87jTmsHjX}%5sPs-ih
z-7CWJ9sY=ppYd0enT?5)VR$q+*@$?JiM%cC#!ON!?|X(aCy#<*<8b0{h|a<$xNnv;Q{H64OI+biV4T&
zM@0Rvq#8>)tF18{{}
z^7L)To0WZ_SBLsn{PSR_m8rPN+W0Ootb`(|mS6oAh2~`A&?^&PU(_Fv2SdGxn5p>L
zp=&kof=vUsB3`58f&x9Vap;rE54vqzE?AF&x&z`I9q*~#A=juJ0E;8y69GFd-X-
zJ~45FqJHIG_N82V!E;*7$t5bAvN)B0RUA_m#$=<=!_~aT2*ReZnqt>KFw^&{dMV|4Vb;T{h942W}djBMpA(@{5o4@66zwl(|NjEHNDG0?UK
zdj#l}$oF#0=<$MF!f*{1-Vyh(sPZ}cLg`B))iTd%GbbB|es{z@^jJHWwFh{9qGD>d
ztcl1*p!R?WZ^^)v9x-_o=s72zq2r8R6Y_y=0~lLF7bohbDlV*LF6hxCj{<#j6}QH1
zTluI}4d5r@ALy8gpYpWK*j9>cgPIXBz(oGn;bpp3adqAY`Xnk&Hjqkc$@52g{gZsa
za0!YGReS?f(YFJx*wk;y4dn*u)Xr4g0#`Azm2ZNR51|a
z1-%BuIj+*R=B^95it9q#R$jbq18_f~JtvO>y>5sY%Usa2M>YaIJ`oW*&Zs>hA6OfJ
zgGG15JuIkv&Y{pEBHZT6jBMpIdMNbScrI%XI9M}N@paX{T(G!DHUhO1`Ojw8DqfW)pvQnXN5^~mbjUR}4IowVF__~EtC-+E
zP&*SfsW%`Shx!rm0bM$}Rx{|@0XQf$Q}J!kw}vi;)a_8~ihmvj>Sro0upiB=r5X%t
zp-`&jcflFOW?F&gMLh0~pNzVmJl>Lj^#fh2dG#F|2jHOKOvSq(r&ybOfa8}$jBTsf
z&@rHAL|o&9o?Y?*j=9GZd8QnX$yy9ILcv77S>P>22c$c8Nu=^jux&>k0zD(*8e?<}
z>DeXSu^#ab18v)K6KA*;3J-{L44k3+?^FdNtGLlP2JC03;zrFe6yH>MgMkb7(<3*s
z>i{AmSMfXc&-g6{Ms}i;53r}9mWS{e!=9#?u%|h>k(~zMpkYkpDt^Hj7}<$WKER%a
zS{{Pq7Tw8D$gS)=00#|U64&?*IzHpm_#>$RP2Hc#{T08#K#!&uscXFp$fC=gzbP)W}ratn_Win=IC~
zDE1nFgN?+*P{oBYkjY)i{(pEFllQPDQ_TQ4
z_KN6WqT+_9%oKa9$#o!xjlsrK@rtN=5P>;PuqCf!CU1-p*6|VDD?U_17&gvg5FLDt
zP4FJCnmqBqQ3eoT5E0SAMBYvBj4~fFm)D*6oIDDKCxxNJM1bQFIXnOW002ovPDHLkV1iq@$8rDw
literal 0
HcmV?d00001
diff --git a/src/assets/images/device/ems.png b/src/assets/images/device/ems.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d28dd1cb923e73a9adaf908356e741db188c788
GIT binary patch
literal 1233
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEV430R;uumf=k1Kf(pw%PuJ=O(
ztU9(aJ?2s>JrK6xRYD=FT)4otrlX=eSolR<)*UEg-@#PG-g$y&9-}p5<3-!&bLXG=
z|M~wJZjO$NGcQXwoSSuzq2!thZ-kE6(k1m5PA2teas={pH8F-=GPQ~{P^dhfvdKK*
zZo?so$0bY!dFn5vZ|!HiQlnfEcOu+nznMyn<39%Drg&)qy$R17zgk{gpZ})c@vZ5@
zYZ9NAuw1c}z0kmV(AfCc6Q*f%nwU3C^WNn??~uvfhru8$AtZs-}o>145GRZB>J%K@W>5~5wpI?&*)?)nXckqwI
zNtNC;eTN&|otDiCx-?nphOzP*$GyP@_HIU=%B%Z$6>_i6Q+#qvB3bl+9L$ur9}bzc
z*YTQiH>Ib|;@>JVe)$dm!Ze2;;!jsS4S8+Hd{9_n68x!q2L{7A@qggry2usRxR0uY74z
zzj`X8}
zR4nbZYjVwt4?N{uJPElD5qq`1u{};>u|NOn1kZ`9b(e2T_e7uF{EF8?kJ;!d|FXS@
zEVL8mU7aX-!Jhfc%zI2cC$=7LmDuxUWA@+ohjv`GS$(ZRV~YOkWS7ZcnHT%tIPR_I
z=D8S`I7isveAaokAE$i+gnm7L#c(b>tG)f*w$HA-7UEUR7b3yM$o!AzuI8~6E}aZ4
Psu(<7{an^LB{Ts57ym$)
literal 0
HcmV?d00001
diff --git a/src/assets/images/device/ffs.png b/src/assets/images/device/ffs.png
new file mode 100644
index 0000000000000000000000000000000000000000..8629291207d2a581f9dd3d14a59f5045a222fea6
GIT binary patch
literal 1077
zcmV-51j_q~P)R&7~u;RG+L4VcuveEU0}`e4`eU&HUOF|
zBz;5AJxxx?H&`!;ujp}RHdhb)sR2}2<0hF_dYt`*1Gpn5cnl>2xFM61d;m0lMfm13
zwPPvihCw`$$uLX?&HKhD*zkf+YnL=vk^XTxkx9B*dw~r}uLi>;(9{DlR^T3|G*gp1
z;dvuss>}%y`~486Csu(8N^X^Zo^|Xke}uut%ZQm2!a||8^+Js6-#Yy6|u4@y!<$
zUXiU~ubh}G48d8;K^`K-0DSX>R(z)5C$cl_H;cRqeL;`2NQJU*p)lA-GaD-su?O&z7~!*-Wr_%UFC~4%C3|0zQ84TT
z1r-Xfis#wPG68E7;tpeS!~P}d_pgZyY;e}`YBTHvMHKpd01I2c=UJO7d<@3o*4BR}
zv;SP-p5Srz2L_N6B{nP-?$)0%$Cy<3KYFe)Q((dX{CaB)qrgB4g)72Y$5&(*srDZR
zfKfkZF1-hONw
z3{QsQa-vYUFF4yOipLZR4LCb({RZberXo)w$^blD1v=Z`fV0T!T^R;|Q8-j!>h-P+
z1HdR8DlqkWSB3##6b==bdc7;d05A%N3QWD;m0szE4>ZiR8s9f3;?5$s=$U+`ws)aqo9Bh@el6e631FGp}>la2ItR-5<91*cBCb4&+<-n00000NkvXXu0mjf1JdW|
literal 0
HcmV?d00001
diff --git a/src/assets/images/device/group.png b/src/assets/images/device/group.png
new file mode 100644
index 0000000000000000000000000000000000000000..cebce404fc777f3aba00d1d87d1ea389088b354d
GIT binary patch
literal 878
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU^ex1aSW-L^LEz7LSaJzhj7s)
zEOYoY4$2yo2xu)}abhoFC~@F?sZz%N@GE+j{4;$?_MR|Ch(5%_5hOWG!jq
zaN>=kVC(Puat*m{KM&pF`Z=Y}!zFRw6h_`pntvyB9IDY|mUj7pPPlnb~v5F0y
zxozBYQ>ShI6Abmy@A+svvFrA!*ZgzjBPZINQtTHx-Dm#y?7gKMb`|`T&xwzmenMXE
zq495}dF&Nww@*+sW)Ltd=q
zs#3Ie*@%Ra$(cddMDoW}>w
z#XD`7A>`hd*ZU7u21g7w!JmyA#Hxy4n?f9}uku4|lXLlN_l4gNl^wTgtJO?Z=7o4Q
ytnYX0@p)ezK6py?+|ymN`q?)IAn{86fxhO%(0E1H=hK1th{4m<&t;ucLK6Vm%ctuA
literal 0
HcmV?d00001
diff --git a/src/assets/images/device/mppt.png b/src/assets/images/device/mppt.png
new file mode 100644
index 0000000000000000000000000000000000000000..9581849c5445377640eda4b32cc0251f553161cf
GIT binary patch
literal 1796
zcmV+f2mAPmP)Px*zDYzuRCr$PS_!CDRS^C@h-`t%j7p-ioeCw=7R)l!ByFY8vds+2Hrvb0GA%N*
zEZa*cOcN{>Ez1_sBr!A4Ak?I+Kr5m}49TMH>(2F@pYz}M|L@)Bz4!Nj%fLh5yXVZA
z`Och~Gc$*1 HDBjsZyO44^xJ9?h9o!?7Cyd=6kO$=}ngo5}(t
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
Eg7Bgeg8%>k
literal 0
HcmV?d00001
diff --git a/src/assets/images/device/unit.png b/src/assets/images/device/unit.png
new file mode 100644
index 0000000000000000000000000000000000000000..a8efdfac56d2a3b8008e99bad84b77d5410b3003
GIT binary patch
literal 1122
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
V
+
+
+
+
+
+
diff --git a/src/components/Edfs-button.vue b/src/components/Edfs-button.vue
new file mode 100644
index 0000000..5c3aad1
--- /dev/null
+++ b/src/components/Edfs-button.vue
@@ -0,0 +1,67 @@
+
+
+ {{ innerText }}
+
+
+
+
+
+
diff --git a/src/components/Edfs-dialog.vue b/src/components/Edfs-dialog.vue
new file mode 100644
index 0000000..03d2eef
--- /dev/null
+++ b/src/components/Edfs-dialog.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Edfs-number-input.vue b/src/components/Edfs-number-input.vue
new file mode 100644
index 0000000..3f46ba3
--- /dev/null
+++ b/src/components/Edfs-number-input.vue
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
diff --git a/src/components/Edfs-table/defaults.ts b/src/components/Edfs-table/defaults.ts
new file mode 100644
index 0000000..45628c9
--- /dev/null
+++ b/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 = TableProps & IPaging
\ No newline at end of file
diff --git a/src/components/Edfs-table/index.vue b/src/components/Edfs-table/index.vue
new file mode 100644
index 0000000..b5408de
--- /dev/null
+++ b/src/components/Edfs-table/index.vue
@@ -0,0 +1,213 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Edfs-wrap.vue b/src/components/Edfs-wrap.vue
new file mode 100644
index 0000000..973b02a
--- /dev/null
+++ b/src/components/Edfs-wrap.vue
@@ -0,0 +1,224 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/composables/useMessage.ts b/src/composables/useMessage.ts
new file mode 100644
index 0000000..b2fb1f7
--- /dev/null
+++ b/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',
+ })
+ },
+ }
+}
+
diff --git a/src/composables/useTheme.ts b/src/composables/useTheme.ts
new file mode 100644
index 0000000..3762aaf
--- /dev/null
+++ b/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,
+ }
+}
diff --git a/src/composables/useZMQJsonWorker.ts b/src/composables/useZMQJsonWorker.ts
new file mode 100644
index 0000000..abe2aa2
--- /dev/null
+++ b/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> = new Map();
+ private pubTimeoutHandlers: Map void> = new Map();
+ private readonly host: string;
+ private statusCallback: ((status: string) => void) | null = null;
+ private isAlwaysListenMsgMap: Map> = 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();
+ 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(topic: string, msg: PublishMsg, 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) {
+ 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;
+
diff --git a/src/hooks/useG6/index.ts b/src/hooks/useG6/index.ts
new file mode 100644
index 0000000..28d1d8b
--- /dev/null
+++ b/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({
+ tree,
+ nodeComponent,
+ nodeSize,
+ layoutType = 'TB'
+ }: IUseG6Options,
+ plugins?: PluginOptions
+) {
+
+ register(ExtensionCategory.NODE, 'vue-node', VueNode);
+ register(ExtensionCategory.EDGE, 'my-line-edge', MyLineEdge);
+
+ const container = ref()
+ 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
+ }
+}
\ No newline at end of file
diff --git a/src/hooks/useG6/useG6.d.ts b/src/hooks/useG6/useG6.d.ts
new file mode 100644
index 0000000..04328c0
--- /dev/null
+++ b/src/hooks/useG6/useG6.d.ts
@@ -0,0 +1,9 @@
+import type { Component } from "vue";
+
+
+interface IUseG6Options {
+ tree: T[],
+ nodeComponent: Component,
+ nodeSize: [number, number]
+ layoutType?: 'LR' | 'TB'
+}
\ No newline at end of file
diff --git a/src/hooks/useG6/utils/GenerateGraphData.ts b/src/hooks/useG6/utils/GenerateGraphData.ts
new file mode 100644
index 0000000..c5f89e5
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/src/hooks/useG6/utils/MyLineEdge.ts b/src/hooks/useG6/utils/MyLineEdge.ts
new file mode 100644
index 0000000..b30138c
--- /dev/null
+++ b/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,
+ // });
+ }
+}
diff --git a/src/hooks/useG6/utils/index.ts b/src/hooks/useG6/utils/index.ts
new file mode 100644
index 0000000..23d5a6e
--- /dev/null
+++ b/src/hooks/useG6/utils/index.ts
@@ -0,0 +1,4 @@
+export * from './GenerateGraphData'
+export * from './MyLineEdge'
+
+
diff --git a/src/lib/awesome-qr.js b/src/lib/awesome-qr.js
new file mode 100644
index 0000000..b1c91f8
--- /dev/null
+++ b/src/lib/awesome-qr.js
@@ -0,0 +1 @@
+!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.AwesomeQR=e():t.AwesomeQR=e()}(this,(function(){return(()=>{var t={154:(t,e,r)=>{const o=r(342);e.parseFont=o,e.createCanvas=function(t,e){return Object.assign(document.createElement("canvas"),{width:t,height:e})},e.createImageData=function(t,e,r){switch(arguments.length){case 0:return new ImageData;case 1:return new ImageData(t);case 2:return new ImageData(t,e);default:return new ImageData(t,e,r)}},e.loadImage=function(t,e){return new Promise((function(r,o){const n=Object.assign(document.createElement("img"),e);function i(){n.onload=null,n.onerror=null}n.onload=function(){i(),r(n)},n.onerror=function(){i(),o(new Error('Failed to load the image "'+t+'"'))},n.src=t}))}},342:t=>{"use strict";const e="'([^']+)'|\"([^\"]+)\"|[\\w\\s-]+",r=new RegExp("(bold|bolder|lighter|[1-9]00) +","i"),o=new RegExp("(italic|oblique) +","i"),n=new RegExp("(small-caps) +","i"),i=new RegExp("(ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded) +","i"),a=new RegExp("([\\d\\.]+)(px|pt|pc|in|cm|mm|%|em|ex|ch|rem|q) *((?:"+e+")( *, *(?:"+e+"))*)"),s={};t.exports=function(t){if(s[t])return s[t];const e=a.exec(t);if(!e)return;const l={weight:"normal",style:"normal",stretch:"normal",variant:"normal",size:parseFloat(e[1]),unit:e[2],family:e[3].replace(/["']/g,"").replace(/ *, */g,",")};let u,h,c,f,d=t.substring(0,e.index);switch((u=r.exec(d))&&(l.weight=u[1]),(h=o.exec(d))&&(l.style=h[1]),(c=n.exec(d))&&(l.variant=c[1]),(f=i.exec(d))&&(l.stretch=f[1]),l.unit){case"pt":l.size/=.75;break;case"pc":l.size*=16;break;case"in":l.size*=96;break;case"cm":l.size*=96/2.54;break;case"mm":l.size*=96/25.4;break;case"%":break;case"em":case"rem":l.size*=16/.75;break;case"q":l.size*=96/25.4/4}return s[t]=l}},662:(t,e)=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.loop=e.conditional=e.parse=void 0,e.parse=function t(e,r){var o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:o;if(Array.isArray(r))r.forEach((function(r){return t(e,r,o,n)}));else if("function"==typeof r)r(e,o,n,t);else{var i=Object.keys(r)[0];Array.isArray(r[i])?(n[i]={},t(e,r[i],o,n[i])):n[i]=r[i](e,o,n,t)}return o},e.conditional=function(t,e){return function(r,o,n,i){e(r,o,n)&&i(r,t,o,n)}},e.loop=function(t,e){return function(r,o,n,i){for(var a=[];e(r,o,n);){var s={};i(r,t,o,s),a.push(s)}return a}}},58:(t,e)=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.readBits=e.readArray=e.readUnsigned=e.readString=e.peekBytes=e.readBytes=e.peekByte=e.readByte=e.buildStream=void 0,e.buildStream=function(t){return{data:t,pos:0}};e.readByte=function(){return function(t){return t.data[t.pos++]}},e.peekByte=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;return function(e){return e.data[e.pos+t]}};var r=function(t){return function(e){return e.data.subarray(e.pos,e.pos+=t)}};e.readBytes=r,e.peekBytes=function(t){return function(e){return e.data.subarray(e.pos,e.pos+t)}},e.readString=function(t){return function(e){return Array.from(r(t)(e)).map((function(t){return String.fromCharCode(t)})).join("")}},e.readUnsigned=function(t){return function(e){var o=r(2)(e);return t?(o[1]<<8)+o[0]:(o[0]<<8)+o[1]}},e.readArray=function(t,e){return function(o,n,i){for(var a="function"==typeof e?e(o,n,i):e,s=r(t),l=new Array(a),u=0;u{"use strict";e.Z=void 0;var o=r(662),n=r(58),i={blocks:function(t){for(var e=[],r=t.data.length,o=0,i=(0,n.readByte)()(t);0!==i;i=(0,n.readByte)()(t)){if(t.pos+i>=r){var a=r-t.pos;e.push((0,n.readBytes)(a)(t)),o+=a;break}e.push((0,n.readBytes)(i)(t)),o+=i}for(var s=new Uint8Array(o),l=0,u=0;u0&&n[n.length-1])||6!==i[0]&&2!==i[0])){a=0;continue}if(3===i[0]&&(!n||i[1]>n[0]&&i[1]1)throw new Error("dotScale should be in range (0, 1].");r.components.data.scale=r.dotScale,r.components.timing.scale=r.dotScale,r.components.alignment.scale=r.dotScale}this.options=r,this.canvas=s.createCanvas(e.size,e.size),this.canvasContext=this.canvas.getContext("2d"),this.qrCode=new u.QRCodeModel(-1,this.options.correctLevel),Number.isInteger(this.options.maskPattern)&&(this.qrCode.maskPattern=this.options.maskPattern),Number.isInteger(this.options.version)&&(this.qrCode.typeNumber=this.options.version),this.qrCode.addData(this.options.text),this.qrCode.make()}return t.prototype.draw=function(){var t=this;return new Promise((function(e){return t._draw().then(e)}))},t.prototype._clear=function(){this.canvasContext.clearRect(0,0,this.canvas.width,this.canvas.height)},t._prepareRoundedCornerClip=function(t,e,r,o,n,i){t.beginPath(),t.moveTo(e,r),t.arcTo(e+o,r,e+o,r+n,i),t.arcTo(e+o,r+n,e,r+n,i),t.arcTo(e,r+n,e,r,i),t.arcTo(e,r,e+o,r,i),t.closePath()},t._getAverageRGB=function(t){var e,r,o={r:0,g:0,b:0},n=-4,i={r:0,g:0,b:0},a=0;r=t.naturalHeight||t.height,e=t.naturalWidth||t.width;var l,u=s.createCanvas(e,r).getContext("2d");if(!u)return o;u.drawImage(t,0,0);try{l=u.getImageData(0,0,e,r)}catch(t){return o}for(;(n+=20)200||l.data[n+1]>200||l.data[n+2]>200||(++a,i.r+=l.data[n],i.g+=l.data[n+1],i.b+=l.data[n+2]);return i.r=~~(i.r/a),i.g=~~(i.g/a),i.b=~~(i.b/a),i},t._drawDot=function(t,e,r,o,n,i){void 0===n&&(n=0),void 0===i&&(i=1),t.fillRect((e+n)*o,(r+n)*o,i*o,i*o)},t._drawAlignProtector=function(t,e,r,o){t.clearRect((e-2)*o,(r-2)*o,5*o,5*o),t.fillRect((e-2)*o,(r-2)*o,5*o,5*o)},t._drawAlign=function(e,r,o,n,i,a,s,l){void 0===i&&(i=0),void 0===a&&(a=1);var u=e.fillStyle;e.fillStyle=s,new Array(4).fill(0).map((function(s,l){t._drawDot(e,r-2+l,o-2,n,i,a),t._drawDot(e,r+2,o-2+l,n,i,a),t._drawDot(e,r+2-l,o+2,n,i,a),t._drawDot(e,r-2,o+2-l,n,i,a)})),t._drawDot(e,r,o,n,i,a),l||(e.fillStyle="rgba(255, 255, 255, 0.6)",new Array(2).fill(0).map((function(s,l){t._drawDot(e,r-1+l,o-1,n,i,a),t._drawDot(e,r+1,o-1+l,n,i,a),t._drawDot(e,r+1-l,o+1,n,i,a),t._drawDot(e,r-1,o+1-l,n,i,a)}))),e.fillStyle=u},t.prototype._draw=function(){var e,r,o,a,f,p,g,m,v,y,w,b,C,B,P,x,A,E,R;return n(this,void 0,void 0,(function(){var n,k,D,T,_,L,M,S,I,O,N,Q,F,U,j,z,G,H,q,K,X,Y,J,Z,W,V,$,tt,et,rt,ot,nt,it,at,st,lt,ut,ht,ct,ft,dt,pt,gt,mt,vt,yt,wt,bt,Ct,Bt,Pt,xt,At,Et,Rt,kt,Dt,Tt,_t,Lt,Mt,St,It;return i(this,(function(i){switch(i.label){case 0:if(n=null===(e=this.qrCode)||void 0===e?void 0:e.moduleCount,k=this.options.size,((D=this.options.margin)<0||2*D>=k)&&(D=0),T=Math.ceil(D),_=k-2*D,L=this.options.whiteMargin,M=this.options.backgroundDimming,S=Math.ceil(_/n),O=(I=S*n)+2*T,N=s.createCanvas(O,O),Q=N.getContext("2d"),this._clear(),Q.save(),Q.translate(T,T),F=s.createCanvas(O,O),U=F.getContext("2d"),j=null,z=[],!this.options.gifBackground)return[3,1];if(G=l.parseGIF(this.options.gifBackground),j=G,z=l.decompressFrames(G,!0),this.options.autoColor){for(H=0,q=0,K=0,X=0,gt=0;gt200||Y[1]>200||Y[2]>200||0===Y[0]&&0===Y[1]&&0===Y[2]||(X++,H+=Y[0],q+=Y[1],K+=Y[2]);H=~~(H/X),q=~~(q/X),K=~~(K/X),this.options.colorDark="rgb("+H+","+q+","+K+")"}return[3,4];case 1:return this.options.backgroundImage?[4,s.loadImage(this.options.backgroundImage)]:[3,3];case 2:return J=i.sent(),this.options.autoColor&&(Z=t._getAverageRGB(J),this.options.colorDark="rgb("+Z.r+","+Z.g+","+Z.b+")"),U.drawImage(J,0,0,J.width,J.height,0,0,O,O),U.rect(0,0,O,O),U.fillStyle=M,U.fill(),[3,4];case 3:U.rect(0,0,O,O),U.fillStyle=this.options.colorLight,U.fill(),i.label=4;case 4:for(W=u.QRUtil.getPatternPosition(this.qrCode.typeNumber),V=(null===(o=null===(r=this.options.components)||void 0===r?void 0:r.data)||void 0===o?void 0:o.scale)||c,$=.5*(1-V),tt=0;tt=8&&et<=n-8||6==et&&tt>=8&&tt<=n-8,nt=et<8&&(tt<8||tt>=n-8)||et>=n-8&&tt<8||ot,gt=1;gt=W[gt]-2&&tt<=W[gt]+2&&et>=W[gt]-2&&et<=W[gt]+2;it=et*S+(nt?0:$*S),at=tt*S+(nt?0:$*S),Q.strokeStyle=rt?this.options.colorDark:this.options.colorLight,Q.lineWidth=.5,Q.fillStyle=rt?this.options.colorDark:"rgba(255, 255, 255, 0.6)",0===W.length?nt||Q.fillRect(it,at,(nt?1:V)*S,(nt?1:V)*S):(st=et=n-4-5&&tt=n-4-5,nt||st||Q.fillRect(it,at,(nt?1:V)*S,(nt?1:V)*S))}if(lt=W[W.length-1],Q.fillStyle="rgba(255, 255, 255, 0.6)",Q.fillRect(0,0,8*S,8*S),Q.fillRect(0,(n-8)*S,8*S,8*S),Q.fillRect((n-8)*S,0,8*S,8*S),(null===(f=null===(a=this.options.components)||void 0===a?void 0:a.timing)||void 0===f?void 0:f.protectors)&&(Q.fillRect(8*S,6*S,(n-8-8)*S,S),Q.fillRect(6*S,8*S,S,(n-8-8)*S)),(null===(g=null===(p=this.options.components)||void 0===p?void 0:p.cornerAlignment)||void 0===g?void 0:g.protectors)&&t._drawAlignProtector(Q,lt,lt,S),null===(v=null===(m=this.options.components)||void 0===m?void 0:m.alignment)||void 0===v?void 0:v.protectors)for(gt=0;gt=1)&&(bt=.2),Ct<0&&(Ct=0),Bt<0&&(Bt=0),At=xt=.5*(O-(Pt=I*bt)),Q.restore(),Q.fillStyle="#FFFFFF",Q.save(),t._prepareRoundedCornerClip(Q,xt-Ct,At-Ct,Pt+2*Ct,Pt+2*Ct,Bt+Ct),Q.clip(),Et=Q.globalCompositeOperation,Q.globalCompositeOperation="destination-out",Q.fill(),Q.globalCompositeOperation=Et,Q.restore(),Q.save(),t._prepareRoundedCornerClip(Q,xt,At,Pt,Pt,Bt),Q.clip(),Q.drawImage(wt,xt,At,Pt,Pt),Q.restore(),Q.save(),Q.translate(T,T),i.label=6;case 6:if(j){if(z.forEach((function(t){Rt||((Rt=new h.default(k,k)).setDelay(t.delay),Rt.setRepeat(0));var e=t.dims,r=e.width,o=e.height;kt||(kt=s.createCanvas(r,o),(Dt=kt.getContext("2d")).rect(0,0,kt.width,kt.height),Dt.fillStyle="#ffffff",Dt.fill()),Tt&&Lt&&r===Tt.width&&o===Tt.height||(Tt=s.createCanvas(r,o),_t=Tt.getContext("2d"),Lt=_t.createImageData(r,o)),Lt.data.set(t.patch),_t.putImageData(Lt,0,0),Dt.drawImage(Tt,t.dims.left,t.dims.top);var n=s.createCanvas(O,O),i=n.getContext("2d");i.drawImage(kt,0,0,O,O),i.rect(0,0,O,O),i.fillStyle=M,i.fill(),i.drawImage(N,0,0,O,O);var a=s.createCanvas(k,k),l=a.getContext("2d");l.drawImage(n,0,0,k,k),Rt.addFrame(l.getImageData(0,0,a.width,a.height).data)})),!Rt)throw new Error("No frames.");return Rt.finish(),d(this.canvas)?(Mt=Rt.stream().toFlattenUint8Array(),St=Mt.reduce((function(t,e){return t+String.fromCharCode(e)}),""),[2,Promise.resolve("data:image/gif;base64,"+window.btoa(St))]):[2,Promise.resolve(Buffer.from(Rt.stream().toFlattenUint8Array()))]}return U.drawImage(N,0,0,O,O),Q.drawImage(F,-T,-T,O,O),(It=s.createCanvas(k,k)).getContext("2d").drawImage(N,0,0,k,k),this.canvas=It,d(this.canvas)?[2,Promise.resolve(this.canvas.toDataURL())]:[2,Promise.resolve(this.canvas.toBuffer())]}}))}))},t.CorrectLevel=u.QRErrorCorrectLevel,t.defaultComponentOptions={data:{scale:1},timing:{scale:1,protectors:!1},alignment:{scale:1,protectors:!1},cornerAlignment:{scale:1,protectors:!0}},t.defaultOptions={text:"",size:400,margin:20,colorDark:"#000000",colorLight:"#ffffff",correctLevel:u.QRErrorCorrectLevel.M,backgroundImage:void 0,backgroundDimming:"rgba(0,0,0,0)",logoImage:void 0,logoScale:.2,logoMargin:4,logoCornerRadius:8,whiteMargin:!0,components:t.defaultComponentOptions,autoColor:!0},t}();function d(t){try{return t instanceof HTMLElement}catch(e){return"object"==typeof t&&1===t.nodeType&&"object"==typeof t.style&&"object"==typeof t.ownerDocument}}e.AwesomeQR=f},642:function(t,e,r){"use strict";var o=this&&this.__createBinding||(Object.create?function(t,e,r,o){void 0===o&&(o=r),Object.defineProperty(t,o,{enumerable:!0,get:function(){return e[r]}})}:function(t,e,r,o){void 0===o&&(o=r),t[o]=e[r]}),n=this&&this.__exportStar||function(t,e){for(var r in t)"default"===r||e.hasOwnProperty(r)||o(e,t,r)};Object.defineProperty(e,"__esModule",{value:!0}),n(r(937),e);var i=r(294);Object.defineProperty(e,"AwesomeQR",{enumerable:!0,get:function(){return i.AwesomeQR}})},937:(t,e)=>{"use strict";function r(t){var e=encodeURI(t).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return e.length+(e.length!=Number(t)?3:0)}Object.defineProperty(e,"__esModule",{value:!0}),e.QRMath=e.QRUtil=e.QRMaskPattern=e.QRErrorCorrectLevel=e.QRCodeModel=void 0;var o=function(){function t(t){this.mode=i.MODE_8BIT_BYTE,this.parsedData=[],this.data=t;for(var e=[],r=0,o=this.data.length;r65536?(n[0]=240|(1835008&a)>>>18,n[1]=128|(258048&a)>>>12,n[2]=128|(4032&a)>>>6,n[3]=128|63&a):a>2048?(n[0]=224|(61440&a)>>>12,n[1]=128|(4032&a)>>>6,n[2]=128|63&a):a>128?(n[0]=192|(1984&a)>>>6,n[1]=128|63&a):n[0]=a,e.push(n)}this.parsedData=Array.prototype.concat.apply([],e),this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}return t.prototype.getLength=function(){return this.parsedData.length},t.prototype.write=function(t){for(var e=0,r=this.parsedData.length;ec.length)throw new Error("Too long data");return n}(t,this.errorCorrectLevel);else{if(this.typeNumber>40)throw new Error("Invalid QR version: "+this.typeNumber);if(!function(t,o,n){var i=r(o),a=t-1,s=0;switch(n){case e.QRErrorCorrectLevel.L:s=c[a][0];break;case e.QRErrorCorrectLevel.M:s=c[a][1];break;case e.QRErrorCorrectLevel.Q:s=c[a][2];break;case e.QRErrorCorrectLevel.H:s=c[a][3]}return i<=s}(this.typeNumber,t,this.errorCorrectLevel))throw new Error("Data is too long for QR version: "+this.typeNumber)}var n=new o(t);this.dataList.push(n),this.dataCache=void 0},t.prototype.isDark=function(t,e){if(t<0||this.moduleCount<=t||e<0||this.moduleCount<=e)throw new Error(t+","+e);return this.modules[t][e]},t.prototype.getModuleCount=function(){return this.moduleCount},t.prototype.make=function(){this.makeImpl(!1,this.getBestMaskPattern())},t.prototype.makeImpl=function(e,r){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var o=0;o=7&&this.setupTypeNumber(e),null==this.dataCache&&(this.dataCache=t.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,r)},t.prototype.setupPositionProbePattern=function(t,e){for(var r=-1;r<=7;r++)if(!(t+r<=-1||this.moduleCount<=t+r))for(var o=-1;o<=7;o++)e+o<=-1||this.moduleCount<=e+o||(this.modules[t+r][e+o]=0<=r&&r<=6&&(0==o||6==o)||0<=o&&o<=6&&(0==r||6==r)||2<=r&&r<=4&&2<=o&&o<=4)},t.prototype.getBestMaskPattern=function(){if(Number.isInteger(this.maskPattern)&&Object.values(e.QRMaskPattern).includes(this.maskPattern))return this.maskPattern;for(var t=0,r=0,o=0;o<8;o++){this.makeImpl(!0,o);var n=a.getLostPoint(this);(0==o||t>n)&&(t=n,r=o)}return r},t.prototype.setupTimingPattern=function(){for(var t=8;t>r&1);this.modules[Math.floor(r/3)][r%3+this.moduleCount-8-3]=o}for(r=0;r<18;r++)o=!t&&1==(e>>r&1),this.modules[r%3+this.moduleCount-8-3][Math.floor(r/3)]=o},t.prototype.setupTypeInfo=function(t,e){for(var r=this.errorCorrectLevel<<3|e,o=a.getBCHTypeInfo(r),n=0;n<15;n++){var i=!t&&1==(o>>n&1);n<6?this.modules[n][8]=i:n<8?this.modules[n+1][8]=i:this.modules[this.moduleCount-15+n][8]=i}for(n=0;n<15;n++)i=!t&&1==(o>>n&1),n<8?this.modules[8][this.moduleCount-n-1]=i:n<9?this.modules[8][15-n-1+1]=i:this.modules[8][15-n-1]=i;this.modules[this.moduleCount-8][8]=!t},t.prototype.mapData=function(t,e){for(var r=-1,o=this.moduleCount-1,n=7,i=0,s=this.moduleCount-1;s>0;s-=2)for(6==s&&s--;;){for(var l=0;l<2;l++)if(null==this.modules[o][s-l]){var u=!1;i>>n&1)),a.getMask(e,o,s-l)&&(u=!u),this.modules[o][s-l]=u,-1==--n&&(i++,n=7)}if((o+=r)<0||this.moduleCount<=o){o-=r,r=-r;break}}},t.createData=function(e,r,o){for(var n=u.getRSBlocks(e,r),i=new h,s=0;s8*c)throw new Error("code length overflow. ("+i.getLengthInBits()+">"+8*c+")");for(i.getLengthInBits()+4<=8*c&&i.put(0,4);i.getLengthInBits()%8!=0;)i.putBit(!1);for(;!(i.getLengthInBits()>=8*c||(i.put(t.PAD0,8),i.getLengthInBits()>=8*c));)i.put(t.PAD1,8);return t.createBytes(i,n)},t.createBytes=function(t,e){for(var r=0,o=0,n=0,i=new Array(e.length),s=new Array(e.length),u=0;u=0?p.get(g):0}}var m=0;for(f=0;f=0;)r^=t.G15<=0;)r^=t.G18<>>=1;return e},t.getPatternPosition=function(e){return t.PATTERN_POSITION_TABLE[e-1]},t.getMask=function(t,r,o){switch(t){case e.QRMaskPattern.PATTERN000:return(r+o)%2==0;case e.QRMaskPattern.PATTERN001:return r%2==0;case e.QRMaskPattern.PATTERN010:return o%3==0;case e.QRMaskPattern.PATTERN011:return(r+o)%3==0;case e.QRMaskPattern.PATTERN100:return(Math.floor(r/2)+Math.floor(o/3))%2==0;case e.QRMaskPattern.PATTERN101:return r*o%2+r*o%3==0;case e.QRMaskPattern.PATTERN110:return(r*o%2+r*o%3)%2==0;case e.QRMaskPattern.PATTERN111:return(r*o%3+(r+o)%2)%2==0;default:throw new Error("bad maskPattern:"+t)}},t.getErrorCorrectPolynomial=function(t){for(var e=new l([1],0),r=0;r5&&(r+=3+i-5)}for(o=0;o=256;)e-=255;return t.EXP_TABLE[e]},t.EXP_TABLE=new Array(256),t.LOG_TABLE=new Array(256),t._constructor=function(){for(var e=0;e<8;e++)t.EXP_TABLE[e]=1<>>7-t%8&1)},t.prototype.put=function(t,e){for(var r=0;r>>e-r-1&1))},t.prototype.getLengthInBits=function(){return this.length},t.prototype.putBit=function(t){var e=Math.floor(this.length/8);this.buffer.length<=e&&this.buffer.push(0),t&&(this.buffer[e]|=128>>>this.length%8),this.length++},t}(),c=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]]},137:(t,e,r)=>{var o=r(110),n=r(970);function i(){this.page=-1,this.pages=[],this.newPage()}i.pageSize=4096,i.charMap={};for(var a=0;a<256;a++)i.charMap[a]=String.fromCharCode(a);function s(t,e){this.width=~~t,this.height=~~e,this.transparent=null,this.transIndex=0,this.repeat=-1,this.delay=0,this.image=null,this.pixels=null,this.indexedPixels=null,this.colorDepth=null,this.colorTab=null,this.neuQuant=null,this.usedEntry=new Array,this.palSize=7,this.dispose=-1,this.firstFrame=!0,this.sample=10,this.dither=!1,this.globalPalette=!1,this.out=new i}i.prototype.newPage=function(){this.pages[++this.page]=new Uint8Array(i.pageSize),this.cursor=0},i.prototype.getData=function(){for(var t="",e=0;et+e.length),0));return t.reduce(((t,e)=>(r.set(e,t),t+e.length)),0),r},i.prototype.writeByte=function(t){this.cursor>=i.pageSize&&this.newPage(),this.pages[this.page][this.cursor++]=t},i.prototype.writeUTFBytes=function(t){for(var e=t.length,r=0;r=0&&(this.dispose=t)},s.prototype.setRepeat=function(t){this.repeat=t},s.prototype.setTransparent=function(t){this.transparent=t},s.prototype.addFrame=function(t){this.image=t,this.colorTab=this.globalPalette&&this.globalPalette.slice?this.globalPalette:null,this.getImagePixels(),this.analyzePixels(),!0===this.globalPalette&&(this.globalPalette=this.colorTab),this.firstFrame&&(this.writeHeader(),this.writeLSD(),this.writePalette(),this.repeat>=0&&this.writeNetscapeExt()),this.writeGraphicCtrlExt(),this.writeImageDesc(),this.firstFrame||this.globalPalette||this.writePalette(),this.writePixels(),this.firstFrame=!1},s.prototype.finish=function(){this.out.writeByte(59)},s.prototype.setQuality=function(t){t<1&&(t=1),this.sample=t},s.prototype.setDither=function(t){!0===t&&(t="FloydSteinberg"),this.dither=t},s.prototype.setGlobalPalette=function(t){this.globalPalette=t},s.prototype.getGlobalPalette=function(){return this.globalPalette&&this.globalPalette.slice&&this.globalPalette.slice(0)||this.globalPalette},s.prototype.writeHeader=function(){this.out.writeUTFBytes("GIF89a")},s.prototype.analyzePixels=function(){this.colorTab||(this.neuQuant=new o(this.pixels,this.sample),this.neuQuant.buildColormap(),this.colorTab=this.neuQuant.getColormap()),this.dither?this.ditherPixels(this.dither.replace("-serpentine",""),null!==this.dither.match(/-serpentine/)):this.indexPixels(),this.pixels=null,this.colorDepth=8,this.palSize=7,null!==this.transparent&&(this.transIndex=this.findClosest(this.transparent,!0))},s.prototype.indexPixels=function(t){var e=this.pixels.length/3;this.indexedPixels=new Uint8Array(e);for(var r=0,o=0;o=0&&C+h=0&&B+u>16,(65280&t)>>8,255&t,e)},s.prototype.findClosestRGB=function(t,e,r,o){if(null===this.colorTab)return-1;if(this.neuQuant&&!o)return this.neuQuant.lookupRGB(t,e,r);for(var n=0,i=16777216,a=this.colorTab.length,s=0,l=0;s=0&&(e=7&this.dispose),e<<=2,this.out.writeByte(0|e|t),this.writeShort(this.delay),this.out.writeByte(this.transIndex),this.out.writeByte(0)},s.prototype.writeImageDesc=function(){this.out.writeByte(44),this.writeShort(0),this.writeShort(0),this.writeShort(this.width),this.writeShort(this.height),this.firstFrame||this.globalPalette?this.out.writeByte(0):this.out.writeByte(128|this.palSize)},s.prototype.writeLSD=function(){this.writeShort(this.width),this.writeShort(this.height),this.out.writeByte(240|this.palSize),this.out.writeByte(0),this.out.writeByte(0)},s.prototype.writeNetscapeExt=function(){this.out.writeByte(33),this.out.writeByte(255),this.out.writeByte(11),this.out.writeUTFBytes("NETSCAPE2.0"),this.out.writeByte(3),this.out.writeByte(1),this.writeShort(this.repeat),this.out.writeByte(0)},s.prototype.writePalette=function(){this.out.writeBytes(this.colorTab);for(var t=768-this.colorTab.length,e=0;e>8&255)},s.prototype.writePixels=function(){new n(this.width,this.height,this.indexedPixels,this.colorDepth).encode(this.out)},s.prototype.stream=function(){return this.out},t.exports=s},970:t=>{var e=5003,r=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];t.exports=function(t,o,n,i){var a,s,l,u,h,c,f,d,p,g=Math.max(2,i),m=new Uint8Array(256),v=new Int32Array(e),y=new Int32Array(e),w=0,b=0,C=!1;function B(t,e){m[s++]=t,s>=254&&A(e)}function P(t){x(e),b=h+2,C=!0,k(h,t)}function x(t){for(var e=0;e0&&(t.writeByte(s),t.writeBytes(m,0,s),s=0)}function E(t){return(1<0?a|=t<=8;)B(255&a,e),a>>=8,w-=8;if((b>l||C)&&(C?(l=E(p=u),C=!1):(++p,l=12==p?4096:E(p))),t==c){for(;w>0;)B(255&a,e),a>>=8,w-=8;A(e)}}this.encode=function(r){r.writeByte(g),f=t*o,d=0,function(t,r){var o,n,i,a,f,d;for(C=!1,l=E(p=u=t),c=1+(h=1<=0){f=5003-i,0===i&&(f=1);do{if((i-=f)<0&&(i+=5003),v[i]===o){a=y[i];continue t}}while(v[i]>=0)}k(a,r),a=n,b<4096?(y[i]=b++,v[i]=o):P(r)}else a=y[i];k(a,r),k(c,r)}(g+1,r),r.writeByte(0)}}},110:t=>{var e=256,r=1024,o=1<<18;t.exports=function(t,n){var i,a,s,l,u;function h(t,e,o,n,a){i[e][0]-=t*(i[e][0]-o)/r,i[e][1]-=t*(i[e][1]-n)/r,i[e][2]-=t*(i[e][2]-a)/r}function c(t,r,n,a,s){for(var l,h,c=Math.abs(r-t),f=Math.min(r+t,e),d=r+1,p=r-1,g=1;dc;)h=u[g++],dc&&((l=i[p--])[0]-=h*(l[0]-n)/o,l[1]-=h*(l[1]-a)/o,l[2]-=h*(l[2]-s)/o)}function f(t,r,o){var n,a,u,h,c,f=~(1<<31),d=f,p=-1,g=p;for(n=0;n>12))>10,l[n]-=c,s[n]+=c<<10;return l[p]+=64,s[p]-=65536,g}this.buildColormap=function(){!function(){var t,r;for(i=[],a=new Int32Array(256),s=new Int32Array(e),l=new Int32Array(e),u=new Int32Array(32),t=0;t>6;for(w<=1&&(w=0),e=0;e=d&&(b-=d),0===m&&(m=1),++e%m==0)for(v-=v/p,(w=(y-=y/30)>>6)<=1&&(w=0),l=0;l>=4,i[t][1]>>=4,i[t][2]>>=4,i[t][3]=t}(),function(){var t,r,o,n,s,l,u=0,h=0;for(t=0;t>1,r=u+1;r>1,r=u+1;r<256;r++)a[r]=255}()},this.getColormap=function(){for(var t=[],r=[],o=0;o=0;)c=u?c=e:(c++,l<0&&(l=-l),(n=s[0]-t)<0&&(n=-n),(l+=n)=0&&((l=r-(s=i[f])[1])>=u?f=-1:(f--,l<0&&(l=-l),(n=s[0]-t)<0&&(n=-n),(l+=n){"use strict";r.r(e),r.d(e,{decompressFrame:()=>s,decompressFrames:()=>l,parseGIF:()=>a});var o=r(323),n=r(662),i=r(58);const a=t=>{const e=new Uint8Array(t);return(0,n.parse)((0,i.buildStream)(e),o.Z)},s=(t,e,r)=>{if(!t.image)return void console.warn("gif frame does not have associated image.");const{image:o}=t,n=o.descriptor.width*o.descriptor.height;var i=((t,e,r)=>{const o=4096,n=r;var i,a,s,l,u,h,c,f,d,p;const g=new Array(r),m=new Array(o),v=new Array(o),y=new Array(4097);for(u=1+(a=1<<(p=t)),i=a+2,c=-1,s=(1<<(l=p+1))-1,f=0;f>=l,b-=l,f>i||f==u)break;if(f==a){s=(1<<(l=p+1))-1,i=a+2,c=-1;continue}if(-1==c){y[B++]=v[f],c=f,C=f;continue}for(h=f,f==i&&(y[B++]=C,f=c);f>a;)y[B++]=v[f],f=m[f];C=255&v[f],y[B++]=C,i{const r=new Array(t.length),o=t.length/e,n=function(o,n){const i=t.slice(n*e,(n+1)*e);r.splice.apply(r,[o*e,e].concat(i))},i=[0,4,2,1],a=[8,8,4,2];for(var s=0,l=0;l<4;l++)for(var u=i[l];u{const e=t.pixels.length,r=new Uint8ClampedArray(4*e);for(var o=0;ot.frames.filter((t=>t.image)).map((r=>s(r,t.gct,e)))}},e={};function r(o){var n=e[o];if(void 0!==n)return n.exports;var i=e[o]={exports:{}};return t[o].call(i.exports,i,i.exports,r),i.exports}return r.d=(t,e)=>{for(var o in e)r.o(e,o)&&!r.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:e[o]})},r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r(642)})()}));
\ No newline at end of file
diff --git a/src/lib/zmq/zmqClient.d.ts b/src/lib/zmq/zmqClient.d.ts
new file mode 100644
index 0000000..b223670
--- /dev/null
+++ b/src/lib/zmq/zmqClient.d.ts
@@ -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;
+ 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;
+ publishStr(topic: string, dataStr: string): Promise;
+ close(host: string, callback?: (...args: any[]) => void): void;
+}
+//# sourceMappingURL=zmqClient.d.ts.map
\ No newline at end of file
diff --git a/src/lib/zmq/zmqClient.d.ts.map b/src/lib/zmq/zmqClient.d.ts.map
new file mode 100644
index 0000000..fc1eb30
--- /dev/null
+++ b/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"
+}
diff --git a/src/lib/zmq/zmqClient.js b/src/lib/zmq/zmqClient.js
new file mode 100644
index 0000000..88ffdb2
--- /dev/null
+++ b/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
diff --git a/src/lib/zmq/zmqClient.js.map b/src/lib/zmq/zmqClient.js.map
new file mode 100644
index 0000000..b3697a9
--- /dev/null
+++ b/src/lib/zmq/zmqClient.js.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "file": "zmqClient.js",
+ "sourceRoot": "",
+ "sources": [
+ "../src/zmqClient.ts"
+ ],
+ "names": [],
+ "mappings": "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"
+}
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..a0be3ab
--- /dev/null
+++ b/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')
diff --git a/src/router/index.ts b/src/router/index.ts
new file mode 100644
index 0000000..982c53d
--- /dev/null
+++ b/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
diff --git a/src/stores/transferData.ts b/src/stores/transferData.ts
new file mode 100644
index 0000000..a77eadf
--- /dev/null
+++ b/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
+}
+
+export const useTransferDataStore = defineStore('transfer', () => {
+ const subDevices = getSubTopic('client', 'status', 'transfer')
+ const worker = ZMQWorker.getInstance()
+ const isConnected = ref(false)
+
+ const connectSite = ref(null)
+
+ const checkDeviceStatusInterval = ref()
+
+ const siteMap = reactive(new Map())
+
+ const devicesMap = reactive(new Map())
+
+ // =========== 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()),
+ })
+ }
+
+ // 获取站点信息并更新
+ 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
+ }
+})
diff --git a/src/types/device.ts b/src/types/device.ts
new file mode 100644
index 0000000..e7f5dd3
--- /dev/null
+++ b/src/types/device.ts
@@ -0,0 +1,5 @@
+import type { NodeData } from "@antv/g6";
+export type Device = any
+export interface MyNodeData extends Omit {
+ data?: Device;
+}
\ No newline at end of file
diff --git a/src/uno-preset/src/index.ts b/src/uno-preset/src/index.ts
new file mode 100644
index 0000000..1878b2c
--- /dev/null
+++ b/src/uno-preset/src/index.ts
@@ -0,0 +1,53 @@
+// @unocss-include
+import type { Preset, PresetUnoTheme } from 'unocss'
+
+export function presetSoybeanAdmin(): Preset {
+ const preset: Preset = {
+ 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
diff --git a/src/utils/is.ts b/src/utils/is.ts
new file mode 100644
index 0000000..eec86a9
--- /dev/null
+++ b/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 = (val?: T): val is T => {
+ return typeof val !== 'undefined'
+}
+
+export const isUnDef = (val?: T): val is T => {
+ return !isDef(val)
+}
+
+export const isObject = (val: any): val is Record => {
+ return val !== null && is(val, 'Object')
+}
+
+export const isEmpty = (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 = (val: unknown): val is Promise => {
+ 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 => {
+ 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 => {
+ 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
+}
diff --git a/src/utils/zmq.ts b/src/utils/zmq.ts
new file mode 100644
index 0000000..c97b1b3
--- /dev/null
+++ b/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 {
+ 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 (massage: Omit, 'id'>): PublishMsg {
+ 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(action: T, params: any, reply: 'yes' | 'no' = 'yes') {
+ return generatePubMsg({
+ 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('')
+}
diff --git a/src/utils/zmqJsonWorker.ts b/src/utils/zmqJsonWorker.ts
new file mode 100644
index 0000000..022b3b0
--- /dev/null
+++ b/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 | 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()
+
+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
+ traceMessages.set(parseMsg.id, {
+ timestamp: Date.now(),
+ topic: topic,
+ isAlwaysListen,
+ })
+ }
+ pubClient?.publishStr(topic, msg)
+ break
+ case WorkerCMD.STOP:
+ disconnect()
+ break
+ }
+}
diff --git a/src/views/engineering/index.vue b/src/views/engineering/index.vue
new file mode 100644
index 0000000..1f63808
--- /dev/null
+++ b/src/views/engineering/index.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/views/layout/index.vue b/src/views/layout/index.vue
new file mode 100644
index 0000000..6f2ff2b
--- /dev/null
+++ b/src/views/layout/index.vue
@@ -0,0 +1,189 @@
+
+
+
+
+
+
+
+
+
+
+ EPM-工程管理平台
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentTime }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tsconfig.app.json b/tsconfig.app.json
new file mode 100644
index 0000000..ce14bd2
--- /dev/null
+++ b/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/*"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..ca6debb
--- /dev/null
+++ b/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"
+ ]
+ },
+}
\ No newline at end of file
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..19770ff
--- /dev/null
+++ b/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"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo
new file mode 100644
index 0000000..2552b15
--- /dev/null
+++ b/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"}
\ No newline at end of file
diff --git a/uno.config.js b/uno.config.js
new file mode 100644
index 0000000..6c43dbe
--- /dev/null
+++ b/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)()],
+});
diff --git a/uno.config.ts b/uno.config.ts
new file mode 100644
index 0000000..76636c6
--- /dev/null
+++ b/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()],
+})
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..d2e9a40
--- /dev/null
+++ b/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/, ''),
+ },
+ }
+ },
+ }
+})
\ No newline at end of file