| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674 |
- <template>
- <div class="repair-container">
- <!-- 上半部分:天地图(左) + 图表(右) -->
- <div class="top-section">
- <el-card class="topo-card">
- <template #header>
- <div class="topo-header">
- <span class="card-title">燃气管网维修记录拓扑图</span>
- <div class="topo-legend">
- <span class="legend-item"><i class="legend-line high"></i> 高压</span>
- <span class="legend-item"><i class="legend-line medium"></i> 中压</span>
- <span class="legend-item"><i class="legend-line low"></i> 低压</span>
- <span class="legend-item"><i class="legend-dot repaired"></i> 有维修</span>
- <span class="legend-item"><i class="legend-dot point"></i> 管点</span>
- </div>
- </div>
- </template>
- <div class="map-wrapper">
- <div id="repairTdtMap" class="tdt-map"></div>
- <div v-if="mapError" class="map-error">{{ mapError }}</div>
- </div>
- </el-card>
- <div class="charts-column">
- <el-card class="chart-card">
- <template #header><span class="card-title">维修统计</span></template>
- <div id="riskPieChart" style="height:260px"></div>
- </el-card>
- <el-card class="chart-card">
- <template #header><span class="card-title">维修趋势</span></template>
- <div id="repairTrendChart" style="height:260px"></div>
- </el-card>
- </div>
- </div>
- <!-- 下半部分:搜索 + 表格 -->
- <el-card class="table-card">
- <div class="table-toolbar">
- <div class="table-toolbar-left">
- <el-form :inline="true" :model="searchForm" class="search-form">
- <el-form-item label="管网ID">
- <el-select v-model="searchForm.networkId" placeholder="请选择" clearable style="width:190px" @change="onFilterChange">
- <el-option v-for="n in networkOptions" :key="n.networkId" :label="`${n.networkName}(${n.networkCode})`" :value="n.networkId" />
- </el-select>
- </el-form-item>
- <el-form-item label="维修类型">
- <el-select v-model="searchForm.repairType" placeholder="请选择" clearable style="width:160px">
- <el-option v-for="d in repairTypeDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
- </el-select>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" @click="handleSearch">查询</el-button>
- <el-button @click="handleReset">重置</el-button>
- </el-form-item>
- </el-form>
- </div>
- <div class="table-toolbar-right">
- <el-button type="success" @click="handleAdd">新增维修记录</el-button>
- </div>
- </div>
- <el-table :data="tableData" border stripe v-loading="loading" max-height="380" size="small">
- <el-table-column label="管网" width="130">
- <template #default="{ row }">{{ getNetworkName(row.networkId) }}</template>
- </el-table-column>
- <el-table-column prop="repairDate" label="维修日期" width="120" />
- <el-table-column prop="repairType" label="维修类型" width="120">
- <template #default="{ row }">{{ getDictLabel(repairTypeDict, row.repairType) }}</template>
- </el-table-column>
- <el-table-column prop="repairContent" label="维修内容" min-width="230" show-overflow-tooltip />
- <el-table-column prop="repairCompany" label="维修单位" min-width="160" show-overflow-tooltip />
- <el-table-column prop="repairCost" label="费用(元)" width="110" align="right" />
- <el-table-column prop="repairPerson" label="负责人" width="100" />
- <el-table-column label="图片" width="80">
- <template #default="{ row }">
- <template v-if="parseImageUrls(row.repairImages).length">
- <el-image
- :src="getImageUrl(parseImageUrls(row.repairImages)[0])"
- fit="cover"
- :preview-src-list="parseImageUrls(row.repairImages).map(getImageUrl)"
- preview-teleported
- class="table-image"
- />
- </template>
- <el-tag v-else type="info" size="small">无</el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="createTime" label="创建时间" width="170" />
- <el-table-column label="操作" width="150" fixed="right">
- <template #default="{ row }">
- <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
- <el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- <el-pagination
- v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total"
- layout="total, prev, pager, next, sizes" :page-sizes="[10, 20, 50]"
- @size-change="() => { pageNum = 1; loadData() }" @current-change="() => loadData()"
- style="margin-top:16px;justify-content:flex-end"
- />
- </el-card>
- <!-- 详情对话框 -->
- <el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px" destroy-on-close>
- <el-form :model="formData" ref="formRef" label-width="100px">
- <el-form-item label="管网ID" prop="networkId">
- <el-select v-model="formData.networkId" style="width:100%">
- <el-option v-for="n in networkOptions" :key="n.networkId" :label="`${n.networkName}(${n.networkCode})`" :value="n.networkId" />
- </el-select>
- </el-form-item>
- <el-form-item label="维修日期" prop="repairDate">
- <el-date-picker v-model="formData.repairDate" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width:100%" />
- </el-form-item>
- <el-form-item label="维修类型" prop="repairType">
- <el-select v-model="formData.repairType" style="width:100%">
- <el-option v-for="d in repairTypeDict" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
- </el-select>
- </el-form-item>
- <el-form-item label="维修内容"><el-input v-model="formData.repairContent" type="textarea" :rows="3" /></el-form-item>
- <el-form-item label="维修单位"><el-input v-model="formData.repairCompany" /></el-form-item>
- <el-form-item label="费用(元)"><el-input-number v-model="formData.repairCost" :precision="2" :step="100" style="width:100%" /></el-form-item>
- <el-form-item label="负责人"><el-input v-model="formData.repairPerson" /></el-form-item>
- <el-form-item label="备注"><el-input v-model="formData.remark" type="textarea" :rows="2" /></el-form-item>
- <el-form-item label="维修图片">
- <el-upload
- action="#"
- :auto-upload="false"
- :show-file-list="false"
- :http-request="() => {}"
- :on-change="handleImageChange"
- accept="image/jpeg,image/png,image/jpg"
- multiple
- >
- <el-button type="primary" plain>选择图片</el-button>
- <template #tip>
- <div class="el-upload__tip">支持 JPG/PNG 格式,单张不超过 5MB</div>
- </template>
- </el-upload>
- <div v-if="imageList.length" class="image-list">
- <div v-for="(img, index) in imageList" :key="index" class="image-item">
- <img :src="img" class="image-thumb" @click="previewImage(index)" />
- <el-icon class="image-remove" @click="removeImage(index)"><Close /></el-icon>
- </div>
- </div>
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="dialogVisible = false">取消</el-button>
- <el-button type="primary" @click="handleSubmit">确定</el-button>
- </template>
- </el-dialog>
- <!-- 图片预览 -->
- <el-image-viewer
- v-if="previewVisible"
- :url-list="previewList"
- :initial-index="previewIndex"
- hide-on-click-modal
- @close="previewVisible = false"
- />
- </div>
- </template>
- <script setup>
- import { whenMapReady } from '@/utils/mapApi'
- import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
- import * as echarts from 'echarts'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import { Close } from '@element-plus/icons-vue'
- import request from '@/utils/request'
- import { convertCoord } from '@/utils/coordTransform'
- import { getPipeNetworkOptions, getRepairRecordPage, getRepairStats, addRepairRecord, updateRepairRecord, deleteRepairRecord } from '@/api/pipeNetwork/basic'
- const baseApi = import.meta.env.VITE_APP_BASE_API
- // ==================== 数据 ====================
- const loading = ref(false)
- const pageNum = ref(1)
- const pageSize = ref(10)
- const total = ref(0)
- const searchForm = reactive({ networkId: '', repairType: '' })
- const dialogVisible = ref(false)
- const dialogTitle = ref('新增')
- const formRef = ref(null)
- const formData = reactive({ repairId: '', networkId: '', repairDate: '', repairType: '', repairContent: '', repairCompany: '', repairCost: null, repairPerson: '', remark: '', repairImages: '' })
- const repairTypeDict = ref([
- { dictValue: '1', dictLabel: '抢修' },
- { dictValue: '2', dictLabel: '计划维修' },
- { dictValue: '3', dictLabel: '防腐层修复' },
- { dictValue: '4', dictLabel: '管件更换' },
- { dictValue: '5', dictLabel: '焊缝修补' },
- { dictValue: '6', dictLabel: '阀门维修' }
- ])
- function getDictLabel(dict, value) {
- const item = dict.find(d => d.dictValue === String(value))
- return item ? item.dictLabel : value
- }
- // ==================== 天地图配置 ====================
- const centerPoint = { lng: 110.386, lat: 28.445 }
- const MAP_BOUNDS = { southWest: { lng: 110.355, lat: 28.428 }, northEast: { lng: 110.410, lat: 28.460 } }
- const MAP_MIN_ZOOM = 13
- const MAP_MAX_ZOOM = 19
- const MAP_DEFAULT_ZOOM = 15
- const mapError = ref('')
- const networkOptions = ref([])
- let mapInstance = null
- let activeInfoWindow = null
- let overlayClicked = false
- let mapOverlays = []
- // ==================== 管网数据 (沅陵县真实坐标) ====================
- const pipelineData = [
- { id: 'net001', name: '燃气1', pressure: 'high',
- points: [{lng:110.404947,lat:28.433419},{lng:110.404836,lat:28.433429},{lng:110.403275,lat:28.433571},{lng:110.401084,lat:28.433876},{lng:110.399601,lat:28.433876},{lng:110.398112,lat:28.433979},{lng:110.396717,lat:28.434314},{lng:110.394299,lat:28.434538},{lng:110.394352,lat:28.434887},{lng:110.391969,lat:28.434985},{lng:110.384374,lat:28.435695},{lng:110.381418,lat:28.43559},{lng:110.377993,lat:28.436193},{lng:110.376062,lat:28.436385},{lng:110.374231,lat:28.43655},{lng:110.372584,lat:28.437107},{lng:110.371292,lat:28.4377},{lng:110.370581,lat:28.438198},{lng:110.369831,lat:28.438821}] },
- { id: 'net002', name: '燃气2', pressure: 'medium',
- points: [{lng:110.361433,lat:28.454979},{lng:110.362363,lat:28.453169},{lng:110.362851,lat:28.452213},{lng:110.364377,lat:28.452443},{lng:110.365138,lat:28.45246},{lng:110.366462,lat:28.452157},{lng:110.369549,lat:28.451441},{lng:110.37219,lat:28.450897},{lng:110.372753,lat:28.450901},{lng:110.373875,lat:28.451194},{lng:110.373913,lat:28.451107},{lng:110.374597,lat:28.451476}] },
- ]
- // 管点数据 (管网节点位置)
- const pipePointData = [
- { name: 'A点', lng: 110.362363, lat: 28.453169, pipelineId: 'net001' },
- { name: 'B点', lng: 110.364377, lat: 28.452443, pipelineId: 'net001' },
- { name: 'C点', lng: 110.372753, lat: 28.450901, pipelineId: 'net001' },
- { name: 'D点', lng: 110.401084, lat: 28.433876, pipelineId: 'net001' },
- { name: 'E点', lng: 110.391969, lat: 28.434985, pipelineId: 'net001' },
- { name: 'F点', lng: 110.376062, lat: 28.436385, pipelineId: 'net001' },
- { name: 'G点', lng: 110.370581, lat: 28.438198, pipelineId: 'net001' },
- ]
- // ==================== 维修记录数据 ====================
- const tableData = ref([])
- // 图片相关
- const imageList = ref([]) // 展示用预览列表(URL)
- const imageUrls = ref([]) // 已上传的图片URL列表
- const imageUploading = ref(false) // 上传中状态
- const previewVisible = ref(false)
- const previewList = ref([])
- const previewIndex = ref(0)
- /** 获取图片展示 URL(直接使用上传接口返回的完整 URL) */
- function getImageUrl(url) {
- if (!url) return ''
- return url.trim()
- }
- /** 将存储的 repairImages 字符串转为有效 URL 数组 */
- function parseImageUrls(repairImages) {
- if (!repairImages) return []
- return repairImages.split(',').map(s => s.trim()).filter(Boolean)
- }
- /** 图片选择后立即上传到 MinIO */
- async function handleImageChange(file) {
- if (!file.raw) return
- const isImage = file.raw.type === 'image/jpeg' || file.raw.type === 'image/png' || file.raw.type === 'image/jpg'
- if (!isImage) { ElMessage.error('只能上传图片文件 (JPG/PNG)!'); return }
- if (file.raw.size / 1024 / 1024 >= 5) { ElMessage.error('图片大小不能超过 5MB!'); return }
- imageUploading.value = true
- try {
- const uploadFormData = new FormData()
- uploadFormData.append('file', file.raw)
- uploadFormData.append('dir', 'upload') // 指定存储目录(可选)
- const uploadResponse = await request({
- url: '/minio/upload',
- method: 'post',
- data: uploadFormData
- })
- const url = uploadResponse.url || uploadResponse.fileName
- imageUrls.value.push(url)
- imageList.value.push(getImageUrl(url))
- } catch (e) {
- ElMessage.error('图片上传失败')
- } finally {
- imageUploading.value = false
- }
- }
- /** 删除图片 */
- function removeImage(index) {
- imageUrls.value.splice(index, 1)
- imageList.value.splice(index, 1)
- }
- /** 预览大图 */
- function previewImage(index) {
- previewList.value = [...imageList.value]
- previewIndex.value = index
- previewVisible.value = true
- }
- const pressureColors = { high: '#e63946', medium: '#f77f00', low: '#2a9d8f' }
- const repairedNetworkIds = computed(() => { const s = new Set(); tableData.value.forEach(r => s.add(r.networkId)); return s })
- async function loadNetworkOptions() {
- if (networkOptions.value.length > 0) return
- try { const res = await getPipeNetworkOptions(); networkOptions.value = res.data || [] } catch (e) { console.error(e) }
- }
- function getNetworkName(id) {
- const found = networkOptions.value.find(n => n.networkId === id)
- return found ? found.networkName : id
- }
- function getPipelineName(id) {
- const name = getNetworkName(id)
- return name !== id ? name : (pipelineData.find(p => p.id === id) || {}).name || id
- }
- // ==================== 数据加载 ====================
- async function loadData() {
- loading.value = true
- try {
- const res = await getRepairRecordPage(pageNum.value, pageSize.value, searchForm)
- tableData.value = (res.data && res.data.records) ? res.data.records : []
- total.value = (res.data && res.data.total) ? res.data.total : 0
- } catch (e) { console.error(e) } finally { loading.value = false }
- }
- // ==================== 天地图 ====================
- function closeInfoWindow() {
- if (activeInfoWindow) { try { activeInfoWindow.close(); mapInstance.removeOverlay(activeInfoWindow) } catch (_) {}; activeInfoWindow = null }
- }
- function getMidPoint(points) {
- let lng = 0, lat = 0
- points.forEach(p => { lng += p.lng; lat += p.lat })
- return { lng: lng / points.length, lat: lat / points.length }
- }
- function initMap() {
- const el = document.getElementById('repairTdtMap')
- if (!el) { mapError.value = '地图容器未找到。'; return }
- try { mapInstance = new TdtMap.Map('repairTdtMap', { enableMapClick: true }) } catch (e) {
- console.error('创建地图实例失败:', e); mapError.value = '天地图初始化失败,请检查 VITE_TIANDITU_TK 与网络连接。'; return
- }
- const [clng, clat] = convertCoord(centerPoint.lng, centerPoint.lat); try { mapInstance.centerAndZoom(new TdtMap.Point(clng, clat), MAP_DEFAULT_ZOOM); mapInstance.enableScrollWheelZoom(true) } catch (e) { console.error('设置中心失败:', e) }
- try { if (typeof mapInstance.setMinZoom === 'function') mapInstance.setMinZoom(MAP_MIN_ZOOM); if (typeof mapInstance.setMaxZoom === 'function') mapInstance.setMaxZoom(MAP_MAX_ZOOM) } catch (e) {}
- try {
- if (typeof mapInstance.setMaxBounds === 'function') {
- const sw = new TdtMap.Point(MAP_BOUNDS.southWest.lng, MAP_BOUNDS.southWest.lat)
- const ne = new TdtMap.Point(MAP_BOUNDS.northEast.lng, MAP_BOUNDS.northEast.lat)
- mapInstance.setMaxBounds(new TdtMap.Bounds(sw, ne))
- }
- } catch (e) {}
- mapInstance.addEventListener('click', () => {
- if (overlayClicked) { overlayClicked = false; return }
- closeInfoWindow()
- })
- try { renderMapScene() } catch (e) { console.error('渲染地图失败:', e); mapError.value = '地图要素渲染失败。' }
- }
- function renderMapScene() {
- if (!mapInstance || typeof TdtMap === 'undefined') return
- closeInfoWindow()
- try { mapInstance.clearOverlays() } catch (_) {}
- mapOverlays = []
- // 绘制管线
- pipelineData.forEach(pipe => {
- try {
- const hasRepair = repairedNetworkIds.value.has(pipe.id)
- const pts = pipe.points.map(p => { const [lng, lat] = convertCoord(p.lng, p.lat); return new TdtMap.Point(lng, lat) })
- const polyline = new TdtMap.Polyline(pts, {
- strokeColor: hasRepair ? '#f59e0b' : pressureColors[pipe.pressure],
- strokeWeight: hasRepair ? 5 : 3.5,
- strokeOpacity: 0.9
- })
- mapInstance.addOverlay(polyline)
- mapOverlays.push({ overlay: polyline, pipe })
- polyline.addEventListener('click', (e) => {
- overlayClicked = true
- if (e && e.domEvent) e.domEvent.stopPropagation()
- closeInfoWindow()
- const rawMid = getMidPoint(pipe.points)
- const mid = { lng: convertCoord(rawMid.lng, rawMid.lat)[0], lat: convertCoord(rawMid.lng, rawMid.lat)[1] }
- const count = tableData.value.filter(r => r.networkId === pipe.id).length
- const repairList = tableData.value.filter(r => r.networkId === pipe.id).slice(0, 3).map(r => `${r.repairDate} ${getDictLabel(repairTypeDict, r.repairType)}`).join('<br>')
- const html = `<div style="font-size:13px;max-width:300px;"><b>${pipe.name}</b>(${getNetworkName(pipe.id)})<br>
- <span style="color:${pressureColors[pipe.pressure]};font-weight:bold;">${pipe.pressure === 'high' ? '高压' : pipe.pressure === 'medium' ? '中压' : '低压'}</span>
- 累计维修 <b style="color:#e63946;">${count}</b> 次
- ${count > 0 ? '<br><span style="color:#909399;font-size:11px;">' + repairList + '</span>' : ''}</div>`
- activeInfoWindow = new TdtMap.InfoWindow(html, { width: 260 })
- mapInstance.openInfoWindow(activeInfoWindow, new TdtMap.Point(mid.lng, mid.lat))
- // 筛选表格
- searchForm.networkId = pipe.id
- pageNum.value = 1
- })
- } catch (_) {}
- })
- // 绘制管点
- pipePointData.forEach(point => {
- try {
- const hasRepair = repairedNetworkIds.value.has(point.pipelineId)
- const [plng, plat] = convertCoord(point.lng, point.lat); const pt = new TdtMap.Point(plng, plat)
- const color = hasRepair ? '#f59e0b' : '#3b82f6'
- const size = hasRepair ? 10 : 7
- const label = new TdtMap.Label(point.name, {
- position: pt,
- offset: new TdtMap.Size(-22, -22)
- })
- label.setStyle({
- color: '#fff', fontSize: '10px', padding: '2px 6px',
- backgroundColor: color, borderRadius: '3px', whiteSpace: 'nowrap',
- border: hasRepair ? '2px solid #fff' : 'none', boxShadow: hasRepair ? '0 0 6px rgba(245,158,11,0.6)' : 'none'
- })
- mapInstance.addOverlay(label)
- mapOverlays.push({ overlay: label, point })
- label.addEventListener('click', (e) => {
- overlayClicked = true
- if (e && e.domEvent) e.domEvent.stopPropagation()
- closeInfoWindow()
- const pipeName = getPipelineName(point.pipelineId)
- const count = tableData.value.filter(r => r.networkId === point.pipelineId).length
- const html = `<div style="font-size:13px;"><b>${point.name}</b><br>所属管线: ${point.pipelineId} ${pipeName}<br>维修记录: <b style="color:#e63946;">${count}</b> 次</div>`
- activeInfoWindow = new TdtMap.InfoWindow(html, { width: 240 })
- mapInstance.openInfoWindow(activeInfoWindow, pt)
- searchForm.networkId = point.pipelineId
- pageNum.value = 1
- })
- } catch (_) {}
- })
- // 维修标记点 (有维修记录的管线中点位置)
- pipelineData.forEach(pipe => {
- if (!repairedNetworkIds.value.has(pipe.id)) return
- try {
- const rawMid2 = getMidPoint(pipe.points)
- const mid2 = { lng: convertCoord(rawMid2.lng, rawMid2.lat)[0], lat: convertCoord(rawMid2.lng, rawMid2.lat)[1] }
- const pt = new TdtMap.Point(mid2.lng, mid2.lat)
- const count = tableData.value.filter(r => r.networkId === pipe.id).length
- const marker = new TdtMap.Label(String(count), {
- position: pt,
- offset: new TdtMap.Size(-12, -12)
- })
- marker.setStyle({
- color: '#fff', fontSize: '11px', fontWeight: 'bold',
- backgroundColor: '#e63946', borderRadius: '12px',
- padding: '2px 7px', border: '2px solid #fff',
- boxShadow: '0 0 8px rgba(230,57,70,0.5)'
- })
- mapInstance.addOverlay(marker)
- mapOverlays.push({ overlay: marker, pipe })
- } catch (_) {}
- })
- }
- // ==================== ECharts ====================
- let riskPieChart = null
- let repairTrendChart = null
- function initRiskPie(data) {
- const dom = document.getElementById('riskPieChart')
- if (!dom) return
- if (riskPieChart) riskPieChart.dispose()
- riskPieChart = echarts.init(dom)
- riskPieChart.setOption({
- tooltip: { trigger: 'item', formatter: '{b}: {c} 次 ({d}%)' },
- legend: { orient: 'vertical', right: 6, top: 'center', itemWidth: 10, itemHeight: 10, textStyle: { color: '#606a78', fontSize: 11 }, itemGap: 14 },
- series: [{
- type: 'pie', radius: ['50%', '76%'], center: ['38%', '50%'],
- avoidLabelOverlap: false, itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 3 },
- label: { show: false }, emphasis: { scale: true, scaleSize: 6 },
- data: (data || []).map(d => ({ value: d.value, name: d.name, itemStyle: { color: d.color } }))
- }]
- })
- }
- function initRepairTrend(data) {
- const dom = document.getElementById('repairTrendChart')
- if (!dom) return
- if (repairTrendChart) repairTrendChart.dispose()
- repairTrendChart = echarts.init(dom)
- const t = data || {}
- const months = t.months || ['1月','2月','3月','4月','5月','6月']
- repairTrendChart.setOption({
- tooltip: { trigger: 'axis' },
- legend: { top: 0, textStyle: { color: '#606a78', fontSize: 11 } },
- grid: { left: 44, right: 20, top: 36, bottom: 32 },
- xAxis: { type: 'category', data: months, boundaryGap: false, axisLabel: { color: '#9098a6', fontSize: 11 }, axisLine: { lineStyle: { color: '#d0d5dd' } } },
- yAxis: { type: 'value', name: '次数', minInterval: 1, axisLabel: { color: '#9098a6', fontSize: 11 }, splitLine: { lineStyle: { color: '#f0f2f6', type: 'dashed' } } },
- series: [
- { name: '高压', type: 'line', data: t.high || [], smooth: true, symbol: 'circle', symbolSize: 7, lineStyle: { width: 3, color: '#e63946' }, itemStyle: { color: '#e63946', borderColor: '#fff', borderWidth: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(230,57,70,0.12)' }, { offset: 1, color: 'rgba(230,57,70,0)' }]) } },
- { name: '中压', type: 'line', data: t.medium || [], smooth: true, symbol: 'circle', symbolSize: 7, lineStyle: { width: 3, color: '#f77f00' }, itemStyle: { color: '#f77f00', borderColor: '#fff', borderWidth: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(247,127,0,0.12)' }, { offset: 1, color: 'rgba(247,127,0,0)' }]) } },
- { name: '低压', type: 'line', data: t.low || [], smooth: true, symbol: 'circle', symbolSize: 7, lineStyle: { width: 3, color: '#2a9d8f' }, itemStyle: { color: '#2a9d8f', borderColor: '#fff', borderWidth: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(42,157,143,0.12)' }, { offset: 1, color: 'rgba(42,157,143,0)' }]) } }
- ]
- })
- }
- async function loadStats() {
- try {
- const res = await getRepairStats()
- const d = res.data || {}
- initRiskPie(d.typeStats)
- initRepairTrend(d.trend)
- } catch (e) { console.error(e) }
- }
- function resizeCharts() { riskPieChart?.resize(); repairTrendChart?.resize() }
- // ==================== 操作 ====================
- function onFilterChange() { pageNum.value = 1; loadData() }
- function handleSearch() { pageNum.value = 1; loadData() }
- function handleReset() {
- searchForm.networkId = ''; searchForm.repairType = ''; pageNum.value = 1
- loadData()
- }
- function handleAdd() {
- dialogTitle.value = '新增'; dialogVisible.value = true
- Object.assign(formData, { repairId: '', networkId: '', repairDate: '', repairType: '', repairContent: '', repairCompany: '', repairCost: null, repairPerson: '', remark: '', repairImages: '' })
- imageList.value = []
- imageUrls.value = []
- loadNetworkOptions()
- }
- function handleEdit(row) {
- dialogTitle.value = '编辑'; dialogVisible.value = true
- Object.assign(formData, { ...row })
- // 回显已有图片
- const urls = parseImageUrls(row.repairImages)
- imageUrls.value = [...urls]
- imageList.value = urls.map(url => getImageUrl(url))
- loadNetworkOptions()
- }
- async function handleSubmit() {
- try {
- if (imageUploading.value) { ElMessage.warning('图片正在上传,请稍候'); return }
- // 将图片URL列表拼接为逗号分隔字符串
- formData.repairImages = imageUrls.value.join(',')
- if (formData.repairId) { await updateRepairRecord(formData); ElMessage.success('修改成功') }
- else { await addRepairRecord(formData); ElMessage.success('新增成功') }
- dialogVisible.value = false; loadData(); nextTick(() => renderMapScene())
- } catch (e) { const msg = e?.response?.data?.msg || e?.message || '操作失败'; ElMessage.error(msg) }
- }
- function handleDelete(row) {
- ElMessageBox.confirm('确认删除该维修记录?', '提示', { type: 'warning' }).then(async () => {
- try { await deleteRepairRecord(row.repairId); ElMessage.success('删除成功'); loadData(); nextTick(() => renderMapScene()) } catch (e) { ElMessage.error('删除失败') }
- }).catch(() => {})
- }
- // ==================== 生命周期 ====================
- let resizeTimer = null
- onMounted(async () => {
- await nextTick()
- await loadNetworkOptions()
- await loadData()
- whenMapReady(initMap)
- loadStats()
- window.addEventListener('resize', () => {
- clearTimeout(resizeTimer)
- resizeTimer = setTimeout(() => {
- resizeCharts()
- if (mapInstance) {
- try { mapInstance.resize() } catch (_) {}
- try { mapInstance.setViewport([new TdtMap.Point(centerPoint.lng, centerPoint.lat)], { zoom: mapInstance.getZoom() }) } catch (_) {}
- }
- }, 200)
- })
- })
- onBeforeUnmount(() => {
- window.removeEventListener('resize', resizeCharts)
- riskPieChart?.dispose(); repairTrendChart?.dispose()
- if (mapInstance) { try { mapInstance.clearOverlays() } catch (_) {}; mapInstance = null }
- })
- </script>
- <style scoped>
- .repair-container { padding: 16px; display: flex; flex-direction: column; gap: 16px; background: #f0f2f6; min-height: calc(100vh - 84px); }
- /* 多图上传样式 */
- .table-image {
- width: 40px;
- height: 40px;
- border-radius: 4px;
- cursor: pointer;
- }
- .image-list {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- margin-top: 8px;
- }
- .image-item {
- position: relative;
- width: 80px;
- height: 80px;
- border-radius: 4px;
- overflow: visible;
- }
- .image-thumb {
- width: 80px;
- height: 80px;
- object-fit: cover;
- border-radius: 4px;
- border: 1px solid #dcdfe6;
- cursor: pointer;
- }
- .image-thumb:hover {
- border-color: #409eff;
- }
- .image-remove {
- position: absolute;
- top: -6px;
- right: -6px;
- width: 18px;
- height: 18px;
- background: #f56c6c;
- color: #fff;
- border-radius: 50%;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 12px;
- z-index: 1;
- }
- .image-remove:hover {
- background: #e64a4a;
- }
- /* ========== 上半部分 ========== */
- .top-section { display: flex; gap: 16px; align-items: stretch; }
- .topo-card { flex: 1; min-width: 0; display: flex; flex-direction: column; }
- .charts-column { width: 340px; flex-shrink: 0; display: flex; flex-direction: column; gap: 16px; }
- .chart-card { flex: 1; }
- .card-title { font-size: 15px; font-weight: 700; color: #1f2d3d; }
- .topo-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; }
- .topo-legend { display: flex; gap: 14px; flex-wrap: wrap; }
- .legend-item { font-size: 11px; color: #606a78; display: flex; align-items: center; gap: 5px; }
- .legend-line { display: inline-block; width: 24px; height: 3px; border-radius: 2px; }
- .legend-line.high { background: #e63946; }
- .legend-line.medium { background: #f77f00; }
- .legend-line.low { background: #2a9d8f; }
- .legend-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
- .legend-dot.repaired { background: #f59e0b; border: 2px solid #fff; box-shadow: 0 0 4px rgba(245,158,11,0.5); }
- .legend-dot.point { background: #3b82f6; border: 2px solid #fff; }
- .map-wrapper { position: relative; width: 100%; height: 100%; min-height: 460px; border-radius: 6px; overflow: hidden; }
- .tdt-map { width: 100%; height: 100%; min-height: 460px; }
- .map-error { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: #fafbfc; color: #e63946; font-size: 14px; font-weight: 600; }
- .topo-card { display: flex; flex-direction: column; }
- :deep(.topo-card .el-card__body) { flex: 1; display: flex; padding: 0; overflow: hidden; }
- /* ========== 表格 ========== */
- .table-toolbar { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; }
- .search-form { margin-bottom: 0; }
- :deep(.search-form .el-form-item) { margin-bottom: 0; }
- :deep(.el-card__header) { padding: 12px 18px; border-bottom: 1px solid #f0f2f6; flex-shrink: 0; }
- :deep(.topo-card .el-card__body) { flex: 1; display: flex; padding: 0; overflow: hidden; }
- :deep(.chart-card .el-card__body) { padding: 12px; }
- :deep(.table-card .el-card__body) { padding: 12px; }
- @media (max-width: 1400px) {
- .top-section { flex-direction: column; }
- .map-wrapper { height: 400px; }
- .charts-column { width: 100%; flex-direction: row; }
- .charts-column .chart-card { flex: 1; min-width: 300px; }
- }
- </style>
|