chore(frontend): introduce prettier + eslint and prune unused code
Backend CI / Backend (push) Has been cancelled
Frontend CI / Frontend (push) Failing after 1m39s

- 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:
2026-05-01 20:39:09 +08:00
parent 21c7892ee4
commit 162abdc97c
258 changed files with 42261 additions and 37556 deletions
+170 -155
View File
@@ -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>