fix: 无领涨股题材导致/themes页面崩溃(startsWith null)

东方财富题材列表存在 securityCode 为 null 的题材(如"铟"01726),
getStockBoard(null) 调用 startsWith 抛 TypeError,React 渲染崩溃,
页面被错误边界接管。修复 getStockBoard 使其对空值兜底返回主板,
同时保护全部 8 处调用方;ThemeItem.securityCode/securityName 类型
如实标注为 string|null。新增最小回归脚本复现并验证修复。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sakurasan
2026-08-12 18:49:27 +08:00
co-authored by Claude
parent 8cf4c40e2b
commit a6497caf73
4 changed files with 73 additions and 3 deletions
+3 -1
View File
@@ -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" };
}
+2 -2
View File
@@ -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; // 领涨股涨幅
+5
View File
@@ -0,0 +1,5 @@
// stubgetStockBoard 测试用,仅用于解析 @/lib/api-client 别名(CommonJS
function getApiBaseUrl() {
return "http://localhost:8000";
}
module.exports = { getApiBaseUrl };
+63
View File
@@ -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 为 nullEast Money 题材列表实测存在),修复后应兜底返回主板
assertNoThrow(() => getStockBoard(null), "null securityCode");
assertNoThrow(() => getStockBoard(undefined), "undefined securityCode");
// 空串不崩
assertNoThrow(() => getStockBoard(""), "空串 getStockBoard('')");