index.vue 7.6 KB

12345678910111213141516171819202122232425
  1. <template>
  2. <div class="statistics-page"><section class="page-head"><div><span class="eyebrow">SERVICE QUALITY</span><h1>处置时效分析</h1><p>聚焦已完成处置的响应速度,识别低效专项。</p></div><el-button type="primary" :loading="loading" @click="loadData"><el-icon><Refresh /></el-icon>刷新统计</el-button></section>
  3. <el-card class="filter-card" shadow="never"><el-date-picker v-model="dateRange" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" /><el-input v-model="warningSpecial" clearable placeholder="输入预警专项" class="special-input" /><el-select v-model="sortType" class="sort-select"><el-option label="时效从长到短" value="desc" /><el-option label="时效从短到长" value="asc" /></el-select><el-button type="primary" @click="loadData">查询</el-button></el-card>
  4. <el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon closable @close="errorMessage = ''" />
  5. <div class="metric-grid"><div v-for="metric in metrics" :key="metric.label" class="metric-card"><span>{{ metric.label }}</span><strong>{{ metric.value }}</strong><small>{{ metric.note }}</small></div></div>
  6. <div class="chart-grid"><el-card shadow="never"><template #header><div class="card-title"><span>处置时效区间</span><em>小时</em></div></template><div ref="rangeRef" class="chart-box" /></el-card><el-card shadow="never"><template #header><div class="card-title"><span>专项平均处置时效</span><em>高时效优先督导</em></div></template><div ref="specialRef" class="chart-box" /></el-card></div>
  7. <el-card shadow="never" class="table-card"><template #header><div class="card-title"><span>已处置预警明细</span><em>{{ total }} 条</em></div></template><el-table v-loading="loading" :data="rows" stripe><el-table-column prop="warningNo" label="预警编号" min-width="150"/><el-table-column prop="warningName" label="预警事项" min-width="180" show-overflow-tooltip/><el-table-column label="预警专项" min-width="140"><template #default="{row}">{{ row.warning_special || row.warningSpecial || '-' }}</template></el-table-column><el-table-column label="发布时间" min-width="160"><template #default="{row}">{{ row.publish_time || row.publishTime || '-' }}</template></el-table-column><el-table-column label="处置时长" width="120"><template #default="{row}"><b>{{ row.dispose_hours ?? row.disposeHours ?? '-' }}</b> 小时</template></el-table-column><el-table-column label="状态" width="100"><template #default="{row}"><el-tag type="success">{{ row.status === 'CLOSED' ? '已解除' : '已处置' }}</el-tag></template></el-table-column></el-table><pagination v-show="total > 0" v-model:page="query.pageNum" v-model:limit="query.pageSize" :total="total" @pagination="loadPage" /></el-card>
  8. </div>
  9. </template>
  10. <script setup name="WarningDisposeEfficiency">
  11. import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
  12. import { ElMessage } from 'element-plus'
  13. import { Refresh } from '@element-plus/icons-vue'
  14. import * as echarts from 'echarts'
  15. import { getDisposeEfficiencyOverview, getDisposeEfficiencyRange, getDisposeEfficiencySpecial, pageDisposeEfficiency } from '@/api/warning/statistics'
  16. import { defaultRange, rangeParams, responseData, rowsOf, safeNumber, totalOf } from '../statisticsShared'
  17. const dateRange=ref(defaultRange()); const warningSpecial=ref(''); const sortType=ref('desc'); const loading=ref(false); const errorMessage=ref(''); const overview=ref({}); const rangeRows=ref([]); const specialRows=ref([]); const rows=ref([]); const total=ref(0); const query=ref({pageNum:1,pageSize:10}); const rangeRef=ref(); const specialRef=ref(); let rangeChart; let specialChart
  18. const value = (key, fallback='-') => overview.value[key] ?? overview.value[key.replace(/_([a-z])/g, (_,c)=>c.toUpperCase())] ?? fallback
  19. const metrics=computed(()=>[{label:'平均处置时长',value:safeNumber(value('avg_hours'),2),note:'小时'},{label:'最长处置时长',value:safeNumber(value('max_hours'),2),note:'小时'},{label:'最短处置时长',value:safeNumber(value('min_hours'),2),note:'小时'},{label:'完成处置数',value:value('total_count',0),note:'已处置/已解除'}])
  20. function renderCharts(){if(!rangeRef.value||!specialRef.value)return;rangeChart ||= echarts.init(rangeRef.value);specialChart ||= echarts.init(specialRef.value);rangeChart.setOption({tooltip:{trigger:'axis'},grid:{left:45,right:18,top:22,bottom:38},xAxis:{type:'category',data:rangeRows.value.map(r=>r.time_range||r.timeRange)},yAxis:{type:'value',minInterval:1},series:[{type:'bar',barWidth:34,data:rangeRows.value.map(r=>Number(r.warning_count??r.warningCount??0)),itemStyle:{color:'#2563eb',borderRadius:[6,6,0,0]}}]});specialChart.setOption({tooltip:{trigger:'axis'},grid:{left:55,right:18,top:22,bottom:54},xAxis:{type:'category',data:specialRows.value.map(r=>r.warning_special||r.warningSpecial||'未设置'),axisLabel:{rotate:25}},yAxis:{type:'value',name:'小时'},series:[{type:'bar',barWidth:28,data:specialRows.value.map(r=>Number(r.avg_dispose_hours??r.avgDisposeHours??0)),itemStyle:{color:'#0f766e',borderRadius:[6,6,0,0]}}]})}
  21. async function loadData(){loading.value=true;errorMessage.value='';try{const params={...rangeParams(dateRange.value),warningSpecial:warningSpecial.value||undefined};const [a,b,c]=await Promise.all([getDisposeEfficiencyOverview(params),getDisposeEfficiencyRange(params),getDisposeEfficiencySpecial(params)]);overview.value=responseData(a,{});rangeRows.value=responseData(b,[]);specialRows.value=responseData(c,[]);await loadPage();await nextTick();renderCharts()}catch(error){errorMessage.value=error?.message||'统计数据加载失败';ElMessage.error(errorMessage.value)}finally{loading.value=false}}
  22. async function loadPage(){const res=await pageDisposeEfficiency({...rangeParams(dateRange.value),warningSpecial:warningSpecial.value||undefined,sortType:sortType.value,...query.value});rows.value=rowsOf(res).map(row=>({...row,warningNo:row.warningNo??row.warning_no,warningName:row.warningName??row.warning_name}));total.value=totalOf(res)}
  23. function resize(){rangeChart?.resize();specialChart?.resize()};onMounted(()=>{loadData();window.addEventListener('resize',resize)});onUnmounted(()=>{window.removeEventListener('resize',resize);rangeChart?.dispose();specialChart?.dispose()})
  24. </script>
  25. <style scoped lang="scss">.statistics-page{padding:22px;background:#f4f7fb;min-height:calc(100vh - 84px);color:#172b4d}.page-head{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:18px}.eyebrow{font-size:11px;letter-spacing:2px;color:#0f766e}.page-head h1{margin:6px 0 4px;font-size:26px}.page-head p{margin:0;color:#718096}.filter-card{display:flex;gap:12px;align-items:center;margin-bottom:16px}.special-input{width:190px}.sort-select{width:150px}.metric-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:16px}.metric-card{padding:18px 20px;background:#fff;border:1px solid #e6ebf2;border-radius:8px;display:flex;flex-direction:column;gap:8px}.metric-card span{font-size:13px;color:#718096}.metric-card strong{font-size:26px;color:#172b4d}.metric-card small{color:#9aa7b8}.chart-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px}.card-title{display:flex;justify-content:space-between;font-weight:600}.card-title em{font-style:normal;font-size:12px;color:#94a3b8;font-weight:400}.chart-box{height:290px}.table-card{margin-bottom:20px}@media(max-width:900px){.metric-grid,.chart-grid{grid-template-columns:1fr 1fr}.filter-card{flex-wrap:wrap}}@media(max-width:600px){.metric-grid,.chart-grid{grid-template-columns:1fr}.filter-card>*{width:100%!important}.page-head{align-items:flex-start;gap:12px;flex-direction:column}}</style>