2026秋软件工程结对作业(第二次之程序实现)
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering/ |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering/homework/16744 |
| 这个作业的目标 | 完成校园失物招领核心功能的代码实现 |
| 成员1 | 102401605 王乐禾 |
| 成员2 | 102401222 李江炜 |
| 结对同学李江炜的博客 | https://www.cnblogs.com/LeonOrLi219 |
| GitHub项目地址 | https://github.com/LeonOrLi219/102401222-102401605 |
一、具体分工
李江炜负责数据层与首页、详情页,王乐禾负责逻辑层与发布页、我的发布页,单元测试两人各写自己模块对应的部分。
二、PSP表格
| 阶段 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|
| 需求分析与原型回顾 | 30 | 40 |
| 目录结构与数据模型设计 | 30 | 30 |
| 数据层 storage.js | 40 | 60 |
| 搜索/筛选 search.js | 40 | 50 |
| 表单校验 validate.js | 30 | 40 |
| 四个页面 HTML/CSS | 180 | 220 |
| 页面逻辑 index/publish/detail/my | 150 | 200 |
| 单元测试编写与调试 | 90 | 120 |
| README 与博客 | 90 | 120 |
| Git 协作与 PR | 60 | 90 |
| 合计 | 740 | 970 |
三、解题思路描述与设计实现说明
代码实现思路
项目采用原生 HTML + CSS + JavaScript 实现,数据存储在浏览器 localStorage 中,整体分为三层:数据层(storage.js)封装 localStorage 的增删改查,逻辑层(search.js、validate.js)提供纯函数便于单元测试,表现层(四个 HTML 页面 + 对应 JS)负责渲染和交互。页面之间通过 URL 参数传递 id,数据流为:用户操作 → 调用数据层写入 → 重新读取 → 过滤 → 渲染页面。
流程图或数据流图
代码片段
片段1:集中导出所有数据操作函数,页面调用统一接口,同时满足 Jest 单元测试 require 的需求。
if (typeof module !== 'undefined') {
module.exports = { KEY, getAll, saveAll, add, getById, updateItem, removeItem, stats };
}
片段2:纯函数无 DOM 依赖。关键词同时匹配标题和地点,多个条件用 AND 组合,'all' 表示不筛选。
function filterItems(items, opt) {
const { keyword = '', category = 'all', type = 'all', status = 'all' } = opt || {};
const kw = keyword.trim().toLowerCase();
return items.filter(it => {
const hitKw = !kw ||
(it.title || '').toLowerCase().includes(kw) ||
(it.location || '').toLowerCase().includes(kw);
const hitCat = category === 'all' || it.category === category;
const hitType = type === 'all' || it.type === type;
return hitKw && hitCat && hitType;
});
}
片段3:发布者点击"标记为已找到/已归还"后,只修改 status 字段,其他数据保留,刷新页面显示"已解决"徽章
updateItem(item.id, { status: 'done' });
location.reload();
四、附加特点设计与展示
设计的创意与意义
围绕校园失物招领的两个核心痛点——信息太多找不着、找到后联系麻烦,我们做了 4 个附加设计:
| 附加特点 | 解决什么问题 |
|---|---|
| 顶部搜索框 | 支持按物品名称、描述、地点快速检索 |
| 统计卡(寻物中 / 已解决 / 全部) | 一眼看清当前平台的整体情况 |
| 分类标签栏(证件卡类 / 数码产品 / 钥匙 / 生活用品 / 书籍) | 点标签即可按类别过滤,比打字更快 |
| 发布页表单校验 | 名称、分类、地点、时间、联系方式必填,避免无效信息 |
| 底部导航栏(首页 / 发布 / 我的) | 一键切换页面,操作路径短 |
创意点:把"搜索 + 统计 + 分类筛选"三者放在首页同一屏内,用户无需跳转就能完成大部分操作。
实现思路
- 搜索框:绑定
input事件,实时调用filterItems渲染列表 - 统计卡:调用
storage.js的stats()返回{ total, open, done },渲染三个数字 - 分类标签:点击标签更新全局变量
activeCat,重新调用filterItems - 表单校验:提交时调用
validatePublish(data),返回{ valid, errors },不通过则在页面显示错误 - 底部导航:固定定位
position: fixed,三个链接分别指向index.html、publish.html、my.html
代码片段
片段1:点击标签更新 activeCat,重新渲染列表,实现"点一下即筛选"。
const CATS = ['all','证件卡类','数码产品','钥匙','生活用品','书籍','其他'];
let activeCat = 'all';
document.querySelectorAll('.cat').forEach(el => {
el.onclick = () => {
activeCat = el.dataset.cat;
render();
};
});
片段2:纯函数,检查每个必填项,返回错误列表。发布页调用后,不通过则在页面上显示错误提示。
function validatePublish(data) {
const errors = [];
if (!data.title || !data.title.trim()) errors.push('请填写物品名称');
if (!data.category) errors.push('请选择物品分类');
if (!data.location || !data.location.trim()) errors.push('请填写丢失/拾取地点');
if (!data.time) errors.push('请选择时间');
if (!data.contact || !data.contact.trim()) errors.push('请填写联系方式');
return { valid: errors.length === 0, errors };
}
成果展示
首页:包含顶部搜索框、统计卡(寻物中 / 已解决 / 全部)、分类标签栏(全部 / 证件卡类 / 数码产品 / 钥匙 / 生活用品 / 书籍)以及底部导航栏(首页 / 发布 / 我的)。初始状态列表为空,显示"还没有符合条件的物品"。

发布页:包含物品类型(寻物 / 招领)、物品名称、物品分类、丢失/拾取地点、时间、详细描述、联系方式、发布者昵称等表单项,底部是绿色"发布"按钮和底部导航栏。

五、目录说明和使用说明
目录说明
102401222-102401605/
├── index.html 首页:列表 + 搜索 + 分类筛选
├── publish.html 发布页:寻物/招领表单
├── detail.html 详情页:物品信息 + 联系方式 + 状态更新
├── my.html 我的发布:统计 + 列表 + 状态标记
├── css/
│ ├── reset.css 样式重置
│ └── style.css 全局样式
├── js/
│ ├── storage.js 数据层:localStorage 增删改查
│ ├── search.js 搜索与多条件筛选
│ ├── validate.js 表单校验
│ ├── index.js 首页逻辑
│ ├── publish.js 发布页逻辑
│ ├── detail.js 详情页逻辑
│ └── my.js 我的发布页逻辑
├── tests/
│ ├── storage.test.js
│ ├── search.test.js
│ └── validate.test.js
├── package.json Jest 测试配置
└── README.md
使用说明
- 下载或 clone 整个仓库到本地
- 用谷歌浏览器直接双击打开
index.html - 走一遍主流程:
- 首页浏览 → 搜索/筛选 → 点卡片进详情 → 复制联系方式 → 发布者点"标记为已找到/已归还"
- 如需跑单元测试:
npm install
npm test
预期输出:3 个测试文件全 PASS。
六、单元测试
测试工具与教程
我们选用 Jest 作为单元测试框架,学习方式是通过 B 站视频 + 官方文档快速上手:先了解 test、expect、describe 的基本用法,然后照着官方文档搭建环境、写第一个测试跑通,再逐步给项目里的 storage.js、search.js、validate.js 三个模块补测试。
简易教程:
1.npm init -y 初始化项目;
2.npm install --save-dev jest jest-environment-jsdom 安装依赖;
3. 在 package.json 里配置 "scripts": { "test": "jest" } 和 "jest": { "testEnvironment": "jsdom" };
4. 写测试文件 tests/xxx.test.js,用 test('描述', () => { ... }) 定义用例,用 expect(实际值).toBe(期望值) 断言,用 require('../js/xxx') 引入被测模块;
5. 运行 npm test,看到 PASS 即成功。
部分单元测试代码与说明
测试文件 1:tests/search.test.js —— 测试 filterItems 函数(关键词搜索与多条件筛选)
const { filterItems } = require('../js/search');
const mock = [
{ id:'1', type:'lost', title:'校园一卡通', category:'证件卡类', location:'图书馆', description:'姓名王同学', status:'open' },
{ id:'2', type:'found', title:'白色无线耳机', category:'数码产品', location:'食堂', description:'带充电仓', status:'done' },
{ id:'3', type:'lost', title:'宿舍钥匙', category:'钥匙', location:'操场', description:'小熊挂件', status:'open' },
];
test('关键词命中标题', () => {
expect(filterItems(mock, { keyword:'一卡通' }).length).toBe(1);
});
test('关键词命中描述', () => {
expect(filterItems(mock, { keyword:'充电仓' })[0].id).toBe('2');
});
test('按类别筛选', () => {
expect(filterItems(mock, { category:'钥匙' })[0].id).toBe('3');
});
test('组合筛选:寻物 + 进行中', () => {
expect(filterItems(mock, { type:'lost', status:'open' }).length).toBe(2);
});
test('空条件返回全部', () => {
expect(filterItems(mock, {}).length).toBe(3);
});
测试文件 2:tests/validate.test.js —— 测试 validatePublish 函数(发布表单校验)
const { validatePublish } = require('../js/validate');
test('空表单应报错', () => {
expect(validatePublish({}).valid).toBe(false);
});
test('完整表单应通过', () => {
const r = validatePublish({
title: '黑色雨伞', category: '生活用品',
location: '图书馆', time: '2026-10-09', contact: 'QQ123'
});
expect(r.valid).toBe(true);
});
test('名称超30字应报错', () => {
const r = validatePublish({
title: 'a'.repeat(31), category: '生活用品',
location: 'x', time: '2026-10-09', contact: 'y'
});
expect(r.valid).toBe(false);
});
测试文件 3:tests/storage.test.js —— 测试 add、getById、updateItem、stats 函数(数据层)
const { add, getAll, getById, updateItem, stats } = require('../js/storage');
beforeEach(() => localStorage.clear());
test('add 后 getAll 能读到', () => {
add({ id:'a', title:'伞', status:'open' });
expect(getAll().length).toBe(1);
});
test('updateItem 修改状态', () => {
add({ id:'a', title:'伞', status:'open' });
expect(updateItem('a', { status:'done' })).toBe(true);
expect(getById('a').status).toBe('done');
});
test('stats 统计进行中与已完成', () => {
add({ id:'a', status:'open' });
add({ id:'b', status:'done' });
const s = stats();
expect(s.total).toBe(2);
expect(s.open).toBe(1);
expect(s.done).toBe(1);
});
测试数据构造思路
构造测试数据时,围绕 正常、边界、异常 三类考虑:
- 正常数据:mock 数组放 3 条记录,覆盖"寻物+进行中"、"招领+已完成"、"寻物+进行中"三种组合。
- 边界数据:
title: 'a'.repeat(31)测超长名称;filterItems(mock, {})测空筛选条件;validatePublish({})测空表单。 - 异常数据:
getById('nope')、updateItem('nope', ...)测 id 不存在时的返回值。
针对将来测试人员的刁难,我们考虑了:空数组(返回 [] 不报错)、非法输入(filterItems(null, {}) 通过 opt || {} 兜底)、特殊字符(escapeHtml 转义防 XSS)、localStorage 异常(getAll 里 try/catch,解析失败返回 [] 不白屏)。
七、Github的代码签入记录截图

八、遇到的代码模块异常或结对困难及解决方法
-
搜索后列表没刷新,首页输入关键词后列表没变,要刷新页面才生效。排查发现是输入事件里只更新了变量,没调用重新渲染的函数。在回调里加上
render()后解决。收获是改完数据一定要手动触发一次视图更新。 -
发布页提交后跳转详情页拿不到数据,发布成功后跳转
detail.html,页面却显示"未找到该信息"。排查发现是add(item)时生成的id用的是Date.now(),而详情页用URLSearchParams取到的id是字符串,getById里用===严格比较时类型不一致。把id统一转成字符串后解决。收获是:URL 参数都是字符串,做比较时要统一类型。
九、评价你的队友
- 值得学习的地方:队友对页面样式的细节很用心,配色和排版反复调整过,最终呈现的移动端效果干净整洁。遇到问题时愿意主动沟通,push 前会先本地跑测试确认
- 需要改进的地方:提交 commit 时偶尔忘记先
git pull同步最新代码,导致本地落后远程;建议以后 push 前先拉一次主仓库更新。
浙公网安备 33010602011771号