feat(frontend): Task 8 - 聊天设置页 + 消息搜索页 + 联系人改造
pages/chat/settings.vue(聊天设置页): - 对方信息展示(头像+昵称) - 置顶/取消置顶切换 - 清空聊天记录(确认弹窗) - 删除会话(确认弹窗 + 双层返回) pages/chat/search.vue(消息搜索页): - 全局消息搜索(关键词匹配) - 搜索结果列表(发送者头像+昵称+内容+时间) - 点击跳转到对应会话 联系人模块改造: - contact/detail.vue "发消息"按钮改为跳转聊天页 Made-with: Cursor
This commit is contained in:
217
frontend/src/pages/chat/search.vue
Normal file
217
frontend/src/pages/chat/search.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<!--
|
||||
消息搜索页
|
||||
|
||||
设计系统:design-system/echochat/MASTER.md
|
||||
功能:全局消息搜索,结果按会话分组,点击跳转到聊天页
|
||||
-->
|
||||
<template>
|
||||
<view class="page-wrapper">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input-wrap">
|
||||
<text class="search-icon">🔍</text>
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="keyword"
|
||||
placeholder="搜索聊天记录"
|
||||
placeholder-style="color: #94A3B8"
|
||||
confirm-type="search"
|
||||
focus
|
||||
@confirm="onSearch"
|
||||
/>
|
||||
</view>
|
||||
<text class="search-cancel" @tap="goBack">取消</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索结果 -->
|
||||
<scroll-view scroll-y class="result-list">
|
||||
<!-- 空状态 -->
|
||||
<view v-if="searched && results.length === 0" class="empty-state">
|
||||
<text class="empty-text">未找到相关消息</text>
|
||||
</view>
|
||||
|
||||
<!-- 结果列表 -->
|
||||
<view
|
||||
v-for="item in results"
|
||||
:key="item.id"
|
||||
class="result-item"
|
||||
@tap="openChat(item)"
|
||||
>
|
||||
<view class="result-avatar-wrap">
|
||||
<image v-if="item.sender_avatar" class="result-avatar" :src="item.sender_avatar" mode="aspectFill" />
|
||||
<view v-else class="result-avatar result-avatar-placeholder">
|
||||
<text class="avatar-text">{{ (item.sender_nickname || '?')[0] }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="result-info">
|
||||
<text class="result-name">{{ item.sender_nickname || '未知用户' }}</text>
|
||||
<text class="result-content">{{ item.content }}</text>
|
||||
<text class="result-time">{{ item.created_at }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue'
|
||||
import imApi from '@/api/im'
|
||||
|
||||
export default {
|
||||
name: 'ChatSearch',
|
||||
setup() {
|
||||
const keyword = ref('')
|
||||
const results = ref([])
|
||||
const searched = ref(false)
|
||||
|
||||
const onSearch = async () => {
|
||||
const kw = keyword.value.trim()
|
||||
if (!kw) return
|
||||
|
||||
searched.value = true
|
||||
try {
|
||||
const res = await imApi.searchMessages(kw)
|
||||
results.value = (res.data && res.data.list) || []
|
||||
} catch {
|
||||
results.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const openChat = (item) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/chat/conversation?conversationId=${item.conversation_id}&peerId=${item.sender_id}&peerName=${encodeURIComponent(item.sender_nickname || '')}&peerAvatar=${encodeURIComponent(item.sender_avatar || '')}`
|
||||
})
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
return {
|
||||
keyword,
|
||||
results,
|
||||
searched,
|
||||
onSearch,
|
||||
openChat,
|
||||
goBack
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-wrapper {
|
||||
min-height: 100vh;
|
||||
background-color: #F8FAFC;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 24rpx;
|
||||
background-color: #FFFFFF;
|
||||
border-bottom: 1rpx solid #E2E8F0;
|
||||
}
|
||||
|
||||
.search-input-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #F1F5F9;
|
||||
border-radius: 36rpx;
|
||||
padding: 0 24rpx;
|
||||
height: 68rpx;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 28rpx;
|
||||
margin-right: 12rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
.search-cancel {
|
||||
margin-left: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #2563EB;
|
||||
}
|
||||
|
||||
.result-list {
|
||||
height: calc(100vh - 100rpx);
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 32rpx;
|
||||
background-color: #FFFFFF;
|
||||
border-bottom: 1rpx solid #F1F5F9;
|
||||
}
|
||||
|
||||
.result-avatar-wrap {
|
||||
flex-shrink: 0;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.result-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.result-avatar-placeholder {
|
||||
background-color: #2563EB;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
color: #FFFFFF;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.result-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #1E293B;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
font-size: 26rpx;
|
||||
color: #64748B;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.result-time {
|
||||
font-size: 22rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 200rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
</style>
|
||||
186
frontend/src/pages/chat/settings.vue
Normal file
186
frontend/src/pages/chat/settings.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<!--
|
||||
聊天设置页
|
||||
|
||||
设计系统:design-system/echochat/MASTER.md
|
||||
功能:对方信息展示、置顶、清空聊天记录、删除会话
|
||||
-->
|
||||
<template>
|
||||
<view class="page-wrapper">
|
||||
<!-- 对方信息 -->
|
||||
<view class="user-card">
|
||||
<view class="user-avatar-wrap">
|
||||
<image v-if="peerAvatar" class="user-avatar" :src="peerAvatar" mode="aspectFill" />
|
||||
<view v-else class="user-avatar user-avatar-placeholder">
|
||||
<text class="avatar-text">{{ (peerName || '?')[0] }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="user-name">{{ peerName || '未知用户' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 操作项 -->
|
||||
<view class="settings-group">
|
||||
<view class="settings-item" @tap="togglePin">
|
||||
<text class="settings-label">{{ isPinned ? '取消置顶' : '置顶聊天' }}</text>
|
||||
<text class="settings-value">{{ isPinned ? '已置顶' : '' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="settings-group">
|
||||
<view class="settings-item settings-danger" @tap="onClearHistory">
|
||||
<text class="settings-label-danger">清空聊天记录</text>
|
||||
</view>
|
||||
<view class="settings-item settings-danger" @tap="onDeleteConversation">
|
||||
<text class="settings-label-danger">删除会话</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { useChatStore } from '@/store/chat'
|
||||
|
||||
export default {
|
||||
name: 'ChatSettings',
|
||||
setup() {
|
||||
const chatStore = useChatStore()
|
||||
const conversationId = ref(0)
|
||||
const peerId = ref(0)
|
||||
const peerName = ref('')
|
||||
const peerAvatar = ref('')
|
||||
const isPinned = ref(false)
|
||||
|
||||
onLoad((query) => {
|
||||
conversationId.value = parseInt(query.conversationId) || 0
|
||||
peerId.value = parseInt(query.peerId) || 0
|
||||
peerName.value = decodeURIComponent(query.peerName || '')
|
||||
peerAvatar.value = decodeURIComponent(query.peerAvatar || '')
|
||||
|
||||
const conv = chatStore.conversationList.find(c => c.id === conversationId.value)
|
||||
if (conv) {
|
||||
isPinned.value = conv.is_pinned
|
||||
}
|
||||
})
|
||||
|
||||
const togglePin = async () => {
|
||||
const newVal = !isPinned.value
|
||||
await chatStore.pinConversation(conversationId.value, newVal)
|
||||
isPinned.value = newVal
|
||||
uni.showToast({ title: newVal ? '已置顶' : '已取消置顶', icon: 'none' })
|
||||
}
|
||||
|
||||
const onClearHistory = () => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定清空聊天记录?此操作不可恢复',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
await chatStore.clearHistory(conversationId.value)
|
||||
uni.showToast({ title: '已清空', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onDeleteConversation = () => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定删除该会话?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
await chatStore.deleteConversation(conversationId.value)
|
||||
uni.navigateBack({ delta: 2 })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
peerName,
|
||||
peerAvatar,
|
||||
isPinned,
|
||||
togglePin,
|
||||
onClearHistory,
|
||||
onDeleteConversation
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-wrapper {
|
||||
min-height: 100vh;
|
||||
background-color: #F1F5F9;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48rpx 32rpx;
|
||||
background-color: #FFFFFF;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.user-avatar-wrap {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 30rpx;
|
||||
}
|
||||
|
||||
.user-avatar-placeholder {
|
||||
background-color: #2563EB;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
color: #FFFFFF;
|
||||
font-size: 48rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
background-color: #FFFFFF;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.settings-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #F1F5F9;
|
||||
}
|
||||
|
||||
.settings-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.settings-label {
|
||||
font-size: 30rpx;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
.settings-value {
|
||||
font-size: 26rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
.settings-label-danger {
|
||||
font-size: 30rpx;
|
||||
color: #EF4444;
|
||||
}
|
||||
</style>
|
||||
@@ -136,7 +136,11 @@ const selectGroup = () => {
|
||||
}
|
||||
|
||||
const sendMessage = () => {
|
||||
uni.showToast({ title: '聊天功能开发中', icon: 'none' })
|
||||
const f = friend.value
|
||||
if (!f) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/chat/conversation?conversationId=0&peerId=${f.user_id}&peerName=${encodeURIComponent(f.remark || f.nickname || f.username)}&peerAvatar=${encodeURIComponent(f.avatar || '')}`
|
||||
})
|
||||
}
|
||||
|
||||
const handleBlock = () => {
|
||||
|
||||
Reference in New Issue
Block a user