283 lines
7.5 KiB
Vue
283 lines
7.5 KiB
Vue
|
<template>
|
|||
|
<div class="message-management">
|
|||
|
<div class="page-header">
|
|||
|
<h2>留言管理</h2>
|
|||
|
</div>
|
|||
|
|
|||
|
<el-table :data="messages" style="width: 100%">
|
|||
|
<el-table-column prop="name" label="姓名" width="120" />
|
|||
|
<el-table-column prop="email" label="邮箱" width="200" />
|
|||
|
<el-table-column prop="content" label="内容">
|
|||
|
<template #default="scope">
|
|||
|
<div class="message-preview">
|
|||
|
{{ getPreviewContent(scope.row.content) }}
|
|||
|
<el-button
|
|||
|
v-if="scope.row.content.length > 50"
|
|||
|
type="text"
|
|||
|
@click="showMessageDetail(scope.row)"
|
|||
|
>
|
|||
|
查看更多
|
|||
|
</el-button>
|
|||
|
</div>
|
|||
|
</template>
|
|||
|
</el-table-column>
|
|||
|
<el-table-column prop="status" label="状态" width="100">
|
|||
|
<template #default="scope">
|
|||
|
<el-tag :type="scope.row.status === 'unread' ? 'warning' : 'success'">
|
|||
|
{{ scope.row.status === 'unread' ? '未读' : '已读' }}
|
|||
|
</el-tag>
|
|||
|
</template>
|
|||
|
</el-table-column>
|
|||
|
<el-table-column prop="created_at" label="留言时间" width="180">
|
|||
|
<template #default="scope">
|
|||
|
{{ formatDate(scope.row.created_at) }}
|
|||
|
</template>
|
|||
|
</el-table-column>
|
|||
|
<el-table-column v-if="isAdmin" prop="read_info" label="阅读信息" width="200">
|
|||
|
<template #default="scope">
|
|||
|
<div v-if="scope.row.read_by">
|
|||
|
<div>阅读者: {{ scope.row.reader_name }}</div>
|
|||
|
<div>阅读时间: {{ formatDate(scope.row.read_at) }}</div>
|
|||
|
</div>
|
|||
|
<span v-else>-</span>
|
|||
|
</template>
|
|||
|
</el-table-column>
|
|||
|
<el-table-column label="操作" width="160" align="center">
|
|||
|
<template #default="scope">
|
|||
|
<div class="action-buttons">
|
|||
|
<el-button
|
|||
|
v-if="scope.row.status === 'unread'"
|
|||
|
size="small"
|
|||
|
type="primary"
|
|||
|
@click="showMessageDetail(scope.row)"
|
|||
|
>
|
|||
|
阅读
|
|||
|
</el-button>
|
|||
|
<el-button
|
|||
|
size="small"
|
|||
|
type="danger"
|
|||
|
@click="deleteMessage(scope.row.id)"
|
|||
|
>
|
|||
|
删除
|
|||
|
</el-button>
|
|||
|
</div>
|
|||
|
</template>
|
|||
|
</el-table-column>
|
|||
|
</el-table>
|
|||
|
|
|||
|
<!-- 留言详情对话框 -->
|
|||
|
<el-dialog
|
|||
|
v-model="dialogVisible"
|
|||
|
title="留言详情"
|
|||
|
width="600px"
|
|||
|
:before-close="handleDialogClose"
|
|||
|
>
|
|||
|
<div v-if="selectedMessage" class="message-detail">
|
|||
|
<div class="detail-item">
|
|||
|
<label>姓名:</label>
|
|||
|
<span>{{ selectedMessage.name }}</span>
|
|||
|
</div>
|
|||
|
<div class="detail-item">
|
|||
|
<label>邮箱:</label>
|
|||
|
<span>{{ selectedMessage.email }}</span>
|
|||
|
</div>
|
|||
|
<div class="detail-item">
|
|||
|
<label>留言时间:</label>
|
|||
|
<span>{{ formatDate(selectedMessage.created_at) }}</span>
|
|||
|
</div>
|
|||
|
<div class="detail-item">
|
|||
|
<label>内容:</label>
|
|||
|
<div class="message-content">{{ selectedMessage.content }}</div>
|
|||
|
</div>
|
|||
|
<div v-if="isAdmin && selectedMessage.read_by" class="detail-item">
|
|||
|
<label>阅读信息:</label>
|
|||
|
<div>
|
|||
|
<p>阅读者: {{ selectedMessage.reader_name }}</p>
|
|||
|
<p>阅读时间: {{ formatDate(selectedMessage.read_at) }}</p>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</el-dialog>
|
|||
|
</div>
|
|||
|
</template>
|
|||
|
|
|||
|
<script>
|
|||
|
import { ref, onMounted, computed, onUnmounted } from 'vue'
|
|||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|||
|
import axios from 'axios'
|
|||
|
import { useRouter } from 'vue-router'
|
|||
|
|
|||
|
export default {
|
|||
|
name: 'MessageManagement',
|
|||
|
setup() {
|
|||
|
const router = useRouter()
|
|||
|
const messages = ref([])
|
|||
|
const dialogVisible = ref(false)
|
|||
|
const selectedMessage = ref(null)
|
|||
|
const currentUserRole = localStorage.getItem('admin_role')
|
|||
|
const ws = ref(null)
|
|||
|
const unreadCount = ref(0)
|
|||
|
|
|||
|
const isAdmin = computed(() => {
|
|||
|
return ['superadmin', 'admin'].includes(currentUserRole)
|
|||
|
})
|
|||
|
|
|||
|
const connectWebSocket = () => {
|
|||
|
const token = localStorage.getItem('admin_token')
|
|||
|
if (!token) return
|
|||
|
|
|||
|
const wsUrl = `ws://localhost:3000?token=${token}`
|
|||
|
ws.value = new WebSocket(wsUrl)
|
|||
|
|
|||
|
ws.value.onmessage = (event) => {
|
|||
|
const data = JSON.parse(event.data)
|
|||
|
if (data.type === 'new_message') {
|
|||
|
// 将新消息添加到列表开头
|
|||
|
messages.value.unshift(data.data)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
ws.value.onclose = () => {
|
|||
|
setTimeout(connectWebSocket, 5000) // 断线重连
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const fetchMessages = async () => {
|
|||
|
try {
|
|||
|
const response = await axios.get('/api/messages')
|
|||
|
messages.value = response.data
|
|||
|
unreadCount.value = messages.value.filter(m => m.status === 'unread').length
|
|||
|
window.dispatchEvent(new CustomEvent('unread-messages-update', {
|
|||
|
detail: unreadCount.value
|
|||
|
}))
|
|||
|
} catch (error) {
|
|||
|
ElMessage.error('获取留言列表失败')
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const getPreviewContent = (content) => {
|
|||
|
return content.length > 50 ? content.slice(0, 50) + '...' : content
|
|||
|
}
|
|||
|
|
|||
|
const showMessageDetail = async (message) => {
|
|||
|
selectedMessage.value = message
|
|||
|
dialogVisible.value = true
|
|||
|
|
|||
|
// 如果是未读消息,标记为已读
|
|||
|
if (message.status === 'unread') {
|
|||
|
try {
|
|||
|
await axios.put(`/api/messages/${message.id}/read`)
|
|||
|
message.status = 'read'
|
|||
|
unreadCount.value--
|
|||
|
window.dispatchEvent(new CustomEvent('unread-messages-update', {
|
|||
|
detail: unreadCount.value
|
|||
|
}))
|
|||
|
// 重新获取消息列表以更新阅读信息
|
|||
|
fetchMessages()
|
|||
|
} catch (error) {
|
|||
|
console.error('Error marking message as read:', error)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const deleteMessage = async (id) => {
|
|||
|
try {
|
|||
|
await ElMessageBox.confirm(
|
|||
|
'确定要删除这条留言吗?此操作不可恢复',
|
|||
|
'警告',
|
|||
|
{
|
|||
|
confirmButtonText: '确定',
|
|||
|
cancelButtonText: '取消',
|
|||
|
type: 'warning'
|
|||
|
}
|
|||
|
)
|
|||
|
|
|||
|
await axios.delete(`/api/messages/${id}`)
|
|||
|
ElMessage.success('留言删除成功')
|
|||
|
messages.value = messages.value.filter(msg => msg.id !== id)
|
|||
|
} catch (error) {
|
|||
|
if (error !== 'cancel') {
|
|||
|
ElMessage.error('删除失败')
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const formatDate = (date) => {
|
|||
|
return new Date(date).toLocaleString()
|
|||
|
}
|
|||
|
|
|||
|
const handleDialogClose = (done) => {
|
|||
|
selectedMessage.value = null
|
|||
|
done()
|
|||
|
}
|
|||
|
|
|||
|
onMounted(() => {
|
|||
|
fetchMessages()
|
|||
|
connectWebSocket()
|
|||
|
})
|
|||
|
|
|||
|
onUnmounted(() => {
|
|||
|
if (ws.value) {
|
|||
|
ws.value.close()
|
|||
|
}
|
|||
|
})
|
|||
|
|
|||
|
return {
|
|||
|
messages,
|
|||
|
dialogVisible,
|
|||
|
selectedMessage,
|
|||
|
isAdmin,
|
|||
|
getPreviewContent,
|
|||
|
showMessageDetail,
|
|||
|
deleteMessage,
|
|||
|
formatDate,
|
|||
|
handleDialogClose
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
</script>
|
|||
|
|
|||
|
<style scoped>
|
|||
|
.message-management {
|
|||
|
padding: 20px;
|
|||
|
}
|
|||
|
|
|||
|
.page-header {
|
|||
|
margin-bottom: 20px;
|
|||
|
}
|
|||
|
|
|||
|
.action-buttons {
|
|||
|
display: flex;
|
|||
|
gap: 8px;
|
|||
|
justify-content: center;
|
|||
|
}
|
|||
|
|
|||
|
.message-preview {
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
gap: 10px;
|
|||
|
}
|
|||
|
|
|||
|
.message-detail {
|
|||
|
padding: 20px;
|
|||
|
}
|
|||
|
|
|||
|
.detail-item {
|
|||
|
margin-bottom: 15px;
|
|||
|
}
|
|||
|
|
|||
|
.detail-item label {
|
|||
|
font-weight: bold;
|
|||
|
margin-right: 10px;
|
|||
|
color: #666;
|
|||
|
}
|
|||
|
|
|||
|
.message-content {
|
|||
|
margin-top: 10px;
|
|||
|
white-space: pre-wrap;
|
|||
|
line-height: 1.5;
|
|||
|
background: #f8f9fa;
|
|||
|
padding: 15px;
|
|||
|
border-radius: 4px;
|
|||
|
}
|
|||
|
</style>
|