diff --git a/frontend/src/adapters/gateways/api/ChartServiceClient.ts b/frontend/src/adapters/gateways/api/ChartServiceClient.ts index b1974ed..acd9128 100644 --- a/frontend/src/adapters/gateways/api/ChartServiceClient.ts +++ b/frontend/src/adapters/gateways/api/ChartServiceClient.ts @@ -1,6 +1,8 @@ import { ApiClient } from './ApiClient'; import { API_BASE_URLS } from './config'; +const PREFIX = '/api/v1/charts'; + export class ChartServiceClient { private client: ApiClient; @@ -8,13 +10,8 @@ export class ChartServiceClient { this.client = new ApiClient(API_BASE_URLS.chart); } - async createChart( - datasetId: string, - chartType: string, - bindings?: any[], - style?: any, - ): Promise { - return this.client.post('/api/v1/charts', { + async createChart(datasetId: string, chartType: string, bindings?: any[], style?: any): Promise { + return this.client.post(PREFIX, { dataset_id: datasetId, chart_type: chartType, bindings, @@ -23,42 +20,26 @@ export class ChartServiceClient { } async listCharts(): Promise { - return this.client.get('/charts'); + return this.client.get(PREFIX); } async getChart(id: string): Promise { - return this.client.get(`/charts/${encodeURIComponent(id)}`); + return this.client.get(`${PREFIX}/${encodeURIComponent(id)}`); } - async updateChart( - id: string, - updates: { - bindings?: any; - style?: any; - filters?: any; - sort_config?: any; - top_n?: number; - }, - ): Promise { - return this.client.put( - `/charts/${encodeURIComponent(id)}`, - updates, - ); + async updateChart(id: string, updates: { bindings?: any; style?: any; filters?: any; sort_config?: any; top_n?: number }): Promise { + return this.client.put(`${PREFIX}/${encodeURIComponent(id)}`, updates); } async deleteChart(id: string): Promise { - return this.client.delete(`/charts/${encodeURIComponent(id)}`); + return this.client.delete(`${PREFIX}/${encodeURIComponent(id)}`); } async getChartOption(id: string): Promise { - return this.client.get( - `/charts/${encodeURIComponent(id)}/option`, - ); + return this.client.get(`${PREFIX}/${encodeURIComponent(id)}/option`); } async recommendCharts(datasetId: string): Promise { - return this.client.post('/charts/recommend', { - dataset_id: datasetId, - }); + return this.client.post(`${PREFIX}/recommend`, { dataset_id: datasetId }); } } diff --git a/frontend/src/adapters/gateways/api/DataServiceClient.ts b/frontend/src/adapters/gateways/api/DataServiceClient.ts index 8a86908..6027c22 100644 --- a/frontend/src/adapters/gateways/api/DataServiceClient.ts +++ b/frontend/src/adapters/gateways/api/DataServiceClient.ts @@ -1,6 +1,8 @@ import { ApiClient } from './ApiClient'; import { API_BASE_URLS } from './config'; +const PREFIX = '/api/v1/datasets'; + export class DataServiceClient { private client: ApiClient; @@ -11,48 +13,38 @@ export class DataServiceClient { async importFile(file: File): Promise { const formData = new FormData(); formData.append('file', file); - return this.client.postForm('/api/v1/datasets/import', formData); + return this.client.postForm(`${PREFIX}/import`, formData); } async listDatasets(): Promise { - return this.client.get('/datasets'); + return this.client.get(PREFIX); } async getDataset(id: string): Promise { - return this.client.get(`/datasets/${encodeURIComponent(id)}`); + return this.client.get(`${PREFIX}/${encodeURIComponent(id)}`); } - async getRows( - id: string, - limit?: number, - offset?: number, - ): Promise { + async getRows(id: string, limit?: number, offset?: number): Promise { const params: Record = {}; if (limit !== undefined) params.limit = String(limit); if (offset !== undefined) params.offset = String(offset); return this.client.get( - `/datasets/${encodeURIComponent(id)}/rows`, + `${PREFIX}/${encodeURIComponent(id)}/rows`, Object.keys(params).length > 0 ? params : undefined, ); } async deleteDataset(id: string): Promise { - return this.client.delete(`/datasets/${encodeURIComponent(id)}`); + return this.client.delete(`${PREFIX}/${encodeURIComponent(id)}`); } async getStructure(id: string): Promise { - return this.client.get( - `/datasets/${encodeURIComponent(id)}/structure`, - ); + return this.client.get(`${PREFIX}/${encodeURIComponent(id)}/structure`); } - async updateCell( - id: string, - rowIndex: number, - data: any, - ): Promise { + async updateCell(id: string, rowIndex: number, data: any): Promise { await this.client.put( - `/datasets/${encodeURIComponent(id)}/rows/${rowIndex}`, + `${PREFIX}/${encodeURIComponent(id)}/rows/${rowIndex}`, data, ); } diff --git a/frontend/src/adapters/gateways/api/ExportServiceClient.ts b/frontend/src/adapters/gateways/api/ExportServiceClient.ts index 0e9f4ef..ea6d46b 100644 --- a/frontend/src/adapters/gateways/api/ExportServiceClient.ts +++ b/frontend/src/adapters/gateways/api/ExportServiceClient.ts @@ -1,6 +1,7 @@ import { ApiClient } from './ApiClient'; import { API_BASE_URLS } from './config'; +const PREFIX = '/api/v1/exports'; const DEFAULT_POLL_INTERVAL_MS = 1000; const DEFAULT_MAX_RETRIES = 60; @@ -11,68 +12,29 @@ export class ExportServiceClient { this.client = new ApiClient(API_BASE_URLS.export); } - async createExport(data: { - format: string; - chart_ids: string[]; - dataset_id: string; - file_name?: string; - }): Promise<{ id: string; status: string }> { - return this.client.post<{ id: string; status: string }>( - '/api/v1/exports', - data, - ); + async createExport(data: { format: string; chart_ids: string[]; dataset_id: string; file_name?: string }): Promise<{ id: string; status: string }> { + return this.client.post<{ id: string; status: string }>(PREFIX, data); } - async getExportStatus( - id: string, - ): Promise<{ - id: string; - status: string; - file_path?: string; - error_message?: string; - }> { - return this.client.get<{ - id: string; - status: string; - file_path?: string; - error_message?: string; - }>(`/exports/${encodeURIComponent(id)}`); + async getExportStatus(id: string): Promise<{ id: string; status: string; file_path?: string; error_message?: string }> { + return this.client.get(`${PREFIX}/${encodeURIComponent(id)}`); } async downloadExport(id: string): Promise { - return this.client.getBlob( - `/exports/${encodeURIComponent(id)}/download`, - ); + return this.client.getBlob(`${PREFIX}/${encodeURIComponent(id)}/download`); } - /** - * Poll the export status until it completes, then download the result. - * Throws if the export fails or polling exceeds `maxRetries`. - */ - async waitForExport( - id: string, - intervalMs: number = DEFAULT_POLL_INTERVAL_MS, - maxRetries: number = DEFAULT_MAX_RETRIES, - ): Promise { + async waitForExport(id: string, intervalMs = DEFAULT_POLL_INTERVAL_MS, maxRetries = DEFAULT_MAX_RETRIES): Promise { for (let attempt = 0; attempt < maxRetries; attempt++) { const status = await this.getExportStatus(id); - if (status.status === 'completed' || status.status === 'done') { return this.downloadExport(id); } - if (status.status === 'failed' || status.status === 'error') { - throw new Error( - status.error_message || `Export ${id} failed with status: ${status.status}`, - ); + throw new Error(status.error_message || `Export ${id} failed`); } - - // Wait before next poll await new Promise((resolve) => setTimeout(resolve, intervalMs)); } - - throw new Error( - `Export ${id} did not complete within ${maxRetries} retries`, - ); + throw new Error(`Export ${id} did not complete within ${maxRetries} retries`); } } diff --git a/frontend/src/adapters/gateways/api/TemplateServiceClient.ts b/frontend/src/adapters/gateways/api/TemplateServiceClient.ts index 60d3202..5c1695e 100644 --- a/frontend/src/adapters/gateways/api/TemplateServiceClient.ts +++ b/frontend/src/adapters/gateways/api/TemplateServiceClient.ts @@ -1,6 +1,8 @@ import { ApiClient } from './ApiClient'; import { API_BASE_URLS } from './config'; +const PREFIX = '/api/v1/templates'; + export class TemplateServiceClient { private client: ApiClient; @@ -8,42 +10,31 @@ export class TemplateServiceClient { this.client = new ApiClient(API_BASE_URLS.template); } - async saveTemplate(data: { - name: string; - description: string; - chart_configs: any[]; - layout: any[]; - theme: string; - }): Promise { - return this.client.post('/api/v1/templates', data); + async saveTemplate(data: { name: string; description: string; chart_configs: any[]; layout: any[]; theme: string }): Promise { + return this.client.post(PREFIX, data); } async listTemplates(): Promise { - return this.client.get('/templates'); + return this.client.get(PREFIX); } async getTemplate(id: string): Promise { - return this.client.get(`/templates/${encodeURIComponent(id)}`); + return this.client.get(`${PREFIX}/${encodeURIComponent(id)}`); } async updateTemplate(id: string, data: any): Promise { - return this.client.put( - `/templates/${encodeURIComponent(id)}`, - data, - ); + return this.client.put(`${PREFIX}/${encodeURIComponent(id)}`, data); } async deleteTemplate(id: string): Promise { - return this.client.delete(`/templates/${encodeURIComponent(id)}`); + return this.client.delete(`${PREFIX}/${encodeURIComponent(id)}`); } async importTemplate(json: string): Promise { - return this.client.post('/templates/import', JSON.parse(json)); + return this.client.post(`${PREFIX}/import`, JSON.parse(json)); } async exportTemplate(id: string): Promise { - return this.client.get( - `/templates/${encodeURIComponent(id)}/export`, - ); + return this.client.get(`${PREFIX}/${encodeURIComponent(id)}/export`); } } diff --git a/frontend/src/frameworks/components/charts/ChartRenderer.tsx b/frontend/src/frameworks/components/charts/ChartRenderer.tsx index a02dc7a..ba7bbd9 100644 --- a/frontend/src/frameworks/components/charts/ChartRenderer.tsx +++ b/frontend/src/frameworks/components/charts/ChartRenderer.tsx @@ -2,29 +2,66 @@ import React, { useMemo } from 'react'; import { type ChartInstance } from '@/domain/entities/ChartInstance'; +import { type StyleConfig } from '@/domain/entities/StyleConfig'; import { chartRenderer } from '@/frameworks/di/container'; import { EChartsBase } from './EChartsBase'; import { KPICard } from './KPICard'; import { DataTable } from './DataTable'; +const DEFAULT_STYLE: StyleConfig = { + title: { text: '', visible: true, fontSize: 16, fontWeight: 'bold', color: '#333', align: 'left' }, + colors: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4'], + legend: { visible: true, position: 'top', orient: 'horizontal', fontSize: 12, color: '#666' }, + xAxis: { visible: true, labelVisible: true, labelFontSize: 12, labelColor: '#666', labelRotation: 0, titleVisible: false, titleText: '', gridVisible: true, gridColor: '#eee' }, + yAxis: { visible: true, labelVisible: true, labelFontSize: 12, labelColor: '#666', labelRotation: 0, titleVisible: false, titleText: '', gridVisible: true, gridColor: '#eee' }, + dataLabel: { visible: false, fontSize: 12, color: '#333', position: 'outside', format: 'value' }, + background: { color: '#fff', opacity: 1, borderRadius: 4 }, + border: { visible: false, color: '#ddd', width: 1, style: 'solid' }, + animation: { enabled: true, duration: 500, easing: 'ease-out' }, +}; + +function ensureStyle(style: any): StyleConfig { + if (!style || typeof style !== 'object') return DEFAULT_STYLE; + return { + title: { ...DEFAULT_STYLE.title, ...(style.title ?? {}) }, + colors: style.colors ?? DEFAULT_STYLE.colors, + legend: { ...DEFAULT_STYLE.legend, ...(style.legend ?? {}) }, + xAxis: { ...DEFAULT_STYLE.xAxis, ...(style.xAxis ?? {}) }, + yAxis: { ...DEFAULT_STYLE.yAxis, ...(style.yAxis ?? {}) }, + dataLabel: { ...DEFAULT_STYLE.dataLabel, ...(style.dataLabel ?? {}) }, + background: { ...DEFAULT_STYLE.background, ...(style.background ?? {}) }, + border: { ...DEFAULT_STYLE.border, ...(style.border ?? {}) }, + animation: { ...DEFAULT_STYLE.animation, ...(style.animation ?? {}) }, + }; +} + export interface ChartRendererProps { chart: ChartInstance; data: Record[]; } export const ChartRenderer: React.FC = ({ chart, data }) => { - const echartsOption = useMemo(() => { - if (chart.type === 'kpi' || chart.type === 'data-table') return null; - return chartRenderer.build(chart, data); - }, [chart, data]); + const safeChart = useMemo(() => ({ + ...chart, + style: ensureStyle(chart.style), + }), [chart]); - switch (chart.type) { + const echartsOption = useMemo(() => { + if (safeChart.type === 'kpi' || safeChart.type === 'data-table') return null; + try { + return chartRenderer.build(safeChart, data); + } catch { + return null; + } + }, [safeChart, data]); + + switch (safeChart.type) { case 'kpi': - return ; + return ; case 'data-table': - return ; + return ; default: - return echartsOption ? : null; + return echartsOption ? :
请绑定数据字段
; } }; diff --git a/frontend/src/frameworks/components/charts/KPICard.tsx b/frontend/src/frameworks/components/charts/KPICard.tsx index 91478a9..7f66972 100644 --- a/frontend/src/frameworks/components/charts/KPICard.tsx +++ b/frontend/src/frameworks/components/charts/KPICard.tsx @@ -44,7 +44,9 @@ export const KPICard: React.FC = ({ chart, data }) => { return { mainValue: val, label: lbl, yoy: yoyVal, mom: momVal }; }, [data, valueBinding, labelBinding]); - const { background, border, title: titleStyle } = style; + const background = style.background ?? { color: '#fff', opacity: 1, borderRadius: 4 }; + const border = style.border ?? { visible: false, color: '#ddd', width: 1, style: 'solid' as const }; + const titleStyle = style.title ?? { text: '', visible: true, fontSize: 16, fontWeight: 'bold' as const, color: '#333', align: 'left' as const }; return (