fix: improve V2.0 screen interactions and pasture regions

V2.0
M先生 1 month ago
parent 56d5662cab
commit b84873480f
  1. 0
      docs/assets/AlibabaPuHuiTi-14435b65-balabala-1783008015343.woff2
  2. BIN
      docs/assets/AlimamaShuHeiTi-Bold-6401fd0d-balabala-1783008015343.ttf
  3. BIN
      docs/assets/AlimamaShuHeiTi-Bold-a95023f2-balabala-1783008015343.woff
  4. BIN
      docs/assets/AlimamaShuHeiTi-Bold-fa4a258d-balabala-1783008015343.woff2
  5. 2
      docs/assets/SmartChartRenderer-0a778777-1783008015343.js
  6. 2
      docs/assets/SmartChartRenderer-1d89a60f-1782986963996.js
  7. 0
      docs/assets/SmartChartRenderer-8fd37aa6-balabala-1783008015343.css
  8. 0
      docs/assets/bg-3acb94e3-balabala-1783008015343.png
  9. 0
      docs/assets/bottom-menu-arrow-big-180b7071-balabala-1783008015343.svg
  10. 0
      docs/assets/bottom-menu-arrow-small-19304179-balabala-1783008015343.svg
  11. 0
      docs/assets/bottom-menu-bg-b465793d-balabala-1783008015343.png
  12. 0
      docs/assets/bottom-menu-btn-ee5c8064-balabala-1783008015343.png
  13. 0
      docs/assets/bottom-menu-btn-hover-df33e514-balabala-1783008015343.png
  14. 0
      docs/assets/chinaBlurLine-1a2cbc0f-balabala-1783008015343.png
  15. 0
      docs/assets/content-bottom-left-arrow-3a8a61f3-balabala-1783008015343.svg
  16. 0
      docs/assets/content-bottom-right-arrow-a3c5270d-balabala-1783008015343.svg
  17. 0
      docs/assets/content-middle-line-162bc0d5-balabala-1783008015343.svg
  18. 0
      docs/assets/focus_bg-b6803e2d-balabala-1783008015343.png
  19. 0
      docs/assets/focus_move_bg-cf41417a-balabala-1783008015343.png
  20. 0
      docs/assets/header-bg-65b38012-balabala-1783008015343.png
  21. 0
      docs/assets/huiguang-3c5dfda5-balabala-1783008015343.png
  22. 0
      docs/assets/icon1-6f7c222f-balabala-1783008015343.png
  23. 0
      docs/assets/icon2-4f77ccec-balabala-1783008015343.png
  24. 1
      docs/assets/index-09f1a743-balabala-1782986963996.css
  25. 25
      docs/assets/index-1782986963996.js
  26. 84
      docs/assets/index-1783008015343.js
  27. 2
      docs/assets/index-1a2ad4f0-balabala-1783008015343.css
  28. 0
      docs/assets/index-47d6f37a-balabala-1783008015343.css
  29. 10842
      docs/assets/index-638d0d75-1782986963996.js
  30. 2
      docs/assets/index-644d6de0-1783008015343.js
  31. 10783
      docs/assets/index-65701168-1783008015343.js
  32. 1
      docs/assets/index-ed59517a-balabala-1783008015343.css
  33. 0
      docs/assets/left-kuang-5d6bd1ae-balabala-1783008015343.svg
  34. 0
      docs/assets/live-scene-1-786cdd0a-balabala-1783008015343.png
  35. 0
      docs/assets/menu-btn-91a961cf-balabala-1783008015343.png
  36. 0
      docs/assets/menu-btn-hover-62eba0bb-balabala-1783008015343.png
  37. 0
      docs/assets/ocean-bg-8c848b8f-balabala-1783008015343.png
  38. 0
      docs/assets/pathLine2-d4134d4b-balabala-1783008015343.png
  39. 0
      docs/assets/right-kuang-b1937caf-balabala-1783008015343.svg
  40. 0
      docs/assets/ring2-a1b91808-balabala-1783008015343.png
  41. 0
      docs/assets/ring3-fe0f60d7-balabala-1783008015343.png
  42. 0
      docs/assets/ring4-f2e324d6-balabala-1783008015343.png
  43. 0
      docs/assets/rotationBorder1-2ce8e0ef-balabala-1783008015343.png
  44. 0
      docs/assets/rotationBorder2-7496b968-balabala-1783008015343.png
  45. 0
      docs/assets/saoguang-ccf84a88-balabala-1783008015343.svg
  46. 0
      docs/assets/title-zs1-c15a5ee8-balabala-1783008015343.svg
  47. 4
      docs/index.html
  48. 8
      src/assets/style/font.scss
  49. 4
      src/components/mHeader/index.vue
  50. 6
      src/config/townshipBoundaries.js
  51. 4
      src/main.js
  52. 3
      src/services/balanceApi.js
  53. 9
      src/services/homeVideoApi.js
  54. 42
      src/services/openApi.js
  55. 4
      src/utils/chartTheme.js
  56. 54
      src/utils/echartsTooltipGuard.js
  57. 14
      src/views/ecologicalProtection/data.js
  58. 4
      src/views/ecologicalProtection/index.vue
  59. 223
      src/views/forestGrassWet/components/BusinessPanelRenderer.vue
  60. 4
      src/views/forestGrassWet/index.vue
  61. 109
      src/views/forestGrassWet/map.js
  62. 35
      src/views/forestGrassWet/map.vue
  63. 20
      src/views/forestGrassWet/map/scatter.js
  64. 4
      src/views/gdMap/livestockData.js
  65. 24
      src/views/gdMap/map.js
  66. 4
      src/views/gdMap/map.vue
  67. 20
      src/views/gdMap/map/scatter.js
  68. 239
      src/views/grassLivestockBalance/data.js
  69. 235
      src/views/grassLivestockBalance/index.vue
  70. 43
      src/views/home/index.vue
  71. 2
      src/views/pastureManagement/data.js
  72. 631
      src/views/pastureManagement/index.vue
  73. 4
      src/views/platform/index.vue
  74. 5
      src/views/yakIndustryChain/data.js
  75. 2
      src/views/yakManagement/index.vue

@ -0,0 +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{e as b,l as H,x as F,M as h,o as g,c as p,a as f,t as u,g as z,b as R,u as V,n as P}from"./index-1783008015343.js";import{H as j}from"./index-65701168-1783008015343.js";const q={class:"smart-chart-header"},G={key:0},M={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"],L=b(()=>{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=b(()=>{const t=i.chart.rows||[];return i.chart.type==="bar"&&x(t)}),y=b(()=>{const t=i.chart.source||{},e=[t.mode,t.table,t.period,t.generatedAt].filter(Boolean);return e.length?e.join(" / "):""});H(()=>{F(()=>{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 h(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 h(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 h(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:P(["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)):z("",!0)]),R(V(j),{class:"smart-chart",option:L.value,autoresize:""},null,8,["option"]),y.value?(g(),p("div",M,u(y.value),1)):z("",!0)],2))}};export{J as default};

@ -1,2 +0,0 @@
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

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-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%}} .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-1783008015343.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-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}; 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,S as w,T as S,e as h,o as _,c as f,a as e,L as U,I as g,J as y,t as b,g as V,d as p,U as k}from"./index-1783008015343.js";const B={class:"login-screen"},D={class:"login-shell"},I={class:"login-field"},M={class:"login-field"},R={key:0,class:"login-error"},q=["disabled"],C={__name:"index",setup(v){const d=w(),n=S(),i=p(""),a=p(""),u=p(!1),o=p(""),r=h(()=>{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:U(l,["prevent"])},[s[4]||(s[4]=e("div",{class:"login-panel-title"},"用户登录",-1)),e("label",I,[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",R,b(o.value),1)):V("",!0),e("button",{class:"login-submit",disabled:u.value,type:"submit"},b(u.value?"登录中":"登录"),9,q)],32)])]))}},P=x(C,[["__scopeId","data-v-02c20620"]]);export{P as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

@ -14,6 +14,14 @@
font-family: "Alibaba PuHuiTi"; font-family: "Alibaba PuHuiTi";
src: url("~@/assets/font/AlibabaPuHuiTi.woff2") format("woff2"); src: url("~@/assets/font/AlibabaPuHuiTi.woff2") format("woff2");
} }
@font-face {
font-family: "Alimama ShuHeiTi";
font-weight: 700;
src: url("~@/assets/font/alimama/AlimamaShuHeiTi-Bold.woff2") format("woff2"),
url("~@/assets/font/alimama/AlimamaShuHeiTi-Bold.woff") format("woff"),
url("~@/assets/font/alimama/AlimamaShuHeiTi-Bold.ttf") format("truetype");
font-display: swap;
}
/* 在线链接服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份。 */ /* 在线链接服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份。 */
/* 在线链接服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份。 */ /* 在线链接服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份。 */
/* 在线链接服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份。 */ /* 在线链接服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份。 */

@ -94,8 +94,10 @@ defineProps({
&-title { &-title {
color: #fff; color: #fff;
font-size: 44px; font-size: 44px;
font-weight: 700;
letter-spacing: 1px; letter-spacing: 1px;
font-family: "YouSheBiaoTiHei", "阿里妈妈数黑体 Bold"; font-family: "Alimama ShuHeiTi", "阿里妈妈数黑体 Bold", sans-serif;
font-synthesis: none;
background: -webkit-linear-gradient(rgba(117, 232, 255, 1), rgba(255, 255, 255, 1)); background: -webkit-linear-gradient(rgba(117, 232, 255, 1), rgba(255, 255, 255, 1));
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;

@ -29,6 +29,10 @@ const townshipValues = {
江茸乡: 514, 江茸乡: 514,
} }
const townshipLabelCenterOverrides = {
龙日镇: [102.5608, 32.5099],
}
function parseGeoJson(raw) { function parseGeoJson(raw) {
try { try {
return JSON.parse(raw) return JSON.parse(raw)
@ -71,7 +75,7 @@ export const hongyuanTownshipCodeByName = hongyuanTownshipFeatures.reduce((resul
export const hongyuanTownshipLabelPoints = hongyuanTownshipFeatures.map((feature) => { export const hongyuanTownshipLabelPoints = hongyuanTownshipFeatures.map((feature) => {
const properties = feature.properties || {} const properties = feature.properties || {}
const name = normalizeHongyuanTownName(properties.name) const name = normalizeHongyuanTownName(properties.name)
const center = readPoint(feature) const center = townshipLabelCenterOverrides[name] || readPoint(feature)
return { return {
name, name,
code: properties.code, code: properties.code,

@ -3,5 +3,9 @@ import router from "./router"
import App from "./App.vue" import App from "./App.vue"
import "normalize.css" import "normalize.css"
import "./style.css" import "./style.css"
import { installEchartsTooltipGuard } from "./utils/echartsTooltipGuard"
installEchartsTooltipGuard()
let app = createApp(App) let app = createApp(App)
app.use(router).mount("#app") app.use(router).mount("#app")

@ -81,10 +81,9 @@ function hasMeaningfulBalanceResult(data) {
return balanceKeys.some((key) => data[key] !== undefined && data[key] !== null && data[key] !== "") return balanceKeys.some((key) => data[key] !== undefined && data[key] !== null && data[key] !== "")
} }
export async function getBalanceAdvice({ areaCode, pasturesId } = {}) { export async function getBalanceAdvice({ areaCode } = {}) {
const params = { const params = {
areaCode, areaCode,
pasturesId,
} }
const requests = [ const requests = [
() => get(JSY_API_BASE, "/ai/livestockBalance", params, { timeout: 18000 }), () => get(JSY_API_BASE, "/ai/livestockBalance", params, { timeout: 18000 }),

@ -1,6 +1,7 @@
import { normalizeResponseData, requestJson } from "./request" import { normalizeResponseData, requestJson } from "./request"
const JSY_API_BASE = import.meta.env.VITE_JSY_API_BASE || "/jsyApi" const JSY_API_BASE = import.meta.env.VITE_JSY_API_BASE || "/jsyApi"
const EZVIZ_ACCESS_TOKEN = String(import.meta.env.VITE_EZVIZ_ACCESS_TOKEN || "").trim()
const HONGYUAN_TOWN_NAMES = [ const HONGYUAN_TOWN_NAMES = [
"邛溪镇", "邛溪镇",
"刷经寺镇", "刷经寺镇",
@ -42,6 +43,10 @@ function pickValue(item, keys, fallback = "") {
return fallback return fallback
} }
function getMonitoringToken(row) {
return EZVIZ_ACCESS_TOKEN || pickValue(row, ["token", "accessToken", "access_token"])
}
function inferTownName(name = "") { function inferTownName(name = "") {
const normalizedName = String(name) const normalizedName = String(name)
const town = HONGYUAN_TOWN_NAMES.find((item) => normalizedName.includes(item.replace(/[镇乡]$/, ""))) const town = HONGYUAN_TOWN_NAMES.find((item) => normalizedName.includes(item.replace(/[镇乡]$/, "")))
@ -77,7 +82,7 @@ function normalizeMonitoring(row, index, total, validTotal) {
const name = pickValue(row, ["name", "title", "deviceName"], `视频监控${index + 1}`) const name = pickValue(row, ["name", "title", "deviceName"], `视频监控${index + 1}`)
const hdPlayUrl = pickValue(row, ["hdPlayUrl", "hd_play_url", "url"]) const hdPlayUrl = pickValue(row, ["hdPlayUrl", "hd_play_url", "url"])
const playUrl = pickValue(row, ["playUrl", "play_url"], hdPlayUrl) const playUrl = pickValue(row, ["playUrl", "play_url"], hdPlayUrl)
const token = pickValue(row, ["token", "accessToken", "access_token"]) const token = getMonitoringToken(row)
const isOnline = Boolean(token && (hdPlayUrl || playUrl)) const isOnline = Boolean(token && (hdPlayUrl || playUrl))
const onlineRate = total ? Math.round((validTotal / total) * 100) : 0 const onlineRate = total ? Math.round((validTotal / total) * 100) : 0
@ -108,6 +113,6 @@ export async function getHomeMonitorings(params = {}) {
timeout: 18000, timeout: 18000,
}).then(unwrapResponse) }).then(unwrapResponse)
const rows = Array.isArray(data) ? data : data?.rows || data?.list || [] const rows = Array.isArray(data) ? data : data?.rows || data?.list || []
const validTotal = rows.filter((row) => pickValue(row, ["token", "accessToken", "access_token"]) && pickValue(row, ["hdPlayUrl", "playUrl", "url"])).length const validTotal = rows.filter((row) => getMonitoringToken(row) && pickValue(row, ["hdPlayUrl", "playUrl", "url"])).length
return rows.map((row, index) => normalizeMonitoring(row, index, rows.length, validTotal)) return rows.map((row, index) => normalizeMonitoring(row, index, rows.length, validTotal))
} }

@ -4,6 +4,8 @@ import { getLayerByKey as getLayerByKeyForRanking } from "@/config/layers"
import { getHongyuanTownshipNameByCode, hongyuanTownshipDisplayOrder } from "@/config/townshipBoundaries" import { getHongyuanTownshipNameByCode, hongyuanTownshipDisplayOrder } from "@/config/townshipBoundaries"
const OPEN_API_BASE = import.meta.env.VITE_OPEN_API_BASE || "/openApi" const OPEN_API_BASE = import.meta.env.VITE_OPEN_API_BASE || "/openApi"
const CXPT_GRASSLAND_AREA_WAN_MU = 756.37
const CXPT_WETLAND_AREA_WAN_MU = 291.82
const HONGYUAN_TOWN_DISPLAY_ORDER = hongyuanTownshipDisplayOrder.map(normalizeTownName) const HONGYUAN_TOWN_DISPLAY_ORDER = hongyuanTownshipDisplayOrder.map(normalizeTownName)
function joinBase(path) { function joinBase(path) {
@ -115,8 +117,8 @@ function normalizeBasicGrassland(data) {
return [ return [
{ {
key: "grassland-total-area", key: "grassland-total-area",
name: "草地面积", name: "草地面积",
value: formatNumber(pickValue(overview, ["totalAreaWanMu", "total_area_wan_mu", "totalArea", "total_area", "areaWanMu"])), value: CXPT_GRASSLAND_AREA_WAN_MU,
unit: pickValue(overview, ["areaUnit", "area_unit"]) || "万亩", unit: pickValue(overview, ["areaUnit", "area_unit"]) || "万亩",
}, },
{ {
@ -142,7 +144,7 @@ function ensureCoreShape(items, topicKey) {
{ key: "woodland-public-rate", name: "公益林占比", unit: "%" }, { key: "woodland-public-rate", name: "公益林占比", unit: "%" },
], ],
grassland: [ grassland: [
{ key: "grassland-area", name: "草地面积", unit: "万亩" }, { key: "grassland-area", name: "草地面积", unit: "万亩" },
{ key: "basic-grassland", name: "基本草原", unit: "万亩" }, { key: "basic-grassland", name: "基本草原", unit: "万亩" },
{ key: "general-grassland", name: "一般草原", unit: "万亩" }, { key: "general-grassland", name: "一般草原", unit: "万亩" },
], ],
@ -159,6 +161,15 @@ function ensureCoreShape(items, topicKey) {
name: items[index]?.name || preset.name, name: items[index]?.name || preset.name,
unit: items[index]?.unit || preset.unit, unit: items[index]?.unit || preset.unit,
})) }))
if (topicKey === "wetland" && merged[0]) {
merged[0] = {
...merged[0],
key: merged[0].key || "wetland-area",
name: "湿地面积",
value: CXPT_WETLAND_AREA_WAN_MU,
unit: merged[0].unit || "万亩",
}
}
return merged.length ? merged : items return merged.length ? merged : items
} }
@ -1229,7 +1240,7 @@ function findGrasslandCoreRow(rows = [], keywords = []) {
} }
function buildGrasslandResourceRows(core = []) { function buildGrasslandResourceRows(core = []) {
const totalRow = findGrasslandCoreRow(core, ["grassland-total", "grassland-area", "草地总面积"]) const totalRow = findGrasslandCoreRow(core, ["grassland-total", "grassland-area", "草地总面积", "草地面积"])
const basicRow = findGrasslandCoreRow(core, ["basic-grassland", "基本草原"]) const basicRow = findGrasslandCoreRow(core, ["basic-grassland", "基本草原"])
const generalRow = findGrasslandCoreRow(core, ["general-grassland", "一般草原"]) const generalRow = findGrasslandCoreRow(core, ["general-grassland", "一般草原"])
const total = toNumber(totalRow?.value) const total = toNumber(totalRow?.value)
@ -1280,20 +1291,18 @@ function buildGrasslandResourceStructureRows(core = []) {
].filter((row) => row.value !== undefined)) ].filter((row) => row.value !== undefined))
} }
function buildGrasslandTopRows(core = [], yieldSummary = {}, degradationRows = [], yieldRows = []) { function buildGrasslandTopRows(core = [], yieldSummary = {}, yieldRows = []) {
const degradedRate = buildGrasslandDegradationFocusRows(degradationRows)
.find((row) => row.key === "degraded-rate")
const totalYield = toNumber(yieldSummary?.xc) ?? sumRowValues(yieldRows) const totalYield = toNumber(yieldSummary?.xc) ?? sumRowValues(yieldRows)
return [ return [
core[0], core[0],
core[1], core[1],
core[2],
{ {
key: "grassland-yield-total", key: "grassland-yield-total",
name: "鲜草产量", name: "鲜草产量",
value: formatNumber(totalYield), value: formatNumber(totalYield),
unit: yieldSummary?.unit || "万吨", unit: yieldSummary?.unit || "万吨",
}, },
degradedRate,
].filter((row) => row?.value !== undefined) ].filter((row) => row?.value !== undefined)
} }
@ -1408,13 +1417,6 @@ function buildGrasslandDegradationFocusRows(rows = []) {
unit: "万亩", unit: "万亩",
icon: "area", icon: "area",
}, },
{
key: "degraded-rate",
name: "退化率",
value: formatNumber(percentOf(degradedTotal, total)),
unit: "%",
icon: "rate",
},
{ {
key: "medium-heavy-area", key: "medium-heavy-area",
name: "中重度", name: "中重度",
@ -1435,7 +1437,7 @@ function buildGrasslandDegradationFocusRows(rows = []) {
function buildFallbackTopicStats(topicKey, layerKey) { function buildFallbackTopicStats(topicKey, layerKey) {
if (topicKey === "grassland") { if (topicKey === "grassland") {
const core = ensureCoreShape([ const core = ensureCoreShape([
{ key: "grassland-area", name: "草地面积", value: 756.43, unit: "万亩" }, { key: "grassland-area", name: "草地面积", value: CXPT_GRASSLAND_AREA_WAN_MU, unit: "万亩" },
{ key: "basic-grassland", name: "基本草原", value: 637.13, unit: "万亩" }, { key: "basic-grassland", name: "基本草原", value: 637.13, unit: "万亩" },
{ key: "general-grassland", name: "一般草原", value: 119.31, unit: "万亩" }, { key: "general-grassland", name: "一般草原", value: 119.31, unit: "万亩" },
], topicKey) ], topicKey)
@ -1527,14 +1529,14 @@ function buildFallbackTopicStats(topicKey, layerKey) {
const result = { const result = {
status: "fallback", status: "fallback",
...grasslandPanels, ...grasslandPanels,
top: buildGrasslandTopRows(core, { xc: 297.29, unit: "万吨" }, degradationRows, yieldRows), top: buildGrasslandTopRows(core, { xc: 297.29, unit: "万吨" }, yieldRows),
} }
return applyLayerPanelSelection(result, topicKey, layerKey) return applyLayerPanelSelection(result, topicKey, layerKey)
} }
if (topicKey === "wetland") { if (topicKey === "wetland") {
const core = ensureCoreShape([ const core = ensureCoreShape([
{ key: "wetland-area", name: "湿地面积", value: 291.82, unit: "万亩" }, { key: "wetland-area", name: "湿地面积", value: CXPT_WETLAND_AREA_WAN_MU, unit: "万亩" },
{ key: "wetland-yield", name: "湿地生产力(鲜草)", value: 108.84, unit: "万吨" }, { key: "wetland-yield", name: "湿地生产力(鲜草)", value: 108.84, unit: "万吨" },
{ key: "wetland-level-1-area", name: "1级湿地面积", value: 213.22, unit: "万亩" }, { key: "wetland-level-1-area", name: "1级湿地面积", value: 213.22, unit: "万亩" },
], topicKey) ], topicKey)
@ -1766,7 +1768,7 @@ function buildGrasslandDefaultPanels({
const typePanel = buildPanel("grassland-type-proportion", "草原类占比", grassTypeRows, "donut", { span: 1 }) const typePanel = buildPanel("grassland-type-proportion", "草原类占比", grassTypeRows, "donut", { span: 1 })
const edibleYieldPanel = buildPanel("grassland-edible-yield", "可食鲜草产量分布", topRows(edibleYieldRows, 10), "column", { span: 1, variant: "slim" }) const edibleYieldPanel = buildPanel("grassland-edible-yield", "可食鲜草产量分布", topRows(edibleYieldRows, 10), "column", { span: 1, variant: "slim" })
const degradationPanel = buildPanel("grassland-degradation", "草地退化统计", buildGrasslandDegradationDistributionRows(degradationRows), "structure", { span: 1 }) const degradationPanel = buildPanel("grassland-degradation", "草地退化统计", buildGrasslandDegradationDistributionRows(degradationRows), "structure", { span: 1 })
const healthPanel = buildPanel("grassland-health-proportion", "草地健康占比", healthRows, "donut", { span: 1 }) const healthPanel = buildPanel("grassland-health-proportion", "草地健康占比", healthRows, "rose", { span: 1 })
const levelPanel = buildPanel("grassland-level-proportion", "草原级占比", grassLevelRows, "donut", { span: 1 }) const levelPanel = buildPanel("grassland-level-proportion", "草原级占比", grassLevelRows, "donut", { span: 1 })
const foragePanel = buildPanel("grassland-forage-capacity", "饲草供给", forageRows, "bar", { span: 1, variant: "capacity" }) const foragePanel = buildPanel("grassland-forage-capacity", "饲草供给", forageRows, "bar", { span: 1, variant: "capacity" })
const leftPanels = diversifyPanelColumn([ const leftPanels = diversifyPanelColumn([
@ -2092,7 +2094,7 @@ export async function getTopicBusinessStats(topicKey, layerKey) {
const result = { const result = {
status: "success", status: "success",
...grasslandPanels, ...grasslandPanels,
top: buildGrasslandTopRows(core, yieldSummary, degradationRows, grassYieldRows), top: buildGrasslandTopRows(core, yieldSummary, grassYieldRows),
} }
return applyLayerPanelSelection(result, topicKey, layerKey) return applyLayerPanelSelection(result, topicKey, layerKey)
} }

@ -12,6 +12,8 @@ export const chartTextStyle = {
export function chartTooltip(formatter, overrides = {}) { export function chartTooltip(formatter, overrides = {}) {
return { return {
trigger: "item", trigger: "item",
appendToBody: true,
confine: false,
backgroundColor: "rgba(5, 31, 51, 0.96)", backgroundColor: "rgba(5, 31, 51, 0.96)",
borderColor: "rgba(0, 210, 255, 0.54)", borderColor: "rgba(0, 210, 255, 0.54)",
borderWidth: 1, borderWidth: 1,
@ -22,7 +24,7 @@ export function chartTooltip(formatter, overrides = {}) {
fontSize: 12, fontSize: 12,
lineHeight: 18, lineHeight: 18,
}, },
extraCssText: "box-shadow:0 0 18px rgba(0,210,255,.2);", extraCssText: "box-shadow:0 0 18px rgba(0,210,255,.2);z-index:2147483647!important;pointer-events:none;",
formatter, formatter,
...overrides, ...overrides,
} }

@ -0,0 +1,54 @@
import * as echarts from "echarts"
const tooltipCssText = [
"z-index:2147483647!important",
"pointer-events:none",
].join(";")
let installed = false
export function installEchartsTooltipGuard() {
if (installed) return
installed = true
echarts.registerPreprocessor((option) => {
normalizeTooltips(option)
})
}
function normalizeTooltips(target, seen = new WeakSet()) {
if (!target || typeof target !== "object") return
if (seen.has(target)) return
seen.add(target)
if (Array.isArray(target)) {
target.forEach((item) => normalizeTooltips(item, seen))
return
}
if (target.tooltip && typeof target.tooltip === "object") {
target.tooltip = normalizeTooltip(target.tooltip)
}
Object.keys(target).forEach((key) => {
const value = target[key]
if (!value || typeof value !== "object") return
normalizeTooltips(value, seen)
})
}
function normalizeTooltip(tooltip) {
return {
...tooltip,
appendToBody: tooltip.appendToBody ?? true,
confine: tooltip.confine ?? false,
extraCssText: mergeCssText(tooltip.extraCssText),
}
}
function mergeCssText(cssText = "") {
const trimmed = String(cssText || "").trim()
if (!trimmed) return tooltipCssText
if (/z-index\s*:/i.test(trimmed)) return trimmed
return `${trimmed.replace(/;$/, "")};${tooltipCssText}`
}

@ -1,3 +1,5 @@
import { hongyuanTownshipDisplayOrder } from "@/config/townshipBoundaries"
const toWanMu = (mu) => Number((Number(mu || 0) / 10000).toFixed(2)) const toWanMu = (mu) => Number((Number(mu || 0) / 10000).toFixed(2))
const sumBy = (rows, field) => rows.reduce((sum, item) => sum + Number(item[field] || 0), 0) const sumBy = (rows, field) => rows.reduce((sum, item) => sum + Number(item[field] || 0), 0)
const round = (value, digits = 2) => Number(Number(value || 0).toFixed(digits)) const round = (value, digits = 2) => Number(Number(value || 0).toFixed(digits))
@ -526,9 +528,17 @@ export const parkPanels = {
], ],
} }
const validTownRows = (restorationSummary.townships || []) const restorationTownByName = new Map(
(restorationSummary.townships || [])
.filter((item) => /[乡镇]$/.test(String(item.name || ""))) .filter((item) => /[乡镇]$/.test(String(item.name || "")))
.slice(0, 8) .map((item) => [item.name, item])
)
const validTownRows = hongyuanTownshipDisplayOrder.map((name) => ({
name,
areaWanMu: restorationTownByName.get(name)?.areaWanMu || 0,
lengthKm: restorationTownByName.get(name)?.lengthKm || 0,
projectCount: restorationTownByName.get(name)?.projectCount || 0,
}))
const restorationProjectRows = (restorationSummary.projects || []).map((item, index) => { const restorationProjectRows = (restorationSummary.projects || []).map((item, index) => {
const hasArea = Number(item.areaWanMu) > 0 const hasArea = Number(item.areaWanMu) > 0

@ -323,8 +323,8 @@ async function loadMapOnce(attempt = 0) {
mapLoadStarted = false mapLoadStarted = false
return return
} }
mapSceneRef.value?.loadMap(assets) const mapLoaded = Boolean(mapSceneRef.value?.loadMap?.(assets) || mapSceneRef.value?.hasMap?.())
if (mapSceneRef.value?.canvasMap) { if (mapLoaded) {
await hideLoading() await hideLoading()
await nextTick() await nextTick()
mapSceneRef.value?.play() mapSceneRef.value?.play()

@ -98,6 +98,64 @@
<div v-else class="business-panel-empty">暂无业务统计数据</div> <div v-else class="business-panel-empty">暂无业务统计数据</div>
</div> </div>
<div v-else-if="panelType === 'rose'" class="business-rose" :class="{ 'is-dense': pieRows.length > 5 }">
<template v-if="pieRows.length">
<div class="business-rose-visual">
<svg viewBox="0 0 150 150" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
<g class="business-rose-guides">
<line
v-for="segment in roseSegments"
:key="`guide-${segment.key}`"
:x1="segment.lineStart.x"
:y1="segment.lineStart.y"
:x2="segment.lineEnd.x"
:y2="segment.lineEnd.y"
:stroke="segment.color"
/>
</g>
<g class="business-rose-segments">
<path
v-for="segment in roseSegments"
:key="segment.key"
class="business-rose-segment"
:class="{ 'is-active': segment.index === normalizedActiveIndex }"
:d="segment.path"
:fill="segment.color"
:stroke="segment.color"
/>
</g>
<circle class="business-rose-glow" cx="75" cy="75" r="24" />
<circle class="business-rose-hole" cx="75" cy="75" r="16" />
<g class="business-rose-labels">
<text
v-for="segment in roseSegments"
:key="`label-${segment.key}`"
:x="segment.label.x"
:y="segment.label.y"
:text-anchor="segment.anchor"
>
{{ segment.item.name }}
</text>
</g>
</svg>
</div>
<div class="business-donut-legend business-rose-legend">
<div
v-for="(item, index) in pieRows"
:key="item.key || item.name"
class="business-donut-row business-rose-row"
:class="{ 'is-active': index === normalizedActiveIndex }"
>
<i :style="{ borderColor: chartColor(index), background: chartColor(index), color: chartColor(index) }"></i>
<span>{{ item.name }}</span>
<strong>{{ displayValue(item) }}</strong>
<em>{{ item.unit }}</em>
</div>
</div>
</template>
<div v-else class="business-panel-empty">暂无业务统计数据</div>
</div>
<div v-else-if="panelType === 'structure'" class="business-structure"> <div v-else-if="panelType === 'structure'" class="business-structure">
<template v-if="structureRows.length"> <template v-if="structureRows.length">
<div class="business-structure-overview"> <div class="business-structure-overview">
@ -457,7 +515,12 @@ const values = computed(() => rows.value.map((item) => numericValue(item)).filte
const maxValue = computed(() => Math.max(...values.value, 1)) const maxValue = computed(() => Math.max(...values.value, 1))
const sumValue = computed(() => values.value.reduce((sum, value) => sum + value, 0)) const sumValue = computed(() => values.value.reduce((sum, value) => sum + value, 0))
const splitRows = computed(() => rows.value.filter((item) => numericValue(item) > 0)) const splitRows = computed(() => rows.value.filter((item) => numericValue(item) > 0))
const chartRows = computed(() => rows.value.filter((item) => numericValue(item) > 0)) const chartRows = computed(() => {
if (panelType.value === "line") {
return rows.value.filter((item) => item?.value !== undefined && item?.value !== null && item?.value !== "")
}
return rows.value.filter((item) => numericValue(item) > 0)
})
const tableRows = computed(() => chartRows.value) const tableRows = computed(() => chartRows.value)
const columnUnitLabel = computed(() => { const columnUnitLabel = computed(() => {
if (panelType.value !== "column") return "" if (panelType.value !== "column") return ""
@ -544,6 +607,7 @@ const normalizedActiveIndex = computed(() => {
const activeLoopLength = computed(() => { const activeLoopLength = computed(() => {
if (panelType.value === "structure") return structureRows.value.length if (panelType.value === "structure") return structureRows.value.length
if (panelType.value === "donut") return pieRows.value.length if (panelType.value === "donut") return pieRows.value.length
if (panelType.value === "rose") return pieRows.value.length
if (panelType.value === "bar" && props.panel?.variant === "capacity") return capacitySegmentRows.value.length if (panelType.value === "bar" && props.panel?.variant === "capacity") return capacitySegmentRows.value.length
if (panelType.value === "bar") return chartRows.value.length if (panelType.value === "bar") return chartRows.value.length
if (panelType.value === "column") return chartRows.value.length if (panelType.value === "column") return chartRows.value.length
@ -587,6 +651,37 @@ const ringStyle = computed(() => {
"--ring-active-color": chartColor(normalizedActiveIndex.value), "--ring-active-color": chartColor(normalizedActiveIndex.value),
} }
}) })
const roseSegments = computed(() => {
const data = pieRows.value
const count = data.length
if (!count) return []
const max = Math.max(...data.map((item) => numericValue(item)), 1)
const step = 360 / count
const gap = Math.min(8, step * 0.16)
return data.map((item, index) => {
const value = numericValue(item)
const isActive = index === normalizedActiveIndex.value
const radius = 36 + Math.sqrt(value / max) * 31 + (isActive ? 5 : 0)
const innerRadius = 17
const start = -90 + index * step + gap / 2
const end = -90 + (index + 1) * step - gap / 2
const mid = (start + end) / 2
const lineStart = polarPoint(75, 75, radius + 3, mid)
const lineEnd = polarPoint(75, 75, radius + 15, mid)
const label = polarPoint(75, 75, radius + 20, mid)
return {
key: item.key || item.name || index,
index,
item,
color: chartColor(index),
path: describeRoseSegment(75, 75, innerRadius, radius, start, end),
lineStart,
lineEnd,
label,
anchor: Math.cos((mid * Math.PI) / 180) >= 0 ? "start" : "end",
}
})
})
onMounted(() => { onMounted(() => {
setupPanelVisibility() setupPanelVisibility()
@ -619,7 +714,7 @@ const linePointList = computed(() => {
const height = 66 const height = 66
const top = 10 const top = 10
const step = data.length > 1 ? width / (data.length - 1) : width const step = data.length > 1 ? width / (data.length - 1) : width
const lineValues = data.map((item) => numericValue(item)).filter((value) => value > 0) const lineValues = data.map((item) => numericValue(item)).filter((value) => Number.isFinite(value) && value >= 0)
const lineMinValue = lineValues.length ? Math.min(...lineValues) : 0 const lineMinValue = lineValues.length ? Math.min(...lineValues) : 0
const lineMaxValue = lineValues.length ? Math.max(...lineValues) : 1 const lineMaxValue = lineValues.length ? Math.max(...lineValues) : 1
const lineRange = Math.max(lineMaxValue - lineMinValue, lineMaxValue * 0.08, 1) const lineRange = Math.max(lineMaxValue - lineMinValue, lineMaxValue * 0.08, 1)
@ -877,6 +972,29 @@ function getPiePercent(data) {
return `${formatNumber((value / total) * 100)}%` return `${formatNumber((value / total) * 100)}%`
} }
function polarPoint(cx, cy, radius, angle) {
const radian = (angle * Math.PI) / 180
return {
x: Number((cx + radius * Math.cos(radian)).toFixed(2)),
y: Number((cy + radius * Math.sin(radian)).toFixed(2)),
}
}
function describeRoseSegment(cx, cy, innerRadius, outerRadius, startAngle, endAngle) {
const outerStart = polarPoint(cx, cy, outerRadius, startAngle)
const outerEnd = polarPoint(cx, cy, outerRadius, endAngle)
const innerEnd = polarPoint(cx, cy, innerRadius, endAngle)
const innerStart = polarPoint(cx, cy, innerRadius, startAngle)
const largeArc = endAngle - startAngle > 180 ? 1 : 0
return [
`M ${outerStart.x} ${outerStart.y}`,
`A ${outerRadius} ${outerRadius} 0 ${largeArc} 1 ${outerEnd.x} ${outerEnd.y}`,
`L ${innerEnd.x} ${innerEnd.y}`,
`A ${innerRadius} ${innerRadius} 0 ${largeArc} 0 ${innerStart.x} ${innerStart.y}`,
"Z",
].join(" ")
}
function chartColor(index) { function chartColor(index) {
return colors[index % colors.length] return colors[index % colors.length]
} }
@ -884,7 +1002,7 @@ function chartColor(index) {
function getMetricIcon(item, index) { function getMetricIcon(item, index) {
if (item?.icon) return String(item.icon).replace(/[^a-z0-9-]/gi, "") if (item?.icon) return String(item.icon).replace(/[^a-z0-9-]/gi, "")
const name = String(item?.name || "") const name = String(item?.name || "")
if (name.includes("退化率") || name.includes("利用率") || name.includes("占比")) return "rate" if (name.includes("利用率") || name.includes("占比")) return "rate"
if (name.includes("重度") || name.includes("退化")) return "risk" if (name.includes("重度") || name.includes("退化")) return "risk"
if (name.includes("可食") || name.includes("供给")) return "leaf" if (name.includes("可食") || name.includes("供给")) return "leaf"
if (name.includes("不可")) return "stack" if (name.includes("不可")) return "stack"
@ -1644,6 +1762,92 @@ function columnGradient(index) {
min-width: 0; min-width: 0;
} }
.business-rose {
display: grid;
grid-template-columns: 158px minmax(0, 1fr);
align-items: center;
gap: 4px;
height: 100%;
padding: 4px 12px 7px 8px;
box-sizing: border-box;
}
.business-rose-visual {
position: relative;
width: 158px;
height: 136px;
min-width: 0;
svg {
width: 100%;
height: 100%;
overflow: visible;
}
}
.business-rose-segment {
opacity: 0.92;
stroke-width: 1.5;
stroke-opacity: 0.5;
transform-box: fill-box;
transform-origin: center;
transition: opacity 0.3s ease, filter 0.3s ease, transform 0.3s ease;
filter:
drop-shadow(0 2px 2px rgba(0, 10, 18, 0.34))
drop-shadow(0 0 5px rgba(48, 220, 255, 0.18));
&.is-active {
opacity: 1;
transform: scale(1.035);
filter:
drop-shadow(0 3px 5px rgba(0, 10, 18, 0.42))
drop-shadow(0 0 10px rgba(36, 246, 196, 0.42));
}
}
.business-rose-guides line {
stroke-width: 1;
stroke-opacity: 0.5;
}
.business-rose-labels text {
fill: rgba(226, 252, 255, 0.68);
font-size: 8px;
font-weight: 600;
dominant-baseline: middle;
}
.business-rose-glow {
fill: rgba(36, 246, 196, 0.1);
stroke: rgba(48, 220, 255, 0.24);
stroke-width: 1;
filter: drop-shadow(0 0 8px rgba(36, 246, 196, 0.26));
}
.business-rose-hole {
fill: rgba(3, 18, 30, 0.94);
stroke: rgba(227, 253, 255, 0.22);
stroke-width: 1.2;
}
.business-rose-legend {
gap: 6px;
padding-right: 0;
}
.business-rose-row {
grid-template-columns: 13px minmax(0, 1fr) 56px 28px;
gap: 7px;
min-height: 23px;
i {
width: 10px;
height: 10px;
border-width: 1px;
box-shadow: 0 0 7px currentColor;
}
}
.business-three-pie { .business-three-pie {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -3477,10 +3681,10 @@ function columnGradient(index) {
.business-line { .business-line {
display: grid; display: grid;
grid-template-rows: minmax(0, 1fr) 28px; grid-template-rows: minmax(0, 1fr) 34px;
gap: 4px; gap: 3px;
height: 100%; height: 100%;
padding: 9px 12px 7px; padding: 7px 10px 7px;
box-sizing: border-box; box-sizing: border-box;
} }
@ -3601,19 +3805,20 @@ function columnGradient(index) {
display: grid; display: grid;
gap: 2px; gap: 2px;
min-width: 0; min-width: 0;
align-items: start;
span { span {
min-width: 0; min-width: 0;
color: rgba(255, 255, 255, 0.62); color: rgba(255, 255, 255, 0.62);
font-size: 10px; font-size: 10px;
line-height: 11px; line-height: 12px;
text-align: center; text-align: center;
overflow: hidden; overflow: visible;
text-overflow: clip; text-overflow: clip;
white-space: pre-line; white-space: pre-line;
writing-mode: horizontal-tb; writing-mode: horizontal-tb;
justify-self: center; justify-self: center;
max-height: 28px; max-height: none;
} }
} }

@ -318,8 +318,8 @@ async function loadMapOnce(attempt = 0) {
mapLoadStarted = false mapLoadStarted = false
return return
} }
mapSceneRef.value?.loadMap(assets) const mapLoaded = Boolean(mapSceneRef.value?.loadMap?.(assets) || mapSceneRef.value?.hasMap?.())
if (mapSceneRef.value?.canvasMap) { if (mapLoaded) {
await hideLoading() await hideLoading()
await nextTick() await nextTick()
mapSceneRef.value?.play() mapSceneRef.value?.play()

@ -134,7 +134,11 @@ function normalizeMapGeoJsonData(data) {
} }
function getFeaturePolygons(geometry) { function getFeaturePolygons(geometry) {
if (!geometry?.coordinates) return [] if (!geometry?.coordinates) return []
if (geometry.type === "Polygon") return [geometry.coordinates] if (geometry.type === "Polygon") {
return Array.isArray(geometry.coordinates?.[0]?.[0]?.[0])
? geometry.coordinates
: [geometry.coordinates]
}
if (geometry.type === "MultiPolygon") return geometry.coordinates if (geometry.type === "MultiPolygon") return geometry.coordinates
return [] return []
} }
@ -150,11 +154,6 @@ export class World extends Mini3d {
this.tradeNationalDepth = 0.24 this.tradeNationalDepth = 0.24
// 地图拉伸高度 // 地图拉伸高度
this.depth = 0.5 this.depth = 0.5
this.mapFocusLabelInfo = {
name: "红原县",
enName: "HONGYUAN COUNTY",
center: [102.6305459723107, 32.22],
}
// 是否点击 // 是否点击
this.clicked = false this.clicked = false
// 雾 // 雾
@ -1061,6 +1060,37 @@ export class World extends Mini3d {
const duration = view && Number.isFinite(view.duration) ? view.duration : 0.75 const duration = view && Number.isFinite(view.duration) ? view.duration : 0.75
this.animateMapCamera(position, target, duration) this.animateMapCamera(position, target, duration)
} }
ensureMapRevealed() {
if (this.mapAnimationComplete) return
this.animateTl?.progress?.(1, false)
this.animateTl?.pause?.()
this.mapAnimationComplete = true
if (this.focusMapGroup) {
this.focusMapGroup.position.set(0, 0, 0)
this.focusMapGroup.scale.set(1, 1, 1)
}
if (this.focusMapTopMaterial) this.focusMapTopMaterial.opacity = 1
if (this.focusMapSideMaterial) {
this.focusMapSideMaterial.opacity = 1
this.focusMapSideMaterial.transparent = false
}
if (this.mapGroup) this.mapGroup.visible = true
if (this.labelGroup) this.labelGroup.visible = true
if (this.flyLineGroup) this.flyLineGroup.visible = true
if (this.flyLineFocusGroup) this.flyLineFocusGroup.visible = true
if (this.scatterGroup) this.scatterGroup.visible = true
if (this.InfoPointGroup) this.InfoPointGroup.visible = this.showTownRankingPointMarkers && this.hasTownRankingRows()
this.applyLayerMapView(this.currentTopicLayer, { force: true })
this.updateYakIndustryPointMarkers(this.currentTopicLayer, this.currentVectorFeatures)
this.updateYakIndustryPointLabels(this.currentTopicLayer, this.currentVectorFeatures)
if (shouldHideMapLabels(this.currentTownRanking)) {
this.hideAllMapStatisticLabels()
return
}
this.updateVillageRankingLabels(this.currentTownRanking)
this.updateTradeFlowLayer(this.currentTownRanking)
this.updateInfoPointStats()
}
getLayerMapViewTarget(view) { getLayerMapViewTarget(view) {
if (view?.targetLngLat) { if (view?.targetLngLat) {
return this.lngLatToWorldPoint(view.targetLngLat, view.targetLocalZ ?? 0.72) return this.lngLatToWorldPoint(view.targetLngLat, view.targetLocalZ ?? 0.72)
@ -1258,15 +1288,21 @@ export class World extends Mini3d {
} catch (error) { } catch (error) {
apiError = error apiError = error
} }
if (!rows.length && layer.fallbackPath) { let features = this.buildWktLayerFeatures(rows)
if ((!rows.length || !features.length) && layer.fallbackPath) {
const fallback = await fetch(buildPublicUrl(layer.fallbackPath), { credentials: "include" }) const fallback = await fetch(buildPublicUrl(layer.fallbackPath), { credentials: "include" })
if (!fallback.ok) throw apiError || new Error(`HTTP ${fallback.status}`) if (!fallback.ok) throw apiError || new Error(`HTTP ${fallback.status}`)
const json = await fallback.json() const json = await fallback.json()
rows = Array.isArray(json?.rows) ? json.rows : [] rows = Array.isArray(json?.rows) ? json.rows : []
features = this.buildWktLayerFeatures(rows)
} }
return { return {
type: "FeatureCollection", type: "FeatureCollection",
features: rows features,
}
}
buildWktLayerFeatures(rows = []) {
return (rows || [])
.map((row) => ({ .map((row) => ({
type: "Feature", type: "Feature",
properties: { properties: {
@ -1278,10 +1314,12 @@ export class World extends Mini3d {
cdScl: row.cdScl, cdScl: row.cdScl,
sdScl: row.sdScl, sdScl: row.sdScl,
}, },
geometry: this.parseWktGeometry(row.wkt), geometry: this.parseWktGeometry(this.pickWktValue(row)),
})) }))
.filter((feature) => feature.geometry), .filter((feature) => feature.geometry)
} }
pickWktValue(row = {}) {
return row.wkt || row.WKT || row.geom || row.geometry || row.the_geom || row.theGeom || row.shape || row.Shape
} }
drawVectorTopicLayer(layer, features, options = {}) { drawVectorTopicLayer(layer, features, options = {}) {
const context = this.topicLayerContext const context = this.topicLayerContext
@ -1300,9 +1338,25 @@ export class World extends Mini3d {
const style = this.getVectorFeatureStyle(layer, feature) const style = this.getVectorFeatureStyle(layer, feature)
this.drawVectorFeature(context, feature.geometry, style) this.drawVectorFeature(context, feature.geometry, style)
}) })
this.drawNationalParkBoundaryOverlay(context, layer, sortedFeatures)
this.drawSelectedVectorOverlay(context) this.drawSelectedVectorOverlay(context)
context.restore() context.restore()
} }
drawNationalParkBoundaryOverlay(context, layer, features = []) {
if (layer?.key !== "national-park-zoning" || !features.length) return
features.forEach((feature) => {
const baseStyle = this.getVectorFeatureStyle(layer, feature)
this.drawVectorFeature(context, feature.geometry, {
...baseStyle,
fill: baseStyle.stroke,
stroke: baseStyle.stroke,
polygonAlpha: 0,
lineAlpha: 1,
lineWidth: 8,
glow: true,
})
})
}
drawSelectedVectorOverlay(context) { drawSelectedVectorOverlay(context) {
const features = this.selectedVectorFeatures || [] const features = this.selectedVectorFeatures || []
const analysisFeatures = this.analysisMatchedFeatures || [] const analysisFeatures = this.analysisMatchedFeatures || []
@ -1812,6 +1866,17 @@ export class World extends Mini3d {
marker: true, marker: true,
} }
} }
if (layer?.key === "national-park-zoning") {
return {
fill: legend.fillColor || "#30DCFF",
stroke: legend.strokeColor || legend.fillColor || "#F2FFFF",
polygonAlpha: 0.54,
lineAlpha: 1,
lineWidth: 4.8,
pointRadius: 4.5,
glow: true,
}
}
if (layer?.boundaryOnly) { if (layer?.boundaryOnly) {
return { return {
fill: legend.fillColor || "#24F6C4", fill: legend.fillColor || "#24F6C4",
@ -2844,9 +2909,12 @@ export class World extends Mini3d {
const rows = this.getTownRankingRows(ranking, Number.POSITIVE_INFINITY, false) const rows = this.getTownRankingRows(ranking, Number.POSITIVE_INFINITY, false)
const meta = this.getTownRankingMeta(ranking) const meta = this.getTownRankingMeta(ranking)
const rowByName = new Map(rows.map((row) => [normalizeTownName(row.name), row])) const rowByName = new Map(rows.map((row) => [normalizeTownName(row.name), row]))
const canShowZeroStat = Boolean(ranking && rows.length && (meta.metricLabel || meta.unit || meta.title))
this.townNameLabels.forEach((label) => { this.townNameLabels.forEach((label) => {
const townName = label.userData?.townName || label.userData?.name const townName = label.userData?.townName || label.userData?.name
const row = rowByName.get(normalizeTownName(townName)) const row = rowByName.get(normalizeTownName(townName)) || (canShowZeroStat
? { name: townName, value: 0, unit: meta.unit, metricLabel: meta.metricLabel, rank: null }
: null)
if (!row) { if (!row) {
this.clearTownNameLabelStat(label) this.clearTownNameLabelStat(label)
return return
@ -3359,14 +3427,6 @@ export class World extends Mini3d {
otherLabel.push(label) otherLabel.push(label)
townNameLabels.push(label) townNameLabels.push(label)
}) })
let mapFocusLabel = labelStyle02(
{
...this.mapFocusLabelInfo,
},
label3d,
labelGroup
)
otherLabel.push(mapFocusLabel)
this.otherLabel = otherLabel this.otherLabel = otherLabel
this.townNameLabels = townNameLabels this.townNameLabels = townNameLabels
function labelStyle01(province, label3d, labelGroup) { function labelStyle01(province, label3d, labelGroup) {
@ -3387,17 +3447,6 @@ export class World extends Mini3d {
} }
return label return label
} }
function labelStyle02(province, label3d, labelGroup) {
let label = label3d.create("", "map-label", true)
const [x, y] = self.geoProjection(province.center)
label.init(
`<div class="other-label"><span>${province.name}</span><span>${province.enName}</span></div>`,
new Vector3(x, -y, 0.4)
)
label3d.setLabelStyle(label, 0.015, "x")
label.setParent(labelGroup)
return label
}
} }
createRotateBorder() { createRotateBorder() {
let max = 13.2 let max = 13.2

@ -23,6 +23,7 @@ let resizeObserver = null;
let resizeFrame = null; let resizeFrame = null;
let resizeFallbackTimer = null; let resizeFallbackTimer = null;
let resizeCheckTimer = null; let resizeCheckTimer = null;
let introFallbackTimer = null;
let activeLayerSignature = ""; let activeLayerSignature = "";
let mapRankingSignature = ""; let mapRankingSignature = "";
let hasPlayedIntro = false; let hasPlayedIntro = false;
@ -33,7 +34,8 @@ onBeforeUnmount(() => {
destroyMap(); destroyMap();
}); });
function loadMap(assets) { function loadMap(assets) {
if (canvasMap.value || !canvasRef.value) return; if (canvasMap.value) return true;
if (!canvasRef.value) return false;
canvasMap.value = new World(canvasRef.value, assets, { canvasMap.value = new World(canvasRef.value, assets, {
onFeatureLoading: (loading) => emit("feature-loading", loading), onFeatureLoading: (loading) => emit("feature-loading", loading),
onFeatureInfo: (payload) => emit("feature-info", payload), onFeatureInfo: (payload) => emit("feature-info", payload),
@ -47,14 +49,24 @@ function loadMap(assets) {
canvasMap.value.setTownRanking(props.mapRanking); canvasMap.value.setTownRanking(props.mapRanking);
connectMapResizeObserver(); connectMapResizeObserver();
scheduleMapResize(); scheduleMapResize();
return true;
}
function hasMap() {
return Boolean(canvasMap.value);
} }
async function play() { async function play() {
if (!canvasMap.value) return; if (!canvasMap.value) return;
resume(); resume();
if (hasPlayedIntro || canvasMap.value.mapAnimationComplete) return; if (canvasMap.value.mapAnimationComplete) return;
if (hasPlayedIntro) {
canvasMap.value.animateTl?.play?.();
scheduleIntroFallback();
return;
}
hasPlayedIntro = true; hasPlayedIntro = true;
canvasMap.value.animateTl?.timeScale?.(1); // canvasMap.value.animateTl?.timeScale?.(1); //
canvasMap.value.animateTl?.play?.(); canvasMap.value.animateTl?.play?.();
scheduleIntroFallback();
} }
function resume() { function resume() {
if (!canvasMap.value) return; if (!canvasMap.value) return;
@ -65,6 +77,7 @@ function resume() {
} }
function pause() { function pause() {
if (!canvasMap.value) return; if (!canvasMap.value) return;
clearIntroFallback();
canvasMap.value.animateTl?.pause?.(); canvasMap.value.animateTl?.pause?.();
canvasMap.value.pauseScene?.(); canvasMap.value.pauseScene?.();
if (!canvasMap.value.pauseScene) canvasMap.value.time?.pause?.(); if (!canvasMap.value.pauseScene) canvasMap.value.time?.pause?.();
@ -104,10 +117,23 @@ function showAnalysisResult(payload) {
canvasMap.value?.showAnalysisResult?.(payload); canvasMap.value?.showAnalysisResult?.(payload);
} }
function destroyMap() { function destroyMap() {
clearIntroFallback();
cleanupMapResizeObserver(); cleanupMapResizeObserver();
canvasMap.value?.destroy?.(); canvasMap.value?.destroy?.();
canvasMap.value = null; canvasMap.value = null;
} }
function scheduleIntroFallback() {
clearIntroFallback();
introFallbackTimer = window.setTimeout(() => {
introFallbackTimer = null;
canvasMap.value?.ensureMapRevealed?.();
}, 5200);
}
function clearIntroFallback() {
if (!introFallbackTimer) return;
window.clearTimeout(introFallbackTimer);
introFallbackTimer = null;
}
function scheduleMapResize() { function scheduleMapResize() {
if (resizeFrame) window.cancelAnimationFrame(resizeFrame); if (resizeFrame) window.cancelAnimationFrame(resizeFrame);
resizeFrame = window.requestAnimationFrame(() => { resizeFrame = window.requestAnimationFrame(() => {
@ -188,6 +214,7 @@ watch(
); );
defineExpose({ defineExpose({
loadMap, loadMap,
hasMap,
play, play,
resume, resume,
pause, pause,
@ -887,13 +914,15 @@ function getMapRankingSignature(ranking) {
} }
.town-stat-label { .town-stat-label {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: center;
gap: 4px; gap: 4px;
min-width: 74px; min-width: 74px;
padding: 5px 8px 4px; padding: 5px 8px 4px;
border-color: rgba(126, 251, 246, 0.3); border-color: rgba(126, 251, 246, 0.3);
background: linear-gradient(180deg, rgba(4, 33, 46, 0.66), rgba(3, 20, 32, 0.56)); background: linear-gradient(180deg, rgba(4, 33, 46, 0.66), rgba(3, 20, 32, 0.56));
box-shadow: 0 0 10px rgba(48, 220, 255, 0.12); box-shadow: 0 0 10px rgba(48, 220, 255, 0.12);
text-align: center;
transform: translateY(-50%);
} }
.town-name-row { .town-name-row {
display: flex; display: flex;

@ -1,12 +1,8 @@
export default [ import { hongyuanTownshipRankingPoints } from "@/config/townshipBoundaries"
{ value: 180, lng: 102.60242339873115, lat: 32.83022561372293 },
{ value: 160, lng: 102.55651980382184, lat: 32.204097908180216 }, export default hongyuanTownshipRankingPoints.map((item) => ({
{ value: 150, lng: 102.38447320923945, lat: 32.696113484169786 }, name: item.name,
{ value: 177, lng: 102.48704663740175, lat: 32.471364980560764 }, value: item.value,
{ value: 130, lng: 102.29046083534963, lat: 32.355689518554044 }, lng: item.center?.[0],
{ value: 165, lng: 102.03268479676977, lat: 32.440590072394265 }, lat: item.center?.[1],
{ value: 165, lng: 102.71535664604494, lat: 33.12869670968013 }, }))
{ value: 132, lng: 102.77925597565002, lat: 32.73632379185147 },
{ value: 158, lng: 102.95718106156616, lat: 33.15445682575394 },
{ value: 187, lng: 103.12540730227074, lat: 33.010462261362875 },
]

@ -17,8 +17,8 @@ function scaleCountRowsToTotal(rows, total, key = "count") {
export const homeCore = { export const homeCore = {
herdsmanCount: 129, herdsmanCount: 129,
grasslandArea: 7568515, grasslandArea: 7563700,
wetArea: 2429693, wetArea: 2918200,
yakCount: YAK_STOCK_TOTAL, yakCount: YAK_STOCK_TOTAL,
projectCount: 17, projectCount: 17,
baseCount: 12, baseCount: 12,

@ -83,11 +83,6 @@ export class World extends Mini3d {
this.flyLineCenter = [102.60242339873115, 32.83022561372293] this.flyLineCenter = [102.60242339873115, 32.83022561372293]
// 地图拉伸高度 // 地图拉伸高度
this.depth = 0.5 this.depth = 0.5
this.mapFocusLabelInfo = {
name: "红原县",
enName: "HONGYUAN COUNTY",
center: [102.6305459723107, 32.08],
}
// 是否点击 // 是否点击
this.clicked = false this.clicked = false
this.sceneMode = options.sceneMode || "default" this.sceneMode = options.sceneMode || "default"
@ -937,13 +932,6 @@ export class World extends Mini3d {
let label = labelStyle01(province, label3d, labelGroup) let label = labelStyle01(province, label3d, labelGroup)
otherLabel.push(label) otherLabel.push(label)
}) })
let mapFocusLabel = labelStyle02(
{
...this.mapFocusLabelInfo,
},
label3d,
labelGroup
)
let iconLabel1 = labelStyle03( let iconLabel1 = labelStyle03(
{ {
icon: labelIcon, icon: labelIcon,
@ -966,7 +954,6 @@ export class World extends Mini3d {
label3d, label3d,
labelGroup labelGroup
) )
otherLabel.push(mapFocusLabel)
otherLabel.push(iconLabel1) otherLabel.push(iconLabel1)
otherLabel.push(iconLabel2) otherLabel.push(iconLabel2)
this.otherLabel = otherLabel this.otherLabel = otherLabel
@ -981,17 +968,6 @@ export class World extends Mini3d {
label.setParent(labelGroup) label.setParent(labelGroup)
return label return label
} }
function labelStyle02(province, label3d, labelGroup) {
let label = label3d.create("", "map-label", false)
const [x, y] = self.geoProjection(province.center)
label.init(
`<div class="other-label"><span>${province.name}</span><span>${province.enName}</span></div>`,
new Vector3(x, -y, 0.4)
)
label3d.setLabelStyle(label, 0.015, "x")
label.setParent(labelGroup)
return label
}
function labelStyle03(data, label3d, labelGroup) { function labelStyle03(data, label3d, labelGroup) {
let label = label3d.create("", `decoration-label ${data.reflect ? " reflect" : ""}`, false) let label = label3d.create("", `decoration-label ${data.reflect ? " reflect" : ""}`, false)
const [x, y] = self.geoProjection(data.center) const [x, y] = self.geoProjection(data.center)

@ -157,7 +157,7 @@ watch(
} }
.provinces-label { .provinces-label {
&-wrap { &-wrap {
transform: translate(50%, 200%); transform: translate(-50%, -50%);
opacity: 0; opacity: 0;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -226,6 +226,8 @@ watch(
box-shadow: 0 0 14px rgba(48, 220, 255, 0.2); box-shadow: 0 0 14px rgba(48, 220, 255, 0.2);
will-change: transform; will-change: transform;
white-space: nowrap; white-space: nowrap;
text-align: center;
transform: translateY(-50%);
} }
&.blur { &.blur {

@ -1,12 +1,8 @@
export default [ import { hongyuanTownshipRankingPoints } from "@/config/townshipBoundaries"
{ value: 180, lng: 102.60242339873115, lat: 32.83022561372293 },
{ value: 160, lng: 102.55651980382184, lat: 32.204097908180216 }, export default hongyuanTownshipRankingPoints.map((item) => ({
{ value: 150, lng: 102.38447320923945, lat: 32.696113484169786 }, name: item.name,
{ value: 177, lng: 102.48704663740175, lat: 32.471364980560764 }, value: item.value,
{ value: 130, lng: 102.29046083534963, lat: 32.355689518554044 }, lng: item.center?.[0],
{ value: 165, lng: 102.03268479676977, lat: 32.440590072394265 }, lat: item.center?.[1],
{ value: 165, lng: 102.71535664604494, lat: 33.12869670968013 }, }))
{ value: 132, lng: 102.77925597565002, lat: 32.73632379185147 },
{ value: 158, lng: 102.95718106156616, lat: 33.15445682575394 },
{ value: 187, lng: 103.12540730227074, lat: 33.010462261362875 },
]

@ -1,5 +1,4 @@
import { grasslandYakRows } from "@/views/gdMap/livestockData" import { grasslandYakRows } from "@/views/gdMap/livestockData"
import { pastureListRows } from "@/views/pastureManagement/data"
import { getHongyuanTownshipCodeByName } from "@/config/townshipBoundaries" import { getHongyuanTownshipCodeByName } from "@/config/townshipBoundaries"
export const BALANCE_CALCULATION_PARAMS = { export const BALANCE_CALCULATION_PARAMS = {
@ -60,26 +59,8 @@ export const fieldMap = {
advice: ["advice", "balanceAdvice", "balance_advice", "balanceAdivce", "balance_adivce"], advice: ["advice", "balanceAdvice", "balance_advice", "balanceAdivce", "balance_adivce"],
year: ["year"], year: ["year"],
areaName: ["areaName", "area_name", "name"], areaName: ["areaName", "area_name", "name"],
pasturesName: ["pasturesName", "pastures_name", "pastureName", "muhuName", "name"],
} }
export const pastureAreaKeys = [
"grasslandArea",
"grassland_area",
"acreage",
"areaRange",
"area_range",
"area",
"cdAcreage",
"cd_acreage",
"totalAcreage",
"total_acreage",
"grasslandAcreage",
"grassland_acreage",
"pastureArea",
"pasture_area",
]
const round = (value, decimals = 2) => { const round = (value, decimals = 2) => {
const factor = 10 ** decimals const factor = 10 ** decimals
return Math.round((Number(value) || 0) * factor) / factor return Math.round((Number(value) || 0) * factor) / factor
@ -150,7 +131,6 @@ export function buildBalanceRow({
type, type,
name, name,
areaCode, areaCode,
pasturesId,
areaRange, areaRange,
yakNumber, yakNumber,
location, location,
@ -170,7 +150,6 @@ export function buildBalanceRow({
type, type,
name, name,
areaCode, areaCode,
pasturesId,
areaRange: round(areaRange, 0), areaRange: round(areaRange, 0),
yakNumber: round(yakNumber, 0), yakNumber: round(yakNumber, 0),
bearingCapacity, bearingCapacity,
@ -221,27 +200,13 @@ export const countyBalanceRow = {
year: "2026", year: "2026",
} }
export const balancePastureRows = sortBalancePastureRows(pastureListRows.map((item) => buildBalanceRow({
key: item.id || item.key || item.name,
type: "pasture",
name: item.name,
areaCode: getHongyuanTownshipCodeByName(item.township),
pasturesId: item.id,
areaRange: item.grasslandArea,
yakNumber: item.yakCount,
location: item.location,
township: item.township,
hasBoundary: item.hasBoundary || item.canLocate || hasGrasslandGeometry(item),
source: item,
})))
export const balanceFallbackRows = [ export const balanceFallbackRows = [
countyBalanceRow, countyBalanceRow,
...balanceTownRows, ...balanceTownRows,
...balancePastureRows,
] ]
export function normalizeBalanceResult(source = {}, fallback = {}) { export function normalizeBalanceResult(source = {}, fallback = {}) {
const hasSourceResult = source && typeof source === "object" && Object.keys(source).length > 0
const rawAreaRange = pickValue(source, fieldMap.areaRange) const rawAreaRange = pickValue(source, fieldMap.areaRange)
const rawYakNumber = pickValue(source, fieldMap.yakNumber) const rawYakNumber = pickValue(source, fieldMap.yakNumber)
const rawBearingCapacity = pickValue(source, fieldMap.bearingCapacity) const rawBearingCapacity = pickValue(source, fieldMap.bearingCapacity)
@ -257,28 +222,35 @@ export function normalizeBalanceResult(source = {}, fallback = {}) {
const fallbackLivestock = toNumber(fallback.livestockCapacity) const fallbackLivestock = toNumber(fallback.livestockCapacity)
const sourceHasArea = rawAreaRange !== undefined && rawAreaRange !== null && rawAreaRange !== "" const sourceHasArea = rawAreaRange !== undefined && rawAreaRange !== null && rawAreaRange !== ""
const sourceHasYakNumber = rawYakNumber !== undefined && rawYakNumber !== null && rawYakNumber !== "" const sourceHasYakNumber = rawYakNumber !== undefined && rawYakNumber !== null && rawYakNumber !== ""
const livestockCapacity = toNumber(rawLivestockCapacity) const sourceLivestock = toNumber(rawLivestockCapacity) ?? legacyLivestockCapacity
?? legacyLivestockCapacity const sourceBearing = toNumber(rawBearingCapacity)
?? (sourceHasYakNumber ? calculateLivestockCapacity(yakNumber) : fallbackLivestock) const livestockCapacity = hasSourceResult
?? calculateLivestockCapacity(yakNumber) ? sourceLivestock
const bearingCapacity = toNumber(rawBearingCapacity) ?? inferLivestockCapacity(sourceBearing, directRate)
?? fallbackLivestock
?? inferLivestockCapacity(fallbackBearing, directRate)
?? (sourceHasYakNumber ? calculateLivestockCapacity(yakNumber) : null)
: fallbackLivestock ?? (sourceHasYakNumber ? calculateLivestockCapacity(yakNumber) : null)
const bearingCapacity = hasSourceResult
? sourceBearing
?? inferBearingCapacity(livestockCapacity, directRate) ?? inferBearingCapacity(livestockCapacity, directRate)
?? (sourceHasArea ? calculateBearingCapacity(areaRange) : fallbackBearing) ?? fallbackBearing
?? calculateBearingCapacity(areaRange) ?? (sourceHasArea ? calculateBearingCapacity(areaRange) : null)
: fallbackBearing ?? (sourceHasArea ? calculateBearingCapacity(areaRange) : null)
const overloadRate = directRate !== null const overloadRate = directRate !== null
? round(directRate, 1) ? round(directRate, 1)
: bearingCapacity : bearingCapacity
&& livestockCapacity !== null
? round(((livestockCapacity - bearingCapacity) / bearingCapacity) * 100, 1) ? round(((livestockCapacity - bearingCapacity) / bearingCapacity) * 100, 1)
: 0 : null
const risk = getRiskLevel(overloadRate) const risk = getRiskLevel(overloadRate)
return { return {
...fallback, ...fallback,
...source, ...source,
key: fallback.key || source.key || source.id || source.areaCode || source.pasturesId || fallback.name, key: fallback.key || source.key || source.id || source.areaCode || fallback.name,
type: fallback.type || source.type || (source.pasturesId ? "pasture" : "town"), type: fallback.type || source.type || "town",
name: pickValue(source, [...fieldMap.pasturesName, ...fieldMap.areaName], fallback.name || "核算对象"), name: pickValue(source, fieldMap.areaName, fallback.name || "核算对象"),
areaCode: source.areaCode || source.area_code || fallback.areaCode, areaCode: source.areaCode || source.area_code || fallback.areaCode,
pasturesId: source.pasturesId || source.pastures_id || fallback.pasturesId,
areaRange, areaRange,
yakNumber, yakNumber,
bearingCapacity, bearingCapacity,
@ -287,96 +259,10 @@ export function normalizeBalanceResult(source = {}, fallback = {}) {
level: pickValue(source, fieldMap.level, risk.label), level: pickValue(source, fieldMap.level, risk.label),
advice: pickValue(source, fieldMap.advice, fallback.advice || risk.actions.join(";")), advice: pickValue(source, fieldMap.advice, fallback.advice || risk.actions.join(";")),
year: pickValue(source, fieldMap.year, fallback.year || "当前年度"), year: pickValue(source, fieldMap.year, fallback.year || "当前年度"),
hasBoundary: Boolean(source.hasBoundary ?? source.has_boundary ?? source.canLocate ?? fallback.hasBoundary),
boundaryCount: toNumber(source.boundaryCount ?? source.boundary_count, fallback.boundaryCount ?? 0),
grasslands: Array.isArray(source.grasslands) ? source.grasslands : fallback.grasslands,
source: source.raw || source.source || fallback.source, source: source.raw || source.source || fallback.source,
} }
} }
export function buildBalancePastureRows(rows = [], boundaryById = new Map()) {
return (Array.isArray(rows) ? rows : [])
.map((item, index) => normalizePastureTargetRow(item, index, boundaryById))
.filter(Boolean)
.sort(compareBalancePastureRows)
}
export function sortBalancePastureRows(rows = []) {
return [...(Array.isArray(rows) ? rows : [])].sort(compareBalancePastureRows)
}
export function compareBalancePastureRows(a, b) {
const boundaryA = hasPastureBoundary(a) ? 1 : 0
const boundaryB = hasPastureBoundary(b) ? 1 : 0
if (boundaryA !== boundaryB) return boundaryB - boundaryA
const areaA = pickPastureArea(a)
const areaB = pickPastureArea(b)
const hasAreaA = areaA > 0 ? 1 : 0
const hasAreaB = areaB > 0 ? 1 : 0
if (hasAreaA !== hasAreaB) return hasAreaB - hasAreaA
if (areaA !== areaB) return areaB - areaA
return String(a?.name || "").localeCompare(String(b?.name || ""), "zh-CN")
}
export function hasPastureBoundary(item = {}) {
if (item?.hasBoundary || item?.has_boundary || item?.canLocate || item?.hasGeometry || item?.has_geometry) return true
if ((Number(item?.boundaryCount) || Number(item?.boundary_count) || 0) > 0) return true
if (Array.isArray(item?.boundaryList) && item.boundaryList.length) return true
if (Array.isArray(item?.geometryList) && item.geometryList.length) return true
if (Array.isArray(item?.features) && item.features.length) return true
if (Array.isArray(item?.grasslands) && item.grasslands.some(hasGrasslandGeometry)) return true
if (Array.isArray(item?.boundaryRows) && item.boundaryRows.some(hasGrasslandGeometry)) return true
return hasGrasslandGeometry(item) || normalizeRowGrasslands(item).some(hasGrasslandGeometry)
}
export function pickPastureArea(item = {}) {
const values = pastureAreaKeys.map((key) => parsePastureArea(item?.[key]))
const positive = values.find((value) => value > 0)
return positive || values.find((value) => value === 0) || 0
}
export function parsePastureArea(value) {
if (typeof value === "number") return Number.isFinite(value) ? value : 0
const text = String(value ?? "").trim()
if (!text || text === "--") return 0
const match = text.replace(/,/g, "").match(/-?\d+(?:\.\d+)?/)
if (!match) return 0
const number = Number(match[0])
if (!Number.isFinite(number)) return 0
return text.includes("万") ? number * 10000 : number
}
function normalizePastureTargetRow(item = {}, index = 0, boundaryById = new Map()) {
const pasturesId = pickValue(item, ["pasturesId", "pastures_id", "pastureId", "pasture_id", "muhuId", "id", "value"])
const name = pickValue(item, [...fieldMap.pasturesName, "herdsmanName", "ownerName", "label"], pasturesId || "")
if (!name && !pasturesId) return null
const township = pickValue(item, ["township", "townName", "areaName", "area_name", "zoneName", "address"])
const grasslands = normalizeRowGrasslands(item)
const sourceBoundaryRows = grasslands.filter(hasGrasslandGeometry)
const boundaryInfo = pasturesId ? boundaryById.get(String(pasturesId)) : null
const boundaryRows = boundaryInfo?.rows || []
const grasslandRows = boundaryRows.length ? boundaryRows : grasslands
const areaRange = pickPastureArea(item) || sumPastureGrasslandArea(grasslandRows)
const hasBoundary = Boolean(boundaryInfo?.hasBoundary || sourceBoundaryRows.length || hasGrasslandGeometry(item))
return buildBalanceRow({
key: pasturesId ? `pasture-${pasturesId}` : `pasture-${name}-${index}`,
type: "pasture",
name,
areaCode: item.areaCode || item.area_code || getHongyuanTownshipCodeByName(township),
pasturesId,
areaRange,
yakNumber: toNumber(pickValue(item, fieldMap.yakNumber), 0),
location: item.location,
township,
hasBoundary,
boundaryCount: boundaryRows.length || sourceBoundaryRows.length,
grasslands: boundaryRows.length ? boundaryRows : sourceBoundaryRows.length ? sourceBoundaryRows : grasslands,
source: item,
})
}
function getLegacyLivestockCapacity(source = {}, rawLivestockCapacity, fallbackYakNumber = null) { function getLegacyLivestockCapacity(source = {}, rawLivestockCapacity, fallbackYakNumber = null) {
if (!source || typeof source !== "object") return null if (!source || typeof source !== "object") return null
if (rawLivestockCapacity !== undefined && rawLivestockCapacity !== null && rawLivestockCapacity !== "") return null if (rawLivestockCapacity !== undefined && rawLivestockCapacity !== null && rawLivestockCapacity !== "") return null
@ -399,57 +285,29 @@ function inferBearingCapacity(livestockCapacity, overloadRate) {
return Math.round(livestock / (1 + rate / 100)) return Math.round(livestock / (1 + rate / 100))
} }
function sumPastureGrasslandArea(rows = []) { function inferLivestockCapacity(bearingCapacity, overloadRate) {
return (Array.isArray(rows) ? rows : []).reduce((sum, row) => { const bearing = toNumber(bearingCapacity)
const area = pickPastureArea(row) const rate = toNumber(overloadRate)
return sum + (area > 0 ? area : 0) if (bearing === null || rate === null || rate <= -100) return null
}, 0) return Math.round(bearing * (1 + rate / 100))
}
function normalizeRowGrasslands(item = {}) {
const rows = []
const nestedKeys = ["grasslands", "grasslandList", "grasslandRows", "boundaryRows", "boundaryList", "geometryList", "landList", "plots", "features"]
nestedKeys.forEach((key) => {
const value = item?.[key]
if (Array.isArray(value)) rows.push(...value)
else if (Array.isArray(value?.rows)) rows.push(...value.rows)
else if (Array.isArray(value?.list)) rows.push(...value.list)
else if (Array.isArray(value?.data)) rows.push(...value.data)
})
const wkt = pickValue(item, ["wkt", "geo", "geom", "geometryWkt", "shape"])
if (wkt || item?.geometry) {
rows.push({
...item,
id: item?.grasslandId || item?.gid || item?.id,
pastureId: item?.pastureId || item?.pasturesId || item?.pastures_id || item?.muhuId || item?.id,
name: item?.grasslandName || item?.name,
wkt,
geometry: item?.geometry,
})
}
return rows.filter(Boolean)
}
function hasGrasslandGeometry(item = {}) {
if (typeof item === "string") return Boolean(item.trim())
if (item?.type === "Feature") return true
if (item?.type && item?.coordinates) return true
if (item?.geometry?.type || item?.geometry?.coordinates || typeof item?.geometry === "string") return true
return Boolean(pickValue(item, ["wkt", "geo", "geom", "geometryWkt", "shape"]))
} }
export function deriveBalance(row = {}) { export function deriveBalance(row = {}) {
const bearing = toNumber(row.bearingCapacity, 0) const bearing = toNumber(row.bearingCapacity)
const livestock = toNumber(row.livestockCapacity, 0) const livestock = toNumber(row.livestockCapacity)
const areaRange = toNumber(row.areaRange, 0) const areaRange = toNumber(row.areaRange, 0)
const yakNumber = toNumber(row.yakNumber, 0) const yakNumber = toNumber(row.yakNumber, 0)
const overloadRate = toNumber(row.overloadRate, bearing ? ((livestock - bearing) / bearing) * 100 : 0) const overloadRate = toNumber(
const delta = livestock - bearing row.overloadRate,
const usageRate = bearing ? round((livestock / bearing) * 100, 1) : 0 bearing && livestock !== null ? ((livestock - bearing) / bearing) * 100 : null
)
const delta = bearing !== null && livestock !== null ? livestock - bearing : null
const usageRate = bearing && livestock !== null ? round((livestock / bearing) * 100, 1) : null
const risk = getRiskLevel(overloadRate) const risk = getRiskLevel(overloadRate)
const yakSheepUnit = Math.round(yakNumber * BALANCE_CALCULATION_PARAMS.yakToSheepUnit) const yakSheepUnit = Math.round(yakNumber * BALANCE_CALCULATION_PARAMS.yakToSheepUnit)
const pressureFactor = yakSheepUnit ? round(livestock / yakSheepUnit, 2) : 1 const pressureFactor = yakSheepUnit && livestock !== null ? round(livestock / yakSheepUnit, 2) : null
const bearingCoefficient = areaRange ? round(bearing / areaRange, 4) : BALANCE_CALCULATION_PARAMS.grasslandCapacityPerMu const bearingCoefficient = areaRange && bearing !== null ? round(bearing / areaRange, 4) : null
const maxCapacity = Math.max(bearing || 0, livestock || 0, 1)
return { return {
bearing, bearing,
livestock, livestock,
@ -458,12 +316,12 @@ export function deriveBalance(row = {}) {
yakSheepUnit, yakSheepUnit,
pressureFactor, pressureFactor,
bearingCoefficient, bearingCoefficient,
overloadRate: round(overloadRate, 1), overloadRate: overloadRate === null ? null : round(overloadRate, 1),
delta, delta,
usageRate, usageRate,
risk, risk,
bearingPercent: Math.max(4, Math.min(100, round((bearing / Math.max(bearing, livestock, 1)) * 100, 1))), bearingPercent: bearing === null ? 4 : Math.max(4, Math.min(100, round((bearing / maxCapacity) * 100, 1))),
livestockPercent: Math.max(4, Math.min(100, round((livestock / Math.max(bearing, livestock, 1)) * 100, 1))), livestockPercent: livestock === null ? 4 : Math.max(4, Math.min(100, round((livestock / maxCapacity) * 100, 1))),
} }
} }
@ -482,22 +340,3 @@ export function splitAdvice(row = {}, risk = getRiskLevel(row.overloadRate)) {
.filter((item, index, array) => array.indexOf(item) === index) .filter((item, index, array) => array.indexOf(item) === index)
.slice(0, 4) .slice(0, 4)
} }
export function normalizeTargetRows(rows = []) {
return rows
.map((item, index) => {
const name = pickValue(item, ["name", "label", "areaName", "area_name", "pasturesName", "pastures_name"], "")
const areaCode = pickValue(item, ["areaCode", "area_code", "code"])
const pasturesId = pickValue(item, ["pasturesId", "pastures_id", "id", "value"])
if (!name && !areaCode && !pasturesId) return null
return {
key: pasturesId ? `pasture-${pasturesId}` : `area-${areaCode || index}`,
type: pasturesId && item.key !== "areaCode" ? "pasture" : "town",
name: name || areaCode || pasturesId,
areaCode,
pasturesId: item.key === "pasturesId" ? pasturesId : undefined,
raw: item,
}
})
.filter(Boolean)
}

@ -70,7 +70,7 @@
<div class="balance-running-title"> <div class="balance-running-title">
<span>{{ activeBalance.name }}</span> <span>{{ activeBalance.name }}</span>
<strong>{{ currentProcessStep.desc }}</strong> <strong>{{ currentProcessStep.desc }}</strong>
<small>{{ currentProcessStep.expression }}</small> <small>{{ currentProcessStep.plan }}</small>
<div class="balance-progress-line"> <div class="balance-progress-line">
<i :style="{ width: `${analysisProgress}%` }"></i> <i :style="{ width: `${analysisProgress}%` }"></i>
</div> </div>
@ -87,9 +87,8 @@
<span>{{ step.index }}</span> <span>{{ step.index }}</span>
<div> <div>
<strong>{{ step.name }}</strong> <strong>{{ step.name }}</strong>
<p>{{ shouldRevealProcessStep(index) ? step.expression : step.pending }}</p> <p>{{ shouldRevealProcessStep(index) ? step.plan : step.pending }}</p>
</div> </div>
<em>{{ shouldRevealProcessStep(index) ? step.result : "等待" }}</em>
</div> </div>
</div> </div>
</section> </section>
@ -138,7 +137,7 @@
<div class="balance-verdict-card" :class="`is-${activeDerived.risk.key}`"> <div class="balance-verdict-card" :class="`is-${activeDerived.risk.key}`">
<span>分析结论</span> <span>分析结论</span>
<strong>{{ activeDerived.risk.label }}</strong> <strong>{{ activeDerived.risk.label }}</strong>
<small>{{ activeDerived.delta > 0 ? "需调控载畜量" : "承载能力可覆盖" }}</small> <small>{{ verdictHint }}</small>
</div> </div>
</div> </div>
@ -188,7 +187,7 @@
<div class="balance-decision"> <div class="balance-decision">
<div class="balance-section-title"> <div class="balance-section-title">
<span>结论与建议</span> <span>结论与建议</span>
<small>{{ activeDerived.delta > 0 ? "需要调控" : "保持监测" }}</small> <small>{{ decisionHint }}</small>
</div> </div>
<div class="balance-conclusion"> <div class="balance-conclusion">
<strong>{{ activeDerived.risk.description }}</strong> <strong>{{ activeDerived.risk.description }}</strong>
@ -217,19 +216,15 @@
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue" import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"
import mCard from "@/components/mCard/index.vue" import mCard from "@/components/mCard/index.vue"
import { getAreaChildren, getBalanceAdvice } from "@/services/balanceApi" import { getAreaChildren, getBalanceAdvice } from "@/services/balanceApi"
import { getMuhuList, getPastureGrasslandsById, getPastureList } from "@/services/pastureApi"
import { import {
BALANCE_CALCULATION_PARAMS, BALANCE_CALCULATION_PARAMS,
balanceFallbackRows, balanceFallbackRows,
buildBalanceRow, buildBalanceRow,
buildBalancePastureRows,
deriveBalance, deriveBalance,
formatNumber, formatNumber,
formatPercent, formatPercent,
hasPastureBoundary,
normalizeBalanceResult, normalizeBalanceResult,
pickValue, pickValue,
sortBalancePastureRows,
splitAdvice, splitAdvice,
toNumber, toNumber,
} from "./data" } from "./data"
@ -246,39 +241,34 @@ const selectorCardWidth = 326
const selectorCardHeight = 720 const selectorCardHeight = 720
const sheetCardWidth = 1310 const sheetCardWidth = 1310
const mainCardHeight = 820 const mainCardHeight = 820
const MIN_ANALYSIS_DURATION = 4200 const MIN_ANALYSIS_DURATION = 6200
const activeScope = ref("county") const activeScope = ref("county")
const activeKey = ref("") const activeKey = ref("")
const analysisState = ref("idle") const analysisState = ref("idle")
const analysisProgress = ref(0) const analysisProgress = ref(0)
const processStageIndex = ref(0) const processStageIndex = ref(0)
const analysisError = ref("")
const apiResults = ref({}) const apiResults = ref({})
const fallbackPastureRows = sortBalancePastureRows(balanceFallbackRows.filter((item) => item.type === "pasture"))
const villageTargetRows = ref([]) const villageTargetRows = ref([])
const pastureTargetRows = ref(fallbackPastureRows)
let destroyed = false let destroyed = false
let requestSerial = 0 let requestSerial = 0
let analysisTimer = null let analysisTimer = null
let villageLoadSerial = 0 let villageLoadSerial = 0
let pastureLoadSerial = 0
let activeAnalysisRequest = null let activeAnalysisRequest = null
const balanceRows = computed(() => const balanceRows = computed(() =>
[ [
...balanceFallbackRows.filter((row) => row.type !== "pasture"), ...balanceFallbackRows,
...villageTargetRows.value, ...villageTargetRows.value,
...pastureTargetRows.value,
].map((row) => normalizeBalanceResult(apiResults.value[row.key], row)) ].map((row) => normalizeBalanceResult(apiResults.value[row.key], row))
) )
const countyRows = computed(() => balanceRows.value.filter((item) => item.type === "county")) const countyRows = computed(() => balanceRows.value.filter((item) => item.type === "county"))
const townRows = computed(() => balanceRows.value.filter((item) => item.type === "town")) const townRows = computed(() => balanceRows.value.filter((item) => item.type === "town"))
const villageRows = computed(() => balanceRows.value.filter((item) => item.type === "village")) const villageRows = computed(() => balanceRows.value.filter((item) => item.type === "village"))
const pastureRows = computed(() => sortBalancePastureRows(balanceRows.value.filter((item) => item.type === "pasture")))
const visibleTargetRows = computed(() => { const visibleTargetRows = computed(() => {
if (activeScope.value === "town") return townRows.value if (activeScope.value === "town") return townRows.value
if (activeScope.value === "village") return villageRows.value if (activeScope.value === "village") return villageRows.value
if (activeScope.value === "pasture") return pastureRows.value
return countyRows.value return countyRows.value
}) })
const activeBalance = computed(() => balanceRows.value.find((item) => item.key === activeKey.value) || null) const activeBalance = computed(() => balanceRows.value.find((item) => item.key === activeKey.value) || null)
@ -290,16 +280,15 @@ const scopeOptions = computed(() => [
{ key: "county", name: "县" }, { key: "county", name: "县" },
{ key: "town", name: "镇" }, { key: "town", name: "镇" },
{ key: "village", name: "村" }, { key: "village", name: "村" },
{ key: "pasture", name: "牧户" },
]) ])
const scopeLabel = computed(() => scopeOptions.value.find((item) => item.key === activeScope.value)?.name || "县") const scopeLabel = computed(() => scopeOptions.value.find((item) => item.key === activeScope.value)?.name || "县")
const subjectMeta = computed(() => { const subjectMeta = computed(() => {
if (!activeBalance.value) return "等待选择核算对象" if (!activeBalance.value) return "等待选择核算对象"
if (activeBalance.value.pasturesId) return `牧户编号 ${activeBalance.value.pasturesId}`
if (activeBalance.value.areaCode) return `行政区划 ${activeBalance.value.areaCode}` if (activeBalance.value.areaCode) return `行政区划 ${activeBalance.value.areaCode}`
return activeBalance.value.township || "核算对象" return activeBalance.value.township || "核算对象"
}) })
const selectedStatusText = computed(() => { const selectedStatusText = computed(() => {
if (analysisError.value) return analysisError.value
if (!hasSelection.value) return "请先选择具体对象" if (!hasSelection.value) return "请先选择具体对象"
if (analysisDone.value) return "分析已完成" if (analysisDone.value) return "分析已完成"
if (analysisRunning.value) return "正在执行核算" if (analysisRunning.value) return "正在执行核算"
@ -317,15 +306,25 @@ const waitTitle = computed(() => {
return `${activeBalance.value.name} 待开始分析` return `${activeBalance.value.name} 待开始分析`
}) })
const waitDescription = computed(() => { const waitDescription = computed(() => {
if (!hasSelection.value) return "选择县、镇、村或牧户后,系统才会进入草地承载力核算。" if (analysisError.value) return analysisError.value
if (analysisRunning.value) return "正在读取核算参数、执行公式计算,并生成结论与调控建议。" if (!hasSelection.value) return "选择县、镇或村后,系统才会进入草地承载力核算。"
if (analysisRunning.value) return "正在读取草畜平衡结果,核对承载力、载畜量和超载率。"
return "点击左侧开始分析后,再展示结果、分析过程、结论和建议。" return "点击左侧开始分析后,再展示结果、分析过程、结论和建议。"
}) })
const conclusionSummary = computed(() => { const conclusionSummary = computed(() => {
const derived = activeDerived.value const derived = activeDerived.value
if (derived.delta === null) return "根据草畜平衡结果表返回的承载力、载畜量和超载率生成判定。"
const deltaName = derived.delta > 0 ? "超载量" : "承载余量" const deltaName = derived.delta > 0 ? "超载量" : "承载余量"
return `${deltaName} ${formatNumber(Math.abs(derived.delta))},承载利用率 ${formatPercent(derived.usageRate)}` return `${deltaName} ${formatNumber(Math.abs(derived.delta))},承载利用率 ${formatPercent(derived.usageRate)}`
}) })
const verdictHint = computed(() => {
if (activeDerived.value.delta === null) return "按结果表判定"
return activeDerived.value.delta > 0 ? "需调控载畜量" : "承载能力可覆盖"
})
const decisionHint = computed(() => {
if (activeDerived.value.delta === null) return "结果表口径"
return activeDerived.value.delta > 0 ? "需要调控" : "保持监测"
})
const usageAngle = computed(() => { const usageAngle = computed(() => {
const usage = Math.max(0, Math.min(130, Number(activeDerived.value.usageRate) || 0)) const usage = Math.max(0, Math.min(130, Number(activeDerived.value.usageRate) || 0))
return `${Math.round((usage / 130) * 360)}deg` return `${Math.round((usage / 130) * 360)}deg`
@ -336,25 +335,25 @@ const calculationSteps = computed(() => [
key: "productivity", key: "productivity",
index: "01", index: "01",
name: "草地生产力核算", name: "草地生产力核算",
desc: "核算草地面积与单位面积产草能力。", desc: "核定草地范围、面积和产草量等级,形成生产力基础口径。",
}, },
{ {
key: "forage", key: "forage",
index: "02", index: "02",
name: "可利用草量折算", name: "可利用草量折算",
desc: "扣除不可食部分与生态保留量,折算可利用草量。", desc: "扣除不可食草量、生态保留量和利用限制,折算可供放牧利用草量。",
}, },
{ {
key: "livestock", key: "livestock",
index: "03", index: "03",
name: "牦牛数量核算", name: "载畜量折算",
desc: "按牦牛数量与理论承载头数计算超载压力。", desc: "依据实际存栏和草畜平衡标准,统一形成载畜压力口径。",
}, },
{ {
key: "balance", key: "balance",
index: "04", index: "04",
name: "平衡判定", name: "平衡判定",
desc: "比对理论承载力与实际载畜量,判定平衡状态。", desc: "对比草地承载能力与载畜压力,输出平衡状态和调控方案。",
}, },
]) ])
@ -367,8 +366,7 @@ const analysisProcessRows = computed(() => {
name: "草地生产力核算", name: "草地生产力核算",
desc: "核算草地面积与产草能力", desc: "核算草地面积与产草能力",
pending: "等待草地参数", pending: "等待草地参数",
expression: "正在汇总草地范围、面积和生产力参数", plan: "核定草地范围、面积和产草量等级",
result: "已核算",
}, },
{ {
key: "forage", key: "forage",
@ -376,17 +374,15 @@ const analysisProcessRows = computed(() => {
name: "可利用草量折算", name: "可利用草量折算",
desc: "折算可供放牧利用草量", desc: "折算可供放牧利用草量",
pending: "等待草量折算", pending: "等待草量折算",
expression: "正在扣除不可食与生态保留部分", plan: "扣除不可食草量、生态保留量和利用限制",
result: "已折算",
}, },
{ {
key: "livestock", key: "livestock",
index: "03", index: "03",
name: "牦牛数量核算", name: "载畜量折算",
desc: "形成实际牦牛数量口径", desc: "形成实际载畜压力口径",
pending: "等待载畜折算", pending: "等待载畜折算",
expression: "正在按牦牛数量和理论承载头数核算超载率", plan: "依据实际存栏和草畜平衡标准进行折算",
result: "已折算",
}, },
{ {
key: "balance", key: "balance",
@ -394,8 +390,7 @@ const analysisProcessRows = computed(() => {
name: "平衡判定", name: "平衡判定",
desc: "生成结论与调控方向", desc: "生成结论与调控方向",
pending: "等待平衡判定", pending: "等待平衡判定",
expression: derived.delta > 0 ? "正在判定超载风险并生成调控方向" : "正在判定平衡状态并生成监测方向", plan: derived.delta > 0 ? "判定超载风险并生成调控方案" : "判定平衡状态并生成监测方案",
result: derived.risk.label,
}, },
] ]
}) })
@ -406,21 +401,21 @@ const workflowRows = computed(() => [
key: "productivity", key: "productivity",
index: "01", index: "01",
name: "生产力核算", name: "生产力核算",
desc: analysisDone.value ? "草地生产力核算" : hasSelection.value ? "等待核算" : "等待选择", desc: analysisDone.value ? "草地生产力核算" : hasSelection.value ? "等待核算" : "等待选择",
state: hasSelection.value ? "done" : "active", state: hasSelection.value ? "done" : "active",
}, },
{ {
key: "forage", key: "forage",
index: "02", index: "02",
name: "草量折算", name: "草量折算",
desc: analysisDone.value ? "可利用草量折算" : "分析后折算", desc: analysisDone.value ? "可利用草量折算" : "分析后折算",
state: analysisRunning.value ? "active" : analysisDone.value ? "done" : hasSelection.value ? "pending" : "disabled", state: analysisRunning.value ? "active" : analysisDone.value ? "done" : hasSelection.value ? "pending" : "disabled",
}, },
{ {
key: "livestock", key: "livestock",
index: "03", index: "03",
name: "载畜折算", name: "载畜折算",
desc: analysisDone.value ? "载畜压力已换算" : "等待折算", desc: analysisDone.value ? "载畜压力算" : "等待折算",
state: analysisRunning.value ? "active" : analysisDone.value ? "done" : "disabled", state: analysisRunning.value ? "active" : analysisDone.value ? "done" : "disabled",
}, },
{ {
@ -438,14 +433,12 @@ onMounted(() => {
emit("loading-progress", { progress: 100 }) emit("loading-progress", { progress: 100 })
emit("map-ready") emit("map-ready")
loadVillageTargets() loadVillageTargets()
loadPastureTargets()
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
destroyed = true destroyed = true
requestSerial += 1 requestSerial += 1
villageLoadSerial += 1 villageLoadSerial += 1
pastureLoadSerial += 1
clearAnalysisTimer() clearAnalysisTimer()
}) })
@ -455,6 +448,7 @@ watch(
analysisState.value = "idle" analysisState.value = "idle"
analysisProgress.value = 0 analysisProgress.value = 0
processStageIndex.value = 0 processStageIndex.value = 0
analysisError.value = ""
activeAnalysisRequest = null activeAnalysisRequest = null
clearAnalysisTimer() clearAnalysisTimer()
} }
@ -467,6 +461,7 @@ watch(
analysisState.value = "idle" analysisState.value = "idle"
analysisProgress.value = 0 analysisProgress.value = 0
processStageIndex.value = 0 processStageIndex.value = 0
analysisError.value = ""
activeAnalysisRequest = null activeAnalysisRequest = null
clearAnalysisTimer() clearAnalysisTimer()
}, },
@ -476,13 +471,15 @@ function handleScopeChange(scopeKey) {
activeScope.value = scopeKey activeScope.value = scopeKey
activeKey.value = "" activeKey.value = ""
analysisState.value = "idle" analysisState.value = "idle"
analysisError.value = ""
} }
function selectBalance(row) { function selectBalance(row) {
if (!row) return if (!row) return
activeScope.value = ["county", "town", "village", "pasture"].includes(row.type) ? row.type : "county" activeScope.value = ["county", "town", "village"].includes(row.type) ? row.type : "county"
activeKey.value = row.key activeKey.value = row.key
analysisState.value = "idle" analysisState.value = "idle"
analysisError.value = ""
} }
function startAnalysis() { function startAnalysis() {
@ -490,9 +487,15 @@ function startAnalysis() {
analysisState.value = "running" analysisState.value = "running"
analysisProgress.value = 0 analysisProgress.value = 0
processStageIndex.value = 0 processStageIndex.value = 0
analysisError.value = ""
clearAnalysisTimer() clearAnalysisTimer()
const startedAt = Date.now() const startedAt = Date.now()
const analysisRequest = refreshActiveBalance() let requestFailed = false
const analysisRequest = refreshActiveBalance().catch((error) => {
requestFailed = true
analysisError.value = error?.message || "未查询到该对象的草畜平衡结果"
return null
})
activeAnalysisRequest = analysisRequest activeAnalysisRequest = analysisRequest
analysisTimer = window.setInterval(() => { analysisTimer = window.setInterval(() => {
if (destroyed) return if (destroyed) return
@ -510,31 +513,36 @@ function startAnalysis() {
} }
if (destroyed || analysisState.value !== "running" || activeAnalysisRequest !== analysisRequest) return if (destroyed || analysisState.value !== "running" || activeAnalysisRequest !== analysisRequest) return
activeAnalysisRequest = null activeAnalysisRequest = null
clearAnalysisTimer()
if (requestFailed || !apiResults.value[activeBalance.value?.key]) {
analysisProgress.value = 0
processStageIndex.value = 0
analysisState.value = "idle"
return
}
analysisProgress.value = 100 analysisProgress.value = 100
processStageIndex.value = analysisProcessRows.value.length - 1 processStageIndex.value = analysisProcessRows.value.length - 1
clearAnalysisTimer()
analysisState.value = "done" analysisState.value = "done"
}) })
} }
async function refreshActiveBalance() { async function refreshActiveBalance() {
const row = activeBalance.value const row = activeBalance.value
if (!row || apiResults.value[row.key]) return null if (!row) throw new Error("请先选择具体核算对象")
if (apiResults.value[row.key]) return apiResults.value[row.key]
const requestId = ++requestSerial const requestId = ++requestSerial
try {
const result = await getBalanceAdvice({ const result = await getBalanceAdvice({
areaCode: row.type === "pasture" ? undefined : row.areaCode, areaCode: row.areaCode,
pasturesId: row.type === "pasture" ? row.pasturesId : undefined,
}) })
if (destroyed || requestId !== requestSerial || !result) return null if (destroyed || requestId !== requestSerial) return null
if (!result || typeof result !== "object" || !Object.keys(result).length) {
throw new Error("未查询到该对象的草畜平衡结果")
}
apiResults.value = { apiResults.value = {
...apiResults.value, ...apiResults.value,
[row.key]: result, [row.key]: result,
} }
return result return result
} catch (error) {
return null
}
} }
async function loadVillageTargets() { async function loadVillageTargets() {
@ -649,122 +657,6 @@ function getTownPressureFactor(town = {}) {
return Number.isFinite(pressureFactor) && pressureFactor > 0 ? pressureFactor : 1 return Number.isFinite(pressureFactor) && pressureFactor > 0 ? pressureFactor : 1
} }
async function loadPastureTargets() {
const loadId = ++pastureLoadSerial
try {
const [muhuResult, pastureResult] = await Promise.allSettled([
getMuhuList("", 1, 10000),
getPastureList(),
])
if (destroyed || loadId !== pastureLoadSerial) return
const muhuRows = muhuResult.status === "fulfilled" ? muhuResult.value : []
const pastureRows = pastureResult.status === "fulfilled" ? pastureResult.value : []
const sourceRows = mergePastureSourceRows(muhuRows, pastureRows)
const normalizedRows = buildBalancePastureRows(sourceRows)
if (normalizedRows.length) {
pastureTargetRows.value = normalizedRows
}
const boundaryMap = await buildPastureBoundaryMap(normalizedRows, loadId)
if (destroyed || loadId !== pastureLoadSerial || !boundaryMap.size) return
pastureTargetRows.value = buildBalancePastureRows(sourceRows, boundaryMap)
} catch (error) {
pastureTargetRows.value = fallbackPastureRows
}
}
function mergePastureSourceRows(primaryRows = [], supplementRows = []) {
const supplements = new Map()
;(Array.isArray(supplementRows) ? supplementRows : []).forEach((row) => {
getPastureSourceKeys(row).forEach((key) => {
supplements.set(key, { ...(supplements.get(key) || {}), ...row })
})
})
const rows = Array.isArray(primaryRows) && primaryRows.length ? primaryRows : supplementRows
return (Array.isArray(rows) ? rows : []).map((row) => {
const supplement = getPastureSourceKeys(row)
.map((key) => supplements.get(key))
.find(Boolean)
return supplement ? fillMissingPastureSource(row, supplement) : row
})
}
function fillMissingPastureSource(row = {}, supplement = {}) {
const merged = { ...row }
Object.entries(supplement).forEach(([key, value]) => {
if (merged[key] === undefined || merged[key] === null || merged[key] === "") {
merged[key] = value
}
})
return merged
}
function getPastureSourceKeys(row = {}) {
return [
row.id,
row.pasturesId,
row.pastures_id,
row.pastureId,
row.pasture_id,
row.muhuId,
row.muhu_id,
row.name,
row.pasturesName,
row.pastures_name,
row.muhuName,
row.muhu_name,
row.pastureName,
row.pasture_name,
]
.map((value) => String(value || "").trim())
.filter(Boolean)
}
async function buildPastureBoundaryMap(rows = [], loadId) {
const boundaryMap = new Map()
const candidates = rows.filter((row) => row?.pasturesId)
await runLimited(candidates, 12, async (row) => {
if (destroyed || loadId !== pastureLoadSerial) return
try {
const grasslands = await getPastureGrasslandsById(row.pasturesId)
if (destroyed || loadId !== pastureLoadSerial) return
boundaryMap.set(String(row.pasturesId), {
rows: Array.isArray(grasslands) ? grasslands : [],
hasBoundary: hasPastureBoundary({ grasslands }),
})
if (hasPastureBoundary({ grasslands })) {
pastureTargetRows.value = sortBalancePastureRows(pastureTargetRows.value.map((item) => (
String(item.pasturesId) === String(row.pasturesId)
? {
...item,
hasBoundary: true,
boundaryCount: Array.isArray(grasslands) ? grasslands.length : item.boundaryCount,
grasslands: Array.isArray(grasslands) ? grasslands : item.grasslands,
}
: item
)))
}
} catch (error) {
boundaryMap.set(String(row.pasturesId), {
rows: [],
hasBoundary: false,
})
}
})
return boundaryMap
}
async function runLimited(items = [], limit = 6, worker) {
const queue = [...items]
const workers = Array.from({ length: Math.min(limit, queue.length) }, async () => {
while (queue.length && !destroyed) {
const item = queue.shift()
await worker(item)
}
})
await Promise.all(workers)
}
function formatFixed(value, digits = 2) { function formatFixed(value, digits = 2) {
const number = Number(value) const number = Number(value)
if (!Number.isFinite(number)) return "--" if (!Number.isFinite(number)) return "--"
@ -1127,7 +1019,7 @@ function getProcessStepState(index) {
.balance-scope-tabs { .balance-scope-tabs {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px; gap: 6px;
button { button {
@ -1706,8 +1598,7 @@ function getProcessStepState(index) {
small { small {
margin-top: 10px; margin-top: 10px;
color: rgba(255, 215, 108, 0.72); color: rgba(255, 215, 108, 0.72);
font-family: D-DIN, Arial, sans-serif; font-size: 16px;
font-size: 18px;
} }
} }
@ -1748,7 +1639,7 @@ function getProcessStepState(index) {
.balance-live-step { .balance-live-step {
position: relative; position: relative;
display: grid; display: grid;
grid-template-columns: 44px minmax(0, 1fr) 116px; grid-template-columns: 44px minmax(0, 1fr);
align-items: center; align-items: center;
gap: 14px; gap: 14px;
min-height: 74px; min-height: 74px;

@ -188,6 +188,7 @@ let chartMotionKickTimer = null
let videoPlayer = null let videoPlayer = null
let videoPlayerReady = false let videoPlayerReady = false
let videoPlayRun = 0 let videoPlayRun = 0
let videoLoadTimeoutTimer = null
let homeIdleWorkHandle = null let homeIdleWorkHandle = null
let homeIdleWorkType = "" let homeIdleWorkType = ""
let homeIdleWorkLoaded = false let homeIdleWorkLoaded = false
@ -202,8 +203,10 @@ const palette = ["#30dcff", "#27f0c0", "#ffd76c", "#7aa7ff", "#ff8f6b", "#a6f06d
const sidePanelHeight = 186 const sidePanelHeight = 186
const rightChartPanelHeight = sidePanelHeight const rightChartPanelHeight = sidePanelHeight
const videoPanelHeight = rightChartPanelHeight const videoPanelHeight = rightChartPanelHeight
const videoLoadTimeoutMs = 12000
const forecastTrendPeriod = getForecastPeriod("2028") const forecastTrendPeriod = getForecastPeriod("2028")
const trendSeries = buildForecastSeries(forecastTrendPeriod) const trendSeries = buildForecastSeries(forecastTrendPeriod)
const HOME_VIDEO_ACCESS_TOKEN = String(import.meta.env.VITE_EZVIZ_ACCESS_TOKEN || "").trim()
const homeLayerTopics = [ const homeLayerTopics = [
...forestGrassWetTopics.map(({ key, name, color }) => ({ key, name: name.replace("资源", ""), color })), ...forestGrassWetTopics.map(({ key, name, color }) => ({ key, name: name.replace("资源", ""), color })),
...yakIndustryChainTopics.map(({ key, name, color }) => ({ key, name: "牦牛", color })), ...yakIndustryChainTopics.map(({ key, name, color }) => ({ key, name: "牦牛", color })),
@ -261,7 +264,7 @@ const fallbackHomeVideoRows = [
image: liveSceneImage, image: liveSceneImage,
playUrl: "", playUrl: "",
hdPlayUrl: "", hdPlayUrl: "",
token: "", token: HOME_VIDEO_ACCESS_TOKEN,
}, },
{ {
key: "fallback-video-2", key: "fallback-video-2",
@ -279,7 +282,7 @@ const fallbackHomeVideoRows = [
image: liveSceneImage, image: liveSceneImage,
playUrl: "", playUrl: "",
hdPlayUrl: "", hdPlayUrl: "",
token: "", token: HOME_VIDEO_ACCESS_TOKEN,
}, },
{ {
key: "fallback-video-3", key: "fallback-video-3",
@ -297,7 +300,7 @@ const fallbackHomeVideoRows = [
image: liveSceneImage, image: liveSceneImage,
playUrl: "", playUrl: "",
hdPlayUrl: "", hdPlayUrl: "",
token: "", token: HOME_VIDEO_ACCESS_TOKEN,
}, },
] ]
const activeVideoIndex = computed(() => { const activeVideoIndex = computed(() => {
@ -318,7 +321,7 @@ const leftPanels = computed(() => [
const rightPanels = computed(() => [ const rightPanels = computed(() => [
{ {
key: "home-video-monitor", key: "home-video-monitor",
title: "视频监控", title: "牧场实时监控",
type: "video", type: "video",
height: videoPanelHeight, height: videoPanelHeight,
live: currentHomeVideo.value, live: currentHomeVideo.value,
@ -563,8 +566,8 @@ function initHomeMap() {
if (destroyed) return if (destroyed) return
await nextTick() await nextTick()
const scene = mapSceneRef.value const scene = mapSceneRef.value
scene?.loadMap?.(mapAssets) const mapLoaded = Boolean(scene?.loadMap?.(mapAssets) || scene?.hasMap?.())
if (scene?.canvasMap) { if (mapLoaded) {
emit("loading-progress", { progress: 100 }) emit("loading-progress", { progress: 100 })
emit("map-ready") emit("map-ready")
scheduleHomeIdleWork() scheduleHomeIdleWork()
@ -713,6 +716,7 @@ async function playCurrentVideo() {
const playRun = ++videoPlayRun const playRun = ++videoPlayRun
let player = null let player = null
startVideoLoadTimeout(playRun)
try { try {
const screenRect = homeVideoScreenRef.value?.getBoundingClientRect?.() const screenRect = homeVideoScreenRef.value?.getBoundingClientRect?.()
const playerWidth = Math.max(300, Math.floor(screenRect?.width || 340)) const playerWidth = Math.max(300, Math.floor(screenRect?.width || 340))
@ -727,6 +731,7 @@ async function playCurrentVideo() {
themeData: buildEzvizThemeData(), themeData: buildEzvizThemeData(),
handleError(error) { handleError(error) {
if (playRun !== videoPlayRun) return if (playRun !== videoPlayRun) return
clearVideoLoadTimeout()
console.warn("EZUIKit player error", error) console.warn("EZUIKit player error", error)
videoError.value = "视频加载失败" videoError.value = "视频加载失败"
videoLoading.value = false videoLoading.value = false
@ -738,6 +743,7 @@ async function playCurrentVideo() {
}, },
handleSuccess() { handleSuccess() {
if (playRun !== videoPlayRun) return if (playRun !== videoPlayRun) return
clearVideoLoadTimeout()
videoError.value = "" videoError.value = ""
videoPlayerReady = true videoPlayerReady = true
videoLoading.value = false videoLoading.value = false
@ -752,6 +758,7 @@ async function playCurrentVideo() {
videoPlayer = player videoPlayer = player
videoPlaying.value = true videoPlaying.value = true
} catch (error) { } catch (error) {
clearVideoLoadTimeout()
console.warn("EZUIKit init error", error) console.warn("EZUIKit init error", error)
videoError.value = "视频加载失败" videoError.value = "视频加载失败"
stopVideoPlayer() stopVideoPlayer()
@ -760,6 +767,7 @@ async function playCurrentVideo() {
function stopVideoPlayer(options = {}) { function stopVideoPlayer(options = {}) {
const { clearElement = true, resetError = false } = options const { clearElement = true, resetError = false } = options
clearVideoLoadTimeout()
const player = videoPlayer const player = videoPlayer
const ready = videoPlayerReady const ready = videoPlayerReady
videoPlayer = null videoPlayer = null
@ -772,6 +780,21 @@ function stopVideoPlayer(options = {}) {
if (resetError) videoError.value = "" if (resetError) videoError.value = ""
} }
function startVideoLoadTimeout(playRun) {
clearVideoLoadTimeout()
videoLoadTimeoutTimer = window.setTimeout(() => {
if (playRun !== videoPlayRun || !videoLoading.value) return
videoError.value = "视频加载超时"
stopVideoPlayer({ resetError: false })
}, videoLoadTimeoutMs)
}
function clearVideoLoadTimeout() {
if (!videoLoadTimeoutTimer) return
window.clearTimeout(videoLoadTimeoutTimer)
videoLoadTimeoutTimer = null
}
function stopEzvizPlayer(player) { function stopEzvizPlayer(player) {
if (!player || typeof player.stop !== "function") return if (!player || typeof player.stop !== "function") return
try { try {
@ -1350,6 +1373,14 @@ function isHomeLegendItemSelectable(item) {
pointer-events: auto; pointer-events: auto;
} }
.home-video-screen .loading-container,
.home-video-screen .loading-item,
.home-video-screen #loading-icon {
display: none !important;
visibility: hidden !important;
pointer-events: none !important;
}
.home-video-nav { .home-video-nav {
position: absolute; position: absolute;
top: 50%; top: 50%;

@ -49,6 +49,8 @@ export const pastureOnlineRows = [
{ areaCode: "513233208", areaName: "色地镇", uplineRate: 8, year: 2024 }, { areaCode: "513233208", areaName: "色地镇", uplineRate: 8, year: 2024 },
{ areaCode: "513233205", areaName: "阿木乡", uplineRate: 12, year: 2024 }, { areaCode: "513233205", areaName: "阿木乡", uplineRate: 12, year: 2024 },
{ areaCode: "513233202", areaName: "江茸乡", uplineRate: 14, year: 2024 }, { areaCode: "513233202", areaName: "江茸乡", uplineRate: 14, year: 2024 },
{ areaCode: "513233203", areaName: "查尔玛乡", uplineRate: 18, year: 2024 },
{ areaCode: "513233204", areaName: "瓦切镇", uplineRate: 16, year: 2024 },
{ areaCode: "513233101", areaName: "刷经寺镇", uplineRate: 9, year: 2024 }, { areaCode: "513233101", areaName: "刷经寺镇", uplineRate: 9, year: 2024 },
].map((item) => ({ ].map((item) => ({
...item, ...item,

@ -21,8 +21,42 @@
<business-panel-renderer :panel="grasslandPanel" compact /> <business-panel-renderer :panel="grasslandPanel" compact />
</m-card> </m-card>
<m-card class="pasture-side-card left-card" title="夏冬草场面积" :width="sideCardWidth" :height="sideCardHeight"> <m-card class="pasture-side-card left-card pasture-video-card" title="牧场实时监控" :width="sideCardWidth" :height="sideCardHeight">
<business-panel-renderer :panel="seasonPanel" compact /> <div class="pasture-video-monitor">
<button
class="pasture-video-nav pasture-video-prev"
type="button"
:disabled="pastureVideoRows.length <= 1"
aria-label="上一个监控点"
@click="showPrevPastureVideo"
>
</button>
<div
:key="currentPastureVideo.key"
:ref="setPastureVideoScreenRef"
class="pasture-video-screen"
:class="{ 'is-playing': pastureVideoPlaying, 'is-loading': pastureVideoLoading }"
:style="pastureVideoScreenStyle(currentPastureVideo)"
@click="playCurrentPastureVideo"
>
<div :id="pastureVideoElementId" class="pasture-video-player"></div>
<div v-if="!pastureVideoPlaying" class="pasture-video-scan"></div>
<div v-if="!pastureVideoPlaying && currentPastureVideo.hdPlayUrl" class="pasture-video-play"></div>
<div v-if="pastureVideoLoading" class="pasture-video-loading">加载中</div>
<div v-if="pastureVideoError" class="pasture-video-error">{{ pastureVideoError }}</div>
<div class="pasture-video-point-name">{{ currentPastureVideo.title }}</div>
</div>
<button
class="pasture-video-nav pasture-video-next"
type="button"
:disabled="pastureVideoRows.length <= 1"
aria-label="下一个监控点"
@click="showNextPastureVideo"
>
</button>
</div>
</m-card> </m-card>
<m-card class="pasture-side-card left-card" title="牧户上线率" :width="sideCardWidth" :height="sideCardHeight"> <m-card class="pasture-side-card left-card" title="牧户上线率" :width="sideCardWidth" :height="sideCardHeight">
@ -64,6 +98,7 @@
<script setup> <script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue" import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
import EZUIKit from "ezuikit-js"
import mCard from "@/components/mCard/index.vue" import mCard from "@/components/mCard/index.vue"
import MapScene from "@/views/forestGrassWet/map.vue" import MapScene from "@/views/forestGrassWet/map.vue"
import LayerPanel from "@/views/forestGrassWet/components/LayerPanel.vue" import LayerPanel from "@/views/forestGrassWet/components/LayerPanel.vue"
@ -71,7 +106,9 @@ import BusinessPanelRenderer from "@/views/forestGrassWet/components/BusinessPan
import { Assets } from "@/views/forestGrassWet/assets" import { Assets } from "@/views/forestGrassWet/assets"
import { getLayerByKey } from "@/config/layers" import { getLayerByKey } from "@/config/layers"
import { getHongyuanTownshipNameByCode, hongyuanTownshipsGeoJsonRaw, isHongyuanTownshipName } from "@/config/townshipBoundaries" import { getHongyuanTownshipNameByCode, hongyuanTownshipsGeoJsonRaw, isHongyuanTownshipName } from "@/config/townshipBoundaries"
import { getHomeMonitorings } from "@/services/homeVideoApi"
import { getPastureGrasslandsById, getPastureProfile, getPasturesDetailsById } from "@/services/pastureApi" import { getPastureGrasslandsById, getPastureProfile, getPasturesDetailsById } from "@/services/pastureApi"
import liveSceneImage from "@/assets/images/gd-live/live-scene-1.png"
import { import {
pastureAgeRows, pastureAgeRows,
pastureCoreRows, pastureCoreRows,
@ -81,7 +118,6 @@ import {
pastureListRows, pastureListRows,
pastureMapRows, pastureMapRows,
pastureOnlineRows, pastureOnlineRows,
pastureSeasonRows,
pastureTopStats, pastureTopStats,
pastureTownRows, pastureTownRows,
} from "./data" } from "./data"
@ -104,6 +140,71 @@ const pastureRows = ref(pastureListRows)
const activeLayerKey = ref("") const activeLayerKey = ref("")
const selectedLegendKey = ref("") const selectedLegendKey = ref("")
const pastureTopStatNames = ["牧户数量", "户均牦牛头数", "户均草地面积", "联户数量"] const pastureTopStatNames = ["牧户数量", "户均牦牛头数", "户均草地面积", "联户数量"]
const PASTURE_VIDEO_ACCESS_TOKEN = String(import.meta.env.VITE_EZVIZ_ACCESS_TOKEN || "").trim()
const pastureVideoElementId = `pasture-video-player-${Math.random().toString(36).slice(2)}`
const pastureVideoLoadTimeoutMs = 12000
const fallbackPastureVideoRows = [
{
key: "pasture-fallback-video-1",
title: "红原机场种草基地",
town: "邛溪镇",
scene: "种草基地",
device: "HY-CAM-021",
channel: "01",
cameraCount: 3,
status: "直播中",
onlineRate: 98,
updateTime: "12:18",
operator: "萤石云 · 在线预览",
progress: 86,
image: liveSceneImage,
playUrl: "",
hdPlayUrl: "",
token: PASTURE_VIDEO_ACCESS_TOKEN,
},
{
key: "pasture-fallback-video-2",
title: "瓦切镇草基地3",
town: "瓦切镇",
scene: "草场巡护",
device: "HY-CAM-026",
channel: "02",
cameraCount: 3,
status: "直播中",
onlineRate: 96,
updateTime: "12:16",
operator: "萤石云 · 在线预览",
progress: 74,
image: liveSceneImage,
playUrl: "",
hdPlayUrl: "",
token: PASTURE_VIDEO_ACCESS_TOKEN,
},
{
key: "pasture-fallback-video-3",
title: "色地镇牧场入口监控",
town: "色地镇",
scene: "牧场入口",
device: "HY-CAM-031",
channel: "03",
cameraCount: 3,
status: "直播中",
onlineRate: 97,
updateTime: "12:14",
operator: "萤石云 · 在线预览",
progress: 91,
image: liveSceneImage,
playUrl: "",
hdPlayUrl: "",
token: PASTURE_VIDEO_ACCESS_TOKEN,
},
]
const pastureVideoScreenRef = ref(null)
const pastureVideoRows = ref(fallbackPastureVideoRows)
const activePastureVideoManualIndex = ref(0)
const pastureVideoPlaying = ref(false)
const pastureVideoLoading = ref(false)
const pastureVideoError = ref("")
let mapAssets = null let mapAssets = null
let mapAssetsReady = false let mapAssetsReady = false
let mapLoadStarted = false let mapLoadStarted = false
@ -111,6 +212,10 @@ let mapLoadTimer = null
let destroyed = false let destroyed = false
let pastureEnrichToken = 0 let pastureEnrichToken = 0
let pastureEnrichTimer = null let pastureEnrichTimer = null
let pastureVideoPlayer = null
let pastureVideoPlayerReady = false
let pastureVideoPlayRun = 0
let pastureVideoLoadTimeoutTimer = null
const pastureFallbackTownshipById = { const pastureFallbackTownshipById = {
"4028abc18eecd2af018eecdc44b7086b": "瓦切镇", "4028abc18eecd2af018eecdc44b7086b": "瓦切镇",
"402881828b486d54018b48741f590270": "查尔玛乡", "402881828b486d54018b48741f590270": "查尔玛乡",
@ -187,11 +292,13 @@ const pastureMapLayer = computed(() => {
}) })
const coreRows = computed(() => normalizeCoreRows(apiProfile.value.core).length ? normalizeCoreRows(apiProfile.value.core) : pastureCoreRows) const coreRows = computed(() => normalizeCoreRows(apiProfile.value.core).length ? normalizeCoreRows(apiProfile.value.core) : pastureCoreRows)
const townRows = computed(() => normalizeTownRows(apiProfile.value.distribution).length ? normalizeTownRows(apiProfile.value.distribution) : pastureTownRows) const townRows = computed(() => normalizeTownRows(apiProfile.value.distribution).length ? normalizeTownRows(apiProfile.value.distribution) : pastureTownRows)
const onlineRows = computed(() => normalizeOnlineRows(apiProfile.value.online).length ? normalizeOnlineRows(apiProfile.value.online) : pastureOnlineRows) const onlineRows = computed(() => {
const normalizedRows = normalizeOnlineRows(apiProfile.value.online)
return completePastureOnlineRows(normalizedRows.length ? normalizedRows : pastureOnlineRows)
})
const ageRows = computed(() => normalizeAgeRows(apiProfile.value.age).length ? normalizeAgeRows(apiProfile.value.age) : pastureAgeRows) const ageRows = computed(() => normalizeAgeRows(apiProfile.value.age).length ? normalizeAgeRows(apiProfile.value.age) : pastureAgeRows)
const genderRows = computed(() => normalizeGenderRows(apiProfile.value.age).length ? normalizeGenderRows(apiProfile.value.age) : pastureGenderRows) const genderRows = computed(() => normalizeGenderRows(apiProfile.value.age).length ? normalizeGenderRows(apiProfile.value.age) : pastureGenderRows)
const grasslandRows = computed(() => aggregatePastureAreaRows(pastureRows.value).length ? aggregatePastureAreaRows(pastureRows.value) : pastureGrasslandRows) const grasslandRows = computed(() => aggregatePastureAreaRows(pastureRows.value).length ? aggregatePastureAreaRows(pastureRows.value) : pastureGrasslandRows)
const seasonRows = computed(() => aggregatePastureSeasonRows(pastureRows.value).length ? aggregatePastureSeasonRows(pastureRows.value) : pastureSeasonRows)
const mapRanking = computed(() => ({ const mapRanking = computed(() => ({
title: "牧户分布排行", title: "牧户分布排行",
metricLabel: "牧户", metricLabel: "牧户",
@ -218,15 +325,6 @@ const grasslandPanel = computed(() => ({
type: "heat", type: "heat",
rows: grasslandRows.value, rows: grasslandRows.value,
})) }))
const seasonPanel = computed(() => ({
key: "pasture-season",
type: "stackColumn",
segments: [
{ key: "summer", name: "夏草场" },
{ key: "winter", name: "冬草场" },
],
rows: seasonRows.value.slice(0, 7),
}))
const onlinePanel = computed(() => ({ const onlinePanel = computed(() => ({
key: "pasture-online", key: "pasture-online",
type: "line", type: "line",
@ -257,15 +355,23 @@ const pastureListPanel = computed(() => ({
], ],
rows: pastureRows.value, rows: pastureRows.value,
})) }))
const activePastureVideoIndex = computed(() => {
const length = pastureVideoRows.value.length
if (!length) return 0
return activePastureVideoManualIndex.value % length
})
const currentPastureVideo = computed(() => pastureVideoRows.value[activePastureVideoIndex.value] || fallbackPastureVideoRows[0])
onMounted(() => { onMounted(() => {
emitPastureTopStats() emitPastureTopStats()
initPastureMap() initPastureMap()
loadPastureProfile() loadPastureProfile()
loadPastureMonitorings()
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
destroyed = true destroyed = true
stopPastureVideoPlayer()
if (mapLoadTimer) window.clearTimeout(mapLoadTimer) if (mapLoadTimer) window.clearTimeout(mapLoadTimer)
if (pastureEnrichTimer) window.clearTimeout(pastureEnrichTimer) if (pastureEnrichTimer) window.clearTimeout(pastureEnrichTimer)
pastureEnrichToken += 1 pastureEnrichToken += 1
@ -275,6 +381,7 @@ onBeforeUnmount(() => {
watch( watch(
() => props.active, () => props.active,
(active) => { (active) => {
if (!active) stopPastureVideoPlayer({ resetError: true })
if (!mapSceneRef.value) return if (!mapSceneRef.value) return
if (active) { if (active) {
schedulePastureMapSceneLoad() schedulePastureMapSceneLoad()
@ -288,6 +395,194 @@ watch(
}, },
) )
async function loadPastureMonitorings() {
try {
const rows = await getHomeMonitorings()
if (destroyed) return
pastureVideoRows.value = rows.length ? rows : fallbackPastureVideoRows
activePastureVideoManualIndex.value = 0
} catch (error) {
if (destroyed) return
pastureVideoRows.value = fallbackPastureVideoRows
pastureVideoError.value = "监控接口暂不可用"
window.setTimeout(() => {
if (!destroyed && pastureVideoError.value === "监控接口暂不可用") pastureVideoError.value = ""
}, 2400)
}
}
function setPastureVideoScreenRef(el) {
pastureVideoScreenRef.value = el || null
}
function pastureVideoScreenStyle(live) {
const image = live?.image || liveSceneImage
return pastureVideoPlaying.value
? undefined
: {
backgroundImage: `linear-gradient(180deg, rgba(0, 16, 26, 0.08), rgba(0, 16, 26, 0.32)), url(${image})`,
}
}
function showPrevPastureVideo() {
stepPastureVideo(-1)
}
function showNextPastureVideo() {
stepPastureVideo(1)
}
function stepPastureVideo(step) {
const length = pastureVideoRows.value.length
if (!length) return
stopPastureVideoPlayer({ resetError: true })
activePastureVideoManualIndex.value = (activePastureVideoIndex.value + step + length) % length
}
async function playCurrentPastureVideo() {
const live = currentPastureVideo.value
if (!live?.hdPlayUrl || !live?.token) {
pastureVideoError.value = "无效的监控参数"
return
}
if (pastureVideoPlaying.value || pastureVideoLoading.value) return
if (pastureVideoPlayer) stopPastureVideoPlayer({ resetError: true })
pastureVideoError.value = ""
pastureVideoLoading.value = true
await nextTick()
const playRun = ++pastureVideoPlayRun
let player = null
startPastureVideoLoadTimeout(playRun)
try {
const screenRect = pastureVideoScreenRef.value?.getBoundingClientRect?.()
const playerWidth = Math.max(300, Math.floor(screenRect?.width || 340))
const playerHeight = Math.max(80, Math.floor(screenRect?.height || 120))
player = new EZUIKit.EZUIKitPlayer({
id: pastureVideoElementId,
accessToken: live.token,
url: live.hdPlayUrl,
audio: 0,
width: playerWidth,
height: playerHeight,
themeData: buildPastureEzvizThemeData(),
handleError(error) {
if (playRun !== pastureVideoPlayRun) return
clearPastureVideoLoadTimeout()
console.warn("Pasture EZUIKit player error", error)
pastureVideoError.value = "视频加载失败"
pastureVideoLoading.value = false
pastureVideoPlaying.value = false
pastureVideoPlayRun += 1
pastureVideoPlayer = null
pastureVideoPlayerReady = false
clearPastureVideoElement()
},
handleSuccess() {
if (playRun !== pastureVideoPlayRun) return
clearPastureVideoLoadTimeout()
pastureVideoError.value = ""
pastureVideoPlayerReady = true
pastureVideoLoading.value = false
},
fullScreenChangeCallBack() {},
})
if (playRun !== pastureVideoPlayRun) {
stopPastureEzvizPlayer(player)
clearPastureVideoElement()
return
}
pastureVideoPlayer = player
pastureVideoPlaying.value = true
} catch (error) {
clearPastureVideoLoadTimeout()
console.warn("Pasture EZUIKit init error", error)
pastureVideoError.value = "视频加载失败"
stopPastureVideoPlayer()
}
}
function stopPastureVideoPlayer(options = {}) {
const { clearElement = true, resetError = false } = options
clearPastureVideoLoadTimeout()
const player = pastureVideoPlayer
pastureVideoPlayer = null
pastureVideoPlayerReady = false
pastureVideoPlayRun += 1
if (player) stopPastureEzvizPlayer(player)
if (clearElement) clearPastureVideoElement()
pastureVideoPlaying.value = false
pastureVideoLoading.value = false
if (resetError) pastureVideoError.value = ""
}
function startPastureVideoLoadTimeout(playRun) {
clearPastureVideoLoadTimeout()
pastureVideoLoadTimeoutTimer = window.setTimeout(() => {
if (playRun !== pastureVideoPlayRun || !pastureVideoLoading.value) return
pastureVideoError.value = "视频加载超时"
stopPastureVideoPlayer({ resetError: false })
}, pastureVideoLoadTimeoutMs)
}
function clearPastureVideoLoadTimeout() {
if (!pastureVideoLoadTimeoutTimer) return
window.clearTimeout(pastureVideoLoadTimeoutTimer)
pastureVideoLoadTimeoutTimer = null
}
function stopPastureEzvizPlayer(player) {
if (!player || typeof player.stop !== "function") return
try {
const stopped = player.stop()
if (stopped && typeof stopped.catch === "function") {
stopped.catch(() => {})
}
} catch (error) {
console.warn("Pasture EZUIKit stop error", error)
}
}
function clearPastureVideoElement() {
if (typeof document === "undefined") return
const container = document.getElementById(pastureVideoElementId)
if (container) {
container.replaceChildren()
}
}
function buildPastureEzvizThemeData() {
return {
header: {
color: "#1890ff",
activeColor: "#ffffff",
backgroundColor: "#000000",
btnList: [
{ iconId: "deviceID", part: "left", defaultActive: 0, memo: "顶部设备序列号", isrender: 0 },
{ iconId: "deviceName", part: "left", defaultActive: 0, memo: "顶部设备名称", isrender: 0 },
{ iconId: "cloudRec", part: "right", defaultActive: 0, memo: "云存储", isrender: 0 },
{ iconId: "rec", part: "right", defaultActive: 0, memo: "SD卡回放", isrender: 0 },
],
},
footer: {
color: "#ffffff",
activeColor: "#1890ff",
backgroundColor: "#00000021",
btnList: [
{ iconId: "play", part: "left", defaultActive: 1, memo: "播放", isrender: 1 },
{ iconId: "capturePicture", part: "left", defaultActive: 0, memo: "截屏按钮", isrender: 0 },
{ iconId: "sound", part: "left", defaultActive: 0, memo: "声音按钮", isrender: 1 },
{ iconId: "pantile", part: "left", defaultActive: 0, memo: "云台控制按钮", isrender: 1 },
{ iconId: "recordvideo", part: "left", defaultActive: 0, memo: "录制按钮", isrender: 0 },
{ iconId: "zoom", part: "left", defaultActive: 0, memo: "电子放大", isrender: 1 },
{ iconId: "hd", part: "right", defaultActive: 0, memo: "清晰度切换按钮", isrender: 1 },
{ iconId: "expend", part: "right", defaultActive: 0, memo: "全局全屏按钮", isrender: 1 },
],
},
}
}
async function loadPastureProfile() { async function loadPastureProfile() {
try { try {
const profile = await getPastureProfile() const profile = await getPastureProfile()
@ -306,16 +601,12 @@ async function loadPastureProfile() {
} }
} }
function schedulePastureEnrichment(rows = pastureRows.value, delay = 4200) { function schedulePastureEnrichment(rows = pastureRows.value, delay = 600) {
if (destroyed || !props.active) return if (destroyed || !props.active) return
if (pastureEnrichTimer) window.clearTimeout(pastureEnrichTimer) if (pastureEnrichTimer) window.clearTimeout(pastureEnrichTimer)
pastureEnrichTimer = window.setTimeout(() => { pastureEnrichTimer = window.setTimeout(() => {
pastureEnrichTimer = null pastureEnrichTimer = null
if (destroyed || !props.active) return if (destroyed || !props.active) return
if (!mapReady.value) {
schedulePastureEnrichment(rows, 1800)
return
}
enrichPastureRowsWithGrasslands(rows) enrichPastureRowsWithGrasslands(rows)
}, delay) }, delay)
} }
@ -323,27 +614,34 @@ function schedulePastureEnrichment(rows = pastureRows.value, delay = 4200) {
async function enrichPastureRowsWithGrasslands(rows = []) { async function enrichPastureRowsWithGrasslands(rows = []) {
const token = ++pastureEnrichToken const token = ++pastureEnrichToken
const targets = (Array.isArray(rows) ? rows : []) const targets = (Array.isArray(rows) ? rows : [])
.filter((row) => row?.township === "--" && getRealPastureId(row)) .filter(shouldEnrichPastureTownship)
.slice(0, 24) .slice(0, 60)
if (!targets.length) return if (!targets.length) return
const results = await mapWithConcurrency(targets, 5, async (row) => { const results = await mapWithConcurrency(targets, 5, async (row) => {
const pastureId = getRealPastureId(row) const pastureId = getRealPastureId(row)
let grasslands = [] const rowGrasslands = normalizeRowGrasslands(row)
let grasslands = rowGrasslands
let detail = null let detail = null
let township = getPastureTownship(row, rowGrasslands) || getPastureFallbackTownship(row, row.name, pastureId)
try { try {
if (pastureId && (!township || !grasslands.length)) {
grasslands = normalizeGrasslandDetailRows(await getPastureGrasslandsById(pastureId)) grasslands = normalizeGrasslandDetailRows(await getPastureGrasslandsById(pastureId))
}
} catch (error) { } catch (error) {
grasslands = [] grasslands = rowGrasslands
} }
let township = getPastureTownship(row, grasslands) || getPastureFallbackTownship(row, row.name, pastureId) township = township || getPastureTownship(row, grasslands) || getPastureFallbackTownship(row, row.name, pastureId)
if (!township) { if (!township) {
try { try {
detail = await getPasturesDetailsById(pastureId) detail = pastureId ? await getPasturesDetailsById(pastureId) : null
} catch (error) { } catch (error) {
detail = null detail = null
} }
township = getPastureTownshipFromDetails(detail) const detailGrasslands = normalizeRowGrasslands(detail)
township = getPastureTownshipFromDetails(detail) || getPastureTownship(detail, detailGrasslands)
if (detailGrasslands.length && !grasslands.length) grasslands = detailGrasslands
} }
township = township || getPastureFallbackTownship(row, row.name, pastureId)
const canLocate = Boolean(pastureId || grasslands.some(hasGrasslandGeometry) || hasGrasslandGeometry(row)) const canLocate = Boolean(pastureId || grasslands.some(hasGrasslandGeometry) || hasGrasslandGeometry(row))
if (!township && !grasslands.length && canLocate === row.canLocate) return null if (!township && !grasslands.length && canLocate === row.canLocate) return null
const result = { const result = {
@ -370,6 +668,12 @@ async function enrichPastureRowsWithGrasslands(rows = []) {
applyPastureEnrichmentResults(resultMap) applyPastureEnrichmentResults(resultMap)
} }
function shouldEnrichPastureTownship(row = {}) {
const township = String(row?.township || "").trim()
if (township && township !== "--") return false
return pickPastureArea(row) > 0 || isPastureLocatable(row)
}
function applyPastureEnrichmentResult(result, token = pastureEnrichToken) { function applyPastureEnrichmentResult(result, token = pastureEnrichToken) {
if (!result || destroyed || token !== pastureEnrichToken) return if (!result || destroyed || token !== pastureEnrichToken) return
const resultMap = new Map() const resultMap = new Map()
@ -669,7 +973,33 @@ function normalizeOnlineRows(rows = []) {
} }
}) })
.filter(Boolean) .filter(Boolean)
.slice(0, 7) }
function completePastureOnlineRows(rows = []) {
const rowMap = new Map()
;(Array.isArray(rows) ? rows : []).forEach((row) => {
const codeKey = String(row?.key || row?.code || row?.areaCode || row?.area_code || "").trim()
const nameKey = normalizePastureTownName(row?.name || row?.areaName || row?.area_name || row?.townName || row?.zoneName)
;[codeKey, nameKey].filter(Boolean).forEach((key) => rowMap.set(key, row))
})
return pastureTownRows.map((town) => {
const row = rowMap.get(String(town.key || "")) || rowMap.get(town.name)
if (row) {
return {
...row,
key: town.key,
name: town.name,
unit: row.unit || "%",
}
}
return {
key: town.key,
name: town.name,
value: 0,
unit: "%",
extra: "--",
}
})
} }
function normalizeAgeRows(rows = []) { function normalizeAgeRows(rows = []) {
@ -800,30 +1130,6 @@ function aggregatePastureAreaRows(rows = []) {
return aggregatePastureRows(rows, (row) => Number(row.grasslandArea) || 0, "万亩", (value) => Math.round((value / 10000) * 100) / 100) return aggregatePastureRows(rows, (row) => Number(row.grasslandArea) || 0, "万亩", (value) => Math.round((value / 10000) * 100) / 100)
} }
function aggregatePastureSeasonRows(rows = []) {
const map = new Map()
;(Array.isArray(rows) ? rows : []).forEach((row) => {
const name = row?.township || row?.areaName || row?.area_name
if (!name || name === "--") return
const current = map.get(name) || { key: `${name}-season`, name, values: { summer: 0, winter: 0 }, total: 0, unit: "万亩" }
current.values.summer += Number(pickValue(row, ["summerAcreage", "summer_acreage", "summerArea", "summer_area"])) || 0
current.values.winter += Number(pickValue(row, ["winterAcreage", "winter_acreage", "winterArea", "winter_area"])) || 0
current.total = current.values.summer + current.values.winter
map.set(name, current)
})
return Array.from(map.values())
.filter((row) => row.total > 0)
.map((row) => ({
...row,
value: Math.round((row.total / 10000) * 100) / 100,
values: {
summer: Math.round((row.values.summer / 10000) * 100) / 100,
winter: Math.round((row.values.winter / 10000) * 100) / 100,
},
}))
.sort((a, b) => b.value - a.value)
}
function aggregatePastureRows(rows = [], pick, unit, transform = (value) => value) { function aggregatePastureRows(rows = [], pick, unit, transform = (value) => value) {
const map = new Map() const map = new Map()
;(Array.isArray(rows) ? rows : []).forEach((row) => { ;(Array.isArray(rows) ? rows : []).forEach((row) => {
@ -893,12 +1199,36 @@ function pickValue(item = {}, keys = []) {
} }
function getPastureTownship(item = {}, grasslands = []) { function getPastureTownship(item = {}, grasslands = []) {
const directName = pickValue(item, ["township", "townName", "town_name", "areaName", "area_name", "zoneName", "zone_name"]) const directName = pickValue(item, [
"township",
"townName",
"town_name",
"areaName",
"area_name",
"zoneName",
"zone_name",
"fullName",
"full_name",
"address",
"牧场所在区域",
])
if (directName) return normalizePastureTownName(directName) if (directName) return normalizePastureTownName(directName)
const codeName = getPastureTownNameFromCode(pickValue(item, ["areaCode", "area_code", "townCode", "town_code", "code"])) const codeName = getPastureTownNameFromCode(pickValue(item, ["areaCode", "area_code", "townCode", "town_code", "code"]))
if (codeName) return codeName if (codeName) return codeName
for (const grassland of grasslands) { for (const grassland of grasslands) {
const grasslandName = pickValue(grassland, ["township", "townName", "town_name", "areaName", "area_name", "zoneName", "zone_name"]) const grasslandName = pickValue(grassland, [
"township",
"townName",
"town_name",
"areaName",
"area_name",
"zoneName",
"zone_name",
"fullName",
"full_name",
"address",
"牧场所在区域",
])
if (grasslandName) return normalizePastureTownName(grasslandName) if (grasslandName) return normalizePastureTownName(grasslandName)
const grasslandCodeName = getPastureTownNameFromCode(pickValue(grassland, ["areaCode", "area_code", "townCode", "town_code", "code"])) const grasslandCodeName = getPastureTownNameFromCode(pickValue(grassland, ["areaCode", "area_code", "townCode", "town_code", "code"]))
if (grasslandCodeName) return grasslandCodeName if (grasslandCodeName) return grasslandCodeName
@ -917,7 +1247,9 @@ function normalizePastureTownName(value) {
if (!text || text === "--") return "" if (!text || text === "--") return ""
const codeName = getPastureTownNameFromCode(text) const codeName = getPastureTownNameFromCode(text)
if (codeName) return codeName if (codeName) return codeName
return isHongyuanTownshipName(text) ? text : "" if (isHongyuanTownshipName(text)) return text
const extracted = extractPastureTownName(text)
return extracted && extracted !== text ? normalizePastureTownName(extracted) : ""
} }
function getPastureFallbackTownship(item = {}, name = "", realId = "") { function getPastureFallbackTownship(item = {}, name = "", realId = "") {
@ -1218,6 +1550,197 @@ function isLngLatInPolygon(point, polygon) {
} }
} }
.pasture-video-card {
.m-card-bd-content {
bottom: 12px;
}
}
.pasture-video-monitor {
position: relative;
display: block;
height: 100%;
padding: 2px 18px;
box-sizing: border-box;
pointer-events: auto;
}
.pasture-video-screen {
position: relative;
width: 88%;
height: 100%;
min-width: 0;
margin: 0 auto;
overflow: hidden;
cursor: pointer;
border: 1px solid rgba(48, 220, 255, 0.34);
background-color: #020d14;
background-position: center;
background-size: cover;
box-shadow:
inset 0 0 0 1px rgba(10, 255, 214, 0.08),
inset 0 0 26px rgba(48, 220, 255, 0.16),
0 0 16px rgba(18, 145, 190, 0.14);
}
.pasture-video-screen.is-playing {
background-image: none !important;
}
.pasture-video-player,
.pasture-video-player > div,
.pasture-video-player video,
.pasture-video-player canvas,
.pasture-video-player iframe {
position: absolute !important;
inset: 0 !important;
z-index: 1;
width: 100% !important;
height: 100% !important;
}
.pasture-video-player {
pointer-events: auto;
}
.pasture-video-player video {
object-fit: cover;
}
.pasture-video-screen .loading-container,
.pasture-video-screen .loading-item,
.pasture-video-screen #loading-icon {
display: none !important;
visibility: hidden !important;
pointer-events: none !important;
}
.pasture-video-nav {
position: absolute;
top: 50%;
z-index: 6;
width: 22px;
height: 42px;
border: 1px solid rgba(48, 220, 255, 0.26);
color: rgba(227, 253, 255, 0.88);
font-size: 20px;
line-height: 20px;
cursor: pointer;
background: linear-gradient(180deg, rgba(48, 220, 255, 0.18), rgba(10, 39, 54, 0.56));
box-shadow: inset 0 0 12px rgba(48, 220, 255, 0.1);
transform: translateY(-50%);
}
.pasture-video-nav:disabled {
cursor: not-allowed;
opacity: 0.38;
}
.pasture-video-prev {
left: 25px;
}
.pasture-video-next {
right: 25px;
}
.pasture-video-screen::before {
position: absolute;
inset: 0;
z-index: 2;
content: "";
pointer-events: none;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent 34%, rgba(0, 0, 0, 0.44)),
repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.04) 0, rgba(255, 255, 255, 0.04) 1px, transparent 1px, transparent 5px);
}
.pasture-video-screen.is-playing::before {
background: linear-gradient(180deg, rgba(0, 0, 0, 0.36), transparent 28%, rgba(0, 0, 0, 0.24));
}
.pasture-video-scan {
position: absolute;
inset: 0;
z-index: 3;
pointer-events: none;
background: linear-gradient(90deg, transparent, rgba(126, 251, 246, 0.2), transparent);
transform: translateX(-100%);
animation: pastureVideoScan 3.2s linear infinite;
}
.pasture-video-play {
position: absolute;
left: 50%;
top: 46%;
z-index: 4;
width: 30px;
height: 30px;
border: 1px solid rgba(255, 255, 255, 0.62);
border-radius: 50%;
background: rgba(0, 0, 0, 0.36);
box-shadow: 0 0 12px rgba(48, 220, 255, 0.2);
transform: translate(-50%, -50%);
}
.pasture-video-play::after {
position: absolute;
left: 12px;
top: 8px;
width: 0;
height: 0;
content: "";
border-top: 7px solid transparent;
border-bottom: 7px solid transparent;
border-left: 10px solid #ffffff;
}
.pasture-video-point-name {
position: absolute;
right: 10px;
bottom: 8px;
left: 10px;
z-index: 4;
overflow: hidden;
padding: 4px 8px;
border-left: 2px solid rgba(69, 233, 164, 0.78);
color: #ffffff;
font-size: 12px;
line-height: 15px;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
background: rgba(0, 0, 0, 0.42);
}
.pasture-video-loading,
.pasture-video-error {
position: absolute;
left: 50%;
top: 50%;
z-index: 5;
min-width: 72px;
padding: 4px 8px;
border: 1px solid rgba(48, 220, 255, 0.32);
color: #ffffff;
font-size: 11px;
line-height: 14px;
text-align: center;
background: rgba(0, 0, 0, 0.62);
transform: translate(-50%, -50%);
}
.pasture-video-error {
border-color: rgba(255, 122, 69, 0.44);
color: #ffb18e;
}
@keyframes pastureVideoScan {
to {
transform: translateX(100%);
}
}
.pasture-map-layer-tools { .pasture-map-layer-tools {
position: absolute; position: absolute;
top: 372px; top: 372px;

@ -261,9 +261,9 @@ const totalView = computed(() => {
} }
return buildTopCountItems([ return buildTopCountItems([
{ {
zh: "草地面积", zh: "草地面积",
en: "", en: "",
value: 756.43, value: 756.37,
unit: "万亩", unit: "万亩",
decimals: 2, decimals: 2,
}, },

@ -122,6 +122,7 @@ const equipmentRecognitionRows = townRecognitionRows
.filter((item) => item.value > 0) .filter((item) => item.value > 0)
.sort(sortByValue) .sort(sortByValue)
const farmEquipmentPointRows = buildFarmEquipmentPointRows() const farmEquipmentPointRows = buildFarmEquipmentPointRows()
const grasslandResourceWanMu = round(homeCore.grasslandArea / 10000, 2)
export const industryDatasetGeneratedAt = classifiedData.generatedAt || "" export const industryDatasetGeneratedAt = classifiedData.generatedAt || ""
export const supportPointRows = forageStorageItems.map(toLegacyPointRow) export const supportPointRows = forageStorageItems.map(toLegacyPointRow)
@ -153,7 +154,7 @@ export function getStageTopStats(stageKey) {
{ zh: "种草面积", name: "种草面积", value: round(forageAreaMu / 10000, 2), unit: "万亩", decimals: 2 }, { zh: "种草面积", name: "种草面积", value: round(forageAreaMu / 10000, 2), unit: "万亩", decimals: 2 },
{ zh: "饲草点位", name: "饲草点位", value: stagePointCount("artificial-grassland"), unit: "个" }, { zh: "饲草点位", name: "饲草点位", value: stagePointCount("artificial-grassland"), unit: "个" },
{ zh: "草种类型", name: "草种类型", value: forageGrassTypeRows.length, unit: "类" }, { zh: "草种类型", name: "草种类型", value: forageGrassTypeRows.length, unit: "类" },
{ zh: "草地资源", name: "草地资源", value: round(homeCore.grasslandArea / 10000, 1), unit: "万亩", decimals: 1 }, { zh: "草地资源", name: "草地资源", value: grasslandResourceWanMu, unit: "万亩", decimals: 2 },
], ],
"scale-breeding": [ "scale-breeding": [
{ zh: "养殖点位", name: "养殖点位", value: stagePointCount("scale-breeding"), unit: "个" }, { zh: "养殖点位", name: "养殖点位", value: stagePointCount("scale-breeding"), unit: "个" },
@ -234,7 +235,7 @@ export const industryStageRows = [
name: "人工草地", name: "人工草地",
value: round(forageAreaMu / 10000, 2), value: round(forageAreaMu / 10000, 2),
unit: "万亩", unit: "万亩",
capacity: round(homeCore.grasslandArea / 10000, 1), capacity: grasslandResourceWanMu,
capacityUnit: "万亩", capacityUnit: "万亩",
note: "种草饲草基地、草种基地与草地资源", note: "种草饲草基地、草种基地与草地资源",
fields: [ fields: [

@ -3274,7 +3274,7 @@ function formatSignedWan(value) {
} }
.china-label .town-stat-label { .china-label .town-stat-label {
transform: translate(50%, 0) scale(0.92); transform: translateY(-50%) scale(0.92);
} }
} }

Loading…
Cancel
Save