bug:修复消息不能撤回
This commit is contained in:
parent
7d1896d9d2
commit
e83568f5fb
|
|
@ -10,7 +10,27 @@ export function sessionDetailApi(id) {
|
|||
return request({ url: '/admin/session/detail/' + id, method: 'get' })
|
||||
}
|
||||
|
||||
// 会话消息列表
|
||||
export function sessionMessagesApi(conversationId, params) {
|
||||
return request({ url: '/admin/session/' + conversationId + '/messages', method: 'get', params })
|
||||
}
|
||||
|
||||
// 删除会话
|
||||
export function sessionDeleteApi(id) {
|
||||
return request({ url: '/admin/session/delete/' + id, method: 'post' })
|
||||
}
|
||||
|
||||
// 删除消息
|
||||
export function sessionMessageDeleteApi(messageId) {
|
||||
return request({ url: '/admin/session/message/delete/' + messageId, method: 'post' })
|
||||
}
|
||||
|
||||
// 撤回消息
|
||||
export function sessionMessageRecallApi(messageId) {
|
||||
return request({ url: '/admin/session/message/recall/' + messageId, method: 'post' })
|
||||
}
|
||||
|
||||
// 会话统计
|
||||
export function sessionStatisticsApi() {
|
||||
return request({ url: '/admin/session/statistics', method: 'get' })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,164 +1,602 @@
|
|||
<template>
|
||||
<div class="divBox">
|
||||
<el-card shadow="never" class="ivu-mt">
|
||||
<div class="divBox relative">
|
||||
<el-card :bordered="false" shadow="never" class="ivu-mt" :body-style="{ padding: 0 }">
|
||||
<div class="padding-add">
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :inline="true" :model="searchForm" size="small" class="mb20">
|
||||
<el-form-item label="发送方昵称">
|
||||
<el-input v-model="searchForm.senderNickname" placeholder="请输入发送方昵称" clearable class="selWidth" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发送方电话">
|
||||
<el-input v-model="searchForm.senderPhone" placeholder="请输入发送方电话" clearable class="selWidth" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分类时间">
|
||||
<el-date-picker
|
||||
v-model="searchForm.startTime"
|
||||
type="datetime"
|
||||
placeholder="开始时间"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
class="selWidth"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="起止时间">
|
||||
<el-date-picker
|
||||
v-model="searchForm.endTime"
|
||||
type="datetime"
|
||||
placeholder="结束时间"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
class="selWidth"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
</el-form-item>
|
||||
<el-form inline size="small" :model="queryForm" ref="queryForm" label-width="90px">
|
||||
<div class="acea-row search-form row-between">
|
||||
<div class="search-form-box">
|
||||
<el-form-item label="发送方昵称:">
|
||||
<el-input v-model="queryForm.senderNickname" placeholder="请输入发送方昵称" clearable class="selWidth" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发送方电话:">
|
||||
<el-input v-model="queryForm.senderPhone" placeholder="请输入发送方电话" clearable class="selWidth" />
|
||||
</el-form-item>
|
||||
<el-form-item label="时间选择:">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
align="right"
|
||||
unlink-panels
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
format="yyyy-MM-dd"
|
||||
size="small"
|
||||
type="daterange"
|
||||
placeholder="选择时间范围"
|
||||
class="selWidth"
|
||||
@change="onDateChange"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item class="search-form-sub">
|
||||
<el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset" size="small">重置</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table v-loading="loading" :data="tableData" border size="small">
|
||||
<el-table-column prop="id" label="id" width="80" align="center" />
|
||||
<el-table-column label="发送者头像" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-image
|
||||
v-if="scope.row.sender_avatar"
|
||||
:src="scope.row.sender_avatar"
|
||||
:preview-src-list="[scope.row.sender_avatar]"
|
||||
style="width: 50px; height: 50px; cursor: pointer"
|
||||
fit="cover"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sender_nickname" label="发送者昵称" width="120" align="center" />
|
||||
<el-table-column prop="sender_phone" label="发送者电话" width="130" align="center" />
|
||||
<el-table-column label="对方头像" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-image
|
||||
v-if="scope.row.receiver_avatar"
|
||||
:src="scope.row.receiver_avatar"
|
||||
:preview-src-list="[scope.row.receiver_avatar]"
|
||||
style="width: 50px; height: 50px; cursor: pointer"
|
||||
fit="cover"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="receiver_nickname" label="对方昵称" width="120" align="center" />
|
||||
<el-table-column prop="receiver_phone" label="对方昵称电话" width="130" align="center" />
|
||||
<el-table-column prop="create_time" label="创建时间" width="160" align="center" />
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="small" @click="handleDelete(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="acea-row row-center page mt20">
|
||||
<span class="mr10">共 {{ total }} 条</span>
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size="limit"
|
||||
:total="total"
|
||||
layout="prev, pager, next, sizes, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card mt14">
|
||||
<div slot="header" class="clearfix">
|
||||
<span class="card-title">会话管理</span>
|
||||
<div class="header-right">
|
||||
<el-tag type="info">总会话数:{{ statistics.totalConversations || 0 }}</el-tag>
|
||||
<el-tag type="success" class="ml10">总消息数:{{ statistics.totalMessages || 0 }}</el-tag>
|
||||
<el-tag type="warning" class="ml10">今日消息:{{ statistics.todayMessages || 0 }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
size="mini"
|
||||
highlight-current-row
|
||||
>
|
||||
<el-table-column type="expand">
|
||||
<template slot-scope="props">
|
||||
<div class="message-preview" v-loading="props.row.messagesLoading">
|
||||
<div class="message-header">
|
||||
<span>最近消息记录</span>
|
||||
<el-button type="text" size="mini" @click="loadMessages(props.row)">刷新</el-button>
|
||||
</div>
|
||||
<div class="message-list" v-if="props.row.messages && props.row.messages.length">
|
||||
<div
|
||||
class="message-item"
|
||||
v-for="msg in props.row.messages"
|
||||
:key="msg.id"
|
||||
:class="{ 'message-right': msg.senderId === props.row.user1_id }"
|
||||
>
|
||||
<div class="message-sender">{{ msg.senderName || '用户' + msg.senderId }}</div>
|
||||
<div class="message-content">{{ msg.content }}</div>
|
||||
<div class="message-time">{{ msg.createTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="暂无消息记录" :image-size="60"></el-empty>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="id" label="会话ID" width="80" />
|
||||
<el-table-column label="发送者" min-width="150">
|
||||
<template slot-scope="scope">
|
||||
<div class="user-info">
|
||||
<el-avatar :size="32" :src="scope.row.sender_avatar" icon="el-icon-user"></el-avatar>
|
||||
<div class="user-detail">
|
||||
<span class="user-name">{{ scope.row.sender_nickname || '用户' + scope.row.user1_id }}</span>
|
||||
<span class="user-id">{{ scope.row.sender_phone || 'ID: ' + scope.row.user1_id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="对方" min-width="150">
|
||||
<template slot-scope="scope">
|
||||
<div class="user-info">
|
||||
<el-avatar :size="32" :src="scope.row.receiver_avatar" icon="el-icon-user"></el-avatar>
|
||||
<div class="user-detail">
|
||||
<span class="user-name">{{ scope.row.receiver_nickname || '用户' + scope.row.user2_id }}</span>
|
||||
<span class="user-id">{{ scope.row.receiver_phone || 'ID: ' + scope.row.user2_id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="last_message" label="最后消息" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="last_message_time" label="最后消息时间" width="160" />
|
||||
<el-table-column label="消息数" width="80">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini">{{ scope.row.message_count || 0 }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" label="创建时间" width="160" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="viewMessages(scope.row)">查看</el-button>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<el-popconfirm title="确定删除该会话及所有消息吗?" @confirm="deleteConversation(scope.row)">
|
||||
<el-button slot="reference" type="text" size="mini" class="text-danger">删除</el-button>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="block">
|
||||
<el-pagination
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size="queryForm.limit"
|
||||
:current-page="queryForm.page"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
background
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 消息详情对话框 -->
|
||||
<el-dialog
|
||||
:title="'会话详情 - ' + (currentConversation ? currentConversation.id : '')"
|
||||
:visible.sync="dialogVisible"
|
||||
width="700px"
|
||||
top="5vh"
|
||||
>
|
||||
<div class="dialog-users" v-if="currentConversation">
|
||||
<div class="dialog-user">
|
||||
<el-avatar :size="48" :src="currentConversation.sender_avatar" icon="el-icon-user"></el-avatar>
|
||||
<div>
|
||||
<div class="user-name">{{ currentConversation.sender_nickname || '用户' + currentConversation.user1_id }}</div>
|
||||
<div class="user-id">{{ currentConversation.sender_phone || 'ID: ' + currentConversation.user1_id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-arrow">
|
||||
<i class="el-icon-sort"></i>
|
||||
</div>
|
||||
<div class="dialog-user">
|
||||
<el-avatar :size="48" :src="currentConversation.receiver_avatar" icon="el-icon-user"></el-avatar>
|
||||
<div>
|
||||
<div class="user-name">{{ currentConversation.receiver_nickname || '用户' + currentConversation.user2_id }}</div>
|
||||
<div class="user-id">{{ currentConversation.receiver_phone || 'ID: ' + currentConversation.user2_id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-messages" v-loading="messagesLoading">
|
||||
<div
|
||||
class="dialog-message"
|
||||
v-for="msg in currentMessages"
|
||||
:key="msg.id"
|
||||
:class="{ 'message-from-user1': currentConversation && msg.senderId === currentConversation.user1_id }"
|
||||
>
|
||||
<div class="msg-avatar">
|
||||
<el-avatar
|
||||
:size="36"
|
||||
:src="getMessageAvatar(msg)"
|
||||
icon="el-icon-user"
|
||||
></el-avatar>
|
||||
</div>
|
||||
<div class="msg-body">
|
||||
<div class="msg-header">
|
||||
<span class="msg-sender">{{ msg.senderName || '用户' + msg.senderId }}</span>
|
||||
<el-tag v-if="msg.isRecalled" type="warning" size="mini" class="recalled-tag">已撤回</el-tag>
|
||||
<span class="msg-time">{{ msg.createTime }}</span>
|
||||
<el-popconfirm
|
||||
v-if="!msg.isRecalled"
|
||||
title="确定要撤回这条消息吗?"
|
||||
@confirm="recallMessage(msg)"
|
||||
class="msg-recall"
|
||||
>
|
||||
<el-button slot="reference" type="text" size="mini" class="recall-btn">撤回</el-button>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
<!-- 如果消息已撤回,显示原始内容 -->
|
||||
<div v-if="msg.isRecalled && msg.originalContent" class="msg-content recalled-content">
|
||||
<div class="original-label">原始内容:</div>
|
||||
<div class="original-text">{{ msg.originalContent }}</div>
|
||||
</div>
|
||||
<div v-else class="msg-content">{{ msg.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="!messagesLoading && currentMessages.length === 0" description="暂无消息记录"></el-empty>
|
||||
</div>
|
||||
|
||||
<div class="dialog-pagination" v-if="messageTotal > messagePageSize">
|
||||
<el-pagination
|
||||
small
|
||||
layout="prev, pager, next"
|
||||
:total="messageTotal"
|
||||
:page-size="messagePageSize"
|
||||
:current-page="messagePage"
|
||||
@current-change="handleMessagePageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { sessionListApi, sessionDeleteApi } from '@/api/session';
|
||||
import { sessionListApi, sessionMessagesApi, sessionDeleteApi, sessionStatisticsApi, sessionMessageRecallApi } from '@/api/session';
|
||||
|
||||
export default {
|
||||
name: 'SessionList',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
searchForm: {
|
||||
tableData: [],
|
||||
total: 0,
|
||||
queryForm: {
|
||||
senderNickname: '',
|
||||
senderPhone: '',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
page: 1,
|
||||
limit: 10,
|
||||
},
|
||||
tableData: [],
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
dateRange: [],
|
||||
statistics: {
|
||||
totalConversations: 0,
|
||||
totalMessages: 0,
|
||||
todayMessages: 0,
|
||||
},
|
||||
dialogVisible: false,
|
||||
currentConversation: null,
|
||||
currentMessages: [],
|
||||
messagesLoading: false,
|
||||
messagePage: 1,
|
||||
messagePageSize: 20,
|
||||
messageTotal: 0,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getList();
|
||||
this.getStatistics();
|
||||
},
|
||||
methods: {
|
||||
getList() {
|
||||
async getList() {
|
||||
this.loading = true;
|
||||
sessionListApi({
|
||||
...this.searchForm,
|
||||
page: this.page,
|
||||
limit: this.limit
|
||||
})
|
||||
.then((res) => {
|
||||
this.tableData = res.data.list || [];
|
||||
this.total = res.data.total || 0;
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
try {
|
||||
const res = await sessionListApi(this.queryForm);
|
||||
this.tableData = (res.list || []).map(item => ({
|
||||
...item,
|
||||
messages: [],
|
||||
messagesLoading: false,
|
||||
}));
|
||||
this.total = res.total || 0;
|
||||
} catch (error) {
|
||||
console.error('获取会话列表失败:', error);
|
||||
this.$message.error('获取会话列表失败');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async getStatistics() {
|
||||
try {
|
||||
const res = await sessionStatisticsApi();
|
||||
this.statistics = res || {};
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error);
|
||||
}
|
||||
},
|
||||
async loadMessages(row) {
|
||||
row.messagesLoading = true;
|
||||
try {
|
||||
const res = await sessionMessagesApi(row.id, { page: 1, pageSize: 5 });
|
||||
row.messages = res.list || [];
|
||||
} catch (error) {
|
||||
console.error('加载消息失败:', error);
|
||||
} finally {
|
||||
row.messagesLoading = false;
|
||||
}
|
||||
},
|
||||
async viewMessages(row) {
|
||||
this.currentConversation = row;
|
||||
this.dialogVisible = true;
|
||||
this.messagePage = 1;
|
||||
await this.loadDialogMessages();
|
||||
},
|
||||
async loadDialogMessages() {
|
||||
if (!this.currentConversation) return;
|
||||
this.messagesLoading = true;
|
||||
try {
|
||||
const res = await sessionMessagesApi(this.currentConversation.id, {
|
||||
page: this.messagePage,
|
||||
pageSize: this.messagePageSize,
|
||||
});
|
||||
this.currentMessages = res.list || [];
|
||||
this.messageTotal = res.total || 0;
|
||||
} catch (error) {
|
||||
console.error('加载消息失败:', error);
|
||||
this.$message.error('加载消息失败');
|
||||
} finally {
|
||||
this.messagesLoading = false;
|
||||
}
|
||||
},
|
||||
async deleteConversation(row) {
|
||||
try {
|
||||
await sessionDeleteApi(row.id);
|
||||
this.$message.success('删除成功');
|
||||
this.getList();
|
||||
this.getStatistics();
|
||||
} catch (error) {
|
||||
console.error('删除会话失败:', error);
|
||||
this.$message.error('删除失败');
|
||||
}
|
||||
},
|
||||
async recallMessage(msg) {
|
||||
try {
|
||||
await sessionMessageRecallApi(msg.id);
|
||||
this.$message.success('撤回成功');
|
||||
// 刷新消息列表
|
||||
await this.loadDialogMessages();
|
||||
} catch (error) {
|
||||
console.error('撤回消息失败:', error);
|
||||
this.$message.error('撤回失败');
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.page = 1;
|
||||
this.queryForm.page = 1;
|
||||
this.getList();
|
||||
},
|
||||
handleReset() {
|
||||
this.queryForm = {
|
||||
senderNickname: '',
|
||||
senderPhone: '',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
page: 1,
|
||||
limit: 10,
|
||||
};
|
||||
this.dateRange = [];
|
||||
this.getList();
|
||||
},
|
||||
onDateChange(val) {
|
||||
if (val && val.length === 2) {
|
||||
this.queryForm.startTime = val[0];
|
||||
this.queryForm.endTime = val[1];
|
||||
} else {
|
||||
this.queryForm.startTime = '';
|
||||
this.queryForm.endTime = '';
|
||||
}
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.limit = val;
|
||||
this.page = 1;
|
||||
this.queryForm.limit = val;
|
||||
this.getList();
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.page = val;
|
||||
handlePageChange(val) {
|
||||
this.queryForm.page = val;
|
||||
this.getList();
|
||||
},
|
||||
handleDelete(id) {
|
||||
this.$confirm('确定要删除该会话吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
sessionDeleteApi(id).then(() => {
|
||||
this.$message.success('删除成功');
|
||||
this.getList();
|
||||
});
|
||||
});
|
||||
handleMessagePageChange(val) {
|
||||
this.messagePage = val;
|
||||
this.loadDialogMessages();
|
||||
},
|
||||
getMessageAvatar(msg) {
|
||||
if (!this.currentConversation) return '';
|
||||
if (msg.senderId === this.currentConversation.user1_id) {
|
||||
return this.currentConversation.sender_avatar || '';
|
||||
}
|
||||
return this.currentConversation.receiver_avatar || '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.padding-add {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.selWidth {
|
||||
width: 180px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.mt14 {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.ml10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.user-id {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #f56c6c !important;
|
||||
}
|
||||
|
||||
.message-preview {
|
||||
padding: 10px 20px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #409eff;
|
||||
}
|
||||
|
||||
.message-item.message-right {
|
||||
border-left-color: #67c23a;
|
||||
}
|
||||
|
||||
.message-sender {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 11px;
|
||||
color: #c0c4cc;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.dialog-users {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dialog-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dialog-arrow {
|
||||
font-size: 24px;
|
||||
color: #909399;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.dialog-messages {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.dialog-message {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.dialog-message.message-from-user1 {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
}
|
||||
|
||||
.msg-avatar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.msg-body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.msg-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.msg-sender {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.msg-time {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-left: auto;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.recall-btn {
|
||||
color: #f56c6c !important;
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.msg-content {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.recalled-tag {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.recalled-content {
|
||||
background: #fff3e0;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #ff9800;
|
||||
}
|
||||
|
||||
.original-label {
|
||||
font-size: 12px;
|
||||
color: #ff9800;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.original-text {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.dialog-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.block {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -10,80 +10,302 @@ import org.springframework.jdbc.core.JdbcTemplate;
|
|||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 会话管理控制器(社交互动模块)
|
||||
* 与私聊管理使用相同的数据表:eb_conversation 和 eb_private_message
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("api/admin/session")
|
||||
@Api(tags = "会话管理")
|
||||
@Api(tags = "社交互动 - 会话管理")
|
||||
@Validated
|
||||
public class SessionController {
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
*/
|
||||
@ApiOperation(value = "会话列表")
|
||||
@RequestMapping(value = "/list", method = RequestMethod.GET)
|
||||
@GetMapping("/list")
|
||||
public CommonResult<CommonPage<Map<String, Object>>> getList(
|
||||
@RequestParam(value = "senderNickname", required = false) String senderNickname,
|
||||
@RequestParam(value = "senderPhone", required = false) String senderPhone,
|
||||
@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "startTime", required = false) String startTime,
|
||||
@RequestParam(value = "endTime", required = false) String endTime,
|
||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "limit", defaultValue = "10") Integer limit) {
|
||||
|
||||
StringBuilder sql = new StringBuilder("SELECT * FROM eb_session WHERE 1=1");
|
||||
StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM eb_session WHERE 1=1");
|
||||
|
||||
if (senderNickname != null && !senderNickname.isEmpty()) {
|
||||
String safeName = senderNickname.replace("'", "''");
|
||||
sql.append(" AND sender_nickname LIKE '%").append(safeName).append("%'");
|
||||
countSql.append(" AND sender_nickname LIKE '%").append(safeName).append("%'");
|
||||
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT c.id, c.user1_id, c.user2_id, c.last_message, c.last_message_time, c.create_time, ");
|
||||
sql.append("u1.nickname as sender_nickname, u1.avatar as sender_avatar, u1.phone as sender_phone, ");
|
||||
sql.append("u2.nickname as receiver_nickname, u2.avatar as receiver_avatar, u2.phone as receiver_phone, ");
|
||||
sql.append("(SELECT COUNT(*) FROM eb_private_message WHERE conversation_id = c.id) as message_count ");
|
||||
sql.append("FROM eb_conversation c ");
|
||||
sql.append("LEFT JOIN eb_user u1 ON c.user1_id = u1.uid ");
|
||||
sql.append("LEFT JOIN eb_user u2 ON c.user2_id = u2.uid ");
|
||||
sql.append("WHERE 1=1 ");
|
||||
|
||||
StringBuilder countSql = new StringBuilder();
|
||||
countSql.append("SELECT COUNT(*) FROM eb_conversation c ");
|
||||
countSql.append("LEFT JOIN eb_user u1 ON c.user1_id = u1.uid ");
|
||||
countSql.append("LEFT JOIN eb_user u2 ON c.user2_id = u2.uid ");
|
||||
countSql.append("WHERE 1=1 ");
|
||||
|
||||
// 发送方昵称搜索
|
||||
if (senderNickname != null && !senderNickname.trim().isEmpty()) {
|
||||
String condition = " AND (u1.nickname LIKE '%" + senderNickname.replace("'", "''") + "%' OR u2.nickname LIKE '%" + senderNickname.replace("'", "''") + "%') ";
|
||||
sql.append(condition);
|
||||
countSql.append(condition);
|
||||
}
|
||||
if (senderPhone != null && !senderPhone.isEmpty()) {
|
||||
String safePhone = senderPhone.replace("'", "''");
|
||||
sql.append(" AND sender_phone LIKE '%").append(safePhone).append("%'");
|
||||
countSql.append(" AND sender_phone LIKE '%").append(safePhone).append("%'");
|
||||
|
||||
// 发送方电话搜索
|
||||
if (senderPhone != null && !senderPhone.trim().isEmpty()) {
|
||||
String condition = " AND (u1.phone LIKE '%" + senderPhone.replace("'", "''") + "%' OR u2.phone LIKE '%" + senderPhone.replace("'", "''") + "%') ";
|
||||
sql.append(condition);
|
||||
countSql.append(condition);
|
||||
}
|
||||
if (startTime != null && !startTime.isEmpty()) {
|
||||
sql.append(" AND create_time >= '").append(startTime).append("'");
|
||||
countSql.append(" AND create_time >= '").append(startTime).append("'");
|
||||
|
||||
// 关键词搜索(兼容私聊管理的参数)
|
||||
if (keyword != null && !keyword.trim().isEmpty()) {
|
||||
String condition = " AND (u1.nickname LIKE '%" + keyword + "%' OR u2.nickname LIKE '%" + keyword + "%' "
|
||||
+ "OR c.user1_id = '" + keyword + "' OR c.user2_id = '" + keyword + "') ";
|
||||
sql.append(condition);
|
||||
countSql.append(condition);
|
||||
}
|
||||
if (endTime != null && !endTime.isEmpty()) {
|
||||
sql.append(" AND create_time <= '").append(endTime).append("'");
|
||||
countSql.append(" AND create_time <= '").append(endTime).append("'");
|
||||
|
||||
// 时间范围
|
||||
if (startTime != null && !startTime.trim().isEmpty()) {
|
||||
String condition = " AND c.create_time >= '" + startTime + "' ";
|
||||
sql.append(condition);
|
||||
countSql.append(condition);
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY id DESC");
|
||||
Long total = jdbcTemplate.queryForObject(countSql.toString(), Long.class);
|
||||
|
||||
if (endTime != null && !endTime.trim().isEmpty()) {
|
||||
String condition = " AND c.create_time <= '" + endTime + "' ";
|
||||
sql.append(condition);
|
||||
countSql.append(condition);
|
||||
}
|
||||
|
||||
// 排序
|
||||
sql.append("ORDER BY c.last_message_time DESC ");
|
||||
|
||||
// 统计总数
|
||||
Long total = 0L;
|
||||
try {
|
||||
total = jdbcTemplate.queryForObject(countSql.toString(), Long.class);
|
||||
} catch (Exception e) {
|
||||
log.error("查询会话总数失败: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 分页
|
||||
int offset = (page - 1) * limit;
|
||||
sql.append(" LIMIT ").append(offset).append(", ").append(limit);
|
||||
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql.toString());
|
||||
|
||||
sql.append("LIMIT ").append(offset).append(", ").append(limit);
|
||||
|
||||
List<Map<String, Object>> list;
|
||||
try {
|
||||
list = jdbcTemplate.queryForList(sql.toString());
|
||||
} catch (Exception e) {
|
||||
log.error("查询会话列表失败: {}", e.getMessage());
|
||||
list = java.util.Collections.emptyList();
|
||||
}
|
||||
|
||||
CommonPage<Map<String, Object>> result = new CommonPage<>();
|
||||
result.setList(list);
|
||||
result.setTotal(total);
|
||||
result.setTotal(total != null ? total : 0L);
|
||||
result.setPage(page);
|
||||
result.setLimit(limit);
|
||||
result.setTotalPage((int) Math.ceil((double) total / limit));
|
||||
|
||||
result.setTotalPage((int) Math.ceil((double) (total != null ? total : 0) / limit));
|
||||
|
||||
return CommonResult.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话详情
|
||||
*/
|
||||
@ApiOperation(value = "会话详情")
|
||||
@RequestMapping(value = "/detail/{id}", method = RequestMethod.GET)
|
||||
public CommonResult<Map<String, Object>> getDetail(@PathVariable Integer id) {
|
||||
String sql = "SELECT * FROM eb_session WHERE id = ?";
|
||||
Map<String, Object> detail = jdbcTemplate.queryForMap(sql, id);
|
||||
return CommonResult.success(detail);
|
||||
@GetMapping("/detail/{id}")
|
||||
public CommonResult<Map<String, Object>> getDetail(@PathVariable Long id) {
|
||||
try {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT c.*, ");
|
||||
sql.append("u1.nickname as sender_nickname, u1.avatar as sender_avatar, u1.phone as sender_phone, ");
|
||||
sql.append("u2.nickname as receiver_nickname, u2.avatar as receiver_avatar, u2.phone as receiver_phone ");
|
||||
sql.append("FROM eb_conversation c ");
|
||||
sql.append("LEFT JOIN eb_user u1 ON c.user1_id = u1.uid ");
|
||||
sql.append("LEFT JOIN eb_user u2 ON c.user2_id = u2.uid ");
|
||||
sql.append("WHERE c.id = ?");
|
||||
|
||||
Map<String, Object> detail = jdbcTemplate.queryForMap(sql.toString(), id);
|
||||
return CommonResult.success(detail);
|
||||
} catch (Exception e) {
|
||||
log.error("获取会话详情失败: {}", e.getMessage());
|
||||
return CommonResult.failed("获取会话详情失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话消息列表
|
||||
*/
|
||||
@ApiOperation(value = "会话消息列表")
|
||||
@GetMapping("/{conversationId}/messages")
|
||||
public CommonResult<CommonPage<Map<String, Object>>> getMessages(
|
||||
@PathVariable Long conversationId,
|
||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
|
||||
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT m.*, u.nickname as sender_name, u.avatar as sender_avatar ");
|
||||
sql.append("FROM eb_private_message m ");
|
||||
sql.append("LEFT JOIN eb_user u ON m.sender_id = u.uid ");
|
||||
sql.append("WHERE m.conversation_id = ? ");
|
||||
sql.append("ORDER BY m.create_time DESC ");
|
||||
|
||||
Long total = 0L;
|
||||
try {
|
||||
String countSql = "SELECT COUNT(*) FROM eb_private_message WHERE conversation_id = ?";
|
||||
total = jdbcTemplate.queryForObject(countSql, Long.class, conversationId);
|
||||
} catch (Exception e) {
|
||||
log.error("查询消息总数失败: {}", e.getMessage());
|
||||
}
|
||||
|
||||
int offset = (page - 1) * pageSize;
|
||||
sql.append("LIMIT ").append(offset).append(", ").append(pageSize);
|
||||
|
||||
List<Map<String, Object>> list;
|
||||
try {
|
||||
list = jdbcTemplate.queryForList(sql.toString(), conversationId);
|
||||
// 转换字段名
|
||||
list.forEach(item -> {
|
||||
item.put("senderId", item.get("sender_id"));
|
||||
item.put("receiverId", item.get("receiver_id"));
|
||||
item.put("senderName", item.get("sender_name"));
|
||||
item.put("senderAvatar", item.get("sender_avatar"));
|
||||
item.put("messageType", item.get("message_type"));
|
||||
item.put("createTime", item.get("create_time"));
|
||||
item.put("isRecalled", item.get("is_recalled"));
|
||||
item.put("originalContent", item.get("original_content"));
|
||||
item.put("recallTime", item.get("recall_time"));
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("查询消息列表失败: {}", e.getMessage());
|
||||
list = java.util.Collections.emptyList();
|
||||
}
|
||||
|
||||
CommonPage<Map<String, Object>> result = new CommonPage<>();
|
||||
result.setList(list);
|
||||
result.setTotal(total != null ? total : 0L);
|
||||
result.setPage(page);
|
||||
result.setLimit(pageSize);
|
||||
result.setTotalPage((int) Math.ceil((double) (total != null ? total : 0) / pageSize));
|
||||
|
||||
return CommonResult.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话(包括所有消息)
|
||||
*/
|
||||
@ApiOperation(value = "删除会话")
|
||||
@RequestMapping(value = "/delete/{id}", method = RequestMethod.POST)
|
||||
public CommonResult<String> delete(@PathVariable Integer id) {
|
||||
jdbcTemplate.update("DELETE FROM eb_session WHERE id = ?", id);
|
||||
return CommonResult.success("删除成功");
|
||||
@PostMapping("/delete/{id}")
|
||||
public CommonResult<String> delete(@PathVariable Long id) {
|
||||
try {
|
||||
// 先删除消息
|
||||
jdbcTemplate.update("DELETE FROM eb_private_message WHERE conversation_id = ?", id);
|
||||
// 删除会话
|
||||
jdbcTemplate.update("DELETE FROM eb_conversation WHERE id = ?", id);
|
||||
return CommonResult.success("删除成功");
|
||||
} catch (Exception e) {
|
||||
log.error("删除会话失败: {}", e.getMessage());
|
||||
return CommonResult.failed("删除失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除单条消息
|
||||
*/
|
||||
@ApiOperation(value = "删除消息")
|
||||
@PostMapping("/message/delete/{messageId}")
|
||||
public CommonResult<String> deleteMessage(@PathVariable Long messageId) {
|
||||
try {
|
||||
jdbcTemplate.update("DELETE FROM eb_private_message WHERE id = ?", messageId);
|
||||
return CommonResult.success("删除成功");
|
||||
} catch (Exception e) {
|
||||
log.error("删除消息失败: {}", e.getMessage());
|
||||
return CommonResult.failed("删除失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回消息(管理员可以撤回任意消息)
|
||||
*/
|
||||
@ApiOperation(value = "撤回消息")
|
||||
@PostMapping("/message/recall/{messageId}")
|
||||
public CommonResult<String> recallMessage(@PathVariable Long messageId) {
|
||||
try {
|
||||
// 先获取原始消息内容
|
||||
Map<String, Object> message = jdbcTemplate.queryForMap(
|
||||
"SELECT content FROM eb_private_message WHERE id = ?", messageId);
|
||||
String originalContent = (String) message.get("content");
|
||||
|
||||
// 保存原始内容并标记为撤回
|
||||
int updated = jdbcTemplate.update(
|
||||
"UPDATE eb_private_message SET is_recalled = 1, original_content = ?, recall_time = NOW(), content = '[消息已被管理员撤回]' WHERE id = ?",
|
||||
originalContent, messageId
|
||||
);
|
||||
if (updated > 0) {
|
||||
return CommonResult.success("撤回成功");
|
||||
} else {
|
||||
return CommonResult.failed("消息不存在");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("撤回消息失败: {}", e.getMessage());
|
||||
return CommonResult.failed("撤回失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话统计数据
|
||||
*/
|
||||
@ApiOperation(value = "会话统计")
|
||||
@GetMapping("/statistics")
|
||||
public CommonResult<Map<String, Object>> getStatistics() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 总会话数
|
||||
Long totalConversations = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM eb_conversation", Long.class);
|
||||
stats.put("totalConversations", totalConversations != null ? totalConversations : 0);
|
||||
|
||||
// 总消息数
|
||||
Long totalMessages = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM eb_private_message", Long.class);
|
||||
stats.put("totalMessages", totalMessages != null ? totalMessages : 0);
|
||||
|
||||
// 今日消息数
|
||||
Long todayMessages = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM eb_private_message WHERE DATE(create_time) = CURDATE()", Long.class);
|
||||
stats.put("todayMessages", todayMessages != null ? todayMessages : 0);
|
||||
|
||||
// 活跃会话数(最近7天有消息的会话)
|
||||
Long activeConversations = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(DISTINCT conversation_id) FROM eb_private_message WHERE create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)",
|
||||
Long.class);
|
||||
stats.put("activeConversations", activeConversations != null ? activeConversations : 0);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取统计数据失败: {}", e.getMessage());
|
||||
stats.put("totalConversations", 0);
|
||||
stats.put("totalMessages", 0);
|
||||
stats.put("todayMessages", 0);
|
||||
stats.put("activeConversations", 0);
|
||||
}
|
||||
|
||||
return CommonResult.success(stats);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,4 +82,12 @@ public class PrivateMessage implements Serializable {
|
|||
@ApiModelProperty(value = "是否已撤回")
|
||||
@Column(name = "is_recalled", columnDefinition = "TINYINT(1) DEFAULT 0")
|
||||
private Boolean isRecalled;
|
||||
|
||||
@ApiModelProperty(value = "原始消息内容(撤回前)")
|
||||
@Column(name = "original_content", columnDefinition = "TEXT")
|
||||
private String originalContent;
|
||||
|
||||
@ApiModelProperty(value = "撤回时间")
|
||||
@Column(name = "recall_time")
|
||||
private Date recallTime;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,3 +229,4 @@ public class CallController {
|
|||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -145,4 +145,15 @@ public class ConversationController {
|
|||
Integer userId = userService.getUserIdException();
|
||||
return CommonResult.success(conversationService.deleteMessage(id, userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回消息
|
||||
*/
|
||||
@ApiOperation(value = "撤回消息")
|
||||
@ApiImplicitParam(name = "id", value = "消息ID", required = true)
|
||||
@PostMapping("/messages/{id}/recall")
|
||||
public CommonResult<Boolean> recallMessage(@PathVariable Long id) {
|
||||
Integer userId = userService.getUserIdException();
|
||||
return CommonResult.success(conversationService.recallMessage(id, userId));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -412,10 +412,12 @@ public class ConversationServiceImpl extends ServiceImpl<ConversationDao, Conver
|
|||
if (diffMinutes > 2) {
|
||||
throw new CrmebException("消息发送超过2分钟,无法撤回");
|
||||
}
|
||||
// 标记消息为已撤回
|
||||
// 保存原始内容,然后标记消息为已撤回
|
||||
LambdaUpdateWrapper<PrivateMessage> uw = new LambdaUpdateWrapper<>();
|
||||
uw.eq(PrivateMessage::getId, messageId)
|
||||
.set(PrivateMessage::getIsRecalled, true)
|
||||
.set(PrivateMessage::getOriginalContent, message.getContent()) // 保存原始内容
|
||||
.set(PrivateMessage::getRecallTime, new Date()) // 记录撤回时间
|
||||
.set(PrivateMessage::getContent, "[消息已撤回]");
|
||||
return privateMessageDao.update(null, uw) > 0;
|
||||
}
|
||||
|
|
|
|||
8
Zhibo/zhibo-h/sql/add_recall_fields.sql
Normal file
8
Zhibo/zhibo-h/sql/add_recall_fields.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- 为私信消息表添加撤回相关字段
|
||||
-- 执行此脚本前请备份数据库
|
||||
|
||||
-- 添加原始内容字段(保存撤回前的消息内容)
|
||||
ALTER TABLE eb_private_message ADD COLUMN original_content TEXT COMMENT '原始消息内容(撤回前)';
|
||||
|
||||
-- 添加撤回时间字段
|
||||
ALTER TABLE eb_private_message ADD COLUMN recall_time DATETIME COMMENT '撤回时间';
|
||||
|
|
@ -109,6 +109,9 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
setupMessages();
|
||||
setupInput();
|
||||
|
||||
// 确保输入框显示正确的提示文本
|
||||
binding.messageInput.setHint("输入消息...");
|
||||
|
||||
// 标记会话为已读
|
||||
if (initialUnreadCount > 0 && conversationId != null) {
|
||||
markConversationAsRead();
|
||||
|
|
@ -414,9 +417,16 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
PopupMenu popupMenu = new PopupMenu(this, anchorView);
|
||||
popupMenu.getMenu().add(0, 0, 0, "复制");
|
||||
popupMenu.getMenu().add(0, 2, 0, "表情回应");
|
||||
// 只有自己发送的消息才能删除
|
||||
if ("我".equals(message.getUsername())) {
|
||||
// 只有自己发送的消息才能删除和撤回
|
||||
if ("我".equals(message.getUsername()) || message.isOutgoing()) {
|
||||
popupMenu.getMenu().add(0, 1, 0, "删除");
|
||||
// 检查是否在2分钟内,可以撤回
|
||||
long messageTime = message.getTimestamp();
|
||||
long now = System.currentTimeMillis();
|
||||
long diffMinutes = (now - messageTime) / (1000 * 60);
|
||||
if (diffMinutes <= 2) {
|
||||
popupMenu.getMenu().add(0, 3, 0, "撤回");
|
||||
}
|
||||
}
|
||||
|
||||
popupMenu.setOnMenuItemClickListener(item -> {
|
||||
|
|
@ -429,6 +439,9 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
} else if (item.getItemId() == 2) {
|
||||
showEmojiPicker(message);
|
||||
return true;
|
||||
} else if (item.getItemId() == 3) {
|
||||
recallMessage(message, position);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
|
@ -497,6 +510,68 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回消息
|
||||
*/
|
||||
private void recallMessage(ChatMessage message, int position) {
|
||||
if (position < 0 || position >= messages.size()) return;
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("撤回消息")
|
||||
.setMessage("确定要撤回这条消息吗?")
|
||||
.setPositiveButton("撤回", (dialog, which) -> recallMessageFromServer(message, position))
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用服务器撤回消息接口
|
||||
*/
|
||||
private void recallMessageFromServer(ChatMessage message, int position) {
|
||||
String token = AuthStore.getToken(this);
|
||||
if (token == null) return;
|
||||
|
||||
String messageId = message.getMessageId();
|
||||
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/messages/" + messageId + "/recall";
|
||||
Log.d(TAG, "撤回消息: " + url);
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Authori-zation", token)
|
||||
.post(RequestBody.create("", MediaType.parse("application/json")))
|
||||
.build();
|
||||
|
||||
httpClient.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.e(TAG, "撤回消息失败", e);
|
||||
runOnUiThread(() -> Snackbar.make(binding.getRoot(), "撤回失败", Snackbar.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
String body = response.body() != null ? response.body().string() : "";
|
||||
Log.d(TAG, "撤回消息响应: " + body);
|
||||
runOnUiThread(() -> {
|
||||
try {
|
||||
JSONObject json = new JSONObject(body);
|
||||
if (json.optInt("code", -1) == 200) {
|
||||
// 更新本地消息显示为已撤回
|
||||
message.setMessage("[消息已撤回]");
|
||||
adapter.notifyItemChanged(position);
|
||||
Snackbar.make(binding.getRoot(), "消息已撤回", Snackbar.LENGTH_SHORT).show();
|
||||
} else {
|
||||
String errorMsg = json.optString("message", "撤回失败");
|
||||
Snackbar.make(binding.getRoot(), errorMsg, Snackbar.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "解析撤回响应失败", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void setupInput() {
|
||||
binding.sendButton.setOnClickListener(new DebounceClickListener(300) {
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
<EditText
|
||||
android:id="@+id/messageInput"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="40dp"
|
||||
|
|
@ -136,7 +136,10 @@
|
|||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:textColor="#111111"
|
||||
android:textSize="14sp" />
|
||||
android:textColorHint="#999999"
|
||||
android:textSize="14sp"
|
||||
android:importantForAutofill="no"
|
||||
android:autofillHints="" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/sendButton"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user