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. 656
      src/views/forestGrassWet/map.js
  72. 150
      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. 633
      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. 362
      src/views/yakIndustryChain/data.js
  85. 400
      src/views/yakIndustryChain/index.vue
  86. 5431
      src/views/yakIndustryChain/yakIndustryClassifiedData.json
  87. 794
      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};

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" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>红原县数字畜牧业监管平台</title>
<script type="module" crossorigin src="./assets/index-1782661552655.js"></script>
<link rel="stylesheet" href="./assets/index-b8a77371-balabala-1782661552655.css">
<script type="module" crossorigin src="./assets/index-1782919616283.js"></script>
<link rel="stylesheet" href="./assets/index-47d6f37a-balabala-1782919616283.css">
</head>
<body>

7
package-lock.json generated

@ -12,6 +12,7 @@
"d3-geo": "^3.1.0",
"delaunator": "^5.0.0",
"echarts": "^5.5.0",
"ezuikit-js": "^0.7.2",
"gsap": "^3.12.2",
"normalize.css": "^8.0.1",
"point-in-polygon": "^1.1.0",
@ -1058,6 +1059,12 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"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": {
"version": "0.6.10",
"resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.6.10.tgz",

@ -13,6 +13,7 @@
"d3-geo": "^3.1.0",
"delaunator": "^5.0.0",
"echarts": "^5.5.0",
"ezuikit-js": "^0.7.2",
"gsap": "^3.12.2",
"normalize.css": "^8.0.1",
"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 = {
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 = {
...forestGrassWetLayers,
...ecologicalProtectionLayers,
...yakIndustryChainLayers,
}
const topicRegistry = [...forestGrassWetTopics, ...ecologicalProtectionTopics]
const topicRegistry = [...forestGrassWetTopics, ...ecologicalProtectionTopics, ...yakIndustryChainTopics]
export function getTopicConfig(topicKey) {
return topicRegistry.find((topic) => topic.key === topicKey) || forestGrassWetTopics[0]

@ -17,12 +17,6 @@ export const platformModules = [
bottomLabel: "生态保护",
status: "ready",
},
{
key: "grass-livestock-balance",
label: "草畜平衡",
bottomLabel: "草畜平衡",
status: "ready",
},
{
key: "yak-management",
label: "牦牛管理",
@ -47,6 +41,12 @@ export const platformModules = [
bottomLabel: "智慧管理",
status: "ready",
},
{
key: "grass-livestock-balance",
label: "草畜平衡",
bottomLabel: "草畜平衡",
status: "ready",
},
]
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 COOKIE_PATHS = ["/", "/screen2", "/screen2/"]
function encodeRedirect(path) {
return encodeURIComponent(path || "/platform")
@ -13,7 +14,15 @@ export function getAuthorizationToken() {
}
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") {
@ -29,6 +38,7 @@ export function redirectToLogin(redirect) {
}
export async function login(username, password) {
clearAuthorizationToken()
const response = await fetch("/login", {
method: "POST",
credentials: "include",

@ -1,6 +1,6 @@
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"
function joinBase(base, path) {
@ -8,6 +8,11 @@ function joinBase(base, 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) {
const failed =
response?.success === false ||
@ -37,20 +42,60 @@ function normalizeRows(data) {
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 } = {}) {
const params = {
areaCode,
pasturesId,
}
const requests = [
() => get(OPEN_API_BASE, "/ai/balance/advice", params, { timeout: 18000 }),
() => get(JSY_API_BASE, "/ai/livestockBalance", params, { timeout: 18000 }),
() => get(OPEN_API_BASE, "/ai/balance/advice", params, { timeout: 18000 }),
]
let lastError
for (const request of requests) {
try {
return await request()
const result = await request()
if (hasMeaningfulBalanceResult(result)) return result
} catch (error) {
lastError = error
}
@ -67,14 +112,33 @@ export async function queryBalanceTargets(question) {
let lastError
for (const request of requests) {
try {
return normalizeRows(await request())
const rows = normalizeRows(await request())
if (rows.length) return rows
} catch (error) {
lastError = error
}
}
throw lastError || new Error("草畜平衡对象识别接口不可用")
if (lastError) throw lastError
return []
}
export function getAreaChildren(areaCode) {
return get(JSY_API_BASE, "/area/children", { areaCode }, { timeout: 12000 }).then(normalizeRows)
export async function getAreaChildren(areaCode) {
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)
}
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 = {}) {
const {
headCount = 4,
@ -925,18 +961,18 @@ function buildWetlandDefaultPanels({
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 minTownPanel = buildPanel("wetland-min-town", "优势小类乡镇分布", topRows(minTownRows, 6), "bar", { span: 1, variant: "distribution" })
const leftPanels = [
const leftPanels = diversifyPanelColumn([
townAreaPanel,
typePanel,
levelStructurePanel,
minTypePanel,
]
const rightPanels = [
])
const rightPanels = diversifyPanelColumn([
edibleYieldPanel,
yieldPanel,
muYieldPanel,
yieldCapacityPanel,
]
])
return {
leftPanels,
rightPanels,
@ -978,20 +1014,20 @@ function buildWetlandLayerPanels(panels = {}) {
} = panels
return {
"wetland-distribution": {
leftPanels: [townAreaPanel, typePanel, levelStructurePanel, minTypePanel].filter(Boolean),
rightPanels: [edibleYieldPanel, yieldPanel, muYieldPanel, yieldCapacityPanel].filter(Boolean),
leftPanels: diversifyPanelColumn([townAreaPanel, typePanel, levelStructurePanel, minTypePanel].filter(Boolean)),
rightPanels: diversifyPanelColumn([edibleYieldPanel, yieldPanel, muYieldPanel, yieldCapacityPanel].filter(Boolean)),
},
"wetland-level": {
leftPanels: [levelStructurePanel, levelPanel, levelTownPanel, typePanel].filter(Boolean),
rightPanels: [edibleYieldPanel, yieldPanel, muYieldPanel, yieldCapacityPanel].filter(Boolean),
leftPanels: diversifyPanelColumn([levelStructurePanel, levelPanel, levelTownPanel, typePanel].filter(Boolean)),
rightPanels: diversifyPanelColumn([edibleYieldPanel, yieldPanel, muYieldPanel, yieldCapacityPanel].filter(Boolean)),
},
"wetland-type": {
leftPanels: [typePanel, typeTownPanel, minTypePanel, minTownPanel].filter(Boolean),
rightPanels: [edibleYieldPanel, yieldPanel, edibleMuPanel, yieldCapacityPanel].filter(Boolean),
leftPanels: diversifyPanelColumn([typePanel, typeTownPanel, minTypePanel, minTownPanel].filter(Boolean)),
rightPanels: diversifyPanelColumn([edibleYieldPanel, yieldPanel, edibleMuPanel, yieldCapacityPanel].filter(Boolean)),
},
"wetland-yield": {
leftPanels: [townAreaPanel, typePanel, levelStructurePanel, minTypePanel].filter(Boolean),
rightPanels: [edibleYieldPanel, yieldPanel, muYieldPanel, yieldCapacityPanel].filter(Boolean),
leftPanels: diversifyPanelColumn([townAreaPanel, typePanel, levelStructurePanel, minTypePanel].filter(Boolean)),
rightPanels: diversifyPanelColumn([edibleYieldPanel, yieldPanel, muYieldPanel, yieldCapacityPanel].filter(Boolean)),
},
}
}
@ -1002,8 +1038,8 @@ function buildGrasslandLayerPanels(panels = {}) {
rightPanels = [],
} = panels
const stablePanels = {
leftPanels: leftPanels.filter(Boolean),
rightPanels: rightPanels.filter(Boolean),
leftPanels: diversifyPanelColumn(leftPanels.filter(Boolean)),
rightPanels: diversifyPanelColumn(rightPanels.filter(Boolean)),
}
return {
"grassland-distribution": stablePanels,
@ -1051,14 +1087,14 @@ function buildWoodlandDefaultPanels({
const qualityLevelPanel = buildPanel("woodland-quality-level", "林地质量等级", withPercentExtra(qualityLevelRows), "donut", { 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 leftPanels = fillPanelSlots(
const leftPanels = diversifyPanelColumn(fillPanelSlots(
[townPanel, typePanel, protectionPanel, ownershipPanel],
[scalePanel, ownerPanel, forestTypePanel, qualityPanel]
)
const rightPanels = fillPanelSlots(
))
const rightPanels = diversifyPanelColumn(fillPanelSlots(
[qualityPanel, originPanel, agePanel, canopyPanel],
[scalePanel, ownerPanel, forestTypePanel, regionPanel, townConcentrationPanel, typeConcentrationPanel]
)
))
return {
leftPanels,
rightPanels,
@ -1108,24 +1144,24 @@ function buildWoodlandLayerPanels(panels = {}) {
} = panels
return {
"woodland-distribution": {
leftPanels: fillPanelSlots([townPanel, typePanel, protectionPanel, ownershipPanel], [scalePanel, forestTypePanel, ownerPanel]),
rightPanels: fillPanelSlots([qualityPanel, originPanel, agePanel, canopyPanel], [scalePanel, ownerPanel, regionPanel, townConcentrationPanel, typeConcentrationPanel, qualityLevelPanel]),
leftPanels: diversifyPanelColumn(fillPanelSlots([townPanel, typePanel, protectionPanel, ownershipPanel], [scalePanel, forestTypePanel, ownerPanel])),
rightPanels: diversifyPanelColumn(fillPanelSlots([qualityPanel, originPanel, agePanel, canopyPanel], [scalePanel, ownerPanel, regionPanel, townConcentrationPanel, typeConcentrationPanel, qualityLevelPanel])),
},
"woodland-protection": {
leftPanels: fillPanelSlots([protectionPanel, forestTypePanel, manageLevelPanel, townPanel], [typePanel, ownershipPanel, scalePanel]),
rightPanels: fillPanelSlots([regionPanel, qualityPanel, originPanel, ownershipPanel], [ownerPanel, typePanel, scalePanel]),
leftPanels: diversifyPanelColumn(fillPanelSlots([protectionPanel, forestTypePanel, manageLevelPanel, townPanel], [typePanel, ownershipPanel, scalePanel])),
rightPanels: diversifyPanelColumn(fillPanelSlots([regionPanel, qualityPanel, originPanel, ownershipPanel], [ownerPanel, typePanel, scalePanel])),
},
"woodland-type": {
leftPanels: fillPanelSlots([typePanel, townPanel, typeTownPanel, scalePanel], [protectionPanel, ownershipPanel, forestTypePanel]),
rightPanels: fillPanelSlots([qualityPanel, originPanel, protectionPanel, ownershipPanel], [agePanel, canopyPanel, ownerPanel, typeConcentrationPanel]),
leftPanels: diversifyPanelColumn(fillPanelSlots([typePanel, townPanel, typeTownPanel, scalePanel], [protectionPanel, ownershipPanel, forestTypePanel])),
rightPanels: diversifyPanelColumn(fillPanelSlots([qualityPanel, originPanel, protectionPanel, ownershipPanel], [agePanel, canopyPanel, ownerPanel, typeConcentrationPanel])),
},
"woodland-quality": {
leftPanels: fillPanelSlots([qualityPanel, originPanel, agePanel, canopyPanel], [qualityLevelPanel, scalePanel, typePanel]),
rightPanels: fillPanelSlots([townPanel, typePanel, protectionPanel, ownershipPanel], [forestTypePanel, ownerPanel, scalePanel]),
leftPanels: diversifyPanelColumn(fillPanelSlots([qualityPanel, originPanel, agePanel, canopyPanel], [qualityLevelPanel, scalePanel, typePanel])),
rightPanels: diversifyPanelColumn(fillPanelSlots([townPanel, typePanel, protectionPanel, ownershipPanel], [forestTypePanel, ownerPanel, scalePanel])),
},
"woodland-ownership": {
leftPanels: fillPanelSlots([ownershipPanel, ownerPanel, townPanel, typePanel], [scalePanel, protectionPanel, qualityPanel]),
rightPanels: fillPanelSlots([protectionPanel, qualityPanel, originPanel, scalePanel], [forestTypePanel, agePanel, canopyPanel]),
leftPanels: diversifyPanelColumn(fillPanelSlots([ownershipPanel, ownerPanel, townPanel, typePanel], [scalePanel, protectionPanel, qualityPanel])),
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 levelPanel = buildPanel("grassland-level-proportion", "草原级占比", grassLevelRows, "donut", { span: 1 })
const foragePanel = buildPanel("grassland-forage-capacity", "饲草供给", forageRows, "bar", { span: 1, variant: "capacity" })
const leftPanels = [
const leftPanels = diversifyPanelColumn([
areaPanel,
typePanel,
degradationPanel,
levelPanel,
]
const rightPanels = [
])
const rightPanels = diversifyPanelColumn([
edibleYieldPanel,
productivityPanel,
healthPanel,
foragePanel,
]
])
return {
core,
leftPanels,

@ -74,6 +74,10 @@ export function getPastureDetailsById(id) {
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() {
return get("/api/pasturesList").then(normalizeRows)
}
@ -85,16 +89,73 @@ export async function getPastureProfile() {
getPastureAge(),
getPastureOnline(),
getPastureList(),
getMuhuList("", 1, 80),
getMuhuList("", 1, 300),
])
const pastureRows = pastureList.status === "fulfilled" ? pastureList.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 {
core: core.status === "fulfilled" ? core.value : [],
distribution: distribution.status === "fulfilled" ? distribution.value : [],
age: age.status === "fulfilled" ? age.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;
margin: 0;
}
* {
box-sizing: border-box;
}
html,
body,
#app {
@ -10,3 +13,14 @@ body,
height: 100%;
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,
})
const parkVillageMapPoints = {
洞拉村: [102.82, 33.22],
滚塘村: [102.9, 33.23],
让里村: [102.99, 33.23],
茸塔玛村: [103.08, 33.2],
唐日村: [102.5, 33.11],
德香村: [102.63, 33.12],
日干村: [102.72, 33.08],
色永村: [103.12, 33.04],
}
const parkVillageMapRows = parkYakVillageRows
.map((item) => {
const point = parkVillageMapPoints[item.name]
if (!point) return null
return {
...item,
lng: point[0],
lat: point[1],
value: item.value,
rawValue: item.value,
metricLabel: "牦牛数量",
displayUnit: item.unit,
displayMode: "park-village",
}
})
.filter(Boolean)
const parkEdibleGrassVillageRows = buildParkVillageRows({
sourceKey: "edibleGrasslandWanTon",
weightKey: "edible",
@ -508,15 +536,14 @@ export const parkPanels = {
{
key: "park-yak-village",
title: "牦牛分布村级排行",
type: "column",
variant: "pin",
type: "bar",
variant: "distribution",
rows: parkYakVillageRows.slice(0, 8),
},
{
key: "park-edible-grass-village",
title: "可食草地资源分布",
type: "column",
variant: "slim",
type: "split",
rows: parkEdibleGrassVillageRows.slice(0, 8),
},
{
@ -609,7 +636,8 @@ export const restorationPanels = {
{
key: "restoration-town-area",
title: "乡镇治理面积分布",
type: "line",
type: "bar",
variant: "distribution",
rows: validTownRows.map((item) => ({
key: item.name,
name: item.name,
@ -730,13 +758,13 @@ export function getEcologicalMapRanking(viewKey) {
}
return {
status: "success",
title: "国家公园牦牛分布",
title: "国家公园覆盖村",
scope: "village",
labelMode: "village",
hideTownNames: false,
displayMode: "park-village",
metricLabel: "牦牛数量",
unit: "头",
rows: parkTownRows.map((item) => ({
name: item.name,
value: item.yakCount,
unit: "头",
})),
rows: parkVillageMapRows,
}
}

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

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

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

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

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

@ -10,12 +10,18 @@
:key="topic.key"
class="topic-switcher-item"
: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"
@pointerup="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>
</div>
<div class="topic-switcher-arrow is-reverse">
@ -96,7 +102,7 @@ function handleTopicClick(event, topicKey) {
&-item {
position: relative;
width: 112px;
width: var(--topic-item-width, 112px);
height: 32px;
padding: 0;
border: 0;
@ -118,10 +124,16 @@ function handleTopicClick(event, topicKey) {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 100%;
padding: 0 5px;
text-align: center;
white-space: nowrap;
}
.topic-name {
.topic-name,
.topic-title,
.topic-name small {
padding-top: 0;
font-size: 16px;
font-weight: 700;
@ -130,5 +142,19 @@ function handleTopicClick(event, topicKey) {
-webkit-background-clip: text;
-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>

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

File diff suppressed because it is too large Load Diff

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

@ -1,52 +1,3 @@
export default [
{
name: "邛溪镇",
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],
},
]
import { hongyuanTownshipLabelPoints } from "@/config/townshipBoundaries"
export default hongyuanTownshipLabelPoints.map((item) => ({ ...item }))

@ -1,72 +1,3 @@
export default [
{
name: "色地镇",
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,
},
];
import { hongyuanTownshipRankingPoints } from "@/config/townshipBoundaries"
export default hongyuanTownshipRankingPoints.map((item) => ({ ...item }))

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

@ -1,52 +1,3 @@
export default [
{
name: "邛溪镇",
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],
},
]
import { hongyuanTownshipLabelPoints } from "@/config/townshipBoundaries"
export default hongyuanTownshipLabelPoints.map((item) => ({ ...item }))

@ -1,72 +1,3 @@
export default [
{
name: "色地镇",
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,
},
];
import { hongyuanTownshipRankingPoints } from "@/config/townshipBoundaries"
export default hongyuanTownshipRankingPoints.map((item) => ({ ...item }))

@ -61,6 +61,23 @@ export const fieldMap = {
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 = {
邛溪镇: "513233100",
刷经寺镇: "513233101",
@ -143,7 +160,7 @@ function calculateBearingCapacity(areaRange) {
return Math.round((Number(areaRange) || 0) * BALANCE_CALCULATION_PARAMS.grasslandCapacityPerMu)
}
function buildBalanceRow({
export function buildBalanceRow({
key,
type,
name,
@ -153,6 +170,10 @@ function buildBalanceRow({
yakNumber,
location,
township,
hasBoundary = false,
boundaryCount = 0,
grasslands = [],
source,
pressureFactor = 1,
}) {
const bearingCapacity = calculateBearingCapacity(areaRange)
@ -175,6 +196,10 @@ function buildBalanceRow({
year: "2026",
location,
township,
hasBoundary: Boolean(hasBoundary),
boundaryCount: Number(boundaryCount) || 0,
grasslands,
source,
}
}
@ -211,7 +236,7 @@ export const countyBalanceRow = {
year: "2026",
}
export const balancePastureRows = pastureListRows.map((item) => buildBalanceRow({
export const balancePastureRows = sortBalancePastureRows(pastureListRows.map((item) => buildBalanceRow({
key: item.id || item.key || item.name,
type: "pasture",
name: item.name,
@ -221,7 +246,9 @@ export const balancePastureRows = pastureListRows.map((item) => buildBalanceRow(
yakNumber: item.yakCount,
location: item.location,
township: item.township,
}))
hasBoundary: item.hasBoundary || item.canLocate || hasGrasslandGeometry(item),
source: item,
})))
export const balanceFallbackRows = [
countyBalanceRow,
@ -230,11 +257,29 @@ export const balanceFallbackRows = [
]
export function normalizeBalanceResult(source = {}, fallback = {}) {
const areaRange = toNumber(pickValue(source, fieldMap.areaRange), fallback.areaRange ?? 0)
const yakNumber = toNumber(pickValue(source, fieldMap.yakNumber), fallback.yakNumber ?? 0)
const bearingCapacity = toNumber(pickValue(source, fieldMap.bearingCapacity), fallback.bearingCapacity ?? calculateBearingCapacity(areaRange))
const livestockCapacity = toNumber(pickValue(source, fieldMap.livestockCapacity), fallback.livestockCapacity ?? calculateLivestockCapacity(yakNumber))
const rawAreaRange = pickValue(source, fieldMap.areaRange)
const rawYakNumber = pickValue(source, fieldMap.yakNumber)
const rawBearingCapacity = pickValue(source, fieldMap.bearingCapacity)
const rawLivestockCapacity = pickValue(source, fieldMap.livestockCapacity)
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
? round(directRate, 1)
: bearingCapacity
@ -257,7 +302,155 @@ export function normalizeBalanceResult(source = {}, fallback = {}) {
level: pickValue(source, fieldMap.level, risk.label),
advice: pickValue(source, fieldMap.advice, fallback.advice || risk.actions.join(";")),
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 = {}) {

@ -31,7 +31,6 @@
@click="selectBalance(item)"
>
<span>{{ item.name }}</span>
<small>{{ getTargetSubText(item) }}</small>
</button>
</div>
@ -147,12 +146,12 @@
<div class="balance-bar-row">
<span>理论承载力</span>
<div><i :style="{ width: `${activeDerived.bearingPercent}%` }"></i></div>
<strong>{{ formatNumber(activeDerived.bearing) }}<em>羊单位</em></strong>
<strong>{{ formatNumber(activeDerived.bearing) }}</strong>
</div>
<div class="balance-bar-row is-livestock">
<span>实际载畜量</span>
<div><i :style="{ width: `${activeDerived.livestockPercent}%` }"></i></div>
<strong>{{ formatNumber(activeDerived.livestock) }}<em>羊单位</em></strong>
<strong>{{ formatNumber(activeDerived.livestock) }}</strong>
</div>
</div>
@ -181,10 +180,10 @@
<span>{{ step.index }}</span>
<div>
<strong>{{ step.name }}</strong>
<p>{{ step.formula }}</p>
<small>{{ step.expression }}</small>
<p>{{ step.plan }}</p>
<small>{{ step.desc }}</small>
</div>
<em>{{ step.result }}{{ step.unit ? ` ${step.unit}` : "" }}</em>
<em>{{ step.status }}</em>
</div>
</div>
@ -219,15 +218,22 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from "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 {
BALANCE_CALCULATION_PARAMS,
balanceFallbackRows,
buildBalanceRow,
buildBalancePastureRows,
deriveBalance,
formatNumber,
formatPercent,
hasPastureBoundary,
normalizeBalanceResult,
pickValue,
sortBalancePastureRows,
splitAdvice,
toNumber,
} from "./data"
const emit = defineEmits(["loading-progress", "map-ready"])
@ -236,6 +242,7 @@ const selectorCardWidth = 326
const selectorCardHeight = 720
const sheetCardWidth = 1310
const mainCardHeight = 820
const MIN_ANALYSIS_DURATION = 4200
const activeScope = ref("county")
const activeKey = ref("")
@ -243,18 +250,30 @@ const analysisState = ref("idle")
const analysisProgress = ref(0)
const processStageIndex = ref(0)
const apiResults = ref({})
const fallbackPastureRows = sortBalancePastureRows(balanceFallbackRows.filter((item) => item.type === "pasture"))
const villageTargetRows = ref([])
const pastureTargetRows = ref(fallbackPastureRows)
let destroyed = false
let requestSerial = 0
let analysisTimer = null
let villageLoadSerial = 0
let pastureLoadSerial = 0
let activeAnalysisRequest = null
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 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(() => {
if (activeScope.value === "town") return townRows.value
if (activeScope.value === "village") return villageRows.value
if (activeScope.value === "pasture") return pastureRows.value
return countyRows.value
})
@ -264,11 +283,12 @@ const analysisRunning = computed(() => analysisState.value === "running")
const analysisDone = computed(() => hasSelection.value && analysisState.value === "done")
const activeDerived = computed(() => deriveBalance(activeBalance.value || {}))
const scopeOptions = computed(() => [
{ key: "county", name: "县域" },
{ key: "town", name: "乡镇" },
{ key: "county", name: "县" },
{ key: "town", name: "镇" },
{ key: "village", name: "村" },
{ key: "pasture", name: "牧户" },
])
const scopeLabel = computed(() => scopeOptions.value.find((item) => item.key === activeScope.value)?.name || "县")
const scopeLabel = computed(() => scopeOptions.value.find((item) => item.key === activeScope.value)?.name || "县")
const subjectMeta = computed(() => {
if (!activeBalance.value) return "等待选择核算对象"
if (activeBalance.value.pasturesId) return `牧户编号 ${activeBalance.value.pasturesId}`
@ -293,14 +313,14 @@ const waitTitle = computed(() => {
return `${activeBalance.value.name} 待开始分析`
})
const waitDescription = computed(() => {
if (!hasSelection.value) return "选择县镇或牧户后,系统才会进入草地承载力核算。"
if (!hasSelection.value) return "选择县、镇、村或牧户后,系统才会进入草地承载力核算。"
if (analysisRunning.value) return "正在读取核算参数、执行公式计算,并生成结论与调控建议。"
return "点击左侧开始分析后,再展示结果、分析过程、结论和建议。"
})
const conclusionSummary = computed(() => {
const derived = activeDerived.value
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 usage = Math.max(0, Math.min(130, Number(activeDerived.value.usageRate) || 0))
@ -308,99 +328,80 @@ const usageAngle = computed(() => {
})
const calculationSteps = computed(() => {
const row = activeBalance.value || {}
const derived = activeDerived.value
const areaWanMu = (derived.areaRange || 0) / 10000
const deltaText = formatSignedNumber(derived.delta)
return [
{
key: "bearing",
key: "productivity",
index: "01",
name: "理论承载力",
formula: "核算草地面积 × 草地承载系数",
expression: `${formatNumber(areaWanMu, { maximumFractionDigits: 2 })}万亩 × 10000 × ${formatFixed(derived.bearingCoefficient, 4)}`,
result: formatNumber(derived.bearing),
unit: "羊单位",
name: "草地生产力核算",
plan: "核算草地面积与单位面积产草能力",
desc: "结合核算对象草地范围,确定可参与草畜平衡计算的草地生产基础。",
status: "已完成",
},
{
key: "livestock",
key: "forage",
index: "02",
name: "实际载畜量",
formula: "牦牛数量 × 折羊单位系数 × 压力修正系数",
expression: `${formatNumber(row.yakNumber)}头 × ${formatFixed(BALANCE_CALCULATION_PARAMS.yakToSheepUnit, 1)} × ${formatFixed(derived.pressureFactor, 2)}`,
result: formatNumber(derived.livestock),
unit: "羊单位",
name: "可利用草量折算",
plan: "扣除不可食与生态保留部分",
desc: "按草地利用系数、放牧季节和保护要求,折算为可供放牧利用的草量。",
status: "已完成",
},
{
key: "delta",
key: "livestock",
index: "03",
name: "承载差额",
formula: "实际载畜量 - 理论承载力",
expression: `${formatNumber(derived.livestock)} - ${formatNumber(derived.bearing)}`,
result: deltaText,
unit: "羊单位",
name: "载畜量折算",
plan: "将牦牛存栏折算为标准载畜压力",
desc: "依据牦牛数量、折算系数和压力修正参数,形成实际载畜量口径。",
status: "已完成",
},
{
key: "rate",
key: "balance",
index: "04",
name: "超载率",
formula: "承载差额 ÷ 理论承载力 × 100%",
expression: `${deltaText} ÷ ${formatNumber(derived.bearing)} × 100%`,
result: formatPercent(derived.overloadRate),
unit: "",
name: "平衡判定",
plan: derived.delta > 0 ? "比对承载力并形成超载调控方案" : "比对承载力并形成稳态保持方案",
desc: "根据承载利用率和超载率判定平衡状态,输出治理建议。",
status: derived.risk.shortLabel || "完成",
},
]
})
const analysisProcessRows = computed(() => {
const row = activeBalance.value || {}
const derived = activeDerived.value
const areaWanMu = (derived.areaRange || 0) / 10000
const deltaText = formatSignedNumber(derived.delta)
return [
{
key: "target",
key: "productivity",
index: "01",
name: "锁定核算对象",
desc: "确认行政区划与对象口径",
pending: "等待对象确认",
expression: `${activeBalance.value?.name || "--"} / ${subjectMeta.value}`,
result: "已锁定",
name: "草地生产力核算",
desc: "核算草地面积与产草能力",
pending: "等待草地参数",
expression: "正在汇总草地范围、面积和生产力参数",
result: "已核算",
},
{
key: "params",
key: "forage",
index: "02",
name: "读取基础参数",
desc: "读取草地面积、牦牛数量与折算系数",
pending: "等待参数读取",
expression: `草地 ${formatNumber(areaWanMu, { maximumFractionDigits: 2 })} 万亩,牦牛 ${formatNumber(row.yakNumber)} 头,折羊系数 ${formatFixed(BALANCE_CALCULATION_PARAMS.yakToSheepUnit, 1)}`,
result: "已读取",
name: "可利用草量折算",
desc: "折算可供放牧利用草量",
pending: "等待草量折算",
expression: "正在扣除不可食与生态保留部分",
result: "已折算",
},
{
key: "bearing",
key: "livestock",
index: "03",
name: "计算理论承载力",
desc: "代入草地承载系数",
pending: "等待承载力计算",
expression: `${formatNumber(areaWanMu, { maximumFractionDigits: 2 })}万亩 × 10000 × ${formatFixed(derived.bearingCoefficient, 4)} = ${formatNumber(derived.bearing)} 羊单位`,
result: formatNumber(derived.bearing),
name: "载畜量折算",
desc: "形成实际载畜压力口径",
pending: "等待载畜折算",
expression: "正在按牦牛存栏和折算系数换算载畜压力",
result: "已折算",
},
{
key: "livestock",
key: "balance",
index: "04",
name: "折算实际载畜量",
desc: "按牦牛数量与压力系数折算",
pending: "等待载畜量折算",
expression: `${formatNumber(row.yakNumber)}头 × ${formatFixed(BALANCE_CALCULATION_PARAMS.yakToSheepUnit, 1)} × ${formatFixed(derived.pressureFactor, 2)} = ${formatNumber(derived.livestock)} 羊单位`,
result: formatNumber(derived.livestock),
},
{
key: "conclusion",
index: "05",
name: "生成平衡结论",
desc: "比较差额并输出调控建议",
pending: "等待结论生成",
expression: `${deltaText} ÷ ${formatNumber(derived.bearing)} × 100% = ${formatPercent(derived.overloadRate)}`,
name: "平衡判定",
desc: "生成结论与调控方向",
pending: "等待平衡判定",
expression: derived.delta > 0 ? "正在判定超载风险并生成调控方向" : "正在判定平衡状态并生成监测方向",
result: derived.risk.label,
},
]
@ -409,31 +410,31 @@ const currentProcessStep = computed(() => analysisProcessRows.value[Math.min(pro
const workflowRows = computed(() => [
{
key: "select",
key: "productivity",
index: "01",
name: "对象锁定",
desc: hasSelection.value ? activeBalance.value.name : "等待选择",
name: "生产力核算",
desc: analysisDone.value ? "草地生产力已核算" : hasSelection.value ? "等待核算" : "等待选择",
state: hasSelection.value ? "done" : "active",
},
{
key: "params",
key: "forage",
index: "02",
name: "参数读取",
desc: analysisDone.value ? "草地与牦牛参数已确认" : "分析后读取",
name: "草量折算",
desc: analysisDone.value ? "可利用草量已折算" : "分析后折算",
state: analysisRunning.value ? "active" : analysisDone.value ? "done" : hasSelection.value ? "pending" : "disabled",
},
{
key: "formula",
key: "livestock",
index: "03",
name: "模型推演",
desc: analysisDone.value ? "承载模型已完成" : "等待公式计算",
name: "载畜折算",
desc: analysisDone.value ? "载畜压力已换算" : "等待折算",
state: analysisRunning.value ? "active" : analysisDone.value ? "done" : "disabled",
},
{
key: "result",
key: "balance",
index: "04",
name: "结论生成",
desc: analysisDone.value ? activeDerived.value.risk.label : "等待建议生成",
name: "平衡判定",
desc: analysisDone.value ? activeDerived.value.risk.label : "等待判定",
state: analysisDone.value ? "done" : "disabled",
},
])
@ -443,11 +444,15 @@ const activeAdviceRows = computed(() => analysisDone.value ? splitAdvice(activeB
onMounted(() => {
emit("loading-progress", { progress: 100 })
emit("map-ready")
loadVillageTargets()
loadPastureTargets()
})
onBeforeUnmount(() => {
destroyed = true
requestSerial += 1
villageLoadSerial += 1
pastureLoadSerial += 1
clearAnalysisTimer()
})
@ -457,6 +462,7 @@ watch(
analysisState.value = "idle"
analysisProgress.value = 0
processStageIndex.value = 0
activeAnalysisRequest = null
clearAnalysisTimer()
}
)
@ -469,7 +475,7 @@ function handleScopeChange(scopeKey) {
function selectBalance(row) {
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
analysisState.value = "idle"
}
@ -480,19 +486,30 @@ function startAnalysis() {
analysisProgress.value = 0
processStageIndex.value = 0
clearAnalysisTimer()
refreshActiveBalance()
const startedAt = Date.now()
const analysisRequest = refreshActiveBalance()
activeAnalysisRequest = analysisRequest
analysisTimer = window.setInterval(() => {
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(
analysisProcessRows.value.length - 1,
Math.floor(analysisProgress.value / 22)
Math.floor(analysisProgress.value / 25)
)
if (analysisProgress.value >= 100) {
}, 140)
analysisRequest.finally(async () => {
const remaining = MIN_ANALYSIS_DURATION - (Date.now() - startedAt)
if (remaining > 0) {
await new Promise((resolve) => window.setTimeout(resolve, remaining))
}
if (destroyed || analysisState.value !== "running" || activeAnalysisRequest !== analysisRequest) return
activeAnalysisRequest = null
analysisProgress.value = 100
processStageIndex.value = analysisProcessRows.value.length - 1
clearAnalysisTimer()
analysisState.value = "done"
}
}, 90)
})
}
async function refreshActiveBalance() {
@ -515,10 +532,232 @@ async function refreshActiveBalance() {
}
}
function getTargetSubText(row) {
if (row.type === "pasture") return row.township || "牧户"
if (row.type === "town") return "乡镇对象"
return "县域对象"
async function loadVillageTargets() {
const loadId = ++villageLoadSerial
const towns = townRows.value
.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) {
@ -883,8 +1122,8 @@ function getProcessStepState(index) {
.balance-scope-tabs {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 6px;
button {
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(90deg, rgba(126, 251, 246, 0.04), transparent);
color: rgba(223, 250, 255, 0.76);
font-size: 13px;
font-size: 12px;
cursor: pointer;
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;
@ -945,9 +1184,8 @@ function getProcessStepState(index) {
button {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) 64px;
grid-template-columns: minmax(0, 1fr);
align-items: center;
column-gap: 10px;
min-width: 0;
height: 42px;
padding: 0 12px 0 15px;
@ -2481,4 +2719,191 @@ function getProcessStepState(index) {
.balance-core-caption {
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>

@ -22,37 +22,37 @@ const grasslandYieldWanTon = 297.29
const woodlandPublicWanMu = 159.33
export const homeTopStats = [
{
key: "forest-grass-wet-area",
zh: "林草湿总量",
name: "林草湿总量",
value: totalForestGrassWetWanMu,
unit: "万亩",
decimals: 2,
},
{
key: "yak-count",
zh: "牦牛存栏",
name: "牦牛存栏",
zh: "牦牛数量",
name: "牦牛数量",
value: totalTownYak,
unit: "头",
decimals: 0,
},
{
key: "trade-flow",
zh: "交易流通",
name: "交易流通",
value: totalTradeHead,
unit: "",
decimals: 0,
key: "grassland-area",
zh: "草地面积",
name: "草地面积",
value: grasslandAreaWanMu,
unit: "万亩",
decimals: 2,
},
{
key: "support-capacity",
zh: "产业载体",
name: "产业载体",
value: homeCore.baseCount + homeCore.projectCount,
unit: "个",
decimals: 0,
key: "wetland-area",
zh: "湿地面积",
name: "湿地面积",
value: wetlandAreaWanMu,
unit: "万亩",
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>
</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>
</template>
@ -54,10 +66,12 @@
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue"
import mCard from "@/components/mCard/index.vue"
import MapScene from "@/views/forestGrassWet/map.vue"
import LayerPanel from "@/views/forestGrassWet/components/LayerPanel.vue"
import BusinessPanelRenderer from "@/views/forestGrassWet/components/BusinessPanelRenderer.vue"
import { Assets } from "@/views/forestGrassWet/assets"
import { getLayerByKey } from "@/config/layers"
import { getPastureGrasslandsById, getPastureProfile } from "@/services/pastureApi"
import { getHongyuanTownshipNameByCode, hongyuanTownshipsGeoJsonRaw, normalizeHongyuanTownName } from "@/config/townshipBoundaries"
import { getPastureGrasslandsById, getPastureProfile, getPasturesDetailsById } from "@/services/pastureApi"
import {
pastureAgeRows,
pastureCoreRows,
@ -81,12 +95,109 @@ const mapSceneRef = ref(null)
const mapReady = ref(false)
const apiProfile = ref(pastureFallbackProfile)
const pastureRows = ref(pastureListRows)
const pastureMapLayer = getLayerByKey("grassland", "grassland-distribution")
const activeLayerKey = ref("")
const selectedLegendKey = ref("")
const pastureTopStatNames = ["牧户数量", "户均牦牛头数", "户均草地面积", "联户数量"]
let mapAssets = null
let mapAssetsReady = 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 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)
@ -98,6 +209,10 @@ const mapRanking = computed(() => ({
title: "牧户分布排行",
metricLabel: "牧户",
unit: "户",
hideMapLabels: false,
hideTownNames: false,
showBars: false,
showPointMarkers: false,
rows: (townRows.value.length ? townRows.value : pastureMapRows).slice(0, 8).map((item) => ({
name: item.name,
value: Number(item.value) || 0,
@ -113,8 +228,7 @@ const townPanel = computed(() => ({
}))
const grasslandPanel = computed(() => ({
key: "pasture-grassland-area",
type: "bar",
variant: "distribution",
type: "heat",
rows: grasslandRows.value,
}))
const seasonPanel = computed(() => ({
@ -178,11 +292,143 @@ async function loadPastureProfile() {
}
emitPastureTopStats()
const normalizedList = normalizePastureRows(profile.list)
pastureRows.value = normalizedList.length ? normalizedList : pastureListRows
pastureRows.value = normalizedList.length ? normalizedList : sortPastureRows(pastureListRows)
enrichPastureRowsWithGrasslands(pastureRows.value)
} catch (error) {
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) {
@ -421,10 +667,10 @@ function normalizePastureRows(rows = []) {
.map((item, index) => {
const name = pickValue(item, ["name", "pasturesName", "pastures_name", "muhuName", "muhu_name", "pastureName", "pasture_name", "herdsmanName"])
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 grasslandArea = pickPastureArea(item) || sumPastureGrasslandArea(grasslands)
const realId = getRealPastureId(item)
const township = getPastureTownship(item, grasslands) || getPastureFallbackTownship(item, name, realId)
const normalized = {
...item,
key: item.key || item.id || item.pasturesId || item.pastures_id || `${name}-${index}`,
@ -432,8 +678,8 @@ function normalizePastureRows(rows = []) {
realPastureId: realId,
name,
township: township || "--",
grasslandArea: Number(grasslandArea) || 0,
acreage: Number(grasslandArea) || 0,
grasslandArea,
acreage: grasslandArea,
grasslands,
}
normalized.canLocate = Boolean(realId || grasslands.some(hasGrasslandGeometry) || hasGrasslandGeometry(item))
@ -444,6 +690,67 @@ function normalizePastureRows(rows = []) {
return normalized
})
.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 = []) {
@ -500,7 +807,7 @@ function normalizeRowGrasslands(item = {}) {
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"])
const wkt = pickValue(item, ["wkt", "geo", "geom", "geometryWkt", "geometry_wkt", "shape", "location"])
if (wkt || item?.geometry) {
rows.push({
...item,
@ -519,11 +826,11 @@ function hasGrasslandGeometry(item = {}) {
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"]))
return Boolean(pickValue(item, ["wkt", "geo", "geom", "geometryWkt", "geometry_wkt", "shape", "location"]))
}
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
}
@ -542,6 +849,224 @@ function pickValue(item = {}, keys = []) {
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>
<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 {
color: #ffffff !important;
background: none !important;

@ -98,9 +98,6 @@
>
<template #right>
<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>
@ -118,7 +115,7 @@
</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="{ 'is-four': totalView.length > 3 }"
>
@ -223,11 +220,8 @@ const activeModuleKey = ref("home")
const activeEcologyView = ref("national-park")
const renderedModules = ref({ home: true })
const moduleTopStats = ref({})
const currentDate = ref("")
const currentTime = ref("")
const loadingVisible = ref(true)
const loadingProgress = ref(0)
let timeTimer = null
let screenAutofit = null
let loadingHideStarted = false
const loadingStartedAt = Date.now()
@ -284,12 +278,9 @@ onMounted(() => {
resize: true,
})
resetRootScroll()
updateCurrentTime()
timeTimer = window.setInterval(updateCurrentTime, 1000)
})
onBeforeUnmount(() => {
if (timeTimer) window.clearInterval(timeTimer)
screenAutofit?.off?.("#large-screen")
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() {
window.scrollTo(0, 0)
document.documentElement.scrollTop = 0
@ -538,18 +522,6 @@ function handleLogout() {
align-items: center;
gap: 12px;
pointer-events: auto;
.m-header-date {
display: flex;
align-items: center;
gap: 12px;
}
span {
color: #c4f3fe;
font-size: 14px;
white-space: nowrap;
}
}
.logout-button {

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

@ -5,50 +5,22 @@
class="industry-map-scene"
:active-layer="industryMapLayer"
:map-ranking="mapRanking"
@feature-info="handleMapFeatureInfo"
/>
<div class="industry-map-scrim"></div>
<div v-if="!mapReady" class="industry-map-loading">3D地图加载中</div>
<div class="industry-left industry-side-stack">
<m-card class="industry-card left-card" title="全链核心指标" :width="sideCardWidth" :height="sideCardHeight">
<business-panel-renderer :panel="overviewPanel" compact />
</m-card>
<m-card class="industry-card left-card" title="产业节点结构" :width="sideCardWidth" :height="sideCardHeight">
<business-panel-renderer :panel="stageStructurePanel" compact />
</m-card>
<m-card class="industry-card left-card" title="链路协同指数" :width="sideCardWidth" :height="sideCardHeight">
<business-panel-renderer :panel="chainFlowPanel" compact />
</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 v-if="selectedPoint" class="industry-point-detail" :style="pointDetailStyle">
<div class="industry-point-detail__head">
<span>{{ selectedPointTitle }}</span>
<button type="button" title="关闭" @click="clearSelectedPoint">×</button>
</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 class="industry-point-detail__name">{{ selectedPointName }}</div>
<div class="industry-point-detail__grid">
<div v-for="row in selectedPointRows" :key="row.label" class="industry-point-detail__row">
<span>{{ row.label }}</span>
<strong>{{ row.value }}</strong>
</div>
<div class="industry-center">
<div class="industry-chain-title">
<span>牦牛全产业链</span>
<strong>{{ activeStage.name }}</strong>
</div>
</div>
@ -64,124 +36,101 @@
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue"
import mCard from "@/components/mCard/index.vue"
import MapScene from "@/views/forestGrassWet/map.vue"
import BusinessPanelRenderer from "@/views/forestGrassWet/components/BusinessPanelRenderer.vue"
import TopicSwitcher from "@/views/forestGrassWet/components/TopicSwitcher.vue"
import { Assets } from "@/views/forestGrassWet/assets"
import { getLayerByKey } from "@/config/layers"
import {
chainFlowRows,
getStageCapacityRows,
getStageDef,
getStageDetailRows,
getStageTownRows,
industryFinancialRows,
industryOverviewRows,
industryStageDefs,
industryStageRows,
industryTopStats,
stageHealthRows,
} from "./data"
const emit = defineEmits(["module-stats", "loading-progress", "map-ready"])
const sideCardWidth = 398
const sideCardHeight = 190
const activeStageKey = ref("support")
const activeStageKey = ref("family-pasture")
const mapSceneRef = ref(null)
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 destroyed = false
const activeStage = computed(() => getStageDef(activeStageKey.value))
const capacityRows = computed(() => getStageCapacityRows(activeStageKey.value))
const townRows = computed(() => getStageTownRows(activeStageKey.value))
const detailRows = computed(() => getStageDetailRows(activeStageKey.value))
const industryMapLayer = computed(() => {
const layer = getLayerByKey("yak-industry-chain", "yak-industry-points")
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(() =>
industryStageDefs.map((stage) => ({
key: stage.key,
name: stage.name,
color: stage.color,
width: stage.width,
}))
)
const mapRanking = computed(() => ({
showTownNames: true,
showBars: false,
showPointMarkers: false,
title: `${activeStage.value.name}排行`,
metricLabel: activeStage.value.metricLabel,
unit: townRows.value[0]?.unit || activeStage.value.unit,
rows: townRows.value.slice(0, 8).map((item) => ({
name: item.name,
value: Number(item.value) || 0,
unit: item.unit || activeStage.value.unit,
})),
unit: activeStage.value.unit,
rows: [],
}))
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(() => {
emit("module-stats", {
moduleKey: "yak-industry-chain",
items: industryTopStats,
})
initIndustryMap()
const selectedPointRows = computed(() => {
const point = selectedPoint.value
if (!point) return []
return [
{ label: "所属环节", value: formatDisplayText(point.stageName) },
{ 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(() => {
destroyed = true
mapSceneRef.value?.destroyMap?.()
@ -189,6 +138,7 @@ onBeforeUnmount(() => {
function handleStageSelect(key) {
activeStageKey.value = key
clearSelectedPoint()
}
function initIndustryMap() {
@ -228,15 +178,45 @@ function initIndustryMap() {
})
}
function handleDetailSelect(row) {
const lng = Number(row?.longitude)
const lat = Number(row?.latitude)
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
function handleMapFeatureInfo(payload = {}) {
const properties = payload.feature?.properties || {}
if (properties.stageKey) {
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
canvasMap?.flyToLngLat?.([lng, lat], {
duration: 0.72,
statusText: "已定位产业点位",
})
canvasMap?.clearSelectedFeature?.()
}
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>
@ -263,7 +243,7 @@ function handleDetailSelect(row) {
z-index: 2;
pointer-events: none;
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));
}
@ -277,92 +257,92 @@ function handleDetailSelect(row) {
font-size: 18px;
}
.industry-side-stack {
.industry-point-detail {
position: absolute;
top: 176px;
z-index: 5;
display: flex;
flex-direction: column;
gap: 12px;
width: 398px;
z-index: 7;
width: 430px;
padding: 18px 20px 20px;
border: 1px solid rgba(48, 220, 255, 0.42);
background:
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;
}
.industry-left {
left: 32px;
transform: perspective(900px) rotateY(4deg);
}
.industry-right {
right: 32px;
transform: perspective(900px) rotateY(-4deg);
.industry-point-detail::after {
content: "";
position: absolute;
left: var(--point-arrow-side, 0);
top: var(--point-arrow-top, 50%);
width: 16px;
height: 16px;
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 {
flex: 0 0 auto;
.m-card-bd-content {
top: 40px;
bottom: 8px;
color: #f3feff;
}
.m-card-bd-bg {
filter: brightness(1.08) saturate(1.05);
}
.industry-point-detail__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: #ffe06b;
font-size: 16px;
font-weight: 700;
.m-card-bd-bg::after {
content: "";
position: absolute;
inset: 4px 5px 6px;
z-index: 0;
pointer-events: none;
border: 1px solid rgba(48, 220, 255, 0.14);
background:
linear-gradient(180deg, rgba(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);
button {
width: 26px;
height: 26px;
border: 1px solid rgba(48, 220, 255, 0.36);
border-radius: 0;
background: rgba(6, 28, 43, 0.72);
color: rgba(239, 255, 255, 0.88);
cursor: pointer;
font-size: 20px;
line-height: 20px;
}
}
.yak-industry-chain .m-card-hd-title {
color: #ffffff !important;
background: none !important;
-webkit-text-fill-color: #ffffff !important;
text-shadow: none !important;
.industry-point-detail__name {
margin-top: 12px;
color: #ffffff;
font-size: 22px;
font-weight: 800;
line-height: 1.35;
}
.industry-center {
position: absolute;
left: 50%;
top: 690px;
z-index: 4;
width: 820px;
transform: translateX(-50%);
pointer-events: none;
.industry-point-detail__grid {
display: grid;
gap: 8px;
margin-top: 14px;
}
.industry-chain-title {
display: flex;
align-items: baseline;
justify-content: center;
gap: 14px;
margin-bottom: 13px;
text-shadow: 0 0 18px rgba(48, 220, 255, 0.28);
.industry-point-detail__row {
display: grid;
grid-template-columns: 88px minmax(0, 1fr);
gap: 12px;
min-height: 32px;
align-items: center;
border-bottom: 1px solid rgba(48, 220, 255, 0.12);
span {
color: rgba(224, 252, 255, 0.9);
font-size: 20px;
font-weight: 700;
color: rgba(196, 237, 244, 0.74);
font-size: 14px;
}
strong {
color: #30dcff;
font-size: 32px;
font-family: AlimamaShuHeiTi, AlibabaPuHuiTi, sans-serif;
letter-spacing: 0;
overflow: hidden;
color: rgba(246, 255, 255, 0.95);
font-size: 15px;
font-weight: 700;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
}
@ -378,15 +358,25 @@ function handleDetailSelect(row) {
pointer-events: all;
.topic-switcher {
width: 940px;
width: 1060px;
}
.topic-switcher-item {
width: 112px;
width: var(--topic-item-width, 112px);
}
.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>

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 jsyApiTarget = env.VITE_JSY_API_PROXY_TARGET || openApiTarget;
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({
base: './', //env.VITE_APP_BASE_URL, // 动态改变base值
assetsInclude: [
@ -113,6 +114,11 @@ export default ({ mode }) => {
target: geoserverTarget,
changeOrigin: true,
},
'/ezviz': {
target: ezvizTarget,
changeOrigin: true,
rewrite: (proxyPath) => proxyPath.replace(/^\/ezviz/, ''),
},
},
},
envDir: '.',

Loading…
Cancel
Save