@ -1,2 +1,2 @@ |
||||
var B=Object.defineProperty,O=Object.defineProperties;var D=Object.getOwnPropertyDescriptors;var S=Object.getOwnPropertySymbols;var W=Object.prototype.hasOwnProperty,$=Object.prototype.propertyIsEnumerable;var v=(a,r,o)=>r in a?B(a,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):a[r]=o,m=(a,r)=>{for(var o in r||(r={}))W.call(r,o)&&v(a,o,r[o]);if(S)for(var o of S(r))$.call(r,o)&&v(a,o,r[o]);return a},d=(a,r)=>O(a,D(r));import{L as b,H}from"./index-d3bd1e52-1782613078975.js";import{e as h,h as F,C as R,o as g,c as p,a as f,t as u,g as L,b as V,u as P,n as j}from"./index-1782613078975.js";const q={class:"smart-chart-header"},G={key:0},E={key:0,class:"smart-chart-source"},J={__name:"SmartChartRenderer",props:{chart:{type:Object,required:!0}},emits:["ready"],setup(a,{emit:r}){const o=r,i=a,l=["#30dcff","#27f0c0","#ffd76c","#7aa7ff","#ff8f6b","#a6f06d"],z=h(()=>{const t=(i.chart.rows||[]).filter(e=>Number.isFinite(Number(e.value)));return i.chart.type==="pie"?N(t):i.chart.type==="line"?w(t):C(t)}),k=h(()=>{const t=i.chart.rows||[];return i.chart.type==="bar"&&x(t)}),y=h(()=>{const t=i.chart.source||{},e=[t.mode,t.table,t.period,t.generatedAt].filter(Boolean);return e.length?e.join(" / "):""});F(()=>{R(()=>{window.requestAnimationFrame(()=>o("ready"))})});function C(t){return x(t)?_(t):{animationDuration:700,grid:{left:52,right:26,top:28,bottom:54},tooltip:s(),xAxis:{type:"category",data:t.map(e=>e.name),axisTick:{show:!1},axisLine:{lineStyle:{color:"rgba(48, 220, 255, 0.28)"}},axisLabel:{color:"rgba(223, 250, 255, 0.76)",fontSize:11,interval:0,rotate:t.length>6?28:0}},yAxis:{type:"value",axisLine:{show:!1},axisTick:{show:!1},splitLine:{lineStyle:{color:"rgba(48, 220, 255, 0.1)",type:"dashed"}},axisLabel:{color:"rgba(223, 250, 255, 0.58)",fontSize:11}},series:[{name:i.chart.title,type:"bar",barWidth:t.length>7?16:22,data:t.map((e,n)=>({value:e.value,name:e.name,itemStyle:{borderRadius:[8,8,0,0],color:new b(0,0,0,1,[{offset:0,color:l[n%l.length]},{offset:1,color:"rgba(48, 220, 255, 0.08)"}])}})),label:{show:!0,position:"top",color:"#ffffff",fontSize:11,formatter:({value:e})=>c(e)}}]}}function _(t){return{animationDuration:700,grid:{left:78,right:72,top:22,bottom:20},tooltip:d(m({},s()),{trigger:"axis",axisPointer:{type:"shadow"}}),xAxis:{type:"value",axisLine:{show:!1},axisTick:{show:!1},splitLine:{lineStyle:{color:"rgba(48, 220, 255, 0.1)",type:"dashed"}},axisLabel:{color:"rgba(223, 250, 255, 0.58)",fontSize:11}},yAxis:{type:"category",inverse:!0,data:t.map(e=>e.name),axisTick:{show:!1},axisLine:{lineStyle:{color:"rgba(48, 220, 255, 0.22)"}},axisLabel:{color:"rgba(223, 250, 255, 0.82)",fontSize:12}},series:[{name:i.chart.title,type:"bar",barWidth:13,data:t.map((e,n)=>({value:e.value,name:e.name,itemStyle:{borderRadius:[0,8,8,0],color:new b(0,0,1,0,[{offset:0,color:"rgba(48, 220, 255, 0.16)"},{offset:1,color:l[n%l.length]}])}})),label:{show:!0,position:"right",color:"#ffffff",fontSize:11,formatter:({value:e})=>c(e)}}]}}function N(t){return{animationDuration:700,tooltip:s(),legend:{orient:"vertical",right:10,top:"middle",itemWidth:10,itemHeight:10,textStyle:{color:"rgba(223, 250, 255, 0.78)",fontSize:11}},series:[{name:i.chart.title,type:"pie",radius:["48%","68%"],center:["35%","52%"],avoidLabelOverlap:!0,label:{color:"#fff",fontSize:11,formatter:`{b}
|
||||
{d}%`},labelLine:{lineStyle:{color:"rgba(223, 250, 255, 0.42)"}},itemStyle:{borderColor:"rgba(3, 18, 31, 0.92)",borderWidth:3},data:t.map((e,n)=>({name:e.name,value:e.value,itemStyle:{color:l[n%l.length]}}))}]}}function w(t){return{animationDuration:760,grid:{left:52,right:28,top:30,bottom:48},tooltip:d(m({},s()),{trigger:"axis",axisPointer:{type:"line",lineStyle:{color:"rgba(255, 215, 108, 0.52)"}}}),xAxis:{type:"category",boundaryGap:!1,data:t.map(e=>e.name),axisTick:{show:!1},axisLine:{lineStyle:{color:"rgba(48, 220, 255, 0.28)"}},axisLabel:{color:"rgba(223, 250, 255, 0.76)",fontSize:11,interval:0}},yAxis:{type:"value",axisLine:{show:!1},axisTick:{show:!1},splitLine:{lineStyle:{color:"rgba(48, 220, 255, 0.1)",type:"dashed"}},axisLabel:{color:"rgba(223, 250, 255, 0.58)",fontSize:11}},series:[{name:i.chart.title,type:"line",smooth:!0,symbol:"circle",symbolSize:8,lineStyle:{width:3,color:"#30dcff"},itemStyle:{color:"#ffd76c",borderColor:"#041522",borderWidth:2},areaStyle:{color:new b(0,0,0,1,[{offset:0,color:"rgba(48, 220, 255, 0.24)"},{offset:1,color:"rgba(48, 220, 255, 0.02)"}])},label:{show:!0,position:"top",color:"#ffffff",fontSize:11,formatter:({value:e})=>c(e)},data:t.map(e=>({value:e.value,name:e.name}))}]}}function s(){return{trigger:"item",backgroundColor:"rgba(2, 13, 22, 0.94)",borderColor:"rgba(48, 220, 255, 0.34)",borderWidth:1,textStyle:{color:"#fff",fontSize:12},formatter:t=>{const e=(i.chart.rows||[]).find(A=>A.name===t.name),n=(e==null?void 0:e.unit)||i.chart.unit||"",T=e!=null&&e.extra?`<br/><span style="color:rgba(223,250,255,.62)">${e.extra}</span>`:"";return`${t.marker}${t.name}: ${c(t.value)}${n}${T}`}}}function x(t=[]){return t.length>6||t.some(e=>String(e.name||"").length>4)}function c(t){const e=Number(t);return Number.isFinite(e)?Number.isInteger(e)?e.toLocaleString("zh-CN"):e.toLocaleString("zh-CN",{maximumFractionDigits:2}):"--"}return(t,e)=>(g(),p("div",{class:j(["smart-chart-card",{"is-horizontal":k.value}])},[f("div",q,[f("div",null,[f("strong",null,u(a.chart.title),1),f("span",null,u(a.chart.subtitle),1)]),a.chart.unit?(g(),p("em",G,u(a.chart.unit),1)):L("",!0)]),V(P(H),{class:"smart-chart",option:z.value,autoresize:""},null,8,["option"]),y.value?(g(),p("div",E,u(y.value),1)):L("",!0)],2))}};export{J as default}; |
||||
var B=Object.defineProperty,O=Object.defineProperties;var D=Object.getOwnPropertyDescriptors;var S=Object.getOwnPropertySymbols;var W=Object.prototype.hasOwnProperty,$=Object.prototype.propertyIsEnumerable;var v=(a,r,o)=>r in a?B(a,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):a[r]=o,m=(a,r)=>{for(var o in r||(r={}))W.call(r,o)&&v(a,o,r[o]);if(S)for(var o of S(r))$.call(r,o)&&v(a,o,r[o]);return a},d=(a,r)=>O(a,D(r));import{L as b,H}from"./index-dd7cb1f5-1782659605222.js";import{e as h,h as F,q as R,o as g,c as p,a as f,t as u,g as L,b as V,u as q,n as P}from"./index-1782659605222.js";const j={class:"smart-chart-header"},G={key:0},E={key:0,class:"smart-chart-source"},J={__name:"SmartChartRenderer",props:{chart:{type:Object,required:!0}},emits:["ready"],setup(a,{emit:r}){const o=r,i=a,l=["#30dcff","#27f0c0","#ffd76c","#7aa7ff","#ff8f6b","#a6f06d"],z=h(()=>{const t=(i.chart.rows||[]).filter(e=>Number.isFinite(Number(e.value)));return i.chart.type==="pie"?N(t):i.chart.type==="line"?w(t):_(t)}),k=h(()=>{const t=i.chart.rows||[];return i.chart.type==="bar"&&x(t)}),y=h(()=>{const t=i.chart.source||{},e=[t.mode,t.table,t.period,t.generatedAt].filter(Boolean);return e.length?e.join(" / "):""});F(()=>{R(()=>{window.requestAnimationFrame(()=>o("ready"))})});function _(t){return x(t)?C(t):{animationDuration:700,grid:{left:52,right:26,top:28,bottom:54},tooltip:s(),xAxis:{type:"category",data:t.map(e=>e.name),axisTick:{show:!1},axisLine:{lineStyle:{color:"rgba(48, 220, 255, 0.28)"}},axisLabel:{color:"rgba(223, 250, 255, 0.76)",fontSize:11,interval:0,rotate:t.length>6?28:0}},yAxis:{type:"value",axisLine:{show:!1},axisTick:{show:!1},splitLine:{lineStyle:{color:"rgba(48, 220, 255, 0.1)",type:"dashed"}},axisLabel:{color:"rgba(223, 250, 255, 0.58)",fontSize:11}},series:[{name:i.chart.title,type:"bar",barWidth:t.length>7?16:22,data:t.map((e,n)=>({value:e.value,name:e.name,itemStyle:{borderRadius:[8,8,0,0],color:new b(0,0,0,1,[{offset:0,color:l[n%l.length]},{offset:1,color:"rgba(48, 220, 255, 0.08)"}])}})),label:{show:!0,position:"top",color:"#ffffff",fontSize:11,formatter:({value:e})=>c(e)}}]}}function C(t){return{animationDuration:700,grid:{left:78,right:72,top:22,bottom:20},tooltip:d(m({},s()),{trigger:"axis",axisPointer:{type:"shadow"}}),xAxis:{type:"value",axisLine:{show:!1},axisTick:{show:!1},splitLine:{lineStyle:{color:"rgba(48, 220, 255, 0.1)",type:"dashed"}},axisLabel:{color:"rgba(223, 250, 255, 0.58)",fontSize:11}},yAxis:{type:"category",inverse:!0,data:t.map(e=>e.name),axisTick:{show:!1},axisLine:{lineStyle:{color:"rgba(48, 220, 255, 0.22)"}},axisLabel:{color:"rgba(223, 250, 255, 0.82)",fontSize:12}},series:[{name:i.chart.title,type:"bar",barWidth:13,data:t.map((e,n)=>({value:e.value,name:e.name,itemStyle:{borderRadius:[0,8,8,0],color:new b(0,0,1,0,[{offset:0,color:"rgba(48, 220, 255, 0.16)"},{offset:1,color:l[n%l.length]}])}})),label:{show:!0,position:"right",color:"#ffffff",fontSize:11,formatter:({value:e})=>c(e)}}]}}function N(t){return{animationDuration:700,tooltip:s(),legend:{orient:"vertical",right:10,top:"middle",itemWidth:10,itemHeight:10,textStyle:{color:"rgba(223, 250, 255, 0.78)",fontSize:11}},series:[{name:i.chart.title,type:"pie",radius:["48%","68%"],center:["35%","52%"],avoidLabelOverlap:!0,label:{color:"#fff",fontSize:11,formatter:`{b}
|
||||
{d}%`},labelLine:{lineStyle:{color:"rgba(223, 250, 255, 0.42)"}},itemStyle:{borderColor:"rgba(3, 18, 31, 0.92)",borderWidth:3},data:t.map((e,n)=>({name:e.name,value:e.value,itemStyle:{color:l[n%l.length]}}))}]}}function w(t){return{animationDuration:760,grid:{left:52,right:28,top:30,bottom:48},tooltip:d(m({},s()),{trigger:"axis",axisPointer:{type:"line",lineStyle:{color:"rgba(255, 215, 108, 0.52)"}}}),xAxis:{type:"category",boundaryGap:!1,data:t.map(e=>e.name),axisTick:{show:!1},axisLine:{lineStyle:{color:"rgba(48, 220, 255, 0.28)"}},axisLabel:{color:"rgba(223, 250, 255, 0.76)",fontSize:11,interval:0}},yAxis:{type:"value",axisLine:{show:!1},axisTick:{show:!1},splitLine:{lineStyle:{color:"rgba(48, 220, 255, 0.1)",type:"dashed"}},axisLabel:{color:"rgba(223, 250, 255, 0.58)",fontSize:11}},series:[{name:i.chart.title,type:"line",smooth:!0,symbol:"circle",symbolSize:8,lineStyle:{width:3,color:"#30dcff"},itemStyle:{color:"#ffd76c",borderColor:"#041522",borderWidth:2},areaStyle:{color:new b(0,0,0,1,[{offset:0,color:"rgba(48, 220, 255, 0.24)"},{offset:1,color:"rgba(48, 220, 255, 0.02)"}])},label:{show:!0,position:"top",color:"#ffffff",fontSize:11,formatter:({value:e})=>c(e)},data:t.map(e=>({value:e.value,name:e.name}))}]}}function s(){return{trigger:"item",backgroundColor:"rgba(2, 13, 22, 0.94)",borderColor:"rgba(48, 220, 255, 0.34)",borderWidth:1,textStyle:{color:"#fff",fontSize:12},formatter:t=>{const e=(i.chart.rows||[]).find(A=>A.name===t.name),n=(e==null?void 0:e.unit)||i.chart.unit||"",T=e!=null&&e.extra?`<br/><span style="color:rgba(223,250,255,.62)">${e.extra}</span>`:"";return`${t.marker}${t.name}: ${c(t.value)}${n}${T}`}}}function x(t=[]){return t.length>6||t.some(e=>String(e.name||"").length>4)}function c(t){const e=Number(t);return Number.isFinite(e)?Number.isInteger(e)?e.toLocaleString("zh-CN"):e.toLocaleString("zh-CN",{maximumFractionDigits:2}):"--"}return(t,e)=>(g(),p("div",{class:P(["smart-chart-card",{"is-horizontal":k.value}])},[f("div",j,[f("div",null,[f("strong",null,u(a.chart.title),1),f("span",null,u(a.chart.subtitle),1)]),a.chart.unit?(g(),p("em",G,u(a.chart.unit),1)):L("",!0)]),V(q(H),{class:"smart-chart",option:z.value,autoresize:""},null,8,["option"]),y.value?(g(),p("div",E,u(y.value),1)):L("",!0)],2))}};export{J as default}; |
||||
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 701 B After Width: | Height: | Size: 701 B |
|
Before Width: | Height: | Size: 668 B After Width: | Height: | Size: 668 B |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 149 B After Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 149 B After Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 981 B After Width: | Height: | Size: 981 B |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 282 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
@ -0,0 +1 @@ |
||||
.login-screen[data-v-02c20620]{position:fixed;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;overflow:hidden;background:linear-gradient(180deg,rgba(4,16,38,.1),rgba(2,8,22,.62)),url(./bg-3acb94e3-balabala-1782659605222.png) center/cover no-repeat,#05142f;color:#fff}.login-screen[data-v-02c20620]:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(90deg,rgba(0,0,0,.46),transparent 42%,rgba(0,0,0,.35)),radial-gradient(circle at 52% 46%,rgba(48,220,255,.16),transparent 32%)}.login-shell[data-v-02c20620]{position:relative;z-index:1;width:min(980px,100vw - 48px);display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:56px;align-items:center}.login-brand[data-v-02c20620]{display:flex;flex-direction:column;gap:14px;min-width:0;text-shadow:0 0 20px rgba(48,220,255,.36)}.login-brand span[data-v-02c20620]{font-size:42px;line-height:1.2;font-weight:700}.login-brand small[data-v-02c20620]{color:#c4f3feb8;font-size:13px;letter-spacing:0}.login-panel[data-v-02c20620]{width:360px;padding:30px 28px 28px;border:1px solid rgba(48,220,255,.34);background:rgba(3,14,32,.82);box-shadow:0 24px 70px #00000057,inset 0 0 28px #30dcff14;box-sizing:border-box}.login-panel-title[data-v-02c20620]{margin-bottom:26px;color:#c4f3fe;font-size:22px;font-weight:700;text-align:center}.login-field[data-v-02c20620]{display:block;margin-bottom:18px}.login-field span[data-v-02c20620]{display:block;margin-bottom:8px;color:#c4f3fec7;font-size:13px}.login-field input[data-v-02c20620]{width:100%;height:42px;padding:0 12px;border:1px solid rgba(48,220,255,.26);outline:none;background:rgba(0,10,24,.62);color:#fff;font-size:15px;box-sizing:border-box}.login-field input[data-v-02c20620]:focus{border-color:#30dcffb8;box-shadow:0 0 0 2px #30dcff1f}.login-error[data-v-02c20620]{min-height:20px;margin:-4px 0 12px;color:#ffcf8a;font-size:13px;line-height:20px}.login-submit[data-v-02c20620]{width:100%;height:44px;border:1px solid rgba(48,220,255,.52);background:linear-gradient(90deg,rgba(24,140,196,.92),rgba(37,204,218,.92));color:#fff;cursor:pointer;font-size:16px;font-weight:700}.login-submit[data-v-02c20620]:disabled{cursor:not-allowed;opacity:.68}@media (max-width: 820px){.login-shell[data-v-02c20620]{grid-template-columns:1fr;gap:28px}.login-brand span[data-v-02c20620]{font-size:30px}.login-panel[data-v-02c20620]{width:100%}} |
||||
@ -0,0 +1 @@ |
||||
var m=(v,d,n)=>new Promise((i,a)=>{var u=l=>{try{r(n.next(l))}catch(t){a(t)}},o=l=>{try{r(n.throw(l))}catch(t){a(t)}},r=l=>l.done?i(l.value):Promise.resolve(l.value).then(u,o);r((n=n.apply(v,d)).next())});import{_ as x,P as w,Q as h,e as B,o as _,c as f,a as e,D,A as g,B as y,t as b,g as R,d as p,R as S}from"./index-1782659605222.js";const V={class:"login-screen"},k={class:"login-shell"},M={class:"login-field"},P={class:"login-field"},U={key:0,class:"login-error"},q=["disabled"],A={__name:"index",setup(v){const d=w(),n=h(),i=p(""),a=p(""),u=p(!1),o=p(""),r=B(()=>{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 S(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",V,[e("div",k,[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:D(l,["prevent"])},[s[4]||(s[4]=e("div",{class:"login-panel-title"},"用户登录",-1)),e("label",M,[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",P,[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",U,b(o.value),1)):R("",!0),e("button",{class:"login-submit",disabled:u.value,type:"submit"},b(u.value?"登录中":"登录"),9,q)],32)])]))}},I=x(A,[["__scopeId","data-v-02c20620"]]);export{I as default}; |
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 499 B After Width: | Height: | Size: 499 B |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@ -0,0 +1,85 @@ |
||||
const AUTH_COOKIE_NAME = "Authorization" |
||||
|
||||
function encodeRedirect(path) { |
||||
return encodeURIComponent(path || "/platform") |
||||
} |
||||
|
||||
export function getAuthorizationToken() { |
||||
const cookie = document.cookie |
||||
.split(";") |
||||
.map((item) => item.trim()) |
||||
.find((item) => item.startsWith(`${AUTH_COOKIE_NAME}=`)) |
||||
return cookie ? decodeURIComponent(cookie.slice(AUTH_COOKIE_NAME.length + 1)) : "" |
||||
} |
||||
|
||||
export function clearAuthorizationToken() { |
||||
document.cookie = `${AUTH_COOKIE_NAME}=; Max-Age=0; path=/` |
||||
} |
||||
|
||||
export function getLoginPath(redirect = window.location.hash.replace(/^#/, "") || "/platform") { |
||||
return `/login?redirect=${encodeRedirect(redirect)}` |
||||
} |
||||
|
||||
export function redirectToLogin(redirect) { |
||||
clearAuthorizationToken() |
||||
const loginPath = getLoginPath(redirect) |
||||
if (window.location.hash.replace(/^#/, "") !== loginPath) { |
||||
window.location.hash = loginPath |
||||
} |
||||
} |
||||
|
||||
export async function login(username, password) { |
||||
const response = await fetch("/login", { |
||||
method: "POST", |
||||
credentials: "include", |
||||
headers: { |
||||
"Content-Type": "application/json", |
||||
Accept: "application/json", |
||||
"Content-Language": "zh_CN", |
||||
}, |
||||
body: JSON.stringify({ username, password }), |
||||
}) |
||||
const data = await response.json().catch(() => ({})) |
||||
if (!response.ok || data?.code !== 200) { |
||||
const error = new Error(data?.msg || "登录失败,请检查用户名或密码。") |
||||
error.status = response.status |
||||
error.code = data?.code |
||||
throw error |
||||
} |
||||
if (data?.data?.token) { |
||||
document.cookie = `${AUTH_COOKIE_NAME}=${encodeURIComponent(data.data.token)}; path=/` |
||||
} |
||||
return data |
||||
} |
||||
|
||||
export async function logout() { |
||||
await fetch("/logout", { |
||||
method: "POST", |
||||
credentials: "include", |
||||
headers: { |
||||
Accept: "application/json", |
||||
"Content-Type": "application/json", |
||||
"Content-Language": "zh_CN", |
||||
}, |
||||
body: "{}", |
||||
}).catch(() => null) |
||||
redirectToLogin("/platform") |
||||
} |
||||
|
||||
export async function fetchCurrentUser() { |
||||
const response = await fetch("/getInfo", { |
||||
credentials: "include", |
||||
headers: { |
||||
Accept: "application/json", |
||||
"Content-Language": "zh_CN", |
||||
}, |
||||
}) |
||||
const data = await response.json().catch(() => ({})) |
||||
if (!response.ok || data?.code !== 200) { |
||||
const error = new Error(data?.msg || "用户未登录或授权已过期。") |
||||
error.status = response.status || 401 |
||||
error.code = data?.code || 401 |
||||
throw error |
||||
} |
||||
return data.data |
||||
} |
||||
@ -0,0 +1,80 @@ |
||||
import { normalizeResponseData, requestJson } from "./request" |
||||
|
||||
const JSY_API_BASE = import.meta.env.VITE_JSY_API_BASE || "/jsyApi/api" |
||||
const OPEN_API_BASE = import.meta.env.VITE_OPEN_API_BASE || "/openApi" |
||||
|
||||
function joinBase(base, path) { |
||||
if (/^https?:\/\//.test(path)) return path |
||||
return `${base}${path.startsWith("/") ? path : `/${path}`}` |
||||
} |
||||
|
||||
function unwrapResponse(response) { |
||||
const failed = |
||||
response?.success === false || |
||||
(response?.code !== undefined && ![0, 200, "0", "200"].includes(response.code)) |
||||
|
||||
if (failed) { |
||||
const error = new Error(response?.msg || response?.message || "接口请求失败") |
||||
error.code = response?.code |
||||
throw error |
||||
} |
||||
|
||||
return normalizeResponseData(response) |
||||
} |
||||
|
||||
function get(base, path, params, options = {}) { |
||||
return requestJson(joinBase(base, path), { |
||||
params, |
||||
timeout: options.timeout || 16000, |
||||
}).then(unwrapResponse) |
||||
} |
||||
|
||||
function normalizeRows(data) { |
||||
if (Array.isArray(data)) return data |
||||
if (Array.isArray(data?.rows)) return data.rows |
||||
if (Array.isArray(data?.list)) return data.list |
||||
if (Array.isArray(data?.data)) return data.data |
||||
return [] |
||||
} |
||||
|
||||
export async function getBalanceAdvice({ areaCode, pasturesId } = {}) { |
||||
const params = { |
||||
areaCode, |
||||
pasturesId, |
||||
} |
||||
const requests = [ |
||||
() => get(OPEN_API_BASE, "/ai/balance/advice", params, { timeout: 18000 }), |
||||
() => get(JSY_API_BASE, "/ai/livestockBalance", params, { timeout: 18000 }), |
||||
] |
||||
|
||||
let lastError |
||||
for (const request of requests) { |
||||
try { |
||||
return await request() |
||||
} catch (error) { |
||||
lastError = error |
||||
} |
||||
} |
||||
throw lastError || new Error("草畜平衡接口不可用") |
||||
} |
||||
|
||||
export async function queryBalanceTargets(question) { |
||||
const requests = [ |
||||
() => get(OPEN_API_BASE, "/ai/balance/targets", { question }, { timeout: 16000 }), |
||||
() => get(JSY_API_BASE, "/ai/question/balance", { question }, { timeout: 16000 }), |
||||
] |
||||
|
||||
let lastError |
||||
for (const request of requests) { |
||||
try { |
||||
return normalizeRows(await request()) |
||||
} catch (error) { |
||||
lastError = error |
||||
} |
||||
} |
||||
throw lastError || new Error("草畜平衡对象识别接口不可用") |
||||
} |
||||
|
||||
export function getAreaChildren(areaCode) { |
||||
return get(JSY_API_BASE, "/area/children", { areaCode }, { timeout: 12000 }).then(normalizeRows) |
||||
} |
||||
@ -0,0 +1,325 @@ |
||||
import { grasslandYakRows } from "@/views/gdMap/livestockData" |
||||
import { pastureListRows } from "@/views/pastureManagement/data" |
||||
|
||||
export const BALANCE_CALCULATION_PARAMS = { |
||||
grasslandCapacityPerMu: 0.047, |
||||
yakToSheepUnit: 4.8, |
||||
} |
||||
|
||||
export const COUNTY_TARGET = { |
||||
key: "county-513233", |
||||
type: "county", |
||||
name: "红原县", |
||||
areaCode: "513233", |
||||
} |
||||
|
||||
export const RISK_LEVELS = [ |
||||
{ |
||||
key: "healthy", |
||||
label: "草畜平衡", |
||||
shortLabel: "平衡", |
||||
range: "≤ 0%", |
||||
description: "承载力覆盖实际载畜量,当前放牧压力处于合理区间。", |
||||
actions: ["保持当前载畜水平与轮牧节奏", "定期复测草地生产力和载畜量", "关注极端天气对牧草生长的影响"], |
||||
}, |
||||
{ |
||||
key: "watch", |
||||
label: "轻度超载", |
||||
shortLabel: "关注", |
||||
range: "0% - 10%", |
||||
description: "已出现轻度超载迹象,建议提前介入调控。", |
||||
actions: ["适度压减超载畜群规模", "延长冬春舍饲补饲周期", "增加人工草场补饲供应"], |
||||
}, |
||||
{ |
||||
key: "warning", |
||||
label: "中度超载", |
||||
shortLabel: "预警", |
||||
range: "10% - 20%", |
||||
description: "超载较明显,草地退化风险上升,应启动主动调控。", |
||||
actions: ["压减载畜量至理论承载力以内", "推行划区轮牧与阶段性休牧", "扩大优质人工饲草供给"], |
||||
}, |
||||
{ |
||||
key: "danger", |
||||
label: "严重超载", |
||||
shortLabel: "高压", |
||||
range: "> 20%", |
||||
description: "超载压力较高,建议纳入重点管控和生态修复范围。", |
||||
actions: ["强制压减载畜量并实施禁牧休牧", "对退化草地实施生态修复", "建立重点区域长效监测与考核机制"], |
||||
}, |
||||
] |
||||
|
||||
export const fieldMap = { |
||||
areaRange: ["areaRange", "area_range", "acreage", "grasslandArea", "area"], |
||||
yakNumber: ["yakNumber", "yak_number", "number", "yakCount", "livestockCount"], |
||||
bearingCapacity: ["bearingCapacity", "bearing_capacity", "capacity", "theoryCapacity"], |
||||
livestockCapacity: ["livestockCapacity", "livestock_capacity", "carryingCapacity", "actualCapacity"], |
||||
overloadRate: ["overloadRate", "overload_rate", "rate"], |
||||
level: ["level", "conclusion"], |
||||
advice: ["advice", "balanceAdvice", "balance_advice", "balanceAdivce", "balance_adivce"], |
||||
year: ["year"], |
||||
areaName: ["areaName", "area_name", "name"], |
||||
pasturesName: ["pasturesName", "pastures_name", "pastureName", "muhuName", "name"], |
||||
} |
||||
|
||||
export const townCodeByName = { |
||||
邛溪镇: "513233100", |
||||
刷经寺镇: "513233101", |
||||
安曲镇: "513233103", |
||||
龙日镇: "513233105", |
||||
江茸乡: "513233202", |
||||
查尔玛乡: "513233203", |
||||
瓦切镇: "513233204", |
||||
瓦切乡: "513233204", |
||||
阿木乡: "513233205", |
||||
麦洼乡: "513233207", |
||||
色地乡: "513233208", |
||||
色地镇: "513233208", |
||||
壤口乡: "513233209", |
||||
} |
||||
|
||||
const round = (value, decimals = 2) => { |
||||
const factor = 10 ** decimals |
||||
return Math.round((Number(value) || 0) * factor) / factor |
||||
} |
||||
|
||||
const townPressureFactorByName = { |
||||
色地乡: 1.38, |
||||
麦洼乡: 1.26, |
||||
壤口乡: 0.92, |
||||
阿木乡: 1.12, |
||||
瓦切镇: 1.2, |
||||
查尔玛乡: 1.04, |
||||
江茸乡: 0.88, |
||||
龙日镇: 1.16, |
||||
安曲镇: 1.28, |
||||
刷经寺镇: 0.78, |
||||
邛溪镇: 1.08, |
||||
} |
||||
|
||||
export function toNumber(value, fallback = null) { |
||||
if (value === undefined || value === null || value === "") return fallback |
||||
const normalized = typeof value === "string" |
||||
? value.replace(/,/g, "").replace(/%/g, "").trim() |
||||
: value |
||||
const number = Number(normalized) |
||||
return Number.isFinite(number) ? number : fallback |
||||
} |
||||
|
||||
export function pickValue(item = {}, keys = [], fallback = undefined) { |
||||
for (const key of keys) { |
||||
const value = item?.[key] |
||||
if (value !== undefined && value !== null && value !== "") return value |
||||
} |
||||
return fallback |
||||
} |
||||
|
||||
export function formatNumber(value, options = {}) { |
||||
const number = toNumber(value) |
||||
if (number === null) return "--" |
||||
return number.toLocaleString("zh-CN", { |
||||
maximumFractionDigits: options.maximumFractionDigits ?? (Number.isInteger(number) ? 0 : 2), |
||||
}) |
||||
} |
||||
|
||||
export function formatPercent(value) { |
||||
const number = toNumber(value) |
||||
if (number === null) return "--" |
||||
return `${formatNumber(number)}%` |
||||
} |
||||
|
||||
export function getRiskLevel(value) { |
||||
const rate = toNumber(value) |
||||
if (rate === null || rate <= 0) return RISK_LEVELS[0] |
||||
if (rate <= 10) return RISK_LEVELS[1] |
||||
if (rate <= 20) return RISK_LEVELS[2] |
||||
return RISK_LEVELS[3] |
||||
} |
||||
|
||||
function calculateLivestockCapacity(yakCount) { |
||||
return Math.round((Number(yakCount) || 0) * BALANCE_CALCULATION_PARAMS.yakToSheepUnit) |
||||
} |
||||
|
||||
function calculateBearingCapacity(areaRange) { |
||||
return Math.round((Number(areaRange) || 0) * BALANCE_CALCULATION_PARAMS.grasslandCapacityPerMu) |
||||
} |
||||
|
||||
function buildBalanceRow({ |
||||
key, |
||||
type, |
||||
name, |
||||
areaCode, |
||||
pasturesId, |
||||
areaRange, |
||||
yakNumber, |
||||
location, |
||||
township, |
||||
pressureFactor = 1, |
||||
}) { |
||||
const bearingCapacity = calculateBearingCapacity(areaRange) |
||||
const livestockCapacity = Math.round(calculateLivestockCapacity(yakNumber) * pressureFactor) |
||||
const overloadRate = bearingCapacity ? round(((livestockCapacity - bearingCapacity) / bearingCapacity) * 100, 1) : 0 |
||||
const risk = getRiskLevel(overloadRate) |
||||
return { |
||||
key, |
||||
type, |
||||
name, |
||||
areaCode, |
||||
pasturesId, |
||||
areaRange: round(areaRange, 0), |
||||
yakNumber: round(yakNumber, 0), |
||||
bearingCapacity, |
||||
livestockCapacity, |
||||
overloadRate, |
||||
level: risk.label, |
||||
advice: risk.actions.join(";"), |
||||
year: "2026", |
||||
location, |
||||
township, |
||||
} |
||||
} |
||||
|
||||
export const balanceTownRows = grasslandYakRows.map((item, index) => buildBalanceRow({ |
||||
key: `town-${item.zoneName}`, |
||||
type: "town", |
||||
name: item.zoneName, |
||||
areaCode: townCodeByName[item.zoneName], |
||||
areaRange: item.grasslandArea, |
||||
yakNumber: item.yakCount, |
||||
location: null, |
||||
pressureFactor: townPressureFactorByName[item.zoneName] || 1, |
||||
index, |
||||
})) |
||||
|
||||
const countyAreaRange = balanceTownRows.reduce((sum, item) => sum + item.areaRange, 0) |
||||
const countyYakNumber = balanceTownRows.reduce((sum, item) => sum + item.yakNumber, 0) |
||||
const countyBearingCapacity = balanceTownRows.reduce((sum, item) => sum + item.bearingCapacity, 0) |
||||
const countyLivestockCapacity = balanceTownRows.reduce((sum, item) => sum + item.livestockCapacity, 0) |
||||
const countyOverloadRate = countyBearingCapacity |
||||
? round(((countyLivestockCapacity - countyBearingCapacity) / countyBearingCapacity) * 100, 1) |
||||
: 0 |
||||
const countyRisk = getRiskLevel(countyOverloadRate) |
||||
|
||||
export const countyBalanceRow = { |
||||
...COUNTY_TARGET, |
||||
areaRange: countyAreaRange, |
||||
yakNumber: countyYakNumber, |
||||
bearingCapacity: countyBearingCapacity, |
||||
livestockCapacity: countyLivestockCapacity, |
||||
overloadRate: countyOverloadRate, |
||||
level: countyRisk.label, |
||||
advice: countyRisk.actions.join(";"), |
||||
year: "2026", |
||||
} |
||||
|
||||
export const balancePastureRows = pastureListRows.map((item) => buildBalanceRow({ |
||||
key: item.id || item.key || item.name, |
||||
type: "pasture", |
||||
name: item.name, |
||||
areaCode: townCodeByName[item.township], |
||||
pasturesId: item.id, |
||||
areaRange: item.grasslandArea, |
||||
yakNumber: item.yakCount, |
||||
location: item.location, |
||||
township: item.township, |
||||
})) |
||||
|
||||
export const balanceFallbackRows = [ |
||||
countyBalanceRow, |
||||
...balanceTownRows, |
||||
...balancePastureRows, |
||||
] |
||||
|
||||
export function normalizeBalanceResult(source = {}, fallback = {}) { |
||||
const areaRange = toNumber(pickValue(source, fieldMap.areaRange), fallback.areaRange ?? 0) |
||||
const yakNumber = toNumber(pickValue(source, fieldMap.yakNumber), fallback.yakNumber ?? 0) |
||||
const bearingCapacity = toNumber(pickValue(source, fieldMap.bearingCapacity), fallback.bearingCapacity ?? calculateBearingCapacity(areaRange)) |
||||
const livestockCapacity = toNumber(pickValue(source, fieldMap.livestockCapacity), fallback.livestockCapacity ?? calculateLivestockCapacity(yakNumber)) |
||||
const directRate = toNumber(pickValue(source, fieldMap.overloadRate)) |
||||
const overloadRate = directRate !== null |
||||
? round(directRate, 1) |
||||
: bearingCapacity |
||||
? round(((livestockCapacity - bearingCapacity) / bearingCapacity) * 100, 1) |
||||
: 0 |
||||
const risk = getRiskLevel(overloadRate) |
||||
return { |
||||
...fallback, |
||||
...source, |
||||
key: fallback.key || source.key || source.id || source.areaCode || source.pasturesId || fallback.name, |
||||
type: fallback.type || source.type || (source.pasturesId ? "pasture" : "town"), |
||||
name: pickValue(source, [...fieldMap.pasturesName, ...fieldMap.areaName], fallback.name || "核算对象"), |
||||
areaCode: source.areaCode || source.area_code || fallback.areaCode, |
||||
pasturesId: source.pasturesId || source.pastures_id || fallback.pasturesId, |
||||
areaRange, |
||||
yakNumber, |
||||
bearingCapacity, |
||||
livestockCapacity, |
||||
overloadRate, |
||||
level: pickValue(source, fieldMap.level, risk.label), |
||||
advice: pickValue(source, fieldMap.advice, fallback.advice || risk.actions.join(";")), |
||||
year: pickValue(source, fieldMap.year, fallback.year || "当前年度"), |
||||
} |
||||
} |
||||
|
||||
export function deriveBalance(row = {}) { |
||||
const bearing = toNumber(row.bearingCapacity, 0) |
||||
const livestock = toNumber(row.livestockCapacity, 0) |
||||
const areaRange = toNumber(row.areaRange, 0) |
||||
const yakNumber = toNumber(row.yakNumber, 0) |
||||
const overloadRate = toNumber(row.overloadRate, bearing ? ((livestock - bearing) / bearing) * 100 : 0) |
||||
const delta = livestock - bearing |
||||
const usageRate = bearing ? round((livestock / bearing) * 100, 1) : 0 |
||||
const risk = getRiskLevel(overloadRate) |
||||
const yakSheepUnit = Math.round(yakNumber * BALANCE_CALCULATION_PARAMS.yakToSheepUnit) |
||||
const pressureFactor = yakSheepUnit ? round(livestock / yakSheepUnit, 2) : 1 |
||||
const bearingCoefficient = areaRange ? round(bearing / areaRange, 4) : BALANCE_CALCULATION_PARAMS.grasslandCapacityPerMu |
||||
return { |
||||
bearing, |
||||
livestock, |
||||
areaRange, |
||||
yakNumber, |
||||
yakSheepUnit, |
||||
pressureFactor, |
||||
bearingCoefficient, |
||||
overloadRate: round(overloadRate, 1), |
||||
delta, |
||||
usageRate, |
||||
risk, |
||||
bearingPercent: Math.max(4, Math.min(100, round((bearing / Math.max(bearing, livestock, 1)) * 100, 1))), |
||||
livestockPercent: Math.max(4, Math.min(100, round((livestock / Math.max(bearing, livestock, 1)) * 100, 1))), |
||||
} |
||||
} |
||||
|
||||
export function splitAdvice(row = {}, risk = getRiskLevel(row.overloadRate)) { |
||||
const text = String(row.advice || "").trim() |
||||
const apiPoints = text |
||||
? text.split(/[。;;\n]/).map((item) => item.trim()).filter(Boolean) |
||||
: [] |
||||
const unsuitableWhenOverloaded = ["提高放牧畜禽的密度", "增加草地的利用率", "密度不能过高"] |
||||
const cleanedApiPoints = apiPoints.filter((item) => { |
||||
if (unsuitableWhenOverloaded.some((keyword) => item.includes(keyword))) return false |
||||
if (risk.key === "healthy") return true |
||||
return !unsuitableWhenOverloaded.some((keyword) => item.includes(keyword)) |
||||
}) |
||||
return [...(risk.actions || []), ...cleanedApiPoints] |
||||
.filter((item, index, array) => array.indexOf(item) === index) |
||||
.slice(0, 4) |
||||
} |
||||
|
||||
export function normalizeTargetRows(rows = []) { |
||||
return rows |
||||
.map((item, index) => { |
||||
const name = pickValue(item, ["name", "label", "areaName", "area_name", "pasturesName", "pastures_name"], "") |
||||
const areaCode = pickValue(item, ["areaCode", "area_code", "code"]) |
||||
const pasturesId = pickValue(item, ["pasturesId", "pastures_id", "id", "value"]) |
||||
if (!name && !areaCode && !pasturesId) return null |
||||
return { |
||||
key: pasturesId ? `pasture-${pasturesId}` : `area-${areaCode || index}`, |
||||
type: pasturesId && item.key !== "areaCode" ? "pasture" : "town", |
||||
name: name || areaCode || pasturesId, |
||||
areaCode, |
||||
pasturesId: item.key === "pasturesId" ? pasturesId : undefined, |
||||
raw: item, |
||||
} |
||||
}) |
||||
.filter(Boolean) |
||||
} |
||||
@ -0,0 +1,200 @@ |
||||
<template> |
||||
<div class="login-screen"> |
||||
<div class="login-shell"> |
||||
<div class="login-brand"> |
||||
<span>红原县数字畜牧业监管平台</span> |
||||
<small>Hongyuan Digital Animal Husbandry Supervision Platform</small> |
||||
</div> |
||||
|
||||
<form class="login-panel" @submit.prevent="handleSubmit"> |
||||
<div class="login-panel-title">用户登录</div> |
||||
<label class="login-field"> |
||||
<span>账号</span> |
||||
<input v-model.trim="username" autocomplete="username" autofocus placeholder="请输入账号" /> |
||||
</label> |
||||
<label class="login-field"> |
||||
<span>密码</span> |
||||
<input v-model="password" autocomplete="current-password" placeholder="请输入密码" type="password" /> |
||||
</label> |
||||
<div v-if="errorMessage" class="login-error">{{ errorMessage }}</div> |
||||
<button class="login-submit" :disabled="submitting" type="submit"> |
||||
{{ submitting ? "登录中" : "登录" }} |
||||
</button> |
||||
</form> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup> |
||||
import { computed, ref } from "vue" |
||||
import { useRoute, useRouter } from "vue-router" |
||||
import { login } from "@/services/auth" |
||||
|
||||
const route = useRoute() |
||||
const router = useRouter() |
||||
const username = ref("") |
||||
const password = ref("") |
||||
const submitting = ref(false) |
||||
const errorMessage = ref("") |
||||
const redirectPath = computed(() => { |
||||
const redirect = typeof route.query.redirect === "string" ? decodeURIComponent(route.query.redirect) : "/platform" |
||||
return redirect.startsWith("/login") ? "/platform" : redirect || "/platform" |
||||
}) |
||||
|
||||
async function handleSubmit() { |
||||
errorMessage.value = "" |
||||
if (!username.value || !password.value) { |
||||
errorMessage.value = "请输入账号和密码。" |
||||
return |
||||
} |
||||
submitting.value = true |
||||
try { |
||||
await login(username.value, password.value) |
||||
router.replace(redirectPath.value) |
||||
} catch (error) { |
||||
errorMessage.value = error?.message || "登录失败,请稍后再试。" |
||||
} finally { |
||||
submitting.value = false |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.login-screen { |
||||
position: fixed; |
||||
inset: 0; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
overflow: hidden; |
||||
background: |
||||
linear-gradient(180deg, rgba(4, 16, 38, 0.1), rgba(2, 8, 22, 0.62)), |
||||
url("@/assets/images/bg.png") center / cover no-repeat, |
||||
#05142f; |
||||
color: #fff; |
||||
} |
||||
|
||||
.login-screen::before { |
||||
content: ""; |
||||
position: absolute; |
||||
inset: 0; |
||||
background: |
||||
linear-gradient(90deg, rgba(0, 0, 0, 0.46), transparent 42%, rgba(0, 0, 0, 0.35)), |
||||
radial-gradient(circle at 52% 46%, rgba(48, 220, 255, 0.16), transparent 32%); |
||||
} |
||||
|
||||
.login-shell { |
||||
position: relative; |
||||
z-index: 1; |
||||
width: min(980px, calc(100vw - 48px)); |
||||
display: grid; |
||||
grid-template-columns: minmax(0, 1fr) 360px; |
||||
gap: 56px; |
||||
align-items: center; |
||||
} |
||||
|
||||
.login-brand { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 14px; |
||||
min-width: 0; |
||||
text-shadow: 0 0 20px rgba(48, 220, 255, 0.36); |
||||
} |
||||
|
||||
.login-brand span { |
||||
font-size: 42px; |
||||
line-height: 1.2; |
||||
font-weight: 700; |
||||
} |
||||
|
||||
.login-brand small { |
||||
color: rgba(196, 243, 254, 0.72); |
||||
font-size: 13px; |
||||
letter-spacing: 0; |
||||
} |
||||
|
||||
.login-panel { |
||||
width: 360px; |
||||
padding: 30px 28px 28px; |
||||
border: 1px solid rgba(48, 220, 255, 0.34); |
||||
background: rgba(3, 14, 32, 0.82); |
||||
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.34), inset 0 0 28px rgba(48, 220, 255, 0.08); |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
.login-panel-title { |
||||
margin-bottom: 26px; |
||||
color: #c4f3fe; |
||||
font-size: 22px; |
||||
font-weight: 700; |
||||
text-align: center; |
||||
} |
||||
|
||||
.login-field { |
||||
display: block; |
||||
margin-bottom: 18px; |
||||
} |
||||
|
||||
.login-field span { |
||||
display: block; |
||||
margin-bottom: 8px; |
||||
color: rgba(196, 243, 254, 0.78); |
||||
font-size: 13px; |
||||
} |
||||
|
||||
.login-field input { |
||||
width: 100%; |
||||
height: 42px; |
||||
padding: 0 12px; |
||||
border: 1px solid rgba(48, 220, 255, 0.26); |
||||
outline: none; |
||||
background: rgba(0, 10, 24, 0.62); |
||||
color: #fff; |
||||
font-size: 15px; |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
.login-field input:focus { |
||||
border-color: rgba(48, 220, 255, 0.72); |
||||
box-shadow: 0 0 0 2px rgba(48, 220, 255, 0.12); |
||||
} |
||||
|
||||
.login-error { |
||||
min-height: 20px; |
||||
margin: -4px 0 12px; |
||||
color: #ffcf8a; |
||||
font-size: 13px; |
||||
line-height: 20px; |
||||
} |
||||
|
||||
.login-submit { |
||||
width: 100%; |
||||
height: 44px; |
||||
border: 1px solid rgba(48, 220, 255, 0.52); |
||||
background: linear-gradient(90deg, rgba(24, 140, 196, 0.92), rgba(37, 204, 218, 0.92)); |
||||
color: #fff; |
||||
cursor: pointer; |
||||
font-size: 16px; |
||||
font-weight: 700; |
||||
} |
||||
|
||||
.login-submit:disabled { |
||||
cursor: not-allowed; |
||||
opacity: 0.68; |
||||
} |
||||
|
||||
@media (max-width: 820px) { |
||||
.login-shell { |
||||
grid-template-columns: 1fr; |
||||
gap: 28px; |
||||
} |
||||
|
||||
.login-brand span { |
||||
font-size: 30px; |
||||
} |
||||
|
||||
.login-panel { |
||||
width: 100%; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,576 @@ |
||||
import { |
||||
estimatedStock, |
||||
grasslandYakRows, |
||||
homeCore, |
||||
townRecognitionRows, |
||||
tradeImpactRows, |
||||
} from "@/views/gdMap/livestockData" |
||||
|
||||
export const industryStageDefs = [ |
||||
{ key: "infrastructure", name: "基础设施", color: "#32dcff", unit: "项", metricLabel: "项目" }, |
||||
{ key: "breeding", name: "种养基地", color: "#24f6c4", unit: "个", metricLabel: "基地" }, |
||||
{ key: "support", name: "配套产业", color: "#ffe06b", unit: "个", metricLabel: "点位" }, |
||||
{ key: "processing", name: "加工基地", color: "#ff9a62", unit: "家", metricLabel: "企业" }, |
||||
{ key: "service", name: "社会化服务", color: "#9ea7ff", unit: "个", metricLabel: "主体" }, |
||||
{ key: "equipment", name: "机械设备", color: "#7ee269", unit: "套", metricLabel: "设备" }, |
||||
{ key: "vr", name: "产业VR", color: "#30dcff", unit: "项", metricLabel: "场景" }, |
||||
] |
||||
|
||||
export const supportPointRows = [ |
||||
{ |
||||
key: "support-disaster-001", |
||||
name: "嚷口防灾减灾饲草储备库", |
||||
stageKey: "support", |
||||
typeName: "防震减灾", |
||||
capacity: 1500, |
||||
capacityUnit: "吨", |
||||
town: "刷经寺镇", |
||||
address: "壤口村", |
||||
status: "正常运营", |
||||
products: "燕麦草、披碱草、老芒麦", |
||||
longitude: 102.489619, |
||||
latitude: 32.20577086, |
||||
}, |
||||
{ |
||||
key: "support-disaster-002", |
||||
name: "安曲防灾减灾饲草储备库", |
||||
stageKey: "support", |
||||
typeName: "防震减灾", |
||||
capacity: 1500, |
||||
capacityUnit: "吨", |
||||
town: "安曲镇", |
||||
address: "夺龙村", |
||||
status: "正常运营", |
||||
products: "燕麦草、披碱草、老芒麦", |
||||
longitude: 102.3283137, |
||||
latitude: 32.63727531, |
||||
}, |
||||
{ |
||||
key: "support-disaster-003", |
||||
name: "瓦切唐日合作社饲草储备库", |
||||
stageKey: "support", |
||||
typeName: "防震减灾", |
||||
capacity: 1500, |
||||
capacityUnit: "吨", |
||||
town: "瓦切镇", |
||||
address: "唐日村", |
||||
status: "正常运营", |
||||
products: "燕麦草、披碱草、老芒麦", |
||||
longitude: 102.6162349, |
||||
latitude: 33.11047864, |
||||
}, |
||||
{ |
||||
key: "support-disaster-004", |
||||
name: "瓦切牧草交易中心饲草储备库", |
||||
stageKey: "support", |
||||
typeName: "防震减灾", |
||||
capacity: 1500, |
||||
capacityUnit: "吨", |
||||
town: "瓦切镇", |
||||
address: "达俄村", |
||||
status: "正常运营", |
||||
products: "燕麦草、披碱草、老芒麦", |
||||
longitude: 102.6014834, |
||||
latitude: 33.09604707, |
||||
}, |
||||
{ |
||||
key: "support-disaster-005", |
||||
name: "龙日坝龙日坝村 1 饲草储备库", |
||||
stageKey: "support", |
||||
typeName: "防震减灾", |
||||
capacity: 1500, |
||||
capacityUnit: "吨", |
||||
town: "龙日镇", |
||||
address: "龙日坝村", |
||||
status: "正常运营", |
||||
products: "燕麦草、披碱草、老芒麦", |
||||
longitude: 102.383916, |
||||
latitude: 32.44992916, |
||||
}, |
||||
{ |
||||
key: "support-disaster-006", |
||||
name: "龙日坝龙日坝村 2 饲草储备库", |
||||
stageKey: "support", |
||||
typeName: "防震减灾", |
||||
capacity: 1500, |
||||
capacityUnit: "吨", |
||||
town: "龙日镇", |
||||
address: "龙日坝村", |
||||
status: "正常运营", |
||||
products: "燕麦草、披碱草、老芒麦", |
||||
longitude: 102.3834086, |
||||
latitude: 32.45041994, |
||||
}, |
||||
{ |
||||
key: "support-disaster-007", |
||||
name: "江茸乡饲草储备库", |
||||
stageKey: "support", |
||||
typeName: "防震减灾", |
||||
capacity: 1500, |
||||
capacityUnit: "吨", |
||||
town: "江茸乡", |
||||
address: "茸日玛村", |
||||
status: "正常运营", |
||||
products: "燕麦草、披碱草、老芒麦", |
||||
longitude: 102.256868, |
||||
latitude: 32.46996565, |
||||
}, |
||||
] |
||||
|
||||
export const processingPointRows = [ |
||||
{ |
||||
key: "support-processing-001", |
||||
name: "西部牦牛产业集团有限责任公司", |
||||
stageKey: "processing", |
||||
typeName: "乳制品加工", |
||||
capacity: 3000, |
||||
capacityUnit: "吨", |
||||
outputValue: 30000, |
||||
town: "邛溪镇", |
||||
address: "瑞庆西路37号", |
||||
status: "存续(在营)", |
||||
products: "牦牛奶粉", |
||||
longitude: 102.5292, |
||||
latitude: 32.7917, |
||||
}, |
||||
{ |
||||
key: "support-processing-002", |
||||
name: "四川红原菊乐牦牛乳业有限公司", |
||||
stageKey: "processing", |
||||
typeName: "乳制品加工", |
||||
capacity: 4000, |
||||
capacityUnit: "吨", |
||||
outputValue: 7000, |
||||
town: "邛溪镇", |
||||
address: "瑞庆东路76号附6号", |
||||
status: "存续(在营)", |
||||
products: "圣舟牦牛奶", |
||||
longitude: 102.5666, |
||||
latitude: 32.8179, |
||||
}, |
||||
{ |
||||
key: "support-processing-003", |
||||
name: "四川雅克乳业有限公司", |
||||
stageKey: "processing", |
||||
typeName: "乳制品加工", |
||||
capacity: 1000, |
||||
capacityUnit: "吨", |
||||
outputValue: 3000, |
||||
town: "邛溪镇", |
||||
address: "瑞庆西路", |
||||
status: "存续(在营)", |
||||
products: "牦牛酸奶、牦牛奶粉", |
||||
longitude: 102.5315, |
||||
latitude: 32.7921, |
||||
}, |
||||
{ |
||||
key: "support-processing-004", |
||||
name: "四川红原遛遛牛食品有限责任公司", |
||||
stageKey: "processing", |
||||
typeName: "肉制品加工", |
||||
capacity: 4500, |
||||
capacityUnit: "吨", |
||||
outputValue: 8000, |
||||
town: "邛溪镇", |
||||
address: "瑞庆东路72号", |
||||
status: "存续(在营)", |
||||
products: "风干牦牛肉、卤制牦牛肉", |
||||
longitude: 102.5592, |
||||
latitude: 32.8151, |
||||
}, |
||||
{ |
||||
key: "support-processing-005", |
||||
name: "四川红原黑帐篷牦牛肉食品有限责任公司", |
||||
stageKey: "processing", |
||||
typeName: "肉制品加工", |
||||
capacity: 300, |
||||
capacityUnit: "吨", |
||||
outputValue: 1000, |
||||
town: "邛溪镇", |
||||
address: "瑞庆中路", |
||||
status: "存续(在营)", |
||||
products: "风干牦牛肉、牦牛肉礼包", |
||||
longitude: 102.551, |
||||
latitude: 32.7987, |
||||
}, |
||||
{ |
||||
key: "support-processing-006", |
||||
name: "红原御坊斋牦牛肉制品厂", |
||||
stageKey: "processing", |
||||
typeName: "肉制品加工", |
||||
capacity: 200, |
||||
capacityUnit: "吨", |
||||
outputValue: 600, |
||||
town: "邛溪镇", |
||||
address: "阳嘎北街", |
||||
status: "存续(在营)", |
||||
products: "牛肉干、牦牛肉粒", |
||||
longitude: 102.5431, |
||||
latitude: 32.7962, |
||||
}, |
||||
{ |
||||
key: "support-processing-007", |
||||
name: "四川牦牛火了生态农业科技有限公司", |
||||
stageKey: "processing", |
||||
typeName: "肉制品加工", |
||||
capacity: 0, |
||||
capacityUnit: "吨", |
||||
outputValue: 500, |
||||
town: "邛溪镇", |
||||
address: "瑞庆中路13号附1号", |
||||
status: "存续(在营)", |
||||
products: "包装牦牛肉干", |
||||
longitude: 102.5402, |
||||
latitude: 32.7958, |
||||
}, |
||||
] |
||||
|
||||
export const industryPointRows = [...supportPointRows, ...processingPointRows] |
||||
|
||||
const infrastructureProjectDistribution = [3, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1] |
||||
const serviceSubjectTotal = 2618 |
||||
const tradeFlowTotal = 220605 |
||||
const equipmentTotal = townRecognitionRows.reduce((sum, item) => sum + Number(item.devices || 0), 0) |
||||
const supportCapacity = supportPointRows.reduce((sum, item) => sum + Number(item.capacity || 0), 0) |
||||
const processingCapacity = processingPointRows.reduce((sum, item) => sum + Number(item.capacity || 0), 0) |
||||
const processingOutputValue = processingPointRows.reduce((sum, item) => sum + Number(item.outputValue || 0), 0) |
||||
const allTownCount = new Set(grasslandYakRows.map((item) => item.zoneName)).size |
||||
const carrierTotal = homeCore.projectCount + homeCore.baseCount + industryPointRows.length + serviceSubjectTotal + equipmentTotal |
||||
|
||||
export const industryTopStats = [ |
||||
{ zh: "牦牛存栏估算", name: "牦牛存栏估算", value: estimatedStock, unit: "头" }, |
||||
{ zh: "全链载体", name: "全链载体", value: carrierTotal, unit: "个/套" }, |
||||
{ zh: "加工储备能力", name: "加工储备能力", value: supportCapacity + processingCapacity, unit: "吨" }, |
||||
{ zh: "覆盖乡镇", name: "覆盖乡镇", value: allTownCount, unit: "个" }, |
||||
] |
||||
|
||||
export const industryOverviewRows = [ |
||||
{ key: "chain-node", name: "产业节点", value: industryStageDefs.length, unit: "类", icon: "town" }, |
||||
{ key: "chain-carrier", name: "主体点位", value: carrierTotal, unit: "个/套", icon: "yield" }, |
||||
{ key: "chain-capacity", name: "加工储备", value: supportCapacity + processingCapacity, unit: "吨", icon: "stack" }, |
||||
{ key: "chain-town", name: "镇域覆盖", value: allTownCount, unit: "个", icon: "area" }, |
||||
] |
||||
|
||||
export const industryStageRows = [ |
||||
{ |
||||
key: "infrastructure", |
||||
name: "基础设施", |
||||
value: homeCore.projectCount, |
||||
unit: "项", |
||||
capacity: homeCore.projectCount, |
||||
capacityUnit: "项", |
||||
note: "产业道路、工程与配套项目", |
||||
}, |
||||
{ |
||||
key: "breeding", |
||||
name: "种养基地", |
||||
value: homeCore.baseCount, |
||||
unit: "个", |
||||
capacity: estimatedStock, |
||||
capacityUnit: "头", |
||||
note: "基地、牧场与存栏承载", |
||||
}, |
||||
{ |
||||
key: "support", |
||||
name: "配套产业", |
||||
value: supportPointRows.length, |
||||
unit: "个", |
||||
capacity: supportCapacity, |
||||
capacityUnit: "吨", |
||||
note: "饲草储备与防灾保障", |
||||
}, |
||||
{ |
||||
key: "processing", |
||||
name: "加工基地", |
||||
value: processingPointRows.length, |
||||
unit: "家", |
||||
capacity: processingCapacity, |
||||
capacityUnit: "吨", |
||||
note: "乳品、肉品加工转化", |
||||
}, |
||||
{ |
||||
key: "service", |
||||
name: "社会化服务", |
||||
value: serviceSubjectTotal, |
||||
unit: "个", |
||||
capacity: tradeFlowTotal, |
||||
capacityUnit: "头", |
||||
note: "保险、交易、检疫与流通", |
||||
}, |
||||
{ |
||||
key: "equipment", |
||||
name: "机械设备", |
||||
value: equipmentTotal, |
||||
unit: "套", |
||||
capacity: equipmentTotal, |
||||
capacityUnit: "套", |
||||
note: "识别、监测与生产装备", |
||||
}, |
||||
{ |
||||
key: "vr", |
||||
name: "产业VR", |
||||
value: 1, |
||||
unit: "项", |
||||
capacity: 6, |
||||
capacityUnit: "类", |
||||
note: "产业链沉浸式展示", |
||||
}, |
||||
] |
||||
|
||||
export const chainFlowRows = [ |
||||
{ key: "resource", name: "草地供给", value: 84, unit: "%", extra: "资源底座" }, |
||||
{ key: "breeding", name: "基地养殖", value: 88, unit: "%", extra: "存栏校准" }, |
||||
{ key: "health", name: "防疫保障", value: 92, unit: "%", extra: "台账接入" }, |
||||
{ key: "trade", name: "交易流通", value: 78, unit: "%", extra: "检疫出证" }, |
||||
{ key: "processing", name: "加工转化", value: 81, unit: "%", extra: "乳肉加工" }, |
||||
{ key: "market", name: "品牌销售", value: 73, unit: "%", extra: "外销拓展" }, |
||||
] |
||||
|
||||
export const stageHealthRows = { |
||||
infrastructure: [ |
||||
{ key: "project-online", name: "项目入库", value: 95, unit: "%", extra: "95%" }, |
||||
{ key: "facility-cover", name: "设施覆盖", value: 82, unit: "%", extra: "82%" }, |
||||
{ key: "fund-track", name: "资金跟踪", value: 76, unit: "%", extra: "76%" }, |
||||
{ key: "maintenance", name: "运维闭环", value: 68, unit: "%", extra: "68%" }, |
||||
], |
||||
breeding: [ |
||||
{ key: "base-online", name: "基地在线", value: 92, unit: "%", extra: "92%" }, |
||||
{ key: "stock-match", name: "存栏匹配", value: 88, unit: "%", extra: "88%" }, |
||||
{ key: "pasture-link", name: "草场关联", value: 74, unit: "%", extra: "74%" }, |
||||
{ key: "source-trace", name: "源头追溯", value: 69, unit: "%", extra: "69%" }, |
||||
], |
||||
support: [ |
||||
{ key: "storage-ready", name: "储备达标", value: 91, unit: "%", extra: "91%" }, |
||||
{ key: "town-cover", name: "镇域覆盖", value: 64, unit: "%", extra: "64%" }, |
||||
{ key: "dispatch", name: "调拨能力", value: 83, unit: "%", extra: "83%" }, |
||||
{ key: "risk", name: "防灾韧性", value: 79, unit: "%", extra: "79%" }, |
||||
], |
||||
processing: [ |
||||
{ key: "license", name: "许可状态", value: 86, unit: "%", extra: "86%" }, |
||||
{ key: "capacity", name: "产能利用", value: 77, unit: "%", extra: "77%" }, |
||||
{ key: "cold-chain", name: "冷链能力", value: 72, unit: "%", extra: "72%" }, |
||||
{ key: "farmer-drive", name: "带动牧户", value: 84, unit: "%", extra: "84%" }, |
||||
], |
||||
service: [ |
||||
{ key: "insurance", name: "保险服务", value: 89, unit: "%", extra: "89%" }, |
||||
{ key: "quarantine", name: "检疫出证", value: 92, unit: "%", extra: "92%" }, |
||||
{ key: "trade", name: "交易撮合", value: 78, unit: "%", extra: "78%" }, |
||||
{ key: "supply-demand", name: "供需明细", value: 81, unit: "%", extra: "81%" }, |
||||
], |
||||
equipment: [ |
||||
{ key: "drone", name: "无人机识别", value: 86, unit: "%", extra: "86%" }, |
||||
{ key: "iot", name: "物联感知", value: 79, unit: "%", extra: "79%" }, |
||||
{ key: "machine", name: "生产装备", value: 68, unit: "%", extra: "68%" }, |
||||
{ key: "offline", name: "在线巡检", value: 74, unit: "%", extra: "74%" }, |
||||
], |
||||
vr: [ |
||||
{ key: "resource-scene", name: "资源场景", value: 88, unit: "%", extra: "88%" }, |
||||
{ key: "base-scene", name: "基地场景", value: 82, unit: "%", extra: "82%" }, |
||||
{ key: "processing-scene", name: "加工场景", value: 76, unit: "%", extra: "76%" }, |
||||
{ key: "display-scene", name: "展陈联动", value: 72, unit: "%", extra: "72%" }, |
||||
], |
||||
} |
||||
|
||||
export function getStageDef(key) { |
||||
return industryStageDefs.find((item) => item.key === key) || industryStageDefs[0] |
||||
} |
||||
|
||||
export function getStageRow(key) { |
||||
return industryStageRows.find((item) => item.key === key) || industryStageRows[0] |
||||
} |
||||
|
||||
export function getStageCapacityRows(stageKey) { |
||||
if (stageKey === "support") { |
||||
return aggregateRowsByField(supportPointRows, "town", "capacity", "吨") |
||||
} |
||||
if (stageKey === "processing") { |
||||
return processingPointRows |
||||
.map((item) => ({ |
||||
key: item.key, |
||||
name: item.name.replace("四川红原", "").replace("有限责任公司", ""), |
||||
value: item.capacity, |
||||
unit: item.capacityUnit, |
||||
extra: item.typeName, |
||||
})) |
||||
.sort((a, b) => b.value - a.value) |
||||
} |
||||
if (stageKey === "breeding") { |
||||
return grasslandYakRows |
||||
.map((item) => ({ |
||||
key: item.zoneName, |
||||
name: item.zoneName, |
||||
value: item.yakCount, |
||||
unit: "头", |
||||
extra: `${formatWanMu(item.grasslandArea)}万亩`, |
||||
})) |
||||
.sort((a, b) => b.value - a.value) |
||||
.slice(0, 7) |
||||
} |
||||
if (stageKey === "equipment") { |
||||
return townRecognitionRows |
||||
.map((item) => ({ key: item.name, name: item.name, value: item.devices, unit: "套", extra: `${item.count}头` })) |
||||
.sort((a, b) => b.value - a.value) |
||||
.slice(0, 7) |
||||
} |
||||
if (stageKey === "service") { |
||||
return tradeImpactRows.map((item) => ({ |
||||
key: item.label, |
||||
name: item.label, |
||||
value: item.value, |
||||
unit: "头", |
||||
extra: `${item.yoy > 0 ? "+" : ""}${item.yoy}%`, |
||||
})) |
||||
} |
||||
if (stageKey === "vr") { |
||||
return chainFlowRows.map((item) => ({ |
||||
key: item.key, |
||||
name: item.name, |
||||
value: item.value, |
||||
unit: item.unit, |
||||
extra: item.extra, |
||||
})) |
||||
} |
||||
return getInfrastructureTownRows().slice(0, 7) |
||||
} |
||||
|
||||
export function getStageTownRows(stageKey) { |
||||
if (stageKey === "support") { |
||||
return aggregateRowsByField(supportPointRows, "town", "capacity", "吨") |
||||
} |
||||
if (stageKey === "processing") { |
||||
return aggregateRowsByField(processingPointRows, "town", "capacity", "吨") |
||||
} |
||||
if (stageKey === "equipment") { |
||||
return townRecognitionRows.map((item) => ({ |
||||
key: item.name, |
||||
name: item.name, |
||||
value: item.devices, |
||||
unit: "套", |
||||
})) |
||||
} |
||||
if (stageKey === "service") { |
||||
const totalYak = grasslandYakRows.reduce((sum, item) => sum + Number(item.yakCount || 0), 0) || 1 |
||||
return grasslandYakRows |
||||
.map((item) => ({ |
||||
key: item.zoneName, |
||||
name: item.zoneName, |
||||
value: Math.max(12, Math.round((item.yakCount / totalYak) * serviceSubjectTotal)), |
||||
unit: "个", |
||||
extra: "服务主体", |
||||
})) |
||||
.sort((a, b) => b.value - a.value) |
||||
} |
||||
if (stageKey === "vr") { |
||||
return [ |
||||
{ key: "vr-resource", name: "资源底图", value: 6, unit: "类", extra: "三维展示" }, |
||||
{ key: "vr-base", name: "种养基地", value: homeCore.baseCount, unit: "个", extra: "基地漫游" }, |
||||
{ key: "vr-processing", name: "加工企业", value: processingPointRows.length, unit: "家", extra: "工艺展示" }, |
||||
{ key: "vr-service", name: "服务链路", value: 4, unit: "类", extra: "业务联动" }, |
||||
] |
||||
} |
||||
if (stageKey === "infrastructure") return getInfrastructureTownRows() |
||||
return grasslandYakRows |
||||
.map((item) => ({ key: item.zoneName, name: item.zoneName, value: item.yakCount, unit: "头" })) |
||||
.sort((a, b) => b.value - a.value) |
||||
} |
||||
|
||||
export function getStageDetailRows(stageKey) { |
||||
if (stageKey === "support") return supportPointRows.map(toDetailRow) |
||||
if (stageKey === "processing") return processingPointRows.map(toDetailRow) |
||||
if (stageKey === "breeding") { |
||||
return grasslandYakRows.map((item, index) => ({ |
||||
key: `base-${index + 1}`, |
||||
name: `${item.zoneName}生态种养基地`, |
||||
stageKey, |
||||
typeName: "种养基地", |
||||
town: item.zoneName, |
||||
capacity: item.yakCount, |
||||
capacityUnit: "头", |
||||
status: "在线", |
||||
products: `${formatWanMu(item.grasslandArea)}万亩草地`, |
||||
})) |
||||
} |
||||
if (stageKey === "equipment") { |
||||
return townRecognitionRows.map((item, index) => ({ |
||||
key: `equipment-${index + 1}`, |
||||
name: `${item.name}识别监测设备`, |
||||
stageKey, |
||||
typeName: "机械设备", |
||||
town: item.name, |
||||
capacity: item.devices, |
||||
capacityUnit: "套", |
||||
status: "在线", |
||||
products: `${item.count}头识别样本`, |
||||
})) |
||||
} |
||||
if (stageKey === "service") { |
||||
return [ |
||||
{ key: "service-insurance", name: "牦牛保险服务", stageKey, typeName: "保险", town: "全县", capacity: serviceSubjectTotal, capacityUnit: "户", status: "运行中", products: "参保、理赔、风险事项" }, |
||||
{ key: "service-quarantine", name: "检疫出证流通服务", stageKey, typeName: "检疫", town: "全县", capacity: 9140, capacityUnit: "条", status: "运行中", products: "A证/B证流通明细" }, |
||||
{ key: "service-trade", name: "活畜交易撮合服务", stageKey, typeName: "交易", town: "全县", capacity: tradeFlowTotal, capacityUnit: "头", status: "运行中", products: "交易总览、供需明细" }, |
||||
{ key: "service-trace", name: "质量追溯与品牌服务", stageKey, typeName: "追溯", town: "全县", capacity: 6, capacityUnit: "类", status: "建设中", products: "批次、检疫、加工、销售" }, |
||||
] |
||||
} |
||||
if (stageKey === "vr") { |
||||
return [ |
||||
{ key: "vr-resource", name: "林草湿资源 VR 场景", stageKey, typeName: "产业VR", town: "全县", capacity: 6, capacityUnit: "类", status: "可展示", products: "草地、湿地、林地、牧场" }, |
||||
{ key: "vr-base", name: "种养基地 VR 场景", stageKey, typeName: "产业VR", town: "全县", capacity: homeCore.baseCount, capacityUnit: "个", status: "可展示", products: "基地、物联感知、虫情监测" }, |
||||
{ key: "vr-processing", name: "加工基地 VR 场景", stageKey, typeName: "产业VR", town: "邛溪镇", capacity: processingPointRows.length, capacityUnit: "家", status: "可展示", products: "乳品加工、肉品加工" }, |
||||
{ key: "vr-chain", name: "全链路 VR 展陈", stageKey, typeName: "产业VR", town: "全县", capacity: industryStageDefs.length, capacityUnit: "段", status: "建设中", products: "生产、加工、流通、品牌" }, |
||||
] |
||||
} |
||||
return getInfrastructureTownRows().map((item, index) => ({ |
||||
key: `infra-${index + 1}`, |
||||
name: `${item.name}产业基础设施项目`, |
||||
stageKey, |
||||
typeName: "基础设施", |
||||
town: item.name, |
||||
capacity: item.value, |
||||
capacityUnit: item.unit, |
||||
status: "入库", |
||||
products: item.extra, |
||||
})) |
||||
} |
||||
|
||||
function toDetailRow(item) { |
||||
return { |
||||
...item, |
||||
value: Number(item.capacity || 0), |
||||
unit: item.capacityUnit, |
||||
} |
||||
} |
||||
|
||||
function getInfrastructureTownRows() { |
||||
return grasslandYakRows.map((item, index) => ({ |
||||
key: item.zoneName, |
||||
name: item.zoneName, |
||||
value: infrastructureProjectDistribution[index] || 1, |
||||
unit: "项", |
||||
extra: `${formatWanMu(item.grasslandArea)}万亩`, |
||||
})) |
||||
} |
||||
|
||||
function aggregateRowsByField(rows, nameField, valueField, unit) { |
||||
const map = new Map() |
||||
rows.forEach((item) => { |
||||
const name = item[nameField] || "未填写" |
||||
const current = map.get(name) || { key: name, name, value: 0, count: 0, unit, extra: "" } |
||||
current.value += Number(item[valueField] || 0) |
||||
current.count += 1 |
||||
current.extra = `${current.count}个` |
||||
map.set(name, current) |
||||
}) |
||||
return [...map.values()].sort((a, b) => b.value - a.value || b.count - a.count) |
||||
} |
||||
|
||||
function formatWanMu(value) { |
||||
const number = Number(value || 0) / 10000 |
||||
return Number(number.toFixed(1)) |
||||
} |
||||
|
||||
export const industryFinancialRows = [ |
||||
{ key: "support-capacity", name: "饲草储备能力", value: supportCapacity, unit: "吨", extra: `${supportPointRows.length}个点位` }, |
||||
{ key: "processing-capacity", name: "加工转化能力", value: processingCapacity, unit: "吨", extra: `${processingPointRows.length}家企业` }, |
||||
{ key: "processing-output", name: "加工产值", value: processingOutputValue, unit: "万元", extra: "乳品/肉品" }, |
||||
{ key: "trade-flow", name: "检疫流通头数", value: tradeFlowTotal, unit: "头", extra: "供需明细" }, |
||||
] |
||||
@ -0,0 +1,392 @@ |
||||
<template> |
||||
<div class="yak-industry-chain"> |
||||
<map-scene |
||||
ref="mapSceneRef" |
||||
class="industry-map-scene" |
||||
:active-layer="industryMapLayer" |
||||
:map-ranking="mapRanking" |
||||
/> |
||||
<div class="industry-map-scrim"></div> |
||||
<div v-if="!mapReady" class="industry-map-loading">3D地图加载中</div> |
||||
|
||||
<div class="industry-left industry-side-stack"> |
||||
<m-card class="industry-card left-card" title="全链核心指标" :width="sideCardWidth" :height="sideCardHeight"> |
||||
<business-panel-renderer :panel="overviewPanel" compact /> |
||||
</m-card> |
||||
|
||||
<m-card class="industry-card left-card" title="产业节点结构" :width="sideCardWidth" :height="sideCardHeight"> |
||||
<business-panel-renderer :panel="stageStructurePanel" compact /> |
||||
</m-card> |
||||
|
||||
<m-card class="industry-card left-card" title="链路协同指数" :width="sideCardWidth" :height="sideCardHeight"> |
||||
<business-panel-renderer :panel="chainFlowPanel" compact /> |
||||
</m-card> |
||||
|
||||
<m-card class="industry-card left-card" :title="`${activeStage.name}能力排行`" :width="sideCardWidth" :height="sideCardHeight"> |
||||
<business-panel-renderer :panel="capacityRankPanel" compact /> |
||||
</m-card> |
||||
</div> |
||||
|
||||
<div class="industry-right industry-side-stack"> |
||||
<m-card class="industry-card right-card" title="加工储备产能" :width="sideCardWidth" :height="sideCardHeight"> |
||||
<business-panel-renderer :panel="financialPanel" compact /> |
||||
</m-card> |
||||
|
||||
<m-card class="industry-card right-card" :title="`${activeStage.name}镇域覆盖`" :width="sideCardWidth" :height="sideCardHeight"> |
||||
<business-panel-renderer :panel="townCoveragePanel" compact /> |
||||
</m-card> |
||||
|
||||
<m-card class="industry-card right-card" :title="`${activeStage.name}运行画像`" :width="sideCardWidth" :height="sideCardHeight"> |
||||
<business-panel-renderer :panel="stageHealthPanel" compact /> |
||||
</m-card> |
||||
|
||||
<m-card class="industry-card right-card industry-list-card" :title="`${activeStage.name}明细`" :width="sideCardWidth" :height="sideCardHeight"> |
||||
<business-panel-renderer :panel="detailTablePanel" compact @row-select="handleDetailSelect" /> |
||||
</m-card> |
||||
</div> |
||||
|
||||
<div class="industry-center"> |
||||
<div class="industry-chain-title"> |
||||
<span>牦牛全产业链</span> |
||||
<strong>{{ activeStage.name }}</strong> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="industry-bottom-switcher"> |
||||
<topic-switcher |
||||
:model-value="activeStageKey" |
||||
:topics="stageSwitcherTopics" |
||||
@update:model-value="handleStageSelect" |
||||
/> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup> |
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue" |
||||
import mCard from "@/components/mCard/index.vue" |
||||
import MapScene from "@/views/forestGrassWet/map.vue" |
||||
import BusinessPanelRenderer from "@/views/forestGrassWet/components/BusinessPanelRenderer.vue" |
||||
import TopicSwitcher from "@/views/forestGrassWet/components/TopicSwitcher.vue" |
||||
import { Assets } from "@/views/forestGrassWet/assets" |
||||
import { getLayerByKey } from "@/config/layers" |
||||
import { |
||||
chainFlowRows, |
||||
getStageCapacityRows, |
||||
getStageDef, |
||||
getStageDetailRows, |
||||
getStageTownRows, |
||||
industryFinancialRows, |
||||
industryOverviewRows, |
||||
industryStageDefs, |
||||
industryStageRows, |
||||
industryTopStats, |
||||
stageHealthRows, |
||||
} from "./data" |
||||
|
||||
const emit = defineEmits(["module-stats", "loading-progress", "map-ready"]) |
||||
const sideCardWidth = 398 |
||||
const sideCardHeight = 190 |
||||
const activeStageKey = ref("support") |
||||
const mapSceneRef = ref(null) |
||||
const mapReady = ref(false) |
||||
const industryMapLayer = getLayerByKey("grassland", "grassland-distribution") |
||||
let mapAssets = null |
||||
let destroyed = false |
||||
|
||||
const activeStage = computed(() => getStageDef(activeStageKey.value)) |
||||
const capacityRows = computed(() => getStageCapacityRows(activeStageKey.value)) |
||||
const townRows = computed(() => getStageTownRows(activeStageKey.value)) |
||||
const detailRows = computed(() => getStageDetailRows(activeStageKey.value)) |
||||
const stageSwitcherTopics = computed(() => |
||||
industryStageDefs.map((stage) => ({ |
||||
key: stage.key, |
||||
name: stage.name, |
||||
color: stage.color, |
||||
})) |
||||
) |
||||
const mapRanking = computed(() => ({ |
||||
title: `${activeStage.value.name}排行`, |
||||
metricLabel: activeStage.value.metricLabel, |
||||
unit: townRows.value[0]?.unit || activeStage.value.unit, |
||||
rows: townRows.value.slice(0, 8).map((item) => ({ |
||||
name: item.name, |
||||
value: Number(item.value) || 0, |
||||
unit: item.unit || activeStage.value.unit, |
||||
})), |
||||
})) |
||||
|
||||
const overviewPanel = computed(() => ({ |
||||
key: "industry-overview", |
||||
type: "iconMetric", |
||||
rows: industryOverviewRows, |
||||
})) |
||||
const stageStructurePanel = computed(() => ({ |
||||
key: "industry-stage-structure", |
||||
type: "bar", |
||||
variant: "distribution", |
||||
rows: industryStageRows.map((item) => ({ |
||||
...item, |
||||
extra: item.note, |
||||
})), |
||||
})) |
||||
const chainFlowPanel = computed(() => ({ |
||||
key: "industry-chain-flow", |
||||
type: "line", |
||||
rows: chainFlowRows, |
||||
})) |
||||
const capacityRankPanel = computed(() => ({ |
||||
key: `industry-capacity-${activeStageKey.value}`, |
||||
type: "bar", |
||||
variant: "distribution", |
||||
rows: capacityRows.value.slice(0, 7), |
||||
})) |
||||
const financialPanel = computed(() => ({ |
||||
key: "industry-financial", |
||||
type: "column", |
||||
variant: "slim", |
||||
rows: industryFinancialRows, |
||||
})) |
||||
const townCoveragePanel = computed(() => ({ |
||||
key: `industry-town-${activeStageKey.value}`, |
||||
type: "bar", |
||||
variant: "distribution", |
||||
rows: townRows.value.slice(0, 7), |
||||
})) |
||||
const stageHealthPanel = computed(() => ({ |
||||
key: `industry-health-${activeStageKey.value}`, |
||||
type: "heat", |
||||
rows: stageHealthRows[activeStageKey.value] || [], |
||||
})) |
||||
const detailTablePanel = computed(() => ({ |
||||
key: `industry-detail-${activeStageKey.value}`, |
||||
type: "projectTable", |
||||
searchPlaceholder: "搜索主体 / 乡镇", |
||||
emptyText: "无匹配主体", |
||||
searchKeys: ["name", "typeName", "town", "address", "products", "status"], |
||||
columns: [ |
||||
{ key: "name", label: "主体名称", field: "name", width: "34%" }, |
||||
{ key: "type", label: "类型", field: "typeName", width: "18%" }, |
||||
{ key: "town", label: "乡镇", field: "town", width: "16%" }, |
||||
{ key: "capacity", label: "能力", field: "capacity", width: "18%", align: "right", strong: true, format: "number", unitField: "capacityUnit" }, |
||||
{ key: "status", label: "状态", field: "status", width: "14%" }, |
||||
], |
||||
rows: detailRows.value, |
||||
})) |
||||
|
||||
onMounted(() => { |
||||
emit("module-stats", { |
||||
moduleKey: "yak-industry-chain", |
||||
items: industryTopStats, |
||||
}) |
||||
initIndustryMap() |
||||
}) |
||||
|
||||
onBeforeUnmount(() => { |
||||
destroyed = true |
||||
mapSceneRef.value?.destroyMap?.() |
||||
}) |
||||
|
||||
function handleStageSelect(key) { |
||||
activeStageKey.value = key |
||||
} |
||||
|
||||
function initIndustryMap() { |
||||
mapAssets = new Assets() |
||||
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, |
||||
}) |
||||
}) |
||||
const loadMapOnce = async (attempt = 0) => { |
||||
if (destroyed) return |
||||
await nextTick() |
||||
const scene = mapSceneRef.value |
||||
scene?.loadMap?.(mapAssets) |
||||
if (scene?.canvasMap) { |
||||
mapReady.value = true |
||||
emit("loading-progress", { progress: 100 }) |
||||
emit("map-ready") |
||||
scene.play?.() |
||||
return |
||||
} |
||||
if (attempt < 24) { |
||||
window.setTimeout(() => loadMapOnce(attempt + 1), 120) |
||||
return |
||||
} |
||||
mapReady.value = true |
||||
emit("map-ready") |
||||
} |
||||
mapAssets.ready?.then(loadMapOnce).catch(() => { |
||||
mapReady.value = true |
||||
emit("map-ready") |
||||
}) |
||||
} |
||||
|
||||
function handleDetailSelect(row) { |
||||
const lng = Number(row?.longitude) |
||||
const lat = Number(row?.latitude) |
||||
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return |
||||
const canvasMap = mapSceneRef.value?.canvasMap?.value || mapSceneRef.value?.canvasMap |
||||
canvasMap?.flyToLngLat?.([lng, lat], { |
||||
duration: 0.72, |
||||
statusText: "已定位产业点位", |
||||
}) |
||||
} |
||||
|
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
.yak-industry-chain { |
||||
position: absolute; |
||||
inset: 0; |
||||
z-index: 1; |
||||
overflow: hidden; |
||||
color: #fff; |
||||
background: #03121c; |
||||
} |
||||
|
||||
.industry-map-scene { |
||||
position: absolute; |
||||
inset: 0; |
||||
z-index: 1; |
||||
} |
||||
|
||||
.industry-map-scrim { |
||||
position: absolute; |
||||
inset: 0; |
||||
z-index: 2; |
||||
pointer-events: none; |
||||
background: |
||||
radial-gradient(circle at 50% 48%, rgba(4, 18, 28, 0.02) 0%, rgba(4, 18, 28, 0.08) 37%, rgba(2, 10, 17, 0.45) 78%, rgba(1, 7, 12, 0.72) 100%), |
||||
linear-gradient(90deg, rgba(0, 12, 20, 0.42), transparent 26%, transparent 74%, rgba(0, 12, 20, 0.42)); |
||||
} |
||||
|
||||
.industry-map-loading { |
||||
position: absolute; |
||||
left: 50%; |
||||
top: 50%; |
||||
z-index: 4; |
||||
transform: translate(-50%, -50%); |
||||
color: rgba(220, 250, 255, 0.78); |
||||
font-size: 18px; |
||||
} |
||||
|
||||
.industry-side-stack { |
||||
position: absolute; |
||||
top: 176px; |
||||
z-index: 5; |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 12px; |
||||
width: 398px; |
||||
pointer-events: all; |
||||
} |
||||
|
||||
.industry-left { |
||||
left: 32px; |
||||
transform: perspective(900px) rotateY(4deg); |
||||
} |
||||
|
||||
.industry-right { |
||||
right: 32px; |
||||
transform: perspective(900px) rotateY(-4deg); |
||||
} |
||||
|
||||
.industry-card { |
||||
flex: 0 0 auto; |
||||
|
||||
.m-card-bd-content { |
||||
top: 40px; |
||||
bottom: 8px; |
||||
color: #f3feff; |
||||
} |
||||
|
||||
.m-card-bd-bg { |
||||
filter: brightness(1.08) saturate(1.05); |
||||
} |
||||
|
||||
.m-card-bd-bg::after { |
||||
content: ""; |
||||
position: absolute; |
||||
inset: 4px 5px 6px; |
||||
z-index: 0; |
||||
pointer-events: none; |
||||
border: 1px solid rgba(48, 220, 255, 0.14); |
||||
background: |
||||
linear-gradient(180deg, rgba(15, 69, 88, 0.16), rgba(6, 28, 43, 0.13)), |
||||
linear-gradient(90deg, rgba(48, 220, 255, 0.09), transparent 42%, rgba(255, 224, 107, 0.05)); |
||||
box-shadow: |
||||
inset 0 0 0 1px rgba(48, 220, 255, 0.05), |
||||
inset 0 12px 28px rgba(48, 220, 255, 0.04); |
||||
} |
||||
} |
||||
|
||||
.yak-industry-chain .m-card-hd-title { |
||||
color: #ffffff !important; |
||||
background: none !important; |
||||
-webkit-text-fill-color: #ffffff !important; |
||||
text-shadow: none !important; |
||||
} |
||||
|
||||
.industry-center { |
||||
position: absolute; |
||||
left: 50%; |
||||
top: 690px; |
||||
z-index: 4; |
||||
width: 820px; |
||||
transform: translateX(-50%); |
||||
pointer-events: none; |
||||
} |
||||
|
||||
.industry-chain-title { |
||||
display: flex; |
||||
align-items: baseline; |
||||
justify-content: center; |
||||
gap: 14px; |
||||
margin-bottom: 13px; |
||||
text-shadow: 0 0 18px rgba(48, 220, 255, 0.28); |
||||
|
||||
span { |
||||
color: rgba(224, 252, 255, 0.9); |
||||
font-size: 20px; |
||||
font-weight: 700; |
||||
} |
||||
|
||||
strong { |
||||
color: #30dcff; |
||||
font-size: 32px; |
||||
font-family: AlimamaShuHeiTi, AlibabaPuHuiTi, sans-serif; |
||||
letter-spacing: 0; |
||||
} |
||||
} |
||||
|
||||
.industry-bottom-switcher { |
||||
position: absolute; |
||||
left: 50%; |
||||
bottom: 25px; |
||||
z-index: 8; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
transform: translateX(-50%); |
||||
pointer-events: all; |
||||
|
||||
.topic-switcher { |
||||
width: 940px; |
||||
} |
||||
|
||||
.topic-switcher-item { |
||||
width: 112px; |
||||
} |
||||
|
||||
.topic-switcher-item .topic-name { |
||||
font-size: 15px; |
||||
} |
||||
} |
||||
</style> |
||||