feat: connect template editor to APIs
This commit is contained in:
@@ -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 数据推进到后端接口驱动,构建验证通过。
|
||||||
|
|||||||
@@ -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 { 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 };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user