feat: tighten desktop presence loop and refine admin-web UX
Desktop Client Build / Resolve Build Metadata (push) Successful in 43s
Frontend CI / Frontend (push) Successful in 3m46s
Backend CI / Backend (push) Failing after 7m52s
Desktop Client Build / Build Desktop Client (push) Successful in 27m13s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 59s
Desktop Client Build / Resolve Build Metadata (push) Successful in 43s
Frontend CI / Frontend (push) Successful in 3m46s
Backend CI / Backend (push) Failing after 7m52s
Desktop Client Build / Build Desktop Client (push) Successful in 27m13s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 59s
- Shrink desktop presence TTL to 30s and heartbeat to 15s so unexpected client exits surface within one TTL even if the explicit offline call is missed; keep migration default aligned. - Trust a known presence miss in resolveDesktopClientOnline instead of falling back to the recent-heartbeat heuristic, so a still-cached LastSeenAt cannot mask an offline client. - Release the runtime session before clearing renderer desktop sessions on shutdown to avoid leaving stale leases behind. - Auto-refresh the MediaView account list every 5s and revamp card layout (inline platform badge, status tags, UID row, meta box). - Let the brand-question AI wizard accept multiple seed topics via a tag-style input; candidates from each topic are merged and deduped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,9 @@ const platformsQuery = useQuery({
|
||||
const accountsQuery = useQuery({
|
||||
queryKey: ['tenant', 'desktop-accounts', 'media-view'],
|
||||
queryFn: () => tenantAccountsApi.list(),
|
||||
staleTime: 0,
|
||||
refetchInterval: 5_000,
|
||||
refetchOnWindowFocus: true,
|
||||
})
|
||||
|
||||
const loading = computed(() => platformsQuery.isPending.value || accountsQuery.isPending.value)
|
||||
@@ -374,57 +377,47 @@ async function refreshAll(): Promise<void> {
|
||||
</span>
|
||||
|
||||
<div class="media-card__identity-copy">
|
||||
<h3>{{ account.displayName }}</h3>
|
||||
<p>{{ account.platformLabel }}</p>
|
||||
<h3>
|
||||
{{ account.displayName }}
|
||||
<img
|
||||
v-if="account.platformLogoUrl"
|
||||
:src="account.platformLogoUrl"
|
||||
:alt="account.platformLabel"
|
||||
class="media-card__inline-platform-icon"
|
||||
/>
|
||||
<span v-else class="media-card__inline-platform-text" :style="{ color: account.platformAccent }">{{ account.platformShortName }}</span>
|
||||
</h3>
|
||||
<p>
|
||||
<span class="media-card__platform-name">{{ account.platformLabel }}</span>
|
||||
<span class="media-card__uid-divider">|</span>
|
||||
<span class="media-card__uid">UID: {{ account.platformUid }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="media-card__platform-badge" :style="{ color: account.platformAccent }">
|
||||
<img
|
||||
v-if="account.platformLogoUrl"
|
||||
:src="account.platformLogoUrl"
|
||||
:alt="account.platformLabel"
|
||||
/>
|
||||
<span v-else>{{ account.platformShortName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="media-card__meta">
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">平台 UID</span>
|
||||
<span class="meta-value">{{ account.platformUid }}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">授权状态</span>
|
||||
<a-tag :color="healthColor(account.health)">
|
||||
{{ healthLabel(account.health) }}
|
||||
<div class="media-card__tags">
|
||||
<a-tag :color="healthColor(account.health)">
|
||||
{{ healthLabel(account.health) }}
|
||||
</a-tag>
|
||||
<a-tag :color="publishStateColor(account.publishState)">
|
||||
{{ publishStateLabel(account.publishState) }}
|
||||
</a-tag>
|
||||
<a-tooltip :title="clientStatusText(account)" placement="top">
|
||||
<a-tag :color="clientStatusColor(account)" class="client-status-tag">
|
||||
{{ clientStatusText(account) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="media-card__meta-box">
|
||||
<div class="meta-box-item">
|
||||
<span class="meta-label">最近校验</span>
|
||||
<span class="meta-value">
|
||||
{{ account.verifiedAt ? formatDateTime(account.verifiedAt) : '--' }}
|
||||
</span>
|
||||
<span class="meta-value">{{ account.verifiedAt ? formatDateTime(account.verifiedAt) : '--' }}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">客户端状态</span>
|
||||
<a-tooltip :title="clientStatusText(account)" placement="topLeft">
|
||||
<a-tag :color="clientStatusColor(account)" class="client-status-tag">
|
||||
{{ clientStatusText(account) }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<div class="meta-box-item">
|
||||
<span class="meta-label">最近心跳</span>
|
||||
<span class="meta-value">
|
||||
{{ account.clientLastSeenAt ? formatDateTime(account.clientLastSeenAt) : '--' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">发布状态</span>
|
||||
<a-tag :color="publishStateColor(account.publishState)">
|
||||
{{ publishStateLabel(account.publishState) }}
|
||||
</a-tag>
|
||||
<span class="meta-value">{{ account.clientLastSeenAt ? formatDateTime(account.clientLastSeenAt) : '--' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -564,43 +557,47 @@ async function refreshAll(): Promise<void> {
|
||||
}
|
||||
|
||||
.media-card {
|
||||
border: 1px solid #e6edf5;
|
||||
border-radius: 16px;
|
||||
padding: 18px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(31, 92, 255, 0.06), transparent 28%),
|
||||
linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
gap: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.media-card:hover {
|
||||
box-shadow: 0 12px 28px -6px rgba(0, 0, 0, 0.05);
|
||||
border-color: transparent;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.media-card__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.media-card__identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.media-card__avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 14px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.media-card__avatar img {
|
||||
@@ -615,68 +612,93 @@ async function refreshAll(): Promise<void> {
|
||||
|
||||
.media-card__identity-copy h3 {
|
||||
margin: 0;
|
||||
color: #1a1a1a;
|
||||
font-size: 15px;
|
||||
color: #1e293b;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.media-card__inline-platform-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.media-card__inline-platform-text {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
background: #f8fafc;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.media-card__identity-copy p {
|
||||
margin: 4px 0 0;
|
||||
color: #8c8c8c;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.media-card__platform-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.media-card__uid-divider {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.media-card__uid {
|
||||
color: #94a3b8;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.media-card__platform-badge {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 12px;
|
||||
background: #f7f9fc;
|
||||
border: 1px solid #e6edf5;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
.media-card__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.media-card__platform-badge img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
.media-card__tags :deep(.ant-tag) {
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.media-card__meta {
|
||||
.media-card__meta-box {
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
.meta-box-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
color: #1a1a1a;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.client-status-tag {
|
||||
max-width: 160px;
|
||||
max-width: 140px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
Reference in New Issue
Block a user