
简介面向产品经理、UI设计师、开发工程师与实施顾问的供应链系统HTML页面原型以采购、库存、生产、物流、销售、财务、报表分析等核心业务为设计蓝图直观呈现各模块的页面布局、表单交互与导航流程能有效减少需求评审与设计阶段的沟通偏差。压缩包共299个文件、大小仅661KB包含87个可直接打开的HTML页面、43张PNG界面图、126个GIF操作演示以及配套CSS、JS样式脚本和少量Excel数据表便于快速查看页面效果与交互逻辑目录结构也便于按模块检索。目前已有1174人学习浏览适合作为供应链产品设计的参考样例。借助这套原型可快速梳理各业务环节的功能边界、字段构成与跳转关系页面中的按钮、表单、图表、导航等元素亦可作为需求文档、视觉设计与功能测试的参考基础并支持基于现有页面二次修改从而加速原型迭代提升项目落地成功率。1. 供应链系统的页面原型这份 HTML 骨架比设计稿多给了什么供应链系统这类中后台项目里页面原型通常是一堆设计稿截图切图交付后开发还要反向核对交互细节。这份资源把原型做成了能在浏览器里直接跑的 HTML 页面集采购订单、库存台账、审批流、供应商管理这些核心模块都有对应页面点击侧边栏能切换表格能筛选状态标签跟着数据变。它的价值不在页面多好看而在状态流转和页面跳转都是可操作的产品可以顺一遍业务前端可以拿它当前后端接口联调前的桩子高校实训也能直接用来当课程设计演示底稿。适合产品、前端初级开发以及需要快速搭出供应链系统演示场景的同学。后面我从骨架、业务页、数据层、避坑、接口对接五个角度把它拆透。2. 目录与路由先看懂这套原型的骨架再开始改页面拿到资源后先别急着双击 main.html建议先在编辑器里把目录结构完整看一遍。这套原型不是零散页面而是一组按职责拆开的 HTML 片段理解了这个约定后续加页面、改字段都不会乱。2.1 目录结构页面文件怎么摆新增页面怎么挂supply-chain-prototype/ ├── index.html # 登录入口页 ├── main.html # 主框架页包含侧边栏和内容容器 ├── pages/ │ ├── dashboard.html # 首页看板 │ ├── purchase.html # 采购订单列表 │ ├── purchase-detail.html # 采购订单详情 │ ├── inventory.html # 库存台账 │ ├── inbound.html # 入库单列表 │ ├── outbound.html # 出库单列表 │ ├── supplier.html # 供应商管理 │ └── approval.html # 订单审批流 ├── assets/ │ ├── css/ │ │ └── main.css # 全局样式与状态标签 │ └── js/ │ ├── router.js # hash 路由 │ ├── store.js # 本地数据读写 │ ├── mock-data.js # 采购、库存、供应商的初始数据 │ └── utils.js # 金额、日期格式化 └── README.md # 使用说明和模块清单pages 目录里放的是整页 HTML 片段不是完整页面这一点很关键。main.html 只负责撑起侧边栏、顶栏和内容区点击菜单时由 router.js 去 pages 里拉取对应的 HTML 片段再塞进内容区。这样拆的好处有两点一是每个业务页维护起来互不干扰采购页的改动不会碰坏库存页二是新增页面只要复制一个已有页面的结构改掉表格列和卡片字段就行路由配置补一行就能挂载。我刚拿到这套原型时踩了一个小坑直接双击打开 pages/purchase.html发现它没有侧边栏。如果读者也碰到类似情况不用怀疑文件损坏这个页面本来就不是独立页面它只是内容片段必须从 main.html 进去才能看到完整布局。2.2 自写 hash 路由为什么不用 vue-router怎么挂原型没有引入任何前端框架路由是自己实现的一层 hash 路由。配合调用的方式是a href#/purchase采购订单/a点击后 location.hash 变化loadRoute重新执行一次取出对应页面。const routes { #/dashboard: { page: pages/dashboard.html, auth: viewer }, #/purchase: { page: pages/purchase.html, auth: operator }, #/inventory: { page: pages/inventory.html, auth: operator }, #/supplier: { page: pages/supplier.html, auth: admin }, #/approval: { page: pages/approval.html, auth: operator } } async function loadRoute() { const hash window.location.hash || #/dashboard const conf routes[hash] if (!conf) { window.location.hash #/dashboard return } const cached document.querySelector(link[data-route${hash}]) let html if (cached) { html cached.textContent } else { const res await fetch(conf.page) html await res.text() // 缓存到页面的隐藏 link 标签里避免重复读本地文件 const link document.createElement(link) link.dataset.route hash link.textContent html document.body.appendChild(link) } document.querySelector(#content).innerHTML html document.title hash - 供应链页面原型 } window.addEventListener(hashchange, loadRoute) window.setRoute path { window.location.hash path }这里有两个值得解释的设计。第一routes 配置里同时带了 page 和 authpage 是页面片段路径auth 是后续权限菜单要用的角色标识这个字段在权限拦截和菜单渲染里能省不少事。第二页面片段被缓存到一个看不见的 link 标签里第二次点击同一菜单不用再 fetch 一遍磁盘文件在 file 协议下体验提升尤其明显。选 hash 而不是 history或者直接上 vue-router原因很实际这套原型要求能双击本地文件直接打开history 模式在 file 协议下会出现刷新后 404 的问题vue-router 则引入额外依赖还要维护构建配置。hash 模式唯一的代价是 URL 里多了个#但对页面原型来说完全没有影响。后续要接真实后端hash 路由也不冲突后端只需要关心接口路径即可。2.3 公共样式约定色板、状态标签、弹窗组件页面片段里统一引用 assets/css/main.css其中三块内容是改业务页面时最常用的颜色变量、状态标签、弹窗。:root { --primary: #2563eb; --primary-bg: #eff6ff; --warning: #f59e0b; --warning-bg: #fffbeb; --danger: #dc2626; --danger-bg: #fef2f2; --success: #16a34a; --success-bg: #f0fdf4; --text-main: #1f2937; --text-sub: #6b7280; } .tag { display: inline-block; padding: 2px 10px; border-radius: 4px; font-size: 12px; line-height: 20px; } .tag--pending { background: var(--warning-bg); color: var(--warning); } .tag--approved { background: var(--success-bg); color: var(--success); } .tag--rejected { background: var(--danger-bg); color: var(--danger); } .modal { position: fixed; inset: 0; background: rgba(15, 23, 42, 0.5); display: none; align-items: center; justify-content: center; } .modal--open { display: flex; }设计变量是一开始就定好的后续改主题色只需要改根节点的几个变量不用在几十个页面里逐个找十六进制颜色值。状态标签统一用tag--状态名的约定状态颜色集中在 CSS 里控制页面里只写 class 不写颜色。弹窗同样约定了modal默认隐藏、加上modal--open才显示的行为这样新增表单弹窗时不需要重新设计交互。如果后面有人往页面里写死颜色值建议回填成变量否则改主题时就要满项目找颜色了。3. 三大业务页采购、库存、审批的状态流转怎么落地页面骨架搭好之后真正决定原型能不能用的是业务数据怎么组织、操作按钮怎么改变页面状态。这一章拆三个最常见的模块采购订单列表、库存台账、审批流。3.1 采购订单列表状态映射与关键词筛选采购列表页的核心不是表格渲染而是“状态怎么显示、搜索怎么过滤”。这段代码直接展示了我常用的做法。const statusMap { pending: { text: 待审批, className: tag--pending }, approved: { text: 已通过, className: tag--approved }, rejected: { text: 已驳回, className: tag--rejected } } const orders [ { id: PO20250101, supplierName: 华南原料商, amount: 12800, status: pending, createdAt: 2025-01-01 }, { id: PO20250102, supplierName: 华东电子件商, amount: 5400, status: approved, createdAt: 2025-01-02 }, { id: PO20250103, supplierName: 西部包装商, amount: 9800, status: rejected, createdAt: 2025-01-03 } ] function renderOrders(list) { const tbody document.querySelector(#purchaseTable tbody) if (!tbody) return tbody.innerHTML list.map(item { const st statusMap[item.status] || statusMap.pending return tr>const DEFAULT_INVENTORY [ { sku: SKU-A101, name: A类电容, stock: 820, safetyStock: 500, unit: 个 }, { sku: SKU-B202, name: B型电阻, stock: 42, safetyStock: 200, unit: 个 } ] function loadInventory() { const saved localStorage.getItem(inv_data) if (saved) { return JSON.parse(saved) } localStorage.setItem(inv_data, JSON.stringify(DEFAULT_INVENTORY)) return DEFAULT_INVENTORY } function renderInventory() { const list loadInventory() const tbody document.querySelector(#invTable tbody) if (!tbody) return tbody.innerHTML list.map(item { const low item.stock item.safetyStock return tr class${low ? row--warning : } td${item.sku}/td td${item.name}/td td${item.stock} ${item.unit}/td td${item.safetyStock} ${item.unit}/td td${low ? 低于安全库存 : 库存充足}/td td button onclickadjustStock(${item.sku}, 1)入库/button button onclickadjustStock(${item.sku}, -1)出库/button /td /tr }).join() } function adjustStock(sku, delta) { const list loadInventory() const row list.find(item item.sku sku) if (!row) return const next row.stock delta if (next 0) { console.warn(库存不允许负数${sku} 当前 ${row.stock}尝试扣减 ${Math.abs(delta)}) return } row.stock next localStorage.setItem(inv_data, JSON.stringify(list)) renderInventory() }loadInventory 先检查 localStorage没有数据才用默认值这是保证“刷新后数据还在”的关键。adjustStock 里我刻意做了负库存拦截而不是把负数截断成 0原因是原型要暴露问题如果连原型里的库存都能被扣成负数说明上游单据逻辑一定有漏洞用 console.warn 记一条比硬扛着继续跑更有价值。安全库存预警用row--warning给整行加底色低库存一眼就能扫出来。3.3 审批流用状态机表约束订单流转订单审批最怕状态散落在一堆 if/else 里改一个分支漏一个分支。我用一个状态机表把流转规则集中管理。const stateMachine { draft: { submit: pending }, pending: { approve: approved, reject: rejected }, approved: {}, rejected: { resubmit: pending } } const appState { orders: [], transitions: [] } function transition(order, action, note ) { const allowed stateMachine[order.status] const nextStatus allowed allowed[action] if (!nextStatus) { throw new Error(不允许从 ${order.status} 状态执行 ${action} 操作) } appState.transitions.push({ orderId: order.id, from: order.status, to: nextStatus, action, note, time: new Date().toISOString() }) order.status nextStatus return nextStatus } function approveOrder(orderId) { const order appState.orders.find(o o.id orderId) if (!order) return try { transition(order, approve, 采购组确认价格) renderOrders() } catch (e) { alert(e.message) } }stateMachine 这个对象就是全部规则draft 只能 submitpending 可以 approve 或 rejectrejected 只能 resubmit。非法操作直接抛异常前端 alert 出来不会静默失败。transitions数组是一份审计轨迹记录了每一步从哪来到哪去谁操作的、备注是什么将来接真实接口时可以直接把这批记录转发给后端保存。这种表驱动的写法比散落的 if/else 好维护得多新加状态只改 stateMachine 和 statusMap 两处。4. 避坑排查这套 HTML 原型最常见的五个翻车点静态原型看起来简单真正用起来的时候翻车点不少。我把这套原型使用过程中高频出现的问题整理成五条每一条都是“现象 → 原因 → 解决”的结构。4.1 双击页面报 Failed to fetchfile 协议撑不起 fetch现象直接双击 main.html 打开侧边栏能显示但内容区一直是空白控制台报Failed to fetch。原因router.js 用fetch(conf.page)拉取页面片段而 fetch 在 file:// 协议下会被浏览器拦截。这不是代码写错了是浏览器安全策略不允许本地文件发起这种请求。解决起一个本地静态服务用 http 协议访问。cd supply-chain-prototype python3 -m http.server 8080然后浏览器访问http://localhost:8080/main.html。如果你本机没有 python3用npx serve .也能起服务。后续所有页面跳转都走这个地址。提示如果用 VS Code 的 Live Server 插件默认端口可能不是 8080看插件输出栏的实际端口就行。4.2 侧边栏跳转后内容区空白hashchange 只监听了后续变化现象点击侧边栏菜单 URL 变了但内容区不刷新手动改 URL 回车又能正常显示。原因代码里只写了window.addEventListener(hashchange, loadRoute)监听的是 hash 变化事件。页面刚加载时如果 URL 上已经带了#/purchase此时 hashchange 并不会触发loadRoute 没有被执行。解决页面底部手动调用一次加载函数兜底首屏渲染。window.addEventListener(hashchange, loadRoute) loadRoute()这一行loadRoute()就是后悔药。以后自己写类似的 hash 路由时养成习惯监听事件后立刻手动执行一次不要等事件来敲门。4.3 金额显示怪数字没格式化toLocaleString 安排上现象表格里的金额显示成12800没有千分位更夸张的会出现1.23456789e7这类科学计数法。原因原生 JS 对象里的数字直接拼接进模板字符串没有做格式化处理数值超过一定位数时显示就很难看。解决金额、数量这类字段渲染前统一走toLocaleString(zh-CN)。const amount 12345678.9 console.log(amount.toLocaleString(zh-CN)) // 输出12,345,678.9这个方法不用引第三方库格式化规则跟随浏览器语言设置大多数场景够用。要注意别对字符串调用它数字才能正确处理。4.4 打印对象变成 [object Object]localStorage 只认字符串现象数据明明存进了 localStorage取出来发现是个[object Object]页面上直接渲染出这行字。原因localStorage 只能存字符串直接把对象塞进去JS 会默认调用 toString() 把对象转成[object Object]。存的时候代码可能没报错取出来才炸。解决写入前JSON.stringify读取后JSON.parse。const order { id: PO001, status: pending } localStorage.setItem(currentOrder, JSON.stringify(order)) const raw localStorage.getItem(currentOrder) const parsed raw ? JSON.parse(raw) : nullread 的时候要判断空值返回 null 而不是抛异常。这个习惯在 store.js 里已经封装过了如果自己新开页面存数据务必遵守同样的格式。4.5 驳回后无法重新提交状态机规则没覆盖 resubmit现象审批流里操作员点了驳回订单变成“已驳回”但再次点提交按钮时页面报错提示不允许从 rejected 执行 submit。原因状态机表里 rejected 只写了{ resubmit: pending }而页面提交按钮绑定的是 submit 动作。两个动作名对不上流转规则自然不允许。解决把状态机规则补全让 rejected 也能走 submit或者按钮动作名统一改成 resubmit。const stateMachine { draft: { submit: pending }, pending: { approve: approved, reject: rejected }, approved: {}, rejected: { submit: pending, resubmit: pending } }这里反映出一个设计原则状态机的动作名是给代码看的不是给业务看的。页面按钮文案可以写“重新提交”但内部动作统一叫 resubmit或者统一叫 submit不要混用。混用就会像上面这样界面看起来没问题报表盘的时候才报错。5. 往真实接口靠拢用一层 request 隔离 mock 与 real如果只是拿来演示前面四章的东西已经够用了。但如果你想把它变成半成品代码继续往后开发我强烈建议在动手之前先加一层 request 函数让所有页面代码只认数据不认来源。const API_MODE mock // 切换成 real 就请求真实后端 async function request({ url, method GET, data null }) { if (API_MODE mock) { return mockMap[url](data) } const options { method, headers: { Content-Type: application/json } } if (data) options.body JSON.stringify(data) const res await fetch(url, options) if (!res.ok) throw new Error(HTTP ${res.status}) return res.json() }mockMap 里放的是每一个 url 对应的 mock 函数返回的数据结构和真实接口保持一致建议统一包一层{ code, message, data }。切到 real 模式时页面代码不需要改因为 request 的入参出参规格没变变的只是内部实现。这样从原型到半成品的过渡就是一行开关的事。另外一个习惯是先把状态码定义成一张表写进 README所有页面引用状态码时都用常量不直接写数字。状态码含义允许的操作0草稿submit10待审批approve / reject20已通过无30已驳回resubmit从那次因为状态码不一致导致联调返工之后我每次拿到新原型都先花半小时把状态码定义打印出来贴到屏幕上再开始动页面逻辑避免在状态流转上翻车。希望帮到你。本文还有配套的精品资源点击获取