This commit is contained in:
fanjia 2026-04-08 15:34:52 +08:00
commit 0d0a1d4899
24 changed files with 3372 additions and 30 deletions

View File

@ -0,0 +1,434 @@
<template>
<div class="base-date-time-picker">
<!-- 使用 van-field 作为展示区域 - 修复 bug1改为 modelValue -->
<van-field
v-model="displayValue"
:label="label"
:placeholder="placeholder"
:disabled="disabled"
:readonly="true"
:right-icon="rightIcon"
:clickable="!disabled"
@click="openPicker"
/>
<!-- 弹出层同时包含日期和时间选择器 -->
<van-popup v-model:show="showPicker" position="bottom" round>
<div class="picker-container">
<div class="picker-header">
<span class="picker-cancel" @click="showPicker = false">取消</span>
<span class="picker-title">{{ pickerTitle }}</span>
<span class="picker-confirm" @click="onConfirm">确定</span>
</div>
<div class="picker-wrapper" :class="{ 'has-time': hasTime }">
<!-- 日期选择器 -->
<van-date-picker
v-model="currentDate"
:min-date="minDate"
:max-date="maxDate"
:columns-type="dateColumnsType"
:formatter="formatter"
:filter="filter"
:loading="loading"
:show-toolbar="false"
/>
<!-- 时间选择器当有小时列时显示 -->
<van-time-picker
v-if="hasTime"
v-model="currentTime"
:columns-type="timeColumnsType"
:min-hour="minHour"
:max-hour="maxHour"
:min-minute="minMinute"
:max-minute="maxMinute"
:formatter="formatter"
:show-toolbar="false"
/>
</div>
</div>
</van-popup>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { Field, Popup, DatePicker, TimePicker } from 'vant'
// Props
const props = defineProps({
// (v-model)
modelValue: {
type: String,
default: ''
},
//
label: {
type: String,
default: ''
},
//
placeholder: {
type: String,
default: '请选择日期时间'
},
//
disabled: {
type: Boolean,
default: false
},
// Picker
pickerTitle: {
type: String,
default: '选择日期时间'
},
// 'year', 'month', 'day', 'hour', 'minute', 'second'
columnsType: {
type: Array,
default: () => ['year', 'month', 'day', 'hour', 'minute']
},
//
minDate: {
type: Date,
default: () => new Date(1900, 0, 1)
},
//
maxDate: {
type: Date,
default: () => new Date(2100, 11, 31)
},
//
minHour: {
type: Number,
default: 0
},
//
maxHour: {
type: Number,
default: 23
},
//
minMinute: {
type: Number,
default: 0
},
//
maxMinute: {
type: Number,
default: 59
},
//
loading: {
type: Boolean,
default: false
},
//
rightIcon: {
type: String,
default: 'arrow-down'
},
//
formatter: {
type: Function,
default: (type, option) => {
if (type === 'year') option.text += '年'
if (type === 'month') option.text += '月'
if (type === 'day') option.text += '日'
if (type === 'hour') option.text += '时'
if (type === 'minute') option.text += '分'
if (type === 'second') option.text += '秒'
return option
}
},
//
filter: {
type: Function,
default: null
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'change'])
//
const showPicker = ref(false)
// ['2024', '01', '01']
const currentDate = ref([])
// ['14', '30', '00']
const currentTime = ref([])
//
const dateColumnsType = computed(() => {
return props.columnsType.filter(col => ['year', 'month', 'day'].includes(col))
})
const timeColumnsType = computed(() => {
return props.columnsType.filter(col => ['hour', 'minute', 'second'].includes(col))
})
//
const hasTime = computed(() => {
return timeColumnsType.value.length > 0
})
// bug2
const displayValue = computed(() => {
if (!props.modelValue) return ''
// columnsType
const hasYear = props.columnsType.includes('year')
const hasMonth = props.columnsType.includes('month')
const hasDay = props.columnsType.includes('day')
const hasHour = props.columnsType.includes('hour')
const hasMinute = props.columnsType.includes('minute')
const hasSecond = props.columnsType.includes('second')
//
let datePart = ''
let timePart = ''
if (props.modelValue.includes(' ')) {
[datePart, timePart] = props.modelValue.split(' ')
} else if (props.modelValue.includes('-')) {
datePart = props.modelValue
} else if (props.modelValue.includes(':')) {
timePart = props.modelValue
}
//
let result = ''
if (datePart && (hasYear || hasMonth || hasDay)) {
const dateArr = datePart.split('-')
if (hasYear && dateArr[0]) result += dateArr[0]
if (hasMonth && dateArr[1]) result += (result ? '-' : '') + dateArr[1]
if (hasDay && dateArr[2]) result += (result ? '-' : '') + dateArr[2]
}
// - bug2
if (timePart && (hasHour || hasMinute || hasSecond)) {
const timeArr = timePart.split(':')
let timeResult = ''
if (hasHour && timeArr[0]) timeResult += timeArr[0]
if (hasMinute && timeArr[1]) timeResult += (timeResult ? ':' : '') + timeArr[1]
if (hasSecond && timeArr[2]) timeResult += (timeResult ? ':' : '') + timeArr[2]
if (timeResult) {
result += (result ? ' ' : '') + timeResult
}
}
return result || props.modelValue
})
//
const getDefaultTime = () => {
const now = new Date()
const timeValues = []
if (timeColumnsType.value.includes('hour')) {
timeValues.push(String(now.getHours()).padStart(2, '0'))
}
if (timeColumnsType.value.includes('minute')) {
timeValues.push(String(now.getMinutes()).padStart(2, '0'))
}
if (timeColumnsType.value.includes('second')) {
timeValues.push(String(now.getSeconds()).padStart(2, '0'))
}
return timeValues
}
//
const initValue = () => {
//
if (props.modelValue) {
// YYYY-MM-DD HH:mm:ss
let datePart = ''
let timePart = ''
if (props.modelValue.includes(' ')) {
[datePart, timePart] = props.modelValue.split(' ')
} else if (props.modelValue.includes('-')) {
datePart = props.modelValue
} else if (props.modelValue.includes(':')) {
timePart = props.modelValue
}
if (datePart) {
const dateArr = datePart.split('-')
currentDate.value = []
if (dateColumnsType.value.includes('year') && dateArr[0]) {
currentDate.value.push(dateArr[0])
}
if (dateColumnsType.value.includes('month') && dateArr[1]) {
currentDate.value.push(dateArr[1])
}
if (dateColumnsType.value.includes('day') && dateArr[2]) {
currentDate.value.push(dateArr[2])
}
} else {
const now = new Date()
currentDate.value = []
if (dateColumnsType.value.includes('year')) {
currentDate.value.push(String(now.getFullYear()))
}
if (dateColumnsType.value.includes('month')) {
currentDate.value.push(String(now.getMonth() + 1).padStart(2, '0'))
}
if (dateColumnsType.value.includes('day')) {
currentDate.value.push(String(now.getDate()).padStart(2, '0'))
}
}
//
if (hasTime.value) {
if (timePart) {
const timeArr = timePart.split(':')
const result = []
if (timeColumnsType.value.includes('hour')) {
result.push(timeArr[0] || '00')
}
if (timeColumnsType.value.includes('minute')) {
result.push(timeArr[1] || '00')
}
if (timeColumnsType.value.includes('second')) {
result.push(timeArr[2] || '00')
}
currentTime.value = result
} else {
currentTime.value = getDefaultTime()
}
}
} else {
//
const now = new Date()
currentDate.value = []
if (dateColumnsType.value.includes('year')) {
currentDate.value.push(String(now.getFullYear()))
}
if (dateColumnsType.value.includes('month')) {
currentDate.value.push(String(now.getMonth() + 1).padStart(2, '0'))
}
if (dateColumnsType.value.includes('day')) {
currentDate.value.push(String(now.getDate()).padStart(2, '0'))
}
//
if (hasTime.value) {
currentTime.value = getDefaultTime()
}
}
}
//
const openPicker = () => {
if (!props.disabled) {
initValue()
showPicker.value = true
}
}
//
const formatDatePart = () => {
if (!currentDate.value || currentDate.value.length === 0) return ''
let result = currentDate.value.join('-')
return result
}
//
const formatTimePart = () => {
if (!hasTime.value) return ''
if (!currentTime.value || currentTime.value.length === 0) return ''
let result = currentTime.value.join(':')
return result
}
//
const onConfirm = () => {
let finalValue = formatDatePart()
if (hasTime.value) {
const timePart = formatTimePart()
if (timePart) {
finalValue += ' ' + timePart
}
}
emit('update:modelValue', finalValue)
emit('change', finalValue)
showPicker.value = false
}
// modelValue
watch(() => props.modelValue, () => {
initValue()
}, { immediate: true })
</script>
<style scoped>
.base-date-time-picker {
width: 100%;
}
:deep(.van-field__control--readonly) {
cursor: pointer;
}
.picker-container {
background: #fff;
border-radius: 16px 16px 0 0;
overflow: hidden;
}
.picker-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid #ebedf0;
}
.picker-cancel {
font-size: 14px;
color: #969799;
cursor: pointer;
}
.picker-title {
font-size: 16px;
font-weight: 500;
color: #323233;
}
.picker-confirm {
font-size: 14px;
color: #1989fa;
cursor: pointer;
}
.picker-wrapper {
display: flex;
}
.picker-wrapper .van-picker {
flex: 1;
}
/* 只有日期选择器时占满宽度 */
.picker-wrapper:not(.has-time) .van-picker {
width: 100%;
}
/* 同时有日期和时间时调整宽度比例 */
.picker-wrapper.has-time .van-date-picker {
flex: 2;
}
.picker-wrapper.has-time .van-time-picker {
flex: 1;
}
</style>

View File

@ -0,0 +1,139 @@
<template>
<div class="base-picker">
<!-- 使用 van-field 作为展示区域 -->
<van-field
:modelValue="displayValue"
:label="label"
:placeholder="placeholder"
:disabled="disabled"
:readonly="true"
:right-icon="rightIcon"
:clickable="!disabled"
@click="openPicker"
/>
<!-- 弹出层选择器 -->
<van-popup v-model:show="showPicker" position="bottom" round>
<van-picker
:columns="columns"
:title="pickerTitle"
:loading="loading"
show-toolbar
@confirm="onConfirm"
@cancel="showPicker = false"
/>
</van-popup>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { Field, Popup, Picker } from 'vant'
// Props
const props = defineProps({
// (v-model)
modelValue: {
type: [String, Number, Boolean],
default: null
},
// [{ label: '', value: '' }]
options: {
type: Array,
required: true,
default: () => []
},
//
label: {
type: String,
default: ''
},
//
placeholder: {
type: String,
default: '请选择'
},
//
disabled: {
type: Boolean,
default: false
},
// Picker
pickerTitle: {
type: String,
default: '请选择'
},
//
loading: {
type: Boolean,
default: false
},
//
rightIcon: {
type: String,
default: 'arrow-down'
},
// label
labelKey: {
type: String,
default: 'label'
},
// value
valueKey: {
type: String,
default: 'value'
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'change'])
//
const showPicker = ref(false)
// Picker options Picker
const columns = computed(() => {
return props.options.map(item => {
return {
text: item[props.labelKey],
value: item[props.valueKey]
}
})
})
//
const displayValue = computed(() => {
if (props.modelValue === null || props.modelValue === undefined || props.modelValue === '') {
return ''
}
const selected = props.options.find(item => item[props.valueKey] === props.modelValue)
return selected ? selected[props.labelKey] : ''
})
//
const openPicker = () => {
if (!props.disabled) {
showPicker.value = true
}
}
//
const onConfirm = ({ selectedValues, selectedOptions }) => {
const value = selectedOptions[0][props.valueKey]
const label = selectedOptions[0][props.labelKey]
emit('update:modelValue', value)
emit('change', { value, label })
showPicker.value = false
}
</script>
<style scoped>
.base-picker {
width: 100%;
}
/* 可选:调整 Field 的只读样式 */
:deep(.van-field__control--readonly) {
cursor: pointer;
}
</style>

View File

@ -39,7 +39,7 @@ const props = defineProps({
.card-item {
position: relative;
width: 100%;
padding: 21px 50px 17px 10px;
padding: 20px;
background-color: #fff;
border-radius: 8px;
}

View File

@ -1,6 +1,6 @@
<template>
<div class="page-container">
<van-nav-bar title="气象预警" fixed left-arrow @click-left="onClickLeft" />
<van-nav-bar :title="title" fixed left-arrow @click-left="onClickLeft" />
<div class="page-content-wrapper">
<slot></slot>
</div>
@ -10,6 +10,13 @@
<script setup>
import { onMounted, ref } from 'vue'
const props = defineProps({
title: {
type: String,
default: ''
}
})
const emit = defineEmits(['back'])
const onClickLeft = () => {

View File

@ -0,0 +1,50 @@
<template>
<div class="panel-item">
<slot v-if="title" name="header">
<div class="header">
<div class="header-title">{{ title }}</div>
<div class="header-extra" v-if="$slots.headerExtra">
<slot name="headerExtra"></slot>
</div>
</div>
</slot>
<slot />
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
const props = defineProps({
title: {
type: String,
default: ''
},
})
</script>
<style scoped lang="scss">
.panel-item {
position: relative;
width: 100%;
padding: 20px;
background-color: #fff;
border-radius: 8px;
& + .panel-item {
margin-top: 10px;
}
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
}
.header-title {
font-weight: 500;
font-size: 15px;
color: #4a4a4a;
line-height: 16px;
}
</style>

View File

@ -3,18 +3,26 @@
<div class="input-wrapper">
<div class="input-block">
<van-icon class="search-icon" name="search" />
<input class="inner-input" v-model="modelValue" :placeholder="placeholder" />
<van-icon class="close-icon" name="clear" v-if="modelValue !== ''" @click="modelValue = ''" />
<input
class="inner-input"
v-model="modelValue"
:placeholder="placeholder"
/>
<van-icon
class="close-icon"
name="clear"
v-if="modelValue !== ''"
@click="clearInput"
/>
</div>
<!-- 右侧插槽用于放置全部和筛选按钮 -->
<div class="slot-wrapper" v-if="$slots.extra">
<slot name="extra"></slot>
</div>
<!-- 占位符 -->
<!-- <div class="placeholder-block" v-if="modelValue === '111'">
<van-icon class="search-icon" name="search" />
<span class="placeholder-text">{{ placeholder }}</span>
</div> -->
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
@ -26,7 +34,13 @@ const props = defineProps({
default: '请输入关键词'
}
})
//
const clearInput = () => {
modelValue.value = ''
}
</script>
<style scoped lang="scss">
.search-input {
position: relative;
@ -37,19 +51,23 @@ const props = defineProps({
}
.input-wrapper {
display: flex;
align-items: center;
width: 100%;
height: 100%;
background-color: #fff;
border-radius: 4px;
gap: 12px; //
padding-right: 12px; //
box-sizing: border-box;
}
.input-block {
position: relative;
flex: 1;
height: 100%;
display: flex;
justify-content: center;
width: 100%;
height: 100%;
padding: 0 20px;
box-sizing: border-box;
.search-icon, .close-icon {
@ -59,10 +77,18 @@ const props = defineProps({
transform: translateY(-50%);
font-size: 20px;
color: #9b9b9b;
pointer-events: none; //
}
.close-icon {
left: unset;
right: 20px;
pointer-events: auto; //
cursor: pointer;
&:active {
opacity: 0.6;
}
}
}
@ -71,28 +97,17 @@ const props = defineProps({
border: none;
padding: 0;
margin: 0;
width: 100%;
height: 100%;
text-align: center;
font-size: 14px;
background: transparent;
}
.placeholder-block {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.slot-wrapper {
display: flex;
align-items: center;
pointer-events: none;
.search-icon {
font-size: 20px;
margin-right: 3px;
color: #9b9b9b;
}
.placeholder-text {
color: #9b9b9b;
}
gap: 8px; //
flex-shrink: 0; //
}
</style>
</style>

View File

@ -0,0 +1,223 @@
<template>
<div class="tag-filter" v-show="visible">
<div class="filter-mask" @click="close"></div>
<div class="filter-container">
<div class="filter-header">
<span class="filter-title">类型筛选</span>
<van-icon name="close" class="close-icon" @click="close" />
</div>
<div class="filter-content">
<div class="tag-list">
<span
v-for="tag in tagList"
:key="tag.value"
class="tag-item"
:class="{ active: tempSelectedTag === tag.value }"
@click="selectTag(tag.value)"
>
{{ tag.label }}
</span>
</div>
</div>
<div class="filter-footer">
<van-button class="reset-btn" size="small" @click="resetFilter">重置</van-button>
<van-button type="primary" class="confirm-btn" size="small" @click="confirmFilter">确定</van-button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
import { Icon as VanIcon, Button as VanButton } from 'vant'
const props = defineProps({
// v-model
modelValue: {
type: String,
default: 'all'
},
//
visible: {
type: Boolean,
default: false
},
//
tags: {
type: Array,
default: () => [
{ label: '全部', value: 'all' },
{ label: '边坡坍塌', value: '边坡坍塌' },
{ label: '泥石流', value: '泥石流' },
{ label: '路基沉陷', value: '路基沉陷' },
{ label: '山体滑坡', value: '山体滑坡' },
{ label: '行道树倒塌', value: '行道树倒塌' },
{ label: '积水', value: '积水' },
{ label: '积雪', value: '积雪' },
{ label: '其他', value: '其他' }
]
}
})
const emit = defineEmits(['update:modelValue', 'update:visible', 'confirm'])
//
const tempSelectedTag = ref(props.modelValue)
//
const tagList = computed(() => props.tags)
// modelValue
watch(() => props.modelValue, (newVal) => {
tempSelectedTag.value = newVal
})
// visible
watch(() => props.visible, (newVal) => {
if (newVal) {
tempSelectedTag.value = props.modelValue
}
})
//
const selectTag = (value) => {
tempSelectedTag.value = value
}
//
const resetFilter = () => {
tempSelectedTag.value = 'all'
}
//
const confirmFilter = () => {
emit('update:modelValue', tempSelectedTag.value)
emit('confirm', tempSelectedTag.value)
close()
}
//
const close = () => {
emit('update:visible', false)
}
</script>
<style scoped lang="scss">
.tag-filter {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
}
.filter-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.filter-container {
position: absolute;
top: 0;
left: 0;
right: 0;
background-color: #fff;
border-radius: 0 0 16px 16px;
animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
from {
transform: translateY(-100%);
}
to {
transform: translateY(0);
}
}
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 16px 12px;
border-bottom: 1px solid #ebedf0;
.filter-title {
font-size: 16px;
font-weight: 500;
color: #323233;
}
.close-icon {
font-size: 22px;
color: #969799;
cursor: pointer;
&:active {
opacity: 0.6;
}
}
}
.filter-content {
padding: 16px;
max-height: 400px;
overflow-y: auto;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.tag-item {
padding: 6px 16px;
background-color: #f7f8fa;
border-radius: 24px;
font-size: 14px;
color: #646566;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
&:active {
transform: scale(0.96);
}
&.active {
background-color: #1989fa;
color: #fff;
}
}
.filter-footer {
display: flex;
gap: 12px;
padding: 12px 16px 20px;
border-top: 1px solid #ebedf0;
.reset-btn,
.confirm-btn {
flex: 1;
height: 40px;
border-radius: 24px;
font-size: 14px;
}
.reset-btn {
background-color: #f7f8fa;
border: none;
color: #646566;
&:active {
opacity: 0.8;
}
}
}
</style>

View File

@ -80,6 +80,31 @@ const routes = [
path: '/warningMessageHandle',
name: 'WarningMessageHandle',
component: () => import('../views/WarningMessage/WarningMessageHandle.vue')
},
{
path: '/rebuild',
name: 'Rebuild',
component: () => import('../views/Rebuild/Rebuild.vue')
},
{
path: '/rebuild-add/:data?',
name: 'RebuildAdd',
component: () => import('../views/Rebuild/RebuildAdd.vue')
},
{
path: '/rebuild-details/:data?',
name: 'RebuildDetails',
component: () => import('../views/Rebuild/RebuildDetails.vue')
},
{
path: '/disasterManagement',
name: 'DisasterManagement',
component: () => import('../views/DisasterManagement/DisasterManagement.vue')
},
{
path: '/disasterReport',
name: 'DisasterReport',
component: () => import('../views/DisasterManagement/DisasterReport.vue')
}
]

View File

@ -0,0 +1,274 @@
<template>
<PageContainer title="灾害管理" @click-back="handleClickBack">
<CurrentSite />
<div class="list-panel">
<CardItem v-for="(item, index) in list" :key="index" :title="item.title" @click="handleClickItem(item)">
<template #headerExtra>
<van-tag :type="item.status === '未解除' ? 'danger' : 'success'" plain size="medium">
{{ item.status }}
</van-tag>
</template>
<div class="info-block">
<div class="time-box">
<span class="info-label">发生时间</span>
<span class="info-value">{{ item.occurTime }}</span>
</div>
<div class="time-box">
<span class="info-label">预计恢复时间</span>
<span class="info-value">{{ item.estimateRecoverTime }}</span>
</div>
<div class="disaster-type-wrapper">
<van-tag type="primary" size="medium" plain>{{ item.disasterType }}</van-tag>
</div>
</div>
<van-icon class="jump-icon-absolute" name="arrow" />
</CardItem>
<div v-if="loading" class="loading-wrapper">
<van-loading size="24px" vertical>加载中...</van-loading>
</div>
<EmptyBox v-if="!loading && list.length === 0" :placeholder="emptyText" />
</div>
<van-button type="primary" class="footer-btn" @click="handleAdd"> 灾害填报 </van-button>
<!-- 筛选组件v-model 绑定选中的值visible 控制显示隐藏 -->
<TagFilter
v-model="selectedDisasterType"
:visible="showFilter"
@update:visible="showFilter = $event"
@confirm="handleFilterConfirm"
/>
</PageContainer>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { showToast, Tag as VanTag, Loading as VanLoading, Icon as VanIcon, Button as VanButton } from 'vant'
import PageContainer from '@/components/PageContainer.vue'
import SearchInput from '@/components/SearchInput.vue'
import CardItem from '@/components/CardItem.vue'
import EmptyBox from '@/components/EmptyBox.vue'
import CurrentSite from '@/components/CurrentSite.vue'
import TagFilter from '@/components/TagFilter.vue'
import { request } from "@shared/utils/request";
const router = useRouter()
//
const searchValue = ref('')
//
const list = ref([])
//
const loading = ref(false)
//
const emptyText = ref('暂无相关灾毁信息')
//
const showFilter = ref(false)
// v-model TagFilter
const selectedDisasterType = ref('all')
//
const disasterTypes = [
{ label: '全部', value: 'all' },
{ label: '边坡坍塌', value: '边坡坍塌' },
{ label: '泥石流', value: '泥石流' },
{ label: '路基沉陷', value: '路基沉陷' },
{ label: '山体滑坡', value: '山体滑坡' },
{ label: '行道树倒塌', value: '行道树倒塌' },
{ label: '积水', value: '积水' },
{ label: '积雪', value: '积雪' },
{ label: '其他', value: '其他' }
]
//
const getShortTypeName = (type) => {
const typeMap = {
'边坡坍塌': '边坡',
'泥石流': '泥石',
'路基沉陷': '路基',
'山体滑坡': '滑坡',
'行道树倒塌': '树倒',
'积水': '积水',
'积雪': '积雪',
'其他': '其他'
}
return typeMap[type] || type.substring(0, 2)
}
//
const getDisasterList = async (keyword = '', disasterType = 'all') => {
loading.value = true
try {
const result = await request({
url: '/snow-ops-platform/water-damage/list',
method: 'get',
params: {
keyword: keyword.trim(),
disasterType: disasterType === 'all' ? '' : disasterType
}
})
if (result?.data?.records) {
list.value = result.data.records
} else {
showToast(result.message || '获取数据失败')
list.value = []
}
} catch (error) {
console.error('获取灾毁列表失败:', error)
showToast('获取数据失败,请稍后重试')
list.value = []
} finally {
loading.value = false
}
}
//
const handleSearch = () => {
getDisasterList(searchValue.value, selectedDisasterType.value)
}
//
const handleAllClick = () => {
if (selectedDisasterType.value !== 'all') {
selectedDisasterType.value = 'all'
getDisasterList(searchValue.value, 'all')
}
}
//
const handleFilterConfirm = (type) => {
// selectedDisasterType v-model
getDisasterList(searchValue.value, type)
}
//
const handleClickBack = () => {
router.push('/')
}
//
const handleClickItem = (item) => {
router.push({
path: '/disaster-detail',
query: {
id: item.id,
title: item.title,
status: item.status,
occurTime: item.occurTime,
estimateRecoverTime: item.estimateRecoverTime,
disasterType: item.disasterType
}
})
}
//
const handleAdd = () => {
router.push('/disasterReport')
}
//
onMounted(() => {
getDisasterList()
})
</script>
<style lang="scss" scoped>
.list-panel {
display: flex;
flex-direction: column;
gap: 8px;
padding-bottom: 80px;
}
:deep(.card-item) {
position: relative;
}
.info-block {
display: flex;
flex-direction: column;
gap: 8px;
padding-right: 24px;
}
.time-box {
display: flex;
align-items: baseline;
flex-wrap: wrap;
}
.info-label {
font-weight: 400;
font-size: 13px;
color: #999999;
}
.info-value {
font-weight: 400;
font-size: 14px;
color: #333333;
}
.jump-icon-absolute {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
font-size: 16px;
color: rgba(102, 102, 102, 0.4);
cursor: pointer;
}
.disaster-type-wrapper {
margin-top: 4px;
}
.footer-btn {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
width: calc(100% - 32px);
max-width: 340px;
border-radius: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
cursor: pointer;
z-index: 10;
&:active {
opacity: 0.9;
transform: translateX(-50%) scale(0.98);
}
}
.loading-wrapper {
display: flex;
justify-content: center;
padding: 40px 0;
}
:deep(.van-button) {
&.active {
background-color: #1989fa;
color: #fff;
border-color: #1989fa;
}
}
</style>

View File

@ -0,0 +1,148 @@
<template>
<PageContainer title="灾毁填报" @click-back="handleClickBack" class="page-container">
<!-- 当前站点信息 -->
<CurrentSite />
<!-- 事件类型 -->
<PanelItem title="事件类型">
<van-radio-group v-model="eventType" direction="horizontal" class="event-type-group">
<van-radio name="water">水毁灾害</van-radio>
<van-radio name="ice">冰雪灾害</van-radio>
</van-radio-group>
</PanelItem>
<!-- 根据事件类型渲染不同表单 -->
<WaterDisaster
v-if="eventType === 'water'"
ref="waterDisasterRef"
/>
<!-- 冰雪灾害表单待实现 -->
<div v-else class="coming-soon">
<van-empty description="冰雪灾害表单开发中..." />
</div>
<!-- 提交按钮 -->
<van-button type="primary" class="footer-btn" @click="handleSubmit" :loading="submitting">
提交
</van-button>
</PageContainer>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { showToast, showSuccessToast, showFailToast } from 'vant'
import PageContainer from '@/components/PageContainer.vue'
import CurrentSite from '@/components/CurrentSite.vue'
import PanelItem from '@/components/PanelItem.vue'
import WaterDisaster from './WaterDisaster/WaterDisaster.vue'
import { request } from "@shared/utils/request";
import mockFormData from './waterDisasterFormData.json'
const router = useRouter()
//
const eventType = ref('water')
//
const formData = ref(mockFormData)
const waterDisasterRef = ref(null)
const submitting = ref(false)
//
const handleClickBack = () => {
router.replace('/disasterManagement')
}
//
const handleSubmit = async () => {
//
if (eventType.value === 'water') {
if (!waterDisasterRef.value.validate()) {
return
}
}
submitting.value = true
try {
//
let formData = {}
if (eventType.value === 'water') {
formData = waterDisasterRef.value.getFormData()
}
//
const submitData = {
...formData,
//
}
const res = await request({
url: '/snow-ops-platform/water-damage/addOrUpdate',
method: 'post',
data: submitData
})
//
await new Promise(resolve => setTimeout(resolve, 1000))
showSuccessToast('提交成功')
//
setTimeout(() => {
router.replace('/disasterManagement')
}, 1500)
} catch (error) {
showFailToast('提交失败,请重试')
console.error('提交失败:', error)
} finally {
submitting.value = false
}
}
onMounted(() => {
waterDisasterRef.value.initFormData(formData.value)
})
</script>
<style lang="scss" scoped>
.page-container {
padding-bottom: 80px;
background-color: #f5f7fa;
}
.event-type-group {
display: flex;
gap: 24px;
:deep(.van-radio) {
margin-right: 0;
}
}
.coming-soon {
padding: 40px 20px;
}
.footer-btn {
position: fixed;
bottom: 15px;
left: 50%;
transform: translateX(-50%);
width: calc(100% - 32px);
max-width: 340px;
border-radius: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
cursor: pointer;
z-index: 10;
&:active {
opacity: 0.9;
transform: translateX(-50%) scale(0.98);
}
}
</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,42 @@
[
{
"id": 1,
"title": "G242金铃乡老窖坪发生积雪",
"status": "未解除",
"occurTime": "2025/10/10 20:29",
"estimateRecoverTime": "2025/10/10 20:29",
"disasterType": "积雪"
},
{
"id": 2,
"title": "S521白鹿镇X发生边坡坍塌",
"status": "已解除",
"occurTime": "2025/10/10 20:29",
"estimateRecoverTime": "2025/10/10 20:29",
"disasterType": "边坡坍塌"
},
{
"id": 3,
"title": "彭水S523发生边坡坍塌",
"status": "未解除",
"occurTime": "2025/10/10 20:29",
"estimateRecoverTime": "2025/10/10 20:29",
"disasterType": "路基沉陷"
},
{
"id": 4,
"title": "梁平蟠龙镇G318发生山体滑坡",
"status": "已解除",
"occurTime": "2025/10/10 20:29",
"estimateRecoverTime": "2025/10/10 20:29",
"disasterType": "山体滑坡"
},
{
"id": 5,
"title": "重庆市大足区XX县G201行道树倒塌",
"status": "已解除",
"occurTime": "2025/10/10 20:29",
"estimateRecoverTime": "2025/10/10 20:29",
"disasterType": "行道树倒塌"
}
]

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": []
}

View File

@ -100,6 +100,13 @@ const gridItems = [
params: { data: encodeURIComponent(JSON.stringify(yhzinfo.value)) },
},
},
{
icon: group106Icon,
text: "灾害管理",
to: {
name: "DisasterManagement",
},
},
{
icon: group105Icon,
text: "预警信息",
@ -107,6 +114,13 @@ const gridItems = [
name: "WarningMessage",
},
},
{
icon: group106Icon,
text: '恢复重建',
to: {
name: 'Rebuild',
}
}
];
//

View File

@ -0,0 +1,169 @@
<template>
<PageContainer title="恢复重建" @click-back="handleClickBack">
<SearchInput v-model="searchValue" />
<CurrentSite />
<div class="list-panel">
<CardItem v-for="(item, index) in list" :key="index" :title="`${item.area} ${item.rNumber} ${item.type}`"
@click="handleClickItem(item)">
<template #headerExtra>
<van-tag v-if="item.status === '审批通过'" type="success" plain size="medium">{{ item.status }}</van-tag>
<van-tag v-else-if="item.status === '审批驳回'" type="danger" plain size="medium">{{ item.status
}}</van-tag>
<van-tag v-else type="warning" plain size="medium">{{ item.status }}</van-tag>
</template>
<div class="content">
<div class="left-info">
<div><span class="label">起止桩号</span><span class="value">{{ item.stationNumber }}</span></div>
<div><span class="label">路况位置</span><span class="value">{{ item.position }}</span></div>
<div><span class="label">提交日期</span><span class="value">{{ item.publishTime }}</span></div>
</div>
<div class="right-arrow" @click.stop="handleClickItem(item)">
<van-icon name="arrow" />
</div>
</div>
</CardItem>
<!-- 空状态提示 -->
<EmptyBox v-if="list.length === 0" placeholder="暂无相关预警信息" />
</div>
<van-button type="primary" class="add-btn" icon="plus" @click="handleAddDevice">
项目填报
</van-button>
</PageContainer>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import PageContainer from '@/components/PageContainer.vue'
import SearchInput from '@/components/SearchInput.vue'
import CurrentSite from '@/components/CurrentSite.vue'
import CardItem from '@/components/CardItem.vue'
import EmptyBox from '@/components/EmptyBox.vue'
const router = useRouter()
onMounted(() => {
getData()
})
const getData = async () => {
}
//
const searchValue = ref('')
//
const list = ref([
{
id: 1,
area: '彭水',
rNumber: 'G211',
type: '发生水毁道路崩塌恢复重建',
stationNumber: 'K1674.16-1678.84',
position: '徐家镇村口南分叉路口',
publishTime: '2025-05-20',
status: '审批通过'
},
{
id: 2,
area: '巴南',
rNumber: 'S303',
type: '道路发生边坡坍塌',
stationNumber: 'K1674.16-1678.84',
position: '徐家镇村口南分叉路口',
publishTime: '2025-05-20',
status: '审批驳回'
},
{
id: 3,
area: '彭水',
rNumber: 'G211',
type: '道路崩塌改造工程',
stationNumber: 'K1674.16-1678.84',
position: '徐家镇村口南分叉路口',
publishTime: '2025-05-20',
status: '审批通过'
},
])
const handleClickBack = () => {
router.push('/')
}
const handleClickItem = (item) => {
router.push({
name: 'RebuildDetails',
params: {
data: encodeURIComponent(JSON.stringify(item.id))
}
})
}
const handleAddDevice = () => {
router.push('/rebuild-add')
// router.push({
// name: "RebuildAdd",
// params: {
// data: encodeURIComponent(JSON.stringify(11)),
// },
// });
}
</script>
<style scoped lang="scss">
.list-panel {
display: flex;
flex-direction: column;
gap: 8px;
}
.content {
display: flex;
justify-content: space-between;
align-items: center;
}
.left-info {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 14px;
.label {
color: #666;
}
.value {
color: #aaa;
}
}
.right-arrow {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
cursor: pointer;
}
.add-btn {
position: fixed;
bottom: 20px;
left: 16px;
right: 16px;
width: calc(100% - 32px);
margin: 0 auto;
border-radius: 24px;
font-size: 16px;
height: 44px;
z-index: 999;
}
</style>

View File

@ -0,0 +1,154 @@
<template>
<PageContainer title="项目填报" @click-back="handleClickBack">
<div class="content">
<PanelItem>
<van-form label-align="left" colon>
<van-field v-model="form.name" label="区县名称" center placeholder="请填写" required
:rules="[{ required: true, message: '请填写区县名称' }]" />
<van-field v-model="form.name" label="线路编号" center placeholder="请填写" required
:rules="[{ required: true, message: '请填写线路编号' }]" />
<van-field v-model="form.name" label="起点桩号" center placeholder="请填写" required
:rules="[{ required: true, message: '请填写起点桩号' }]" />
<van-field v-model="form.name" label="止点桩号" center placeholder="请填写" required
:rules="[{ required: true, message: '请填写止点桩号' }]" />
<van-field v-model="form.name" label="止点桩号" center placeholder="请填写" required
:rules="[{ required: true, message: '请填写止点桩号' }]" />
<van-field v-model="form.number" label="实施里程" center placeholder="单位:公里" required type="number"
:rules="[{ required: true, message: '请填写实施里程' }]">
<template #extra>
公里
</template>
</van-field>
<van-field v-model="form.name" label="塌方及损失" center placeholder="(方/万元)" required
:rules="[{ required: true, message: '请填写塌方及损失' }]" />
<van-field v-model="form.name" label="灾害类型" center placeholder="请填写" required
:rules="[{ required: true, message: '请填写灾害类型' }]" />
<van-field v-model="form.name" label="地点路线" center placeholder="请填写" required
:rules="[{ required: true, message: '请填写地点路线' }]" />
<van-field v-model="form.name" label="路况位置" center placeholder="请填写" required
:rules="[{ required: true, message: '请填写路况位置' }]" />
<van-field v-model="form.name" label="阻断点小地名" center placeholder="请填写" required
:rules="[{ required: true, message: '请填写阻断点小地名' }]" />
<van-field v-model="form.name" label="恢复重建预估费用" center placeholder="请填写" required
:rules="[{ required: true, message: '请填写恢复重建预估费用' }]">
<template #extra>
万元
</template>
</van-field>
<van-field label="附件" center>
<template #input>
<van-uploader v-model="fileList" @delete="handleDelete" name="photos" :file-list="fileList"
:file-type="['image/jpeg', 'image/png']" :after-read="afterRead" multiple
:max-count="6" />
</template>
</van-field>
</van-form>
</PanelItem>
</div>
<van-button type="primary" class="add-btn" icon="plus" @click="handleAdd">
提交
</van-button>
</PageContainer>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import PageContainer from '@/components/PageContainer.vue'
import { showToast, showLoadingToast } from "vant";
import PanelItem from '@/components/PanelItem.vue'
const router = useRouter()
const route = useRoute()
const form = ref({})
const fileList = ref([]);
onMounted(() => {
if (route.params.data) {
const data = JSON.parse(decodeURIComponent(route.params.data));
console.log('@@@@data', data);
// todo
} else {
console.log('无传入数据');
}
})
const handleClickBack = () => {
if (route.params.data) {
} else {
router.push('/rebuild')
}
}
//
const handleDelete = (file) => {
if (file.serverUrl) {
const index = form.photos.findIndex((p) => p.photoUrl === file.serverUrl);
if (index !== -1) {
form.photos.splice(index, 1);
}
}
};
//
const afterRead = async (file) => {
try {
const toast = showLoadingToast({
message: "上传中...",
forbidClick: true,
duration: 0, // 0
});
const formData = new FormData();
formData.append("file", file.file);
const res = await request({
url: "/snow-ops-platform/file/upload",
method: "post",
data: formData,
});
toast.close();
if (res.code === "00000") {
form.photos.push({ photoUrl: res.data });
const index = fileList.value.findIndex((f) => f.file === file.file);
if (index !== -1) {
fileList.value[index].serverUrl = res.data;
}
console.log("form.photos", toRaw(form.photos));
console.log("fileList.value", fileList.value);
} else {
throw new Error(res.message);
}
} catch (error) {
toast.close();
showToast({
type: "fail",
message: error.message,
});
}
};
</script>
<style scoped lang="scss">
.content {
padding: 20px 0px 80px 0px;
}
.add-btn {
position: fixed;
bottom: 20px;
left: 16px;
right: 16px;
width: calc(100% - 32px);
margin: 0 auto;
border-radius: 24px;
font-size: 16px;
height: 44px;
z-index: 999;
}
</style>

View File

@ -0,0 +1,104 @@
<template>
<PageContainer title="项目填报" @click-back="handleClickBack">
<div class="content">
<PanelItem>
<div class="detail">
<div class="header">
<div class="header-title">{{ `${data.area} ${data.rNumber} ${data.type}` }}</div>
<div class="header-extra">
<van-tag v-if="data.status === '审批通过'" type="success" plain size="medium">{{ data.status
}}</van-tag>
<van-tag v-else-if="data.status === '审批驳回'" type="danger" plain size="medium">{{ data.status
}}</van-tag>
<van-tag v-else type="warning" plain size="medium">{{ data.status }}</van-tag>
</div>
</div>
<div class="item">区县名称 {{ data.area }}</div>
<div class="item">线路编号 {{ data.area }}</div>
<div class="item">起点桩号 {{ data.area }}</div>
<div class="item">止点桩号 {{ data.area }}</div>
<div class="item">实施里程 {{ data.area }}</div>
<div class="item">塌方及损失 {{ data.area }}</div>
<div class="item">灾害类型 {{ data.area }}</div>
<div class="item">地点路线 {{ data.area }}</div>
<div class="item">阻断点小地名 {{ data.area }}</div>
<div class="item">提交时间 {{ data.area }}</div>
<div class="item">恢复重建预估费用 {{ data.area }}</div>
</div>
</PanelItem>
<PanelItem title="附件">
<!-- 附件 -->
</PanelItem>
<PanelItem v-if="data.xxx">
<!-- 驳回理由 -->
</PanelItem>
</div>
</PageContainer>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import PageContainer from '@/components/PageContainer.vue'
import { showToast, showLoadingToast } from "vant";
import PanelItem from '@/components/PanelItem.vue'
const router = useRouter()
const route = useRoute()
const data = ref({
area: '',
rNumber: '',
type: '',
status: '审批通过',
})
onMounted(() => {
if (route.params.data) {
const data = JSON.parse(decodeURIComponent(route.params.data));
console.log('@@@@data', data);
// todo
} else {
console.log('无传入数据');
}
})
const handleClickBack = () => {
router.push('/rebuild')
}
</script>
<style scoped lang="scss">
.content {
padding: 20px 0px 0px 0px;
display: flex;
flex-direction: column;
gap: 10px;
}
.detail {
display: flex;
flex-direction: column;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
}
.header-title {
font-weight: 500;
font-size: 20px;
color: #4a4a4a;
line-height: 16px;
}
.item {
margin-bottom: 18px;
}
</style>

View File

@ -139,6 +139,16 @@ const routes = [
breadcrumb: true,
parentRoute: 'warningManagement' // 用于在面包屑中建立父子关系
}
},
// 项目管理
{
path: '/projectManagement',
name: 'projectManagement',
component: () => import('../views/ProjectManagement_Rebuild/index.vue'),
meta: {
title: '项目管理',
breadcrumb: true
}
}
]

View File

@ -0,0 +1,372 @@
<template>
<div class="detail-container">
<el-form ref="formRef" :model="form" label-position="right" label-width="auto"
style="max-height: 60vh; overflow-y: auto; padding-right: 50px" :rules="rules">
<el-row style="margin: 20px 0px;">
<h4>项目信息</h4>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="工程状态" prop="工程状态">
<el-radio-group v-model="form.zt">
<el-radio value="1">在建</el-radio>
<el-radio value="2">停工</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="所属区县" prop="所属区县">
<el-select v-model="qx" filterable remote reserve-keyword clearable placeholder="输入区县名称查询"
:remote-method="remoteMethod_qx" :loading="loading" @change="handleSelect_qx" value-key="index">
<el-option v-for="(item, index) in qxList" :key="index" :label="item.qxmc" :value="item.qxmc" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="项目名称" prop="项目名称">
<el-input />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="驻地名称" prop="驻地名称">
<el-input />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="驻地类型" prop="驻地类型">
<el-select>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="坐标点位" prop="坐标点位">
<el-row :gutter="10">
<el-col :span="12">
<el-input aria-label="经度" placeholder="经度" />
</el-col>
<el-col :span="12">
<el-input aria-label="纬度" placeholder="纬度" />
</el-col>
</el-row>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="所属项目名称:">
<el-input />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="项目类型:">
<el-select>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="建设单位:">
<el-input />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="施工单位:">
<el-input />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="行政区域:">
<el-select ></el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="驻地人数:">
<el-input />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="驻地风险等级:">
<el-select ></el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="房建类型:">
<el-select ></el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="搬迁状态:">
<el-select ></el-select>
</el-form-item>
</el-col>
</el-row>
<el-row style="margin: 20px 0px;">
<h4>项目联系人信息</h4>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="吹哨人姓名:">
<el-input />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="吹哨人电话:">
<el-input />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="建设单位包保责任人姓名:">
<el-input />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="建设单位包保责任人电话:">
<el-input />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="施工单位包保责任人姓名:">
<el-input />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="施工单位包保责任人电话:">
<el-input />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="驻地包保责任人姓名:">
<el-input />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="驻地包保责任人电话:">
<el-input />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="区县级包保责任人姓名:">
<el-input />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="区县级包保责任人电话:">
<el-input />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="市级包保责任人姓名:">
<el-input />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="市级包保责任人电话:">
<el-input />
</el-form-item>
</el-col>
</el-row>
<el-row style="margin: 20px 0px;">
<h4>其他信息</h4>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注:">
<el-input />
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</template>
<script setup>
import { ref, onMounted, watch, computed } from "vue";
import { request } from "@/utils/request";
const formRef = ref(null);
defineExpose({ formRef });
const props = defineProps({
form: {
type: Object,
default: () => ({}),
},
});
const sfjwd = ref("是");
const qx = ref("");
const loading = ref(false);
const selectOptions = ref([]);
const qxList = ref([]);
//
const getUserList = async (key) => {
try {
const keyword = key;
let url = "";
if (keyword) {
url = `/snow-ops-platform/yhzry/getUserByKey?key=${keyword}`;
} else {
url = `/snow-ops-platform/yhzry/getUserByKey?key=`;
}
const res = await request({
url: url,
method: "GET",
});
if (res.code === "00000") {
return res.data;
} else {
throw new Error(res.message);
}
} catch (error) {
ElMessage.error(error.message);
console.log(error);
}
};
//
const remoteMethod = async (query) => {
if (query === "") {
selectOptions.value = [];
return [];
}
loading.value = true;
const res = await getUserList(query);
if (res) {
selectOptions.value = res;
}
loading.value = false;
};
//
const getQxList = async (key) => {
try {
const keyword = key;
let url = "";
if (keyword) {
url = `/snow-ops-platform/district/listDistricts?qxmc=${keyword}`;
} else {
url = `/snow-ops-platform/district/listDistricts?qxmc=`;
}
const res = await request({
url: url,
method: "GET",
});
if (res.code === "00000") {
return res.data;
} else {
throw new Error(res.message);
}
} catch (error) { }
};
//
const remoteMethod_qx = async (query) => {
loading.value = true;
const res = await getQxList(query);
if (res) {
qxList.value = res;
}
loading.value = false;
};
//
const handleSelect_qx = (value) => {
props.form.qxmc = value;
};
//
const handleSelect = (value) => {
console.log("value", value);
props.form.fzrXm = value.realName;
props.form.fzrSjhm = value.phone;
props.form.fzrUserId = value.userId;
};
const rules = computed(() => {
return {
mc: [
{
required: true,
validator: (rule, value, callback) => {
if (props.form.mc) {
callback();
} else {
callback(new Error("请输入服务站名称"));
}
},
trigger: "blur",
},
],
qxmc: [
{
required: true,
validator: (rule, value, callback) => {
if (props.form.qxmc) {
callback();
} else {
callback(new Error("请选择所属区县"));
}
},
trigger: "blur",
},
],
fzr: [
{
required: true,
validator: (rule, value, callback) => {
if (props.form.fzrUserId && props.form.fzrXm && props.form.fzrSjhm) {
callback();
} else {
callback(new Error("请选择负责人"));
}
},
trigger: "blur",
},
],
jd: [
{
required: sfjwd.value === "否",
validator: (rule, value, callback) => {
if (props.form.jd) {
callback();
} else {
callback(new Error("请输入站点经度"));
}
},
trigger: "blur",
},
],
wd: [
{
required: sfjwd.value === "否",
validator: (rule, value, callback) => {
if (props.form.wd) {
callback();
} else {
callback(new Error("请输入站点纬度"));
}
},
trigger: "blur",
},
],
};
});
</script>
<style></style>

View File

@ -0,0 +1,205 @@
import { h, ref, onMounted, reactive, watch, toRaw, nextTick } from "vue";
import { request } from "@/utils/request";
import { useRoute, useRouter } from 'vue-router'
import AddDialog from "./addDialog.vue";
const tableData = ref([]); // 表格数据
const modelVisible = ref(false); // 弹窗状态
const drawerVisible = ref(false); // 抽屉状态
// 弹窗内容
const model = reactive({
title: '',
content: null,
props: {},
onCancel: null,
onConfirm: null,
width: '',
});
const form = reactive({
});
const INIT_FORM = {
};
// 抽屉内容
const drawer = reactive({
title: '',
content: null,
props: {},
onCancel: null,
onConfirm: null,
direction: 'rtl',
size: '50%'
});
const dialogRef = ref(null); // 弹窗实例
const drawerRef = ref(null); // 抽屉实例
const columns = [
{
prop: "xxx",
label: "区县",
},
{
prop: "xxx",
label: "路线编码",
},
{
prop: "xxx",
label: "灾害类型",
},
{
prop: "xxx",
label: "起点桩号",
},
{
prop: "xxx",
label: "止点桩号",
},
{
prop: "xxx",
label: "实施里程(公里)",
},
{
prop: "xxx",
label: "技术等级",
},
{
prop: "xxx",
label: "总投资金额(万元)",
},
{
prop: "xxx",
label: "投资估算(万元)",
},
{
prop: "xxx",
label: "开工或预计开工时间",
},
{
prop: "xxx",
label: "完工或预计完工时间",
},
{
prop: "xxx",
label: "申报状态",
},
{
prop: "xxx",
label: "审批状态",
},
{
prop: "xxx",
label: "更新日期",
},
{
label: "操作",
fixed: "right",
width: 150,
render: (row) => () =>
h("div", { class: "action-btns" }, [
h(
ElButton,
{
type: "primary",
link: true,
onClick: async () => {
},
},
() => "审批"
),
h(
ElButton,
{
type: "primary",
link: true,
style: "margin-left: 10px;",
onClick: async () => {
},
},
() => "详情"
),
]),
},
]
// 过滤条件
const filterData = reactive({
year: "",
code: "",
})
// 分页
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
pageSizes: [10, 20, 50],
layout: "prev, pager, next, jumper",
onChange: (page, pageSize) => {
pagination.current = page;
pagination.pageSize = pageSize;
getTableData(filterData);
},
});
// 获取预警列表
const getTableData = async (filterData) => {
try {
const res = await request({
url: '',
method: "GET",
params: {
}
})
} catch (error) {
}
}
// 打开填报项目弹窗
const openAddDialog = () => {
model.title = '填报项目';
Object.assign(form, INIT_FORM);
model.props = {
form: form,
};
model.content = AddDialog;
model.onCancel = () => {
modelVisible.value = false;
};
model.onConfirm = async () => {
dialogType.value = '';
await dialogRef?.value?.dynamicComponentRef?.formRef.validate().then(() => {
console.log('@@@@@填报项目', form);
})
.catch((err) => {
ElMessage.error('请处理表单中的错误项');
});
};
model.width = "70%"
modelVisible.value = true;
}
export default () => {
const router = useRouter();
return {
tableData,
filterData,
pagination,
columns,
modelVisible,
model,
drawerVisible,
drawer,
dialogRef,
drawerRef,
openAddDialog,
}
}

View File

@ -0,0 +1,52 @@
<template>
<div class="root">
<div class="search-box">
<el-date-picker v-model="script.filterData.year" type="year" placeholder="年度" format="YYYY"
style="width: 240px; margin-right: 10px" size="large" />
<el-input v-model="script.filterData.code" style="width: 240px; margin-right: 10px" size="large"
placeholder="路线编码" :suffix-icon="Search" />
</div>
<div class="event-box">
<el-button type="primary" @click="script.openAddDialog">项目填报</el-button>
<el-button type="primary" color="#952DE6" @click="">导出</el-button>
</div>
<DynamicTable :dataSource="script.tableData.value" :columns="script.columns" :autoHeight="true"
:pagination="script.pagination">
</DynamicTable>
<div class="model-box">
<MyDialog v-model="script.modelVisible.value" :title="script.model?.title"
:dynamicComponent="script.model?.content" :component-props="script.model?.props"
:onConfirm="script.model?.onConfirm" :onCancel="script.model?.onCancel" ref="dialogRef"
:width="script.model?.width">
</MyDialog>
<MyDrawer v-model="script.drawerVisible.value" :title="script.drawer?.title"
:dynamicComponent="script.drawer?.content" :component-props="script.drawer?.props"
:onConfirm="script.drawer?.onConfirm" :onCancel="script.drawer?.onCancel" ref="drawerRef"
:direction="script.drawer?.direction" :size="script.drawer?.size">
</MyDrawer>
</div>
</div>
</template>
<script setup>
import DynamicTable from "../../component/DynamicTable";
import { Search } from "@element-plus/icons-vue";
import MyDialog from "../../component/MyDialog";
import MyDrawer from "../../component/MyDrawer";
import scriptFn from "./index.js";
const script = scriptFn();
</script>
<style scoped>
.root {
height: 100%;
padding: 25px;
}
.event-box {
margin: 20px 0;
display: flex;
justify-content: flex-end;
}
</style>