
简介本资源是一套基于Vue.js开发的智能会议室预约系统前端完整源码面向前端初学者、企业信息化开发者及Vue技术实践者旨在解决传统人工预约效率低、易冲突、难追溯等管理痛点适用于中小型企业或办公场景的数字化会议调度需求。压缩包共37个文件含15个Vue组件实现会议室列表、详情、搜索、预约表单等核心界面、6个JavaScript逻辑脚本处理筛选、表单提交、状态管理、5个CSS样式文件保障响应式布局与视觉一致性以及JSON配置、HTML入口页、图标与文档等配套资源整体体积仅962KB轻量易部署。已有322人学习下载资源结构清晰、模块职责分明涵盖Vite工程配置、Pinia状态管理见stores/counter.js、路由控制router/index.js及BootstrapIcon集成可直接运行调试是理解Vue组件化开发、前后端分离架构落地的优质实战范例。 公司行政那边天天在群里喊“谁订了会议室又不来”技术团队想开个评审会找不到有空档的会议室最后只能挤在茶水间。实在受不了我花了两周业余时间用 Vue 写了这套智能会议室预约系统的前端把预定、审批、冲突检测、数据统计全部做成了一套完整的前端源码方案分享出来给有同样需求的团队参考。如果你是 Vue 开发者、前端新人想找实战项目练手或者公司正好需要一套内部会议室管理系统这篇内容应该能省下你不少调研时间。整套源码采用 Vue 3 Vite Element Plus Pinia 技术栈核心覆盖会议室多维度筛选、时间段冲突检测、预约审批流转、签到与使用统计等完整业务闭环。代码层面做了组件拆分、权限模拟、接口数据 mock后端没就绪时前端也能独立跑起来看效果。下面我会把整个设计思路、核心模块实现、关键代码和踩坑记录都拆开讲清楚内容包括可以直接复制使用的代码片段和完整的数据结构设计说明。1. 项目整体设计与需求拆解1.1 会议室管理场景的核心需求分析写代码之前我先花了一天时间蹲在行政部观察他们到底怎么管会议室的顺手翻了近三个月的预约记录。最终提炼出五个必须解决的核心问题信息不透明员工不知道哪个会议室空闲、能坐多少人、有没有投影仪只能一间间跑去看行政的纸质登记本没人愿意翻。冲突频发多人同时段订同一间会议室现场经常发生“谁先到谁用”的抢房大战缺乏系统层面的硬性冲突拦截。爽约严重预约了不来、也不取消导致真正需要的人用不上资源利用率极低需要一个签到的环节来约束。审批无流程领导会议室、高管会议室等特殊资源需要审批权限控制普通员工不应该能直接预定。数据无沉淀使用率、高峰时段、各会议室利用率全靠行政拍脑袋估计没有量化数据支撑工位和会议室规划。前端不能只做“展示页面”必须在代码层面真正解决这些业务痛点。我设计了“列表浏览 时段冲突校验 预约审批流 签到状态机 统计报表”五层功能架构每一层对应解决上面一个具体问题。1.2 技术选型与方案对比技术选型上我对比了三套方案最终选定的组合如下方案技术栈优点缺点最终选择方案AVue 2 Element UI Vuex资料多、团队熟悉、生态成熟组合式API支持差、状态管理代码冗余、Vue 2已停止维护不考虑方案BVue 3 Vite Element Plus Pinia组合式API写业务逻辑清晰、Vite冷启动快、Pinia语法简洁周边生态迭代快、需要适应新写法最终选用方案CReact Ant Design组件丰富、社区活跃团队以Vue为主学习成本偏高不考虑选 Vue 3 最主要的原因是组合式 API 的setup语法糖可以把“某个会议室的状态和操作”聚在一起定义代码内聚性比 Options API 好太多。之前用 Vue 2 写大型项目data 里塞一堆字段、methods 里散落几百个方法维护起来真是一个头两个大。Vue 3 的ref、computed、watch组合起来逻辑关注点分离极其舒服。Pinia 比 Vuex 好的地方在于不需要写冗长的 mutations直接改 state 就行模块化天然支持对 TypeScript 的支持要好很多。这套项目虽然业务状态不算特别复杂但涉及“当前登录用户”“筛选条件”“预约流程草稿”等多个跨组件共享状态Pinia 的 store 拆分比组件 props 层层传递干净得多。1.3 整体页面架构与数据流设计整个前端分为五个核心页面模块会议室总览以卡片墙形式展示所有会议室的状态支持按楼栋、容量、设备筛选核心功能是快速锁定可用会议室。预约流程当前时间线、选择会议室、选择时间段、填写会议信息、提交预约核心功能是带冲突检测的预约下单。审批中心待审批列表、审批详情、通过/驳回操作核心功能是流程化审批。我的预约当前用户的历史预约记录支持取消、签到、评价核心功能是预约全生命周期管理。数据统计大屏ECharts 展示会议室周使用率、热门时段、预约趋势核心功能是辅助行政做资源规划。数据流的设计遵循“单向数据流”原则页面通过api目录下封装好的 ajax 方法从后端mock 接口取数据存储在 Pinia 的 store 中组件通过计算属性或 storeToRefs 读取数据操作通过 actions 发出请求并更新状态。组件内部不再直接改 store 的数据这样多人协作时状态变化链路清晰出问题也好排查。2. 核心模块实现与关键技术细节2.1 会议室列表的 3D 楼层可视化前端实现普通的卡片列表太无聊了而且对“找到物理位置”这一需求不够直观。我做了个 2.5D 楼层平面图用 CSS Grid 模拟楼层平面网格会议室用圆角矩形表示空闲态亮绿色、占用态暗灰色、当前选中态高亮橙色。圆角矩形内部展示会议室编号和容量鼠标移上去浮层展示设备信息和下一空闲时段。位置定位和会议室设施都用 JSON 数据驱动。没有用真正 WebGL 的 3D 渲染原因很实际会议室平面图是规则的矩形排布2.5D 效果性能更好、代码复杂度低很多、维护也方便视觉上还比卡片列表直观得多。如果需要接入真实 CAD 图纸可以考虑用 SVG 解析器但内部管理工具没必要上这么重的方案。template div classfloor-map :stylegridStyle div v-forroom in rooms :keyroom.id classroom-cell :class{ occupied: room.status occupied, selected: selectedRoomId room.id } :stylegetRoomPosition(room) clickhandleSelectRoom(room) span classroom-no{{ room.no }}/span span classroom-capacity{{ room.capacity }}人/span /div /div /template script setup import { computed } from vue const props defineProps({ rooms: { type: Array, required: true }, selectedRoomId: { type: [String, Number], default: null }, columns: { type: Number, default: 8 } }) const emit defineEmits([select]) const gridStyle computed(() ({ gridTemplateColumns: repeat(${props.columns}, 1fr) })) function getRoomPosition(room) { return { gridRow: room.row, gridColumn: room.column, backgroundColor: room.status occupied ? #d9d9d9 : #b7eb8f } } function handleSelectRoom(room) { emit(select, room) } /script这个 2.5D 楼层图实现有几个小技巧位置数据驱动每个会议室对象包含 row 和 column 属性直接映射到 grid 布局的行列换楼层只需要切换数据不需要改模板。状态色不要用纯色#b7eb8f和#d9d9d9都是柔和色长时间盯屏幕不刺眼。之前用纯#52c41a太亮时间长了眼睛疼。浮层信息不要太多悬浮提示里只放“设备类型 下一空闲时段”完整设备信息放到点击后的详情抽屉里。信息分层展示保证第一屏不做信息轰炸。2.2 预约时间选择器与冲突检测算法预约模块是整个系统的核心时间选择的交互直接影响用户会不会误操作。我实现了两个核心功能时间选择器禁选过去时间使用 Element Plus 的el-date-picker的disabled-date属性把今天之前的日期和当前时间之前的时间全部禁用避免用户约到“昨天”。时段冲突检测用户选择完时间段后前端不直接提交而是先从 store 中取出该会议室的所有未取消预约逐一做时间段重叠判断。判重算法用的是标准的区间重叠逻辑function isTimeRangeOverlap(startA, endA, startB, endB) { return new Date(startA) new Date(endB) new Date(endA) new Date(startB) }将新预约时间段与已有预约逐一判断只要有一组重叠就提示用户换时间并给出最近的可用空闲时段建议。预约后还带一天多次预约的限制同一个预约人同一天最多 3 次预约超过时前端拦截并提示联系行政处理。el-date-picker v-modelform.timeRange typedatetimerange range-separator至 start-placeholder开始时间 end-placeholder结束时间 formatYYYY-MM-DD HH:mm :disabled-datedisabledPastDate :disabled-timedisabledPastTime /const disabledPastDate (time) { return time.getTime() Date.now() - 8.64e7 } const disabledPastTime (time) { const today new Date() today.setHours(0, 0, 0, 0) if (time time.getTime() today.getTime()) { return true } return false }这里有个细节需要注意disabled-date判断的粒度是天disabled-time判断的粒度是时分秒两者要配合使用才能真正限制到“当前时刻之后”。另外8.64e7是一天的毫秒数这样写效果等同24 * 60 * 60 * 1000但代码更简洁属于团队约定的小习惯。2.3 预约流程的状态机设计与实现预约从“草稿”到“完成”不是一个简单二元状态我设计了完整的状态机draft → pending → confirmed → completed ↘ rejected ↘ cancelled状态机转移约束如下draft用户选择会议室但未提交此时只存 localStorage刷新页面不丢失主动退出时清除。pending已提交、等待审批人处理此时用户可以主动取消。confirmed审批通过用户需在会议开始前 15 分钟内到场签到签到码是一串二维码。completed会议结束后自动完成或由主持人点击手动结束。rejected审批被驳回用户可以查看驳回理由并修改后重新提交。cancelled用户或管理员主动取消时段释放其他人可以预约。前端用computed根据状态值动态渲染对应的操作按钮拒绝出现“已取消的预约还能签到”这种逻辑漏洞。核心代码如下const statusMap { draft: { label: 草稿, type: info }, pending: { label: 待审批, type: warning }, confirmed: { label: 已确认, type: success }, completed: { label: 已完成, type: }, rejected: { label: 已驳回, type: danger }, cancelled: { label: 已取消, type: info } } const allowedActions computed(() { const actions [] if (props.booking.status pending) actions.push(cancel) if (props.booking.status confirmed canSignIn.value) actions.push(signin) if (props.booking.status rejected) actions.push(edit) return actions })状态机的好处是所有状态转移收敛在同一处逻辑中前端不会出现逻辑互相矛盾的操作按钮。如果后续后端加了更多状态如“使用中”只需要在 statusMap 和 allowedActions 里扩展不影响其他模块。2.4 日历视图与自动推荐时段的实现除了列表和平层图我还做了月历视图。月历用的是v-calendar组件把每月已有的预约以不同颜色标记在日期格子里绿色代表已确认、黄色代表待审批、灰色代表已取消。点击具体日期后右侧面板列出当天全部预约时间线用户可以直观看到哪个时段还有空档。预约推荐算法也很简单但实用用户选择时长后如 1 小时系统自动扫描当天 9:00-18:00 内未被占用的连续时段将符合时长要求的空闲时间段排列出来用户点击即可快速填充表单中的时间选择器。这个功能的代码不复杂但使用率极高因为绝大多数人不知道自己要约哪段时间他们知道自己“要开多长的会”function findAvailableSlots(bookings, durationMinutes, workingStart 9, workingEnd 18) { const occupied bookings .filter(b b.status confirmed || b.status pending) .map(b ({ start: new Date(b.startTime).getHours() new Date(b.startTime).getMinutes() / 60, end: new Date(b.endTime).getHours() new Date(b.endTime).getMinutes() / 60 })) .sort((a, b) a.start - b.start) const freeSlots [] let cursor workingStart for (const item of occupied) { if (item.start cursor) { freeSlots.push({ start: cursor, end: item.start }) } cursor Math.max(cursor, item.end) } if (cursor workingEnd) { freeSlots.push({ start: cursor, end: workingEnd }) } return freeSlots.filter(slot (slot.end - slot.start) * 60 durationMinutes) }这个算法是典型的贪心区间合并不算最优解但完全够用。真实业务里还要注意午休时段 12:00-13:30 需要特殊处理默认不推荐预约。会议室如果设置了“只允许整点/半点预约”在推荐结果里做时间取整就好。2.5 审批中心与消息通知的前端方案审批中心的数据结构比较简单待办列表 详情抽屉。但前端处理上有两个注意点轮询 vs WebSocket公司内部系统用户量不大我直接用了 30s 一次的setInterval轮询待办数量有新待办时浏览器标题栏闪烁提醒。如果用户量超过 200 人建议换 WebSocket 推送轮询会浪费大量无效请求。审批操作后状态联动审批通过后不仅要更新当前列表还要同步更新 Pinia 中的会议室状态和“我的预约”里的预约状态。如果后端接口返回了完整的预约对象前端可以直接 replace 更新比重新拉全量列表省流量。// 审批通过后只更新局部数据而不是全量刷新 function handleApprove(booking) { bookingStore.updateBooking({ ...booking, status: confirmed }) roomStore.updateRoomStatus(booking.roomId, occupied, booking.timeRange) }如果后端没有提供“查询某时间段某会议室可用性”的接口前端可以在审批通过时先本地做一次冲突检测如果有重叠就主动弹窗提示审批人“该时段已有其他预约确认覆盖”避免后端接口状态不同步导致的数据不一致。3. 工程化落地与实用代码实现3.1 项目目录结构与模块划分规范这套源码的目录结构是经过了几个项目迭代后沉淀下来的实践目录即规范新成员进来看目录就知道该把代码放哪src/ ├── api/ # 接口请求层 │ ├── modules/ │ │ ├── room.js # 会议室相关接口 │ │ ├── booking.js # 预约相关接口 │ │ └── user.js # 用户相关接口 │ └── request.js # axios 实例封装拦截器、错误处理 ├── assets/ # 静态资源 ├── components/ # 通用组件 │ ├── RoomCard.vue │ ├── RoomFloorMap.vue │ ├── BookingForm.vue │ └── common/ # 按钮、弹窗、空状态等基础组件 ├── composables/ # 组合式函数 │ ├── useTimeRange.js # 时间区间处理 │ └── useConflictCheck.js # 冲突检测 ├── layouts/ # 布局组件 │ └── DefaultLayout.vue ├── router/ # 路由配置 │ └── index.js ├── stores/ # Pinia 状态 │ ├── room.js │ ├── booking.js │ ├── user.js │ └── app.js ├── styles/ # 全局样式 ├── utils/ # 工具函数 │ ├── format.js │ └── validate.js └── views/ # 页面级组件 ├── RoomOverview.vue ├── BookingCreate.vue ├── ApprovalCenter.vue ├── MyBookings.vue └── Statistics.vueapi/modules按业务模块拆分文件避免所有接口挤在同一个文件里越来越大。composables是 Vue 3 带来的核心优势所在把时间处理和冲突检测这类可复用逻辑抽成独立函数多个组件共享不需要用 Mixin 那种容易命名冲突的方案。views下每个页面组件尽量保持“一个页面只干一件事”如果页面超过 500 行就要考虑拆成子组件了。3.2 接口层封装与 Mock 方案详解后端接口还没开发时前端不能干等。我在src/api/request.js里封装了 axios 实例后在vite.config.js里配了server.proxy代理并实现了一个简单的 Mock 中间件方案用 Vite 的configureServer钩子直接在 dev server 里拦截接口请求// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })Mock 数据放src/mock/index.js根据请求的 URL 和 method 返回对应的假数据不用额外引入 mockjs 库减少依赖体积。如果后端联调时只需要“临时切到真实接口”在request.js里加一行const USE_MOCK false就行。这样整个开发流程可以实现“后端未就绪前端先行”后端好了无缝切换。3.3 路由权限与动态菜单的实现思路不同角色看到的菜单和能访问的页面不一样。管理员能看到“审批中心”和“数据统计”普通员工只能看到“会议室总览”和“我的预约”。我用的方案是前端路由注册全部页面但通过路由守卫配合用户的 role 字段动态过滤菜单。这里有一个重要的安全认知前端路由守卫只是“提升体验让用户看不到进不去的入口”真正的权限校验必须依赖后端接口的权限控制否则技术好的用户直接改 localStorage 里的角色字段就绕过限制了。// 在 pinia user store 里存储角色 const router createRouter({ history: createWebHashHistory(), routes }) const WHITE_LIST [/login] router.beforeEach((to, from, next) { const userStore useUserStore() if (WHITE_LIST.includes(to.path)) { next() return } if (!userStore.token) { next(/login) return } if (to.meta.roles !to.meta.roles.includes(userStore.role)) { next(/403) return } next() })菜单渲染我用的方式是根据route.meta.roles过滤routes生成菜单项而不是硬编码菜单数据。这样以后新增页面只需要在路由表里加一条配置菜单会自动出现联动维护成本低很多。3.4 状态管理与持久化的落地实践Pinia 的 store 默认不持久化刷新页面状态就丢了。我用了pinia-plugin-persistedstate插件按 store 维度配置持久化策略用户信息和预约草稿持久化到 localStorage会议室列表等接口数据不持久化每次进入重新请求保证数据新鲜度。export const useBookingStore defineStore(booking, { state: () ({ bookings: [], draft: null, filters: { date: null, capacity: 0, amenities: [] } }), actions: { async fetchBookings(params) { const res await getBookingList(params) this.bookings res.data }, saveDraft(draft) { this.draft draft }, clearDraft() { this.draft null } }, persist: { key: meeting-booking-store, pick: [draft, filters] } })pick属性可以精确控制哪些字段要持久化避免把大量接口数据塞进 localStorage 导致容量超限。持久化的数据要在 store 初始化时做版本号字段未来结构变化时方便做迁移或清理。3.5 大数据量下的表格性能优化技巧数据统计页面有“全部预约记录”的表格数据量能到几万行。如果直接一次性渲染全部数据浏览器直接卡死。我做了三个层面的优化分页方案最常用也最简单后端接口做分页前端展示当前页但缺点是要不断切换页码查看数据。虚拟滚动一次性请求所有数据但只渲染可视区域的 10-20 行滚动时动态替换渲染数据体验比分页好。用el-table-v2组件实现官方出品稳定可靠。大数据量分析先行表格只是“明细”核心数据看 ECharts 图表图表从统计口径预先聚合数据量小渲染快。统计查询还做了“懒加载”处理页面进入时只渲染图表表格等用户点击“查看明细”按钮才发起请求并渲染。首次加载时间从 3.2s 降到了 0.8s感知提升非常明显。4. 常见问题与排查技巧实录4.1 跨域、404、接口数据格式不一致的排查开发中遇到最多的问题集中在三个跨域请求失败表现是浏览器控制台报ERR_CONNECTION_REFUSED或CORS error。排查思路先看Network面板请求是否发出、响应是什么状态码再确认 vite proxy 配置正确最后确认后端接口实际地址连通性。这里有个经验供参考如果本地curl能通但浏览器不行大概率是 CORS 头部缺失如果本地curl也不通那后端服务根本没起来。刷新页面出现 404典型原因是用history模式路由但生产环境服务器没有配置try_files回退到index.html。开发环境 Vite 已经处理了部署到 Nginx 需要在location /中配置location / { try_files $uri $uri/ /index.html; }如果项目部署在子目录比如https://example.com/meeting/还需要在 Vite 配置base: /meeting/否则静态资源路径会全部错乱。接口返回的数据结构和前端预期不一致后端的code可能是200、200或0前端统一处理方式是在request.js拦截器里做归一化service.interceptors.response.use( (response) { const res response.data if (res.code 200 || res.code 200 || res.code 0) { return res } ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message || 请求失败)) } )4.2 会议室跨天预约和午夜时段的时间边界 bug这个 bug 对测试来说很隐蔽但真实业务几乎一定会踩到用户预约 22:00 到次日 02:00 的跨天会议。我在第一次实现时间冲突检测时只比较了“开始时间”和“结束时间”的日期部分是否相同导致跨天预约被强制拆成两天且无法判断冲突。后来改成直接用完整的时间戳含日期比较区间重叠彻底解决。教训是所有时间操作一定要保存完整时间戳不要只保存日期时间的展示字符串否则排序、比较、计算时长全部会出问题。4.3 多标签页状态不同步的处理方案公司内部系统用户很习惯开多个标签页一个页面提交了预约另一个标签页的列表不刷新用户会以为提交失败了。最简单的方案是在App.vue里监听visibilitychange事件document.addEventListener(visibilitychange, () { if (document.visibilityState visible) { bookingStore.fetchBookings() roomStore.fetchRooms() } })页面从后台切回前台时自动拉取最新数据。这个方案简单粗暴但有效不用引入BroadcastChannel或LocalStorage事件监听适合中小团队内部系统。如果做的是实时性要求更高的产品建议后端加 WebSocket 主动推送。4.4 keep-alive 列表滚动位置复位的坑我在“我的预约”列表页遇到了一个经典 Vue 坑从列表页进入详情页再返回el-table的滚动条自动跳回顶部用户找不到刚才看的那一行。原因是el-table的高度变化导致的重新渲染而且被keep-alive缓存后滚动状态没有保存。解决方案是用el-table的ref在onActivated钩子里手动恢复滚动位置const tableRef ref(null) let scrollTop 0 function saveScrollTop() { scrollTop tableRef.value?.getScrollRef()?.scrollTop || 0 } onActivated(() { if (tableRef.value) { tableRef.value.getScrollRef().scrollTop scrollTop } }) onBeforeUnmount(saveScrollTop)这个坑排查花了半小时原因隐蔽在“缓存激活”的时机上。如果遇到keep-alive包裹组件后onMounted不再执行的场景也要警惕应该用onActivated。遇到 Element Plus 表格大数据渲染卡顿的先看是不是网络请求慢再看是不是表格列太多超过 20 列会明显吃性能最后才考虑虚拟滚动。4.5 组件按需引入与首屏加载优化项目开发阶段直接全量引入 Element Plus 挺舒服但打包后 vendor.js 有 1.2MB首屏加载要 3-5 秒。生产构建时不能忍改为按需自动引入后降到了 320KB// vite.config.js import AutoImport from unplugin-auto-import/vite import Components from unplugin-vue-components/vite import { ElementPlusResolver } from unplugin-vue-components/resolvers export default defineConfig({ plugins: [ AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ] })另外把 ECharts 也按需引入只引入柱状图、折线图、饼图等用到的模块体积又少了一半。如果项目还有首屏优化需求可以考虑路由懒加载和骨架屏。路由懒加载的方式const RoomOverview () import(/views/RoomOverview.vue)这是收益最高的一行代码改动建议每个 Vue 项目都无条件加上。5. 扩展从预约系统衍生出的可复用前端经验做这个项目的过程中有几个方法论层面的收获我觉得比代码本身更有价值先梳理状态机再写页面。预约这个业务任何一个操作都会改变“资格”和“阶段”如果前期不把状态转移图想清楚后期改起来会非常痛苦。组件可以先画交互草图但状态逻辑必须先理清楚。组件拆分的度不是越细越好。为了复用而拆组件是过度设计为了职责清晰而拆组件才是合理拆分。我踩过的坑是最开始拆了 20 多个小组件代码看着整齐但 props 传递链很长改一个字段要动七八个文件。后来合并到 12 个组件反而更好维护。真实业务中“低频但重要”的功能不要忽略。比如审批驳回理由、取消预约原因、导出报表这些功能在演示视频里不容易出彩但使用者长期用下来会觉得“这个系统是真懂我”。写需求时多问一句“如果出错了怎么办”“如果用户想反悔怎么办”通常能挖出很多有价值的功能点。权限问题一定要前后端联动。前端只能做到“不让普通用户看到管理入口”但真正的数据安全和权限校验必须靠后端接口。如果后端暂时没做权限控制前端至少要把接口层做一层封装方便后续随时在 request 拦截器里加 token 校验和权限判断不用改业务代码。测试环境尽量贴近生产。我的坑是开发时一直用Mock 数据没有验证真实后端接口的字段命名差异后端返回created_at前端用createdAt联调时花了大半天改字段映射。如果项目周期允许尽早约定接口文档和字段规范或者直接用 TypeScript 定义 DTO 类型从编译层面拦截字段错误。最后分享一个实际部署经验如果这套系统最终要部署到内网服务器建议用 Docker 打包前端 Nginx 镜像镜像体积小、部署快版本回滚也方便。Dockerfile 的关键部分FROM node:18-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:alpine COPY --frombuild /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80构建命令只有两行生产团队拿过去就能用不需要本地装 Node 环境。如果公司有现成的 CI/CD 流程把这个 Dockerfile 接到流水线里以后每次提交代码自动构建发布省心很多。本文还有配套的精品资源点击获取