/** * 财务数据处理工具函数 * 包含股票代码规范化、数据格式化等通用功能 */ import type { MarketType, CompanyInfo, FinancialMetricConfig } from '@/types'; // ============================================================================ // 股票代码处理 // ============================================================================ /** * 规范化A股代码为ts_code格式 * @param input - 输入的股票代码 * @returns 规范化后的ts_code,如果无效则返回null * * @example * ```typescript * normalizeTsCode('600519') // '600519.SH' * normalizeTsCode('000001') // '000001.SZ' * normalizeTsCode('600519.SH') // '600519.SH' * normalizeTsCode('invalid') // null * ``` */ export function normalizeTsCode(input: string): string | null { const code = (input || "").trim(); if (!code) return null; // 仅支持数字代码的简易规则:6开头上证,0/3开头深证 if (/^\d{6}$/.test(code)) { if (code.startsWith("6")) return `${code}.SH`; if (code.startsWith("0") || code.startsWith("3")) return `${code}.SZ`; } // 若已含交易所后缀则直接返回 if (/^\d{6}\.(SH|SZ)$/i.test(code)) return code.toUpperCase(); return null; } /** * 验证股票代码是否有效 * @param code - 股票代码 * @returns 是否为有效的股票代码 */ export function isValidTsCode(code: string): boolean { return normalizeTsCode(code) !== null; } /** * 从输入中提取股票代码 * @param input - 可能包含股票代码和公司名的输入 * @returns 提取的股票代码,如果没有找到则返回null */ export function extractTsCode(input: string): string | null { const trimmed = input.trim(); // 尝试从输入开头提取代码 const codeMatch = trimmed.match(/^(\d{6}(?:\.[A-Z]{2})?)/); if (codeMatch) { return normalizeTsCode(codeMatch[1]); } return null; } // ============================================================================ // 数据格式化 // ============================================================================ /** * 格式化财务数值显示 * @param value - 原始数值 * @param group - 指标分组 * @param api - API接口名 * @param metricKey - 指标键名 * @returns 格式化后的字符串 */ export function formatFinancialValue( value: number | null | undefined, group?: string, api?: string, metricKey?: string ): string { if (value === null || typeof value === "undefined") return "-"; const num = Number(value); if (Number.isNaN(num)) return "-"; const nf1 = new Intl.NumberFormat("zh-CN", { minimumFractionDigits: 1, maximumFractionDigits: 1 }); const nf0 = new Intl.NumberFormat("zh-CN", { maximumFractionDigits: 0 }); // 报表类统一按亿元显示(1位小数,千分位) if (group === "income" || group === "balancesheet" || group === "cashflow" || metricKey === "revenue") { const scaled = num / 1e8; return nf1.format(scaled); } // 市值 total_mv(daily_basic,万元)-> 亿元,整数(千分位) if (api === "daily_basic" && metricKey === "total_mv") { const scaledYi = num / 1e4; return nf0.format(Math.round(scaledYi)); } // 员工数 / 股东数 -> 整数(千分位),不做单位换算 if (metricKey === "employees" || metricKey === "holder_num") { return nf0.format(Math.round(num)); } // 其他数值 -> 1位小数 + 千分位 return nf1.format(num); } /** * 获取指标单位文本 * @param group - 指标分组 * @param api - API接口名 * @param metricKey - 指标键名 * @returns 单位文本,如果不需要显示单位则返回空字符串 */ export function getMetricUnit(group?: string, api?: string, metricKey?: string): string { // 报表类和市值显示亿元单位 if ( group === "income" || group === "balancesheet" || group === "cashflow" || metricKey === "total_mv" ) { return "(亿元)"; } return ""; } // ============================================================================ // 配置处理 // ============================================================================ /** * 扁平化API分组配置 * @param apiGroups - API分组配置对象 * @param groupOrder - 分组顺序数组 * @returns 扁平化的配置项数组和映射对象 */ export function flattenApiGroups( apiGroups: Record, groupOrder: string[] = ["income", "fina_indicator", "balancesheet", "cashflow", "daily_basic", "daily", "unknown"] ): { items: FinancialMetricConfig[]; groupMap: Record; apiMap: Record; } { const items: FinancialMetricConfig[] = []; const groupMap: Record = {}; const apiMap: Record = {}; for (const group of groupOrder) { const arr = Array.isArray(apiGroups[group]) ? apiGroups[group] : []; for (const item of arr) { const param = (item?.tushareParam || "").trim(); if (!param) continue; const api = (item?.api || "").trim(); items.push({ displayText: item.displayText, tushareParam: param, api, group }); groupMap[param] = group; apiMap[param] = api; } } return { items, groupMap, apiMap }; } // ============================================================================ // 错误处理 // ============================================================================ /** * 增强错误信息 * @param error - 原始错误 * @returns 增强后的错误信息 */ export function enhanceErrorMessage(error: unknown): string { const errorMsg = error instanceof Error ? error.message : "未知错误"; // 网络相关错误 if (errorMsg.includes('fetch') || errorMsg.includes('NetworkError')) { return "网络连接失败,请检查后端服务是否已启动或网络连接是否正常。"; } if (errorMsg.includes('timeout')) { return "请求超时,请稍后重试。"; } // 数据相关错误 if (errorMsg.includes('未查询到数据')) { return "未查询到数据,请确认代码或稍后重试。"; } // 服务相关错误 if (errorMsg.includes('500') || errorMsg.includes('Internal Server Error')) { return "服务器内部错误,请稍后重试或联系管理员。"; } if (errorMsg.includes('404') || errorMsg.includes('Not Found')) { return "请求的资源不存在,请检查输入参数。"; } // 默认错误处理 if (!errorMsg || errorMsg === "未知错误") { return "查询失败,请检查后端服务是否已启动。"; } return errorMsg; } /** * 判断错误是否可重试 * @param error - 错误对象或错误信息 * @returns 是否可重试 */ export function isRetryableError(error: unknown): boolean { const errorMsg = error instanceof Error ? error.message : String(error); return ( errorMsg.includes('网络') || errorMsg.includes('连接') || errorMsg.includes('超时') || errorMsg.includes('timeout') || errorMsg.includes('fetch') || errorMsg.includes('NetworkError') ); } // ============================================================================ // 数据验证 // ============================================================================ /** * 验证市场类型是否支持 * @param market - 市场类型 * @returns 是否支持该市场 */ export function isSupportedMarket(market: string): market is MarketType { return ['cn', 'us', 'hk', 'jp', 'other'].includes(market); } /** * 验证公司信息是否有效 * @param company - 公司信息对象 * @returns 是否为有效的公司信息 */ export function isValidCompanyInfo(company: unknown): company is CompanyInfo { return ( typeof company === 'object' && company !== null && typeof (company as CompanyInfo).ts_code === 'string' && isValidTsCode((company as CompanyInfo).ts_code) ); } // ============================================================================ // 本地存储工具 // ============================================================================ /** * 检查localStorage是否可用 * @returns localStorage是否可用 */ export function isLocalStorageAvailable(): boolean { try { const testKey = '__localStorage_test__'; localStorage.setItem(testKey, 'test'); localStorage.removeItem(testKey); return true; } catch { return false; } } /** * 安全地从localStorage获取数据 * @param key - 存储键 * @param defaultValue - 默认值 * @returns 存储的数据或默认值 */ export function safeGetFromStorage(key: string, defaultValue: T): T { if (!isLocalStorageAvailable()) { return defaultValue; } try { const stored = localStorage.getItem(key); return stored ? JSON.parse(stored) : defaultValue; } catch { return defaultValue; } } /** * 安全地向localStorage保存数据 * @param key - 存储键 * @param value - 要保存的数据 * @returns 是否保存成功 */ export function safeSetToStorage(key: string, value: unknown): boolean { if (!isLocalStorageAvailable()) { return false; } try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch { return false; } }