Cesium(十) 动态修改白模颜色、白模渐变色、白模光圈特效、白模动态扫描光效、白模着色器

引言

在三维数字城市应用中,建筑可视化不仅是展示空间结构,更是传递信息、增强沉浸感的重要手段。通过 WebGL 着色器技术,我们可以为 3D 建筑模型赋予丰富的视觉效果,例如根据高度变化的渐变色彩、动态扫描光效等。本文分享一个基于 Vue 3 和 Cesium 的实践项目,实现了两个核心效果:

  1. 建筑高度渐变着色:根据建筑高度从底部的颜色渐变为顶部的另一种颜色,直观反映建筑高度分布。

  2. 动态光圈扫描:在渐变基础上叠加周期性移动的光圈,模拟扫描或能量波的效果。

同时项目集成了天地图影像和注记图层,提供了简洁的 UI 控制面板,允许用户实时调整渐变颜色并切换效果模式。本文将详细解析技术实现,包括 Cesium 的初始化、3D Tiles 加载、自定义着色器编写,以及 Vue 3 的响应式交互。

技术栈

  • 前端框架:Vue 3(Composition API)

  • 三维地球引擎:Cesium 1.x

  • 底图服务:天地图(影像 + 注记)

  • 着色器语言:GLSL(OpenGL Shading Language)

  • 构建工具:Vite

效果预览

项目运行后,会加载一个城市级 3D 建筑模型(3D Tiles),默认显示普通影像底图。左上角控制面板包含两个颜色选择器(底部色、顶部色)和两个按钮:"动态光圈扫描"与"清除效果"。

  • 渐变模式 :建筑颜色随高度线性插值,用户可以自定义起始色和终止色。

  • 扫描模式 :在渐变的基础上,一个明亮的光环从底部向顶部周期性移动,产生扫描动效。

核心实现详解

1. Cesium 实例化与配置

项目在 onMounted 中调用 initCesium 函数创建 Viewer。为了保持整洁,禁用了大部分默认控件(时间轴、导航、图层选择器等),仅保留地球视图。同时设置了 imageryProvider: false,后续通过自定义方式添加天地图图层。

javascript 复制代码
const newViewer = new Cesium.Viewer(cesiumContainer.value, {
  timeline: false,
  animation: false,
  homeButton: false,
  sceneModePicker: false,
  // ... 其他禁用
  imageryProvider: false,
  shouldAnimate: true,   // 启用动画,为动态扫描做准备
});

2. 天地图底图集成

为了提供清晰的地理底图,项目分别添加了天地图影像服务和注记服务。影像服务使用 UrlTemplateImageryProvider 拼接瓦片 URL,注记服务类似。由于需要支持未来的颜色自定义(如黑白化、单色滤镜),我们定义了一个 ColorizedImageryProvider 类继承自 UrlTemplateImageryProvider,虽然当前版本未实现具体颜色处理,但为扩展预留了接口。

javascript 复制代码
class ColorizedImageryProvider extends Cesium.UrlTemplateImageryProvider {
  constructor(options) {
    super(options);
    this.colorMode = options.colorMode || 'normal';
    this.customColorRGB = options.customColorRGB || { r: 58, g: 134, b: 255 };
  }
  // 可在此添加自定义着色器逻辑,对影像进行实时颜色变换
}

影像图层和注记图层分别设置 zIndex,确保注记始终显示在影像之上。

3. 加载 3D Tiles 建筑模型

通过 Cesium.Cesium3DTileset.fromUrl 加载本地或网络的 tileset.json 文件。加载成功后,将 tileset 添加到场景,并调用 getTilesetMaxHeight 函数获取模型的最大高度。这一高度值对于后续着色器中的高度归一化至关重要。

getTilesetMaxHeight 通过解析 tileset 的根节点包围盒(box、region 或 sphere)估算模型最大高度,避免硬编码。若获取失败则回退到默认值 155 米。

javascript

javascript 复制代码
const maxBuildingHeight = await getTilesetMaxHeight(tileset);

4. 自定义着色器实现视觉效果

Cesium 提供了 CustomShader API,允许开发者直接在渲染管线中插入 GLSL 代码,修改模型的材质。我们的两个效果均通过替换 tileset.customShader 实现。

4.1 高度渐变着色器

onColorChange 中(颜色选择器变化时触发),我们动态生成一个新的 CustomShader。核心 GLSL 代码如下:

glsl

javascript 复制代码
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {
  vec3 positionMC = fsInput.attributes.positionMC;
  float height = positionMC.y;                      // 模型坐标系中的高度
  float maxHeight = ${maxBuildingHeight.toFixed(2)};
  float heightRatio = clamp(height / maxHeight, 0.0, 1.0);
  
  vec3 baseColor = vec3(${baseColor.r}, ${baseColor.g}, ${baseColor.b});
  vec3 gradientColor = vec3(${gradientColor.r}, ${gradientColor.g}, ${gradientColor.b});
  
  material.diffuse = mix(baseColor, gradientColor, heightRatio);
}
  • positionMC 是模型坐标系下的顶点坐标,Y 轴向上表示高度。

  • 通过 height / maxHeight 得到归一化的高度比例,然后使用 mix 函数线性插值两种颜色。

  • 颜色值从 Vue 组件的响应式变量中读取,并在每次变化时重新生成着色器字符串。

4.2 动态光圈扫描着色器

按钮 customShader2 触发更复杂的着色器。它在渐变的基础上,加入了两个动态效果:

  1. 亮度随高度周期性波动 :使用 fract(czm_frameNumber / 360.0) 产生一个随时间线性递增的相位,结合正弦函数使亮度在高度方向上有波纹效果。

  2. 移动光圈 :定义一个随时间移动的归一化高度阈值 vtxf_a13,当建筑某点的高度比例接近该阈值时,增加亮度,形成扫描光环。

核心代码片段:

glsl

javascript 复制代码
// 亮度波纹
float vtxf_a11 = fract(czm_frameNumber / 360.0) * 3.14159265 * 2.0;
float vtxf_a12 = vtxf_height / _heightRange + sin(vtxf_a11) * 0.1;
material.diffuse *= vec3(vtxf_a12, vtxf_a12, vtxf_a12);

// 移动光圈
float vtxf_a13 = fract(czm_frameNumber / 360.0);
float vtxf_h = clamp(vtxf_height / _glowRange, 0.0, 1.0);
vtxf_a13 = abs(vtxf_a13 - 0.5) * 2.0;
float vtxf_diff = step(0.01, abs(vtxf_h - vtxf_a13));
material.diffuse += material.diffuse * (1.0 - vtxf_diff);
  • czm_frameNumber 是 Cesium 内置的帧计数器,用于驱动动画。

  • _heightRange 控制波纹的波长,_glowRange 定义光圈移动的总高度范围。

  • step 函数用来判断当前高度是否在光圈附近,1.0 - vtxf_diff 表示如果高度接近光圈阈值,则额外叠加亮度。

5. Vue 响应式交互

用户通过颜色选择器或按钮改变效果时,对应的响应式变量(如 modelColorHexactiveShader)更新,触发相应的函数重新生成着色器并赋给 tileset。这充分利用了 Vue 的响应式系统,使得着色器效果与 UI 实时同步。

javascript

javascript 复制代码
const onColorChange = () => {
  activeShader.value = 1;
  // 生成渐变着色器并应用
};

"清除效果"按钮则将 tileset.customShader 设为 undefined,恢复模型的原始纹理。

6. 资源清理与内存管理

在组件卸载时,destroyCesium 函数负责移除 tileset、销毁 Viewer 实例,避免内存泄漏。

关键点与优化思考

  1. 高度估算的准确性getTilesetMaxHeight 依赖 boundingVolume 的解析,不同数据源可能结构不同,需要增加容错。实际项目中可考虑预计算并存储在元数据中。

  2. 着色器性能:所有效果均在 GPU 中完成,性能开销很小,适合大规模城市模型。

  3. 天地图 token :代码中引用了 TD_MAP_KEY,实际使用时需要替换为自己的密钥。

  4. 动态动画流畅性shouldAnimate 必须设置为 true,否则 czm_frameNumber 不会递增,动画将停滞。

运行与体验

项目基于 Vue 3 和 Vite 构建。运行前请确保:

  • 将 3D Tiles 数据放置在 /public/data/tileset.json 路径下(可替换为任意符合规范的 tileset)

  • 申请天地图服务 token 并配置到 config.js

启动开发服务器后,即可在浏览器中体验交互效果。左上角的颜色选择器可实时调整渐变色,点击"动态光圈扫描"按钮,建筑表面将出现缓慢移动的光环,视觉效果极具科技感。

总结

本文展示了一个结合 Vue 3 和 Cesium 的 3D 建筑可视化项目,重点介绍了如何通过自定义着色器实现动态视觉增强。这种技术可广泛应用于智慧城市、数字孪生、灾害模拟等场景,帮助用户更直观地理解空间数据。

未来可以扩展的方向包括:

  • 支持更多着色器效果(如波纹扩散、热力图、夜间灯光等)

  • 允许用户自定义扫描速度、光圈宽度等参数

  • 将颜色与业务数据(如人口密度、楼龄)关联,实现数据驱动的可视化

  • 优化天地图颜色处理,实现底图单色化或叠加分析图层

通过本次实践,我们深刻体会到 WebGL 着色器为三维可视化带来的无限可能。希望这篇文章能为您的 Cesium 开发提供一些灵感和实用参考。

完整代码

javascript 复制代码
<template>
  <div class="app-container">
    <div class="top-left-controls">
      <div class="color-control-group">
        <div class="color-row">
          <label class="color-label">渐变颜色:</label>
          <input type="color" v-model="modelColorHex" class="color-input" @input="onColorChange" />
        </div>
        <div class="color-row">
          <label class="color-label">到</label>
          <input type="color" v-model="gradientColorHex" class="color-input" @input="onColorChange" />
        </div>
      </div>

      <div class="liquid-btn-group">
        <button @click="customShader2" :class="['liquid-btn', activeShader === 2 ? 'active' : '']">
          <span>动态光圈扫描</span>
        </button>
        <button @click="customShader3" :class="['liquid-btn', activeShader === 3 ? 'active' : '']">
          <span>清除效果</span>
        </button>
      </div>
    </div>

    <div ref="cesiumContainer" class="cesium-container"></div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { TD_MAP_KEY } from '../config.js';

const cesiumContainer = ref(null);
const activeShader = ref(0);
const modelColorHex = ref('#00aaff');
const gradientColorHex = ref('#ffffff');
let tileset = null;
let customShader = null;
let maxBuildingHeight = 150.0;

let colorizedImageryProvider = null;
let satelliteLayer = null;
let labelLayer = null;
let currentViewer = null;

class ColorizedImageryProvider extends Cesium.UrlTemplateImageryProvider {
  constructor(options) {
    super(options);
    this._tilingScheme = new Cesium.WebMercatorTilingScheme();
    this.colorMode = options.colorMode || 'normal';
    this.customColorRGB = options.customColorRGB || { r: 58, g: 134, b: 255 };
  }

  get tilingScheme() {
    return this._tilingScheme;
  }
 
}

const createLabelLayer = (viewer) => {
  const layer = viewer.imageryLayers.addImageryProvider(new Cesium.UrlTemplateImageryProvider({
    url: `https://t{s}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${TD_MAP_KEY}`,
    subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
    credit: '天地图',
    maximumLevel: 18,
    minimumLevel: 0,
    maximumScreenSpaceError: 0,
    disableDepthTestAgainstTerrain: true
  }));
  layer.zIndex = 1;
  return layer;
};

const refreshSatelliteLayer = () => {
  if (!currentViewer || !colorizedImageryProvider) return;
  const oldLayer = satelliteLayer;
  if (oldLayer) {
    currentViewer.imageryLayers.remove(oldLayer);
  }
  satelliteLayer = currentViewer.imageryLayers.addImageryProvider(colorizedImageryProvider);
  satelliteLayer.zIndex = 0;
  if (labelLayer) {
    currentViewer.imageryLayers.raiseToTop(labelLayer);
  }
};

const initMapLayers = (viewer) => {
  currentViewer = viewer;
  colorizedImageryProvider = new ColorizedImageryProvider({
    url: `https://t{s}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${TD_MAP_KEY}`,
    subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
    colorMode: 'normal',
    customColorRGB: { r: 58, g: 134, b: 255 },
    credit: '天地图',
    maximumLevel: 18,
    minimumLevel: 0
  });
  satelliteLayer = viewer.imageryLayers.addImageryProvider(colorizedImageryProvider);
  satelliteLayer.zIndex = 0;

  labelLayer = createLabelLayer(viewer);
};

const hexToRgb = (hex) => {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result ? {
    r: parseInt(result[1], 16) / 255,
    g: parseInt(result[2], 16) / 255,
    b: parseInt(result[3], 16) / 255
  } : { r: 0, g: 0.67, b: 1 };
};

const getTilesetMaxHeight = async (tileset) => {
  try {
    await tileset.readyPromise;
    let maxHeight = 75.0 + 80;
    const root = tileset.root;
    if (root && root.boundingVolume) {
      const boundingVolume = root.boundingVolume;
      if (boundingVolume.box) {
        const centerZ = boundingVolume.box[2];
        const halfZ = boundingVolume.box[11];
        maxHeight = centerZ + halfZ;
      } else if (boundingVolume.region) {
        maxHeight = boundingVolume.region[5];
      } else if (boundingVolume.sphere) {
        const centerZ = boundingVolume.sphere[2];
        const radius = boundingVolume.sphere[3];
        maxHeight = centerZ + radius;
      }
      if (isNaN(maxHeight) || maxHeight <= 75.0) {
        maxHeight = 155.0;
      }
    }
    maxHeight = Math.max(maxHeight + 5, 95.0);
    console.log(`模型最大高度: ${maxHeight.toFixed(2)} 米`);
    return maxHeight;
  } catch (error) {
    console.warn('获取模型最大高度失败,使用默认值:', error);
    return 155.0;
  }
};

const initCesium = () => {
  try {
    if (!cesiumContainer.value) {
      console.error('Cesium容器DOM元素不存在');
      return;
    }

    if (window.cesiumViewer) {
      window.cesiumViewer.destroy();
      window.cesiumViewer = null;
    }

    const newViewer = new Cesium.Viewer(cesiumContainer.value, {
      timeline: false,
      animation: false,
      homeButton: false,
      sceneModePicker: false,
      navigationHelpButton: false,
      baseLayerPicker: false,
      infoBox: false,
      selectionIndicator: false,
      navigationInstructionsInitiallyVisible: false,
      fullscreenButton: false,
      geocoder: false,
      imageryProvider: false,
      shouldAnimate: true,
    });

    window.cesiumViewer = newViewer;

    initMapLayers(newViewer);

    load3DTiles(newViewer);
  } catch (error) {
    console.error('Cesium 初始化失败:', error);
  }
};

const load3DTiles = async (viewer) => {
  try {
    tileset = await Cesium.Cesium3DTileset.fromUrl('/data/tileset.json');
    viewer.scene.primitives.add(tileset);
    maxBuildingHeight = await getTilesetMaxHeight(tileset);
    viewer.zoomTo(tileset);
  } catch (error) {
    console.error('加载3DTiles失败:', error);
  }
};

const onColorChange = () => {
  activeShader.value = 1;
  const baseColor = hexToRgb(modelColorHex.value);
  const gradientColor = hexToRgb(gradientColorHex.value);

  const shader = new Cesium.CustomShader({
    fragmentShaderText: `
      void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {
        vec3 positionMC = fsInput.attributes.positionMC;
        float height = positionMC.y;
        float maxHeight = ${maxBuildingHeight.toFixed(2)};
        float heightRatio = clamp(height / maxHeight, 0.0, 1.0);
        
        vec3 baseColor = vec3(${baseColor.r.toFixed(3)}, ${baseColor.g.toFixed(3)}, ${baseColor.b.toFixed(3)});
        vec3 gradientColor = vec3(${gradientColor.r.toFixed(3)}, ${gradientColor.g.toFixed(3)}, ${gradientColor.b.toFixed(3)});
        
        material.diffuse = mix(baseColor, gradientColor, heightRatio);
      }
    `
  });
  tileset.customShader = shader;
  customShader = shader;
};

const customShader2 = () => {
  activeShader.value = 2;
  const baseColor = hexToRgb(modelColorHex.value);
  const gradientColor = hexToRgb(gradientColorHex.value);
  const baseHeight = 0;
  const heightRange = 60.0;
  const glowRange = maxBuildingHeight - baseHeight;

  const shader = new Cesium.CustomShader({
    fragmentShaderText: `
      void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {
        vec3 positionMC = fsInput.attributes.positionMC;
        float height = positionMC.y;
        float maxHeight = ${maxBuildingHeight.toFixed(2)};
        float heightRatio = clamp(height / maxHeight, 0.0, 1.0);
        
        vec3 baseColor = vec3(${baseColor.r.toFixed(3)}, ${baseColor.g.toFixed(3)}, ${baseColor.b.toFixed(3)});
        vec3 gradientColor = vec3(${gradientColor.r.toFixed(3)}, ${gradientColor.g.toFixed(3)}, ${gradientColor.b.toFixed(3)});
        
        material.diffuse = mix(baseColor, gradientColor, heightRatio);

        if (height >= ${baseHeight}.0) {
          float _baseHeight = ${baseHeight}.0;
          float _heightRange = ${heightRange}.0;
          float _glowRange = ${glowRange.toFixed(2)};

          float vtxf_height = height - _baseHeight;
          float vtxf_a11 = fract(czm_frameNumber / 360.0) * 3.14159265 * 2.0;
          float vtxf_a12 = vtxf_height / _heightRange + sin(vtxf_a11) * 0.1;
          material.diffuse *= vec3(vtxf_a12, vtxf_a12, vtxf_a12);

          float vtxf_a13 = fract(czm_frameNumber / 360.0);
          float vtxf_h = clamp(vtxf_height / _glowRange, 0.0, 1.0);
          vtxf_a13 = abs(vtxf_a13 - 0.5) * 2.0;
          float vtxf_diff = step(0.01, abs(vtxf_h - vtxf_a13));
          material.diffuse += material.diffuse * (1.0 - vtxf_diff);
        }
      }
    `
  });
  tileset.customShader = shader;
  customShader = shader;
};

const customShader3 = () => {
  activeShader.value = 3;
  clearShader();
};

const clearShader = () => {
  if (tileset) {
    tileset.customShader = undefined;
  }
  customShader = null;
};

const destroyCesium = () => {
  clearShader();

  if (tileset && window.cesiumViewer) {
    window.cesiumViewer.scene.primitives.remove(tileset);
    tileset = null;
  }

  if (window.cesiumViewer) {
    window.cesiumViewer.destroy();
    window.cesiumViewer = null;
  }
};

onMounted(() => {
  if (cesiumContainer.value) {
    initCesium();
  }
});

onUnmounted(() => {
  destroyCesium();
});
</script>

<style scoped>
.app-container {
  position: relative;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  font-family: 'Segoe UI', 'Poppins', 'Roboto', sans-serif;
}

.cesium-container {
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}

.top-left-controls {
  position: absolute;
  z-index: 1000;
  left: 20px;
  top: 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.color-control-group {
  display: flex;
  gap: 16px;
  background: rgba(15, 25, 45, 0.4);
  backdrop-filter: blur(8px);
  padding: 8px 16px;
  border-radius: 60px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
}

.color-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.color-label {
  font-size: 12px;
  color: rgba(255, 255, 255, 0.8);
  letter-spacing: 0.5px;
}

.color-input {
  width: 32px;
  height: 32px;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 50%;
  cursor: pointer;
  background: transparent;
  padding: 0;
  outline: none;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  transition: transform 0.2s ease;
}

.color-input::-webkit-color-swatch-wrapper {
  padding: 0;
}

.color-input::-webkit-color-swatch {
  border: none;
  border-radius: 50%;
}

.color-input:hover {
  transform: scale(1.05);
}

.liquid-btn-group {
  display: flex;
  gap: 16px;
  background: rgba(15, 25, 45, 0.3);
  backdrop-filter: blur(8px);
  padding: 8px 16px;
  border-radius: 60px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}

.liquid-btn {
  position: relative;
  padding: 8px 24px;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.5px;
  color: rgba(255, 255, 255, 0.9);
  background: transparent;
  border: none;
  border-radius: 40px;
  cursor: pointer;
  overflow: hidden;
  transition: all 0.35s cubic-bezier(0.2, 0.9, 0.4, 1.1);
  backdrop-filter: blur(4px);
  font-family: inherit;
}

.liquid-btn span {
  position: relative;
  z-index: 2;
}

.liquid-btn::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: linear-gradient(120deg, 
    rgba(64, 128, 255, 0.2) 0%,
    rgba(100, 180, 255, 0.3) 30%,
    rgba(64, 128, 255, 0.2) 60%,
    rgba(40, 100, 210, 0.2) 100%);
  border-radius: 40px;
  opacity: 0.6;
  transition: opacity 0.4s ease;
  z-index: 1;
}

.liquid-btn:hover::before {
  animation: liquidFlow 1.2s ease-in-out infinite;
  background: linear-gradient(120deg, 
    rgba(80, 150, 255, 0.4) 0%,
    rgba(120, 200, 255, 0.5) 30%,
    rgba(80, 150, 255, 0.4) 60%,
    rgba(60, 120, 230, 0.3) 100%);
  background-size: 200% 100%;
}

@keyframes liquidFlow {
  0% {
    background-position: 0% 50%;
    opacity: 0.5;
  }
  50% {
    background-position: 100% 50%;
    opacity: 0.8;
  }
  100% {
    background-position: 0% 50%;
    opacity: 0.5;
  }
}

.liquid-btn.active {
  color: white;
  background: rgba(64, 128, 255, 0.3);
  box-shadow: 0 0 12px rgba(64, 128, 255, 0.4), inset 0 1px 1px rgba(255, 255, 255, 0.2);
  border: 1px solid rgba(64, 128, 255, 0.6);
}

.liquid-btn.active::before {
  background: linear-gradient(120deg, 
    rgba(64, 128, 255, 0.5) 0%,
    rgba(100, 180, 255, 0.7) 30%,
    rgba(64, 128, 255, 0.5) 60%,
    rgba(40, 100, 210, 0.4) 100%);
  opacity: 0.9;
  animation: liquidFlow 1.8s ease-in-out infinite;
}

.liquid-btn:not(.active):hover {
  background: rgba(64, 128, 255, 0.2);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}

.liquid-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px rgba(64, 128, 255, 0.5), 0 0 0 4px rgba(0, 0, 0, 0.2);
}
</style>
相关推荐
酉鬼女又兒2 小时前
零基础快速入门前端蓝桥杯Web备考:BOM与定时器核心知识点详解(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·javascript·职场和发展·蓝桥杯
ThridTianFuStreet小貂蝉2 小时前
面试题1:请系统讲讲 Vue2 与 Vue3 的核心差异(响应式、API 设计、性能与编译器)。
前端·javascript·vue.js
俊劫2 小时前
AI Harness - 2026 AI 工程新范式
前端·openai·ai编程
前端付豪2 小时前
Prompt Playground(实现提示词工作台)
前端·人工智能·后端
竹林8182 小时前
在NFT项目中集成IPFS:从Pinata上传到前端展示的完整实战与踩坑
前端·javascript
取名不易2 小时前
canves实现画布
前端
AlkaidSTART2 小时前
深入浅出 React Hooks 原理:从 Fiber 的 memoizedState 链表讲到 updateQueue 调度
前端
一颗小行星2 小时前
Harness Engineering 前端开发的下一个阶段
前端·ai编程
踩着两条虫2 小时前
重磅!这款AI低代码平台火了:拖拽生成应用,一键输出Web/H5/UniApp
前端·低代码·ai编程