实现可拖拽和调整大小的DIV组件,并限制在父容器内

实现可拖拽和调整大小的div组件,并限制在父容器内

本文详细介绍了如何使用纯JavaScript创建可拖拽和调整大小的HTML DIV元素,并确保这些元素在操作过程中始终被限制在一个指定的父容器内部,避免溢出。教程涵盖了HTML结构、CSS样式以及核心JavaScript逻辑,包括事件监听、坐标计算、边界检测和状态管理,旨在提供一个结构清晰、功能完善的交互式组件实现方案。

引言:构建交互式前端组件

在现代Web应用开发中,创建用户界面(UI)元素,使其能够被用户自由拖拽和调整大小,是提升交互体验的关键。例如,仪表盘中的小部件、可移动的对话框或布局管理器。然而,仅仅实现拖拽和调整大小功能是不够的,我们还需要确保这些交互式元素不会超出其预定的父容器边界,以保持界面的整洁和可用性。本文将深入探讨如何使用原生JavaScript实现这一功能,并提供一个健壮且易于理解的解决方案。

核心概念:拖拽与调整大小

拖拽和调整大小功能的核心在于响应鼠标事件并实时更新元素的位置和尺寸。

事件监听

mousedown:当用户按下鼠标按钮时触发,用于记录初始位置,并准备开始拖拽或调整大小操作。mousemove:当鼠标指针在元素上移动时触发(在mousedown之后,mouseup之前),用于实时计算元素的新位置或新尺寸。mouseup:当用户释放鼠标按钮时触发,用于结束拖拽或调整大小操作,并清理事件监听器。

位置与尺寸计算

拖拽:通过计算鼠标当前位置与初始点击位置的偏移量,加上元素初始的left和top值,来确定元素的新left和top样式。调整大小:通过计算鼠标当前位置与初始点击位置的偏移量,加上元素初始的width和height值,来确定元素的新width和height样式。

CSS定位:为了能够自由地通过JavaScript控制元素的位置,我们需要将可拖拽/调整大小的元素设置为position: absolute;,并将其父容器设置为position: relative;或position: absolute;,以便子元素相对于父容器进行定位。

限制在父容器内:边界管理

为了防止子元素溢出父容器,我们需要在每次更新元素位置或尺寸时进行边界检查。

移动边界限制

在更新元素的left和top值之前,我们需要检查计算出的新位置是否会导致元素超出父容器的左、上、右、下边界。

左边界:x 上边界:y 右边界:x + draggable.offsetWidth > container.offsetWidth,如果为真,则将x设为container.offsetWidth – draggable.offsetWidth。下边界:y + draggable.offsetHeight > container.offsetHeight,如果为真,则将y设为container.offsetHeight – draggable.offsetHeight。

调整大小边界限制

在更新元素的width和height值之前,同样需要进行边界检查。调整大小时,元素的左上角位置通常是固定的,因此我们主要关注右下角是否超出父容器。

右边界:draggable.offsetLeft + newWidth > container.offsetWidth,如果为真,则将newWidth设为container.offsetWidth – draggable.offsetLeft。下边界:draggable.offsetTop + newHeight > container.offsetHeight,如果为真,则将newHeight设为container.offsetHeight – draggable.offsetTop。此外,还应考虑元素的min-width和min-height,确保其不会缩小到不可见的程度。

HTML结构:容器与可交互元素

为了实现拖拽和调整大小功能,我们需要一个父容器来限制子元素,以及多个具有拖拽手柄和调整大小手柄的子元素。

可拖拽区域
非拖拽内容
可拖拽区域
非拖拽内容

.container:作为所有可拖拽/调整大小元素的父容器,它定义了边界。.draggable:表示一个可拖拽和调整大小的组件。left和top样式用于初始定位。.move:这是拖拽手柄,用户点击并拖动此区域来移动整个.draggable元素。.resize:这是调整大小手柄,用户点击并拖动此区域来改变.draggable元素的尺寸。

CSS样式:美化与功能实现

CSS样式不仅提供了视觉效果,还为拖拽和调整大小功能提供了必要的布局基础,例如position: absolute和cursor样式。

html,body{  height:100%;  margin:0;  padding:0;}*{  box-sizing: border-box; /* 确保padding和border不增加元素总尺寸 */}.draggable{  position: absolute; /* 绝对定位,便于通过JS控制位置 */  padding:45px 15px 15px 15px; /* 为内容留出空间,并避免与手柄重叠 */  border-radius:4px;  background:#ddd;  user-select: none; /* 防止拖拽时选中文字 */  left: 15px;  top: 15px;  min-width:200px; /* 最小宽度 */  min-height: 100px; /* 最小高度 (根据内容和padding调整) */  z-index: 9; /* 初始z-index */}.draggable>.move{  line-height: 30px;  padding: 0 15px;  background:#bbb;  border-bottom: 1px solid #777;  cursor:move; /* 拖拽手柄鼠标样式 */  position:absolute;  left:0;  top:0;  height:30px;  width:100%;  border-radius: 4px 4px 0 0;}.draggable>.resize{  cursor:nw-resize; /* 调整大小手柄鼠标样式 */  position:absolute;  right:0;  bottom:0;  height:16px;  width:16px;  border-radius: 0 0 4px 0;  background: linear-gradient(to left top, #777 50%, transparent 50%); /* 视觉上的调整大小手柄 */}.container{  left:15px;  top:15px;  background: #111;  border-radius:4px;  width:calc(100% - 30px);  height:calc(100% - 30px);  position: relative; /* 相对定位,作为draggable元素的参照 */  overflow: hidden; /* 确保即使有bug,内容也不会溢出 */}

JavaScript实现:动态行为逻辑

JavaScript代码是实现拖拽、调整大小和边界限制的核心。我们将创建一个makeDraggableResizable函数来封装单个元素的行为,并使用Proxy来优雅地管理状态。

const container = document.querySelector('.container'); // 获取父容器// 获取所有可拖拽/调整大小的元素const draggables = document.querySelectorAll('.draggable');draggables.forEach(elem => {  makeDraggableResizable(elem); // 为每个元素初始化功能  // 鼠标按下时,将当前操作的元素Z-index提高,使其浮于其他元素之上  elem.addEventListener('mousedown', () => {     const maxZ = Math.max(...[...draggables].map(elem => parseInt(getComputedStyle(elem)['z-index']) || 0));     elem.style['z-index'] = maxZ + 1;  });});/** * 为给定的元素添加拖拽和调整大小功能,并限制在父容器内。 * @param {HTMLElement} draggable - 需要添加功能的元素。 */function makeDraggableResizable(draggable){  /**   * 移动元素。   * @param {number} x - 鼠标的当前X坐标。   * @param {number} y - 鼠标的当前Y坐标。   */  const move = (x, y) => {    // 计算新的left和top值    let newX = state.fromX + (x - state.startX);    let newY = state.fromY + (y - state.startY);    // 移动边界检查    if (newX  container.offsetWidth) newX = container.offsetWidth - draggable.offsetWidth; // 右边界    if (newY  container.offsetHeight) newY = container.offsetHeight - draggable.offsetHeight; // 下边界    draggable.style.left = newX + 'px';    draggable.style.top = newY + 'px';  };  /**   * 调整元素大小。   * @param {number} x - 鼠标的当前X坐标。   * @param {number} y - 鼠标的当前Y坐标。   */  const resize = (x, y) => {    // 计算新的width和height值    let newWidth = state.fromWidth + (x - state.startX);    let newHeight = state.fromHeight + (y - state.startY);    // 最小尺寸限制    const minWidth = parseInt(getComputedStyle(draggable).minWidth);    const minHeight = parseInt(getComputedStyle(draggable).minHeight);    if (newWidth < minWidth) newWidth = minWidth;    if (newHeight  container.offsetWidth) newWidth = container.offsetWidth - state.fromX; // 右边界    if (state.fromY + newHeight > container.offsetHeight ) newHeight = container.offsetHeight - state.fromY; // 下边界    draggable.style.width = newWidth + 'px';    draggable.style.height = newHeight + 'px';  };  /**   * 添加或移除全局事件监听器。   * @param {'add'|'remove'} op - 操作类型,'add'或'remove'。   */  const toggleGlobalListeners = (op = 'add') =>    Object.entries(globalListeners).slice(1) // 排除 mousedown,因为它在内部处理      .forEach(([name, listener]) => document[op + 'EventListener'](name, listener));  // 使用Proxy管理状态,当状态变化时自动执行相应的操作  const state = new Proxy({}, {    set(target, prop, val){      const out = Reflect.set(...arguments); // 执行默认的设置操作      const ops = {        startY: () => { // 鼠标按下时,初始化拖拽/调整大小的起始状态          toggleGlobalListeners(); // 添加全局mousemove和mouseup监听          const style = getComputedStyle(draggable);          // 记录元素当前的left, top, width, height          [target.fromX, target.fromY] = [parseInt(style.left), parseInt(style.top)];          [target.fromWidth, target.fromHeight] = [parseInt(style.width), parseInt(style.height)];        },        dragY: () => target.action(target.dragX, target.dragY), // 鼠标移动时,执行拖拽或调整大小操作        stopY: () => toggleGlobalListeners('remove') + target.action(target.stopX, target.stopY), // 鼠标松开时,移除全局监听并执行最后一次操作      };      // 使用Promise.resolve().then()将操作推迟为微任务,确保状态设置的顺序不影响操作执行      ops[prop] && Promise.resolve().then(ops[prop]);      return out;    }  });  // 全局事件监听器,用于捕获mousemove和mouseup事件  const globalListeners = {    mousedown: e => Object.assign(state, {startX: e.pageX, startY: e.pageY}), // 记录鼠标按下时的起始坐标    mousemove: e => Object.assign(state, {dragY: e.pageY, dragX: e.pageX}), // 记录鼠标移动时的当前坐标    mouseup: e => Object.assign(state, {stopX: e.pageX, stopY: e.pageY}), // 记录鼠标松开时的最终坐标  };  // 为拖拽手柄和调整大小手柄添加mousedown事件监听  for(const [name, action] of Object.entries({move, resize})){    draggable.querySelector(`.${name}`).addEventListener('mousedown', e => {      e.stopPropagation(); // 阻止事件冒泡,避免与父元素的mousedown冲突      state.action = action; // 设置当前要执行的操作(move或resize)      globalListeners.mousedown(e); // 调用mousedown处理函数,初始化state    });  }}

代码详解

draggables.forEach(…):遍历所有.draggable元素,为每个元素应用功能。mousedown事件用于在拖拽/调整大小开始时提升元素的z-index,确保当前操作的元素始终在最前面。makeDraggableResizable(draggable):这是核心函数,它接收一个.draggable元素作为参数。move(x, y):负责计算并设置元素的新left和top样式。关键在于边界检查:它确保newX和newY不会导致元素超出container的边界。resize(x, y):负责计算并设置元素的新width和height样式。除了边界检查,它还考虑了元素的min-width和min-height,防止元素过小。toggleGlobalListeners(op):一个辅助函数,用于在拖拽/调整大小开始时添加mousemove和mouseup的全局监听器,并在结束时移除它们。这样可以确保即使鼠标移出元素,操作也能继续进行,直到鼠标松开。state Proxy:这是一个巧妙的状态管理机制。当state对象的属性(如startY, dragY, stopY)被设置时,Proxy的set方法会自动触发。当startY被设置时(即鼠标按下),它会记录元素的初始位置和尺寸,并激活全局事件监听器。当dragY被设置时(即鼠标移动),它会调用当前设定的state.action(move或resize)来更新元素。当stopY被设置时(即鼠标松开),它会移除全局事件监听器,并执行最后一次操作。Promise.resolve().then(ops[prop]):这个模式将操作推迟到当前任务队列的末尾(作为微任务),确保state的所有相关属性都已设置完毕,才执行对应的操作,避免了因属性设置顺序导致的潜在问题。globalListeners:定义了mousedown, mousemove, mouseup的原始处理逻辑,主要用于更新state对象中的鼠标坐标。手柄事件监听:为.move和.resize元素添加mousedown监听器。当用户点击这些手柄时,会设置state.action来决定是拖拽还是调整大小,并调用globalListeners.mousedown来启动整个流程。e.stopPropagation()防止事件冒泡到父元素,避免不必要的行为触发。

注意事项与最佳实践

用户体验与反馈

Z-index管理:确保正在操作的元素始终位于其他元素之上,提供清晰的视觉焦点。鼠标样式:通过CSS的cursor属性为拖拽和调整大小手柄提供直观的鼠标指针样式。最小尺寸:为可调整大小的元素设置min-width和min-height,防止其缩小到无法操作或内容被遮挡。

性能考量

mousemove事件触发非常频繁,尤其是在快速移动鼠标时。对于复杂的计算或大量元素的场景,可以考虑使用节流(throttle)防抖(debounce)来限制mousemove事件处理函数的执行频率,以优化性能。然而,对于本例中单个元素的简单位置/尺寸更新,通常不需要额外优化。避免在mousemove处理函数中进行DOM查询或大量DOM操作,尽可能预先获取元素引用。

兼容性

触摸设备:为了支持触摸屏设备,除了mousedown/mousemove/mouseup,还需要监听touchstart/touchmove/touchend事件,并处理event.touches[0].pageX/pageY等触摸点信息。浏览器差异:getComputedStyle()在现代浏览器中广泛支持,但对于老旧浏览器可能需要polyfill。Proxy是ES6特性,如果需要支持IE浏览器,则需要Babel等工具进行转译。

代码结构

将拖拽和调整大小逻辑封装在独立的函数中,提高代码的可读性和可维护性。使用Proxy进行状态管理,使事件处理逻辑更加清晰和自动化。

总结

通过上述HTML、CSS和JavaScript的组合,我们成功实现了一个功能完善的交互式DIV组件,它不仅可以被用户自由拖拽和调整大小,还能智能地限制在指定的父容器内部,有效防止溢出。这种实现方式兼顾了用户体验、性能和代码结构,为构建更复杂的交互式Web界面提供了坚实的基础。通过理解和应用这些核心概念,开发者可以进一步扩展功能,例如多选拖拽、网格吸附等,以满足更高级的需求。

以上就是实现可拖拽和调整大小的DIV组件,并限制在父容器内的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1540185.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
依赖版本锁定策略_保证项目稳定性的方案
上一篇 2025年12月21日 12:14:40
MongoDB 聚合查询中实现多集合嵌套关联与数据类型转换
下一篇 2025年12月21日 12:15:01

相关推荐

  • win8怎么设置静态ip地址_win8静态ip地址设置步骤

    首先通过控制面板、命令提示符或高级网络设置配置静态IP和DNS,具体步骤包括打开网络共享中心、修改IPv4属性或使用netsh命令,最终保存设置并验证网络连接。 如果您尝试访问某个网站,但服务器无法访问,则可能是由于IP地址配置不当导致网络连接失败。以下是解决此问题的步骤: 本文运行环境:联想小新P…

    2026年8月30日
    000
  • 安装VC++2015提示0x80070666错误怎么办?全面修复方案来了

    安装VC++2015提示0x80070666错误怎么办?全面修复方案来了安装VC++2015提示0x80070666错误怎么办?全面修复方案来了安装VC++2015提示0x80070666错误怎么办?全面修复方案来了安装VC++2015提示0x80070666错误怎么办?全面修复方案来了

    在运行某些游戏、设计类软件或开发#%#$#%@%@%$#%$#%#%#$%@_20dc++e2c6fa909a5cd62526615fe2788a时,你可能经常会碰到一个令人头疼的问题:安装 microsoft visual c++ 2015 运行库时弹出错误代码 0x80070666,导致安装中断…

    2026年8月30日 用户投稿
    200
  • 金士顿NV3PCIe 4.0 NVMe固态硬盘推出全新规格

    2025年7月8日北京消息,知名存储品牌金士顿今日宣布,在原有2280规格基础上,正式发布全新的NV3PCIe 4.0 NVMe M.2 2230固态硬盘。该产品适用于笔记本、超薄本、迷你PC(NUC)以及掌机等多种设备类型,为那些需要高速度、低功耗,并且对主机内部空间要求较高的用户提供了一个新的升…

    2026年8月30日
    100
  • 小红书如何优化商品购买链接 小红书电商功能的使用技巧

    优化小红书商品购买链接的核心是打造从内容到交易的无缝信任旅程,需以优质内容为基础,将链接自然融入笔记、评论或直播中,避免生硬插入;2. 优先使用“薯店”实现平台内闭环交易,提升加载速度与用户信任,降低跳转流失;3. 利用“合集”功能整合主题内容,形成系统化购物指南,提升连带销售与用户停留时长;4. …

    2026年8月30日
    000
  • 如何为iPhoneSE2022获取固件?快速下载操作教程

    首先使用iTunes或Finder通过按住Shift(Windows)或Option(macOS)键点击“检查更新”来自动下载iPhone SE(2022年款)的官方固件;其次可借助爱思助手或3uTools等可信第三方工具选择并下载指定iOS版本固件;最后还可访问ipsw.me等可靠网站手动下载IP…

    2026年8月30日
    000
  • win10怎么截图_win10截图的快捷键与多种方法

    使用PrintScreen键可将全屏截图复制到剪贴板,再粘贴至画图等应用保存;2. 按Win+PrintScreen可自动将全屏截图以PNG格式保存至“图片/屏幕截图”文件夹;3. Alt+PrintScreen用于截取当前活动窗口并复制到剪贴板;4. Win+Shift+S调出系统截图工具栏,支持…

    2026年8月30日
    100
  • 悟空浏览器能看电视剧吗怎么看

    悟空浏览器确实可以免费看电视剧,无需额外开通会员,其内置聚合视频功能整合了多个平台资源,涵盖热播剧、经典剧、电影、综艺等,用户只需打开app点击“视频”或“影视”入口,或直接搜索剧名如“庆余年第二部”,即可选择播放源观看,操作简单适合各类用户,更新速度紧跟官方节奏,但播放时可能有广告,部分高清资源需…

    2026年8月30日
    000
  • 微信小程序离线表单提交:如何实现即使在无网络状态下也能提交表单?

    微信小程序离线表单:无网络也能提交 许多开发者都希望微信小程序具备离线功能,尤其在需要用户持续填写信息的场景,例如表单提交。本文将详细介绍如何在网络状况不佳或离线情况下,实现微信小程序表单的填写和提交。 核心问题在于:小程序能否在离线状态下工作并保存用户数据?答案是肯定的。本文将提供一个完整示例,包…

    2026年8月30日
    000
  • u盘重装系统后桌面文件如何恢复,重装系统后的u盘怎么恢复

    一、准备工作 在开始恢复桌面文件前,请准备好以下工具: 1. U盘:用于存放恢复软件及备份所需数据。 2. 数据恢复工具:建议使用知名软件EaseUS Data Recovery Wizard,它能有效找回误删或丢失的文件。 二、操作步骤 1. 将U盘连接至电脑,并安装好数据恢复软件。 2. 打开软…

    2026年8月30日
    000
  • win10注册表怎么打开_win10注册表编辑器打开方式

    1、通过Win+R输入regedit可快速打开注册表编辑器;2、开始菜单搜索“注册表编辑器”并以管理员身份运行;3、任务管理器中创建新任务regedit并勾选管理员权限;4、资源管理器进入System32找到regedit.exe运行;5、命令提示符或PowerShell输入regedit启动。 如…

    2026年8月30日
    000
  • 安徽研究未来产业时,为什么要请这一领域的“大V”?

    蛇年伊始,科普大咖袁岚峰教授便马不停蹄,不仅创作微博和短视频,参与多场科技直播,还奔赴上海电视台,主持全新科技访谈节目《锚点》。 与此同时,“科学翻译官”杨远帆及其“可视科学”团队,已成功讲述30余位院士的科研历程,生动展现了20余座国家大科学装置及重大科技基础设施的核心科研成果。这支孵化于中国科大…

    2026年8月30日
    100
  • 《马里奥赛车:世界》非官方互动地图应用突然下架

    《马里奥赛车:世界》的开放探索地图规模巨大,再加上将近400个p-switch挑战任务需要发现并完成,想要一一记录和追踪并不容易。幸运的是,玩家们一直在寻找解决办法,虽然任天堂似乎已经下架了其中一款工具,但仍有一些免费替代方案可供使用。 曾有一款名为“MK世界旅行指南”的非官方辅助应用,它在iOS应…

    2026年8月30日
    000
  • 如何用自定义表达式实现问卷选项的动态控制?

    利用自定义表达式实现问卷逻辑的动态控制 本文介绍如何通过自定义表达式,动态控制问卷下拉选项的可选择性和自动选中状态。 假设问卷包含多个下拉选项,每个选项都关联一个表达式。当表达式结果为真时,选项不可选;如果用户选择了某个选项(例如选项B),则系统会根据表达式自动选中其他关联选项。 问卷选项的数据结构…

    2026年8月30日
    000
  • Vite构建UMD库后,如何避免全局污染直接在HTML中调用导出方法?

    本文探讨如何安全地在HTML中调用使用Vite构建的UMD库导出的方法,避免全局命名空间污染。 Vite配置已指向导出canvasEditorFun对象的src/main.ts文件,该对象包含initEditorByFile、getHTML、saveCEData、initEditorByData等方…

    2026年8月30日
    000
  • 如何解决PrestaShop图表显示问题?使用prestashop/graphnvd3可以提升你的数据可视化效果

    可以通过一下地址学习composer:学习地址 在使用prestashop进行电商运营时,数据可视化是非常重要的一部分。通过图表,我们可以直观地了解销售趋势、客户行为等关键信息。然而,在使用prestashop的默认图表功能时,我遇到了一个问题:图表显示效果不佳,数据不够直观,严重影响了我的分析效率…

    用户投稿 2026年8月30日
    000
  • 刚刚,DeepSeek开源FlashMLA,推理加速核心技术,Star量飞涨中

    刚刚,DeepSeek开源FlashMLA,推理加速核心技术,Star量飞涨中刚刚,DeepSeek开源FlashMLA,推理加速核心技术,Star量飞涨中刚刚,DeepSeek开源FlashMLA,推理加速核心技术,Star量飞涨中刚刚,DeepSeek开源FlashMLA,推理加速核心技术,Star量飞涨中

    deepseek开源高效型mla解码核flashmla,助力hopper gpu推理加速!上周五deepseek预告开源周计划,并于北京时间周一上午9点开源了首个项目——flashmla,一款针对hopper gpu优化的高效mla解码内核,仅上线45分钟便收获400+star! ☞☞☞AI 智能聊…

    2026年8月30日 用户投稿
    000
  • 视频号视频怎么分组?分组功能在哪?

    随着视频号内容不断增多,如何按主题进行归类整理,以便自己和粉丝更方便地浏览?其实,微信视频号提供了“合集”功能,能够轻松实现视频的分类与分组。本文将详细讲解如何创建和管理视频合集,助你打造清晰有序的主页布局。 一、如何对视频号视频进行分组? 视频号中的“分组”功能,官方称为“合集”。通过该功能,你可…

    2026年8月30日
    200
  • win10怎么解决麦克风权限问题_win10麦克风应用权限问题解决方法

    首先检查系统麦克风总权限是否开启,依次进入设置→隐私→麦克风,打开“允许此设备上的应用访问麦克风”;接着在相同页面确保“允许应用访问你的麦克风”开关已开,并为具体应用(如微信、Teams)单独开启权限;然后向下找到并开启“允许桌面应用访问你的麦克风”,以授权QQ、钉钉等传统程序使用麦克风;若麦克风集…

    2026年8月30日
    000
  • 微信小程序离线表单提交:如何实现离线填写表单并在网络恢复后自动提交?

    微信小程序离线表单提交:确保数据安全可靠 本文介绍一种微信小程序离线表单提交方案,解决网络不稳定情况下用户填写表单的需求。即使在离线或网络差的情况下,用户也能填写表单,并在网络恢复后自动提交数据。 该方案的核心在于利用小程序的本地存储和网络状态监听功能。以下步骤将详细解释如何实现: 第一步:配置网络…

    2026年8月30日
    000
  • 在哪里可以下载aiapp

    在数字化浪潮迅猛推进的今天,人工智能已悄然渗透到各行各业,成为推动变革的关键力量。而 aiapp 下载,则为我开启了一段通往智能未来的奇妙旅程。 初次接触 aiapp,我就被它琳琅满目的功能深深吸引。它仿佛是一个蕴藏无限可能的智慧宝盒,每一项应用都闪烁着科技的光芒,等待我去一一解锁。 智能写作助手是…

    2026年8月30日
    200

发表回复

登录后才能评论
关注微信