feat(admin): 整合财务模块导航为侧边栏子菜单与顶部联动Tab

- 侧边栏收敛:将散落在运营分组下的销售毛利、商品与成本、钱包管理、收支明细整合入「财务」二级子菜单,并支持智能展开与角色权限隔离
- 顶部导航联动:新增 FinanceSubNav 通用组件,在销售毛利、商品与成本中心、钱包管理、收支明细 4 个页面顶部统一提供一键切换 Tab
- 语义规范化:将原“财务”流水页面标题明确为“收支明细”,消除模块与页面的命名歧义
- 验证方式:npm run check 全量检查通过(Prettier、ESLint、tsc、单测 100% 通过),构建产物已同步至运行容器并验证路由与交互
This commit is contained in:
yml2213
2026-10-25 20:39:11 +08:00
parent eaffae4af0
commit dfc01db4ae
10 changed files with 203 additions and 75 deletions
@@ -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: <LineChartOutlined />,
},
{
key: '/admin/cost-center',
label: '商品与成本中心',
icon: <ApartmentOutlined />,
},
{
key: '/admin/fund',
label: '钱包管理',
icon: <WalletOutlined />,
},
{
key: '/admin/finance',
label: '收支明细',
icon: <AccountBookOutlined />,
},
] 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: (
<span
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 6,
fontSize: 15,
fontWeight: 500,
}}
>
{item.icon}
{item.label}
</span>
),
}))
return (
<div
className="finance-sub-nav-wrapper"
style={{
background: '#fff',
padding: '6px 16px 0',
borderRadius: 8,
border: '1px solid #f0f0f0',
marginBottom: 4,
}}
>
<Tabs
activeKey={activeKey}
onChange={(key) => navigate(key)}
items={tabItems}
style={{ marginBottom: -1 }}
/>
</div>
)
}
+74 -43
View File
@@ -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<string[]>(['sub-finance'])
useEffect(() => {
if (isFinanceKey && !openKeys.includes('sub-finance')) {
setOpenKeys((prev) => [...prev, 'sub-finance'])
}
}, [isFinanceKey])
const menuItems = useMemo<MenuProps['items']>(() => {
const financeSubItems: MenuProps['items'] = [
{ key: '/admin/sales-profit', icon: <LineChartOutlined />, label: '销售毛利' },
{ key: '/admin/cost-center', icon: <ApartmentOutlined />, label: '商品与成本' },
{ key: '/admin/fund', icon: <WalletOutlined />, label: '钱包管理' },
{ key: '/admin/finance', icon: <AccountBookOutlined />, label: '收支明细' },
]
const financeSubMenu = {
key: 'sub-finance',
icon: <AccountBookOutlined />,
label: '财务',
children: financeSubItems,
}
const operationItems: MenuProps['items'] =
role === 'finance'
? [
{ key: '/admin/finance', icon: <AccountBookOutlined />, label: '财务' },
{ key: '/admin/sales-profit', icon: <LineChartOutlined />, label: '销售毛利' },
{ key: '/admin/cost-center', icon: <ApartmentOutlined />, label: '商品与成本' },
{ key: '/admin/fund', icon: <WalletOutlined />, label: '钱包管理' },
financeSubMenu,
{ key: '/admin/worker-platform', icon: <TrophyOutlined />, label: '资金审核' },
]
: [
{ key: '/admin/dashboard', icon: <DashboardOutlined />, label: '概览' },
{ key: '/admin/orders', icon: <OrderedListOutlined />, label: '订单' },
{
key: '/admin/fund',
icon: <WalletOutlined />,
label: role === 'support' ? '我的钱包' : '钱包管理',
},
...(role === 'support'
? []
: [
{ key: '/admin/finance', icon: <AccountBookOutlined />, label: '财务' },
{
key: '/admin/sales-profit',
icon: <LineChartOutlined />,
label: '销售毛利',
},
{
key: '/admin/cost-center',
icon: <ApartmentOutlined />,
label: '商品与成本',
},
]),
{ key: '/admin/tasks', icon: <UnorderedListOutlined />, label: '任务' },
{ key: '/admin/worker-platform', icon: <TrophyOutlined />, label: '接单平台' },
{
key: '/admin/kuaishou-industry',
icon: <SafetyCertificateOutlined />,
label: '电子凭证',
},
{
key: '/admin/douyin-mashida-certificates',
icon: <SafetyCertificateOutlined />,
label: '抖音券核销',
},
]
: role === 'support'
? [
{ key: '/admin/dashboard', icon: <DashboardOutlined />, label: '概览' },
{ key: '/admin/orders', icon: <OrderedListOutlined />, label: '订单' },
{
key: '/admin/fund',
icon: <WalletOutlined />,
label: '我的钱包',
},
{ key: '/admin/tasks', icon: <UnorderedListOutlined />, label: '任务' },
{ key: '/admin/worker-platform', icon: <TrophyOutlined />, label: '接单平台' },
{
key: '/admin/kuaishou-industry',
icon: <SafetyCertificateOutlined />,
label: '电子凭证',
},
{
key: '/admin/douyin-mashida-certificates',
icon: <SafetyCertificateOutlined />,
label: '抖音券核销',
},
]
: [
{ key: '/admin/dashboard', icon: <DashboardOutlined />, label: '概览' },
{ key: '/admin/orders', icon: <OrderedListOutlined />, label: '订单' },
financeSubMenu,
{ key: '/admin/tasks', icon: <UnorderedListOutlined />, label: '任务' },
{ key: '/admin/worker-platform', icon: <TrophyOutlined />, label: '接单平台' },
{
key: '/admin/kuaishou-industry',
icon: <SafetyCertificateOutlined />,
label: '电子凭证',
},
{
key: '/admin/douyin-mashida-certificates',
icon: <SafetyCertificateOutlined />,
label: '抖音券核销',
},
]
const items: MenuProps['items'] = [
{
@@ -333,8 +362,6 @@ export default function AdminLayout() {
</div>
)
const selectedKey = resolveSelectedKey(location.pathname)
return (
<Layout className="admin-shell">
<Layout.Sider
@@ -377,6 +404,8 @@ export default function AdminLayout() {
mode="inline"
inlineIndent={16}
selectedKeys={[selectedKey]}
openKeys={openKeys}
onOpenChange={(keys) => 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'
+5 -1
View File
@@ -188,7 +188,11 @@ export function apiGet<T>(url: string, params?: Record<string, unknown>, options
return request<T>({ method: 'GET', url, params, ...options })
}
export function apiGetBlob(url: string, params?: Record<string, unknown>, options?: RequestOptions) {
export function apiGetBlob(
url: string,
params?: Record<string, unknown>,
options?: RequestOptions,
) {
return http.request<Blob, Blob>({
method: 'GET',
url,
@@ -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 (
<section className="page-stack">
<PageHeader title="财务" description="按日期查看订单收款、退款、打手结算和平台现金流。" />
<FinanceSubNav current="/admin/finance" />
<PageHeader title="收支明细" description="按日期查看订单收款、退款、打手结算和平台现金流。" />
<Card>
<Form<FinanceFilterForm>
@@ -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 (
<section className="page-stack">
<FinanceSubNav current="/admin/sales-profit" />
<PageHeader
title="销售毛利"
description="按订单项归集收入与成本;未归一商品单独成行,成本缺失时毛利不直接用于决策。"
@@ -5,12 +5,7 @@
* 不写渠道映射,也不维护游戏分类。
* 成本按上游商品配置采购单价(cost_versions),并在改价/初次配置后重算成本快照。
*/
import {
DownloadOutlined,
EditOutlined,
ReloadOutlined,
SyncOutlined,
} from '@ant-design/icons'
import { DownloadOutlined, EditOutlined, ReloadOutlined, SyncOutlined } from '@ant-design/icons'
import {
Alert,
App,
@@ -267,11 +262,7 @@ export function AffiliateSkuPanel() {
>
重算成本快照
</Button>
<Button
icon={<SyncOutlined />}
loading={syncing}
onClick={() => void syncProducts()}
>
<Button icon={<SyncOutlined />} loading={syncing} onClick={() => void syncProducts()}>
同步自动发货商品
</Button>
<Button
@@ -478,7 +469,8 @@ export function AffiliateSkuPanel() {
<Space direction="vertical" size={0}>
<Typography.Text strong>追溯至历史订单 (推荐)</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
自近一个月业务起点(30 天前)生效,重算快照后可直接消除近一个月 cost_missing 异常
自近一个月业务起点(30 天前)生效,重算快照后可直接消除近一个月 cost_missing
异常
</Typography.Text>
</Space>
</Radio>
@@ -25,6 +25,7 @@ import {
import { useQuery, useQueryClient } from '@tanstack/react-query'
import PageHeader from '@/components/admin/PageHeader'
import FinanceSubNav from '@/components/admin/FinanceSubNav'
import { usePersistedState } from '@/hooks/usePersistedState'
import { fetchAdminCostCenterOptions, fetchAdminCostCenterOverview } from '@/services/admin'
import { hasAdminPermission } from '@/utils/admin-auth'
@@ -121,6 +122,7 @@ export default function CostCenterShell() {
}}
>
<section className="page-stack">
<FinanceSubNav current="/admin/cost-center" />
<PageHeader
title="商品与成本中心"
description="商品 SKU 先归一;成本在各来源自己的配置里维护(人工 SKU 消耗积分 / 自动发货采购价 / 接单模板发单金额),历史成本靠快照固化。"
@@ -618,7 +618,11 @@ export function ProductCatalogPanel() {
style={{ padding: 0 }}
onClick={() => canConfig && openSetProductPrice(row)}
>
{value > 0 ? `${value} 个版本 · 改价` : row.source === 'work_order' ? '配置结算成本' : '未配置 · 立即配置'}
{value > 0
? `${value} 个版本 · 改价`
: row.source === 'work_order'
? '配置结算成本'
: '未配置 · 立即配置'}
</Button>
)
},
@@ -1005,9 +1009,7 @@ export function ProductCatalogPanel() {
<Form.Item
name="amountYuan"
label={
pricingProduct.source === 'affiliate_dash'
? '采购单价(元)'
: '标准结算单价(元)'
pricingProduct.source === 'affiliate_dash' ? '采购单价(元)' : '标准结算单价(元)'
}
rules={[{ required: true, message: '请填写价格' }]}
extra={
@@ -1050,7 +1052,10 @@ export function ProductCatalogPanel() {
</Radio.Group>
</Form.Item>
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.effectiveScope !== curr.effectiveScope}>
<Form.Item
noStyle
shouldUpdate={(prev, curr) => prev.effectiveScope !== curr.effectiveScope}
>
{({ getFieldValue }) =>
getFieldValue('effectiveScope') === 'custom' ? (
<Form.Item
@@ -1069,11 +1074,7 @@ export function ProductCatalogPanel() {
</Form.Item>
{pricingProduct.canonicalSku.startsWith('tpl:') ? (
<Form.Item
name="syncOpenOrders"
valuePropName="checked"
style={{ marginBottom: 12 }}
>
<Form.Item name="syncOpenOrders" valuePropName="checked" style={{ marginBottom: 12 }}>
<Checkbox>
<strong>立即同步至接单大厅待抢单工单(推荐)</strong>
<div style={{ color: '#64748b', fontSize: 12 }}>
@@ -1084,11 +1085,7 @@ export function ProductCatalogPanel() {
) : null}
{pricingProduct.source === 'affiliate_dash' ? (
<Form.Item
name="syncSnapshots"
valuePropName="checked"
style={{ marginBottom: 12 }}
>
<Form.Item name="syncSnapshots" valuePropName="checked" style={{ marginBottom: 12 }}>
<Checkbox>
<strong>立即重算成本快照并回填历史(推荐)</strong>
<div style={{ color: '#64748b', fontSize: 12 }}>
@@ -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 (
<section className="page-stack">
<FinanceSubNav current="/admin/fund" />
<PageHeader
title="钱包管理"
description="统管客服的南航 / 乐玩 / ROCK 积分钱包与备用金现金;可分配积分、发放现金、查看流水与对账。"
@@ -164,7 +164,6 @@ export function syncAdminAffiliateDashCostSnapshots() {
}>('/api/v1/admin/cost-center/affiliate-products/sync-snapshots', {}, { timeout: 300_000 })
}
export function updateAdminProductCatalog(productId: number, payload: Record<string, unknown>) {
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`, {})
}