feat: add template upload UI flow
This commit is contained in:
@@ -164,3 +164,15 @@
|
|||||||
5. 改造 `TemplateEdit.vue`,从路由参数读取模板 ID 后加载对应模板。
|
5. 改造 `TemplateEdit.vue`,从路由参数读取模板 ID 后加载对应模板。
|
||||||
6. 执行前端生产构建验证。
|
6. 执行前端生产构建验证。
|
||||||
- **执行结果**: 前端模板编辑页已从纯 mock 数据推进到后端接口驱动,构建验证通过。
|
- **执行结果**: 前端模板编辑页已从纯 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,9 +3,6 @@ import axios from "axios";
|
|||||||
export const apiClient = axios.create({
|
export const apiClient = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000/api",
|
baseURL: import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000/api",
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
apiClient.interceptors.response.use(
|
apiClient.interceptors.response.use(
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { apiClient } from "./base";
|
import { apiClient } from "./base";
|
||||||
import type { BlockConfig } from "@/stores/templateStore";
|
import type { BlockConfig } from "@/stores/templateStore";
|
||||||
|
|
||||||
|
export type UploadTemplateResponse = {
|
||||||
|
template_id: number;
|
||||||
|
name: string;
|
||||||
|
block_count: number;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TemplateDetailResponse = {
|
export type TemplateDetailResponse = {
|
||||||
template: {
|
template: {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -30,6 +37,19 @@ export async function getTemplateDetail(templateId: number) {
|
|||||||
return response.data.data;
|
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(
|
export async function saveTemplateBlockConfig(
|
||||||
templateId: number,
|
templateId: number,
|
||||||
blockId: string,
|
blockId: string,
|
||||||
|
|||||||
@@ -1,11 +1,63 @@
|
|||||||
<script setup lang="ts">
|
<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 { FileTextOutlined, PlusOutlined } from "@ant-design/icons-vue";
|
||||||
|
import { message } from "ant-design-vue";
|
||||||
|
|
||||||
|
import { uploadTemplate } from "@/api/templates";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
const router = useRouter();
|
||||||
const statusClass = computed(() => (appStore.saveStatus === "已保存" ? "saved" : "dirty"));
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -31,7 +83,7 @@ const statusClass = computed(() => (appStore.saveStatus === "已保存" ? "saved
|
|||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<span class="save-status" :class="statusClass">{{ appStore.saveStatus }}</span>
|
<span class="save-status" :class="statusClass">{{ appStore.saveStatus }}</span>
|
||||||
<a-button type="primary">
|
<a-button type="primary" @click="openUploadModal">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon><PlusOutlined /></template>
|
||||||
上传模板
|
上传模板
|
||||||
</a-button>
|
</a-button>
|
||||||
@@ -46,6 +98,44 @@ const statusClass = computed(() => (appStore.saveStatus === "已保存" ? "saved
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</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>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -211,4 +301,40 @@ h1 {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--c-text-muted);
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user