优化大屏界面与地图图层

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

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

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 93 KiB

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

File diff suppressed because one or more lines are too long

@ -1 +1 @@
.login-screen[data-v-8ebd0d6e]{position:fixed;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;overflow:hidden;background:linear-gradient(180deg,rgba(4,16,38,.1),rgba(2,8,22,.62)),url(./bg-3acb94e3-balabala-1783264824576.png) center/cover no-repeat,#05142f;color:#fff}.login-screen[data-v-8ebd0d6e]:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(90deg,rgba(0,0,0,.46),transparent 42%,rgba(0,0,0,.35)),radial-gradient(circle at 52% 46%,rgba(48,220,255,.16),transparent 32%)}.login-shell[data-v-8ebd0d6e]{position:relative;z-index:1;width:min(980px,100vw - 48px);display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:56px;align-items:center}.login-brand[data-v-8ebd0d6e]{display:flex;flex-direction:column;gap:14px;min-width:0;text-shadow:0 0 20px rgba(48,220,255,.36)}.login-brand span[data-v-8ebd0d6e]{font-size:42px;line-height:1.2;font-weight:700}.login-brand small[data-v-8ebd0d6e]{color:#c4f3feb8;font-size:13px;letter-spacing:0}.login-panel[data-v-8ebd0d6e]{width:360px;padding:30px 28px 28px;border:1px solid rgba(48,220,255,.34);background:rgba(3,14,32,.82);box-shadow:0 24px 70px #00000057,inset 0 0 28px #30dcff14;box-sizing:border-box}.login-panel-title[data-v-8ebd0d6e]{margin-bottom:26px;color:#fff;font-size:22px;font-weight:700;text-align:center}.login-field[data-v-8ebd0d6e]{display:block;margin-bottom:18px}.login-field span[data-v-8ebd0d6e]{display:block;margin-bottom:8px;color:#c4f3fec7;font-size:13px}.login-field input[data-v-8ebd0d6e]{width:100%;height:42px;padding:0 12px;border:1px solid rgba(48,220,255,.26);outline:none;background:rgba(0,10,24,.62);color:#fff;font-size:15px;box-sizing:border-box}.login-field input[data-v-8ebd0d6e]:focus{border-color:#30dcffb8;box-shadow:0 0 0 2px #30dcff1f}.login-error[data-v-8ebd0d6e]{min-height:20px;margin:-4px 0 12px;color:#ffcf8a;font-size:13px;line-height:20px}.login-submit[data-v-8ebd0d6e]{width:100%;height:44px;border:1px solid rgba(48,220,255,.52);background:linear-gradient(90deg,rgba(24,140,196,.92),rgba(37,204,218,.92));color:#fff;cursor:pointer;font-size:16px;font-weight:700}.login-submit[data-v-8ebd0d6e]:disabled{cursor:not-allowed;opacity:.68}@media (max-width: 820px){.login-shell[data-v-8ebd0d6e]{grid-template-columns:1fr;gap:28px}.login-brand span[data-v-8ebd0d6e]{font-size:30px}.login-panel[data-v-8ebd0d6e]{width:100%}} .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%}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

@ -546,6 +546,17 @@ const parkTownYakRows = sortParkTownRows(parkTownBusinessRows).map((item) => ({
extra: `${round((item.yakCount / Math.max(parkSummary.yakCount, 1)) * 100, 1)}%`, extra: `${round((item.yakCount / Math.max(parkSummary.yakCount, 1)) * 100, 1)}%`,
})) }))
const parkTownHouseholdRows = sortParkTownRows(parkTownBusinessRows).map((item) => ({
key: `${item.name}-household`,
name: item.name,
value: item.householdCount,
unit: "户",
metricLabel: "牧户",
ecologyJobs: item.ecologyJobs,
peopleCount: item.peopleCount,
extra: `公益岗位 ${item.ecologyJobs}`,
}))
const parkTownBalanceRows = sortParkTownRows(parkTownBusinessRows).map((item) => ({ const parkTownBalanceRows = sortParkTownRows(parkTownBusinessRows).map((item) => ({
key: `${item.name}-balance`, key: `${item.name}-balance`,
name: item.name, name: item.name,
@ -555,11 +566,23 @@ const parkTownBalanceRows = sortParkTownRows(parkTownBusinessRows).map((item) =>
extra: `载畜 ${item.livestockCapacity}`, extra: `载畜 ${item.livestockCapacity}`,
})) }))
const parkBalanceGaugeRows = [
{
key: "park-balance-overload-rate",
name: "超载率",
value: parkSummary.balanceOverloadRate,
unit: "%",
status: "总体可控",
color: "#24f6c4",
},
]
const parkTownEcologyJobRows = sortParkTownRows(parkTownBusinessRows).map((item) => ({ const parkTownEcologyJobRows = sortParkTownRows(parkTownBusinessRows).map((item) => ({
key: `${item.name}-job`, key: `${item.name}-job`,
name: item.name, name: item.name,
value: item.ecologyJobs, value: item.ecologyJobs,
unit: "个", unit: "户",
color: item.name === "麦洼乡" ? "#24f6c4" : item.name === "瓦切镇" ? "#32dcff" : "#ffe06b",
extra: `牧户 ${item.householdCount}`, extra: `牧户 ${item.householdCount}`,
})) }))
@ -577,7 +600,7 @@ export const parkTopStats = [
{ zh: "红原县划定面积", value: parkSummary.hongyuanDelimitedAreaWanMu, unit: "万亩", decimals: 2 }, { zh: "红原县划定面积", value: parkSummary.hongyuanDelimitedAreaWanMu, unit: "万亩", decimals: 2 },
{ zh: "牦牛数量", value: parkSummary.yakCount, unit: "头", decimals: 0 }, { zh: "牦牛数量", value: parkSummary.yakCount, unit: "头", decimals: 0 },
{ zh: "牧户数量", value: parkSummary.householdCount, unit: "户", decimals: 0 }, { zh: "牧户数量", value: parkSummary.householdCount, unit: "户", decimals: 0 },
{ zh: "草畜平衡超载率", value: parkSummary.balanceOverloadRate, unit: "%", decimals: 2 }, { zh: "公益岗位", value: parkSummary.ecologyJobCount, unit: "", decimals: 0 },
{ zh: "瓦切镇面积", value: parkTownBusinessRows.find((item) => item.name === "瓦切镇")?.protectedAreaWanMu || 0, unit: "万亩", decimals: 2 }, { zh: "瓦切镇面积", value: parkTownBusinessRows.find((item) => item.name === "瓦切镇")?.protectedAreaWanMu || 0, unit: "万亩", decimals: 2 },
{ zh: "麦洼乡面积", value: parkTownBusinessRows.find((item) => item.name === "麦洼乡")?.protectedAreaWanMu || 0, unit: "万亩", decimals: 2 }, { zh: "麦洼乡面积", value: parkTownBusinessRows.find((item) => item.name === "麦洼乡")?.protectedAreaWanMu || 0, unit: "万亩", decimals: 2 },
{ zh: "色地镇面积", value: parkTownBusinessRows.find((item) => item.name === "色地镇")?.protectedAreaWanMu || 0, unit: "万亩", decimals: 2 }, { zh: "色地镇面积", value: parkTownBusinessRows.find((item) => item.name === "色地镇")?.protectedAreaWanMu || 0, unit: "万亩", decimals: 2 },
@ -648,7 +671,7 @@ export const parkPanels = {
title: "草畜平衡超载率", title: "草畜平衡超载率",
type: "gauge", type: "gauge",
max: 100, max: 100,
rows: parkTownBalanceRows, rows: parkBalanceGaugeRows,
}, },
], ],
right: [ right: [
@ -658,6 +681,19 @@ export const parkPanels = {
type: "donut", type: "donut",
rows: withParkPercentExtra(parkZoningRows), rows: withParkPercentExtra(parkZoningRows),
}, },
{
key: "park-town-household",
title: "牧户分布",
type: "split",
rows: parkTownHouseholdRows,
},
{
key: "park-town-ecology-job",
title: "公益岗位分布",
type: "column",
variant: "slim",
rows: parkTownEcologyJobRows,
},
], ],
} }
@ -806,7 +842,9 @@ export const restorationPanels = {
title: "年度项目数量", title: "年度项目数量",
type: "column", type: "column",
variant: "projectYear", variant: "projectYear",
rows: (restorationSummary.years || []).map((item) => ({ rows: (restorationSummary.years || [])
.filter((item) => item.name !== "未知")
.map((item) => ({
key: item.name, key: item.name,
name: `${item.name}`, name: `${item.name}`,
value: item.projectCount, value: item.projectCount,

@ -360,6 +360,37 @@ async function loadMapOnce(attempt = 0) {
mapLoadStarted = false mapLoadStarted = false
} }
function clearMapLoadTimer() {
if (!mapLoadTimer) return
window.clearTimeout(mapLoadTimer)
mapLoadTimer = null
}
function refreshMapView() {
mapSceneRef.value?.resume?.()
mapSceneRef.value?.refreshView?.()
}
function resetMap() {
closeFeaturePanel()
mapSceneRef.value?.setBoundaryDrawing?.(false)
mapSceneRef.value?.clearAnalysisBoundary?.()
if (mapSceneRef.value?.hasMap?.()) {
refreshMapView()
return
}
clearMapLoadTimer()
mapLoadStarted = false
nextTick(() => {
scheduleMapLoadIfActive()
})
}
defineExpose({
refreshMapView,
resetMap,
})
function toScreenLocalPoint(point) { function toScreenLocalPoint(point) {
const screen = document.querySelector(".ecological-protection") const screen = document.querySelector(".ecological-protection")
const rect = screen?.getBoundingClientRect() const rect = screen?.getBoundingClientRect()
@ -507,23 +538,27 @@ function hideLoading() {
<style lang="scss"> <style lang="scss">
.ecological-protection { .ecological-protection {
.fgw-map-layer-tools { .fgw-map-layer-tools {
top: 372px; top: auto;
right: 458px; right: 458px;
bottom: 72px;
left: auto;
width: 248px; width: 248px;
height: 430px; height: 430px;
&.is-analysis { &.is-analysis {
top: 282px; top: auto;
right: 72px; right: 458px;
transform: scale(0.92); bottom: 72px;
left: auto;
transform: none;
transform-origin: right top; transform-origin: right top;
} }
&.is-national-park { &.is-national-park {
top: auto; top: auto;
left: auto; left: auto;
right: 452px; right: 458px;
bottom: 144px; bottom: 72px;
width: 178px; width: 178px;
height: 118px; height: 118px;
opacity: 0.92; opacity: 0.92;

@ -328,7 +328,10 @@
v-for="(item, index) in chartRows" v-for="(item, index) in chartRows"
:key="item.key || item.name" :key="item.key || item.name"
class="business-column-item" class="business-column-item"
:class="{ 'is-active': index === normalizedActiveIndex }" :class="{
'is-active': index === normalizedActiveIndex,
'is-zero': item?.zero === true || item?.isZero === true,
}"
:data-hover-value="columnHoverValue(item)" :data-hover-value="columnHoverValue(item)"
:title="columnHoverValue(item)" :title="columnHoverValue(item)"
:data-chart-tooltip="chartTooltipText(item)" :data-chart-tooltip="chartTooltipText(item)"
@ -1235,7 +1238,9 @@ function gaugeColor(item, index) {
function gaugeStatusText(item) { function gaugeStatusText(item) {
if (item?.status || item?.level) return item.status || item.level if (item?.status || item?.level) return item.status || item.level
const value = gaugeRawValue(item) const value = gaugeRawValue(item)
if (value <= 0) return "未超载" if (value < 0) return "合理"
if (value <= 5) return "基本实现草畜平衡"
if (value <= 10) return "轻度超载"
if (value > 100) return "高超载" if (value > 100) return "高超载"
return "超载预警" return "超载预警"
} }
@ -3870,9 +3875,9 @@ function columnGradient(index, item = {}) {
} }
&.is-variant-projectYear { &.is-variant-projectYear {
grid-template-columns: repeat(7, minmax(38px, 1fr)); grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 10px; gap: 3px;
padding: 26px 16px 10px; padding: 25px 14px 9px 8px;
&:before { &:before {
top: 47px; top: 47px;
@ -3887,7 +3892,7 @@ function columnGradient(index, item = {}) {
.business-column-item strong { .business-column-item strong {
color: #ffffff; color: #ffffff;
font-size: 14px; font-size: 11px;
line-height: 18px; line-height: 18px;
text-align: center; text-align: center;
text-shadow: 0 0 9px rgba(255, 255, 255, 0.28); text-shadow: 0 0 9px rgba(255, 255, 255, 0.28);
@ -3896,14 +3901,14 @@ function columnGradient(index, item = {}) {
.business-column-item span { .business-column-item span {
max-width: none; max-width: none;
color: rgba(240, 253, 255, 0.9); color: rgba(240, 253, 255, 0.9);
font-size: 12px; font-size: 10px;
line-height: 15px; line-height: 15px;
white-space: nowrap; white-space: nowrap;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
} }
.business-column-track { .business-column-track {
width: 28px; width: 19px;
height: 92px; height: 92px;
min-height: 74px; min-height: 74px;
border: 1px solid rgba(48, 220, 255, 0.18); border: 1px solid rgba(48, 220, 255, 0.18);
@ -3919,7 +3924,7 @@ function columnGradient(index, item = {}) {
position: absolute; position: absolute;
left: 50%; left: 50%;
bottom: -6px; bottom: -6px;
width: 42px; width: 28px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
background: radial-gradient(ellipse at center, rgba(48, 220, 255, 0.38), rgba(48, 220, 255, 0) 72%); background: radial-gradient(ellipse at center, rgba(48, 220, 255, 0.38), rgba(48, 220, 255, 0) 72%);
@ -3928,8 +3933,8 @@ function columnGradient(index, item = {}) {
} }
.business-column-track i { .business-column-track i {
left: 3px; left: 2px;
right: 3px; right: 2px;
min-height: 12px; min-height: 12px;
border-radius: 4px 4px 1px 1px; border-radius: 4px 4px 1px 1px;
box-shadow: box-shadow:

@ -391,6 +391,43 @@ async function loadMapOnce(attempt = 0) {
mapLoadStarted = false mapLoadStarted = false
} }
function clearMapLoadTimer() {
if (!mapLoadTimer) return
window.clearTimeout(mapLoadTimer)
mapLoadTimer = null
}
function refreshMapView() {
mapSceneRef.value?.resume?.()
mapSceneRef.value?.refreshView?.()
}
function resetMap() {
closeFeaturePanel()
layerStatus.value = {
type: "loading",
text: "地图正在复位",
}
if (mapSceneRef.value?.hasMap?.()) {
refreshMapView()
layerStatus.value = {
type: "success",
text: "地图已复位",
}
return
}
clearMapLoadTimer()
mapLoadStarted = false
nextTick(() => {
scheduleMapLoadIfActive()
})
}
defineExpose({
refreshMapView,
resetMap,
})
async function loadBusinessStats(topicKey, layerKey = activeLayerKey.value) { async function loadBusinessStats(topicKey, layerKey = activeLayerKey.value) {
const requestId = ++businessRequestId const requestId = ++businessRequestId
coreLoading.value = true coreLoading.value = true
@ -692,8 +729,10 @@ function hideLoading() {
} }
.fgw-map-layer-tools { .fgw-map-layer-tools {
top: 372px; top: auto;
right: 458px; right: 458px;
left: auto;
bottom: 72px;
width: 248px; width: 248px;
height: 430px; height: 430px;

@ -74,6 +74,15 @@ const SELECTED_FEATURE_GLOW = "#FF4A3D"
const SELECTED_FEATURE_FILL_HEX = 0xff2d2d const SELECTED_FEATURE_FILL_HEX = 0xff2d2d
const SELECTED_FEATURE_STROKE_HEX = 0xffecec const SELECTED_FEATURE_STROKE_HEX = 0xffecec
const SELECTED_FEATURE_GLOW_HEX = 0xff4a3d const SELECTED_FEATURE_GLOW_HEX = 0xff4a3d
const YAK_INDUSTRY_MARKER_TYPES = {
"family-pasture": "pasture",
"artificial-grassland": "grass",
"scale-breeding": "breeding",
"forage-storage": "storage",
"slaughter-processing": "processing",
"farm-equipment": "equipment",
"social-service": "service",
}
function sortByValue(data) { function sortByValue(data) {
data.sort((a, b) => b.value - a.value) data.sort((a, b) => b.value - a.value)
return data return data
@ -102,6 +111,9 @@ function isVillageRanking(ranking) {
function isTradeFlowRanking(ranking) { function isTradeFlowRanking(ranking) {
return ranking?.scope === "trade-flow" || ranking?.labelMode === "trade-flow" return ranking?.scope === "trade-flow" || ranking?.labelMode === "trade-flow"
} }
function shouldShowTradeFlowLines(ranking) {
return ranking?.showFlowLines !== false
}
function shouldHideTownNames(ranking) { function shouldHideTownNames(ranking) {
return Boolean(ranking?.hideTownNames || ranking?.showTownNames === false) return Boolean(ranking?.hideTownNames || ranking?.showTownNames === false)
} }
@ -114,6 +126,9 @@ function isCalloutRanking(ranking) {
function isVectorTopicLayer(layer) { function isVectorTopicLayer(layer) {
return ["wkt-api", "geojson", "wfs"].includes(layer?.sourceType) return ["wkt-api", "geojson", "wfs"].includes(layer?.sourceType)
} }
function isReadOnlyTopicLayer(layer) {
return layer?.key === "yak-recognition-layer"
}
function buildPublicUrl(path) { function buildPublicUrl(path) {
if (!path) return "" if (!path) return ""
if (/^https?:\/\//.test(path) || path.startsWith("/")) return path if (/^https?:\/\//.test(path) || path.startsWith("/")) return path
@ -1132,7 +1147,11 @@ export class World extends Mini3d {
applyLayerMapView(layer, options = {}) { applyLayerMapView(layer, options = {}) {
if (!this.camera?.instance || !this.camera?.controls) return if (!this.camera?.instance || !this.camera?.controls) return
if (!this.mapAnimationComplete && !options.force) return if (!this.mapAnimationComplete && !options.force) return
const view = layer?.mapView || (layer?.cameraPosition ? { cameraPosition: layer.cameraPosition } : null) const ranking = layer ? null : this.currentTownRanking
const view = layer?.mapView
|| (layer?.cameraPosition ? { cameraPosition: layer.cameraPosition } : null)
|| ranking?.mapView
|| (ranking?.cameraPosition ? { cameraPosition: ranking.cameraPosition } : null)
const target = view ? this.getLayerMapViewTarget(view) : this.defaultMapTarget.clone() const target = view ? this.getLayerMapViewTarget(view) : this.defaultMapTarget.clone()
const position = view ? this.getLayerMapViewCameraPosition(view, target) : this.defaultMapCameraPosition.clone() const position = view ? this.getLayerMapViewCameraPosition(view, target) : this.defaultMapCameraPosition.clone()
const duration = view && Number.isFinite(view.duration) ? view.duration : 0.75 const duration = view && Number.isFinite(view.duration) ? view.duration : 0.75
@ -1739,27 +1758,29 @@ export class World extends Mini3d {
markerFeatures.forEach((feature, index) => { markerFeatures.forEach((feature, index) => {
let marker = this.yakIndustryPointMarkers[index] let marker = this.yakIndustryPointMarkers[index]
const style = this.getVectorFeatureStyle(layer, feature) const style = this.getVectorFeatureStyle(layer, feature)
const material = this.getYakIndustryMarkerMaterial(style) 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.98)
const scale = Number(layer?.pointSpriteScale) || 0.34 const scale = Number(layer?.pointSpriteScale) || 0.34
const markerType = this.getYakIndustryMarkerType(feature)
if (!marker) { if (!marker) {
marker = new Sprite(material) marker = new Sprite(material)
marker.name = "yak-industry-point-marker" marker.name = "yak-industry-point-marker"
marker.renderOrder = 42 marker.renderOrder = 42
marker.center.set(0.5, 0.08)
this.bindYakIndustryPointMarkerEvents(marker) this.bindYakIndustryPointMarkerEvents(marker)
this.interactionManager.add(marker) this.interactionManager.add(marker)
this.yakIndustryPointMarkerGroup.add(marker) this.yakIndustryPointMarkerGroup.add(marker)
this.yakIndustryPointMarkers.push(marker) this.yakIndustryPointMarkers.push(marker)
} }
marker.material = material marker.material = material
marker.center.set(0.5, 0.5)
marker.position.copy(position) marker.position.copy(position)
marker.scale.set(scale, scale * 1.32, 1) marker.scale.set(scale * 1.08, scale * 1.08, 1)
marker.visible = true marker.visible = true
marker.userData = { marker.userData = {
feature, feature,
layer, layer,
index, index,
markerType,
} }
}) })
this.yakIndustryPointMarkers.slice(markerFeatures.length).forEach((marker) => { this.yakIndustryPointMarkers.slice(markerFeatures.length).forEach((marker) => {
@ -1810,11 +1831,12 @@ export class World extends Mini3d {
document.body.style.cursor = "default" document.body.style.cursor = "default"
}) })
} }
getYakIndustryMarkerMaterial(style) { getYakIndustryMarkerMaterial(style, feature) {
const key = `${style.fill || "#24F6C4"}-${style.stroke || "#B7FFF2"}` const markerType = style.markerType || this.getYakIndustryMarkerType(feature)
const key = `${style.fill || "#24F6C4"}-${style.stroke || "#B7FFF2"}-${markerType}`
const cached = this.yakIndustryMarkerMaterialCache.get(key) const cached = this.yakIndustryMarkerMaterialCache.get(key)
if (cached) return cached if (cached) return cached
const texture = this.createYakIndustryMarkerTexture(style.fill, style.stroke) const texture = this.createYakIndustryMarkerTexture(style.fill, style.stroke, markerType)
const material = new SpriteMaterial({ const material = new SpriteMaterial({
map: texture, map: texture,
transparent: true, transparent: true,
@ -1825,54 +1847,243 @@ export class World extends Mini3d {
this.yakIndustryMarkerMaterialCache.set(key, material) this.yakIndustryMarkerMaterialCache.set(key, material)
return material return material
} }
createYakIndustryMarkerTexture(fill = "#24F6C4", stroke = "#B7FFF2") { getYakIndustryMarkerType(feature = {}) {
const stageKey = String(feature?.properties?.stageKey || "")
return YAK_INDUSTRY_MARKER_TYPES[stageKey] || "point"
}
createYakIndustryMarkerTexture(fill = "#24F6C4", stroke = "#B7FFF2", markerType = "point") {
const canvas = document.createElement("canvas") const canvas = document.createElement("canvas")
canvas.width = 96 canvas.width = 112
canvas.height = 124 canvas.height = 112
const context = canvas.getContext("2d") const context = canvas.getContext("2d")
const x = 48 const x = 56
const top = 10 const y = 56
const tip = 111
const headY = 43
const radius = 28
context.clearRect(0, 0, canvas.width, canvas.height) context.clearRect(0, 0, canvas.width, canvas.height)
context.beginPath() context.beginPath()
context.arc(x, 62, 38, 0, Math.PI * 2) context.arc(x, y, 42, 0, Math.PI * 2)
context.fillStyle = this.hexToRgba(fill, 0.18) context.fillStyle = this.hexToRgba(fill, 0.18)
context.shadowColor = fill context.shadowColor = fill
context.shadowBlur = 18 context.shadowBlur = 20
context.fill() context.fill()
this.drawYakIndustryMarkerFrame(context, markerType, x, y, fill, stroke || fill)
context.save()
context.shadowBlur = 0
context.beginPath() context.beginPath()
context.moveTo(x, tip) context.arc(x, y, 19, 0, Math.PI * 2)
context.bezierCurveTo(x + 31, 79, x + 38, top + 20, x, top) context.fillStyle = "rgba(3, 23, 34, 0.64)"
context.bezierCurveTo(x - 38, top + 20, x - 31, 79, x, tip) context.fill()
context.closePath() context.strokeStyle = this.hexToRgba("#FFFFFF", 0.32)
context.fillStyle = this.hexToRgba(fill, 0.96) context.lineWidth = 2
context.stroke()
context.restore()
this.drawYakIndustryMarkerGlyph(context, markerType, x, y, "#FFFFFF")
const texture = new CanvasTexture(canvas)
texture.colorSpace = SRGBColorSpace
texture.needsUpdate = true
return texture
}
drawYakIndustryMarkerFrame(context, markerType, x, y, fill = "#24F6C4", stroke = "#B7FFF2") {
context.save()
context.translate(x, y)
context.fillStyle = this.hexToRgba(fill, 0.92)
context.strokeStyle = this.hexToRgba(stroke || fill, 0.98) context.strokeStyle = this.hexToRgba(stroke || fill, 0.98)
context.lineWidth = 5 context.lineWidth = 4
context.shadowColor = stroke || fill context.shadowColor = stroke || fill
context.shadowBlur = 14 context.shadowBlur = 14
if (markerType === "pasture") {
context.beginPath()
context.moveTo(-34, -9)
context.lineTo(0, -38)
context.lineTo(34, -9)
context.lineTo(28, -9)
context.lineTo(28, 24)
context.quadraticCurveTo(28, 31, 21, 31)
context.lineTo(-21, 31)
context.quadraticCurveTo(-28, 31, -28, 24)
context.lineTo(-28, -9)
context.closePath()
} else if (markerType === "grass") {
context.beginPath()
context.moveTo(0, -38)
context.bezierCurveTo(31, -22, 34, 18, 0, 38)
context.bezierCurveTo(-34, 18, -31, -22, 0, -38)
context.closePath()
} else if (markerType === "breeding") {
context.beginPath()
for (let index = 0; index < 6; index += 1) {
const angle = -Math.PI / 2 + (Math.PI * 2 * index) / 6
const px = Math.cos(angle) * 35
const py = Math.sin(angle) * 35
if (index === 0) context.moveTo(px, py)
else context.lineTo(px, py)
}
context.closePath()
} else if (markerType === "storage") {
context.beginPath()
context.moveTo(-34, -25)
context.lineTo(25, -25)
context.lineTo(35, -13)
context.lineTo(35, 27)
context.lineTo(-25, 27)
context.lineTo(-35, 15)
context.lineTo(-35, -25)
context.closePath()
} else if (markerType === "processing") {
context.beginPath()
for (let index = 0; index < 10; index += 1) {
const angle = -Math.PI / 2 + (Math.PI * 2 * index) / 10
const radius = index % 2 === 0 ? 38 : 30
const px = Math.cos(angle) * radius
const py = Math.sin(angle) * radius
if (index === 0) context.moveTo(px, py)
else context.lineTo(px, py)
}
context.closePath()
} else if (markerType === "equipment") {
context.beginPath()
for (let index = 0; index < 16; index += 1) {
const angle = -Math.PI / 2 + (Math.PI * 2 * index) / 16
const radius = index % 2 === 0 ? 38 : 30
const px = Math.cos(angle) * radius
const py = Math.sin(angle) * radius
if (index === 0) context.moveTo(px, py)
else context.lineTo(px, py)
}
context.closePath()
} else if (markerType === "service") {
context.beginPath()
context.moveTo(0, -39)
context.lineTo(31, -24)
context.lineTo(25, 14)
context.quadraticCurveTo(0, 39, -25, 14)
context.lineTo(-31, -24)
context.closePath()
} else {
context.beginPath()
context.arc(0, 0, 34, 0, Math.PI * 2)
}
context.fill() context.fill()
context.stroke() context.stroke()
context.restore()
}
drawYakIndustryMarkerGlyph(context, markerType, x, y, fill = "#24F6C4") {
context.save()
context.translate(x, y)
context.lineCap = "round"
context.lineJoin = "round"
context.shadowBlur = 0 context.shadowBlur = 0
context.fillStyle = this.hexToRgba(fill, 0.98)
context.strokeStyle = this.hexToRgba(fill, 0.98)
context.lineWidth = 3
if (markerType === "pasture") {
context.beginPath()
context.moveTo(-12, -2)
context.lineTo(0, -11)
context.lineTo(12, -2)
context.stroke()
context.fillRect(-9, -2, 18, 12)
context.clearRect(-3, 2, 6, 8)
} else if (markerType === "grass") {
context.beginPath()
context.moveTo(0, 10)
context.quadraticCurveTo(-10, 1, -7, -10)
context.moveTo(0, 10)
context.quadraticCurveTo(10, 1, 7, -10)
context.moveTo(0, 10)
context.quadraticCurveTo(0, 0, 1, -12)
context.stroke()
} else if (markerType === "breeding") {
context.beginPath() context.beginPath()
context.arc(x, headY, radius * 0.46, 0, Math.PI * 2) context.moveTo(-8, -5)
context.fillStyle = this.hexToRgba("#FFFFFF", 0.95) context.quadraticCurveTo(-15, -12, -18, -4)
context.moveTo(8, -5)
context.quadraticCurveTo(15, -12, 18, -4)
context.stroke()
context.beginPath()
context.ellipse(0, 1, 11, 12, 0, 0, Math.PI * 2)
context.fill() context.fill()
context.fillStyle = this.hexToRgba("#08202D", 0.85)
context.fillRect(-5, -2, 3, 3)
context.fillRect(2, -2, 3, 3)
} else if (markerType === "storage") {
context.beginPath() context.beginPath()
context.arc(x, headY, radius * 0.74, 0, Math.PI * 2) context.moveTo(-13, -3)
context.strokeStyle = this.hexToRgba("#FFFFFF", 0.38) context.lineTo(0, -11)
context.lineWidth = 4 context.lineTo(13, -3)
context.lineTo(13, 9)
context.lineTo(-13, 9)
context.closePath()
context.fill()
context.strokeStyle = this.hexToRgba("#08202D", 0.72)
context.lineWidth = 2
context.beginPath()
context.moveTo(-8, 0)
context.lineTo(8, 0)
context.moveTo(-8, 5)
context.lineTo(8, 5)
context.stroke() context.stroke()
} else if (markerType === "processing") {
const texture = new CanvasTexture(canvas) context.beginPath()
texture.colorSpace = SRGBColorSpace context.moveTo(-13, 8)
texture.needsUpdate = true context.lineTo(-13, -1)
return texture context.lineTo(-5, -6)
context.lineTo(-5, -1)
context.lineTo(3, -6)
context.lineTo(3, -1)
context.lineTo(12, -6)
context.lineTo(12, 8)
context.closePath()
context.fill()
context.fillRect(-13, -11, 5, 7)
context.fillStyle = this.hexToRgba("#08202D", 0.72)
context.fillRect(-8, 2, 4, 3)
context.fillRect(0, 2, 4, 3)
context.fillRect(7, 2, 4, 3)
} else if (markerType === "equipment") {
for (let index = 0; index < 8; index += 1) {
context.save()
context.rotate((Math.PI * 2 * index) / 8)
context.fillRect(8, -2, 5, 4)
context.restore()
}
context.beginPath()
context.arc(0, 0, 9, 0, Math.PI * 2)
context.fill()
context.fillStyle = this.hexToRgba("#FFFFFF", 0.98)
context.beginPath()
context.arc(0, 0, 4, 0, Math.PI * 2)
context.fill()
} else if (markerType === "service") {
context.beginPath()
context.moveTo(0, -13)
context.lineTo(12, -7)
context.lineTo(9, 6)
context.quadraticCurveTo(0, 14, -9, 6)
context.lineTo(-12, -7)
context.closePath()
context.fill()
context.strokeStyle = this.hexToRgba("#FFFFFF", 0.94)
context.lineWidth = 2.4
context.beginPath()
context.moveTo(0, -7)
context.lineTo(0, 6)
context.moveTo(-6, -1)
context.lineTo(6, -1)
context.stroke()
} else {
context.beginPath()
context.arc(0, 0, 7, 0, Math.PI * 2)
context.fill()
}
context.restore()
} }
ensureYakIndustryPointLabelGroup() { ensureYakIndustryPointLabelGroup() {
if (this.yakIndustryPointLabelGroup) return if (this.yakIndustryPointLabelGroup) return
@ -2114,6 +2325,7 @@ export class World extends Mini3d {
glow: true, glow: true,
halo: true, halo: true,
marker: true, marker: true,
markerType: YAK_INDUSTRY_MARKER_TYPES[String(properties.stageKey || "")] || "point",
} }
} }
if (layer?.key === "national-park-zoning") { if (layer?.key === "national-park-zoning") {
@ -2835,10 +3047,10 @@ export class World extends Mini3d {
this.applyLayerMapView(this.currentTopicLayer, { force: true }) this.applyLayerMapView(this.currentTopicLayer, { force: true })
} }
} }
createTradeSourceMarker(row, index, maxValue, source) { createTradeSourceMarker(row, index, maxValue, source, showFlowLine = true) {
const color = new Color(index % 3 === 1 ? 0xffd76c : index % 3 === 2 ? 0x27f0c0 : 0x30dcff) const color = new Color(index % 3 === 1 ? 0xffd76c : index % 3 === 2 ? 0x27f0c0 : 0x30dcff)
const endPoint = this.getTradeFlowPoint(row, 0.72) const endPoint = this.getTradeFlowPoint(row, 0.72)
if (source) { if (source && showFlowLine) {
const curve = this.getLocalTradeFlowCurve(row, source, index) const curve = this.getLocalTradeFlowCurve(row, source, index)
const flowMaterial = new MeshBasicMaterial({ const flowMaterial = new MeshBasicMaterial({
color, color,
@ -2984,10 +3196,11 @@ export class World extends Mini3d {
} }
this.clearTradeFlowLayer() this.clearTradeFlowLayer()
this.tradeFlowGroup.visible = this.mapAnimationComplete this.tradeFlowGroup.visible = this.mapAnimationComplete
const showFlowLines = shouldShowTradeFlowLines(ranking)
this.createTradeMarketMarker(source) this.createTradeMarketMarker(source)
const maxValue = Math.max(...rows.map((row) => row.value), 1) const maxValue = Math.max(...rows.map((row) => row.value), 1)
rows.forEach((row, index) => { rows.forEach((row, index) => {
this.createTradeSourceMarker(row, index, maxValue, source) this.createTradeSourceMarker(row, index, maxValue, source, showFlowLines)
}) })
const requiredLabelCount = rows.length + 1 const requiredLabelCount = rows.length + 1
@ -3367,6 +3580,7 @@ export class World extends Mini3d {
}) })
} }
this.updateTradeFlowLayer(ranking) this.updateTradeFlowLayer(ranking)
if (ranking?.mapView || ranking?.cameraPosition) this.applyLayerMapView(null)
return return
} }
this.setTradeNationalMapVisible(false) this.setTradeNationalMapVisible(false)
@ -3971,6 +4185,11 @@ export class World extends Mini3d {
}) })
return return
} }
if (isReadOnlyTopicLayer(this.currentTopicLayer)) {
this.clearSelectedFeature()
this.callbacks.onFeatureInfo?.({ feature: null })
return
}
if (isVectorTopicLayer(this.currentTopicLayer)) { if (isVectorTopicLayer(this.currentTopicLayer)) {
const picked = this.pickVectorFeatureAtPoint(lngLat, screen) const picked = this.pickVectorFeatureAtPoint(lngLat, screen)
const feature = picked?.feature || picked const feature = picked?.feature || picked

@ -208,11 +208,15 @@ function refreshView() {
connectMapResizeObserver(); connectMapResizeObserver();
const refresh = () => { const refresh = () => {
if (!canvasMap.value) return; if (!canvasMap.value) return;
canvasMap.value?.resumeScene?.();
if (!canvasMap.value.resumeScene) canvasMap.value.time?.resume?.();
canvasMap.value?.sizes?.init?.(); canvasMap.value?.sizes?.init?.();
canvasMap.value?.resize?.(); canvasMap.value?.resize?.();
canvasMap.value?.label3d?.resize?.(); canvasMap.value?.label3d?.resize?.();
canvasMap.value?.label3d?.update?.(); canvasMap.value?.label3d?.update?.();
canvasMap.value?.setTopicLayers?.(getActiveLayerStack(props.activeLayer), props.activeLayer);
canvasMap.value?.setTownRanking?.(props.mapRanking); canvasMap.value?.setTownRanking?.(props.mapRanking);
canvasMap.value?.ensureMapRevealed?.();
}; };
scheduleMapResize(); scheduleMapResize();
window.requestAnimationFrame(refresh); window.requestAnimationFrame(refresh);
@ -307,6 +311,7 @@ function getMapRankingSignature(ranking) {
ranking.showBars ?? "", ranking.showBars ?? "",
ranking.showTownNames ?? "", ranking.showTownNames ?? "",
ranking.showMapLabels ?? "", ranking.showMapLabels ?? "",
ranking.showFlowLines ?? "",
ranking.onlyRankedTownLabels ?? "", ranking.onlyRankedTownLabels ?? "",
ranking.showMissingTownStats ?? "", ranking.showMissingTownStats ?? "",
ranking.rows?.length || 0, ranking.rows?.length || 0,

@ -14,17 +14,25 @@ export const COUNTY_TARGET = {
export const RISK_LEVELS = [ export const RISK_LEVELS = [
{ {
key: "healthy", key: "healthy",
label: "草畜平衡", label: "合理",
shortLabel: "平衡", shortLabel: "合理",
range: " 0%", range: "< 0%",
description: "承载力覆盖实际载畜量,当前放牧压力处于合理区间。", description: "承载力覆盖实际载畜量,当前放牧压力处于合理区间。",
actions: ["保持当前载畜水平与轮牧节奏", "定期复测草地生产力和载畜量", "关注极端天气对牧草生长的影响"], actions: ["保持当前载畜水平与轮牧节奏", "定期复测草地生产力和载畜量", "关注极端天气对牧草生长的影响"],
}, },
{ {
key: "watch", key: "watch",
label: "基本实现草畜平衡",
shortLabel: "基本平衡",
range: "0% - 5%",
description: "实际载畜量与草地承载力基本匹配,草畜平衡总体实现。",
actions: ["保持当前载畜规模基本稳定", "持续核查草地生产力和牲畜存栏", "结合季节变化做好补饲和轮牧安排"],
},
{
key: "light",
label: "轻度超载", label: "轻度超载",
shortLabel: "关注", shortLabel: "关注",
range: "0% - 10%", range: "5% - 10%",
description: "已出现轻度超载迹象,建议提前介入调控。", description: "已出现轻度超载迹象,建议提前介入调控。",
actions: ["适度压减超载畜群规模", "延长冬春舍饲补饲周期", "增加人工草场补饲供应"], actions: ["适度压减超载畜群规模", "延长冬春舍饲补饲周期", "增加人工草场补饲供应"],
}, },
@ -98,10 +106,11 @@ export function formatPercent(value) {
export function getRiskLevel(value) { export function getRiskLevel(value) {
const rate = toNumber(value) const rate = toNumber(value)
if (rate === null || rate <= 0) return RISK_LEVELS[0] if (rate === null || rate < 0) return RISK_LEVELS[0]
if (rate <= 10) return RISK_LEVELS[1] if (rate <= 5) return RISK_LEVELS[1]
if (rate <= 20) return RISK_LEVELS[2] if (rate <= 10) return RISK_LEVELS[2]
return RISK_LEVELS[3] if (rate <= 20) return RISK_LEVELS[3]
return RISK_LEVELS[4]
} }
function calculateLivestockCapacity(yakCount) { function calculateLivestockCapacity(yakCount) {
@ -160,22 +169,22 @@ export const countyBalanceRow = {
bearingCapacity: 471506, bearingCapacity: 471506,
livestockCapacity: 474995, livestockCapacity: 474995,
overloadRate: 0.74, overloadRate: 0.74,
level: "轻度超载", level: "基本实现草畜平衡",
advice: CXPT_BALANCE_ADVICE, advice: CXPT_BALANCE_ADVICE,
year: "2026", year: "2026",
} }
export const balanceTownRows = [ 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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "513233208", areaRange: 991265.25, yakNumber: 131956, bearingCapacity: 69233, livestockCapacity: 70715, overloadRate: 2.14, level: "基本实现草畜平衡", advice: CXPT_BALANCE_ADVICE, year: "2026" },
] ]
export const balanceFallbackRows = [ export const balanceFallbackRows = [
@ -319,7 +328,7 @@ export function splitAdvice(row = {}, risk = getRiskLevel(row.overloadRate)) {
: [] : []
const unsuitableWhenOverloaded = ["提高放牧畜禽的密度", "增加草地的利用率", "密度不能过高"] const unsuitableWhenOverloaded = ["提高放牧畜禽的密度", "增加草地的利用率", "密度不能过高"]
const outdatedSafeAdvice = ["轻度超载", "已出现超载", "超载迹象", "压减超载畜群规模"] const outdatedSafeAdvice = ["轻度超载", "已出现超载", "超载迹象", "压减超载畜群规模"]
const safeRisk = risk.key === "healthy" const safeRisk = ["healthy", "watch"].includes(risk.key)
const cleanedApiPoints = apiPoints.filter((item) => { const cleanedApiPoints = apiPoints.filter((item) => {
if (unsuitableWhenOverloaded.some((keyword) => item.includes(keyword))) return false if (unsuitableWhenOverloaded.some((keyword) => item.includes(keyword))) return false
if (safeRisk && outdatedSafeAdvice.some((keyword) => item.includes(keyword))) return false if (safeRisk && outdatedSafeAdvice.some((keyword) => item.includes(keyword))) return false

@ -322,13 +322,15 @@ const conclusionSummary = computed(() => {
}) })
const verdictHint = computed(() => { const verdictHint = computed(() => {
if (activeDerived.value.risk.key === "healthy") return "保持监测" if (activeDerived.value.risk.key === "healthy") return "保持监测"
if (activeDerived.value.risk.key === "watch") return "关注调控" if (activeDerived.value.risk.key === "watch") return "基本平衡"
if (activeDerived.value.risk.key === "light") return "关注调控"
if (activeDerived.value.delta === null) return "按结果表判定" if (activeDerived.value.delta === null) return "按结果表判定"
return activeDerived.value.delta > 0 ? "需调控载畜量" : "承载能力可覆盖" return activeDerived.value.delta > 0 ? "需调控载畜量" : "承载能力可覆盖"
}) })
const decisionHint = computed(() => { const decisionHint = computed(() => {
if (activeDerived.value.risk.key === "healthy") return "保持监测" if (activeDerived.value.risk.key === "healthy") return "保持监测"
if (activeDerived.value.risk.key === "watch") return "关注调控" if (activeDerived.value.risk.key === "watch") return "保持监测"
if (activeDerived.value.risk.key === "light") return "关注调控"
if (activeDerived.value.delta === null) return "结果表口径" if (activeDerived.value.delta === null) return "结果表口径"
return activeDerived.value.delta > 0 ? "需要调控" : "保持监测" return activeDerived.value.delta > 0 ? "需要调控" : "保持监测"
}) })
@ -397,7 +399,7 @@ const analysisProcessRows = computed(() => {
name: "平衡判定", name: "平衡判定",
desc: "生成结论与调控方向", desc: "生成结论与调控方向",
pending: "等待平衡判定", pending: "等待平衡判定",
plan: derived.risk.key === "healthy" ? "判定平衡状态并生成监测方案" : "判定超载风险并生成调控方案", plan: ["healthy", "watch"].includes(derived.risk.key) ? "判定平衡状态并生成监测方案" : "判定超载风险并生成调控方案",
}, },
] ]
}) })
@ -590,9 +592,38 @@ function buildVillageRows(rows = [], town = {}) {
const name = pickAreaName(row) const name = pickAreaName(row)
return Boolean(areaCode || name) return Boolean(areaCode || name)
}) })
return validRows const normalizedRows = validRows
.map((row, index) => buildVillageRow(row, index, town)) .map((row, index) => buildVillageRow(row, index, town))
.filter(Boolean) .filter(Boolean)
return dedupeVillageRows(normalizedRows, town)
}
function dedupeVillageRows(rows = [], town = {}) {
const rowMap = new Map()
rows.forEach((row) => {
const dedupeKey = normalizeVillageDedupeKey(row, town)
const current = rowMap.get(dedupeKey)
if (!current) {
rowMap.set(dedupeKey, { ...row, duplicateSources: [row.source].filter(Boolean) })
return
}
const keepNextAreaCode = !current.areaCode && row.areaCode
const nextRow = keepNextAreaCode ? { ...current, ...row } : current
rowMap.set(dedupeKey, {
...nextRow,
duplicateSources: [
...(current.duplicateSources || []),
row.source,
].filter(Boolean),
})
})
return [...rowMap.values()]
}
function normalizeVillageDedupeKey(row = {}, town = {}) {
const nameKey = String(row.name || "").replace(/\s+/g, "").trim()
const townKey = String(row.township || town.name || "").replace(/\s+/g, "").trim()
return `${townKey || "unknown-town"}-${nameKey || row.areaCode || "unknown-village"}`
} }
function buildVillageRow(row = {}, index = 0, town = {}) { function buildVillageRow(row = {}, index = 0, town = {}) {
@ -1831,7 +1862,8 @@ function getProcessStepState(index) {
} }
&.is-healthy strong, &.is-healthy strong,
&.is-balanced strong { &.is-balanced strong,
&.is-watch strong {
color: #7efbf6; color: #7efbf6;
} }

@ -187,6 +187,27 @@
:option="panel.option" :option="panel.option"
:autoresize="true" :autoresize="true"
/> />
<div v-else-if="panel.key === 'home-town-balance-gauge'" class="home-balance-gauge-grid">
<div
v-for="(item, index) in homeBalanceGaugeRows"
:key="item.key"
class="home-balance-gauge-card"
:class="{ 'is-active': index === homeBalanceActiveGaugeIndex }"
:style="homeBalanceGaugeStyle(item)"
>
<div class="home-balance-gauge-head">
<strong>{{ item.status }}</strong>
</div>
<div class="home-balance-gauge-ring">
<div class="home-balance-gauge-value">
<strong>{{ formatNumber(item.value) }}</strong>
<em>%</em>
</div>
</div>
<div class="home-balance-gauge-name">{{ item.name }}</div>
<div class="home-balance-gauge-extra">{{ item.extra }}</div>
</div>
</div>
<business-panel-renderer <business-panel-renderer
v-else v-else
:panel="panel" :panel="panel"
@ -328,11 +349,14 @@ const featurePanelStyle = computed(() => {
if (!featurePanelPoint.value) return {} if (!featurePanelPoint.value) return {}
const panelWidth = 460 const panelWidth = 460
const panelHeight = featurePanelHeight.value const panelHeight = featurePanelHeight.value
const margin = 24 const mapSafeLeft = 430
const mapSafeRight = 430
const point = toHomeScreenPoint(featurePanelPoint.value) const point = toHomeScreenPoint(featurePanelPoint.value)
const xOffset = point.x > 820 ? -panelWidth - 28 : 28 const x = Math.min(
const x = Math.min(Math.max(point.x + xOffset, margin), 1920 - panelWidth - margin) Math.max(point.x - panelWidth / 2, mapSafeLeft),
const y = Math.min(Math.max(point.y - panelHeight - 72, 188), 1080 - panelHeight - 132) 1920 - panelWidth - mapSafeRight,
)
const y = Math.min(Math.max(point.y - panelHeight - 36, 188), 1080 - panelHeight - 132)
return { return {
left: `${x}px`, left: `${x}px`,
top: `${y}px`, top: `${y}px`,
@ -345,20 +369,30 @@ const activeForecastYearIndex = computed(() => getLoopIndex(forecastPeriods.leng
const activeForecastGranularityMeta = computed(() => getForecastGranularity(activeForecastGranularity.value)) const activeForecastGranularityMeta = computed(() => getForecastGranularity(activeForecastGranularity.value))
const trendSeries = computed(() => buildForecastSeriesByGranularity(forecastTrendPeriod, activeForecastGranularityMeta.value.key)) const trendSeries = computed(() => buildForecastSeriesByGranularity(forecastTrendPeriod, activeForecastGranularityMeta.value.key))
const activeForecastTrendIndex = computed(() => getLoopIndex(trendSeries.value.labels.length)) const activeForecastTrendIndex = computed(() => getLoopIndex(trendSeries.value.labels.length))
const homeBalanceGaugeRows = computed(() => { const homeBalanceRows = computed(() => {
const rows = balanceTownRows.filter((item) => item?.name) const rows = balanceTownRows.filter((item) => item?.name)
const pageSize = 3 return rows.map((item, index) => ({
const start = rows.length ? (Math.floor(chartMotionTick.value / 2) * pageSize) % rows.length : 0
return Array.from({ length: Math.min(pageSize, rows.length) }, (_, index) => rows[(start + index) % rows.length]).map((item, index) => ({
key: `home-balance-${item.areaCode || item.key || item.name}`, key: `home-balance-${item.areaCode || item.key || item.name}`,
sourceIndex: index,
name: item.name, name: item.name,
value: item.overloadRate, value: item.overloadRate,
unit: "%", unit: "%",
status: item.level || "总体可控", status: item.level || "总体可控",
extra: `载畜 ${formatNumber(item.livestockCapacity)}`, extra: `载畜 ${formatNumber(item.livestockCapacity)}`,
color: palette[(start + index) % palette.length], color: palette[index % palette.length],
})) }))
}) })
const homeBalanceGaugeRows = computed(() => {
const rows = homeBalanceRows.value
const pageSize = 3
const start = rows.length ? (Math.floor(chartMotionTick.value / 2) * pageSize) % rows.length : 0
return Array.from({ length: Math.min(pageSize, rows.length) }, (_, index) => rows[(start + index) % rows.length])
})
const homeBalanceActiveGaugeIndex = computed(() => {
const length = homeBalanceGaugeRows.value.length
if (!length) return 0
return chartMotionTick.value % length
})
const emptyHomeVideo = { const emptyHomeVideo = {
key: "home-empty-video", key: "home-empty-video",
title: "暂无在线可播放摄像头", title: "暂无在线可播放摄像头",
@ -709,7 +743,13 @@ function initHomeMap() {
itemsTotal, itemsTotal,
}) })
}) })
const loadMapOnce = async (attempt = 0) => { mapAssets.ready?.then(() => loadHomeMapOnce()).catch(() => {
emit("map-ready")
scheduleHomeIdleWork()
})
}
async function loadHomeMapOnce(attempt = 0) {
if (destroyed) return if (destroyed) return
await nextTick() await nextTick()
const scene = mapSceneRef.value const scene = mapSceneRef.value
@ -722,18 +762,34 @@ function initHomeMap() {
return return
} }
if (attempt < 24) { if (attempt < 24) {
window.setTimeout(() => loadMapOnce(attempt + 1), 120) window.setTimeout(() => loadHomeMapOnce(attempt + 1), 120)
return return
} }
emit("map-ready") emit("map-ready")
scheduleHomeIdleWork() scheduleHomeIdleWork()
}
function refreshMapView() {
mapSceneRef.value?.resume?.()
mapSceneRef.value?.refreshView?.()
}
function resetMap() {
closeFeaturePanel()
if (mapSceneRef.value?.hasMap?.()) {
refreshMapView()
return
} }
mapAssets.ready?.then(loadMapOnce).catch(() => { nextTick(() => {
emit("map-ready") loadHomeMapOnce()
scheduleHomeIdleWork()
}) })
} }
defineExpose({
refreshMapView,
resetMap,
})
function scheduleHomeIdleWork() { function scheduleHomeIdleWork() {
cancelHomeIdleWork() cancelHomeIdleWork()
const run = () => { const run = () => {
@ -848,6 +904,20 @@ function stepVideo(step) {
chartMotionTick.value = 0 chartMotionTick.value = 0
} }
function homeBalanceGaugeProgress(item) {
const value = Math.max(0, Number(item?.value || 0))
const maxValue = Math.max(5, ...homeBalanceRows.value.map((row) => Math.max(0, Number(row.value || 0))))
return Math.max(6, Math.min(100, (value / maxValue) * 100))
}
function homeBalanceGaugeStyle(item) {
const color = item?.color || "#30dcff"
return {
"--balance-color": color,
"--balance-progress": `${homeBalanceGaugeProgress(item)}%`,
}
}
async function playCurrentVideo() { async function playCurrentVideo() {
const live = currentHomeVideo.value const live = currentHomeVideo.value
if (!live?.hdPlayUrl || !live?.token) { if (!live?.hdPlayUrl || !live?.token) {
@ -1477,8 +1547,10 @@ function handleOutsideFeaturePointerDown(event) {
.home-layer-tools { .home-layer-tools {
position: absolute; position: absolute;
top: 372px; top: auto;
right: 458px; right: 458px;
bottom: 72px;
left: auto;
z-index: 7; z-index: 7;
width: 248px; width: 248px;
box-sizing: border-box; box-sizing: border-box;
@ -1809,6 +1881,180 @@ function handleOutsideFeaturePointerDown(event) {
height: 100%; height: 100%;
} }
.home-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;
}
.home-balance-gauge-card {
position: relative;
display: grid;
min-width: 0;
min-height: 0;
grid-template-rows: 32px minmax(72px, 1fr) 22px 16px;
justify-items: center;
padding: 10px 8px 9px;
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: 9px;
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);
}
}
.home-balance-gauge-head {
position: relative;
z-index: 1;
display: flex;
width: 100%;
align-items: center;
justify-content: center;
span,
strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
span {
color: rgba(238, 253, 255, 0.58);
font-size: 10px;
font-weight: 700;
}
strong {
width: 100%;
color: var(--balance-color);
font-size: 11px;
font-weight: 900;
text-align: center;
text-shadow: 0 0 8px color-mix(in srgb, var(--balance-color) 34%, transparent);
}
}
.home-balance-gauge-ring {
position: relative;
z-index: 1;
align-self: center;
width: min(94px, 78%);
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);
}
}
.home-balance-gauge-value {
position: absolute;
inset: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
strong {
color: #ffffff;
font-family: D-DIN, Arial, sans-serif;
font-size: 24px;
line-height: 28px;
font-weight: 900;
text-shadow: 0 0 10px color-mix(in srgb, var(--balance-color) 42%, transparent);
}
em {
margin-left: 2px;
color: rgba(238, 253, 255, 0.76);
font-size: 11px;
font-style: normal;
font-weight: 800;
}
}
.home-balance-gauge-name,
.home-balance-gauge-extra {
position: relative;
z-index: 1;
max-width: 100%;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.home-balance-gauge-name {
color: #ffffff;
font-size: 15px;
line-height: 21px;
font-weight: 900;
text-shadow: 0 0 8px rgba(48, 220, 255, 0.18);
}
.home-balance-gauge-extra {
color: rgba(238, 253, 255, 0.66);
font-size: 11px;
line-height: 16px;
font-weight: 700;
}
.home-forecast-trend-card { .home-forecast-trend-card {
position: relative; position: relative;
width: 100%; width: 100%;

@ -1,3 +1,5 @@
import { grasslandYakRows } from "@/views/gdMap/livestockData"
const round = (value, decimals = 2) => { const round = (value, decimals = 2) => {
const factor = 10 ** decimals const factor = 10 ** decimals
return Math.round((Number(value) || 0) * factor) / factor return Math.round((Number(value) || 0) * factor) / factor
@ -110,10 +112,12 @@ export const pastureListRows = [
searchText: `${item.name} ${item.areaName}`, searchText: `${item.name} ${item.areaName}`,
})) }))
export const pastureGrasslandRows = aggregateBy(pastureListRows, "township", "grasslandArea", "亩").map((item) => ({ export const pastureGrasslandRows = grasslandYakRows.map((item) => ({
...item, key: item.zoneName,
value: toWanMu(item.value), name: item.zoneName,
value: toWanMu(item.grasslandArea),
unit: "万亩", unit: "万亩",
extra: "草场资源",
})) }))
export const pastureSeasonRows = Array.from(pastureListRows.reduce((map, item) => { export const pastureSeasonRows = Array.from(pastureListRows.reduce((map, item) => {

@ -1,15 +1,19 @@
<template> <template>
<div class="pasture-management"> <div class="pasture-management">
<map-scene <yak-ol-map
:key="pastureMapKey"
ref="mapSceneRef" ref="mapSceneRef"
class="pasture-map-scene" class="pasture-ol-map-layer"
:active-layer="pastureMapLayer" :active="props.active"
:map-ranking="mapRanking" :tile-url-template="pastureMapTileUrlTemplate"
:admin-boundary-line-scale="2.4" :center="pastureOlCenter"
@scene-mounted="handleMapSceneMounted" :initial-zoom="16"
:max-zoom="21"
:overlay-layer="pastureMapLayer"
@ready="handlePastureOlReady"
/> />
<div class="pasture-map-scrim"></div> <div class="pasture-map-scrim"></div>
<div v-if="!mapReady" class="pasture-map-loading">3D地图加载中</div> <div v-if="!mapReady" class="pasture-map-loading">影像地图加载中</div>
<div class="pasture-management-ui"> <div class="pasture-management-ui">
<div class="pasture-left"> <div class="pasture-left">
@ -18,7 +22,7 @@
<business-panel-renderer :panel="householdColumnPanel" compact /> <business-panel-renderer :panel="householdColumnPanel" compact />
</m-card> </m-card>
<m-card class="pasture-side-card left-card" title="草面积分布" :width="sideCardWidth" :height="sideCardHeight"> <m-card class="pasture-side-card left-card" title="草面积分布" :width="sideCardWidth" :height="sideCardHeight">
<business-panel-renderer :panel="grasslandColumnPanel" compact /> <business-panel-renderer :panel="grasslandColumnPanel" compact />
</m-card> </m-card>
@ -101,11 +105,9 @@
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue" import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
import EZUIKit from "ezuikit-js" import EZUIKit from "ezuikit-js"
import mCard from "@/components/mCard/index.vue" import mCard from "@/components/mCard/index.vue"
import MapScene from "@/views/forestGrassWet/map.vue" import YakOlMap from "@/views/yakManagement/YakOlMap.vue"
import LayerPanel from "@/views/forestGrassWet/components/LayerPanel.vue" import LayerPanel from "@/views/forestGrassWet/components/LayerPanel.vue"
import BusinessPanelRenderer from "@/views/forestGrassWet/components/BusinessPanelRenderer.vue" import BusinessPanelRenderer from "@/views/forestGrassWet/components/BusinessPanelRenderer.vue"
import { Assets } from "@/views/forestGrassWet/assets"
import { getLayerByKey } from "@/config/layers"
import { getHongyuanTownshipNameByCode, hongyuanTownshipsGeoJsonRaw, isHongyuanTownshipName } from "@/config/townshipBoundaries" import { getHongyuanTownshipNameByCode, hongyuanTownshipsGeoJsonRaw, isHongyuanTownshipName } from "@/config/townshipBoundaries"
import { getHomeMonitorings } from "@/services/homeVideoApi" import { getHomeMonitorings } from "@/services/homeVideoApi"
import { getPastureGrasslandsById, getPastureProfile, getPasturesDetailsById } from "@/services/pastureApi" import { getPastureGrasslandsById, getPastureProfile, getPasturesDetailsById } from "@/services/pastureApi"
@ -144,14 +146,19 @@ const sideCardHeight = computed(() => {
const doubleCardHeight = computed(() => sideCardHeight.value * 2 + cardGap) const doubleCardHeight = computed(() => sideCardHeight.value * 2 + cardGap)
const mapSceneRef = ref(null) const mapSceneRef = ref(null)
const mapReady = ref(false) const mapReady = ref(false)
const pastureMapKey = ref(0)
const apiProfile = ref(pastureFallbackProfile) const apiProfile = ref(pastureFallbackProfile)
const pastureRows = ref(pastureListRows) const pastureRows = ref(pastureListRows)
const activeLayerKey = ref("") const pastureBoundaryLayerKey = "pasture-grassland-boundary"
const activeLayerKey = ref(pastureBoundaryLayerKey)
const selectedLegendKey = ref("") const selectedLegendKey = ref("")
const pastureTopStatNames = ["牧户数量", "户均牦牛头数", "户均草地面积", "联户数量"] const pastureTopStatNames = ["牧户数量", "户均牦牛头数", "户均草地面积", "联户数量"]
const pasturePalette = ["#23f6c8", "#35d8ff", "#f0d96a", "#76e36b", "#ff9a5c", "#a59cff", "#20e5c4"] const pasturePalette = ["#23f6c8", "#35d8ff", "#f0d96a", "#76e36b", "#ff9a5c", "#a59cff", "#20e5c4"]
const pastureVideoElementId = `pasture-video-player-${Math.random().toString(36).slice(2)}` const pastureVideoElementId = `pasture-video-player-${Math.random().toString(36).slice(2)}`
const pastureVideoLoadTimeoutMs = 12000 const pastureVideoLoadTimeoutMs = 12000
const pastureMapTileUrlTemplate = "https://qkl-map.oss-cn-chengdu.aliyuncs.com/hy-result/{z}/{x}/{y}.png"
const pastureOlCenter = [103.07984, 33.029058]
const pastureBoundarySldBody = `<?xml version="1.0" encoding="UTF-8"?><StyledLayerDescriptor version="1.0.0" xmlns="http://www.opengis.net/sld" xmlns:ogc="http://www.opengis.net/ogc" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><NamedLayer><Name>t_grassland_result_caodi</Name><UserStyle><Title>pasture boundary</Title><FeatureTypeStyle><Rule><PolygonSymbolizer><Fill><CssParameter name="fill">#FFE35A</CssParameter><CssParameter name="fill-opacity">0</CssParameter></Fill><Stroke><CssParameter name="stroke">#FFE35A</CssParameter><CssParameter name="stroke-width">2.4</CssParameter><CssParameter name="stroke-opacity">0.98</CssParameter></Stroke></PolygonSymbolizer></Rule></FeatureTypeStyle></UserStyle></NamedLayer></StyledLayerDescriptor>`
const emptyPastureVideo = { const emptyPastureVideo = {
key: "pasture-empty-video", key: "pasture-empty-video",
title: "暂无在线可播放摄像头", title: "暂无在线可播放摄像头",
@ -176,10 +183,6 @@ const activePastureVideoManualIndex = ref(0)
const pastureVideoPlaying = ref(false) const pastureVideoPlaying = ref(false)
const pastureVideoLoading = ref(false) const pastureVideoLoading = ref(false)
const pastureVideoError = ref("") const pastureVideoError = ref("")
let mapAssets = null
let mapAssetsReady = false
let mapLoadStarted = false
let mapLoadTimer = null
let destroyed = false let destroyed = false
let pastureEnrichToken = 0 let pastureEnrichToken = 0
let pastureEnrichTimer = null let pastureEnrichTimer = null
@ -235,15 +238,39 @@ const pastureFallbackTownshipByName = {
} }
const pastureTownshipFeatures = normalizeTownshipFeatures(hongyuanTownshipsGeoJsonRaw) const pastureTownshipFeatures = normalizeTownshipFeatures(hongyuanTownshipsGeoJsonRaw)
const pastureLayerOptions = computed(() => { const pastureLayerOptions = computed(() => [
const boundaryLayer = getLayerByKey("grassland", "grassland-distribution") {
if (!boundaryLayer) return [] key: pastureBoundaryLayerKey,
return [{ sourceType: "wms",
...boundaryLayer, renderMode: "image",
layerName: "ne:t_grassland_result_caodi",
name: "草场边界", name: "草场边界",
legends: [{ name: "草场边界", fillColor: "#1D953F", strokeColor: "#1D953F" }], styles: "",
}] sldBody: pastureBoundarySldBody,
}) color: "#FFE35A",
fillColor: "rgba(255, 227, 90, 0.04)",
strokeColor: "#FFE35A",
glowColor: "rgba(255, 227, 90, 0.34)",
zIndex: 35,
opacity: 0.96,
queryBuffer: 8,
mapView: {
targetLngLat: [103.07984, 33.029058],
distanceScale: 0.66,
targetLocalZ: 0.78,
duration: 0.85,
},
legends: [
{
key: "pasture-grassland-boundary",
name: "草场边界",
fillColor: "#FFE35A",
strokeColor: "#FFE35A",
selectable: false,
},
],
},
])
const basePastureMapLayer = computed(() => const basePastureMapLayer = computed(() =>
activeLayerKey.value ? pastureLayerOptions.value.find((layer) => layer.key === activeLayerKey.value) || null : null activeLayerKey.value ? pastureLayerOptions.value.find((layer) => layer.key === activeLayerKey.value) || null : null
) )
@ -270,8 +297,7 @@ const onlineRows = computed(() => {
const ageRows = computed(() => normalizeAgeRows(apiProfile.value.age).length ? normalizeAgeRows(apiProfile.value.age) : pastureAgeRows) const ageRows = computed(() => normalizeAgeRows(apiProfile.value.age).length ? normalizeAgeRows(apiProfile.value.age) : pastureAgeRows)
const genderRows = computed(() => normalizeGenderRows(apiProfile.value.age).length ? normalizeGenderRows(apiProfile.value.age) : pastureGenderRows) const genderRows = computed(() => normalizeGenderRows(apiProfile.value.age).length ? normalizeGenderRows(apiProfile.value.age) : pastureGenderRows)
const grasslandRows = computed(() => { const grasslandRows = computed(() => {
const aggregatedRows = aggregatePastureAreaRows(pastureRows.value) return completePastureGrasslandRows(pastureGrasslandRows)
return completePastureGrasslandRows(aggregatedRows.length ? aggregatedRows : pastureGrasslandRows)
}) })
const mapRanking = computed(() => ({ const mapRanking = computed(() => ({
title: "牧户分布排行", title: "牧户分布排行",
@ -392,10 +418,8 @@ onBeforeUnmount(() => {
destroyed = true destroyed = true
window.removeEventListener("resize", updateScreenViewportHeight) window.removeEventListener("resize", updateScreenViewportHeight)
stopPastureVideoPlayer() stopPastureVideoPlayer()
if (mapLoadTimer) window.clearTimeout(mapLoadTimer)
if (pastureEnrichTimer) window.clearTimeout(pastureEnrichTimer) if (pastureEnrichTimer) window.clearTimeout(pastureEnrichTimer)
pastureEnrichToken += 1 pastureEnrichToken += 1
mapSceneRef.value?.destroyMap?.()
}) })
watch( watch(
@ -404,13 +428,11 @@ watch(
if (!active) stopPastureVideoPlayer({ resetError: true }) if (!active) stopPastureVideoPlayer({ resetError: true })
if (!mapSceneRef.value) return if (!mapSceneRef.value) return
if (active) { if (active) {
schedulePastureMapSceneLoad() mapSceneRef.value.refreshView?.()
mapSceneRef.value.resume?.()
} else { } else {
if (pastureEnrichTimer) window.clearTimeout(pastureEnrichTimer) if (pastureEnrichTimer) window.clearTimeout(pastureEnrichTimer)
pastureEnrichTimer = null pastureEnrichTimer = null
pastureEnrichToken += 1 pastureEnrichToken += 1
mapSceneRef.value.pause?.()
} }
}, },
) )
@ -804,74 +826,40 @@ async function handlePastureSelect(row) {
} }
function initPastureMap() { function initPastureMap() {
mapAssets = new Assets() mapReady.value = false
mapAssetsReady = false
emit("loading-progress", { progress: 0 }) emit("loading-progress", { progress: 0 })
mapAssets.instance?.on?.("onProgress", (path, itemsLoaded, itemsTotal) => {
const total = itemsTotal || 1
emit("loading-progress", {
progress: Math.floor((itemsLoaded / total) * 100),
path,
itemsLoaded,
itemsTotal,
})
})
mapAssets.ready?.then(() => {
mapAssetsReady = true
schedulePastureMapSceneLoad()
}).catch(() => {
mapReady.value = true
emit("map-ready")
})
} }
function handleMapSceneMounted() { function handlePastureOlReady() {
schedulePastureMapSceneLoad() mapReady.value = true
emit("loading-progress", { progress: 100 })
emit("map-ready")
} }
function schedulePastureMapSceneLoad() { function refreshMapView() {
if (destroyed || !props.active || !mapAssetsReady || mapLoadStarted) return mapSceneRef.value?.refreshView?.()
mapLoadStarted = true
if (mapLoadTimer) window.clearTimeout(mapLoadTimer)
mapLoadTimer = window.setTimeout(() => {
mapLoadTimer = null
loadPastureMapScene()
}, 600)
} }
async function loadPastureMapScene(attempt = 0) { function resetMap() {
if (destroyed || !mapAssetsReady || !mapAssets) return if (mapSceneRef.value?.hasMap?.()) {
if (!props.active) {
mapLoadStarted = false
return
}
await nextTick()
if (!props.active) {
mapLoadStarted = false
return
}
const scene = mapSceneRef.value
scene?.loadMap?.(mapAssets)
if (scene?.canvasMap) {
mapReady.value = true mapReady.value = true
emit("loading-progress", { progress: 100 }) mapSceneRef.value?.resetView?.()
emit("map-ready")
if (props.active) {
scene.play?.()
} else {
scene.pause?.()
}
return return
} }
if (attempt < 24) { mapReady.value = false
window.setTimeout(() => loadPastureMapScene(attempt + 1), 120) pastureMapKey.value += 1
return nextTick(() => {
} window.setTimeout(() => {
mapLoadStarted = false mapSceneRef.value?.refreshView?.()
mapReady.value = true }, 160)
emit("map-ready") })
} }
defineExpose({
refreshMapView,
resetMap,
})
function emitPastureTopStats() { function emitPastureTopStats() {
emit("module-stats", { emit("module-stats", {
moduleKey: "pasture-management", moduleKey: "pasture-management",
@ -1491,7 +1479,8 @@ function isLngLatInPolygon(point, polygon) {
background: #03121c; background: #03121c;
} }
.pasture-map-scene { .pasture-map-scene,
.pasture-ol-map-layer {
position: absolute; position: absolute;
inset: 0; inset: 0;
z-index: 1; z-index: 1;
@ -2022,8 +2011,10 @@ function isLngLatInPolygon(point, polygon) {
.pasture-map-layer-tools { .pasture-map-layer-tools {
position: absolute; position: absolute;
top: 372px; top: auto;
right: 458px; right: 458px;
bottom: 72px;
left: auto;
z-index: 7; z-index: 7;
width: 248px; width: 248px;
height: 430px; height: 430px;

@ -1,5 +1,6 @@
<template> <template>
<div <div
ref="platformScreenRef"
class="large-screen platform-screen" class="large-screen platform-screen"
:class="{ :class="{
'is-home': activeModule.key === 'home', 'is-home': activeModule.key === 'home',
@ -20,6 +21,7 @@
class="platform-module-pane" class="platform-module-pane"
> >
<home-dashboard <home-dashboard
ref="homeDashboardRef"
:active="activeModule.key === 'home'" :active="activeModule.key === 'home'"
@loading-progress="handleHomeLoadingProgress" @loading-progress="handleHomeLoadingProgress"
@map-ready="handleHomeMapReady" @map-ready="handleHomeMapReady"
@ -32,6 +34,7 @@
class="platform-module-pane" class="platform-module-pane"
> >
<forest-grass-wet <forest-grass-wet
ref="forestGrassWetRef"
:active="activeModule.key === 'forest-grass-wet'" :active="activeModule.key === 'forest-grass-wet'"
:show-loading="false" :show-loading="false"
@topic-stats="handleModuleStats" @topic-stats="handleModuleStats"
@ -43,6 +46,7 @@
class="platform-module-pane" class="platform-module-pane"
> >
<ecological-protection <ecological-protection
ref="ecologicalProtectionRef"
:active="activeModule.key === 'ecological-protection'" :active="activeModule.key === 'ecological-protection'"
:active-view="activeEcologyView" :active-view="activeEcologyView"
:show-loading="false" :show-loading="false"
@ -68,6 +72,7 @@
class="platform-module-pane" class="platform-module-pane"
> >
<yak-management <yak-management
ref="yakManagementRef"
:active="activeModule.key === 'yak-management'" :active="activeModule.key === 'yak-management'"
@module-stats="handleModuleStats" @module-stats="handleModuleStats"
/> />
@ -78,6 +83,7 @@
class="platform-module-pane" class="platform-module-pane"
> >
<pasture-management <pasture-management
ref="pastureManagementRef"
:active="activeModule.key === 'pasture-management'" :active="activeModule.key === 'pasture-management'"
@loading-progress="handleHomeLoadingProgress" @loading-progress="handleHomeLoadingProgress"
@map-ready="handleHomeMapReady" @map-ready="handleHomeMapReady"
@ -90,6 +96,7 @@
class="platform-module-pane" class="platform-module-pane"
> >
<yak-industry-chain <yak-industry-chain
ref="yakIndustryChainRef"
:active="activeModule.key === 'yak-industry-chain'" :active="activeModule.key === 'yak-industry-chain'"
@loading-progress="handleHomeLoadingProgress" @loading-progress="handleHomeLoadingProgress"
@map-ready="handleHomeMapReady" @map-ready="handleHomeMapReady"
@ -163,6 +170,15 @@
</template> </template>
</div> </div>
<div v-if="mapToolsVisible" class="platform-map-tools" aria-label="地图工具">
<button type="button" :title="platformFullscreenLabel" :aria-label="platformFullscreenLabel" @click="handlePlatformFullscreen">
<span class="platform-map-tool-icon is-fullscreen" aria-hidden="true"></span>
</button>
<button type="button" title="复位地图" aria-label="复位地图" @click="handleActiveMapReset">
<span class="platform-map-tool-icon is-reset" aria-hidden="true"></span>
</button>
</div>
<div v-if="activeModule.status !== 'ready'" class="module-coming"> <div v-if="activeModule.status !== 'ready'" class="module-coming">
<div class="module-coming-title">{{ activeModule.label }}</div> <div class="module-coming-title">{{ activeModule.label }}</div>
<div class="module-coming-text">模块建设中平台框架已预留入口</div> <div class="module-coming-text">模块建设中平台框架已预留入口</div>
@ -263,6 +279,14 @@ const renderedModules = ref({ home: true })
const moduleTopStats = ref({}) const moduleTopStats = ref({})
const loadingVisible = ref(true) const loadingVisible = ref(true)
const loadingProgress = ref(0) const loadingProgress = ref(0)
const platformScreenRef = ref(null)
const homeDashboardRef = ref(null)
const forestGrassWetRef = ref(null)
const ecologicalProtectionRef = ref(null)
const yakManagementRef = ref(null)
const pastureManagementRef = ref(null)
const yakIndustryChainRef = ref(null)
const platformFullscreen = ref(false)
let screenAutofit = null let screenAutofit = null
let loadingHideStarted = false let loadingHideStarted = false
let pendingModuleRenderTimer = null let pendingModuleRenderTimer = null
@ -292,6 +316,8 @@ const topStatIconPool = [
const topStatIconSet = new Set(topStatIconPool) const topStatIconSet = new Set(topStatIconPool)
const activeModule = computed(() => getModuleByKey(activeModuleKey.value)) const activeModule = computed(() => getModuleByKey(activeModuleKey.value))
const platformFullscreenLabel = computed(() => (platformFullscreen.value ? "退出" : "全屏"))
const mapToolsVisible = computed(() => !["smart-management", "grass-livestock-balance"].includes(activeModule.value.key))
const bottomMenuItems = computed(() => { const bottomMenuItems = computed(() => {
if (activeModuleKey.value === "ecological-protection") return ecologicalProtectionMenu if (activeModuleKey.value === "ecological-protection") return ecologicalProtectionMenu
return platformModules return platformModules
@ -343,11 +369,14 @@ onMounted(() => {
el: "#large-screen", el: "#large-screen",
resize: true, resize: true,
}) })
bindPlatformFullscreenEvents()
syncPlatformFullscreenState()
resetRootScroll() resetRootScroll()
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
screenAutofit?.off?.("#large-screen") screenAutofit?.off?.("#large-screen")
unbindPlatformFullscreenEvents()
if (pendingModuleRenderTimer) window.clearTimeout(pendingModuleRenderTimer) if (pendingModuleRenderTimer) window.clearTimeout(pendingModuleRenderTimer)
if (pendingModuleRenderFrame) window.cancelAnimationFrame(pendingModuleRenderFrame) if (pendingModuleRenderFrame) window.cancelAnimationFrame(pendingModuleRenderFrame)
gsap.killTweensOf(loadingTweenState) gsap.killTweensOf(loadingTweenState)
@ -550,6 +579,82 @@ function resetRootScroll() {
if (app) app.scrollTop = 0 if (app) app.scrollTop = 0
} }
const platformFullscreenEvents = ["fullscreenchange", "webkitfullscreenchange", "mozfullscreenchange", "MSFullscreenChange"]
function getPlatformFullscreenElement() {
if (typeof document === "undefined") return null
return document.fullscreenElement
|| document.webkitFullscreenElement
|| document.mozFullScreenElement
|| document.msFullscreenElement
|| null
}
function syncPlatformFullscreenState() {
const fullscreenElement = getPlatformFullscreenElement()
const root = platformScreenRef.value
platformFullscreen.value = Boolean(root && fullscreenElement && (
fullscreenElement === root
|| root.contains(fullscreenElement)
|| fullscreenElement.contains?.(root)
))
}
function bindPlatformFullscreenEvents() {
if (typeof document === "undefined") return
platformFullscreenEvents.forEach((eventName) => document.addEventListener(eventName, syncPlatformFullscreenState))
}
function unbindPlatformFullscreenEvents() {
if (typeof document === "undefined") return
platformFullscreenEvents.forEach((eventName) => document.removeEventListener(eventName, syncPlatformFullscreenState))
}
function refreshYakMapAfterResize() {
getActiveMapModuleRef()?.refreshMapView?.()
}
async function handlePlatformFullscreen() {
if (typeof document === "undefined") return
const fullscreenElement = getPlatformFullscreenElement()
try {
if (fullscreenElement) {
const exitFullscreen = document.exitFullscreen
|| document.webkitExitFullscreen
|| document.mozCancelFullScreen
|| document.msExitFullscreen
await exitFullscreen?.call(document)
} else {
const target = platformScreenRef.value || document.documentElement
const requestFullscreen = target.requestFullscreen
|| target.webkitRequestFullscreen
|| target.mozRequestFullScreen
|| target.msRequestFullscreen
await requestFullscreen?.call(target)
}
} catch (error) {
console.warn("[platform] fullscreen toggle failed", error)
} finally {
syncPlatformFullscreenState()
window.setTimeout(refreshYakMapAfterResize, 80)
window.setTimeout(refreshYakMapAfterResize, 280)
}
}
function handleActiveMapReset() {
getActiveMapModuleRef()?.resetMap?.()
}
function getActiveMapModuleRef() {
if (activeModule.value.key === "home") return homeDashboardRef.value
if (activeModule.value.key === "forest-grass-wet") return forestGrassWetRef.value
if (activeModule.value.key === "ecological-protection") return ecologicalProtectionRef.value
if (activeModule.value.key === "yak-management") return yakManagementRef.value
if (activeModule.value.key === "pasture-management") return pastureManagementRef.value
if (activeModule.value.key === "yak-industry-chain") return yakIndustryChainRef.value
return null
}
function handleLogout() { function handleLogout() {
logout() logout()
} }
@ -652,6 +757,100 @@ function handleLogout() {
z-index: 1; z-index: 1;
} }
.platform-map-tools {
position: absolute;
left: 448px;
bottom: 168px;
z-index: 80;
display: flex;
flex-direction: column;
gap: 7px;
pointer-events: all;
transform: perspective(620px) rotateY(6deg);
transform-origin: left center;
button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
padding: 0;
border: 1px solid rgba(126, 251, 246, 0.62);
border-radius: 3px;
color: #f0feff;
font-weight: 900;
background:
linear-gradient(180deg, rgba(18, 99, 123, 0.92), rgba(4, 36, 56, 0.96)),
rgba(4, 24, 37, 0.9);
box-shadow:
0 0 14px rgba(48, 220, 255, 0.32),
0 0 3px rgba(126, 251, 246, 0.4),
inset 0 0 0 1px rgba(255, 255, 255, 0.1),
inset 0 -8px 14px rgba(48, 220, 255, 0.16);
cursor: pointer;
&:hover {
border-color: rgba(126, 251, 246, 0.76);
color: #ffffff;
background:
linear-gradient(180deg, rgba(25, 127, 148, 0.96), rgba(6, 48, 68, 0.98)),
rgba(6, 35, 52, 0.92);
box-shadow:
0 0 18px rgba(48, 220, 255, 0.42),
0 0 4px rgba(126, 251, 246, 0.5),
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
inset 0 -8px 14px rgba(48, 220, 255, 0.18);
}
&:active {
transform: translateY(1px);
}
}
}
.platform-map-tool-icon {
position: relative;
flex: 0 0 15px;
width: 15px;
height: 15px;
color: #7efbf6;
&.is-fullscreen {
background:
linear-gradient(currentColor, currentColor) left top / 5px 2px no-repeat,
linear-gradient(currentColor, currentColor) left top / 2px 5px no-repeat,
linear-gradient(currentColor, currentColor) right top / 5px 2px no-repeat,
linear-gradient(currentColor, currentColor) right top / 2px 5px no-repeat,
linear-gradient(currentColor, currentColor) left bottom / 5px 2px no-repeat,
linear-gradient(currentColor, currentColor) left bottom / 2px 5px no-repeat,
linear-gradient(currentColor, currentColor) right bottom / 5px 2px no-repeat,
linear-gradient(currentColor, currentColor) right bottom / 2px 5px no-repeat;
}
&.is-reset {
width: 15px;
height: 15px;
border: 2px solid currentColor;
border-left-color: transparent;
border-radius: 50%;
box-sizing: border-box;
&::after {
content: "";
position: absolute;
left: -2px;
top: 0;
width: 0;
height: 0;
border-top: 3px solid transparent;
border-bottom: 3px solid transparent;
border-right: 5px solid currentColor;
transform: rotate(-18deg);
}
}
}
.top-menu { .top-menu {
position: absolute; position: absolute;
left: 50%; left: 50%;

@ -320,7 +320,6 @@ export const breedingLevelRows = buildBreedingLevelRows()
export const forageSecurityRows = aggregateRows(forageStorageItems, normalizeTown, (item) => metricValue(item, ["总库容 (吨)"])) export const forageSecurityRows = aggregateRows(forageStorageItems, normalizeTown, (item) => metricValue(item, ["总库容 (吨)"]))
.map((item) => ({ ...item, unit: "吨", extra: `${item.count}个库点` })) .map((item) => ({ ...item, unit: "吨", extra: `${item.count}个库点` }))
.slice(0, 6)
export const industryValueRows = [ export const industryValueRows = [
{ key: "milk-processing", name: "乳品加工", value: sumBy(processingItems.filter(isMilkProcessing), (item) => metricValue(item, ["年产值 (万元)"])), unit: "万元", color: "#32dcff" }, { key: "milk-processing", name: "乳品加工", value: sumBy(processingItems.filter(isMilkProcessing), (item) => metricValue(item, ["年产值 (万元)"])), unit: "万元", color: "#32dcff" },
@ -349,9 +348,9 @@ export function getStagePanelConfig(stageKey) {
const configs = { const configs = {
"family-pasture": { "family-pasture": {
left: [ left: [
chartPanel("family-town", "牧场镇域分布", "bar", getStageCapacityRows(stageKey).slice(0, 5), { variant: "rankBar" }), chartPanel("family-town", "牧场镇域分布", "column", getStageCapacityRows(stageKey), { variant: "townCompact", scrollable: false }),
chartPanel("family-town-stock", "镇域存栏支撑", "column", getTownYakStockRows(), { variant: "slim" }), chartPanel("family-town-stock", "镇域存栏支撑", "column", getTownYakStockRows(), { variant: "townCompact", scrollable: false }),
chartPanel("family-grass-resource", "草地资源保障", "heat", getTownGrasslandResourceRows()), chartPanel("family-grass-resource", "草地资源保障", "structure", getTownGrasslandResourceRows()),
], ],
right: [ right: [
industryChainListPanel(stageKey), industryChainListPanel(stageKey),
@ -359,9 +358,9 @@ export function getStagePanelConfig(stageKey) {
}, },
"artificial-grassland": { "artificial-grassland": {
left: [ left: [
chartPanel("grass-town", "种草面积排行", "column", getStageCapacityRows(stageKey).slice(0, 7), { variant: "slim" }), chartPanel("grass-town", "种草面积分布", "column", getStageCapacityRows(stageKey), { variant: "townCompact", scrollable: false }),
chartPanel("grass-heat", "基地面积热区", "heat", getForageBaseAreaRows()), chartPanel("grass-base-town", "基地点位分布", "column", getTownCountRows(forageBaseItems, "基地点位"), { variant: "townCompact", scrollable: false }),
chartPanel("grass-type", "草种保障结构", "structure", forageGrassTypeRows), chartPanel("grass-type", "草种保障结构", "donut", forageGrassTypeRows, { variant: "ring" }),
], ],
right: [ right: [
industryChainListPanel(stageKey), industryChainListPanel(stageKey),
@ -369,9 +368,9 @@ export function getStagePanelConfig(stageKey) {
}, },
"scale-breeding": { "scale-breeding": {
left: [ left: [
chartPanel("breeding-stock", "设计存栏排行", "bar", getStageCapacityRows(stageKey).slice(0, 5), { variant: "rankBar" }), chartPanel("breeding-stock", "设计存栏分布", "column", getStageCapacityRows(stageKey), { variant: "townCompact", scrollable: false }),
chartPanel("breeding-output", "年出栏能力排行", "bar", getBreedingOutputRows(), { variant: "raceBar" }), chartPanel("breeding-output", "年出栏能力分布", "column", getTownMetricRows(breedingBaseItems, ["年出栏量 (头)"], { unit: "头", extra: "年出栏" }), { variant: "townCompact", scrollable: false }),
chartPanel("breeding-level", "育种等级结构", "donut", breedingLevelRows), chartPanel("breeding-level", "育种等级结构", "donut", breedingLevelRows, { variant: "ring" }),
], ],
right: [ right: [
industryChainListPanel(stageKey), industryChainListPanel(stageKey),
@ -379,8 +378,8 @@ export function getStagePanelConfig(stageKey) {
}, },
"forage-storage": { "forage-storage": {
left: [ left: [
chartPanel("storage-town", "库容镇域排行", "bar", getStageCapacityRows(stageKey).slice(0, 7)), chartPanel("storage-town", "库容镇域分布", "column", getStageCapacityRows(stageKey), { variant: "townCompact", scrollable: false }),
chartPanel("storage-point", "库点能力热区", "heat", getStoragePointRows()), chartPanel("storage-point", "库点数量分布", "column", getTownCountRows(forageStorageItems, "储备库点"), { variant: "townCompact", scrollable: false }),
chartPanel("storage-grass", "储备草种结构", "structure", forageGrassTypeRows), chartPanel("storage-grass", "储备草种结构", "structure", forageGrassTypeRows),
], ],
right: [ right: [
@ -389,9 +388,9 @@ export function getStagePanelConfig(stageKey) {
}, },
"slaughter-processing": { "slaughter-processing": {
left: [ left: [
chartPanel("processing-value", "产值结构", "bar", industryValueRows), chartPanel("processing-value", "产值结构", "donut", industryValueRows, { variant: "ring" }),
chartPanel("processing-chain", "产能与带动", "structure", processingChainRows), chartPanel("processing-chain", "产能与带动", "structure", processingChainRows),
chartPanel("processing-heads", "屠宰能力排行", "bar", getSlaughterHeadRows(), { variant: "raceBar" }), chartPanel("processing-heads", "屠宰能力分布", "column", getTownMetricRows(slaughterItems, ["年屠宰总量 (头)"], { unit: "头", extra: "年屠宰" }), { variant: "townCompact", scrollable: false }),
], ],
right: [ right: [
industryChainListPanel(stageKey), industryChainListPanel(stageKey),
@ -399,8 +398,8 @@ export function getStagePanelConfig(stageKey) {
}, },
"farm-equipment": { "farm-equipment": {
left: [ left: [
chartPanel("equipment-device", "识别设备分布", "column", equipmentDeviceRows.slice(0, 7), { variant: "slim" }), chartPanel("equipment-device", "识别设备分布", "column", completeTownColumnRows(equipmentDeviceRows, { unit: "套", extra: "识别设备" }), { variant: "townCompact", scrollable: false }),
chartPanel("equipment-recognition", "识别样本排行", "bar", equipmentRecognitionRows.slice(0, 7)), chartPanel("equipment-recognition", "识别样本分布", "column", completeTownColumnRows(equipmentRecognitionRows, { unit: "头", extra: "识别样本" }), { variant: "townCompact", scrollable: false }),
], ],
right: [ right: [
industryChainListPanel(stageKey), industryChainListPanel(stageKey),
@ -408,9 +407,9 @@ export function getStagePanelConfig(stageKey) {
}, },
"social-service": { "social-service": {
left: [ left: [
chartPanel("service-type", "服务主体结构", "structure", serviceTypeRows), chartPanel("service-type", "服务主体结构", "donut", serviceTypeRows, { variant: "ring" }),
chartPanel("service-credit", "金融授信排行", "bar", getFinanceCreditRows()), chartPanel("service-town", "服务镇域分布", "column", getTownCountRows([...diseaseControlItems, ...trainingItems, ...financeItems, ...insuranceItems], "服务主体"), { variant: "townCompact", scrollable: false }),
chartPanel("service-coverage", "服务覆盖对比", "bar", getServiceCoverageRows(), { variant: "raceBar" }), chartPanel("service-training-town", "培训人次分布", "column", getTownMetricRows(trainingItems, ["年培训人次"], { unit: "人次", extra: "技术培训" }), { variant: "townCompact", scrollable: false }),
chartPanel("service-training", "培训服务能力", "line", getTrainingRows()), chartPanel("service-training", "培训服务能力", "line", getTrainingRows()),
], ],
right: [ right: [
@ -421,9 +420,9 @@ export function getStagePanelConfig(stageKey) {
const fallback = { const fallback = {
left: [ left: [
chartPanel(`${stage.key}-rank`, `${stage.metricLabel}排行`, "bar", getStageCapacityRows(stageKey).slice(0, 7)), chartPanel(`${stage.key}-rank`, `${stage.metricLabel}分布`, "column", getStageCapacityRows(stageKey), { variant: "townCompact", scrollable: false }),
chartPanel(`${stage.key}-subject`, "主体结构", "structure", subjectRows), chartPanel(`${stage.key}-subject`, "主体结构", "structure", subjectRows),
chartPanel(`${stage.key}-town`, "镇域覆盖热区", "heat", getStageTownRows(stageKey).slice(0, 8)), chartPanel(`${stage.key}-town`, "镇域覆盖分布", "column", completeTownColumnRows(getStageTownRows(stageKey), { unit: "个", extra: "镇域覆盖" }), { variant: "townCompact", scrollable: false }),
chartPanel(`${stage.key}-capacity`, "能力规模分布", "split", getStageCapacityRows(stageKey).slice(0, 7)), chartPanel(`${stage.key}-capacity`, "能力规模分布", "split", getStageCapacityRows(stageKey).slice(0, 7)),
], ],
right: [ right: [
@ -455,30 +454,22 @@ export function getStageInsightRows(stageKey) {
export function getStageCapacityRows(stageKey) { export function getStageCapacityRows(stageKey) {
if (stageKey === "family-pasture") { if (stageKey === "family-pasture") {
return aggregateRows(familyPastureItems, normalizeTown, () => 1) const rows = aggregateRows(familyPastureItems, normalizeTown, () => 1)
.map((item) => ({ ...item, unit: "户", extra: "家庭生态牧场" })) .map((item) => ({ ...item, unit: "户", extra: "家庭生态牧场" }))
.slice(0, 7) return completeTownColumnRows(rows, { unit: "户", extra: "家庭生态牧场" })
} }
if (stageKey === "artificial-grassland") { if (stageKey === "artificial-grassland") {
return aggregateRows(forageBaseItems, normalizeTown, (item) => metricValue(item, ["种植面积 (亩)", "acreage"]) || parseNumber(item.scale)) const rows = aggregateRows(forageBaseItems, normalizeTown, (item) => metricValue(item, ["种植面积 (亩)", "acreage"]) || parseNumber(item.scale))
.map((item) => ({ ...item, value: round(item.value / 10000, 2), unit: "万亩", extra: `${item.count}个基地` })) .map((item) => ({ ...item, value: round(item.value / 10000, 2), unit: "万亩", extra: `${item.count}个基地` }))
.slice(0, 7) return completeTownColumnRows(rows, { unit: "万亩", extra: "种草基地" })
} }
if (stageKey === "scale-breeding") { if (stageKey === "scale-breeding") {
return categoryItems("YC-BREEDING-BASE") const rows = aggregateRows(categoryItems("YC-BREEDING-BASE"), normalizeTown, (item) => metricValue(item, ["设计存栏量 (头)"]))
.map((item) => ({ .map((item) => ({ ...item, unit: "头", extra: `${item.count}个基地` }))
key: item.id, return completeTownColumnRows(rows, { unit: "头", extra: "养殖基地" })
name: compactName(item.name),
value: metricValue(item, ["设计存栏量 (头)"]),
unit: "头",
extra: `年出栏${formatNumber(metricValue(item, ["年出栏量 (头)"]))}`,
}))
.filter((item) => item.value > 0)
.sort(sortByValue)
.slice(0, 7)
} }
if (stageKey === "forage-storage") { if (stageKey === "forage-storage") {
return forageSecurityRows return completeTownColumnRows(forageSecurityRows, { unit: "吨", extra: "饲草储备" })
} }
if (stageKey === "slaughter-processing") { if (stageKey === "slaughter-processing") {
return [...processingItems, ...slaughterItems] return [...processingItems, ...slaughterItems]
@ -846,7 +837,7 @@ function industryChainListPanel(stageKey) {
} }
function getTownYakStockRows() { function getTownYakStockRows() {
return grasslandYakRows const rows = grasslandYakRows
.map((item) => ({ .map((item) => ({
key: item.zoneName, key: item.zoneName,
name: item.zoneName, name: item.zoneName,
@ -855,8 +846,7 @@ function getTownYakStockRows() {
extra: "牦牛存栏", extra: "牦牛存栏",
})) }))
.filter((item) => item.value > 0) .filter((item) => item.value > 0)
.sort(sortByValue) return completeTownColumnRows(rows, { unit: "头", extra: "牦牛存栏" })
.slice(0, 8)
} }
function getTownGrasslandResourceRows() { function getTownGrasslandResourceRows() {
@ -870,7 +860,26 @@ function getTownGrasslandResourceRows() {
})) }))
.filter((item) => item.value > 0) .filter((item) => item.value > 0)
.sort(sortByValue) .sort(sortByValue)
.slice(0, 8) }
function getTownMetricRows(items = [], metricNames = [], options = {}) {
const rows = aggregateRows(items, normalizeTown, (item) => metricValue(item, metricNames))
.map((item) => ({
...item,
unit: options.unit || "",
extra: options.extra || `${item.count}个点位`,
}))
return completeTownColumnRows(rows, options)
}
function getTownCountRows(items = [], extra = "点位") {
const rows = aggregateRows(items, normalizeTown, () => 1)
.map((item) => ({
...item,
unit: "个",
extra,
}))
return completeTownColumnRows(rows, { unit: "个", extra })
} }
function getForageBaseAreaRows() { function getForageBaseAreaRows() {
@ -1035,6 +1044,36 @@ function aggregateRows(rows, nameGetter, valueGetter) {
return [...map.values()].sort(sortByValue) return [...map.values()].sort(sortByValue)
} }
function completeTownColumnRows(rows = [], options = {}) {
const byTown = new Map()
rows.forEach((row) => {
const name = cleanText(row?.name)
const matchedTown = townNames.find((town) => town === name) || townNames.find((town) => {
const stem = town.replace(/[镇乡]$/, "")
return stem.length >= 2 && name.includes(stem)
})
if (!matchedTown) return
byTown.set(matchedTown, row)
})
return townNames.map((town, index) => {
const row = byTown.get(town)
const rawValue = Number(row?.value || 0)
const hasValue = Number.isFinite(rawValue) && rawValue > 0
return {
key: row?.key || town,
name: town,
value: hasValue ? rawValue : 0.001,
displayValue: hasValue ? row.displayValue : "0",
scaleValue: row?.scaleValue,
unit: row?.unit || options.unit || "",
extra: row?.extra || options.extra || "",
color: row?.color,
zero: !hasValue,
sourceIndex: index,
}
})
}
function metricValue(item = {}, names = []) { function metricValue(item = {}, names = []) {
const metrics = item.metrics || {} const metrics = item.metrics || {}
const nameList = Array.isArray(names) ? names : [names] const nameList = Array.isArray(names) ? names : [names]

@ -21,8 +21,14 @@
:width="sideCardWidth" :width="sideCardWidth"
:height="panel.height" :height="panel.height"
> >
<v-chart
v-if="isTownColumnPanel(panel)"
class="industry-town-column-chart"
:option="createTownColumnOption(panel)"
:autoresize="true"
/>
<business-panel-renderer <business-panel-renderer
v-if="panel.renderer === 'business'" v-else-if="panel.renderer === 'business'"
:panel="panel" :panel="panel"
compact compact
@row-select="handleIndustryListSelect" @row-select="handleIndustryListSelect"
@ -50,8 +56,14 @@
:width="sideCardWidth" :width="sideCardWidth"
:height="panel.height" :height="panel.height"
> >
<v-chart
v-if="isTownColumnPanel(panel)"
class="industry-town-column-chart"
:option="createTownColumnOption(panel)"
:autoresize="true"
/>
<business-panel-renderer <business-panel-renderer
v-if="panel.renderer === 'business'" v-else-if="panel.renderer === 'business'"
:panel="panel" :panel="panel"
compact compact
@row-select="handleIndustryListSelect" @row-select="handleIndustryListSelect"
@ -95,6 +107,7 @@
<script setup> <script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue" import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
import VChart from "vue-echarts"
import mCard from "@/components/mCard/index.vue" import mCard from "@/components/mCard/index.vue"
import MapScene from "@/views/forestGrassWet/map.vue" import MapScene from "@/views/forestGrassWet/map.vue"
import BusinessPanelRenderer from "@/views/forestGrassWet/components/BusinessPanelRenderer.vue" import BusinessPanelRenderer from "@/views/forestGrassWet/components/BusinessPanelRenderer.vue"
@ -281,6 +294,182 @@ function filterDisplayPanels(panels = []) {
return panels.filter((panel) => panel?.type !== "iconMetric") return panels.filter((panel) => panel?.type !== "iconMetric")
} }
function isTownColumnPanel(panel = {}) {
return panel?.renderer === "business" && panel?.type === "column" && panel?.variant === "townCompact"
}
function createTownColumnOption(panel = {}) {
const rows = Array.isArray(panel.rows) ? panel.rows : []
const values = rows.map((item) => Number(item?.zero ? 0 : item?.value || 0))
const maxValue = Math.max(...values, 1)
const unit = getSharedUnit(rows)
return {
animationDuration: 720,
animationEasing: "cubicOut",
grid: {
top: 30,
right: 8,
bottom: 58,
left: 8,
containLabel: false,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
shadowStyle: {
color: "rgba(48, 220, 255, 0.08)",
},
},
confine: true,
backgroundColor: "rgba(4, 24, 36, 0.94)",
borderColor: "rgba(48, 220, 255, 0.42)",
borderWidth: 1,
padding: [7, 9],
textStyle: {
color: "#e8fcff",
fontSize: 12,
},
formatter(params = []) {
const data = params[0]?.data || {}
const value = data.zero ? "0" : data.displayValue
const valueUnit = data.unit || unit || ""
const extra = data.extra ? `<br/><span style="color:rgba(232,252,255,.62)">${data.extra}</span>` : ""
return `${data.name || ""}<br/><b style="color:#35f1ff">${value}${valueUnit}</b>${extra}`
},
},
graphic: unit
? [{
type: "text",
right: 10,
top: 4,
silent: true,
style: {
text: `单位:${unit}`,
fill: "rgba(232,252,255,0.68)",
font: "10px PingFang SC, Arial",
},
}]
: [],
xAxis: {
type: "category",
data: rows.map((item) => item?.name || ""),
axisTick: { show: false },
axisLine: {
show: true,
lineStyle: {
color: "rgba(68, 238, 255, 0.42)",
},
},
axisLabel: {
color: "rgba(255,255,255,0.9)",
fontSize: 11,
fontWeight: 700,
lineHeight: 14,
interval: 0,
margin: 10,
formatter: formatTownAxisLabel,
},
},
yAxis: {
type: "value",
min: 0,
max: Math.ceil(maxValue * 1.18),
splitNumber: 3,
axisLabel: { show: false },
axisTick: { show: false },
axisLine: { show: false },
splitLine: {
show: true,
lineStyle: {
color: "rgba(159, 220, 230, 0.14)",
type: "dashed",
},
},
},
series: [{
type: "bar",
data: rows.map((item, index) => {
const zero = Boolean(item?.zero || item?.isZero)
const value = zero ? 0 : Number(item?.value || 0)
return {
name: item?.name,
value,
displayValue: item?.displayValue ?? formatChartNumber(value),
unit: item?.unit || unit || "",
extra: item?.extra || "",
zero,
itemStyle: {
color: zero ? "rgba(48, 220, 255, 0.1)" : townColumnColor(index, item),
},
}
}),
barWidth: 14,
barMinHeight: 3,
showBackground: true,
backgroundStyle: {
color: "rgba(48, 220, 255, 0.06)",
borderColor: "rgba(48, 220, 255, 0.08)",
borderWidth: 1,
},
itemStyle: {
borderRadius: [4, 4, 0, 0],
},
label: {
show: false,
},
emphasis: {
focus: "series",
itemStyle: {
shadowBlur: 14,
shadowColor: "rgba(36, 246, 196, 0.42)",
},
},
}],
}
}
function getSharedUnit(rows = []) {
const units = [...new Set(rows.map((item) => item?.unit).filter(Boolean))]
return units.length === 1 ? units[0] : ""
}
function formatTownAxisLabel(name) {
const text = String(name || "").replace(/\s+/g, "")
return Array.from(text).slice(0, 3).join("\n")
}
function townColumnColor(index, item = {}) {
if (item.color || item.fillColor) return item.color || item.fillColor
const palette = [
["#35f1ff", "#13aebd"],
["#5cf5a6", "#18b978"],
["#f6d663", "#c69c28"],
["#7aa3ff", "#3a65d8"],
["#ff8b62", "#c25d3d"],
]
const [top, bottom] = palette[index % palette.length]
return {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: "rgba(255,255,255,0.96)" },
{ offset: 0.14, color: top },
{ offset: 0.72, color: bottom },
{ offset: 1, color: "rgba(20, 244, 220, 0.28)" },
],
}
}
function formatChartNumber(value) {
const number = Number(value)
if (!Number.isFinite(number)) return "--"
return Number.isInteger(number) ? `${number}` : `${Number(number.toFixed(2))}`
}
function updateScreenViewportHeight() { function updateScreenViewportHeight() {
screenViewportHeight.value = typeof window === "undefined" ? 1080 : window.innerHeight screenViewportHeight.value = typeof window === "undefined" ? 1080 : window.innerHeight
} }
@ -353,6 +542,37 @@ async function loadIndustryMapOnce(attempt = 0) {
emit("map-ready") emit("map-ready")
} }
function clearIndustryMapLoadTimer() {
if (!mapLoadTimer) return
window.clearTimeout(mapLoadTimer)
mapLoadTimer = null
}
function refreshMapView() {
mapSceneRef.value?.resume?.()
mapSceneRef.value?.refreshView?.()
}
function resetMap() {
clearSelectedPoint()
if (mapSceneRef.value?.hasMap?.()) {
mapReady.value = true
refreshMapView()
return
}
clearIndustryMapLoadTimer()
mapReady.value = false
mapLoadStarted = false
nextTick(() => {
scheduleIndustryMapLoad()
})
}
defineExpose({
refreshMapView,
resetMap,
})
function handleMapFeatureInfo(payload = {}) { function handleMapFeatureInfo(payload = {}) {
const properties = payload.feature?.properties || {} const properties = payload.feature?.properties || {}
if (properties.stageKey) { if (properties.stageKey) {
@ -542,6 +762,172 @@ function normalizeFeatureScreen(screen = {}) {
inset 0 0 0 1px rgba(48, 220, 255, 0.06), inset 0 0 0 1px rgba(48, 220, 255, 0.06),
inset 0 12px 28px rgba(48, 220, 255, 0.04); inset 0 12px 28px rgba(48, 220, 255, 0.04);
} }
.industry-town-column-chart {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
min-height: 0;
}
.business-columns.is-variant-plain {
grid-template-columns: repeat(auto-fit, minmax(34px, 1fr));
gap: 8px;
padding-right: 12px;
padding-left: 12px;
}
.business-columns.is-variant-plain .business-column-item {
grid-template-rows: 16px minmax(62px, 1fr) 32px;
}
.business-columns.is-variant-plain .business-column-item span {
display: -webkit-box;
max-width: 58px;
color: rgba(240, 253, 255, 0.86);
line-height: 13px;
white-space: normal;
word-break: keep-all;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.business-columns.is-variant-plain .business-column-track {
width: 18px;
}
.business-columns.is-variant-townCompact {
grid-template-columns: repeat(10, minmax(0, 1fr));
gap: 5px;
padding: 35px 13px 10px;
}
.business-columns.is-variant-townCompact::before {
left: 13px;
right: 13px;
top: 55px;
bottom: 40px;
opacity: 0.46;
background:
linear-gradient(to bottom, transparent calc(100% - 1px), rgba(48, 220, 255, 0.58) calc(100% - 1px)),
repeating-linear-gradient(
to bottom,
rgba(159, 220, 230, 0.16) 0 1px,
transparent 1px 21px
);
}
.business-columns.is-variant-townCompact::after {
content: "";
position: absolute;
left: 13px;
right: 13px;
bottom: 39px;
height: 1px;
pointer-events: none;
background: linear-gradient(90deg, transparent, rgba(60, 244, 255, 0.72), transparent);
box-shadow: 0 0 10px rgba(48, 220, 255, 0.24);
}
.business-columns.is-variant-townCompact .business-column-item {
position: relative;
z-index: 1;
grid-template-rows: 17px minmax(74px, 1fr) 29px;
min-width: 0;
}
.business-columns.is-variant-townCompact .business-column-item strong {
width: 100%;
color: rgba(244, 252, 255, 0.94);
font-size: 10px;
line-height: 14px;
text-align: center;
text-shadow: 0 0 8px rgba(255, 255, 255, 0.18);
}
.business-columns.is-variant-townCompact .business-column-item span {
display: -webkit-box;
align-self: start;
width: 100%;
max-width: 36px;
height: 25px;
margin-top: 3px;
color: rgba(232, 252, 255, 0.82);
font-size: 10px;
line-height: 12px;
text-align: center;
white-space: normal;
word-break: break-all;
writing-mode: horizontal-tb;
text-orientation: initial;
letter-spacing: 0;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.business-columns.is-variant-townCompact .business-column-track {
width: 24px;
height: 100%;
min-height: 74px;
align-self: end;
border: 0;
border-radius: 2px 2px 0 0;
background:
linear-gradient(180deg, rgba(48, 220, 255, 0.12), rgba(36, 246, 196, 0.015)),
linear-gradient(90deg, rgba(48, 220, 255, 0.1), rgba(48, 220, 255, 0.02) 48%, rgba(48, 220, 255, 0.1)),
rgba(4, 26, 38, 0.22);
box-shadow:
inset 0 -1px 0 rgba(48, 220, 255, 0.2),
inset 0 0 10px rgba(48, 220, 255, 0.06);
}
.business-columns.is-variant-townCompact .business-column-track::after {
content: "";
position: absolute;
left: 50%;
bottom: -5px;
width: 34px;
height: 7px;
border-radius: 50%;
background: radial-gradient(ellipse at center, rgba(36, 246, 196, 0.28), rgba(36, 246, 196, 0) 68%);
transform: translateX(-50%);
}
.business-columns.is-variant-townCompact .business-column-track i {
left: 4px;
right: 4px;
min-height: 5px;
border-radius: 3px 3px 0 0;
box-shadow:
0 0 12px rgba(36, 246, 196, 0.32),
inset 0 1px 0 rgba(255, 255, 255, 0.26);
}
.business-columns.is-variant-townCompact .business-column-track i::before,
.business-columns.is-variant-townCompact .business-column-track i::after {
display: none;
}
.business-columns.is-variant-townCompact .business-column-item.is-zero strong {
opacity: 0;
}
.business-columns.is-variant-townCompact .business-column-item.is-zero .business-column-track i {
height: 4px !important;
opacity: 0.16;
box-shadow: none;
}
.business-bars.is-rank-bar {
padding-right: 12px;
padding-left: 12px;
}
.business-bars.is-rank-bar .business-bar-name {
font-size: 12px;
line-height: 14px;
}
} }
.industry-ledger-list { .industry-ledger-list {

@ -1,6 +1,6 @@
<template> <template>
<div class="yak-ol-map"> <div class="yak-ol-map">
<div ref="mapEl" class="yak-ol-map__canvas"></div> <div :ref="setMapElement" class="yak-ol-map__canvas"></div>
<div <div
v-if="coordinateCopyTip.visible" v-if="coordinateCopyTip.visible"
class="yak-ol-map__copy-tip" class="yak-ol-map__copy-tip"
@ -29,16 +29,24 @@ import Map from "ol/Map.js"
import View from "ol/View.js" import View from "ol/View.js"
import Feature from "ol/Feature.js" import Feature from "ol/Feature.js"
import GeoJSON from "ol/format/GeoJSON.js" import GeoJSON from "ol/format/GeoJSON.js"
import ImageLayer from "ol/layer/Image.js"
import TileLayer from "ol/layer/Tile.js" import TileLayer from "ol/layer/Tile.js"
import VectorLayer from "ol/layer/Vector.js" import VectorLayer from "ol/layer/Vector.js"
import Point from "ol/geom/Point.js" import Point from "ol/geom/Point.js"
import ImageWMS from "ol/source/ImageWMS.js"
import TileWMS from "ol/source/TileWMS.js" import TileWMS from "ol/source/TileWMS.js"
import XYZ from "ol/source/XYZ.js" import XYZ from "ol/source/XYZ.js"
import VectorSource from "ol/source/Vector.js" import VectorSource from "ol/source/Vector.js"
import { bbox as bboxStrategy } from "ol/loadingstrategy.js"
import { defaults as defaultControls } from "ol/control/defaults.js" import { defaults as defaultControls } from "ol/control/defaults.js"
import { unByKey } from "ol/Observable.js" import { unByKey } from "ol/Observable.js"
import { fromLonLat, toLonLat } from "ol/proj.js" import { fromLonLat, toLonLat } from "ol/proj.js"
import { Circle as CircleStyle, Fill, RegularShape, Stroke, Style, Text } from "ol/style.js" import CircleStyle from "ol/style/Circle.js"
import Fill from "ol/style/Fill.js"
import RegularShape from "ol/style/RegularShape.js"
import Stroke from "ol/style/Stroke.js"
import Style from "ol/style/Style.js"
import Text from "ol/style/Text.js"
import { hongyuanTownshipLabelPoints, hongyuanTownshipsGeoJson } from "@/config/townshipBoundaries" import { hongyuanTownshipLabelPoints, hongyuanTownshipsGeoJson } from "@/config/townshipBoundaries"
const props = defineProps({ const props = defineProps({
@ -48,7 +56,7 @@ const props = defineProps({
}, },
tileUrlTemplate: { tileUrlTemplate: {
type: String, type: String,
default: "/hy-result/{z}/{x}/{y}.png", default: "https://qkl-map.oss-cn-chengdu.aliyuncs.com/hy-result/{z}/{x}/{y}.png",
}, },
center: { center: {
type: Array, type: Array,
@ -62,6 +70,22 @@ const props = defineProps({
type: Number, type: Number,
default: 21, default: 21,
}, },
initialZoom: {
type: Number,
default: 21,
},
overlayLayer: {
type: Object,
default: () => ({
sourceType: "wms",
layerName: "ne:daping_yak_marks",
styles: "yak_marks",
cqlFilter: "gov_show = true",
name: "牦牛识别标记",
color: "#FFFF00",
opacity: 0.96,
}),
},
}) })
const emit = defineEmits(["ready", "tile-status", "layer-status"]) const emit = defineEmits(["ready", "tile-status", "layer-status"])
@ -95,7 +119,8 @@ const labelPointStyle = new Style({
const mapEl = ref(null) const mapEl = ref(null)
let olMap = null let olMap = null
let baseSource = null let baseSource = null
let yakWmsSource = null let overlaySource = null
let overlayMapLayer = null
let contextMenuHandler = null let contextMenuHandler = null
let rightPointerDownHandler = null let rightPointerDownHandler = null
let coordinateTipTimer = null let coordinateTipTimer = null
@ -124,6 +149,11 @@ const tileState = {
zoom: INITIAL_ZOOM, zoom: INITIAL_ZOOM,
} }
function setMapElement(el) {
mapEl.value = el || null
if (el) initMap()
}
function tileCoordKey(event) { function tileCoordKey(event) {
const coord = event?.tile?.getTileCoord?.() const coord = event?.tile?.getTileCoord?.()
return Array.isArray(coord) ? coord.join("/") : "" return Array.isArray(coord) ? coord.join("/") : ""
@ -131,7 +161,7 @@ function tileCoordKey(event) {
function currentZoom() { function currentZoom() {
const zoom = Number(olMap?.getView?.()?.getZoom?.()) const zoom = Number(olMap?.getView?.()?.getZoom?.())
return Number.isFinite(zoom) ? Math.round(zoom) : INITIAL_ZOOM return Number.isFinite(zoom) ? Math.round(zoom) : normalizeInitialZoom()
} }
function normalizeCenter(center = props.center) { function normalizeCenter(center = props.center) {
@ -144,12 +174,21 @@ function normalizeCenter(center = props.center) {
function locateMap(center = props.center) { function locateMap(center = props.center) {
const view = olMap?.getView?.() const view = olMap?.getView?.()
if (!view) return if (!view) return
resetTileState(INITIAL_ZOOM) const zoom = normalizeInitialZoom()
resetTileState(zoom)
view.setCenter(fromLonLat(normalizeCenter(center))) view.setCenter(fromLonLat(normalizeCenter(center)))
view.setZoom(INITIAL_ZOOM) view.setZoom(zoom)
window.setTimeout(() => olMap?.updateSize?.(), 0) window.setTimeout(() => olMap?.updateSize?.(), 0)
} }
function normalizeInitialZoom() {
const zoom = Number(props.initialZoom)
const minZoom = Number(props.minZoom)
const maxZoom = Number(props.maxZoom)
const safeZoom = Number.isFinite(zoom) ? zoom : INITIAL_ZOOM
return Math.min(Number.isFinite(maxZoom) ? maxZoom : INITIAL_ZOOM, Math.max(Number.isFinite(minZoom) ? minZoom : 0, safeZoom))
}
function resetTileState(zoom = currentZoom()) { function resetTileState(zoom = currentZoom()) {
pendingTiles.clear() pendingTiles.clear()
tileState.requested = 0 tileState.requested = 0
@ -224,29 +263,244 @@ function buildBaseSource() {
return baseSource return baseSource
} }
function buildYakWmsSource() { function normalizeOverlayLayer(layer = props.overlayLayer) {
yakWmsSource = new TileWMS({ if (!layer || layer.visible === false || !layer.layerName) return null
url: "/geoserver/ne/wms", const sourceType = layer.sourceType || "wms"
params: { return {
sourceType,
url: layer.url || (sourceType === "wfs" ? "/geoserver/ne/wfs" : "/geoserver/ne/wms"),
renderMode: layer.renderMode || "tile",
layerName: layer.layerName,
styles: layer.styles || "",
sldBody: layer.sldBody || "",
cqlFilter: layer.cqlFilter || "",
name: layer.name || "业务图层",
color: layer.color || layer.strokeColor || layer.fillColor || "#FFFF00",
fillColor: layer.fillColor || layer.color || "rgba(255, 227, 90, 0.05)",
strokeColor: layer.strokeColor || layer.color || "#FFFF00",
glowColor: layer.glowColor || "rgba(255, 227, 90, 0.3)",
opacity: Number.isFinite(Number(layer.opacity)) ? Number(layer.opacity) : 0.96,
lineWidth: Number.isFinite(Number(layer.lineWidth)) ? Number(layer.lineWidth) : 2.6,
zIndex: Number.isFinite(Number(layer.zIndex)) ? Number(layer.zIndex) : 20,
maxFeatures: layer.maxFeatures || 12000,
geometryField: layer.geometryField || "geom",
loadingStrategy: layer.loadingStrategy || "",
propertyName: layer.propertyName || (layer.boundaryOnly ? [layer.geometryField || "geom"] : undefined),
bboxPadding: Number.isFinite(Number(layer.bboxPadding)) ? Number(layer.bboxPadding) : 0.003,
}
}
function buildOverlayWmsParams(layer) {
const params = {
SERVICE: "WMS", SERVICE: "WMS",
VERSION: "1.1.1", VERSION: "1.1.1",
LAYERS: "ne:daping_yak_marks", LAYERS: layer.layerName,
STYLES: "yak_marks",
CQL_FILTER: "gov_show = true",
FORMAT: "image/png", FORMAT: "image/png",
TRANSPARENT: true, TRANSPARENT: true,
TILED: true, TILED: true,
}, }
if (layer.styles) params.STYLES = layer.styles
if (layer.cqlFilter) params.CQL_FILTER = layer.cqlFilter
if (layer.sldBody) params.SLD_BODY = layer.sldBody
return params
}
function bindOverlayImageEvents(source, layer) {
wmsEventKeys.push(source.on("imageloadstart", () => emitLayerStatus("loading", `正在加载${layer.name}`)))
wmsEventKeys.push(source.on("imageloadend", () => emitLayerStatus("success", `${layer.name}已加载`)))
wmsEventKeys.push(source.on("imageloaderror", () => emitLayerStatus("error", `${layer.name}加载失败`)))
}
function bindOverlayTileEvents(source, layer) {
wmsEventKeys.push(source.on("tileloadstart", () => emitLayerStatus("loading", `正在加载${layer.name}`)))
wmsEventKeys.push(source.on("tileloadend", () => emitLayerStatus("success", `${layer.name}已加载`)))
wmsEventKeys.push(source.on("tileloaderror", () => emitLayerStatus("error", `${layer.name}加载失败`)))
}
function buildOverlayImageWmsSource(layer) {
const source = new ImageWMS({
url: layer.url,
params: buildOverlayWmsParams(layer),
serverType: "geoserver",
crossOrigin: "anonymous",
ratio: 1,
})
bindOverlayImageEvents(source, layer)
return source
}
function buildOverlayWmsSource(layer) {
const source = new TileWMS({
url: layer.url,
params: buildOverlayWmsParams(layer),
serverType: "geoserver", serverType: "geoserver",
crossOrigin: "anonymous", crossOrigin: "anonymous",
transition: 0, transition: 0,
}) })
wmsEventKeys.push(yakWmsSource.on("tileloadstart", () => emitLayerStatus("loading", "正在加载牦牛识别标记"))) bindOverlayTileEvents(source, layer)
wmsEventKeys.push(yakWmsSource.on("tileloadend", () => emitLayerStatus("success", "牦牛识别标记已加载"))) return source
wmsEventKeys.push(yakWmsSource.on("tileloaderror", () => emitLayerStatus("error", "牦牛识别标记加载失败"))) }
function combineCqlFilters(...filters) {
const clauses = filters.map((item) => String(item || "").trim()).filter(Boolean)
if (!clauses.length) return ""
return clauses.length === 1 ? clauses[0] : clauses.map((item) => `(${item})`).join(" AND ")
}
function buildOverlayWfsUrl(layer, options = {}) {
const params = new URLSearchParams({
service: "WFS",
version: "1.0.0",
request: "GetFeature",
typeName: layer.layerName,
outputFormat: "json",
maxFeatures: String(layer.maxFeatures),
})
const propertyName = options.propertyName ?? layer.propertyName
if (Array.isArray(propertyName) && propertyName.length) params.set("propertyName", propertyName.join(","))
else if (propertyName) params.set("propertyName", propertyName)
const cqlFilter = combineCqlFilters(layer.cqlFilter, options.cqlFilter)
if (cqlFilter) params.set("CQL_FILTER", cqlFilter)
return `${layer.url}?${params.toString()}`
}
function createBboxCqlFilter(layer, extent) {
const bottomLeft = toLonLat([extent[0], extent[1]])
const topRight = toLonLat([extent[2], extent[3]])
const padding = layer.bboxPadding
const minLng = Math.min(bottomLeft[0], topRight[0]) - padding
const minLat = Math.min(bottomLeft[1], topRight[1]) - padding
const maxLng = Math.max(bottomLeft[0], topRight[0]) + padding
const maxLat = Math.max(bottomLeft[1], topRight[1]) + padding
return `BBOX(${layer.geometryField},${minLng},${minLat},${maxLng},${maxLat})`
}
function buildOverlayBboxVectorSource(layer) {
const source = new VectorSource({
wrapX: false,
strategy: bboxStrategy,
})
const format = new GeoJSON()
const loadedFeatureIds = new Set()
source.setLoader((extent, resolution, projection, success, failure) => {
emitLayerStatus("loading", `正在加载${layer.name}`)
fetch(buildOverlayWfsUrl(layer, { cqlFilter: createBboxCqlFilter(layer, extent) }), { credentials: "include" })
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.json()
})
.then((geojson) => {
if (source !== overlaySource) return
const features = format.readFeatures(geojson, {
dataProjection: "EPSG:4326",
featureProjection: projection,
})
const nextFeatures = features.filter((feature) => {
const id = feature.getId?.()
if (!id || loadedFeatureIds.has(id)) return false
loadedFeatureIds.add(id)
return true
})
source.addFeatures(nextFeatures)
success?.(nextFeatures)
emitLayerStatus(features.length ? "success" : "warning", features.length ? `${layer.name}已加载` : `${layer.name}暂无数据`)
})
.catch(() => {
if (source !== overlaySource) return
source.removeLoadedExtent?.(extent)
failure?.()
emitLayerStatus("error", `${layer.name}加载失败`)
})
})
return source
}
function buildOverlayVectorSource(layer) {
if (layer.loadingStrategy === "bbox") return buildOverlayBboxVectorSource(layer)
const source = new VectorSource({ wrapX: false })
const format = new GeoJSON()
emitLayerStatus("loading", `正在加载${layer.name}`)
fetch(buildOverlayWfsUrl(layer), { credentials: "include" })
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.json()
})
.then((geojson) => {
if (source !== overlaySource) return
const features = format.readFeatures(geojson, {
dataProjection: "EPSG:4326",
featureProjection: "EPSG:3857",
})
source.clear(true)
source.addFeatures(features)
emitLayerStatus(features.length ? "success" : "warning", features.length ? `${layer.name}已加载` : `${layer.name}暂无数据`)
})
.catch(() => {
if (source !== overlaySource) return
emitLayerStatus("error", `${layer.name}加载失败`)
})
return source
}
function createOverlayVectorStyle(layer) {
const lineWidth = layer.lineWidth
return [
new Style({
fill: new Fill({ color: layer.fillColor }),
stroke: new Stroke({ color: layer.glowColor, width: lineWidth + 5 }),
}),
new Style({
stroke: new Stroke({ color: layer.strokeColor, width: lineWidth }),
}),
]
}
return yakWmsSource function createOverlayMapLayer() {
const layer = normalizeOverlayLayer()
if (!layer) return null
if (layer.sourceType === "wfs") {
overlaySource = buildOverlayVectorSource(layer)
const overlayVectorStyle = createOverlayVectorStyle(layer)
return new VectorLayer({
source: overlaySource,
style: () => overlayVectorStyle,
zIndex: layer.zIndex,
renderBuffer: 160,
updateWhileAnimating: true,
updateWhileInteracting: true,
})
}
if (layer.renderMode === "image") {
overlaySource = buildOverlayImageWmsSource(layer)
return new ImageLayer({
source: overlaySource,
opacity: layer.opacity,
zIndex: layer.zIndex,
})
}
overlaySource = buildOverlayWmsSource(layer)
return new TileLayer({
source: overlaySource,
opacity: layer.opacity,
zIndex: layer.zIndex,
})
}
function refreshOverlayLayer() {
if (!olMap) return
if (overlayMapLayer) {
olMap.removeLayer(overlayMapLayer)
overlayMapLayer = null
}
if (wmsEventKeys.length) {
unByKey(wmsEventKeys)
wmsEventKeys = []
}
overlaySource = null
overlayMapLayer = createOverlayMapLayer()
if (overlayMapLayer) olMap.addLayer(overlayMapLayer)
} }
function createBoundaryLayers() { function createBoundaryLayers() {
@ -275,7 +529,7 @@ function createBoundaryLayers() {
return [ return [
new VectorLayer({ new VectorLayer({
source: boundarySource, source: boundarySource,
style: boundaryGlowStyle, style: () => boundaryGlowStyle,
zIndex: 28, zIndex: 28,
renderBuffer: 80, renderBuffer: 80,
updateWhileAnimating: true, updateWhileAnimating: true,
@ -283,7 +537,7 @@ function createBoundaryLayers() {
}), }),
new VectorLayer({ new VectorLayer({
source: boundarySource, source: boundarySource,
style: boundaryLineStyle, style: () => boundaryLineStyle,
zIndex: 29, zIndex: 29,
renderBuffer: 80, renderBuffer: 80,
updateWhileAnimating: true, updateWhileAnimating: true,
@ -521,14 +775,15 @@ function unbindContextMenuCopy() {
} }
async function initMap() { async function initMap() {
if (olMap || !props.active) return if (olMap) return
await nextTick() await nextTick()
if (!mapEl.value || olMap || !props.active) return if (!mapEl.value || olMap) return
resetTileState(INITIAL_ZOOM) const initialZoom = normalizeInitialZoom()
resetTileState(initialZoom)
const view = new View({ const view = new View({
center: fromLonLat(normalizeCenter()), center: fromLonLat(normalizeCenter()),
zoom: INITIAL_ZOOM, zoom: initialZoom,
minZoom: props.minZoom, minZoom: props.minZoom,
maxZoom: props.maxZoom, maxZoom: props.maxZoom,
enableRotation: false, enableRotation: false,
@ -549,15 +804,11 @@ async function initMap() {
preload: 1, preload: 1,
zIndex: 1, zIndex: 1,
}), }),
new TileLayer({
source: buildYakWmsSource(),
opacity: 0.96,
zIndex: 20,
}),
...createBoundaryLayers(), ...createBoundaryLayers(),
], ],
view, view,
}) })
refreshOverlayLayer()
bindContextMenuCopy() bindContextMenuCopy()
eventKeys.push( eventKeys.push(
@ -588,7 +839,8 @@ function disposeMap() {
} }
pendingTiles.clear() pendingTiles.clear()
baseSource = null baseSource = null
yakWmsSource = null overlaySource = null
overlayMapLayer = null
if (olMap) { if (olMap) {
olMap.setTarget(null) olMap.setTarget(null)
olMap.dispose?.() olMap.dispose?.()
@ -602,8 +854,31 @@ function refreshView() {
window.setTimeout(() => olMap?.updateSize?.(), 120) window.setTimeout(() => olMap?.updateSize?.(), 120)
} }
function refreshTileSources() {
if (baseSource) {
resetTileState(currentZoom())
baseSource.refresh?.()
emitTileStatus("loading", `正在刷新 hy-result ${tileState.zoom}级影像底图`)
}
overlaySource?.refresh?.()
}
function resetView() {
locateMap()
refreshTileSources()
olMap?.updateSize?.()
window.setTimeout(() => olMap?.updateSize?.(), 80)
window.setTimeout(() => olMap?.updateSize?.(), 220)
}
function hasMap() {
return Boolean(olMap)
}
defineExpose({ defineExpose({
refreshView, refreshView,
resetView,
hasMap,
}) })
onMounted(() => { onMounted(() => {
@ -645,6 +920,14 @@ watch(
emitTileStatus("loading", `正在加载 hy-result ${tileState.zoom}级影像底图`) emitTileStatus("loading", `正在加载 hy-result ${tileState.zoom}级影像底图`)
}, },
) )
watch(
() => props.overlayLayer,
() => {
refreshOverlayLayer()
},
{ deep: true },
)
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

@ -1,5 +1,5 @@
<template> <template>
<div ref="yakManagementRef" class="yak-management"> <div class="yak-management">
<map-scene <map-scene
v-show="!isYakOlMapView" v-show="!isYakOlMapView"
:key="yakMapSceneKey" :key="yakMapSceneKey"
@ -27,16 +27,6 @@
/> />
<div class="yak-map-scrim" :class="{ 'is-ol-map': isYakOlMapView }"></div> <div class="yak-map-scrim" :class="{ 'is-ol-map': isYakOlMapView }"></div>
<div v-if="yakMapLoadingVisible" class="yak-map-loading">{{ yakMapLoadingText }}</div> <div v-if="yakMapLoadingVisible" class="yak-map-loading">{{ yakMapLoadingText }}</div>
<div class="yak-map-tools" aria-label="地图工具">
<button type="button" :title="yakFullscreenLabel" :aria-label="yakFullscreenLabel" @click="handleYakMapFullscreen">
<span class="yak-map-tool-icon is-fullscreen" aria-hidden="true"></span>
<em>{{ yakFullscreenLabel }}</em>
</button>
<button type="button" title="复位地图" aria-label="复位地图" @click="handleYakMapReset">
<span class="yak-map-tool-icon is-reset" aria-hidden="true"></span>
<em>复位</em>
</button>
</div>
<div class="yak-management-ui"> <div class="yak-management-ui">
<div v-if="activeView === 'trade' && tradeYears.length" class="trade-year-switcher" aria-label="交易年份切换"> <div v-if="activeView === 'trade' && tradeYears.length" class="trade-year-switcher" aria-label="交易年份切换">
<span>交易年份</span> <span>交易年份</span>
@ -369,9 +359,12 @@
v-for="item in forecastHeatRows" v-for="item in forecastHeatRows"
:key="item.name" :key="item.name"
class="heat-row" class="heat-row"
:style="{ '--heat-width': `${item.width}%` }" :class="{ 'is-negative': item.change < 0 }"
:style="forecastHeatStyle(item)"
:title="forecastHeatTitle(item)"
> >
<span>{{ item.name }}</span> <span>{{ item.name }}</span>
<b><i></i></b>
<strong>{{ formatSigned(item.change) }}</strong> <strong>{{ formatSigned(item.change) }}</strong>
</div> </div>
</div> </div>
@ -716,11 +709,13 @@
<button type="button" @click="closeEpidemicImagePreview">关闭</button> <button type="button" @click="closeEpidemicImagePreview">关闭</button>
</div> </div>
<div class="epidemic-image-preview-body"> <div class="epidemic-image-preview-body">
<div class="epidemic-image-preview-frame">
<img :src="epidemicImagePreviewUrl" alt="防疫照片预览" /> <img :src="epidemicImagePreviewUrl" alt="防疫照片预览" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script setup> <script setup>
@ -789,9 +784,15 @@ const epidemicLedgerCardHeight = computed(() => sideCardHeight.value * 4 + sideC
const tradeSourceCardHeight = computed(() => sideCardHeight.value + 24) const tradeSourceCardHeight = computed(() => sideCardHeight.value + 24)
const tradeLedgerCardHeight = computed(() => sideCardHeight.value * 2 - sideCardGap) const tradeLedgerCardHeight = computed(() => sideCardHeight.value * 2 - sideCardGap)
const epidemicImageBaseUrl = "https://qkl-hy.oss-cn-chengdu.aliyuncs.com/yak-vaccine-images" const epidemicImageBaseUrl = "https://qkl-hy.oss-cn-chengdu.aliyuncs.com/yak-vaccine-images"
const yakMapTileUrlTemplate = "/hy-result/{z}/{x}/{y}.png" const yakMapTileUrlTemplate = "https://qkl-map.oss-cn-chengdu.aliyuncs.com/hy-result/{z}/{x}/{y}.png"
const yakStructureMapCenter = [103.07984, 33.029058] const yakStructureMapCenter = [103.07984, 33.029058]
const yakForecastMapCenter = [102.682809, 33.131561] const yakForecastMapCenter = [102.682809, 33.131561]
const yakTradeMapView = {
targetLngLat: [102.58, 32.69],
targetLocalZ: 0.72,
distanceScale: 1.12,
duration: 0.75,
}
const yakMapSceneRevision = "hy-result-dynamic-zoom21-v4" const yakMapSceneRevision = "hy-result-dynamic-zoom21-v4"
const palette = ["#30dcff", "#27f0c0", "#ffd76c", "#7aa7ff", "#ff8f6b", "#a6f06d"] const palette = ["#30dcff", "#27f0c0", "#ffd76c", "#7aa7ff", "#ff8f6b", "#a6f06d"]
const epidemicPieColors = ["#24f6c4", "#32dcff", "#ffe06b", "#7ee269", "#ff9a62", "#9ea7ff"] const epidemicPieColors = ["#24f6c4", "#32dcff", "#ffe06b", "#7ee269", "#ff9a62", "#9ea7ff"]
@ -869,12 +870,10 @@ const tradeLedgerModalFilters = ref({
usageType: "", usageType: "",
certType: "", certType: "",
}) })
const yakManagementRef = ref(null)
const mapSceneRef = ref(null) const mapSceneRef = ref(null)
const yakOlMapRef = ref(null) const yakOlMapRef = ref(null)
const mapReady = ref(false) const mapReady = ref(false)
const olMapReady = ref(false) const olMapReady = ref(false)
const isYakFullscreen = ref(false)
const yakMapSceneResetRevision = ref(0) const yakMapSceneResetRevision = ref(0)
const yakOlMapKey = ref(0) const yakOlMapKey = ref(0)
const tileStatus = ref({ type: "idle", text: "" }) const tileStatus = ref({ type: "idle", text: "" })
@ -1297,7 +1296,6 @@ const yakMapLoadingVisible = computed(() => {
return !mapReady.value return !mapReady.value
}) })
const yakMapLoadingText = computed(() => (isYakOlMapView.value ? "影像地图加载中" : "3D地图加载中")) const yakMapLoadingText = computed(() => (isYakOlMapView.value ? "影像地图加载中" : "3D地图加载中"))
const yakFullscreenLabel = computed(() => (isYakFullscreen.value ? "退出" : "全屏"))
const mapRanking = computed(() => { const mapRanking = computed(() => {
if (activeView.value === "trade") { if (activeView.value === "trade") {
return { return {
@ -1310,6 +1308,8 @@ const mapRanking = computed(() => {
total: tradeMetricValue("headCount"), total: tradeMetricValue("headCount"),
}, },
rows: tradeFlowRows.value, rows: tradeFlowRows.value,
showFlowLines: false,
mapView: yakTradeMapView,
} }
} }
return { return {
@ -1938,6 +1938,20 @@ const forecastHeatRows = computed(() => {
})) }))
}) })
function forecastHeatStyle(item) {
const positive = Number(item?.change || 0) >= 0
return {
"--heat-width": `${item.width}%`,
"--heat-color": positive ? "#24f6c4" : "#ff8f6b",
"--heat-color-soft": positive ? "rgba(36, 246, 196, 0.28)" : "rgba(255, 143, 107, 0.26)",
"--heat-color-dim": positive ? "rgba(36, 246, 196, 0.08)" : "rgba(255, 143, 107, 0.08)",
}
}
function forecastHeatTitle(item) {
return `${item.name}${formatSigned(item.change)}`
}
const epidemicTrendOption = computed(() => { const epidemicTrendOption = computed(() => {
const rows = epidemicTrendRows.value const rows = epidemicTrendRows.value
const values = rows.map((item) => item.value) const values = rows.map((item) => item.value)
@ -2402,11 +2416,18 @@ const epidemicPreventionStatOption = computed(() => {
selectedMode: false, selectedMode: false,
}, },
], ],
grid: { left: 36, right: 8, top: 38, bottom: 50 }, grid: { left: 42, right: 12, top: 46, bottom: 68 },
xAxis: { xAxis: {
type: "category", type: "category",
data: rows.map((item) => item.name), data: rows.map((item) => item.name),
axisLabel: { color: "#fff", fontSize: 10, interval: 0, formatter: verticalAxisLabel }, axisLabel: {
color: "#fff",
fontSize: 10,
interval: 0,
lineHeight: 11,
margin: 8,
formatter: verticalTownAxisLabel,
},
axisLine: { show: false }, axisLine: { show: false },
axisTick: { show: false }, axisTick: { show: false },
splitLine: { show: false }, splitLine: { show: false },
@ -2711,37 +2732,6 @@ function updateScreenViewportHeight() {
screenViewportHeight.value = typeof window === "undefined" ? 1080 : window.innerHeight screenViewportHeight.value = typeof window === "undefined" ? 1080 : window.innerHeight
} }
const yakFullscreenEvents = ["fullscreenchange", "webkitfullscreenchange", "mozfullscreenchange", "MSFullscreenChange"]
function getFullscreenElement() {
if (typeof document === "undefined") return null
return document.fullscreenElement
|| document.webkitFullscreenElement
|| document.mozFullScreenElement
|| document.msFullscreenElement
|| null
}
function syncYakFullscreenState() {
const fullscreenElement = getFullscreenElement()
const root = yakManagementRef.value
isYakFullscreen.value = Boolean(root && fullscreenElement && (
fullscreenElement === root
|| root.contains(fullscreenElement)
|| fullscreenElement.contains?.(root)
))
}
function bindYakFullscreenEvents() {
if (typeof document === "undefined") return
yakFullscreenEvents.forEach((eventName) => document.addEventListener(eventName, syncYakFullscreenState))
}
function unbindYakFullscreenEvents() {
if (typeof document === "undefined") return
yakFullscreenEvents.forEach((eventName) => document.removeEventListener(eventName, syncYakFullscreenState))
}
function refreshYakMapView() { function refreshYakMapView() {
if (isYakOlMapView.value) { if (isYakOlMapView.value) {
yakOlMapRef.value?.refreshView?.() yakOlMapRef.value?.refreshView?.()
@ -2751,33 +2741,6 @@ function refreshYakMapView() {
mapSceneRef.value?.refreshView?.() mapSceneRef.value?.refreshView?.()
} }
async function handleYakMapFullscreen() {
if (typeof document === "undefined") return
const fullscreenElement = getFullscreenElement()
try {
if (fullscreenElement) {
const exitFullscreen = document.exitFullscreen
|| document.webkitExitFullscreen
|| document.mozCancelFullScreen
|| document.msExitFullscreen
await exitFullscreen?.call(document)
} else {
const target = yakManagementRef.value || document.documentElement
const requestFullscreen = target.requestFullscreen
|| target.webkitRequestFullscreen
|| target.mozRequestFullScreen
|| target.msRequestFullscreen
await requestFullscreen?.call(target)
}
} catch (error) {
console.warn("[yak-management] fullscreen toggle failed", error)
} finally {
syncYakFullscreenState()
window.setTimeout(refreshYakMapView, 80)
window.setTimeout(refreshYakMapView, 280)
}
}
function clearYakMapLoadTimer() { function clearYakMapLoadTimer() {
if (!mapLoadTimer) return if (!mapLoadTimer) return
window.clearTimeout(mapLoadTimer) window.clearTimeout(mapLoadTimer)
@ -2789,29 +2752,41 @@ function handleYakMapReset() {
topicLayerStatus.value = { type: "idle", text: "" } topicLayerStatus.value = { type: "idle", text: "" }
if (isYakOlMapView.value) { if (isYakOlMapView.value) {
if (yakOlMapRef.value?.hasMap?.()) {
olMapReady.value = true
yakOlMapRef.value?.resetView?.()
} else {
olMapReady.value = false olMapReady.value = false
yakOlMapKey.value += 1 yakOlMapKey.value += 1
nextTick(() => { nextTick(() => {
window.setTimeout(() => yakOlMapRef.value?.refreshView?.(), 160) window.setTimeout(() => yakOlMapRef.value?.refreshView?.(), 160)
}) })
}
return return
} }
if (mapSceneRef.value?.hasMap?.()) {
mapReady.value = true
refreshYakMapView()
return
}
clearYakMapLoadTimer() clearYakMapLoadTimer()
mapReady.value = false mapReady.value = false
mapLoadStarted = false mapLoadStarted = false
mapSceneRef.value?.destroyMap?.()
yakMapSceneResetRevision.value += 1 yakMapSceneResetRevision.value += 1
nextTick(() => { nextTick(() => {
scheduleYakMapLoad() scheduleYakMapLoad()
}) })
} }
defineExpose({
refreshMapView: refreshYakMapView,
resetMap: handleYakMapReset,
})
onMounted(() => { onMounted(() => {
updateScreenViewportHeight() updateScreenViewportHeight()
window.addEventListener("resize", updateScreenViewportHeight) window.addEventListener("resize", updateScreenViewportHeight)
bindYakFullscreenEvents()
syncYakFullscreenState()
initYakMap() initYakMap()
loadRemoteRows() loadRemoteRows()
loadEpidemicData() loadEpidemicData()
@ -2823,7 +2798,6 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
destroyed = true destroyed = true
window.removeEventListener("resize", updateScreenViewportHeight) window.removeEventListener("resize", updateScreenViewportHeight)
unbindYakFullscreenEvents()
if (mapLoadTimer) window.clearTimeout(mapLoadTimer) if (mapLoadTimer) window.clearTimeout(mapLoadTimer)
stopForecastRotation() stopForecastRotation()
stopFoundationRotation() stopFoundationRotation()
@ -3191,6 +3165,11 @@ function verticalAxisLabel(value) {
return formatPlaceAxisLabel(value) return formatPlaceAxisLabel(value)
} }
function verticalTownAxisLabel(value) {
const text = String(value || "").trim().replace(/\s+/g, "")
return text ? Array.from(text).join("\n") : ""
}
function formatPlaceAxisLabel(value) { function formatPlaceAxisLabel(value) {
const text = String(value || "").trim() const text = String(value || "").trim()
if (!text) return "" if (!text) return ""
@ -5339,105 +5318,6 @@ function formatSignedWan(value) {
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
.yak-map-tools {
position: absolute;
left: 462px;
bottom: 112px;
z-index: 9;
display: flex;
gap: 8px;
pointer-events: all;
transform: perspective(620px) rotateY(6deg);
transform-origin: left center;
button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-width: 72px;
height: 34px;
padding: 0 12px;
border: 1px solid rgba(48, 220, 255, 0.46);
border-radius: 3px;
color: rgba(224, 252, 255, 0.92);
font-size: 13px;
line-height: 34px;
font-weight: 900;
background:
linear-gradient(180deg, rgba(13, 67, 88, 0.74), rgba(5, 27, 43, 0.86)),
rgba(4, 24, 37, 0.72);
box-shadow:
0 0 16px rgba(48, 220, 255, 0.16),
inset 0 0 0 1px rgba(255, 255, 255, 0.06),
inset 0 -10px 18px rgba(48, 220, 255, 0.08);
cursor: pointer;
&:hover {
border-color: rgba(126, 251, 246, 0.76);
color: #ffffff;
background:
linear-gradient(180deg, rgba(21, 96, 116, 0.86), rgba(6, 39, 58, 0.92)),
rgba(6, 35, 52, 0.78);
box-shadow:
0 0 20px rgba(48, 220, 255, 0.3),
inset 0 0 0 1px rgba(255, 255, 255, 0.09),
inset 0 -10px 18px rgba(48, 220, 255, 0.14);
}
&:active {
transform: translateY(1px);
}
em {
font-style: normal;
white-space: nowrap;
}
}
}
.yak-map-tool-icon {
position: relative;
flex: 0 0 16px;
width: 16px;
height: 16px;
color: #7efbf6;
&.is-fullscreen {
background:
linear-gradient(currentColor, currentColor) left top / 7px 2px no-repeat,
linear-gradient(currentColor, currentColor) left top / 2px 7px no-repeat,
linear-gradient(currentColor, currentColor) right top / 7px 2px no-repeat,
linear-gradient(currentColor, currentColor) right top / 2px 7px no-repeat,
linear-gradient(currentColor, currentColor) left bottom / 7px 2px no-repeat,
linear-gradient(currentColor, currentColor) left bottom / 2px 7px no-repeat,
linear-gradient(currentColor, currentColor) right bottom / 7px 2px no-repeat,
linear-gradient(currentColor, currentColor) right bottom / 2px 7px no-repeat;
}
&.is-reset {
width: 15px;
height: 15px;
border: 2px solid currentColor;
border-left-color: transparent;
border-radius: 50%;
box-sizing: border-box;
&::after {
content: "";
position: absolute;
left: -2px;
top: 1px;
width: 0;
height: 0;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-right: 6px solid currentColor;
transform: rotate(-18deg);
}
}
}
.yak-bottom-topic { .yak-bottom-topic {
position: absolute; position: absolute;
left: 50%; left: 50%;
@ -6019,78 +5899,137 @@ function formatSignedWan(value) {
display: flex; display: flex;
height: 100%; height: 100%;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 4px;
padding: 7px 12px 8px; padding: 8px 13px 9px;
box-sizing: border-box; box-sizing: border-box;
} }
.heat-row { .heat-row {
position: relative; position: relative;
display: grid; display: grid;
grid-template-columns: 88px minmax(0, 1fr); grid-template-columns: 70px minmax(0, 1fr) 60px;
align-items: center; align-items: center;
gap: 8px;
min-height: 0; min-height: 0;
flex: 1 1 0; flex: 1 1 0;
overflow: hidden; padding: 0 8px;
border: 1px solid rgba(48, 220, 255, 0.22); overflow: visible;
border: 1px solid rgba(48, 220, 255, 0.14);
background: background:
linear-gradient(90deg, rgba(2, 19, 31, 0.96), rgba(2, 19, 31, 0.78)), linear-gradient(90deg, rgba(48, 220, 255, 0.08), transparent 34%, rgba(36, 246, 196, 0.04)),
rgba(3, 18, 29, 0.88); rgba(2, 18, 30, 0.58);
box-shadow: box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.025), inset 0 0 0 1px rgba(255, 255, 255, 0.018),
inset 0 0 16px rgba(0, 0, 0, 0.2); inset 0 0 12px rgba(48, 220, 255, 0.04);
&::before { &::before {
content: ""; content: "";
position: absolute; position: absolute;
left: 0; left: 78px;
top: 0; right: 68px;
bottom: 0; top: 50%;
width: var(--heat-width); height: 1px;
background: background: linear-gradient(90deg, transparent, rgba(48, 220, 255, 0.24), transparent);
linear-gradient(90deg, rgba(255, 215, 108, 0.52), rgba(48, 220, 255, 0.44)), transform: translateY(-50%);
linear-gradient(90deg, rgba(255, 255, 255, 0.14), transparent);
box-shadow: 0 0 16px rgba(48, 220, 255, 0.24);
} }
&::after { &::after {
content: ""; content: "";
position: absolute; position: absolute;
right: 6px; left: 0;
top: 4px; top: 6px;
bottom: 4px; bottom: 6px;
width: 64px; width: 2px;
background: linear-gradient(90deg, transparent, rgba(2, 17, 28, 0.78) 34%, rgba(2, 17, 28, 0.94)); background: var(--heat-color);
box-shadow: 0 0 10px var(--heat-color);
opacity: 0.78;
} }
span, span,
strong { strong,
b {
position: relative; position: relative;
z-index: 1; z-index: 1;
padding: 0 9px;
white-space: nowrap; white-space: nowrap;
} }
span { span {
color: #effdff; color: #effdff;
font-size: 13px; font-size: 12px;
line-height: 15px; line-height: 14px;
font-weight: 900; font-weight: 900;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: text-shadow:
0 1px 2px rgba(0, 0, 0, 0.72), 0 1px 2px rgba(0, 0, 0, 0.72),
0 0 8px rgba(48, 220, 255, 0.18); 0 0 8px rgba(48, 220, 255, 0.18);
} }
b {
display: block;
height: 11px;
padding: 2px;
border: 1px solid rgba(48, 220, 255, 0.22);
background:
linear-gradient(90deg, rgba(48, 220, 255, 0.08), rgba(48, 220, 255, 0.025)),
rgba(0, 14, 24, 0.66);
box-shadow:
inset 0 0 10px rgba(0, 0, 0, 0.34),
0 0 10px rgba(48, 220, 255, 0.08);
i {
position: relative;
display: block;
width: var(--heat-width);
height: 100%;
min-width: 8px;
background:
linear-gradient(90deg, rgba(255, 255, 255, 0.86), var(--heat-color) 16%, rgba(48, 220, 255, 0.72) 78%, rgba(255, 255, 255, 0.9)),
linear-gradient(90deg, var(--heat-color-soft), transparent);
box-shadow:
0 0 10px var(--heat-color-soft),
0 0 18px var(--heat-color-dim);
overflow: visible;
&::after {
content: "";
position: absolute;
right: -4px;
top: 50%;
width: 7px;
height: 7px;
border-radius: 50%;
background: #ffffff;
box-shadow:
0 0 8px #ffffff,
0 0 14px var(--heat-color);
transform: translateY(-50%);
}
&::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.42), transparent);
opacity: 0.72;
}
}
}
strong { strong {
color: #ffffff; color: #ffffff;
font-family: D-DIN, Arial, sans-serif; font-family: D-DIN, Arial, sans-serif;
font-size: 14px; font-size: 15px;
line-height: 16px; line-height: 17px;
font-weight: 900; font-weight: 900;
text-align: right; text-align: right;
text-shadow: text-shadow:
0 1px 2px rgba(0, 0, 0, 0.72), 0 1px 2px rgba(0, 0, 0, 0.72),
0 0 10px rgba(48, 220, 255, 0.24); 0 0 10px var(--heat-color-soft);
}
&.is-negative {
border-color: rgba(255, 143, 107, 0.18);
} }
} }
@ -6835,6 +6774,7 @@ function formatSignedWan(value) {
overflow: hidden; overflow: hidden;
.epidemic-modal-photo-thumb { .epidemic-modal-photo-thumb {
position: relative;
width: 100%; width: 100%;
height: 96px; height: 96px;
padding: 0; padding: 0;
@ -6848,6 +6788,17 @@ function formatSignedWan(value) {
border-color: rgba(255, 215, 108, 0.58); border-color: rgba(255, 215, 108, 0.58);
box-shadow: 0 0 14px rgba(255, 215, 108, 0.16); box-shadow: 0 0 14px rgba(255, 215, 108, 0.16);
} }
&::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 18px;
content: "";
background: linear-gradient(180deg, rgba(0, 12, 20, 0), rgba(0, 12, 20, 0.92) 52%, rgba(0, 12, 20, 0.98));
pointer-events: none;
}
} }
img { img {
@ -6954,6 +6905,16 @@ function formatSignedWan(value) {
justify-content: center; justify-content: center;
padding: 16px; padding: 16px;
background: rgba(0, 10, 17, 0.42); background: rgba(0, 10, 17, 0.42);
}
.epidemic-image-preview-frame {
position: relative;
display: flex;
max-width: 100%;
max-height: calc(78vh - 84px);
align-items: center;
justify-content: center;
overflow: hidden;
img { img {
display: block; display: block;
@ -6961,6 +6922,17 @@ function formatSignedWan(value) {
max-height: calc(78vh - 84px); max-height: calc(78vh - 84px);
object-fit: contain; object-fit: contain;
} }
&::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 38px;
content: "";
background: linear-gradient(180deg, rgba(0, 10, 17, 0), rgba(0, 10, 17, 0.96) 58%, rgba(0, 10, 17, 0.99));
pointer-events: none;
}
} }
.epidemic-ledger-pager { .epidemic-ledger-pager {

Loading…
Cancel
Save