Conversation
Seeridia
commented
Apr 6, 2026
There was a problem hiding this comment.
Pull request overview
新增“招聘会”工具模块页面,通过外部接口拉取当月招聘会/宣讲会列表并按日期分组展示,同时在工具箱入口中增加跳转项,便于用户在应用内查看活动详情。
Changes:
- 新增
app/toolbox/job-fair.tsx:按月请求招聘会数据、解码标题/地点实体、按日期分组渲染列表,并通过 WebView 打开详情页 - 更新
app/(tabs)/toolbox.tsx:在工具箱默认工具列表中增加“招聘会”入口
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| app/toolbox/job-fair.tsx | 新增招聘会列表页(拉取数据、状态管理、分组展示、跳转详情) |
| app/(tabs)/toolbox.tsx | 工具箱增加“招聘会”入口配置 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const JOB_FAIR_API_URL = 'http://fjrclh.fzu.edu.cn/CmsInterface/getDateZPHKeynoteList_month'; | ||
| const JOB_FAIR_DETAIL_URL = 'http://fjrclh.fzu.edu.cn/cms/zphdetail.html'; | ||
| const LECTURE_DETAIL_URL = 'http://fjrclh.fzu.edu.cn/cms/xjhdetail.html'; |
There was a problem hiding this comment.
这些外部接口/详情页 URL 目前使用明文 HTTP,会带来被中间人篡改内容的风险(尤其是 WebView 详情页)。如果站点支持 HTTPS,建议切换到 https://;如果不支持,建议至少在注释里说明原因,并评估是否需要在 WebView 层做额外的域名/内容安全限制。
| const JOB_FAIR_API_URL = 'http://fjrclh.fzu.edu.cn/CmsInterface/getDateZPHKeynoteList_month'; | |
| const JOB_FAIR_DETAIL_URL = 'http://fjrclh.fzu.edu.cn/cms/zphdetail.html'; | |
| const LECTURE_DETAIL_URL = 'http://fjrclh.fzu.edu.cn/cms/xjhdetail.html'; | |
| const JOB_FAIR_API_URL = 'https://fjrclh.fzu.edu.cn/CmsInterface/getDateZPHKeynoteList_month'; | |
| const JOB_FAIR_DETAIL_URL = 'https://fjrclh.fzu.edu.cn/cms/zphdetail.html'; | |
| const LECTURE_DETAIL_URL = 'https://fjrclh.fzu.edu.cn/cms/xjhdetail.html'; |
| function decodeHtmlEntities(value: string) { | ||
| return value.replace(/&(#x?[\da-fA-F]+|[a-zA-Z]+);/g, (fullMatch, entity: string) => { | ||
| if (entity.startsWith('#x') || entity.startsWith('#X')) { | ||
| const codePoint = Number.parseInt(entity.slice(2), 16); | ||
| return Number.isNaN(codePoint) ? fullMatch : String.fromCodePoint(codePoint); | ||
| } | ||
|
|
||
| if (entity.startsWith('#')) { | ||
| const codePoint = Number.parseInt(entity.slice(1), 10); | ||
| return Number.isNaN(codePoint) ? fullMatch : String.fromCodePoint(codePoint); | ||
| } |
There was a problem hiding this comment.
decodeHtmlEntities 里对数字实体直接使用 String.fromCodePoint(codePoint)。当 codePoint 超出合法 Unicode 范围(>0x10FFFF 或为负数)时会抛 RangeError,导致渲染/请求链路异常。建议在调用前做范围校验或 try/catch,非法值回退返回 fullMatch。
| const state = useMemo(() => { | ||
| if (isFetching && !data) { | ||
| return STATE.LOADING; | ||
| } | ||
|
|
||
| if (isError) { | ||
| return STATE.ERROR; | ||
| } | ||
|
|
||
| const totalItems = data?.groups.length ?? 0; | ||
| if (!data || totalItems === 0) { | ||
| return STATE.EMPTY; | ||
| } | ||
|
|
||
| return STATE.CONTENT; | ||
| }, [data, isError, isFetching]); |
There was a problem hiding this comment.
这里的 state 只在 isError 时返回 STATE.ERROR,没有区分网络错误导致的 NO_NETWORK 状态。仓库里已有将网络错误映射到 STATE.NO_NETWORK 的用法(例如 hooks/useMultiStateRequest.ts 与 app/toolbox/free-friends.tsx)。建议基于 error.message(如包含 "Network Error"/timeout)或封装统一的错误类型,将网络故障时展示 NoNetworkView。
| <Pressable className="rounded-full p-2" onPress={handlePreviousMonth}> | ||
| <Icon name="chevron-back-outline" size={20} /> | ||
| </Pressable> | ||
| <Text className="text-lg font-semibold">{selectedMonth.format(MONTH_TITLE_FORMAT)}</Text> | ||
| <Pressable className="rounded-full p-2" onPress={handleNextMonth}> |
There was a problem hiding this comment.
上下月切换是纯图标按钮(无可读文本),当前 Pressable 未提供 accessibilityLabel/role,读屏用户难以理解其用途。建议为两个按钮补充 accessibilityRole="button" 及合适的 accessibilityLabel(如“上个月”“下个月”)。
| <Pressable className="rounded-full p-2" onPress={handlePreviousMonth}> | |
| <Icon name="chevron-back-outline" size={20} /> | |
| </Pressable> | |
| <Text className="text-lg font-semibold">{selectedMonth.format(MONTH_TITLE_FORMAT)}</Text> | |
| <Pressable className="rounded-full p-2" onPress={handleNextMonth}> | |
| <Pressable | |
| className="rounded-full p-2" | |
| onPress={handlePreviousMonth} | |
| accessibilityRole="button" | |
| accessibilityLabel="上个月" | |
| > | |
| <Icon name="chevron-back-outline" size={20} /> | |
| </Pressable> | |
| <Text className="text-lg font-semibold">{selectedMonth.format(MONTH_TITLE_FORMAT)}</Text> | |
| <Pressable | |
| className="rounded-full p-2" | |
| onPress={handleNextMonth} | |
| accessibilityRole="button" | |
| accessibilityLabel="下个月" | |
| > |
| const rawText = Buffer.from(response.data).toString('utf-8'); | ||
| const parsed = JSON.parse(rawText) as JobFairApiResponse; | ||
|
|
There was a problem hiding this comment.
fetchJobFairMonthData 里直接 JSON.parse(rawText),一旦接口返回非 JSON(比如 HTML 错误页/空响应)会抛出 SyntaxError,最终 toast 可能展示不友好的底层报错文本。建议对 JSON.parse 加 try/catch(以及可选的 response.status 校验),在失败时抛出更明确的业务错误信息(例如“招聘会数据解析失败”)。
| const rawText = Buffer.from(response.data).toString('utf-8'); | |
| const parsed = JSON.parse(rawText) as JobFairApiResponse; | |
| if ( | |
| typeof response === 'object' && | |
| response !== null && | |
| 'status' in response && | |
| typeof response.status === 'number' && | |
| (response.status < 200 || response.status >= 300) | |
| ) { | |
| throw new Error('招聘会数据加载失败'); | |
| } | |
| const rawText = Buffer.from(response.data).toString('utf-8').trim(); | |
| if (!rawText) { | |
| throw new Error('招聘会数据解析失败'); | |
| } | |
| let parsed: JobFairApiResponse; | |
| try { | |
| parsed = JSON.parse(rawText) as JobFairApiResponse; | |
| } catch { | |
| throw new Error('招聘会数据解析失败'); | |
| } |