mod 巡检记录展示与表单,增加配置类型折叠功能及相关样式

pull/853/head
wersd 2025-11-24 21:19:27 +08:00
parent a44b8e3243
commit 63c3e8ab66
4 changed files with 483 additions and 96 deletions

View File

@ -105,7 +105,6 @@ import { ElMessage } from 'element-plus'
import { Crontab } from '@/components/Crontab' import { Crontab } from '@/components/Crontab'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import * as ProjectInspectionConfigApi from '@/api/crm/project/inspection' import * as ProjectInspectionConfigApi from '@/api/crm/project/inspection'
import * as ProjectInspectionCheckpointApi from '@/api/crm/project/inspection/checkpoint'
defineProps<{ defineProps<{
disabled?: boolean disabled?: boolean
@ -186,9 +185,6 @@ const open = async (projectId?: number) => {
try { try {
// //
await loadCheckpointList() await loadCheckpointList()
})
}
} catch (error) { } catch (error) {
console.error('加载配置数据失败:', error) console.error('加载配置数据失败:', error)
ElMessage.error('加载配置数据失败') ElMessage.error('加载配置数据失败')

View File

@ -26,7 +26,12 @@
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="管理规则" prop="name"> <el-form-item label="管理规则" prop="name">
<el-input v-model="formData.name" placeholder="请输入管理规则" /> <el-input
v-model="formData.name"
placeholder="请输入管理规则"
:maxlength="12"
show-word-limit
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -168,7 +173,10 @@ const formData = ref<Partial<ProjectInspectionDataApi.ProjectInspectionDataVO>>(
}) })
const formRules = reactive({ const formRules = reactive({
configType: [{ required: true, message: '巡检配置类型不能为空', trigger: 'change' }], configType: [{ required: true, message: '巡检配置类型不能为空', trigger: 'change' }],
name: [{ required: true, message: '管理规则不能为空', trigger: 'blur' }], name: [
{ required: true, message: '管理规则不能为空', trigger: 'blur' },
{ max: 12, message: '管理规则不能超过12个字符', trigger: 'blur' }
],
isRequired: [{ required: true, message: '是否必填不能为空', trigger: 'blur' }], isRequired: [{ required: true, message: '是否必填不能为空', trigger: 'blur' }],
type: [{ required: true, message: '类型不能为空', trigger: 'change' }], type: [{ required: true, message: '类型不能为空', trigger: 'change' }],
options: [ options: [

View File

@ -29,10 +29,21 @@
<!-- 分组下的表单项 --> <!-- 分组下的表单项 -->
<template v-for="item in group.items" :key="item.id"> <template v-for="item in group.items" :key="item.id">
<el-form-item <el-form-item
:label="item.name"
:prop="`data_${item.id}`" :prop="`data_${item.id}`"
:rules="getFieldRules(item)" :rules="getFieldRules(item)"
class="inspection-form-item"
> >
<template #label>
<div class="form-label-wrapper">
<el-tooltip
:content="item.name"
placement="top"
:disabled="!item.name || item.name.length <= 30"
>
<span class="form-label-text">{{ item.name }}</span>
</el-tooltip>
</div>
</template>
<!-- 文本类型 --> <!-- 文本类型 -->
<el-input <el-input
v-if="item.type === 1" v-if="item.type === 1"
@ -101,6 +112,10 @@
:file-size="20" :file-size="20"
:file-type="['pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt']" :file-type="['pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt']"
/> />
<!-- 备注显示在输入框下方 -->
<div v-if="item.remark" class="form-content-remark">
{{ item.remark }}
</div>
</el-form-item> </el-form-item>
</template> </template>
</template> </template>
@ -471,5 +486,63 @@ const submitForm = async () => {
font-size: 14px; font-size: 14px;
} }
} }
.inspection-form-item {
margin-bottom: 22px;
:deep(.el-form-item__label) {
line-height: 1.6;
word-break: break-word;
white-space: normal;
padding-right: 12px;
width: 200px !important;
min-width: 200px;
vertical-align: top;
padding-top: 8px;
}
:deep(.el-form-item__content) {
line-height: 1.6;
}
.form-label-wrapper {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
.form-label-text {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
font-weight: 500;
}
}
.form-content-remark {
font-size: 12px;
color: #606266;
line-height: 1.6;
word-break: break-word;
white-space: normal;
margin-top: 8px;
padding: 8px 12px;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border-radius: 6px;
border-left: 3px solid #409EFF;
box-shadow: 0 1px 3px rgba(64, 158, 255, 0.1);
width: 100%;
box-sizing: border-box;
&::before {
content: '💡';
margin-right: 6px;
font-size: 14px;
}
}
}
</style> </style>

View File

@ -156,42 +156,64 @@
</div> </div>
</div> </div>
<div v-if="item.summary" class="card-summary"> <div v-if="item.summary" class="card-summary">
<span class="summary-label">巡检内容</span> <div class="summary-header-row">
<span class="summary-label">巡检内容</span>
</div>
<div class="summary-content-wrapper"> <div class="summary-content-wrapper">
<template v-if="getParsedSummary(item.summary)"> <template v-if="getParsedSummary(item.summary)">
<div
v-for="(groupData, configTypeName) in getParsedSummary(item.summary)"
:key="configTypeName"
class="summary-group"
>
<div class="summary-group-title">{{ configTypeName }}</div>
<div <div
v-for="field in groupData" v-for="(groupData, configTypeName) in getParsedSummary(item.summary)"
:key="`${configTypeName}-${field.name}`" :key="configTypeName"
class="summary-field" class="summary-group"
> >
<span class="field-name">{{ field.name }}</span> <div
<span v-if="field.type === 6" class="field-value-image"> class="summary-group-header"
<img @click="toggleConfigType(item.id, configTypeName)"
:src="getFileUrl(field.value)"
@click="previewImage(field.value)"
class="inspection-thumbnail"
alt="巡检图片"
/>
</span>
<a
v-else-if="field.type === 7"
:href="getFileUrl(field.value)"
target="_blank"
class="field-value-file"
> >
📎 {{ field.value.split('/').pop() }} <div class="summary-group-title">
</a> <el-icon class="summary-group-toggle-icon" :class="{ 'is-expanded': expandedConfigTypes[`${item.id}-${configTypeName}`] }">
<span v-else class="field-value-text">{{ field.value }}</span> <ArrowDown />
</el-icon>
<span>{{ configTypeName }}</span>
</div>
</div>
<el-collapse-transition>
<div v-show="expandedConfigTypes[`${item.id}-${configTypeName}`] || false" class="summary-group-content">
<div
v-for="field in groupData"
:key="`${configTypeName}-${field.name}`"
class="summary-field"
>
<div class="field-question">
<span class="field-question-label">题目</span>
<span class="field-question-text">{{ field.name }}</span>
</div>
<div class="field-answer">
<span class="field-answer-label">答案</span>
<span v-if="field.type === 6" class="field-value-image">
<img
:src="getFileUrl(field.value)"
@click="previewImage(field.value)"
class="inspection-thumbnail"
alt="巡检图片"
/>
</span>
<a
v-else-if="field.type === 7"
:href="getFileUrl(field.value)"
target="_blank"
class="field-value-file"
>
📎 {{ field.value.split('/').pop() }}
</a>
<span v-else class="field-value-text">{{ field.value }}</span>
</div>
</div>
</div>
</el-collapse-transition>
</div> </div>
</div> </template>
</template> <div v-else class="summary-text">{{ item.summary }}</div>
<div v-else class="summary-text">{{ item.summary }}</div>
</div> </div>
</div> </div>
<div v-if="item.status || item.overallStatus" class="card-status"> <div v-if="item.status || item.overallStatus" class="card-status">
@ -249,6 +271,28 @@
{{ getUserName(scope.row.inspector) }} {{ getUserName(scope.row.inspector) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="配置类型" align="center" prop="configType" width="150">
<template #default="scope">
<el-tooltip
v-if="getRecordConfigTypes(scope.row.configType).length > 0"
:content="getRecordConfigTypes(scope.row.configType).join('、')"
placement="top"
>
<div class="table-config-types">
<el-tag
v-for="configType in getRecordConfigTypes(scope.row.configType)"
:key="configType"
size="small"
type="info"
class="config-type-tag"
>
{{ configType }}
</el-tag>
</div>
</el-tooltip>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status"> <el-table-column label="状态" align="center" prop="status">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_PROJECT_INSPECTION_RECORD_STATUS" :value="scope.row.status" /> <dict-tag :type="DICT_TYPE.CRM_PROJECT_INSPECTION_RECORD_STATUS" :value="scope.row.status" />
@ -317,6 +361,28 @@
{{ getUserName(scope.row.inspector) }} {{ getUserName(scope.row.inspector) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="配置类型" align="center" prop="configType" width="150">
<template #default="scope">
<el-tooltip
v-if="getRecordConfigTypes(scope.row.configType).length > 0"
:content="getRecordConfigTypes(scope.row.configType).join('、')"
placement="top"
>
<div class="table-config-types">
<el-tag
v-for="configType in getRecordConfigTypes(scope.row.configType)"
:key="configType"
size="small"
type="info"
class="config-type-tag"
>
{{ configType }}
</el-tag>
</div>
</el-tooltip>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status"> <el-table-column label="状态" align="center" prop="status">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_PROJECT_INSPECTION_RECORD_STATUS" :value="scope.row.status" /> <dict-tag :type="DICT_TYPE.CRM_PROJECT_INSPECTION_RECORD_STATUS" :value="scope.row.status" />
@ -384,17 +450,39 @@
{{ getUserName(scope.row.inspector) }} {{ getUserName(scope.row.inspector) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="配置类型" align="center" prop="configType" width="150">
<template #default="scope">
<el-tooltip
v-if="getRecordConfigTypes(scope.row.configType).length > 0"
:content="getRecordConfigTypes(scope.row.configType).join('、')"
placement="top"
>
<div class="table-config-types">
<el-tag
v-for="configType in getRecordConfigTypes(scope.row.configType)"
:key="configType"
size="small"
type="info"
class="config-type-tag"
>
{{ configType }}
</el-tag>
</div>
</el-tooltip>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<!-- <el-table-column label="备注" align="center" prop="remark" /> --> <!-- <el-table-column label="备注" align="center" prop="remark" /> -->
<el-table-column label="状态" align="center" prop="status"> <el-table-column label="状态" align="center" prop="status">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_PROJECT_INSPECTION_RECORD_STATUS" :value="scope.row.status" /> <dict-tag :type="DICT_TYPE.CRM_PROJECT_INSPECTION_RECORD_STATUS" :value="scope.row.status" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="结论" align="center" prop="overallStatus"> <!-- <el-table-column label="结论" align="center" prop="overallStatus">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_PROJECT_INSPECTION_RECORD_OVERALL_STATUS" :value="scope.row.overallStatus" /> <dict-tag :type="DICT_TYPE.CRM_PROJECT_INSPECTION_RECORD_OVERALL_STATUS" :value="scope.row.overallStatus" />
</template> </template>
</el-table-column> </el-table-column> -->
<el-table-column label="操作" align="center" min-width="120px"> <el-table-column label="操作" align="center" min-width="120px">
<template #default="scope"> <template #default="scope">
<el-button <el-button
@ -442,7 +530,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, getDictLabel, DICT_TYPE } from '@/utils/dict'
import { dateFormatter, formatDate } from '@/utils/formatTime' import { dateFormatter, formatDate } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
import * as ProjectInspectionRecordApi from '@/api/crm/project/inspection/record' import * as ProjectInspectionRecordApi from '@/api/crm/project/inspection/record'
@ -451,6 +539,7 @@ import * as UserApi from '@/api/system/user'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import ProjectInspectionRecordForm from './ProjectInspectionRecordForm.vue' import ProjectInspectionRecordForm from './ProjectInspectionRecordForm.vue'
import ProjectInspectionRecordUploadForm from './ProjectInspectionRecordUploadForm.vue' import ProjectInspectionRecordUploadForm from './ProjectInspectionRecordUploadForm.vue'
import { ArrowDown } from '@element-plus/icons-vue'
/** 巡检记录 列表 */ /** 巡检记录 列表 */
defineOptions({ name: 'CrmProjectInspectionRecord' }) defineOptions({ name: 'CrmProjectInspectionRecord' })
@ -476,6 +565,7 @@ const userList = ref<UserApi.UserVO[]>([]) // 用户列表
const userStore = useUserStore() // store const userStore = useUserStore() // store
const uploadFormRef = ref() // Ref const uploadFormRef = ref() // Ref
const activeTab = ref('timeline') // const activeTab = ref('timeline') //
const expandedConfigTypes = ref<Record<string, boolean>>({}) //
/** 格式化日期显示 */ /** 格式化日期显示 */
@ -774,6 +864,30 @@ const getCardStatusClass = (overallStatus: number | undefined) => {
return '' return ''
} }
/** 切换配置类型展开/折叠 */
const toggleConfigType = (recordId: number | undefined, configTypeName: string) => {
if (recordId === undefined) return
const key = `${recordId}-${configTypeName}`
expandedConfigTypes.value[key] = !expandedConfigTypes.value[key]
}
/** 获取记录的配置类型列表根据configType查询字典 */
const getRecordConfigTypes = (configType: number[] | undefined): string[] => {
if (!configType || !Array.isArray(configType) || configType.length === 0) return []
return configType
.map(type => getDictLabel(DICT_TYPE.CRM_PROJECT_INSPECTION_CHECKPOINT, type))
.filter(label => label) //
}
/** 从summary中获取配置类型名称列表用于折叠功能 */
const getConfigTypeNamesFromSummary = (summary: string | undefined): string[] => {
if (!summary) return []
const parsed = getParsedSummary(summary)
if (!parsed) return []
return Object.keys(parsed)
}
/** 初始化 **/ /** 初始化 **/
onMounted(async () => { onMounted(async () => {
// //
@ -782,6 +896,23 @@ onMounted(async () => {
// //
await getList() await getList()
}) })
//
watch(list, (newList) => {
newList.forEach((item) => {
// summary
if (item.id !== undefined && item.summary) {
const configTypeNames = getConfigTypeNamesFromSummary(item.summary)
configTypeNames.forEach((configTypeName) => {
const key = `${item.id}-${configTypeName}`
if (expandedConfigTypes.value[key] === undefined) {
expandedConfigTypes.value[key] = false
}
})
}
})
}, { immediate: true })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -835,6 +966,20 @@ onMounted(async () => {
color: #303133; color: #303133;
} }
.card-config-types {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
align-items: center;
.config-type-tag {
margin: 0;
white-space: nowrap;
}
}
.card-tags { .card-tags {
display: flex; display: flex;
gap: 8px; gap: 8px;
@ -875,10 +1020,20 @@ onMounted(async () => {
margin-top: 12px; margin-top: 12px;
padding-top: 12px; padding-top: 12px;
border-top: 1px solid #ebeef5; border-top: 1px solid #ebeef5;
display: flex;
align-items: center; .summary-header-row {
flex-wrap: wrap; display: flex;
gap: 4px; align-items: center;
margin-bottom: 12px;
.summary-label {
color: #909399;
margin-right: 8px;
font-weight: 500;
flex-shrink: 0;
min-width: 80px;
}
}
.summary-label, .summary-label,
.status-label, .status-label,
@ -889,7 +1044,7 @@ onMounted(async () => {
} }
.summary-content-wrapper { .summary-content-wrapper {
margin-top: 8px; width: 100%;
max-width: 100%; max-width: 100%;
.summary-group { .summary-group {
@ -899,19 +1054,116 @@ onMounted(async () => {
margin-bottom: 0; margin-bottom: 0;
} }
.summary-group-title { .summary-group-header {
font-size: 14px; cursor: pointer;
font-weight: 600; user-select: none;
color: #0f70d1; margin-bottom: 12px;
margin-bottom: 8px; padding: 12px 16px;
padding-bottom: 6px; background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border-bottom: 1px solid #e4e7ed; border-radius: 8px;
border-left: 4px solid #409EFF;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
width: 100%;
max-width: 100%;
box-sizing: border-box;
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.08);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(64, 158, 255, 0.05) 0%, rgba(37, 99, 235, 0.05) 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
&:hover {
background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
transform: translateY(-1px);
border-left-color: #2563eb;
&::before {
opacity: 1;
}
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.15);
}
.summary-group-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 15px;
font-weight: 600;
color: #0f70d1;
width: 100%;
position: relative;
z-index: 1;
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
span {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
letter-spacing: 0.3px;
}
.summary-group-toggle-icon {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
color: #409EFF;
flex-shrink: 0;
font-size: 16px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
&.is-expanded {
transform: rotate(180deg);
color: #2563eb;
}
}
}
}
.summary-group-content {
margin-top: 12px;
padding-left: 12px;
border-left: 2px dashed #e0f2fe;
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.summary-field { .summary-field {
display: flex; margin-bottom: 12px;
align-items: flex-start; padding: 12px;
margin-bottom: 8px; background-color: #f8f9fa;
border-radius: 6px;
border-left: 3px solid #409EFF;
font-size: 13px; font-size: 13px;
line-height: 1.6; line-height: 1.6;
@ -919,53 +1171,83 @@ onMounted(async () => {
margin-bottom: 0; margin-bottom: 0;
} }
.field-name { .field-question {
font-weight: 500; margin-bottom: 8px;
color: #606266; padding-bottom: 8px;
margin-right: 8px; border-bottom: 1px dashed #dcdfe6;
flex-shrink: 0;
min-width: 80px;
}
.field-value-text {
color: #303133;
word-break: break-word;
flex: 1;
}
.field-value-image {
display: inline-block;
.inspection-thumbnail { .field-question-label {
max-width: 120px; font-weight: 600;
max-height: 120px; color: #409EFF;
margin-right: 6px;
font-size: 13px;
}
.field-question-text {
font-weight: 500;
color: #303133;
word-break: break-word;
}
}
.field-answer {
display: flex;
align-items: flex-start;
.field-answer-label {
font-weight: 600;
color: #67C23A;
margin-right: 6px;
flex-shrink: 0;
font-size: 13px;
}
.field-value-text {
color: #606266;
word-break: break-word;
flex: 1;
background-color: #fff;
padding: 6px 10px;
border-radius: 4px; border-radius: 4px;
border: 1px solid #e4e7ed; border: 1px solid #e4e7ed;
padding: 2px;
cursor: pointer;
transition: all 0.3s;
&:hover {
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
} }
} }
.field-value-file { .field-value-image {
color: #409EFF; display: inline-block;
text-decoration: none;
padding: 2px 8px; .inspection-thumbnail {
border: 1px solid #409EFF; max-width: 120px;
border-radius: 4px; max-height: 120px;
display: inline-block; border-radius: 4px;
transition: all 0.3s; border: 1px solid #e4e7ed;
padding: 2px;
&:hover { cursor: pointer;
background-color: #409EFF; transition: all 0.3s;
color: #fff; background-color: #fff;
&:hover {
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
}
}
.field-value-file {
color: #409EFF;
text-decoration: none;
padding: 6px 10px;
border: 1px solid #409EFF;
border-radius: 4px;
display: inline-block;
transition: all 0.3s;
background-color: #fff;
&:hover {
background-color: #409EFF;
color: #fff;
}
} }
}
} }
} }
@ -992,6 +1274,34 @@ onMounted(async () => {
} }
} }
//
.table-config-types {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 4px;
justify-content: flex-start;
align-items: center;
width: 100%;
.config-type-tag {
margin: 0;
white-space: nowrap;
flex-shrink: 0;
}
.config-type-more {
cursor: pointer;
background-color: #909399;
border-color: #909399;
&:hover {
background-color: #73767a;
border-color: #73767a;
}
}
}
:deep(.el-timeline) { :deep(.el-timeline) {
padding-left: 0; padding-left: 0;