2026秋软件工程结对作业(第二次)

课程 H202601软件工程与软件工程实践
作业要求 2026秋软件工程结对作业(第二次之程序实现)
作业目标 完成「校园失物招领小程序」程序编写
学号 102401339任宝硕、102401301吴莉芊
第一次作业链接 https://www.cnblogs.com/baoshuo/p/23122595
GitHub 仓库地址 https://github.com/renbaoshuo/102401339-102401301

我们的作品主要以 APP 的形式呈现,同时兼顾了对网页端的支持,但是为了分发便捷我们只在此提供 APK 安装包的下载:https://github.com/renbaoshuo/102401339-102401301/releases/download/v1.0.0-alpha.15/app-release.apk

一、具体分工

我们按页面和模块划分任务,各自开发,完成后互相提 Pull Request 复审。

任宝硕负责工程搭建、底层模块和平台适配。包括创建仓库、初始化 Expo 项目、配置 ESLint 和 GitHub Actions、底部导航、本地存储层(SQLite 和 IndexedDB)、物品业务模块(校验、查询规则、状态流转)、Jest 测试框架接入、编辑已发布信息页面,以及 Web 端和 HarmonyOS 端的适配。

吴莉芊负责应用的五个主要页面和页面里的全部界面组件。首页的卡片、分类标签和搜索栏,搜索页的输入框、筛选面板、结果卡片和空状态提示,详情页的状态徽章和复制提示,发布页的表单控件、日期时间输入、地点选择弹层和发布成功页,我的发布页的发布卡片和统计头部都由她实现。页面需要的示例图片和图标素材也由她整理接入。交互分支她处理得比较细,加载、搜索无结果、表单校验失败这些状态都在页面里做了。此外物品模块的单元测试也是她写的,三个测试文件覆盖了业务校验、查询过滤和状态徽章。

两人共同参与需求梳理和界面核对。每个页面完成后,另一个人在模拟器上实际操作一遍,发现问题再修改。以上的功能划分和两人的提交记录一一对应,见第七节的签入记录。

二、PSP 表格

PSP 2.1 Personal Software Process Stages 预估耗时(分钟) 任宝硕实际(分钟) 吴莉芊实际(分钟)
Planning 计划 35 30 30
Estimate 估计这个任务需要多少时间 15 15 15
Development 开发 1245 1310 1245
Analysis 需求分析(包括学习新技术) 180 260 200
Design Spec 生成设计文档 40 30 35
Design Review 设计复审 30 25 30
Coding Standard 代码规范 30 25 20
Design 具体设计 80 70 75
Coding 具体编码 640 650 660
Code Review 代码复审 60 70 55
Test 测试(自我测试,修改代码,提交修改) 185 180 170
Reporting 报告 90 90 95
Test Report 测试报告 45 45 45
Size Measurement 计算工作量 15 10 10
Postmortem & Process Improvement Plan 事后总结,并提出过程改进计划 30 35 40
合计 1385 1445 1385

和上次作业不同,这次的预估和实际没有完全对上。超时最多的是需求分析里的新技术学习。项目用的是 Expo SDK 55 加 React 19,不少旧教程的写法已经不适用,查文档和试错花的时间比预计多。我们在学习 jest-expo 迁移时发现 react-test-renderer 已经不支持 React 19,换成 React Native Testing Library 后原来的写法要调整,这里多花了约八十分钟。编码基本持平,中间有一次返工是存储层的异步初始化竞态问题,详见后文的困难记录。

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

代码实现思路

这次做的不是只跑在一种设备上的程序。一套业务代码同时覆盖 Android、iOS、HarmonyOS 和 Web 四个平台,手机上是原生 APP,浏览器里打开就是网页版,不需要安装任何客户端。

技术栈是 Expo(React Native 0.83、React 19)加 TypeScript,样式用 NativeWind,导航用 Expo Router。Android 和 iOS 由 Expo 官方工具链支持。HarmonyOS 通过 expo-harmony 接入。Web 端由 Expo 打包,数据存 IndexedDB。四个平台共享同一份业务层和数据模型,差异被隔离在存储层和个别平台分支文件里。

数据模型上,一条失物招领信息就是一个 CampusItem,包含类型(寻物 lost 或招领 found)、状态(进行中 active 或已解决 resolved)、物品名称、地点、日期时间、描述、联系方式、图片和归属人。这个类型定义在 src/features/items/item.ts,全部业务都围绕它展开。

代码分成四层,从下到上依次是存储、业务、状态和界面。

存储层在 src/storage/。手机上用 Expo SQLite,Web 上用 IndexedDB,两者实现同一个 ItemRepository 接口,包含增删改查和状态更新。界面代码不关心数据具体存在哪里,只依赖接口。

业务层在 src/features/items/。item-service.ts 负责校验和状态流转,必填字段、日期格式、不能晚于今天、时间必须是 24 小时制这些都集中在这里,界面不用重复判断。item-query.ts 是纯函数,负责关键词过滤、区域时间筛选和排序,输入物品数组和筛选条件,输出结果数组,方便单元测试。

状态层是 ItemsContext。它把物品数组和增删改方法通过 React Context 分发给所有页面,任何一处修改后列表自动重新排序刷新,首页、搜索页和我的发布页看到的数据始终保持一致。

界面层在 src/app/,按 Expo Router 的文件路由组织。首页负责浏览和分类,搜索页负责关键词加筛选,发布页负责表单,详情页负责展示和复制联系方式,我的发布页负责编辑、删除和状态更新。

基本流程是这样的。用户在发布页选择寻物或招领,填写物品名称、时间、地点、描述和联系方式,前端先做必填检查,通过后交给业务层校验并写入本地数据库,列表刷新后新信息出现在首页。其他用户可以按关键词搜索,也可以按区域和时间筛选,点开卡片进入详情页核对特征,复制联系方式线下联系。物品找回或归还后,发布者在我的发布页把信息标记为已找回或已归还,这条信息在列表里显示为已解决状态。

关键实现的流程图

主流程覆盖发布、浏览、搜索、详情、联系和状态更新六个环节。

flowchart TD Start(["打开应用"]) --> Home["首页列表"] Home -->|点击分类| Tabs["全部 / 寻物 / 招领"] Home -->|输入关键词| Search["搜索页"] Search --> Filter["区域、时间、排序筛选"] Filter --> Results{"有匹配结果?"} Results -->|否| Empty["提示没有找到相关物品,提供清除筛选"] Results -->|是| Detail["查看详情"] Home -->|点击卡片| Detail Detail --> Photo["查看物品图片"] Detail --> Copy["复制联系方式,提示已复制"] Home --> Publish["点击发布"] Publish --> Form["选择寻物或招领,填写表单"] Form --> Check{"必填项完整?"} Check -->|否| Hint["表单底部提示缺少几项,保留已填内容"] --> Form Check -->|是| Validate["业务层校验:日期不能晚于今天、时间 24 小时制"] Validate -->|不通过| Hint Validate -->|通过| Save["写入 SQLite / IndexedDB"] Save --> Success["发布成功页,可查看详情或返回首页"] Success --> Home Home --> Mine["我的发布"] Mine --> Edit["编辑信息,回填原内容"] Edit --> Mine Mine --> Resolve{"确认标记?"} Resolve -->|寻物| Found["标记为已找回"] Resolve -->|招领| Returned["标记为已归还"] Found --> Refresh["列表刷新,卡片显示已解决状态"] Returned --> Refresh Refresh --> Mine

数据流图说明各层之间的调用关系。

flowchart LR UI["界面层<br/>首页 / 搜索 / 发布 / 详情 / 我的"] --> CTX["ItemsContext<br/>物品数组与增删改方法"] CTX -->|调用| SVC["item-service<br/>校验与状态流转"] SVC -->|依赖接口| REPO["ItemRepository 接口"] REPO --> SQL["SQLiteItemsRepository<br/>Android / iOS"] REPO --> IDB["IndexedDBItemsRepository<br/>Web"] SQL --> DB[("本地数据库")] IDB --> DB SVC -->|过滤排序| Q["item-query 纯函数"] CTX -->|修改后重新排序| Q

重要代码片段

发布校验集中在业务层,界面只收集输入。validateContent 去掉首尾空白后检查必填项,日期用 isValidDate 验证格式和真实性(2024-02-30 会被拒绝),不允许晚于今天,时间必须是 24 小时制。校验失败抛出带字段名的 ItemError,表单拿到字段名后定位到对应输入框提示。

function validateContent(input: Partial<ItemContent>, now: number): ItemContent {
  const title = text(input.title, 'title', '物品名称', true);
  const location = text(input.location, 'location', '地点', true);
  const date = text(input.date, 'date', '日期', true);
  const time = text(input.time, 'time', '时间');
  const contact = text(input.contact, 'contact', '联系方式', true);
  if (!isValidDate(date) || date > localDateString(now)) {
    throw new ItemError('validation', '请选择有效的丢失或拾取日期,日期不能晚于今天', 'date');
  }
  if (time && !/^([01]\d|2[0-3]):[0-5]\d$/.test(time)) {
    throw new ItemError('validation', '时间应为 24 小时制,例如 18:40', 'time');
  }
  // ...
}

搜索和筛选是纯函数。关键词同时匹配标题和地点,忽略大小写和首尾空白,区域筛选里实验楼归入教学楼片区,排序支持最新和最早发布,发布时间相同按 id 决定先后。

export function queryItems(items: readonly CampusItem[], filters: ItemFilters = {}): CampusItem[] {
  const now = Date.now();
  const keyword = filters.keyword?.trim().toLowerCase() ?? '';
  const results = items.filter(
    (item) =>
      (!filters.type || filters.type === 'all' || item.type === filters.type) &&
      (!filters.status || item.status === filters.status) &&
      (!filters.ownerId || item.ownerId === filters.ownerId) &&
      matchesArea(item.location, filters.area ?? '全部区域') &&
      matchesTime(item.date, filters.time ?? '全部时间', now) &&
      (!keyword || item.title.toLowerCase().includes(keyword) || item.location.toLowerCase().includes(keyword)),
  );
  const direction = filters.sort === '最早发布' ? 1 : -1;
  return results.sort((a, b) => direction * (a.createdAt - b.createdAt || Number(a.id) - Number(b.id)));
}

状态更新的入口在我的发布页。markResolved 先检查归属,只能操作自己发布的信息,重复标记时直接返回原记录,保证幂等。存储层的 UPDATE 语句带上 owner_id 条件,数据库层面再兜底一次。

async markResolved(id: string): Promise<CampusItem> {
  return mutate(async () => {
    const existing = await ownItem(id);
    if (existing.status === 'resolved') {
      return existing;
    }
    const updated = await storage(() => repository.resolve(existing.id, currentUserId, Date.now()));
    if (!updated) {
      throw new ItemError('not_found', '该信息已被删除或不存在');
    }
    return updated;
  });
}

实现效果展示

按主流程走一遍应用的截图。首页进入后是横幅、搜索框和分类标签,列表默认显示全部信息。切换到寻物分类后,列表只保留寻物信息,已找回的信息显示绿色状态徽章。

首页 首页按分类筛选

搜索页输入关键词提交后显示匹配结果。搜索无结果时页面提示更换关键词,并提供清除筛选按钮,不会白屏。

搜索结果 搜索无结果的空状态

点击卡片进入详情页,显示图片、时间地点、描述和联系方式。复制按钮点击后有提示。

信息详情页

发布页切换寻物或招领,填写必填项。必填内容不完整时提交,表单底部提示还差几项,已填写的内容保留。

发布表单 发布必填校验提示

补全信息后提交,进入发布成功页,可以直接跳转到新发布的信息。

发布成功

我的发布页顶部是发布统计,下方是自己发布的信息。物品找回后点击改为已找回,卡片状态徽章和顶部统计同步更新。

我的发布 标记为已找回后状态更新

编辑页回填原信息,修改后保存。

编辑已发布信息

四、附加特点设计与展示

特点一 区域、时间、排序组合筛选和发布页地点选择器

作业要求的搜索是按关键词找信息,但实际使用中还有一个问题。搜索「校园卡」可能出现十几条结果,失主想缩小范围,最自然的办法是按地点和时间段过滤。我们在搜索页加了区域(教学楼、图书馆、食堂、宿舍、操场)、时间(近 7 天、近 30 天)和排序三组筛选项。区域归并做了一个小处理,实验楼在教学楼对面,选教学楼时实验楼的信息也一并显示。

发布页的地点不用手输,点开后是底部弹层,提供七个常用地点单选。这样发布的信息地点用词统一,区域筛选才能准确命中,也避免了「教学区」「A栋」这类写法导致筛选失效。

实现上筛选逻辑全在 item-query.ts 的纯函数里,界面组件只负责收集条件。

function matchesArea(location: string, area: AreaOption): boolean {
  if (area === '全部区域') {
    return true;
  }
  return area === '教学楼' ? location.includes('教学楼') || location.includes('实验楼') : location.includes(area);
}

筛选面板和地点选择器的实际效果如下。

搜索页筛选面板 发布页地点选择器

特点二 一键复制联系方式

详情页的联系方式旁有复制按钮。点一下号码就进剪贴板,页面底部弹出「联系方式已复制」的提示。没有这个按钮时,用户要手动选中号码再复制,在手机上选中一段文本并不顺手。

复制用 expo-clipboard 实现,提示用 Animated 做淡入淡出。连续点击时先清掉上一次的隐藏定时器,保证提示总显示 1.8 秒后再消失。

const copyContact = async () => {
  if (!item) {
    return;
  }
  await Clipboard.setStringAsync(item.contact);
  setToastVisible(true);
  Animated.timing(toastOpacity, { toValue: 1, duration: 150, useNativeDriver: true }).start();
  if (hideTimer.current) {
    clearTimeout(hideTimer.current);
  }
  hideTimer.current = setTimeout(() => {
    Animated.timing(toastOpacity, { toValue: 0, duration: 200, useNativeDriver: true }).start(() =>
      setToastVisible(false),
    );
  }, 1800);
};
复制联系方式并提示

其他细节

发布页支持最多九张图片,Web 端选图后把 blob 地址转成 data URI 再存储,重启浏览器后图片仍然显示。我的发布页顶部有发布数、进行中和已解决三项统计。

跨平台是贯穿整个项目的设计,四个端共用同一套页面和业务代码,测试人员在哪个平台打开看到的功能都一致,区别只在存储实现,Android、iOS 和鸿蒙端用 SQLite,Web 端用 IndexedDB。

▲ 网页端的访问效果

五、目录说明和使用说明

目录说明

仓库根目录下的 src/ 放全部源码,assets/ 放图标和示例图片,docs/ 放作业文档。android/ 目录由 expo prebuild 生成,用于构建 APK。

├── app.json                  # Expo 应用配置(名称、图标、插件)
├── jest.config.js            # Jest 单元测试配置
├── assets/                   # 应用图标、启动图、页面示例图片
└── src/
    ├── app/                  # Expo Router 页面路由
    │   ├── (tabs)/           #   底部标签页:首页 index、发布 publish、我的 profile
    │   ├── detail/[id].tsx   #   信息详情页
    │   ├── edit/[id].tsx     #   编辑已发布信息
    │   └── search.tsx        #   搜索页
    ├── components/           # 界面组件,按页面分子目录
    │   ├── home/             #   首页卡片、分类标签、搜索栏
    │   ├── search/           #   搜索输入、筛选面板、结果卡片、空状态
    │   ├── publish/          #   表单控件、日期时间选择、地点弹层、成功页
    │   ├── detail/           #   状态徽章、复制提示
    │   └── profile/          #   我的发布卡片、编辑表单、统计头部
    ├── features/items/       # 业务核心
    │   ├── item.ts           #   数据类型和 ItemRepository 接口
    │   ├── item-service.ts   #   校验、归属检查、状态流转
    │   ├── item-query.ts     #   过滤、筛选、排序、统计(纯函数)
    │   └── items-context.tsx #   全局状态管理
    ├── storage/              # 存储实现
    │   ├── sqlite-items-repository.ts     # Android / iOS / HarmonyOS 存储
    │   ├── indexeddb-items-repository.ts  # Web 存储
    │   ├── item-schema.ts    #   建表语句和版本号
    │   └── seed-items.ts     #   首次启动的示例数据
    └── data/                 # 固定展示配置

使用说明

环境要求 Node.js 20 以上和 yarn。测试人员按以下步骤运行。

git clone https://github.com/renbaoshuo/102401339-102401301.git
cd 102401339-102401301
yarn install
yarn start

yarn start 会启动 Metro 开发服务器。按提示按键在对应平台打开,a 启动 Android 模拟器或真机,i 启动 iOS 模拟器,w 在浏览器打开 Web 版。手机上也可以用 Expo Go 扫描终端二维码预览。

Web 版不需要安装任何东西,部署后在浏览器输入地址就能访问,数据保存在浏览器的 IndexedDB 里。本地体验执行 yarn web,也可以用 npx expo export --platform web 导出静态文件,放到任意静态服务器上。

HarmonyOS 端通过 expo-harmony 构建。安装 DevEco Studio 后,在项目里执行 npx expo-harmony prebuild 生成鸿蒙原生工程,再执行 npx expo-harmony run 构建 HAP 并安装到鸿蒙设备或模拟器,详细步骤见 expo-harmony 的 快速开始文档。

需要安装 APK 的话,仓库的 GitHub Actions 在每次推送后自动构建,在 Actions 页面下载 app-release.apk 装到 Android 手机即可。本地构建执行 yarn android 或在 android/ 目录运行 ./gradlew assembleRelease,产物在 android/app/build/outputs/apk/release/。

首次启动应用自带八条示例信息,可以直接体验浏览、搜索和详情。发布一条信息后,在「我的」页面可以看到自己的发布,进行编辑、删除和标记状态。

单元测试在本地运行 yarn test,覆盖率用 yarn test:coverage。

六、单元测试

测试工具的选择和学习

我们选 Jest 加 jest-expo 预设,组件测试用 React Native Testing Library。选型时对照了 Expo 官方文档的单位测试指引。文档提到 react-test-renderer 从 React 19 起不再支持,推荐迁移到 React Native Testing Library,所以我们直接用了后者,没有走弯路。

学习过程主要分三步。先读 Expo 文档的 Unit testing 章节和 jest-expo 的用法,再照官方示例写了第一个组件测试跑通流程,最后结合项目里的纯函数模块补充业务测试。下面是我们的接入过程,也可以当作一份简易教程。

第一步安装依赖。npx expo install 会自动选择和当前 SDK 匹配的版本。

npx expo install jest-expo jest @types/jest -- --dev
npx expo install @testing-library/react-native -- --dev

第二步写配置。我们的 jest.config.js 如下。preset 指定为 jest-expo 后,Expo SDK 的原生模块会被自动 mock。项目里用 @/ 路径别名,Jest 不经过 Metro,所以要用 moduleNameMapper 把别名映射到真实目录,样式导入也要单独转换。

module.exports = {
  preset: 'jest-expo',
  clearMocks: true,
  moduleNameMapper: {
    '^@/assets/(.*)$': '<rootDir>/assets/$1',
  },
  transform: {
    '\\.css$': require.resolve('jest-expo/src/preset/assetFileTransformer'),
  },
  collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'],
};

第三步写测试。jest-expo 会把 *.test.ts 和 *.test.tsx 识别为测试文件,测试文件放在被测模块旁边的 __tests__/ 目录。一个最小的组件测试长这样。

import { render } from '@testing-library/react-native';

describe('组件渲染', () => {
  test('文本正常显示', async () => {
    const { getByText } = await render(<MyComponent />);
    getByText('你好');
  });
});

最后在 package.json 加一条脚本,yarn test 运行,yarn test:watch 监听文件变化自动重跑。我们还把测试放进了 GitHub Actions,每次推送自动执行。

测试代码展示

项目现在有五个测试文件、四十五个用例,全部通过。覆盖三个纯函数模块(item-service、item-query、item-presentation)和两个组件流程(发布日期选择、编辑信息完整流程)。

业务层校验的测试。用内存 Map 实现了一个假仓储,验证 service 的规则而不依赖数据库。

it('拒绝空白物品名称', async () => {
  const service = createItemService(createInMemoryRepository());
  await expectItemError(service.createItem(createInput({ title: '   ' })), 'validation', 'title');
});

it('拒绝晚于今天的日期', async () => {
  const service = createItemService(createInMemoryRepository());
  const future = localDateString(Date.now() + 86400000);
  await expectItemError(service.createItem(createInput({ date: future })), 'validation', 'date');
});

查询过滤的测试。构造四条不同类型、状态、地点和日期的数据,验证关键词匹配、区域归并和时间边界。

it('时间筛选:近7天含第 6 天、近30天含第 8 天,非法日期始终排除', () => {
  expect(ids(queryItems(ITEMS, { time: '近7天' }))).toEqual(['1', '2']);
  expect(ids(queryItems(ITEMS, { time: '近30天' }))).toEqual(['1', '2', '3']);
  const withInvalidDate = [...ITEMS, makeItem({ id: '5', date: 'not-a-date', createdAt: 6000 })];
  expect(ids(queryItems(withInvalidDate, { time: '近30天' }))).toEqual(['1', '2', '3']);
});

组件流程的测试。模拟用户从编辑表单改字段、提交、看到成功提示的完整过程,并验证校验失败时草稿不丢。

it('必填和日期校验失败不写入,保留草稿供修正', async () => {
  await render(<ItemEditForm item={ITEM} />);
  await fireEvent.changeText(screen.getByLabelText('物品名称'), '  ');
  await fireEvent.changeText(screen.getByLabelText('物品描述'), '草稿不丢失');
  await fireEvent.press(screen.getByRole('button', { name: '保存修改' }));
  expect(screen.getByRole('alert')).toHaveTextContent('请填写物品名称');
  expect(updateContent).not.toHaveBeenCalled();
  expect(screen.getByLabelText('物品描述')).toHaveDisplayValue('草稿不丢失');
});

测试数据构造思路

数据构造用工厂函数。makeItem 生成一条完整合法的数据,单个用例只覆盖自己关心的字段,测试意图一目了然。

情况覆盖按白盒思路来。校验函数有多个分支,每个分支至少一个用例,正常路径、空白字符串、纯空格、错误格式(2024/05/20)、不存在的日期(2024-02-30)、未来日期、超出 24 小时制的时间(25:00)各一条。状态徽章是类型乘状态的四格矩阵,用 describe.each 把四种组合跑全。时间筛选专门测了边界,近 7 天包含第 6 天,近 30 天包含第 8 天,非法日期在任何时间范围下都被排除。权限类操作测了三种身份,本人、他人和不存在的信息。

对于将来测试人员的刁难,我们的考虑是他们大概率会输入各种意外内容。空白和纯空格输入、非数字 id、重复标记状态、存储失败后的重试、连续快速点击提交按钮,这些场景在测试里都有对应用例。编辑表单的测试还专门验证了保存失败后输入内容不丢失,可以修正后重试。

这套用例覆盖了业务规则的主要分支,但还没有覆盖存储层和数据库的真实交互,SQLite 仓储目前靠内存仓储替身间接测试。存储层的 SQL 语句正确性主要靠手工在模拟器上验证,这是后面可以补的部分。

七、Github 代码签入记录

仓库由任宝硕创建,吴莉芊 fork 后在自己分支开发,通过 Pull Request 合回 master。每个功能完成并自测后至少签入一次,提交信息按功能命名。

image

image

八、遇到的困难及解决方法

困难一 存储层异步初始化的竞态

问题描述。SQLite 仓储把打开数据库和写入操作都设计成异步的。测试中发现一个偶发问题,应用刚启动就发布信息时,发布操作可能在数据库建表完成前执行,导致写入失败。

做过哪些尝试。最初在每次操作前都检查数据库是否就绪,没有就等待,代码里到处都是判断。后来把打开逻辑改成缓存一个连接 Promise,所有操作在同一个 Promise 队列上排队,前一个完成才执行下一个。

是否解决。解决了。队列保证初始化一定先于任何读写,并发写入也不再交错。这个结构写好后没有再出现启动阶段的写入失败。

有何收获。异步资源的初始化顺序要在架构层面保证,不能靠调用方自觉。把排队逻辑收敛在存储层内部,上层代码完全不用关心数据库是否已打开。

困难二 react-test-renderer 不能用于 React 19

问题描述。搭测试框架时按旧教程用 react-test-renderer 写组件测试,运行时直接报错,提示当前 React 版本不支持。

做过哪些尝试。先尝试降级 React,但 Expo SDK 55 要求 React 19,降级会破坏其他依赖。之后查 Expo 官方文档,确认 react-test-renderer 已被弃用,官方推荐 React Native Testing Library。

是否解决。解决了。换成 React Native Testing Library 后,原来的断言改用 getByLabelText、getByRole 这类查询,写起来比手写渲染树直观,测试还顺带覆盖了无障碍标签。

有何收获。移动端生态版本迭代快,旧教程不能直接照抄。遇到 API 报错先查对应版本文档,比自行猜测快得多。

困难三 日期时间选择在三个平台表现不一致

问题描述。发布页的时间输入用了原生日历控件,Android 和 iOS 正常,Web 端点击后没有反应,无法选择时间。

做过哪些尝试。排查后确认 @react-native-community/datetimepicker 不支持 Web。方案有两个,引入第三方 Web 日历组件,或者给 Web 单独写一个 input[type=datetime-local] 的实现。

是否解决。解决了。利用 React Native 的平台文件约定,把组件拆成 date-time-input.tsx 和 date-time-input.web.tsx,打包时按平台自动选择,两者对上层暴露相同的 props。两端的值都会转成统一的日期和时间字符串再提交。

有何收获。跨平台开发中第三方库不一定覆盖所有平台,平台分支文件是 React Native 提供的标准解法。接口先统一,实现再分平台,业务代码就不需要写平台判断。

九、评价队友

我的队友做的很好值得我学习的地方是,她的页面交付又快又完整,和原型设计高度一致,并且还考虑到了各种边界情况的处理。在面对不懂的地方的时候她也会去主动去网上查找资料学习,合作起来非常的舒服。遇到拿不准的交互她会先说一句自己的理解再动手,两个人对齐的成本很低,不像我自己只会闷头单干。需要改进的地方是她的搜索能力仍有欠缺,偶尔会关注不到信息的及时性。

posted @ 2026-09-29 15:51  宝硕  阅读(63)  评论(2)    收藏  举报