From 33a1285afbcabd59fbf486b1f8e233103e422f82 Mon Sep 17 00:00:00 2001 From: Cmochance <3216202644@qq.com> Date: Sun, 21 Jun 2026 14:20:07 +0800 Subject: [PATCH] =?UTF-8?q?i18n(chart-editor):=20ChartEditor=20=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E6=96=87=E6=A1=88=E6=8E=A5=E5=85=A5=20i18n(=E6=B6=88?= =?UTF-8?q?=E9=99=A4=E4=B8=AD=E8=8B=B1=E6=B7=B7=E6=8E=92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChartEditor 的图表类型 / 区块标题 / 字段标签 / 按钮 / 状态 / 矩阵工具栏等全部走 t(); 字体名、matplotlib 图例位置、Colormap、DPI 等技术标识符保留不译 - i18n 字典新增 ce.* 中英键;RunView 的「图表微调」入口按钮统一用 t("ce.launch") --- src/components/ChartEditor.tsx | 172 +++++++++++++-------------------- src/components/RunView.tsx | 2 +- src/i18n.tsx | 92 ++++++++++++++++++ 3 files changed, 161 insertions(+), 105 deletions(-) diff --git a/src/components/ChartEditor.tsx b/src/components/ChartEditor.tsx index 27f4505..472606c 100644 --- a/src/components/ChartEditor.tsx +++ b/src/components/ChartEditor.tsx @@ -2,16 +2,18 @@ import { useEffect, useRef, useState, type ReactNode } from "react"; import { previewPlot, toDataUrl, COLOR_PRESETS } from "../api/plotApi"; import type { PlotSpec, PlotStyle, PlotData, PlotSeries, PlotGrid } from "../types/plot"; import { CMAP_PRESETS } from "../types/plot"; +import { useI18n } from "../i18n"; +// chart_type 值 → i18n key(值发给后端,标签随界面语言) const CHART_TYPES = [ - { value: "line", label: "折线图 Line" }, - { value: "bar", label: "柱状图 Bar" }, - { value: "heatmap", label: "热图 Heatmap" }, - { value: "line_dual_y", label: "双 Y 轴 Dual-Y" }, + { value: "line", key: "ce.typeLine" }, + { value: "bar", key: "ce.typeBar" }, + { value: "heatmap", key: "ce.typeHeatmap" }, + { value: "line_dual_y", key: "ce.typeDualY" }, ]; +// 字体名 / matplotlib 图例位置是技术标识符,保留不译 const FONTS = ["serif", "sans-serif", "Arial", "Times New Roman", "DejaVu Serif", "DejaVu Sans", "SimSun"]; - const LEGEND_LOCS = ["best", "upper right", "upper left", "lower right", "lower left", "center"]; interface Props { @@ -20,8 +22,6 @@ interface Props { onBack: () => void; } -// ── 小工具组件 ── - function Section({ title, children }: { title: string; children: ReactNode }) { return (
@@ -45,13 +45,7 @@ function ColorPicker({ value, onChange }: { value: string; onChange: (v: string)
{COLOR_PRESETS.map((c) => ( -
onChange(e.target.value)} className="ce-color-input" /> @@ -60,29 +54,26 @@ function ColorPicker({ value, onChange }: { value: string; onChange: (v: string) ); } -// ── 主组件 ── - export default function ChartEditor({ initialSpec, initialData, onBack }: Props) { + const { t } = useI18n(); const [spec, setSpec] = useState(initialSpec); const [data, setData] = useState(initialData); const [previewUrl, setPreviewUrl] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const payloadRef = useRef(""); - const reqIdRef = useRef(0); + const reqIdRef = useRef(0); const chartType = spec.chart_type || "line"; const st: PlotStyle = spec.style ?? {}; const series = data.series ?? []; const updateSpec = (patch: Partial) => setSpec((p) => ({ ...p, ...patch })); - const updateStyle = (patch: Partial) => - setSpec((p) => ({ ...p, style: { ...p.style, ...patch } })); + const updateStyle = (patch: Partial) => setSpec((p) => ({ ...p, style: { ...p.style, ...patch } })); const updateSeries = (i: number, patch: Partial) => setData((p) => ({ ...p, series: p.series.map((s, idx) => (idx === i ? { ...s, ...patch } : s)) })); const updateGrid = (patch: Partial) => setData((p) => ({ ...p, grid: { ...p.grid, mode: "heatmap", ...patch } })); - // 确保 heatmap 切入时有默认 grid 数据 const grid: PlotGrid = data.grid ?? { mode: "heatmap", values: [[1, 2, 3], [4, 5, 6], [7, 8, 9]], cmap: "viridis", origin: "lower" }; const matrix: number[][] = grid.values ?? []; const setCell = (r: number, c: number, v: number) => { @@ -95,35 +86,30 @@ export default function ChartEditor({ initialSpec, initialData, onBack }: Props) const addCol = () => updateGrid({ values: matrix.map((row) => [...row, 0]) }); const delCol = () => updateGrid({ values: matrix.map((row) => row.slice(0, -1)) }); - // 防抖预览:参数变化 → 400ms → 本地渲染 useEffect(() => { const payload = JSON.stringify({ chartType, spec, data }); if (payload === payloadRef.current) return; - // 参数变化时立即使旧的在途请求失效(不等 400ms 防抖) const myId = ++reqIdRef.current; - const timer = setTimeout(async () => { payloadRef.current = payload; setLoading(true); setError(null); - // heatmap 切入时如果 data.grid 不存在,注入默认矩阵 const renderData = chartType === "heatmap" && !data.grid ? { ...data, grid: { mode: "heatmap" as const, values: [[1, 2, 3], [4, 5, 6], [7, 8, 9]], cmap: "viridis", origin: "lower" as const } } : data; try { const resp = await previewPlot(chartType, spec, renderData, "svg"); - if (myId !== reqIdRef.current) return; // 丢弃过期响应 + if (myId !== reqIdRef.current) return; setPreviewUrl(toDataUrl(resp.imageFormat, resp.imageBase64)); } catch (e) { if (myId !== reqIdRef.current) return; - setError(String(e)); + setError(String(e)); } finally { if (myId === reqIdRef.current) setLoading(false); } }, 400); - return () => clearTimeout(timer); }, [chartType, spec, data]); @@ -140,181 +126,162 @@ export default function ChartEditor({ initialSpec, initialData, onBack }: Props) a.download = `chart.${fmt}`; a.click(); } catch (e) { - setError(String(e)); + setError(String(e)); } } return (
- -

图表微调

+ +

{t("ce.title")}

- {loading ? "渲染中…" : error ? "出错" : "已同步"} - - + {loading ? t("ce.rendering") : error ? t("ce.errored") : t("ce.synced")} + +
- {/* ── 左侧参数面板 ── */}
-
- +
+ - + updateSpec({ title: e.target.value })} />
- + updateSpec({ x_label: e.target.value })} /> - + updateSpec({ x_unit: e.target.value })} />
- + updateSpec({ y_label: e.target.value })} /> - + updateSpec({ y_unit: e.target.value })} />
-
- +
+
- - updateStyle({ font_size: +e.target.value })} /> + + updateStyle({ font_size: +e.target.value })} /> - updateStyle({ dpi: +e.target.value })} /> + updateStyle({ dpi: +e.target.value })} />
- + updateStyle({ figure_size: [+e.target.value, st.figure_size?.[1] ?? 6] })} /> - + updateStyle({ figure_size: [st.figure_size?.[0] ?? 10, +e.target.value] })} />
{st.grid !== false && ( - - updateStyle({ grid_alpha: +e.target.value })} /> + + updateStyle({ grid_alpha: +e.target.value })} /> )}
-
+
{st.legend?.enabled !== false && ( - - updateStyle({ legend: { ...st.legend, location: e.target.value } })}> {LEGEND_LOCS.map((l) => )} )} {st.spines?.enabled !== false && (
- - updateStyle({ spines: { ...st.spines, width: +e.target.value } })} /> + + updateStyle({ spines: { ...st.spines, width: +e.target.value } })} /> - - updateStyle({ spines: { ...st.spines, color: v } })} /> + + updateStyle({ spines: { ...st.spines, color: v } })} />
)}
-
+
{series.map((s, i) => (
- updateSeries(i, { visible: e.target.checked })} /> - updateSeries(i, { name: e.target.value })} /> + updateSeries(i, { visible: e.target.checked })} /> + updateSeries(i, { name: e.target.value })} />
- + updateSeries(i, { color: v })} /> - - updateSeries(i, { line_width: +e.target.value })} /> + + updateSeries(i, { line_width: +e.target.value })} />
))} - {series.length === 0 &&

无数据序列。

} + {series.length === 0 &&

{t("ce.noSeries")}

}
{chartType === "heatmap" && ( -
+
- +
- + updateGrid({ z_label: e.target.value })} placeholder="Intensity" />
- {matrix.length}行 × {matrix[0]?.length ?? 0}列 + {t("ce.rowsCols", { r: matrix.length, c: matrix[0]?.length ?? 0 })}
- - - - + + + +
{matrix.map((row, r) => (
{row.map((val, c) => ( - setCell(r, c, +e.target.value)} - /> + setCell(r, c, +e.target.value)} /> ))}
))} @@ -323,13 +290,10 @@ export default function ChartEditor({ initialSpec, initialData, onBack }: Props) )}
- {/* ── 右侧预览 ── */}
{error &&
{error}
} - {previewUrl && ( - chart preview - )} - {!previewUrl && !error &&
{loading ? "渲染中…" : "等待渲染"}
} + {previewUrl && chart preview} + {!previewUrl && !error &&
{loading ? t("ce.rendering") : t("ce.waiting")}
}
diff --git a/src/components/RunView.tsx b/src/components/RunView.tsx index 44c82ea..20f312e 100644 --- a/src/components/RunView.tsx +++ b/src/components/RunView.tsx @@ -43,7 +43,7 @@ export default function RunView({ run, onOpenEditor }: { run: RunController; onO {skillId === "nature-figure" && ( )}
diff --git a/src/i18n.tsx b/src/i18n.tsx index 4960d94..835bc1f 100644 --- a/src/i18n.tsx +++ b/src/i18n.tsx @@ -186,6 +186,52 @@ const ZH: Dict = { "settings.emailPh": "you@example.com(PubMed 邮箱)", "settings.emailInvalid": "请输入有效邮箱(用于 PubMed 礼貌标识)", "settings.dangerConfirm": "全放开沙箱(dangerFullAccess):Codex 将不受文件/网络沙箱限制地执行命令。仅在你完全信任任务时启用。确定开启?", + + "ce.launch": "图表微调", + "ce.title": "图表微调", + "ce.rendering": "渲染中…", + "ce.errored": "出错", + "ce.synced": "已同步", + "ce.export": "导出", + "ce.waiting": "等待渲染", + "ce.typeLine": "折线图", + "ce.typeBar": "柱状图", + "ce.typeHeatmap": "热图", + "ce.typeDualY": "双 Y 轴", + "ce.secGlobal": "全局", + "ce.secStyle": "样式", + "ce.secLegend": "图例 & 边框", + "ce.secSeries": "数据序列", + "ce.secMatrix": "热图矩阵", + "ce.chartType": "图表类型", + "ce.fTitle": "标题", + "ce.xLabel": "X 轴标签", + "ce.xUnit": "X 单位", + "ce.yLabel": "Y 轴标签", + "ce.yUnit": "Y 单位", + "ce.font": "字体", + "ce.fontSize": "字号", + "ce.figW": "画布宽", + "ce.figH": "画布高", + "ce.grid": "网格", + "ce.gridAlpha": "网格透明度", + "ce.showLegend": "显示图例", + "ce.legendLoc": "图例位置", + "ce.showSpines": "显示边框", + "ce.spineWidth": "边框宽度", + "ce.spineColor": "边框颜色", + "ce.color": "颜色", + "ce.lineWidth": "线宽", + "ce.origin": "原点", + "ce.originLower": "lower(左下)", + "ce.originUpper": "upper(左上)", + "ce.cbarLabel": "Colorbar 标签", + "ce.addRow": "+ 行", + "ce.delRow": "- 行", + "ce.addCol": "+ 列", + "ce.delCol": "- 列", + "ce.rowsCols": "{r} 行 × {c} 列", + "ce.noSeries": "无数据序列。", }; const EN: Dict = { @@ -354,6 +400,52 @@ const EN: Dict = { "settings.emailPh": "you@example.com (PubMed email)", "settings.emailInvalid": "Enter a valid email (PubMed politeness identifier)", "settings.dangerConfirm": "Full access sandbox (dangerFullAccess): Codex will run commands without file/network sandboxing. Enable only if you fully trust the task. Continue?", + + "ce.launch": "Chart editor", + "ce.title": "Chart editor", + "ce.rendering": "Rendering…", + "ce.errored": "Error", + "ce.synced": "Synced", + "ce.export": "Export", + "ce.waiting": "Waiting to render", + "ce.typeLine": "Line", + "ce.typeBar": "Bar", + "ce.typeHeatmap": "Heatmap", + "ce.typeDualY": "Dual-Y", + "ce.secGlobal": "Global", + "ce.secStyle": "Style", + "ce.secLegend": "Legend & frame", + "ce.secSeries": "Data series", + "ce.secMatrix": "Heatmap matrix", + "ce.chartType": "Chart type", + "ce.fTitle": "Title", + "ce.xLabel": "X label", + "ce.xUnit": "X unit", + "ce.yLabel": "Y label", + "ce.yUnit": "Y unit", + "ce.font": "Font", + "ce.fontSize": "Font size", + "ce.figW": "Width", + "ce.figH": "Height", + "ce.grid": "Grid", + "ce.gridAlpha": "Grid opacity", + "ce.showLegend": "Show legend", + "ce.legendLoc": "Legend position", + "ce.showSpines": "Show frame", + "ce.spineWidth": "Frame width", + "ce.spineColor": "Frame color", + "ce.color": "Color", + "ce.lineWidth": "Line width", + "ce.origin": "Origin", + "ce.originLower": "lower (bottom-left)", + "ce.originUpper": "upper (top-left)", + "ce.cbarLabel": "Colorbar label", + "ce.addRow": "+ row", + "ce.delRow": "- row", + "ce.addCol": "+ col", + "ce.delCol": "- col", + "ce.rowsCols": "{r}×{c}", + "ce.noSeries": "No data series.", }; const TABLE: Record = { zh: ZH, en: EN };