404 lines
10 KiB
Vue
404 lines
10 KiB
Vue
|
|
<script setup lang="ts">
|
|||
|
|
import { computed } from "vue";
|
|||
|
|
import { WarningOutlined } from "@ant-design/icons-vue";
|
|||
|
|
|
|||
|
|
import StatusBadge from "../components/StatusBadge.vue";
|
|||
|
|
import { useDesktopRuntime } from "../composables/useDesktopRuntime";
|
|||
|
|
import { formatDateTime, titleCaseToken } from "../lib/formatters";
|
|||
|
|
import { desktopMonitoringMediaCatalog } from "../lib/media-catalog";
|
|||
|
|
import type { RuntimeAccount } from "../types";
|
|||
|
|
|
|||
|
|
type AccountRow = Readonly<Omit<RuntimeAccount, "tags">> & { tags: readonly string[] };
|
|||
|
|
|
|||
|
|
const { snapshot } = useDesktopRuntime();
|
|||
|
|
|
|||
|
|
const clientsById = computed(
|
|||
|
|
() => new Map((snapshot.value?.clients ?? []).map((item) => [item.id, item])),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const aiAccounts = computed(() =>
|
|||
|
|
(snapshot.value?.accounts ?? []).filter((account) =>
|
|||
|
|
desktopMonitoringMediaCatalog.some((platform) => platform.id === account.platform),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const overview = computed(() => ({
|
|||
|
|
bound: aiAccounts.value.length,
|
|||
|
|
live: aiAccounts.value.filter((item) => item.health === "live").length,
|
|||
|
|
attention: aiAccounts.value.filter((item) => item.health !== "live").length,
|
|||
|
|
configuredPlatforms: desktopMonitoringMediaCatalog.length,
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
function authLabel(account: AccountRow | null) {
|
|||
|
|
if (!account) {
|
|||
|
|
return "未绑定";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
switch (account.health) {
|
|||
|
|
case "live":
|
|||
|
|
return "已绑定";
|
|||
|
|
case "captcha":
|
|||
|
|
return "待补登";
|
|||
|
|
case "expired":
|
|||
|
|
return "授权过期";
|
|||
|
|
default:
|
|||
|
|
return "风险观察";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const platformCards = computed(() =>
|
|||
|
|
desktopMonitoringMediaCatalog.map((platform) => {
|
|||
|
|
const matched = aiAccounts.value.filter((account) => account.platform === platform.id);
|
|||
|
|
return {
|
|||
|
|
...platform,
|
|||
|
|
account: matched[0] ?? null,
|
|||
|
|
duplicate: matched.length > 1,
|
|||
|
|
};
|
|||
|
|
}),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
function sessionTone(account: AccountRow | null) {
|
|||
|
|
if (!account) {
|
|||
|
|
return "default";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
switch (account.sessionState) {
|
|||
|
|
case "hot":
|
|||
|
|
return "success";
|
|||
|
|
case "warm":
|
|||
|
|
return "processing";
|
|||
|
|
default:
|
|||
|
|
return "default";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function clientName(account: AccountRow | null): string {
|
|||
|
|
if (!account) {
|
|||
|
|
return "未绑定客户端";
|
|||
|
|
}
|
|||
|
|
return clientsById.value.get(account.clientId)?.deviceName ?? account.clientId;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function shortName(value: string): string {
|
|||
|
|
return titleCaseToken(value).slice(0, 1);
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<section class="media-view">
|
|||
|
|
<section class="media-view__top-card">
|
|||
|
|
<div class="media-view__header">
|
|||
|
|
<div class="media-view__header-title">
|
|||
|
|
<p class="eyebrow" style="color: #8c8c8c; font-size: 11px; text-transform: uppercase; letter-spacing: 0.14em; margin-bottom: 6px;">AI PLATFORMS</p>
|
|||
|
|
<h2>AI 平台管理</h2>
|
|||
|
|
<p class="summary" style="margin-top: 10px; line-height: 1.6; color: #595959; font-size: 13px; max-width: 800px;">
|
|||
|
|
AI 平台按“每个平台只绑定一个账号”建模,所以这里不走媒体账号表格,而是用一平台一卡片的结构直接看绑定状态、会话热度和客户端归属。
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="overview-strip border-t">
|
|||
|
|
<div class="overview-chip">
|
|||
|
|
<span>已绑定平台</span>
|
|||
|
|
<strong>{{ overview.bound }}</strong>
|
|||
|
|
</div>
|
|||
|
|
<div class="overview-chip">
|
|||
|
|
<span>正常可用</span>
|
|||
|
|
<strong>{{ overview.live }}</strong>
|
|||
|
|
</div>
|
|||
|
|
<div class="overview-chip">
|
|||
|
|
<span>待处理</span>
|
|||
|
|
<strong>{{ overview.attention }}</strong>
|
|||
|
|
</div>
|
|||
|
|
<div class="overview-chip">
|
|||
|
|
<span>支持平台</span>
|
|||
|
|
<strong>{{ overview.configuredPlatforms }}</strong>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section class="media-list-wrapper panel">
|
|||
|
|
<div class="media-list-content">
|
|||
|
|
<p class="eyebrow" style="color: #8c8c8c; font-size: 11px; text-transform: uppercase; letter-spacing: 0.14em; margin-bottom: 6px;">SINGLE BINDING</p>
|
|||
|
|
<h4 class="media-list-title" style="font-size: 16px; margin-bottom: 8px;">每个平台最多绑定一个 AI 账号</h4>
|
|||
|
|
<p class="media-list-desc">这里使用桌面端内置的 AI 平台目录。当前以 DeepSeek、通义千问、豆包为一平台一绑定模型。</p>
|
|||
|
|
|
|||
|
|
<section class="media-grid">
|
|||
|
|
<article
|
|||
|
|
v-for="platform in platformCards"
|
|||
|
|
:key="platform.id"
|
|||
|
|
class="media-card"
|
|||
|
|
:class="platform.account ? 'media-card--bound' : 'media-card--unbound'"
|
|||
|
|
>
|
|||
|
|
<div class="media-card__head">
|
|||
|
|
<div class="media-card__identity">
|
|||
|
|
<span class="media-card__badge" :style="{ backgroundColor: platform.accent + '15', color: platform.accent, borderColor: platform.accent + '30' }">
|
|||
|
|
{{ platform.shortName || shortName(platform.label) }}
|
|||
|
|
</span>
|
|||
|
|
<div class="media-card__identity-text">
|
|||
|
|
<h3>{{ platform.label }}</h3>
|
|||
|
|
<p>{{ platform.description }}</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<a-tag :color="platform.account ? (platform.account.health === 'live' ? 'green' : 'orange') : 'default'" style="margin: 0; border-radius: 12px; font-size: 12px; border: 1px solid #e6edf5;">
|
|||
|
|
{{ authLabel(platform.account) }}
|
|||
|
|
</a-tag>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-if="platform.account" class="account-shell">
|
|||
|
|
<div class="account-row-info">
|
|||
|
|
<span>当前账号</span>
|
|||
|
|
<strong>{{ platform.account.displayName }}</strong>
|
|||
|
|
</div>
|
|||
|
|
<div class="account-row-info">
|
|||
|
|
<span>平台 UID</span>
|
|||
|
|
<strong>{{ platform.account.platformUid }}</strong>
|
|||
|
|
</div>
|
|||
|
|
<div class="account-row-info">
|
|||
|
|
<span>归属客户端</span>
|
|||
|
|
<strong>{{ clientName(platform.account) }}</strong>
|
|||
|
|
</div>
|
|||
|
|
<div class="account-row-info">
|
|||
|
|
<span>最近同步</span>
|
|||
|
|
<strong>{{ formatDateTime(platform.account.lastSyncAt) }}</strong>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="status-row-badges">
|
|||
|
|
<a-badge :status="sessionTone(platform.account) as any" :text="`Session: ${platform.account.sessionState}`" />
|
|||
|
|
<a-badge :status="platform.account.online ? 'success' : 'warning'" :text="platform.account.online ? 'Online' : 'Offline'" style="margin-left: 16px;" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else class="empty-card-inner">
|
|||
|
|
<strong>当前未绑定账号</strong>
|
|||
|
|
<p>这个平台现在还没有登录态。后续接入真实登录流后,可以在这里发起单平台绑定。</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-if="platform.duplicate" style="margin-top: 12px;">
|
|||
|
|
<a-alert type="error" show-icon message="发现多个账号绑定到同一 AI 平台,这和当前“一平台一绑定”的约束冲突。" banner />
|
|||
|
|
</div>
|
|||
|
|
</article>
|
|||
|
|
</section>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
</section>
|
|||
|
|
</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: 24px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #1a1a1a;
|
|||
|
|
line-height: 1.4;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.overview-strip {
|
|||
|
|
display: flex;
|
|||
|
|
background: #fafafb;
|
|||
|
|
padding: 16px 24px;
|
|||
|
|
gap: 32px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.border-t {
|
|||
|
|
border-top: 1px solid #e6edf5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.overview-chip {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
background: #ffffff;
|
|||
|
|
border: 1px solid #e6edf5;
|
|||
|
|
padding: 16px 20px;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
min-width: 140px;
|
|||
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.02);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.overview-chip span {
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: #8c8c8c;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.overview-chip strong {
|
|||
|
|
margin-top: 6px;
|
|||
|
|
font-size: 28px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #1a1a1a;
|
|||
|
|
line-height: 1.2;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.panel {
|
|||
|
|
background: #ffffff;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
border: 1px solid #e6edf5;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.media-list-wrapper {
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.media-list-content {
|
|||
|
|
padding: 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.media-list-title {
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #1a1a1a;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.media-list-desc {
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: #8c8c8c;
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.media-grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|||
|
|
gap: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.media-card {
|
|||
|
|
position: relative;
|
|||
|
|
overflow: hidden;
|
|||
|
|
padding: 20px;
|
|||
|
|
border: 1px solid #e6edf5;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
background: #fafafb;
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.media-card:hover {
|
|||
|
|
border-color: #1677ff;
|
|||
|
|
box-shadow: 0 8px 24px rgba(22, 119, 255, 0.06);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.media-card--bound {
|
|||
|
|
background: #ffffff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.media-card__head {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 12px;
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.media-card__identity {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.media-card__badge {
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
width: 40px;
|
|||
|
|
height: 40px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
font-size: 18px;
|
|||
|
|
font-weight: 700;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.media-card__identity-text {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.media-card__identity-text h3 {
|
|||
|
|
margin: 0;
|
|||
|
|
color: #1a1a1a;
|
|||
|
|
font-size: 15px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.media-card__identity-text p {
|
|||
|
|
margin: 2px 0 0;
|
|||
|
|
color: #8c8c8c;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.account-shell {
|
|||
|
|
background: #fafafb;
|
|||
|
|
border: 1px solid #e6edf5;
|
|||
|
|
padding: 16px;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-card-inner {
|
|||
|
|
background: #ffffff;
|
|||
|
|
border: 1px solid #f0f0f0;
|
|||
|
|
padding: 16px;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-card-inner strong {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: #1a1a1a;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-card-inner p {
|
|||
|
|
margin: 6px 0 0;
|
|||
|
|
color: #8c8c8c;
|
|||
|
|
font-size: 12px;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.account-row-info {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.account-row-info span {
|
|||
|
|
color: #8c8c8c;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.account-row-info strong {
|
|||
|
|
color: #1a1a1a;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-row-badges {
|
|||
|
|
margin-top: 6px;
|
|||
|
|
padding-top: 12px;
|
|||
|
|
border-top: 1px dashed #e6edf5;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
</style>
|