diff --git a/app/api/config/route.ts b/app/api/config/route.ts index 9d4b50d..b1a3cf4 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { getConfig, saveDBConfig, saveAIConfig, type DBConfig, type AIConfig } from "@/lib/config"; +import { getConfig, saveDBConfig, saveAIConfig, saveExportConfig, type DBConfig, type AIConfig, type ExportConfig } from "@/lib/config"; // GET /api/config - 获取完整配置 export async function GET() { @@ -18,11 +18,11 @@ export async function GET() { } // POST /api/config - 保存配置 -// Body 格式: { db?: DBConfig, ai?: AIConfig } +// Body 格式: { db?: DBConfig, ai?: AIConfig, export?: ExportConfig } export async function POST(request: Request) { try { const body = await request.json(); - const { db, ai } = body; + const { db, ai, export: exportConfig } = body; if (db) { saveDBConfig(db); @@ -32,8 +32,12 @@ export async function POST(request: Request) { saveAIConfig(ai); } + if (exportConfig) { + saveExportConfig(exportConfig); + } + // 如果同时发送了完整配置 - if (!db && !ai && body.db === undefined && body.ai === undefined) { + if (!db && !ai && !exportConfig && body.db === undefined && body.ai === undefined && body.export === undefined) { // 可能是旧的扁平格式,尝试兼容 if (body.apiKey !== undefined || body.baseUrl !== undefined || body.model !== undefined) { saveAIConfig({ diff --git a/app/page.tsx b/app/page.tsx index 7425a28..026064b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import {useState, useCallback} from "react"; +import {useState, useCallback, useEffect} from "react"; import { Upload, Sun, @@ -116,6 +116,31 @@ export default function RecipeTranslationPage() { const [data, setData] = useState(null); const [error, setError] = useState(null); const [fileName, setFileName] = useState(""); + const [exportConfig, setExportConfig] = useState<{ + untranslatedColor: string; + secondPageHeader: boolean; + orientation: "portrait" | "landscape"; + }>({ + untranslatedColor: "CC0000", + secondPageHeader: false, + orientation: "landscape", + }); + + // 加载导出配置 + useEffect(() => { + fetch("/api/config") + .then((res) => res.json()) + .then((result) => { + if (result.success && result.data?.export) { + setExportConfig({ + untranslatedColor: result.data.export.untranslatedColor || "CC0000", + secondPageHeader: result.data.export.secondPageHeader || false, + orientation: result.data.export.orientation || "landscape", + }); + } + }) + .catch(console.error); + }, []); // 收集所有未翻译的菜品名称 const getAllDishNames = useCallback((menu: WeeklyMenu): string[] => { @@ -195,7 +220,10 @@ export default function RecipeTranslationPage() { if (!data) return; try { - // 1. 收集所有需要翻译的内容(星期、餐食类型、菜品) + // 1. 获取导出设置 + const { untranslatedColor, secondPageHeader, orientation } = exportConfig; + + // 2. 收集所有需要翻译的内容(星期、餐食类型、菜品) const allNames: string[] = []; // 收集星期名称(使用实际数据中的星期) @@ -321,7 +349,7 @@ export default function RecipeTranslationPage() { } else { cellContent.push( new Paragraph({ - children: [new TextRun({text: zhText, color: "CC0000"})], + children: [new TextRun({text: zhText, color: untranslatedColor})], }) ); } @@ -340,16 +368,20 @@ export default function RecipeTranslationPage() { tableRows.push(new TableRow({children: dayCells})); } - // 5. 创建文档 - 横向 A4 + // 5. 创建文档 - 根据设置选择页面方向 + const isLandscape = orientation === "landscape"; + const pageWidth = isLandscape ? 11906 : 16838; + const pageHeight = isLandscape ? 16838 : 11906; + const doc = new Document({ sections: [ { properties: { page: { size: { - orientation: PageOrientation.LANDSCAPE, - width: 11906, - height: 16838, + orientation: isLandscape ? PageOrientation.LANDSCAPE : PageOrientation.PORTRAIT, + width: pageWidth, + height: pageHeight, }, margin: { top: 700, // 约1.27厘米 @@ -395,7 +427,7 @@ export default function RecipeTranslationPage() { console.error("Export error:", err); alert("导出失败,请重试"); } - }, [data, fileName]); + }, [data, fileName, exportConfig]); const handleFileUpload = useCallback( async (file: File) => { diff --git a/app/settings/export/page.tsx b/app/settings/export/page.tsx index 78fc47f..39a5183 100644 --- a/app/settings/export/page.tsx +++ b/app/settings/export/page.tsx @@ -1,18 +1,188 @@ -import {FileText} from "lucide-react"; +"use client"; + +import { useState, useEffect } from "react"; +import { useToast } from "@/components/ui/toast"; +import { Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +interface ExportConfig { + untranslatedColor: string; + secondPageHeader: boolean; + orientation: "portrait" | "landscape"; +} + +const defaultExportConfig: ExportConfig = { + untranslatedColor: "CC0000", + secondPageHeader: false, + orientation: "landscape", +}; export default function ExportPage() { + const [config, setConfig] = useState(defaultExportConfig); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const { addToast } = useToast(); + + useEffect(() => { + fetch("/api/config") + .then((res) => res.json()) + .then((result) => { + if (result.success && result.data) { + setConfig({ + untranslatedColor: result.data.export?.untranslatedColor || defaultExportConfig.untranslatedColor, + secondPageHeader: result.data.export?.secondPageHeader ?? defaultExportConfig.secondPageHeader, + orientation: result.data.export?.orientation || defaultExportConfig.orientation, + }); + } + }) + .catch(console.error) + .finally(() => setLoading(false)); + }, []); + + const handleSave = async () => { + setSaving(true); + try { + const response = await fetch("/api/config", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ export: config }), + }); + const result = await response.json(); + if (result.success) { + addToast({ type: "success", title: "导出设置已保存" }); + } else { + addToast({ type: "error", title: "保存失败", description: result.error }); + } + } catch { + addToast({ type: "error", title: "保存失败" }); + } finally { + setSaving(false); + } + }; + + if (loading) { return ( -
-
-

导出设置

-

配置导出 Word 文档的格式

-
-
- -

- 导出设置功能开发中... -

-
+
+
+

导出设置

+

配置导出 Word 文档的格式

+
+
); + } + + return ( +
+
+

导出设置

+

配置导出 Word 文档的格式

+
+ + {/* 未翻译文本颜色 */} +
+

文本颜色

+
+ +
+ setConfig({ ...config, untranslatedColor: e.target.value.toUpperCase() })} + className="h-10 w-20 cursor-pointer rounded-md border border-input" + /> + { + const val = e.target.value.toUpperCase(); + if (/^[0-9A-Fa-f]{0,6}$/.test(val)) { + setConfig({ ...config, untranslatedColor: val }); + } + }} + className="w-24 rounded-md border border-input bg-background px-3 py-2 text-sm font-mono" + placeholder="CC0000" + /> + HEX 颜色值(如 CC0000) +
+
+
+ + {/* 页面设置 */} +
+

页面设置

+
+ +
+ + +
+
+ +
+ +

启用后,如果内容超过一页,第二页及后续页面会在顶部显示表头行

+
+
+ + {/* 预览 */} +
+

预览

+
+
+
+ 未翻译示例: + + 宫保鸡丁 + +
+
+ 页面方向: + {config.orientation === "landscape" ? "横向 (Landscape)" : "纵向 (Portrait)"} +
+
+ 第二页表头: + {config.secondPageHeader ? "启用" : "禁用"} +
+
+
+
+ +
+ +
+
+ ); } diff --git a/config.json b/config.json index 2991019..da545d4 100644 --- a/config.json +++ b/config.json @@ -10,5 +10,10 @@ "apiKey": "6a81b361620a44fd86aa3c8fcd833800.PjPyiU8kSzCTviYa", "baseUrl": "https://open.bigmodel.cn/api/paas/v4/", "model": "GLM-4.7-Flash" + }, + "export": { + "untranslatedColor": "CC0000", + "secondPageHeader": false, + "orientation": "landscape" } } \ No newline at end of file diff --git a/lib/config.ts b/lib/config.ts index 4f8f45e..c51c0cf 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -15,9 +15,19 @@ export interface AIConfig { model: string; } +export interface ExportConfig { + // 未翻译文本的颜色 (hex) + untranslatedColor: string; + // 第二页是否显示表头 + secondPageHeader: boolean; + // 页面方向: portrait | landscape + orientation: "portrait" | "landscape"; +} + export interface AppConfig { db: DBConfig; ai: AIConfig; + export: ExportConfig; } // 配置文件路径(单一文件) @@ -101,6 +111,29 @@ export function getAIConfig(): AIConfig { return { apiKey, baseUrl, model }; } +/** + * 获取导出配置 + * 优先级:config.json(没有环境变量) + */ +export function getExportConfig(): ExportConfig { + const defaultConfig: ExportConfig = { + untranslatedColor: "CC0000", + secondPageHeader: false, + orientation: "landscape", + }; + + const fileConfig = readConfigFile(); + if (fileConfig.export) { + return { + untranslatedColor: fileConfig.export.untranslatedColor || defaultConfig.untranslatedColor, + secondPageHeader: fileConfig.export.secondPageHeader ?? defaultConfig.secondPageHeader, + orientation: fileConfig.export.orientation || defaultConfig.orientation, + }; + } + + return defaultConfig; +} + /** * 获取完整配置(兼容旧接口) */ @@ -108,6 +141,7 @@ export function getConfig(): AppConfig { return { db: getDBConfig(), ai: getAIConfig(), + export: getExportConfig(), }; } @@ -129,6 +163,7 @@ export function saveDBConfig(config: Partial): DBConfig { const fileConfig = readConfigFile(); const updatedConfig: AppConfig = { ai: fileConfig.ai ?? { apiKey: "", baseUrl: "https://api.openai.com/v1", model: "gpt-3.5-turbo" }, + export: fileConfig.export ?? { untranslatedColor: "CC0000", secondPageHeader: false, orientation: "landscape" }, db: newConfig, }; writeConfigFile(updatedConfig); @@ -152,6 +187,7 @@ export function saveAIConfig(config: Partial): AIConfig { const fileConfig = readConfigFile(); const updatedConfig: AppConfig = { db: fileConfig.db ?? { host: "127.0.0.1", port: 3306, user: "root", password: "", database: "recipe_tools" }, + export: fileConfig.export ?? { untranslatedColor: "CC0000", secondPageHeader: false, orientation: "landscape" }, ai: newConfig, }; writeConfigFile(updatedConfig); @@ -159,6 +195,30 @@ export function saveAIConfig(config: Partial): AIConfig { return newConfig; } +/** + * 保存导出配置 + */ +export function saveExportConfig(config: Partial): ExportConfig { + const current = getExportConfig(); + + const newConfig: ExportConfig = { + untranslatedColor: config.untranslatedColor ?? current.untranslatedColor, + secondPageHeader: config.secondPageHeader ?? current.secondPageHeader, + orientation: config.orientation ?? current.orientation, + }; + + // 读取现有配置,更新 export 部分 + const fileConfig = readConfigFile(); + const updatedConfig: AppConfig = { + db: fileConfig.db ?? { host: "127.0.0.1", port: 3306, user: "root", password: "", database: "recipe_tools" }, + ai: fileConfig.ai ?? { apiKey: "", baseUrl: "https://api.openai.com/v1", model: "gpt-3.5-turbo" }, + export: newConfig, + }; + writeConfigFile(updatedConfig); + + return newConfig; +} + /** * 保存完整配置(兼容旧接口) */ @@ -169,5 +229,6 @@ export function saveConfig(config: Partial): AppConfig { return { db: config.db ? saveDBConfig(config.db) : current.db, ai: config.ai ? saveAIConfig(config.ai) : current.ai, + export: config.export ? saveExportConfig(config.export) : current.export, }; }