[{"content":"说明 我这里写个是为了做一下整理 我觉得有趣，如果开发累了，可以换一换脑子 注：题目可直抵题目，所以不写题目内容 题目讲解 哈希：快速映射和快速查找 两数之和 暴力法 暴力法就是遍历所有数，然后两两相加，如果和等于目标值，则返回结果，否则返回-1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public: vector\u0026lt;int\u0026gt; twoSum(std::vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) { int n = nums.size(); for (int i = 0; i \u0026lt; n; i++) { for (int j = i + 1; j \u0026lt; n; j++) { if (nums[i] + nums[j] == target) { return {i, j}; } } } return {}; } }; 哈希表 哈希表就是将数组中的数作为键，索引作为值，然后遍历数组，将每个数作为键，索引作为值，如果哈希表中存在目标值，则返回结果，否则返回-1 时间复杂度：O(n) 为什么可以优化时间呢？ 因为哈希表的查找时间杂度是 O(1)，而数组的查找时间复杂度是 O(n)，所以哈希表可以提高查找效率。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public: vector\u0026lt;int\u0026gt; twoSum(std::vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) { int n = nums.size(); unordered_map\u0026lt;int,int\u0026gt;mp; for(int i=0;i\u0026lt;n;i++){ if(mp.find(target-nums[i])!=mp.end()){ return {i,mp[target-nums[i]]}; } mp[nums[i]]=i; } return {}; } }; 字母异位词分组 思路：哈希表，将字符串排序，作为键，将原字符串作为值，然后遍历字符串，将排序后的字符串作为键，将原字符串作为值，如果哈希表中存在目标值，则返回结果，否则返回-1 时间复杂度：O(n) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public: vector\u0026lt;vector\u0026lt;string\u0026gt;\u0026gt; groupAnagrams(vector\u0026lt;string\u0026gt;\u0026amp; strs) { unordered_map\u0026lt;string,vector\u0026lt;string\u0026gt;\u0026gt;m; for(string\u0026amp; s:strs){ string s1=s; sort(s1.begin(),s1.end()); m[s1].push_back(s); } vector\u0026lt;vector\u0026lt;string\u0026gt;\u0026gt;ans; ans.reserve(m.size()); for(auto \u0026amp; [x,value]:m){ ans.push_back(value); } return ans; } }; ####最长连续序列\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution { public: int longestConsecutive(vector\u0026lt;int\u0026gt;\u0026amp; nums) { unordered_set\u0026lt;int\u0026gt;st(nums.begin(),nums.end()); int ans=0; for(int x:st){ if(!st.count(x-1)){ continue; } int now=x; int cnt=1; while(st.count(now+1)){ now++； cnt++; } ans=max(ans,cnt); } return ans; } } 双指针 移动零 思路：双指针，一个指针指向当前位置，一个指针指向下一个位置，如果当前位置为0，则将下一个位置的值赋给当前位置，然后将下一个位置指针后移一位，直到下一个位置为0，然后将当前位置指针后移一位，直到数组末尾 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public: void moveZeroes(vector\u0026lt;int\u0026gt;\u0026amp; nums) { int l=0,r=1; if(nums.size()==1) return; while(r\u0026lt;nums.size()){ if(nums[l]!=0){ l++; } if(nums[l]==0){ if(nums[r]!=0){ swap(nums[l],nums[r]); l++; } } r++; } } }; 盛最多水的容器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public: int maxArea(vector\u0026lt;int\u0026gt;\u0026amp; height) { int l=0,r=height.size()-1; int ans=0; while(l\u0026lt;r){ if(height[l]\u0026gt;=height[r]){ ans=max(ans,height[r]*(r-l)); r--; } else{ ans=max(ans,height[l]*(r-l)); l++; } } return ans; } }; 三数之和 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution { public: vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; threeSum(vector\u0026lt;int\u0026gt;\u0026amp; nums) { vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;ans; int n=nums.size(); sort(nums.begin(),nums.end()); for(int i=0;i\u0026lt;n;i++){ if(nums[i]\u0026gt;0) break; if(i\u0026gt;0\u0026amp;\u0026amp;nums[i]==nums[i-1]) continue; int l=i+1,r=n-1; while(l\u0026lt;r){ int sum=nums[i]+nums[l]+nums[r]; if(sum==0){ ans.push_back({nums[i],nums[l],nums[r]}); while(l\u0026lt;r\u0026amp;\u0026amp;nums[l]==nums[l+1]) l++; while(l\u0026lt;r\u0026amp;\u0026amp;nums[r]==nums[r-1]) r--; l++,r--; } else if(sum\u0026gt;0) r--; else l++; } } return ans; } }; ####接雨水\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution { public: int trap(vector\u0026lt;int\u0026gt;\u0026amp; height) { int lmax=0,rmax=0; int l=0,r=height.size()-1; int n=height.size(); int ans=0; while(l\u0026lt;r){ lmax=max(lmax,height[l]); rmax=max(rmax,height[r]); if(lmax\u0026gt;rmax) { ans+=rmax-height[r]; r--; } else{ ans+=lmax-height[l]; l++; } } return ans; } }; ###滑动窗口\n无重复字符的最长子串 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public: int lengthOfLongestSubstring(string s) { int l=0; unordered_map\u0026lt;char,int\u0026gt;mp; int ans=0; for(int i=0;i\u0026lt;s.length();i++){ char c=s[i]; mp[c]++; while(mp[c]\u0026gt;1){ mp[s[l]]--; l++; } ans=max(ans,i-l+1); } return ans; } }; ","date":"2026-04-01T01:00:00Z","permalink":"https://ye-guan-xing.github.io/p/%E5%8A%9B%E6%89%A3hot100%E7%B2%BE%E8%AE%B2/","title":"力扣hot100(精讲)"},{"content":"Git 新手第一次使用：克隆、配置远程与首次推送 1. 克隆仓库到本地 1 2 git clone https://github.com/username/repo.git cd repo 2. 查看当前远程仓库地址 1 git remote -v 你会看到类似：\norigin https://github.com/username/repo.git (fetch) origin https://github.com/username/repo.git (push) 3. 设置或修改远程仓库地址 场景 A：已经有 origin，只想修改地址 1 git remote set-url origin https://github.com/yourname/new-repo.git 场景 B：没有 origin，第一次添加远程地址 1 git remote add origin https://github.com/yourname/new-repo.git 修改后再次检查：\n1 git remote -v 4. 首次提交并推送 先把改动提交到本地仓库：\n1 2 git add . git commit -m \u0026#34;init project\u0026#34; 再首次推送到远程分支：\n1 git push -u origin main 如果你的默认分支叫 master，则使用：\n1 git push -u origin master -u 的作用是建立本地分支和远程分支的追踪关系。设置后，后续可以直接使用：\n1 2 git push git pull 5. 常见检查命令 1 2 3 git status git branch git log --oneline ","date":"2026-04-01T00:00:00Z","permalink":"https://ye-guan-xing.github.io/p/git-%E6%96%B0%E6%89%8B%E7%AC%AC%E4%B8%80%E6%AC%A1%E4%BD%BF%E7%94%A8%E5%85%8B%E9%9A%86%E9%85%8D%E7%BD%AE%E8%BF%9C%E7%A8%8B%E4%B8%8E%E9%A6%96%E6%AC%A1%E6%8E%A8%E9%80%81/","title":"Git 新手第一次使用：克隆、配置远程与首次推送"},{"content":"Git 工作流程与基本操作图解 我们可以把 Git 的工作流程想象成一个写作业并交给老师的过程。\n四个核心区域 1. 工作区（Working Directory）—— 你的书桌 这是你实际修改文件的地方。你在这里写代码、删删改改。\n动作：你在这里编写了新功能或修复了 Bug。 2. 暂存区（Staging Area）—— 你的待邮寄篮子 当你觉得作业写得差不多了，你会把它放进一个篮子里，准备打包。\n关键指令：git add 意义：告诉 Git，这些改动我确认要提交了，先帮我记着。 3. 本地仓库（Local Repository）—— 你的个人保险箱 你把篮子里的东西打包好，贴上标签（提交信息），锁进自己的保险箱。\n关键指令：git commit 意义：改动正式成为了项目历史的一部分。即便你之后改乱了，也可以随时从这里找回。 4. 远程仓库（Remote）—— 老师的收件箱（如 GitHub/GitLab） 最后，你把保险箱里的代码通过网络发送给远程服务器，方便其他人查看或合作。\n关键指令：git push 意义：备份代码，并与团队共享进度。 知识点说明 git stash（贮藏区） 作业写了一半，突然要改另一个急活，但又不想把没写完的作业提交。这时可以先用 stash 把代码藏进抽屉，等忙完再拿出来继续写（git stash pop）。\ngit pull（拉取） 看看老师（远程仓库）那里有没有别人交的新作业，直接同步到你的书桌上。\ngit fetch + git merge 先看看远程有什么更新（fetch），确认没问题后再合并（merge）到自己的代码里。\n一句话总结 修改代码 -\u0026gt; add（放进篮子）-\u0026gt; commit（存入箱子）-\u0026gt; push（寄给远方）。\n命令说明 1 克隆仓库 1 2 git clone https://github.com/username/repo.git cd repo 2 创建新分支 1 git checkout -b new-feature 3 工作目录 在工作目录中进行代码编辑、添加新文件或删除不需要的文件。\n4 暂存文件 1 2 git add filename git add . 5 提交更改 1 git commit -m \u0026#34;Add new feature\u0026#34; 6 拉取最新更改 1 2 git pull origin main git pull origin new-feature 7 推送更改 1 git push origin new-feature 8 创建 Pull Request（PR） 在 GitHub 或其他托管平台上创建 Pull Request，邀请团队成员进行代码审查。PR 合并后，你的更改就会合并到主分支。\n9 合并更改 1 2 3 git checkout main git pull origin main git merge new-feature 10 删除分支 1 2 git branch -d new-feature git push origin --delete new-feature ","date":"2026-03-31T10:00:00Z","permalink":"https://ye-guan-xing.github.io/p/git-%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B%E4%B8%8E%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C%E5%9B%BE%E8%A7%A3/","title":"Git 工作流程与基本操作图解"},{"content":"前言 这份前端学习路线源自 roadmap.sh，是一份被全球开发者广泛参考的学习指南。它清晰地梳理了从基础到进阶的所有核心知识点，并标注了每个内容的学习优先级，帮助你在浩瀚的前端知识海洋中找准方向，高效学习。（参考图见尾部）\n图例说明 在开始之前，请先了解图中标记的含义，这将帮助你判断学习的优先级：\n❤️ 个人推荐 / 意见：核心必学内容，是前端开发的基石，必须掌握。 ✅ 替代选项 — 选择此项或是紫色项目）：选其一深入即可，通常是不同的技术方案或框架，无需全部学习。 ☑ 不必严格按照路线图的顺序（随时可学）：可灵活安排学习顺序，属于进阶或补充性知识。 ⚫ 我不推荐：优先级较低或可跳过，在当前技术环境下必要性不高。 一、互联网基础 🌐 在学习具体的前端技术之前，理解互联网的工作原理至关重要，这能帮你建立起坚实的知识根基。\n❤️ 互联网是如何运作的？ ❤️ 什么是 HTTP？ ❤️ 浏览器及其工作原理？ ❤️ DNS 及其工作原理？ ❤️ 什么是域名 (Domain Name)？ ❤️ 什么是主机托管 (Hosting)？ 二、HTML 🏗️ HTML 是网页的骨架，负责定义内容的结构和语义。\n✅ 学习基础知识 ❤️ 撰写语义化 (Semantic) HTML ❤️ 表单 (Form) 和验证 (Validation) ❤️ 惯例和最佳实践 (Best Practice) ☑ 无障碍 (Accessibility) ☑ 基础的搜索引擎优化 (SEO) 三、CSS 🎨 CSS 负责网页的视觉呈现，让你的页面变得美观和响应式。\n✅ 学习基础知识 ✅ 制作页面布局 (Layout) ✅ 响应式设计* 和 媒体查询** ❤️ 浮动 (Float) ❤️ 定位 (Positioning) ❤️ 显示属性 (Display) ❤️ 盒模型 (Box Model) ❤️ CSS 网格 (Grid) ❤️ 弹性盒子 (Flex Box) Responsive Design，也译作“响应式网页设计” *Media Query，媒体查询是实现响应式设计的核心技术 四、JavaScript 💻 JavaScript 是前端的灵魂，赋予网页交互和动态能力。\n❤️ 语法和基本结构 ❤️ 学习 DOM 操作 ❤️ 了解 Fetch API / Ajax (XHR) ❤️ ES6+ 及模块化 JavaScript ❤️ 理解提升 (Hoisting)、事件冒泡 (Event Bubbling)、作用域 (Scope)、原型 (Prototype)、影子 (Shadow) DOM、严格模式 (Strict) 等核心概念 “作用域 (Scope)”也有译作“范畴”的情况，前端领域通用译法为“作用域” 五、工具与工程化 🛠️ 现代前端开发离不开各种工具，它们能极大地提升你的开发效率和代码质量。\n版本控制系统 (VCS) ☑ 版本控制系统的定义及使用价值 ❤️ Git 基本用法 ☑ 代码仓库托管 (Repo Hosting) 服务 ✅ 注册账号并学习使用 GitHub ❤️ GitHub ☑ GitLab ☑ Bitbucket 包管理系统 ❤️ npm ❤️ yarn ☑ 补充说明：npm 和 yarn 功能相近，选择其一或两者都学均可，无本质差异 网络安全知识 ☑ 至少掌握以下内容的基础概念 ☑ HTTPS ☑ 内容安全政策 (CSP) ☑ 跨域资源共享 (CORS) ☑ OWASP 安全风险（Web 应用程序常见安全漏洞） CSS 架构 (Architecture) ☑ 补充说明：现代框架和 CSS-in-JS 普及后，无需深入钻研这类架构，但熟悉 BEM 仍有价值 ✅ BEM（Block-Element-Modifier，一种 CSS 命名规范） ⚫ OOCSS（面向对象的 CSS） ⚫ SMACSS（可扩展的模块化 CSS） CSS 预处理器 (Preprocessor) ☑ 补充说明：随着现代框架发展，CSS-in-JS 逐渐成为主流，这类预处理器非必需，但熟悉基础仍有帮助 ❤️ Sass（含 SCSS 语法） ☑ PostCSS ⚫ Less 任务执行器 (Task Runner) ❤️ npm scripts ⚫ Gulp 模块打包工具 (Module Bundler) ❤️ Webpack ☑ Rollup ☑ Parcel 代码检查与格式化工具 一种静态代码分析工具，用于检测代码语法错误、规范问题 ❤️ ESLint ❤️ Prettier ⚫ StandardJS 六、前端框架与库 📚 框架和库能帮你快速构建复杂的单页应用 (SPA)，选择一个深入学习是进阶的关键。\n选择一款框架 (Framework) ❤️ React.js ❤️ Redux（状态管理库） ❤️ MobX（替代 Redux 的状态管理库） ✅ Angular ❤️ RxJS（响应式编程库） ❤️ NgRx（Angular 生态的状态管理库） ✅ Vue.js ❤️ VueX（Vue 生态的状态管理库） 现代 CSS 方案 ❤️ Styled Components ❤️ CSS Modules ❤️ Styled JSX ❤️ Emotion ⚫ Radium ⚫ Glamorous Web 组件 (Web Component) ☑ HTML 模板 (Template) ☑ 自定义元素 (Custom Elements) ☑ 影子 (Shadow) DOM CSS 框架 (Framework) 基于 JS 的 CSS 框架（适配 JS 应用程序）： ❤️ Reactstrap（适配 React 的 Bootstrap 组件） ❤️ Material UI ❤️ Tailwind CSS ❤️ Chakra UI 纯 CSS 优先的框架（默认不含 JS 组件）： ❤️ Bootstrap ❤️ Materialize CSS ❤️ Bulma 七、测试与质量保障 ✅ 写出高质量、可维护的代码，测试是必不可少的一环。\n☑ 理解单元测试 (Unit Test)、集成测试 (Integration Test)、功能测试 (Functional Test) 的区别，并学习使用以下工具编写测试： ❤️ Jest ❤️ react-testing-library ❤️ Cypress ❤️ Enzyme ☑ Mocha ☑ Chai ☑ Ava ☑ Jasmine ☑ 补充说明：以上工具可满足各类测试需求，无需全部掌握，按需选择即可 八、进阶与拓展 🚀 当你掌握了基础和框架后，可以向更广阔的领域拓展。\n渐进式网页应用程序 (PWA*) Progressive Web App，渐进式网页应用，可实现类原生应用的体验 ❤️ Storage（存储 API） ❤️ Web Sockets（网络通信 API） ❤️ Service Workers（离线缓存核心） ❤️ Location（地理位置 API） ❤️ Notifications（通知 API） ❤️ Device Orientation（设备方向 API） ❤️ Payments（支付 API） ❤️ Credentials（凭证 API） ☑ 补充说明：需了解 PWA 中涉及的各类 Web API 基础用法 性能优化 ☑ PRPL 模式 (Pattern)（前端加载性能优化模式） ☑ RAIL 模型 (Model)（以用户体验为核心的性能评估模型） ☑ 性能指标* (Performance Metric) 如 LCP、FID、CLS 等核心 Web 性能指标 ☑ 使用 Lighthouse（Chrome 内置性能检测工具） ☑ 使用浏览器开发者工具 (DevTool) ☑ 计算、测量并优化应用性能 类型检查工具 (Type Checker) ❤️ TypeScript ⚫ Flow 服务器端渲染 (Server Side Rendering, SSR) ❤️ React.js 生态： ❤️ Next.js ⚫ After.js ✅ Angular 生态： ☑ Universal ✅ Vue.js 生态： ❤️ Nuxt.js GraphQL ❤️ Apollo（GraphQL 客户端/服务端框架） ❤️ Relay Modern（React 生态的 GraphQL 框架） 移动端应用开发 ❤️ React Native（基于 React 的跨平台原生应用框架） ☑ NativeScript（跨平台原生应用框架） ☑ Flutter（Google 推出的跨平台 UI 框架） ☑ Ionic（基于 Web 技术的混合应用框架） 桌面端应用开发 ❤️ Electron（基于 Web 技术的跨平台桌面应用框架） ⚫ Carlo（轻量级桌面应用框架） ⚫ Proton Native（类 React Native 的桌面应用框架） 静态网站生成器 (Static Site Generator) ☑ Next.js ☑ GatsbyJS（React 生态） ☑ Nuxt.js（Vue 生态） ☑ Vuepress（Vue 生态文档生成工具） ☑ Jekyll（Ruby 开发的静态生成器） ☑ Hugo（Go 开发的高性能静态生成器） ☑ Gridsome（Vue 生态，类似 Gatsby） Web Assembly ☑ 补充说明：Web Assembly（简称 WASM）是由高级语言（如 Go、C、C++、Rust）编译生成的二进制指令集，运行速度远超 JavaScript。WASM 1.0 已被主流浏览器支持，W3C 于 2019 年底将其列为官方标准，但距离全面普及仍需时间。 九、持续学习 📖 前端技术日新月异，这份路线图只是一个起点。保持好奇心，持续学习新的特性、工具和最佳实践，才能在这个领域不断进步。\n图 信息来源 台湾正体中文翻译原作者：goodjack/developer-roadmap-chinese / littlegoodjack 路线图完整版及更多资源：http://roadmap.sh ","date":"2026-02-13T16:36:00Z","permalink":"https://ye-guan-xing.github.io/p/%E5%89%8D%E7%AB%AF%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E8%BF%9B%E9%98%B6/","title":"前端学习路线：从入门到进阶"},{"content":"前言 这份后端学习路线源自 roadmap.sh，是全球开发者广泛参考的后端技术学习指南。它清晰梳理了从基础原理到架构设计的核心知识点，并标注了学习优先级，帮助你在后端技术体系中高效构建知识体系，避免盲目学习。（参考图见尾部）\n图例说明 在开始之前，请先了解图中标记的含义，这将帮助你判断学习的优先级：\n❤️ 个人推荐 / 意见：核心必学内容，是后端开发的基石，必须掌握。 ✅ 替代选项 — 选择此项或是紫色项目：选其一深入即可，通常是不同的技术方案或框架，无需全部学习。 ☑ 不必严格按照路线图的顺序（随时可学）：可灵活安排学习顺序，属于进阶或补充性知识。 ⚫ 我不推荐：优先级较低或可跳过，在当前技术环境下必要性不高。 一、互联网基础与前端知识 🌐 后端开发离不开对互联网和前端的理解，这是构建服务的基础。\n❤️ 互联网是如何运作的？ ❤️ 什么是 HTTP？ ❤️ 浏览器及其工作原理？ ❤️ DNS 及其工作原理？ ❤️ 什么是域名 (Domain Name)？ ❤️ 什么是主机托管 (Hosting)？ ✅ HTML ☑ CSS ❤️ JavaScript 二、操作系统和基础知识 💻 操作系统是后端服务运行的根基，掌握其原理能让你写出更高效、稳定的代码。\n❤️ 终端机的使用 ❤️ 操作系统如何运作 ❤️ 行程管理（Process Management） ❤️ 执行绪（Thread）和并行（Concurrency） ❤️ 基础的终端机指令（grep, awk, sed, lsof, curl, wget, tail, head, less, find, ssh, kill） ❤️ 记忆体管理 ❤️ 行程间通讯（IPC） ❤️ I/O 管理 ❤️ POSIX 的基础认知（stdin, stdout, stderr, pipes） ❤️ 基础的网络概念 三、学习一门语言 📚 选择一门后端语言深入学习，理解其运行时特性（如并行、内存模型）是进阶的关键。\n✅ Java ✅ C# ✅ PHP ☑ Rust ☑ Go ❤️ JavaScript ❤️ Python ✅ Ruby 四、版本控制系统 (VCS) 🛠️ 现代后端开发离不开版本控制，它是团队协作和代码管理的核心工具。\n☑ 版本控制系统的定义及使用价值 ❤️ Git 的基本用法 ☑ 仓库代管（Repo Hosting）服务 ✅ 注册账号并学习使用 GitHub ❤️ GitHub ☑ GitLab ☑ Bitbucket 五、数据库 🗄️ 数据库是后端服务存储数据的核心，关系型与 NoSQL 数据库各有适用场景。\n关系式数据库 ❤️ PostgreSQL ✅ MySQL ✅ MariaDB ☑ MS SQL ☑ Oracle NoSQL 数据库 ❤️ 文件资料库（MongoDB, CouchDB） ✅ 栏位资料库（Cassandra） ☑ 时序型资料库（InfluxDB, TimescaleDB） ☑ 即时资料库（Firebase, RethinkDB） 更多关于资料库 ❤️ ORM（物件关系映射） ❤️ ACID ❤️ 交易（Transaction） ❤️ N+1 问题 ❤️ 资料库正规化（Normalization） ❤️ 索引（Index）及其运作方式 ☑ 资料复写（Replication） ☑ 分片（Sharding）策略 ☑ CAP 定理 六、学习 API 相关知识 🚀 API 是后端服务对外交互的核心，掌握不同类型的 API 设计与实现至关重要。\n❤️ Cookie Based ❤️ OAuth ❤️ Basic Authentication ❤️ Token Authentication ❤️ JWT ❤️ OpenID ☑ SAML ☑ HATEOAS ❤️ OpenAPI 规范以及 Swagger ❤️ 认证（Authentication） ❤️ REST ☑ 阅读 Roy Fielding 的论文 ☑ JSON API ☑ SOAP ❤️ gRPC 七、缓存 (Caching) 🚀 缓存是提升后端服务性能的关键手段，合理使用缓存能显著降低数据库压力。\n❤️ Redis ✅ Memcached ☑ CDN ☑ 伺服器端（Server Side） ☑ 用户端（Client Side） 八、网络安全知识 🔒 安全是后端服务不可忽视的环节，掌握常见安全机制与风险防范是必备能力。\n❤️ MD5 以及为什么不要使用它 ❤️ SHA 家族 ☑ scrypt ❤️ bcrypt ❤️ 杂凑（Hashing）演算法 ❤️ HTTPS ❤️ 内容安全政策（CSP） ❤️ CORS ❤️ SSL/TLS ❤️ OWASP 安全风险 九、测试 (Testing) ✅ 测试是保障后端服务质量的核心，不同类型的测试覆盖不同的质量维度。\n❤️ 整合测试（Integration Testing） ❤️ 单元测试（Unit Testing） ❤️ 功能测试（Functional Testing） ☑ CI/CD（持续整合/持续交付） 十、设计和开发原则 📐 遵循良好的设计原则能让代码更易维护、扩展，是资深后端开发者的核心素养。\n☑ 四人帮（GOF）设计模式 ☑ 领域驱动设计（DDD） ☑ 测试驱动开发（TDD） ❤️ SOLID ❤️ KISS ❤️ YAGNI ❤️ DRY 搜索引擎（Search Engine） ❤️ Elasticsearch ✅ Solr 十一、架构模式 🏗️ 架构模式决定了系统的扩展性与维护性，不同场景下选择合适的架构至关重要。\n☑ 整合型*（Monolithic）应用程式 ❤️ 微服务（Microservice） ☑ 服务导向架构（SOA） ☑ CQRS 和事件来源模式 ☑ 无伺服器（Serverless） *译注：又译作单体式 十二、容器化 vs 虚拟化 (Containerization / Virtualization) 🐳 容器化技术简化了部署与环境一致性，是现代后端部署的主流方式。\n❤️ Docker ⚫ rkt ⚫ LXC 讯息代理*（Message Broker） ❤️ RabbitMQ ✅ Kafka *译注：又译作讯息中介 十三、GraphQL 📊 GraphQL 提供了更灵活的 API 查询方式，适用于复杂数据场景的后端服务。\n❤️ Apollo ✅ Relay Modern 十四、图资料库* (Graph Database) 🗄️ 图资料库擅长处理复杂的关联数据，适用于社交网络、知识图谱等场景。\n❤️ Neo4j *译注：又译作图形资料库、图数据库 十五、WebSocket 🔌 WebSocket 实现了双向实时通信，是实时应用（如聊天、直播）的核心技术。\n缓解策略（Mitigation Strategy） ☑ 从容退化（Graceful Degradation） ☑ 请求频率限制（Throttle） ☑ 背压（Backpressure） ☑ 负载转移（Loadshifting） ☑ 断路器（Circuit Breaker） *译注：又译作反压 十六、网页服务器 (Web Server) 🌐 网页服务器是后端服务的入口，选择合适的服务器能提升服务性能与稳定性。\n❤️ Nginx ✅ Apache ☑ Caddy ☑ MS IIS 十七、规模化建设 📈 当服务用户量增长时，规模化建设是保障服务可用性与性能的关键。\n❤️ 迁移策略（Migration Strategy） ❤️ 水平 vs 垂直扩展（Horizontal vs Vertical Scaling） 以可观测性（Observability）为前提进行建设 ☑ 仪表（Instrumentation） ☑ 监测（Monitoring） ☑ 遥测（Telemetry） ☑ 指标纪录以及其他可观测的项目 ☑ 可以在出错时帮助你除错和解决问题 十八、持续学习 📖 后端技术生态迭代迅速，从云原生到 Serverless，持续学习新工具与最佳实践是保持竞争力的核心。\n图 信息来源 台湾正体中文翻译原作者：goodjack/developer-roadmap-chinese / littlegoodjack 路线图完整版及更多资源：http://roadmap.sh ","date":"2026-02-13T17:00:00+08:00","permalink":"https://ye-guan-xing.github.io/p/%E5%90%8E%E7%AB%AF%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF%E4%BB%8E%E5%9F%BA%E7%A1%80%E5%88%B0%E6%9E%B6%E6%9E%84/","title":"后端学习路线：从基础到架构"},{"content":"【Git 新手通关指南】解决 master 与开发分支代码不一致，手把手教你同步分支 作为 Git 新手，刚接触分支管理时大概率会遇到这个问题：本地master主分支和自己创建的z1开发分支代码不一样了，该怎么同步？为什么不能直接在master分支改代码？这篇文章用最通俗的话+一步步实操，帮你彻底搞懂！\n一、先搞懂：master 和开发分支的“分工”（新手先记牢） 在团队协作或个人开发中，Git 分支有明确的“职责划分”，这是解决分支同步问题的基础：\nmaster（主分支）：可以理解为项目的“正式版”，是稳定、可上线的代码版本，绝对不能随便改； z1（开发分支）：你自己的“工作版”，所有开发、修改、调试都在这个分支做，相当于“草稿纸”，改乱了也不影响主分支。 所谓“同步代码”，本质就是把master分支的最新内容，整合到你的z1开发分支里，让两个分支的代码保持一致，同时又不破坏master的稳定性。\n二、核心操作：同步 master 与 z1 分支的完整步骤 假设你已经克隆了仓库（git clone https://gitee.com/yigen1981/test.git），且创建了z1分支（git branch z1），现在要解决两个分支代码不一致的问题，按以下步骤来，一步都不能错！\n前置准备 打开本地仓库对应的终端（Git Bash/CMD/终端都可以），确保你在仓库根目录下。\n步骤 1：拉取远程最新代码（避免合并出错） 先把云端（Gitee/GitHub）的所有分支最新代码拉到本地，防止合并时因为本地代码过时出问题：\n1 git pull ✅ 新手解读：这个命令相当于“刷新”本地代码，让你的master和z1都先同步云端的最新版本。\n步骤 2：切换到 z1 开发分支（核心！必须先切） 所有操作都要在z1分支做，先确认并切换到z1：\n1 2 3 4 # 切换到z1分支 git checkout z1 # 可选：检查当前分支（带*的就是当前分支，确保是z1） git branch ✅ 新手解读：如果直接在master分支操作，相当于直接改“正式版”，风险极高，这是新手最容易踩的坑！\n步骤 3：合并 master 代码到 z1（同步的核心） 执行合并命令，把master的最新代码整合到z1：\n1 git merge master ✅ 新手解读：这个命令的意思是“把 master 的最新内容，合并到我当前所在的 z1 分支”，合并后 z1 就包含了 master 的所有代码，实现同步。\n❌ 常见问题：如果合并时提示“冲突（conflict）”，比如“Auto-merging xxx.txt”“CONFLICT (content)”，别慌！这是因为同一个文件在master和z1里改了不同内容，Git 不知道该保留哪个，需要手动解决：\n打开提示冲突的文件，会看到类似这样的标记： 1 2 3 4 5 \u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt; HEAD （这是z1分支的内容） 我在z1分支改的内容 ======= （分隔线，两边是不同分支的内容） 别人在master分支改的内容 \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; master （这是master分支的内容） 删除这些标记（\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;/=======/\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;），保留你需要的内容（比如保留最新的、正确的内容）； 保存文件，回到终端继续下一步。 步骤 4：提交合并后的代码并推到云端 合并完成（冲突解决后），要把同步后的代码保存并上传到云端的z1分支：\n1 2 3 4 5 6 # 1. 把所有改动的文件加入“待提交清单”（.代表所有文件） git add . # 2. 保存改动并加备注（备注要清晰，新手别乱写） git commit -m \u0026#34;合并master分支代码，同步最新内容\u0026#34; # 3. 把本地z1的代码推到云端z1分支（关键！不然云端代码还是旧的） git push origin z1 ✅ 新手解读：这三步相当于“保存草稿 → 给草稿写备注 → 把草稿上传到云端”，确保你的修改不会丢。\n三、关键原则：为什么绝对不能在 master 分支提交代码？ 新手最容易犯的错就是直接在master分支改代码、提交，这是严格禁止的，原因很简单：\n保护主分支稳定性：master是正式版代码，直接修改可能导致项目运行出错，甚至影响整个团队； 符合分支管理规范：所有开发工作都在开发分支（如 z1）完成，确认没问题后，再由负责人统一合并到master； 避免代码混乱：如果多人都直接改master，很容易出现代码冲突无法解决，最后整个项目代码乱套。 四、完整流程复盘（新手直接抄） 把所有步骤串起来，形成可直接复制的操作流：\n1 2 3 4 5 6 7 8 9 10 11 # 1. 拉取远程最新代码 git pull # 2. 切换到z1分支 git checkout z1 # 3. 合并master代码到z1 git merge master # 4. 解决冲突（如果有）→ 保存文件 # 5. 提交并推送 git add . git commit -m \u0026#34;合并master分支，同步代码\u0026#34; git push origin z1 五、新手总结 分支同步的核心逻辑：切换到开发分支（z1）→ 合并主分支（master）→ 提交推送，始终围绕开发分支操作； 绝对禁忌：不在master分支做任何开发、提交操作，保护主分支的稳定性； 遇到冲突别慌：冲突是 Git 的正常提示，手动删除冲突标记、保留正确内容即可，合并后一定要推送代码到云端。 ","date":"2026-01-13T10:30:00+08:00","permalink":"https://ye-guan-xing.github.io/p/%E8%A7%A3%E5%86%B3master%E4%B8%8E%E5%BC%80%E5%8F%91%E5%88%86%E6%94%AF%E4%BB%A3%E7%A0%81%E4%B8%8D%E4%B8%80%E8%87%B4%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E4%BD%A0%E5%90%8C%E6%AD%A5%E5%88%86%E6%94%AF/","title":"解决master与开发分支代码不一致,手把手教你同步分支"},{"content":"从新闻页面 CSS+成果图看布局设计：实用 Flex 与结构化布局技巧 在前端开发中，布局是页面的骨架，直接决定了内容呈现的逻辑性和视觉舒适度。最近落地了一个新闻列表页面（如下成果图所示），其布局设计简洁高效，尤其适合信息密集型页面（如新闻、资讯类），今天就结合成果图+对应 CSS 代码，拆解其中的布局思路和实用技巧。\n（注：下图即为本次分析对应的页面成果，后续会一一对应布局模块） 一、布局基础：全局样式与容器搭建（成果图整体框架） 任何布局的第一步都是打“地基”——成果图里的整个内容区域，就是通过以下代码实现的核心容器：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 * { margin: 0; padding: 0; box-sizing: border-box; } body { background: orange; background-attachment: fixed; padding-top: 25px; } .big_box { width: 80%; margin: 0 auto; padding: 20px; background: linear-gradient(180deg, #f0f0f0 0%, whitesmoke 100%); border-radius: 20px; } 对应成果图的效果： 成果图里的所有内容（左侧新闻+右侧热搜）都包裹在big_box中，实现了水平居中+左右留白，适配不同屏幕； 容器的渐变背景+圆角，让成果图里的内容区和外部橙色背景区分开，视觉更整洁。 二、核心布局：Flex 的灵活运用（成果图主次分栏+内容排列） 成果图的“左主新闻区+右热搜区”“单图/多图新闻”等结构，全靠 Flex 布局实现：\n1. 主次分栏布局（成果图左+右区域） 1 2 3 4 5 6 7 8 9 10 11 12 13 .box { max-width: 1200px; display: flex; gap: 50px; align-items: flex-start; } .left { flex: 1; } /* 成果图左侧主新闻区 */ .right { width: 360px; flex-shrink: 0; } /* 成果图右侧热搜区 */ 对应成果图的效果： 左侧主区（left）自动占满剩余空间，呈现多条新闻；右侧热搜区（right）固定宽度不收缩，保证榜单布局稳定（成果图右侧的热搜列表不会被挤压）； 两者顶部对齐（align-items: flex-start），对应成果图里左、右区域的顶部是齐平的。 2. 单图新闻布局（成果图“2024 高考倒计时”那条） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 .row_box { display: flex; gap: 16px; } .img1 { width: 32%; aspect-ratio: 4/3; flex-shrink: 0; border-radius: 8px; } .text_box { flex: 1; display: flex; flex-direction: column; justify-content: space-between; } 对应成果图的效果： 成果图里“2024 高考倒计时”左侧的空白块，就是img1（固定宽占比+比例），不会因文本长度变形； 右侧文本区上下分布（标题在上、时间/来源在下），对应成果图里这条新闻的文字区域布局均匀。 3. 多图新闻布局（成果图第三条新闻的三个图） 1 2 3 4 5 6 7 8 9 10 .img3_box { display: flex; gap: 6px; margin: 10px 0; } .img3 { flex: 1; aspect-ratio: 16/10; border-radius: 6px; } 对应成果图的效果： 成果图第三条新闻下方的三个空白块，就是通过flex: 1实现的均等分栏，无需手动算宽度，保证三张图整齐排列。\n4. 热搜榜单布局（成果图右侧列表） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 .hot_li { display: flex; align-items: flex-start; margin-bottom: 14px; } .rank { width: 18px; flex-shrink: 0; font-weight: 700; } .hot_text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 对应成果图的效果： 成果图右侧的“1、2、3\u0026hellip;”排名，是rank（固定宽度不位移）； 超长热搜标题自动省略（比如成果图里的第一条热搜），避免换行打乱布局。 三、布局设计的核心思路总结（结合成果图） 这个页面的布局之所以在成果图里呈现得清晰舒适，核心是“结构化+灵活性”的平衡：\n先骨架后内容：先通过big_box/box定整体框架（成果图的大容器+分栏），再填内部组件； Flex 适配场景：分栏用 Flex、单图/多图用 Flex、榜单用 Flex，替代浮动/定位，代码简洁且适配成果图的各种排列需求； 固定+灵活结合：热搜区/图片/排名固定（保证成果图里的关键区域稳定），主新闻区灵活伸缩（适配不同屏幕）； 细节控体验：文本省略、统一间距、固定比例，让成果图里的布局既规整又不呆板。 ","date":"2025-12-14T22:36:00Z","permalink":"https://ye-guan-xing.github.io/p/css%E5%B0%8F%E6%A1%88%E4%BE%8B%E8%AE%B2%E5%B8%83%E5%B1%80flex-grid/","title":"CSS小案例，讲布局（Flex+Grid）"},{"content":"从 Vue CLI 迁移到 Vite 的完整指南 📋 迁移概览 将项目从 Vue CLI 迁移到 Vite 确实如你所说，主要包括以下几个核心步骤。\n🗺️ 迁移路线图 步骤 1：备份原有项目 1 2 # 在开始前一定要备份 cp -r my-project my-project-backup 步骤 2：删除旧配置文件 1 2 3 4 # 删除 Vue CLI 相关配置文件 rm vue.config.js rm babel.config.js # 如果存在 rm .eslintrc.js # 如果使用单独的配置文件 步骤 3：清理项目依赖 1 2 3 # 删除 node_modules 和锁文件 rm -rf node_modules rm package-lock.json # 或 rm yarn.lock 📝 详细步骤说明 1. 修改 package.json 主要变化：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { \u0026#34;type\u0026#34;: \u0026#34;module\u0026#34;, // 新增：声明为 ES 模块 \u0026#34;scripts\u0026#34;: { // Vue CLI 脚本 → Vite 脚本 \u0026#34;serve\u0026#34;: \u0026#34;vue-cli-service serve\u0026#34;, // 删除 \u0026#34;build\u0026#34;: \u0026#34;vue-cli-service build\u0026#34;, // 删除 \u0026#34;dev\u0026#34;: \u0026#34;vite\u0026#34;, // 新增 \u0026#34;build\u0026#34;: \u0026#34;vite build\u0026#34;, // 修改 \u0026#34;preview\u0026#34;: \u0026#34;vite preview\u0026#34; // 新增 }, \u0026#34;dependencies\u0026#34;: { // 移除 Vue CLI 特定的依赖 // 保持应用层依赖不变 }, \u0026#34;devDependencies\u0026#34;: { // 完全替换开发依赖 \u0026#34;@vue/cli-*\u0026#34;: \u0026#34;~5.0.0\u0026#34;, // 删除所有 \u0026#34;@vitejs/plugin-vue\u0026#34;: \u0026#34;^5.0.5\u0026#34;, // 新增 \u0026#34;vite\u0026#34;: \u0026#34;^5.4.8\u0026#34;, // 新增 \u0026#34;sass-embedded\u0026#34;: \u0026#34;^1.83.0\u0026#34; // 推荐使用 sass-embedded } } 为什么要这样改？\nVue CLI 基于 Webpack，而 Vite 是全新的构建工具 type: \u0026quot;module\u0026quot; 让 Node.js 支持 ES6 模块语法 Vite 的命令更简洁直观 2. 配置文件迁移 从 vue.config.js 到 vite.config.js\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // vue.config.js (旧) module.exports = { devServer: { proxy: { \u0026#34;/api\u0026#34;: { target: \u0026#34;http://10.245.30.86:8080\u0026#34;, changeOrigin: true, }, }, }, }; // vite.config.js (新) import { defineConfig } from \u0026#34;vite\u0026#34;; export default defineConfig({ server: { proxy: { \u0026#34;/api\u0026#34;: { target: \u0026#34;http://10.245.30.86:8080\u0026#34;, changeOrigin: true, }, }, }, }); 关键差异：\nVite 使用 ES 模块语法 (import/export) 配置结构更扁平 热更新速度显著提升 3. 环境变量处理 Vue CLI 方式：\n1 2 3 4 5 6 // .env.development VUE_APP_BASE_API=/api VUE_APP_PROXY_TARGET=http://10.245.30.86:8080 // 代码中使用 process.env.VUE_APP_BASE_API Vite 方式：\n1 2 3 4 5 6 // .env.development VITE_BASE_API=/api VITE_PROXY_TARGET=http://10.245.30.86:8080 // 代码中使用 import.meta.env.VITE_BASE_API 或者保留 Vue 前缀：\n1 2 3 4 // vite.config.js 中配置 envPrefix: [\u0026#34;VUE_APP_\u0026#34;, \u0026#34;VITE_\u0026#34;], // 这样可以使用两种前缀 import.meta.env.VUE_APP_BASE_API; 4. 请求配置文件修改 修改 src/utils/request.js（或类似文件）：\n1 2 3 4 5 6 7 8 9 10 11 // 原来的 Vue CLI 方式 const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API || \u0026#34;/api\u0026#34;, timeout: 10000, }); // 修改为 Vite 方式 const service = axios.create({ baseURL: import.meta.env.VUE_APP_BASE_API || \u0026#34;/api\u0026#34;, timeout: 10000, }); 重要说明：\nprocess.env → import.meta.env 只有以 VITE_ 或配置的前缀开头的变量才会被暴露 环境变量在构建时被替换 5. HTML 文件处理 Vue CLI：\n1 2 3 public/ index.html src/ Vite：\n1 2 3 根目录/ index.html # 移动到根目录 src/ 修改 index.html：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u0026lt;!-- 原来的 --\u0026gt; \u0026lt;script src=\u0026#34;/js/chunk-vendors.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script src=\u0026#34;/js/app.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;!-- 现在：删除所有显式的脚本引入 --\u0026gt; \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;zh-CN\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34; /\u0026gt; \u0026lt;link rel=\u0026#34;icon\u0026#34; type=\u0026#34;image/svg+xml\u0026#34; href=\u0026#34;/vite.svg\u0026#34; /\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34; /\u0026gt; \u0026lt;title\u0026gt;Vite App\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;!-- Vite 会自动注入脚本 --\u0026gt; \u0026lt;script type=\u0026#34;module\u0026#34; src=\u0026#34;/src/main.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 6. 静态资源引用方式 Vue CLI：\n1 2 3 4 5 import logo from \u0026#39;@/assets/logo.png\u0026#39;; \u0026lt;img :src=\u0026#34;logo\u0026#34; /\u0026gt; // 或 \u0026lt;img src=\u0026#34;@/assets/logo.png\u0026#34; /\u0026gt; Vite：\n1 2 3 4 5 6 7 8 9 // 方式1：使用相对或绝对路径 \u0026lt;img src=\u0026#34;/src/assets/logo.png\u0026#34; /\u0026gt; // 方式2：使用 import import logoUrl from \u0026#39;@/assets/logo.png\u0026#39;; \u0026lt;img :src=\u0026#34;logoUrl\u0026#34; /\u0026gt; // 方式3：使用 public 目录（不经过构建） \u0026lt;img src=\u0026#34;/logo.png\u0026#34; /\u0026gt; // 放在 public/logo.png 7. 路径别名配置 vite.config.js 中配置：\n1 2 3 4 5 6 7 8 9 10 11 import path from \u0026#34;path\u0026#34;; export default defineConfig({ resolve: { alias: { \u0026#34;@\u0026#34;: path.resolve(__dirname, \u0026#34;./src\u0026#34;), // 可以添加更多别名 \u0026#34;@components\u0026#34;: path.resolve(__dirname, \u0026#34;./src/components\u0026#34;), }, }, }); 🔧 常见问题解决 问题 1：CommonJS 模块报错 1 2 3 4 5 // 错误：require is not defined const module = require(\u0026#34;module\u0026#34;); // 解决：改为 ES6 导入 import module from \u0026#34;module\u0026#34;; 问题 2：process 变量报错 1 2 3 4 5 6 7 // 错误：process is not defined if (process.env.NODE_ENV === \u0026#34;development\u0026#34;) { } // 解决：使用 import.meta.env if (import.meta.env.MODE === \u0026#34;development\u0026#34;) { } 问题 3：CSS 预处理器错误 1 2 3 4 5 6 7 8 9 10 // vite.config.js 中正确配置 export default defineConfig({ css: { preprocessorOptions: { scss: { additionalData: `@import \u0026#34;@/styles/variables.scss\u0026#34;;`, }, }, }, }); 📦 完整迁移脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #!/bin/bash # migrate-to-vite.sh echo \u0026#34;🚀 开始迁移 Vue CLI 到 Vite...\u0026#34; # 1. 备份 echo \u0026#34;📦 备份原项目...\u0026#34; cp -r ./ ./backup-$(date +%Y%m%d-%H%M%S) # 2. 删除旧文件 echo \u0026#34;🗑️ 删除旧配置文件...\u0026#34; rm -f vue.config.js rm -f babel.config.js # 3. 清理依赖 echo \u0026#34;🧹 清理 node_modules...\u0026#34; rm -rf node_modules rm -f package-lock.json # 4. 移动 HTML 文件 echo \u0026#34;📄 移动 HTML 文件...\u0026#34; if [ -f \u0026#34;public/index.html\u0026#34; ]; then mv public/index.html ./ rm -rf public fi # 5. 更新 package.json echo \u0026#34;📝 更新 package.json...\u0026#34; # 这里可以编写 sed 命令或手动修改 # 6. 创建 vite.config.js echo \u0026#34;⚙️ 创建 Vite 配置文件...\u0026#34; cat \u0026gt; vite.config.js \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; import { defineConfig } from \u0026#34;vite\u0026#34;; import vue from \u0026#34;@vitejs/plugin-vue\u0026#34;; import path from \u0026#34;path\u0026#34;; export default defineConfig({ plugins: [vue()], resolve: { alias: { \u0026#34;@\u0026#34;: path.resolve(__dirname, \u0026#34;./src\u0026#34;), }, }, server: { port: 8081, open: true, proxy: { \u0026#34;/api\u0026#34;: { target: \u0026#34;http://localhost:8080\u0026#34;, changeOrigin: true, }, }, }, }); EOF echo \u0026#34;✅ 迁移完成！\u0026#34; echo \u0026#34;📦 请运行: npm install\u0026#34; echo \u0026#34;🚀 然后运行: npm run dev\u0026#34; 📊 迁移前后对比 特性 Vue CLI (Webpack) Vite 启动速度 20-30 秒 1-3 秒 热更新 1-3 秒 50-300 毫秒 配置复杂度 复杂 简单 插件生态 成熟 快速增长 构建速度 中等 快速 🎯 迁移后的优势 极速启动：Vite 启动速度比 Vue CLI 快 10-100 倍 闪电热更新：无论项目大小，热更新几乎瞬间完成 更简单配置：配置文件更简洁易懂 现代工具链：原生支持 ES 模块、TypeScript 等 更好的开发体验：按需编译，无需等待整个应用构建 📚 总结 迁移到 Vite 确实是一个相对简单的过程，主要就是：\n改配置：package.json + vite.config.js 改代码：环境变量访问方式 改结构：HTML 文件位置 改引用：静态资源导入方式 ","date":"2025-12-14T22:36:00Z","permalink":"https://ye-guan-xing.github.io/p/%E5%A6%82%E4%BD%95%E5%B0%86vue-cli-%E5%86%99%E7%9A%84%E5%89%8D%E7%AB%AF%E6%96%87%E4%BB%B6%E8%BD%AC%E6%8D%A2%E4%B8%BAvite%E8%AF%A6%E7%BB%86%E6%9D%BF/","title":"如何将vue cli 写的前端文件转换为vite（详细板）"},{"content":"写给小白的校园 OJ 开发详解 前言：为什么需要这些配置？ 想象一下你要建一座房子 🏠：\nvue.config.js = 房子的施工图纸和施工规则 main.js = 房子的地基和主要结构 App.vue = 房子的外壳框架 router = 房子的导航系统（房间分布图） layout = 房子的装修布局（每个房间的固定摆设） 下面我一步步拆解，保证你能完全看懂！\n注：本文基于 Vue3 开发，如果你用的是 Vue2，请自行修改。 的本个项目的代码可以看这个仓库：仓库地址\nvue.config.js 这个文件告诉 Vue 项目“怎么干活”：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 就像给工人说：从环境变量读取接口地址，默认用\u0026#34;/api\u0026#34; const VUE_APP_BASE_API = process.env.VUE_APP_BASE_API || \u0026#34;/api\u0026#34;; module.exports = { devServer: { port: 8081, // 开发服务器端口（房子建在8081号地皮） open: true, // 自动打开浏览器（房子建好自动开门） proxy: { // 配置代理：把本地/api开头的请求转发到真正的后端服务器 \u0026#34;/api\u0026#34;: { target: \u0026#34;http://real-backend.com\u0026#34;, // 真正的后端地址 changeOrigin: true, // 改变请求源（假装是同源的） pathRewrite: { \u0026#34;^/api\u0026#34;: \u0026#34;\u0026#34; }, // 转发时去掉/api前缀 ws: false, // 关闭WebSocket代理（避免冲突） }, }, }, }; 通俗理解：\n开发时，前端在localhost:8081运行，后端可能在localhost:8080或其他地方。配置代理后，前端访问/api/user时，会自动转发到http://real-backend.com/user，解决了跨域问题。\n二、地基建设：main.js 这是整个应用的启动入口：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // 1. 导入Vue框架（拿到建房子的工具包） import { createApp } from \u0026#34;vue\u0026#34;; // 2. 导入三个核心模块（三大件） import App from \u0026#34;./App.vue\u0026#34;; // 房子蓝图 import router from \u0026#34;./router\u0026#34;; // 导航系统 import store from \u0026#34;./store\u0026#34;; // 中央仓库（存数据） // 3. 导入UI组件库（预制好的门窗、家具） import ElementPlus from \u0026#34;element-plus\u0026#34;; // 4. 创建Vue应用实例（开始施工） const app = createApp(App); // 5. 安装各种功能模块 app.use(router); // 安装导航系统 app.use(store); // 安装中央仓库 app.use(ElementPlus); // 安装预制家具 // 6. 挂载到页面（把建好的房子放到#app这个地块上） app.mount(\u0026#34;#app\u0026#34;); 一张图看懂：\n1 2 浏览器打开 → main.js启动 → 创建Vue实例 → 挂载路由/状态管理/UI库 → 渲染App.vue → 显示页面 三、房子外壳：App.vue 这是最外层的容器，所有页面都装在这里：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;template\u0026gt; \u0026lt;!-- 这里就是一个动态展示区，根据URL显示不同页面 --\u0026gt; \u0026lt;router-view /\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script setup\u0026gt; // 这里是空的，因为这个组件只负责\u0026#34;装东西\u0026#34;，不处理具体业务 \u0026lt;/script\u0026gt; \u0026lt;style lang=\u0026#34;scss\u0026#34;\u0026gt; /* 导入全局样式（全房子通用） */ @import \u0026#34;@/assets/styles/variables.scss\u0026#34;; // 颜色/尺寸变量 @import \u0026#34;@/assets/styles/common.scss\u0026#34;; // 公共样式 \u0026lt;/style\u0026gt; 重要概念：\n\u0026lt;router-view /\u0026gt; 就像一个幻灯片投影仪，URL 变化时，自动切换显示的内容 全局样式在这里引入，所有子页面都能用这些样式 四、房间布局：MainLayout.vue 这是页面的通用布局模板（像酒店的标准间装修）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;template\u0026gt; \u0026lt;div class=\u0026#34;main-layout\u0026#34;\u0026gt; \u0026lt;!-- 顶部导航栏（每个页面都有） --\u0026gt; \u0026lt;AppNavbar /\u0026gt; \u0026lt;!-- 主要内容区（会变化的部分） --\u0026gt; \u0026lt;main class=\u0026#34;content\u0026#34;\u0026gt; \u0026lt;router-view /\u0026gt; \u0026lt;!-- 这里显示具体页面 --\u0026gt; \u0026lt;/main\u0026gt; \u0026lt;!-- 底部页脚（每个页面都有） --\u0026gt; \u0026lt;AppFooter /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; 布局逻辑：\n1 2 3 4 5 6 7 8 9 10 ┌─────────────────────┐ │ 导航栏 Navbar │ ← 固定，每个页面都有 ├─────────────────────┤ │ │ │ \u0026lt;router-view\u0026gt; │ ← 动态变化的部分 │ (当前路由页面) │ │ │ ├─────────────────────┤ │ 页脚 Footer │ ← 固定，每个页面都有 └─────────────────────┘ 五、导航系统：路由配置 1. 路由规则（routes.js） 这是房间分布图，告诉系统每个 URL 对应哪个页面：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const routes = [ { path: \u0026#34;/\u0026#34;, // 访问路径 component: MainLayout, // 用哪个布局模板 redirect: \u0026#34;/home\u0026#34;, // 重定向到首页 children: [ // 子路由（布局内的具体内容） { path: \u0026#34;home\u0026#34;, // 实际路径：/home name: \u0026#34;Home\u0026#34;, // 路由名字（方便跳转） component: () =\u0026gt; import(\u0026#34;@/views/AppHome.vue\u0026#34;), // 懒加载组件 meta: { title: \u0026#34;首页\u0026#34; }, // 额外信息（页面标题、权限要求等） }, { path: \u0026#34;problems\u0026#34;, name: \u0026#34;ProblemList\u0026#34;, component: () =\u0026gt; import(\u0026#34;@/views/problem/ProblemList.vue\u0026#34;), meta: { title: \u0026#34;题目列表\u0026#34; }, }, ], }, { path: \u0026#34;/login\u0026#34;, // 登录页不需要MainLayout布局 component: () =\u0026gt; import(\u0026#34;@/views/login/AppLogin.vue\u0026#34;), meta: { title: \u0026#34;登录\u0026#34;, requiresGuest: true }, // 仅游客可访问 }, ]; 路由类型：\n公共路由：所有人可访问（首页、题目列表） 登录路由：需要登录才能访问（个人中心） 管理员路由：需要管理员权限（题目管理） 404 路由：找不到页面时显示 2. 路由守卫（index.js） 这是门口的保安，控制谁能进哪个房间：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 router.beforeEach(async (to, from, next) =\u0026gt; { // 1. 设置页面标题 if (to.meta.title) { document.title = `${to.meta.title} - OJ平台`; } // 2. 检查是否登录（看有没有token） const hasToken = store.getters.token; if (hasToken) { // 已登录的情况 if (to.path === \u0026#34;/login\u0026#34;) { next(\u0026#34;/\u0026#34;); // 已登录还去登录页？跳回首页！ } else { // 检查是否是管理员页面 if (to.meta.requiresAdmin \u0026amp;\u0026amp; !isAdmin) { next(\u0026#34;/\u0026#34;); // 不是管理员？滚回首页！ } else { next(); // 放行！ } } } else { // 未登录的情况 if (白名单.includes(to.path)) { next(); // 登录页可以进 } else { next(`/login?redirect=${to.path}`); // 去登录，记下来路 } } }); 保安的工作流程：\n1 2 3 4 5 6 7 8 9 用户访问页面 → 保安拦截 → ↓ 判断是否登录？ ├─ 已登录 → 想进登录页？→ 踢回首页 │ → 想进管理页？→ 检查权限 → 放行/踢回 │ → 进普通页 → 直接放行 │ └─ 未登录 → 想进登录页？→ 放行 → 想进其他页？→ 带到登录页（记住想去哪） 六、权限控制逻辑 1. 路由元信息（meta 字段） 1 2 3 4 5 6 meta: { title: \u0026#34;页面标题\u0026#34;, requiresAuth: true, // 需要登录 requiresAdmin: true, // 需要管理员 requiresGuest: true // 仅游客（未登录） } 2. 用户角色判断 1 2 3 4 5 6 7 8 9 10 // 从store获取用户信息 const userInfo = store.getters.userInfo; // 判断是否是管理员 const isAdmin = userInfo.roles?.includes(\u0026#34;admin\u0026#34;); // 根据角色显示不同菜单 \u0026lt;el-menu v-if=\u0026#34;isAdmin\u0026#34;\u0026gt; \u0026lt;el-menu-item\u0026gt;题目管理\u0026lt;/el-menu-item\u0026gt; \u0026lt;/el-menu\u0026gt;; 七、开发流程总结 步骤 1：环境配置 配置vue.config.js（代理、端口等） 配置环境变量（接口地址等） 步骤 2：项目初始化 main.js引入核心依赖 App.vue设置路由容器和全局样式 步骤 3：布局设计 创建MainLayout.vue（通用布局） 设计导航栏、页脚等公共组件 步骤 4：路由配置 在routes.js定义所有路由 按需设置权限（public、auth、admin） 在router/index.js配置路由守卫 步骤 5：页面开发 在views/目录创建页面组件 根据路由配置连接页面 步骤 6：权限集成 登录后保存 token 到 store 路由守卫根据 token 和角色控制访问 页面内根据角色显示不同内容 常见问题解答 Q1：为什么需要路由守卫？ A：就像学校大门，要检查学生证（登录）、权限卡（角色），防止无关人员进入。\nQ2：懒加载（() =\u0026gt; import()）有什么好处？ A：按需加载，首次打开只下载首页代码，其他页面点到了再下载，提高首次加载速度。\nQ3：为什么 token 要存 store 而不是 localStorage？ A：store 是响应式的，组件能实时感知登录状态变化，localStorage 需要手动监听。\nQ4：如何新增一个页面？ 1 2 1. 在views/创建组件 → 2. 在routes.js添加路由 → 3. 在导航栏添加菜单（如果需要） → 4. 测试访问 最后的小贴士 配置先行：先配好路由和权限，再开发页面 分模块开发：用户模块、题目模块、提交模块分开 权限细化：按钮级别也要控制权限 错误处理：404 页面、无权限提示要友好 记住这个开发流程，你就能搭建一个结构清晰、权限分明的校园 OJ 系统了！加油 💪\n","date":"2025-12-06T12:36:00+08:00","permalink":"https://ye-guan-xing.github.io/p/%E6%A0%A1%E5%9B%ADoj%E5%BC%80%E5%8F%91%E4%B9%8B%E5%B8%83%E5%B1%80%E9%80%BB%E8%BE%91/","title":"校园OJ开发之布局逻辑"},{"content":"手把手教你理解校园 OJ 登录系统：从输入密码到进入首页 一、前言：登录系统的“三件套” 想象一下你要进宿舍楼：\n门禁卡（token） - 证明你是楼里的学生 保安（路由守卫） - 检查你有没有门禁卡 楼长（Vuex） - 登记你的入住信息 我们的登录系统就是这样的三级验证机制！\n二、核心模块详解 1. Token 管理系统 - auth.js（你的电子门禁卡） 这个文件专门管理你的“门禁卡”（token）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 就像宿舍楼的门禁系统 const TOKEN_KEY = \u0026#34;oj_user_token\u0026#34;; // 门禁卡的编号 // 进门时刷卡（获取token） export function getToken() { return localStorage.getItem(TOKEN_KEY); } // 办新卡（设置token） export function setToken(token) { localStorage.setItem(TOKEN_KEY, token); } // 卡丢了/毕业了（移除token） export function removeToken() { localStorage.removeItem(TOKEN_KEY); } // 检查有没有卡（是否已登录） export function isAuthenticated() { return !!getToken(); // !!就是把任何值变成true/false的魔法 } 比喻理解：\nlocalStorage = 你的钱包，专门放重要卡片 TOKEN_KEY = 卡包里的特定卡槽，只放门禁卡 每次进出，保安只看这个卡槽有没有卡 2. 通信专员 - request.js（你的专属信使） 这个文件负责和后端服务器对话，就像你有个专属信使：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import axios from \u0026#34;axios\u0026#34;; // 信使工具包 import { getToken, removeToken } from \u0026#34;./auth\u0026#34;; // 带上/退回门禁卡 import { ElMessage } from \u0026#34;element-plus\u0026#34;; // 消息提示喇叭 // 招募一个专属信使 const request = axios.create({ baseURL: process.env.VUE_APP_BASE_API || \u0026#34;/api\u0026#34;, // 服务器地址 timeout: 10000, // 10秒没回复就算超时 }); // 【出发前的准备】请求拦截器 - 信使出发前做的事 request.interceptors.request.use((config) =\u0026gt; { const token = getToken(); // 从钱包拿出门禁卡 if (token) { config.headers[\u0026#34;Authorization\u0026#34;] = `Bearer ${token}`; // 把卡挂在脖子上 } return config; // 可以出发了！ }); // 【回来后的处理】响应拦截器 - 信使回来后做的事 request.interceptors.response.use( (response) =\u0026gt; { const res = response.data; // 打开信封 if (res.code === 1) { // 如果信上说\u0026#34;一切正常\u0026#34; return res.data; // 把真正的内容给你 } else { // 信上说\u0026#34;有问题\u0026#34; ElMessage.error(res.message || \u0026#34;请求失败\u0026#34;); // 用喇叭广播问题 throw new Error(res.message || \u0026#34;请求失败\u0026#34;); // 抛出问题 } }, (error) =\u0026gt; { // 信使自己出问题了（网络错误） const { status } = error.response || {}; switch (status) { case 401: // 门禁卡过期 ElMessage.error(\u0026#34;登录已过期，请重新登录\u0026#34;); removeToken(); // 扔掉过期卡 if (window.location.pathname !== \u0026#34;/login\u0026#34;) { window.location.href = \u0026#34;/login\u0026#34;; // 赶回登录处重新办卡 } break; case 403: // 权限不足 ElMessage.error(\u0026#34;没有权限访问\u0026#34;); break; default: ElMessage.error(\u0026#34;网络错误\u0026#34;); } throw error; // 把问题继续上报 } ); 信使工作流程图：\n1 2 3 你要发信 → 信使出发前（拦截器）→ 检查带没带门禁卡 → 出发送信 ↓ 服务器回信 → 信使回来后（拦截器）→ 检查信的内容 → 正常就给你，异常就广播 3. 用户服务接口 - user.js（能办的四件事） 这个文件定义了你能让信使办的四件事：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import request from \u0026#34;@/utils/request\u0026#34;; // 调用刚才的信使 // 1. 办门禁卡（登录） export function login(data) { return request({ url: \u0026#34;/user/login\u0026#34;, // 去\u0026#34;办卡处\u0026#34; method: \u0026#34;post\u0026#34;, // 用\u0026#34;申请\u0026#34;的方式 data, // 带上身份证明（账号密码） }); } // 2. 注册新身份（注册） export function register(data) { return request({ url: \u0026#34;/user/register\u0026#34;, // 去\u0026#34;登记处\u0026#34; method: \u0026#34;post\u0026#34;, data, }); } // 3. 更新个人信息（修改资料） export function updateUserInfo(data) { return request({ url: \u0026#34;/user/info\u0026#34;, // 去\u0026#34;资料修改处\u0026#34; method: \u0026#34;put\u0026#34;, // 用\u0026#34;修改\u0026#34;的方式 data, }); } // 4. 注销门禁卡（退出登录） export function logout() { return request({ url: \u0026#34;/user/logout\u0026#34;, // 去\u0026#34;注销处\u0026#34; method: \u0026#34;post\u0026#34;, }); } // 5. 查看个人档案（获取用户信息） export function getUserInfo() { return request({ url: `/user/stats`, // 去\u0026#34;档案室\u0026#34; method: \u0026#34;get\u0026#34;, }); } 通俗理解：\n每个export function就像一张办事指南 request()就是让信使按指南办事 返回值是一个Promise（承诺书），承诺会给你结果 4. 用户管理中心 - user.js (store/modules)（楼长的登记簿） 这里是 Vuex 的用户模块，负责管理所有用户相关的全局状态：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 // 【状态仓库】楼长手里的登记簿 const state = { token: getToken(), // 当前有效的门禁卡 userInfo: null, // 住户的详细信息 }; // 【修改规则】楼长能做的修改操作 const mutations = { SET_TOKEN: (state, token) =\u0026gt; { state.token = token; // 登记新卡 }, SET_USER_INFO: (state, userInfo) =\u0026gt; { state.userInfo = userInfo; // 登记住户信息 }, CLEAR_USER: (state) =\u0026gt; { state.token = \u0026#34;\u0026#34;; // 注销卡片 state.userInfo = null; // 清除住户信息 removeToken(); // 从钱包里也扔掉 }, }; // 【办事流程】楼长处理事务的流程 const actions = { // 办理入住（登录） login({ commit }, userInfo) { return new Promise((resolve, reject) =\u0026gt; { // 情况1：模拟模式（开发时用） if (useMock) { if (用户名密码正确) { commit(\u0026#34;SET_TOKEN\u0026#34;, \u0026#34;临时卡\u0026#34;); // 发临时卡 commit(\u0026#34;SET_USER_INFO\u0026#34;, { 角色: \u0026#34;管理员\u0026#34; }); // 登记信息 resolve(); // 办好了！ } else { reject(\u0026#34;密码错了\u0026#34;); // 办不了 } } // 情况2：真实模式（实际上线用） else { apiLogin(userInfo).then((响应) =\u0026gt; { if (响应.成功) { commit(\u0026#34;SET_TOKEN\u0026#34;, 响应.token); // 发正式卡 commit(\u0026#34;SET_USER_INFO\u0026#34;, 响应.用户信息); // 登记信息 resolve(); // 办好了！ } }); } }); }, // 查看住户档案（获取用户信息） getInfo({ commit, state }) { return new Promise((resolve, reject) =\u0026gt; { if (!state.token) { reject(\u0026#34;你没卡啊！\u0026#34;); // 没卡看什么档案 return; } // ...获取信息的逻辑 }); }, // 办理退宿（退出登录） logout({ commit }) { return new Promise((resolve, reject) =\u0026gt; { // 1. 通知服务器我要走了 apiLogout().then(() =\u0026gt; { // 2. 本地也清理 commit(\u0026#34;CLEAR_USER\u0026#34;); resolve(); }); }); }, }; Vuex 核心概念（必须理解！）：\nstate = 当前的状态（现在有什么） mutations = 能做什么修改（只能通过这里改！） actions = 做事的流程（可以包含异步操作） 修改状态的标准流程：\n1 你要改数据 → 调用action → action调用mutation → mutation修改state 5. 快捷访问门 - getters.js（快速查询通道） 这个文件提供快速查询状态的方法：\n1 2 3 4 5 const getters = { token: (state) =\u0026gt; state.user.token, // \u0026#34;给我当前的门禁卡\u0026#34; userInfo: (state) =\u0026gt; state.user.userInfo, // \u0026#34;给我住户信息\u0026#34; isLogin: (state) =\u0026gt; !!state.user.token, // \u0026#34;告诉我有没有卡\u0026#34;（!!转布尔值） }; 为什么需要 getters？\n就像图书馆的查询机，不用自己去书库翻 统一的查询接口，避免拼写错误 可以计算衍生数据（如isLogin） 6. Vuex 总仓库 - index.js（所有登记簿的集合） 这里是 Vuex 的入口文件，整合所有模块：\n1 2 3 4 5 6 7 8 9 10 11 12 import { createStore } from \u0026#34;vuex\u0026#34;; import getters from \u0026#34;./getters\u0026#34;; import user from \u0026#34;./modules/user\u0026#34;; export default createStore({ modules: { user, // 用户登记簿 // problem, 题目登记簿（可以后续添加） // submission, 提交记录登记簿 }, getters, // 快速查询通道 }); 模块化设计的好处：\n用户数据放user模块 题目数据放problem模块 提交记录放submission模块 互不干扰，清晰明了 7. 登录页面 - AppLogin.vue（前台接待处） 这是用户看到的登录界面：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 \u0026lt;template\u0026gt; \u0026lt;div class=\u0026#34;login-container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;login-card\u0026#34;\u0026gt; \u0026lt;!-- 标题区域 --\u0026gt; \u0026lt;div class=\u0026#34;login-header\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;OJ平台登录\u0026lt;/h2\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;!-- 表单区域 --\u0026gt; \u0026lt;el-form @submit.prevent=\u0026#34;handleLogin\u0026#34;\u0026gt; \u0026lt;el-form-item prop=\u0026#34;username\u0026#34;\u0026gt; \u0026lt;el-input v-model=\u0026#34;formData.username\u0026#34; placeholder=\u0026#34;用户名\u0026#34; /\u0026gt; \u0026lt;/el-form-item\u0026gt; \u0026lt;el-form-item prop=\u0026#34;password\u0026#34;\u0026gt; \u0026lt;el-input v-model=\u0026#34;formData.password\u0026#34; type=\u0026#34;password\u0026#34; /\u0026gt; \u0026lt;/el-form-item\u0026gt; \u0026lt;el-form-item\u0026gt; \u0026lt;el-button @click=\u0026#34;handleLogin\u0026#34;\u0026gt;登录\u0026lt;/el-button\u0026gt; \u0026lt;/el-form-item\u0026gt; \u0026lt;/el-form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script setup\u0026gt; import { ref, reactive } from \u0026#34;vue\u0026#34;; import { useRouter, useRoute } from \u0026#34;vue-router\u0026#34;; import { useStore } from \u0026#34;vuex\u0026#34;; import { ElMessage } from \u0026#34;element-plus\u0026#34;; // 获取工具 const router = useRouter(); // 导航员（负责跳转页面） const route = useRoute(); // 当前路线信息 const store = useStore(); // 楼长（Vuex） // 表单数据 const formData = reactive({ username: \u0026#34;\u0026#34;, password: \u0026#34;\u0026#34;, }); // 登录函数 const handleLogin = async () =\u0026gt; { try { // 1. 找楼长办卡（调用Vuex的login） await store.dispatch(\u0026#34;user/login\u0026#34;, { username: formData.username, password: formData.password, }); // 2. 办卡后登记详细信息（调用getInfo） const userInfo = await store.dispatch(\u0026#34;user/getInfo\u0026#34;); // 3. 欢迎入住！ ElMessage.success(`欢迎回来，${userInfo.username}！`); // 4. 导航员带你去想去的地方 const redirect = route.query.redirect || \u0026#34;/home\u0026#34;; router.push(redirect); } catch (error) { // 出错了，喇叭广播 ElMessage.error(error.message); } }; \u0026lt;/script\u0026gt; 页面组件关键点：\nuseRouter() - 获取导航员，用于页面跳转 useStore() - 获取楼长，用于状态管理 store.dispatch() - 让楼长办事（异步操作） router.push() - 让导航员带路 三、完整登录流程（故事版） 让我们跟着\u0026quot;小明\u0026quot;走一遍完整流程：\n第一幕：来到登录页面 小明打开浏览器，输入 OJ 平台网址，看到登录页面（AppLogin.vue）。\n第二幕：填写信息 小明输入：\n用户名：xiaoming 密码：123456 第三幕：点击登录按钮 1 2 3 4 5 // 发生的事： 1. 页面调用 handleLogin() 2. 找楼长（store）：\u0026#34;帮我办卡！\u0026#34; 3. 楼长查看登记簿（state），发现没卡 4. 楼长派信使（request）去服务器办卡 第四幕：信使出发 1 2 3 4 // request.js 的工作： 1. 信使出发前检查：\u0026#34;带门禁卡了吗？\u0026#34; → 没带（第一次登录） 2. 出发去服务器（baseURL + \u0026#34;/user/login\u0026#34;） 3. 带上小明的账号密码（data） 第五幕：服务器验证 1 2 3 4 5 // 服务器的工作： 1. 检查账号密码 2. 正确：生成一张门禁卡（token） 3. 返回：{ code: 1, data: { token: \u0026#34;abc123\u0026#34;, userInfo: {...} } } 4. 错误：返回 { code: 0, message: \u0026#34;密码错误\u0026#34; } 第六幕：信使归来 1 2 3 4 // request.js 的响应拦截器： 1. 打开信封看code 2. code=1：把data给页面 3. code=0：用喇叭广播\u0026#34;密码错误\u0026#34; 第七幕：楼长登记 1 2 3 4 5 // user.js (store) 的login action： 1. 收到信使带回的token 2. 调用SET_TOKEN登记到簿子 3. 调用setToken存到钱包（localStorage） 4. 调用SET_USER_INFO登记用户信息 第八幕：获取完整信息 1 2 3 4 5 // 登录后自动调用getInfo： 1. 楼长：\u0026#34;刚办了卡，现在去档案室拿详细资料\u0026#34; 2. 派信使去/user/stats 3. 拿回完整档案（roles、permissions等） 4. 登记到簿子（SET_USER_INFO） 第九幕：页面跳转 1 2 3 4 5 // AppLogin.vue 的最后： 1. 喇叭广播：\u0026#34;欢迎回来，小明！\u0026#34; 2. 导航员查看：\u0026#34;小明原来想去哪？\u0026#34; 3. 发现route.query.redirect是\u0026#34;/problems\u0026#34; 4. 带小明去题目列表页 第十幕：后续访问 小明第二天再访问：\n1 2 3 4 1. 进大门（打开网站） 2. 保安（路由守卫）拦截：\u0026#34;请出示门禁卡！\u0026#34; 3. 小明从钱包拿出卡（getToken()） 4. 保安验证有效，放行到首页 四、关键概念精讲 1. 什么是 Token？ 比喻：Token 就像酒店的房卡。\n办入住时（登录）给你一张 每次进房间（访问页面）要刷卡 退房时（退出登录）收回 过期了要重新办（token 过期） 2. localStorage vs sessionStorage 1 2 3 4 5 6 7 8 9 localStorage（长期钱包）： - 关浏览器还在 - 适合存token这种重要东西 - 容量大（5MB） sessionStorage（临时口袋）： - 关浏览器就丢 - 适合临时数据 - 同标签页共享 3. Promise 异步处理 比喻：叫外卖的过程\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 点外卖（发起请求） const 外卖 = 点餐(\u0026#34;鱼香肉丝\u0026#34;); // then：外卖到了做什么 外卖.then((餐) =\u0026gt; { 吃(餐); }); // catch：外卖出问题了 外卖.catch((错误) =\u0026gt; { 打电话投诉(错误); }); // async/await：优雅的等外卖 async function 吃饭() { try { const 餐 = await 点餐(\u0026#34;鱼香肉丝\u0026#34;); 吃(餐); } catch (错误) { 打电话投诉(错误); } } 4. Vuex 数据流 1 2 3 单向数据流（必须遵守！）： 组件 → dispatch Action → commit Mutation → 修改 State → 更新组件 （做什么事） （怎么改） （改哪里） （看到变化） 五、常见问题解答 Q1：为什么登录后页面刷新，又变未登录了？ 可能原因：\ntoken 没存进 localStorage（检查setToken） 刷新后 Vuex 的 state 重置了，但 token 还在 localStorage 需要在main.js或App.vue初始化时从 localStorage 读 token Q2：如何实现\u0026quot;记住我\u0026quot;功能？ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // auth.js 增加 export function setToken(token, remember = false) { if (remember) { localStorage.setItem(TOKEN_KEY, token); // 长期存储 } else { sessionStorage.setItem(TOKEN_KEY, token); // 会话存储 } } // 登录时 await store.dispatch(\u0026#34;user/login\u0026#34;, { username, password, remember: true, // 传这个参数 }); Q3：多个标签页同时登录怎么办？ 1 2 3 4 5 6 7 // 监听storage变化 window.addEventListener(\u0026#34;storage\u0026#34;, (event) =\u0026gt; { if (event.key === TOKEN_KEY) { // token变了，同步更新Vuex store.commit(\u0026#34;user/SET_TOKEN\u0026#34;, event.newValue); } }); Q4：如何防止 XSS 攻击盗取 token？ 安全措施：\ntoken 设置合理过期时间 使用 HttpOnly Cookie（后端设置） 重要操作需要二次验证 定期更换 token 六、登录系统架构总结 1 2 3 4 5 6 7 8 【三层架构】 1. 展示层（View） - AppLogin.vue ↓ 用户交互 2. 逻辑层（ViewModel） - Vuex (user模块) ↓ 状态管理 3. 服务层（Service） - request.js + user.js(api) ↓ 网络通信 4. 持久层（Persistence） - localStorage + auth.js 1 2 3 4 5 6 【五大模块】 1. 认证模块（auth.js）- 管卡 2. 通信模块（request.js）- 管信使 3. 接口模块（user.js api）- 管能办什么事 4. 状态模块（user.js store）- 管登记簿 5. 界面模块（AppLogin.vue）- 管用户看到什么 七、给新手的黄金法则 法则 1：先理解流程，再写代码 画个流程图，搞清楚\u0026quot;点击登录\u0026quot;到\u0026quot;进入首页\u0026quot;中间发生了什么。\n法则 2：分层思考，各司其职 auth.js：只关心 token 存哪、怎么取 request.js：只关心怎么发请求、怎么处理响应 store：只关心数据怎么存、怎么改 组件：只关心用户怎么交互、数据怎么展示 法则 3：错误处理要全面 每个可能出错的地方都要有应对方案：\n网络错误 服务器错误 用户输入错误 token 过期错误 法则 4：用户体验要友好 登录中显示 loading 错误提示要明确 成功后有反馈 记住用户上次操作 八、下一步学习方向 掌握了登录系统后，你可以继续学习：\n权限管理：不同角色看到不同菜单 路由守卫：更精细的访问控制 第三方登录：微信、QQ 快速登录 双因素认证：密码+手机验证码 单点登录：一个账号通行所有系统 记住，登录系统是 Web 应用的大门，大门设计得好不好，直接影响用户体验和系统安全。现在你不仅知道怎么用，还知道为什么这样设计，这就是成为高级开发者的第一步！\n加油，你已经掌握了现代前端登录系统的核心原理！🚀\n","date":"2025-12-05T18:36:00+08:00","permalink":"https://ye-guan-xing.github.io/p/%E6%A0%A1%E5%9B%ADoj%E9%A1%B9%E7%9B%AE%E7%9A%84%E8%AF%A6%E7%BB%86%E5%BC%80%E5%8F%91%E7%99%BB%E5%BD%95%E7%95%8C%E9%9D%A2/","title":"校园OJ项目的详细开发(登录界面)"},{"content":"引言（为什么写这个?） 在完成全域生活服务平台后，我对前后端协同开发的流程有了更深入的理解。但一直想挑战一个更贴近编程学习场景的项目——在线判题系统（OJ，Online Judge）。这类平台不仅需要清晰的用户交互设计，还涉及代码提交、实时判题、数据统计等复杂业务逻辑，非常适合用来巩固前端工程化思想。\n本次分享将聚焦前端开发细节，包括项目架构设计、核心功能实现及业务流程梳理。后端部分由好友 shuimo 负责搭建，后续若有机会会邀请他补充后端架构细节。\n先放一张完整的项目架构图，帮助大家建立整体认知：\n下面是校园 OJ 平台前端项目的完整目录结构，这个结构体现了现代 Vue 项目的模块化设计思想：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 src ├─ api # 接口请求层：封装所有后端接口调用 │ ├─ admin.js # 管理员相关接口（题目管理、权限操作等） │ ├─ problem.js # 题目相关接口（题目列表、详情、提交判题等） │ ├─ submission.js # 提交记录相关接口（提交列表、判题结果查询等） │ └─ user.js # 用户相关接口（登录、注册、个人信息、设置等） ├─ assets # 静态资源层：存放样式、图片等静态文件 │ ├─ css # 通用CSS文件（未拆分到styles的基础样式） │ ├─ images # 项目图片资源（图标、背景图等） │ └─ styles # 核心样式文件 │ ├─ Applogin.css # 登录/注册页面专属样式 │ ├─ AppNavbar.css # 导航栏组件专属样式 │ ├─ common.css # 全局通用样式（重置样式、公共类名等） │ └─ variables.scss # SCSS全局变量（颜色、字体、间距等） ├─ components # 通用组件层：可复用的Vue组件（按业务拆分） │ ├─ admin # 管理员专属组件 │ │ ├─ ProblemForm.vue # 题目新增/编辑表单组件 │ │ └─ ProblemManagement.vue # 题目列表管理组件（查询、删除、编辑） │ ├─ common # 全局通用组件 │ │ ├─ AppFooter.vue # 页面底部版权/导航组件 │ │ └─ AppNavbar.vue # 页面顶部导航栏（含登录态、菜单切换） │ ├─ problem # 题目相关组件 │ │ ├─ ProblemDetail.vue # 题目详情展示组件（题干、输入输出示例等） │ │ ├─ ProblemItem.vue # 题目列表项组件（单个题目卡片） │ │ └─ ProblemTags.vue # 题目标签组件（难度、题型等标签展示） │ └─ submission # 提交记录相关组件 │ ├─ CodeEditor.vue # 代码编辑组件（支持语法高亮、代码提交） │ ├─ JudgeResult.vue # 判题结果展示组件（AC/WA/RE等状态+详情） │ ├─ SubmissionItem.vue # 提交记录列表项组件（单个提交记录展示） │ └─ SubmissionList.vue # 提交记录列表组件（查询、筛选提交记录） ├─ layouts # 布局组件层：页面整体布局容器 │ └─ MainLayout.vue # 主布局组件（包含Navbar+Footer+内容区域） ├─ router # 路由配置层：前端路由管理 │ ├─ index.js # 路由入口（创建路由实例、挂载路由守卫等） │ └─ routes.js # 路由规则定义（所有页面的路由路径、组件映射） ├─ store # 状态管理层：Vuex全局状态管理（按模块拆分） │ ├─ modules # Vuex模块拆分 │ │ ├─ problem.js # 题目相关状态（题目列表、当前题目详情等） │ │ ├─ submission.js # 提交相关状态（提交记录、判题结果等） │ │ └─ user.js # 用户相关状态（登录态、用户信息、权限等） │ ├─ getters.js # Vuex全局getters（统一获取各模块状态） │ └─ index.js # Vuex入口（创建store实例、注册模块等） ├─ utils # 工具函数层：通用工具方法封装 │ ├─ auth.js # 权限相关工具（JWT存储/解析、登录态校验等） │ ├─ constant.js # 全局常量（接口地址、状态码、枚举值等） │ ├─ debounce.js # 防抖函数（防止重复点击、频繁请求） │ ├─ format.js # 格式化工具（时间、判题状态、文件大小等） │ ├─ mockData.js # 模拟数据（开发阶段替代后端接口） │ └─ request.js # 请求封装（Axios拦截器、请求/响应统一处理） ├─ views # 页面视图层：对应路由的页面级组件 │ ├─ admin # 管理员页面 │ │ ├─ ProblemEdit.vue # 题目编辑页面（复用ProblemForm组件） │ │ └─ ProblemManagement.vue # 题目管理页面（复用ProblemManagement组件） │ ├─ login # 登录/注册页面 │ │ ├─ AppLogin.vue # 登录页面（账号密码登录、验证码等） │ │ └─ AppRegister.vue # 注册页面（用户信息填写、校验等） │ ├─ problem # 题目相关页面 │ │ ├─ ProblemDetailView.vue # 题目详情页面（基于MainLayout+ProblemDetail） │ │ └─ ProblemListView.vue # 题目列表页面（基于MainLayout+ProblemItem） │ ├─ submission # 提交记录页面 │ │ └─ SubmissionHistory.vue # 提交历史页面（基于MainLayout+SubmissionList） │ ├─ user # 用户中心页面 │ │ ├─ UserProfile.vue # 个人资料页面（展示/编辑用户信息） │ │ └─ UserSetting.vue # 用户设置页面（密码修改、偏好设置等） │ ├─ AppHome.vue # 项目首页（展示平台介绍、热门题目等） │ └─ NotFound.vue # 404页面（路由匹配失败时展示） ├─ App.vue # 根组件（挂载路由出口、全局样式引入） └─ main.js # 项目入口文件（创建Vue实例、挂载路由/store等） 这个架构清晰地将项目分为 10 个核心模块，每个模块都有明确的职责边界，确保了代码的可维护性和可扩展性。\n技术栈解析 核心技术框架 Vue 3 - 渐进式 JavaScript 框架，负责整个应用的核心逻辑和 UI 渲染 Vue Router - 官方路由管理器，处理页面导航和权限控制 Vuex - 状态管理模式，管理全局共享数据 Element Plus - UI 组件库，提供丰富的预制界面元素 开发工具链 Vue CLI - 项目脚手架，快速初始化标准化项目结构 npm - 包管理器，管理项目依赖和构建脚本 SCSS/Sass - CSS 预处理器，提供变量、嵌套等高级功能 ESLint - 代码检查工具，保证代码质量和风格统一 Babel - JavaScript 编译器，确保代码兼容性 辅助工具库 Axios - HTTP 客户端，处理网络请求和数据交互 Vue I18n (可选) - 国际化支持 Vite (可选) - 下一代前端构建工具，提供更快开发体验 各模块深度解析 api/ - 网络通信枢纽 这个模块是前端与后端之间的通信桥梁。它将 HTTP 请求封装成语义化的函数，让业务代码无需关心底层网络细节。\nadmin.js - 管理员专属接口网关，处理题目管理、权限控制等高阶操作 problem.js - 题目数据接口，提供题目列表查询、详情获取等服务 submission.js - 提交记录服务，处理代码提交、评测结果查询 users.js - 用户身份服务，管理登录、注册和个人信息 设计理念：每个文件对应一个后端服务领域，通过统一的请求拦截器处理认证、错误等通用逻辑。\nassets/ - 静态资源仓库 这里是项目的视觉资产库，存放所有非代码资源。\ncss/login.css - 登录页专属样式，实现特殊视觉效果 images/ - 图片资源中心，管理 logo、图标、背景图等 styles/variables.scss - 设计令牌系统，定义颜色、间距、字体等设计常量 styles/common.scss - 全局样式基础，包含重置样式、工具类、布局框架 设计理念：通过 CSS 变量和混合宏实现主题可配置性，支持白天/黑夜模式切换。\ncomponents/ - 可复用组件工厂 这个模块采用乐高积木式的设计思想，每个组件都是独立、可复用的 UI 单元。\ncommon/ - 基础设施组件\nAppNavbar：全局导航系统，根据用户权限动态显示菜单 AppFooter：版权信息展示区，包含网站信息和外部链接 problem/ - 题目相关组件套件\nProblemDetail：题目展示器，渲染题目描述、示例和难度信息 ProblemItem：题目卡片，在列表中展示题目的核心信息 ProblemTags：标签管理系统，支持标签展示和筛选 admin/ - 管理后台组件集\nProblemForm：题目编辑器，提供富文本编辑和测试用例配置 ProblemManagement：题目控制面板，支持批量操作和高级搜索 submission/ - 代码提交组件包\nCodeEditor：代码编辑器，提供语法高亮和智能提示 SubmissionItem：提交记录卡片，显示评测状态和时间消耗 SubmissionList：提交历史视图，支持分页和过滤 设计理念：组件遵循单一职责原则，通过 Props 和 Events 与父组件通信，支持深度定制。\nlayouts/ - 页面布局框架 MainLayout.vue 是应用的骨架系统，定义了页面的基本结构：\n顶部：导航栏区域 中部：动态内容区域 底部：页脚信息区域 这个布局被大多数页面复用，确保用户体验的一致性。\nrouter/ - 应用导航系统 路由模块是应用的GPS 导航系统，管理着所有页面的访问路径。\nroutes.js - 路线图数据库，定义了 URL 到页面的映射关系 index.js - 导航控制中心，包含权限守卫和路由钩子 权限分级：\n公共路由：无需登录即可访问（首页、题目列表） 用户路由：需要登录凭证（提交记录、个人中心） 管理员路由：需要管理员权限（题目管理、用户管理） store/ - 全局状态管理中心 这里是应用的中央数据中心，采用模块化设计管理全局状态。\nmodules/user.js - 用户状态库，管理登录态、权限信息和个人数据 modules/problem.js - 题目状态库，缓存题目列表和详情数据 modules/submission.js - 提交状态库，跟踪提交历史和评测结果 getters.js - 数据查询接口，提供便捷的状态访问方法 index.js - 状态库入口，集成所有模块并提供插件支持 数据流：组件 → Actions → Mutations → State → 组件\nutils/ - 工具函数库 工具库是项目的瑞士军刀，提供各种通用功能。\nrequest.js - HTTP 请求引擎，封装 Axios 并提供拦截器 auth.js - 身份验证工具，管理 Token 的存储和验证 constant.js - 常量字典，集中管理接口地址、状态码等 format.js - 数据格式化器，统一日期、数字等展示格式 mockData.js - 模拟数据生成器，支持离线开发和测试 views/ - 页面视图层 视图层是用户直接交互的界面，每个页面都是路由的终点。\nlogin/ - 身份验证门户\nAppLogin.vue：登录注册一体化页面，支持第三方登录 problem/ - 题目浏览区\nProblemList.vue：题目目录页，支持搜索和筛选 ProblemDetailView.vue：题目详情页，集成代码编辑器 submission/ - 提交记录区\nSubmissionHistory.vue：个人提交历史，支持状态过滤 admin/ - 管理控制台\nProblemManagement.vue：题目管理面板 ProblemEdit.vue：题目编辑界面 user/ - 个人空间\nAppHome.vue：个人主页，展示统计信息和活动记录 NotFound.vue - 404 错误页面，提供友好的错误提示\n核心入口文件 App.vue - 应用根容器，定义全局样式和基础结构 main.js - 应用启动器，集成所有插件并挂载到 DOM ","date":"2025-12-05T17:36:00+08:00","permalink":"https://ye-guan-xing.github.io/p/%E6%A0%A1%E5%9B%ADoj%E9%A1%B9%E7%9B%AE%E7%9A%84%E6%95%B4%E4%BD%93%E7%BB%93%E6%9E%84/","title":"校园OJ项目的整体结构"},{"content":"由于没有后端，我来为你添加前端模拟数据和模拟 API，并解释为什么不需要处理 token。\n1. 创建模拟数据存储 首先在 src/utils/ 下创建模拟数据管理工具：\nsrc/utils/mockData.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 // 模拟数据存储 class MockStorage { constructor() { this.storageKey = \u0026#34;oj_mock_data\u0026#34;; this.initData(); } initData() { if (!localStorage.getItem(this.storageKey)) { const initialData = { problems: [ { id: 1, title: \u0026#34;两数之和\u0026#34;, label: \u0026#34;数组,哈希表,简单\u0026#34;, testPointNum: 3, description: \u0026#34;给定一个整数数组 nums 和一个目标值 target，请你在该数组中找出和为目标值的那两个整数，并返回它们的数组下标。\\n\\n你可以假设每种输入只会对应一个答案。但是，数组中同一个元素不能使用两遍。\u0026#34;, testPointList: [ { input: \u0026#34;3\\n2 7 11 15\\n9\u0026#34;, output: \u0026#34;0 1\u0026#34;, isSample: 1 }, { input: \u0026#34;2\\n3 2 4\\n6\u0026#34;, output: \u0026#34;1 2\u0026#34;, isSample: 0 }, { input: \u0026#34;2\\n3 3\\n6\u0026#34;, output: \u0026#34;0 1\u0026#34;, isSample: 0 }, ], createTime: \u0026#34;2024-01-15 10:00:00\u0026#34;, }, { id: 2, title: \u0026#34;验证回文串\u0026#34;, label: \u0026#34;字符串,双指针,简单\u0026#34;, testPointNum: 6, description: \u0026#34;给定一个字符串，验证它是否是回文串，只考虑字母和数字字符，可以忽略字母的大小写。\\n\\n说明：本题中，我们将空字符串定义为有效的回文串。\u0026#34;, testPointList: [ { input: \u0026#39;\u0026#34;A man, a plan, a canal: Panama\u0026#34;\u0026#39;, output: \u0026#34;true\u0026#34;, isSample: 1, }, { input: \u0026#39;\u0026#34;race a car\u0026#34;\u0026#39;, output: \u0026#34;false\u0026#34;, isSample: 1 }, { input: \u0026#39;\u0026#34;\u0026#34;\u0026#39;, output: \u0026#34;true\u0026#34;, isSample: 0 }, ], createTime: \u0026#34;2024-01-16 14:30:00\u0026#34;, }, ], nextProblemId: 3, }; this.saveData(initialData); } } getData() { return JSON.parse(localStorage.getItem(this.storageKey)) || {}; } saveData(data) { localStorage.setItem(this.storageKey, JSON.stringify(data)); } // 获取所有题目 getProblems() { return this.getData().problems || []; } // 添加题目 addProblem(problem) { const data = this.getData(); const newProblem = { ...problem, id: data.nextProblemId++, createTime: new Date().toLocaleString(\u0026#34;zh-CN\u0026#34;), }; data.problems.push(newProblem); this.saveData(data); return newProblem; } // 更新题目 updateProblem(updatedProblem) { const data = this.getData(); const index = data.problems.findIndex((p) =\u0026gt; p.id === updatedProblem.id); if (index !== -1) { data.problems[index] = { ...data.problems[index], ...updatedProblem, // 保留创建时间 createTime: data.problems[index].createTime, }; this.saveData(data); return data.problems[index]; } return null; } // 删除题目 deleteProblem(id) { const data = this.getData(); data.problems = data.problems.filter((p) =\u0026gt; p.id !== id); this.saveData(data); return true; } // 分页查询 getProblemsByPage(pageNum = 1, size = 10) { const problems = this.getProblems(); const start = (pageNum - 1) * size; const end = start + size; const pageData = problems.slice(start, end); return { records: pageData, total: problems.length, pageNum, size, }; } // 根据ID查询 getProblemById(id) { const problems = this.getProblems(); return problems.find((p) =\u0026gt; p.id === Number(id)); } } // 创建单例实例 export const mockStorage = new MockStorage(); 2. 修改 API 文件支持模拟数据 src/api/admin.js (更新版本) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 import request from \u0026#34;@/utils/request\u0026#34;; import { mockStorage } from \u0026#34;@/utils/mockData\u0026#34;; // 是否使用模拟数据 const USE_MOCK = true; // 模拟延迟 const mockDelay = (ms = 500) =\u0026gt; new Promise((resolve) =\u0026gt; setTimeout(resolve, ms)); // 题目管理接口 export const problemAdminAPI = { // 添加题目 addProblem: async (data) =\u0026gt; { if (USE_MOCK) { await mockDelay(); const result = mockStorage.addProblem(data); return { data: result, code: 200, message: \u0026#34;添加成功\u0026#34; }; } return request({ url: \u0026#34;/testQuestion/addTestQuestion\u0026#34;, method: \u0026#34;POST\u0026#34;, data, }); }, // 删除题目 deleteProblem: async (id) =\u0026gt; { if (USE_MOCK) { await mockDelay(); const success = mockStorage.deleteProblem(id); return { data: null, code: success ? 200 : 400, message: success ? \u0026#34;删除成功\u0026#34; : \u0026#34;删除失败\u0026#34;, }; } return request({ url: \u0026#34;/testQuestion/deleteTestQuestionById\u0026#34;, method: \u0026#34;DELETE\u0026#34;, data: { id }, }); }, // 更新题目 updateProblem: async (data) =\u0026gt; { if (USE_MOCK) { await mockDelay(); const result = mockStorage.updateProblem(data); if (result) { return { data: result, code: 200, message: \u0026#34;更新成功\u0026#34; }; } else { return { data: null, code: 400, message: \u0026#34;题目不存在\u0026#34; }; } } return request({ url: \u0026#34;/testQuestion/updateTestQuestion\u0026#34;, method: \u0026#34;POST\u0026#34;, data, }); }, // 题目分页查询 getProblemsByPage: async (params) =\u0026gt; { if (USE_MOCK) { await mockDelay(300); const { pageNum = 1, size = 10, keyword } = params; let problems = mockStorage.getProblems(); // 模拟搜索功能 if (keyword) { problems = problems.filter( (p) =\u0026gt; p.title.toLowerCase().includes(keyword.toLowerCase()) || p.label.toLowerCase().includes(keyword.toLowerCase()) ); } const start = (pageNum - 1) * size; const end = start + size; const pageData = problems.slice(start, end); return { data: { records: pageData, total: problems.length, pageNum, size, }, code: 200, message: \u0026#34;查询成功\u0026#34;, }; } return request({ url: \u0026#34;/testQuestion/getTestQuestionByPage\u0026#34;, method: \u0026#34;GET\u0026#34;, params, }); }, // 根据ID查询题目 getProblemById: async (id) =\u0026gt; { if (USE_MOCK) { await mockDelay(); const problem = mockStorage.getProblemById(id); if (problem) { return { data: problem, code: 200, message: \u0026#34;查询成功\u0026#34; }; } else { return { data: null, code: 404, message: \u0026#34;题目不存在\u0026#34; }; } } return request({ url: \u0026#34;/testQuestion/getTestQuestionById\u0026#34;, method: \u0026#34;GET\u0026#34;, params: { id }, }); }, }; 3. 更新组件处理模拟数据响应 src/components/admin/ProblemManagement.vue (关键部分更新) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 // 在 fetchProblems 方法中更新数据处理： const fetchProblems = async () =\u0026gt; { loading.value = true; try { const params = { pageNum: pagination.value.pageNum, size: pagination.value.size, }; if (searchKeyword.value) { params.keyword = searchKeyword.value; } const response = await problemAdminAPI.getProblemsByPage(params); // 处理模拟数据和真实数据的响应格式差异 if (USE_MOCK) { problemList.value = response.data.records || []; pagination.value.total = response.data.total || 0; } else { problemList.value = response.data?.records || []; pagination.value.total = response.data?.total || 0; } } catch (error) { ElMessage.error(\u0026#34;获取题目列表失败\u0026#34;); console.error(\u0026#34;获取题目列表失败:\u0026#34;, error); } finally { loading.value = false; } }; // 在 handleDelete 方法中： const handleDelete = async (id) =\u0026gt; { try { await ElMessageBox.confirm(\u0026#34;确定要删除这个题目吗？\u0026#34;, \u0026#34;警告\u0026#34;, { type: \u0026#34;warning\u0026#34;, confirmButtonText: \u0026#34;确定\u0026#34;, cancelButtonText: \u0026#34;取消\u0026#34;, }); const response = await problemAdminAPI.deleteProblem(id); // 检查响应是否成功 if (response.code === 200) { ElMessage.success(\u0026#34;删除成功\u0026#34;); fetchProblems(); } else { ElMessage.error(response.message || \u0026#34;删除失败\u0026#34;); } } catch (error) { if (error !== \u0026#34;cancel\u0026#34;) { ElMessage.error(\u0026#34;删除失败\u0026#34;); console.error(\u0026#34;删除失败:\u0026#34;, error); } } }; src/components/admin/ProblemForm.vue (关键部分更新) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 // 在 handleSubmit 方法中更新： const handleSubmit = async () =\u0026gt; { try { await formRef.value.validate(); submitting.value = true; // 更新测试点数量 form.testPointNum = form.testPointList.length; const response = isEdit.value ? await problemAdminAPI.updateProblem(form) : await problemAdminAPI.addProblem(form); // 检查响应是否成功 if (response.code === 200) { ElMessage.success(isEdit.value ? \u0026#34;更新成功\u0026#34; : \u0026#34;创建成功\u0026#34;); router.push(\u0026#34;/admin/problem\u0026#34;); } else { ElMessage.error( response.message || (isEdit.value ? \u0026#34;更新失败\u0026#34; : \u0026#34;创建失败\u0026#34;) ); } } catch (error) { if (error instanceof Error) { ElMessage.error(isEdit.value ? \u0026#34;更新失败\u0026#34; : \u0026#34;创建失败\u0026#34;); console.error(\u0026#34;提交失败:\u0026#34;, error); } // 如果是验证失败，不显示错误消息 } finally { submitting.value = false; } }; // 在 fetchProblemDetail 方法中更新： const fetchProblemDetail = async (id) =\u0026gt; { try { const response = await problemAdminAPI.getProblemById(id); if (response.code === 200 \u0026amp;\u0026amp; response.data) { Object.assign(form, response.data); } else { ElMessage.error(response.message || \u0026#34;获取题目详情失败\u0026#34;); } } catch (error) { ElMessage.error(\u0026#34;获取题目详情失败\u0026#34;); console.error(\u0026#34;获取题目详情失败:\u0026#34;, error); } }; 4. 为什么不需要处理 Token 原因分析： 模拟环境特性：\n在模拟环境中，所有数据都存储在前端（localStorage） 不需要与真实服务器通信，因此不需要身份验证 数据操作完全在前端完成，没有跨域或安全限制 开发阶段便利性：\n简化开发流程，专注于功能实现 避免因认证问题阻碍开发进度 便于测试和演示 架构设计考虑：\n保持 API 接口的一致性 真实环境切换简单（只需修改 USE_MOCK 常量） 模拟数据层与业务逻辑分离 真实环境切换： 当需要连接真实后端时，只需：\n将 src/api/admin.js 中的 USE_MOCK 改为 false 确保 src/utils/request.js 中的 token 处理正常工作 配置正确的后端 API 地址 Token 处理的预备工作： 虽然在模拟环境中不需要 token，但你的项目已经具备了 token 处理能力：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 在 src/utils/request.js 中已经有类似的代码： import store from \u0026#34;@/store\u0026#34;; // 请求拦截器 - 添加 token service.interceptors.request.use( (config) =\u0026gt; { if (store.getters.token) { config.headers[\u0026#34;Authorization\u0026#34;] = `Bearer ${store.getters.token}`; } return config; }, (error) =\u0026gt; { return Promise.reject(error); } ); 这样设计确保了：\n开发阶段：快速原型开发，无需后端依赖 生产环境：无缝切换到真实 API，自动处理认证 代码质量：保持接口一致性，便于维护 现在你的题目管理功能已经完全可以在前端独立运行，所有数据都会持久化保存在浏览器的 localStorage 中！\n","date":"2025-11-30T12:36:00Z","permalink":"https://ye-guan-xing.github.io/p/%E6%A0%A1%E5%9B%ADoj%E5%BC%80%E5%8F%91%E4%B9%8Badmin%E9%A1%B5%E9%9D%A2%E7%9A%84%E5%BC%80%E5%8F%91/","title":"校园OJ开发之admin页面的开发"},{"content":"前言 在前后端分离开发模式下，跨域是前端开发者绕不开的问题。浏览器的同源策略限制了不同域名/端口间的资源请求，而解决跨域的核心方案主要有两种：CORS（跨域资源共享） 和反向代理。本文结合全域生活平台的示例，详解这两种方案的原理、应用场景及踩坑要点。\n注：后文会详细讲解 Nginx 反向代理（这是前端程序员的必备知识） 一、问题背景：为什么会跨域？ 先看我们的项目环境：\n前端（Vue）运行在 http://localhost:8081（通过vue.config.js配置）； 后端（Express）运行在 http://localhost:8080（接口路径如/api/merchant）。 浏览器的同源策略要求“协议+域名+端口”完全一致，因此前端直接请求后端接口会触发跨域错误——比如控制台提示No 'Access-Control-Allow-Origin' header is present on the requested resource。\n二、CORS：后端主导的跨域解决方案 CORS 是 W3C 标准，通过后端设置响应头允许指定源的跨域请求，是最直接的跨域方案之一。以下是两种常见的 CORS 配置方式：\n1. 方案 1：使用cors中间件（便捷高效） Express 生态提供了cors包，可一键配置跨域规则，也是项目中最常用的方式：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // 使用cors中间件的Express代码 const express = require(\u0026#34;express\u0026#34;); const cors = require(\u0026#34;cors\u0026#34;); const app = express(); app.use(express.json()); // 全局启用cors，默认允许所有源（生产环境建议指定具体源） app.use( cors({ origin: \u0026#34;http://localhost:8081\u0026#34;, // 仅允许前端8081端口访问 methods: [\u0026#34;GET\u0026#34;, \u0026#34;POST\u0026#34;, \u0026#34;PUT\u0026#34;, \u0026#34;DELETE\u0026#34;, \u0026#34;OPTIONS\u0026#34;], // 允许的请求方法 allowedHeaders: [\u0026#34;Content-Type\u0026#34;, \u0026#34;Authorization\u0026#34;], // 允许的请求头 }) ); // 接口路由 const merchantRouder = require(\u0026#34;./routes/merchant\u0026#34;); const customerRouder = require(\u0026#34;./routes/service\u0026#34;); const orderRouder = require(\u0026#34;./routes/order\u0026#34;); app.use(\u0026#34;/api/merchant\u0026#34;, merchantRouder); app.use(\u0026#34;/api/service\u0026#34;, customerRouder); app.use(\u0026#34;/api/order\u0026#34;, orderRouder); app.listen(8080, () =\u0026gt; { console.log(\u0026#34;服务器启动成功！\u0026#34;); console.log(\u0026#34;http://localhost:8080\u0026#34;); }); 这种方式无需手动处理预检请求，cors包会自动拦截OPTIONS请求并返回正确响应头。\n2. 方案 2：手动配置 CORS（深入原理） 若不想依赖第三方包，可手动设置响应头并处理预检请求（适合理解 CORS 底层逻辑）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 // 手动处理CORS的Express代码 const express = require(\u0026#34;express\u0026#34;); const app = express(); app.use(express.json()); // 自定义跨域中间件 app.use((req, res, next) =\u0026gt; { // 允许前端8081端口访问 res.setHeader(\u0026#34;Access-Control-Allow-Origin\u0026#34;, \u0026#34;http://localhost:8081\u0026#34;); // 允许的请求方法（覆盖所有常用HTTP方法） res.setHeader( \u0026#34;Access-Control-Allow-Methods\u0026#34;, \u0026#34;GET, POST, PUT, DELETE, OPTIONS\u0026#34; ); // 允许的请求头（支持JSON、Token等） res.setHeader(\u0026#34;Access-Control-Allow-Headers\u0026#34;, \u0026#34;Content-Type, Authorization\u0026#34;); // 处理OPTIONS预检请求（非简单请求必加） if (req.method === \u0026#34;OPTIONS\u0026#34;) { return res.sendStatus(200); // 预检请求直接返回200，不进入业务路由 } next(); // 非预检请求，继续执行后续中间件/路由 }); // 接口路由不变 app.use(\u0026#34;/api/merchant\u0026#34;, require(\u0026#34;./routes/merchant\u0026#34;)); app.use(\u0026#34;/api/service\u0026#34;, require(\u0026#34;./routes/service\u0026#34;)); app.use(\u0026#34;/api/order\u0026#34;, require(\u0026#34;./routes/order\u0026#34;)); app.listen(8080, () =\u0026gt; { console.log(\u0026#34;服务器启动成功！http://localhost:8080\u0026#34;); }); 3. CORS 的关键细节 预检请求（OPTIONS）：前端发送PUT/DELETE或带自定义头的请求时，浏览器会先发OPTIONS请求“探路”，后端必须正确响应才能触发真实请求； Access-Control-Allow-Origin：生产环境需指定具体域名（如https://your-domain.com），而非通配符*（否则前端无法携带 Cookie/Token）； 简单请求豁免预检：GET/POST/HEAD请求且请求头仅含Accept/Content-Type等基础字段时，无需预检。 三、devServer 反向代理：前端开发环境的跨域捷径 虽然 CORS 能解决跨域，但开发阶段频繁修改后端配置效率低。Vue CLI 的devServer.proxy通过反向代理绕过浏览器跨域限制，是更优雅的开发方案。\n1. 反向代理的工作原理 devServer.proxy让前端开发服务器充当“中间人”：\n前端请求同源的开发服务器（如http://localhost:8081/api/merchant）； 开发服务器将请求转发给后端（http://localhost:8080/api/merchant）； 后端响应结果由开发服务器回传给前端。 整个过程中浏览器仅与同源的localhost:8081交互，自然不会触发跨域限制。\n2. 前端配置源码解析（vue.config.js） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // web-admin/vue.config.js module.exports = { devServer: { port: 8081, // 前端开发端口（避开后端8080端口冲突） open: true, // 启动后自动打开浏览器 proxy: { \u0026#34;/api\u0026#34;: { target: \u0026#34;http://localhost:8080\u0026#34;, // 后端接口真实地址 changeOrigin: true, // 模拟后端服务器的Origin（关键！避免后端跨域拦截） // pathRewrite: { \u0026#34;^/api\u0026#34;: \u0026#34;\u0026#34; }, // 后端接口无/api前缀时启用（如后端是/merchant） }, }, }, }; 3. 关键配置说明 target：后端接口的根地址； changeOrigin: true：让后端误以为请求来自localhost:8080（同源），避免后端的跨域拦截逻辑； pathRewrite：示例中注释掉是因为后端接口带/api前缀（如/api/merchant），若后端接口无/api，需通过此配置去掉前缀。 4. 踩坑实录：为什么去掉 CORS 仍报错？ 很多开发者疑惑：“明明配了devServer.proxy，为什么后端去掉 CORS 还是报跨域错？”\n核心原因是非简单请求的OPTIONS预检请求会被代理转发到后端，而后端未处理预检——哪怕前端用了代理，浏览器收到后端非法的预检响应后，仍会判定跨域失败。\n（1）预检请求的“转发陷阱” 前端devServer.proxy会转发所有请求（包括OPTIONS预检请求）到后端。比如前端发送POST /api/merchant（带 JSON 数据，属于非简单请求）：\n浏览器先发送OPTIONS /api/merchant到前端开发服务器（localhost:8081）； 开发服务器把这个OPTIONS请求转发到后端（localhost:8080/api/merchant）； 若后端没配置 CORS、也没手动处理OPTIONS，会返回无跨域响应头的默认响应（甚至 404）； 浏览器收到这个非法响应，直接抛出跨域错误，真实的POST请求根本不会发送。 你的后端代码（去掉cors且无预检处理）：\n1 2 3 4 // 后端未处理OPTIONS预检请求 app.use(express.json()); // 无CORS配置，也无OPTIONS处理逻辑 app.use(\u0026#34;/api/merchant\u0026#34;, merchantRouder); 此时后端对OPTIONS请求的响应里没有Access-Control-Allow-Origin等头，浏览器自然判定跨域失败。\n（2）开发环境最优解：保留代理+后端极简预检处理 所谓“后端无需配置 CORS”，并非完全不用处理跨域，而是不用依赖cors包，但需手动处理预检请求（仅需几行代码）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const express = require(\u0026#34;express\u0026#34;); const app = express(); app.use(express.json()); // 极简预检处理（开发环境够用） app.use((req, res, next) =\u0026gt; { // 允许前端源（开发环境可直接用*，生产环境需指定域名） res.setHeader(\u0026#34;Access-Control-Allow-Origin\u0026#34;, \u0026#34;*\u0026#34;); // 允许的请求方法和头（覆盖非简单请求需求） res.setHeader( \u0026#34;Access-Control-Allow-Methods\u0026#34;, \u0026#34;GET, POST, PUT, DELETE, OPTIONS\u0026#34; ); res.setHeader(\u0026#34;Access-Control-Allow-Headers\u0026#34;, \u0026#34;Content-Type\u0026#34;); // 拦截OPTIONS预检请求，直接返回200 if (req.method === \u0026#34;OPTIONS\u0026#34;) { return res.sendStatus(200); } next(); }); app.use(\u0026#34;/api/merchant\u0026#34;, merchantRouder); // ...其他路由 app.listen(8080); 这样后端能合法响应预检请求，前端代理就能正常工作——既不用装cors包，也能避免跨域错误。\n（3）简单请求的“例外情况” 如果前端只发简单请求（比如GET /api/merchant，无自定义头、无复杂数据），即使后端没处理预检，也可能成功：\n简单请求不会触发OPTIONS预检，前端代理直接转发GET请求到后端； 后端返回数据给前端服务器，前端服务器再返回给浏览器（浏览器只看前端服务器的同源响应，不会校验后端的跨域头）。 但只要涉及POST（JSON 数据）、PUT/DELETE等非简单请求，必须处理预检，否则必报错。\n四、Nginx 反向代理：生产环境的终极方案 devServer.proxy仅适用于开发环境，生产环境需用 Nginx 反向代理——既能解决跨域，又能优化资源加载和接口转发，是前端部署的标配。\n1. Nginx 反向代理的优势 统一域名：将前端静态资源和后端接口代理到同一域名下（如https://your-domain.com）； 性能优化：支持缓存、压缩、负载均衡； 安全防护：隐藏后端真实地址，降低攻击风险。 2. Nginx 配置示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # nginx.conf 或 conf.d/your-project.conf server { listen 80; # 监听80端口（HTTP） server_name your-domain.com; # 生产环境域名 # 前端静态资源配置（Vue打包后的dist目录） location / { root /usr/share/nginx/html/your-project; # 前端dist目录路径 index index.html index.htm; # 默认首页 try_files $uri $uri/ /index.html; # 解决Vue History路由刷新404问题 } # 后端接口反向代理 location /api { proxy_pass http://localhost:8080; # 转发到后端服务地址 proxy_set_header Host $host; # 传递请求主机名 proxy_set_header X-Real-IP $remote_addr; # 传递客户端真实IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 传递代理链IP } } 3. HTTPS 配置（生产环境必加） 若需支持 HTTPS，只需添加 SSL 证书配置：\n1 2 3 4 5 6 7 8 9 10 server { listen 443 ssl; server_name your-domain.com; # SSL证书路径 ssl_certificate /etc/nginx/ssl/your-domain.crt; ssl_certificate_key /etc/nginx/ssl/your-domain.key; # 前端静态资源+接口代理配置同上... } 五、总结 开发环境：优先使用devServer.proxy反向代理，无需频繁修改后端配置； 生产环境：推荐 Nginx 反向代理（统一域名+性能优化），或后端配置 CORS（不常用，这算黑魔法）； CORS 核心：处理预检请求+设置正确的响应头； 反向代理核心：让前端请求同源地址，由代理服务器转发到后端。 结合源码理解这两种方案，跨域问题就能迎刃而解！\n","date":"2025-11-29T22:36:00Z","permalink":"https://ye-guan-xing.github.io/p/%E8%B7%A8%E5%9F%9F%E9%97%AE%E9%A2%98%E8%AE%B2%E8%A7%A3/","title":"跨域问题讲解"},{"content":"说明 我这里写个是为了做一下整理 我觉得有趣，如果开发累了，可以换一换脑子 注：题目可直抵题目，所以不写题目内容 题目讲解 两数之和 哈哈哈,虽然这是讲 hot150,但是这道题还是 要说说的 暴力法 暴力法就是遍历所有数，然后两两相加，如果和等于目标值，则返回结果，否则返回-1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public: vector\u0026lt;int\u0026gt; twoSum(std::vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) { int n = nums.size(); for (int i = 0; i \u0026lt; n; i++) { for (int j = i + 1; j \u0026lt; n; j++) { if (nums[i] + nums[j] == target) { return {i, j}; } } } return {}; } }; 哈希表 哈希表就是将数组中的数作为键，索引作为值，然后遍历数组，将每个数作为键，索引作为值，如果哈希表中存在目标值，则返回结果，否则返回-1 时间复杂度：O(n) 为什么可以优化时间呢？ 因为哈希表的查找时间杂度是 O(1)，而数组的查找时间复杂度是 O(n)，所以哈希表可以提高查找效率。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public: vector\u0026lt;int\u0026gt; twoSum(std::vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) { int n = nums.size(); unordered_map\u0026lt;int,int\u0026gt;mp; for(int i=0;i\u0026lt;n;i++){ if(mp.find(target-nums[i])!=mp.end()){ return {i,mp[target-nums[i]]}; } mp[nums[i]]=i; } return {}; } }; 删除有序数组中的重复项 这里将 I 和 II 的解法都给出，并给出过程 题目 I 我的想法是这样的，没什么好说的，stl 大法好 1 2 3 4 5 6 7 class Solution { public: int removeDuplicates(vector\u0026lt;int\u0026gt;\u0026amp; nums) { nums.erase(unique(nums.begin(),nums.end()),nums.end()); return nums.size(); } }; 如果不能用呢？用快慢指针，慢指针最后的位置就是去重后的数组长度，可以优化空间复杂度虽然空间不值钱 1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution { public: int removeDuplicates(vector\u0026lt;int\u0026gt;\u0026amp; nums) { int k=0; for(int i=0;i\u0026lt;nums.size();i++){ if(nums[i]!=nums[k]){ k++; nums[k]=nums[i]; } } return k+1;//从0开始计数 } }; 题目 II 我的思路：遍历数组，判断当前元素是否和前两个元素相同，相同则跳过，不同则赋给下一个位置好像也是双指针 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution { public: int removeDuplicates(vector\u0026lt;int\u0026gt;\u0026amp; nums) { int n=nums.size(); if(n\u0026lt;=2) return n; int j=2; for(int i=2;i\u0026lt;n;i++){ if(nums[i]!=nums[j-2]){ nums[j]=nums[i]; j++; } //相同则跳过 } return j; } }; 双指针思路：同一的快慢指针思路，只是判断条件不同 O(n) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution { public: int removeDuplicates(vector\u0026lt;int\u0026gt;\u0026amp; nums) { int n=nums.size(); if(n\u0026lt;=2) return n; int j=0; int cnt=1; for(int i=1;i\u0026lt;n;i++){ if(nums[i]==nums[j]){ if(cnt\u0026gt;=2) continue; cnt++; j++; nums[j]=nums[i]; } else{ cnt=1; j++; nums[j]=nums[i]; } } return j+1; } }; O(1) 时间插入、删除和获取随机元素 说实话我不会，这是我想的，算暴力吧 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class RandomizedSet { public: RandomizedSet() { srand(time(0)); } int find(int val){ for(int i = 0; i \u0026lt; a.size(); i++){ if (a[i] == val){ return i; } } return -1; } bool insert(int val) { int loc=find(val); if(loc==-1){ a.push_back(val); return 1; } return 0; } bool remove(int val) { int loc=find(val); if(loc==-1) return 0; else{ a.erase(a.begin()+loc); return 1; } } int getRandom() { if(a.size()==0) return 0; return a[rand()%a.size()]; } public: vector\u0026lt;int\u0026gt;a; }; 过了好一段时间，才想到的哈希 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class RandomizedSet { public: unordered_map\u0026lt;int, int\u0026gt; mp; vector\u0026lt;int\u0026gt; a; RandomizedSet() { srand(time(0)); } int find(int val) { if (mp.count(val)) { return mp[val]; } return -1; } bool insert(int val) { if (find(val) != -1) { return false; } a.push_back(val); mp[val] = a.size() - 1; //这两步写完，才算完成。 return true; } bool remove(int val) { int loc = find(val); if (loc == -1) { return false; } int last = a.back(); a[loc] = last; mp[last] = loc; mp.erase(val); a.pop_back(); return true; } int getRandom() { if (a.empty()) return 0; return a[rand() % a.size()]; } }; 讲解一下remove()方法(不太好想) 先明确两个前提：\n数组a的特点：尾部增删元素（push_back/pop_back）是 O (1) 时间，但中间删除元素（erase）是 O (n) 时间（因为后面的元素要整体前移）。 哈希表mp的作用：记录 “元素值→数组索引”，比如mp[3]=2表示元素 3 在数组a的索引 2 位置。 你可能觉得 “元素要相连才合理”，但的核心需求是： 插入 / 删除 / 随机访问都是 O (1)； 元素不重复。 它不要求元素保持任何顺序，所以哪怕用 “八竿子打不着的最后一个元素” 替换，只要最终删掉了目标元素，且哈希表映射正确，就完全符合要求。 其实你想想这是一个误区，数组的顺序反而不重要 接雨水 网红题目,不必多言据说字节的扫地阿姨都会做 我的思路：其实我不理解为什么说难看一个柱子能接多少水，应该就能想到贪心+双指针/单调栈 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution { public: int trap(vector\u0026lt;int\u0026gt;\u0026amp; height) { int lmax=0,rmax=0; int l=0,r=height.size()-1; int n=height.size(); int ans=0; while(l\u0026lt;r){ lmax=max(lmax,height[l]); rmax=max(rmax,height[r]); if(lmax\u0026gt;rmax) { ans+=rmax-height[r]; r--; } else{ ans+=lmax-height[l]; l++; } } return ans; } }; ","date":"2025-11-28T08:36:00Z","permalink":"https://ye-guan-xing.github.io/p/%E5%8A%9B%E6%89%A3hot150%E7%B2%BE%E8%AE%B2/","title":"力扣hot150(精讲)"},{"content":"全域生活服务平台 - 从零开始完整开发指南 🎯 项目概述 我们将构建一个完整的全域生活服务平台，包含：\nWeb 管理后台：管理员管理商家、服务、订单 微信小程序：用户浏览服务、下单 后端 API：Node.js + Express + MySQL 数据库：MySQL，使用 DataGrip 管理 🛠️ 阶段一：项目准备与环境搭建 第 1 步：安装必要软件 1.1 安装 Node.js（后端运行环境） 访问：Node.js 下载 LTS 版本（长期支持版） 双击安装，全部点\u0026quot;下一步\u0026quot; 验证安装：按 Win + R，输入 cmd 回车，输入： 1 node -v 显示版本号如 v18.x.x 即成功！\n1.2 安装 Vue CLI（网页管理后台工具） 在 cmd 中继续输入：\n1 npm install -g @vue/cli 等待安装完成（可能需要几分钟）\n1.3 安装微信开发者工具（小程序开发） 访问：微信开发者工具 下载\u0026quot;稳定版\u0026quot;，安装后用微信扫码登录 1.4 安装 MySQL（数据库） 访问：MySQL Community Server 下载 MySQL Community Server 安装时记住设置的root 密码（建议设为 123456） 1.5 安装 DataGrip（数据库可视化工具） 访问：DataGrip 下载安装，学生可免费使用（用教育邮箱注册） 或使用 30 天免费试用 第 2 步：创建项目文件夹结构 在 D 盘创建项目文件夹：\n1 2 3 4 D:/life-service/ ├── server/ （后端API） ├── web-admin/ （网页管理后台） └── mini-user/ （微信小程序） 第 3 步：配置 DataGrip 连接数据库 3.1 连接 MySQL 打开 DataGrip，点击 \u0026ldquo;New Project\u0026rdquo; 项目名称：life_service_platform 在右侧 \u0026ldquo;Database\u0026rdquo; 面板，点击 \u0026quot;+\u0026quot; → \u0026ldquo;Data Source\u0026rdquo; → \u0026ldquo;MySQL\u0026rdquo; 填写连接信息： 1 2 3 4 Host: localhost Port: 3306 User: root Password: 123456 （你设置的MySQL密码） 点击 \u0026ldquo;Test Connection\u0026rdquo;，看到 ✅ Success 表示成功 点击 \u0026ldquo;OK\u0026rdquo; 3.2 创建数据库 在 DataGrip 中执行 SQL 创建数据库：\n按 Ctrl+Enter 打开新查询窗口 输入并执行： 1 2 3 CREATE DATABASE IF NOT EXISTS life_service DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; 3.3 创建数据表 在 DataGrip 中执行以下 SQL 创建表：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 -- 1. 商家表 CREATE TABLE merchants ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT \u0026#39;商家ID\u0026#39;, name VARCHAR(100) NOT NULL COMMENT \u0026#39;商家名称\u0026#39;, address VARCHAR(200) NOT NULL COMMENT \u0026#39;地址\u0026#39;, phone VARCHAR(20) NOT NULL COMMENT \u0026#39;联系方式\u0026#39;, status TINYINT NOT NULL DEFAULT 0 COMMENT \u0026#39;状态：0-审核中，1-已通过\u0026#39;, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;创建时间\u0026#39; ) COMMENT \u0026#39;商家表\u0026#39;; -- 2. 服务表 CREATE TABLE services ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT \u0026#39;服务ID\u0026#39;, merchant_id INT NOT NULL COMMENT \u0026#39;关联商家ID\u0026#39;, name VARCHAR(100) NOT NULL COMMENT \u0026#39;服务名称\u0026#39;, price DECIMAL(10,2) NOT NULL COMMENT \u0026#39;价格\u0026#39;, category VARCHAR(50) NOT NULL COMMENT \u0026#39;分类\u0026#39;, image_url VARCHAR(200) COMMENT \u0026#39;图片地址\u0026#39;, stock INT NOT NULL DEFAULT 0 COMMENT \u0026#39;库存\u0026#39;, status TINYINT NOT NULL DEFAULT 0 COMMENT \u0026#39;状态：0-下架，1-上架\u0026#39;, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;创建时间\u0026#39;, FOREIGN KEY (merchant_id) REFERENCES merchants(id) ON DELETE CASCADE ) COMMENT \u0026#39;服务表\u0026#39;; -- 3. 订单表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT \u0026#39;订单ID\u0026#39;, service_id INT NOT NULL COMMENT \u0026#39;关联服务ID\u0026#39;, user_name VARCHAR(50) NOT NULL COMMENT \u0026#39;用户姓名\u0026#39;, user_phone VARCHAR(20) NOT NULL COMMENT \u0026#39;用户电话\u0026#39;, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;下单时间\u0026#39;, status TINYINT NOT NULL DEFAULT 0 COMMENT \u0026#39;状态：0-待支付，1-已完成\u0026#39;, FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE ) COMMENT \u0026#39;订单表\u0026#39;; 3.4 插入测试数据 1 2 3 4 5 6 7 8 9 10 11 12 -- 插入测试商家 INSERT INTO merchants (name, address, phone, status) VALUES (\u0026#39;阳光家政\u0026#39;, \u0026#39;北京市朝阳区建国路100号\u0026#39;, \u0026#39;13800138000\u0026#39;, 1), (\u0026#39;快速维修\u0026#39;, \u0026#39;上海市浦东新区张江路200号\u0026#39;, \u0026#39;13900139000\u0026#39;, 1), (\u0026#39;保洁专家\u0026#39;, \u0026#39;广州市天河区体育西路300号\u0026#39;, \u0026#39;13700137000\u0026#39;, 1); -- 插入测试服务 INSERT INTO services (merchant_id, name, price, category, stock, status) VALUES (1, \u0026#39;日常保洁\u0026#39;, 150.00, \u0026#39;家政\u0026#39;, 10, 1), (1, \u0026#39;深度清洁\u0026#39;, 300.00, \u0026#39;家政\u0026#39;, 5, 1), (2, \u0026#39;空调维修\u0026#39;, 200.00, \u0026#39;维修\u0026#39;, 8, 1), (3, \u0026#39;办公室保洁\u0026#39;, 500.00, \u0026#39;保洁\u0026#39;, 3, 1); 💻 阶段二：后端 API 开发（2-3 天） 第 1 步：创建后端项目 打开 cmd，进入 server 文件夹： 1 cd D:/life-service/server 初始化项目： 1 npm init -y 安装依赖包： 1 npm install express mysql2 cors nodemon 第 2 步：创建项目文件结构 在 server 文件夹中创建以下文件结构：\n1 2 3 4 5 6 7 8 9 10 11 12 server/ ├── app.js （主入口文件） ├── db/ │ └── index.js （数据库连接） ├── routes/ │ ├── merchant.js （商家接口） │ ├── service.js （服务接口） │ └── order.js （订单接口） └── controllers/ ├── merchantCtrl.js ├── serviceCtrl.js └── orderCtrl.js 第 3 步：编写后端代码 3.1 数据库连接配置 (db/index.js) 1 2 3 4 5 6 7 8 9 10 11 const mysql = require(\u0026#34;mysql2/promise\u0026#34;); const pool = mysql.createPool({ host: \u0026#34;localhost\u0026#34;, user: \u0026#34;root\u0026#34;, password: \u0026#34;123456\u0026#34;, database: \u0026#34;life_server\u0026#34;, port: 3306, }); console.log(\u0026#34;数据库连接成功！\u0026#34;); module.exports = pool; 建议写一个 test.js 文件，测试数据库连接是否成功 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 引入我们创建的连接池 const pool = require(\u0026#34;./index.js\u0026#34;); // 执行一条简单的 SQL：查询 MySQL 数据库的版本（不需要创建表，通用测试） pool.query(\u0026#34;SELECT VERSION() AS version\u0026#34;, (err, results) =\u0026gt; { // 回调函数：SQL 执行完成后会触发这个函数 if (err) { console.error(\u0026#34;数据库操作失败：\u0026#34;, err.message); return; } console.log(\u0026#34;数据库连接成功！MySQL 版本是：\u0026#34;, results[0].version); }); // 测试完成后，关闭连接池ps(我一般不建议关,除非你有需求） // pool.end(); 3.2 主入口文件 (app.js) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const express = require(\u0026#34;express\u0026#34;); const cors = require(\u0026#34;cors\u0026#34;); const app = express(); //中间件 app.use(express.json()); app.use(cors()); //路由（比作一个餐厅的话，像是服务员） const merchantRouder = require(\u0026#34;./routes/merchant\u0026#34;); const customerRouder = require(\u0026#34;./routes/service\u0026#34;); const orderRouder = require(\u0026#34;./routes/order\u0026#34;); //使用路由 app.use(\u0026#34;/api/merchant\u0026#34;, merchantRouder); app.use(\u0026#34;/api/service\u0026#34;, customerRouder); app.use(\u0026#34;/api/order\u0026#34;, orderRouder); //启动 app.listen(8080, () =\u0026gt; { console.log(\u0026#34;服务器启动成功！\u0026#34;); console.log(\u0026#34;http://localhost:8080\u0026#34;); }); 3.3 商家控制器 (controllers/merchantCtrl.js) 控制器是实际干活的（像是餐厅的主厨） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 const pool = require(\u0026#34;../db\u0026#34;); //获取商家列表 exports.getMerchantList = async (req, res) =\u0026gt; { try { //执行 const [result] = await pool.execute(`SELECT * FROM merchants`); res.json({ code: 200, message: \u0026#34;获取成功\u0026#34;, data: result, }); //报错 } catch (err) { res.json({ code: 500, message: \u0026#34;获取失败\u0026#34;, error: err.message, }); } }; //新增 exports.addMerchant = async (req, res) =\u0026gt; { //获取数据 const { name, address, phone } = req.body; try { const [result] = await pool.execute( `INSERT INTO merchants (name,address,phone) VALUES(?,?,?)`, [name, address, phone] ); res.json({ code: 200, message: \u0026#34;添加成功\u0026#34;, data: { id: result.insertId }, }); //报错 } catch (err) { res.json({ code: 500, message: \u0026#34;添加失败\u0026#34;, data: err.message, }); console.log(err); } }; // 审核商家（更新状态） exports.approveMerchant = async (req, res) =\u0026gt; { const { id } = req.params; try { const [result] = await pool.execute( \u0026#34;UPDATE merchants SET status = 1 WHERE id = ?\u0026#34;, [id] ); if (result.affectedRows === 1) { res.json({ code: 200, msg: \u0026#34;商家审核通过！\u0026#34;, }); } else { res.json({ code: 404, msg: \u0026#34;商家不存在\u0026#34;, }); } } catch (err) { res.json({ code: 500, msg: \u0026#34;服务器出错\u0026#34;, error: err.message, }); } }; 3.4 商家路由 (routes/merchant.js) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const express = require(\u0026#34;express\u0026#34;); const router = express.Router(); //引入控制器 const merchantCtrl = require(\u0026#34;../controllers/merchantCtrl\u0026#34;); //发送到/add，用什么文件的什么方法处理 router.post(\u0026#34;/add\u0026#34;, merchantCtrl.addMerchant); router.get(\u0026#34;/list\u0026#34;, merchantCtrl.getMerchantList); // 审核商家 router.put(\u0026#34;/approve/:id\u0026#34;, merchantCtrl.approveMerchant); //导出让add.js使用 module.exports = router; 3.5 服务控制器 (controllers/serviceCtrl.js) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 const pool = require(\u0026#34;../db\u0026#34;); // 关联 merchants 表的本质是通过数据库的关联查询能力， // 一次性整合 “服务” 和 “商家” 的关联数据， // 既满足了前端展示 “服务所属商家” 的业务需求 // 即使右表（merchants）中没有匹配的记录 // （比如商家被删除，但服务记录未清理），左表（services）的记录依然会被返回（此时商家名称为 NULL） exports.getServices = async (req, res) =\u0026gt; { const { category } = req.query; let sql = ` SELECT s.*,m.name AS merchant_name FROM services AS s LEFT JOIN merchants AS m ON s.merchant_id = m.id where 1=1 `; let params = []; // 根据前端传递的分类参数（category） // 动态为 SQL 查询添加 “服务分类” 筛选条件，实现 “按分类筛选服务列表” 的功能，同时兼顾灵活性和安全性。 if (category \u0026amp;\u0026amp; category !== \u0026#34;all\u0026#34;) { sql += ` AND s.category=?`; params.push(category); } try { const [row] = await pool.execute(sql, params); res.json({ code: 200, message: \u0026#34;获取成功\u0026#34;, data: row, }); } catch (err) { res.json({ code: 500, message: \u0026#34;获取失败\u0026#34;, error: err.message, }); } }; //新增 exports.addService = async (req, res) =\u0026gt; { const { merchant_id, name, price, category, image_url, stock } = req.body; try { const [result] = await pool.execute( `INSERT INTO services (name,price,category, image_url,merchant_id,stock) VALUES(?,?,?,?,?,?)`, [name, price, category, image_url, merchant_id, stock] ); res.json({ code: 200, message: \u0026#34;新增服务成功\u0026#34;, data: { id: result.insertId }, }); } catch (err) { res.json({ code: 500, message: \u0026#34;新增服务失败\u0026#34;, data: err.message, }); } }; // 服务上架（更新状态为“上架”） exports.publishService = async (req, res) =\u0026gt; { const { id } = req.params; try { const [result] = await pool.execute( \u0026#34;UPDATE services SET status = 1 WHERE id = ?\u0026#34;, [id] ); if (result.affectedRows === 1) { res.json({ code: 200, msg: \u0026#34;服务上架成功！\u0026#34; }); } else { res.json({ code: 404, msg: \u0026#34;服务不存在\u0026#34; }); } } catch (err) { res.json({ code: 500, msg: \u0026#34;服务器出错\u0026#34;, error: err.message }); } }; // 服务下架（更新状态为“下架”） exports.unpublishService = async (req, res) =\u0026gt; { const { id } = req.params; try { const [result] = await pool.execute( \u0026#34;UPDATE services SET status = 0 WHERE id = ?\u0026#34;, [id] ); if (result.affectedRows === 1) { res.json({ code: 200, msg: \u0026#34;服务下架成功！\u0026#34; }); } else { res.json({ code: 404, msg: \u0026#34;服务不存在\u0026#34; }); } } catch (err) { res.json({ code: 500, msg: \u0026#34;服务器出错\u0026#34;, error: err.message }); } }; 3.6 服务路由 (routes/service.js) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const express = require(\u0026#34;express\u0026#34;); const router = express.Router(); const serviceCtrl = require(\u0026#34;../controllers/serviceCtrl\u0026#34;); // 获取服务列表 router.get(\u0026#34;/list\u0026#34;, serviceCtrl.getServices); // 新增服务 router.post(\u0026#34;/add\u0026#34;, serviceCtrl.addService); // 服务上架 router.put(\u0026#34;/publish/:id\u0026#34;, serviceCtrl.publishService); router.put(\u0026#34;/unpublish/:id\u0026#34;, serviceCtrl.unpublishService); module.exports = router; 3.7 订单控制器 (controllers/orderCtrl.js) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 const pool = require(\u0026#34;../db\u0026#34;); //创建定单 exports.createOrder = async (req, res) =\u0026gt; { const { service_id, user_name, user_phone } = req.body; try { const [result] = await pool.execute( `INSERT INTO orders (service_id,user_name,user_phone) VALUES(?,?,?)`, [service_id, user_name, user_phone] ); res.json({ code: 200, message: \u0026#34;创建定单成功\u0026#34;, data: { id: result.insertId }, }); } catch (err) { res.json({ code: 500, message: \u0026#34;创建定单失败\u0026#34;, error: err.message, }); } }; // WHERE 1=1 // 这是一个 “技巧性写法”，方便后续动态添加条件。 // 比如后面有 if (status !== undefined) 时， // 会拼接 AND o.status = ?。如果没有 1=1，初始的 WHERE 子句是空的 // 第一次添加条件时需要写 WHERE o.status = ?，第二次添加才写 AND ...，代码里就要判断 “是不是第一个条件”，很麻烦。 // 有了 1=1 后，不管后面加多少条件，直接用 AND ... 拼接就行 // （比如 WHERE 1=1 AND o.status=? AND o.user_id=?），简化了动态条件的拼接逻辑。 //stayus 0:待处理 1:处理中 2:完成 //获取定单列表 exports.getOrders = async (req, res) =\u0026gt; { const { status } = req.query; try { let sql = `SELECT o.*, s.name AS server_name, m.name AS merchant_name FROM orders AS o LEFT JOIN services AS s ON o.service_id = s.id LEFT JOIN merchants AS m ON s.merchant_id = m.id WHERE 1=1`; let params = []; if (status !== undefined) { sql += ` AND o.status=?`; params.push(status); } //加一个空格 sql += ` ORDER BY o.create_time DESC`; const [row] = await pool.execute(sql, params); res.json({ code: 200, message: \u0026#34;获取成功\u0026#34;, data: row, }); } catch (err) { res.json({ code: 500, message: \u0026#34;获取失败\u0026#34;, error: err.message, }); } }; 3.8 订单路由 (routes/order.js) 1 2 3 4 5 6 7 8 9 10 11 const express = require(\u0026#34;express\u0026#34;); const router = express.Router(); const orderCtrl = require(\u0026#34;../controllers/orderCtrl\u0026#34;); // 创建订单 router.post(\u0026#34;/create\u0026#34;, orderCtrl.createOrder); // 获取订单列表 router.get(\u0026#34;/list\u0026#34;, orderCtrl.getOrders); module.exports = router; 第 4 步：配置 package.json 脚本 修改 server/package.json 中的 scripts 部分：\n1 2 3 4 5 6 { \u0026#34;scripts\u0026#34;: { \u0026#34;dev\u0026#34;: \u0026#34;nodemon app.js\u0026#34;, \u0026#34;start\u0026#34;: \u0026#34;node app.js\u0026#34; } } 第 5 步：启动后端服务 1 2 cd D:/life-service/server npm run dev 看到 ✅ 后端服务启动成功！ 表示后端正常运行。\n第 6 步：测试后端 API 使用 Postman 或浏览器测试接口：\nGET http://localhost:8080/api/merchant/list - 获取商家列表 POST http://localhost:8080/api/merchant/add - 新增商家 🌐 阶段三：Web 管理后台开发（3-4 天） 第 1 步：创建 Vue 项目 打开新的 cmd 窗口： 1 2 cd D:/life-service vue create web-admin 选择配置：\nVue 3 Babel Router 其他按回车用默认配置（仔细看，别选错了） 进入项目并安装依赖：\n1 2 cd web-admin npm install axios 第 2 步：项目结构配置 在 src 文件夹中创建以下结构：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 src/ ├── api/ │ ├── merchant.js │ ├── service.js │ └── order.js ├── components/ ├── views/ │ ├── Merchant/ │ │ └── index.vue │ ├── Service/ │ │ └── index.vue │ └── Order/ │ └── index.vue └── router/ └── index.js 第 3 步：配置路由 修改 src/router/index.js：\n这里路由说的详细一点，方便理解这个整体架构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 // 1. 导入创建路由的工具 import { createRouter, createWebHistory } from \u0026#34;vue-router\u0026#34;; // createRouter：用来创建路由实例（相当于“接待员”本身） // createWebHistory：路由模式（用无#号的URL，比如 http://localhost:8080/service，更美观） // 2. 导入需要跳转的页面组件（“房间”本身） import Merchant from \u0026#34;../views/Merchant/index.vue\u0026#34;; import Service from \u0026#34;../views/Service/index.vue\u0026#34;; import Order from \u0026#34;../views/Order/index.vue\u0026#34;; // 3. 定义“地址→页面”的对应规则（“房间号→房间”的对照表） const routes = [ { path: \u0026#34;/\u0026#34;, redirect: \u0026#34;/merchant\u0026#34;, // 核心：访问 \u0026#34;/\u0026#34; 时自动跳转到 \u0026#34;/merchant\u0026#34; }, { path: \u0026#34;/merchant\u0026#34;, // 地址：网站根路径（http://localhost:8080/） component: Merchant, // 对应页面：商家管理页 name: \u0026#34;商家管理\u0026#34;, // 给这个路由起个名字（方便后续引用，可选） }, { path: \u0026#34;/service\u0026#34;, // 地址：/service（http://localhost:8080/service） name: \u0026#34;服务管理\u0026#34;, component: Service, // 对应页面：服务管理页 }, { path: \u0026#34;/order\u0026#34;, // 地址：/order（http://localhost:8080/order） name: \u0026#34;订单管理\u0026#34;, component: Order, // 对应页面：订单管理页 }, ]; // 4. 创建路由实例（初始化“接待员”，告诉他规则和工作模式） const router = createRouter({ history: createWebHistory(), // 用无#号的URL模式 routes, // 刚才定义的“地址→页面”规则 }); // 5. 导出路由实例，让整个项目能用（把“接待员”安排到酒店工作） export default router; 第 4 步：封装 API 请求 创建 src/api/merchant.js：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import axios from \u0026#34;axios\u0026#34;; axios.defaults.baseURL = \u0026#34;http://localhost:8080\u0026#34;; export const merchantAPI = { addMerchant(merchant) { return axios.post(\u0026#34;/api/merchant/add\u0026#34;, merchant); }, getMerchantList() { return axios.get(\u0026#34;/api/merchant/list\u0026#34;); }, // 审核商家 approveMerchant(id) { return axios.put(`/api/merchant/approve/${id}`); }, }; export default merchantAPI; 创建 src/api/service.js：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import axios from \u0026#34;axios\u0026#34;; export const serviceAPI = { getServices: (category = \u0026#34;\u0026#34;) =\u0026gt; { const params = { ...(category ? { category } : {}), t: Date.now(), }; return axios.get(\u0026#34;/api/service/list\u0026#34;, { params }); }, addService: (data) =\u0026gt; { return axios.post(\u0026#34;/api/service/add\u0026#34;, data); }, publishService: (id) =\u0026gt; { axios.put(`/api/service/publish/${id}`); }, unpublishService: (id) =\u0026gt; { axios.put(`/api/service/unpublish/${id}`); }, }; export default serviceAPI; 创建 src/api/order.js：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import axios from \u0026#34;axios\u0026#34;; export const orderAPI = { // 获取订单列表 getOrders: (status) =\u0026gt; { const params = status !== undefined ? { status } : {}; return axios.get(\u0026#34;/order/list\u0026#34;, { params }); }, // 创建订单 createOrder: (data) =\u0026gt; { return axios.post(\u0026#34;/order/add\u0026#34;, data); }, }; export default orderAPI; 第 5 步：开发商家管理页面 创建 src/views/Merchant/index.vue：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 \u0026lt;template\u0026gt; \u0026lt;div class=\u0026#34;merchant-page\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;🏪 商家管理\u0026lt;/h2\u0026gt; \u0026lt;!-- 新增商家表单 --\u0026gt; \u0026lt;div class=\u0026#34;add-form\u0026#34;\u0026gt; \u0026lt;h3\u0026gt;➕ 新增商家\u0026lt;/h3\u0026gt; \u0026lt;div class=\u0026#34;form-group\u0026#34;\u0026gt; \u0026lt;input v-model=\u0026#34;newMerchant.name\u0026#34; placeholder=\u0026#34;商家名称\u0026#34; class=\u0026#34;input\u0026#34; :disabled=\u0026#34;isSubmitting\u0026#34; /\u0026gt; \u0026lt;input v-model=\u0026#34;newMerchant.address\u0026#34; placeholder=\u0026#34;商家地址\u0026#34; class=\u0026#34;input\u0026#34; :disabled=\u0026#34;isSubmitting\u0026#34; /\u0026gt; \u0026lt;input v-model=\u0026#34;newMerchant.phone\u0026#34; placeholder=\u0026#34;联系电话\u0026#34; class=\u0026#34;input\u0026#34; :disabled=\u0026#34;isSubmitting\u0026#34; /\u0026gt; \u0026lt;button @click=\u0026#34;addMerchant\u0026#34; class=\u0026#34;btn btn-primary\u0026#34; :disabled=\u0026#34;isSubmitting\u0026#34; \u0026gt; {{ isSubmitting ? \u0026#34;提交中...\u0026#34; : \u0026#34;添加商家\u0026#34; }} \u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;!-- 商家列表 --\u0026gt; \u0026lt;div class=\u0026#34;merchant-list\u0026#34;\u0026gt; \u0026lt;h3\u0026gt;📋 商家列表\u0026lt;/h3\u0026gt; \u0026lt;!-- 加载状态 --\u0026gt; \u0026lt;div class=\u0026#34;loading\u0026#34; v-if=\u0026#34;isLoading\u0026#34;\u0026gt; \u0026lt;span\u0026gt;加载中...\u0026lt;/span\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;table class=\u0026#34;table\u0026#34; v-else\u0026gt; \u0026lt;thead\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;ID\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;名称\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;地址\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;电话\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;状态\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;创建时间\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;操作\u0026lt;/th\u0026gt; \u0026lt;!-- 新增操作列，更清晰 --\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; \u0026lt;!-- 空状态处理 --\u0026gt; \u0026lt;tr v-if=\u0026#34;merchants.length === 0\u0026#34;\u0026gt; \u0026lt;td colspan=\u0026#34;7\u0026#34; class=\u0026#34;empty-cell\u0026#34;\u0026gt;暂无商家数据\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr v-for=\u0026#34;merchant in merchants\u0026#34; :key=\u0026#34;merchant.id\u0026#34;\u0026gt; \u0026lt;td\u0026gt;{{ merchant.id }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{{ merchant.name }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{{ merchant.address }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{{ merchant.phone }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;span :class=\u0026#34; merchant.status === 0 ? \u0026#39;status-pending\u0026#39; : \u0026#39;status-approved\u0026#39; \u0026#34; \u0026gt; {{ merchant.status === 0 ? \u0026#34;审核中\u0026#34; : \u0026#34;已通过\u0026#34; }} \u0026lt;/span\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{{ formatTime(merchant.create_time) }}\u0026lt;/td\u0026gt; \u0026lt;td class=\u0026#34;operation-cell\u0026#34;\u0026gt; \u0026lt;!-- 审核按钮：只在\u0026#34;审核中\u0026#34;且非加载状态显示 --\u0026gt; \u0026lt;button class=\u0026#34;approve-btn\u0026#34; @click=\u0026#34;handleApprove(merchant.id)\u0026#34; v-if=\u0026#34;merchant.status === 0 \u0026amp;\u0026amp; !isLoading\u0026#34; :disabled=\u0026#34;isApproving[merchant.id]\u0026#34; \u0026gt; {{ isApproving[merchant.id] ? \u0026#34;审核中...\u0026#34; : \u0026#34;审核通过\u0026#34; }} \u0026lt;/button\u0026gt; \u0026lt;!-- 已通过状态提示 --\u0026gt; \u0026lt;span class=\u0026#34;approved-text\u0026#34; v-else-if=\u0026#34;merchant.status === 1\u0026#34;\u0026gt; 已审核 \u0026lt;/span\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import { ref, onMounted } from \u0026#34;vue\u0026#34;; import merchantAPI from \u0026#34;../../api/merchant\u0026#34;; export default { name: \u0026#34;MerchantView\u0026#34;, setup() { // 商家列表数据 const merchants = ref([]); // 新增商家表单数据 const newMerchant = ref({ name: \u0026#34;\u0026#34;, address: \u0026#34;\u0026#34;, phone: \u0026#34;\u0026#34;, }); // 加载状态（列表加载中） const isLoading = ref(false); // 提交状态（新增商家时） const isSubmitting = ref(false); // 审核状态（针对每个商家的单独加载状态，避免重复点击） const isApproving = ref({}); // 结构：{ 1: true, 2: false, ... } // 获取商家列表（封装为独立方法，便于复用） const getMerchants = async () =\u0026gt; { try { isLoading.value = true; // 显示加载状态 const response = await merchantAPI.getMerchantList(); merchants.value = response.data.data || []; // 兼容空数据 } catch (error) { alert(\u0026#34;获取商家列表失败：\u0026#34; + (error.message || \u0026#34;网络错误\u0026#34;)); console.error(\u0026#34;商家列表加载失败：\u0026#34;, error); } finally { isLoading.value = false; // 无论成功失败，关闭加载状态 } }; // 新增商家 const addMerchant = async () =\u0026gt; { // 表单验证 if (!newMerchant.value.name.trim()) { alert(\u0026#34;请输入商家名称！\u0026#34;); return; } if (!newMerchant.value.address.trim()) { alert(\u0026#34;请输入商家地址！\u0026#34;); return; } if (!newMerchant.value.phone.trim()) { alert(\u0026#34;请输入联系电话！\u0026#34;); return; } // 简单手机号格式验证（11位数字） if (!/^\\d{11}$/.test(newMerchant.value.phone)) { alert(\u0026#34;请输入有效的11位手机号！\u0026#34;); return; } try { isSubmitting.value = true; // 防止重复提交 await merchantAPI.addMerchant(newMerchant.value); alert(\u0026#34;商家添加成功！\u0026#34;); // 清空表单 newMerchant.value = { name: \u0026#34;\u0026#34;, address: \u0026#34;\u0026#34;, phone: \u0026#34;\u0026#34; }; // 刷新列表 getMerchants(); } catch (error) { alert(\u0026#34;添加商家失败：\u0026#34; + (error.message || \u0026#34;服务器错误\u0026#34;)); console.error(\u0026#34;新增商家失败：\u0026#34;, error); } finally { isSubmitting.value = false; // 恢复提交状态 } }; // 审核商家（核心补充） const handleApprove = async (id) =\u0026gt; { // 确认操作（避免误点） if (!confirm(\u0026#34;确定要通过该商家的审核吗？\u0026#34;)) { return; } try { // 标记当前商家正在审核中 isApproving.value[id] = true; await merchantAPI.approveMerchant(id); alert(\u0026#34;商家审核通过！\u0026#34;); // 刷新列表 getMerchants(); } catch (error) { alert(\u0026#34;审核失败：\u0026#34; + (error.message || \u0026#34;服务器错误\u0026#34;)); console.error(`审核商家${id}失败：`, error); } finally { // 清除审核状态 isApproving.value[id] = false; } }; // 格式化时间（兼容空值） const formatTime = (timeString) =\u0026gt; { if (!timeString) return \u0026#34;-\u0026#34;; return new Date(timeString).toLocaleString(); }; // 页面加载时初始化数据 onMounted(() =\u0026gt; { getMerchants(); }); return { merchants, newMerchant, isLoading, isSubmitting, isApproving, addMerchant, handleApprove, // 导出审核方法（关键补充） formatTime, }; }, }; \u0026lt;/script\u0026gt; \u0026lt;style scoped\u0026gt; .merchant-page { padding: 20px; max-width: 1200px; margin: 0 auto; } .add-form { margin: 30px 0; padding: 20px; border: 1px solid #e1e1e1; border-radius: 8px; background: #f9f9f9; } .form-group { display: flex; gap: 10px; align-items: end; flex-wrap: wrap; } .input { padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; flex: 1; min-width: 200px; font-size: 14px; } .input:disabled { background: #f0f0f0; cursor: not-allowed; } .btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.3s; } .btn:disabled { background: #9e9e9e; cursor: not-allowed; } .btn-primary { background: #4caf50; color: white; } .btn-primary:hover:not(:disabled) { background: #45a049; } .merchant-list { margin-top: 30px; } .table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } .table th, .table td { border: 1px solid #ddd; padding: 12px; text-align: left; } .table th { background: #f5f5f5; font-weight: bold; white-space: nowrap; /* 表头不换行 */ } /* 空状态单元格 */ .empty-cell { text-align: center; padding: 40px 0; color: #999; } /* 状态样式 */ .status-pending { color: #ff9800; font-weight: bold; } .status-approved { color: #4caf50; font-weight: bold; } /* 操作列样式 */ .operation-cell { white-space: nowrap; /* 操作按钮不换行 */ } .approve-btn { background: #2196f3; color: white; border: none; border-radius: 4px; padding: 4px 10px; cursor: pointer; font-size: 13px; transition: background 0.3s; } .approve-btn:hover:not(:disabled) { background: #0b7dda; } .approve-btn:disabled { background: #bbdefb; cursor: not-allowed; } .approved-text { color: #666; font-size: 13px; } /* 加载状态 */ .loading { text-align: center; padding: 40px 0; color: #666; font-size: 14px; } \u0026lt;/style\u0026gt; 第 6 步：修改 App.vue 更新 src/App.vue：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 \u0026lt;template\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt; \u0026lt;nav class=\u0026#34;navbar\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;nav-container\u0026#34;\u0026gt; \u0026lt;h1 class=\u0026#34;nav-title\u0026#34;\u0026gt;全域生活服务平台\u0026lt;/h1\u0026gt; \u0026lt;div class=\u0026#34;nav-links\u0026#34;\u0026gt; \u0026lt;router-link to=\u0026#34;/merchant\u0026#34; class=\u0026#34;nav-link\u0026#34;\u0026gt;商家管理\u0026lt;/router-link\u0026gt; \u0026lt;router-link to=\u0026#34;/service\u0026#34; class=\u0026#34;nav-link\u0026#34;\u0026gt;服务管理\u0026lt;/router-link\u0026gt; \u0026lt;router-link to=\u0026#34;/order\u0026#34; class=\u0026#34;nav-link\u0026#34;\u0026gt;订单管理\u0026lt;/router-link\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/nav\u0026gt; \u0026lt;main class=\u0026#34;main-content\u0026#34;\u0026gt; \u0026lt;router-view /\u0026gt; \u0026lt;/main\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; export default { name: \u0026#34;App\u0026#34;, }; \u0026lt;/script\u0026gt; \u0026lt;style\u0026gt; * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: \u0026#34;Segoe UI\u0026#34;, Tahoma, Geneva, Verdana, sans-serif; background: #f5f5f5; } .navbar { background: #2c3e50; color: white; padding: 0; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .nav-container { max-width: 1200px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; padding: 0 20px; } .nav-title { font-size: 24px; font-weight: bold; } .nav-links { display: flex; gap: 20px; } .nav-link { color: white; text-decoration: none; padding: 15px 20px; border-radius: 4px; transition: background 0.3s; } .nav-link:hover, .nav-link.router-link-active { background: #34495e; } .main-content { min-height: calc(100vh - 60px); padding: 20px; } \u0026lt;/style\u0026gt; 第 7 步：开发服务管理页面 创建 src/views/Service/index.vue：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 \u0026lt;template\u0026gt; \u0026lt;div class=\u0026#34;service-page\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;🛎️ 服务管理\u0026lt;/h2\u0026gt; \u0026lt;!-- 新增服务表单 --\u0026gt; \u0026lt;div class=\u0026#34;add-form\u0026#34;\u0026gt; \u0026lt;h3\u0026gt;➕ 新增服务\u0026lt;/h3\u0026gt; \u0026lt;div class=\u0026#34;form-grid\u0026#34;\u0026gt; \u0026lt;input v-model=\u0026#34;newService.name\u0026#34; placeholder=\u0026#34;服务名称\u0026#34; class=\u0026#34;input\u0026#34; /\u0026gt; \u0026lt;select v-model=\u0026#34;newService.merchant_id\u0026#34; class=\u0026#34;input\u0026#34;\u0026gt; \u0026lt;option value=\u0026#34;\u0026#34;\u0026gt;选择商家\u0026lt;/option\u0026gt; \u0026lt;option v-for=\u0026#34;merchant in merchants\u0026#34; :key=\u0026#34;merchant.id\u0026#34; :value=\u0026#34;merchant.id\u0026#34; \u0026gt; {{ merchant.name }} \u0026lt;/option\u0026gt; \u0026lt;/select\u0026gt; \u0026lt;input v-model=\u0026#34;newService.price\u0026#34; type=\u0026#34;number\u0026#34; placeholder=\u0026#34;价格\u0026#34; class=\u0026#34;input\u0026#34; /\u0026gt; \u0026lt;select v-model=\u0026#34;newService.category\u0026#34; class=\u0026#34;input\u0026#34;\u0026gt; \u0026lt;option value=\u0026#34;\u0026#34;\u0026gt;选择分类\u0026lt;/option\u0026gt; \u0026lt;option value=\u0026#34;家政\u0026#34;\u0026gt;家政\u0026lt;/option\u0026gt; \u0026lt;option value=\u0026#34;维修\u0026#34;\u0026gt;维修\u0026lt;/option\u0026gt; \u0026lt;option value=\u0026#34;保洁\u0026#34;\u0026gt;保洁\u0026lt;/option\u0026gt; \u0026lt;/select\u0026gt; \u0026lt;input v-model=\u0026#34;newService.stock\u0026#34; type=\u0026#34;number\u0026#34; placeholder=\u0026#34;库存\u0026#34; class=\u0026#34;input\u0026#34; /\u0026gt; \u0026lt;input v-model=\u0026#34;newService.image_url\u0026#34; placeholder=\u0026#34;图片URL\u0026#34; class=\u0026#34;input\u0026#34; /\u0026gt; \u0026lt;button @click=\u0026#34;addService\u0026#34; class=\u0026#34;btn btn-primary\u0026#34;\u0026gt;添加服务\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;!-- 服务列表 --\u0026gt; \u0026lt;div class=\u0026#34;service-list\u0026#34;\u0026gt; \u0026lt;h3\u0026gt;📋 服务列表\u0026lt;/h3\u0026gt; \u0026lt;table class=\u0026#34;table\u0026#34;\u0026gt; \u0026lt;thead\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;ID\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;服务名称\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;商家\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;价格\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;分类\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;库存\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;状态\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;操作\u0026lt;/th\u0026gt; \u0026lt;!-- 新增操作列 --\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; \u0026lt;tr v-for=\u0026#34;service in services\u0026#34; :key=\u0026#34;service.id\u0026#34;\u0026gt; \u0026lt;td\u0026gt;{{ service.id }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{{ service.name }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{{ service.merchant_name }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;¥{{ service.price }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{{ service.category }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{{ service.stock }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;span :class=\u0026#34;service.status === 0 ? \u0026#39;status-off\u0026#39; : \u0026#39;status-on\u0026#39;\u0026#34;\u0026gt; {{ service.status === 0 ? \u0026#34;下架\u0026#34; : \u0026#34;上架\u0026#34; }} \u0026lt;/span\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;!-- 上架按钮（下架状态时显示） --\u0026gt; \u0026lt;button v-if=\u0026#34;service.status === 0\u0026#34; class=\u0026#34;btn btn-publish\u0026#34; @click=\u0026#34;handlePublish(service.id)\u0026#34; \u0026gt; 上架 \u0026lt;/button\u0026gt; \u0026lt;!-- 下架按钮（上架状态时显示） --\u0026gt; \u0026lt;button v-else class=\u0026#34;btn btn-unpublish\u0026#34; @click=\u0026#34;handleUnpublish(service.id)\u0026#34; \u0026gt; 下架 \u0026lt;/button\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import { ref, onMounted } from \u0026#34;vue\u0026#34;; import serviceAPI from \u0026#34;../../api/service\u0026#34;; import merchantAPI from \u0026#34;../../api/merchant\u0026#34;; export default { name: \u0026#34;ServiceView\u0026#34;, setup() { const services = ref([]); const merchants = ref([]); const newService = ref({ name: \u0026#34;\u0026#34;, merchant_id: \u0026#34;\u0026#34;, price: \u0026#34;\u0026#34;, category: \u0026#34;\u0026#34;, stock: \u0026#34;\u0026#34;, image_url: \u0026#34;\u0026#34;, }); // 获取服务列表 const getServices = async () =\u0026gt; { try { const response = await serviceAPI.getServices(); services.value = response.data.data; } catch (error) { alert(\u0026#34;获取服务列表失败！\u0026#34;); console.error(error); } }; // 获取商家列表 const getMerchants = async () =\u0026gt; { try { const response = await merchantAPI.getMerchantList(); merchants.value = response.data.data; } catch (error) { alert(\u0026#34;获取商家列表失败！\u0026#34;); console.error(error); } }; // 新增服务 const addService = async () =\u0026gt; { if ( !newService.value.name || !newService.value.merchant_id || !newService.value.price ) { alert(\u0026#34;请填写完整信息！\u0026#34;); return; } try { await serviceAPI.addService(newService.value); alert(\u0026#34;服务添加成功！\u0026#34;); newService.value = { name: \u0026#34;\u0026#34;, merchant_id: \u0026#34;\u0026#34;, price: \u0026#34;\u0026#34;, category: \u0026#34;\u0026#34;, stock: \u0026#34;\u0026#34;, image_url: \u0026#34;\u0026#34;, }; getServices(); } catch (error) { alert(\u0026#34;添加服务失败！\u0026#34;); console.error(error); } }; // 上架服务（状态更新为1） const handlePublish = async (id) =\u0026gt; { try { await serviceAPI.publishService(id); alert(\u0026#34;服务上架成功！\u0026#34;); getServices(); // 刷新列表 } catch (error) { alert(\u0026#34;上架失败！\u0026#34;); console.error(error); } }; // 下架服务（状态更新为0） const handleUnpublish = async (id) =\u0026gt; { try { await serviceAPI.unpublishService(id); alert(\u0026#34;服务下架成功！\u0026#34;); getServices(); // 刷新列表 } catch (error) { alert(\u0026#34;下架失败！\u0026#34;); console.error(error); } }; onMounted(() =\u0026gt; { getServices(); getMerchants(); }); return { services, merchants, newService, addService, handlePublish, // 导出上架方法 handleUnpublish, // 导出下架方法 }; }, }; \u0026lt;/script\u0026gt; \u0026lt;style scoped\u0026gt; .service-page { padding: 20px; max-width: 1200px; margin: 0 auto; } .add-form { margin: 30px 0; padding: 20px; border: 1px solid #e1e1e1; border-radius: 8px; background: #f9f9f9; } .form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; align-items: end; } .input { padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; width: 100%; } .btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; } .btn-primary { background: #4caf50; color: white; grid-column: 1 / -1; justify-self: start; } .btn-primary:hover { background: #45a049; } /* 上架按钮样式 */ .btn-publish { background: #2196f3; color: white; } .btn-publish:hover { background: #0b7dda; } /* 下架按钮样式 */ .btn-unpublish { background: #ff9800; color: white; } .btn-unpublish:hover { background: #e68900; } .service-list { margin-top: 30px; } .table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } .table th, .table td { border: 1px solid #ddd; padding: 12px; text-align: left; } .table th { background: #f5f5f5; font-weight: bold; } .status-on { color: #4caf50; font-weight: bold; } .status-off { color: #f44336; font-weight: bold; } \u0026lt;/style\u0026gt; 第 8 步：开发订单管理页面 创建 src/views/Order/index.vue：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 \u0026lt;template\u0026gt; \u0026lt;div class=\u0026#34;order-page\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;📦 订单管理\u0026lt;/h2\u0026gt; \u0026lt;!-- 筛选条件 --\u0026gt; \u0026lt;div class=\u0026#34;filter-section\u0026#34;\u0026gt; \u0026lt;h3\u0026gt;筛选条件\u0026lt;/h3\u0026gt; \u0026lt;div class=\u0026#34;filter-group\u0026#34;\u0026gt; \u0026lt;label\u0026gt; \u0026lt;input type=\u0026#34;radio\u0026#34; v-model=\u0026#34;filterStatus\u0026#34; value=\u0026#34;\u0026#34; /\u0026gt; 全部订单 \u0026lt;/label\u0026gt; \u0026lt;label\u0026gt; \u0026lt;input type=\u0026#34;radio\u0026#34; v-model=\u0026#34;filterStatus\u0026#34; value=\u0026#34;0\u0026#34; /\u0026gt; 待支付 \u0026lt;/label\u0026gt; \u0026lt;label\u0026gt; \u0026lt;input type=\u0026#34;radio\u0026#34; v-model=\u0026#34;filterStatus\u0026#34; value=\u0026#34;1\u0026#34; /\u0026gt; 已完成 \u0026lt;/label\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;!-- 订单列表 --\u0026gt; \u0026lt;div class=\u0026#34;order-list\u0026#34;\u0026gt; \u0026lt;h3\u0026gt;订单列表\u0026lt;/h3\u0026gt; \u0026lt;table class=\u0026#34;table\u0026#34;\u0026gt; \u0026lt;thead\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;订单ID\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;服务名称\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;商家\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;用户姓名\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;用户电话\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;价格\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;状态\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;下单时间\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; \u0026lt;tr v-for=\u0026#34;order in orders\u0026#34; :key=\u0026#34;order.id\u0026#34;\u0026gt; \u0026lt;td\u0026gt;{{ order.id }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{{ order.service_name }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{{ order.merchant_name }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{{ order.user_name }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{{ order.user_phone }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;¥{{ order.price }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;span :class=\u0026#34; order.status === 0 ? \u0026#39;status-pending\u0026#39; : \u0026#39;status-completed\u0026#39; \u0026#34; \u0026gt; {{ order.status === 0 ? \u0026#34;待支付\u0026#34; : \u0026#34;已完成\u0026#34; }} \u0026lt;/span\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{{ formatTime(order.create_time) }}\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import { ref, onMounted, watch } from \u0026#34;vue\u0026#34;; import orderAPI from \u0026#34;../../api/order\u0026#34;; export default { name: \u0026#34;OrderView\u0026#34;, setup() { const orders = ref([]); const filterStatus = ref(\u0026#34;\u0026#34;); // 获取订单列表 const getOrders = async (status = \u0026#34;\u0026#34;) =\u0026gt; { try { const response = await orderAPI.getOrders(status); orders.value = response.data.data; } catch (error) { alert(\u0026#34;获取订单列表失败！\u0026#34;); console.error(error); } }; // 格式化时间 const formatTime = (timeString) =\u0026gt; { return new Date(timeString).toLocaleString(); }; // 监听筛选条件变化 watch(filterStatus, (newStatus) =\u0026gt; { getOrders(newStatus); }); // 页面加载时获取数据 onMounted(() =\u0026gt; { getOrders(); }); return { orders, filterStatus, formatTime, }; }, }; \u0026lt;/script\u0026gt; \u0026lt;style scoped\u0026gt; .order-page { padding: 20px; max-width: 1200px; margin: 0 auto; } .filter-section { margin: 30px 0; padding: 20px; border: 1px solid #e1e1e1; border-radius: 8px; background: #f9f9f9; } .filter-group { display: flex; gap: 20px; margin-top: 10px; } .filter-group label { display: flex; align-items: center; gap: 5px; cursor: pointer; } .order-list { margin-top: 30px; } .table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } .table th, .table td { border: 1px solid #ddd; padding: 12px; text-align: left; } .table th { background: #f5f5f5; font-weight: bold; } .status-pending { color: #ff9800; font-weight: bold; } .status-completed { color: #4caf50; font-weight: bold; } \u0026lt;/style\u0026gt; 第 9 步：启动 Web 管理后台 注意此时，还有前后端跨域问题（用 cores 或者 nginx 代理解决） PS（我会在后续的文章中解决） 1 2 cd D:/life-service/web-admin npm run serve 访问：http://localhost:8080\n📱 阶段四：微信小程序开发（3-4 天） 第 1 步：创建小程序项目 打开微信开发者工具 点击\u0026quot;新建项目\u0026quot; 填写信息： 项目名称：生活服务平台 目录：选择 D:/life-service/mini-user AppID：点击\u0026quot;测试号\u0026quot; 后端服务：不使用云服务 点击\u0026quot;新建\u0026quot; 第 2 步：项目结构配置 在小程序项目中创建以下结构：\n1 2 3 4 5 6 7 8 9 10 mini-user/ ├── pages/ │ ├── index/（首页） │ ├── serviceList/（服务列表） │ ├── orderCreate/（创建订单） │ └── orderList/（订单列表） ├── utils/ │ └── request.js（请求封装） ├── images/（图片资源） └── app.js（小程序入口） 第 3 步：封装网络请求 创建 utils/request.js：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const request = (url, method = \u0026#34;GET\u0026#34;, data = {}) =\u0026gt; { return new Promise((resolve, reject) =\u0026gt; { wx.request({ url: \u0026#34;http://localhost:8080/api\u0026#34; + url, method, data, success: (res) =\u0026gt; { if (res.data.code === 200) { resolve(res.data.data); } else { reject(res.data.msg); } }, fail: (err) =\u0026gt; { reject(\u0026#34;网络请求失败：\u0026#34; + err.errMsg); }, }); }); }; module.exports = request; 第 4 步：配置小程序页面 修改 app.json：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { \u0026#34;pages\u0026#34;: [ \u0026#34;pages/index/index\u0026#34;, \u0026#34;pages/serviceList/serviceList\u0026#34;, \u0026#34;pages/orderCreate/orderCreate\u0026#34;, \u0026#34;pages/orderList/orderList\u0026#34; ], \u0026#34;window\u0026#34;: { \u0026#34;backgroundTextStyle\u0026#34;: \u0026#34;light\u0026#34;, \u0026#34;navigationBarBackgroundColor\u0026#34;: \u0026#34;#2c3e50\u0026#34;, \u0026#34;navigationBarTitleText\u0026#34;: \u0026#34;生活服务平台\u0026#34;, \u0026#34;navigationBarTextStyle\u0026#34;: \u0026#34;white\u0026#34; }, \u0026#34;style\u0026#34;: \u0026#34;v2\u0026#34;, \u0026#34;sitemapLocation\u0026#34;: \u0026#34;sitemap.json\u0026#34; } 第 5 步：开发首页 创建 pages/index/index.js：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 const request = require(\u0026#34;../../utils/request\u0026#34;); Page({ data: { services: [], categories: [ { name: \u0026#34;家政\u0026#34;, icon: \u0026#34;🏠\u0026#34;, type: \u0026#34;家政\u0026#34; }, { name: \u0026#34;维修\u0026#34;, icon: \u0026#34;🔧\u0026#34;, type: \u0026#34;维修\u0026#34; }, { name: \u0026#34;保洁\u0026#34;, icon: \u0026#34;✨\u0026#34;, type: \u0026#34;保洁\u0026#34; }, ], }, onLoad() { this.getServices(); }, // 获取推荐服务 async getServices() { try { const services = await request(\u0026#34;/service/list\u0026#34;); // 只取前4个作为推荐 this.setData({ services: services.slice(0, 4), }); } catch (err) { wx.showToast({ title: \u0026#34;加载失败\u0026#34;, icon: \u0026#34;none\u0026#34;, }); } }, // 跳转到服务列表 toServiceList(e) { const type = e.currentTarget.dataset.type; wx.navigateTo({ url: `/pages/serviceList/serviceList?type=${type}`, }); }, // 跳转到订单列表 toOrderList() { wx.navigateTo({ url: \u0026#34;/pages/orderList/orderList\u0026#34;, }); }, }); 创建 pages/index/index.wxml：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 \u0026lt;view class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;!-- 头部 --\u0026gt; \u0026lt;view class=\u0026#34;header\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;title\u0026#34;\u0026gt;生活服务平台\u0026lt;/text\u0026gt; \u0026lt;text class=\u0026#34;subtitle\u0026#34;\u0026gt;便捷生活，一键送达\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 服务分类 --\u0026gt; \u0026lt;view class=\u0026#34;category-section\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;section-title\u0026#34;\u0026gt;服务分类\u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;category-grid\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;category-item\u0026#34; wx:for=\u0026#34;{{categories}}\u0026#34; wx:key=\u0026#34;type\u0026#34; bindtap=\u0026#34;toServiceList\u0026#34; data-type=\u0026#34;{{item.type}}\u0026#34; \u0026gt; \u0026lt;text class=\u0026#34;category-icon\u0026#34;\u0026gt;{{item.icon}}\u0026lt;/text\u0026gt; \u0026lt;text class=\u0026#34;category-name\u0026#34;\u0026gt;{{item.name}}\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 推荐服务 --\u0026gt; \u0026lt;view class=\u0026#34;recommend-section\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;section-header\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;section-title\u0026#34;\u0026gt;推荐服务\u0026lt;/text\u0026gt; \u0026lt;text class=\u0026#34;more\u0026#34; bindtap=\u0026#34;toServiceList\u0026#34; data-type=\u0026#34;\u0026#34;\u0026gt;查看更多\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;service-grid\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;service-card\u0026#34; wx:for=\u0026#34;{{services}}\u0026#34; wx:key=\u0026#34;id\u0026#34; bindtap=\u0026#34;toOrderCreate\u0026#34; data-service=\u0026#34;{{item}}\u0026#34; \u0026gt; \u0026lt;image class=\u0026#34;service-image\u0026#34; src=\u0026#34;{{item.image_url || \u0026#39;/images/default-service.png\u0026#39;}}\u0026#34;\u0026gt;\u0026lt;/image\u0026gt; \u0026lt;view class=\u0026#34;service-info\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;service-name\u0026#34;\u0026gt;{{item.name}}\u0026lt;/text\u0026gt; \u0026lt;text class=\u0026#34;service-merchant\u0026#34;\u0026gt;{{item.merchant_name}}\u0026lt;/text\u0026gt; \u0026lt;view class=\u0026#34;service-bottom\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;service-price\u0026#34;\u0026gt;¥{{item.price}}\u0026lt;/text\u0026gt; \u0026lt;text class=\u0026#34;service-category\u0026#34;\u0026gt;{{item.category}}\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 底部导航 --\u0026gt; \u0026lt;view class=\u0026#34;bottom-nav\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;nav-item\u0026#34; bindtap=\u0026#34;toServiceList\u0026#34; data-type=\u0026#34;\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;nav-icon\u0026#34;\u0026gt;🔍\u0026lt;/text\u0026gt; \u0026lt;text class=\u0026#34;nav-text\u0026#34;\u0026gt;全部服务\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;nav-item\u0026#34; bindtap=\u0026#34;toOrderList\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;nav-icon\u0026#34;\u0026gt;📦\u0026lt;/text\u0026gt; \u0026lt;text class=\u0026#34;nav-text\u0026#34;\u0026gt;我的订单\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; 创建 pages/index/index.wxss：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 .container { padding: 20rpx; background: #f5f5f5; min-height: 100vh; } /* 头部样式 */ .header { text-align: center; margin: 40rpx 0 60rpx; } .title { display: block; font-size: 48rpx; font-weight: bold; color: #2c3e50; margin-bottom: 10rpx; } .subtitle { display: block; font-size: 28rpx; color: #7f8c8d; } /* 分类区域 */ .category-section { margin-bottom: 40rpx; } .section-title { font-size: 36rpx; font-weight: bold; color: #2c3e50; margin-bottom: 30rpx; display: block; } .category-grid { display: flex; justify-content: space-around; background: white; padding: 30rpx; border-radius: 20rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); } .category-item { display: flex; flex-direction: column; align-items: center; } .category-icon { font-size: 60rpx; margin-bottom: 15rpx; } .category-name { font-size: 28rpx; color: #2c3e50; } /* 推荐服务 */ .recommend-section { margin-bottom: 100rpx; } .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30rpx; } .more { font-size: 28rpx; color: #3498db; } .service-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20rpx; } .service-card { background: white; border-radius: 20rpx; overflow: hidden; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); } .service-image { width: 100%; height: 200rpx; background: #ecf0f1; } .service-info { padding: 20rpx; } .service-name { display: block; font-size: 30rpx; font-weight: bold; color: #2c3e50; margin-bottom: 10rpx; } .service-merchant { display: block; font-size: 24rpx; color: #7f8c8d; margin-bottom: 15rpx; } .service-bottom { display: flex; justify-content: space-between; align-items: center; } .service-price { color: #e74c3c; font-size: 28rpx; font-weight: bold; } .service-category { font-size: 24rpx; color: #3498db; background: #ecf0f1; padding: 5rpx 15rpx; border-radius: 20rpx; } /* 底部导航 */ .bottom-nav { position: fixed; bottom: 0; left: 0; right: 0; background: white; display: flex; padding: 20rpx; box-shadow: 0 -2rpx 20rpx rgba(0, 0, 0, 0.1); } .nav-item { flex: 1; display: flex; flex-direction: column; align-items: center; } .nav-icon { font-size: 40rpx; margin-bottom: 10rpx; } .nav-text { font-size: 24rpx; color: #2c3e50; } 第 6 步：开发服务列表页面 创建 pages/serviceList/serviceList.js：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 const request = require(\u0026#34;../../utils/request\u0026#34;); Page({ data: { services: [], categories: [\u0026#34;全部\u0026#34;, \u0026#34;家政\u0026#34;, \u0026#34;维修\u0026#34;, \u0026#34;保洁\u0026#34;], activeCategory: \u0026#34;全部\u0026#34;, searchKeyword: \u0026#34;\u0026#34;, }, onLoad(options) { if (options.type) { this.setData({ activeCategory: options.type }); } this.getServices(); }, // 获取服务列表 async getServices() { const { activeCategory, searchKeyword } = this.data; try { let category = activeCategory === \u0026#34;全部\u0026#34; ? \u0026#34;\u0026#34; : activeCategory; let services = await request(\u0026#34;/service/list\u0026#34;, \u0026#34;GET\u0026#34;, { category }); // 前端搜索过滤 if (searchKeyword) { services = services.filter( (service) =\u0026gt; service.name.includes(searchKeyword) || service.merchant_name.includes(searchKeyword) ); } this.setData({ services }); } catch (err) { wx.showToast({ title: \u0026#34;加载失败\u0026#34;, icon: \u0026#34;none\u0026#34;, }); } }, // 切换分类 switchCategory(e) { const category = e.currentTarget.dataset.category; this.setData({ activeCategory: category, searchKeyword: \u0026#34;\u0026#34;, }); this.getServices(); }, // 搜索输入 onSearchInput(e) { this.setData({ searchKeyword: e.detail.value }); }, // 执行搜索 onSearch() { this.getServices(); }, // 跳转到下单页面（修改后） toOrderCreate(e) { const service = e.currentTarget.dataset.service; wx.navigateTo({ url: `/pages/orderCreate/orderCreate?service=${encodeURIComponent( JSON.stringify(service) )}`, }); }, }); 创建 pages/serviceList/serviceList.wxml：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 \u0026lt;view class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;!-- 搜索框 --\u0026gt; \u0026lt;view class=\u0026#34;search-box\u0026#34;\u0026gt; \u0026lt;input class=\u0026#34;search-input\u0026#34; placeholder=\u0026#34;搜索服务或商家...\u0026#34; value=\u0026#34;{{searchKeyword}}\u0026#34; bindinput=\u0026#34;onSearchInput\u0026#34; /\u0026gt; \u0026lt;button class=\u0026#34;search-btn\u0026#34; bindtap=\u0026#34;onSearch\u0026#34;\u0026gt;搜索\u0026lt;/button\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 分类筛选 --\u0026gt; \u0026lt;scroll-view class=\u0026#34;category-scroll\u0026#34; scroll-x\u0026gt; \u0026lt;view class=\u0026#34;category-list\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;category-item {{activeCategory === item ? \u0026#39;active\u0026#39; : \u0026#39;\u0026#39;}}\u0026#34; wx:for=\u0026#34;{{categories}}\u0026#34; wx:key=\u0026#34;*this\u0026#34; bindtap=\u0026#34;switchCategory\u0026#34; data-category=\u0026#34;{{item}}\u0026#34;\u0026gt; {{item}} \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/scroll-view\u0026gt; \u0026lt;!-- 服务列表 --\u0026gt; \u0026lt;view class=\u0026#34;service-list\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;service-item\u0026#34; wx:for=\u0026#34;{{services}}\u0026#34; wx:key=\u0026#34;id\u0026#34; bindtap=\u0026#34;toOrderCreate\u0026#34; data-service=\u0026#34;{{item}}\u0026#34;\u0026gt; \u0026lt;image class=\u0026#34;service-image\u0026#34; src=\u0026#34;{{item.image_url || \u0026#39;/images/default-service.png\u0026#39;}}\u0026#34;\u0026gt;\u0026lt;/image\u0026gt; \u0026lt;view class=\u0026#34;service-content\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;service-header\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;service-name\u0026#34;\u0026gt;{{item.name}}\u0026lt;/text\u0026gt; \u0026lt;text class=\u0026#34;service-price\u0026#34;\u0026gt;¥{{item.price}}\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;service-merchant\u0026#34;\u0026gt;{{item.merchant_name}}\u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;service-footer\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;service-category\u0026#34;\u0026gt;{{item.category}}\u0026lt;/text\u0026gt; \u0026lt;text class=\u0026#34;service-stock\u0026#34;\u0026gt;库存: {{item.stock}}\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 空状态 --\u0026gt; \u0026lt;view class=\u0026#34;empty-state\u0026#34; wx:if=\u0026#34;{{services.length === 0}}\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;empty-text\u0026#34;\u0026gt;暂无服务\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; 创建 pages/serviceList/serviceList.wxss：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 .container { padding: 20rpx; background: #f5f5f5; min-height: 100vh; } /* 搜索框 */ .search-box { display: flex; gap: 20rpx; margin-bottom: 30rpx; } .search-input { flex: 1; background: white; padding: 20rpx; border-radius: 10rpx; font-size: 28rpx; } .search-btn { background: #3498db; color: white; border: none; border-radius: 10rpx; padding: 0 30rpx; font-size: 28rpx; } /* 分类筛选 */ .category-scroll { white-space: nowrap; margin-bottom: 30rpx; } .category-list { display: inline-flex; gap: 20rpx; } .category-item { display: inline-block; padding: 15rpx 30rpx; background: white; border-radius: 30rpx; font-size: 28rpx; color: #666; } .category-item.active { background: #3498db; color: white; } /* 服务列表 */ .service-list { display: flex; flex-direction: column; gap: 20rpx; } .service-item { display: flex; background: white; border-radius: 20rpx; overflow: hidden; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); } .service-image { width: 200rpx; height: 200rpx; background: #ecf0f1; } .service-content { flex: 1; padding: 30rpx; display: flex; flex-direction: column; justify-content: space-between; } .service-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 15rpx; } .service-name { font-size: 32rpx; font-weight: bold; color: #2c3e50; flex: 1; margin-right: 20rpx; } .service-price { font-size: 32rpx; color: #e74c3c; font-weight: bold; } .service-merchant { font-size: 26rpx; color: #7f8c8d; margin-bottom: 15rpx; } .service-footer { display: flex; justify-content: space-between; align-items: center; } .service-category { font-size: 24rpx; color: #3498db; background: #ecf0f1; padding: 8rpx 20rpx; border-radius: 20rpx; } .service-stock { font-size: 24rpx; color: #95a5a6; } /* 空状态 */ .empty-state { text-align: center; padding: 100rpx 0; } .empty-text { font-size: 32rpx; color: #bdc3c7; } 第 7 步：开发创建订单页面 创建 pages/orderCreate/orderCreate.js：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 const request = require(\u0026#34;../../utils/request\u0026#34;); Page({ data: { service: null, userInfo: { name: \u0026#34;\u0026#34;, phone: \u0026#34;\u0026#34;, }, }, onLoad(options) { if (options.service) { try { const serviceStr = decodeURIComponent(options.service); const service = JSON.parse(serviceStr); this.setData({ service }); } catch (err) { // 3. 捕获解析错误，避免页面崩溃并提示用户 console.error(\u0026#34;服务信息解析失败：\u0026#34;, err); wx.showToast({ title: \u0026#34;服务信息错误\u0026#34;, icon: \u0026#34;none\u0026#34;, duration: 2000, }); // 解析失败时返回上一页 setTimeout(() =\u0026gt; { wx.navigateBack(); }, 2000); } } else { // 没有传递 service 参数时的提示 wx.showToast({ title: \u0026#34;未获取到服务信息\u0026#34;, icon: \u0026#34;none\u0026#34;, duration: 2000, }); setTimeout(() =\u0026gt; { wx.navigateBack(); }, 2000); } }, // 输入用户姓名 onNameInput(e) { this.setData({ \u0026#34;userInfo.name\u0026#34;: e.detail.value, }); }, // 输入用户电话 onPhoneInput(e) { this.setData({ \u0026#34;userInfo.phone\u0026#34;: e.detail.value, }); }, // 提交订单 async submitOrder() { const { service, userInfo } = this.data; if (!userInfo.name.trim()) { wx.showToast({ title: \u0026#34;请输入姓名\u0026#34;, icon: \u0026#34;none\u0026#34;, }); return; } if (!userInfo.phone.trim()) { wx.showToast({ title: \u0026#34;请输入手机号\u0026#34;, icon: \u0026#34;none\u0026#34;, }); return; } // 简单的手机号验证 const phoneRegex = /^1[3-9]\\d{9}$/; if (!phoneRegex.test(userInfo.phone)) { wx.showToast({ title: \u0026#34;请输入正确的手机号\u0026#34;, icon: \u0026#34;none\u0026#34;, }); return; } try { wx.showLoading({ title: \u0026#34;提交中...\u0026#34;, }); await request(\u0026#34;/order/create\u0026#34;, \u0026#34;POST\u0026#34;, { service_id: service.id, user_name: userInfo.name, user_phone: userInfo.phone, }); wx.hideLoading(); wx.showToast({ title: \u0026#34;订单创建成功！\u0026#34;, icon: \u0026#34;success\u0026#34;, duration: 2000, }); // 跳转到订单列表 setTimeout(() =\u0026gt; { wx.navigateTo({ url: \u0026#34;/pages/orderList/orderList\u0026#34;, }); }, 2000); } catch (err) { wx.hideLoading(); wx.showToast({ title: \u0026#34;订单创建失败\u0026#34;, icon: \u0026#34;none\u0026#34;, }); } }, }); 创建 pages/orderCreate/orderCreate.wxml：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 \u0026lt;view class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;!-- 服务信息 --\u0026gt; \u0026lt;view class=\u0026#34;service-card\u0026#34; wx:if=\u0026#34;{{service}}\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;card-title\u0026#34;\u0026gt;服务信息\u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;service-info\u0026#34;\u0026gt; \u0026lt;image class=\u0026#34;service-image\u0026#34; src=\u0026#34;{{service.image_url || \u0026#39;/images/default-service.png\u0026#39;}}\u0026#34;\u0026gt;\u0026lt;/image\u0026gt; \u0026lt;view class=\u0026#34;service-details\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;service-name\u0026#34;\u0026gt;{{service.name}}\u0026lt;/text\u0026gt; \u0026lt;text class=\u0026#34;service-merchant\u0026#34;\u0026gt;{{service.merchant_name}}\u0026lt;/text\u0026gt; \u0026lt;text class=\u0026#34;service-price\u0026#34;\u0026gt;¥{{service.price}}\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 用户信息表单 --\u0026gt; \u0026lt;view class=\u0026#34;form-card\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;card-title\u0026#34;\u0026gt;填写订单信息\u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;form-item\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;form-label\u0026#34;\u0026gt;姓名\u0026lt;/text\u0026gt; \u0026lt;input class=\u0026#34;form-input\u0026#34; placeholder=\u0026#34;请输入您的姓名\u0026#34; value=\u0026#34;{{userInfo.name}}\u0026#34; bindinput=\u0026#34;onNameInput\u0026#34; /\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;form-item\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;form-label\u0026#34;\u0026gt;手机号\u0026lt;/text\u0026gt; \u0026lt;input class=\u0026#34;form-input\u0026#34; placeholder=\u0026#34;请输入您的手机号\u0026#34; type=\u0026#34;number\u0026#34; value=\u0026#34;{{userInfo.phone}}\u0026#34; bindinput=\u0026#34;onPhoneInput\u0026#34; /\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 提交按钮 --\u0026gt; \u0026lt;view class=\u0026#34;submit-section\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;price-display\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;price-label\u0026#34;\u0026gt;总计：\u0026lt;/text\u0026gt; \u0026lt;text class=\u0026#34;price-amount\u0026#34;\u0026gt;¥{{service ? service.price : \u0026#39;0\u0026#39;}}\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;button class=\u0026#34;submit-btn\u0026#34; bindtap=\u0026#34;submitOrder\u0026#34;\u0026gt;立即下单\u0026lt;/button\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; 创建 pages/orderCreate/orderCreate.wxss：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 .container { padding: 20rpx; background: #f5f5f5; min-height: 100vh; padding-bottom: 200rpx; } /* 卡片样式 */ .service-card, .form-card { background: white; border-radius: 20rpx; padding: 30rpx; margin-bottom: 20rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); } .card-title { font-size: 32rpx; font-weight: bold; color: #2c3e50; margin-bottom: 30rpx; } /* 服务信息 */ .service-info { display: flex; gap: 30rpx; } .service-image { width: 120rpx; height: 120rpx; border-radius: 10rpx; background: #ecf0f1; } .service-details { flex: 1; display: flex; flex-direction: column; justify-content: space-between; } .service-name { font-size: 32rpx; font-weight: bold; color: #2c3e50; } .service-merchant { font-size: 26rpx; color: #7f8c8d; } .service-price { font-size: 36rpx; color: #e74c3c; font-weight: bold; } /* 表单样式 */ .form-item { display: flex; align-items: center; padding: 30rpx 0; border-bottom: 1rpx solid #ecf0f1; } .form-item:last-child { border-bottom: none; } .form-label { font-size: 30rpx; color: #2c3e50; width: 150rpx; } .form-input { flex: 1; font-size: 30rpx; color: #2c3e50; } /* 提交区域 */ .submit-section { position: fixed; bottom: 0; left: 0; right: 0; background: white; padding: 30rpx; display: flex; align-items: center; justify-content: space-between; box-shadow: 0 -2rpx 20rpx rgba(0, 0, 0, 0.1); } .price-display { display: flex; align-items: baseline; } .price-label { font-size: 28rpx; color: #2c3e50; margin-right: 10rpx; } .price-amount { font-size: 40rpx; color: #e74c3c; font-weight: bold; } .submit-btn { background: #e74c3c; color: white; border: none; border-radius: 50rpx; padding: 25rpx 60rpx; font-size: 32rpx; font-weight: bold; } 第 8 步：开发订单列表页面 创建 pages/orderList/orderList.js：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 const request = require(\u0026#34;../../utils/request\u0026#34;); Page({ data: { orders: [], }, onLoad() { this.getOrders(); }, onShow() { this.getOrders(); }, // 获取订单列表 async getOrders() { try { const orders = await request(\u0026#34;/order/list\u0026#34;); this.setData({ orders }); } catch (err) { wx.showToast({ title: \u0026#34;加载失败\u0026#34;, icon: \u0026#34;none\u0026#34;, }); } }, // 格式化时间 formatTime(timeString) { const date = new Date(timeString); return `${date.getFullYear()}-${(date.getMonth() + 1) .toString() .padStart(2, \u0026#34;0\u0026#34;)}-${date.getDate().toString().padStart(2, \u0026#34;0\u0026#34;)} ${date .getHours() .toString() .padStart(2, \u0026#34;0\u0026#34;)}:${date.getMinutes().toString().padStart(2, \u0026#34;0\u0026#34;)}`; }, }); 创建 pages/orderList/orderList.wxml：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 \u0026lt;view class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;header\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;title\u0026#34;\u0026gt;我的订单\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;order-list\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;order-item\u0026#34; wx:for=\u0026#34;{{orders}}\u0026#34; wx:key=\u0026#34;id\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;order-header\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;order-no\u0026#34;\u0026gt;订单号: {{item.id}}\u0026lt;/text\u0026gt; \u0026lt;text class=\u0026#34;order-status {{item.status === 0 ? \u0026#39;pending\u0026#39; : \u0026#39;completed\u0026#39;}}\u0026#34;\u0026gt; {{item.status === 0 ? \u0026#39;待支付\u0026#39; : \u0026#39;已完成\u0026#39;}} \u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;order-content\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;service-info\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;service-name\u0026#34;\u0026gt;{{item.service_name}}\u0026lt;/text\u0026gt; \u0026lt;text class=\u0026#34;merchant-name\u0026#34;\u0026gt;{{item.merchant_name}}\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;text class=\u0026#34;service-price\u0026#34;\u0026gt;¥{{item.price}}\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;order-footer\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;user-info\u0026#34;\u0026gt;{{item.user_name}} · {{item.user_phone}}\u0026lt;/text\u0026gt; \u0026lt;text class=\u0026#34;order-time\u0026#34;\u0026gt;{{formatTime(item.create_time)}}\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;empty-state\u0026#34; wx:if=\u0026#34;{{orders.length === 0}}\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;empty-text\u0026#34;\u0026gt;暂无订单\u0026lt;/text\u0026gt; \u0026lt;text class=\u0026#34;empty-desc\u0026#34;\u0026gt;去首页看看有什么服务吧\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; 创建 pages/orderList/orderList.wxss：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 .container { padding: 20rpx; background: #f5f5f5; min-height: 100vh; } .header { text-align: center; margin: 40rpx 0; } .title { font-size: 48rpx; font-weight: bold; color: #2c3e50; } .order-list { display: flex; flex-direction: column; gap: 20rpx; } .order-item { background: white; border-radius: 20rpx; padding: 30rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); } .order-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; padding-bottom: 20rpx; border-bottom: 1rpx solid #ecf0f1; } .order-no { font-size: 26rpx; color: #7f8c8d; } .order-status { font-size: 26rpx; font-weight: bold; padding: 8rpx 20rpx; border-radius: 20rpx; } .order-status.pending { background: #fff3cd; color: #856404; } .order-status.completed { background: #d4edda; color: #155724; } .order-content { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20rpx; } .service-info { flex: 1; } .service-name { display: block; font-size: 32rpx; font-weight: bold; color: #2c3e50; margin-bottom: 10rpx; } .merchant-name { font-size: 26rpx; color: #7f8c8d; } .service-price { font-size: 36rpx; color: #e74c3c; font-weight: bold; } .order-footer { display: flex; justify-content: space-between; align-items: center; padding-top: 20rpx; border-top: 1rpx solid #ecf0f1; } .user-info { font-size: 26rpx; color: #2c3e50; } .order-time { font-size: 24rpx; color: #95a5a6; } .empty-state { text-align: center; padding: 100rpx 0; } .empty-text { display: block; font-size: 32rpx; color: #bdc3c7; margin-bottom: 20rpx; } .empty-desc { display: block; font-size: 28rpx; color: #bdc3c7; } 第 9 步：配置小程序入口 修改 app.js：\n1 2 3 4 5 6 7 8 9 App({ onLaunch() { console.log(\u0026#34;小程序启动\u0026#34;); }, globalData: { userInfo: null, }, }); 🧪 阶段五：集成测试与上线 第 1 步：完整流程测试 测试流程： 启动所有服务：\n1 2 3 4 5 6 7 # 终端1 - 后端 cd D:/life-service/server npm run dev # 终端2 - 前端 cd D:/life-service/web-admin npm run serve 在 DataGrip 中监控数据：\n实时查看表数据变化 验证外键关系 测试完整业务流程：\nWeb 端：添加商家 → 添加服务 小程序：浏览服务 → 下单 Web 端：查看订单 第 2 步：代码优化 后端优化： 添加参数验证 错误处理完善 添加日志记录 前端优化： 加载状态提示 错误边界处理 表单验证加强 第 3 步：部署准备 整理项目文档： 创建 README.md：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 # 全域生活服务平台 ## 项目介绍 一个完整的生活服务平台，包含 Web 管理后台和微信小程序。 ## 技术栈 - 后端：Node.js + Express + MySQL - 前端：Vue 3 + Vue Router - 小程序：微信小程序原生开发 - 数据库：MySQL 8.0 - 工具：DataGrip ## 启动步骤 ### 后端 1. cd server 2. npm install 3. 修改 db/index.js 中的数据库密码 4. npm run dev ### Web 管理后台 1. cd web-admin 2. npm install 3. npm run serve ### 小程序 1. 微信开发者工具中打开 mini-user 文件夹 2. 点击预览 第 4 步：Git 版本控制 1 2 3 4 5 6 7 8 9 10 11 12 13 # 初始化Git cd D:/life-service git init # 添加所有文件 git add . # 提交代码 git commit -m \u0026#34;初始提交：完成全域生活服务平台开发\u0026#34; # 推送到GitHub/Gitee git remote add origin 你的仓库地址 git push -u origin main 后续的更新统管理 1 2 3 git init git add . git commit -m \u0026#34;update\u0026#34; 🎉 项目完成！ 你已掌握的技能： ✅ 全栈开发：前端 + 后端 + 数据库\n✅ 多端开发：Web + 微信小程序\n✅ 数据库设计：MySQL 表设计与关系\n✅ API 开发：RESTful 接口设计\n✅ 工具使用：DataGrip 数据库管理\n面试亮点： \u0026ldquo;独立完成从数据库设计到前后端开发的全流程\u0026rdquo; \u0026ldquo;掌握 Vue + Node.js + MySQL 技术栈整合\u0026rdquo; \u0026ldquo;具备多端开发能力（Web + 小程序）\u0026rdquo; \u0026ldquo;使用 DataGrip 进行专业的数据库管理\u0026rdquo; 下一步建议： 功能扩展：添加用户登录、支付功能 性能优化：添加缓存、分页查询 部署上线：购买云服务器部署项目 持续学习：学习 TypeScript、Docker 等进阶技术 ","date":"2025-10-20T16:36:00Z","permalink":"https://ye-guan-xing.github.io/p/%E5%85%A8%E5%9F%9F%E7%94%9F%E6%B4%BB%E6%9C%8D%E5%8A%A1%E5%B9%B3%E5%8F%B0%E5%BC%80%E5%8F%91%E6%B5%81%E7%A8%8B%E8%AF%A6%E8%A7%A3/","title":"全域生活服务平台开发流程详解"},{"content":"大一新生 IT 学习路线指南 亲爱的大一新生，刚踏入大学，面对丰富多彩的 IT 领域或许会有些迷茫，这里学长给一些路径建议：\n算法竞赛 -算法竞赛 1970 年代起步，1990-2000 年代兴盛，21 世纪后趋稳定,其中 ACM-ICPC 为计算机竞赛含金量之最，获奖者多能获得计算机大厂 offer。\n链接：算法竞赛 PS(如果有天赋，有恒心，有毅力，在经过测验后(或 Codeforces1200 分以上)，可以找我们参加明年的 HBCPC). 白名单竞赛推荐 竞赛名称 官网链接 ACM-ICPC 国际大学生程序设计竞赛 https://icpc.global/ 中国大学生程序设计竞赛（CCPC） https://ccpc.io/ 中国高校计算机大赛（天梯赛） http://www.c4best.cn/ 蓝桥杯全国软件和信息技术专业人才大赛 http://dasai.lanqiao.cn/ 百度之星程序设计大赛 https://star.baidu.com/#/ 码蹄杯全国职业院校程序设计大赛 https://matiji.net/matibei 睿抗机器人开发者大赛(RAICOM) https://www.robocom.com.cn/ 人工智能方向 在 50-60 年代兴起，人们一直在努力，于 2012 引爆深度学习，在之后，CharGPT 和 Deepseek、GPT3 等模型，被用于各种领域，如图像识别、语音识别、文本生成、代码生成、翻译等，到现在已成为最热门的方向，其就业岗位，薪资高、门槛高\u0026ndash;多在研究生为主 机器学习 学习路线链接：机器学习 简介：机器学习是人工智能的核心，通过算法让计算机从数据中“学习规律”。你将学习决策树、支持向量机等各类机器学习算法，并用它们解决分类、回归、聚类等实际问题。 深度学习 学习路线链接：深度学习 简介：深度学习是机器学习的“进阶版”，通过神经网络模拟人脑学习。你将学习 CNN（卷积神经网络）、RNN（循环神经网络）等网络结构，应用于图像识别、语音识别、自然语言处理等复杂场景。 自然语言处理（NLP） 学习路线链接：自然语言处理 NLP 简介：NLP 让计算机“理解人类语言”。学习后你可以掌握分词、语义分析、情感识别等技术，开发聊天机器人、智能问答、机器翻译等应用。 计算机视觉（CV）工程师 学习路线链接：计算机视觉 cv 工程师 简介：CV 让计算机“看见并理解世界”。你将学习图像处理、目标检测、图像识别等技术，应用于自动驾驶、人脸识别、工业质检等领域。 前端开发 路线图链接：前端开发的路线图 简介：前端开发聚焦于网页的“视觉与交互”，从精美界面到灵动交互效果都由前端工程师打造。就业市场上，前端开发起步于 2000 年后（HTML/CSS 普及、早期 JavaScript 简单应用落地），2015 年后随 React/Vue/Angular 等框架爆发进入鼎盛期（企业对复杂交互、单页应用需求激增），2020 年后需求趋于稳定（框架生态成熟，侧重工程化、跨端适配与性能优化）。跟着这份路线图，你可以系统学习 HTML、CSS、JavaScript 及各类前端框架，成长为能设计并实现炫酷网页的前端开发者。 Java 后端开发 简介：Java 后端开发是支撑企业业务逻辑的“核心骨架”，负责搭建服务器、处理数据交互与业务逻辑实现。就业市场上，Java 后端起步于 2000 年左右（Java EE 推出，企业级应用逐步采用），2010-2020 年随微服务兴起、Spring 生态成熟进入鼎盛期（成为各行业后端开发标配，需求爆发式增长），2020 年后进入稳定期（需求保持高位稳定，侧重高并发、云原生适配与系统安全性）。学习这条路线，你会掌握 Java 核心语法、框架应用与服务器部署等知识，成长为能搭建稳定、高效企业后端系统的开发工程师。 数据分析 学习路线链接：数据分析学习路线 简介：数据分析是从海量数据中挖掘价值的“金矿挖掘术”。就业市场上，数据分析岗位起步于 2010 年左右（数据量激增，企业需专职人员提炼数据价值），2018-2023 年随大数据技术成熟进入鼎盛期（各行业重视数据驱动，需求爆发），2023 年后进入稳定期（需求平稳，侧重业务理解、数据建模与可视化深度）。通过这条路线，你将掌握 Python、SQL 等数据处理工具，以及数据分析方法和可视化技术，成为能从数据中发现规律、辅助业务决策的分析师。 移动开发 安卓开发 学习路线链接：安卓开发路线 简介：安卓开发专注于安卓手机应用的开发。就业市场上，安卓开发起步于 2008 年（安卓系统发布后，早期手机 APP 开发需求出现），2012-2018 年随智能手机普及进入鼎盛期（社交、工具、电商类 APP 爆发，岗位需求旺盛），2018 年后进入稳定期（需求趋于平稳，侧重跨端适配、性能优化与鸿蒙生态兼容）。学习后你可以掌握安卓开发框架、UI 设计、性能优化等知识，打造各类安卓应用（如社交 APP、工具类 APP）。 鸿蒙开发 学习路线链接：鸿蒙开发路线 简介：鸿蒙是面向全场景的分布式操作系统，学习鸿蒙开发可以让你掌握其开发框架和生态，开发跨手机、平板、智能手表等设备的应用。就业市场上，鸿蒙开发起步于 2020 年（鸿蒙 OS 2.0 发布，生态逐步搭建），当前处于快速发展期（企业加速布局鸿蒙应用，需求逐步增长，尚未进入鼎盛），未来随生态完善（设备覆盖扩大、开发工具成熟）将进入稳定期，具备鸿蒙开发能力的人才在物联网、智能设备领域竞争力突出。 ","date":"2025-10-20T16:36:32+08:00","permalink":"https://ye-guan-xing.github.io/p/%E5%A4%A7%E4%B8%80%E7%9A%84%E5%AD%A6%E4%B9%A0%E8%B7%AF%E5%BE%84%E8%BD%AF%E4%BB%B6%E7%AF%87/","title":"大一的学习路径（软件篇）"}]