From dfc01db4ae028045346e42dcd54448a67ea9c12d Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 25 Oct 2026 20:39:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E6=95=B4=E5=90=88=E8=B4=A2?= =?UTF-8?q?=E5=8A=A1=E6=A8=A1=E5=9D=97=E5=AF=BC=E8=88=AA=E4=B8=BA=E4=BE=A7?= =?UTF-8?q?=E8=BE=B9=E6=A0=8F=E5=AD=90=E8=8F=9C=E5=8D=95=E4=B8=8E=E9=A1=B6?= =?UTF-8?q?=E9=83=A8=E8=81=94=E5=8A=A8Tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 侧边栏收敛:将散落在运营分组下的销售毛利、商品与成本、钱包管理、收支明细整合入「财务」二级子菜单,并支持智能展开与角色权限隔离 - 顶部导航联动:新增 FinanceSubNav 通用组件,在销售毛利、商品与成本中心、钱包管理、收支明细 4 个页面顶部统一提供一键切换 Tab - 语义规范化:将原“财务”流水页面标题明确为“收支明细”,消除模块与页面的命名歧义 - 验证方式:npm run check 全量检查通过(Prettier、ESLint、tsc、单测 100% 通过),构建产物已同步至运行容器并验证路由与交互 --- .../src/components/admin/FinanceSubNav.tsx | 93 ++++++++++++++ apps/frontend/src/layouts/AdminLayout.tsx | 117 +++++++++++------- apps/frontend/src/lib/http.ts | 6 +- .../src/pages/admin/AdminFinancePage.tsx | 4 +- .../src/pages/admin/AdminSalesProfitPage.tsx | 9 +- .../admin/cost-center/AffiliateSkuPanel.tsx | 16 +-- .../admin/cost-center/CostCenterShell.tsx | 2 + .../admin/cost-center/ProductCatalogPanel.tsx | 27 ++-- .../pages/admin/fund/FundManagementView.tsx | 2 + .../src/services/admin/cost-center.ts | 2 - 10 files changed, 203 insertions(+), 75 deletions(-) create mode 100644 apps/frontend/src/components/admin/FinanceSubNav.tsx diff --git a/apps/frontend/src/components/admin/FinanceSubNav.tsx b/apps/frontend/src/components/admin/FinanceSubNav.tsx new file mode 100644 index 00000000..1a2c96ad --- /dev/null +++ b/apps/frontend/src/components/admin/FinanceSubNav.tsx @@ -0,0 +1,93 @@ +import { + AccountBookOutlined, + ApartmentOutlined, + LineChartOutlined, + WalletOutlined, +} from '@ant-design/icons' +import { Tabs } from 'antd' +import type { TabsProps } from 'antd' +import { useLocation, useNavigate } from 'react-router' +import { getAdminRole } from '@/utils/admin-auth' + +export const FINANCE_NAV_ITEMS = [ + { + key: '/admin/sales-profit', + label: '销售毛利', + icon: , + }, + { + key: '/admin/cost-center', + label: '商品与成本中心', + icon: , + }, + { + key: '/admin/fund', + label: '钱包管理', + icon: , + }, + { + key: '/admin/finance', + label: '收支明细', + icon: , + }, +] as const + +type FinanceNavKey = (typeof FINANCE_NAV_ITEMS)[number]['key'] + +type FinanceSubNavProps = { + current?: FinanceNavKey +} + +export default function FinanceSubNav({ current }: FinanceSubNavProps) { + const location = useLocation() + const navigate = useNavigate() + const role = getAdminRole() + + // 客服角色无完整财务权限,不展示财务联动 Tab + if (role === 'support') { + return null + } + + const activeKey = + current || + FINANCE_NAV_ITEMS.find((item) => location.pathname.startsWith(item.key))?.key || + '/admin/sales-profit' + + const tabItems: TabsProps['items'] = FINANCE_NAV_ITEMS.map((item) => ({ + key: item.key, + label: ( + + {item.icon} + {item.label} + + ), + })) + + return ( +
+ navigate(key)} + items={tabItems} + style={{ marginBottom: -1 }} + /> +
+ ) +} diff --git a/apps/frontend/src/layouts/AdminLayout.tsx b/apps/frontend/src/layouts/AdminLayout.tsx index 91f0d880..9e567ecd 100644 --- a/apps/frontend/src/layouts/AdminLayout.tsx +++ b/apps/frontend/src/layouts/AdminLayout.tsx @@ -168,52 +168,81 @@ export default function AdminLayout() { }, }) + const selectedKey = resolveSelectedKey(location.pathname) + const isFinanceKey = [ + '/admin/sales-profit', + '/admin/cost-center', + '/admin/fund', + '/admin/finance', + ].includes(selectedKey) + const [openKeys, setOpenKeys] = useState(['sub-finance']) + + useEffect(() => { + if (isFinanceKey && !openKeys.includes('sub-finance')) { + setOpenKeys((prev) => [...prev, 'sub-finance']) + } + }, [isFinanceKey]) + const menuItems = useMemo(() => { + const financeSubItems: MenuProps['items'] = [ + { key: '/admin/sales-profit', icon: , label: '销售毛利' }, + { key: '/admin/cost-center', icon: , label: '商品与成本' }, + { key: '/admin/fund', icon: , label: '钱包管理' }, + { key: '/admin/finance', icon: , label: '收支明细' }, + ] + + const financeSubMenu = { + key: 'sub-finance', + icon: , + label: '财务', + children: financeSubItems, + } + const operationItems: MenuProps['items'] = role === 'finance' ? [ - { key: '/admin/finance', icon: , label: '财务' }, - { key: '/admin/sales-profit', icon: , label: '销售毛利' }, - { key: '/admin/cost-center', icon: , label: '商品与成本' }, - { key: '/admin/fund', icon: , label: '钱包管理' }, + financeSubMenu, { key: '/admin/worker-platform', icon: , label: '资金审核' }, ] - : [ - { key: '/admin/dashboard', icon: , label: '概览' }, - { key: '/admin/orders', icon: , label: '订单' }, - { - key: '/admin/fund', - icon: , - label: role === 'support' ? '我的钱包' : '钱包管理', - }, - ...(role === 'support' - ? [] - : [ - { key: '/admin/finance', icon: , label: '财务' }, - { - key: '/admin/sales-profit', - icon: , - label: '销售毛利', - }, - { - key: '/admin/cost-center', - icon: , - label: '商品与成本', - }, - ]), - { key: '/admin/tasks', icon: , label: '任务' }, - { key: '/admin/worker-platform', icon: , label: '接单平台' }, - { - key: '/admin/kuaishou-industry', - icon: , - label: '电子凭证', - }, - { - key: '/admin/douyin-mashida-certificates', - icon: , - label: '抖音券核销', - }, - ] + : role === 'support' + ? [ + { key: '/admin/dashboard', icon: , label: '概览' }, + { key: '/admin/orders', icon: , label: '订单' }, + { + key: '/admin/fund', + icon: , + label: '我的钱包', + }, + { key: '/admin/tasks', icon: , label: '任务' }, + { key: '/admin/worker-platform', icon: , label: '接单平台' }, + { + key: '/admin/kuaishou-industry', + icon: , + label: '电子凭证', + }, + { + key: '/admin/douyin-mashida-certificates', + icon: , + label: '抖音券核销', + }, + ] + : [ + { key: '/admin/dashboard', icon: , label: '概览' }, + { key: '/admin/orders', icon: , label: '订单' }, + financeSubMenu, + { key: '/admin/tasks', icon: , label: '任务' }, + { key: '/admin/worker-platform', icon: , label: '接单平台' }, + { + key: '/admin/kuaishou-industry', + icon: , + label: '电子凭证', + }, + { + key: '/admin/douyin-mashida-certificates', + icon: , + label: '抖音券核销', + }, + ] const items: MenuProps['items'] = [ { @@ -333,8 +362,6 @@ export default function AdminLayout() { ) - const selectedKey = resolveSelectedKey(location.pathname) - return ( setOpenKeys(keys as string[])} items={menuItems} className="admin-menu" onClick={({ key }) => navigate(String(key))} @@ -802,8 +831,10 @@ function playNotificationSound(kind: NotificationAlertKind) { function resolveSelectedKey(pathname: string) { if (pathname.startsWith('/admin/orders')) return '/admin/orders' - if (pathname.startsWith('/admin/finance')) return '/admin/finance' + if (pathname.startsWith('/admin/sales-profit')) return '/admin/sales-profit' if (pathname.startsWith('/admin/cost-center')) return '/admin/cost-center' + if (pathname.startsWith('/admin/fund')) return '/admin/fund' + if (pathname.startsWith('/admin/finance')) return '/admin/finance' if (pathname.startsWith('/admin/tasks')) return '/admin/tasks' if (pathname.startsWith('/admin/worker-platform')) return '/admin/worker-platform' if (pathname.startsWith('/admin/kuaishou-industry')) return '/admin/kuaishou-industry' diff --git a/apps/frontend/src/lib/http.ts b/apps/frontend/src/lib/http.ts index 666e0b75..fb8e6802 100644 --- a/apps/frontend/src/lib/http.ts +++ b/apps/frontend/src/lib/http.ts @@ -188,7 +188,11 @@ export function apiGet(url: string, params?: Record, options return request({ method: 'GET', url, params, ...options }) } -export function apiGetBlob(url: string, params?: Record, options?: RequestOptions) { +export function apiGetBlob( + url: string, + params?: Record, + options?: RequestOptions, +) { return http.request({ method: 'GET', url, diff --git a/apps/frontend/src/pages/admin/AdminFinancePage.tsx b/apps/frontend/src/pages/admin/AdminFinancePage.tsx index 089dd181..3c1ba94a 100644 --- a/apps/frontend/src/pages/admin/AdminFinancePage.tsx +++ b/apps/frontend/src/pages/admin/AdminFinancePage.tsx @@ -19,6 +19,7 @@ import { useState } from 'react' import { AdminRangePicker } from '@/components/admin/AdminDatePicker' import PageHeader from '@/components/admin/PageHeader' +import FinanceSubNav from '@/components/admin/FinanceSubNav' import dayjs from '@/lib/dayjs' import { fetchAdminFinanceSummary, @@ -160,7 +161,8 @@ export default function AdminFinancePage() { return (
- + + diff --git a/apps/frontend/src/pages/admin/AdminSalesProfitPage.tsx b/apps/frontend/src/pages/admin/AdminSalesProfitPage.tsx index 6fe21efe..44e977fb 100644 --- a/apps/frontend/src/pages/admin/AdminSalesProfitPage.tsx +++ b/apps/frontend/src/pages/admin/AdminSalesProfitPage.tsx @@ -21,6 +21,7 @@ import { useState } from 'react' import { AdminRangePicker } from '@/components/admin/AdminDatePicker' import PageHeader from '@/components/admin/PageHeader' +import FinanceSubNav from '@/components/admin/FinanceSubNav' import dayjs from '@/lib/dayjs' import { fetchAdminSalesDailyFacts, @@ -98,7 +99,12 @@ export default function AdminSalesProfitPage() { function resetFilters() { form.resetFields() setPage(1) - setFilters({ dateFrom: defaultDateFrom, dateTo: defaultDateTo, provider: '', fulfillmentMode: '' }) + setFilters({ + dateFrom: defaultDateFrom, + dateTo: defaultDateTo, + provider: '', + fulfillmentMode: '', + }) } async function rebuildFacts() { @@ -243,6 +249,7 @@ export default function AdminSalesProfitPage() { return (
+ 重算成本快照 - ) }, @@ -1005,9 +1009,7 @@ export function ProductCatalogPanel() { - prev.effectiveScope !== curr.effectiveScope}> + prev.effectiveScope !== curr.effectiveScope} + > {({ getFieldValue }) => getFieldValue('effectiveScope') === 'custom' ? ( {pricingProduct.canonicalSku.startsWith('tpl:') ? ( - + 立即同步至接单大厅待抢单工单(推荐)
@@ -1084,11 +1085,7 @@ export function ProductCatalogPanel() { ) : null} {pricingProduct.source === 'affiliate_dash' ? ( - + 立即重算成本快照并回填历史(推荐)
diff --git a/apps/frontend/src/pages/admin/fund/FundManagementView.tsx b/apps/frontend/src/pages/admin/fund/FundManagementView.tsx index 85b26aa5..47cc6759 100644 --- a/apps/frontend/src/pages/admin/fund/FundManagementView.tsx +++ b/apps/frontend/src/pages/admin/fund/FundManagementView.tsx @@ -38,6 +38,7 @@ import { PlatformCreditPanel } from './PlatformCreditPanel' import ImagePreviewList from '@/components/files/ImagePreviewList' import ImageUpload from '@/components/files/ImageUpload' import PageHeader from '@/components/admin/PageHeader' +import FinanceSubNav from '@/components/admin/FinanceSubNav' import { confirmAdminFundReconciliation, createAdminFundAccount, @@ -875,6 +876,7 @@ export default function FundManagementView({ role }: { role: ManagementRole }) { return (
+ ('/api/v1/admin/cost-center/affiliate-products/sync-snapshots', {}, { timeout: 300_000 }) } - export function updateAdminProductCatalog(productId: number, payload: Record) { return apiPatch<{ product: ProductCatalogItem }>( `/api/v1/admin/cost-center/products/${productId}`, @@ -504,4 +503,3 @@ export function syncAdminProductPrice(productId: number) { message: string }>(`/api/v1/admin/cost-center/products/${productId}/sync-price`, {}) } -