feat: connect template editor to APIs

This commit is contained in:
zwt13703
2026-07-01 21:48:11 +08:00
parent 60bdd5f268
commit 5a24edc3b0
4 changed files with 98 additions and 7 deletions
+12
View File
@@ -152,3 +152,15 @@
8. `TopToolbar.vue` 接入模板信息和保存状态。 8. `TopToolbar.vue` 接入模板信息和保存状态。
9. 执行前端构建验证。 9. 执行前端构建验证。
- **执行结果**: 完成任务 067-073,基础 MVP 任务清单 001-073 已全部标注完成。 - **执行结果**: 完成任务 067-073,基础 MVP 任务清单 001-073 已全部标注完成。
## 会话 ID: 20260701-frontend-api-integration
- [2026-07-01 21:47:57]
- **执行原因**: 用户要求提交当前代码后继续推进项目实现。
- **执行过程**:
1. 检查 git 工作区,确认上一轮代码已提交且无未提交变更,因此不创建空提交。
2. 新增前端模板 API 模块,封装 `GET /api/templates/{id}``POST /api/templates/{id}/blocks/{blockId}/config`
3. 改造 `templateStore`,从后端模板详情接口加载模板、预览 HTML、blocks、tree 和 configs。
4. 改造配置保存逻辑,保存时调用后端 block config upsert 接口。
5. 改造 `TemplateEdit.vue`,从路由参数读取模板 ID 后加载对应模板。
6. 执行前端生产构建验证。
- **执行结果**: 前端模板编辑页已从纯 mock 数据推进到后端接口驱动,构建验证通过。
+43
View File
@@ -0,0 +1,43 @@
import { apiClient } from "./base";
import type { BlockConfig } from "@/stores/templateStore";
export type TemplateDetailResponse = {
template: {
id: number;
name: string;
version: string;
};
preview_html: string;
blocks: Array<{
block_id: string;
block_type?: string;
type?: string;
text_preview?: string;
text?: string;
level?: number;
sort_order?: number;
table_rows?: number;
table_cols?: number;
}>;
tree: Array<Record<string, unknown>>;
configs: Record<string, BlockConfig>;
};
export async function getTemplateDetail(templateId: number) {
const response = await apiClient.get<{ data: TemplateDetailResponse; message: string }>(
`/templates/${templateId}`,
);
return response.data.data;
}
export async function saveTemplateBlockConfig(
templateId: number,
blockId: string,
config: BlockConfig,
) {
const response = await apiClient.post<{ data: BlockConfig; message: string }>(
`/templates/${templateId}/blocks/${blockId}/config`,
config,
);
return response.data.data;
}
+39 -6
View File
@@ -1,4 +1,5 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { getTemplateDetail, saveTemplateBlockConfig } from "@/api/templates";
export type RegionType = "ai_generate" | "manual" | "fixed" | "table"; export type RegionType = "ai_generate" | "manual" | "fixed" | "table";
@@ -48,6 +49,21 @@ const mockPreviewHtml = `
</table> </table>
`; `;
function normalizeTreeNode(node: Record<string, unknown>): TreeNode {
const id = String(node.id ?? node.block_id ?? "");
const name = String(node.name ?? node.text ?? node.text_preview ?? id);
const children = Array.isArray(node.children)
? node.children.map((child) => normalizeTreeNode(child as Record<string, unknown>))
: undefined;
return {
id,
name,
status: "empty",
children,
};
}
export const useTemplateStore = defineStore("template", { export const useTemplateStore = defineStore("template", {
state: () => ({ state: () => ({
loading: false, loading: false,
@@ -112,20 +128,37 @@ export const useTemplateStore = defineStore("template", {
configById: (state) => (blockId: string) => state.configs[blockId], configById: (state) => (blockId: string) => state.configs[blockId],
}, },
actions: { actions: {
async loadTemplate() { async loadTemplate(templateId = 1) {
this.loading = true; this.loading = true;
this.error = ""; this.error = "";
try { try {
await new Promise((resolve) => window.setTimeout(resolve, 200)); const detail = await getTemplateDetail(templateId);
} catch { this.template = {
this.error = "模板加载失败"; id: detail.template.id,
name: detail.template.name,
version: detail.template.version,
};
this.previewHtml = detail.preview_html;
this.blocks = detail.blocks.map((block) => ({
block_id: block.block_id,
block_type: (block.block_type ?? block.type ?? "paragraph") as TemplateBlock["block_type"],
text_preview: block.text_preview ?? block.text ?? "",
level: block.level ?? 0,
sort_order: block.sort_order ?? 0,
table_rows: block.table_rows,
table_cols: block.table_cols,
}));
this.tree = detail.tree.map((node) => normalizeTreeNode(node));
this.configs = detail.configs;
} catch (error) {
this.error = error instanceof Error ? error.message : "模板加载失败";
} finally { } finally {
this.loading = false; this.loading = false;
} }
}, },
async saveBlockConfig(blockId: string, config: BlockConfig) { async saveBlockConfig(blockId: string, config: BlockConfig) {
await new Promise((resolve) => window.setTimeout(resolve, 400)); const savedConfig = await saveTemplateBlockConfig(this.template.id, blockId, config);
this.configs[blockId] = { ...config }; this.configs[blockId] = { ...savedConfig };
}, },
}, },
}); });
+4 -1
View File
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from "vue"; import { onMounted } from "vue";
import { useRoute } from "vue-router";
import ConfigPanel from "@/components/TemplateEdit/ConfigPanel.vue"; import ConfigPanel from "@/components/TemplateEdit/ConfigPanel.vue";
import LeftMenu from "@/components/TemplateEdit/LeftMenu.vue"; import LeftMenu from "@/components/TemplateEdit/LeftMenu.vue";
import StructureTree from "@/components/TemplateEdit/StructureTree.vue"; import StructureTree from "@/components/TemplateEdit/StructureTree.vue";
@@ -8,9 +9,11 @@ import WordPreview from "@/components/TemplateEdit/WordPreview.vue";
import { useTemplateStore } from "@/stores/templateStore"; import { useTemplateStore } from "@/stores/templateStore";
const templateStore = useTemplateStore(); const templateStore = useTemplateStore();
const route = useRoute();
onMounted(() => { onMounted(() => {
templateStore.loadTemplate(); const templateId = Number(route.params.id || 1);
templateStore.loadTemplate(Number.isFinite(templateId) ? templateId : 1);
}); });
</script> </script>