10 changed files with 719 additions and 200 deletions
@ -0,0 +1,312 @@ |
|||||
|
<template> |
||||
|
<div class="notice-message-page app_content"> |
||||
|
<!-- 保留一份反馈记录 --> |
||||
|
<!-- 顶部导航 --> |
||||
|
<!-- <van-nav-bar |
||||
|
title="预告留言" |
||||
|
left-arrow |
||||
|
@click-left="onBack" |
||||
|
/> --> |
||||
|
|
||||
|
<!-- 消息列表区域 --> |
||||
|
<div class="message-area" ref="messageAreaRef"> |
||||
|
<div v-if="loading" class="loading-wrap"> |
||||
|
<van-loading size="18" /> 加载中... |
||||
|
</div> |
||||
|
<template v-else> |
||||
|
<van-empty v-if="!messageList.length" description="暂无留言" image-size="100" /> |
||||
|
<div |
||||
|
v-for="(msg, idx) in messageList" |
||||
|
:key="msg.id || idx" |
||||
|
class="message-item" |
||||
|
:class="{ 'message-right': msg.msgFrom === 0, 'message-left': msg.msgFrom === 1 }" |
||||
|
> |
||||
|
<!-- 左侧(企业消息) --> |
||||
|
<div v-if="msg.msgFrom === 1" class="msg-avatar left-avatar">企</div> |
||||
|
<div class="msg-bubble" :class="{ 'bubble-left': msg.msgFrom === 1, 'bubble-right': msg.msgFrom === 0 }"> |
||||
|
<div class="msg-sender">{{ msg.msgFrom === 0 ? (msg.bureauDeptName || useCommon.bureauUnitName || '执法单位') : msg.userName }}</div> |
||||
|
<div class="msg-content">{{ msg.message }}</div> |
||||
|
<div class="msg-time">{{ formatTime(msg.createTime) }}</div> |
||||
|
</div> |
||||
|
<!-- 右侧(执法单位消息) --> |
||||
|
</div> |
||||
|
</template> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 底部输入区域(非只读时显示) --> |
||||
|
<div v-if="!isReadonly" class="input-area"> |
||||
|
<van-field |
||||
|
v-model="inputMessage" |
||||
|
type="textarea" |
||||
|
placeholder="输入留言内容..." |
||||
|
rows="1" |
||||
|
autosize |
||||
|
class="input-field" |
||||
|
/> |
||||
|
<van-button |
||||
|
type="primary" |
||||
|
size="small" |
||||
|
class="send-btn" |
||||
|
:disabled="!inputMessage.trim()" |
||||
|
:loading="sending" |
||||
|
@click="onSend" |
||||
|
> |
||||
|
留言 |
||||
|
</van-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup name="cpNoticeMessage"> |
||||
|
import { useRouter, useRoute } from 'vue-router'; |
||||
|
import { showToast } from 'vant'; |
||||
|
import { getNoticeMessages, addNoticeMessage } from '@/api/enforcement/notice'; |
||||
|
|
||||
|
const router = useRouter(); |
||||
|
const route = useRoute(); |
||||
|
const { useCommon } = $globalStore; |
||||
|
|
||||
|
const noticeId = computed(() => route.query.id || ''); |
||||
|
const isReadonly = computed(() => route.query.readonly === '1'); |
||||
|
|
||||
|
const messageList = ref([]); |
||||
|
const loading = ref(false); |
||||
|
const inputMessage = ref(''); |
||||
|
const sending = ref(false); |
||||
|
const messageAreaRef = ref(null); |
||||
|
let pollTimer = null; |
||||
|
|
||||
|
onMounted(() => { |
||||
|
loadMessages(); |
||||
|
startPolling(); |
||||
|
}); |
||||
|
|
||||
|
onUnmounted(() => { |
||||
|
stopPolling(); |
||||
|
}); |
||||
|
|
||||
|
const startPolling = () => { |
||||
|
stopPolling(); |
||||
|
pollTimer = setInterval(() => { |
||||
|
pollMessages(); |
||||
|
}, 16000); |
||||
|
}; |
||||
|
|
||||
|
const stopPolling = () => { |
||||
|
if (pollTimer) { |
||||
|
clearInterval(pollTimer); |
||||
|
pollTimer = null; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 轮询加载新消息(静默,不显示loading) |
||||
|
const pollMessages = () => { |
||||
|
if (!noticeId.value) return; |
||||
|
getNoticeMessages(noticeId.value, (res) => { |
||||
|
if (res.code === 200 && Array.isArray(res.data)) { |
||||
|
const oldCount = messageList.value.length; |
||||
|
messageList.value = res.data; |
||||
|
if (res.data.length > oldCount) { |
||||
|
scrollToBottom(); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
const loadMessages = () => { |
||||
|
if (!noticeId.value) return; |
||||
|
loading.value = true; |
||||
|
getNoticeMessages(noticeId.value, (res) => { |
||||
|
loading.value = false; |
||||
|
if (res.code === 200 && Array.isArray(res.data)) { |
||||
|
messageList.value = res.data; |
||||
|
scrollToBottom(); |
||||
|
} |
||||
|
}, () => { |
||||
|
loading.value = false; |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
const onSend = () => { |
||||
|
const text = inputMessage.value.trim(); |
||||
|
if (!text) return; |
||||
|
sending.value = true; |
||||
|
addNoticeMessage({ |
||||
|
noticeId: noticeId.value, |
||||
|
userId: String(useCommon.userId || ''), |
||||
|
userName: useCommon.nickName || useCommon.userName || '', |
||||
|
bureauDeptName: useCommon.bureauUnitName || '', |
||||
|
msgFrom: 0, // 执法单位=0 |
||||
|
message: text, |
||||
|
}, (res) => { |
||||
|
sending.value = false; |
||||
|
if (res.code === 200) { |
||||
|
inputMessage.value = ''; |
||||
|
loadMessages(); |
||||
|
} else { |
||||
|
showToast(res.msg || '发送失败'); |
||||
|
} |
||||
|
}, () => { |
||||
|
sending.value = false; |
||||
|
showToast('发送失败,请重试'); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
const scrollToBottom = () => { |
||||
|
nextTick(() => { |
||||
|
if (messageAreaRef.value) { |
||||
|
messageAreaRef.value.scrollTop = messageAreaRef.value.scrollHeight; |
||||
|
} |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
const formatTime = (dateStr) => { |
||||
|
if (!dateStr) return ''; |
||||
|
return String(dateStr).substring(0, 16).replace('T', ' '); |
||||
|
}; |
||||
|
|
||||
|
const onBack = () => { |
||||
|
router.back(); |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="less" scoped> |
||||
|
.notice-message-page { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
height: 100%; |
||||
|
background: #fbfdff; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
:deep(.van-nav-bar) { |
||||
|
background: #347bc3; |
||||
|
flex-shrink: 0; |
||||
|
.van-nav-bar__title { color: #fff; font-size: 18px; font-weight: 600; } |
||||
|
.van-icon { color: #fff; } |
||||
|
} |
||||
|
|
||||
|
.message-area { |
||||
|
flex: 1; |
||||
|
overflow-y: auto; |
||||
|
padding: 16px 12px; |
||||
|
} |
||||
|
|
||||
|
.loading-wrap { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 30px 0; |
||||
|
color: #999; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.message-item { |
||||
|
display: flex; |
||||
|
align-items: flex-start; |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
.message-left { |
||||
|
flex-direction: row; |
||||
|
} |
||||
|
|
||||
|
.message-right { |
||||
|
flex-direction: row-reverse; |
||||
|
} |
||||
|
|
||||
|
.msg-avatar { |
||||
|
width: 36px; |
||||
|
height: 36px; |
||||
|
border-radius: 50%; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
font-size: 14px; |
||||
|
font-weight: 600; |
||||
|
color: #fff; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.left-avatar { |
||||
|
background: linear-gradient(135deg, #f5a623, #f7c948); |
||||
|
margin-right: 10px; |
||||
|
} |
||||
|
|
||||
|
.right-avatar { |
||||
|
background: linear-gradient(135deg, #1890ff, #0b63d1); |
||||
|
margin-left: 10px; |
||||
|
} |
||||
|
|
||||
|
.msg-bubble { |
||||
|
max-width: 70%; |
||||
|
padding: 10px 14px; |
||||
|
border-radius: 12px; |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
.bubble-left { |
||||
|
background: #fff; |
||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); |
||||
|
border-top-left-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
.bubble-right { |
||||
|
background: linear-gradient(135deg, #1890ff, #0b63d1); |
||||
|
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2); |
||||
|
border-top-right-radius: 4px; |
||||
|
.msg-sender { color: rgba(255, 255, 255, 0.8); } |
||||
|
.msg-content { color: #fff; } |
||||
|
.msg-time { color: rgba(255, 255, 255, 0.6); } |
||||
|
} |
||||
|
|
||||
|
.msg-sender { |
||||
|
font-size: 12px; |
||||
|
color: #999; |
||||
|
margin-bottom: 4px; |
||||
|
} |
||||
|
|
||||
|
.msg-content { |
||||
|
font-size: 15px; |
||||
|
color: #333; |
||||
|
line-height: 1.5; |
||||
|
word-break: break-all; |
||||
|
} |
||||
|
|
||||
|
.msg-time { |
||||
|
font-size: 11px; |
||||
|
color: #bbb; |
||||
|
margin-top: 4px; |
||||
|
text-align: right; |
||||
|
} |
||||
|
|
||||
|
.input-area { |
||||
|
display: flex; |
||||
|
align-items: flex-end; |
||||
|
gap: 10px; |
||||
|
padding: 10px 12px; |
||||
|
background: #fff; |
||||
|
border-top: 1px solid #e4edf7; |
||||
|
flex-shrink: 0; |
||||
|
|
||||
|
.input-field { |
||||
|
flex: 1; |
||||
|
background: #fbfdff; |
||||
|
border-radius: 20px; |
||||
|
padding: 8px 14px; |
||||
|
|
||||
|
:deep(.van-field__control) { |
||||
|
min-height: 24px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.send-btn { |
||||
|
height: 36px; |
||||
|
border-radius: 18px; |
||||
|
padding: 0 18px; |
||||
|
font-size: 14px; |
||||
|
background: linear-gradient(135deg, #1890ff, #0b63d1); |
||||
|
border: none; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue