From 3bfa482e032b7130f70c32cde24173c69d365cc2 Mon Sep 17 00:00:00 2001 From: Swanky <413564165@qq.com> Date: Sat, 13 Jun 2026 22:58:46 +0800 Subject: [PATCH] Update package.json and package-lock.json to include flv.js and hls.js dependencies; enhance Vite config with additional API proxy; refactor App.vue and ChinaMap.vue for improved component functionality and layout; optimize MarketEnvironmentMonitor and MarketRealtimeMonitor components for better user experience; update SupplyDemandData and PurchaserAnalysis components for enhanced data presentation. --- package-lock.json | 54 + package.json | 2 + public/datas/market-cameras.json | 54 + public/images/图例bg.png | Bin 0 -> 2848 bytes public/images/大气压强.png | Bin 0 -> 2001 bytes public/images/已交易.png | Bin 0 -> 3889 bytes public/images/待交易.png | Bin 0 -> 3428 bytes public/images/总数量.png | Bin 0 -> 878 bytes public/images/成交订单.png | Bin 0 -> 1793 bytes public/images/按钮.png | Bin 0 -> 7783 bytes public/images/按钮选中.png | Bin 0 -> 7617 bytes public/images/温度.png | Bin 0 -> 1661 bytes public/images/湿度.png | Bin 0 -> 2113 bytes public/images/空气质量.png | Bin 0 -> 2205 bytes public/images/紫外线.png | Bin 0 -> 1270 bytes public/images/降雨量.png | Bin 0 -> 1510 bytes public/images/风力.png | Bin 0 -> 2686 bytes public/images/风向.png | Bin 0 -> 1849 bytes src/App.vue | 26 +- src/components/ChinaMap.vue | 969 ++++++++-- src/components/MarketEnvironmentMonitor.vue | 601 ++++--- src/components/MarketRealtimeMonitor.vue | 849 ++++----- src/components/MonitorLivePlayer.vue | 76 + src/components/PurchaserAnalysis.vue | 494 ++++-- src/components/SupplyDemandData.vue | 1776 +++++++++++-------- src/components/YakSalesTypeStats.vue | 351 ++-- src/utils/liveStreamPlayer.js | 123 ++ src/utils/pie3d.js | 246 ++- src/utils/systemConfig.js | 22 +- src/views/Dashboard.vue | 26 +- vite.config.js | 4 + 31 files changed, 3630 insertions(+), 2043 deletions(-) create mode 100644 public/datas/market-cameras.json create mode 100644 public/images/图例bg.png create mode 100644 public/images/大气压强.png create mode 100644 public/images/已交易.png create mode 100644 public/images/待交易.png create mode 100644 public/images/总数量.png create mode 100644 public/images/成交订单.png create mode 100644 public/images/按钮.png create mode 100644 public/images/按钮选中.png create mode 100644 public/images/温度.png create mode 100644 public/images/湿度.png create mode 100644 public/images/空气质量.png create mode 100644 public/images/紫外线.png create mode 100644 public/images/降雨量.png create mode 100644 public/images/风力.png create mode 100644 public/images/风向.png create mode 100644 src/components/MonitorLivePlayer.vue create mode 100644 src/utils/liveStreamPlayer.js diff --git a/package-lock.json b/package-lock.json index 53a4f62..de327f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "axios": "^1.5.0", "echarts": "^5.4.3", "echarts-gl": "^2.1.0", + "flv.js": "^1.6.2", + "hls.js": "^1.6.16", "lunar-javascript": "^1.7.3", "vue": "^3.3.4", "vue-router": "^4.5.1" @@ -1098,6 +1100,12 @@ "node": ">= 0.4" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", @@ -1153,6 +1161,16 @@ "node": ">=8" } }, + "node_modules/flv.js": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/flv.js/-/flv.js-1.6.2.tgz", + "integrity": "sha512-xre4gUbX1MPtgQRKj2pxJENp/RnaHaxYvy3YToVVCrSmAWUu85b9mug6pTXF6zakUjNP2lFWZ1rkSX7gxhB/2A==", + "license": "Apache-2.0", + "dependencies": { + "es6-promise": "^4.2.8", + "webworkify-webpack": "^2.1.5" + } + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -1291,6 +1309,12 @@ "node": ">= 0.4" } }, + "node_modules/hls.js": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz", + "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==", + "license": "Apache-2.0" + }, "node_modules/immutable": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", @@ -1622,6 +1646,12 @@ "vue": "^3.2.0" } }, + "node_modules/webworkify-webpack": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/webworkify-webpack/-/webworkify-webpack-2.1.5.tgz", + "integrity": "sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==", + "license": "MIT" + }, "node_modules/zrender": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", @@ -2215,6 +2245,11 @@ "hasown": "^2.0.2" } }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "esbuild": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", @@ -2260,6 +2295,15 @@ "to-regex-range": "^5.0.1" } }, + "flv.js": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/flv.js/-/flv.js-1.6.2.tgz", + "integrity": "sha512-xre4gUbX1MPtgQRKj2pxJENp/RnaHaxYvy3YToVVCrSmAWUu85b9mug6pTXF6zakUjNP2lFWZ1rkSX7gxhB/2A==", + "requires": { + "es6-promise": "^4.2.8", + "webworkify-webpack": "^2.1.5" + } + }, "follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -2341,6 +2385,11 @@ "function-bind": "^1.1.2" } }, + "hls.js": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz", + "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==" + }, "immutable": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", @@ -2531,6 +2580,11 @@ "@vue/devtools-api": "^6.6.4" } }, + "webworkify-webpack": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/webworkify-webpack/-/webworkify-webpack-2.1.5.tgz", + "integrity": "sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==" + }, "zrender": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", diff --git a/package.json b/package.json index 260ce0a..82e8c2c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "axios": "^1.5.0", "echarts": "^5.4.3", "echarts-gl": "^2.1.0", + "flv.js": "^1.6.2", + "hls.js": "^1.6.16", "lunar-javascript": "^1.7.3", "vue": "^3.3.4", "vue-router": "^4.5.1" diff --git a/public/datas/market-cameras.json b/public/datas/market-cameras.json new file mode 100644 index 0000000..560c193 --- /dev/null +++ b/public/datas/market-cameras.json @@ -0,0 +1,54 @@ +{ + "pageSize": 2, + "autoPlayInterval": 10000, + "cameras": [ + { + "id": 1, + "name": "交易大厅主区", + "resolution": "1920x1080", + "preview": "/images/monitor/交易大厅主区.jpg", + "streamUrl": "", + "status": "online" + }, + { + "id": 2, + "name": "牦牛展示区", + "resolution": "1920x1080", + "preview": "/images/monitor/牦牛展示区.jpg", + "streamUrl": "", + "status": "online" + }, + { + "id": 3, + "name": "停车场入口", + "resolution": "1280x720", + "preview": "/images/monitor/停车场入口.jpg", + "streamUrl": "", + "status": "online" + }, + { + "id": 4, + "name": "安全出口", + "resolution": "1280x720", + "preview": "/images/monitor/安全出口.jpg", + "streamUrl": "", + "status": "offline" + }, + { + "id": 5, + "name": "办公区域", + "resolution": "1920x1080", + "preview": "/images/monitor/办公区域.jpg", + "streamUrl": "", + "status": "error" + }, + { + "id": 6, + "name": "仓储区域", + "resolution": "1280x720", + "preview": "/images/monitor/仓储区域.jpg", + "streamUrl": "", + "status": "online" + } + ] +} diff --git a/public/images/图例bg.png b/public/images/图例bg.png new file mode 100644 index 0000000000000000000000000000000000000000..557d7c4257236e95a06eef031c4c6a74f446ad8d GIT binary patch literal 2848 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 literal 0 HcmV?d00001 diff --git a/public/images/已交易.png b/public/images/已交易.png new file mode 100644 index 0000000000000000000000000000000000000000..636c7ceadf654eeb6ecad3252cddd090dd76a1e2 GIT binary patch literal 3889 zcmV-156LaDiHs4@J2MUbrfVN&%(#pr6lLYC+mbrqd*NWRfY9 zq_iKDhv2h_MG+Lx>LpCo)iJuKv6UxM|3TG3)2RsPa9?r%e3TG0#QGODz zWo}Q`tf}ahUmKHZka?NJskp(5BiZ=FB!LHr`3Ar`X8s~N^S^*Y0gH(EU}Bm>MDqYk zxGiuc@_mw8ZjgD|KLUT#Y4*mobHX|6jm;`h%?M(#JOqp79U_#2{4{`1;s=E# zK1B9Wg=lI~F`7ygR;O+)bf$Jb70tcuBQRl72yd_8pz0$j^@XyDpN4aZP2o)PGkO%y z$y&4PX`o?)gmQ^9uvl(`^7jDz(jSTVoWghcCy%3lKOs2fZv1B zbV4zfTJ`Yxm%r-f8`B^x{9Q240Jzg5JUH~e{MvPYcN1TWWoIh0dLQ~W2ymyRk!&LF zCb-MWjg-sbOkyR3h zHt{CRu?-cDkpdGh>0i}-{|MS}hNrSDoJ+2on@b+kknj&aTK6v5Pj4~Wjgijgb|4ri zmHd_isxnA`N&6b!P6%C}k9PmSufcj~Fvey)m8(f)jS|@w%ErH3k6b8|><;gG%Y)%; z;t%t(i4*IQ@#7hyy`KWzoaS#2XOmTLB#%LyMupCXusc1gf-isGO=zS%uQ&b<7(;ic zB8Jhn0n>6tgn)$Gzk#t8Hn1dXOm zAz|+7U$tScNv|7c_TPCjZ3?eXeAv|UUzRmv&#U|Y0-=BpnRQChSo%K-E+gn>xwYb(cWWYzd0UUwY^-zTY*mSV*>3e{gjb6o${SlKOT-Z_=H$)XurX8%1Vbvh-GPC{)1)W zNZvpFpMlLInDj)~p=H`LaSZb>47!V%H%ay2RAL z!snK0jzL20B|U8SFB5eROJ4K1W$AFAMkfq9Fhsr!o1Z#>&@hdO+*5Kg#YxH$3a z?v8bRc-!4>XnOB#MA|gXF$gnNEpE2hI%2MQS_Xw!_aCrW)(SUz+Y&k-Bjc-H;>0mg z%5)*x5NSJ2YgQu*%BdPDg9zPUjP=}G##@;eqUl?dke;5UT6(d5Ei1%Qm)GKHwytCc zegav~9tJbwM9@ghYcIVSg@XW<#EM%!Ix5Oh=#Qngve?Iv&{IIoR7AMp1ni`*F)Lby zKNGWdW2FAptxt%KKO76Y5~lw{1q_7iNMc(sv`3I8GrUee8`eQy4cYkRVK?LwmyuE@ zk)mgP)E5--(GkQxJu;4b;z~^qWs{%uk!y&AW#UdlV8M&+r3K0s(#MJJx&c^cr<>4d zIikajL|?}USm6Nim&gr5D={r|s5^qHe@CP4QamUuq=p>Pa!qHF>q6N?s#>&u3fkCV za$}Sns&Mmjy%q9Aa8K|_FRgxTHdbp4`DpzT&2ggN%k>a%PMR<{Mf*i?&PO#M2z{g? z68*{>r4kn=^17X7Pt)-SPeU%{tt{4Dl*emG1|^RX>hX(&)htX`6)}Vh2Hp_rMAAgO z-e5x->Sq30pDPwxx6}yZ+ynpqA{@aNM^GnYw%?$z+Rk*a%7ve<6q_s*_Ova>JYS7D zxtWP>M#JAX0rcxoztwC4wBPDFlV9Vg`yrm{78W|M5KUcV$~J=Ev+SBdVfu$h#%660 zf>B&#mVgUuu4q}Md^CM2jxU$yqp33y!(3tl=;tyAmzoU(h}Y;klV9V|_fhyJAI}dT zIhdji<1nnp6KCc&ZH)aqnuLcu_#`4igU0C8jmhiubt(WO(bE=2psfw!iFIJ5ojw(v z(QO1&Tbs_^AW8+?_+*x00KKVoH6a0Q5O{4a8k0UOb`d787-V|Z)NYGBZ_*|MKNwLt z=xqnM({tJ&d)ySvEAy@6u5PHTKbz2DzsUNox0s-#-UFpl)!7M#@mcdTi8q`48V~m- z4jt>%zM+nRdGFo>^@n|76e~%H9)m&H^Df3)XEs?*e5AtIXkp%^*1>Sy@3fh=v!d-{ z2X>ecD3!L8_GWSMBm|;|AP4>`Ia0I(r}o>m?s~uX@aT3StcLW1_pV_~&%hw^R~H|O zJb%vqbP!G8XWJzmr#ucS^pMma)XYHHg#Q3-tfUuuz2KQLdVSOL{?PXX~eCWheHBxQbT2}QwhC; zh(7_VpXCQc@?q5LZ`8>B!=eheUrxseIMbA4;9bZht(9d7zy&iDvXJ&Y3~Fo8`MnlAQ~rXX?Pdt02hh`SCCp!4wyEsq1~F^Fas zC2^g507soTW2yfL(j#0aGKrspzdCbBfA_M$+Y|2ddjM4iDR!iHX%7*?-pHBIxAYbq z^na@q$tIRD$tttctXOYAP4cK8fGzdOfJ`VCU#nOl^3D$#+y`Jwey8(Mo>B2yMR7kT z!>_s8r+-#{^~JyPp${7*lu3L5mD)LrvT#~RyRl*nF>nyg3NYRF#@<-1t-HgxuP@=; zMgNaJ=zBuuNq;LuQ`K9B5f*34FpdaEaCi1PH&Q7J9WhN!+fnPIxeD(-Dn!;_bP`^f zKl5q+`L>{ak`KK?atwlogyOF!U2L1;V@6!>Yqx#`&{Bp7RE_MP^ z=%F{gB0{IsYYZ)_4WjcZcxHw5&;082-Gyjce*w4#M~Lqlum|3x$X=xV6t;tJ%}2ZM zsuG+6!rWZ)m~b}nO`EBYNPj~@em_L|<@jPOrN4XitM{diX%Ii|XFD@5^+$W|DyaT*M9y%O1mc0LUV0;&?G{AO$gjxOr0NL6iZLPG|rt*X9Sec0s0 zG{<4r{~<#A5?C89fV24PftjV92uIuov8oUDK`WtJ(th!I$_2)`pEd9O0^U zRy2sUV#AD~!A1F4+V{^s$`%_hpE84xsEz2%(ExqeG#kVENG5tG7VX7Ee4a3WK}b#^ zB7MMxtvVT>dZETY-2k`YsA4R&>d`g5Rq+*nk+G?2?5bAR@DVN4_#_MWDB@!a(bPHG zZD^Hl)hb2f>A{G&cs!mO^?K*M6C-LJ46(F*`U{8TW8LxoXt(y4YKS#nHDv}VMpM^f zo}WT2UjgWVlRgX5pMUDF0rfeHb_D-QBEJ%*8#Rvzr-b-ZV$;IV>z`hjkEYhb5St%1 z{*tos+a6ouUjP6A|Nmd1-s%7V00v1!K~w_(fZ~CQ-l!U{00000NkvXXu0mjf;K+0g literal 0 HcmV?d00001 diff --git a/public/images/待交易.png b/public/images/待交易.png new file mode 100644 index 0000000000000000000000000000000000000000..12de3e1036c9509fca71a7833ad69a22777e01f1 GIT binary patch literal 3428 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 literal 0 HcmV?d00001 diff --git a/public/images/温度.png b/public/images/温度.png new file mode 100644 index 0000000000000000000000000000000000000000..dd1ff03579b5b84b818e776dfb69cc1ebcee1afd GIT binary patch literal 1661 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 literal 0 HcmV?d00001 diff --git a/public/images/空气质量.png b/public/images/空气质量.png new file mode 100644 index 0000000000000000000000000000000000000000..59e970189998bb652bfb6ae4c1585c530e60e29b GIT binary patch literal 2205 zcmV;O2x9k%P)db}GVe`v^a)THM&CpKk2XZ#K2-Jxv|@N8`qJ{R4GR zp#LM}jZN*k7JW7ReRvV&+0BRay#tx#ptnSGht6Qo`-gYR3oas$Igu6OWC?@MG2^&%&0I1`N2RRmTFfPDTDqub+o2 z;K7rVF-+`y_zF?ECmNAmwmJmlj^=i~qD6H`X6FL}OcdrOqVjh-sDU%-AQRZ(oZL#( zz5{Su|QYw@`rY4)EWH{kPcxC_ptVdjtT&9r|c(J4C#(2X8=!GLKr zk0`9C=k%VV(;bw*d{iV}Rpu^;$J}j|G50-qWo4OrxGLthRF%8GP``<~2l4qi((Fr- zb6$x_WzxxHz+VHnyXmVQ?n`khevs^xhmcYbU=GJ;a9#&{Itz4p1@LuDZW(=jA}KNn@QdkZJrnsKG9bpg(v-Y_n-1!kG+blOA-6`H%ph9# z*??i)n5!tqI z9Cn`cza=cc;%sRO0jXSHQReKRP{>ZrT{^nCQ@_GCx+J?t;kCQk_5FCyfQ|OqgMgVh zk?U29$gj&XEUV+??$L~LR#lU7yQVP#=_F;%1yTPL334*Oy*+0rRz zi<6H@>bI*{<-aL4^=wcrY3$URrY`k9DMOZL!^=9HYVVf-&nC)}74Y7@`fiMV?+9ro z?^eKHh{!~fA#WWw%B3#_uH9>*c8#mlG5E%OS8%jg>`EUM_SMl{F2O<13i#DhISbiMqT&ioYbe3)H5q!B77ue7GVj8N{*lHHEy-2%RMX84dnO9QJKO!!JW=I-f1$e zx%Hli+<>(dHrmI^^vY0K0ds%9h_@f48ek{*arMq-uRX0Qw~B~A@vpHh9%F?*5-)QL zD^GJ4Va$b*$A(*ggX-K{dlbmN9roj^|A05F^R zB&*Bd1GHPtD`T%WtPFdy7gyQs(nb>7s~1)m2EBUdOo&(sYGQd z4tC%W(#E^+y{7bK8=!ApP$u{N7$uc0RL(@NhCTgXoz!rkE`x7;S4^gJtfq;|auL}A z?|}niGN-erD}q9fN3aSm2u9#Rn;%u@_=?F2*p@$0B)8GpUyY~%8IW6&#c}~(1-}kz zc-Yd#N4jm5SO5ijx_-_HUV*D`oQo1sWVlO>R=pinNCCJ+Tu6FkMT)H^^6&{gg{rRT> zH@E53rViZ*DBB9>(21uWuMn{se6I54la-cn$Oga2=w~w%v@MlU)mse!17aQtdMl>O zVNYFj_9ea`EOk8|R$9ub!#|xX!}-9Wdc)3 z{%fj6y60U)uEy918`-{I1L!-Se??EXih(lQZVrKXf_A~jE%FElb6z1AjR$NIwEq^? zaV>wGWtqCkhCjd@=xRPUA+n;3_sf7Zdf?sxo&a zk+%QN4rTb+($)~E^=zdllL^_G5ZS>5|K)hwLOviWQ}G`Od)guqxrj`%)>jlF(|`+c z_CEZzw2}id5fIA(pX~+5_PKqv_$FSmK(d`ld^H}EPxbfJ;T!aog92=w*a5MNLU@G) zwEge90%?1EKX!Y=dJSp++WZr_iKVYaev)lBDIRn8I!6r^1EoO&_5%gLW9~tK+(P81 zA;-d_;i2#-crrYTrC*G+!|j_6N}e>}L5+G6jZOhRiQpB~^c@%BHvj+t|Nn1FXb1oR f00v1!K~w_(c30td7{Q7u00000NkvXXu0mjf#!E#{ literal 0 HcmV?d00001 diff --git a/public/images/紫外线.png b/public/images/紫外线.png new file mode 100644 index 0000000000000000000000000000000000000000..ceb549260fa5d10bdd1e7c4bf1f453c4e6294a18 GIT binary patch literal 1270 zcmV<4@nOHa~v1dhxwhn56sFs(C~9 zH8umn@k_g|d0rx|K4OGg%2s_!-=CKVNEV$ulGjDpNI~=!p!855B@HQ;F{9&X7@;ju z+Nw|K`x}Jb6?GF>_~(c5n#nfUZFpaN1`*Z*(rwo@g|uy1aQ-9-NNs5=4ylhlM(sge zZLlp-2j9MALw$qKLwZ^GH%eF=0NSo=UXa(%pFhasu*LbF{^E?f&yn_>y2y)l8qZgX zL|hE|jA+tBznZLelZ5Srv@^b?JQ5*?->BM^1ZS^Iv=#E`WJ6Egopsyje?;O)71HP) z)uXJ6Y;wrIS;F+E=MUi-ZAw(m{r$ zzB76dDFkxgRH-hcH`-<$>Yv2U@3rM3VSr;A92~}4@Te#0(_F|On$jcc)YNAQ69H#+ zNd=-5^RWiMrz2@F;>PlXkAmM*o_Luhyk0lIWIG(a;_#vgl5Jxt2BTKCZhU--{Y&*( z!jLq=8KXGK4cC^W#;@5HANPsy`A`v=oO{{VEoH>=pmn~kPhzhjmiB+QN`?0v)d+HP zDntgSkMEJTU*B?wLR;j~tjVV>!}6TPca5Xa7QtiJE%gB=e%>;VT-p|$jOFkE2;<&Y zMt~dY5i{2ZBMIRipEYDQQP>pnsVOREz#I4tOe1C@3J=dNJwE5_{+fI`L&aCNN z4C&;McJo^MbBW|)pLSkYy$7cGz(={X`N3NQ6P4%cqY;)YMrD|pc;);mX{7DASR$s$ z+_H2)Cll>OlJ+YqIoYKE%gr|Jmwf%ob_+J130RR7Km+tof g000I_L_t&o00jSHcOEh&ZU6uP07*qoM6N<$f^Ii*YXATM literal 0 HcmV?d00001 diff --git a/public/images/降雨量.png b/public/images/降雨量.png new file mode 100644 index 0000000000000000000000000000000000000000..e33aaddfa83d56430e6dcef7952cffafaace6803 GIT binary patch literal 1510 zcmVNklZXebu>%!UA_PUI z7g1(pgb3b5g=uLPVOUt1^@-=t-%oaYci!FEch?VJ5R~WVGxN;Mf1hu5c6RnXrgen< zJDTv}zF|lH3=a$pEQWQ^pI|NV=izi`QxEf?YD`Ki#5Xhk8+adP!vr`^{Z7hecnqB` zfD;p3Nc>DV7Bs!}sxc|85I+N~FQg{dEC$McLZQE7SgLUJnF_m$2UV(+M3U+|Y%-Oz(a%6>sqt+f(_=ErC zQYO0^|2y=-Ng24dl_tz$9}btnEVv)$43c{oKLv)hFI^~^w9n60T3qj?xfOzlx=Xm= z?`7cA@DjW{NL=?fqH!{$%(js`*m7CyNa7*zPxwby1zHAUIVyrp`9#j$>3gclKc&%Bq-v(E} z2yk|P!*7Jh$`YPL4>xjLYX$$!gW2#v4(kt6pHq`3>9-iRfo4)SQ+NShh3CLKI~uR8 z=Rf~IA3}<4D@$0h(8IrQ1yAu$up!5K%Eo@g*I^_5w!ofDcqrcGb|ro+oC99heRw(L z_uy;r`na~0CH!x@xpf}p>D)GN-&nki#*YLa44MzgT$?{IawTklHQ=)Q0RI*&;*WDe z>_@e2WeI;z^B)@r)E>e=1@qxkl6C!3ELvH@ zAJgp%@O*GK-owZYybp)mMcJ57HpS*fLOa13{0hGemcuGo11^iCXj#G^(%Yx{GyKos zS;37w(0TZwOYf|ZFn^&*y5L_Y4G%Pr;lm+C?@PeLPk7>K?wu79_R=K16}(*w@ozx> z@ZurI!%n|_drfJ=y-uRZ^^ys9im;81f$QN;@L*Ip$Pga_lVE1XM7*!`NRHSrP(v*> z&y6}%RB9)ThMzC4;U_4kwl%-QuA$xSN-P^A6q#G1UlwXTOpDsM?$#t>4<)|qCn75+ z;5*IPJfEOn4B}hN7@^1LU`E+5aA8FJ8!`+7Fu(ki

PYamhBt$qvF^Q*Q-sqyERGJZ!OQMdJp!yy2~ef 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 literal 0 HcmV?d00001 diff --git a/public/images/风向.png b/public/images/风向.png new file mode 100644 index 0000000000000000000000000000000000000000..1c5b8fcfd47593f4e940aadf0799f51fd4b6734d GIT binary patch literal 1849 zcmV-92gdk`P)URHkF8il$Kvrt3_1cwm>bk_kH}FdT;4XdkbxZCzmn zkyyMl7cOE0vpy*1Lzw2JZ-}v)_QN5)`(QZNbz5@bOOW{qk$hN@o~WwQ z#n>Gt=nU)gj$TMU8cf&p_UghS7`P0EU!eRgI0`r=Izrh6`oMW$IrG+Atj(&_M#Z|n zx=O2}E?pmY>xM*^RvxR-DkZi0xU@E((BIMB29YN$=oaQL0<&3vp>K?OiJ(`~E;dfE zXv_Rs`|pCY?N|}nw6!;+Z7)jLlZUK>g&NBL~k`%MSUTd29lsSm#Qv!r$*;^WhlxK7RKFsLv>Hq1HKb9rF@9>TDI1nKH8%V ziM(!t=;zxGhimdvM;QKv#h6~-d@XQ?BDy-df57OYu%gHg9Cs{{T&Gh2&L= zbaf)3+mo^GZA{rS=)S_Z+y6uql%ifDQAulp8kd>%XFqP8%_-Z{l(y%yDcj6O{?VDz zgOJ`Xew2|C+D~-a8fxqwdqVC4ue9qSZYr%@2*7S#5EDvm@G<)D1t z--)1|baaD=<1(AFM8-#0s&*^5xd$&D^ZMz6aI~Yl0}y>>xP$Ax?l7}p3AIrla00q> zsh7yO2t$;2NxopjRWc?UmeT87nXk)#rurz>g&Oip_G>@Qejvp$>M?wZ7llr7K%=VGbLr z-rqG_KGM^HTPl-zfBz zNIAl8K!n=w#0OYlNncWHAbZ#JTK%Sp{GcIYTN^X>pT=7K<#?}tSrO|V68Il*Vz1?; zxt#I}O8@J<4P$@9##UDLe=*M`{2qRp4L(ZW?-*Z$?iD0KcR+Mg(+u-<85%C-2=}nl zhqwWj<&#>QP1~09tL?!2T01z$%#XpKCF9@8^JTw3o9paVbapjn>;=ll=Vj~>PdnqF zTR9Tf6P0g5?AvBqhJkB{&@JK{kq?9o=)UMl5dEXdh=kek5zg;ZO)2|-bG5xOs)bVw zdfGWZV=rgxY@0{LiQ&a(vpAtKZTmc{^9`TE%;4~dgb80iKnC?DO=U#FoGqfJk;l$3 z^+`!;G@ncn9?d%wF*fDliSXiRHl{p0ns+8*Y|6tE;l

- +
@@ -119,10 +124,25 @@ const systemConfig = ref({ }) const isLayoutReady = ref(false) // 控制页面渲染时机 const isSupplyExpanded = ref(false) // 控制供应信息组件是否展开 +const supplyDetailId = ref('') // 处理供应信息组件展开/收缩 -const handleSupplyExpand = (expanded) => { - isSupplyExpanded.value = expanded +const handleSupplyExpand = (payload) => { + if (typeof payload === 'boolean') { + isSupplyExpanded.value = payload + if (!payload) { + supplyDetailId.value = '' + } + return + } + isSupplyExpanded.value = !!payload.expanded + if (payload.detailId) { + supplyDetailId.value = payload.detailId + } +} + +const clearSupplyDetailId = () => { + supplyDetailId.value = '' } onMounted(async () => { diff --git a/src/components/ChinaMap.vue b/src/components/ChinaMap.vue index fbc18f3..73a0936 100644 --- a/src/components/ChinaMap.vue +++ b/src/components/ChinaMap.vue @@ -1,16 +1,22 @@ @@ -80,6 +105,10 @@ import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue' import * as echarts from 'echarts' import BaseCard from './BaseCard.vue' +import { loadSystemConfig, DEFAULT_MAP_HUB, DEFAULT_MAP_FLOW_LEVELS } from '../utils/systemConfig.js' + +const MAP_TRADING_API = '/api/dashboard/map-trading-network' +const FALLBACK_TRADING_JSON = './yak-trading-data.json' const chartRef = ref(null) const sourceChartRef = ref(null) @@ -90,6 +119,9 @@ let sourceChartInstance = null let salesChartInstance = null let chinaMapData = null let tradingData = null +let mapSystemConfig = null +let cachedOutlineKey = '' +let cachedOutlinePaths = [] // 浮动面板状态 const showSourcePanel = ref(false) @@ -108,6 +140,47 @@ const modes = [ { key: 'local', label: '红原出栏分布图' } ] +const BTN_NORMAL = '/images/按钮.png' +const BTN_ACTIVE = '/images/按钮选中.png' +const LEGEND_BG = '/images/图例bg.png' +const GEO_DATA_INDEX = 2 +const WIREFRAME_GEO_INDEX = 3 +const MAP_ASPECT_SCALE = 0.82 +const MAP_ZOOM_FACTOR = 0.97 +const MAP_LAYOUT_SIZE = '84%' +const MAP_ANCHOR_CENTER = [104.8, 37.2] +const MAP_ANCHOR_ZOOM = 1.05 +const MAP_GLOBAL_OFFSET_LNG = 1.45 +const MAP_GLOBAL_OFFSET_LAT = 0.9 +const MAP_GLOBAL_LAYOUT_X = -2.8 +const MAP_GLOBAL_LAYOUT_Y = 3.6 +const MAP_SHADOW_OFFSET_FAR = 1.25 +const MAP_SHADOW_OFFSET_NEAR = 0.75 +const MAP_SHADOW_OFFSET_LNG = -0.45 +const MAP_SHADOW_OFFSET_FAR_LOCAL = 5 +const MAP_SHADOW_OFFSET_NEAR_LOCAL = 3 +const MAP_SHADOW_OFFSET_LNG_LOCAL = -2 +const MAP_WIREFRAME_OFFSET = -0.65 +const MAP_WIREFRAME_OFFSET_LNG = 0.12 +const MAP_WIREFRAME_OFFSET_LOCAL = -3.5 +const MAP_WIREFRAME_OFFSET_LNG_LOCAL = 0.6 +const MAP_FILL_WEST = [45, 181, 181] +const MAP_FILL_EAST = [12, 123, 176] +const MAP_GRADIENT_MIN_LNG = 73 +const MAP_GRADIENT_MAX_LNG = 135 +const MAP_GRADIENT_MIN_LAT = 18 +const MAP_GRADIENT_MAX_LAT = 54 +const MAP_WIREFRAME_COLOR = '#f8feff' +const MAP_INNER_BORDER = 'rgba(6, 38, 62, 0.62)' +const MAP_INNER_BORDER_WIDTH = 0.8 +const MAP_SHADOW_FAR = '#000000' +const MAP_SHADOW_NEAR = '#000000' +const MAP_LABEL_COLOR = '#ffffff' +const MAP_FILL_OPACITY = 0.88 +const MAP_TEXTURE_SIZE = 128 + +const mapTextureCache = new Map() + // 获取当前模式对应的地图文件路径 const getMapFilePath = (mode) => { switch (mode) { @@ -128,21 +201,74 @@ const getMapName = (mode) => { } } +const getHubName = () => { + return tradingData?.centerCity?.name || mapSystemConfig?.mapHub?.name || DEFAULT_MAP_HUB.name +} + +const buildGeoCoordMap = (systemConfig, fallbackGeoCoordMap = {}) => { + const hub = systemConfig?.mapHub || DEFAULT_MAP_HUB + return { + ...fallbackGeoCoordMap, + ...(systemConfig?.mapGeoCoordMap || {}), + [hub.name]: hub.coordinates + } +} + +const loadTradingNetwork = async (systemConfig, fallbackPayload) => { + const hub = systemConfig?.mapHub || DEFAULT_MAP_HUB + const buildPayload = (tradingModes) => ({ + centerCity: { + name: hub.name, + coordinates: hub.coordinates, + description: hub.description || fallbackPayload?.centerCity?.description || '' + }, + geoCoordMap: buildGeoCoordMap(systemConfig, fallbackPayload?.geoCoordMap), + tradingModes, + flowLevels: systemConfig?.mapFlowLevels || fallbackPayload?.flowLevels || DEFAULT_MAP_FLOW_LEVELS + }) + + try { + const response = await fetch(MAP_TRADING_API) + if (!response.ok) { + throw new Error(`HTTP ${response.status}`) + } + const result = await response.json() + if (result.code !== 1 || !result.data?.tradingModes) { + throw new Error(result.message || '地图迁徙接口返回异常') + } + return buildPayload(result.data.tradingModes) + } catch (error) { + console.warn('加载地图迁徙数据失败,使用本地兜底数据:', error) + if (!fallbackPayload?.tradingModes) { + return null + } + return buildPayload(fallbackPayload.tradingModes) + } +} + // 加载地图数据和交易数据 const loadData = async (mapMode = 'china') => { try { - // 加载对应的地图数据 const mapFilePath = getMapFilePath(mapMode) - const mapResponse = await fetch(mapFilePath) + const [mapResponse, systemConfig, fallbackResponse] = await Promise.all([ + fetch(mapFilePath), + loadSystemConfig(), + fetch(FALLBACK_TRADING_JSON) + ]) chinaMapData = await mapResponse.json() - - // 加载牦牛交易数据 - const tradingResponse = await fetch('./yak-trading-data.json') - tradingData = await tradingResponse.json() + mapSystemConfig = systemConfig + const fallbackPayload = await fallbackResponse.json() + tradingData = await loadTradingNetwork(systemConfig, fallbackPayload) + if (!tradingData) { + return false + } // 注册地图 const mapName = getMapName(mapMode) echarts.registerMap(mapName, chinaMapData) + cachedOutlineKey = '' + cachedOutlinePaths = [] + mapTextureCache.clear() return true } catch (error) { @@ -160,17 +286,15 @@ const getFlowColor = (value) => { } // 获取节点颜色(区分流出和流入) -const getNodeColor = (city, modeData) => { +const getNodeColor = (city) => { + const hubName = getHubName() if (currentMode.value === 'outflow') { - // 流出模式:红原县为红色(流出源),其他为蓝色(流入目标) - return city === '红原县' ? '#FF6B6B' : '#00D4FF' - } else if (currentMode.value === 'inflow') { - // 流入模式:红原县为绿色(流入目标),其他为橙色(流出源) - return city === '红原县' ? '#67C23A' : '#E6A23C' - } else { - // 本地模式:红原县为紫色,其他为青色 - return city === '红原县' ? '#9C27B0' : '#26C6DA' + return city === hubName ? '#FFD048' : '#6ecfff' + } + if (currentMode.value === 'inflow') { + return city === hubName ? '#67C23A' : '#5eb8ff' } + return city === hubName ? '#FFD048' : '#6ecfff' } // 获取当前模式的数据 @@ -195,6 +319,7 @@ const generateScatterData = () => { citySet.forEach(city => { const coord = tradingData.geoCoordMap[city] if (coord) { + const hubName = getHubName() // 计算该城市的总流量 let totalFlow = 0 modeData.flows.forEach(flow => { @@ -206,13 +331,14 @@ const generateScatterData = () => { scatterData.push({ name: city, value: coord.concat([totalFlow]), + symbol: city === hubName ? 'diamond' : 'circle', symbolSize: Math.max(6, Math.min(18, totalFlow / 80)), itemStyle: { - color: getNodeColor(city, modeData), + color: getNodeColor(city), borderColor: '#fff', borderWidth: 1.5, shadowBlur: 6, - shadowColor: getNodeColor(city, modeData) + shadowColor: getNodeColor(city) } }) } @@ -224,16 +350,22 @@ const generateScatterData = () => { // 生成流向线数据 const generateLinesData = () => { const modeData = getCurrentModeData() - return modeData.flows.map(flow => ({ - fromName: flow.from, - toName: flow.to, - coords: [ - tradingData.geoCoordMap[flow.from], - tradingData.geoCoordMap[flow.to] - ], - value: flow.value, - description: flow.description - })) + return modeData.flows + .map((flow) => { + const fromCoord = tradingData.geoCoordMap[flow.from] + const toCoord = tradingData.geoCoordMap[flow.to] + if (!fromCoord || !toCoord) { + return null + } + return { + fromName: flow.from, + toName: flow.to, + coords: [fromCoord, toCoord], + value: flow.value, + description: flow.description + } + }) + .filter(Boolean) } // 生成波纹效果数据 @@ -249,18 +381,21 @@ const generateEffectScatterData = () => { }) // 为流量大的城市添加波纹效果 + const flowThreshold = tradingData?.flowLevels?.medium?.threshold || 40 + const hubName = getHubName() Object.entries(cityFlows).forEach(([city, totalFlow]) => { - if (totalFlow > 400) { // 流量阈值 + if (totalFlow > flowThreshold) { const coord = tradingData.geoCoordMap[city] if (coord) { effectData.push({ name: city, value: coord.concat([totalFlow]), + symbol: city === hubName ? 'diamond' : 'circle', symbolSize: Math.max(14, Math.min(26, totalFlow / 60)), itemStyle: { - color: getNodeColor(city, modeData), + color: getNodeColor(city), shadowBlur: 12, - shadowColor: getNodeColor(city, modeData) + shadowColor: getNodeColor(city) } }) } @@ -483,7 +618,7 @@ const generateDetailData = (cityName) => { ] // 根据不同城市返回相应的明细数据 - if (cityName === '红原县') { + if (cityName === getHubName()) { return baseData } else { // 为其他城市生成相应的数据 @@ -521,70 +656,386 @@ const closeDetailPanel = () => { // 计算地图最佳视野范围 const calculateMapBounds = () => { - // 对于红原出栏分布图模式,让ECharts自动适应GeoJSON边界,不需要手动计算 if (currentMode.value === 'local') { - console.log('红原出栏分布图模式:使用ECharts自动缩放') - return { center: [104, 35], zoom: 1.0 } // 返回默认值,实际不会使用 + return { center: [104, 35], zoom: 1.0 } } - - // 其他模式使用原有的基于交易数据的计算方式 - const modeData = getCurrentModeData() - const cities = new Set() - - // 收集当前模式下所有涉及的城市 - modeData.flows.forEach(flow => { - cities.add(flow.from) - cities.add(flow.to) + + return { + center: [...MAP_ANCHOR_CENTER], + zoom: MAP_ANCHOR_ZOOM * MAP_ZOOM_FACTOR + } +} + +const clamp = (value, min, max) => Math.min(max, Math.max(min, value)) + +const getFeatureCenter = (feature) => { + const cp = feature?.properties?.cp || feature?.properties?.center + if (Array.isArray(cp) && cp.length >= 2) { + return cp + } + if (Array.isArray(cp) && cp.length === 1) { + return [cp[0], 35] + } + return [104, 35] +} + +const getRegionAreaColor = (lng, lat, minLng, maxLng, minLat, maxLat) => { + const { r, g, b } = blendDesignColor(lng, lat, minLng, maxLng, minLat, maxLat) + const cacheKey = `${r},${g},${b},${MAP_FILL_OPACITY}` + + if (!mapTextureCache.has(cacheKey)) { + mapTextureCache.set(cacheKey, createRegionTextureCanvas(r, g, b, MAP_FILL_OPACITY)) + } + + return { + image: mapTextureCache.get(cacheKey), + repeat: 'repeat' + } +} + +const createRegionTextureCanvas = (r, g, b, alpha) => { + if (typeof document === 'undefined') { + return null + } + + const size = MAP_TEXTURE_SIZE + const canvas = document.createElement('canvas') + canvas.width = size + canvas.height = size + const ctx = canvas.getContext('2d') + if (!ctx) { + return null + } + + ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})` + ctx.fillRect(0, 0, size, size) + + for (let i = 0; i < 420; i += 1) { + const x = Math.random() * size + const y = Math.random() * size + const radius = Math.random() * 1.6 + 0.3 + ctx.fillStyle = `rgba(255, 255, 255, ${Math.random() * 0.1 + 0.02})` + ctx.beginPath() + ctx.arc(x, y, radius, 0, Math.PI * 2) + ctx.fill() + } + + for (let i = 0; i < 260; i += 1) { + const x = Math.random() * size + const y = Math.random() * size + const radius = Math.random() * 2.4 + 0.6 + ctx.fillStyle = `rgba(8, 45, 72, ${Math.random() * 0.08 + 0.02})` + ctx.beginPath() + ctx.arc(x, y, radius, 0, Math.PI * 2) + ctx.fill() + } + + for (let i = 0; i < 6; i += 1) { + const x = Math.random() * size + const y = Math.random() * size + const radius = Math.random() * 28 + 18 + const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius) + gradient.addColorStop(0, `rgba(255, 255, 255, ${Math.random() * 0.06 + 0.02})`) + gradient.addColorStop(1, 'rgba(255, 255, 255, 0)') + ctx.fillStyle = gradient + ctx.beginPath() + ctx.arc(x, y, radius, 0, Math.PI * 2) + ctx.fill() + } + + return canvas +} + +const blendDesignColor = (lng, lat, minLng, maxLng, minLat, maxLat) => { + const lngRatio = clamp((lng - minLng) / (maxLng - minLng), 0, 1) + const latRatio = clamp((lat - minLat) / (maxLat - minLat), 0, 1) + + let r = MAP_FILL_WEST[0] + (MAP_FILL_EAST[0] - MAP_FILL_WEST[0]) * lngRatio + let g = MAP_FILL_WEST[1] + (MAP_FILL_EAST[1] - MAP_FILL_WEST[1]) * lngRatio + let b = MAP_FILL_WEST[2] + (MAP_FILL_EAST[2] - MAP_FILL_WEST[2]) * lngRatio + + const northBoost = (0.62 - latRatio) * 16 + const southDepth = (latRatio - 0.38) * 10 + r = clamp(r + northBoost - southDepth * 0.35, 0, 255) + g = clamp(g + northBoost * 0.95 - southDepth * 0.25, 0, 255) + b = clamp(b + northBoost * 0.55 - southDepth * 0.15, 0, 255) + + return { + r: Math.round(r), + g: Math.round(g), + b: Math.round(b) + } +} + +const pointKey = ([lng, lat]) => `${lng.toFixed(5)},${lat.toFixed(5)}` + +const edgeKey = (a, b) => { + const ka = pointKey(a) + const kb = pointKey(b) + return ka < kb ? `${ka}|${kb}` : `${kb}|${ka}` +} + +const forEachOuterRing = (geometry, callback) => { + if (!geometry) return + if (geometry.type === 'Polygon') { + if (geometry.coordinates[0]) { + callback(geometry.coordinates[0]) + } + } else if (geometry.type === 'MultiPolygon') { + geometry.coordinates.forEach((polygon) => { + if (polygon[0]) { + callback(polygon[0]) + } + }) + } +} + +const stitchBoundarySegments = (segments) => { + const adj = new Map() + segments.forEach(([a, b], idx) => { + const ka = pointKey(a) + const kb = pointKey(b) + if (!adj.has(ka)) adj.set(ka, []) + if (!adj.has(kb)) adj.set(kb, []) + adj.get(ka).push({ point: b, idx }) + adj.get(kb).push({ point: a, idx }) }) - - // 获取所有城市的坐标 - const coordinates = Array.from(cities) - .map(city => tradingData.geoCoordMap[city]) - .filter(coord => coord && coord.length === 2) - - if (coordinates.length === 0) { - return { center: [104, 35], zoom: 0.9 } + + const used = new Set() + const paths = [] + + segments.forEach(([a, b], idx) => { + if (used.has(idx)) { + return + } + + used.add(idx) + const path = [a, b] + + const extendTail = () => { + const tailKey = pointKey(path[path.length - 1]) + const candidates = (adj.get(tailKey) || []).filter((item) => !used.has(item.idx)) + if (!candidates.length) { + return false + } + const next = candidates[0] + used.add(next.idx) + path.push(next.point) + return true + } + + const extendHead = () => { + const headKey = pointKey(path[0]) + const candidates = (adj.get(headKey) || []).filter((item) => !used.has(item.idx)) + if (!candidates.length) { + return false + } + const next = candidates[0] + used.add(next.idx) + path.unshift(next.point) + return true + } + + while (extendTail()) {} + while (extendHead()) {} + paths.push(path) + }) + + return paths +} + +const buildNationalOutlinePaths = (geojson) => { + if (!geojson?.features?.length) { + return [] } - - // 计算经纬度边界 - const lngs = coordinates.map(coord => coord[0]) - const lats = coordinates.map(coord => coord[1]) - - const minLng = Math.min(...lngs) - const maxLng = Math.max(...lngs) - const minLat = Math.min(...lats) - const maxLat = Math.max(...lats) - - // 计算中心点 - const centerLng = (minLng + maxLng) / 2 - const centerLat = (minLat + maxLat) / 2 - - // 计算跨度 - const lngSpan = maxLng - minLng - const latSpan = maxLat - minLat - const maxSpan = Math.max(lngSpan, latSpan) - - // 根据跨度计算缩放级别,添加适当的边距 - let zoom = 0.9 - if (maxSpan > 0) { - // 基础缩放计算,加上20%的边距 - const baseZoom = Math.min(20 / (maxSpan * 1.2), 3.5) - zoom = Math.max(1.2, Math.min(baseZoom, 3.0)) + + const edgeCount = new Map() + const edgeList = [] + + geojson.features.forEach((feature) => { + forEachOuterRing(feature.geometry, (ring) => { + if (!Array.isArray(ring) || ring.length < 2) { + return + } + for (let i = 0; i < ring.length - 1; i += 1) { + const a = ring[i] + const b = ring[i + 1] + if (!a || !b || (a[0] === b[0] && a[1] === b[1])) { + continue + } + const key = edgeKey(a, b) + edgeCount.set(key, (edgeCount.get(key) || 0) + 1) + edgeList.push({ key, a, b }) + } + }) + }) + + const boundarySegments = [] + edgeList.forEach(({ key, a, b }) => { + if (edgeCount.get(key) === 1) { + boundarySegments.push([a, b]) + } + }) + + return stitchBoundarySegments(boundarySegments) +} + +const getWireframePaths = () => { + const mapName = getMapName(currentMode.value) + const cacheKey = `${mapName}-${chinaMapData?.features?.length || 0}` + if (cacheKey === cachedOutlineKey) { + return cachedOutlinePaths } - - // 针对不同模式进行微调 - if (currentMode.value === 'outflow' || currentMode.value === 'inflow') { - zoom = Math.min(zoom * 1.1, 2.8) // 销售和供应模式稍微放大一些 - } else { - zoom = Math.min(zoom * 1.2, 3.0) // 本地模式更大放大 + + const minPoints = currentMode.value === 'local' ? 8 : 30 + cachedOutlineKey = cacheKey + cachedOutlinePaths = buildNationalOutlinePaths(chinaMapData) + .filter((path) => path.length >= minPoints) + + return cachedOutlinePaths +} + +const buildRegionFillData = () => { + if (!chinaMapData?.features?.length) { + return [] } - + + let minLng = MAP_GRADIENT_MIN_LNG + let maxLng = MAP_GRADIENT_MAX_LNG + let minLat = MAP_GRADIENT_MIN_LAT + let maxLat = MAP_GRADIENT_MAX_LAT + + if (currentMode.value === 'local') { + const centers = chinaMapData.features.map(getFeatureCenter) + const lngs = centers.map((center) => center[0]) + const lats = centers.map((center) => center[1]) + minLng = Math.min(...lngs) + maxLng = Math.max(...lngs) + minLat = Math.min(...lats) + maxLat = Math.max(...lats) + if (maxLng - minLng < 0.001) { + maxLng = minLng + 1 + } + if (maxLat - minLat < 0.001) { + maxLat = minLat + 1 + } + } + + return chinaMapData.features.map((feature) => { + const name = feature.properties?.name + if (!name) { + return null + } + const [lng, lat] = getFeatureCenter(feature) + return { + name, + itemStyle: { + areaColor: getRegionAreaColor(lng, lat, minLng, maxLng, minLat, maxLat), + borderColor: MAP_INNER_BORDER, + borderWidth: MAP_INNER_BORDER_WIDTH + } + } + }).filter(Boolean) +} + +const getMapProjection = (mapBounds, latOffset = 0, lngOffset = 0) => { + const isLocal = currentMode.value === 'local' + if (isLocal) { + return { + aspectScale: MAP_ASPECT_SCALE, + layoutCenter: [ + `${50 + lngOffset + MAP_GLOBAL_LAYOUT_X}%`, + `${50 + latOffset + MAP_GLOBAL_LAYOUT_Y}%` + ], + layoutSize: MAP_LAYOUT_SIZE + } + } + const center = mapBounds.center || MAP_ANCHOR_CENTER + const zoom = mapBounds.zoom || MAP_ANCHOR_ZOOM return { - center: [centerLng, centerLat], - zoom: zoom + aspectScale: MAP_ASPECT_SCALE, + zoom, + center: [ + center[0] + lngOffset + MAP_GLOBAL_OFFSET_LNG, + center[1] + latOffset + MAP_GLOBAL_OFFSET_LAT + ] } } +const buildGeoLayers = (mapName, mapBounds) => { + const isLocal = currentMode.value === 'local' + const shadowFarOffset = isLocal ? MAP_SHADOW_OFFSET_FAR_LOCAL : MAP_SHADOW_OFFSET_FAR + const shadowNearOffset = isLocal ? MAP_SHADOW_OFFSET_NEAR_LOCAL : MAP_SHADOW_OFFSET_NEAR + const shadowLngOffset = isLocal ? MAP_SHADOW_OFFSET_LNG_LOCAL : MAP_SHADOW_OFFSET_LNG + const wireframeOffset = isLocal ? MAP_WIREFRAME_OFFSET_LOCAL : MAP_WIREFRAME_OFFSET + const wireframeLngOffset = isLocal ? MAP_WIREFRAME_OFFSET_LNG_LOCAL : MAP_WIREFRAME_OFFSET_LNG + + return [ + { + map: mapName, + roam: false, + ...getMapProjection(mapBounds, shadowFarOffset, shadowLngOffset), + silent: true, + zlevel: 0, + z: 1, + label: { show: false }, + itemStyle: { + areaColor: MAP_SHADOW_FAR, + borderColor: MAP_SHADOW_FAR, + borderWidth: 0 + } + }, + { + map: mapName, + roam: false, + ...getMapProjection(mapBounds, shadowNearOffset, shadowLngOffset), + silent: true, + zlevel: 0, + z: 2, + label: { show: false }, + itemStyle: { + areaColor: MAP_SHADOW_NEAR, + borderColor: MAP_SHADOW_NEAR, + borderWidth: 0 + } + }, + { + map: mapName, + roam: false, + ...getMapProjection(mapBounds, 0), + silent: true, + zlevel: 1, + z: 1, + label: { show: false }, + itemStyle: { + areaColor: 'rgba(0, 0, 0, 0)', + borderColor: 'transparent', + borderWidth: 0 + }, + emphasis: { + disabled: true + } + }, + { + map: mapName, + roam: false, + ...getMapProjection(mapBounds, wireframeOffset, wireframeLngOffset), + silent: true, + zlevel: 4, + z: 1, + label: { show: false }, + itemStyle: { + areaColor: 'rgba(0, 0, 0, 0)', + borderColor: 'transparent', + borderWidth: 0 + }, + emphasis: { + disabled: true + } + } + ] +} + // 初始化图表配置 const getChartOption = () => { const modeData = getCurrentModeData() @@ -592,6 +1043,10 @@ const getChartOption = () => { const linesData = generateLinesData() const effectScatterData = generateEffectScatterData() const mapBounds = calculateMapBounds() + const mapName = getMapName(currentMode.value) + const mapLayout = getMapProjection(mapBounds, 0) + const outlinePaths = getWireframePaths() + const wireframeData = outlinePaths.map((coords) => ({ coords })) return { backgroundColor: 'transparent', // 透明背景,与卡片一致 @@ -626,7 +1081,8 @@ const getChartOption = () => { ` } else if (params.seriesType === 'scatter') { const value = params.value[2] || 0 - const nodeType = params.name === '红原县' ? '中心节点' : + const hubName = getHubName() + const nodeType = params.name === hubName ? '中心节点' : (currentMode.value === 'outflow' ? '销售市场' : '供应来源') return `
${params.name}
@@ -654,80 +1110,77 @@ const getChartOption = () => { return params.name } }, - legend: { - orient: 'vertical', - left: 'left', - top: 'bottom', - data: ['交易城市', '流向线', '重要节点'], - textStyle: { - color: '#8cc8ff', - fontSize: 12 - }, - itemStyle: { - borderColor: '#00d4ff' - } - }, - geo: { - map: getMapName(currentMode.value), - roam: true, - // 只有在非local模式时才手动设置zoom和center,让ECharts自动适应GeoJSON范围 - ...(currentMode.value !== 'local' && { - zoom: mapBounds.zoom, - center: mapBounds.center - }), - label: { - show: true, - color: '#ffffff', - fontSize: 11, - fontWeight: 'bold', - shadowBlur: 3, - shadowColor: '#000' - }, - emphasis: { + geo: buildGeoLayers(mapName, mapBounds), + series: [ + { + name: 'mapFill', + type: 'map', + map: mapName, + roam: false, + ...mapLayout, + zlevel: 2, + silent: true, + selectedMode: false, + data: buildRegionFillData(), + itemStyle: { + borderColor: MAP_INNER_BORDER, + borderWidth: MAP_INNER_BORDER_WIDTH + }, label: { show: true, - color: '#00d4ff', - fontSize: 13, - fontWeight: 'bold' + color: MAP_LABEL_COLOR, + fontSize: 11, + fontFamily: 'Microsoft YaHei, sans-serif', + fontWeight: 'bold', + textBorderColor: 'rgba(0, 0, 0, 0.45)', + textBorderWidth: 2 }, - itemStyle: { - areaColor: '#1e3a5f', - borderColor: '#00d4ff', - borderWidth: 2, - shadowBlur: 10, - shadowColor: '#00d4ff' + emphasis: { + disabled: true + } + }, + { + name: 'mapWireframe', + type: 'lines', + coordinateSystem: 'geo', + geoIndex: WIREFRAME_GEO_INDEX, + zlevel: 5, + polyline: true, + silent: true, + data: wireframeData, + lineStyle: { + color: MAP_WIREFRAME_COLOR, + width: 3.2, + opacity: 0.96, + cap: 'round', + join: 'round', + shadowBlur: 16, + shadowColor: 'rgba(120, 230, 255, 0.6)' } }, - itemStyle: { - areaColor: '#0f1b2e', // 深蓝色地图 - borderColor: '#2c5282', - borderWidth: 1, - shadowBlur: 5, - shadowColor: 'rgba(0, 212, 255, 0.3)' - } - }, - series: [ { name: '交易城市', type: 'scatter', coordinateSystem: 'geo', + geoIndex: GEO_DATA_INDEX, + zlevel: 6, data: scatterData, symbolSize: 8, label: { show: true, position: 'top', - color: '#ffffff', + color: MAP_LABEL_COLOR, fontSize: 11, fontWeight: 'bold', formatter: '{b}', - shadowBlur: 2, - shadowColor: '#000' + textBorderColor: 'rgba(0, 0, 0, 0.35)', + textBorderWidth: 2 }, emphasis: { label: { show: true, fontSize: 13, - color: '#00d4ff' + color: MAP_LABEL_COLOR }, itemStyle: { borderColor: '#00d4ff', @@ -737,28 +1190,30 @@ const getChartOption = () => { } }, { - name: '流向线', + name: '流向', type: 'lines', coordinateSystem: 'geo', + geoIndex: GEO_DATA_INDEX, + zlevel: 6, data: linesData, large: true, effect: { show: true, - constantSpeed: 40, + constantSpeed: 45, symbol: 'arrow', - symbolSize: 12, - trailLength: 0.15, - color: '#ffffff', - shadowBlur: 6, - shadowColor: '#00d4ff' + symbolSize: 10, + trailLength: 0.2, + color: '#e8f7ff', + shadowBlur: 8, + shadowColor: 'rgba(110, 207, 255, 0.8)' }, lineStyle: { - color: '#00d4ff', - width: 1.2, - opacity: 0.7, - curveness: 0.3, - shadowBlur: 3, - shadowColor: '#00d4ff' + color: '#7ee8ff', + width: 1.5, + opacity: 0.85, + curveness: 0.28, + shadowBlur: 8, + shadowColor: 'rgba(110, 232, 255, 0.55)' }, emphasis: { lineStyle: { @@ -772,6 +1227,8 @@ const getChartOption = () => { name: '重要节点', type: 'effectScatter', coordinateSystem: 'geo', + geoIndex: GEO_DATA_INDEX, + zlevel: 6, data: effectScatterData, symbolSize: function(val) { return Math.max(14, Math.min(26, val[2] / 60)) @@ -785,12 +1242,12 @@ const getChartOption = () => { label: { show: true, position: 'top', - color: '#ffffff', + color: MAP_LABEL_COLOR, fontSize: 12, fontWeight: 'bold', formatter: '{b}', - shadowBlur: 3, - shadowColor: '#000' + textBorderColor: 'rgba(0, 0, 0, 0.35)', + textBorderWidth: 2 }, itemStyle: { shadowBlur: 15 @@ -819,6 +1276,9 @@ const reloadMapData = async (mapMode) => { const mapName = getMapName(mapMode) echarts.registerMap(mapName, chinaMapData) + cachedOutlineKey = '' + cachedOutlinePaths = [] + mapTextureCache.clear() return true } catch (error) { @@ -872,11 +1332,8 @@ const updateChart = async (needReloadMap = false) => { } chartInstance.setOption(getChartOption(), { - replaceMerge: ['series', 'geo'], - transition: { - duration: 1200, - easing: 'cubicInOut' - } + notMerge: false, + replaceMerge: ['geo', 'series'] }) } } @@ -911,64 +1368,184 @@ onUnmounted(() => { \ No newline at end of file + diff --git a/src/components/MarketRealtimeMonitor.vue b/src/components/MarketRealtimeMonitor.vue index 692c3bc..45c9970 100644 --- a/src/components/MarketRealtimeMonitor.vue +++ b/src/components/MarketRealtimeMonitor.vue @@ -1,75 +1,124 @@ @@ -77,112 +126,110 @@ \ No newline at end of file + diff --git a/src/components/MonitorLivePlayer.vue b/src/components/MonitorLivePlayer.vue new file mode 100644 index 0000000..c7d8663 --- /dev/null +++ b/src/components/MonitorLivePlayer.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/src/components/PurchaserAnalysis.vue b/src/components/PurchaserAnalysis.vue index b0e6528..1f6f95b 100644 --- a/src/components/PurchaserAnalysis.vue +++ b/src/components/PurchaserAnalysis.vue @@ -1,6 +1,18 @@ @@ -8,216 +20,344 @@ import { ref, onMounted, onUnmounted } from 'vue' import * as echarts from 'echarts' import BaseCard from './BaseCard.vue' +import { bindChartBaseSync, syncChartBaseToGrid } from '../utils/chartBaseLayout.js' + +const API_URL = '/api/dashboard/buyer-source-analysis' + +const CHART_GRID = { + left: 48, + right: 24, + bottom: 56, + top: 18 +} + +const BAR_WIDTH = 13 +const CAP_RY = 3.5 +const MIN_BAR_PX = 8 + +const CYLINDER_PALETTES = [ + { + edge: 'rgba(44, 162, 163, 0.60)', + center: 'rgba(2, 206, 214, 0.80)', + capCenter: 'rgba(2, 206, 214, 0.95)', + capEdge: 'rgba(44, 162, 163, 0.70)', + bottom: 'rgba(44, 162, 163, 0.42)', + zeroCenter: 'rgba(2, 206, 214, 0.45)' + }, + { + edge: 'rgba(44, 162, 163, 0.60)', + center: 'rgba(25, 217, 170, 0.80)', + capCenter: 'rgba(25, 217, 170, 0.95)', + capEdge: 'rgba(44, 162, 163, 0.70)', + bottom: 'rgba(44, 162, 163, 0.42)', + zeroCenter: 'rgba(25, 217, 170, 0.45)' + } +] + +const ZERO_EDGE = 'rgba(44, 162, 163, 0.35)' +const ZERO_BOTTOM = 'rgba(44, 162, 163, 0.22)' const chartRef = ref(null) +const baseRef = ref(null) +const regionsData = ref([]) let chartInstance = null +let tipTimer = null +let tipIndex = 0 +let unbindChartBaseSync = null + +const calcAxisMax = (values, ratio = 1.25) => { + const maxVal = Math.max(0, ...values.map((v) => Number(v) || 0)) + if (maxVal === 0) { + return 5 + } + + const raw = maxVal * ratio + const magnitude = Math.pow(10, Math.floor(Math.log10(raw))) + const normalized = raw / magnitude + let nice = 10 + + if (normalized <= 1) nice = 1 + else if (normalized <= 2) nice = 2 + else if (normalized <= 5) nice = 5 -// 采购商户来源数据 - 基于销售网络分布图的目标区域 -const regionsData = ref([ - { name: '果洛藏族自治州', value: 156, color: '#409EFF', description: '青海果洛州采购商户' }, - { name: '成都', value: 142, color: '#67C23A', description: '四川省会城市采购商户' }, - { name: '玉树藏族自治州', value: 128, color: '#E6A23C', description: '青海玉树州采购商户' }, - { name: '甘孜藏族自治州', value: 115, color: '#F56C6C', description: '四川甘孜州采购商户' }, - { name: '拉萨', value: 98, color: '#909399', description: '西藏自治区采购商户' }, - { name: '西宁', value: 89, color: '#00D4AA', description: '青海省会城市采购商户' }, - { name: '兰州', value: 76, color: '#8B5CF6', description: '甘肃省会城市采购商户' }, - { name: '甘南藏族自治州', value: 68, color: '#FF6B9D', description: '甘肃甘南州采购商户' }, - { name: '昌都', value: 54, color: '#FFB800', description: '西藏昌都地区采购商户' } -]) - -// 数据更新函数 -const updateData = () => { - regionsData.value.forEach(region => { - // 小幅度随机调整数据 - const variance = (Math.random() - 0.5) * 20 // ±10的变化 - region.value = Math.max(20, Math.floor(region.value + variance)) - }) + return nice * magnitude } -const initChart = () => { - if (!chartRef.value) return - - chartInstance = echarts.init(chartRef.value) - - const updateChart = () => { - const option = { - tooltip: { - trigger: 'axis', - backgroundColor: 'rgba(15, 25, 45, 0.95)', - borderColor: '#00d4ff', - borderWidth: 1, - textStyle: { - color: '#fff', - fontSize: 15 - }, - formatter: function(params) { - const item = regionsData.value[params[0].dataIndex] - return `
- ${params[0].name}
- 采购商户: ${params[0].value} 家
- ${item.description} -
` - } - }, - grid: { - left: '8%', - right: '8%', - bottom: '20%', - top: '15%', - containLabel: true +const getZeroDisplayValue = (axisMax) => axisMax * 0.04 + +const createCylinderBarRenderer = (axisMax) => (params, api) => { + const categoryIndex = params.dataIndex + const rawValue = Number(api.value(1)) || 0 + const displayValue = rawValue > 0 ? rawValue : getZeroDisplayValue(axisMax) + const isZero = rawValue === 0 + + const base = api.coord([categoryIndex, 0]) + const top = api.coord([categoryIndex, displayValue]) + const cx = base[0] + const topY = top[1] + const bodyTop = topY + CAP_RY + const bodyHeight = Math.max(base[1] - bodyTop, MIN_BAR_PX) + const x = cx - BAR_WIDTH / 2 + const palette = CYLINDER_PALETTES[categoryIndex % 2] + + const bodyGradient = new echarts.graphic.LinearGradient(0, 0, 1, 0, [ + { offset: 0, color: isZero ? ZERO_EDGE : palette.edge }, + { offset: 0.5, color: isZero ? palette.zeroCenter : palette.center }, + { offset: 1, color: isZero ? ZERO_EDGE : palette.edge } + ]) + + const capGradient = new echarts.graphic.LinearGradient(0, 0, 1, 0, [ + { offset: 0, color: isZero ? ZERO_EDGE : palette.capEdge }, + { offset: 0.5, color: isZero ? palette.zeroCenter : palette.capCenter }, + { offset: 1, color: isZero ? ZERO_EDGE : palette.capEdge } + ]) + + const bottomCap = { + type: 'ellipse', + shape: { + cx, + cy: base[1], + rx: BAR_WIDTH / 2, + ry: CAP_RY * 0.55 + }, + style: { + fill: isZero ? ZERO_BOTTOM : palette.bottom + } + } + + const body = { + type: 'rect', + shape: { + x, + y: bodyTop, + width: BAR_WIDTH, + height: bodyHeight, + r: [0, 0, 0, 0] + }, + style: { + fill: bodyGradient + } + } + + const topCap = { + type: 'ellipse', + shape: { + cx, + cy: bodyTop, + rx: BAR_WIDTH / 2, + ry: CAP_RY + }, + style: { + fill: capGradient + } + } + + return { + type: 'group', + children: [bottomCap, body, topCap] + } +} + +const buildChartOption = (axisMax) => { + const labels = regionsData.value.map((item) => item.name) + const values = regionsData.value.map((item) => item.value) + + return { + animation: true, + animationDuration: 800, + animationEasing: 'cubicOut', + tooltip: { + trigger: 'axis', + backgroundColor: 'rgba(6, 28, 58, 0.94)', + borderColor: '#41a6fc', + borderWidth: 1, + padding: [10, 14], + textStyle: { + color: '#ffffff', + fontSize: 13, + fontFamily: 'Microsoft YaHei, sans-serif' }, - xAxis: { - type: 'category', - data: regionsData.value.map(item => item.name), - axisLine: { - lineStyle: { - color: '#4a5568' - } - }, - axisTick: { - show: false - }, - axisLabel: { - color: '#a0a8b8', - fontSize: 12, - rotate: 30, - interval: 0, - formatter: function(value) { - // 对长地名进行换行处理 - if (value.length > 6) { - return value.substring(0, 4) + '\n' + value.substring(4) - } + extraCssText: 'box-shadow: 0 0 14px rgba(65, 166, 252, 0.35); border-radius: 6px;', + formatter(params) { + const idx = params[0]?.dataIndex ?? 0 + const item = regionsData.value[idx] + if (!item) { + return '' + } + return [ + `
${item.name}
`, + `
采购商户:${item.value}家
`, + `
${item.description}
` + ].join('') + } + }, + grid: CHART_GRID, + xAxis: { + type: 'category', + data: labels, + boundaryGap: true, + axisLine: { show: false }, + axisTick: { show: false }, + axisLabel: { + color: '#ffffff', + fontSize: 11, + fontFamily: 'Microsoft YaHei, sans-serif', + interval: 0, + margin: 10, + formatter(value) { + if (value.length <= 5) { return value } + return `${value.slice(0, 5)}\n${value.slice(5)}` } - }, - yAxis: { - type: 'value', - name: '采购商户数量(家)', - nameTextStyle: { - color: '#a0a8b8', - fontSize: 13 - }, - splitLine: { - lineStyle: { - color: '#2d3748', - type: 'dashed' - } - }, - axisLine: { - show: false - }, - axisTick: { - show: false - }, - axisLabel: { - color: '#a0a8b8', - fontSize: 13 + } + }, + yAxis: { + type: 'value', + min: 0, + max: axisMax, + splitNumber: 5, + splitLine: { + lineStyle: { + color: 'rgba(80, 160, 220, 0.22)', + type: 'dashed' } }, - series: [{ - name: '采购商户数量', - type: 'bar', - data: regionsData.value.map((item, index) => ({ - value: item.value, - itemStyle: { - color: { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, - colorStops: [{ - offset: 0, - color: item.color - }, { - offset: 1, - color: item.color + '40' // 添加透明度 - }] - }, - borderRadius: [6, 6, 0, 0], - borderWidth: 2, - borderColor: item.color, - shadowBlur: 8, - shadowColor: item.color + '30' - } - })), - barWidth: '60%', - emphasis: { - itemStyle: { - shadowBlur: 15, - shadowColor: 'rgba(0, 212, 255, 0.6)', - borderColor: '#00d4ff', - borderWidth: 3 - }, - scaleSize: 5 - }, - label: { - show: true, - position: 'top', - color: '#fff', - fontSize: 13, - fontWeight: 'bold', - formatter: '{c}家' - }, - animationDelay: function (idx) { - return idx * 100 - } - }], - animationEasing: 'elasticOut', - animationDelayUpdate: function (idx) { - return idx * 50 + axisLine: { show: false }, + axisTick: { show: false }, + axisLabel: { + color: '#ffffff', + fontSize: 12, + fontFamily: 'Microsoft YaHei, sans-serif' } - } - chartInstance.setOption(option) + }, + series: [{ + name: '采购商户数量', + type: 'custom', + renderItem: createCylinderBarRenderer(axisMax), + data: values.map((value, index) => [index, value]), + encode: { x: 0, y: 1 }, + z: 2 + }] } +} - updateChart() +const updateChart = () => { + if (!chartInstance || !regionsData.value.length) { + return + } - // 添加动态效果 - let currentIndex = 0 - const timer = setInterval(() => { + const values = regionsData.value.map((item) => item.value) + const axisMax = calcAxisMax(values, 1.3) + chartInstance.setOption(buildChartOption(axisMax), true) + syncChartBaseToGrid(chartInstance, baseRef.value) +} + +const startTipCarousel = () => { + stopTipCarousel() + if (!chartInstance || regionsData.value.length === 0) { + return + } + + tipTimer = setInterval(() => { + chartInstance.dispatchAction({ type: 'hideTip' }) chartInstance.dispatchAction({ type: 'showTip', seriesIndex: 0, - dataIndex: currentIndex + dataIndex: tipIndex }) - currentIndex = (currentIndex + 1) % regionsData.value.length + tipIndex = (tipIndex + 1) % regionsData.value.length }, 3000) +} + +const stopTipCarousel = () => { + if (tipTimer) { + clearInterval(tipTimer) + tipTimer = null + } +} + +const loadData = async () => { + try { + const response = await fetch(API_URL) + if (!response.ok) { + throw new Error(`HTTP ${response.status}`) + } + + const result = await response.json() + if (result.code !== 1 || !result.data) { + throw new Error(result.message || '接口返回异常') + } - // 定期更新数据 - const dataTimer = setInterval(() => { - updateData() + regionsData.value = (result.data || []).map((item) => ({ + name: item.name, + value: Number(item.value) || 0, + fullName: item.fullName || '', + description: item.description || `${item.name}采购商户` + })) + tipIndex = 0 updateChart() - }, 12000) // 12秒更新一次 + startTipCarousel() + } catch (error) { + console.error('加载采购商户来源分析失败:', error) + } +} - chartInstance._autoTimer = timer - chartInstance._dataTimer = dataTimer +const handleResize = () => { + chartInstance?.resize() + syncChartBaseToGrid(chartInstance, baseRef.value) } -onMounted(() => { - initChart() - window.addEventListener('resize', () => { - chartInstance?.resize() - }) +onMounted(async () => { + if (chartRef.value) { + chartInstance = echarts.init(chartRef.value) + unbindChartBaseSync = bindChartBaseSync(chartInstance, baseRef.value) + } + await loadData() + window.addEventListener('resize', handleResize) + window.addEventListener('dataReloaded', loadData) }) onUnmounted(() => { - if (chartInstance) { - if (chartInstance._autoTimer) { - clearInterval(chartInstance._autoTimer) - } - if (chartInstance._dataTimer) { - clearInterval(chartInstance._dataTimer) - } - chartInstance.dispose() - } + stopTipCarousel() + window.removeEventListener('resize', handleResize) + window.removeEventListener('dataReloaded', loadData) + unbindChartBaseSync?.() + chartInstance?.dispose() }) \ No newline at end of file + diff --git a/src/components/SupplyDemandData.vue b/src/components/SupplyDemandData.vue index f6d8ff9..ef19684 100644 --- a/src/components/SupplyDemandData.vue +++ b/src/components/SupplyDemandData.vue @@ -1,88 +1,100 @@ - - \ No newline at end of file + diff --git a/src/components/YakSalesTypeStats.vue b/src/components/YakSalesTypeStats.vue index d042d06..e58048f 100644 --- a/src/components/YakSalesTypeStats.vue +++ b/src/components/YakSalesTypeStats.vue @@ -5,14 +5,12 @@
-
+
-
- {{ item.name }} -
- {{ item.count }}头 - {{ item.value }}% -
+
+ {{ item.name }}: + {{ formatPercent(item.percent) }}% + {{ item.value }}头
@@ -21,276 +19,201 @@ \ No newline at end of file + diff --git a/src/utils/liveStreamPlayer.js b/src/utils/liveStreamPlayer.js new file mode 100644 index 0000000..8d5c05b --- /dev/null +++ b/src/utils/liveStreamPlayer.js @@ -0,0 +1,123 @@ +import Hls from 'hls.js' +import flvjs from 'flv.js' + +const DEFAULT_FORMAT_RULES = [ + { type: 'hls', match: '.m3u8', library: 'hls.js' }, + { type: 'flv', match: '.flv', library: 'flv.js' }, + { type: 'mp4', match: '.mp4', library: 'native' }, + { type: 'webm', match: '.webm', library: 'native' } +] + +export function getFormatRules(playerConfig) { + const rules = playerConfig?.formatRules + return Array.isArray(rules) && rules.length ? rules : DEFAULT_FORMAT_RULES +} + +export function detectStreamType(url, playerConfig) { + if (!url) { + return 'native' + } + const lower = ('' + url).toLowerCase() + const rules = getFormatRules(playerConfig) + for (const rule of rules) { + const match = rule.match ? ('' + rule.match).toLowerCase() : '' + if (match && lower.includes(match)) { + return rule.type || 'native' + } + } + return 'native' +} + +export function resolveStreamUrl(camera, playerConfig) { + if (!camera) { + return '' + } + const priority = playerConfig?.urlPriority || ['hdStreamUrl', 'streamUrl', 'playUrl', 'preview'] + for (const key of priority) { + const value = camera[key] + if (value) { + return value + } + } + return '' +} + +export function createLivePlayer(videoEl, url, playerConfig = {}) { + if (!videoEl || !url) { + return null + } + + const streamType = detectStreamType(url, playerConfig) + const options = playerConfig?.playerOptions || {} + + if (streamType === 'hls') { + if (videoEl.canPlayType('application/vnd.apple.mpegurl')) { + videoEl.src = url + videoEl.play().catch(() => {}) + return { type: 'native-hls', videoEl } + } + if (Hls.isSupported()) { + const hls = new Hls({ + enableWorker: true, + lowLatencyMode: true, + ...(options.hls || {}) + }) + hls.loadSource(url) + hls.attachMedia(videoEl) + hls.on(Hls.Events.MANIFEST_PARSED, () => { + videoEl.play().catch(() => {}) + }) + hls.on(Hls.Events.ERROR, (_, data) => { + if (data?.fatal) { + console.error('HLS 播放失败:', data) + } + }) + return { type: 'hls', instance: hls, videoEl } + } + } + + if (streamType === 'flv' && flvjs.isSupported()) { + const flvPlayer = flvjs.createPlayer( + { + type: 'flv', + url, + isLive: true, + hasAudio: true, + hasVideo: true, + ...(options.flv || {}) + }, + { + enableWorker: true, + enableStashBuffer: false, + ...(options.flvMedia || {}) + } + ) + flvPlayer.attachMediaElement(videoEl) + flvPlayer.load() + flvPlayer.play().catch(() => {}) + return { type: 'flv', instance: flvPlayer, videoEl } + } + + videoEl.src = url + videoEl.play().catch(() => {}) + return { type: 'native', videoEl } +} + +export function destroyLivePlayer(player) { + if (!player) { + return + } + if (player.type === 'hls' && player.instance) { + player.instance.destroy() + } else if (player.type === 'flv' && player.instance) { + player.instance.pause() + player.instance.unload() + player.instance.detachMediaElement() + player.instance.destroy() + } + if (player.videoEl) { + player.videoEl.pause() + player.videoEl.removeAttribute('src') + player.videoEl.load() + } +} diff --git a/src/utils/pie3d.js b/src/utils/pie3d.js index 0cf18e9..62dd882 100644 --- a/src/utils/pie3d.js +++ b/src/utils/pie3d.js @@ -73,37 +73,171 @@ export const getParametricEquation = ( const PLACEHOLDER_SLICE_COUNT = 5 const PLACEHOLDER_SLICE_HEIGHT = 0.1 -const LABEL_LAYOUT = { +const DEFAULT_LABEL_LAYOUT = { centerX: 50, - centerY: 37, - radiusX: 29, - radiusY: 21, - radialLen: 7, - horizontalLen: 10 + centerY: 40, + projectScale: 33, + yCompress: 0.66, + startAngle: 0, + radialLen: 9, + horizontalLen: 11, + lineColor: 'rgba(142, 207, 255, 0.55)', + textColor: '#d8ecff', + percentColor: '#ffffff', + fontSize: 13 } -const buildLabelGraphics = (sortedData, sumValue) => { +const buildOverlayLabelSeries = (pieData, overlayConfig = {}) => { + const { + center = ['38%', '41%'], + radius = ['24%', '44%'], + startAngle = 48, + clockwise = false, + nameColor = '#41a6fc', + percentColor = '#ffffff', + lineColor = 'rgba(65, 166, 252, 0.55)', + fontSize = 13, + labelLineLength = 10, + labelLineLength2 = 12 + } = overlayConfig + + return { + name: 'pie-label-overlay', + type: 'pie', + radius, + center, + startAngle, + clockwise, + silent: true, + animation: false, + z: 10, + zlevel: 2, + itemStyle: { + color: 'transparent', + borderWidth: 0, + opacity: 0 + }, + emphasis: { + disabled: true + }, + labelLayout: { + hideOverlap: false + }, + labelLine: { + show: true, + length: labelLineLength, + length2: labelLineLength2, + smooth: false, + lineStyle: { + color: lineColor, + width: 1 + } + }, + label: { + show: true, + alignTo: 'edge', + edgeDistance: 4, + formatter: (params) => { + const percent = params.data?.percent ?? params.percent ?? 0 + return `{name|${params.name}}\n{percent|${percent}%}` + }, + rich: { + name: { + color: nameColor, + fontSize, + fontWeight: 500, + lineHeight: 18, + fontFamily: 'Microsoft YaHei' + }, + percent: { + color: percentColor, + fontSize, + fontWeight: 700, + lineHeight: 18, + fontFamily: 'Microsoft YaHei' + } + } + }, + data: pieData.map((item) => ({ + name: item.name, + value: item.value, + percent: item.percent, + labelLine: Number(item.percent) < 5 + ? { + length: 6, + length2: 22, + lineStyle: { color: lineColor, width: 1 } + } + : undefined + })) + } +} + +const projectSlicePoint = (midRatio, layout, view) => { + const { + centerX, + centerY, + projectScale, + yCompress, + startAngle + } = layout + const viewAlpha = view.viewAlpha ?? 28 + const viewBeta = view.viewBeta ?? 48 + + const midRadian = startAngle + midRatio * Math.PI * 2 + const x = Math.cos(midRadian) + const y = Math.sin(midRadian) + + const betaRad = (viewBeta * Math.PI) / 180 + const alphaRad = (viewAlpha * Math.PI) / 180 + + const xb = x * Math.cos(betaRad) - y * Math.sin(betaRad) + const yb = x * Math.sin(betaRad) + y * Math.cos(betaRad) + const yf = yb * Math.cos(alphaRad) + const xf = xb + + return { + anchorX: centerX + xf * projectScale, + anchorY: centerY - yf * projectScale * yCompress, + dirX: xf, + dirY: -yf + } +} + +const buildLabelGraphics = (sortedData, sumValue, labelOptions = {}) => { if (!sortedData.length || !sumValue) { return [] } - const { centerX, centerY, radiusX, radiusY, radialLen, horizontalLen } = LABEL_LAYOUT + const layout = { + ...DEFAULT_LABEL_LAYOUT, + ...(labelOptions.layout || {}) + } + const view = labelOptions.view || {} + const textColor = labelOptions.textColor || layout.textColor + const percentColor = labelOptions.percentColor || layout.percentColor + const lineColor = labelOptions.lineColor || layout.lineColor + const fontSize = labelOptions.fontSize || layout.fontSize + let startValue = 0 const graphics = [] sortedData.forEach((item) => { const endValue = startValue + item.value const midRatio = (startValue + endValue) / 2 / sumValue - const midRadian = midRatio * Math.PI * 2 - const cos = Math.cos(midRadian) - const sin = Math.sin(midRadian) - const onRight = cos >= 0 - - const anchorX = centerX + cos * radiusX - const anchorY = centerY - sin * radiusY - const radialX = cos * radialLen * 3.2 - const radialY = -sin * radialLen * 2.4 - const endX = radialX + (onRight ? horizontalLen * 3.6 : -horizontalLen * 3.6) + const sliceRatio = (endValue - startValue) / sumValue + const { anchorX, anchorY, dirX, dirY } = projectSlicePoint(midRatio, layout, view) + const onRight = dirX >= 0 + const dirLen = Math.hypot(dirX, dirY) || 1 + const normX = dirX / dirLen + const normY = dirY / dirLen + const radialBoost = sliceRatio < 0.05 ? 6 : 0 + const horizontalBoost = sliceRatio < 0.05 ? 8 : 0 + const { radialLen, horizontalLen } = layout + + const radialX = normX * (radialLen + radialBoost) + const radialY = normY * (radialLen + radialBoost) + const endX = radialX + (onRight ? horizontalLen + horizontalBoost : -(horizontalLen + horizontalBoost)) const endY = radialY graphics.push({ @@ -122,23 +256,41 @@ const buildLabelGraphics = (sortedData, sumValue) => { ] }, style: { - stroke: 'rgba(142, 207, 255, 0.55)', + stroke: lineColor, lineWidth: 1, fill: null }, silent: true }, { - type: 'text', - x: endX + (onRight ? 5 : -5), + type: 'group', + x: endX + (onRight ? 6 : -6), y: endY, - style: { - text: `${item.name}\n${item.percent}%`, - fill: '#d8ecff', - font: '13px Microsoft YaHei', - textAlign: onRight ? 'left' : 'right', - textVerticalAlign: 'middle' - }, + children: [ + { + type: 'text', + style: { + text: item.name, + fill: textColor, + font: `${fontSize}px Microsoft YaHei`, + textAlign: onRight ? 'left' : 'right', + textVerticalAlign: 'bottom' + }, + silent: true + }, + { + type: 'text', + y: 16, + style: { + text: `${item.percent}%`, + fill: percentColor, + font: `700 ${fontSize}px Microsoft YaHei`, + textAlign: onRight ? 'left' : 'right', + textVerticalAlign: 'top' + }, + silent: true + } + ], silent: true } ], @@ -190,7 +342,16 @@ export const buildPie3DOption = ({ viewAlpha = 28, viewBeta = 48, viewDistance = 185, - placeholderMode = false + placeholderMode = false, + preserveOrder = false, + labelMode = 'graphic', + autoRotate = false, + autoRotateSpeed = 6, + labelColor, + labelPercentColor, + labelLayout, + labelLineColor, + labelOverlay }) => { const series = [] let sumValue = 0 @@ -204,7 +365,7 @@ export const buildPie3DOption = ({ return null } - const sortedData = placeholderMode + const sortedData = placeholderMode || preserveOrder ? validData : [...validData].sort((a, b) => b.value - a.value) @@ -283,7 +444,8 @@ export const buildPie3DOption = ({ rotateSensitivity: 0, zoomSensitivity: 0, panSensitivity: 0, - autoRotate: false + autoRotate, + autoRotateSpeed }, light: { main: { @@ -297,10 +459,28 @@ export const buildPie3DOption = ({ } } }, - series, + series: [ + ...series, + ...(labelMode === 'overlay' && !placeholderMode + ? [buildOverlayLabelSeries(sortedData, { + nameColor: labelColor, + percentColor: labelPercentColor, + lineColor: labelLineColor, + ...(labelOverlay || {}) + })] + : []) + ], graphic: placeholderMode ? buildCenterGraphic('暂无统计数据') - : buildLabelGraphics(sortedData, sumValue) + : labelMode === 'none' || labelMode === 'overlay' + ? [] + : buildLabelGraphics(sortedData, sumValue, { + textColor: labelColor, + percentColor: labelPercentColor, + lineColor: labelLineColor, + layout: labelLayout, + view: { viewAlpha, viewBeta, viewDistance } + }) } } diff --git a/src/utils/systemConfig.js b/src/utils/systemConfig.js index b13f11e..b04f39b 100644 --- a/src/utils/systemConfig.js +++ b/src/utils/systemConfig.js @@ -1,10 +1,30 @@ const API_URL = '/api/dashboard/system-config' +const DEFAULT_MAP_HUB = { + name: '红原县', + coordinates: [102.568685, 32.826358], + description: '四川省阿坝州红原县,中国重要的牦牛养殖基地', + localOriginKeyword: '红原' +} + +const DEFAULT_MAP_FLOW_LEVELS = { + high: { threshold: 80, color: '#E6A23C', description: '主要流向' }, + medium: { threshold: 40, color: '#409EFF', description: '重要流向' }, + low: { threshold: 0, color: '#67C23A', description: '一般流向' } +} + const DEFAULT_SYSTEM_CONFIG = { title: '/标题.png', - titleBackground: '/images/标题背景.png' + titleBackground: '/images/标题背景.png', + mapHub: DEFAULT_MAP_HUB, + mapFlowLevels: DEFAULT_MAP_FLOW_LEVELS, + mapGeoCoordMap: { + '红原县': [102.568685, 32.826358] + } } +export { DEFAULT_MAP_HUB, DEFAULT_MAP_FLOW_LEVELS } + export async function loadSystemConfig() { try { const response = await fetch(API_URL) diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue index e8c2c03..b732557 100644 --- a/src/views/Dashboard.vue +++ b/src/views/Dashboard.vue @@ -54,7 +54,12 @@
- +