React Three Fiber 實現 3D 模型點擊高亮交互的核心技巧

在 WebGL 3D 開發中,模型交互是提升用戶體驗的關鍵功能之一。本文將基于 React Three Fiber(R3F)和 Three.js,總結 3D 模型點擊高亮(包括模型本身和邊框)的核心技術技巧,幫助開發者快速掌握復雜 3D 交互的實現思路。本文主要圍著以下功能進行講述

  • 加載 GLB?格式的 3D 城市模型
  • 通過鼠標點擊實現模型的選中 / 取消選中狀態切換
  • 選中時同時顯示兩種高亮效果:
    • 模型本身的半透明材質覆蓋
    • 模型邊緣的線框高亮
  • 完善的資源管理和內存清理機制

操作面板和模型導入功能參考這篇博客

Three.js 如何控制 GLB 模型的內置屬性實現精準顯示-CSDN博客

?

?3D 模型加載與引用管理

?使用?useGLTF?鉤子簡化 GLTF 模型加載,這是 R3F 生態中處理 3D 資源的標準方式,內部已處理加載狀態、資源緩存和內存管理;通過?ref?引用 Three.js 原生 Group 對象,使我們能直接操作 3D 場景對象;使用?<primitive>?組件將 Three.js 原生對象掛載到 React 虛擬 DOM 中,實現 React 對 Three.js 對象的管理

import { useGLTF } from '@react-three/drei'export const CityModel = ({ url }: { url: string }) => {const { scene } = useGLTF(url)const modelRef = useRef<THREE.Group>(null)// 組件渲染return <primitive object={scene} ref={modelRef} />
}

?射線檢測實現模型點擊交互

?這是 3D 交互的核心機制,通過?Raycaster?模擬一條從相機發射到點擊位置的射線。坐標轉換是關鍵:將屏幕像素坐標(0~window.innerWidth)轉換為 Three.js 標準化設備坐標(-1~1)。intersectObject?方法第二個參數設為?true?表示遞歸檢測所有子對象,確保能選中復雜模型的子網格。優先處理?intersects[0](最近的交點),符合真實世界的交互邏輯

const raycaster = useRef(new THREE.Raycaster())
const pointer = useRef(new THREE.Vector2())const handleClick = (event: MouseEvent) => {// 僅響應左鍵點擊if (event.button !== 0) return// 屏幕坐標轉換為 Three.js 標準化設備坐標pointer.current.x = (event.clientX / window.innerWidth) * 2 - 1pointer.current.y = -(event.clientY / window.innerHeight) * 2 + 1detectClickedMesh()
}const detectClickedMesh = () => {if (!modelRef.current) return// 更新射線(從相機到點擊位置)raycaster.current.setFromCamera(pointer.current, camera)// 檢測與模型的交點(遞歸檢測所有子Mesh)const intersects = raycaster.current.intersectObject(modelRef.current, true)if (intersects.length > 0) {const clickedObject = intersects[0].object as THREE.Mesh// 處理點擊邏輯...}
}

材質切換實現模型高亮

利用 Three.js 的?userData?存儲原始材質,這是一種安全的擴展對象屬性的方式;同時支持單材質和多材質(MeshFaceMaterial)的場景,處理更全面

使用?clone()?方法創建材質副本,避免多個對象共享同一材質實例導致的樣式沖突

半透明材質(transparent: true)實現高亮效果的同時不遮擋其他重要信息

// 高亮材質(半透明效果)
const highlightMaterial = useRef(new THREE.MeshBasicMaterial({ color: '#040912', transparent: true, opacity: 0.4 })
)// 保存原始材質并應用高亮材質
const saveOriginalAndApplyHighlight = (mesh: THREE.Mesh) => {// 保存原始材質(處理單材質和多材質情況)if (!mesh.userData.originalMaterial) {if (Array.isArray(mesh.material)) {mesh.userData.originalMaterial = [...mesh.material]mesh.material = mesh.material.map(() => highlightMaterial.current.clone())} else {mesh.userData.originalMaterial = mesh.materialmesh.material = highlightMaterial.current.clone()}}
}// 恢復原始材質
const restoreOriginalMaterial = (mesh: THREE.Mesh[]) => {mesh.forEach(m => {if (m.userData.originalMaterial) {m.material = m.userData.originalMaterial}})
}

邊緣線框高亮效果實現

  • 使用?EdgesGeometry?從網格幾何體生成邊緣線,自動計算模型的輪廓邊緣
  • 通過?LineSegments?創建線框對象,比?Line?更適合表現閉合輪廓
  • 將線框作為模型的子對象,確保線框能跟隨模型一起變換(移動、旋轉、縮放)
  • 使用?Map?+?uuid?管理線框對象,確保每個模型對應唯一的線框,方便添加 / 移除
// 存儲所有創建的邊緣線對象
const edgeLines = useRef<Map<string, THREE.LineSegments>>(new Map())// 添加邊緣高亮效果
const addHighlight = (object: THREE.Mesh) => {if (!object.geometry) return// 創建邊緣幾何體const geometry = new THREE.EdgesGeometry(object.geometry)// 創建邊緣線材質const material = new THREE.LineBasicMaterial({color: 0x4C8BF5, // 藍色邊緣linewidth: 2,})// 創建邊緣線對象const line = new THREE.LineSegments(geometry, material)// 繼承原始模型的變換屬性line.position.copy(object.position)line.rotation.copy(object.rotation)line.scale.copy(object.scale)// 添加到模型并記錄object.add(line)edgeLines.current.set(object.uuid, line)
}// 移除邊緣高亮效果
const removeHighlight = (object: THREE.Mesh) => {const line = edgeLines.current.get(object.uuid)if (line && object.children.includes(line)) {object.remove(line)}edgeLines.current.delete(object.uuid)
}

組件生命周期與資源管理

  • 在?useEffect?中管理事件監聽,確保只在模型加載完成后綁定事件
  • 卸載時執行完整的清理工作:移除事件監聽、清理線框對象、恢復材質狀態
  • 避免內存泄漏:Three.js 對象如果不手動清理,即使組件卸載也可能殘留在內存中
  • 狀態重置:確保組件卸載后所有引用和狀態都回到初始狀態?
useEffect(() => {if (!modelRef.current) return// 模型初始化邏輯...// 綁定點擊事件window.addEventListener('click', handleClick)// 組件卸載時清理return () => {window.removeEventListener('click', handleClick)// 移除所有高亮邊緣線highlightedMeshRef.current.forEach(mesh => {removeHighlight(mesh)})edgeLines.current.clear()// 清理高亮狀態if (highlightedMeshRef.current) {restoreOriginalMaterial(highlightedMeshRef.current)}highlightedMeshRef.current = []}
}, [modelRef.current])

完整代碼?

其中? const helper = useModelManager()是我用于操作面板管理時獲取模型數據,不用可以移除

?CityModel.tsx

import { useGLTF } from '@react-three/drei'
import { useThree } from '@react-three/fiber'
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
import { useModelManager } from '../../../utils/viewHelper/viewContext'export const CityModel = ({ url }: { url: string }) => {const { scene } = useGLTF(url)const modelRef = useRef<THREE.Group>(null)const helper = useModelManager()const { camera } = useThree()const raycaster = useRef(new THREE.Raycaster())const pointer = useRef(new THREE.Vector2())const highlightedMeshRef = useRef<THREE.Mesh[]>([])// 存儲所有創建的邊緣線對象const edgeLines = useRef<Map<string, THREE.LineSegments>>(new Map())// 高亮材質(半透明青色)const highlightMaterial = useRef(new THREE.MeshBasicMaterial({color: '#5a6f85',transparent: true,opacity: 0.7,}),)// 添加邊緣高亮效果const addHighlight = (object: THREE.Mesh) => {if (!object.geometry) return// 創建邊緣幾何體const geometry = new THREE.EdgesGeometry(object.geometry)// 創建邊緣線材質const material = new THREE.LineBasicMaterial({color: 0x4c8bf5, // 藍色邊緣linewidth: 2, // 線寬})// 創建邊緣線對象const line = new THREE.LineSegments(geometry, material)line.name = 'surroundLine'// 復制原始網格的變換line.position.copy(object.position)line.rotation.copy(object.rotation)line.scale.copy(object.scale)// 設置為模型的子對象,確保跟隨模型變換object.add(line)edgeLines.current.set(object.uuid, line)}// 移除邊緣高亮效果const removeHighlight = (object: THREE.Mesh) => {const line = edgeLines.current.get(object.uuid)if (line && object.children.includes(line)) {object.remove(line)}edgeLines.current.delete(object.uuid)}// 處理左鍵點擊事件const handleClick = (event: MouseEvent) => {// 僅響應左鍵點擊(排除右鍵/中鍵/滾輪)if (event.button !== 0) return// 計算點擊位置的標準化設備坐標pointer.current.x = (event.clientX / window.innerWidth) * 2 - 1pointer.current.y = -(event.clientY / window.innerHeight) * 2 + 1// 執行射線檢測,判斷點擊目標detectClickedMesh()}// 檢測點擊的Mesh并切換高亮狀態const detectClickedMesh = () => {if (!modelRef.current) return// 更新射線(從相機到點擊位置)raycaster.current.setFromCamera(pointer.current, camera)// 檢測與模型的交點(遞歸檢測所有子Mesh)const intersects = raycaster.current.intersectObject(modelRef.current, true)if (intersects.length > 0) {const clickedObject = intersects[0].object as THREE.Mesh// 僅處理標記為可交互的Meshif (clickedObject instanceof THREE.Mesh &&clickedObject.userData.interactive) {// 切換高亮狀態:點擊已高亮的Mesh則取消,否則高亮新Meshif (highlightedMeshRef.current?.includes(clickedObject)) {console.log('取消高亮', clickedObject.name)// 取消高亮:恢復原始材質restoreOriginalMaterial([clickedObject])// 移除邊框高亮removeHighlight(clickedObject)const newHighlighted = highlightedMeshRef.current.filter((m) => m.name !== clickedObject.name,)highlightedMeshRef.current = [...newHighlighted]} else {console.log('高亮', clickedObject.name)// 高亮當前點擊的MeshsaveOriginalAndApplyHighlight(clickedObject)// 添加邊框高亮addHighlight(clickedObject)highlightedMeshRef.current = [...highlightedMeshRef.current,clickedObject,]}}}}// 工具函數:恢復原始材質const restoreOriginalMaterial = (mesh: THREE.Mesh[]) => {mesh.forEach((m) => {if (m.userData.originalMaterial) {m.material = m.userData.originalMaterial}})}// 工具函數:保存原始材質并應用高亮材質const saveOriginalAndApplyHighlight = (mesh: THREE.Mesh) => {// 保存原始材質(處理單材質和多材質情況)if (!mesh.userData.originalMaterial) {if (Array.isArray(mesh.material)) {mesh.userData.originalMaterial = [...mesh.material]mesh.material = mesh.material.map(() =>highlightMaterial.current.clone(),)} else {mesh.userData.originalMaterial = mesh.materialmesh.material = highlightMaterial.current.clone()}} else {// 已保存過原始材質,直接應用高亮if (Array.isArray(mesh.material)) {mesh.material = mesh.material.map(() =>highlightMaterial.current.clone(),)} else {mesh.material = highlightMaterial.current.clone()}}}// 模型加載后初始化useEffect(() => {if (!modelRef.current) returnaddModel()const box = new THREE.Box3().setFromObject(modelRef.current)const center = new THREE.Vector3()box.getCenter(center)const size = new THREE.Vector3()box.getSize(size)const maxDim = Math.max(size.x, size.y, size.z)const fov = 100const cameraZ = Math.abs(maxDim / 2 / Math.tan((Math.PI * fov) / 360))camera.position.set(0, maxDim * 0.8, cameraZ * 1.2)camera.lookAt(0, 0, 0)// 遍歷模型設置通用屬性并標記可交互modelRef.current.traverse((child) => {if (child instanceof THREE.Mesh) {child.castShadow = truechild.receiveShadow = truechild.material.transparent = true// 標記為可交互(后續可通過此屬性過濾)child.userData.interactive = true}})// 綁定點擊事件監聽window.addEventListener('click', handleClick)// 組件卸載時清理return () => {window.removeEventListener('click', handleClick)// 移除所有高亮邊緣線highlightedMeshRef.current.forEach((mesh) => {removeHighlight(mesh)})edgeLines.current.clear()// 清理高亮狀態if (highlightedMeshRef.current) {restoreOriginalMaterial(highlightedMeshRef.current)}highlightedMeshRef.current = []}}, [modelRef.current])// 添加模型到管理器const addModel = () => {if (modelRef.current) {helper.addModel({id: '模型1',name: '模型1',url: url,model: modelRef.current,})}}return <primitive object={scene} ref={modelRef} />
}

?CityScene.tsx

import { Suspense, useRef } from 'react'
import { CityModel } from '../model/CityModal'
import { OrbitControls } from '@react-three/drei'export const CityScene = ({ modelUrl }: { modelUrl: string }) => {const controlsRef = useRef<any>(null)return (<><Suspense><CityModel url={modelUrl} />{/* 控制器 */}<OrbitControls ref={controlsRef} /></Suspense>{/* 環境光和方向光 */}<ambientLight intensity={0.5} color={0xffffff} /><directionalLightposition={[100, 200, 100]}intensity={3}castShadowcolor="#ffffff"shadow-mapSize-width={2048}shadow-mapSize-height={2048}/>{/* 環境貼圖 */}{/* 純色背景替代環境貼圖 */}<color attach="background" args={['#0a1a3a']} /></>)
}

?CityView.tsx

import { Canvas } from '@react-three/fiber'
import { CityScene } from './scene/CityScene'export const CityView = () => {const cityModelUrl = '/models/city-_shanghai-sandboxie.glb'return (<div className="w-[100vw] h-full absolute"><Canvas style={{ width: '100vw', height: '100vh' }}  shadows={true}   ><ambientLight /><CityScene modelUrl={cityModelUrl} /></Canvas></div>)
}

Home.tsx?

import { CityView } from './CityView'
import './index.less'
import { OperationPanel } from './OperationPanel'
export const Home = () => {return (<div className="screen-container"><CityView /><OperationPanel /></div>)
}

?操作面板有需要的話自取

OperationPanel.tsx

import { Space, Tag } from 'antd'
import { useModelManager, useModels } from '../../utils/viewHelper/viewContext'
import './index.less'
import * as THREE from 'three'export const OperationPanel = () => {const helper = useModelManager()const models = useModels()// 收集模型子對象const getMeshChildren = (model: THREE.Group | undefined): THREE.Mesh[] => {const meshes: THREE.Mesh[] = []model?.traverse((child) => {if (child instanceof THREE.Mesh) {meshes.push(child)}})return meshes}// 切換可見性const toggleMeshVisibility = (mesh: THREE.Mesh) => {mesh.visible = !mesh.visiblehelper.updateModelVisibility(mesh.uuid, mesh.visible) // 可選:通知管理器保存狀態}return (<div className="screen-operation absolute top-[10px] right-[30px] w-[20vw] h-[60vh] text-white z-10 ">{/* 面板標題欄 */}<div className=" px-[12px] py-3 shadow-lg"><span className="text-lg font-semibold tracking-wide">操作面板</span></div>{/* 模型列表 */}<div className="overflow-y-auto h-[calc(100%-30px)]">{models.map((model) => {const meshes = getMeshChildren(model.model)return (<div key={model.id} className="mt-[10px]">{/* 子對象列表 */}<div className="space-y-1 pl-[3px] pb-[6px]">{meshes.length > 0 ? (meshes.map((mesh) => (<divkey={mesh.uuid}// 核心樣式:狀態顏色 + 懸浮效果className={`px-[6px] my-[6px] rounded-md cursor-pointer transition-all duration-300 ease-out${/* 基礎樣式 */ 'shadow-md transform border border-transparent'}${/* 懸浮效果 */ 'hover:shadow-lg hover:shadow-blue-900/20'}${/* 顯示狀態 */ mesh.visible? 'bg-gradient-to-r from-[#47718b] to-[#3a5a70] text-[#fff] hover:border-[#8ec5fc]': 'bg-gradient-to-r from-[#2d3b45] to-[#1f2930] text-[#a0a0a0] hover:border-[#555]'}`}title={mesh.name}onClick={() => toggleMeshVisibility(mesh)}><div className="flex items-center justify-between"><span className="font-medium w-[14vw] overflow-hidden whitespace-nowrap text-ellipsis">{mesh.name}</span><Space><Tag>{mesh.type}</Tag><Tag color={mesh.visible ? 'success' : 'error'}>{' '}{mesh.visible ? '顯示中' : '已隱藏'}</Tag></Space></div></div>))) : (<div className="px-3 py-4 text-center text-gray-400 italic">無可用模型數據</div>)}</div></div>)})}</div></div>)
}

?模型管理用到的類和方法在參考這篇博客,不在贅述

Three.js 如何控制 GLB 模型的內置屬性實現精準顯示-CSDN博客

暗黑模式是怎么做的?

添加這個材質即可

    child.material.color.setStyle('#040912')

?

總結與擴展

本文通過 CityModel 組件的實現,詳細講解了 3D 模型交互中的核心技術點:

  1. 射線檢測是 3D 交互的基礎,掌握坐標轉換和交點檢測是關鍵
  2. 材質管理需考慮原始狀態保存和多材質場景處理
  3. 線框高亮通過幾何處理和父子關系實現,增強視覺反饋
  4. 資源清理在 Three.js 開發中尤為重要,直接影響應用性能

擴展方向

  • 支持框選多個模型(通過?RectAreaLight?或鼠標拖拽區域檢測)
  • 添加高亮動畫過渡(使用?gsap?等動畫庫實現材質屬性平滑過渡)
  • 結合后期處理(如?OutlinePass)實現更復雜的高亮效果
  • 優化大規模場景性能(使用 LOD、實例化、視錐體剔除)

掌握這些技巧后,你可以輕松實現更復雜的 3D 交互功能,為用戶帶來沉浸式的 Web 3D 體驗。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/88863.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/88863.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/88863.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

卷積神經網絡實戰:MNIST手寫數字識別

夜漸深&#xff0c;我還在&#x1f618; 老地方 睡覺了&#x1f64c; 文章目錄&#x1f4da; 卷積神經網絡實戰&#xff1a;MNIST手寫數字識別&#x1f9e0; 4.1 預備知識?? 4.1.1 torch.nn.Conv2d() 三維卷積操作&#x1f4cf; 4.1.2 nn.MaxPool2d() 池化層的作用&#x1f4…

HarmonyOS應用無響應(AppFreeze)深度解析:從檢測原理到問題定位

HarmonyOS應用無響應&#xff08;AppFreeze&#xff09;深度解析&#xff1a;從檢測原理到問題定位 在日常應用使用中&#xff0c;我們常會遇到點擊無反應、界面卡頓甚至完全卡死的情況——這些都可能是應用無響應&#xff08;AppFreeze&#xff09; 導致的。對于開發者而言&am…

湖北設立100億元人形機器人產業投資母基金

湖北設立100億元人形機器人產業投資母基金 湖北工信 2025年07月08日 12:03 湖北 &#xff0c;時長01:20 近日&#xff0c;湖北設立100億元人形機器人產業投資母基金&#xff0c;重點支持人形機器人和人工智能相關產業發展。 人形機器人產業投資母基金由湖北省財政廳依托省政府…

時序預測 | Pytorch實現CNN-LSTM-KAN電力負荷時間序列預測模型

預測效果 代碼主要功能 該代碼實現了一個結合CNN&#xff08;卷積神經網絡&#xff09;、LSTM&#xff08;長短期記憶網絡&#xff09;和KAN&#xff08;Kolmogorov-Arnold Network&#xff09;的混合模型&#xff0c;用于時間序列預測任務。主要流程包括&#xff1a; 數據加…

OCR 識別:車牌識別相機的 “火眼金睛”

車牌識別相機在交通管理、停車場收費等場景中&#xff0c;需快速準確識別車牌信息。但實際環境中&#xff0c;車牌可能存在污漬、磨損、光照不均等情況&#xff0c;傳統識別方式易出現誤讀、漏讀。OCR 技術讓車牌識別相機如虎添翼。它能精準提取車牌上的字符&#xff0c;不管是…

Java面試基礎:面向對象(2)

1. 接口里可以定義哪些方法抽象方法&#xff1a;抽象方法是接口的核心部分&#xff0c;所有實現接口的類都必須實現這些方法。抽象方法默認是 public 和 abstract 修飾&#xff0c;這些修飾符可以省略。public interface Animal {void Sound(); }默認方法&#xff1a;默認方法是…

有哪些更加簡潔的for循環?循環語句?

目錄 簡潔的for循環 循環過程修改循環變量 循環語句 不同編程語言支持的循環語句 foreach 無限循環 for循環歷史 break和continue 循環判斷結束值 循環標簽 循環語句優化 循環表達式返回值 簡潔的for循環 如果需要快速枚舉一個集合的元素&#xff0c;盡管C語言可以…

RK3568/3588 Android 12 源碼默認使用藍牙mic錄音

遇到客戶一個需求&#xff0c;如果連接了帶mic的藍牙耳機&#xff0c;默認所有的錄音要走藍牙mic通道。這個功能搞了好久&#xff0c;終于搞定了。1. 向RK尋求幫助&#xff0c;先打通 bt sco能力。此時&#xff0c;還無法默認就切換到藍牙 mic通道&#xff0c;接下來我們需求默…

解鎖HTTP:從理論到實戰的奇妙之旅

目錄一、HTTP 協議基礎入門1.1 HTTP 協議是什么1.2 HTTP 協議的特點1.3 HTTP 請求與響應的結構二、HTTP 應用場景大揭秘2.1 網頁瀏覽2.2 API 調用2.3 文件傳輸2.4 內容分發網絡&#xff08;CDN&#xff09;2.5 流媒體服務三、HTTP 應用實例深度剖析3.1 使用 JavaScript 的 fetc…

uvm_config_db examples

通過uvm_config_db類訪問的UVM配置數據庫,是在多個測試平臺組件之間傳遞不同對象的絕佳方式。 methods 有兩個主要函數用于從數據庫中放入和檢索項目,分別是 set() 和 get()。 static function void set ( uvm_component cntxt,string inst_name,string …

(C++)任務管理系統(文件存儲)(正式版)(迭代器)(list列表基礎教程)(STL基礎知識)

目錄 前言&#xff1a; 源代碼&#xff1a; 代碼解析&#xff1a; 一.頭文件和命名空間 1. #include - 輸入輸出功能2. #include - 鏈表容器3. #include - 字符串處理4. using namespace std; - 命名空間 可視化比喻&#xff1a;建造房子 &#x1f3e0; 二.menu()函數 …

Java 中的異步編程詳解

前言 在現代軟件開發中&#xff0c;異步編程&#xff08;Asynchronous Programming&#xff09; 已經成為構建高性能、高并發應用程序的關鍵技術之一。Java 作為一門廣泛應用于后端服務開發的語言&#xff0c;在其發展過程中不斷引入和優化異步編程的支持。從最初的 Thread 和…

MySQL邏輯刪除與唯一索引沖突解決

問題背景 在MySQL數據庫設計中&#xff0c;邏輯刪除&#xff08;軟刪除&#xff09;是一種常見的實踐&#xff0c;它通過設置標志位&#xff08;如is_delete&#xff09;來標記記錄被"刪除"&#xff0c;而不是實際刪除數據。然而&#xff0c;當表中存在唯一約束時&am…

php命名空間用正斜杠還是反斜杠?

在PHP中&#xff0c;命名空間使用反斜杠&#xff08;\&#xff09;作為分隔符&#xff0c;這是PHP語言規范明確規定的。反斜杠在命名空間中扮演路徑分隔的角色&#xff0c;用于區分不同層級的命名空間。 具體說明&#xff1a;語法規則 PHP命名空間使用反斜杠&#xff08;\&…

《從依賴糾纏到接口協作:ASP.NET Core注入式開發指南》

在C#的ASP.NET Core開發中&#xff0c;依賴注入絕非簡單的技術技巧&#xff0c;而是重構代碼關系的底層邏輯。它像一套隱形的神經網絡&#xff0c;讓程序模塊擺脫硬編碼的束縛&#xff0c;在運行時實現動態連接&#xff0c;從而為系統注入可測試、可進化的核心生命力。理解其深…

星云ERP本地環境搭建筆記

看到星云ERP兩個比較實用的功能&#xff0c;編號規則和打印模板&#xff0c;如下圖所示&#xff0c;于是本地跑起來學習學習。開發環境必備&#xff1a;1. JDK 1.82. MySQL 5.73. Redis 44. RabbitMQ 3.12.45. nodejs 206. pnpm 9.7.1 (npm install -g pnpm9.7.1)其他開發工具&…

RedisJSON 的 `JSON.ARRAPPEND`一行命令讓數組動態生長

1 、 為什么選擇 JSON.ARRAPPEND 在傳統的鍵值模型里&#xff0c;若要往數組尾部追加元素&#xff0c;通常需要 取→改→寫 三步&#xff1a; GET 整個 JSON&#xff1b;在應用層把元素 push 進數組&#xff1b;SET 回 Redis。 一條 JSON.ARRAPPEND 則可一次完成&#xff0c;具…

14:00開始面試,14:08就出來了,問的問題有點變態。。。

從小廠出來&#xff0c;沒想到在另一家公司又寄了。 到這家公司開始上班&#xff0c;加班是每天必不可少的&#xff0c;看在錢給的比較多的份上&#xff0c;就不太計較了。沒想到4月一紙通知&#xff0c;所有人不準加班&#xff0c;加班費不僅沒有了&#xff0c;薪資還要降40%…

Unity物理系統由淺入深第四節:物理約束求解與穩定性

Unity物理系統由淺入深第一節&#xff1a;Unity 物理系統基礎與應用 Unity物理系統由淺入深第二節&#xff1a;物理系統高級特性與優化 Unity物理系統由淺入深第三節&#xff1a;物理引擎底層原理剖析 Unity物理系統由淺入深第四節&#xff1a;物理約束求解與穩定性 物理引擎的…

深入淺出Kafka Consumer源碼解析:設計哲學與實現藝術

一、Kafka Consumer全景架構 1.1 核心組件交互圖 #mermaid-svg-JDEEOd2M5PzLkYa6 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-JDEEOd2M5PzLkYa6 .error-icon{fill:#552222;}#mermaid-svg-JDEEOd2M5PzLkYa6 .erro…