<!-- Description: h5-运维记录查询 Author: 李亚光 Date: 2025-01-02 --> <script lang="ts" setup name="H5OperationRecord"> import dayjs from 'dayjs' import { showToast } from 'vant' import { getDictByCode } from '@/api/system/dict' import { getDeviceTypeListPage } from '@/api/home/device/type' import { getDeptTreeList } from '@/api/system/dept' import useUserStore from '@/store/modules/user' import { exportOperationList, getOperationListPage } from '@/api/mobile/record' import { showLoadingToast, closeToast } from 'vant' import { exportFile } from '@/utils/exportUtils' import { keepSearchParams } from '@/utils/keepQuery' import { debounce } from 'lodash-es' const list = ref<any>([]) const total = ref(0) const loading = ref(true) const userInfo = useUserStore() const searchQuery = ref({ repairType: '', deviceType: '', userId: '', timeType: '', // 时间类型 1 近7日 2近30日 3 自定义时间 beginTime: '', endTime: '', deptid: '', offset: 1, limit: 10, }) const searchQueryForNames = ref({ repairTypeName: '运维类型', deviceTypeName: '设备类型', deptName: '', }) // 运维类型 const showRepairType = ref(false) const pickerRepairType = ref([]) // 选择运维类型 const selectRepairType = () => { showRepairType.value = true } // 确认运维类型 const onConfirmRepairType = ({ selectedValues, selectedOptions }: any) => { searchQuery.value.repairType = selectedOptions[0]?.value searchQueryForNames.value.repairTypeName = selectedOptions[0]?.text === '全部' ? '运维类型' : selectedOptions[0]?.text pickerRepairType.value = selectedValues showRepairType.value = false } // 管理单位 const showDept = ref(false) const loadingDept = ref(false) const allDeptList = ref<{ name: string; id: string; pid: string }[]>([]) // 管理单位 const groupDeptList = ref<{ text: string; value: string; pValue: string }[]>([]) // 集团 const unitDeptList = ref<{ text: string; value: string; pValue: string }[]>([]) // 单位 const divisionDeptList = ref<{ text: string; value: string; pValue: string }[]>([]) // 部门 const teamDeptList = ref<{ text: string; value: string; pValue: string }[]>([]) // 班组 const areaDeptList = ref<{ text: string; value: string; pValue: string }[]>([]) // 片区 // 选择管理单位 const selectDept = () => { showDept.value = true } const deptActiveTab = ref(0) const deptTabs = ref<string[]>(['集团']) // 选择集团 const pickerGroupDept = ref<string[]>([]) // 选中的集团 const pickerUnitDept = ref<string[]>([]) // 选中的单位 const pickerDivisionDept = ref<string[]>([]) // 选中的部门 const pickerTeamDept = ref<string[]>([]) // 选中的班组 const pickerAreaDept = ref<string[]>([]) // 选中的片区 // 集团选项变化 const changeGroupDept = ({ selectedValues, selectedOptions }: any) => { // 清空 单位 部门 班组 unitDeptList.value = [] divisionDeptList.value = [] teamDeptList.value = [] areaDeptList.value = [] pickerUnitDept.value = [] pickerDivisionDept.value = [] pickerTeamDept.value = [] pickerAreaDept.value = [] if (!selectedOptions[0].text.includes('全部')) { // 填充单位 unitDeptList.value = allDeptList.value.filter(item => item.pid === selectedValues[0]).map((item: { name: string; id: string; pid: string }) => ({ text: item.name, value: item.id, pValue: item.pid })) unitDeptList.value.unshift({ text: '全部', value: selectedOptions[0].value, pValue: selectedOptions[0].pValue }) pickerUnitDept.value = [selectedOptions[0].value as string] deptTabs.value = ['集团', '单位'] } else { deptTabs.value = ['集团'] } } // 点击集团直接确定选项 const clickGroupDept = ({ selectedValues, selectedOptions }: any) => { if (!selectedOptions[0].text.includes('全部')) { changeGroupDept({ selectedValues, selectedOptions }) deptTabs.value = ['集团', '单位'] setTimeout(() => { deptActiveTab.value = 1 }, 300) } else { deptTabs.value = ['集团'] } } // 单位选项变化 const changeUnitDept = ({ selectedValues, selectedOptions }: any) => { // 清空 部门 班组 divisionDeptList.value = [] teamDeptList.value = [] areaDeptList.value = [] pickerDivisionDept.value = [] pickerTeamDept.value = [] pickerAreaDept.value = [] if (!selectedOptions[0].text.includes('全部')) { // 填充 部门 divisionDeptList.value = allDeptList.value.filter(item => item.pid === selectedValues[0]).map((item: { name: string; id: string; pid: string }) => ({ text: item.name, value: item.id, pValue: item.pid })) divisionDeptList.value.unshift({ text: '全部', value: selectedOptions[0].value, pValue: selectedOptions[0].pValue }) pickerDivisionDept.value = [selectedOptions[0].value as string] deptTabs.value = ['集团', '单位', '部门'] } else { deptTabs.value = ['集团', '单位'] } } // 点击单位直接确定选项 const clickUnitDept = ({ selectedValues, selectedOptions }: any) => { if (selectedOptions[0].text.includes('全部')) { deptTabs.value = ['集团', '单位'] // showDept.value = false // 确定 // searchQuery.value.deptid = selectedOptions[0].value // searchQueryForNames.value.deptName = allDeptList.value.filter(item => item.id === selectedOptions[0].value)[0].name } else { changeUnitDept({ selectedValues, selectedOptions }) deptTabs.value = ['集团', '单位', '部门'] setTimeout(() => { deptActiveTab.value = 2 }, 300) } } // 部门选项变化 const changeDivisionDept = ({ selectedValues, selectedOptions }: any) => { // 清空 班组 teamDeptList.value = [] areaDeptList.value = [] pickerTeamDept.value = [] pickerAreaDept.value = [] if (!selectedOptions[0].text.includes('全部')) { // 填充 班组 teamDeptList.value = allDeptList.value.filter(item => item.pid === selectedValues[0]).map((item: { name: string; id: string; pid: string }) => ({ text: item.name, value: item.id, pValue: item.pid })) teamDeptList.value.unshift({ text: '全部', value: selectedOptions[0].value, pValue: selectedOptions[0].pValue }) pickerTeamDept.value = [selectedOptions[0].value as string] deptTabs.value = ['集团', '单位', '部门', '班组'] } else { deptTabs.value = ['集团', '单位', '部门'] } } // 点击部门直接确定选项 const clickDivisionDept = ({ selectedValues, selectedOptions }: any) => { if (selectedOptions[0].text.includes('全部')) { deptTabs.value = ['集团', '单位', '部门'] } else { changeDivisionDept({ selectedValues, selectedOptions }) deptTabs.value = ['集团', '单位', '部门', '班组'] setTimeout(() => { deptActiveTab.value = 3 }, 300) } } // 班组选项变化 const changeTeamDept = ({ selectedValues, selectedOptions }: any) => { areaDeptList.value = [] pickerAreaDept.value = [] if (!selectedOptions[0].text.includes('全部')) { // 填充 片区 areaDeptList.value = allDeptList.value.filter(item => item.pid === selectedValues[0]).map((item: { name: string; id: string; pid: string }) => ({ text: item.name, value: item.id, pValue: item.pid })) areaDeptList.value.unshift({ text: '全部', value: selectedOptions[0].value, pValue: selectedOptions[0].pValue }) pickerAreaDept.value = [selectedOptions[0].value as string] deptTabs.value = ['集团', '单位', '部门', '班组', '片区'] } else { deptTabs.value = ['集团', '单位', '部门', '班组'] } } // 点击班组 const clickTeamDept = ({ selectedValues, selectedOptions }: any) => { if (selectedOptions[0].text.includes('全部')) { deptTabs.value = ['集团', '单位', '部门', '班组'] } else { changeTeamDept({ selectedValues, selectedOptions }) deptTabs.value = ['集团', '单位', '部门', '班组', '片区'] setTimeout(() => { deptActiveTab.value = 4 }, 300) } } // 确认管理单位 const onConfirmDept = (data: any[]) => { const dict = { 0: pickerGroupDept, 1: pickerUnitDept, 2: pickerDivisionDept, 3: pickerTeamDept, 4: pickerAreaDept } as { [key: string]: any } data.forEach((item: any, index: number) => { dict[index].value = item.selectedValues }) searchQuery.value.deptid = data[deptTabs.value.length - 1].selectedValues if (!searchQuery.value.deptid[0]) { searchQueryForNames.value.deptName = '' showDept.value = false return } searchQueryForNames.value.deptName = allDeptList.value.filter(item => item.id === data[deptTabs.value.length - 1].selectedValues[0])[0].name showDept.value = false } // 设备类型 const showDeviceType = ref(false) const pickerDeviceType = ref([]) // 选择设备类型 const selectDeviceType = () => { showDeviceType.value = true } // 确认设备类型 const onConfirmDeviceType = ({ selectedValues, selectedOptions }: any) => { searchQuery.value.deviceType = selectedOptions[0]?.value searchQueryForNames.value.deviceTypeName = selectedOptions[0]?.text === '全部' ? '设备类型' : selectedOptions[0]?.text pickerDeviceType.value = selectedValues showDeviceType.value = false } // 按钮类选择 const searchBtn = (type: string) => { // 仅看本人 if (type === 'self') { searchQuery.value.userId = searchQuery.value.userId ? '' : userInfo.id } // 近7日 else if (type === 'week') { if (searchQuery.value.timeType === '1') { searchQuery.value.timeType = '' searchQuery.value.beginTime = '' searchQuery.value.endTime = '' } else { searchQuery.value.timeType = '1' searchQuery.value.beginTime = dayjs().subtract(7, 'day').format('YYYY-MM-DD') searchQuery.value.endTime = dayjs().format('YYYY-MM-DD') } } // 近30日 else if (type === 'month') { if (searchQuery.value.timeType === '2') { searchQuery.value.timeType = '' searchQuery.value.beginTime = '' searchQuery.value.endTime = '' } else { searchQuery.value.timeType = '2' searchQuery.value.beginTime = dayjs().subtract(1, 'month').format('YYYY-MM-DD') searchQuery.value.endTime = dayjs().format('YYYY-MM-DD') } } } // 自定义日期 const showDate = ref(false) // 默认开始日期 const startDate = ref<string[]>([]) // 默认结束日期 const endDate = ref<string[]>([]) startDate.value = dayjs().subtract(1, 'month').format('YYYY-MM-DD').split('-') endDate.value = dayjs().format('YYYY-MM-DD').split('-') // 结束日期的最大日期限制 // const endMaxDate = ref(new Date(2025, 5, 1)) // 选择日期 const selectDate = () => { showDate.value = true } // 确认日期选择 const onConfirmDate = () => { // 先判断结束日期是否在开始日期之前 if (new Date(endDate.value.join('-')).getTime() - new Date(startDate.value.join('-')).getTime() < 0) { showToast('请选择正确时间范围'); return } searchQuery.value.timeType = '3' searchQuery.value.beginTime = startDate.value.join('-') searchQuery.value.endTime = endDate.value.join('-') showDate.value = false } // 取消日期选择 const onCancelDate = () => { showDate.value = false } // 清空自定义日期 const cancelDate = () => { searchQuery.value.timeType = '' searchQuery.value.beginTime = '' searchQuery.value.endTime = '' } const repairTypeList = ref<{ text: string; value: string }[]>([]) // 运维类型 const deviceTypeList = ref<{ text: string; value: string }[]>([]) // 设备类型 // 获取字典 const fetchDict = async () => { // 运维类型 getDictByCode('repairType').then(res => { repairTypeList.value = res.data.map((item: { id: string; name: string; value: string }) => ({ text: item.name, value: item.value })) repairTypeList.value.unshift({ text: '全部', value: '' }) }) // 设备类型 getDeviceTypeListPage({ offset: 1, limit: 9999 }).then(res => { deviceTypeList.value = res.data.rows.map((item: any) => ({ text: item.typeName, value: item.id })) deviceTypeList.value.unshift({ text: '全部', value: '' }) }) // 获取管理单位 loadingDept.value = true getDeptTreeList().then(res => { allDeptList.value = res.data groupDeptList.value = res.data.filter((item: { name: string; id: string; pid: string }) => item.pid === '0').map((item: { name: string; id: string; pid: string }) => ({ text: item.name, value: item.id, pValue: item.pid })) groupDeptList.value.unshift({ text: '全部', value: '', pValue: '' }) loadingDept.value = false }).catch(() => { loadingDept.value = false }) } fetchDict() // 计算滚动区域高度 const scrollHeight = ref(0) const calcHeight = () => { // 公共头部高度40 // 边距安全 30 // 导出按钮 const exportBtnHeight = document.getElementById('export-btn-log')?.offsetHeight || 0 // 查询头部 const searchHeaderHeight = document.getElementById('search-area-log')?.offsetHeight || 0 scrollHeight.value = window.innerHeight - 40 - exportBtnHeight - searchHeaderHeight - 30 } const search = ref({}) // 查询条件 给导出使用 const fetchData = () => { const obj = { deviceType: searchQuery.value.deviceType, offset: searchQuery.value.offset, limit: 10, repairType: searchQuery.value.repairType, deptid: searchQuery.value.deptid ? searchQuery.value.deptid[0] : '', repairPerson: searchQuery.value.userId ? userInfo.name : '', begTime: searchQuery.value.beginTime, endTime: searchQuery.value.endTime, } search.value = obj getOperationListPage(obj).then(res => { total.value = res.data.total list.value = [...list.value, ...(res.data.rows || [])] list.value = list.value.map((item: any) => ({ ...item, repairTime: item.repairTime ? dayjs().format('YYYY-MM-DD') : '' })) loading.value = false }) } watch([() => searchQuery.value.beginTime, () => searchQuery.value.deptid, () => searchQuery.value.deviceType, () => searchQuery.value.endTime, () => searchQuery.value.repairType, () => searchQuery.value.timeType, () => searchQuery.value.userId ], () => { list.value = [] searchQuery.value.offset = 1 loading.value = true fetchData() }, { deep: true, }) onMounted(() => { calcHeight() loading.value = true fetchData() }) window.addEventListener('resize', () => { calcHeight() }) onBeforeUnmount(() => { window.addEventListener('resize', () => { }) }) // 查看详情 const $router = useRouter() const detail = (event: any, row: any) => { if (event.target.innerHTML === ('收起') || event.target.innerHTML === ('展开')) { return } $router.push({ name: 'RecordDetail', query: { row: JSON.stringify(row) } }) } // 导出运维记录 const exportLog = () => { const toast = showLoadingToast({ duration: 0, forbidClick: true, message: '加载中...', }) exportOperationList(search.value).then(res => { exportFile(res.data, '运维记录') closeToast() }) } // 页面缓存 onBeforeRouteLeave((to: any) => { keepSearchParams(to.path, 'H5OperationRecord') }) onActivated(() => { if (!($router.options.history.state.forward as string || '').includes('detail')) { fetchData() } }) // 滚动条 const scrollbarRef = ref() const handleScroll = (a) => { // 判断滚动条是否滚动到底部 const scrollbarContainer = scrollbarRef.value.$el.querySelector('.el-scrollbar__wrap') const isScrolledToBottom = scrollbarContainer.scrollHeight - scrollbarContainer.scrollTop <= scrollbarContainer.clientHeight + 50 if (isScrolledToBottom) { if (list.value.length === total.value) { return } // debounce(() => { loading.value = true searchQuery.value.offset += 1 fetchData() // }, 200) } } </script> <template> <div style="position: relative;"> <!-- 查询条件 --> <div id="search-area-log" class="search-area"> <!-- 运维类型选择 --> <van-popup v-model:show="showRepairType" destroy-on-close position="bottom"> <van-picker :columns="repairTypeList" :model-value="pickerRepairType" @confirm="onConfirmRepairType" @cancel="showRepairType = false" /> </van-popup> <!-- 管理单位 --> <van-popup v-model:show="showDept" destroy-on-close position="bottom"> <!-- 树形选择 --> <!-- , '班组' --> <van-picker-group v-model:active-tab="deptActiveTab" title="管理单位" :tabs="deptTabs" @confirm="onConfirmDept" @cancel="showDept = false"> <!-- 集团 --> <van-picker v-show="deptTabs.includes('集团')" :loading="loadingDept" :swipe-duration="500" :columns="groupDeptList" :model-value="pickerGroupDept" @change="changeGroupDept" @click-option="clickGroupDept" /> <!-- 单位 --> <van-picker v-show="deptTabs.includes('单位')" :swipe-duration="500" :columns="unitDeptList" :model-value="pickerUnitDept" @change="changeUnitDept" @click-option="clickUnitDept" /> <!-- 部门 --> <van-picker v-show="deptTabs.includes('部门')" :swipe-duration="500" :columns="divisionDeptList" :model-value="pickerDivisionDept" @change="changeDivisionDept" @click-option="clickDivisionDept" /> <!-- 班组 --> <van-picker v-show="deptTabs.includes('班组')" :swipe-duration="500" :columns="teamDeptList" :model-value="pickerTeamDept" @change="changeTeamDept" @click-option="clickTeamDept" /> <!-- 片区 --> <van-picker v-show="deptTabs.includes('片区')" :swipe-duration="500" :columns="areaDeptList" :model-value="pickerAreaDept" /> </van-picker-group> </van-popup> <!-- 设备类型选择 --> <van-popup v-model:show="showDeviceType" destroy-on-close position="bottom"> <van-picker :columns="deviceTypeList" :model-value="pickerDeviceType" @confirm="onConfirmDeviceType" @cancel="showDeviceType = false" /> </van-popup> <!-- 开始结束日期 --> <van-popup v-model:show="showDate" destroy-on-close position="bottom"> <van-picker-group title="预约日期" :tabs="['开始日期', '结束日期']" @confirm="onConfirmDate" @cancel="onCancelDate"> <van-date-picker v-model="startDate" /> <van-date-picker v-model="endDate" /> </van-picker-group> </van-popup> <div class="search-container-top"> <div class="search-item" @click="selectRepairType"> <span class="value" :class="searchQueryForNames.repairTypeName === '运维类型' ? '' : 'active'">{{ searchQueryForNames.repairTypeName }}</span> <van-icon name="arrow-down" class="icon" /> </div> <div class="search-item" @click="selectDept"> <span class="value" :class="searchQueryForNames.deptName === '' ? '' : 'active'"> {{ searchQueryForNames.deptName === '' ? '管理单位' : searchQueryForNames.deptName }} </span> <van-icon name="arrow-down" class="icon" /> </div> <div class="search-item" @click="selectDeviceType"> <span class="value" :class="searchQueryForNames.deviceTypeName === '设备类型' ? '' : 'active'">{{ searchQueryForNames.deviceTypeName }}</span> <van-icon name="arrow-down" class="icon" /> </div> <div class="search-btn" @click="searchBtn('self')" :class="searchQuery.userId ? 'active-btn' : ''"> 仅看本人 </div> </div> <div class="search-container-bottom"> <div class="search-btn" @click="searchBtn('week')" :class="searchQuery.timeType === '1' ? 'active-btn' : ''"> 近7日 </div> <div class="search-btn" @click="searchBtn('month')" :class="searchQuery.timeType === '2' ? 'active-btn' : ''"> 近30日 </div> <div class="date" :class="searchQuery.timeType === '3' ? 'active-btn' : ''"> <span @click="selectDate" style="text-align: center;flex: 1;"> <!-- <span> </span> --> {{ searchQuery.timeType === '3' ? searchQuery.beginTime : '开始时间' }} </span> <span @click="selectDate">至</span> <!-- <span> --> <span @click="selectDate" style="text-align: center;flex: 1;">{{ searchQuery.timeType === '3' ? searchQuery.endTime : '结束时间' }}</span> <!-- <span> </span> --> <!-- </span> --> <van-icon v-if="searchQuery.timeType === '3'" class="icon" name="close" size="1.2rem" @click="cancelDate" /> </div> </div> </div> <!-- 查询结果 --> <div v-loading="loading" class="search-result"> <el-scrollbar ref="scrollbarRef" :max-height="`${scrollHeight}px`" @scroll="handleScroll"> <lazy-component v-if="list.length"> <div v-for="(item, index) in list" :key="index" class="result-item" :class="index !== 0 ? 'top-border' : ''" @click="(event) => detail(event, item)"> <div class="devcode"> {{ item.devcode }}</div> <div class="cell"> <div class="title">设备类型</div> <div class="value"> <van-text-ellipsis :rows="1" :content="item.deviceTypeName" expand-text="展开" collapse-text="收起" /> </div> </div> <div class="cell"> <div class="title">运维类型</div> <div class="value"> <van-text-ellipsis :rows="1" :content="item.repairType" expand-text="展开" collapse-text="收起" /> </div> </div> <div class="cell"> <div class="title">安装位置</div> <div class="value">{{ item.ledgerNumber }}</div> </div> <div class="cell"> <div class="title">管理单位</div> <div class="value"> <van-text-ellipsis :rows="1" :content="item.deptName" expand-text="展开" collapse-text="收起" /> </div> </div> <div class="cell"> <div class="title">运维时间</div> <div class="value">{{ item.repairTime }}</div> </div> </div> </lazy-component> <div class="to-top"> <van-back-top /> </div> <van-empty v-if="!list.length" description="暂无数据" /> </el-scrollbar> </div> <!-- 导出按钮 --> <div id="export-btn-log" class="export-btn"> <el-button type="primary" style="width: 96%;" @click="exportLog">导出运维记录</el-button> </div> </div> </template> <style lang="scss" scoped> .to-top { ::v-deep(.van-back-top) { opacity: 0.4 !important; } } $primary: #0D76D4; $--van-primary-color: #0D76D4; ::v-deep(.van-picker-column__item--selected) { color: $primary !important; } .active { color: $primary !important; } .active-btn { color: $primary !important; border: 1px solid $primary !important; } .search-area { background-color: #fff; margin: 6px; font-size: 1rem; border-radius: 8px; font-weight: 400; } .search-container-top { width: 100%; display: flex; padding: 6px; // padding-left: 0; // padding-right: 0; justify-content: space-around; color: #555; .search-item { width: 26%; text-align: center; padding: 4px; display: flex; // justify-content: space-between; align-items: center; vertical-align: middle; .value { width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .icon { // display: inline-block; // width: 20px; // height: 20px; vertical-align: middle; margin-left: 6px; // background: url('@/assets/icons/search-down.svg') no-repeat center center / cover; } } .search-btn { width: 20%; padding: 4px; text-align: center; background-color: #fff; border-radius: 16px; border: 1px solid #e4e7ed; white-space: nowrap; /* 确保文本在一行内显示 */ overflow: hidden; /* 超出容器部分隐藏 */ text-overflow: ellipsis; /* 文字溢出显示省略号 */ } } .search-container-bottom { width: 100%; display: flex; padding: 6px; // padding-left: 0; // padding-right: 0; padding-top: 0; color: #555; justify-content: space-around; .search-btn { width: 18%; padding: 4px; text-align: center; background-color: #fff; border-radius: 16px; border: 1px solid #e4e7ed; } .date { width: 62%; background-color: #fff; border-radius: 6px; display: flex; justify-content: space-between; align-items: center; padding: 0 5px; border: 1px solid #e4e7ed; vertical-align: middle; .icon { vertical-align: middle; } } } .search-result { background-color: #fff; margin: 6px; font-size: 0.8rem; border-radius: 8px; color: #444; padding: 14px; padding-bottom: 0px; // height: 600px; .top-border { border-top: 1px solid #e4e7ed; // margin-top: 8px; } .result-item { padding: 4px; // box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, .04), 0px 8px 20px rgba(0, 0, 0, .08); .devcode { font-weight: 700; font-size: 1.1rem; } .cell { display: flex; justify-content: space-between; padding-top: 6px; padding-bottom: 6px; padding-right: 14px; .value { color: #888; width: 80%; text-align: right; font-size: 1rem; } .title { width: 20%; white-space: nowrap; font-weight: 500; font-size: 1rem; } .title, .value { white-space: nowrap; // overflow: hidden; // text-overflow: ellipsis; } } } } .export-btn { width: 100%; position: fixed; bottom: 0; display: flex; justify-content: center; ::v-deep(.el-button) { font-size: 18px; } } </style>