
简介本资源是一套基于黑马程序员前端教程实践的「小兔鲜」电商网站静态页面项目面向HTML/CSS/JavaScript初学者与前端入门学习者用于系统掌握语义化HTML5结构、响应式布局、表单交互及购物车动态逻辑等核心前端开发能力。压缩包共179个文件含6个HTML主页面登录、首页、购物车、个人中心等、7个CSS样式文件如denglu.css、shopping_car.css覆盖各页面独立样式与base.css基础重置、4个JS脚本支撑登录验证、购物车增删改等交互、以及162张图片资源JPG/PNG和1个ICO图标整体大小12.78MB结构清晰、模块分离明确。已有4377人学习下载资源完整呈现了从页面搭建、样式美化到基础JS交互的全流程实现附带多页面联动逻辑与适配移动端的媒体查询实践是巩固前端三件套知识、理解真实项目组织方式的优质练手素材。1. 小兔鲜五个核心页面登录、首页、分类、购物车、个人中心的前端工程落地实录这不是一个 UI 设计稿转代码的“切图任务”而是一次典型的中型电商类 H5 应用从零搭建的闭环实践。所谓“小兔鲜”本质是一个轻量级生鲜快消品类的移动端 Web 应用原型覆盖用户从身份认证登录、商品浏览首页/分类、决策加购购物车到身份归属个人中心的完整链路。它不追求大而全的后台能力但对路由跳转一致性、表单校验鲁棒性、本地状态同步及时性、跨页面数据共享合理性有明确要求——这些恰恰是很多新手在 Vue/React 项目里写到第三页就开始“状态散落、路由混乱、localStorage 脏读”的高发区。本文面向已掌握基础 HTML/CSS/JS 和任一主流框架以 Vue 3 Pinia Vue Router 为例的开发者目标很实在用一套可复现、可调试、可延展的最小可行结构把这五个页面串成一条不掉链子的业务流。过程中不绕开 localStorage 容错、token 过期重定向、购物车数量 badge 同步、页面返回栈管理等真实高频问题。你不需要懂 Node 后端但需要知道前端如何“假装”和后端协同。2. 项目骨架与技术选型为什么是 Vue 3 Pinia Vue Router 而不是其他组合2.1 选型逻辑轻量、可控、调试友好“小兔鲜”定位是快速验证流程与交互的前端 Demo不是长期维护的 SaaS 系统。因此技术栈必须满足三个硬指标启动快npm create vuelatest三分钟内完成初始化无 Webpack 配置地狱状态边界清晰五个页面间需共享登录态、购物车数据但又不能让首页逻辑意外修改个人中心的头像缓存调试即所见当购物车数量没更新能立刻在 DevTools 的响应式面板里看到cartList数组是否真的 push 了新项而不是在 Redux DevTools 里翻十层嵌套对象。Vue 3 的 Composition API 天然支持逻辑按功能拆分如useAuth()、useCart()Pinia 作为官方推荐的状态库API 极简defineStore一行声明store.cartList.push(item)直接操作且支持 SSR 友好序列化Vue Router 4 的router.beforeEach可精准拦截未登录访问/cart的跳转并重定向至/login?redirect/cart—— 这种控制粒度在 React Router v6 的Navigate组件里需要额外封装useRequireAuthHook 才能等效实现对新手不够直白。提示本方案不引入 TypeScript 初始配置因“五个页面”规模下类型定义收益小于学习成本但所有接口调用均预留any类型占位后续补类型只需改一处interface ApiResponseT即可。2.2 初始化命令与目录结构约定执行以下命令创建项目全程选择默认选项仅在最后一步启用 Pinia 和 Routernpm create vuelatest # 项目名xiao-tu-xian # 是否添加 TypeScript → No # 是否添加 JSX 支持 → No # 是否添加 Vue Router → Yes # 是否添加 Pinia → Yes # 是否添加 Vitest → No验证阶段暂不加单元测试 # 是否添加 Cypress → No # 是否添加 ESLint → Yes选 Prettier Standard 风格生成后按业务域重组织src/目录非 Vue 默认结构src/ ├── assets/ # 全局静态资源图标、字体、公共 CSS 变量 ├── components/ # 可复用原子组件Button、Input、Badge ├── composables/ # 自定义组合式函数useAuth、useCart、useRequest ├── layouts/ # 页面布局容器DefaultLayout.vue含 TabBar ├── pages/ # 五个核心页面Login.vue, Home.vue, Category.vue, Cart.vue, Profile.vue ├── stores/ # Pinia 状态仓库auth.ts, cart.ts ├── utils/ # 工具函数http 请求封装、时间格式化、防抖 ├── router/ # 路由配置index.ts含路由守卫 └── main.ts # 入口文件挂载 router/store此结构刻意规避“pages 下再建 index.vue”的嵌套冗余每个页面文件名即路由路径/login→pages/Login.vue降低路径映射心智负担。2.3 路由配置五页路由 命名视图 重定向守卫src/router/index.ts内容如下关键点已注释import { createRouter, createWebHistory } from vue-router import DefaultLayout from /layouts/DefaultLayout.vue // 懒加载页面提升首屏性能即使只有 5 页也养成习惯 const Login () import(/pages/Login.vue) const Home () import(/pages/Home.vue) const Category () import(/pages/Category.vue) const Cart () import(/pages/Cart.vue) const Profile () import(/pages/Profile.vue) const router createRouter({ history: createWebHistory(), routes: [ // 登录页独立布局不套 DefaultLayout { path: /login, name: Login, component: Login, meta: { requiresAuth: false } // 显式标记无需登录 }, // 其他四页共用底部 TabBar 布局 { path: /, component: DefaultLayout, children: [ { path: , name: Home, component: Home, meta: { tab: home } }, { path: category, name: Category, component: Category, meta: { tab: category } }, { path: cart, name: Cart, component: Cart, meta: { tab: cart, requiresAuth: true } }, { path: profile, name: Profile, component: Profile, meta: { tab: profile, requiresAuth: true } } ] }, // 404 路由捕获所有未定义路径 { path: /:pathMatch(.*)*, redirect: / } ] }) // 全局前置守卫检查登录态 router.beforeEach((to, from, next) { const authStore useAuthStore() const requiresAuth to.meta.requiresAuth if (requiresAuth !authStore.isLogin) { // 保存目标路径登录成功后跳回 next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } }) export default router参数说明meta.requiresAuth是自定义元字段用于声明该路由是否需要登录态redirect: to.fullPath将原始路径如/cart?itemapple完整保留避免登录后丢失查询参数children路由不设path: /而是用空字符串匹配根路径使/→Home成为默认页符合用户预期DefaultLayout.vue内通过router-view /渲染子路由同时内置 TabBar 组件其激活态由route.meta.tab控制避免在每个页面重复写 TabBar 逻辑。3. 登录与认证状态管理Token 存储、自动刷新、过期拦截3.1 认证仓库设计useAuthStore 的最小必要接口src/stores/auth.ts实现登录态的核心持有者。它不处理 HTTP 请求细节只负责状态存取与事件通知import { defineStore } from pinia import { ref, computed } from vue import { setToken, getToken, removeToken } from /utils/auth export const useAuthStore defineStore(auth, () { // 响应式状态 const token refstring | null(getToken() || null) const userInfo ref{ nickname: string; avatar: string } | null(null) // 计算属性是否已登录 const isLogin computed(() !!token.value) // 动作登录成功后设置 token 和用户信息 const login (newToken: string, userData: { nickname: string; avatar: string }) { token.value newToken userInfo.value userData setToken(newToken) // 写入 localStorage } // 动作登出清除所有状态 const logout () { token.value null userInfo.value null removeToken() } // 动作手动更新用户信息如修改昵称后 const updateUserInfo (data: Partialtypeof userInfo.value) { if (userInfo.value) { userInfo.value { ...userInfo.value, ...data } } } return { token, userInfo, isLogin, login, logout, updateUserInfo } })关键设计点getToken()/setToken()封装了 localStorage 操作并增加try...catch防止隐私模式下写入失败导致白屏userInfo不从 token 解析而是由登录接口返回的明文数据赋值避免 JWT 过期时间与用户信息不同步的陷阱updateUserInfo接收Partial类型允许只更新昵称或只更新头像不强制传全量对象。3.2 登录页面实现表单校验 请求封装 错误反馈src/pages/Login.vue是首个用户触点需兼顾体验与健壮性template div classlogin-page h1小兔鲜 · 登录/h1 form submit.preventhandleSubmit classlogin-form div classform-item label手机号/label input v-modelphone typetel placeholder请输入手机号 :class{ error: errors.phone } / span v-iferrors.phone classerror-msg{{ errors.phone }}/span /div div classform-item label验证码/label div classcode-input input v-modelcode typetext maxlength6 placeholder6位验证码 :class{ error: errors.code } / button typebutton clicksendCode :disabledisSending classsend-btn {{ sendBtnText }} /button /div span v-iferrors.code classerror-msg{{ errors.code }}/span /div button typesubmit classlogin-btn :disabledisSubmitting {{ isSubmitting ? 登录中... : 登录 }} /button /form /div /template script setup langts import { ref, reactive } from vue import { useRouter } from vue-router import { useAuthStore } from /stores/auth import { sendSmsCode, loginByCode } from /composables/useRequest const router useRouter() const authStore useAuthStore() // 表单数据 const phone ref() const code ref() const isSubmitting ref(false) const isSending ref(false) const sendBtnText ref(获取验证码) // 错误信息实时校验 const errors reactive({ phone: , code: }) // 发送验证码 const sendCode async () { if (!/^1[3-9]\d{9}$/.test(phone.value)) { errors.phone 请输入正确的手机号 return } isSending.value true sendBtnText.value 60s后重发 try { await sendSmsCode(phone.value) // 启动倒计时 let seconds 60 const timer setInterval(() { seconds-- sendBtnText.value ${seconds}s后重发 if (seconds 0) { clearInterval(timer) sendBtnText.value 获取验证码 isSending.value false } }, 1000) } catch (err) { errors.phone 验证码发送失败请重试 isSending.value false } } // 提交登录 const handleSubmit async () { // 前置校验 errors.phone /^1[3-9]\d{9}$/.test(phone.value) ? : 手机号格式错误 errors.code code.value.length 6 ? : 请输入6位验证码 if (errors.phone || errors.code) return isSubmitting.value true try { const res await loginByCode(phone.value, code.value) // 模拟接口返回{ token: xxx, nickname: 小兔子, avatar: /avatar.png } authStore.login(res.token, { nickname: res.nickname, avatar: res.avatar }) // 跳转优先返回 redirect 参数指定的页面否则默认首页 const redirect new URLSearchParams(location.search).get(redirect) || / router.push(redirect) } catch (err) { errors.code err.response?.data?.message || 登录失败请检查验证码 } finally { isSubmitting.value false } } /script逻辑说明表单校验分两层提交时集中校验handleSubmit 输入时即时反馈sendCode中的正则验证码倒计时用setInterval而非setTimeout递归避免多次点击触发多个定时器loginByCode是封装好的请求函数见 3.3统一处理 loading、错误 toast、HTTP 状态码登录成功后router.push(redirect)精准跳转确保用户回到原意图页面如从商品详情页点击“去购物车”跳转登录登录后自动回到购物车。3.3 请求封装useRequest 组合式函数统一处理网络层src/composables/useRequest.ts抽离通用请求逻辑避免每个页面重复写axios.createimport axios from axios import { ElMessage } from element-plus // 或使用更轻量的 toast 库 import { useAuthStore } from /stores/auth // 创建实例设置 baseURL 和默认 headers const request axios.create({ baseURL: /api, // 开发时通过 vite.config.ts 代理到后端 timeout: 10000, headers: { Content-Type: application/json } }) // 请求拦截器自动携带 token request.interceptors.request.use( (config) { const authStore useAuthStore() if (authStore.token) { config.headers.Authorization Bearer ${authStore.token} } return config }, (error) Promise.reject(error) ) // 响应拦截器统一错误处理 request.interceptors.response.use( (response) { // 假设后端返回 { code: 0, data: {}, message: } if (response.data.code 0) { return response.data.data } else { throw new Error(response.data.message || 请求失败) } }, (error) { const { response } error if (response?.status 401) { // token 过期清空状态并跳转登录 const authStore useAuthStore() authStore.logout() // 使用 replace 避免用户点返回键回到过期页面 window.location.replace(/login?redirect encodeURIComponent(window.location.pathname)) } else if (response?.status 500) { ElMessage.error(服务器开小差了请稍后再试) } else if (response?.status 400) { ElMessage.error(response.data?.message || 参数错误) } return Promise.reject(error) } ) // 导出具体业务方法此处仅示例两个 export const sendSmsCode (phone: string) request.post(/sms/send, { phone }) export const loginByCode (phone: string, code: string) request.post(/login/code, { phone, code }) // 其他方法可按需扩展...参数说明baseURL: /api配合 Vite 代理见 4.1开发时请求/api/login/code自动转发至http://localhost:3000/login/coderequest.interceptors.response.use中401处理是关键直接window.location.replace强制跳转而非router.push因为此时路由可能已处于受保护状态router.push会再次触发守卫导致死循环所有业务请求函数如loginByCode只返回data字段上层无需关心response.data.data降低使用成本。4. 购物车状态同步与跨页面更新为什么 Badge 总是滞后4.1 购物车仓库useCartStore 的响应式设计src/stores/cart.ts管理购物车核心状态。难点在于如何让首页的商品“加入购物车”按钮点击后底部 TabBar 的购物车徽标badge和购物车页面内的商品列表同时更新且不引发重复渲染import { defineStore } from pinia import { ref, computed } from vue import { getCartList, addToCart, removeFromCart, updateCartCount } from /composables/useRequest export const useCartStore defineStore(cart, () { // 购物车商品列表注意用 ref 包裹数组而非 reactive避免深层响应式开销 const list refArray{ id: string name: string price: number count: number skuId: string }([]) // 总数量计算属性自动响应 list 变化 const totalCount computed(() list.value.reduce((sum, item) sum item.count, 0) ) // 总价计算属性 const totalPrice computed(() list.value.reduce((sum, item) sum item.price * item.count, 0) ) // 加入购物车支持数量叠加 const addItem async (item: { id: string; name: string; price: number; skuId: string }) { const existing list.value.find(i i.skuId item.skuId) if (existing) { // 已存在数量1 existing.count 1 await updateCartCount(existing.skuId, existing.count) } else { // 新增初始数量为1 const newItem { ...item, count: 1, skuId: item.skuId } list.value.push(newItem) await addToCart(newItem) } } // 减少数量最小为1 const reduceItem async (skuId: string) { const item list.value.find(i i.skuId skuId) if (item item.count 1) { item.count - 1 await updateCartCount(skuId, item.count) } } // 删除单项 const removeItem async (skuId: string) { list.value list.value.filter(i i.skuId ! skuId) await removeFromCart(skuId) } // 初始化页面加载时拉取 const init async () { try { const res await getCartList() list.value res.list || [] } catch (err) { console.error(购物车初始化失败, err) list.value [] } } return { list, totalCount, totalPrice, addItem, reduceItem, removeItem, init } })关键设计点list用refArray...而非reactive([])因为reactive对数组的.push()响应有缺陷Vue 3.2 已修复但为兼容性和语义清晰仍推荐reftotalCount和totalPrice是computed天然具备缓存和响应式联动比在addItem里手动this.totalCount更可靠addItem内部先操作本地list.value再发请求实现“乐观更新”Optimistic UI用户点击瞬间就看到数量1失败时再回滚本例暂未实现回滚但架构已预留。4.2 购物车徽标Badge的全局同步策略TabBar 的购物车图标旁需显示未读数量如 “3”。常见错误是在DefaultLayout.vue里直接import { useCartStore } from /stores/cart并读取cartStore.totalCount—— 这会导致DefaultLayout在首次加载时无法响应后续addItem触发的更新。正确做法在DefaultLayout.vue中使用computedstoreToRefstemplate div classdefault-layout !-- 主内容 -- main router-view / /main !-- 底部 TabBar -- nav classtabbar router-link to/ classtab-item :class{ active: route.meta.tab home } i classicon-home/i span首页/span /router-link router-link to/category classtab-item :class{ active: route.meta.tab category } i classicon-category/i span分类/span /router-link router-link to/cart classtab-item :class{ active: route.meta.tab cart } i classicon-cart/i span购物车/span !-- 关键用 computed 包裹 totalCount确保响应式 -- span v-ifcartTotalCount 0 classbadge{{ cartTotalCount }}/span /router-link router-link to/profile classtab-item :class{ active: route.meta.tab profile } i classicon-profile/i span我的/span /router-link /nav /div /template script setup langts import { useRoute } from vue-router import { useCartStore } from /stores/cart import { storeToRefs } from pinia const route useRoute() const cartStore useCartStore() // ✅ 正确用 storeToRefs 解构 totalCount获得响应式引用 const { totalCount: cartTotalCount } storeToRefs(cartStore) // ❌ 错误const cartTotalCount cartStore.totalCount // 此处 totalCount 是函数非响应式值 /script原理说明storeToRefs将computed属性如totalCount转换为Ref使其能在模板中被 Vue 的响应式系统追踪若直接cartStore.totalCount得到的是一个函数模板中{{ cartStore.totalCount }}会输出function () { ... }而非数值storeToRefs是 Pinia 官方推荐解构方式比手动computed(() cartStore.totalCount)更简洁。4.3 首页商品卡片的“加入购物车”按钮实现src/pages/Home.vue中每个商品卡片需有“加入购物车”按钮。重点在于点击后不仅更新购物车列表还要让 TabBar 徽标立即变化且不重新请求整个购物车列表。template div classhome-page div classgoods-list div v-foritem in goods :keyitem.id classgoods-card img :srcitem.image :altitem.name / h3{{ item.name }}/h3 p classprice¥{{ item.price }}/p button clickhandleAddToCart(item) classadd-btn :disabledisAdding[item.id] {{ isAdding[item.id] ? 添加中... : 加入购物车 }} /button /div /div /div /template script setup langts import { ref } from vue import { useCartStore } from /stores/cart const cartStore useCartStore() // 模拟商品列表实际从 API 获取 const goods [ { id: 1, name: 有机苹果, price: 12.5, image: /apple.jpg, skuId: sku-apple-001 }, { id: 2, name: 新鲜菠菜, price: 8.9, image: /spinach.jpg, skuId: sku-spinach-001 } ] // 记录每个商品的 loading 状态避免重复点击 const isAdding refRecordstring, boolean({}) const handleAddToCart async (item: typeof goods[0]) { isAdding.value[item.id] true try { // ✅ 直接调用 cartStore.addItem触发全局响应式更新 await cartStore.addItem({ id: item.id, name: item.name, price: item.price, skuId: item.skuId }) // ✅ 此时 cartStore.totalCount 已更新DefaultLayout 中的 badge 会自动刷新 } catch (err) { console.error(添加失败, err) } finally { isAdding.value[item.id] false } } /script关键点cartStore.addItem()是唯一数据源所有页面都通过它修改购物车保证状态单一可信isAdding用Recordstring, boolean精确控制每个按钮的 loading 状态比全局isLoading更细腻无需在Home.vue中watchcartStore.totalCount因为DefaultLayout已通过storeToRefs建立响应式连接更新是自动的。5. 常见问题排查五个页面开发中最容易踩的 4 个坑5.1 现象登录后跳转到/cart页面空白控制台报Uncaught (in promise) Navigation cancelled from /cart to /cart with a new navigation原因路由守卫router.beforeEach中当requiresAuth为true但authStore.isLogin为false时执行next({ path: /login })但此时authStore.isLogin是computed其 getter 内部依赖token.value而token.value在login()方法中才被赋值。若login()调用前isLogin已被读取如在beforeEach中console.log(authStore.isLogin)会触发computed的依赖收集导致后续token.value newToken时isLogin的computed重新求值并触发next()造成导航冲突。解决确保authStore.login()在路由守卫外完成且isLogin的判断逻辑不提前触发。将authStore.isLogin的读取移至next()回调内或使用await next()Vue Router 4.1 支持// ✅ 正确写法Vue Router 4.1 router.beforeEach(async (to, from, next) { const authStore useAuthStore() if (to.meta.requiresAuth !authStore.isLogin) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })5.2 现象购物车页面中删除某商品列表更新了但 TabBar 徽标数字没变原因DefaultLayout.vue中未使用storeToRefs解构totalCount而是直接{{ cartStore.totalCount }}导致totalCount作为函数被渲染而非其返回值。或者cartStore.removeItems()方法中直接修改了list.value数组但未触发totalCount的重新计算因list.value是ref其.filter()操作是响应式的但需确认totalCount的computed依赖是否正确。解决确保DefaultLayout.vue中使用const { totalCount } storeToRefs(cartStore)检查totalCount的computed是否正确依赖list.valuelist.value.reduce(...)中list.value是ref的.valuecomputed能正确追踪在removeItem方法末尾添加console.log(cart list after remove:, cartStore.list)验证list.value确实已更新。5.3 现象输入手机号后点击“获取验证码”按钮变灰但无倒计时或倒计时结束后按钮未恢复原因sendCode方法中setInterval创建的定时器未被正确清理。当用户快速连续点击“获取验证码”会创建多个定时器彼此干扰或组件卸载如导航离开登录页后定时器仍在运行尝试更新已销毁组件的sendBtnText导致 Vue 警告。解决使用ref存储定时器 ID并在sendCode开始时清除旧定时器在onUnmounted钩子中清除定时器Vue 3 Composition APIimport { onUnmounted } from vue // 在 setup 中 const timerId refNodeJS.Timeout | null(null) const sendCode async () { // 清除旧定时器 if (timerId.value) { clearInterval(timerId.value) } // ... 其他逻辑 timerId.value setInterval(() { // ... }, 1000) } onUnmounted(() { if (timerId.value) { clearInterval(timerId.value) } })5.4 现象从首页点击商品进入详情页假设存在再点击“加入购物车”返回首页后发现商品卡片的“加入购物车”按钮文字仍是“加入购物车”而非“已加入”原因这是 UI 状态与数据状态未同步的典型问题。“已加入”状态属于局部 UI不应由全局购物车仓库管理而应在首页组件内维护一个addedSet: Setstring记录已添加的 skuId并用watch监听cartStore.list变化来更新它。但新手常忽略watch的deep选项或immediate时机。解决在Home.vue的setup中添加import { watch } from vue const addedSet refSetstring(new Set()) // 监听购物车列表变化更新 addedSet watch( () cartStore.list, (newList) { addedSet.value new Set(newList.map(item item.skuId)) }, { immediate: true, deep: true } // immediate 确保初始化时也执行 ) // 模板中按钮文字 // button clickhandleAddToCart(item) // {{ addedSet.value.has(item.skuId) ? 已加入 : 加入购物车 }} // /button6. 进阶技巧用 localStorage 持久化购物车让“关掉浏览器再打开”购物车还在6.1 为什么需要持久化以及为什么不能只靠 tokenToken 通常只存用户身份不包含购物车明细。用户关闭浏览器后内存中的cartStore.list会丢失但用户期望“上次加的苹果还在购物车里”。这要求购物车数据在页面卸载前写入localStorage并在应用启动时从localStorage恢复。注意这不是替代后端购物车而是提升离线体验和首屏速度的“前端兜底”策略。真实项目中前端持久化数据需在登录后与服务端合并本例简化为登录即覆盖。6.2 实现在 useCartStore 中集成 localStorage 同步修改src/stores/cart.ts在addItem、removeItem、init等关键动作后同步写入localStorageimport { defineStore } from pinia import { ref, computed, onMounted } from vue import { getCartList, addToCart, removeFromCart, updateCartCount } from /composables/useRequest // 本地存储 key const CART_STORAGE_KEY xiao-tu-xian-cart // 从 localStorage 读取带错误处理 const loadCartFromStorage (): Arrayany { try { const json localStorage.getItem(CART_STORAGE_KEY) return json ? JSON.parse(json) : [] } catch (e) { console.warn(读取购物车本地存储失败, e) return [] } } // 写入 localStorage const saveCartToStorage (list: Arrayany) { try { localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(list)) } catch (e) { console.warn(写入购物车本地存储失败, e) } } export const useCartStore defineStore(cart, () { const list ref(loadCartFromStorage()) // 初始化时从 localStorage 读取 // ... 其他 computed 和方法保持不变 ... // 加入购物车更新本地 远程 const addItem async (item: { id: string; name: string; price: number; skuId: string }) { const existing list.value.find(i i.skuId item.skuId) if (existing) { existing.count 1 await updateCartCount(existing.skuId, existing.count) } else { p a hrefhttps://download.csdn.net/download/S_cai_ji/86783953 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p