甲方只给了一张卫星截图,我用 Three.js + Blender 建了一座 3D 能源站

一个「综合能源补给站」数字孪生大屏的完整实现:从一张卫星截图出发,反求设计稿相机、脚本化建模、Cycles 烘焙夜景、牵引车倒车入位、运营仿真,最后让三维画面和设计稿逐像素对齐。

一、需求:一张截图,一份清单

甲方的需求很简单:做一个综合能源补给站的 3D 可视化大屏。站里有加油加气站、LNG 加气站、重卡超充、乘用车换电站和办公楼。

能拿到手的资料也很简单:一份设施清单,加上一张地图卫星截图,红线圈出的就是这块地:

没有地址,没有 CAD,没有尺寸。所以整件事要分成几步来做:

  1. 当规划师:估出比例尺,排出每栋建筑的位置,规划车辆怎么进、怎么出;
  2. 定视觉:先出一张 AI 概念效果图给甲方确认(下图);
  3. 当建模师:在 Blender 里把规划「种」成三维模型,烘焙夜景灯光;
  4. 当前端:用 three.js 加载模型,接上仿真数据,让车流动起来;
  5. 当质检:确认实现和设计稿一致。

最终效果,左边是甲方给的,右边是交付的(同一个三维视角):

技术栈

环节 工具
页面 Vue 3 + Vite,px→rem 等比缩放(postcss-pxtorem,设计稿 1920 宽)
三维 three.js:正交相机、UnrealBloomPass 辉光、RectAreaLight、InstancedMesh
建模 Blender 5.1,全部用 Python 脚本建模(bpy + bmesh),Cycles 烘焙地面光照
规划 Python(PIL / NumPy / OpenCV):规划数据、规划图、航拍底图处理
车辆模型 Sketchfab CC BY 4.0 模型(MAN TGX、Actros 牵引车、半挂车、轿车),脚本减面换材质
还原度检查 无头 Chrome 截图 + Python 叠图对比

二、整体流水线

整个项目坚持一条原则:所有产物都由脚本生成,不手改模型、不手改数据。规划数据只写在一个地方,其余产物都从它派生出来:

text 复制代码
draw_plan.py(规划数据:唯一数据源)
 ├─→ plan-v2.png          规划图(给人看)
 ├─→ site.json            Blender 布局
 ├─→ plan.json            页面数据(开场动画的分区色块、红线)
 └─→ site.webp            去掉手绘红线、四周外扩的航拍底图
                │
Blender(scripts/blender/energy/*.py)
 ├─→ station.glb          场站模型(树 / 路灯导出为 GPU 实例)
 ├─→ vehicles.glb         车辆预制(车轴位置写进 glTF extras)
 ├─→ ground_bake.webp     Cycles 烘焙的地面光照
 └─→ outline.json         沙盘轮廓、车道范围
                │
three.js(EnergyScene)+ 仿真器(Simulator)+ Vue 面板

改一个罩棚的位置,只需要改 draw_plan.py 里的一行坐标,然后重跑一遍流水线。

三、从设计稿「反求」布局

最开始我是照自己画的规划草图建模的,结果和甲方确认的概念图对不上:沿路的设施顺序反了,地块形状不同,重卡车位的排法也不同。甲方认的是概念图,所以要反过来,从概念图推出规划。

3.1 标定设计稿的相机

概念图是 AI 生成的,没有相机参数,但它看起来是一张近似正交的等轴测图。正交相机有个很好用的性质:

地面上的点投到屏幕上是一个仿射变换:横向长度按比例 s 缩放,沿视线方向的长度再多压缩 sin(俯角) 倍。

设相机方位角为 θ,俯角为 φ,比例为 s(像素/米),地面点 (x, y, z) 投到屏幕上是:

python 复制代码
import numpy as np

def project(p, az, el, s, c):
    """正交相机:世界点 (x, y, z) → 设计稿像素(简化示意)"""
    th, ph = np.radians(az), np.radians(el)
    X = -np.sin(th) * p[0] + np.cos(th) * p[1]  # 屏幕横向
    Y = (-np.sin(ph) * (np.cos(th) * p[0] + np.sin(th) * p[1])  # 地面纵深,被压扁
         + np.cos(ph) * p[2])  # 高度,直接抬升
    return c + s * np.array([X, -Y])

概念图里有很多「地面上的矩形」:罩棚屋面、车位前沿、车棚前沿。它们在真实世界里是直角、互相平行的。把这些矩形的像素角点标出来,用最小二乘解出 θ、φ、s 和画面偏移,使「矩形投影后的角点」和「图上的角点」误差最小。加油罩棚长度按常见的 40 m 取值,用来确定尺度。

得到的结果:方位 -47.06° 、俯角 23.66° 、8.33 px/m,残差约 3 px。

3.2 反投影回地面

相机确定后,屏幕点 → 地面点就是解一个 2×2 线性方程。屋面上的点先按估计的高度扣掉抬升量,再反解:

python 复制代码
def unproject(px, z, az, el, s, c):
    """设计稿像素 + 已知高度 z → 地面 (x, y)(简化示意)"""
    th, ph = np.radians(az), np.radians(el)
    X, Y = (px - c) / s
    Y = -Y - np.cos(ph) * z  # 扣掉高度带来的抬升
    A = np.array([[-np.sin(th), np.cos(th)],
                  [-np.sin(ph) * np.cos(th), -np.sin(ph) * np.sin(th)]])
    return np.linalg.solve(A, [X, Y])

把概念图里每栋建筑、每条道路、沙盘边缘的像素点都反投影回去,就得到了规划的米坐标。这些坐标写进 draw_plan.py,变成唯一的数据源:

python 复制代码
# 沙盘外轮廓:超椭圆(中心 PLAT_C,半轴 a 沿路、b 进深,指数 n;n=2 为椭圆,越大越接近圆角矩形),含门前国道
PLAT_C = (120.0, 17.0)
PLAT_A, PLAT_B, PLAT_N = 125.0, 49.0, 2.2

def platform(n_pts=360):
    """超椭圆外轮廓点列(逆时针)"""
    out = []
    for i in range(n_pts):
        t = 2 * math.pi * i / n_pts
        c, s = math.cos(t), math.sin(t)
        out.append((PLAT_C[0] + PLAT_A * math.copysign(abs(c) ** (2 / PLAT_N), c),
                    PLAT_C[1] + PLAT_B * math.copysign(abs(s) ** (2 / PLAT_N), s)))
    return out

概念图里那种「圆角胶囊形」沙盘,用一个指数 2.2 的超椭圆就能拟合得很好。

规划坐标统一用 (u, v):u 沿门前道路向南,v 垂直道路、指向场内。Blender 和 three.js 各有一条固定的换算公式(three.js 里是 x = 110 - u, z = 40 - v)。三端共用一套坐标系,路线、锚点、标签就不会错位。

设计稿和甲方清单冲突时,以甲方的数量为准:概念图画了 12 个重卡位,甲方要 10 个,那就保留 10 个,把单个车位放宽到 6.4 m,整排长度和概念图保持一致。

四、Blender 脚本化建模

为什么不手工建模?因为布局改了好几轮,每次手工挪几十个对象是噩梦。用脚本建模的话,改完数据重跑一遍就行:

python 复制代码
# Blender 里通过 MCP 执行
import energy.build as b; b.run()      # 清空场景,按 site.json 重建,保存 .blend
import energy.export as e; e.export()  # 导出 station.glb + vehicles.glb

脚本按职责分成几个模块:

模块 职责
palette.py 材质与程序化贴图(沥青、混凝土、光伏板、价格屏......)
assets.py 预制件:加油机、罩棚、换电站、LNG 储罐、超充桩、路灯、树......
layout.py 读 site.json,换算坐标,摆放全部要素,合并地面
lights.py / bake.py 烘焙专用灯光与 Cycles 烘焙
vendor.py 外部车辆模型的减面、换材质、实测车轴

命名就是接口 。three.js 是按对象名驱动模型的:pile_C01~pile_C10 是超充终端,bay_C01 是车位描边,anchor_fuel 是标签锚点,canopy_fuel_light 是罩棚灯面。改名会直接让前端找不到对象,所以这些名字写进了文档,当作约定来维护。

导出时有几个值得一提的选项:

python 复制代码
bpy.ops.export_scene.gltf(
    export_format="GLB",
    export_image_format="WEBP",                 # 贴图转 WebP,体积小很多
    export_extras=col_name == "VEHICLES",       # 车轴位置(rig_*)走 glTF extras → three 的 userData
    export_gpu_instances=True,                  # 几百棵树、路灯导出为 GPU 实例
    # ......
)

五、夜景:把 Cycles 的光「烘」进 three.js

概念图的质感几乎全来自光:罩棚下被照亮的站坪、路灯下边缘清楚的暖色光斑、树下的投光,还有湿润路面上的灯光倒影。three.js 实时光照做不出这种效果,路灯有几十盏,实时阴影也扛不住。

5.1 地面光照烘焙

做法是:在 Blender 里给罩棚、路灯、树下各放一盏烘焙专用灯(不导出),用 Cycles 把最终画面 (COMBINED:贴图 × 光照 × 阴影)烘进一张地面贴图。three.js 里地面直接显示这张图:

js 复制代码
_applyGroundBake(tex) {
  tex.flipY = false // 与 glTF 的 UV 原点约定一致
  tex.colorSpace = THREE.SRGBColorSpace
  tex.channel = 1 // 走第二套 UV(Lightmap)
  this.groundMat = new THREE.MeshStandardMaterial({
    color: 0x000000, // 基色为黑:不受实时光影响
    emissive: 0xffffff,
    emissiveMap: tex, // 烘焙结果当自发光显示
    emissiveIntensity: THEME.ground.intensity,
    roughness: THEME.ground.roughness, // 低粗糙度 + 环境反射 → 湿润路面的光泽
    metalness: 0,
    envMapIntensity: THEME.ground.env
  })
  // ......
}

关键点是黑色基色 + 烘焙图当自发光:地面完全不吃实时光,看到的就是 Cycles 渲染的结果,同时还保留了 PBR 的环境反射,可以做出湿路面的光泽。

烘焙这一步踩了两个坑:

  • 重叠的路面烘成一片黑 :地面由很多层多边形拼成(沥青、混凝土站坪、草地、标线),合并后用一套从正上方平铺的 Lightmap UV。上下重叠的面如果完全共面,Cycles 烘焙时光线会打到另一块面上,整片变黑。解决办法是给每一层错开 1.5 mm 的高度,后建的层盖住先建的层。
  • 棱柱的侧面、底面和顶面抢像素 :平面投影的 UV 里,侧面和底面会和顶面映射到同一块像素。合并时只保留朝上的面(normal.z >= 0.3)就好了。

整图 6144 宽、96 采样,在 M2 GPU 上大约要 3 分钟。MCP 调用会超时,所以把烘焙交给 bpy.app.timers 在 Blender 里异步执行,调用本身立即返回。

5.2 湿路面倒影

湿沥青上的灯光倒影有个规律:它总是沿着「光源脚下 → 观察者」的方向拉长。正交相机的视线方向是固定的,所以只要每帧把所有光带转到同一个角度即可:

js 复制代码
/** 按相机水平方向转动所有光带(方向变化很小时跳过) */
update(camera, target) {
  const yaw = Math.atan2(
    camera.position.x - target.x,
    camera.position.z - target.z
  )
  if (this._yaw !== null && Math.abs(yaw - this._yaw) < 0.002) return
  this._yaw = yaw
  this._q.setFromAxisAngle(new THREE.Vector3(0, 1, 0), yaw)
  this.sources.forEach((s, i) => {
    this._p.set(s.pos.x, 0.08, s.pos.z)
    this._s.set(s.width, 1, s.length)
    this._m.compose(this._p, this._q, this._s)
    this.mesh.setMatrixAt(i, this._m)
  })
  this.mesh.instanceMatrix.needsUpdate = true
}

几十条光带(每盏路灯一条,罩棚彩色檐口每 3 m 一条)共用一个 InstancedMesh,用加法混合,实例颜色就是光色乘以强度,总共只有一次绘制调用。

5.3 发光与色调

  • 色调映射用 NeutralToneMapping:ACES 会把高亮的彩色灯带褪成白色,Neutral 能保住青、紫、绿几种灯带的色相;
  • 自发光按材质单独给强度 :Blender 导出的自发光强度整体压低,最亮的几样(罩棚檐口、加油机灯带、车棚绿光)再在 THEME.glow.override 里单独指定,保证它们在辉光里清楚地亮起来;
  • 光伏板的天光反射 :夜景里光伏板映着天空泛蓝,实时环境反射太弱,就给 M_pv 加一层均匀的蓝色自发光来近似;
  • 雾 :远山和村庄按深度隐入夜色,但村庄的窗光材质关掉 fog,要让它们在夜色里透出来;
  • 后处理 MSAA :EffectComposer 默认的渲染目标没有抗锯齿,开辉光后边缘会发虚,给 WebGLRenderTarget 加上 samples: 4 就好了。

六、让三维画面和设计稿逐像素对齐

概念图是放在整屏效果图中间的(左右两侧是数据面板),场站要刚好落在面板之间。常见做法是按包围盒自适应,但那样永远对不准。

既然已经标定出了设计稿的相机,就可以直接用标定结果来算正交视锥:先指定世界里的锚点落在设计稿的哪个像素,再定下每米多少像素,left/right/top/bottom 就全确定了。

js 复制代码
// 节选,有删减
_fitFrustum() {
  const w = this.container.clientWidth
  const h = this.container.clientHeight
  const s = w / DESIGN_W // 页面按 1920 设计稿等比缩放
  const { anchor, at, scale } = THEME.view // 锚点、锚点所在像素、像素/米
  // 把锚点变换到主视角相机(probe)的相机空间
  const a = new THREE.Vector3(...anchor).applyMatrix4(probe.matrixWorldInverse)
  const k = 1 / scale // 米 / 设计稿像素
  cam.left = a.x - at[0] * k
  cam.right = cam.left + DESIGN_W * k
  cam.top = a.y + at[1] * k
  cam.bottom = cam.top - (h / s) * k
  cam.updateProjectionMatrix()
}

这样得到的是一个不对称视锥 ,场站自然避开了左右面板,窗口缩放时也按设计稿宽度等比缩放。设计稿 1536 宽的 8.33 px/m,放进 1920 整屏的 1096 宽区域后换算成 5.94 px/m,正好就是 THEME.view.scale。

另外,_toDesign() 把世界坐标投影回设计稿坐标,HTML 标签(设施名称、超充桩详情浮窗)每帧按它定位,和三维对象始终对得上。

七、开场动画:从卫星图「长」出能源站

进入页面先播放一段 8.6 秒的开场动画,讲清楚「这个站是怎么来的」:

时间(秒) 画面
0.0--0.8 俯视镜头,卫星底图淡入
0.6--2.4 用地红线逐段描出(按索引数 setDrawRange 逐段显示)
2.0--4.2 各功能分区色块与名称依次落下
4.2--7.6 设施从地面「长」出来,由北向南扫过
4.6--8.4 镜头从俯视缓缓倾斜到主视角

「长出来」的实现很朴素:把所有高出地面的对象先压扁(scale.y ≈ 0),再按 x 坐标排出延迟,逐个用缓动恢复原高:

js 复制代码
for (const r of this.risers) {
  const g = ease(span(t, 4.2 + r.delay, 5.6 + r.delay))
  r.o.scale.y = r.scale * Math.max(0.001, g)
}

这里有三个容易忽略的细节:

  1. 空对象的包围盒是空的 :anchor_* 标签锚点是没有几何的空对象,Box3 为空,算出的中心是 NaN。它混进延迟计算后,所有设施的 scale 都会变成 NaN,整个「长出来」阶段全部不可见,结束时才突然出现。解决办法是用 box.isEmpty() 跳过它们;
  2. 卫星图不够大 :截图只覆盖约 447 × 370 m,俯视镜头斜着看时屏幕角落会露出黑底。处理办法是用 OpenCV 把截图四周镜像外扩 32%,外扩部分越往外越模糊、越暗,最外一圈渐隐到页面背景色。顺便用 cv2.inpaint 把截图上手画的旧红线和地图 UI 抹掉;
  3. 俯视用对称视锥,倾斜时再过渡到设计稿的不对称视锥:开场时面板是隐藏的,不需要给面板让位,俯视阶段场站居中、国道横在画面下方;随着镜头倾斜,视锥宽高保持不变、中心线性插值到主视角的不对称视锥,方位角同时从「正对国道」转到设计稿的斜视角。

八、车辆:牵引车 + 挂车倒车入位

小车加油、CNG 出租车加气、换电车穿过换电站、LNG 重卡加气、LNG 槽车卸车,这些都是沿手写路线行驶、停车、离开。最有意思的是重卡倒车入位充电:

行驶模型是一个够用的运动学近似:

  • 路线是一条 Catmull-Rom 曲线(向心参数化,转角不过冲),s 是前轴在曲线上的弧长位置;
  • 车身由前轴点 P(s) 和后轴点 P(s - 轴距) 决定位置和朝向,转弯时车身自然沿切线跟随;
  • 挂车的牵引销挂在牵引车的鞍座上,挂车后轴组取曲线上更靠后的点,于是转弯时挂车会「内切」;
  • 倒车 时,后轴点在前方 P(s + 轴距),挂车后轴组也取曲线上更前方的点:挂车领先沿曲线拐进车位,牵引车跟着推。
js 复制代码
// 节选,有删减
place(curve, s, reverse = false) {
  const F = pointAt(curve, s, new THREE.Vector3())
  const r = this.rigs[0]
  const wb = r.front - r.rear
  const R = pointAt(curve, reverse ? s + wb : s - wb, new THREE.Vector3())
  const h = F.clone().sub(R).normalize() // 车头朝向
  const c = F.clone().addScaledVector(h, -r.front)
  this.parts[0].position.copy(c)
  this.parts[0].rotation.set(0, yaw(h.x, h.z), 0)
  if (this.kind === "semi") {
    // 鞍座 → 挂车后轴组:挂车绕牵引销转动
    const t = this.rigs[1]
    const K = c.clone().addScaledVector(h, r.fifth)
    const back = r.front - r.fifth + t.kingpin - t.rear
    const T = pointAt(curve, reverse ? s + back : s - back, new THREE.Vector3())
    const g = K.clone().sub(T).normalize()
    this.parts[1].position.copy(K.clone().addScaledVector(g, -t.kingpin))
    this.parts[1].rotation.set(0, yaw(g.x, g.z), 0)
  }
}

前轴、后轴组、鞍座、牵引销的位置,是 vendor.py 在 Blender 里对轮胎顶点聚类实测 出来的,写进 glTF extras,three.js 里从 userData.rig_* 读取。换一个车型模型,铰接也不会穿帮。

进站路线分两段:先沿车位前的通道越过目标车位 25 m,再沿一段 8 m 的倒车弧线退进车位。车停稳后插上充电线(TubeGeometry 沿二次贝塞尔曲线生成,发光材质随时间呼吸),车位描边变成绿色。

速度控制也很简单:按到下一个停车点的剩余距离算出刹车上限 √(2·a·d),再和限速取最小值(国道快、站内慢、倒车更慢),车就能平滑地加减速、刚好停在停车点上。

九、运营仿真:让数字和画面对得上

没有后端,数据全部来自一个纯 JS 的仿真器,按真实时钟运行:

  • 每类补给(加油、CNG、LNG、换电)各有一条 24 小时车流曲线(早晚高峰、夜间货运),每秒按泊松过程决定要不要发一辆车;
  • 10 个超充位各是一个状态机:idle → arriving → charging ⇄ fault → leaving → idle。SOC 70% 前满功率,之后线性回落;偶尔「枪温过高」,降功率 60~90 秒,地面冒出红色告警波纹;
  • 库存会联动:加油扣减油罐,LNG 低于 35% 自动叫槽车补液,CNG 压力随售气下降、压缩机启动后回升。

一个让面板「可信」的小设计是:场景接住了这辆车,仿真器才计数。

js 复制代码
/** 发起一次补给车次(场景接住才计数并扣库存) */
_trip(kind, now) {
  const ok = this.onTrip({ kind, plate: this._plate(kind === "lng") })
  if (ok === false) return // 车道占满,三维里放不下这辆车,就不计数
  this.trips++
  this.count[kind]++
  // ......扣库存
}

这样面板上的今日补给车次和画面上进出的车,在节奏上是一致的。不会出现画面里空无一车,数字却在疯涨的情况。

另外,充电会话做了「演示加速」:一次充电几分钟就走完,保证画面上始终有重卡进出。其余车流则按真实车流率。

十、还原度检查:别靠肉眼

「看起来差不多」是最不靠谱的验收标准。我写了两个小工具:

bash 复制代码
# 无头 Chrome 打开页面,等开场动画结束,把镜头固定在主视角,隐藏面板截图
node scripts/energy/capture.mjs https://localhost:8892 out.png --clean --design-frame

# 与设计稿对齐,输出三联图:设计稿 | 实现 | 边缘叠图
python3 scripts/energy/compare.py out.png

叠图里,设计稿的边缘描成品红,实现的边缘描成青色,重合的地方偏白,哪栋楼偏了一眼就能看出来:

位置对齐之后再对光影:按区域取平均色,和设计稿的同一区域比较亮度比,逐项调到接近 1:

区域 实现 / 设计稿
加油站坪 1.10
罩棚下 0.94
LNG 站坪 0.92
国道 0.89
车位前通道 1.10
办公楼 0.92
光伏屋面 0.94
远山 0.96

这个过程中定位到了好几个只靠肉眼很难发现的问题。比如加油站坪「又亮又偏蓝」,用 --eval 逐个关掉候选光源对比后,确认是两个原因叠加:一是 RectAreaLight 在低粗糙度地面上的高光把站坪刷白了,二是青色檐口灯带的辉光把整片站坪染青了。两者分别压低强度后,站坪就回到了设计稿的暖白色。

十一、踩坑汇总

现象 原因 解决
烘焙后部分路面一片黑 重叠的地面多边形共面 每层错开 1.5 mm 高度
烘焙后建筑周边地面发黑 棱柱的侧面、底面在平面投影 UV 里抢像素 合并地面时只保留朝上的面
彩色灯带在辉光里变白 ACES 色调映射压高光 改用 NeutralToneMapping
开启后处理后边缘锯齿发虚 离屏渲染目标没有 MSAA WebGLRenderTarget({ samples: 4 })
开场俯视时屏幕角落是黑的 卫星截图覆盖范围不够 镜像外扩 + 渐进模糊 + 渐隐到背景色
开场镜头偏到底图外面 场站中心按模型包围盒算,而包围盒包含了后方远山 改用沙盘外轮廓计算中心
设施「长出来」时全部不可见 空锚点的 Box3 为空,中心是 NaN,污染了延迟 跳过 box.isEmpty() 的对象
开场时卫星图上飘着绿色线条 充电线没跟着车辆一起隐藏 充电线、告警挂到车辆分组下
半挂车倒车时挂车方向反了 挂车后轴组仍取 s - back 倒车段改取 s + back

十二、总结

回头看,这个项目最值得分享的不是哪个炫酷的效果,而是几个朴素的做法:

  1. 单一数据源 + 全脚本生成:规划数据只写在一处,规划图、Blender 布局、页面数据都由它生成。改了五六轮布局,每一轮都只是重跑流水线;
  2. 把设计稿当「测量对象」:标定相机、反投影,把一张 AI 概念图变成可以精确还原的规格;
  3. 离线烘焙 + 实时叠加:静态的光影交给 Cycles,动态的部分(车灯、倒影、告警、辉光)交给 three.js,各做擅长的事;
  4. 用工具验收:截图、叠图、按区域测色,还原度从「感觉像」变成「数字接近 1」。

甲方给的是一张卫星截图,交出去的是一座会呼吸的数字能源站。


说明:场址为假定地块,运行数据为仿真数据。车辆模型来自 Sketchfab(CC BY 4.0),页面中已署名。

相关推荐
数脉2 小时前
数据血缘的八个典型企业应用场景
数据挖掘·数据分析·数据可视化
妖精的羽翼2 小时前
Web3D(Three.js)应用分级 + 学习路线规划(由浅入深,贴合接单 + 求职)
学习·three.js
踩着两条虫21 小时前
我在浏览器里“盖”了一个村:Vue + Three.js 大屏开发踩坑记
前端·vue.js·three.js
SL_staff21 小时前
BI不该只服务高管:JVS如何让仓库管理员自主配置库存预警
spring boot·数据分析·数据可视化
子龙_1 天前
来在网页上用threejs画个蛋白质吧
前端·webgl·three.js
Q26433650231 天前
【有源码】基于Hadoop+Spark的低能见度易雾站点识别与可视化分析系统-面向气象保障的低能见度监测数据挖掘与风险画像系统
大数据·hadoop·数据挖掘·spark·毕业设计·课程设计·数据可视化
天理智能科技2 天前
物流仿真项目投入成本怎么算?制造企业收益测算思路
数据可视化
龙亘川2 天前
AI 协同赋能城市治理:支撑政协数字化履职的技术路径探析
大数据·人工智能·智慧城市·开源软件·数据可视化
rememberme0012 天前
Windows Server 服务器监控部署文档(Prometheus + Grafana)
运维开发·数据可视化