This commit is contained in:
huangchenhao 2026-04-08 15:47:59 +08:00
commit 4a4f6107ab
47 changed files with 3136 additions and 2728 deletions

View File

@ -34,7 +34,7 @@ import { Field, Popup, Picker } from 'vant'
const props = defineProps({ const props = defineProps({
// (v-model) // (v-model)
modelValue: { modelValue: {
type: [String, Number], type: [String, Number, Boolean],
default: null default: null
}, },
// [{ label: '', value: '' }] // [{ label: '', value: '' }]

View File

@ -34,7 +34,7 @@
<EmptyBox v-if="!loading && list.length === 0" :placeholder="emptyText" /> <EmptyBox v-if="!loading && list.length === 0" :placeholder="emptyText" />
</div> </div>
<van-button type="primary" class="footer-btn" icon="plus" @click="handleAdd"> 冰雪填报 </van-button> <van-button type="primary" class="footer-btn" @click="handleAdd"> 灾害填报 </van-button>
<!-- 筛选组件v-model 绑定选中的值visible 控制显示隐藏 --> <!-- 筛选组件v-model 绑定选中的值visible 控制显示隐藏 -->
<TagFilter <TagFilter
@ -56,7 +56,7 @@ import CardItem from '@/components/CardItem.vue'
import EmptyBox from '@/components/EmptyBox.vue' import EmptyBox from '@/components/EmptyBox.vue'
import CurrentSite from '@/components/CurrentSite.vue' import CurrentSite from '@/components/CurrentSite.vue'
import TagFilter from '@/components/TagFilter.vue' import TagFilter from '@/components/TagFilter.vue'
import mockDataJSON from './mockData.json' import { request } from "@shared/utils/request";
const router = useRouter() const router = useRouter()
@ -111,54 +111,21 @@ const getDisasterList = async (keyword = '', disasterType = 'all') => {
loading.value = true loading.value = true
try { try {
// TODO: const result = await request({
// const response = await fetch('/api/disaster/list', { url: '/snow-ops-platform/water-damage/list',
// method: 'POST', method: 'get',
// headers: { params: {
// 'Content-Type': 'application/json' keyword: keyword.trim(),
// }, disasterType: disasterType === 'all' ? '' : disasterType
// body: JSON.stringify({ }
// keyword: keyword.trim(), })
// disasterType: disasterType === 'all' ? '' : disasterType if (result?.data?.records) {
// }) list.value = result.data.records
// })
// const result = await response.json()
// if (result.code === 200) {
// list.value = result.data
// } else {
// showToast(result.message || '')
// list.value = []
// }
// ========== ==========
await new Promise((resolve) => setTimeout(resolve, 500))
const mockData = mockDataJSON
let filteredData = [...mockData]
if (keyword) {
filteredData = filteredData.filter((item) =>
item.title.toLowerCase().includes(keyword.toLowerCase())
)
}
if (disasterType !== 'all') {
filteredData = filteredData.filter((item) =>
item.disasterType === disasterType
)
}
list.value = filteredData
if (keyword && filteredData.length === 0) {
emptyText.value = '未搜索到相关灾毁信息'
} else if (disasterType !== 'all' && filteredData.length === 0) {
const typeLabel = disasterTypes.find(t => t.value === disasterType)?.label || disasterType
emptyText.value = `暂无${typeLabel}类型灾毁信息`
} else { } else {
emptyText.value = '暂无相关灾毁信息' showToast(result.message || '获取数据失败')
list.value = []
} }
// ========== ==========
} catch (error) { } catch (error) {
console.error('获取灾毁列表失败:', error) console.error('获取灾毁列表失败:', error)
showToast('获取数据失败,请稍后重试') showToast('获取数据失败,请稍后重试')

View File

@ -15,7 +15,6 @@
<WaterDisaster <WaterDisaster
v-if="eventType === 'water'" v-if="eventType === 'water'"
ref="waterDisasterRef" ref="waterDisasterRef"
v-model="waterDisasterData"
/> />
<!-- 冰雪灾害表单待实现 --> <!-- 冰雪灾害表单待实现 -->
@ -31,13 +30,15 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showToast, showSuccessToast, showFailToast } from 'vant' import { showToast, showSuccessToast, showFailToast } from 'vant'
import PageContainer from '@/components/PageContainer.vue' import PageContainer from '@/components/PageContainer.vue'
import CurrentSite from '@/components/CurrentSite.vue' import CurrentSite from '@/components/CurrentSite.vue'
import PanelItem from '@/components/PanelItem.vue' import PanelItem from '@/components/PanelItem.vue'
import WaterDisaster from './WaterDisaster.vue' import WaterDisaster from './WaterDisaster/WaterDisaster.vue'
import { request } from "@shared/utils/request";
import mockFormData from './waterDisasterFormData.json'
const router = useRouter() const router = useRouter()
@ -45,7 +46,7 @@ const router = useRouter()
const eventType = ref('water') const eventType = ref('water')
// //
const waterDisasterData = ref({}) const formData = ref(mockFormData)
const waterDisasterRef = ref(null) const waterDisasterRef = ref(null)
const submitting = ref(false) const submitting = ref(false)
@ -73,12 +74,16 @@ const handleSubmit = async () => {
// //
const submitData = { const submitData = {
eventType: eventType.value,
...formData, ...formData,
// //
} }
const res = await request({
url: '/snow-ops-platform/water-damage/addOrUpdate',
method: 'post',
data: submitData
})
console.log('提交数据:', submitData)
// //
await new Promise(resolve => setTimeout(resolve, 1000)) await new Promise(resolve => setTimeout(resolve, 1000))
@ -95,6 +100,10 @@ const handleSubmit = async () => {
submitting.value = false submitting.value = false
} }
} }
onMounted(() => {
waterDisasterRef.value.initFormData(formData.value)
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,532 +0,0 @@
<template>
<div class="water-disaster">
<!-- 基本信息 -->
<PanelItem title="基本信息">
<van-form>
<!-- 路况类别 -->
<BasePicker v-model="formData.roadCondition" :options="roadConditionOptions" label="路况类别" placeholder="请选择" />
<!-- 是否阻断 -->
<BasePicker v-model="formData.isBlocked" :options="blockedOptions" label="是否阻断" placeholder="请选择" />
<!-- 抢修进度 -->
<BasePicker v-model="formData.repairProgress" :options="repairProgressOptions" label="抢修进度" placeholder="请选择" />
<!-- 水毁处数 -->
<van-field v-model="formData.waterDamageCount" label="水毁处数" placeholder="请填写" type="number" />
<!-- 阻断里程 -->
<van-field v-model="formData.blockedMileage" label="阻断里程" placeholder="请填写" type="digit">
<template #button>
<span class="field-unit">公里</span>
</template>
</van-field>
<!-- 发生时间 -->
<BaseDatePicker v-model="formData.occurTime" label="发生时间" placeholder="请选择时间" :columnsType="['year', 'month', 'day', 'hour', 'minute']" />
<div class="calibrate-time-btn" @click="calibrateTime">
<van-icon name="replay" />
<span>校准时间</span>
</div>
<!-- 线路编号 -->
<van-field v-model="formData.lineCode" label="线路编号" placeholder="请填写" />
<!-- 起点桩号 -->
<van-field v-model="formData.startPileNo" label="起点桩号(K)" placeholder="请填写" />
<!-- 起点桩经纬度 -->
<div class="coordinate-row">
<van-field v-model="formData.startLongitude" label="起点桩经度" placeholder="经度" class="coordinate-field" />
<van-field v-model="formData.startLatitude" label="起点桩纬度" placeholder="纬度" class="coordinate-field" />
</div>
<div class="calibrate-coord-btn" @click="calibrateStartCoord">
<van-icon name="location-o" />
<span>校准经纬度</span>
</div>
<!-- 止点桩号 -->
<van-field v-model="formData.endPileNo" label="止点桩号(K)" placeholder="请填写" />
<!-- 止点桩经纬度 -->
<div class="coordinate-row">
<van-field v-model="formData.endLongitude" label="止点桩经度" placeholder="经度" class="coordinate-field" />
<van-field v-model="formData.endLatitude" label="止点桩纬度" placeholder="纬度" class="coordinate-field" />
</div>
<div class="calibrate-coord-btn" @click="calibrateEndCoord">
<van-icon name="location-o" />
<span>校准经纬度</span>
</div>
<!-- 路况位置 -->
<van-field v-model="formData.roadLocation" label="路况位置" placeholder="请填写" />
<!-- 阻断点小地名 -->
<van-field v-model="formData.smallPlaceName" label="阻断点小地名" placeholder="请填写" />
</van-form>
</PanelItem>
<!-- 处置情况 -->
<PanelItem title="处置情况">
<div class="disposal-measures">
<span class="measures-label">处置措施</span>
<div class="measures-options">
<van-checkbox-group v-model="formData.disposalMeasures" direction="horizontal">
<van-checkbox name="halfClose">半幅封闭</van-checkbox>
<van-checkbox name="fullClose">全副封闭</van-checkbox>
<van-checkbox name="bypass">便道通行</van-checkbox>
<van-checkbox name="normal">正常通行</van-checkbox>
</van-checkbox-group>
</div>
</div>
<!-- 预计恢复时间 -->
<BaseDatePicker v-model="formData.estimatedRecoverTime" label="预计恢复时间" placeholder="请选择时间" :min-date="minDate" :max-date="maxDate" type="datetime" />
<!-- 实际恢复时间 -->
<BaseDatePicker v-model="formData.actualRecoverTime" label="实际恢复时间" placeholder="请选择时间" :min-date="minDate" :max-date="maxDate" type="datetime" />
</PanelItem>
<!-- 人员车辆 -->
<PanelItem title="人员车辆">
<van-form>
<van-field v-model="formData.injuredCount" label="受伤人员" placeholder="请填写" type="number">
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<van-field v-model="formData.deathCount" label="死亡人员" placeholder="请填写" type="number">
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<van-field v-model="formData.strandedPeople" label="滞留人员" placeholder="请填写" type="number">
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<van-field v-model="formData.damagedVehicles" label="损坏车辆" placeholder="请填写" type="number">
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<van-field v-model="formData.strandedVehicles" label="滞留车辆" placeholder="请填写" type="number">
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
</van-form>
</PanelItem>
<!-- 灾毁损失 -->
<PanelItem title="灾毁损失">
<div class="loss-row">
<span class="loss-label">塌方及损失</span>
<span class="loss-value">{{ formData.collapseLoss }}/万元</span>
</div>
<van-button size="small" block type="primary" plain @click="showLossDialog = true">添加损失</van-button>
<van-field v-model="formData.handlingSituation" label="处理情况" placeholder="请填写(选填)" />
<van-field v-model="formData.totalLossAmount" label="损失总金额" placeholder="请填写(选填)" type="digit">
<template #button>
<span class="field-unit">万元</span>
</template>
</van-field>
</PanelItem>
<PanelItem>
<van-field v-model="formData.machineryInput" label="已投机械" placeholder="请填写" type="digit">
<template #button>
<span class="field-unit">/</span>
</template>
</van-field>
<van-field v-model="formData.laborInput" label="已投入力" placeholder="请填写" type="number">
<template #button>
<span class="field-unit">人次</span>
</template>
</van-field>
<van-field v-model="formData.fundsInput" label="已投资金" placeholder="请填写" type="digit">
<template #button>
<span class="field-unit">万元</span>
</template>
</van-field>
<van-field v-model="formData.siteDescription" label="现场描述" placeholder="请填写" type="textarea" rows="2" autosize />
<van-field label="附件">
<template #input>
</template>
</van-field>
</PanelItem>
<!-- 附件 -->
<!-- <PanelItem title="附件">
<div class="attachment-tip">图片只能上传jpg/png文件且不超过500kb视频仅支持20s内的视频</div>
<div class="upload-area">
<van-uploader
v-model="formData.imageFiles"
:after-read="afterImageRead"
accept="image/jpeg,image/png"
:max-size="500 * 1024"
@oversize="onOversize"
multiple
:max-count="9"
>
<div class="upload-btn">
<van-icon name="photo-o" size="24" />
<span>上传图片</span>
</div>
</van-uploader>
<van-uploader v-model="formData.videoFile" :after-read="afterVideoRead" accept="video/*" :max-size="20 * 1024 * 1024" @oversize="onVideoOversize">
<div class="upload-btn">
<van-icon name="video-o" size="24" />
<span>上传视频</span>
</div>
</van-uploader>
</div>
<div v-if="formData.videoFile.length > 0 && formData.videoFile[0].content" class="video-preview">
<video :src="formData.videoFile[0].content" controls style="width: 100%; max-height: 200px"></video>
</div>
</PanelItem> -->
<!-- 损失计算弹窗 -->
<van-dialog v-model:show="showLossDialog" title="添加塌方损失" show-cancel-button @confirm="confirmLoss" @cancel="showLossDialog = false">
<div class="loss-dialog-content">
<van-field v-model="lossForm.length" label="长度(m)" type="number" placeholder="请输入长度" />
<van-field v-model="lossForm.width" label="宽度(m)" type="number" placeholder="请输入宽度" />
<van-field v-model="lossForm.height" label="高度(m)" type="number" placeholder="请输入高度" />
<van-field v-model="lossForm.unitPrice" label="单价(元/m³)" type="number" placeholder="请输入单价" />
<div class="loss-calc-result">预估塌方量{{ calculatedVolume }} </div>
<div class="loss-calc-result">预估损失{{ calculatedLoss }} 万元</div>
</div>
</van-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch } from 'vue'
import { showToast, showFailToast } from 'vant'
import PanelItem from '@/components/PanelItem.vue'
import BasePicker from '@/components/BasePicker.vue'
import BaseDatePicker from '@/components/BaseDatePicker.vue'
// props
const props = defineProps({
modelValue: {
type: Object,
default: () => ({})
}
})
// emits
const emit = defineEmits(['update:modelValue', 'submit'])
//
const formData = reactive({
roadCondition: '',
isBlocked: '',
repairProgress: '',
waterDamageCount: '',
blockedMileage: '',
occurTime: '',
lineCode: '',
startPileNo: '',
startLongitude: '',
startLatitude: '',
endPileNo: '',
endLongitude: '',
endLatitude: '',
roadLocation: '',
smallPlaceName: '',
disposalMeasures: [],
estimatedRecoverTime: '',
actualRecoverTime: '',
injuredCount: '',
deathCount: '',
strandedPeople: '',
damagedVehicles: '',
strandedVehicles: '',
collapseLoss: '0',
handlingSituation: '',
totalLossAmount: '',
machineryInput: '',
laborInput: '',
fundsInput: '',
siteDescription: '',
imageFiles: [],
videoFile: []
})
//
const showLossDialog = ref(false)
// BasePicker
const roadConditionOptions = [
{ label: '高速公路', value: '高速公路' },
{ label: '国道', value: '国道' },
{ label: '省道', value: '省道' },
{ label: '县道', value: '县道' },
{ label: '乡道', value: '乡道' },
{ label: '村道', value: '村道' }
]
const blockedOptions = [
{ label: '是', value: '是' },
{ label: '否', value: '否' }
]
const repairProgressOptions = [
{ label: '未开始', value: '未开始' },
{ label: '进行中', value: '进行中' },
{ label: '已抢通', value: '已抢通' },
{ label: '已修复', value: '已修复' }
]
//
const minDate = new Date(2020, 0, 1)
const maxDate = new Date(2030, 11, 31)
//
const lossForm = reactive({
length: '',
width: '',
height: '',
unitPrice: ''
})
//
const calculatedVolume = computed(() => {
const l = parseFloat(lossForm.length) || 0
const w = parseFloat(lossForm.width) || 0
const h = parseFloat(lossForm.height) || 0
return (l * w * h).toFixed(2)
})
//
const calculatedLoss = computed(() => {
const volume = parseFloat(calculatedVolume.value) || 0
const price = parseFloat(lossForm.unitPrice) || 0
const lossYuan = volume * price
return (lossYuan / 10000).toFixed(2)
})
//
watch(
() => props.modelValue,
(newVal) => {
if (newVal && Object.keys(newVal).length > 0) {
Object.assign(formData, newVal)
}
},
{ immediate: true, deep: true }
)
//
watch(
formData,
() => {
emit('update:modelValue', { ...formData })
},
{ deep: true }
)
//
const calibrateTime = () => {
const now = new Date()
const formatted = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
formData.occurTime = formatted
showToast('时间已校准为当前时间')
}
//
const calibrateStartCoord = () => {
formData.startLongitude = '108.41763025'
formData.startLatitude = '108.41763025'
showToast('起点经纬度已校准')
}
//
const calibrateEndCoord = () => {
formData.endLongitude = '108.41763025'
formData.endLatitude = '108.41763025'
showToast('止点经纬度已校准')
}
//
const confirmLoss = () => {
const lossValue = calculatedLoss.value
if (parseFloat(lossValue) > 0) {
formData.collapseLoss = lossValue
if (formData.totalLossAmount) {
const currentTotal = parseFloat(formData.totalLossAmount) || 0
formData.totalLossAmount = (currentTotal + parseFloat(lossValue)).toFixed(2)
} else {
formData.totalLossAmount = lossValue
}
} else {
showToast('请填写有效的长宽高和单价')
}
lossForm.length = ''
lossForm.width = ''
lossForm.height = ''
lossForm.unitPrice = ''
showLossDialog.value = false
}
//
const afterImageRead = (file) => {
console.log('图片上传:', file)
}
const onOversize = () => {
showFailToast('图片大小不能超过500KB')
}
const afterVideoRead = (file) => {
console.log('视频上传:', file)
}
const onVideoOversize = () => {
showFailToast('视频大小不能超过20MB')
}
//
const validate = () => {
if (!formData.occurTime) {
showToast('请填写发生时间')
return false
}
if (!formData.lineCode) {
showToast('请填写线路编号')
return false
}
return true
}
//
const getFormData = () => {
return { ...formData }
}
//
defineExpose({
validate,
getFormData
})
</script>
<style lang="scss" scoped>
.water-disaster {
.coordinate-row {
display: flex;
gap: 8px;
.coordinate-field {
flex: 1;
}
}
.calibrate-time-btn,
.calibrate-coord-btn {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #1989fa;
margin: 8px 0 8px 12px;
padding: 4px 8px;
background: #f0f7ff;
border-radius: 20px;
cursor: pointer;
width: fit-content;
}
.field-unit {
color: #969799;
font-size: 14px;
margin-left: 4px;
}
.disposal-measures {
margin-bottom: 16px;
.measures-label {
font-size: 14px;
color: #323233;
display: block;
margin-bottom: 8px;
}
.measures-options {
:deep(.van-checkbox-group) {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
:deep(.van-checkbox) {
margin-right: 0;
}
}
}
.loss-row {
display: flex;
align-items: center;
justify-content: space-between;
background: #f8f9fa;
padding: 10px 12px;
border-radius: 8px;
margin: 12px 0;
.loss-label {
font-size: 14px;
color: #323233;
}
.loss-value {
font-size: 16px;
font-weight: 600;
color: #ee0a24;
}
}
.attachment-tip {
font-size: 12px;
color: #969799;
margin-bottom: 12px;
}
.upload-area {
display: flex;
gap: 16px;
flex-wrap: wrap;
.upload-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
background: #f8f9fa;
border: 1px dashed #dcdee0;
border-radius: 8px;
gap: 4px;
font-size: 12px;
color: #969799;
cursor: pointer;
}
}
.video-preview {
margin-top: 12px;
}
.loss-dialog-content {
padding: 8px 16px 16px;
.loss-calc-result {
font-size: 14px;
color: #1989fa;
margin-top: 12px;
text-align: center;
}
}
:deep(.van-field__label) {
width: 90px;
}
}
</style>

View File

@ -0,0 +1,159 @@
<template>
<!-- 损失计算弹窗 -->
<van-dialog
v-model:show="visible"
title="塌方损失信息"
show-cancel-button
@confirm="confirm"
@cancel="cancelLoss"
confirm-button-text="确定"
cancel-button-text="取消"
>
<div class="loss-dialog-content">
<!-- 塌方长 -->
<van-field v-model="formData.length" label="塌方长" placeholder="请填写长度" type="digit" clearable>
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<!-- 塌方宽 -->
<van-field v-model="formData.width" label="塌方宽" placeholder="请填写宽度" type="digit" clearable>
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<!-- 塌方高 -->
<van-field v-model="formData.height" label="塌方高" placeholder="请填写高度" type="digit" clearable>
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<!-- 单价 (0-2000) -->
<van-field v-model="formData.unitPrice" label="单价(0-2000元)" placeholder="请填写单价" type="digit" clearable>
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<van-field v-model="formData.totalPrice" label="塌方损失金额" placeholder="请填写金额" type="digit" clearable>
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
</div>
</van-dialog>
</template>
<script setup>
import { onMounted, ref, reactive, watch, computed } from 'vue'
import { showToast } from 'vant'
//
const visible = ref(false)
//
const formData = ref({
length: '',
width: '',
height: '',
unitPrice: ''
})
const totalPrice = computed(() => {
const l = parseFloat(formData.value.length)
const w = parseFloat(formData.value.width)
const h = parseFloat(formData.value.height)
const price = parseFloat(formData.value.unitPrice)
if (isNaN(l) || l <= 0) {
return 0
}
if (isNaN(w) || w <= 0) {
return 0
}
if (isNaN(h) || h <= 0) {
return 0
}
if (isNaN(price) || price < 0) {
return 0
}
if (price > 2000) {
return 0
}
return (l * w * h * price)
})
watch(totalPrice, ()=>{
formData.value.totalPrice = totalPrice.value
})
//
const show = () => {
visible.value = true
}
//
const validateForm = () => {
const l = parseFloat(formData.length)
const w = parseFloat(formData.width)
const h = parseFloat(formData.height)
const price = parseFloat(formData.unitPrice)
if (isNaN(l) || l <= 0) {
showToast('请填写有效的塌方长度')
return false
}
if (isNaN(w) || w <= 0) {
showToast('请填写有效的塌方宽度')
return false
}
if (isNaN(h) || h <= 0) {
showToast('请填写有效的塌方高度')
return false
}
if (isNaN(price) || price < 0) {
showToast('请填写有效的单价')
return false
}
if (price > 2000) {
showToast('单价不能超过2000元')
return false
}
return true
}
//
const confirm = () => {
if(!formData.value.totalPrice) {
showToast('请填写损失金额')
return
}
emit('confirm', formData.value.totalPrice)
//
resetForm()
visible.value = false
}
//
const cancelLoss = () => {
resetForm()
visible.value = false
}
//
const resetForm = () => {
formData.value.length = ''
formData.value.width = ''
formData.value.height = ''
formData.value.unitPrice = ''
formData.value.totalPrice
}
//
const emit = defineEmits(['confirm'])
defineExpose({
show
})
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,112 @@
<template>
<div class="loss-list">
<template v-for="item in modelValue">
<van-field v-model="item.totalAmount" :label="getItemLabel(item)" placeholder="请填写" type="digit" @click="cubeCalculateDialog.show()">
<template #button>
<span class="field-unit">/万元</span>
</template>
</van-field>
</template>
<van-button size="small" block type="primary" plain @click="addLoss">添加损失</van-button>
<CubeCalculateDialog ref="cubeCalculateDialog" />
<LossPicker ref="lossPicker" :options="options" @confirm="confirmAddLoss" />
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import CubeCalculateDialog from './CubeCalculateDialog.vue'
import { request } from '@shared/utils/request'
import LossPicker from './LossPicker.vue'
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: {
type: Object,
default: () => ({})
}
})
const lossPicker = ref(null)
const options = ref({})
//
const addLoss = () => {
lossPicker.value?.show()
}
const confirmAddLoss = (item) => {
emit('update:modelValue', [...props.modelValue, item])
}
const cubeCalculateDialog = ref(null)
const getItemLabel = (item) => {
const loss = options.value.loss?.find((loss) => loss.value === item.lossTypeId)
return loss?.text
}
const getLossDict = async (params) => {
const res = await request({
url: '/snow-ops-platform/water-damage/loss/typeAndInfo',
method: 'get',
params
})
options.value.loss = res.data.records.map((item)=>{
return {
text: item.lossTypeName,
value: item.lossTypeId,
item
}
})
}
onMounted(async () => {
await getLossDict()
})
</script>
<style scoped lang="scss">
.field-unit {
color: #969799;
font-size: 14px;
margin-left: 4px;
}
.loss-dialog-content {
padding: 16px;
:deep(.van-field) {
margin-bottom: 12px;
}
}
.calculation-preview {
background-color: #f7f8fa;
border-radius: 8px;
padding: 12px;
margin-top: 12px;
.preview-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
&:last-child {
margin-bottom: 0;
}
.preview-label {
color: #646566;
font-size: 14px;
}
.preview-value {
color: #ee0a24;
font-size: 14px;
font-weight: 500;
}
}
}
</style>

View File

@ -0,0 +1,47 @@
<template>
<van-popup v-model:show="showPicker" position="bottom" round>
<van-picker :columns="columns" :title="pickerTitle" show-toolbar @confirm="onConfirm" @cancel="showPicker = false" />
</van-popup>
</template>
<script setup>
import { onMounted, ref, computed } from 'vue'
const emit = defineEmits(['confirm'])
const props = defineProps({
options: {
type: Object,
default: () => ({})
}
})
const pickerTitle = ref("请选择损失类型")
const columns = computed(() => {
return props.options.loss || []
})
const showPicker = ref(false)
const show = () => {
showPicker.value = true
}
const clsoe = () => {
showPicker.value = false
}
const onConfirm = ({ selectedValues, selectedOptions }) => {
emit('confirm', selectedOptions[0].item)
showPicker.value = false
}
defineExpose({
show,
clsoe
})
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,524 @@
<template>
<div class="water-disaster">
<!-- 基本信息 -->
<PanelItem title="基本信息">
<van-form>
<!-- 路况类别 -->
<BasePicker v-model="formData.roadConditionType" :options="roadConditionOptions" label="路况类别" placeholder="请选择" />
<!-- 是否阻断 (event.isBlocked) -->
<BasePicker v-model="formData.event.isBlocked" :options="blockedOptions" label="是否阻断" placeholder="请选择" />
<!-- 抢修进度 (event.repairProgress) -->
<BasePicker v-model="formData.event.repairProgress" :options="repairProgressOptions" label="抢修进度" placeholder="请选择" />
<!-- 水毁处数 (event.damageCount) -->
<van-field v-model="formData.event.damageCount" label="水毁处数" placeholder="请填写" type="number" />
<!-- 阻断里程 (event.blockedMileage) -->
<van-field v-model="formData.event.blockedMileage" label="阻断里程" placeholder="请填写" type="digit">
<template #button>
<span class="field-unit">公里</span>
</template>
</van-field>
<!-- 发生时间 (顶层 occurTime) -->
<BaseDatePicker v-model="formData.occurTime" label="发生时间" placeholder="请选择时间" :columnsType="['year', 'month', 'day', 'hour', 'minute']" />
<div class="calibrate-time-btn" @click="calibrateTime">
<van-icon name="replay" />
<span>校准时间</span>
</div>
<!-- 线路编号 (顶层 routeNo) -->
<van-field v-model="formData.routeNo" label="线路编号" placeholder="请填写" />
<!-- 起点桩号 (event.startStakeNo) -->
<van-field v-model="formData.event.startStakeNo" label="起点桩号(K)" placeholder="请填写" />
<!-- 起点桩经纬度 (event.startStakeLng / startStakeLat) -->
<div class="coordinate-row">
<van-field v-model="formData.event.startStakeLng" label="起点桩经度" placeholder="经度" class="coordinate-field" />
<van-field v-model="formData.event.startStakeLat" label="起点桩纬度" placeholder="纬度" class="coordinate-field" />
</div>
<div class="calibrate-coord-btn" @click="calibrateStartCoord">
<van-icon name="location-o" />
<span>校准经纬度</span>
</div>
<!-- 止点桩号 (event.endStakeNo) -->
<van-field v-model="formData.event.endStakeNo" label="止点桩号(K)" placeholder="请填写" />
<!-- 止点桩经纬度 (event.endStakeLng / endStakeLat) -->
<div class="coordinate-row">
<van-field v-model="formData.event.endStakeLng" label="止点桩经度" placeholder="经度" class="coordinate-field" />
<van-field v-model="formData.event.endStakeLat" label="止点桩纬度" placeholder="纬度" class="coordinate-field" />
</div>
<div class="calibrate-coord-btn" @click="calibrateEndCoord">
<van-icon name="location-o" />
<span>校准经纬度</span>
</div>
<!-- 路况位置 (event.endStakeNo) -->
<van-field v-model="formData.occurLocation" label="路况位置" placeholder="请填写" />
<!-- 阻断点小地名 (event.blockedPointName) -->
<van-field v-model="formData.event.blockedPointName" label="阻断点小地名" placeholder="请填写" />
</van-form>
</PanelItem>
<!-- 处置情况 (report) -->
<PanelItem title="处置情况">
<div class="disposal-measures">
<span class="measures-label">处置措施</span>
<div class="measures-options">
<van-checkbox-group v-model="disposalMeasuresArray" direction="horizontal">
<van-checkbox name="halfClose">半幅封闭</van-checkbox>
<van-checkbox name="fullClose">全副封闭</van-checkbox>
<van-checkbox name="bypass">便道通行</van-checkbox>
<van-checkbox name="normal">正常通行</van-checkbox>
</van-checkbox-group>
</div>
</div>
<!-- 预计恢复时间 (report.expectRecoverTime) -->
<BaseDatePicker v-model="formData.report.expectRecoverTime" label="预计恢复时间" placeholder="请选择时间" :min-date="minDate" :max-date="maxDate" type="datetime" />
<!-- 实际恢复时间 (report.actualRecoverTime) -->
<BaseDatePicker v-model="formData.report.actualRecoverTime" label="实际恢复时间" placeholder="请选择时间" :min-date="minDate" :max-date="maxDate" type="datetime" />
</PanelItem>
<!-- 人员车辆 (report) -->
<PanelItem title="人员车辆">
<van-form>
<van-field v-model="formData.report.injuredCount" label="受伤人员" placeholder="请填写" type="number">
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<van-field v-model="formData.report.deadCount" label="死亡人员" placeholder="请填写" type="number">
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<van-field v-model="formData.report.strandedPersonCount" label="滞留人员" placeholder="请填写" type="number">
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<van-field v-model="formData.report.damagedVehicleCount" label="损坏车辆" placeholder="请填写" type="number">
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<van-field v-model="formData.report.strandedVehicleCount" label="滞留车辆" placeholder="请填写" type="number">
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
</van-form>
</PanelItem>
<!-- 灾毁损失 (lossList) -->
<PanelItem title="灾毁损失">
<LossList v-model="formData.lossList" />
<van-field v-model="formData.report.remark" label="处理情况" placeholder="请填写(选填)" />
<van-field v-model="formData.report.totalLossAmount" label="损失总金额" placeholder="请填写(选填)" type="digit">
<template #button>
<span class="field-unit">万元</span>
</template>
</van-field>
</PanelItem>
<!-- 投入资源 (report) -->
<PanelItem>
<van-field v-model="formData.report.investedMachinery" label="已投机械" placeholder="请填写" type="digit">
<template #button>
<span class="field-unit">/</span>
</template>
</van-field>
<van-field v-model="formData.report.investedManpower" label="已投入力" placeholder="请填写" type="number">
<template #button>
<span class="field-unit">人次</span>
</template>
</van-field>
<van-field v-model="formData.report.investedFunds" label="已投资金" placeholder="请填写" type="digit">
<template #button>
<span class="field-unit">万元</span>
</template>
</van-field>
<van-field v-model="formData.report.siteDescription" label="现场描述" placeholder="请填写" type="textarea" rows="2" autosize />
</PanelItem>
<!-- 附件 (fileList) -->
<!-- <PanelItem title="附件">
<div class="attachment-tip">图片只能上传jpg/png文件且不超过500kb视频仅支持20s内的视频</div>
<div class="upload-area">
<van-uploader v-model="imageFileList" :after-read="afterImageRead" accept="image/jpeg,image/png" :max-size="500 * 1024" @oversize="onOversize" multiple :max-count="9">
<div class="upload-btn">
<van-icon name="photo-o" size="24" />
<span>上传图片</span>
</div>
</van-uploader>
<van-uploader v-model="videoFileList" :after-read="afterVideoRead" accept="video/*" :max-size="20 * 1024 * 1024" @oversize="onVideoOversize">
<div class="upload-btn">
<van-icon name="video-o" size="24" />
<span>上传视频</span>
</div>
</van-uploader>
</div>
<div v-if="videoFileList.length > 0 && videoFileList[0].content" class="video-preview">
<video :src="videoFileList[0].content" controls style="width: 100%; max-height: 200px"></video>
</div>
</PanelItem> -->
<PanelItem>
<!-- 是否需要恢复重建 (event.needsRecovery) -->
<BasePicker v-model="formData.event.needsRecovery" :options="needsRecoveryOptions" label="是否需要恢复重建" placeholder="请选择" />
<!-- 恢复重建预估费用 (event.estimatedRecoveryCost) -->
<van-field v-model="formData.event.estimatedRecoveryCost" label="恢复重建预估费用" placeholder="请填写" type="digit">
<template #button>
<span class="field-unit">万元</span>
</template>
</van-field>
</PanelItem>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch } from 'vue'
import { showToast, showFailToast } from 'vant'
import PanelItem from '@/components/PanelItem.vue'
import BasePicker from '@/components/BasePicker.vue'
import BaseDatePicker from '@/components/BaseDatePicker.vue'
import LossList from './LossList.vue'
//
const disposalMeasuresArray = ref([])
//
const imageFileList = ref([])
const videoFileList = ref([])
// - Request
const formData = reactive({
//
occurLocation: '', //
occurTime: '', //
roadConditionType: '', //
routeNo: '', // 线
// event
event: {
blockedMileage: '', //
blockedPointName: '', //
contactPerson: '', //
contactPhone: '', //
damageCount: '', //
district: '', //
endStakeLat: '', //
endStakeLng: '', //
endStakeNo: '', //
estimatedRecoveryCost: '', //
inspectionMileage: '', //
isBlocked: '', //
needsRecovery: '', //
repairProgress: '', //
reporterUnit: '', //
startStakeLat: '', //
startStakeLng: '', //
startStakeNo: '' //
},
// report
report: {
actualRecoverTime: '', //
damagedVehicleCount: '', //
deadCount: '', //
disposalMeasures: '', //
expectRecoverTime: '', //
injuredCount: '', //
investedFunds: '', //
investedMachinery: '', //
investedManpower: '', //
remark: '', // /
siteDescription: '', //
strandedPersonCount: '', //
strandedVehicleCount: '', //
totalLossAmount: '' //
},
// lossList
lossList: [],
// fileList
fileList: []
})
// report.disposalMeasures
watch(
disposalMeasuresArray,
(newVal) => {
formData.report.disposalMeasures = newVal.join(',')
},
{ deep: true }
)
// fileList
watch(
imageFileList,
(newVal) => {
//
formData.fileList = [
...imageFileList.value.map((f) => ({
fileName: f.file?.name || '',
fileSize: f.file?.size || 0,
fileType: 1, // 1-
fileUrl: f.content || ''
})),
...videoFileList.value.map((f) => ({
fileName: f.file?.name || '',
fileSize: f.file?.size || 0,
fileType: 2, // 2-
fileUrl: f.content || ''
}))
]
},
{ deep: true }
)
watch(
videoFileList,
(newVal) => {
formData.fileList = [
...imageFileList.value.map((f) => ({
fileName: f.file?.name || '',
fileSize: f.file?.size || 0,
fileType: 1,
fileUrl: f.content || ''
})),
...newVal.map((f) => ({
fileName: f.file?.name || '',
fileSize: f.file?.size || 0,
fileType: 2,
fileUrl: f.content || ''
}))
]
},
{ deep: true }
)
// report.disposalMeasures
watch(
() => formData.report.disposalMeasures,
(newVal) => {
if (newVal && typeof newVal === 'string') {
disposalMeasuresArray.value = newVal.split(',').filter(Boolean)
}
},
{ immediate: true }
)
// BasePicker
const roadConditionOptions = [
{ label: '高速公路', value: '高速公路' },
{ label: '国道', value: '国道' },
{ label: '省道', value: '省道' },
{ label: '县道', value: '县道' },
{ label: '乡道', value: '乡道' },
{ label: '村道', value: '村道' }
]
const blockedOptions = [
{ label: '是', value: true },
{ label: '否', value: false }
]
const repairProgressOptions = [
{ label: '未抢修', value: '未抢修' },
{ label: '抢修中', value: '抢修中' },
{ label: '已完成', value: '已完成' },
]
const needsRecoveryOptions = [
{ label: '是', value: true },
{ label: '否', value: false }
]
//
const minDate = new Date(2020, 0, 1)
const maxDate = new Date(2030, 11, 31)
const initFormData = (newVal) => {
if (newVal && Object.keys(newVal).length > 0) {
//
Object.assign(formData, {
occurLocation: newVal.occurLocation || '',
occurTime: newVal.occurTime || '',
roadConditionType: newVal.roadConditionType || '',
routeNo: newVal.routeNo || '',
event: { ...formData.event, ...(newVal.event || {}) },
report: { ...formData.report, ...(newVal.report || {}) },
lossList: newVal.lossList || [],
fileList: newVal.fileList || []
})
//
if (newVal.report?.disposalMeasures) {
disposalMeasuresArray.value = newVal.report.disposalMeasures.split(',').filter(Boolean)
}
}
}
//
const calibrateTime = () => {
const now = new Date()
const formatted = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
formData.occurTime = formatted
showToast('时间已校准为当前时间')
}
//
const calibrateStartCoord = () => {
formData.event.startStakeLng = '108.41763025'
formData.event.startStakeLat = '108.41763025'
showToast('起点经纬度已校准')
}
//
const calibrateEndCoord = () => {
formData.event.endStakeLng = '108.41763025'
formData.event.endStakeLat = '108.41763025'
showToast('止点经纬度已校准')
}
//
const afterImageRead = (file) => {
console.log('图片上传:', file)
}
const onOversize = () => {
showFailToast('图片大小不能超过500KB')
}
const afterVideoRead = (file) => {
console.log('视频上传:', file)
}
const onVideoOversize = () => {
showFailToast('视频大小不能超过20MB')
}
//
const validate = () => {
if (!formData.occurTime) {
showToast('请填写发生时间')
return false
}
if (!formData.routeNo) {
showToast('请填写线路编号')
return false
}
return true
}
//
const getFormData = () => {
return { ...formData }
}
//
defineExpose({
validate,
initFormData,
getFormData
})
</script>
<style lang="scss" scoped>
.water-disaster {
.coordinate-row {
display: flex;
gap: 8px;
.coordinate-field {
flex: 1;
}
}
.calibrate-time-btn,
.calibrate-coord-btn {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #1989fa;
margin: 8px 0 8px 12px;
padding: 4px 8px;
background: #f0f7ff;
border-radius: 20px;
cursor: pointer;
width: fit-content;
}
.field-unit {
color: #969799;
font-size: 14px;
margin-left: 4px;
}
.disposal-measures {
margin-bottom: 16px;
.measures-label {
font-size: 14px;
color: #323233;
display: block;
margin-bottom: 8px;
}
.measures-options {
:deep(.van-checkbox-group) {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
:deep(.van-checkbox) {
margin-right: 0;
}
}
}
.attachment-tip {
font-size: 12px;
color: #969799;
margin-bottom: 12px;
}
.upload-area {
display: flex;
gap: 16px;
flex-wrap: wrap;
.upload-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
background: #f8f9fa;
border: 1px dashed #dcdee0;
border-radius: 8px;
gap: 4px;
font-size: 12px;
color: #969799;
cursor: pointer;
}
}
.video-preview {
margin-top: 12px;
}
:deep(.van-field__label) {
width: 110px;
}
}
</style>

View File

@ -0,0 +1,63 @@
{
"occurLocation": "G108国道 K2250+300处",
"occurTime": "2024-07-15 14:30:00",
"roadConditionType": "国道",
"routeNo": "G108",
"event": {
"blockedMileage": 1.5,
"blockedPointName": "磨盘山隧道口",
"contactPerson": "张明",
"contactPhone": "13812345678",
"damageCount": 3,
"district": "武侯区",
"endStakeLat": "30.658712",
"endStakeLng": "104.082356",
"endStakeNo": "K2251+200",
"estimatedRecoveryCost": 120.5,
"inspectionMileage": 25.6,
"isBlocked": true,
"needsRecovery": true,
"repairProgress": "抢修中",
"reporterUnit": "武侯区交通运输局",
"startStakeLat": "30.652145",
"startStakeLng": "104.075632",
"startStakeNo": "K2250+300"
},
"report": {
"damagedVehicleCount": 2,
"strandedPersonCount": 12,
"deadCount": 0,
"strandedVehicleCount": 12,
"disposalMeasures": "halfClose,bypass",
"actualRecoverTime": "2024-07-17 12:00:00",
"expectRecoverTime": "2024-07-18 18:00:00",
"injuredCount": 1,
"investedFunds": 35.8,
"investedMachinery": 6,
"investedManpower": 45,
"remark": "已组织抢险队伍进行抢通,便道已修建完成",
"siteDescription": "因持续强降雨导致山体滑坡掩埋路面约50米边坡垮塌严重",
"totalLossAmount": 85.6
},
"lossList": [
{
"length": 50,
"width": 8.5,
"height": 2.5,
"unitPrice": 380,
"totalAmount": 38.9,
"lossCategory": "路面损毁",
"remark": "沥青路面严重损坏"
},
{
"length": 30,
"width": 2.5,
"height": 6,
"unitPrice": 520,
"totalAmount": 23.4,
"lossCategory": "挡墙损毁",
"remark": "浆砌片石挡墙垮塌"
}
],
"fileList": []
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -17,7 +17,7 @@
<div class="filter-row"> <div class="filter-row">
<div class="filter-item"> <div class="filter-item">
<span class="filter-label">影响区域</span> <span class="filter-label">影响区域</span>
<el-select v-model="filterForm.region" placeholder="请选择" class="filter-select" clearable> <el-select :teleported="false" v-model="filterForm.region" placeholder="请选择" class="filter-select" clearable>
<el-option <el-option
v-for="item in regionOptions" v-for="item in regionOptions"
:key="item.value" :key="item.value"
@ -28,7 +28,7 @@
</div> </div>
<div class="filter-item"> <div class="filter-item">
<span class="filter-label">类型</span> <span class="filter-label">类型</span>
<el-select v-model="filterForm.type" placeholder="请选择" class="filter-select" clearable> <el-select :teleported="false" v-model="filterForm.type" placeholder="请选择" class="filter-select" clearable>
<el-option <el-option
v-for="item in typeOptions" v-for="item in typeOptions"
:key="item.value" :key="item.value"
@ -39,7 +39,7 @@
</div> </div>
<div class="filter-item"> <div class="filter-item">
<span class="filter-label">管控措施</span> <span class="filter-label">管控措施</span>
<el-select v-model="filterForm.controlMeasure" placeholder="请选择" class="filter-select" clearable> <el-select :teleported="false" v-model="filterForm.controlMeasure" placeholder="请选择" class="filter-select" clearable>
<el-option <el-option
v-for="item in controlMeasureOptions" v-for="item in controlMeasureOptions"
:key="item.value" :key="item.value"

View File

@ -18,7 +18,7 @@
<div class="filter-row"> <div class="filter-row">
<div class="filter-item"> <div class="filter-item">
<span class="filter-label">影响区域</span> <span class="filter-label">影响区域</span>
<el-select v-model="filterForm.region" placeholder="请选择" class="filter-select" clearable> <el-select :teleported="false" v-model="filterForm.region" placeholder="请选择" class="filter-select" clearable>
<el-option <el-option
v-for="item in regionOptions" v-for="item in regionOptions"
:key="item.value" :key="item.value"
@ -29,7 +29,7 @@
</div> </div>
<div class="filter-item"> <div class="filter-item">
<span class="filter-label">驻地风险等级</span> <span class="filter-label">驻地风险等级</span>
<el-select v-model="filterForm.riskLevel" placeholder="请选择" class="filter-select" clearable> <el-select :teleported="false" v-model="filterForm.riskLevel" placeholder="请选择" class="filter-select" clearable>
<el-option <el-option
v-for="item in riskLevelOptions" v-for="item in riskLevelOptions"
:key="item.value" :key="item.value"

View File

@ -18,7 +18,14 @@
<div class="filter-row"> <div class="filter-row">
<div class="filter-item"> <div class="filter-item">
<span class="filter-label">影响区域</span> <span class="filter-label">影响区域</span>
<el-select v-model="filterForm.region" placeholder="请选择" class="filter-select" clearable> <el-select :teleported="false"
v-model="filterForm.region"
placeholder="请选择"
popper-class="custom-select-popper"
:popper-append-to-body="false"
class="filter-select"
clearable
>
<el-option <el-option
v-for="item in regionOptions" v-for="item in regionOptions"
:key="item.value" :key="item.value"
@ -29,7 +36,12 @@
</div> </div>
<div class="filter-item"> <div class="filter-item">
<span class="filter-label">类型</span> <span class="filter-label">类型</span>
<el-select v-model="filterForm.type" placeholder="请选择" class="filter-select" clearable> <el-select :teleported="false"
v-model="filterForm.type"
placeholder="请选择"
class="filter-select"
clearable
>
<el-option <el-option
v-for="item in typeOptions" v-for="item in typeOptions"
:key="item.value" :key="item.value"
@ -75,13 +87,13 @@ const tableHeight = ref(300);
// //
const tableColumns = ref([ const tableColumns = ref([
{ prop: 'id', label: '序号', width: '50' }, { prop: "id", label: "序号", width: "60px" },
{ prop: 'district', label: '区县/镇街', width: '100' }, { prop: "district", label: "区县/镇街", width: "120px" },
{ prop: 'name', label: '姓名', width: '80' }, { prop: "name", label: "姓名", width: "80px" },
{ prop: 'phone', label: '电话', width: '120' }, { prop: "phone", label: "电话", width: "120px" },
{ prop: 'type', label: '类型', width: '100' }, { prop: "type", label: "类型", width: "120px" },
{ prop: 'role', label: '角色', width: '140' }, { prop: "role", label: "角色", width: "120px" },
{ prop: 'dispatchTime', label: '调度时间', width: '160' }, { prop: "dispatchTime", label: "调度时间", width: "160px" },
]); ]);
// //
@ -178,54 +190,50 @@ watch(
currentPage.value = 1; currentPage.value = 1;
fetchData(); fetchData();
} }
} },
); );
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
// //
.filter-section { .filter-row {
margin-bottom: 16px; display: flex;
gap: 12px;
flex-wrap: wrap;
}
.filter-row { .filter-item {
display: flex; display: flex;
gap: 12px; align-items: center;
flex-wrap: wrap; gap: 8px;
.filter-label {
font-size: 13px;
color: rgba(255, 255, 255, 0.8);
white-space: nowrap;
} }
.filter-item { .filter-select {
display: flex; width: 140px;
align-items: center;
gap: 8px;
.filter-label { :deep(.el-input__wrapper) {
font-size: 13px; background-color: rgba(30, 70, 120, 0.4);
color: rgba(255, 255, 255, 0.8); border: 1px solid rgba(64, 169, 255, 0.3);
white-space: nowrap; box-shadow: none;
} border-radius: 4px;
.filter-select { .el-input__inner {
width: 140px; color: #fff;
font-size: 13px;
:deep(.el-input__wrapper) { &::placeholder {
background-color: rgba(30, 70, 120, 0.4); color: rgba(255, 255, 255, 0.4);
border: 1px solid rgba(64, 169, 255, 0.3);
box-shadow: none;
border-radius: 4px;
.el-input__inner {
color: #fff;
font-size: 13px;
&::placeholder {
color: rgba(255, 255, 255, 0.4);
}
} }
}
.el-input__suffix { .el-input__suffix {
.el-icon { .el-icon {
color: rgba(255, 255, 255, 0.6); color: rgba(255, 255, 255, 0.6);
}
} }
} }
} }

View File

@ -18,7 +18,12 @@
<div class="filter-row"> <div class="filter-row">
<div class="filter-item"> <div class="filter-item">
<span class="filter-label">影响区域</span> <span class="filter-label">影响区域</span>
<el-select v-model="filterForm.region" placeholder="请选择" class="filter-select" clearable> <el-select :teleported="false"
v-model="filterForm.region"
placeholder="请选择"
class="filter-select"
clearable
>
<el-option <el-option
v-for="item in regionOptions" v-for="item in regionOptions"
:key="item.value" :key="item.value"
@ -32,7 +37,9 @@
<!-- 调度数列插槽 --> <!-- 调度数列插槽 -->
<template #dispatchCount="{ row }"> <template #dispatchCount="{ row }">
<span class="dispatch-count" @click="handleDispatchClick(row)">{{ row.dispatchCount }}</span> <span class="dispatch-count" @click="handleDispatchClick(row)">{{
row.dispatchCount
}}</span>
</template> </template>
</base-dialog> </base-dialog>
</template> </template>
@ -52,8 +59,8 @@ const props = defineProps({
const emit = defineEmits({ const emit = defineEmits({
"update:visible": (value) => typeof value === "boolean", "update:visible": (value) => typeof value === "boolean",
"close": () => true, close: () => true,
"dispatchClick": (item) => item !== undefined dispatchClick: (item) => item !== undefined,
}); });
// //
@ -69,10 +76,10 @@ const tableHeight = ref(300);
// //
const tableColumns = ref([ const tableColumns = ref([
{ prop: 'id', label: '序号', width: '60' }, { prop: "id", label: "序号", width: "" },
{ prop: 'region', label: '影响区域', width: '120' }, { prop: "region", label: "影响区域", width: "" },
{ prop: 'dispatchCount', label: '调度数', width: '100', slot: 'dispatchCount' }, { prop: "dispatchCount", label: "调度数", width: "", slot: "dispatchCount" },
{ prop: 'lastDispatchTime', label: '最近调度时间', width: '160' }, { prop: "lastDispatchTime", label: "最近调度时间", width: "" },
]); ]);
// //
@ -164,7 +171,7 @@ watch(
filterForm.value.region = ""; filterForm.value.region = "";
fetchData(); fetchData();
} }
} },
); );
</script> </script>

View File

@ -18,21 +18,80 @@
<template #header> <template #header>
<!-- 统计卡片 --> <!-- 统计卡片 -->
<div class="stats-cards"> <div class="stats-cards">
<div class="stat-card"> <div
<div class="stat-label">影响桥梁</div> @click="handleClick('0')"
<div class="stat-value">2933</div> class="stat-card"
:style="{
backgroundImage: `url(${cardType === '0' ? selectedIcon : unselectedIcon})`,
backgroundSize: '100% 100%',
backgroundPosition: 'center',
}"
>
<div class="stat-icon"><img :src="Icon0" alt="" /></div>
<div class="stat-content">
<span class="stat-label">影响桥梁</span>
<span class="stat-value">(1430)</span>
</div>
</div> </div>
<div class="stat-card"> <div
<div class="stat-label">影响边坡</div> @click="handleClick('1')"
<div class="stat-value">2933</div> class="stat-card"
:style="{
backgroundImage: `url(${cardType === '1' ? selectedIcon : unselectedIcon})`,
backgroundSize: '100% 100%',
backgroundPosition: 'center',
}"
>
<div class="stat-icon"><img :src="Icon1" alt="" /></div>
<div class="stat-content">
<span class="stat-label">影响边坡</span>
<span class="stat-value">(933)</span>
</div>
</div> </div>
<div class="stat-card"> <div
<div class="stat-label">影响隧道</div> @click="handleClick('2')"
<div class="stat-value">2933</div> class="stat-card"
:style="{
backgroundImage: `url(${cardType === '2' ? selectedIcon : unselectedIcon})`,
backgroundSize: '100% 100%',
backgroundPosition: 'center',
}"
>
<div class="stat-icon"><img :src="Icon2" alt="" /></div>
<div class="stat-content">
<span class="stat-label">影响隧道</span>
<span class="stat-value">(1033)</span>
</div>
</div> </div>
<div class="stat-card"> <div
<div class="stat-label">影响项目</div> @click="handleClick('3')"
<div class="stat-value">2933</div> class="stat-card"
:style="{
backgroundImage: `url(${cardType === '3' ? selectedIcon : unselectedIcon})`,
backgroundSize: '100% 100%',
backgroundPosition: 'center',
}"
>
<div class="stat-icon"><img :src="Icon3" alt="" /></div>
<div class="stat-content">
<span class="stat-label">影响项目</span>
<span class="stat-value">(832)</span>
</div>
</div>
<div
@click="handleClick('4')"
class="stat-card"
:style="{
backgroundImage: `url(${cardType === '4' ? selectedIcon : unselectedIcon})`,
backgroundSize: '100% 100%',
backgroundPosition: 'center',
}"
>
<div class="stat-icon"><img :src="Icon4" alt="" /></div>
<div class="stat-content">
<span class="stat-label">影响路段</span>
<span class="stat-value">(832)</span>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -41,7 +100,12 @@
<template #filter> <template #filter>
<div class="filter-row"> <div class="filter-row">
<div class="filter-item"> <div class="filter-item">
<el-select v-model="filterForm.pointType" size="small" placeholder="影响点类型" class="filter-select"> <el-select :teleported="false"
v-model="filterForm.pointType"
size="small"
placeholder="影响点类型"
class="filter-select"
>
<el-option <el-option
v-for="option in pointTypeOptions" v-for="option in pointTypeOptions"
:key="option.value" :key="option.value"
@ -51,7 +115,12 @@
</el-select> </el-select>
</div> </div>
<div class="filter-item"> <div class="filter-item">
<el-select v-model="filterForm.pointLevel" size="small" placeholder="影响点等级" class="filter-select"> <el-select :teleported="false"
v-model="filterForm.pointLevel"
size="small"
placeholder="影响点等级"
class="filter-select"
>
<el-option <el-option
v-for="option in pointLevelOptions" v-for="option in pointLevelOptions"
:key="option.value" :key="option.value"
@ -61,7 +130,12 @@
</el-select> </el-select>
</div> </div>
<div class="filter-item"> <div class="filter-item">
<el-select v-model="filterForm.region" size="small" placeholder="影响区域" class="filter-select"> <el-select :teleported="false"
v-model="filterForm.region"
size="small"
placeholder="影响区域"
class="filter-select"
>
<el-option <el-option
v-for="option in regionOptionsWithAll" v-for="option in regionOptionsWithAll"
:key="option.value" :key="option.value"
@ -80,7 +154,9 @@
<!-- 影响点等级列插槽 --> <!-- 影响点等级列插槽 -->
<template #pointLevel="{ row }"> <template #pointLevel="{ row }">
<span class="level-tag" :class="row.levelClass">{{ row.pointLevel }}</span> <span class="level-tag" :class="row.levelClass">{{
row.pointLevel
}}</span>
</template> </template>
<!-- 交通主管部门负责人列插槽 --> <!-- 交通主管部门负责人列插槽 -->
@ -125,9 +201,24 @@
<script setup> <script setup>
import { ref, computed, watch } from "vue"; import { ref, computed, watch } from "vue";
import { Close, ArrowLeft, ArrowRight } from "@element-plus/icons-vue"; import { Close, ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
import { pointTypeOptions, pointLevelOptions, regionOptionsWithAll } from "../component/index.js"; import {
pointTypeOptions,
pointLevelOptions,
regionOptionsWithAll,
} from "../component/index.js";
import baseDialog from "../component/baseDialog.vue"; import baseDialog from "../component/baseDialog.vue";
import respondedIcon from "../../../assets/xiangying/有回应@2x.png";
import notRespondedIcon from "../../../assets/xiangying/无回应@2x.png";
import selectedIcon from "../../../assets/xiangying/选中bg@2x.png";
import unselectedIcon from "../../../assets/xiangying/未选中bg@2x.png";
import Icon0 from "../../../assets/xiangying/选中@2x.png";
import Icon1 from "../../../assets/xiangying/未选中1@2x.png";
import Icon2 from "../../../assets/xiangying/未选中2@2x.png";
import Icon3 from "../../../assets/xiangying/未选中3@2x.png";
import Icon4 from "../../../assets/xiangying/未选中4@2x.png";
const props = defineProps({ const props = defineProps({
visible: { visible: {
type: Boolean, type: Boolean,
@ -146,16 +237,26 @@ const filterForm = ref({
// //
const tableColumns = ref([ const tableColumns = ref([
{ prop: "id", label: "序号", width: "50px" }, { prop: "id", label: "序号", width: "" },
{ prop: "region", label: "影响区域", width: "80px" }, { prop: "region", label: "影响区域", width: "" },
{ prop: "pointType", label: "影响点类型", width: "80px" }, { prop: "pointType", label: "影响点类型", width: "" },
{ prop: "pointLocation", label: "影响点位置", width: "180px" }, { prop: "pointLocation", label: "影响点位置", width: "" },
{ prop: "pointLevel", label: "影响点等级", width: "90px", slot: "pointLevel" }, { prop: "pointLevel", label: "影响点等级", width: "", slot: "pointLevel" },
{ prop: "trafficDept", label: "交通主管部门负责人", width: "130px", slot: "trafficDept" }, {
{ prop: "roadOrg", label: "公路机构责任人", width: "110px", slot: "roadOrg" }, prop: "trafficDept",
{ prop: "maintenance", label: "养护站负责人", width: "110px", slot: "maintenance" }, label: "交通主管部门负责人",
{ prop: "roadKeeper", label: "护路员", width: "100px", slot: "roadKeeper" }, width: "",
{ prop: "operation", label: "操作", width: "60px", slot: "operation" }, slot: "trafficDept",
},
{ prop: "roadOrg", label: "公路机构责任人", width: "", slot: "roadOrg" },
{
prop: "maintenance",
label: "养护站负责人",
width: "",
slot: "maintenance",
},
{ prop: "roadKeeper", label: "护路员", width: "", slot: "roadKeeper" },
{ prop: "operation", label: "操作", width: "", slot: "operation" },
]); ]);
// //
@ -198,6 +299,12 @@ const tableData = ref([
}, },
]); ]);
const cardType = ref("");
//
const handleClick = (type) => {
cardType.value = type;
};
// //
const currentPage = ref(1); const currentPage = ref(1);
const pageSize = ref(10); const pageSize = ref(10);
@ -266,7 +373,7 @@ watch(
currentPage.value = 1; currentPage.value = 1;
fetchData(); fetchData();
} }
} },
); );
</script> </script>
@ -276,8 +383,6 @@ watch(
@return calc($px / 1920 * 100vw); @return calc($px / 1920 * 100vw);
} }
// //
.stats-cards { .stats-cards {
display: grid; display: grid;
@ -285,7 +390,11 @@ watch(
gap: vw(16); gap: vw(16);
.stat-card { .stat-card {
background: linear-gradient(135deg, rgba(30, 70, 120, 0.6) 0%, rgba(20, 50, 90, 0.8) 100%); background: linear-gradient(
135deg,
rgba(30, 70, 120, 0.6) 0%,
rgba(20, 50, 90, 0.8) 100%
);
border: vw(2) solid rgba(64, 169, 255, 0.4); border: vw(2) solid rgba(64, 169, 255, 0.4);
text-align: center; text-align: center;
transition: all 0.3s; transition: all 0.3s;
@ -315,7 +424,6 @@ watch(
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 12px; gap: 12px;
} }
.filter-item { .filter-item {
@ -425,4 +533,58 @@ watch(
} }
} }
} }
//
.stats-cards {
display: flex;
gap: 12px;
.stat-card {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
transition: all 0.3s;
cursor: pointer;
flex: 1;
&:hover {
background: rgba(30, 70, 120, 0.9);
border-color: rgba(64, 169, 255, 0.6);
box-shadow: 0 0 15px rgba(64, 169, 255, 0.3);
}
.stat-icon {
font-size: 18px;
color: #40a9ff;
width: 34px;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
}
}
.stat-content {
display: flex;
align-items: center;
gap: 6px;
.stat-label {
font-size: 14px;
color: #fff;
font-weight: 500;
}
.stat-value {
font-size: 14px;
font-weight: bold;
color: #40a9ff;
}
}
}
}
</style> </style>

View File

@ -9,7 +9,7 @@
:current-page="currentPage" :current-page="currentPage"
:page-size="pageSize" :page-size="pageSize"
:z-index="1000" :z-index="1000"
:max-width="700" :max-width="1000"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@close="handleClose" @close="handleClose"
@ -97,12 +97,12 @@ const stats = ref({
// //
const tableColumns = ref([ const tableColumns = ref([
{ prop: "id", label: "序号", width: "60px" }, { prop: "id", label: "序号", width: "" },
{ prop: "district", label: "区县/镇街", width: "120px" }, { prop: "district", label: "区县/镇街", width: "" },
{ prop: "name", label: "姓名", width: "100px" }, { prop: "name", label: "姓名", width: "" },
{ prop: "phone", label: "电话", width: "120px" }, { prop: "phone", label: "电话", width: "" },
{ prop: "role", label: "角色", width: "200px" }, { prop: "role", label: "角色", width: "" },
{ prop: "position", label: "职务", width: "100px" }, { prop: "position", label: "职务", width: "" },
{ prop: "operation", label: "操作", width: "120px", slot: "operation" }, { prop: "operation", label: "操作", width: "120px", slot: "operation" },
]); ]);

View File

@ -18,25 +18,80 @@
<template #header> <template #header>
<!-- 统计卡片 --> <!-- 统计卡片 -->
<div class="stats-cards"> <div class="stats-cards">
<div class="stat-card"> <div
<div class="stat-label">影响桥梁</div> @click="handleClick('0')"
<div class="stat-value">2933</div> class="stat-card"
:style="{
backgroundImage: `url(${cardType === '0' ? selectedIcon : unselectedIcon})`,
backgroundSize: '100% 100%',
backgroundPosition: 'center',
}"
>
<div class="stat-icon"><img :src="Icon0" alt="" /></div>
<div class="stat-content">
<span class="stat-label">影响桥梁</span>
<span class="stat-value">(1430)</span>
</div>
</div> </div>
<div class="stat-card"> <div
<div class="stat-label">影响边坡</div> @click="handleClick('1')"
<div class="stat-value">2933</div> class="stat-card"
:style="{
backgroundImage: `url(${cardType === '1' ? selectedIcon : unselectedIcon})`,
backgroundSize: '100% 100%',
backgroundPosition: 'center',
}"
>
<div class="stat-icon"><img :src="Icon1" alt="" /></div>
<div class="stat-content">
<span class="stat-label">影响边坡</span>
<span class="stat-value">(933)</span>
</div>
</div> </div>
<div class="stat-card"> <div
<div class="stat-label">影响隧道</div> @click="handleClick('2')"
<div class="stat-value">2933</div> class="stat-card"
:style="{
backgroundImage: `url(${cardType === '2' ? selectedIcon : unselectedIcon})`,
backgroundSize: '100% 100%',
backgroundPosition: 'center',
}"
>
<div class="stat-icon"><img :src="Icon2" alt="" /></div>
<div class="stat-content">
<span class="stat-label">影响隧道</span>
<span class="stat-value">(1033)</span>
</div>
</div> </div>
<div class="stat-card"> <div
<div class="stat-label">影响项目</div> @click="handleClick('3')"
<div class="stat-value">2933</div> class="stat-card"
:style="{
backgroundImage: `url(${cardType === '3' ? selectedIcon : unselectedIcon})`,
backgroundSize: '100% 100%',
backgroundPosition: 'center',
}"
>
<div class="stat-icon"><img :src="Icon3" alt="" /></div>
<div class="stat-content">
<span class="stat-label">影响项目</span>
<span class="stat-value">(832)</span>
</div>
</div> </div>
<div class="stat-card"> <div
<div class="stat-label">影响路段</div> @click="handleClick('4')"
<div class="stat-value">2432</div> class="stat-card"
:style="{
backgroundImage: `url(${cardType === '4' ? selectedIcon : unselectedIcon})`,
backgroundSize: '100% 100%',
backgroundPosition: 'center',
}"
>
<div class="stat-icon"><img :src="Icon4" alt="" /></div>
<div class="stat-content">
<span class="stat-label">影响路段</span>
<span class="stat-value">(832)</span>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -44,8 +99,8 @@
<!-- 筛选区域 --> <!-- 筛选区域 -->
<template #filter> <template #filter>
<div class="filter-row"> <div class="filter-row">
<div class="filter-item"> <!-- <div class="filter-item">
<el-select <el-select :teleported="false"
v-model="filterForm.pointType" v-model="filterForm.pointType"
placeholder="影响点类型" placeholder="影响点类型"
class="filter-select" class="filter-select"
@ -57,9 +112,10 @@
:value="option.value" :value="option.value"
/> />
</el-select> </el-select>
</div> </div> -->
<div class="filter-item"> <div class="filter-item">
<el-select <span class="filter-label">影响点等级</span>
<el-select :teleported="false"
v-model="filterForm.pointLevel" v-model="filterForm.pointLevel"
placeholder="影响点等级" placeholder="影响点等级"
class="filter-select" class="filter-select"
@ -73,7 +129,8 @@
</el-select> </el-select>
</div> </div>
<div class="filter-item"> <div class="filter-item">
<el-select <span class="filter-label">是否回应</span>
<el-select :teleported="false"
v-model="filterForm.isResponded" v-model="filterForm.isResponded"
placeholder="是否回应" placeholder="是否回应"
class="filter-select" class="filter-select"
@ -91,13 +148,26 @@
<!-- 影响点等级列插槽 --> <!-- 影响点等级列插槽 -->
<template #pointLevel="{ row }"> <template #pointLevel="{ row }">
<span class="level-tag" :class="row.levelClass">{{ row.pointLevel }}</span> <span class="level-tag" :class="row.levelClass">{{
row.pointLevel
}}</span>
</template> </template>
<!-- 交通主管部门负责人列插槽 --> <!-- 交通主管部门负责人列插槽 -->
<template #trafficDept="{ row }"> <template #trafficDept="{ row }">
<div class="person-info"> <div class="person-info">
<span class="person-name">{{ row.trafficDept.name }}</span> <div class="person-name center">
<span style="margin-right: 5px">{{ row.trafficDept.name }}</span>
<img
class="response-icon"
:src="
row.trafficDept.isResponded
? row.trafficDept.img
: row.trafficDept.img
"
alt
/>
</div>
<span class="person-phone">{{ row.trafficDept.phone }}</span> <span class="person-phone">{{ row.trafficDept.phone }}</span>
</div> </div>
</template> </template>
@ -128,7 +198,9 @@
<!-- 回应状态列插槽 --> <!-- 回应状态列插槽 -->
<template #responseStatus="{ row }"> <template #responseStatus="{ row }">
<span class="response-status" :class="row.responseClass">{{ row.responseStatus }}</span> <span class="response-status" :class="row.responseClass">{{
row.responseStatus
}}</span>
</template> </template>
<!-- 最新催告时间列插槽 --> <!-- 最新催告时间列插槽 -->
@ -149,9 +221,24 @@
<script setup> <script setup>
import { ref, computed, watch } from "vue"; import { ref, computed, watch } from "vue";
import { Close, ArrowLeft, ArrowRight } from "@element-plus/icons-vue"; import { Close, ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
import { pointTypeOptions, pointLevelOptions, isRespondedOptions } from "../component/index.js"; import {
pointTypeOptions,
pointLevelOptions,
isRespondedOptions,
} from "../component/index.js";
import baseDialog from "../component/baseDialog.vue"; import baseDialog from "../component/baseDialog.vue";
import respondedIcon from "../../../assets/xiangying/有回应@2x.png";
import notRespondedIcon from "../../../assets/xiangying/无回应@2x.png";
import selectedIcon from "../../../assets/xiangying/选中bg@2x.png";
import unselectedIcon from "../../../assets/xiangying/未选中bg@2x.png";
import Icon0 from "../../../assets/xiangying/选中@2x.png";
import Icon1 from "../../../assets/xiangying/未选中1@2x.png";
import Icon2 from "../../../assets/xiangying/未选中2@2x.png";
import Icon3 from "../../../assets/xiangying/未选中3@2x.png";
import Icon4 from "../../../assets/xiangying/未选中4@2x.png";
const props = defineProps({ const props = defineProps({
visible: { visible: {
type: Boolean, type: Boolean,
@ -168,18 +255,40 @@ const filterForm = ref({
isResponded: "", isResponded: "",
}); });
const cardType = ref("0");
// //
const tableColumns = ref([ const tableColumns = ref([
{ prop: "id", label: "序号", width: "50px" }, { prop: "id", label: "序号", width: "50px" },
{ prop: "pointType", label: "影响点类型", width: "80px" }, { prop: "pointType", label: "影响点类型", width: "80px" },
{ prop: "pointLocation", label: "影响点位置", width: "150px" }, { prop: "pointLocation", label: "影响点位置", width: "150px" },
{ prop: "pointLevel", label: "影响点等级", width: "90px", slot: "pointLevel" }, {
prop: "pointLevel",
label: "影响点等级",
width: "90px",
slot: "pointLevel",
},
{ prop: "checkCount", label: "查次数", width: "60px" }, { prop: "checkCount", label: "查次数", width: "60px" },
{ prop: "trafficDept", label: "交通主管部门负责人", width: "120px", slot: "trafficDept" }, {
prop: "trafficDept",
label: "交通主管部门负责人",
width: "120px",
slot: "trafficDept",
},
{ prop: "roadOrg", label: "公路机构责任人", width: "110px", slot: "roadOrg" }, { prop: "roadOrg", label: "公路机构责任人", width: "110px", slot: "roadOrg" },
{ prop: "maintenance", label: "养护站负责人", width: "110px", slot: "maintenance" }, {
prop: "maintenance",
label: "养护站负责人",
width: "110px",
slot: "maintenance",
},
{ prop: "roadKeeper", label: "护路员", width: "80px", slot: "roadKeeper" }, { prop: "roadKeeper", label: "护路员", width: "80px", slot: "roadKeeper" },
{ prop: "responseStatus", label: "回应状态", width: "70px", slot: "responseStatus" }, {
prop: "responseStatus",
label: "回应状态",
width: "70px",
slot: "responseStatus",
},
{ prop: "urgeTime", label: "最新催告时间", width: "110px", slot: "urgeTime" }, { prop: "urgeTime", label: "最新催告时间", width: "110px", slot: "urgeTime" },
{ prop: "operation", label: "操作", width: "50px", slot: "operation" }, { prop: "operation", label: "操作", width: "50px", slot: "operation" },
]); ]);
@ -193,10 +302,30 @@ const tableData = ref([
pointLevel: "一般隐患", pointLevel: "一般隐患",
levelClass: "level-normal", levelClass: "level-normal",
checkCount: 2, checkCount: 2,
trafficDept: { name: "罗宸", phone: "17623865172" }, trafficDept: {
roadOrg: { name: "李海平", phone: "1372386532" }, name: "罗宸",
maintenance: { name: "苏祖兵", phone: "13594331090" }, phone: "17623865172",
roadKeeper: { name: "凌承礼", phone: "1592393704" }, img: respondedIcon,
isResponded: true,
},
roadOrg: {
name: "李海平",
phone: "1372386532",
img: notRespondedIcon,
isResponded: false,
},
maintenance: {
name: "苏祖兵",
phone: "13594331090",
img: notRespondedIcon,
isResponded: false,
},
roadKeeper: {
name: "凌承礼",
phone: "1592393704",
img: respondedIcon,
isResponded: true,
},
responseStatus: "已回应", responseStatus: "已回应",
responseClass: "status-responded", responseClass: "status-responded",
urgeTime: { date: "2026-03-28", time: "12:30:00" }, urgeTime: { date: "2026-03-28", time: "12:30:00" },
@ -231,6 +360,11 @@ const handleClose = () => {
emit("close"); emit("close");
}; };
//
const handleClick = (type) => {
cardType.value = type;
};
// base-dialog // base-dialog
// //
@ -268,20 +402,25 @@ watch(
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
// //
.filter-section { .filter-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 20px; margin-bottom: 20px;
.filter-row { .filter-item {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; gap: 8px;
gap: 12px;
} .filter-label {
font-size: 13px;
color: rgba(255, 255, 255, 0.8);
white-space: nowrap;
}
.filter-item {
.filter-select { .filter-select {
width: 150px; width: 150px;
@ -334,6 +473,10 @@ watch(
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
.response-icon {
width: 30px;
height: 15px;
}
.person-name { .person-name {
font-size: 12px; font-size: 12px;
@ -406,42 +549,54 @@ watch(
// //
.stats-cards { .stats-cards {
display: grid; display: flex;
grid-template-columns: repeat(5, 1fr); gap: 12px;
gap: 20px;
margin-bottom: 20px;
.stat-card { .stat-card {
background: linear-gradient( display: flex;
135deg, align-items: center;
rgba(30, 70, 120, 0.6) 0%, gap: 8px;
rgba(20, 50, 90, 0.8) 100% padding: 8px 16px;
);
border: 2px solid rgba(64, 169, 255, 0.4);
border-radius: 8px;
padding: 8px 20px;
text-align: center;
transition: all 0.3s; transition: all 0.3s;
cursor: pointer; cursor: pointer;
flex: 1;
&:hover { &:hover {
border-color: rgba(64, 169, 255, 0.8); background: rgba(30, 70, 120, 0.9);
box-shadow: 0 0 20px rgba(64, 169, 255, 0.3); border-color: rgba(64, 169, 255, 0.6);
transform: translateY(-2px); box-shadow: 0 0 15px rgba(64, 169, 255, 0.3);
} }
.stat-label { .stat-icon {
font-size: 14px; font-size: 18px;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 8px;
font-weight: 500;
}
.stat-value {
font-size: 28px;
font-weight: bold;
color: #40a9ff; color: #40a9ff;
text-shadow: 0 0 10px rgba(64, 169, 255, 0.5); width: 34px;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
}
}
.stat-content {
display: flex;
align-items: center;
gap: 6px;
.stat-label {
font-size: 14px;
color: #fff;
font-weight: 500;
}
.stat-value {
font-size: 14px;
font-weight: bold;
color: #40a9ff;
}
} }
} }
} }

View File

@ -0,0 +1,203 @@
<template>
<teleport to="body">
<div
v-if="visible"
class="center-card-dialog-overlay"
:style="{ zIndex: zIndex }"
@click.self="handleClose"
>
<div class="center-card-dialog" :style="dialogStyle">
<!-- 头部区域 -->
<div class="dialog-header" @click="handleDetail">
<div class="header-left">
<el-icon class="location-icon"><Location /></el-icon>
<span class="dialog-title">潼南</span>
</div>
<el-icon class="arrow-icon"><ArrowRight /></el-icon>
</div>
<!-- 内容区域 -->
<div class="dialog-content">
<div class="info-item">
<span class="info-label">人数</span>
<span class="info-value">{{ value }}</span>
<span class="info-unit"></span>
</div>
<div class="info-item">
<span class="info-label">路段</span>
<span class="info-value">{{ roadCount }}</span>
<span class="info-unit"></span>
</div>
</div>
</div>
</div>
</teleport>
</template>
<script setup>
import { computed } from "vue";
import { ArrowRight, Location } from "@element-plus/icons-vue";
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
value: {
type: [String, Number],
default: "",
},
roadCount: {
type: [String, Number],
default: "128",
},
zIndex: {
type: Number,
default: 1000,
},
width: {
type: String,
default: "200px",
},
});
const emit = defineEmits(["update:visible", "close", "detail"]);
const dialogStyle = computed(() => ({
width: props.width,
}));
//
const handleClose = () => {
emit("update:visible", false);
emit("close");
};
//
const handleDetail = () => {
emit("detail");
};
</script>
<style lang="scss" scoped>
// -
@function vw($px) {
@return calc($px / 1920 * 100vw);
}
.center-card-dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.center-card-dialog {
position: relative;
background-image: url("../../../assets/MaMap_img/区县弹窗背景@2x.png");
background-size: 100% 100%;
background-repeat: no-repeat;
min-width: vw(200);
padding: vw(12) vw(16);
//
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
margin-bottom: vw(8);
.header-left {
display: flex;
align-items: center;
gap: vw(6);
.location-icon {
font-size: vw(16);
color: #fff;
}
.dialog-title {
font-size: vw(16);
font-weight: 600;
color: #fff;
}
}
.arrow-icon {
font-size: vw(14);
color: rgba(255, 255, 255, 0.8);
}
}
//
.dialog-content {
display: flex;
align-items: center;
gap: vw(20);
.info-item {
display: flex;
align-items: center;
gap: vw(4);
.info-label {
font-size: vw(13);
color: rgba(255, 255, 255, 0.8);
}
.info-value {
font-size: vw(14);
font-weight: 600;
color: #40a9ff;
}
.info-unit {
font-size: vw(12);
color: rgba(255, 255, 255, 0.7);
}
}
}
}
//
.center-card-dialog-overlay {
.center-card-dialog {
animation: dialogIn 0.3s ease-out;
}
}
@keyframes dialogIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
// 退
.center-card-dialog-overlay.v-leave-active {
.center-card-dialog {
animation: dialogOut 0.2s ease-in;
}
}
@keyframes dialogOut {
from {
opacity: 1;
transform: scale(1) translateY(0);
}
to {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
}
</style>

View File

@ -1,88 +1,47 @@
<template> <template>
<div v-if="visible" class="tongnan-dialog-overlay" @click="handleOverlayClick"> <base-dialog
<div class="tongnan-dialog" @click.stop> v-model:visible="props.visible"
<!-- 标题栏 --> title="潼南基本信息表"
<div class="dialog-header"> :table-data="tableData"
<div class="header-title">潼南基本信息表</div> :table-columns="tableColumns"
<div class="close-btn" @click="handleClose"> :table-height="320"
<el-icon><Close /></el-icon> :total="total"
:current-page="currentPage"
:page-size="pageSize"
:z-index="2100"
:max-width="1000"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@close="handleClose"
>
<!-- 操作列插槽 -->
<template #operation="{ row }">
<div class="action-btns">
<div class="action-btn" @click="handleVideo(row)" title="视频">
<el-icon><VideoCamera /></el-icon>
</div>
<div class="action-btn" @click="handleVoice(row)" title="语音">
<el-icon><Microphone /></el-icon>
</div>
<div class="action-btn" @click="handleCall(row)" title="电话">
<el-icon><Phone /></el-icon>
</div> </div>
</div> </div>
</template>
<!-- 数据表格 --> <!-- 驻地名称列插槽 -->
<div class="table-section"> <template #stationName="{ row }">
<div class="table-header"> <el-tooltip :content="row.stationName" placement="top" :show-after="500">
<div <span class="station-name-text" @click="handleStationNameClick(row)">{{ row.stationName }}</span>
v-for="(column, index) in tableColumns" </el-tooltip>
:key="index" </template>
class="th" </base-dialog>
:style="{ width: column.width, flex: column.flex || 'none' }"
>
{{ column.label }}
</div>
</div>
<div class="table-body">
<div
v-for="(item, index) in tableData"
:key="item.id"
class="table-row"
:class="{ 'row-even': index % 2 === 1 }"
>
<div class="td" style="width: 60px">{{ item.id }}</div>
<div class="td" style="width: 140px">{{ item.region }}</div>
<div class="td" style="width: 100px">{{ item.name }}</div>
<div class="td" style="width: 120px">{{ item.phone }}</div>
<div class="td" style="width: 180px">
<el-tooltip :content="item.stationName" placement="top" :show-after="500">
<span class="station-name-text" @click="handleStationNameClick(item)">{{ item.stationName }}</span>
</el-tooltip>
</div>
<div class="td" style="flex: 1">{{ item.type }}</div>
<div class="td" style="width: 140px">
<div class="action-btns">
<div class="action-btn" @click="handleVideo(item)" title="视频">
<el-icon><VideoCamera /></el-icon>
</div>
<div class="action-btn" @click="handleVoice(item)" title="语音">
<el-icon><Microphone /></el-icon>
</div>
<div class="action-btn" @click="handleCall(item)" title="电话">
<el-icon><Phone /></el-icon>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination">
<span class="total">{{ total }}条数据</span>
<div class="page-btns">
<div class="page-btn" :class="{ disabled: currentPage === 1 }" @click="prevPage">
<el-icon><ArrowLeft /></el-icon>
</div>
<div
v-for="page in visiblePages"
:key="page"
class="page-btn"
:class="{ active: currentPage === page }"
@click="goToPage(page)"
>
{{ page }}
</div>
<div class="page-btn" :class="{ disabled: currentPage === totalPages }" @click="nextPage">
<el-icon><ArrowRight /></el-icon>
</div>
</div>
</div>
</div>
</div>
</template> </template>
<script setup> <script setup>
import { ref, computed, watch } from "vue"; import { ref, computed, watch } from "vue";
import { Close, VideoCamera, Microphone, Phone, ArrowLeft, ArrowRight } from "@element-plus/icons-vue"; import { VideoCamera, Microphone, Phone } from "@element-plus/icons-vue";
import baseDialog from "../component/baseDialog.vue";
const props = defineProps({ const props = defineProps({
visible: { visible: {
@ -95,13 +54,13 @@ const emit = defineEmits(["update:visible", "close", "video", "voice", "call", "
// //
const tableColumns = ref([ const tableColumns = ref([
{ label: "序号", width: "60px" }, { prop: "id", label: "序号", width: "60px" },
{ label: "区县/镇街", width: "140px" }, { prop: "region", label: "区县/镇街", width: "140px" },
{ label: "姓名", width: "100px" }, { prop: "name", label: "姓名", width: "100px" },
{ label: "电话", width: "120px" }, { prop: "phone", label: "电话", width: "120px" },
{ label: "驻地名称", width: "180px" }, { prop: "stationName", label: "驻地名称", width: "180px", slot: "stationName" },
{ label: "类型", flex: "1" }, { prop: "type", label: "类型", width: "auto" },
{ label: "调度", width: "140px" }, { prop: "operation", label: "调度", width: "140px", slot: "operation" },
]); ]);
// //
@ -110,7 +69,7 @@ const tableData = ref([
id: 1, id: 1,
region: "沙坪坝区", region: "沙坪坝区",
name: "赵海浪", name: "赵海浪",
phone: "1862352068", phone: "18623520688",
stationName: "沙坪坝区S545茅山峡公路桥新建工程渝黔铁路扩能改造工程项目经理部", stationName: "沙坪坝区S545茅山峡公路桥新建工程渝黔铁路扩能改造工程项目经理部",
type: "交通主管部门", type: "交通主管部门",
}, },
@ -118,7 +77,7 @@ const tableData = ref([
id: 2, id: 2,
region: "沙坪坝区", region: "沙坪坝区",
name: "府效能", name: "府效能",
phone: "1862352068", phone: "18623520688",
stationName: "沙坪坝区S545茅山峡公路桥新建工程渝黔铁路扩能改造工程项目经理部", stationName: "沙坪坝区S545茅山峡公路桥新建工程渝黔铁路扩能改造工程项目经理部",
type: "公路机构", type: "公路机构",
}, },
@ -126,7 +85,7 @@ const tableData = ref([
id: 3, id: 3,
region: "万州区柏梓镇", region: "万州区柏梓镇",
name: "王鑫", name: "王鑫",
phone: "1862352068", phone: "18623520688",
stationName: "万州区项目经理部", stationName: "万州区项目经理部",
type: "公路机构", type: "公路机构",
}, },
@ -134,7 +93,7 @@ const tableData = ref([
id: 4, id: 4,
region: "万州区柏梓镇", region: "万州区柏梓镇",
name: "王鑫", name: "王鑫",
phone: "1862352068", phone: "18623520688",
stationName: "万州区项目经理部", stationName: "万州区项目经理部",
type: "公路机构", type: "公路机构",
}, },
@ -169,10 +128,7 @@ const handleClose = () => {
emit("close"); emit("close");
}; };
// // base-dialog
const handleOverlayClick = () => {
handleClose();
};
// //
const handleVideo = (item) => { const handleVideo = (item) => {
@ -192,22 +148,14 @@ const handleStationNameClick = (item) => {
emit("stationNameClick", item); emit("stationNameClick", item);
}; };
// //
const prevPage = () => { const handleSizeChange = (size) => {
if (currentPage.value > 1) { pageSize.value = size;
currentPage.value--; currentPage.value = 1;
fetchData(); fetchData();
}
}; };
const nextPage = () => { const handleCurrentChange = (page) => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
fetchData();
}
};
const goToPage = (page) => {
currentPage.value = page; currentPage.value = page;
fetchData(); fetchData();
}; };
@ -231,221 +179,48 @@ watch(
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.tongnan-dialog-overlay { //
position: fixed; .action-btns {
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex; display: flex;
align-items: center;
justify-content: center; justify-content: center;
z-index: 2100; gap: 16px;
}
.tongnan-dialog { .action-btn {
width: 80vw; width: 28px;
max-width: 1000px; height: 28px;
background: linear-gradient(135deg, rgba(20, 50, 90, 0.95) 0%, rgba(10, 30, 60, 0.98) 100%);
border: 1px solid rgba(64, 169, 255, 0.3);
border-radius: 12px;
padding: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
//
.dialog-header {
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin-bottom: 20px;
.header-title {
font-size: 20px;
font-weight: 600;
color: #fff;
padding: 8px 40px;
background: linear-gradient(90deg, transparent 0%, rgba(64, 169, 255, 0.2) 20%, rgba(64, 169, 255, 0.2) 80%, transparent 100%);
border-bottom: 2px solid #40a9ff;
}
.close-btn {
position: absolute;
right: 0;
top: 0;
width: 32px;
height: 32px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.7);
cursor: pointer; cursor: pointer;
font-size: 20px; font-size: 18px;
transition: color 0.3s; transition: all 0.3s;
&:hover { &:hover {
color: #fff; color: #40a9ff;
transform: scale(1.1);
}
.el-icon {
font-size: 18px;
} }
} }
} }
// //
.table-section { .station-name-text {
background-color: rgba(30, 70, 120, 0.3); display: block;
border-radius: 8px; cursor: pointer;
overflow: hidden; overflow: hidden;
margin-bottom: 20px; text-overflow: ellipsis;
white-space: nowrap;
max-width: 180px;
color: #40a9ff;
transition: all 0.3s;
.table-header { &:hover {
display: flex; color: #69c0ff;
background-color: rgba(64, 169, 255, 0.2); text-shadow: 0 0 8px rgba(105, 192, 255, 0.6);
padding: 12px 16px;
.th {
font-size: 14px;
font-weight: 500;
color: #fff;
text-align: center;
}
} }
.table-body {
max-height: 320px;
overflow-y: auto;
.table-row {
display: flex;
padding: 14px 16px;
align-items: center;
transition: background-color 0.3s;
&:hover {
background-color: rgba(64, 169, 255, 0.1);
}
&.row-even {
background-color: rgba(30, 70, 120, 0.2);
}
.td {
font-size: 13px;
color: rgba(255, 255, 255, 0.85);
text-align: center;
.station-name-text {
display: block;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 180px;
color: #40a9ff;
transition: all 0.3s;
&:hover {
color: #69c0ff;
text-shadow: 0 0 8px rgba(105, 192, 255, 0.6);
}
}
.action-btns {
display: flex;
justify-content: center;
gap: 16px;
.action-btn {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
font-size: 18px;
transition: all 0.3s;
&:hover {
color: #40a9ff;
transform: scale(1.1);
}
.el-icon {
font-size: 18px;
}
}
}
}
}
}
}
//
.pagination {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 16px;
.total {
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
}
.page-btns {
display: flex;
gap: 8px;
.page-btn {
min-width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(64, 169, 255, 0.1);
border: 1px solid rgba(64, 169, 255, 0.2);
border-radius: 4px;
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
transition: all 0.3s;
&:hover:not(.disabled):not(.active) {
background-color: rgba(64, 169, 255, 0.2);
border-color: rgba(64, 169, 255, 0.4);
}
&.active {
background-color: #40a9ff;
border-color: #40a9ff;
color: #fff;
}
&.disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
}
}
//
.table-body::-webkit-scrollbar {
width: 6px;
}
.table-body::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.table-body::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, #40a9ff 0%, #1890ff 100%);
border-radius: 3px;
}
.table-body::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, #69c0ff 0%, #40a9ff 100%);
} }
</style> </style>

View File

@ -1,77 +1,29 @@
<template> <template>
<div v-if="visible" class="responsible-dialog-overlay" @click="handleOverlayClick"> <base-dialog
<div class="responsible-dialog" @click.stop> v-model:visible="props.visible"
<!-- 标题栏 --> title="潼南建设项目责任人明细"
<div class="dialog-header"> :table-data="tableData"
<div class="header-title">潼南建设项目责任人明细</div> :table-columns="tableColumns"
<div class="close-btn" @click="handleClose"> :table-height="400"
<el-icon><Close /></el-icon> :total="total"
</div> :current-page="currentPage"
</div> :page-size="pageSize"
:z-index="1000"
<!-- 数据表格 --> :max-width="1100"
<div class="table-section"> @size-change="handleSizeChange"
<div class="table-header"> @current-change="handleCurrentChange"
<div @close="handleClose"
v-for="(column, index) in tableColumns" >
:key="index" <!-- 操作列插槽 -->
class="th" <template #operation="{ row }">
:style="{ width: column.width, flex: column.flex || 'none' }" <span class="detail-link" @click="handleDetail(row)">详情</span>
> </template>
{{ column.label }} </base-dialog>
</div>
</div>
<div class="table-body">
<div
v-for="(item, index) in tableData"
:key="item.id"
class="table-row"
:class="{ 'row-even': index % 2 === 1 }"
>
<div class="td" style="width: 60px">{{ item.id }}</div>
<div class="td" style="width: 100px">{{ item.region }}</div>
<div class="td" style="width: 70px">{{ item.totalCount }}</div>
<div class="td" style="width: 70px">{{ item.whistleblower }}</div>
<div class="td" style="width: 130px">{{ item.constructionUnit }}</div>
<div class="td" style="width: 130px">{{ item.contractorUnit }}</div>
<div class="td" style="width: 120px">{{ item.stationed }}</div>
<div class="td" style="width: 120px">{{ item.districtLevel }}</div>
<div class="td" style="width: 120px">{{ item.cityLevel }}</div>
<div class="td" style="width: 60px">
<span class="detail-link" @click="handleDetail(item)">详情</span>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination">
<span class="total">{{ total }}条数据</span>
<div class="page-btns">
<div class="page-btn" :class="{ disabled: currentPage === 1 }" @click="prevPage">
<el-icon><ArrowLeft /></el-icon>
</div>
<div
v-for="page in visiblePages"
:key="page"
class="page-btn"
:class="{ active: currentPage === page }"
@click="goToPage(page)"
>
{{ page }}
</div>
<div class="page-btn" :class="{ disabled: currentPage === totalPages }" @click="nextPage">
<el-icon><ArrowRight /></el-icon>
</div>
</div>
</div>
</div>
</div>
</template> </template>
<script setup> <script setup>
import { ref, computed, watch } from "vue"; import { ref, computed, watch } from "vue";
import { Close, ArrowLeft, ArrowRight } from "@element-plus/icons-vue"; import baseDialog from "../component/baseDialog.vue";
const props = defineProps({ const props = defineProps({
visible: { visible: {
@ -84,16 +36,16 @@ const emit = defineEmits(["update:visible", "close", "detail"]);
// //
const tableColumns = ref([ const tableColumns = ref([
{ label: "序号", width: "60px" }, { prop: "id", label: "序号", width: "" },
{ label: "区县/镇街", width: "100px" }, { prop: "region", label: "区县/镇街", width: "" },
{ label: "总人数", width: "70px" }, { prop: "totalCount", label: "总人数", width: "" },
{ label: "吹哨人", width: "70px" }, { prop: "whistleblower", label: "吹哨人", width: "" },
{ label: "建设单位包保责任人", width: "130px" }, { prop: "constructionUnit", label: "建设单位包保责任人", width: "" },
{ label: "施工单位包保责任人", width: "130px" }, { prop: "contractorUnit", label: "施工单位包保责任人", width: "" },
{ label: "驻地包保责任人", width: "120px" }, { prop: "stationed", label: "驻地包保责任人", width: "" },
{ label: "区县级包保责任人", width: "120px" }, { prop: "districtLevel", label: "区县级包保责任人", width: "" },
{ label: "市级包保责任人", width: "120px" }, { prop: "cityLevel", label: "市级包保责任人", width: "" },
{ label: "操作", width: "60px" }, { prop: "operation", label: "操作", width: "60px", slot: "operation" },
]); ]);
// //
@ -151,32 +103,21 @@ const handleClose = () => {
emit("close"); emit("close");
}; };
// // base-dialog
const handleOverlayClick = () => {
handleClose();
};
// //
const handleDetail = (item) => { const handleDetail = (item) => {
emit("detail", item); emit("detail", item);
}; };
// //
const prevPage = () => { const handleSizeChange = (size) => {
if (currentPage.value > 1) { pageSize.value = size;
currentPage.value--; currentPage.value = 1;
fetchData(); fetchData();
}
}; };
const nextPage = () => { const handleCurrentChange = (page) => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
fetchData();
}
};
const goToPage = (page) => {
currentPage.value = page; currentPage.value = page;
fetchData(); fetchData();
}; };
@ -200,189 +141,15 @@ watch(
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.responsible-dialog-overlay { //
position: fixed; .detail-link {
top: 0; color: #40a9ff;
left: 0; cursor: pointer;
right: 0; font-size: 13px;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.responsible-dialog { &:hover {
width: 80vw; color: #69c0ff;
max-width: 1100px; text-decoration: underline;
background: linear-gradient(135deg, rgba(20, 50, 90, 0.95) 0%, rgba(10, 30, 60, 0.98) 100%);
border: 1px solid rgba(64, 169, 255, 0.3);
border-radius: 12px;
padding: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
//
.dialog-header {
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin-bottom: 20px;
.header-title {
font-size: 20px;
font-weight: 600;
color: #fff;
padding: 8px 40px;
background: linear-gradient(90deg, transparent 0%, rgba(64, 169, 255, 0.2) 20%, rgba(64, 169, 255, 0.2) 80%, transparent 100%);
border-bottom: 2px solid #40a9ff;
} }
.close-btn {
position: absolute;
right: 0;
top: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
font-size: 20px;
transition: color 0.3s;
&:hover {
color: #fff;
}
}
}
//
.table-section {
background-color: rgba(30, 70, 120, 0.3);
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
.table-header {
display: flex;
background-color: rgba(64, 169, 255, 0.2);
padding: 12px 16px;
.th {
font-size: 13px;
font-weight: 500;
color: #fff;
text-align: center;
}
}
.table-body {
max-height: 40vh;
overflow-y: auto;
.table-row {
display: flex;
padding: 14px 16px;
align-items: center;
transition: background-color 0.3s;
&:hover {
background-color: rgba(64, 169, 255, 0.1);
}
&.row-even {
background-color: rgba(30, 70, 120, 0.2);
}
.td {
font-size: 13px;
color: rgba(255, 255, 255, 0.85);
text-align: center;
.detail-link {
color: #40a9ff;
cursor: pointer;
font-size: 13px;
&:hover {
color: #69c0ff;
text-decoration: underline;
}
}
}
}
}
}
//
.pagination {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 16px;
.total {
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
}
.page-btns {
display: flex;
gap: 8px;
.page-btn {
min-width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(64, 169, 255, 0.1);
border: 1px solid rgba(64, 169, 255, 0.2);
border-radius: 4px;
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
transition: all 0.3s;
&:hover:not(.disabled):not(.active) {
background-color: rgba(64, 169, 255, 0.2);
border-color: rgba(64, 169, 255, 0.4);
}
&.active {
background-color: #40a9ff;
border-color: #40a9ff;
color: #fff;
}
&.disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
}
}
//
.table-body::-webkit-scrollbar {
width: 6px;
}
.table-body::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.table-body::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, #40a9ff 0%, #1890ff 100%);
border-radius: 3px;
}
.table-body::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, #69c0ff 0%, #40a9ff 100%);
} }
</style> </style>

View File

@ -1,74 +1,29 @@
<template> <template>
<div v-if="visible" class="team-dialog-overlay" @click="handleOverlayClick"> <base-dialog
<div class="team-dialog" @click.stop> v-model:visible="props.visible"
<!-- 标题栏 --> title="潼南护路团队成员"
<div class="dialog-header"> :table-data="tableData"
<div class="header-title">潼南护路团队成员</div> :table-columns="tableColumns"
<div class="close-btn" @click="handleClose"> :table-height="400"
<el-icon><Close /></el-icon> :total="total"
</div> :current-page="currentPage"
</div> :page-size="pageSize"
:z-index="2000"
<!-- 数据表格 --> :max-width="900"
<div class="table-section"> @size-change="handleSizeChange"
<div class="table-header"> @current-change="handleCurrentChange"
<div @close="handleClose"
v-for="(column, index) in tableColumns" >
:key="index" <!-- 操作列插槽 -->
class="th" <template #operation="{ row }">
:style="{ width: column.width, flex: column.flex || 'none' }" <span class="view-link" @click="handleView(row)">查看</span>
> </template>
{{ column.label }} </base-dialog>
</div>
</div>
<div class="table-body">
<div
v-for="(item, index) in tableData"
:key="item.id"
class="table-row"
:class="{ 'row-even': index % 2 === 1 }"
>
<div class="td" style="width: 60px">{{ item.id }}</div>
<div class="td" style="width: 100px">{{ item.district }}</div>
<div class="td" style="width: 80px">{{ item.totalCount }}</div>
<div class="td" style="width: 140px">{{ item.trafficDept }}</div>
<div class="td" style="width: 120px">{{ item.roadOrg }}</div>
<div class="td" style="width: 140px">{{ item.maintenance }}</div>
<div class="td" style="width: 80px">{{ item.roadKeeper }}</div>
<div class="td" style="flex: 1">
<span class="view-link" @click="handleView(item)">查看</span>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination">
<div class="page-btn" :class="{ disabled: currentPage === 1 }" @click="prevPage">
上一个
</div>
<div class="page-numbers">
<div
v-for="page in visiblePages"
:key="page"
class="page-num"
:class="{ active: currentPage === page }"
@click="goToPage(page)"
>
{{ page }}
</div>
</div>
<div class="page-btn" :class="{ disabled: currentPage === totalPages }" @click="nextPage">
下一个
</div>
</div>
</div>
</div>
</template> </template>
<script setup> <script setup>
import { ref, computed, watch } from "vue"; import { ref, computed, watch } from "vue";
import { Close } from "@element-plus/icons-vue"; import baseDialog from "../component/baseDialog.vue";
const props = defineProps({ const props = defineProps({
visible: { visible: {
@ -81,14 +36,14 @@ const emit = defineEmits(["update:visible", "close", "view"]);
// //
const tableColumns = ref([ const tableColumns = ref([
{ label: "序号", width: "60px" }, { prop: "id", label: "序号", width: "60px" },
{ label: "区县", width: "100px" }, { prop: "district", label: "区县", width: "100px" },
{ label: "总人数", width: "80px" }, { prop: "totalCount", label: "总人数", width: "80px" },
{ label: "交通主管部门责任人", width: "140px" }, { prop: "trafficDept", label: "交通主管部门责任人", width: "140px" },
{ label: "公路机构责任人", width: "120px" }, { prop: "roadOrg", label: "公路机构责任人", width: "120px" },
{ label: "养护站道班责任人", width: "140px" }, { prop: "maintenance", label: "养护站道班责任人", width: "140px" },
{ label: "护路员", width: "80px" }, { prop: "roadKeeper", label: "护路员", width: "80px" },
{ label: "操作", flex: "1" }, { prop: "operation", label: "操作", width: "auto", slot: "operation" },
]); ]);
// //
@ -160,32 +115,21 @@ const handleClose = () => {
emit("close"); emit("close");
}; };
// // base-dialog
const handleOverlayClick = () => {
handleClose();
};
// //
const handleView = (item) => { const handleView = (item) => {
emit("view", item); emit("view", item);
}; };
// //
const prevPage = () => { const handleSizeChange = (size) => {
if (currentPage.value > 1) { pageSize.value = size;
currentPage.value--; currentPage.value = 1;
fetchData(); fetchData();
}
}; };
const nextPage = () => { const handleCurrentChange = (page) => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
fetchData();
}
};
const goToPage = (page) => {
currentPage.value = page; currentPage.value = page;
fetchData(); fetchData();
}; };
@ -209,202 +153,16 @@ watch(
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.team-dialog-overlay { //
position: fixed; .view-link {
top: 0; color: #40a9ff;
left: 0; cursor: pointer;
right: 0; font-size: 13px;
bottom: 0; transition: color 0.3s;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.team-dialog { &:hover {
width: 80vw; color: #69c0ff;
max-width: 900px; text-decoration: underline;
background: linear-gradient(135deg, rgba(20, 50, 90, 0.95) 0%, rgba(10, 30, 60, 0.98) 100%);
border: 1px solid rgba(64, 169, 255, 0.3);
border-radius: 12px;
padding: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
//
.dialog-header {
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin-bottom: 20px;
.header-title {
font-size: 20px;
font-weight: 600;
color: #fff;
padding: 8px 40px;
background: linear-gradient(90deg, transparent 0%, rgba(64, 169, 255, 0.2) 20%, rgba(64, 169, 255, 0.2) 80%, transparent 100%);
border-bottom: 2px solid #40a9ff;
} }
.close-btn {
position: absolute;
right: 0;
top: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
font-size: 20px;
transition: color 0.3s;
&:hover {
color: #fff;
}
}
}
//
.table-section {
background-color: rgba(30, 70, 120, 0.3);
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
.table-header {
display: flex;
background-color: rgba(64, 169, 255, 0.2);
padding: 12px 16px;
.th {
font-size: 14px;
font-weight: 500;
color: #fff;
text-align: center;
}
}
.table-body {
max-height: 40vh;
overflow-y: auto;
.table-row {
display: flex;
padding: 14px 16px;
align-items: center;
transition: background-color 0.3s;
&:hover {
background-color: rgba(64, 169, 255, 0.1);
}
&.row-even {
background-color: rgba(30, 70, 120, 0.2);
}
.td {
font-size: 13px;
color: rgba(255, 255, 255, 0.85);
text-align: center;
//
.view-link {
color: #40a9ff;
cursor: pointer;
font-size: 13px;
transition: color 0.3s;
&:hover {
color: #69c0ff;
text-decoration: underline;
}
}
}
}
}
}
//
.pagination {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
.page-btn {
padding: 6px 16px;
background-color: rgba(64, 169, 255, 0.1);
border: 1px solid rgba(64, 169, 255, 0.3);
border-radius: 4px;
font-size: 13px;
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
transition: all 0.3s;
&:hover:not(.disabled) {
background-color: rgba(64, 169, 255, 0.2);
border-color: rgba(64, 169, 255, 0.5);
}
&.disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
.page-numbers {
display: flex;
gap: 8px;
.page-num {
min-width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(64, 169, 255, 0.1);
border: 1px solid rgba(64, 169, 255, 0.3);
border-radius: 4px;
font-size: 13px;
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
transition: all 0.3s;
&:hover:not(.active) {
background-color: rgba(64, 169, 255, 0.2);
border-color: rgba(64, 169, 255, 0.5);
}
&.active {
background-color: #40a9ff;
border-color: #40a9ff;
color: #fff;
}
}
}
}
//
.table-body::-webkit-scrollbar {
width: 6px;
}
.table-body::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.table-body::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, #40a9ff 0%, #1890ff 100%);
border-radius: 3px;
}
.table-body::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, #69c0ff 0%, #40a9ff 100%);
} }
</style> </style>

View File

@ -1,50 +1,45 @@
<template> <template>
<div class="tunnel-info-dialog" v-if="visible"> <base-dialog
<!-- 四个角的装饰 --> v-model:visible="props.visible"
<div class="corner corner-top-left"></div> :title="rescueTeamData.title"
<div class="corner corner-top-right"></div> :table-data="[]"
<div class="corner corner-bottom-left"></div> :table-columns="[]"
<div class="corner corner-bottom-right"></div> :table-height="0"
:total="0"
<div class="dialog-header"> :current-page="1"
<div class="header-title"> :page-size="10"
<span class="title-text">{{ rescueTeamData.title }}</span> :z-index="1000"
:max-width="400"
:show-filter="false"
:show-pagination="false"
@close="handleClose"
>
<!-- 标题栏下方自定义插槽 -->
<template #header>
<div class="dialog-content">
<div class="info-item" v-for="(item, index) in rescueTeamData.items" :key="index">
<label class="info-label">{{ item.label }}</label>
<span class="info-value">{{ item.value }}</span>
</div>
</div>
<div class="dialog-imgs">
<img <img
class="title-icon" class="dialog-img"
src="../../../assets/RiskWarning_img/图标_media_dvr@2x.png" v-for="(img, index) in rescueTeamData.imgs"
:key="index"
:src="img"
alt="" alt=""
/> />
</div> </div>
<!-- <div class="close-btn" @click="closeDialog"> </template>
<span class="close-icon">×</span> </base-dialog>
</div> -->
<div class="close-btn" style="pointer-events: auto" @click="closeDialog">
<el-icon color="#5DD7F6"><Close /></el-icon>
</div>
</div>
<div class="dialog-content">
<div class="info-item" v-for="(item, index) in rescueTeamData.items" :key="index">
<label class="info-label">{{ item.label }}</label>
<span class="info-value">{{ item.value }}</span>
</div>
</div>
<div class="dialog-imgs">
<img
class="dialog-img"
v-for="(img, index) in rescueTeamData.imgs"
:key="index"
:src="img"
alt=""
/>
</div>
</div>
</template> </template>
<script setup> <script setup>
import { defineProps, defineEmits } from "vue"; import { defineProps, defineEmits } from "vue";
import { Close } from "@element-plus/icons-vue"; import baseDialog from "../component/baseDialog.vue";
defineProps({ const props = defineProps({
visible: { visible: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -145,150 +140,55 @@ defineExpose({
rescueTeamData, rescueTeamData,
}); });
const emit = defineEmits(["close"]); const emit = defineEmits(["update:visible", "close"]);
const closeDialog = () => { const handleClose = () => {
emit("update:visible", false);
emit("close"); emit("close");
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
// - //
@function vw($px) { .dialog-content {
@return calc($px / 1920 * 100vw); padding: 16px 20px;
}
.tunnel-info-dialog {
max-width: vw(300);
width: vw(300);
max-height: vw(600);
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
background: rgba(64, 169, 255, 0.2);
border: 1px solid rgba(64, 169, 255, 0.3);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
position: fixed;
top: 20%;
left: 45%;
z-index: 1000;
&::-webkit-scrollbar { .info-item {
display: none;
}
//
.corner {
position: absolute;
width: vw(20);
height: vw(20);
border: 1px solid #40a9ff;
pointer-events: none;
&.corner-top-left {
top: 0;
left: 0;
border-right: none;
border-bottom: none;
}
&.corner-top-right {
top: 0;
right: 0;
border-left: none;
border-bottom: none;
}
&.corner-bottom-left {
bottom: 0;
left: 0;
border-right: none;
border-top: none;
}
&.corner-bottom-right {
bottom: 0;
right: 0;
border-left: none;
border-top: none;
}
}
.dialog-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
padding: vw(16) vw(20); padding: 8px 0;
border-bottom: 1px solid rgba(64, 169, 255, 0.2); border-bottom: 1px solid rgba(64, 169, 255, 0.1);
.header-title { .info-label {
display: flex; font-size: 14px;
align-items: center; color: rgba(255, 255, 255, 0.7);
gap: vw(10); flex: 0 0 100px;
.title-text {
font-size: vw(16);
font-weight: 600;
color: #fff;
}
.title-icon {
width: vw(24);
height: vw(24);
}
} }
.close-btn { .info-value {
cursor: pointer; font-size: 14px;
transition: all 0.3s; color: #4fecff;
flex: 1;
.close-icon {
font-size: vw(24);
color: rgba(255, 255, 255, 0.7);
}
&:hover {
.close-icon {
color: #fff;
}
}
} }
} }
.dialog-content { .info-item:last-child {
padding: vw(16) vw(20); border-bottom: none;
.info-item {
display: flex;
align-items: center;
padding: vw(8) 0;
border-bottom: 1px solid rgba(64, 169, 255, 0.1);
.info-label {
font-size: vw(14);
color: rgba(255, 255, 255, 0.7);
flex: 0 0 vw(100);
}
.info-value {
font-size: vw(14);
color: #4fecff;
flex: 1;
}
}
.info-item:last-child {
border-bottom: none;
}
} }
} }
//
.dialog-imgs { .dialog-imgs {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: vw(6); gap: 6px;
padding: vw(16) vw(20); padding: 16px 20px;
.dialog-img { .dialog-img {
width: vw(75); width: 75px;
height: vw(75); height: 75px;
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
} }
} }
</style> </style>

View File

@ -1,138 +1,104 @@
<template> <template>
<div <base-dialog
v-if="visible" v-model:visible="props.visible"
class="warning-dialog-overlay" title="响应情况"
@click="handleOverlayClick" :table-data="tableData"
:table-columns="tableColumns"
:table-height="300"
:total="total"
:current-page="currentPage"
:page-size="pageSize"
:z-index="1000"
:max-width="1000"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@close="handleClose"
> >
<div class="warning-dialog" @click.stop> <!-- 筛选区域插槽 -->
<!-- 标题栏 --> <template #filter>
<div class="dialog-header"> <div class="filter-row">
<div class="header-title">响应情况</div> <div class="filter-item">
<div class="close-btn" @click="handleClose"> <el-select :teleported="false"
<el-icon><Close /></el-icon> v-model="filterForm.warningLevel"
placeholder="预警等级"
class="filter-select"
>
<el-option
v-for="option in warningLevelOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
<div class="filter-item">
<el-select :teleported="false"
v-model="filterForm.region"
placeholder="影响区域"
class="filter-select"
>
<el-option
v-for="option in regionOptionsWithAll"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
<div class="filter-item">
<el-select :teleported="false"
v-model="filterForm.isEnded"
placeholder="是否结束"
class="filter-select"
>
<el-option
v-for="option in isEndedOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
<div class="filter-item">
<el-select :teleported="false"
v-model="filterForm.isResponded"
placeholder="是否回应"
class="filter-select"
>
<el-option
v-for="option in isRespondedOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
<div class="filter-item">
<el-button type="primary" class="search-btn" @click="handleSearch">
<el-icon><Search /></el-icon>
查询
</el-button>
</div> </div>
</div> </div>
</template>
<!-- 筛选区域 --> <!-- 预警等级列插槽 -->
<div class="filter-section"> <template #warningLevel="{ row }">
<div class="filter-row"> <span class="warning-level" :class="row.levelClass">{{ row.warningLevel }}</span>
<div class="filter-item"> </template>
<el-select
v-model="filterForm.warningLevel"
placeholder="预警等级"
class="filter-select"
>
<el-option
v-for="option in warningLevelOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
<div class="filter-item">
<el-select
v-model="filterForm.region"
placeholder="影响区域"
class="filter-select"
>
<el-option
v-for="option in regionOptionsWithAll"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
<div class="filter-item">
<el-select
v-model="filterForm.isEnded"
placeholder="是否结束"
class="filter-select"
>
<el-option
v-for="option in isEndedOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
<div class="filter-item">
<el-select
v-model="filterForm.isResponded"
placeholder="是否回应"
class="filter-select"
>
<el-option
v-for="option in isRespondedOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
<div class="filter-item">
<el-button type="primary" class="search-btn" @click="handleSearch">
<el-icon><Search /></el-icon>
查询
</el-button>
</div>
</div>
</div>
<!-- 数据表格 --> <!-- 已叫应列插槽 -->
<div class="table-section"> <template #called="{ row }">
<el-table <span class="clickable-cell" @click="handleCalledClick(row)">{{ row.called }}</span>
:data="tableData" </template>
:height="tableHeight" </base-dialog>
style="width: 100%"
:header-cell-style="headerCellStyle"
:cell-style="cellStyle"
size="small"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="weatherSource" label="气象来源" width="80" />
<el-table-column prop="warningLevel" label="预警等级" width="100">
<template #default="{ row }">
<span class="warning-level" :class="row.levelClass">{{ row.warningLevel }}</span>
</template>
</el-table-column>
<el-table-column prop="region" label="影响区域" width="100" />
<el-table-column prop="warningTime" label="预警时间" width="160" />
<el-table-column prop="endTime" label="结束时间" width="160" />
<el-table-column prop="impactPoints" label="影响点数量" width="100" />
<el-table-column prop="called" label="已叫应" width="80">
<template #default="{ row }">
<span class="clickable-cell" @click="handleCalledClick(row)">{{ row.called }}</span>
</template>
</el-table-column>
<el-table-column prop="responded" label="已回应" width="80" />
<el-table-column prop="notResponded" label="未回应" width="80" />
<el-table-column prop="urged" label="已催告" width="80" />
</el-table>
</div>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 40]"
:total="total"
background
layout="prev, pager, next, jumper, ->, total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
</template> </template>
<script setup> <script setup>
import { ref, computed, watch } from "vue"; import { ref, computed, watch } from "vue";
import { Close, Search, ArrowLeft, ArrowRight } from "@element-plus/icons-vue"; import { Search } from "@element-plus/icons-vue";
import baseDialog from "../component/baseDialog.vue";
import { import {
warningLevelOptions, warningLevelOptions,
regionOptionsWithAll, regionOptionsWithAll,
@ -159,17 +125,17 @@ const filterForm = ref({
// //
const tableColumns = ref([ const tableColumns = ref([
{ label: "序号", width: "60px" }, { prop: "id", label: "序号", width: "" },
{ label: "气象来源", width: "80px" }, { prop: "weatherSource", label: "气象来源", width: "" },
{ label: "预警等级", width: "100px" }, { prop: "warningLevel", label: "预警等级", width: "", slot: "warningLevel" },
{ label: "影响区域", width: "100px" }, { prop: "region", label: "影响区域", width: "" },
{ label: "预警时间", width: "160px" }, { prop: "warningTime", label: "预警时间", width: "" },
{ label: "结束时间", width: "160px" }, { prop: "endTime", label: "结束时间", width: "" },
{ label: "影响点数量", width: "100px" }, { prop: "impactPoints", label: "影响点数量", width: "" },
{ label: "已叫应", width: "80px" }, { prop: "called", label: "已叫应", width: "", slot: "called" },
{ label: "已回应", width: "80px" }, { prop: "responded", label: "已回应", width: "" },
{ label: "未回应", width: "80px" }, { prop: "notResponded", label: "未回应", width: "" },
{ label: "已催告", width: "80px" }, { prop: "urged", label: "已催告", width: "" },
]); ]);
// //
@ -245,10 +211,7 @@ const handleClose = () => {
emit("close"); emit("close");
}; };
// // base-dialog
const handleOverlayClick = () => {
handleClose();
};
// //
const handleCalledClick = () => { const handleCalledClick = () => {
@ -292,305 +255,106 @@ watch(
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.warning-dialog-overlay { //
position: fixed; .filter-row {
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: 12px;
z-index: 1000; flex-wrap: wrap;
} }
.warning-dialog { .filter-item {
width: 80vw; .filter-select {
max-width: 1000px; width: 120px;
background: linear-gradient(
135deg,
rgba(20, 50, 90, 0.95) 0%,
rgba(10, 30, 60, 0.98) 100%
);
border: 1px solid rgba(64, 169, 255, 0.3);
border-radius: 12px;
padding: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
// :deep(.el-input__wrapper) {
.dialog-header {
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin-bottom: 20px;
.header-title {
font-size: 20px;
font-weight: 600;
color: #fff;
padding: 8px 40px;
background: linear-gradient(
90deg,
transparent 0%,
rgba(64, 169, 255, 0.2) 20%,
rgba(64, 169, 255, 0.2) 80%,
transparent 100%
);
border-bottom: 2px solid #40a9ff;
}
.close-btn {
position: absolute;
right: 0;
top: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
font-size: 20px;
transition: color 0.3s;
&:hover {
color: #fff;
}
}
}
//
.filter-section {
margin-bottom: 20px;
.filter-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.filter-item {
.filter-select {
width: 120px;
:deep(.el-input__wrapper) {
background-color: rgba(30, 70, 120, 0.4);
border: 1px solid rgba(64, 169, 255, 0.3);
box-shadow: none;
border-radius: 4px;
.el-input__inner {
color: #fff;
font-size: 13px;
&::placeholder {
color: rgba(255, 255, 255, 0.5);
}
}
.el-input__suffix {
.el-icon {
color: rgba(255, 255, 255, 0.6);
}
}
}
}
.search-btn {
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
border: none;
border-radius: 4px;
padding: 0 20px;
height: 32px;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
&:hover {
background: linear-gradient(135deg, #69c0ff 0%, #40a9ff 100%);
}
.el-icon {
font-size: 14px;
}
}
}
}
//
.table-section {
background-color: rgba(30, 70, 120, 0.3);
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
:deep(.el-table) {
background-color: transparent;
border: none;
.el-table__header-wrapper {
background-color: transparent;
}
.el-table__body-wrapper {
background-color: transparent;
}
.el-table__row {
background-color: transparent;
transition: background-color 0.3s;
&:hover {
background-color: rgba(64, 169, 255, 0.1);
}
&:nth-child(even) {
background-color: rgba(30, 70, 120, 0.2);
}
}
.el-table__cell {
color: rgba(255, 255, 255, 0.85);
font-size: 13px;
text-align: center;
border: none;
padding: 12px 16px;
}
.el-table__header .el-table__cell {
background-color: rgba(64, 169, 255, 0.2);
color: #fff;
font-size: 14px;
font-weight: 500;
text-align: center;
border: none;
padding: 12px 16px;
}
}
.warning-level {
display: inline-block;
padding: 2px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
&.level-red {
background-color: rgba(255, 77, 79, 0.2);
color: #ff4d4f;
border: 1px solid rgba(255, 77, 79, 0.4);
}
&.level-orange {
background-color: rgba(255, 122, 69, 0.2);
color: #ff7a45;
border: 1px solid rgba(255, 122, 69, 0.4);
}
&.level-yellow {
background-color: rgba(250, 219, 95, 0.2);
color: #fadb5f;
border: 1px solid rgba(250, 219, 95, 0.4);
}
&.level-blue {
background-color: rgba(64, 169, 255, 0.2);
color: #40a9ff;
border: 1px solid rgba(64, 169, 255, 0.4);
}
}
.clickable-cell {
cursor: pointer;
color: #40a9ff;
font-weight: 500;
transition: all 0.3s;
&:hover {
color: #69c0ff;
text-shadow: 0 0 8px rgba(105, 192, 255, 0.6);
}
}
}
//
.pagination {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 16px;
:deep(.el-pagination) {
.el-pagination__total {
color: rgba(255, 255, 255, 0.6);
font-size: 13px;
}
.el-pagination__sizes .el-input__inner {
color: #fff;
background-color: rgba(30, 70, 120, 0.4); background-color: rgba(30, 70, 120, 0.4);
border: 1px solid rgba(64, 169, 255, 0.3); border: 1px solid rgba(64, 169, 255, 0.3);
font-size: 13px; box-shadow: none;
}
.el-pagination__btn {
color: rgba(255, 255, 255, 0.8);
background-color: rgba(64, 169, 255, 0.1);
border: 1px solid rgba(64, 169, 255, 0.2);
border-radius: 4px; border-radius: 4px;
transition: all 0.3s;
&:hover:not(.is-disabled) { .el-input__inner {
background-color: rgba(64, 169, 255, 0.2);
border-color: rgba(64, 169, 255, 0.4);
}
&.is-disabled {
opacity: 0.4;
}
}
.el-pagination__item {
color: rgba(255, 255, 255, 0.8);
background-color: rgba(64, 169, 255, 0.1);
border: 1px solid rgba(64, 169, 255, 0.2);
border-radius: 4px;
transition: all 0.3s;
&:hover:not(.is-disabled) {
background-color: rgba(64, 169, 255, 0.2);
border-color: rgba(64, 169, 255, 0.4);
}
&.is-active {
background-color: #40a9ff;
border-color: #40a9ff;
color: #fff; color: #fff;
font-size: 13px;
&::placeholder {
color: rgba(255, 255, 255, 0.5);
}
}
.el-input__suffix {
.el-icon {
color: rgba(255, 255, 255, 0.6);
}
} }
} }
}
.el-pagination__jump .el-input__inner { .search-btn {
color: #fff; background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
background-color: rgba(30, 70, 120, 0.4); border: none;
border: 1px solid rgba(64, 169, 255, 0.3); border-radius: 4px;
font-size: 13px; padding: 0 20px;
height: 32px;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
&:hover {
background: linear-gradient(135deg, #69c0ff 0%, #40a9ff 100%);
}
.el-icon {
font-size: 14px;
} }
} }
} }
// //
:deep(.el-table__body-wrapper::-webkit-scrollbar) { .warning-level {
display: none; display: inline-block;
padding: 2px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
&.level-red {
background-color: rgba(255, 77, 79, 0.2);
color: #ff4d4f;
border: 1px solid rgba(255, 77, 79, 0.4);
}
&.level-orange {
background-color: rgba(255, 122, 69, 0.2);
color: #ff7a45;
border: 1px solid rgba(255, 122, 69, 0.4);
}
&.level-yellow {
background-color: rgba(250, 219, 95, 0.2);
color: #fadb5f;
border: 1px solid rgba(250, 219, 95, 0.4);
}
&.level-blue {
background-color: rgba(64, 169, 255, 0.2);
color: #40a9ff;
border: 1px solid rgba(64, 169, 255, 0.4);
}
} }
:deep(.el-table__body-wrapper) { //
-ms-overflow-style: none; .clickable-cell {
scrollbar-width: none; cursor: pointer;
color: #40a9ff;
font-weight: 500;
transition: all 0.3s;
&:hover {
color: #69c0ff;
text-shadow: 0 0 8px rgba(105, 192, 255, 0.6);
}
} }
// //

View File

@ -20,9 +20,10 @@
<el-select <el-select
v-model="filterForm.warningLevel" v-model="filterForm.warningLevel"
placeholder="请选择" placeholder="请选择"
class="filter-select" class="filter-select el-select"
clearable clearable
size="small" size="small"
:teleported="false"
> >
<el-option <el-option
v-for="item in warningLevelOptions" v-for="item in warningLevelOptions"
@ -40,6 +41,7 @@
class="filter-select" class="filter-select"
clearable clearable
size="small" size="small"
:teleported="false"
> >
<el-option <el-option
v-for="item in regionOptions" v-for="item in regionOptions"
@ -50,13 +52,14 @@
</el-select> </el-select>
</div> </div>
<div class="filter-item"> <div class="filter-item">
<span class="filter-label">是否结束</span> <span class="filter-label">是否失效</span>
<el-select <el-select
v-model="filterForm.isEnded" v-model="filterForm.isEnded"
placeholder="请选择" placeholder="请选择"
class="filter-select" class="filter-select"
clearable clearable
size="small" size="small"
:teleported="false"
> >
<el-option <el-option
v-for="item in isEndedOptions" v-for="item in isEndedOptions"
@ -66,18 +69,30 @@
/> />
</el-select> </el-select>
</div> </div>
<div class="filter-item">
<span class="filter-label">生效时间</span>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="-"
start-placeholder="开始时间"
end-placeholder="结束时间"
size="small"
popper-class="custom-date-picker"
:teleported="false"
:prefix-icon="Calendar"
@change="handleDateChange"
/>
</div>
</div> </div>
</template> </template>
<!-- 预警等级列插槽 --> <!-- 预警等级列插槽 -->
<template #warningLevel="{ row }"> <template #warningLevel="{ row }">
<span <span :class="['warning-level-tag', getWarningClass(row.warningLevel)]">{{
:class="[ row.warningLevel
'warning-level-tag', }}</span>
getWarningClass(row.warningLevel),
]"
>{{ row.warningLevel }}</span
>
</template> </template>
<!-- 影响点数量列插槽 --> <!-- 影响点数量列插槽 -->
@ -106,6 +121,14 @@ const props = defineProps({
}, },
}); });
//
const dateRange = ref([]);
//
const handleDateChange = (val) => {
filterForm.value.dateRange = val;
};
const emit = defineEmits(["update:visible", "close", "impactClick"]); const emit = defineEmits(["update:visible", "close", "impactClick"]);
// //
@ -130,13 +153,23 @@ const tableHeight = ref(300);
// //
const tableColumns = ref([ const tableColumns = ref([
{ prop: 'index', label: '序号', width: '60' }, { prop: "index", label: "序号", width: "" },
{ prop: 'warningLevel', label: '预警等级', width: '100', slot: 'warningLevel' }, {
{ prop: 'weatherType', label: '气象类型', width: '100' }, prop: "warningLevel",
{ prop: 'region', label: '行政区域', width: '100' }, label: "预警等级",
{ prop: 'warningTime', label: '预警时间', width: '160' }, width: "",
{ prop: 'endTime', label: '结束时间', width: '160' }, slot: "warningLevel",
{ prop: 'impactCount', label: '影响点数量', width: '100', slot: 'impactCount' }, },
{ prop: "weatherType", label: "气象类型", width: "" },
{ prop: "region", label: "影响区域", width: "" },
{ prop: "warningTime", label: "生效时间", width: "" },
{ prop: "endTime", label: "失效时间", width: "" },
{
prop: "impactCount",
label: "影响点数量",
width: "",
slot: "impactCount",
},
]); ]);
// //
@ -182,7 +215,8 @@ const tableData = ref([
impactCount: 2, impactCount: 2,
}, },
]); ]);
tableData.value.push(...tableData.value);
tableData.value.push(...tableData.value);
// //
const currentPage = ref(1); const currentPage = ref(1);
const pageSize = ref(10); const pageSize = ref(10);
@ -240,84 +274,29 @@ watch(
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
//
.filter-item {
display: flex;
align-items: center;
gap: 8px;
.filter-label {
// font-size: 13px;
.filter-section { color: rgba(255, 255, 255, 0.8);
margin-bottom: 16px; white-space: nowrap;
padding: 0 24px;
.filter-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
} }
.filter-item { .filter-select {
display: flex; width: 120px;
align-items: center;
gap: 8px;
.filter-label { :deep(.el-input__wrapper) {
font-size: 13px; background-color: rgba(30, 70, 120, 0.4);
color: rgba(255, 255, 255, 0.8); border: 1px solid rgba(64, 169, 255, 0.3);
white-space: nowrap; box-shadow: none;
} border-radius: 4px;
width: 210px !important;
.filter-select { .el-input__inner {
width: 120px;
:deep(.el-input__wrapper) {
background-color: rgba(30, 70, 120, 0.4);
border: 1px solid rgba(64, 169, 255, 0.3);
box-shadow: none;
border-radius: 4px;
width: 210px !important;
.el-input__inner {
color: #fff;
font-size: 13px;
&::placeholder {
color: rgba(255, 255, 255, 0.4);
}
}
.el-input__suffix {
.el-icon {
color: rgba(255, 255, 255, 0.6);
}
}
}
}
.date-range-picker {
:deep(.el-input__wrapper) {
background-color: rgba(30, 70, 120, 0.4);
border: 1px solid rgba(64, 169, 255, 0.3);
box-shadow: none;
border-radius: 4px;
width: 210px !important;
.el-input__inner {
color: #fff;
font-size: 13px;
background: transparent;
&::placeholder {
color: rgba(255, 255, 255, 0.4);
}
}
.el-input__suffix {
.el-icon {
color: rgba(255, 255, 255, 0.6);
}
}
}
:deep(.el-range-input) {
background: transparent;
color: #fff; color: #fff;
font-size: 13px; font-size: 13px;
@ -326,243 +305,105 @@ watch(
} }
} }
:deep(.el-range-separator) { .el-input__suffix {
color: rgba(255, 255, 255, 0.6); .el-icon {
font-size: 13px; color: rgba(255, 255, 255, 0.6);
}
} }
} }
} }
} }
// //
.table-section { .warning-level-tag {
width: 100%; display: inline-block;
margin-bottom: 20px; padding: 2px 10px;
padding: 0 24px; border-radius: 4px;
font-size: 12px;
font-weight: 500;
:deep(.el-table) { &.warning-red {
background-color: transparent; background-color: rgba(255, 77, 79, 0.2);
border: none;
.el-table__header-wrapper {
background-color: transparent;
}
.el-table__body-wrapper {
background-color: transparent;
}
.el-table__row {
width: 100% !important;
background-color: transparent;
transition: background-color 0.3s;
&:hover {
background-color: rgba(64, 169, 255, 0.1);
}
&:nth-child(even) {
background-color: rgba(30, 70, 120, 0.2);
}
}
.el-table__cell {
color: rgba(255, 255, 255, 0.85);
font-size: 13px;
text-align: center;
border: none;
padding: 12px 16px;
}
.el-table__header {
width: 100% !important;
}
.el-table__header .el-table__cell {
background-color: #1c4979;
color: #fff;
font-size: 14px;
font-weight: 500;
text-align: center;
border: none;
padding: 12px 16px;
}
}
//
.warning-level-tag {
display: inline-block;
padding: 2px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
&.warning-red {
background-color: rgba(255, 77, 79, 0.2);
color: #ff4d4f;
border: 1px solid rgba(255, 77, 79, 0.4);
}
&.warning-orange {
background-color: rgba(255, 122, 0, 0.2);
color: #ff7a00;
border: 1px solid rgba(255, 122, 0, 0.4);
}
&.warning-yellow {
background-color: rgba(250, 219, 20, 0.2);
color: #fadb14;
border: 1px solid rgba(250, 219, 20, 0.4);
}
&.warning-blue {
background-color: rgba(64, 169, 255, 0.2);
color: #40a9ff;
border: 1px solid rgba(64, 169, 255, 0.4);
}
}
//
.impact-count {
color: #ff4d4f; color: #ff4d4f;
cursor: pointer; border: 1px solid rgba(255, 77, 79, 0.4);
font-weight: 600; }
transition: all 0.3s;
&:hover { &.warning-orange {
color: #ff7875; background-color: rgba(255, 122, 0, 0.2);
text-shadow: 0 0 8px rgba(255, 77, 79, 0.6); color: #ff7a00;
} border: 1px solid rgba(255, 122, 0, 0.4);
}
&.warning-yellow {
background-color: rgba(250, 219, 20, 0.2);
color: #fadb14;
border: 1px solid rgba(250, 219, 20, 0.4);
}
&.warning-blue {
background-color: rgba(64, 169, 255, 0.2);
color: #40a9ff;
border: 1px solid rgba(64, 169, 255, 0.4);
} }
} }
// //
.pagination { .impact-count {
display: flex; color: #ff4d4f;
align-items: center; cursor: pointer;
justify-content: flex-end; font-weight: 600;
gap: 16px; transition: all 0.3s;
padding: 12px;
:deep(.el-pagination) { &:hover {
.el-pagination__total { color: #ff7875;
color: rgba(255, 255, 255, 0.6); text-shadow: 0 0 8px rgba(255, 77, 79, 0.6);
font-size: 13px;
}
.el-pagination__sizes .el-input__inner {
color: #fff;
background-color: rgba(30, 70, 120, 0.4);
border: 1px solid rgba(64, 169, 255, 0.3);
font-size: 13px;
}
.el-pagination__btn {
color: rgba(255, 255, 255, 0.8);
background-color: rgba(64, 169, 255, 0.1);
border: 1px solid rgba(64, 169, 255, 0.2);
border-radius: 4px;
transition: all 0.3s;
&:hover:not(.is-disabled) {
background-color: rgba(64, 169, 255, 0.2);
border-color: rgba(64, 169, 255, 0.4);
}
&.is-disabled {
opacity: 0.4;
}
}
.el-pagination__item {
color: rgba(255, 255, 255, 0.8);
background-color: rgba(64, 169, 255, 0.1);
border: 1px solid rgba(64, 169, 255, 0.2);
border-radius: 4px;
transition: all 0.3s;
&:hover:not(.is-disabled) {
background-color: rgba(64, 169, 255, 0.2);
border-color: rgba(64, 169, 255, 0.4);
}
&.is-active {
background-color: #40a9ff;
border-color: #40a9ff;
color: #fff;
}
}
.el-pagination__jump .el-input__inner {
color: #fff;
background-color: rgba(30, 70, 120, 0.4);
border: 1px solid rgba(64, 169, 255, 0.3);
font-size: 13px;
}
} }
} }
:deep(.el-table__body) { //
width: 100% !important; .filter-section {
} margin-bottom: vw(20);
//
:deep(.el-table__body-wrapper::-webkit-scrollbar) {
display: none;
}
:deep(.el-table__body-wrapper) { .filter-row {
-ms-overflow-style: none; display: flex;
scrollbar-width: none; align-items: center;
} flex-wrap: wrap;
</style> gap: 12px;
<style lang="scss">
//
.el-picker-panel {
background: rgba(20, 50, 90, 0.98) !important;
border: 1px solid rgba(64, 169, 255, 0.3) !important;
.el-picker-panel__content {
color: #fff;
.el-date-table th {
color: rgba(255, 255, 255, 0.6);
border-bottom-color: rgba(64, 169, 255, 0.3);
}
.el-date-table td {
color: rgba(255, 255, 255, 0.8);
&.selected .el-date-table-cell {
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
}
&.today span {
color: #40a9ff;
font-weight: bold;
}
&.in-range,
&.start-date,
&.end-date {
background: rgba(64, 169, 255, 0.2);
}
}
} }
.el-picker-panel__footer { .filter-item {
border-top: 1px solid rgba(64, 169, 255, 0.3); .filter-select {
width: vw(150);
.el-button { :deep(.el-input__wrapper) {
background: rgba(30, 70, 120, 0.4); background-color: rgba(30, 70, 120, 0.4);
border-color: rgba(64, 169, 255, 0.3); border: 1px solid rgba(64, 169, 255, 0.3);
color: #fff; box-shadow: none;
border-radius: vw(4);
&:hover { .el-input__inner {
background: rgba(64, 169, 255, 0.3); color: #fff;
font-size: vw(13);
&::placeholder {
color: rgba(255, 255, 255, 0.5);
}
}
.el-input__suffix {
.el-icon {
color: rgba(255, 255, 255, 0.6);
}
}
} }
} }
.el-button--primary { .search-btn {
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%); background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
border: none; border: none;
border-radius: vw(4);
padding: 0 vw(24);
height: vw(32);
font-size: vw(13);
&:hover { &:hover {
background: linear-gradient(135deg, #69c0ff 0%, #40a9ff 100%); background: linear-gradient(135deg, #69c0ff 0%, #40a9ff 100%);
@ -571,7 +412,9 @@ watch(
} }
} }
.el-table--fit .el-table__inner-wrapper:before { :deep(.el-range-editor.el-input__wrapper) {
width: 0% !important; width: 240px !important;
height: 30px !important;
background-color: #122C46 !important;
} }
</style> </style>

View File

@ -9,12 +9,21 @@
<span class="error-text">{{ error }}</span> <span class="error-text">{{ error }}</span>
<button class="retry-btn" @click="loadMapData">重试</button> <button class="retry-btn" @click="loadMapData">重试</button>
</div> </div>
<TongnanCenterCardDialog
ref="tongnanCenterCardDialog"
:visible.sync="visible"
:value="value"
:z-index="zIndex"
:width="width"
></TongnanCenterCardDialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted } from "vue"; import { ref, onMounted, onUnmounted } from "vue";
import axios from 'axios'; import axios from "axios";
import { request } from "@/utils/request";
import TongnanCenterCardDialog from "@/views/RiskWarning/Dialog/tongnanCenterCardDialog.vue";
const mapContainer = ref(null); const mapContainer = ref(null);
const loading = ref(false); const loading = ref(false);
@ -29,6 +38,163 @@ const emit = defineEmits(["districtClick"]);
const selectedDistrict = ref(null); const selectedDistrict = ref(null);
let selectedLayer = null; let selectedLayer = null;
//
const affectedCountyData = ref({
byName: {},
sortedList: [],
});
//
const getAffectedCountyData = async () => {
try {
const res = await request({
url: "snow-ops-platform/weather-warning/affected-county",
method: "GET",
params: {
start: "2025-03-03 12:33:00",
end: "2025-07-30 12:33:00",
},
});
console.log("受影响对象数据:", res);
if (res.code === "00000" && res.data) {
//
const warningStats = countWarningsByCounty(res.data);
console.log("区县预警统计:", warningStats);
affectedCountyData.value = warningStats;
loadMapData();
}
} catch (error) {
console.error("获取受影响对象数据失败:", error);
}
};
//
const getMainWarningColor = (levels) => {
if (!levels || Object.keys(levels).length === 0) return "#1890ff"; //
// > > >
const priority = ["红色", "橙色", "黄色", "蓝色"];
//
const sortedLevels = Object.entries(levels)
.map(([level, count]) => ({ level, count }))
.sort((a, b) => b.count - a.count);
//
const maxCount = sortedLevels[0].count;
const maxLevels = sortedLevels.filter((item) => item.count === maxCount);
if (maxLevels.length === 1) {
return getColorByLevel(maxLevels[0].level);
} else {
//
for (const level of priority) {
if (maxLevels.some((item) => item.level === level)) {
return getColorByLevel(level);
}
}
return getColorByLevel(maxLevels[0].level);
}
};
//
const getColorByLevel = (level) => {
const colorMap = {
红色预警: "#FF4D4F",
橙色预警: "#EC7345",
黄色预警: "#FBC23C",
蓝色预警: "#3799FC",
未知: "#1890ff",
};
return colorMap[level] || "#1890ff";
};
//
const countWarningsByCounty = (data) => {
if (!data || !Array.isArray(data)) return {};
const stats = {};
data.forEach((item) => {
//
const rawCountyName =
item.countyName || item.name || item.qxmc || item.gl1Qxmc;
const riskLevel =
item.riskLevel || item.level || item.warningLevel || item.gl1Yjdj;
//
const countyName = simplifyDistrictName(rawCountyName);
if (countyName) {
if (!stats[countyName]) {
stats[countyName] = {
total: 0,
levels: {},
};
}
//
stats[countyName].total += 1;
//
const level = riskLevel || "未知";
if (stats[countyName].levels[level]) {
stats[countyName].levels[level] += 1;
} else {
stats[countyName].levels[level] = 1;
}
}
});
//
if (stats["渝北区"] || stats["江北区"]) {
const yubeiData = stats["渝北区"] || {
total: 0,
levels: {},
};
const jiangbeiData = stats["江北区"] || {
total: 0,
levels: {},
};
//
const total = yubeiData.total + jiangbeiData.total;
//
const levels = { ...yubeiData.levels };
Object.entries(jiangbeiData.levels).forEach(([level, count]) => {
if (levels[level]) {
levels[level] += count;
} else {
levels[level] = count;
}
});
//
stats["两江新区"] = {
total,
levels,
};
//
delete stats["渝北区"];
delete stats["江北区"];
}
//
const sortedList = Object.entries(stats)
.map(([name, data]) => ({
name,
total: data.total,
levels: data.levels,
}))
.sort((a, b) => b.total - a.total);
return {
byName: stats, // { : { total: , levels: { : } } }
sortedList: sortedList, // [{ name: , total: , levels: { : } }]
};
};
// GeoJSON API - 使 // GeoJSON API - 使
const GEOJSON_URL = const GEOJSON_URL =
"https://geo.datav.aliyun.com/areas_v3/bound/500000_full.json"; "https://geo.datav.aliyun.com/areas_v3/bound/500000_full.json";
@ -43,30 +209,29 @@ const loadMapData = async () => {
// URLMap=dev // URLMap=dev
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const isDev = urlParams.get('Map') === 'dev'; const isDev = urlParams.get("Map") === "dev";
let geoJsonData; let geoJsonData;
if (isDev) { if (isDev) {
// //
const response = await fetch(GEOJSON_URL) const response = await fetch(GEOJSON_URL);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`) throw new Error(`HTTP error! status: ${response.status}`);
} }
geoJsonData = await response.json() geoJsonData = await response.json();
} else { } else {
// //
const response = await axios.get('/aliyun-geo/bound/500000_full.json'); const response = await axios.get("/aliyun-geo/bound/500000_full.json");
geoJsonData = response.data; geoJsonData = response.data;
if (!geoJsonData) { if (!geoJsonData) {
throw new Error('地图数据为空'); throw new Error("地图数据为空");
} }
} }
// // // //
processDistrictMerge(geoJsonData); processDistrictMerge(geoJsonData);
initMap(geoJsonData) initMap(geoJsonData);
} catch (err) { } catch (err) {
console.error("加载地图数据失败:", err); console.error("加载地图数据失败:", err);
error.value = "地图数据加载失败,请检查网络连接"; error.value = "地图数据加载失败,请检查网络连接";
@ -151,6 +316,17 @@ const getCentroid = (coordinates) => {
return count > 0 ? [sumLat / count, sumLng / count] : null; return count > 0 ? [sumLat / count, sumLng / count] : null;
}; };
//
const simplifyDistrictName = (name) => {
const nameMap = {
酉阳土家族苗族自治县: "酉阳县",
秀山土家族苗族自治县: "秀山县",
彭水苗族土家族自治县: "彭水县",
石柱土家族自治县: "石柱县",
};
return nameMap[name] || name;
};
// //
const initMap = (geoJsonData) => { const initMap = (geoJsonData) => {
if (!mapContainer.value) return; if (!mapContainer.value) return;
@ -164,7 +340,7 @@ const initMap = (geoJsonData) => {
// //
mapInstance = new window.L.Map(mapContainer.value, { mapInstance = new window.L.Map(mapContainer.value, {
center: [29.563, 106.551], // center: [29.563, 106.551], //
zoom: 7, zoom: 6,
minZoom: 6, minZoom: 6,
maxZoom: 18, maxZoom: 18,
zoomControl: false, zoomControl: false,
@ -185,72 +361,80 @@ const initMap = (geoJsonData) => {
// GeoJSON // GeoJSON
geoJsonLayer = new window.L.GeoJSON(geoJsonData, { geoJsonLayer = new window.L.GeoJSON(geoJsonData, {
style: (feature) => ({ style: (feature) => {
fillColor: "#1890ff", const districtName = feature.properties.name;
weight: 1.5, let fillColor = "#132C44"; //
opacity: 0.6, //
color: "#40a9ff", if (
fillOpacity: 0.15, affectedCountyData.value &&
}), affectedCountyData.value.byName &&
affectedCountyData.value.byName[districtName]
) {
const districtData = affectedCountyData.value.byName[districtName];
console.log(districtData.levels);
fillColor = getMainWarningColor(districtData.levels);
}
return {
fillColor: fillColor,
weight: 1.5,
opacity: 0.6,
color: "#40a9ff",
fillOpacity: 1,
};
},
onEachFeature: (feature, layer) => { onEachFeature: (feature, layer) => {
if (feature.properties && feature.properties.name) { if (feature.properties && feature.properties.name) {
// //
layer.on("click", (e) => { // layer.on("click", (e) => {
// // //
if (selectedLayer) { // if (selectedLayer) {
selectedLayer.setStyle({ // selectedLayer.setStyle({
fillColor: "#1890ff", // fillColor: "#1890ff",
fillOpacity: 0.15, // fillOpacity: 0.15,
weight: 1.5, // weight: 1.5,
color: "#40a9ff", // color: "#40a9ff",
}); // });
} // }
// //
// // layer.setStyle({
layer.setStyle({ // fillColor: "#ff4d4f",
fillColor: "#ff4d4f", // fillOpacity: 0.6,
fillOpacity: 0.6, // weight: 2.5,
weight: 2.5, // color: "#ff7875",
color: "#ff7875", // });
}); // selectedLayer = layer;
// selectedDistrict.value = feature.properties.name;
selectedLayer = layer; // //
selectedDistrict.value = feature.properties.name; // emit("districtClick", {
// name: feature.properties.name,
// // feature: feature,
emit("districtClick", { // latlng: e.latlng,
name: feature.properties.name, // });
feature: feature, // //
latlng: e.latlng, // mapInstance.panTo(e.latlng, { animate: true, duration: 0.5 });
}); // });
//
mapInstance.panTo(e.latlng, { animate: true, duration: 0.5 });
});
// //
layer.on("mouseover", () => { // layer.on("mouseover", () => {
layer.setStyle({ // layer.setStyle({
fillColor: "#1890ff", // fillColor: "#1890ff",
fillOpacity: 0.4, // fillOpacity: 0.4,
weight: 2, // weight: 2,
color: "#69c0ff", // color: "#69c0ff",
}); // });
}); // });
// layer.on("mouseout", () => {
layer.on("mouseout", () => { // layer.setStyle({
layer.setStyle({ // fillColor: "#1890ff",
fillColor: "#1890ff", // fillOpacity: 0.15,
fillOpacity: 0.15, // weight: 1.5,
weight: 1.5, // color: "#40a9ff",
color: "#40a9ff", // });
}); // });
});
// popup // popup
layer.bindPopup(`<div class="map-popup"> // layer.bindPopup(`<div class="map-popup">
<strong>${feature.properties.name}</strong> // <strong>${feature.properties.name}</strong>
</div>`); // </div>`);
} }
}, },
}); });
@ -270,51 +454,52 @@ const initMap = (geoJsonData) => {
} }
if (centroid) { if (centroid) {
const displayName = simplifyDistrictName(feature.properties.name);
const label = window.L.divIcon({ const label = window.L.divIcon({
className: "district-label", className: "district-label",
html: `<div class="label-content">${feature.properties.name}</div>`, html: `<div class="label-content">${displayName}</div>`,
iconSize: [80, 30], iconSize: [80, 30],
iconAnchor: [40, 15], iconAnchor: [40, 15],
}); });
const marker = window.L.marker(centroid, { icon: label }); const marker = window.L.marker(centroid, { icon: label });
marker.on("click", (e) => { // marker.on("click", (e) => {
// // //
if (selectedLayer) { // if (selectedLayer) {
selectedLayer.setStyle({ // selectedLayer.setStyle({
fillColor: "#1E3A8A", // fillColor: "#1E3A8A",
fillOpacity: 0.3, // fillOpacity: 0.3,
weight: 2, // weight: 2,
}); // });
} // }
// // //
geoJsonLayer.eachLayer((layer) => { // geoJsonLayer.eachLayer((layer) => {
if ( // if (
layer.feature && // layer.feature &&
layer.feature.properties.name === feature.properties.name // layer.feature.properties.name === feature.properties.name
) { // ) {
layer.setStyle({ // layer.setStyle({
fillColor: "#ff4d4f", // fillColor: "#ff4d4f",
fillOpacity: 0.6, // fillOpacity: 0.6,
weight: 2.5, // weight: 2.5,
color: "#ff7875", // color: "#ff7875",
}); // });
selectedLayer = layer; // selectedLayer = layer;
selectedDistrict.value = feature.properties.name; // selectedDistrict.value = feature.properties.name;
} // }
}); // });
// // //
emit("districtClick", { // emit("districtClick", {
name: feature.properties.name, // name: feature.properties.name,
feature: feature, // feature: feature,
latlng: e.latlng, // latlng: e.latlng,
}); // });
// // //
mapInstance.panTo(e.latlng, { animate: true, duration: 0.5 }); // mapInstance.panTo(e.latlng, { animate: true, duration: 0.5 });
}); // });
mapInstance.addLayer(marker); mapInstance.addLayer(marker);
} }
} }
@ -330,6 +515,9 @@ const initMap = (geoJsonData) => {
// //
onMounted(() => { onMounted(() => {
//
getAffectedCountyData();
// Leaflet // Leaflet
if (typeof window.L === "undefined") { if (typeof window.L === "undefined") {
// Leaflet CSS JS // Leaflet CSS JS

View File

@ -5,7 +5,11 @@
:style="{ zIndex: props.zIndex }" :style="{ zIndex: props.zIndex }"
@click="handleOverlayClick" @click="handleOverlayClick"
> >
<div class="base-dialog" @click.stop :style="{ maxWidth: `${props.maxWidth}px` }"> <div
class="base-dialog"
@click.stop
:style="{ maxWidth: `${props.maxWidth}px` }"
>
<!-- 四个角的装饰 --> <!-- 四个角的装饰 -->
<div class="corner corner-top-left"></div> <div class="corner corner-top-left"></div>
<div class="corner corner-top-right"></div> <div class="corner corner-top-right"></div>
@ -29,7 +33,6 @@
<div class="filter-section" v-if="props.showFilter"> <div class="filter-section" v-if="props.showFilter">
<slot name="filter"></slot> <slot name="filter"></slot>
</div> </div>
<!-- 数据表格 --> <!-- 数据表格 -->
<div class="table-section" v-if="props.tableData.length > 0"> <div class="table-section" v-if="props.tableData.length > 0">
<el-table <el-table
@ -44,7 +47,9 @@
v-for="column in props.tableColumns" v-for="column in props.tableColumns"
:key="column.prop" :key="column.prop"
:prop="column.prop" :prop="column.prop"
:label="column.label" :label="column.label"
:width="column.width"
:min-width="column.width === 'auto' ? '100px' : undefined"
> >
<template v-if="column.slot" #default="{ row }"> <template v-if="column.slot" #default="{ row }">
<slot :name="column.slot" :row="row"></slot> <slot :name="column.slot" :row="row"></slot>
@ -54,13 +59,15 @@
</div> </div>
<!-- 分页 --> <!-- 分页 -->
<div class="pagination" v-if="props.showPagination && props.tableData.length > 0"> <div
class="pagination"
v-if="props.showPagination && props.tableData.length > 0"
>
<el-pagination <el-pagination
:current-page="props.currentPage" :current-page="props.currentPage"
:page-size="props.pageSize" :page-size="props.pageSize"
:page-sizes="props.pageSizes" :page-sizes="props.pageSizes"
:total="props.total" :total="props.total"
background
layout="prev, pager, next, jumper, ->, total" layout="prev, pager, next, jumper, ->, total"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@ -73,7 +80,6 @@
<script setup> <script setup>
import { ref, watch } from "vue"; import { ref, watch } from "vue";
import { Close } from "@element-plus/icons-vue"; import { Close } from "@element-plus/icons-vue";
import { ElTable, ElTableColumn, ElPagination } from "element-plus";
const props = defineProps({ const props = defineProps({
visible: { visible: {
@ -130,7 +136,14 @@ const props = defineProps({
}, },
}); });
const emit = defineEmits(["update:visible", "close", "size-change", "current-change", "update:current-page", "update:page-size"]); const emit = defineEmits([
"update:visible",
"close",
"size-change",
"current-change",
"update:current-page",
"update:page-size",
]);
// //
const handleClose = () => { const handleClose = () => {
@ -202,7 +215,7 @@ const cellStyle = () => {
justify-content: center; justify-content: center;
} }
:deep(.el-table--small) { :deep(.el-table--small) {
background: #16334E; background: #16334e;
} }
.base-dialog { .base-dialog {
@ -310,13 +323,20 @@ const cellStyle = () => {
.filter-section { .filter-section {
margin-bottom: 16px; margin-bottom: 16px;
padding: 0 24px; padding: 0 24px;
:deep(.el-select__wrapper) {
background: #122c46 !important;
}
} }
// //
.table-section { .table-section {
width: 100%; width: 100%;
padding: 0 24px; padding: 0 24px;
background: #16334E background: #16334e;
}
:deep(.el-table__inner-wrapper:before) {
width: 0% !important;
} }
// //
@ -324,53 +344,128 @@ const cellStyle = () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
gap: 16px; padding: 12px 24px;
padding: 12px; :deep(.btn-prev) {
background: #16334E; background: #19334b !important;
color: #fff;
border: 1px solid #1f4f80;
}
:deep(.btn-next) {
background: #19334b !important;
color: #fff;
border: 1px solid #1f4f80;
}
:deep(.number) {
background: #19334b !important;
color: #2d90e3;
border: 1px solid #2d90e3;
}
:deep(.el-pagination) { :deep(.el-pagination) {
background: #16334E; background: #16334e !important;
gap: 10px;
.el-pagination__total { }
color: rgba(255, 255, 255, 0.6); :deep(.el-pager) {
background: #16334e !important;
gap: 10px;
}
:deep(.el-input__wrapper) {
background: #16334e !important;
.el-input__inner {
color: #fff !important;
} }
}
.el-pagination__sizes .el-input__inner { :deep(.el-pagination__jump) {
background: #16334E; color: #fff !important;
border: 1px solid rgba(64, 169, 255, 0.3); }
:deep(.el-pagination__total) {
color: #fff !important;
}
}
//
.el-select {
.el-select__wrapper {
border-radius: 4px;
box-shadow: 0 0 0 1px #c5c5c5 inset;
height: 40px;
line-height: 40px;
background: #122c46;
color: #fff;
}
.el-select__wrapper.is-hovering,
.el-select__wrapper.is-focused {
box-shadow: 0 0 0 1px #3380ec inset;
// filter: brightness(1.3);
}
}
:deep(.el-select__placeholder) {
font-weight: 400;
color: #40a9ff !important;
}
//
.el-popper {
//
.el-select-dropdown {
background: #122c46;
.el-select-dropdown__item {
display: flex;
align-items: center;
color: #fff; color: #fff;
} background-color: #122c46;
text-stroke: 0px #3a6fbf;
-webkit-text-stroke: 0px #3a6fbf;
height: 40px;
.el-pagination__btn { &.is-hovering {
background: #16334E; background: #0f2e6d;
border: 1px solid rgba(64, 169, 255, 0.3); font-weight: bold;
color: rgba(255, 255, 255, 0.8); color: #7bc8ef;
text-stroke: 0px #7bc8ef;
&:hover:not(.is-disabled) { font-style: normal;
background: rgba(64, 169, 255, 0.2); text-transform: none;
-webkit-text-stroke: 0px #7bc8ef;
} }
} }
.el-pagination__item {
background: #16334E;
border: 1px solid rgba(64, 169, 255, 0.3);
color: rgba(255, 255, 255, 0.8);
&:hover:not(.is-disabled) {
background: rgba(64, 169, 255, 0.2);
}
&.is-active {
background: #40a9ff;
border-color: #40a9ff;
}
}
.el-pagination__jump .el-input__inner {
background: #16334E;
border: 1px solid rgba(64, 169, 255, 0.3);
color: #fff;
}
} }
} }
</style>
/* 修改小三角箭头的背景色和边框 */
:deep(.el-popper__arrow::before) {
background-color: #122c46 !important;
}
:deep(.custom-select-popper) {
background-color: #122c46 !important; /* 整体背景色 */
border: 1px solid #122c46 !important; /* 边框颜色 */
border-radius: 8px !important; /* 圆角 */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important; /* 阴影 */
}
/* 覆盖下拉框的白色边框 */
:deep(.el-popper.is-light .el-popper__arrow::before) {
background-color: #122c46 !important;
border-color: #122c46 !important;
}
:deep(.el-select-dropdown) {
border: 1px solid #122c46 !important;
background-color: #122c46 !important;
}
:deep(.el-popper[data-popper-placement^="bottom"]) {
border: 1px solid #122c46 !important;
}
:deep(.el-popper[data-popper-placement^="top"]) {
border: 1px solid #122c46 !important;
}
:deep(.el-popper__arrow) {
border-color: #122c46 !important;
}
:deep(.el-popper__arrow::after) {
border-color: #122c46 !important;
}
</style>

View File

@ -0,0 +1,224 @@
/* ========================================
el-date-picker 自定义主题样式
背景色: #122C45
文字色: #FFFFFF
主题色/选中背景色: #289DFF
======================================== */
/* 防止样式被覆盖,提高优先级 */
.custom-date-picker {
/* 弹出框整体背景色 */
background-color: #122C45 !important;
/* 弹出框边框(主题色) */
border: 1px solid #289DFF !important;
/* 可选:阴影效果 */
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.3) !important;
}
.el-date-editor .el-range-input {
color: #289DFF !important;
}
/* ========== 1. 顶部栏(年月切换区域) ========== */
.custom-date-picker .el-date-picker__header,
.custom-date-picker .el-picker-panel__header {
background-color: #122C45 !important;
border-bottom: 1px solid rgba(40, 157, 255, 0.3) !important;
}
.custom-date-picker .el-date-picker__header-label,
.custom-date-picker .el-picker-panel__header-label {
color: #FFFFFF !important;
}
.custom-date-picker .el-picker-panel__icon-btn {
color: #FFFFFF !important;
}
.custom-date-picker .el-picker-panel__icon-btn:hover {
color: #289DFF !important;
}
/* 年份/月份快速选择面板 */
.custom-date-picker .el-year-table td .el-year-table__cell__text,
.custom-date-picker .el-month-table td .el-month-table__cell__text {
color: #FFFFFF !important;
background-color: transparent !important;
}
.custom-date-picker .el-year-table td .el-year-table__cell__text:hover,
.custom-date-picker .el-month-table td .el-month-table__cell__text:hover {
color: #289DFF !important;
background-color: rgba(40, 157, 255, 0.2) !important;
}
.custom-date-picker .el-year-table td.current .el-year-table__cell__text,
.custom-date-picker .el-month-table td.current .el-month-table__cell__text {
background-color: #289DFF !important;
color: #FFFFFF !important;
}
/* ========== 2. 星期栏 ========== */
.custom-date-picker .el-date-table th,
.custom-date-picker .el-date-table__header th {
color: #289DFF !important;
background-color: #122C45 !important;
border-bottom: 1px solid rgba(40, 157, 255, 0.3) !important;
}
/* ========== 3. 日期单元格(默认状态) ========== */
.custom-date-picker .el-date-table td .el-date-table-cell__text {
color: #FFFFFF !important;
background-color: transparent !important;
border-radius: 4px;
transition: all 0.2s ease;
}
.custom-date-picker .el-date-table td .el-date-table-cell__text:hover {
color: #289DFF !important;
background-color: rgba(40, 157, 255, 0.2) !important;
}
/* ========== 4. 选中状态(核心样式) ========== */
.custom-date-picker .el-date-table td.current .el-date-table-cell__text {
background-color: #289DFF !important;
color: #FFFFFF !important;
}
/* ========== 5. 今天按钮样式 ========== */
.custom-date-picker .el-date-table td.today .el-date-table-cell__text {
color: #289DFF !important;
font-weight: bold;
position: relative;
}
.custom-date-picker .el-date-table td.today.current .el-date-table-cell__text {
color: #FFFFFF !important;
}
/* ========== 6. 其他月份日期(灰色显示) ========== */
.custom-date-picker .el-date-table td.next-month .el-date-table-cell__text,
.custom-date-picker .el-date-table td.prev-month .el-date-table-cell__text {
color: rgba(255, 255, 255, 0.35) !important;
}
/* ========== 7. 禁用日期样式 ========== */
.custom-date-picker .el-date-table td.disabled .el-date-table-cell__text {
color: rgba(255, 255, 255, 0.2) !important;
background-color: rgba(0, 0, 0, 0.2) !important;
cursor: not-allowed;
}
/* ========== 8. 底部按钮区域 ========== */
.custom-date-picker .el-picker-panel__footer {
background-color: #122C45 !important;
border-top: 1px solid rgba(40, 157, 255, 0.3) !important;
}
.custom-date-picker .el-button--text {
color: #289DFF !important;
background-color: transparent !important;
}
.custom-date-picker .el-button--text:hover {
color: #66ccff !important;
background-color: transparent !important;
}
.custom-date-picker .el-button--primary {
background-color: #289DFF !important;
border-color: #289DFF !important;
}
.custom-date-picker .el-button--primary:hover {
background-color: #1a7acc !important;
border-color: #1a7acc !important;
}
/* ========== 9. 小三角箭头 ========== */
/* Element Plus 版本 */
.custom-date-picker .el-popper__arrow::before {
background-color: #122C45 !important;
border: 1px solid #289DFF !important;
}
/* Element UI 版本 */
.custom-date-picker .popper__arrow {
border-bottom-color: #122C45 !important;
}
.custom-date-picker .popper__arrow::after {
border-bottom-color: #122C45 !important;
}
/* ========== 10. 范围选择器样式 ========== */
/* 范围选择器整体背景 */
.custom-date-picker.el-picker-panel--date-range {
background-color: #122C45 !important;
}
/* 范围选择左右面板 */
.custom-date-picker .el-date-range-picker__content {
background-color: #122C45 !important;
}
/* 范围选择头部 */
.custom-date-picker .el-date-range-picker__header {
color: #FFFFFF !important;
background-color: #122C45 !important;
}
/* 范围选择 - 起始日期 */
.custom-date-picker .el-date-table td.start-date .el-date-table-cell__text,
.custom-date-picker .el-date-table td.end-date .el-date-table-cell__text {
background-color: #289DFF !important;
color: #FFFFFF !important;
border-radius: 4px;
}
/* 范围选择 - 中间区间 */
.custom-date-picker .el-date-table td.in-range .el-date-table-cell__text {
background-color: rgba(40, 157, 255, 0.25) !important;
color: #FFFFFF !important;
border-radius: 0;
}
/* 范围选择 - 区间悬停效果 */
.custom-date-picker .el-date-table td.in-range .el-date-table-cell__text:hover {
background-color: rgba(40, 157, 255, 0.4) !important;
}
/* ========== 11. 滚动条样式 ========== */
.custom-date-picker .el-scrollbar__bar {
background-color: rgba(255, 255, 255, 0.1) !important;
}
.custom-date-picker .el-scrollbar__thumb {
background-color: #289DFF !important;
}
/* ========== 12. 时间选择器样式(如有) ========== */
.custom-date-picker .el-time-spinner__item {
color: #FFFFFF !important;
background-color: #122C45 !important;
}
.custom-date-picker .el-time-spinner__item:hover {
background-color: rgba(40, 157, 255, 0.2) !important;
}
.custom-date-picker .el-time-spinner__item.active {
background-color: #289DFF !important;
color: #FFFFFF !important;
}
/* ========== 13. 快捷选项样式 ========== */
.custom-date-picker .el-picker-panel__shortcut {
color: #FFFFFF !important;
background-color: #122C45 !important;
}
.custom-date-picker .el-picker-panel__shortcut:hover {
background-color: rgba(40, 157, 255, 0.2) !important;
color: #289DFF !important;
}
.el-range__icon{
color: #289DFF !important;
}

View File

@ -0,0 +1,65 @@
// 下拉
.el-select {
.el-select__wrapper {
border-radius: 4px;
box-shadow: 0 0 0 1px #c5c5c5 inset;
height: 30px !important;
line-height: 30px !important;
background: #122c46;
.is-transparent {
background: transparent;
border: none;
box-shadow: none;
outline: none;
color: #fff !important;
}
.el-select__placeholder {
color: #16D9E0;
font-weight: 400;
}
.el-select__placeholder {
font-weight: 400;
}
}
.el-select__wrapper.is-hovering,
.el-select__wrapper.is-focused {
box-shadow: 0 0 0 1px #3380ec inset;
// filter: brightness(1.3);
}
}
// 下拉选项浮窗
.el-popper {
border-radius: 6px;
// 下拉选项
.el-select-dropdown {
background: #122c46;
.el-select-dropdown__item {
display: flex;
align-items: center;
color: #fff;
background-color: #122c46;
text-stroke: 0px #16D9E0;
-webkit-text-stroke: 0px #16D9E0;
height: 40px;
&.is-hovering {
background: #0f2e6d;
font-weight: bold;
color: #7bc8ef;
text-stroke: 0px #7bc8ef;
font-style: normal;
text-transform: none;
-webkit-text-stroke: 0px #7bc8ef;
border-radius: 8px;
}
}
}
}

View File

@ -26,7 +26,7 @@
<div class="corner corner-bottom-right"></div> <div class="corner corner-bottom-right"></div>
<!-- 中心数据展示卡片 --> <!-- 中心数据展示卡片 -->
<div class="center-info-card-container"> <div class="center-info-card-container">
<div <!-- <div
class="center-info-card" class="center-info-card"
@click="openDialog('tongnanResponsible')" @click="openDialog('tongnanResponsible')"
v-if="showCenterCard.type === 'first'" v-if="showCenterCard.type === 'first'"
@ -44,7 +44,7 @@
<span class="info-unit"></span> <span class="info-unit"></span>
</div> </div>
</div> </div>
</div> </div> -->
<div <div
class="center-info-card" class="center-info-card"
@ -280,6 +280,9 @@ import tongnanTeamDialog from "./Dialog/tongnanTeamDialog.vue";
import warningSituationDialog from "./Dialog/warningSituationDialog.vue"; import warningSituationDialog from "./Dialog/warningSituationDialog.vue";
import tunnelInfoDialog from "./Dialog/tunnelInfoDialog.vue"; import tunnelInfoDialog from "./Dialog/tunnelInfoDialog.vue";
import './component/el-select.scss'
import './component/date-picker-theme.scss'
// //
const dialogVisible = ref({ const dialogVisible = ref({
responseSituation: false, responseSituation: false,
@ -616,4 +619,31 @@ onMounted(() => {
} }
} }
} }
//
.el-popper {
//
.el-select-dropdown {
background: #122c46;
.el-select-dropdown__item {
display: flex;
align-items: center;
color: #fff;
background-color: #122c46;
text-stroke: 0px #3a6fbf;
-webkit-text-stroke: 0px #3a6fbf;
height: 40px;
&.is-hovering {
background: #0f2e6d;
font-weight: bold;
color: #7bc8ef;
text-stroke: 0px #7bc8ef;
font-style: normal;
text-transform: none;
-webkit-text-stroke: 0px #7bc8ef;
}
}
}
}
</style> </style>

View File

@ -188,7 +188,7 @@ import imgHelp from "../../assets/RiskWarning_img/响应图标5@2x.png";
import imgCheck from "../../assets/RiskWarning_img/抽查人数icon@2x.png"; import imgCheck from "../../assets/RiskWarning_img/抽查人数icon@2x.png";
// //
const setRefreshLeftData = inject('setRefreshLeftData'); const setRefreshLeftData = inject("setRefreshLeftData");
const props = defineProps({ const props = defineProps({
visible: { visible: {
@ -212,7 +212,7 @@ onMounted(() => {
if (props.visible) { if (props.visible) {
init(); init();
} }
// //
if (setRefreshLeftData) { if (setRefreshLeftData) {
setRefreshLeftData(init); setRefreshLeftData(init);
@ -287,7 +287,7 @@ const weatherWarningData = ref([
// //
const impactData = ref([ const impactData = ref([
{ name: "路段", count: 830 }, { name: "路段", count: 0 },
{ name: "桥梁", count: 312 }, { name: "桥梁", count: 312 },
{ name: "隧道", count: 405 }, { name: "隧道", count: 405 },
{ name: "边坡", count: 634 }, { name: "边坡", count: 634 },
@ -377,7 +377,23 @@ const districtLoadLoad = async () => {
if (res.code == "00000") { if (res.code == "00000") {
const data = res.data; const data = res.data;
if (data) { if (data) {
districtData.value = data; //
const sortedData = data.sort((a, b) => {
const totalA =
(a.roadSectionCount || 0) +
(a.bridgeCount || 0) +
(a.tunnelCount || 0) +
(a.slopeCount || 0) +
(a.projectCount || 0);
const totalB =
(b.roadSectionCount || 0) +
(b.bridgeCount || 0) +
(b.tunnelCount || 0) +
(b.slopeCount || 0) +
(b.projectCount || 0);
return totalB - totalA; //
});
districtData.value = sortedData;
} }
} }
} catch (error) { } catch (error) {
@ -497,6 +513,7 @@ const totalValue = computed(() => {
// //
const getBarHeight = (value) => { const getBarHeight = (value) => {
const actualValue = value.count || value.value; const actualValue = value.count || value.value;
if (!actualValue || actualValue == 0) return "0";
// 10%100% // 10%100%
const height = (actualValue / totalValue.value) * 200; const height = (actualValue / totalValue.value) * 200;
// 5% // 5%
@ -517,19 +534,7 @@ const tableHeight = computed(() => {
}); });
// //
const districtData = ref([ const districtData = ref([]);
{ extension: "江北区", road: 1, bridge: 1, tunnel: 1, slope: 8, project: 11 },
{ extension: "南岸区", road: 1, bridge: 2, tunnel: 2, slope: 6, project: 12 },
{
extension: "九龙坡区",
road: 2,
bridge: 1,
tunnel: 1,
slope: 9,
project: 9,
},
{ extension: "万州区", road: 1, bridge: 2, tunnel: 2, slope: 11, project: 7 },
]);
// //
const responseStats = ref([ const responseStats = ref([
@ -941,8 +946,17 @@ const cellStyle = () => ({
// //
.district-table-section { .district-table-section {
height: vw(100); height: vw(150);
overflow: hidden; overflow: hidden;
:deep(.el-table__empty-block) {
max-height: 40px !important;
.el-table__empty-text {
line-height: 30px !important;
}
}
:deep(.el-table__empty-block) {
min-height: 40px !important;
}
:deep(.el-table) { :deep(.el-table) {
background: transparent; background: transparent;
@ -968,7 +982,7 @@ const cellStyle = () => ({
.el-table__body-wrapper { .el-table__body-wrapper {
background: transparent; background: transparent;
overflow-y: auto; overflow-y: auto;
max-height: calc(#{vw(100)} - #{vw(40)}); /* 减去表头高度 */ max-height: calc(#{vw(150)} - #{vw(0)}); /* 减去表头高度 */
scrollbar-width: none; scrollbar-width: none;
-ms-overflow-style: none; -ms-overflow-style: none;

View File

@ -27,12 +27,8 @@
<!-- 管控路段数 --> <!-- 管控路段数 -->
<div class="control-section"> <div class="control-section">
<div class="control-title display jc_sb ai_center"> <div class="control-title display jc_sb ai_center">
<div class="f1"> <div class="f1">管控路段数 <span class="control-num">2</span></div>
管控路段数 <span class="control-num">2</span> <div class="f1">管控项目 <span class="control-num">2</span></div>
</div>
<div class="f1">
管控项目 <span class="control-num">2</span>
</div>
</div> </div>
<div style="display: flex; justify-content: space-between"> <div style="display: flex; justify-content: space-between">
<div class="control-grid"> <div class="control-grid">
@ -196,27 +192,12 @@
</template> </template>
<script setup> <script setup>
import { ref, computed } from "vue"; import { ref, computed, onMounted } from "vue";
import { request } from "@/utils/request";
import SectionHeader from "./component/sectionHeader.vue"; import SectionHeader from "./component/sectionHeader.vue";
import { Calendar } from "@element-plus/icons-vue"; import { Calendar } from "@element-plus/icons-vue";
const emit = defineEmits(["openClearanceSituation", "openControlSituation"]);
//
const handleControlClick = (item) => {
if (item.label === "封闭管控数") {
emit("openClearanceSituation");
} else if (item.label === "关闭驻地数") {
emit("openControlSituation");
}
};
//
const handleBlockClick = () => {
emit("openClearanceSituation");
};
import icon1 from "../../assets/RiskWarning_img/icon1@2x.png"; import icon1 from "../../assets/RiskWarning_img/icon1@2x.png";
import icon2 from "../../assets/RiskWarning_img/icon2@2x.png"; import icon2 from "../../assets/RiskWarning_img/icon2@2x.png";
import icon3 from "../../assets/RiskWarning_img/icon3@2x.png"; import icon3 from "../../assets/RiskWarning_img/icon3@2x.png";
@ -234,40 +215,159 @@ import icon62 from "../../assets/RiskWarning_img/路径62@2x.png";
import icon621 from "../../assets/RiskWarning_img/路径62@2x (1).png"; import icon621 from "../../assets/RiskWarning_img/路径62@2x (1).png";
import icon622 from "../../assets/RiskWarning_img/路径62@2x (2).png"; import icon622 from "../../assets/RiskWarning_img/路径62@2x (2).png";
const emit = defineEmits(["openClearanceSituation", "openControlSituation"]);
//
onMounted(() => {
getYhYjllList();
getYhYjllListMaterials();
});
//
const getYhYjllList = async () => {
try {
const res = await request({
url: "/snow-ops-platform/yhYjll/list",
method: "GET",
params: {
// longitude: 114.305556,
// latitude: 22.624722,
// maxDistance: 10 // 1000km
},
});
console.log(res);
if (res.code == "00000") {
let gl1Rysls = 0; //
let gl1Yjllmcs = 0; //
res.data.forEach((item) => {
gl1Yjllmcs = gl1Yjllmcs + 1;
gl1Rysls = Number(item.gl1Rysl) + gl1Rysls;
});
if (gl1Rysls > 10000) {
gl1Rysls = (gl1Rysls / 10000).toFixed(2);
resourceData.value[1].unit = "万人";
} else {
resourceData.value[1].value = gl1Rysls;
resourceData.value[1].unit = "人";
}
resourceData.value[0].value = gl1Yjllmcs;
}
} catch (error) {
console.error("获取应急力量列表失败:", error);
}
};
//
const getIconByType = (type) => {
const iconMap = {
1: icon1, //
2: icon2, //
3: icon3, //
4: icon4, //
};
return iconMap[type] || icon1;
};
//
const getYhYjllListMaterials = async () => {
try {
const res = await request({
url: "/snow-ops-platform/yhYjll/listMaterials",
method: "GET",
params: {},
});
console.log("物资列表:", res);
if (res.code == "00000" && res.data) {
let equipment = 0; //
let materials = 0; //
res.data.forEach((item) => {
if (item.gl1Wzlx == 1) {
equipment = equipment + extractAndSumNumbers(item.gl1Wzsl);
} else if (item.gl1Wzlx == 2) {
materials = materials + extractAndSumNumbers(item.gl1Wzsl);
}
});
if (materials > 10000) {
resourceData.value[3].value = (materials / 10000).toFixed(2);
resourceData.value[3].unit = "万件";
} else {
resourceData.value[3].value = materials;
resourceData.value[3].unit = "件";
}
console.log(equipment, materials);
//
resourceData.value[2].value = equipment || "0";
}
} catch (error) {
console.error("获取物资列表失败:", error);
}
};
//
const extractAndSumNumbers = (value) => {
//
if (typeof value === "number") {
return value;
}
// 0
if (!value || typeof value !== "string") return 0;
//
const numbers = value.match(/\d+\.?\d*/g);
if (!numbers) return 0;
//
return numbers.reduce((sum, num) => sum + Number(num), 0);
};
//
const handleControlClick = (item) => {
if (item.label === "封闭管控数") {
emit("openClearanceSituation");
} else if (item.label === "关闭驻地数") {
emit("openControlSituation");
}
};
//
const handleBlockClick = () => {
emit("openClearanceSituation");
};
// //
const dateRange = ref([]); const dateRange = ref([]);
// //
const resourceData = [ const resourceData = ref([
{ {
label: "全市普通公路抢险队伍", label: "全市普通公路抢险队伍",
value: "39", value: "",
unit: "支", unit: "支",
iconClass: "icon-team", iconClass: "icon-team",
img: icon1, img: icon1,
}, },
{ {
label: "人员", label: "人员",
value: "1612", value: "",
unit: "人", unit: "人",
iconClass: "icon-person", iconClass: "icon-person",
img: icon2, img: icon2,
}, },
{ {
label: "储备装备", label: "储备装备",
value: "1157", value: "",
unit: "台", unit: "台",
iconClass: "icon-equip", iconClass: "icon-equip",
img: icon3, img: icon3,
}, },
{ {
label: "物资", label: "物资",
value: "41.5", value: "",
unit: "万件", unit: "件",
iconClass: "icon-material", iconClass: "icon-material",
img: icon4, img: icon4,
}, },
]; ]);
// //
const controlData1 = [ const controlData1 = [
@ -397,7 +497,7 @@ const majorEvent = "0";
height: vw(16); height: vw(16);
min-width: 14px; min-width: 14px;
min-height: 14px; min-height: 14px;
background: linear-gradient(135deg, #18F2F9 0%, #1890ff 100%); background: linear-gradient(135deg, #18f2f9 0%, #1890ff 100%);
border-radius: 4px; border-radius: 4px;
display: flex; display: flex;
align-items: center; align-items: center;
@ -480,7 +580,7 @@ const majorEvent = "0";
.resource-value { .resource-value {
font-size: vw(16); font-size: vw(16);
font-weight: bold; font-weight: bold;
color: #18F2F9; color: #18f2f9;
.unit { .unit {
font-size: vw(14); font-size: vw(14);
@ -506,7 +606,7 @@ const majorEvent = "0";
.control-num { .control-num {
font-size: vw(16); font-size: vw(16);
color: #18F2F9; color: #18f2f9;
font-weight: bold; font-weight: bold;
margin-left: vw(5); margin-left: vw(5);
} }
@ -535,14 +635,13 @@ const majorEvent = "0";
&:hover { &:hover {
background-color: rgba(64, 169, 255, 0.15); background-color: rgba(64, 169, 255, 0.15);
} }
} }
.control-value { .control-value {
font-size: vw(18); font-size: vw(18);
font-weight: bold; font-weight: bold;
color: #18F2F9; color: #18f2f9;
// margin-bottom: vw(4); // margin-bottom: vw(4);
} }
@ -572,7 +671,7 @@ const majorEvent = "0";
.patrol-mileage { .patrol-mileage {
font-size: vw(16); font-size: vw(16);
font-weight: bold; font-weight: bold;
color: #4FECFF; color: #4fecff;
} }
} }
@ -593,7 +692,7 @@ const majorEvent = "0";
.patrol-value { .patrol-value {
font-size: vw(18); font-size: vw(18);
font-weight: bold; font-weight: bold;
color: #4FECFF; color: #4fecff;
margin-bottom: vw(4); margin-bottom: vw(4);
text-align: left; text-align: left;
margin-left: vw(5); margin-left: vw(5);
@ -723,7 +822,7 @@ const majorEvent = "0";
background: linear-gradient( background: linear-gradient(
180deg, 180deg,
transparent 0%, transparent 0%,
#18F2F9 50%, #18f2f9 50%,
transparent 100% transparent 100%
); );
// margin: 0 auto; // margin: 0 auto;
@ -744,7 +843,7 @@ const majorEvent = "0";
margin-bottom: vw(6); margin-bottom: vw(6);
.current { .current {
color: #18F2F9; color: #18f2f9;
} }
.separator { .separator {
@ -820,7 +919,7 @@ const majorEvent = "0";
} }
&.blue .damage-value { &.blue .damage-value {
color: #18F2F9; color: #18f2f9;
.unit { .unit {
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.7);
} }

View File

@ -94,6 +94,8 @@ export default defineConfig(({ command, mode }) => {
proxy: { proxy: {
'/snow-ops-platform': { '/snow-ops-platform': {
target: 'http://192.168.110.16:8661/', target: 'http://192.168.110.16:8661/',
// target: 'http://8.137.54.85:8661/', //测试环境
// target: 'http://192.168.110.36:8661/', //张启生本地环境
changeOrigin: true, changeOrigin: true,
}, },
} }