news 2026/10/5 11:22:06

Vue 3实战:自习室座位预约系统开发与部署全记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue 3实战:自习室座位预约系统开发与部署全记录

上个月帮学校图书馆做了一套自习室座位预约系统,正赶上期末季“一座难求”的节点上线,每天几千个学生同时进来抢座。前端用 Vue 3 写,从 Vite 初始化到打包放进后端服务里,走完了一整条生产线。这套系统算不上大,但麻雀虽小,路由设计、组件拆分、状态管理、实时推送、打包部署这些前端高频技能点全占了。这篇就把整个实现过程和踩过的坑讲清楚,适合已经会 Vue 基础语法、想做点真实项目练手的同学,也适合要在课程设计里交一套可视化系统的朋友。

1. 自习室预约系统要解决的三个核心问题

1.1 业务需求拆解:预约系统不只是“选个座”

做系统之前,我先把“自习室预约”这件事掰开揉碎看了一眼。表面上就是学生选时间选座位,但落到业务上,至少要覆盖这五个环节:

  • 用户登录与身份识别:学生和自习室管理员的权限完全不同,学生能预约、取消、查记录,管理员要看整体占用率、释放超时座位。
  • 座位与时间段建模:一个自习室有几十个座位,一天又拆成早中晚多个时段,座位和时段必须组合绑定,才能判断“这个座这个时段被占了没有”。
  • 预约与释放流程:正常预约要有占用校验,取消后座位要能马上释放给其他人,超时未签到还要有惩罚或强制释放逻辑。
  • 可视化展示:用户需要一眼看清楚哪些座位可约,而不是看一张文字表格。这就逼着前端做一个座位地图。
  • 记录与统计:预约历史、高峰时段分析,这些是图书馆老师明确要的功能,也是加分项。

我见过不少新手一上来就写“选座位页面”,结果做到一半才发现时段和座位是两个维度,数据模型没设计好,后面全部返工。这套系统的核心不在页面,而在“座位状态”的实时一致性,前端再花哨,数据对不上就全白搭。

1.2 技术选型为什么是 Vue 而不是其他

选 Vue 3 作为前端框架,有很现实的原因:

  • 中文社区资料最全,遇到问题搜出来的答案能直接看懂;
  • Element Plus 这类组件库成熟,日期选择器、表格、表单校验开箱即用,能省掉大量基础组件编写时间;
  • Vue Router 和 Pinia 足以支撑这套系统的路由和状态管理需求,不需要引 Redux 级别的重型状态库;
  • 项目规模适中,Vue 的渐进式特性很合适,从零到一搭起来快。

具体技术栈清单如下:

模块选型用途
构建工具Vite项目初始化、开发热更新、打包优化
框架Vue 3 (Composition API)页面开发、组件组织
路由Vue Router 4页面跳转、动态路由、权限控制
状态管理Pinia用户信息、预约状态共享
UI 组件库Element Plus表单、日期选择、弹窗、表格
HTTP 请求Axios与后端接口通信
实时通信SSE (EventSource)座位状态实时刷新

这套组合的另一个好处是:一个人写也不会失控。组件层级清晰,状态只存在 Pinia 的 store 里,页面上不堆逻辑。

1.3 项目整体模块划分

我按功能域把前端分成了六个页面区:登录、自习室列表、座位预约、我的预约、管理后台、个人中心。对应路由结构:

路由路径页面功能
/login登录页账号密码登录,保存 Token
/rooms自习室列表展示可用自习室、座位总数、空闲数
/rooms/:id座位预约页座位地图、日期时段选择、确认预约
/reservations我的预约查看记录、取消预约
/admin管理后台座位管理、释放超时座位、统计报表
/profile个人中心个人信息、修改密码

模块化设计决定了后面数据模型怎么写,也决定了动态路由往哪些方向挂,这一点建议动手前先画出来,哪怕画在草稿纸上。

2. Vite 初始化与依赖安装的实操细节

2.1 从零创建 Vue 3 项目

创建项目我用的是 Vite,命令行操作非常直接:

npm create vite@latest study-room-front -- --template vue cd study-room-front npm install npm run dev

这里有个容易被忽略的点:--template vue生成的是 Vue 3 + JavaScript 版本,如果你平时写的是 TypeScript,把最后的vue换成vue-ts。我建议课程设计或中小型项目用 JS 就够了,TypeScript 会带来额外的类型标注成本,不是必需。

项目结构初始化好后,我会立刻删掉默认的HelloWorld.vue和style.css里的模板冗余代码,再按自己的模块规划建文件夹:

src/ ├── api/ # 接口请求封装 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── layout/ # 页面布局组件 ├── router/ # 路由配置 ├── stores/ # Pinia 状态 └── views/ # 页面组件

这一步看似简单,但它决定了后面代码好不好找。我见过很多项目把所有页面堆在views里、所有请求散落在页面里,改一个接口要全局搜索,排错体验极差。

2.2 依赖清单与版本坑

安装依赖时我踩过一个很典型的问题:直接npm install vue-router@latest装到了 Vue Router 5 的测试版,结果和 Vue 3 不兼容,运行直接报错。所以建议按明确版本安装:

npm install vue-router@4 pinia axios npm install element-plus @element-plus/icons-vue

进package.json确认一下版本号:

{ "dependencies": { "vue": "^3.4.0", "vue-router": "^4.3.0", "pinia": "^2.1.7", "axios": "^1.7.0", "element-plus": "^2.7.0" } }

如果安装过程中遇到依赖冲突,最实用的办法是先删掉node_modules和package-lock.json,再重新装一遍,不要盲目改版本号。

2.3 npm 安装缓慢与源配置

很多新手卡在npm install这一步,等十几分钟不动,或者报各种网络超时。这通常不是代码问题,而是默认源慢。我一般会先检查源:

npm config get registry

如果返回的不是常见的国内镜像源,可以直接改配置:

npm config set registry https://registry.npmmirror.com

再执行安装,速度会明显提升。这里要提醒一句:改源只是降低网络延迟,不涉及任何特殊网络操作,按这个配置填写即可。

2.4 引入 Element Plus 的正确姿势

Element Plus 有两种引入方式。项目不大,我直接全量引入,省心:

// main.js import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import zhCn from 'element-plus/es/locale/lang/zh-cn' import App from './App.vue' const app = createApp(App) app.use(ElementPlus, { locale: zhCn }) app.mount('#app')

注意这里一定要引入zh-cn语言包,不然日期选择器、分页器这些组件显示的都是英文。全量引入会让打包体积变大,但对课程设计、内部系统完全够用,等真的需要优化再换成按需导入。

3. 页面布局与组件拆分:插槽的正确打开方式

3.1 用布局组件统一三端页面结构

预约系统的页面分为学生端和个人端两大块,但整体结构是统一的:顶部导航栏、左侧菜单(管理员才有)、右侧内容区。这个统一结构我用一个Layout.vue组件承载,里面大量使用了插槽。

<!-- src/layout/AdminLayout.vue --> <template> <div class="layout"> <header class="layout-header"> <!-- 具名插槽:顶部栏内容由父组件决定 --> <slot name="header">默认标题</slot> </header> <aside class="layout-side"> <slot name="sidebar"></slot> </aside> <main class="layout-content"> <!-- 默认插槽:放页面主体 --> <slot></slot> </main> </div> </template>

这样设计的好处是:登录后不管是学生页面还是管理后台,都复用同一个外壳,只是往插槽里喂不同的内容。如果以后要把侧边栏从左边换到右边,只改布局组件一个文件就行,业务页面完全不用动。

3.2 作用域插槽:座位状态卡片的自定义渲染

座位预约页的核心组件是SeatGrid.vue,它接收座位数组,按矩阵渲染出格子。难点在于:座位有四种状态(空闲、已约、待签到、维修中),每种状态的卡片颜色和操作按钮都不一样。

这里如果让子组件内部写死渲染逻辑,扩展性会很差。我用了作用域插槽让父页面决定每个座位格长什么样:

<!-- SeatGrid.vue --> <template> <div class="seat-grid"> <div v-for="seat in seats" :key="seat.id" class="seat-cell"> <slot name="seat" :seat="seat"> <!-- 默认渲染:按状态显示颜色 --> <div :class="['seat-default', statusClass(seat.status)]"> {{ seat.seatNo }} </div> </slot> </div> </div> </template> <script setup> const props = defineProps({ seats: { type: Array, required: true } }) const statusClass = (status) => { const map = { available: 'seat-available', booked: 'seat-booked', pending: 'seat-pending', maintain: 'seat-maintain' } return map[status] || 'seat-available' } </script>

在预约页面调用时,用v-slot拿到当前座位的完整数据:

<SeatGrid :seats="currentSeatList"> <template #seat="{ seat }"> <el-tooltip :content="seat.tips"> <el-button :type="seat.status === 'available' ? 'primary' : 'info'" :disabled="seat.status !== 'available'" @click="selectSeat(seat)" > {{ seat.seatNo }} </el-button> </el-tooltip> </template> </SeatGrid>

作用域插槽的最大价值,是把“数据展示”和“交互逻辑”的边界划清楚了。座位网格只负责遍历、布局、传数据,至于格子是按钮、卡片还是图表,完全由上层业务决定,测试也方便。

3.3 预约表单的字段设计与校验

预约就是让用户选择日期、时段、座位号。这个表单放在座位地图上方,字段不多,但校验要求明确:

  • 日期不能是过去时间;
  • 时段必须二选一或三选一,且只能选一个;
  • 必须先从地图上点了某个座位,才能提交,且已约座位不能再点。

官方文档里的<el-form>校验方式足够用。我加了一条自定义校验规则,保证“座位编号在提交前已经选定”:

const rules = { date: [{ required: true, message: '请选择日期', trigger: 'change' }], timeSlot: [{ required: true, message: '请选择时间段', trigger: 'change' }], seatNo: [{ validator: (rule, value, callback) => { if (!selectedSeat.value?.id) { callback(new Error('请先在平面图上选择座位')) } else { callback() } }, trigger: 'change' }] }

日期选择我用<el-date-picker>加disabled-date禁掉过去日期,这一步能省不少后端校验压力。表单校验做在前端,不纯粹是用户体验,也是给后端减轻无效请求的负担。

4. 预约流程与座位状态管理:从轮询到 SSE 实时推送

4.1 座位状态的数据模型怎么设计

前端管理座位状态,最重要的是把“后端数据”和“本地交互”区分开。我的 Pinia store 里维护了这样的结构:

// stores/room.js export const useRoomStore = defineStore('room', { state: () => ({ roomDetail: null, date: new Date().toISOString().slice(0, 10), timeSlot: 'morning', seats: [], selectedSeat: null, loading: false }), getters: { availableCount: (state) => state.seats.filter(s => s.status === 'available').length, bookedCount: (state) => state.seats.filter(s => s.status === 'booked').length }, actions: { async fetchSeats(date, timeSlot) { this.loading = true try { const res = await api.getSeats(this.roomDetail.id, { date, timeSlot }) this.seats = res.data } finally { this.loading = false } }, selectSeat(seat) { if (seat.status !== 'available') return if (this.selectedSeat?.id === seat.id) { this.selectedSeat = null } else { this.selectedSeat = seat } } } })

日期或时段一变化,seats数组就整体刷新。这个设计的好处是很容易扩展:如果以后要把维修中的座位排除在外,只需要在getters里多加一个派生数据,页面配合改一下渲染逻辑。

4.2 实时刷新座位状态:SSE 比轮询靠谱在哪

开发初期我用了定时轮询,每 5 秒请求一次座位状态接口。结果高峰期数据库压力不小,而且有 5 秒延迟,经常出现两个人同时抢同一个座位的现象。

后来我改成 SSE(Server-Sent Events),后端在座位状态变更时主动把最新数据推给前端,前端彻底摆脱了定时器。

前端实现很简单:

// utils/sse.js export function createSeatSse(roomId, callback) { const eventSource = new EventSource(`/api/sse/seat-stream?roomId=${roomId}`) eventSource.onmessage = (event) => { const data = JSON.parse(event.data) callback(data) } eventSource.onerror = () => { // 网络异常会自动重连,这里只需补一个提示 console.warn('SSE 连接异常,等待重连...') } return eventSource }

在预约页挂载时建立连接,离开时关闭:

let source = null onMounted(() => { source = createSeatSse(roomId, (message) => { if (message.type === 'SEAT_STATUS_CHANGE') { roomStore.updateSeatStatus(message.seatId, message.status) } }) }) onBeforeUnmount(() => { source?.close() })

使用 SSE 之后,座位状态延迟基本降到秒级,两个人抢座时,后到的人能立刻看到座位变红,体验好了不少。需要注意,SSE 是单向推送,只能后端往前端发,前端控制指令仍然走普通 HTTP 接口。

4.3 防重复提交与业务状态机

预约系统最怕的就是连续点两次提交按钮,生成两条预约。前端我已经做了两个防线:

  • 按钮提交后立刻禁用,直到后端返回结果再恢复;
  • 提交期间显示 loading 状态,从视觉上杜绝连续操作。
const submitting = ref(false) const handleSubmit = async () => { if (submitting.value) return submitting.value = true try { await api.createReservation(payload) ElMessage.success('预约成功') await roomStore.fetchSeats(roomStore.date, roomStore.timeSlot) } catch (err) { ElMessage.error(err.response?.data?.message || '预约失败') } finally { submitting.value = false } }

但前端防重复是软防,关键硬保证在后端:预约接口必须做幂等控制,比如同一个座位同一个时段只能存在一条有效预约。我习惯把后端业务做成一整套状态机:

当前状态操作目标状态
待签到取消预约已释放
待签到超过签到时间未到已违约
已签到使用结束已完成
维修中维修完成空闲

前端拿到这些状态码,做对应的文案和按钮渲染就够了。核心原则是:前端只负责状态展示,真正的状态转换永远由后端决定。

5. 前后端联调、动态路由与权限控制

5.1 Axios 实例封装与统一错误处理

预约系统的请求不算复杂,但登录 Token、错误提示、加载状态这些逻辑如果散落在每个页面,后期维护会很难受。我在src/api/request.js里做了一个统一封装:

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const instance = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动带 Token instance.interceptors.request.use((config) => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截器:统一处理业务错误 instance.interceptors.response.use( (response) => response.data, (error) => { if (error.response?.status === 401) { localStorage.removeItem('token') router.push('/login') } else { ElMessage.error(error.response?.data?.message || '网络异常') } return Promise.reject(error) } ) export default instance

这样做的直接收益是:后端调整返回结构时,我只需要改动截器里的response.data取值方式,全项目所有请求均同步生效。页面里的请求代码也简洁很多:

const res = await request.get('/rooms')

5.2 基于角色的动态路由注册

这个系统里学生和管理员能访问的页面不一样。如果直接把所有路由写死并全部注册,那么普通学生虽然看不到后台入口,但手动输入 URL 还是能进管理页。所以我把权限控制做到了路由层。

先在router/index.js里只注册公开路由,比如登录页;然后登录成功后,根据用户角色动态追加路由:

// router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/rooms', component: () => import('@/views/RoomList.vue') } ] const router = createRouter({ history: createWebHistory(), routes }) // 动态添加管理员路由 export function setupAdminRoutes() { router.addRoute({ path: '/admin', component: () => import('@/layout/AdminLayout.vue'), children: [ { path: '', component: () => import('@/views/admin/AdminDashboard.vue') }, { path: 'seats', component: () => import('@/views/admin/SeatManage.vue') } ] }) }

登录后获取到用户角色时,如果是管理员就调用setupAdminRoutes()。再配合全局前置守卫做一层校验:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { next('/login') return } if (token && to.path === '/login') { next('/rooms') return } next() })

动态路由这个点也是面试里经常被问到的细节。真正做权限系统时,后端通常会返回该用户可访问的路由表,前端用router.addRoute一条条挂载,比写死前端路由再判断角色要灵活得多。

5.3 联调阶段的跨域处理:Vite Proxy 的配置

前后端分开开发时,跨域是必踩的一关。最简单的方式是在 Vite 配置里加代理,把/api前缀的请求转发给后端开发地址:

// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

配置后,前端代码里请求写/api/rooms,开发时 Vite 会自动转发到http://localhost:8080/rooms上,浏览器里看不到跨域报错,也不需要后端开启 CORS 通配。上线时再用 Nginx 或 SpringBoot 做同样路径的转发,前端页面代码一行不用改。

提示:如果后端要求所有接口都带/api前缀,那么代理只写/api即可;如果后端接口没有统一前缀,可以在 Axios 的 baseURL 里补齐。

6. 打包部署进 SpringBoot 的完整实操记录

6.1 Vite 构建配置与静态资源路径

本地开发一切正常,但部署是个大头。很多同学第一次把 Vue 项目打包后放到后端项目里,打开页面发现样式全没,或者路由直接 404。这大概率是 Vite 的base配置没设置。

默认情况下,Vite 打包出的资源路径是绝对路径(/assets/...),但后端项目如果部署在域名的子路径下(比如/study/),这些资源请求会直接 404。我改成相对路径:

// vite.config.js export default defineConfig({ base: './', build: { outDir: 'dist' } })

这样打包后index.html里引用的资源路径就是./assets/...,无论把静态文件放到哪个目录,都能通过相对路径找到资源。

6.2 打包产物怎么放进 SpringBoot

我这里的后端是 SpringBoot,最省事的托管方式是直接把前端打包产物放到src/main/resources/static/目录下。步骤很简单:

  1. 前端执行npm run build,生成dist目录;
  2. 将dist目录下的文件全部复制到后端项目的static目录;
  3. 重新打包运行 SpringBoot,访问http://localhost:8080/就能直接打开前端页面。

但这里有个我绕了很久的坑:如果前端路由用的是 history 模式(HTML5 Mode),部署后访问/rooms这种前端路由地址,浏览器会向后端发一次真正的 HTTP 请求,而后端没有这个路径的接口,返回 404,页面白屏。

解决办法有两个:

方案一:前端路由改 hash 模式,这也是最省心的:

const router = createRouter({ history: createWebHashHistory(), routes })

改完以后,前端路由地址带#号,比如http://localhost:8080/#/rooms,这个地址始终会请求/index.html,天然规避了 404 问题。

方案二:后端配置转发,把非接口路径全部转到静态首页。在 SpringBoot 里加一个简单的资源转发配置:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/{path:[^\\.]*}") .setViewName("forward:/index.html"); } }

这个配置保证所有不带.的路径都回滚到index.html,再交给前端路由去解析,同时.js、.css等带点号的请求还能正常走静态资源。

两种方案怎么选?如果没有特殊的 SEO 需求,内部管理系统优先选 hash 模式,稳定省心。

6.3 打包体积优化与接口地址适配

打包部署没问题后,我又做了两件优化。

第一件是路由懒加载。如果直接 import 所有页面,打完包生成的 JS 可能有 1MB 以上,首屏加载会很慢。把页面组件改成动态导入后,Vite 会自动按路由拆代码块,用户访问哪页就下载哪页的代码:

const routes = [ { path: '/rooms/:id', component: () => import('@/views/RoomDetail.vue') } ]

第二件是接口地址的适配。上线后前端的baseURL不能再写http://localhost:8080,我把接口前缀统一写成/api,然后通过代理层转发到真正的后端服务。可以用 Vite 环境变量区分开发与生产:

# .env.development VITE_API_BASE=/api # .env.production VITE_API_BASE=/api

前端代码里配合 Axios 的baseURL读取:

baseURL: import.meta.env.VITE_API_BASE || '/api'

这样开发和生产环境前端代码保持一致,差异只在部署的代理配置上体现。

6.4 上线前最后的体检清单

结合这次实际上线经验,我整理了自习室预约系统上线前的检查项,避免上线后手忙脚乱:

检查项预期结果原因
刷新/reservations页面页面正常不白屏确认路由模式与后端转发正确
手机尺寸访问布局不溢出,按钮可点学生多用手机端,需要一定响应式处理
同一账号双开页面抢同一座位只有一个能预约成功验证后端幂等,前端提示合理
管理员释放座位学生端 2 秒内看到状态变化验证 SSE 推送链路
关闭后端服务后访问前端页面正常打开,接口报错提示友好静态资源与后端逻辑分离,错误拦截器正常
预约记录分页数据不重复、不漏确认后端分页参数与前端组件对接一致

这套系统从开发到上线前后花了两周多时间,最大的体会是:Vue 项目最容易出问题的从来不是某个 API 不会用,而是工程化链条上那些零碎的细节——路由模式、资源路径、请求代理、状态一致性。把这些串起来,才是真正把项目从“本地能跑”变成“线上可用”的关键。

最后说一个个人习惯:每次改动前后端联调接口时,我都会先在浏览器 Network 面板里确认请求路径和响应结构,排错效率比直接看代码高不少。如果你也正准备做类似的预约系统,建议先用思维导图把数据模型画明白,再开始写 Vue 组件,后面能省一大半返工时间。

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

Base64 为什么会让数据涨三分之一:原理、长度计算与几个踩过的坑

Base64 大概是所有编码里「用法人人都会、原理少有人讲清」的典型。大多数人第一次接触它是在传图片或者调接口时&#xff0c;照着抄一行代码就完事了&#xff1b;等到某天发现上传的体积莫名超标、或者某段字符串解不开&#xff0c;才开始回头问&#xff1a;它到底在干什么。 …

作者头像 李华
网站建设 2026/10/5 11:20:37

从MusicFree到IAR:插件机制与加载失败排查实战

如果你最近也刷到过musicfree plugins、iar plugins 是干什么的、failed to load plugins web boot: 2 entries did not activate这类热词&#xff0c;却说不清插件到底在玩什么名堂&#xff0c;那这篇东西就是写给你看的。我把“plugins”这个词当成一个横切面来拆&#xff1a…

作者头像 李华
网站建设 2026/10/5 11:18:20

Visual C++ DirectX仿暗黑RPG源码解析:从编译到运行

简介&#xff1a;这份资源是面向C游戏开发初学者与进阶者的仿Diablo暗黑破坏神RPG游戏完整源代码&#xff0c;基于Visual C与DirectX技术栈实现&#xff0c;可用于学习2D/3D图形渲染、游戏逻辑架构与资源管理等核心开发技能。压缩包共126个文件&#xff0c;约721KB&#xff0c;…

作者头像 李华
网站建设 2026/10/5 11:18:20

人力替代技术演进史:从机械臂到AI大模型与具身智能

1. 人力替代技术&#xff1a;从机械臂到脑机接口的演进主线“人力替代技术”这个词&#xff0c;这几年被频繁提起&#xff0c;但很多人对它的理解还停留在“机器人抢饭碗”这种新闻标题上。我自己从做自动化产线起步&#xff0c;后来转向智能流程系统&#xff0c;亲眼看着这门技…

作者头像 李华
网站建设 2026/10/5 11:16:26

西电程序设计基础课程设计全流程指南:从选题到答辩

刚把程序设计基础的所有题目肝完&#xff0c;又看到课程设计任务书的同学&#xff0c;我懂你现在的心情。网上搜“西电 程序设计基础课程设计”&#xff0c;出来的多是零散代码片断或者师兄师姐的只言片语&#xff0c;信息碎得像散装零件&#xff0c;拼不出一个能跑通全流程的方…

作者头像 李华
网站建设 2026/10/5 11:16:12

汇编视角下的While循环逆向分析:特征识别与实战技巧

拿到一个未知样本或固件&#xff0c;在IDA里跟着控制流绕来绕去&#xff0c;最让我头疼的不是各种花指令&#xff0c;也不是混淆过的函数调用&#xff0c;反而是那些看起来平平无奇的循环结构。尤其是While循环——它不像for循环那样自带“初始化、条件、增量”的显眼三件套&am…

作者头像 李华