feat(chat): 聊天排版升级微信标准(左右分侧+头像+绿气泡)+is_send数据修复
- is_send 根因:云客消息方向在 mine 字段(isSend 全 null),归一化加 fallback +存量回填 60.7 万行(51万收/16万发)——分侧数据首次完整 - MessageBubble:头像(对方=raw headUrl 在左/自己=默认在右)、自己侧隐藏昵称、 接收白底描边/发送微信绿 #95ec69、气泡列 max-width 78% - 实测混合对话:13发(绿+头像右)/7收(白+头像左)全部正确 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -655,7 +655,10 @@ async def scrape_messages(cookie_str: str, max_conversations: int = 100):
|
|||||||
# 用发送的 friend_id(与客户表一致)优先,API 返回的 talker 可能脱敏
|
# 用发送的 friend_id(与客户表一致)优先,API 返回的 talker 可能脱敏
|
||||||
"talker_id": friend_id or row.get("talker") or row.get("oriTalker"),
|
"talker_id": friend_id or row.get("talker") or row.get("oriTalker"),
|
||||||
"room_id": None,
|
"room_id": None,
|
||||||
"is_send": bool(row.get("isSend", 0)) if row.get("isSend") is not None else None,
|
"is_send": (
|
||||||
|
bool(row["isSend"]) if row.get("isSend") is not None
|
||||||
|
else (bool(row["mine"]) if row.get("mine") is not None else None)
|
||||||
|
),
|
||||||
"type": _normalize_msg_type(row.get("type")),
|
"type": _normalize_msg_type(row.get("type")),
|
||||||
"type_raw": row.get("type"),
|
"type_raw": row.get("type"),
|
||||||
"text": row.get("content") or row.get("text") or "",
|
"text": row.get("content") or row.get("text") or "",
|
||||||
@@ -744,7 +747,10 @@ def _normalize_group_message(row: dict, owner_id: str, room_id: str) -> dict | N
|
|||||||
"owner_account_id": owner_id,
|
"owner_account_id": owner_id,
|
||||||
"talker_id": talker or None,
|
"talker_id": talker or None,
|
||||||
"room_id": row.get("roomid") or room_id,
|
"room_id": row.get("roomid") or room_id,
|
||||||
"is_send": bool(row.get("isSend", 0)) if row.get("isSend") is not None else None,
|
"is_send": (
|
||||||
|
bool(row["isSend"]) if row.get("isSend") is not None
|
||||||
|
else (bool(row["mine"]) if row.get("mine") is not None else None)
|
||||||
|
),
|
||||||
"type": _normalize_msg_type(row.get("type")),
|
"type": _normalize_msg_type(row.get("type")),
|
||||||
"type_raw": row.get("type"),
|
"type_raw": row.get("type"),
|
||||||
"text": row.get("content") or row.get("text") or "",
|
"text": row.get("content") or row.get("text") or "",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { Microphone, VideoPlay, Document, Picture, Paperclip } from '@element-plus/icons-vue'
|
import { Microphone, VideoPlay, Document, Picture, Paperclip, User } from '@element-plus/icons-vue'
|
||||||
import type { Message } from '@/api/types'
|
import type { Message } from '@/api/types'
|
||||||
import { formatDuration, formatTime, wechatTypeLabel } from '@/utils/format'
|
import { formatDuration, formatTime, wechatTypeLabel } from '@/utils/format'
|
||||||
|
|
||||||
@@ -37,6 +37,13 @@ const emit = defineEmits<{ (e: 'open-detail', message: Message): void }>()
|
|||||||
|
|
||||||
const isSend = computed(() => !!props.message.is_send)
|
const isSend = computed(() => !!props.message.is_send)
|
||||||
|
|
||||||
|
/** 对方头像(raw_json.headUrl);自己侧用默认占位(headUrl 语义不保证是自己)。 */
|
||||||
|
const peerAvatarUrl = computed(() => {
|
||||||
|
const rj = props.message.raw_json as Record<string, unknown> | null | undefined
|
||||||
|
const u = rj?.headUrl
|
||||||
|
return typeof u === 'string' && u.startsWith('http') ? u : undefined
|
||||||
|
})
|
||||||
|
|
||||||
// Centered (system/divider) rows ignore direction.
|
// Centered (system/divider) rows ignore direction.
|
||||||
const isCentered = computed(
|
const isCentered = computed(
|
||||||
() => props.message.type === 'divider' || props.message.type === 'system',
|
() => props.message.type === 'divider' || props.message.type === 'system',
|
||||||
@@ -187,8 +194,17 @@ const typeTagLabel = computed(() => {
|
|||||||
children (image / video / audio / file link) stop propagation so
|
children (image / video / audio / file link) stop propagation so
|
||||||
their own clicks don't also open the drawer. -->
|
their own clicks don't also open the drawer. -->
|
||||||
<div v-else class="yk-msg__row yk-msg__row--clickable" @click="emit('open-detail', message)">
|
<div v-else class="yk-msg__row yk-msg__row--clickable" @click="emit('open-detail', message)">
|
||||||
|
<!-- 头像:对方=raw headUrl;自己=默认占位(row-reverse 时视觉在右) -->
|
||||||
|
<div class="yk-msg__avatar-wrap">
|
||||||
|
<img v-if="!isSend && peerAvatarUrl" class="yk-msg__avatar" :src="peerAvatarUrl" alt="" />
|
||||||
|
<div v-else class="yk-msg__avatar yk-msg__avatar--ph">
|
||||||
|
<el-icon :size="20"><User /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="yk-msg__body">
|
||||||
<div class="yk-msg__meta">
|
<div class="yk-msg__meta">
|
||||||
<span>{{ senderLabel }}</span>
|
<span v-if="!isSend">{{ senderLabel }}</span>
|
||||||
<span v-if="timeLabel">{{ timeLabel }}</span>
|
<span v-if="timeLabel">{{ timeLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -299,6 +315,7 @@ const typeTagLabel = computed(() => {
|
|||||||
<el-icon><Picture /></el-icon>
|
<el-icon><Picture /></el-icon>
|
||||||
[{{ typeTagLabel }}]
|
[{{ typeTagLabel }}]
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+41
-7
@@ -142,7 +142,7 @@ a {
|
|||||||
.yk-msg {
|
.yk-msg {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 6px 0;
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.yk-msg--send {
|
.yk-msg--send {
|
||||||
@@ -151,14 +151,46 @@ a {
|
|||||||
|
|
||||||
.yk-msg__row {
|
.yk-msg__row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
max-width: 78%;
|
||||||
max-width: 70%;
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.yk-msg--send .yk-msg__row {
|
.yk-msg--send .yk-msg__row {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yk-msg__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yk-msg--send .yk-msg__body {
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 头像(微信式):对方在左/自己在右(row-reverse),小圆角方形 */
|
||||||
|
.yk-msg__avatar-wrap {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yk-msg__avatar {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
background: #f2f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yk-msg__avatar--ph {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #a8abb2;
|
||||||
|
}
|
||||||
|
|
||||||
/* Clickable bubble row — opens the all-fields drawer. Subtle hover so users
|
/* Clickable bubble row — opens the all-fields drawer. Subtle hover so users
|
||||||
* discover the interaction without changing the bubble's own look. */
|
* discover the interaction without changing the bubble's own look. */
|
||||||
.yk-msg__row--clickable {
|
.yk-msg__row--clickable {
|
||||||
@@ -186,8 +218,9 @@ a {
|
|||||||
|
|
||||||
.yk-msg__bubble {
|
.yk-msg__bubble {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
background: #f4f4f5;
|
background: #ffffff;
|
||||||
|
border: 1px solid #e5e6eb;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
@@ -195,8 +228,9 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.yk-msg--send .yk-msg__bubble {
|
.yk-msg--send .yk-msg__bubble {
|
||||||
background: var(--el-color-primary-light-8, #d9ecff);
|
background: #95ec69;
|
||||||
color: #1d2129;
|
border-color: #95ec69;
|
||||||
|
color: #0f170a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.yk-msg__image {
|
.yk-msg__image {
|
||||||
|
|||||||
Reference in New Issue
Block a user