资讯动态

原生前端项目迭代:任务大模式与模式切换实践

发布时间:2026/9/6 7:39:41 来源:尧图企业网站定制
“代号一”是一个完全使用 HTML、CSS 和原生 JavaScript 制作的自制网页项目。在没有框架、没有构建工具、没有后端服务的条件下项目一点点迭代到了 1.3 版本。这次的更新不只修了几个小问题而是新增了一个完整的大模式任务大模式。它把原本分散在页面里的工具入口、任务流、统计信息和历史记录整合成一个连续的工作界面。这篇文章会完整拆解 1.3 版本的迭代过程从版本规划、界面结构、模式切换机制到状态管理、运行验证和常见问题排查全部给出可复现的实践步骤和代码结构。无论你是在做个人项目还是维护一个长期迭代的小型前端应用这套“自制网页版本升级”的思路都值得参考。1. “代号一”为什么要做到 1.3 版本大模式解决了什么问题1.1 自制网页项目常见的问题功能越来越多页面越来越乱这类自制网页项目通常从一个简单的信息展示页开始。第一版可能只有几个模块项目标题、背景说明、按钮交互、页面样式。使用者在页面上点击某个按钮页面显示对应的结果这就是最原始的“功能”。随着迭代继续功能点会逐渐增加。比如今天加一个计数器明天加一个随机数据生成器后天增加一个表单提交预览。每个功能都放在页面上时页面的 DOM 节点变多事件绑定变得分散样式之间的冲突也开始出现。你可能会遇到几个非常典型的现象功能按钮太多视觉上没有主次用户不知道先点什么。新增模块后旧模块的 JavaScript 逻辑被新代码覆盖。页面初始化时所有模块的脚本都会加载页面首屏变慢。修改某个功能时经常意外影响到其他模块的样式或行为。“代号一”在 1.2 版本之前也有这些问题。项目里已经积累了信息展示、基础交互表单、卡片列表和快捷操作等模块但页面结构仍然是“从上到下依次排列”。在手机端尤其明显用户需要滚动很长时间才能看到后面的功能。1.3 版本的核心目标不是继续往页面底部追加模块而是新增一个“大模式”把已有的功能重新组织成一个完整的任务工作界面。用户点击进入后页面从普通的浏览态切换到一个集中操作态把最常用的功能放在同一个屏幕内减少滚动缩短操作路径。1.2 什么是“大模式”它与普通页面模式有什么不同在“代号一”项目里“大模式”是一个完整的界面运行状态。它不是简单弹窗也不是新开一个独立 HTML 文件而是同一份页面代码通过模式切换机制在普通浏览模式和任务大模式之间切换。普通模式保持 1.2 版本已有的展示结构顶部导航、内容分区、底部信息。任务大模式下页面进入一个紧凑的操作布局包含以下部分快速启动区域常用功能以卡片或按钮形式集中显示一键触发。任务流程区多个步骤按顺序排列用户逐项完成。数据统计区展示本次页面使用过程中的计数、状态和结果。历史记录区记录已完成操作并支持清空记录。两种模式的关系可以用下面这张表理解对比维度普通浏览模式任务大模式页面结构从上到下长页面紧凑型工作台布局功能密度低按分区展示高功能集中呈现交互目标阅读和浏览连续完成任务状态保持页面刷新后重置通过 localStorage 保持适用场景信息展示、入门体验需要连续操作的日常使用代码实现复杂度中等较高需要模式管理大模式的“大”并不是文件体积大或页面更长而是功能组织方式的升级用一个统一入口承接所有高频操作让用户在一个界面里完成更多事。这个设计思想在很多中后台系统里非常常见但放在自制网页项目里也完全适用。2. 1.3 版本项目结构与技术选型先把设计和依赖对齐2.1 技术选型为什么坚持使用原生三件套在 1.3 版本中“代号一”仍然选择 HTML、CSS、JavaScript 原生实现不引入 Vue、React、Webpack 等框架和构建工具。原因有三点第一项目体积小功能边界清晰。这个项目没有复杂的状态管理和组件通信需求原生 JavaScript 完全能够胜任。引入框架需要额外处理构建、依赖和运行时开销反而增加维护成本。第二自制网页项目的核心价值在于“直接打开就能看”。用户不需要安装 Node.js不需要执行 npm install只要双击 index.html 或在浏览器地址栏打开本地路径页面就能完整运行。第三原生实现更容易理解技术原理。模式切换、状态存储、事件委托、DOM 渲染这些基础能力恰恰是框架封装的核心逻辑。自己实现一次对后续学习框架会有很大帮助。1.3 版本只增加了一个轻量级模块化拆分方式项目内的 JavaScript 文件按照职责划分成 storage、mode、app 三个功能文件。这是一个工程组织层面的升级不依赖任何外部库。2.2 项目目录结构为扩展新模块预留位置在 1.3 版本中建议采用以下目录结构code-one/ ├── index.html ├── css/ │ ├── style.css │ └── mode-large.css └── js/ ├── storage.js ├── mode.js └── app.js如果你在一个服务器环境里部署可以增加 assets、images 等目录。但当前项目的资源不多静态资源直接放在根目录或 css、js 子目录下即可。为了后续扩展方便还可以在 js 目录下增加 modules/ 子目录用于存放各个独立功能模块。1.3 版本先用 mode.js 统一管理模式后续如果功能继续增多再拆成多个功能插件。2.3 版本演进要提前考虑不能只在页面里加内容迭代自制网页项目时最容易犯的错误是“只做加法”。每增加一个新功能就往页面里加一个 section往 CSS 里加一组样式往 JS 文件里追加一段逻辑。短期跑得通但到一定阶段后代码会变得难以维护。1.3 版本的迭代策略是先设计模式管理机制再接入大模式内容。具体版本规划可以按下表执行版本节点版本目标主要改动落地检查v1.0基础信息展示HTML 结构、基础 CSS页面能正常显示v1.1增加简单交互按钮、点击事件点击后页面反馈正常v1.2模块化拆分按功能拆分 CSS、JS 文件模块间互不影响v1.3新增任务大模式模式管理、状态持久化、新布局普通模式与大模式可互相切换从 v1.2 到 v1.3 不是量的累积而是组织方式的升级。先明确模式这一层抽象把页面分成“普通模式”“任务大模式”两种状态后面的功能增加才会更有序。3. 任务大模式的整体设计入口、注册表、布局与状态3.1 大模式的功能入口怎么设计才不会干扰默认页面入口的位置和形态很重要。对于个人自制网页项目入口既不能太显眼也不能太难找。太显眼会干扰默认页面的浏览体验太难找用户不知道这里有新功能。“代号一”1.3 版本采用固定浮动按钮的方案。按钮固定在页面右下角按钮文字为“进入大模式”。当页面处于大模式时按钮文字变为“退出大模式”。该按钮使用 fixed 定位因此在滚动页面时始终可见。实现思路如下button idmodeToggle classmode-toggle进入大模式/button按钮的样式放在 mode-large.css 中避免和主样式表冲突。这样大模式相关样式只在按钮或模式状态出现时加载。3.2 模式注册表用数据结构驱动页面渲染为了让模式切换更灵活项目把两种模式的元信息保存在一个模式注册表中。这是一个数组每个模式包含唯一标识、显示名称、图标、容器选择器等字段const modeList [ { id: normal, name: 普通模式, container: #appNormal, icon: book }, { id: large, name: 任务大模式, container: #appLarge, icon: grid } ];渲染大模式时只需要遍历 modeList 找到 id 为 large 的配置项然后调用对应的渲染函数即可。这种配置驱动渲染的方式有很好的扩展性以后新增第三种模式不需要修改核心模式切换逻辑只需要增加一条配置并实现对应的渲染函数。3.3 大模式界面核心布局一张完整工作台的划分进入任务大模式后页面结构会变成四块独立区域。典型结构如下div idappLarge classlarge-mode header classlarge-header h2任务大模式/h2 span idlargeStats/span /header section classlarge-grid div classlarge-card idquickActions h3快速启动/h3 !-- 功能按钮由 JavaScript 渲染 -- /div div classlarge-card idtaskFlow h3任务流程/h3 !-- 步骤列表 -- /div div classlarge-card idhistoryPanel h3历史记录/h3 !-- 操作记录 -- /div /section /div布局使用 CSS Grid 实现。桌面端三列卡片并排移动端切换为单列排列。这个布局可以在 mode-large.css 中使用以下方式实现.large-mode .large-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } media (max-width: 768px) { .large-mode .large-grid { grid-template-columns: 1fr; } }这种弹性布局让大模式在不同屏幕尺寸下都有较好的可用性。3.4 大模式状态记录localStorage 保存哪些数据任务大模式需要记录两类状态当前页面处于哪种模式。用户在大模式中执行过哪些操作例如完成的任务数量、最近的几条记录。状态保存在 localStorage 中key 设计如下key存储内容示例值code-one-mode当前模式 idlargecode-one-stats使用统计数据{taskCount:12,lastTime:2024-06-01 10:00}code-one-history最近操作列表[完成步骤A,完成步骤B]不建议把所有状态写成一个 JSON 大对象。拆分成多个 key 的好处是读取时互不影响后期升级时只需要迁移对应字段。4. 核心代码实现从模式管理到任务流程4.1 安全的 localStorage 封装避免 JSON 解析报错直接使用 localStorage 时最常见的错误是 JSON.parse 解析失败。例如之前往 localStorage 写入过一个普通字符串后面却用 JSON.parse 去读取页面会直接抛错。“代号一”项目封装了一个简易存储工具 storage.jsconst storage { get(key, fallback) { try { const raw localStorage.getItem(key); if (raw null) { return fallback; } return JSON.parse(raw); } catch (error) { console.warn(读取存储失败:, key, error); return fallback; } }, set(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch (error) { console.warn(写入存储失败:, key, error); return false; } }, remove(key) { localStorage.removeItem(key); } };这个封装统一了读写格式并给所有读写加了 try catch。即使浏览器禁止使用 localStorage页面也不会直接崩溃只会输出警告并返回默认值。4.2 模式管理模块 ModeManager负责切换和初始化mode.js 是本次版本更新的核心模块。它负责注册模式、初始化状态、执行切换动作。ModeManager 代码如下const ModeManager { currentMode: normal, init(modeKey) { const savedMode storage.get(code-one-mode, normal); const validMode modeList.some(item item.id savedMode) ? savedMode : modeKey; this.switchMode(validMode); }, switchMode(modeId) { const config modeList.find(item item.id modeId); if (!config) { console.error(未找到模式配置:, modeId); return; } this.currentMode modeId; document.querySelectorAll(.app-container).forEach(container { container.hidden true; }); const target document.querySelector(config.container); if (target) { target.hidden false; } storage.set(code-one-mode, modeId); this.updateToggleButton(config.name); document.body.dataset.mode modeId; }, toggle() { const nextMode this.currentMode normal ? large : normal; this.switchMode(nextMode); }, updateToggleButton(modeName) { const toggle document.getElementById(modeToggle); if (toggle) { toggle.textContent this.currentMode large ? 退出大模式 : 进入大模式; } } };关键点有三个初始化时读取 localStorage如果读到模式 id 不存在则回退到普通模式。切换时先隐藏所有 .app-container再显示目标容器。这比手动控制单个容器更可靠。在 document.body 上记录>function renderLargeMode() { renderQuickActions(); renderTaskFlow(); renderHistory(); updateStats(); }renderQuickActions 负责渲染快速启动按钮。按钮组的数据可以提前定义const quickActions [ { id: action-copy, label: 复制当前页面标题, icon: copy }, { id: action-reset, label: 重置任务数据, icon: reset }, { id: action-console, label: 输出页面信息到控制台, icon: terminal } ]; function renderQuickActions() { const wrap document.getElementById(quickActions); if (!wrap) return; wrap.innerHTML quickActions.map(action { return button classquick-btn>const taskSteps [ { id: step-check, name: 确认环境可达 }, { id: step-run, name: 执行主任务 }, { id: step-report, name: 查看结果报告 } ]; function renderTaskFlow() { const wrap document.getElementById(taskFlow); if (!wrap) return; wrap.innerHTML taskSteps.map((step, index) { const stateStorage storage.get(code-one-step- step.id, pending); const stateText stateStorage done ? 已完成 : 未开始; return div classtask-step>function renderHistory() { const wrap document.getElementById(historyPanel); if (!wrap) return; const history storage.get(code-one-history, []); if (history.length 0) { wrap.innerHTML p classempty-tip暂无操作记录/p; return; } wrap.innerHTML history.map(item p classhistory-item${item}/p).join(); }4.4 事件委托统一绑定大模式内部按钮事件大模式内部按钮较多最稳妥的事件绑定方式是事件委托。只在外层容器监听一次 click通过 event.target.dataset 判断动作document.addEventListener(click, function(event) { const action event.target.dataset.action; if (!action) return; if (action action-copy) { copyPageTitle(); } else if (action action-reset) { resetAllData(); } else if (action action-console) { logPageInfo(); } const step event.target.dataset.step; if (step) { markStepDone(step); } });使用事件委托有两个明显好处后续新增按钮不需要重新绑定一个个监听器。由 JavaScript 动态生成的内容也能被监听不需要在生成内容的同时额外绑定事件。不建议在每个按钮生成时单独 addEventListener。那样写容易造成内存占用增加而且在 DOM 被替换时会产生“幽灵事件”事件绑定在旧节点上永远执行。4.5 辅助动作函数复制、重置、输出信息复制页面标题、重置数据、输出页面信息这三个动作是快速启动区的常用功能function copyPageTitle() { const title document.title || 代号一; navigator.clipboard.writeText(title).then(() { addHistory(已复制页面标题: title); alert(复制成功); }).catch(() { alert(复制失败请手动选择文本); }); } function resetAllData() { storage.remove(code-one-history); storage.remove(code-one-stats); [step-check, step-run, step-report].forEach(id { storage.remove(code-one-step- id); }); updateStats(); renderTaskFlow(); renderHistory(); alert(所有任务数据已重置); } function logPageInfo() { const info { title: document.title, url: location.href, mode: ModeManager.currentMode, userAgent: navigator.userAgent }; console.table(info); } function markStepDone(stepId) { storage.set(code-one-step- stepId, done); renderTaskFlow(); addHistory(完成任务步骤: stepId); } function addHistory(record) { const history storage.get(code-one-history, []); history.unshift(record); if (history.length 10) { history.pop(); } storage.set(code-one-history, history); renderHistory(); updateStats(); } function updateStats() { const history storage.get(code-one-history, []); const statsEl document.getElementById(largeStats); if (statsEl) { statsEl.textContent 当前任务统计: history.length 条记录; } }注意navigator.clipboard 在非 HTTPS 环境或有隔离限制时可能不可用。代码中保留 catch 分支失败提醒用户手动复制这是更稳妥的做法。5. 模式切换入口与初始化流程让页面一启动就处于正确状态5.1 页面初始化逻辑先读存储再渲染页面加载时需要按以下顺序执行初始化等待 DOM 就绪。初始化存储工具。读取当前模式并调用 ModeManager.init。如果当前是普通模式渲染普通页面分区。如果当前是任务大模式调用 renderLargeMode 渲染大模式内容。绑定模式切换按钮。对应的入口代码放在 app.js 中document.addEventListener(DOMContentLoaded, function() { ModeManager.init(normal); const currentMode storage.get(code-one-mode, normal); if (currentMode large) { renderLargeMode(); } const toggleBtn document.getElementById(modeToggle); if (toggleBtn) { toggleBtn.addEventListener(click, function() { ModeManager.toggle(); if (ModeManager.currentMode large) { renderLargeMode(); } }); } });这段逻辑做到了两件事页面刷新后能恢复上次使用的模式。每次进入大模式时都会重新渲染内部内容保证和历史记录、统计信息保持同步。5.2 CSS 模式作用域确保两种模式互不干扰普通模式和大模式使用不同的 CSS 文件。index.html 中引入两个样式文件link relstylesheet hrefcss/style.css link relstylesheet hrefcss/mode-large.cssstyle.css 负责全局基础样式。mode-large.css 中所有选择器加上 .large-mode 作用域前缀同时利用 body[data-modelarge] 区分当前是否处于大模式。示例body[data-modelarge] .app-normal { display: none; }这样做的好处是即使两个模式都在 DOM 中存在页面也只会展示当前模式对应区域。如果后续把代码改为 SPA 路由式渲染这套 class 和 data 属性也能平滑过渡。6. 运行验证从本地到生产环境都要检查6.1 本地环境验证步骤完成代码后先不要直接部署。以下本地验证步骤能发现大部分问题。第一步用浏览器直接打开 index.html确认页面默认显示普通模式且没有控制台报错。第二步点击“进入大模式”按钮确认页面切换到任务大模式布局。快速启动区出现三个按钮任务流程区显示三个步骤历史记录区可能是空状态。第三步点击快速启动区内任一个按钮例如“复制当前页面标题”。如果浏览器提示复制成功查看历史记录区是否新增一条记录。第四步点击任务流程中的一个步骤确认步骤状态从“未开始”变为“已完成”。第五步刷新页面确认页面自动进入任务大模式且之前的操作记录仍然存在。这一步验证 localStorage 持久化是否生效。第六步点击“退出大模式”确认回到普通模式。再点击“进入大模式”确认之前的任务状态还在。以上 6 步覆盖了模式切换、功能点击、历史记录、数据持久化四个核心能力。6.2 控制台数据检查与预期输出如果运行过程有问题可以直接在浏览器控制台检查 storage 中的数据localStorage.getItem(code-one-mode); localStorage.getItem(code-one-history); localStorage.getItem(code-one-step-step-check);预期输出分别对应large[已复制页面标题: 代号一]done如果数据不符合预期说明对应模块的写入逻辑存在问题。可以结合之前代码中的 console.warn 输出缩小范围。6.3 生产环境部署时要注意的三个差异自制网页从本地打开到部署到服务器或静态托管平台有几点差异需要处理。第一文件路径从相对路径和绝对路径的差异。如果部署在子目录例如https://example.com/code-one/CSS 和 JS 的引用必须使用相对路径或基于 base 标签的绝对路径。不要在 JS 里硬编码首页 URL否则换到其他环境会失效。第二浏览器缓存问题。CSS 和 JS 更新后用户可能仍加载旧文件。建议在 index.html 中给资源链接增加版本参数link relstylesheet hrefcss/mode-large.css?v1.3.0 script srcjs/mode.js?v1.3.0/script版本参数改变后浏览器会视为新资源请求避免加载旧缓存。第三HTTPS 环境与 Clipboard API。如果站点使用 HTTPSnavigator.clipboard 通常可用如果部署在纯 HTTP 环境或本地 file 协议下可能无法使用。复制功能在代码里已经做了失败兜底部署后实际测试一次即可。7. 常见问题与排查链路让版本迭代不再“改了没效果”7.1 大模式按钮点击无效页面没有切换现象点击“进入大模式”后页面没有切换到大模式布局按钮文字也没有变化。排查顺序打开浏览器控制台查看是否有 JavaScript 报错。检查 index.html 中 script 脚本引入顺序。mode.js 必须早于 app.js否则 ModeManager 未定义。检查按钮 id 是否和 JavaScript 中的getElementById(modeToggle)一致。检查 CSS 样式是否被浏览器缓存尝试强制刷新。常见原因是脚本引入顺序错误或者按钮 id 修改后 JS 中未同步。7.2 任务模式能打开但大模式内部模块不显示现象页面切换成功进入大模式但快速启动区、任务流程区、历史记录区都是空白。可能原因renderLargeMode 没有被调用。大模式内部容器 id 和渲染函数中使用的 id 不一致。某个子函数抛出异常中断了后续渲染。建议在纯函数入口增加调试日志例如在 renderLargeMode 第一行输出确认信息console.log(开始渲染大模式);如果日志没有出现说明渲染函数没有被调用。如果日志出现但其他内容没有继续在 renderQuickActions、renderTaskFlow、renderHistory 前分别加日志判断哪一步中断。7.3 模式已切换但刷新后又回到旧模式现象点击进入大模式后一切正常刷新页面页面又变回普通模式。排查时先看 localStorage 中是否真正写入了数据localStorage.getItem(code-one-mode);如果返回 null说明写入失败或写入时传入值不对。常见问题浏览器处于隐私模式localStorage 写入被禁止。写入了large但读取时用了另一个 key。storage.set 内部 catch 到了异常但没有明显提示。预防办法在 ModeManager.switchMode 中切换成功后显式调用 storage.set(code-one-mode, modeId)。同时在页面初始化读取模式时做一次合法性检查非法值回退到 normal。7.4 样式错乱或普通模式内容在大模式下仍然显示现象进入大模式后普通模式的内容仍然出现在页面上样式混在一起。检查点index.html 中是否用.app-container统一包裹了两种模式区域。ModeManager.switchMode 中是否执行了document.querySelectorAll(.app-container)隐藏操作。CSS 中是否正确设置了body[data-modelarge] .app-normal { display: none; }。如果普通模式内容和大模式内容是兄弟节点关系隐藏逻辑必须覆盖容器本身而不是只覆盖容器内的某些元素。7.5 表格常见现象、原因和处理建议问题现象常见原因检查方式处理建议进入大模式后页面空白JS 报错导致渲染中断打开控制台查看异常堆栈修复最先抛错的前置函数点击复制无反应非 HTTPS 环境Clipboard API 不可用在控制台手动执行复制命令增加手动复制提示刷新后模式不保留localStorage 写入失败或 key 不一致手动读取 localStorage 对应 key统一使用 storage 工具读写大模式样式未生效新 CSS 文件未引入或引入顺序错误检查 network 面板是否加载 mode-large.css在 index.html 中引入并调整顺序历史记录无限增长addHistory 未限制长度查看 localStorage 中的数据量限制 history 数组最大长度例如 10 条8. 版本发布前检查清单与最佳实践8.1 发布前检查清单发布 1.3 版本前可以对照以下清单逐项确认普通模式下所有旧功能是否正常没有被大模式覆盖或影响。大模式下四个核心区域是否都能正确渲染。切换模式时目标区域是否立即显示旧区域是否立即隐藏。刷新页面后当前模式和历史记录是否恢复正确。连续点击切换按钮多次没有重复事件或页面状态错乱。移入 localStorage 不可用的浏览器环境时页面不显示白屏只输出警告并回退普通模式。使用手机模拟器或小屏窗口确认大模式布局能够切换到单列布局。检查 JS 文件在浏览器控制台中无未捕获异常。检查 CSS 和 JS 文件是否都带上了版本号参数避免缓存问题。生产环境部署后实际访问一次子路径确认图片、CSS、JS 路径无误。8.2 面向 1.3 版本的最佳实践从这次迭代中可以总结出几条可复用的实践建议第一模式状态尽量通过统一入口变更。不要到处直接写 localStorage.setItem统一通过 storage 和 ModeManager 管理能减少 key 拼写不一致的问题。第二按钮事件使用事件委托。即使按钮是动态生成的也只需要绑定一次。第三所有从 localStorage 读取的 JSON 都要设置兜底值和 try catch。不要信任贬值的旧数据结构。第四CSS 文件名和选择器作用域要清晰。大模式专属样式全部带 .large-mode 前缀普通样式不与之混杂。第五版本号记录到页面底部或控制台。可以在页面底部显示版本1.3.0方便他人查看当前部署版本也便于排查缓存问题。8.3 后续版本可以扩展的方向1.3 版本完成后“代号一”项目仍然可以继续扩展。几个主流方向如下把模式切换升级为带路由的页面级切换例如 URL 参数?modelarge直接进入大模式。增加导出功能把历史记录和统计数据导出为 JSON 文件下载。引入更完整的构建工具链例如 Vite把多个 JS 模块打包成一个文件。若引入构建工具就要在处理路径和缓存策略时重新设计。增加移动端触摸手势例如向左滑动返回普通模式。给大模式增加可拖拽卡片区域让用户自定义工作台布局。这些方向都可以在不推翻现有架构的前提下逐步实现。项目的核心机制已经建立起来后续主要是功能扩展和体验优化。9. 写在最后1.3 版本迭代真正练到的能力这次“代号一”1.3 版本的迭代表面上是在做网页功能更新实际上是在练习一个前端项目长期演进时需要持续掌握的能力在已有结构上做抽象和重构。任务大模式看起来只是多了一个页面布局但它的落地需要同时处理模式注册、状态持久化、DOM 渲染、事件委托和 CSS 作用域。这些技术点拆开看都很基础组合在一起后就能让一个自制网页从简单页面升级为具有“应用感”的工具界面。对刚入门前端的人来说完整实现一遍这套代码比单纯看框架文档更有价值。对已经在做个人项目的开发者来说这套模式切换机制可以复用到题库系统、网盘工具、在线简历、个人导航页等多种场景。把 1.3 版本的思路保留下来下次新增版本时你就不需要在混乱的页面里继续堆代码了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价