334 lines
10 KiB
TypeScript
334 lines
10 KiB
TypeScript
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>
|
||
)
|
||
}
|