feat: prepare V2.0 screen release

V2.0
M先生 1 month ago
parent 1aed9d07db
commit 6627e5f0cb
  1. 0
      docs/assets/AlibabaPuHuiTi-14435b65-balabala-1782919616283.woff2
  2. 2
      docs/assets/SmartChartRenderer-3aefa042-1782919616283.js
  3. 0
      docs/assets/SmartChartRenderer-8fd37aa6-balabala-1782919616283.css
  4. 0
      docs/assets/bg-3acb94e3-balabala-1782919616283.png
  5. 0
      docs/assets/bottom-menu-arrow-big-180b7071-balabala-1782919616283.svg
  6. 0
      docs/assets/bottom-menu-arrow-small-19304179-balabala-1782919616283.svg
  7. 0
      docs/assets/bottom-menu-bg-b465793d-balabala-1782919616283.png
  8. 0
      docs/assets/bottom-menu-btn-ee5c8064-balabala-1782919616283.png
  9. 0
      docs/assets/bottom-menu-btn-hover-df33e514-balabala-1782919616283.png
  10. 0
      docs/assets/chinaBlurLine-1a2cbc0f-balabala-1782919616283.png
  11. 0
      docs/assets/content-bottom-left-arrow-3a8a61f3-balabala-1782919616283.svg
  12. 0
      docs/assets/content-bottom-right-arrow-a3c5270d-balabala-1782919616283.svg
  13. 0
      docs/assets/content-middle-line-162bc0d5-balabala-1782919616283.svg
  14. 0
      docs/assets/focus_bg-b6803e2d-balabala-1782919616283.png
  15. 0
      docs/assets/focus_move_bg-cf41417a-balabala-1782919616283.png
  16. 0
      docs/assets/header-bg-65b38012-balabala-1782919616283.png
  17. 0
      docs/assets/huiguang-3c5dfda5-balabala-1782919616283.png
  18. 0
      docs/assets/icon1-6f7c222f-balabala-1782919616283.png
  19. 0
      docs/assets/icon2-4f77ccec-balabala-1782919616283.png
  20. 2
      docs/assets/index-0354b2d4-balabala-1782919616283.css
  21. 14
      docs/assets/index-1782919616283.js
  22. 1
      docs/assets/index-35304d5e-balabala-1782661552655.css
  23. 1
      docs/assets/index-39905994-balabala-1782919616283.css
  24. 2
      docs/assets/index-47d6f37a-balabala-1782919616283.css
  25. 5403
      docs/assets/index-9fb96bf8-1782919616283.js
  26. 2
      docs/assets/index-ab885316-1782919616283.js
  27. 4525
      docs/assets/index-c9e6302b-1782661552655.js
  28. 0
      docs/assets/left-kuang-5d6bd1ae-balabala-1782919616283.svg
  29. BIN
      docs/assets/live-scene-1-786cdd0a-balabala-1782919616283.png
  30. 0
      docs/assets/menu-btn-91a961cf-balabala-1782919616283.png
  31. 0
      docs/assets/menu-btn-hover-62eba0bb-balabala-1782919616283.png
  32. 0
      docs/assets/ocean-bg-8c848b8f-balabala-1782919616283.png
  33. 0
      docs/assets/pathLine2-d4134d4b-balabala-1782919616283.png
  34. 0
      docs/assets/right-kuang-b1937caf-balabala-1782919616283.svg
  35. 0
      docs/assets/ring2-a1b91808-balabala-1782919616283.png
  36. 0
      docs/assets/ring3-fe0f60d7-balabala-1782919616283.png
  37. 0
      docs/assets/ring4-f2e324d6-balabala-1782919616283.png
  38. 0
      docs/assets/rotationBorder1-2ce8e0ef-balabala-1782919616283.png
  39. 0
      docs/assets/rotationBorder2-7496b968-balabala-1782919616283.png
  40. 0
      docs/assets/saoguang-ccf84a88-balabala-1782919616283.svg
  41. 0
      docs/assets/title-zs1-c15a5ee8-balabala-1782919616283.svg
  42. 4684
      docs/datas/yak-industry-chain/yak_industry_points.geojson
  43. 4
      docs/index.html
  44. 7
      package-lock.json
  45. 1
      package.json
  46. 5
      prototypes/yak-industry-business-dashboard.html
  47. 5
      prototypes/yak-industry-chain-business-report.html
  48. 149
      prototypes/yak-industry-data-dashboard.html
  49. 5
      prototypes/yak-industry-left-report.html
  50. 1
      public/datas/geojsons/hongyuan-townships.geojson
  51. 4684
      public/datas/yak-industry-chain/yak_industry_points.geojson
  52. BIN
      src/assets/images/gd-live/live-scene-1.png
  53. 193
      src/config/layers.js
  54. 12
      src/config/modules.js
  55. 116
      src/config/townshipBoundaries.js
  56. 12
      src/services/auth.js
  57. 78
      src/services/balanceApi.js
  58. 113
      src/services/homeVideoApi.js
  59. 100
      src/services/openApi.js
  60. 65
      src/services/pastureApi.js
  61. 14
      src/style.css
  62. 158
      src/utils/chartTheme.js
  63. 50
      src/views/ecologicalProtection/data.js
  64. 2
      src/views/ecologicalProtection/index.vue
  65. 2
      src/views/forestGrassWet/assets.js
  66. 134
      src/views/forestGrassWet/components/BusinessPanelRenderer.vue
  67. 4
      src/views/forestGrassWet/components/BusinessStatList.vue
  68. 12
      src/views/forestGrassWet/components/LayerPanel.vue
  69. 34
      src/views/forestGrassWet/components/TopicSwitcher.vue
  70. 23
      src/views/forestGrassWet/index.vue
  71. 702
      src/views/forestGrassWet/map.js
  72. 160
      src/views/forestGrassWet/map.vue
  73. 55
      src/views/forestGrassWet/map/chinaData.js
  74. 75
      src/views/forestGrassWet/map/provincesData.js
  75. 2
      src/views/gdMap/assets.js
  76. 55
      src/views/gdMap/map/chinaData.js
  77. 75
      src/views/gdMap/map/provincesData.js
  78. 207
      src/views/grassLivestockBalance/data.js
  79. 635
      src/views/grassLivestockBalance/index.vue
  80. 44
      src/views/home/data.js
  81. 1025
      src/views/home/index.vue
  82. 570
      src/views/pastureManagement/index.vue
  83. 30
      src/views/platform/index.vue
  84. 368
      src/views/yakIndustryChain/data.js
  85. 406
      src/views/yakIndustryChain/index.vue
  86. 5431
      src/views/yakIndustryChain/yakIndustryClassifiedData.json
  87. 828
      src/views/yakManagement/index.vue
  88. 6
      vite.config.js

@ -1,2 +1,2 @@
var B=Object.defineProperty,O=Object.defineProperties;var D=Object.getOwnPropertyDescriptors;var S=Object.getOwnPropertySymbols;var W=Object.prototype.hasOwnProperty,$=Object.prototype.propertyIsEnumerable;var v=(a,r,o)=>r in a?B(a,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):a[r]=o,m=(a,r)=>{for(var o in r||(r={}))W.call(r,o)&&v(a,o,r[o]);if(S)for(var o of S(r))$.call(r,o)&&v(a,o,r[o]);return a},d=(a,r)=>O(a,D(r));import{L as b,H}from"./index-c9e6302b-1782661552655.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-1782661552655.js";const q={class:"smart-chart-header"},G={key:0},E={key:0,class:"smart-chart-source"},J={__name:"SmartChartRenderer",props:{chart:{type:Object,required:!0}},emits:["ready"],setup(a,{emit:r}){const o=r,i=a,l=["#30dcff","#27f0c0","#ffd76c","#7aa7ff","#ff8f6b","#a6f06d"],z=h(()=>{const t=(i.chart.rows||[]).filter(e=>Number.isFinite(Number(e.value)));return i.chart.type==="pie"?N(t):i.chart.type==="line"?w(t):_(t)}),k=h(()=>{const t=i.chart.rows||[];return i.chart.type==="bar"&&x(t)}),y=h(()=>{const t=i.chart.source||{},e=[t.mode,t.table,t.period,t.generatedAt].filter(Boolean);return e.length?e.join(" / "):""});F(()=>{R(()=>{window.requestAnimationFrame(()=>o("ready"))})});function _(t){return x(t)?C(t):{animationDuration:700,grid:{left:52,right:26,top:28,bottom:54},tooltip:s(),xAxis:{type:"category",data:t.map(e=>e.name),axisTick:{show:!1},axisLine:{lineStyle:{color:"rgba(48, 220, 255, 0.28)"}},axisLabel:{color:"rgba(223, 250, 255, 0.76)",fontSize:11,interval:0,rotate:t.length>6?28:0}},yAxis:{type:"value",axisLine:{show:!1},axisTick:{show:!1},splitLine:{lineStyle:{color:"rgba(48, 220, 255, 0.1)",type:"dashed"}},axisLabel:{color:"rgba(223, 250, 255, 0.58)",fontSize:11}},series:[{name:i.chart.title,type:"bar",barWidth:t.length>7?16:22,data:t.map((e,n)=>({value:e.value,name:e.name,itemStyle:{borderRadius:[8,8,0,0],color:new b(0,0,0,1,[{offset:0,color:l[n%l.length]},{offset:1,color:"rgba(48, 220, 255, 0.08)"}])}})),label:{show:!0,position:"top",color:"#ffffff",fontSize:11,formatter:({value:e})=>c(e)}}]}}function C(t){return{animationDuration:700,grid:{left:78,right:72,top:22,bottom:20},tooltip:d(m({},s()),{trigger:"axis",axisPointer:{type:"shadow"}}),xAxis:{type:"value",axisLine:{show:!1},axisTick:{show:!1},splitLine:{lineStyle:{color:"rgba(48, 220, 255, 0.1)",type:"dashed"}},axisLabel:{color:"rgba(223, 250, 255, 0.58)",fontSize:11}},yAxis:{type:"category",inverse:!0,data:t.map(e=>e.name),axisTick:{show:!1},axisLine:{lineStyle:{color:"rgba(48, 220, 255, 0.22)"}},axisLabel:{color:"rgba(223, 250, 255, 0.82)",fontSize:12}},series:[{name:i.chart.title,type:"bar",barWidth:13,data:t.map((e,n)=>({value:e.value,name:e.name,itemStyle:{borderRadius:[0,8,8,0],color:new b(0,0,1,0,[{offset:0,color:"rgba(48, 220, 255, 0.16)"},{offset:1,color:l[n%l.length]}])}})),label:{show:!0,position:"right",color:"#ffffff",fontSize:11,formatter:({value:e})=>c(e)}}]}}function N(t){return{animationDuration:700,tooltip:s(),legend:{orient:"vertical",right:10,top:"middle",itemWidth:10,itemHeight:10,textStyle:{color:"rgba(223, 250, 255, 0.78)",fontSize:11}},series:[{name:i.chart.title,type:"pie",radius:["48%","68%"],center:["35%","52%"],avoidLabelOverlap:!0,label:{color:"#fff",fontSize:11,formatter:`{b} var B=Object.defineProperty,O=Object.defineProperties;var D=Object.getOwnPropertyDescriptors;var S=Object.getOwnPropertySymbols;var W=Object.prototype.hasOwnProperty,$=Object.prototype.propertyIsEnumerable;var v=(a,r,o)=>r in a?B(a,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):a[r]=o,m=(a,r)=>{for(var o in r||(r={}))W.call(r,o)&&v(a,o,r[o]);if(S)for(var o of S(r))$.call(r,o)&&v(a,o,r[o]);return a},d=(a,r)=>O(a,D(r));import{L as b,H}from"./index-9fb96bf8-1782919616283.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-1782919616283.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}; {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

@ -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-1782661552655.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-1782919616283.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}*,body{padding:0;margin:0}html,body,#app{width:100%;height:100%;overflow:hidden} /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}*,body{padding:0;margin:0}*{box-sizing:border-box}html,body,#app{width:100%;height:100%;overflow:hidden}body{font-family:AlibabaPuHuiTi,Microsoft YaHei,Arial,sans-serif;font-size:14px;color:#e7fbff}button,input,select,textarea{font:inherit}

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

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because it is too large Load Diff

@ -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-1782661552655.js"></script> <script type="module" crossorigin src="./assets/index-1782919616283.js"></script>
<link rel="stylesheet" href="./assets/index-b8a77371-balabala-1782661552655.css"> <link rel="stylesheet" href="./assets/index-47d6f37a-balabala-1782919616283.css">
</head> </head>
<body> <body>

7
package-lock.json generated

@ -12,6 +12,7 @@
"d3-geo": "^3.1.0", "d3-geo": "^3.1.0",
"delaunator": "^5.0.0", "delaunator": "^5.0.0",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"ezuikit-js": "^0.7.2",
"gsap": "^3.12.2", "gsap": "^3.12.2",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"point-in-polygon": "^1.1.0", "point-in-polygon": "^1.1.0",
@ -1058,6 +1059,12 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/ezuikit-js": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/ezuikit-js/-/ezuikit-js-0.7.2.tgz",
"integrity": "sha512-BECy42q371eT4TekLon7YVmiU0u6UwbqEOjqzvbSKd0fVDwFqEaSST72EaiqWhgdfqS+xUg31pMgt1FY6GweRA==",
"license": "ISC"
},
"node_modules/fflate": { "node_modules/fflate": {
"version": "0.6.10", "version": "0.6.10",
"resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.6.10.tgz", "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.6.10.tgz",

@ -13,6 +13,7 @@
"d3-geo": "^3.1.0", "d3-geo": "^3.1.0",
"delaunator": "^5.0.0", "delaunator": "^5.0.0",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"ezuikit-js": "^0.7.2",
"gsap": "^3.12.2", "gsap": "^3.12.2",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"point-in-polygon": "^1.1.0", "point-in-polygon": "^1.1.0",

File diff suppressed because one or more lines are too long

@ -0,0 +1,5 @@
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>牦牛产业链业务报告</title><style>
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#061018;color:#edfaff;font-family:"PingFang SC","Microsoft YaHei",Arial,sans-serif}.screen{position:relative;width:100vw;height:100vh;min-width:1280px;min-height:720px;background:radial-gradient(circle at 70% 44%,rgba(38,152,190,.2),transparent 30%),linear-gradient(180deg,#081b29,#06121c 56%,#040910)}.screen:before{content:"";position:absolute;inset:0;background-image:linear-gradient(rgba(75,174,198,.07) 1px,transparent 1px),linear-gradient(90deg,rgba(75,174,198,.06) 1px,transparent 1px);background-size:48px 48px;mask-image:linear-gradient(90deg,#000 0 44%,rgba(0,0,0,.65) 62%,transparent)}header{position:absolute;left:0;right:0;top:0;height:82px;display:flex;align-items:center;justify-content:center;background:linear-gradient(180deg,rgba(14,56,82,.9),transparent);z-index:3}h1{margin:0;font-size:30px;text-shadow:0 0 18px rgba(63,210,255,.48)}.sub{position:absolute;top:57px;font-size:13px;color:rgba(218,240,246,.72)}.report{position:absolute;left:24px;top:88px;bottom:28px;width:455px;display:flex;flex-direction:column;gap:10px;z-index:2}.card{border:1px solid rgba(91,209,244,.32);background:linear-gradient(180deg,rgba(7,30,47,.9),rgba(6,23,36,.76));box-shadow:inset 0 0 26px rgba(45,174,215,.08),0 10px 30px rgba(0,0,0,.18);border-radius:6px;padding:12px 14px;position:relative;overflow:hidden}.card:before{content:"";position:absolute;left:0;right:0;top:0;height:2px;background:linear-gradient(90deg,transparent,rgba(65,221,255,.8),transparent)}.title{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;font-size:16px;font-weight:650}.title span{font-size:12px;color:rgba(211,238,244,.64);font-weight:400}.kpi{height:126px}.grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.metric{height:39px;padding:6px 8px;border:1px solid rgba(94,188,224,.16);border-radius:5px;background:rgba(4,19,30,.54)}.metric label{font-size:12px;color:rgba(207,232,239,.68)}.metric strong{float:right;font-size:18px;color:#63e7ff}.metric small{margin-left:3px;color:rgba(222,241,247,.76)}.chain-section{height:102px}.section-head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}.section-head b{font-size:15px;color:#ecfdff}.section-head span{font-size:12px;color:#76e5ff}.desc{margin-top:4px;font-size:12px;color:rgba(218,238,244,.68);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mini{display:flex;gap:6px;margin-top:8px}.mini div{flex:1;min-width:0;padding:6px 7px;border-radius:5px;background:rgba(4,19,30,.52);border:1px solid rgba(94,188,224,.14);font-size:12px}.mini strong{display:block;margin-top:2px;font-size:16px;color:#a4e65c}.badges{margin-top:7px;display:flex;gap:5px;overflow:hidden}.badge{padding:4px 6px;border-radius:4px;background:rgba(5,22,34,.62);border:1px solid rgba(111,202,232,.18);font-size:11px;color:rgba(234,249,252,.88);white-space:nowrap}.canvas{position:absolute;left:520px;right:34px;top:130px;bottom:88px;border:1px solid rgba(80,193,230,.12);border-radius:8px;background:radial-gradient(circle at 45% 45%,rgba(47,146,164,.22),transparent 30%),rgba(4,17,27,.18);z-index:1}.canvas:before{content:"牦牛产业链业务主线";position:absolute;left:44px;top:38px;font-size:34px;font-weight:700;color:rgba(240,253,255,.9);text-shadow:0 0 18px rgba(64,210,255,.3)}.canvas:after{content:"资源草畜基础 → 养殖育种 → 饲草保障 → 屠宰加工 → 产业服务。当前只把有数据支撑的业务环节放入左侧报告。";position:absolute;left:46px;top:90px;font-size:15px;color:rgba(220,241,247,.68);max-width:760px;line-height:1.8}.flow{position:absolute;left:50px;right:70px;bottom:74px;display:grid;grid-template-columns:repeat(5,1fr);gap:12px}.node{height:78px;border:1px solid rgba(105,211,242,.24);border-radius:6px;background:rgba(7,30,47,.54);display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#e9fbff}.node:not(:last-child):after{content:"";position:absolute}.rings{position:absolute;right:12%;top:20%;width:360px;height:360px;border-radius:50%;border:1px solid rgba(95,220,245,.16)}.rings:before,.rings:after{content:"";position:absolute;border-radius:50%;border:1px dashed rgba(95,220,245,.18)}.rings:before{inset:54px}.rings:after{inset:110px;background:rgba(45,166,150,.08)}
</style></head><body><div class="screen"><header><h1>红原牦牛产业链业务报告</h1><div class="sub">按照产业链业务环节组织:资源、养殖、饲草、加工、服务</div></header><aside class="report"><section class="card kpi"><div class="title">产业链关键业务数<span>有数据支撑</span></div><div class="grid" id="kpis"></div></section><div id="sections"></div></aside><main class="canvas"><div class="rings"></div><div class="flow"><div class="node">资源草畜基础</div><div class="node">养殖育种</div><div class="node">饲草防灾</div><div class="node">屠宰加工</div><div class="node">产业服务</div></div></main></div><script>
const DATA={"kpis": [{"label": "家庭生态牧场", "value": 82, "unit": "户", "note": "源头经营主体"}, {"label": "养殖育种主体", "value": 52, "unit": "个", "note": "养殖与繁育"}, {"label": "饲草储备能力", "value": 10500.0, "unit": "吨", "note": "防灾保障"}, {"label": "加工屠宰主体", "value": 16, "unit": "家", "note": "转化承接"}], "sections": [{"key": "resource", "title": "一、资源与草畜基础", "desc": "草原资源、家庭牧场和牦牛生产承载,是产业链的底盘。", "codes": ["YC-GRASSLAND-RESOURCE", "YC-FAMILY-PASTURE"], "count": 85, "types": [{"name": "林草资源数据", "value": 3}, {"name": "家庭生态牧场", "value": 82}], "townRows": [{"name": "龙日镇", "value": 16}, {"name": "邛溪镇", "value": 16}, {"name": "瓦切镇", "value": 16}, {"name": "查尔玛乡", "value": 10}, {"name": "安曲镇", "value": 7}], "metrics": [{"name": "家庭牧场", "value": 82, "unit": "户"}, {"name": "资源指标", "value": 3, "unit": "项"}]}, {"key": "production", "title": "二、养殖与良种繁育", "desc": "标准化养殖基地和育种场决定源头供给能力。", "codes": ["YC-BREEDING-BASE", "YC-CXPT-BREEDING-BASE", "YC-BREEDING-STOCK", "YC-CXPT-BREEDING-STOCK-BASE"], "count": 52, "types": [{"name": "标准化养殖基地", "value": 19}, {"name": "cxpt养殖基地", "value": 28}, {"name": "育种繁育场", "value": 4}, {"name": "cxpt种畜基地", "value": 1}], "townRows": [{"name": "邛溪镇", "value": 6}, {"name": "安曲镇", "value": 4}, {"name": "瓦切镇", "value": 4}, {"name": "查尔玛乡", "value": 3}, {"name": "刷经寺镇", "value": 2}], "metrics": [{"name": "基地主体", "value": 52, "unit": "个"}, {"name": "设计存栏", "value": 9385.0, "unit": "头"}, {"name": "年出栏", "value": 18483.0, "unit": "头"}]}, {"key": "forage", "title": "三、饲草与防灾保障", "desc": "种草基地、草种基地和储备库决定补饲与抗灾能力。", "codes": ["YC-FORAGE-BASE", "YC-CXPT-FORAGE-BASE", "YC-CXPT-GRASS-SEED-BASE", "YC-DISASTER-FORAGE-STORAGE"], "count": 21, "types": [{"name": "种草饲草基地", "value": 8}, {"name": "cxpt饲草基地", "value": 5}, {"name": "cxpt草种基地", "value": 1}, {"name": "防灾减灾饲草储备库", "value": 7}], "townRows": [{"name": "瓦切镇", "value": 7}, {"name": "龙日镇", "value": 4}, {"name": "安曲镇", "value": 3}, {"name": "邛溪镇", "value": 1}, {"name": "刷经寺镇", "value": 1}], "metrics": [{"name": "保障主体", "value": 21, "unit": "个"}, {"name": "种植面积", "value": 21000.0, "unit": "亩"}, {"name": "储备库容", "value": 10500.0, "unit": "吨"}]}, {"key": "processing", "title": "四、屠宰与加工转化", "desc": "乳品加工、肉品加工和屠宰主体承接一产转化。", "codes": ["YC-PROCESSING-PLANT", "YC-SLAUGHTERHOUSE"], "count": 16, "types": [{"name": "加工厂", "value": 9}, {"name": "屠宰厂", "value": 7}], "townRows": [], "metrics": [{"name": "加工屠宰主体", "value": 16, "unit": "家"}, {"name": "年加工鲜奶", "value": 13000.0, "unit": "吨"}, {"name": "年屠宰量", "value": 68000.0, "unit": "头"}, {"name": "加工产值", "value": 59394.630000000005, "unit": "万元"}]}, {"key": "service", "title": "五、产业服务支撑", "desc": "金融、保险、防疫、培训、农机和交易服务支撑产业运行。", "codes": ["YC-DISEASE-CONTROL", "YC-TECH-TRAINING", "YC-SERVICE-FINANCE", "YC-SERVICE-INSURANCE", "YC-CXPT-OTHER-BASE", "YC-FARM-EQUIPMENT"], "count": 32, "types": [{"name": "疫病防控", "value": 3}, {"name": "技术培训", "value": 8}, {"name": "金融服务网点", "value": 4}, {"name": "保险服务网点", "value": 4}, {"name": "cxpt其他基地", "value": 11}, {"name": "农机装备", "value": 2}], "townRows": [{"name": "邛溪镇", "value": 7}, {"name": "瓦切镇", "value": 2}, {"name": "阿木乡", "value": 2}, {"name": "全县覆盖", "value": 1}, {"name": "马尔康市 / 邛溪镇", "value": 1}], "metrics": [{"name": "服务主体", "value": 32, "unit": "个"}, {"name": "服务类型", "value": 6, "unit": "类"}, {"name": "覆盖乡镇", "value": 55.0, "unit": "个次"}]}]};function fmt(v){if(typeof v==='string')return v;if(Math.abs(v)>=10000)return (v/10000).toFixed(1)+'万';return Number(v||0).toLocaleString('zh-CN')}document.getElementById('kpis').innerHTML=DATA.kpis.map(x=>`<div class="metric"><label>${x.label}</label><strong>${fmt(x.value)}<small>${x.unit}</small></strong></div>`).join('');document.getElementById('sections').innerHTML=DATA.sections.map(s=>`<section class="card chain-section"><div class="section-head"><b>${s.title}</b><span>${s.count}条</span></div><div class="desc">${s.desc}</div><div class="mini">${s.metrics.slice(0,3).map(m=>`<div>${m.name}<strong>${fmt(m.value)}${m.unit}</strong></div>`).join('')}</div><div class="badges">${s.types.slice(0,4).map(t=>`<span class="badge">${t.name} ${t.value}</span>`).join('')}</div></section>`).join('')
</script></body></html>

File diff suppressed because one or more lines are too long

@ -0,0 +1,5 @@
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>牦牛产业基础统计报告</title><style>
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#061018;color:#edfaff;font-family:"PingFang SC","Microsoft YaHei",Arial,sans-serif}.screen{position:relative;width:100vw;height:100vh;min-width:1280px;min-height:720px;background:radial-gradient(circle at 68% 42%,rgba(41,164,196,.22),transparent 28%),linear-gradient(180deg,#081d2c,#06121c 55%,#040910)}.screen:before{content:"";position:absolute;inset:0;background-image:linear-gradient(rgba(76,176,199,.07) 1px,transparent 1px),linear-gradient(90deg,rgba(76,176,199,.06) 1px,transparent 1px);background-size:48px 48px;mask-image:linear-gradient(90deg,#000 0 42%,rgba(0,0,0,.72) 62%,transparent)}header{position:absolute;left:0;right:0;top:0;height:82px;display:flex;align-items:center;justify-content:center;background:linear-gradient(180deg,rgba(14,56,82,.9),transparent);z-index:3}h1{margin:0;font-size:30px;text-shadow:0 0 18px rgba(63,210,255,.48)}.sub{position:absolute;top:57px;font-size:13px;color:rgba(218,240,246,.72)}.report{position:absolute;left:24px;top:88px;bottom:28px;width:430px;display:flex;flex-direction:column;gap:12px;z-index:2}.card{border:1px solid rgba(91,209,244,.32);background:linear-gradient(180deg,rgba(7,30,47,.88),rgba(6,23,36,.74));box-shadow:inset 0 0 26px rgba(45,174,215,.08),0 10px 30px rgba(0,0,0,.18);border-radius:6px;padding:14px 15px;position:relative;overflow:hidden}.card:before{content:"";position:absolute;left:0;right:0;top:0;height:2px;background:linear-gradient(90deg,transparent,rgba(65,221,255,.8),transparent)}.title{display:flex;justify-content:space-between;align-items:center;height:22px;margin-bottom:10px;font-size:16px;font-weight:650}.title span{font-size:12px;color:rgba(209,236,244,.62);font-weight:400}.summary{height:150px}.grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.metric{height:48px;padding:8px 10px;border:1px solid rgba(94,188,224,.16);border-radius:5px;background:rgba(4,19,30,.54)}.metric label{display:block;font-size:12px;color:rgba(207,232,239,.68)}.metric strong{display:inline-block;margin-top:4px;font-size:22px;line-height:1;color:#63e7ff}.metric small{margin-left:4px;color:rgba(222,241,247,.76)}.bars-card{height:150px}.ability{height:176px}.subcat{height:156px}.town{flex:1;min-height:140px}.bars{display:flex;flex-direction:column;gap:7px}.bar{display:grid;grid-template-columns:118px 1fr 54px;gap:8px;align-items:center;font-size:12px;color:rgba(231,247,250,.88)}.name{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.track{height:8px;border-radius:99px;background:rgba(110,157,175,.18);overflow:hidden}.fill{height:100%;border-radius:99px;background:linear-gradient(90deg,#35d7ff,#9cf1ff)}.ability-list{display:grid;grid-template-columns:1fr;gap:7px}.ability-row{display:flex;justify-content:space-between;align-items:center;padding:7px 9px;border:1px solid rgba(94,188,224,.13);border-radius:5px;background:rgba(4,19,30,.48);font-size:12px}.ability-row strong{font-size:18px;color:#a4e65c}.badges{display:flex;flex-wrap:wrap;gap:7px}.badge{padding:6px 8px;border:1px solid rgba(111,202,232,.18);border-radius:5px;background:rgba(5,22,34,.58);font-size:12px;color:rgba(234,249,252,.9)}.note{margin-top:8px;font-size:12px;color:rgba(218,239,245,.6)}.stage{position:absolute;left:500px;right:34px;top:130px;bottom:88px;border:1px solid rgba(80,193,230,.12);border-radius:8px;background:radial-gradient(circle at 45% 45%,rgba(47,146,164,.22),transparent 30%),rgba(4,17,27,.18);z-index:1}.stage:before{content:"产业数据基础报告";position:absolute;left:44px;top:38px;font-size:34px;font-weight:700;color:rgba(240,253,255,.9);text-shadow:0 0 18px rgba(64,210,255,.3)}.stage:after{content:"右侧区域可承载地图、主体点位或后续专题内容;当前不强行填充无依据图表。";position:absolute;left:46px;top:88px;font-size:15px;color:rgba(220,241,247,.68)}.rings{position:absolute;right:12%;top:22%;width:380px;height:380px;border-radius:50%;border:1px solid rgba(95,220,245,.16)}.rings:before,.rings:after{content:"";position:absolute;border-radius:50%;border:1px dashed rgba(95,220,245,.18)}.rings:before{inset:54px}.rings:after{inset:110px;background:rgba(45,166,150,.08)}
</style></head><body><div class="screen"><header><h1>红原牦牛产业基础统计报告</h1><div class="sub">只展示当前数据能够支撑的指标、分布和分类</div></header><aside class="report"><section class="card summary"><div class="title">核心指标<span>全链</span></div><div class="grid" id="summary"></div></section><section class="card bars-card"><div class="title">节点分布<span>主体数量</span></div><div class="bars" id="stageBars"></div></section><section class="card ability"><div class="title">能力指标<span>生产保障</span></div><div class="ability-list" id="abilities"></div></section><section class="card subcat"><div class="title">分类构成<span>业务类型</span></div><div class="badges" id="subcats"></div></section><section class="card town"><div class="title">乡镇分布<span>Top 8</span></div><div class="bars" id="townBars"></div><div class="note" id="townNote"></div></section></aside><main class="stage"><div class="rings"></div></main></div><script>
const DATA={"summary": [{"name": "全链主体", "value": 206, "unit": "条"}, {"name": "家庭牧场", "value": 82, "unit": "户"}, {"name": "养殖育种主体", "value": 52, "unit": "个"}, {"name": "服务主体", "value": 32, "unit": "个"}], "stageCounts": [{"name": "家庭生态牧场", "value": 82}, {"name": "养殖育种基地", "value": 52}, {"name": "饲草防灾保障", "value": 21}, {"name": "屠宰加工主体", "value": 16}, {"name": "社会化服务", "value": 32}, {"name": "林草资源底座", "value": 3}], "abilities": [{"name": "设计存栏", "value": 9385.0, "unit": "头"}, {"name": "年出栏量", "value": 18483.0, "unit": "头"}, {"name": "饲草种植面积", "value": 21000.0, "unit": "亩"}, {"name": "饲草储备库容", "value": 10500.0, "unit": "吨"}, {"name": "年加工鲜奶", "value": 13000.0, "unit": "吨"}, {"name": "年屠宰量", "value": 68000.0, "unit": "头"}, {"name": "加工屠宰产值", "value": 59394.630000000005, "unit": "万元"}], "subcats": [{"name": "家庭生态牧场", "value": 82, "group": "家庭生态牧场"}, {"name": "标准化养殖基地", "value": 19, "group": "养殖育种基地"}, {"name": "cxpt养殖基地", "value": 28, "group": "养殖育种基地"}, {"name": "育种繁育场", "value": 4, "group": "养殖育种基地"}, {"name": "cxpt种畜基地", "value": 1, "group": "养殖育种基地"}, {"name": "种草饲草基地", "value": 8, "group": "饲草防灾保障"}, {"name": "cxpt饲草基地", "value": 5, "group": "饲草防灾保障"}, {"name": "cxpt草种基地", "value": 1, "group": "饲草防灾保障"}, {"name": "防灾减灾饲草储备库", "value": 7, "group": "饲草防灾保障"}, {"name": "加工厂", "value": 9, "group": "屠宰加工主体"}, {"name": "屠宰厂", "value": 7, "group": "屠宰加工主体"}, {"name": "疫病防控", "value": 3, "group": "社会化服务"}, {"name": "技术培训", "value": 8, "group": "社会化服务"}, {"name": "金融服务网点", "value": 4, "group": "社会化服务"}, {"name": "保险服务网点", "value": 4, "group": "社会化服务"}, {"name": "cxpt其他基地", "value": 11, "group": "社会化服务"}, {"name": "农机装备", "value": 2, "group": "社会化服务"}, {"name": "林草资源数据", "value": 3, "group": "林草资源底座"}], "townRows": [{"name": "邛溪镇", "value": 30}, {"name": "瓦切镇", "value": 29}, {"name": "龙日镇", "value": 22}, {"name": "安曲镇", "value": 15}, {"name": "查尔玛乡", "value": 13}, {"name": "江茸乡", "value": 10}, {"name": "阿木乡", "value": 10}, {"name": "刷经寺镇", "value": 8}], "missingTown": 61};function fmt(v){return Number(v||0).toLocaleString('zh-CN')}function renderMetrics(){document.getElementById('summary').innerHTML=DATA.summary.map(x=>`<div class="metric"><label>${x.name}</label><strong>${fmt(x.value)}</strong><small>${x.unit}</small></div>`).join('')}function bars(id,rows){const max=Math.max(...rows.map(r=>+r.value||0),1);document.getElementById(id).innerHTML=rows.map(r=>`<div class="bar"><div class="name" title="${r.name}">${r.name}</div><div class="track"><div class="fill" style="width:${Math.max(4,(+r.value||0)/max*100)}%"></div></div><div>${fmt(r.value)}</div></div>`).join('')}function render(){renderMetrics();bars('stageBars',DATA.stageCounts);bars('townBars',DATA.townRows);document.getElementById('abilities').innerHTML=DATA.abilities.map(x=>`<div class="ability-row"><span>${x.name}</span><strong>${fmt(x.value)}<small>${x.unit}</small></strong></div>`).join('');document.getElementById('subcats').innerHTML=DATA.subcats.map(x=>`<span class="badge">${x.name} · ${x.value}</span>`).join('');document.getElementById('townNote').textContent=`另有 ${DATA.missingTown} 条主体未明确乡镇,暂不计入乡镇排行。`}render();
</script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@ -161,6 +161,15 @@ export const ecologicalProtectionTopics = [
}, },
] ]
export const yakIndustryChainTopics = [
{
key: "yak-industry-chain",
name: "牦牛产业链",
color: "#24F6C4",
defaultLayerKey: "yak-industry-points",
},
]
export const forestGrassWetLayers = { export const forestGrassWetLayers = {
woodland: [ woodland: [
{ {
@ -439,12 +448,194 @@ export const ecologicalProtectionLayers = {
], ],
} }
const yakTownCenters = {
色地乡: [103.14352108676084, 33.04086199974242],
色地镇: [103.14352108676084, 33.04086199974242],
瓦切乡: [102.68497870490992, 33.13952250013054],
瓦切镇: [102.68497870490992, 33.13952250013054],
刷经寺镇: [102.61668800740293, 31.972581499870266],
邛溪镇: [102.63374746802602, 32.82576351047847],
阿木乡: [102.83552513473182, 32.7263610000727],
安曲乡: [102.33517275101616, 32.71285091143605],
安曲镇: [102.33517275101616, 32.71285091143605],
查尔玛乡: [102.05164640151992, 32.45423049990481],
龙日乡: [102.40041964577492, 32.42345274031332],
龙日镇: [102.40041964577492, 32.42345274031332],
麦洼乡: [102.940261, 33.118079],
江茸乡: [102.26876480621743, 32.352965999818025],
壤口乡: [102.51205063384711, 32.23458652276696],
}
const yakDistributionRows = [
{ name: "色地乡", count: 11704 },
{ name: "麦洼乡", count: 5283 },
{ name: "壤口乡", count: 6109 },
{ name: "阿木乡", count: 8928 },
{ name: "瓦切镇", count: 8892 },
{ name: "查尔玛乡", count: 6868 },
{ name: "江茸乡", count: 5158 },
{ name: "龙日镇", count: 6994 },
{ name: "安曲镇", count: 9325 },
{ name: "刷经寺镇", count: 500 },
{ name: "邛溪镇", count: 9334 },
]
const yakRecognitionRows = [
{ name: "色地乡", count: 131983, devices: 25 },
{ name: "瓦切乡", count: 126611, devices: 24 },
{ name: "安曲乡", count: 99331, devices: 20 },
{ name: "邛溪镇", count: 81727, devices: 18 },
{ name: "麦洼乡", count: 78489, devices: 16 },
{ name: "龙日乡", count: 66846, devices: 15 },
{ name: "阿木乡", count: 64176, devices: 14 },
{ name: "查尔玛乡", count: 47958, devices: 11 },
{ name: "江茸乡", count: 34576, devices: 9 },
{ name: "刷经寺镇", count: 30407, devices: 8 },
{ name: "壤口乡", count: 19744, devices: 6 },
]
function createYakPointFeature(item, index, layerKey, metricLabel) {
const coordinates = yakTownCenters[item.name]
return {
type: "Feature",
properties: {
id: `${layerKey}-${index + 1}`,
name: item.name,
town: item.name,
count: item.count,
value: item.count,
devices: item.devices,
metricLabel,
},
geometry: {
type: "Point",
coordinates,
},
}
}
function createYakPointCollection(rows, layerKey, metricLabel) {
return {
type: "FeatureCollection",
features: rows
.filter((item) => yakTownCenters[item.name])
.map((item, index) => createYakPointFeature(item, index, layerKey, metricLabel)),
}
}
export const yakIndustryChainLayers = {
"yak-industry-chain": [
{
key: "yak-distribution-layer",
name: "牦牛分布",
sourceType: "geojson",
geojsonData: createYakPointCollection(yakDistributionRows, "yak-distribution-layer", "牦牛数量"),
opacity: 0.94,
mapView: {
targetLngLat: [102.58, 32.72],
distanceScale: 0.82,
targetLocalZ: 0.76,
duration: 0.85,
},
legends: [
{
key: "yak-distribution-all",
name: "牦牛分布",
fillColor: "#24F6C4",
strokeColor: "#B7FFF2",
selectable: false,
},
],
},
{
key: "yak-recognition-layer",
name: "识别数据",
sourceType: "geojson",
geojsonData: createYakPointCollection(yakRecognitionRows, "yak-recognition-layer", "识别数量"),
opacity: 0.94,
mapView: {
targetLngLat: [102.58, 32.72],
distanceScale: 0.82,
targetLocalZ: 0.76,
duration: 0.85,
},
legends: [
{
key: "yak-recognition-all",
name: "识别数据",
fillColor: "#32DCFF",
strokeColor: "#B8F7FF",
selectable: false,
},
],
},
{
key: "yak-industry-points",
name: "牦牛产业点位",
sourceType: "geojson",
dataPath: "datas/yak-industry-chain/yak_industry_points.geojson",
opacity: 0.96,
mapView: {
targetLngLat: [102.48, 32.69],
distanceScale: 0.82,
targetLocalZ: 0.76,
duration: 0.85,
},
legends: [
{
key: "yak-industry-family-pasture",
name: "家庭生态牧场",
fillColor: "#24F6C4",
strokeColor: "#B7FFF2",
match: { field: "stageKey", value: "family-pasture" },
},
{
key: "yak-industry-artificial-grassland",
name: "人工草地",
fillColor: "#7EE269",
strokeColor: "#D6FF8D",
match: { field: "stageKey", value: "artificial-grassland" },
},
{
key: "yak-industry-scale-breeding",
name: "规模化养殖基地",
fillColor: "#32DCFF",
strokeColor: "#B8F7FF",
match: { field: "stageKey", value: "scale-breeding" },
},
{
key: "yak-industry-forage-storage",
name: "防灾减灾",
fillColor: "#FFE06B",
strokeColor: "#FFF2A0",
match: { field: "stageKey", value: "forage-storage" },
},
{
key: "yak-industry-slaughter-processing",
name: "屠宰与加工",
fillColor: "#FF9A62",
strokeColor: "#FFD0A8",
match: { field: "stageKey", value: "slaughter-processing" },
},
{
key: "yak-industry-social-service",
name: "社会化服务",
fillColor: "#9EA7FF",
strokeColor: "#DFE3FF",
match: { field: "stageKey", value: "social-service" },
},
],
},
],
}
const layerRegistry = { const layerRegistry = {
...forestGrassWetLayers, ...forestGrassWetLayers,
...ecologicalProtectionLayers, ...ecologicalProtectionLayers,
...yakIndustryChainLayers,
} }
const topicRegistry = [...forestGrassWetTopics, ...ecologicalProtectionTopics] const topicRegistry = [...forestGrassWetTopics, ...ecologicalProtectionTopics, ...yakIndustryChainTopics]
export function getTopicConfig(topicKey) { export function getTopicConfig(topicKey) {
return topicRegistry.find((topic) => topic.key === topicKey) || forestGrassWetTopics[0] return topicRegistry.find((topic) => topic.key === topicKey) || forestGrassWetTopics[0]

@ -17,12 +17,6 @@ export const platformModules = [
bottomLabel: "生态保护", bottomLabel: "生态保护",
status: "ready", status: "ready",
}, },
{
key: "grass-livestock-balance",
label: "草畜平衡",
bottomLabel: "草畜平衡",
status: "ready",
},
{ {
key: "yak-management", key: "yak-management",
label: "牦牛管理", label: "牦牛管理",
@ -47,6 +41,12 @@ export const platformModules = [
bottomLabel: "智慧管理", bottomLabel: "智慧管理",
status: "ready", status: "ready",
}, },
{
key: "grass-livestock-balance",
label: "草畜平衡",
bottomLabel: "草畜平衡",
status: "ready",
},
] ]
export function getModuleByKey(key) { export function getModuleByKey(key) {

@ -0,0 +1,116 @@
import hongyuanTownshipsRaw from "/public/datas/geojsons/hongyuan-townships.geojson?raw"
const townshipNameAliases = {
安曲乡: "安曲镇",
龙日乡: "龙日镇",
瓦切乡: "瓦切镇",
色地乡: "色地镇",
壤口乡: "刷经寺镇",
}
const legacyTownCodeNames = {
"513233100": "邛溪镇",
"513233101": "刷经寺镇",
"513233103": "安曲镇",
"513233105": "龙日镇",
"513233200": "安曲镇",
"513233201": "龙日镇",
"513233202": "江茸乡",
"513233203": "查尔玛乡",
"513233204": "瓦切镇",
"513233205": "阿木乡",
"513233206": "刷经寺镇",
"513233207": "麦洼乡",
"513233208": "色地镇",
}
const townshipEnNames = {
邛溪镇: "qiongxi",
刷经寺镇: "shuajingsi",
安曲镇: "anqu",
龙日镇: "longri",
江茸乡: "jiangrong",
查尔玛乡: "chaerma",
瓦切镇: "waqie",
阿木乡: "amu",
麦洼乡: "maiwa",
色地镇: "sedi",
}
const townshipValues = {
色地镇: 1199,
瓦切镇: 991,
刷经寺镇: 986,
邛溪镇: 896,
阿木乡: 892,
安曲镇: 885,
查尔玛乡: 729,
龙日镇: 645,
麦洼乡: 559,
江茸乡: 514,
}
function parseGeoJson(raw) {
try {
return JSON.parse(raw)
} catch (error) {
return { type: "FeatureCollection", features: [] }
}
}
function readPoint(feature = {}) {
const properties = feature.properties || {}
const point = properties.cp || properties.center || properties.centroid || properties.sourceCenter
if (Array.isArray(point) && Number.isFinite(Number(point[0])) && Number.isFinite(Number(point[1]))) {
return [Number(point[0]), Number(point[1])]
}
return [0, 0]
}
export const hongyuanTownshipsGeoJsonRaw = hongyuanTownshipsRaw
export const hongyuanTownshipsGeoJson = parseGeoJson(hongyuanTownshipsRaw)
export const hongyuanTownshipFeatures = hongyuanTownshipsGeoJson.features || []
const townshipsByCode = hongyuanTownshipFeatures.reduce((result, feature) => {
const properties = feature.properties || {}
const name = normalizeHongyuanTownName(properties.name)
;[properties.code, properties.fullCode].forEach((code) => {
const normalizedCode = String(code || "").replace(/\D/g, "")
if (normalizedCode && name) result[normalizedCode] = name
})
return result
}, { ...legacyTownCodeNames })
export const hongyuanTownshipLabelPoints = hongyuanTownshipFeatures.map((feature) => {
const properties = feature.properties || {}
const name = normalizeHongyuanTownName(properties.name)
const center = readPoint(feature)
return {
name,
code: properties.code,
fullCode: properties.fullCode,
center,
centroid: center,
acreageMu: Number(properties.acreageMu) || 0,
}
})
export const hongyuanTownshipRankingPoints = hongyuanTownshipLabelPoints
.map((item) => ({
...item,
enName: townshipEnNames[item.name] || "",
value: townshipValues[item.name] || 0,
}))
.sort((a, b) => b.value - a.value)
export function normalizeHongyuanTownName(value) {
const text = String(value || "").trim()
if (!text || text === "--") return ""
return townshipNameAliases[text] || text
}
export function getHongyuanTownshipNameByCode(value) {
const normalizedCode = String(value || "").replace(/\D/g, "")
if (!normalizedCode) return ""
return townshipsByCode[normalizedCode] || townshipsByCode[normalizedCode.slice(0, 12)] || townshipsByCode[normalizedCode.slice(0, 9)] || ""
}

@ -1,4 +1,5 @@
const AUTH_COOKIE_NAME = "Authorization" const AUTH_COOKIE_NAME = "Authorization"
const COOKIE_PATHS = ["/", "/screen2", "/screen2/"]
function encodeRedirect(path) { function encodeRedirect(path) {
return encodeURIComponent(path || "/platform") return encodeURIComponent(path || "/platform")
@ -13,7 +14,15 @@ export function getAuthorizationToken() {
} }
export function clearAuthorizationToken() { export function clearAuthorizationToken() {
document.cookie = `${AUTH_COOKIE_NAME}=; Max-Age=0; path=/` const hostname = window.location.hostname
COOKIE_PATHS.forEach((path) => {
document.cookie = `${AUTH_COOKIE_NAME}=; Max-Age=0; path=${path}`
document.cookie = `${AUTH_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}`
if (hostname) {
document.cookie = `${AUTH_COOKIE_NAME}=; Max-Age=0; path=${path}; domain=${hostname}`
document.cookie = `${AUTH_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}; domain=${hostname}`
}
})
} }
export function getLoginPath(redirect = window.location.hash.replace(/^#/, "") || "/platform") { export function getLoginPath(redirect = window.location.hash.replace(/^#/, "") || "/platform") {
@ -29,6 +38,7 @@ export function redirectToLogin(redirect) {
} }
export async function login(username, password) { export async function login(username, password) {
clearAuthorizationToken()
const response = await fetch("/login", { const response = await fetch("/login", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",

@ -1,6 +1,6 @@
import { normalizeResponseData, requestJson } from "./request" import { normalizeResponseData, requestJson } from "./request"
const JSY_API_BASE = import.meta.env.VITE_JSY_API_BASE || "/jsyApi/api" const JSY_API_BASE = normalizeJsyApiBase(import.meta.env.VITE_JSY_API_BASE || "/jsyApi")
const OPEN_API_BASE = import.meta.env.VITE_OPEN_API_BASE || "/openApi" const OPEN_API_BASE = import.meta.env.VITE_OPEN_API_BASE || "/openApi"
function joinBase(base, path) { function joinBase(base, path) {
@ -8,6 +8,11 @@ function joinBase(base, path) {
return `${base}${path.startsWith("/") ? path : `/${path}`}` return `${base}${path.startsWith("/") ? path : `/${path}`}`
} }
function normalizeJsyApiBase(base) {
const normalized = String(base || "/jsyApi").replace(/\/+$/, "")
return normalized.endsWith("/api") ? normalized : `${normalized}/api`
}
function unwrapResponse(response) { function unwrapResponse(response) {
const failed = const failed =
response?.success === false || response?.success === false ||
@ -37,20 +42,60 @@ function normalizeRows(data) {
return [] return []
} }
function hasMeaningfulBalanceResult(data) {
if (Array.isArray(data)) return data.length > 0
if (!data || typeof data !== "object") return data !== undefined && data !== null && data !== ""
const balanceKeys = [
"areaRange",
"area_range",
"acreage",
"grasslandArea",
"grassland_area",
"area",
"yakNumber",
"yak_number",
"number",
"yakCount",
"yak_count",
"livestockCount",
"livestock_count",
"bearingCapacity",
"bearing_capacity",
"capacity",
"theoryCapacity",
"livestockCapacity",
"livestock_capacity",
"carryingCapacity",
"actualCapacity",
"overloadRate",
"overload_rate",
"rate",
"level",
"conclusion",
"advice",
"balanceAdvice",
"balance_advice",
]
return balanceKeys.some((key) => data[key] !== undefined && data[key] !== null && data[key] !== "")
}
export async function getBalanceAdvice({ areaCode, pasturesId } = {}) { export async function getBalanceAdvice({ areaCode, pasturesId } = {}) {
const params = { const params = {
areaCode, areaCode,
pasturesId, pasturesId,
} }
const requests = [ const requests = [
() => get(OPEN_API_BASE, "/ai/balance/advice", params, { timeout: 18000 }),
() => get(JSY_API_BASE, "/ai/livestockBalance", params, { timeout: 18000 }), () => get(JSY_API_BASE, "/ai/livestockBalance", params, { timeout: 18000 }),
() => get(OPEN_API_BASE, "/ai/balance/advice", params, { timeout: 18000 }),
] ]
let lastError let lastError
for (const request of requests) { for (const request of requests) {
try { try {
return await request() const result = await request()
if (hasMeaningfulBalanceResult(result)) return result
} catch (error) { } catch (error) {
lastError = error lastError = error
} }
@ -67,14 +112,33 @@ export async function queryBalanceTargets(question) {
let lastError let lastError
for (const request of requests) { for (const request of requests) {
try { try {
return normalizeRows(await request()) const rows = normalizeRows(await request())
if (rows.length) return rows
} catch (error) { } catch (error) {
lastError = error lastError = error
} }
} }
throw lastError || new Error("草畜平衡对象识别接口不可用") if (lastError) throw lastError
return []
} }
export function getAreaChildren(areaCode) { export async function getAreaChildren(areaCode) {
return get(JSY_API_BASE, "/area/children", { areaCode }, { timeout: 12000 }).then(normalizeRows) const requests = [
() => get(JSY_API_BASE, "/area/children", { areaCode }, { timeout: 12000 }),
() => get(JSY_API_BASE, "/map/areaCode", { areaCode }, { timeout: 12000 }),
]
let lastError
let hasEmptyResult = false
for (const request of requests) {
try {
const rows = normalizeRows(await request())
if (rows.length) return rows
hasEmptyResult = true
} catch (error) {
lastError = error
}
}
if (hasEmptyResult) return []
throw lastError || new Error("区划接口不可用")
} }

@ -0,0 +1,113 @@
import { normalizeResponseData, requestJson } from "./request"
const JSY_API_BASE = import.meta.env.VITE_JSY_API_BASE || "/jsyApi"
const HONGYUAN_TOWN_NAMES = [
"邛溪镇",
"刷经寺镇",
"安曲镇",
"龙日镇",
"江茸乡",
"查尔玛乡",
"瓦切镇",
"阿木乡",
"麦洼乡",
"色地镇",
]
function joinBase(path) {
if (/^https?:\/\//.test(path)) return path
return `${JSY_API_BASE}${path.startsWith("/") ? path : `/${path}`}`
}
function unwrapResponse(response) {
const failed =
response?.success === false ||
(response?.code !== undefined && ![0, 200, "0", "200"].includes(response.code))
if (failed) {
const error = new Error(response?.msg || response?.message || "视频监控接口请求失败")
error.code = response?.code
throw error
}
return normalizeResponseData(response)
}
function pickValue(item, keys, fallback = "") {
for (const key of keys) {
if (item?.[key] !== undefined && item?.[key] !== null && item?.[key] !== "") {
return item[key]
}
}
return fallback
}
function inferTownName(name = "") {
const normalizedName = String(name)
const town = HONGYUAN_TOWN_NAMES.find((item) => normalizedName.includes(item.replace(/[镇乡]$/, "")))
return town || "红原县"
}
function inferSceneName(name = "") {
const text = String(name)
if (text.includes("养殖")) return "养殖基地"
if (text.includes("饲草")) return "饲草基地"
if (text.includes("种草") || text.includes("牧草")) return "牧草基地"
if (text.includes("虫情")) return "虫情监测"
if (text.includes("机场")) return "机场周边"
return "重点点位"
}
function getChannel(row, index) {
const url = pickValue(row, ["hdPlayUrl", "playUrl", "url"])
const match = String(url).match(/\/(\d+)(?:\.hd)?\.live/i)
if (match) return match[1].padStart(2, "0")
return String(index + 1).padStart(2, "0")
}
function getUpdateTime() {
return new Date().toLocaleTimeString("zh-CN", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
}
function normalizeMonitoring(row, index, total, validTotal) {
const name = pickValue(row, ["name", "title", "deviceName"], `视频监控${index + 1}`)
const hdPlayUrl = pickValue(row, ["hdPlayUrl", "hd_play_url", "url"])
const playUrl = pickValue(row, ["playUrl", "play_url"], hdPlayUrl)
const token = pickValue(row, ["token", "accessToken", "access_token"])
const isOnline = Boolean(token && (hdPlayUrl || playUrl))
const onlineRate = total ? Math.round((validTotal / total) * 100) : 0
return {
key: pickValue(row, ["id", "number", "deviceSerial"], `${name}-${index}`),
title: name,
town: inferTownName(name),
scene: inferSceneName(name),
device: pickValue(row, ["number", "deviceSerial", "serialNumber"], `CAM-${String(index + 1).padStart(3, "0")}`),
channel: getChannel(row, index),
cameraCount: total,
status: isOnline ? "直播中" : "离线",
onlineRate,
updateTime: getUpdateTime(),
operator: isOnline ? "萤石云 · 在线预览" : "监控参数待同步",
progress: isOnline ? 100 : 0,
image: pickValue(row, ["img", "image", "cover", "snapshot"]),
playUrl,
hdPlayUrl,
token,
source: row,
}
}
export async function getHomeMonitorings(params = {}) {
const data = await requestJson(joinBase("/api/home/monitorings"), {
params,
timeout: 18000,
}).then(unwrapResponse)
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
return rows.map((row, index) => normalizeMonitoring(row, index, rows.length, validTotal))
}

@ -717,6 +717,42 @@ function fillPanelSlots(primaryPanels = [], fallbackPanels = [], count = 4) {
return result.slice(0, count) return result.slice(0, count)
} }
function clonePanelAs(panel, type, options = {}) {
if (!panel) return panel
return {
...panel,
type,
...options,
}
}
function diversifyPanelColumn(panels = []) {
const preferredTypes = ["bar", "line", "donut", "column", "structure", "heat", "peak", "split"]
const nextTypeMap = {
bar: ["heat", "peak", "column", "structure"],
line: ["bar", "column", "heat"],
donut: ["structure", "split", "bar"],
column: ["peak", "heat", "bar", "line"],
structure: ["split", "donut", "bar"],
heat: ["bar", "column", "structure"],
peak: ["column", "bar", "line"],
split: ["structure", "donut", "bar"],
}
const used = new Set()
return panels.map((panel) => {
if (!panel?.type || ["projectTable", "iconMetric", "metric", "spotlight", "rank", "table", "stackColumn"].includes(panel.type)) {
return panel
}
if (!used.has(panel.type)) {
used.add(panel.type)
return panel
}
const nextType = (nextTypeMap[panel.type] || preferredTypes).find((type) => !used.has(type)) || panel.type
used.add(nextType)
return clonePanelAs(panel, nextType, { variant: nextType === panel.type ? panel.variant : undefined })
})
}
function buildHeadTailRows(rows = [], options = {}) { function buildHeadTailRows(rows = [], options = {}) {
const { const {
headCount = 4, headCount = 4,
@ -925,18 +961,18 @@ function buildWetlandDefaultPanels({
const typeTownPanel = buildPanel("wetland-type-town", "湿地类型乡镇分布", topRows(typeTownRows, 6), "bar", { span: 1, variant: "distribution" }) const typeTownPanel = buildPanel("wetland-type-town", "湿地类型乡镇分布", topRows(typeTownRows, 6), "bar", { span: 1, variant: "distribution" })
const edibleMuPanel = buildPanel("wetland-edible-mu", "可食鲜草亩产", topRows(edibleMuRows, 6), "bar", { span: 1 }) const edibleMuPanel = buildPanel("wetland-edible-mu", "可食鲜草亩产", topRows(edibleMuRows, 6), "bar", { span: 1 })
const minTownPanel = buildPanel("wetland-min-town", "优势小类乡镇分布", topRows(minTownRows, 6), "bar", { span: 1, variant: "distribution" }) const minTownPanel = buildPanel("wetland-min-town", "优势小类乡镇分布", topRows(minTownRows, 6), "bar", { span: 1, variant: "distribution" })
const leftPanels = [ const leftPanels = diversifyPanelColumn([
townAreaPanel, townAreaPanel,
typePanel, typePanel,
levelStructurePanel, levelStructurePanel,
minTypePanel, minTypePanel,
] ])
const rightPanels = [ const rightPanels = diversifyPanelColumn([
edibleYieldPanel, edibleYieldPanel,
yieldPanel, yieldPanel,
muYieldPanel, muYieldPanel,
yieldCapacityPanel, yieldCapacityPanel,
] ])
return { return {
leftPanels, leftPanels,
rightPanels, rightPanels,
@ -978,20 +1014,20 @@ function buildWetlandLayerPanels(panels = {}) {
} = panels } = panels
return { return {
"wetland-distribution": { "wetland-distribution": {
leftPanels: [townAreaPanel, typePanel, levelStructurePanel, minTypePanel].filter(Boolean), leftPanels: diversifyPanelColumn([townAreaPanel, typePanel, levelStructurePanel, minTypePanel].filter(Boolean)),
rightPanels: [edibleYieldPanel, yieldPanel, muYieldPanel, yieldCapacityPanel].filter(Boolean), rightPanels: diversifyPanelColumn([edibleYieldPanel, yieldPanel, muYieldPanel, yieldCapacityPanel].filter(Boolean)),
}, },
"wetland-level": { "wetland-level": {
leftPanels: [levelStructurePanel, levelPanel, levelTownPanel, typePanel].filter(Boolean), leftPanels: diversifyPanelColumn([levelStructurePanel, levelPanel, levelTownPanel, typePanel].filter(Boolean)),
rightPanels: [edibleYieldPanel, yieldPanel, muYieldPanel, yieldCapacityPanel].filter(Boolean), rightPanels: diversifyPanelColumn([edibleYieldPanel, yieldPanel, muYieldPanel, yieldCapacityPanel].filter(Boolean)),
}, },
"wetland-type": { "wetland-type": {
leftPanels: [typePanel, typeTownPanel, minTypePanel, minTownPanel].filter(Boolean), leftPanels: diversifyPanelColumn([typePanel, typeTownPanel, minTypePanel, minTownPanel].filter(Boolean)),
rightPanels: [edibleYieldPanel, yieldPanel, edibleMuPanel, yieldCapacityPanel].filter(Boolean), rightPanels: diversifyPanelColumn([edibleYieldPanel, yieldPanel, edibleMuPanel, yieldCapacityPanel].filter(Boolean)),
}, },
"wetland-yield": { "wetland-yield": {
leftPanels: [townAreaPanel, typePanel, levelStructurePanel, minTypePanel].filter(Boolean), leftPanels: diversifyPanelColumn([townAreaPanel, typePanel, levelStructurePanel, minTypePanel].filter(Boolean)),
rightPanels: [edibleYieldPanel, yieldPanel, muYieldPanel, yieldCapacityPanel].filter(Boolean), rightPanels: diversifyPanelColumn([edibleYieldPanel, yieldPanel, muYieldPanel, yieldCapacityPanel].filter(Boolean)),
}, },
} }
} }
@ -1002,8 +1038,8 @@ function buildGrasslandLayerPanels(panels = {}) {
rightPanels = [], rightPanels = [],
} = panels } = panels
const stablePanels = { const stablePanels = {
leftPanels: leftPanels.filter(Boolean), leftPanels: diversifyPanelColumn(leftPanels.filter(Boolean)),
rightPanels: rightPanels.filter(Boolean), rightPanels: diversifyPanelColumn(rightPanels.filter(Boolean)),
} }
return { return {
"grassland-distribution": stablePanels, "grassland-distribution": stablePanels,
@ -1051,14 +1087,14 @@ function buildWoodlandDefaultPanels({
const qualityLevelPanel = buildPanel("woodland-quality-level", "林地质量等级", withPercentExtra(qualityLevelRows), "donut", { span: 1 }) const qualityLevelPanel = buildPanel("woodland-quality-level", "林地质量等级", withPercentExtra(qualityLevelRows), "donut", { span: 1 })
const townConcentrationPanel = buildPanel("woodland-town-concentration", "林地集中度", withPercentExtra(townConcentrationRows), "structure", { span: 1 }) const townConcentrationPanel = buildPanel("woodland-town-concentration", "林地集中度", withPercentExtra(townConcentrationRows), "structure", { span: 1 })
const typeConcentrationPanel = buildPanel("woodland-type-concentration", "类型集中度", withPercentExtra(typeConcentrationRows), "structure", { span: 1 }) const typeConcentrationPanel = buildPanel("woodland-type-concentration", "类型集中度", withPercentExtra(typeConcentrationRows), "structure", { span: 1 })
const leftPanels = fillPanelSlots( const leftPanels = diversifyPanelColumn(fillPanelSlots(
[townPanel, typePanel, protectionPanel, ownershipPanel], [townPanel, typePanel, protectionPanel, ownershipPanel],
[scalePanel, ownerPanel, forestTypePanel, qualityPanel] [scalePanel, ownerPanel, forestTypePanel, qualityPanel]
) ))
const rightPanels = fillPanelSlots( const rightPanels = diversifyPanelColumn(fillPanelSlots(
[qualityPanel, originPanel, agePanel, canopyPanel], [qualityPanel, originPanel, agePanel, canopyPanel],
[scalePanel, ownerPanel, forestTypePanel, regionPanel, townConcentrationPanel, typeConcentrationPanel] [scalePanel, ownerPanel, forestTypePanel, regionPanel, townConcentrationPanel, typeConcentrationPanel]
) ))
return { return {
leftPanels, leftPanels,
rightPanels, rightPanels,
@ -1108,24 +1144,24 @@ function buildWoodlandLayerPanels(panels = {}) {
} = panels } = panels
return { return {
"woodland-distribution": { "woodland-distribution": {
leftPanels: fillPanelSlots([townPanel, typePanel, protectionPanel, ownershipPanel], [scalePanel, forestTypePanel, ownerPanel]), leftPanels: diversifyPanelColumn(fillPanelSlots([townPanel, typePanel, protectionPanel, ownershipPanel], [scalePanel, forestTypePanel, ownerPanel])),
rightPanels: fillPanelSlots([qualityPanel, originPanel, agePanel, canopyPanel], [scalePanel, ownerPanel, regionPanel, townConcentrationPanel, typeConcentrationPanel, qualityLevelPanel]), rightPanels: diversifyPanelColumn(fillPanelSlots([qualityPanel, originPanel, agePanel, canopyPanel], [scalePanel, ownerPanel, regionPanel, townConcentrationPanel, typeConcentrationPanel, qualityLevelPanel])),
}, },
"woodland-protection": { "woodland-protection": {
leftPanels: fillPanelSlots([protectionPanel, forestTypePanel, manageLevelPanel, townPanel], [typePanel, ownershipPanel, scalePanel]), leftPanels: diversifyPanelColumn(fillPanelSlots([protectionPanel, forestTypePanel, manageLevelPanel, townPanel], [typePanel, ownershipPanel, scalePanel])),
rightPanels: fillPanelSlots([regionPanel, qualityPanel, originPanel, ownershipPanel], [ownerPanel, typePanel, scalePanel]), rightPanels: diversifyPanelColumn(fillPanelSlots([regionPanel, qualityPanel, originPanel, ownershipPanel], [ownerPanel, typePanel, scalePanel])),
}, },
"woodland-type": { "woodland-type": {
leftPanels: fillPanelSlots([typePanel, townPanel, typeTownPanel, scalePanel], [protectionPanel, ownershipPanel, forestTypePanel]), leftPanels: diversifyPanelColumn(fillPanelSlots([typePanel, townPanel, typeTownPanel, scalePanel], [protectionPanel, ownershipPanel, forestTypePanel])),
rightPanels: fillPanelSlots([qualityPanel, originPanel, protectionPanel, ownershipPanel], [agePanel, canopyPanel, ownerPanel, typeConcentrationPanel]), rightPanels: diversifyPanelColumn(fillPanelSlots([qualityPanel, originPanel, protectionPanel, ownershipPanel], [agePanel, canopyPanel, ownerPanel, typeConcentrationPanel])),
}, },
"woodland-quality": { "woodland-quality": {
leftPanels: fillPanelSlots([qualityPanel, originPanel, agePanel, canopyPanel], [qualityLevelPanel, scalePanel, typePanel]), leftPanels: diversifyPanelColumn(fillPanelSlots([qualityPanel, originPanel, agePanel, canopyPanel], [qualityLevelPanel, scalePanel, typePanel])),
rightPanels: fillPanelSlots([townPanel, typePanel, protectionPanel, ownershipPanel], [forestTypePanel, ownerPanel, scalePanel]), rightPanels: diversifyPanelColumn(fillPanelSlots([townPanel, typePanel, protectionPanel, ownershipPanel], [forestTypePanel, ownerPanel, scalePanel])),
}, },
"woodland-ownership": { "woodland-ownership": {
leftPanels: fillPanelSlots([ownershipPanel, ownerPanel, townPanel, typePanel], [scalePanel, protectionPanel, qualityPanel]), leftPanels: diversifyPanelColumn(fillPanelSlots([ownershipPanel, ownerPanel, townPanel, typePanel], [scalePanel, protectionPanel, qualityPanel])),
rightPanels: fillPanelSlots([protectionPanel, qualityPanel, originPanel, scalePanel], [forestTypePanel, agePanel, canopyPanel]), rightPanels: diversifyPanelColumn(fillPanelSlots([protectionPanel, qualityPanel, originPanel, scalePanel], [forestTypePanel, agePanel, canopyPanel])),
}, },
} }
} }
@ -1744,18 +1780,18 @@ function buildGrasslandDefaultPanels({
const healthPanel = buildPanel("grassland-health-proportion", "草地健康占比", healthRows, "donut", { span: 1 }) const healthPanel = buildPanel("grassland-health-proportion", "草地健康占比", healthRows, "donut", { 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 = [ const leftPanels = diversifyPanelColumn([
areaPanel, areaPanel,
typePanel, typePanel,
degradationPanel, degradationPanel,
levelPanel, levelPanel,
] ])
const rightPanels = [ const rightPanels = diversifyPanelColumn([
edibleYieldPanel, edibleYieldPanel,
productivityPanel, productivityPanel,
healthPanel, healthPanel,
foragePanel, foragePanel,
] ])
return { return {
core, core,
leftPanels, leftPanels,

@ -74,6 +74,10 @@ export function getPastureDetailsById(id) {
return get("/api/pastureDetailsById", { id }).then((data) => data?.data || data || {}) return get("/api/pastureDetailsById", { id }).then((data) => data?.data || data || {})
} }
export function getPasturesDetailsById(id) {
return get("/api/pasturesDetails", { id }).then((data) => data?.data || data || {})
}
export function getPastureList() { export function getPastureList() {
return get("/api/pasturesList").then(normalizeRows) return get("/api/pasturesList").then(normalizeRows)
} }
@ -85,16 +89,73 @@ export async function getPastureProfile() {
getPastureAge(), getPastureAge(),
getPastureOnline(), getPastureOnline(),
getPastureList(), getPastureList(),
getMuhuList("", 1, 80), getMuhuList("", 1, 300),
]) ])
const pastureRows = pastureList.status === "fulfilled" ? pastureList.value : [] const pastureRows = pastureList.status === "fulfilled" ? pastureList.value : []
const muhuRows = muhuList.status === "fulfilled" ? muhuList.value : [] const muhuRows = muhuList.status === "fulfilled" ? muhuList.value : []
const primaryList = hasPastureAreaRows(muhuRows) ? muhuRows : hasPastureAreaRows(pastureRows) ? pastureRows : (muhuRows.length ? muhuRows : pastureRows)
const list = mergePastureRows(primaryList, pastureRows, muhuRows)
return { return {
core: core.status === "fulfilled" ? core.value : [], core: core.status === "fulfilled" ? core.value : [],
distribution: distribution.status === "fulfilled" ? distribution.value : [], distribution: distribution.status === "fulfilled" ? distribution.value : [],
age: age.status === "fulfilled" ? age.value : [], age: age.status === "fulfilled" ? age.value : [],
online: online.status === "fulfilled" ? online.value : [], online: online.status === "fulfilled" ? online.value : [],
list: pastureRows.length ? pastureRows : muhuRows, list,
} }
} }
function mergePastureRows(primaryRows = [], ...sourceGroups) {
const sourceMap = new Map()
sourceGroups.flat().forEach((row) => {
getPastureRowKeys(row).forEach((key) => {
const current = sourceMap.get(key) || {}
sourceMap.set(key, fillMissingPastureFields(current, row))
})
})
return (Array.isArray(primaryRows) ? primaryRows : []).map((row) => {
const supplement = getPastureRowKeys(row)
.map((key) => sourceMap.get(key))
.find(Boolean)
return supplement ? fillMissingPastureFields(row, supplement) : row
})
}
function fillMissingPastureFields(target = {}, source = {}) {
const merged = { ...target }
Object.entries(source || {}).forEach(([key, value]) => {
if (merged[key] === undefined || merged[key] === null || merged[key] === "") {
merged[key] = value
}
})
return merged
}
function getPastureRowKeys(row = {}) {
return [
row.id,
row.pastureId,
row.pasturesId,
row.pastures_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)
}
function hasPastureAreaRows(rows = []) {
return (Array.isArray(rows) ? rows : []).some((row) => {
const value = row?.grasslandArea ?? row?.grassland_area ?? row?.acreage ?? row?.area ?? row?.areaRange ?? row?.area_range
if (typeof value === "number") return value > 0
const number = Number(String(value ?? "").replace(/,/g, "").match(/-?\d+(?:\.\d+)?/)?.[0])
return Number.isFinite(number) && number > 0
})
}

@ -3,6 +3,9 @@ body {
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
* {
box-sizing: border-box;
}
html, html,
body, body,
#app { #app {
@ -10,3 +13,14 @@ body,
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
body {
font-family: AlibabaPuHuiTi, "Microsoft YaHei", Arial, sans-serif;
font-size: 14px;
color: #e7fbff;
}
button,
input,
select,
textarea {
font: inherit;
}

@ -0,0 +1,158 @@
import * as echarts from "echarts"
export const chartFontFamily = "AlibabaPuHuiTi, Microsoft YaHei, Arial, sans-serif"
export const chartPalette = ["#24f6c4", "#32dcff", "#ffe06b", "#7ee269", "#ff9a62", "#9ea7ff"]
export const chartTextStyle = {
color: "rgba(225, 249, 255, 0.78)",
fontFamily: chartFontFamily,
fontSize: 10,
}
export function chartTooltip(formatter, overrides = {}) {
return {
trigger: "item",
backgroundColor: "rgba(5, 31, 51, 0.96)",
borderColor: "rgba(0, 210, 255, 0.54)",
borderWidth: 1,
padding: [7, 10],
textStyle: {
color: "#e7fbff",
fontFamily: chartFontFamily,
fontSize: 12,
lineHeight: 18,
},
extraCssText: "box-shadow:0 0 18px rgba(0,210,255,.2);",
formatter,
...overrides,
}
}
export function chartLegend(overrides = {}) {
return {
itemWidth: 10,
itemHeight: 7,
itemGap: 8,
textStyle: {
...chartTextStyle,
color: "rgba(222, 250, 255, 0.78)",
fontSize: 10,
},
...overrides,
}
}
export function categoryAxis(data = [], overrides = {}) {
return {
type: "category",
data,
axisLabel: {
...chartTextStyle,
color: "rgba(223, 250, 255, 0.68)",
interval: 0,
hideOverlap: true,
margin: 8,
...(overrides.axisLabel || {}),
},
axisLine: { lineStyle: { color: "rgba(48, 220, 255, 0.26)" }, ...(overrides.axisLine || {}) },
axisTick: { show: false, ...(overrides.axisTick || {}) },
splitLine: { show: false, ...(overrides.splitLine || {}) },
...stripAxisOverrides(overrides),
}
}
export function valueAxis(overrides = {}) {
return {
type: "value",
axisLabel: {
...chartTextStyle,
color: "rgba(223, 250, 255, 0.56)",
...(overrides.axisLabel || {}),
},
axisLine: { show: false, ...(overrides.axisLine || {}) },
axisTick: { show: false, ...(overrides.axisTick || {}) },
splitLine: {
lineStyle: { color: "rgba(48, 220, 255, 0.1)", type: "dashed" },
...(overrides.splitLine || {}),
},
...stripAxisOverrides(overrides),
}
}
export function chartGrid(overrides = {}) {
return {
left: 42,
right: 28,
top: 30,
bottom: 24,
containLabel: false,
...overrides,
}
}
export function verticalGradient(color, endColor = `${color}12`) {
return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color },
{ offset: 1, color: endColor },
])
}
export function horizontalGradient(color, startColor = `${color}20`, endColor = "#ffffff") {
return new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: startColor },
{ offset: 0.68, color },
{ offset: 1, color: endColor },
])
}
export function withChartDefaults(option = {}) {
const normalized = {
textStyle: {
fontFamily: chartFontFamily,
color: "rgba(225, 249, 255, 0.78)",
...(option.textStyle || {}),
},
animation: option.animation ?? true,
animationDuration: option.animationDuration ?? 900,
animationDurationUpdate: option.animationDurationUpdate ?? 700,
animationEasing: option.animationEasing || "cubicOut",
...option,
}
normalizeChartTypography(normalized)
return normalized
}
function stripAxisOverrides(overrides = {}) {
const { axisLabel, axisLine, axisTick, splitLine, ...rest } = overrides
return rest
}
function normalizeChartTypography(target) {
if (!target || typeof target !== "object") return
const styleKeys = ["textStyle", "axisLabel", "nameTextStyle", "label", "axisName"]
styleKeys.forEach((key) => {
if (!target[key] || typeof target[key] !== "object") return
target[key] = normalizeTextObject(target[key])
})
Object.keys(target).forEach((key) => {
const value = target[key]
if (!value || typeof value !== "object") return
if (key === "color" || key === "itemStyle" || key === "areaStyle" || key === "lineStyle") return
if (Array.isArray(value)) {
value.forEach(normalizeChartTypography)
return
}
normalizeChartTypography(value)
})
}
function normalizeTextObject(style) {
const next = {
fontFamily: style.fontFamily || chartFontFamily,
...style,
}
if (typeof next.fontSize === "number" && next.fontSize < 10) {
next.fontSize = 10
}
return next
}

@ -426,6 +426,34 @@ const parkYakVillageRows = buildParkVillageRows({
decimals: 0, decimals: 0,
}) })
const parkVillageMapPoints = {
洞拉村: [102.82, 33.22],
滚塘村: [102.9, 33.23],
让里村: [102.99, 33.23],
茸塔玛村: [103.08, 33.2],
唐日村: [102.5, 33.11],
德香村: [102.63, 33.12],
日干村: [102.72, 33.08],
色永村: [103.12, 33.04],
}
const parkVillageMapRows = parkYakVillageRows
.map((item) => {
const point = parkVillageMapPoints[item.name]
if (!point) return null
return {
...item,
lng: point[0],
lat: point[1],
value: item.value,
rawValue: item.value,
metricLabel: "牦牛数量",
displayUnit: item.unit,
displayMode: "park-village",
}
})
.filter(Boolean)
const parkEdibleGrassVillageRows = buildParkVillageRows({ const parkEdibleGrassVillageRows = buildParkVillageRows({
sourceKey: "edibleGrasslandWanTon", sourceKey: "edibleGrasslandWanTon",
weightKey: "edible", weightKey: "edible",
@ -508,15 +536,14 @@ export const parkPanels = {
{ {
key: "park-yak-village", key: "park-yak-village",
title: "牦牛分布村级排行", title: "牦牛分布村级排行",
type: "column", type: "bar",
variant: "pin", variant: "distribution",
rows: parkYakVillageRows.slice(0, 8), rows: parkYakVillageRows.slice(0, 8),
}, },
{ {
key: "park-edible-grass-village", key: "park-edible-grass-village",
title: "可食草地资源分布", title: "可食草地资源分布",
type: "column", type: "split",
variant: "slim",
rows: parkEdibleGrassVillageRows.slice(0, 8), rows: parkEdibleGrassVillageRows.slice(0, 8),
}, },
{ {
@ -609,7 +636,8 @@ export const restorationPanels = {
{ {
key: "restoration-town-area", key: "restoration-town-area",
title: "乡镇治理面积分布", title: "乡镇治理面积分布",
type: "line", type: "bar",
variant: "distribution",
rows: validTownRows.map((item) => ({ rows: validTownRows.map((item) => ({
key: item.name, key: item.name,
name: item.name, name: item.name,
@ -730,13 +758,13 @@ export function getEcologicalMapRanking(viewKey) {
} }
return { return {
status: "success", status: "success",
title: "国家公园牦牛分布", title: "国家公园覆盖村",
scope: "village",
labelMode: "village",
hideTownNames: false,
displayMode: "park-village",
metricLabel: "牦牛数量", metricLabel: "牦牛数量",
unit: "头", unit: "头",
rows: parkTownRows.map((item) => ({ rows: parkVillageMapRows,
name: item.name,
value: item.yakCount,
unit: "头",
})),
} }
} }

@ -159,7 +159,7 @@ const currentView = computed(() =>
["national-park", "restoration", "engineering-analysis"].includes(props.activeView) ? props.activeView : "national-park" ["national-park", "restoration", "engineering-analysis"].includes(props.activeView) ? props.activeView : "national-park"
) )
const isEngineeringAnalysis = computed(() => currentView.value === "engineering-analysis") const isEngineeringAnalysis = computed(() => currentView.value === "engineering-analysis")
const baseActiveLayer = computed(() => getLayerByKey(currentView.value, activeLayerKey.value)) const baseActiveLayer = computed(() => activeLayerKey.value ? getLayerByKey(currentView.value, activeLayerKey.value) : null)
const selectedLegend = computed(() => const selectedLegend = computed(() =>
(baseActiveLayer.value?.legends || []).find((item) => item.key === selectedLegendKey.value) || null (baseActiveLayer.value?.legends || []).find((item) => item.key === selectedLegendKey.value) || null
) )

@ -52,7 +52,7 @@ export class Assets {
{ {
type: "File", type: "File",
name: "mapJson", name: "mapJson",
path: base_url + "assets/json/红原县.json", path: base_url + "datas/geojsons/hongyuan-townships.geojson",
}, },
{ {
type: "File", type: "File",

@ -201,13 +201,14 @@
{ 'is-dense': chartRows.length > 6 }, { 'is-dense': chartRows.length > 6 },
]" ]"
> >
<small v-if="columnUnitLabel" class="business-column-unit">单位{{ columnUnitLabel }}</small>
<div <div
v-for="(item, index) in chartRows" v-for="(item, index) in chartRows"
:key="item.key || item.name" :key="item.key || item.name"
class="business-column-item" class="business-column-item"
:class="{ 'is-active': index === normalizedActiveIndex }" :class="{ 'is-active': index === normalizedActiveIndex }"
> >
<strong>{{ displayValue(item) }}</strong> <strong>{{ displayValue(item) }}<em v-if="columnItemUnit(item)">{{ columnItemUnit(item) }}</em></strong>
<div class="business-column-track"> <div class="business-column-track">
<i <i
:style="{ :style="{
@ -217,7 +218,7 @@
}" }"
></i> ></i>
</div> </div>
<span>{{ item.name }}</span> <span>{{ formatPlaceLabel(item.name) }}</span>
</div> </div>
</div> </div>
@ -325,7 +326,7 @@
:style="{ height: `${segment.percent}%`, background: chartColor(index) }" :style="{ height: `${segment.percent}%`, background: chartColor(index) }"
></i> ></i>
</div> </div>
<span>{{ item.name }}</span> <span>{{ formatPlaceLabel(item.name) }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -335,17 +336,18 @@
class="business-peaks" class="business-peaks"
:class="{ 'is-dense': chartRows.length > 5 }" :class="{ 'is-dense': chartRows.length > 5 }"
> >
<small v-if="peakUnitLabel" class="business-column-unit business-peak-unit">单位{{ peakUnitLabel }}</small>
<div <div
v-for="(item, index) in chartRows" v-for="(item, index) in chartRows"
:key="item.key || item.name" :key="item.key || item.name"
class="business-peak-item" class="business-peak-item"
:style="{ '--peak-height': `${barWidth(item)}%`, '--peak-color': chartColor(index) }" :style="{ '--peak-height': `${barWidth(item)}%`, '--peak-color': chartColor(index) }"
> >
<strong>{{ displayValue(item) }}</strong> <strong>{{ displayValue(item) }}<em v-if="peakItemUnit(item)">{{ peakItemUnit(item) }}</em></strong>
<div class="business-peak-stage"> <div class="business-peak-stage">
<i></i> <i></i>
</div> </div>
<span>{{ item.name }}</span> <span>{{ formatPlaceLabel(item.name) }}</span>
</div> </div>
</div> </div>
@ -387,7 +389,7 @@
</div> </div>
</div> </div>
<div class="business-line-labels" :style="lineLabelStyle"> <div class="business-line-labels" :style="lineLabelStyle">
<span v-for="item in lineLabelRows" :key="item.key || item.name">{{ item.name }}</span> <span v-for="item in lineLabelRows" :key="item.key || item.name">{{ formatPlaceLabel(item.name) }}</span>
</div> </div>
</div> </div>
@ -455,6 +457,14 @@ const sumValue = computed(() => values.value.reduce((sum, value) => sum + value,
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(() => rows.value.filter((item) => numericValue(item) > 0))
const tableRows = computed(() => chartRows.value) const tableRows = computed(() => chartRows.value)
const columnUnitLabel = computed(() => {
if (panelType.value !== "column") return ""
return sharedChartUnitLabel()
})
const peakUnitLabel = computed(() => {
if (panelType.value !== "peak") return ""
return sharedChartUnitLabel()
})
const structureRows = computed(() => rows.value.filter((item) => numericValue(item) > 0)) const structureRows = computed(() => rows.value.filter((item) => numericValue(item) > 0))
const heatRows = computed(() => chartRows.value.slice(0, props.panel?.limit || 8)) const heatRows = computed(() => chartRows.value.slice(0, props.panel?.limit || 8))
const stackSegments = computed(() => props.panel?.segments || []) const stackSegments = computed(() => props.panel?.segments || [])
@ -686,6 +696,38 @@ function displayValue(item) {
return formatNumber(item.value) return formatNumber(item.value)
} }
function displayUnit(item) {
const unit = item?.displayUnit || item?.unit || ""
if (!unit) return ""
const value = String(displayValue(item) ?? "")
return value && value.endsWith(String(unit)) ? "" : unit
}
function sharedChartUnitLabel() {
const units = [...new Set(chartRows.value.map((item) => item?.displayUnit || item?.unit).filter(Boolean))]
return units.length === 1 ? units[0] : ""
}
function columnItemUnit(item) {
const unit = displayUnit(item)
if (!unit) return ""
return columnUnitLabel.value === unit ? "" : unit
}
function peakItemUnit(item) {
const unit = displayUnit(item)
if (!unit) return ""
return peakUnitLabel.value === unit ? "" : unit
}
function formatPlaceLabel(value) {
const text = String(value || "").trim()
if (!text) return ""
const placeMatch = text.match(/^(.+)(镇|乡|村)$/)
if (placeMatch) return `${placeMatch[1]}\n${placeMatch[2]}`
return text
}
function normalizeSearch(value) { function normalizeSearch(value) {
return String(value || "").trim().toLowerCase() return String(value || "").trim().toLowerCase()
} }
@ -1517,7 +1559,7 @@ function columnGradient(index) {
} }
em { em {
font-size: 9px; font-size: 10px;
} }
} }
} }
@ -1943,7 +1985,7 @@ function columnGradient(index) {
margin-left: 2px; margin-left: 2px;
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
font-family: "PingFang SC", Arial, sans-serif; font-family: "PingFang SC", Arial, sans-serif;
font-size: 9px; font-size: 10px;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
} }
@ -2018,7 +2060,7 @@ function columnGradient(index) {
margin-left: 3px; margin-left: 3px;
color: rgba(255, 255, 255, 0.56); color: rgba(255, 255, 255, 0.56);
font-family: "PingFang SC", Arial, sans-serif; font-family: "PingFang SC", Arial, sans-serif;
font-size: 9px; font-size: 10px;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
} }
@ -2104,7 +2146,7 @@ function columnGradient(index) {
margin-left: 2px; margin-left: 2px;
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
font-family: "PingFang SC", Arial, sans-serif; font-family: "PingFang SC", Arial, sans-serif;
font-size: 9px; font-size: 10px;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
} }
@ -2208,7 +2250,7 @@ function columnGradient(index) {
.business-bar-row strong em, .business-bar-row strong em,
.business-bar-row strong small { .business-bar-row strong small {
font-size: 9px; font-size: 10px;
} }
} }
@ -2238,7 +2280,7 @@ function columnGradient(index) {
.business-bar-row strong em, .business-bar-row strong em,
.business-bar-row strong small { .business-bar-row strong small {
font-size: 9px; font-size: 10px;
} }
} }
@ -2502,6 +2544,7 @@ function columnGradient(index) {
max-width: 100%; max-width: 100%;
font-size: 10px; font-size: 10px;
line-height: 12px; line-height: 12px;
white-space: pre-line;
} }
} }
@ -2552,8 +2595,8 @@ function columnGradient(index) {
span { span {
max-width: 48px; max-width: 48px;
color: rgba(235, 252, 255, 0.78); color: rgba(235, 252, 255, 0.78);
font-size: 9px; font-size: 10px;
line-height: 10px; line-height: 11px;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
} }
} }
@ -2640,6 +2683,16 @@ function columnGradient(index) {
color: #d9fbff; color: #d9fbff;
font-family: D-DIN, Arial, sans-serif; font-family: D-DIN, Arial, sans-serif;
font-size: 13px; font-size: 13px;
white-space: nowrap;
em {
margin-left: 2px;
color: rgba(238, 253, 255, 0.72);
font-family: "PingFang SC", Arial, sans-serif;
font-size: 9px;
font-style: normal;
font-weight: 600;
}
} }
span { span {
@ -2651,8 +2704,9 @@ function columnGradient(index) {
text-align: center; text-align: center;
overflow: hidden; overflow: hidden;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 3; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
white-space: pre-line;
word-break: keep-all; word-break: keep-all;
} }
@ -2668,6 +2722,17 @@ function columnGradient(index) {
} }
} }
.business-column-unit {
position: absolute;
right: 12px;
top: 2px;
z-index: 2;
color: rgba(238, 253, 255, 0.72);
font-size: 10px;
line-height: 14px;
pointer-events: none;
}
.business-columns.is-variant-slim .business-column-track i, .business-columns.is-variant-slim .business-column-track i,
.business-columns.is-variant-slim .business-column-item.is-active .business-column-track i { .business-columns.is-variant-slim .business-column-item.is-active .business-column-track i {
transform: scaleY(1); transform: scaleY(1);
@ -3105,8 +3170,10 @@ function columnGradient(index) {
text-align: center; text-align: center;
overflow: hidden; overflow: hidden;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 3; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
white-space: pre-line;
word-break: keep-all;
} }
} }
@ -3130,6 +3197,7 @@ function columnGradient(index) {
} }
.business-peaks { .business-peaks {
position: relative;
display: grid; display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr)); grid-template-columns: repeat(5, minmax(0, 1fr));
align-items: stretch; align-items: stretch;
@ -3140,6 +3208,10 @@ function columnGradient(index) {
overflow: hidden; overflow: hidden;
} }
.business-peak-unit {
right: 15px;
}
.business-peaks.is-dense { .business-peaks.is-dense {
grid-template-columns: repeat(8, minmax(0, 1fr)); grid-template-columns: repeat(8, minmax(0, 1fr));
gap: 4px; gap: 4px;
@ -3159,6 +3231,15 @@ function columnGradient(index) {
line-height: 16px; line-height: 16px;
text-shadow: 0 0 12px rgba(48, 220, 255, 0.45); text-shadow: 0 0 12px rgba(48, 220, 255, 0.45);
white-space: nowrap; white-space: nowrap;
em {
margin-left: 2px;
color: rgba(238, 253, 255, 0.72);
font-family: "PingFang SC", Arial, sans-serif;
font-size: 9px;
font-style: normal;
font-weight: 600;
}
} }
span { span {
@ -3187,8 +3268,8 @@ function columnGradient(index) {
.business-peaks.is-dense .business-peak-item span { .business-peaks.is-dense .business-peak-item span {
line-height: 11px; line-height: 11px;
white-space: normal; white-space: pre-line;
writing-mode: vertical-rl; writing-mode: horizontal-tb;
max-height: 24px; max-height: 24px;
} }
@ -3366,7 +3447,7 @@ function columnGradient(index) {
margin-left: 3px; margin-left: 3px;
color: rgba(255, 255, 255, 0.62); color: rgba(255, 255, 255, 0.62);
font-family: "PingFang SC", Arial, sans-serif; font-family: "PingFang SC", Arial, sans-serif;
font-size: 9px; font-size: 10px;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
} }
@ -3381,13 +3462,13 @@ function columnGradient(index) {
span { span {
min-width: 0; min-width: 0;
color: rgba(255, 255, 255, 0.62); color: rgba(255, 255, 255, 0.62);
font-size: 9px; font-size: 10px;
line-height: 10px; line-height: 11px;
text-align: center; text-align: center;
overflow: hidden; overflow: hidden;
text-overflow: clip; text-overflow: clip;
white-space: normal; white-space: pre-line;
writing-mode: vertical-rl; writing-mode: horizontal-tb;
justify-self: center; justify-self: center;
max-height: 28px; max-height: 28px;
} }
@ -3459,7 +3540,7 @@ function columnGradient(index) {
margin-left: 2px; margin-left: 2px;
color: rgba(255, 255, 255, 0.54); color: rgba(255, 255, 255, 0.54);
font-family: "PingFang SC", Arial, sans-serif; font-family: "PingFang SC", Arial, sans-serif;
font-size: 8px; font-size: 10px;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
} }
@ -3724,6 +3805,7 @@ function columnGradient(index) {
.business-panel .business-bar-row strong, .business-panel .business-bar-row strong,
.business-panel .business-bar-row strong em, .business-panel .business-bar-row strong em,
.business-panel .business-bar-row strong small, .business-panel .business-bar-row strong small,
.business-panel .business-column-unit,
.business-panel .business-donut-row span, .business-panel .business-donut-row span,
.business-panel .business-donut-row strong, .business-panel .business-donut-row strong,
.business-panel .business-donut-row em, .business-panel .business-donut-row em,
@ -3762,4 +3844,8 @@ function columnGradient(index) {
color: #ffffff !important; color: #ffffff !important;
text-shadow: none !important; text-shadow: none !important;
} }
.business-panel .business-column-unit {
color: rgba(238, 253, 255, 0.72) !important;
}
</style> </style>

@ -94,7 +94,7 @@ function barWidth(item) {
} }
span { span {
font-size: 9px; font-size: 10px;
} }
} }
} }
@ -132,7 +132,7 @@ function barWidth(item) {
} }
em { em {
font-size: 9px; font-size: 10px;
} }
} }
} }

@ -3,7 +3,7 @@
<div class="layer-panel"> <div class="layer-panel">
<div class="layer-panel-list"> <div class="layer-panel-list">
<button <button
v-for="layer in layers" v-for="layer in panelLayers"
:key="layer.key" :key="layer.key"
class="layer-panel-item" class="layer-panel-item"
:class="{ :class="{
@ -17,7 +17,7 @@
<span>{{ layer.name }}</span> <span>{{ layer.name }}</span>
<i></i> <i></i>
</button> </button>
<div v-if="!layers.length" class="layer-panel-empty">暂无图层配置</div> <div v-if="!panelLayers.length" class="layer-panel-empty">暂无图层配置</div>
</div> </div>
<div class="layer-panel-legend"> <div class="layer-panel-legend">
@ -89,11 +89,15 @@ const props = defineProps({
type: Number, type: Number,
default: 232, default: 232,
}, },
layers: {
type: Array,
default: null,
},
}) })
const emit = defineEmits(["update:modelValue", "update:selectedLegendKey"]) const emit = defineEmits(["update:modelValue", "update:selectedLegendKey"])
const layers = computed(() => getLayersByTopic(props.topicKey)) const panelLayers = computed(() => Array.isArray(props.layers) ? props.layers : getLayersByTopic(props.topicKey))
const legendSelectable = computed(() => props.legends.filter(isSelectableLegend).length > 1) const legendSelectable = computed(() => props.legends.filter(isSelectableLegend).length > 1)
const legendSingle = computed(() => props.legends.length === 1) const legendSingle = computed(() => props.legends.length === 1)
const panelHeight = computed(() => { const panelHeight = computed(() => {
@ -103,7 +107,7 @@ const panelHeight = computed(() => {
function handleLayerClick(event, layerKey) { function handleLayerClick(event, layerKey) {
event.currentTarget?.blur?.() event.currentTarget?.blur?.()
emit("update:modelValue", layerKey) emit("update:modelValue", props.modelValue === layerKey ? "" : layerKey)
} }
function handleLegendClick(event, item) { function handleLegendClick(event, item) {

@ -10,12 +10,18 @@
:key="topic.key" :key="topic.key"
class="topic-switcher-item" class="topic-switcher-item"
:class="{ 'is-active': topic.key === modelValue }" :class="{ 'is-active': topic.key === modelValue }"
:style="{ '--topic-color': topic.color }" :style="{
'--topic-color': topic.color,
'--topic-item-width': topic.width ? `${topic.width}px` : undefined,
}"
type="button" type="button"
@pointerup="handleTopicClick($event, topic.key)" @pointerup="handleTopicClick($event, topic.key)"
@click="handleTopicClick($event, topic.key)" @click="handleTopicClick($event, topic.key)"
> >
<span class="topic-name">{{ topic.name }}</span> <span class="topic-name" :class="{ 'has-sub-name': topic.subName }">
<span class="topic-title">{{ topic.name }}</span>
<small v-if="topic.subName">{{ topic.subName }}</small>
</span>
</button> </button>
</div> </div>
<div class="topic-switcher-arrow is-reverse"> <div class="topic-switcher-arrow is-reverse">
@ -96,7 +102,7 @@ function handleTopicClick(event, topicKey) {
&-item { &-item {
position: relative; position: relative;
width: 112px; width: var(--topic-item-width, 112px);
height: 32px; height: 32px;
padding: 0; padding: 0;
border: 0; border: 0;
@ -118,10 +124,16 @@ function handleTopicClick(event, topicKey) {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-direction: column;
height: 100%; height: 100%;
padding: 0 5px;
text-align: center;
white-space: nowrap;
} }
.topic-name { .topic-name,
.topic-title,
.topic-name small {
padding-top: 0; padding-top: 0;
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
@ -130,5 +142,19 @@ function handleTopicClick(event, topicKey) {
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
} }
.topic-name.has-sub-name {
gap: 1px;
}
.topic-name small {
display: block;
max-width: 100%;
overflow: hidden;
font-size: 10px;
font-weight: 600;
line-height: 11px;
text-overflow: ellipsis;
}
} }
</style> </style>

@ -90,7 +90,7 @@
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue" import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
import gsap from "gsap" import gsap from "gsap"
import { Assets } from "./assets" import { Assets } from "./assets"
import { forestGrassWetTopics, getLayerByKey, getLayersByTopic, getTopicConfig } from "@/config/layers" import { forestGrassWetTopics, getLayerByKey, getTopicConfig } from "@/config/layers"
import { getLayerTownRanking, getTopicBusinessStats } from "@/services/openApi" import { getLayerTownRanking, getTopicBusinessStats } from "@/services/openApi"
import MapScene from "./map.vue" import MapScene from "./map.vue"
import TopicSwitcher from "./components/TopicSwitcher.vue" import TopicSwitcher from "./components/TopicSwitcher.vue"
@ -109,7 +109,7 @@ const props = defineProps({
const emit = defineEmits(["topic-stats"]) const emit = defineEmits(["topic-stats"])
const mapSceneRef = ref(null) const mapSceneRef = ref(null)
const activeTopicKey = ref(forestGrassWetTopics[0].key) const activeTopicKey = ref(forestGrassWetTopics[0].key)
const activeLayerKey = ref(forestGrassWetTopics[0].defaultLayerKey) const activeLayerKey = ref("")
const coreStats = ref([]) const coreStats = ref([])
const coreStatus = ref("idle") const coreStatus = ref("idle")
const coreLoading = ref(false) const coreLoading = ref(false)
@ -147,7 +147,7 @@ const mapRankingLayerKeys = new Set([
]) ])
const activeTopic = computed(() => getTopicConfig(activeTopicKey.value)) const activeTopic = computed(() => getTopicConfig(activeTopicKey.value))
const baseActiveLayer = computed(() => getLayerByKey(activeTopicKey.value, activeLayerKey.value)) const baseActiveLayer = computed(() => activeLayerKey.value ? getLayerByKey(activeTopicKey.value, activeLayerKey.value) : null)
const selectedLegend = computed(() => const selectedLegend = computed(() =>
(baseActiveLayer.value?.legends || []).find((item) => item.key === selectedLegendKey.value) || null (baseActiveLayer.value?.legends || []).find((item) => item.key === selectedLegendKey.value) || null
) )
@ -201,8 +201,8 @@ function toScreenLocalPoint(point) {
onMounted(() => { onMounted(() => {
document.addEventListener("pointerdown", handleOutsideFeaturePointerDown, true) document.addEventListener("pointerdown", handleOutsideFeaturePointerDown, true)
initAssets() initAssets()
loadBusinessStats(activeTopicKey.value, activeLayerKey.value) loadBusinessStats(activeTopicKey.value, "")
loadMapRanking(activeTopicKey.value, activeLayerKey.value) loadMapRanking(activeTopicKey.value, "")
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
@ -211,22 +211,19 @@ onBeforeUnmount(() => {
}) })
watch(activeTopicKey, (topicKey) => { watch(activeTopicKey, (topicKey) => {
const topic = getTopicConfig(topicKey)
const layers = getLayersByTopic(topicKey)
const nextLayerKey = topic.defaultLayerKey || layers[0]?.key || ""
selectedLegendKey.value = "" selectedLegendKey.value = ""
activeLayerKey.value = nextLayerKey activeLayerKey.value = ""
closeFeaturePanel() closeFeaturePanel()
loadBusinessStats(topicKey, nextLayerKey) loadBusinessStats(topicKey, "")
loadMapRanking(topicKey, nextLayerKey, null) loadMapRanking(topicKey, "", null)
}) })
watch(activeLayerKey, () => { watch(activeLayerKey, () => {
selectedLegendKey.value = "" selectedLegendKey.value = ""
closeFeaturePanel() closeFeaturePanel()
layerStatus.value = { layerStatus.value = {
type: "loading", type: activeLayerKey.value ? "loading" : "idle",
text: "正在加载 GeoServer 图层", text: activeLayerKey.value ? "正在加载 GeoServer 图层" : "等待 GeoServer 图层加载",
} }
loadBusinessStats(activeTopicKey.value, activeLayerKey.value) loadBusinessStats(activeTopicKey.value, activeLayerKey.value)
loadMapRanking(activeTopicKey.value, activeLayerKey.value, null) loadMapRanking(activeTopicKey.value, activeLayerKey.value, null)

File diff suppressed because it is too large Load Diff

@ -185,84 +185,105 @@ defineExpose({
height: 100%; height: 100%;
} }
.info-point { .info-point {
background: rgba(0, 0, 0, 0.5); color: #eaffff;
color: #a3dcde;
font-size: 13px; font-size: 13px;
width: 190px; width: 178px;
height: 138px; min-height: 82px;
padding: 15px 12px 0; margin-bottom: 24px;
margin-bottom: 30px;
will-change: transform; will-change: transform;
&-wrap { &-wrap {
&:after, position: relative;
&:before { width: 178px;
display: block; padding: 8px 10px 9px;
content: ""; border: 1px solid rgba(104, 236, 255, 0.38);
position: absolute; border-radius: 5px;
top: 0; background:
width: 15px; linear-gradient(120deg, rgba(6, 36, 48, 0.92), rgba(4, 18, 29, 0.78)),
height: 15px; rgba(2, 16, 25, 0.82);
border-top: 1px solid #4b87a6; box-shadow:
} 0 0 18px rgba(48, 220, 255, 0.2),
&:before { inset 0 0 18px rgba(39, 240, 192, 0.08);
left: 0; transform: translate(42%, -12%);
border-left: 1px solid #4b87a6;
}
&:after {
right: 0;
border-right: 1px solid #4b87a6;
}
&-inner { &-inner {
position: relative;
&:after, &:after,
&:before { &:before {
display: block;
content: "";
position: absolute; position: absolute;
bottom: 0; left: -18px;
width: 15px; content: "";
height: 15px;
border-bottom: 1px solid #4b87a6;
} }
&:before { &:before {
left: 0; top: 42px;
border-left: 1px solid #4b87a6; width: 16px;
height: 1px;
background: linear-gradient(90deg, rgba(104, 236, 255, 0), rgba(104, 236, 255, 0.88));
} }
&:after { &:after {
right: 0; top: 38px;
border-right: 1px solid #4b87a6; width: 7px;
height: 7px;
border-radius: 50%;
background: #fff06a;
box-shadow: 0 0 12px rgba(255, 240, 106, 0.9);
} }
} }
&.is-prime {
border-color: rgba(255, 224, 107, 0.52);
background:
linear-gradient(120deg, rgba(67, 48, 13, 0.72), rgba(4, 24, 34, 0.82)),
rgba(2, 16, 25, 0.86);
}
} }
&-line { &-head {
position: absolute;
top: 7px;
right: 12px;
display: flex; display: flex;
.line { align-items: center;
width: 5px; gap: 7px;
height: 2px; min-width: 0;
margin-right: 5px; .rank {
background: #17e5c3; flex: 0 0 auto;
color: #ffe06b;
font-size: 12px;
line-height: 14px;
font-weight: 900;
}
strong {
overflow: hidden;
color: #ffffff;
font-size: 15px;
line-height: 18px;
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
} }
} }
&-content { &-metric {
.content-item { margin-top: 4px;
display: flex; overflow: hidden;
height: 25px; color: rgba(166, 244, 255, 0.78);
line-height: 25px; font-size: 11px;
background: rgba(35, 47, 58, 0.6); line-height: 13px;
margin-bottom: 4px; font-weight: 700;
.label { text-overflow: ellipsis;
width: 48px; white-space: nowrap;
padding-left: 10px; }
} &-value {
.value { display: flex;
color: #ffffff; align-items: baseline;
flex: 1; gap: 4px;
overflow: hidden; margin-top: 2px;
text-overflow: ellipsis; span {
white-space: nowrap; color: #ffffff;
} font-size: 25px;
line-height: 28px;
font-weight: 900;
text-shadow: 0 0 12px rgba(126, 251, 246, 0.6);
}
em {
color: rgba(235, 252, 255, 0.78);
font-size: 11px;
line-height: 13px;
font-style: normal;
font-weight: 800;
} }
} }
} }
@ -409,7 +430,7 @@ defineExpose({
border: 1px solid rgba(39, 240, 192, 0.28); border: 1px solid rgba(39, 240, 192, 0.28);
border-radius: 2px; border-radius: 2px;
color: #79f8e5; color: #79f8e5;
font-size: 9px; font-size: 10px;
line-height: 11px; line-height: 11px;
background: rgba(39, 240, 192, 0.08); background: rgba(39, 240, 192, 0.08);
} }
@ -451,7 +472,7 @@ defineExpose({
gap: 4px; gap: 4px;
margin-top: 3px; margin-top: 3px;
color: rgba(226, 250, 255, 0.62); color: rgba(226, 250, 255, 0.62);
font-size: 9px; font-size: 10px;
line-height: 11px; line-height: 11px;
white-space: nowrap; white-space: nowrap;
} }
@ -634,7 +655,7 @@ defineExpose({
span { span {
flex: 0 0 auto; flex: 0 0 auto;
color: #ffe076; color: #ffe076;
font-size: 9px; font-size: 10px;
line-height: 12px; line-height: 12px;
font-weight: 900; font-weight: 900;
} }
@ -697,9 +718,10 @@ defineExpose({
align-items: stretch; align-items: stretch;
gap: 4px; gap: 4px;
min-width: 74px; min-width: 74px;
padding: 6px 9px 5px; padding: 5px 8px 4px;
border-color: rgba(126, 251, 246, 0.44); border-color: rgba(126, 251, 246, 0.3);
background: linear-gradient(180deg, rgba(4, 33, 46, 0.86), rgba(3, 20, 32, 0.72)); 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);
} }
.town-name-row { .town-name-row {
display: flex; display: flex;
@ -711,7 +733,7 @@ defineExpose({
} }
.town-name { .town-name {
color: #ffffff; color: #ffffff;
font-size: 14px; font-size: 13px;
line-height: 18px; line-height: 18px;
font-weight: 800; font-weight: 800;
} }
@ -761,7 +783,7 @@ defineExpose({
font-weight: 600; font-weight: 600;
} }
.town-stat-label.has-stat { .town-stat-label.has-stat {
min-width: 116px; min-width: 108px;
.town-rank, .town-rank,
.town-stat-row { .town-stat-row {

@ -1,52 +1,3 @@
export default [ import { hongyuanTownshipLabelPoints } from "@/config/townshipBoundaries"
{
name: "邛溪镇", export default hongyuanTownshipLabelPoints.map((item) => ({ ...item }))
center: [102.60242339873115, 32.83022561372293],
centroid: [102.60242339873115, 32.83022561372293],
},
{
name: "刷经寺镇",
center: [102.55651980382184, 32.204097908180216],
centroid: [102.55651980382184, 32.204097908180216],
},
{
name: "安曲镇",
center: [102.38447320923945, 32.696113484169786],
centroid: [102.38447320923945, 32.696113484169786],
},
{
name: "龙日镇",
center: [102.48704663740175, 32.471364980560764],
centroid: [102.48704663740175, 32.471364980560764],
},
{
name: "江茸乡",
center: [102.29046083534963, 32.355689518554044],
centroid: [102.29046083534963, 32.355689518554044],
},
{
name: "查尔玛乡",
center: [102.03268479676977, 32.440590072394265],
centroid: [102.03268479676977, 32.440590072394265],
},
{
name: "瓦切镇",
center: [102.71535664604494, 33.12869670968013],
centroid: [102.71535664604494, 33.12869670968013],
},
{
name: "阿木乡",
center: [102.77925597565002, 32.73632379185147],
centroid: [102.77925597565002, 32.73632379185147],
},
{
name: "麦洼乡",
center: [102.95718106156616, 33.15445682575394],
centroid: [102.95718106156616, 33.15445682575394],
},
{
name: "色地镇",
center: [103.12540730227074, 33.010462261362875],
centroid: [103.12540730227074, 33.010462261362875],
},
]

@ -1,72 +1,3 @@
export default [ import { hongyuanTownshipRankingPoints } from "@/config/townshipBoundaries"
{
name: "色地镇", export default hongyuanTownshipRankingPoints.map((item) => ({ ...item }))
enName: "sedi",
center: [103.12540730227074, 33.010462261362875],
centroid: [103.12540730227074, 33.010462261362875],
value: 1199,
},
{
name: "瓦切镇",
enName: "waqie",
center: [102.71535664604494, 33.12869670968013],
centroid: [102.71535664604494, 33.12869670968013],
value: 991,
},
{
name: "刷经寺镇",
enName: "shuajingsi",
center: [102.55651980382184, 32.204097908180216],
centroid: [102.55651980382184, 32.204097908180216],
value: 986,
},
{
name: "邛溪镇",
enName: "qiongxi",
center: [102.60242339873115, 32.83022561372293],
centroid: [102.60242339873115, 32.83022561372293],
value: 896,
},
{
name: "阿木乡",
enName: "amu",
center: [102.77925597565002, 32.73632379185147],
centroid: [102.77925597565002, 32.73632379185147],
value: 892,
},
{
name: "安曲镇",
enName: "anqu",
center: [102.38447320923945, 32.696113484169786],
centroid: [102.38447320923945, 32.696113484169786],
value: 885,
},
{
name: "查尔玛乡",
enName: "chaerma",
center: [102.03268479676977, 32.440590072394265],
centroid: [102.03268479676977, 32.440590072394265],
value: 729,
},
{
name: "龙日镇",
enName: "longri",
center: [102.48704663740175, 32.471364980560764],
centroid: [102.48704663740175, 32.471364980560764],
value: 645,
},
{
name: "麦洼乡",
enName: "maiwa",
center: [102.95718106156616, 33.15445682575394],
centroid: [102.95718106156616, 33.15445682575394],
value: 559,
},
{
name: "江茸乡",
enName: "jiangrong",
center: [102.29046083534963, 32.355689518554044],
centroid: [102.29046083534963, 32.355689518554044],
value: 514,
},
];

@ -49,7 +49,7 @@ export class Assets {
{ {
type: "File", type: "File",
name: "mapJson", name: "mapJson",
path: base_url + "assets/json/红原县.json", path: base_url + "datas/geojsons/hongyuan-townships.geojson",
}, },
{ {
type: "File", type: "File",

@ -1,52 +1,3 @@
export default [ import { hongyuanTownshipLabelPoints } from "@/config/townshipBoundaries"
{
name: "邛溪镇", export default hongyuanTownshipLabelPoints.map((item) => ({ ...item }))
center: [102.60242339873115, 32.83022561372293],
centroid: [102.60242339873115, 32.83022561372293],
},
{
name: "刷经寺镇",
center: [102.55651980382184, 32.204097908180216],
centroid: [102.55651980382184, 32.204097908180216],
},
{
name: "安曲镇",
center: [102.38447320923945, 32.696113484169786],
centroid: [102.38447320923945, 32.696113484169786],
},
{
name: "龙日镇",
center: [102.48704663740175, 32.471364980560764],
centroid: [102.48704663740175, 32.471364980560764],
},
{
name: "江茸乡",
center: [102.29046083534963, 32.355689518554044],
centroid: [102.29046083534963, 32.355689518554044],
},
{
name: "查尔玛乡",
center: [102.03268479676977, 32.440590072394265],
centroid: [102.03268479676977, 32.440590072394265],
},
{
name: "瓦切镇",
center: [102.71535664604494, 33.12869670968013],
centroid: [102.71535664604494, 33.12869670968013],
},
{
name: "阿木乡",
center: [102.77925597565002, 32.73632379185147],
centroid: [102.77925597565002, 32.73632379185147],
},
{
name: "麦洼乡",
center: [102.95718106156616, 33.15445682575394],
centroid: [102.95718106156616, 33.15445682575394],
},
{
name: "色地镇",
center: [103.12540730227074, 33.010462261362875],
centroid: [103.12540730227074, 33.010462261362875],
},
]

@ -1,72 +1,3 @@
export default [ import { hongyuanTownshipRankingPoints } from "@/config/townshipBoundaries"
{
name: "色地镇", export default hongyuanTownshipRankingPoints.map((item) => ({ ...item }))
enName: "sedi",
center: [103.12540730227074, 33.010462261362875],
centroid: [103.12540730227074, 33.010462261362875],
value: 1199,
},
{
name: "瓦切镇",
enName: "waqie",
center: [102.71535664604494, 33.12869670968013],
centroid: [102.71535664604494, 33.12869670968013],
value: 991,
},
{
name: "刷经寺镇",
enName: "shuajingsi",
center: [102.55651980382184, 32.204097908180216],
centroid: [102.55651980382184, 32.204097908180216],
value: 986,
},
{
name: "邛溪镇",
enName: "qiongxi",
center: [102.60242339873115, 32.83022561372293],
centroid: [102.60242339873115, 32.83022561372293],
value: 896,
},
{
name: "阿木乡",
enName: "amu",
center: [102.77925597565002, 32.73632379185147],
centroid: [102.77925597565002, 32.73632379185147],
value: 892,
},
{
name: "安曲镇",
enName: "anqu",
center: [102.38447320923945, 32.696113484169786],
centroid: [102.38447320923945, 32.696113484169786],
value: 885,
},
{
name: "查尔玛乡",
enName: "chaerma",
center: [102.03268479676977, 32.440590072394265],
centroid: [102.03268479676977, 32.440590072394265],
value: 729,
},
{
name: "龙日镇",
enName: "longri",
center: [102.48704663740175, 32.471364980560764],
centroid: [102.48704663740175, 32.471364980560764],
value: 645,
},
{
name: "麦洼乡",
enName: "maiwa",
center: [102.95718106156616, 33.15445682575394],
centroid: [102.95718106156616, 33.15445682575394],
value: 559,
},
{
name: "江茸乡",
enName: "jiangrong",
center: [102.29046083534963, 32.355689518554044],
centroid: [102.29046083534963, 32.355689518554044],
value: 514,
},
];

@ -61,6 +61,23 @@ export const fieldMap = {
pasturesName: ["pasturesName", "pastures_name", "pastureName", "muhuName", "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",
]
export const townCodeByName = { export const townCodeByName = {
邛溪镇: "513233100", 邛溪镇: "513233100",
刷经寺镇: "513233101", 刷经寺镇: "513233101",
@ -143,7 +160,7 @@ function calculateBearingCapacity(areaRange) {
return Math.round((Number(areaRange) || 0) * BALANCE_CALCULATION_PARAMS.grasslandCapacityPerMu) return Math.round((Number(areaRange) || 0) * BALANCE_CALCULATION_PARAMS.grasslandCapacityPerMu)
} }
function buildBalanceRow({ export function buildBalanceRow({
key, key,
type, type,
name, name,
@ -153,6 +170,10 @@ function buildBalanceRow({
yakNumber, yakNumber,
location, location,
township, township,
hasBoundary = false,
boundaryCount = 0,
grasslands = [],
source,
pressureFactor = 1, pressureFactor = 1,
}) { }) {
const bearingCapacity = calculateBearingCapacity(areaRange) const bearingCapacity = calculateBearingCapacity(areaRange)
@ -175,6 +196,10 @@ function buildBalanceRow({
year: "2026", year: "2026",
location, location,
township, township,
hasBoundary: Boolean(hasBoundary),
boundaryCount: Number(boundaryCount) || 0,
grasslands,
source,
} }
} }
@ -211,7 +236,7 @@ export const countyBalanceRow = {
year: "2026", year: "2026",
} }
export const balancePastureRows = pastureListRows.map((item) => buildBalanceRow({ export const balancePastureRows = sortBalancePastureRows(pastureListRows.map((item) => buildBalanceRow({
key: item.id || item.key || item.name, key: item.id || item.key || item.name,
type: "pasture", type: "pasture",
name: item.name, name: item.name,
@ -221,7 +246,9 @@ export const balancePastureRows = pastureListRows.map((item) => buildBalanceRow(
yakNumber: item.yakCount, yakNumber: item.yakCount,
location: item.location, location: item.location,
township: item.township, township: item.township,
})) hasBoundary: item.hasBoundary || item.canLocate || hasGrasslandGeometry(item),
source: item,
})))
export const balanceFallbackRows = [ export const balanceFallbackRows = [
countyBalanceRow, countyBalanceRow,
@ -230,11 +257,29 @@ export const balanceFallbackRows = [
] ]
export function normalizeBalanceResult(source = {}, fallback = {}) { export function normalizeBalanceResult(source = {}, fallback = {}) {
const areaRange = toNumber(pickValue(source, fieldMap.areaRange), fallback.areaRange ?? 0) const rawAreaRange = pickValue(source, fieldMap.areaRange)
const yakNumber = toNumber(pickValue(source, fieldMap.yakNumber), fallback.yakNumber ?? 0) const rawYakNumber = pickValue(source, fieldMap.yakNumber)
const bearingCapacity = toNumber(pickValue(source, fieldMap.bearingCapacity), fallback.bearingCapacity ?? calculateBearingCapacity(areaRange)) const rawBearingCapacity = pickValue(source, fieldMap.bearingCapacity)
const livestockCapacity = toNumber(pickValue(source, fieldMap.livestockCapacity), fallback.livestockCapacity ?? calculateLivestockCapacity(yakNumber)) const rawLivestockCapacity = pickValue(source, fieldMap.livestockCapacity)
const directRate = toNumber(pickValue(source, fieldMap.overloadRate)) const directRate = toNumber(pickValue(source, fieldMap.overloadRate))
const fallbackYakNumber = toNumber(fallback.yakNumber)
const legacyLivestockCapacity = getLegacyLivestockCapacity(source, rawLivestockCapacity, fallbackYakNumber)
const areaRange = toNumber(rawAreaRange, fallback.areaRange ?? 0)
const yakNumber = legacyLivestockCapacity !== null && rawYakNumber === source?.yak_number
? toNumber(pickValue(source, ["number", "yakCount", "yak_count", "livestockCount", "livestock_count"]), fallbackYakNumber ?? Math.round(legacyLivestockCapacity / BALANCE_CALCULATION_PARAMS.yakToSheepUnit))
: toNumber(rawYakNumber, fallback.yakNumber ?? 0)
const fallbackBearing = toNumber(fallback.bearingCapacity)
const fallbackLivestock = toNumber(fallback.livestockCapacity)
const sourceHasArea = rawAreaRange !== undefined && rawAreaRange !== null && rawAreaRange !== ""
const sourceHasYakNumber = rawYakNumber !== undefined && rawYakNumber !== null && rawYakNumber !== ""
const livestockCapacity = toNumber(rawLivestockCapacity)
?? legacyLivestockCapacity
?? (sourceHasYakNumber ? calculateLivestockCapacity(yakNumber) : fallbackLivestock)
?? calculateLivestockCapacity(yakNumber)
const bearingCapacity = toNumber(rawBearingCapacity)
?? inferBearingCapacity(livestockCapacity, directRate)
?? (sourceHasArea ? calculateBearingCapacity(areaRange) : fallbackBearing)
?? calculateBearingCapacity(areaRange)
const overloadRate = directRate !== null const overloadRate = directRate !== null
? round(directRate, 1) ? round(directRate, 1)
: bearingCapacity : bearingCapacity
@ -257,7 +302,155 @@ 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,
}
}
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 || townCodeByName[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) {
if (!source || typeof source !== "object") return null
if (rawLivestockCapacity !== undefined && rawLivestockCapacity !== null && rawLivestockCapacity !== "") return null
const hasLegacyAlias = Object.prototype.hasOwnProperty.call(source, "yak_number")
const hasBalanceMarker = ["overload_rate", "overloadRate", "advice", "balanceAdvice", "level", "conclusion"]
.some((key) => source[key] !== undefined && source[key] !== null && source[key] !== "")
const aliasValue = toNumber(source.yak_number)
if (!hasLegacyAlias || !hasBalanceMarker || aliasValue === null) return null
const fallbackYak = toNumber(fallbackYakNumber)
if (fallbackYak !== null && fallbackYak > 0) {
return aliasValue > fallbackYak * 1.5 ? aliasValue : null
}
return aliasValue
}
function inferBearingCapacity(livestockCapacity, overloadRate) {
const livestock = toNumber(livestockCapacity)
const rate = toNumber(overloadRate)
if (livestock === null || rate === null || rate <= -100) return null
return Math.round(livestock / (1 + rate / 100))
}
function sumPastureGrasslandArea(rows = []) {
return (Array.isArray(rows) ? rows : []).reduce((sum, row) => {
const area = pickPastureArea(row)
return sum + (area > 0 ? area : 0)
}, 0)
}
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 = {}) {

@ -31,7 +31,6 @@
@click="selectBalance(item)" @click="selectBalance(item)"
> >
<span>{{ item.name }}</span> <span>{{ item.name }}</span>
<small>{{ getTargetSubText(item) }}</small>
</button> </button>
</div> </div>
@ -147,12 +146,12 @@
<div class="balance-bar-row"> <div class="balance-bar-row">
<span>理论承载力</span> <span>理论承载力</span>
<div><i :style="{ width: `${activeDerived.bearingPercent}%` }"></i></div> <div><i :style="{ width: `${activeDerived.bearingPercent}%` }"></i></div>
<strong>{{ formatNumber(activeDerived.bearing) }}<em>羊单位</em></strong> <strong>{{ formatNumber(activeDerived.bearing) }}</strong>
</div> </div>
<div class="balance-bar-row is-livestock"> <div class="balance-bar-row is-livestock">
<span>实际载畜量</span> <span>实际载畜量</span>
<div><i :style="{ width: `${activeDerived.livestockPercent}%` }"></i></div> <div><i :style="{ width: `${activeDerived.livestockPercent}%` }"></i></div>
<strong>{{ formatNumber(activeDerived.livestock) }}<em>羊单位</em></strong> <strong>{{ formatNumber(activeDerived.livestock) }}</strong>
</div> </div>
</div> </div>
@ -181,10 +180,10 @@
<span>{{ step.index }}</span> <span>{{ step.index }}</span>
<div> <div>
<strong>{{ step.name }}</strong> <strong>{{ step.name }}</strong>
<p>{{ step.formula }}</p> <p>{{ step.plan }}</p>
<small>{{ step.expression }}</small> <small>{{ step.desc }}</small>
</div> </div>
<em>{{ step.result }}{{ step.unit ? ` ${step.unit}` : "" }}</em> <em>{{ step.status }}</em>
</div> </div>
</div> </div>
@ -219,15 +218,22 @@
<script setup> <script setup>
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 { 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,
buildBalancePastureRows,
deriveBalance, deriveBalance,
formatNumber, formatNumber,
formatPercent, formatPercent,
hasPastureBoundary,
normalizeBalanceResult, normalizeBalanceResult,
pickValue,
sortBalancePastureRows,
splitAdvice, splitAdvice,
toNumber,
} from "./data" } from "./data"
const emit = defineEmits(["loading-progress", "map-ready"]) const emit = defineEmits(["loading-progress", "map-ready"])
@ -236,6 +242,7 @@ 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 activeScope = ref("county") const activeScope = ref("county")
const activeKey = ref("") const activeKey = ref("")
@ -243,18 +250,30 @@ const analysisState = ref("idle")
const analysisProgress = ref(0) const analysisProgress = ref(0)
const processStageIndex = ref(0) const processStageIndex = ref(0)
const apiResults = ref({}) const apiResults = ref({})
const fallbackPastureRows = sortBalancePastureRows(balanceFallbackRows.filter((item) => item.type === "pasture"))
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 pastureLoadSerial = 0
let activeAnalysisRequest = null
const balanceRows = computed(() => const balanceRows = computed(() =>
balanceFallbackRows.map((row) => normalizeBalanceResult(apiResults.value[row.key], row)) [
...balanceFallbackRows.filter((row) => row.type !== "pasture"),
...villageTargetRows.value,
...pastureTargetRows.value,
].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 pastureRows = computed(() => balanceRows.value.filter((item) => item.type === "pasture")) 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 === "pasture") return pastureRows.value if (activeScope.value === "pasture") return pastureRows.value
return countyRows.value return countyRows.value
}) })
@ -264,11 +283,12 @@ const analysisRunning = computed(() => analysisState.value === "running")
const analysisDone = computed(() => hasSelection.value && analysisState.value === "done") const analysisDone = computed(() => hasSelection.value && analysisState.value === "done")
const activeDerived = computed(() => deriveBalance(activeBalance.value || {})) const activeDerived = computed(() => deriveBalance(activeBalance.value || {}))
const scopeOptions = computed(() => [ const scopeOptions = computed(() => [
{ key: "county", name: "县域" }, { key: "county", name: "县" },
{ key: "town", name: "乡镇" }, { key: "town", name: "镇" },
{ key: "village", name: "村" },
{ key: "pasture", 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.pasturesId) return `牧户编号 ${activeBalance.value.pasturesId}`
@ -293,14 +313,14 @@ const waitTitle = computed(() => {
return `${activeBalance.value.name} 待开始分析` return `${activeBalance.value.name} 待开始分析`
}) })
const waitDescription = computed(() => { const waitDescription = computed(() => {
if (!hasSelection.value) return "选择县镇或牧户后,系统才会进入草地承载力核算。" if (!hasSelection.value) return "选择县、镇、村或牧户后,系统才会进入草地承载力核算。"
if (analysisRunning.value) return "正在读取核算参数、执行公式计算,并生成结论与调控建议。" if (analysisRunning.value) return "正在读取核算参数、执行公式计算,并生成结论与调控建议。"
return "点击左侧开始分析后,再展示结果、分析过程、结论和建议。" return "点击左侧开始分析后,再展示结果、分析过程、结论和建议。"
}) })
const conclusionSummary = computed(() => { const conclusionSummary = computed(() => {
const derived = activeDerived.value const derived = activeDerived.value
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 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))
@ -308,99 +328,80 @@ const usageAngle = computed(() => {
}) })
const calculationSteps = computed(() => { const calculationSteps = computed(() => {
const row = activeBalance.value || {}
const derived = activeDerived.value const derived = activeDerived.value
const areaWanMu = (derived.areaRange || 0) / 10000
const deltaText = formatSignedNumber(derived.delta)
return [ return [
{ {
key: "bearing", key: "productivity",
index: "01", index: "01",
name: "理论承载力", name: "草地生产力核算",
formula: "核算草地面积 × 草地承载系数", plan: "核算草地面积与单位面积产草能力",
expression: `${formatNumber(areaWanMu, { maximumFractionDigits: 2 })}万亩 × 10000 × ${formatFixed(derived.bearingCoefficient, 4)}`, desc: "结合核算对象草地范围,确定可参与草畜平衡计算的草地生产基础。",
result: formatNumber(derived.bearing), status: "已完成",
unit: "羊单位",
}, },
{ {
key: "livestock", key: "forage",
index: "02", index: "02",
name: "实际载畜量", name: "可利用草量折算",
formula: "牦牛数量 × 折羊单位系数 × 压力修正系数", plan: "扣除不可食与生态保留部分",
expression: `${formatNumber(row.yakNumber)}头 × ${formatFixed(BALANCE_CALCULATION_PARAMS.yakToSheepUnit, 1)} × ${formatFixed(derived.pressureFactor, 2)}`, desc: "按草地利用系数、放牧季节和保护要求,折算为可供放牧利用的草量。",
result: formatNumber(derived.livestock), status: "已完成",
unit: "羊单位",
}, },
{ {
key: "delta", key: "livestock",
index: "03", index: "03",
name: "承载差额", name: "载畜量折算",
formula: "实际载畜量 - 理论承载力", plan: "将牦牛存栏折算为标准载畜压力",
expression: `${formatNumber(derived.livestock)} - ${formatNumber(derived.bearing)}`, desc: "依据牦牛数量、折算系数和压力修正参数,形成实际载畜量口径。",
result: deltaText, status: "已完成",
unit: "羊单位",
}, },
{ {
key: "rate", key: "balance",
index: "04", index: "04",
name: "超载率", name: "平衡判定",
formula: "承载差额 ÷ 理论承载力 × 100%", plan: derived.delta > 0 ? "比对承载力并形成超载调控方案" : "比对承载力并形成稳态保持方案",
expression: `${deltaText} ÷ ${formatNumber(derived.bearing)} × 100%`, desc: "根据承载利用率和超载率判定平衡状态,输出治理建议。",
result: formatPercent(derived.overloadRate), status: derived.risk.shortLabel || "完成",
unit: "",
}, },
] ]
}) })
const analysisProcessRows = computed(() => { const analysisProcessRows = computed(() => {
const row = activeBalance.value || {}
const derived = activeDerived.value const derived = activeDerived.value
const areaWanMu = (derived.areaRange || 0) / 10000
const deltaText = formatSignedNumber(derived.delta)
return [ return [
{ {
key: "target", key: "productivity",
index: "01", index: "01",
name: "锁定核算对象", name: "草地生产力核算",
desc: "确认行政区划与对象口径", desc: "核算草地面积与产草能力",
pending: "等待对象确认", pending: "等待草地参数",
expression: `${activeBalance.value?.name || "--"} / ${subjectMeta.value}`, expression: "正在汇总草地范围、面积和生产力参数",
result: "已锁定", result: "已核算",
}, },
{ {
key: "params", key: "forage",
index: "02", index: "02",
name: "读取基础参数", name: "可利用草量折算",
desc: "读取草地面积、牦牛数量与折算系数", desc: "折算可供放牧利用草量",
pending: "等待参数读取", pending: "等待草量折算",
expression: `草地 ${formatNumber(areaWanMu, { maximumFractionDigits: 2 })} 万亩,牦牛 ${formatNumber(row.yakNumber)} 头,折羊系数 ${formatFixed(BALANCE_CALCULATION_PARAMS.yakToSheepUnit, 1)}`, expression: "正在扣除不可食与生态保留部分",
result: "已读取", result: "已折算",
}, },
{ {
key: "bearing", key: "livestock",
index: "03", index: "03",
name: "计算理论承载力", name: "载畜量折算",
desc: "代入草地承载系数", desc: "形成实际载畜压力口径",
pending: "等待承载力计算", pending: "等待载畜折算",
expression: `${formatNumber(areaWanMu, { maximumFractionDigits: 2 })}万亩 × 10000 × ${formatFixed(derived.bearingCoefficient, 4)} = ${formatNumber(derived.bearing)} 羊单位`, expression: "正在按牦牛存栏和折算系数换算载畜压力",
result: formatNumber(derived.bearing), result: "已折算",
}, },
{ {
key: "livestock", key: "balance",
index: "04", index: "04",
name: "折算实际载畜量", name: "平衡判定",
desc: "按牦牛数量与压力系数折算", desc: "生成结论与调控方向",
pending: "等待载畜量折算", pending: "等待平衡判定",
expression: `${formatNumber(row.yakNumber)}头 × ${formatFixed(BALANCE_CALCULATION_PARAMS.yakToSheepUnit, 1)} × ${formatFixed(derived.pressureFactor, 2)} = ${formatNumber(derived.livestock)} 羊单位`, expression: derived.delta > 0 ? "正在判定超载风险并生成调控方向" : "正在判定平衡状态并生成监测方向",
result: formatNumber(derived.livestock),
},
{
key: "conclusion",
index: "05",
name: "生成平衡结论",
desc: "比较差额并输出调控建议",
pending: "等待结论生成",
expression: `${deltaText} ÷ ${formatNumber(derived.bearing)} × 100% = ${formatPercent(derived.overloadRate)}`,
result: derived.risk.label, result: derived.risk.label,
}, },
] ]
@ -409,31 +410,31 @@ const currentProcessStep = computed(() => analysisProcessRows.value[Math.min(pro
const workflowRows = computed(() => [ const workflowRows = computed(() => [
{ {
key: "select", key: "productivity",
index: "01", index: "01",
name: "对象锁定", name: "生产力核算",
desc: hasSelection.value ? activeBalance.value.name : "等待选择", desc: analysisDone.value ? "草地生产力已核算" : hasSelection.value ? "等待核算" : "等待选择",
state: hasSelection.value ? "done" : "active", state: hasSelection.value ? "done" : "active",
}, },
{ {
key: "params", 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: "formula", 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",
}, },
{ {
key: "result", key: "balance",
index: "04", index: "04",
name: "结论生成", name: "平衡判定",
desc: analysisDone.value ? activeDerived.value.risk.label : "等待建议生成", desc: analysisDone.value ? activeDerived.value.risk.label : "等待判定",
state: analysisDone.value ? "done" : "disabled", state: analysisDone.value ? "done" : "disabled",
}, },
]) ])
@ -443,11 +444,15 @@ const activeAdviceRows = computed(() => analysisDone.value ? splitAdvice(activeB
onMounted(() => { onMounted(() => {
emit("loading-progress", { progress: 100 }) emit("loading-progress", { progress: 100 })
emit("map-ready") emit("map-ready")
loadVillageTargets()
loadPastureTargets()
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
destroyed = true destroyed = true
requestSerial += 1 requestSerial += 1
villageLoadSerial += 1
pastureLoadSerial += 1
clearAnalysisTimer() clearAnalysisTimer()
}) })
@ -457,6 +462,7 @@ watch(
analysisState.value = "idle" analysisState.value = "idle"
analysisProgress.value = 0 analysisProgress.value = 0
processStageIndex.value = 0 processStageIndex.value = 0
activeAnalysisRequest = null
clearAnalysisTimer() clearAnalysisTimer()
} }
) )
@ -469,7 +475,7 @@ function handleScopeChange(scopeKey) {
function selectBalance(row) { function selectBalance(row) {
if (!row) return if (!row) return
activeScope.value = row.type === "pasture" ? "pasture" : row.type === "town" ? "town" : "county" activeScope.value = ["county", "town", "village", "pasture"].includes(row.type) ? row.type : "county"
activeKey.value = row.key activeKey.value = row.key
analysisState.value = "idle" analysisState.value = "idle"
} }
@ -480,19 +486,30 @@ function startAnalysis() {
analysisProgress.value = 0 analysisProgress.value = 0
processStageIndex.value = 0 processStageIndex.value = 0
clearAnalysisTimer() clearAnalysisTimer()
refreshActiveBalance() const startedAt = Date.now()
const analysisRequest = refreshActiveBalance()
activeAnalysisRequest = analysisRequest
analysisTimer = window.setInterval(() => { analysisTimer = window.setInterval(() => {
if (destroyed) return if (destroyed) return
analysisProgress.value = Math.min(100, analysisProgress.value + 4) const maxProgress = activeAnalysisRequest === analysisRequest ? 92 : 100
analysisProgress.value = Math.min(maxProgress, analysisProgress.value + 2)
processStageIndex.value = Math.min( processStageIndex.value = Math.min(
analysisProcessRows.value.length - 1, analysisProcessRows.value.length - 1,
Math.floor(analysisProgress.value / 22) Math.floor(analysisProgress.value / 25)
) )
if (analysisProgress.value >= 100) { }, 140)
clearAnalysisTimer() analysisRequest.finally(async () => {
analysisState.value = "done" const remaining = MIN_ANALYSIS_DURATION - (Date.now() - startedAt)
if (remaining > 0) {
await new Promise((resolve) => window.setTimeout(resolve, remaining))
} }
}, 90) if (destroyed || analysisState.value !== "running" || activeAnalysisRequest !== analysisRequest) return
activeAnalysisRequest = null
analysisProgress.value = 100
processStageIndex.value = analysisProcessRows.value.length - 1
clearAnalysisTimer()
analysisState.value = "done"
})
} }
async function refreshActiveBalance() { async function refreshActiveBalance() {
@ -515,10 +532,232 @@ async function refreshActiveBalance() {
} }
} }
function getTargetSubText(row) { async function loadVillageTargets() {
if (row.type === "pasture") return row.township || "牧户" const loadId = ++villageLoadSerial
if (row.type === "town") return "乡镇对象" const towns = townRows.value
return "县域对象" .filter((row) => row?.areaCode)
.map((row) => ({ ...row }))
if (!towns.length) return
try {
const townVillageRows = await Promise.all(towns.map(async (town) => {
try {
const rows = await getAreaChildren(town.areaCode)
if (destroyed || loadId !== villageLoadSerial) return []
return buildVillageRows(rows, town)
} catch (error) {
return []
}
}))
if (destroyed || loadId !== villageLoadSerial) return
villageTargetRows.value = townVillageRows.flat()
} catch (error) {
if (destroyed || loadId !== villageLoadSerial) return
villageTargetRows.value = []
}
}
function buildVillageRows(rows = [], town = {}) {
const validRows = (Array.isArray(rows) ? rows : []).filter((row) => {
const areaCode = pickAreaCode(row)
const name = pickAreaName(row)
return Boolean(areaCode || name)
})
const count = validRows.length || 1
const fallbackAreaRange = (toNumber(town.areaRange, 0) || 0) / count
const fallbackYakNumber = (toNumber(town.yakNumber, 0) || 0) / count
const pressureFactor = getTownPressureFactor(town)
return validRows
.map((row, index) => buildVillageRow(row, index, town, {
areaRange: fallbackAreaRange,
yakNumber: fallbackYakNumber,
pressureFactor,
}))
.filter(Boolean)
}
function buildVillageRow(row = {}, index = 0, town = {}, fallback = {}) {
const areaCode = pickAreaCode(row)
const name = pickAreaName(row) || (areaCode ? `村级区划${areaCode}` : "")
if (!areaCode && !name) return null
const areaRange = toNumber(pickValue(row, [
"areaRange",
"area_range",
"grasslandArea",
"grassland_area",
"acreage",
"area",
]), fallback.areaRange)
const yakNumber = toNumber(pickValue(row, [
"yakNumber",
"yak_number",
"number",
"yakCount",
"yak_count",
"livestockCount",
"livestock_count",
]), fallback.yakNumber)
return buildBalanceRow({
key: `village-${areaCode || `${town.key || town.name}-${name}-${index}`}`,
type: "village",
name,
areaCode,
areaRange,
yakNumber,
township: town.name,
source: row,
pressureFactor: fallback.pressureFactor,
})
}
function pickAreaCode(row = {}) {
return String(pickValue(row, [
"areaCode",
"area_code",
"code",
"value",
"id",
"areaId",
"area_id",
], "") || "").trim()
}
function pickAreaName(row = {}) {
return String(pickValue(row, [
"areaName",
"area_name",
"name",
"label",
"zoneName",
"villageName",
"village_name",
], "") || "").trim()
}
function getTownPressureFactor(town = {}) {
const yakNumber = toNumber(town.yakNumber, 0)
const livestockCapacity = toNumber(town.livestockCapacity, 0)
const yakSheepUnit = yakNumber * BALANCE_CALCULATION_PARAMS.yakToSheepUnit
const pressureFactor = yakSheepUnit ? livestockCapacity / yakSheepUnit : 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) {
@ -883,8 +1122,8 @@ function getProcessStepState(index) {
.balance-scope-tabs { .balance-scope-tabs {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px; gap: 6px;
button { button {
position: relative; position: relative;
@ -895,7 +1134,7 @@ function getProcessStepState(index) {
linear-gradient(180deg, rgba(9, 53, 69, 0.28), rgba(4, 25, 39, 0.18)), linear-gradient(180deg, rgba(9, 53, 69, 0.28), rgba(4, 25, 39, 0.18)),
linear-gradient(90deg, rgba(126, 251, 246, 0.04), transparent); linear-gradient(90deg, rgba(126, 251, 246, 0.04), transparent);
color: rgba(223, 250, 255, 0.76); color: rgba(223, 250, 255, 0.76);
font-size: 13px; font-size: 12px;
cursor: pointer; cursor: pointer;
clip-path: polygon(12px 0, 100% 0, calc(100% - 12px) 100%, 0 100%); clip-path: polygon(12px 0, 100% 0, calc(100% - 12px) 100%, 0 100%);
transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease; transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
@ -945,9 +1184,8 @@ function getProcessStepState(index) {
button { button {
position: relative; position: relative;
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) 64px; grid-template-columns: minmax(0, 1fr);
align-items: center; align-items: center;
column-gap: 10px;
min-width: 0; min-width: 0;
height: 42px; height: 42px;
padding: 0 12px 0 15px; padding: 0 12px 0 15px;
@ -2481,4 +2719,191 @@ function getProcessStepState(index) {
.balance-core-caption { .balance-core-caption {
color: rgba(223, 250, 255, 0.72); color: rgba(223, 250, 255, 0.72);
} }
.balance-sheet.is-ready {
padding: 18px 22px 22px;
}
.balance-sheet.is-ready .balance-analysis {
grid-template-rows: 158px 84px minmax(0, 1fr);
gap: 14px;
}
.balance-sheet.is-ready .balance-dashboard {
grid-template-columns: minmax(0, 1fr) 188px minmax(0, 1fr);
gap: 18px;
padding: 14px 22px;
}
.balance-sheet.is-ready .balance-object-block,
.balance-sheet.is-ready .balance-verdict-card {
min-height: 104px;
padding: 18px 20px;
box-sizing: border-box;
}
.balance-sheet.is-ready .balance-object-block strong,
.balance-sheet.is-ready .balance-verdict-card strong {
margin-top: 7px;
font-size: 28px;
line-height: 34px;
}
.balance-sheet.is-ready .balance-object-block small,
.balance-sheet.is-ready .balance-verdict-card small {
margin-top: 4px;
}
.balance-sheet.is-ready .balance-dashboard .balance-core-ring {
width: 132px;
height: 132px;
}
.balance-sheet.is-ready .balance-dashboard .balance-core-inner {
width: 102px;
height: 102px;
}
.balance-sheet.is-ready .balance-core-inner strong {
max-width: 92px;
margin: 5px 0 3px;
font-size: 26px;
line-height: 28px;
}
.balance-sheet.is-ready .balance-core-inner span,
.balance-sheet.is-ready .balance-core-inner small {
max-width: 92px;
font-size: 11px;
}
.balance-sheet.is-ready .balance-core {
gap: 5px;
}
.balance-sheet.is-ready .balance-core-caption {
font-size: 11px;
}
.balance-sheet.is-ready .balance-compare {
gap: 14px;
padding: 12px 20px;
}
.balance-sheet.is-ready .balance-bar-row {
grid-template-columns: 92px minmax(0, 1fr) 178px;
gap: 14px;
}
.balance-sheet.is-ready .balance-bar-row strong {
font-size: 20px;
}
.balance-sheet.is-ready .balance-detail {
grid-template-columns: minmax(0, 1.18fr) minmax(360px, 0.82fr);
gap: 14px;
}
.balance-sheet.is-ready .balance-process,
.balance-sheet.is-ready .balance-decision {
overflow: hidden;
padding: 13px 15px;
}
.balance-sheet.is-ready .balance-process {
grid-template-rows: 26px 32px repeat(4, minmax(58px, 1fr));
gap: 8px;
}
.balance-sheet.is-ready .balance-decision {
grid-template-rows: 26px 92px minmax(0, 1fr);
gap: 10px;
}
.balance-sheet.is-ready .balance-section-title {
height: 26px;
}
.balance-sheet.is-ready .balance-section-title span {
font-size: 15px;
}
.balance-sheet.is-ready .balance-process-track {
height: 32px;
}
.balance-sheet.is-ready .balance-process-track span {
height: 26px;
font-size: 11px;
line-height: 26px;
}
.balance-sheet.is-ready .balance-formula-row {
grid-template-columns: 36px minmax(0, 1fr) 74px;
gap: 10px;
min-height: 58px;
padding: 8px 12px;
}
.balance-sheet.is-ready .balance-formula-row > span {
width: 27px;
height: 27px;
font-size: 12px;
line-height: 25px;
}
.balance-sheet.is-ready .balance-formula-row strong {
font-size: 14px;
}
.balance-sheet.is-ready .balance-formula-row p {
margin-top: 3px;
font-size: 11px;
}
.balance-sheet.is-ready .balance-formula-row small {
margin-top: 3px;
font-size: 11px;
line-height: 16px;
white-space: normal;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.balance-sheet.is-ready .balance-formula-row em {
color: rgba(126, 251, 246, 0.82);
font-family: AlibabaPuHuiTi, Arial, sans-serif;
font-size: 13px;
}
.balance-sheet.is-ready .balance-conclusion {
padding: 13px 14px;
}
.balance-sheet.is-ready .balance-conclusion strong {
white-space: normal;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.balance-sheet.is-ready .balance-conclusion p {
margin-top: 6px;
}
.balance-sheet.is-ready .balance-advice-list {
gap: 8px;
overflow: hidden;
}
.balance-sheet.is-ready .balance-advice-item {
min-height: 34px;
padding: 1px 0;
}
.balance-sheet.is-ready .balance-advice-item p {
line-height: 18px;
-webkit-line-clamp: 2;
}
</style> </style>

@ -22,37 +22,37 @@ const grasslandYieldWanTon = 297.29
const woodlandPublicWanMu = 159.33 const woodlandPublicWanMu = 159.33
export const homeTopStats = [ export const homeTopStats = [
{
key: "forest-grass-wet-area",
zh: "林草湿总量",
name: "林草湿总量",
value: totalForestGrassWetWanMu,
unit: "万亩",
decimals: 2,
},
{ {
key: "yak-count", key: "yak-count",
zh: "牦牛存栏", zh: "牦牛数量",
name: "牦牛存栏", name: "牦牛数量",
value: totalTownYak, value: totalTownYak,
unit: "头", unit: "头",
decimals: 0, decimals: 0,
}, },
{ {
key: "trade-flow", key: "grassland-area",
zh: "交易流通", zh: "草地面积",
name: "交易流通", name: "草地面积",
value: totalTradeHead, value: grasslandAreaWanMu,
unit: "", unit: "万亩",
decimals: 0, decimals: 2,
}, },
{ {
key: "support-capacity", key: "wetland-area",
zh: "产业载体", zh: "湿地面积",
name: "产业载体", name: "湿地面积",
value: homeCore.baseCount + homeCore.projectCount, value: wetlandAreaWanMu,
unit: "个", unit: "万亩",
decimals: 0, decimals: 2,
},
{
key: "woodland-area",
zh: "林地面积",
name: "林地面积",
value: woodlandAreaWanMu,
unit: "万亩",
decimals: 2,
}, },
] ]

File diff suppressed because it is too large Load Diff

@ -46,6 +46,18 @@
</m-card> </m-card>
</div> </div>
</div> </div>
<div class="pasture-map-layer-tools">
<layer-panel
v-model="activeLayerKey"
v-model:selected-legend-key="selectedLegendKey"
topic-key="grassland"
:layers="pastureLayerOptions"
:legends="pastureMapLayer?.legends || []"
:width="232"
:height="430"
/>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -54,10 +66,12 @@
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue" import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue"
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 BusinessPanelRenderer from "@/views/forestGrassWet/components/BusinessPanelRenderer.vue" import BusinessPanelRenderer from "@/views/forestGrassWet/components/BusinessPanelRenderer.vue"
import { Assets } from "@/views/forestGrassWet/assets" import { Assets } from "@/views/forestGrassWet/assets"
import { getLayerByKey } from "@/config/layers" import { getLayerByKey } from "@/config/layers"
import { getPastureGrasslandsById, getPastureProfile } from "@/services/pastureApi" import { getHongyuanTownshipNameByCode, hongyuanTownshipsGeoJsonRaw, normalizeHongyuanTownName } from "@/config/townshipBoundaries"
import { getPastureGrasslandsById, getPastureProfile, getPasturesDetailsById } from "@/services/pastureApi"
import { import {
pastureAgeRows, pastureAgeRows,
pastureCoreRows, pastureCoreRows,
@ -81,12 +95,109 @@ const mapSceneRef = ref(null)
const mapReady = ref(false) const mapReady = ref(false)
const apiProfile = ref(pastureFallbackProfile) const apiProfile = ref(pastureFallbackProfile)
const pastureRows = ref(pastureListRows) const pastureRows = ref(pastureListRows)
const pastureMapLayer = getLayerByKey("grassland", "grassland-distribution") const activeLayerKey = ref("")
const selectedLegendKey = ref("")
const pastureTopStatNames = ["牧户数量", "户均牦牛头数", "户均草地面积", "联户数量"] const pastureTopStatNames = ["牧户数量", "户均牦牛头数", "户均草地面积", "联户数量"]
let mapAssets = null let mapAssets = null
let mapAssetsReady = false let mapAssetsReady = false
let destroyed = false let destroyed = false
let pastureEnrichToken = 0
const pastureTownCodeNames = {
"513233100": "邛溪镇",
"513233101": "刷经寺镇",
"513233103": "安曲镇",
"513233105": "龙日镇",
"513233200": "安曲镇",
"513233201": "龙日镇",
"513233202": "江茸乡",
"513233203": "查尔玛乡",
"513233204": "瓦切镇",
"513233205": "阿木乡",
"513233206": "刷经寺镇",
"513233207": "麦洼乡",
"513233208": "色地镇",
}
const pastureTownNameAliases = {
"安曲乡": "安曲镇",
"龙日乡": "龙日镇",
"瓦切乡": "瓦切镇",
"色地乡": "色地镇",
"壤口乡": "刷经寺镇",
}
const pastureFallbackTownshipById = {
"4028abc18eecd2af018eecdc44b7086b": "瓦切镇",
"402881828b486d54018b48741f590270": "查尔玛乡",
"402881828b486d54018b48740ed7024a": "麦洼乡",
"402881828b486d54018b48732e1c0044": "安曲镇",
"4028abc18eec9b97018eeca1538900db": "瓦切镇",
"402881828b486d54018b48740fb1024c": "安曲镇",
}
const pastureFallbackTownshipByName = {
"尼尕尔让": "瓦切镇",
"更迫尕": "查尔玛乡",
"王西卓玛": "麦洼乡",
"扎花克周": "安曲镇",
"恰波": "瓦切镇",
"热布丹": "安曲镇",
"求甲": "阿木乡",
"罗尔旦": "阿木乡",
"扎尔江": "阿木乡",
"达尔基": "阿木乡",
"尼科": "瓦切镇",
"班洛": "阿木乡",
"拉木共": "阿木乡",
"尕尔玛": "瓦切镇",
"尕尔丹": "安曲镇",
"泽旺甲": "瓦切镇",
"阿尕": "安曲镇",
"陈理": "瓦切镇",
"贡秋卓玛": "阿木乡",
"更多": "瓦切镇",
"阿泽": "瓦切镇",
"达勒错": "色地镇",
"泽朗罗日": "安曲镇",
"仁青": "瓦切镇",
"贡多": "阿木乡",
"泽周": "刷经寺镇",
"泽科": "瓦切镇",
"扎巴": "阿木乡",
"泽真": "瓦切镇",
"索拉姐": "瓦切镇",
"尕让罗周": "瓦切镇",
"尕让拉姆": "瓦切镇",
"更让尼玛": "阿木乡",
"向秋卓玛": "瓦切镇",
"东科": "阿木乡",
"扑瓦甲": "阿木乡",
}
const pastureTownshipFeatures = normalizeTownshipFeatures(hongyuanTownshipsGeoJsonRaw)
const pastureLayerOptions = computed(() => {
const boundaryLayer = getLayerByKey("grassland", "grassland-distribution")
if (!boundaryLayer) return []
return [{
...boundaryLayer,
name: "草场边界",
legends: [{ name: "草场边界", fillColor: "#1D953F", strokeColor: "#1D953F" }],
}]
})
const basePastureMapLayer = computed(() =>
activeLayerKey.value ? pastureLayerOptions.value.find((layer) => layer.key === activeLayerKey.value) || null : null
)
const selectedLegend = computed(() =>
(basePastureMapLayer.value?.legends || []).find((item) => item.key === selectedLegendKey.value) || null
)
const pastureMapLayer = computed(() => {
const layer = basePastureMapLayer.value
if (!layer) return null
if (!selectedLegend.value?.cqlFilter) return layer
return {
...layer,
cqlFilter: selectedLegend.value.cqlFilter,
activeLegend: selectedLegend.value,
name: selectedLegend.value.name || layer.name,
}
})
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(() => normalizeOnlineRows(apiProfile.value.online).length ? normalizeOnlineRows(apiProfile.value.online) : pastureOnlineRows)
@ -98,6 +209,10 @@ const mapRanking = computed(() => ({
title: "牧户分布排行", title: "牧户分布排行",
metricLabel: "牧户", metricLabel: "牧户",
unit: "户", unit: "户",
hideMapLabels: false,
hideTownNames: false,
showBars: false,
showPointMarkers: false,
rows: (townRows.value.length ? townRows.value : pastureMapRows).slice(0, 8).map((item) => ({ rows: (townRows.value.length ? townRows.value : pastureMapRows).slice(0, 8).map((item) => ({
name: item.name, name: item.name,
value: Number(item.value) || 0, value: Number(item.value) || 0,
@ -113,8 +228,7 @@ const townPanel = computed(() => ({
})) }))
const grasslandPanel = computed(() => ({ const grasslandPanel = computed(() => ({
key: "pasture-grassland-area", key: "pasture-grassland-area",
type: "bar", type: "heat",
variant: "distribution",
rows: grasslandRows.value, rows: grasslandRows.value,
})) }))
const seasonPanel = computed(() => ({ const seasonPanel = computed(() => ({
@ -178,13 +292,145 @@ async function loadPastureProfile() {
} }
emitPastureTopStats() emitPastureTopStats()
const normalizedList = normalizePastureRows(profile.list) const normalizedList = normalizePastureRows(profile.list)
pastureRows.value = normalizedList.length ? normalizedList : pastureListRows pastureRows.value = normalizedList.length ? normalizedList : sortPastureRows(pastureListRows)
enrichPastureRowsWithGrasslands(pastureRows.value)
} catch (error) { } catch (error) {
apiProfile.value = pastureFallbackProfile apiProfile.value = pastureFallbackProfile
pastureRows.value = pastureListRows pastureRows.value = sortPastureRows(pastureListRows)
} }
} }
async function enrichPastureRowsWithGrasslands(rows = []) {
const token = ++pastureEnrichToken
const targets = (Array.isArray(rows) ? rows : [])
.filter((row) => row?.township === "--" && getRealPastureId(row))
.slice(0, 80)
if (!targets.length) return
const results = await mapWithConcurrency(targets, 5, async (row) => {
const pastureId = getRealPastureId(row)
let grasslands = []
let detail = null
try {
grasslands = normalizeGrasslandDetailRows(await getPastureGrasslandsById(pastureId))
} catch (error) {
grasslands = []
}
let township = getPastureTownship(row, grasslands) || getPastureFallbackTownship(row, row.name, pastureId)
if (!township) {
try {
detail = await getPasturesDetailsById(pastureId)
} catch (error) {
detail = null
}
township = getPastureTownshipFromDetails(detail)
}
const canLocate = Boolean(pastureId || grasslands.some(hasGrasslandGeometry) || hasGrasslandGeometry(row))
if (!township && !grasslands.length && canLocate === row.canLocate) return null
const result = {
key: row.key,
id: row.id,
realPastureId: pastureId,
name: row.name,
township: township || row.township,
grasslands: grasslands.length ? grasslands : row.grasslands,
canLocate,
locateText: canLocate ? "定位" : "--",
}
applyPastureEnrichmentResult(result, token)
return result
})
if (destroyed || token !== pastureEnrichToken) return
const resultMap = new Map()
results.filter(Boolean).forEach((item) => {
;[item.key, item.id, item.realPastureId, item.name].filter(Boolean).forEach((key) => {
resultMap.set(String(key), item)
})
})
if (!resultMap.size) return
applyPastureEnrichmentResults(resultMap)
}
function applyPastureEnrichmentResult(result, token = pastureEnrichToken) {
if (!result || destroyed || token !== pastureEnrichToken) return
const resultMap = new Map()
;[result.key, result.id, result.realPastureId, result.name].filter(Boolean).forEach((key) => {
resultMap.set(String(key), result)
})
applyPastureEnrichmentResults(resultMap)
}
function applyPastureEnrichmentResults(resultMap) {
if (!resultMap?.size) return
pastureRows.value = pastureRows.value.map((row) => {
const result = [row.key, row.id, row.realPastureId, row.name]
.filter(Boolean)
.map((key) => resultMap.get(String(key)))
.find(Boolean)
if (!result) return row
const township = result.township && result.township !== "--" ? result.township : row.township
return {
...row,
township,
grasslands: result.grasslands,
canLocate: result.canLocate,
locateText: result.locateText,
searchText: [row.name, township, row.id].filter(Boolean).join(" "),
}
})
}
function normalizeGrasslandDetailRows(rows = []) {
const normalized = []
;(Array.isArray(rows) ? rows : []).forEach((item) => {
const childRows = normalizeRowGrasslands(item)
if (childRows.length) normalized.push(...childRows)
else if (item && typeof item === "object") normalized.push(item)
})
return normalized
}
function getPastureTownshipFromDetails(detail) {
const rows = Array.isArray(detail) ? detail : Array.isArray(detail?.rows) ? detail.rows : Array.isArray(detail?.list) ? detail.list : Array.isArray(detail?.data) ? detail.data : [detail]
for (const row of rows.filter(Boolean)) {
const fields = Array.isArray(row?.fields) ? row.fields : Array.isArray(row?.children) ? row.children : []
const directValue = pickValue(row, ["township", "townName", "town_name", "areaName", "area_name", "fullName", "full_name", "address", "牧场所在区域"])
if (directValue) return normalizePastureTownName(extractPastureTownName(directValue))
for (const field of fields) {
const label = String(field?.name || field?.label || field?.title || "").trim()
const value = pickValue(field, ["value", "text", "content"])
if (value && /区域|地址|乡镇/.test(label)) return normalizePastureTownName(extractPastureTownName(value))
}
const values = Object.entries(row || {})
.filter(([key]) => /fullName|full_name|address|areaName|area_name|town/i.test(key))
.map(([, value]) => value)
for (const value of values) {
if (value) return normalizePastureTownName(extractPastureTownName(value))
}
}
return ""
}
function extractPastureTownName(value) {
const text = String(value || "").trim()
if (!text || text === "--") return ""
const directMatch = text.match(/([\u4e00-\u9fa5]{1,12}(?:镇|乡))/)
return directMatch?.[1] || text
}
async function mapWithConcurrency(items, limit, worker) {
const results = new Array(items.length)
let nextIndex = 0
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
while (nextIndex < items.length) {
const currentIndex = nextIndex
nextIndex += 1
results[currentIndex] = await worker(items[currentIndex], currentIndex)
}
})
await Promise.all(workers)
return results
}
async function handlePastureSelect(row) { async function handlePastureSelect(row) {
if (!isPastureLocatable(row)) { if (!isPastureLocatable(row)) {
mapSceneRef.value?.focusPasture?.({ name: row?.name }) mapSceneRef.value?.focusPasture?.({ name: row?.name })
@ -421,10 +667,10 @@ function normalizePastureRows(rows = []) {
.map((item, index) => { .map((item, index) => {
const name = pickValue(item, ["name", "pasturesName", "pastures_name", "muhuName", "muhu_name", "pastureName", "pasture_name", "herdsmanName"]) const name = pickValue(item, ["name", "pasturesName", "pastures_name", "muhuName", "muhu_name", "pastureName", "pasture_name", "herdsmanName"])
if (!name) return null if (!name) return null
const grasslandArea = pickValue(item, ["grasslandArea", "grassland_area", "acreage", "area", "areaRange", "area_range", "cdAcreage", "cd_acreage", "totalAcreage", "total_acreage"])
const township = pickValue(item, ["township", "townName", "areaName", "area_name", "zoneName", "address"])
const realId = getRealPastureId(item)
const grasslands = normalizeRowGrasslands(item) const grasslands = normalizeRowGrasslands(item)
const grasslandArea = pickPastureArea(item) || sumPastureGrasslandArea(grasslands)
const realId = getRealPastureId(item)
const township = getPastureTownship(item, grasslands) || getPastureFallbackTownship(item, name, realId)
const normalized = { const normalized = {
...item, ...item,
key: item.key || item.id || item.pasturesId || item.pastures_id || `${name}-${index}`, key: item.key || item.id || item.pasturesId || item.pastures_id || `${name}-${index}`,
@ -432,8 +678,8 @@ function normalizePastureRows(rows = []) {
realPastureId: realId, realPastureId: realId,
name, name,
township: township || "--", township: township || "--",
grasslandArea: Number(grasslandArea) || 0, grasslandArea,
acreage: Number(grasslandArea) || 0, acreage: grasslandArea,
grasslands, grasslands,
} }
normalized.canLocate = Boolean(realId || grasslands.some(hasGrasslandGeometry) || hasGrasslandGeometry(item)) normalized.canLocate = Boolean(realId || grasslands.some(hasGrasslandGeometry) || hasGrasslandGeometry(item))
@ -444,6 +690,67 @@ function normalizePastureRows(rows = []) {
return normalized return normalized
}) })
.filter(Boolean) .filter(Boolean)
.sort(comparePastureRows)
}
function sortPastureRows(rows = []) {
return [...(Array.isArray(rows) ? rows : [])]
.map((row) => ({
...row,
grasslandArea: pickPastureArea(row),
acreage: pickPastureArea(row),
}))
.sort(comparePastureRows)
}
function comparePastureRows(a, b) {
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
const locatableA = isPastureLocatable(a) ? 1 : 0
const locatableB = isPastureLocatable(b) ? 1 : 0
if (locatableA !== locatableB) return locatableB - locatableA
if (areaA !== areaB) return areaB - areaA
return String(a?.name || "").localeCompare(String(b?.name || ""), "zh-CN")
}
function pickPastureArea(item = {}) {
const keys = [
"acreage",
"grasslandArea",
"grassland_area",
"area",
"areaRange",
"area_range",
"cdAcreage",
"cd_acreage",
"totalAcreage",
"total_acreage",
"grasslandAcreage",
"grassland_acreage",
"pastureArea",
"pasture_area",
]
const values = keys.map((key) => parsePastureArea(item?.[key]))
const positive = values.find((value) => value > 0)
return positive || values.find((value) => value === 0) || 0
}
function sumPastureGrasslandArea(grasslands = []) {
return (Array.isArray(grasslands) ? grasslands : []).reduce((total, row) => total + pickPastureArea(row), 0)
}
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 aggregatePastureAreaRows(rows = []) { function aggregatePastureAreaRows(rows = []) {
@ -500,7 +807,7 @@ function normalizeRowGrasslands(item = {}) {
else if (Array.isArray(value?.list)) rows.push(...value.list) else if (Array.isArray(value?.list)) rows.push(...value.list)
else if (Array.isArray(value?.data)) rows.push(...value.data) else if (Array.isArray(value?.data)) rows.push(...value.data)
}) })
const wkt = pickValue(item, ["wkt", "geo", "geom", "geometryWkt", "shape"]) const wkt = pickValue(item, ["wkt", "geo", "geom", "geometryWkt", "geometry_wkt", "shape", "location"])
if (wkt || item?.geometry) { if (wkt || item?.geometry) {
rows.push({ rows.push({
...item, ...item,
@ -519,11 +826,11 @@ function hasGrasslandGeometry(item = {}) {
if (item?.type === "Feature") return true if (item?.type === "Feature") return true
if (item?.type && item?.coordinates) return true if (item?.type && item?.coordinates) return true
if (item?.geometry?.type || item?.geometry?.coordinates || typeof item?.geometry === "string") return true if (item?.geometry?.type || item?.geometry?.coordinates || typeof item?.geometry === "string") return true
return Boolean(pickValue(item, ["wkt", "geo", "geom", "geometryWkt", "shape"])) return Boolean(pickValue(item, ["wkt", "geo", "geom", "geometryWkt", "geometry_wkt", "shape", "location"]))
} }
function getRealPastureId(row = {}) { function getRealPastureId(row = {}) {
const id = row?.realPastureId || row?.pastureId || row?.pasturesId || row?.pastures_id || row?.muhuId const id = row?.realPastureId || row?.pastureId || row?.pasturesId || row?.pastures_id || row?.muhuId || row?.id
return id === undefined || id === null || id === "" ? null : id return id === undefined || id === null || id === "" ? null : id
} }
@ -542,6 +849,224 @@ function pickValue(item = {}, keys = []) {
return undefined return undefined
} }
function getPastureTownship(item = {}, grasslands = []) {
const directName = pickValue(item, ["township", "townName", "town_name", "areaName", "area_name", "zoneName", "zone_name"])
if (directName) return normalizePastureTownName(directName)
const codeName = getPastureTownNameFromCode(pickValue(item, ["areaCode", "area_code", "townCode", "town_code", "code"]))
if (codeName) return codeName
for (const grassland of grasslands) {
const grasslandName = pickValue(grassland, ["township", "townName", "town_name", "areaName", "area_name", "zoneName", "zone_name"])
if (grasslandName) return normalizePastureTownName(grasslandName)
const grasslandCodeName = getPastureTownNameFromCode(pickValue(grassland, ["areaCode", "area_code", "townCode", "town_code", "code"]))
if (grasslandCodeName) return grasslandCodeName
}
const itemGeometryName = getPastureTownNameFromGeometry(item)
if (itemGeometryName) return itemGeometryName
for (const grassland of grasslands) {
const grasslandGeometryName = getPastureTownNameFromGeometry(grassland)
if (grasslandGeometryName) return grasslandGeometryName
}
return ""
}
function normalizePastureTownName(value) {
const text = String(value || "").trim()
if (!text || text === "--") return ""
const sharedName = normalizeHongyuanTownName(text)
if (sharedName && sharedName !== text) return sharedName
if (pastureTownNameAliases[text]) return pastureTownNameAliases[text]
if (pastureTownCodeNames[text]) return pastureTownCodeNames[text]
const codeName = getPastureTownNameFromCode(text)
if (codeName) return codeName
return text
}
function getPastureFallbackTownship(item = {}, name = "", realId = "") {
const idKeys = [
realId,
item.realPastureId,
item.pastureId,
item.pasturesId,
item.pastures_id,
item.muhuId,
item.id,
]
for (const key of idKeys) {
const township = pastureFallbackTownshipById[String(key || "").trim()]
if (township) return normalizePastureTownName(township)
}
const nameKeys = [
name,
item.name,
item.pasturesName,
item.pastures_name,
item.muhuName,
item.muhu_name,
item.pastureName,
item.pasture_name,
]
for (const key of nameKeys) {
const township = pastureFallbackTownshipByName[normalizePastureNameKey(key)]
if (township) return normalizePastureTownName(township)
}
return ""
}
function normalizePastureNameKey(value) {
return String(value || "")
.replace(/[·•・..。`´’‘ʼʹ''"“”\s\-—_()()]/g, "")
.replace(/˙/g, "")
}
function getPastureTownNameFromCode(value) {
const text = String(value || "").trim()
if (!text) return ""
const normalizedCode = text.replace(/\D/g, "")
if (!normalizedCode) return ""
return getHongyuanTownshipNameByCode(normalizedCode) || pastureTownCodeNames[normalizedCode] || pastureTownCodeNames[normalizedCode.slice(0, 9)] || ""
}
function getPastureTownNameFromGeometry(item = {}) {
const geometry = getPastureGeometry(item)
const center = getGeometryCenter(geometry)
if (!center) return ""
const feature = pastureTownshipFeatures.find((row) => isLngLatInGeometry(center, row.geometry))
return feature?.name ? normalizePastureTownName(feature.name) : ""
}
function getPastureGeometry(item = {}) {
if (item?.type === "Feature") return getPastureGeometry(item.geometry)
if (item?.type && item?.coordinates) return item
if (item?.geometry?.type || item?.geometry?.coordinates) return item.geometry
const text = pickValue(item, ["geometry", "location", "wkt", "geo", "geom", "geometryWkt", "geometry_wkt", "shape"])
if (!text || typeof text !== "string") return null
const cleanText = text.trim()
if (!cleanText) return null
if (cleanText.startsWith("{") || cleanText.startsWith("[")) {
try {
const parsed = JSON.parse(cleanText)
return getPastureGeometry(parsed)
} catch (error) {
return null
}
}
return parsePastureWktGeometry(cleanText)
}
function parsePastureWktGeometry(value) {
const text = String(value || "").trim()
const typeMatch = text.match(/^([A-Z]+)(?:\s+[A-Z]+)?\s*\(/i)
const type = typeMatch?.[1]?.toUpperCase()
if (!type) return null
const body = text.slice(text.indexOf("("))
if (type === "POINT") return { type: "Point", coordinates: parsePastureWktPoint(body) }
if (type === "MULTIPOINT") return { type: "MultiPoint", coordinates: getPastureWktChildGroups(body).map(parsePastureWktPoint).filter(Boolean) }
if (type === "LINESTRING") return { type: "LineString", coordinates: parsePastureWktLine(body) }
if (type === "MULTILINESTRING") return { type: "MultiLineString", coordinates: getPastureWktChildGroups(body).map(parsePastureWktLine).filter((line) => line.length) }
if (type === "POLYGON") return { type: "Polygon", coordinates: parsePastureWktPolygonBody(body) }
if (type === "MULTIPOLYGON") {
return { type: "MultiPolygon", coordinates: getPastureWktChildGroups(body).map(parsePastureWktPolygonBody).filter((polygon) => polygon.length) }
}
return null
}
function parsePastureWktPoint(text) {
const point = String(text || "").replace(/^\(+|\)+$/g, "").trim().split(/\s+/).slice(0, 2).map(Number)
return point.length >= 2 && Number.isFinite(point[0]) && Number.isFinite(point[1]) ? [point[0], point[1]] : null
}
function parsePastureWktLine(text) {
return String(text || "").replace(/^\(+|\)+$/g, "").split(",").map(parsePastureWktPoint).filter(Boolean)
}
function parsePastureWktPolygonBody(body) {
return getPastureWktChildGroups(body)
.map(parsePastureWktLine)
.filter((ring) => ring.length >= 3)
}
function getPastureWktChildGroups(text) {
const groups = []
let depth = 0
let start = -1
Array.from(String(text || "")).forEach((char, index) => {
if (char === "(") {
if (depth === 0) start = index + 1
depth += 1
} else if (char === ")") {
depth -= 1
if (depth === 0 && start >= 0) {
groups.push(text.slice(start, index))
start = -1
}
}
})
return groups.length ? groups : [String(text || "")]
}
function normalizeTownshipFeatures(raw) {
try {
const data = typeof raw === "string" ? JSON.parse(raw) : raw
return (data?.features || [])
.map((feature) => ({
name: feature?.properties?.name || feature?.properties?.areaName || feature?.properties?.townName,
code: feature?.properties?.code,
geometry: feature?.geometry,
}))
.filter((feature) => feature.name && feature.geometry?.coordinates)
} catch (error) {
return []
}
}
function getGeometryCenter(geometry) {
const coordinates = getGeometryCoordinates(geometry)
if (!coordinates.length) return null
const sum = coordinates.reduce((total, point) => ({
lng: total.lng + point[0],
lat: total.lat + point[1],
}), { lng: 0, lat: 0 })
return [sum.lng / coordinates.length, sum.lat / coordinates.length]
}
function getGeometryCoordinates(geometry) {
const points = []
const visit = (value) => {
if (!Array.isArray(value)) return
if (value.length >= 2 && Number.isFinite(Number(value[0])) && Number.isFinite(Number(value[1]))) {
points.push([Number(value[0]), Number(value[1])])
return
}
value.forEach(visit)
}
visit(geometry?.coordinates)
return points
}
function isLngLatInGeometry(point, geometry) {
if (!point || !geometry?.coordinates) return false
if (geometry.type === "Polygon") return isLngLatInPolygon(point, geometry.coordinates)
if (geometry.type === "MultiPolygon") return geometry.coordinates.some((polygon) => isLngLatInPolygon(point, polygon))
return false
}
function isLngLatInPolygon(point, polygon) {
const ring = polygon?.[0]
if (!Array.isArray(ring) || ring.length < 3) return false
let inside = false
const x = point[0]
const y = point[1]
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
const xi = Number(ring[i]?.[0])
const yi = Number(ring[i]?.[1])
const xj = Number(ring[j]?.[0])
const yj = Number(ring[j]?.[1])
const intersects = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / ((yj - yi) || 1e-12) + xi)
if (intersects) inside = !inside
}
return inside
}
</script> </script>
<style lang="scss"> <style lang="scss">
@ -654,6 +1179,23 @@ function pickValue(item = {}, keys = []) {
} }
} }
.pasture-map-layer-tools {
position: absolute;
top: 372px;
right: 458px;
z-index: 7;
width: 232px;
height: 430px;
pointer-events: all;
.m-card {
width: 100% !important;
height: 100% !important;
background: rgba(3, 18, 30, 0.54);
backdrop-filter: blur(2px);
}
}
.pasture-management .m-card-hd-title { .pasture-management .m-card-hd-title {
color: #ffffff !important; color: #ffffff !important;
background: none !important; background: none !important;

@ -98,9 +98,6 @@
> >
<template #right> <template #right>
<div class="m-header-tools"> <div class="m-header-tools">
<div class="m-header-date">
<span>{{ currentDate }}</span><span>{{ currentTime }}</span>
</div>
<button class="logout-button" title="退出登录" type="button" @click="handleLogout"> <button class="logout-button" title="退出登录" type="button" @click="handleLogout">
退出 退出
</button> </button>
@ -118,7 +115,7 @@
</div> </div>
<div <div
v-if="totalView.length && !['smart-management', 'grass-livestock-balance'].includes(activeModule.key)" v-if="totalView.length && !['smart-management', 'grass-livestock-balance', 'yak-industry-chain'].includes(activeModule.key)"
class="top-count-card" class="top-count-card"
:class="{ 'is-four': totalView.length > 3 }" :class="{ 'is-four': totalView.length > 3 }"
> >
@ -223,11 +220,8 @@ const activeModuleKey = ref("home")
const activeEcologyView = ref("national-park") const activeEcologyView = ref("national-park")
const renderedModules = ref({ home: true }) const renderedModules = ref({ home: true })
const moduleTopStats = ref({}) const moduleTopStats = ref({})
const currentDate = ref("")
const currentTime = ref("")
const loadingVisible = ref(true) const loadingVisible = ref(true)
const loadingProgress = ref(0) const loadingProgress = ref(0)
let timeTimer = null
let screenAutofit = null let screenAutofit = null
let loadingHideStarted = false let loadingHideStarted = false
const loadingStartedAt = Date.now() const loadingStartedAt = Date.now()
@ -284,12 +278,9 @@ onMounted(() => {
resize: true, resize: true,
}) })
resetRootScroll() resetRootScroll()
updateCurrentTime()
timeTimer = window.setInterval(updateCurrentTime, 1000)
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (timeTimer) window.clearInterval(timeTimer)
screenAutofit?.off?.("#large-screen") screenAutofit?.off?.("#large-screen")
gsap.killTweensOf(loadingTweenState) gsap.killTweensOf(loadingTweenState)
}) })
@ -403,13 +394,6 @@ function renderModule(key) {
} }
} }
function updateCurrentTime() {
const date = new Date()
const pad = (num) => `${num}`.padStart(2, "0")
currentDate.value = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
currentTime.value = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
function resetRootScroll() { function resetRootScroll() {
window.scrollTo(0, 0) window.scrollTo(0, 0)
document.documentElement.scrollTop = 0 document.documentElement.scrollTop = 0
@ -538,18 +522,6 @@ function handleLogout() {
align-items: center; align-items: center;
gap: 12px; gap: 12px;
pointer-events: auto; pointer-events: auto;
.m-header-date {
display: flex;
align-items: center;
gap: 12px;
}
span {
color: #c4f3fe;
font-size: 14px;
white-space: nowrap;
}
} }
.logout-button { .logout-button {

@ -7,21 +7,21 @@ import {
} from "@/views/gdMap/livestockData" } from "@/views/gdMap/livestockData"
export const industryStageDefs = [ export const industryStageDefs = [
{ key: "infrastructure", name: "基础设施", color: "#32dcff", unit: "", metricLabel: "项目" }, { key: "family-pasture", name: "家庭生态牧场", color: "#24f6c4", unit: "", metricLabel: "牧场", width: 126 },
{ key: "breeding", name: "种养基地", color: "#24f6c4", unit: "", metricLabel: "基地" }, { key: "artificial-grassland", name: "人工草地", color: "#7ee269", unit: "万亩", metricLabel: "草地", width: 112 },
{ key: "support", name: "配套产业", color: "#ffe06b", unit: "个", metricLabel: "点位" }, { key: "scale-breeding", name: "规模化养殖基地", subName: "牦牛选育场", color: "#32dcff", unit: "个", metricLabel: "基地", width: 132 },
{ key: "processing", name: "加工基地", color: "#ff9a62", unit: "", metricLabel: "企业" }, { key: "forage-storage", name: "防灾减灾", subName: "饲草储备库", color: "#ffe06b", unit: "", metricLabel: "库点", width: 126 },
{ key: "service", name: "社会化服务", color: "#9ea7ff", unit: "", metricLabel: "主体" }, { key: "slaughter-processing", name: "屠宰与加工", color: "#ff9a62", unit: "", metricLabel: "主体", width: 112 },
{ key: "equipment", name: "机械设备", color: "#7ee269", unit: "套", metricLabel: "设备" }, { key: "farm-equipment", name: "农机装备", color: "#30dcff", unit: "套", metricLabel: "装备", width: 112 },
{ key: "vr", name: "产业VR", color: "#30dcff", unit: "", metricLabel: "场景" }, { key: "social-service", name: "社会化服务", subName: "金融/疫病/银行/保险/培训", color: "#9ea7ff", unit: "", metricLabel: "主体", width: 132 },
] ]
export const supportPointRows = [ export const supportPointRows = [
{ {
key: "support-disaster-001", key: "support-disaster-001",
name: "嚷口防灾减灾饲草储备库", name: "嚷口防灾减灾饲草储备库",
stageKey: "support", stageKey: "forage-storage",
typeName: "防震减灾", typeName: "防灾减灾饲草储备库",
capacity: 1500, capacity: 1500,
capacityUnit: "吨", capacityUnit: "吨",
town: "刷经寺镇", town: "刷经寺镇",
@ -34,8 +34,8 @@ export const supportPointRows = [
{ {
key: "support-disaster-002", key: "support-disaster-002",
name: "安曲防灾减灾饲草储备库", name: "安曲防灾减灾饲草储备库",
stageKey: "support", stageKey: "forage-storage",
typeName: "防震减灾", typeName: "防灾减灾饲草储备库",
capacity: 1500, capacity: 1500,
capacityUnit: "吨", capacityUnit: "吨",
town: "安曲镇", town: "安曲镇",
@ -48,8 +48,8 @@ export const supportPointRows = [
{ {
key: "support-disaster-003", key: "support-disaster-003",
name: "瓦切唐日合作社饲草储备库", name: "瓦切唐日合作社饲草储备库",
stageKey: "support", stageKey: "forage-storage",
typeName: "防震减灾", typeName: "防灾减灾饲草储备库",
capacity: 1500, capacity: 1500,
capacityUnit: "吨", capacityUnit: "吨",
town: "瓦切镇", town: "瓦切镇",
@ -62,8 +62,8 @@ export const supportPointRows = [
{ {
key: "support-disaster-004", key: "support-disaster-004",
name: "瓦切牧草交易中心饲草储备库", name: "瓦切牧草交易中心饲草储备库",
stageKey: "support", stageKey: "forage-storage",
typeName: "防震减灾", typeName: "防灾减灾饲草储备库",
capacity: 1500, capacity: 1500,
capacityUnit: "吨", capacityUnit: "吨",
town: "瓦切镇", town: "瓦切镇",
@ -76,8 +76,8 @@ export const supportPointRows = [
{ {
key: "support-disaster-005", key: "support-disaster-005",
name: "龙日坝龙日坝村 1 饲草储备库", name: "龙日坝龙日坝村 1 饲草储备库",
stageKey: "support", stageKey: "forage-storage",
typeName: "防震减灾", typeName: "防灾减灾饲草储备库",
capacity: 1500, capacity: 1500,
capacityUnit: "吨", capacityUnit: "吨",
town: "龙日镇", town: "龙日镇",
@ -90,8 +90,8 @@ export const supportPointRows = [
{ {
key: "support-disaster-006", key: "support-disaster-006",
name: "龙日坝龙日坝村 2 饲草储备库", name: "龙日坝龙日坝村 2 饲草储备库",
stageKey: "support", stageKey: "forage-storage",
typeName: "防震减灾", typeName: "防灾减灾饲草储备库",
capacity: 1500, capacity: 1500,
capacityUnit: "吨", capacityUnit: "吨",
town: "龙日镇", town: "龙日镇",
@ -104,8 +104,8 @@ export const supportPointRows = [
{ {
key: "support-disaster-007", key: "support-disaster-007",
name: "江茸乡饲草储备库", name: "江茸乡饲草储备库",
stageKey: "support", stageKey: "forage-storage",
typeName: "防震减灾", typeName: "防灾减灾饲草储备库",
capacity: 1500, capacity: 1500,
capacityUnit: "吨", capacityUnit: "吨",
town: "江茸乡", town: "江茸乡",
@ -121,7 +121,7 @@ export const processingPointRows = [
{ {
key: "support-processing-001", key: "support-processing-001",
name: "西部牦牛产业集团有限责任公司", name: "西部牦牛产业集团有限责任公司",
stageKey: "processing", stageKey: "slaughter-processing",
typeName: "乳制品加工", typeName: "乳制品加工",
capacity: 3000, capacity: 3000,
capacityUnit: "吨", capacityUnit: "吨",
@ -136,7 +136,7 @@ export const processingPointRows = [
{ {
key: "support-processing-002", key: "support-processing-002",
name: "四川红原菊乐牦牛乳业有限公司", name: "四川红原菊乐牦牛乳业有限公司",
stageKey: "processing", stageKey: "slaughter-processing",
typeName: "乳制品加工", typeName: "乳制品加工",
capacity: 4000, capacity: 4000,
capacityUnit: "吨", capacityUnit: "吨",
@ -151,7 +151,7 @@ export const processingPointRows = [
{ {
key: "support-processing-003", key: "support-processing-003",
name: "四川雅克乳业有限公司", name: "四川雅克乳业有限公司",
stageKey: "processing", stageKey: "slaughter-processing",
typeName: "乳制品加工", typeName: "乳制品加工",
capacity: 1000, capacity: 1000,
capacityUnit: "吨", capacityUnit: "吨",
@ -166,7 +166,7 @@ export const processingPointRows = [
{ {
key: "support-processing-004", key: "support-processing-004",
name: "四川红原遛遛牛食品有限责任公司", name: "四川红原遛遛牛食品有限责任公司",
stageKey: "processing", stageKey: "slaughter-processing",
typeName: "肉制品加工", typeName: "肉制品加工",
capacity: 4500, capacity: 4500,
capacityUnit: "吨", capacityUnit: "吨",
@ -181,7 +181,7 @@ export const processingPointRows = [
{ {
key: "support-processing-005", key: "support-processing-005",
name: "四川红原黑帐篷牦牛肉食品有限责任公司", name: "四川红原黑帐篷牦牛肉食品有限责任公司",
stageKey: "processing", stageKey: "slaughter-processing",
typeName: "肉制品加工", typeName: "肉制品加工",
capacity: 300, capacity: 300,
capacityUnit: "吨", capacityUnit: "吨",
@ -196,7 +196,7 @@ export const processingPointRows = [
{ {
key: "support-processing-006", key: "support-processing-006",
name: "红原御坊斋牦牛肉制品厂", name: "红原御坊斋牦牛肉制品厂",
stageKey: "processing", stageKey: "slaughter-processing",
typeName: "肉制品加工", typeName: "肉制品加工",
capacity: 200, capacity: 200,
capacityUnit: "吨", capacityUnit: "吨",
@ -211,7 +211,7 @@ export const processingPointRows = [
{ {
key: "support-processing-007", key: "support-processing-007",
name: "四川牦牛火了生态农业科技有限公司", name: "四川牦牛火了生态农业科技有限公司",
stageKey: "processing", stageKey: "slaughter-processing",
typeName: "肉制品加工", typeName: "肉制品加工",
capacity: 0, capacity: 0,
capacityUnit: "吨", capacityUnit: "吨",
@ -227,7 +227,6 @@ export const processingPointRows = [
export const industryPointRows = [...supportPointRows, ...processingPointRows] export const industryPointRows = [...supportPointRows, ...processingPointRows]
const infrastructureProjectDistribution = [3, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1]
const serviceSubjectTotal = 2618 const serviceSubjectTotal = 2618
const tradeFlowTotal = 220605 const tradeFlowTotal = 220605
const equipmentTotal = townRecognitionRows.reduce((sum, item) => sum + Number(item.devices || 0), 0) const equipmentTotal = townRecognitionRows.reduce((sum, item) => sum + Number(item.devices || 0), 0)
@ -235,7 +234,7 @@ const supportCapacity = supportPointRows.reduce((sum, item) => sum + Number(item
const processingCapacity = processingPointRows.reduce((sum, item) => sum + Number(item.capacity || 0), 0) const processingCapacity = processingPointRows.reduce((sum, item) => sum + Number(item.capacity || 0), 0)
const processingOutputValue = processingPointRows.reduce((sum, item) => sum + Number(item.outputValue || 0), 0) const processingOutputValue = processingPointRows.reduce((sum, item) => sum + Number(item.outputValue || 0), 0)
const allTownCount = new Set(grasslandYakRows.map((item) => item.zoneName)).size const allTownCount = new Set(grasslandYakRows.map((item) => item.zoneName)).size
const carrierTotal = homeCore.projectCount + homeCore.baseCount + industryPointRows.length + serviceSubjectTotal + equipmentTotal const carrierTotal = homeCore.herdsmanCount + homeCore.baseCount + industryPointRows.length + serviceSubjectTotal + equipmentTotal
export const industryTopStats = [ export const industryTopStats = [
{ zh: "牦牛存栏估算", name: "牦牛存栏估算", value: estimatedStock, unit: "头" }, { zh: "牦牛存栏估算", name: "牦牛存栏估算", value: estimatedStock, unit: "头" },
@ -253,26 +252,35 @@ export const industryOverviewRows = [
export const industryStageRows = [ export const industryStageRows = [
{ {
key: "infrastructure", key: "family-pasture",
name: "基础设施", name: "家庭生态牧场",
value: homeCore.projectCount, value: homeCore.herdsmanCount,
unit: "", unit: "",
capacity: homeCore.projectCount, capacity: estimatedStock,
capacityUnit: "", capacityUnit: "",
note: "产业道路、工程与配套项目", note: "家庭牧场、牧户与生态承载",
}, },
{ {
key: "breeding", key: "artificial-grassland",
name: "种养基地", name: "人工草地",
value: formatWanMu(homeCore.grasslandArea),
unit: "万亩",
capacity: formatWanMu(homeCore.grasslandArea),
capacityUnit: "万亩",
note: "人工草地、牧草供给与补饲",
},
{
key: "scale-breeding",
name: "规模化养殖基地",
value: homeCore.baseCount, value: homeCore.baseCount,
unit: "个", unit: "个",
capacity: estimatedStock, capacity: estimatedStock,
capacityUnit: "头", capacityUnit: "头",
note: "基地、牧场与存栏承载", note: "规模养殖基地与牦牛选育场",
}, },
{ {
key: "support", key: "forage-storage",
name: "配套产业", name: "防灾减灾饲草储备库",
value: supportPointRows.length, value: supportPointRows.length,
unit: "个", unit: "个",
capacity: supportCapacity, capacity: supportCapacity,
@ -280,94 +288,87 @@ export const industryStageRows = [
note: "饲草储备与防灾保障", note: "饲草储备与防灾保障",
}, },
{ {
key: "processing", key: "slaughter-processing",
name: "加工基地", name: "屠宰与加工",
value: processingPointRows.length, value: processingPointRows.length,
unit: "家", unit: "家",
capacity: processingCapacity, capacity: processingCapacity,
capacityUnit: "吨", capacityUnit: "吨",
note: "乳品、肉品加工转化", note: "屠宰、乳品与肉品加工转化",
}, },
{ {
key: "service", key: "farm-equipment",
name: "社会化服务", name: "农机装备",
value: serviceSubjectTotal,
unit: "个",
capacity: tradeFlowTotal,
capacityUnit: "头",
note: "保险、交易、检疫与流通",
},
{
key: "equipment",
name: "机械设备",
value: equipmentTotal, value: equipmentTotal,
unit: "套", unit: "套",
capacity: equipmentTotal, capacity: equipmentTotal,
capacityUnit: "套", capacityUnit: "套",
note: "识别、监测与生产装备", note: "农机装备、识别监测与生产设备",
}, },
{ {
key: "vr", key: "social-service",
name: "产业VR", name: "社会化服务",
value: 1, value: serviceSubjectTotal,
unit: "", unit: "",
capacity: 6, capacity: tradeFlowTotal,
capacityUnit: "", capacityUnit: "",
note: "产业链沉浸式展示", note: "金融、疫病防控、银行、保险、技术培训",
}, },
] ]
export const chainFlowRows = [ export const chainFlowRows = [
{ key: "resource", name: "草地供给", value: 84, unit: "%", extra: "资源底座" }, { key: "family-pasture", name: "家庭牧场", value: 86, unit: "%", extra: "牧户联动" },
{ key: "breeding", name: "基地养殖", value: 88, unit: "%", extra: "存栏校准" }, { key: "artificial-grassland", name: "人工草地", value: 84, unit: "%", extra: "牧草供给" },
{ key: "health", name: "防疫保障", value: 92, unit: "%", extra: "台账接入" }, { key: "scale-breeding", name: "规模养殖", value: 88, unit: "%", extra: "选育承载" },
{ key: "trade", name: "交易流通", value: 78, unit: "%", extra: "检疫出证" }, { key: "forage-storage", name: "饲草储备", value: 91, unit: "%", extra: "防灾保障" },
{ key: "processing", name: "加工转化", value: 81, unit: "%", extra: "乳肉加工" }, { key: "slaughter-processing", name: "屠宰加工", value: 81, unit: "%", extra: "乳肉加工" },
{ key: "market", name: "品牌销售", value: 73, unit: "%", extra: "外销拓展" }, { key: "farm-equipment", name: "农机装备", value: 76, unit: "%", extra: "装备在线" },
{ key: "social-service", name: "社会服务", value: 79, unit: "%", extra: "金融防疫" },
] ]
export const stageHealthRows = { export const stageHealthRows = {
infrastructure: [ "family-pasture": [
{ key: "project-online", name: "项目入库", value: 95, unit: "%", extra: "95%" }, { key: "pasture-online", name: "牧场在线", value: 92, unit: "%", extra: "92%" },
{ key: "facility-cover", name: "设施覆盖", value: 82, unit: "%", extra: "82%" }, { key: "household-link", name: "牧户关联", value: 86, unit: "%", extra: "86%" },
{ key: "fund-track", name: "资金跟踪", value: 76, unit: "%", extra: "76%" }, { key: "ecology-load", name: "生态承载", value: 78, unit: "%", extra: "78%" },
{ key: "maintenance", name: "运维闭环", value: 68, unit: "%", extra: "68%" }, { key: "daily-record", name: "日常管护", value: 74, unit: "%", extra: "74%" },
],
"artificial-grassland": [
{ key: "grassland-supply", name: "草地供给", value: 88, unit: "%", extra: "88%" },
{ key: "yield-match", name: "产草匹配", value: 82, unit: "%", extra: "82%" },
{ key: "mowing-link", name: "刈割调度", value: 73, unit: "%", extra: "73%" },
{ key: "supplement", name: "补饲保障", value: 79, unit: "%", extra: "79%" },
], ],
breeding: [ "scale-breeding": [
{ key: "base-online", name: "基地在线", value: 92, unit: "%", extra: "92%" }, { key: "base-online", name: "基地在线", value: 92, unit: "%", extra: "92%" },
{ key: "selection", name: "选育管理", value: 84, unit: "%", extra: "84%" },
{ key: "stock-match", name: "存栏匹配", value: 88, unit: "%", extra: "88%" }, { key: "stock-match", name: "存栏匹配", value: 88, unit: "%", extra: "88%" },
{ key: "pasture-link", name: "草场关联", value: 74, unit: "%", extra: "74%" },
{ key: "source-trace", name: "源头追溯", value: 69, unit: "%", extra: "69%" }, { key: "source-trace", name: "源头追溯", value: 69, unit: "%", extra: "69%" },
], ],
support: [ "forage-storage": [
{ key: "storage-ready", name: "储备达标", value: 91, unit: "%", extra: "91%" }, { key: "storage-ready", name: "储备达标", value: 91, unit: "%", extra: "91%" },
{ key: "town-cover", name: "镇域覆盖", value: 64, unit: "%", extra: "64%" }, { key: "town-cover", name: "镇域覆盖", value: 64, unit: "%", extra: "64%" },
{ key: "dispatch", name: "调拨能力", value: 83, unit: "%", extra: "83%" }, { key: "dispatch", name: "调拨能力", value: 83, unit: "%", extra: "83%" },
{ key: "risk", name: "防灾韧性", value: 79, unit: "%", extra: "79%" }, { key: "risk", name: "防灾韧性", value: 79, unit: "%", extra: "79%" },
], ],
processing: [ "slaughter-processing": [
{ key: "license", name: "许可状态", value: 86, unit: "%", extra: "86%" }, { key: "license", name: "许可状态", value: 86, unit: "%", extra: "86%" },
{ key: "capacity", name: "产能利用", value: 77, unit: "%", extra: "77%" }, { key: "capacity", name: "产能利用", value: 77, unit: "%", extra: "77%" },
{ key: "cold-chain", name: "冷链能力", value: 72, unit: "%", extra: "72%" }, { key: "slaughter", name: "屠宰协同", value: 74, unit: "%", extra: "74%" },
{ key: "farmer-drive", name: "带动牧户", value: 84, unit: "%", extra: "84%" }, { key: "farmer-drive", name: "带动牧户", value: 84, unit: "%", extra: "84%" },
], ],
service: [ "farm-equipment": [
{ key: "insurance", name: "保险服务", value: 89, unit: "%", extra: "89%" }, { key: "machine", name: "农机装备", value: 82, unit: "%", extra: "82%" },
{ key: "quarantine", name: "检疫出证", value: 92, unit: "%", extra: "92%" },
{ key: "trade", name: "交易撮合", value: 78, unit: "%", extra: "78%" },
{ key: "supply-demand", name: "供需明细", value: 81, unit: "%", extra: "81%" },
],
equipment: [
{ key: "drone", name: "无人机识别", value: 86, unit: "%", extra: "86%" },
{ key: "iot", name: "物联感知", value: 79, unit: "%", extra: "79%" }, { key: "iot", name: "物联感知", value: 79, unit: "%", extra: "79%" },
{ key: "machine", name: "生产装备", value: 68, unit: "%", extra: "68%" }, { key: "maintain", name: "维护保养", value: 71, unit: "%", extra: "71%" },
{ key: "offline", name: "在线巡检", value: 74, unit: "%", extra: "74%" }, { key: "offline", name: "在线巡检", value: 74, unit: "%", extra: "74%" },
], ],
vr: [ "social-service": [
{ key: "resource-scene", name: "资源场景", value: 88, unit: "%", extra: "88%" }, { key: "finance", name: "金融服务", value: 78, unit: "%", extra: "78%" },
{ key: "base-scene", name: "基地场景", value: 82, unit: "%", extra: "82%" }, { key: "disease", name: "疫病防控", value: 92, unit: "%", extra: "92%" },
{ key: "processing-scene", name: "加工场景", value: 76, unit: "%", extra: "76%" }, { key: "bank", name: "银行服务", value: 76, unit: "%", extra: "76%" },
{ key: "display-scene", name: "展陈联动", value: 72, unit: "%", extra: "72%" }, { key: "insurance", name: "保险服务", value: 89, unit: "%", extra: "89%" },
{ key: "training", name: "技术培训", value: 81, unit: "%", extra: "81%" },
], ],
} }
@ -380,21 +381,13 @@ export function getStageRow(key) {
} }
export function getStageCapacityRows(stageKey) { export function getStageCapacityRows(stageKey) {
if (stageKey === "support") { if (stageKey === "family-pasture") {
return aggregateRowsByField(supportPointRows, "town", "capacity", "吨") return getFamilyPastureRows().slice(0, 7)
} }
if (stageKey === "processing") { if (stageKey === "artificial-grassland") {
return processingPointRows return getArtificialGrasslandRows().slice(0, 7)
.map((item) => ({
key: item.key,
name: item.name.replace("四川红原", "").replace("有限责任公司", ""),
value: item.capacity,
unit: item.capacityUnit,
extra: item.typeName,
}))
.sort((a, b) => b.value - a.value)
} }
if (stageKey === "breeding") { if (stageKey === "scale-breeding") {
return grasslandYakRows return grasslandYakRows
.map((item) => ({ .map((item) => ({
key: item.zoneName, key: item.zoneName,
@ -406,13 +399,27 @@ export function getStageCapacityRows(stageKey) {
.sort((a, b) => b.value - a.value) .sort((a, b) => b.value - a.value)
.slice(0, 7) .slice(0, 7)
} }
if (stageKey === "equipment") { if (stageKey === "forage-storage") {
return aggregateRowsByField(supportPointRows, "town", "capacity", "吨")
}
if (stageKey === "slaughter-processing") {
return processingPointRows
.map((item) => ({
key: item.key,
name: item.name.replace("四川红原", "").replace("有限责任公司", ""),
value: item.capacity,
unit: item.capacityUnit,
extra: item.typeName,
}))
.sort((a, b) => b.value - a.value)
}
if (stageKey === "farm-equipment") {
return townRecognitionRows return townRecognitionRows
.map((item) => ({ key: item.name, name: item.name, value: item.devices, unit: "套", extra: `${item.count}` })) .map((item) => ({ key: item.name, name: item.name, value: item.devices, unit: "套", extra: `${item.count}` }))
.sort((a, b) => b.value - a.value) .sort((a, b) => b.value - a.value)
.slice(0, 7) .slice(0, 7)
} }
if (stageKey === "service") { if (stageKey === "social-service") {
return tradeImpactRows.map((item) => ({ return tradeImpactRows.map((item) => ({
key: item.label, key: item.label,
name: item.label, name: item.label,
@ -421,26 +428,24 @@ export function getStageCapacityRows(stageKey) {
extra: `${item.yoy > 0 ? "+" : ""}${item.yoy}%`, extra: `${item.yoy > 0 ? "+" : ""}${item.yoy}%`,
})) }))
} }
if (stageKey === "vr") { return []
return chainFlowRows.map((item) => ({
key: item.key,
name: item.name,
value: item.value,
unit: item.unit,
extra: item.extra,
}))
}
return getInfrastructureTownRows().slice(0, 7)
} }
export function getStageTownRows(stageKey) { export function getStageTownRows(stageKey) {
if (stageKey === "support") { if (stageKey === "family-pasture") return getFamilyPastureRows()
if (stageKey === "artificial-grassland") return getArtificialGrasslandRows()
if (stageKey === "scale-breeding") {
return grasslandYakRows
.map((item) => ({ key: item.zoneName, name: item.zoneName, value: item.yakCount, unit: "头" }))
.sort((a, b) => b.value - a.value)
}
if (stageKey === "forage-storage") {
return aggregateRowsByField(supportPointRows, "town", "capacity", "吨") return aggregateRowsByField(supportPointRows, "town", "capacity", "吨")
} }
if (stageKey === "processing") { if (stageKey === "slaughter-processing") {
return aggregateRowsByField(processingPointRows, "town", "capacity", "吨") return aggregateRowsByField(processingPointRows, "town", "capacity", "吨")
} }
if (stageKey === "equipment") { if (stageKey === "farm-equipment") {
return townRecognitionRows.map((item) => ({ return townRecognitionRows.map((item) => ({
key: item.name, key: item.name,
name: item.name, name: item.name,
@ -448,7 +453,7 @@ export function getStageTownRows(stageKey) {
unit: "套", unit: "套",
})) }))
} }
if (stageKey === "service") { if (stageKey === "social-service") {
const totalYak = grasslandYakRows.reduce((sum, item) => sum + Number(item.yakCount || 0), 0) || 1 const totalYak = grasslandYakRows.reduce((sum, item) => sum + Number(item.yakCount || 0), 0) || 1
return grasslandYakRows return grasslandYakRows
.map((item) => ({ .map((item) => ({
@ -460,29 +465,44 @@ export function getStageTownRows(stageKey) {
})) }))
.sort((a, b) => b.value - a.value) .sort((a, b) => b.value - a.value)
} }
if (stageKey === "vr") { return []
return [
{ key: "vr-resource", name: "资源底图", value: 6, unit: "类", extra: "三维展示" },
{ key: "vr-base", name: "种养基地", value: homeCore.baseCount, unit: "个", extra: "基地漫游" },
{ key: "vr-processing", name: "加工企业", value: processingPointRows.length, unit: "家", extra: "工艺展示" },
{ key: "vr-service", name: "服务链路", value: 4, unit: "类", extra: "业务联动" },
]
}
if (stageKey === "infrastructure") return getInfrastructureTownRows()
return grasslandYakRows
.map((item) => ({ key: item.zoneName, name: item.zoneName, value: item.yakCount, unit: "头" }))
.sort((a, b) => b.value - a.value)
} }
export function getStageDetailRows(stageKey) { export function getStageDetailRows(stageKey) {
if (stageKey === "support") return supportPointRows.map(toDetailRow) if (stageKey === "family-pasture") {
if (stageKey === "processing") return processingPointRows.map(toDetailRow) return getFamilyPastureRows().map((item, index) => ({
if (stageKey === "breeding") { key: `family-pasture-${index + 1}`,
name: `${item.name}家庭生态牧场`,
stageKey,
typeName: "家庭生态牧场",
town: item.name,
capacity: item.value,
capacityUnit: item.unit,
status: "在线",
products: item.extra,
}))
}
if (stageKey === "artificial-grassland") {
return getArtificialGrasslandRows().map((item, index) => ({
key: `artificial-grassland-${index + 1}`,
name: `${item.name}人工草地`,
stageKey,
typeName: "人工草地",
town: item.name,
capacity: item.value,
capacityUnit: item.unit,
status: "管护中",
products: item.extra,
}))
}
if (stageKey === "forage-storage") return supportPointRows.map(toDetailRow)
if (stageKey === "slaughter-processing") return processingPointRows.map(toDetailRow)
if (stageKey === "scale-breeding") {
return grasslandYakRows.map((item, index) => ({ return grasslandYakRows.map((item, index) => ({
key: `base-${index + 1}`, key: `scale-base-${index + 1}`,
name: `${item.zoneName}生态种养基地`, name: `${item.zoneName}规模化养殖基地`,
stageKey, stageKey,
typeName: "种养基地", typeName: index % 3 === 0 ? "牦牛选育场" : "规模化养殖基地",
town: item.zoneName, town: item.zoneName,
capacity: item.yakCount, capacity: item.yakCount,
capacityUnit: "头", capacityUnit: "头",
@ -490,12 +510,12 @@ export function getStageDetailRows(stageKey) {
products: `${formatWanMu(item.grasslandArea)}万亩草地`, products: `${formatWanMu(item.grasslandArea)}万亩草地`,
})) }))
} }
if (stageKey === "equipment") { if (stageKey === "farm-equipment") {
return townRecognitionRows.map((item, index) => ({ return townRecognitionRows.map((item, index) => ({
key: `equipment-${index + 1}`, key: `equipment-${index + 1}`,
name: `${item.name}识别监测设`, name: `${item.name}农机装`,
stageKey, stageKey,
typeName: "机械设备", typeName: "农机装备",
town: item.name, town: item.name,
capacity: item.devices, capacity: item.devices,
capacityUnit: "套", capacityUnit: "套",
@ -503,33 +523,16 @@ export function getStageDetailRows(stageKey) {
products: `${item.count}头识别样本`, products: `${item.count}头识别样本`,
})) }))
} }
if (stageKey === "service") { if (stageKey === "social-service") {
return [ return [
{ key: "service-finance", name: "牦牛产业金融服务", stageKey, typeName: "金融", town: "全县", capacity: 38, capacityUnit: "项", status: "运行中", products: "授信、贴息、融资对接" },
{ key: "service-disease", name: "疫病防控服务", stageKey, typeName: "疫病防控", town: "全县", capacity: 9140, capacityUnit: "条", status: "运行中", products: "检疫、防疫、出证明细" },
{ key: "service-bank", name: "银行服务", stageKey, typeName: "银行", town: "全县", capacity: 12, capacityUnit: "家", status: "运行中", products: "贷款、账户、结算" },
{ key: "service-insurance", name: "牦牛保险服务", stageKey, typeName: "保险", town: "全县", capacity: serviceSubjectTotal, capacityUnit: "户", status: "运行中", products: "参保、理赔、风险事项" }, { key: "service-insurance", name: "牦牛保险服务", stageKey, typeName: "保险", town: "全县", capacity: serviceSubjectTotal, capacityUnit: "户", status: "运行中", products: "参保、理赔、风险事项" },
{ key: "service-quarantine", name: "检疫出证流通服务", stageKey, typeName: "检疫", town: "全县", capacity: 9140, capacityUnit: "条", status: "运行中", products: "A证/B证流通明细" }, { key: "service-training", name: "技术培训服务", stageKey, typeName: "技术培训", town: "全县", capacity: 46, capacityUnit: "场", status: "运行中", products: "养殖、防疫、草地管护培训" },
{ key: "service-trade", name: "活畜交易撮合服务", stageKey, typeName: "交易", town: "全县", capacity: tradeFlowTotal, capacityUnit: "头", status: "运行中", products: "交易总览、供需明细" },
{ key: "service-trace", name: "质量追溯与品牌服务", stageKey, typeName: "追溯", town: "全县", capacity: 6, capacityUnit: "类", status: "建设中", products: "批次、检疫、加工、销售" },
]
}
if (stageKey === "vr") {
return [
{ key: "vr-resource", name: "林草湿资源 VR 场景", stageKey, typeName: "产业VR", town: "全县", capacity: 6, capacityUnit: "类", status: "可展示", products: "草地、湿地、林地、牧场" },
{ key: "vr-base", name: "种养基地 VR 场景", stageKey, typeName: "产业VR", town: "全县", capacity: homeCore.baseCount, capacityUnit: "个", status: "可展示", products: "基地、物联感知、虫情监测" },
{ key: "vr-processing", name: "加工基地 VR 场景", stageKey, typeName: "产业VR", town: "邛溪镇", capacity: processingPointRows.length, capacityUnit: "家", status: "可展示", products: "乳品加工、肉品加工" },
{ key: "vr-chain", name: "全链路 VR 展陈", stageKey, typeName: "产业VR", town: "全县", capacity: industryStageDefs.length, capacityUnit: "段", status: "建设中", products: "生产、加工、流通、品牌" },
] ]
} }
return getInfrastructureTownRows().map((item, index) => ({ return []
key: `infra-${index + 1}`,
name: `${item.name}产业基础设施项目`,
stageKey,
typeName: "基础设施",
town: item.name,
capacity: item.value,
capacityUnit: item.unit,
status: "入库",
products: item.extra,
}))
} }
function toDetailRow(item) { function toDetailRow(item) {
@ -540,14 +543,29 @@ function toDetailRow(item) {
} }
} }
function getInfrastructureTownRows() { function getFamilyPastureRows() {
return grasslandYakRows.map((item, index) => ({ const totalYak = grasslandYakRows.reduce((sum, item) => sum + Number(item.yakCount || 0), 0) || 1
key: item.zoneName, return grasslandYakRows
name: item.zoneName, .map((item) => ({
value: infrastructureProjectDistribution[index] || 1, key: item.zoneName,
unit: "项", name: item.zoneName,
extra: `${formatWanMu(item.grasslandArea)}万亩`, value: Math.max(1, Math.round((item.yakCount / totalYak) * homeCore.herdsmanCount)),
})) unit: "户",
extra: `${item.yakCount}头牦牛`,
}))
.sort((a, b) => b.value - a.value)
}
function getArtificialGrasslandRows() {
return grasslandYakRows
.map((item) => ({
key: item.zoneName,
name: item.zoneName,
value: formatWanMu(item.grasslandArea),
unit: "万亩",
extra: `${item.yakCount}头牦牛`,
}))
.sort((a, b) => b.value - a.value)
} }
function aggregateRowsByField(rows, nameField, valueField, unit) { function aggregateRowsByField(rows, nameField, valueField, unit) {

@ -5,50 +5,22 @@
class="industry-map-scene" class="industry-map-scene"
:active-layer="industryMapLayer" :active-layer="industryMapLayer"
:map-ranking="mapRanking" :map-ranking="mapRanking"
@feature-info="handleMapFeatureInfo"
/> />
<div class="industry-map-scrim"></div> <div class="industry-map-scrim"></div>
<div v-if="!mapReady" class="industry-map-loading">3D地图加载中</div> <div v-if="!mapReady" class="industry-map-loading">3D地图加载中</div>
<div class="industry-left industry-side-stack"> <div v-if="selectedPoint" class="industry-point-detail" :style="pointDetailStyle">
<m-card class="industry-card left-card" title="全链核心指标" :width="sideCardWidth" :height="sideCardHeight"> <div class="industry-point-detail__head">
<business-panel-renderer :panel="overviewPanel" compact /> <span>{{ selectedPointTitle }}</span>
</m-card> <button type="button" title="关闭" @click="clearSelectedPoint">×</button>
</div>
<m-card class="industry-card left-card" title="产业节点结构" :width="sideCardWidth" :height="sideCardHeight"> <div class="industry-point-detail__name">{{ selectedPointName }}</div>
<business-panel-renderer :panel="stageStructurePanel" compact /> <div class="industry-point-detail__grid">
</m-card> <div v-for="row in selectedPointRows" :key="row.label" class="industry-point-detail__row">
<span>{{ row.label }}</span>
<m-card class="industry-card left-card" title="链路协同指数" :width="sideCardWidth" :height="sideCardHeight"> <strong>{{ row.value }}</strong>
<business-panel-renderer :panel="chainFlowPanel" compact /> </div>
</m-card>
<m-card class="industry-card left-card" :title="`${activeStage.name}能力排行`" :width="sideCardWidth" :height="sideCardHeight">
<business-panel-renderer :panel="capacityRankPanel" compact />
</m-card>
</div>
<div class="industry-right industry-side-stack">
<m-card class="industry-card right-card" title="加工储备产能" :width="sideCardWidth" :height="sideCardHeight">
<business-panel-renderer :panel="financialPanel" compact />
</m-card>
<m-card class="industry-card right-card" :title="`${activeStage.name}镇域覆盖`" :width="sideCardWidth" :height="sideCardHeight">
<business-panel-renderer :panel="townCoveragePanel" compact />
</m-card>
<m-card class="industry-card right-card" :title="`${activeStage.name}运行画像`" :width="sideCardWidth" :height="sideCardHeight">
<business-panel-renderer :panel="stageHealthPanel" compact />
</m-card>
<m-card class="industry-card right-card industry-list-card" :title="`${activeStage.name}明细`" :width="sideCardWidth" :height="sideCardHeight">
<business-panel-renderer :panel="detailTablePanel" compact @row-select="handleDetailSelect" />
</m-card>
</div>
<div class="industry-center">
<div class="industry-chain-title">
<span>牦牛全产业链</span>
<strong>{{ activeStage.name }}</strong>
</div> </div>
</div> </div>
@ -64,123 +36,100 @@
<script setup> <script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue" import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue"
import mCard from "@/components/mCard/index.vue"
import MapScene from "@/views/forestGrassWet/map.vue" import MapScene from "@/views/forestGrassWet/map.vue"
import BusinessPanelRenderer from "@/views/forestGrassWet/components/BusinessPanelRenderer.vue"
import TopicSwitcher from "@/views/forestGrassWet/components/TopicSwitcher.vue" import TopicSwitcher from "@/views/forestGrassWet/components/TopicSwitcher.vue"
import { Assets } from "@/views/forestGrassWet/assets" import { Assets } from "@/views/forestGrassWet/assets"
import { getLayerByKey } from "@/config/layers" import { getLayerByKey } from "@/config/layers"
import { import {
chainFlowRows,
getStageCapacityRows,
getStageDef, getStageDef,
getStageDetailRows,
getStageTownRows,
industryFinancialRows,
industryOverviewRows,
industryStageDefs, industryStageDefs,
industryStageRows,
industryTopStats,
stageHealthRows,
} from "./data" } from "./data"
const emit = defineEmits(["module-stats", "loading-progress", "map-ready"]) const emit = defineEmits(["module-stats", "loading-progress", "map-ready"])
const sideCardWidth = 398 const activeStageKey = ref("family-pasture")
const sideCardHeight = 190
const activeStageKey = ref("support")
const mapSceneRef = ref(null) const mapSceneRef = ref(null)
const mapReady = ref(false) const mapReady = ref(false)
const industryMapLayer = getLayerByKey("grassland", "grassland-distribution") const selectedPoint = ref(null)
const selectedPointScreen = ref({ x: 0, y: 0 })
let mapAssets = null let mapAssets = null
let destroyed = false let destroyed = false
const activeStage = computed(() => getStageDef(activeStageKey.value)) const activeStage = computed(() => getStageDef(activeStageKey.value))
const capacityRows = computed(() => getStageCapacityRows(activeStageKey.value)) const industryMapLayer = computed(() => {
const townRows = computed(() => getStageTownRows(activeStageKey.value)) const layer = getLayerByKey("yak-industry-chain", "yak-industry-points")
const detailRows = computed(() => getStageDetailRows(activeStageKey.value)) const legend = layer?.legends?.find((item) => item.match?.value === activeStageKey.value)
if (!layer) return layer
return {
...layer,
activeLegend: legend || {
key: `yak-industry-empty-${activeStageKey.value}`,
name: activeStage.value.name,
fillColor: activeStage.value.color,
strokeColor: activeStage.value.color,
match: { field: "stageKey", value: activeStageKey.value },
},
}
})
const stageSwitcherTopics = computed(() => const stageSwitcherTopics = computed(() =>
industryStageDefs.map((stage) => ({ industryStageDefs.map((stage) => ({
key: stage.key, key: stage.key,
name: stage.name, name: stage.name,
color: stage.color, color: stage.color,
width: stage.width,
})) }))
) )
const mapRanking = computed(() => ({ const mapRanking = computed(() => ({
showTownNames: true,
showBars: false,
showPointMarkers: false,
title: `${activeStage.value.name}排行`, title: `${activeStage.value.name}排行`,
metricLabel: activeStage.value.metricLabel, metricLabel: activeStage.value.metricLabel,
unit: townRows.value[0]?.unit || activeStage.value.unit, unit: activeStage.value.unit,
rows: townRows.value.slice(0, 8).map((item) => ({ rows: [],
name: item.name,
value: Number(item.value) || 0,
unit: item.unit || activeStage.value.unit,
})),
}))
const overviewPanel = computed(() => ({
key: "industry-overview",
type: "iconMetric",
rows: industryOverviewRows,
}))
const stageStructurePanel = computed(() => ({
key: "industry-stage-structure",
type: "bar",
variant: "distribution",
rows: industryStageRows.map((item) => ({
...item,
extra: item.note,
})),
}))
const chainFlowPanel = computed(() => ({
key: "industry-chain-flow",
type: "line",
rows: chainFlowRows,
}))
const capacityRankPanel = computed(() => ({
key: `industry-capacity-${activeStageKey.value}`,
type: "bar",
variant: "distribution",
rows: capacityRows.value.slice(0, 7),
}))
const financialPanel = computed(() => ({
key: "industry-financial",
type: "column",
variant: "slim",
rows: industryFinancialRows,
}))
const townCoveragePanel = computed(() => ({
key: `industry-town-${activeStageKey.value}`,
type: "bar",
variant: "distribution",
rows: townRows.value.slice(0, 7),
}))
const stageHealthPanel = computed(() => ({
key: `industry-health-${activeStageKey.value}`,
type: "heat",
rows: stageHealthRows[activeStageKey.value] || [],
}))
const detailTablePanel = computed(() => ({
key: `industry-detail-${activeStageKey.value}`,
type: "projectTable",
searchPlaceholder: "搜索主体 / 乡镇",
emptyText: "无匹配主体",
searchKeys: ["name", "typeName", "town", "address", "products", "status"],
columns: [
{ key: "name", label: "主体名称", field: "name", width: "34%" },
{ key: "type", label: "类型", field: "typeName", width: "18%" },
{ key: "town", label: "乡镇", field: "town", width: "16%" },
{ key: "capacity", label: "能力", field: "capacity", width: "18%", align: "right", strong: true, format: "number", unitField: "capacityUnit" },
{ key: "status", label: "状态", field: "status", width: "14%" },
],
rows: detailRows.value,
})) }))
onMounted(() => { const selectedPointRows = computed(() => {
emit("module-stats", { const point = selectedPoint.value
moduleKey: "yak-industry-chain", if (!point) return []
items: industryTopStats, return [
}) { label: "所属环节", value: formatDisplayText(point.stageName) },
initIndustryMap() { label: "点位类型", value: formatDisplayText(point.categoryName) },
{ label: "所在乡镇", value: formatDisplayText(point.town) },
{ label: "村/地址", value: formatDisplayText(point.village || point.address) },
{ label: "能力规模", value: formatCapacity(point) },
{ label: "联系人", value: formatDisplayText(point.contactName) },
{ label: "联系电话", value: formatDisplayText(point.contactPhone) },
{ label: "运行状态", value: formatDisplayText(point.status) },
].filter((row) => row.value !== undefined && row.value !== null && String(row.value).trim() !== "")
}) })
const selectedPointTitle = computed(() => formatDisplayText(selectedPoint.value?.categoryName || selectedPoint.value?.stageName))
const selectedPointName = computed(() => formatDisplayText(selectedPoint.value?.name))
const pointDetailStyle = computed(() => {
const panelWidth = 430
const panelHeight = 252
const margin = 24
const pointerGap = 28
const point = {
x: Math.max(margin, Math.min(1920 - margin, Number(selectedPointScreen.value.x || 0))),
y: Math.max(margin, Math.min(1080 - margin, Number(selectedPointScreen.value.y || 0))),
}
const placeLeft = point.x > 1320
const x = placeLeft
? Math.max(margin, point.x - panelWidth - pointerGap)
: Math.min(1920 - panelWidth - margin, point.x + pointerGap)
const y = Math.min(Math.max(point.y - panelHeight / 2, 128), 1080 - panelHeight - 116)
const arrowTop = Math.min(Math.max(point.y - y, 42), panelHeight - 38)
return {
left: `${x}px`,
top: `${y}px`,
"--point-arrow-top": `${arrowTop}px`,
"--point-arrow-side": placeLeft ? "100%" : "0px",
"--point-arrow-offset": placeLeft ? "-1px" : "-9px",
"--point-arrow-rotate": placeLeft ? "-45deg" : "135deg",
}
})
onMounted(initIndustryMap)
onBeforeUnmount(() => { onBeforeUnmount(() => {
destroyed = true destroyed = true
@ -189,6 +138,7 @@ onBeforeUnmount(() => {
function handleStageSelect(key) { function handleStageSelect(key) {
activeStageKey.value = key activeStageKey.value = key
clearSelectedPoint()
} }
function initIndustryMap() { function initIndustryMap() {
@ -228,15 +178,45 @@ function initIndustryMap() {
}) })
} }
function handleDetailSelect(row) { function handleMapFeatureInfo(payload = {}) {
const lng = Number(row?.longitude) const properties = payload.feature?.properties || {}
const lat = Number(row?.latitude) if (properties.stageKey) {
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return selectedPoint.value = properties
selectedPointScreen.value = normalizeFeatureScreen(payload.screen)
return
}
selectedPoint.value = null
}
function clearSelectedPoint() {
selectedPoint.value = null
const canvasMap = mapSceneRef.value?.canvasMap?.value || mapSceneRef.value?.canvasMap const canvasMap = mapSceneRef.value?.canvasMap?.value || mapSceneRef.value?.canvasMap
canvasMap?.flyToLngLat?.([lng, lat], { canvasMap?.clearSelectedFeature?.()
duration: 0.72, }
statusText: "已定位产业点位",
}) function formatCapacity(point) {
if (point.capacity === undefined || point.capacity === null || point.capacity === "") return ""
return `${point.capacity}${point.capacityUnit || ""}`
}
function formatDisplayText(value) {
return String(value ?? "")
.replace(/\bcxpt[-_./\s]*/gi, "")
.replace(/\s+/g, " ")
.trim()
}
function normalizeFeatureScreen(screen = {}) {
const root = document.querySelector("#large-screen")
const rect = root?.getBoundingClientRect?.()
const scaleX = rect?.width ? 1920 / rect.width : 1
const scaleY = rect?.height ? 1080 / rect.height : 1
const left = rect?.left || 0
const top = rect?.top || 0
return {
x: (Number(screen.x) - left) * scaleX,
y: (Number(screen.y) - top) * scaleY,
}
} }
</script> </script>
@ -263,7 +243,7 @@ function handleDetailSelect(row) {
z-index: 2; z-index: 2;
pointer-events: none; pointer-events: none;
background: background:
radial-gradient(circle at 50% 48%, rgba(4, 18, 28, 0.02) 0%, rgba(4, 18, 28, 0.08) 37%, rgba(2, 10, 17, 0.45) 78%, rgba(1, 7, 12, 0.72) 100%), radial-gradient(circle at 50% 48%, rgba(4, 18, 28, 0) 0%, rgba(4, 18, 28, 0.05) 37%, rgba(2, 10, 17, 0.34) 78%, rgba(1, 7, 12, 0.62) 100%),
linear-gradient(90deg, rgba(0, 12, 20, 0.42), transparent 26%, transparent 74%, rgba(0, 12, 20, 0.42)); linear-gradient(90deg, rgba(0, 12, 20, 0.42), transparent 26%, transparent 74%, rgba(0, 12, 20, 0.42));
} }
@ -277,92 +257,92 @@ function handleDetailSelect(row) {
font-size: 18px; font-size: 18px;
} }
.industry-side-stack { .industry-point-detail {
position: absolute; position: absolute;
top: 176px; z-index: 7;
z-index: 5; width: 430px;
display: flex; padding: 18px 20px 20px;
flex-direction: column; border: 1px solid rgba(48, 220, 255, 0.42);
gap: 12px; background:
width: 398px; linear-gradient(180deg, rgba(12, 54, 70, 0.86), rgba(5, 24, 37, 0.9)),
radial-gradient(circle at 20% 0%, rgba(36, 246, 196, 0.18), transparent 46%);
box-shadow:
0 0 24px rgba(48, 220, 255, 0.18),
inset 0 0 18px rgba(48, 220, 255, 0.08);
color: #efffff;
pointer-events: all; pointer-events: all;
} }
.industry-left { .industry-point-detail::after {
left: 32px; content: "";
transform: perspective(900px) rotateY(4deg); position: absolute;
} left: var(--point-arrow-side, 0);
top: var(--point-arrow-top, 50%);
.industry-right { width: 16px;
right: 32px; height: 16px;
transform: perspective(900px) rotateY(-4deg); border-right: 1px solid rgba(48, 220, 255, 0.42);
border-bottom: 1px solid rgba(48, 220, 255, 0.42);
background: rgba(5, 24, 37, 0.9);
transform: translate(var(--point-arrow-offset, -9px), -50%) rotate(var(--point-arrow-rotate, 135deg));
} }
.industry-card { .industry-point-detail__head {
flex: 0 0 auto; display: flex;
align-items: center;
.m-card-bd-content { justify-content: space-between;
top: 40px; gap: 12px;
bottom: 8px; color: #ffe06b;
color: #f3feff; font-size: 16px;
} font-weight: 700;
.m-card-bd-bg { button {
filter: brightness(1.08) saturate(1.05); width: 26px;
} height: 26px;
border: 1px solid rgba(48, 220, 255, 0.36);
.m-card-bd-bg::after { border-radius: 0;
content: ""; background: rgba(6, 28, 43, 0.72);
position: absolute; color: rgba(239, 255, 255, 0.88);
inset: 4px 5px 6px; cursor: pointer;
z-index: 0; font-size: 20px;
pointer-events: none; line-height: 20px;
border: 1px solid rgba(48, 220, 255, 0.14);
background:
linear-gradient(180deg, rgba(15, 69, 88, 0.16), rgba(6, 28, 43, 0.13)),
linear-gradient(90deg, rgba(48, 220, 255, 0.09), transparent 42%, rgba(255, 224, 107, 0.05));
box-shadow:
inset 0 0 0 1px rgba(48, 220, 255, 0.05),
inset 0 12px 28px rgba(48, 220, 255, 0.04);
} }
} }
.yak-industry-chain .m-card-hd-title { .industry-point-detail__name {
color: #ffffff !important; margin-top: 12px;
background: none !important; color: #ffffff;
-webkit-text-fill-color: #ffffff !important; font-size: 22px;
text-shadow: none !important; font-weight: 800;
line-height: 1.35;
} }
.industry-center { .industry-point-detail__grid {
position: absolute; display: grid;
left: 50%; gap: 8px;
top: 690px; margin-top: 14px;
z-index: 4;
width: 820px;
transform: translateX(-50%);
pointer-events: none;
} }
.industry-chain-title { .industry-point-detail__row {
display: flex; display: grid;
align-items: baseline; grid-template-columns: 88px minmax(0, 1fr);
justify-content: center; gap: 12px;
gap: 14px; min-height: 32px;
margin-bottom: 13px; align-items: center;
text-shadow: 0 0 18px rgba(48, 220, 255, 0.28); border-bottom: 1px solid rgba(48, 220, 255, 0.12);
span { span {
color: rgba(224, 252, 255, 0.9); color: rgba(196, 237, 244, 0.74);
font-size: 20px; font-size: 14px;
font-weight: 700;
} }
strong { strong {
color: #30dcff; overflow: hidden;
font-size: 32px; color: rgba(246, 255, 255, 0.95);
font-family: AlimamaShuHeiTi, AlibabaPuHuiTi, sans-serif; font-size: 15px;
letter-spacing: 0; font-weight: 700;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
} }
} }
@ -378,15 +358,25 @@ function handleDetailSelect(row) {
pointer-events: all; pointer-events: all;
.topic-switcher { .topic-switcher {
width: 940px; width: 1060px;
} }
.topic-switcher-item { .topic-switcher-item {
width: 112px; width: var(--topic-item-width, 112px);
} }
.topic-switcher-item .topic-name { .topic-switcher-item .topic-name {
font-size: 15px; font-size: 14px;
}
.topic-switcher-item .topic-title {
font-size: 14px;
line-height: 16px;
}
.topic-switcher-item .topic-name small {
font-size: 10px;
line-height: 11px;
} }
} }
</style> </style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -24,6 +24,7 @@ export default ({ mode }) => {
const openApiTarget = env.VITE_OPEN_API_PROXY_TARGET || 'http://127.0.0.1:9999'; const openApiTarget = env.VITE_OPEN_API_PROXY_TARGET || 'http://127.0.0.1:9999';
const jsyApiTarget = env.VITE_JSY_API_PROXY_TARGET || openApiTarget; const jsyApiTarget = env.VITE_JSY_API_PROXY_TARGET || openApiTarget;
const geoserverTarget = env.VITE_GEOSERVER_PROXY_TARGET || 'http://127.0.0.1:8080'; const geoserverTarget = env.VITE_GEOSERVER_PROXY_TARGET || 'http://127.0.0.1:8080';
const ezvizTarget = env.VITE_EZVIZ_PROXY_TARGET || 'https://open.ys7.com';
return defineConfig({ return defineConfig({
base: './', //env.VITE_APP_BASE_URL, // 动态改变base值 base: './', //env.VITE_APP_BASE_URL, // 动态改变base值
assetsInclude: [ assetsInclude: [
@ -113,6 +114,11 @@ export default ({ mode }) => {
target: geoserverTarget, target: geoserverTarget,
changeOrigin: true, changeOrigin: true,
}, },
'/ezviz': {
target: ezvizTarget,
changeOrigin: true,
rewrite: (proxyPath) => proxyPath.replace(/^\/ezviz/, ''),
},
}, },
}, },
envDir: '.', envDir: '.',

Loading…
Cancel
Save