消息模块、爬虫
This commit is contained in:
326
schoolNewsWeb/src/views/public/message/MessageDetail.vue
Normal file
326
schoolNewsWeb/src/views/public/message/MessageDetail.vue
Normal file
@@ -0,0 +1,326 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="message-detail">
|
||||
<div v-if="messageData" class="detail-container">
|
||||
<!-- 返回按钮 -->
|
||||
<el-button
|
||||
v-if="showBackButton"
|
||||
type="text"
|
||||
@click="handleBack"
|
||||
class="back-button"
|
||||
>
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
{{ backButtonText }}
|
||||
</el-button>
|
||||
|
||||
<!-- 消息头部 -->
|
||||
<div class="message-header">
|
||||
<div class="header-left">
|
||||
<h2 class="message-title">{{ messageData.title }}</h2>
|
||||
<div class="message-meta">
|
||||
<el-tag :type="getMessageTypeTag(messageData.messageType || '')" size="large">
|
||||
{{ getMessageTypeLabel(messageData.messageType || '') }}
|
||||
</el-tag>
|
||||
<MessagePriorityBadge :priority="messageData.priority || ''" size="large" />
|
||||
<MessageStatusBadge :status="messageData.status||''" size="large" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<!-- 消息信息 -->
|
||||
<div class="message-info">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="创建时间">
|
||||
<el-icon><Clock /></el-icon>
|
||||
{{ messageData.createTime }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
<el-icon><Clock /></el-icon>
|
||||
{{ messageData.updateTime }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="计划发送时间">
|
||||
<el-icon><Timer /></el-icon>
|
||||
{{ messageData.scheduledTime || '立即发送' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="实际发送时间">
|
||||
<el-icon><Timer /></el-icon>
|
||||
{{ messageData.actualSendTime || '未发送' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发送方式" :span="2">
|
||||
<el-tag
|
||||
v-for="method in messageData.sendMethods"
|
||||
:key="method"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
{{ getSendMethodLabel(method) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最大重试次数">
|
||||
{{ messageData.maxRetryCount }} 次
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前重试次数">
|
||||
{{ messageData.retryCount || 0 }} 次
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<el-divider content-position="left">消息内容</el-divider>
|
||||
|
||||
<!-- 消息内容 -->
|
||||
<div class="message-content">
|
||||
<el-card shadow="never" class="content-card">
|
||||
<div class="content-text">{{ messageData.content }}</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 发送统计(如果有统计数据) -->
|
||||
<template v-if="showStatistics && hasStatistics">
|
||||
<el-divider content-position="left">发送统计</el-divider>
|
||||
<MessageStatistic :statistics="messageData" />
|
||||
</template>
|
||||
|
||||
<!-- 操作按钮(如果提供) -->
|
||||
<div v-if="showActions" class="message-actions">
|
||||
<slot name="actions" :message="messageData" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-else-if="!loading" description="消息不存在" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ArrowLeft, Clock, Timer } from '@element-plus/icons-vue';
|
||||
import { messageApi } from '@/apis/message';
|
||||
import { MessagePriorityBadge, MessageStatusBadge } from '@/components/message';
|
||||
import { MessageStatistic } from '@/views/admin/manage/message/components';
|
||||
import type { MessageVO } from '@/types';
|
||||
|
||||
interface Props {
|
||||
messageId: string;
|
||||
showBackButton?: boolean;
|
||||
backButtonText?: string;
|
||||
showStatistics?: boolean;
|
||||
showActions?: boolean;
|
||||
autoMarkRead?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'back'): void;
|
||||
(e: 'loaded', message: MessageVO): void;
|
||||
(e: 'error', error: any): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
showBackButton: false,
|
||||
backButtonText: '返回',
|
||||
showStatistics: false,
|
||||
showActions: false,
|
||||
autoMarkRead: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const loading = ref(false);
|
||||
const messageData = ref<MessageVO | null>(null);
|
||||
|
||||
/** 是否有统计数据 */
|
||||
const hasStatistics = computed(() => {
|
||||
if (!messageData.value) return false;
|
||||
return (
|
||||
messageData.value.targetUserCount !== undefined &&
|
||||
messageData.value.targetUserCount > 0
|
||||
);
|
||||
});
|
||||
|
||||
/** 加载消息详情 */
|
||||
async function loadMessage() {
|
||||
if (!props.messageId) {
|
||||
ElMessage.warning('消息ID不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await messageApi.getMessageDetail(props.messageId);
|
||||
if (result.success && result.data) {
|
||||
messageData.value = result.data;
|
||||
emit('loaded', result.data);
|
||||
|
||||
// 自动标记已读(用户端)
|
||||
if (props.autoMarkRead) {
|
||||
markAsRead();
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(result.message || '加载失败');
|
||||
emit('error', new Error(result.message || '加载失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载消息详情失败:', error);
|
||||
ElMessage.error('加载失败');
|
||||
emit('error', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 标记已读 */
|
||||
async function markAsRead() {
|
||||
if (!props.messageId) return;
|
||||
|
||||
try {
|
||||
await messageApi.markAsRead(props.messageId);
|
||||
} catch (error) {
|
||||
console.error('标记已读失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回 */
|
||||
function handleBack() {
|
||||
emit('back');
|
||||
}
|
||||
|
||||
/** 获取消息类型标签 */
|
||||
function getMessageTypeTag(type: string): string {
|
||||
const map: Record<string, string> = {
|
||||
notice: '',
|
||||
announcement: 'success',
|
||||
warning: 'warning',
|
||||
system: 'info'
|
||||
};
|
||||
return map[type] || '';
|
||||
}
|
||||
|
||||
/** 获取消息类型文本 */
|
||||
function getMessageTypeLabel(type: string): string {
|
||||
const map: Record<string, string> = {
|
||||
notice: '通知',
|
||||
announcement: '公告',
|
||||
warning: '警告',
|
||||
system: '系统消息'
|
||||
};
|
||||
return map[type] || type;
|
||||
}
|
||||
|
||||
/** 获取发送方式文本 */
|
||||
function getSendMethodLabel(method: string): string {
|
||||
const map: Record<string, string> = {
|
||||
system: '系统消息',
|
||||
email: '邮件通知',
|
||||
sms: '短信通知'
|
||||
};
|
||||
return map[method] || method;
|
||||
}
|
||||
|
||||
/** 刷新数据 */
|
||||
function refresh() {
|
||||
loadMessage();
|
||||
}
|
||||
|
||||
/** 监听messageId变化 */
|
||||
watch(() => props.messageId, () => {
|
||||
loadMessage();
|
||||
}, { immediate: false });
|
||||
|
||||
onMounted(() => {
|
||||
loadMessage();
|
||||
});
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
refresh,
|
||||
messageData
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.message-detail {
|
||||
.detail-container {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
color: #409eff;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
}
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.message-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.message-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.message-info {
|
||||
margin-bottom: 24px;
|
||||
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 140px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-content {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.content-card {
|
||||
background: #f9f9f9;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-text {
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
color: #606266;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.message-actions {
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
7
schoolNewsWeb/src/views/public/message/index.ts
Normal file
7
schoolNewsWeb/src/views/public/message/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @description 公共消息组件导出
|
||||
* @author Claude
|
||||
* @since 2025-11-13
|
||||
*/
|
||||
|
||||
export { default as MessageDetail } from './MessageDetail.vue';
|
||||
Reference in New Issue
Block a user