feat: add template upload UI flow

This commit is contained in:
zwt13703
2026-07-01 21:49:06 +08:00
parent 5a24edc3b0
commit 5948334d15
4 changed files with 160 additions and 5 deletions
+12
View File
@@ -164,3 +164,15 @@
5. 改造 `TemplateEdit.vue`,从路由参数读取模板 ID 后加载对应模板。
6. 执行前端生产构建验证。
- **执行结果**: 前端模板编辑页已从纯 mock 数据推进到后端接口驱动,构建验证通过。
## 会话 ID: 20260701-template-upload-ui
- [2026-07-01 21:48:56]
- **执行原因**: 继续推进模板创建到编辑页的真实使用闭环。
- **执行过程**:
1. 调整 axios base 配置,移除固定 `Content-Type`,支持 JSON 和 multipart 自动识别。
2. 在前端模板 API 模块中新增 `uploadTemplate`,封装 `POST /api/templates/upload`
3. 在模板中心增加上传模板弹窗,包含模板名称、模板类型和 `.docx` 文件选择。
4. 上传成功后展示解析区域数量,并跳转到 `/templates/{template_id}/edit`
5. 上传失败时给出后端服务或数据库未启动的提示。
6. 执行前端生产构建验证。
- **执行结果**: 模板中心“上传模板”按钮已接入后端上传接口,成功后可进入模板编辑页。
-3
View File
@@ -3,9 +3,6 @@ import axios from "axios";
export const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000/api",
timeout: 15000,
headers: {
"Content-Type": "application/json",
},
});
apiClient.interceptors.response.use(
+20
View File
@@ -1,6 +1,13 @@
import { apiClient } from "./base";
import type { BlockConfig } from "@/stores/templateStore";
export type UploadTemplateResponse = {
template_id: number;
name: string;
block_count: number;
status: string;
};
export type TemplateDetailResponse = {
template: {
id: number;
@@ -30,6 +37,19 @@ export async function getTemplateDetail(templateId: number) {
return response.data.data;
}
export async function uploadTemplate(file: File, name: string, type: string) {
const formData = new FormData();
formData.append("file", file);
formData.append("name", name);
formData.append("type", type);
const response = await apiClient.post<{ data: UploadTemplateResponse; message: string }>(
"/templates/upload",
formData,
);
return response.data.data;
}
export async function saveTemplateBlockConfig(
templateId: number,
blockId: string,
+128 -2
View File
@@ -1,11 +1,63 @@
<script setup lang="ts">
import { computed } from "vue";
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import { FileTextOutlined, PlusOutlined } from "@ant-design/icons-vue";
import { message } from "ant-design-vue";
import { uploadTemplate } from "@/api/templates";
import { useAppStore } from "@/stores/appStore";
const appStore = useAppStore();
const router = useRouter();
const statusClass = computed(() => (appStore.saveStatus === "已保存" ? "saved" : "dirty"));
const uploadOpen = ref(false);
const uploading = ref(false);
const fileInputRef = ref<HTMLInputElement | null>(null);
const selectedFile = ref<File | null>(null);
const form = ref({
name: "",
type: "report",
});
function openUploadModal() {
uploadOpen.value = true;
}
function chooseFile() {
fileInputRef.value?.click();
}
function handleFileChange(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0] ?? null;
selectedFile.value = file;
if (file && !form.value.name) {
form.value.name = file.name.replace(/\.docx$/i, "");
}
}
async function submitUpload() {
if (!selectedFile.value) {
message.warning("请先选择 .docx 文件");
return;
}
if (!form.value.name.trim()) {
message.warning("请输入模板名称");
return;
}
uploading.value = true;
try {
const result = await uploadTemplate(selectedFile.value, form.value.name.trim(), form.value.type);
message.success(`模板上传成功,解析 ${result.block_count} 个区域`);
uploadOpen.value = false;
await router.push(`/templates/${result.template_id}/edit`);
} catch {
message.error("模板上传失败,请确认后端服务和数据库已启动");
} finally {
uploading.value = false;
}
}
</script>
<template>
@@ -31,7 +83,7 @@ const statusClass = computed(() => (appStore.saveStatus === "已保存" ? "saved
</div>
<div class="toolbar-actions">
<span class="save-status" :class="statusClass">{{ appStore.saveStatus }}</span>
<a-button type="primary">
<a-button type="primary" @click="openUploadModal">
<template #icon><PlusOutlined /></template>
上传模板
</a-button>
@@ -46,6 +98,44 @@ const statusClass = computed(() => (appStore.saveStatus === "已保存" ? "saved
</div>
</section>
</section>
<a-modal
v-model:open="uploadOpen"
title="上传模板"
:confirm-loading="uploading"
ok-text="上传"
cancel-text="取消"
@ok="submitUpload"
>
<div class="upload-form">
<label>
模板名称
<a-input v-model:value="form.name" placeholder="请输入模板名称" />
</label>
<label>
模板类型
<a-select v-model:value="form.type">
<a-select-option value="report">报告类</a-select-option>
<a-select-option value="official">公文类</a-select-option>
<a-select-option value="summary">总结类</a-select-option>
<a-select-option value="contract">合同类</a-select-option>
</a-select>
</label>
<div class="file-picker">
<input
ref="fileInputRef"
class="hidden-input"
type="file"
accept=".docx"
@change="handleFileChange"
/>
<button type="button" @click="chooseFile">
<FileTextOutlined />
{{ selectedFile ? selectedFile.name : "选择 .docx 文件" }}
</button>
</div>
</div>
</a-modal>
</main>
</template>
@@ -211,4 +301,40 @@ h1 {
margin: 0;
color: var(--c-text-muted);
}
.upload-form {
display: grid;
gap: 14px;
}
.upload-form label {
display: grid;
gap: 6px;
color: var(--c-text-secondary);
font-size: 12px;
}
.hidden-input {
display: none;
}
.file-picker button {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 88px;
gap: 8px;
border: 1px dashed var(--c-border);
border-radius: var(--radius-md);
color: var(--c-text-secondary);
background: var(--c-surface);
cursor: pointer;
}
.file-picker button:hover {
border-color: var(--c-primary);
color: var(--c-primary);
background: var(--c-primary-soft);
}
</style>