feat: connect template editor to APIs
This commit is contained in:
@@ -152,3 +152,15 @@
|
||||
8. `TopToolbar.vue` 接入模板信息和保存状态。
|
||||
9. 执行前端构建验证。
|
||||
- **执行结果**: 完成任务 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 数据推进到后端接口驱动,构建验证通过。
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { getTemplateDetail, saveTemplateBlockConfig } from "@/api/templates";
|
||||
|
||||
export type RegionType = "ai_generate" | "manual" | "fixed" | "table";
|
||||
|
||||
@@ -48,6 +49,21 @@ const mockPreviewHtml = `
|
||||
</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", {
|
||||
state: () => ({
|
||||
loading: false,
|
||||
@@ -112,20 +128,37 @@ export const useTemplateStore = defineStore("template", {
|
||||
configById: (state) => (blockId: string) => state.configs[blockId],
|
||||
},
|
||||
actions: {
|
||||
async loadTemplate() {
|
||||
async loadTemplate(templateId = 1) {
|
||||
this.loading = true;
|
||||
this.error = "";
|
||||
try {
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 200));
|
||||
} catch {
|
||||
this.error = "模板加载失败";
|
||||
const detail = await getTemplateDetail(templateId);
|
||||
this.template = {
|
||||
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 {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async saveBlockConfig(blockId: string, config: BlockConfig) {
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 400));
|
||||
this.configs[blockId] = { ...config };
|
||||
const savedConfig = await saveTemplateBlockConfig(this.template.id, blockId, config);
|
||||
this.configs[blockId] = { ...savedConfig };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import ConfigPanel from "@/components/TemplateEdit/ConfigPanel.vue";
|
||||
import LeftMenu from "@/components/TemplateEdit/LeftMenu.vue";
|
||||
import StructureTree from "@/components/TemplateEdit/StructureTree.vue";
|
||||
@@ -8,9 +9,11 @@ import WordPreview from "@/components/TemplateEdit/WordPreview.vue";
|
||||
import { useTemplateStore } from "@/stores/templateStore";
|
||||
|
||||
const templateStore = useTemplateStore();
|
||||
const route = useRoute();
|
||||
|
||||
onMounted(() => {
|
||||
templateStore.loadTemplate();
|
||||
const templateId = Number(route.params.id || 1);
|
||||
templateStore.loadTemplate(Number.isFinite(templateId) ? templateId : 1);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user