feat: add yak map controls

V2.0
M先生 1 month ago
parent 321bf4a139
commit de2625b27c
  1. 0
      docs/assets/AlibabaPuHuiTi-14435b65-balabala-1783264824576.woff2
  2. 0
      docs/assets/AlimamaShuHeiTi-Bold-6401fd0d-balabala-1783264824576.ttf
  3. 0
      docs/assets/AlimamaShuHeiTi-Bold-a95023f2-balabala-1783264824576.woff
  4. 0
      docs/assets/AlimamaShuHeiTi-Bold-fa4a258d-balabala-1783264824576.woff2
  5. 0
      docs/assets/SmartChartRenderer-8fd37aa6-balabala-1783264824576.css
  6. 2
      docs/assets/SmartChartRenderer-fe68a3bf-1783264824576.js
  7. BIN
      docs/assets/area-1144cf4a-balabala-1783244194656.png
  8. BIN
      docs/assets/area-b57940eb-balabala-1783264824576.png
  9. 0
      docs/assets/bg-3acb94e3-balabala-1783264824576.png
  10. 0
      docs/assets/bottom-menu-arrow-big-180b7071-balabala-1783264824576.svg
  11. 0
      docs/assets/bottom-menu-arrow-small-19304179-balabala-1783264824576.svg
  12. 0
      docs/assets/bottom-menu-bg-b465793d-balabala-1783264824576.png
  13. 0
      docs/assets/bottom-menu-btn-ee5c8064-balabala-1783264824576.png
  14. 0
      docs/assets/bottom-menu-btn-hover-df33e514-balabala-1783264824576.png
  15. BIN
      docs/assets/certificate-355c8c0e-balabala-1783244194656.png
  16. 6
      docs/assets/chartTheme-3259b6f8-1783264824576.js
  17. 0
      docs/assets/chartTheme-812dab15-balabala-1783264824576.css
  18. 0
      docs/assets/chinaBlurLine-1a2cbc0f-balabala-1783264824576.png
  19. 0
      docs/assets/content-bottom-left-arrow-3a8a61f3-balabala-1783264824576.svg
  20. 0
      docs/assets/content-bottom-right-arrow-a3c5270d-balabala-1783264824576.svg
  21. 0
      docs/assets/content-middle-line-162bc0d5-balabala-1783264824576.svg
  22. BIN
      docs/assets/device-74309daf-balabala-1783244194656.png
  23. 0
      docs/assets/focus_bg-b6803e2d-balabala-1783264824576.png
  24. 0
      docs/assets/focus_move_bg-cf41417a-balabala-1783264824576.png
  25. BIN
      docs/assets/forest-a956b24c-balabala-1783244194656.png
  26. BIN
      docs/assets/grass-basic-b26948b5-balabala-1783244194656.png
  27. BIN
      docs/assets/grass-general-9f6c02e6-balabala-1783244194656.png
  28. 0
      docs/assets/header-bg-65b38012-balabala-1783264824576.png
  29. 0
      docs/assets/huiguang-3c5dfda5-balabala-1783264824576.png
  30. 0
      docs/assets/icon1-6f7c222f-balabala-1783264824576.png
  31. 0
      docs/assets/icon2-4f77ccec-balabala-1783264824576.png
  32. 66
      docs/assets/index-1783264824576.js
  33. 2
      docs/assets/index-34a7150e-balabala-1783264824576.css
  34. 0
      docs/assets/index-47d6f37a-balabala-1783264824576.css
  35. 330
      docs/assets/index-636a3fb1-1783264824576.js
  36. 1
      docs/assets/index-7323850c-balabala-1783244194656.css
  37. 1
      docs/assets/index-8cfc31f2-balabala-1783264824576.css
  38. 0
      docs/assets/index-8d752fbe-balabala-1783264824576.css
  39. 2
      docs/assets/index-9516394f-1783264824576.js
  40. 2
      docs/assets/index-fa14e23f-1783264824576.js
  41. BIN
      docs/assets/leaf-61e84605-balabala-1783244194656.png
  42. BIN
      docs/assets/leaf-7be9efef-balabala-1783264824576.png
  43. 0
      docs/assets/left-kuang-5d6bd1ae-balabala-1783264824576.svg
  44. BIN
      docs/assets/length-32ee20b8-balabala-1783244194656.png
  45. 0
      docs/assets/live-scene-1-786cdd0a-balabala-1783264824576.png
  46. 0
      docs/assets/menu-btn-91a961cf-balabala-1783264824576.png
  47. 0
      docs/assets/menu-btn-hover-62eba0bb-balabala-1783264824576.png
  48. 0
      docs/assets/ocean-bg-8c848b8f-balabala-1783264824576.png
  49. BIN
      docs/assets/park-b1db3900-balabala-1783244194656.png
  50. BIN
      docs/assets/park-ee0533e0-balabala-1783264824576.png
  51. 0
      docs/assets/pathLine2-d4134d4b-balabala-1783264824576.png
  52. BIN
      docs/assets/project-da9e3256-balabala-1783244194656.png
  53. BIN
      docs/assets/rate-bfe355f6-balabala-1783244194656.png
  54. 0
      docs/assets/right-kuang-b1937caf-balabala-1783264824576.svg
  55. 0
      docs/assets/ring2-a1b91808-balabala-1783264824576.png
  56. 0
      docs/assets/ring3-fe0f60d7-balabala-1783264824576.png
  57. 0
      docs/assets/ring4-f2e324d6-balabala-1783264824576.png
  58. BIN
      docs/assets/risk-2e5b3a69-balabala-1783244194656.png
  59. 0
      docs/assets/rotationBorder1-2ce8e0ef-balabala-1783264824576.png
  60. 0
      docs/assets/rotationBorder2-7496b968-balabala-1783264824576.png
  61. 0
      docs/assets/saoguang-ccf84a88-balabala-1783264824576.svg
  62. BIN
      docs/assets/stack-7a426057-balabala-1783264824576.png
  63. BIN
      docs/assets/stack-8cf8bdb6-balabala-1783244194656.png
  64. 0
      docs/assets/title-zs1-c15a5ee8-balabala-1783264824576.svg
  65. BIN
      docs/assets/town-0200c4bb-balabala-1783244194656.png
  66. BIN
      docs/assets/town-f8c0c9cc-balabala-1783264824576.png
  67. BIN
      docs/assets/wetland-d67b919e-balabala-1783244194656.png
  68. BIN
      docs/assets/yield-66e9af79-balabala-1783264824576.png
  69. BIN
      docs/assets/yield-b201b10e-balabala-1783244194656.png
  70. 4
      docs/index.html
  71. BIN
      src/assets/images/top-stat-icons/area.png
  72. BIN
      src/assets/images/top-stat-icons/certificate.png
  73. BIN
      src/assets/images/top-stat-icons/device.png
  74. BIN
      src/assets/images/top-stat-icons/forest.png
  75. BIN
      src/assets/images/top-stat-icons/grass-basic.png
  76. BIN
      src/assets/images/top-stat-icons/grass-general.png
  77. BIN
      src/assets/images/top-stat-icons/leaf.png
  78. BIN
      src/assets/images/top-stat-icons/length.png
  79. BIN
      src/assets/images/top-stat-icons/park.png
  80. BIN
      src/assets/images/top-stat-icons/project.png
  81. BIN
      src/assets/images/top-stat-icons/rate.png
  82. BIN
      src/assets/images/top-stat-icons/risk.png
  83. BIN
      src/assets/images/top-stat-icons/stack.png
  84. BIN
      src/assets/images/top-stat-icons/town.png
  85. BIN
      src/assets/images/top-stat-icons/wetland.png
  86. BIN
      src/assets/images/top-stat-icons/yield.png
  87. 8
      src/components/mCountCard/index.vue
  88. 58
      src/config/layers.js
  89. 4
      src/services/balanceApi.js
  90. 37
      src/services/homeVideoApi.js
  91. 62
      src/services/smartApi.js
  92. 114
      src/views/ecologicalProtection/data.js
  93. 1
      src/views/ecologicalProtection/index.vue
  94. 560
      src/views/forestGrassWet/components/BusinessPanelRenderer.vue
  95. 15
      src/views/forestGrassWet/components/BusinessStatList.vue
  96. 1
      src/views/forestGrassWet/index.vue
  97. 121
      src/views/forestGrassWet/map.js
  98. 6
      src/views/forestGrassWet/map.vue
  99. 8
      src/views/gdMap/map.js
  100. 124
      src/views/grassLivestockBalance/data.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,2 +1,2 @@
var D=Object.defineProperty,W=Object.defineProperties;var H=Object.getOwnPropertyDescriptors;var z=Object.getOwnPropertySymbols;var F=Object.prototype.hasOwnProperty,P=Object.prototype.propertyIsEnumerable;var k=(r,o,n)=>o in r?D(r,o,{enumerable:!0,configurable:!0,writable:!0,value:n}):r[o]=n,h=(r,o)=>{for(var n in o||(o={}))F.call(o,n)&&k(r,n,o[n]);if(z)for(var n of z(o))P.call(o,n)&&k(r,n,o[n]);return r},p=(r,o)=>W(r,H(o));import{e as g,j as R,k as V,L as y,o as x,c as S,a as f,t as d,g as _,b as j,u as q,n as G}from"./index-1783244194656.js";import{aS as M}from"./chartTheme-fbd5f176-1783244194656.js";const E={class:"smart-chart-header"},I={key:0},U={key:0,class:"smart-chart-source"},Q={__name:"SmartChartRenderer",props:{chart:{type:Object,required:!0}},emits:["ready"],setup(r,{emit:o}){const n=o,i=r,l=["#30dcff","#27f0c0","#ffd76c","#7aa7ff","#ff8f6b","#a6f06d"],N=g(()=>{const e=(i.chart.rows||[]).filter(t=>Number.isFinite(Number(t.value)));return i.chart.type==="pie"?w(e):i.chart.type==="line"?B(e):A(e)}),T=g(()=>{const e=i.chart.rows||[];return i.chart.type==="bar"&&L(e)}),v=g(()=>{const e=i.chart.source||{},t=$(e),a=m(e.period||e.dataPeriod||i.chart.period),b=m(e.generatedAt||e.updateTime||e.updatedAt,!0),u=[t,a,b].filter(Boolean);return u.length?`统计口径:${u.join(" / ")}`:"统计口径:业务统计库"});R(()=>{V(()=>{window.requestAnimationFrame(()=>n("ready"))})});function A(e){return L(e)?C(e):{animationDuration:700,grid:{left:52,right:26,top:28,bottom:54},tooltip:s(),xAxis:{type:"category",data:e.map(t=>t.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:e.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:e.length>7?16:22,data:e.map((t,a)=>({value:t.value,name:t.name,itemStyle:{borderRadius:[8,8,0,0],color:new y(0,0,0,1,[{offset:0,color:l[a%l.length]},{offset:1,color:"rgba(48, 220, 255, 0.08)"}])}})),label:{show:!0,position:"top",color:"#ffffff",fontSize:11,formatter:({value:t})=>c(t)}}]}}function C(e){return{animationDuration:700,grid:{left:78,right:72,top:22,bottom:20},tooltip:p(h({},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:e.map(t=>t.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:e.map((t,a)=>({value:t.value,name:t.name,itemStyle:{borderRadius:[0,8,8,0],color:new y(0,0,1,0,[{offset:0,color:"rgba(48, 220, 255, 0.16)"},{offset:1,color:l[a%l.length]}])}})),label:{show:!0,position:"right",color:"#ffffff",fontSize:11,formatter:({value:t})=>c(t)}}]}}function w(e){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 D=Object.defineProperty,W=Object.defineProperties;var H=Object.getOwnPropertyDescriptors;var z=Object.getOwnPropertySymbols;var F=Object.prototype.hasOwnProperty,P=Object.prototype.propertyIsEnumerable;var k=(r,o,n)=>o in r?D(r,o,{enumerable:!0,configurable:!0,writable:!0,value:n}):r[o]=n,h=(r,o)=>{for(var n in o||(o={}))F.call(o,n)&&k(r,n,o[n]);if(z)for(var n of z(o))P.call(o,n)&&k(r,n,o[n]);return r},p=(r,o)=>W(r,H(o));import{e as g,j as R,k as V,L as y,o as x,c as S,a as f,t as d,g as _,b as j,u as q,n as G}from"./index-1783264824576.js";import{aS as M}from"./chartTheme-3259b6f8-1783264824576.js";const E={class:"smart-chart-header"},I={key:0},U={key:0,class:"smart-chart-source"},Q={__name:"SmartChartRenderer",props:{chart:{type:Object,required:!0}},emits:["ready"],setup(r,{emit:o}){const n=o,i=r,l=["#30dcff","#27f0c0","#ffd76c","#7aa7ff","#ff8f6b","#a6f06d"],N=g(()=>{const e=(i.chart.rows||[]).filter(t=>Number.isFinite(Number(t.value)));return i.chart.type==="pie"?w(e):i.chart.type==="line"?B(e):A(e)}),T=g(()=>{const e=i.chart.rows||[];return i.chart.type==="bar"&&L(e)}),v=g(()=>{const e=i.chart.source||{},t=$(e),a=m(e.period||e.dataPeriod||i.chart.period),b=m(e.generatedAt||e.updateTime||e.updatedAt,!0),u=[t,a,b].filter(Boolean);return u.length?`统计口径:${u.join(" / ")}`:"统计口径:业务统计库"});R(()=>{V(()=>{window.requestAnimationFrame(()=>n("ready"))})});function A(e){return L(e)?C(e):{animationDuration:700,grid:{left:52,right:26,top:28,bottom:54},tooltip:s(),xAxis:{type:"category",data:e.map(t=>t.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:e.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:e.length>7?16:22,data:e.map((t,a)=>({value:t.value,name:t.name,itemStyle:{borderRadius:[8,8,0,0],color:new y(0,0,0,1,[{offset:0,color:l[a%l.length]},{offset:1,color:"rgba(48, 220, 255, 0.08)"}])}})),label:{show:!0,position:"top",color:"#ffffff",fontSize:11,formatter:({value:t})=>c(t)}}]}}function C(e){return{animationDuration:700,grid:{left:78,right:72,top:22,bottom:20},tooltip:p(h({},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:e.map(t=>t.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:e.map((t,a)=>({value:t.value,name:t.name,itemStyle:{borderRadius:[0,8,8,0],color:new y(0,0,1,0,[{offset:0,color:"rgba(48, 220, 255, 0.16)"},{offset:1,color:l[a%l.length]}])}})),label:{show:!0,position:"right",color:"#ffffff",fontSize:11,formatter:({value:t})=>c(t)}}]}}function w(e){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:e.map((t,a)=>({name:t.name,value:t.value,itemStyle:{color:l[a%l.length]}}))}]}}function B(e){return{animationDuration:760,grid:{left:52,right:28,top:30,bottom:48},tooltip:p(h({},s()),{trigger:"axis",axisPointer:{type:"line",lineStyle:{color:"rgba(255, 215, 108, 0.52)"}}}),xAxis:{type:"category",boundaryGap:!1,data:e.map(t=>t.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 y(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:t})=>c(t)},data:e.map(t=>({value:t.value,name:t.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:e=>{const t=(i.chart.rows||[]).find(u=>u.name===e.name),a=(t==null?void 0:t.unit)||i.chart.unit||"",b=t!=null&&t.extra?`<br/><span style="color:rgba(223,250,255,.62)">${t.extra}</span>`:"";return`${e.marker}${e.name}: ${c(e.value)}${a}${b}`}}}function L(e=[]){return e.length>6||e.some(t=>String(t.name||"").length>4)}function $(e={}){const t=m(e.sourceName||e.name||e.label);if(t)return t;const a=String(e.mode||"").trim();return/openapi|api|stat/i.test(a)?"":/local|mock|fallback/i.test(a)?"":""}function m(e,t=!1){const a=String(e||"").trim();return a?t&&/^\d{4}-\d{2}-\d{2}/.test(a)?a.slice(0,10):O(a)?"":a:""}function O(e){return/^[a-zA-Z0-9_./:-]+$/.test(e)&&(e.includes("_")||e.length>24)}function c(e){const t=Number(e);return Number.isFinite(t)?Number.isInteger(t)?t.toLocaleString("zh-CN"):t.toLocaleString("zh-CN",{maximumFractionDigits:2}):"--"}return(e,t)=>(x(),S("div",{class:G(["smart-chart-card",{"is-horizontal":T.value}])},[f("div",E,[f("div",null,[f("strong",null,d(r.chart.title),1),f("span",null,d(r.chart.subtitle),1)]),r.chart.unit?(x(),S("em",I,d(r.chart.unit),1)):_("",!0)]),j(q(M),{class:"smart-chart",option:N.value,autoresize:""},null,8,["option"]),v.value?(x(),S("div",U,d(v.value),1)):_("",!0)],2))}};export{Q as default};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

File diff suppressed because one or more lines are too long

@ -1 +1 @@
.login-screen[data-v-8ebd0d6e]{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-1783244194656.png) center/cover no-repeat,#05142f;color:#fff}.login-screen[data-v-8ebd0d6e]: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-8ebd0d6e]{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-8ebd0d6e]{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-8ebd0d6e]{font-size:42px;line-height:1.2;font-weight:700}.login-brand small[data-v-8ebd0d6e]{color:#c4f3feb8;font-size:13px;letter-spacing:0}.login-panel[data-v-8ebd0d6e]{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-8ebd0d6e]{margin-bottom:26px;color:#fff;font-size:22px;font-weight:700;text-align:center}.login-field[data-v-8ebd0d6e]{display:block;margin-bottom:18px}.login-field span[data-v-8ebd0d6e]{display:block;margin-bottom:8px;color:#c4f3fec7;font-size:13px}.login-field input[data-v-8ebd0d6e]{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-8ebd0d6e]:focus{border-color:#30dcffb8;box-shadow:0 0 0 2px #30dcff1f}.login-error[data-v-8ebd0d6e]{min-height:20px;margin:-4px 0 12px;color:#ffcf8a;font-size:13px;line-height:20px}.login-submit[data-v-8ebd0d6e]{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-8ebd0d6e]:disabled{cursor:not-allowed;opacity:.68}@media (max-width: 820px){.login-shell[data-v-8ebd0d6e]{grid-template-columns:1fr;gap:28px}.login-brand span[data-v-8ebd0d6e]{font-size:30px}.login-panel[data-v-8ebd0d6e]{width:100%}}
.login-screen[data-v-8ebd0d6e]{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-1783264824576.png) center/cover no-repeat,#05142f;color:#fff}.login-screen[data-v-8ebd0d6e]: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-8ebd0d6e]{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-8ebd0d6e]{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-8ebd0d6e]{font-size:42px;line-height:1.2;font-weight:700}.login-brand small[data-v-8ebd0d6e]{color:#c4f3feb8;font-size:13px;letter-spacing:0}.login-panel[data-v-8ebd0d6e]{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-8ebd0d6e]{margin-bottom:26px;color:#fff;font-size:22px;font-weight:700;text-align:center}.login-field[data-v-8ebd0d6e]{display:block;margin-bottom:18px}.login-field span[data-v-8ebd0d6e]{display:block;margin-bottom:8px;color:#c4f3fec7;font-size:13px}.login-field input[data-v-8ebd0d6e]{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-8ebd0d6e]:focus{border-color:#30dcffb8;box-shadow:0 0 0 2px #30dcff1f}.login-error[data-v-8ebd0d6e]{min-height:20px;margin:-4px 0 12px;color:#ffcf8a;font-size:13px;line-height:20px}.login-submit[data-v-8ebd0d6e]{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-8ebd0d6e]:disabled{cursor:not-allowed;opacity:.68}@media (max-width: 820px){.login-shell[data-v-8ebd0d6e]{grid-template-columns:1fr;gap:28px}.login-brand span[data-v-8ebd0d6e]{font-size:30px}.login-panel[data-v-8ebd0d6e]{width:100%}}

File diff suppressed because one or more lines are too long

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,J as w,K as h,e as M,o as _,c as f,a as e,y as S,z as g,A as y,t as b,g as V,d as p,M as k}from"./index-1783244194656.js";const B={class:"login-screen"},D={class:"login-shell"},R={class:"login-field"},U={class:"login-field"},q={key:0,class:"login-error"},A=["disabled"],C={__name:"index",setup(v){const d=w(),n=h(),i=p(""),a=p(""),u=p(!1),o=p(""),r=M(()=>{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",R,[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",U,[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",q,b(o.value),1)):V("",!0),e("button",{class:"login-submit",disabled:u.value,type:"submit"},b(u.value?"登录中":"登录"),9,A)],32)])]))}},N=x(C,[["__scopeId","data-v-8ebd0d6e"]]);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,J as w,K as h,e as M,o as _,c as f,a as e,y as S,z as g,A as y,t as b,g as V,d as p,M as k}from"./index-1783264824576.js";const B={class:"login-screen"},D={class:"login-shell"},R={class:"login-field"},U={class:"login-field"},q={key:0,class:"login-error"},A=["disabled"],C={__name:"index",setup(v){const d=w(),n=h(),i=p(""),a=p(""),u=p(!1),o=p(""),r=M(()=>{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",R,[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",U,[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",q,b(o.value),1)):V("",!0),e("button",{class:"login-submit",disabled:u.value,type:"submit"},b(u.value?"登录中":"登录"),9,A)],32)])]))}},N=x(C,[["__scopeId","data-v-8ebd0d6e"]]);export{N as default};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

@ -246,10 +246,8 @@ onBeforeUnmount(() => {
flex: 0 0 42px;
background-repeat: no-repeat;
background-position: center center;
background-size: 100%;
filter:
drop-shadow(0 0 6px rgba(48, 220, 255, 0.58))
drop-shadow(0 0 14px rgba(48, 220, 255, 0.22));
background-size: contain;
filter: none;
&::before,
&::after {
@ -348,7 +346,7 @@ onBeforeUnmount(() => {
&.is-counting {
.count-card-icon {
filter: drop-shadow(0 0 9px rgba(48, 220, 255, 0.52));
filter: none;
}
.count-card-right .value {

@ -1,5 +1,3 @@
import { hongyuanTownshipLabelPoints } from "@/config/townshipBoundaries"
export const HONGYUAN_BOUNDS = {
west: 101.852492,
south: 31.851389,
@ -462,58 +460,16 @@ export const ecologicalProtectionLayers = {
],
}
const yakTownCenters = Object.fromEntries(hongyuanTownshipLabelPoints.map((item) => [item.name, item.center]))
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 },
]
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-recognition-layer",
name: "牦牛识别",
sourceType: "geojson",
geojsonData: createYakPointCollection(yakRecognitionRows, "yak-recognition-layer", "识别数量"),
opacity: 0.94,
layerName: "ne:daping_yak_marks",
styles: "yak_marks",
cqlFilter: "gov_show = true",
opacity: 0.96,
queryBuffer: 10,
mapView: {
targetLngLat: [102.58, 32.72],
distanceScale: 0.82,
@ -524,8 +480,8 @@ export const yakIndustryChainLayers = {
{
key: "yak-recognition-all",
name: "牦牛识别",
fillColor: "#32DCFF",
strokeColor: "#B8F7FF",
fillColor: "#FFFF00",
strokeColor: "#FFFF00",
selectable: false,
},
],

@ -91,8 +91,8 @@ export async function getBalanceAdvice({ areaCode } = {}) {
areaCode,
}
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
@ -109,8 +109,8 @@ export async function getBalanceAdvice({ areaCode } = {}) {
export async function queryBalanceTargets(question) {
const requests = [
() => get(OPEN_API_BASE, "/ai/balance/targets", { question }, { timeout: 16000 }),
() => get(JSY_API_BASE, "/ai/question/balance", { question }, { timeout: 16000 }),
() => get(OPEN_API_BASE, "/ai/balance/targets", { question }, { timeout: 16000 }),
]
let lastError

@ -47,6 +47,37 @@ function getMonitoringToken(row) {
return EZVIZ_ACCESS_TOKEN || pickValue(row, ["token", "accessToken", "access_token"])
}
function getMonitoringPlayUrl(row) {
return pickValue(row, ["hdPlayUrl", "hd_play_url", "playUrl", "play_url", "url"])
}
function isMonitoringOnline(row) {
const status = pickValue(row, [
"isOnline",
"online",
"status",
"deviceStatus",
"statusName",
"state",
"onlineStatus",
"deviceState",
])
if (status === "") return true
if (typeof status === "boolean") return status
if (typeof status === "number") return status !== 0
const text = String(status).trim().toLowerCase()
if (!text) return true
if (["1", "true", "online", "on"].includes(text)) return true
if (["0", "false", "offline", "off"].includes(text)) return false
if (/(离线|不在线|未在线|断线|断开|异常|故障|停用|不可用|offline|disabled|closed|stop)/i.test(text)) return false
if (/(在线|直播中|正常|可用|online|enabled|open|running|active)/i.test(text)) return true
return true
}
export function isPlayableMonitoring(row) {
return Boolean(getMonitoringToken(row) && getMonitoringPlayUrl(row) && isMonitoringOnline(row))
}
function inferTownName(name = "") {
const normalizedName = String(name)
const town = HONGYUAN_TOWN_NAMES.find((item) => normalizedName.includes(item.replace(/[镇乡]$/, "")))
@ -83,7 +114,7 @@ function normalizeMonitoring(row, index, total, validTotal) {
const hdPlayUrl = pickValue(row, ["hdPlayUrl", "hd_play_url", "url"])
const playUrl = pickValue(row, ["playUrl", "play_url"], hdPlayUrl)
const token = getMonitoringToken(row)
const isOnline = Boolean(token && (hdPlayUrl || playUrl))
const isOnline = isPlayableMonitoring(row)
const onlineRate = total ? Math.round((validTotal / total) * 100) : 0
return {
@ -113,6 +144,6 @@ export async function getHomeMonitorings(params = {}) {
timeout: 18000,
}).then(unwrapResponse)
const rows = Array.isArray(data) ? data : data?.rows || data?.list || []
const validTotal = rows.filter((row) => getMonitoringToken(row) && pickValue(row, ["hdPlayUrl", "playUrl", "url"])).length
return rows.map((row, index) => normalizeMonitoring(row, index, rows.length, validTotal))
const playableRows = rows.filter(isPlayableMonitoring)
return playableRows.map((row, index) => normalizeMonitoring(row, index, rows.length, playableRows.length))
}

@ -1,12 +1,18 @@
import { normalizeResponseData, requestJson } from "./request"
const OPEN_API_BASE = import.meta.env.VITE_OPEN_API_BASE || "/openApi"
const JSY_API_BASE = normalizeJsyApiBase(import.meta.env.VITE_JSY_API_BASE || "/jsyApi")
function joinBase(base, path) {
if (/^https?:\/\//.test(path)) return path
return `${base}${path.startsWith("/") ? path : `/${path}`}`
}
function normalizeJsyApiBase(base) {
const normalized = String(base || "/jsyApi").replace(/\/+$/, "")
return normalized.endsWith("/api") ? normalized : `${normalized}/api`
}
function get(base, path, params, options = {}) {
return requestJson(joinBase(base, path), {
params,
@ -14,6 +20,25 @@ function get(base, path, params, options = {}) {
}).then(unwrapResponse)
}
function normalizeRows(data) {
if (Array.isArray(data)) return data
return data?.rows || data?.list || []
}
async function firstMeaningful(requests, isMeaningful = (data) => Boolean(data)) {
let lastError
for (const request of requests) {
try {
const data = await request()
if (isMeaningful(data)) return data
} catch (error) {
lastError = error
}
}
if (lastError) throw lastError
return null
}
function unwrapResponse(response) {
const failed =
response?.success === false ||
@ -41,29 +66,24 @@ export function askStatQuestion(question, pagePath = window.location.hash || win
)
}
export function queryBalanceTargets(question) {
return get(
OPEN_API_BASE,
"/ai/balance/targets",
{
question,
},
{ timeout: 18000 },
).then((data) => {
if (Array.isArray(data)) return data
return data?.rows || data?.list || []
})
export async function queryBalanceTargets(question) {
const rows = await firstMeaningful(
[
() => get(JSY_API_BASE, "/ai/question/balance", { question }, { timeout: 18000 }),
() => get(OPEN_API_BASE, "/ai/balance/targets", { question }, { timeout: 18000 }),
],
(data) => normalizeRows(data).length > 0,
)
return normalizeRows(rows)
}
export function getLivestockBalance({ areaCode, pasturesId } = {}) {
return get(
OPEN_API_BASE,
"/ai/balance/advice",
{
areaCode,
pasturesId,
},
{ timeout: 18000 },
export async function getLivestockBalance({ areaCode, pasturesId } = {}) {
return firstMeaningful(
[
() => get(JSY_API_BASE, "/ai/livestockBalance", { areaCode, pasturesId }, { timeout: 18000 }),
() => get(OPEN_API_BASE, "/ai/balance/advice", { areaCode, pasturesId }, { timeout: 18000 }),
],
(data) => data && typeof data === "object" && Object.keys(data).length > 0,
)
}

@ -287,16 +287,16 @@ export const ecologicalProtectionMenu = [
]
export const parkTownRows = [
{ name: "麦洼乡", protectedAreaWanMu: 65.94, coreProtectionAreaWanMu: 28.69, generalControlAreaWanMu: 37.25, grasslandAreaWanMu: 31.41, wetlandAreaWanMu: 11.16, ecologyJobs: 179, yakCount: 61720, livestockCount: 63507, cover: 85, villages: "洞拉村、滚塘村、让里村、茸塔玛村" },
{ name: "瓦切镇", protectedAreaWanMu: 74.44, coreProtectionAreaWanMu: 31.1, generalControlAreaWanMu: 43.34, grasslandAreaWanMu: 31.42, wetlandAreaWanMu: 11.15, ecologyJobs: 160, yakCount: 33887, livestockCount: 34589, cover: 85, villages: "唐日村、德香村、日干村" },
{ name: "色地镇", protectedAreaWanMu: 20.93, coreProtectionAreaWanMu: 7.6, generalControlAreaWanMu: 13.33, grasslandAreaWanMu: 31.42, wetlandAreaWanMu: 11.16, ecologyJobs: 39, yakCount: 9572, livestockCount: 9843, cover: 86, villages: "色永村" },
{ name: "瓦切镇", protectedAreaWanMu: 75.01, coreProtectionAreaWanMu: 31.65, generalControlAreaWanMu: 43.36, grasslandAreaWanMu: 32.07, wetlandAreaWanMu: 11.73, ecologyJobs: 160, householdCount: 1400, peopleCount: 6375, yakCount: 43459, livestockCount: 45260, cover: 85, villages: "唐日村、德香村、日干村、色永村、曲登塘社区" },
{ name: "麦洼乡", protectedAreaWanMu: 66.61, coreProtectionAreaWanMu: 28.1, generalControlAreaWanMu: 38.51, grasslandAreaWanMu: 19.66, wetlandAreaWanMu: 6.52, ecologyJobs: 179, householdCount: 1049, peopleCount: 4857, yakCount: 35773, livestockCount: 36776, cover: 85, villages: "洞拉村、滚塘村" },
{ name: "色地镇", protectedAreaWanMu: 20.98, coreProtectionAreaWanMu: 8.85, generalControlAreaWanMu: 12.13, grasslandAreaWanMu: 42.52, wetlandAreaWanMu: 15.22, ecologyJobs: 39, householdCount: 405, peopleCount: 1692, yakCount: 25947, livestockCount: 26731, cover: 86, villages: "让里村、茸塔玛村" },
]
export const parkSummary = {
name: "若尔盖国家公园红原区域",
nationalParkTotalAreaWanMu: 2074.39,
hongyuanDelimitedAreaWanMu: 161.31,
totalAreaWanMu: 161.31,
nationalParkTotalAreaWanMu: 1250.4,
hongyuanDelimitedAreaWanMu: 162.55,
totalAreaWanMu: 162.55,
grasslandAreaWanMu: 94.25,
wetlandAreaWanMu: 33.47,
woodlandAreaWanMu: null,
@ -304,7 +304,8 @@ export const parkSummary = {
vegetationCoverage: round(sumBy(parkTownRows, "cover") / parkTownRows.length, 1),
townCount: parkTownRows.length,
villageCount: parkTownRows.reduce((sum, item) => sum + String(item.villages || "").split("、").filter(Boolean).length, 0),
householdCount: 3023,
householdCount: sumBy(parkTownRows, "householdCount"),
peopleCount: sumBy(parkTownRows, "peopleCount"),
ecologyJobCount: sumBy(parkTownRows, "ecologyJobs"),
yakCount: sumBy(parkTownRows, "yakCount"),
livestockCount: sumBy(parkTownRows, "livestockCount"),
@ -315,7 +316,7 @@ parkSummary.otherProtectedAreaWanMu = round(
)
export const parkCoreRows = [
{ key: "park-national-area", name: "国家公园总面积", value: parkSummary.nationalParkTotalAreaWanMu, unit: "万亩", icon: "park" },
{ key: "park-national-area", name: "四川片区面积", value: parkSummary.nationalParkTotalAreaWanMu, unit: "万亩", icon: "park" },
{ key: "park-hongyuan-area", name: "红原县划定面积", value: parkSummary.hongyuanDelimitedAreaWanMu, unit: "万亩", icon: "park" },
{ key: "park-yak-count", name: "牦牛数量", value: parkSummary.yakCount, unit: "头", icon: "stack" },
]
@ -343,35 +344,36 @@ const parkTownListRows = parkTownRows.map((item) => ({
}))
const parkZoningRows = [
{ key: "core-protection", name: "核心保护区", value: 67.39, unit: "万亩", color: NATIONAL_PARK_ZONING_COLORS.core.fillColor },
{ key: "general-control", name: "一般控制区", value: 93.92, unit: "万亩", color: NATIONAL_PARK_ZONING_COLORS.general.fillColor },
{ key: "core-protection", name: "核心保护区", value: 68.6, unit: "万亩", color: NATIONAL_PARK_ZONING_COLORS.core.fillColor },
{ key: "general-control", name: "一般控制区", value: 94, unit: "万亩", color: NATIONAL_PARK_ZONING_COLORS.general.fillColor },
]
const parkGrasslandProductivityWanTon = 43.66
const parkWetlandProductivityWanTon = 12.48
const parkEdibleGrasslandWanTon = round(parkGrasslandProductivityWanTon * (192.26 / 297.29), 2)
const parkGrasslandCapacityPerMu = 0.06229833103624831
const parkBalanceTownRowMap = new Map([
["瓦切镇", { areaRange: 747837.6, yakNumber: 121654, bearingCapacity: 63347.29, livestockCapacity: 66751, overloadRate: 2.05, level: "轻度超载", year: "2026" }],
["麦洼乡", { areaRange: 458338.35, yakNumber: 78465, bearingCapacity: 37634.86, livestockCapacity: 55974, overloadRate: 1.18, level: "轻度超载", year: "2026" }],
["色地镇", { areaRange: 991265.25, yakNumber: 131956, bearingCapacity: 83382.49, livestockCapacity: 70715, overloadRate: 2.14, level: "轻度超载", year: "2026" }],
["瓦切镇", { areaRange: 750120, yakNumber: 43459, bearingCapacity: 44927.54, livestockCapacity: 45260, overloadRate: 0.74, level: "总体可控", year: "2026" }],
["麦洼乡", { areaRange: 666075, yakNumber: 35773, bearingCapacity: 36505.86, livestockCapacity: 36776, overloadRate: 0.74, level: "总体可控", year: "2026" }],
["色地镇", { areaRange: 209805, yakNumber: 25947, bearingCapacity: 26534.58, livestockCapacity: 26731, overloadRate: 0.74, level: "总体可控", year: "2026" }],
])
const parkVillageWeights = {
麦洼乡: [
{ name: "洞拉村", grassland: 0.29, wetland: 0.34, household: 0.3, yak: 0.32, edible: 0.31, woodland: 0.3 },
{ name: "滚塘村", grassland: 0.26, wetland: 0.27, household: 0.25, yak: 0.25, edible: 0.26, woodland: 0.25 },
{ name: "让里村", grassland: 0.24, wetland: 0.21, household: 0.24, yak: 0.23, edible: 0.23, woodland: 0.24 },
{ name: "茸塔玛村", grassland: 0.21, wetland: 0.18, household: 0.21, yak: 0.2, edible: 0.2, woodland: 0.21 },
{ name: "洞拉村", grassland: 0.632, wetland: 0.632, household: 0.5643, yak: 0.545, edible: 0.632, woodland: 0.632 },
{ name: "滚塘村", grassland: 0.368, wetland: 0.368, household: 0.4357, yak: 0.455, edible: 0.368, woodland: 0.368 },
],
瓦切镇: [
{ name: "唐日村", grassland: 0.38, wetland: 0.42, household: 0.36, yak: 0.4, edible: 0.39, woodland: 0.38 },
{ name: "德香村", grassland: 0.32, wetland: 0.32, household: 0.33, yak: 0.32, edible: 0.32, woodland: 0.32 },
{ name: "日干村", grassland: 0.3, wetland: 0.26, household: 0.31, yak: 0.28, edible: 0.29, woodland: 0.3 },
{ name: "唐日村", grassland: 0.3059, wetland: 0.3059, household: 0.1957, yak: 0.2843, edible: 0.3059, woodland: 0.3059 },
{ name: "德香村", grassland: 0.2369, wetland: 0.2369, household: 0.2679, yak: 0.2848, edible: 0.2369, woodland: 0.2369 },
{ name: "日干村", grassland: 0.1603, wetland: 0.1603, household: 0.2071, yak: 0.2107, edible: 0.1603, woodland: 0.1603 },
{ name: "色永村", grassland: 0.2969, wetland: 0.2969, household: 0.2607, yak: 0.2202, edible: 0.2969, woodland: 0.2969 },
{ name: "曲登塘社区", grassland: 0, wetland: 0, household: 0.0679, yak: 0, edible: 0, woodland: 0 },
],
色地镇: [
{ name: "色永村", grassland: 1, wetland: 1, household: 1, yak: 1, edible: 1, woodland: 1 },
{ name: "让里村", grassland: 0.4623, wetland: 0.4623, household: 0.7309, yak: 0.6411, edible: 0.4623, woodland: 0.4623 },
{ name: "茸塔玛村", grassland: 0.5377, wetland: 0.5377, household: 0.2691, yak: 0.3589, edible: 0.5377, woodland: 0.5377 },
],
}
const parkTownLivestockTotal = sumBy(parkTownRows, "livestockCount") || sumBy(parkTownRows, "yakCount") || 1
const parkTownBusinessRows = parkTownRows.map((item) => ({
...item,
value: item.protectedAreaWanMu,
@ -380,7 +382,7 @@ const parkTownBusinessRows = parkTownRows.map((item) => ({
core: item.coreProtectionAreaWanMu,
general: item.generalControlAreaWanMu,
},
householdCount: Math.round((parkSummary.householdCount * Number(item.livestockCount || item.yakCount || 0)) / parkTownLivestockTotal),
householdCount: Number(item.householdCount || 0),
edibleGrasslandWanTon: round(
(Number(item.grasslandAreaWanMu || 0) / Math.max(parkSummary.grasslandAreaWanMu, 1)) * parkEdibleGrasslandWanTon,
2
@ -418,9 +420,9 @@ const parkBalanceDirectRates = parkTownBusinessRows
.map((item) => Number(item.overloadRate))
.filter((value) => Number.isFinite(value))
parkSummary.balanceOverloadRate = parkBalanceDirectRates.length
? round(parkBalanceDirectRates.reduce((sum, value) => sum + value, 0) / parkBalanceDirectRates.length, 1)
? round(parkBalanceDirectRates.reduce((sum, value) => sum + value, 0) / parkBalanceDirectRates.length, 2)
: parkBalanceBearingCapacity
? round(((parkBalanceLivestockCapacity - parkBalanceBearingCapacity) / parkBalanceBearingCapacity) * 100, 1)
? round(((parkBalanceLivestockCapacity - parkBalanceBearingCapacity) / parkBalanceBearingCapacity) * 100, 2)
: 0
const withParkPercentExtra = (rows, total = sumBy(rows, "value")) =>
@ -510,6 +512,32 @@ const parkTownZoningRows = sortParkTownRows(parkTownBusinessRows).map((item) =>
},
}))
const parkTownGrassWetRows = sortParkTownRows(parkTownBusinessRows).map((item) => ({
key: `${item.name}-grass-wet-area`,
name: item.name,
value: round(item.grasslandAreaWanMu + item.wetlandAreaWanMu, 2),
unit: "万亩",
segments: {
grassland: item.grasslandAreaWanMu,
wetland: item.wetlandAreaWanMu,
},
}))
const parkTownProductivityRows = sortParkTownRows(parkTownBusinessRows).map((item) => ({
key: `${item.name}-grass-wet-productivity`,
name: item.name,
value: round(
(item.grasslandAreaWanMu / Math.max(parkSummary.grasslandAreaWanMu, 1)) * parkGrasslandProductivityWanTon
+ (item.wetlandAreaWanMu / Math.max(parkSummary.wetlandAreaWanMu, 1)) * parkWetlandProductivityWanTon,
2
),
unit: "万吨",
segments: {
grassland: round((item.grasslandAreaWanMu / Math.max(parkSummary.grasslandAreaWanMu, 1)) * parkGrasslandProductivityWanTon, 2),
wetland: round((item.wetlandAreaWanMu / Math.max(parkSummary.wetlandAreaWanMu, 1)) * parkWetlandProductivityWanTon, 2),
},
}))
const parkTownYakRows = sortParkTownRows(parkTownBusinessRows).map((item) => ({
key: `${item.name}-yak`,
name: item.name,
@ -545,11 +573,11 @@ const parkTownLedgerRows = sortParkTownRows(parkTownBusinessRows).map((item) =>
}))
export const parkTopStats = [
{ zh: "国家公园总面积", value: parkSummary.nationalParkTotalAreaWanMu, unit: "万亩", decimals: 2 },
{ zh: "四川片区面积", value: parkSummary.nationalParkTotalAreaWanMu, unit: "万亩", decimals: 2 },
{ zh: "红原县划定面积", value: parkSummary.hongyuanDelimitedAreaWanMu, unit: "万亩", decimals: 2 },
{ zh: "牦牛数量", value: parkSummary.yakCount, unit: "头", decimals: 0 },
{ zh: "牧户数量", value: parkSummary.householdCount, unit: "户", decimals: 0 },
{ zh: "草畜平衡超载率", value: parkSummary.balanceOverloadRate, unit: "%", decimals: 1 },
{ zh: "草畜平衡超载率", value: parkSummary.balanceOverloadRate, unit: "%", decimals: 2 },
{ zh: "瓦切镇面积", value: parkTownBusinessRows.find((item) => item.name === "瓦切镇")?.protectedAreaWanMu || 0, unit: "万亩", decimals: 2 },
{ zh: "麦洼乡面积", value: parkTownBusinessRows.find((item) => item.name === "麦洼乡")?.protectedAreaWanMu || 0, unit: "万亩", decimals: 2 },
{ zh: "色地镇面积", value: parkTownBusinessRows.find((item) => item.name === "色地镇")?.protectedAreaWanMu || 0, unit: "万亩", decimals: 2 },
@ -561,7 +589,6 @@ export const restorationTopStats = [
{ zh: "修复项目", value: restorationSummary.kpi.projectCount, unit: "个", decimals: 0 },
{ zh: "治理面积", value: restorationSummary.kpi.areaWanMu, unit: "万亩", decimals: 2 },
{ zh: "工程长度", value: restorationSummary.kpi.lengthKm, unit: "公里", decimals: 2 },
{ zh: "涉及乡镇", value: restorationSummary.kpi.townshipCount, unit: "个", decimals: 0 },
]
export const engineeringAnalysisTopStats = [
@ -586,21 +613,28 @@ export function getEcologicalTopStats(viewKey) {
export const parkPanels = {
left: [
{
key: "park-town-area",
title: "乡镇划定面积",
type: "column",
variant: "plain",
rows: parkTownAreaRows,
key: "park-town-grass-wet-area",
title: "草地湿地面积分布",
type: "stackColumn",
variant: "dual",
unit: "万亩",
segments: [
{ key: "grassland", name: "草地面积", color: "#24f6c4" },
{ key: "wetland", name: "湿地面积", color: "#ffe06b" },
],
rows: parkTownGrassWetRows,
},
{
key: "park-town-zoning",
title: "乡镇管控区构成",
key: "park-town-productivity",
title: "草地湿地生产力",
type: "stackColumn",
variant: "dual",
unit: "万吨",
segments: [
{ key: "core", name: "核心保护区", color: NATIONAL_PARK_ZONING_COLORS.core.fillColor },
{ key: "general", name: "一般控制区", color: NATIONAL_PARK_ZONING_COLORS.general.fillColor },
{ key: "grassland", name: "草地生产力", color: "#32dcff" },
{ key: "wetland", name: "湿地生产力", color: "#ffe06b" },
],
rows: parkTownZoningRows,
rows: parkTownProductivityRows,
},
{
key: "park-town-yak",
@ -620,7 +654,7 @@ export const parkPanels = {
right: [
{
key: "park-zoning-structure",
title: "管控区面积结构",
title: "红原管控区结构",
type: "donut",
rows: withParkPercentExtra(parkZoningRows),
},
@ -771,7 +805,7 @@ export const restorationPanels = {
key: "restoration-year-project",
title: "年度项目数量",
type: "column",
variant: "slim",
variant: "projectYear",
rows: (restorationSummary.years || []).map((item) => ({
key: item.name,
name: `${item.name}`,

@ -4,6 +4,7 @@
ref="mapSceneRef"
:active-layer="activeLayer"
:map-ranking="mapRanking"
:admin-boundary-line-scale="2.4"
@feature-loading="featureLoading = $event"
@feature-info="handleFeatureInfo"
@layer-status="handleLayerStatus"

@ -10,13 +10,20 @@
'is-column-label-standard': columnLabelStandard,
},
]"
@mousemove="handleChartTooltipMove"
@mouseleave="hideChartTooltip"
>
<div v-if="!rows.length" class="business-panel-empty">暂无业务统计数据</div>
<business-stat-list v-else-if="panelType === 'rank'" :rows="rows" compact />
<div v-else-if="panelType === 'metric'" class="business-metrics">
<div v-for="item in rows.slice(0, 4)" :key="item.key || item.name" class="business-metric-item">
<div
v-for="item in rows.slice(0, 4)"
:key="item.key || item.name"
class="business-metric-item"
:data-chart-tooltip="chartTooltipText(item)"
>
<span>{{ item.name }}</span>
<strong>{{ displayValue(item) }}</strong>
<em>{{ item.unit }}</em>
@ -29,6 +36,7 @@
:key="item.key || item.name"
class="business-icon-metric-item"
:class="`is-tone-${index % colors.length}`"
:data-chart-tooltip="chartTooltipText(item)"
>
<i class="business-icon-metric-mark" :class="`is-${item.icon}`"><span></span></i>
<div class="business-icon-metric-copy">
@ -40,13 +48,18 @@
<div v-else-if="panelType === 'spotlight'" class="business-spotlight">
<template v-if="spotlightMain">
<div class="business-spotlight-main">
<div class="business-spotlight-main" :data-chart-tooltip="chartTooltipText(spotlightMain)">
<span>{{ spotlightMain.name }}</span>
<strong>{{ displayValue(spotlightMain) }}<em>{{ spotlightMain.unit }}</em></strong>
<small>{{ spotlightMain.extra || panel?.note || "核心业务指标" }}</small>
</div>
<div class="business-spotlight-side">
<div v-for="(item, index) in rows.slice(1, 4)" :key="item.key || item.name" class="business-spotlight-chip">
<div
v-for="(item, index) in rows.slice(1, 4)"
:key="item.key || item.name"
class="business-spotlight-chip"
:data-chart-tooltip="chartTooltipText(item)"
>
<i :style="{ borderColor: chartColor(index + 1) }"></i>
<span>{{ item.name }}</span>
<strong>{{ displayValue(item) }}<em>{{ item.unit }}</em></strong>
@ -82,6 +95,7 @@
:stroke-dasharray="segment.dasharray"
:stroke-dashoffset="segment.dashoffset"
:style="{ color: segment.color }"
:data-chart-tooltip="pieTooltipText(segment.item)"
/>
</svg>
<div class="business-ring-inner">
@ -113,6 +127,7 @@
class="business-donut-row"
:class="{ 'is-active': index === normalizedActiveIndex }"
:style="{ '--row-color': chartColor(index), '--row-percent': `${Math.max(4, piePercentValue(item))}%` }"
:data-chart-tooltip="pieTooltipText(item)"
>
<i :style="{ borderColor: chartColor(index) }"></i>
<span>{{ item.name }}</span>
@ -148,6 +163,7 @@
:d="segment.path"
:fill="segment.color"
:stroke="segment.color"
:data-chart-tooltip="pieTooltipText(segment.item)"
/>
</g>
<circle class="business-rose-glow" cx="75" cy="75" r="24" />
@ -171,6 +187,7 @@
:key="item.key || item.name"
class="business-donut-row business-rose-row"
:class="{ 'is-active': index === normalizedActiveIndex }"
:data-chart-tooltip="pieTooltipText(item)"
>
<i :style="{ borderColor: chartColor(index), background: chartColor(index), color: chartColor(index) }"></i>
<span>{{ item.name }}</span>
@ -192,6 +209,7 @@
:class="{ 'is-active': index === normalizedActiveIndex }"
:style="{ width: `${structurePercent(item)}%`, background: chartColor(index), color: chartColor(index) }"
:title="`${item.name}:${displayValue(item)}${item.unit || ''} ${structurePercentText(item)}`"
:data-chart-tooltip="structureTooltipText(item)"
></i>
</div>
</div>
@ -202,6 +220,7 @@
class="business-structure-row"
:class="{ 'is-active': item.displayIndex === normalizedActiveIndex }"
:style="{ '--row-color': chartColor(item.displayIndex), '--row-percent': `${Math.max(4, structurePercent(item))}%` }"
:data-chart-tooltip="structureTooltipText(item)"
>
<i :style="{ background: chartColor(item.displayIndex), color: chartColor(item.displayIndex) }"></i>
<span>{{ item.name }}</span>
@ -215,7 +234,7 @@
<div v-else-if="panelType === 'bar' && panel?.variant === 'capacity'" class="business-capacity">
<template v-if="capacityTotalRow">
<div class="business-capacity-head">
<div class="business-capacity-head" :data-chart-tooltip="chartTooltipText(capacityTotalRow)">
<span>{{ capacityTotalRow.name }}</span>
<strong>{{ displayValue(capacityTotalRow) }}<em>{{ capacityTotalRow.unit }}</em></strong>
</div>
@ -226,6 +245,7 @@
:class="{ 'is-active': index === normalizedActiveIndex }"
:style="{ width: `${capacitySegmentWidth(item)}%`, background: capacityColor(item, index), color: capacityColor(item, index) }"
:title="`${item.name}:${displayValue(item)}${item.unit || ''} ${capacityPercent(item)}`"
:data-chart-tooltip="capacityTooltipText(item)"
></i>
</div>
<div class="business-capacity-list">
@ -234,6 +254,7 @@
:key="item.key || item.name"
class="business-capacity-row"
:class="{ 'is-active': index === normalizedActiveIndex }"
:data-chart-tooltip="capacityTooltipText(item)"
>
<i :style="{ background: capacityColor(item, index), color: capacityColor(item, index) }"></i>
<span>{{ item.name }}</span>
@ -241,7 +262,7 @@
<small>{{ capacityPercent(item) }}</small>
</div>
</div>
<div v-if="capacityTopTownRow" class="business-capacity-town">
<div v-if="capacityTopTownRow" class="business-capacity-town" :data-chart-tooltip="chartTooltipText(capacityTopTownRow)">
<span>{{ capacityTopTownRow.name }}</span>
<strong>{{ displayValue(capacityTopTownRow) }}<em>{{ capacityTopTownRow.unit }}</em></strong>
</div>
@ -261,6 +282,7 @@
'is-capacity': panel?.variant === 'capacity',
'is-rank-bar': panel?.variant === 'rankBar',
'is-race-bar': isRaceBarVariant,
'is-few-rows': isRaceBarVariant && chartRows.length <= 3,
'is-degradation': panel?.variant === 'degradation',
'is-race-nowrap': panel?.variant === 'raceBarNoWrap',
'is-race-relaxed': panel?.variant === 'raceBarRelaxed',
@ -273,6 +295,7 @@
class="business-bar-row"
:class="{ 'is-active': index === normalizedActiveIndex }"
:style="{ '--bar-color': item.color || item.fillColor || chartColor(index) }"
:data-chart-tooltip="chartTooltipText(item)"
>
<span v-if="panel?.variant === 'rankBar'" class="business-bar-rank">{{ index + 1 }}</span>
<span class="business-bar-name">{{ item.name }}</span>
@ -308,6 +331,7 @@
:class="{ 'is-active': index === normalizedActiveIndex }"
:data-hover-value="columnHoverValue(item)"
:title="columnHoverValue(item)"
:data-chart-tooltip="chartTooltipText(item)"
>
<strong v-if="!columnLabelStandard">{{ displayValue(item) }}<em v-if="columnItemUnit(item)">{{ columnItemUnit(item) }}</em></strong>
<div class="business-column-track">
@ -389,6 +413,7 @@
:key="item.key || item.name"
class="business-table-row"
:class="{ 'is-active': index === normalizedActiveIndex }"
:data-chart-tooltip="chartTooltipText(item)"
>
<span class="business-table-name">{{ item.name }}</span>
<div class="business-table-value">
@ -404,7 +429,10 @@
<div
v-else-if="panelType === 'stackColumn'"
class="business-stack-columns"
:class="{ 'is-dense': stackColumnRows.length > 6 }"
:class="[
`is-variant-${panel?.variant || 'standard'}`,
{ 'is-dense': stackColumnRows.length > 6 },
]"
>
<div class="business-stack-legend">
<span
@ -416,17 +444,23 @@
</div>
<div class="business-stack-chart">
<div
v-for="item in stackColumnRows"
v-for="(item, index) in stackColumnRows"
:key="item.key || item.name"
class="business-stack-item"
:class="{ 'is-active': index === normalizedActiveIndex }"
:data-hover-value="stackColumnHoverValue(item)"
:title="stackColumnHoverValue(item)"
:data-chart-tooltip="stackColumnTooltipText(item)"
>
<strong v-if="showStackColumnValue()" class="business-stack-total">
{{ formatNumber(stackItemTotal(item)) }}<em v-if="stackColumnUnit(item)">{{ stackColumnUnit(item) }}</em>
</strong>
<div class="business-stack-track">
<i
v-for="(segment, index) in stackItemSegments(item)"
v-for="(segment, segmentIndex) in stackItemSegments(item)"
:key="segment.key || segment.name"
:style="{ height: `${segment.percent}%`, background: segment.color }"
:style="{ height: `${segment.percent}%`, background: segment.color, color: segment.color }"
:data-chart-tooltip="stackSegmentTooltipText(item, segment, segmentIndex)"
></i>
</div>
<span>{{ columnLabelStandard ? verticalShortLabel(item.name) : formatPlaceLabel(item.name) }}</span>
@ -447,6 +481,7 @@
:style="{ '--peak-height': `${barWidth(item)}%`, '--peak-color': chartColor(index) }"
:data-hover-value="columnHoverValue(item)"
:title="columnHoverValue(item)"
:data-chart-tooltip="chartTooltipText(item)"
>
<strong v-if="!columnLabelStandard">{{ displayValue(item) }}<em v-if="peakItemUnit(item)">{{ peakItemUnit(item) }}</em></strong>
<div class="business-peak-stage">
@ -513,6 +548,7 @@
'--gauge-color': gaugeColor(item, index),
'--needle-rotate': `${gaugeNeedleAngle(item)}deg`,
}"
:data-chart-tooltip="gaugeTooltipText(item)"
>
<div class="business-gauge-visual">
<svg viewBox="0 0 152 96" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
@ -543,6 +579,7 @@
:key="item.key || item.name"
class="business-heat-cell"
:style="heatCellStyle(item, index)"
:data-chart-tooltip="chartTooltipText(item)"
>
<i></i>
<span>{{ item.name }}</span>
@ -556,10 +593,16 @@
v-for="(item, index) in splitRows"
:key="item.key || item.name"
:style="{ width: `${splitWidth(item)}%`, background: chartColor(index) }"
:data-chart-tooltip="splitTooltipText(item)"
></i>
</div>
<div class="business-split-list">
<div v-for="(item, index) in splitRows" :key="item.key || item.name" class="business-split-row">
<div
v-for="(item, index) in splitRows"
:key="item.key || item.name"
class="business-split-row"
:data-chart-tooltip="splitTooltipText(item)"
>
<span><i :style="{ background: chartColor(index) }"></i>{{ item.name }}</span>
<strong>{{ displayValue(item) }}{{ item.unit }}</strong>
</div>
@ -567,6 +610,17 @@
</div>
<business-stat-list v-else :rows="rows" compact />
<teleport to="body">
<div
v-if="chartTooltip.visible"
class="business-chart-tooltip"
:style="chartTooltipStyle"
>
<strong>{{ chartTooltip.title }}</strong>
<span v-for="line in chartTooltip.lines" :key="line">{{ line }}</span>
</div>
</teleport>
</div>
</template>
@ -628,6 +682,7 @@ const gaugeRows = computed(() =>
)
const stackSegments = computed(() => props.panel?.segments || [])
const stackColumnRows = computed(() => rows.value.filter((item) => stackItemTotal(item) > 0))
const stackColumnMaxTotal = computed(() => Math.max(...stackColumnRows.value.map((item) => stackItemTotal(item)), 1))
const projectSearchText = ref("")
const selectedProjectKey = ref(null)
const defaultProjectTableColumns = [
@ -687,10 +742,30 @@ const activeDonutIndex = ref(0)
const lineHoverIndex = ref(null)
const panelRef = ref(null)
const panelVisible = ref(true)
const chartTooltip = ref({
visible: false,
x: 0,
y: 0,
title: "",
lines: [],
})
let donutTimer = null
let visibilityObserver = null
const pieLoopDelay = 3000
const pieKey = computed(() => `${props.panel?.key || props.panel?.title || "pie"}-${pieRows.value.map((item) => `${item.name}:${item.value}`).join("|")}`)
const chartTooltipStyle = computed(() => {
const offset = 14
const tooltipWidth = 190
const tooltipHeight = 82
const viewportWidth = typeof window === "undefined" ? 1920 : window.innerWidth
const viewportHeight = typeof window === "undefined" ? 1080 : window.innerHeight
const left = Math.min(viewportWidth - tooltipWidth - 8, chartTooltip.value.x + offset)
const top = Math.min(viewportHeight - tooltipHeight - 8, chartTooltip.value.y + offset)
return {
left: `${Math.max(8, left)}px`,
top: `${Math.max(8, top)}px`,
}
})
const normalizedActiveIndex = computed(() => {
const dataLength = activeLoopLength.value
@ -708,6 +783,7 @@ const activeLoopLength = computed(() => {
if (panelType.value === "table") return tableRows.value.length
if (panelType.value === "line") return chartRows.value.length
if (panelType.value === "gauge") return gaugeRows.value.length
if (panelType.value === "stackColumn") return stackColumnRows.value.length
if (panelType.value === "split") return splitRows.value.length
return 0
})
@ -804,6 +880,7 @@ watch(
lineHoverIndex.value = null
projectSearchText.value = ""
selectedProjectKey.value = null
hideChartTooltip()
syncDonutLoop()
}
)
@ -882,10 +959,11 @@ function stackItemSegments(item) {
if (!total) return []
return stackSegments.value.map((segment, index) => {
const value = Number(values[segment.key]) || 0
const percentBase = props.panel?.variant === "dual" ? stackColumnMaxTotal.value : total
return {
...segment,
value,
percent: Math.max(value > 0 ? 3 : 0, (value / total) * 100),
percent: Math.max(value > 0 ? 3 : 0, (value / Math.max(percentBase, 1)) * 100),
color: segmentColor(segment, index),
}
})
@ -948,12 +1026,103 @@ function columnHoverValue(item) {
return `${name}${displayValue(item)}${unit}`
}
function tooltipText(title, lines = []) {
return [
formatPlaceLabel(title) || "数据",
...lines.filter((line) => line !== undefined && line !== null && String(line).trim() !== "").map((line) => String(line)),
].join("\n")
}
function chartTooltipText(item, extraLines = []) {
if (!item) return ""
const unit = displayUnit(item)
const value = displayValue(item)
const lines = []
if (value !== "--") lines.push(`数值:${value}${unit}`)
if (item.extra) lines.push(String(item.extra))
return tooltipText(item.name, [...lines, ...extraLines])
}
function pieTooltipText(item) {
return chartTooltipText(item, [`占比:${getPiePercent(item)}`])
}
function structureTooltipText(item) {
return chartTooltipText(item, [`占比:${structurePercentText(item)}`])
}
function capacityTooltipText(item) {
return chartTooltipText(item, [`占比:${capacityPercent(item)}`])
}
function stackColumnTooltipText(item) {
const unit = stackColumnUnit(item)
const segmentLines = stackItemSegments(item)
.filter((segment) => segment.value > 0)
.map((segment) => `${segment.name}${formatNumber(segment.value)}${unit}`)
return tooltipText(item?.name, [`合计:${formatNumber(stackItemTotal(item))}${unit}`, ...segmentLines])
}
function stackSegmentTooltipText(item, segment) {
const unit = stackColumnUnit(item)
return tooltipText(`${formatPlaceLabel(item?.name)} ${segment?.name || ""}`, [`数值:${formatNumber(segment?.value || 0)}${unit}`])
}
function gaugeTooltipText(item) {
return chartTooltipText(item, [`状态:${gaugeStatusText(item)}`])
}
function splitTooltipText(item) {
const percent = sumValue.value ? `${formatNumber((numericValue(item) / sumValue.value) * 100)}%` : "--"
return chartTooltipText(item, [`占比:${percent}`])
}
function handleChartTooltipMove(event) {
const target = event.target?.closest?.("[data-chart-tooltip], [data-hover-value]")
if (!target || !panelRef.value?.contains(target)) {
hideChartTooltip()
return
}
const rawText = target.getAttribute("data-chart-tooltip") || target.getAttribute("data-hover-value") || ""
const lines = String(rawText).split(/\n+/).map((line) => line.trim()).filter(Boolean)
if (!lines.length) {
hideChartTooltip()
return
}
chartTooltip.value = {
visible: true,
x: event.clientX,
y: event.clientY,
title: lines[0],
lines: lines.slice(1),
}
}
function hideChartTooltip() {
if (!chartTooltip.value.visible) return
chartTooltip.value = {
visible: false,
x: 0,
y: 0,
title: "",
lines: [],
}
}
function stackColumnHoverValue(item) {
const name = formatPlaceLabel(item?.name) || "数据"
const unit = item?.displayUnit || item?.unit || props.panel?.unit || ""
const unit = stackColumnUnit(item)
return `${name}${formatNumber(stackItemTotal(item))}${unit}`
}
function stackColumnUnit(item) {
return item?.displayUnit || item?.unit || props.panel?.unit || ""
}
function showStackColumnValue() {
return props.panel?.showValue === true || props.panel?.variant === "dual"
}
function normalizeSearch(value) {
return String(value || "").trim().toLowerCase()
}
@ -1269,6 +1438,44 @@ function columnGradient(index, item = {}) {
overflow: hidden;
}
.business-chart-tooltip {
position: fixed;
z-index: 9999;
min-width: 148px;
max-width: 220px;
padding: 8px 10px;
pointer-events: none;
color: rgba(229, 252, 255, 0.9);
border: 1px solid rgba(48, 220, 255, 0.44);
background:
linear-gradient(135deg, rgba(6, 33, 49, 0.96), rgba(2, 18, 30, 0.92)),
radial-gradient(circle at 12% 0%, rgba(36, 246, 196, 0.22), transparent 54%);
box-shadow:
inset 0 0 18px rgba(48, 220, 255, 0.12),
0 0 18px rgba(48, 220, 255, 0.24);
backdrop-filter: blur(3px);
strong {
display: block;
max-width: 100%;
color: #ffffff;
font-size: 13px;
line-height: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
span {
display: block;
margin-top: 4px;
color: rgba(220, 248, 255, 0.78);
font-size: 12px;
line-height: 15px;
white-space: nowrap;
}
}
.business-panel-empty {
display: flex;
align-items: center;
@ -1951,32 +2158,32 @@ function columnGradient(index, item = {}) {
}
&.is-dense {
grid-template-columns: 126px minmax(0, 1fr);
gap: 8px;
padding: 5px 10px 5px 11px;
grid-template-columns: 148px minmax(0, 1fr);
gap: 10px;
padding: 5px 12px 6px 14px;
.business-ring-visual {
width: 126px;
height: 126px;
width: 148px;
height: 148px;
}
.business-ring-chart {
width: 122px;
height: 122px;
width: 144px;
height: 144px;
}
.business-ring-inner {
inset: 31px 27px 29px;
inset: 37px 32px 35px;
strong {
font-size: 20px;
line-height: 22px;
font-size: 22px;
line-height: 24px;
}
span {
max-width: 66px;
font-size: 10px;
line-height: 12px;
max-width: 74px;
font-size: 11px;
line-height: 13px;
}
}
@ -1985,33 +2192,34 @@ function columnGradient(index, item = {}) {
justify-content: center;
height: auto;
max-height: 100%;
gap: 3px;
gap: 5px;
}
.business-donut-row {
flex: 1 1 0;
grid-template-columns: 9px minmax(0, 1fr) max-content max-content;
gap: 4px;
min-height: 17px;
grid-template-columns: 10px minmax(0, 1fr) max-content max-content;
gap: 5px;
min-height: 21px;
padding: 2px;
i {
width: 7px;
height: 7px;
width: 8px;
height: 8px;
border-width: 1px;
}
span {
font-size: 10px;
line-height: 12px;
font-size: 12px;
line-height: 15px;
}
strong {
font-size: 13px;
line-height: 15px;
font-size: 15px;
line-height: 17px;
}
em {
font-size: 9px;
font-size: 10px;
}
}
}
@ -2569,7 +2777,7 @@ function columnGradient(index, item = {}) {
.business-capacity {
display: grid;
grid-template-rows: 22px 22px minmax(36px, 1fr) 18px;
grid-template-rows: 22px 40px minmax(34px, 1fr) 18px;
gap: 5px;
height: 100%;
padding: 8px 14px 7px;
@ -2624,8 +2832,8 @@ function columnGradient(index, item = {}) {
.business-capacity-band {
display: flex;
align-self: center;
height: 15px;
padding: 3px;
height: 32px;
padding: 5px;
border: 1px solid rgba(48, 220, 255, 0.2);
background: linear-gradient(90deg, rgba(48, 220, 255, 0.11), rgba(48, 220, 255, 0.03));
box-shadow: inset 0 0 14px rgba(48, 220, 255, 0.08);
@ -3163,6 +3371,56 @@ function columnGradient(index, item = {}) {
}
}
&.is-race-bar.is-few-rows {
justify-content: center;
gap: 13px;
padding: 15px 12px 16px 13px;
.business-bar-row {
min-height: 42px;
grid-template-columns: 112px minmax(0, 1fr) 96px;
gap: 9px;
}
.business-bar-name {
padding-left: 15px;
font-size: 12px;
line-height: 15px;
word-break: keep-all;
white-space: nowrap;
&:before {
top: 4px;
width: 8px;
height: 8px;
}
}
.business-bar-track {
height: 25px;
&:before {
top: -9px;
bottom: -9px;
}
}
.business-bar-row strong {
font-size: 15px;
line-height: 17px;
}
.business-bar-row strong em {
font-size: 11px;
}
.business-bar-row strong small {
margin-top: 2px;
font-size: 11px;
line-height: 12px;
}
}
&.is-race-bar.is-degradation {
justify-content: flex-start;
gap: 10px;
@ -3611,6 +3869,85 @@ function columnGradient(index, item = {}) {
}
}
&.is-variant-projectYear {
grid-template-columns: repeat(7, minmax(38px, 1fr));
gap: 10px;
padding: 26px 16px 10px;
&:before {
top: 47px;
bottom: 43px;
opacity: 0.42;
}
.business-column-item {
grid-template-rows: 18px minmax(74px, 1fr) 26px;
min-width: 0;
}
.business-column-item strong {
color: #ffffff;
font-size: 14px;
line-height: 18px;
text-align: center;
text-shadow: 0 0 9px rgba(255, 255, 255, 0.28);
}
.business-column-item span {
max-width: none;
color: rgba(240, 253, 255, 0.9);
font-size: 12px;
line-height: 15px;
white-space: nowrap;
-webkit-line-clamp: 1;
}
.business-column-track {
width: 28px;
height: 92px;
min-height: 74px;
border: 1px solid rgba(48, 220, 255, 0.18);
background:
linear-gradient(180deg, rgba(48, 220, 255, 0.1), rgba(48, 220, 255, 0.025)),
rgba(5, 29, 42, 0.3);
box-shadow:
inset 0 0 12px rgba(48, 220, 255, 0.08),
0 0 10px rgba(48, 220, 255, 0.08);
&:after {
content: "";
position: absolute;
left: 50%;
bottom: -6px;
width: 42px;
height: 8px;
border-radius: 50%;
background: radial-gradient(ellipse at center, rgba(48, 220, 255, 0.38), rgba(48, 220, 255, 0) 72%);
transform: translateX(-50%);
}
}
.business-column-track i {
left: 3px;
right: 3px;
min-height: 12px;
border-radius: 4px 4px 1px 1px;
box-shadow:
0 0 16px rgba(36, 246, 196, 0.36),
inset 0 1px 0 rgba(255, 255, 255, 0.26);
&:before {
height: 8px;
top: -5px;
transform: skewX(-32deg);
}
&:after {
display: none;
}
}
}
&.is-variant-compact {
grid-template-columns: repeat(auto-fit, minmax(30px, 1fr));
gap: 7px;
@ -4314,6 +4651,151 @@ function columnGradient(index, item = {}) {
width: 18px;
}
}
&.is-variant-dual {
grid-template-rows: 24px minmax(0, 1fr);
gap: 4px;
padding: 8px 14px 7px;
.business-stack-legend {
justify-content: center;
gap: 14px;
padding-right: 0;
span {
color: rgba(245, 253, 255, 0.9);
font-size: 11px;
line-height: 16px;
}
i {
width: 8px;
height: 8px;
border-radius: 50%;
}
}
.business-stack-chart {
position: relative;
grid-template-columns: repeat(auto-fit, minmax(62px, 1fr));
align-items: stretch;
gap: 14px;
padding: 1px 4px 0;
&:before {
content: "";
position: absolute;
left: 4px;
right: 4px;
top: 25px;
bottom: 31px;
background:
repeating-linear-gradient(
to bottom,
rgba(159, 220, 230, 0.15) 0 1px,
transparent 1px 18px
);
pointer-events: none;
}
}
.business-stack-item {
position: relative;
z-index: 1;
grid-template-rows: 18px minmax(62px, 1fr) 27px;
justify-items: center;
min-width: 0;
&.is-active {
.business-stack-track i {
animation: businessDataPulse 1.8s ease-in-out infinite;
}
span,
strong {
color: #ffffff;
}
}
}
.business-stack-total {
align-self: end;
color: rgba(245, 253, 255, 0.9);
font-family: D-DIN, Arial, sans-serif;
font-size: 11px;
line-height: 16px;
white-space: nowrap;
text-shadow: 0 0 8px rgba(255, 255, 255, 0.22);
em {
margin-left: 2px;
color: rgba(245, 253, 255, 0.68);
font-family: "PingFang SC", Arial, sans-serif;
font-size: 9px;
font-style: normal;
font-weight: 600;
}
}
.business-stack-track {
position: relative;
align-self: end;
width: 25px;
height: 100%;
min-height: 58px;
max-height: 94px;
overflow: visible;
border: 1px solid rgba(48, 220, 255, 0.14);
background:
linear-gradient(180deg, rgba(48, 220, 255, 0.09), rgba(48, 220, 255, 0.025)),
rgba(5, 30, 42, 0.24);
box-shadow: inset 0 0 10px rgba(48, 220, 255, 0.05);
&:after {
content: "";
position: absolute;
left: 50%;
bottom: -6px;
width: 42px;
height: 8px;
border-radius: 50%;
background: radial-gradient(ellipse at center, rgba(48, 220, 255, 0.34), rgba(48, 220, 255, 0) 72%);
transform: translateX(-50%);
pointer-events: none;
}
i {
position: relative;
display: block;
min-height: 3px;
box-shadow:
0 0 14px color-mix(in srgb, currentColor 44%, transparent),
inset 0 1px 0 rgba(255, 255, 255, 0.24);
&:last-child:before {
content: "";
position: absolute;
left: 0;
right: 0;
top: -5px;
height: 8px;
background: inherit;
filter: brightness(1.22);
transform: skewX(-32deg);
transform-origin: left bottom;
}
}
}
.business-stack-item span {
max-width: 60px;
color: rgba(250, 254, 255, 0.82);
font-size: 11px;
line-height: 13px;
white-space: nowrap;
-webkit-line-clamp: 1;
}
}
}
.business-stack-legend {

@ -1,7 +1,12 @@
<template>
<div class="business-stat-list" :class="{ 'is-compact': compact, 'is-dense': rows.length > 10 }">
<div v-if="!rows.length" class="business-stat-empty">暂无业务统计数据</div>
<div v-for="(item, index) in rows" :key="item.key || item.name" class="business-stat-row">
<div
v-for="(item, index) in rows"
:key="item.key || item.name"
class="business-stat-row"
:data-chart-tooltip="rowTooltip(item)"
>
<div class="business-stat-rank">{{ index + 1 }}</div>
<div class="business-stat-main">
<div class="business-stat-name">{{ item.name }}</div>
@ -42,6 +47,14 @@ function barWidth(item) {
if (Number.isNaN(value)) return 36
return Math.max(8, Math.round((value / maxValue.value) * 100))
}
function rowTooltip(item) {
const value = item?.value === undefined || item?.value === null || item?.value === "" ? "--" : item.value
const unit = item?.unit || ""
const lines = [`数值:${value}${unit}`]
if (item?.extra) lines.push(String(item.extra))
return [item?.name || "数据", ...lines].join("\n")
}
</script>
<style lang="scss">

@ -4,6 +4,7 @@
ref="mapSceneRef"
:active-layer="activeLayer"
:map-ranking="mapRanking"
:admin-boundary-line-scale="2.4"
@feature-loading="featureLoading = $event"
@feature-info="handleFeatureInfo"
@layer-status="handleLayerStatus"

@ -68,6 +68,12 @@ const INTRO_MAP_CAMERA_POSITION = new Vector3(-13.767695123014105, 12.9901521630
const CUSTOM_SATELLITE_MAX_ZOOM = 21
const DYNAMIC_SATELLITE_TILE_WINDOW = 9
const DYNAMIC_SATELLITE_CHECK_INTERVAL = 260
const SELECTED_FEATURE_FILL = "#FF2D2D"
const SELECTED_FEATURE_STROKE = "#FFECEC"
const SELECTED_FEATURE_GLOW = "#FF4A3D"
const SELECTED_FEATURE_FILL_HEX = 0xff2d2d
const SELECTED_FEATURE_STROKE_HEX = 0xffecec
const SELECTED_FEATURE_GLOW_HEX = 0xff4a3d
function sortByValue(data) {
data.sort((a, b) => b.value - a.value)
return data
@ -183,6 +189,8 @@ export class World extends Mini3d {
this.callbacks = callbacks
this.tileUrlTemplate = callbacks.tileUrlTemplate || ""
this.usesCustomSatelliteTiles = Boolean(this.tileUrlTemplate)
this.adminBoundaryLineScale = this.normalizeAdminBoundaryLineScale(callbacks.adminBoundaryLineScale)
this.mapBoundaryEmphasisMaterial = null
this.dynamicSatelliteMaxZoom = CUSTOM_SATELLITE_MAX_ZOOM
this.dynamicSatelliteTileWindow = DYNAMIC_SATELLITE_TILE_WINDOW
this.dynamicSatelliteOverlay = null
@ -392,11 +400,22 @@ export class World extends Mini3d {
duration: 0.5,
delay: 0.3,
opacity: 1,
},
"focusMapOpacity"
)
tl.to(
this.rotateBorder1.scale,
},
"focusMapOpacity"
)
if (this.mapBoundaryEmphasisMaterial) {
tl.to(
this.mapBoundaryEmphasisMaterial,
{
duration: 0.5,
delay: 0.3,
opacity: this.getAdminBoundaryEmphasisOpacity(),
},
"focusMapOpacity"
)
}
tl.to(
this.rotateBorder1.scale,
{
delay: 0.3,
duration: 1,
@ -556,14 +575,15 @@ export class World extends Mini3d {
this.mapGroup = mapGroup
this.focusMapGroup = focusMapGroup
let { china, chinaTopLine } = this.createChina()
let { map, mapTop, mapLine } = this.createProvince()
let { map, mapTop, mapLine, mapBoundaryEmphasis } = this.createProvince()
china.setParent(mapGroup)
chinaTopLine.setParent(mapGroup)
// 创建扩散
this.createDiffuse()
map.setParent(focusMapGroup)
mapTop.setParent(focusMapGroup)
map.setParent(focusMapGroup)
mapTop.setParent(focusMapGroup)
mapLine.setParent(focusMapGroup)
mapBoundaryEmphasis?.setParent(focusMapGroup)
focusMapGroup.position.set(0, 0, -0.01)
focusMapGroup.scale.set(1, 1, 0)
mapGroup.add(focusMapGroup)
@ -815,8 +835,8 @@ export class World extends Mini3d {
uColor2: 0x0b8fa9,
})
}
this.defaultMaterial = faceMaterial
this.defaultLightMaterial = this.defaultMaterial.clone()
this.defaultMaterial = faceMaterial
this.defaultLightMaterial = this.defaultMaterial.clone()
this.defaultLightMaterial.color = new Color("rgba(128,244,255,1)")
this.defaultLightMaterial.opacity = 0.9
// this.defaultLightMaterial.emissive.setHex(new Color("rgba(115,208,255,1)"));
@ -839,26 +859,64 @@ export class World extends Mini3d {
}
})
})
this.mapLineMaterial = new LineBasicMaterial({
this.mapLineMaterial = new LineBasicMaterial({
color: 0xbafcff,
opacity: 0,
transparent: true,
fog: false,
})
transparent: true,
linewidth: this.adminBoundaryLineScale,
fog: false,
})
let mapLine = new Line(this, {
geoProjectionCenter: this.geoProjectionCenter,
geoProjectionScale: this.geoProjectionScale,
data: mapJsonData,
material: this.mapLineMaterial,
renderOrder: 3,
})
mapLine.lineGroup.position.z += this.depth + 0.23
})
mapLine.lineGroup.position.z += this.depth + 0.23
let mapBoundaryEmphasis = null
const emphasisRadius = this.getAdminBoundaryEmphasisRadius()
if (emphasisRadius > 0) {
this.mapBoundaryEmphasisMaterial = new MeshBasicMaterial({
color: 0x92fbff,
transparent: true,
opacity: 0,
depthTest: false,
depthWrite: false,
fog: false,
blending: AdditiveBlending,
})
mapBoundaryEmphasis = new Line(this, {
geoProjectionCenter: this.geoProjectionCenter,
geoProjectionScale: this.geoProjectionScale,
data: mapJsonData,
material: this.mapBoundaryEmphasisMaterial,
renderOrder: this.usesCustomSatelliteTiles ? 24 : 18,
type: "Line3",
tubeRadius: emphasisRadius,
})
mapBoundaryEmphasis.lineGroup.position.z += this.depth + 0.255
}
return {
map,
mapTop,
mapLine,
mapBoundaryEmphasis,
}
}
normalizeAdminBoundaryLineScale(value) {
const scale = Number(value)
if (!Number.isFinite(scale)) return 1
return Math.min(4, Math.max(1, scale))
}
getAdminBoundaryEmphasisRadius() {
if (this.adminBoundaryLineScale <= 1) return 0
return Math.min(0.04, 0.011 * this.adminBoundaryLineScale)
}
getAdminBoundaryEmphasisOpacity() {
if (this.adminBoundaryLineScale <= 1) return 0
return Math.min(0.5, 0.2 + this.adminBoundaryLineScale * 0.08)
}
createTopicLayer() {
if (!this.mapJsonData) return
this.initHongyuanBoundaryPolygons()
@ -1541,19 +1599,19 @@ export class World extends Mini3d {
if (!features.length && !analysisFeatures.length && !boundary && !drawPoints.length) return
const quietProjectLayer = this.isEcologicalProjectLayer(this.currentTopicLayer)
const glowStyle = {
fill: "#FFE15A",
stroke: "#24F6C4",
polygonAlpha: quietProjectLayer ? 0.14 : 0.24,
lineAlpha: quietProjectLayer ? 0.72 : 0.98,
fill: SELECTED_FEATURE_FILL,
stroke: SELECTED_FEATURE_GLOW,
polygonAlpha: quietProjectLayer ? 0.2 : 0.28,
lineAlpha: quietProjectLayer ? 0.78 : 0.98,
lineWidth: quietProjectLayer ? 6 : 12,
pointRadius: 11,
glow: !quietProjectLayer,
halo: !quietProjectLayer,
}
const focusStyle = {
fill: "#FFF06A",
stroke: "#FFFDF2",
polygonAlpha: quietProjectLayer ? 0.28 : 0.46,
fill: SELECTED_FEATURE_FILL,
stroke: SELECTED_FEATURE_STROKE,
polygonAlpha: quietProjectLayer ? 0.34 : 0.52,
lineAlpha: quietProjectLayer ? 0.94 : 1,
lineWidth: quietProjectLayer ? 3.2 : 5,
pointRadius: 6.5,
@ -2446,6 +2504,7 @@ export class World extends Mini3d {
showBars: ranking?.showBars ?? false,
showPointMarkers: ranking?.showPointMarkers ?? calloutMode,
onlyRankedTownLabels: ranking?.onlyRankedTownLabels ?? false,
showMissingTownStats: ranking?.showMissingTownStats === true,
}
}
getTownRankingRows(ranking = this.currentTownRanking, limit = 7, useFallback = true) {
@ -3103,7 +3162,7 @@ export class World extends Mini3d {
const rows = this.getTownRankingRows(ranking, Number.POSITIVE_INFINITY, false)
const meta = this.getTownRankingMeta(ranking)
const rowByName = new Map(rows.map((row) => [normalizeTownName(row.name), row]))
const canShowZeroStat = Boolean(ranking && rows.length && (meta.metricLabel || meta.unit || meta.title))
const canShowMissingStat = Boolean(meta.showMissingTownStats && ranking && rows.length && (meta.metricLabel || meta.unit || meta.title))
this.townNameLabels.forEach((label) => {
const townName = label.userData?.townName || label.userData?.name
const rankedRow = rowByName.get(normalizeTownName(townName))
@ -3112,7 +3171,7 @@ export class World extends Mini3d {
label.visible = false
return
}
const row = rankedRow || (canShowZeroStat
const row = rankedRow || (canShowMissingStat
? { name: townName, value: 0, unit: meta.unit, metricLabel: meta.metricLabel, rank: null }
: null)
if (!row) {
@ -4781,16 +4840,16 @@ export class World extends Mini3d {
getSelectedFeatureStyle(options = {}) {
const isPasture = options.mode === "pasture"
return {
fillColor: isPasture ? 0xffd34d : 0xffe15a,
fillOpacity: isPasture ? 0.42 : 0.5,
lineColor: 0xffffff,
fillColor: SELECTED_FEATURE_FILL_HEX,
fillOpacity: isPasture ? 0.46 : 0.54,
lineColor: SELECTED_FEATURE_STROKE_HEX,
lineOpacity: 0.98,
glowColor: isPasture ? 0x23f9d0 : 0x24f6c4,
glowColor: SELECTED_FEATURE_GLOW_HEX,
glowOpacity: isPasture ? 0.92 : 0.96,
renderOrderBase: isPasture ? 42 : 35,
boundary: isPasture,
boundaryColor: 0xfff06a,
boundaryGlowColor: 0x24f6c4,
boundaryColor: SELECTED_FEATURE_FILL_HEX,
boundaryGlowColor: SELECTED_FEATURE_GLOW_HEX,
boundaryWidth: 0.018,
boundaryGlowWidth: 0.06,
boundaryOpacity: 0.95,

@ -19,6 +19,10 @@ const props = defineProps({
type: String,
default: "",
},
adminBoundaryLineScale: {
type: Number,
default: 1,
},
});
const emit = defineEmits(["feature-loading", "feature-info", "layer-status", "tile-status", "draw-boundary", "scene-mounted"]);
const canvasRef = ref(null);
@ -47,6 +51,7 @@ function loadMap(assets) {
onTileStatus: (status) => emit("tile-status", status),
onDrawBoundary: (payload) => emit("draw-boundary", payload),
tileUrlTemplate: props.tileUrlTemplate,
adminBoundaryLineScale: props.adminBoundaryLineScale,
});
canvasMap.value.time.pause();
activeLayerSignature = getLayerSignature(props.activeLayer);
@ -303,6 +308,7 @@ function getMapRankingSignature(ranking) {
ranking.showTownNames ?? "",
ranking.showMapLabels ?? "",
ranking.onlyRankedTownLabels ?? "",
ranking.showMissingTownStats ?? "",
ranking.rows?.length || 0,
ranking.rows?.map((row) => `${row.key || row.name || ""}:${row.value ?? ""}`).join(",") || "",
].join("|");

@ -501,10 +501,10 @@ export class World extends Mini3d {
uColor1: 0x12bbe0,
uColor2: 0x0094b5,
})
this.defaultMaterial = faceMaterial
this.defaultLightMaterial = this.defaultMaterial.clone()
this.defaultLightMaterial.color = new Color("rgba(115,208,255,1)")
this.defaultLightMaterial.opacity = 0.8
this.defaultMaterial = faceMaterial
this.defaultLightMaterial = this.defaultMaterial.clone()
this.defaultLightMaterial.color = new Color("rgba(115,208,255,1)")
this.defaultLightMaterial.opacity = 0.8
// this.defaultLightMaterial.emissive.setHex(new Color("rgba(115,208,255,1)"));
// this.defaultLightMaterial.emissiveIntensity = 3.5;
let mapTop = new BaseMap(this, {

@ -1,6 +1,3 @@
import { grasslandYakRows } from "@/views/gdMap/livestockData"
import { getHongyuanTownshipCodeByName } from "@/config/townshipBoundaries"
export const BALANCE_CALCULATION_PARAMS = {
grasslandCapacityPerMu: 0.06229833103624831,
muPerYak: 16.051794379822947,
@ -17,26 +14,18 @@ export const COUNTY_TARGET = {
export const RISK_LEVELS = [
{
key: "healthy",
label: "合理",
shortLabel: "合理",
range: "< 0%",
description: "承载力高于实际载畜量,当前放牧压力处于合理区间。",
actions: ["保持当前载畜水平与轮牧节奏", "定期复测草地生产力和载畜量", "关注极端天气对牧草生长的影响"],
},
{
key: "balanced",
label: "基本实现草畜平衡",
label: "草畜平衡",
shortLabel: "平衡",
range: "0% - 5%",
description: "超载率在5%以内,基本实现草畜平衡。",
actions: ["保持当前载畜水平与轮牧节奏", "持续跟踪草地生产力和载畜量变化", "关注局部草场季节性放牧压力"],
range: "≤ 0%",
description: "承载力覆盖实际载畜量,当前放牧压力处于合理区间。",
actions: ["保持当前载畜水平与轮牧节奏", "定期复测草地生产力和载畜量", "关注极端天气对牧草生长的影响"],
},
{
key: "watch",
label: "轻度超载",
shortLabel: "关注",
range: "> 5% - 10%",
description: "超载率超过5%,已出现轻度超载迹象,建议提前介入调控。",
range: "0% - 10%",
description: "已出现轻度超载迹象,建议提前介入调控。",
actions: ["适度压减超载畜群规模", "延长冬春舍饲补饲周期", "增加人工草场补饲供应"],
},
{
@ -74,18 +63,7 @@ const round = (value, decimals = 2) => {
return Math.round((Number(value) || 0) * factor) / factor
}
const townPressureFactorByName = {
色地镇: 1.38,
麦洼乡: 1.26,
阿木乡: 1.12,
瓦切镇: 1.2,
查尔玛乡: 1.04,
江茸乡: 0.88,
龙日镇: 1.16,
安曲镇: 1.28,
刷经寺镇: 0.78,
邛溪镇: 1.08,
}
const CXPT_BALANCE_ADVICE = "加强人工饲草饲料基地建设;购买饲草饲料,增加饲草饲料供应量;实行舍饲圈养,减轻草原放牧压力;加快牲畜出栏,优化畜群结构。"
export function toNumber(value, fallback = null) {
if (value === undefined || value === null || value === "") return fallback
@ -120,11 +98,10 @@ export function formatPercent(value) {
export function getRiskLevel(value) {
const rate = toNumber(value)
if (rate === null || rate < 0) return RISK_LEVELS[0]
if (rate <= 5) return RISK_LEVELS[1]
if (rate <= 10) return RISK_LEVELS[2]
if (rate <= 20) return RISK_LEVELS[3]
return RISK_LEVELS[4]
if (rate === null || rate <= 0) return RISK_LEVELS[0]
if (rate <= 10) return RISK_LEVELS[1]
if (rate <= 20) return RISK_LEVELS[2]
return RISK_LEVELS[3]
}
function calculateLivestockCapacity(yakCount) {
@ -176,39 +153,31 @@ export function buildBalanceRow({
}
}
export const balanceTownRows = grasslandYakRows.map((item, index) => buildBalanceRow({
key: `town-${item.zoneName}`,
type: "town",
name: item.zoneName,
areaCode: getHongyuanTownshipCodeByName(item.zoneName),
areaRange: item.grasslandArea,
yakNumber: item.yakCount,
location: null,
pressureFactor: townPressureFactorByName[item.zoneName] || 1,
index,
}))
const countyAreaRange = balanceTownRows.reduce((sum, item) => sum + item.areaRange, 0)
const countyYakNumber = balanceTownRows.reduce((sum, item) => sum + item.yakNumber, 0)
const countyBearingCapacity = balanceTownRows.reduce((sum, item) => sum + item.bearingCapacity, 0)
const countyLivestockCapacity = balanceTownRows.reduce((sum, item) => sum + item.livestockCapacity, 0)
const countyOverloadRate = countyBearingCapacity
? round(((countyLivestockCapacity - countyBearingCapacity) / countyBearingCapacity) * 100, 1)
: 0
const countyRisk = getRiskLevel(countyOverloadRate)
export const countyBalanceRow = {
...COUNTY_TARGET,
areaRange: countyAreaRange,
yakNumber: countyYakNumber,
bearingCapacity: countyBearingCapacity,
livestockCapacity: countyLivestockCapacity,
overloadRate: countyOverloadRate,
level: countyRisk.label,
advice: countyRisk.actions.join(";"),
areaRange: 7568515.06,
yakNumber: 1364767,
bearingCapacity: 471506,
livestockCapacity: 474995,
overloadRate: 0.74,
level: "轻度超载",
advice: CXPT_BALANCE_ADVICE,
year: "2026",
}
export const balanceTownRows = [
{ key: "town-邛溪镇", type: "town", name: "邛溪镇", areaCode: "513233100", areaRange: 930604.2, yakNumber: 81698, bearingCapacity: 49006, livestockCapacity: 50461, overloadRate: 2.97, level: "轻度超载", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-刷经寺镇", type: "town", name: "刷经寺镇", areaCode: "513233101", areaRange: 743557.5, yakNumber: 1264, bearingCapacity: 29082, livestockCapacity: 29536, overloadRate: 1.56, level: "轻度超载", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-安曲镇", type: "town", name: "安曲镇", areaCode: "513233103", areaRange: 900696.75, yakNumber: 138268, bearingCapacity: 65850, livestockCapacity: 67253, overloadRate: 2.13, level: "轻度超载", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-龙日镇", type: "town", name: "龙日镇", areaCode: "513233105", areaRange: 628303.8, yakNumber: 66822, bearingCapacity: 36216, livestockCapacity: 36886, overloadRate: 1.85, level: "轻度超载", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-江茸乡", type: "town", name: "江茸乡", areaCode: "513233202", areaRange: 496888.95, yakNumber: 34575, bearingCapacity: 26567, livestockCapacity: 26859, overloadRate: 1.1, level: "轻度超载", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-查尔玛乡", type: "town", name: "查尔玛乡", areaCode: "513233203", areaRange: 808628.4, yakNumber: 47941, bearingCapacity: 43160, livestockCapacity: 43958, overloadRate: 1.85, level: "轻度超载", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-瓦切镇", type: "town", name: "瓦切镇", areaCode: "513233204", areaRange: 747837.6, yakNumber: 121654, bearingCapacity: 65410, livestockCapacity: 66751, overloadRate: 2.05, level: "轻度超载", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-阿木乡", type: "town", name: "阿木乡", areaCode: "513233205", areaRange: 863346, yakNumber: 64165, bearingCapacity: 44369, livestockCapacity: 45589, overloadRate: 2.75, level: "轻度超载", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-麦洼乡", type: "town", name: "麦洼乡", areaCode: "513233207", areaRange: 458338.35, yakNumber: 78465, bearingCapacity: 55321, livestockCapacity: 55974, overloadRate: 1.18, level: "轻度超载", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-色地镇", type: "town", name: "色地镇", areaCode: "513233208", areaRange: 991265.25, yakNumber: 131956, bearingCapacity: 69233, livestockCapacity: 70715, overloadRate: 2.14, level: "轻度超载", advice: CXPT_BALANCE_ADVICE, year: "2026" },
]
export const balanceFallbackRows = [
countyBalanceRow,
...balanceTownRows,
@ -216,11 +185,11 @@ export const balanceFallbackRows = [
export function normalizeBalanceResult(source = {}, fallback = {}) {
const hasSourceResult = source && typeof source === "object" && Object.keys(source).length > 0
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 rawAreaRange = pickValue(source, fieldMap.areaRange, fallback.areaRange)
const rawYakNumber = pickValue(source, fieldMap.yakNumber, fallback.yakNumber)
const rawBearingCapacity = pickValue(source, fieldMap.bearingCapacity, fallback.bearingCapacity)
const rawLivestockCapacity = pickValue(source, fieldMap.livestockCapacity, fallback.livestockCapacity)
const directRate = toNumber(pickValue(source, fieldMap.overloadRate, fallback.overloadRate))
const fallbackYakNumber = toNumber(fallback.yakNumber)
const legacyLivestockCapacity = getLegacyLivestockCapacity(source, rawLivestockCapacity, fallbackYakNumber)
const areaRange = toNumber(rawAreaRange, fallback.areaRange ?? 0)
@ -232,22 +201,24 @@ export function normalizeBalanceResult(source = {}, fallback = {}) {
const sourceHasArea = rawAreaRange !== undefined && rawAreaRange !== null && rawAreaRange !== ""
const sourceHasYakNumber = rawYakNumber !== undefined && rawYakNumber !== null && rawYakNumber !== ""
const sourceLivestock = toNumber(rawLivestockCapacity) ?? legacyLivestockCapacity
const sourceBearing = toNumber(rawBearingCapacity)
const sourceBearingCandidate = toNumber(rawBearingCapacity)
const inferredSourceBearing = inferBearingCapacity(sourceLivestock, directRate)
const sourceBearing = resolveBearingCapacity(sourceBearingCandidate, inferredSourceBearing)
const livestockCapacity = hasSourceResult
? sourceLivestock
?? inferLivestockCapacity(sourceBearing, directRate)
?? fallbackLivestock
?? inferLivestockCapacity(fallbackBearing, directRate)
?? (sourceHasYakNumber ? calculateLivestockCapacity(yakNumber) : null)
: fallbackLivestock ?? (sourceHasYakNumber ? calculateLivestockCapacity(yakNumber) : null)
: fallbackLivestock ?? inferLivestockCapacity(fallbackBearing, directRate) ?? (sourceHasYakNumber ? calculateLivestockCapacity(yakNumber) : null)
const bearingCapacity = hasSourceResult
? sourceBearing
?? inferBearingCapacity(livestockCapacity, directRate)
?? fallbackBearing
?? (sourceHasArea ? calculateBearingCapacity(areaRange) : null)
: fallbackBearing ?? (sourceHasArea ? calculateBearingCapacity(areaRange) : null)
: fallbackBearing ?? inferBearingCapacity(livestockCapacity, directRate) ?? (sourceHasArea ? calculateBearingCapacity(areaRange) : null)
const overloadRate = directRate !== null
? round(directRate, 1)
? round(directRate, 2)
: bearingCapacity
&& livestockCapacity !== null
? round(((livestockCapacity - bearingCapacity) / bearingCapacity) * 100, 1)
@ -294,6 +265,13 @@ function inferBearingCapacity(livestockCapacity, overloadRate) {
return Math.round(livestock / (1 + rate / 100))
}
function resolveBearingCapacity(sourceBearing, inferredBearing) {
if (sourceBearing === null) return inferredBearing
if (inferredBearing === null) return sourceBearing
const diffRatio = Math.abs(sourceBearing - inferredBearing) / Math.max(Math.abs(inferredBearing), 1)
return diffRatio > 0.25 ? inferredBearing : sourceBearing
}
function inferLivestockCapacity(bearingCapacity, overloadRate) {
const bearing = toNumber(bearingCapacity)
const rate = toNumber(overloadRate)
@ -325,7 +303,7 @@ export function deriveBalance(row = {}) {
yakSheepUnit,
pressureFactor,
bearingCoefficient,
overloadRate: overloadRate === null ? null : round(overloadRate, 1),
overloadRate: overloadRate === null ? null : round(overloadRate, 2),
delta,
usageRate,
risk,
@ -341,7 +319,7 @@ export function splitAdvice(row = {}, risk = getRiskLevel(row.overloadRate)) {
: []
const unsuitableWhenOverloaded = ["提高放牧畜禽的密度", "增加草地的利用率", "密度不能过高"]
const outdatedSafeAdvice = ["轻度超载", "已出现超载", "超载迹象", "压减超载畜群规模"]
const safeRisk = ["healthy", "balanced"].includes(risk.key)
const safeRisk = risk.key === "healthy"
const cleanedApiPoints = apiPoints.filter((item) => {
if (unsuitableWhenOverloaded.some((keyword) => item.includes(keyword))) return false
if (safeRisk && outdatedSafeAdvice.some((keyword) => item.includes(keyword))) return false

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save