feat: update screen v2 production build

V2.0
M先生 1 month ago
parent 1279b948d2
commit 1e5dd1da27
  1. 0
      docs/assets/AlibabaPuHuiTi-14435b65-balabala-1783349984056.woff2
  2. 0
      docs/assets/AlimamaShuHeiTi-Bold-6401fd0d-balabala-1783349984056.ttf
  3. 0
      docs/assets/AlimamaShuHeiTi-Bold-a95023f2-balabala-1783349984056.woff
  4. 0
      docs/assets/AlimamaShuHeiTi-Bold-fa4a258d-balabala-1783349984056.woff2
  5. 2
      docs/assets/SmartChartRenderer-6f0457ba-1783298613872.js
  6. 0
      docs/assets/SmartChartRenderer-8fd37aa6-balabala-1783349984056.css
  7. 2
      docs/assets/SmartChartRenderer-c426275a-1783349984056.js
  8. 0
      docs/assets/area-b57940eb-balabala-1783349984056.png
  9. 0
      docs/assets/bg-3acb94e3-balabala-1783349984056.png
  10. 0
      docs/assets/bottom-menu-arrow-big-180b7071-balabala-1783349984056.svg
  11. 0
      docs/assets/bottom-menu-arrow-small-19304179-balabala-1783349984056.svg
  12. 0
      docs/assets/bottom-menu-bg-b465793d-balabala-1783349984056.png
  13. 0
      docs/assets/bottom-menu-btn-ee5c8064-balabala-1783349984056.png
  14. 0
      docs/assets/bottom-menu-btn-hover-df33e514-balabala-1783349984056.png
  15. 6
      docs/assets/chartTheme-259740ff-1783349984056.js
  16. 0
      docs/assets/chartTheme-812dab15-balabala-1783349984056.css
  17. 0
      docs/assets/chinaBlurLine-1a2cbc0f-balabala-1783349984056.png
  18. 0
      docs/assets/content-bottom-left-arrow-3a8a61f3-balabala-1783349984056.svg
  19. 0
      docs/assets/content-bottom-right-arrow-a3c5270d-balabala-1783349984056.svg
  20. 0
      docs/assets/content-middle-line-162bc0d5-balabala-1783349984056.svg
  21. 0
      docs/assets/focus_bg-b6803e2d-balabala-1783349984056.png
  22. 0
      docs/assets/focus_move_bg-cf41417a-balabala-1783349984056.png
  23. 0
      docs/assets/header-bg-65b38012-balabala-1783349984056.png
  24. 0
      docs/assets/huiguang-3c5dfda5-balabala-1783349984056.png
  25. 0
      docs/assets/icon1-6f7c222f-balabala-1783349984056.png
  26. 0
      docs/assets/icon2-4f77ccec-balabala-1783349984056.png
  27. 48
      docs/assets/index-1783349984056.js
  28. 1
      docs/assets/index-26f276ad-balabala-1783349984056.css
  29. 1
      docs/assets/index-36e36714-balabala-1783298613872.css
  30. 2
      docs/assets/index-379df943-1783349984056.js
  31. 0
      docs/assets/index-47d6f37a-balabala-1783349984056.css
  32. 2
      docs/assets/index-740e65e9-1783349984056.js
  33. 0
      docs/assets/index-8d752fbe-balabala-1783349984056.css
  34. 2
      docs/assets/index-bb092357-balabala-1783349984056.css
  35. 326
      docs/assets/index-dfc71065-1783349984056.js
  36. 0
      docs/assets/leaf-7be9efef-balabala-1783349984056.png
  37. 0
      docs/assets/left-kuang-5d6bd1ae-balabala-1783349984056.svg
  38. 0
      docs/assets/live-scene-1-786cdd0a-balabala-1783349984056.png
  39. 0
      docs/assets/menu-btn-91a961cf-balabala-1783349984056.png
  40. 0
      docs/assets/menu-btn-hover-62eba0bb-balabala-1783349984056.png
  41. 0
      docs/assets/ocean-bg-8c848b8f-balabala-1783349984056.png
  42. 0
      docs/assets/park-ee0533e0-balabala-1783349984056.png
  43. 0
      docs/assets/pathLine2-d4134d4b-balabala-1783349984056.png
  44. 0
      docs/assets/right-kuang-b1937caf-balabala-1783349984056.svg
  45. 0
      docs/assets/ring2-a1b91808-balabala-1783349984056.png
  46. 0
      docs/assets/ring3-fe0f60d7-balabala-1783349984056.png
  47. 0
      docs/assets/ring4-f2e324d6-balabala-1783349984056.png
  48. 0
      docs/assets/rotationBorder1-2ce8e0ef-balabala-1783349984056.png
  49. 0
      docs/assets/rotationBorder2-7496b968-balabala-1783349984056.png
  50. 0
      docs/assets/saoguang-ccf84a88-balabala-1783349984056.svg
  51. 0
      docs/assets/stack-7a426057-balabala-1783349984056.png
  52. 0
      docs/assets/title-zs1-c15a5ee8-balabala-1783349984056.svg
  53. 0
      docs/assets/town-f8c0c9cc-balabala-1783349984056.png
  54. 0
      docs/assets/yield-66e9af79-balabala-1783349984056.png
  55. 4
      docs/index.html
  56. 23
      src/services/auth.js
  57. 36
      src/services/openApi.js
  58. 6
      src/services/pastureApi.js
  59. 14
      src/views/ecologicalProtection/data.js
  60. 328
      src/views/forestGrassWet/components/BusinessPanelRenderer.vue
  61. 156
      src/views/forestGrassWet/components/FeatureInfoPanel.vue
  62. 82
      src/views/forestGrassWet/components/GrasslandNdviTimeline.vue
  63. 12
      src/views/forestGrassWet/map.js
  64. 26
      src/views/grassLivestockBalance/data.js
  65. 12
      src/views/grassLivestockBalance/index.vue
  66. 2
      src/views/home/data.js
  67. 14
      src/views/home/index.vue
  68. 72
      src/views/pastureManagement/data.js
  69. 228
      src/views/pastureManagement/index.vue
  70. 36
      src/views/wisdomManagement/components/SmartChartRenderer.vue
  71. 250
      src/views/wisdomManagement/index.vue
  72. 18
      src/views/yakIndustryChain/index.vue
  73. 218
      src/views/yakManagement/YakOlMap.vue
  74. 25
      src/views/yakManagement/index.vue

@ -1,2 +0,0 @@
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-1783298613872.js";import{aS as M}from"./chartTheme-6ff1ec31-1783298613872.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};

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

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 93 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
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-1783298613872.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-1783349984056.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};

@ -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-1783298613872.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-1783349984056.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%}}

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

@ -1,6 +1,19 @@
const AUTH_COOKIE_NAME = "Authorization"
const COOKIE_PATHS = ["/", "/screen2", "/screen2/"]
function getCookieDomainCandidates() {
const hostname = window.location.hostname
if (!hostname || /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) return hostname ? [hostname] : []
const parts = hostname.split(".").filter(Boolean)
const domains = new Set([hostname, `.${hostname}`])
if (parts.length > 2) {
const parentDomain = parts.slice(-2).join(".")
domains.add(parentDomain)
domains.add(`.${parentDomain}`)
}
return Array.from(domains)
}
function encodeRedirect(path) {
return encodeURIComponent(path || "/platform")
}
@ -14,14 +27,14 @@ export function getAuthorizationToken() {
}
export function clearAuthorizationToken() {
const hostname = window.location.hostname
const domains = getCookieDomainCandidates()
COOKIE_PATHS.forEach((path) => {
document.cookie = `${AUTH_COOKIE_NAME}=; Max-Age=0; path=${path}`
document.cookie = `${AUTH_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}`
if (hostname) {
document.cookie = `${AUTH_COOKIE_NAME}=; Max-Age=0; path=${path}; domain=${hostname}`
document.cookie = `${AUTH_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}; domain=${hostname}`
}
domains.forEach((domain) => {
document.cookie = `${AUTH_COOKIE_NAME}=; Max-Age=0; path=${path}; domain=${domain}`
document.cookie = `${AUTH_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}; domain=${domain}`
})
})
}

@ -50,6 +50,19 @@ function formatNumber(value) {
return Number.isInteger(number) ? number : Number(number.toFixed(2))
}
function normalizeAreaPartsToTotal(total, primary, secondary) {
const totalNumber = toNumber(total)
const primaryNumber = toNumber(primary)
const secondaryNumber = toNumber(secondary)
if (totalNumber === undefined || primaryNumber === undefined || secondaryNumber === undefined) {
return [formatNumber(primary), formatNumber(secondary)]
}
const sourceTotal = primaryNumber + secondaryNumber
if (!sourceTotal) return [0, 0]
const normalizedPrimary = formatNumber((primaryNumber / sourceTotal) * totalNumber)
return [normalizedPrimary, formatNumber(Math.max(totalNumber - normalizedPrimary, 0))]
}
function percentOf(value, total) {
const number = toNumber(value)
const totalNumber = toNumber(total)
@ -116,24 +129,30 @@ function normalizeWoodlandDistribution(data) {
function normalizeBasicGrassland(data) {
const overview = data?.overview || data?.summary || data || {}
const unit = pickValue(overview, ["areaUnit", "area_unit"]) || "万亩"
const [basicArea, generalArea] = normalizeAreaPartsToTotal(
CXPT_GRASSLAND_AREA_WAN_MU,
pickValue(overview, ["basicAreaWanMu", "basic_area_wan_mu", "basicArea", "basic_area"]),
pickValue(overview, ["generalAreaWanMu", "general_area_wan_mu", "generalArea", "general_area"]),
)
return [
{
key: "grassland-total-area",
name: "草地面积",
value: CXPT_GRASSLAND_AREA_WAN_MU,
unit: pickValue(overview, ["areaUnit", "area_unit"]) || "万亩",
unit,
},
{
key: "basic-grassland-area",
name: "基本草原",
value: formatNumber(pickValue(overview, ["basicAreaWanMu", "basic_area_wan_mu", "basicArea", "basic_area"])),
unit: pickValue(overview, ["areaUnit", "area_unit"]) || "万亩",
value: basicArea,
unit,
},
{
key: "general-grassland-area",
name: "一般草原",
value: formatNumber(pickValue(overview, ["generalAreaWanMu", "general_area_wan_mu", "generalArea", "general_area"])),
unit: pickValue(overview, ["areaUnit", "area_unit"]) || "万亩",
value: generalArea,
unit,
},
]
}
@ -1445,10 +1464,11 @@ function buildGrasslandDegradationFocusRows(rows = []) {
function buildFallbackTopicStats(topicKey, layerKey) {
if (topicKey === "grassland") {
const [fallbackBasicGrasslandArea, fallbackGeneralGrasslandArea] = normalizeAreaPartsToTotal(CXPT_GRASSLAND_AREA_WAN_MU, 637.13, 119.31)
const core = ensureCoreShape([
{ key: "grassland-area", name: "草地面积", value: CXPT_GRASSLAND_AREA_WAN_MU, unit: "万亩" },
{ key: "basic-grassland", name: "基本草原", value: 637.13, unit: "万亩" },
{ key: "general-grassland", name: "一般草原", value: 119.31, unit: "万亩" },
{ key: "basic-grassland", name: "基本草原", value: fallbackBasicGrasslandArea, unit: "万亩" },
{ key: "general-grassland", name: "一般草原", value: fallbackGeneralGrasslandArea, unit: "万亩" },
], topicKey)
const townRows = normalizeRankRows([
{ name: "色地镇", value: 99.09, basicAreaWanMu: 66.58 },
@ -1479,7 +1499,7 @@ function buildFallbackTopicStats(topicKey, layerKey) {
], { nameKeys: ["areaName"], valueKeys: ["yield"], unit: "万吨" })
const typeRows = aggregateTailRows([
{ key: "grazing", name: "重要放牧场", value: 516.41, unit: "万亩" },
{ key: "general", name: "一般草原", value: 119.31, unit: "万亩" },
{ key: "general", name: "一般草原", value: fallbackGeneralGrasslandArea, unit: "万亩" },
{ key: "ecology", name: "生态调节草原", value: 97.23, unit: "万亩" },
{ key: "mowing", name: "割草地", value: 15.95, unit: "万亩" },
{ key: "improved", name: "改良草地", value: 4.33, unit: "万亩" },

@ -70,6 +70,10 @@ export function getPastureGrasslandsById(id) {
return get("/api/home/grasslandByPastureId", { id }).then(normalizeRows)
}
export function getGrasslandClickInfo(params = {}) {
return get("/api/home/grasslandClickInfo", params, { timeout: 20000 }).then((data) => data?.data || data || null)
}
export function getPastureDetailsById(id) {
return get("/api/pastureDetailsById", { id }).then((data) => data?.data || data || {})
}
@ -89,7 +93,7 @@ export async function getPastureProfile() {
getPastureAge(),
getPastureOnline(),
getPastureList(),
getMuhuList("", 1, 300),
getMuhuList("", 1, 10000),
])
const pastureRows = pastureList.status === "fulfilled" ? pastureList.value : []
const muhuRows = muhuList.status === "fulfilled" ? muhuList.value : []

@ -353,9 +353,9 @@ const parkWetlandProductivityWanTon = 12.48
const parkEdibleGrasslandWanTon = round(parkGrasslandProductivityWanTon * (192.26 / 297.29), 2)
const parkGrasslandCapacityPerMu = 0.06229833103624831
const parkBalanceTownRowMap = new Map([
["瓦切镇", { 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" }],
["瓦切镇", { areaRange: 750120, yakNumber: 43459, bearingCapacity: 44927.54, livestockCapacity: 45260, overloadRate: 0.74, year: "2026" }],
["麦洼乡", { areaRange: 666075, yakNumber: 35773, bearingCapacity: 36505.86, livestockCapacity: 36776, overloadRate: 0.74, year: "2026" }],
["色地镇", { areaRange: 209805, yakNumber: 25947, bearingCapacity: 26534.58, livestockCapacity: 26731, overloadRate: 0.74, year: "2026" }],
])
const parkVillageWeights = {
麦洼乡: [
@ -401,7 +401,7 @@ parkTownBusinessRows.forEach((item) => {
: bearingCapacity
? round(((livestockCapacity - bearingCapacity) / bearingCapacity) * 100, 1)
: 0
const risk = balanceRow?.level ? { label: balanceRow.level } : getRiskLevel(overloadRate)
const risk = getRiskLevel(overloadRate)
item.bearingCapacity = bearingCapacity
item.livestockCapacity = livestockCapacity
item.overloadRate = round(overloadRate, 2)
@ -572,7 +572,7 @@ const parkBalanceGaugeRows = [
name: "超载率",
value: parkSummary.balanceOverloadRate,
unit: "%",
status: "总体可控",
status: getRiskLevel(parkSummary.balanceOverloadRate).label,
color: "#24f6c4",
},
]
@ -669,8 +669,8 @@ export const parkPanels = {
{
key: "park-town-balance",
title: "草畜平衡超载率",
type: "gauge",
max: 100,
type: "balanceGaugeGrid",
limit: 1,
rows: parkBalanceGaugeRows,
},
],

@ -394,6 +394,13 @@
<strong v-if="column.strong">
{{ formatProjectCell(item, column) }}<em v-if="getProjectCellUnit(item, column)">{{ getProjectCellUnit(item, column) }}</em>
</strong>
<span
v-else-if="column.icon && formatProjectCell(item, column) !== '--'"
class="business-project-cell-icon"
:class="`business-project-cell-icon--${column.icon}`"
role="img"
:aria-label="formatProjectCell(item, column)"
></span>
<span v-else>{{ formatProjectCell(item, column) }}</span>
</td>
</tr>
@ -536,6 +543,33 @@
</div>
</div>
<div
v-else-if="panelType === 'balanceGaugeGrid'"
class="business-balance-gauge-grid"
:class="{ 'is-single': balanceGaugeRows.length === 1 }"
>
<div
v-for="(item, index) in balanceGaugeRows"
:key="item.key || item.name"
class="business-balance-gauge-card"
:class="{ 'is-active': index === normalizedActiveIndex }"
:style="balanceGaugeStyle(item, index)"
:data-chart-tooltip="gaugeTooltipText(item)"
>
<div class="business-balance-gauge-head">
<strong>{{ gaugeStatusText(item) }}</strong>
</div>
<div class="business-balance-gauge-ring">
<div class="business-balance-gauge-value">
<strong>{{ displayValue(item) }}</strong>
<em v-if="displayUnit(item)">{{ displayUnit(item) }}</em>
</div>
</div>
<div class="business-balance-gauge-name">{{ item.name }}</div>
<div v-if="item.extra" class="business-balance-gauge-extra">{{ item.extra }}</div>
</div>
</div>
<div v-else-if="panelType === 'gauge'" class="business-gauges" :class="{ 'is-dense': gaugeRows.length > 3 }">
<div
v-for="(item, index) in gaugeRows"
@ -683,6 +717,11 @@ const gaugeRows = computed(() =>
.filter((item) => item?.value !== undefined && item?.value !== null && item?.value !== "")
.slice(0, props.panel?.limit || 6)
)
const balanceGaugeRows = computed(() =>
rows.value
.filter((item) => item?.value !== undefined && item?.value !== null && item?.value !== "")
.slice(0, props.panel?.limit || 3)
)
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))
@ -710,6 +749,7 @@ const projectTableColumns = computed(() => {
unitField: column.unitField,
fallback: column.fallback,
format: column.format,
icon: column.icon,
className: column.className,
}))
})
@ -730,7 +770,7 @@ const projectSearchKeys = computed(() => {
})
const projectTableRows = computed(() => {
const keyword = normalizeSearch(projectSearchText.value)
if (!keyword) return rows.value
if (!keyword) return limitProjectRows(rows.value)
return rows.value.filter((item) => normalizeSearch(getProjectSearchText(item)).includes(keyword))
})
const capacityTotalRow = computed(() => rows.value.find((item) => ["forage-total", "wetland-yield-total"].includes(item.key)) || rows.value[0])
@ -785,6 +825,7 @@ const activeLoopLength = computed(() => {
if (panelType.value === "column") return chartRows.value.length
if (panelType.value === "table") return tableRows.value.length
if (panelType.value === "line") return chartRows.value.length
if (panelType.value === "balanceGaugeGrid") return balanceGaugeRows.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
@ -1135,6 +1176,12 @@ function getProjectSearchText(item) {
return [item?.searchText, ...values].filter(Boolean).join(" ")
}
function limitProjectRows(sourceRows = []) {
const maxRows = Number(props.panel?.maxRows)
if (!Number.isFinite(maxRows) || maxRows <= 0) return sourceRows
return sourceRows.slice(0, maxRows)
}
function getProjectCellRawValue(item, column) {
const fields = [column?.field, ...(column?.fields || [])].filter(Boolean)
for (const field of fields) {
@ -1235,6 +1282,30 @@ function gaugeColor(item, index) {
return chartColor(index)
}
function balanceGaugeProgress(item) {
const value = Math.max(0, gaugeRawValue(item))
const max = Math.max(5, ...balanceGaugeRows.value.map((row) => Math.max(0, gaugeRawValue(row))))
return Math.max(6, Math.min(100, Number(((value / max) * 100).toFixed(1))))
}
function balanceGaugeColor(item) {
const value = gaugeRawValue(item)
const status = String(item?.status || item?.level || "")
if (value > 20 || /严重|重度|高超载/.test(status)) return "#ff5a64"
if (value > 10 || /中度|预警/.test(status)) return "#ff9a5c"
if (value > 5 || /轻度/.test(status)) return "#ffe06b"
if (value <= 0 || /总体可控|合理|正常|达标|无超载/.test(status)) return "#24f6c4"
return "#30dcff"
}
function balanceGaugeStyle(item) {
const color = balanceGaugeColor(item)
return {
"--balance-color": color,
"--balance-progress": `${balanceGaugeProgress(item)}%`,
}
}
function gaugeStatusText(item) {
if (item?.status || item?.level) return item.status || item.level
const value = gaugeRawValue(item)
@ -4609,6 +4680,68 @@ function columnGradient(index, item = {}) {
font-weight: 600;
text-shadow: 0 0 10px rgba(83, 249, 143, 0.3);
}
.business-project-cell-icon {
display: inline-flex;
position: relative;
width: 18px;
height: 18px;
align-items: center;
justify-content: center;
color: #53f98f;
vertical-align: middle;
}
.business-project-cell-icon--locate {
border: 1.5px solid currentColor;
border-radius: 50%;
background:
linear-gradient(currentColor, currentColor) center top / 1px 3px no-repeat,
linear-gradient(currentColor, currentColor) center bottom / 1px 3px no-repeat,
linear-gradient(currentColor, currentColor) left center / 3px 1px no-repeat,
linear-gradient(currentColor, currentColor) right center / 3px 1px no-repeat,
rgba(83, 249, 143, 0.08);
box-shadow:
0 0 10px rgba(83, 249, 143, 0.34),
inset 0 0 7px rgba(83, 249, 143, 0.12);
&::before {
position: absolute;
inset: 4px;
content: "";
border: 1px solid currentColor;
border-radius: 50%;
opacity: 0.76;
}
&::after {
position: absolute;
width: 3px;
height: 3px;
content: "";
border-radius: 50%;
background: #ffffff;
box-shadow: 0 0 7px rgba(255, 255, 255, 0.72);
}
}
tbody tr.is-active .business-project-cell-icon--locate {
color: #ff4054;
background:
linear-gradient(currentColor, currentColor) center top / 1px 3px no-repeat,
linear-gradient(currentColor, currentColor) center bottom / 1px 3px no-repeat,
linear-gradient(currentColor, currentColor) left center / 3px 1px no-repeat,
linear-gradient(currentColor, currentColor) right center / 3px 1px no-repeat,
rgba(255, 64, 84, 0.14);
box-shadow:
0 0 13px rgba(255, 64, 84, 0.52),
inset 0 0 8px rgba(255, 64, 84, 0.18);
}
tbody tr.is-active .business-project-cell-icon--locate::after {
background: #fff3f5;
box-shadow: 0 0 8px rgba(255, 255, 255, 0.86);
}
}
.business-project-col-name {
@ -5747,6 +5880,199 @@ function columnGradient(index, item = {}) {
color: rgba(238, 253, 255, 0.72) !important;
}
.business-balance-gauge-grid {
display: grid;
height: 100%;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
padding: 8px 12px 10px;
box-sizing: border-box;
overflow: hidden;
}
.business-balance-gauge-grid.is-single {
grid-template-columns: minmax(0, 1fr);
padding: 12px 16px 14px;
.business-balance-gauge-card {
width: min(230px, 100%);
justify-self: center;
grid-template-rows: 30px minmax(92px, 1fr) 24px;
padding: 10px 16px 12px;
}
.business-balance-gauge-ring {
width: min(122px, 56%);
}
.business-balance-gauge-value strong {
font-size: 26px;
line-height: 30px;
}
.business-balance-gauge-name {
font-size: 16px;
}
.business-balance-gauge-extra {
display: none;
}
}
.business-balance-gauge-card {
position: relative;
display: grid;
min-width: 0;
min-height: 0;
grid-template-rows: 30px minmax(70px, 1fr) 22px 16px;
justify-items: center;
padding: 9px 7px 8px;
overflow: hidden;
border: 1px solid rgba(48, 220, 255, 0.18);
background:
radial-gradient(circle at 50% 46%, color-mix(in srgb, var(--balance-color) 14%, transparent), transparent 55%),
linear-gradient(180deg, rgba(48, 220, 255, 0.075), rgba(5, 27, 40, 0.46));
box-shadow:
inset 0 0 18px rgba(48, 220, 255, 0.07),
0 0 0 rgba(48, 220, 255, 0);
transition:
border-color 0.35s ease,
box-shadow 0.35s ease,
background 0.35s ease;
&::before {
position: absolute;
inset: 8px;
content: "";
border: 1px solid rgba(255, 255, 255, 0.035);
pointer-events: none;
}
&.is-active {
border-color: color-mix(in srgb, var(--balance-color) 64%, rgba(48, 220, 255, 0.22));
background:
radial-gradient(circle at 50% 46%, color-mix(in srgb, var(--balance-color) 20%, transparent), transparent 58%),
linear-gradient(180deg, rgba(48, 220, 255, 0.095), rgba(5, 27, 40, 0.52));
box-shadow:
inset 0 0 22px rgba(48, 220, 255, 0.09),
0 0 14px color-mix(in srgb, var(--balance-color) 16%, transparent);
}
}
.business-balance-gauge-head {
position: relative;
z-index: 1;
display: flex;
width: 100%;
align-items: center;
justify-content: center;
strong {
width: 100%;
overflow: hidden;
color: var(--balance-color);
font-size: 11px;
font-weight: 900;
text-align: center;
text-overflow: ellipsis;
text-shadow: 0 0 8px color-mix(in srgb, var(--balance-color) 34%, transparent);
white-space: nowrap;
}
}
.business-balance-gauge-ring {
position: relative;
z-index: 1;
align-self: center;
width: min(92px, 82%);
aspect-ratio: 1;
border-radius: 50%;
background:
conic-gradient(from 218deg, var(--balance-color) 0 var(--balance-progress), rgba(93, 175, 199, 0.18) var(--balance-progress) 100%),
rgba(3, 23, 35, 0.78);
box-shadow:
0 0 18px color-mix(in srgb, var(--balance-color) 22%, transparent),
inset 0 0 18px rgba(0, 0, 0, 0.42);
&::before {
position: absolute;
inset: 12px;
content: "";
border-radius: 50%;
background:
radial-gradient(circle at 50% 45%, rgba(48, 220, 255, 0.12), rgba(3, 23, 35, 0.98) 62%),
rgba(3, 23, 35, 0.96);
box-shadow:
inset 0 0 12px rgba(0, 0, 0, 0.42),
inset 0 0 0 1px rgba(48, 220, 255, 0.08);
}
&::after {
position: absolute;
left: 50%;
bottom: 9px;
width: 2px;
height: 16px;
content: "";
background: rgba(0, 10, 18, 0.8);
transform: translateX(-50%) rotate(35deg);
transform-origin: 50% 100%;
box-shadow: 0 0 5px rgba(0, 10, 18, 0.58);
}
}
.business-balance-gauge-value {
position: absolute;
inset: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
strong {
color: #ffffff !important;
font-family: D-DIN, Arial, sans-serif;
font-size: 22px;
line-height: 26px;
font-weight: 900;
text-shadow: 0 0 10px color-mix(in srgb, var(--balance-color) 42%, transparent) !important;
}
em {
margin-left: 2px;
color: rgba(238, 253, 255, 0.76) !important;
font-size: 11px;
font-style: normal;
font-weight: 800;
}
}
.business-balance-gauge-name,
.business-balance-gauge-extra {
position: relative;
z-index: 1;
max-width: 100%;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.business-balance-gauge-name {
color: #ffffff !important;
font-size: 15px;
line-height: 21px;
font-weight: 900;
text-shadow: 0 0 8px rgba(48, 220, 255, 0.18) !important;
}
.business-balance-gauge-extra {
color: rgba(238, 253, 255, 0.66) !important;
font-size: 11px;
line-height: 16px;
font-weight: 700;
}
.business-gauges {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));

@ -45,10 +45,14 @@ const props = defineProps({
type: Number,
default: 430,
},
compactGrassland: {
type: Boolean,
default: false,
},
})
const locationBusinessFields = [
[["town_name", "xiangName", "xiang_name", "xiang", "xzqmc", "xzqdm", "town_code", "area_code"], "所在乡镇"],
[["townName", "town_name", "xiangName", "xiang_name", "xiang", "xzqmc", "xzqdm", "town_code", "areaCode", "area_code"], "所在乡镇"],
[["village_name", "cunName", "cun_name", "cun", "c_name", "cmc"], "所在村"],
[["full_name"], "所在区域"],
[["land_number", "patch_no"], "图斑编号"],
@ -62,6 +66,18 @@ const tailBusinessFields = [
[["adjust_reason"], "调整原因"],
]
const grasslandCoreBusinessFields = [
[["pastureName", "pasture_name"], "所属牧场"],
[["yakCount", "yak_count"], "牦牛数量"],
[["acreage", "area_mu", "mj", "xbmj", "tbdlmj", "tbmj", "geomj", "shape_area", "areaWanMu", "area_wan_mu"], "草场面积"],
[["full_name", "townName", "town_name", "xiangName", "xiang_name", "xiang", "xzqmc", "xzqdm", "town_code", "areaCode", "area_code"], "所在区域"],
[["grassTypeName", "grass_type_name", "grassland_type_name", "grassType", "grass_type"], "草原类"],
[["grasslandLevelName", "grassland_level_name", "grasslandLevel", "grassland_level", "level_name", "level"], "草原级别"],
[["vegetationCover", "vegetation_cover"], "植被盖度"],
[["xcMuYield", "xc_mu_yield", "xbXcMuYield", "xb_xc_mu_yield", "mu_yield"], "鲜草亩产"],
[["gcMuYield", "gc_mu_yield", "xbGcMuYield", "xb_gc_mu_yield"], "干草亩产"],
]
const topicBusinessFields = {
woodland: [
[["dlmc", "layer_class_name"], "地类名称"],
@ -92,35 +108,37 @@ const topicBusinessFields = {
...tailBusinessFields,
],
grassland: [
...grasslandCoreBusinessFields,
[["pastureName", "pasture_name"], "所属牧场"],
...locationBusinessFields,
[["land_class_name", "land_use_name", "land_use"], "地类名称"],
[["land_type"], "资源大类"],
[["resource_type"], "资源类别"],
[["grassland_category_name", "grassland category", "base_grass"], "草原类别"],
[["land_class_name", "landUseName", "land_use_name", "landUse", "land_use"], "地类名称"],
[["landType", "land_type"], "资源大类"],
[["resourceType", "resource_type"], "资源类别"],
[["grasslandCategory", "grassland_category_name", "grassland category", "base_grass"], "草原类别"],
[["basic_type_name"], "基本草原类型"],
[["grass_type_name", "grassland_type_name", "grass_type"], "草原类"],
[["grass_min_type"], "草原型"],
[["grassland_level_name", "grassland_level", "level_name", "level"], "草原级别"],
[["degradation_level_name", "degradation_level"], "退化等级"],
[["vegetation_structure_name", "vegetation_structure"], "植被结构"],
[["vegetation_cover"], "植被盖度"],
[["grassTypeName", "grass_type_name", "grassland_type_name", "grassType", "grass_type"], "草原类"],
[["grassMinType", "grass_min_type"], "草原型"],
[["grasslandLevelName", "grassland_level_name", "grasslandLevel", "grassland_level", "level_name", "level"], "草原级别"],
[["degradationLevelName", "degradation_level_name", "degradationLevel", "degradation_level"], "退化等级"],
[["vegetationStructureName", "vegetation_structure_name", "vegetationStructure", "vegetation_structure"], "植被结构"],
[["vegetationCover", "vegetation_cover"], "植被盖度"],
[["lbmjbl"], "裸斑面积比例"],
[["dominance_level"], "优势度"],
[["dominant_grass_species"], "优势草种"],
[["functional_category"], "功能类别"],
[["xc_yield"], "鲜草产量"],
[["xc_mu_yield", "xb_xc_mu_yield", "mu_yield"], "鲜草亩产"],
[["xb_xc_yield"], "小班鲜草产量"],
[["xb_xc_mu_yield"], "小班鲜草亩产"],
[["gc_yield"], "干草产量"],
[["gc_mu_yield", "xb_gc_mu_yield"], "干草亩产"],
[["xb_gc_yield"], "小班干草产量"],
[["xb_gc_mu_yield"], "小班干草亩产"],
[["dominanceLevel", "dominance_level"], "优势度"],
[["dominantGrassSpecies", "dominant_grass_species"], "优势草种"],
[["functionalCategory", "functional_category"], "功能类别"],
[["xcYield", "xc_yield"], "鲜草产量"],
[["xcMuYield", "xc_mu_yield", "xbXcMuYield", "xb_xc_mu_yield", "mu_yield"], "鲜草亩产"],
[["xbXcYield", "xb_xc_yield"], "小班鲜草产量"],
[["xbXcMuYield", "xb_xc_mu_yield"], "小班鲜草亩产"],
[["gcYield", "gc_yield"], "干草产量"],
[["gcMuYield", "gc_mu_yield", "xbGcMuYield", "xb_gc_mu_yield"], "干草亩产"],
[["xbGcYield", "xb_gc_yield"], "小班干草产量"],
[["xbGcMuYield", "xb_gc_mu_yield"], "小班干草亩产"],
[["elevation"], "海拔"],
[["landform_name", "landform"], "地貌"],
[["landformName", "landform_name", "landform"], "地貌"],
[["slope"], "坡度"],
[["aspect"], "坡向"],
[["soil_depth", "tu_ceng_hd"], "土层厚度"],
[["soilDepth", "soil_depth", "tu_ceng_hd"], "土层厚度"],
[["soil texture"], "土壤质地"],
[["sddl"], "三调地类"],
[["hqlm"], "划区轮牧"],
@ -177,6 +195,15 @@ const codeNameMaps = {
grassland: "草地",
wetland: "湿地",
},
landType: {
1: "草地",
2: "湿地",
caodi: "草地",
shidi: "湿地",
woodland: "林地",
grassland: "草地",
wetland: "湿地",
},
vegetation_structure: {
1: "草本型",
},
@ -262,6 +289,7 @@ const fieldLabels = {
xzqmc: "所在乡镇",
town_code: "所在乡镇",
area_code: "所在乡镇",
areaCode: "所在乡镇",
cun: "所在村",
cmc: "所在村",
c_name: "所在村",
@ -273,28 +301,58 @@ const fieldLabels = {
tbmj: "图斑面积",
shape_area: "图斑面积",
area_mu: "图斑面积",
pastureName: "所属牧场",
pasture_name: "所属牧场",
pastureAcreage: "牧场面积",
pasture_acreage: "牧场面积",
relationAcreage: "关系面积",
relation_acreage: "关系面积",
pastureOverlapAcreage: "重叠面积",
pasture_overlap_acreage: "重叠面积",
relatedPastureCount: "关联牧场数",
related_pasture_count: "关联牧场数",
relatedPastureNames: "关联牧场",
related_pasture_names: "关联牧场",
yakCount: "牦牛数量",
yak_count: "牦牛数量",
grass_min_type: "草原型",
grassMinType: "草原型",
grass_type: "草原类",
grassType: "草原类",
grass_type_name: "草原类",
grassTypeName: "草原类",
grassland_type_name: "草原类",
patch_no: "图斑编号",
land_number: "图斑编号",
grassland_level: "草原级别",
grasslandLevel: "草原级别",
grassland_level_name: "草原级别",
grasslandLevelName: "草原级别",
level: "等级",
level_name: "等级",
land_class_name: "地类名称",
land_use_name: "地类名称",
landUseName: "地类名称",
landUse: "地类名称",
basic_type_name: "草原类型",
grassland_category_name: "草原类别",
grasslandCategory: "草原类别",
degradation_level: "退化等级",
degradationLevel: "退化等级",
degradation_level_name: "退化等级",
degradationLevelName: "退化等级",
vegetation_structure: "植被结构",
vegetationStructure: "植被结构",
vegetation_structure_name: "植被结构",
vegetationStructureName: "植被结构",
xc_yield: "鲜草产量",
xcYield: "鲜草产量",
gc_yield: "干草产量",
gcYield: "干草产量",
xc_mu_yield: "鲜草亩产",
xcMuYield: "鲜草亩产",
gc_mu_yield: "干草亩产",
gcMuYield: "干草亩产",
mu_yield: "亩产",
edible_xc_yield: "可食鲜草产量",
edible_xc_mu_yield: "可食鲜草亩产",
@ -320,9 +378,13 @@ const fieldLabels = {
"grassland category": "草原类别",
dominant_grass_species: "优势草种",
functional_category: "功能类别",
functionalCategory: "功能类别",
vegetation_cover: "植被盖度",
vegetationCover: "植被盖度",
xb_xc_yield: "小班鲜草产量",
xbXcYield: "小班鲜草产量",
xb_gc_yield: "小班干草产量",
xbGcYield: "小班干草产量",
base_grass: "基本草原",
hqlm: "划区轮牧",
elevation: "海拔",
@ -337,11 +399,15 @@ const fieldLabels = {
investigator: "调查人",
survey_time: "调查时间",
land_type: "土地类型",
landType: "土地类型",
status: "状态",
type: "类型",
xb_gc_mu_yield: "干草亩产",
xbGcMuYield: "干草亩产",
xb_xc_mu_yield: "鲜草亩产",
xbXcMuYield: "鲜草亩产",
grass_yield: "草地产量",
grassYield: "草地产量",
full_name: "所在区域",
gyl_bhlx: "公益林保护类型",
ling_zu: "龄组",
@ -431,15 +497,17 @@ const activeTopicKey = computed(() => {
const data = propsData.value
if (layerSignal.includes("national-park") || layerSignal.includes("restoration") || data.project_name || data.project_category || data.totalAcreage) return "ecological"
if (data.dlmc || layerId.includes("woodland") || layerId.includes("t_woodland") || layerSignal.includes("woodland")) return "woodland"
if (layerSignal.includes("wetland") || layerSignal.includes("shidi") || layerId.includes("shidi") || data.land_type === "shidi" || data.land_type === "湿地" || data.land_type === 2) return "wetland"
if (layerSignal.includes("grassland") || layerSignal.includes("caodi") || data.grassland_category_name || data.basic_type_name || layerId.includes("caodi") || data.land_type === "caodi" || data.land_type === 1) return "grassland"
if (layerSignal.includes("wetland") || layerSignal.includes("shidi") || layerId.includes("shidi") || data.land_type === "shidi" || data.landType === "shidi" || data.land_type === "湿地" || data.landType === "湿地" || data.land_type === 2 || data.landType === 2 || data.landType === "2") return "wetland"
if (layerSignal.includes("grassland") || layerSignal.includes("caodi") || data.grassland_category_name || data.grasslandCategory || data.basic_type_name || layerId.includes("caodi") || data.land_type === "caodi" || data.landType === "caodi" || data.land_type === 1 || data.landType === 1 || data.landType === "1") return "grassland"
return "woodland"
})
const displayRows = computed(() => {
const rows = []
const used = new Set()
const businessFields = topicBusinessFields[activeTopicKey.value] || topicBusinessFields.woodland
const isGrassland = activeTopicKey.value === "grassland"
const useCompactGrassland = isGrassland && props.compactGrassland
const businessFields = useCompactGrassland ? grasslandCoreBusinessFields : topicBusinessFields[activeTopicKey.value] || topicBusinessFields.woodland
businessFields.forEach(([keys, label]) => {
const row = buildRow(propsData.value, keys, label, used)
if (row && !rows.some((item) => item.label === row.label)) {
@ -447,9 +515,11 @@ const displayRows = computed(() => {
used.add(row.sourceKey)
}
})
if (!rows.some((row) => row.label === "图斑面积")) {
addStaticRow(rows, used, "图斑面积", formatGeometryArea(featureGeometry.value), "geometry-area")
const areaLabel = isGrassland ? "草场面积" : "图斑面积"
if (!rows.some((row) => row.label === areaLabel || row.label === "图斑面积")) {
addStaticRow(rows, used, areaLabel, formatGeometryArea(featureGeometry.value), "geometry-area")
}
if (useCompactGrassland) return rows.slice(0, 10)
Object.entries(propsData.value).forEach(([key, value]) => {
if (used.has(key) || shouldHideFallbackField(key, value)) return
const formattedValue = formatValue(key, value)
@ -478,6 +548,16 @@ const orderedDisplayRows = computed(() => {
function getRowPriority(row, index) {
const label = row?.label || ""
const sourceKey = String(row?.sourceKey || "").toLowerCase()
if ([
"所属牧场",
"牦牛数量",
"草场面积",
"牧场面积",
"关系面积",
"重叠面积",
"关联牧场数",
"关联牧场",
].includes(label)) return index
if ([
"所在乡镇",
"所在村",
@ -574,9 +654,9 @@ function hasValue(value) {
function formatValue(key, value) {
if (!hasValue(value)) return ""
if (codeNameMaps[key]?.[String(value)]) return codeNameMaps[key][String(value)]
if (["xiang", "xzqdm", "town_code", "area_code"].includes(key)) return getTownNameFromCode(value)
if (["xiang", "xzqdm", "town_code", "area_code", "areaCode"].includes(key)) return getTownNameFromCode(value)
if (["cun", "village_code"].includes(key) && isCodeOnly(value)) return ""
if (["grassland_level", "level"].includes(key) && isNumberLike(value)) {
if (["grassland_level", "grasslandLevel", "level"].includes(key) && isNumberLike(value)) {
return activeTopicKey.value === "wetland" ? `${Number(value)}级湿地` : `${Number(value)}级草原`
}
if (codedTextKeys.has(key) && isCodeOnly(value)) return ""
@ -588,20 +668,22 @@ function formatValue(key, value) {
if (number === 0 && zeroHiddenKeys.has(key)) return ""
const formatted = Number.isInteger(number) ? `${number}` : `${Number(number.toFixed(2))}`
if (["xbmj", "tbdlmj", "geomj", "area_square_meter"].includes(key)) return `${formatNumber(number / 666.6667)}`
if (["acreage", "mj", "area_mu"].includes(key)) return `${formatted}`
if (["acreage", "mj", "area_mu", "pastureAcreage", "pasture_acreage", "relationAcreage", "relation_acreage", "pastureOverlapAcreage", "pasture_overlap_acreage"].includes(key)) return `${formatted}`
if (["yakCount", "yak_count"].includes(key)) return `${formatted}`
if (["relatedPastureCount", "related_pasture_count"].includes(key)) return `${formatted}`
if (["length_meter"].includes(key)) return `${formatted}`
if (["totalAcreage", "cdAcreage", "sdAcreage"].includes(key)) return `${formatted} 万亩`
if (["cdScl", "sdScl"].includes(key)) return `${formatted} 万吨`
if (["areaWanMu", "area_wan_mu"].includes(key)) return `${formatted} 万亩`
if (["xc_yield", "xb_xc_yield", "gc_yield", "xb_gc_yield", "edible_xc_yield", "grass_yield"].includes(key)) return `${formatted}`
if (["mu_yield", "xc_mu_yield", "gc_mu_yield", "xb_xc_mu_yield", "xb_gc_mu_yield", "edible_xc_mu_yield"].includes(key)) return `${formatted}`
if (["vegetation_cover", "lbmjbl", "yu_bi_du"].includes(key)) return `${formatted}%`
if (["elevation", "tu_ceng_hd", "pingjun_sg"].includes(key)) return `${formatted}`
if (["xc_yield", "xcYield", "xb_xc_yield", "xbXcYield", "gc_yield", "gcYield", "xb_gc_yield", "xbGcYield", "edible_xc_yield", "grass_yield", "grassYield"].includes(key)) return `${formatted}`
if (["mu_yield", "xc_mu_yield", "xcMuYield", "gc_mu_yield", "gcMuYield", "xb_xc_mu_yield", "xbXcMuYield", "xb_gc_mu_yield", "xbGcMuYield", "edible_xc_mu_yield"].includes(key)) return `${formatted}`
if (["vegetation_cover", "vegetationCover", "lbmjbl", "yu_bi_du"].includes(key)) return `${formatted}%`
if (["elevation", "tu_ceng_hd", "soilDepth", "soil_depth", "pingjun_sg"].includes(key)) return `${formatted}`
if (["po_du", "slope"].includes(key)) return `${formatted}°`
return formatted
}
const text = String(value)
if (activeTopicKey.value === "wetland" && ["grassland_level", "level"].includes(key)) {
if (activeTopicKey.value === "wetland" && ["grassland_level", "grasslandLevel", "level"].includes(key)) {
return text.replace(/草原/g, "湿地")
}
return text

@ -1,5 +1,5 @@
<template>
<div class="grassland-ndvi-timeline">
<div class="grassland-ndvi-timeline" :class="{ 'is-dense': denseMode }">
<div class="grassland-ndvi-timeline-head">
<span>草地健康/退化</span>
<strong>{{ activeItem?.label || statusText }}</strong>
@ -11,7 +11,11 @@
<button class="grassland-ndvi-timeline-nav" type="button" title="上一期" @click="step(-1)"></button>
<div
class="grassland-ndvi-timeline-track"
:style="{ '--timeline-active-left': activePosition, '--timeline-fill-width': timelineFillWidth }"
:style="{
'--timeline-active-left': activePosition,
'--timeline-fill-width': timelineFillWidth,
'--timeline-edge-offset': `${timelineEdgeOffset}px`,
}"
role="radiogroup"
aria-label="草地健康退化月份"
>
@ -24,7 +28,11 @@
v-for="(item, index) in items"
:key="item.key"
class="grassland-ndvi-timeline-mark"
:class="{ 'is-active': item.key === activeItem?.key, 'is-past': index <= currentIndex }"
:class="{
'is-active': item.key === activeItem?.key,
'is-past': index <= currentIndex,
'is-staggered': denseMode && index % 2 === 1,
}"
type="button"
role="radio"
:aria-checked="item.key === activeItem?.key"
@ -61,11 +69,14 @@ const props = defineProps({
const emit = defineEmits(["update:modelValue"])
const TIMELINE_EDGE_OFFSET = 42
const DENSE_TIMELINE_EDGE_OFFSET = 28
const activeIndex = computed(() => props.items.findIndex((item) => item.key === props.modelValue))
const currentIndex = computed(() => activeIndex.value === -1 ? Math.max(0, props.items.length - 1) : activeIndex.value)
const activeItem = computed(() => props.items[currentIndex.value] || null)
const statusText = computed(() => (props.loading ? "加载中" : "未选择"))
const denseMode = computed(() => props.items.length > 7)
const timelineEdgeOffset = computed(() => denseMode.value ? DENSE_TIMELINE_EDGE_OFFSET : TIMELINE_EDGE_OFFSET)
const activePosition = computed(() => getItemPosition(currentIndex.value))
const timelineFillWidth = computed(() => getTimelineFillWidth(currentIndex.value))
@ -89,14 +100,14 @@ function getItemRatio(index) {
function getItemPosition(index) {
const ratio = getItemRatio(index)
const percent = formatCssNumber(ratio * 100)
const offset = TIMELINE_EDGE_OFFSET * (1 - ratio * 2)
const offset = timelineEdgeOffset.value * (1 - ratio * 2)
return `calc(${percent}% ${offset < 0 ? "-" : "+"} ${formatCssNumber(Math.abs(offset))}px)`
}
function getTimelineFillWidth(index) {
const ratio = getItemRatio(index)
if (ratio <= 0) return "0px"
return `calc(${formatCssNumber(ratio * 100)}% - ${formatCssNumber(TIMELINE_EDGE_OFFSET * 2 * ratio)}px)`
return `calc(${formatCssNumber(ratio * 100)}% - ${formatCssNumber(timelineEdgeOffset.value * 2 * ratio)}px)`
}
function formatCssNumber(value) {
@ -121,6 +132,63 @@ function formatCssNumber(value) {
0 10px 28px rgba(0, 12, 26, 0.34);
color: #efffff;
pointer-events: all;
&.is-dense {
width: 740px;
height: 86px;
padding: 8px 14px 10px;
.grassland-ndvi-timeline-head {
margin-bottom: 5px;
}
.grassland-ndvi-timeline-body {
align-items: start;
}
.grassland-ndvi-timeline-track {
height: 52px;
margin: 0 4px;
}
.grassland-ndvi-timeline-line,
.grassland-ndvi-timeline-fill,
.grassland-ndvi-timeline-thumb {
top: 13px;
}
.grassland-ndvi-timeline-mark {
width: 70px;
height: 52px;
.grassland-ndvi-timeline-tick {
top: 8px;
height: 12px;
}
.grassland-ndvi-timeline-label {
top: 24px;
width: 68px;
font-size: 11px;
line-height: 15px;
}
&.is-staggered {
.grassland-ndvi-timeline-label {
top: 38px;
}
}
&.is-active {
z-index: 4;
.grassland-ndvi-timeline-label {
width: 72px;
font-size: 12px;
}
}
}
}
}
.grassland-ndvi-timeline-head {
@ -204,14 +272,14 @@ function formatCssNumber(value) {
.grassland-ndvi-timeline-line,
.grassland-ndvi-timeline-fill {
position: absolute;
left: 42px;
left: var(--timeline-edge-offset);
top: 8px;
height: 3px;
border-radius: 999px;
}
.grassland-ndvi-timeline-line {
right: 42px;
right: var(--timeline-edge-offset);
background: rgba(90, 199, 228, 0.28);
box-shadow: inset 0 0 8px rgba(0, 7, 18, 0.28);
}

@ -1447,7 +1447,7 @@ export class World extends Mini3d {
getYakIndustryPointScreen(payload = {}) {
const coordinate = this.getYakIndustryPayloadCoordinate(payload)
if (!coordinate) return null
return this.lngLatToLayerScreenPoint(coordinate, this.depth + 0.255)
return this.lngLatToLayerScreenPoint(coordinate, this.depth + 0.36)
|| this.lngLatToScreenPoint(coordinate, this.depth + 0.32)
}
getYakIndustryPayloadCoordinate(payload = {}) {
@ -1759,7 +1759,7 @@ export class World extends Mini3d {
let marker = this.yakIndustryPointMarkers[index]
const style = this.getVectorFeatureStyle(layer, feature)
const material = this.getYakIndustryMarkerMaterial(style, feature)
const position = this.lngLatToLayerLocalPoint(feature.geometry.coordinates, this.depth + 0.98)
const position = this.lngLatToLayerLocalPoint(feature.geometry.coordinates, this.depth + 0.36)
const scale = Number(layer?.pointSpriteScale) || 0.34
const markerType = this.getYakIndustryMarkerType(feature)
if (!marker) {
@ -1774,7 +1774,7 @@ export class World extends Mini3d {
marker.material = material
marker.center.set(0.5, 0.5)
marker.position.copy(position)
marker.scale.set(scale * 1.08, scale * 1.08, 1)
marker.scale.set(scale * 0.88, scale * 0.88, 1)
marker.visible = true
marker.userData = {
feature,
@ -2146,7 +2146,7 @@ export class World extends Mini3d {
return match ? Number(match[0]) || 0 : 0
}
getYakIndustryPointLabelPosition(feature, index = 0) {
const point = this.lngLatToLayerLocalPoint(feature?.geometry?.coordinates, this.depth + 1.08 + Math.min(index, 5) * 0.015)
const point = this.lngLatToLayerLocalPoint(feature?.geometry?.coordinates, this.depth + 0.42 + Math.min(index, 5) * 0.01)
point.x += ((index % 3) - 1) * 0.08
point.y += index % 2 === 0 ? 0.07 : -0.06
return point
@ -4526,7 +4526,7 @@ export class World extends Mini3d {
return coordinates
}
pickVectorFeatureAtPoint(lngLat, screen) {
const features = this.currentVectorFeatures || []
const features = this.getVisibleVectorFeatures(this.currentTopicLayer, this.currentVectorFeatures || [])
if (this.currentTopicLayer?.key === "yak-industry-points") {
const screenFeature = this.pickPointFeatureAtScreenPoint(features, screen)
if (screenFeature) return screenFeature
@ -4561,7 +4561,7 @@ export class World extends Mini3d {
const geometry = feature?.geometry || feature?.feature?.geometry
if (geometry?.type !== "Point") return null
if (this.currentTopicLayer?.key === "yak-industry-points") {
return this.lngLatToLayerScreenPoint(geometry.coordinates, this.depth + 0.255)
return this.lngLatToLayerScreenPoint(geometry.coordinates, this.depth + 0.36)
}
return this.lngLatToScreenPoint(geometry.coordinates, this.depth + 0.32)
}

@ -165,9 +165,9 @@ export function buildBalanceRow({
export const countyBalanceRow = {
...COUNTY_TARGET,
areaRange: 7568515.06,
yakNumber: 1364767,
bearingCapacity: 471506,
livestockCapacity: 474995,
yakNumber: 458230,
bearingCapacity: 454864,
livestockCapacity: 458230,
overloadRate: 0.74,
level: "基本实现草畜平衡",
advice: CXPT_BALANCE_ADVICE,
@ -175,16 +175,16 @@ export const countyBalanceRow = {
}
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" },
{ key: "town-邛溪镇", type: "town", name: "邛溪镇", areaCode: "513233100", areaRange: 930604.2, yakNumber: 49140, bearingCapacity: 47723, livestockCapacity: 49140, overloadRate: 2.97, level: "基本实现草畜平衡", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-刷经寺镇", type: "town", name: "刷经寺镇", areaCode: "513233101", areaRange: 743557.5, yakNumber: 18283, bearingCapacity: 18002, livestockCapacity: 18283, overloadRate: 1.56, level: "基本实现草畜平衡", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-安曲镇", type: "town", name: "安曲镇", areaCode: "513233103", areaRange: 900696.75, yakNumber: 59725, bearingCapacity: 58479, livestockCapacity: 59725, overloadRate: 2.13, level: "基本实现草畜平衡", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-龙日镇", type: "town", name: "龙日镇", areaCode: "513233105", areaRange: 628303.8, yakNumber: 40192, bearingCapacity: 39462, livestockCapacity: 40192, overloadRate: 1.85, level: "基本实现草畜平衡", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-江茸乡", type: "town", name: "江茸乡", areaCode: "513233202", areaRange: 496888.95, yakNumber: 20789, bearingCapacity: 20563, livestockCapacity: 20789, overloadRate: 1.1, level: "基本实现草畜平衡", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-查尔玛乡", type: "town", name: "查尔玛乡", areaCode: "513233203", areaRange: 808628.4, yakNumber: 28836, bearingCapacity: 28312, livestockCapacity: 28836, overloadRate: 1.85, level: "基本实现草畜平衡", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-瓦切镇", type: "town", name: "瓦切镇", areaCode: "513233204", areaRange: 747837.6, yakNumber: 76127, bearingCapacity: 74598, livestockCapacity: 76127, overloadRate: 2.05, level: "基本实现草畜平衡", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-阿木乡", type: "town", name: "阿木乡", areaCode: "513233205", areaRange: 863346, yakNumber: 38587, bearingCapacity: 37554, livestockCapacity: 38587, overloadRate: 2.75, level: "基本实现草畜平衡", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-麦洼乡", type: "town", name: "麦洼乡", areaCode: "513233207", areaRange: 458338.35, yakNumber: 47193, bearingCapacity: 46643, livestockCapacity: 47193, overloadRate: 1.18, level: "基本实现草畜平衡", advice: CXPT_BALANCE_ADVICE, year: "2026" },
{ key: "town-色地镇", type: "town", name: "色地镇", areaCode: "513233208", areaRange: 991265.25, yakNumber: 79358, bearingCapacity: 77695, livestockCapacity: 79358, overloadRate: 2.14, level: "基本实现草畜平衡", advice: CXPT_BALANCE_ADVICE, year: "2026" },
]
export const balanceFallbackRows = [

@ -6,7 +6,7 @@
<div class="balance-energy-band"></div>
</div>
<div class="balance-screen">
<div class="balance-screen" :style="{ '--balance-card-height': `${mainCardHeight}px` }">
<m-card class="balance-card balance-target-card" title="核算对象" :width="selectorCardWidth" :height="selectorCardHeight">
<div class="balance-selector">
<div class="balance-scope-tabs">
@ -243,7 +243,7 @@ const mainCardHeight = computed(() => {
const viewportHeight = Math.max(1080, Number(screenViewportHeight.value) || 1080)
return Math.max(944, viewportHeight - balanceScreenTop - balanceScreenBottomGap)
})
const selectorCardHeight = computed(() => Math.max(844, mainCardHeight.value - 100))
const selectorCardHeight = computed(() => mainCardHeight.value)
const MIN_ANALYSIS_DURATION = 6200
const activeScope = ref("county")
@ -840,11 +840,17 @@ function getProcessStepState(index) {
}
.balance-card {
height: var(--balance-card-height) !important;
filter:
drop-shadow(0 18px 28px rgba(0, 0, 0, 0.28))
drop-shadow(0 0 24px rgba(31, 209, 220, 0.04));
pointer-events: all;
.m-card-bd,
.m-card-bd-bg {
height: var(--balance-card-height) !important;
}
.m-card-hd-bg,
.m-card-hd-zs1,
.m-card-bd-bg > svg,
@ -996,6 +1002,7 @@ function getProcessStepState(index) {
&::before {
border-color: rgba(48, 220, 255, 0.055);
border-left-color: rgba(48, 220, 255, 0.16);
border-bottom-color: rgba(48, 220, 255, 0.12);
}
}
@ -2413,6 +2420,7 @@ function getProcessStepState(index) {
&::before {
border-color: rgba(48, 220, 255, 0.16);
border-left-color: rgba(48, 220, 255, 0.28);
border-bottom-color: rgba(48, 220, 255, 0.12);
}
}
}

@ -17,7 +17,7 @@ const woodlandAreaWanMu = 177.92
const totalForestGrassWetWanMu = round(grasslandAreaWanMu + wetlandAreaWanMu + woodlandAreaWanMu, 2)
const yakDensity = round(yakStockTotal / Math.max(grasslandAreaWanMu, 1), 1)
const wetlandRate = round((wetlandAreaWanMu / Math.max(totalForestGrassWetWanMu, 1)) * 100, 1)
const basicGrasslandWanMu = 637.13
const basicGrasslandWanMu = 637.07
const wetlandYieldWanTon = 108.84
const grasslandYieldWanTon = 297.29
const woodlandPublicWanMu = 159.33

@ -379,7 +379,6 @@ const homeBalanceRows = computed(() => {
unit: "%",
status: item.level || "总体可控",
extra: `载畜 ${formatNumber(item.livestockCapacity)}`,
color: palette[index % palette.length],
}))
})
const homeBalanceGaugeRows = computed(() => {
@ -910,8 +909,19 @@ function homeBalanceGaugeProgress(item) {
return Math.max(6, Math.min(100, (value / maxValue) * 100))
}
function homeBalanceGaugeColor(item) {
const value = Number(item?.value)
const status = String(item?.status || item?.level || "")
if (!Number.isFinite(value)) return "#30dcff"
if (value > 20 || /严重|重度|高超载/.test(status)) return "#ff5a64"
if (value > 10 || /中度|预警/.test(status)) return "#ff9a5c"
if (value > 5 || /轻度/.test(status)) return "#ffe06b"
if (value <= 0 || /总体可控|合理|正常|达标|无超载/.test(status)) return "#24f6c4"
return "#30dcff"
}
function homeBalanceGaugeStyle(item) {
const color = item?.color || "#30dcff"
const color = homeBalanceGaugeColor(item)
return {
"--balance-color": color,
"--balance-progress": `${homeBalanceGaugeProgress(item)}%`,

@ -7,8 +7,40 @@ const round = (value, decimals = 2) => {
const toWanMu = (value) => round((Number(value) || 0) / 10000, 2)
export const pastureHouseholdTotal = 9110
const scaleRowsToTotal = (rows, total, valueKey = "number") => {
const sourceTotal = rows.reduce((sum, item) => sum + (Number(item[valueKey]) || 0), 0)
if (!sourceTotal || !total) return rows.map((item) => ({ ...item, [valueKey]: 0 }))
const scaledRows = rows.map((item) => ({
...item,
[valueKey]: Math.round(((Number(item[valueKey]) || 0) / sourceTotal) * total),
}))
const delta = total - scaledRows.reduce((sum, item) => sum + (Number(item[valueKey]) || 0), 0)
if (delta && scaledRows.length) {
const targetIndex = scaledRows.reduce((bestIndex, item, index) => (
Number(item[valueKey]) > Number(scaledRows[bestIndex]?.[valueKey] || 0) ? index : bestIndex
), 0)
scaledRows[targetIndex][valueKey] += delta
}
return scaledRows
}
const pastureTownSourceRows = scaleRowsToTotal([
{ areaCode: "513233100", areaName: "邛溪镇", number: 1452, year: 2024 },
{ areaCode: "513233101", areaName: "刷经寺镇", number: 1500, year: 2024 },
{ areaCode: "513233103", areaName: "安曲镇", number: 76, year: 2024 },
{ areaCode: "513233105", areaName: "龙日镇", number: 828, year: 2024 },
{ areaCode: "513233202", areaName: "江茸乡", number: 902, year: 2024 },
{ areaCode: "513233203", areaName: "查尔玛乡", number: 450, year: 2024 },
{ areaCode: "513233204", areaName: "瓦切镇", number: 602, year: 2024 },
{ areaCode: "513233205", areaName: "阿木乡", number: 300, year: 2024 },
{ areaCode: "513233207", areaName: "麦洼乡", number: 1204, year: 2024 },
{ areaCode: "513233208", areaName: "色地镇", number: 2638, year: 2024 },
], pastureHouseholdTotal)
const cxptCoreRows = [
{ key: "pasture-total", name: "牧户数量", value: 9110, unit: "户", icon: "town" },
{ key: "pasture-total", name: "牧户数量", value: pastureHouseholdTotal, unit: "户", icon: "town" },
{ key: "avg-yak-per-household", name: "户均牦牛头数", value: 52, unit: "头", icon: "stack" },
{ key: "avg-grassland-area", name: "户均草地面积", value: 1131, unit: "亩", icon: "area" },
{ key: "joint-household-count", name: "联户数量", value: 237, unit: "户", icon: "town" },
@ -24,18 +56,7 @@ export const pastureTopStats = cxptCoreRows.map((item) => ({
decimals: 0,
}))
export const pastureTownRows = [
{ areaCode: "513233100", areaName: "邛溪镇", number: 1452, year: 2024 },
{ areaCode: "513233101", areaName: "刷经寺镇", number: 1500, year: 2024 },
{ areaCode: "513233103", areaName: "安曲镇", number: 76, year: 2024 },
{ areaCode: "513233105", areaName: "龙日镇", number: 828, year: 2024 },
{ areaCode: "513233202", areaName: "江茸乡", number: 902, year: 2024 },
{ areaCode: "513233203", areaName: "查尔玛乡", number: 450, year: 2024 },
{ areaCode: "513233204", areaName: "瓦切镇", number: 602, year: 2024 },
{ areaCode: "513233205", areaName: "阿木乡", number: 300, year: 2024 },
{ areaCode: "513233207", areaName: "麦洼乡", number: 1204, year: 2024 },
{ areaCode: "513233208", areaName: "色地镇", number: 2638, year: 2024 },
].map((item) => ({
export const pastureTownRows = pastureTownSourceRows.map((item) => ({
...item,
key: item.areaCode,
name: item.areaName,
@ -64,16 +85,16 @@ export const pastureOnlineRows = [
}))
export const pastureStructureRows = [
{ areaName: "全县", ageRange: "0-20", sex: "男", number: 340, value: 340, year: 2024 },
{ areaName: "全县", ageRange: "0-20", sex: "女", number: 348, value: 348, year: 2024 },
{ areaName: "全县", ageRange: "21-40", sex: "男", number: 1700, value: 1700, year: 2024 },
{ areaName: "全县", ageRange: "21-40", sex: "女", number: 2040, value: 2040, year: 2024 },
{ areaName: "全县", ageRange: "41-60", sex: "男", number: 2040, value: 2040, year: 2024 },
{ areaName: "全县", ageRange: "41-60", sex: "女", number: 2104, value: 2104, year: 2024 },
{ areaName: "全县", ageRange: "61-80", sex: "男", number: 680, value: 680, year: 2024 },
{ areaName: "全县", ageRange: "61-80", sex: "女", number: 580, value: 580, year: 2024 },
{ areaName: "全县", ageRange: "81-100", sex: "男", number: 60, value: 60, year: 2024 },
{ areaName: "全县", ageRange: "81-100", sex: "女", number: 60, value: 60, year: 2024 },
{ areaName: "全县", ageRange: "0-20", sex: "男", number: 311, value: 311, year: 2024 },
{ areaName: "全县", ageRange: "0-20", sex: "女", number: 319, value: 319, year: 2024 },
{ areaName: "全县", ageRange: "21-40", sex: "男", number: 1556, value: 1556, year: 2024 },
{ areaName: "全县", ageRange: "21-40", sex: "女", number: 1868, value: 1868, year: 2024 },
{ areaName: "全县", ageRange: "41-60", sex: "男", number: 1868, value: 1868, year: 2024 },
{ areaName: "全县", ageRange: "41-60", sex: "女", number: 1925, value: 1925, year: 2024 },
{ areaName: "全县", ageRange: "61-80", sex: "男", number: 622, value: 622, year: 2024 },
{ areaName: "全县", ageRange: "61-80", sex: "女", number: 531, value: 531, year: 2024 },
{ areaName: "全县", ageRange: "81-100", sex: "男", number: 55, value: 55, year: 2024 },
{ areaName: "全县", ageRange: "81-100", sex: "女", number: 55, value: 55, year: 2024 },
]
const aggregateBy = (rows, nameKey, valueKey, unit) => Array.from(rows.reduce((map, item) => {
@ -85,10 +106,7 @@ const aggregateBy = (rows, nameKey, valueKey, unit) => Array.from(rows.reduce((m
}, new Map()).values())
export const pastureAgeRows = aggregateBy(pastureStructureRows, "ageRange", "value", "人")
export const pastureGenderRows = [
{ key: "male", name: "男", value: 4820, unit: "人" },
{ key: "female", name: "女", value: 5132, unit: "人" },
]
export const pastureGenderRows = aggregateBy(pastureStructureRows, "sex", "value", "人")
export const pastureListRows = [
{ name: "更洼尔", areaName: "瓦切镇", acreage: 4704.87, summerAcreage: 2267.26, winterAcreage: 2437.61, year: 2024 },

@ -11,6 +11,7 @@
:max-zoom="21"
:overlay-layer="pastureMapLayer"
@ready="handlePastureOlReady"
@map-click="handlePastureMapClick"
/>
<div class="pasture-map-scrim"></div>
<div v-if="!mapReady" class="pasture-map-loading">影像地图加载中</div>
@ -97,6 +98,17 @@
:height="430"
/>
</div>
<div v-if="grasslandFeaturePanelVisible" class="pasture-map-feature-info" :style="grasslandFeaturePanelStyle">
<feature-info-panel
:feature="selectedGrasslandFeature"
:empty="grasslandFeatureEmpty"
:loading="grasslandFeatureLoading"
:width="460"
:height="grasslandFeaturePanelHeight"
compact-grassland
/>
</div>
</div>
</div>
</template>
@ -108,9 +120,10 @@ import mCard from "@/components/mCard/index.vue"
import YakOlMap from "@/views/yakManagement/YakOlMap.vue"
import LayerPanel from "@/views/forestGrassWet/components/LayerPanel.vue"
import BusinessPanelRenderer from "@/views/forestGrassWet/components/BusinessPanelRenderer.vue"
import { getHongyuanTownshipNameByCode, hongyuanTownshipsGeoJsonRaw, isHongyuanTownshipName } from "@/config/townshipBoundaries"
import FeatureInfoPanel from "@/views/forestGrassWet/components/FeatureInfoPanel.vue"
import { getHongyuanTownshipNameByCode, hongyuanTownshipDisplayOrder, hongyuanTownshipsGeoJsonRaw, isHongyuanTownshipName } from "@/config/townshipBoundaries"
import { getHomeMonitorings } from "@/services/homeVideoApi"
import { getPastureGrasslandsById, getPastureProfile, getPasturesDetailsById } from "@/services/pastureApi"
import { getGrasslandClickInfo, getPastureGrasslandsById, getPastureProfile, getPasturesDetailsById } from "@/services/pastureApi"
import liveSceneImage from "@/assets/images/gd-live/live-scene-1.png"
import {
pastureAgeRows,
@ -118,6 +131,7 @@ import {
pastureFallbackProfile,
pastureGenderRows,
pastureGrasslandRows,
pastureHouseholdTotal,
pastureListRows,
pastureMapRows,
pastureOnlineRows,
@ -147,6 +161,10 @@ const doubleCardHeight = computed(() => sideCardHeight.value * 2 + cardGap)
const mapSceneRef = ref(null)
const mapReady = ref(false)
const pastureMapKey = ref(0)
const selectedGrasslandFeature = ref(null)
const grasslandFeatureEmpty = ref(false)
const grasslandFeatureLoading = ref(false)
const grasslandFeaturePoint = ref(null)
const apiProfile = ref(pastureFallbackProfile)
const pastureRows = ref(pastureListRows)
const pastureBoundaryLayerKey = "pasture-grassland-boundary"
@ -190,6 +208,7 @@ let pastureVideoPlayer = null
let pastureVideoPlayerReady = false
let pastureVideoPlayRun = 0
let pastureVideoLoadTimeoutTimer = null
let grasslandFeatureRequestId = 0
const pastureFallbackTownshipById = {
"4028abc18eecd2af018eecdc44b7086b": "瓦切镇",
"402881828b486d54018b48741f590270": "查尔玛乡",
@ -288,14 +307,45 @@ const pastureMapLayer = computed(() => {
name: selectedLegend.value.name || layer.name,
}
})
const grasslandFeaturePanelVisible = computed(() => Boolean(grasslandFeaturePoint.value))
const grasslandFeaturePanelHeight = computed(() => {
if (grasslandFeatureLoading.value || grasslandFeatureEmpty.value || !selectedGrasslandFeature.value) return 186
const rowCount = estimateGrasslandFeatureRowCount(selectedGrasslandFeature.value)
return Math.min(500, Math.max(320, 86 + Math.min(rowCount, 13) * 34))
})
const grasslandFeaturePanelStyle = computed(() => {
if (!grasslandFeaturePoint.value) return {}
const panelWidth = 460
const panelHeight = grasslandFeaturePanelHeight.value
const margin = 24
const point = toPastureScreenLocalPoint(grasslandFeaturePoint.value)
const xOffset = point.x > 1420 ? -panelWidth - 28 : 28
const x = Math.min(Math.max(point.x + xOffset, margin), 1920 - panelWidth - margin)
const y = Math.min(Math.max(point.y - panelHeight - 72, 188), 1080 - panelHeight - 132)
return {
left: `${x}px`,
top: `${y}px`,
right: "auto",
bottom: "auto",
}
})
const coreRows = computed(() => normalizeCoreRows(apiProfile.value.core).length ? normalizeCoreRows(apiProfile.value.core) : pastureCoreRows)
const townRows = computed(() => normalizeTownRows(apiProfile.value.distribution).length ? normalizeTownRows(apiProfile.value.distribution) : pastureTownRows)
const townRows = computed(() => scaleRowsToTargetTotal(
normalizeTownRows(apiProfile.value.distribution).length ? normalizeTownRows(apiProfile.value.distribution) : pastureTownRows,
pastureHouseholdTotal,
))
const onlineRows = computed(() => {
const normalizedRows = normalizeOnlineRows(apiProfile.value.online)
return completePastureOnlineRows(normalizedRows.length ? normalizedRows : pastureOnlineRows)
})
const ageRows = computed(() => normalizeAgeRows(apiProfile.value.age).length ? normalizeAgeRows(apiProfile.value.age) : pastureAgeRows)
const genderRows = computed(() => normalizeGenderRows(apiProfile.value.age).length ? normalizeGenderRows(apiProfile.value.age) : pastureGenderRows)
const ageRows = computed(() => scaleRowsToTargetTotal(
normalizeAgeRows(apiProfile.value.age).length ? normalizeAgeRows(apiProfile.value.age) : pastureAgeRows,
pastureHouseholdTotal,
))
const genderRows = computed(() => scaleRowsToTargetTotal(
normalizeGenderRows(apiProfile.value.age).length ? normalizeGenderRows(apiProfile.value.age) : pastureGenderRows,
pastureHouseholdTotal,
))
const grasslandRows = computed(() => {
return completePastureGrasslandRows(pastureGrasslandRows)
})
@ -373,12 +423,13 @@ const pastureListPanel = computed(() => ({
type: "projectTable",
searchPlaceholder: "搜索牧户 / 乡镇",
emptyText: "无匹配牧户",
searchKeys: ["name", "township", "id", "grasslandArea"],
maxRows: 300,
searchKeys: ["name", "herdsmanName", "herdsman_name", "phone", "phonenumber", "idCard", "id_card", "township", "id", "grasslandArea", "searchText"],
columns: [
{ key: "name", label: "牧户名称", field: "name", width: "30%" },
{ key: "grasslandArea", label: "草场面积", field: "grasslandArea", width: "28%", align: "right", strong: true, format: "number", unit: "亩" },
{ key: "township", label: "区域", field: "township", width: "24%" },
{ key: "locate", label: "定位", field: "locateText", width: "18%", align: "center", fallback: "定位" },
{ key: "locate", label: "定位", field: "locateText", width: "18%", align: "center", fallback: "定位", icon: "locate" },
],
rows: pastureRows.value,
}))
@ -401,6 +452,24 @@ function normalizeVisualRows(rows = []) {
.filter((item) => item.name && Number.isFinite(item.value) && item.value >= 0)
}
function scaleRowsToTargetTotal(rows = [], targetTotal = pastureHouseholdTotal) {
const sourceRows = Array.isArray(rows) ? rows : []
const sourceTotal = sourceRows.reduce((sum, item) => sum + (Number(item.value) || 0), 0)
if (!sourceRows.length || !sourceTotal || !targetTotal || sourceTotal === targetTotal) return sourceRows
const scaledRows = sourceRows.map((item) => ({
...item,
value: Math.round(((Number(item.value) || 0) / sourceTotal) * targetTotal),
}))
const delta = targetTotal - scaledRows.reduce((sum, item) => sum + (Number(item.value) || 0), 0)
if (delta && scaledRows.length) {
const targetIndex = scaledRows.reduce((bestIndex, item, index) => (
Number(item.value) > Number(scaledRows[bestIndex]?.value || 0) ? index : bestIndex
), 0)
scaledRows[targetIndex].value += delta
}
return scaledRows
}
function updateScreenViewportHeight() {
screenViewportHeight.value = typeof window === "undefined" ? 1080 : window.innerHeight
}
@ -836,6 +905,77 @@ function handlePastureOlReady() {
emit("map-ready")
}
function isPastureGrasslandLayer(layer = pastureMapLayer.value) {
const key = String(layer?.key || "").toLowerCase()
const layerName = String(layer?.layerName || "").toLowerCase()
return key === pastureBoundaryLayerKey || layerName.includes("t_grassland_result_caodi")
}
function toPastureScreenLocalPoint(point = {}) {
const screen = document.querySelector(".pasture-management")
const rect = screen?.getBoundingClientRect()
if (!rect) return { x: Number(point.x) || 960, y: Number(point.y) || 540 }
return {
x: (Number(point.x) || rect.left + rect.width / 2) - rect.left,
y: (Number(point.y) || rect.top + rect.height / 2) - rect.top,
}
}
function estimateGrasslandFeatureRowCount(feature) {
const properties = feature?.properties || {}
return Math.min(10, Object.entries(properties).filter(([, value]) => value !== undefined && value !== null && value !== "").length)
}
function normalizeGrasslandClickFeature(detail = {}, payload = {}) {
return {
id: detail.id,
properties: detail,
feature: {
type: "Feature",
properties: detail,
geometry: null,
},
layer: pastureMapLayer.value || basePastureMapLayer.value,
lngLat: payload.lngLat,
}
}
function closeGrasslandFeaturePanel() {
selectedGrasslandFeature.value = null
grasslandFeaturePoint.value = null
grasslandFeatureEmpty.value = false
grasslandFeatureLoading.value = false
}
async function handlePastureMapClick(payload = {}) {
if (!isPastureGrasslandLayer(payload.overlayLayer || pastureMapLayer.value)) return
const lng = Number(payload?.lngLat?.lng)
const lat = Number(payload?.lngLat?.lat)
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
const requestId = ++grasslandFeatureRequestId
grasslandFeaturePoint.value = payload.screen || { x: 960, y: 540 }
selectedGrasslandFeature.value = null
grasslandFeatureEmpty.value = false
grasslandFeatureLoading.value = true
try {
const detail = await getGrasslandClickInfo({ lng, lat })
if (requestId !== grasslandFeatureRequestId) return
if (!detail?.id) {
closeGrasslandFeaturePanel()
return
}
selectedGrasslandFeature.value = normalizeGrasslandClickFeature(detail, payload)
grasslandFeatureEmpty.value = false
} catch (error) {
if (requestId !== grasslandFeatureRequestId) return
closeGrasslandFeaturePanel()
} finally {
if (requestId === grasslandFeatureRequestId) {
grasslandFeatureLoading.value = false
}
}
}
function refreshMapView() {
mapSceneRef.value?.refreshView?.()
}
@ -879,6 +1019,13 @@ function buildPastureTopStats() {
if (!item?.name || isHiddenCoreMetric(item.name)) return
metrics.set(item.name, item)
})
const pastureTotalMetric = metrics.get("牧户数量") || pastureTopStats.find((item) => item.name === "牧户数量" || item.zh === "牧户数量")
metrics.set("牧户数量", {
...pastureTotalMetric,
name: "牧户数量",
value: pastureHouseholdTotal,
unit: pastureTotalMetric?.unit || "户",
})
return pastureTopStatNames
.map((name) => metrics.get(name) || pastureTopStats.find((item) => item.name === name || item.zh === name))
.filter(Boolean)
@ -1071,6 +1218,7 @@ function normalizePastureRows(rows = []) {
if (!name) return null
const grasslands = normalizeRowGrasslands(item)
const grasslandArea = pickPastureArea(item) || sumPastureGrasslandArea(grasslands)
const grasslandCount = pickGrasslandBoundaryCount(item, grasslands)
const realId = getRealPastureId(item)
const township = getPastureTownship(item, grasslands) || getPastureFallbackTownship(item, name, realId)
const normalized = {
@ -1082,13 +1230,25 @@ function normalizePastureRows(rows = []) {
township: township || "--",
grasslandArea,
acreage: grasslandArea,
grasslandCount,
hasGrasslandBoundary: grasslandCount > 0,
grasslands,
}
normalized.canLocate = Boolean(realId || grasslands.some(hasGrasslandGeometry) || hasGrasslandGeometry(item))
normalized.locateText = normalized.canLocate ? "定位" : "--"
normalized.value = normalized.grasslandArea
normalized.unit = "亩"
normalized.searchText = [normalized.name, normalized.township, normalized.id].filter(Boolean).join(" ")
normalized.searchText = [
normalized.name,
normalized.herdsmanName,
normalized.herdsman_name,
normalized.phone,
normalized.phonenumber,
normalized.idCard,
normalized.id_card,
normalized.township,
normalized.id,
].filter(Boolean).join(" ")
return normalized
})
.filter(Boolean)
@ -1106,6 +1266,12 @@ function sortPastureRows(rows = []) {
}
function comparePastureRows(a, b) {
const boundaryA = hasPastureGrasslandBoundary(a) ? 1 : 0
const boundaryB = hasPastureGrasslandBoundary(b) ? 1 : 0
if (boundaryA !== boundaryB) return boundaryB - boundaryA
const boundaryCountA = pickGrasslandBoundaryCount(a)
const boundaryCountB = pickGrasslandBoundaryCount(b)
if (boundaryCountA !== boundaryCountB) return boundaryCountB - boundaryCountA
const areaA = pickPastureArea(a)
const areaB = pickPastureArea(b)
const hasAreaA = areaA > 0 ? 1 : 0
@ -1118,6 +1284,31 @@ function comparePastureRows(a, b) {
return String(a?.name || "").localeCompare(String(b?.name || ""), "zh-CN")
}
function hasPastureGrasslandBoundary(item = {}) {
const flag = pickValue(item, ["hasGrasslandBoundary", "has_grassland_boundary", "hasBoundary", "has_boundary"])
if (flag === true || flag === 1 || flag === "1") return true
if (typeof flag === "string" && ["true", "yes", "y"].includes(flag.trim().toLowerCase())) return true
return pickGrasslandBoundaryCount(item) > 0
}
function pickGrasslandBoundaryCount(item = {}, grasslands = item?.grasslands) {
const keys = [
"grasslandCount",
"grassland_count",
"grasslandsCount",
"grasslands_count",
"grasslandBoundaryCount",
"grassland_boundary_count",
"boundaryCount",
"boundary_count",
]
for (const key of keys) {
const count = Number(item?.[key])
if (Number.isFinite(count) && count > 0) return count
}
return Array.isArray(grasslands) ? grasslands.length : 0
}
function pickPastureArea(item = {}) {
const keys = [
"acreage",
@ -1241,7 +1432,8 @@ function getPastureTownship(item = {}, grasslands = []) {
"address",
"牧场所在区域",
])
if (directName) return normalizePastureTownName(directName)
const directTownship = normalizePastureTownName(directName)
if (directTownship) return directTownship
const codeName = getPastureTownNameFromCode(pickValue(item, ["areaCode", "area_code", "townCode", "town_code", "code"]))
if (codeName) return codeName
for (const grassland of grasslands) {
@ -1258,7 +1450,8 @@ function getPastureTownship(item = {}, grasslands = []) {
"address",
"牧场所在区域",
])
if (grasslandName) return normalizePastureTownName(grasslandName)
const grasslandTownship = normalizePastureTownName(grasslandName)
if (grasslandTownship) return grasslandTownship
const grasslandCodeName = getPastureTownNameFromCode(pickValue(grassland, ["areaCode", "area_code", "townCode", "town_code", "code"]))
if (grasslandCodeName) return grasslandCodeName
}
@ -1277,6 +1470,8 @@ function normalizePastureTownName(value) {
const codeName = getPastureTownNameFromCode(text)
if (codeName) return codeName
if (isHongyuanTownshipName(text)) return text
const embeddedTownship = hongyuanTownshipDisplayOrder.find((name) => text.includes(name))
if (embeddedTownship) return embeddedTownship
const extracted = extractPastureTownName(text)
return extracted && extracted !== text ? normalizePastureTownName(extracted) : ""
}
@ -2028,6 +2223,19 @@ function isLngLatInPolygon(point, polygon) {
}
}
.pasture-map-feature-info {
position: absolute;
z-index: 9;
width: 460px;
pointer-events: all;
.m-card {
width: 100% !important;
background: rgba(3, 18, 30, 0.7);
backdrop-filter: blur(2px);
}
}
.pasture-management .m-card-hd-title {
color: #ffffff !important;
background: none !important;

@ -3,7 +3,7 @@
<div class="smart-chart-header">
<div>
<strong>{{ chart.title }}</strong>
<span>{{ chart.subtitle }}</span>
<span v-if="displaySubtitle">{{ displaySubtitle }}</span>
</div>
<em v-if="chart.unit">{{ chart.unit }}</em>
</div>
@ -27,6 +27,7 @@ const props = defineProps({
})
const palette = ["#30dcff", "#27f0c0", "#ffd76c", "#7aa7ff", "#ff8f6b", "#a6f06d"]
const scopeLabel = "\u53e3\u5f84"
const chartOption = computed(() => {
const rows = (props.chart.rows || []).filter((row) => Number.isFinite(Number(row.value)))
@ -40,13 +41,23 @@ const isHorizontalBar = computed(() => {
return props.chart.type === "bar" && shouldUseHorizontalBar(rows)
})
const displaySubtitle = computed(() => {
const subtitle = normalizeSubtitle(props.chart.subtitle)
if (subtitle) return subtitle
const source = props.chart.source || {}
const period = normalizeSourceMeta(source.period || source.dataPeriod || props.chart.period)
const unit = props.chart.unit ? `单位:${props.chart.unit}` : ""
return [period, unit].filter(Boolean).join(" · ")
})
const sourceText = computed(() => {
const source = props.chart.source || {}
const sourceLabel = getSourceLabel(source)
const period = normalizeSourceMeta(source.period || source.dataPeriod || props.chart.period)
const generatedAt = normalizeSourceMeta(source.generatedAt || source.updateTime || source.updatedAt, true)
const parts = [sourceLabel, period, generatedAt].filter(Boolean)
return parts.length ? `统计口径:${parts.join(" / ")}` : "统计口径:业务统计库"
const parts = [sourceLabel, period, generatedAt ? `更新 ${generatedAt}` : ""].filter(Boolean)
return parts.length ? `数据来源${parts.join(" · ")}` : "数据来源:业务统计库"
})
onMounted(() => {
@ -341,6 +352,25 @@ function normalizeSourceMeta(value, dateOnly = false) {
return text
}
function normalizeSubtitle(value) {
const text = String(value || "").trim()
if (!text) return ""
const scopePattern = new RegExp(scopeLabel, "g")
const statScopePattern = new RegExp(`统计${scopeLabel}`, "g")
const cleaned = text
.replace(statScopePattern, "统计数据")
.replace(scopePattern, "数据")
.replace(/来源[::]\s*openApi\s*统计库\s*[a-zA-Z0-9_./:-]*[,,、]?\s*/gi, "")
.replace(/openApi\s*统计库\s*/gi, "")
.replace(/\b[a-zA-Z][a-zA-Z0-9]*(?:_[a-zA-Z0-9]+){2,}\b/g, "")
.replace(/[,,、]\s*[,,、]+/g, ",")
.replace(/^[\s,,、::]+|[\s,,、]+$/g, "")
if (!cleaned || /^来源[::]?$/.test(cleaned) || isTechnicalSourceText(cleaned)) return ""
return cleaned
}
function isTechnicalSourceText(text) {
return /^[a-zA-Z0-9_./:-]+$/.test(text) && (text.includes("_") || text.length > 24)
}

@ -9,7 +9,7 @@
<aside v-if="leftPanels.length" class="wisdom-side wisdom-side-left">
<section v-for="panel in leftPanels" :key="panel.id" class="wisdom-side-panel" :class="`is-${panel.type}`">
<header class="wisdom-side-header">
<header v-if="!panel.chart" class="wisdom-side-header">
<div>
<span>{{ panel.kicker }}</span>
<strong>{{ panel.title }}</strong>
@ -40,7 +40,7 @@
<aside v-if="rightPanels.length" class="wisdom-side wisdom-side-right">
<section v-for="panel in rightPanels" :key="panel.id" class="wisdom-side-panel" :class="`is-${panel.type}`">
<header class="wisdom-side-header">
<header v-if="!panel.chart" class="wisdom-side-header">
<div>
<span>{{ panel.kicker }}</span>
<strong>{{ panel.title }}</strong>
@ -256,6 +256,16 @@ function createId() {
return `${Date.now()}-${Math.random().toString(16).slice(2)}`
}
function normalizeYakText(value) {
return String(value || "")
.replace(/耗牛/g, "牦牛")
.replace(/犛牛/g, "牦牛")
}
function normalizeQuestionText(value) {
return normalizeYakText(value).trim()
}
function initSpeechRecognition() {
if (typeof window === "undefined") return
const Recognition = window.SpeechRecognition || window.webkitSpeechRecognition
@ -316,7 +326,7 @@ function stopVoiceInput() {
}
async function sendMessage(text = currentText.value) {
const question = String(text || "").trim()
const question = normalizeQuestionText(text)
if (!question || loading.value) return
stopVoiceInput()
@ -355,54 +365,84 @@ async function sendMessage(text = currentText.value) {
async function handleStatQuestion(question, assistantId) {
patchAssistant(assistantId, {
digestText: "正在理解问题并查询统计库",
digestText: "正在调用AI统计问答",
})
const answerPromise = askStatQuestion(question, window.location.hash || window.location.pathname, "智慧管理")
const chartsPromise = getSmartChartsForQuestion(question, window.location.hash || window.location.pathname, "智慧管理")
const [answerResult, chartResult] = await Promise.allSettled([answerPromise, chartsPromise])
const pagePath = window.location.hash || window.location.pathname
const [answerResult, chartResult] = await Promise.allSettled([
askStatQuestion(question, pagePath, "智慧管理"),
getSmartChartsForQuestion(question, pagePath, "智慧管理"),
])
const charts = chartResult.status === "fulfilled" ? chartResult.value || [] : []
const chartError = chartResult.status === "rejected" ? chartResult.reason : null
if (answerResult.status === "fulfilled") {
const data = answerResult.value
const answer = data?.answer || "小牧暂时没有查到相关统计数据,可以换一种说法再问一次。"
const displayAnswer = chartError ? `${answer}\n\n**图表状态**\n- ${getFriendlyErrorMessage(chartError, "图表接口暂时不可用,已先返回文字分析。")}` : answer
const insights = charts.length ? buildChartInsights(charts) : []
const suggestions = buildFollowUpSuggestions(question, charts)
const steps = [`正在理解:${question}`, "已查询统计库", "已生成统计回答"]
const aiAnswer = normalizeYakText(extractAiAnswer(data))
if (aiAnswer && !isAiFailureAnswer(aiAnswer)) {
const displayAnswer = chartError
? `${aiAnswer}\n\n**图表状态**\n- ${getFriendlyErrorMessage(chartError, "图表接口暂时不可用,已先返回AI文字分析。")}`
: aiAnswer
const insights = charts.length ? buildChartInsights(charts) : []
const suggestions = buildFollowUpSuggestions(question, charts)
const steps = [`正在理解:${question}`, "已生成AI回答"]
if (charts.length) {
steps.push(`右侧已生成 ${charts.length} 个数据图表`)
} else if (chartError) {
steps.push("图表生成暂时不可用")
}
patchAssistant(assistantId, {
digesting: false,
steps,
insights,
suggestions,
allowFollowUp: true,
})
updateDynamicPanels({
question,
answer: displayAnswer,
charts,
insights,
suggestions,
aiPanels: extractAiPanels(data),
})
streamAssistantText(assistantId, displayAnswer, charts)
return
}
if (charts.length) {
steps.push(`右侧已生成 ${charts.length} 个数据图表`)
} else if (chartError) {
steps.push("图表生成暂时不可用")
const answer = "AI文字分析暂时不可用,已先展示统计图表。请稍后重新提问,或换一个更具体的问题。"
const insights = buildChartInsights(charts)
const suggestions = buildFollowUpSuggestions(question, charts)
patchAssistant(assistantId, {
digesting: false,
steps: [`正在理解:${question}`, "AI文字分析暂时不可用", `右侧已生成 ${charts.length} 个数据图表`],
insights,
suggestions,
allowFollowUp: true,
})
updateDynamicPanels({ question, answer, charts, insights, suggestions })
streamAssistantText(assistantId, answer, charts)
return
}
patchAssistant(assistantId, {
digesting: false,
steps,
insights,
suggestions,
allowFollowUp: true,
})
updateDynamicPanels({
question,
answer: displayAnswer,
charts,
insights,
suggestions,
aiPanels: extractAiPanels(data),
})
streamAssistantText(assistantId, displayAnswer, charts)
const errorText = getFriendlyErrorMessage(
new Error(aiAnswer || "AI接口没有返回有效回答"),
"AI问答暂时没有返回有效结果,请稍后再试,或换一个更具体的问题。",
)
updateDynamicPanels({ question, answer: errorText, charts: [], insights: [], suggestions: buildFollowUpSuggestions(question, []) })
finishAssistant(assistantId, errorText)
return
}
if (charts.length) {
const answer = "文字分析服务暂时没有返回结果,已按统计库真实数据生成可视化图表。"
const answer = "AI文字分析暂时不可用,已先展示统计图表。请稍后重新提问,或换一个更具体的问题。"
const insights = buildChartInsights(charts)
const suggestions = buildFollowUpSuggestions(question, charts)
patchAssistant(assistantId, {
digesting: false,
steps: [`正在理解:${question}`, "已查询统计库", `右侧已生成 ${charts.length} 个数据图表`, "文字分析暂时不可用"],
steps: [`正在理解:${question}`, "AI文字分析暂时不可用", `右侧已生成 ${charts.length} 个数据图表`],
insights,
suggestions,
allowFollowUp: true,
@ -412,11 +452,28 @@ async function handleStatQuestion(question, assistantId) {
return
}
const errorText = getFriendlyErrorMessage(answerResult.reason, "小牧暂时没有获取到统计结果,请稍后再试。")
const errorText = getFriendlyErrorMessage(answerResult.reason, "AI问答暂时不可用,请稍后再试,或换一个更具体的问题。")
updateDynamicPanels({ question, answer: errorText, charts: [], insights: [], suggestions: buildFollowUpSuggestions(question, []) })
finishAssistant(assistantId, errorText)
}
function extractAiAnswer(data) {
if (typeof data === "string") return data.trim()
const candidates = [
data?.answer,
data?.text,
data?.content,
data?.reply,
data?.message?.content,
data?.choices?.[0]?.message?.content,
]
return candidates.find((item) => typeof item === "string" && item.trim())?.trim() || ""
}
function isAiFailureAnswer(answer) {
return /调用\s*(DeepSeek|AI|模型).*失败|未解析到回答内容|AI接口没有返回有效回答|服务异常|请求失败/i.test(String(answer || ""))
}
async function handleBalanceQuestion(question, assistantId) {
patchAssistant(assistantId, {
digestText: "正在检索草畜平衡对象",
@ -584,7 +641,12 @@ function normalizePanelList(panels = []) {
actions: normalizePanelActions(panel?.actions || panel?.suggestions),
}
})
.filter((panel) => panel.title && (panel.text || panel.chart || panel.items.length || panel.actions.length))
.filter((panel) => panel.title && !isFollowUpPanel(panel) && (panel.text || panel.chart || panel.items.length || panel.actions.length))
}
function isFollowUpPanel(panel = {}) {
const text = `${panel.kicker || ""}${panel.title || ""}${panel.type || ""}`
return text.includes("\u7ee7\u7eed\u8ffd\u95ee") || text.includes("\u5173\u8054\u95ee\u9898") || /suggestions/i.test(text)
}
function normalizePanelType(type) {
@ -663,19 +725,6 @@ function buildFallbackPanels({ question, answer, charts = [], insights = [], sug
})
}
if (suggestions.length) {
left.push({
id: `suggestion-${createId()}`,
type: "actions",
kicker: "继续追问",
title: "关联问题",
badge: String(suggestions.length),
actions: normalizePanelActions(suggestions),
items: [],
text: "",
})
}
charts.slice(0, 2).forEach((chart, index) => {
right.push({
id: `chart-${chart.id || index}-${createId()}`,
@ -725,7 +774,7 @@ function updateBalancePanels(name, result = {}, answer = "") {
badge: level,
items: [
{ label: "草地面积", value: `${formatNumber(areaRange)}`, note: "承载基础" },
{ label: "牦牛/载畜量", value: `${formatNumber(yakNumber)}`, note: "当前口径" },
{ label: "牦牛/载畜量", value: `${formatNumber(yakNumber)}`, note: "当前数据" },
{ label: "超载率", value: `${formatNumber(overloadRate === "" ? 0 : overloadRate)}%`, note: level },
],
actions: [],
@ -807,39 +856,7 @@ function buildChartInsights(charts = []) {
}
function buildFollowUpSuggestions(question, charts = []) {
const text = String(question || "")
const suggestions = []
const add = (label, nextQuestion) => {
if (!suggestions.some((item) => item.question === nextQuestion) && nextQuestion !== text) {
suggestions.push({ label, question: nextQuestion })
}
}
if (/牦牛|畜群|存栏|牛/.test(text) || charts.some((chart) => chart.source?.localKey?.includes("yak"))) {
add("看畜群结构", "牦牛结构占比")
add("看存栏预测", "牦牛存栏预测")
add("看交易结构", "牦牛交易结构")
}
if (/草地|草原|饲草|鲜草/.test(text)) {
add("看草地健康", "草地健康占比")
add("看生产力", "草地生产力分布")
add("看退化统计", "草地退化统计")
}
if (/湿地/.test(text)) {
add("看湿地类型", "湿地类型结构")
add("看湿地级别", "湿地级别面积统计")
}
if (/林地|公益林|森林/.test(text)) {
add("看权属结构", "林地权属结构")
add("看公益林等级", "公益林保护等级统计")
}
if (/草畜|平衡|承载|载畜/.test(text)) {
add("看牦牛分布", "牦牛分布")
add("看草地面积", "草地面积分布")
}
add("换个图表", `${text} 图表`)
return suggestions.slice(0, 3)
return []
}
function patchAssistant(id, patch, shouldScroll = true) {
@ -858,7 +875,7 @@ function finishAssistant(id, text) {
function getFriendlyErrorMessage(error, fallback) {
const message = String(error?.message || "")
if (error?.code === 401 || error?.code === "401" || error?.code === "HTTP_401" || /未登录|授权|Unauthorized|HTTP 401/i.test(message)) {
return "接口需要登录授权。当前智慧管理不会使用假数据,请先完成平台登录或使用 openApi 统计库口径继续问数。"
return "接口需要登录授权。当前智慧管理不会使用假数据,请先完成平台登录,或继续使用业务统计数据问数。"
}
if (error?.code === "REQUEST_TIMEOUT" || /timeout|timed out|超时/i.test(message)) {
return "服务响应时间较长,请稍后再试,或换一个更具体的问题。"
@ -874,7 +891,7 @@ function getFriendlyErrorMessage(error, fallback) {
function streamAssistantText(id, text) {
stopStreaming()
const source = String(text || "")
const source = normalizeYakText(text)
const chunkSize = source.length > 1200 ? 36 : source.length > 700 ? 24 : 14
let index = 0
patchAssistant(id, {
@ -1051,15 +1068,15 @@ async function scrollToBottom() {
.wisdom-side {
position: absolute;
top: 128px;
bottom: 8px;
top: 112px;
bottom: 18px;
z-index: 5;
width: 374px;
width: clamp(390px, 21vw, 460px);
overflow-y: auto;
display: grid;
align-content: start;
gap: 14px;
padding: 8px 0;
gap: 16px;
padding: 6px 0 10px;
box-sizing: border-box;
scrollbar-width: none;
}
@ -1070,11 +1087,11 @@ async function scrollToBottom() {
}
.wisdom-side-left {
left: 32px;
left: clamp(24px, 2.4vw, 52px);
}
.wisdom-side-right {
right: 32px;
right: clamp(24px, 2.4vw, 52px);
}
.wisdom-side-panel {
@ -1084,10 +1101,17 @@ async function scrollToBottom() {
linear-gradient(135deg, rgba(6, 37, 50, 0.9), rgba(4, 19, 32, 0.86)),
radial-gradient(circle at 18% 0%, rgba(39, 240, 192, 0.12), transparent 34%);
box-shadow: inset 0 0 24px rgba(48, 220, 255, 0.07), 0 14px 28px rgba(0, 0, 0, 0.22);
padding: 13px;
padding: 15px;
box-sizing: border-box;
}
.wisdom-side-panel.is-chart {
padding: 0;
border: 0;
background: transparent;
box-shadow: none;
}
.wisdom-side-header {
display: flex;
align-items: flex-start;
@ -1216,18 +1240,18 @@ async function scrollToBottom() {
.wisdom-side-chart.smart-chart-card {
width: 100%;
height: 268px;
height: 348px;
margin-top: 0;
}
.wisdom-side-chart .smart-chart-header {
height: 48px;
padding: 9px 11px 0;
height: 58px;
padding: 10px 13px 0;
}
.wisdom-side-chart .smart-chart-header strong {
font-size: 14px;
max-width: 210px;
font-size: 15px;
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -1239,12 +1263,12 @@ async function scrollToBottom() {
}
.wisdom-side-chart .smart-chart {
height: 198px;
height: 264px;
}
.wisdom-side-chart .smart-chart-source {
height: 20px;
padding: 0 11px;
height: 24px;
padding: 0 13px;
}
.wisdom-avatar-ring,
@ -1272,10 +1296,10 @@ async function scrollToBottom() {
.wisdom-chat {
position: absolute;
left: 50%;
top: 128px;
bottom: 8px;
top: 112px;
bottom: 18px;
z-index: 4;
width: clamp(600px, calc(100% - 900px), 720px);
width: clamp(720px, calc(100% - 1040px), 900px);
display: flex;
flex-direction: column;
min-width: 0;
@ -1750,19 +1774,29 @@ async function scrollToBottom() {
@media (max-width: 1480px) {
.wisdom-side {
width: 300px;
top: 112px;
bottom: 16px;
width: 318px;
}
.wisdom-side-left {
left: 24px;
left: 20px;
}
.wisdom-side-right {
right: 24px;
right: 20px;
}
.wisdom-side-chart.smart-chart-card {
height: 320px;
}
.wisdom-side-chart .smart-chart {
height: 236px;
}
.wisdom-chat {
width: clamp(520px, calc(100% - 700px), 720px);
width: clamp(560px, calc(100% - 760px), 760px);
}
}

@ -283,6 +283,12 @@ function normalizeStagePanelGroups(config = {}) {
}
function normalizeStagePanelHeight(panel, groupCount = 0) {
if (panel?.type === "projectTable") {
return {
...panel,
height: getProjectTablePanelHeight(panel),
}
}
const isDouble = Number(panel?.height || 0) >= 400 || panel?.span === 2
return {
...panel,
@ -290,6 +296,14 @@ function normalizeStagePanelHeight(panel, groupCount = 0) {
}
}
function getProjectTablePanelHeight(panel = {}) {
const rows = Array.isArray(panel.rows) ? panel.rows : []
const headerHeight = 100
const rowHeight = 32
const preferredHeight = headerHeight + 25 + rows.length * rowHeight
return Math.min(sidePanelDoubleHeight.value, Math.max(sidePanelHeight.value, preferredHeight))
}
function filterDisplayPanels(panels = []) {
return panels.filter((panel) => panel?.type !== "iconMetric")
}
@ -576,6 +590,10 @@ defineExpose({
function handleMapFeatureInfo(payload = {}) {
const properties = payload.feature?.properties || {}
if (properties.stageKey) {
if (String(properties.stageKey) !== String(activeStageKey.value)) {
selectedPoint.value = null
return
}
selectedPoint.value = properties
selectedPointScreen.value = normalizeFeatureScreen(payload.screen)
return

@ -88,7 +88,7 @@ const props = defineProps({
},
})
const emit = defineEmits(["ready", "tile-status", "layer-status"])
const emit = defineEmits(["ready", "tile-status", "layer-status", "map-click"])
const HONGYUAN_CENTER = [103.07984, 33.029058]
const INITIAL_ZOOM = 21
@ -108,6 +108,15 @@ const boundaryLineStyle = new Style({
width: 1.4,
}),
})
const pastureFocusStyle = [
new Style({
fill: new Fill({ color: "rgba(255, 76, 96, 0.2)" }),
stroke: new Stroke({ color: "rgba(255, 76, 96, 0.42)", width: 8 }),
}),
new Style({
stroke: new Stroke({ color: "rgba(255, 48, 68, 0.98)", width: 2.8 }),
}),
]
const labelPointStyle = new Style({
image: new CircleStyle({
radius: 2.4,
@ -121,6 +130,8 @@ let olMap = null
let baseSource = null
let overlaySource = null
let overlayMapLayer = null
let pastureFocusSource = null
let pastureFocusLayer = null
let contextMenuHandler = null
let rightPointerDownHandler = null
let coordinateTipTimer = null
@ -555,6 +566,185 @@ function createBoundaryLayers() {
]
}
function createPastureFocusLayer() {
pastureFocusSource = new VectorSource({ wrapX: false })
pastureFocusLayer = new VectorLayer({
source: pastureFocusSource,
style: () => pastureFocusStyle,
zIndex: 180,
renderBuffer: 160,
updateWhileAnimating: true,
updateWhileInteracting: true,
})
return pastureFocusLayer
}
function normalizePastureGrasslandRows(value) {
if (!value) return []
if (typeof value === "string") return [value]
if (Array.isArray(value)) return value
if (value?.type === "FeatureCollection" && Array.isArray(value.features)) return value.features
if (value?.type === "Feature" || isGeoJsonGeometry(value)) return [value]
if (Array.isArray(value?.rows)) return value.rows
if (Array.isArray(value?.data)) return value.data
if (value?.data) return normalizePastureGrasslandRows(value.data)
if (Array.isArray(value?.list)) return value.list
if (Array.isArray(value?.features)) return value.features
if (value?.wkt || value?.geo || value?.geom || value?.geometryWkt || value?.geometry_wkt || value?.shape || value?.location || value?.geometry) return [value]
return []
}
function normalizePastureGrasslandGeometry(row) {
if (typeof row === "string") return parseGeometryText(row)
if (row?.type === "Feature") return normalizePastureGrasslandGeometry(row.geometry)
if (isGeoJsonGeometry(row)) return row
if (typeof row?.geometry === "string") return parseGeometryText(row.geometry)
if (row?.geometry?.type === "Feature") return normalizePastureGrasslandGeometry(row.geometry.geometry)
if (isGeoJsonGeometry(row?.geometry)) return row.geometry
return parseGeometryText(row?.wkt || row?.geo || row?.geom || row?.geometryWkt || row?.geometry_wkt || row?.shape || row?.location)
}
function normalizePastureGrasslandProperties(row) {
if (!row || typeof row !== "object" || Array.isArray(row)) return { wkt: row }
if (row.type === "Feature") return row.properties || {}
return row
}
function parseGeometryText(value) {
const text = String(value || "").trim()
if (!text) return null
if (text.startsWith("{") || text.startsWith("[")) {
try {
const parsed = JSON.parse(text)
if (parsed?.type === "Feature") return normalizePastureGrasslandGeometry(parsed)
if (isGeoJsonGeometry(parsed)) return parsed
} catch (error) {
return null
}
}
return parseWktGeometry(text)
}
function isGeoJsonGeometry(value) {
return Boolean(
value &&
["Polygon", "MultiPolygon", "LineString", "MultiLineString", "Point", "MultiPoint"].includes(value.type) &&
value.coordinates
)
}
function parseWktGeometry(wkt) {
const text = String(wkt || "").trim()
if (!text) return null
const typeMatch = text.match(/^([A-Z]+)(?:\s+[A-Z]+)?\s*\(/i)
const type = typeMatch?.[1]?.toUpperCase()
if (!type) return null
const body = text.slice(text.indexOf("("))
if (type === "POLYGON") return { type: "Polygon", coordinates: parseWktPolygonBody(body) }
if (type === "MULTIPOLYGON") return { type: "MultiPolygon", coordinates: getWktChildGroups(body).map(parseWktPolygonBody).filter((polygon) => polygon.length) }
if (type === "POINT") return { type: "Point", coordinates: parseWktPoint(body) }
if (type === "MULTIPOINT") return { type: "MultiPoint", coordinates: getWktChildGroups(body).map(parseWktPoint).filter(Boolean) }
if (type === "LINESTRING") return { type: "LineString", coordinates: parseWktLine(body) }
if (type === "MULTILINESTRING") return { type: "MultiLineString", coordinates: getWktChildGroups(body).map(parseWktLine).filter((line) => line.length) }
return null
}
function parseWktPolygonBody(body) {
return getWktChildGroups(body)
.map(parseWktLine)
.filter((ring) => ring.length >= 3)
}
function parseWktLine(text) {
return String(text || "").replace(/^\(+|\)+$/g, "").split(",").map(parseWktPoint).filter(Boolean)
}
function parseWktPoint(text) {
const point = String(text || "").replace(/^\(+|\)+$/g, "").trim().split(/\s+/).slice(0, 2).map(Number)
return point.length >= 2 && Number.isFinite(point[0]) && Number.isFinite(point[1]) ? [point[0], point[1]] : null
}
function getWktChildGroups(text) {
const trimmed = String(text || "").trim()
const inner = trimmed.startsWith("(") && trimmed.endsWith(")") ? trimmed.slice(1, -1) : trimmed
const groups = []
let depth = 0
let start = -1
Array.from(inner).forEach((char, index) => {
if (char === "(") {
if (depth === 0) start = index
depth += 1
} else if (char === ")") {
depth -= 1
if (depth === 0 && start >= 0) {
groups.push(inner.slice(start, index + 1))
start = -1
}
}
})
return groups.length ? groups : [inner]
}
function createPastureFocusFeatures(payload = {}) {
const format = new GeoJSON()
return [
...normalizePastureGrasslandRows(payload?.grasslands),
...normalizePastureGrasslandRows(payload?.wkt),
]
.map((row, index) => {
const geometry = normalizePastureGrasslandGeometry(row)
if (!geometry) return null
const properties = normalizePastureGrasslandProperties(row)
const feature = format.readFeature(
{
type: "Feature",
properties: {
...properties,
id: properties.id || properties.gid || `${payload?.name || "pasture"}-${index}`,
pasture_name: payload?.name || properties.pastureName || properties.name,
},
geometry,
},
{
dataProjection: "EPSG:4326",
featureProjection: "EPSG:3857",
},
)
feature.setId(properties.id || properties.gid || `${payload?.name || "pasture"}-${index}`)
return feature
})
.filter(Boolean)
}
function focusPasture(payload = {}) {
if (!olMap) return false
const features = createPastureFocusFeatures(payload)
if (!features.length) {
emitLayerStatus("warning", "当前牧户暂无可定位数据")
return false
}
if (!pastureFocusSource) {
olMap.addLayer(createPastureFocusLayer())
}
pastureFocusSource.clear(true)
pastureFocusSource.addFeatures(features)
const extent = pastureFocusSource.getExtent()
if (!extent || extent.some((value) => !Number.isFinite(value))) {
emitLayerStatus("warning", "当前牧户暂无可定位数据")
return false
}
const size = olMap.getSize?.() || [1280, 720]
const horizontalPadding = Math.min(360, Math.max(96, Number(size[0] || 0) * 0.24))
olMap.getView()?.fit(extent, {
duration: 700,
maxZoom: 17,
padding: [96, horizontalPadding, 96, horizontalPadding],
})
emitLayerStatus("success", "已定位具体草场")
window.setTimeout(() => olMap?.updateSize?.(), 0)
return true
}
function createTownLabelStyle(feature, resolution) {
const name = feature.get("name") || ""
const compact = resolution > 150
@ -744,6 +934,24 @@ function handleContextMenu(event) {
copyCoordinateFromMapEvent(event)
}
function handleMapSingleClick(event) {
if (!olMap || !event?.coordinate) return
hideCoordinateCopyMenu()
const [lng, lat] = toLonLat(event.coordinate)
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
const sourceEvent = event.originalEvent || {}
emit("map-click", {
lngLat: { lng, lat },
coordinate: event.coordinate,
pixel: event.pixel,
screen: {
x: Number(sourceEvent.clientX),
y: Number(sourceEvent.clientY),
},
overlayLayer: props.overlayLayer,
})
}
function bindContextMenuCopy() {
const viewport = olMap?.getViewport?.()
if (!viewport || contextMenuHandler) return
@ -805,12 +1013,17 @@ async function initMap() {
zIndex: 1,
}),
...createBoundaryLayers(),
createPastureFocusLayer(),
],
view,
})
refreshOverlayLayer()
bindContextMenuCopy()
eventKeys.push(
olMap.on("singleclick", handleMapSingleClick),
)
eventKeys.push(
view.on("change:resolution", () => {
const zoom = currentZoom()
@ -841,6 +1054,8 @@ function disposeMap() {
baseSource = null
overlaySource = null
overlayMapLayer = null
pastureFocusSource = null
pastureFocusLayer = null
if (olMap) {
olMap.setTarget(null)
olMap.dispose?.()
@ -879,6 +1094,7 @@ defineExpose({
refreshView,
resetView,
hasMap,
focusPasture,
})
onMounted(() => {

@ -784,7 +784,7 @@ const epidemicLedgerCardHeight = computed(() => sideCardHeight.value * 4 + sideC
const tradeSourceCardHeight = computed(() => sideCardHeight.value + 24)
const tradeLedgerCardHeight = computed(() => sideCardHeight.value * 2 - sideCardGap)
const epidemicImageBaseUrl = "https://qkl-hy.oss-cn-chengdu.aliyuncs.com/yak-vaccine-images"
const yakMapTileUrlTemplate = "https://qkl-map.oss-cn-chengdu.aliyuncs.com/hy-result/{z}/{x}/{y}.png"
const yakHighResolutionTileUrlTemplate = "https://qkl-map.oss-cn-chengdu.aliyuncs.com/hy-result/{z}/{x}/{y}.png"
const yakStructureMapCenter = [103.07984, 33.029058]
const yakForecastMapCenter = [102.682809, 33.131561]
const yakTradeMapView = {
@ -896,7 +896,9 @@ const forecastGranularityOptions = [
]
const activePeriod = computed(() => getForecastPeriod(activePeriodKey.value))
const yakMapSceneKey = computed(() => `yak-management-map-${yakMapTileUrlTemplate}-${yakMapSceneRevision}-${yakMapSceneResetRevision.value}`)
const yakMapTileUrlTemplate = computed(() => (activeView.value === "trade" ? "" : yakHighResolutionTileUrlTemplate))
const yakMapTileModeKey = computed(() => (yakMapTileUrlTemplate.value ? "hy-result" : "no-hy-result"))
const yakMapSceneKey = computed(() => `yak-management-map-${yakMapTileModeKey.value}-${yakMapSceneRevision}-${yakMapSceneResetRevision.value}`)
const stockNetAdjustment = computed(() => expertCorrection + tradeDeduction)
const activeForecastChange = computed(() => activePeriod.value.stock - estimatedStock)
const forecastWaterfallRows = computed(() => buildForecastWaterfallRows(activePeriod.value))
@ -2815,18 +2817,35 @@ watch(
watch(activeView, (view, previousView) => {
const nextUsesOlMap = view === "structure" || view === "forecast"
const previousUsesOlMap = previousView === "structure" || previousView === "forecast"
const changesThreeMapTileMode = !nextUsesOlMap && !previousUsesOlMap && (view === "trade" || previousView === "trade")
if (nextUsesOlMap && !previousUsesOlMap) {
olMapReady.value = false
tileStatus.value = { type: "idle", text: "" }
topicLayerStatus.value = { type: "idle", text: "" }
}
if (!nextUsesOlMap && previousUsesOlMap) {
clearYakMapLoadTimer()
mapReady.value = false
mapLoadStarted = false
tileStatus.value = { type: "idle", text: "" }
topicLayerStatus.value = { type: "idle", text: "" }
nextTick(() => {
scheduleYakMapLoad()
mapSceneRef.value?.resume?.()
mapSceneRef.value?.refreshView?.()
window.setTimeout(() => mapSceneRef.value?.refreshView?.(), 180)
})
}
if (changesThreeMapTileMode) {
clearYakMapLoadTimer()
mapReady.value = false
mapLoadStarted = false
tileStatus.value = { type: "idle", text: "" }
topicLayerStatus.value = { type: "idle", text: "" }
nextTick(() => {
scheduleYakMapLoad()
})
}
syncForecastRotation()
syncFoundationRotation()
})
@ -4011,6 +4030,7 @@ async function loadYakMapOnce(attempt = 0) {
const scene = mapSceneRef.value
scene?.loadMap?.(mapAssets)
if (scene?.canvasMap) {
mapLoadStarted = false
mapReady.value = true
scene.play?.()
return
@ -4483,6 +4503,7 @@ function buildStructureCompositionRows(rows = [], fallbackRows = []) {
const total = sourceRows.reduce((sum, item) => sum + Number(item.count || 0), 0) || estimatedStock || 1
const definitions = [
{ key: "adultCow", name: "成年母牛", color: "#27f0c0", matcher: (key, name) => key === "adultCow" || name === "成年母牛" },
{ key: "otherAdult", name: "其他成年牛", color: "#7aa7ff", matcher: (key, name) => key === "otherAdult" || name.includes("其他成年") },
{ key: "calf", name: "幼畜", color: "#30dcff", matcher: (key, name) => key === "calf" || name.includes("幼畜") },
{ key: "yearling", name: "育成牛", color: "#ffd76c", matcher: (key, name) => key === "yearling" || name.includes("育成牛") },
{ key: "bull", name: "公牛", color: "#a6f06d", matcher: (key, name) => key === "bull" || name === "公牛" },

Loading…
Cancel
Save