feat: wire template stores and linkage
This commit is contained in:
@@ -89,14 +89,14 @@
|
|||||||
| 065 | 保存按钮 + loading 状态 + 成功/失败提示 | 前端 | 0.25d | ✅ 已完成 |
|
| 065 | 保存按钮 + loading 状态 + 成功/失败提示 | 前端 | 0.25d | ✅ 已完成 |
|
||||||
| 066 | 未选中区域时的空状态占位提示 | 前端 | 0.25d | ✅ 已完成 |
|
| 066 | 未选中区域时的空状态占位提示 | 前端 | 0.25d | ✅ 已完成 |
|
||||||
| | **Pinia Store** | | | |
|
| | **Pinia Store** | | | |
|
||||||
| 067 | templateStore | 前端 | 0.5d | ⏳ 未完成 |
|
| 067 | templateStore | 前端 | 0.5d | ✅ 已完成 |
|
||||||
| 068 | selectionStore | 前端 | 0.25d | ⏳ 未完成 |
|
| 068 | selectionStore | 前端 | 0.25d | ✅ 已完成 |
|
||||||
| 069 | uiStore | 前端 | 0.25d | ⏳ 未完成 |
|
| 069 | uiStore | 前端 | 0.25d | ✅ 已完成 |
|
||||||
| | **三区联动** | | | |
|
| | **三区联动** | | | |
|
||||||
| 070 | watch selectedBlockId → 配置面板加载 | 前端 | 0.5d | ⏳ 未完成 |
|
| 070 | watch selectedBlockId → 配置面板加载 | 前端 | 0.5d | ✅ 已完成 |
|
||||||
| 071 | watch selectedBlockId → 结构树节点高亮 | 前端 | 0.25d | ⏳ 未完成 |
|
| 071 | watch selectedBlockId → 结构树节点高亮 | 前端 | 0.25d | ✅ 已完成 |
|
||||||
| 072 | 页面初始化 loading(全页遮罩 + a-spin) | 前端 | 0.25d | ⏳ 未完成 |
|
| 072 | 页面初始化 loading(全页遮罩 + a-spin) | 前端 | 0.25d | ✅ 已完成 |
|
||||||
| 073 | 加载失败错误页 + 重新加载按钮 | 前端 | 0.25d | ⏳ 未完成 |
|
| 073 | 加载失败错误页 + 重新加载按钮 | 前端 | 0.25d | ✅ 已完成 |
|
||||||
| | **总计** | | **~18.25d** | |
|
| | **总计** | | **~18.25d** | |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -137,3 +137,18 @@
|
|||||||
9. 增加未选中区域时的空状态占位。
|
9. 增加未选中区域时的空状态占位。
|
||||||
10. 执行前端构建验证。
|
10. 执行前端构建验证。
|
||||||
- **执行结果**: 完成任务 058-066,配置面板具备完整基础表单能力。
|
- **执行结果**: 完成任务 058-066,配置面板具备完整基础表单能力。
|
||||||
|
|
||||||
|
## 会话 ID: 20260701-stores-linkage
|
||||||
|
- [2026-07-01 20:47:36]
|
||||||
|
- **执行原因**: 按任务清单继续完成 Pinia Store 和三区联动 067-073。
|
||||||
|
- **执行过程**:
|
||||||
|
1. 新增 `templateStore`,集中管理模板信息、预览 HTML、blocks、tree、configs、loading 和 error。
|
||||||
|
2. 新增 `selectionStore`,集中管理当前选中的 `selectedBlockId`。
|
||||||
|
3. 新增 `uiStore`,集中管理标签显示、缩放比例和保存状态。
|
||||||
|
4. `TemplateEdit.vue` 接入初始化 loading、加载失败错误页和重新加载按钮。
|
||||||
|
5. `WordPreview.vue` 接入 store,点击预览块会更新全局选中区域。
|
||||||
|
6. `StructureTree.vue` 接入 store,选中节点会同步高亮并定位预览区。
|
||||||
|
7. `ConfigPanel.vue` watch `selectedBlockId`,切换区域时自动加载对应配置。
|
||||||
|
8. `TopToolbar.vue` 接入模板信息和保存状态。
|
||||||
|
9. 执行前端构建验证。
|
||||||
|
- **执行结果**: 完成任务 067-073,基础 MVP 任务清单 001-073 已全部标注完成。
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, ref } from "vue";
|
import { computed, reactive, ref, watch } from "vue";
|
||||||
import { CloseOutlined, PlusOutlined, SaveOutlined } from "@ant-design/icons-vue";
|
import { CloseOutlined, PlusOutlined, SaveOutlined } from "@ant-design/icons-vue";
|
||||||
import { message } from "ant-design-vue";
|
import { message } from "ant-design-vue";
|
||||||
|
import { useSelectionStore } from "@/stores/selectionStore";
|
||||||
|
import { type BlockConfig, type RegionType, useTemplateStore } from "@/stores/templateStore";
|
||||||
|
import { useUiStore } from "@/stores/uiStore";
|
||||||
|
|
||||||
type RegionType = "ai_generate" | "manual" | "fixed" | "table";
|
const templateStore = useTemplateStore();
|
||||||
|
const selectionStore = useSelectionStore();
|
||||||
const selectedBlock = ref({
|
const uiStore = useUiStore();
|
||||||
block_id: "block_003",
|
const selectedBlock = computed(() => templateStore.blockById(selectionStore.selectedBlockId));
|
||||||
text_preview: "一、经营概况",
|
|
||||||
});
|
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
|
|
||||||
const dataSourceOptions = [
|
const dataSourceOptions = [
|
||||||
@@ -17,11 +18,11 @@ const dataSourceOptions = [
|
|||||||
{ code: "research", name: "调研材料" },
|
{ code: "research", name: "调研材料" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive<BlockConfig>({
|
||||||
region_name: selectedBlock.value.text_preview,
|
region_name: "",
|
||||||
region_type: "ai_generate" as RegionType,
|
region_type: "ai_generate" as RegionType,
|
||||||
data_sources: ["business_data"],
|
data_sources: [],
|
||||||
prompt: "请基于业务数据生成经营概况,突出核心指标变化和原因。",
|
prompt: "",
|
||||||
output_format: "formal_paragraph",
|
output_format: "formal_paragraph",
|
||||||
need_review: 1,
|
need_review: 1,
|
||||||
remark: "",
|
remark: "",
|
||||||
@@ -45,10 +46,29 @@ function sourceName(code: string) {
|
|||||||
return dataSourceOptions.find((item) => item.code === code)?.name ?? code;
|
return dataSourceOptions.find((item) => item.code === code)?.name ?? code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadSelectedConfig() {
|
||||||
|
const block = selectedBlock.value;
|
||||||
|
if (!block) return;
|
||||||
|
|
||||||
|
const config = templateStore.configById(block.block_id);
|
||||||
|
form.region_name = config?.region_name || block.text_preview;
|
||||||
|
form.region_type = config?.region_type || (block.block_type === "table" ? "table" : "ai_generate");
|
||||||
|
form.data_sources = [...(config?.data_sources ?? [])];
|
||||||
|
form.prompt = config?.prompt ?? "";
|
||||||
|
form.output_format = config?.output_format || "formal_paragraph";
|
||||||
|
form.need_review = config?.need_review ?? 1;
|
||||||
|
form.remark = config?.remark ?? "";
|
||||||
|
form.enabled = config?.enabled ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
|
const block = selectedBlock.value;
|
||||||
|
if (!block) return;
|
||||||
|
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve) => window.setTimeout(resolve, 500));
|
await templateStore.saveBlockConfig(block.block_id, { ...form, data_sources: [...form.data_sources] });
|
||||||
|
uiStore.markSaved();
|
||||||
message.success("配置已保存");
|
message.success("配置已保存");
|
||||||
} catch {
|
} catch {
|
||||||
message.error("配置保存失败");
|
message.error("配置保存失败");
|
||||||
@@ -56,6 +76,8 @@ async function saveConfig() {
|
|||||||
saving.value = false;
|
saving.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(() => selectionStore.selectedBlockId, loadSelectedConfig, { immediate: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,29 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref } from "vue";
|
import { reactive } from "vue";
|
||||||
|
|
||||||
import StructureTreeNode, { type StructureNode } from "./StructureTreeNode.vue";
|
import StructureTreeNode, { type StructureNode } from "./StructureTreeNode.vue";
|
||||||
|
import { useSelectionStore } from "@/stores/selectionStore";
|
||||||
|
import { useTemplateStore } from "@/stores/templateStore";
|
||||||
|
|
||||||
const nodes: StructureNode[] = [
|
const templateStore = useTemplateStore();
|
||||||
{
|
const selectionStore = useSelectionStore();
|
||||||
id: "block_001",
|
|
||||||
name: "企业经营分析报告",
|
|
||||||
status: "done",
|
|
||||||
children: [
|
|
||||||
{ id: "block_002", name: "报告说明", status: "review" },
|
|
||||||
{
|
|
||||||
id: "block_003",
|
|
||||||
name: "一、经营概况",
|
|
||||||
status: "done",
|
|
||||||
children: [
|
|
||||||
{ id: "block_004", name: "经营概况段落", status: "empty" },
|
|
||||||
{ id: "block_005", name: "经营指标表", status: "disabled" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const activeId = ref("block_003");
|
|
||||||
const expandedKeys = reactive<Record<string, boolean>>({
|
const expandedKeys = reactive<Record<string, boolean>>({
|
||||||
block_001: true,
|
block_001: true,
|
||||||
block_003: true,
|
block_003: true,
|
||||||
@@ -34,7 +17,7 @@ function toggleNode(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectNode(node: StructureNode) {
|
function selectNode(node: StructureNode) {
|
||||||
activeId.value = node.id;
|
selectionStore.selectBlock(node.id);
|
||||||
const target = document.querySelector<HTMLElement>(`[data-block-id="${node.id}"]`);
|
const target = document.querySelector<HTMLElement>(`[data-block-id="${node.id}"]`);
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
@@ -48,14 +31,14 @@ function selectNode(node: StructureNode) {
|
|||||||
<aside class="structure-tree">
|
<aside class="structure-tree">
|
||||||
<header>
|
<header>
|
||||||
<span>文档结构</span>
|
<span>文档结构</span>
|
||||||
<span class="count">{{ nodes.length }}</span>
|
<span class="count">{{ templateStore.blocks.length }}</span>
|
||||||
</header>
|
</header>
|
||||||
<ul class="tree-list">
|
<ul class="tree-list">
|
||||||
<StructureTreeNode
|
<StructureTreeNode
|
||||||
v-for="node in nodes"
|
v-for="node in templateStore.tree"
|
||||||
:key="node.id"
|
:key="node.id"
|
||||||
:node="node"
|
:node="node"
|
||||||
:active-id="activeId"
|
:active-id="selectionStore.selectedBlockId"
|
||||||
:expanded-keys="expandedKeys"
|
:expanded-keys="expandedKeys"
|
||||||
@select="selectNode"
|
@select="selectNode"
|
||||||
@toggle="toggleNode"
|
@toggle="toggleNode"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from "vue";
|
|
||||||
import {
|
import {
|
||||||
DownloadOutlined,
|
DownloadOutlined,
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
@@ -7,21 +6,19 @@ import {
|
|||||||
PlayCircleOutlined,
|
PlayCircleOutlined,
|
||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
} from "@ant-design/icons-vue";
|
} from "@ant-design/icons-vue";
|
||||||
|
import { useTemplateStore } from "@/stores/templateStore";
|
||||||
|
import { useUiStore } from "@/stores/uiStore";
|
||||||
|
|
||||||
const breadcrumbs = ["模板管理", "模板标注"];
|
const breadcrumbs = ["模板管理", "模板标注"];
|
||||||
const templateInfo = {
|
const templateStore = useTemplateStore();
|
||||||
name: "企业经营分析报告模板",
|
const uiStore = useUiStore();
|
||||||
version: "v1.0.0",
|
|
||||||
};
|
|
||||||
const isSaved = ref(true);
|
|
||||||
const saveStatusText = computed(() => (isSaved.value ? "已保存" : "未保存"));
|
|
||||||
|
|
||||||
function markDirty() {
|
function markDirty() {
|
||||||
isSaved.value = false;
|
uiStore.markDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTemplate() {
|
function saveTemplate() {
|
||||||
isSaved.value = true;
|
uiStore.markSaved();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -32,9 +29,11 @@ function saveTemplate() {
|
|||||||
<span v-for="item in breadcrumbs" :key="item">{{ item }}</span>
|
<span v-for="item in breadcrumbs" :key="item">{{ item }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="template-meta">
|
<div class="template-meta">
|
||||||
<strong>{{ templateInfo.name }}</strong>
|
<strong>{{ templateStore.template.name }}</strong>
|
||||||
<a-tag color="blue">{{ templateInfo.version }}</a-tag>
|
<a-tag color="blue">{{ templateStore.template.version }}</a-tag>
|
||||||
<span class="save-status" :class="{ dirty: !isSaved }">{{ saveStatusText }}</span>
|
<span class="save-status" :class="{ dirty: !uiStore.isSaved }">
|
||||||
|
{{ uiStore.saveStatusText }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nextTick, onMounted, ref } from "vue";
|
import { nextTick, onMounted, ref, watch } from "vue";
|
||||||
|
import { useSelectionStore } from "@/stores/selectionStore";
|
||||||
|
import { useTemplateStore } from "@/stores/templateStore";
|
||||||
|
import { useUiStore } from "@/stores/uiStore";
|
||||||
|
|
||||||
type RegionType = "ai_generate" | "manual" | "fixed" | "table" | "none";
|
|
||||||
|
|
||||||
const selectedBlockId = ref("block_003");
|
|
||||||
const showLabels = ref(true);
|
|
||||||
const zoom = ref(100);
|
|
||||||
const zoomOptions = [70, 100, 150];
|
const zoomOptions = [70, 100, 150];
|
||||||
|
|
||||||
const regionTypes: Record<string, RegionType> = {
|
|
||||||
block_001: "fixed",
|
|
||||||
block_002: "manual",
|
|
||||||
block_003: "ai_generate",
|
|
||||||
block_004: "ai_generate",
|
|
||||||
block_005: "table",
|
|
||||||
};
|
|
||||||
|
|
||||||
const previewHtml = `
|
|
||||||
<h1 data-block-id="block_001" style="margin:0 0 28px;text-align:center;font-size:20px;">企业经营分析报告</h1>
|
|
||||||
<p data-block-id="block_002" style="margin:8px 0;text-align:justify;text-indent:2em;">本报告用于展示 Word 模板解析后的 A4 预览效果,后续会由后端返回的 preview_html 驱动渲染。</p>
|
|
||||||
<h2 data-block-id="block_003" style="margin:24px 0 12px;padding-bottom:6px;border-bottom:2px solid #1a1d24;font-size:16px;">一、经营概况</h2>
|
|
||||||
<p data-block-id="block_004" style="margin:8px 0;text-align:justify;text-indent:2em;">点击预览区块后,左侧配置面板将展示对应区域的提示词、数据源和输出格式配置。</p>
|
|
||||||
<table data-block-id="block_005" style="width:100%;margin:14px 0;border-collapse:collapse;">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="padding:7px 10px;border:1px solid #e0e2e6;text-align:left;background:#f0f1f3;">指标</th>
|
|
||||||
<th style="padding:7px 10px;border:1px solid #e0e2e6;text-align:left;background:#f0f1f3;">本期</th>
|
|
||||||
<th style="padding:7px 10px;border:1px solid #e0e2e6;text-align:left;background:#f0f1f3;">同比</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="padding:7px 10px;border:1px solid #e0e2e6;">营业收入</td>
|
|
||||||
<td style="padding:7px 10px;border:1px solid #e0e2e6;">-</td>
|
|
||||||
<td style="padding:7px 10px;border:1px solid #e0e2e6;">-</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding:7px 10px;border:1px solid #e0e2e6;">利润率</td>
|
|
||||||
<td style="padding:7px 10px;border:1px solid #e0e2e6;">-</td>
|
|
||||||
<td style="padding:7px 10px;border:1px solid #e0e2e6;">-</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const previewRef = ref<HTMLElement | null>(null);
|
const previewRef = ref<HTMLElement | null>(null);
|
||||||
|
const templateStore = useTemplateStore();
|
||||||
|
const selectionStore = useSelectionStore();
|
||||||
|
const uiStore = useUiStore();
|
||||||
|
|
||||||
function enhancePreviewBlocks() {
|
function enhancePreviewBlocks() {
|
||||||
const root = previewRef.value;
|
const root = previewRef.value;
|
||||||
@@ -52,11 +17,13 @@ function enhancePreviewBlocks() {
|
|||||||
|
|
||||||
root.querySelectorAll<HTMLElement>("[data-block-id]").forEach((element) => {
|
root.querySelectorAll<HTMLElement>("[data-block-id]").forEach((element) => {
|
||||||
const blockId = element.dataset.blockId ?? "";
|
const blockId = element.dataset.blockId ?? "";
|
||||||
const regionType = regionTypes[blockId] ?? "none";
|
const block = templateStore.blockById(blockId);
|
||||||
|
const config = templateStore.configById(blockId);
|
||||||
|
const regionType = config?.region_type ?? (block?.block_type === "table" ? "table" : "none");
|
||||||
element.classList.add("doc-block", `region-${regionType}`);
|
element.classList.add("doc-block", `region-${regionType}`);
|
||||||
element.classList.toggle("active", blockId === selectedBlockId.value);
|
element.classList.toggle("active", blockId === selectionStore.selectedBlockId);
|
||||||
element.dataset.blockLabel = blockId;
|
element.dataset.blockLabel = blockId;
|
||||||
element.dataset.labels = showLabels.value ? "on" : "off";
|
element.dataset.labels = uiStore.showBlockLabels ? "on" : "off";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,19 +32,19 @@ async function selectBlock(event: MouseEvent) {
|
|||||||
const block = target?.closest<HTMLElement>("[data-block-id]");
|
const block = target?.closest<HTMLElement>("[data-block-id]");
|
||||||
if (!block) return;
|
if (!block) return;
|
||||||
|
|
||||||
selectedBlockId.value = block.dataset.blockId ?? "";
|
selectionStore.selectBlock(block.dataset.blockId ?? "");
|
||||||
await nextTick();
|
await nextTick();
|
||||||
enhancePreviewBlocks();
|
enhancePreviewBlocks();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setZoom(value: number) {
|
async function setZoom(value: number) {
|
||||||
zoom.value = value;
|
uiStore.setZoom(value);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
enhancePreviewBlocks();
|
enhancePreviewBlocks();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleLabels(checked: boolean) {
|
async function toggleLabels(checked: boolean) {
|
||||||
showLabels.value = checked;
|
uiStore.setShowBlockLabels(checked);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
enhancePreviewBlocks();
|
enhancePreviewBlocks();
|
||||||
}
|
}
|
||||||
@@ -85,19 +52,33 @@ async function toggleLabels(checked: boolean) {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
enhancePreviewBlocks();
|
enhancePreviewBlocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [
|
||||||
|
templateStore.previewHtml,
|
||||||
|
selectionStore.selectedBlockId,
|
||||||
|
uiStore.showBlockLabels,
|
||||||
|
uiStore.zoom,
|
||||||
|
Object.keys(templateStore.configs).join(","),
|
||||||
|
],
|
||||||
|
async () => {
|
||||||
|
await nextTick();
|
||||||
|
enhancePreviewBlocks();
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="preview-shell">
|
<section class="preview-shell">
|
||||||
<header class="preview-toolbar">
|
<header class="preview-toolbar">
|
||||||
<a-switch size="small" :checked="showLabels" @change="toggleLabels" />
|
<a-switch size="small" :checked="uiStore.showBlockLabels" @change="toggleLabels" />
|
||||||
<span>显示标签</span>
|
<span>显示标签</span>
|
||||||
<div class="zoom-group">
|
<div class="zoom-group">
|
||||||
<button
|
<button
|
||||||
v-for="item in zoomOptions"
|
v-for="item in zoomOptions"
|
||||||
:key="item"
|
:key="item"
|
||||||
type="button"
|
type="button"
|
||||||
:class="{ active: zoom === item }"
|
:class="{ active: uiStore.zoom === item }"
|
||||||
@click="setZoom(item)"
|
@click="setZoom(item)"
|
||||||
>
|
>
|
||||||
{{ item }}%
|
{{ item }}%
|
||||||
@@ -109,9 +90,9 @@ onMounted(() => {
|
|||||||
<div
|
<div
|
||||||
ref="previewRef"
|
ref="previewRef"
|
||||||
class="doc-page"
|
class="doc-page"
|
||||||
:style="{ transform: `scale(${zoom / 100})` }"
|
:style="{ transform: `scale(${uiStore.zoom / 100})` }"
|
||||||
@click="selectBlock"
|
@click="selectBlock"
|
||||||
v-html="previewHtml"
|
v-html="templateStore.previewHtml"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
export const useSelectionStore = defineStore("selection", {
|
||||||
|
state: () => ({
|
||||||
|
selectedBlockId: "block_003",
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
selectBlock(blockId: string) {
|
||||||
|
this.selectedBlockId = blockId;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
export type RegionType = "ai_generate" | "manual" | "fixed" | "table";
|
||||||
|
|
||||||
|
export type TemplateBlock = {
|
||||||
|
block_id: string;
|
||||||
|
block_type: "title" | "heading" | "paragraph" | "table";
|
||||||
|
text_preview: string;
|
||||||
|
level: number;
|
||||||
|
sort_order: number;
|
||||||
|
table_rows?: number;
|
||||||
|
table_cols?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TreeNode = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: "done" | "review" | "disabled" | "empty";
|
||||||
|
children?: TreeNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BlockConfig = {
|
||||||
|
region_name: string;
|
||||||
|
region_type: RegionType;
|
||||||
|
data_sources: string[];
|
||||||
|
prompt: string;
|
||||||
|
output_format: string;
|
||||||
|
need_review: number;
|
||||||
|
remark: string;
|
||||||
|
enabled: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockPreviewHtml = `
|
||||||
|
<h1 data-block-id="block_001" style="margin:0 0 28px;text-align:center;font-size:20px;">企业经营分析报告</h1>
|
||||||
|
<p data-block-id="block_002" style="margin:8px 0;text-align:justify;text-indent:2em;">本报告用于展示 Word 模板解析后的 A4 预览效果,后续会由后端返回的 preview_html 驱动渲染。</p>
|
||||||
|
<h2 data-block-id="block_003" style="margin:24px 0 12px;padding-bottom:6px;border-bottom:2px solid #1a1d24;font-size:16px;">一、经营概况</h2>
|
||||||
|
<p data-block-id="block_004" style="margin:8px 0;text-align:justify;text-indent:2em;">点击预览区块后,左侧配置面板将展示对应区域的提示词、数据源和输出格式配置。</p>
|
||||||
|
<table data-block-id="block_005" style="width:100%;margin:14px 0;border-collapse:collapse;">
|
||||||
|
<thead><tr>
|
||||||
|
<th style="padding:7px 10px;border:1px solid #e0e2e6;text-align:left;background:#f0f1f3;">指标</th>
|
||||||
|
<th style="padding:7px 10px;border:1px solid #e0e2e6;text-align:left;background:#f0f1f3;">本期</th>
|
||||||
|
<th style="padding:7px 10px;border:1px solid #e0e2e6;text-align:left;background:#f0f1f3;">同比</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td style="padding:7px 10px;border:1px solid #e0e2e6;">营业收入</td><td style="padding:7px 10px;border:1px solid #e0e2e6;">-</td><td style="padding:7px 10px;border:1px solid #e0e2e6;">-</td></tr>
|
||||||
|
<tr><td style="padding:7px 10px;border:1px solid #e0e2e6;">利润率</td><td style="padding:7px 10px;border:1px solid #e0e2e6;">-</td><td style="padding:7px 10px;border:1px solid #e0e2e6;">-</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const useTemplateStore = defineStore("template", {
|
||||||
|
state: () => ({
|
||||||
|
loading: false,
|
||||||
|
error: "",
|
||||||
|
template: {
|
||||||
|
id: 1,
|
||||||
|
name: "企业经营分析报告模板",
|
||||||
|
version: "v1.0.0",
|
||||||
|
},
|
||||||
|
previewHtml: mockPreviewHtml,
|
||||||
|
blocks: [
|
||||||
|
{ block_id: "block_001", block_type: "title", text_preview: "企业经营分析报告", level: 0, sort_order: 1 },
|
||||||
|
{ block_id: "block_002", block_type: "paragraph", text_preview: "报告说明", level: 0, sort_order: 2 },
|
||||||
|
{ block_id: "block_003", block_type: "heading", text_preview: "一、经营概况", level: 1, sort_order: 3 },
|
||||||
|
{ block_id: "block_004", block_type: "paragraph", text_preview: "经营概况段落", level: 0, sort_order: 4 },
|
||||||
|
{ block_id: "block_005", block_type: "table", text_preview: "经营指标表", level: 0, sort_order: 5, table_rows: 3, table_cols: 3 },
|
||||||
|
] as TemplateBlock[],
|
||||||
|
tree: [
|
||||||
|
{
|
||||||
|
id: "block_001",
|
||||||
|
name: "企业经营分析报告",
|
||||||
|
status: "done",
|
||||||
|
children: [
|
||||||
|
{ id: "block_002", name: "报告说明", status: "review" },
|
||||||
|
{
|
||||||
|
id: "block_003",
|
||||||
|
name: "一、经营概况",
|
||||||
|
status: "done",
|
||||||
|
children: [
|
||||||
|
{ id: "block_004", name: "经营概况段落", status: "empty" },
|
||||||
|
{ id: "block_005", name: "经营指标表", status: "disabled" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as TreeNode[],
|
||||||
|
configs: {
|
||||||
|
block_003: {
|
||||||
|
region_name: "一、经营概况",
|
||||||
|
region_type: "ai_generate",
|
||||||
|
data_sources: ["business_data"],
|
||||||
|
prompt: "请基于业务数据生成经营概况,突出核心指标变化和原因。",
|
||||||
|
output_format: "formal_paragraph",
|
||||||
|
need_review: 1,
|
||||||
|
remark: "",
|
||||||
|
enabled: 1,
|
||||||
|
},
|
||||||
|
block_005: {
|
||||||
|
region_name: "经营指标表",
|
||||||
|
region_type: "table",
|
||||||
|
data_sources: ["business_data"],
|
||||||
|
prompt: "",
|
||||||
|
output_format: "table_summary",
|
||||||
|
need_review: 1,
|
||||||
|
remark: "",
|
||||||
|
enabled: 0,
|
||||||
|
},
|
||||||
|
} as Record<string, BlockConfig>,
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
blockById: (state) => (blockId: string) => state.blocks.find((block) => block.block_id === blockId),
|
||||||
|
configById: (state) => (blockId: string) => state.configs[blockId],
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async loadTemplate() {
|
||||||
|
this.loading = true;
|
||||||
|
this.error = "";
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => window.setTimeout(resolve, 200));
|
||||||
|
} catch {
|
||||||
|
this.error = "模板加载失败";
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async saveBlockConfig(blockId: string, config: BlockConfig) {
|
||||||
|
await new Promise((resolve) => window.setTimeout(resolve, 400));
|
||||||
|
this.configs[blockId] = { ...config };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
export const useUiStore = defineStore("ui", {
|
||||||
|
state: () => ({
|
||||||
|
showBlockLabels: true,
|
||||||
|
zoom: 100,
|
||||||
|
isSaved: true,
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
saveStatusText: (state) => (state.isSaved ? "已保存" : "未保存"),
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setZoom(value: number) {
|
||||||
|
this.zoom = value;
|
||||||
|
},
|
||||||
|
setShowBlockLabels(value: boolean) {
|
||||||
|
this.showBlockLabels = value;
|
||||||
|
},
|
||||||
|
markDirty() {
|
||||||
|
this.isSaved = false;
|
||||||
|
},
|
||||||
|
markSaved() {
|
||||||
|
this.isSaved = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from "vue";
|
||||||
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";
|
||||||
import TopToolbar from "@/components/TemplateEdit/TopToolbar.vue";
|
import TopToolbar from "@/components/TemplateEdit/TopToolbar.vue";
|
||||||
import WordPreview from "@/components/TemplateEdit/WordPreview.vue";
|
import WordPreview from "@/components/TemplateEdit/WordPreview.vue";
|
||||||
|
import { useTemplateStore } from "@/stores/templateStore";
|
||||||
|
|
||||||
|
const templateStore = useTemplateStore();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
templateStore.loadTemplate();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -11,7 +19,17 @@ import WordPreview from "@/components/TemplateEdit/WordPreview.vue";
|
|||||||
<LeftMenu />
|
<LeftMenu />
|
||||||
<section class="edit-main">
|
<section class="edit-main">
|
||||||
<TopToolbar />
|
<TopToolbar />
|
||||||
<div class="edit-workspace">
|
<div v-if="templateStore.loading" class="state-view">
|
||||||
|
<a-spin size="large" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="templateStore.error" class="state-view">
|
||||||
|
<a-result status="error" title="加载失败" :sub-title="templateStore.error">
|
||||||
|
<template #extra>
|
||||||
|
<a-button type="primary" @click="templateStore.loadTemplate()">重新加载</a-button>
|
||||||
|
</template>
|
||||||
|
</a-result>
|
||||||
|
</div>
|
||||||
|
<div v-else class="edit-workspace">
|
||||||
<ConfigPanel />
|
<ConfigPanel />
|
||||||
<WordPreview />
|
<WordPreview />
|
||||||
<StructureTree />
|
<StructureTree />
|
||||||
@@ -41,4 +59,11 @@ import WordPreview from "@/components/TemplateEdit/WordPreview.vue";
|
|||||||
grid-template-columns: 380px minmax(520px, 1fr) 240px;
|
grid-template-columns: 380px minmax(520px, 1fr) 240px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.state-view {
|
||||||
|
display: grid;
|
||||||
|
min-height: 0;
|
||||||
|
place-items: center;
|
||||||
|
background: var(--c-bg);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user