软件工程第四次作业

校园失物招领(WEB 版)—— 2026 秋软件工程结对作业(第二次之程序实现)

作业链接

项目 内容
这个作业属于哪个课程 H202601 软件工程与软件工程实践
这个作业要求在哪里 2026 秋软件工程结对作业(第二次之程序实现)
这个作业的目标 把上次结对作业的校园失物招领原型做成可运行的 Web 程序,实现发布、浏览搜索、联系与状态标记的完整流程。
GitHub 仓库 102401526-102401525
结对成员 102401526 林鹏远、102401525 丁鑫河
队友博客 https://www.cnblogs.com/sudaaa/p/23254743
本作业博客 https://www.cnblogs.com/-code-yuan/p/23243585

一、具体分工

成员 具体分工
林鹏远 需求分析,编写核心代码,撰写readme,设计单元测试
丁鑫河 需求分析,对readme做审查,设计单元测试,检查核心代码

二、PSP 表格

按作业要求,动手前先预估各步骤耗时,完成后记录实际耗时。表格按 PSP 2.1 模板组织,Planning 与 Reporting 两行是汇总行,数值等于其下属子项之和。

PSP 阶段 任务内容 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 40
Estimate 明确需求与功能边界,估计开发总耗时 30 40
Development 开发 810 1065
Analysis 需求分析(分析原型 → 可运行程序的差距) 70 85
Design Spec 设计文档(数据结构、模块划分) 50 70
Design Review 设计复审(与队友讨论确认) 30 40
Coding Standard 代码规范(命名、注释、目录约定) 20 15
Design 具体设计(页面流转、localStorage 键设计) 100 130
Coding 具体编码(数据层 / 逻辑层 / 视图层) 400 480
Code Review 代码复审 50 85
Test 测试(14 个单元测试 + 浏览器走查,修改代码) 90 160
Reporting 报告 200 260
Test Report 测试报告 50 60
Size Measurement 计算工作量 20 20
Postmortem 事后总结与过程改进计划 130 180
合计 1040 1365

预估与实际的差异分析

实际合计比预估多出约三成,偏差集中在两处:测试环节超时最多,异常输入的边界用例设计远比运行用例费时,不过正是这部分帮我们提前暴露了"改状态后界面不刷新"的同步缺陷;编码环节的超时主要花在状态更新后多视图同步的返工上。其余环节与预估基本持平。改进计划:下次把"异常输入的用例设计"单独立项,并给多视图交互预留返工余量。


三、解题思路描述与设计实现说明

3.1 需求分析与取舍

上次结对作业我们站在"产品经理"视角给出了原型;这次要站在"程序员"视角,把核心流程真正做出来、能用起来。

作业正文给出的核心流程是:发布信息 → 浏览/搜索 → 查看详情 → 联系发布者 → 更新状态。结合老师在前情提要中的提醒(不要因为页面画过就默认解决了按位置查找的问题),我们先把功能边界砍干净:

做的:发布寻物/招领信息(含图片)、集中浏览、关键词 + 分类搜索、详情与联系方式、状态更新与撤销、完结信息的可见性变化。

明确不做的(按作业要求):后台管理、实名认证、即时聊天、地图定位。替代方案是"结构化地点字段 + 编号系统",即用文字地点和唯一编号定位信息,而不是地图。

另外两个老师在博客点评里点名的问题,我们在设计阶段就给出了解法:

  1. "谁来修改状态?从哪个入口修改?修改后其他用户在哪看到结果?" —— 见 3.3 的状态机设计,这是本次设计的核心。
  2. "界面画出来和用户能用起来是两回事" —— 所有异常路径(表单不完整、搜索无结果、未来时间、图片超 3 张、存储空间不足)都有真实的交互响应,不只是一条顺利路线。

3.2 代码实现思路(文字描述)

总体架构:三层分离,逻辑层与视图层解耦

┌─────────────────────────────┐
│  视图层 app.js               │  路由(hash)、6 个视图渲染、DOM 事件
├─────────────────────────────┤
│  逻辑层 utils.js(纯函数)    │  校验 validateItem、筛选 filterItems、
│                             │  编号 bizNo、图片压缩、复制 —— 可独立单测
├─────────────────────────────┤
│  数据层 storage.js           │  localStorage 读写、示例数据、状态更新
└─────────────────────────────┘

之所以把校验和搜索做成不依赖 DOM 的纯函数,是为了让单元测试可以直接调用函数断言结果,而不需要模拟页面操作——这直接决定了第七节测试的写法。

数据模型:一条信息(item)就是一个普通对象,整个应用只有一条主数据(lf_items_v1)加两条辅助键(本机发布的 id 列表 lf_my_ids_v1、编号自增序列 lf_seq_v1):

{
  id: 'k3x9...',                  // 唯一标识
  bizNo: 'XW2026100801',          // 编号:寻物XW / 招领ZL + 8位日期 + 2位序号
  type: 'lost' | 'found',         // 寻物 / 招领
  status: 'doing' | 'done',       // 进行中 / 已完结
  name, category, location,       // 名称、分类、地点
  happenTime, createTime,         // 发生时间、发布时间
  description,                    // 描述
  contactName, contact,           // 联系人、手机号/微信号
  images: [base64, ...]           // 最多 3 张,Canvas 压缩后存储
}

"当前用户"的简化:作业不要求实名认证,我们约定"当前浏览器即一名用户"——在这台设备上发布的信息 id 记入 lf_my_ids_v1,即视为"我的发布"。这个约定让状态更新权限判断(只有发布者能完结)和演示(预置 2 条"我的"信息)都变得简单。

呈现形式:移动端 H5 单页应用,hash 路由切换 6 个视图(首页 / 搜索 / 发布 / 详情 / 我的 / 发布成功)。选 WEB 而非 APP 的原因:作业要求"下载所有文件后用谷歌浏览器运行 html 就能展现预期结果",纯静态前端双击即用,零环境依赖。

3.3 关键实现的流程图

图 1:核心业务流程(本次作业要求的端到端主流程)

flowchart TD A[用户打开应用] --> B[首页:集中浏览<br/>全部/寻物/招领筛选] B --> C[关键词+分类搜索] C -->|无结果| D[空状态页<br/>一键重置 或 去发布] C -->|有结果| E[点击卡片查看详情<br/>编号/地点/时间/描述/图片] B --> E E --> F[联系发布者<br/>复制手机号/微信号 或 一键拨号] F --> G[失主与拾到者线下沟通] G --> H{物品找回/归还?} H -->|是| I[发布者标记:已找到/已归还] H -->|否| B A --> J[发布:填写表单<br/>类型/名称/分类/地点/时间/描述/联系方式/图片] J -->|校验不通过| K[红色行内提示<br/>逐字段标错] K --> J J -->|校验通过| L[发布成功页<br/>引导查看详情/继续发布/返回首页] L --> B I --> M[其他用户的可见变化:<br/>列表置灰+绿标签;详情页绿色横幅;<br/>非发布者联系方式收起] M --> B

图 2:信息状态机(回应"谁来改、从哪改、别人在哪看")

stateDiagram-v2 [*] --> doing : 发布 addItem doing --> done : 发布者标记「已找到/已归还」<br/>入口1:底部「我的」页信息卡片<br/>入口2:详情页管理区(仅发布者可见) done --> doing : 发布者「撤销完结」 done --> [*] note right of done 其他用户在两处看到结果: ① 首页列表:卡片置灰 + 绿色状态标签 ② 详情页:顶部绿色横幅, 联系方式收起,避免无效联系 end note

图 3:发布表单校验流程(白盒分支的来源,与第六节用例 1~4 对应)

flowchart TD S[提交表单] --> T1{name 必填?} T1 -->|空| E1[错误:请填写物品名称] T1 -->|超30字| E2[错误:名称不能超过30字] T1 -->|通过| T2{type 是 lost/found?} T2 -->|否| E3[错误:请选择启事类型] T2 -->|通过| T3{category 在合法列表?} T3 -->|否| E4[错误:请选择物品分类] T3 -->|通过| T4{happenTime 合法?} T4 -->|空或格式错| E5[错误:请选择时间/格式不正确] T4 -->|晚于当前+1h| E6[错误:时间不能晚于当前时间] T4 -->|通过| T5{images ≤ 3 张?} T5 -->|否| E7[错误:最多上传3张] T5 -->|通过| OK[valid=true,Store.addItem 入库]

图 4:一次发布的数据流(分层架构的落地)

flowchart LR U[用户填写表单] -->|提交| V[app.js 视图层<br/>收集字段为 data 对象] V -->|validateItem| L[utils.js 逻辑层<br/>纯函数校验] L -->|valid=true| V V -->|addItem| D[storage.js 数据层<br/>生成 id/bizNo,push 进数组] D -->|JSON.stringify| S[(localStorage<br/>lf_items_v1)] S -->|getItems| V V -->|渲染| U

3.4 重要代码片段与解释

片段 1:表单校验 validateItem(纯函数,逐字段返回错误) —— 不抛异常、不碰 DOM,返回 {valid, errors},视图层拿 errors 逐字段画红字,测试层直接断言。其中"未来时间"分支是老师上次点评"只按顺利路线设计"后我们主动补的防线:

function validateItem(data) {
  var errors = {};
  data = data || {};

  if (!data.name || !String(data.name).trim()) {
    errors.name = '请填写物品名称';
  } else if (String(data.name).trim().length > 30) {
    errors.name = '物品名称不能超过 30 个字';
  }
  // ...type / category / location / contactName / contact 同理...
  if (!data.happenTime) {
    errors.happenTime = '请选择时间';
  } else if (isNaN(new Date(data.happenTime).getTime())) {
    errors.happenTime = '时间格式不正确';
  } else if (new Date(data.happenTime).getTime() > Date.now() + 60 * 60 * 1000) {
    errors.happenTime = '时间不能晚于当前时间';   // 丢失/拾到不可能发生在未来
  }
  if (data.images && data.images.length > MAX_IMAGES) {
    errors.images = '最多上传 ' + MAX_IMAGES + ' 张图片';
  }
  return { valid: Object.keys(errors).length === 0, errors: errors };
}

片段 2:搜索筛选 filterItems(关键词 × 类型 × 分类三维叠加,纯函数) —— 关键词对 名称/地点/描述/编号 四字段拼接后的 haystack 做包含匹配;排序规则把"进行中"排在"已完结"之前,保证首页首屏信息有效:

function filterItems(items, cond) {
  cond = cond || {};
  var kw = String(cond.keyword || '').trim().toLowerCase();
  var type = cond.type || '';
  var category = cond.category || '';

  var result = (items || []).filter(function (it) {
    if (type && it.type !== type) return false;       // 类型筛选
    if (category && it.category !== category) return false; // 分类筛选
    if (kw) {
      var hay = [it.name, it.location, it.description, it.bizNo]
        .join(' ').toLowerCase();
      if (hay.indexOf(kw) < 0) return false;          // 关键词匹配
    }
    return true;
  });

  // 进行中的排在已完结前面;同级按发布时间倒序
  return result.sort(function (a, b) {
    if ((a.status === 'doing') !== (b.status === 'doing')) {
      return a.status === 'doing' ? -1 : 1;
    }
    return new Date(b.createTime).getTime() - new Date(a.createTime).getTime();
  });
}

片段 3:状态更新 updateStatus(核心流程的最后一环) —— 只接受两个合法状态值,其余一律拒绝,防止视图层传错值污染数据:

// status: 'doing' | 'done'
function updateStatus(id, status) {
  if (status !== U.STATUS_DOING && status !== U.STATUS_DONE) return false;
  var items = getItems();
  for (var i = 0; i < items.length; i++) {
    if (items[i].id === id) {
      items[i].status = status;
      write(KEY_ITEMS, items);   // 持久化,其他用户(浏览器)下次打开即见
      return true;
    }
  }
  return false;
}

片段 4:编号生成 bizNo —— 每条信息有全局唯一业务编号(寻物 XW / 招领 ZL + 日期 + 2 位序号),失主和拾到者线下核对时只报编号即可,这也是详情页和搜索都支持按编号检索的原因。Math.min(99, seq) 处理序号超 99 的截断边界(单元测试覆盖了该分支):

function bizNo(type, date, seq) {
  var d = date instanceof Date ? date : new Date(date);
  var ymd = d.getFullYear() + pad2(d.getMonth() + 1) + pad2(d.getDate());
  return (type === TYPE_LOST ? 'XW' : 'ZL') + ymd + pad2(Math.min(99, seq));
}

四、附加特点设计与展示

附加特点共设计了 5 个,按"意义 → 思路 → 代码 → 展示"逐个说明。

4.1 状态更新的双入口 + 撤销完结

  • 创意与意义:老师点评里问"谁来修改、从哪个入口修改、修改后其他用户在哪看到结果"。我们把这题当成设计题来做:只有发布者能看到管理入口(详情页管理区 + 「我的」页两个入口,普通浏览者看不到按钮,从权限上杜绝误改);同时提供"撤销完结",因为"已找到"标记错了没法找回会打击使用意愿。
  • 实现思路:权限判断用 Store.isMine(id)(本机发布的 id 列表);确认操作走二次确认弹层,防手滑。
  • 成果展示:

image

image

image

image

4.2 一键复制联系方式 / 手机号一键拨号

  • 创意与意义:失物招领的高频动作是"联系发布者"。用户在电脑上浏览,掏出手机输入微信号太麻烦,一键复制把操作从"看→记→切设备→输入"缩成一步;纯手机号还提供 tel: 链接直接唤起拨号。
  • 实现思路:优先 navigator.clipboard,失败降级 execCommand('copy')。
  • 重要代码:降级路径的细节——navigator.clipboard 只在安全上下文且授权后可用,双击打开的本地页面在不同浏览器下表现不一;降级方案里隐藏的 textarea 也有讲究,必须保持"可选中"状态:
function fallback() {
  var ta = document.createElement('textarea');
  ta.value = text;
  // 降级路径兼容性更好;textarea 不能 display:none 或完全透明,否则部分浏览器会跳过复制
  ta.style.position = 'fixed';
  ta.style.left = '-9999px';
  ta.style.top = '0';
  document.body.appendChild(ta);
  ta.focus();
  ta.setSelectionRange(0, ta.value.length);
  try { document.execCommand('copy') ? resolve() : reject(new Error('复制失败')); }
  catch (err) { reject(err); }
  document.body.removeChild(ta);
}

image

4.3 客户端图片压缩

  • 创意与意义:localStorage 只有约 5MB,手机原图动辄 3~5MB,直接存一张就爆。压缩让"传图片"这个功能真正可用,而不是摆设。
  • 实现思路:FileReader 读图 → 按最大边 800px 等比缩放 → Canvas 重绘 → JPEG(质量 0.72)转 base64,PNG 透明图保留格式。最多 3 张(原型设定,校验强制)。
  • 重要代码:
function compressImage(file, maxSize) {
  maxSize = maxSize || 800;
  return new Promise(function (resolve, reject) {
    // ...FileReader 读文件、Image 校验...
    img.onload = function () {
      var w = img.width, h = img.height;
      if (w > maxSize || h > maxSize) {          // 等比缩放
        if (w >= h) { h = Math.round(h * maxSize / w); w = maxSize; }
        else        { w = Math.round(w * maxSize / h); h = maxSize; }
      }
      var canvas = document.createElement('canvas');
      canvas.width = w; canvas.height = h;
      canvas.getContext('2d').drawImage(img, 0, 0, w, h);
      var mime = file.type === 'image/png' ? 'image/png' : 'image/jpeg';
      resolve(canvas.toDataURL(mime, 0.72));     // JPEG 0.72 平衡画质与体积
    };
  });
}
  • 成果展示:

image

4.4 搜索无结果的空状态引导

  • 创意与意义:老师强调"搜索无结果等异常情况要提前预警"。空状态不做成冷冰冰的"0 条",而是给出两条出路:一键重置筛选、直接跳去发布——把死胡同变成引导。
  • 实现思路:filterItems 返回空数组时,视图层渲染空状态组件(本地图标 empty.svg + 两个按钮)。
  • 成果展示:

image

4.5 编号系统 + 类型切换文案联动

  • 创意与意义:编号(XW/ZL 前缀)让线下核对不用念完整描述;发布页切换"寻物/招领"时,地点/时间标签自动变为"丢失地点/拾到地点、丢失时间/拾到时间",状态文案也从"寻找中/已找到"对应为"待认领/已归还"——同一套数据结构通过文案映射服务两种业务语义,而不是复制两套代码。
  • 实现思路:常量映射表 STATUS_TEXT:
STATUS_TEXT[TYPE_LOST]  = { doing: '寻找中', done: '已找到' };
STATUS_TEXT[TYPE_FOUND] = { doing: '待认领', done: '已归还' };

image
image


五、目录说明和使用说明

5.1 目录是如何组织的

软件工程作业4/
├── index.html            # 应用入口(单页应用,6 个视图在其中切换)
├── css/
│   └── style.css         # 全部样式(黑白灰简洁风格、移动端布局)
├── js/
│   ├── utils.js          # 逻辑层:校验、搜索筛选、编号、压缩、复制(纯函数,可单测)
│   ├── storage.js        # 数据层:localStorage 读写、示例数据、状态更新
│   └── app.js            # 视图层:hash 路由、6 个视图渲染与全部交互
├── assets/               # 图片素材(本地 SVG,离线可用)
│   ├── bag.svg / card.svg / headphones.svg / key.svg
│   ├── umbrella.svg / book.svg / cup.svg / watch.svg
│   └── empty.svg         # 搜索空状态插图
├── test/
│   └── tests.html        # 单元测试页(14 个用例,浏览器直接打开运行)
└── README.md             # 目录说明 + 使用说明(与博客本节同步)

组织原则:按职责分目录(样式 / 逻辑 / 数据 / 视图 / 素材 / 测试),其中 utils.js 与 storage.js 不含任何 DOM 操作,tests.html 只引这两个文件即可完成测试,不依赖页面。

5.2 测试人员如何运行

方式一(推荐,即作业要求的测试方式):下载全部文件 → 用谷歌浏览器(Chrome)直接双击打开根目录的 index.html。无需服务器、无需联网、无需安装任何依赖。

方式二(可选):项目根目录启动静态服务器后访问,例如:

# Windows
py -m http.server 8000
# macOS / Linux
python3 -m http.server 8000

体验指引(2 分钟走完核心流程):

  1. 首页浏览示例信息,顶部可切"全部/寻物/招领",点卡片进详情;
  2. 详情页点"复制微信号/手机号",或直接"拨打";
  3. 底部"+ 发布"发布一条寻物信息(试试留空提交看校验提示);
  4. 底部"我的"→ 对刚发布的(或预置的 2 条"我的"信息)点"标记已找到",回首页看置灰效果;
  5. 搜索框输入不存在的词(如"电视机")看空状态页;
  6. 在详情页/我的页"撤销完结",信息恢复进行中。

数据说明:数据存浏览器 localStorage,预置 10 条示例(含 2 条"我的发布"、2 条已完结)。在控制台执行 Store.resetData() 可随时恢复初始示例数据。

image
image
image
image


六、单元测试

6.1 测试工具与简易教程

选用的工具:没有引入 Jest/Mocha 等框架,而是用原生 JS 自写了一个 30 行的极简测试运行器,放在 test/tests.html 里。原因有三:① 作业要求"下载后浏览器直接打开就能测",框架需要 node/npm 环境;② 我们的逻辑层是无 DOM 依赖的纯函数,断言足够;③ 顺便借此理解测试框架的本质(runner + assert 的封装)。

我们是如何学习单元测试的:先过了一遍等价类划分、边界值分析、白盒分支覆盖的概念(软件工程教材 + 课堂内容),再对照上机实践。下面是我们的简易教程(也是我们实际做的事):

第 1 步:写运行器 —— it(用例名, 步骤, 函数) 收集结果,assert/eq 抛错即失败:
    function it(name, steps, fn) {
      try { fn(); results.push({ name, steps, pass: true }); }
      catch (e) { results.push({ name, steps, pass: false, err: e.message }); }
    }
    function assert(cond, msg) { if (!cond) throw new Error(msg || '断言失败'); }
    function eq(a, b, msg) {
      if (a !== b) throw new Error(msg + ':期望「' + b + '」,实际「' + a + '」');
    }

第 2 步:写夹具 —— validForm(override) 生成一份合法表单数据,
         每个用例前 Store.resetData() 恢复干净环境,用例互不污染。

第 3 步:设计用例 —— 白盒对着 validateItem 的每个判定分支、
         bizNo 的序号边界(99 截断)写;黑盒按等价类给搜索输入分类,
         用错误推测法补"撤销完结"这类容易漏的路径。

第 4 步:跑 + 看 —— 浏览器打开 tests.html,绿=PASS 红=FAIL,
         失败会打印"期望值 vs 实际值"。

image

6.2 测试代码展示(说明测试的函数)

共 14 个用例,覆盖四个模块加一组白盒专项:表单校验(用例 1~4)、发布入库(用例 5~6)、搜索筛选(用例 7~10)、状态更新(用例 11~12)、白盒专项——分支/边界(用例 13~14)。测试的函数是 Utils.validateItem、Utils.filterItems、Utils.matchKeyword、Utils.bizNo(经发布流程与边界用例)、Store.addItem、Store.updateStatus。举三个有代表性的:

// 用例 3(白盒-分支):validateItem 的未来时间拒绝分支
it('用例 3:表单校验 —— 丢失/拾到时间晚于当前时间时拒绝', '...', function () {
  var future = new Date(Date.now() + 24 * 3600000).toISOString();
  var r = U.validateItem(validForm({ happenTime: future }));
  assert(!r.valid, '未来时间应校验失败');
  assert(!!r.errors.happenTime, '应给出 happenTime 错误提示');
});

// 用例 5(集成):addItem 入库 + bizNo 前缀与格式 + "我的发布"归属
it('用例 5:发布信息 —— 新增寻物信息后可被查询,编号以 XW 开头', '...', function () {
  reset();
  var item = S.addItem(validForm());
  assert(!!item && !!item.id, '应返回带 id 的新信息');
  var got = S.getItem(item.id);
  eq(got.name, '测试物品', '名称应一致');
  eq(got.status, U.STATUS_DOING, '新信息状态应为 doing');
  assert(/^XW\d{8}\d{2}$/.test(got.bizNo), '寻物编号应为 XW+日期+序号,实际:' + got.bizNo);
  assert(S.isMine(item.id), '新发布的信息应属于"我的发布"');
  reset();
});

// 用例 12(错误推测):完结后撤销恢复 —— 对应状态机的 done→doing 双向边
it('用例 12:状态更新 —— 招领信息完结后可撤销恢复为「待认领」', '...', function () {
  reset();
  var target = S.getItems().filter(function (it) { return it.type === U.TYPE_FOUND; })[0];
  S.updateStatus(target.id, U.STATUS_DONE);
  S.updateStatus(target.id, U.STATUS_DOING);
  var got = S.getItem(target.id);
  eq(got.status, U.STATUS_DOING, '撤销后应恢复 doing');
  eq(U.STATUS_TEXT[U.TYPE_FOUND][U.STATUS_DOING], '待认领', '文案应为「待认领」');
  reset();
});

6.3 构造测试数据的思路

如何考虑各种情况:三层思路——

  1. 白盒对着代码分支造:validateItem 内部共有 12 个判定分支(名称 2 个、类型/分类/地点/联系人/联系方式各 1 个、时间 3 个、描述 1 个、图片 1 个)。单元测试直接覆盖其中 5 类关键输入——空名称、完整合法通过、未来时间、图片超 3 张、超长名称(用例 1~4 及 13);其余分支(空地点、空联系方式、非法类型等)由发布页的行内校验兜底,并通过浏览器人工走查验证。bizNo 的 Math.min(99, seq) 是显式边界,单独设边界用例(用例 14)。
  2. 黑盒按等价类造:搜索输入划成三类等价类——命中名称("校园卡")、命中地点/描述("图书馆")、完全无命中("电视机");再叠加"类型+分类"组合筛选验证条件之间是 AND 关系。
  3. 错误推测法兜底:想"用户最可能怎么把程序玩坏"——状态来回切(完结→撤销→再完结)、存满(write 失败走 alert 提示分支)、时间恰好等于当前(±1 小时容差边界)。

如何考虑将来测试人员的刁难:

  • 数据隔离:每个用例前 resetData(),测试人员手动玩过应用后再跑测试,结果不受脏数据影响;跑完同样恢复示例,不破坏演示环境。
  • 输入容错:测试人员可能贴超长文本、全空格、未来日期——这些全部被校验拦在界面层,不会写入脏数据。
  • 可重复:不依赖网络、不依赖执行顺序,tests.html 刷新即重跑;失败信息打印"期望 vs 实际",照着就能定位。

七、GitHub 代码签入记录

仓库:https://github.com/codeyuan1/102401526-102401525

提交按开发阶段分批签入,commit message 与实际工作内容一一对应:

# 提交信息 对应工作
1 搭建项目骨架:入口页面与黑白灰移动端样式 index.html + style.css
2 添加工具函数与localStorage数据层(含校验、筛选、示例数据) utils.js + storage.js
3 实现六大视图与核心交互(发布/搜索/详情/状态更新) app.js
4 添加分类图标与空状态SVG素材(本地离线可用) assets/
5 添加14个单元测试用例(白盒分支/边界+黑盒等价类) test/tests.html
6 完善README:目录结构、使用说明与测试设计方法 README.md
7 chore: 添加.gitignore忽略系统文件 .gitignore

image


八、遇到的代码模块异常或结对困难及解决方法

困难 1:图片上传撑爆 localStorage

  • 问题描述:手机照片动辄 3~5MB,而 localStorage 总容量只有约 5MB,传两张原图就抛 QuotaExceededError,发布失败。
  • 做过哪些尝试:写入前用 Canvas 压缩——按最长边 800px 等比缩小、以质量 0.72 重新编码为 JPEG,单张压到 100KB 以内;压缩依赖 img.onload,提交流程随之改成 Promise 链等待压缩完成再写库。
  • 是否解决:解决。压缩后单张降到几十 KB,3 张图片加全部文字一次写入无压力。
  • 有何收获:localStorage 容量要精打细算;带图片的提交天然是异步流程,同步思维会写出竞态。

困难 2:状态更新后多视图不同步

  • 问题描述:详情页把信息标记为"已归还"后,返回列表卡片还是"进行中",首页统计也没变——数据改了,界面没跟上。
  • 做过哪些尝试:约定所有状态变更必须走 Store.updateStatus 这一个口子,变更后重渲染当前视图,切回其他视图由 hash 路由触发重渲染。
  • 是否解决:解决。列表卡片、详情横幅、统计数字三处显示一致。
  • 有何收获:多视图共享数据时,"谁通知界面"要开工前定规则;这正是"改完别人在哪看到结果"的工程答案。

九、评价你的队友

值得学习的地方:丁鑫河在需求分析阶段很有想法,并且敢于砍掉不合理,难以实现的需求;我有些代码上的小漏洞,也是他通过细心的测试发现出来。

需要改进的地方:他介入开发偏晚,前期以把关和测试为主,如果能把审查中发现的问题自己动手改掉一部分,结对开发的效率会更高。


十、个人总结

这次作业最大的收获,是真正体会了从"画出来"到"能用起来"的差距:界面背后的校验、数据和各种异常分支,比画一个按钮费时得多,也更值得花时间。

posted @ 2026-10-10 01:54  _code_yuan  阅读(20)  评论(0)    收藏  举报