feat: add template center list
This commit is contained in:
@@ -17,6 +17,12 @@ from ..services.template_service import create_template_record, save_template_bl
|
|||||||
router = APIRouter(prefix="/api/templates", tags=["templates"])
|
router = APIRouter(prefix="/api/templates", tags=["templates"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
def list_templates(db: Session = Depends(get_db)):
|
||||||
|
templates = db.query(Template).order_by(Template.updated_at.desc(), Template.id.desc()).all()
|
||||||
|
return {"data": [serialize_template(template) for template in templates], "message": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/upload")
|
@router.post("/upload")
|
||||||
def upload_template(
|
def upload_template(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
|
|||||||
@@ -176,3 +176,16 @@
|
|||||||
5. 上传失败时给出后端服务或数据库未启动的提示。
|
5. 上传失败时给出后端服务或数据库未启动的提示。
|
||||||
6. 执行前端生产构建验证。
|
6. 执行前端生产构建验证。
|
||||||
- **执行结果**: 模板中心“上传模板”按钮已接入后端上传接口,成功后可进入模板编辑页。
|
- **执行结果**: 模板中心“上传模板”按钮已接入后端上传接口,成功后可进入模板编辑页。
|
||||||
|
|
||||||
|
## 会话 ID: 20260701-template-list
|
||||||
|
- [2026-07-01 21:49:52]
|
||||||
|
- **执行原因**: 继续完善模板中心,使上传后的模板可在列表中查看和进入标注页。
|
||||||
|
- **执行过程**:
|
||||||
|
1. 后端新增 `GET /api/templates`,按更新时间和 ID 倒序返回模板列表。
|
||||||
|
2. 前端模板 API 模块新增 `getTemplates`。
|
||||||
|
3. 模板中心页面初始化时加载模板列表。
|
||||||
|
4. 将模板中心空态升级为 Ant Design Vue 表格,展示名称、类型、版本、状态、更新时间和标注操作。
|
||||||
|
5. 增加列表加载失败提示和重试按钮。
|
||||||
|
6. 上传成功后刷新模板列表并跳转编辑页。
|
||||||
|
7. 执行后端路由检查和前端生产构建验证。
|
||||||
|
- **执行结果**: 模板中心已具备模板列表展示、失败重试和进入标注页能力。
|
||||||
|
|||||||
@@ -30,6 +30,21 @@ export type TemplateDetailResponse = {
|
|||||||
configs: Record<string, BlockConfig>;
|
configs: Record<string, BlockConfig>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TemplateListItem = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
version: string;
|
||||||
|
status: number;
|
||||||
|
created_at?: string | null;
|
||||||
|
updated_at?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getTemplates() {
|
||||||
|
const response = await apiClient.get<{ data: TemplateListItem[]; message: string }>("/templates");
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getTemplateDetail(templateId: number) {
|
export async function getTemplateDetail(templateId: number) {
|
||||||
const response = await apiClient.get<{ data: TemplateDetailResponse; message: string }>(
|
const response = await apiClient.get<{ data: TemplateDetailResponse; message: string }>(
|
||||||
`/templates/${templateId}`,
|
`/templates/${templateId}`,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { FileTextOutlined, PlusOutlined } from "@ant-design/icons-vue";
|
import { EditOutlined, FileTextOutlined, PlusOutlined, ReloadOutlined } from "@ant-design/icons-vue";
|
||||||
import { message } from "ant-design-vue";
|
import { message } from "ant-design-vue";
|
||||||
|
|
||||||
import { uploadTemplate } from "@/api/templates";
|
import { getTemplates, type TemplateListItem, uploadTemplate } from "@/api/templates";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
@@ -14,11 +14,39 @@ const uploadOpen = ref(false);
|
|||||||
const uploading = ref(false);
|
const uploading = ref(false);
|
||||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||||
const selectedFile = ref<File | null>(null);
|
const selectedFile = ref<File | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const loadError = ref("");
|
||||||
|
const templates = ref<TemplateListItem[]>([]);
|
||||||
const form = ref({
|
const form = ref({
|
||||||
name: "",
|
name: "",
|
||||||
type: "report",
|
type: "report",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tableColumns = [
|
||||||
|
{ title: "模板名称", dataIndex: "name", key: "name" },
|
||||||
|
{ title: "类型", dataIndex: "type", key: "type", width: 120 },
|
||||||
|
{ title: "版本", dataIndex: "version", key: "version", width: 120 },
|
||||||
|
{ title: "状态", dataIndex: "status", key: "status", width: 100 },
|
||||||
|
{ title: "更新时间", dataIndex: "updated_at", key: "updated_at", width: 190 },
|
||||||
|
{ title: "操作", key: "actions", width: 110 },
|
||||||
|
];
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadTemplates();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadTemplates() {
|
||||||
|
loading.value = true;
|
||||||
|
loadError.value = "";
|
||||||
|
try {
|
||||||
|
templates.value = await getTemplates();
|
||||||
|
} catch {
|
||||||
|
loadError.value = "模板列表加载失败,请确认后端服务和数据库已启动";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openUploadModal() {
|
function openUploadModal() {
|
||||||
uploadOpen.value = true;
|
uploadOpen.value = true;
|
||||||
}
|
}
|
||||||
@@ -51,6 +79,7 @@ async function submitUpload() {
|
|||||||
const result = await uploadTemplate(selectedFile.value, form.value.name.trim(), form.value.type);
|
const result = await uploadTemplate(selectedFile.value, form.value.name.trim(), form.value.type);
|
||||||
message.success(`模板上传成功,解析 ${result.block_count} 个区域`);
|
message.success(`模板上传成功,解析 ${result.block_count} 个区域`);
|
||||||
uploadOpen.value = false;
|
uploadOpen.value = false;
|
||||||
|
await loadTemplates();
|
||||||
await router.push(`/templates/${result.template_id}/edit`);
|
await router.push(`/templates/${result.template_id}/edit`);
|
||||||
} catch {
|
} catch {
|
||||||
message.error("模板上传失败,请确认后端服务和数据库已启动");
|
message.error("模板上传失败,请确认后端服务和数据库已启动");
|
||||||
@@ -58,6 +87,15 @@ async function submitUpload() {
|
|||||||
uploading.value = false;
|
uploading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openEditor(templateId: number) {
|
||||||
|
router.push(`/templates/${templateId}/edit`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(value?: string | null) {
|
||||||
|
if (!value) return "-";
|
||||||
|
return value.replace("T", " ").slice(0, 19);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -91,10 +129,55 @@ async function submitUpload() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="content">
|
<section class="content">
|
||||||
<div class="empty-panel">
|
<a-alert
|
||||||
|
v-if="loadError"
|
||||||
|
class="list-alert"
|
||||||
|
type="error"
|
||||||
|
show-icon
|
||||||
|
:message="loadError"
|
||||||
|
>
|
||||||
|
<template #action>
|
||||||
|
<a-button size="small" @click="loadTemplates">
|
||||||
|
<template #icon><ReloadOutlined /></template>
|
||||||
|
重试
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</a-alert>
|
||||||
|
|
||||||
|
<a-table
|
||||||
|
v-if="templates.length"
|
||||||
|
:columns="tableColumns"
|
||||||
|
:data-source="templates"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="{ pageSize: 8 }"
|
||||||
|
row-key="id"
|
||||||
|
size="middle"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'name'">
|
||||||
|
<strong>{{ record.name }}</strong>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'status'">
|
||||||
|
<a-tag :color="record.status === 1 ? 'green' : 'default'">
|
||||||
|
{{ record.status === 1 ? "启用" : "停用" }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'updated_at'">
|
||||||
|
{{ formatTime(record.updated_at) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'actions'">
|
||||||
|
<a-button size="small" type="link" @click="openEditor(record.id)">
|
||||||
|
<template #icon><EditOutlined /></template>
|
||||||
|
标注
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
|
||||||
|
<div v-else class="empty-panel">
|
||||||
<FileTextOutlined class="empty-icon" />
|
<FileTextOutlined class="empty-icon" />
|
||||||
<h2>前端基础工程已就绪</h2>
|
<h2>{{ loading ? "正在加载模板" : "暂无模板" }}</h2>
|
||||||
<p>Vue 3、Vite、Ant Design Vue、Router、Pinia、Axios 与全局样式变量已接入。</p>
|
<p>{{ loadError ? "可点击重试或先上传一个 Word 模板。" : "上传 Word 模板后即可进入标注配置页。" }}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
@@ -273,6 +356,10 @@ h1 {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-alert {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-panel {
|
.empty-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 320px;
|
min-height: 320px;
|
||||||
|
|||||||
Reference in New Issue
Block a user