From a6497caf73e73f1c4b182f35a08c438c39533834 Mon Sep 17 00:00:00 2001 From: Sakurasan <26715255+Sakurasan@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:49:27 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=97=A0=E9=A2=86=E6=B6=A8=E8=82=A1?= =?UTF-8?q?=E9=A2=98=E6=9D=90=E5=AF=BC=E8=87=B4/themes=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=B4=A9=E6=BA=83(startsWith=20null)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 东方财富题材列表存在 securityCode 为 null 的题材(如"铟"01726), getStockBoard(null) 调用 startsWith 抛 TypeError,React 渲染崩溃, 页面被错误边界接管。修复 getStockBoard 使其对空值兜底返回主板, 同时保护全部 8 处调用方;ThemeItem.securityCode/securityName 类型 如实标注为 string|null。新增最小回归脚本复现并验证修复。 Co-Authored-By: Claude --- src/lib/stock-api.ts | 4 ++- src/lib/theme-api.ts | 4 +-- tests/_stub-api-client.cjs | 5 +++ tests/get-stock-board-repro.mjs | 63 +++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 tests/_stub-api-client.cjs create mode 100644 tests/get-stock-board-repro.mjs diff --git a/src/lib/stock-api.ts b/src/lib/stock-api.ts index 930d064..0d3825a 100755 --- a/src/lib/stock-api.ts +++ b/src/lib/stock-api.ts @@ -97,7 +97,9 @@ export interface BoardInfo { * - 8/4/920开头:北交所(BJB) * - 其他:主板 */ -export function getStockBoard(code: string): BoardInfo { +export function getStockBoard(code: string | null | undefined): BoardInfo { + // 题材列表等场景下 securityCode 可能为 null(无领涨股的题材),兜底为主板 + if (!code) return { board: "main", label: "", className: "" }; if (code.startsWith("688")) { return { board: "kcb", label: "科", className: "bg-red-500/10 text-red-500 border-red-500/30" }; } diff --git a/src/lib/theme-api.ts b/src/lib/theme-api.ts index ac44ae6..88d9b6b 100644 --- a/src/lib/theme-api.ts +++ b/src/lib/theme-api.ts @@ -6,8 +6,8 @@ import { getApiBaseUrl } from "@/lib/api-client"; export interface ThemeItem { themeCode: string; themeName: string; - securityName: string; // 领涨股名称 - securityCode: string; // 领涨股代码 + securityName: string | null; // 领涨股名称(无领涨股的题材为 null) + securityCode: string | null; // 领涨股代码(无领涨股的题材为 null) codeWithSuffix: string; hotRank: number; // 热度排名 f3: number | null; // 领涨股涨幅 diff --git a/tests/_stub-api-client.cjs b/tests/_stub-api-client.cjs new file mode 100644 index 0000000..2dc2bf2 --- /dev/null +++ b/tests/_stub-api-client.cjs @@ -0,0 +1,5 @@ +// stub:getStockBoard 测试用,仅用于解析 @/lib/api-client 别名(CommonJS) +function getApiBaseUrl() { + return "http://localhost:8000"; +} +module.exports = { getApiBaseUrl }; diff --git a/tests/get-stock-board-repro.mjs b/tests/get-stock-board-repro.mjs new file mode 100644 index 0000000..8892ff0 --- /dev/null +++ b/tests/get-stock-board-repro.mjs @@ -0,0 +1,63 @@ +// 最小复现/验证脚本:直接编译 src/lib/stock-api.ts 真实源码, +// 调用 getStockBoard(null),复现「Cannot read properties of null (reading 'startsWith')」。 +// 无测试框架,node 直接运行:node tests/get-stock-board-repro.mjs +import fs from "node:fs"; +import path from "node:path"; +import Module from "node:module"; +import ts from "typescript"; + +const srcPath = path.resolve("src/lib/stock-api.ts"); +const source = fs.readFileSync(srcPath, "utf8"); + +const js = ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, esModuleInterop: true }, +}).outputText; + +const mod = new Module(srcPath); +mod.filename = srcPath; +mod.paths = Module._nodeModulePaths(path.dirname(srcPath)); + +// 拦截 @/lib/api-client 别名导入(getStockBoard 本身不依赖它) +const origResolve = Module._resolveFilename; +Module._resolveFilename = function (request, ...args) { + if (request === "@/lib/api-client") return path.resolve("tests/_stub-api-client.cjs"); + return origResolve.call(this, request, ...args); +}; + +try { + mod._compile(js, srcPath); +} finally { + Module._resolveFilename = origResolve; +} + +const { getStockBoard } = mod.exports; + +// ---- 断言 ---- +function assertThrows(fn, label) { + try { + fn(); + console.log(`✗ ${label}: 未抛错`); + process.exitCode = 1; + } catch (e) { + console.log(`✓ ${label}: 抛错 -> ${e.message}`); + } +} + +function assertNoThrow(fn, label) { + try { + const r = fn(); + console.log(`✓ ${label}: 未抛错 -> ${JSON.stringify(r)}`); + return r; + } catch (e) { + console.log(`✗ ${label}: 抛错 -> ${e.message}`); + process.exitCode = 1; + } +} + +// 正常代码 +assertNoThrow(() => getStockBoard("600000"), "正常代码 getStockBoard('600000')"); +// 崩盘场景:securityCode 为 null(East Money 题材列表实测存在),修复后应兜底返回主板 +assertNoThrow(() => getStockBoard(null), "null securityCode"); +assertNoThrow(() => getStockBoard(undefined), "undefined securityCode"); +// 空串不崩 +assertNoThrow(() => getStockBoard(""), "空串 getStockBoard('')");