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,数据流为:用户操作 → 调用数据层写入 → 重新读取 → 过滤 → 渲染页面。

流程图或数据流图

flowchart LR A[发布信息<br/>publish.html] --> B[浏览/搜索<br/>index.html] B --> C[查看详情<br/>detail.html] C --> D[联系发布者<br/>复制联系方式] C --> E[更新状态<br/>标记已找到/已归还] A -.写入.-> F[(localStorage)] B -.读取.-> F C -.读取.-> F E -.更新.-> F

代码片段

片段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 };
}

成果展示

首页:包含顶部搜索框、统计卡(寻物中 / 已解决 / 全部)、分类标签栏(全部 / 证件卡类 / 数码产品 / 钥匙 / 生活用品 / 书籍)以及底部导航栏(首页 / 发布 / 我的)。初始状态列表为空,显示"还没有符合条件的物品"。
6ca886b66da38c5fdfe115bef162868f
发布页:包含物品类型(寻物 / 招领)、物品名称、物品分类、丢失/拾取地点、时间、详细描述、联系方式、发布者昵称等表单项,底部是绿色"发布"按钮和底部导航栏。
763640a0cc3986fba2f26c9df89d9738

五、目录说明和使用说明

目录说明

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

使用说明

  1. 下载或 clone 整个仓库到本地
  2. 用谷歌浏览器直接双击打开 index.html
  3. 走一遍主流程:
    • 首页浏览 → 搜索/筛选 → 点卡片进详情 → 复制联系方式 → 发布者点"标记为已找到/已归还"
  4. 如需跑单元测试:
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);
});

测试数据构造思路

构造测试数据时,围绕 正常、边界、异常 三类考虑:

  1. 正常数据:mock 数组放 3 条记录,覆盖"寻物+进行中"、"招领+已完成"、"寻物+进行中"三种组合。
  2. 边界数据:title: 'a'.repeat(31) 测超长名称;filterItems(mock, {}) 测空筛选条件;validatePublish({}) 测空表单。
  3. 异常数据:getById('nope')、updateItem('nope', ...) 测 id 不存在时的返回值。

针对将来测试人员的刁难,我们考虑了:空数组(返回 [] 不报错)、非法输入(filterItems(null, {}) 通过 opt || {} 兜底)、特殊字符(escapeHtml 转义防 XSS)、localStorage 异常(getAll 里 try/catch,解析失败返回 [] 不白屏)。

七、Github的代码签入记录截图

image

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

  • 搜索后列表没刷新,首页输入关键词后列表没变,要刷新页面才生效。排查发现是输入事件里只更新了变量,没调用重新渲染的函数。在回调里加上 render() 后解决。收获是改完数据一定要手动触发一次视图更新。

  • 发布页提交后跳转详情页拿不到数据,发布成功后跳转 detail.html,页面却显示"未找到该信息"。排查发现是 add(item) 时生成的 id 用的是 Date.now(),而详情页用 URLSearchParams 取到的 id 是字符串,getById 里用 === 严格比较时类型不一致。把 id 统一转成字符串后解决。收获是:URL 参数都是字符串,做比较时要统一类型。

九、评价你的队友

  • 值得学习的地方:队友对页面样式的细节很用心,配色和排版反复调整过,最终呈现的移动端效果干净整洁。遇到问题时愿意主动沟通,push 前会先本地跑测试确认
  • 需要改进的地方:提交 commit 时偶尔忘记先 git pull 同步最新代码,导致本地落后远程;建议以后 push 前先拉一次主仓库更新。
posted @ 2026-10-09 21:12  adcbau  阅读(5)  评论(0)    收藏  举报