2026-04-03 00:39:15 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-04-20 09:52:48 +08:00
|
|
|
|
import { ReloadOutlined, SearchOutlined } from "@ant-design/icons-vue";
|
|
|
|
|
|
import { useQuery } from "@tanstack/vue-query";
|
|
|
|
|
|
import type { DesktopAccountInfo, MediaPlatform } from "@geo/shared-types";
|
|
|
|
|
|
import { computed, ref } from "vue";
|
2026-04-03 00:39:15 +08:00
|
|
|
|
import { useI18n } from "vue-i18n";
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
import { mediaApi, resolveApiURL, tenantAccountsApi } from "@/lib/api";
|
2026-04-27 21:34:05 +08:00
|
|
|
|
import { resolveAccountCheckedAt, resolveAccountHealth } from "@/lib/desktop-account-runtime";
|
2026-04-03 00:39:15 +08:00
|
|
|
|
import { formatDateTime } from "@/lib/display";
|
2026-04-20 15:44:51 +08:00
|
|
|
|
import { getPublishPlatformMeta, isPublishPlatformId, normalizePublishPlatformId } from "@/lib/publish-platforms";
|
2026-04-20 09:52:48 +08:00
|
|
|
|
|
|
|
|
|
|
type AccountHealthFilter = "all" | "live" | "captcha" | "risk" | "expired";
|
|
|
|
|
|
type PublishState = "immediate" | "queued" | "unavailable";
|
|
|
|
|
|
|
|
|
|
|
|
interface MediaAccountCard {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
platform: string;
|
|
|
|
|
|
platformLabel: string;
|
|
|
|
|
|
platformShortName: string;
|
|
|
|
|
|
platformAccent: string;
|
|
|
|
|
|
platformLogoUrl: string | null;
|
|
|
|
|
|
displayName: string;
|
|
|
|
|
|
platformUid: string;
|
|
|
|
|
|
avatarUrl: string | null;
|
|
|
|
|
|
health: DesktopAccountInfo["health"];
|
|
|
|
|
|
verifiedAt: string | null;
|
|
|
|
|
|
clientId: string | null;
|
|
|
|
|
|
clientOnline: boolean | null;
|
|
|
|
|
|
clientDeviceName: string | null;
|
|
|
|
|
|
clientLastSeenAt: string | null;
|
|
|
|
|
|
publishState: PublishState;
|
|
|
|
|
|
publishHint: string;
|
|
|
|
|
|
deleteRequestedAt: string | null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const platformLogoCatalog: Record<string, string> = {
|
|
|
|
|
|
toutiaohao: "/logos/logo_toutiao.png",
|
|
|
|
|
|
baijiahao: "/logos/logo_baijiahao.png",
|
|
|
|
|
|
sohuhao: "/logos/logo_souhu.png",
|
|
|
|
|
|
qiehao: "/logos/logo_qiehao.png",
|
|
|
|
|
|
zhihu: "/logos/logo_zhihu.png",
|
|
|
|
|
|
wangyihao: "/logos/logo_wangyihao.png",
|
|
|
|
|
|
jianshu: "/logos/logo_jianshu.svg",
|
|
|
|
|
|
bilibili: "/logos/logo_bilibili.png",
|
|
|
|
|
|
juejin: "/logos/logo_juejin.png",
|
|
|
|
|
|
smzdm: "/logos/logo_smzdm.svg",
|
|
|
|
|
|
weixin_gzh: "/logos/logo_weixin_gzh.svg",
|
|
|
|
|
|
zol: "/logos/logo_zol.png",
|
|
|
|
|
|
dongchedi: "/logos/logo_dongchedi.svg",
|
|
|
|
|
|
};
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
|
|
|
|
|
const { t } = useI18n();
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
const searchQuery = ref("");
|
|
|
|
|
|
const selectedHealth = ref<AccountHealthFilter>("all");
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
|
|
|
|
|
const platformsQuery = useQuery({
|
2026-04-20 09:52:48 +08:00
|
|
|
|
queryKey: ["media", "platforms", "media-view"],
|
2026-04-03 00:39:15 +08:00
|
|
|
|
queryFn: () => mediaApi.platforms(),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const accountsQuery = useQuery({
|
2026-04-20 09:52:48 +08:00
|
|
|
|
queryKey: ["tenant", "desktop-accounts", "media-view"],
|
|
|
|
|
|
queryFn: () => tenantAccountsApi.list(),
|
2026-04-03 00:39:15 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
const loading = computed(() => platformsQuery.isPending.value || accountsQuery.isPending.value);
|
|
|
|
|
|
const refreshing = computed(() => platformsQuery.isFetching.value || accountsQuery.isFetching.value);
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
const platformMap = computed(() => {
|
|
|
|
|
|
return new Map(
|
|
|
|
|
|
(platformsQuery.data.value ?? []).map((platform: MediaPlatform) => [
|
|
|
|
|
|
normalizePublishPlatformId(platform.platform_id),
|
|
|
|
|
|
platform,
|
|
|
|
|
|
]),
|
|
|
|
|
|
);
|
2026-04-03 00:39:15 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
const mediaAccounts = computed<MediaAccountCard[]>(() => {
|
|
|
|
|
|
return [...(accountsQuery.data.value ?? [])]
|
|
|
|
|
|
.filter((account) => !account.deleted_at)
|
2026-04-20 15:44:51 +08:00
|
|
|
|
.filter((account) => isPublishPlatformId(account.platform))
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.map((account) => {
|
|
|
|
|
|
const platformId = normalizePublishPlatformId(account.platform);
|
|
|
|
|
|
const platform = platformMap.value.get(platformId);
|
|
|
|
|
|
const fallback = getPublishPlatformMeta(platformId);
|
|
|
|
|
|
const publishState = resolvePublishState(account);
|
2026-04-27 21:34:05 +08:00
|
|
|
|
const health = resolveAccountHealth(account);
|
2026-04-20 09:52:48 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: account.id,
|
|
|
|
|
|
platform: platformId,
|
|
|
|
|
|
platformLabel: platform?.name || fallback.name,
|
|
|
|
|
|
platformShortName: platform?.short_name || fallback.shortName,
|
|
|
|
|
|
platformAccent: platform?.accent_color || fallback.accent,
|
|
|
|
|
|
platformLogoUrl: resolvePlatformLogoUrl(platformId, platform?.logo_url),
|
|
|
|
|
|
displayName: account.display_name,
|
|
|
|
|
|
platformUid: normalizePlatformUid(account.platform_uid),
|
|
|
|
|
|
avatarUrl: resolveApiURL(account.avatar_url),
|
2026-04-27 21:34:05 +08:00
|
|
|
|
health,
|
|
|
|
|
|
verifiedAt: resolveAccountCheckedAt(account),
|
2026-04-20 09:52:48 +08:00
|
|
|
|
clientId: account.client_id,
|
|
|
|
|
|
clientOnline: account.client_online,
|
|
|
|
|
|
clientDeviceName: account.client_device_name,
|
|
|
|
|
|
clientLastSeenAt: account.client_last_seen_at,
|
|
|
|
|
|
publishState,
|
|
|
|
|
|
publishHint: resolvePublishHint(account, publishState),
|
|
|
|
|
|
deleteRequestedAt: account.delete_requested_at,
|
|
|
|
|
|
};
|
|
|
|
|
|
})
|
|
|
|
|
|
.sort((left, right) => {
|
|
|
|
|
|
const publishGap = publishRank(left.publishState) - publishRank(right.publishState);
|
|
|
|
|
|
if (publishGap !== 0) {
|
|
|
|
|
|
return publishGap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const leftAt = Date.parse(left.verifiedAt ?? "") || 0;
|
|
|
|
|
|
const rightAt = Date.parse(right.verifiedAt ?? "") || 0;
|
|
|
|
|
|
return rightAt - leftAt;
|
|
|
|
|
|
});
|
2026-04-03 00:39:15 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
const overview = computed(() => ({
|
|
|
|
|
|
total: mediaAccounts.value.length,
|
|
|
|
|
|
live: mediaAccounts.value.filter((item) => item.health === "live").length,
|
|
|
|
|
|
immediate: mediaAccounts.value.filter((item) => item.publishState === "immediate").length,
|
|
|
|
|
|
queued: mediaAccounts.value.filter((item) => item.publishState === "queued").length,
|
|
|
|
|
|
attention: mediaAccounts.value.filter((item) => item.publishState === "unavailable").length,
|
|
|
|
|
|
}));
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
const filteredAccounts = computed(() => {
|
|
|
|
|
|
const keyword = searchQuery.value.trim().toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
return mediaAccounts.value.filter((account) => {
|
|
|
|
|
|
if (selectedHealth.value !== "all" && account.health !== selectedHealth.value) {
|
2026-04-03 00:39:15 +08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-04-20 09:52:48 +08:00
|
|
|
|
|
|
|
|
|
|
if (!keyword) {
|
|
|
|
|
|
return true;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
2026-04-20 09:52:48 +08:00
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
account.displayName,
|
|
|
|
|
|
account.platformLabel,
|
|
|
|
|
|
account.platformUid,
|
|
|
|
|
|
account.clientDeviceName ?? "",
|
|
|
|
|
|
].join(" ").toLowerCase().includes(keyword);
|
2026-04-03 00:39:15 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
function resolvePublishState(account: DesktopAccountInfo): PublishState {
|
2026-04-27 21:34:05 +08:00
|
|
|
|
if (resolveAccountHealth(account) !== "live") {
|
2026-04-20 09:52:48 +08:00
|
|
|
|
return "unavailable";
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!account.client_id) {
|
|
|
|
|
|
return "unavailable";
|
|
|
|
|
|
}
|
|
|
|
|
|
return account.client_online === true ? "immediate" : "queued";
|
|
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
function resolvePublishHint(account: DesktopAccountInfo, publishState: PublishState): string {
|
2026-04-25 18:09:12 +08:00
|
|
|
|
if (publishState === "immediate") {
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
if (publishState === "queued") {
|
|
|
|
|
|
return "客户端当前离线,任务会先进入发布队列,等桌面端上线后自动继续。";
|
|
|
|
|
|
}
|
2026-04-27 21:34:05 +08:00
|
|
|
|
if (resolveAccountHealth(account) !== "live") {
|
2026-04-20 09:52:48 +08:00
|
|
|
|
return "授权状态异常,先在桌面端重新校验后再发布。";
|
|
|
|
|
|
}
|
|
|
|
|
|
return "还没有绑定桌面客户端,当前无法加入发布队列。";
|
|
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
function normalizePlatformUid(value?: string | null): string {
|
|
|
|
|
|
let normalized = String(value ?? "").trim();
|
|
|
|
|
|
while (normalized.toLowerCase().startsWith("platform:")) {
|
|
|
|
|
|
normalized = normalized.slice("platform:".length).trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
return normalized || "--";
|
|
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
function resolvePlatformLogoUrl(platformId: string, remoteLogoUrl?: string | null): string | null {
|
|
|
|
|
|
const localLogoUrl = platformLogoCatalog[platformId];
|
|
|
|
|
|
if (localLogoUrl) {
|
|
|
|
|
|
return localLogoUrl;
|
|
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
const resolvedRemoteLogoUrl = resolveApiURL(remoteLogoUrl);
|
|
|
|
|
|
return resolvedRemoteLogoUrl || null;
|
|
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
function accountInitial(account: MediaAccountCard): string {
|
|
|
|
|
|
const name = account.displayName.trim();
|
|
|
|
|
|
return name ? name.slice(0, 1).toUpperCase() : account.platformShortName;
|
|
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
function publishRank(state: PublishState): number {
|
|
|
|
|
|
switch (state) {
|
|
|
|
|
|
case "immediate":
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
case "queued":
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
default:
|
|
|
|
|
|
return 2;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
2026-04-20 09:52:48 +08:00
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
function healthLabel(health: DesktopAccountInfo["health"]): string {
|
|
|
|
|
|
switch (health) {
|
|
|
|
|
|
case "live":
|
|
|
|
|
|
return "授权正常";
|
|
|
|
|
|
case "captcha":
|
|
|
|
|
|
return "待处理";
|
|
|
|
|
|
case "risk":
|
|
|
|
|
|
return "风险观察";
|
|
|
|
|
|
default:
|
|
|
|
|
|
return "授权异常";
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
2026-04-20 09:52:48 +08:00
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
function healthColor(health: DesktopAccountInfo["health"]): string {
|
|
|
|
|
|
switch (health) {
|
|
|
|
|
|
case "live":
|
|
|
|
|
|
return "green";
|
|
|
|
|
|
case "captcha":
|
|
|
|
|
|
return "orange";
|
|
|
|
|
|
case "risk":
|
|
|
|
|
|
return "gold";
|
|
|
|
|
|
default:
|
|
|
|
|
|
return "red";
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
2026-04-20 09:52:48 +08:00
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
function clientStatusLabel(account: MediaAccountCard): string {
|
|
|
|
|
|
if (!account.clientId) {
|
|
|
|
|
|
return "未绑定";
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
2026-04-20 09:52:48 +08:00
|
|
|
|
return account.clientOnline ? "在线" : "离线";
|
|
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
function clientStatusColor(account: MediaAccountCard): string {
|
|
|
|
|
|
if (!account.clientId) {
|
|
|
|
|
|
return "default";
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
2026-04-20 09:52:48 +08:00
|
|
|
|
return account.clientOnline ? "green" : "gold";
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
function clientStatusText(account: MediaAccountCard): string {
|
|
|
|
|
|
if (!account.clientId) {
|
|
|
|
|
|
return "未绑定桌面客户端";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const device = account.clientDeviceName?.trim();
|
|
|
|
|
|
return device ? `${clientStatusLabel(account)} · ${device}` : clientStatusLabel(account);
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
function publishStateLabel(state: PublishState): string {
|
|
|
|
|
|
switch (state) {
|
|
|
|
|
|
case "immediate":
|
|
|
|
|
|
return "可立即发布";
|
|
|
|
|
|
case "queued":
|
|
|
|
|
|
return "离线排队";
|
|
|
|
|
|
default:
|
|
|
|
|
|
return "不可发布";
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
function publishStateColor(state: PublishState): string {
|
|
|
|
|
|
switch (state) {
|
|
|
|
|
|
case "immediate":
|
|
|
|
|
|
return "green";
|
|
|
|
|
|
case "queued":
|
|
|
|
|
|
return "gold";
|
|
|
|
|
|
default:
|
|
|
|
|
|
return "red";
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
async function refreshAll(): Promise<void> {
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
|
platformsQuery.refetch(),
|
|
|
|
|
|
accountsQuery.refetch(),
|
|
|
|
|
|
]);
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="media-view">
|
|
|
|
|
|
<section class="media-view__top-card">
|
|
|
|
|
|
<div class="media-view__header">
|
|
|
|
|
|
<div class="media-view__header-title">
|
2026-04-20 09:52:48 +08:00
|
|
|
|
<h2>{{ t("route.media.title") }}</h2>
|
|
|
|
|
|
<p>这里展示 `platform_accounts` 里当前用户已绑定的桌面媒体账号,以及它们是否能立即发布、离线排队或需要处理。</p>
|
2026-04-03 00:39:15 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="media-view__header-actions">
|
2026-04-20 09:52:48 +08:00
|
|
|
|
<a-button :loading="refreshing" @click="refreshAll">
|
2026-04-03 00:39:15 +08:00
|
|
|
|
<template #icon><ReloadOutlined /></template>
|
2026-04-20 09:52:48 +08:00
|
|
|
|
刷新状态
|
2026-04-03 00:39:15 +08:00
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-20 09:52:48 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="overview-strip border-t">
|
|
|
|
|
|
<div class="overview-chip">
|
|
|
|
|
|
<span>账号总数</span>
|
|
|
|
|
|
<strong>{{ overview.total }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="overview-chip">
|
|
|
|
|
|
<span>可立即发布</span>
|
|
|
|
|
|
<strong>{{ overview.immediate }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="overview-chip">
|
|
|
|
|
|
<span>离线排队</span>
|
|
|
|
|
|
<strong>{{ overview.queued }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="overview-chip">
|
|
|
|
|
|
<span>需处理</span>
|
|
|
|
|
|
<strong>{{ overview.attention }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-03 00:39:15 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
<section class="panel media-list-panel">
|
|
|
|
|
|
<div class="media-list-toolbar">
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="searchQuery"
|
|
|
|
|
|
placeholder="搜索昵称、UID、平台、客户端"
|
|
|
|
|
|
class="media-search"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<SearchOutlined style="color: #8c8c8c" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-input>
|
|
|
|
|
|
|
|
|
|
|
|
<a-select v-model:value="selectedHealth" style="width: 160px">
|
|
|
|
|
|
<a-select-option value="all">全部状态</a-select-option>
|
|
|
|
|
|
<a-select-option value="live">授权正常</a-select-option>
|
|
|
|
|
|
<a-select-option value="captcha">待处理</a-select-option>
|
|
|
|
|
|
<a-select-option value="risk">风险观察</a-select-option>
|
|
|
|
|
|
<a-select-option value="expired">授权异常</a-select-option>
|
|
|
|
|
|
</a-select>
|
2026-04-03 00:39:15 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
<div class="media-list-header">
|
|
|
|
|
|
<h3 class="panel-title">已绑定媒体账号</h3>
|
|
|
|
|
|
<p class="panel-desc">只展示当前用户已经写入 `platform_accounts` 的账号,本地没有 session / partition 的账号不会出现在这里。</p>
|
2026-04-03 00:39:15 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
<a-spin :spinning="loading">
|
|
|
|
|
|
<a-empty
|
|
|
|
|
|
v-if="!filteredAccounts.length"
|
|
|
|
|
|
description="当前还没有可展示的媒体账号,请先在桌面端绑定。"
|
|
|
|
|
|
/>
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
<section v-else class="media-grid">
|
2026-04-03 00:39:15 +08:00
|
|
|
|
<article
|
2026-04-20 09:52:48 +08:00
|
|
|
|
v-for="account in filteredAccounts"
|
|
|
|
|
|
:key="account.id"
|
|
|
|
|
|
class="media-card"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="media-card__head">
|
|
|
|
|
|
<div class="media-card__identity">
|
|
|
|
|
|
<span class="media-card__avatar" :style="{ background: account.platformAccent }">
|
2026-04-27 01:00:04 +08:00
|
|
|
|
<img v-if="account.avatarUrl" :src="account.avatarUrl" :alt="account.displayName" referrerpolicy="no-referrer" />
|
2026-04-20 09:52:48 +08:00
|
|
|
|
<span v-else>{{ accountInitial(account) }}</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="media-card__identity-copy">
|
|
|
|
|
|
<h3>{{ account.displayName }}</h3>
|
|
|
|
|
|
<p>{{ account.platformLabel }}</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>
|
2026-04-03 00:39:15 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
<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) }}</a-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="meta-row">
|
|
|
|
|
|
<span class="meta-label">最近校验</span>
|
|
|
|
|
|
<span class="meta-value">{{ account.verifiedAt ? formatDateTime(account.verifiedAt) : "--" }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="meta-row">
|
|
|
|
|
|
<span class="meta-label">客户端状态</span>
|
2026-04-20 12:00:10 +08:00
|
|
|
|
<a-tooltip :title="clientStatusText(account)" placement="topLeft">
|
|
|
|
|
|
<a-tag :color="clientStatusColor(account)" class="client-status-tag">
|
|
|
|
|
|
{{ clientStatusText(account) }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</a-tooltip>
|
2026-04-20 09:52:48 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="meta-row">
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-25 17:39:02 +08:00
|
|
|
|
<div v-if="account.deleteRequestedAt || account.publishHint" class="media-card__footer">
|
2026-04-20 09:52:48 +08:00
|
|
|
|
<span v-if="account.deleteRequestedAt" class="footer-badge footer-badge--warning">
|
|
|
|
|
|
已申请解绑
|
|
|
|
|
|
</span>
|
2026-04-25 17:39:02 +08:00
|
|
|
|
<span v-else-if="account.publishHint" class="footer-badge">
|
2026-04-20 09:52:48 +08:00
|
|
|
|
{{ account.publishHint }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</a-spin>
|
2026-04-03 00:39:15 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.media-view {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-view__top-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1px solid #e6edf5;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-view__header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-view__header-title h2 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #1a1a1a;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-view__header-title p {
|
2026-04-20 09:52:48 +08:00
|
|
|
|
margin: 6px 0 0;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #8c8c8c;
|
2026-04-20 09:52:48 +08:00
|
|
|
|
line-height: 1.7;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-view__header-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.overview-strip {
|
2026-04-03 00:39:15 +08:00
|
|
|
|
display: grid;
|
2026-04-20 09:52:48 +08:00
|
|
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.overview-chip {
|
|
|
|
|
|
padding: 18px 24px;
|
|
|
|
|
|
border-top: 1px solid #eef2f7;
|
|
|
|
|
|
border-right: 1px solid #eef2f7;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.overview-chip:last-child {
|
|
|
|
|
|
border-right: none;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.overview-chip span {
|
2026-04-03 00:39:15 +08:00
|
|
|
|
display: block;
|
|
|
|
|
|
color: #8c8c8c;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.overview-chip strong {
|
2026-04-03 00:39:15 +08:00
|
|
|
|
display: block;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
color: #1a1a1a;
|
2026-04-20 09:52:48 +08:00
|
|
|
|
font-size: 24px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.panel {
|
2026-04-03 00:39:15 +08:00
|
|
|
|
background: #fff;
|
2026-04-20 09:52:48 +08:00
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
border: 1px solid #e6edf5;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.media-list-panel {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 20px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-list-toolbar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
2026-04-20 09:52:48 +08:00
|
|
|
|
gap: 16px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-search {
|
|
|
|
|
|
width: 320px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.media-list-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 6px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.panel-title {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 16px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #1a1a1a;
|
2026-04-20 09:52:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-desc {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: #8c8c8c;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.7;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
|
|
|
|
gap: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-card {
|
2026-04-20 09:52:48 +08:00
|
|
|
|
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%);
|
2026-04-03 00:39:15 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-04-20 09:52:48 +08:00
|
|
|
|
gap: 18px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-card__head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-card__identity {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
2026-04-20 09:52:48 +08:00
|
|
|
|
min-width: 0;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.media-card__avatar {
|
|
|
|
|
|
width: 44px;
|
|
|
|
|
|
height: 44px;
|
|
|
|
|
|
border-radius: 14px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-04-20 09:52:48 +08:00
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 18px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
font-weight: 700;
|
2026-04-20 09:52:48 +08:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
flex-shrink: 0;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.media-card__avatar img {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.media-card__identity-copy {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-card__identity-copy h3 {
|
2026-04-03 00:39:15 +08:00
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: #1a1a1a;
|
2026-04-20 09:52:48 +08:00
|
|
|
|
font-size: 15px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
font-weight: 700;
|
2026-04-20 09:52:48 +08:00
|
|
|
|
line-height: 1.3;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.media-card__identity-copy p {
|
|
|
|
|
|
margin: 4px 0 0;
|
|
|
|
|
|
color: #8c8c8c;
|
|
|
|
|
|
font-size: 12px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.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;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
2026-04-20 09:52:48 +08:00
|
|
|
|
|
|
|
|
|
|
.media-card__platform-badge img {
|
|
|
|
|
|
width: 20px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
object-fit: contain;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.media-card__meta {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 10px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.meta-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
align-items: center;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.meta-label {
|
2026-04-03 00:39:15 +08:00
|
|
|
|
color: #8c8c8c;
|
2026-04-20 09:52:48 +08:00
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
flex-shrink: 0;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.meta-value {
|
|
|
|
|
|
color: #1a1a1a;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
font-size: 13px;
|
2026-04-20 09:52:48 +08:00
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
word-break: break-all;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 12:00:10 +08:00
|
|
|
|
.client-status-tag {
|
|
|
|
|
|
max-width: 160px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 00:39:15 +08:00
|
|
|
|
.media-card__footer {
|
2026-04-20 09:52:48 +08:00
|
|
|
|
padding-top: 4px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.footer-badge {
|
2026-04-03 00:39:15 +08:00
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
2026-04-20 09:52:48 +08:00
|
|
|
|
min-height: 28px;
|
|
|
|
|
|
padding: 0 10px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #f3f6fb;
|
|
|
|
|
|
color: #516074;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
font-size: 12px;
|
2026-04-20 09:52:48 +08:00
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
line-height: 1.6;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.footer-badge--warning {
|
|
|
|
|
|
background: #fff7e6;
|
|
|
|
|
|
color: #d48806;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
|
.overview-strip {
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.overview-chip:nth-child(2) {
|
|
|
|
|
|
border-right: none;
|
|
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.overview-chip:nth-child(n + 3) {
|
|
|
|
|
|
border-top: 1px solid #eef2f7;
|
|
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.media-list-toolbar {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: stretch;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
|
.media-search {
|
|
|
|
|
|
width: 100%;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|