当前位置: 首页 > news >正文

基于 CSS Grid 的简易拖拉拽 Vue3 组件,从代码到NPM发布(1)- 拖拉拽交互

基于特定的应用场景,需要在页面中以网格的方式,实现目标组件在网格中可以进行拖拉拽、修改大小等交互。本章开始分享如何一步步从代码设计,最后到如何在 NPM 上发布。

请大家动动小手,给我一个免费的 Star 吧~

大家如果发现了 Bug,欢迎来提 Issue 哟~

github源码

示例地址

特别说明一下,此组件是基于 CSS 的 display: grid 的,并非全能型拖拉拽交互,grid 不支持的基本就是不支持的,此组件的目标是达到一些简易的网格布局拖拉拽交互。

效果图

在这里插入图片描述

项目结构

项目结构是基于另外一个项目 konva-designer-sample,特别说一下需要关注的部分:

└─ dist - 构建的组件库文件
└─ docs - 构建的在线示例网站
└─ src└─ demo│   └─ App.vue - 在线示例页面└─ lib└─ components└─ GridDragResize - 组件目录└─ GridDragResize.vue - 组件└─ GridDragResizeItem.vue - 子组件└─ index.ts - 组件入口└─ style.less - 组件样式└─ types.ts - 组件配套类型声明└─ main.ts - 在线示例代码入口
└─ index.html - 在线示例HTML入口
└─ package.json - 库信息
└─ tsconfig.build.json - 用于构建组件库配套的类型声明文件
└─ vite.config.ts - 构建配置

使用方式

直接先看看组件的使用方式:

src/demo/App.vue

<script setup lang="ts">
import { ref, h, type Ref } from 'vue'
// 组件
import { GridDragResize } from '@/lib/components/GridDragResize'
// 组件配套类型声明
import type { GridDragResizeProps } from '@/lib/components/GridDragResize/types'// 组件数据结构
const children: Ref<GridDragResizeProps['children']> = ref([{dragHandler: '.demo-item>button',render: () => h('div', { class: "demo-item", style: { background: '#eb9c64' } }, [h('button', 'drag handler')])},{columnStart: 2,draggable: false,render: () => h('div', { class: "demo-item", style: { background: '#ff8789' } }, 'disable drag')},{rowStart: 2,columnStart: 2,render: () => h('div', { class: "demo-item", style: { background: '#554e4f' } }, '1')},{rowStart: 2,rowEnd: 4,columnStart: 4,columnEnd: 5,render: () => h('div', { class: "demo-item", style: { background: '#8fbf9f' } }, '2')},{rowStart: 4,rowEnd: 6,columnStart: 2,columnEnd: 4,render: () => h('div', { class: "demo-item", style: { background: '#346145' } }, '3')},{rowStart: 4,rowEnd: 5,columnStart: 1,columnEnd: 2,render: () => h('div', { class: "demo-item", style: { background: '#c2baa6' } }, '4')},
])
</script><template>
<div class="page"><!-- 组件使用 --><GridDragResize :columns="4" :rows="5" :gap="10" :row-size="100" :readonly="false" :children="children"></GridDragResize><!-- 组件数据结构 实时状态 --><div v-html="JSON.stringify(children, null, 2).replace(/\n/g, '<br>').replace(/\s/g, '&nbsp; ')"></div>
</div>
</template><style lang="less">
// 一些样式初始化*,
*::before,
*::after {box-sizing: border-box;margin: 0;font-weight: normal;
}body {min-height: 100vh;color: var(--color-text);background: var(--color-background);transition:color 0.5s,background-color 0.5s;line-height: 1.6;font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Fira Sans','Droid Sans','Helvetica Neue',sans-serif;font-size: 15px;text-rendering: optimizeLegibility;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;
}
</style>
<style lang="less">
// 示例样式
.page {padding: 32px;
}.demo-item {padding: 10px;height: 100%;
}// 组件样式覆盖
.grid-drag-resize {background-color: #eee;.grid-drag-resize__item {background-color: #ddd;&--dragging {box-shadow: 0 0 6px 2px #0000ff;}}
}
</style>

上面可以看出,render 是比较关键的地方,该组件使用方式并非 插槽,而是通过数据结构传入的 render 实现每一块的显示的,它可以是 h 可以是一个个 其他组件。

接下来,可以看看定义:

组件 Props 定义

// src/lib/components/GridDragResize/types.tsimport type { VNode } from 'vue'// 子组件的 Props
export interface GridDragResizeItemProps {draggable?: booleandragHandler?: string // 满足 querySelector 的查询字符串,指向可拖拉拽的元素位置// css display grid 属性columnStart?: numbercolumnEnd?: numberrowStart?: numberrowEnd?: number//render?: () => VNode
}// 组件的 Props
export interface GridDragResizeProps {dragHandler?: string // 同上,优先级 低于 子组件readonly?: boolean // 优先级 低于 子组件 的 draggable//columns?: number // 列数rows?: number // 行数gap?: number // 间隙columnSize?: number // 列宽,默认是 1frrowSize?: number // 行高,默认是 1fr//children?: GridDragResizeItemProps[] // 子组件
}

目前为止,定义非常简单。

组件

src/lib/components/GridDragResize/GridDragResize.vue
逻辑说明,请留意代码注释

<script setup lang="ts">
import { ref, computed, provide, type Ref } from 'vue'import type { GridDragResizeProps, GridDragResizeItemProps } from './types'import GridDragResizeItem from './GridDragResizeItem.vue'const props = withDefaults(defineProps<GridDragResizeProps>(), {children: () => []
});const style = computed(() => {return {'grid-template-columns': Number.isInteger(props.columns) ? `repeat(${props.columns},${Number.isInteger(props.columnSize) ? `${props.columnSize}px` : '1fr'})` : '','grid-template-rows': Number.isInteger(props.rows) ? `repeat(${props.rows},${Number.isInteger(props.rowSize) ? `${props.rowSize}px` : '1fr'})` : '','grid-gap': Number.isInteger(props.gap) ? `${props.gap}px ${props.gap}px` : ''}
})const rootEle: Ref<HTMLElement | undefined> = ref()// 给子组件穿透转递组件 Props
provide('parentProps', props)// 组件位置、大小信息
const rootRect = computed(() => {return rootEle?.value?.getBoundingClientRect() ?? {height: 0,width: 0,x: 0,y: 0,bottom: 0,right: 0}
})// 列宽
const columnSize = computed(() => {return (rootRect.value.width - (props.gap ?? 0) * ((props.columns ?? 1) - 1)) / (props.columns ?? 1)
})// 行高
const rowSize = computed(() => {return (rootRect.value.height - (props.gap ?? 0) * ((props.rows ?? 1) - 1)) / (props.rows ?? 1)
})// 根据鼠标拖动偏移量,计算列/行方向上,移动后最新的位置和大小
function calcStartEnd(opts: { size: number, gap: number, span: number, max: number, offset: number, startBefore: number }) {let { size, gap, span, max, offset, startBefore } = optslet offsetStart = Math.round(offset / (size + gap))let start = startBefore + offsetStartif (start < 1) {start = 1}if (start + span > max) {start = max - span + 1}return {start,end: start + span}
}// 当前拖动小组件的数据项
const draggingChild: Ref<GridDragResizeItemProps | undefined> = ref()
// 当前拖动小组件的数据项(初始状态)
const draggingChildBefore: Ref<GridDragResizeItemProps | undefined> = ref()
// 当前拖动小组件的位置、大小信息
const draggingChildRect: Ref<DOMRect | undefined> = ref()// 拖动开始位置
let dragStartClientX = 0, dragStartClientY = 0;// 拖动偏移量
let dragOffsetClientX = 0, dragOffsetClientY = 0;let dragging = false// 开始拖动
function dragstart(e: MouseEvent) {if (!props.readonly) {dragging = true// 记录 拖动开始位置dragStartClientX = e.clientXdragStartClientY = e.clientY}
}// 拖动中
function drag(e: MouseEvent) {if (dragging && draggingChild.value && draggingChildRect.value) {// 计算 拖动开始位置dragOffsetClientX = e.clientX - dragStartClientXdragOffsetClientY = e.clientY - dragStartClientY// 当前拖动小组件的 grid 大小let rowSpan = (draggingChild.value.rowEnd ?? draggingChild.value.rowStart ?? 1) - (draggingChild.value.rowStart ?? 1)let columnSpan = (draggingChild.value.columnEnd ?? draggingChild.value.columnStart ?? 1) - (draggingChild.value.columnStart ?? 1)// 边界处理if (rowSpan <= 0) {rowSpan = 1}if (columnSpan <= 0) {columnSpan = 1}// 计算行方向上,移动后最新的位置和大小let { start: rowStart, end: rowEnd } = calcStartEnd({size: rowSize.value, gap: (props.gap ?? 0), span: rowSpan, max: props.rows ?? 1, offset: dragOffsetClientY, startBefore: draggingChildBefore.value?.rowStart ?? 1})// 计算列方向上,移动后最新的位置和大小let { start: columnStart, end: columnEnd } = calcStartEnd({size: columnSize.value, gap: (props.gap ?? 0), span: columnSpan, max: props.columns ?? 1, offset: dragOffsetClientX, startBefore: draggingChildBefore.value?.columnStart ?? 1})// 当前拖动小组件的数据项draggingChild.value.columnStart = columnStartdraggingChild.value.columnEnd = columnEnddraggingChild.value.rowStart = rowStartdraggingChild.value.rowEnd = rowEnd}
}// 拖动结束
function dragend(e: MouseEvent) {e.stopPropagation()dragging = falsedraggingChild.value = undefined
}// 超出组件区域,补充结束事件
document.body.addEventListener('mouseup', dragend)
</script><template>
<div class="grid-drag-resize" :style="style" @mousedown="dragstart" @mousemove="drag" @mouseup="dragend" ref="rootEle"><template v-for="(child, idx) of props.children" :key="idx"><GridDragResizeItem v-bind="child" v-model:column-start="child.columnStart" v-model:column-end="child.columnEnd"v-model:row-start="child.rowStart" v-model:row-end="child.rowEnd"@dragging="(rect) => { draggingChild = child; draggingChildBefore = { ...child }; draggingChildRect = rect }":style="{ 'zIndex': draggingChild === child ? props.children.length + 1 : idx + 1 }":class="{ 'grid-drag-resize__item--dragging': draggingChild === child }"><component :is="child.render"></component></GridDragResizeItem></template>
</div>
</template>

子组件

src/lib/components/GridDragResize/GridDragResizeItem.vue
逻辑说明,请留意代码注释

<script setup lang="ts">
import { ref, computed, watchEffect, inject, type Ref } from 'vue'import type { GridDragResizeProps, GridDragResizeItemProps } from './types'const parentProps = inject<GridDragResizeProps>('parentProps')const props = withDefaults(defineProps<GridDragResizeItemProps>(), {draggable: true
});const emit = defineEmits(['update:columnStart', 'update:columnEnd', 'update:rowStart', 'update:rowEnd', 'dragging'])// 数据整理
watchEffect(() => {if (props.columnStart !== void 0) {if (props.columnEnd === void 0 || props.columnEnd < props.columnStart) {emit('update:columnEnd', props.columnStart + 1)}} else {emit('update:columnStart', 1)}if (props.rowStart !== void 0) {if (props.rowEnd === void 0 || props.rowEnd < props.rowStart) {emit('update:rowEnd', props.rowStart + 1)}} else {emit('update:rowStart', 1)}
})// 样式
const style = computed(() => {return {'grid-column-start': props.columnStart,'grid-column-end': props.columnEnd,'grid-row-start': props.rowStart,'grid-row-end': props.rowEnd,}
})const itemEle: Ref<HTMLElement | undefined> = ref()const dragHandlerParsed = computed(() => props.dragHandler ?? parentProps?.dragHandler)
const draggableParsed = computed(() => parentProps?.readonly ? false : props.draggable)// dragHandler 定位、处理、事件绑定
watchEffect(() => {if (draggableParsed.value && dragHandlerParsed.value && itemEle.value) {const handlerEle = itemEle.value.querySelector(dragHandlerParsed.value)if (handlerEle instanceof HTMLElement) {handlerEle.style.cursor = 'grab'handlerEle.addEventListener('mousedown', dragstart)}}
})// 拖动开始
function dragstart() {if (draggableParsed.value) {// 通知父组件 当前拖动小组件emit('dragging', itemEle?.value?.getBoundingClientRect() ?? {height: 0,width: 0,x: 0,y: 0,bottom: 0,right: 0})}
}
</script><template>
<div class="grid-drag-resize__item" :class="{'grid-drag-resize__item--draggable': draggableParsed,'grid-drag-resize__item--draggable-full': draggableParsed && dragHandlerParsed === void 0
}" :style="style" @mousedown="() => dragHandlerParsed ? undefined : dragstart()" ref="itemEle"><slot></slot>
</div>
</template>

样式

.grid-drag-resize {display: grid;.grid-drag-resize__item {&--draggable-full {cursor: grab;user-select: none;}&--dragging {opacity: 0.6;}}
}

组件入口

// src/lib/components/GridDragResize/index.ts
import GridDragResize from './GridDragResize.vue'
import GridDragResizeItem from './GridDragResizeItem.vue'import './style.less'export * from './types'export { GridDragResize, GridDragResizeItem }

Thanks watching~

下一章,我们说说如何构建在线示例、组件库,及其如何发布到 NPM 上供开源使用!

More Stars please!勾勾手指~

github源码

示例地址

相关文章:

基于 CSS Grid 的简易拖拉拽 Vue3 组件,从代码到NPM发布(1)- 拖拉拽交互

基于特定的应用场景&#xff0c;需要在页面中以网格的方式&#xff0c;实现目标组件在网格中可以进行拖拉拽、修改大小等交互。本章开始分享如何一步步从代码设计&#xff0c;最后到如何在 NPM 上发布。 请大家动动小手&#xff0c;给我一个免费的 Star 吧~ 大家如果发现了 Bug…...

【华为HCIP实战课程六】OSPF邻居关系排错网络子网掩码问题,网络工程师

一、链路上网络和掩码引发的OSPF邻居问题 R3和R4已经建立正常的ospf邻居关系 更改IP地址前R3接口IP地址 interface Serial2/0/0 link-protocol ppp ip address 10.1.34.3 255.255.255.240 [R3-Serial2/0/0]ip address 10.1.88.2 255.255.255.240 更改为10.1.88.2 R3和R4虽…...

基础教程 | 用VuePress搭建一个简单的个人博客(附源码)

先附上自己个人博客页面&#xff1a;https://illusionno.github.io/ 源码也在这里&#xff1a;https://github.com/illusionno/my-blog &#xff08;如果觉得有帮助&#xff0c;可以点颗star✨&#xff09; 使用的主题是vuepress-theme-reco2.x&#xff0c;并在上面进行了一些调…...

Ubuntu20.04,编译安装BCC

https://github.com/iovisor/bcc/blob/master/INSTALL.md 一、内核配置 In general, to use these features, a Linux kernel version 4.1 or newer is required. In addition, the kernel should have been compiled with the following flags set: CONFIG_BPFy CONFIG_BP…...

# 显卡算力参数对比

显卡算力参数对比 文章目录 显卡算力参数对比A 显卡参数查询B 显卡性能对比&#xff1a; 综合看&#xff1a;T4最具性价比 A 显卡参数查询 查询网址&#xff1a;https://www.techpowerup.com/gpu-specs/ &#xff0c;以下列出部分&#xff1a; Product NameGPU ChipReleasedB…...

掌握RocketMQ4.X消息中间件(一)-RocketMQ基本概念与系统架构

1 MQ介绍 MQ(Message Quene) : 翻译为 消息队列,别名为 消息中间件&#xff0c;通过典型的 生产者和消费者模型,生产者不断向消息队列中生产消息&#xff0c;消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的&#xff0c;而且只关心消息的发送和接收&#xff0c…...

实际开发中,java开发的准备工作

实际开发中&#xff0c;java开发的准备工作 一、IDEA工具环境设置 1、编码设置...

SQL进阶技巧:Order by 中 NULLS LAST特性使用?

目录 1 需求描述 2 数据准备 3 问题分析 4 小结 如果觉得本文对你有帮助&#xff0c;想进一步学习SQL语言这门艺术的&#xff0c;那么不妨也可以选择去看看我的博客专栏 &#xff0c;部分内容如下&#xff1a; 数字化建设通关指南 专栏 原价99&#xff0c;现在活动价59…...

Redis:cpp.redis++类型操作

Redis&#xff1a;cpp.redis类型操作 stringsetmsetmgetgetrangesetrangeincrbydecrby listlpushrpushlrangellenlpoprpopblpopbrpop setsaddsmemeberssismemberscardspopsintersinterstore hashhsethgethexistshdelhkeyshvalshmsethmget zsetzaddzrangezcardzremzscorezrank 总…...

感冒用药记录

问题描述&#xff1a;国庆感冒了&#xff0c;头昏喉咙不舒服 用药过程&#xff1a; – 前3天&#xff1a;未用药&#xff0c;不好也不坏 – 中间2天&#xff1a;开始喉痛&#xff0c;使用复方氨酚烷胺胶囊【含对乙酰氨基酚】&#xff0c;基本没有效果 – 后面1天&#xff1a;开…...

JMeter性能测试时,如何做CSV参数化

在现代软件开发中&#xff0c;性能测试是保证应用程序在高负载条件下稳定运行的重要环节。为了实现真实场景的测试&#xff0c;参数化技术应运而生。其中&#xff0c;CSV参数化是一种高效且灵活的方法&#xff0c;可以让测试人员通过外部数据文件驱动测试脚本&#xff0c;从而模…...

爬虫获取不同数据类型(如JSON,HTML)的处理方法以及图片相对URL地址的转换

当我们爬取图片的URL地址时&#xff0c;我们要确保它们都是有效的绝对URL&#xff0c;这样就可以直接用这些URL来下载图片了。但是很多时候&#xff0c;它们都不是绝对URL地址&#xff0c;因此我们需要它进行URL转换。 if img_url.startswith(//): 这个条件检查URL是否以//开头…...

Elasticsearch 实战应用

Elasticsearch 实战应用 引言 Elasticsearch 是一个分布式、RESTful 风格的搜索和分析引擎&#xff0c;能够快速、实时地处理大规模数据&#xff0c;广泛应用于全文搜索、日志分析、推荐系统等领域。在这篇博客中&#xff0c;我们将从 Elasticsearch 的基本概念入手&#xff…...

前端数据加载慢的解决方法

都是和前端性能优化非常类似的做法。 1. 懒加载 (Lazy Loading) 对于图片、视频等资源&#xff0c;或者某些组件&#xff0c;在用户滚动到相关区域时再加载&#xff0c;而不是页面一开始就加载所有内容。使用 IntersectionObserver 实现懒加载&#xff0c;或者一些 UI 框架&am…...

探索MultiApp:一款强大的多应用管理工具

探索MultiApp&#xff1a;一款强大的多应用管理工具 在这个数字化时代&#xff0c;多任务并行已经成为我们日常生活的一部分。无论是工作还是娱乐&#xff0c;我们都需要频繁地在多个应用之间切换。今天&#xff0c;我要向大家介绍一款能够帮助你在同一设备上无缝切换和管理多…...

qt QGraphicsItem详解

一、概述 QGraphicsItem是Qt框架中图形视图框架&#xff08;Graphics View Framework&#xff09;的一个核心组件&#xff0c;它是用于表示2D图形元素的基类。 它支持的功能包括&#xff1a; 设置和获取图形项的位置和尺寸。控制图形项的外观&#xff0c;如颜色、笔刷、边框…...

LVS搭建负载均衡

LVS搭建负载均衡 引言 在现代互联网应用中&#xff0c;用户对服务的可用性和响应速度要求越来越高。为了应对高并发请求&#xff0c;保证系统的稳定性和容错能力&#xff0c;负载均衡技术应运而生。LVS&#xff08;Linux Virtual Server&#xff09;是一种高性能、高可用性的…...

Unity MVC框架演示 1-1 理论分析

本文仅作学习笔记分享与交流&#xff0c;不做任何商业用途&#xff0c;该课程资源来源于唐老狮 1.一般的图解MVC 什么是MVC我就不说了&#xff0c;老生常谈&#xff0c;网上有大量的介绍&#xff0c;想看看这三层都起到什么职责&#xff1f;那就直接上图吧 2.我举一个栗子 我有…...

基于springboot+vue人脸识别的考勤管理系统(源码+定制+开发)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…...

【api连接ChatGPT的最简单方式】

通过api连接ChatGPT的最简单方式 建立client 其中base_url为代理&#xff0c;若连接官网可省略&#xff1b;配置环境变量 from openai import OpenAI client OpenAI(base_url"https://api.chatanywhere.tech/v1" )或给出api和base_url client OpenAI(api_key&…...

技术成神之路:设计模式(二十)装饰模式

介绍 装饰模式&#xff08;Decorator Pattern&#xff09;是一种结构型设计模式&#xff0c;它允许在不改变对象自身的情况下&#xff0c;动态地为对象添加额外的职责。这个模式通常用于增强或改变对象的功能。 1.定义 装饰模式通过创建一个装饰类&#xff0c;将功能动态地添加…...

利用特征点采样一致性改进icp算法点云配准方法

1、index、vector 2、kdtree和kdtreeflann 3、if kdtree.radiusSearch(。。。) > 0)...

LabVIEW惯性导航系统仿真平台

LabVIEW开发捷联惯性导航系统仿真平台&#xff0c;采用模块化设计&#xff0c;利用LabVIEW的图形化编程特性&#xff0c;提高了系统仿真的效率和精度&#xff0c;同时具备良好的可扩展性和用户交互性。 项目背景 当前&#xff0c;惯性导航系统&#xff08;INS&#xff09;的研…...

es简单实现文章检索功能

使用的api是&#xff1a;Elasticsearch Java API client 8.0 官网&#xff1a;Package structure and namespace clients | Elasticsearch Java API Client [8.15] | Elastic 1.建立索引库 实现搜索功能字段&#xff1a; title&#xff1a;文章标题content&#xff1a;文章内…...

太速科技-607-基于FMC的12收和12发的光纤子卡

基于FMC的12收和12发的光纤子卡 一、板卡概述 本卡是一个FPGA夹层卡&#xff08;FMC&#xff09;模块&#xff0c;可提供高达2个CXP模块接口&#xff0c;提供12路收&#xff0c;12路发的光纤通道。每个通道支持10Gbps,通过Aurora协议&#xff0c;可以组成X4&#xff0…...

UEFI学习笔记(十):系统表与ACPI表的遍历

一、概述 在 UEFI 系统表中&#xff0c;有几个关键的表用于提供系统信息、服务和硬件抽象。这些表可以通过 EFI_SYSTEM_TABLE 访问&#xff0c;常见的 UEFI 系统表如下&#xff1a; 1、EFI_SYSTEM_TABLE (系统表) EFI_SYSTEM_TABLE 是一个指针&#xff0c;包含多个服务和系统…...

【深度学习基础模型】液态状态机(Liquid State Machines, LSM)详细理解并附实现代码。

【深度学习基础模型】液态状态机&#xff08;Liquid State Machines, LSM&#xff09;详细理解并附实现代码。 【深度学习基础模型】液态状态机&#xff08;Liquid State Machines, LSM&#xff09;详细理解并附实现代码。 文章目录 【深度学习基础模型】液态状态机&#xff0…...

深入理解链表(SList)操作

目录&#xff1a; 一、 链表介绍1.1、 为什么引入链表1.2、 链表的概念及结构1.3、 链表的分类 二、 无头单向非[循环链表](https://so.csdn.net/so/search?q循环链表&spm1001.2101.3001.7020)的实现2.1、 [单链表](https://so.csdn.net/so/search?q单链表&spm1001.2…...

03. prometheus 监控 Linux 主机

文章目录 一、prometheus 监控 Linux 主机二、防火墙打开端口1. 方式一&#xff1a;使用 iptables 添加白名单&#xff08;推荐使用&#xff09;&#xff1a;2. 方式二&#xff1a;重载防火墙 一、prometheus 监控 Linux 主机 1. 官网下载 node_exporter 官网&#xff1a;htt…...

AI占据2024诺贝尔两大奖项,是否预示着未来AI即一切?

本次诺贝尔物理学和学奖的获得者都与AI息息相关&#xff0c;可谓是“AI领域的大丰收”。 2024年诺贝尔物理学奖揭晓&#xff1a;瑞典皇家科学院公布了2024年诺贝尔物理学奖的获得者。他们是美国的约翰霍普菲尔德&#xff08;John J. Hopfield&#xff09;&#xff0c;以及加拿…...