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
@@ -1,30 +1,33 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{
tone?: "primary" | "secondary" | "ghost" | "danger" | "text";
size?: "sm" | "md";
loading?: boolean;
disabled?: boolean;
type?: "button" | "submit" | "reset";
title?: string;
}>(), {
tone: "secondary",
size: "md",
loading: false,
disabled: false,
type: "button",
title: undefined,
});
const props = withDefaults(
defineProps<{
tone?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'text'
size?: 'sm' | 'md'
loading?: boolean
disabled?: boolean
type?: 'button' | 'submit' | 'reset'
title?: string
}>(),
{
tone: 'secondary',
size: 'md',
loading: false,
disabled: false,
type: 'button',
title: undefined,
},
)
const emit = defineEmits<{
(event: "click", payload: MouseEvent): void;
}>();
(event: 'click', payload: MouseEvent): void
}>()
function handleClick(event: MouseEvent) {
if (props.loading || props.disabled) {
event.preventDefault();
return;
event.preventDefault()
return
}
emit("click", event);
emit('click', event)
}
</script>
@@ -57,7 +60,11 @@ function handleClick(event: MouseEvent) {
background: #ffffff;
color: #1f2937;
cursor: pointer;
transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease, opacity 0.2s ease;
transition:
border-color 0.2s ease,
background-color 0.2s ease,
color 0.2s ease,
opacity 0.2s ease;
}
.button:disabled {
@@ -1,79 +1,79 @@
<script setup lang="ts">
import { computed } from "vue";
import { RouterLink, RouterView, useRoute } from "vue-router";
import {
AppstoreOutlined,
SendOutlined,
LinkOutlined,
RobotOutlined,
LogoutOutlined,
RobotOutlined,
SendOutlined,
SettingOutlined,
} from "@ant-design/icons-vue";
} from '@ant-design/icons-vue'
import { computed } from 'vue'
import { RouterLink, RouterView, useRoute } from 'vue-router'
import { useDesktopRuntime } from "../composables/useDesktopRuntime";
import { useDesktopSession } from "../composables/useDesktopSession";
import { desktopMonitoringMediaCatalog, desktopPublishMediaCatalog } from "../lib/media-catalog";
import { useDesktopRuntime } from '../composables/useDesktopRuntime'
import { useDesktopSession } from '../composables/useDesktopSession'
import { desktopMonitoringMediaCatalog, desktopPublishMediaCatalog } from '../lib/media-catalog'
const route = useRoute();
const { snapshot, error, refreshAccounts } = useDesktopRuntime();
const { session, logout } = useDesktopSession();
const route = useRoute()
const { snapshot, error, refreshAccounts } = useDesktopRuntime()
const { session, logout } = useDesktopSession()
const accountAwareRoutes = new Set(["/media-platforms", "/ai-platforms"]);
const accountAwareRoutes = new Set(['/media-platforms', '/ai-platforms'])
function handleNavClick(target: string) {
if (accountAwareRoutes.has(target)) {
void refreshAccounts();
void refreshAccounts()
}
}
const navItems = computed(() => {
const data = snapshot.value;
const accounts = data?.accounts ?? [];
const publishIDs = new Set(desktopPublishMediaCatalog.map((item) => item.id));
const monitoringIDs = new Set(desktopMonitoringMediaCatalog.map((item) => item.id));
const data = snapshot.value
const accounts = data?.accounts ?? []
const publishIDs = new Set(desktopPublishMediaCatalog.map((item) => item.id))
const monitoringIDs = new Set(desktopMonitoringMediaCatalog.map((item) => item.id))
return [
{
to: "/",
title: "控制台",
description: "运行总览、活动流和总体健康度",
to: '/',
title: '控制台',
description: '运行总览、活动流和总体健康度',
count: data?.summary.issuesOpen ?? 0,
icon: AppstoreOutlined,
},
{
to: "/publish-management",
title: "发布管理",
description: "查看待发布队列、历史发送结果,并对文章再次发送",
to: '/publish-management',
title: '发布管理',
description: '查看待发布队列、历史发送结果,并对文章再次发送',
count: data?.summary.queuedTasks ?? 0,
icon: SendOutlined,
},
{
to: "/media-platforms",
title: "媒体账号",
description: "发布媒体授权、多个账号绑定管理",
to: '/media-platforms',
title: '媒体账号',
description: '发布媒体授权、多个账号绑定管理',
count: accounts.filter((item) => publishIDs.has(item.platform)).length,
icon: LinkOutlined,
},
{
to: "/ai-platforms",
title: "AI 平台",
description: "AI 平台一平台一绑定,按卡片查看状态与归属",
to: '/ai-platforms',
title: 'AI 平台',
description: 'AI 平台一平台一绑定,按卡片查看状态与归属',
count: accounts.filter((item) => monitoringIDs.has(item.platform)).length,
icon: RobotOutlined,
},
];
});
]
})
const operatorName = computed(() => session.value?.user?.name ?? "未命名操作员");
const operatorEmail = computed(() => session.value?.user?.email ?? "");
const liveClientLabel = computed(() => snapshot.value?.clients[0]?.deviceName ?? null);
const operatorName = computed(() => session.value?.user?.name ?? '未命名操作员')
const operatorEmail = computed(() => session.value?.user?.email ?? '')
const liveClientLabel = computed(() => snapshot.value?.clients[0]?.deviceName ?? null)
const clientLabel = computed(
() => liveClientLabel.value ?? session.value?.desktopClient?.device_name ?? "当前设备尚未注册 client",
);
() =>
liveClientLabel.value ?? session.value?.desktopClient?.device_name ?? '当前设备尚未注册 client',
)
function openSettingsWindow() {
void window.desktopBridge.app.openSettingsWindow();
void window.desktopBridge.app.openSettingsWindow()
}
</script>
@@ -95,19 +95,19 @@ function openSettingsWindow() {
<nav class="nav">
<RouterLink
v-for="item in navItems"
:key="item.to"
:to="item.to"
class="nav-link"
:class="{ active: route.path === item.to }"
@click="handleNavClick(item.to)"
>
<div class="nav-copy">
<component :is="item.icon" class="nav-icon" />
<strong>{{ item.title }}</strong>
</div>
<span class="nav-count">{{ item.count }}</span>
</RouterLink>
</nav>
:key="item.to"
:to="item.to"
class="nav-link"
:class="{ active: route.path === item.to }"
@click="handleNavClick(item.to)"
>
<div class="nav-copy">
<component :is="item.icon" class="nav-icon" />
<strong>{{ item.title }}</strong>
</div>
<span class="nav-count">{{ item.count }}</span>
</RouterLink>
</nav>
<footer class="sidebar-footer">
<section class="user-profile-panel">
@@ -132,7 +132,11 @@ function openSettingsWindow() {
</button>
</a-tooltip>
<a-tooltip title="退出登录" placement="top">
<button type="button" class="footer-icon-btn footer-icon-btn--danger" @click="logout">
<button
type="button"
class="footer-icon-btn footer-icon-btn--danger"
@click="logout"
>
<LogoutOutlined />
</button>
</a-tooltip>
@@ -142,7 +146,7 @@ function openSettingsWindow() {
<div class="footer-bottom-row">
<span class="version-label">Version</span>
<strong class="version-value">{{ snapshot?.app.version ?? "0.1.0" }}</strong>
<strong class="version-value">{{ snapshot?.app.version ?? '0.1.0' }}</strong>
</div>
<p v-if="error" class="error-text">{{ error }}</p>
</footer>
@@ -177,13 +181,13 @@ function openSettingsWindow() {
-webkit-app-region: drag;
}
:global(html[data-platform="darwin"]) .window-drag-bar {
:global(html[data-platform='darwin']) .window-drag-bar {
left: 80px;
right: 0;
height: 28px;
}
.app-shell :where(button, a, input, select, textarea, [role="button"], [role="link"]) {
.app-shell :where(button, a, input, select, textarea, [role='button'], [role='link']) {
-webkit-app-region: no-drag;
}
@@ -344,13 +348,17 @@ h1 {
border-radius: 16px;
background: #ffffff;
border: 1px solid #e2e8f0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02), 0 2px 4px -2px rgba(0, 0, 0, 0.01);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.02),
0 2px 4px -2px rgba(0, 0, 0, 0.01);
transition: all 0.2s ease;
}
.user-profile-panel:hover {
border-color: #cbd5e1;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.03), 0 2px 4px -2px rgba(0, 0, 0, 0.02);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.03),
0 2px 4px -2px rgba(0, 0, 0, 0.02);
}
.user-info-row {
@@ -371,7 +379,7 @@ h1 {
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
.user-details {
@@ -1,10 +1,13 @@
<script setup lang="ts">
withDefaults(defineProps<{
title: string;
open?: boolean;
}>(), {
open: false,
});
withDefaults(
defineProps<{
title: string
open?: boolean
}>(),
{
open: false,
},
)
</script>
<template>
@@ -56,7 +59,7 @@ withDefaults(defineProps<{
.panel__indicator::before,
.panel__indicator::after {
content: "";
content: '';
position: absolute;
top: 50%;
left: 50%;
@@ -65,7 +68,9 @@ withDefaults(defineProps<{
border-radius: 999px;
background: #64748b;
transform: translate(-50%, -50%);
transition: transform 0.2s ease, opacity 0.2s ease;
transition:
transform 0.2s ease,
opacity 0.2s ease;
}
.panel__indicator::after {
@@ -1,13 +1,13 @@
<script setup lang="ts">
import type { Component } from 'vue';
import type { Component } from 'vue'
defineProps<{
eyebrow: string;
value: string | number;
label: string;
tone?: "neutral" | "brand" | "success" | "warn" | "danger" | "info";
icon?: Component;
}>();
eyebrow: string
value: string | number
label: string
tone?: 'neutral' | 'brand' | 'success' | 'warn' | 'danger' | 'info'
icon?: Component
}>()
</script>
<template>
@@ -33,7 +33,9 @@ defineProps<{
border: 1px solid #f0f3fa;
display: flex;
flex-direction: column;
transition: box-shadow 0.2s, transform 0.2s;
transition:
box-shadow 0.2s,
transform 0.2s;
}
.metric-card:hover {
@@ -59,12 +61,30 @@ defineProps<{
flex-shrink: 0;
}
.metric-icon-wrapper.neutral { background: #f0f2f5; color: #595959; }
.metric-icon-wrapper.brand { background: #e6f4ff; color: #1677ff; }
.metric-icon-wrapper.success { background: #f6ffed; color: #52c41a; }
.metric-icon-wrapper.warn { background: #fffbe6; color: #faad14; }
.metric-icon-wrapper.danger { background: #fff2f0; color: #ff4d4f; }
.metric-icon-wrapper.info { background: #e6f7ff; color: #1890ff; }
.metric-icon-wrapper.neutral {
background: #f0f2f5;
color: #595959;
}
.metric-icon-wrapper.brand {
background: #e6f4ff;
color: #1677ff;
}
.metric-icon-wrapper.success {
background: #f6ffed;
color: #52c41a;
}
.metric-icon-wrapper.warn {
background: #fffbe6;
color: #faad14;
}
.metric-icon-wrapper.danger {
background: #fff2f0;
color: #ff4d4f;
}
.metric-icon-wrapper.info {
background: #e6f7ff;
color: #1890ff;
}
.metric-title-group {
display: flex;
@@ -101,18 +121,42 @@ defineProps<{
margin-top: auto;
}
html[data-theme="dark"] .metric-card {
html[data-theme='dark'] .metric-card {
background: #141414;
border-color: #303030;
}
html[data-theme="dark"] .metric-icon-wrapper.neutral { background: #262626; color: rgba(255, 255, 255, 0.45); }
html[data-theme="dark"] .metric-icon-wrapper.brand { background: rgba(22, 119, 255, 0.2); color: #177ddc; }
html[data-theme="dark"] .metric-icon-wrapper.success { background: rgba(82, 196, 26, 0.2); color: #49aa19; }
html[data-theme="dark"] .metric-icon-wrapper.warn { background: rgba(250, 173, 20, 0.2); color: #d89614; }
html[data-theme="dark"] .metric-icon-wrapper.danger { background: rgba(255, 77, 79, 0.2); color: #a61d24; }
html[data-theme="dark"] .metric-icon-wrapper.info { background: rgba(24, 144, 255, 0.2); color: #177ddc; }
html[data-theme="dark"] .eyebrow,
html[data-theme="dark"] .label { color: rgba(255, 255, 255, 0.45); }
html[data-theme="dark"] .value { color: rgba(255, 255, 255, 0.85); }
html[data-theme="dark"] .label { border-top-color: #303030; }
html[data-theme='dark'] .metric-icon-wrapper.neutral {
background: #262626;
color: rgba(255, 255, 255, 0.45);
}
html[data-theme='dark'] .metric-icon-wrapper.brand {
background: rgba(22, 119, 255, 0.2);
color: #177ddc;
}
html[data-theme='dark'] .metric-icon-wrapper.success {
background: rgba(82, 196, 26, 0.2);
color: #49aa19;
}
html[data-theme='dark'] .metric-icon-wrapper.warn {
background: rgba(250, 173, 20, 0.2);
color: #d89614;
}
html[data-theme='dark'] .metric-icon-wrapper.danger {
background: rgba(255, 77, 79, 0.2);
color: #a61d24;
}
html[data-theme='dark'] .metric-icon-wrapper.info {
background: rgba(24, 144, 255, 0.2);
color: #177ddc;
}
html[data-theme='dark'] .eyebrow,
html[data-theme='dark'] .label {
color: rgba(255, 255, 255, 0.45);
}
html[data-theme='dark'] .value {
color: rgba(255, 255, 255, 0.85);
}
html[data-theme='dark'] .label {
border-top-color: #303030;
}
</style>
@@ -1,60 +1,63 @@
<script setup lang="ts">
import { computed } from "vue";
import { computed } from 'vue'
const props = withDefaults(defineProps<{
current: number;
pageSize: number;
total: number;
maxVisiblePages?: number;
}>(), {
maxVisiblePages: 5,
});
const props = withDefaults(
defineProps<{
current: number
pageSize: number
total: number
maxVisiblePages?: number
}>(),
{
maxVisiblePages: 5,
},
)
const emit = defineEmits<{
(event: "change", page: number): void;
}>();
(event: 'change', page: number): void
}>()
type PageToken = number | "ellipsis";
type PageToken = number | 'ellipsis'
const totalPages = computed(() => Math.max(1, Math.ceil(props.total / props.pageSize)));
const totalPages = computed(() => Math.max(1, Math.ceil(props.total / props.pageSize)))
const tokens = computed<PageToken[]>(() => {
if (totalPages.value <= props.maxVisiblePages) {
return Array.from({ length: totalPages.value }, (_value, index) => index + 1);
return Array.from({ length: totalPages.value }, (_value, index) => index + 1)
}
const pages: PageToken[] = [1];
let start = Math.max(2, props.current - 1);
let end = Math.min(totalPages.value - 1, props.current + 1);
const pages: PageToken[] = [1]
let start = Math.max(2, props.current - 1)
let end = Math.min(totalPages.value - 1, props.current + 1)
while (end - start < 2 && start > 2) {
start -= 1;
start -= 1
}
while (end - start < 2 && end < totalPages.value - 1) {
end += 1;
end += 1
}
if (start > 2) {
pages.push("ellipsis");
pages.push('ellipsis')
}
for (let page = start; page <= end; page += 1) {
pages.push(page);
pages.push(page)
}
if (end < totalPages.value - 1) {
pages.push("ellipsis");
pages.push('ellipsis')
}
pages.push(totalPages.value);
return pages;
});
pages.push(totalPages.value)
return pages
})
function goTo(page: number) {
if (page === props.current || page < 1 || page > totalPages.value) {
return;
return
}
emit("change", page);
emit('change', page)
}
</script>
@@ -65,7 +68,10 @@ function goTo(page: number) {
</button>
<div class="pagination__pages">
<template v-for="token in tokens" :key="`${token}-${typeof token === 'number' ? token : Math.random()}`">
<template
v-for="token in tokens"
:key="`${token}-${typeof token === 'number' ? token : Math.random()}`"
>
<span v-if="token === 'ellipsis'" class="pagination__ellipsis"></span>
<button
v-else
@@ -78,7 +84,11 @@ function goTo(page: number) {
</template>
</div>
<button class="pagination__nav" :disabled="props.current >= totalPages" @click="goTo(props.current + 1)">
<button
class="pagination__nav"
:disabled="props.current >= totalPages"
@click="goTo(props.current + 1)"
>
下一页
</button>
</nav>
@@ -112,7 +122,11 @@ function goTo(page: number) {
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, opacity 0.2s ease;
transition:
background-color 0.2s ease,
border-color 0.2s ease,
color 0.2s ease,
opacity 0.2s ease;
}
.pagination__nav:hover:not(:disabled),
@@ -1,9 +1,9 @@
<script setup lang="ts">
const props = defineProps<{
tone?: "neutral" | "brand" | "success" | "warn" | "danger" | "info";
label: string;
subtle?: boolean;
}>();
tone?: 'neutral' | 'brand' | 'success' | 'warn' | 'danger' | 'info'
label: string
subtle?: boolean
}>()
</script>
<template>
@@ -62,7 +62,7 @@ const props = defineProps<{
border-color: rgba(37, 99, 235, 0.22);
}
html[data-theme="dark"] .badge {
html[data-theme='dark'] .badge {
background: rgba(255, 255, 255, 0.04);
}
</style>
@@ -1,11 +1,9 @@
<script setup lang="ts">
import { PlaySquareOutlined } from "@ant-design/icons-vue";
defineProps<{
eyebrow?: string;
title: string;
description?: string;
}>();
eyebrow?: string
title: string
description?: string
}>()
</script>
<template>
@@ -62,7 +60,7 @@ defineProps<{
}
.panel-title::before {
content: "";
content: '';
display: inline-block;
width: 4px;
height: 14px;