97ae601cfd
Desktop Client Build / Resolve Build Metadata (push) Successful in 28s
Frontend CI / Frontend (push) Successful in 2m53s
Backend CI / Backend (push) Successful in 16m10s
Desktop Client Build / Build Desktop Client (push) Successful in 23m44s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 50s
3884 lines
98 KiB
Vue
3884 lines
98 KiB
Vue
<script setup lang="ts">
|
||
import {
|
||
ApiOutlined,
|
||
AppstoreOutlined,
|
||
CheckCircleOutlined,
|
||
CloseCircleOutlined,
|
||
DeleteOutlined,
|
||
DesktopOutlined,
|
||
DownloadOutlined,
|
||
EditOutlined,
|
||
ExclamationCircleOutlined,
|
||
GlobalOutlined,
|
||
LinkOutlined,
|
||
LockOutlined,
|
||
MoreOutlined,
|
||
PlusOutlined,
|
||
ReloadOutlined,
|
||
SearchOutlined,
|
||
SyncOutlined,
|
||
} from '@ant-design/icons-vue'
|
||
import type {
|
||
DesktopAccountInfo,
|
||
DesktopClientDownloadableRelease,
|
||
EnterpriseSiteCmsType,
|
||
EnterpriseSiteConnection,
|
||
MediaPlatform,
|
||
} from '@geo/shared-types'
|
||
import { useMutation, useQuery } from '@tanstack/vue-query'
|
||
import { Modal, message } from 'ant-design-vue'
|
||
import { computed, ref } from 'vue'
|
||
|
||
import {
|
||
desktopClientApi,
|
||
enterpriseSitesApi,
|
||
mediaApi,
|
||
resolveApiURL,
|
||
tenantAccountsApi,
|
||
} from '@/lib/api'
|
||
import { resolveAccountCheckedAt, resolveAccountHealth } from '@/lib/desktop-account-runtime'
|
||
import { formatBytes, formatDateTime } from '@/lib/display'
|
||
import { formatError, formatStoredErrorMessage } from '@/lib/errors'
|
||
import {
|
||
getPublishPlatformMeta,
|
||
isMediaPublishAccount,
|
||
normalizePublishPlatformId,
|
||
} from '@/lib/publish-platforms'
|
||
import { useCompanyStore } from '@/stores/company'
|
||
|
||
type AccountHealthFilter = 'all' | 'live' | 'captcha' | 'risk' | 'expired'
|
||
type PublishState = 'immediate' | 'queued' | 'unavailable'
|
||
type MediaScope = 'ugc' | 'enterprise'
|
||
type CmsType = EnterpriseSiteCmsType
|
||
|
||
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
|
||
accountSessionOnline: boolean | null
|
||
taskConsumerOnline: boolean | null
|
||
clientDeviceName: string | null
|
||
clientLastSeenAt: string | null
|
||
queuedPublishTaskCount: number
|
||
oldestQueuedPublishTaskAt: string | null
|
||
publishState: PublishState
|
||
publishHint: string
|
||
deleteRequestedAt: string | null
|
||
}
|
||
|
||
interface EnterpriseSiteFormState {
|
||
name: string
|
||
siteUrl: string
|
||
cmsType: CmsType
|
||
appid: string
|
||
secret: string
|
||
defaultAcode: string
|
||
defaultMcode: string
|
||
defaultScode: string
|
||
}
|
||
|
||
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',
|
||
}
|
||
|
||
const companyStore = useCompanyStore()
|
||
const activeMediaScope = ref<MediaScope>('ugc')
|
||
const searchQuery = ref('')
|
||
const selectedHealth = ref<AccountHealthFilter>('all')
|
||
const downloadingReleaseKey = ref<string | null>(null)
|
||
const requestingAccountDeleteId = ref<string | null>(null)
|
||
const unbindModalOpen = ref(false)
|
||
const unbindAccountTarget = ref<MediaAccountCard | null>(null)
|
||
const unbindVerifyInput = ref('')
|
||
const unbindConfirmLoading = ref(false)
|
||
|
||
const isUnbindVerified = computed(() => {
|
||
if (!unbindAccountTarget.value) return false
|
||
const targetName = unbindAccountTarget.value.displayName.trim()
|
||
const inputVal = unbindVerifyInput.value.trim()
|
||
return inputVal === targetName || inputVal === '解绑'
|
||
})
|
||
|
||
const enterpriseSiteModalOpen = ref(false)
|
||
const editingEnterpriseSiteId = ref<number | null>(null)
|
||
const deletingEnterpriseSiteId = ref<number | null>(null)
|
||
const failedEnterpriseSiteFavicons = ref<Record<string, boolean>>({})
|
||
const enterpriseSiteForm = ref<EnterpriseSiteFormState>({
|
||
name: '',
|
||
siteUrl: '',
|
||
cmsType: 'pbootcms',
|
||
appid: '',
|
||
secret: '',
|
||
defaultAcode: 'cn',
|
||
defaultMcode: '2',
|
||
defaultScode: '',
|
||
})
|
||
const mediaScopes: Array<{ key: MediaScope; label: string; meta: string }> = [
|
||
{ key: 'ugc', label: 'UGC媒体平台', meta: '桌面端账号' },
|
||
{ key: 'enterprise', label: '企业自建站点', meta: 'CMS连接' },
|
||
]
|
||
|
||
const cmsTypeOptions: Array<{ label: string; value: CmsType }> = [
|
||
{ label: 'PBootCMS', value: 'pbootcms' },
|
||
{ label: 'WordPress', value: 'wordpress' },
|
||
]
|
||
|
||
const platformsQuery = useQuery({
|
||
queryKey: ['media', 'platforms', 'media-view'],
|
||
queryFn: () => mediaApi.platforms(),
|
||
})
|
||
|
||
const accountsQuery = useQuery({
|
||
queryKey: ['tenant', 'desktop-accounts', 'media-view'],
|
||
queryFn: () => tenantAccountsApi.list(),
|
||
staleTime: 0,
|
||
refetchInterval: 5_000,
|
||
refetchOnWindowFocus: true,
|
||
})
|
||
|
||
const downloadableReleasesQuery = useQuery({
|
||
queryKey: ['tenant', 'desktop-client', 'downloadable-releases', 'stable'],
|
||
queryFn: async () => {
|
||
try {
|
||
return await desktopClientApi.downloadableReleases('stable')
|
||
} catch {
|
||
return desktopClientApi.publicFallbackDownloadableReleases()
|
||
}
|
||
},
|
||
staleTime: 60_000,
|
||
refetchOnWindowFocus: true,
|
||
})
|
||
|
||
const enterpriseSitesQuery = useQuery({
|
||
queryKey: ['tenant', 'enterprise-sites', 'media-view'],
|
||
queryFn: () => enterpriseSitesApi.list(),
|
||
staleTime: 10_000,
|
||
refetchOnWindowFocus: true,
|
||
})
|
||
|
||
const saveEnterpriseSiteMutation = useMutation({
|
||
mutationFn: () => saveEnterpriseSiteRequest(),
|
||
onSuccess: async (site) => {
|
||
enterpriseSiteModalOpen.value = false
|
||
const wasEditing = editingEnterpriseSiteId.value !== null
|
||
editingEnterpriseSiteId.value = null
|
||
message.success(wasEditing ? '站点连接已更新' : '站点凭证已保存')
|
||
await enterpriseSitesQuery.refetch()
|
||
if (!wasEditing) {
|
||
void enterpriseSitesApi
|
||
.ping(site.id)
|
||
.then(() => enterpriseSitesQuery.refetch())
|
||
.catch(() => undefined)
|
||
}
|
||
},
|
||
onError: (error) => message.error(formatError(error)),
|
||
})
|
||
|
||
const pingEnterpriseSiteMutation = useMutation({
|
||
mutationFn: (siteId: number) => enterpriseSitesApi.ping(siteId),
|
||
onSuccess: async () => {
|
||
message.success('站点连通正常')
|
||
await enterpriseSitesQuery.refetch()
|
||
},
|
||
onError: (error) => message.error(formatError(error)),
|
||
})
|
||
|
||
const syncEnterpriseCategoriesMutation = useMutation({
|
||
mutationFn: (siteId: number) => enterpriseSitesApi.syncCategories(siteId),
|
||
onSuccess: async (categories) => {
|
||
message.success(`已同步 ${categories.length} 个栏目`)
|
||
await enterpriseSitesQuery.refetch()
|
||
},
|
||
onError: (error) => message.error(formatError(error)),
|
||
})
|
||
|
||
const deleteEnterpriseSiteMutation = useMutation({
|
||
mutationFn: (siteId: number) => enterpriseSitesApi.remove(siteId),
|
||
onSuccess: async () => {
|
||
message.success('站点连接已删除')
|
||
await enterpriseSitesQuery.refetch()
|
||
},
|
||
onError: (error) => message.error(formatError(error)),
|
||
onSettled: () => {
|
||
deletingEnterpriseSiteId.value = null
|
||
},
|
||
})
|
||
|
||
const requestAccountDeleteMutation = useMutation({
|
||
mutationFn: (payload: { id: string; undo?: boolean }) =>
|
||
tenantAccountsApi.requestDelete(payload.id, { undo: payload.undo }),
|
||
onSuccess: async (_account, payload) => {
|
||
message.success(payload.undo ? '已撤销解绑申请' : '已申请解绑,客户端会自动清理本地授权')
|
||
await accountsQuery.refetch()
|
||
},
|
||
onError: (error) => message.error(formatError(error)),
|
||
onSettled: () => {
|
||
requestingAccountDeleteId.value = null
|
||
},
|
||
})
|
||
|
||
const removeAccountMutation = useMutation({
|
||
mutationFn: (id: string) => tenantAccountsApi.remove(id),
|
||
onSuccess: async (result) => {
|
||
const cancelledCount = result.cancelled_queued_publish_task_count ?? 0
|
||
message.success(
|
||
cancelledCount > 0
|
||
? `已完成解绑,并取消 ${cancelledCount} 个排队发布任务`
|
||
: '已完成解绑',
|
||
)
|
||
await accountsQuery.refetch()
|
||
},
|
||
onError: (error) => message.error(formatError(error)),
|
||
onSettled: () => {
|
||
requestingAccountDeleteId.value = null
|
||
},
|
||
})
|
||
|
||
const loading = computed(() => platformsQuery.isPending.value || accountsQuery.isPending.value)
|
||
const refreshing = computed(
|
||
() =>
|
||
platformsQuery.isFetching.value ||
|
||
accountsQuery.isFetching.value ||
|
||
downloadableReleasesQuery.isFetching.value ||
|
||
enterpriseSitesQuery.isFetching.value,
|
||
)
|
||
const enterpriseSiteModalTitle = computed(() =>
|
||
editingEnterpriseSiteId.value === null ? '新增企业站点' : '编辑企业站点',
|
||
)
|
||
const enterpriseSiteModalOkText = computed(() =>
|
||
editingEnterpriseSiteId.value === null ? '保存凭证' : '保存修改',
|
||
)
|
||
const enterpriseSiteAppIDLabel = computed(() =>
|
||
enterpriseSiteForm.value.cmsType === 'wordpress' ? '用户名' : 'AppID',
|
||
)
|
||
const enterpriseSiteAppIDPlaceholder = computed(() =>
|
||
enterpriseSiteForm.value.cmsType === 'wordpress'
|
||
? 'WordPress 用户名或邮箱'
|
||
: 'PBootCMS 后台 api_appid',
|
||
)
|
||
const enterpriseSiteSecretPlaceholder = computed(() => {
|
||
if (editingEnterpriseSiteId.value !== null) {
|
||
return '留空则保留原凭证'
|
||
}
|
||
return enterpriseSiteForm.value.cmsType === 'wordpress'
|
||
? 'WordPress Application Password'
|
||
: 'PBootCMS 后台 api_secret'
|
||
})
|
||
const enterpriseSiteDefaultScodeLabel = computed(() =>
|
||
enterpriseSiteForm.value.cmsType === 'wordpress' ? '默认分类 ID' : '默认栏目',
|
||
)
|
||
const enterpriseSiteDefaultScodePlaceholder = computed(() =>
|
||
enterpriseSiteForm.value.cmsType === 'wordpress' ? '例如:12,可选' : '可选',
|
||
)
|
||
|
||
const platformMap = computed(() => {
|
||
return new Map(
|
||
(platformsQuery.data.value ?? []).map((platform: MediaPlatform) => [
|
||
normalizePublishPlatformId(platform.platform_id),
|
||
platform,
|
||
]),
|
||
)
|
||
})
|
||
|
||
const mediaAccounts = computed<MediaAccountCard[]>(() => {
|
||
return [...(accountsQuery.data.value ?? [])]
|
||
.filter(isMediaPublishAccount)
|
||
.map((account) => {
|
||
const platformId = normalizePublishPlatformId(account.platform)
|
||
const platform = platformMap.value.get(platformId)
|
||
const fallback = getPublishPlatformMeta(platformId)
|
||
const publishState = resolvePublishState(account)
|
||
const health = resolveAccountHealth(account)
|
||
|
||
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),
|
||
health,
|
||
verifiedAt: resolveAccountCheckedAt(account),
|
||
clientId: account.client_id,
|
||
clientOnline: account.client_online,
|
||
accountSessionOnline: account.account_session_online ?? null,
|
||
taskConsumerOnline: account.task_consumer_online ?? null,
|
||
clientDeviceName: account.client_device_name,
|
||
clientLastSeenAt: account.client_last_seen_at,
|
||
queuedPublishTaskCount: account.queued_publish_task_count ?? 0,
|
||
oldestQueuedPublishTaskAt: account.oldest_queued_publish_task_at ?? null,
|
||
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
|
||
})
|
||
})
|
||
|
||
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,
|
||
}))
|
||
|
||
const enterpriseOverview = computed(() => {
|
||
const sites = enterpriseSites.value
|
||
const publishable = sites.filter((site) => site.status === 'connected').length
|
||
return {
|
||
total: sites.length,
|
||
publishable,
|
||
attention: sites.length - publishable,
|
||
categories: sites.reduce((acc, site) => acc + (site.category_count || 0), 0),
|
||
}
|
||
})
|
||
|
||
const filteredAccounts = computed(() => {
|
||
const keyword = searchQuery.value.trim().toLowerCase()
|
||
|
||
return mediaAccounts.value.filter((account) => {
|
||
if (selectedHealth.value !== 'all' && account.health !== selectedHealth.value) {
|
||
return false
|
||
}
|
||
|
||
if (!keyword) {
|
||
return true
|
||
}
|
||
|
||
return [
|
||
account.displayName,
|
||
account.platformLabel,
|
||
account.platformUid,
|
||
account.clientDeviceName ?? '',
|
||
]
|
||
.join(' ')
|
||
.toLowerCase()
|
||
.includes(keyword)
|
||
})
|
||
})
|
||
|
||
const downloadableReleases = computed(() => downloadableReleasesQuery.data.value?.items ?? [])
|
||
const enterpriseSites = computed(() => enterpriseSitesQuery.data.value ?? [])
|
||
const downloadableReleasesError = computed(() => {
|
||
const error = downloadableReleasesQuery.error.value
|
||
if (!error) {
|
||
return ''
|
||
}
|
||
if (error instanceof Error) {
|
||
return error.message
|
||
}
|
||
return String(error)
|
||
})
|
||
|
||
function resolvePublishState(account: DesktopAccountInfo): PublishState {
|
||
if (account.delete_requested_at) {
|
||
return 'unavailable'
|
||
}
|
||
if (resolveAccountHealth(account) !== 'live') {
|
||
return 'unavailable'
|
||
}
|
||
if (!account.client_id) {
|
||
return 'unavailable'
|
||
}
|
||
return account.task_consumer_online === true ? 'immediate' : 'queued'
|
||
}
|
||
|
||
function resolvePublishHint(account: DesktopAccountInfo, publishState: PublishState): string {
|
||
if (account.delete_requested_at) {
|
||
return '此运行节点绑定正在解绑中,不会再接收新的发布任务。'
|
||
}
|
||
if (publishState === 'immediate') {
|
||
return ''
|
||
}
|
||
if (publishState === 'queued') {
|
||
const count = account.queued_publish_task_count ?? 0
|
||
const prefix = count > 0 ? `当前已有 ${count} 个发布任务排队。` : ''
|
||
if (account.client_online === true) {
|
||
return `${prefix}客户端心跳在线,但任务消费通道未就绪;任务会进入发布队列,通道恢复后自动继续。`
|
||
}
|
||
return `${prefix}客户端当前离线,任务会先进入发布队列,等桌面端上线后自动继续。`
|
||
}
|
||
if (resolveAccountHealth(account) !== 'live') {
|
||
return '授权状态异常,先在桌面端重新校验后再发布。'
|
||
}
|
||
return '还没有绑定桌面客户端,当前无法加入发布队列。'
|
||
}
|
||
|
||
function normalizePlatformUid(value?: string | null): string {
|
||
let normalized = String(value ?? '').trim()
|
||
while (normalized.toLowerCase().startsWith('platform:')) {
|
||
normalized = normalized.slice('platform:'.length).trim()
|
||
}
|
||
return normalized || '--'
|
||
}
|
||
|
||
function resolvePlatformLogoUrl(platformId: string, remoteLogoUrl?: string | null): string | null {
|
||
const localLogoUrl = platformLogoCatalog[platformId]
|
||
if (localLogoUrl) {
|
||
return localLogoUrl
|
||
}
|
||
|
||
const resolvedRemoteLogoUrl = resolveApiURL(remoteLogoUrl)
|
||
return resolvedRemoteLogoUrl || null
|
||
}
|
||
|
||
function accountInitial(account: MediaAccountCard): string {
|
||
const name = account.displayName.trim()
|
||
return name ? name.slice(0, 1).toUpperCase() : account.platformShortName
|
||
}
|
||
|
||
function publishRank(state: PublishState): number {
|
||
switch (state) {
|
||
case 'immediate':
|
||
return 0
|
||
case 'queued':
|
||
return 1
|
||
default:
|
||
return 2
|
||
}
|
||
}
|
||
|
||
function healthLabel(health: DesktopAccountInfo['health']): string {
|
||
switch (health) {
|
||
case 'live':
|
||
return '授权正常'
|
||
case 'captcha':
|
||
return '待处理'
|
||
case 'risk':
|
||
return '风险观察'
|
||
default:
|
||
return '授权异常'
|
||
}
|
||
}
|
||
|
||
function healthColor(health: DesktopAccountInfo['health']): string {
|
||
switch (health) {
|
||
case 'live':
|
||
return 'green'
|
||
case 'captcha':
|
||
return 'orange'
|
||
case 'risk':
|
||
return 'gold'
|
||
default:
|
||
return 'red'
|
||
}
|
||
}
|
||
|
||
function clientStatusLabel(account: MediaAccountCard): string {
|
||
if (!account.clientId) {
|
||
return '未绑定'
|
||
}
|
||
if (account.taskConsumerOnline) {
|
||
return '任务通道在线'
|
||
}
|
||
return account.clientOnline ? '心跳在线,任务通道离线' : '离线'
|
||
}
|
||
|
||
function clientStatusColor(account: MediaAccountCard): string {
|
||
if (!account.clientId) {
|
||
return 'default'
|
||
}
|
||
if (account.taskConsumerOnline) {
|
||
return 'green'
|
||
}
|
||
return account.clientOnline ? 'orange' : 'gold'
|
||
}
|
||
|
||
function clientStatusText(account: MediaAccountCard): string {
|
||
if (!account.clientId) {
|
||
return '未绑定桌面客户端'
|
||
}
|
||
|
||
const device = account.clientDeviceName?.trim()
|
||
return device ? `${clientStatusLabel(account)} · ${device}` : clientStatusLabel(account)
|
||
}
|
||
|
||
function publishStateLabel(state: PublishState): string {
|
||
switch (state) {
|
||
case 'immediate':
|
||
return '可立即发布'
|
||
case 'queued':
|
||
return '排队等待'
|
||
default:
|
||
return '不可发布'
|
||
}
|
||
}
|
||
|
||
function publishStateColor(state: PublishState): string {
|
||
switch (state) {
|
||
case 'immediate':
|
||
return 'green'
|
||
case 'queued':
|
||
return 'gold'
|
||
default:
|
||
return 'red'
|
||
}
|
||
}
|
||
|
||
async function refreshAll(): Promise<void> {
|
||
await Promise.all([
|
||
platformsQuery.refetch(),
|
||
accountsQuery.refetch(),
|
||
downloadableReleasesQuery.refetch(),
|
||
enterpriseSitesQuery.refetch(),
|
||
])
|
||
}
|
||
|
||
function openEnterpriseSiteModal(cmsType: CmsType = 'pbootcms'): void {
|
||
editingEnterpriseSiteId.value = null
|
||
enterpriseSiteForm.value = {
|
||
name: '',
|
||
siteUrl: '',
|
||
cmsType,
|
||
appid: '',
|
||
secret: '',
|
||
defaultAcode: cmsType === 'wordpress' ? '' : 'cn',
|
||
defaultMcode: cmsType === 'wordpress' ? '' : '2',
|
||
defaultScode: '',
|
||
}
|
||
enterpriseSiteModalOpen.value = true
|
||
}
|
||
|
||
function openEnterpriseSiteEditModal(site: EnterpriseSiteConnection): void {
|
||
editingEnterpriseSiteId.value = site.id
|
||
enterpriseSiteForm.value = {
|
||
name: site.name,
|
||
siteUrl: site.site_url,
|
||
cmsType: site.cms_type as CmsType,
|
||
appid: site.appid,
|
||
secret: '',
|
||
defaultAcode: site.default_acode ?? 'cn',
|
||
defaultMcode: site.default_mcode ?? '2',
|
||
defaultScode: site.default_scode ?? '',
|
||
}
|
||
enterpriseSiteModalOpen.value = true
|
||
}
|
||
|
||
function closeEnterpriseSiteModal(): void {
|
||
enterpriseSiteModalOpen.value = false
|
||
editingEnterpriseSiteId.value = null
|
||
}
|
||
|
||
function handleEnterpriseSiteCMSTypeChange(value: CmsType): void {
|
||
enterpriseSiteForm.value = {
|
||
...enterpriseSiteForm.value,
|
||
cmsType: value,
|
||
defaultAcode: value === 'wordpress' ? '' : 'cn',
|
||
defaultMcode: value === 'wordpress' ? '' : '2',
|
||
defaultScode: '',
|
||
}
|
||
}
|
||
|
||
function getEnterpriseSiteSelectPopupContainer(triggerNode: HTMLElement): HTMLElement {
|
||
return (triggerNode.closest('.ant-modal-content') as HTMLElement) || document.body
|
||
}
|
||
|
||
function normalizeSiteUrl(value: string): string {
|
||
const trimmed = value.trim()
|
||
if (!trimmed) {
|
||
return ''
|
||
}
|
||
return /^https?:\/\//i.test(trimmed)
|
||
? trimmed.replace(/\/+$/, '')
|
||
: `https://${trimmed}`.replace(/\/+$/, '')
|
||
}
|
||
|
||
async function saveEnterpriseSiteRequest(): Promise<EnterpriseSiteConnection> {
|
||
const form = enterpriseSiteForm.value
|
||
const siteUrl = normalizeSiteUrl(form.siteUrl)
|
||
const appid = form.appid.trim()
|
||
const secret = form.secret.trim()
|
||
const editingId = editingEnterpriseSiteId.value
|
||
|
||
if (!siteUrl || !appid || (editingId === null && !secret)) {
|
||
throw new Error('请填写站点域名、AppID、Secret')
|
||
}
|
||
let name = form.name.trim()
|
||
try {
|
||
name = name || new URL(siteUrl).hostname
|
||
} catch {
|
||
throw new Error('站点域名格式不正确')
|
||
}
|
||
if (editingId !== null) {
|
||
return enterpriseSitesApi.update(editingId, {
|
||
name,
|
||
site_url: siteUrl,
|
||
appid,
|
||
secret: secret || undefined,
|
||
default_acode: form.defaultAcode.trim() || null,
|
||
default_mcode: form.defaultMcode.trim() || null,
|
||
default_scode: form.defaultScode.trim() || null,
|
||
})
|
||
}
|
||
return enterpriseSitesApi.create({
|
||
name,
|
||
site_url: siteUrl,
|
||
cms_type: form.cmsType,
|
||
appid,
|
||
secret,
|
||
default_acode: form.defaultAcode.trim() || null,
|
||
default_mcode: form.defaultMcode.trim() || null,
|
||
default_scode: form.defaultScode.trim() || null,
|
||
})
|
||
}
|
||
|
||
function cmsTypeLabel(cmsType: CmsType): string {
|
||
return cmsTypeOptions.find((option) => option.value === cmsType)?.label ?? cmsType
|
||
}
|
||
|
||
function enterpriseSiteCredentialLabel(site: EnterpriseSiteConnection): string {
|
||
return String(site.cms_type).trim() === 'wordpress' ? '用户名' : 'AppID'
|
||
}
|
||
|
||
function enterpriseSiteStatusLabel(status: string): string {
|
||
switch (status) {
|
||
case 'connected':
|
||
return '已连通'
|
||
case 'error':
|
||
return '连接异常'
|
||
case 'disabled':
|
||
return '已禁用'
|
||
default:
|
||
return '待校验'
|
||
}
|
||
}
|
||
|
||
function enterpriseSiteStatusColor(status: string): string {
|
||
switch (status) {
|
||
case 'connected':
|
||
return 'green'
|
||
case 'error':
|
||
return 'red'
|
||
case 'disabled':
|
||
return 'default'
|
||
default:
|
||
return 'gold'
|
||
}
|
||
}
|
||
|
||
function syncEnterpriseCategories(site: EnterpriseSiteConnection): void {
|
||
syncEnterpriseCategoriesMutation.mutate(site.id)
|
||
}
|
||
|
||
function pingEnterpriseSite(site: EnterpriseSiteConnection): void {
|
||
pingEnterpriseSiteMutation.mutate(site.id)
|
||
}
|
||
|
||
function deleteEnterpriseSite(site: EnterpriseSiteConnection): void {
|
||
Modal.confirm({
|
||
title: '确认删除这个站点连接?',
|
||
content: '删除后,绑定该站点的发文通道将被清除,且无法恢复。确定继续吗?',
|
||
okText: '删除',
|
||
cancelText: '取消',
|
||
okButtonProps: { danger: true },
|
||
onOk: () => {
|
||
deletingEnterpriseSiteId.value = site.id
|
||
deleteEnterpriseSiteMutation.mutate(site.id)
|
||
},
|
||
})
|
||
}
|
||
|
||
function requestUnbindAccount(account: MediaAccountCard): void {
|
||
unbindAccountTarget.value = account
|
||
unbindVerifyInput.value = ''
|
||
unbindConfirmLoading.value = false
|
||
unbindModalOpen.value = true
|
||
}
|
||
|
||
async function handleConfirmUnbind(): Promise<void> {
|
||
if (!isUnbindVerified.value || !unbindAccountTarget.value) return
|
||
|
||
unbindConfirmLoading.value = true
|
||
try {
|
||
requestingAccountDeleteId.value = unbindAccountTarget.value.id
|
||
await removeAccountMutation.mutateAsync(unbindAccountTarget.value.id)
|
||
unbindModalOpen.value = false
|
||
} catch (err) {
|
||
// Error is handled by removeAccountMutation onError
|
||
} finally {
|
||
unbindConfirmLoading.value = false
|
||
}
|
||
}
|
||
|
||
function undoUnbindAccount(account: MediaAccountCard): void {
|
||
requestingAccountDeleteId.value = account.id
|
||
requestAccountDeleteMutation.mutate({ id: account.id, undo: true })
|
||
}
|
||
|
||
function isAccountDeleteRequestPending(accountId: string): boolean {
|
||
return (
|
||
(requestAccountDeleteMutation.isPending.value || removeAccountMutation.isPending.value) &&
|
||
requestingAccountDeleteId.value === accountId
|
||
)
|
||
}
|
||
|
||
function enterpriseSiteFaviconURL(site: EnterpriseSiteConnection): string {
|
||
try {
|
||
const parsed = new URL(site.site_url)
|
||
return `${parsed.origin}/favicon.ico`
|
||
} catch {
|
||
return ''
|
||
}
|
||
}
|
||
|
||
function enterpriseSiteFaviconKey(site: EnterpriseSiteConnection): string {
|
||
return `${site.id}:${enterpriseSiteFaviconURL(site)}`
|
||
}
|
||
|
||
function shouldShowEnterpriseSiteFavicon(site: EnterpriseSiteConnection): boolean {
|
||
const faviconURL = enterpriseSiteFaviconURL(site)
|
||
return Boolean(faviconURL && !failedEnterpriseSiteFavicons.value[enterpriseSiteFaviconKey(site)])
|
||
}
|
||
|
||
function markEnterpriseSiteFaviconFailed(site: EnterpriseSiteConnection): void {
|
||
failedEnterpriseSiteFavicons.value = {
|
||
...failedEnterpriseSiteFavicons.value,
|
||
[enterpriseSiteFaviconKey(site)]: true,
|
||
}
|
||
}
|
||
|
||
function enterpriseSiteLastError(site: EnterpriseSiteConnection): string {
|
||
return formatStoredErrorMessage(site.last_error)
|
||
}
|
||
|
||
async function downloadRelease(release: DesktopClientDownloadableRelease): Promise<void> {
|
||
const key = releaseDownloadKey(release)
|
||
if (downloadingReleaseKey.value) {
|
||
return
|
||
}
|
||
|
||
downloadingReleaseKey.value = key
|
||
const downloadWindow = window.open('about:blank', '_blank')
|
||
if (downloadWindow) {
|
||
downloadWindow.opener = null
|
||
}
|
||
try {
|
||
const result = await desktopClientApi.publicDownloadURL({
|
||
platform: release.platform,
|
||
arch: release.arch,
|
||
channel: release.channel,
|
||
})
|
||
const downloadURL = String(result.download_url ?? '').trim()
|
||
if (!downloadURL) {
|
||
message.warning('下载地址为空')
|
||
downloadWindow?.close()
|
||
return
|
||
}
|
||
if (downloadWindow) {
|
||
downloadWindow.location.href = downloadURL
|
||
} else {
|
||
window.open(downloadURL, '_blank', 'noopener,noreferrer')
|
||
}
|
||
} catch (error) {
|
||
downloadWindow?.close()
|
||
const detail =
|
||
typeof error === 'object' && error !== null && 'detail' in error ? error.detail : null
|
||
const reason =
|
||
typeof detail === 'string' && detail.trim()
|
||
? detail
|
||
: error instanceof Error
|
||
? error.message
|
||
: '请稍后重试'
|
||
message.error(`获取下载地址失败:${reason}`)
|
||
} finally {
|
||
if (downloadingReleaseKey.value === key) {
|
||
downloadingReleaseKey.value = null
|
||
}
|
||
}
|
||
}
|
||
|
||
function releaseDownloadKey(release: DesktopClientDownloadableRelease): string {
|
||
return `${release.platform}:${release.arch}:${release.channel}`
|
||
}
|
||
|
||
function releaseTargetLabel(release: DesktopClientDownloadableRelease): string {
|
||
return `${releasePlatformLabel(release.platform)} ${releaseArchLabel(release.arch)}`
|
||
}
|
||
|
||
function releasePlatformLabel(platform: string): string {
|
||
switch (platform) {
|
||
case 'darwin':
|
||
return 'macOS'
|
||
case 'win32':
|
||
return 'Windows'
|
||
case 'linux':
|
||
return 'Linux'
|
||
default:
|
||
return platform
|
||
}
|
||
}
|
||
|
||
function releaseArchLabel(arch: string): string {
|
||
switch (arch) {
|
||
case 'arm64':
|
||
return 'ARM64'
|
||
case 'x64':
|
||
return 'x64'
|
||
case 'ia32':
|
||
return 'ia32'
|
||
case 'universal':
|
||
return 'Universal'
|
||
default:
|
||
return arch
|
||
}
|
||
}
|
||
|
||
function releaseDate(release: DesktopClientDownloadableRelease): string {
|
||
return formatDateTime(release.published_at ?? release.updated_at)
|
||
}
|
||
|
||
function releaseSize(value?: number | null): string {
|
||
return typeof value === 'number' && Number.isFinite(value) ? formatBytes(value) : '--'
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="media-view">
|
||
<!-- Premium Dark Banner Section -->
|
||
<section class="media-view__top-card">
|
||
<div class="media-view__header">
|
||
<div class="media-view__header-title">
|
||
<h2>媒体管理</h2>
|
||
<p>将 UGC 账号、新闻源资源和企业自建站点分开管理,发布链路按目标类型独立执行。</p>
|
||
</div>
|
||
<div class="media-view__header-actions">
|
||
<a-button class="btn-refresh" :loading="refreshing" @click="refreshAll">
|
||
<template #icon><ReloadOutlined /></template>
|
||
刷新状态
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="media-scope-tabs">
|
||
<button
|
||
v-for="scope in mediaScopes"
|
||
:key="scope.key"
|
||
class="media-scope-tab"
|
||
:class="{ 'media-scope-tab--active': activeMediaScope === scope.key }"
|
||
type="button"
|
||
@click="activeMediaScope = scope.key"
|
||
>
|
||
<DesktopOutlined v-if="scope.key === 'ugc'" class="tab-icon" />
|
||
<GlobalOutlined v-else-if="scope.key === 'enterprise'" class="tab-icon" />
|
||
<span>{{ scope.label }}</span>
|
||
<small>{{ scope.meta }}</small>
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="activeMediaScope === 'ugc'" class="overview-strip border-t">
|
||
<div class="overview-chip">
|
||
<div class="chip-icon-box total-bg">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||
<path
|
||
d="M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2zm1 14h-2v-6h2v6zm0-8h-2V6h2v2z"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<div class="chip-copy">
|
||
<span>账号总数</span>
|
||
<strong>{{ overview.total }}</strong>
|
||
</div>
|
||
</div>
|
||
<div class="overview-chip">
|
||
<div class="chip-icon-box live-bg">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
|
||
</svg>
|
||
</div>
|
||
<div class="chip-copy">
|
||
<span>可立即发布</span>
|
||
<strong>{{ overview.immediate }}</strong>
|
||
</div>
|
||
</div>
|
||
<div class="overview-chip">
|
||
<div class="chip-icon-box queued-bg">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||
<path
|
||
d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<div class="chip-copy">
|
||
<span>排队等待</span>
|
||
<strong>{{ overview.queued }}</strong>
|
||
</div>
|
||
</div>
|
||
<div class="overview-chip">
|
||
<div class="chip-icon-box attention-bg">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||
<path
|
||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<div class="chip-copy">
|
||
<span>需处理</span>
|
||
<strong>{{ overview.attention }}</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="activeMediaScope === 'enterprise'" class="overview-strip border-t">
|
||
<div class="overview-chip">
|
||
<div class="chip-icon-box total-bg">
|
||
<GlobalOutlined />
|
||
</div>
|
||
<div class="chip-copy">
|
||
<span>站点总数</span>
|
||
<strong>{{ enterpriseOverview.total }}</strong>
|
||
</div>
|
||
</div>
|
||
<div class="overview-chip">
|
||
<div class="chip-icon-box live-bg">
|
||
<CheckCircleOutlined />
|
||
</div>
|
||
<div class="chip-copy">
|
||
<span>可发布站点</span>
|
||
<strong>{{ enterpriseOverview.publishable }}</strong>
|
||
</div>
|
||
</div>
|
||
<div class="overview-chip">
|
||
<div class="chip-icon-box queued-bg">
|
||
<CloseCircleOutlined />
|
||
</div>
|
||
<div class="chip-copy">
|
||
<span>待处理站点</span>
|
||
<strong>{{ enterpriseOverview.attention }}</strong>
|
||
</div>
|
||
</div>
|
||
<div class="overview-chip">
|
||
<div class="chip-icon-box total-bg">
|
||
<AppstoreOutlined />
|
||
</div>
|
||
<div class="chip-copy">
|
||
<span>栏目总数</span>
|
||
<strong>{{ enterpriseOverview.categories }}</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<template v-if="activeMediaScope === 'enterprise'">
|
||
<section class="enterprise-grid enterprise-grid--single">
|
||
<article class="panel connector-panel">
|
||
<div class="panel-head-compact">
|
||
<div>
|
||
<h3 class="panel-title">企业自建站点</h3>
|
||
<p class="panel-desc">
|
||
管理客户官网、品牌站、行业站的连接凭证和连通状态。发布结果统一进入发文管理查看。
|
||
</p>
|
||
</div>
|
||
<div class="panel-head-actions">
|
||
<a-button
|
||
type="primary"
|
||
class="btn-enterprise-primary"
|
||
@click="openEnterpriseSiteModal()"
|
||
>
|
||
<template #icon><PlusOutlined /></template>
|
||
新增站点
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<a-spin :spinning="enterpriseSitesQuery.isPending.value">
|
||
<div v-if="!enterpriseSites.length" class="connector-empty-premium">
|
||
<div class="empty-glow"></div>
|
||
<div class="empty-icon-wrapper">
|
||
<GlobalOutlined class="empty-globe" />
|
||
<PlusOutlined class="empty-plus" />
|
||
</div>
|
||
<h3>暂无企业自建站点</h3>
|
||
<p>
|
||
对接您的 CMS 站点(如 PBootCMS、WordPress 等)后,系统将能够通过 API
|
||
自动将文章推送到您的自建网站,实现多渠道一键发布与同步。
|
||
</p>
|
||
<a-button
|
||
type="primary"
|
||
size="large"
|
||
class="btn-empty-add"
|
||
@click="openEnterpriseSiteModal()"
|
||
>
|
||
<template #icon><PlusOutlined /></template>
|
||
立即添加第一个站点
|
||
</a-button>
|
||
</div>
|
||
|
||
<div v-else class="site-connection-grid">
|
||
<article v-for="site in enterpriseSites" :key="site.id" class="site-connection-card">
|
||
<!-- Top accent based on status -->
|
||
<div class="site-card-accent" :class="`accent-${site.status || 'pending'}`"></div>
|
||
|
||
<div class="site-connection-card__header">
|
||
<span class="site-connection-card__icon">
|
||
<img
|
||
v-if="shouldShowEnterpriseSiteFavicon(site)"
|
||
:src="enterpriseSiteFaviconURL(site)"
|
||
:alt="`${site.name} 站点图标`"
|
||
class="site-connection-card__favicon"
|
||
@error="markEnterpriseSiteFaviconFailed(site)"
|
||
/>
|
||
<GlobalOutlined v-else />
|
||
</span>
|
||
|
||
<div class="site-connection-card__header-main">
|
||
<div class="site-name-row">
|
||
<h4>{{ site.name }}</h4>
|
||
<span class="site-badge site-badge--cms">
|
||
{{ cmsTypeLabel(site.cms_type as CmsType) }}
|
||
</span>
|
||
</div>
|
||
|
||
<span
|
||
class="site-badge site-badge--status"
|
||
:class="`site-badge--status-${site.status || 'pending'}`"
|
||
>
|
||
<span class="site-badge__dot"></span>
|
||
{{ enterpriseSiteStatusLabel(site.status) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="site-connection-card__content">
|
||
<div class="site-url-box">
|
||
<a :href="site.site_url" target="_blank" rel="noreferrer" class="site-link">
|
||
<LinkOutlined class="link-icon" />
|
||
<span>{{ site.site_url }}</span>
|
||
</a>
|
||
</div>
|
||
|
||
<div class="site-meta-grid">
|
||
<div class="meta-cell">
|
||
<span class="meta-label">{{ enterpriseSiteCredentialLabel(site) }}</span>
|
||
<span class="meta-value" :title="site.appid">{{ site.appid }}</span>
|
||
</div>
|
||
<div class="meta-cell">
|
||
<span class="meta-label">栏目</span>
|
||
<span class="meta-value">{{ site.category_count }} 个</span>
|
||
</div>
|
||
<div class="meta-cell full-width">
|
||
<span class="meta-label">栏目同步</span>
|
||
<span class="meta-value">
|
||
{{ site.last_sync_at ? formatDateTime(site.last_sync_at) : '待同步' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="site-connection-card__footer-actions">
|
||
<a-button
|
||
size="middle"
|
||
class="btn-action-ping"
|
||
:loading="
|
||
pingEnterpriseSiteMutation.isPending.value &&
|
||
pingEnterpriseSiteMutation.variables.value === site.id
|
||
"
|
||
@click="pingEnterpriseSite(site)"
|
||
>
|
||
<template #icon><ApiOutlined /></template>
|
||
测试连接
|
||
</a-button>
|
||
|
||
<div class="footer-actions-right">
|
||
<a-button
|
||
size="middle"
|
||
class="btn-action-sync"
|
||
:loading="
|
||
syncEnterpriseCategoriesMutation.isPending.value &&
|
||
syncEnterpriseCategoriesMutation.variables.value === site.id
|
||
"
|
||
@click="syncEnterpriseCategories(site)"
|
||
>
|
||
<template #icon><SyncOutlined /></template>
|
||
同步栏目
|
||
</a-button>
|
||
|
||
<a-dropdown :trigger="['click']" placement="bottomRight">
|
||
<a-button size="middle" class="action-more-btn" type="text" @click.stop>
|
||
<template #icon><MoreOutlined /></template>
|
||
</a-button>
|
||
<template #overlay>
|
||
<a-menu>
|
||
<a-menu-item key="edit" @click="openEnterpriseSiteEditModal(site)">
|
||
<EditOutlined />
|
||
<span>编辑站点</span>
|
||
</a-menu-item>
|
||
<a-menu-divider />
|
||
<a-menu-item key="delete" danger @click="deleteEnterpriseSite(site)">
|
||
<DeleteOutlined />
|
||
<span>删除站点</span>
|
||
</a-menu-item>
|
||
</a-menu>
|
||
</template>
|
||
</a-dropdown>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="enterpriseSiteLastError(site)"
|
||
class="site-connection-card__error-banner"
|
||
>
|
||
<ExclamationCircleOutlined class="error-banner-icon" />
|
||
<div class="error-banner-content">
|
||
<div class="error-banner-title">连接失败诊断</div>
|
||
<div class="error-banner-desc">{{ enterpriseSiteLastError(site) }}</div>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</a-spin>
|
||
</article>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<!-- Client Download Section (App Store Style) -->
|
||
<section class="panel client-download-panel">
|
||
<div class="client-download-head">
|
||
<div>
|
||
<div class="title-with-badge">
|
||
<h3 class="panel-title">客户端下载</h3>
|
||
<span class="badge-pulsing-stable">Stable</span>
|
||
</div>
|
||
<p class="panel-desc">高并发发布与任务执行节点,版本根据后台发布配置自动更新与推送。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<a-spin :spinning="downloadableReleasesQuery.isPending.value">
|
||
<a-alert
|
||
v-if="downloadableReleasesError"
|
||
type="warning"
|
||
show-icon
|
||
class="premium-alert"
|
||
:message="`客户端列表读取失败:${downloadableReleasesError}`"
|
||
/>
|
||
|
||
<a-empty
|
||
v-else-if="!downloadableReleases.length"
|
||
class="premium-empty"
|
||
description="当前暂无可下载客户端,请等待运营后台发布。"
|
||
/>
|
||
|
||
<div v-else class="client-download-grid">
|
||
<article
|
||
v-for="release in downloadableReleases"
|
||
:key="release.id"
|
||
class="client-download-card"
|
||
>
|
||
<div class="client-download-card__body">
|
||
<div class="client-download-card__logo" :class="`logo-${release.platform}`">
|
||
<template v-if="release.platform === 'darwin'">
|
||
<svg viewBox="0 0 24 24" class="os-svg-icon">
|
||
<path
|
||
fill="currentColor"
|
||
d="M18.71,19.5 C17.88,20.74 17,21.95 15.66,21.97 C14.32,22 13.89,21.18 12.37,21.18 C10.84,21.18 10.37,21.95 9.1,22 C7.79,22.05 6.8,20.68 5.96,19.48 C4.25,17 2.94,12.45 4.7,9.39 C5.57,7.87 7.13,6.91 8.82,6.88 C10.1,6.86 11.32,7.75 12.11,7.75 C12.89,7.75 14.37,6.68 15.92,6.84 C16.57,6.87 18.39,7.1 19.56,8.82 C19.47,8.88 17.39,10.1 17.41,12.63 C17.44,15.65 20.06,16.66 20.1,16.67 C20.08,16.74 19.67,18.11 18.71,19.5 M15.97,4.17 C16.63,3.37 17.07,2.28 16.95,1 C16,1.04 14.9,1.6 14.24,2.38 C13.68,3.04 13.19,4.14 13.34,5.39 C14.39,5.47 15.4,4.88 15.97,4.17 Z"
|
||
/>
|
||
</svg>
|
||
</template>
|
||
<template v-else-if="release.platform === 'win32'">
|
||
<svg viewBox="0 0 24 24" class="os-svg-icon">
|
||
<path
|
||
fill="currentColor"
|
||
d="M0 3.449L9.75 2.1v9.45H0V3.449zM0 12.45h9.75v9.45L0 20.551V12.45zm11.25-10.549L24 0v11.55H11.25V1.901zM11.25 12.45H24v11.55l-12.75-1.901V12.45z"
|
||
/>
|
||
</svg>
|
||
</template>
|
||
<template v-else>
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
class="os-svg-icon"
|
||
stroke="currentColor"
|
||
fill="none"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<polyline points="4 17 10 11 4 5"></polyline>
|
||
<line x1="12" y1="19" x2="20" y2="19"></line>
|
||
</svg>
|
||
</template>
|
||
</div>
|
||
<div class="client-download-card__content">
|
||
<h4>{{ releaseTargetLabel(release) }}</h4>
|
||
<div class="meta-inline">
|
||
<span class="version-tag">v{{ release.version }}</span>
|
||
<span class="dot-separator">•</span>
|
||
<span>{{ releaseSize(release.file_size_bytes) }}</span>
|
||
<span class="dot-separator">•</span>
|
||
<span>{{ releaseDate(release) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="client-download-card__file">
|
||
<span class="file-icon">
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
width="12"
|
||
height="12"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
>
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||
<polyline points="14 2 14 8 20 8"></polyline>
|
||
</svg>
|
||
</span>
|
||
<span class="file-name" :title="release.file_name || '安装包'">
|
||
{{ release.file_name || '安装包' }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="client-download-card__actions">
|
||
<a-button
|
||
type="primary"
|
||
class="btn-download"
|
||
:loading="downloadingReleaseKey === releaseDownloadKey(release)"
|
||
:disabled="Boolean(downloadingReleaseKey)"
|
||
@click="downloadRelease(release)"
|
||
>
|
||
<template #icon><DownloadOutlined /></template>
|
||
获取客户端
|
||
</a-button>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</a-spin>
|
||
</section>
|
||
|
||
<!-- Bound accounts grid section -->
|
||
<section class="panel media-list-panel">
|
||
<div class="media-list-header-row">
|
||
<div class="media-list-title-box">
|
||
<h3 class="panel-title">已绑定媒体账号</h3>
|
||
<p class="panel-desc">管理各媒体账号的授权效期、排队就绪状态和客户端物理节点映射。</p>
|
||
</div>
|
||
|
||
<div class="media-list-toolbar">
|
||
<a-input
|
||
v-model:value="searchQuery"
|
||
placeholder="搜索账号昵称、UID、发布渠道..."
|
||
class="media-search"
|
||
allow-clear
|
||
>
|
||
<template #prefix>
|
||
<SearchOutlined style="color: #a0aec0" />
|
||
</template>
|
||
</a-input>
|
||
|
||
<a-select v-model:value="selectedHealth" class="media-select" style="width: 150px">
|
||
<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>
|
||
</div>
|
||
</div>
|
||
|
||
<a-spin :spinning="loading">
|
||
<a-empty
|
||
v-if="!filteredAccounts.length"
|
||
class="premium-empty"
|
||
description="没有符合过滤条件的媒体账号,请确保桌面端客户端在线并已绑定账号。"
|
||
/>
|
||
|
||
<section v-else class="media-grid">
|
||
<article v-for="account in filteredAccounts" :key="account.id" class="media-card">
|
||
<!-- Top brand line accent using accent color -->
|
||
<div
|
||
class="media-card__accent-bar"
|
||
:style="{ background: account.platformAccent }"
|
||
></div>
|
||
|
||
<div class="media-card__head">
|
||
<div class="media-card__identity">
|
||
<span
|
||
class="media-card__avatar"
|
||
:style="{ border: `2px solid ${account.platformAccent}` }"
|
||
>
|
||
<img
|
||
v-if="account.avatarUrl"
|
||
:src="account.avatarUrl"
|
||
:alt="account.displayName"
|
||
referrerpolicy="no-referrer"
|
||
/>
|
||
<span
|
||
v-else
|
||
class="avatar-initial"
|
||
:style="{ background: account.platformAccent }"
|
||
>
|
||
{{ accountInitial(account) }}
|
||
</span>
|
||
</span>
|
||
|
||
<div class="media-card__identity-copy">
|
||
<h3 :title="account.displayName">
|
||
{{ account.displayName }}
|
||
<span class="platform-logo-container">
|
||
<img
|
||
v-if="account.platformLogoUrl"
|
||
:src="account.platformLogoUrl"
|
||
:alt="account.platformLabel"
|
||
class="media-card__inline-platform-icon"
|
||
/>
|
||
<span
|
||
v-else
|
||
class="media-card__inline-platform-text"
|
||
:style="{
|
||
color: account.platformAccent,
|
||
background: `${account.platformAccent}15`,
|
||
}"
|
||
>
|
||
{{ account.platformShortName }}
|
||
</span>
|
||
</span>
|
||
</h3>
|
||
<p>
|
||
<span class="media-card__platform-name">{{ account.platformLabel }}</span>
|
||
<span class="media-card__uid-divider">•</span>
|
||
<span class="media-card__uid" :title="account.platformUid">
|
||
ID: {{ account.platformUid }}
|
||
</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<a-dropdown :trigger="['click']" placement="bottomRight" overlayClassName="premium-unbind-dropdown">
|
||
<a-button
|
||
type="text"
|
||
class="media-card__more"
|
||
:loading="isAccountDeleteRequestPending(account.id)"
|
||
@click.stop
|
||
>
|
||
<template #icon><MoreOutlined /></template>
|
||
</a-button>
|
||
<template #overlay>
|
||
<a-menu>
|
||
<a-menu-item
|
||
v-if="account.deleteRequestedAt"
|
||
key="complete-unbind"
|
||
danger
|
||
@click="requestUnbindAccount(account)"
|
||
>
|
||
<DeleteOutlined />
|
||
<span>完成解绑</span>
|
||
</a-menu-item>
|
||
<a-menu-item
|
||
v-if="account.deleteRequestedAt"
|
||
key="undo-unbind"
|
||
@click="undoUnbindAccount(account)"
|
||
>
|
||
<SyncOutlined />
|
||
<span>撤销解绑申请</span>
|
||
</a-menu-item>
|
||
<a-menu-item
|
||
v-else
|
||
key="unbind"
|
||
danger
|
||
@click="requestUnbindAccount(account)"
|
||
>
|
||
<DeleteOutlined />
|
||
<span>解绑账号</span>
|
||
</a-menu-item>
|
||
</a-menu>
|
||
</template>
|
||
</a-dropdown>
|
||
</div>
|
||
|
||
<div class="media-card__tags">
|
||
<span class="badge-status" :class="`badge-status--${healthColor(account.health)}`">
|
||
<span class="indicator-dot"></span>
|
||
{{ healthLabel(account.health) }}
|
||
</span>
|
||
|
||
<span
|
||
class="badge-status"
|
||
:class="`badge-status--${publishStateColor(account.publishState)}`"
|
||
>
|
||
<span class="indicator-dot"></span>
|
||
{{ publishStateLabel(account.publishState) }}
|
||
</span>
|
||
</div>
|
||
|
||
<!-- Client mapping connection card -->
|
||
<div class="media-card__client-info">
|
||
<div class="client-info-inner">
|
||
<span class="client-label">运行节点</span>
|
||
<a-tooltip :title="clientStatusText(account)" placement="top">
|
||
<span
|
||
class="client-value"
|
||
:class="`client-value--${clientStatusColor(account)}`"
|
||
>
|
||
{{ clientStatusText(account) }}
|
||
</span>
|
||
</a-tooltip>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="media-card__meta-box">
|
||
<div class="meta-box-item">
|
||
<span class="meta-label">安全校验</span>
|
||
<span class="meta-value">
|
||
{{ account.verifiedAt ? formatDateTime(account.verifiedAt) : '--' }}
|
||
</span>
|
||
</div>
|
||
<div class="meta-box-item">
|
||
<span class="meta-label">节点心跳</span>
|
||
<span class="meta-value">
|
||
{{ account.clientLastSeenAt ? formatDateTime(account.clientLastSeenAt) : '--' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Foot note panel -->
|
||
<div
|
||
v-if="account.deleteRequestedAt || account.publishHint"
|
||
class="media-card__footer"
|
||
>
|
||
<div v-if="account.deleteRequestedAt" class="footer-alert footer-alert--warning">
|
||
<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
|
||
<path
|
||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"
|
||
/>
|
||
</svg>
|
||
<span>已申请解绑,可完成服务端解绑</span>
|
||
</div>
|
||
<div v-else-if="account.publishHint" class="footer-alert">
|
||
<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
|
||
<path
|
||
d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"
|
||
/>
|
||
</svg>
|
||
<span :title="account.publishHint">{{ account.publishHint }}</span>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</section>
|
||
</a-spin>
|
||
</section>
|
||
</template>
|
||
|
||
<!-- Custom Account Unbind Modal -->
|
||
<a-modal
|
||
v-model:open="unbindModalOpen"
|
||
:destroy-on-close="true"
|
||
:footer="null"
|
||
:width="480"
|
||
class="premium-unbind-modal"
|
||
:closable="true"
|
||
>
|
||
<div class="unbind-modal-header">
|
||
<div class="unbind-warning-icon-wrapper">
|
||
<DeleteOutlined class="unbind-warning-icon" />
|
||
</div>
|
||
<h3 class="unbind-modal-title">解绑此运行节点上的媒体账号</h3>
|
||
<p class="unbind-modal-subtitle">
|
||
只解除当前卡片对应的运行节点绑定,同一平台账号在其他节点上的绑定不受影响。
|
||
</p>
|
||
</div>
|
||
|
||
<div class="unbind-account-preview" v-if="unbindAccountTarget">
|
||
<div class="preview-avatar-wrapper">
|
||
<img
|
||
v-if="unbindAccountTarget.avatarUrl"
|
||
:src="unbindAccountTarget.avatarUrl"
|
||
class="preview-avatar"
|
||
/>
|
||
<span
|
||
v-else
|
||
class="preview-avatar-placeholder"
|
||
:style="{ background: unbindAccountTarget.platformAccent }"
|
||
>
|
||
{{ accountInitial(unbindAccountTarget) }}
|
||
</span>
|
||
<img
|
||
v-if="unbindAccountTarget.platformLogoUrl"
|
||
:src="unbindAccountTarget.platformLogoUrl"
|
||
class="preview-platform-icon"
|
||
/>
|
||
</div>
|
||
<div class="preview-info">
|
||
<span class="preview-name">{{ unbindAccountTarget.displayName }}</span>
|
||
<span class="preview-meta">
|
||
{{ unbindAccountTarget.platformLabel }} · ID: {{ unbindAccountTarget.platformUid }}
|
||
</span>
|
||
<span class="preview-meta">
|
||
运行节点 · {{ unbindAccountTarget.clientDeviceName || '未命名客户端' }}
|
||
</span>
|
||
</div>
|
||
<div class="preview-status">
|
||
<span
|
||
class="badge-status"
|
||
:class="`badge-status--${unbindAccountTarget.clientOnline ? 'success' : 'warning'}`"
|
||
>
|
||
<span class="indicator-dot"></span>
|
||
{{ unbindAccountTarget.clientOnline ? '在线' : '离线' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="unbind-consequences" v-if="unbindAccountTarget">
|
||
<div class="consequence-item">
|
||
<div class="consequence-icon-wrapper">
|
||
<ExclamationCircleOutlined />
|
||
</div>
|
||
<div class="consequence-text">
|
||
<strong>解除节点绑定</strong>:系统服务端将移除此运行节点上的账号映射,这张卡片不再接收发布任务。
|
||
</div>
|
||
</div>
|
||
<div class="consequence-item">
|
||
<div class="consequence-icon-wrapper">
|
||
<LockOutlined />
|
||
</div>
|
||
<div class="consequence-text">
|
||
<strong>处理本地会话</strong>:
|
||
<span v-if="unbindAccountTarget.clientOnline">
|
||
在线客户端同步后会清理当前节点的本地会话缓存。
|
||
</span>
|
||
<span v-else>
|
||
当前运行节点离线,服务端仍会立即完成解绑;该节点下次同步时会清理本地会话缓存。
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="consequence-item">
|
||
<div class="consequence-icon-wrapper">
|
||
<CloseCircleOutlined />
|
||
</div>
|
||
<div class="consequence-text">
|
||
<strong>取消排队发布</strong>:发往此运行节点此账号的等待发布任务会被取消,其他节点上的同账号任务不受影响。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="unbind-verification" v-if="unbindAccountTarget">
|
||
<div class="verification-label">
|
||
请输入账号名称 <span class="highlight-name">{{ unbindAccountTarget.displayName }}</span> 或输入 <span class="highlight-keyword">解绑</span> 进行确认:
|
||
</div>
|
||
<a-input
|
||
v-model:value="unbindVerifyInput"
|
||
placeholder="请输入名称或“解绑”"
|
||
class="unbind-input"
|
||
@pressEnter="handleConfirmUnbind"
|
||
/>
|
||
</div>
|
||
|
||
<div class="unbind-modal-footer">
|
||
<a-button @click="unbindModalOpen = false" class="btn-cancel">取消</a-button>
|
||
<a-button
|
||
type="primary"
|
||
danger
|
||
:disabled="!isUnbindVerified"
|
||
:loading="unbindConfirmLoading"
|
||
@click="handleConfirmUnbind"
|
||
class="btn-confirm"
|
||
>
|
||
确认解绑
|
||
</a-button>
|
||
</div>
|
||
</a-modal>
|
||
|
||
<a-modal
|
||
v-model:open="enterpriseSiteModalOpen"
|
||
:title="enterpriseSiteModalTitle"
|
||
:destroy-on-close="true"
|
||
:ok-text="enterpriseSiteModalOkText"
|
||
cancel-text="取消"
|
||
:confirm-loading="saveEnterpriseSiteMutation.isPending.value"
|
||
@ok="saveEnterpriseSiteMutation.mutate()"
|
||
@cancel="closeEnterpriseSiteModal"
|
||
>
|
||
<div class="enterprise-site-form">
|
||
<div class="form-field">
|
||
<span>CMS 类型</span>
|
||
<a-select
|
||
v-model:value="enterpriseSiteForm.cmsType"
|
||
:disabled="editingEnterpriseSiteId !== null"
|
||
:options="cmsTypeOptions"
|
||
:get-popup-container="getEnterpriseSiteSelectPopupContainer"
|
||
popup-class-name="enterprise-site-cms-dropdown"
|
||
@change="handleEnterpriseSiteCMSTypeChange"
|
||
/>
|
||
</div>
|
||
|
||
<label class="form-field">
|
||
<span>站点名称</span>
|
||
<a-input v-model:value="enterpriseSiteForm.name" placeholder="例如:品牌官网" />
|
||
</label>
|
||
|
||
<label class="form-field">
|
||
<span>站点域名</span>
|
||
<a-input
|
||
v-model:value="enterpriseSiteForm.siteUrl"
|
||
placeholder="https://www.example.com"
|
||
/>
|
||
</label>
|
||
|
||
<label class="form-field">
|
||
<span>{{ enterpriseSiteAppIDLabel }}</span>
|
||
<a-input
|
||
v-model:value="enterpriseSiteForm.appid"
|
||
:placeholder="enterpriseSiteAppIDPlaceholder"
|
||
/>
|
||
</label>
|
||
|
||
<label class="form-field">
|
||
<span>Secret</span>
|
||
<a-input-password
|
||
v-model:value="enterpriseSiteForm.secret"
|
||
:placeholder="enterpriseSiteSecretPlaceholder"
|
||
/>
|
||
</label>
|
||
|
||
<div
|
||
class="enterprise-form-grid"
|
||
:class="{ 'enterprise-form-grid--wordpress': enterpriseSiteForm.cmsType === 'wordpress' }"
|
||
>
|
||
<template v-if="enterpriseSiteForm.cmsType !== 'wordpress'">
|
||
<label class="form-field">
|
||
<span>默认语言</span>
|
||
<a-input v-model:value="enterpriseSiteForm.defaultAcode" placeholder="cn" />
|
||
</label>
|
||
<label class="form-field">
|
||
<span>模型编码</span>
|
||
<a-input v-model:value="enterpriseSiteForm.defaultMcode" placeholder="2" />
|
||
</label>
|
||
</template>
|
||
<label class="form-field">
|
||
<span>{{ enterpriseSiteDefaultScodeLabel }}</span>
|
||
<a-input
|
||
v-model:value="enterpriseSiteForm.defaultScode"
|
||
:placeholder="enterpriseSiteDefaultScodePlaceholder"
|
||
/>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</a-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* Page Layout */
|
||
.media-view {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24px;
|
||
background-color: #f8fafc;
|
||
min-height: 100%;
|
||
}
|
||
|
||
/* Premium Dark Banner Section */
|
||
.media-view__top-card {
|
||
position: relative;
|
||
background: #ffffff;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
padding: 0;
|
||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||
}
|
||
|
||
.media-view__header {
|
||
position: relative;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 32px;
|
||
z-index: 2;
|
||
}
|
||
|
||
.media-view__header-title {
|
||
max-width: 650px;
|
||
}
|
||
|
||
.media-view__header-title h2 {
|
||
margin: 0;
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: #0f172a;
|
||
line-height: 1.3;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.media-view__header-title p {
|
||
margin: 8px 0 0;
|
||
font-size: 13.5px;
|
||
color: #475569;
|
||
line-height: 1.6;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.media-view__header-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* Premium Refresh Button Override */
|
||
:deep(.btn-refresh.ant-btn) {
|
||
background: #ffffff;
|
||
border: 1px solid #e2e8f0;
|
||
color: #0f172a;
|
||
border-radius: 8px;
|
||
height: 40px;
|
||
padding: 0 18px;
|
||
font-weight: 500;
|
||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||
box-shadow: 0 2px 4px rgba(148, 163, 184, 0.05);
|
||
}
|
||
|
||
:deep(.btn-refresh.ant-btn:hover) {
|
||
background: #f8fafc;
|
||
border-color: #cbd5e1;
|
||
color: #1d4ed8;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(148, 163, 184, 0.08);
|
||
}
|
||
|
||
.media-scope-tabs {
|
||
position: relative;
|
||
z-index: 2;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
background: #f1f5f9;
|
||
padding: 4px;
|
||
border-radius: 10px;
|
||
margin: 0 32px 24px;
|
||
border: 1px solid rgba(148, 163, 184, 0.08);
|
||
box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.03);
|
||
}
|
||
|
||
.media-scope-tab {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
height: 38px;
|
||
padding: 0 18px;
|
||
border: none;
|
||
border-radius: 7px;
|
||
background: transparent;
|
||
color: #64748b;
|
||
font-size: 13.5px;
|
||
font-weight: 550;
|
||
cursor: pointer;
|
||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.media-scope-tab .tab-icon {
|
||
font-size: 14px;
|
||
color: #94a3b8;
|
||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.media-scope-tab small {
|
||
color: #94a3b8;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
background: rgba(148, 163, 184, 0.06);
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.media-scope-tab:hover {
|
||
color: #334155;
|
||
}
|
||
|
||
.media-scope-tab:hover .tab-icon {
|
||
color: #64748b;
|
||
}
|
||
|
||
.media-scope-tab--active {
|
||
background: #ffffff;
|
||
color: #0f172a;
|
||
font-weight: 600;
|
||
box-shadow:
|
||
0 1px 3px rgba(0, 0, 0, 0.05),
|
||
0 4px 12px rgba(15, 23, 42, 0.04);
|
||
}
|
||
|
||
.media-scope-tab--active .tab-icon {
|
||
color: #1677ff;
|
||
}
|
||
|
||
.media-scope-tab--active small {
|
||
color: #1677ff;
|
||
background: #e6f4ff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* Metrics strip styling */
|
||
.overview-strip {
|
||
position: relative;
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
background: rgba(255, 255, 255, 0.4);
|
||
backdrop-filter: blur(12px);
|
||
border-top: 1px solid #e2e8f0;
|
||
z-index: 2;
|
||
}
|
||
|
||
.overview-chip {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
padding: 24px 32px;
|
||
border-right: 1px solid #e2e8f0;
|
||
transition: all 0.25s ease;
|
||
}
|
||
|
||
.overview-chip:last-child {
|
||
border-right: none;
|
||
}
|
||
|
||
.overview-chip:hover {
|
||
background: rgba(255, 255, 255, 0.7);
|
||
}
|
||
|
||
.chip-icon-box {
|
||
width: 42px;
|
||
height: 42px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Metric colored boxes styling */
|
||
.total-bg {
|
||
background: #dbeafe;
|
||
color: #1d4ed8;
|
||
border: 1px solid #bfdbfe;
|
||
}
|
||
|
||
.live-bg {
|
||
background: #d1fae5;
|
||
color: #059669;
|
||
border: 1px solid #a7f3d0;
|
||
}
|
||
|
||
.queued-bg {
|
||
background: #fef3c7;
|
||
color: #d97706;
|
||
border: 1px solid #fde68a;
|
||
}
|
||
|
||
.attention-bg {
|
||
background: #ffe4e6;
|
||
color: #e11d48;
|
||
border: 1px solid #fecdd3;
|
||
}
|
||
|
||
.chip-copy {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.chip-copy span {
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.chip-copy strong {
|
||
color: #0f172a;
|
||
font-size: 26px;
|
||
font-weight: 700;
|
||
margin-top: 4px;
|
||
line-height: 1;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
/* Panel Layout Styling */
|
||
.panel {
|
||
background: #ffffff;
|
||
border-radius: 8px;
|
||
padding: 32px;
|
||
border: 1px solid #e2e8f0;
|
||
box-shadow: 0 4px 20px rgba(148, 163, 184, 0.05);
|
||
transition:
|
||
box-shadow 0.3s ease,
|
||
border-color 0.3s ease;
|
||
}
|
||
|
||
.panel:hover {
|
||
box-shadow: 0 10px 30px rgba(148, 163, 184, 0.08);
|
||
}
|
||
|
||
.panel-title {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: #0f172a;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.panel-desc {
|
||
margin: 6px 0 0;
|
||
color: #64748b;
|
||
font-size: 13.5px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* Client download header and grid */
|
||
.client-download-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24px;
|
||
}
|
||
|
||
.client-download-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.title-with-badge {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.badge-pulsing-stable {
|
||
background: rgba(37, 99, 235, 0.1);
|
||
color: #1d4ed8;
|
||
border: 1px solid rgba(37, 99, 235, 0.2);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
padding: 3px 10px;
|
||
border-radius: 8px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.badge-pulsing-stable::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: -100%;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(90deg, transparent, rgba(37, 99, 235, 0.18), transparent);
|
||
animation: shine-sweep 3s infinite linear;
|
||
}
|
||
|
||
@keyframes shine-sweep {
|
||
0% {
|
||
left: -100%;
|
||
}
|
||
100% {
|
||
left: 100%;
|
||
}
|
||
}
|
||
|
||
.client-download-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: 18px;
|
||
}
|
||
|
||
/* Client download cards */
|
||
.client-download-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
min-height: 200px;
|
||
padding: 24px;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
position: relative;
|
||
}
|
||
|
||
.client-download-card:hover {
|
||
transform: translateY(-4px);
|
||
border-color: #cbd5e1;
|
||
box-shadow: 0 12px 24px rgba(148, 163, 184, 0.12);
|
||
}
|
||
|
||
.client-download-card__body {
|
||
display: flex;
|
||
gap: 16px;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
/* Elegant custom OS SVGs logos wrapper */
|
||
.client-download-card__logo {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.logo-darwin {
|
||
background: #0f172a;
|
||
color: #ffffff;
|
||
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.15);
|
||
}
|
||
|
||
.logo-win32 {
|
||
background: #0078d4;
|
||
color: #ffffff;
|
||
box-shadow: 0 4px 12px rgba(0, 120, 212, 0.2);
|
||
}
|
||
|
||
.logo-default,
|
||
.logo-linux {
|
||
background: #1e293b;
|
||
color: #38bdf8;
|
||
box-shadow: 0 4px 12px rgba(30, 41, 59, 0.2);
|
||
}
|
||
|
||
.os-svg-icon {
|
||
width: 22px;
|
||
height: 22px;
|
||
}
|
||
|
||
.client-download-card__content {
|
||
min-width: 0;
|
||
}
|
||
|
||
.client-download-card__content h4 {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: #0f172a;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.meta-inline {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
margin-top: 6px;
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.version-tag {
|
||
background: #f1f5f9;
|
||
color: #475569;
|
||
font-weight: 600;
|
||
padding: 1px 6px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.dot-separator {
|
||
color: #cbd5e1;
|
||
font-size: 8px;
|
||
}
|
||
|
||
/* Code-looking filename strip */
|
||
.client-download-card__file {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
background: #f1f5f9;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
padding: 10px 12px;
|
||
margin-top: 18px;
|
||
margin-bottom: 18px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.file-icon {
|
||
color: #64748b;
|
||
display: flex;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.file-name {
|
||
color: #334155;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||
font-size: 11.5px;
|
||
font-weight: 500;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.client-download-card__actions {
|
||
display: flex;
|
||
}
|
||
|
||
/* Download Button styling */
|
||
:deep(.btn-download.ant-btn-primary) {
|
||
background: #1d4ed8;
|
||
border-color: #1d4ed8;
|
||
border-radius: 8px;
|
||
height: 38px;
|
||
width: 100%;
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
|
||
transition: all 0.25s ease;
|
||
}
|
||
|
||
:deep(.btn-download.ant-btn-primary:hover:not(:disabled)) {
|
||
background: #1e40af;
|
||
border-color: #1e40af;
|
||
box-shadow: 0 6px 18px rgba(37, 99, 235, 0.26);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
:deep(.btn-download.ant-btn-primary:disabled) {
|
||
background: #cbd5e1;
|
||
border-color: #cbd5e1;
|
||
color: #94a3b8;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.enterprise-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 26px;
|
||
}
|
||
|
||
.enterprise-panel__main {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
gap: 24px;
|
||
align-items: start;
|
||
}
|
||
|
||
.section-kicker {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
height: 22px;
|
||
padding: 0 8px;
|
||
border-radius: 6px;
|
||
background: #eef2ff;
|
||
color: #1e3a8a;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
letter-spacing: 0;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.enterprise-panel__copy h3 {
|
||
margin: 12px 0 0;
|
||
color: #0f172a;
|
||
font-size: 22px;
|
||
font-weight: 760;
|
||
line-height: 1.25;
|
||
}
|
||
|
||
.enterprise-panel__copy p {
|
||
max-width: 660px;
|
||
margin: 8px 0 0;
|
||
color: #475569;
|
||
font-size: 13.5px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.enterprise-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
:deep(.btn-enterprise-primary.ant-btn-primary) {
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
background: #1d4ed8;
|
||
border-color: #1d4ed8;
|
||
font-weight: 650;
|
||
box-shadow: none;
|
||
}
|
||
|
||
:deep(.btn-enterprise-primary.ant-btn-primary:hover) {
|
||
background: #1e40af;
|
||
border-color: #1e40af;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
:deep(.btn-enterprise-secondary.ant-btn) {
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
border-color: #cbd5e1;
|
||
color: #334155;
|
||
font-weight: 650;
|
||
}
|
||
|
||
.enterprise-flow {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.enterprise-step {
|
||
display: grid;
|
||
grid-template-columns: auto minmax(0, 1fr);
|
||
gap: 14px;
|
||
min-height: 108px;
|
||
padding: 18px;
|
||
background: #ffffff;
|
||
border-right: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.enterprise-step--action {
|
||
cursor: pointer;
|
||
transition:
|
||
background 0.2s ease,
|
||
box-shadow 0.2s ease;
|
||
}
|
||
|
||
.enterprise-step--action:hover {
|
||
background: #eff6ff;
|
||
box-shadow: inset 0 0 0 1px #93c5fd;
|
||
}
|
||
|
||
.enterprise-step:last-child {
|
||
border-right: 0;
|
||
}
|
||
|
||
.enterprise-step__index {
|
||
color: #1d4ed8;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.enterprise-step h4 {
|
||
margin: 0;
|
||
color: #0f172a;
|
||
font-size: 14px;
|
||
font-weight: 720;
|
||
}
|
||
|
||
.enterprise-step p {
|
||
margin: 6px 0 0;
|
||
color: #64748b;
|
||
font-size: 12.5px;
|
||
line-height: 1.55;
|
||
}
|
||
|
||
.enterprise-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1.15fr) minmax(360px, 0.85fr);
|
||
gap: 24px;
|
||
}
|
||
|
||
.enterprise-grid--single {
|
||
grid-template-columns: 1fr;
|
||
width: 100%;
|
||
}
|
||
|
||
.connector-panel,
|
||
.adapter-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 22px;
|
||
}
|
||
|
||
.panel-head-compact {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
}
|
||
|
||
.connector-empty-premium {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
padding: 48px 24px;
|
||
border: 1px dashed #cbd5e1;
|
||
border-radius: 16px;
|
||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||
overflow: hidden;
|
||
box-shadow: 0 4px 12px rgba(148, 163, 184, 0.02);
|
||
}
|
||
|
||
.empty-glow {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: 240px;
|
||
height: 240px;
|
||
background: radial-gradient(circle, rgba(37, 99, 235, 0.05) 0%, transparent 70%);
|
||
z-index: 1;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.empty-icon-wrapper {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 72px;
|
||
height: 72px;
|
||
border-radius: 20px;
|
||
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
||
color: #2563eb;
|
||
margin-bottom: 20px;
|
||
border: 1px solid rgba(37, 99, 235, 0.1);
|
||
box-shadow: 0 8px 16px -4px rgba(37, 99, 235, 0.1);
|
||
z-index: 2;
|
||
}
|
||
|
||
.empty-globe {
|
||
font-size: 32px;
|
||
}
|
||
|
||
.empty-plus {
|
||
position: absolute;
|
||
bottom: -4px;
|
||
right: -4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
background: #2563eb;
|
||
color: #ffffff;
|
||
font-size: 10px;
|
||
border: 2px solid #ffffff;
|
||
box-shadow: 0 2px 4px rgba(37, 99, 235, 0.2);
|
||
}
|
||
|
||
.connector-empty-premium h3 {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: #0f172a;
|
||
margin: 0 0 8px;
|
||
z-index: 2;
|
||
}
|
||
|
||
.connector-empty-premium p {
|
||
max-width: 480px;
|
||
font-size: 13.5px;
|
||
line-height: 1.6;
|
||
color: #64748b;
|
||
margin: 0 0 24px;
|
||
z-index: 2;
|
||
}
|
||
|
||
.btn-empty-add.ant-btn {
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
padding: 0 24px;
|
||
font-weight: 600;
|
||
background: #2563eb;
|
||
border-color: #2563eb;
|
||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
|
||
transition: all 0.2s ease;
|
||
z-index: 2;
|
||
}
|
||
|
||
.btn-empty-add.ant-btn:hover {
|
||
background: #1d4ed8;
|
||
border-color: #1d4ed8;
|
||
box-shadow: 0 6px 16px rgba(37, 99, 235, 0.3);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.site-connection-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||
gap: 24px;
|
||
width: 100%;
|
||
}
|
||
|
||
.site-connection-card {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #ffffff;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
overflow: hidden;
|
||
box-shadow:
|
||
0 4px 6px -1px rgba(0, 0, 0, 0.02),
|
||
0 2px 4px -1px rgba(0, 0, 0, 0.01);
|
||
}
|
||
|
||
.site-connection-card:hover {
|
||
border-color: #cbd5e1;
|
||
box-shadow:
|
||
0 20px 25px -5px rgba(15, 23, 42, 0.08),
|
||
0 10px 10px -5px rgba(15, 23, 42, 0.04);
|
||
transform: translateY(-4px);
|
||
}
|
||
|
||
.site-card-accent {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 4px;
|
||
}
|
||
|
||
.site-card-accent.accent-connected {
|
||
background: linear-gradient(90deg, #10b981, #059669);
|
||
}
|
||
|
||
.site-card-accent.accent-error {
|
||
background: linear-gradient(90deg, #f43f5e, #e11d48);
|
||
}
|
||
|
||
.site-card-accent.accent-disabled {
|
||
background: linear-gradient(90deg, #94a3b8, #64748b);
|
||
}
|
||
|
||
.site-card-accent.accent-pending {
|
||
background: linear-gradient(90deg, #f59e0b, #d97706);
|
||
}
|
||
|
||
.site-connection-card__header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.site-connection-card__icon {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 12px;
|
||
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
||
color: #2563eb;
|
||
font-size: 22px;
|
||
border: 1px solid rgba(37, 99, 235, 0.1);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.site-connection-card__favicon {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 4px;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.site-connection-card__header-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.site-name-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.site-connection-card h4 {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: #0f172a;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.site-badge {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
padding: 1px 8px;
|
||
border-radius: 6px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.site-badge--cms {
|
||
background: #eff6ff;
|
||
color: #1e40af;
|
||
border: 1px solid #bfdbfe;
|
||
}
|
||
|
||
.site-badge--status {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
width: fit-content;
|
||
}
|
||
|
||
.site-badge__dot {
|
||
display: inline-block;
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.site-badge--status-connected {
|
||
background: rgba(16, 185, 129, 0.08);
|
||
color: #059669;
|
||
border: 1px solid rgba(16, 185, 129, 0.15);
|
||
}
|
||
|
||
.site-badge--status-connected .site-badge__dot {
|
||
background: #10b981;
|
||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
|
||
animation: pulse-dot-green 2s infinite;
|
||
}
|
||
|
||
@keyframes pulse-dot-green {
|
||
0% {
|
||
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
|
||
}
|
||
70% {
|
||
box-shadow: 0 0 0 6px rgba(16, 185, 129, 0);
|
||
}
|
||
100% {
|
||
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
|
||
}
|
||
}
|
||
|
||
.site-badge--status-error {
|
||
background: rgba(244, 63, 94, 0.08);
|
||
color: #e11d48;
|
||
border: 1px solid rgba(244, 63, 94, 0.15);
|
||
}
|
||
|
||
.site-badge--status-error .site-badge__dot {
|
||
background: #f43f5e;
|
||
box-shadow: 0 0 0 2px rgba(244, 63, 94, 0.2);
|
||
animation: pulse-dot-red 2s infinite;
|
||
}
|
||
|
||
@keyframes pulse-dot-red {
|
||
0% {
|
||
box-shadow: 0 0 0 0 rgba(244, 63, 94, 0.7);
|
||
}
|
||
70% {
|
||
box-shadow: 0 0 0 6px rgba(244, 63, 94, 0);
|
||
}
|
||
100% {
|
||
box-shadow: 0 0 0 0 rgba(244, 63, 94, 0);
|
||
}
|
||
}
|
||
|
||
.site-badge--status-disabled {
|
||
background: rgba(100, 116, 139, 0.08);
|
||
color: #475569;
|
||
border: 1px solid rgba(100, 116, 139, 0.15);
|
||
}
|
||
|
||
.site-badge--status-disabled .site-badge__dot {
|
||
background: #64748b;
|
||
}
|
||
|
||
.site-badge--status-pending {
|
||
background: rgba(245, 158, 11, 0.08);
|
||
color: #d97706;
|
||
border: 1px solid rgba(245, 158, 11, 0.15);
|
||
}
|
||
|
||
.site-badge--status-pending .site-badge__dot {
|
||
background: #f59e0b;
|
||
}
|
||
|
||
.site-url-box {
|
||
background: #f8fafc;
|
||
border: 1px solid #f1f5f9;
|
||
border-radius: 8px;
|
||
padding: 8px 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.site-link {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: #64748b;
|
||
font-size: 13px;
|
||
transition: color 0.2s ease;
|
||
}
|
||
|
||
.site-link:hover {
|
||
color: #2563eb;
|
||
}
|
||
|
||
.site-link .link-icon {
|
||
color: #94a3b8;
|
||
transition: color 0.2s ease;
|
||
}
|
||
|
||
.site-link:hover .link-icon {
|
||
color: #2563eb;
|
||
}
|
||
|
||
.site-meta-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.meta-cell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.meta-cell.full-width {
|
||
grid-column: span 2;
|
||
}
|
||
|
||
.meta-cell .meta-label {
|
||
font-size: 11px;
|
||
color: #94a3b8;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.meta-cell .meta-value {
|
||
font-size: 12.5px;
|
||
color: #475569;
|
||
font-weight: 600;
|
||
text-overflow: ellipsis;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.site-connection-card__footer-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-top: 1px solid #f1f5f9;
|
||
padding-top: 16px;
|
||
margin-top: auto;
|
||
}
|
||
|
||
.footer-actions-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.btn-action-ping.ant-btn,
|
||
.btn-action-sync.ant-btn {
|
||
height: 32px;
|
||
border-radius: 6px;
|
||
font-size: 12.5px;
|
||
font-weight: 500;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
border-color: #cbd5e1;
|
||
color: #475569;
|
||
background: #ffffff;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.btn-action-ping.ant-btn:hover,
|
||
.btn-action-sync.ant-btn:hover {
|
||
border-color: #2563eb;
|
||
color: #2563eb;
|
||
background: #eff6ff;
|
||
}
|
||
|
||
.action-more-btn.ant-btn-text {
|
||
height: 32px;
|
||
width: 32px;
|
||
border-radius: 6px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #64748b;
|
||
border: 1px solid #cbd5e1;
|
||
background: #ffffff;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.action-more-btn.ant-btn-text:hover {
|
||
color: #0f172a;
|
||
background: #f1f5f9;
|
||
}
|
||
|
||
.site-connection-card__error-banner {
|
||
margin-top: 12px;
|
||
padding: 12px 14px;
|
||
border: 1px solid #fecdd3;
|
||
border-left: 4px solid #f43f5e;
|
||
border-radius: 8px;
|
||
background: #fff5f5;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
}
|
||
|
||
.connector-sample {
|
||
background: #f8fafc;
|
||
border: 1px solid #e2e8f0;
|
||
border-left: 3px solid #475569;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.connector-sample__line {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
padding: 0;
|
||
border-bottom: none;
|
||
}
|
||
|
||
.connector-sample__line span {
|
||
color: #475569;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
background: #e2e8f0;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.connector-sample__line strong {
|
||
color: #0f172a;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
overflow-wrap: anywhere;
|
||
text-align: right;
|
||
background: rgba(15, 23, 42, 0.03);
|
||
padding: 3px 8px;
|
||
border-radius: 5px;
|
||
border: 1px solid rgba(15, 23, 42, 0.05);
|
||
}
|
||
|
||
.adapter-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.adapter-row {
|
||
display: grid;
|
||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||
gap: 14px;
|
||
align-items: center;
|
||
padding: 14px 16px;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 10px;
|
||
background: #ffffff;
|
||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.adapter-row:hover {
|
||
border-color: #cbd5e1;
|
||
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.03);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.adapter-row__mark {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 8px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.adapter-row--ready .adapter-row__mark {
|
||
background: #f0fdf4;
|
||
color: #16a34a;
|
||
border: 1px solid #dcfce7;
|
||
}
|
||
|
||
.adapter-row--planned {
|
||
background: #fafafb;
|
||
border-style: dashed;
|
||
}
|
||
|
||
.adapter-row--planned .adapter-row__mark {
|
||
background: #f1f5f9;
|
||
color: #64748b;
|
||
border: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.adapter-row__body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 3px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.adapter-row__body strong {
|
||
color: #0f172a;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.adapter-row__body span {
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.enterprise-site-form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
padding-top: 4px;
|
||
}
|
||
|
||
.form-field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 7px;
|
||
}
|
||
|
||
.form-field span {
|
||
color: #334155;
|
||
font-size: 13px;
|
||
font-weight: 650;
|
||
}
|
||
|
||
.enterprise-form-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.enterprise-form-grid--wordpress {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.enterprise-publish-hint {
|
||
padding: 10px 12px;
|
||
border: 1px solid #bfdbfe;
|
||
border-radius: 8px;
|
||
background: #eff6ff;
|
||
color: #1e40af;
|
||
font-size: 12.5px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.enterprise-publish-hint--warning {
|
||
border-color: #fde68a;
|
||
background: #fffbeb;
|
||
color: #92400e;
|
||
}
|
||
|
||
:global(.enterprise-site-cms-dropdown) {
|
||
z-index: 1100;
|
||
}
|
||
|
||
/* Spinners, alerts, empty state styling */
|
||
.premium-alert {
|
||
border-radius: 12px;
|
||
border: 1px solid #fecaca;
|
||
background: #fef2f2;
|
||
}
|
||
|
||
.premium-empty {
|
||
padding: 40px 0;
|
||
border: 1px dashed #e2e8f0;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
/* Media list panel & search toolbar */
|
||
.media-list-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24px;
|
||
}
|
||
|
||
.media-list-header-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
gap: 24px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.media-list-title-box {
|
||
flex: 1;
|
||
min-width: 250px;
|
||
}
|
||
|
||
.media-list-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
/* Styled Inputs & Selects */
|
||
:deep(.media-search.ant-input-affix-wrapper) {
|
||
width: 320px;
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
border-color: #cbd5e1;
|
||
background-color: #ffffff;
|
||
padding-left: 12px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
:deep(.media-search.ant-input-affix-wrapper:hover) {
|
||
border-color: #94a3b8;
|
||
}
|
||
|
||
:deep(.media-search.ant-input-affix-wrapper-focused) {
|
||
border-color: #1d4ed8;
|
||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.14);
|
||
}
|
||
|
||
:deep(.media-select.ant-select .ant-select-selector) {
|
||
height: 40px !important;
|
||
border-radius: 8px !important;
|
||
border-color: #cbd5e1 !important;
|
||
padding: 4px 12px !important;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
:deep(.media-select.ant-select:hover .ant-select-selector) {
|
||
border-color: #94a3b8 !important;
|
||
}
|
||
|
||
:deep(.media-select.ant-select-focused .ant-select-selector) {
|
||
border-color: #1d4ed8 !important;
|
||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.14) !important;
|
||
}
|
||
|
||
/* Media Card Grid */
|
||
.media-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(295px, 1fr));
|
||
gap: 20px;
|
||
}
|
||
|
||
/* Premium Media Card */
|
||
.media-card {
|
||
position: relative;
|
||
background: #ffffff;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
padding: 24px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||
overflow: hidden;
|
||
box-shadow: 0 4px 12px rgba(148, 163, 184, 0.03);
|
||
}
|
||
|
||
.media-card__accent-bar {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 4px;
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.media-card:hover {
|
||
transform: translateY(-5px);
|
||
border-color: #cbd5e1;
|
||
box-shadow: 0 16px 36px rgba(148, 163, 184, 0.12);
|
||
}
|
||
|
||
.media-card__head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
|
||
.media-card__identity {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.media-card__more.ant-btn-text {
|
||
width: 32px;
|
||
height: 32px;
|
||
flex-shrink: 0;
|
||
border-radius: 50%;
|
||
color: #64748b;
|
||
background: transparent;
|
||
border: none;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.media-card__more.ant-btn-text:hover,
|
||
.media-card__more.ant-btn-text:focus {
|
||
color: #0f172a;
|
||
background: #f1f5f9;
|
||
border: none;
|
||
}
|
||
|
||
/* Circular Avatar glow container */
|
||
.media-card__avatar {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
background: #f1f5f9;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.media-card:hover .media-card__avatar {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.media-card__avatar img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.avatar-initial {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #ffffff;
|
||
font-weight: 700;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.media-card__identity-copy {
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.media-card__identity-copy h3 {
|
||
margin: 0;
|
||
color: #0f172a;
|
||
font-size: 15.5px;
|
||
font-weight: 700;
|
||
line-height: 1.3;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.platform-logo-container {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.media-card__inline-platform-icon {
|
||
width: 16px;
|
||
height: 16px;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.media-card__inline-platform-text {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
padding: 1px 5px;
|
||
border-radius: 4px;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.media-card__identity-copy p {
|
||
margin: 5px 0 0;
|
||
color: #64748b;
|
||
font-size: 12.5px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.media-card__platform-name {
|
||
font-weight: 600;
|
||
color: #475569;
|
||
}
|
||
|
||
.media-card__uid-divider {
|
||
color: #cbd5e1;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.media-card__uid {
|
||
color: #94a3b8;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||
font-size: 11px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
flex: 1;
|
||
}
|
||
|
||
/* Polished Pill Badges */
|
||
.media-card__tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.badge-status {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 11.5px;
|
||
font-weight: 600;
|
||
padding: 3px 10px;
|
||
border-radius: 8px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.indicator-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
display: inline-block;
|
||
}
|
||
|
||
/* Colorful status mapping */
|
||
.badge-status--green {
|
||
background: rgba(16, 185, 129, 0.08);
|
||
color: #059669;
|
||
border: 1px solid rgba(16, 185, 129, 0.15);
|
||
}
|
||
.badge-status--green .indicator-dot {
|
||
background-color: #10b981;
|
||
}
|
||
|
||
.badge-status--orange {
|
||
background: rgba(245, 158, 11, 0.08);
|
||
color: #d97706;
|
||
border: 1px solid rgba(245, 158, 11, 0.15);
|
||
}
|
||
.badge-status--orange .indicator-dot {
|
||
background-color: #f59e0b;
|
||
}
|
||
|
||
.badge-status--gold {
|
||
background: rgba(217, 119, 6, 0.08);
|
||
color: #b45309;
|
||
border: 1px solid rgba(217, 119, 6, 0.15);
|
||
}
|
||
.badge-status--gold .indicator-dot {
|
||
background-color: #d97706;
|
||
}
|
||
|
||
.badge-status--red {
|
||
background: rgba(244, 63, 94, 0.08);
|
||
color: #e11d48;
|
||
border: 1px solid rgba(244, 63, 94, 0.15);
|
||
}
|
||
.badge-status--red .indicator-dot {
|
||
background-color: #f43f5e;
|
||
}
|
||
|
||
.badge-status--default {
|
||
background: rgba(100, 116, 139, 0.08);
|
||
color: #475569;
|
||
border: 1px solid rgba(100, 116, 139, 0.15);
|
||
}
|
||
.badge-status--default .indicator-dot {
|
||
background-color: #64748b;
|
||
}
|
||
|
||
/* Client runtime block */
|
||
.media-card__client-info {
|
||
background: #f8fafc;
|
||
border: 1px solid #f1f5f9;
|
||
border-radius: 8px;
|
||
padding: 10px 12px;
|
||
}
|
||
|
||
.client-info-inner {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.client-label {
|
||
color: #64748b;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.client-value {
|
||
font-weight: 600;
|
||
max-width: 160px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.client-value--green {
|
||
color: #059669;
|
||
}
|
||
|
||
.client-value--gold {
|
||
color: #d97706;
|
||
}
|
||
|
||
.client-value--default {
|
||
color: #64748b;
|
||
}
|
||
|
||
/* Metadata box */
|
||
.media-card__meta-box {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
border-top: 1px solid #f1f5f9;
|
||
padding-top: 12px;
|
||
}
|
||
|
||
.meta-box-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 11.5px;
|
||
}
|
||
|
||
.meta-label {
|
||
color: #94a3b8;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.meta-value {
|
||
color: #475569;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* Card footer details hints */
|
||
.media-card__footer {
|
||
margin-top: auto;
|
||
border-top: 1px dashed #f1f5f9;
|
||
padding-top: 12px;
|
||
}
|
||
|
||
.footer-alert {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 11px;
|
||
color: #64748b;
|
||
background: #f8fafc;
|
||
border-radius: 6px;
|
||
padding: 6px 10px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.footer-alert svg {
|
||
flex-shrink: 0;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.footer-alert span {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.footer-alert--warning {
|
||
background: #fffbeb;
|
||
color: #b45309;
|
||
border: 1px solid #fef3c7;
|
||
}
|
||
|
||
.footer-alert--warning svg {
|
||
color: #d97706;
|
||
}
|
||
|
||
/* Responsiveness adjustments */
|
||
@media (max-width: 1024px) {
|
||
.enterprise-panel__main,
|
||
.enterprise-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.enterprise-actions {
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.enterprise-flow {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.enterprise-step:nth-child(2) {
|
||
border-right: 0;
|
||
}
|
||
|
||
.enterprise-step:nth-child(-n + 2) {
|
||
border-bottom: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.overview-strip {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
|
||
.overview-chip {
|
||
border-bottom: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.overview-chip:nth-child(even) {
|
||
border-right: none;
|
||
}
|
||
|
||
.overview-chip:nth-child(n + 3) {
|
||
border-bottom: none;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.media-view__header {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: 20px;
|
||
padding: 24px;
|
||
}
|
||
|
||
.media-view__header-actions {
|
||
width: 100%;
|
||
}
|
||
|
||
.media-view__header-actions :deep(.ant-btn) {
|
||
width: 100%;
|
||
}
|
||
|
||
.media-scope-tabs {
|
||
margin: 0 24px 20px;
|
||
display: flex;
|
||
padding: 4px;
|
||
}
|
||
|
||
.media-scope-tab {
|
||
flex: 1;
|
||
min-width: 0;
|
||
justify-content: center;
|
||
}
|
||
|
||
.site-connection-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.overview-chip {
|
||
padding: 16px 20px;
|
||
}
|
||
|
||
.panel {
|
||
padding: 20px;
|
||
}
|
||
|
||
.media-list-header-row {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: 16px;
|
||
}
|
||
|
||
.media-list-toolbar {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.media-search.ant-input-affix-wrapper) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.media-select.ant-select) {
|
||
width: 100% !important;
|
||
}
|
||
|
||
.enterprise-flow {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.enterprise-step,
|
||
.enterprise-step:nth-child(2) {
|
||
border-right: 0;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.enterprise-step:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
|
||
.connector-empty {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.connector-sample__line,
|
||
.adapter-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.enterprise-form-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.overview-strip {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.overview-chip {
|
||
border-right: none;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.overview-chip:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.client-download-grid,
|
||
.media-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.site-connection-card {
|
||
padding: 16px;
|
||
}
|
||
|
||
.site-meta-grid {
|
||
grid-template-columns: 1fr;
|
||
gap: 8px;
|
||
}
|
||
|
||
.meta-cell.full-width {
|
||
grid-column: span 1;
|
||
}
|
||
|
||
.site-connection-card__footer-actions {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: 12px;
|
||
}
|
||
|
||
.btn-action-ping.ant-btn {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
|
||
.footer-actions-right {
|
||
justify-content: space-between;
|
||
width: 100%;
|
||
}
|
||
|
||
.btn-action-sync.ant-btn {
|
||
flex: 1;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
:deep(.action-more-btn.ant-btn-text) {
|
||
color: #64748b;
|
||
background: transparent;
|
||
border: 1px solid #cbd5e1;
|
||
}
|
||
|
||
:deep(.action-more-btn.ant-btn-text:hover) {
|
||
color: #0f172a;
|
||
background: #f1f5f9;
|
||
}
|
||
|
||
:deep(.ant-dropdown-menu-item) {
|
||
padding: 8px 14px;
|
||
font-size: 13.5px;
|
||
color: #334155;
|
||
}
|
||
|
||
:deep(.ant-dropdown-menu-title-content) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
:deep(.ant-dropdown-menu-item .anticon) {
|
||
font-size: 15px;
|
||
color: #64748b;
|
||
transition: color 0.2s ease;
|
||
}
|
||
|
||
:deep(.ant-dropdown-menu-item:not(.ant-dropdown-menu-item-danger):hover .anticon) {
|
||
color: #2563eb;
|
||
}
|
||
|
||
:deep(.ant-dropdown-menu-item-danger:hover .anticon) {
|
||
color: #ffffff;
|
||
}
|
||
|
||
/* Custom Account Unbind Modal Styles */
|
||
.premium-unbind-modal :deep(.ant-modal-content) {
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.premium-unbind-modal :deep(.ant-modal-header) {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.unbind-modal-header {
|
||
text-align: center;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.unbind-warning-icon-wrapper {
|
||
width: 56px;
|
||
height: 56px;
|
||
background-color: #fef2f2;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin: 0 auto 16px;
|
||
border: 1px solid #fee2e2;
|
||
animation: pulse-warning 2s infinite ease-in-out;
|
||
}
|
||
|
||
@keyframes pulse-warning {
|
||
0%, 100% {
|
||
transform: scale(1);
|
||
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.15);
|
||
}
|
||
50% {
|
||
transform: scale(1.05);
|
||
box-shadow: 0 0 0 10px rgba(239, 68, 68, 0);
|
||
}
|
||
}
|
||
|
||
.unbind-warning-icon {
|
||
font-size: 24px;
|
||
color: #ef4444;
|
||
}
|
||
|
||
.unbind-modal-title {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: #0f172a;
|
||
margin: 0 0 8px;
|
||
}
|
||
|
||
.unbind-modal-subtitle {
|
||
font-size: 13px;
|
||
color: #64748b;
|
||
margin: 0;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* Account Preview inside Modal */
|
||
.unbind-account-preview {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
background-color: #f8fafc;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 12px;
|
||
padding: 12px 16px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.preview-avatar-wrapper {
|
||
position: relative;
|
||
width: 40px;
|
||
height: 40px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.preview-avatar {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 50%;
|
||
object-fit: cover;
|
||
border: 1px solid #cbd5e1;
|
||
}
|
||
|
||
.preview-avatar-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #fff;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.preview-platform-icon {
|
||
position: absolute;
|
||
bottom: -2px;
|
||
right: -2px;
|
||
width: 16px;
|
||
height: 16px;
|
||
background: white;
|
||
border-radius: 50%;
|
||
padding: 2px;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.preview-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.preview-name {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.preview-meta {
|
||
font-size: 12px;
|
||
color: #64748b;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.preview-status {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Consequences List */
|
||
.unbind-consequences {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
background-color: #fffaf0;
|
||
border: 1px dashed #feebc8;
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.consequence-item {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.consequence-icon-wrapper {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 18px;
|
||
height: 18px;
|
||
flex-shrink: 0;
|
||
margin-top: 2px;
|
||
color: #dd6b20;
|
||
}
|
||
|
||
.consequence-icon-wrapper .anticon {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.consequence-text {
|
||
font-size: 13px;
|
||
color: #7b341e;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.consequence-text strong {
|
||
color: #dd6b20;
|
||
}
|
||
|
||
/* Verification Input */
|
||
.unbind-verification {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.verification-label {
|
||
font-size: 13px;
|
||
color: #475569;
|
||
margin-bottom: 8px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.highlight-name {
|
||
font-family: monospace;
|
||
font-weight: 700;
|
||
color: #ef4444;
|
||
background-color: #fef2f2;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
border: 1px solid #fee2e2;
|
||
margin: 0 2px;
|
||
}
|
||
|
||
.highlight-keyword {
|
||
font-weight: 700;
|
||
color: #3b82f6;
|
||
background-color: #eff6ff;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
border: 1px solid #dbeafe;
|
||
margin: 0 2px;
|
||
}
|
||
|
||
.unbind-input.ant-input {
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
border-color: #cbd5e1;
|
||
font-size: 14px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.unbind-input.ant-input:focus {
|
||
border-color: #ef4444;
|
||
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.12);
|
||
}
|
||
|
||
/* Footer Actions */
|
||
.unbind-modal-footer {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.unbind-modal-footer .btn-cancel {
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
padding: 0 16px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #475569;
|
||
border-color: #cbd5e1;
|
||
}
|
||
|
||
.unbind-modal-footer .btn-cancel:hover {
|
||
color: #1e293b;
|
||
border-color: #94a3b8;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.unbind-modal-footer .btn-confirm {
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
padding: 0 20px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Premium Dropdown styles */
|
||
:global(.premium-unbind-dropdown .ant-dropdown-menu) {
|
||
border-radius: 12px !important;
|
||
padding: 6px !important;
|
||
border: 1px solid #f1f5f9 !important;
|
||
box-shadow: 0 10px 25px -5px rgba(15, 23, 42, 0.08), 0 8px 16px -6px rgba(15, 23, 42, 0.04) !important;
|
||
}
|
||
|
||
:global(.premium-unbind-dropdown .ant-dropdown-menu-item) {
|
||
border-radius: 8px !important;
|
||
padding: 8px 16px !important;
|
||
font-size: 13.5px !important;
|
||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||
}
|
||
|
||
:global(.premium-unbind-dropdown .ant-dropdown-menu-item:not(.ant-dropdown-menu-item-danger):hover) {
|
||
background-color: #eff6ff !important;
|
||
color: #2563eb !important;
|
||
}
|
||
|
||
:global(.premium-unbind-dropdown .ant-dropdown-menu-item:not(.ant-dropdown-menu-item-danger):hover .anticon) {
|
||
color: #2563eb !important;
|
||
}
|
||
|
||
:global(.premium-unbind-dropdown .ant-dropdown-menu-item-danger) {
|
||
color: #ef4444 !important;
|
||
}
|
||
|
||
:global(.premium-unbind-dropdown .ant-dropdown-menu-item-danger .anticon) {
|
||
color: #ef4444 !important;
|
||
font-size: 15px !important;
|
||
}
|
||
|
||
:global(.premium-unbind-dropdown .ant-dropdown-menu-item-danger:hover) {
|
||
background-color: #fef2f2 !important;
|
||
color: #dc2626 !important;
|
||
}
|
||
|
||
:global(.premium-unbind-dropdown .ant-dropdown-menu-item-danger:hover .anticon) {
|
||
color: #dc2626 !important;
|
||
}
|
||
</style>
|