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> |