feat: refine V2.0 screen dashboards

V2.0
M先生 1 month ago
parent 0d26772d51
commit 01605804ff
  1. 0
      docs/assets/AlibabaPuHuiTi-14435b65-balabala-1782986963996.woff2
  2. 2
      docs/assets/SmartChartRenderer-1d89a60f-1782986963996.js
  3. 0
      docs/assets/SmartChartRenderer-8fd37aa6-balabala-1782986963996.css
  4. 0
      docs/assets/bg-3acb94e3-balabala-1782986963996.png
  5. 0
      docs/assets/bottom-menu-arrow-big-180b7071-balabala-1782986963996.svg
  6. 0
      docs/assets/bottom-menu-arrow-small-19304179-balabala-1782986963996.svg
  7. 0
      docs/assets/bottom-menu-bg-b465793d-balabala-1782986963996.png
  8. 0
      docs/assets/bottom-menu-btn-ee5c8064-balabala-1782986963996.png
  9. 0
      docs/assets/bottom-menu-btn-hover-df33e514-balabala-1782986963996.png
  10. 0
      docs/assets/chinaBlurLine-1a2cbc0f-balabala-1782986963996.png
  11. 0
      docs/assets/content-bottom-left-arrow-3a8a61f3-balabala-1782986963996.svg
  12. 0
      docs/assets/content-bottom-right-arrow-a3c5270d-balabala-1782986963996.svg
  13. 0
      docs/assets/content-middle-line-162bc0d5-balabala-1782986963996.svg
  14. 0
      docs/assets/focus_bg-b6803e2d-balabala-1782986963996.png
  15. 0
      docs/assets/focus_move_bg-cf41417a-balabala-1782986963996.png
  16. 0
      docs/assets/header-bg-65b38012-balabala-1782986963996.png
  17. 0
      docs/assets/huiguang-3c5dfda5-balabala-1782986963996.png
  18. 0
      docs/assets/icon1-6f7c222f-balabala-1782986963996.png
  19. 0
      docs/assets/icon2-4f77ccec-balabala-1782986963996.png
  20. 1
      docs/assets/index-09f1a743-balabala-1782986963996.css
  21. 2
      docs/assets/index-1782986963996.js
  22. 0
      docs/assets/index-47d6f37a-balabala-1782986963996.css
  23. 6158
      docs/assets/index-638d0d75-1782986963996.js
  24. 2
      docs/assets/index-76001d23-balabala-1782986963996.css
  25. 1
      docs/assets/index-b51b8f1f-balabala-1782921062964.css
  26. 2
      docs/assets/index-bd96e761-1782986963996.js
  27. 0
      docs/assets/left-kuang-5d6bd1ae-balabala-1782986963996.svg
  28. 0
      docs/assets/live-scene-1-786cdd0a-balabala-1782986963996.png
  29. 0
      docs/assets/menu-btn-91a961cf-balabala-1782986963996.png
  30. 0
      docs/assets/menu-btn-hover-62eba0bb-balabala-1782986963996.png
  31. 0
      docs/assets/ocean-bg-8c848b8f-balabala-1782986963996.png
  32. 0
      docs/assets/pathLine2-d4134d4b-balabala-1782986963996.png
  33. 0
      docs/assets/right-kuang-b1937caf-balabala-1782986963996.svg
  34. 0
      docs/assets/ring2-a1b91808-balabala-1782986963996.png
  35. 0
      docs/assets/ring3-fe0f60d7-balabala-1782986963996.png
  36. 0
      docs/assets/ring4-f2e324d6-balabala-1782986963996.png
  37. 0
      docs/assets/rotationBorder1-2ce8e0ef-balabala-1782986963996.png
  38. 0
      docs/assets/rotationBorder2-7496b968-balabala-1782986963996.png
  39. 0
      docs/assets/saoguang-ccf84a88-balabala-1782986963996.svg
  40. 0
      docs/assets/title-zs1-c15a5ee8-balabala-1782986963996.svg
  41. 1
      docs/datas/geojsons/hongyuan-townships-screen.geojson
  42. 446
      docs/datas/yak-industry-chain/yak_industry_points.geojson
  43. 4
      docs/index.html
  44. 1
      public/datas/geojsons/hongyuan-townships-screen.geojson
  45. 446
      public/datas/yak-industry-chain/yak_industry_points.geojson
  46. 12
      src/components/mCard/index.vue
  47. 8
      src/components/mMenu/index.vue
  48. 121
      src/components/mPie/index.vue
  49. 12
      src/config/layers.js
  50. 8
      src/config/mapResources.js
  51. 24
      src/config/townshipBoundaries.js
  52. 2
      src/mini3d/core/Renderer.js
  53. 8
      src/mini3d/utils/Resource.js
  54. 12
      src/mini3d/utils/Sizes.js
  55. 14
      src/mini3d/utils/utils.js
  56. 21
      src/services/openApi.js
  57. 197
      src/views/ecologicalProtection/components/EngineeringAnalysisPanel.vue
  58. 40
      src/views/ecologicalProtection/data.js
  59. 77
      src/views/ecologicalProtection/index.vue
  60. 43
      src/views/forestGrassWet/assets.js
  61. 149
      src/views/forestGrassWet/components/BusinessPanelRenderer.vue
  62. 25
      src/views/forestGrassWet/components/FeatureInfoPanel.vue
  63. 9
      src/views/forestGrassWet/components/LayerPanel.vue
  64. 1
      src/views/forestGrassWet/components/TopicSwitcher.vue
  65. 102
      src/views/forestGrassWet/index.vue
  66. 623
      src/views/forestGrassWet/map.js
  67. 234
      src/views/forestGrassWet/map.vue
  68. 8
      src/views/gdMap/assets.js
  69. 1
      src/views/gdMap/index.vue
  70. 65
      src/views/gdMap/livestockData.js
  71. 66
      src/views/gdMap/map.js
  72. 22
      src/views/gdMap/map.vue
  73. 27
      src/views/grassLivestockBalance/data.js
  74. 30
      src/views/grassLivestockBalance/index.vue
  75. 11
      src/views/home/data.js
  76. 85
      src/views/home/index.vue
  77. 100
      src/views/pastureManagement/index.vue
  78. 64
      src/views/platform/index.vue
  79. 20
      src/views/wisdomManagement/index.vue
  80. 1493
      src/views/yakIndustryChain/data.js
  81. 354
      src/views/yakIndustryChain/index.vue
  82. 68
      src/views/yakManagement/data.js
  83. 735
      src/views/yakManagement/index.vue

@ -1,2 +1,2 @@
var B=Object.defineProperty,O=Object.defineProperties;var D=Object.getOwnPropertyDescriptors;var S=Object.getOwnPropertySymbols;var W=Object.prototype.hasOwnProperty,$=Object.prototype.propertyIsEnumerable;var v=(a,r,o)=>r in a?B(a,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):a[r]=o,m=(a,r)=>{for(var o in r||(r={}))W.call(r,o)&&v(a,o,r[o]);if(S)for(var o of S(r))$.call(r,o)&&v(a,o,r[o]);return a},d=(a,r)=>O(a,D(r));import{L as b,H}from"./index-cd2f2b0a-1782921062964.js";import{e as h,l as F,v as R,o as g,c as p,a as f,t as u,g as L,b as V,u as P,n as j}from"./index-1782921062964.js";const q={class:"smart-chart-header"},G={key:0},E={key:0,class:"smart-chart-source"},J={__name:"SmartChartRenderer",props:{chart:{type:Object,required:!0}},emits:["ready"],setup(a,{emit:r}){const o=r,i=a,l=["#30dcff","#27f0c0","#ffd76c","#7aa7ff","#ff8f6b","#a6f06d"],z=h(()=>{const t=(i.chart.rows||[]).filter(e=>Number.isFinite(Number(e.value)));return i.chart.type==="pie"?N(t):i.chart.type==="line"?w(t):_(t)}),k=h(()=>{const t=i.chart.rows||[];return i.chart.type==="bar"&&x(t)}),y=h(()=>{const t=i.chart.source||{},e=[t.mode,t.table,t.period,t.generatedAt].filter(Boolean);return e.length?e.join(" / "):""});F(()=>{R(()=>{window.requestAnimationFrame(()=>o("ready"))})});function _(t){return x(t)?C(t):{animationDuration:700,grid:{left:52,right:26,top:28,bottom:54},tooltip:s(),xAxis:{type:"category",data:t.map(e=>e.name),axisTick:{show:!1},axisLine:{lineStyle:{color:"rgba(48, 220, 255, 0.28)"}},axisLabel:{color:"rgba(223, 250, 255, 0.76)",fontSize:11,interval:0,rotate:t.length>6?28:0}},yAxis:{type:"value",axisLine:{show:!1},axisTick:{show:!1},splitLine:{lineStyle:{color:"rgba(48, 220, 255, 0.1)",type:"dashed"}},axisLabel:{color:"rgba(223, 250, 255, 0.58)",fontSize:11}},series:[{name:i.chart.title,type:"bar",barWidth:t.length>7?16:22,data:t.map((e,n)=>({value:e.value,name:e.name,itemStyle:{borderRadius:[8,8,0,0],color:new b(0,0,0,1,[{offset:0,color:l[n%l.length]},{offset:1,color:"rgba(48, 220, 255, 0.08)"}])}})),label:{show:!0,position:"top",color:"#ffffff",fontSize:11,formatter:({value:e})=>c(e)}}]}}function C(t){return{animationDuration:700,grid:{left:78,right:72,top:22,bottom:20},tooltip:d(m({},s()),{trigger:"axis",axisPointer:{type:"shadow"}}),xAxis:{type:"value",axisLine:{show:!1},axisTick:{show:!1},splitLine:{lineStyle:{color:"rgba(48, 220, 255, 0.1)",type:"dashed"}},axisLabel:{color:"rgba(223, 250, 255, 0.58)",fontSize:11}},yAxis:{type:"category",inverse:!0,data:t.map(e=>e.name),axisTick:{show:!1},axisLine:{lineStyle:{color:"rgba(48, 220, 255, 0.22)"}},axisLabel:{color:"rgba(223, 250, 255, 0.82)",fontSize:12}},series:[{name:i.chart.title,type:"bar",barWidth:13,data:t.map((e,n)=>({value:e.value,name:e.name,itemStyle:{borderRadius:[0,8,8,0],color:new b(0,0,1,0,[{offset:0,color:"rgba(48, 220, 255, 0.16)"},{offset:1,color:l[n%l.length]}])}})),label:{show:!0,position:"right",color:"#ffffff",fontSize:11,formatter:({value:e})=>c(e)}}]}}function N(t){return{animationDuration:700,tooltip:s(),legend:{orient:"vertical",right:10,top:"middle",itemWidth:10,itemHeight:10,textStyle:{color:"rgba(223, 250, 255, 0.78)",fontSize:11}},series:[{name:i.chart.title,type:"pie",radius:["48%","68%"],center:["35%","52%"],avoidLabelOverlap:!0,label:{color:"#fff",fontSize:11,formatter:`{b}
var B=Object.defineProperty,O=Object.defineProperties;var D=Object.getOwnPropertyDescriptors;var S=Object.getOwnPropertySymbols;var W=Object.prototype.hasOwnProperty,$=Object.prototype.propertyIsEnumerable;var v=(a,r,o)=>r in a?B(a,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):a[r]=o,m=(a,r)=>{for(var o in r||(r={}))W.call(r,o)&&v(a,o,r[o]);if(S)for(var o of S(r))$.call(r,o)&&v(a,o,r[o]);return a},d=(a,r)=>O(a,D(r));import{L as b,H}from"./index-638d0d75-1782986963996.js";import{e as h,l as F,v as R,o as g,c as p,a as f,t as u,g as L,b as V,u as P,n as j}from"./index-1782986963996.js";const q={class:"smart-chart-header"},G={key:0},E={key:0,class:"smart-chart-source"},J={__name:"SmartChartRenderer",props:{chart:{type:Object,required:!0}},emits:["ready"],setup(a,{emit:r}){const o=r,i=a,l=["#30dcff","#27f0c0","#ffd76c","#7aa7ff","#ff8f6b","#a6f06d"],z=h(()=>{const t=(i.chart.rows||[]).filter(e=>Number.isFinite(Number(e.value)));return i.chart.type==="pie"?N(t):i.chart.type==="line"?w(t):_(t)}),k=h(()=>{const t=i.chart.rows||[];return i.chart.type==="bar"&&x(t)}),y=h(()=>{const t=i.chart.source||{},e=[t.mode,t.table,t.period,t.generatedAt].filter(Boolean);return e.length?e.join(" / "):""});F(()=>{R(()=>{window.requestAnimationFrame(()=>o("ready"))})});function _(t){return x(t)?C(t):{animationDuration:700,grid:{left:52,right:26,top:28,bottom:54},tooltip:s(),xAxis:{type:"category",data:t.map(e=>e.name),axisTick:{show:!1},axisLine:{lineStyle:{color:"rgba(48, 220, 255, 0.28)"}},axisLabel:{color:"rgba(223, 250, 255, 0.76)",fontSize:11,interval:0,rotate:t.length>6?28:0}},yAxis:{type:"value",axisLine:{show:!1},axisTick:{show:!1},splitLine:{lineStyle:{color:"rgba(48, 220, 255, 0.1)",type:"dashed"}},axisLabel:{color:"rgba(223, 250, 255, 0.58)",fontSize:11}},series:[{name:i.chart.title,type:"bar",barWidth:t.length>7?16:22,data:t.map((e,n)=>({value:e.value,name:e.name,itemStyle:{borderRadius:[8,8,0,0],color:new b(0,0,0,1,[{offset:0,color:l[n%l.length]},{offset:1,color:"rgba(48, 220, 255, 0.08)"}])}})),label:{show:!0,position:"top",color:"#ffffff",fontSize:11,formatter:({value:e})=>c(e)}}]}}function C(t){return{animationDuration:700,grid:{left:78,right:72,top:22,bottom:20},tooltip:d(m({},s()),{trigger:"axis",axisPointer:{type:"shadow"}}),xAxis:{type:"value",axisLine:{show:!1},axisTick:{show:!1},splitLine:{lineStyle:{color:"rgba(48, 220, 255, 0.1)",type:"dashed"}},axisLabel:{color:"rgba(223, 250, 255, 0.58)",fontSize:11}},yAxis:{type:"category",inverse:!0,data:t.map(e=>e.name),axisTick:{show:!1},axisLine:{lineStyle:{color:"rgba(48, 220, 255, 0.22)"}},axisLabel:{color:"rgba(223, 250, 255, 0.82)",fontSize:12}},series:[{name:i.chart.title,type:"bar",barWidth:13,data:t.map((e,n)=>({value:e.value,name:e.name,itemStyle:{borderRadius:[0,8,8,0],color:new b(0,0,1,0,[{offset:0,color:"rgba(48, 220, 255, 0.16)"},{offset:1,color:l[n%l.length]}])}})),label:{show:!0,position:"right",color:"#ffffff",fontSize:11,formatter:({value:e})=>c(e)}}]}}function N(t){return{animationDuration:700,tooltip:s(),legend:{orient:"vertical",right:10,top:"middle",itemWidth:10,itemHeight:10,textStyle:{color:"rgba(223, 250, 255, 0.78)",fontSize:11}},series:[{name:i.chart.title,type:"pie",radius:["48%","68%"],center:["35%","52%"],avoidLabelOverlap:!0,label:{color:"#fff",fontSize:11,formatter:`{b}
{d}%`},labelLine:{lineStyle:{color:"rgba(223, 250, 255, 0.42)"}},itemStyle:{borderColor:"rgba(3, 18, 31, 0.92)",borderWidth:3},data:t.map((e,n)=>({name:e.name,value:e.value,itemStyle:{color:l[n%l.length]}}))}]}}function w(t){return{animationDuration:760,grid:{left:52,right:28,top:30,bottom:48},tooltip:d(m({},s()),{trigger:"axis",axisPointer:{type:"line",lineStyle:{color:"rgba(255, 215, 108, 0.52)"}}}),xAxis:{type:"category",boundaryGap:!1,data:t.map(e=>e.name),axisTick:{show:!1},axisLine:{lineStyle:{color:"rgba(48, 220, 255, 0.28)"}},axisLabel:{color:"rgba(223, 250, 255, 0.76)",fontSize:11,interval:0}},yAxis:{type:"value",axisLine:{show:!1},axisTick:{show:!1},splitLine:{lineStyle:{color:"rgba(48, 220, 255, 0.1)",type:"dashed"}},axisLabel:{color:"rgba(223, 250, 255, 0.58)",fontSize:11}},series:[{name:i.chart.title,type:"line",smooth:!0,symbol:"circle",symbolSize:8,lineStyle:{width:3,color:"#30dcff"},itemStyle:{color:"#ffd76c",borderColor:"#041522",borderWidth:2},areaStyle:{color:new b(0,0,0,1,[{offset:0,color:"rgba(48, 220, 255, 0.24)"},{offset:1,color:"rgba(48, 220, 255, 0.02)"}])},label:{show:!0,position:"top",color:"#ffffff",fontSize:11,formatter:({value:e})=>c(e)},data:t.map(e=>({value:e.value,name:e.name}))}]}}function s(){return{trigger:"item",backgroundColor:"rgba(2, 13, 22, 0.94)",borderColor:"rgba(48, 220, 255, 0.34)",borderWidth:1,textStyle:{color:"#fff",fontSize:12},formatter:t=>{const e=(i.chart.rows||[]).find(A=>A.name===t.name),n=(e==null?void 0:e.unit)||i.chart.unit||"",T=e!=null&&e.extra?`<br/><span style="color:rgba(223,250,255,.62)">${e.extra}</span>`:"";return`${t.marker}${t.name}: ${c(t.value)}${n}${T}`}}}function x(t=[]){return t.length>6||t.some(e=>String(e.name||"").length>4)}function c(t){const e=Number(t);return Number.isFinite(e)?Number.isInteger(e)?e.toLocaleString("zh-CN"):e.toLocaleString("zh-CN",{maximumFractionDigits:2}):"--"}return(t,e)=>(g(),p("div",{class:j(["smart-chart-card",{"is-horizontal":k.value}])},[f("div",q,[f("div",null,[f("strong",null,u(a.chart.title),1),f("span",null,u(a.chart.subtitle),1)]),a.chart.unit?(g(),p("em",G,u(a.chart.unit),1)):L("",!0)]),V(P(H),{class:"smart-chart",option:z.value,autoresize:""},null,8,["option"]),y.value?(g(),p("div",E,u(y.value),1)):L("",!0)],2))}};export{J as default};

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 93 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
.login-screen[data-v-02c20620]{position:fixed;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;overflow:hidden;background:linear-gradient(180deg,rgba(4,16,38,.1),rgba(2,8,22,.62)),url(./bg-3acb94e3-balabala-1782921062964.png) center/cover no-repeat,#05142f;color:#fff}.login-screen[data-v-02c20620]:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(90deg,rgba(0,0,0,.46),transparent 42%,rgba(0,0,0,.35)),radial-gradient(circle at 52% 46%,rgba(48,220,255,.16),transparent 32%)}.login-shell[data-v-02c20620]{position:relative;z-index:1;width:min(980px,100vw - 48px);display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:56px;align-items:center}.login-brand[data-v-02c20620]{display:flex;flex-direction:column;gap:14px;min-width:0;text-shadow:0 0 20px rgba(48,220,255,.36)}.login-brand span[data-v-02c20620]{font-size:42px;line-height:1.2;font-weight:700}.login-brand small[data-v-02c20620]{color:#c4f3feb8;font-size:13px;letter-spacing:0}.login-panel[data-v-02c20620]{width:360px;padding:30px 28px 28px;border:1px solid rgba(48,220,255,.34);background:rgba(3,14,32,.82);box-shadow:0 24px 70px #00000057,inset 0 0 28px #30dcff14;box-sizing:border-box}.login-panel-title[data-v-02c20620]{margin-bottom:26px;color:#c4f3fe;font-size:22px;font-weight:700;text-align:center}.login-field[data-v-02c20620]{display:block;margin-bottom:18px}.login-field span[data-v-02c20620]{display:block;margin-bottom:8px;color:#c4f3fec7;font-size:13px}.login-field input[data-v-02c20620]{width:100%;height:42px;padding:0 12px;border:1px solid rgba(48,220,255,.26);outline:none;background:rgba(0,10,24,.62);color:#fff;font-size:15px;box-sizing:border-box}.login-field input[data-v-02c20620]:focus{border-color:#30dcffb8;box-shadow:0 0 0 2px #30dcff1f}.login-error[data-v-02c20620]{min-height:20px;margin:-4px 0 12px;color:#ffcf8a;font-size:13px;line-height:20px}.login-submit[data-v-02c20620]{width:100%;height:44px;border:1px solid rgba(48,220,255,.52);background:linear-gradient(90deg,rgba(24,140,196,.92),rgba(37,204,218,.92));color:#fff;cursor:pointer;font-size:16px;font-weight:700}.login-submit[data-v-02c20620]:disabled{cursor:not-allowed;opacity:.68}@media (max-width: 820px){.login-shell[data-v-02c20620]{grid-template-columns:1fr;gap:28px}.login-brand span[data-v-02c20620]{font-size:30px}.login-panel[data-v-02c20620]{width:100%}}
.login-screen[data-v-02c20620]{position:fixed;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;overflow:hidden;background:linear-gradient(180deg,rgba(4,16,38,.1),rgba(2,8,22,.62)),url(./bg-3acb94e3-balabala-1782986963996.png) center/cover no-repeat,#05142f;color:#fff}.login-screen[data-v-02c20620]:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(90deg,rgba(0,0,0,.46),transparent 42%,rgba(0,0,0,.35)),radial-gradient(circle at 52% 46%,rgba(48,220,255,.16),transparent 32%)}.login-shell[data-v-02c20620]{position:relative;z-index:1;width:min(980px,100vw - 48px);display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:56px;align-items:center}.login-brand[data-v-02c20620]{display:flex;flex-direction:column;gap:14px;min-width:0;text-shadow:0 0 20px rgba(48,220,255,.36)}.login-brand span[data-v-02c20620]{font-size:42px;line-height:1.2;font-weight:700}.login-brand small[data-v-02c20620]{color:#c4f3feb8;font-size:13px;letter-spacing:0}.login-panel[data-v-02c20620]{width:360px;padding:30px 28px 28px;border:1px solid rgba(48,220,255,.34);background:rgba(3,14,32,.82);box-shadow:0 24px 70px #00000057,inset 0 0 28px #30dcff14;box-sizing:border-box}.login-panel-title[data-v-02c20620]{margin-bottom:26px;color:#c4f3fe;font-size:22px;font-weight:700;text-align:center}.login-field[data-v-02c20620]{display:block;margin-bottom:18px}.login-field span[data-v-02c20620]{display:block;margin-bottom:8px;color:#c4f3fec7;font-size:13px}.login-field input[data-v-02c20620]{width:100%;height:42px;padding:0 12px;border:1px solid rgba(48,220,255,.26);outline:none;background:rgba(0,10,24,.62);color:#fff;font-size:15px;box-sizing:border-box}.login-field input[data-v-02c20620]:focus{border-color:#30dcffb8;box-shadow:0 0 0 2px #30dcff1f}.login-error[data-v-02c20620]{min-height:20px;margin:-4px 0 12px;color:#ffcf8a;font-size:13px;line-height:20px}.login-submit[data-v-02c20620]{width:100%;height:44px;border:1px solid rgba(48,220,255,.52);background:linear-gradient(90deg,rgba(24,140,196,.92),rgba(37,204,218,.92));color:#fff;cursor:pointer;font-size:16px;font-weight:700}.login-submit[data-v-02c20620]:disabled{cursor:not-allowed;opacity:.68}@media (max-width: 820px){.login-shell[data-v-02c20620]{grid-template-columns:1fr;gap:28px}.login-brand span[data-v-02c20620]{font-size:30px}.login-panel[data-v-02c20620]{width:100%}}

File diff suppressed because one or more lines are too long

@ -1 +1 @@
var m=(v,d,n)=>new Promise((i,a)=>{var u=l=>{try{r(n.next(l))}catch(t){a(t)}},o=l=>{try{r(n.throw(l))}catch(t){a(t)}},r=l=>l.done?i(l.value):Promise.resolve(l.value).then(u,o);r((n=n.apply(v,d)).next())});import{_ as x,P as w,Q as h,e as R,o as _,c as f,a as e,J as S,G as g,H as y,t as b,g as V,d as p,R as k}from"./index-1782921062964.js";const B={class:"login-screen"},D={class:"login-shell"},H={class:"login-field"},M={class:"login-field"},P={key:0,class:"login-error"},U=["disabled"],q={__name:"index",setup(v){const d=w(),n=h(),i=p(""),a=p(""),u=p(!1),o=p(""),r=R(()=>{const t=typeof d.query.redirect=="string"?decodeURIComponent(d.query.redirect):"/platform";return t.startsWith("/login")?"/platform":t||"/platform"});function l(){return m(this,null,function*(){if(o.value="",!i.value||!a.value){o.value="请输入账号和密码。";return}u.value=!0;try{yield k(i.value,a.value),n.replace(r.value)}catch(t){o.value=(t==null?void 0:t.message)||"登录失败,请稍后再试。"}finally{u.value=!1}})}return(t,s)=>(_(),f("div",B,[e("div",D,[s[5]||(s[5]=e("div",{class:"login-brand"},[e("span",null,"红原县数字畜牧业监管平台"),e("small",null,"Hongyuan Digital Animal Husbandry Supervision Platform")],-1)),e("form",{class:"login-panel",onSubmit:S(l,["prevent"])},[s[4]||(s[4]=e("div",{class:"login-panel-title"},"用户登录",-1)),e("label",H,[s[2]||(s[2]=e("span",null,"账号",-1)),g(e("input",{"onUpdate:modelValue":s[0]||(s[0]=c=>i.value=c),autocomplete:"username",autofocus:"",placeholder:"请输入账号"},null,512),[[y,i.value,void 0,{trim:!0}]])]),e("label",M,[s[3]||(s[3]=e("span",null,"密码",-1)),g(e("input",{"onUpdate:modelValue":s[1]||(s[1]=c=>a.value=c),autocomplete:"current-password",placeholder:"请输入密码",type:"password"},null,512),[[y,a.value]])]),o.value?(_(),f("div",P,b(o.value),1)):V("",!0),e("button",{class:"login-submit",disabled:u.value,type:"submit"},b(u.value?"登录中":"登录"),9,U)],32)])]))}},N=x(q,[["__scopeId","data-v-02c20620"]]);export{N as default};
var m=(v,d,n)=>new Promise((i,a)=>{var u=l=>{try{r(n.next(l))}catch(t){a(t)}},o=l=>{try{r(n.throw(l))}catch(t){a(t)}},r=l=>l.done?i(l.value):Promise.resolve(l.value).then(u,o);r((n=n.apply(v,d)).next())});import{_ as x,P as w,Q as h,e as R,o as _,c as f,a as e,J as S,G as g,H as y,t as b,g as V,d as p,R as k}from"./index-1782986963996.js";const B={class:"login-screen"},D={class:"login-shell"},H={class:"login-field"},M={class:"login-field"},P={key:0,class:"login-error"},U=["disabled"],q={__name:"index",setup(v){const d=w(),n=h(),i=p(""),a=p(""),u=p(!1),o=p(""),r=R(()=>{const t=typeof d.query.redirect=="string"?decodeURIComponent(d.query.redirect):"/platform";return t.startsWith("/login")?"/platform":t||"/platform"});function l(){return m(this,null,function*(){if(o.value="",!i.value||!a.value){o.value="请输入账号和密码。";return}u.value=!0;try{yield k(i.value,a.value),n.replace(r.value)}catch(t){o.value=(t==null?void 0:t.message)||"登录失败,请稍后再试。"}finally{u.value=!1}})}return(t,s)=>(_(),f("div",B,[e("div",D,[s[5]||(s[5]=e("div",{class:"login-brand"},[e("span",null,"红原县数字畜牧业监管平台"),e("small",null,"Hongyuan Digital Animal Husbandry Supervision Platform")],-1)),e("form",{class:"login-panel",onSubmit:S(l,["prevent"])},[s[4]||(s[4]=e("div",{class:"login-panel-title"},"用户登录",-1)),e("label",H,[s[2]||(s[2]=e("span",null,"账号",-1)),g(e("input",{"onUpdate:modelValue":s[0]||(s[0]=c=>i.value=c),autocomplete:"username",autofocus:"",placeholder:"请输入账号"},null,512),[[y,i.value,void 0,{trim:!0}]])]),e("label",M,[s[3]||(s[3]=e("span",null,"密码",-1)),g(e("input",{"onUpdate:modelValue":s[1]||(s[1]=c=>a.value=c),autocomplete:"current-password",placeholder:"请输入密码",type:"password"},null,512),[[y,a.value]])]),o.value?(_(),f("div",P,b(o.value),1)):V("",!0),e("button",{class:"login-submit",disabled:u.value,type:"submit"},b(u.value?"登录中":"登录"),9,U)],32)])]))}},N=x(q,[["__scopeId","data-v-02c20620"]]);export{N as default};

File diff suppressed because one or more lines are too long

@ -2552,34 +2552,6 @@
]
}
},
{
"type": "Feature",
"properties": {
"id": "YC-BREEDING-BASE-012",
"name": "江茸绵羊养殖基地",
"stageKey": "scale-breeding",
"stageName": "规模化养殖基地",
"categoryCode": "YC-BREEDING-BASE",
"categoryName": "标准化养殖基地",
"town": "",
"village": "",
"address": "茸日村",
"capacity": 502,
"capacityUnit": "头",
"contactName": "",
"contactPhone": "",
"status": "正常运营",
"locationType": "主体坐标",
"sourceSheet": "标准化养殖基地"
},
"geometry": {
"type": "Point",
"coordinates": [
102.26005599,
32.50027984
]
}
},
{
"type": "Feature",
"properties": {
@ -4260,34 +4232,6 @@
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e01870483927c0025",
"name": "瓦切镇特色蔬菜种植园",
"stageKey": "artificial-grassland",
"stageName": "人工草地",
"categoryCode": "YC-CXPT-FORAGE-BASE",
"categoryName": "cxpt饲草基地",
"town": "瓦切镇",
"village": "",
"address": "四川省阿坝藏族羌族自治州红原县瓦切镇",
"capacity": null,
"capacityUnit": "",
"contactName": "",
"contactPhone": "",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
},
"geometry": {
"type": "Point",
"coordinates": [
102.5959684,
33.08257632
]
}
},
{
"type": "Feature",
"properties": {
@ -4347,336 +4291,280 @@
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e018704831d77001c",
"name": "红原县金针菇特色种植园区",
"stageKey": "artificial-grassland",
"stageName": "人工草地",
"categoryCode": "YC-CXPT-GRASS-SEED-BASE",
"categoryName": "cxpt草种基地",
"town": "邛溪镇",
"id": "YC-FARM-EQUIPMENT-TOWN-001",
"name": "色地镇农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "色地镇",
"village": "",
"address": "红原县邛溪镇",
"capacity": null,
"capacityUnit": "",
"contactName": "",
"contactPhone": "",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "色地镇乡镇中心",
"capacity": 25,
"capacityUnit": "",
"value": 25,
"count": 131983,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.5421184,
32.79970142
103.14126598596769,
33.03468485062284
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e0187048215ac000e",
"name": "刷经寺镇露地蔬菜种植基地",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"town": "刷经寺镇",
"id": "YC-FARM-EQUIPMENT-TOWN-002",
"name": "瓦切镇农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "瓦切镇",
"village": "",
"address": "红原县刷经寺镇",
"capacity": null,
"capacityUnit": "",
"contactName": "文明智",
"contactPhone": "0837-2667185",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "瓦切镇乡镇中心",
"capacity": 24,
"capacityUnit": "",
"value": 24,
"count": 126611,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.620282,
32.037843
102.6838973693402,
33.13732598840634
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e01870482023a000c",
"name": "四川红原遛遛牛食品有限责任公司",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"id": "YC-FARM-EQUIPMENT-TOWN-003",
"name": "安曲镇农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "安曲镇",
"village": "",
"address": "四川省红原县绿色产业园区",
"capacity": "25亩",
"capacityUnit": "",
"contactName": "张自龙",
"contactPhone": "13909047233",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "安曲镇乡镇中心",
"capacity": 20,
"capacityUnit": "",
"value": 20,
"count": 99331,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.324776,
32.689929
102.35372199561037,
32.712944518303445
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e018704826d4c0010",
"name": "植物园",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"id": "YC-FARM-EQUIPMENT-TOWN-004",
"name": "邛溪镇农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "邛溪镇",
"village": "",
"address": "四川省阿坝藏族羌族自治州红原县邛溪镇",
"capacity": null,
"capacityUnit": "",
"contactName": "",
"contactPhone": "13888888888",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "邛溪镇乡镇中心",
"capacity": 18,
"capacityUnit": "",
"value": 18,
"count": 81727,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.551944,
32.780316
102.63776751384222,
32.82512378846329
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e01870481f0a0000a",
"name": "活畜交易市场",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"town": "邛溪镇",
"village": "",
"address": "红原县邛溪镇",
"capacity": "3000亩",
"capacityUnit": "",
"contactName": "向白",
"contactPhone": "18090223335",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
},
"geometry": {
"type": "Point",
"coordinates": [
102.57314,
32.82984
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e01870481f4e9000b",
"name": "物联网大数据监测系统",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"town": "阿木乡",
"id": "YC-FARM-EQUIPMENT-TOWN-005",
"name": "麦洼乡农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "麦洼乡",
"village": "",
"address": "红原县绿色工业园区",
"capacity": "10000",
"capacityUnit": "",
"contactName": "熊先生",
"contactPhone": "13845444444",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "麦洼乡乡镇中心",
"capacity": 16,
"capacityUnit": "套",
"value": 16,
"count": 78489,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.776153,
32.72242
102.93341761730755,
33.11135771891013
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e0187048384dc0024",
"name": "瓦切阳诺合作社",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"town": "瓦切镇",
"village": "",
"address": "四川省阿坝藏族羌族自治州红原县瓦切镇",
"capacity": "3000亩",
"capacityUnit": "",
"contactName": "",
"contactPhone": "",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
},
"geometry": {
"type": "Point",
"coordinates": [
102.63675654,
33.14253159
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e018704811f290000",
"name": "红原县城市污水处理厂",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"town": "邛溪镇",
"id": "YC-FARM-EQUIPMENT-TOWN-006",
"name": "龙日镇农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "龙日镇",
"village": "",
"address": "四川省阿坝藏族羌族自治州红原县邛溪镇",
"capacity": "2.5公顷",
"capacityUnit": "",
"contactName": "",
"contactPhone": "",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "龙日镇乡镇中心",
"capacity": 15,
"capacityUnit": "套",
"value": 15,
"count": 66846,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.559801,
32.816904
102.40058259505227,
32.42604846213644
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e01870481b7180008",
"name": "红原县永源牦牛肉业有限公司",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"id": "YC-FARM-EQUIPMENT-TOWN-007",
"name": "阿木乡农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "阿木乡",
"village": "",
"address": "红原县邛溪镇绿色工业园区",
"capacity": "30亩",
"capacityUnit": "",
"contactName": "张健",
"contactPhone": "13566787888",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "阿木乡乡镇中心",
"capacity": 14,
"capacityUnit": "",
"value": 14,
"count": 64176,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.628332,
32.639351
102.84021326842111,
32.73181473528267
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e01870482067d000d",
"name": "红原县茸日玛绵羊养殖专业合作社",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"town": "江茸乡",
"id": "YC-FARM-EQUIPMENT-TOWN-008",
"name": "查尔玛乡农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "查尔玛乡",
"village": "",
"address": "红原县江茸镇日玛村",
"capacity": "5000亩",
"capacityUnit": "",
"contactName": "旦真则旺",
"contactPhone": "13541576727",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "查尔玛乡乡镇中心",
"capacity": 11,
"capacityUnit": "",
"value": 11,
"count": 47958,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.23279,
32.402614
102.04239391087305,
32.455224568167694
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e01870481aa840007",
"name": "红原牦牛乳业有限责任公司",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"town": "邛溪镇",
"id": "YC-FARM-EQUIPMENT-TOWN-009",
"name": "江茸乡农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "江茸乡",
"village": "",
"address": "四川省红原县邛溪镇",
"capacity": "92490平方米",
"capacityUnit": "",
"contactName": "杨勇",
"contactPhone": "13551770158",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "江茸乡乡镇中心",
"capacity": 9,
"capacityUnit": "",
"value": 9,
"count": 34576,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.747914,
32.802653
102.26744142868846,
32.352994695555495
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e01870481cc520009",
"name": "(无害化)红原县宜霖阳光环保科技有限公司",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"town": "龙日镇",
"id": "YC-FARM-EQUIPMENT-TOWN-010",
"name": "刷经寺镇农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "刷经寺镇",
"village": "",
"address": "红原县邛溪镇绛熙东街",
"capacity": "占地20亩",
"capacityUnit": "",
"contactName": "谢光伍",
"contactPhone": "13609975631",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "刷经寺镇乡镇中心",
"capacity": 8,
"capacityUnit": "",
"value": 8,
"count": 30407,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.352372,
32.538109
102.49877817263959,
32.16081640345188
]
}
}

@ -6,8 +6,8 @@
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>红原县数字畜牧业监管平台</title>
<script type="module" crossorigin src="./assets/index-1782921062964.js"></script>
<link rel="stylesheet" href="./assets/index-47d6f37a-balabala-1782921062964.css">
<script type="module" crossorigin src="./assets/index-1782986963996.js"></script>
<link rel="stylesheet" href="./assets/index-47d6f37a-balabala-1782986963996.css">
</head>
<body>

File diff suppressed because one or more lines are too long

@ -2552,34 +2552,6 @@
]
}
},
{
"type": "Feature",
"properties": {
"id": "YC-BREEDING-BASE-012",
"name": "江茸绵羊养殖基地",
"stageKey": "scale-breeding",
"stageName": "规模化养殖基地",
"categoryCode": "YC-BREEDING-BASE",
"categoryName": "标准化养殖基地",
"town": "",
"village": "",
"address": "茸日村",
"capacity": 502,
"capacityUnit": "头",
"contactName": "",
"contactPhone": "",
"status": "正常运营",
"locationType": "主体坐标",
"sourceSheet": "标准化养殖基地"
},
"geometry": {
"type": "Point",
"coordinates": [
102.26005599,
32.50027984
]
}
},
{
"type": "Feature",
"properties": {
@ -4260,34 +4232,6 @@
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e01870483927c0025",
"name": "瓦切镇特色蔬菜种植园",
"stageKey": "artificial-grassland",
"stageName": "人工草地",
"categoryCode": "YC-CXPT-FORAGE-BASE",
"categoryName": "cxpt饲草基地",
"town": "瓦切镇",
"village": "",
"address": "四川省阿坝藏族羌族自治州红原县瓦切镇",
"capacity": null,
"capacityUnit": "",
"contactName": "",
"contactPhone": "",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
},
"geometry": {
"type": "Point",
"coordinates": [
102.5959684,
33.08257632
]
}
},
{
"type": "Feature",
"properties": {
@ -4347,336 +4291,280 @@
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e018704831d77001c",
"name": "红原县金针菇特色种植园区",
"stageKey": "artificial-grassland",
"stageName": "人工草地",
"categoryCode": "YC-CXPT-GRASS-SEED-BASE",
"categoryName": "cxpt草种基地",
"town": "邛溪镇",
"id": "YC-FARM-EQUIPMENT-TOWN-001",
"name": "色地镇农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "色地镇",
"village": "",
"address": "红原县邛溪镇",
"capacity": null,
"capacityUnit": "",
"contactName": "",
"contactPhone": "",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "色地镇乡镇中心",
"capacity": 25,
"capacityUnit": "",
"value": 25,
"count": 131983,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.5421184,
32.79970142
103.14126598596769,
33.03468485062284
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e0187048215ac000e",
"name": "刷经寺镇露地蔬菜种植基地",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"town": "刷经寺镇",
"id": "YC-FARM-EQUIPMENT-TOWN-002",
"name": "瓦切镇农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "瓦切镇",
"village": "",
"address": "红原县刷经寺镇",
"capacity": null,
"capacityUnit": "",
"contactName": "文明智",
"contactPhone": "0837-2667185",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "瓦切镇乡镇中心",
"capacity": 24,
"capacityUnit": "",
"value": 24,
"count": 126611,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.620282,
32.037843
102.6838973693402,
33.13732598840634
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e01870482023a000c",
"name": "四川红原遛遛牛食品有限责任公司",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"id": "YC-FARM-EQUIPMENT-TOWN-003",
"name": "安曲镇农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "安曲镇",
"village": "",
"address": "四川省红原县绿色产业园区",
"capacity": "25亩",
"capacityUnit": "",
"contactName": "张自龙",
"contactPhone": "13909047233",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "安曲镇乡镇中心",
"capacity": 20,
"capacityUnit": "",
"value": 20,
"count": 99331,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.324776,
32.689929
102.35372199561037,
32.712944518303445
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e018704826d4c0010",
"name": "植物园",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"id": "YC-FARM-EQUIPMENT-TOWN-004",
"name": "邛溪镇农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "邛溪镇",
"village": "",
"address": "四川省阿坝藏族羌族自治州红原县邛溪镇",
"capacity": null,
"capacityUnit": "",
"contactName": "",
"contactPhone": "13888888888",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "邛溪镇乡镇中心",
"capacity": 18,
"capacityUnit": "",
"value": 18,
"count": 81727,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.551944,
32.780316
102.63776751384222,
32.82512378846329
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e01870481f0a0000a",
"name": "活畜交易市场",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"town": "邛溪镇",
"village": "",
"address": "红原县邛溪镇",
"capacity": "3000亩",
"capacityUnit": "",
"contactName": "向白",
"contactPhone": "18090223335",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
},
"geometry": {
"type": "Point",
"coordinates": [
102.57314,
32.82984
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e01870481f4e9000b",
"name": "物联网大数据监测系统",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"town": "阿木乡",
"id": "YC-FARM-EQUIPMENT-TOWN-005",
"name": "麦洼乡农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "麦洼乡",
"village": "",
"address": "红原县绿色工业园区",
"capacity": "10000",
"capacityUnit": "",
"contactName": "熊先生",
"contactPhone": "13845444444",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "麦洼乡乡镇中心",
"capacity": 16,
"capacityUnit": "套",
"value": 16,
"count": 78489,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.776153,
32.72242
102.93341761730755,
33.11135771891013
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e0187048384dc0024",
"name": "瓦切阳诺合作社",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"town": "瓦切镇",
"village": "",
"address": "四川省阿坝藏族羌族自治州红原县瓦切镇",
"capacity": "3000亩",
"capacityUnit": "",
"contactName": "",
"contactPhone": "",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
},
"geometry": {
"type": "Point",
"coordinates": [
102.63675654,
33.14253159
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e018704811f290000",
"name": "红原县城市污水处理厂",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"town": "邛溪镇",
"id": "YC-FARM-EQUIPMENT-TOWN-006",
"name": "龙日镇农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "龙日镇",
"village": "",
"address": "四川省阿坝藏族羌族自治州红原县邛溪镇",
"capacity": "2.5公顷",
"capacityUnit": "",
"contactName": "",
"contactPhone": "",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "龙日镇乡镇中心",
"capacity": 15,
"capacityUnit": "套",
"value": 15,
"count": 66846,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.559801,
32.816904
102.40058259505227,
32.42604846213644
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e01870481b7180008",
"name": "红原县永源牦牛肉业有限公司",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"id": "YC-FARM-EQUIPMENT-TOWN-007",
"name": "阿木乡农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "阿木乡",
"village": "",
"address": "红原县邛溪镇绿色工业园区",
"capacity": "30亩",
"capacityUnit": "",
"contactName": "张健",
"contactPhone": "13566787888",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "阿木乡乡镇中心",
"capacity": 14,
"capacityUnit": "",
"value": 14,
"count": 64176,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.628332,
32.639351
102.84021326842111,
32.73181473528267
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e01870482067d000d",
"name": "红原县茸日玛绵羊养殖专业合作社",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"town": "江茸乡",
"id": "YC-FARM-EQUIPMENT-TOWN-008",
"name": "查尔玛乡农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "查尔玛乡",
"village": "",
"address": "红原县江茸镇日玛村",
"capacity": "5000亩",
"capacityUnit": "",
"contactName": "旦真则旺",
"contactPhone": "13541576727",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "查尔玛乡乡镇中心",
"capacity": 11,
"capacityUnit": "",
"value": 11,
"count": 47958,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.23279,
32.402614
102.04239391087305,
32.455224568167694
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e01870481aa840007",
"name": "红原牦牛乳业有限责任公司",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"town": "邛溪镇",
"id": "YC-FARM-EQUIPMENT-TOWN-009",
"name": "江茸乡农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "江茸乡",
"village": "",
"address": "四川省红原县邛溪镇",
"capacity": "92490平方米",
"capacityUnit": "",
"contactName": "杨勇",
"contactPhone": "13551770158",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "江茸乡乡镇中心",
"capacity": 9,
"capacityUnit": "",
"value": 9,
"count": 34576,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.747914,
32.802653
102.26744142868846,
32.352994695555495
]
}
},
{
"type": "Feature",
"properties": {
"id": "4028c4f88704809e01870481cc520009",
"name": "(无害化)红原县宜霖阳光环保科技有限公司",
"stageKey": "social-service",
"stageName": "社会化服务",
"categoryCode": "YC-CXPT-OTHER-BASE",
"categoryName": "cxpt其他基地",
"town": "龙日镇",
"id": "YC-FARM-EQUIPMENT-TOWN-010",
"name": "刷经寺镇农机装备监测点",
"stageKey": "farm-equipment",
"stageName": "农机装备",
"categoryCode": "YC-FARM-EQUIPMENT",
"categoryName": "农机装备监测点",
"town": "刷经寺镇",
"village": "",
"address": "红原县邛溪镇绛熙东街",
"capacity": "占地20亩",
"capacityUnit": "",
"contactName": "谢光伍",
"contactPhone": "13609975631",
"status": "",
"locationType": "主体坐标",
"sourceSheet": "t_base_land"
"address": "刷经寺镇乡镇中心",
"capacity": 8,
"capacityUnit": "",
"value": 8,
"count": 30407,
"status": "监测样本",
"locationType": "乡镇中心点",
"sourceSheet": "农机装备/识别设备分布"
},
"geometry": {
"type": "Point",
"coordinates": [
102.352372,
32.538109
102.49877817263959,
32.16081640345188
]
}
}

@ -415,16 +415,23 @@ onMounted(() => {
<style lang="scss">
.m-card {
position: relative;
overflow: visible;
&-hd {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 36px;
z-index: 2;
pointer-events: none;
&-bg {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 36px;
}
&-zs1 {
position: absolute;
@ -436,6 +443,8 @@ onMounted(() => {
&-title {
position: absolute;
left: 22px;
right: 128px;
top: 0;
color: #fff;
font-size: 16px;
font-weight: 400;
@ -444,6 +453,9 @@ onMounted(() => {
line-height: 32px;
font-family: "PingFangSc";
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background: -webkit-linear-gradient(rgba(219, 249, 255, 1), rgba(169, 240, 255, 1));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;

@ -15,6 +15,7 @@ const props = defineProps({
const activeIndex = ref(props.defaultActive);
//
const updateActive = (index) => {
if (index === activeIndex.value) return;
activeIndex.value = index;
emit("select", index);
};
@ -41,17 +42,18 @@ watch(
align-items: center;
pointer-events: all;
justify-content: center;
min-width: 100px;
min-width: 112px;
width: auto;
height: 32px;
box-sizing: border-box;
padding: 0 14px;
padding: 0 20px;
background-image: url("~@/assets/images/menu-btn.png");
background-size: 100% 100%;
color: rgba(255, 255, 255, 0.6);
font-size: 15px;
margin: 0 8px;
margin: 0 6px;
white-space: nowrap;
line-height: 32px;
cursor: pointer;

@ -55,6 +55,12 @@ export default {
this.controls = null
this.axes = null
this.pieGroup = new THREE.Group()
this.planeTweens = []
this.resizeHandler = null
this.visibilityObserver = null
this.isVisible = true
this.isDestroyed = false
this.isLooping = false
return {
width: 300,
height: 200,
@ -64,13 +70,15 @@ export default {
}
},
mounted() {
this.width = this.$refs["pieDom"].offsetWidth
this.height = this.$refs["pieDom"].offsetHeight
this.count = this.data.map((item) => item.value).reduce((prev, current) => prev + current, 0)
this.isVisible = this.isElementVisible(this.$el)
this.updateSize()
this.count = this.data.map((item) => Number(item.value) || 0).reduce((prev, current) => prev + current, 0)
this.init()
this.setupVisibilityObserver()
this.resizeHandler = () => this.resize()
window.addEventListener("resize", this.resizeHandler)
},
beforeUnmount() {
clearInterval(this.timer)
this.destroy()
},
methods: {
@ -101,7 +109,7 @@ export default {
color: "#00ffff",
})
this.createPie()
this.loop()
if (this.isVisible) this.start()
},
createPlane(opt) {
let defaultOpt = {
@ -129,12 +137,13 @@ export default {
mesh.position.copy(options.position)
mesh.rotation.x = (-1 * Math.PI) / 2
if (options.animate) {
gsap.to(mesh.rotation, {
const tween = gsap.to(mesh.rotation, {
z: 2 * Math.PI,
repeat: -1,
ease: "none",
duration: 3,
})
this.planeTweens.push(tween)
}
this.scene.add(mesh)
},
@ -144,6 +153,7 @@ export default {
return texture
},
createPie() {
if (!this.data.length || this.count <= 0) return
let startAngle = 0
let endAngle = 0
@ -165,11 +175,7 @@ export default {
this.pieGroup.add(ring)
}
this.scene.add(this.pieGroup)
this.chooseRing(this.activeIndex, true)
this.timer = setInterval(() => {
this.loopChange()
}, this.delay)
if (this.pieGroup.children.length) this.chooseRing(this.activeIndex, true)
},
loopChange() {
@ -182,14 +188,18 @@ export default {
this.chooseRing(index)
},
chooseRing(activeIndex = 0, isFirst = false) {
if (!this.pieGroup.children.length) return
let prevIndex = activeIndex - 1 < 0 ? this.data.length - 1 : activeIndex - 1
let prevMesh = this.pieGroup.children[prevIndex]
this.prevMesh = prevMesh
this.activeIndex = activeIndex
let chooseMesh = this.pieGroup.children[activeIndex]
if (!chooseMesh) return
if (!isFirst) {
gsap.to(prevMesh.scale, { z: 1 })
gsap.to(prevMesh.material, { opacity: this.opacity })
if (prevMesh) {
gsap.to(prevMesh.scale, { z: 1 })
gsap.to(prevMesh.material, { opacity: this.opacity })
}
}
gsap.to(chooseMesh.scale, { z: 2 })
gsap.to(chooseMesh.material, { opacity: 0.8 })
@ -250,7 +260,7 @@ export default {
antialias: true,
alpha: true,
})
this.renderer.setPixelRatio(window.devicePixelRatio)
this.renderer.setPixelRatio(this.getPixelRatio())
this.renderer.setSize(this.width, this.height)
this.$refs["pieDom"].appendChild(this.renderer.domElement)
},
@ -284,6 +294,8 @@ export default {
},
loop() {
if (!this.renderer || this.isLooping) return
this.isLooping = true
this.renderer.setAnimationLoop(() => {
this.renderer.render(this.scene, this.camera)
this.controls && this.controls.update()
@ -291,20 +303,73 @@ export default {
})
},
start() {
if (this.isDestroyed || !this.renderer || !this.data.length) return
this.isVisible = true
this.loop()
this.controls.enabled = true
this.timer = setInterval(() => {
this.planeTweens.forEach((tween) => tween?.resume?.())
if (this.controls) this.controls.enabled = true
this.startTimer()
},
startTimer() {
this.stopTimer()
if (this.data.length < 2) return
this.timer = window.setInterval(() => {
this.loopChange()
}, this.delay)
},
stop() {
clearInterval(this.timer)
this.stopTimer()
if (this.controls) {
this.controls.enabled = false
}
if (this.renderer) {
this.renderer.setAnimationLoop(null)
}
this.planeTweens.forEach((tween) => tween?.pause?.())
this.isLooping = false
},
stopTimer() {
if (!this.timer) return
window.clearInterval(this.timer)
this.timer = null
},
setupVisibilityObserver() {
const wrap = this.$el
if (!wrap || typeof window.IntersectionObserver !== "function") return
this.visibilityObserver = new IntersectionObserver((entries) => {
const entry = entries[0]
const nextVisible = Boolean(entry?.isIntersecting) && this.isElementVisible(wrap)
if (nextVisible === this.isVisible) return
this.isVisible = nextVisible
if (nextVisible) {
this.resize()
this.start()
} else {
this.stop()
}
})
this.visibilityObserver.observe(wrap)
this.isVisible = this.isElementVisible(wrap)
if (!this.isVisible) this.stop()
},
isElementVisible(element) {
if (!element || typeof window.getComputedStyle !== "function") return true
let current = element
while (current && current !== document.body) {
const style = window.getComputedStyle(current)
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return false
current = current.parentElement
}
const rect = element.getBoundingClientRect()
return rect.width > 0 && rect.height > 0
},
getPixelRatio() {
return Math.min(window.devicePixelRatio || 1, 1.5)
},
updateSize() {
const target = this.$refs["pieDom"]
this.width = target?.offsetWidth || this.$el?.offsetWidth || 300
this.height = target?.offsetHeight || this.$el?.offsetHeight || 200
},
//
getScene() {
@ -314,29 +379,35 @@ export default {
return this.renderer
},
resize() {
this.width = this.$refs["pieDom"].offsetWidth
this.height = this.$refs["pieDom"].offsetHeight
if (!this.camera || !this.renderer) return
this.updateSize()
let aspect = this.width / this.height
this.camera.aspect = aspect
this.camera.updateProjectionMatrix()
this.renderer.setSize(this.width, this.height)
this.renderer.setPixelRatio(window.devicePixelRatio)
this.renderer.setPixelRatio(this.getPixelRatio())
},
destroy() {
this.isDestroyed = true
if (this.prevMesh) {
gsap.set(this.prevMesh.scale, { z: 1 })
gsap.set(this.prevMesh.material, { opacity: this.opacity })
}
this.stop()
window.removeEventListener("resize", () => {
this.resize()
})
this.visibilityObserver?.disconnect?.()
this.visibilityObserver = null
if (this.resizeHandler) {
window.removeEventListener("resize", this.resizeHandler)
this.resizeHandler = null
}
if (this.renderer) {
this.planeTweens.forEach((tween) => tween?.kill?.())
this.planeTweens = []
emptyObject(this.pieGroup)
this.renderer.dispose()
this.renderer.forceContextLoss()
this.controls.dispose()
this.$refs["pieDom"].innerHTML = ""
this.controls?.dispose?.()
if (this.$refs["pieDom"]) this.$refs["pieDom"].innerHTML = ""
this.scene = null
this.camera = null
this.renderer = null

@ -12,7 +12,7 @@ export const WMS_IMAGE_SIZE = {
height: 2400,
}
export const SATELLITE_TEXTURE_ZOOM = 12
export const SATELLITE_TEXTURE_ZOOM = 9
function escapeCqlLiteral(value) {
return String(value).replace(/'/g, "''")
@ -323,6 +323,7 @@ export const ecologicalProtectionLayers = {
sourceType: "wkt-api",
apiUrl: "/openApi/gjgy/gjgy_area",
fallbackPath: "datas/ecological-protection/national_park_wkt.json",
clipToHongyuanBoundary: true,
opacity: 0.9,
clipBounds: {
west: 101.852492,
@ -524,6 +525,8 @@ export const yakIndustryChainLayers = {
sourceType: "geojson",
dataPath: "datas/yak-industry-chain/yak_industry_points.geojson",
opacity: 0.96,
pointLabelLimit: 12,
showPointLabels: false,
mapView: {
targetLngLat: [102.48, 32.69],
distanceScale: 0.82,
@ -566,6 +569,13 @@ export const yakIndustryChainLayers = {
strokeColor: "#FFD0A8",
match: { field: "stageKey", value: "slaughter-processing" },
},
{
key: "yak-industry-farm-equipment",
name: "农机装备",
fillColor: "#30DCFF",
strokeColor: "#B8F7FF",
match: { field: "stageKey", value: "farm-equipment" },
},
{
key: "yak-industry-social-service",
name: "社会化服务",

@ -0,0 +1,8 @@
export const HONGYUAN_TOWNSHIP_GEOJSON_PATH = "datas/geojsons/hongyuan-townships.geojson"
export const HONGYUAN_TOWNSHIP_SCREEN_GEOJSON_PATH = "datas/geojsons/hongyuan-townships-screen.geojson"
export const HONGYUAN_COUNTY_OUTLINE_PATH = "assets/json/红原县-轮廓.json"
export const CHINA_GEOJSON_PATH = "assets/json/中华人民共和国.json"
export function getPublicMapResourcePath(path) {
return `${import.meta.env.BASE_URL}${path}`
}

@ -1,4 +1,7 @@
import hongyuanTownshipsRaw from "/public/datas/geojsons/hongyuan-townships.geojson?raw"
import { HONGYUAN_TOWNSHIP_GEOJSON_PATH } from "@/config/mapResources"
export const hongyuanTownshipGeoJsonPath = HONGYUAN_TOWNSHIP_GEOJSON_PATH
const townshipEnNames = {
邛溪镇: "qiongxi",
@ -57,6 +60,14 @@ const townshipsByCode = hongyuanTownshipFeatures.reduce((result, feature) => {
return result
}, {})
export const hongyuanTownshipCodeByName = hongyuanTownshipFeatures.reduce((result, feature) => {
const properties = feature.properties || {}
const name = normalizeHongyuanTownName(properties.name)
const code = String(properties.code || "").replace(/\D/g, "")
if (name && code) result[name] = code
return result
}, {})
export const hongyuanTownshipLabelPoints = hongyuanTownshipFeatures.map((feature) => {
const properties = feature.properties || {}
const name = normalizeHongyuanTownName(properties.name)
@ -79,6 +90,8 @@ export const hongyuanTownshipRankingPoints = hongyuanTownshipLabelPoints
}))
.sort((a, b) => b.value - a.value)
export const hongyuanTownshipDisplayOrder = hongyuanTownshipLabelPoints.map((item) => item.name)
export function normalizeHongyuanTownName(value) {
const text = String(value || "").trim()
if (!text || text === "--") return ""
@ -90,3 +103,14 @@ export function getHongyuanTownshipNameByCode(value) {
if (!normalizedCode) return ""
return townshipsByCode[normalizedCode] || townshipsByCode[normalizedCode.slice(0, 12)] || townshipsByCode[normalizedCode.slice(0, 9)] || ""
}
export function getHongyuanTownshipCodeByName(value) {
const name = normalizeHongyuanTownName(value)
if (!name) return ""
return hongyuanTownshipCodeByName[name] || ""
}
export function isHongyuanTownshipName(value) {
const name = normalizeHongyuanTownName(value)
return Boolean(name && hongyuanTownshipLabelPoints.some((item) => item.name === name))
}

@ -12,7 +12,7 @@ export class Renderer {
setInstance() {
this.instance = new WebGLRenderer({
alpha: false,
antialias: true,
antialias: false,
canvas: this.canvas,
})
this.instance.setSize(this.sizes.width, this.sizes.height)

@ -20,9 +20,11 @@ let ResourceType = {
}
const types = Object.values(ResourceType)
export class Resource extends EventEmitter {
constructor({ dracoPath } = {}) {
constructor({ dracoPath, cloneTextures = false } = {}) {
super()
this.dracoPath = dracoPath || "./draco/gltf/"
this.cloneTextures = cloneTextures
this.loaded = false
this.itemsLoaded = 0
this.itemsTotal = 0
this.assets = []
@ -104,6 +106,7 @@ export class Resource extends EventEmitter {
Promise.all(promise)
.then((res) => {
this.assets = res
this.loaded = true
this.emit("onLoad")
resolve(res)
})
@ -139,6 +142,9 @@ export class Resource extends EventEmitter {
if (!current) {
throw new Error(`资源${name}不存在`)
}
if (this.cloneTextures && current.type === ResourceType.TextureLoader && current.data?.isTexture) {
return current.data.clone()
}
return current.data
}
destroy() {

@ -3,20 +3,22 @@ export class Sizes extends EventEmitter {
constructor({ canvas }) {
super();
this.canvas = canvas;
this.pixelRatio = 2;
this.init();
window.addEventListener("resize", () => {
this.maxPixelRatio = 1;
this.handleResize = () => {
this.init();
this.emit("resize");
});
};
this.init();
window.addEventListener("resize", this.handleResize);
}
init() {
const target = this.canvas?.parentNode || this.canvas || document.documentElement;
this.width = target.offsetWidth || window.innerWidth || 1;
this.height = target.offsetHeight || window.innerHeight || 1;
this.pixelRatio = this.pixelRatio || Math.min(window.devicePixelRatio, 2);
this.pixelRatio = Math.min(window.devicePixelRatio || 1, this.maxPixelRatio);
}
destroy() {
window.removeEventListener("resize", this.handleResize);
this.off("resize");
}
}

@ -60,17 +60,21 @@ export function getBoundBox(group) {
}
export const transfromMapGeoJSON = (data) => {
console.log(data);
let worldData = JSON.parse(data);
let features = worldData.features;
return normalizeMapGeoJSON(typeof data === "string" ? JSON.parse(data) : data);
};
function normalizeMapGeoJSON(worldData) {
let features = worldData?.features || [];
for (let i = 0; i < features.length; i++) {
const element = features[i];
if (["Polygon"].includes(element.geometry.type)) {
const coordinates = element.geometry?.coordinates;
const isRawPolygon = Array.isArray(coordinates) && typeof coordinates?.[0]?.[0]?.[0] === "number";
if (element.geometry?.type === "Polygon" && isRawPolygon) {
element.geometry.coordinates = [element.geometry.coordinates];
}
}
return worldData;
};
}
export const transformGeoRoad = (roadData) => {
let features = roadData.features;

@ -1,21 +1,10 @@
import { normalizeResponseData, requestJson } from "./request"
import { getWfsFeatureProperties } from "./geoserver"
import { getLayerByKey as getLayerByKeyForRanking } from "@/config/layers"
import { getHongyuanTownshipNameByCode, hongyuanTownshipDisplayOrder } from "@/config/townshipBoundaries"
const OPEN_API_BASE = import.meta.env.VITE_OPEN_API_BASE || "/openApi"
const HONGYUAN_TOWN_CODE_NAMES = {
"513233100": "邛溪镇",
"513233101": "刷经寺镇",
"513233103": "安曲镇",
"513233105": "龙日镇",
"513233202": "江茸乡",
"513233203": "查尔玛乡",
"513233204": "瓦切镇",
"513233205": "阿木乡",
"513233207": "麦洼乡",
"513233208": "色地镇",
}
const HONGYUAN_TOWN_DISPLAY_ORDER = Object.values(HONGYUAN_TOWN_CODE_NAMES).map(normalizeTownName)
const HONGYUAN_TOWN_DISPLAY_ORDER = hongyuanTownshipDisplayOrder.map(normalizeTownName)
function joinBase(path) {
if (/^https?:\/\//.test(path)) return path
@ -404,7 +393,7 @@ async function getWfsTownAreaRanking({ topicKey, layerKey, layerName, title, cql
;(data?.features || []).forEach((feature) => {
const properties = feature.properties || {}
const townCode = String(properties.xiang || "")
const name = HONGYUAN_TOWN_CODE_NAMES[townCode]
const name = getHongyuanTownshipNameByCode(townCode)
const acreage = toNumber(properties.acreage)
if (!name || acreage === undefined) return
const current = grouped.get(name) || {
@ -466,7 +455,7 @@ function resolveTownName(properties = {}, townCodeKeys = [], townNameKeys = [])
.map((key) => pickValue(properties, [key]))
.find((value) => value !== undefined)
const normalizedCode = String(townCode || "").slice(0, 9)
return HONGYUAN_TOWN_CODE_NAMES[normalizedCode] || ""
return getHongyuanTownshipNameByCode(normalizedCode)
}
async function getLegendWfsAreaRanking(topicKey, layerKey, layer, legend) {
@ -1196,7 +1185,7 @@ async function getGrasslandDegradationStats() {
levelGrouped.set(name, (levelGrouped.get(name) || 0) + areaWanMu)
if (name === "未退化") return
const townCode = String(properties.area_code || "").slice(0, 9)
const townName = HONGYUAN_TOWN_CODE_NAMES[townCode]
const townName = getHongyuanTownshipNameByCode(townCode)
if (!townName) return
townGrouped.set(townName, (townGrouped.get(townName) || 0) + areaWanMu)
})

@ -638,41 +638,71 @@ defineExpose({
left: 32px;
top: 188px;
bottom: 132px;
width: 398px;
width: 430px;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 12px;
gap: 14px;
pointer-events: auto;
}
.analysis-card {
position: relative;
overflow: hidden;
min-height: 0;
padding: 12px 14px;
border: 1px solid rgba(48, 220, 255, 0.28);
padding: 14px 16px;
border: 1px solid rgba(48, 220, 255, 0.32);
background:
linear-gradient(135deg, rgba(8, 37, 54, 0.88), rgba(5, 26, 39, 0.66)),
radial-gradient(circle at 18% 0, rgba(36, 246, 196, 0.16), transparent 42%);
box-shadow: inset 0 0 22px rgba(48, 220, 255, 0.12), 0 12px 28px rgba(0, 0, 0, 0.28);
linear-gradient(135deg, rgba(7, 34, 50, 0.9), rgba(4, 20, 31, 0.76)),
radial-gradient(circle at 20% 0, rgba(36, 246, 196, 0.16), transparent 42%);
box-shadow:
inset 0 1px 0 rgba(180, 252, 255, 0.16),
inset 0 0 26px rgba(48, 220, 255, 0.12),
0 16px 30px rgba(0, 0, 0, 0.32);
box-sizing: border-box;
&::before {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, #24f6c4, rgba(48, 220, 255, 0.18), transparent);
opacity: 0.8;
}
}
.analysis-card-head {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
height: 24px;
height: 26px;
color: #ffffff;
span {
font-size: 15px;
font-weight: 600;
padding-left: 10px;
font-size: 16px;
font-weight: 800;
letter-spacing: 0;
&::before {
content: "";
position: absolute;
left: 0;
top: 7px;
width: 3px;
height: 14px;
background: #24f6c4;
box-shadow: 0 0 10px rgba(36, 246, 196, 0.8);
}
}
strong {
color: #24f6c4;
font-size: 12px;
font-weight: 500;
font-size: 13px;
font-weight: 700;
}
}
@ -702,16 +732,28 @@ defineExpose({
align-content: center;
align-items: center;
min-width: 0;
height: 62px;
padding: 0 16px;
border: 1px solid rgba(48, 220, 255, 0.3);
height: 58px;
padding: 0 18px;
border: 1px solid rgba(48, 220, 255, 0.34);
background:
linear-gradient(90deg, rgba(48, 220, 255, 0.18), rgba(48, 220, 255, 0.05)),
rgba(4, 23, 34, 0.48);
linear-gradient(90deg, rgba(48, 220, 255, 0.18), rgba(48, 220, 255, 0.04)),
rgba(4, 23, 34, 0.62);
color: #ffffff;
text-align: left;
cursor: pointer;
box-sizing: border-box;
box-shadow: inset 0 0 18px rgba(48, 220, 255, 0.08);
&::after {
content: "";
position: absolute;
right: 9px;
top: 9px;
width: 18px;
height: 18px;
border-top: 1px solid rgba(36, 246, 196, 0.45);
border-right: 1px solid rgba(36, 246, 196, 0.45);
}
span {
font-size: 16px;
@ -757,10 +799,12 @@ defineExpose({
display: grid;
align-items: center;
gap: 8px;
margin-top: 12px;
padding: 0 10px;
border: 1px solid rgba(36, 246, 196, 0.28);
background: rgba(36, 246, 196, 0.09);
margin-top: 14px;
padding: 0 12px;
border: 1px solid rgba(36, 246, 196, 0.34);
background:
linear-gradient(90deg, rgba(36, 246, 196, 0.13), rgba(48, 220, 255, 0.06)),
rgba(4, 23, 34, 0.58);
box-sizing: border-box;
}
@ -779,8 +823,8 @@ defineExpose({
}
.analysis-boundary-bar {
grid-template-columns: 90px minmax(0, 1fr) 82px;
height: 38px;
grid-template-columns: 92px minmax(0, 1fr) 86px;
height: 42px;
span {
color: #24f6c4;
@ -799,11 +843,12 @@ defineExpose({
}
button {
height: 28px;
border: 1px solid rgba(255, 225, 90, 0.42);
background: rgba(255, 225, 90, 0.08);
height: 30px;
border: 1px solid rgba(255, 225, 90, 0.5);
background: rgba(255, 225, 90, 0.1);
color: #ffe15a;
font-size: 12px;
font-size: 13px;
font-weight: 700;
cursor: pointer;
}
}
@ -874,8 +919,8 @@ defineExpose({
.analysis-result {
display: grid;
grid-template-rows: 24px auto minmax(0, 1fr);
gap: 10px;
grid-template-rows: 26px auto minmax(0, 1fr);
gap: 12px;
min-height: 0;
}
@ -895,10 +940,12 @@ defineExpose({
.analysis-conclusion {
display: grid;
gap: 7px;
padding: 9px 10px;
border: 1px solid rgba(48, 220, 255, 0.18);
background: rgba(4, 23, 34, 0.38);
gap: 8px;
padding: 12px;
border: 1px solid rgba(48, 220, 255, 0.22);
background:
linear-gradient(135deg, rgba(48, 220, 255, 0.09), rgba(4, 23, 34, 0.48)),
rgba(4, 23, 34, 0.5);
box-sizing: border-box;
&.is-hit {
@ -920,12 +967,13 @@ defineExpose({
small {
margin: 0;
color: rgba(232, 252, 255, 0.76);
font-size: 11px;
line-height: 17px;
font-size: 12px;
line-height: 20px;
}
small {
color: rgba(255, 225, 90, 0.86);
font-weight: 600;
}
}
@ -950,18 +998,18 @@ defineExpose({
.analysis-conclusion-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
gap: 6px;
span {
max-width: 100%;
height: 21px;
padding: 0 7px;
height: 24px;
padding: 0 9px;
overflow: hidden;
border: 1px solid rgba(48, 220, 255, 0.14);
background: rgba(48, 220, 255, 0.06);
border: 1px solid rgba(48, 220, 255, 0.2);
background: rgba(48, 220, 255, 0.08);
color: rgba(232, 252, 255, 0.8);
font-size: 10px;
line-height: 19px;
font-size: 11px;
line-height: 22px;
text-overflow: ellipsis;
white-space: nowrap;
box-sizing: border-box;
@ -971,25 +1019,44 @@ defineExpose({
.analysis-project-list {
min-height: 0;
overflow: auto;
padding-right: 3px;
padding-right: 6px;
scrollbar-color: rgba(48, 220, 255, 0.58) rgba(48, 220, 255, 0.1);
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: rgba(48, 220, 255, 0.08);
}
&::-webkit-scrollbar-thumb {
border-radius: 0;
background: linear-gradient(180deg, rgba(36, 246, 196, 0.9), rgba(48, 220, 255, 0.42));
}
}
.analysis-project-row {
display: grid;
grid-template-columns: minmax(0, 1fr);
align-items: start;
gap: 5px;
gap: 7px;
width: 100%;
min-height: 72px;
margin-bottom: 6px;
padding: 8px;
border: 1px solid rgba(48, 220, 255, 0.12);
background: rgba(48, 220, 255, 0.05);
min-height: 86px;
margin-bottom: 8px;
padding: 10px 12px;
border: 1px solid rgba(48, 220, 255, 0.18);
border-left-color: rgba(36, 246, 196, 0.56);
background:
linear-gradient(90deg, rgba(36, 246, 196, 0.1), rgba(48, 220, 255, 0.04)),
rgba(4, 23, 34, 0.54);
color: #ffffff;
font-size: 11px;
font-size: 12px;
text-align: left;
cursor: pointer;
box-sizing: border-box;
box-shadow: inset 0 0 16px rgba(48, 220, 255, 0.05);
.analysis-project-name,
b {
@ -1001,13 +1068,14 @@ defineExpose({
.analysis-project-name {
display: block;
color: #ffffff;
font-size: 11px;
line-height: 16px;
font-size: 13px;
font-weight: 700;
line-height: 18px;
}
.analysis-project-meta {
display: grid;
grid-template-columns: 44px 58px minmax(0, 1fr);
grid-template-columns: 52px 72px minmax(0, 1fr);
align-items: center;
gap: 6px;
min-width: 0;
@ -1016,13 +1084,13 @@ defineExpose({
strong,
em,
small {
height: 22px;
height: 26px;
min-width: 0;
overflow: hidden;
border: 1px solid rgba(48, 220, 255, 0.12);
background: rgba(4, 23, 34, 0.35);
font-size: 10px;
line-height: 20px;
border: 1px solid rgba(48, 220, 255, 0.18);
background: rgba(4, 23, 34, 0.46);
font-size: 12px;
line-height: 24px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
@ -1046,10 +1114,17 @@ defineExpose({
}
b {
color: rgba(232, 252, 255, 0.58);
font-size: 10px;
color: rgba(232, 252, 255, 0.66);
font-size: 12px;
font-weight: 500;
line-height: 14px;
line-height: 16px;
}
&:hover {
border-color: rgba(36, 246, 196, 0.48);
background:
linear-gradient(90deg, rgba(36, 246, 196, 0.14), rgba(48, 220, 255, 0.06)),
rgba(4, 23, 34, 0.62);
}
}
</style>

@ -425,34 +425,6 @@ const parkYakVillageRows = buildParkVillageRows({
decimals: 0,
})
const parkVillageMapPoints = {
洞拉村: [102.82, 33.22],
滚塘村: [102.9, 33.23],
让里村: [102.99, 33.23],
茸塔玛村: [103.08, 33.2],
唐日村: [102.5, 33.11],
德香村: [102.63, 33.12],
日干村: [102.72, 33.08],
色永村: [103.12, 33.04],
}
const parkVillageMapRows = parkYakVillageRows
.map((item) => {
const point = parkVillageMapPoints[item.name]
if (!point) return null
return {
...item,
lng: point[0],
lat: point[1],
value: item.value,
rawValue: item.value,
metricLabel: "牦牛数量",
displayUnit: item.unit,
displayMode: "park-village",
}
})
.filter(Boolean)
const parkEdibleGrassVillageRows = buildParkVillageRows({
sourceKey: "edibleGrasslandWanTon",
weightKey: "edible",
@ -757,13 +729,11 @@ export function getEcologicalMapRanking(viewKey) {
}
return {
status: "success",
title: "国家公园覆盖村",
scope: "village",
labelMode: "village",
title: "国家公园覆盖范围",
scope: "town",
hideTownNames: false,
displayMode: "park-village",
metricLabel: "牦牛数量",
unit: "头",
rows: parkVillageMapRows,
metricLabel: "",
unit: "",
rows: [],
}
}

@ -132,6 +132,10 @@ const props = defineProps({
type: Boolean,
default: true,
},
active: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(["module-stats", "view-change"])
@ -154,6 +158,9 @@ const layerStatus = ref({
let assets = null
let destroyed = false
let mapAssetsReady = false
let mapLoadStarted = false
let mapLoadTimer = null
const currentView = computed(() =>
["national-park", "restoration", "engineering-analysis"].includes(props.activeView) ? props.activeView : "national-park"
@ -215,6 +222,7 @@ onMounted(() => {
onBeforeUnmount(() => {
destroyed = true
if (mapLoadTimer) window.clearTimeout(mapLoadTimer)
document.removeEventListener("pointerdown", handleOutsideFeaturePointerDown, true)
})
@ -231,6 +239,19 @@ watch(selectedLegendKey, () => {
closeFeaturePanel()
})
watch(
() => props.active,
(active) => {
if (!mapSceneRef.value) return
if (active) {
scheduleMapLoadIfActive()
mapSceneRef.value.resume?.()
} else {
mapSceneRef.value.pause?.()
}
},
)
function syncView(viewKey) {
const topic = ecologicalProtectionTopics.find((item) => item.key === viewKey) || ecologicalProtectionTopics[0]
const layers = getLayersByTopic(viewKey)
@ -249,12 +270,13 @@ function syncView(viewKey) {
}
function handleViewSelect(viewKey) {
emit("view-change", ["national-park", "restoration", "engineering-analysis"].includes(viewKey) ? viewKey : "national-park")
const nextKey = ["national-park", "restoration", "engineering-analysis"].includes(viewKey) ? viewKey : "national-park"
if (nextKey === currentView.value) return
emit("view-change", nextKey)
}
function initAssets() {
assets = new Assets()
let mapLoaded = false
const params = { progress: 0 }
assets.instance.on("onProgress", (path, itemsLoaded, itemsTotal) => {
const value = Math.floor((itemsLoaded / itemsTotal) * 100)
@ -265,16 +287,13 @@ function initAssets() {
},
})
})
const loadMapOnce = async () => {
if (destroyed || mapLoaded) return
mapLoaded = true
mapSceneRef.value?.loadMap(assets)
await hideLoading()
await nextTick()
mapSceneRef.value?.play()
const markAssetsReady = () => {
if (destroyed || mapAssetsReady) return
mapAssetsReady = true
scheduleMapLoadIfActive()
}
assets.instance.on("onLoad", loadMapOnce)
assets.ready?.then(loadMapOnce).catch(() => {
assets.instance.on("onLoad", markAssetsReady)
assets.ready?.then(markAssetsReady).catch(() => {
if (destroyed) return
layerStatus.value = {
type: "error",
@ -283,6 +302,41 @@ function initAssets() {
})
}
function scheduleMapLoadIfActive() {
if (destroyed || !props.active || !mapAssetsReady || mapLoadStarted) return
mapLoadStarted = true
if (mapLoadTimer) window.clearTimeout(mapLoadTimer)
mapLoadTimer = window.setTimeout(() => {
mapLoadTimer = null
loadMapOnce()
}, 600)
}
async function loadMapOnce(attempt = 0) {
if (destroyed) return
if (!props.active) {
mapLoadStarted = false
return
}
await nextTick()
if (!props.active) {
mapLoadStarted = false
return
}
mapSceneRef.value?.loadMap(assets)
if (mapSceneRef.value?.canvasMap) {
await hideLoading()
await nextTick()
mapSceneRef.value?.play()
return
}
if (attempt < 24) {
window.setTimeout(() => loadMapOnce(attempt + 1), 120)
return
}
mapLoadStarted = false
}
function toScreenLocalPoint(point) {
const screen = document.querySelector(".ecological-protection")
const rect = screen?.getBoundingClientRect()
@ -371,6 +425,7 @@ function handleAnalysisResult(payload = {}) {
mapSceneRef.value?.showAnalysisResult?.({
boundary: payload.boundary,
matches: payload.matches,
preserveView: currentView.value === "engineering-analysis",
})
if (currentView.value === "engineering-analysis") return
}

@ -1,5 +1,6 @@
import { Resource } from "@/mini3d"
import { FileLoader } from "three"
import { CHINA_GEOJSON_PATH, getPublicMapResourcePath, HONGYUAN_COUNTY_OUTLINE_PATH, HONGYUAN_TOWNSHIP_SCREEN_GEOJSON_PATH } from "@/config/mapResources"
import pathLine from "@/assets/texture/pathLine4.png"
import pathLine3 from "@/assets/texture/pathLine2.png"
@ -21,6 +22,9 @@ import focusBarTexture from "@/assets/texture/focus/focus_bar.png"
import focusBgTexture from "@/assets/texture/focus/focus_bg.png"
import focusMidQuanTexture from "@/assets/texture/focus/focus_mid_quan.png"
import focusMoveBgTexture from "@/assets/texture/focus/focus_move_bg.png"
const sharedAssetLoaders = new Map()
export class Assets {
constructor(options = {}) {
this.options = {
@ -31,12 +35,17 @@ export class Assets {
this.init()
}
init() {
this.instance = new Resource()
// 添加Fileloader
this.instance.addLoader(FileLoader, "FileLoader")
const key = this.getSharedKey()
const shared = getSharedAssetLoader(key, () => this.createAssetList())
this.instance = shared.instance
this.ready = shared.ready
}
getSharedKey() {
return this.options.includeChinaMap ? "hongyuan-map-with-china" : "hongyuan-map"
}
// 资源加载
let base_url = import.meta.env.BASE_URL
createAssetList() {
let assets = [
{ type: "Texture", name: "flyline", path: pathLine },
{ type: "Texture", name: "pathLine", path: pathLine },
@ -46,18 +55,18 @@ export class Assets {
{
type: "File",
name: "china",
path: base_url + "assets/json/红原县-轮廓.json",
path: getPublicMapResourcePath(HONGYUAN_COUNTY_OUTLINE_PATH),
},
{
type: "File",
name: "mapJson",
path: base_url + "datas/geojsons/hongyuan-townships.geojson",
path: getPublicMapResourcePath(HONGYUAN_TOWNSHIP_SCREEN_GEOJSON_PATH),
},
{
type: "File",
name: "mapStroke",
path: base_url + "assets/json/红原县-轮廓.json",
path: getPublicMapResourcePath(HONGYUAN_COUNTY_OUTLINE_PATH),
},
{ type: "Texture", name: "huiguang", path: huiguang },
{ type: "Texture", name: "rotationBorder1", path: rotationBorder1 },
@ -81,10 +90,22 @@ export class Assets {
assets.push({
type: "File",
name: "chinaMap",
path: base_url + "assets/json/中华人民共和国.json",
path: getPublicMapResourcePath(CHINA_GEOJSON_PATH),
})
}
// 资源加载
this.ready = this.instance.loadAll(assets)
return assets
}
}
function getSharedAssetLoader(key, createAssetList) {
if (sharedAssetLoaders.has(key)) return sharedAssetLoaders.get(key)
const instance = new Resource({ cloneTextures: true })
// 添加Fileloader
instance.addLoader(FileLoader, "FileLoader")
// 只共享前端静态底图资源,避免每个地图模块重复下载和解码。
const ready = instance.loadAll(createAssetList())
const shared = { instance, ready }
sharedAssetLoaders.set(key, shared)
return shared
}

@ -1,5 +1,5 @@
<template>
<div class="business-panel" :class="[`is-${panelType}`, `is-${donutVariant}`, { 'is-compact': compact }]">
<div ref="panelRef" class="business-panel" :class="[`is-${panelType}`, `is-${donutVariant}`, { 'is-compact': compact }]">
<div v-if="!rows.length" class="business-panel-empty">暂无业务统计数据</div>
<business-stat-list v-else-if="panelType === 'rank'" :rows="rows" compact />
@ -173,6 +173,7 @@
'is-single': panel?.span === 1,
'is-distribution': panel?.variant === 'distribution',
'is-capacity': panel?.variant === 'capacity',
'is-rank-bar': panel?.variant === 'rankBar',
}"
>
<div
@ -181,6 +182,7 @@
class="business-bar-row"
:class="{ 'is-active': index === normalizedActiveIndex }"
>
<span v-if="panel?.variant === 'rankBar'" class="business-bar-rank">{{ index + 1 }}</span>
<span class="business-bar-name">{{ item.name }}</span>
<div class="business-bar-track">
<i :style="{ width: `${barWidth(item)}%`, background: barGradient(index) }"></i>
@ -526,7 +528,10 @@ const capacitySegmentRows = computed(() => rows.value.filter((item) => [
const capacityTopTownRow = computed(() => rows.value.find((item) => ["forage-top-town", "wetland-yield-top-town"].includes(item.key)))
const activeDonutIndex = ref(0)
const lineHoverIndex = ref(null)
const panelRef = ref(null)
const panelVisible = ref(true)
let donutTimer = null
let visibilityObserver = null
const pieLoopDelay = 3000
const pieKey = computed(() => `${props.panel?.key || props.panel?.title || "pie"}-${pieRows.value.map((item) => `${item.name}:${item.value}`).join("|")}`)
@ -584,10 +589,12 @@ const ringStyle = computed(() => {
})
onMounted(() => {
startDonutLoop()
setupPanelVisibility()
syncDonutLoop()
})
onBeforeUnmount(() => {
cleanupPanelVisibility()
stopDonutLoop()
})
@ -598,10 +605,14 @@ watch(
lineHoverIndex.value = null
projectSearchText.value = ""
selectedProjectKey.value = null
startDonutLoop()
syncDonutLoop()
}
)
watch(panelVisible, () => {
syncDonutLoop()
})
const linePointList = computed(() => {
const data = chartRows.value
const width = 320
@ -886,18 +897,53 @@ function getMetricIcon(item, index) {
function startDonutLoop() {
stopDonutLoop()
const loopLength = activeLoopLength.value
if (!panelVisible.value) return
if (loopLength < 2) return
donutTimer = window.setInterval(() => {
activeDonutIndex.value = (activeDonutIndex.value + 1) % loopLength
}, pieLoopDelay)
}
function syncDonutLoop() {
if (panelVisible.value) {
startDonutLoop()
} else {
stopDonutLoop()
}
}
function stopDonutLoop() {
if (!donutTimer) return
window.clearInterval(donutTimer)
donutTimer = null
}
function setupPanelVisibility() {
updatePanelVisibility()
if (!panelRef.value || typeof window.IntersectionObserver !== "function") return
visibilityObserver = new IntersectionObserver((entries) => {
const entry = entries[0]
panelVisible.value = Boolean(entry?.isIntersecting) && isPanelActuallyVisible()
})
visibilityObserver.observe(panelRef.value)
window.requestAnimationFrame(updatePanelVisibility)
}
function cleanupPanelVisibility() {
visibilityObserver?.disconnect?.()
visibilityObserver = null
}
function updatePanelVisibility() {
panelVisible.value = isPanelActuallyVisible()
}
function isPanelActuallyVisible() {
const element = panelRef.value
if (!element) return true
return Boolean(element.offsetParent || element.getClientRects().length)
}
function barGradient(index) {
return `linear-gradient(90deg, rgba(36, 246, 196, 0.18), ${chartColor(index)})`
}
@ -2387,6 +2433,103 @@ function columnGradient(index) {
text-shadow: none;
}
}
&.is-rank-bar {
gap: 2px;
padding: 3px 14px 2px;
overflow: hidden;
.business-bar-row {
position: relative;
grid-template-columns: 26px minmax(0, 1fr) auto;
grid-template-areas:
"rank name value"
"rank track track";
align-items: center;
min-height: 18px;
gap: 1px 8px;
padding: 1px 0 2px;
border-bottom: 1px solid rgba(48, 220, 255, 0.075);
}
.business-bar-rank {
grid-area: rank;
display: flex;
width: 18px;
height: 18px;
align-items: center;
justify-content: center;
border: 1px solid rgba(48, 220, 255, 0.28);
background:
linear-gradient(180deg, rgba(48, 220, 255, 0.18), rgba(36, 246, 196, 0.055)),
rgba(4, 28, 42, 0.52);
color: rgba(232, 252, 255, 0.9);
font-family: D-DIN, Arial, sans-serif;
font-size: 11px;
font-weight: 800;
box-shadow: inset 0 0 10px rgba(48, 220, 255, 0.08);
}
.business-bar-name {
color: rgba(245, 253, 255, 0.94);
font-size: 11px;
line-height: 13px;
font-weight: 700;
}
.business-bar-track {
position: relative;
height: 5px;
margin-top: 0;
padding: 1px;
overflow: hidden;
border-color: rgba(48, 220, 255, 0.2);
background:
repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.045) 0 1px, transparent 1px 28px),
linear-gradient(90deg, rgba(48, 220, 255, 0.13), rgba(36, 246, 196, 0.035));
}
.business-bar-track i {
min-width: 12px;
box-shadow:
0 0 10px rgba(48, 220, 255, 0.36),
inset 0 0 7px rgba(255, 255, 255, 0.22);
&:before {
content: "";
position: absolute;
right: 0;
top: -2px;
width: 3px;
height: calc(100% + 4px);
background: rgba(255, 255, 255, 0.86);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.56);
}
}
.business-bar-row strong {
color: #ffffff;
font-size: 13px;
line-height: 14px;
text-shadow: 0 0 8px rgba(48, 220, 255, 0.36);
}
.business-bar-row strong em,
.business-bar-row strong small {
color: rgba(238, 253, 255, 0.78);
font-size: 10px;
}
.business-bar-row.is-active {
.business-bar-rank {
border-color: rgba(255, 224, 107, 0.46);
color: #ffe06b;
box-shadow:
inset 0 0 12px rgba(255, 224, 107, 0.1),
0 0 10px rgba(255, 224, 107, 0.14);
}
}
}
}
.business-bar-row {

@ -22,6 +22,7 @@
<script setup>
import { computed } from "vue"
import mCard from "@/components/mCard/index.vue"
import { getHongyuanTownshipNameByCode } from "@/config/townshipBoundaries"
const props = defineProps({
feature: {
@ -166,22 +167,6 @@ const topicBusinessFields = {
],
}
const townCodeNames = {
"513233100": "邛溪镇",
"513233101": "刷经寺镇",
"513233103": "安曲镇",
"513233105": "龙日镇",
"513233202": "江茸乡",
"513233203": "查尔玛乡",
"513233204": "瓦切镇",
"513233205": "阿木乡",
"513233207": "麦洼乡",
"513233208": "色地镇",
"513233101000": "刷经寺镇",
"513233103000": "安曲镇",
"513233105000": "龙日镇",
}
const codeNameMaps = {
land_type: {
1: "草地",
@ -625,13 +610,9 @@ function formatValue(key, value) {
function getTownNameFromCode(value) {
const text = String(value || "").trim()
if (!text) return ""
if (townCodeNames[text]) return townCodeNames[text]
const compact = text.replace(/\D/g, "")
if (townCodeNames[compact]) return townCodeNames[compact]
for (let length = Math.min(12, compact.length); length >= 6; length--) {
const prefix = compact.slice(0, length)
if (townCodeNames[prefix]) return townCodeNames[prefix]
}
const townshipName = getHongyuanTownshipNameByCode(compact || text)
if (townshipName) return townshipName
return isCodeOnly(text) ? "" : text
}

@ -23,12 +23,12 @@
<div class="layer-panel-legend">
<div class="layer-panel-section-title">图例</div>
<div
v-if="legends.length"
v-if="panelLegends.length"
class="layer-panel-legend-grid"
:class="{ 'is-single': legendSingle }"
>
<button
v-for="item in legends"
v-for="item in panelLegends"
:key="item.key || item.name"
class="layer-panel-legend-row"
:class="{
@ -98,8 +98,9 @@ const props = defineProps({
const emit = defineEmits(["update:modelValue", "update:selectedLegendKey"])
const panelLayers = computed(() => Array.isArray(props.layers) ? props.layers : getLayersByTopic(props.topicKey))
const legendSelectable = computed(() => props.legends.filter(isSelectableLegend).length > 1)
const legendSingle = computed(() => props.legends.length === 1)
const panelLegends = computed(() => Array.isArray(props.legends) ? props.legends : [])
const legendSelectable = computed(() => panelLegends.value.filter(isSelectableLegend).length > 1)
const legendSingle = computed(() => panelLegends.value.length === 1)
const panelHeight = computed(() => {
if (props.height) return props.height
return 430

@ -54,6 +54,7 @@ const switcherTopics = computed(() => (props.topics?.length ? props.topics : for
function handleTopicClick(event, topicKey) {
event.currentTarget?.blur?.()
if (topicKey === props.modelValue) return
const now = Date.now()
if (event.type === "click" && lastPointerTopic === topicKey && now - lastPointerAt < 450) return
if (event.type === "pointerup") {

@ -104,6 +104,10 @@ const props = defineProps({
type: Boolean,
default: true,
},
active: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(["topic-stats"])
@ -117,7 +121,6 @@ const businessLeft = ref({ title: "业务统计", rows: [] })
const businessRight = ref({ title: "业务统计", rows: [] })
const leftPanels = ref([])
const rightPanels = ref([])
const businessStatsCache = ref({})
const mapRanking = ref(null)
const selectedLegendKey = ref("")
const selectedFeature = ref(null)
@ -133,8 +136,13 @@ const layerStatus = ref({
let assets = null
let destroyed = false
let mapAssetsReady = false
let mapLoadStarted = false
let mapLoadTimer = null
let rankingRequestId = 0
let businessRequestId = 0
let resettingTopicLayer = false
let resettingLegend = false
const mapRankingLayerKeys = new Set([
"grassland-distribution",
@ -207,18 +215,29 @@ onMounted(() => {
onBeforeUnmount(() => {
destroyed = true
if (mapLoadTimer) window.clearTimeout(mapLoadTimer)
document.removeEventListener("pointerdown", handleOutsideFeaturePointerDown, true)
})
watch(activeTopicKey, (topicKey) => {
resettingTopicLayer = true
selectedLegendKey.value = ""
activeLayerKey.value = ""
closeFeaturePanel()
layerStatus.value = {
type: "idle",
text: "等待 GeoServer 图层加载",
}
loadBusinessStats(topicKey, "")
loadMapRanking(topicKey, "", null)
nextTick(() => {
resettingTopicLayer = false
})
})
watch(activeLayerKey, () => {
if (resettingTopicLayer) return
resettingLegend = true
selectedLegendKey.value = ""
closeFeaturePanel()
layerStatus.value = {
@ -227,16 +246,32 @@ watch(activeLayerKey, () => {
}
loadBusinessStats(activeTopicKey.value, activeLayerKey.value)
loadMapRanking(activeTopicKey.value, activeLayerKey.value, null)
nextTick(() => {
resettingLegend = false
})
})
watch(selectedLegendKey, () => {
if (resettingTopicLayer || resettingLegend) return
closeFeaturePanel()
loadMapRanking(activeTopicKey.value, activeLayerKey.value, selectedLegend.value)
})
watch(
() => props.active,
(active) => {
if (!mapSceneRef.value) return
if (active) {
scheduleMapLoadIfActive()
mapSceneRef.value.resume?.()
} else {
mapSceneRef.value.pause?.()
}
},
)
function initAssets() {
assets = new Assets()
let mapLoaded = false
const params = { progress: 0 }
assets.instance.on("onProgress", (path, itemsLoaded, itemsTotal) => {
const value = Math.floor((itemsLoaded / itemsTotal) * 100)
@ -247,17 +282,13 @@ function initAssets() {
},
})
})
const loadMapOnce = async () => {
if (destroyed) return
if (mapLoaded) return
mapLoaded = true
mapSceneRef.value?.loadMap(assets)
await hideLoading()
await nextTick()
mapSceneRef.value?.play()
const markAssetsReady = () => {
if (destroyed || mapAssetsReady) return
mapAssetsReady = true
scheduleMapLoadIfActive()
}
assets.instance.on("onLoad", loadMapOnce)
assets.ready?.then(loadMapOnce).catch(() => {
assets.instance.on("onLoad", markAssetsReady)
assets.ready?.then(markAssetsReady).catch(() => {
if (destroyed) return
layerStatus.value = {
type: "error",
@ -266,13 +297,42 @@ function initAssets() {
})
}
async function loadBusinessStats(topicKey, layerKey = activeLayerKey.value) {
const cacheKey = `${topicKey}:${layerKey || ""}`
const cached = businessStatsCache.value[cacheKey]
if (cached) {
applyBusinessStats(cached, topicKey)
function scheduleMapLoadIfActive() {
if (destroyed || !props.active || !mapAssetsReady || mapLoadStarted) return
mapLoadStarted = true
if (mapLoadTimer) window.clearTimeout(mapLoadTimer)
mapLoadTimer = window.setTimeout(() => {
mapLoadTimer = null
loadMapOnce()
}, 600)
}
async function loadMapOnce(attempt = 0) {
if (destroyed) return
if (!props.active) {
mapLoadStarted = false
return
}
await nextTick()
if (!props.active) {
mapLoadStarted = false
return
}
mapSceneRef.value?.loadMap(assets)
if (mapSceneRef.value?.canvasMap) {
await hideLoading()
await nextTick()
mapSceneRef.value?.play()
return
}
if (attempt < 24) {
window.setTimeout(() => loadMapOnce(attempt + 1), 120)
return
}
mapLoadStarted = false
}
async function loadBusinessStats(topicKey, layerKey = activeLayerKey.value) {
const requestId = ++businessRequestId
coreLoading.value = true
coreStatus.value = "loading"
@ -280,10 +340,6 @@ async function loadBusinessStats(topicKey, layerKey = activeLayerKey.value) {
if (destroyed || requestId !== businessRequestId) return
if (topicKey !== activeTopicKey.value) return
if ((layerKey || "") !== (activeLayerKey.value || "")) return
businessStatsCache.value = {
...businessStatsCache.value,
[cacheKey]: result,
}
applyBusinessStats(result, topicKey)
}
@ -496,7 +552,7 @@ function hideLoading() {
.m-card-bd-content {
top: 40px;
bottom: 8px;
bottom: 10px;
color: #f3feff;
}
@ -542,7 +598,7 @@ function hideLoading() {
.fgw-map-layer-tools {
top: 372px;
right: 458px;
width: 232px;
width: 248px;
height: 430px;
.m-card {

@ -122,6 +122,16 @@ function parseGeoJsonData(data) {
}
return data
}
function normalizeMapGeoJsonData(data) {
const parsed = parseGeoJsonData(data)
if (!parsed?.features) return parsed
parsed.features.forEach((feature) => {
if (feature.geometry?.type === "Polygon") {
feature.geometry.coordinates = [feature.geometry.coordinates]
}
})
return parsed
}
function getFeaturePolygons(geometry) {
if (!geometry?.coordinates) return []
if (geometry.type === "Polygon") return [geometry.coordinates]
@ -143,7 +153,7 @@ export class World extends Mini3d {
this.mapFocusLabelInfo = {
name: "红原县",
enName: "HONGYUAN COUNTY",
center: [102.6305459723107, 32.08],
center: [102.6305459723107, 32.22],
}
// 是否点击
this.clicked = false
@ -162,6 +172,8 @@ export class World extends Mini3d {
this.camera.controls.target.copy(this.defaultMapTarget)
this.camera.controls.update()
this.activeMapViewTweens = []
this.sceneTweens = []
this.sceneTimers = []
// 创建交互管理
this.interactionManager = new InteractionManager(this.renderer.instance, this.camera.instance, this.canvas)
@ -190,8 +202,18 @@ export class World extends Mini3d {
this.boundaryDrawPoints = []
this.lastBoundaryDrawPoint = null
this.currentVectorFeatures = []
this.yakIndustryPointLabelGroup = null
this.yakIndustryPointLabels = []
this.yakIndustryPointMarkerGroup = null
this.yakIndustryPointMarkers = []
this.yakIndustryMarkerMaterialCache = new Map()
this.hongyuanBoundaryPolygons = []
this.townMetaByName = new Map(provincesData.map((item) => [normalizeTownName(item.name), item]))
this.staticMapData = {
china: normalizeMapGeoJsonData(this.assets.instance.getResource("china")),
mapJson: normalizeMapGeoJsonData(this.assets.instance.getResource("mapJson")),
mapStroke: normalizeMapGeoJsonData(this.assets.instance.getResource("mapStroke")),
}
this.handleCanvasBoundaryPointerDown = this.handleCanvasBoundaryPointerDown.bind(this)
this.handleCanvasBoundaryDoubleClick = this.handleCanvasBoundaryDoubleClick.bind(this)
this.canvas?.addEventListener?.("pointerdown", this.handleCanvasBoundaryPointerDown, true)
@ -252,6 +274,8 @@ export class World extends Mini3d {
this.mapAnimationComplete = true
this.applyLayerMapView(this.currentTopicLayer, { force: true })
this.setTradeNationalMapVisible(false)
this.updateYakIndustryPointMarkers(this.currentTopicLayer, this.currentVectorFeatures)
this.updateYakIndustryPointLabels(this.currentTopicLayer, this.currentVectorFeatures)
if (shouldHideMapLabels(this.currentTownRanking)) {
this.hideAllMapStatisticLabels()
return
@ -300,6 +324,8 @@ export class World extends Mini3d {
this.scatterGroup.visible = true
this.InfoPointGroup.visible = !shouldHideMapLabels(this.currentTownRanking) && this.showTownRankingPointMarkers && this.hasTownRankingRows()
this.setTradeNationalMapVisible(false)
this.updateYakIndustryPointMarkers(this.currentTopicLayer, this.currentVectorFeatures)
this.updateYakIndustryPointLabels(this.currentTopicLayer, this.currentVectorFeatures)
if (shouldHideMapLabels(this.currentTownRanking)) {
this.hideAllMapStatisticLabels()
return
@ -539,7 +565,7 @@ export class World extends Mini3d {
chinaBgMaterialColor: "#0d3349",
lineColor: "#39c7ef",
}
let chinaData = this.assets.instance.getResource("china")
let chinaData = this.staticMapData.china
let chinaBgMaterial = new MeshLambertMaterial({
color: new Color(params.chinaBgMaterialColor),
transparent: true,
@ -574,12 +600,12 @@ export class World extends Mini3d {
if (this.tradeNationalMapGroup) return true
let chinaMapData = null
try {
chinaMapData = this.assets.instance.getResource("chinaMap")
chinaMapData = normalizeMapGeoJsonData(this.assets.instance.getResource("chinaMap"))
} catch (error) {
console.warn("牦牛交易全国地图资源未加载", error)
return false
}
const chinaMapJson = parseGeoJsonData(chinaMapData)
const chinaMapJson = chinaMapData
if (!chinaMapData || !chinaMapJson?.features?.length) return false
const nationalGroup = new Group()
nationalGroup.name = "trade-national-map"
@ -737,7 +763,7 @@ export class World extends Mini3d {
}
}
createProvince() {
let mapJsonData = this.assets.instance.getResource("mapJson")
let mapJsonData = this.staticMapData.mapJson
this.mapJsonData = mapJsonData
const satellite = createHongyuanSatelliteTexture({
geoProjectionCenter: this.geoProjectionCenter,
@ -862,7 +888,7 @@ export class World extends Mini3d {
}
initHongyuanBoundaryPolygons() {
if (this.hongyuanBoundaryPolygons?.length) return
const boundaryData = parseGeoJsonData(this.assets?.instance?.getResource("mapStroke"))
const boundaryData = this.staticMapData.mapStroke
this.hongyuanBoundaryPolygons = getFeaturePolygons(boundaryData?.features?.[0]?.geometry)
}
createSelectedFeatureGroup() {
@ -896,6 +922,8 @@ export class World extends Mini3d {
this.applyLayerMapView(layer)
const loadId = ++this.topicLayerLoadId
this.currentVectorFeatures = []
this.clearYakIndustryPointMarkers()
this.clearYakIndustryPointLabels()
this.topicLayerContext.clearRect(0, 0, this.topicLayerCanvas.width, this.topicLayerCanvas.height)
this.topicLayerTexture.needsUpdate = true
this.topicLayerMaterial.opacity = 0
@ -942,6 +970,8 @@ export class World extends Mini3d {
this.visibleTopicLayers = []
this.currentTopicLayer = null
this.currentVectorFeatures = []
this.clearYakIndustryPointMarkers()
this.clearYakIndustryPointLabels()
this.clearSelectedFeature()
this.applyLayerMapView(null)
this.topicLayerContext.clearRect(0, 0, this.topicLayerCanvas.width, this.topicLayerCanvas.height)
@ -960,6 +990,8 @@ export class World extends Mini3d {
this.currentTopicLayer = activeLayer || validLayers[validLayers.length - 1]
this.visibleTopicLayers = validLayers
this.clearSelectedFeature()
this.clearYakIndustryPointMarkers()
this.clearYakIndustryPointLabels()
this.applyLayerMapView(this.currentTopicLayer)
const loadId = ++this.topicLayerLoadId
this.currentVectorFeatures = []
@ -970,21 +1002,28 @@ export class World extends Mini3d {
type: "loading",
text: `正在加载 ${validLayers.length} 个林地图层`,
})
const drawableLayers = validLayers.filter((layer) => !isVectorTopicLayer(layer))
Promise.all(drawableLayers.map((layer) => this.loadTopicLayerImage(layer)))
Promise.all(validLayers.map((layer) => this.loadCompositeTopicLayerItem(layer)))
.then((items) => {
if (loadId !== this.topicLayerLoadId) return
const loadedItems = items.filter(Boolean)
const activeVectorItem = loadedItems.find((item) => item.type === "vector" && item.layer?.key === this.currentTopicLayer?.key)
this.currentVectorFeatures = activeVectorItem?.geojson?.features || []
this.topicLayerContext.clearRect(0, 0, this.topicLayerCanvas.width, this.topicLayerCanvas.height)
loadedItems.forEach(({ image, layer }, index) => {
this.topicLayerContext.save()
const opacity = index === loadedItems.length - 1
? Math.max(0.78, layer.opacity ?? 0.9)
: Math.min(0.52, layer.opacity ?? 0.52)
this.topicLayerContext.globalAlpha = Math.min(1, Math.max(0.28, opacity))
this.topicLayerContext.drawImage(image, 0, 0, this.topicLayerCanvas.width, this.topicLayerCanvas.height)
this.topicLayerContext.restore()
loadedItems.forEach((item, index) => {
if (item.type === "image") {
this.topicLayerContext.save()
const opacity = index === loadedItems.length - 1
? Math.max(0.78, item.layer.opacity ?? 0.9)
: Math.min(0.52, item.layer.opacity ?? 0.52)
this.topicLayerContext.globalAlpha = Math.min(1, Math.max(0.28, opacity))
this.topicLayerContext.drawImage(item.image, 0, 0, this.topicLayerCanvas.width, this.topicLayerCanvas.height)
this.topicLayerContext.restore()
return
}
this.drawVectorTopicLayer(item.layer, item.geojson.features || [], { clear: false })
})
this.updateYakIndustryPointMarkers(this.currentTopicLayer, this.currentVectorFeatures)
this.updateYakIndustryPointLabels(this.currentTopicLayer, this.currentVectorFeatures)
this.topicLayerTexture.needsUpdate = true
this.topicLayerMaterial.opacity = loadedItems.length ? 1 : 0
this.callbacks.onLayerStatus?.({
@ -993,6 +1032,17 @@ export class World extends Mini3d {
})
})
}
async loadCompositeTopicLayerItem(layer) {
if (isVectorTopicLayer(layer)) {
try {
const geojson = await this.fetchVectorLayerGeojson(layer)
return { type: "vector", layer, geojson }
} catch (error) {
return null
}
}
return this.loadTopicLayerImage(layer).then((item) => (item ? { type: "image", ...item } : null))
}
loadTopicLayerImage(layer) {
return new Promise((resolve) => {
const image = new Image()
@ -1073,6 +1123,16 @@ export class World extends Mini3d {
})
)
}
addSceneTween(tween) {
if (!tween) return tween
this.sceneTweens.push(tween)
if (this.scenePaused) tween.pause?.()
return tween
}
addSceneTimer(timer) {
if (timer) this.sceneTimers.push(timer)
return timer
}
focusTownByName(townName, options = {}) {
const townMeta = this.townMetaByName.get(normalizeTownName(townName))
if (!townMeta?.center) return false
@ -1084,12 +1144,73 @@ export class World extends Mini3d {
this.animateMapCamera(position, target, options.duration ?? 0.75)
return true
}
focusYakIndustryPoint(payload = {}) {
const coordinate = this.getYakIndustryPayloadCoordinate(payload)
if (!coordinate) {
this.callbacks.onLayerStatus?.({
type: "warning",
text: "当前主体暂无可定位坐标",
})
return { focused: false, screen: null }
}
const feature = this.findYakIndustryPointFeature(payload, coordinate)
if (feature) {
this.setSelectedVectorFeatures([feature])
} else {
this.clearSelectedFeature()
}
const focused = this.flyToLngLat(coordinate, {
span: 0.035,
duration: 0.72,
statusText: "已定位产业主体",
})
return {
focused,
screen: this.getYakIndustryPointScreen(payload),
feature,
}
}
getYakIndustryPointScreen(payload = {}) {
const coordinate = this.getYakIndustryPayloadCoordinate(payload)
if (!coordinate) return null
return this.lngLatToLayerScreenPoint(coordinate, this.depth + 0.255)
|| this.lngLatToScreenPoint(coordinate, this.depth + 0.32)
}
getYakIndustryPayloadCoordinate(payload = {}) {
const coordinates = payload.geometry?.coordinates || payload.coordinates
const lng = Number(payload.longitude ?? payload.lng ?? coordinates?.[0])
const lat = Number(payload.latitude ?? payload.lat ?? coordinates?.[1])
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null
return [lng, lat]
}
findYakIndustryPointFeature(payload = {}, coordinate = this.getYakIndustryPayloadCoordinate(payload)) {
const sourceFeatures = this.getVisibleVectorFeatures(this.currentTopicLayer, this.currentVectorFeatures || [])
const id = String(payload.id || payload.key || "")
const name = String(payload.fullName || payload.name || "").trim()
const stageKey = String(payload.stageKey || "")
const matched = sourceFeatures.find((feature) => {
const properties = feature?.properties || {}
if (id && String(properties.id || "") === id) return true
if (name && String(properties.name || properties.fullName || "").trim() === name) return true
return false
})
if (matched) return matched
if (!coordinate) return null
return sourceFeatures.find((feature) => {
if (feature?.geometry?.type !== "Point") return false
const properties = feature.properties || {}
if (stageKey && String(properties.stageKey || "") !== stageKey) return false
return this.getCoordinateDistance(feature.geometry.coordinates, coordinate) <= 0.0008
}) || null
}
async loadVectorTopicLayer(layer, loadId) {
try {
const geojson = await this.fetchVectorLayerGeojson(layer)
if (loadId !== this.topicLayerLoadId) return
this.currentVectorFeatures = geojson.features || []
this.drawVectorTopicLayer(layer, this.currentVectorFeatures)
this.updateYakIndustryPointMarkers(layer, this.currentVectorFeatures)
this.updateYakIndustryPointLabels(layer, this.currentVectorFeatures)
this.topicLayerTexture.needsUpdate = true
this.topicLayerMaterial.opacity = layer.opacity ?? 0.9
this.callbacks.onLayerStatus?.({
@ -1099,6 +1220,8 @@ export class World extends Mini3d {
} catch (error) {
if (loadId !== this.topicLayerLoadId) return
this.currentVectorFeatures = []
this.clearYakIndustryPointMarkers()
this.clearYakIndustryPointLabels()
this.topicLayerContext.clearRect(0, 0, this.topicLayerCanvas.width, this.topicLayerCanvas.height)
this.topicLayerTexture.needsUpdate = true
this.topicLayerMaterial.opacity = 0
@ -1126,14 +1249,18 @@ export class World extends Mini3d {
: { type: "FeatureCollection", features: response?.features || [] }
}
let rows = []
let apiError = null
try {
const response = await fetch(layer.apiUrl, { credentials: "include" })
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const json = await response.json()
rows = Array.isArray(json?.data) ? json.data : []
rows = Array.isArray(json?.data) ? json.data : Array.isArray(json?.data?.rows) ? json.data.rows : []
} catch (error) {
apiError = error
}
if (!rows.length && layer.fallbackPath) {
const fallback = await fetch(buildPublicUrl(layer.fallbackPath), { credentials: "include" })
if (!fallback.ok) throw error
if (!fallback.ok) throw apiError || new Error(`HTTP ${fallback.status}`)
const json = await fallback.json()
rows = Array.isArray(json?.rows) ? json.rows : []
}
@ -1156,9 +1283,9 @@ export class World extends Mini3d {
.filter((feature) => feature.geometry),
}
}
drawVectorTopicLayer(layer, features) {
drawVectorTopicLayer(layer, features, options = {}) {
const context = this.topicLayerContext
context.clearRect(0, 0, this.topicLayerCanvas.width, this.topicLayerCanvas.height)
if (options.clear !== false) context.clearRect(0, 0, this.topicLayerCanvas.width, this.topicLayerCanvas.height)
context.save()
context.lineJoin = "round"
context.lineCap = "round"
@ -1264,6 +1391,7 @@ export class World extends Mini3d {
if (!this.topicLayerContext || !this.topicLayerTexture) return
if (!this.currentTopicLayer || !isVectorTopicLayer(this.currentTopicLayer)) return
this.drawVectorTopicLayer(this.currentTopicLayer, this.currentVectorFeatures || [])
this.updateYakIndustryPointMarkers(this.currentTopicLayer, this.currentVectorFeatures)
this.topicLayerTexture.needsUpdate = true
}
getVisibleVectorFeatures(layer, features) {
@ -1281,8 +1409,311 @@ export class World extends Mini3d {
}
return [...(features || [])]
}
updateYakIndustryPointLabels(layer = this.currentTopicLayer, features = this.currentVectorFeatures) {
this.clearYakIndustryPointLabels()
if (layer?.showPointLabels === false) return
if (layer?.key !== "yak-industry-points" || !this.label3d) return
const labelFeatures = this.getYakIndustryLabelFeatures(layer, features)
if (!labelFeatures.length) return
this.ensureYakIndustryPointLabelGroup()
labelFeatures.forEach((feature, index) => {
let label = this.yakIndustryPointLabels[index]
if (!label) {
label = this.label3d.create("", "yak-industry-point-label", true)
this.label3d.setLabelStyle(label, 0.012, "x")
label.setParent(this.yakIndustryPointLabelGroup)
this.yakIndustryPointLabels.push(label)
}
label.element.className = "yak-industry-point-label"
label.init(
this.getYakIndustryPointLabelHtml(feature),
this.getYakIndustryPointLabelPosition(feature, index)
)
label.visible = true
label.show?.()
label.userData = { feature }
})
this.yakIndustryPointLabels.slice(labelFeatures.length).forEach((label) => {
label.visible = false
label.hide?.()
label.userData = null
})
this.yakIndustryPointLabelGroup.visible = Boolean(this.mapAnimationComplete)
}
updateYakIndustryPointMarkers(layer = this.currentTopicLayer, features = this.currentVectorFeatures) {
this.clearYakIndustryPointMarkers()
if (layer?.key !== "yak-industry-points") return
const markerFeatures = this.getVisibleVectorFeatures(layer, features)
.filter((feature) => feature?.geometry?.type === "Point")
if (!markerFeatures.length) return
this.ensureYakIndustryPointMarkerGroup()
markerFeatures.forEach((feature, index) => {
let marker = this.yakIndustryPointMarkers[index]
const style = this.getVectorFeatureStyle(layer, feature)
const material = this.getYakIndustryMarkerMaterial(style)
const position = this.lngLatToLayerLocalPoint(feature.geometry.coordinates, this.depth + 0.98)
const scale = Number(layer?.pointSpriteScale) || 0.34
if (!marker) {
marker = new Sprite(material)
marker.name = "yak-industry-point-marker"
marker.renderOrder = 42
marker.center.set(0.5, 0.08)
this.bindYakIndustryPointMarkerEvents(marker)
this.interactionManager.add(marker)
this.yakIndustryPointMarkerGroup.add(marker)
this.yakIndustryPointMarkers.push(marker)
}
marker.material = material
marker.position.copy(position)
marker.scale.set(scale, scale * 1.32, 1)
marker.visible = true
marker.userData = {
feature,
layer,
index,
}
})
this.yakIndustryPointMarkers.slice(markerFeatures.length).forEach((marker) => {
marker.visible = false
marker.userData = null
})
this.yakIndustryPointMarkerGroup.visible = Boolean(this.mapAnimationComplete)
}
ensureYakIndustryPointMarkerGroup() {
if (this.yakIndustryPointMarkerGroup) return
this.yakIndustryPointMarkerGroup = new Group()
this.yakIndustryPointMarkerGroup.name = "yak-industry-point-markers"
this.yakIndustryPointMarkerGroup.visible = false
this.focusMapGroup?.add(this.yakIndustryPointMarkerGroup)
}
clearYakIndustryPointMarkers() {
if (this.yakIndustryPointMarkerGroup) this.yakIndustryPointMarkerGroup.visible = false
this.yakIndustryPointMarkers?.forEach((marker) => {
marker.visible = false
})
}
bindYakIndustryPointMarkerEvents(marker) {
marker.addEventListener("mousedown", (event) => {
if (this.clicked || !this.yakIndustryPointMarkerGroup?.visible) return false
const feature = event.target?.userData?.feature
if (!feature) return false
this.clicked = true
const coordinates = feature.geometry?.coordinates || []
this.setSelectedVectorFeatures([feature])
this.callbacks.onFeatureInfo?.({
feature: {
id: feature.properties?.id || feature.properties?.feature_id || feature.properties?.project_name,
properties: feature.properties || {},
feature,
layer: this.currentTopicLayer,
lngLat: { lng: Number(coordinates[0]), lat: Number(coordinates[1]) },
},
screen: this.getPointFeatureScreenPoint(feature),
})
})
marker.addEventListener("mouseup", () => {
this.clicked = false
})
marker.addEventListener("mouseover", () => {
if (marker.visible) document.body.style.cursor = "pointer"
})
marker.addEventListener("mouseout", () => {
document.body.style.cursor = "default"
})
}
getYakIndustryMarkerMaterial(style) {
const key = `${style.fill || "#24F6C4"}-${style.stroke || "#B7FFF2"}`
const cached = this.yakIndustryMarkerMaterialCache.get(key)
if (cached) return cached
const texture = this.createYakIndustryMarkerTexture(style.fill, style.stroke)
const material = new SpriteMaterial({
map: texture,
transparent: true,
depthTest: false,
depthWrite: false,
fog: false,
})
this.yakIndustryMarkerMaterialCache.set(key, material)
return material
}
createYakIndustryMarkerTexture(fill = "#24F6C4", stroke = "#B7FFF2") {
const canvas = document.createElement("canvas")
canvas.width = 96
canvas.height = 124
const context = canvas.getContext("2d")
const x = 48
const top = 10
const tip = 111
const headY = 43
const radius = 28
context.clearRect(0, 0, canvas.width, canvas.height)
context.beginPath()
context.arc(x, 62, 38, 0, Math.PI * 2)
context.fillStyle = this.hexToRgba(fill, 0.18)
context.shadowColor = fill
context.shadowBlur = 18
context.fill()
context.beginPath()
context.moveTo(x, tip)
context.bezierCurveTo(x + 31, 79, x + 38, top + 20, x, top)
context.bezierCurveTo(x - 38, top + 20, x - 31, 79, x, tip)
context.closePath()
context.fillStyle = this.hexToRgba(fill, 0.96)
context.strokeStyle = this.hexToRgba(stroke || fill, 0.98)
context.lineWidth = 5
context.shadowColor = stroke || fill
context.shadowBlur = 14
context.fill()
context.stroke()
context.shadowBlur = 0
context.beginPath()
context.arc(x, headY, radius * 0.46, 0, Math.PI * 2)
context.fillStyle = this.hexToRgba("#FFFFFF", 0.95)
context.fill()
context.beginPath()
context.arc(x, headY, radius * 0.74, 0, Math.PI * 2)
context.strokeStyle = this.hexToRgba("#FFFFFF", 0.38)
context.lineWidth = 4
context.stroke()
const texture = new CanvasTexture(canvas)
texture.colorSpace = SRGBColorSpace
texture.needsUpdate = true
return texture
}
ensureYakIndustryPointLabelGroup() {
if (this.yakIndustryPointLabelGroup) return
this.yakIndustryPointLabelGroup = new Group()
this.yakIndustryPointLabelGroup.name = "yak-industry-point-labels"
this.yakIndustryPointLabelGroup.rotation.x = -Math.PI / 2
this.yakIndustryPointLabelGroup.visible = false
this.scene.add(this.yakIndustryPointLabelGroup)
}
clearYakIndustryPointLabels() {
if (this.yakIndustryPointLabelGroup) this.yakIndustryPointLabelGroup.visible = false
this.yakIndustryPointLabels?.forEach((label) => {
label.visible = false
label.hide?.()
})
}
getYakIndustryLabelFeatures(layer, features) {
const visibleFeatures = this.getVisibleVectorFeatures(layer, features)
.filter((feature) => feature?.geometry?.type === "Point")
const limit = Number(layer?.pointLabelLimit) || (visibleFeatures.length > 36 ? 12 : Math.min(visibleFeatures.length, 16))
const sorted = visibleFeatures
.map((feature) => ({
feature,
score: this.getYakIndustryFeatureScore(feature),
town: normalizeTownName(feature.properties?.town),
}))
.sort((a, b) => b.score - a.score || String(a.feature.properties?.name || "").localeCompare(String(b.feature.properties?.name || ""), "zh-CN"))
const selected = []
const usedTowns = new Set()
sorted.forEach((item) => {
if (selected.length >= limit) return
if (!item.town || usedTowns.has(item.town)) return
selected.push(item.feature)
usedTowns.add(item.town)
})
sorted.forEach((item) => {
if (selected.length >= limit) return
if (selected.includes(item.feature)) return
selected.push(item.feature)
})
return selected
}
getYakIndustryFeatureScore(feature) {
const properties = feature?.properties || {}
const values = [
properties.outputValue,
properties.capacity,
properties.value,
properties.count,
properties["年产值 (万元)"],
properties["年总产值 (万元)"],
properties["设计存栏量 (头)"],
properties["总库容 (吨)"],
].map((value) => this.parseMapMetricNumber(value))
return Math.max(...values, 0)
}
parseMapMetricNumber(value) {
if (typeof value === "number") return Number.isFinite(value) ? value : 0
const match = String(value ?? "").replace(/,/g, "").match(/-?\d+(?:\.\d+)?/)
return match ? Number(match[0]) || 0 : 0
}
getYakIndustryPointLabelPosition(feature, index = 0) {
const point = this.lngLatToLayerLocalPoint(feature?.geometry?.coordinates, this.depth + 1.08 + Math.min(index, 5) * 0.015)
point.x += ((index % 3) - 1) * 0.08
point.y += index % 2 === 0 ? 0.07 : -0.06
return point
}
getYakIndustryPointLabelHtml(feature) {
const properties = feature?.properties || {}
const name = this.compactYakIndustryLabelName(properties.name || properties.fullName || "产业主体")
const category = this.formatYakIndustryLabelText(properties.categoryName || properties.stageName || "产业主体")
const town = properties.town || properties.village || "红原县"
const metric = this.getYakIndustryPointMetricText(properties)
const color = this.getYakIndustryLabelColor(properties)
return `<div class="yak-industry-point-label-wrap" style="--yak-point-color:${escapeHtml(color)}">
<div class="yak-industry-point-label-head"><i></i><span>${escapeHtml(category)}</span></div>
<div class="yak-industry-point-label-name">${escapeHtml(name)}</div>
<div class="yak-industry-point-label-meta">
<span>${escapeHtml(town)}</span>
${metric ? `<strong>${escapeHtml(metric)}</strong>` : ""}
</div>
</div>`
}
compactYakIndustryLabelName(value = "") {
const text = String(value)
.replace(/^四川红原/, "")
.replace(/^红原县/, "")
.replace(/有限责任公司$/, "")
.replace(/农民专业合作社$/, "合作社")
.replace(/\s+/g, " ")
.trim()
return text.length > 16 ? `${text.slice(0, 15)}` : text
}
formatYakIndustryLabelText(value = "") {
return String(value)
.replace(/\bcxpt[-_./\s]*/gi, "")
.replace(/^cxpt/i, "")
.replace(/\s+/g, " ")
.trim()
}
getYakIndustryPointMetricText(properties = {}) {
if (properties.capacity !== undefined && properties.capacity !== null && properties.capacity !== "" && this.parseMapMetricNumber(properties.capacity) > 0) {
return this.formatYakIndustryMetricText(properties.capacity, properties.capacityUnit)
}
if (this.parseMapMetricNumber(properties.outputValue) > 0) return `${this.formatMapRankingValue(properties.outputValue)}万元`
if (this.isMeaningfulMapText(properties.locationType)) return properties.locationType
if (this.isMeaningfulMapText(properties.status)) return properties.status
return ""
}
isMeaningfulMapText(value) {
const text = String(value ?? "").trim()
return Boolean(text && text !== "0" && text !== "无" && text !== "待完善")
}
formatYakIndustryMetricText(value, unit = "") {
const text = String(value ?? "").trim()
if (!text || text === "0") return ""
if (typeof value === "string" && /[^\d.,\s-]/.test(text)) return text
return `${this.formatMapRankingValue(value)}${unit || ""}`
}
getYakIndustryLabelColor(properties = {}) {
const stageKey = String(properties.stageKey || "")
const legend = (this.currentTopicLayer?.legends || []).find((item) => String(item.match?.value || "") === stageKey)
|| (String(this.currentTopicLayer?.activeLegend?.match?.value || "") === stageKey ? this.currentTopicLayer.activeLegend : null)
return legend?.fillColor || this.currentTopicLayer?.activeLegend?.fillColor || "#24F6C4"
}
clipVectorLayerContext(context, layer) {
this.clipContextToHongyuanBoundary(context)
if (this.shouldClipLayerToHongyuanBoundary(layer)) {
this.clipContextToHongyuanBoundary(context)
}
if (!this.shouldClipLayerToLayerBounds(layer)) return
const bounds = this.getLayerClipBounds(layer)
if (!bounds) return
const topLeft = this.lngLatToLayerCanvasPoint([bounds.west, bounds.north])
@ -1313,11 +1744,14 @@ export class World extends Mini3d {
context.clip("evenodd")
}
shouldClipLayerToHongyuanBoundary(layer = this.currentTopicLayer) {
if (!layer) return false
if (layer.clipToHongyuanBoundary === false) return false
if (layer.clipToHongyuanBoundary === true) return true
const key = String(layer?.key || "")
return Boolean(layer?.clipToHongyuanBoundary || key === "national-park-zoning" || key.startsWith("yak-"))
return Boolean(key === "national-park-zoning" || key.startsWith("yak-"))
}
shouldClipLayerToLayerBounds(layer = this.currentTopicLayer) {
return Boolean(layer?.clipBounds && layer?.key === "national-park-zoning")
return Boolean(layer?.clipBounds && layer?.clipToLayerBounds !== false)
}
isLngLatInHongyuanBoundary(lngLat) {
const polygons = this.hongyuanBoundaryPolygons || []
@ -1351,7 +1785,7 @@ export class World extends Mini3d {
if (!match) return false
return String(properties[match.field] ?? "") === String(match.value)
})
const legend = matchedLegend || layer.legends?.[0] || {}
const legend = matchedLegend || layer.activeLegend || layer.legends?.[0] || {}
const isLine = String(feature.geometry?.type || "").includes("LineString")
const isPoint = feature.geometry?.type === "Point"
if (layer?.key === "yak-epidemic-village-boundary") {
@ -1375,6 +1809,18 @@ export class World extends Mini3d {
pointRadius: this.getVectorPointRadius(layer, feature),
glow: true,
halo: true,
marker: true,
}
}
if (layer?.boundaryOnly) {
return {
fill: legend.fillColor || "#24F6C4",
stroke: legend.strokeColor || legend.fillColor || "#18F8FF",
polygonAlpha: 0.04,
lineAlpha: 0.96,
lineWidth: 10,
pointRadius: 4.5,
glow: true,
}
}
return {
@ -1390,7 +1836,7 @@ export class World extends Mini3d {
getVectorPointRadius(layer, feature) {
const value = Number(feature?.properties?.count || feature?.properties?.value || 0)
if (layer?.key === "yak-recognition-layer") return Math.max(3.8, Math.min(8.6, 3.6 + Math.sqrt(value) / 68))
if (layer?.key === "yak-industry-points") return 4.8
if (layer?.key === "yak-industry-points") return 10.4
return 4.5
}
drawVectorFeature(context, geometry, style) {
@ -1455,6 +1901,10 @@ export class World extends Mini3d {
context.restore()
}
drawPointPath(context, coordinate, style) {
if (style.marker) {
this.drawMarkerPointPath(context, coordinate, style)
return
}
const point = this.lngLatToLayerCanvasPoint(coordinate)
context.save()
if (style.halo) {
@ -1478,6 +1928,43 @@ export class World extends Mini3d {
}
context.restore()
}
drawMarkerPointPath(context, coordinate, style) {
const point = this.lngLatToLayerCanvasPoint(coordinate)
const radius = style.pointRadius || 6
context.save()
context.beginPath()
context.arc(point.x, point.y + radius * 0.22, radius * 2.55, 0, Math.PI * 2)
context.fillStyle = this.hexToRgba(style.fill, 0.16)
context.shadowColor = style.fill
context.shadowBlur = 24
context.fill()
context.beginPath()
context.moveTo(point.x, point.y + radius * 2.05)
context.bezierCurveTo(point.x + radius * 1.46, point.y + radius * 0.62, point.x + radius * 1.34, point.y - radius * 1.34, point.x, point.y - radius * 1.62)
context.bezierCurveTo(point.x - radius * 1.34, point.y - radius * 1.34, point.x - radius * 1.46, point.y + radius * 0.62, point.x, point.y + radius * 2.05)
context.closePath()
context.fillStyle = this.hexToRgba(style.fill, 0.94)
context.strokeStyle = this.hexToRgba(style.stroke, 0.98)
context.lineWidth = Math.max(2, style.lineWidth || 2)
context.shadowColor = style.stroke
context.shadowBlur = 14
context.fill()
context.stroke()
context.beginPath()
context.arc(point.x, point.y - radius * 0.34, radius * 0.52, 0, Math.PI * 2)
context.fillStyle = this.hexToRgba("#FFFFFF", 0.96)
context.shadowBlur = 0
context.fill()
context.beginPath()
context.arc(point.x, point.y - radius * 0.34, radius * 0.82, 0, Math.PI * 2)
context.lineWidth = Math.max(1.2, radius * 0.14)
context.strokeStyle = this.hexToRgba("#FFFFFF", 0.42)
context.stroke()
context.restore()
}
lngLatToLayerCanvasPoint(coordinate) {
const lng = Number(coordinate?.[0])
const lat = Number(coordinate?.[1])
@ -1972,30 +2459,15 @@ export class World extends Mini3d {
</div>`
}
getTradeFlowLabelOffset(row, index) {
const map = {
left: new Vector3(-0.96, 0.14 + (index % 2) * 0.14, 0),
right: new Vector3(0.92, 0.12 - (index % 2) * 0.12, 0),
top: new Vector3(-0.34 + (index % 2) * 0.58, 0.58, 0),
bottom: new Vector3(-0.32 + (index % 2) * 0.56, -0.56, 0),
"top-left": new Vector3(-0.9, 0.56, 0),
"top-right": new Vector3(0.84, 0.56, 0),
"bottom-left": new Vector3(-0.9, -0.52, 0),
"bottom-right": new Vector3(0.84, -0.52, 0),
}
return map[row.labelSide] || [
new Vector3(-0.96, 0.26, 0),
new Vector3(0.92, 0.14, 0),
new Vector3(-0.86, -0.34, 0),
new Vector3(0.86, -0.32, 0),
][index % 4]
return new Vector3(0, 0, 0)
}
getTradeSourceLabelSide(townMeta, index = 0) {
const lng = Number(townMeta?.center?.[0])
const lat = Number(townMeta?.center?.[1])
if (Number.isFinite(lng) && lng < this.geoProjectionCenter[0] - 0.16) return index % 2 ? "top-left" : "left"
if (Number.isFinite(lng) && lng > this.geoProjectionCenter[0] + 0.22) return index % 2 ? "bottom-right" : "right"
if (Number.isFinite(lat) && lat > this.geoProjectionCenter[1] + 0.24) return index % 2 ? "top-right" : "top"
if (Number.isFinite(lat) && lat < this.geoProjectionCenter[1] - 0.28) return index % 2 ? "bottom-left" : "bottom"
if (Number.isFinite(lng) && lng < this.geoProjectionCenter[0] - 0.18) return "left"
if (Number.isFinite(lng) && lng > this.geoProjectionCenter[0] + 0.2) return "right"
if (Number.isFinite(lat) && lat > this.geoProjectionCenter[1] + 0.22) return "top"
if (Number.isFinite(lat) && lat < this.geoProjectionCenter[1] - 0.26) return "bottom"
return ["right", "left", "top", "bottom"][index % 4]
}
getTradeLocalPoint(lng, lat, localZ = 0) {
@ -2211,7 +2683,7 @@ export class World extends Mini3d {
sourceLabel.element.className = "trade-market-label"
sourceLabel.init(
this.getTradeMarketHtml(source, rows),
this.getTradeLocalPoint(source.lng, source.lat, 1.46).add(new Vector3(0.52, -0.98, 0))
this.getTradeLocalPoint(source.lng, source.lat, 1.46)
)
sourceLabel.visible = true
sourceLabel.userData = { ...source, rows: rows.length }
@ -2808,14 +3280,15 @@ export class World extends Mini3d {
diffuseColor: 0x71918e,
diffuseWidth: 2.0,
callback: (pointShader) => {
setTimeout(() => {
gsap.to(pointShader.uniforms.uTime, {
const timer = setTimeout(() => {
this.addSceneTween(gsap.to(pointShader.uniforms.uTime, {
value: 4,
repeat: -1,
duration: 6,
ease: "power1.easeIn",
})
}))
}, 3)
this.addSceneTimer(timer)
},
})
let mesh = new Mesh(geometry, material)
@ -3110,6 +3583,7 @@ export class World extends Mini3d {
}
}
createInfoPointLabelLoop() {
if (this.scenePaused || !this.infoLabelElement?.length) return
clearInterval(this.infoPointLabelTime)
this.infoPointLabelTime = setInterval(() => {
this.infoPointIndex++
@ -3126,7 +3600,7 @@ export class World extends Mini3d {
}, 3000)
}
createStorke() {
const mapStroke = this.assets.instance.getResource("mapStroke")
const mapStroke = this.staticMapData.mapStroke
const texture = this.assets.instance.getResource("pathLine3")
texture.wrapS = texture.wrapT = RepeatWrapping
texture.repeat.set(2, 1)
@ -3564,15 +4038,25 @@ export class World extends Mini3d {
}
return this.lngLatToScreenPoint(geometry.coordinates, this.depth + 0.32)
}
lngLatToLayerScreenPoint(coordinate, localZ = 0) {
lngLatToLayerLocalPoint(coordinate, localZ = 0) {
const fallbackPoint = () => {
const lng = Number(coordinate?.[0])
const lat = Number(coordinate?.[1])
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return new Vector3(0, 0, Number(localZ) || 0)
const [x, y] = this.geoProjection([lng, lat])
return new Vector3(x, -y, Number(localZ) || 0)
}
const bounds = this.topicLayerUvBounds
if (!bounds) return this.lngLatToScreenPoint(coordinate, localZ)
if (!bounds) return fallbackPoint()
const layerPoint = this.lngLatToLayerCanvasPoint(coordinate)
const canvas = this.topicLayerCanvas
if (!canvas?.width || !canvas?.height) return this.lngLatToScreenPoint(coordinate, localZ)
if (!canvas?.width || !canvas?.height) return fallbackPoint()
const x = bounds.left + (layerPoint.x / canvas.width) * (bounds.right - bounds.left)
const y = bounds.bottom + ((canvas.height - layerPoint.y) / canvas.height) * (bounds.top - bounds.bottom)
return this.localMapPointToScreenPoint(new Vector3(x, y, Number(localZ) || 0))
return new Vector3(x, y, Number(localZ) || 0)
}
lngLatToLayerScreenPoint(coordinate, localZ = 0) {
return this.localMapPointToScreenPoint(this.lngLatToLayerLocalPoint(coordinate, localZ))
}
lngLatToScreenPoint(coordinate, localZ = 0) {
const lng = Number(coordinate?.[0])
@ -3724,7 +4208,6 @@ export class World extends Mini3d {
type: "success",
text: "分析边界已生成",
})
this.focusAnalysisGeometry(feature.geometry)
this.redrawVectorTopicLayer()
return feature
}
@ -3754,10 +4237,12 @@ export class World extends Mini3d {
this.analysisMatchedFeatures = matches
.map((item) => item?.feature || item)
.filter((feature) => feature?.geometry)
if (!hasBoundaryPayload && this.analysisMatchedFeatures.length) {
this.focusAnalysisFeatures(this.analysisMatchedFeatures)
} else if (this.analysisBoundaryFeature?.geometry) {
this.focusAnalysisGeometry(this.analysisBoundaryFeature.geometry)
if (!payload.preserveView) {
if (!hasBoundaryPayload && this.analysisMatchedFeatures.length) {
this.focusAnalysisFeatures(this.analysisMatchedFeatures)
} else if (this.analysisBoundaryFeature?.geometry) {
this.focusAnalysisGeometry(this.analysisBoundaryFeature.geometry)
}
}
this.redrawVectorTopicLayer()
return true
@ -4355,7 +4840,27 @@ export class World extends Mini3d {
this.updateTradeFlowPulses(delta)
this.interactionManager && this.interactionManager.update()
}
pauseScene() {
this.scenePaused = true
this.time?.pause?.()
this.sceneTweens.forEach((tween) => tween?.pause?.())
clearInterval(this.infoPointLabelTime)
this.infoPointLabelTime = null
}
resumeScene() {
this.scenePaused = false
this.time?.resume?.()
this.sceneTweens.forEach((tween) => tween?.resume?.())
if (this.showTownRankingPointMarkers && this.mapAnimationComplete && !this.infoPointLabelTime) {
this.createInfoPointLabelLoop()
}
}
destroy() {
this.pauseScene()
this.sceneTimers.forEach((timer) => clearTimeout(timer))
this.sceneTimers = []
this.sceneTweens.forEach((tween) => tween?.kill?.())
this.sceneTweens = []
this.callbacks = {}
this.canvas?.removeEventListener?.("pointerdown", this.handleCanvasBoundaryPointerDown, true)
this.canvas?.removeEventListener?.("dblclick", this.handleCanvasBoundaryDoubleClick, true)

@ -23,6 +23,9 @@ let resizeObserver = null;
let resizeFrame = null;
let resizeFallbackTimer = null;
let resizeCheckTimer = null;
let activeLayerSignature = "";
let mapRankingSignature = "";
let hasPlayedIntro = false;
onMounted(() => {
emit("scene-mounted");
});
@ -38,16 +41,34 @@ function loadMap(assets) {
onDrawBoundary: (payload) => emit("draw-boundary", payload),
});
canvasMap.value.time.pause();
canvasMap.value.setTopicLayers(props.activeLayer ? [props.activeLayer] : []);
activeLayerSignature = getLayerSignature(props.activeLayer);
mapRankingSignature = getMapRankingSignature(props.mapRanking);
canvasMap.value.setTopicLayers(getActiveLayerStack(props.activeLayer), props.activeLayer);
canvasMap.value.setTownRanking(props.mapRanking);
connectMapResizeObserver();
scheduleMapResize();
}
async function play() {
if (!canvasMap.value) return;
canvasMap.value.time.resume();
canvasMap.value.animateTl.timeScale(1); //
canvasMap.value.animateTl.play();
resume();
if (hasPlayedIntro || canvasMap.value.mapAnimationComplete) return;
hasPlayedIntro = true;
canvasMap.value.animateTl?.timeScale?.(1); //
canvasMap.value.animateTl?.play?.();
}
function resume() {
if (!canvasMap.value) return;
connectMapResizeObserver();
scheduleMapResize();
canvasMap.value.resumeScene?.();
if (!canvasMap.value.resumeScene) canvasMap.value.time.resume();
}
function pause() {
if (!canvasMap.value) return;
canvasMap.value.animateTl?.pause?.();
canvasMap.value.pauseScene?.();
if (!canvasMap.value.pauseScene) canvasMap.value.time?.pause?.();
cleanupMapResizeObserver();
}
function clearSelectedFeature() {
canvasMap.value?.clearSelectedFeature?.();
@ -61,6 +82,12 @@ function focusPasture(payload) {
function focusTown(townName, options) {
return canvasMap.value?.focusTownByName?.(townName, options);
}
function focusYakIndustryPoint(payload) {
return canvasMap.value?.focusYakIndustryPoint?.(payload);
}
function getYakIndustryPointScreen(payload) {
return canvasMap.value?.getYakIndustryPointScreen?.(payload);
}
function setBoundaryDrawing(enabled) {
canvasMap.value?.setBoundaryDrawing?.(enabled);
}
@ -138,8 +165,11 @@ function cleanupMapResizeObserver() {
watch(
() => props.activeLayer,
(layer) => {
const signature = getLayerSignature(layer);
if (signature === activeLayerSignature) return;
activeLayerSignature = signature;
if (canvasMap.value) {
canvasMap.value.setTopicLayers(layer ? [layer] : []);
canvasMap.value.setTopicLayers(getActiveLayerStack(layer), layer);
}
},
{ deep: true },
@ -147,6 +177,9 @@ watch(
watch(
() => props.mapRanking,
(ranking) => {
const signature = getMapRankingSignature(ranking);
if (signature === mapRankingSignature) return;
mapRankingSignature = signature;
if (canvasMap.value) {
canvasMap.value.setTownRanking(ranking);
}
@ -156,10 +189,14 @@ watch(
defineExpose({
loadMap,
play,
resume,
pause,
clearSelectedFeature,
focusProject,
focusPasture,
focusTown,
focusYakIndustryPoint,
getYakIndustryPointScreen,
setBoundaryDrawing,
finishBoundaryDrawing,
undoBoundaryPoint,
@ -168,6 +205,47 @@ defineExpose({
destroyMap,
canvasMap,
});
function getLayerSignature(layer) {
if (!layer) return "";
return getActiveLayerStack(layer)
.map((item) => [
item.key || "",
item.layerName || "",
item.styles || "",
item.cqlFilter || "",
item.activeLegend?.key || "",
item.sourceType || "",
item.dataPath || "",
item.fallbackPath || "",
].join("|"))
.join("||");
}
function getActiveLayerStack(layer) {
if (!layer) return [];
const overlayLayers = Array.isArray(layer.overlayLayers) ? layer.overlayLayers.filter(Boolean) : [];
return [layer, ...overlayLayers];
}
function getMapRankingSignature(ranking) {
if (!ranking) return "";
return [
ranking.status || "",
ranking.topicKey || "",
ranking.layerKey || "",
ranking.title || "",
ranking.metricLabel || "",
ranking.scope || "",
ranking.displayMode || "",
ranking.showPointMarkers ?? "",
ranking.showBars ?? "",
ranking.showTownNames ?? "",
ranking.showMapLabels ?? "",
ranking.rows?.length || 0,
ranking.rows?.map((row) => `${row.key || row.name || ""}:${row.value ?? ""}`).join(",") || "",
].join("|");
}
</script>
<style lang="scss">
@ -184,6 +262,94 @@ defineExpose({
width: 100%;
height: 100%;
}
.yak-industry-point-label {
width: 172px;
min-height: 72px;
margin-bottom: 28px;
color: #f2feff;
font-size: 12px;
pointer-events: none;
will-change: transform;
}
.yak-industry-point-label-wrap {
position: relative;
width: 172px;
padding: 8px 10px 9px;
border: 1px solid color-mix(in srgb, var(--yak-point-color, #24f6c4) 56%, transparent);
border-radius: 4px;
background:
linear-gradient(120deg, rgba(6, 37, 49, 0.88), rgba(3, 16, 27, 0.76)),
linear-gradient(90deg, color-mix(in srgb, var(--yak-point-color, #24f6c4) 18%, transparent), transparent 72%);
box-shadow:
0 0 18px color-mix(in srgb, var(--yak-point-color, #24f6c4) 26%, transparent),
inset 0 0 14px rgba(48, 220, 255, 0.08);
}
.yak-industry-point-label-wrap::after {
content: "";
position: absolute;
left: 14px;
bottom: -9px;
width: 14px;
height: 14px;
border-right: 1px solid color-mix(in srgb, var(--yak-point-color, #24f6c4) 52%, transparent);
border-bottom: 1px solid color-mix(in srgb, var(--yak-point-color, #24f6c4) 52%, transparent);
background: rgba(3, 16, 27, 0.82);
transform: rotate(45deg);
}
.yak-industry-point-label-head,
.yak-industry-point-label-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.yak-industry-point-label-head {
color: rgba(218, 249, 255, 0.7);
font-size: 10px;
}
.yak-industry-point-label-head i {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--yak-point-color, #24f6c4);
box-shadow: 0 0 10px var(--yak-point-color, #24f6c4);
}
.yak-industry-point-label-head span,
.yak-industry-point-label-name,
.yak-industry-point-label-meta span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.yak-industry-point-label-name {
margin-top: 5px;
color: #ffffff;
font-size: 13px;
font-weight: 800;
line-height: 1.35;
}
.yak-industry-point-label-meta {
margin-top: 5px;
color: rgba(211, 241, 246, 0.68);
font-size: 10px;
}
.yak-industry-point-label-meta strong {
flex: 0 0 auto;
color: #ffe06b;
font-size: 11px;
font-weight: 800;
}
.info-point {
color: #eaffff;
font-size: 13px;
@ -510,7 +676,7 @@ defineExpose({
linear-gradient(110deg, rgba(68, 44, 10, 0.72), rgba(4, 24, 34, 0.58)),
rgba(5, 20, 30, 0.58);
box-shadow: 0 0 10px rgba(255, 207, 98, 0.14), inset 0 0 10px rgba(255, 207, 98, 0.06);
transform: translate(38%, 18%);
transform: translate(60%, 0);
&:before {
position: absolute;
@ -577,12 +743,10 @@ defineExpose({
linear-gradient(105deg, rgba(4, 37, 50, 0.58), rgba(3, 17, 27, 0.48)),
rgba(1, 17, 27, 0.46);
box-shadow: 0 0 8px rgba(48, 220, 255, 0.12), inset 0 0 8px rgba(39, 240, 192, 0.05);
transform: translate(36%, -18%);
transform: translate(60%, 0);
&.is-left,
&.is-top-left,
&.is-bottom-left {
transform: translate(-112%, -18%);
&.is-left {
transform: translate(-66%, 0);
&:before {
right: -10px;
@ -596,32 +760,40 @@ defineExpose({
}
}
&.is-top,
&.is-top-left,
&.is-top-right {
transform: translate(-50%, -118%);
}
&.is-top {
transform: translate(0, -108%);
&.is-bottom,
&.is-bottom-left,
&.is-bottom-right {
transform: translate(-50%, 20%);
}
&:before {
left: 50%;
top: auto;
bottom: -10px;
width: 1px;
height: 10px;
background: linear-gradient(180deg, rgba(85, 226, 255, 0.84), rgba(85, 226, 255, 0));
}
&.is-top-left {
transform: translate(-110%, -112%);
&:after {
left: calc(50% - 3px);
top: auto;
bottom: -14px;
}
}
&.is-top-right {
transform: translate(24%, -112%);
}
&.is-bottom {
transform: translate(0, 82%);
&.is-bottom-left {
transform: translate(-110%, 18%);
}
&:before {
left: 50%;
top: -10px;
width: 1px;
height: 10px;
background: linear-gradient(180deg, rgba(85, 226, 255, 0), rgba(85, 226, 255, 0.84));
}
&.is-bottom-right {
transform: translate(24%, 18%);
&:after {
left: calc(50% - 3px);
top: -14px;
}
}
&:before {

@ -1,5 +1,6 @@
import { Resource } from "@/mini3d"
import { FileLoader } from "three"
import { getPublicMapResourcePath, HONGYUAN_COUNTY_OUTLINE_PATH, HONGYUAN_TOWNSHIP_SCREEN_GEOJSON_PATH } from "@/config/mapResources"
import pathLine from "@/assets/texture/pathLine4.png"
import pathLine3 from "@/assets/texture/pathLine2.png"
@ -33,7 +34,6 @@ export class Assets {
this.instance.addLoader(FileLoader, "FileLoader")
// 资源加载
let base_url = import.meta.env.BASE_URL
let assets = [
{ type: "Texture", name: "flyline", path: pathLine },
{ type: "Texture", name: "pathLine", path: pathLine },
@ -43,18 +43,18 @@ export class Assets {
{
type: "File",
name: "china",
path: base_url + "assets/json/红原县-轮廓.json",
path: getPublicMapResourcePath(HONGYUAN_COUNTY_OUTLINE_PATH),
},
{
type: "File",
name: "mapJson",
path: base_url + "datas/geojsons/hongyuan-townships.geojson",
path: getPublicMapResourcePath(HONGYUAN_TOWNSHIP_SCREEN_GEOJSON_PATH),
},
{
type: "File",
name: "mapStroke",
path: base_url + "assets/json/红原县-轮廓.json",
path: getPublicMapResourcePath(HONGYUAN_COUNTY_OUTLINE_PATH),
},
{ type: "Texture", name: "huiguang", path: huiguang },

@ -215,7 +215,6 @@ function updateCurrentTime() {
//
function initAssets(onLoadCallback) {
assets.value = new Assets();
console.log("assets.value", assets.value);
//
let params = {
progress: 0,

@ -1,14 +1,30 @@
// Data mapped from cxph-jgpt real system screens and static data files.
export const YAK_STOCK_TOTAL = 458230;
export const YAK_DRONE_RECOGNITION_TOTAL = 450230;
export const YAK_EXPERT_CORRECTION = YAK_STOCK_TOTAL - YAK_DRONE_RECOGNITION_TOTAL;
function scaleCountRowsToTotal(rows, total, key = "count") {
const sourceTotal = rows.reduce((sum, item) => sum + (Number(item[key]) || 0), 0);
if (!sourceTotal || !total) return rows.map((item) => ({ ...item, [key]: 0 }));
const scaledRows = rows.map((item) => ({
...item,
[key]: Math.round(((Number(item[key]) || 0) / sourceTotal) * total),
}));
const delta = total - scaledRows.reduce((sum, item) => sum + (Number(item[key]) || 0), 0);
if (delta && scaledRows[0]) scaledRows[0][key] += delta;
return scaledRows;
}
export const homeCore = {
herdsmanCount: 129,
grasslandArea: 7568515,
wetArea: 2429693,
yakCount: 10965,
yakCount: YAK_STOCK_TOTAL,
projectCount: 17,
baseCount: 12,
};
export const grasslandYakRows = [
const rawGrasslandYakRows = [
{ zoneName: "色地镇", grasslandArea: 1463044, yakCount: 11704 },
{ zoneName: "麦洼乡", grasslandArea: 660352, yakCount: 5283 },
{ zoneName: "阿木乡", grasslandArea: 1115947, yakCount: 8928 },
@ -35,21 +51,21 @@ export const grasslandWetRows = [
];
export const yakForecastRows = [
{ month: "1月", count: 485, speed: 380 },
{ month: "2月", count: 502, speed: 313 },
{ month: "3月", count: 471, speed: 142 },
{ month: "4月", count: 893, speed: 574 },
{ month: "5月", count: 874, speed: 231 },
{ month: "6月", count: 879, speed: 555 },
{ month: "7月", count: 145, speed: 654 },
{ month: "8月", count: 521, speed: 123 },
{ month: "9月", count: 741, speed: 456 },
{ month: "10月", count: 744, speed: 458 },
{ month: "11月", count: 777, speed: 444 },
{ month: "12月", count: 563, speed: 254 },
{ month: "1月", count: 458230, speed: 0 },
{ month: "2月", count: 458860, speed: 630 },
{ month: "3月", count: 459520, speed: 660 },
{ month: "4月", count: 460180, speed: 660 },
{ month: "5月", count: 461260, speed: 1080 },
{ month: "6月", count: 462050, speed: 790 },
{ month: "7月", count: 462850, speed: 800 },
{ month: "8月", count: 463160, speed: 310 },
{ month: "9月", count: 463520, speed: 360 },
{ month: "10月", count: 463940, speed: 420 },
{ month: "11月", count: 464350, speed: 410 },
{ month: "12月", count: 464760, speed: 410 },
];
export const townRecognitionRows = [
const rawTownRecognitionRows = [
{ name: "色地镇", count: 131983, devices: 25 },
{ name: "瓦切镇", count: 126611, devices: 24 },
{ name: "安曲镇", count: 99331, devices: 20 },
@ -62,10 +78,17 @@ export const townRecognitionRows = [
{ name: "刷经寺镇", count: 30407, devices: 8 },
];
export const townRecognitionRows = scaleCountRowsToTotal(rawTownRecognitionRows, YAK_DRONE_RECOGNITION_TOTAL);
const finalTownCountMap = new Map(scaleCountRowsToTotal(rawTownRecognitionRows, YAK_STOCK_TOTAL).map((item) => [item.name, item.count]));
export const grasslandYakRows = rawGrasslandYakRows.map((item) => ({
...item,
yakCount: finalTownCountMap.get(item.zoneName) || item.yakCount,
}));
export const recognitionTotal = townRecognitionRows.reduce((sum, item) => sum + item.count, 0);
export const expertCorrection = 1824;
export const tradeDeduction = -3672;
export const estimatedStock = recognitionTotal + expertCorrection + tradeDeduction;
export const expertCorrection = YAK_EXPERT_CORRECTION;
export const tradeDeduction = 0;
export const estimatedStock = YAK_STOCK_TOTAL;
homeCore.yakCount = estimatedStock;
function withStockCounts(rows, total) {
const nextRows = rows.map((item) => ({
@ -99,9 +122,9 @@ export const tradeImpactRows = [
export const stockForecastRows = [
{ name: "当前", stock: estimatedStock, births: 0, outflow: 0 },
{ name: "2026年底", stock: 781236, births: 44763, outflow: 21684 },
{ name: "2027年底", stock: 779428, births: 121374, outflow: 61286 },
{ name: "2028年底", stock: 782617, births: 199642, outflow: 102517 },
{ name: "2026年底", stock: 462850, births: 24680, outflow: 19020 },
{ name: "2027年底", stock: 468920, births: 67420, outflow: 55180 },
{ name: "2028年底", stock: 475360, births: 109680, outflow: 90140 },
];
export const supportIndustryRows = [

@ -56,6 +56,21 @@ function sortByValue(data) {
data.sort((a, b) => b.value - a.value)
return data
}
function parseGeoJsonData(data) {
if (!data) return null
if (typeof data === "string") return JSON.parse(data)
return data
}
function normalizeMapGeoJsonData(data) {
const parsed = parseGeoJsonData(data)
if (!parsed?.features) return parsed
parsed.features.forEach((feature) => {
if (feature.geometry?.type === "Polygon") {
feature.geometry.coordinates = [feature.geometry.coordinates]
}
})
return parsed
}
export class World extends Mini3d {
constructor(canvas, assets, options = {}) {
super(canvas)
@ -77,6 +92,8 @@ export class World extends Mini3d {
this.clicked = false
this.sceneMode = options.sceneMode || "default"
this.mapAnimationDone = false
this.sceneTweens = []
this.sceneTimers = []
// 雾
this.scene.fog = new Fog(0x102736, 1, 50)
// 背景
@ -91,10 +108,25 @@ export class World extends Mini3d {
this.interactionManager = new InteractionManager(this.renderer.instance, this.camera.instance, this.canvas)
this.assets = assets
this.staticMapData = {
china: normalizeMapGeoJsonData(this.assets.instance.getResource("china")),
mapJson: normalizeMapGeoJsonData(this.assets.instance.getResource("mapJson")),
mapStroke: normalizeMapGeoJsonData(this.assets.instance.getResource("mapStroke")),
}
// 创建环境光
this.initEnvironment()
this.init()
}
addSceneTween(tween) {
if (!tween) return tween
this.sceneTweens.push(tween)
if (this.scenePaused) tween.pause?.()
return tween
}
addSceneTimer(timer) {
if (timer) this.sceneTimers.push(timer)
return timer
}
init() {
// 标签组
this.labelGroup = new Group()
@ -411,7 +443,7 @@ export class World extends Mini3d {
chinaBgMaterialColor: "#152c47",
lineColor: "#3f82cd",
}
let chinaData = this.assets.instance.getResource("china")
let chinaData = this.staticMapData.china
let chinaBgMaterial = new MeshLambertMaterial({
color: new Color(params.chinaBgMaterialColor),
transparent: true,
@ -441,7 +473,7 @@ export class World extends Mini3d {
return { china, chinaTopLine }
}
createProvince() {
let mapJsonData = this.assets.instance.getResource("mapJson")
let mapJsonData = this.staticMapData.mapJson
const satellite = createHongyuanSatelliteTexture({
geoProjectionCenter: this.geoProjectionCenter,
geoProjectionScale: this.geoProjectionScale,
@ -829,14 +861,15 @@ export class World extends Mini3d {
diffuseColor: 0x71918e,
diffuseWidth: 2.0,
callback: (pointShader) => {
setTimeout(() => {
gsap.to(pointShader.uniforms.uTime, {
const timer = setTimeout(() => {
this.addSceneTween(gsap.to(pointShader.uniforms.uTime, {
value: 4,
repeat: -1,
duration: 6,
ease: "power1.easeIn",
})
}))
}, 3)
this.addSceneTimer(timer)
},
})
let mesh = new Mesh(geometry, material)
@ -1266,6 +1299,7 @@ export class World extends Mini3d {
})
}
createInfoPointLabelLoop() {
if (this.scenePaused || !this.infoLabelElement?.length) return
clearInterval(this.infoPointLabelTime)
this.infoPointLabelTime = setInterval(() => {
this.infoPointIndex++
@ -1282,7 +1316,7 @@ export class World extends Mini3d {
}, 3000)
}
createStorke() {
const mapStroke = this.assets.instance.getResource("mapStroke")
const mapStroke = this.staticMapData.mapStroke
const texture = this.assets.instance.getResource("pathLine3")
texture.wrapS = texture.wrapT = RepeatWrapping
texture.repeat.set(2, 1)
@ -1319,7 +1353,27 @@ export class World extends Mini3d {
super.update()
this.interactionManager && this.interactionManager.update()
}
pauseScene() {
this.scenePaused = true
this.time?.pause?.()
this.sceneTweens.forEach((tween) => tween?.pause?.())
clearInterval(this.infoPointLabelTime)
this.infoPointLabelTime = null
}
resumeScene() {
this.scenePaused = false
this.time?.resume?.()
this.sceneTweens.forEach((tween) => tween?.resume?.())
if (this.InfoPointGroup?.visible && !this.infoPointLabelTime) {
this.createInfoPointLabelLoop()
}
}
destroy() {
this.pauseScene()
this.sceneTimers.forEach((timer) => clearTimeout(timer))
this.sceneTimers = []
this.sceneTweens.forEach((tween) => tween?.kill?.())
this.sceneTweens = []
super.destroy()
this.label3d && this.label3d.destroy()
}

@ -15,6 +15,7 @@ const props = defineProps({
},
});
const emit = defineEmits(["town-click", "play-complete"]);
let hasPlayedIntro = false;
onBeforeUnmount(() => {
destroyMap();
});
@ -29,9 +30,22 @@ function loadMap(assets) {
}
async function play() {
if (!canvasMap.value) return;
canvasMap.value.time.resume();
canvasMap.value.animateTl.timeScale(1); //
canvasMap.value.animateTl.play();
resume();
if (hasPlayedIntro || canvasMap.value.mapAnimationDone) return;
hasPlayedIntro = true;
canvasMap.value.animateTl?.timeScale?.(1); //
canvasMap.value.animateTl?.play?.();
}
function resume() {
if (!canvasMap.value) return;
canvasMap.value.resumeScene?.();
if (!canvasMap.value.resumeScene) canvasMap.value.time.resume();
}
function pause() {
if (!canvasMap.value) return;
canvasMap.value.animateTl?.pause?.();
canvasMap.value.pauseScene?.();
if (!canvasMap.value.pauseScene) canvasMap.value.time?.pause?.();
}
function destroyMap() {
canvasMap.value?.destroy?.();
@ -40,6 +54,8 @@ function destroyMap() {
defineExpose({
loadMap,
play,
resume,
pause,
destroyMap,
canvasMap,
});

@ -1,9 +1,11 @@
import { grasslandYakRows } from "@/views/gdMap/livestockData"
import { pastureListRows } from "@/views/pastureManagement/data"
import { getHongyuanTownshipCodeByName } from "@/config/townshipBoundaries"
export const BALANCE_CALCULATION_PARAMS = {
grasslandCapacityPerMu: 0.047,
yakToSheepUnit: 4.8,
grasslandCapacityPerMu: 0.06229833103624831,
muPerYak: 16.051794379822947,
yakToSheepUnit: 1,
}
export const COUNTY_TARGET = {
@ -78,19 +80,6 @@ export const pastureAreaKeys = [
"pasture_area",
]
export const townCodeByName = {
邛溪镇: "513233100",
刷经寺镇: "513233101",
安曲镇: "513233103",
龙日镇: "513233105",
江茸乡: "513233202",
查尔玛乡: "513233203",
瓦切镇: "513233204",
阿木乡: "513233205",
麦洼乡: "513233207",
色地镇: "513233208",
}
const round = (value, decimals = 2) => {
const factor = 10 ** decimals
return Math.round((Number(value) || 0) * factor) / factor
@ -149,7 +138,7 @@ export function getRiskLevel(value) {
}
function calculateLivestockCapacity(yakCount) {
return Math.round((Number(yakCount) || 0) * BALANCE_CALCULATION_PARAMS.yakToSheepUnit)
return Math.round(Number(yakCount) || 0)
}
function calculateBearingCapacity(areaRange) {
@ -203,7 +192,7 @@ export const balanceTownRows = grasslandYakRows.map((item, index) => buildBalanc
key: `town-${item.zoneName}`,
type: "town",
name: item.zoneName,
areaCode: townCodeByName[item.zoneName],
areaCode: getHongyuanTownshipCodeByName(item.zoneName),
areaRange: item.grasslandArea,
yakNumber: item.yakCount,
location: null,
@ -236,7 +225,7 @@ export const balancePastureRows = sortBalancePastureRows(pastureListRows.map((it
key: item.id || item.key || item.name,
type: "pasture",
name: item.name,
areaCode: townCodeByName[item.township],
areaCode: getHongyuanTownshipCodeByName(item.township),
pasturesId: item.id,
areaRange: item.grasslandArea,
yakNumber: item.yakCount,
@ -375,7 +364,7 @@ function normalizePastureTargetRow(item = {}, index = 0, boundaryById = new Map(
key: pasturesId ? `pasture-${pasturesId}` : `pasture-${name}-${index}`,
type: "pasture",
name,
areaCode: item.areaCode || item.area_code || townCodeByName[township],
areaCode: item.areaCode || item.area_code || getHongyuanTownshipCodeByName(township),
pasturesId,
areaRange,
yakNumber: toNumber(pickValue(item, fieldMap.yakNumber), 0),

@ -234,6 +234,12 @@ import {
toNumber,
} from "./data"
const props = defineProps({
active: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(["loading-progress", "map-ready"])
const selectorCardWidth = 326
@ -341,8 +347,8 @@ const calculationSteps = computed(() => [
{
key: "livestock",
index: "03",
name: "载畜量折算",
desc: "将牦牛存栏按标准折算系数换算为载畜压力。",
name: "牦牛数量核算",
desc: "按牦牛数量与理论承载头数计算超载压力。",
},
{
key: "balance",
@ -376,10 +382,10 @@ const analysisProcessRows = computed(() => {
{
key: "livestock",
index: "03",
name: "载畜量折算",
desc: "形成实际载畜压力口径",
name: "牦牛数量核算",
desc: "形成实际牦牛数量口径",
pending: "等待载畜折算",
expression: "正在按牦牛存栏和折算系数换算载畜压力",
expression: "正在按牦牛数量和理论承载头数核算超载率",
result: "已折算",
},
{
@ -454,6 +460,18 @@ watch(
}
)
watch(
() => props.active,
(active) => {
if (active || !analysisRunning.value) return
analysisState.value = "idle"
analysisProgress.value = 0
processStageIndex.value = 0
activeAnalysisRequest = null
clearAnalysisTimer()
},
)
function handleScopeChange(scopeKey) {
activeScope.value = scopeKey
activeKey.value = ""
@ -468,7 +486,7 @@ function selectBalance(row) {
}
function startAnalysis() {
if (!activeBalance.value || analysisRunning.value) return
if (!props.active || !activeBalance.value || analysisRunning.value) return
analysisState.value = "running"
analysisProgress.value = 0
processStageIndex.value = 0

@ -1,4 +1,4 @@
import { grasslandWetRows, grasslandYakRows, homeCore, supportIndustryRows, tradeImpactRows } from "@/views/gdMap/livestockData"
import { estimatedStock, grasslandWetRows, grasslandYakRows, homeCore, supportIndustryRows, tradeImpactRows } from "@/views/gdMap/livestockData"
const round = (value, decimals = 2) => {
const factor = 10 ** decimals
@ -10,11 +10,12 @@ const toWanMu = (value) => round((Number(value) || 0) / 10000, 2)
const totalTradeHead = sumBy(tradeImpactRows, "value")
const totalTownYak = sumBy(grasslandYakRows, "yakCount")
const yakStockTotal = estimatedStock
const grasslandAreaWanMu = round(homeCore.grasslandArea / 10000, 2)
const wetlandAreaWanMu = round(homeCore.wetArea / 10000, 2)
const woodlandAreaWanMu = 177.92
const totalForestGrassWetWanMu = round(grasslandAreaWanMu + wetlandAreaWanMu + woodlandAreaWanMu, 2)
const yakDensity = round(totalTownYak / Math.max(grasslandAreaWanMu, 1), 1)
const yakDensity = round(yakStockTotal / Math.max(grasslandAreaWanMu, 1), 1)
const wetlandRate = round((wetlandAreaWanMu / Math.max(totalForestGrassWetWanMu, 1)) * 100, 1)
const basicGrasslandWanMu = 637.13
const wetlandYieldWanTon = 108.84
@ -26,7 +27,7 @@ export const homeTopStats = [
key: "yak-count",
zh: "牦牛数量",
name: "牦牛数量",
value: totalTownYak,
value: yakStockTotal,
unit: "头",
decimals: 0,
},
@ -159,7 +160,7 @@ export const dataFlowRows = [
{ key: "flow-grassland", module: "林草湿", name: "草地面积分布完成乡镇归集", value: grasslandAreaWanMu, unit: "万亩", trend: "+0.8%" },
{ key: "flow-wetland", module: "林草湿", name: "湿地生产力指标进入总览", value: wetlandYieldWanTon, unit: "万吨", trend: "+1.2%" },
{ key: "flow-woodland", module: "林草湿", name: "公益林保护面积同步", value: woodlandPublicWanMu, unit: "万亩", trend: "89.6%" },
{ key: "flow-yak-town", module: "牦牛", name: "乡镇牦牛存栏排行刷新", value: totalTownYak, unit: "头", trend: "+2.4%" },
{ key: "flow-yak-town", module: "牦牛", name: "畜群结构总量同步", value: yakStockTotal, unit: "头", trend: "+2.4%" },
{ key: "flow-yak-density", module: "草畜", name: "草畜密度联动核算", value: yakDensity, unit: "头/万亩", trend: "联动" },
{ key: "flow-trade-1", module: "交易", name: "屠宰出栏流通记录入库", value: tradeRows[0]?.value || 0, unit: "头", trend: "+6%" },
{ key: "flow-trade-2", module: "交易", name: "活畜交易影响扣减存栏", value: tradeRows[1]?.value || 0, unit: "头", trend: "-3%" },
@ -170,6 +171,6 @@ export const centerOrbitNodes = [
{ key: "grassland", label: "草地", value: grasslandAreaWanMu, unit: "万亩", x: 50, y: 15, color: "#7ee269" },
{ key: "wetland", label: "湿地", value: wetlandAreaWanMu, unit: "万亩", x: 83, y: 48, color: "#32dcff" },
{ key: "trade", label: "交易", value: totalTradeHead, unit: "头", x: 68, y: 72, color: "#ffe06b" },
{ key: "yak", label: "牦牛", value: totalTownYak, unit: "头", x: 24, y: 74, color: "#24f6c4" },
{ key: "yak", label: "牦牛", value: yakStockTotal, unit: "头", x: 24, y: 74, color: "#24f6c4" },
{ key: "woodland", label: "林地", value: woodlandAreaWanMu, unit: "万亩", x: 16, y: 35, color: "#2bd17e" },
]

@ -170,6 +170,12 @@ import {
} from "./data"
import liveSceneImage from "@/assets/images/gd-live/live-scene-1.png"
const props = defineProps({
active: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(["module-stats", "loading-progress", "map-ready"])
const mapSceneRef = ref(null)
const homeVideoScreenRef = ref(null)
@ -178,9 +184,13 @@ const homeChartRefs = new Map()
let mapAssets = null
let destroyed = false
let chartMotionTimer = null
let chartMotionKickTimer = null
let videoPlayer = null
let videoPlayerReady = false
let videoPlayRun = 0
let homeIdleWorkHandle = null
let homeIdleWorkType = ""
let homeIdleWorkLoaded = false
const chartMotionTick = ref(0)
const homeVideoRows = ref([])
const activeVideoManualIndex = ref(0)
@ -497,15 +507,13 @@ onMounted(() => {
moduleKey: "home",
items: homeTopStats,
})
loadForestGrassWetPanels()
loadHomeMonitorings()
initHomeMap()
startChartMotion()
})
onBeforeUnmount(() => {
destroyed = true
stopVideoPlayer()
cancelHomeIdleWork()
stopChartMotion()
homeChartRefs.clear()
mapSceneRef.value?.destroyMap?.()
@ -523,6 +531,22 @@ watch(activeHomeLayerKey, () => {
selectedHomeLegendKey.value = ""
})
watch(
() => props.active,
(active) => {
if (active) {
mapSceneRef.value?.resume?.()
startChartMotion()
if (!homeIdleWorkLoaded) scheduleHomeIdleWork()
} else {
mapSceneRef.value?.pause?.()
cancelHomeIdleWork()
stopChartMotion()
stopVideoPlayer({ resetError: true })
}
},
)
function initHomeMap() {
mapAssets = new Assets()
emit("loading-progress", { progress: 0 })
@ -543,7 +567,8 @@ function initHomeMap() {
if (scene?.canvasMap) {
emit("loading-progress", { progress: 100 })
emit("map-ready")
scene.play?.()
scheduleHomeIdleWork()
if (props.active) scene.play?.()
return
}
if (attempt < 24) {
@ -551,12 +576,45 @@ function initHomeMap() {
return
}
emit("map-ready")
scheduleHomeIdleWork()
}
mapAssets.ready?.then(loadMapOnce).catch(() => {
emit("map-ready")
scheduleHomeIdleWork()
})
}
function scheduleHomeIdleWork() {
cancelHomeIdleWork()
const run = () => {
homeIdleWorkHandle = null
homeIdleWorkType = ""
if (destroyed || !props.active) return
homeIdleWorkLoaded = true
void loadForestGrassWetPanels()
void loadHomeMonitorings()
if (props.active) startChartMotion()
}
if (typeof window.requestIdleCallback === "function") {
homeIdleWorkType = "idle"
homeIdleWorkHandle = window.requestIdleCallback(run, { timeout: 1800 })
return
}
homeIdleWorkType = "timeout"
homeIdleWorkHandle = window.setTimeout(run, 650)
}
function cancelHomeIdleWork() {
if (!homeIdleWorkHandle) return
if (homeIdleWorkType === "idle" && typeof window.cancelIdleCallback === "function") {
window.cancelIdleCallback(homeIdleWorkHandle)
} else {
window.clearTimeout(homeIdleWorkHandle)
}
homeIdleWorkHandle = null
homeIdleWorkType = ""
}
function setHomeChartRef(key, chart) {
if (!key) return
if (chart) {
@ -571,19 +629,26 @@ function setHomeVideoScreenRef(el) {
}
function startChartMotion() {
if (!props.active || destroyed) return
stopChartMotion()
chartMotionTimer = window.setInterval(playChartMotion, 2600)
window.setTimeout(playChartMotion, 800)
chartMotionKickTimer = window.setTimeout(playChartMotion, 800)
}
function stopChartMotion() {
if (!chartMotionTimer) return
window.clearInterval(chartMotionTimer)
chartMotionTimer = null
if (chartMotionTimer) {
window.clearInterval(chartMotionTimer)
chartMotionTimer = null
}
if (chartMotionKickTimer) {
window.clearTimeout(chartMotionKickTimer)
chartMotionKickTimer = null
}
}
function playChartMotion() {
if (destroyed) return
if (destroyed || !props.active) return
chartMotionKickTimer = null
chartMotionTick.value += 1
playYakStructureMotion()
playThreeYearForecastMotion()
@ -1202,7 +1267,7 @@ function isHomeLegendItemSelectable(item) {
.m-card-bd-content {
top: 40px;
bottom: 8px;
bottom: 10px;
color: #f3feff;
}

@ -63,14 +63,14 @@
</template>
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue"
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
import mCard from "@/components/mCard/index.vue"
import MapScene from "@/views/forestGrassWet/map.vue"
import LayerPanel from "@/views/forestGrassWet/components/LayerPanel.vue"
import BusinessPanelRenderer from "@/views/forestGrassWet/components/BusinessPanelRenderer.vue"
import { Assets } from "@/views/forestGrassWet/assets"
import { getLayerByKey } from "@/config/layers"
import { getHongyuanTownshipNameByCode, hongyuanTownshipsGeoJsonRaw } from "@/config/townshipBoundaries"
import { getHongyuanTownshipNameByCode, hongyuanTownshipsGeoJsonRaw, isHongyuanTownshipName } from "@/config/townshipBoundaries"
import { getPastureGrasslandsById, getPastureProfile, getPasturesDetailsById } from "@/services/pastureApi"
import {
pastureAgeRows,
@ -86,6 +86,12 @@ import {
pastureTownRows,
} from "./data"
const props = defineProps({
active: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(["module-stats", "loading-progress", "map-ready"])
const sideCardWidth = 398
const sideCardHeight = 190
@ -100,20 +106,11 @@ const selectedLegendKey = ref("")
const pastureTopStatNames = ["牧户数量", "户均牦牛头数", "户均草地面积", "联户数量"]
let mapAssets = null
let mapAssetsReady = false
let mapLoadStarted = false
let mapLoadTimer = null
let destroyed = false
let pastureEnrichToken = 0
const pastureTownCodeNames = {
"513233100": "邛溪镇",
"513233101": "刷经寺镇",
"513233103": "安曲镇",
"513233105": "龙日镇",
"513233202": "江茸乡",
"513233203": "查尔玛乡",
"513233204": "瓦切镇",
"513233205": "阿木乡",
"513233207": "麦洼乡",
"513233208": "色地镇",
}
let pastureEnrichTimer = null
const pastureFallbackTownshipById = {
"4028abc18eecd2af018eecdc44b7086b": "瓦切镇",
"402881828b486d54018b48741f590270": "查尔玛乡",
@ -269,9 +266,28 @@ onMounted(() => {
onBeforeUnmount(() => {
destroyed = true
if (mapLoadTimer) window.clearTimeout(mapLoadTimer)
if (pastureEnrichTimer) window.clearTimeout(pastureEnrichTimer)
pastureEnrichToken += 1
mapSceneRef.value?.destroyMap?.()
})
watch(
() => props.active,
(active) => {
if (!mapSceneRef.value) return
if (active) {
schedulePastureMapSceneLoad()
mapSceneRef.value.resume?.()
} else {
if (pastureEnrichTimer) window.clearTimeout(pastureEnrichTimer)
pastureEnrichTimer = null
pastureEnrichToken += 1
mapSceneRef.value.pause?.()
}
},
)
async function loadPastureProfile() {
try {
const profile = await getPastureProfile()
@ -283,18 +299,32 @@ async function loadPastureProfile() {
emitPastureTopStats()
const normalizedList = normalizePastureRows(profile.list)
pastureRows.value = normalizedList.length ? normalizedList : sortPastureRows(pastureListRows)
enrichPastureRowsWithGrasslands(pastureRows.value)
schedulePastureEnrichment(pastureRows.value)
} catch (error) {
apiProfile.value = pastureFallbackProfile
pastureRows.value = sortPastureRows(pastureListRows)
}
}
function schedulePastureEnrichment(rows = pastureRows.value, delay = 4200) {
if (destroyed || !props.active) return
if (pastureEnrichTimer) window.clearTimeout(pastureEnrichTimer)
pastureEnrichTimer = window.setTimeout(() => {
pastureEnrichTimer = null
if (destroyed || !props.active) return
if (!mapReady.value) {
schedulePastureEnrichment(rows, 1800)
return
}
enrichPastureRowsWithGrasslands(rows)
}, delay)
}
async function enrichPastureRowsWithGrasslands(rows = []) {
const token = ++pastureEnrichToken
const targets = (Array.isArray(rows) ? rows : [])
.filter((row) => row?.township === "--" && getRealPastureId(row))
.slice(0, 80)
.slice(0, 24)
if (!targets.length) return
const results = await mapWithConcurrency(targets, 5, async (row) => {
const pastureId = getRealPastureId(row)
@ -463,7 +493,7 @@ function initPastureMap() {
})
mapAssets.ready?.then(() => {
mapAssetsReady = true
loadPastureMapScene()
schedulePastureMapSceneLoad()
}).catch(() => {
mapReady.value = true
emit("map-ready")
@ -471,25 +501,48 @@ function initPastureMap() {
}
function handleMapSceneMounted() {
if (mapAssetsReady) loadPastureMapScene()
schedulePastureMapSceneLoad()
}
function schedulePastureMapSceneLoad() {
if (destroyed || !props.active || !mapAssetsReady || mapLoadStarted) return
mapLoadStarted = true
if (mapLoadTimer) window.clearTimeout(mapLoadTimer)
mapLoadTimer = window.setTimeout(() => {
mapLoadTimer = null
loadPastureMapScene()
}, 600)
}
async function loadPastureMapScene(attempt = 0) {
if (destroyed || !mapAssetsReady || !mapAssets) return
if (!props.active) {
mapLoadStarted = false
return
}
await nextTick()
if (!props.active) {
mapLoadStarted = false
return
}
const scene = mapSceneRef.value
scene?.loadMap?.(mapAssets)
if (scene?.canvasMap) {
mapReady.value = true
emit("loading-progress", { progress: 100 })
emit("map-ready")
scene.play?.()
if (props.active) {
scene.play?.()
} else {
scene.pause?.()
}
return
}
if (attempt < 24) {
window.setTimeout(() => loadPastureMapScene(attempt + 1), 120)
return
}
mapLoadStarted = false
mapReady.value = true
emit("map-ready")
}
@ -862,10 +915,9 @@ function getPastureTownship(item = {}, grasslands = []) {
function normalizePastureTownName(value) {
const text = String(value || "").trim()
if (!text || text === "--") return ""
if (pastureTownCodeNames[text]) return pastureTownCodeNames[text]
const codeName = getPastureTownNameFromCode(text)
if (codeName) return codeName
return text
return isHongyuanTownshipName(text) ? text : ""
}
function getPastureFallbackTownship(item = {}, name = "", realId = "") {
@ -910,7 +962,7 @@ function getPastureTownNameFromCode(value) {
if (!text) return ""
const normalizedCode = text.replace(/\D/g, "")
if (!normalizedCode) return ""
return getHongyuanTownshipNameByCode(normalizedCode) || pastureTownCodeNames[normalizedCode] || pastureTownCodeNames[normalizedCode.slice(0, 9)] || ""
return getHongyuanTownshipNameByCode(normalizedCode)
}
function getPastureTownNameFromGeometry(item = {}) {
@ -1142,7 +1194,7 @@ function isLngLatInPolygon(point, polygon) {
.m-card-bd-content {
top: 40px;
bottom: 8px;
bottom: 10px;
color: #f3feff;
}
@ -1171,7 +1223,7 @@ function isLngLatInPolygon(point, polygon) {
top: 372px;
right: 458px;
z-index: 7;
width: 232px;
width: 248px;
height: 430px;
pointer-events: all;

@ -19,6 +19,7 @@
class="platform-module-pane"
>
<home-dashboard
:active="activeModule.key === 'home'"
@loading-progress="handleHomeLoadingProgress"
@map-ready="handleHomeMapReady"
@module-stats="handleModuleStats"
@ -29,7 +30,11 @@
v-show="activeModule.key === 'forest-grass-wet'"
class="platform-module-pane"
>
<forest-grass-wet :show-loading="false" @topic-stats="handleModuleStats" />
<forest-grass-wet
:active="activeModule.key === 'forest-grass-wet'"
:show-loading="false"
@topic-stats="handleModuleStats"
/>
</div>
<div
v-if="renderedModules['ecological-protection']"
@ -37,6 +42,7 @@
class="platform-module-pane"
>
<ecological-protection
:active="activeModule.key === 'ecological-protection'"
:active-view="activeEcologyView"
:show-loading="false"
@module-stats="handleModuleStats"
@ -49,6 +55,7 @@
class="platform-module-pane"
>
<grass-livestock-balance
:active="activeModule.key === 'grass-livestock-balance'"
@loading-progress="handleHomeLoadingProgress"
@map-ready="handleHomeMapReady"
@module-stats="handleModuleStats"
@ -59,7 +66,10 @@
v-show="activeModule.key === 'yak-management'"
class="platform-module-pane"
>
<yak-management @module-stats="handleModuleStats" />
<yak-management
:active="activeModule.key === 'yak-management'"
@module-stats="handleModuleStats"
/>
</div>
<div
v-if="renderedModules['pasture-management']"
@ -67,6 +77,7 @@
class="platform-module-pane"
>
<pasture-management
:active="activeModule.key === 'pasture-management'"
@loading-progress="handleHomeLoadingProgress"
@map-ready="handleHomeMapReady"
@module-stats="handleModuleStats"
@ -78,6 +89,7 @@
class="platform-module-pane"
>
<yak-industry-chain
:active="activeModule.key === 'yak-industry-chain'"
@loading-progress="handleHomeLoadingProgress"
@map-ready="handleHomeMapReady"
@module-stats="handleModuleStats"
@ -88,7 +100,7 @@
v-show="activeModule.key === 'smart-management'"
class="platform-module-pane"
>
<wisdom-management />
<wisdom-management :active="activeModule.key === 'smart-management'" />
</div>
<div class="large-screen-wrap">
@ -115,7 +127,7 @@
</div>
<div
v-if="totalView.length && !['smart-management', 'grass-livestock-balance', 'yak-industry-chain'].includes(activeModule.key)"
v-if="totalView.length && !['smart-management', 'grass-livestock-balance'].includes(activeModule.key)"
class="top-count-card"
:class="{ 'is-four': totalView.length > 3 }"
>
@ -224,6 +236,8 @@ const loadingVisible = ref(true)
const loadingProgress = ref(0)
let screenAutofit = null
let loadingHideStarted = false
let pendingModuleRenderTimer = null
let pendingModuleRenderFrame = null
const loadingStartedAt = Date.now()
const minLoadingVisibleMs = 1200
const loadingTweenState = { progress: 0 }
@ -282,26 +296,32 @@ onMounted(() => {
onBeforeUnmount(() => {
screenAutofit?.off?.("#large-screen")
if (pendingModuleRenderTimer) window.clearTimeout(pendingModuleRenderTimer)
if (pendingModuleRenderFrame) window.cancelAnimationFrame(pendingModuleRenderFrame)
gsap.killTweensOf(loadingTweenState)
})
function handleMenuSelect(key) {
renderModule(key)
if (key === activeModuleKey.value) return
const alreadyRendered = Boolean(renderedModules.value[key])
activeModuleKey.value = key
if (!alreadyRendered) scheduleActiveModuleRender(key)
nextTick(resetRootScroll)
}
function handleBottomMenuSelect(item) {
if (!item?.key) return
if (activeModuleKey.value === "ecological-protection") {
handleEcologyViewChange(item.key)
nextTick(resetRootScroll)
return
}
handleMenuSelect(item.key)
}
function handleEcologyViewChange(key) {
activeEcologyView.value = ecologicalProtectionMenu.some((item) => item.key === key) ? key : "national-park"
const nextKey = ecologicalProtectionMenu.some((item) => item.key === key) ? key : "national-park"
if (nextKey === activeEcologyView.value && activeModuleKey.value === "ecological-protection") return
activeEcologyView.value = nextKey
nextTick(resetRootScroll)
}
@ -394,6 +414,30 @@ function renderModule(key) {
}
}
function scheduleActiveModuleRender(key) {
cancelActiveModuleRender()
pendingModuleRenderTimer = window.setTimeout(() => {
pendingModuleRenderTimer = null
pendingModuleRenderFrame = window.requestAnimationFrame(() => {
pendingModuleRenderFrame = null
pendingModuleRenderTimer = null
if (activeModuleKey.value !== key) return
renderModule(key)
})
}, 180)
}
function cancelActiveModuleRender() {
if (pendingModuleRenderTimer) {
window.clearTimeout(pendingModuleRenderTimer)
pendingModuleRenderTimer = null
}
if (pendingModuleRenderFrame) {
window.cancelAnimationFrame(pendingModuleRenderFrame)
pendingModuleRenderFrame = null
}
}
function resetRootScroll() {
window.scrollTo(0, 0)
document.documentElement.scrollTop = 0
@ -511,7 +555,7 @@ function handleLogout() {
}
.top-menu-mid-space {
width: 740px;
width: 720px;
height: 1px;
pointer-events: none;
}
@ -542,7 +586,7 @@ function handleLogout() {
.top-count-card {
left: 460px;
right: 460px;
top: 134px;
top: 108px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
column-gap: 28px;
@ -556,6 +600,7 @@ function handleLogout() {
.count-card {
min-width: 0;
min-height: 48px;
}
.count-card-title,
@ -593,6 +638,7 @@ function handleLogout() {
&.is-four {
left: 390px;
right: 390px;
top: 106px;
grid-template-columns: repeat(4, minmax(0, 1fr));
column-gap: 18px;

@ -117,7 +117,7 @@
</template>
<script setup>
import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref } from "vue"
import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
import {
askStatQuestion,
getLivestockBalance,
@ -133,6 +133,12 @@ const SmartChartRenderer = defineAsyncComponent({
},
})
const props = defineProps({
active: {
type: Boolean,
default: true,
},
})
const currentText = ref("")
const messages = ref([])
const loading = ref(false)
@ -161,6 +167,18 @@ onBeforeUnmount(() => {
stopStreaming()
})
watch(
() => props.active,
(active) => {
if (active) return
stopStreaming()
loading.value = false
messages.value = messages.value.map((message) =>
message.streaming ? { ...message, streaming: false, digesting: false } : message,
)
},
)
function createId() {
return `${Date.now()}-${Math.random().toString(16).slice(2)}`
}

File diff suppressed because it is too large Load Diff

@ -10,6 +10,64 @@
<div class="industry-map-scrim"></div>
<div v-if="!mapReady" class="industry-map-loading">3D地图加载中</div>
<div v-if="leftStagePanels.length" class="fgw-left industry-side-panel industry-side-panel--left">
<div class="fgw-side-stack industry-side-stack is-left">
<m-card
v-for="panel in leftStagePanels"
:key="panel.key"
class="fgw-side-card industry-side-card left-card"
:title="panel.title"
:width="sideCardWidth"
:height="panel.height"
>
<business-panel-renderer
v-if="panel.renderer === 'business'"
:panel="panel"
compact
@row-select="handleIndustryListSelect"
/>
<div v-else-if="panel.renderer === 'ledger'" class="industry-ledger-list">
<div v-for="item in panel.rows" :key="item.key" class="industry-ledger-row">
<div>
<strong>{{ item.name }}</strong>
<span>{{ item.typeName }} / {{ item.town }}</span>
</div>
<em>{{ item.valueText }}</em>
</div>
</div>
</m-card>
</div>
</div>
<div v-if="rightStagePanels.length" class="fgw-right industry-side-panel industry-side-panel--right">
<div class="fgw-side-stack industry-side-stack is-right">
<m-card
v-for="panel in rightStagePanels"
:key="panel.key"
class="fgw-side-card industry-side-card right-card"
:title="panel.title"
:width="sideCardWidth"
:height="panel.height"
>
<business-panel-renderer
v-if="panel.renderer === 'business'"
:panel="panel"
compact
@row-select="handleIndustryListSelect"
/>
<div v-else-if="panel.renderer === 'ledger'" class="industry-ledger-list">
<div v-for="item in panel.rows" :key="item.key" class="industry-ledger-row">
<div>
<strong>{{ item.name }}</strong>
<span>{{ item.typeName }} / {{ item.town }}</span>
</div>
<em>{{ item.valueText }}</em>
</div>
</div>
</m-card>
</div>
</div>
<div v-if="selectedPoint" class="industry-point-detail" :style="pointDetailStyle">
<div class="industry-point-detail__head">
<span>{{ selectedPointTitle }}</span>
@ -35,26 +93,45 @@
</template>
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue"
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
import mCard from "@/components/mCard/index.vue"
import MapScene from "@/views/forestGrassWet/map.vue"
import BusinessPanelRenderer from "@/views/forestGrassWet/components/BusinessPanelRenderer.vue"
import TopicSwitcher from "@/views/forestGrassWet/components/TopicSwitcher.vue"
import { Assets } from "@/views/forestGrassWet/assets"
import { getLayerByKey } from "@/config/layers"
import {
getStageDef,
getStagePanelConfig,
getPointExtraRows,
industryStageDefs,
getStageTopStats,
} from "./data"
const props = defineProps({
active: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(["module-stats", "loading-progress", "map-ready"])
const sideCardWidth = 398
const activeStageKey = ref("family-pasture")
const mapSceneRef = ref(null)
const mapReady = ref(false)
const selectedPoint = ref(null)
const selectedPointScreen = ref({ x: 0, y: 0 })
let mapAssets = null
let mapAssetsReady = false
let mapLoadStarted = false
let mapLoadTimer = null
let destroyed = false
const activeStage = computed(() => getStageDef(activeStageKey.value))
const activePanelConfig = computed(() => getStagePanelConfig(activeStageKey.value))
const stagePanelGroups = computed(() => normalizeStagePanelGroups(activePanelConfig.value))
const leftStagePanels = computed(() => stagePanelGroups.value.left)
const rightStagePanels = computed(() => stagePanelGroups.value.right)
const industryMapLayer = computed(() => {
const layer = getLayerByKey("yak-industry-chain", "yak-industry-points")
const legend = layer?.legends?.find((item) => item.match?.value === activeStageKey.value)
@ -81,7 +158,7 @@ const stageSwitcherTopics = computed(() =>
const mapRanking = computed(() => ({
showTownNames: true,
showBars: false,
showPointMarkers: false,
showPointMarkers: true,
title: `${activeStage.value.name}排行`,
metricLabel: activeStage.value.metricLabel,
unit: activeStage.value.unit,
@ -93,13 +170,11 @@ const selectedPointRows = computed(() => {
if (!point) return []
return [
{ label: "所属环节", value: formatDisplayText(point.stageName) },
{ label: "点位类型", value: formatDisplayText(point.categoryName) },
{ label: "主体类型", value: formatDisplayText(point.categoryName) },
{ label: "所在乡镇", value: formatDisplayText(point.town) },
{ label: "村/地址", value: formatDisplayText(point.village || point.address) },
{ label: "能力规模", value: formatCapacity(point) },
{ label: "联系人", value: formatDisplayText(point.contactName) },
{ label: "联系电话", value: formatDisplayText(point.contactPhone) },
{ label: "运行状态", value: formatDisplayText(point.status) },
...getPointExtraRows(point),
].filter((row) => row.value !== undefined && row.value !== null && String(row.value).trim() !== "")
})
const selectedPointTitle = computed(() => formatDisplayText(selectedPoint.value?.categoryName || selectedPoint.value?.stageName))
@ -128,21 +203,64 @@ const pointDetailStyle = computed(() => {
"--point-arrow-rotate": placeLeft ? "-45deg" : "135deg",
}
})
onMounted(initIndustryMap)
onMounted(() => {
emitIndustryTopStats()
initIndustryMap()
})
onBeforeUnmount(() => {
destroyed = true
if (mapLoadTimer) window.clearTimeout(mapLoadTimer)
mapSceneRef.value?.destroyMap?.()
})
watch(
() => props.active,
(active) => {
if (!mapSceneRef.value) return
if (active) {
scheduleIndustryMapLoad()
mapSceneRef.value.resume?.()
} else {
mapSceneRef.value.pause?.()
}
},
)
function handleStageSelect(key) {
if (key === activeStageKey.value) return
activeStageKey.value = key
clearSelectedPoint()
emitIndustryTopStats(key)
}
function normalizeStagePanelGroups(config = {}) {
const left = filterDisplayPanels(config.left)
const right = filterDisplayPanels(config.right)
if (right.length) {
return { left, right }
}
const panels = [...left, ...right]
if (panels.length <= 4) {
return { left: panels, right: [] }
}
return { left, right }
}
function filterDisplayPanels(panels = []) {
return panels.filter((panel) => panel?.type !== "iconMetric")
}
function emitIndustryTopStats(stageKey = activeStageKey.value) {
emit("module-stats", {
moduleKey: "yak-industry-chain",
items: getStageTopStats(stageKey),
})
}
function initIndustryMap() {
mapAssets = new Assets()
mapAssetsReady = false
emit("loading-progress", { progress: 0 })
mapAssets.instance?.on?.("onProgress", (path, itemsLoaded, itemsTotal) => {
const total = itemsTotal || 1
@ -153,29 +271,52 @@ function initIndustryMap() {
itemsTotal,
})
})
const loadMapOnce = async (attempt = 0) => {
if (destroyed) return
await nextTick()
const scene = mapSceneRef.value
scene?.loadMap?.(mapAssets)
if (scene?.canvasMap) {
mapReady.value = true
emit("loading-progress", { progress: 100 })
emit("map-ready")
scene.play?.()
return
}
if (attempt < 24) {
window.setTimeout(() => loadMapOnce(attempt + 1), 120)
return
}
mapAssets.ready?.then(() => {
mapAssetsReady = true
scheduleIndustryMapLoad()
}).catch(() => {
mapReady.value = true
emit("map-ready")
})
}
function scheduleIndustryMapLoad() {
if (destroyed || !props.active || !mapAssetsReady || mapLoadStarted) return
mapLoadStarted = true
if (mapLoadTimer) window.clearTimeout(mapLoadTimer)
mapLoadTimer = window.setTimeout(() => {
mapLoadTimer = null
loadIndustryMapOnce()
}, 600)
}
async function loadIndustryMapOnce(attempt = 0) {
if (destroyed) return
if (!props.active) {
mapLoadStarted = false
return
}
await nextTick()
if (!props.active) {
mapLoadStarted = false
return
}
mapAssets.ready?.then(loadMapOnce).catch(() => {
const scene = mapSceneRef.value
scene?.loadMap?.(mapAssets)
if (scene?.canvasMap) {
mapReady.value = true
emit("loading-progress", { progress: 100 })
emit("map-ready")
})
scene.play?.()
return
}
if (attempt < 24) {
window.setTimeout(() => loadIndustryMapOnce(attempt + 1), 120)
return
}
mapLoadStarted = false
mapReady.value = true
emit("map-ready")
}
function handleMapFeatureInfo(payload = {}) {
@ -188,12 +329,56 @@ function handleMapFeatureInfo(payload = {}) {
selectedPoint.value = null
}
async function handleIndustryListSelect(row = {}) {
if (!row.canLocate) return
const stageChanged = Boolean(row.stageKey && row.stageKey !== activeStageKey.value)
if (stageChanged) {
activeStageKey.value = row.stageKey
emitIndustryTopStats(row.stageKey)
await nextTick()
}
const point = buildListPoint(row)
selectedPoint.value = point
selectedPointScreen.value = { x: 960, y: 500 }
window.setTimeout(() => {
const focusResult = mapSceneRef.value?.focusYakIndustryPoint?.(point)
syncSelectedPointScreen(point, focusResult)
window.setTimeout(() => syncSelectedPointScreen(point), 760)
}, stageChanged ? 320 : 80)
}
function clearSelectedPoint() {
selectedPoint.value = null
const canvasMap = mapSceneRef.value?.canvasMap?.value || mapSceneRef.value?.canvasMap
canvasMap?.clearSelectedFeature?.()
}
function buildListPoint(row = {}) {
const longitude = Number(row.longitude)
const latitude = Number(row.latitude)
return {
...row,
id: row.key || row.id,
name: row.fullName || row.name,
stageKey: row.stageKey,
stageName: row.stageName || getStageDef(row.stageKey)?.name,
categoryName: row.categoryName || row.typeName,
capacity: row.value,
capacityUnit: row.unit,
longitude,
latitude,
geometry: Number.isFinite(longitude) && Number.isFinite(latitude)
? { type: "Point", coordinates: [longitude, latitude] }
: undefined,
status: row.status,
}
}
function syncSelectedPointScreen(point, focusResult = null) {
const screen = mapSceneRef.value?.getYakIndustryPointScreen?.(point) || focusResult?.screen
if (screen) selectedPointScreen.value = normalizeFeatureScreen(screen)
}
function formatCapacity(point) {
if (point.capacity === undefined || point.capacity === null || point.capacity === "") return ""
return `${point.capacity}${point.capacityUnit || ""}`
@ -257,6 +442,123 @@ function normalizeFeatureScreen(screen = {}) {
font-size: 18px;
}
.industry-side-panel {
position: absolute;
top: 156px;
bottom: 112px;
z-index: 6;
width: 398px;
perspective: 700px;
perspective-origin: 50% 50%;
pointer-events: all;
}
.industry-side-panel--left {
left: 32px;
}
.industry-side-panel--right {
right: 32px;
}
.industry-side-stack {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
gap: 12px;
transform-style: preserve-3d;
&.is-left {
transform: translate3d(0, 0, 0) rotateY(6deg);
transform-origin: left center;
}
&.is-right {
transform: translate3d(0, 0, 0) rotateY(-6deg);
transform-origin: right center;
}
}
.industry-side-card {
flex: 0 0 auto;
.m-card-bd-content {
top: 40px;
bottom: 10px;
color: #f3feff;
overflow: hidden;
}
.m-card-bd-bg {
filter: brightness(1.08) saturate(1.04);
}
.m-card-bd-bg::after {
content: "";
position: absolute;
inset: 4px 5px 6px;
z-index: 0;
pointer-events: none;
border: 1px solid rgba(48, 220, 255, 0.14);
background:
linear-gradient(180deg, rgba(17, 73, 92, 0.16), rgba(6, 29, 44, 0.14)),
linear-gradient(90deg, rgba(48, 220, 255, 0.1), transparent 42%, rgba(48, 220, 255, 0.06));
box-shadow:
inset 0 0 0 1px rgba(48, 220, 255, 0.06),
inset 0 12px 28px rgba(48, 220, 255, 0.04);
}
}
.industry-ledger-list {
display: grid;
gap: 7px;
}
.industry-ledger-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
min-height: 42px;
padding: 7px 8px;
border: 1px solid rgba(48, 220, 255, 0.1);
border-radius: 4px;
background: rgba(4, 20, 31, 0.42);
div {
min-width: 0;
}
strong,
span {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
strong {
color: rgba(241, 254, 255, 0.92);
font-size: 13px;
font-weight: 760;
}
span {
margin-top: 4px;
color: rgba(201, 237, 244, 0.58);
font-size: 11px;
}
em {
color: #ffe06b;
font-size: 12px;
font-style: normal;
font-weight: 800;
white-space: nowrap;
}
}
.industry-point-detail {
position: absolute;
z-index: 7;

@ -1,26 +1,44 @@
import {
recognitionTotal,
townRecognitionRows,
tradeDeduction,
tradeImpactRows as baseTradeImpactRows,
YAK_STOCK_TOTAL,
yakForecastRows,
} from "@/views/gdMap/livestockData"
export {
recognitionTotal,
tradeDeduction,
YAK_STOCK_TOTAL,
yakForecastRows,
}
export const expertCorrection = 1716
export const estimatedStock = recognitionTotal + expertCorrection + tradeDeduction
export const recognitionTotal = YAK_STOCK_TOTAL
export const expertCorrection = 0
export const tradeDeduction = 0
export const estimatedStock = YAK_STOCK_TOTAL
export function canonicalTownName(name) {
return name
}
export const townRows = townRecognitionRows
.map(({ name, count }) => ({ name, count }))
export function scaleTownRowsToTotal(rows = [], total = YAK_STOCK_TOTAL) {
const sourceRows = rows
.map((item) => ({
...item,
name: canonicalTownName(item.name || item.label || item.town || ""),
count: Number(item.count ?? item.value ?? item.total ?? 0),
}))
.filter((item) => item.name && Number.isFinite(item.count) && item.count >= 0)
const sourceTotal = sourceRows.reduce((sum, item) => sum + item.count, 0)
if (!sourceTotal || !total) return sourceRows.map((item) => ({ ...item, count: 0 }))
const scaledRows = sourceRows.map((item) => ({
...item,
count: Math.round((item.count / sourceTotal) * total),
}))
const delta = total - scaledRows.reduce((sum, item) => sum + item.count, 0)
if (delta && scaledRows[0]) scaledRows[0].count += delta
return scaledRows
}
export const townRows = scaleTownRowsToTotal(townRecognitionRows, estimatedStock)
.sort((a, b) => b.count - a.count)
export const structureRows = withStockCounts(
@ -40,11 +58,11 @@ export const tradeImpactRows = baseTradeImpactRows.map((item, index) => ({
}))
export const genderAgeRows = [
{ age: "≤1岁", cow: 61280, bull: 64892 },
{ age: "1-2岁", cow: 43076, bull: 43752 },
{ age: "2-4岁", cow: 148260, bull: 78240 },
{ age: "4-8岁", cow: 169420, bull: 51860 },
{ age: "8岁以上", cow: 45534, bull: 21416 },
{ age: "≤1岁", cow: 38586, bull: 40861 },
{ age: "1-2岁", cow: 27124, bull: 27549 },
{ age: "2-4岁", cow: 93355, bull: 49265 },
{ age: "4-8岁", cow: 106679, bull: 32655 },
{ age: "8岁以上", cow: 28671, bull: 13485 },
]
export const forecastPeriods = [
@ -53,10 +71,10 @@ export const forecastPeriods = [
label: "2026年底",
shortLabel: "2026",
months: 7,
stock: 781236,
births: 44763,
outflow: 21684,
correction: -21735,
stock: 462850,
births: 24680,
outflow: 19020,
correction: -1040,
structure: { adultCow: 47, otherAdult: 22, calf: 16, yearling: 11, bull: 4 },
},
{
@ -64,10 +82,10 @@ export const forecastPeriods = [
label: "2027年底",
shortLabel: "2027",
months: 19,
stock: 779428,
births: 121374,
outflow: 61286,
correction: -60552,
stock: 468920,
births: 67420,
outflow: 55180,
correction: -1550,
structure: { adultCow: 47, otherAdult: 21, calf: 17, yearling: 11, bull: 4 },
},
{
@ -75,10 +93,10 @@ export const forecastPeriods = [
label: "2028年底",
shortLabel: "2028",
months: 31,
stock: 782617,
births: 199642,
outflow: 102517,
correction: -94400,
stock: 475360,
births: 109680,
outflow: 90140,
correction: -2410,
structure: { adultCow: 46, otherAdult: 22, calf: 17, yearling: 11, bull: 4 },
},
]

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save