vue3实现地图考勤打卡Leaflet 地图库结合 Leaflet Draw 插件

Leaflet 地图库官方网站:https://leafletjs.cn/index.html

下面是一个后台管理的页面代码,里面包含了一些Leaflet 地图使用

javascript 复制代码
<template>
  <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" :close-on-click-modal="false"
    @close="handleClose">
    <div class="dialog-content-wrapper">
      <el-form :model="formData" label-position="top" ref="ruleFormRef" :rules="rules">
        <!-- 考勤组名称 -->
        <el-form-item label="考勤组名称" required prop="name">
          <el-input v-model="formData.name" placeholder="请输入用户名" clearable />
        </el-form-item>

        <!-- 考勤人员 -->
        <el-form-item label="考勤人员">
          <el-cascader :options="options_data" v-model="formData.userIdList" :show-all-levels="false"
            :props="cascaderProps" placeholder="请选择人员" clearable collapse-tags collapse-tags-tooltip
            :max-collapse-tags="5" style="width: 100%" filterable />
        </el-form-item>

        <!-- 考勤班次 -->
        <el-form-item label="考勤班次" prop="shiftId">
          <el-select v-model="formData.shiftId" placeholder="请选择班次" style="width: 100%" clearable><el-option
              v-for="item in shiftOptions" :key="item.id" :label="item.name" :value="item.id" />
          </el-select>
        </el-form-item>

        <!-- 考勤日期 -->
        <el-form-item label="考勤日期">
          <el-checkbox-group v-model="formData.week">
            <el-checkbox :value="1">周一</el-checkbox>
            <el-checkbox :value="2">周二</el-checkbox>
            <el-checkbox :value="3">周三</el-checkbox>
            <el-checkbox :value="4">周四</el-checkbox>
            <el-checkbox :value="5">周五</el-checkbox>
            <el-checkbox :value="6">周六</el-checkbox>
            <el-checkbox :value="7">周日</el-checkbox>
          </el-checkbox-group>
        </el-form-item>

        <!-- 自动排休 -->
        <el-form-item label="自动排休">
          <el-switch v-model="formData.isLegal" :active-value="1" :inactive-value="0" />
          <span style="margin-left: 10px">是否法定假日自动排休</span>
        </el-form-item>

        <!-- 打卡地点 -->
        <el-form-item label="打卡地点" prop="geojson">
          <!-- <el-input clearable
          v-model="formData.geojson"
          placeholder="请输入打卡地点"
          style="width: 100%"
        /> -->
          <el-button type="primary" size="small" @click="openMap">
            选择打卡地点
          </el-button>
        </el-form-item>

        <!-- 特殊时间配置 -->
        <el-form-item>
          <el-collapse style="width: 100%">
            <el-collapse-item title="特殊时间配置">
              <!-- 必须打卡时间 -->
              <div class="time-config-item">
                <h4>必须打卡时间:</h4>
                <div v-if="formData.checkDate.length === 0" class="empty-tip">
                  请添加
                </div>
                <el-button type="primary" size="small" @click="addCheckDate">+ 新增</el-button>
                <div v-for="(time, index) in formData.checkDate" :key="index" class="time-item">
                  <!-- 这里可以根据实际需求添加时间选择器 -->
                  <el-date-picker clearable v-model="formData.checkDate[index]" format="YYYY/MM/DD"
                    value-format="YYYY-MM-DD" type="date" placeholder="请选择时间"
                    style="width: 200px; margin-right: 10px" />
                  <el-button type="danger" size="small" @click="removeCheckDate(index)">删除</el-button>
                </div>
              </div>

              <!-- 无需打卡时间 -->
              <div class="time-config-item">
                <h4>无需打卡时间:</h4>
                <div v-if="formData.noCheckDate.length === 0" class="empty-tip">
                  请添加
                </div>
                <el-button type="primary" size="small" @click="addNoCheckDate">+ 新增</el-button>
                <div v-for="(time, index) in formData.noCheckDate" :key="index" class="time-item">
                  <!-- 这里可以根据实际需求添加时间选择器 -->
                  <!-- <el-input
                  v-model="formData.noCheckDate[index]"
                  placeholder="请选择时间"
                  style="width: 200px; margin-right: 10px"
                /> -->
                  <el-date-picker clearable v-model="formData.noCheckDate[index]" format="YYYY/MM/DD"
                    value-format="YYYY-MM-DD" type="date" placeholder="请选择时间"
                    style="width: 200px; margin-right: 10px" />
                  <el-button type="danger" size="small" @click="removeNoCheckDate(index)">删除</el-button>
                </div>
              </div>
            </el-collapse-item>
          </el-collapse>
        </el-form-item>
      </el-form>
    </div>

    <template #footer>
      <span class="dialog-footer">
        <el-button @click="handleClose">取消</el-button>
        <el-button type="primary" @click="handleConfirm(ruleFormRef)">确认</el-button>
      </span>
    </template>

    <el-dialog v-model="mapVisible" width="800px" height="600px" title="选择打卡地点" append-to-body show-close="false"
      close-on-click-modal="false" :close-on-press-escape="false">
      <!-- 用leaflet绘制打卡地点 -->
      <div ref="mapContainer" style="width: 100%; height: 600px;"></div>
      <div style="margin-top: 10px; text-align: center;">
        <el-button size="small" @click="closeMap">关闭地图</el-button>
        <el-button type="primary" size="small" @click="saveGeoJson">保存区域</el-button>
      </div>
    </el-dialog>

  </el-dialog>
</template>

<script setup>
import { onMounted, ref, watch, reactive, computed, onUnmounted } from "vue";
import { getTree, getBC } from "@/api/Attendance/Group";
import { ElMessage } from 'element-plus';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import 'leaflet-draw/dist/leaflet.draw.css';
import 'leaflet-draw';
import zhCN from './zn-CN.js';

const ruleFormRef = ref();
const rules = reactive({
  name: [{ required: true, message: "请输入考勤组名称", trigger: "blur" }],
  shiftId: [{ required: true, message: "请选择考勤班次", trigger: "change" }],
  geojson: [{ required: true, message: "请选择打卡区域", trigger: "blur" }],
});
const shiftOptions = ref([]);

const mapVisible = ref(false);
const mapContainer = ref(null);
let map = null;
let drawnItems = null;
let drawControl = null;

const openMap = () => {
  // 打开地图选择器
  mapVisible.value = true;
  // 延迟初始化地图,确保DOM已渲染
  setTimeout(() => {
    initMap();
  }, 100);
};

// 初始化地图
const initMap = () => {
  // 如果地图已存在,先销毁
  if (map) {
    map.remove();
  }

  // 应用中文语言包
  L.drawLocal = zhCN;

  // 创建地图实例
  map = L.map(mapContainer.value).setView([39.87298, 116.35479], 17); // 默认北京坐标

  const defaultZoomControl = map.zoomControl;
  // 修改控件配置(仅修改配置,不会自动更新DOM)
  defaultZoomControl.options.zoomInText = '放大';
  defaultZoomControl.options.zoomOutText = '缩小';
  defaultZoomControl.options.zoomInTitle = '点击放大地图';
  defaultZoomControl.options.zoomOutTitle = '点击缩小地图';
  // 6250b3c72eabffd9b2fc97e8f622c344
  // 高德地图普通瓦片
  // L.tileLayer('https://webst02.is.autonavi.com/appmaptile?style=7&x={x}&y={y}&z={z}', {
  //   attribution: '&copy; <a href="https://amap.com">高德地图</a>'
  // }).addTo(map);

  L.tileLayer('http://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=6250b3c72eabffd9b2fc97e8f622c344', {
    attribution: '&copy; <a href="https://www.tianditu.gov.cn/">天地图</a>', // 保留天地图版权信息
    maxZoom: 18, // 天地图最大缩放级别(部分图层支持到19)
    minZoom: 1,
    tileSize: 256 // 瓦片尺寸,固定256
  }).addTo(map);
  L.tileLayer('http://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=6250b3c72eabffd9b2fc97e8f622c344', {
    attribution: '&copy; <a href="https://www.tianditu.gov.cn/">天地图</a>',
    maxZoom: 18,
    minZoom: 1,
    tileSize: 256
  }).addTo(map);
  
  // 创建绘制图层组
  drawnItems = L.featureGroup().addTo(map);

  if (props.editData.geojson) {
    // 解析geojson字符串为对象
    const geojsonObj = props.editData.geojson;
    // 确保是MultiPolygon类型
    if (geojsonObj.type === 'Polygon') {
      // 提取第一个多边形的坐标
      const firstPolygon = geojsonObj.coordinates[0].map(coord => [coord[1], coord[0]]);
      // 创建Leaflet多边形
      L.polygon(firstPolygon, {
        color: '#3388ff', // 显式设置样式,确保可见
        fillColor: '#3388ff',
        fillOpacity: 0.2
      }).addTo(drawnItems);
    }
  }
  // 配置绘制控件
  drawControl = new L.Control.Draw({
    draw: {
      polygon: {
        allowIntersection: false, // 不允许交叉
        showArea: false, // 显示面积
        metric: true, // 使用公制单位
      },
      marker: false,
      circlemarker: false,
      polyline: false,
      rectangle: false,
      circle: false,
    },
    edit: {
      featureGroup: drawnItems,
      remove: true
    }
  });

  // 添加绘制控件到地图
  map.addControl(drawControl);

  // 监听绘制完成事件
  map.on(L.Draw.Event.CREATED, (e) => {
    const layer = e.layer;
    drawnItems.addLayer(layer);
  });
};

// 关闭地图
const closeMap = () => {
  // 清除绘制的图层
  drawnItems.clearLayers();
  // 关闭弹窗
  mapVisible.value = false;
};
// 保存GeoJSON
const saveGeoJson = () => {
  const geoJsonData = drawnItems.toGeoJSON();

  if (geoJsonData.features.length === 0) {
    ElMessage.warning('请先绘制打卡区域');
    return;
  }

  // 只保存第一个绘制的多边形
  const polygonGeoJson = geoJsonData.features[0].geometry;

  // 保存到表单
  formData.value.geojson = polygonGeoJson;


  ElMessage.success('打卡区域已保存');
  mapVisible.value = false;
};

// 组件卸载时清理地图
onUnmounted(() => {
  if (map) {
    map.remove();
  }
});
onMounted(() => {
  getBC({
    week: [1, 2, 3, 4, 5, 6, 7],
    isLegal: 0,
    checkDate: [],
    noCheckDate: [],
  }).then((res) => {
    shiftOptions.value = res.data.list || [];
  });
});
// 定义事件
const emit = defineEmits(["confirm", "close"]);

// 接收props
const props = defineProps({
  // 对话框类型:add-添加,edit-编辑
  dialogType: {
    type: String,
    default: "add"
  },
  // 编辑数据
  editData: {
    type: Object,
    default: () => ({})
  }
});

// 弹窗可见性
const dialogVisible = ref(true);

// 动态标题
const dialogTitle = computed(() => {
  return props.dialogType === "add" ? "添加考勤组" : "编辑考勤组";
});

// 表单数据
const formData = ref({
  name: "", // 考勤组名称
  userIdList: [], // 考勤人员
  shiftId: "", // 考勤班次
  week: [1, 2, 3, 4, 5], // 考勤日期,默认选中周一到周五
  isLegal: 1, // 自动排休,1:开启排休,0:关闭排休
  geojson: "", // 打卡地点
  checkDate: [], // 必须打卡时间
  noCheckDate: [], // 无需打卡时间
  id: "" // 考勤组ID,编辑时需要
});

// 监听编辑数据变化,初始化表单
watch(() => props.editData, (newData) => {
  if (props.dialogType === "edit" && newData) {
    // 复制编辑数据到表单,并进行类型转换
    formData.value = {
      ...newData,
      // 确保数组类型,使用深拷贝避免修改原数据
      userIdList: [...(newData.userIdList || [])],
      week: [...(newData.week || [1, 2, 3, 4, 5])],
      checkDate: [...(newData.checkDate || [])],
      noCheckDate: [...(newData.noCheckDate || [])],
      // 保持GeoJSON对象格式
      geojson: newData.geojson || "",
      // 如果存在shiftInfo,则使用shiftInfo.id作为shiftId
      shiftId: newData.shiftInfo?.id || ""
    };

    // console.log("编辑数据初始化:", formData.value);
  }
}, { immediate: true, deep: true });

// 监听地图可见性变化,当地图关闭时清理
watch(() => mapVisible.value, (newVal) => {
  if (!newVal && map) {
    map.remove();
    map = null;
  }
});

// 级联选择器配置
const cascaderProps = ref({
  multiple: true,
  emitPath: false,
  checkStrictly: false, // 关闭严格父子节点不关联,开启级联关系
  // 关闭懒加载,改为一次性加载所有数据
  lazy: false,
  // 数据源配置
  data: []
});
const options_data = ref([])
// 加载部门和人员数据
const loadUserData = async () => {
  try {
    // 递归加载所有部门和人员数据
    const loadTree = async (deptId = 0) => {
      const result = await getTree({ deptId, type: "user" });
      const nodes = [];
      for (const item of result.data) {
        const isUser = item.type === "user";
        const node = {
          value: item.id,
          label: item.name,
          leaf: isUser,
          type: item.type
        };
        // 如果是部门,递归加载子节点
        if (!isUser) {
          node.children = await loadTree(item.id);
        }
        nodes.push(node);
      }
      return nodes;
    };

    // 加载根节点数据
    const allData = await loadTree();
    options_data.value = allData;
    console.log("加载的所有用户数据:", allData);
  } catch (err) {
    console.error("加载用户数据失败:", err);
    options_data.value = [];
  }
};

// 组件挂载时加载数据
onMounted(() => {
  loadUserData();
  getBC({
    week: [1, 2, 3, 4, 5, 6, 7],
    isLegal: 0,
    checkDate: [],
    noCheckDate: [],
  }).then((res) => {
    shiftOptions.value = res.data.list || [];
  });
});

// 添加必须打卡时间
const addCheckDate = () => {
  formData.value.checkDate.push("");
};

// 删除必须打卡时间
const removeCheckDate = (index) => {
  formData.value.checkDate.splice(index, 1);
};

// 添加无需打卡时间
const addNoCheckDate = () => {
  formData.value.noCheckDate.push("");
};

// 删除无需打卡时间
const removeNoCheckDate = (index) => {
  formData.value.noCheckDate.splice(index, 1);
};

// 确认添加
const handleConfirm = async (formEl) => {
  if (!formEl) return;
  await formEl.validate((valid, fields) => {
    if (valid) {
      console.log(formData.value, "-----formData.value--");
      emit("confirm", formData.value);
      // 重置表单
      resetForm();
    } else {
      console.log("error submit!", fields);
    }
  });
};
// 关闭弹窗
const handleClose = () => {
  emit("close");
  dialogVisible.value = false;
  // 重置表单
  resetForm();
};

// 重置表单
const resetForm = () => {
  formData.value = {
    name: "", // 考勤组名称
    userIdList: [], // 考勤人员
    shiftId: "", // 考勤班次
    week: [1, 2, 3, 4, 5], // 考勤日期,默认选中周一到周五
    isLegal: 1, // 自动排休,1:开启排休,0:关闭排休
    geojson: "", // 打卡地点
    checkDate: [], // 必须打卡时间
    noCheckDate: [], // 无需打卡时间
  };
};
</script>

<style lang='scss' scoped>
/* 弹窗内容包裹层样式 */
.dialog-content-wrapper {
  max-height: 450px;
  overflow-y: auto;
  height: 450px;
  box-sizing: border-box;
  overflow-x: hidden;
  padding-right: 5px;
}

/* 重置dialog body样式 */
:deep(.el-dialog__body) {
  padding-bottom: 0 !important;
  padding-right: 0 !important;
}

:deep(.el-form-item) {
  margin-bottom: 20px;
}

:deep(.el-checkbox-group) {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

/* 特殊时间配置样式 */
.time-config-item {
  margin-bottom: 20px;
}

.time-config-item h4 {
  margin: 0 0 10px 0;
  font-size: 14px;
  font-weight: bold;
}

.empty-tip {
  color: #909399;
  margin: 10px 0;
}

.time-item {
  margin: 10px 0;
  display: flex;
  align-items: center;
}

/* 折叠面板样式 */
:deep(.el-collapse-item__header) {
  padding: 10px 0;
  border-bottom: none;
}

:deep(.el-collapse-item__content) {
  padding: 10px 0;
}
</style>

这段代码中打卡地图功能的具体实现方式,核心是基于 Leaflet 地图库结合 Leaflet Draw 插件实现多边形绘制,并将绘制结果以 GeoJSON 格式保存到表单中。

一、核心实现思路

整个打卡地图功能分为 5 个核心步骤:

  1. 引入地图相关依赖(Leaflet 核心库 + 绘制插件)
  2. 点击「选择打卡地点」按钮打开地图弹窗并初始化地图
  3. 配置地图图层(天地图瓦片)和绘制控件(仅允许绘制多边形)
  4. 监听绘制事件,将用户绘制的多边形保存到图层组
  5. 点击「保存区域」将绘制结果转为 GeoJSON 格式并赋值给表单

二、关键代码拆解

  1. 依赖引入(前置条件)
    首先需要确保安装了相关依赖(如果是 npm 环境)
javascript 复制代码
# 安装核心依赖
npm install leaflet leaflet-draw

代码中通过 import 引入核心库和样式:

javascript 复制代码
import L from 'leaflet';
import 'leaflet/dist/leaflet.css'; // 地图基础样式
import 'leaflet-draw/dist/leaflet.draw.css'; // 绘制插件样式
import 'leaflet-draw'; // 绘制插件核心
  1. 地图初始化(initMap 函数)
    这是地图功能的核心函数,负责创建地图实例、加载图层、配置绘制功能:
javascript 复制代码
const initMap = () => {
  // 1. 销毁已有地图,避免重复创建
  if (map) {
    map.remove();
  }

  // 2. 创建地图实例,绑定到 DOM 元素(mapContainer),设置默认中心点和缩放级别
  map = L.map(mapContainer.value).setView([39.87298, 116.35479], 17); // 默认北京坐标

  // 3. 加载天地图瓦片图层(底图 + 注记层)
  // 天地图矢量底图
  L.tileLayer('http://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=你的密钥', {
    attribution: '&copy; <a href="https://www.tianditu.gov.cn/">天地图</a>',
    maxZoom: 18,
    minZoom: 1,
    tileSize: 256
  }).addTo(map);
  // 天地图注记层(显示地名)
  L.tileLayer('http://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=你的密钥', {
    attribution: '&copy; <a href="https://www.tianditu.gov.cn/">天地图</a>',
    maxZoom: 18,
    minZoom: 1,
    tileSize: 256
  }).addTo(map);
  
  // 4. 创建绘制图层组(用于存放用户绘制的多边形)
  drawnItems = L.featureGroup().addTo(map);

  // 5. 编辑模式下,加载已保存的 GeoJSON 并渲染到地图
  if (props.editData.geojson) {
    const geojsonObj = props.editData.geojson;
    if (geojsonObj.type === 'Polygon') {
      // 转换坐标格式(Leaflet 是 [纬度, 经度],GeoJSON 是 [经度, 纬度])
      const firstPolygon = geojsonObj.coordinates[0].map(coord => [coord[1], coord[0]]);
      // 绘制已保存的多边形
      L.polygon(firstPolygon, {
        color: '#3388ff',
        fillColor: '#3388ff',
        fillOpacity: 0.2
      }).addTo(drawnItems);
    }
  }

  // 6. 配置绘制控件(仅允许绘制多边形,禁用其他类型)
  drawControl = new L.Control.Draw({
    draw: {
      polygon: {
        allowIntersection: false, // 不允许多边形交叉
        showArea: false,
        metric: true,
      },
      marker: false, // 禁用标记
      circlemarker: false,
      polyline: false, // 禁用折线
      rectangle: false, // 禁用矩形
      circle: false, // 禁用圆形
    },
    edit: {
      featureGroup: drawnItems, // 可编辑的图层组
      remove: true // 允许删除
    }
  });

  // 7. 添加绘制控件到地图
  map.addControl(drawControl);

  // 8. 监听绘制完成事件,将绘制的多边形添加到图层组
  map.on(L.Draw.Event.CREATED, (e) => {
    const layer = e.layer;
    drawnItems.addLayer(layer);
  });
};
  1. 核心交互函数
    (1)打开地图弹窗
javascript 复制代码
const openMap = () => {
  mapVisible.value = true;
  // 延迟初始化地图,确保弹窗 DOM 已渲染
  setTimeout(() => {
    initMap();
  }, 100);
};

(2)保存绘制的区域(转为 GeoJSON)

javascript 复制代码
const saveGeoJson = () => {
  // 1. 将绘制的图层转为 GeoJSON 格式
  const geoJsonData = drawnItems.toGeoJSON();

  // 2. 校验是否有绘制内容
  if (geoJsonData.features.length === 0) {
    ElMessage.warning('请先绘制打卡区域');
    return;
  }

  // 3. 只取第一个绘制的多边形
  const polygonGeoJson = geoJsonData.features[0].geometry;

  // 4. 保存到表单
  formData.value.geojson = polygonGeoJson;

  ElMessage.success('打卡区域已保存');
  mapVisible.value = false;
};

(3)清理地图资源

javascript 复制代码
// 组件卸载时清理地图,避免内存泄漏
onUnmounted(() => {
  if (map) {
    map.remove();
  }
});

// 关闭地图弹窗时清理图层
const closeMap = () => {
  drawnItems.clearLayers();
  mapVisible.value = false;
};

三、关键技术点说明

1,坐标格式转换:

  • Leaflet 地图使用 [纬度, 经度] 格式
  • GeoJSON 标准使用 [经度, 纬度] 格式
  • 编辑模式下加载已有数据时,需要执行 coord => [coord[1], coord[0]] 转换

2,天地图瓦片使用

  • 代码中使用了天地图的矢量底图(vec)和注记层(cva)
  • 需要替换 tk 参数为你自己的天地图密钥(可从天地图官网申请)

3,绘制控件配置

  • 仅启用 polygon(多边形)绘制,禁用了 marker、折线、矩形等其他类型
  • 支持编辑和删除已绘制的多边形

4,GeoJSON 数据保存

  • 最终保存到表单的是纯几何信息(Geometry),格式符合 GeoJSON 标准
  • 后端可直接解析该格式进行空间计算

总结

  1. 核心依赖:基于 Leaflet 地图库 + Leaflet Draw 绘制插件实现多边形绘制
  2. 核心流程:打开地图弹窗 → 初始化地图(加载天地图瓦片)→ 绘制多边形 → 转为 GeoJSON → 保存到表单
  3. 关键细节:坐标格式转换(纬度 / 经度 与 经度 / 纬度)、编辑模式下加载已有多边形、组件卸载时清理地图资源避免内存泄漏

这个实现方案的优势是轻量、开源、跨浏览器兼容,且 GeoJSON 是通用的空间数据格式,便于和后端交互。

相关推荐
OEC小胖胖13 分钟前
05|从 `SuspenseException` 到 `retryTimedOutBoundary`:Suspense 的 Ping 与 Retry 机制
前端·前端框架·react·开源库
Eadia2 小时前
React基础框架搭建10-webpack配置:react+router+redux+axios+Tailwind+webpack
react.js·架构·前端框架
ProgramHan4 小时前
React 19 新特性深度解析:告别 useEffect 的时代
前端·react.js·前端框架
晴殇i18 小时前
package.json 中的 dependencies 与 devDependencies:深度解析
前端·设计模式·前端框架
IT=>小脑虎20 小时前
2026版 React 零基础小白进阶知识点【衔接基础·企业级实战】
前端·react.js·前端框架
IT=>小脑虎20 小时前
2026版 React 零基础小白入门知识点【基础完整版】
前端·react.js·前端框架
文心快码BaiduComate21 小时前
我用文心快码开发了一款「积木工坊」:用AI让每个孩子都成为小小建筑师
前端·前端框架
OEC小胖胖1 天前
03|从 `ensureRootIsScheduled` 到 `commitRoot`:React 工作循环(WorkLoop)全景
前端·react.js·前端框架
KlayPeter1 天前
前端数据存储全解析:localStorage、sessionStorage 与 Cookie
开发语言·前端·javascript·vue.js·缓存·前端框架