|
|
|
|
@ -0,0 +1,749 @@
|
|
|
|
|
<!-- 能源报告模板列表页(卡片视图) -->
|
|
|
|
|
<template>
|
|
|
|
|
<div class="template-list-page">
|
|
|
|
|
<!-- 顶部搜索栏 -->
|
|
|
|
|
<div class="search-bar">
|
|
|
|
|
<ElInput
|
|
|
|
|
v-model="keyword"
|
|
|
|
|
placeholder="请输入模板名称搜索"
|
|
|
|
|
clearable
|
|
|
|
|
class="search-input"
|
|
|
|
|
:prefix-icon="Search"
|
|
|
|
|
@keyup.enter="handleSearch"
|
|
|
|
|
@clear="handleSearch"
|
|
|
|
|
/>
|
|
|
|
|
<ElButton type="primary" :icon="Plus" @click="handleCreate">
|
|
|
|
|
新增模板
|
|
|
|
|
</ElButton>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 模板卡片网格 -->
|
|
|
|
|
<div v-loading="loading" class="template-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="item in templateList"
|
|
|
|
|
:key="item.id"
|
|
|
|
|
class="template-card"
|
|
|
|
|
@click="handleView(item)"
|
|
|
|
|
>
|
|
|
|
|
<!-- 右上角下拉菜单 -->
|
|
|
|
|
<div class="card-menu" @click.stop>
|
|
|
|
|
<ElDropdown trigger="click" @command="(cmd: string) => handleCommand(cmd, item)">
|
|
|
|
|
<ElButton link :icon="MoreFilled" class="menu-trigger" />
|
|
|
|
|
<template #dropdown>
|
|
|
|
|
<ElDropdownMenu>
|
|
|
|
|
<ElDropdownItem command="edit" :icon="EditPen">修改</ElDropdownItem>
|
|
|
|
|
</ElDropdownMenu>
|
|
|
|
|
</template>
|
|
|
|
|
</ElDropdown>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card-icon">
|
|
|
|
|
<ElIcon size="48"><Document /></ElIcon>
|
|
|
|
|
</div>
|
|
|
|
|
<h3 class="card-title" :title="item.template_name">{{ item.template_name }}</h3>
|
|
|
|
|
<p class="card-subtitle">
|
|
|
|
|
{{ templateTypeLabel(item.template_type) }}
|
|
|
|
|
<span v-if="item.version" class="version-tag">v{{ item.version }}</span>
|
|
|
|
|
</p>
|
|
|
|
|
<p class="card-time">
|
|
|
|
|
<ElIcon><Clock /></ElIcon>
|
|
|
|
|
最后更新:{{ formatDateTime(item.updated_time || item.created_time) }}
|
|
|
|
|
</p>
|
|
|
|
|
<p class="card-desc" :title="item.description">
|
|
|
|
|
{{ item.description || "暂无描述" }}
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<!-- 标注情况 -->
|
|
|
|
|
<div class="card-annotation">
|
|
|
|
|
<div class="annotation-header">
|
|
|
|
|
<ElIcon><CollectionTag /></ElIcon>
|
|
|
|
|
<span>标注情况</span>
|
|
|
|
|
<ElTag size="small" type="primary" effect="plain">
|
|
|
|
|
{{ item.annotation_count || 0 }} 项
|
|
|
|
|
</ElTag>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- <div class="annotation-rules">
|
|
|
|
|
<span class="no-annotation">暂无标注</span>
|
|
|
|
|
</div> -->
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 卡片底部操作按钮 -->
|
|
|
|
|
<div class="card-actions" @click.stop>
|
|
|
|
|
<ElButton type="primary" :icon="Edit" size="default" @click="handleEdit(item)">编辑</ElButton>
|
|
|
|
|
<ElButton type="danger" :icon="Delete" size="default" plain @click="handleDelete(item)">删除</ElButton>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<ElEmpty
|
|
|
|
|
v-if="!loading && templateList.length === 0"
|
|
|
|
|
class="empty-state"
|
|
|
|
|
description="暂无模板,点击右上角「新增模板」开始创建"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 分页 -->
|
|
|
|
|
<div v-if="total > 0" class="pagination">
|
|
|
|
|
<span class="total-text">共 {{ total }} 条</span>
|
|
|
|
|
<ElPagination
|
|
|
|
|
:current-page="pageNo"
|
|
|
|
|
:page-size="pageSize"
|
|
|
|
|
:total="total"
|
|
|
|
|
:page-sizes="[12, 24, 48]"
|
|
|
|
|
layout="sizes, prev, pager, next, jumper"
|
|
|
|
|
@current-change="onPageChange"
|
|
|
|
|
@size-change="onSizeChange"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 新增模板弹窗 -->
|
|
|
|
|
<ElDialog
|
|
|
|
|
v-model="createVisible"
|
|
|
|
|
title="新增模板"
|
|
|
|
|
width="600px"
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
destroy-on-close
|
|
|
|
|
>
|
|
|
|
|
<ElForm
|
|
|
|
|
ref="createFormRef"
|
|
|
|
|
:model="createForm"
|
|
|
|
|
:rules="createRules"
|
|
|
|
|
label-width="90px"
|
|
|
|
|
>
|
|
|
|
|
<ElFormItem label="模板名称" prop="template_name" required>
|
|
|
|
|
<ElInput v-model="createForm.template_name" placeholder="请输入模板名称" :maxlength="128" />
|
|
|
|
|
</ElFormItem>
|
|
|
|
|
<!-- <ElFormItem label="模板类型" prop="template_type" required>
|
|
|
|
|
<ElSelect v-model="createForm.template_type" placeholder="请选择模板类型" class="full-width">
|
|
|
|
|
<ElOption label="日报" value="daily" />
|
|
|
|
|
<ElOption label="周报" value="weekly" />
|
|
|
|
|
<ElOption label="月报" value="monthly" />
|
|
|
|
|
<ElOption label="年报" value="yearly" />
|
|
|
|
|
<ElOption label="自定义" value="custom" />
|
|
|
|
|
</ElSelect>
|
|
|
|
|
</ElFormItem> -->
|
|
|
|
|
<ElFormItem label="模板版本" prop="version">
|
|
|
|
|
<ElInput v-model="createForm.version" placeholder="如 1.0.0" :maxlength="32" />
|
|
|
|
|
</ElFormItem>
|
|
|
|
|
<ElFormItem label="模板文件" prop="file" required>
|
|
|
|
|
<ElUpload
|
|
|
|
|
ref="uploadRef"
|
|
|
|
|
:auto-upload="false"
|
|
|
|
|
:limit="1"
|
|
|
|
|
accept=".docx"
|
|
|
|
|
:file-list="fileList"
|
|
|
|
|
:on-change="handleFileChange"
|
|
|
|
|
:on-remove="handleFileRemove"
|
|
|
|
|
:on-exceed="handleExceed"
|
|
|
|
|
drag
|
|
|
|
|
>
|
|
|
|
|
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
|
|
|
|
<div class="el-upload__text">
|
|
|
|
|
将文件拖到此处,或<em>点击上传</em>
|
|
|
|
|
</div>
|
|
|
|
|
<template #tip>
|
|
|
|
|
<div class="el-upload__tip">仅支持 .docx 格式的 Word 文档</div>
|
|
|
|
|
</template>
|
|
|
|
|
</ElUpload>
|
|
|
|
|
</ElFormItem>
|
|
|
|
|
<ElFormItem label="模板描述" prop="description">
|
|
|
|
|
<ElInput
|
|
|
|
|
v-model="createForm.description"
|
|
|
|
|
type="textarea"
|
|
|
|
|
:rows="3"
|
|
|
|
|
placeholder="请输入模板描述(可选)"
|
|
|
|
|
/>
|
|
|
|
|
</ElFormItem>
|
|
|
|
|
</ElForm>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<ElButton @click="createVisible = false">取消</ElButton>
|
|
|
|
|
<ElButton type="primary" :loading="submitting" @click="handleSubmitCreate">
|
|
|
|
|
确定
|
|
|
|
|
</ElButton>
|
|
|
|
|
</template>
|
|
|
|
|
</ElDialog>
|
|
|
|
|
|
|
|
|
|
<!-- 修改模板弹窗 -->
|
|
|
|
|
<ElDialog
|
|
|
|
|
v-model="editVisible"
|
|
|
|
|
title="修改模板"
|
|
|
|
|
width="600px"
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
destroy-on-close
|
|
|
|
|
>
|
|
|
|
|
<ElForm
|
|
|
|
|
ref="editFormRef"
|
|
|
|
|
:model="editForm"
|
|
|
|
|
:rules="editRules"
|
|
|
|
|
label-width="90px"
|
|
|
|
|
>
|
|
|
|
|
<ElFormItem label="模板名称" prop="template_name" required>
|
|
|
|
|
<ElInput v-model="editForm.template_name" placeholder="请输入模板名称" :maxlength="128" />
|
|
|
|
|
</ElFormItem>
|
|
|
|
|
<ElFormItem label="模板版本" prop="version">
|
|
|
|
|
<ElInput v-model="editForm.version" placeholder="如 1.0.0" :maxlength="32" />
|
|
|
|
|
</ElFormItem>
|
|
|
|
|
<ElFormItem label="模板文件" prop="file">
|
|
|
|
|
<ElUpload
|
|
|
|
|
:auto-upload="false"
|
|
|
|
|
:limit="1"
|
|
|
|
|
accept=".docx"
|
|
|
|
|
:file-list="editFileList"
|
|
|
|
|
:on-change="handleEditFileChange"
|
|
|
|
|
:on-remove="handleEditFileRemove"
|
|
|
|
|
:on-exceed="handleExceed"
|
|
|
|
|
drag
|
|
|
|
|
>
|
|
|
|
|
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
|
|
|
|
<div class="el-upload__text">
|
|
|
|
|
将文件拖到此处,或<em>点击上传</em>
|
|
|
|
|
</div>
|
|
|
|
|
<template #tip>
|
|
|
|
|
<div class="el-upload__tip">
|
|
|
|
|
<span v-if="editForm.file_id && !editForm._newFile">
|
|
|
|
|
当前文件:{{ currentEditItem?.original_name || "已绑定文件" }},上传新文件将替换
|
|
|
|
|
</span>
|
|
|
|
|
<span v-else>仅支持 .docx 格式的 Word 文档</span>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</ElUpload>
|
|
|
|
|
</ElFormItem>
|
|
|
|
|
<ElFormItem label="模板描述" prop="description">
|
|
|
|
|
<ElInput
|
|
|
|
|
v-model="editForm.description"
|
|
|
|
|
type="textarea"
|
|
|
|
|
:rows="3"
|
|
|
|
|
placeholder="请输入模板描述(可选)"
|
|
|
|
|
/>
|
|
|
|
|
</ElFormItem>
|
|
|
|
|
</ElForm>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<ElButton @click="editVisible = false">取消</ElButton>
|
|
|
|
|
<ElButton type="primary" :loading="editSubmitting" @click="handleSubmitEdit">
|
|
|
|
|
保存
|
|
|
|
|
</ElButton>
|
|
|
|
|
</template>
|
|
|
|
|
</ElDialog>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
defineOptions({ name: "EnergyReportTemplateList" });
|
|
|
|
|
|
|
|
|
|
import { ref, reactive, onMounted } from "vue";
|
|
|
|
|
import { useRouter } from "vue-router";
|
|
|
|
|
import {
|
|
|
|
|
ElMessage,
|
|
|
|
|
ElMessageBox,
|
|
|
|
|
type FormInstance,
|
|
|
|
|
type FormRules,
|
|
|
|
|
type UploadFile,
|
|
|
|
|
type UploadFiles,
|
|
|
|
|
type UploadInstance,
|
|
|
|
|
} from "element-plus";
|
|
|
|
|
import {
|
|
|
|
|
Search,
|
|
|
|
|
Plus,
|
|
|
|
|
Edit,
|
|
|
|
|
Delete,
|
|
|
|
|
EditPen,
|
|
|
|
|
Document,
|
|
|
|
|
Clock,
|
|
|
|
|
CollectionTag,
|
|
|
|
|
UploadFilled,
|
|
|
|
|
MoreFilled,
|
|
|
|
|
} from "@element-plus/icons-vue";
|
|
|
|
|
import { FileAPI, ReportTemplateAPI } from "@/api/module_energy_report/report";
|
|
|
|
|
import type {
|
|
|
|
|
ReportTemplateItem,
|
|
|
|
|
ReportTemplateCreateForm,
|
|
|
|
|
ReportTemplateUpdateForm,
|
|
|
|
|
} from "@/api/module_energy_report/report";
|
|
|
|
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
const templateList = ref<ReportTemplateItem[]>([]);
|
|
|
|
|
const keyword = ref("");
|
|
|
|
|
const total = ref(0);
|
|
|
|
|
const pageNo = ref(1);
|
|
|
|
|
const pageSize = ref(12);
|
|
|
|
|
|
|
|
|
|
async function loadList() {
|
|
|
|
|
loading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const resp = await ReportTemplateAPI.page({
|
|
|
|
|
page_no: pageNo.value,
|
|
|
|
|
page_size: pageSize.value,
|
|
|
|
|
template_name: keyword.value?.trim() || undefined,
|
|
|
|
|
});
|
|
|
|
|
const data = resp?.data?.data;
|
|
|
|
|
templateList.value = data?.items ?? [];
|
|
|
|
|
total.value = data?.total ?? 0;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
templateList.value = [];
|
|
|
|
|
total.value = 0;
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleSearch() {
|
|
|
|
|
pageNo.value = 1;
|
|
|
|
|
loadList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onPageChange(p: number) {
|
|
|
|
|
pageNo.value = p;
|
|
|
|
|
loadList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onSizeChange(s: number) {
|
|
|
|
|
pageSize.value = s;
|
|
|
|
|
pageNo.value = 1;
|
|
|
|
|
loadList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function templateTypeLabel(type: string): string {
|
|
|
|
|
const map: Record<string, string> = {
|
|
|
|
|
daily: "日报模板",
|
|
|
|
|
weekly: "周报模板",
|
|
|
|
|
monthly: "月报模板",
|
|
|
|
|
yearly: "年报模板",
|
|
|
|
|
custom: "自定义模板",
|
|
|
|
|
};
|
|
|
|
|
return map[type] || type;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 新增弹窗 ==========
|
|
|
|
|
const createVisible = ref(false);
|
|
|
|
|
const submitting = ref(false);
|
|
|
|
|
const createFormRef = ref<FormInstance | null>(null);
|
|
|
|
|
const uploadRef = ref<UploadInstance | null>(null);
|
|
|
|
|
const fileList = ref<UploadFile[]>([]);
|
|
|
|
|
|
|
|
|
|
const createForm = reactive<{
|
|
|
|
|
template_name: string;
|
|
|
|
|
template_type: string;
|
|
|
|
|
version: string;
|
|
|
|
|
file: File | null;
|
|
|
|
|
description: string;
|
|
|
|
|
}>({
|
|
|
|
|
template_name: "",
|
|
|
|
|
template_type: "daily",
|
|
|
|
|
version: "1.0.0",
|
|
|
|
|
file: null,
|
|
|
|
|
description: "",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const createRules: FormRules = {
|
|
|
|
|
template_name: [
|
|
|
|
|
{ required: true, message: "请输入模板名称", trigger: "blur" },
|
|
|
|
|
{ max: 128, message: "最多 128 个字符", trigger: "blur" },
|
|
|
|
|
],
|
|
|
|
|
template_type: [{ required: true, message: "请选择模板类型", trigger: "change" }],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function handleCreate() {
|
|
|
|
|
createForm.template_name = "";
|
|
|
|
|
createForm.template_type = "daily";
|
|
|
|
|
createForm.version = "1.0.0";
|
|
|
|
|
createForm.file = null;
|
|
|
|
|
createForm.description = "";
|
|
|
|
|
fileList.value = [];
|
|
|
|
|
createVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleFileChange(file: UploadFile, files: UploadFiles) {
|
|
|
|
|
fileList.value = files.slice(-1);
|
|
|
|
|
if (file.raw) {
|
|
|
|
|
createForm.file = file.raw;
|
|
|
|
|
if (!createForm.template_name && file.name) {
|
|
|
|
|
createForm.template_name = file.name.replace(/\.docx$/i, "");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleFileRemove() {
|
|
|
|
|
fileList.value = [];
|
|
|
|
|
createForm.file = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleExceed() {
|
|
|
|
|
ElMessage.warning("仅支持上传一个文件,新文件将替换旧文件");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleSubmitCreate() {
|
|
|
|
|
try {
|
|
|
|
|
await createFormRef.value?.validate();
|
|
|
|
|
} catch {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!createForm.file) {
|
|
|
|
|
ElMessage.warning("请先上传模板文件");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
submitting.value = true;
|
|
|
|
|
try {
|
|
|
|
|
// 1. 先上传文件
|
|
|
|
|
const uploadResp = await FileAPI.upload(createForm.file);
|
|
|
|
|
const fileId = uploadResp?.data?.data?.id;
|
|
|
|
|
if (!fileId) {
|
|
|
|
|
ElMessage.error("文件上传失败");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 2. 创建模板
|
|
|
|
|
const body: ReportTemplateCreateForm = {
|
|
|
|
|
template_name: createForm.template_name.trim(),
|
|
|
|
|
// template_type: createForm.template_type,
|
|
|
|
|
version: createForm.version?.trim() || undefined,
|
|
|
|
|
file_id: fileId,
|
|
|
|
|
status: 0,
|
|
|
|
|
description: createForm.description?.trim() || undefined,
|
|
|
|
|
};
|
|
|
|
|
const resp = await ReportTemplateAPI.create(body);
|
|
|
|
|
const data = resp?.data?.data;
|
|
|
|
|
createVisible.value = false;
|
|
|
|
|
await loadList();
|
|
|
|
|
// if (data?.id) {
|
|
|
|
|
// router.push(`/energy_report/template/edit?id=${data.id}`);
|
|
|
|
|
// }
|
|
|
|
|
} catch (e) {
|
|
|
|
|
} finally {
|
|
|
|
|
submitting.value = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 卡片操作 ==========
|
|
|
|
|
function handleView(item: ReportTemplateItem) {
|
|
|
|
|
router.push(`/energy_report/template/detail?id=${item.id}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleEdit(item: ReportTemplateItem) {
|
|
|
|
|
router.push(`/energy_report/template/edit?id=${item.id}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 卡片下拉菜单 ==========
|
|
|
|
|
function handleCommand(cmd: string, item: ReportTemplateItem) {
|
|
|
|
|
if (cmd === "edit") {
|
|
|
|
|
openEditDialog(item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 修改弹窗 ==========
|
|
|
|
|
const editVisible = ref(false);
|
|
|
|
|
const editSubmitting = ref(false);
|
|
|
|
|
const editFormRef = ref<FormInstance | null>(null);
|
|
|
|
|
const editFileList = ref<UploadFile[]>([]);
|
|
|
|
|
const currentEditItem = ref<ReportTemplateItem | null>(null);
|
|
|
|
|
|
|
|
|
|
const editForm = reactive<{
|
|
|
|
|
id: number;
|
|
|
|
|
template_name: string;
|
|
|
|
|
version: string;
|
|
|
|
|
file_id: number;
|
|
|
|
|
file: File | null;
|
|
|
|
|
_newFile: boolean;
|
|
|
|
|
description: string;
|
|
|
|
|
}>({
|
|
|
|
|
id: 0,
|
|
|
|
|
template_name: "",
|
|
|
|
|
version: "",
|
|
|
|
|
file_id: 0,
|
|
|
|
|
file: null,
|
|
|
|
|
_newFile: false,
|
|
|
|
|
description: "",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const editRules: FormRules = {
|
|
|
|
|
template_name: [
|
|
|
|
|
{ required: true, message: "请输入模板名称", trigger: "blur" },
|
|
|
|
|
{ max: 128, message: "最多 128 个字符", trigger: "blur" },
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function openEditDialog(item: ReportTemplateItem) {
|
|
|
|
|
currentEditItem.value = item;
|
|
|
|
|
editForm.id = item.id;
|
|
|
|
|
editForm.template_name = item.template_name;
|
|
|
|
|
editForm.version = item.version || "";
|
|
|
|
|
editForm.file_id = item.file_id;
|
|
|
|
|
editForm.file = null;
|
|
|
|
|
editForm._newFile = false;
|
|
|
|
|
editForm.description = item.description || "";
|
|
|
|
|
editFileList.value = [];
|
|
|
|
|
editVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleEditFileChange(file: UploadFile, files: UploadFiles) {
|
|
|
|
|
editFileList.value = files.slice(-1);
|
|
|
|
|
if (file.raw) {
|
|
|
|
|
editForm.file = file.raw;
|
|
|
|
|
editForm._newFile = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleEditFileRemove() {
|
|
|
|
|
editFileList.value = [];
|
|
|
|
|
editForm.file = null;
|
|
|
|
|
editForm._newFile = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleSubmitEdit() {
|
|
|
|
|
try {
|
|
|
|
|
await editFormRef.value?.validate();
|
|
|
|
|
} catch {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
editSubmitting.value = true;
|
|
|
|
|
try {
|
|
|
|
|
let fileId = editForm.file_id;
|
|
|
|
|
if (editForm._newFile && editForm.file) {
|
|
|
|
|
const uploadResp = await FileAPI.upload(editForm.file);
|
|
|
|
|
const newFileId = uploadResp?.data?.data?.id;
|
|
|
|
|
if (!newFileId) {
|
|
|
|
|
ElMessage.error("文件上传失败");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
fileId = newFileId;
|
|
|
|
|
}
|
|
|
|
|
const body: ReportTemplateUpdateForm = {
|
|
|
|
|
template_name: editForm.template_name.trim(),
|
|
|
|
|
version: editForm.version?.trim() || undefined,
|
|
|
|
|
file_id: fileId,
|
|
|
|
|
description: editForm.description?.trim() || undefined,
|
|
|
|
|
};
|
|
|
|
|
await ReportTemplateAPI.update(editForm.id, body);
|
|
|
|
|
editVisible.value = false;
|
|
|
|
|
await loadList();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
} finally {
|
|
|
|
|
editSubmitting.value = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleDelete(item: ReportTemplateItem) {
|
|
|
|
|
ElMessageBox.confirm(`确定要删除模板「${item.template_name}」吗?`, "删除确认", {
|
|
|
|
|
type: "warning",
|
|
|
|
|
confirmButtonText: "删除",
|
|
|
|
|
cancelButtonText: "取消",
|
|
|
|
|
})
|
|
|
|
|
.then(async () => {
|
|
|
|
|
try {
|
|
|
|
|
await ReportTemplateAPI.remove([item.id]);
|
|
|
|
|
loadList();
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatDateTime(value?: string): string {
|
|
|
|
|
if (!value) return "-";
|
|
|
|
|
const d = new Date(value);
|
|
|
|
|
if (Number.isNaN(d.getTime())) return value;
|
|
|
|
|
const pad = (n: number) => String(n).padStart(2, "0");
|
|
|
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(
|
|
|
|
|
d.getHours()
|
|
|
|
|
)}:${pad(d.getMinutes())}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
loadList();
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.template-list-page {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
height: 100%;
|
|
|
|
|
// padding: 16px;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-bar {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-input {
|
|
|
|
|
width: 280px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.template-grid {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
|
|
|
gap: 16px;
|
|
|
|
|
align-content: start;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
padding-right: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.template-card {
|
|
|
|
|
position: relative;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
padding: 24px 20px 16px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
border-color: #409eff;
|
|
|
|
|
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.12);
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
padding-top: 10px;
|
|
|
|
|
border-top: 1px solid #f3f4f6;
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
|
|
|
|
.el-button {
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-menu {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 8px;
|
|
|
|
|
right: 8px;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-trigger {
|
|
|
|
|
padding: 4px;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
&:hover {
|
|
|
|
|
color: #409eff;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-icon {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
width: 64px;
|
|
|
|
|
height: 64px;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
|
|
|
|
color: #409eff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-title {
|
|
|
|
|
margin: 4px 0 0;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #111827;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
padding-right: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-subtitle {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.version-tag {
|
|
|
|
|
background: #f3f4f6;
|
|
|
|
|
padding: 1px 6px;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-time {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-desc {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
-webkit-line-clamp: 1;
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-annotation {
|
|
|
|
|
background: #f9fafb;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.annotation-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #374151;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.annotation-rules {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.no-annotation {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-state {
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
padding: 80px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pagination {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
// background: #fff;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
//border: 1px solid #e5e7eb;
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.total-text {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.full-width {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
</style>
|