266 lines
8.5 KiB
TypeScript
266 lines
8.5 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Table, Button, Tag, Space, Typography, message, Modal, Card, Popconfirm, Empty } from 'antd';
|
|
import { CheckOutlined, DeleteOutlined, EyeOutlined, FilterOutlined, MessageOutlined } from '@ant-design/icons';
|
|
import { useAdminStore } from '../store';
|
|
import { api } from '../api/client';
|
|
import { formatDate } from '../utils/formatDate';
|
|
|
|
interface ContactRequest {
|
|
id: string;
|
|
userId: string;
|
|
phone: string;
|
|
companyName: string;
|
|
industry: string;
|
|
name: string;
|
|
message: string | null;
|
|
isHandled: boolean;
|
|
createdAt: string;
|
|
}
|
|
|
|
const AdminContactRequests: React.FC = () => {
|
|
const [data, setData] = useState<ContactRequest[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
const [page, setPage] = useState(1);
|
|
const [pageSize, setPageSize] = useState(10);
|
|
const [isHandledFilter, setIsHandledFilter] = useState<boolean | null>(null);
|
|
const [selectedItem, setSelectedItem] = useState<ContactRequest | null>(null);
|
|
const [detailModalOpen, setDetailModalOpen] = useState(false);
|
|
|
|
const { user } = useAdminStore();
|
|
|
|
const fetchData = async () => {
|
|
if (!user?.isAdmin) return;
|
|
setLoading(true);
|
|
try {
|
|
const query = new URLSearchParams();
|
|
query.set('page', String(page));
|
|
query.set('page_size', String(pageSize));
|
|
if (isHandledFilter !== null) {
|
|
query.set('is_handled', String(isHandledFilter));
|
|
}
|
|
const res = await api.get<{ items: ContactRequest[]; total: number }>(`/contact/requests?${query.toString()}`);
|
|
setData(res.items);
|
|
setTotal(res.total);
|
|
} catch (err: any) {
|
|
message.error(err?.message || '获取失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [page, pageSize, isHandledFilter]);
|
|
|
|
const handleMarkHandled = async (id: string) => {
|
|
try {
|
|
await api.put(`/contact/requests/${id}/handle`);
|
|
message.success('已标记为处理');
|
|
fetchData();
|
|
} catch (err: any) {
|
|
message.error(err?.message || '操作失败');
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
try {
|
|
await api.delete(`/contact/requests/${id}`);
|
|
message.success('已删除');
|
|
fetchData();
|
|
} catch (err: any) {
|
|
message.error(err?.message || '删除失败');
|
|
}
|
|
};
|
|
|
|
const handleViewDetail = (item: ContactRequest) => {
|
|
setSelectedItem(item);
|
|
setDetailModalOpen(true);
|
|
};
|
|
|
|
const handlePageChange = (p: number, ps: number) => {
|
|
setPage(p);
|
|
setPageSize(ps);
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
title: '姓名',
|
|
dataIndex: 'name',
|
|
key: 'name',
|
|
width: 100,
|
|
render: (v: string) => <Typography.Text strong>{v}</Typography.Text>,
|
|
},
|
|
{
|
|
title: '手机号',
|
|
dataIndex: 'phone',
|
|
key: 'phone',
|
|
width: 120,
|
|
},
|
|
{
|
|
title: '公司名称',
|
|
dataIndex: 'companyName',
|
|
key: 'companyName',
|
|
width: 150,
|
|
ellipsis: true,
|
|
},
|
|
{
|
|
title: '行业',
|
|
dataIndex: 'industry',
|
|
key: 'industry',
|
|
width: 120,
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'isHandled',
|
|
key: 'isHandled',
|
|
width: 80,
|
|
render: (isHandled: boolean) => (
|
|
<Tag color={isHandled ? 'green' : 'orange'}>
|
|
{isHandled ? '已处理' : '待处理'}
|
|
</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '提交时间',
|
|
dataIndex: 'createdAt',
|
|
key: 'createdAt',
|
|
width: 160,
|
|
render: (date: string) => formatDate(date),
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'actions',
|
|
width: 180,
|
|
render: (_: unknown, record: ContactRequest) => (
|
|
<Space size={4}>
|
|
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>
|
|
查看
|
|
</Button>
|
|
{!record.isHandled && (
|
|
<Button type="link" size="small" icon={<CheckOutlined />} onClick={() => handleMarkHandled(record.id)}>
|
|
标记处理
|
|
</Button>
|
|
)}
|
|
<Popconfirm title="确定删除该记录?" onConfirm={() => handleDelete(record.id)}>
|
|
<Button type="link" danger size="small" icon={<DeleteOutlined />}>删除</Button>
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<Card variant="outlined" style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
|
<Space>
|
|
<MessageOutlined style={{ fontSize: 18, color: '#6366f1' }} />
|
|
<Typography.Text strong style={{ fontSize: 16 }}>联系请求管理</Typography.Text>
|
|
<Tag color="purple">共 {total} 条记录</Tag>
|
|
</Space>
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
<Button
|
|
type={isHandledFilter === null ? 'primary' : 'default'}
|
|
onClick={() => setIsHandledFilter(null)}
|
|
icon={<FilterOutlined />}
|
|
size="small"
|
|
>
|
|
全部
|
|
</Button>
|
|
<Button
|
|
type={isHandledFilter === false ? 'primary' : 'default'}
|
|
onClick={() => setIsHandledFilter(false)}
|
|
size="small"
|
|
>
|
|
待处理
|
|
</Button>
|
|
<Button
|
|
type={isHandledFilter === true ? 'primary' : 'default'}
|
|
onClick={() => setIsHandledFilter(true)}
|
|
size="small"
|
|
>
|
|
已处理
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div style={{ textAlign: 'center', padding: 40 }}>加载中...</div>
|
|
) : data.length === 0 ? (
|
|
<Empty description="暂无联系请求" style={{ padding: '40px 0' }} />
|
|
) : (
|
|
<Table
|
|
columns={columns}
|
|
dataSource={data}
|
|
rowKey="id"
|
|
loading={loading}
|
|
pagination={{
|
|
current: page,
|
|
pageSize: pageSize,
|
|
total: total,
|
|
onChange: handlePageChange,
|
|
showSizeChanger: true,
|
|
showTotal: (t) => `共 ${t} 条记录`,
|
|
}}
|
|
scroll={{ x: 900 }}
|
|
/>
|
|
)}
|
|
</Card>
|
|
|
|
<Modal
|
|
title={<Space><EyeOutlined />联系请求详情</Space>}
|
|
open={detailModalOpen}
|
|
onCancel={() => setDetailModalOpen(false)}
|
|
footer={null}
|
|
width={500}
|
|
>
|
|
{selectedItem && (
|
|
<div style={{ padding: 8 }}>
|
|
<div style={{ marginBottom: 16 }}>
|
|
<Typography.Title level={4} style={{ marginBottom: 16 }}>
|
|
{selectedItem.name}
|
|
<Tag color={selectedItem.isHandled ? 'green' : 'orange'} style={{ marginLeft: 12 }}>
|
|
{selectedItem.isHandled ? '已处理' : '待处理'}
|
|
</Tag>
|
|
</Typography.Title>
|
|
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: 12 }}>
|
|
<Typography.Text style={{ color: '#64748b' }}>手机号:</Typography.Text>
|
|
<Typography.Text>{selectedItem.phone}</Typography.Text>
|
|
<Typography.Text style={{ color: '#64748b' }}>公司名称:</Typography.Text>
|
|
<Typography.Text>{selectedItem.companyName}</Typography.Text>
|
|
<Typography.Text style={{ color: '#64748b' }}>行业:</Typography.Text>
|
|
<Typography.Text>{selectedItem.industry}</Typography.Text>
|
|
<Typography.Text style={{ color: '#64748b' }}>提交时间:</Typography.Text>
|
|
<Typography.Text>{formatDate(selectedItem.createdAt)}</Typography.Text>
|
|
{selectedItem.message && (
|
|
<>
|
|
<Typography.Text style={{ color: '#64748b' }}>留言:</Typography.Text>
|
|
<Typography.Text>{selectedItem.message}</Typography.Text>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 12 }}>
|
|
{!selectedItem.isHandled && (
|
|
<Button
|
|
type="primary"
|
|
onClick={() => {
|
|
handleMarkHandled(selectedItem.id);
|
|
setDetailModalOpen(false);
|
|
}}
|
|
icon={<CheckOutlined />}
|
|
>
|
|
标记为已处理
|
|
</Button>
|
|
)}
|
|
<Button onClick={() => setDetailModalOpen(false)}>关闭</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Modal>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AdminContactRequests; |