2026秋软件工程结对作业(第二次之程序实现)

一、作业与结对信息

二、项目简介与范围

“拾见”是我们这次做的校园失物招领网页。想找东西时,翻群聊往往要找很久,所以我们把寻物和招领信息放到一个页面里,让发布、查找、联系和更新状态能接着做下去。

项目简介与范围png

这次先把作业要求的几个主要功能做完整,复杂后台、实名认证、即时聊天和地图定位就没有继续加。考虑到助教需要下载后直接打开网页,我们用 localStorage 保存数据。刷新后记录还在,但换电脑或浏览器就看不到了,这一点也写进了 README。

三、具体分工

分工上,我们选择先后接着做:李嘉星先完成首页、发布和数据接口,李莹莹再 fork 项目,补上搜索、详情和状态更新。最后合并到一起,再检查整个流程。

阶段 成员 工作内容
第一阶段 李嘉星(102401128) 创建仓库和页面结构;设计共享数据模型与本地存储接口;完成首页浏览、类型切换、寻物/招领发布、表单校验、图片预览和响应式基础样式;编写接口约定与队友任务说明。
第二阶段 李莹莹(102401104) fork 项目;实现关键词搜索、详情页、折叠联系方式、“我的发布”、已找到/已归还状态更新;按功能提交 commit,并通过 PR #1 合并。
合并与优化 两人交叉检查,李嘉星集中修复 将搜索改为首页原地筛选;修复返回逻辑、清空搜索、移动端导航和布局;增加组合搜索、只看进行中、认领核对提示、相似线索、非线性动画和桌面/手机适配;完善 README。
测试与博客 两人 李嘉星负责核心规则、存储层与图片读取的自动化用例、运行结果、接口说明及问题修复记录,并汇总博客;李莹莹负责第二阶段搜索、详情、联系方式及状态更新模块的实现说明与复核;双方分别负责自己的 PSP 和队友评价。

先把接口定下来,第二阶段就可以接着同一份数据开发。合并后,我们再回头检查页面之间的跳转和状态是否一致。

协作提交顺序如下:

  1. 李嘉星先在主仓库建立页面骨架、字段约定和共享 Store,完成首页与发布;
  2. 李莹莹从最新 main fork,基于既有接口实现搜索、详情、联系方式、我的发布和状态维护;
  3. 李莹莹发起 PR #1,李嘉星检查并合并;
  4. 合并后两人交叉走查,后续修复搜索逻辑、移动端导航、状态动画和测试问题。

四、PSP 表格

开发前我们先完成了时间预估。复盘后,两人的人工投入合计估算为 1300 分钟,各阶段实际耗时为回溯估算分配,并非逐项计时结果。人机协作中的模型运行等待时间不计入,计入的是需求拆解、提示与审阅、界面调整、调试、验收和博客整理等人工工作。

表中的 Planning、Development 和 Reporting 是父阶段小计,计算总计时不再重复累加它们下面的子项。

PSP 2.1 Personal Software Process Stages 李嘉星预估(分钟) 李嘉星实际(分钟) 李莹莹预估(分钟) 李莹莹实际(分钟)
Planning 计划 30 20 30 20
Estimate 估计任务时间 30 20 30 20
Development 开发(小计) 895 580 905 560
Analysis 需求分析与技术学习 60 30 70 35
Design Spec 生成设计文档 45 25 45 30
Design Review 设计复审 30 15 30 20
Coding Standard 制定代码规范 25 10 25 10
Design 具体设计 75 60 75 55
Coding 具体编码 420 295 420 275
Code Review 代码复审 60 50 60 45
Test 测试、修复和提交 180 95 180 90
Reporting 报告(小计) 125 60 125 60
Test Report 测试报告 60 30 60 30
Size Measurement 工作量统计 20 10 20 10
Postmortem & Process Improvement Plan 事后总结与改进计划 45 20 45 20
合计 1050 660 1060 640

两人的实际投入估算分别比预估少了 390 和 420 分钟,相差 20 分钟,合计 1300 分钟(21 小时 40 分钟)。本次大量使用代码代理辅助实现,缩短了初稿生成和重复编码的时间,但需求仍要自己说清楚,生成的代码也要审阅和调试。后续搜索入口、手机导航和卡片动画的调整,以及边界测试与修复,仍需要人工检查。人工走查已于 2026 年 9 月 30 日完成,测试环境与结果见第 8.4 节。

五、解题思路与设计实现

5.1 从原型到程序的取舍

动手前,我们对照老师的反馈重新看了 HW3 原型。有些页面画出来很完整,真正写代码时还得把操作顺序和异常情况补上,主要调整了下面几点:

  1. 先完成发布、搜索、详情、联系和状态维护,其他功能往后放;
  2. 补全“联系后如何结束信息”的流程:只有本浏览器发布的记录可以在“我的发布”中结束;寻物对应“已找到”,招领对应“已归还”;
  3. 补充异常路径,包括必填项缺失、未来时间、图片格式或大小错误、无搜索结果、无效正则、详情 ID 不存在、复制失败和存储失败;
  4. 明确原型演示与真实能力的区别,不把静态页面写成具有后台、账号认证或跨设备同步能力的系统。

5.2 总体结构

项目没有使用构建工具和远程依赖。index.html 按固定顺序加载 JavaScript:

core.js → storage.js → image-reader.js → app.js → search.js → detail.js → my-posts.js
  • core.js 保存无界面副作用的数据规则,便于独立测试;
  • storage.js 封装 localStorage,所有页面通过同一个 store 读写数据;
  • image-reader.js 管理图片读取,取消旧任务并忽略过期回调,防止连续选图时预览和保存内容不一致;
  • app.js 负责导航、首页、发布、通用卡片和页面注册;
  • pages/detail.js 与 pages/my-posts.js 分别注册详情页和我的发布页;
  • style.css 统一管理桌面端、移动端、状态和动画样式。

三个页面都从同一个 Store 取数据。这样在“我的发布”里点了“已找到”,回到首页或者详情时也会看到新状态,不用再分别修改几份数组。

5.3 数据结构与状态规则

每条信息包含以下主要字段:

{
  id, ownerId, type, name, location,
  date, time, description, contact,
  imageData, status, createdAt
}

type 为 lost 或 found。寻物初始状态是“寻找中”,结束状态是“已找到”;招领初始状态是“招领中”,结束状态是“已归还”。首次打开网页时,程序生成浏览器自己的 clientId;新发布记录使用该 ID 作为 ownerId,“我的发布”只查询相同 ownerId 的记录。

这里的 clientId 只是用来区分本浏览器发布的记录,还没有真正的账号系统。换电脑、换浏览器或清除数据后,记录不会同步。

5.4 完整业务流程

核心业务流程png

搜索、类型筛选和“只看进行中”可以同时生效。用户从筛选结果进入详情后,返回首页仍保留搜索词、类型和状态条件,避免重新操作。

5.5 关键数据流

关键数据流png

发布时先调用 validateDraft,再由 createItem 生成规范化记录,最后写入 store。搜索调用 listItems;结束信息调用 finishItem,它会检查记录是否存在、是否属于当前浏览器、是否已经结束。

5.6 关键代码一:表单校验

function validateDraft(draft, now = new Date()) {
  const errors = {};
  if (!VALID_TYPES.includes(draft.type)) errors.type = "请选择寻物或招领";
  if (!text(draft.name)) errors.name = "请填写物品名称";
  if (!text(draft.location)) errors.location = "请填写地点";
  if (!text(draft.date)) errors.date = "请选择日期";
  if (!text(draft.time)) errors.time = "请选择发生时间";
  if (!text(draft.contact)) errors.contact = "请填写 QQ 或校内账号";

  if (text(draft.date) && text(draft.time)) {
    const date = text(draft.date);
    const time = text(draft.time);
    const happenedAt = new Date(`${date}T${time}:00`);
    const [year, month, day] = date.split("-").map(Number);
    const [hour, minute] = time.split(":").map(Number);
    if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !/^\d{2}:\d{2}$/.test(time) ||
        Number.isNaN(happenedAt.getTime()) || happenedAt.getFullYear() !== year ||
        happenedAt.getMonth() + 1 !== month || happenedAt.getDate() !== day ||
        happenedAt.getHours() !== hour || happenedAt.getMinutes() !== minute) errors.date = "日期或时间无效";
    else if (happenedAt.getTime() > now.getTime()) errors.date = "发生时间不能晚于现在";
  }
  if (text(draft.name).length > 60) errors.name = "物品名称不能超过 60 字";
  if (text(draft.location).length > 100) errors.location = "地点不能超过 100 字";
  if (text(draft.description).length > 500) errors.description = "补充描述不能超过 500 字";
  if (text(draft.contact).length > 100) errors.contact = "联系方式不能超过 100 字";
  return errors;
}

我们把校验和页面显示分开,函数只返回哪些字段有问题。日期解析后还要逐项核对年月日和时分,避免 JavaScript 把“2 月 30 日”自动转成 3 月日期后误判为合法。测试时还能传入固定的 now,这样测“未来时间”就不用担心结果随着运行日期变化。

5.7 关键代码二:发布者结束信息

function finishItem(items, id, ownerId) {
  const item = getItem(items, id);
  if (!item) return { items, error: "信息不存在", item: null };
  if (!text(ownerId) || item.ownerId !== ownerId) {
    return { items, error: "只能修改自己发布的信息", item: null };
  }
  if (isFinished(item)) return { items, error: "这条信息已结束", item };
  const updated = { ...item, status: FINISHED_STATUS[item.type] };
  return {
    items: items.map((entry) => entry.id === id ? updated : entry),
    error: null,
    item: updated,
  };
}

结束信息前要先检查三件事:记录在不在、是不是自己发布的、是否已经结束。通过后只替换对应的记录,其他信息保持原样。页面按钮之外再做这层检查,也能避免重复操作或修改别人的记录。

5.8 关键代码三:组合搜索

return {
  terms: query
    .split(/[\s,,、]+/u)
    .filter(Boolean)
    .map(normalizeSearchText),
  regex: null,
  error: null,
};

普通搜索支持空格、英文逗号、中文逗号和顿号分隔多个线索。每个线索可以分别命中名称、描述或地点,但所有线索都必须命中。例如“东三 雨伞”和“东3,雨伞”都能匹配地点为“东3”、名称含“雨伞”的记录。数字归一化还支持“一/二/三”与“1/2/3”的常见写法。

六、附加特点设计与展示

6.1 智能组合搜索与高级正则搜索

搜索时可以把记得的线索一起输进去,比如“东三 雨伞”。程序会拆开关键词,要求每个词都能在名称、描述或地点中找到。“东三”和“东3”也会按同一种写法处理。想同时找雨伞或耳机,可以用 /雨伞|耳机/;正则写错时会显示提示。

正则采用受限语法,支持字面量、字符类、锚点、或条件及简单重复,例如 /^蓝色.*雨伞$/;表达式最多 80 个字符,标志支持 i、m、s、u。为避免搜索卡顿,不支持分组、回溯引用、Unicode 属性类或花括号重复,每个或条件分支最多一个 *、+ 或 ?。错误或不支持的输入会显示提示,并保留上次有效查询结果。

加这个功能主要是考虑到,大家不一定记得物品的完整名称,地点也可能有人写“东三”、有人写“东3”。允许把零碎线索组合起来,查找时就少一些限制。

组合搜索效果见第 12 节:电脑端输入“玫瑰 卡”匹配校园卡;手机端输入“东三 305 耳机”匹配东3-305的无线耳机,展示多个关键词与地点数字归一化共同生效。

第 12 节进一步展示桌面端与手机端的正则搜索:/雨伞|耳机/ 返回雨伞和耳机两类记录,/[/ 触发错误提示并保留上次有效查询结果。正常查询和错误输入都有明确反馈。

6.2 自动发现相似线索

发布成功后,程序自动在“相反类型且仍进行中”的记录中查找候选:寻物只匹配招领,招领只匹配寻物。如果地点完全相同,或物品名称存在至少两个字的共同片段,就在“我的发布”中显示最多三条可能相关的线索和匹配原因。

function similarityReasons(source, candidate) {
  if (!source || !candidate || source.id === candidate.id ||
      source.type === candidate.type ||
      isFinished(source) || isFinished(candidate)) return [];
  const reasons = [];
  const sourceLocation = normalizeSearchText(source.location).replace(/\s+/g, "");
  const candidateLocation = normalizeSearchText(candidate.location).replace(/\s+/g, "");
  if (sourceLocation && sourceLocation === candidateLocation) {
    reasons.push(`地点相同:${text(source.location)}`);
  }
  const candidateName = normalizeSearchText(candidate.name)
    .replace(/[^\p{L}\p{N}]+/gu, "");
  const shared = nameFragments(source.name)
    .filter((fragment) => candidateName.includes(fragment))
    .sort((a, b) => b.length - a.length || a.localeCompare(b))[0];
  if (shared) reasons.push(`名称共同关键词:${shared}`);
  return reasons;
}

相似线索不一定就是同一件物品,所以这里只给提示,让用户点进详情后自己核对。以后同一个浏览器里又发布了新信息,再打开“我的发布”也会重新匹配已有记录。

实现效果见第 12.1 节“我的发布:更新前”截图:校园卡下方显示可能相关的招领记录,用户可以从线索卡片进入详情继续核对。

6.3 隐私与认领核对提示

发布招领信息时,页面提醒发布者只公开物品的大致特征,保留一项独特细节,联系后让认领者先描述该细节。联系方式默认折叠,只有用户主动点击才显示;详情页还提供一键复制,并在剪贴板 API 不可用时降级为传统复制或选中文字供手动复制。

如果把物品的所有独特特征都写出来,认领时反而不好核对,所以我们加了这个提醒。联系方式可以主动展开和复制,操作也比较直接。第 12 节放了招领提示和联系方式的截图。

6.4 “只看进行中”与局部动画

“只看进行中”可以和关键词、类型筛选一起用。我们后来把动画范围缩小到要隐藏的卡片,避免开关一切换,整个页面都跟着动。设置了 prefers-reduced-motion 的用户会看到减少动画后的效果。

第 12.1 节展示电脑端开关关闭时的 8 条记录与开启后的 4 条记录;第 12.2 节展示手机端开启后的列表。截图说明状态筛选结果,动画效果可在页面中切换开关观察。

七、目录说明与使用说明

7.1 目录结构

.
├── README.md                     项目说明、运行步骤和数据限制
├── index.html                    Chrome 直接打开的网页入口
├── css/
│   └── style.css                 桌面端、移动端和动画样式
├── js/
│   ├── core.js                   校验、搜索、状态和相似匹配规则
│   ├── storage.js                localStorage 与统一数据接口
│   ├── image-reader.js           最新图片读取与页面生命周期保护
│   ├── app.js                    导航、首页、发布和通用卡片
│   └── pages/
│       ├── search.js             早期独立搜索页兼容代码
│       ├── detail.js             详情、联系方式和复制功能
│       └── my-posts.js           我的发布、状态更新和相似线索
├── tests/
│   ├── core.test.js              校验、搜索、状态和相似匹配测试
│   ├── storage.test.js           本地保存、回滚、迁移和损坏数据测试
│   └── image-reader.test.js      连续选图、清空选择及页面退出测试
├── docs/
│   ├── 接口约定.md                第二阶段使用的接口说明
│   └── img/                     项目范围、业务流程及数据流 SVG/PNG
└── prototype/
    ├── App.tsx                   HW3 原型源码,仅作设计参考
    ├── 校园失物招领小程序_核心流程.mmd
    ├── 校园失物招领小程序_需求分析与试用记录.md
    └── 校园失物招领小程序_过程记录.md

7.2 测试人员运行方法

  1. 从 GitHub 下载 ZIP 或执行:
git clone https://github.com/kasinglee/102401128-102401104.git
  1. 进入项目目录;

  2. 使用 Google Chrome 直接打开根目录的 index.html;

  3. 运行网页无需安装 Node.js、依赖、数据库或服务器;执行单元测试需要 Node.js;

  4. 首次打开会显示三条虚构演示数据;新发布数据保存在当前浏览器;

  5. 清除记录时,按 F12 打开 Chrome 开发者工具,在 Application(应用)→ Local Storage(本地存储)中选择当前页面对应的来源,删除键名 shijian.hw4.v1,再刷新网页。此操作会清除本浏览器新增的记录、图片和状态更新,并恢复三条初始演示数据。

Chrome 开发者工具中的本地存储记录

推荐按以下路径验收:发布寻物 → 首页搜索 → 查看详情 → 展开并复制联系方式 → 我的发布 → 标记已找到 → 返回首页确认状态同步。再发布一条招领信息,检查“已归还”和相似线索。

八、单元测试

8.1 测试工具与学习过程

测试部分参考了邹欣老师的《单元测试与回归测试》和 Node.js 的 node:test 文档。写法可以理解为:准备一组输入,调用函数,再用断言检查结果。我们选了 Node.js 自带的 node:test 和 node:assert/strict,不用额外安装测试框架。core.js、storage.js 和 image-reader.js 可以独立测试,图片读取测试通过模拟 FileReader 控制回调先后顺序;运行环境是 Node.js 24.18.0。

简易教程:

  1. 安装 Node.js,在项目根目录打开终端;
  2. 在 tests/*.test.js 中用 require('node:test') 和 require('node:assert/strict') 导入测试工具,再导入目标模块;
  3. 用 test('行为描述', () => { ... }) 写一个独立用例;
  4. 构造固定输入,例如固定的 now 和内存版 storage,调用目标函数;
  5. 用 assert.equal、assert.deepEqual、assert.match 检查正确结果、错误分支和数据未被意外修改;
  6. 运行 node --test,本次结果为 30 个通过、0 个失败;
  7. 运行 node --test --experimental-test-coverage 查看覆盖率。修复代码后再次执行两条命令,检查原有行为是否退化。

8.2 白盒测试用例设计

白盒测试这部分,我们先看函数里的判断,再给每个分支准备输入。比如必填项为空、时间在未来、不是发布者本人、已经结束,分别应该返回什么错误。长度限制还要测刚好到上限和超过一个字的情况,这里用到了判定覆盖和边界值分析。每个用例单独准备数据,日期固定;存储用内存对象模拟,不会改动浏览器里的真实记录。

编号 测试目标 输入或场景 预期结果 实际结果
T01–T05 表单与创建 合法草稿、空白必填、无效日期、未来时间、60/61 字边界、招领状态及缺少编号 各分支返回明确结果 通过
T06–T08 搜索解析 空格/逗号组合、中文数字与全角归一化、合法及错误正则 命中正确记录或返回可读错误 通过
T09–T10 列表规则 类型/发布者/地点/状态组合筛选,进行中优先排序 只返回符合条件的记录且排序正确 通过
T11–T13 状态维护 寻物结束、招领结束、旧状态兼容、记录不存在、越权和重复结束 正确更新或拒绝;输入不被原地修改 通过
T14–T15 相似线索 同地点或名称片段匹配,同类型/已结束/自身排除,候选上限 只返回符合规则的候选 通过
T16–T22 本地存储 初始化、重开后持久化、发布失败回滚、结束失败回滚、旧数据迁移、订阅取消、无效 JSON 数据保持一致且失败有明确反馈 通过
T23–T24 日期与正则边界 不存在的日期、非闰年闰日、越界时间、危险重复正则及常用正则 拒绝非法输入,保留正常匹配能力 通过
T25–T27 结构损坏数据 null 条目、缺字段、异常状态、重复编号、空身份及 reload 迁移 过滤坏条目,保留有效记录与发布者,异常数据不导致白屏 通过
T28–T30 图片读取竞态 A/B 连续选择、旧回调晚到、清空选择、页面退出及读取失败 只使用最新有效结果,旧任务取消且不能覆盖新图片 通过

8.3 部分测试代码

以下片段来自实际运行的 tests/core.test.js(T11):

test('T11 正确发布者结束寻物,输入数组不被原地修改', () => {
  const records = [item()];
  const result = core.finishItem(records, 'lost-1', 'owner-a');
  assert.equal(result.error, null);
  assert.equal(result.item.status, '已找到');
  assert.equal(records[0].status, '寻找中');
  assert.notEqual(result.items, records);
});

这个测试不仅检查结果状态,也检查输入数组没有被原地修改。越权、重复结束和不存在 ID 则覆盖同一函数的其他判断分支。

8.4 如何考虑测试人员的“刁难”

自动化用例覆盖了纯空格、中英文地点数字混用、错误与危险正则、长度边界、未来及不存在的日期、重复结束、越权修改、模拟存储空间不足、结构损坏数据与图片读取竞态。运行结果为 30 个通过,0 个失败;core.js、storage.js 和 image-reader.js 合计行覆盖率 98.93%、分支覆盖率 87.05%、函数覆盖率 96.97%。

这些测试覆盖了核心规则、存储接口和图片读取控制器的主要判断分支,但覆盖率不包含页面 DOM 与 CSS,也不代表所有浏览器交互都已验证。布局、真实文件选择和剪贴板权限还需要结合人工走查评估。

自动化测试之外,我们于 2026 年 9 月 30 日在电脑端 Google Chrome 中进行了人工走查,检查了两种 PC 视口尺寸,并通过开发者工具模拟 iPhone 16 Pro Max 的移动端视口。所检查的页面布局与操作未发现异常;移动端结果来自浏览器模拟,尚未在真实手机上验证。

自动化测试与覆盖率结果

在项目根目录运行 node --test --experimental-test-coverage,30 个测试通过,没有失败。通过率看的是现有测试有没有出错,覆盖率则帮我们找还没测到的地方。

tests 30
pass 30
fail 0

文件              行覆盖率    分支覆盖率    函数覆盖率
core.js           98.58%     86.76%       96.97%
image-reader.js  100.00%     90.48%       90.00%
storage.js        99.22%     86.57%      100.00%
合计              98.93%     87.05%       96.97%

九、GitHub 提交与协作记录

项目按功能拆分 commit,例如:

  • feat: define shared item rules and browser store
  • feat: add offline home and publishing flow
  • feat: add homepage search entry and keyword search page
  • feat: build item detail page with folded contact
  • feat: add my posts status flow
  • feat: support combined clues and regex search
  • feat: add active posts only filter
  • feat: automatically suggest similar opposite posts
  • style: polish surfaces and add eased motion
  • fix: guard regex search, stored records, dates and image reads

李莹莹在 fork 中完成第二阶段后提交 PR #1,李嘉星检查并合并;合并以后再按实际问题逐项修复。提交信息使用 feat、fix、style、docs、refactor 等前缀说明改动目的。

按功能提交的代码记录

下面的记录能看到两人各自的提交,以及 PR #1 的合并。搜索、详情、状态筛选和动画分别提交,后面发现的问题也单独修复,回头找某次改动比较方便。

GitHub 两位成员的提交与合并记录

通过 Pull Request 合并第二阶段功能

这是李莹莹提交并已合并的第二阶段 PR,包含搜索、详情和“我的发布”等功能,合并记录可以在上一张截图中看到。

第二阶段 PR 列表与协作记录

十、遇到的问题、尝试与解决方法

10.1 搜索页面的空间和返回逻辑不自然

  • 问题:早期版本点击搜索后进入单独页面,视觉上像突然进入发布页;返回箭头位置也与正文宽度不一致。
  • 尝试:先补充返回按钮和搜索页面说明,再结合实际操作重新评估信息层级。
  • 解决:将搜索框移动到首页,在当前列表上直接筛选;从结果进入详情后返回时保留筛选条件。
  • 收获:单看搜索页没什么问题,连着首页和详情一起用才发现别扭。以后检查页面时要顺着操作流程走,不能只看每页能不能打开。

10.2 手机导航和内容密度问题

  • 问题:移动端导航曾出现在页面顶部或随内容滚动,发布表单与信息流也显得过密。
  • 尝试:调整 header、底部导航、页面安全间距、卡片间距和表单字段间距,并在多个手机宽度截图下观察。
  • 解决:手机端品牌居中,导航固定在视口底部;正文预留底部空间;首页、发布和我的页面分别调整密度。
  • 收获:电脑上看着合适的间距,到手机上可能就很挤。底部导航、按钮大小和表单间距都得单独看。

10.3 “只看进行中”导致整个页面跳动

  • 问题:初版切换筛选时重新渲染整个页面,用户感觉所有内容都在动。
  • 尝试:缩短动画时间仍然没有解决空间跳变。
  • 解决:只给即将隐藏的已结束卡片添加消失状态,动画结束后移除这些卡片;关闭筛选时只补回缺失卡片。
  • 收获:这次只让结束的卡片动就够了,整页都动反而让人找不到刚才看的位置。

10.4 静态网页的数据共享限制

  • 问题:作业要求下载文件后直接打开,项目没有服务器,因此无法实现真实校园用户之间的数据同步和登录权限。
  • 尝试:分析是否加入后端,但这会增加部署和测试环境要求,也超出本次核心范围。
  • 解决:使用 localStorage 演示完整流程,通过浏览器生成的 clientId 判断“本浏览器发布”;在 README 和页面中明确限制。
  • 收获:直接打开 HTML 很方便,但也带来了数据只能保存在本地的限制。使用说明里要把这一点写清楚,否则别人换个浏览器就会以为信息丢了。

10.5 边界检查暴露的四类问题

  • 问题:复杂正则可能导致搜索卡顿,合法 JSON 中的损坏条目可能使页面初始化报错,JavaScript 会自动归一化不存在的日期,连续选择图片时旧读取结果也可能覆盖新图片。
  • 尝试:构造危险重复正则、含 null 的记录、2 月 30 日与 A/B 图片回调乱序,检查原有测试未覆盖的路径。
  • 解决:限制正则语法;初始化与重新加载时过滤坏条目;解析日期后核对年月日与时分;切换图片或退出发布页时取消旧读取,并忽略过期回调。新增 T23–T30 八个回归用例,全部通过。
  • 收获:常规流程走通后还要检查边界输入和异步时序。修复后重新运行全套测试,能及时发现对已有功能的影响。

十一、队友评价

李嘉星对李莹莹的评价

值得学习的地方:她负责的搜索、详情和“我的发布”接在我写的基础接口上,还通过 fork 和 PR 完成了合并。几个页面都沿用了同一个 Store,后面联调时不需要再整合两套数据,这一点让我省了不少事。

可以改进的地方:合并后,搜索入口和返回按钮这些地方还需要调整。以后做完一个页面,可以多顺着用户的操作点几步,比如从搜索结果进详情,再返回首页,看看条件还在不在。空列表、无结果和重复操作也可以早点一起检查。

李莹莹对李嘉星的评价

值得学习的地方:他前面把数据接口和页面结构整理好了,我接着做功能时有东西可以参考。后面他也一直在改搜索、手机布局和动画这些细节,README 和测试也补得比较完整,方便别人下载后运行和检查。

可以改进的地方:有些交互是在代码写完后才重新确定的,比如搜索最后改成了首页筛选,布局也改过几轮。下次可以先把页面跳转和手机端布局商量清楚再分工。用代码代理帮忙时,要求也最好一次写具体一点,少一些生成以后再来回改的情况。

十二、成果展示与总结

下面是完成后的页面。发布、搜索、查看详情、获取联系方式和更新状态都已经接起来,另外加了相似线索、认领提示和状态筛选。电脑和手机尺寸都做了调整,下载后用 Chrome 打开就能运行。

12.1 电脑端

首页浏览

首页以双列卡片集中展示寻物和招领信息,物品名称、地点、时间及状态可以直接查看;搜索框和类型、状态筛选放在列表上方。

电脑端首页信息列表

组合搜索:地点与物品名称一起匹配

输入“玫瑰 卡”后,地点和物品关键词同时参与匹配,首页留下两条相关校园卡记录。搜索直接作用于当前列表,用户可以继续切换类型或状态,也可以一键清除搜索。

电脑端组合搜索玫瑰与校园卡

高级正则:同时查找雨伞或耳机

输入 /雨伞|耳机/,其中 | 表示“或”,列表同时返回黑色无线耳机与蓝色折叠雨伞。与普通关键词组合的 AND 匹配不同,正则可以表达多个物品任选其一的查询条件。

电脑端正则匹配雨伞或耳机

错误正则:明确提示并保留上次有效结果

输入 /[/ 时,方括号没有闭合,搜索框下方显示“正则表达式无效,请检查括号、转义或标志”。无效输入不会作为新的搜索条件执行,列表与搜索摘要仍保留上一次有效查询的结果,用户可以修改表达式后重试。

电脑端无效正则错误提示

“只看进行中”:筛选前后对比

关闭开关时共有 8 条记录,已找到或已归还的信息也保留供查看;开启后只显示 4 条进行中记录,减少无效联系。下方两张截图依次展示关闭和开启的结果;静态截图展示筛选结果,卡片消失动画需实际操作观察。

电脑端关闭只看进行中显示八条记录

电脑端开启只看进行中显示四条记录

发布寻物

选择“我在寻物”,填写名称、地点、发生时间和联系方式;描述与图片可选。桌面端表单限制宽度并居中展示,避免输入框过长。

电脑端发布寻物表单

发布招领与认领核对提示

切换到“我来招领”后出现认领核对提示,提醒发布者保留一项独特特征,联系后再核对,减少冒领风险。

电脑端发布招领与认领核对提示

详情与联系方式

详情页集中展示物品状态和具体信息。联系方式可展开、收起,并提供复制按钮;返回按钮方便用户回到原来的列表。

电脑端详情页展开联系方式

我的发布:更新前

“我的发布”区分进行中与已结束记录。进行中的校园卡提供“标记已找到”按钮,并展示一条可能相关的招领线索。

电脑端我的发布与相似线索

我的发布:更新后

点击“标记已找到”后,校园卡状态改为“已找到”,进行中数量减少,重复结束按钮消失,并出现状态更新提示。

电脑端我的发布标记已找到后

12.2 手机端

手机上把卡片改成单列,Logo 放在顶部中间,“首页 / 发布 / 我的”放到底部。下面按操作顺序放了截图。

首页浏览 发布寻物
单列卡片展示物品信息,搜索和筛选位于列表上方。 表单适配窄屏,字段分组清楚,底部导航便于切换页面。
手机端首页 手机端寻物发布
发布招领 查看详情
选择招领后展示认领核对提示,提醒保留独特特征。 展示上传图片和物品信息,展开联系方式后可联系发布者。
手机端招领发布提示 手机端物品详情
状态更新前 状态更新后
进行中的记录提供“标记已找到”按钮,此时共有两条进行中记录。 课本变为“已找到”,进行中数量降为一条,并显示成功提示。
手机端我的发布更新前 手机端我的发布更新后
组合搜索:东三 305 耳机 只看进行中
多个线索同时匹配,“东三”与“东3”等价,结果只留下东3-305的无线耳机。 开启状态筛选后,只展示寻找中与招领中的记录,已结束信息被隐藏。
手机端组合搜索东三305与耳机 手机端开启只看进行中
高级正则搜索 错误正则提示
使用正则表达式匹配“雨伞或耳机”,结果显示两条记录。 输入 /[/ 后显示红色错误提示,并保留上次有效搜索的结果。
手机端正则匹配雨伞或耳机 手机端无效正则错误提示

这次做下来,基础功能实现后还有很多小地方要改:搜索完怎么返回、清空输入后列表要不要马上恢复、手机底部导航会不会挡住内容、切换状态时哪些卡片该动。这些在原型里不太明显,真正点起来才会注意到。下次我们会早点把完整流程走一遍,少留一些问题到合并后再改。如果继续做这个项目,下一步会考虑后端和账号,让不同用户能够看到同一份失物信息。

posted @ 2026-09-30 14:25  Jasonxdd  阅读(21)  评论(0)    收藏  举报