400 lines
23 KiB
JavaScript
400 lines
23 KiB
JavaScript
// artboard-overview.jsx — Asset-management overview
|
||
// Filter by: 资产状态 / 部门 / 归属
|
||
// Card shows: 车牌 + VIN + 城市 + 部门 + 客户 + 资产状态
|
||
// Detail shows: 资产档案 + 业务关系 + 实时车况 + 保养预警 (no driver)
|
||
|
||
const AssetStatusChip = ({ status }) => {
|
||
const map = {
|
||
in_stock: { label: "在库", bg: "var(--accent-soft)", fg: "var(--accent)", dot: "ok" },
|
||
leasing: { label: "租赁" , bg: "rgba(46,140,140,0.15)",fg: "var(--info)", dot: "info" },
|
||
abnormal: { label: "异常", bg: "var(--danger-soft)", fg: "var(--danger)", dot: "danger" },
|
||
};
|
||
const m = map[status] || map.in_stock;
|
||
return (
|
||
<span className="chip" style={{background:m.bg, color:m.fg, border:"1px solid " + m.fg + "33", fontSize:10, padding:"2px 7px"}}>
|
||
<span className={"dot " + m.dot} style={{width:5, height:5}}/> {m.label}
|
||
</span>
|
||
);
|
||
};
|
||
|
||
const OwnChip = ({ own }) => (
|
||
<span className="chip" style={{
|
||
fontSize:10, padding:"1px 6px",
|
||
background: own === "self" ? "rgba(31,139,76,0.10)" : "rgba(122,140,46,0.12)",
|
||
color: own === "self" ? "var(--accent)" : "#7A8C2E",
|
||
border: "1px solid " + (own === "self" ? "rgba(31,139,76,0.25)" : "rgba(122,140,46,0.25)"),
|
||
}}>{own === "self" ? "自有" : "外租"}</span>
|
||
);
|
||
|
||
const DeptDot = ({ dept }) => {
|
||
const d = (window.DEPARTMENTS || []).find(x => x.id === dept);
|
||
if (!d) return null;
|
||
return (
|
||
<span style={{display:"inline-flex", alignItems:"center", gap:4, fontSize:11}}>
|
||
<span style={{width:6, height:6, background:d.color, borderRadius:1, display:"inline-block"}}/>
|
||
<span style={{color:"var(--fg-1)"}}>{d.name}</span>
|
||
</span>
|
||
);
|
||
};
|
||
|
||
const ArtboardOverview = () => {
|
||
const allVehicles = (window.VEHICLES || []);
|
||
const { role } = (typeof window.useCurrentRole === "function") ? window.useCurrentRole() : { role: null };
|
||
// Apply role-based scope before user-facing filters
|
||
const vehicles = React.useMemo(() => {
|
||
if (!role || role.scope === "all" || role.scope === "ops" || role.scope === "finance") return allVehicles;
|
||
if (role.scope === "dept") return allVehicles.filter(v => v.dept === role.deptId);
|
||
return allVehicles;
|
||
}, [role, allVehicles]);
|
||
const counts = (window.COUNTS || {});
|
||
const deps = (window.DEPARTMENTS || []);
|
||
const isDeptScoped = role && role.scope === "dept";
|
||
// Scoped counts so KPIs match what the role can actually see
|
||
const scopedCounts = React.useMemo(() => {
|
||
const c = { all: vehicles.length, inStock:0, leasing:0, abnormal:0, self:0, lease:0, online:0 };
|
||
vehicles.forEach(v => {
|
||
if (v.asset === "in_stock") c.inStock++;
|
||
else if (v.asset === "leasing") c.leasing++;
|
||
else if (v.asset === "abnormal") c.abnormal++;
|
||
if (v.own === "self") c.self++; else c.lease++;
|
||
if (v.gps === "online") c.online++;
|
||
});
|
||
return c;
|
||
}, [vehicles]);
|
||
|
||
// Display-scale: 管理员视角下,KPI 按实际车队规模放大到 1006 辆 / 892 在线
|
||
// (部门视角保持真实数字)
|
||
const FLEET_SIZE = 1006;
|
||
const FLEET_ONLINE = 892;
|
||
const scale = !isDeptScoped && scopedCounts.all > 0 ? FLEET_SIZE / scopedCounts.all : 1;
|
||
const sc = (n) => Math.round(n * scale);
|
||
const dispTotal = isDeptScoped ? scopedCounts.all : FLEET_SIZE;
|
||
const dispOnline = isDeptScoped ? scopedCounts.online : FLEET_ONLINE;
|
||
const dispInStock = isDeptScoped ? scopedCounts.inStock : sc(scopedCounts.inStock);
|
||
const dispLeasing = isDeptScoped ? scopedCounts.leasing : sc(scopedCounts.leasing);
|
||
const dispAbnormal = isDeptScoped ? scopedCounts.abnormal : sc(scopedCounts.abnormal);
|
||
|
||
const [selected, setSelected] = React.useState(vehicles[8]?.id || vehicles[0]?.id);
|
||
React.useEffect(() => {
|
||
if (vehicles.length && !vehicles.find(x => x.id === selected)) {
|
||
setSelected(vehicles[0].id);
|
||
}
|
||
}, [vehicles, selected]);
|
||
const [filterAsset, setFilterAsset] = React.useState("all"); // all | in_stock | leasing | abnormal
|
||
const [filterDept, setFilterDept] = React.useState("all");
|
||
const [filterOwn, setFilterOwn] = React.useState("all");
|
||
const [search, setSearch] = React.useState("");
|
||
|
||
const filtered = vehicles.filter(v => {
|
||
if (filterAsset !== "all" && v.asset !== filterAsset) return false;
|
||
if (filterDept !== "all" && v.dept !== filterDept) return false;
|
||
if (filterOwn !== "all" && v.own !== filterOwn) return false;
|
||
if (search && !v.plate.includes(search) && !v.vin.includes(search)) return false;
|
||
return true;
|
||
});
|
||
|
||
const v = vehicles.find(x => x.id === selected) || vehicles[0];
|
||
if (!v) return null;
|
||
|
||
return (
|
||
<div className="app">
|
||
<Sidebar active="map"/>
|
||
<div style={{flex:1, display:"flex", flexDirection:"column", minWidth:0, position:"relative", zIndex:1}}>
|
||
<Topbar
|
||
crumbs={isDeptScoped ? ["羚牛车辆数据中心", "资产管理", role.name.replace(/.*·/,"")] : ["羚牛车辆数据中心", "资产管理", "总览"]}
|
||
kpis={[
|
||
{ lbl: isDeptScoped ? "本部门车辆" : "总车辆", val: dispTotal },
|
||
{ lbl:"在线", val: dispOnline, delta: dispTotal ? Math.round(dispOnline/dispTotal*100) + "%" : "0%", deltaUp:true },
|
||
{ lbl:"在库", val: dispInStock, delta: dispTotal ? Math.round(dispInStock/dispTotal*100) + "%" : "0%" },
|
||
{ lbl:"租赁", val: dispLeasing, delta: dispTotal ? Math.round(dispLeasing/dispTotal*100) + "%" : "0%", deltaUp:true },
|
||
{ lbl:"异常", val: dispAbnormal, delta: dispAbnormal > 0 ? "+" + dispAbnormal : "0", deltaUp:false },
|
||
]}
|
||
/>
|
||
{isDeptScoped && (
|
||
<div style={{
|
||
padding:"7px 16px", background:"var(--accent-soft)",
|
||
borderBottom:"1px solid var(--border-1)", fontSize:11,
|
||
display:"flex", alignItems:"center", gap:10, color:"var(--fg-1)"
|
||
}}>
|
||
<span style={{width:6, height:6, borderRadius:3, background:"var(--accent)"}}/>
|
||
<span><span className="strong">数据权限:</span>当前以 <span className="strong">{role.name}</span> 身份登录,仅可见本部门 {scopedCounts.all} 辆车 · 全公司共 {FLEET_SIZE} 辆</span>
|
||
<span className="muted" style={{marginLeft:"auto"}}>切换身份请使用右下角 Tweaks · 登录身份</span>
|
||
</div>
|
||
)}
|
||
<div style={{flex:1, display:"grid", gridTemplateColumns:"320px 1fr 360px", gap:0, minHeight:0}}>
|
||
{/* Left: fleet list with asset filters */}
|
||
<div style={{borderRight:"1px solid var(--border-1)", background:"var(--bg-1)", display:"flex", flexDirection:"column", minHeight:0}}>
|
||
<div style={{padding:"12px 14px 8px"}}>
|
||
<div className="between" style={{marginBottom:8}}>
|
||
<span className="eyebrow">车辆 · {filtered.length}/{scopedCounts.all}</span>
|
||
<span className="muted" style={{fontSize:11, cursor:"pointer"}}><Icon name="filter" size={11} style={{verticalAlign:"middle"}}/> 高级</span>
|
||
</div>
|
||
<div className="search" style={{height:28}}>
|
||
<Icon name="search" size={12}/>
|
||
<input placeholder="车牌 / VIN" value={search} onChange={e => setSearch(e.target.value)}/>
|
||
</div>
|
||
|
||
{/* Asset status filter */}
|
||
<div style={{marginTop:8}}>
|
||
<div className="muted" style={{fontSize:10, marginBottom:4, letterSpacing:".05em"}}>资产状态</div>
|
||
<div style={{display:"flex", gap:4, flexWrap:"wrap"}}>
|
||
{[
|
||
{k:"all", l:"全部", c: scopedCounts.all},
|
||
{k:"in_stock", l:"在库", c: scopedCounts.inStock},
|
||
{k:"leasing", l:"租赁" , c: scopedCounts.leasing},
|
||
{k:"abnormal", l:"异常", c: scopedCounts.abnormal},
|
||
].map(o => (
|
||
<span key={o.k}
|
||
className={"chip" + (filterAsset === o.k ? " accent" : "")}
|
||
style={{cursor:"pointer", fontSize:11}}
|
||
onClick={() => setFilterAsset(o.k)}>
|
||
{o.l} <span className="muted mono" style={{marginLeft:3, fontSize:10}}>{o.c}</span>
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Ownership */}
|
||
<div style={{marginTop:6}}>
|
||
<div className="muted" style={{fontSize:10, marginBottom:4, letterSpacing:".05em"}}>归属</div>
|
||
<div style={{display:"flex", gap:4, flexWrap:"wrap"}}>
|
||
{[
|
||
{k:"all", l:"全部"},
|
||
{k:"self", l:"自有", c: scopedCounts.self},
|
||
{k:"lease", l:"外租", c: scopedCounts.lease},
|
||
].map(o => (
|
||
<span key={o.k}
|
||
className={"chip" + (filterOwn === o.k ? " accent" : "")}
|
||
style={{cursor:"pointer", fontSize:11}}
|
||
onClick={() => setFilterOwn(o.k)}>
|
||
{o.l}{o.c != null && <span className="muted mono" style={{marginLeft:3, fontSize:10}}>{o.c}</span>}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Department — hidden when role is dept-scoped (only one dept visible) */}
|
||
{!isDeptScoped && (
|
||
<div style={{marginTop:6}}>
|
||
<div className="muted" style={{fontSize:10, marginBottom:4, letterSpacing:".05em"}}>业务部门</div>
|
||
<div style={{display:"flex", gap:4, flexWrap:"wrap"}}>
|
||
<span className={"chip" + (filterDept === "all" ? " accent" : "")}
|
||
style={{cursor:"pointer", fontSize:11}}
|
||
onClick={() => setFilterDept("all")}>全部</span>
|
||
{deps.map(d => (
|
||
<span key={d.id}
|
||
className={"chip" + (filterDept === d.id ? " accent" : "")}
|
||
style={{cursor:"pointer", fontSize:11}}
|
||
onClick={() => setFilterDept(d.id)}>
|
||
<span style={{width:6, height:6, background:d.color, borderRadius:1, display:"inline-block", marginRight:4, verticalAlign:"middle"}}/>
|
||
{d.name}
|
||
<span className="muted mono" style={{marginLeft:3, fontSize:10}}>{counts.byDept?.[d.id] || 0}</span>
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="scroll" style={{flex:1, padding:"4px 0"}}>
|
||
{filtered.map(x => (
|
||
<div key={x.id} onClick={() => setSelected(x.id)}
|
||
style={{
|
||
padding:"10px 14px", borderLeft: "2px solid " + (x.id === selected ? "var(--accent)" : "transparent"),
|
||
background: x.id === selected ? "var(--accent-soft)" : "transparent",
|
||
cursor:"pointer", borderBottom:"1px solid var(--border-1)"
|
||
}}>
|
||
<div className="between">
|
||
<span className="mono strong" style={{fontSize:13, color:"var(--fg-0)"}}>{x.plate}</span>
|
||
<AssetStatusChip status={x.asset}/>
|
||
</div>
|
||
<div className="muted mono" style={{fontSize:10, marginTop:3, opacity:.8}}>{x.vin}</div>
|
||
<div style={{display:"flex", gap:8, marginTop:5, alignItems:"center", flexWrap:"wrap"}}>
|
||
<DeptDot dept={x.dept}/>
|
||
<OwnChip own={x.own}/>
|
||
{x.gps === "offline" && <span className="muted" style={{fontSize:10}}>● GPS离线</span>}
|
||
</div>
|
||
<div className="muted" style={{fontSize:10, marginTop:4}}>
|
||
<Icon name="pin" size={9} style={{verticalAlign:"middle", marginRight:3, opacity:.6}}/>
|
||
{x.city}
|
||
{x.customer !== "—" && (<><span style={{margin:"0 4px"}}>·</span>{x.customer}</>)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
{filtered.length === 0 && (
|
||
<div className="muted" style={{padding:"40px 16px", textAlign:"center", fontSize:12}}>没有匹配的车辆</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Map center */}
|
||
<div style={{position:"relative", minWidth:0, minHeight:0}}>
|
||
<FleetMap selectedId={selected} onSelect={(x)=>setSelected(x.id)} />
|
||
<div style={{position:"absolute", top:12, right:12, display:"flex", flexDirection:"column", gap:6}}>
|
||
{["layers","plus","close","sat","pin"].map((n,i)=>(
|
||
<div key={i} className="icon-btn" style={{width:32, height:32, background:"var(--bg-1)", border:"1px solid var(--border-1)"}}>
|
||
<Icon name={n === "close" ? "expand" : n} size={14}/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{/* Legend by asset */}
|
||
<div style={{position:"absolute", bottom:12, left:12, padding:"8px 10px", background:"var(--bg-1)", border:"1px solid var(--border-1)", borderRadius:6, display:"flex", gap:14, fontSize:10}}>
|
||
<span className="mid gap-1"><span className="dot ok"/> 在库/正常</span>
|
||
<span className="mid gap-1"><span className="dot info"/> 租赁</span>
|
||
<span className="mid gap-1"><span className="dot warn"/> 待整备</span>
|
||
<span className="mid gap-1"><span className="dot danger"/> 异常</span>
|
||
<span className="mid gap-1"><span className="dot idle"/> GPS离线</span>
|
||
</div>
|
||
<div style={{position:"absolute", top:12, left:12, padding:"6px 10px", background:"var(--bg-1)", border:"1px solid var(--border-1)", borderRadius:6, fontSize:11, fontFamily:"var(--font-mono)", display:"flex", alignItems:"center", gap:10, color:"var(--fg-1)", boxShadow:"var(--shadow-1)"}}>
|
||
<span style={{display:"inline-flex", alignItems:"center", gap:4}}><span className="dot ok pulse"/> LIVE</span>
|
||
<span className="muted">|</span>
|
||
<span>嘉兴市·平湖</span>
|
||
<span className="muted">|</span>
|
||
<span>14:32:08</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right: vehicle asset detail panel */}
|
||
<div style={{borderLeft:"1px solid var(--border-1)", background:"var(--bg-1)", display:"flex", flexDirection:"column", minHeight:0}}>
|
||
<div style={{padding:"14px 16px 12px", borderBottom:"1px solid var(--border-1)"}}>
|
||
<div className="between">
|
||
<div style={{minWidth:0, flex:1}}>
|
||
<div className="eyebrow">{v.deptName} · {v.deptLead}</div>
|
||
<div className="mono strong" style={{fontSize:18, fontWeight:600, marginTop:4}}>{v.plate}</div>
|
||
<div className="muted mono" style={{fontSize:10, marginTop:2}}>{v.vin}</div>
|
||
</div>
|
||
<div className="col gap-1" style={{alignItems:"flex-end"}}>
|
||
<AssetStatusChip status={v.asset}/>
|
||
<OwnChip own={v.own}/>
|
||
</div>
|
||
</div>
|
||
<div className="mid gap-2" style={{marginTop:8, fontSize:10}}>
|
||
<span className="muted">车辆等级</span>
|
||
<span className="strong">{v.grade}级</span>
|
||
<span className="muted">·</span>
|
||
<span className="muted">状态时长</span>
|
||
<span className="mono strong">{v.statusDays}天</span>
|
||
{v.fleetCode && <>
|
||
<span className="muted">·</span>
|
||
<span className="muted">编号</span>
|
||
<span className="mono strong">{v.fleetCode}</span>
|
||
</>}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="scroll" style={{flex:1}}>
|
||
{/* 业务关系 */}
|
||
<div style={{padding:"14px 16px", borderBottom:"1px solid var(--border-1)"}}>
|
||
<div className="eyebrow" style={{marginBottom:10}}>业务关系</div>
|
||
<div className="col gap-2" style={{fontSize:11}}>
|
||
<div className="between"><span className="muted">业务部门</span>
|
||
<span className="mid gap-1"><span style={{width:6,height:6,background:v.deptColor,borderRadius:1,display:"inline-block"}}/><span className="strong">{v.deptName}</span></span>
|
||
</div>
|
||
<div className="between"><span className="muted">业务负责人</span><span className="strong">{v.deptLead}</span></div>
|
||
<div className="between"><span className="muted">客户</span><span className="strong" style={{textAlign:"right"}}>{v.customer}</span></div>
|
||
<div className="between"><span className="muted">所属公司</span><span style={{fontSize:10, textAlign:"right"}}>{v.ownCompany}</span></div>
|
||
{v.own === "lease" && <div className="between"><span className="muted">租赁公司</span><span style={{fontSize:10, textAlign:"right"}}>{v.company}</span></div>}
|
||
{v.contractNo && <div className="between"><span className="muted">合同编号</span><span className="mono" style={{fontSize:10}}>{v.contractNo}</span></div>}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 实时车况 — 财务身份不可见 */}
|
||
{role && role.scope === "finance" ? (
|
||
<div style={{padding:"14px 16px", borderBottom:"1px solid var(--border-1)"}}>
|
||
<div className="eyebrow" style={{marginBottom:10}}>实时车况</div>
|
||
<div style={{padding:"14px 12px", background:"var(--bg-2)", border:"1px dashed var(--border-1)", borderRadius:4, textAlign:"center"}}>
|
||
<div style={{fontSize:18, opacity:0.4, marginBottom:6}}>🔒</div>
|
||
<div style={{fontSize:11, color:"var(--fg-2)"}}>实时车况数据已隐藏</div>
|
||
<div style={{fontSize:10, color:"var(--fg-3)", marginTop:3}}>财务身份仅可见资产 · 业务关系 · 合同</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div style={{padding:"14px 16px", borderBottom:"1px solid var(--border-1)"}}>
|
||
<div className="between" style={{marginBottom:10}}>
|
||
<span className="eyebrow">实时车况</span>
|
||
<span className="chip" style={{fontSize:9, padding:"1px 6px"}}>
|
||
<span className={"dot " + (v.gps === "online" ? "ok" : "idle")} style={{width:5,height:5}}/> GPS{v.gps === "online" ? "在线" : "离线"}
|
||
</span>
|
||
</div>
|
||
<div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:10}}>
|
||
<Gauge value={v.speed/120} label={v.speed} sub="km/h" color="var(--info)"/>
|
||
<Gauge value={v.soc/100} label={v.soc + "%"} sub="电量" color={v.soc < 20 ? "var(--danger)" : "var(--accent)"}/>
|
||
</div>
|
||
<div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:10, marginTop:10, fontSize:11}}>
|
||
<div className="col gap-1">
|
||
<span className="muted">氢气压力</span>
|
||
<span className="mono strong">{v.h2} MPa</span>
|
||
</div>
|
||
<div className="col gap-1">
|
||
<span className="muted">续航</span>
|
||
<span className="mono strong">{v.range} km</span>
|
||
</div>
|
||
<div className="col gap-1">
|
||
<span className="muted">电机温度</span>
|
||
<span className="mono strong" style={{color: v.motorTemp > 90 ? "var(--danger)" : "var(--fg-0)"}}>{v.motorTemp}°C</span>
|
||
</div>
|
||
<div className="col gap-1">
|
||
<span className="muted">停车场</span>
|
||
<span className="strong" style={{fontSize:10}}>{v.parking}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 里程 & 保养 */}
|
||
<div style={{padding:"14px 16px", borderBottom:"1px solid var(--border-1)"}}>
|
||
<div className="eyebrow" style={{marginBottom:10}}>里程与保养</div>
|
||
<div className="col gap-2" style={{fontSize:11}}>
|
||
<div className="between"><span className="muted">累计里程</span><span className="mono strong">{v.totalKm.toLocaleString()} km</span></div>
|
||
<div className="between"><span className="muted">上次保养</span><span className="mono">{v.lastMaintDays}天前 · {v.lastMaintKm.toLocaleString()}km</span></div>
|
||
<div>
|
||
<div className="between" style={{marginBottom:4}}>
|
||
<span className="muted">下次保养</span>
|
||
<span className={"mono strong"} style={{color: v.kmToMaint < 1000 ? "var(--warn)" : "var(--fg-0)"}}>
|
||
剩余 {v.kmToMaint.toLocaleString()} km
|
||
</span>
|
||
</div>
|
||
<div className="bar" style={{height:4}}>
|
||
<i style={{
|
||
width: Math.min(100, ((10000 - v.kmToMaint) / 10000) * 100) + "%",
|
||
background: v.kmToMaint < 1000 ? "var(--warn)" : "var(--accent)",
|
||
}}/>
|
||
</div>
|
||
</div>
|
||
{v.handoverKm != null && (
|
||
<div className="between"><span className="muted">交车里程</span><span className="mono">{v.handoverKm.toLocaleString()} km</span></div>
|
||
)}
|
||
{v.returnKm != null && (
|
||
<div className="between"><span className="muted">还车里程</span><span className="mono">{v.returnKm.toLocaleString()} km</span></div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{v.asset === "abnormal" && (
|
||
<div style={{padding:"14px 16px"}}>
|
||
<div className="eyebrow" style={{marginBottom:8}}>异常处理</div>
|
||
<div style={{padding:"8px 10px", background:"var(--danger-soft)", border:"1px solid oklch(0.68 0.220 25 / 0.4)", borderRadius:4, fontSize:11}}>
|
||
<div className="between">
|
||
<span className="strong">资产状态异常</span>
|
||
<span className="mono muted">{v.statusDays}天</span>
|
||
</div>
|
||
<div className="muted" style={{marginTop:3}}>停车场标记为异常 · 待业务部门核查</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div style={{padding:"12px 16px", display:"flex", gap:6}}>
|
||
<button className="btn primary" style={{flex:1}} onClick={() => location.hash = "#/detail"}><Icon name="route" size={13}/> 详情</button>
|
||
<button className="btn" style={{flex:1}} onClick={() => location.hash = "#/playback"}><Icon name="history" size={13}/> 轨迹</button>
|
||
<button className="btn icon"><Icon name="bell" size={13}/></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
window.ArtboardOverview = ArtboardOverview;
|