326 lines
9.1 KiB
TypeScript
326 lines
9.1 KiB
TypeScript
/**
|
||
* 财务数据处理工具函数
|
||
* 包含股票代码规范化、数据格式化等通用功能
|
||
*/
|
||
|
||
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<string, FinancialMetricConfig[]>,
|
||
groupOrder: string[] = ["income", "fina_indicator", "balancesheet", "cashflow", "daily_basic", "daily", "unknown"]
|
||
): {
|
||
items: FinancialMetricConfig[];
|
||
groupMap: Record<string, string>;
|
||
apiMap: Record<string, string>;
|
||
} {
|
||
const items: FinancialMetricConfig[] = [];
|
||
const groupMap: Record<string, string> = {};
|
||
const apiMap: Record<string, string> = {};
|
||
|
||
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<T>(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;
|
||
}
|
||
} |