For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 完善消息管理模块,增加消息发送测试功能,增强消息监控能力,方便管理员测试配置和监控发送状态。
Architecture: 创建独立的消息管理页面(/message/manage),包含发送测试面板和监控列表。新建后端 MessageManageController 和 Service,提供管理员专用的消息查询和监控接口。复用现有消息发送接口进行测试。
Tech Stack: Vue 3, Naive UI, MyBatis-Plus, Spring Boot
forge-admin-ui/src/views/message/manage.vue - 消息管理页面(发送测试+监控列表)forge-admin-ui/src/api/message.js - 新增管理接口调用方法forge-admin-ui/src/router/index.js - 新增路由配置(如需要)forge-plugin-message/controller/MessageManageController.java - 管理员消息管理控制器forge-plugin-message/service/MessageManageService.java - 管理员消息服务接口forge-plugin-message/service/impl/MessageManageServiceImpl.java - 管理员消息服务实现forge-plugin-message/domain/dto/MessageManageQueryDTO.java - 查询DTOforge-plugin-message/domain/vo/MessageManageVO.java - 消息列表VOforge-plugin-message/domain/vo/MessageDetailVO.java - 消息详情VOforge-plugin-message/domain/vo/ReceiverVO.java - 接收人VOFiles:
forge-plugin-message/domain/dto/MessageManageQueryDTO.javaforge-plugin-message/domain/vo/MessageManageVO.javaforge-plugin-message/domain/vo/MessageDetailVO.javaCreate: forge-plugin-message/domain/vo/ReceiverVO.java
[ ] Step 1: 创建 MessageManageQueryDTO
创建文件 forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/domain/dto/MessageManageQueryDTO.java:
package com.mdframe.forge.plugin.message.domain.dto;
import lombok.Data;
@Data
public class MessageManageQueryDTO {
private String type;
private String channel;
private Integer status;
private String startTime;
private String endTime;
private String keyword;
}
创建文件 forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/domain/vo/MessageManageVO.java:
package com.mdframe.forge.plugin.message.domain.vo;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class MessageManageVO {
private Long id;
private String title;
private String type;
private String channel;
private Integer status;
private Integer receiverCount;
private Integer readCount;
private Integer unreadCount;
private LocalDateTime createTime;
private String senderName;
}
创建文件 forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/domain/vo/ReceiverVO.java:
package com.mdframe.forge.plugin.message.domain.vo;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ReceiverVO {
private Long userId;
private String userName;
private String orgName;
private Integer readFlag;
private LocalDateTime readTime;
}
创建文件 forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/domain/vo/MessageDetailVO.java:
package com.mdframe.forge.plugin.message.domain.vo;
import com.mdframe.forge.plugin.message.domain.entity.SysMessage;
import com.mdframe.forge.plugin.message.domain.entity.SysMessageSendRecord;
import lombok.Data;
import java.util.List;
@Data
public class MessageDetailVO {
private SysMessage message;
private SysMessageSendRecord sendRecord;
private List<ReceiverVO> receivers;
}
git add forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/domain/dto/MessageManageQueryDTO.java
git add forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/domain/vo/MessageManageVO.java
git add forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/domain/vo/ReceiverVO.java
git add forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/domain/vo/MessageDetailVO.java
git commit -m "feat: add message manage DTOs and VOs"
Files:
forge-plugin-message/service/MessageManageService.javaCreate: forge-plugin-message/service/impl/MessageManageServiceImpl.java
[ ] Step 1: 创建 MessageManageService 接口
创建文件 forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/service/MessageManageService.java:
package com.mdframe.forge.plugin.message.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.mdframe.forge.plugin.message.domain.dto.MessageManageQueryDTO;
import com.mdframe.forge.plugin.message.domain.vo.MessageDetailVO;
import com.mdframe.forge.plugin.message.domain.vo.MessageManageVO;
public interface MessageManageService {
IPage<MessageManageVO> pageMessages(MessageManageQueryDTO query, Integer pageNum, Integer pageSize);
MessageDetailVO getMessageDetail(Long messageId);
}
创建文件 forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/service/impl/MessageManageServiceImpl.java:
package com.mdframe.forge.plugin.message.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.mdframe.forge.plugin.message.domain.dto.MessageManageQueryDTO;
import com.mdframe.forge.plugin.message.domain.entity.SysMessage;
import com.mdframe.forge.plugin.message.domain.entity.SysMessageReceiver;
import com.mdframe.forge.plugin.message.domain.entity.SysMessageSendRecord;
import com.mdframe.forge.plugin.message.domain.vo.MessageDetailVO;
import com.mdframe.forge.plugin.message.domain.vo.MessageManageVO;
import com.mdframe.forge.plugin.message.domain.vo.ReceiverVO;
import com.mdframe.forge.plugin.message.mapper.SysMessageMapper;
import com.mdframe.forge.plugin.message.mapper.SysMessageReceiverMapper;
import com.mdframe.forge.plugin.message.mapper.SysMessageSendRecordMapper;
import com.mdframe.forge.plugin.message.service.MessageManageService;
import com.mdframe.forge.plugin.system.entity.SysOrg;
import com.mdframe.forge.plugin.system.entity.SysUser;
import com.mdframe.forge.plugin.system.service.ISysOrgService;
import com.mdframe.forge.plugin.system.service.ISysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Service
public class MessageManageServiceImpl implements MessageManageService {
private final SysMessageMapper messageMapper;
private final SysMessageReceiverMapper receiverMapper;
private final SysMessageSendRecordMapper sendRecordMapper;
private final ISysUserService userService;
private final ISysOrgService orgService;
public MessageManageServiceImpl(
SysMessageMapper messageMapper,
SysMessageReceiverMapper receiverMapper,
SysMessageSendRecordMapper sendRecordMapper,
ISysUserService userService,
ISysOrgService orgService) {
this.messageMapper = messageMapper;
this.receiverMapper = receiverMapper;
this.sendRecordMapper = sendRecordMapper;
this.userService = userService;
this.orgService = orgService;
}
@Override
public IPage<MessageManageVO> pageMessages(MessageManageQueryDTO query, Integer pageNum, Integer pageSize) {
Page<SysMessage> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<SysMessage> wrapper = new LambdaQueryWrapper<>();
if (StrUtil.isNotBlank(query.getType())) {
wrapper.eq(SysMessage::getType, query.getType());
}
if (StrUtil.isNotBlank(query.getChannel())) {
wrapper.eq(SysMessage::getSendChannel, query.getChannel());
}
if (query.getStatus() != null) {
wrapper.eq(SysMessage::getStatus, query.getStatus());
}
if (StrUtil.isNotBlank(query.getStartTime())) {
LocalDateTime startTime = LocalDateTime.parse(query.getStartTime(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
wrapper.ge(SysMessage::getCreateTime, startTime);
}
if (StrUtil.isNotBlank(query.getEndTime())) {
LocalDateTime endTime = LocalDateTime.parse(query.getEndTime(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
wrapper.le(SysMessage::getCreateTime, endTime);
}
if (StrUtil.isNotBlank(query.getKeyword())) {
wrapper.and(w -> w.like(SysMessage::getTitle, query.getKeyword())
.or()
.like(SysMessage::getContent, query.getKeyword()));
}
wrapper.orderByDesc(SysMessage::getCreateTime);
Page<SysMessage> messagePage = messageMapper.selectPage(page, wrapper);
List<MessageManageVO> vos = messagePage.getRecords().stream().map(msg -> {
MessageManageVO vo = new MessageManageVO();
vo.setId(msg.getId());
vo.setTitle(msg.getTitle());
vo.setType(msg.getType());
vo.setChannel(msg.getSendChannel());
vo.setStatus(msg.getStatus());
vo.setCreateTime(msg.getCreateTime());
vo.setSenderName(msg.getSenderName());
List<SysMessageReceiver> receivers = receiverMapper.selectList(
new LambdaQueryWrapper<SysMessageReceiver>()
.eq(SysMessageReceiver::getMessageId, msg.getId())
);
vo.setReceiverCount(receivers.size());
vo.setReadCount((int) receivers.stream().filter(r -> r.getReadFlag() == 1).count());
vo.setUnreadCount((int) receivers.stream().filter(r -> r.getReadFlag() == 0).count());
return vo;
}).collect(Collectors.toList());
Page<MessageManageVO> result = new Page<>(pageNum, pageSize);
result.setRecords(vos);
result.setTotal(messagePage.getTotal());
return result;
}
@Override
public MessageDetailVO getMessageDetail(Long messageId) {
SysMessage message = messageMapper.selectById(messageId);
if (message == null) {
return null;
}
SysMessageSendRecord sendRecord = sendRecordMapper.selectOne(
new LambdaQueryWrapper<SysMessageSendRecord>()
.eq(SysMessageSendRecord::getMessageId, messageId)
);
List<SysMessageReceiver> receivers = receiverMapper.selectList(
new LambdaQueryWrapper<SysMessageReceiver>()
.eq(SysMessageReceiver::getMessageId, messageId)
);
List<Long> userIds = receivers.stream()
.map(SysMessageReceiver::getUserId)
.distinct()
.collect(Collectors.toList());
Map<Long, String> userNameMap = Map.of();
Map<Long, String> orgNameMap = Map.of();
if (!userIds.isEmpty()) {
List<SysUser> users = userService.lambdaQuery()
.in(SysUser::getId, userIds)
.list();
userNameMap = users.stream()
.collect(Collectors.toMap(SysUser::getId, SysUser::getUserName));
List<Long> orgIds = users.stream()
.map(SysUser::getOrgId)
.filter(id -> id != null)
.distinct()
.collect(Collectors.toList());
if (!orgIds.isEmpty()) {
List<SysOrg> orgs = orgService.lambdaQuery()
.in(SysOrg::getId, orgIds)
.list();
orgNameMap = orgs.stream()
.collect(Collectors.toMap(SysOrg::getId, SysOrg::getOrgName));
}
}
List<ReceiverVO> receiverVos = receivers.stream().map(r -> {
ReceiverVO vo = new ReceiverVO();
vo.setUserId(r.getUserId());
vo.setUserName(userNameMap.getOrDefault(r.getUserId(), ""));
vo.setOrgName(orgNameMap.getOrDefault(r.getOrgId(), ""));
vo.setReadFlag(r.getReadFlag());
vo.setReadTime(r.getReadTime());
return vo;
}).collect(Collectors.toList());
MessageDetailVO detailVO = new MessageDetailVO();
detailVO.setMessage(message);
detailVO.setSendRecord(sendRecord);
detailVO.setReceivers(receiverVos);
return detailVO;
}
}
git add forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/service/MessageManageService.java
git add forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/service/impl/MessageManageServiceImpl.java
git commit -m "feat: add message manage service"
Files:
Create: forge-plugin-message/controller/MessageManageController.java
[ ] Step 1: 创建 MessageManageController
创建文件 forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/controller/MessageManageController.java:
package com.mdframe.forge.plugin.message.controller;
import com.mdframe.forge.plugin.message.domain.dto.MessageManageQueryDTO;
import com.mdframe.forge.plugin.message.service.MessageManageService;
import com.mdframe.forge.starter.core.domain.RespInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/api/message/manage")
public class MessageManageController {
private final MessageManageService messageManageService;
public MessageManageController(MessageManageService messageManageService) {
this.messageManageService = messageManageService;
}
@PostMapping("/page")
public RespInfo<?> page(
@RequestBody MessageManageQueryDTO query,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
return RespInfo.success(messageManageService.pageMessages(query, pageNum, pageSize));
}
@GetMapping("/{messageId}/detail")
public RespInfo<?> getDetail(@PathVariable Long messageId) {
return RespInfo.success(messageManageService.getMessageDetail(messageId));
}
}
git add forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/controller/MessageManageController.java
git commit -m "feat: add message manage controller"
Files:
Modify: forge-admin-ui/src/api/message.js:47-85
[ ] Step 1: 在 message.js 中新增管理接口方法
修改文件 forge-admin-ui/src/api/message.js,在文件末尾添加:
// ========== 消息管理(管理员) ==========
/**
* 分页查询所有消息列表(管理员)
*/
getMessageManagePage: (query, pageNum, pageSize) =>
request.post(`/api/message/manage/page?pageNum=${pageNum}&pageSize=${pageSize}`, query),
/**
* 查询消息详细信息(包含发送记录和接收人)
*/
getMessageManageDetail: (messageId) =>
request.get(`/api/message/manage/${messageId}/detail`)
git add forge-admin-ui/src/api/message.js
git commit -m "feat: add message manage api methods"
Files:
Create: forge-admin-ui/src/views/message/manage.vue
[ ] Step 1: 创建消息管理页面
创建文件 forge-admin-ui/src/views/message/manage.vue:
<template>
<div class="p-16">
<div class="bg-white rounded p-16">
<!-- 消息发送测试面板 -->
<n-card title="消息发送测试" class="mb-16">
<n-form ref="sendFormRef" :model="sendForm" label-width="100">
<n-form-item label="发送渠道" path="channel">
<n-select
v-model:value="sendForm.channel"
placeholder="选择发送渠道"
:options="channelOptions"
/>
</n-form-item>
<n-form-item label="消息标题" path="title">
<n-input
v-model:value="sendForm.title"
placeholder="请输入消息标题"
/>
</n-form-item>
<n-form-item label="消息内容" path="content">
<n-input
v-model:value="sendForm.content"
type="textarea"
placeholder="请输入消息内容"
:rows="4"
/>
</n-form-item>
<n-form-item label="发送范围" path="sendScope">
<n-select
v-model:value="sendForm.sendScope"
placeholder="选择发送范围"
:options="scopeOptions"
/>
</n-form-item>
<n-form-item v-if="sendForm.sendScope === 'USERS'" label="指定用户" path="userIds">
<n-select
v-model:value="sendForm.userIds"
multiple
placeholder="选择接收用户"
:options="userOptions"
filterable
/>
</n-form-item>
<n-form-item label="消息类型" path="type">
<n-select
v-model:value="sendForm.type"
placeholder="选择消息类型"
:options="typeOptions"
/>
</n-form-item>
<n-space>
<n-button type="primary" @click="handleSend" :loading="sending">
发送测试消息
</n-button>
<n-button @click="handleResetSendForm">
重置
</n-button>
</n-space>
</n-form>
</n-card>
<!-- 消息监控列表 -->
<n-card title="消息监控">
<!-- 搜索栏 -->
<n-space class="mb-16" :vertical="false">
<n-select
v-model:value="queryParams.type"
placeholder="消息类型"
clearable
style="width: 150px"
:options="typeOptions"
/>
<n-select
v-model:value="queryParams.channel"
placeholder="发送渠道"
clearable
style="width: 150px"
:options="channelOptions"
/>
<n-select
v-model:value="queryParams.status"
placeholder="发送状态"
clearable
style="width: 150px"
:options="statusOptions"
/>
<n-date-picker
v-model:value="queryParams.timeRange"
type="daterange"
clearable
placeholder="选择时间范围"
/>
<n-input
v-model:value="queryParams.keyword"
placeholder="搜索标题或内容"
clearable
style="width: 250px"
>
<template #prefix>
<i class="i-material-symbols:search" />
</template>
</n-input>
<n-button type="primary" @click="handleSearch">
<template #icon>
<i class="i-material-symbols:search" />
</template>
搜索
</n-button>
<n-button @click="handleReset">
<template #icon>
<i class="i-material-symbols:refresh" />
</template>
重置
</n-button>
</n-space>
<!-- 消息列表 -->
<n-data-table
:columns="columns"
:data="dataSource"
:loading="loading"
:pagination="pagination"
:row-key="row => row.id"
/>
</n-card>
</div>
<!-- 消息详情弹窗 -->
<n-modal v-model:show="showDetail" preset="card" title="消息详情" style="width: 800px">
<n-descriptions label-placement="left" :column="2" bordered>
<n-descriptions-item label="消息标题">
{{ currentDetail?.message?.title }}
</n-descriptions-item>
<n-descriptions-item label="消息类型">
<n-tag :type="getTypeColor(currentDetail?.message?.type)" size="small">
{{ getTypeText(currentDetail?.message?.type) }}
</n-tag>
</n-descriptions-item>
<n-descriptions-item label="发送渠道">
<n-tag :type="getChannelColor(currentDetail?.message?.sendChannel)" size="small">
{{ getChannelText(currentDetail?.message?.sendChannel) }}
</n-tag>
</n-descriptions-item>
<n-descriptions-item label="发送时间">
{{ currentDetail?.message?.createTime }}
</n-descriptions-item>
<n-descriptions-item label="发送状态">
<n-tag :type="currentDetail?.sendRecord?.status === 1 ? 'success' : 'error'" size="small">
{{ currentDetail?.sendRecord?.status === 1 ? '成功' : '失败' }}
</n-tag>
</n-descriptions-item>
<n-descriptions-item label="接收人数">
{{ currentDetail?.sendRecord?.receiverCount }}
</n-descriptions-item>
<n-descriptions-item label="成功数">
{{ currentDetail?.sendRecord?.successCount }}
</n-descriptions-item>
<n-descriptions-item label="失败数">
{{ currentDetail?.sendRecord?.failCount }}
</n-descriptions-item>
<n-descriptions-item v-if="currentDetail?.sendRecord?.errorMsg" label="错误信息" :span="2">
<n-text type="error">{{ currentDetail?.sendRecord?.errorMsg }}</n-text>
</n-descriptions-item>
</n-descriptions>
<n-divider>消息内容</n-divider>
<div class="message-content" v-html="currentDetail?.message?.content"></div>
<n-divider>接收人列表</n-divider>
<n-data-table
:columns="receiverColumns"
:data="currentDetail?.receivers || []"
:max-height="300"
/>
</n-modal>
</div>
</template>
<script setup>
import { ref, h, onMounted } from 'vue'
import { NTag, NButton } from 'naive-ui'
import messageApi from '@/api/message'
import { request } from '@/utils'
defineOptions({ name: 'MessageManage' })
const loading = ref(false)
const sending = ref(false)
const dataSource = ref([])
const showDetail = ref(false)
const currentDetail = ref(null)
const userOptions = ref([])
const sendForm = ref({
channel: 'WEB',
title: '',
content: '',
sendScope: 'USERS',
userIds: [],
type: 'SYSTEM'
})
const queryParams = ref({
type: null,
channel: null,
status: null,
timeRange: null,
keyword: null
})
const pagination = ref({
page: 1,
pageSize: 10,
itemCount: 0,
showSizePicker: true,
pageSizes: [10, 20, 50, 100],
onChange: (page) => {
pagination.value.page = page
loadData()
},
onUpdatePageSize: (pageSize) => {
pagination.value.pageSize = pageSize
pagination.value.page = 1
loadData()
}
})
const channelOptions = [
{ label: '站内信', value: 'WEB' },
{ label: '短信', value: 'SMS' },
{ label: '邮件', value: 'EMAIL' }
]
const scopeOptions = [
{ label: '指定人员', value: 'USERS' },
{ label: '指定组织', value: 'ORG' },
{ label: '全员', value: 'ALL' }
]
const typeOptions = [
{ label: '系统消息', value: 'SYSTEM' },
{ label: '自定义', value: 'CUSTOM' }
]
const statusOptions = [
{ label: '发送成功', value: 1 },
{ label: '发送失败', value: 2 }
]
const columns = [
{
title: '消息标题',
key: 'title',
ellipsis: { tooltip: true }
},
{
title: '消息类型',
key: 'type',
width: 100,
render: (row) => {
return h(NTag, {
type: getTypeColor(row.type),
size: 'small'
}, { default: () => getTypeText(row.type) })
}
},
{
title: '发送渠道',
key: 'channel',
width: 100,
render: (row) => {
return h(NTag, {
type: getChannelColor(row.channel),
size: 'small'
}, { default: () => getChannelText(row.channel) })
}
},
{
title: '发送状态',
key: 'status',
width: 100,
render: (row) => {
return h(NTag, {
type: row.status === 1 ? 'success' : 'error',
size: 'small'
}, { default: () => row.status === 1 ? '成功' : '失败' })
}
},
{
title: '接收人数',
key: 'receiverCount',
width: 100
},
{
title: '已读/未读',
key: 'readStatus',
width: 120,
render: (row) => {
return `${row.readCount}/${row.unreadCount}`
}
},
{
title: '发送时间',
key: 'createTime',
width: 180
},
{
title: '操作',
key: 'action',
width: 100,
render: (row) => {
return h(NButton, {
size: 'small',
type: 'primary',
text: true,
onClick: () => handleViewDetail(row.id)
}, { default: () => '查看详情' })
}
}
]
const receiverColumns = [
{
title: '用户名',
key: 'userName'
},
{
title: '组织',
key: 'orgName'
},
{
title: '阅读状态',
key: 'readFlag',
width: 100,
render: (row) => {
return h(NTag, {
type: row.readFlag === 1 ? 'success' : 'error',
size: 'small'
}, { default: () => row.readFlag === 1 ? '已读' : '未读' })
}
},
{
title: '阅读时间',
key: 'readTime',
width: 180
}
]
async function loadUsers() {
try {
const res = await request.get('/api/user/list')
if (res.code === 200 && res.data) {
userOptions.value = res.data.map(user => ({
label: user.userName,
value: user.id
}))
}
} catch (error) {
console.error('加载用户列表失败:', error)
}
}
async function loadData() {
try {
loading.value = true
const params = {
type: queryParams.value.type,
channel: queryParams.value.channel,
status: queryParams.value.status,
keyword: queryParams.value.keyword
}
if (queryParams.value.timeRange && queryParams.value.timeRange.length === 2) {
const [start, end] = queryParams.value.timeRange
params.startTime = new Date(start).toISOString().slice(0, 19).replace('T', ' ')
params.endTime = new Date(end).toISOString().slice(0, 19).replace('T', ' ')
}
const res = await messageApi.getMessageManagePage(
params,
pagination.value.page,
pagination.value.pageSize
)
if (res.code === 200 && res.data) {
dataSource.value = res.data.records || []
pagination.value.itemCount = res.data.total || 0
}
} catch (error) {
console.error('加载消息列表失败:', error)
window.$message.error('加载数据失败')
} finally {
loading.value = false
}
}
async function handleSend() {
if (!sendForm.value.title || !sendForm.value.content) {
window.$message.warning('请填写消息标题和内容')
return
}
if (sendForm.value.sendScope === 'USERS' && sendForm.value.userIds.length === 0) {
window.$message.warning('请选择接收用户')
return
}
try {
sending.value = true
const res = await messageApi.sendMessage(sendForm.value)
if (res.code === 200) {
window.$message.success('消息发送成功')
handleResetSendForm()
loadData()
} else {
window.$message.error(res.msg || '发送失败')
}
} catch (error) {
console.error('发送消息失败:', error)
window.$message.error('发送失败')
} finally {
sending.value = false
}
}
function handleResetSendForm() {
sendForm.value = {
channel: 'WEB',
title: '',
content: '',
sendScope: 'USERS',
userIds: [],
type: 'SYSTEM'
}
}
function handleSearch() {
pagination.value.page = 1
loadData()
}
function handleReset() {
queryParams.value = {
type: null,
channel: null,
status: null,
timeRange: null,
keyword: null
}
pagination.value.page = 1
loadData()
}
async function handleViewDetail(messageId) {
try {
const res = await messageApi.getMessageManageDetail(messageId)
if (res.code === 200) {
currentDetail.value = res.data
showDetail.value = true
}
} catch (error) {
console.error('获取消息详情失败:', error)
window.$message.error('获取详情失败')
}
}
function getTypeText(type) {
const map = {
'SYSTEM': '系统消息',
'SMS': '短信',
'EMAIL': '邮件',
'CUSTOM': '自定义'
}
return map[type] || type
}
function getTypeColor(type) {
const map = {
'SYSTEM': 'info',
'SMS': 'warning',
'EMAIL': 'success',
'CUSTOM': 'default'
}
return map[type] || 'default'
}
function getChannelText(channel) {
const map = {
'WEB': '站内信',
'SMS': '短信',
'EMAIL': '邮件',
'PUSH': '推送'
}
return map[channel] || channel
}
function getChannelColor(channel) {
const map = {
'WEB': 'default',
'SMS': 'warning',
'EMAIL': 'success',
'PUSH': 'info'
}
return map[channel] || 'default'
}
onMounted(() => {
loadUsers()
loadData()
})
</script>
<style scoped>
.message-content {
line-height: 1.8;
color: #333;
padding: 16px;
}
</style>
git add forge-admin-ui/src/views/message/manage.vue
git commit -m "feat: add message manage page"
Files:
Modify: forge-admin-ui/src/router/index.js 或路由配置文件
[ ] Step 1: 检查路由配置文件
使用 glob 查找路由配置文件:
glob pattern: forge-admin-ui/src/router/**/*.js
根据实际路由配置文件结构,添加类似以下配置:
{
path: '/message',
name: 'Message',
component: () => import('@/views/message/manage.vue'),
meta: {
title: '消息管理',
icon: 'message',
requiresAuth: true
}
}
git add forge-admin-ui/src/router/相关文件.js
git commit -m "feat: add message manage route"
cd forge/forge-admin
mvn spring-boot:run
Expected: 服务启动成功,无报错
cd forge-admin-ui
npm run dev
Expected: 前端服务启动成功,可以访问页面
Expected: 显示"消息发送成功"提示
Expected: 显示消息详情弹窗,包含发送记录和接收人信息
Expected: 消息列表正确筛选,显示符合条件的消息
Expected: 如果配置正确,显示发送成功;如果配置错误,显示失败和错误信息
设计文档覆盖的功能:
检查是否有占位符或模糊描述:
检查类型和方法签名的一致性:
完成所有任务后,应该达到以下成功标准: