This commit is contained in:
xuncha
2026-01-28 20:27:19 +08:00
8 changed files with 698 additions and 28 deletions

View File

@@ -739,6 +739,59 @@
cursor: zoom-in;
}
.live-badge {
position: absolute;
top: 6px;
right: 6px;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
color: white;
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
border-radius: 4px;
display: flex;
align-items: center;
gap: 2px;
pointer-events: none;
z-index: 2;
transition: opacity 0.2s;
}
.download-btn-overlay {
position: absolute;
bottom: 6px;
right: 6px;
width: 28px;
height: 28px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transform: translateY(10px);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 2;
&:hover {
background: rgba(0, 0, 0, 0.7);
transform: scale(1.1);
border-color: rgba(255, 255, 255, 0.8);
}
}
&:hover {
.download-btn-overlay {
opacity: 1;
transform: translateY(0);
}
}
.media-error-placeholder {
position: absolute;
inset: 0;
@@ -937,4 +990,197 @@
transform: scale(1);
opacity: 1;
}
}
// Debug Dialog Styles
.debug-btn {
margin-left: auto;
background: transparent;
border: 1px solid var(--border-color);
color: var(--text-secondary);
padding: 6px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&:hover {
background: var(--hover-bg);
color: var(--accent-color);
border-color: var(--accent-color);
}
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
backdrop-filter: blur(4px);
}
.debug-dialog {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
width: 90%;
max-width: 800px;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
.debug-dialog-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.close-btn {
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
border-radius: 4px;
transition: all 0.2s;
&:hover {
background: var(--hover-bg);
color: var(--accent-color);
}
}
}
.debug-dialog-body {
flex: 1;
overflow-y: auto;
padding: 20px;
.debug-section {
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border-color);
&:last-child {
border-bottom: none;
}
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: var(--accent-color);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.debug-item {
display: flex;
gap: 12px;
padding: 8px 0;
align-items: flex-start;
.debug-key {
font-weight: 500;
color: var(--text-secondary);
min-width: 140px;
font-size: 13px;
font-family: 'Consolas', 'Microsoft YaHei', 'SimHei', monospace;
}
.debug-value {
flex: 1;
color: var(--text-primary);
font-size: 13px;
word-break: break-all;
font-family: 'Consolas', 'Microsoft YaHei', 'SimHei', monospace;
user-select: text;
cursor: text;
padding: 2px 0;
}
}
.media-debug-item {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
.media-debug-header {
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color);
}
.live-photo-debug {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed var(--border-color);
.live-photo-label {
font-weight: 500;
color: var(--accent-color);
margin-bottom: 8px;
font-size: 13px;
}
}
}
.json-code {
background: var(--bg-tertiary);
color: var(--text-primary);
padding: 16px;
border-radius: 8px;
border: 1px solid var(--border-color);
overflow-x: auto;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
line-height: 1.5;
user-select: all;
max-height: 400px;
overflow-y: auto;
}
.copy-json-btn {
margin-top: 12px;
padding: 8px 16px;
background: var(--accent-color);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
&:hover {
background: var(--accent-hover);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(var(--accent-color-rgb), 0.3);
}
}
}
}
}

View File

@@ -1,5 +1,5 @@
import { useEffect, useState, useRef, useCallback, useMemo } from 'react'
import { RefreshCw, Heart, Search, Calendar, User, X, Filter, Play, ImageIcon } from 'lucide-react'
import { useEffect, useState, useRef, useCallback, useMemo } from 'react'
import { RefreshCw, Heart, Search, Calendar, User, X, Filter, Play, ImageIcon, Zap, Download } from 'lucide-react'
import { Avatar } from '../components/Avatar'
import { ImagePreview } from '../components/ImagePreview'
import JumpToDateDialog from '../components/JumpToDateDialog'
@@ -13,29 +13,65 @@ interface SnsPost {
createTime: number
contentDesc: string
type?: number
media: { url: string; thumb: string }[]
media: {
url: string
thumb: string
md5?: string
token?: string
key?: string
encIdx?: string
livePhoto?: {
url: string
thumb: string
token?: string
key?: string
encIdx?: string
}
}[]
likes: string[]
comments: { id: string; nickname: string; content: string; refCommentId: string; refNickname?: string }[]
rawXml?: string // 原始 XML 数据
}
const MediaItem = ({ url, thumb, onPreview }: { url: string, thumb: string, onPreview: () => void }) => {
const MediaItem = ({ media, onPreview }: { media: any, onPreview: () => void }) => {
const [error, setError] = useState(false);
const { url, thumb, livePhoto } = media;
const isLive = !!livePhoto;
const targetUrl = thumb || url;
const handleDownload = (e: React.MouseEvent) => {
e.stopPropagation();
let downloadUrl = url;
let downloadKey = media.key || '';
if (isLive && media.livePhoto) {
downloadUrl = media.livePhoto.url;
downloadKey = media.livePhoto.key || '';
}
// TODO: 调用后端下载服务
// window.electronAPI.sns.download(downloadUrl, downloadKey);
};
return (
<div className={`media-item ${error ? 'error' : ''}`}>
{!error ? (
<img
src={thumb || url}
alt=""
loading="lazy"
onClick={onPreview}
onError={() => setError(true)}
/>
) : (
<div className="media-error-placeholder" onClick={onPreview}>
<ImageIcon size={24} style={{ opacity: 0.3 }} />
<div className={`media-item ${error ? 'error' : ''}`} onClick={onPreview}>
<img
src={targetUrl}
alt=""
referrerPolicy="no-referrer"
loading="lazy"
onError={() => setError(true)}
/>
{isLive && (
<div className="live-badge">
<Zap size={10} fill="currentColor" />
<span>LIVE</span>
</div>
)}
<button className="download-btn-overlay" onClick={handleDownload} title="下载原图">
<Download size={14} />
</button>
</div>
);
};
@@ -65,6 +101,7 @@ export default function SnsPage() {
const [showJumpDialog, setShowJumpDialog] = useState(false)
const [jumpTargetDate, setJumpTargetDate] = useState<Date | undefined>(undefined)
const [previewImage, setPreviewImage] = useState<string | null>(null)
const [debugPost, setDebugPost] = useState<SnsPost | null>(null)
const postsContainerRef = useRef<HTMLDivElement>(null)
@@ -264,7 +301,7 @@ export default function SnsPage() {
setHasNewer(false)
setSelectedUsernames([])
setSearchKeyword('')
setJumpTargetDate(null)
setJumpTargetDate(undefined)
loadContacts()
loadPosts({ reset: true })
}
@@ -515,6 +552,19 @@ export default function SnsPage() {
<div className="nickname">{post.nickname}</div>
<div className="time">{formatTime(post.createTime)}</div>
</div>
<button
className="debug-btn"
onClick={(e) => {
e.stopPropagation();
setDebugPost(post);
}}
title="查看原始数据"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="16 18 22 12 16 6"></polyline>
<polyline points="8 6 2 12 8 18"></polyline>
</svg>
</button>
</div>
<div className="post-body">
@@ -528,7 +578,7 @@ export default function SnsPage() {
) : post.media.length > 0 && (
<div className={`post-media-grid media-count-${Math.min(post.media.length, 9)}`}>
{post.media.map((m, idx) => (
<MediaItem key={idx} url={m.url} thumb={m.thumb} onPreview={() => setPreviewImage(m.url)} />
<MediaItem key={idx} media={m} onPreview={() => setPreviewImage(m.url)} />
))}
</div>
)}
@@ -605,6 +655,154 @@ export default function SnsPage() {
}}
currentDate={jumpTargetDate || new Date()}
/>
{/* Debug Info Dialog */}
{debugPost && (
<div className="modal-overlay" onClick={() => setDebugPost(null)}>
<div className="debug-dialog" onClick={(e) => e.stopPropagation()}>
<div className="debug-dialog-header">
<h3> - {debugPost.nickname}</h3>
<button className="close-btn" onClick={() => setDebugPost(null)}>
<X size={20} />
</button>
</div>
<div className="debug-dialog-body">
<div className="debug-section">
<h4> </h4>
<div className="debug-item">
<span className="debug-key">ID:</span>
<span className="debug-value">{debugPost.id}</span>
</div>
<div className="debug-item">
<span className="debug-key">:</span>
<span className="debug-value">{debugPost.username}</span>
</div>
<div className="debug-item">
<span className="debug-key">:</span>
<span className="debug-value">{debugPost.nickname}</span>
</div>
<div className="debug-item">
<span className="debug-key">:</span>
<span className="debug-value">{new Date(debugPost.createTime * 1000).toLocaleString()}</span>
</div>
<div className="debug-item">
<span className="debug-key">:</span>
<span className="debug-value">{debugPost.type}</span>
</div>
</div>
<div className="debug-section">
<h4> ({debugPost.media.length} )</h4>
{debugPost.media.map((media, idx) => (
<div key={idx} className="media-debug-item">
<div className="media-debug-header"> {idx + 1}</div>
<div className="debug-item">
<span className="debug-key">URL:</span>
<span className="debug-value">{media.url}</span>
</div>
<div className="debug-item">
<span className="debug-key">:</span>
<span className="debug-value">{media.thumb}</span>
</div>
{media.md5 && (
<div className="debug-item">
<span className="debug-key">MD5:</span>
<span className="debug-value">{media.md5}</span>
</div>
)}
{media.token && (
<div className="debug-item">
<span className="debug-key">Token:</span>
<span className="debug-value">{media.token}</span>
</div>
)}
{media.key && (
<div className="debug-item">
<span className="debug-key">Key ():</span>
<span className="debug-value">{media.key}</span>
</div>
)}
{media.encIdx && (
<div className="debug-item">
<span className="debug-key">Enc Index:</span>
<span className="debug-value">{media.encIdx}</span>
</div>
)}
{media.livePhoto && (
<div className="live-photo-debug">
<div className="live-photo-label"> Live Photo :</div>
<div className="debug-item">
<span className="debug-key"> URL:</span>
<span className="debug-value">{media.livePhoto.url}</span>
</div>
<div className="debug-item">
<span className="debug-key">:</span>
<span className="debug-value">{media.livePhoto.thumb}</span>
</div>
{media.livePhoto.token && (
<div className="debug-item">
<span className="debug-key"> Token:</span>
<span className="debug-value">{media.livePhoto.token}</span>
</div>
)}
{media.livePhoto.key && (
<div className="debug-item">
<span className="debug-key"> Key:</span>
<span className="debug-value">{media.livePhoto.key}</span>
</div>
)}
</div>
)}
</div>
))}
</div>
{/* 原始 XML */}
{debugPost.rawXml && (
<div className="debug-section">
<h4> XML </h4>
<pre className="json-code">{(() => {
// XML 缩进格式化
let formatted = '';
let indent = 0;
const tab = ' ';
const parts = debugPost.rawXml.split(/(<[^>]+>)/g).filter(p => p.trim());
for (const part of parts) {
if (!part.startsWith('<')) {
if (part.trim()) formatted += part;
continue;
}
if (part.startsWith('</')) {
indent = Math.max(0, indent - 1);
formatted += '\n' + tab.repeat(indent) + part;
} else if (part.endsWith('/>')) {
formatted += '\n' + tab.repeat(indent) + part;
} else {
formatted += '\n' + tab.repeat(indent) + part;
indent++;
}
}
return formatted.trim();
})()}</pre>
<button
className="copy-json-btn"
onClick={() => {
navigator.clipboard.writeText(debugPost.rawXml || '');
alert('已复制 XML 到剪贴板');
}}
>
XML
</button>
</div>
)}
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -349,6 +349,8 @@ export interface ElectronAPI {
}>
error?: string
}>
debugResource: (url: string) => Promise<{ success: boolean; status?: number; headers?: any; error?: string }>
proxyImage: (url: string) => Promise<{ success: boolean; dataUrl?: string; error?: string }>
}
}