feat: improve friend circle media and detail rendering
This commit is contained in:
@@ -19,6 +19,8 @@ def _safe_text(value, default=""):
|
||||
if value is None:
|
||||
return default
|
||||
if isinstance(value, dict):
|
||||
if "buffer" in value:
|
||||
return _safe_text(value.get("buffer"), default)
|
||||
if "string" in value:
|
||||
return _safe_text(value.get("string"), default)
|
||||
if "text" in value:
|
||||
@@ -50,13 +52,43 @@ def _extract_list(values):
|
||||
return []
|
||||
|
||||
|
||||
def _find_timeline_xml(value):
|
||||
if isinstance(value, str):
|
||||
return value if "<TimelineObject" in value else ""
|
||||
if isinstance(value, dict):
|
||||
for key in ("buffer", "string", "text", "Content", "Xml", "ObjectDesc", "TimelineObject"):
|
||||
if key in value:
|
||||
result = _find_timeline_xml(value.get(key))
|
||||
if result:
|
||||
return result
|
||||
for item in value.values():
|
||||
result = _find_timeline_xml(item)
|
||||
if result:
|
||||
return result
|
||||
elif isinstance(value, list):
|
||||
for item in value:
|
||||
result = _find_timeline_xml(item)
|
||||
if result:
|
||||
return result
|
||||
return ""
|
||||
|
||||
|
||||
def _parse_media_from_xml(root):
|
||||
media_items = []
|
||||
for media in root.findall(".//mediaList/media"):
|
||||
media_type = _safe_text(media.findtext("type"))
|
||||
size_node = media.find("size")
|
||||
media_items.append({
|
||||
"url": _safe_text(media.findtext("url")),
|
||||
"thumb": _safe_text(media.findtext("thumb")),
|
||||
"id": _safe_text(media.findtext("id"))
|
||||
"hd": _safe_text(media.findtext("hd")),
|
||||
"id": _safe_text(media.findtext("id")),
|
||||
"type": media_type,
|
||||
"is_video": media_type == "6",
|
||||
"video_duration": _safe_text(media.findtext("videoDuration")),
|
||||
"width": _safe_text(size_node.get("width")) if size_node is not None else "",
|
||||
"height": _safe_text(size_node.get("height")) if size_node is not None else "",
|
||||
"description": _safe_text(media.findtext("description"))
|
||||
})
|
||||
return [item for item in media_items if item.get("url") or item.get("thumb")]
|
||||
|
||||
@@ -72,12 +104,24 @@ def _parse_timeline_xml(xml_text):
|
||||
if create_time.isdigit():
|
||||
timestamp = datetime.fromtimestamp(int(create_time)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
location_node = root.find("location")
|
||||
location = {}
|
||||
if location_node is not None:
|
||||
location = {
|
||||
"poi_name": _safe_text(location_node.get("poiName")),
|
||||
"poi_address": _safe_text(location_node.get("poiAddress")),
|
||||
"city": _safe_text(location_node.get("city")),
|
||||
"latitude": _safe_text(location_node.get("latitude")),
|
||||
"longitude": _safe_text(location_node.get("longitude"))
|
||||
}
|
||||
|
||||
return {
|
||||
"id": _safe_text(root.findtext("id")),
|
||||
"author_wxid": _safe_text(root.findtext("username")),
|
||||
"content": _safe_text(root.findtext("contentDesc")),
|
||||
"timestamp": timestamp,
|
||||
"media": _parse_media_from_xml(root)
|
||||
"media": _parse_media_from_xml(root),
|
||||
"location": location
|
||||
}
|
||||
|
||||
|
||||
@@ -91,11 +135,13 @@ def _normalize_comment(comment):
|
||||
"comment_id": comment_id,
|
||||
"author_wxid": (
|
||||
_safe_text(comment.get("UserName"))
|
||||
or _safe_text(comment.get("Username"))
|
||||
or _safe_text(comment.get("username"))
|
||||
or _safe_text(comment.get("Wxid"))
|
||||
),
|
||||
"author_name": (
|
||||
_safe_text(comment.get("NickName"))
|
||||
or _safe_text(comment.get("Nickname"))
|
||||
or _safe_text(comment.get("nickname"))
|
||||
or _safe_text(comment.get("DisplayName"))
|
||||
),
|
||||
@@ -103,7 +149,8 @@ def _normalize_comment(comment):
|
||||
_safe_text(comment.get("Content"))
|
||||
or _safe_text(comment.get("content"))
|
||||
or _safe_text(comment.get("Text"))
|
||||
)
|
||||
),
|
||||
"create_time": _safe_text(comment.get("CreateTime")) or _safe_text(comment.get("createTime"))
|
||||
}
|
||||
|
||||
|
||||
@@ -125,9 +172,8 @@ def _normalize_like(like):
|
||||
def _normalize_post(node, server):
|
||||
xml_payload = ""
|
||||
for key in ("Content", "Xml", "ObjectDesc", "TimelineObject"):
|
||||
value = node.get(key)
|
||||
if isinstance(value, str) and "<TimelineObject" in value:
|
||||
xml_payload = value
|
||||
xml_payload = _find_timeline_xml(node.get(key))
|
||||
if xml_payload:
|
||||
break
|
||||
|
||||
xml_data = _parse_timeline_xml(xml_payload) if xml_payload else {}
|
||||
@@ -144,6 +190,7 @@ def _normalize_post(node, server):
|
||||
)
|
||||
author_name = (
|
||||
_safe_text(node.get("NickName"))
|
||||
or _safe_text(node.get("Nickname"))
|
||||
or _safe_text(node.get("nickname"))
|
||||
or server.contact_manager.get_nickname(author_wxid)
|
||||
)
|
||||
@@ -170,8 +217,15 @@ def _normalize_post(node, server):
|
||||
for item in media_list:
|
||||
media.append({
|
||||
"url": _safe_text(item.get("Url")) or _safe_text(item.get("url")),
|
||||
"hd": _safe_text(item.get("Hd")) or _safe_text(item.get("hd")),
|
||||
"thumb": _safe_text(item.get("ThumbUrl")) or _safe_text(item.get("thumb")),
|
||||
"id": _safe_text(item.get("Id")) or _safe_text(item.get("id"))
|
||||
"id": _safe_text(item.get("Id")) or _safe_text(item.get("id")),
|
||||
"type": _safe_text(item.get("Type")) or _safe_text(item.get("type")),
|
||||
"is_video": (_safe_text(item.get("Type")) or _safe_text(item.get("type"))) == "6",
|
||||
"video_duration": _safe_text(item.get("VideoDuration")) or _safe_text(item.get("videoDuration")),
|
||||
"width": _safe_text(item.get("Width")) or _safe_text(item.get("width")),
|
||||
"height": _safe_text(item.get("Height")) or _safe_text(item.get("height")),
|
||||
"description": _safe_text(item.get("Description")) or _safe_text(item.get("description"))
|
||||
})
|
||||
if not media:
|
||||
media = xml_data.get("media", [])
|
||||
@@ -180,6 +234,8 @@ def _normalize_post(node, server):
|
||||
like_nodes = []
|
||||
for key in ("CommentList", "Comments", "commentList", "comments"):
|
||||
comment_nodes.extend(_extract_list(node.get(key)))
|
||||
for key in ("CommentUserList", "commentUserList"):
|
||||
comment_nodes.extend(_extract_list(node.get(key)))
|
||||
for key in ("LikeList", "Likes", "likeList", "likes"):
|
||||
like_nodes.extend(_extract_list(node.get(key)))
|
||||
|
||||
@@ -190,6 +246,7 @@ def _normalize_post(node, server):
|
||||
"author_avatar": server.contact_manager.get_head_image(author_wxid),
|
||||
"content": content,
|
||||
"timestamp": timestamp,
|
||||
"location": xml_data.get("location", {}),
|
||||
"media": [item for item in media if item.get("url") or item.get("thumb")],
|
||||
"comments": [item for item in (_normalize_comment(x) for x in comment_nodes) if item.get("content") or item.get("author_wxid")],
|
||||
"likes": [item for item in (_normalize_like(x) for x in like_nodes) if item.get("author_wxid")],
|
||||
@@ -204,7 +261,7 @@ def _extract_posts(payload, server):
|
||||
if not isinstance(node, dict):
|
||||
continue
|
||||
looks_like_post = any(key in node for key in ("Id", "id", "ContentDesc", "CommentList", "LikeList", "ObjectDesc"))
|
||||
has_timeline_xml = any(isinstance(value, str) and "<TimelineObject" in value for value in node.values())
|
||||
has_timeline_xml = bool(_find_timeline_xml(node))
|
||||
if not looks_like_post and not has_timeline_xml:
|
||||
continue
|
||||
post = _normalize_post(node, server)
|
||||
|
||||
@@ -90,9 +90,25 @@
|
||||
|
||||
<div class="post-content">{% raw %}{{ post.content || '这条朋友圈没有解析出文字内容' }}{% endraw %}</div>
|
||||
|
||||
<div v-if="post.location && (post.location.poi_name || post.location.poi_address || post.location.city)" class="post-location">
|
||||
<i class="el-icon-location-outline"></i>
|
||||
<span>
|
||||
{% raw %}{{ post.location.poi_name || post.location.poi_address || post.location.city }}{% endraw %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="post.media && post.media.length" class="post-media-grid">
|
||||
<div v-for="(media, index) in post.media" :key="`${post.id}-${index}`" class="post-media-item">
|
||||
<img :src="media.thumb || media.url" :alt="post.content || '朋友圈图片'" @click="previewImage(media)">
|
||||
<div
|
||||
v-for="(media, index) in post.media"
|
||||
:key="`${post.id}-${index}`"
|
||||
class="post-media-item"
|
||||
:class="{ 'is-video': media.is_video }"
|
||||
@click="openMediaPreview(media)">
|
||||
<img :src="media.thumb || media.hd || media.url" :alt="post.content || '朋友圈媒体'">
|
||||
<div v-if="media.is_video" class="video-badge">
|
||||
<i class="el-icon-video-play"></i>
|
||||
<span>{% raw %}{{ formatVideoDuration(media.video_duration) }}{% endraw %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -195,7 +211,16 @@
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog :visible.sync="imagePreviewVisible" width="70%" class="image-preview-dialog">
|
||||
<img v-if="previewImageUrl" :src="previewImageUrl" class="large-preview" alt="朋友圈图片预览">
|
||||
<img v-if="previewMedia && !previewMedia.is_video && previewImageUrl" :src="previewImageUrl" class="large-preview" alt="朋友圈图片预览">
|
||||
<video
|
||||
v-if="previewMedia && previewMedia.is_video"
|
||||
:src="previewMedia.url"
|
||||
:poster="previewMedia.thumb || previewMedia.hd || previewMedia.url"
|
||||
class="large-video"
|
||||
controls
|
||||
playsinline
|
||||
webkit-playsinline>
|
||||
</video>
|
||||
</el-dialog>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -229,6 +254,7 @@
|
||||
rawDialogVisible: false,
|
||||
imagePreviewVisible: false,
|
||||
previewImageUrl: '',
|
||||
previewMedia: null,
|
||||
rawDialogContent: '',
|
||||
selectedPost: null,
|
||||
commentForm: {
|
||||
@@ -373,10 +399,18 @@
|
||||
this.rawDialogContent = JSON.stringify(post.raw || post, null, 2);
|
||||
this.rawDialogVisible = true;
|
||||
},
|
||||
previewImage(media) {
|
||||
this.previewImageUrl = media.url || media.thumb || '';
|
||||
openMediaPreview(media) {
|
||||
this.previewMedia = media;
|
||||
this.previewImageUrl = media.hd || media.url || media.thumb || '';
|
||||
this.imagePreviewVisible = true;
|
||||
},
|
||||
formatVideoDuration(value) {
|
||||
const seconds = Math.round(parseFloat(value || 0));
|
||||
if (!seconds || Number.isNaN(seconds)) return '视频';
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}:${String(secs).padStart(2, '0')}`;
|
||||
},
|
||||
resetFilters() {
|
||||
this.viewMode = 'home';
|
||||
this.selectedTowxid = '';
|
||||
@@ -413,11 +447,28 @@
|
||||
.post-meta { font-size: 12px; color: #64748b; margin-top: 3px; }
|
||||
.post-actions { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
|
||||
.post-content { color: #0f172a; line-height: 1.8; margin-bottom: 14px; white-space: pre-wrap; word-break: break-word; }
|
||||
.post-location {
|
||||
display: inline-flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 8px 12px;
|
||||
border-radius: 999px; background: rgba(14,165,233,0.08); color: #0369a1; font-size: 13px; font-weight: 600;
|
||||
border: 1px solid rgba(14,165,233,0.12);
|
||||
}
|
||||
.post-media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(108px, 1fr)); gap: 10px; margin-bottom: 14px; }
|
||||
.post-media-item img, .upload-preview-item img {
|
||||
width: 100%; height: 108px; object-fit: cover; border-radius: 16px; cursor: pointer;
|
||||
border: 1px solid rgba(148,163,184,0.14); box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
.post-media-item { position: relative; }
|
||||
.post-media-item.is-video::after {
|
||||
content: ''; position: absolute; inset: 0; border-radius: 16px;
|
||||
background: linear-gradient(180deg, rgba(15,23,42,0.02), rgba(15,23,42,0.3));
|
||||
pointer-events: none;
|
||||
}
|
||||
.video-badge {
|
||||
position: absolute; left: 10px; bottom: 10px; display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 6px 10px; border-radius: 999px; background: rgba(15,23,42,0.72); color: #fff;
|
||||
font-size: 12px; font-weight: 600; z-index: 2;
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.18);
|
||||
}
|
||||
.post-feedback { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.feedback-block {
|
||||
padding: 14px; border-radius: 18px; background: rgba(248,250,252,0.88);
|
||||
@@ -448,6 +499,7 @@
|
||||
border: 1px solid rgba(148,163,184,0.12);
|
||||
}
|
||||
.large-preview { width: 100%; border-radius: 18px; }
|
||||
.large-video { width: 100%; max-height: 75vh; border-radius: 18px; background: #000; }
|
||||
@media (max-width: 1100px) {
|
||||
.friend-circle-grid { grid-template-columns: 1fr; }
|
||||
.post-feedback { grid-template-columns: 1fr; }
|
||||
|
||||
Reference in New Issue
Block a user