add 巡检定时任务

pull/853/head
wersd 2025-11-22 23:03:52 +08:00
parent 441c6c59e0
commit 834bb4f53b
11 changed files with 897 additions and 163 deletions

10
package-lock.json generated
View File

@ -24,6 +24,7 @@
"benz-amr-recorder": "^1.1.5", "benz-amr-recorder": "^1.1.5",
"bpmn-js-token-simulation": "^0.36.0", "bpmn-js-token-simulation": "^0.36.0",
"camunda-bpmn-moddle": "^7.0.1", "camunda-bpmn-moddle": "^7.0.1",
"cronstrue": "^3.9.0",
"cropperjs": "^1.6.1", "cropperjs": "^1.6.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
@ -8440,6 +8441,15 @@
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
"node_modules/cronstrue": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-3.9.0.tgz",
"integrity": "sha512-T3S35zmD0Ai2B4ko6+mEM+k9C6tipe2nB9RLiGT6QL2Wn0Vsn2cCZAC8Oeuf4CaE00GZWVdpYitbpWCNlIWqdA==",
"license": "MIT",
"bin": {
"cronstrue": "bin/cli.js"
}
},
"node_modules/cropperjs": { "node_modules/cropperjs": {
"version": "1.6.2", "version": "1.6.2",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/cropperjs/-/cropperjs-1.6.2.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/cropperjs/-/cropperjs-1.6.2.tgz",

View File

@ -41,6 +41,7 @@
"benz-amr-recorder": "^1.1.5", "benz-amr-recorder": "^1.1.5",
"bpmn-js-token-simulation": "^0.36.0", "bpmn-js-token-simulation": "^0.36.0",
"camunda-bpmn-moddle": "^7.0.1", "camunda-bpmn-moddle": "^7.0.1",
"cronstrue": "^3.9.0",
"cropperjs": "^1.6.1", "cropperjs": "^1.6.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",

View File

@ -4,12 +4,14 @@ import request from '@/config/axios'
export interface ProjectInspectionRecordVO { export interface ProjectInspectionRecordVO {
id: number // 主键ID id: number // 主键ID
projectId: number // 项目ID projectId: number // 项目ID
inspectionDate: Date // 巡检日期 inspectionDate: Date // 巡检日期时间
inspector: number // 巡检人 inspector: number // 巡检人
overallStatus: number // 总体状态 overallStatus: number // 总体状态
summary: string // 巡检总结 summary: string // 巡检总结
remark: string // 备注 remark: string // 备注
status: number // 状态 status: number // 状态
configType: number[] // 配置类型列表
updateTime: Date // 更新时间
} }
// 查询巡检记录分页 // 查询巡检记录分页
@ -17,6 +19,11 @@ export const getProjectInspectionRecordPage = async (params: any) => {
return await request.get({ url: `/crm/project-inspection-record/page`, params }) return await request.get({ url: `/crm/project-inspection-record/page`, params })
} }
// 根据更新时间查询巡检记录分页
export const getProjectInspectionRecordPageByUpdateTime = async (params: any) => {
return await request.get({ url: `/crm/project-inspection-record/page-by-update-time`, params })
}
// 查询巡检记录详情 // 查询巡检记录详情
export const getProjectInspectionRecord = async (id: number) => { export const getProjectInspectionRecord = async (id: number) => {
return await request.get({ url: `/crm/project-inspection-record/get?id=` + id }) return await request.get({ url: `/crm/project-inspection-record/get?id=` + id })

View File

@ -512,12 +512,7 @@ const inputChange = () => {
<el-input v-model="defaultValue" class="input-with-select" v-bind="$attrs" @input="inputChange"> <el-input v-model="defaultValue" class="input-with-select" v-bind="$attrs" @input="inputChange">
<template #append> <template #append>
<el-select v-model="select" placeholder="生成器" style="width: 115px"> <el-select v-model="select" placeholder="生成器" style="width: 115px">
<el-option label="每分钟" value="0 * * * * ?" /> <el-option label="每天" value="0 0 0 * * ?" />
<el-option label="每小时" value="0 0 * * * ?" />
<el-option label="每天零点" value="0 0 0 * * ?" />
<el-option label="每月一号零点" value="0 0 0 1 * ?" />
<el-option label="每月最后一天零点" value="0 0 0 L * ?" />
<el-option label="每周星期日零点" value="0 0 0 ? * 1" />
<el-option <el-option
v-for="(item, index) in shortcuts" v-for="(item, index) in shortcuts"
:key="index" :key="index"

21
src/utils/cron.ts Normal file
View File

@ -0,0 +1,21 @@
/**
* Cron
* 使 cronstrue
*/
import cronstrue from 'cronstrue/i18n'
export function cronToChinese(cron: string): string {
if (!cron || typeof cron !== "string") {
return "无效的 Cron 表达式";
}
try {
// 使用 cronstrue 库转换为中文
return cronstrue.toString(cron.trim(), {
locale: 'zh_CN', // 设置语言为简体中文
use24HourTimeFormat: true // 使用24小时制
});
} catch (error) {
return "Cron 表达式格式错误";
}
}

View File

@ -50,6 +50,7 @@
v-model="formData.frequency" v-model="formData.frequency"
placeholder="请输入或选择Cron表达式" placeholder="请输入或选择Cron表达式"
style="width: 100%" style="width: 100%"
:shortcuts="cronShortcuts"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -178,6 +179,14 @@ const formRules = reactive({
}) })
const formRef = ref() // Ref const formRef = ref() // Ref
/** Cron 表达式快捷选项 */
const cronShortcuts = [
{ text: '每周一', value: '0 0 0 ? * 2' },
{ text: '每周六', value: '0 0 0 ? * 7' },
{ text: '每月', value: '0 0 0 1 * ?' },
{ text: '每季度', value: '0 0 0 1 1/3 ?' }
]
/** 打开弹窗 */ /** 打开弹窗 */
const open = async (type: string, id?: number) => { const open = async (type: string, id?: number) => {
dialogVisible.value = true dialogVisible.value = true

View File

@ -25,6 +25,22 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="配置类型" prop="configType">
<el-select
v-model="queryParams.configType"
placeholder="请选择配置类型"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_PROJECT_INSPECTION_CHECKPOINT)"
:key="String(dict.value)"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="负责人" prop="responsiblePersonId"> <el-form-item label="负责人" prop="responsiblePersonId">
<el-select <el-select
v-model="queryParams.responsiblePersonId" v-model="queryParams.responsiblePersonId"
@ -94,9 +110,9 @@
<dict-tag :type="DICT_TYPE.CRM_PROJECT_INSPECTION_CHECKPOINT" :value="scope.row.configType" /> <dict-tag :type="DICT_TYPE.CRM_PROJECT_INSPECTION_CHECKPOINT" :value="scope.row.configType" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="巡检频率" align="center" prop="frequency" min-width="150"> <el-table-column label="巡检频率" align="center" prop="frequency" min-width="350">
<template #default="{ row }"> <template #default="{ row }">
{{ row.frequency || '-' }} {{ row.frequency ? cronToChinese(row.frequency) : '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="管理方" align="center" prop="managementParty" min-width="120"> <el-table-column label="管理方" align="center" prop="managementParty" min-width="120">
@ -161,6 +177,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import download from '@/utils/download' import download from '@/utils/download'
import { cronToChinese } from '@/utils/cron'
import * as ProjectInspectionConfigApi from '@/api/crm/project/inspection' import * as ProjectInspectionConfigApi from '@/api/crm/project/inspection'
import * as ProjectApi from '@/api/crm/project' import * as ProjectApi from '@/api/crm/project'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
@ -180,6 +197,7 @@ const queryParams = reactive({
pageSize: 10, pageSize: 10,
projectId: undefined, projectId: undefined,
responsiblePersonId: undefined, responsiblePersonId: undefined,
configType: undefined,
status: undefined, status: undefined,
}) })
const queryFormRef = ref() // const queryFormRef = ref() //

View File

@ -16,7 +16,7 @@
class="!w-240px" class="!w-240px"
> >
<el-option <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_PROJECT_INSPECTION_DATA_TYPE)" v-for="dict in getIntDictOptions(DICT_TYPE.CRM_PROJECT_INSPECTION_CHECKPOINT)"
:key="dict.value" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"

View File

@ -30,10 +30,11 @@
<el-form-item label="巡检日期" prop="inspectionDate"> <el-form-item label="巡检日期" prop="inspectionDate">
<el-date-picker <el-date-picker
v-model="formData.inspectionDate" v-model="formData.inspectionDate"
type="date" type="datetime"
value-format="YYYY-MM-DD" value-format="x"
placeholder="选择巡检日期" placeholder="选择巡检日期时间"
class="!w-full" class="!w-full"
editable
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -71,7 +72,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20"> <!-- <el-row :gutter="20">
<el-col :span="24"> <el-col :span="24">
<el-form-item label="结论" prop="overallStatus"> <el-form-item label="结论" prop="overallStatus">
<el-radio-group v-model="formData.overallStatus"> <el-radio-group v-model="formData.overallStatus">
@ -85,20 +86,20 @@
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row> -->
<el-row :gutter="20"> <!-- <el-row :gutter="20">
<el-col :span="24"> <el-col :span="24">
<el-form-item label="巡检总结" prop="summary"> <el-form-item label="巡检内容" prop="summary">
<el-input <el-input
v-model="formData.summary" v-model="formData.summary"
type="textarea" type="textarea"
:rows="3" :rows="3"
placeholder="请输入巡检总结" placeholder="请输入巡检内容"
resize="both" resize="both"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row> -->
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="24"> <el-col :span="24">
<el-form-item label="备注" prop="remark"> <el-form-item label="备注" prop="remark">
@ -121,6 +122,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
import * as ProjectInspectionRecordApi from '@/api/crm/project/inspection/record' import * as ProjectInspectionRecordApi from '@/api/crm/project/inspection/record'
import * as ProjectApi from '@/api/crm/project' import * as ProjectApi from '@/api/crm/project'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
@ -168,7 +170,8 @@ const open = async (type: string, id?: number) => {
if (id) { if (id) {
formLoading.value = true formLoading.value = true
try { try {
formData.value = await ProjectInspectionRecordApi.getProjectInspectionRecord(id) const data = await ProjectInspectionRecordApi.getProjectInspectionRecord(id)
formData.value = data
} finally { } finally {
formLoading.value = false formLoading.value = false
} }
@ -184,7 +187,13 @@ const submitForm = async () => {
// //
formLoading.value = true formLoading.value = true
try { try {
const data = formData.value as unknown as ProjectInspectionRecordApi.ProjectInspectionRecordVO // status overallStatus
const data = {
...formData.value,
status: formData.value.status,
overallStatus: formData.value.overallStatus
} as unknown as ProjectInspectionRecordApi.ProjectInspectionRecordVO
if (formType.value === 'create') { if (formType.value === 'create') {
await ProjectInspectionRecordApi.createProjectInspectionRecord(data) await ProjectInspectionRecordApi.createProjectInspectionRecord(data)
message.success(t('common.createSuccess')) message.success(t('common.createSuccess'))

View File

@ -104,6 +104,22 @@
</el-form-item> </el-form-item>
</template> </template>
</template> </template>
<!-- 巡检结论 -->
<el-divider>
<span style="font-weight: bold; font-size: 16px;">巡检结论</span>
</el-divider>
<el-form-item label="巡检结论" prop="overallStatus" :rules="[{ required: true, message: '请选择巡检结论', trigger: 'change' }]">
<el-radio-group v-model="formData.overallStatus">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_PROJECT_INSPECTION_RECORD_OVERALL_STATUS)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button> <el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
@ -120,6 +136,7 @@ import * as UserApi from '@/api/system/user'
import UploadImg from '@/components/UploadFile/src/UploadImg.vue' import UploadImg from '@/components/UploadFile/src/UploadImg.vue'
import UploadFile from '@/components/UploadFile/src/UploadFile.vue' import UploadFile from '@/components/UploadFile/src/UploadFile.vue'
import { getDictLabel, getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getDictLabel, getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
/** 巡检记录填报 表单 */ /** 巡检记录填报 表单 */
@ -241,14 +258,18 @@ const getFieldRules = (item: ProjectInspectionDataApi.ProjectInspectionDataVO) =
} }
/** 打开弹窗 */ /** 打开弹窗 */
const open = async ( const open = async (record: ProjectInspectionRecordApi.ProjectInspectionRecordVO) => {
record: ProjectInspectionRecordApi.ProjectInspectionRecordVO,
dataList: ProjectInspectionDataApi.ProjectInspectionDataVO[]
) => {
dialogVisible.value = true dialogVisible.value = true
formLoading.value = true formLoading.value = true
try { try {
// configType
if (!record.configType || !Array.isArray(record.configType) || record.configType.length === 0) {
message.warning('该巡检记录没有配置巡检类型')
dialogVisible.value = false
return
}
// //
projectList.value = await ProjectApi.getProjectSimpleList() projectList.value = await ProjectApi.getProjectSimpleList()
userList.value = await UserApi.getSimpleUserList() userList.value = await UserApi.getSimpleUserList()
@ -262,14 +283,23 @@ const open = async (
// //
inspectorName.value = userStore.user.nickname || '当前用户' inspectorName.value = userStore.user.nickname || '当前用户'
// //
if (record.inspectionDate) { if (record.inspectionDate) {
const date = new Date(record.inspectionDate) const date = new Date(record.inspectionDate)
inspectionDate.value = date.toISOString().split('T')[0] inspectionDate.value = formatDate(date, 'YYYY-MM-DD HH:mm:ss')
}
// record.configType
const inspectionDataListResult = await ProjectInspectionDataApi.getProjectInspectionDataList(record.configType)
if (!inspectionDataListResult || inspectionDataListResult.length === 0) {
message.warning('没有找到对应的巡检内容')
dialogVisible.value = false
return
} }
// //
inspectionDataList.value = dataList || [] inspectionDataList.value = inspectionDataListResult
// //
formData.value = {} formData.value = {}
@ -287,6 +317,10 @@ const open = async (
// //
formRules.value[fieldKey] = getFieldRules(item) formRules.value[fieldKey] = getFieldRules(item)
}) })
} catch (error) {
console.error('打开填报表单失败:', error)
message.error('获取巡检内容失败,请稍后重试')
dialogVisible.value = false
} finally { } finally {
formLoading.value = false formLoading.value = false
} }
@ -308,12 +342,17 @@ const submitForm = async () => {
return return
} }
// // 使ID
const summaryData: Record<number, Record<number, any>> = {} const summaryData: Record<number, Record<number, any>> = {}
// 使
const readableSummaryData: Record<string, Array<{ name: string; value: any; type: number }>> = {}
// //
groupedInspectionData.value.forEach((group) => { groupedInspectionData.value.forEach((group) => {
const groupData: Record<number, any> = {} const groupData: Record<number, any> = {}
const readableGroupData: Array<{ name: string; value: any; type: number }> = []
const configTypeName = getConfigTypeName(group.configType)
group.items.forEach((item) => { group.items.forEach((item) => {
const fieldKey = `data_${item.id}` const fieldKey = `data_${item.id}`
@ -324,30 +363,61 @@ const submitForm = async () => {
// //
if (value.length > 0) { if (value.length > 0) {
groupData[item.id] = value.join(',') groupData[item.id] = value.join(',')
readableGroupData.push({
name: item.name,
value: value.join('、'), // 使
type: item.type
})
} }
} else if (value !== undefined && value !== null && value !== '') { } else if (value !== undefined && value !== null && value !== '') {
// //
groupData[item.id] = value groupData[item.id] = value
readableGroupData.push({
name: item.name,
value: value,
type: item.type
})
} }
}) })
// summaryData //
if (Object.keys(groupData).length > 0) { if (Object.keys(groupData).length > 0) {
summaryData[group.configType] = groupData summaryData[group.configType] = groupData
readableSummaryData[configTypeName] = readableGroupData
} }
}) })
// JSON便
const summaryJson = JSON.stringify(readableSummaryData)
//
const currentDate = new Date()
currentDate.setHours(0, 0, 0, 0) //
let inspectionDateObj: Date | null = null
if (originalRecord.value.inspectionDate) {
inspectionDateObj = new Date(originalRecord.value.inspectionDate)
inspectionDateObj.setHours(0, 0, 0, 0)
}
// 32
let recordStatus = 2
if (inspectionDateObj && currentDate > inspectionDateObj) {
recordStatus = 3
}
// //
// status API boolean使 // status API boolean使
// summary
const updateData: any = { const updateData: any = {
id: recordId.value, id: recordId.value,
projectId: originalRecord.value.projectId, projectId: originalRecord.value.projectId,
inspectionDate: originalRecord.value.inspectionDate, inspectionDate: originalRecord.value.inspectionDate,
inspector: originalRecord.value.inspector, inspector: originalRecord.value.inspector,
overallStatus: originalRecord.value.overallStatus, overallStatus: formData.value.overallStatus, // 使
summary: JSON.stringify(summaryData), // JSON summary summary: summaryJson || JSON.stringify(summaryData), // 使JSON
remark: originalRecord.value.remark || '', remark: originalRecord.value.remark || '',
status: 2 // 2 status: recordStatus // recordStatusstatus
} }
// API // API

View File

@ -68,21 +68,6 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_PROJECT_INSPECTION_RECORD_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item> <el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
@ -107,8 +92,145 @@
</el-form> </el-form>
</ContentWrap> </ContentWrap>
<!-- 列表 --> <!-- 标签页 -->
<ContentWrap> <ContentWrap>
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<!-- 时间轴页面 -->
<el-tab-pane label="时间轴" name="timeline">
<div v-loading="loading" class="timeline-container">
<el-timeline v-if="list.length > 0">
<el-timeline-item
v-for="item in list"
:key="item.id"
:timestamp="formatDateForTimeline(item.updateTime)"
placement="top"
:color="getTimelineColor(item.status)"
>
<el-card
:class="['timeline-card', getCardStatusClass(item.overallStatus)]"
shadow="hover"
>
<div class="card-header">
<div class="card-title">
<span class="project-name">{{ getProjectName(item.projectId) }}</span>
</div>
</div>
<div class="card-content">
<div class="card-info">
<div class="info-item">
<span class="info-label">巡检人</span>
<span class="info-value">{{ getUserName(item.inspector) }}</span>
</div>
<div v-if="item.inspectionDate" class="info-item">
<span class="info-label">计划完成时间</span>
<span class="info-value">{{ formatDateForDisplay(item.inspectionDate) }}</span>
</div>
<div class="card-actions">
<!-- <el-button
v-if="canShowUploadButton(item)"
link
type="success"
size="small"
@click="handleUpload(item)"
>
填报
</el-button> -->
<el-button
link
type="primary"
size="small"
@click="openForm('update', item.id)"
v-hasPermi="['crm:project-inspection-record:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
size="small"
@click="handleDelete(item.id)"
v-hasPermi="['crm:project-inspection-record:delete']"
>
删除
</el-button>
</div>
</div>
<div v-if="item.summary" class="card-summary">
<span class="summary-label">巡检内容</span>
<div class="summary-content-wrapper">
<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
v-for="field in groupData"
:key="`${configTypeName}-${field.name}`"
class="summary-field"
>
<span class="field-name">{{ field.name }}</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>
</template>
<div v-else class="summary-text">{{ item.summary }}</div>
</div>
</div>
<div v-if="item.status || item.overallStatus" class="card-status">
<span class="status-label">状态</span>
<dict-tag
v-if="item.status"
:type="DICT_TYPE.CRM_PROJECT_INSPECTION_RECORD_STATUS"
:value="item.status"
/>
<span v-if="item.overallStatus" class="overall-status-label" style="margin-left: 16px;"></span>
<dict-tag
v-if="item.overallStatus"
:type="DICT_TYPE.CRM_PROJECT_INSPECTION_RECORD_OVERALL_STATUS"
:value="item.overallStatus"
/>
</div>
<div v-if="item.remark" class="card-remark">
<span class="remark-label">备注</span>
<span class="remark-content">{{ item.remark }}</span>
</div>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无数据" />
<!-- 分页 -->
<div class="pagination-wrapper">
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</div>
</div>
</el-tab-pane>
<!-- 已巡检表格页面 -->
<el-tab-pane label="已巡检" name="table">
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="项目名称" align="center" prop="projectId"> <el-table-column label="项目名称" align="center" prop="projectId">
<template #default="scope"> <template #default="scope">
@ -119,8 +241,143 @@
label="巡检日期" label="巡检日期"
align="center" align="center"
prop="inspectionDate" prop="inspectionDate"
:formatter="dateFormatter2" :formatter="dateFormatter"
width="120" width="180"
/>
<el-table-column label="巡检人" align="center" prop="inspector">
<template #default="scope">
{{ getUserName(scope.row.inspector) }}
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_PROJECT_INSPECTION_RECORD_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="结论" align="center" prop="overallStatus">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_PROJECT_INSPECTION_RECORD_OVERALL_STATUS" :value="scope.row.overallStatus" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
v-if="canShowUploadButton(scope.row)"
link
type="success"
@click="handleUpload(scope.row)"
>
填报
</el-button>
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['crm:project-inspection-record:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['crm:project-inspection-record:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-tab-pane>
<!-- 逾期巡检表格页面 -->
<el-tab-pane label="逾期巡检" name="tableOverdue">
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="项目名称" align="center" prop="projectId">
<template #default="scope">
{{ getProjectName(scope.row.projectId) }}
</template>
</el-table-column>
<el-table-column
label="巡检日期"
align="center"
prop="inspectionDate"
:formatter="dateFormatter"
width="180"
/>
<el-table-column label="巡检人" align="center" prop="inspector">
<template #default="scope">
{{ getUserName(scope.row.inspector) }}
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_PROJECT_INSPECTION_RECORD_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="结论" align="center" prop="overallStatus">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_PROJECT_INSPECTION_RECORD_OVERALL_STATUS" :value="scope.row.overallStatus" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
v-if="canShowUploadButton(scope.row)"
link
type="success"
@click="handleUpload(scope.row)"
>
填报
</el-button>
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['crm:project-inspection-record:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['crm:project-inspection-record:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-tab-pane>
<!-- 未巡检表格页面 -->
<el-tab-pane label="未巡检" name="tableUninspected">
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="项目名称" align="center" prop="projectId">
<template #default="scope">
{{ getProjectName(scope.row.projectId) }}
</template>
</el-table-column>
<el-table-column
label="巡检日期"
align="center"
prop="inspectionDate"
:formatter="dateFormatter"
width="180"
/> />
<el-table-column label="巡检人" align="center" prop="inspector"> <el-table-column label="巡检人" align="center" prop="inspector">
<template #default="scope"> <template #default="scope">
@ -174,6 +431,8 @@
v-model:limit="queryParams.pageSize" v-model:limit="queryParams.pageSize"
@pagination="getList" @pagination="getList"
/> />
</el-tab-pane>
</el-tabs>
</ContentWrap> </ContentWrap>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
@ -184,11 +443,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter2 } 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'
import * as ProjectInspectionConfigApi from '@/api/crm/project/inspection'
import * as ProjectInspectionDataApi from '@/api/crm/project/inspection/data'
import * as ProjectApi from '@/api/crm/project' import * as ProjectApi from '@/api/crm/project'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
@ -211,7 +468,6 @@ const queryParams = reactive({
inspectionDate: [], inspectionDate: [],
inspector: undefined, inspector: undefined,
overallStatus: undefined, overallStatus: undefined,
status: undefined,
}) })
const queryFormRef = ref() // const queryFormRef = ref() //
const exportLoading = ref(false) // const exportLoading = ref(false) //
@ -219,14 +475,99 @@ const projectList = ref<ProjectApi.ProjectVO[]>([]) // 项目列表
const userList = ref<UserApi.UserVO[]>([]) // 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 formatDateForTimeline = (date: string | Date | undefined) => {
if (!date) return ''
return formatDate(new Date(date), 'YYYY-MM-DD HH:mm:ss')
}
/** 格式化日期显示(用于预计巡检日期等) */
const formatDateForDisplay = (date: string | Date | undefined) => {
if (!date) return ''
return formatDate(new Date(date), 'YYYY-MM-DD HH:mm:ss')
}
/** 根据状态获取时间线颜色 */
const getTimelineColor = (status: number | undefined) => {
if (status === undefined) return '#409EFF'
//
//
switch (status) {
case 0:
return '#909399' // -
case 1:
return '#E6A23C' // -
case 2:
return '#67C23A' // 绿 -
default:
return '#409EFF' // -
}
}
/** 标签页切换处理 */
const handleTabChange = () => {
//
queryParams.pageNo = 1
getList()
}
/** 查询列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
const data = await ProjectInspectionRecordApi.getProjectInspectionRecordPage(queryParams) //
const params: any = { ...queryParams }
if (activeTab.value === 'timeline'){
// 使
params.status = [2, 3]
const data = await ProjectInspectionRecordApi.getProjectInspectionRecordPageByUpdateTime(params)
list.value = data.list list.value = data.list
total.value = data.total total.value = data.total
return
} else if (activeTab.value === 'table') {
// status = 2
params.status = 2
} else if (activeTab.value === 'tableOverdue') {
// status = 3
params.status = 3
} else if (activeTab.value === 'tableUninspected') {
// status != 2 status != 3
// statusNotIn
// [0, 1]
//
params.statusNotIn = [2, 3]
// params.status = [0, 1]
}
const data = await ProjectInspectionRecordApi.getProjectInspectionRecordPage(params)
// statusNotIn status 使
if (activeTab.value === 'tableUninspected') {
// statusNotIn
const filteredList = data.list.filter(item => item.status !== 2 && item.status !== 3)
list.value = filteredList
//
// 使 total
// total
if (filteredList.length === data.list.length) {
// 使 total
total.value = data.total
} else {
// statusNotIn
// total
// 使 total
// statusNotIn
total.value = Math.max(0, data.total - (data.list.length - filteredList.length))
}
} else {
list.value = data.list
total.value = data.total
}
} finally { } finally {
loading.value = false loading.value = false
} }
@ -319,7 +660,7 @@ const isAdmin = () => {
/** 判断是否可以看到填报按钮 */ /** 判断是否可以看到填报按钮 */
const canShowUploadButton = (row: ProjectInspectionRecordApi.ProjectInspectionRecordVO) => { const canShowUploadButton = (row: ProjectInspectionRecordApi.ProjectInspectionRecordVO) => {
// 1. status != 2 // 1. status != 2
if (row.status === 2) { if (row.status === 2 || row.status === 3) {
return false return false
} }
@ -342,51 +683,82 @@ const canShowUploadButton = (row: ProjectInspectionRecordApi.ProjectInspectionRe
/** 填报按钮操作 */ /** 填报按钮操作 */
const handleUpload = async (row: ProjectInspectionRecordApi.ProjectInspectionRecordVO) => { const handleUpload = async (row: ProjectInspectionRecordApi.ProjectInspectionRecordVO) => {
// row.configType
uploadFormRef.value.open(row)
}
/** 解析巡检内容数据 */
const parseSummaryData = (summary: string) => {
if (!summary) return null
try { try {
// IDIDconfigType //
const inspectorIdNum = typeof row.inspector === 'string' ? Number(row.inspector) : row.inspector let data: any = summary
const configParams = { if (typeof summary === 'string') {
projectId: row.projectId, // JSONJSON
responsiblePersonId: inspectorIdNum, try {
status: 0, // data = JSON.parse(summary)
pageNo: 1, //
pageSize: 99, // if (typeof data === 'string') {
data = JSON.parse(data)
}
} catch (e) {
// JSONnull
return null
} }
const configData = await ProjectInspectionConfigApi.getProjectInspectionConfigPage(configParams)
if (!configData.list || configData.list.length === 0) {
message.warning('该巡检人没有配置巡检项目')
return
} }
// configType // JSONtype
const configTypes = Array.from( if (typeof data === 'object' && !Array.isArray(data) && data !== null) {
new Set( const result: Record<string, Array<{ name: string; value: any; type: number }>> = {}
configData.list Object.keys(data).forEach((key) => {
.map((config: ProjectInspectionConfigApi.ProjectInspectionConfigVO) => config.configType) const value = data[key]
.filter((type: number | undefined) => type !== undefined) // type
) if (Array.isArray(value) && value.length > 0) {
) as number[] // type
const firstItem = value[0]
if (configTypes.length === 0) { if (typeof firstItem === 'object' && firstItem !== null && 'type' in firstItem && 'name' in firstItem) {
message.warning('该巡检人没有配置巡检类型') result[key] = value
return }
}
})
return Object.keys(result).length > 0 ? result : null
}
return null
} catch (e) {
console.error('解析巡检内容失败:', e, summary)
return null
}
} }
// configType /** 获取解析后的巡检内容(用于模板) */
const inspectionDataList = await ProjectInspectionDataApi.getProjectInspectionDataList(configTypes) const getParsedSummary = (summary: string) => {
return parseSummaryData(summary)
if (!inspectionDataList || inspectionDataList.length === 0) {
message.warning('没有找到对应的巡检内容')
return
} }
// /** 获取文件完整URL */
uploadFormRef.value.open(row, inspectionDataList) const getFileUrl = (url: string): string => {
} catch (error) { if (!url) return ''
console.error('获取巡检配置失败:', error) if (url.startsWith('http://') || url.startsWith('https://')) {
message.error('获取巡检配置失败,请稍后重试') return url
} }
const baseURL = import.meta.env.VITE_BASE_URL || ''
return baseURL ? `${baseURL}${url.startsWith('/') ? '' : '/'}${url}` : url
}
/** 预览图片 */
const previewImage = (imageUrl: string) => {
window.open(getFileUrl(imageUrl), '_blank')
}
/** 根据巡检结论获取卡片状态class */
const getCardStatusClass = (overallStatus: number | undefined) => {
if (overallStatus === 5) {
return 'card-status-danger' //
} else if (overallStatus === 4) {
return 'card-status-warning' //
}
return ''
} }
/** 初始化 **/ /** 初始化 **/
@ -398,3 +770,225 @@ onMounted(async () => {
await getList() await getList()
}) })
</script> </script>
<style lang="scss" scoped>
.timeline-container {
padding: 20px 0;
.timeline-card {
margin-bottom: 20px;
transition: all 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
// 5
&.card-status-danger {
background-color: #fef0f0;
border-left: 4px solid #f56c6c;
&:hover {
background-color: #fde2e2;
box-shadow: 0 4px 12px rgba(245, 108, 108, 0.2);
}
}
// 4
&.card-status-warning {
background-color: #fdf6ec;
border-left: 4px solid #e6a23c;
&:hover {
background-color: #faecd8;
box-shadow: 0 4px 12px rgba(230, 162, 60, 0.2);
}
}
.card-header {
margin-bottom: 12px;
.card-title {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
.project-name {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.card-tags {
display: flex;
gap: 8px;
}
}
}
.card-content {
.card-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
flex-wrap: wrap;
gap: 10px;
.info-item {
.info-label {
color: #909399;
margin-right: 4px;
}
.info-value {
color: #303133;
}
}
.card-actions {
display: flex;
gap: 8px;
}
}
.card-summary,
.card-status,
.card-overall-status,
.card-remark {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #ebeef5;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
.summary-label,
.status-label,
.overall-status-label,
.remark-label {
color: #909399;
margin-right: 4px;
}
.summary-content-wrapper {
margin-top: 8px;
max-width: 100%;
.summary-group {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
.summary-group-title {
font-size: 14px;
font-weight: 600;
color: #0f70d1;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid #e4e7ed;
}
.summary-field {
display: flex;
align-items: flex-start;
margin-bottom: 8px;
font-size: 13px;
line-height: 1.6;
&:last-child {
margin-bottom: 0;
}
.field-name {
font-weight: 500;
color: #606266;
margin-right: 8px;
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 {
max-width: 120px;
max-height: 120px;
border-radius: 4px;
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 {
color: #409EFF;
text-decoration: none;
padding: 2px 8px;
border: 1px solid #409EFF;
border-radius: 4px;
display: inline-block;
transition: all 0.3s;
&:hover {
background-color: #409EFF;
color: #fff;
}
}
}
}
.summary-text {
color: #606266;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
}
.remark-content {
color: #606266;
line-height: 1.6;
}
}
}
}
.pagination-wrapper {
margin-top: 20px;
display: flex;
justify-content: center;
}
}
:deep(.el-timeline) {
padding-left: 0;
.el-timeline-item {
padding-bottom: 20px;
.el-timeline-item__timestamp {
color: #909399;
font-size: 14px;
}
}
}
</style>