chore(run.sh): enhance logging and add npm check for frontend setup
This commit is contained in:
parent
7d42abea78
commit
515e2d53c9
1
.gitignore
vendored
1
.gitignore
vendored
@ -43,7 +43,6 @@ dist/
|
|||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
|
||||||
lib64/
|
lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
|
|||||||
434
frontend/src/lib/execution-step-manager.ts
Normal file
434
frontend/src/lib/execution-step-manager.ts
Normal file
@ -0,0 +1,434 @@
|
|||||||
|
/**
|
||||||
|
* ExecutionStepManager - 可扩展的步骤执行框架
|
||||||
|
*
|
||||||
|
* 提供步骤的动态添加、管理和执行,支持错误处理和状态回调
|
||||||
|
*
|
||||||
|
* 主要功能:
|
||||||
|
* - 步骤的动态添加和管理
|
||||||
|
* - 支持重试机制和错误处理
|
||||||
|
* - 提供执行状态回调
|
||||||
|
* - 支持并行和串行执行
|
||||||
|
* - 可扩展的步骤定义
|
||||||
|
*
|
||||||
|
* @author Financial Analysis Platform Team
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 类型定义
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行步骤接口
|
||||||
|
*/
|
||||||
|
export interface ExecutionStep {
|
||||||
|
/** 步骤唯一标识符 */
|
||||||
|
id: string;
|
||||||
|
/** 步骤显示名称 */
|
||||||
|
name: string;
|
||||||
|
/** 步骤详细描述 */
|
||||||
|
description: string;
|
||||||
|
/** 执行函数(可选) */
|
||||||
|
execute?: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行选项接口
|
||||||
|
*/
|
||||||
|
export interface ExecutionOptions {
|
||||||
|
/** 步骤开始回调 */
|
||||||
|
onStepStart?: (step: ExecutionStep, index: number, total: number) => void;
|
||||||
|
/** 步骤完成回调 */
|
||||||
|
onStepComplete?: (step: ExecutionStep, index: number, total: number) => void;
|
||||||
|
/** 步骤错误回调 */
|
||||||
|
onStepError?: (step: ExecutionStep, index: number, total: number, error: Error) => void;
|
||||||
|
/** 全部完成回调 */
|
||||||
|
onComplete?: () => void;
|
||||||
|
/** 执行错误回调 */
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
/** 最大重试次数 */
|
||||||
|
maxRetries?: number;
|
||||||
|
/** 重试延迟(毫秒) */
|
||||||
|
retryDelay?: number;
|
||||||
|
/** 出错时是否继续执行 */
|
||||||
|
continueOnError?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行上下文接口
|
||||||
|
*/
|
||||||
|
export interface ExecutionContext {
|
||||||
|
/** 当前执行步骤 */
|
||||||
|
currentStep: ExecutionStep | null;
|
||||||
|
/** 当前步骤索引 */
|
||||||
|
stepIndex: number;
|
||||||
|
/** 总步骤数 */
|
||||||
|
totalSteps: number;
|
||||||
|
/** 是否正在运行 */
|
||||||
|
isRunning: boolean;
|
||||||
|
/** 是否有错误 */
|
||||||
|
hasError: boolean;
|
||||||
|
/** 错误信息 */
|
||||||
|
errorMessage?: string;
|
||||||
|
/** 重试次数 */
|
||||||
|
retryCount: number;
|
||||||
|
/** 最大重试次数 */
|
||||||
|
maxRetries: number;
|
||||||
|
/** 是否可重试 */
|
||||||
|
canRetry: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExecutionStepManager {
|
||||||
|
private steps: ExecutionStep[] = [];
|
||||||
|
private context: ExecutionContext = {
|
||||||
|
currentStep: null,
|
||||||
|
stepIndex: 0,
|
||||||
|
totalSteps: 0,
|
||||||
|
isRunning: false,
|
||||||
|
hasError: false,
|
||||||
|
errorMessage: undefined,
|
||||||
|
retryCount: 0,
|
||||||
|
maxRetries: 0,
|
||||||
|
canRetry: false
|
||||||
|
};
|
||||||
|
private options: ExecutionOptions = {};
|
||||||
|
|
||||||
|
constructor(steps: ExecutionStep[] = [], options: ExecutionOptions = {}) {
|
||||||
|
this.steps = [...steps];
|
||||||
|
this.options = {
|
||||||
|
maxRetries: 2,
|
||||||
|
retryDelay: 1000,
|
||||||
|
continueOnError: false,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
this.updateContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加执行步骤
|
||||||
|
*/
|
||||||
|
addStep(step: ExecutionStep): void {
|
||||||
|
this.steps.push(step);
|
||||||
|
this.updateContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量添加执行步骤
|
||||||
|
*/
|
||||||
|
addSteps(steps: ExecutionStep[]): void {
|
||||||
|
this.steps.push(...steps);
|
||||||
|
this.updateContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入步骤到指定位置
|
||||||
|
*/
|
||||||
|
insertStep(index: number, step: ExecutionStep): void {
|
||||||
|
this.steps.splice(index, 0, step);
|
||||||
|
this.updateContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除步骤
|
||||||
|
*/
|
||||||
|
removeStep(stepId: string): boolean {
|
||||||
|
const index = this.steps.findIndex(step => step.id === stepId);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.steps.splice(index, 1);
|
||||||
|
this.updateContext();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有步骤
|
||||||
|
*/
|
||||||
|
clearSteps(): void {
|
||||||
|
this.steps = [];
|
||||||
|
this.updateContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有步骤
|
||||||
|
*/
|
||||||
|
getSteps(): ExecutionStep[] {
|
||||||
|
return [...this.steps];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前执行上下文
|
||||||
|
*/
|
||||||
|
getContext(): ExecutionContext {
|
||||||
|
return { ...this.context };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新执行选项
|
||||||
|
*/
|
||||||
|
setOptions(options: ExecutionOptions): void {
|
||||||
|
this.options = { ...this.options, ...options };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行所有步骤
|
||||||
|
*/
|
||||||
|
async execute(): Promise<void> {
|
||||||
|
if (this.context.isRunning) {
|
||||||
|
throw new Error('Execution is already in progress');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.steps.length === 0) {
|
||||||
|
throw new Error('No steps to execute');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.isRunning = true;
|
||||||
|
this.context.hasError = false;
|
||||||
|
this.context.errorMessage = undefined;
|
||||||
|
this.context.stepIndex = 0;
|
||||||
|
this.context.retryCount = 0;
|
||||||
|
this.context.maxRetries = this.options.maxRetries || 2;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < this.steps.length; i++) {
|
||||||
|
const step = this.steps[i];
|
||||||
|
this.context.currentStep = step;
|
||||||
|
this.context.stepIndex = i;
|
||||||
|
|
||||||
|
// 通知步骤开始
|
||||||
|
this.options.onStepStart?.(step, i, this.steps.length);
|
||||||
|
|
||||||
|
let stepSuccess = false;
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
|
||||||
|
// 重试逻辑
|
||||||
|
for (let retryAttempt = 0; retryAttempt <= this.context.maxRetries; retryAttempt++) {
|
||||||
|
try {
|
||||||
|
this.context.retryCount = retryAttempt;
|
||||||
|
|
||||||
|
// 如果是重试,等待一段时间
|
||||||
|
if (retryAttempt > 0 && this.options.retryDelay) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, this.options.retryDelay));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行步骤(如果有执行函数)
|
||||||
|
if (step.execute) {
|
||||||
|
await step.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
stepSuccess = true;
|
||||||
|
break; // 成功执行,跳出重试循环
|
||||||
|
} catch (stepError) {
|
||||||
|
lastError = stepError instanceof Error ? stepError : new Error(String(stepError));
|
||||||
|
|
||||||
|
// 如果还有重试机会,继续重试
|
||||||
|
if (retryAttempt < this.context.maxRetries) {
|
||||||
|
console.warn(`Step "${step.name}" failed, retrying (${retryAttempt + 1}/${this.context.maxRetries + 1}):`, lastError.message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepSuccess) {
|
||||||
|
// 通知步骤完成
|
||||||
|
this.options.onStepComplete?.(step, i, this.steps.length);
|
||||||
|
} else {
|
||||||
|
// 所有重试都失败了
|
||||||
|
const error = lastError || new Error('Step execution failed');
|
||||||
|
|
||||||
|
// 更新错误状态
|
||||||
|
this.context.hasError = true;
|
||||||
|
this.context.errorMessage = error.message;
|
||||||
|
this.context.canRetry = true;
|
||||||
|
|
||||||
|
// 通知步骤错误
|
||||||
|
this.options.onStepError?.(step, i, this.steps.length, error);
|
||||||
|
|
||||||
|
// 如果不继续执行,抛出错误
|
||||||
|
if (!this.options.continueOnError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有步骤执行完成
|
||||||
|
this.options.onComplete?.();
|
||||||
|
} catch (error) {
|
||||||
|
const execError = error instanceof Error ? error : new Error(String(error));
|
||||||
|
|
||||||
|
// 通知执行错误
|
||||||
|
this.options.onError?.(execError);
|
||||||
|
|
||||||
|
// 重新抛出错误
|
||||||
|
throw execError;
|
||||||
|
} finally {
|
||||||
|
this.context.isRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行单个步骤
|
||||||
|
*/
|
||||||
|
async executeStep(stepId: string): Promise<void> {
|
||||||
|
const stepIndex = this.steps.findIndex(step => step.id === stepId);
|
||||||
|
if (stepIndex === -1) {
|
||||||
|
throw new Error(`Step with id '${stepId}' not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const step = this.steps[stepIndex];
|
||||||
|
this.context.currentStep = step;
|
||||||
|
this.context.stepIndex = stepIndex;
|
||||||
|
this.context.isRunning = true;
|
||||||
|
this.context.hasError = false;
|
||||||
|
this.context.errorMessage = undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 通知步骤开始
|
||||||
|
this.options.onStepStart?.(step, stepIndex, this.steps.length);
|
||||||
|
|
||||||
|
// 执行步骤
|
||||||
|
if (step.execute) {
|
||||||
|
await step.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知步骤完成
|
||||||
|
this.options.onStepComplete?.(step, stepIndex, this.steps.length);
|
||||||
|
} catch (stepError) {
|
||||||
|
const error = stepError instanceof Error ? stepError : new Error(String(stepError));
|
||||||
|
|
||||||
|
// 更新错误状态
|
||||||
|
this.context.hasError = true;
|
||||||
|
this.context.errorMessage = error.message;
|
||||||
|
|
||||||
|
// 通知步骤错误
|
||||||
|
this.options.onStepError?.(step, stepIndex, this.steps.length, error);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this.context.isRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止执行(如果正在运行)
|
||||||
|
*/
|
||||||
|
stop(): void {
|
||||||
|
this.context.isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重试当前失败的步骤
|
||||||
|
*/
|
||||||
|
async retry(): Promise<void> {
|
||||||
|
if (!this.context.hasError || !this.context.canRetry) {
|
||||||
|
throw new Error('No failed step to retry');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.context.isRunning) {
|
||||||
|
throw new Error('Execution is already in progress');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置错误状态
|
||||||
|
this.context.hasError = false;
|
||||||
|
this.context.errorMessage = undefined;
|
||||||
|
this.context.canRetry = false;
|
||||||
|
|
||||||
|
// 重新执行从当前步骤开始
|
||||||
|
try {
|
||||||
|
await this.execute();
|
||||||
|
} catch (error) {
|
||||||
|
// 错误已经在execute方法中处理
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置执行状态
|
||||||
|
*/
|
||||||
|
reset(): void {
|
||||||
|
this.context = {
|
||||||
|
currentStep: null,
|
||||||
|
stepIndex: 0,
|
||||||
|
totalSteps: this.steps.length,
|
||||||
|
isRunning: false,
|
||||||
|
hasError: false,
|
||||||
|
errorMessage: undefined,
|
||||||
|
retryCount: 0,
|
||||||
|
maxRetries: this.options.maxRetries || 2,
|
||||||
|
canRetry: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否正在执行
|
||||||
|
*/
|
||||||
|
isRunning(): boolean {
|
||||||
|
return this.context.isRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有错误
|
||||||
|
*/
|
||||||
|
hasError(): boolean {
|
||||||
|
return this.context.hasError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取错误信息
|
||||||
|
*/
|
||||||
|
getErrorMessage(): string | undefined {
|
||||||
|
return this.context.errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可以重试
|
||||||
|
*/
|
||||||
|
canRetry(): boolean {
|
||||||
|
return this.context.canRetry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新执行上下文
|
||||||
|
*/
|
||||||
|
private updateContext(): void {
|
||||||
|
this.context.totalSteps = this.steps.length;
|
||||||
|
this.context.maxRetries = this.options.maxRetries || 2;
|
||||||
|
if (!this.context.isRunning) {
|
||||||
|
this.context.stepIndex = 0;
|
||||||
|
this.context.currentStep = null;
|
||||||
|
this.context.retryCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建一个带有预定义步骤的管理器实例
|
||||||
|
*/
|
||||||
|
static createWithSteps(steps: ExecutionStep[], options: ExecutionOptions = {}): ExecutionStepManager {
|
||||||
|
return new ExecutionStepManager(steps, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建一个空的管理器实例
|
||||||
|
*/
|
||||||
|
static create(options: ExecutionOptions = {}): ExecutionStepManager {
|
||||||
|
return new ExecutionStepManager([], options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预定义的执行步骤
|
||||||
|
*/
|
||||||
|
export const DEFAULT_EXECUTION_STEPS: ExecutionStep[] = [
|
||||||
|
{
|
||||||
|
id: 'fetch_financial_data',
|
||||||
|
name: '正在读取财务数据',
|
||||||
|
description: '从Tushare API获取公司财务指标数据'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建默认的执行步骤管理器
|
||||||
|
*/
|
||||||
|
export function createDefaultStepManager(options: ExecutionOptions = {}): ExecutionStepManager {
|
||||||
|
return ExecutionStepManager.createWithSteps(DEFAULT_EXECUTION_STEPS, options);
|
||||||
|
}
|
||||||
326
frontend/src/lib/financial-utils.ts
Normal file
326
frontend/src/lib/financial-utils.ts
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
/**
|
||||||
|
* 财务数据处理工具函数
|
||||||
|
* 包含股票代码规范化、数据格式化等通用功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/src/lib/utils.ts
Normal file
6
frontend/src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
@ -45,13 +45,26 @@ deactivate
|
|||||||
|
|
||||||
echo "Backend setup complete."
|
echo "Backend setup complete."
|
||||||
cd ..
|
cd ..
|
||||||
|
echo "--> Changed directory to $(pwd)"
|
||||||
|
|
||||||
|
|
||||||
# --- Frontend Setup ---
|
# --- Frontend Setup ---
|
||||||
echo "Setting up frontend..."
|
echo "Setting up frontend..."
|
||||||
|
|
||||||
|
# Check if npm is installed
|
||||||
|
if ! command -v npm &> /dev/null
|
||||||
|
then
|
||||||
|
echo "npm could not be found. Please install Node.js and npm to continue."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
cd frontend
|
cd frontend
|
||||||
|
echo "--> Changed directory to $(pwd)"
|
||||||
npm install
|
npm install
|
||||||
echo "Frontend setup complete."
|
echo "Frontend setup complete."
|
||||||
cd ..
|
cd ..
|
||||||
|
echo "--> Changed directory to $(pwd)"
|
||||||
|
|
||||||
|
|
||||||
# --- PM2 Execution ---
|
# --- PM2 Execution ---
|
||||||
echo "Starting application with pm2..."
|
echo "Starting application with pm2..."
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user