feat(desktop-ui): AI platforms multi-account binding and probe view
Rework the AI platforms view around multiple accounts per platform: per-account auth-state classification, status/platform filtering, and an inline health probe (verifyAccount) with pending state. Update the nav description to reflect multi-account binding, health checks and auto switching. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -88,7 +88,7 @@ const navItems = computed(() => {
|
||||
{
|
||||
to: '/ai-platforms',
|
||||
title: 'AI 平台',
|
||||
description: 'AI 平台一平台一授权,按卡片查看状态与归属',
|
||||
description: 'AI 模型多账号绑定、健康检查与自动切换',
|
||||
count: accounts.filter((item) => monitoringIDs.has(item.platform)).length,
|
||||
icon: RobotOutlined,
|
||||
},
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ApiOutlined,
|
||||
ArrowRightOutlined,
|
||||
DeleteOutlined,
|
||||
LinkOutlined,
|
||||
ReloadOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
SearchOutlined,
|
||||
SyncOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { notification } from 'ant-design-vue'
|
||||
@@ -12,7 +15,7 @@ import { computed, ref } from 'vue'
|
||||
|
||||
import { useDesktopRuntime } from '../composables/useDesktopRuntime'
|
||||
import { showClientActionError } from '../lib/client-errors'
|
||||
import { formatDateTime, formatRelativeTime, formatVerifiedAtLabel } from '../lib/formatters'
|
||||
import { formatRelativeTime, formatVerifiedAtLabel } from '../lib/formatters'
|
||||
import { desktopMonitoringMediaCatalog } from '../lib/media-catalog'
|
||||
import type { RuntimeAccount } from '../types'
|
||||
|
||||
@@ -20,9 +23,13 @@ type AccountRow = Readonly<Omit<RuntimeAccount, 'tags'>> & { tags: readonly stri
|
||||
|
||||
const { snapshot, refresh, refreshAccounts, probeAccount, loading } = useDesktopRuntime()
|
||||
|
||||
const selectedPlatform = ref('all')
|
||||
const selectedStatus = ref('all')
|
||||
const searchQuery = ref('')
|
||||
const bindPendingPlatformId = ref<string | null>(null)
|
||||
const openPendingAccountId = ref<string | null>(null)
|
||||
const unbindPendingAccountId = ref<string | null>(null)
|
||||
const probePendingAccountIds = ref(new Set<string>())
|
||||
const batchProbePending = ref(false)
|
||||
const batchProbeRunningAccountIds = ref<readonly string[]>([])
|
||||
const batchProbeQueuedAccountIds = ref<readonly string[]>([])
|
||||
@@ -39,8 +46,9 @@ const platformCards = computed(() =>
|
||||
const matched = aiAccounts.value.filter((account) => account.platform === platform.id)
|
||||
return {
|
||||
...platform,
|
||||
account: matched[0] ?? null,
|
||||
duplicateCount: Math.max(0, matched.length - 1),
|
||||
accounts: matched,
|
||||
count: matched.length,
|
||||
latestAccount: matched[0] ?? null,
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -50,19 +58,81 @@ function isLocallyAuthorized(account: AccountRow | null): boolean {
|
||||
}
|
||||
|
||||
const overview = computed(() => ({
|
||||
authorized: platformCards.value.filter((platform) => isLocallyAuthorized(platform.account))
|
||||
.length,
|
||||
pending: platformCards.value.filter(
|
||||
(platform) => platform.account && !isLocallyAuthorized(platform.account),
|
||||
).length,
|
||||
authorized: aiAccounts.value.filter((account) => isLocallyAuthorized(account)).length,
|
||||
pending: aiAccounts.value.filter((account) => !isLocallyAuthorized(account)).length,
|
||||
onlineClients: snapshot.value?.summary.onlineClients ?? 0,
|
||||
}))
|
||||
|
||||
const boundAIAccounts = computed(() =>
|
||||
platformCards.value
|
||||
.map((platform) => platform.account)
|
||||
.filter((account): account is AccountRow => Boolean(account)),
|
||||
)
|
||||
const boundAIAccounts = computed(() => aiAccounts.value)
|
||||
|
||||
type AccountAuthState = 'authorized' | 'expired' | 'attention'
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'authorized', label: '已授权' },
|
||||
{ value: 'attention', label: '待处理' },
|
||||
{ value: 'expired', label: '授权过期' },
|
||||
] as const
|
||||
|
||||
function authState(account: AccountRow): AccountAuthState {
|
||||
switch (account.authState) {
|
||||
case 'active':
|
||||
return 'authorized'
|
||||
case 'expired':
|
||||
case 'revoked':
|
||||
return 'expired'
|
||||
default:
|
||||
return 'attention'
|
||||
}
|
||||
}
|
||||
|
||||
function platformMeta(platform: string) {
|
||||
return (
|
||||
desktopMonitoringMediaCatalog.find((item) => item.id === platform) ?? {
|
||||
id: platform,
|
||||
label: platform,
|
||||
shortName: 'AI',
|
||||
accent: '#64748b',
|
||||
loginUrl: null,
|
||||
logoUrl: null,
|
||||
category: 'monitor',
|
||||
description: 'AI 模型账号',
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const filteredAccounts = computed(() => {
|
||||
const keyword = searchQuery.value.trim().toLowerCase()
|
||||
return aiAccounts.value.filter((account) => {
|
||||
if (selectedPlatform.value !== 'all' && account.platform !== selectedPlatform.value) {
|
||||
return false
|
||||
}
|
||||
if (selectedStatus.value !== 'all') {
|
||||
if (selectedStatus.value === 'attention' && authState(account) === 'authorized') {
|
||||
return false
|
||||
}
|
||||
if (selectedStatus.value !== 'attention' && authState(account) !== selectedStatus.value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (!keyword) {
|
||||
return true
|
||||
}
|
||||
return [account.displayName, account.platformUid, platformMeta(account.platform).label]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(keyword)
|
||||
})
|
||||
})
|
||||
|
||||
const tableColumns = [
|
||||
{ title: '账号 / UID', key: 'name', dataIndex: 'name' },
|
||||
{ title: '模型平台', key: 'platform', dataIndex: 'platform', width: 150 },
|
||||
{ title: '授权状态', key: 'status', dataIndex: 'status', width: 130 },
|
||||
{ title: '本地会话', key: 'session', dataIndex: 'session', width: 130 },
|
||||
{ title: '最近校验', key: 'verified', dataIndex: 'verified', width: 180 },
|
||||
{ title: '操作', key: 'actions', align: 'right' as const, width: 180 },
|
||||
]
|
||||
|
||||
const batchProbeButtonLabel = computed(() => {
|
||||
if (!batchProbePending.value) {
|
||||
@@ -191,6 +261,14 @@ function isReauthorizePending(account: AccountRow): boolean {
|
||||
return bindPendingPlatformId.value === account.platform
|
||||
}
|
||||
|
||||
function isProbePending(account: AccountRow): boolean {
|
||||
return (
|
||||
probePendingAccountIds.value.has(account.id) ||
|
||||
account.probeState === 'queued' ||
|
||||
account.probeState === 'probing'
|
||||
)
|
||||
}
|
||||
|
||||
function onlineLabel(account: AccountRow): string {
|
||||
return account.online ? '客户端在线' : '客户端离线'
|
||||
}
|
||||
@@ -297,6 +375,24 @@ async function bindPlatform(platformId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function selectPlatform(platformId: string) {
|
||||
selectedPlatform.value = platformId
|
||||
}
|
||||
|
||||
async function verifyAccount(account: AccountRow) {
|
||||
probePendingAccountIds.value = new Set([...probePendingAccountIds.value, account.id])
|
||||
try {
|
||||
await probeAccount(account.id)
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
showClientActionError('probe-account', error)
|
||||
} finally {
|
||||
const next = new Set(probePendingAccountIds.value)
|
||||
next.delete(account.id)
|
||||
probePendingAccountIds.value = next
|
||||
}
|
||||
}
|
||||
|
||||
async function openPlatform(account: AccountRow) {
|
||||
openPendingAccountId.value = account.id
|
||||
|
||||
@@ -384,154 +480,249 @@ async function unbindPlatform(account: AccountRow) {
|
||||
|
||||
<section class="content-section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">支持的大模型</h3>
|
||||
<h3 class="section-title">选择平台进行授权</h3>
|
||||
</div>
|
||||
|
||||
<div class="grid-layout">
|
||||
<article
|
||||
v-for="platform in platformCards"
|
||||
:key="platform.id"
|
||||
class="modern-card"
|
||||
:class="platform.account ? 'is-bound' : 'is-unbound'"
|
||||
class="modern-card media-card-clickable"
|
||||
:class="[
|
||||
selectedPlatform === platform.id ? 'is-active' : '',
|
||||
platform.latestAccount ? 'is-bound' : 'is-unbound',
|
||||
]"
|
||||
@click="selectPlatform(platform.id)"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div class="brand-info">
|
||||
<div
|
||||
<span
|
||||
class="brand-logo"
|
||||
:style="{
|
||||
backgroundColor: `${platform.accent}14`,
|
||||
color: platform.accent,
|
||||
border: `1px solid ${platform.accent}24`,
|
||||
background: '#ffffff',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.03), inset 0 0 0 1px #e2e8f0',
|
||||
}"
|
||||
>
|
||||
{{ platform.shortName }}
|
||||
</div>
|
||||
<img
|
||||
v-if="platform.logoUrl"
|
||||
:src="platform.logoUrl"
|
||||
:alt="platform.label"
|
||||
class="brand-logo__image"
|
||||
/>
|
||||
<span v-else>{{ platform.shortName }}</span>
|
||||
</span>
|
||||
<div class="brand-text">
|
||||
<h4>{{ platform.label }}</h4>
|
||||
<p>{{ platform.description }}</p>
|
||||
<p>{{ platform.count > 0 ? `${platform.count} 个账号` : '未绑定' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a-tag :color="authColor(platform.account)" class="status-tag">
|
||||
{{ authLabel(platform.account) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
|
||||
<div v-if="platform.account" class="card-body">
|
||||
<div class="info-list">
|
||||
<div class="info-row">
|
||||
<span class="info-label">当前账号</span>
|
||||
<span class="info-value primary-text">{{ platform.account.displayName }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">平台 UID</span>
|
||||
<span class="info-value mono-text" :title="platform.account.platformUid">
|
||||
{{ platform.account.platformUid }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">本地会话</span>
|
||||
<span class="info-value">{{ sessionLabel(platform.account) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">最近同步</span>
|
||||
<span class="info-value">{{ formatDateTime(platform.account.lastSyncAt) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">校验状态</span>
|
||||
<span class="info-value">{{ verificationLabel(platform.account) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">下次巡检</span>
|
||||
<span class="info-value">{{ nextProbeLabel(platform.account) }}</span>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="media-card__status">
|
||||
<span :class="['status-dot', platform.latestAccount ? 'success' : 'default']"></span>
|
||||
<span>{{ platform.latestAccount ? '已接入' : '待接入' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="online-status">
|
||||
<a-badge
|
||||
:status="platform.account.online ? 'success' : 'default'"
|
||||
:text="onlineLabel(platform.account)"
|
||||
/>
|
||||
<div class="action-group">
|
||||
<a-button
|
||||
v-if="platform.count > 0"
|
||||
class="action-btn-publish"
|
||||
:loading="bindPendingPlatformId === platform.id"
|
||||
@click.stop="bindPlatform(platform.id)"
|
||||
>
|
||||
新增账号
|
||||
<template #icon><ApiOutlined /></template>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-else
|
||||
type="primary"
|
||||
class="action-btn-bind modern-btn"
|
||||
:loading="bindPendingPlatformId === platform.id"
|
||||
@click.stop="bindPlatform(platform.id)"
|
||||
>
|
||||
绑定账号
|
||||
<template #icon><ArrowRightOutlined /></template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<div class="hero-card account-list-card">
|
||||
<div class="account-list-toolbar">
|
||||
<div class="account-list-heading">
|
||||
<h3 class="section-title">授权账号列表</h3>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<a-select v-model:value="selectedPlatform" class="filter-select">
|
||||
<a-select-option value="all">所有平台</a-select-option>
|
||||
<a-select-option
|
||||
v-for="platform in platformCards"
|
||||
:key="platform.id"
|
||||
:value="platform.id"
|
||||
>
|
||||
{{ platform.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="selectedStatus" class="filter-select">
|
||||
<a-select-option
|
||||
v-for="option in statusOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-input
|
||||
v-model:value="searchQuery"
|
||||
allow-clear
|
||||
placeholder="搜索昵称、UID..."
|
||||
class="filter-search"
|
||||
>
|
||||
<template #prefix><SearchOutlined class="filter-search__icon" /></template>
|
||||
</a-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
class="modern-table"
|
||||
:columns="tableColumns"
|
||||
:data-source="filteredAccounts"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
:scroll="{ x: 980 }"
|
||||
>
|
||||
<template #emptyText>
|
||||
<div class="table-empty">
|
||||
<a-empty description="暂无匹配账号,请重置过滤或绑定新账号" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<div class="user-avatar-wrap">
|
||||
<span
|
||||
class="avatar-circle"
|
||||
:style="{ background: platformMeta(record.platform).accent }"
|
||||
>
|
||||
<span class="avatar-initial">
|
||||
{{ record.displayName.slice(0, 1).toUpperCase() }}
|
||||
</span>
|
||||
</span>
|
||||
<div class="info-stack">
|
||||
<span class="title">{{ record.displayName }}</span>
|
||||
<span class="subtitle mono-text" :title="record.platformUid">
|
||||
{{ record.platformUid }}
|
||||
</span>
|
||||
<small v-if="accountActionAlert(record)" class="account-warning">
|
||||
<WarningOutlined />
|
||||
{{ accountActionAlert(record) }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-group">
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'platform'">
|
||||
<div class="platform-display">
|
||||
<span class="platform-logo-mini">
|
||||
<img
|
||||
v-if="platformMeta(record.platform).logoUrl"
|
||||
:src="platformMeta(record.platform).logoUrl!"
|
||||
:alt="platformMeta(record.platform).label"
|
||||
/>
|
||||
<span v-else :style="{ color: platformMeta(record.platform).accent }">
|
||||
{{ platformMeta(record.platform).shortName }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="platform-display__name">
|
||||
{{ platformMeta(record.platform).label }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="authColor(record)" class="status-tag">
|
||||
{{ authLabel(record) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'session'">
|
||||
<div class="info-stack">
|
||||
<span class="title">{{ sessionLabel(record) }}</span>
|
||||
<span class="subtitle">{{ onlineLabel(record) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'verified'">
|
||||
<div class="info-stack">
|
||||
<span class="title">{{ verificationLabel(record) }}</span>
|
||||
<span class="subtitle">下次 {{ nextProbeLabel(record) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<div class="table-actions">
|
||||
<a-tooltip title="打开平台" placement="top">
|
||||
<a-button
|
||||
type="text"
|
||||
class="action-btn"
|
||||
:loading="openPendingAccountId === record.id"
|
||||
@click="openPlatform(record)"
|
||||
>
|
||||
<template #icon><LinkOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip
|
||||
v-if="platform.account.authState === 'expired'"
|
||||
title="重新授权"
|
||||
:title="record.authState === 'expired' ? '重新授权' : '重新校验'"
|
||||
placement="top"
|
||||
>
|
||||
<a-button
|
||||
type="text"
|
||||
class="action-btn"
|
||||
:loading="isReauthorizePending(platform.account)"
|
||||
@click="bindPlatform(platform.id)"
|
||||
:loading="
|
||||
record.authState === 'expired'
|
||||
? isReauthorizePending(record)
|
||||
: isProbePending(record)
|
||||
"
|
||||
@click="
|
||||
record.authState === 'expired'
|
||||
? bindPlatform(record.platform)
|
||||
: verifyAccount(record)
|
||||
"
|
||||
>
|
||||
<template #icon><ArrowRightOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="打开平台" placement="top">
|
||||
<a-button
|
||||
type="text"
|
||||
class="action-btn"
|
||||
:loading="openPendingAccountId === platform.account.id"
|
||||
@click="openPlatform(platform.account)"
|
||||
>
|
||||
<template #icon><LinkOutlined /></template>
|
||||
<template #icon>
|
||||
<ArrowRightOutlined v-if="record.authState === 'expired'" />
|
||||
<SyncOutlined v-else />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-popconfirm
|
||||
title="确认解绑这个 AI 账号吗?"
|
||||
ok-text="解绑"
|
||||
cancel-text="取消"
|
||||
@confirm="unbindPlatform(platform.account)"
|
||||
placement="topRight"
|
||||
@confirm="unbindPlatform(record)"
|
||||
>
|
||||
<a-tooltip title="解绑" placement="top">
|
||||
<a-button
|
||||
danger
|
||||
type="text"
|
||||
class="action-btn danger-btn"
|
||||
:loading="unbindPendingAccountId === platform.account.id"
|
||||
:loading="unbindPendingAccountId === record.id"
|
||||
>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="card-empty-state">
|
||||
<div class="empty-content">
|
||||
<h5>当前未绑定账号</h5>
|
||||
<p>首次授权会打开登录窗口;绑定完成后,后续监测统一在隐藏窗口里静默执行。</p>
|
||||
</div>
|
||||
<a-button
|
||||
type="primary"
|
||||
class="auth-btn"
|
||||
:loading="bindPendingPlatformId === platform.id"
|
||||
@click="bindPlatform(platform.id)"
|
||||
>
|
||||
立即授权
|
||||
<template #icon><ArrowRightOutlined /></template>
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div v-if="accountActionAlert(platform.account)" class="card-alert">
|
||||
<a-alert type="warning" show-icon banner>
|
||||
<template #message>
|
||||
{{ accountActionAlert(platform.account) }}
|
||||
</template>
|
||||
<template #icon><WarningOutlined /></template>
|
||||
</a-alert>
|
||||
</div>
|
||||
|
||||
<div v-if="platform.duplicateCount > 0" class="card-alert">
|
||||
<a-alert type="warning" show-icon banner>
|
||||
<template #message>
|
||||
发现额外 {{ platform.duplicateCount }} 个同平台账号缓存,当前卡片只保留主账号展示。
|
||||
</template>
|
||||
<template #icon><WarningOutlined /></template>
|
||||
</a-alert>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
@@ -661,8 +852,8 @@ async function unbindPlatform(account: AccountRow) {
|
||||
|
||||
.grid-layout {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||
gap: 24px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* Modern Card Design */
|
||||
@@ -672,14 +863,17 @@ async function unbindPlatform(account: AccountRow) {
|
||||
background: #ffffff;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 28px;
|
||||
min-height: 380px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.02);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modern-card:hover {
|
||||
.media-card-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.media-card-clickable:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow:
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.05),
|
||||
@@ -687,12 +881,20 @@ async function unbindPlatform(account: AccountRow) {
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.modern-card.is-active {
|
||||
border-color: #0ea5e9;
|
||||
box-shadow:
|
||||
0 0 0 1px #0ea5e9,
|
||||
0 4px 6px -1px rgba(14, 165, 233, 0.1);
|
||||
background: #f0f9ff;
|
||||
}
|
||||
|
||||
/* Card Header */
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.brand-info {
|
||||
@@ -702,19 +904,25 @@ async function unbindPlatform(account: AccountRow) {
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 14px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
border-radius: 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.brand-logo__image {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.brand-text h4 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
@@ -722,13 +930,14 @@ async function unbindPlatform(account: AccountRow) {
|
||||
.brand-text p {
|
||||
margin: 4px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
margin: 0;
|
||||
border-radius: 9999px;
|
||||
padding: 4px 12px;
|
||||
padding: 2px 10px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
border: 1px solid transparent; /* adjusted dynamically by Ant normally, but let's reset slightly */
|
||||
@@ -794,7 +1003,7 @@ async function unbindPlatform(account: AccountRow) {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: auto;
|
||||
padding-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
@@ -803,6 +1012,55 @@ async function unbindPlatform(account: AccountRow) {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.media-card__status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-dot.success {
|
||||
background-color: #10b981;
|
||||
}
|
||||
|
||||
.status-dot.default {
|
||||
background-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.action-btn-publish {
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 8px;
|
||||
background: #e0f2fe;
|
||||
color: #0284c7;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-btn-publish:hover {
|
||||
border-color: #7dd3fc;
|
||||
background: #bae6fd;
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
.action-btn-bind {
|
||||
height: 34px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
@@ -827,53 +1085,178 @@ async function unbindPlatform(account: AccountRow) {
|
||||
border-color: #fee2e2;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.card-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
background: #f8fafc;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
.account-list-card {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.empty-content h5 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
.account-list-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 24px 32px 16px;
|
||||
border-bottom: 1px solid #eef2f6;
|
||||
}
|
||||
|
||||
.account-list-heading {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.filter-search {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.filter-search__icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
:deep(.modern-table) {
|
||||
padding: 0 32px 32px;
|
||||
}
|
||||
|
||||
:deep(.modern-table .ant-table-thead > tr > th) {
|
||||
padding: 16px 12px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background-color: transparent !important;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.modern-table .ant-table-tbody > tr > td) {
|
||||
padding: 16px 12px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.empty-content p {
|
||||
margin: 12px 0 0;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
:deep(.modern-table .ant-table-tbody > tr:last-child > td) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.auth-btn {
|
||||
margin-top: 24px;
|
||||
:deep(.modern-table .ant-table-tbody > tr:hover > td) {
|
||||
background-color: #f8fafc !important;
|
||||
}
|
||||
|
||||
.user-avatar-wrap {
|
||||
min-width: 260px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.avatar-circle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.avatar-initial {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.info-stack {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-stack .title {
|
||||
color: #0f172a;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-stack .subtitle {
|
||||
max-width: 280px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-stack .mono-text {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
.account-warning {
|
||||
max-width: 460px;
|
||||
margin-top: 6px;
|
||||
color: #d97706;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.platform-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #0f172a;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.platform-logo-mini {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.05),
|
||||
inset 0 0 0 1px #e2e8f0;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.platform-logo-mini img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.platform-display__name {
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
min-width: 152px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-alert {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:deep(.card-alert .ant-alert) {
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
background: #fffbeb;
|
||||
.table-empty {
|
||||
min-height: 180px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Responsiveness */
|
||||
@@ -886,5 +1269,13 @@ async function unbindPlatform(account: AccountRow) {
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
}
|
||||
.account-list-toolbar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
.filter-bar {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user