Files
geo/apps/desktop-client/src/renderer/views/AiPlatformsView.vue
T

404 lines
10 KiB
Vue
Raw Normal View History

<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>