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:
@@ -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" };
|
||||
}
|
||||
|
||||
@@ -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; // 领涨股涨幅
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// stub:getStockBoard 测试用,仅用于解析 @/lib/api-client 别名(CommonJS)
|
||||
function getApiBaseUrl() {
|
||||
return "http://localhost:8000";
|
||||
}
|
||||
module.exports = { getApiBaseUrl };
|
||||
@@ -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('')");
|
||||
Reference in New Issue
Block a user