feat:寄卖增加对接客服,页面移动到接单平台

This commit is contained in:
2026-10-27 18:49:39 +08:00
parent 0d9fb29460
commit 2ddc26091c
6 changed files with 86 additions and 56 deletions
+5 -4
View File
@@ -24,9 +24,6 @@ const AdminFinancePage = lazy(() => import('@/pages/admin/AdminFinancePage'))
const AdminFinanceLedgerPage = lazy(() => import('@/pages/admin/AdminFinanceLedgerPage')) const AdminFinanceLedgerPage = lazy(() => import('@/pages/admin/AdminFinanceLedgerPage'))
const AdminSalesProfitPage = lazy(() => import('@/pages/admin/AdminSalesProfitPage')) const AdminSalesProfitPage = lazy(() => import('@/pages/admin/AdminSalesProfitPage'))
const AdminCostCenterPage = lazy(() => import('@/pages/admin/AdminCostCenterPage')) const AdminCostCenterPage = lazy(() => import('@/pages/admin/AdminCostCenterPage'))
const AdminConsignmentContactsPage = lazy(
() => import('@/pages/admin/AdminConsignmentContactsPage'),
)
const AdminFundPage = lazy(() => import('@/pages/admin/AdminFundPage')) const AdminFundPage = lazy(() => import('@/pages/admin/AdminFundPage'))
const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage')) const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage'))
const AdminKuaishouIndustryPage = lazy(() => import('@/pages/admin/AdminKuaishouIndustryPage')) const AdminKuaishouIndustryPage = lazy(() => import('@/pages/admin/AdminKuaishouIndustryPage'))
@@ -161,6 +158,11 @@ function AdminApplication() {
<Route path="fund" element={<AdminFundPage />} /> <Route path="fund" element={<AdminFundPage />} />
</Route> </Route>
<Route path="worker-platform" element={<AdminWorkerPlatformPage />} /> <Route path="worker-platform" element={<AdminWorkerPlatformPage />} />
{/* 旧「配置 → 对接客服」地址已并入接单平台 tab,书签继续可用。 */}
<Route
path="consignment-contacts"
element={<Navigate to="/admin/worker-platform?tab=consignment-contacts" replace />}
/>
<Route element={<RequireRole roles={['admin', 'operator', 'support']} />}> <Route element={<RequireRole roles={['admin', 'operator', 'support']} />}>
<Route path="kuaishou-industry" element={<AdminKuaishouIndustryPage />} /> <Route path="kuaishou-industry" element={<AdminKuaishouIndustryPage />} />
<Route <Route
@@ -171,7 +173,6 @@ function AdminApplication() {
</Route> </Route>
<Route element={<RequireRole roles={['admin']} />}> <Route element={<RequireRole roles={['admin']} />}>
<Route path="users" element={<AdminUsersPage />} /> <Route path="users" element={<AdminUsersPage />} />
<Route path="consignment-contacts" element={<AdminConsignmentContactsPage />} />
<Route path="platform-shops" element={<AdminPlatformShopsPage />} /> <Route path="platform-shops" element={<AdminPlatformShopsPage />} />
<Route path="platform-fulfillment" element={<AdminPlatformFulfillmentPage />} /> <Route path="platform-fulfillment" element={<AdminPlatformFulfillmentPage />} />
<Route path="audit-logs" element={<AdminAuditLogsPage />} /> <Route path="audit-logs" element={<AdminAuditLogsPage />} />
+2 -7
View File
@@ -4,7 +4,6 @@ import {
ApartmentOutlined, ApartmentOutlined,
BellOutlined, BellOutlined,
BugOutlined, BugOutlined,
CustomerServiceOutlined,
DashboardOutlined, DashboardOutlined,
FileSearchOutlined, FileSearchOutlined,
LogoutOutlined, LogoutOutlined,
@@ -284,11 +283,6 @@ export default function AdminLayout() {
label: '配置', label: '配置',
children: [ children: [
{ key: '/admin/users', icon: <TeamOutlined />, label: '后台用户' }, { key: '/admin/users', icon: <TeamOutlined />, label: '后台用户' },
{
key: '/admin/consignment-contacts',
icon: <CustomerServiceOutlined />,
label: '对接客服',
},
{ key: '/admin/platform-shops', icon: <SettingOutlined />, label: '平台配置' }, { key: '/admin/platform-shops', icon: <SettingOutlined />, label: '平台配置' },
{ key: '/admin/platform-fulfillment', icon: <ShopOutlined />, label: '履约配置' }, { key: '/admin/platform-fulfillment', icon: <ShopOutlined />, label: '履约配置' },
], ],
@@ -857,7 +851,8 @@ function resolveSelectedKey(pathname: string) {
if (pathname.startsWith('/admin/finance')) return '/admin/finance' if (pathname.startsWith('/admin/finance')) return '/admin/finance'
if (pathname.startsWith('/admin/tasks')) return '/admin/tasks' if (pathname.startsWith('/admin/tasks')) return '/admin/tasks'
if (pathname.startsWith('/admin/worker-platform')) return '/admin/worker-platform' if (pathname.startsWith('/admin/worker-platform')) return '/admin/worker-platform'
if (pathname.startsWith('/admin/consignment-contacts')) return '/admin/consignment-contacts' // 旧「配置 → 对接客服」地址已并入接单平台 tab,保留高亮兼容。
if (pathname.startsWith('/admin/consignment-contacts')) return '/admin/worker-platform'
if (pathname.startsWith('/admin/kuaishou-industry')) return '/admin/kuaishou-industry' if (pathname.startsWith('/admin/kuaishou-industry')) return '/admin/kuaishou-industry'
if (pathname === '/admin/platform-shops') return '/admin/platform-shops' if (pathname === '/admin/platform-shops') return '/admin/platform-shops'
return pathname return pathname
@@ -15,6 +15,7 @@ import AfterSalesPanel, { type AfterSalesPanelHandle } from './panels/AfterSales
import AnnouncementPanel from './panels/AnnouncementPanel' import AnnouncementPanel from './panels/AnnouncementPanel'
import CancelRequestsPanel from './panels/CancelRequestsPanel' import CancelRequestsPanel from './panels/CancelRequestsPanel'
import CategoriesPanel from './panels/CategoriesPanel' import CategoriesPanel from './panels/CategoriesPanel'
import ConsignmentContactsPanel from './panels/ConsignmentContactsPanel'
import ConsignmentInventoryPanel from './panels/ConsignmentInventoryPanel' import ConsignmentInventoryPanel from './panels/ConsignmentInventoryPanel'
import ConsignmentSubmissionsPanel from './panels/ConsignmentSubmissionsPanel' import ConsignmentSubmissionsPanel from './panels/ConsignmentSubmissionsPanel'
import FeedbacksPanel from './panels/FeedbacksPanel' import FeedbacksPanel from './panels/FeedbacksPanel'
@@ -44,8 +45,10 @@ export default function AdminWorkerPlatformPage() {
const isFinance = role === 'finance' const isFinance = role === 'finance'
const canViewConsignment = hasAdminPermission('consignment.view') const canViewConsignment = hasAdminPermission('consignment.view')
const isConsignmentTab = const isConsignmentTab =
activeTab === 'consignment-inventory' || activeTab === 'consignment-submissions' activeTab === 'consignment-inventory' ||
// 没有 consignment.view 时这两个 tab 不渲染,把 URL/localStorage 里的旧选择也拉回工单页, activeTab === 'consignment-submissions' ||
activeTab === 'consignment-contacts'
// 没有 consignment.view 时这些 tab 不渲染,把 URL/localStorage 里的旧选择也拉回工单页,
// 否则会落在一个空的 tab 内容上。 // 否则会落在一个空的 tab 内容上。
const visibleActiveTab = isFinance const visibleActiveTab = isFinance
? 'finance' ? 'finance'
@@ -113,6 +116,11 @@ export default function AdminWorkerPlatformPage() {
label: '寄售仓库', label: '寄售仓库',
children: <ConsignmentInventoryPanel />, children: <ConsignmentInventoryPanel />,
}, },
{
key: 'consignment-contacts',
label: '对接客服',
children: <ConsignmentContactsPanel />,
},
{ {
key: 'consignment-submissions', key: 'consignment-submissions',
label: '寄卖提报审核', label: '寄卖提报审核',
@@ -16,8 +16,8 @@ import {
} from 'antd' } from 'antd'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import PageHeader from '@/components/admin/PageHeader'
import PercentTable from '@/components/common/PercentTable' import PercentTable from '@/components/common/PercentTable'
import { fetchAdminSession } from '@/services/admin/auth'
import { import {
createAdminConsignmentContact, createAdminConsignmentContact,
deleteAdminConsignmentContact, deleteAdminConsignmentContact,
@@ -26,6 +26,7 @@ import {
updateAdminConsignmentContact, updateAdminConsignmentContact,
} from '@/services/admin/consignment' } from '@/services/admin/consignment'
import type { ConsignmentContactSupporter } from '@/types/consignment' import type { ConsignmentContactSupporter } from '@/types/consignment'
import { resolveAdminPermission } from '@/utils/admin-auth'
import { formatDateTime } from '@/utils/date-time' import { formatDateTime } from '@/utils/date-time'
import type { TableColumnsType } from 'antd' import type { TableColumnsType } from 'antd'
@@ -45,12 +46,12 @@ const ENABLED_FILTER_OPTIONS = [
] ]
/** /**
* 配置 → 对接客服。 * 接单平台 → 对接客服。
* *
* 独立名单(不复用后台账号):打手发布寄卖时必选,后台按人统计提成与快速联系。 * 独立名单(不复用后台账号):打手发布寄卖时必选,后台按人统计提成与快速联系。
* 已被寄卖记录引用的客服只能禁用,不能删除,保证历史单归属不丢。 * 已被寄卖记录引用的客服只能禁用,不能删除,保证历史单归属不丢。
*/ */
export default function AdminConsignmentContactsPage() { export default function ConsignmentContactsPanel() {
const { message, modal } = App.useApp() const { message, modal } = App.useApp()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [form] = Form.useForm<ContactForm>() const [form] = Form.useForm<ContactForm>()
@@ -68,6 +69,18 @@ export default function AdminConsignmentContactsPage() {
return () => window.clearTimeout(timer) return () => window.clearTimeout(timer)
}, [keywordInput]) }, [keywordInput])
const adminSessionQuery = useQuery({
queryKey: ['admin-session'],
queryFn: fetchAdminSession,
retry: false,
})
// 写操作统一按 consignment.manage 门控:后端接口都挂了 requireAdminPermission,
// 没有权限时按钮点了只会 403,所以直接从界面隐藏。
const canManage = resolveAdminPermission(
adminSessionQuery.data?.data.user.permissions,
'consignment.manage',
)
const query = useQuery({ const query = useQuery({
queryKey: ['admin-consignment-contacts', keyword, enabledFilter], queryKey: ['admin-consignment-contacts', keyword, enabledFilter],
queryFn: () => queryFn: () =>
@@ -195,6 +208,7 @@ export default function AdminConsignmentContactsPage() {
<Switch <Switch
size="small" size="small"
checked={record.enabled} checked={record.enabled}
disabled={!canManage}
loading={actionId === record.id} loading={actionId === record.id}
checkedChildren="启用" checkedChildren="启用"
unCheckedChildren="禁用" unCheckedChildren="禁用"
@@ -216,52 +230,55 @@ export default function AdminConsignmentContactsPage() {
title: '操作', title: '操作',
key: 'actions', key: 'actions',
width: 140, width: 140,
render: (_, record) => ( render: (_, record) =>
<Space size={4}> canManage ? (
<Button size="small" type="link" onClick={() => openEdit(record)}> <Space size={4}>
编辑 <Button size="small" type="link" onClick={() => openEdit(record)}>
</Button> 编辑
<Button size="small" type="link" danger onClick={() => handleDelete(record)}> </Button>
删除 <Button size="small" type="link" danger onClick={() => handleDelete(record)}>
</Button> 删除
</Space> </Button>
), </Space>
) : (
<Text type="secondary">-</Text>
),
}, },
] ]
return ( return (
<div> <div>
<PageHeader
title="对接客服"
description="打手发布寄卖时必须选择对接客服;本名单独立于后台账号,用于提成归属与联系。"
extra={
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
新增对接客服
</Button>
}
/>
<Card size="small" style={{ marginBottom: 16 }}> <Card size="small" style={{ marginBottom: 16 }}>
<Space wrap size={12}> <Space direction="vertical" size={10} style={{ width: '100%' }}>
<Input <Space wrap size={12}>
allowClear <Input
prefix={<SearchOutlined />} allowClear
placeholder="搜索姓名 / 备注" prefix={<SearchOutlined />}
value={keywordInput} placeholder="搜索姓名 / 备注"
onChange={(event) => setKeywordInput(event.target.value)} value={keywordInput}
style={{ width: 280 }} onChange={(event) => setKeywordInput(event.target.value)}
/> style={{ width: 280 }}
<Select />
value={enabledFilter} <Select
options={ENABLED_FILTER_OPTIONS} value={enabledFilter}
onChange={setEnabledFilter} options={ENABLED_FILTER_OPTIONS}
style={{ width: 140 }} onChange={setEnabledFilter}
/> style={{ width: 140 }}
<Button icon={<ReloadOutlined />} onClick={() => void query.refetch()}> />
刷新 <Button icon={<ReloadOutlined />} onClick={() => void query.refetch()}>
</Button> 刷新
<Text type="secondary"> </Button>
共 {total} 个,本页启用 {enabledCount} 个 <Text type="secondary">
共 {total} 个,本页启用 {enabledCount} 个
</Text>
{canManage ? (
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
新增对接客服
</Button>
) : null}
</Space>
<Text type="secondary" style={{ fontSize: 12 }}>
打手发布寄卖时必须选择对接客服;已被寄卖记录使用的客服只能禁用,不能删除。
</Text> </Text>
</Space> </Space>
</Card> </Card>
@@ -14,6 +14,7 @@
export const WORKER_PLATFORM_TAB_KEYS: readonly string[] = [ export const WORKER_PLATFORM_TAB_KEYS: readonly string[] = [
'orders', 'orders',
'consignment-inventory', 'consignment-inventory',
'consignment-contacts',
'consignment-submissions', 'consignment-submissions',
'leaderboard', 'leaderboard',
'cancel-requests', 'cancel-requests',
@@ -38,6 +39,7 @@ export const WORKER_PLATFORM_TAB_KEYS: readonly string[] = [
*/ */
export const WORKER_PLATFORM_TAB_QUERY_KEYS: Record<string, string[][]> = { export const WORKER_PLATFORM_TAB_QUERY_KEYS: Record<string, string[][]> = {
'consignment-inventory': [['admin-consignment-inventories'], ['admin-consignment-stats']], 'consignment-inventory': [['admin-consignment-inventories'], ['admin-consignment-stats']],
'consignment-contacts': [['admin-consignment-contacts']],
'consignment-submissions': [ 'consignment-submissions': [
['admin-consignment-submissions'], ['admin-consignment-submissions'],
['admin-consignment-submissions-pending-total'], ['admin-consignment-submissions-pending-total'],
@@ -18,6 +18,13 @@ test('风控 tab 必须被 isWorkerPlatformTab 接受(漏登记会导致点击
assert.equal(isWorkerPlatformTab('risk-control'), true) assert.equal(isWorkerPlatformTab('risk-control'), true)
}) })
test('对接客服 tab 必须被 isWorkerPlatformTab 接受,并登记切页刷新缓存', () => {
assert.equal(isWorkerPlatformTab('consignment-contacts'), true)
assert.deepEqual(WORKER_PLATFORM_TAB_QUERY_KEYS['consignment-contacts'], [
['admin-consignment-contacts'],
])
})
test('每个登记了切页缓存的 tab 都必须在合法 tab 名单里', () => { test('每个登记了切页缓存的 tab 都必须在合法 tab 名单里', () => {
const nonTabs = new Set(WORKER_PLATFORM_NON_TAB_QUERY_KEYS) const nonTabs = new Set(WORKER_PLATFORM_NON_TAB_QUERY_KEYS)
for (const key of Object.keys(WORKER_PLATFORM_TAB_QUERY_KEYS)) { for (const key of Object.keys(WORKER_PLATFORM_TAB_QUERY_KEYS)) {