Files
order_site/apps/frontend/src/pages/admin/panels/ConsignmentContactsPanel.tsx
T

334 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { PlusOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import {
App,
Button,
Card,
Form,
Input,
InputNumber,
Modal,
Select,
Space,
Switch,
Tag,
Typography,
} from 'antd'
import { useEffect, useState } from 'react'
import PercentTable from '@/components/common/PercentTable'
import { fetchAdminSession } from '@/services/admin/auth'
import {
createAdminConsignmentContact,
deleteAdminConsignmentContact,
fetchAdminConsignmentContacts,
setAdminConsignmentContactEnabled,
updateAdminConsignmentContact,
} from '@/services/admin/consignment'
import type { ConsignmentContactSupporter } from '@/types/consignment'
import { resolveAdminPermission } from '@/utils/admin-auth'
import { formatDateTime } from '@/utils/date-time'
import type { TableColumnsType } from 'antd'
const { Text } = Typography
type ContactForm = {
name: string
note?: string
sortOrder?: number
enabled?: boolean
}
const ENABLED_FILTER_OPTIONS = [
{ label: '全部状态', value: '' },
{ label: '启用中', value: 'true' },
{ label: '已禁用', value: 'false' },
]
/**
* 接单平台 → 对接客服。
*
* 独立名单(不复用后台账号):打手发布寄卖时必选,后台按人统计提成与快速联系。
* 已被寄卖记录引用的客服只能禁用,不能删除,保证历史单归属不丢。
*/
export default function ConsignmentContactsPanel() {
const { message, modal } = App.useApp()
const queryClient = useQueryClient()
const [form] = Form.useForm<ContactForm>()
const [keywordInput, setKeywordInput] = useState('')
const [keyword, setKeyword] = useState('')
const [enabledFilter, setEnabledFilter] = useState('')
const [editingContact, setEditingContact] = useState<ConsignmentContactSupporter | null>(null)
const [formOpen, setFormOpen] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [actionId, setActionId] = useState<number | null>(null)
useEffect(() => {
const timer = window.setTimeout(() => setKeyword(keywordInput.trim()), 300)
return () => window.clearTimeout(timer)
}, [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({
queryKey: ['admin-consignment-contacts', keyword, enabledFilter],
queryFn: () =>
fetchAdminConsignmentContacts({
keyword: keyword || undefined,
enabled: enabledFilter || undefined,
}),
})
const items = query.data?.data.items || []
const total = Number(query.data?.data.total || 0)
const enabledCount = items.filter((item) => item.enabled).length
async function reload() {
await queryClient.invalidateQueries({ queryKey: ['admin-consignment-contacts'] })
}
function openCreate() {
setEditingContact(null)
form.resetFields()
form.setFieldsValue({ enabled: true, sortOrder: 0 })
setFormOpen(true)
}
function openEdit(record: ConsignmentContactSupporter) {
setEditingContact(record)
form.setFieldsValue({
name: record.name,
note: record.note,
sortOrder: record.sortOrder,
enabled: record.enabled,
})
setFormOpen(true)
}
async function handleSubmit() {
try {
const values = await form.validateFields()
setSubmitting(true)
const payload = {
name: values.name,
note: values.note || '',
sortOrder: Number(values.sortOrder || 0),
enabled: values.enabled !== false,
}
if (editingContact) {
await updateAdminConsignmentContact(editingContact.id, payload)
message.success('对接客服已更新')
} else {
await createAdminConsignmentContact(payload)
message.success('对接客服已新增')
}
setFormOpen(false)
setEditingContact(null)
await reload()
} catch (error: any) {
if (error?.errorFields) return
message.error(error?.message || '保存对接客服失败')
} finally {
setSubmitting(false)
}
}
async function handleToggleEnabled(record: ConsignmentContactSupporter, enabled: boolean) {
setActionId(record.id)
try {
await setAdminConsignmentContactEnabled(record.id, enabled)
message.success(enabled ? '已启用该对接客服' : '已禁用该对接客服')
await reload()
} catch (error: any) {
message.error(error?.message || '更新对接客服状态失败')
} finally {
setActionId(null)
}
}
function handleDelete(record: ConsignmentContactSupporter) {
modal.confirm({
title: `删除对接客服「${record.name}」?`,
content:
'只有未被任何寄卖记录使用的客服可以删除。已被使用的请改为「禁用」,历史寄卖单仍会保留对接客服信息。',
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await deleteAdminConsignmentContact(record.id)
message.success('对接客服已删除')
await reload()
} catch (error: any) {
message.error(error?.message || '删除对接客服失败')
throw error
}
},
})
}
const columns: TableColumnsType<ConsignmentContactSupporter> = [
{
title: '姓名',
dataIndex: 'name',
width: 160,
render: (value: string) => <Text strong>{value}</Text>,
},
{
title: '备注',
dataIndex: 'note',
width: 240,
ellipsis: true,
render: (value: string) =>
value ? <Text style={{ fontSize: 12 }}>{value}</Text> : <Text type="secondary">-</Text>,
},
{
title: '排序',
dataIndex: 'sortOrder',
width: 80,
render: (value: number) => value || 0,
},
{
title: '状态',
key: 'enabled',
width: 110,
render: (_, record) => (
<Space size={6}>
<Switch
size="small"
checked={record.enabled}
disabled={!canManage}
loading={actionId === record.id}
checkedChildren="启用"
unCheckedChildren="禁用"
onChange={(checked) => void handleToggleEnabled(record, checked)}
/>
<Tag color={record.enabled ? 'green' : 'default'} style={{ marginInlineEnd: 0 }}>
{record.enabled ? '启用中' : '已禁用'}
</Tag>
</Space>
),
},
{
title: '更新时间',
dataIndex: 'updatedAt',
width: 170,
render: (value: string) => formatDateTime(value),
},
{
title: '操作',
key: 'actions',
width: 140,
render: (_, record) =>
canManage ? (
<Space size={4}>
<Button size="small" type="link" onClick={() => openEdit(record)}>
编辑
</Button>
<Button size="small" type="link" danger onClick={() => handleDelete(record)}>
删除
</Button>
</Space>
) : (
<Text type="secondary">-</Text>
),
},
]
return (
<div>
<Card size="small" style={{ marginBottom: 16 }}>
<Space direction="vertical" size={10} style={{ width: '100%' }}>
<Space wrap size={12}>
<Input
allowClear
prefix={<SearchOutlined />}
placeholder="搜索姓名 / 备注"
value={keywordInput}
onChange={(event) => setKeywordInput(event.target.value)}
style={{ width: 280 }}
/>
<Select
value={enabledFilter}
options={ENABLED_FILTER_OPTIONS}
onChange={setEnabledFilter}
style={{ width: 140 }}
/>
<Button icon={<ReloadOutlined />} onClick={() => void query.refetch()}>
刷新
</Button>
<Text type="secondary">
共 {total} 个,本页启用 {enabledCount} 个
</Text>
{canManage ? (
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
新增对接客服
</Button>
) : null}
</Space>
<Text type="secondary" style={{ fontSize: 12 }}>
打手发布寄卖时必须选择对接客服;已被寄卖记录使用的客服只能禁用,不能删除。
</Text>
</Space>
</Card>
<PercentTable<ConsignmentContactSupporter>
rowKey="id"
loading={query.isLoading}
columns={columns}
dataSource={items}
pagination={false}
size="small"
/>
<Modal
title={editingContact ? `编辑对接客服 · ${editingContact.name}` : '新增对接客服'}
open={formOpen}
onCancel={() => {
setFormOpen(false)
setEditingContact(null)
}}
onOk={() => void handleSubmit()}
confirmLoading={submitting}
destroyOnClose
>
<Form form={form} layout="vertical">
<Form.Item
name="name"
label="姓名"
rules={[{ required: true, message: '请填写对接客服姓名' }]}
>
<Input placeholder="如:小张" maxLength={60} />
</Form.Item>
<Form.Item name="note" label="备注">
<Input.TextArea rows={2} placeholder="如:负责苏尔南寄卖 / 白班" maxLength={200} />
</Form.Item>
<Space size={24} align="start">
<Form.Item
name="sortOrder"
label="排序"
tooltip="数字越小越靠前;打手端默认选中第一个启用项"
>
<InputNumber min={0} max={9999} style={{ width: 140 }} />
</Form.Item>
<Form.Item name="enabled" label="启用" valuePropName="checked">
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
</Form.Item>
</Space>
</Form>
</Modal>
</div>
)
}