feat(export): refine top card copy and sns header count

This commit is contained in:
tisonhuang
2026-03-04 15:28:07 +08:00
parent 55f7ff1842
commit fb1125136c
2 changed files with 155 additions and 94 deletions

View File

@@ -320,6 +320,13 @@
font-weight: 600; font-weight: 600;
} }
.card-title-meta {
color: var(--text-secondary);
font-size: 12px;
white-space: nowrap;
font-weight: 500;
}
.card-refresh-hint { .card-refresh-hint {
color: var(--text-tertiary); color: var(--text-tertiary);
font-size: 11px; font-size: 11px;
@@ -1108,6 +1115,8 @@
} }
.table-wrap { .table-wrap {
--contacts-message-col-width: 92px;
--contacts-action-col-width: 172px;
overflow: hidden; overflow: hidden;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 10px; border-radius: 10px;
@@ -1227,6 +1236,37 @@
word-break: break-word; word-break: break-word;
} }
.contacts-list-header {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px 8px;
border-bottom: 1px solid color-mix(in srgb, var(--border-color) 85%, transparent);
background: color-mix(in srgb, var(--bg-primary) 78%, var(--bg-secondary));
font-size: 12px;
color: var(--text-tertiary);
font-weight: 600;
letter-spacing: 0.01em;
flex-shrink: 0;
}
.contacts-list-header-main {
flex: 1;
min-width: 0;
}
.contacts-list-header-count {
width: var(--contacts-message-col-width);
text-align: right;
flex-shrink: 0;
}
.contacts-list-header-actions {
width: var(--contacts-action-col-width);
text-align: right;
flex-shrink: 0;
}
.contacts-list { .contacts-list {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@@ -1335,21 +1375,15 @@
} }
.row-message-count { .row-message-count {
min-width: 82px; width: var(--contacts-message-col-width);
min-width: var(--contacts-message-col-width);
display: flex; display: flex;
flex-direction: column;
align-items: flex-end; align-items: flex-end;
gap: 2px; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
text-align: right; text-align: right;
} }
.row-message-count-label {
font-size: 11px;
color: var(--text-tertiary);
line-height: 1;
}
.row-message-count-value { .row-message-count-value {
margin: 0; margin: 0;
font-size: 13px; font-size: 13px;
@@ -1504,6 +1538,8 @@
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
gap: 4px; gap: 4px;
width: var(--contacts-action-col-width);
flex-shrink: 0;
.row-action-main { .row-action-main {
display: inline-flex; display: inline-flex;
@@ -2280,12 +2316,22 @@
} }
@media (max-width: 720px) { @media (max-width: 720px) {
.table-wrap .row-message-count { .table-wrap {
min-width: 66px; --contacts-message-col-width: 66px;
--contacts-action-col-width: 148px;
} }
.table-wrap .row-message-count-label { .table-wrap .contacts-list-header {
display: none; gap: 8px;
padding: 8px 10px 6px;
}
.table-wrap .contacts-list {
padding: 0 10px 10px;
}
.table-wrap .row-message-count {
min-width: var(--contacts-message-col-width);
} }
.diag-panel-header { .diag-panel-header {

View File

@@ -2610,7 +2610,7 @@ function ExportPage() {
...item, ...item,
label: contentTypeLabels[item.type], label: contentTypeLabels[item.type],
stats: [ stats: [
{ label: '已导出', value: exported } { label: '已导出', value: exported, unit: '个对话' }
] ]
} }
}) })
@@ -2619,9 +2619,9 @@ function ExportPage() {
type: 'sns' as ContentCardType, type: 'sns' as ContentCardType,
icon: Aperture, icon: Aperture,
label: '朋友圈', label: '朋友圈',
headerCount: snsStats.totalPosts,
stats: [ stats: [
{ label: '朋友圈条数', value: snsStats.totalPosts }, { label: '已导出', value: snsExportedCount, unit: '条' }
{ label: '已导出', value: snsExportedCount }
] ]
} }
@@ -3661,6 +3661,15 @@ function ExportPage() {
<div key={card.type} className="content-card"> <div key={card.type} className="content-card">
<div className="card-header"> <div className="card-header">
<div className="card-title"><Icon size={16} /> {card.label}</div> <div className="card-title"><Icon size={16} /> {card.label}</div>
{card.type === 'sns' && (
<div className="card-title-meta">
{isCardStatsLoading ? (
<span className="count-loading">
<span className="animated-ellipsis" aria-hidden="true">...</span>
</span>
) : `${card.headerCount.toLocaleString()}`}
</div>
)}
</div> </div>
<div className="card-stats"> <div className="card-stats">
{card.stats.map((stat) => ( {card.stats.map((stat) => (
@@ -3671,7 +3680,7 @@ function ExportPage() {
<span className="count-loading"> <span className="count-loading">
<span className="animated-ellipsis" aria-hidden="true">...</span> <span className="animated-ellipsis" aria-hidden="true">...</span>
</span> </span>
) : stat.value.toLocaleString()} ) : `${stat.value.toLocaleString()} ${stat.unit}`}
</strong> </strong>
</div> </div>
))} ))}
@@ -3951,87 +3960,93 @@ function ExportPage() {
<span></span> <span></span>
</div> </div>
) : ( ) : (
<div className="contacts-list" ref={contactsListRef} onScroll={onContactsListScroll}> <>
<div <div className="contacts-list-header">
className="contacts-list-virtual" <span className="contacts-list-header-main">//</span>
style={{ height: filteredContacts.length * CONTACTS_LIST_VIRTUAL_ROW_HEIGHT }} <span className="contacts-list-header-count"></span>
> <span className="contacts-list-header-actions"></span>
{visibleContacts.map((contact, idx) => { </div>
const absoluteIndex = contactStartIndex + idx <div className="contacts-list" ref={contactsListRef} onScroll={onContactsListScroll}>
const top = absoluteIndex * CONTACTS_LIST_VIRTUAL_ROW_HEIGHT <div
const matchedSession = sessionRowByUsername.get(contact.username) className="contacts-list-virtual"
const canExport = Boolean(matchedSession?.hasSession) style={{ height: filteredContacts.length * CONTACTS_LIST_VIRTUAL_ROW_HEIGHT }}
const isRunning = canExport && runningSessionIds.has(contact.username) >
const isQueued = canExport && queuedSessionIds.has(contact.username) {visibleContacts.map((contact, idx) => {
const isPaused = canExport && pausedSessionIds.has(contact.username) const absoluteIndex = contactStartIndex + idx
const recent = canExport ? formatRecentExportTime(lastExportBySession[contact.username], nowTick) : '' const top = absoluteIndex * CONTACTS_LIST_VIRTUAL_ROW_HEIGHT
const countedMessages = normalizeMessageCount(sessionMessageCounts[contact.username]) const matchedSession = sessionRowByUsername.get(contact.username)
const hintedMessages = normalizeMessageCount(matchedSession?.messageCountHint) const canExport = Boolean(matchedSession?.hasSession)
const displayedMessageCount = countedMessages ?? hintedMessages const isRunning = canExport && runningSessionIds.has(contact.username)
const messageCountLabel = !canExport const isQueued = canExport && queuedSessionIds.has(contact.username)
? '--' const isPaused = canExport && pausedSessionIds.has(contact.username)
: typeof displayedMessageCount === 'number' const recent = canExport ? formatRecentExportTime(lastExportBySession[contact.username], nowTick) : ''
? displayedMessageCount.toLocaleString('zh-CN') const countedMessages = normalizeMessageCount(sessionMessageCounts[contact.username])
: (isLoadingSessionCounts ? '统计中…' : '--') const hintedMessages = normalizeMessageCount(matchedSession?.messageCountHint)
return ( const displayedMessageCount = countedMessages ?? hintedMessages
<div const messageCountLabel = !canExport
key={contact.username} ? '--'
className="contact-row" : typeof displayedMessageCount === 'number'
style={{ transform: `translateY(${top}px)` }} ? displayedMessageCount.toLocaleString('zh-CN')
> : (isLoadingSessionCounts ? '统计中…' : '--')
<div className="contact-item"> return (
<div className="contact-avatar"> <div
{contact.avatarUrl ? ( key={contact.username}
<img src={contact.avatarUrl} alt="" loading="lazy" /> className="contact-row"
) : ( style={{ transform: `translateY(${top}px)` }}
<span>{getAvatarLetter(contact.displayName)}</span> >
)} <div className="contact-item">
</div> <div className="contact-avatar">
<div className="contact-info"> {contact.avatarUrl ? (
<div className="contact-name">{contact.displayName}</div> <img src={contact.avatarUrl} alt="" loading="lazy" />
<div className="contact-remark">{contact.username}</div> ) : (
</div> <span>{getAvatarLetter(contact.displayName)}</span>
<div className="row-message-count"> )}
<span className="row-message-count-label"></span> </div>
<strong className={`row-message-count-value ${typeof displayedMessageCount === 'number' ? '' : 'muted'}`}> <div className="contact-info">
{messageCountLabel} <div className="contact-name">{contact.displayName}</div>
</strong> <div className="contact-remark">{contact.username}</div>
</div> </div>
<div className="row-action-cell"> <div className="row-message-count">
<div className="row-action-main"> <strong className={`row-message-count-value ${typeof displayedMessageCount === 'number' ? '' : 'muted'}`}>
<button {messageCountLabel}
className={`row-detail-btn ${showSessionDetailPanel && sessionDetail?.wxid === contact.username ? 'active' : ''}`} </strong>
onClick={() => openSessionDetail(contact.username)} </div>
> <div className="row-action-cell">
<div className="row-action-main">
</button> <button
<button className={`row-detail-btn ${showSessionDetailPanel && sessionDetail?.wxid === contact.username ? 'active' : ''}`}
className={`row-export-btn ${isRunning ? 'running' : ''} ${isPaused ? 'paused' : ''} ${!canExport ? 'no-session' : ''}`} onClick={() => openSessionDetail(contact.username)}
disabled={!canExport || isRunning || isPaused} >
onClick={() => {
if (!matchedSession || !matchedSession.hasSession) return </button>
openSingleExport({ <button
...matchedSession, className={`row-export-btn ${isRunning ? 'running' : ''} ${isPaused ? 'paused' : ''} ${!canExport ? 'no-session' : ''}`}
displayName: contact.displayName || matchedSession.displayName || matchedSession.username disabled={!canExport || isRunning || isPaused}
}) onClick={() => {
}} if (!matchedSession || !matchedSession.hasSession) return
> openSingleExport({
{isRunning ? ( ...matchedSession,
<> displayName: contact.displayName || matchedSession.displayName || matchedSession.username
<Loader2 size={14} className="spin" /> })
}}
</> >
) : !canExport ? '暂无会话' : isPaused ? '已暂停' : isQueued ? '排队中' : '导出'} {isRunning ? (
</button> <>
<Loader2 size={14} className="spin" />
</>
) : !canExport ? '暂无会话' : isPaused ? '已暂停' : isQueued ? '排队中' : '导出'}
</button>
</div>
{recent && <span className="row-export-time">{recent}</span>}
</div> </div>
{recent && <span className="row-export-time">{recent}</span>}
</div> </div>
</div> </div>
</div> )
) })}
})} </div>
</div> </div>
</div> </>
)} )}
</div> </div>