diff --git a/docs/progress/CURRENT_STATUS.md b/docs/progress/CURRENT_STATUS.md
index 1184e5f..28e4879 100644
--- a/docs/progress/CURRENT_STATUS.md
+++ b/docs/progress/CURRENT_STATUS.md
@@ -1,7 +1,7 @@
# EchoChat 项目开发进度
-> **最后更新**:2026-03-03(Phase 2b 即时通讯核心开发完成)
-> **当前阶段**:Phase 2b 开发中
+> **最后更新**:2026-03-03(Phase 2b 即时通讯核心 + ui-ux-pro-max 规范改造完成)
+> **当前阶段**:Phase 2b 已完成
> **当前分支**:`feature/phase2b-instant-messaging`
> **实施计划**:`docs/plans/2026-03-03-phase2b-implementation.plan.md`
> **设计文档**:`docs/plans/2026-03-03-phase2b-design.md`
@@ -22,6 +22,7 @@
| Task 7 | 会话列表页 + 聊天对话页 | ✅ 完成 | 2 个核心页面 |
| Task 8 | 设置页 + 搜索页 + 联系人改造 | ✅ 完成 | 2 个辅助页面 + 发消息跳转 |
| Task 9 | 文档更新 + 代码审查 | ✅ 完成 | 进度/架构文档同步 |
+| UI 改造 | ui-ux-pro-max 规范改造 | ✅ 完成 | uni-icons 替换 emoji + 设计规范文件 |
---
@@ -112,7 +113,9 @@
2. **状态管理**:Pinia 2.1.7 + pinia-plugin-persistedstate@3
3. **WebSocket**:`uni.connectSocket`(小程序)/ `WebSocket`(H5)
4. **IM Store**:chat.js(会话列表 + 消息缓存 + 三态确认 + 全局未读)
-5. **设计系统**:ui-ux-pro-max 规范
+5. **设计系统**:ui-ux-pro-max 规范,MASTER.md + 页面覆盖规范
+6. **图标方案**:@dcloudio/uni-ui uni-icons(easycom 自动引入,跨平台兼容)
+7. **预处理器**:sass(uni-icons SCSS 依赖)
### 后台管理端(admin/)
1. **框架**:Vue 3.5+ + Vite 7.x + Element Plus
diff --git a/frontend/src/pages/chat/conversation.vue b/frontend/src/pages/chat/conversation.vue
index ade8e45..e369c81 100644
--- a/frontend/src/pages/chat/conversation.vue
+++ b/frontend/src/pages/chat/conversation.vue
@@ -1,21 +1,14 @@
-
+
@@ -37,54 +30,64 @@
:scroll-with-animation="true"
@scrolltoupper="onLoadMore"
>
-
- {{ loadingMore ? '加载中...' : '上拉加载更多' }}
+ {{ loadingMore ? '加载中...' : '加载更多' }}
-
-
-
-
-
- {{ (peerName || '?')[0] }}
+
+
+
+
+
+ {{ (peerName || '?')[0] }}
+
-
+
+ 消息已撤回
+ {{ msg.content }}
+
+
-
-
- 消息已撤回
- {{ msg.content }}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ 消息已撤回
+ {{ msg.content }}
+
+
+
+
+ {{ (selfName || '我')[0] }}
+
+
+
-
+
import { onLoad, onUnload } from '@dcloudio/uni-app'
-import { ref, computed, nextTick } from 'vue'
+import { ref, computed, nextTick, watch } from 'vue'
import { useChatStore } from '@/store/chat'
import { useUserStore } from '@/store/user'
@@ -128,9 +131,26 @@ export default {
const messages = computed(() => chatStore.currentMessages)
const hasMore = computed(() => chatStore.hasMoreMap[conversationId.value] !== false)
const isTyping = computed(() => chatStore.typingMap[conversationId.value] || false)
+ const selfAvatar = computed(() => userStore.userInfo?.avatar || '')
+ const selfName = computed(() => userStore.userInfo?.nickname || userStore.userInfo?.username || '我')
const isSelf = (msg) => {
- return msg.sender_id === (userStore.userInfo?.id || 0) || msg._sending
+ const myId = Number(userStore.userInfo?.id) || 0
+ return msg.sender_id === myId || msg._sending === true || msg._failed === true
+ }
+
+ const tryFindExistingConversation = async () => {
+ try {
+ await chatStore.fetchConversations()
+ const existingConv = chatStore.conversationList.find(c => c.peer_user_id === peerId.value)
+ if (existingConv) {
+ conversationId.value = existingConv.id
+ chatStore.setCurrentConversation(existingConv.id)
+ await loadInitialMessages()
+ }
+ } catch (e) {
+ console.warn('[Chat] 查找已有会话失败', e)
+ }
}
onLoad((query) => {
@@ -144,6 +164,8 @@ export default {
if (conversationId.value) {
chatStore.setCurrentConversation(conversationId.value)
loadInitialMessages()
+ } else if (peerId.value) {
+ tryFindExistingConversation()
}
})
@@ -151,6 +173,12 @@ export default {
chatStore.setCurrentConversation(null)
})
+ watch(() => chatStore.currentConversationId, (newId) => {
+ if (newId && newId !== conversationId.value) {
+ conversationId.value = newId
+ }
+ })
+
const loadInitialMessages = async () => {
if (!chatStore.messagesMap[conversationId.value]?.length) {
await chatStore.loadHistoryMessages(conversationId.value)
@@ -161,23 +189,19 @@ export default {
const scrollToBottom = () => {
nextTick(() => {
scrollToId.value = ''
- nextTick(() => {
- scrollToId.value = 'msg-bottom'
- })
+ nextTick(() => { scrollToId.value = 'msg-bottom' })
})
}
const onSend = () => {
const content = inputText.value.trim()
if (!content) return
-
chatStore.sendMessage({
conversationId: conversationId.value || 0,
targetUserId: conversationId.value ? 0 : peerId.value,
content,
type: 1
})
-
inputText.value = ''
scrollToBottom()
}
@@ -188,19 +212,14 @@ export default {
if (conversationId.value) {
chatStore.sendTyping(conversationId.value)
}
- typingTimer = setTimeout(() => {
- typingTimer = null
- }, 3000)
+ typingTimer = setTimeout(() => { typingTimer = null }, 3000)
}
const onLoadMore = async () => {
if (loadingMore.value || !hasMore.value) return
loadingMore.value = true
- try {
- await chatStore.loadHistoryMessages(conversationId.value)
- } finally {
- loadingMore.value = false
- }
+ try { await chatStore.loadHistoryMessages(conversationId.value) }
+ finally { loadingMore.value = false }
}
const onMsgLongPress = (msg) => {
@@ -208,9 +227,7 @@ export default {
uni.showActionSheet({
itemList: ['撤回'],
success: (res) => {
- if (res.tapIndex === 0 && msg.id) {
- chatStore.recallMessage(msg.id)
- }
+ if (res.tapIndex === 0 && msg.id) chatStore.recallMessage(msg.id)
}
})
}
@@ -221,18 +238,18 @@ export default {
content: '是否重新发送?',
success: (res) => {
if (res.confirm) {
- chatStore.sendMessage({
- conversationId: conversationId.value,
- content: msg.content,
- type: msg.type || 1
- })
+ chatStore.sendMessage({ conversationId: conversationId.value, content: msg.content, type: msg.type || 1 })
}
}
})
}
const goBack = () => {
- uni.navigateBack()
+ if (getCurrentPages().length > 1) {
+ uni.navigateBack()
+ } else {
+ uni.switchTab({ url: '/pages/chat/index' })
+ }
}
const goToSettings = () => {
@@ -242,22 +259,11 @@ export default {
}
return {
- peerName,
- peerAvatar,
- inputText,
- scrollToId,
- loadingMore,
- messages,
- hasMore,
- isTyping,
- isSelf,
- onSend,
- onInputChange,
- onLoadMore,
- onMsgLongPress,
- onResend,
- goBack,
- goToSettings
+ peerName, peerAvatar, selfAvatar, selfName,
+ inputText, scrollToId, loadingMore,
+ messages, hasMore, isTyping,
+ isSelf, onSend, onInputChange, onLoadMore,
+ onMsgLongPress, onResend, goBack, goToSettings
}
}
}
@@ -269,9 +275,10 @@ export default {
display: flex;
flex-direction: column;
background-color: #F1F5F9;
+ overflow: hidden;
}
-/* 导航栏 */
+/* ===== 导航栏 ===== */
.nav-bar {
display: flex;
align-items: center;
@@ -281,8 +288,7 @@ export default {
background-color: #FFFFFF;
border-bottom: 1rpx solid #E2E8F0;
}
-
-.nav-left {
+.nav-left, .nav-right {
min-width: 88rpx;
min-height: 88rpx;
display: flex;
@@ -290,149 +296,85 @@ export default {
justify-content: center;
transition: opacity 150ms ease;
}
+.nav-left:active, .nav-right:active { opacity: 0.6; }
+.nav-center { flex: 1; text-align: center; }
+.nav-title { font-size: 32rpx; font-weight: 600; color: #1E293B; }
+.nav-typing { display: block; font-size: 22rpx; color: #2563EB; margin-top: 2rpx; }
-.nav-left:active {
- opacity: 0.6;
-}
-
-.nav-center {
- flex: 1;
- text-align: center;
-}
-
-.nav-title {
- font-size: 32rpx;
- font-weight: 600;
- color: #1E293B;
-}
-
-.nav-typing {
- display: block;
- font-size: 22rpx;
- color: #2563EB;
- margin-top: 2rpx;
-}
-
-.nav-right {
- min-width: 88rpx;
- min-height: 88rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: opacity 150ms ease;
-}
-
-.nav-right:active {
- opacity: 0.6;
-}
-
-/* 消息列表 */
+/* ===== 消息列表 ===== */
.msg-list {
flex: 1;
padding: 16rpx 24rpx;
+ box-sizing: border-box;
+ width: 100%;
}
+.load-more { text-align: center; padding: 16rpx 0; }
+.load-more-text { font-size: 24rpx; color: #94A3B8; }
-.load-more {
- text-align: center;
- padding: 16rpx 0;
-}
-
-.load-more-text {
- font-size: 24rpx;
- color: #94A3B8;
-}
-
-/* 消息行 */
+/* ===== 消息行 ===== */
.msg-row {
display: flex;
align-items: flex-start;
margin-bottom: 24rpx;
}
-
-.msg-row-self {
- flex-direction: row-reverse;
-}
-
.msg-row-other {
- flex-direction: row;
+ justify-content: flex-start;
+}
+.msg-row-self {
+ justify-content: flex-end;
}
-/* 头像 */
-.msg-avatar-wrap {
- flex-shrink: 0;
- margin-right: 16rpx;
-}
+/* ===== 头像 ===== */
+.avatar-wrap { flex-shrink: 0; }
+.msg-row-other .avatar-wrap { margin-right: 16rpx; }
+.msg-row-self .avatar-wrap { margin-left: 16rpx; }
-.msg-avatar {
+.avatar-img {
width: 72rpx;
height: 72rpx;
border-radius: 18rpx;
}
-
-.msg-avatar-placeholder {
- background-color: #2563EB;
+.avatar-placeholder {
display: flex;
align-items: center;
justify-content: center;
}
+.avatar-peer { background-color: #2563EB; }
+.avatar-self { background-color: #64748B; }
+.avatar-char { color: #FFFFFF; font-size: 28rpx; font-weight: 600; }
-.msg-avatar-text {
- color: #FFFFFF;
- font-size: 28rpx;
- font-weight: 600;
-}
-
-/* 气泡 */
-.msg-bubble {
- max-width: 560rpx;
+/* ===== 气泡 ===== */
+.bubble {
+ max-width: 65vw;
padding: 20rpx 24rpx;
border-radius: 24rpx;
- word-break: break-all;
+ word-break: break-word;
+ overflow-wrap: break-word;
}
-
.bubble-self {
background-color: #2563EB;
border-bottom-right-radius: 8rpx;
- margin-left: 16rpx;
}
-
.bubble-other {
background-color: #FFFFFF;
border-bottom-left-radius: 8rpx;
}
-
.bubble-recalled {
background-color: transparent !important;
padding: 8rpx 16rpx;
}
-.msg-text {
- font-size: 30rpx;
- line-height: 42rpx;
-}
+.msg-text { font-size: 30rpx; line-height: 42rpx; color: #1E293B; }
+.msg-text-self { color: #FFFFFF; }
+.recalled-text { font-size: 24rpx; color: #94A3B8; font-style: italic; }
-.bubble-self .msg-text {
- color: #FFFFFF;
-}
-
-.bubble-other .msg-text {
- color: #1E293B;
-}
-
-.msg-recalled {
- font-size: 24rpx;
- color: #94A3B8;
- font-style: italic;
-}
-
-/* 发送状态 */
+/* ===== 发送状态 ===== */
.msg-status {
display: flex;
align-items: center;
- margin: 0 8rpx;
align-self: center;
+ margin: 0 8rpx;
}
-
.msg-status-tap {
min-width: 44rpx;
min-height: 44rpx;
@@ -441,7 +383,7 @@ export default {
justify-content: center;
}
-/* 输入栏 */
+/* ===== 输入栏 ===== */
.input-bar {
display: flex;
align-items: center;
@@ -450,7 +392,6 @@ export default {
background-color: #FFFFFF;
border-top: 1rpx solid #E2E8F0;
}
-
.input-wrap {
flex: 1;
background-color: #F1F5F9;
@@ -460,13 +401,7 @@ export default {
display: flex;
align-items: center;
}
-
-.msg-input {
- flex: 1;
- font-size: 28rpx;
- color: #1E293B;
-}
-
+.msg-input { flex: 1; font-size: 28rpx; color: #1E293B; }
.send-btn {
min-width: 72rpx;
min-height: 72rpx;
@@ -478,12 +413,6 @@ export default {
justify-content: center;
transition: background-color 200ms ease;
}
-
-.send-btn-active {
- background-color: #2563EB;
-}
-
-.send-btn:active {
- opacity: 0.85;
-}
+.send-btn-active { background-color: #2563EB; }
+.send-btn:active { opacity: 0.85; }
diff --git a/frontend/src/store/chat.js b/frontend/src/store/chat.js
index 9456605..74362f7 100644
--- a/frontend/src/store/chat.js
+++ b/frontend/src/store/chat.js
@@ -16,6 +16,7 @@ import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import imApi from '@/api/im'
import wsService from '@/services/websocket'
+import { useUserStore } from '@/store/user'
export const useChatStore = defineStore('chat', () => {
/** 会话列表 */
@@ -83,11 +84,12 @@ export const useChatStore = defineStore('chat', () => {
const sendMessage = (params) => {
const clientMsgId = _generateClientMsgId()
const { conversationId, targetUserId, content, type = 1 } = params
+ const userStore = useUserStore()
const tempMsg = {
id: 0,
conversation_id: conversationId || 0,
- sender_id: 0,
+ sender_id: Number(userStore.userInfo?.id) || 0,
type,
content,
status: 1,
@@ -263,6 +265,11 @@ export const useChatStore = defineStore('chat', () => {
if (code === 0) {
pending.status = 'sent'
const convId = data.conversation_id
+
+ if (pending.tempMsg && pending.tempMsg.conversation_id === 0 && convId) {
+ currentConversationId.value = convId
+ }
+
const messages = messagesMap.value[convId]
if (messages) {
const idx = messages.findIndex(m => m.client_msg_id === data.client_msg_id)