chore(frontend): introduce prettier + eslint and prune unused code
- Add Prettier 3 with prettier-plugin-organize-imports (sorts/removes unused imports) - Add ESLint 10 flat config with typescript-eslint + eslint-plugin-vue + eslint-config-prettier - Add root scripts: format, format:check, lint, lint:fix - Reformat 257 files across admin-web, ops-web, desktop-client, packages - Remove unused locals/exports flagged by --noUnusedLocals/--noUnusedParameters - Fix duplicate localTabLabel key, surrogate-pair regex u-flag, NBSP literal in regex - Skip server/ (Go) and apps/browser-extension/ (deprecated per ADR) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,72 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { ReloadOutlined, SearchOutlined } from "@ant-design/icons-vue";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import type { DesktopAccountInfo, MediaPlatform } from "@geo/shared-types";
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ReloadOutlined, SearchOutlined } from '@ant-design/icons-vue'
|
||||
import type { DesktopAccountInfo, MediaPlatform } from '@geo/shared-types'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { mediaApi, resolveApiURL, tenantAccountsApi } from "@/lib/api";
|
||||
import { resolveAccountCheckedAt, resolveAccountHealth } from "@/lib/desktop-account-runtime";
|
||||
import { formatDateTime } from "@/lib/display";
|
||||
import { getPublishPlatformMeta, isMediaPublishAccount, normalizePublishPlatformId } from "@/lib/publish-platforms";
|
||||
import { mediaApi, resolveApiURL, tenantAccountsApi } from '@/lib/api'
|
||||
import { resolveAccountCheckedAt, resolveAccountHealth } from '@/lib/desktop-account-runtime'
|
||||
import { formatDateTime } from '@/lib/display'
|
||||
import {
|
||||
getPublishPlatformMeta,
|
||||
isMediaPublishAccount,
|
||||
normalizePublishPlatformId,
|
||||
} from '@/lib/publish-platforms'
|
||||
|
||||
type AccountHealthFilter = "all" | "live" | "captcha" | "risk" | "expired";
|
||||
type PublishState = "immediate" | "queued" | "unavailable";
|
||||
type AccountHealthFilter = 'all' | 'live' | 'captcha' | 'risk' | 'expired'
|
||||
type PublishState = 'immediate' | 'queued' | 'unavailable'
|
||||
|
||||
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;
|
||||
clientDeviceName: string | null;
|
||||
clientLastSeenAt: string | null;
|
||||
publishState: PublishState;
|
||||
publishHint: string;
|
||||
deleteRequestedAt: string | null;
|
||||
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
|
||||
clientDeviceName: string | null
|
||||
clientLastSeenAt: string | null
|
||||
publishState: PublishState
|
||||
publishHint: string
|
||||
deleteRequestedAt: string | null
|
||||
}
|
||||
|
||||
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",
|
||||
};
|
||||
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 { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
|
||||
const searchQuery = ref("");
|
||||
const selectedHealth = ref<AccountHealthFilter>("all");
|
||||
const searchQuery = ref('')
|
||||
const selectedHealth = ref<AccountHealthFilter>('all')
|
||||
|
||||
const platformsQuery = useQuery({
|
||||
queryKey: ["media", "platforms", "media-view"],
|
||||
queryKey: ['media', 'platforms', 'media-view'],
|
||||
queryFn: () => mediaApi.platforms(),
|
||||
});
|
||||
})
|
||||
|
||||
const accountsQuery = useQuery({
|
||||
queryKey: ["tenant", "desktop-accounts", "media-view"],
|
||||
queryKey: ['tenant', 'desktop-accounts', 'media-view'],
|
||||
queryFn: () => tenantAccountsApi.list(),
|
||||
});
|
||||
})
|
||||
|
||||
const loading = computed(() => platformsQuery.isPending.value || accountsQuery.isPending.value);
|
||||
const refreshing = computed(() => platformsQuery.isFetching.value || accountsQuery.isFetching.value);
|
||||
const loading = computed(() => platformsQuery.isPending.value || accountsQuery.isPending.value)
|
||||
const refreshing = computed(() => platformsQuery.isFetching.value || accountsQuery.isFetching.value)
|
||||
|
||||
const platformMap = computed(() => {
|
||||
return new Map(
|
||||
@@ -74,18 +78,18 @@ const platformMap = computed(() => {
|
||||
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);
|
||||
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,
|
||||
@@ -106,182 +110,182 @@ const mediaAccounts = computed<MediaAccountCard[]>(() => {
|
||||
publishState,
|
||||
publishHint: resolvePublishHint(account, publishState),
|
||||
deleteRequestedAt: account.delete_requested_at,
|
||||
};
|
||||
}
|
||||
})
|
||||
.sort((left, right) => {
|
||||
const publishGap = publishRank(left.publishState) - publishRank(right.publishState);
|
||||
const publishGap = publishRank(left.publishState) - publishRank(right.publishState)
|
||||
if (publishGap !== 0) {
|
||||
return publishGap;
|
||||
return publishGap
|
||||
}
|
||||
|
||||
const leftAt = Date.parse(left.verifiedAt ?? "") || 0;
|
||||
const rightAt = Date.parse(right.verifiedAt ?? "") || 0;
|
||||
return rightAt - leftAt;
|
||||
});
|
||||
});
|
||||
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,
|
||||
}));
|
||||
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 filteredAccounts = computed(() => {
|
||||
const keyword = searchQuery.value.trim().toLowerCase();
|
||||
const keyword = searchQuery.value.trim().toLowerCase()
|
||||
|
||||
return mediaAccounts.value.filter((account) => {
|
||||
if (selectedHealth.value !== "all" && account.health !== selectedHealth.value) {
|
||||
return false;
|
||||
if (selectedHealth.value !== 'all' && account.health !== selectedHealth.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!keyword) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
return [
|
||||
account.displayName,
|
||||
account.platformLabel,
|
||||
account.platformUid,
|
||||
account.clientDeviceName ?? "",
|
||||
].join(" ").toLowerCase().includes(keyword);
|
||||
});
|
||||
});
|
||||
account.clientDeviceName ?? '',
|
||||
]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(keyword)
|
||||
})
|
||||
})
|
||||
|
||||
function resolvePublishState(account: DesktopAccountInfo): PublishState {
|
||||
if (resolveAccountHealth(account) !== "live") {
|
||||
return "unavailable";
|
||||
if (resolveAccountHealth(account) !== 'live') {
|
||||
return 'unavailable'
|
||||
}
|
||||
if (!account.client_id) {
|
||||
return "unavailable";
|
||||
return 'unavailable'
|
||||
}
|
||||
return account.client_online === true ? "immediate" : "queued";
|
||||
return account.client_online === true ? 'immediate' : 'queued'
|
||||
}
|
||||
|
||||
function resolvePublishHint(account: DesktopAccountInfo, publishState: PublishState): string {
|
||||
if (publishState === "immediate") {
|
||||
return "";
|
||||
if (publishState === 'immediate') {
|
||||
return ''
|
||||
}
|
||||
if (publishState === "queued") {
|
||||
return "客户端当前离线,任务会先进入发布队列,等桌面端上线后自动继续。";
|
||||
if (publishState === 'queued') {
|
||||
return '客户端当前离线,任务会先进入发布队列,等桌面端上线后自动继续。'
|
||||
}
|
||||
if (resolveAccountHealth(account) !== "live") {
|
||||
return "授权状态异常,先在桌面端重新校验后再发布。";
|
||||
if (resolveAccountHealth(account) !== 'live') {
|
||||
return '授权状态异常,先在桌面端重新校验后再发布。'
|
||||
}
|
||||
return "还没有绑定桌面客户端,当前无法加入发布队列。";
|
||||
return '还没有绑定桌面客户端,当前无法加入发布队列。'
|
||||
}
|
||||
|
||||
function normalizePlatformUid(value?: string | null): string {
|
||||
let normalized = String(value ?? "").trim();
|
||||
while (normalized.toLowerCase().startsWith("platform:")) {
|
||||
normalized = normalized.slice("platform:".length).trim();
|
||||
let normalized = String(value ?? '').trim()
|
||||
while (normalized.toLowerCase().startsWith('platform:')) {
|
||||
normalized = normalized.slice('platform:'.length).trim()
|
||||
}
|
||||
return normalized || "--";
|
||||
return normalized || '--'
|
||||
}
|
||||
|
||||
function resolvePlatformLogoUrl(platformId: string, remoteLogoUrl?: string | null): string | null {
|
||||
const localLogoUrl = platformLogoCatalog[platformId];
|
||||
const localLogoUrl = platformLogoCatalog[platformId]
|
||||
if (localLogoUrl) {
|
||||
return localLogoUrl;
|
||||
return localLogoUrl
|
||||
}
|
||||
|
||||
const resolvedRemoteLogoUrl = resolveApiURL(remoteLogoUrl);
|
||||
return resolvedRemoteLogoUrl || null;
|
||||
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;
|
||||
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;
|
||||
case 'immediate':
|
||||
return 0
|
||||
case 'queued':
|
||||
return 1
|
||||
default:
|
||||
return 2;
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
function healthLabel(health: DesktopAccountInfo["health"]): string {
|
||||
function healthLabel(health: DesktopAccountInfo['health']): string {
|
||||
switch (health) {
|
||||
case "live":
|
||||
return "授权正常";
|
||||
case "captcha":
|
||||
return "待处理";
|
||||
case "risk":
|
||||
return "风险观察";
|
||||
case 'live':
|
||||
return '授权正常'
|
||||
case 'captcha':
|
||||
return '待处理'
|
||||
case 'risk':
|
||||
return '风险观察'
|
||||
default:
|
||||
return "授权异常";
|
||||
return '授权异常'
|
||||
}
|
||||
}
|
||||
|
||||
function healthColor(health: DesktopAccountInfo["health"]): string {
|
||||
function healthColor(health: DesktopAccountInfo['health']): string {
|
||||
switch (health) {
|
||||
case "live":
|
||||
return "green";
|
||||
case "captcha":
|
||||
return "orange";
|
||||
case "risk":
|
||||
return "gold";
|
||||
case 'live':
|
||||
return 'green'
|
||||
case 'captcha':
|
||||
return 'orange'
|
||||
case 'risk':
|
||||
return 'gold'
|
||||
default:
|
||||
return "red";
|
||||
return 'red'
|
||||
}
|
||||
}
|
||||
|
||||
function clientStatusLabel(account: MediaAccountCard): string {
|
||||
if (!account.clientId) {
|
||||
return "未绑定";
|
||||
return '未绑定'
|
||||
}
|
||||
return account.clientOnline ? "在线" : "离线";
|
||||
return account.clientOnline ? '在线' : '离线'
|
||||
}
|
||||
|
||||
function clientStatusColor(account: MediaAccountCard): string {
|
||||
if (!account.clientId) {
|
||||
return "default";
|
||||
return 'default'
|
||||
}
|
||||
return account.clientOnline ? "green" : "gold";
|
||||
return account.clientOnline ? 'green' : 'gold'
|
||||
}
|
||||
|
||||
function clientStatusText(account: MediaAccountCard): string {
|
||||
if (!account.clientId) {
|
||||
return "未绑定桌面客户端";
|
||||
return '未绑定桌面客户端'
|
||||
}
|
||||
|
||||
const device = account.clientDeviceName?.trim();
|
||||
return device ? `${clientStatusLabel(account)} · ${device}` : clientStatusLabel(account);
|
||||
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 "离线排队";
|
||||
case 'immediate':
|
||||
return '可立即发布'
|
||||
case 'queued':
|
||||
return '离线排队'
|
||||
default:
|
||||
return "不可发布";
|
||||
return '不可发布'
|
||||
}
|
||||
}
|
||||
|
||||
function publishStateColor(state: PublishState): string {
|
||||
switch (state) {
|
||||
case "immediate":
|
||||
return "green";
|
||||
case "queued":
|
||||
return "gold";
|
||||
case 'immediate':
|
||||
return 'green'
|
||||
case 'queued':
|
||||
return 'gold'
|
||||
default:
|
||||
return "red";
|
||||
return 'red'
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAll(): Promise<void> {
|
||||
await Promise.all([
|
||||
platformsQuery.refetch(),
|
||||
accountsQuery.refetch(),
|
||||
]);
|
||||
await Promise.all([platformsQuery.refetch(), accountsQuery.refetch()])
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -290,8 +294,10 @@ async function refreshAll(): Promise<void> {
|
||||
<section class="media-view__top-card">
|
||||
<div class="media-view__header">
|
||||
<div class="media-view__header-title">
|
||||
<h2>{{ t("route.media.title") }}</h2>
|
||||
<p>这里展示客户端里当前用户已绑定的媒体账号,以及展示它们是否能立即发布、离线排队或需要处理。</p>
|
||||
<h2>{{ t('route.media.title') }}</h2>
|
||||
<p>
|
||||
这里展示客户端里当前用户已绑定的媒体账号,以及展示它们是否能立即发布、离线排队或需要处理。
|
||||
</p>
|
||||
</div>
|
||||
<div class="media-view__header-actions">
|
||||
<a-button :loading="refreshing" @click="refreshAll">
|
||||
@@ -354,15 +360,16 @@ async function refreshAll(): Promise<void> {
|
||||
/>
|
||||
|
||||
<section v-else class="media-grid">
|
||||
<article
|
||||
v-for="account in filteredAccounts"
|
||||
:key="account.id"
|
||||
class="media-card"
|
||||
>
|
||||
<article v-for="account in filteredAccounts" :key="account.id" class="media-card">
|
||||
<div class="media-card__head">
|
||||
<div class="media-card__identity">
|
||||
<span class="media-card__avatar" :style="{ background: account.platformAccent }">
|
||||
<img v-if="account.avatarUrl" :src="account.avatarUrl" :alt="account.displayName" referrerpolicy="no-referrer" />
|
||||
<img
|
||||
v-if="account.avatarUrl"
|
||||
:src="account.avatarUrl"
|
||||
:alt="account.displayName"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<span v-else>{{ accountInitial(account) }}</span>
|
||||
</span>
|
||||
|
||||
@@ -389,11 +396,15 @@ async function refreshAll(): Promise<void> {
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">授权状态</span>
|
||||
<a-tag :color="healthColor(account.health)">{{ healthLabel(account.health) }}</a-tag>
|
||||
<a-tag :color="healthColor(account.health)">
|
||||
{{ healthLabel(account.health) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">最近校验</span>
|
||||
<span class="meta-value">{{ account.verifiedAt ? formatDateTime(account.verifiedAt) : "--" }}</span>
|
||||
<span class="meta-value">
|
||||
{{ account.verifiedAt ? formatDateTime(account.verifiedAt) : '--' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">客户端状态</span>
|
||||
@@ -405,11 +416,15 @@ async function refreshAll(): Promise<void> {
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">最近心跳</span>
|
||||
<span class="meta-value">{{ account.clientLastSeenAt ? formatDateTime(account.clientLastSeenAt) : "--" }}</span>
|
||||
<span class="meta-value">
|
||||
{{ account.clientLastSeenAt ? formatDateTime(account.clientLastSeenAt) : '--' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">发布状态</span>
|
||||
<a-tag :color="publishStateColor(account.publishState)">{{ publishStateLabel(account.publishState) }}</a-tag>
|
||||
<a-tag :color="publishStateColor(account.publishState)">
|
||||
{{ publishStateLabel(account.publishState) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user