news 2026/10/13 9:11:22

纯前端商城系统实战:HTML5+JavaScript+CSS购物系统全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
纯前端商城系统实战:HTML5+JavaScript+CSS购物系统全解析

简介:基于HTML5、JavaScript与CSS的模拟联想购物商城完整项目,适合初中级前端开发者对照学习动态网页构建与数据交互。资源含185个文件,压缩包约7.97MB,以78张JPG、54张PNG图片素材为主,另有19个JS、15个HTML、14个CSS,以及2个PSD、1个ICO、1个GIF和1个DOC文档。系统完整实现动态浏览、关键词检索、用户注册登录与商品增删改,利用AJAX异步刷新数据,CSS3媒体查询保证响应式布局,HTML5语义化标签优化结构。DOC文档详述运行配置步骤,便于快速部署,可深入理解商城前端功能模块与交互逻辑,并锻炼表单验证与数据检索的编程思维。现已有3787人学习/下载,对入门商城开发与前端实践有参考价值。

1. 拿 HTML5+JavaScript+CSS 手搓商城:为什么不推荐一上来就上框架

很多人一听到「商城系统」,第一反应是 Vue、React 配一套 Node 后端,再挂个数据库。但如果你手里这份资源标题写的是 html5+javascript+css 商城(模拟联想)购物系统,说明它定位的是纯前端、可静态托管、以模拟数据驱动的那一类项目——这种资源对入门者和前端基础不牢的从业者反而更有价值。它解决的核心问题不是「高并发下单」,而是让你在一套纯 HTML/CSS/JS 的体系里,完整跑通注册登录、动态浏览、信息检索、增删改查这一整条链路。适合谁?适合正在做课设、想补齐原生 JS 基本功、或者需要在没有后端的情况下快速搭一个可演示购物系统的人。下面我按拆解资源的思路,把这套系统的实现路径、参数设置和隐性坑逐个说清楚。

2. 拆解「模拟联想」购物系统的三层架构:数据层、渲染层、交互层

2.1 为什么要用“模拟数据 + 本地存储”而不是真实后端

这套系统的核心是「模拟」。所谓模拟联想,通俗讲就是所有商品数据、用户数据、购物车数据都不走服务器,而是落在本地存储或者写死的 JS 数组里。常见做法是维护一个db.js,里面放商品列表、用户列表、分类字典,然后用localStorage做持久化。

我一般会在动手前先明确一个原则:数据层决定了整套系统的复杂度上限。如果你准备用真实后端,那注册登录要管会话、要管密码加密、要管接口鉴权,这套资源的主题就变味了。而纯前端模拟的好处是——你把注意力全部集中在 DOM 操作、事件绑定、数据流管理这些前端基本功上。等以后切到 Vue 或者 React,你会发现「数据驱动视图」的思路完全能迁移过去。

// db.js - 模拟数据源 const mockDB = { users: [ { id: 1, username: 'demo', password: '123456', email: 'demo@test.com' } ], products: [ { id: 101, name: '无线机械键盘', category: '外设', price: 399, stock: 50, img: './img/kb.jpg' }, { id: 102, name: '4K 显示器', category: '显示', price: 1999, stock: 12, img: './img/monitor.jpg' }, { id: 103, name: '人体工学椅', category: '家具', price: 1499, stock: 8, img: './img/chair.jpg' } ], cart: [] };

这段代码的逻辑很直白:users存账号,products存商品,cart存当前用户的购物车。注意cart放在mockDB里只是临时占位,真正持久化要用localStorage.setItem('cart_', JSON.stringify(cartArr))这种方式按用户隔离。参数上,price建议用整数单位(元),避免浮点误差;stock库存字段在模拟系统里常被忽略,但加上之后,商品详情页的「已售罄」状态才能正常展示。

2.2 商品渲染的两种方案:字符串拼接 vs 模板克隆

动态浏览是这套系统的重头戏。商品列表从数组渲染到页面上,新手最容易写成innerHTML字符串拼接,数据一变就整块重绘。这种方式不是不行,但会有两个隐患:一是商品卡片里如果嵌了用户输入内容,容易出现 XSS;二是整块重绘会让图片重新加载,页面闪烁明显。

更稳的方案是「模板克隆」,提前在 HTML 里写好一个<template>标签,JS 里通过document.importNode复制节点,再填充数据。这样做的好处是 DOM 结构清晰,事件监听可以绑定到容器上做事件委托,性能也更好。

// renderProducts.js - 模板克隆渲染商品列表 const template = document.getElementById('product-template'); const container = document.querySelector('#product-list'); function renderProducts(list) { container.innerHTML = ''; // 清空容器 list.forEach(item => { const node = template.content.cloneNode(true); node.querySelector('.product-name').textContent = item.name; node.querySelector('.product-price').textContent = '¥' + item.price; node.querySelector('.product-stock').textContent = '库存 ' + item.stock; node.querySelector('.add-cart-btn').dataset.id = item.id; container.appendChild(node); }); }

这段代码里,cloneNode(true)的true表示深克隆,子节点全部复制。textContent赋值比innerHTML安全,因为商品名里如果意外带上了<script>标签,它会被当作纯文本展示而不是执行。dataset.id是 HTML5 标准的数据传递方式,在事件委托里用event.target.closest('.add-cart-btn').dataset.id就能拿到商品 ID。注意一个参数:如果你用的是innerHTML拼模板,记得对用户可控字段做转义,这个后面避坑章会细说。

2.3 注册登录模块:密码处理与会话标记

注册登录看起来简单,但「模拟」不等于「随便写」。真正用心做的资源,会在密码处理上至少做一层btoa编码或简单的哈希,而不是明文存储。这里我会用 Web Crypto API 的crypto.subtle.digest做 SHA-256 哈希,虽然它要求 HTTPS 或 localhost 环境,但本地打开 HTML 文件时浏览器通常允许。

// auth.js - 注册登录逻辑 async function hashPassword(password) { const encoder = new TextEncoder(); const data = encoder.encode(password); const hash = await crypto.subtle.digest('SHA-256', data); return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join(''); } async function register(username, password, email) { const users = JSON.parse(localStorage.getItem('users') || '[]'); if (users.find(u => u.username === username)) { return { success: false, msg: '用户名已存在' }; } users.push({ id: Date.now(), username: username, password: await hashPassword(password), email: email }); localStorage.setItem('users', JSON.stringify(users)); return { success: true }; } function login(username, password) { const users = JSON.parse(localStorage.getItem('users') || '[]'); const user = users.find(u => u.username === username); if (!user) return { success: false, msg: '用户不存在' }; // 登录时同步比对哈希值,不能直接比明文 hashPassword(password).then(hash => { if (user.password === hash) { sessionStorage.setItem('currentUser', JSON.stringify({ username: user.username })); return { success: true }; } return { success: false, msg: '密码错误' }; }); }

这段代码有个值得注意的点:crypto.subtle.digest返回的是 Promise,所以register和login都不是同步函数。你在实际调的时候,如果写的是const result = login(...)然后立刻判断result.success,大概率拿到的会是undefined。我一般会在封装层面统一用async/await,UI 层也同步改成异步流程。Date.now()做用户 ID 在本地场景够用,但如果你后续要导入导出用户数据,建议改成自增计数器存在localStorage里,避免多标签页同时注册产生 ID 冲突。

3. 信息检索与商品管理:从检索逻辑到增删改查的完整闭环

3.1 搜索过滤的关键参数:关键词、分类、价格区间

信息检索在商城系统里对应的是「搜索 + 筛选」功能。这里有两个常见设计:一是搜索框输入关键词,二是在侧边栏点分类或价格区间。实现上,本质都是对products数组做过滤。

// filter.js - 多条件过滤 function getFilteredProducts({ keyword = '', category = '', minPrice = 0, maxPrice = Infinity, sortBy = 'default' }) { let result = mockDB.products.filter(item => { const matchKeyword = item.name.toLowerCase().includes(keyword.toLowerCase()); const matchCategory = category ? item.category === category : true; const matchPrice = item.price >= minPrice && item.price <= maxPrice; return matchKeyword && matchCategory && matchPrice; }); if (sortBy === 'price-asc') { result.sort((a, b) => a.price - b.price); } else if (sortBy === 'price-desc') { result.sort((a, b) => b.price - a.price); } return result; }

这里的参数设置有几个细节:keyword用toLowerCase()做大小写归一,避免用户搜「KEYBOARD」搜不到「keyboard」;分类过滤用全等比较,所以分类名称必须跟db.js里完全一致,一个空格都不能差——这是很常见的翻车点;价格区间用Infinity做默认上限,比999999这种魔法数字更优雅。排序函数里a.price - b.price升序,b.price - a.price降序,如果后续要加「销量排序」或「最新上架」,直接在这个sortBy分支里扩展就行。

3.2 商品增删改:表单校验与列表联动更新

管理员维护商品这块,需要做到「改完数据,页面立刻同步」。很多人习惯改动数组后重新调localStorage.setItem,但容易忘记重新渲染。我常用的模式是「中央数据源 + 订阅渲染」——所有增删改操作统一走updateProducts(newList)函数,函数内部先写存储,再触发渲染。

// admin.js - 商品增删改 function saveProduct(product) { const products = JSON.parse(localStorage.getItem('products') || '[]'); if (product.id) { // 编辑:根据 id 找到索引替换 const idx = products.findIndex(p => p.id === product.id); if (idx !== -1) products[idx] = { ...products[idx], ...product }; } else { // 新增:生成新 id product.id = Date.now(); products.push(product); } localStorage.setItem('products', JSON.stringify(products)); renderProducts(products); return product.id; } function deleteProduct(id) { let products = JSON.parse(localStorage.getItem('products') || '[]'); products = products.filter(p => p.id !== id); localStorage.setItem('products', JSON.stringify(products)); renderProducts(products); }

参数说明:编辑时用{ ...products[idx], ...product }做对象合并,好处是表单里没填的字段能保留原值。新增时用Date.now()做 ID,但要注意同一毫秒内连续新增两个商品会 ID 冲突,稳妥做法是维护一个nextId计数器。deleteProduct里不能直接let products = mockDB.products然后splice,因为这样会改动原数组引用。表单提交前要校验必填字段和价格格式,我习惯在submit事件里e.preventDefault()阻止默认提交,然后手动读取FormData。

3.3 购物车与订单流程:数量加减、总价计算、库存校验

购物车是商城系统的「心脏」。这里核心是管理cart数组,每项包含productId、quantity、selected三个字段。数量加减要触发总价重算,同时校验库存上限;结算时要判断是否勾选商品和库存是否足够。

// cart.js - 购物车核心操作 const cart = JSON.parse(localStorage.getItem('cart_' + currentUser.username)) || []; function addToCart(productId, quantity = 1) { const product = mockDB.products.find(p => p.id === productId); const cartItem = cart.find(i => i.productId === productId); if (cartItem) { if (cartItem.quantity + quantity > product.stock) { alert('库存不足,当前库存 ' + product.stock); return false; } cartItem.quantity += quantity; } else { cart.push({ productId, quantity, selected: true }); } saveCart(); return true; } function updateQuantity(productId, newQuantity) { const cartItem = cart.find(i => i.productId === productId); const product = mockDB.products.find(p => p.id === productId); if (newQuantity > product.stock) { newQuantity = product.stock; // 强制上限 } if (newQuantity <= 0) { removeFromCart(productId); return; } cartItem.quantity = newQuantity; saveCart(); }

这段代码容易被忽略的是「更新数量时不能直接改输入框的 value」——比如用户手输 999,必须做 clamp 到库存上限。selected字段是为了支持「勾选部分结算」,在计算总价时记得filter(i => i.selected)而不是把全部购物车项加起来。saveCart()内部要做的是localStorage.setItem('cart_' + username, JSON.stringify(cart)),这个 key 带用户名才能做到多用户购物车隔离。很多资源在这块偷懒用同一个 key,A 用户加购的商品 B 用户登录也能看到,属于明显 bug。

4. 布局与交互细节:响应式样式、事件委托与本地存储的边界

4.1 响应式布局:媒体查询的断点设置

购物系统的页面通常包含顶部导航、商品推荐区、侧边筛选栏、购物车抽屉。纯 CSS 做响应式,关键在于断点的选择。我一般设三个断点:768px(平板竖屏)、1024px(平板横屏/小笔记本)、1200px(桌面宽屏)。低于768px时,侧边栏改为折叠菜单,商品卡片网格从 4 列降为 1 列。

/* style.css - 响应式核心设置 */ .product-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; padding: 16px; } @media (max-width: 1200px) { .product-grid { grid-template-columns: repeat(3, 1fr); } } @media (max-width: 1024px) { .product-grid { grid-template-columns: repeat(2, 1fr); } .sidebar { display: none; } /* 侧边栏隐藏,改用顶部下拉筛选 */ } @media (max-width: 768px) { .product-grid { grid-template-columns: 1fr; } .nav-cart { font-size: 14px; } }

这里有个细节:断点数值不是拍脑袋定的,要结合你页面里最宽的元素来定。比如商品卡片如果最小宽度 240px,那么grid-template-columns: repeat(4, 1fr)在 1024px 宽度下,单张卡片剩余宽度只有 (1024 - 3*16) / 4 ≈ 244px,刚好能放下;如果再小就会溢出。1fr是等分剩余空间的意思,比固定px更适合这种场景。还有一个容易踩的坑是gap属性在旧版 Safari 上不生效,如果你要兼容,改成在每个卡片上写margin-right和margin-bottom,或者用padding方案。

4.2 事件委托机制:为什么不能给每个按钮单独绑事件

商品列表里的「加入购物车」按钮可能有几十个,如果每个按钮都addEventListener,不仅内存占用高,动态新增商品后还要重新绑事件。事件委托的做法是「把监听器绑在父容器上,利用事件冒泡找到真正触发的按钮」。

// event-delegation.js document.querySelector('#product-list').addEventListener('click', event => { const addBtn = event.target.closest('.add-cart-btn'); if (!addBtn) return; // 点击的不是加购按钮 const productId = Number(addBtn.dataset.id); const product = mockDB.products.find(p => p.id === productId); if (product && product.stock > 0) { addToCart(productId); } else { alert('该商品暂时无货'); } });

closest方法会从当前元素向上查找匹配选择器的祖先节点,即使你点到了按钮内部的<span>图标,也能正确找到.add-cart-btn。Number(addBtn.dataset.id)这步很关键,因为dataset拿到的永远是字符串,如果不转数字,后面find(p => p.id === productId)用的是严格全等,字符串'101'不等于数字101,会直接匹配失败。很多同学在这翻车,排查半天发现是类型不一致。

4.3 localStorage 的容量边界与多标签页同步

localStorage 的存储上限通常 5MB 左右,对图片以外的文本数据完全够用,但有两个边界问题值得注意。一是不能存函数,它只能序列化 JSON 支持的简单类型;二是多标签页数据不同步。如果你打开两个商品管理页,在 A 页改了商品价格,B 页不会自动刷新。

解决多标签页同步,标准方案是监听storage事件:

window.addEventListener('storage', event => { if (event.key === 'products') { const updatedProducts = JSON.parse(event.newValue); renderProducts(updatedProducts); } });

注意这个事件只在「其他标签页」触发,当前标签页自己setItem是不会触发监听回调的,所以别指望它替代自己的渲染调用。event.newValue在removeItem时是null,要做判空处理。这套机制适合商品管理这种低频写入场景,如果购物车频繁变化,我建议直接把渲染函数抽成公共方法,在写入点手动同步调用。

5. 避坑与常见问题排查:六个高频翻车现场

5.1 登录后刷新就掉线:sessionStorage 依赖混乱

现象:登录成功后进入商品页,一切正常;刷新页面或跳转后,登录态消失,直接退回登录页。

原因:资源里用了sessionStorage保存当前用户信息,但sessionStorage的生命周期是「标签页会话」,你在同一个标签页刷新不会丢,跳转到新页面如果开了新标签页或浏览器设置还原会话,就会丢。更隐蔽的是项目里有的地方用localStorage读用户信息,有的地方用sessionStorage,两边不同步。

解决:统一用一个CURRENT_USER_KEY常量,比如const CURRENT_USER_KEY = 'current_user',登录时sessionStorage.setItem(CURRENT_USER_KEY, JSON.stringify(user));在index.html的入口脚本最开始加一段「会话检查」,读不到就location.href = 'login.html'。如果想做得更健壮,改成localStorage持久化登录态,并提供一个「记住我」勾选框控制。

5.2 商品列表空白:JSON 解析失败或路径错误

现象:页面打开后商品区域空白,控制台报Unexpected token '<' in JSON或者404 Not Found。

原因:前者是JSON.parse解析了 HTML 文件内容,通常是db.js的<script src>路径写错,服务器返回了index.html的 HTML 当作 JS 来解析;后者是相对路径问题,比如./img/kb.jpg在你把 HTML 文件放到子目录后失效。

解决:先用fetch('./data/products.json')的方式加载数据并打印response.text()看内容;如果是数据文件,检查src路径层级。我习惯把所有资源相对路径写成./开头,并且只在项目根目录运行,不搞多级子目录嵌套。静态图片建议用 CSP 白名单或统一加onerror回调显示占位图,不要指望路径永远正确。

5.3 加购数量变成 NaN 或负数:parseInt 与输入框取值

现象:商品详情页输入数量 2,点击加购后购物车里数量变成NaN;或者手动清空输入框后数量变成 -1。

原因:输入框的值是字符串,parseInt对空字符串返回NaN;减号按钮没做最小值限制,quantity - 1得到负数。

解决:加一个统一的parseQuantity函数:

function parseQuantity(inputValue, maxStock) { let qty = parseInt(inputValue, 10); // 第二个参数必须传 if (isNaN(qty)) qty = 1; qty = Math.max(1, Math.min(qty, maxStock)); return qty; }

这里parseInt(inputValue, 10)的第二个参数10是告诉浏览器按十进制解析,否则老浏览器遇到'08'这种可能按八进制解析。Math.max(1, Math.min(qty, maxStock))是「双夹紧」,小于 1 强制为 1,大于库存强制为库存。我见过有人直接+inputValue做隐式转换,+'abc'会得到NaN,别用那套。

5.4 修改商品后列表没变化:数据源引用没更新

现象:管理员后台编辑了一个商品的价格,保存后前台列表还是旧价格;刷新页面后新价格才出来。

原因:renderProducts内部读的是mockDB.products,但编辑时写的是localStorage,没有把mockDB和localStorage同步;或者saveProduct里products[idx] = {...}创建了新对象,但mockDB.products里还是旧对象引用。

解决:做一个统一的「同步源」函数,编辑完成后重新把localStorage的内容灌回mockDB:

function reloadProducts() { mockDB.products = JSON.parse(localStorage.getItem('products')) || []; renderProducts(mockDB.products); }

所有数据变更点只写localStorage,然后调用reloadProducts()。这套「单写单读」模式虽然笨,但能杜绝双数据源不一致的毛病。如果觉得麻烦,干脆连mockDB.products的同步读都省了,渲染时直接从localStorage读数组。

5.5 按钮点击无效:事件重复绑定与 DOM 覆盖

现象:第一次点击加购按钮有效,第二次页面刷新后按钮点击完全没反应。

原因:renderProducts每执行一次,父容器innerHTML被清空重绘,旧的事件委托绑定还在,但页面上实际生效的监听器因为挂到了旧 DOM 节点上而「丢失」。

解决:事件委托绑定的是父容器,不要绑在动态生成的子元素上。检查代码里有没有document.querySelectorAll('.add-cart-btn').forEach(btn => btn.addEventListener(...))这类写法,一旦列表重绘,这些按钮全部是新的 DOM 节点,旧的绑定自然无效。统一改成前面第 4 章的事件委托模式,只在#product-list上绑一次。

5.6 商品图片加载失败闪红叉:懒加载与占位图方案

现象:商品区加载时图片区域出现大片空白或浏览器默认的坏图图标,影响演示效果。

原因:模拟数据里的img路径是假的或图片不存在,<img>标签加载失败后原样显示破图。

解决:给图片加onerror处理,并做一个本地 SVG 占位图:

node.querySelector('.product-img').addEventListener('error', function() { this.src = './assets/placeholder.svg'; this.alt = '图片暂不可用'; });

在商品卡片渲染时给<img>加loading="lazy"属性,首屏之外的图片延迟加载。如果资源包里有现成图片素材,检查图片文件名有没有中文、空格或特殊字符——这类字符在本地 file 协议下能打开,部署到服务器后经常 404。我在一个模拟项目里就因为图片名带了个括号,上线后整页红叉,排查了半小时。

6. 让购物系统的演示效果再上一个台阶:商品推荐联动与一键重置数据

当你的商城已经能正常完成注册、登录、浏览、搜索、加购、结算这一整套流程后,剩下的工作是把「演示效果」打磨得更像真实产品。这里分享一个低成本高收益的技巧:做「商品推荐联动」——根据用户最近浏览的商品分类,在首页展示同类商品的推荐列表。

// recommend.js - 基于浏览历史的简单推荐 function getRecommendations(productId) { const currentProduct = mockDB.products.find(p => p.id === productId); if (!currentProduct) return []; // 记录浏览历史:存最近 5 个商品 id let history = JSON.parse(localStorage.getItem('view_history')) || []; history = history.filter(id => id !== productId); history.unshift(productId); localStorage.setItem('view_history', JSON.stringify(history.slice(0, 5))); // 推荐逻辑:同分类随机取 4 个 const sameCategory = mockDB.products.filter(p => p.category === currentProduct.category && p.id !== productId); const shuffled = sameCategory.sort(() => Math.random() - 0.5); return shuffled.slice(0, 4); }

这个函数在每次渲染商品详情页时调用,把返回的数组传到已有的renderProducts里,标题改成「看了又看」。view_history用数组头部插入的方式维护最近浏览,slice(0, 5)控制长度。注意Math.random() - 0.5这种洗牌方式效率不高,但推荐列表最多几十条数据,性能完全够用,不必追求 Fisher-Yates 洗牌。

再给一个实用技巧:准备一个「一键重置数据」按钮,方便演示前把脏数据清干净。很多资源没有这个功能,导致你演示到一半发现购物车里有上次测试的残留数据。

function resetAllData() { // 保留初始用户,清掉新增用户和所有业务数据 localStorage.setItem('users', JSON.stringify([{ id: 1, username: 'demo', password: 'e99a18c428cb38d5f260853678922e03', email: 'demo@test.com' }])); localStorage.setItem('products', JSON.stringify(mockDB.products)); localStorage.removeItem('cart_demo'); localStorage.removeItem('view_history'); location.reload(); }

这个函数里只保留了 demo 用户的哈希密码,其他测试产生的用户、购物车、浏览记录全部清除。注意location.reload()放在最后,确保所有界面从干净状态重启。我在演示前都有一个强制习惯:先开隐私窗口跑一遍完整流程,看是否有缓存残留;再点一次「重置数据」按钮确认列表全部恢复初始值。这套流程走完后,再开始给同事或导师演示,基本不会出现「哎,购物车怎么有东西」的尴尬场面。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 9:11:21

Android Studio新手避坑指南:从环境配置到第一个App运行

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 9:11:17

593张手机图:YOLO轻量模型移动端部署最小有效数据集

简介&#xff1a;本资源是一套专为YOLO系列目标检测算法训练与验证设计的手机图像数据集&#xff0c;面向计算机视觉初学者、算法工程师及AI项目开发者&#xff0c;解决移动端目标检测模型训练缺乏高质量标注数据的痛点。数据集共1780个文件&#xff0c;包含593张JPG格式手机实…

作者头像 李华
网站建设 2026/10/13 9:10:32

VHDL数据类型与顺序语句实战:从类型转换到综合结果的避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 9:08:41

Qt QComboBox 常用方法详解与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 9:07:37

C语言深拷贝实现:从链表复制理解内存安全与算法鲁棒性

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华