fix(desktop): unify log and network layouts

This commit is contained in:
apale7 2026-04-20 03:46:30 +08:00
parent 08343ad576
commit a82754eb40
4 changed files with 104 additions and 114 deletions

View File

@ -1643,27 +1643,18 @@ export default function App() {
) : null} ) : null}
{activeTab === 'log' ? ( {activeTab === 'log' ? (
<section className="tab-layout trace-layout"> <section className="tab-layout aliases-layout">
<article className="panel trace-list-panel"> <article className="panel panel-fill list-column">
<div className="panel-header"> <div className="panel-header">
<div> <div>
<h3>{t('log.title')}</h3> <h3>{t('log.title')}</h3>
<p className="subtle"> <p className="subtle">{traceStatus || t('log.subtitle')}</p>
{logDetailOpen && selectedLogTrace
? `${t('log.detailTitle')}: #${selectedLogTrace.id}`
: traceStatus || t('log.subtitle')}
</p>
</div> </div>
<div className="toolbar toolbar-end"> <div className="list-header-actions">
<span className="subtle">{t('log.count', { count: requestTraces.length })}</span> <span className="subtle list-status-text">{t('log.count', { count: requestTraces.length })}</span>
{logDetailOpen ? (
<button type="button" onClick={closeLogDetail}>
{t('actions.close')}
</button>
) : null}
</div> </div>
</div> </div>
<div className="trace-list"> <div className="scroll-list compact-list trace-scroll-list">
{requestTraces.length === 0 ? ( {requestTraces.length === 0 ? (
<article className="empty-card compact-empty"> <article className="empty-card compact-empty">
<h4>{t('log.empty')}</h4> <h4>{t('log.empty')}</h4>
@ -1671,28 +1662,50 @@ export default function App() {
</article> </article>
) : null} ) : null}
{requestTraces.map((trace) => ( {requestTraces.map((trace) => (
<button <article
key={trace.id} key={trace.id}
type="button" className={`resource-card ${logDetailOpen && selectedLogTrace?.id === trace.id ? 'active' : ''}`}
className={`trace-row ${logDetailOpen && selectedLogTrace?.id === trace.id ? 'active' : ''}`} role="button"
tabIndex={0}
onClick={() => setSelectedLogTraceId(trace.id)} onClick={() => setSelectedLogTraceId(trace.id)}
onKeyDown={(event) => onResourceCardKeyDown(event, () => setSelectedLogTraceId(trace.id))}
> >
<div className="trace-row-top"> <div className="resource-card-top">
<strong>{trace.alias || trace.rawModel || `#${trace.id}`}</strong> <div className="resource-card-heading">
<span className={`badge ${trace.success ? 'live' : 'idle'}`}> <div className="resource-card-titlewrap">
<strong className="resource-card-title">{trace.alias || trace.rawModel || `#${trace.id}`}</strong>
<code className="resource-card-code">#{trace.id}</code>
</div>
<p className="resource-card-subtitle trace-card-subtitle">
<span>{tracePrimaryText(trace)}</span>
<span>{formatDuration(trace.durationMs)}</span>
</p>
</div>
<div className="resource-card-side">
<span className={`badge status-badge ${trace.success ? 'live' : 'idle'}`}>
{trace.success ? t('log.success') : t('log.failed')} {trace.success ? t('log.success') : t('log.failed')}
</span> </span>
</div> </div>
<div className="trace-row-summary">
<span>{tracePrimaryText(trace)}</span>
<span>{formatDuration(trace.durationMs)}</span>
</div> </div>
<div className="inline-pills"> <div className="resource-card-meta">
<span className="pill">#{trace.id}</span> <div className="resource-meta-item">
<span className="pill">{formatDateTime(trace.startedAt)}</span> <span className="resource-meta-label">{t('log.startedAt')}</span>
{trace.failover ? <span className="pill trace-pill-warn">{t('log.failover')}</span> : null} <span className="resource-meta-value">{formatDateTime(trace.startedAt)}</span>
</div> </div>
</button> <div className="resource-meta-item">
<span className="resource-meta-label">{t('log.firstByte')}</span>
<span className="resource-meta-value">{formatDuration(trace.firstByteMs)}</span>
</div>
<div className="resource-meta-item">
<span className="resource-meta-label">{t('log.chainTitle')}</span>
<span className="resource-meta-value">
{trace.failover
? `${trace.attemptCount} · ${t('log.failover')}`
: `${trace.attemptCount}`}
</span>
</div>
</div>
</article>
))} ))}
</div> </div>
</article> </article>
@ -1700,26 +1713,18 @@ export default function App() {
) : null} ) : null}
{activeTab === 'network' ? ( {activeTab === 'network' ? (
<section className="tab-layout trace-layout"> <section className="tab-layout aliases-layout">
<article className="panel trace-list-panel"> <article className="panel panel-fill list-column">
<div className="panel-header"> <div className="panel-header">
<div> <div>
<h3>{t('network.title')}</h3> <h3>{t('network.title')}</h3>
<p className="subtle"> <p className="subtle">{traceStatus || t('network.subtitle')}</p>
{networkDetailOpen && selectedNetworkTrace
? `${t('network.detailTitle')}: #${selectedNetworkTrace.id}`
: traceStatus || t('network.subtitle')}
</p>
</div> </div>
<div className="toolbar toolbar-end"> <div className="list-header-actions">
{networkDetailOpen ? ( <span className="subtle list-status-text">{t('network.count', { count: requestTraces.length })}</span>
<button type="button" onClick={closeNetworkDetail}>
{t('actions.close')}
</button>
) : null}
</div> </div>
</div> </div>
<div className="trace-list"> <div className="scroll-list compact-list trace-scroll-list">
{requestTraces.length === 0 ? ( {requestTraces.length === 0 ? (
<article className="empty-card compact-empty"> <article className="empty-card compact-empty">
<h4>{t('log.empty')}</h4> <h4>{t('log.empty')}</h4>
@ -1727,21 +1732,44 @@ export default function App() {
</article> </article>
) : null} ) : null}
{requestTraces.map((trace) => ( {requestTraces.map((trace) => (
<button <article
key={trace.id} key={trace.id}
type="button" className={`resource-card ${networkDetailOpen && selectedNetworkTrace?.id === trace.id ? 'active' : ''}`}
className={`trace-row ${networkDetailOpen && selectedNetworkTrace?.id === trace.id ? 'active' : ''}`} role="button"
tabIndex={0}
onClick={() => setSelectedNetworkTraceId(trace.id)} onClick={() => setSelectedNetworkTraceId(trace.id)}
onKeyDown={(event) => onResourceCardKeyDown(event, () => setSelectedNetworkTraceId(trace.id))}
> >
<div className="trace-row-top"> <div className="resource-card-top">
<strong>#{trace.id}</strong> <div className="resource-card-heading">
<span className={`badge ${trace.success ? 'live' : 'idle'}`}>{trace.statusCode || '-'}</span> <div className="resource-card-titlewrap">
<strong className="resource-card-title">#{trace.id}</strong>
<code className="resource-card-code">{trace.finalProvider || trace.alias || trace.rawModel || '-'}</code>
</div> </div>
<div className="trace-row-summary"> <p className="resource-card-subtitle trace-card-subtitle">
<span>{trace.finalUrl || tracePrimaryText(trace)}</span> <span>{trace.finalUrl || tracePrimaryText(trace)}</span>
<span>{formatDuration(trace.firstByteMs)}</span> <span>{formatDuration(trace.firstByteMs)}</span>
</p>
</div> </div>
</button> <div className="resource-card-side">
<span className={`badge status-badge ${trace.success ? 'live' : 'idle'}`}>{trace.statusCode || '-'}</span>
</div>
</div>
<div className="resource-card-meta">
<div className="resource-meta-item">
<span className="resource-meta-label">{t('log.startedAt')}</span>
<span className="resource-meta-value">{formatDateTime(trace.startedAt)}</span>
</div>
<div className="resource-meta-item">
<span className="resource-meta-label">{t('network.totalTime')}</span>
<span className="resource-meta-value">{formatDuration(trace.durationMs)}</span>
</div>
<div className="resource-meta-item">
<span className="resource-meta-label">{t('log.chainTitle')}</span>
<span className="resource-meta-value">{trace.attemptCount}</span>
</div>
</div>
</article>
))} ))}
</div> </div>
</article> </article>
@ -2571,7 +2599,7 @@ export default function App() {
<div className="chain-list"> <div className="chain-list">
{(selectedLogTrace.attempts || []).map((attempt) => ( {(selectedLogTrace.attempts || []).map((attempt) => (
<article className="chain-card" key={`${selectedLogTrace.id}-${attempt.attempt}-${attempt.provider}-${attempt.model}`}> <article className="chain-card" key={`${selectedLogTrace.id}-${attempt.attempt}-${attempt.provider}-${attempt.model}`}>
<div className="trace-row-top"> <div className="item-heading">
<strong>{t('log.attemptLabel', { attempt: attempt.attempt })}</strong> <strong>{t('log.attemptLabel', { attempt: attempt.attempt })}</strong>
<span className={`badge ${attempt.success ? 'live' : attempt.skipped ? 'outline' : 'idle'}`}> <span className={`badge ${attempt.success ? 'live' : attempt.skipped ? 'outline' : 'idle'}`}>
{attempt.result || '-'} {attempt.result || '-'}

View File

@ -271,6 +271,7 @@
"network": { "network": {
"title": "Network detail", "title": "Network detail",
"subtitle": "Inspect URL, timing, status, headers, and params without exposing model IO content.", "subtitle": "Inspect URL, timing, status, headers, and params without exposing model IO content.",
"count": "{{count}} requests",
"detailTitle": "Network inspector", "detailTitle": "Network inspector",
"detailSubtitle": "Each attempt shows technical metadata captured around the upstream call.", "detailSubtitle": "Each attempt shows technical metadata captured around the upstream call.",
"url": "URL", "url": "URL",

View File

@ -271,6 +271,7 @@
"network": { "network": {
"title": "网络详情", "title": "网络详情",
"subtitle": "查看 URL、时延、状态码、请求头与参数同时避免暴露模型输入输出正文。", "subtitle": "查看 URL、时延、状态码、请求头与参数同时避免暴露模型输入输出正文。",
"count": "共 {{count}} 条请求",
"detailTitle": "网络检查器", "detailTitle": "网络检查器",
"detailSubtitle": "每次尝试都会展示调用上游时采集到的技术元数据。", "detailSubtitle": "每次尝试都会展示调用上游时采集到的技术元数据。",
"url": "URL", "url": "URL",

View File

@ -503,15 +503,10 @@ dd {
} }
.sync-layout, .sync-layout,
.trace-layout,
.settings-layout { .settings-layout {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.trace-layout {
grid-template-columns: minmax(300px, 0.78fr) minmax(0, 1.22fr);
}
.settings-layout { .settings-layout {
grid-template-columns: minmax(0, 1.7fr) minmax(280px, 0.85fr); grid-template-columns: minmax(0, 1.7fr) minmax(280px, 0.85fr);
align-items: start; align-items: start;
@ -532,45 +527,6 @@ dd {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
.trace-list-panel {
min-width: 0;
}
.trace-list {
display: grid;
gap: 0.85rem;
max-height: calc(100vh - 18rem);
overflow: auto;
padding-right: 0.2rem;
}
.trace-row {
display: grid;
gap: 0.6rem;
width: 100%;
padding: 0.9rem;
border-radius: 18px;
text-align: left;
background: var(--panel-muted);
}
.trace-row.active {
border-color: var(--accent);
box-shadow: var(--shadow-sm);
}
.trace-row-top,
.trace-row-summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.trace-row-summary {
color: var(--text-muted);
font-size: 0.9rem;
}
.trace-hero { .trace-hero {
display: grid; display: grid;
@ -602,11 +558,6 @@ dd {
gap: 0.8rem; gap: 0.8rem;
} }
.trace-pill-warn {
background: var(--warning-soft);
color: var(--warning);
}
.compact-header { .compact-header {
margin-bottom: 0; margin-bottom: 0;
} }
@ -841,6 +792,12 @@ html[data-theme='dark'] button.ghost-danger:hover {
gap: 0.8rem; gap: 0.8rem;
} }
.trace-scroll-list {
max-height: none;
padding-right: 0;
overflow: visible;
}
.resource-card { .resource-card {
display: grid; display: grid;
gap: 0.88rem; gap: 0.88rem;
@ -979,6 +936,17 @@ html[data-theme='dark'] button.ghost-danger:hover {
line-height: 1.45; line-height: 1.45;
} }
.trace-card-subtitle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.trace-card-subtitle span:last-child {
flex-shrink: 0;
}
.detail-panel { .detail-panel {
display: grid; display: grid;
align-content: start; align-content: start;
@ -1564,7 +1532,6 @@ html[data-theme='dark'] .detail-empty-card {
.overview-layout, .overview-layout,
.sync-layout, .sync-layout,
.trace-layout,
.providers-layout, .providers-layout,
.aliases-layout, .aliases-layout,
.stats-grid, .stats-grid,
@ -1622,7 +1589,6 @@ html[data-theme='dark'] .detail-empty-card {
.nav-list, .nav-list,
.overview-layout, .overview-layout,
.sync-layout, .sync-layout,
.trace-layout,
.settings-layout, .settings-layout,
.stats-grid, .stats-grid,
.card-grid-list, .card-grid-list,
@ -1660,12 +1626,6 @@ html[data-theme='dark'] .detail-empty-card {
min-height: auto; min-height: auto;
} }
.trace-row-top,
.trace-row-summary {
flex-direction: column;
align-items: stretch;
}
.detail-backdrop { .detail-backdrop {
align-items: stretch; align-items: stretch;
justify-content: stretch; justify-content: stretch;