import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import { OfficeError } from "../core/errors.ts"; import { STYLE_PRESET_CHOICES, type WordStyleInput } from "./styles.ts"; export type StyleDialogContext = Pick; function cancelled(): never { throw new OfficeError("排版选择已取消。", "STYLE_SELECTION_CANCELLED"); } async function optionalText(ctx: StyleDialogContext, title: string, placeholder: string): Promise { const value = await ctx.ui.input(title, placeholder, ctx.signal ? { signal: ctx.signal } : undefined); if (value === undefined) cancelled(); const trimmed = value.trim(); return trimmed || undefined; } async function optionalNumber(ctx: StyleDialogContext, title: string, placeholder: string): Promise { const value = await optionalText(ctx, title, placeholder); if (value === undefined) return undefined; const number = Number(value); if (!Number.isFinite(number)) throw new OfficeError(`${title}必须是数字。`, "INVALID_STYLE"); return number; } async function requiredNumber(ctx: StyleDialogContext, title: string, placeholder: string): Promise { const value = await optionalNumber(ctx, title, placeholder); if (value === undefined) throw new OfficeError(`${title}不能为空。`, "INVALID_STYLE"); return value; } export async function collectCustomStyle(ctx: StyleDialogContext): Promise { const chineseBodyFont = await optionalText(ctx, "中文正文字体", "留空使用 SimSun(宋体)"); const latinBodyFont = await optionalText(ctx, "英文正文字体", "留空使用 Calibri"); const headingFont = await optionalText(ctx, "标题字体", "留空使用 Microsoft YaHei(微软雅黑)"); const bodySize = await optionalNumber(ctx, "正文字号(pt)", "留空使用 12.5"); const titleSize = await optionalNumber(ctx, "主标题字号(pt)", "留空使用 32"); const heading1Size = await optionalNumber(ctx, "一级标题字号(pt)", "留空使用预设值"); const heading2Size = await optionalNumber(ctx, "二级标题字号(pt)", "留空使用预设值"); const heading3Size = await optionalNumber(ctx, "三级标题字号(pt)", "留空使用预设值"); const lineSpacing = await optionalNumber(ctx, "行距倍数", "留空使用 1.5"); const firstLineIndent = await optionalNumber(ctx, "首行缩进(cm)", "留空使用预设值;例如 0.74 或 2"); const accentColor = await optionalText(ctx, "强调色(可选)", "六位十六进制,例如 1F4E79;留空使用预设"); const marginChoice = await ctx.ui.select("页边距", ["沿用默认", "窄(1.5 cm)", "普通(2.54 cm)", "自定义四边距"], ctx.signal ? { signal: ctx.signal } : undefined); if (marginChoice === undefined) cancelled(); let margins: WordStyleInput["margins"]; if (marginChoice === "窄(1.5 cm)") margins = { top: 1.5, bottom: 1.5, left: 1.5, right: 1.5 }; if (marginChoice === "普通(2.54 cm)") margins = { top: 2.54, bottom: 2.54, left: 2.54, right: 2.54 }; if (marginChoice === "自定义四边距") { margins = { top: await requiredNumber(ctx, "上页边距(cm)", "例如 2.54"), bottom: await requiredNumber(ctx, "下页边距(cm)", "例如 2.54"), left: await requiredNumber(ctx, "左页边距(cm)", "例如 2.54"), right: await requiredNumber(ctx, "右页边距(cm)", "例如 2.54"), }; } const headerFooter = await ctx.ui.confirm("页眉和页码", "是否添加标题页眉与页码?", ctx.signal ? { signal: ctx.signal } : undefined); return { ...(chineseBodyFont !== undefined ? { chineseBodyFont } : {}), ...(latinBodyFont !== undefined ? { latinBodyFont } : {}), ...(headingFont !== undefined ? { headingFont } : {}), ...(bodySize !== undefined ? { bodySize } : {}), ...(titleSize !== undefined ? { titleSize } : {}), ...(heading1Size !== undefined ? { heading1Size } : {}), ...(heading2Size !== undefined ? { heading2Size } : {}), ...(heading3Size !== undefined ? { heading3Size } : {}), ...(lineSpacing !== undefined ? { lineSpacing } : {}), ...(firstLineIndent !== undefined ? { firstLineIndent } : {}), ...(accentColor !== undefined ? { accentColor } : {}), ...(margins !== undefined ? { margins } : {}), headerFooter, }; } export async function chooseStyle(ctx: StyleDialogContext): Promise<{ stylePreset: "editorial" | "business" | "formal" } | { style: WordStyleInput }> { const presetOptions = [...STYLE_PRESET_CHOICES.map((item) => `${item.label} — ${item.summary}`), "自定义 — 设置字体、字号、行距和页边距"]; const choice = await ctx.ui.select("选择排版", presetOptions, ctx.signal ? { signal: ctx.signal } : undefined); if (choice === undefined) cancelled(); const selectedPreset = STYLE_PRESET_CHOICES.find((item) => choice.startsWith(item.label)); if (selectedPreset) return { stylePreset: selectedPreset.preset }; return { style: await collectCustomStyle(ctx) }; }