2026-03-14 15:52:27 +08:00
|
|
|
import { create } from 'zustand';
|
2026-03-15 17:57:09 +08:00
|
|
|
import type { ChartSpec } from './visualizationStore';
|
2026-03-14 15:52:27 +08:00
|
|
|
|
|
|
|
|
type ChartRow = Record<string, unknown>;
|
|
|
|
|
type GridLayout = { i: string; x: number; y: number; w: number; h: number };
|
|
|
|
|
|
|
|
|
|
export interface ChartConfig {
|
|
|
|
|
id: string;
|
|
|
|
|
title: string;
|
|
|
|
|
type: 'bar' | 'line';
|
|
|
|
|
data: ChartRow[];
|
|
|
|
|
sql: string;
|
2026-03-15 17:57:09 +08:00
|
|
|
chartSpec?: ChartSpec | null;
|
2026-03-14 15:52:27 +08:00
|
|
|
layout: GridLayout;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface DashboardState {
|
|
|
|
|
charts: ChartConfig[];
|
|
|
|
|
addChart: (chart: Omit<ChartConfig, 'layout'>) => void;
|
|
|
|
|
removeChart: (id: string) => void;
|
|
|
|
|
updateLayout: (layouts: GridLayout[]) => void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 18:11:26 +08:00
|
|
|
const DASHBOARD_STORAGE_KEY = 'dashboard_charts_v1';
|
|
|
|
|
|
|
|
|
|
function loadChartsFromStorage(): ChartConfig[] {
|
|
|
|
|
if (typeof window === 'undefined') return [];
|
|
|
|
|
try {
|
|
|
|
|
const raw = window.localStorage.getItem(DASHBOARD_STORAGE_KEY);
|
|
|
|
|
if (!raw) return [];
|
|
|
|
|
const parsed = JSON.parse(raw);
|
|
|
|
|
if (!Array.isArray(parsed)) return [];
|
|
|
|
|
return parsed
|
|
|
|
|
.filter((item): item is ChartConfig => Boolean(item?.id && item?.layout))
|
|
|
|
|
.map((item) => ({
|
|
|
|
|
...item,
|
|
|
|
|
layout: {
|
|
|
|
|
i: item.layout.i,
|
|
|
|
|
x: Number.isFinite(item.layout.x) ? item.layout.x : 0,
|
|
|
|
|
y: Number.isFinite(item.layout.y) ? item.layout.y : 0,
|
|
|
|
|
w: Number.isFinite(item.layout.w) ? item.layout.w : 4,
|
|
|
|
|
h: Number.isFinite(item.layout.h) ? item.layout.h : 4,
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
} catch {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function saveChartsToStorage(charts: ChartConfig[]) {
|
|
|
|
|
if (typeof window === 'undefined') return;
|
|
|
|
|
window.localStorage.setItem(DASHBOARD_STORAGE_KEY, JSON.stringify(charts));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 15:52:27 +08:00
|
|
|
export const useDashboardStore = create<DashboardState>((set) => ({
|
2026-03-15 18:11:26 +08:00
|
|
|
charts: loadChartsFromStorage(),
|
2026-03-14 15:52:27 +08:00
|
|
|
addChart: (chart) => set((state) => {
|
2026-03-15 18:11:26 +08:00
|
|
|
const colSize = 4;
|
|
|
|
|
const cols = 12 / colSize;
|
|
|
|
|
const index = state.charts.length;
|
2026-03-14 15:52:27 +08:00
|
|
|
const newLayout: GridLayout = {
|
|
|
|
|
i: chart.id,
|
2026-03-15 18:11:26 +08:00
|
|
|
x: (index % cols) * colSize,
|
|
|
|
|
y: Math.floor(index / cols) * 4,
|
|
|
|
|
w: colSize,
|
2026-03-15 18:04:23 +08:00
|
|
|
h: 4,
|
2026-03-14 15:52:27 +08:00
|
|
|
};
|
2026-03-15 18:11:26 +08:00
|
|
|
const nextCharts = [...state.charts, { ...chart, layout: newLayout }];
|
|
|
|
|
saveChartsToStorage(nextCharts);
|
|
|
|
|
return { charts: nextCharts };
|
2026-03-14 15:52:27 +08:00
|
|
|
}),
|
|
|
|
|
removeChart: (id) => set((state) => ({
|
2026-03-15 18:11:26 +08:00
|
|
|
charts: (() => {
|
|
|
|
|
const nextCharts = state.charts.filter((c) => c.id !== id);
|
|
|
|
|
saveChartsToStorage(nextCharts);
|
|
|
|
|
return nextCharts;
|
|
|
|
|
})(),
|
2026-03-14 15:52:27 +08:00
|
|
|
})),
|
2026-03-15 18:11:26 +08:00
|
|
|
updateLayout: (layouts) => set((state) => {
|
|
|
|
|
const nextCharts = state.charts.map((chart) => {
|
2026-03-14 15:52:27 +08:00
|
|
|
const layout = layouts.find((l) => l.i === chart.id);
|
|
|
|
|
return layout ? { ...chart, layout } : chart;
|
2026-03-15 18:11:26 +08:00
|
|
|
});
|
|
|
|
|
saveChartsToStorage(nextCharts);
|
|
|
|
|
return { charts: nextCharts };
|
|
|
|
|
}),
|
2026-03-14 15:52:27 +08:00
|
|
|
}));
|