feat(admin): 整合财务模块导航为侧边栏子菜单与顶部联动Tab
- 侧边栏收敛:将散落在运营分组下的销售毛利、商品与成本、钱包管理、收支明细整合入「财务」二级子菜单,并支持智能展开与角色权限隔离 - 顶部导航联动:新增 FinanceSubNav 通用组件,在销售毛利、商品与成本中心、钱包管理、收支明细 4 个页面顶部统一提供一键切换 Tab - 语义规范化:将原“财务”流水页面标题明确为“收支明细”,消除模块与页面的命名歧义 - 验证方式:npm run check 全量检查通过(Prettier、ESLint、tsc、单测 100% 通过),构建产物已同步至运行容器并验证路由与交互
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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`, {})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user