完成报告模板请求

This commit is contained in:
wzclm 2025-02-21 04:13:08 +08:00
parent 986f573956
commit 595054eb43
4 changed files with 957 additions and 0 deletions

View File

@ -0,0 +1,71 @@
import request from '@/utils/request'
/**
* 获取报告模板列表
* @param {Object} params - 查询参数
* @param {number} [params.page=1] - 页码
* @param {number} [params.pageSize=10] - 每页条数
* @param {string} [params.templateCode] - 模板编号
* @param {string} [params.templateName] - 模板名称
* @param {string} [params.templateType] - 模板类型
* @param {number} [params.status] - 状态
* @returns {Promise} 返回模板列表数据
*/
export function getTemplateList(params = {}) {
return request.get('/api/reports/templates', {
params: {
page: 1,
pageSize: 10,
...params
}
})
}
/**
* 获取模板详情
* @param {string|number} id - 模板ID
* @returns {Promise} 返回模板详情数据
*/
export function getTemplateDetail(id) {
return request.get(`/api/reports/templates/${id}`)
}
/**
* 创建报告模板
* @param {Object} data - 模板数据
* @returns {Promise} 返回创建结果
*/
export function createTemplate(data) {
return request.post('/api/reports/templates', data)
}
/**
* 更新报告模板
* @param {string|number} id - 模板ID
* @param {Object} data - 更新数据
* @returns {Promise} 返回更新结果
*/
export function updateTemplate(id, data) {
return request.put(`/api/reports/templates/${id}`, data)
}
/**
* 删除报告模板
* @param {string|number} id - 模板ID
* @returns {Promise} 返回删除结果
*/
export function deleteTemplate(id) {
return request.delete(`/api/reports/templates/${id}`)
}
/**
* 更新模板状态
* @param {string|number} id - 模板ID
* @param {number} status - 状态0-禁用 1-启用
* @returns {Promise} 返回更新结果
*/
export function updateTemplateStatus(id, status) {
return request.put(`/api/reports/templates/${id}/status`, {
status: status === 1 ? 1 : 0 // 确保只发送 1 或 0
})
}

View File

@ -137,6 +137,7 @@ const handleLogout = () => {
<span>报告管理</span>
</template>
<el-menu-item index="/report/daily">报告管理</el-menu-item>
<el-menu-item index="/report/reportTemplates">报告模板</el-menu-item>
<el-menu-item index="/report/analysis">分析报告</el-menu-item>
<el-menu-item index="/report/about">项目背景</el-menu-item>
</el-sub-menu>

View File

@ -79,6 +79,11 @@ const router = createRouter({
name: 'DailyReports',
component: () => import('../views/report/daily/index.vue')
},
{
path: 'report/reportTemplates',
name: 'ReportTemplates',
component: () => import('../views/report/reportTemplates/index.vue')
},
{
path: 'report/analysis',
name: 'AnalysisReports',

View File

@ -0,0 +1,880 @@
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search, Refresh } from '@element-plus/icons-vue'
import {
getTemplateList,
createTemplate,
updateTemplate,
deleteTemplate,
updateTemplateStatus
} from '@/api/report/template'
import { formatDateTime } from '@/utils/format'
//
const templateTypeOptions = [
{ label: '日报', value: 'daily' },
{ label: '周报', value: 'weekly' },
{ label: '月报', value: 'monthly' },
{ label: '自定义', value: 'custom' }
]
//
const statusOptions = [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 }
]
//
const searchForm = ref({
templateCode: '',
templateName: '',
templateType: '',
status: ''
})
//
const tableData = ref([])
const loading = ref(false)
//
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
//
const allData = ref([])
//
const sortArrayByField = (array, field, direction = 'asc') => {
return [...array].sort((a, b) => {
if (!a[field]) return direction === 'asc' ? 1 : -1;
if (!b[field]) return direction === 'asc' ? -1 : 1;
const valueA = a[field].toString().toLowerCase();
const valueB = b[field].toString().toLowerCase();
if (valueA < valueB) return direction === 'asc' ? -1 : 1;
if (valueA > valueB) return direction === 'asc' ? 1 : -1;
return 0;
});
};
//
const filteredData = computed(() => {
let result = [...allData.value];
//
if (searchForm.value.templateCode) {
const keyword = searchForm.value.templateCode.toLowerCase();
result = result.filter(item =>
item.template_code?.toLowerCase().includes(keyword)
);
}
//
if (searchForm.value.templateName) {
const keyword = searchForm.value.templateName.toLowerCase();
result = result.filter(item =>
item.template_name?.toLowerCase().includes(keyword)
);
}
//
if (searchForm.value.templateType) {
result = result.filter(item =>
item.template_type === searchForm.value.templateType
);
}
// - 0 1
if (searchForm.value.status === 0 || searchForm.value.status === 1) {
result = result.filter(item =>
item.status === searchForm.value.status
);
}
//
return sortArrayByField(result, 'created_at', 'asc');
});
//
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return filteredData.value.slice(start, end);
});
//
const updateTableData = () => {
tableData.value = paginatedData.value;
total.value = filteredData.value.length;
};
//
const getList = async () => {
loading.value = true;
try {
const res = await getTemplateList();
if (res.success) {
allData.value = res.data || [];
updateTableData();
} else {
ElMessage.error(res.message || '获取模板列表失败');
allData.value = [];
tableData.value = [];
total.value = 0;
}
} catch (error) {
console.error('获取模板列表错误:', error);
ElMessage.error('获取模板列表失败');
allData.value = [];
tableData.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
//
const handleSearch = () => {
currentPage.value = 1;
updateTableData();
};
//
const resetSearch = () => {
//
searchForm.value = {
templateCode: '',
templateName: '',
templateType: '',
status: '' //
};
currentPage.value = 1;
updateTableData();
};
//
watch(
[
() => searchForm.value.templateCode,
() => searchForm.value.templateName,
() => searchForm.value.templateType,
() => searchForm.value.status,
() => currentPage.value,
() => pageSize.value
],
() => {
updateTableData();
}
);
//
const handleRefresh = () => {
getList()
}
// /
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formMode = ref('create') // create or edit
const form = ref({
template_name: '',
template_type: '',
content_structure: {
sections: []
},
variables: {},
status: 1
})
//
const formRules = {
template_name: [
{ required: true, message: '请输入模板名称', trigger: 'blur' },
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
],
template_type: [
{ required: true, message: '请选择模板类型', trigger: 'change' }
]
}
//
const handleAdd = () => {
formMode.value = 'create';
dialogTitle.value = '新增模板';
form.value = {
template_name: '',
template_type: '',
content_structure: defaultTemplateStructure,
variables: defaultVariables,
status: 1
};
dialogVisible.value = true;
};
//
const handleEdit = (row) => {
formMode.value = 'edit'
dialogTitle.value = '编辑模板'
//
form.value = JSON.parse(JSON.stringify(row))
dialogVisible.value = true
}
//
const formRef = ref(null)
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
const submitData = { ...form.value }
let res
if (formMode.value === 'create') {
res = await createTemplate(submitData)
} else {
const id = submitData.id
delete submitData.id
res = await updateTemplate(id, submitData)
}
if (res.success) {
ElMessage.success(`${formMode.value === 'create' ? '新增' : '编辑'}成功`)
dialogVisible.value = false
getList()
} else {
ElMessage.error(res.message || `${formMode.value === 'create' ? '新增' : '编辑'}失败`)
}
} catch (error) {
console.error(`${formMode.value === 'create' ? '新增' : '编辑'}模板错误:`, error)
ElMessage.error(`${formMode.value === 'create' ? '新增' : '编辑'}失败`)
}
}
})
}
//
const handleDelete = (row) => {
//
if (row.report_count > 0) {
ElMessage.warning(`该模板下已关联${row.report_count}份报告,不能删除`);
return;
}
ElMessageBox.confirm('确认删除该模板吗?此操作不可恢复', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteTemplate(row.id)
if (res.success) {
ElMessage.success('删除成功')
getList()
} else {
ElMessage.error(res.message || '删除失败')
}
} catch (error) {
console.error('删除模板错误:', error)
ElMessage.error(error.response?.data?.message || error.message || '删除失败')
}
}).catch(() => {})
}
//
const handleStatusChange = async (row) => {
//
const originalStatus = row.status;
//
if (originalStatus === 0 && row.report_count > 0) {
ElMessage.warning(`该模板下已关联${row.report_count}份报告,不能禁用`);
row.status = 1; //
return;
}
try {
const res = await updateTemplateStatus(row.id, originalStatus);
if (res.success) {
ElMessage.success('状态更新成功');
await getList(); //
} else {
row.status = originalStatus === 1 ? 0 : 1; //
ElMessage.error(res.message || '状态更新失败');
}
} catch (error) {
row.status = originalStatus === 1 ? 0 : 1; //
console.error('更新模板状态错误:', error);
ElMessage.error(error.response?.data?.message || error.message || '状态更新失败');
}
};
// JSON
const formatJSON = (json) => {
try {
return JSON.stringify(json, null, 2)
} catch (error) {
return '{}'
}
}
//
const detailVisible = ref(false)
const currentTemplate = ref(null)
//
const handleView = (row) => {
currentTemplate.value = row;
detailVisible.value = true;
};
//
const getFieldTypeName = (type) => {
const typeMap = {
text: '文本',
textarea: '多行文本',
number: '数字',
datetime: '日期时间',
select: '下拉选择',
checkbox: '多选框',
radio: '单选框',
image: '图片上传'
};
return typeMap[type] || type;
};
//
const defaultTemplateStructure = {
sections: [
{
title: "基本信息",
fields: [
{
name: "inspector",
label: "巡检人员",
type: "text",
required: true
},
{
name: "inspection_time",
label: "巡检时间",
type: "datetime",
required: true
},
{
name: "weather",
label: "天气状况",
type: "select",
options: ["晴", "多云", "阴", "雨", "雪"],
required: true
},
{
name: "temperature",
label: "温度(℃)",
type: "number",
required: true
}
]
},
{
title: "水质监测",
fields: [
{
name: "water_temperature",
label: "水温(℃)",
type: "number",
required: true
},
{
name: "ph_value",
label: "pH值",
type: "number",
required: true,
min: 0,
max: 14
},
{
name: "dissolved_oxygen",
label: "溶解氧(mg/L)",
type: "number",
required: true
},
{
name: "turbidity",
label: "浊度(NTU)",
type: "number",
required: true
}
]
},
{
title: "生态观察",
fields: [
{
name: "plant_status",
label: "植物状况",
type: "textarea",
required: true,
placeholder: "请描述植物生长情况、是否发现外来物种等"
},
{
name: "animal_observation",
label: "动物观察",
type: "textarea",
required: true,
placeholder: "请记录观察到的动物种类、数量等"
},
{
name: "photos",
label: "现场照片",
type: "image",
required: true,
max_count: 5
}
]
},
{
title: "问题记录",
fields: [
{
name: "issues",
label: "发现的问题",
type: "checkbox",
options: [
"水质异常",
"植物病虫害",
"外来物种入侵",
"人为破坏",
"设备故障",
"其他"
],
required: false
},
{
name: "issue_description",
label: "问题描述",
type: "textarea",
required: false,
placeholder: "请详细描述发现的问题"
},
{
name: "emergency_level",
label: "紧急程度",
type: "radio",
options: ["一般", "较急", "紧急", "特急"],
required: false
}
]
},
{
title: "处理建议",
fields: [
{
name: "suggestions",
label: "处理建议",
type: "textarea",
required: false,
placeholder: "请提出处理问题的建议"
}
]
}
]
};
//
const defaultVariables = {
location: {
type: "string",
label: "巡检地点",
default: "主湿地公园"
},
department: {
type: "string",
label: "巡检部门",
default: "生态保护科"
}
};
onMounted(() => {
getList()
})
</script>
<template>
<div class="report-templates">
<el-card>
<template #header>
<div class="card-header">
<span>报告模板</span>
<div class="header-btns">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增模板</el-button>
<el-button :icon="Refresh" @click="handleRefresh">刷新</el-button>
</div>
</div>
</template>
<!-- 搜索表单 -->
<el-form :model="searchForm" inline class="search-form">
<el-form-item label="模板编号" label-width="80px">
<el-input
v-model="searchForm.templateCode"
placeholder="请输入模板编号"
clearable
/>
</el-form-item>
<el-form-item label="模板名称" label-width="80px">
<el-input
v-model="searchForm.templateName"
placeholder="请输入模板名称"
clearable
/>
</el-form-item>
<el-form-item label="模板类型" label-width="80px">
<el-select
v-model="searchForm.templateType"
placeholder="请选择类型"
clearable
>
<el-option
v-for="item in templateTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" label-width="80px">
<el-select
v-model="searchForm.status"
placeholder="请选择状态"
clearable
>
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item class="search-buttons">
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<!-- 模板列表 -->
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
>
<el-table-column type="index" label="序号" width="60" align="center"/>
<el-table-column prop="template_code" label="模板编号" width="120" align="center"/>
<el-table-column prop="template_name" label="模板名称" min-width="150" show-overflow-tooltip align="center"/>
<el-table-column prop="template_type" label="模板类型" width="100" align="center">
<template #default="{ row }">
<el-tag
:type="row.template_type === 'daily' ? 'primary' :
row.template_type === 'weekly' ? 'success' :
row.template_type === 'monthly' ? 'warning' :
row.template_type === 'custom' ? 'danger' :
'info'"
size="small"
>
{{ row.template_type === 'daily' ? '日报' :
row.template_type === 'weekly' ? '周报' :
row.template_type === 'monthly' ? '月报' :
row.template_type === 'custom' ? '自定义' :
'未知' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="report_count" label="关联报告" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.report_count > 0 ? 'warning' : 'info'" size="small">
{{ row.report_count > 0 ? `${row.report_count}份报告` : '无报告' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
@change="() => handleStatusChange(row)"
/>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="160" align="center">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
</el-table-column>
<el-table-column prop="updated_at" label="更新时间" width="160" align="center">
<template #default="{ row }">
{{ formatDateTime(row.updated_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link @click="handleView(row)">查看</el-button>
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页器 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
background
layout="total, sizes, prev, pager, next"
/>
</div>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="800px"
destroy-on-close
>
<el-form
ref="formRef"
:model="form"
:rules="formRules"
label-width="100px"
>
<el-form-item label="模板名称" prop="template_name">
<el-input
v-model="form.template_name"
placeholder="请输入模板名称"
/>
</el-form-item>
<el-form-item label="模板类型" prop="template_type">
<el-select
v-model="form.template_type"
placeholder="请选择模板类型"
>
<el-option
v-for="item in templateTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="内容结构">
<el-input
v-model="form.content_structure"
type="textarea"
:rows="10"
:value="formatJSON(form.content_structure)"
@input="val => form.content_structure = JSON.parse(val || '{}')"
placeholder="请输入JSON格式的内容结构"
/>
</el-form-item>
<el-form-item label="变量定义">
<el-input
v-model="form.variables"
type="textarea"
:rows="6"
:value="formatJSON(form.variables)"
@input="val => form.variables = JSON.parse(val || '{}')"
placeholder="请输入JSON格式的变量定义"
/>
</el-form-item>
<el-form-item label="状态">
<el-switch
v-model="form.status"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 添加查看详情对话框 -->
<el-dialog
v-model="detailVisible"
title="模板详情"
width="800px"
destroy-on-close
>
<el-descriptions
v-if="currentTemplate"
:column="2"
border
>
<el-descriptions-item label="模板编号" :span="2">
{{ currentTemplate.template_code }}
</el-descriptions-item>
<el-descriptions-item label="模板名称" :span="2">
{{ currentTemplate.template_name }}
</el-descriptions-item>
<el-descriptions-item label="模板类型">
<el-tag
:type="currentTemplate.template_type === 'daily' ? 'primary' :
currentTemplate.template_type === 'weekly' ? 'success' :
currentTemplate.template_type === 'monthly' ? 'warning' :
currentTemplate.template_type === 'custom' ? 'danger' :
'info'"
>
{{ currentTemplate.template_type === 'daily' ? '日报' :
currentTemplate.template_type === 'weekly' ? '周报' :
currentTemplate.template_type === 'monthly' ? '月报' :
currentTemplate.template_type === 'custom' ? '自定义' :
'未知' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="currentTemplate.status === 1 ? 'success' : 'info'">
{{ currentTemplate.status === 1 ? '启用' : '禁用' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
<!-- 内容结构 -->
<div v-if="currentTemplate?.content_structure?.sections" class="template-content mt-20">
<div v-for="(section, sIndex) in currentTemplate.content_structure.sections" :key="sIndex" class="section-item">
<h3 class="section-title">{{ section.title }}</h3>
<el-table :data="section.fields" border style="width: 100%">
<el-table-column prop="label" label="字段名称" width="150" />
<el-table-column prop="name" label="字段标识" width="150" />
<el-table-column label="字段类型" width="120">
<template #default="{ row }">
<el-tag size="small">{{ getFieldTypeName(row.type) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="是否必填" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.required ? 'danger' : 'info'" size="small">
{{ row.required ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="其他设置">
<template #default="{ row }">
<div v-if="row.options">选项: {{ row.options.join(', ') }}</div>
<div v-if="row.min !== undefined">最小值: {{ row.min }}</div>
<div v-if="row.max !== undefined">最大值: {{ row.max }}</div>
<div v-if="row.max_count">最大数量: {{ row.max_count }}</div>
<div v-if="row.placeholder">提示文本: {{ row.placeholder }}</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 变量定义 -->
<div v-if="currentTemplate?.variables" class="variables mt-20">
<h3 class="section-title">变量定义</h3>
<el-table :data="Object.entries(currentTemplate.variables).map(([key, value]) => ({
key,
...value
}))" border style="width: 100%">
<el-table-column prop="key" label="变量名" width="150" />
<el-table-column prop="label" label="显示名称" width="150" />
<el-table-column prop="type" label="类型" width="120">
<template #default="{ row }">
<el-tag size="small">{{ getFieldTypeName(row.type) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="default" label="默认值" />
</el-table>
</div>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
.report-templates {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
}
.header-btns {
display: flex;
gap: 12px;
}
}
.search-form {
margin-bottom: 24px;
padding: 24px;
background-color: #f8f9fa;
border-radius: 8px;
:deep(.el-form-item) {
margin-bottom: 16px;
margin-right: 24px;
&:last-child {
margin-right: 0;
margin-bottom: 0;
}
}
:deep(.el-input),
:deep(.el-select) {
width: 220px;
}
.search-buttons {
display: flex;
gap: 12px;
}
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
}
.mt-20 {
margin-top: 20px;
}
.template-content {
.section-item {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
}
}
.section-title {
font-size: 16px;
font-weight: 600;
margin: 16px 0;
padding-left: 10px;
border-left: 4px solid var(--el-color-primary);
}
</style>