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
+160 -151
View File
@@ -2,210 +2,209 @@
import {
AppstoreOutlined,
BookOutlined,
DownOutlined,
GlobalOutlined,
HistoryOutlined,
LogoutOutlined,
PictureOutlined,
RadarChartOutlined,
SearchOutlined,
PictureOutlined,
DownOutlined,
LogoutOutlined,
} from "@ant-design/icons-vue";
import { useQuery } from "@tanstack/vue-query";
import { computed, type Component } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
} from '@ant-design/icons-vue'
import { useQuery } from '@tanstack/vue-query'
import { computed, type Component } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { workspaceApi } from "@/lib/api";
import { useAuthStore } from "@/stores/auth";
import { workspaceApi } from '@/lib/api'
import { useAuthStore } from '@/stores/auth'
const route = useRoute();
const router = useRouter();
const authStore = useAuthStore();
const { locale, t } = useI18n();
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const { locale, t } = useI18n()
const isMembershipBlocked = computed(() => authStore.isMembershipBlocked);
const membershipBlockedMessage = computed(() => (
authStore.membership?.blocked_reason === "user_disabled"
? t("membershipBlocked.userDisabledMessage")
: t("membershipBlocked.expiredMessage")
));
const isMembershipBlocked = computed(() => authStore.isMembershipBlocked)
const membershipBlockedMessage = computed(() =>
authStore.membership?.blocked_reason === 'user_disabled'
? t('membershipBlocked.userDisabledMessage')
: t('membershipBlocked.expiredMessage'),
)
const quotaQuery = useQuery({
queryKey: ["workspace", "quota-summary", "shell"],
queryKey: ['workspace', 'quota-summary', 'shell'],
queryFn: () => workspaceApi.quotaSummary(),
enabled: computed(() => !isMembershipBlocked.value),
});
})
const userDisplayName = computed(() => {
return authStore.user?.name || authStore.user?.phone || authStore.user?.email || t("shell.userFallback");
});
const userSecondaryIdentifier = computed(() => authStore.user?.email || authStore.user?.phone || "--");
const userInitial = computed(() => userDisplayName.value.slice(0, 1).toUpperCase() || "A");
const quotaSummary = computed(() => quotaQuery.data.value);
return (
authStore.user?.name ||
authStore.user?.phone ||
authStore.user?.email ||
t('shell.userFallback')
)
})
const userSecondaryIdentifier = computed(
() => authStore.user?.email || authStore.user?.phone || '--',
)
const userInitial = computed(() => userDisplayName.value.slice(0, 1).toUpperCase() || 'A')
const quotaSummary = computed(() => quotaQuery.data.value)
const membershipExpiryText = computed(() => {
const value = authStore.membership?.end_at;
const value = authStore.membership?.end_at
if (!value) {
return "--";
return '--'
}
const date = new Date(value);
const date = new Date(value)
if (Number.isNaN(date.getTime())) {
return value;
return value
}
return new Intl.DateTimeFormat(locale.value, {
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(date);
});
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(date)
})
const selectedKeys = computed(() => {
if (route.meta.navKey === null) {
return [];
return []
}
return [String(route.meta.navKey ?? route.path)];
});
const pageTitle = computed(() => t(String(route.meta.titleKey ?? "route.workspace.title")));
return [String(route.meta.navKey ?? route.path)]
})
const pageTitle = computed(() => t(String(route.meta.titleKey ?? 'route.workspace.title')))
type HeaderBreadcrumb = {
label: string;
path?: string;
};
label: string
path?: string
}
type NavItem = {
key: string;
label: string;
icon?: Component;
};
key: string
label: string
icon?: Component
}
type NavSection = {
key: string;
title: string;
items: NavItem[];
};
key: string
title: string
items: NavItem[]
}
const navSections = computed<NavSection[]>(() => {
const base = [
{
key: "workspace",
title: "",
key: 'workspace',
title: '',
items: [
{
key: "/workspace",
label: t("nav.workspace"),
key: '/workspace',
label: t('nav.workspace'),
icon: AppstoreOutlined,
},
],
},
{
key: "articleCreation",
title: t("nav.articleCreation"),
key: 'articleCreation',
title: t('nav.articleCreation'),
items: [
{ key: "/articles/templates", label: t("nav.templates") },
{ key: "/articles/custom", label: t("nav.custom") },
{ key: "/articles/free-create", label: t("nav.freeCreate") },
{ key: "/articles/imitations", label: t("nav.imitation") },
{ key: '/articles/templates', label: t('nav.templates') },
{ key: '/articles/custom', label: t('nav.custom') },
{ key: '/articles/free-create', label: t('nav.freeCreate') },
{ key: '/articles/imitations', label: t('nav.imitation') },
],
},
{
key: "brandManagement",
title: t("nav.brandManagement"),
key: 'brandManagement',
title: t('nav.brandManagement'),
items: [{ key: '/brands', label: t('nav.brands'), icon: SearchOutlined }],
},
{
key: 'tracking',
title: t('nav.tracking'),
items: [{ key: '/tracking', label: t('nav.trackingDetail'), icon: RadarChartOutlined }],
},
{
key: 'contentManagement',
title: t('nav.contentManagement'),
items: [
{ key: "/brands", label: t("nav.brands"), icon: SearchOutlined },
{ key: '/media', label: t('nav.media'), icon: GlobalOutlined },
{ key: '/knowledge', label: t('nav.knowledge'), icon: BookOutlined },
{ key: '/images', label: t('nav.images'), icon: PictureOutlined },
],
},
{
key: "tracking",
title: t("nav.tracking"),
items: [
{ key: "/tracking", label: t("nav.trackingDetail"), icon: RadarChartOutlined },
],
key: 'kolMarket',
title: t('nav.kolMarket'),
items: [{ key: '/kol/marketplace', label: t('nav.kolMarketplace') }],
},
{
key: "contentManagement",
title: t("nav.contentManagement"),
items: [
{ key: "/media", label: t("nav.media"), icon: GlobalOutlined },
{ key: "/knowledge", label: t("nav.knowledge"), icon: BookOutlined },
{ key: "/images", label: t("nav.images"), icon: PictureOutlined },
],
key: 'personalCenter',
title: t('nav.personalCenter'),
items: [{ key: '/account/ai-points', label: t('nav.aiPointUsage'), icon: HistoryOutlined }],
},
{
key: "kolMarket",
title: t("nav.kolMarket"),
items: [
{ key: "/kol/marketplace", label: t("nav.kolMarketplace") },
],
},
{
key: "personalCenter",
title: t("nav.personalCenter"),
items: [
{ key: "/account/ai-points", label: t("nav.aiPointUsage"), icon: HistoryOutlined },
],
},
];
]
if (authStore.isActiveKol) {
base.push({
key: "kolWorkspace",
title: t("nav.kolWorkspace"),
key: 'kolWorkspace',
title: t('nav.kolWorkspace'),
items: [
{ key: "/kol/manage", label: t("nav.kolManage") },
{ key: "/kol/dashboard", label: t("nav.kolDashboard") },
{ key: "/kol/profile", label: t("nav.kolProfile") },
{ key: '/kol/manage', label: t('nav.kolManage') },
{ key: '/kol/dashboard', label: t('nav.kolDashboard') },
{ key: '/kol/profile', label: t('nav.kolProfile') },
],
});
})
}
return base;
});
return base
})
const headerBreadcrumbs = computed<HeaderBreadcrumb[]>(() => {
const currentTitle = pageTitle.value;
const navKey = route.meta.navKey === null ? null : String(route.meta.navKey ?? route.path);
const currentTitle = pageTitle.value
const navKey = route.meta.navKey === null ? null : String(route.meta.navKey ?? route.path)
if (!navKey) {
return [{ label: currentTitle }];
return [{ label: currentTitle }]
}
for (const section of navSections.value) {
const navItem = section.items.find((item) => item.key === navKey);
const navItem = section.items.find((item) => item.key === navKey)
if (!navItem) {
continue;
continue
}
const crumbs: HeaderBreadcrumb[] = [];
const crumbs: HeaderBreadcrumb[] = []
const pushCrumb = (crumb: HeaderBreadcrumb) => {
if (!crumb.label || crumbs.at(-1)?.label === crumb.label) {
return;
return
}
crumbs.push(crumb);
};
const isNavPage = route.path === navItem.key || currentTitle === navItem.label;
crumbs.push(crumb)
}
const isNavPage = route.path === navItem.key || currentTitle === navItem.label
pushCrumb({ label: section.title });
pushCrumb({ label: navItem.label, path: isNavPage ? undefined : navItem.key });
pushCrumb({ label: section.title })
pushCrumb({ label: navItem.label, path: isNavPage ? undefined : navItem.key })
if (!isNavPage) {
pushCrumb({ label: currentTitle });
pushCrumb({ label: currentTitle })
}
return crumbs;
return crumbs
}
return [{ label: currentTitle }];
});
return [{ label: currentTitle }]
})
function go(path: string): void {
void router.push(path);
void router.push(path)
}
async function handleLogout(): Promise<void> {
await authStore.logout();
await router.replace("/login");
await authStore.logout()
await router.replace('/login')
}
</script>
@@ -213,23 +212,14 @@ async function handleLogout(): Promise<void> {
<a-layout class="admin-layout">
<a-layout-sider width="250" theme="light" class="admin-sider">
<div class="admin-brand">
{{ t("app.name") }}
{{ t('app.name') }}
</div>
<a-menu
mode="inline"
theme="light"
:selected-keys="selectedKeys"
class="admin-menu"
>
<a-menu mode="inline" theme="light" :selected-keys="selectedKeys" class="admin-menu">
<template v-for="section in navSections" :key="section.key">
<template v-if="section.title">
<a-menu-item-group :title="section.title">
<a-menu-item
v-for="item in section.items"
:key="item.key"
@click="go(item.key)"
>
<a-menu-item v-for="item in section.items" :key="item.key" @click="go(item.key)">
<template v-if="item.icon" #icon><component :is="item.icon" /></template>
{{ item.label }}
</a-menu-item>
@@ -237,11 +227,7 @@ async function handleLogout(): Promise<void> {
</template>
<template v-else>
<a-menu-item
v-for="item in section.items"
:key="item.key"
@click="go(item.key)"
>
<a-menu-item v-for="item in section.items" :key="item.key" @click="go(item.key)">
<template v-if="item.icon" #icon><component :is="item.icon" /></template>
{{ item.label }}
</a-menu-item>
@@ -250,8 +236,14 @@ async function handleLogout(): Promise<void> {
</a-menu>
</a-layout-sider>
<a-layout class="admin-main-layout" :class="{ 'admin-main-layout--blocked': isMembershipBlocked }">
<a-layout-header class="admin-header" :class="{ 'admin-header--blocked': isMembershipBlocked }">
<a-layout
class="admin-main-layout"
:class="{ 'admin-main-layout--blocked': isMembershipBlocked }"
>
<a-layout-header
class="admin-header"
:class="{ 'admin-header--blocked': isMembershipBlocked }"
>
<nav v-if="!isMembershipBlocked" class="admin-header-breadcrumb" aria-label="Breadcrumb">
<span
v-for="(crumb, index) in headerBreadcrumbs"
@@ -275,19 +267,24 @@ async function handleLogout(): Promise<void> {
</span>
</span>
</nav>
<div class="admin-header-actions">
<!-- Quota Indicator -->
<div v-if="!isMembershipBlocked" class="quota-pill">
<a-tag color="blue" :bordered="false" class="quota-pill-tag">{{ quotaSummary?.plan_name || t("shell.planFallback") }}</a-tag>
<a-tag color="blue" :bordered="false" class="quota-pill-tag">
{{ quotaSummary?.plan_name || t('shell.planFallback') }}
</a-tag>
<span class="quota-pill-text">
{{ t("shell.membershipExpiresAt") }}: <strong>{{ membershipExpiryText }}</strong>
<span class="quota-pill-divider">·</span>
{{ t("shell.remainingQuota") }}: <strong>{{ quotaSummary?.balance ?? '--' }}</strong>
<span class="quota-pill-total">/ {{ quotaSummary?.total_quota ?? '--' }}</span>
<span class="quota-pill-divider">·</span>
{{ t("shell.aiPoints") }}: <strong>{{ quotaSummary?.ai_points_balance ?? '--' }}</strong>
<span class="quota-pill-total">/ {{ quotaSummary?.ai_points_total ?? '--' }}</span>
{{ t('shell.membershipExpiresAt') }}:
<strong>{{ membershipExpiryText }}</strong>
<span class="quota-pill-divider">·</span>
{{ t('shell.remainingQuota') }}:
<strong>{{ quotaSummary?.balance ?? '--' }}</strong>
<span class="quota-pill-total">/ {{ quotaSummary?.total_quota ?? '--' }}</span>
<span class="quota-pill-divider">·</span>
{{ t('shell.aiPoints') }}:
<strong>{{ quotaSummary?.ai_points_balance ?? '--' }}</strong>
<span class="quota-pill-total">/ {{ quotaSummary?.ai_points_total ?? '--' }}</span>
</span>
</div>
@@ -305,12 +302,21 @@ async function handleLogout(): Promise<void> {
<span class="user-dropdown-role">{{ userSecondaryIdentifier }}</span>
</div>
<a-menu-divider />
<a-menu-item v-if="!isMembershipBlocked" key="workspace" @click="go('/workspace')" class="user-dropdown-item">
{{ t("nav.workspace") }}
<a-menu-item
v-if="!isMembershipBlocked"
key="workspace"
class="user-dropdown-item"
@click="go('/workspace')"
>
{{ t('nav.workspace') }}
</a-menu-item>
<a-menu-item key="logout" @click="handleLogout" class="user-dropdown-item user-dropdown-logout">
<a-menu-item
key="logout"
class="user-dropdown-item user-dropdown-logout"
@click="handleLogout"
>
<LogoutOutlined class="user-dropdown-logout-icon" />
{{ t("shell.logout") }}
{{ t('shell.logout') }}
</a-menu-item>
</a-menu>
</template>
@@ -318,7 +324,10 @@ async function handleLogout(): Promise<void> {
</div>
</a-layout-header>
<a-layout-content class="admin-content" :class="{ 'admin-content--blocked': isMembershipBlocked }">
<a-layout-content
class="admin-content"
:class="{ 'admin-content--blocked': isMembershipBlocked }"
>
<section
v-if="isMembershipBlocked"
class="membership-expired-state"