新增财务角色与财务报表并完善后台权限
This commit is contained in:
+20
-10
@@ -18,6 +18,7 @@ import AdminLayout from '@/layouts/AdminLayout'
|
||||
import WorkerLayout from '@/layouts/WorkerLayout'
|
||||
|
||||
const AdminDashboardPage = lazy(() => import('@/pages/admin/AdminDashboardPage'))
|
||||
const AdminFinancePage = lazy(() => import('@/pages/admin/AdminFinancePage'))
|
||||
const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage'))
|
||||
const AdminCloudtentaclesRecordsPage = lazy(
|
||||
() => import('@/pages/admin/AdminCloudtentaclesRecordsPage'),
|
||||
@@ -54,9 +55,11 @@ function RequireAdmin() {
|
||||
return <Outlet />
|
||||
}
|
||||
|
||||
function RequireRole({ roles }: { roles: Array<'admin' | 'operator' | 'support'> }) {
|
||||
function RequireRole({ roles }: { roles: Array<'admin' | 'operator' | 'support' | 'finance'> }) {
|
||||
if (!roles.includes(getAdminRole())) {
|
||||
return <Navigate to="/admin/dashboard" replace />
|
||||
return (
|
||||
<Navigate to={getAdminRole() === 'finance' ? '/admin/finance' : '/admin/dashboard'} replace />
|
||||
)
|
||||
}
|
||||
|
||||
return <Outlet />
|
||||
@@ -104,15 +107,22 @@ function AdminApplication() {
|
||||
<Route element={<RequireAdmin />}>
|
||||
<Route path="/admin" element={<AdminLayout />}>
|
||||
<Route index element={<Navigate to="/admin/dashboard" replace />} />
|
||||
<Route path="dashboard" element={<AdminDashboardPage />} />
|
||||
<Route path="orders" element={<AdminOrdersPage />} />
|
||||
<Route path="orders/:orderId" element={<AdminOrderDetailPage />} />
|
||||
<Route path="tasks" element={<AdminTasksPage />} />
|
||||
<Route path="tasks/:taskId" element={<AdminTaskDetailPage />} />
|
||||
<Route element={<RequireRole roles={['admin', 'operator', 'support']} />}>
|
||||
<Route path="dashboard" element={<AdminDashboardPage />} />
|
||||
<Route path="orders" element={<AdminOrdersPage />} />
|
||||
<Route path="orders/:orderId" element={<AdminOrderDetailPage />} />
|
||||
<Route path="tasks" element={<AdminTasksPage />} />
|
||||
<Route path="tasks/:taskId" element={<AdminTaskDetailPage />} />
|
||||
</Route>
|
||||
<Route element={<RequireRole roles={['admin', 'operator', 'finance']} />}>
|
||||
<Route path="finance" element={<AdminFinancePage />} />
|
||||
</Route>
|
||||
<Route path="worker-platform" element={<AdminWorkerPlatformPage />} />
|
||||
<Route path="kuaishou-industry" element={<AdminKuaishouIndustryPage />} />
|
||||
<Route path="cloudtentacles-records" element={<AdminCloudtentaclesRecordsPage />} />
|
||||
<Route path="dev-mock" element={<AdminDevMockPage />} />
|
||||
<Route element={<RequireRole roles={['admin', 'operator', 'support']} />}>
|
||||
<Route path="kuaishou-industry" element={<AdminKuaishouIndustryPage />} />
|
||||
<Route path="cloudtentacles-records" element={<AdminCloudtentaclesRecordsPage />} />
|
||||
<Route path="dev-mock" element={<AdminDevMockPage />} />
|
||||
</Route>
|
||||
<Route element={<RequireRole roles={['admin']} />}>
|
||||
<Route path="users" element={<AdminUsersPage />} />
|
||||
<Route path="platform-shops" element={<AdminPlatformShopsPage />} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
AuditOutlined,
|
||||
AccountBookOutlined,
|
||||
BellOutlined,
|
||||
BugOutlined,
|
||||
DashboardOutlined,
|
||||
@@ -87,12 +88,14 @@ export default function AdminLayout() {
|
||||
const devMockStatusQuery = useQuery({
|
||||
queryKey: ['admin-dev-mock-status'],
|
||||
queryFn: () => fetchDevMockStatus(),
|
||||
enabled: role !== 'finance',
|
||||
retry: false,
|
||||
})
|
||||
const devMockEnabled = Boolean(devMockStatusQuery.data?.data?.enabled)
|
||||
const notificationsQuery = useQuery({
|
||||
queryKey: ['admin-notifications'],
|
||||
queryFn: fetchAdminNotifications,
|
||||
enabled: role !== 'finance',
|
||||
retry: false,
|
||||
})
|
||||
const notifications = notificationsQuery.data?.data.items || []
|
||||
@@ -100,7 +103,7 @@ export default function AdminLayout() {
|
||||
|
||||
useRealtimeEvents({
|
||||
url: '/api/v1/admin/realtime',
|
||||
token: getAdminToken(),
|
||||
token: role === 'finance' ? '' : getAdminToken(),
|
||||
onEvent: (event) => applyAdminRealtimeEvent(event, queryClient),
|
||||
// 会话过期时清登录态回登录页,避免 SSE 带着失效 token 无限重连刷 401。
|
||||
onAuthExpired: () => {
|
||||
@@ -110,14 +113,31 @@ export default function AdminLayout() {
|
||||
})
|
||||
|
||||
const menuItems = useMemo<MenuProps['items']>(() => {
|
||||
const operationItems: MenuProps['items'] = [
|
||||
{ key: '/admin/dashboard', icon: <DashboardOutlined />, label: '概览' },
|
||||
{ key: '/admin/orders', icon: <OrderedListOutlined />, label: '订单' },
|
||||
{ key: '/admin/tasks', icon: <UnorderedListOutlined />, label: '任务' },
|
||||
{ key: '/admin/worker-platform', icon: <TrophyOutlined />, label: '接单平台' },
|
||||
{ key: '/admin/kuaishou-industry', icon: <SafetyCertificateOutlined />, label: '电子凭证' },
|
||||
{ key: '/admin/cloudtentacles-records', icon: <FileSearchOutlined />, label: '发货记录' },
|
||||
]
|
||||
const operationItems: MenuProps['items'] =
|
||||
role === 'finance'
|
||||
? [
|
||||
{ key: '/admin/finance', icon: <AccountBookOutlined />, label: '财务' },
|
||||
{ key: '/admin/worker-platform', icon: <TrophyOutlined />, label: '资金审核' },
|
||||
]
|
||||
: [
|
||||
{ key: '/admin/dashboard', icon: <DashboardOutlined />, label: '概览' },
|
||||
{ key: '/admin/orders', icon: <OrderedListOutlined />, label: '订单' },
|
||||
...(role === 'support'
|
||||
? []
|
||||
: [{ key: '/admin/finance', icon: <AccountBookOutlined />, label: '财务' }]),
|
||||
{ key: '/admin/tasks', icon: <UnorderedListOutlined />, label: '任务' },
|
||||
{ key: '/admin/worker-platform', icon: <TrophyOutlined />, label: '接单平台' },
|
||||
{
|
||||
key: '/admin/kuaishou-industry',
|
||||
icon: <SafetyCertificateOutlined />,
|
||||
label: '电子凭证',
|
||||
},
|
||||
{
|
||||
key: '/admin/cloudtentacles-records',
|
||||
icon: <FileSearchOutlined />,
|
||||
label: '发货记录',
|
||||
},
|
||||
]
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
@@ -601,6 +621,7 @@ 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/tasks')) return '/admin/tasks'
|
||||
if (pathname.startsWith('/admin/worker-platform')) return '/admin/worker-platform'
|
||||
if (pathname.startsWith('/admin/kuaishou-industry')) return '/admin/kuaishou-industry'
|
||||
@@ -629,5 +650,6 @@ function buildNotificationActionPath(notification: AdminNotification) {
|
||||
function roleLabel(role: string) {
|
||||
if (role === 'admin') return '管理员'
|
||||
if (role === 'operator') return '普通运营'
|
||||
if (role === 'finance') return '财务'
|
||||
return '客服'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
import { ReloadOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Form,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
|
||||
import PageHeader from '@/components/admin/PageHeader'
|
||||
import dayjs from '@/lib/dayjs'
|
||||
import { fetchAdminFinanceSummary, fetchAdminFinanceTransactions } from '@/services/admin'
|
||||
import type {
|
||||
AdminFinanceChannelDailyItem,
|
||||
AdminFinanceDailyItem,
|
||||
AdminFinanceTransaction,
|
||||
} from '@/types/admin/finance'
|
||||
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { formatMoney } from './panels/shared'
|
||||
|
||||
type FinanceFilterForm = { dateRange?: unknown }
|
||||
|
||||
const CATEGORY_OPTIONS = [
|
||||
{ value: '', label: '全部账目' },
|
||||
{ value: 'order_paid', label: '订单收款' },
|
||||
{ value: 'order_refund', label: '订单退款' },
|
||||
{ value: 'worker_reward', label: '打手报酬' },
|
||||
{ value: 'withdraw_paid', label: '打手提现' },
|
||||
{ value: 'recharge', label: '打手充值' },
|
||||
{ value: 'worker_recovery', label: '打手资金回收' },
|
||||
{ value: 'after_sales_refund', label: '售后追缴退还' },
|
||||
]
|
||||
|
||||
export default function AdminFinancePage() {
|
||||
const [form] = Form.useForm<FinanceFilterForm>()
|
||||
const [filters, setFilters] = useState({ dateFrom: '', dateTo: '' })
|
||||
const [category, setCategory] = useState('')
|
||||
const [channel, setChannel] = useState('')
|
||||
const [channelSummaryFilter, setChannelSummaryFilter] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
||||
|
||||
const summaryQuery = useQuery({
|
||||
queryKey: ['admin-finance-summary', filters],
|
||||
queryFn: () => fetchAdminFinanceSummary(filters),
|
||||
})
|
||||
const transactionsQuery = useQuery({
|
||||
queryKey: ['admin-finance-transactions', filters, category, page, pageSize],
|
||||
queryFn: () =>
|
||||
fetchAdminFinanceTransactions({
|
||||
...filters,
|
||||
category: category || undefined,
|
||||
channel: channel || undefined,
|
||||
page,
|
||||
pageSize,
|
||||
}),
|
||||
})
|
||||
const overview = summaryQuery.data?.data.overview
|
||||
const daily = summaryQuery.data?.data.daily || []
|
||||
const channelDaily = (summaryQuery.data?.data.channelDaily || []).filter(
|
||||
(item) => !channelSummaryFilter || item.channel === channelSummaryFilter,
|
||||
)
|
||||
const transactionData = transactionsQuery.data?.data
|
||||
|
||||
function applyFilters(values: FinanceFilterForm) {
|
||||
const range = Array.isArray(values.dateRange) ? values.dateRange : []
|
||||
const dateFrom =
|
||||
range[0] && dayjs(range[0]).isValid() ? dayjs(range[0]).format('YYYY-MM-DD HH:mm:ss') : ''
|
||||
const dateTo =
|
||||
range[1] && dayjs(range[1]).isValid()
|
||||
? dayjs(range[1]).second(59).format('YYYY-MM-DD HH:mm:ss')
|
||||
: ''
|
||||
setPage(1)
|
||||
setFilters({ dateFrom, dateTo })
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
form.resetFields()
|
||||
setCategory('')
|
||||
setChannel('')
|
||||
setChannelSummaryFilter('')
|
||||
setPage(1)
|
||||
setFilters({ dateFrom: '', dateTo: '' })
|
||||
}
|
||||
|
||||
const dailyColumns: TableColumnsType<AdminFinanceDailyItem> = [
|
||||
{ title: '日期', dataIndex: 'date', width: 120 },
|
||||
{ title: '支付订单', dataIndex: 'paidOrderCount', width: 100 },
|
||||
{ title: '收款', dataIndex: 'paidAmount', render: formatMoney },
|
||||
{ title: '退款', dataIndex: 'refundAmount', render: formatMoney },
|
||||
{ title: '打手报酬', dataIndex: 'workerRewardAmount', render: formatMoney },
|
||||
{ title: '已提现', dataIndex: 'withdrawAmount', render: formatMoney },
|
||||
{ title: '充值入账', dataIndex: 'rechargeAmount', render: formatMoney },
|
||||
{ title: '售后退还', dataIndex: 'afterSalesRefundAmount', render: formatMoney },
|
||||
{
|
||||
title: '净现金流',
|
||||
dataIndex: 'netCashFlow',
|
||||
render: (value: number) => (
|
||||
<Typography.Text type={value >= 0 ? 'success' : 'danger'}>
|
||||
{formatMoney(value)}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const transactionColumns: TableColumnsType<AdminFinanceTransaction> = [
|
||||
{
|
||||
title: '发生时间',
|
||||
dataIndex: 'occurredAt',
|
||||
width: 180,
|
||||
render: (value) => formatAdminDateTime(value),
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'label',
|
||||
width: 150,
|
||||
render: (value: string, row) => (
|
||||
<Tag color={row.direction === 'in' ? 'green' : 'orange'}>{value}</Tag>
|
||||
),
|
||||
},
|
||||
{ title: '渠道', dataIndex: 'channel', width: 150 },
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'amount',
|
||||
width: 140,
|
||||
render: (value: number, row) => (
|
||||
<Typography.Text type={row.direction === 'in' ? 'success' : 'danger'}>
|
||||
{row.direction === 'in' ? '+' : '-'}
|
||||
{formatMoney(value)}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{ title: '来源编号', dataIndex: 'sourceId', width: 110 },
|
||||
{
|
||||
title: '关联单号/渠道',
|
||||
dataIndex: 'referenceNo',
|
||||
render: (value: string) => value || '-',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="page-stack">
|
||||
<PageHeader title="财务" description="按日期查看订单收款、退款、打手结算和平台现金流。" />
|
||||
|
||||
<Card>
|
||||
<Form<FinanceFilterForm>
|
||||
form={form}
|
||||
layout="inline"
|
||||
className="filter-form"
|
||||
onFinish={applyFilters}
|
||||
>
|
||||
<Form.Item name="dateRange" label="账务日期">
|
||||
<AdminRangePicker showTime={{ format: 'HH:mm' }} format="YYYY-MM-DD HH:mm" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">
|
||||
查询
|
||||
</Button>
|
||||
<Button onClick={resetFilters}>重置</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{summaryQuery.error ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={
|
||||
summaryQuery.error instanceof Error ? summaryQuery.error.message : '读取财务汇总失败'
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<FinanceStatistic title="订单收款" value={overview?.paidAmount || 0} />
|
||||
<FinanceStatistic title="订单退款" value={overview?.refundAmount || 0} />
|
||||
<FinanceStatistic title="打手报酬" value={overview?.workerRewardAmount || 0} />
|
||||
<FinanceStatistic title="已确认提现" value={overview?.withdrawAmount || 0} />
|
||||
<FinanceStatistic title="净现金流" value={overview?.netCashFlow || 0} />
|
||||
<FinanceStatistic title="待提现" value={overview?.pendingWithdrawAmount || 0} />
|
||||
</Row>
|
||||
|
||||
<Card
|
||||
title="每日账目"
|
||||
extra={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={summaryQuery.isFetching}
|
||||
onClick={() => void summaryQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table<AdminFinanceDailyItem>
|
||||
rowKey="date"
|
||||
loading={summaryQuery.isLoading}
|
||||
dataSource={daily}
|
||||
columns={dailyColumns}
|
||||
pagination={false}
|
||||
scroll={{ x: 1080 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="渠道日报"
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Select
|
||||
allowClear
|
||||
value={channelSummaryFilter || undefined}
|
||||
placeholder="全部渠道"
|
||||
style={{ width: 180 }}
|
||||
options={Array.from(
|
||||
new Set((summaryQuery.data?.data.channelDaily || []).map((item) => item.channel)),
|
||||
).map((value) => ({ value, label: value }))}
|
||||
onChange={(value) => setChannelSummaryFilter(value || '')}
|
||||
/>
|
||||
<Typography.Text type="secondary">按日期、渠道和账目类型汇总</Typography.Text>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table<AdminFinanceChannelDailyItem>
|
||||
rowKey={(row) => `${row.date}-${row.channel}-${row.category}`}
|
||||
loading={summaryQuery.isLoading}
|
||||
dataSource={channelDaily}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{ title: '日期', dataIndex: 'date', width: 120 },
|
||||
{ title: '渠道', dataIndex: 'channel', width: 180 },
|
||||
{ title: '账目类型', dataIndex: 'category', width: 150, render: formatFinanceCategory },
|
||||
{ title: '笔数', dataIndex: 'count', width: 90 },
|
||||
{ title: '金额', dataIndex: 'amount', render: formatMoney },
|
||||
]}
|
||||
scroll={{ x: 760 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="资金明细"
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Select
|
||||
value={category}
|
||||
options={CATEGORY_OPTIONS}
|
||||
style={{ width: 160 }}
|
||||
onChange={(value) => {
|
||||
setCategory(value)
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
value={channel || undefined}
|
||||
placeholder="全部渠道"
|
||||
style={{ width: 180 }}
|
||||
options={Array.from(
|
||||
new Set((summaryQuery.data?.data.channelDaily || []).map((item) => item.channel)),
|
||||
).map((value) => ({ value, label: value }))}
|
||||
onChange={(value) => {
|
||||
setChannel(value || '')
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={transactionsQuery.isFetching}
|
||||
onClick={() => void transactionsQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table<AdminFinanceTransaction>
|
||||
rowKey={(row) => `${row.category}-${row.sourceId}-${row.occurredAt}`}
|
||||
loading={transactionsQuery.isLoading}
|
||||
dataSource={transactionData?.items || []}
|
||||
columns={transactionColumns}
|
||||
pagination={buildAdminTablePagination({
|
||||
current: transactionData?.pagination.page || page,
|
||||
pageSize: transactionData?.pagination.pageSize || pageSize,
|
||||
total: transactionData?.pagination.total || 0,
|
||||
onChange: (nextPage, nextPageSize) => {
|
||||
setPage(nextPage)
|
||||
setPageSize(nextPageSize)
|
||||
},
|
||||
})}
|
||||
scroll={{ x: 780 }}
|
||||
/>
|
||||
</Card>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function formatFinanceCategory(value: string) {
|
||||
return CATEGORY_OPTIONS.find((item) => item.value === value)?.label || value || '-'
|
||||
}
|
||||
|
||||
function FinanceStatistic({ title, value }: { title: string; value: number }) {
|
||||
return (
|
||||
<Col xs={24} sm={12} xl={8}>
|
||||
<Card>
|
||||
<Statistic title={title} value={formatMoney(value)} />
|
||||
</Card>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
@@ -56,6 +56,7 @@ type CreateUserForm = {
|
||||
const USER_ROLE_OPTIONS = [
|
||||
{ label: '普通运营', value: 'operator' },
|
||||
{ label: '客服', value: 'support' },
|
||||
{ label: '财务', value: 'finance' },
|
||||
{ label: '管理员', value: 'admin' },
|
||||
]
|
||||
|
||||
@@ -482,6 +483,7 @@ export default function AdminUsersPage() {
|
||||
function formatRoleLabel(role: AdminRole) {
|
||||
if (role === 'admin') return '管理员'
|
||||
if (role === 'support') return '客服'
|
||||
if (role === 'finance') return '财务'
|
||||
return '普通运营'
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,12 @@ export default function AdminWorkerPlatformPage() {
|
||||
const role = getAdminRole()
|
||||
const isAdmin = role === 'admin'
|
||||
const isSupport = role === 'support'
|
||||
const visibleActiveTab = !isAdmin && activeTab === 'notifications' ? 'orders' : activeTab
|
||||
const isFinance = role === 'finance'
|
||||
const visibleActiveTab = isFinance
|
||||
? 'finance'
|
||||
: !isAdmin && activeTab === 'notifications'
|
||||
? 'orders'
|
||||
: activeTab
|
||||
|
||||
useEffect(() => {
|
||||
const tab = searchParams.get('tab')
|
||||
@@ -54,6 +59,7 @@ export default function AdminWorkerPlatformPage() {
|
||||
|
||||
function changeTab(nextTab: string) {
|
||||
if (!isWorkerPlatformTab(nextTab)) return
|
||||
if (isFinance && nextTab !== 'finance') return
|
||||
setActiveTab(nextTab)
|
||||
localStorage.setItem(ACTIVE_TAB_STORAGE_KEY, nextTab)
|
||||
setSearchParams(
|
||||
@@ -77,46 +83,54 @@ export default function AdminWorkerPlatformPage() {
|
||||
title="接单平台"
|
||||
description="管理打手、接单工单、等级权限、售后追责和本地 Mock 工单。"
|
||||
/>
|
||||
<SummaryCards />
|
||||
{!isFinance ? <SummaryCards /> : null}
|
||||
<Tabs
|
||||
destroyOnHidden={false}
|
||||
activeKey={visibleActiveTab}
|
||||
onChange={changeTab}
|
||||
items={
|
||||
isSupport
|
||||
? [
|
||||
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
|
||||
{
|
||||
key: 'cancel-requests',
|
||||
label: '打手退单审核',
|
||||
children: <CancelRequestsPanel />,
|
||||
},
|
||||
{ key: 'feedbacks', label: '问题反馈', children: <FeedbacksPanel /> },
|
||||
afterSalesTab,
|
||||
]
|
||||
: [
|
||||
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
|
||||
{
|
||||
key: 'cancel-requests',
|
||||
label: '打手退单审核',
|
||||
children: <CancelRequestsPanel />,
|
||||
},
|
||||
{ key: 'feedbacks', label: '问题反馈', children: <FeedbacksPanel /> },
|
||||
afterSalesTab,
|
||||
...(isAdmin
|
||||
? [{ key: 'announcement', label: '杂项配置', children: <AnnouncementPanel /> }]
|
||||
: []),
|
||||
{ key: 'rules', label: '接单模板', children: <ProductRulesPanel /> },
|
||||
{ key: 'product-match', label: '匹配诊断', children: <ProductMatchPanel /> },
|
||||
{ key: 'categories', label: '分类', children: <CategoriesPanel /> },
|
||||
{ key: 'workers', label: '打手', children: <WorkersPanel /> },
|
||||
{ key: 'leaderboard', label: '排行榜', children: <LeaderboardPanel /> },
|
||||
{ key: 'finance', label: '资金', children: <FinancePanel /> },
|
||||
{ key: 'levels', label: '等级权限', children: <LevelsPanel /> },
|
||||
...(isAdmin
|
||||
? [{ key: 'notifications', label: '通知配置', children: <NotificationsPanel /> }]
|
||||
: []),
|
||||
]
|
||||
isFinance
|
||||
? [{ key: 'finance', label: '资金审核', children: <FinancePanel /> }]
|
||||
: isSupport
|
||||
? [
|
||||
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
|
||||
{
|
||||
key: 'cancel-requests',
|
||||
label: '打手退单审核',
|
||||
children: <CancelRequestsPanel />,
|
||||
},
|
||||
{ key: 'feedbacks', label: '问题反馈', children: <FeedbacksPanel /> },
|
||||
afterSalesTab,
|
||||
]
|
||||
: [
|
||||
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
|
||||
{
|
||||
key: 'cancel-requests',
|
||||
label: '打手退单审核',
|
||||
children: <CancelRequestsPanel />,
|
||||
},
|
||||
{ key: 'feedbacks', label: '问题反馈', children: <FeedbacksPanel /> },
|
||||
afterSalesTab,
|
||||
...(isAdmin
|
||||
? [{ key: 'announcement', label: '杂项配置', children: <AnnouncementPanel /> }]
|
||||
: []),
|
||||
{ key: 'rules', label: '接单模板', children: <ProductRulesPanel /> },
|
||||
{ key: 'product-match', label: '匹配诊断', children: <ProductMatchPanel /> },
|
||||
{ key: 'categories', label: '分类', children: <CategoriesPanel /> },
|
||||
{ key: 'workers', label: '打手', children: <WorkersPanel /> },
|
||||
{ key: 'leaderboard', label: '排行榜', children: <LeaderboardPanel /> },
|
||||
{ key: 'finance', label: '资金', children: <FinancePanel /> },
|
||||
{ key: 'levels', label: '等级权限', children: <LevelsPanel /> },
|
||||
...(isAdmin
|
||||
? [
|
||||
{
|
||||
key: 'notifications',
|
||||
label: '通知配置',
|
||||
children: <NotificationsPanel />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -38,6 +38,7 @@ import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
|
||||
import dayjs, { ADMIN_DATE_TIME_FORMAT } from '@/lib/dayjs'
|
||||
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { getAdminRole } from '@/utils/admin-auth'
|
||||
import { asRecord, formatMoney } from './shared'
|
||||
type FinanceConfigFormValues = {
|
||||
depositUnfreezeDays?: number
|
||||
@@ -63,6 +64,7 @@ type FinanceReviewAction = 'approved' | 'rejected' | 'cancelled'
|
||||
|
||||
export default function FinancePanel() {
|
||||
const { message } = App.useApp()
|
||||
const canEditConfig = getAdminRole() !== 'finance'
|
||||
const queryClient = useQueryClient()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const [configForm] = Form.useForm<FinanceConfigFormValues>()
|
||||
@@ -436,7 +438,12 @@ export default function FinancePanel() {
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
<Button type="primary" htmlType="submit" loading={savingConfig}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={savingConfig}
|
||||
disabled={!canEditConfig}
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { apiGet } from '@/lib/http'
|
||||
import type { AdminFinanceSummary, AdminFinanceTransactionResult } from '@/types/admin/finance'
|
||||
|
||||
export function fetchAdminFinanceSummary(params: { dateFrom?: string; dateTo?: string } = {}) {
|
||||
return apiGet<AdminFinanceSummary>('/api/v1/admin/finance/summary', params)
|
||||
}
|
||||
|
||||
export function fetchAdminFinanceTransactions(
|
||||
params: {
|
||||
dateFrom?: string
|
||||
dateTo?: string
|
||||
category?: string
|
||||
channel?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
} = {},
|
||||
) {
|
||||
return apiGet<AdminFinanceTransactionResult>('/api/v1/admin/finance/transactions', params)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './auth'
|
||||
export * from './dashboard'
|
||||
export * from './finance'
|
||||
export * from './users'
|
||||
export * from './audit-logs'
|
||||
export * from './platform-config'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type AdminRole = 'admin' | 'operator' | 'support'
|
||||
export type AdminRole = 'admin' | 'operator' | 'support' | 'finance'
|
||||
export type AdminUserStatus = 'active' | 'disabled'
|
||||
|
||||
export interface AdminLoginResponse {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
export type AdminFinanceDailyItem = {
|
||||
date: string
|
||||
paidOrderCount: number
|
||||
paidAmount: number
|
||||
refundAmount: number
|
||||
workerRewardAmount: number
|
||||
withdrawAmount: number
|
||||
rechargeAmount: number
|
||||
afterSalesRefundAmount: number
|
||||
workerRecoveryAmount: number
|
||||
netCashFlow: number
|
||||
}
|
||||
|
||||
export type AdminFinanceOverview = {
|
||||
paidOrderCount: number
|
||||
paidAmount: number
|
||||
refundAmount: number
|
||||
workerRewardAmount: number
|
||||
withdrawAmount: number
|
||||
rechargeAmount: number
|
||||
afterSalesRefundAmount: number
|
||||
workerRecoveryAmount: number
|
||||
netCashFlow: number
|
||||
availableAmount: number
|
||||
frozenDepositAmount: number
|
||||
pendingUnfreezeAmount: number
|
||||
frozenWithdrawAmount: number
|
||||
pendingWithdrawAmount: number
|
||||
pendingWithdrawCount: number
|
||||
}
|
||||
|
||||
export type AdminFinanceTransaction = {
|
||||
category: string
|
||||
channel: string
|
||||
sourceId: number
|
||||
occurredAt: string
|
||||
amount: number
|
||||
direction: 'in' | 'out'
|
||||
label: string
|
||||
referenceNo: string
|
||||
}
|
||||
|
||||
export type AdminFinanceChannelDailyItem = {
|
||||
date: string
|
||||
channel: string
|
||||
category: string
|
||||
count: number
|
||||
amount: number
|
||||
}
|
||||
|
||||
export type AdminFinanceSummary = {
|
||||
range: { from: string; to: string }
|
||||
overview: AdminFinanceOverview
|
||||
daily: AdminFinanceDailyItem[]
|
||||
channelDaily: AdminFinanceChannelDailyItem[]
|
||||
}
|
||||
|
||||
export type AdminFinanceTransactionResult = {
|
||||
items: AdminFinanceTransaction[]
|
||||
pagination: { page: number; pageSize: number; total: number }
|
||||
range: { from: string; to: string }
|
||||
}
|
||||
@@ -13,6 +13,16 @@ export type { AdminAuditLogItem } from './audit-logs'
|
||||
// Dashboard types
|
||||
export type { AdminDashboardSummary } from './dashboard'
|
||||
|
||||
// Finance types
|
||||
export type {
|
||||
AdminFinanceDailyItem,
|
||||
AdminFinanceChannelDailyItem,
|
||||
AdminFinanceOverview,
|
||||
AdminFinanceTransaction,
|
||||
AdminFinanceSummary,
|
||||
AdminFinanceTransactionResult,
|
||||
} from './finance'
|
||||
|
||||
// Login logs
|
||||
export type { AdminLoginLogItem, AdminLoginLogListResult } from './login-logs'
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ const ADMIN_USERNAME_KEY = 'order-site-admin-username'
|
||||
const ADMIN_ROLE_KEY = 'order-site-admin-role'
|
||||
const ADMIN_PERMISSIONS_KEY = 'order-site-admin-permissions'
|
||||
|
||||
const ADMIN_ROLE_LEVEL: Record<'support' | 'operator' | 'admin', number> = {
|
||||
const ADMIN_ROLE_LEVEL: Record<'support' | 'operator' | 'admin' | 'finance', number> = {
|
||||
support: 1,
|
||||
operator: 2,
|
||||
admin: 3,
|
||||
finance: 0,
|
||||
}
|
||||
|
||||
export function getAdminToken() {
|
||||
@@ -30,7 +31,7 @@ export function getAdminUsername() {
|
||||
|
||||
export function getAdminRole() {
|
||||
const role = localStorage.getItem(ADMIN_ROLE_KEY)
|
||||
if (role === 'admin' || role === 'operator' || role === 'support') {
|
||||
if (role === 'admin' || role === 'operator' || role === 'support' || role === 'finance') {
|
||||
return role
|
||||
}
|
||||
|
||||
@@ -57,12 +58,15 @@ export function hasAdminPermission(permission: string) {
|
||||
return getAdminPermissions().includes(String(permission || '').trim())
|
||||
}
|
||||
|
||||
export function hasAdminRole(role: 'admin' | 'operator' | 'support') {
|
||||
export function hasAdminRole(role: 'admin' | 'operator' | 'support' | 'finance') {
|
||||
if (!hasAdminSession()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return ADMIN_ROLE_LEVEL[getAdminRole()] >= ADMIN_ROLE_LEVEL[role]
|
||||
const currentRole = getAdminRole()
|
||||
if (role === 'finance') return currentRole === 'finance'
|
||||
if (currentRole === 'finance') return false
|
||||
return ADMIN_ROLE_LEVEL[currentRole] >= ADMIN_ROLE_LEVEL[role]
|
||||
}
|
||||
|
||||
export function setAdminSession(
|
||||
@@ -71,7 +75,7 @@ export function setAdminSession(
|
||||
user?: {
|
||||
userId: number
|
||||
username: string
|
||||
role: 'admin' | 'operator' | 'support'
|
||||
role: 'admin' | 'operator' | 'support' | 'finance'
|
||||
permissions?: string[]
|
||||
},
|
||||
) {
|
||||
|
||||
@@ -39,6 +39,7 @@ export const adminUserRoleOptions = [
|
||||
{ label: '管理员', value: 'admin' },
|
||||
{ label: '普通运营', value: 'operator' },
|
||||
{ label: '客服', value: 'support' },
|
||||
{ label: '财务', value: 'finance' },
|
||||
]
|
||||
|
||||
export const adminUserStatusOptions = [
|
||||
|
||||
Reference in New Issue
Block a user