2026-04-03 00:39:15 +08:00
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ApiOutlined, DeleteOutlined, LinkOutlined, ReloadOutlined, EllipsisOutlined, ArrowRightOutlined, SearchOutlined } from "@ant-design/icons-vue";
|
|
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
|
|
|
|
|
import { message } from "ant-design-vue";
|
|
|
|
|
import type { MediaPlatform, PlatformAccount, PublisherLocalPlatformState } from "@geo/shared-types";
|
|
|
|
|
import { computed, onMounted, ref } from "vue";
|
|
|
|
|
import { useI18n } from "vue-i18n";
|
|
|
|
|
|
|
|
|
|
import { getApiBaseURL, mediaApi } from "@/lib/api";
|
|
|
|
|
import { formatDateTime } from "@/lib/display";
|
|
|
|
|
import { formatError } from "@/lib/errors";
|
|
|
|
|
import { bindPublisherPlatform } from "@/lib/publisher-plugin";
|
|
|
|
|
import { loadPublisherRuntimeState } from "@/lib/publisher-runtime";
|
|
|
|
|
|
|
|
|
|
type CardTone = "success" | "warning" | "error" | "default";
|
|
|
|
|
|
|
|
|
|
interface PlatformCardView {
|
|
|
|
|
platform: MediaPlatform;
|
|
|
|
|
account: PlatformAccount | null;
|
|
|
|
|
accountCount: number;
|
|
|
|
|
local: PublisherLocalPlatformState | undefined;
|
|
|
|
|
tone: CardTone;
|
|
|
|
|
statusText: string;
|
|
|
|
|
helperText: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { t } = useI18n();
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
|
|
|
|
const runtimeLoading = ref(false);
|
|
|
|
|
const bindingPlatformId = ref<string | null>(null);
|
|
|
|
|
const unbindingAccountId = ref<number | null>(null);
|
|
|
|
|
const pluginInstalled = ref(false);
|
|
|
|
|
const pluginVersion = ref<string | undefined>();
|
|
|
|
|
const pluginInstallationId = ref<number | null>(null);
|
|
|
|
|
const localPlatforms = ref<PublisherLocalPlatformState[]>([]);
|
|
|
|
|
|
|
|
|
|
const platformsQuery = useQuery({
|
|
|
|
|
queryKey: ["media", "platforms"],
|
|
|
|
|
queryFn: () => mediaApi.platforms(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const accountsQuery = useQuery({
|
|
|
|
|
queryKey: ["media", "platform-accounts"],
|
|
|
|
|
queryFn: () => mediaApi.accounts(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const localPlatformMap = computed(() => {
|
|
|
|
|
return new Map(localPlatforms.value.map((item) => [item.platform_id, item]));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const accountGroups = computed(() => {
|
|
|
|
|
const groups = new Map<string, PlatformAccount[]>();
|
|
|
|
|
for (const account of accountsQuery.data.value ?? []) {
|
|
|
|
|
const list = groups.get(account.platform_id) ?? [];
|
|
|
|
|
list.push(account);
|
|
|
|
|
groups.set(account.platform_id, list);
|
|
|
|
|
}
|
|
|
|
|
return groups;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const connectedCount = computed(() => localPlatforms.value.filter((item) => item.connected).length);
|
|
|
|
|
|
|
|
|
|
const platformCards = computed<PlatformCardView[]>(() => {
|
|
|
|
|
return (platformsQuery.data.value ?? []).map((platform) => {
|
|
|
|
|
const accounts = accountGroups.value.get(platform.platform_id) ?? [];
|
|
|
|
|
const account = accounts[0] ?? null;
|
|
|
|
|
const local = localPlatformMap.value.get(platform.platform_id);
|
|
|
|
|
return {
|
|
|
|
|
platform,
|
|
|
|
|
account,
|
|
|
|
|
accountCount: accounts.length,
|
|
|
|
|
local,
|
|
|
|
|
...resolveCardState(platform, account, local),
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const searchQuery = ref("");
|
|
|
|
|
const filterStatus = ref("all");
|
|
|
|
|
const activeCategory = ref("ugc");
|
|
|
|
|
|
|
|
|
|
const filteredCards = computed(() => {
|
|
|
|
|
return platformCards.value.filter(card => {
|
|
|
|
|
if (searchQuery.value && !card.platform.name.toLowerCase().includes(searchQuery.value.toLowerCase())) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (filterStatus.value === 'bound' && !card.account) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (filterStatus.value === 'unbound' && card.account) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const bindMutation = useMutation({
|
|
|
|
|
mutationFn: async (platform: MediaPlatform) => {
|
|
|
|
|
if (!pluginInstalled.value) {
|
|
|
|
|
throw new Error("publisher_plugin_required");
|
|
|
|
|
}
|
|
|
|
|
if (!pluginInstallationId.value) {
|
|
|
|
|
throw new Error("publisher_plugin_register_required");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const session = await mediaApi.createPluginSession({
|
|
|
|
|
action_type: "bind",
|
|
|
|
|
platform_id: platform.platform_id,
|
|
|
|
|
plugin_installation_id: pluginInstallationId.value,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return bindPublisherPlatform({
|
|
|
|
|
platform_id: platform.platform_id,
|
|
|
|
|
login_url: platform.login_url,
|
|
|
|
|
callback_base_url: getApiBaseURL(),
|
|
|
|
|
plugin_session_id: session.plugin_session_id,
|
|
|
|
|
session_token: session.session_token,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const unbindMutation = useMutation({
|
|
|
|
|
mutationFn: (accountId: number) => mediaApi.unbindAccount(accountId),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
void refreshAll();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function resolveCardState(
|
|
|
|
|
platform: MediaPlatform,
|
|
|
|
|
account: PlatformAccount | null,
|
|
|
|
|
local: PublisherLocalPlatformState | undefined,
|
|
|
|
|
): Pick<PlatformCardView, "tone" | "statusText" | "helperText"> {
|
|
|
|
|
if (!account) {
|
|
|
|
|
return {
|
|
|
|
|
tone: "default",
|
|
|
|
|
statusText: t("media.card.unbound"),
|
|
|
|
|
helperText: t("media.card.unboundHint", { platform: platform.name }),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (account.status !== "active") {
|
|
|
|
|
return {
|
|
|
|
|
tone: "warning",
|
|
|
|
|
statusText: t("media.card.needsAttention"),
|
|
|
|
|
helperText: t("media.card.expiredHint"),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!pluginInstalled.value) {
|
|
|
|
|
return {
|
|
|
|
|
tone: "warning",
|
|
|
|
|
statusText: t("media.plugin.notInstalled"),
|
|
|
|
|
helperText: t("media.plugin.installDesc"),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!local?.connected) {
|
|
|
|
|
return {
|
|
|
|
|
tone: "warning",
|
|
|
|
|
statusText: t("media.card.localMissing"),
|
|
|
|
|
helperText: t("media.account.localMissing"),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (local.platform_uid && local.platform_uid !== account.platform_uid) {
|
|
|
|
|
return {
|
|
|
|
|
tone: "error",
|
|
|
|
|
statusText: t("media.card.localMismatch"),
|
|
|
|
|
helperText: t("media.account.localMismatch"),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
tone: "success",
|
|
|
|
|
statusText: t("media.account.ready"),
|
|
|
|
|
helperText: t("media.card.readyHint"),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function refreshRuntime(): Promise<void> {
|
|
|
|
|
runtimeLoading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const runtime = await loadPublisherRuntimeState();
|
|
|
|
|
pluginInstalled.value = runtime.ping.installed;
|
|
|
|
|
pluginVersion.value = runtime.ping.version;
|
|
|
|
|
pluginInstallationId.value = runtime.pluginInstallationId;
|
|
|
|
|
localPlatforms.value = runtime.localPlatforms;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
pluginInstalled.value = false;
|
|
|
|
|
pluginVersion.value = undefined;
|
|
|
|
|
pluginInstallationId.value = null;
|
|
|
|
|
localPlatforms.value = [];
|
|
|
|
|
message.error(formatError(error));
|
|
|
|
|
} finally {
|
|
|
|
|
runtimeLoading.value = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function refreshAll(): Promise<void> {
|
|
|
|
|
await Promise.all([
|
|
|
|
|
refreshRuntime(),
|
|
|
|
|
platformsQuery.refetch(),
|
|
|
|
|
accountsQuery.refetch(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleBind(platform: MediaPlatform): Promise<void> {
|
|
|
|
|
bindingPlatformId.value = platform.platform_id;
|
|
|
|
|
try {
|
|
|
|
|
const result = await bindMutation.mutateAsync(platform);
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
message.warning(result.message || t("media.messages.bindPending"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
message.success(t("media.messages.bindSuccess", { platform: platform.name }));
|
|
|
|
|
await Promise.all([
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["media"] }),
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["articles"] }),
|
|
|
|
|
]);
|
|
|
|
|
await refreshAll();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Error && error.message === "publisher_plugin_required") {
|
|
|
|
|
message.warning(t("media.plugin.notInstalled"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (error instanceof Error && error.message === "publisher_plugin_register_required") {
|
|
|
|
|
message.warning(t("media.plugin.registerRequired"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
message.error(formatError(error));
|
|
|
|
|
} finally {
|
|
|
|
|
bindingPlatformId.value = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleUnbind(accountId: number): Promise<void> {
|
|
|
|
|
unbindingAccountId.value = accountId;
|
|
|
|
|
try {
|
|
|
|
|
await unbindMutation.mutateAsync(accountId);
|
|
|
|
|
message.success(t("media.messages.unbindSuccess"));
|
|
|
|
|
await Promise.all([
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["media"] }),
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["articles"] }),
|
|
|
|
|
]);
|
|
|
|
|
await refreshAll();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
message.error(formatError(error));
|
|
|
|
|
} finally {
|
|
|
|
|
unbindingAccountId.value = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toneClass(tone: CardTone): string {
|
|
|
|
|
return `media-card--${tone}`;
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="media-view">
|
|
|
|
|
<section class="media-view__top-card">
|
|
|
|
|
<div class="media-view__header">
|
|
|
|
|
<div class="media-view__header-title">
|
|
|
|
|
<h2>{{ t('route.media.title') }}</h2>
|
|
|
|
|
<p>{{ t('route.media.description') }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="media-view__header-actions">
|
|
|
|
|
<a-button :loading="runtimeLoading" @click="refreshAll">
|
|
|
|
|
<template #icon><ReloadOutlined /></template>
|
|
|
|
|
{{ t("media.actions.redetect") }}
|
|
|
|
|
</a-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
2026-04-03 17:48:30 +08:00
|
|
|
<a-alert
|
|
|
|
|
v-if="!pluginInstalled"
|
|
|
|
|
type="warning"
|
|
|
|
|
show-icon
|
|
|
|
|
:message="t('media.plugin.installTitle')"
|
|
|
|
|
:description="t('media.plugin.installDesc')"
|
|
|
|
|
/>
|
|
|
|
|
|
2026-04-03 00:39:15 +08:00
|
|
|
<section class="media-runtime-card panel">
|
|
|
|
|
<div class="media-runtime-card__copy">
|
|
|
|
|
<h3 class="panel-title">{{ t("media.plugin.title") }}</h3>
|
|
|
|
|
<h2>{{ pluginInstalled ? t("media.plugin.ready") : t("media.plugin.notInstalled") }}</h2>
|
|
|
|
|
<p>{{ t("media.plugin.backgroundHint") }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="media-runtime-card__stats">
|
|
|
|
|
<article>
|
|
|
|
|
<span>{{ t("media.plugin.versionLabel") }}</span>
|
|
|
|
|
<strong>{{ pluginVersion || "--" }}</strong>
|
|
|
|
|
</article>
|
|
|
|
|
<article>
|
|
|
|
|
<span>{{ t("media.plugin.installationLabel") }}</span>
|
|
|
|
|
<strong>{{ pluginInstallationId ?? "--" }}</strong>
|
|
|
|
|
</article>
|
|
|
|
|
<article>
|
|
|
|
|
<span>{{ t("media.plugin.connectedSummary") }}</span>
|
|
|
|
|
<strong>{{ connectedCount }}/{{ platformCards.length }}</strong>
|
|
|
|
|
</article>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
2026-04-03 17:48:30 +08:00
|
|
|
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
|
|
|
<section class="media-list-wrapper panel">
|
|
|
|
|
<div class="custom-tabs">
|
|
|
|
|
<div class="custom-tab active">
|
|
|
|
|
UGC媒体平台
|
|
|
|
|
<span class="custom-tab-badge">当前支持</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="custom-tab">
|
|
|
|
|
企业自建站点
|
|
|
|
|
</div>
|
|
|
|
|
<div class="custom-tab">
|
|
|
|
|
第三方新闻源
|
|
|
|
|
<span class="custom-tab-badge">当前支持</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="media-list-content">
|
|
|
|
|
<p class="media-list-desc">管理您的媒体账号设定状态与发布配置</p>
|
|
|
|
|
|
|
|
|
|
<div class="media-list-toolbar">
|
|
|
|
|
<a-input
|
|
|
|
|
v-model:value="searchQuery"
|
|
|
|
|
placeholder="搜寻媒体平台..."
|
|
|
|
|
class="media-search"
|
|
|
|
|
>
|
|
|
|
|
<template #prefix>
|
2026-04-03 17:48:30 +08:00
|
|
|
<SearchOutlined style="color: #737373;" />
|
2026-04-03 00:39:15 +08:00
|
|
|
</template>
|
|
|
|
|
</a-input>
|
|
|
|
|
|
|
|
|
|
<a-radio-group v-model:value="filterStatus" class="media-filter">
|
|
|
|
|
<a-radio-button value="all">全部</a-radio-button>
|
|
|
|
|
<a-radio-button value="bound">已绑定</a-radio-button>
|
|
|
|
|
<a-radio-button value="unbound">未绑定</a-radio-button>
|
|
|
|
|
</a-radio-group>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<h4 class="media-list-title">选择媒体平台 ({{ filteredCards.length }})</h4>
|
|
|
|
|
|
|
|
|
|
<section class="media-grid">
|
|
|
|
|
<article
|
|
|
|
|
v-for="card in filteredCards"
|
|
|
|
|
:key="card.platform.platform_id"
|
|
|
|
|
class="media-card"
|
|
|
|
|
:class="toneClass(card.tone)"
|
|
|
|
|
>
|
|
|
|
|
<div class="media-card__head">
|
|
|
|
|
<div class="media-card__identity">
|
|
|
|
|
<span class="media-card__badge" :style="{ color: card.platform.accent_color }">
|
|
|
|
|
{{ card.platform.short_name }}
|
|
|
|
|
</span>
|
|
|
|
|
<div class="media-card__identity-text">
|
|
|
|
|
<h3>{{ card.platform.name }}</h3>
|
|
|
|
|
<p>Media</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="media-card__more">
|
|
|
|
|
<a-dropdown placement="bottomRight">
|
|
|
|
|
<span class="more-icon"><EllipsisOutlined /></span>
|
|
|
|
|
<template #overlay>
|
|
|
|
|
<a-menu>
|
|
|
|
|
<a-menu-item v-if="card.account" @click="handleUnbind(card.account.id)">
|
|
|
|
|
<span style="color: #ff4d4f;"><DeleteOutlined /> {{ t("media.actions.unbind") }}</span>
|
|
|
|
|
</a-menu-item>
|
|
|
|
|
<a-menu-item v-if="card.platform.login_url">
|
|
|
|
|
<a :href="card.platform.login_url" target="_blank" rel="noreferrer" style="color: #8c8c8c;">
|
|
|
|
|
<LinkOutlined /> {{ t("media.card.openPlatform") }}
|
|
|
|
|
</a>
|
|
|
|
|
</a-menu-item>
|
|
|
|
|
<a-menu-item v-if="card.accountCount > 1" disabled>
|
2026-04-03 17:48:30 +08:00
|
|
|
<span style="color: #737373;">{{ t("media.card.multiAccount", { count: card.accountCount }) }}</span>
|
2026-04-03 00:39:15 +08:00
|
|
|
</a-menu-item>
|
|
|
|
|
</a-menu>
|
|
|
|
|
</template>
|
|
|
|
|
</a-dropdown>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="media-card__body">
|
|
|
|
|
<template v-if="card.account">
|
|
|
|
|
<div class="media-card__account-name">{{ card.account.nickname || card.local?.nickname || "--" }}</div>
|
|
|
|
|
<div class="media-card__account-id">ID: {{ card.account.platform_uid || card.local?.platform_uid || "--" }}</div>
|
|
|
|
|
</template>
|
|
|
|
|
<template v-else>
|
|
|
|
|
<div class="media-card__account-unbound">{{ t("media.card.unbound") }}</div>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="media-card__footer">
|
|
|
|
|
<div class="media-card__status">
|
|
|
|
|
<span v-if="card.tone === 'success'" class="status-dot success"></span>
|
|
|
|
|
<span v-else-if="card.tone === 'default'" class="status-dot default"></span>
|
|
|
|
|
<span v-else class="status-dot warning"></span>
|
|
|
|
|
<span class="status-text">{{ card.statusText }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="media-card__actions">
|
|
|
|
|
<!-- Bound state action: Rebind -->
|
|
|
|
|
<a-button
|
|
|
|
|
v-if="card.account"
|
|
|
|
|
class="action-btn-publish"
|
|
|
|
|
:loading="bindingPlatformId === card.platform.platform_id"
|
|
|
|
|
@click="handleBind(card.platform)"
|
|
|
|
|
>
|
|
|
|
|
{{ t("media.actions.rebind") }} <ApiOutlined />
|
|
|
|
|
</a-button>
|
|
|
|
|
|
|
|
|
|
<!-- Unbound state action: Bind -->
|
|
|
|
|
<a-button
|
|
|
|
|
v-else
|
|
|
|
|
class="action-btn-bind"
|
|
|
|
|
:loading="bindingPlatformId === card.platform.platform_id"
|
|
|
|
|
@click="handleBind(card.platform)"
|
|
|
|
|
>
|
|
|
|
|
{{ t("media.actions.bind") }} <ArrowRightOutlined />
|
|
|
|
|
</a-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</article>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
</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 {
|
|
|
|
|
margin: 6px 0 0 0;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #8c8c8c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-view__header-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.panel {
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 24px;
|
|
|
|
|
border: 1px solid #e6edf5;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.panel-title {
|
|
|
|
|
margin: 0 0 16px 0;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: #1a1a1a;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.panel-title::before {
|
|
|
|
|
content: "";
|
|
|
|
|
display: inline-block;
|
|
|
|
|
width: 4px;
|
|
|
|
|
height: 14px;
|
|
|
|
|
background: #1f5cff;
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-runtime-card {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: minmax(0, 1.3fr) minmax(320px, 1fr);
|
|
|
|
|
gap: 18px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-runtime-card__copy {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-runtime-card__copy h2 {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #1a1a1a;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-runtime-card__copy p:last-child {
|
|
|
|
|
margin: 10px 0 0;
|
|
|
|
|
color: #8c8c8c;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-runtime-card__stats {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-runtime-card__stats article {
|
|
|
|
|
padding: 16px;
|
|
|
|
|
border: 1px solid #e6edf5;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
background: #f7f9fc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-runtime-card__stats span {
|
|
|
|
|
display: block;
|
|
|
|
|
color: #8c8c8c;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-runtime-card__stats strong {
|
|
|
|
|
display: block;
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
color: #1a1a1a;
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-list-wrapper {
|
|
|
|
|
padding: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.custom-tabs {
|
|
|
|
|
display: flex;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
padding: 0 24px;
|
|
|
|
|
border-radius: 12px 12px 0 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.custom-tab {
|
|
|
|
|
position: relative;
|
|
|
|
|
padding: 16px 24px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #595959;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.custom-tab.active {
|
|
|
|
|
color: #1677ff;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.custom-tab.active::after {
|
|
|
|
|
content: "";
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: -1px;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
height: 2px;
|
|
|
|
|
background: #1677ff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.custom-tab-badge {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 4px;
|
|
|
|
|
right: -8px;
|
|
|
|
|
background: #f5f5f5;
|
2026-04-03 17:48:30 +08:00
|
|
|
color: f3f#737373;
|
2026-04-03 00:39:15 +08:00
|
|
|
font-size: 10px;
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
line-height: 1;
|
|
|
|
|
transform: scale(0.9);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-list-content {
|
|
|
|
|
padding: 28px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-list-desc {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #8c8c8c;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-list-toolbar {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-search {
|
|
|
|
|
width: 320px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
.media-search :deep(.ant-input) {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-filter {
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-list-title {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: #1a1a1a;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
|
|
|
gap: 18px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-card {
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
border: 1px solid #ccd3da;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-card--default{
|
|
|
|
|
background: hsl(0, 0%, 100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-card:hover {
|
|
|
|
|
border-color: #1677ff;
|
|
|
|
|
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-card--success,
|
|
|
|
|
.media-card--warning,
|
|
|
|
|
.media-card--error {
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-card__badge {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
border: 1px solid #f0f0f0;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-card__identity-text {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-card__identity-text h3 {
|
|
|
|
|
margin: 0;
|
|
|
|
|
color: #1a1a1a;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-card__identity-text p {
|
|
|
|
|
margin: 2px 0 0;
|
2026-04-03 17:48:30 +08:00
|
|
|
color: #737373;
|
2026-04-03 00:39:15 +08:00
|
|
|
font-size: 11px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.more-icon {
|
2026-04-03 17:48:30 +08:00
|
|
|
color: #737373;
|
2026-04-03 00:39:15 +08:00
|
|
|
font-size: 18px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
line-height: 1;
|
|
|
|
|
}
|
|
|
|
|
.more-icon:hover {
|
|
|
|
|
color: #8c8c8c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-card__body {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-card__account-name {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #1a1a1a;
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-card__account-id {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #8c8c8c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-card__account-unbound {
|
|
|
|
|
font-size: 13px;
|
2026-04-03 17:48:30 +08:00
|
|
|
color: #737373;
|
2026-04-03 00:39:15 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-card__footer {
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
border-top: 1px solid #f0f0f0;
|
|
|
|
|
padding-top: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.media-card__status {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #8c8c8c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-dot {
|
|
|
|
|
width: 6px;
|
|
|
|
|
height: 6px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
}
|
|
|
|
|
.status-dot.success { background-color: #52c41a; }
|
|
|
|
|
.status-dot.default { background-color: #d9d9d9; }
|
|
|
|
|
.status-dot.warning, .status-dot.error { background-color: #faad14; }
|
|
|
|
|
|
|
|
|
|
.media-card__actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn-bind {
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #595959;
|
|
|
|
|
border-color: #d9d9d9;
|
|
|
|
|
height: 28px;
|
|
|
|
|
padding: 0 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
.action-btn-bind:hover {
|
|
|
|
|
color: #1677ff;
|
|
|
|
|
border-color: #1677ff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn-publish {
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
background: #e6f4ff;
|
|
|
|
|
color: #1677ff;
|
|
|
|
|
border: none;
|
|
|
|
|
height: 28px;
|
|
|
|
|
padding: 0 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
.action-btn-publish:hover {
|
|
|
|
|
background: #bae0ff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1080px) {
|
|
|
|
|
.media-runtime-card,
|
|
|
|
|
.media-grid {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 720px) {
|
|
|
|
|
.media-runtime-card__stats {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|