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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user