|
|
@@ -0,0 +1,379 @@
|
|
|
+import React, { useState } from 'react';
|
|
|
+import { Modal, Tabs, Descriptions, Table, Spin, Empty, Tag } from 'antd';
|
|
|
+import { App } from 'antd';
|
|
|
+import { useQuery } from '@tanstack/react-query';
|
|
|
+import dayjs from 'dayjs';
|
|
|
+import { clientClient, linkmanClient, hetongClient, expenseClient, fileClient } from '@/client/api';
|
|
|
+import type { InferResponseType } from 'hono/client';
|
|
|
+
|
|
|
+// 定义类型 - 直接匹配API响应格式
|
|
|
+type ClientItem = InferResponseType<typeof clientClient[':id']['$get'], 200>;
|
|
|
+type ContactItem = InferResponseType<typeof linkmanClient.$get, 200>;
|
|
|
+type ContractItem = InferResponseType<typeof hetongClient.$get, 200>;
|
|
|
+type ExpenseItem = InferResponseType<typeof expenseClient.$get, 200>;
|
|
|
+type FileItem = InferResponseType<typeof fileClient.$get, 200>;
|
|
|
+
|
|
|
+const { TabPane } = Tabs;
|
|
|
+
|
|
|
+interface ClientDetailModalProps {
|
|
|
+ clientId: number | null;
|
|
|
+ visible: boolean;
|
|
|
+ onClose: () => void;
|
|
|
+}
|
|
|
+
|
|
|
+const ClientDetailModal: React.FC<ClientDetailModalProps> = ({ clientId, visible, onClose }) => {
|
|
|
+ const { message } = App.useApp();
|
|
|
+ const [activeTab, setActiveTab] = useState('info');
|
|
|
+
|
|
|
+ // 获取客户详情
|
|
|
+ const { data: clientDetail, isLoading: loadingClient } = useQuery({
|
|
|
+ queryKey: ['client', clientId],
|
|
|
+ queryFn: async () => {
|
|
|
+ if (!clientId) return null;
|
|
|
+ const res = await clientClient[':id'].$get({ param: { id: clientId } });
|
|
|
+ if (!res.ok) throw new Error('获取客户详情失败');
|
|
|
+ return res.json();
|
|
|
+ },
|
|
|
+ enabled: !!clientId,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取相关联系人
|
|
|
+ const { data: contacts, isLoading: loadingContacts } = useQuery({
|
|
|
+ queryKey: ['contacts', clientId],
|
|
|
+ queryFn: async () => {
|
|
|
+ if (!clientId) return [];
|
|
|
+ const res = await linkmanClient.$get({ query: { page: 1, pageSize: 100 } });
|
|
|
+ if (!res.ok) throw new Error('获取联系人失败');
|
|
|
+ return res.json();
|
|
|
+ },
|
|
|
+ enabled: !!clientId && activeTab === 'contacts',
|
|
|
+ select: (data) => {
|
|
|
+ // 过滤属于当前客户的联系人
|
|
|
+ return data.data.filter(contact => contact.clientId === clientId);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取合同记录
|
|
|
+ const { data: contracts, isLoading: loadingContracts } = useQuery({
|
|
|
+ queryKey: ['contracts', clientId],
|
|
|
+ queryFn: async () => {
|
|
|
+ if (!clientId) return [];
|
|
|
+ const res = await hetongClient.$get({ query: { page: 1, pageSize: 100 } });
|
|
|
+ if (!res.ok) throw new Error('获取合同失败');
|
|
|
+ return res.json();
|
|
|
+ },
|
|
|
+ enabled: !!clientId && activeTab === 'contracts',
|
|
|
+ select: (data) => {
|
|
|
+ return data.data.filter(contract => contract.clientId === clientId);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取费用记录
|
|
|
+ const { data: expenses, isLoading: loadingExpenses } = useQuery({
|
|
|
+ queryKey: ['expenses', clientId],
|
|
|
+ queryFn: async () => {
|
|
|
+ if (!clientId) return [];
|
|
|
+ const res = await expenseClient.$get({ query: { page: 1, pageSize: 100 } });
|
|
|
+ if (!res.ok) throw new Error('获取费用失败');
|
|
|
+ return res.json();
|
|
|
+ },
|
|
|
+ enabled: !!clientId && activeTab === 'expenses',
|
|
|
+ select: (data) => {
|
|
|
+ return data.data.filter(expense => expense.clientId === clientId);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取文件记录
|
|
|
+ const { data: files, isLoading: loadingFiles } = useQuery({
|
|
|
+ queryKey: ['files', clientId],
|
|
|
+ queryFn: async () => {
|
|
|
+ if (!clientId) return [];
|
|
|
+ // 暂时获取所有文件,后续可根据需要过滤
|
|
|
+ const res = await fileClient.$get({ query: { page: 1, pageSize: 100 } });
|
|
|
+ if (!res.ok) throw new Error('获取文件失败');
|
|
|
+ return res.json();
|
|
|
+ },
|
|
|
+ enabled: !!clientId && activeTab === 'files',
|
|
|
+ select: (data) => {
|
|
|
+ // 这里可以根据实际业务逻辑过滤
|
|
|
+ return data.data;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 客户档案标签页
|
|
|
+ const renderClientInfoTab = () => {
|
|
|
+ if (loadingClient) return <Spin size="large" />;
|
|
|
+ if (!clientDetail) return <Empty description="客户信息加载失败" />;
|
|
|
+
|
|
|
+ const data = clientDetail;
|
|
|
+ return (
|
|
|
+ <div className="px-4">
|
|
|
+ <Descriptions title="基本信息" bordered column={2}>
|
|
|
+ <Descriptions.Item label="公司名称">{data.companyName}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="联系人">{data.contactPerson || '-'}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="联系电话">{data.mobile || data.telephone || '-'}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="邮箱">{data.email || '-'}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="地址">{data.address || '-'}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="行业">{data.industry || '-'}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="客户类型">{data.customerType || '-'}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="来源">{data.source || '-'}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="合作日期">
|
|
|
+ {data.startDate ? dayjs(data.startDate).format('YYYY-MM-DD') : '-'}
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label="状态">
|
|
|
+ <Tag color={data.status === 1 ? 'success' : 'error'}>
|
|
|
+ {data.status === 1 ? '有效' : '无效'}
|
|
|
+ </Tag>
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label="审核状态">
|
|
|
+ <Tag color={data.auditStatus === 1 ? 'success' : data.auditStatus === 2 ? 'error' : 'warning'}>
|
|
|
+ {data.auditStatus === 0 ? '待审核' : data.auditStatus === 1 ? '已审核' : '已拒绝'}
|
|
|
+ </Tag>
|
|
|
+ </Descriptions.Item>
|
|
|
+ </Descriptions>
|
|
|
+
|
|
|
+ <Descriptions title="附加信息" bordered column={2} className="mt-4">
|
|
|
+ <Descriptions.Item label="场地面积">{data.square || '-'}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="邮编">{data.zipCode || '-'}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="传真">{data.fax || '-'}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="网址">{data.homepage || '-'}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="子行业">{data.subIndustry || '-'}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="下次联系时间">
|
|
|
+ {data.nextContactTime ? dayjs(data.nextContactTime).format('YYYY-MM-DD HH:mm') : '-'}
|
|
|
+ </Descriptions.Item>
|
|
|
+ </Descriptions>
|
|
|
+
|
|
|
+ {data.description && (
|
|
|
+ <Descriptions title="备注信息" bordered column={1} className="mt-4">
|
|
|
+ <Descriptions.Item label="详细信息">{data.description}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="备注">{data.remarks || '-'}</Descriptions.Item>
|
|
|
+ </Descriptions>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ // 联系人标签页
|
|
|
+ const renderContactsTab = () => {
|
|
|
+ if (loadingContacts) return <Spin size="large" />;
|
|
|
+ const contactsData = contacts?.data || [];
|
|
|
+ const filteredContacts = contactsData.filter(contact => contact.clientId === clientId);
|
|
|
+
|
|
|
+ if (filteredContacts.length === 0) return <Empty description="暂无联系人" />;
|
|
|
+
|
|
|
+ const columns = [
|
|
|
+ { title: '姓名', dataIndex: 'name', key: 'name' },
|
|
|
+ { title: '职位', dataIndex: 'position', key: 'position', render: (text: string) => text || '-' },
|
|
|
+ { title: '手机', dataIndex: 'mobile', key: 'mobile', render: (text: string) => text || '-' },
|
|
|
+ { title: '电话', dataIndex: 'telephone', key: 'telephone', render: (text: string) => text || '-' },
|
|
|
+ { title: '邮箱', dataIndex: 'email', key: 'email', render: (text: string) => text || '-' },
|
|
|
+ { title: 'QQ', dataIndex: 'qq', key: 'qq', render: (text: string) => text || '-' },
|
|
|
+ ];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Table
|
|
|
+ columns={columns}
|
|
|
+ dataSource={filteredContacts}
|
|
|
+ rowKey="id"
|
|
|
+ pagination={false}
|
|
|
+ scroll={{ x: 800 }}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ // 合同标签页
|
|
|
+ const renderContractsTab = () => {
|
|
|
+ if (loadingContracts) return <Spin size="large" />;
|
|
|
+ const contractsData = contracts?.data || [];
|
|
|
+ const filteredContracts = contractsData.filter(contract => contract.clientId === clientId);
|
|
|
+
|
|
|
+ if (filteredContracts.length === 0) return <Empty description="暂无合同记录" />;
|
|
|
+
|
|
|
+ const columns = [
|
|
|
+ { title: '合同编号', dataIndex: 'contractNumber', key: 'contractNumber' },
|
|
|
+ {
|
|
|
+ title: '合同金额',
|
|
|
+ dataIndex: 'amount',
|
|
|
+ key: 'amount',
|
|
|
+ render: (amount: number) => `¥${amount.toLocaleString()}`,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '开始日期',
|
|
|
+ dataIndex: 'startDate',
|
|
|
+ key: 'startDate',
|
|
|
+ render: (date: string) => dayjs(date).format('YYYY-MM-DD'),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '结束日期',
|
|
|
+ dataIndex: 'endDate',
|
|
|
+ key: 'endDate',
|
|
|
+ render: (date: string) => dayjs(date).format('YYYY-MM-DD'),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '状态',
|
|
|
+ dataIndex: 'status',
|
|
|
+ key: 'status',
|
|
|
+ render: (status: string) => <Tag>{status}</Tag>,
|
|
|
+ },
|
|
|
+ { title: '描述', dataIndex: 'description', key: 'description', render: (text: string) => text || '-' },
|
|
|
+ ];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Table
|
|
|
+ columns={columns}
|
|
|
+ dataSource={filteredContracts}
|
|
|
+ rowKey="id"
|
|
|
+ pagination={{ pageSize: 10 }}
|
|
|
+ scroll={{ x: 1000 }}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ // 费用标签页
|
|
|
+ const renderExpensesTab = () => {
|
|
|
+ if (loadingExpenses) return <Spin size="large" />;
|
|
|
+ const expensesData = expenses?.data || [];
|
|
|
+ const filteredExpenses = expensesData.filter(expense => expense.clientId === clientId);
|
|
|
+
|
|
|
+ if (filteredExpenses.length === 0) return <Empty description="暂无费用记录" />;
|
|
|
+
|
|
|
+ const columns = [
|
|
|
+ { title: '费用类型', dataIndex: 'type', key: 'type' },
|
|
|
+ {
|
|
|
+ title: '金额',
|
|
|
+ dataIndex: 'amount',
|
|
|
+ key: 'amount',
|
|
|
+ render: (amount: number) => `¥${amount.toLocaleString()}`,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '发生日期',
|
|
|
+ dataIndex: 'expenseDate',
|
|
|
+ key: 'expenseDate',
|
|
|
+ render: (date: string) => dayjs(date).format('YYYY-MM-DD'),
|
|
|
+ },
|
|
|
+ { title: '描述', dataIndex: 'description', key: 'description', render: (text: string) => text || '-' },
|
|
|
+ {
|
|
|
+ title: '状态',
|
|
|
+ dataIndex: 'status',
|
|
|
+ key: 'status',
|
|
|
+ render: (status: string) => <Tag>{status}</Tag>,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '部门',
|
|
|
+ dataIndex: 'department',
|
|
|
+ key: 'department',
|
|
|
+ render: (text: string) => text || '-',
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Table
|
|
|
+ columns={columns}
|
|
|
+ dataSource={filteredExpenses}
|
|
|
+ rowKey="id"
|
|
|
+ pagination={{ pageSize: 10 }}
|
|
|
+ scroll={{ x: 800 }}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ // 文件标签页
|
|
|
+ const renderFilesTab = () => {
|
|
|
+ if (loadingFiles) return <Spin size="large" />;
|
|
|
+ const filesData = files?.data || [];
|
|
|
+
|
|
|
+ if (filesData.length === 0) return <Empty description="暂无文件记录" />;
|
|
|
+
|
|
|
+ const columns = [
|
|
|
+ { title: '文件名', dataIndex: 'name', key: 'name' },
|
|
|
+ { title: '类型', dataIndex: 'type', key: 'type', render: (text: string) => text || '-' },
|
|
|
+ {
|
|
|
+ title: '大小',
|
|
|
+ dataIndex: 'size',
|
|
|
+ key: 'size',
|
|
|
+ render: (size: number) => size ? `${(size / 1024).toFixed(2)} KB` : '-',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '上传时间',
|
|
|
+ dataIndex: 'uploadTime',
|
|
|
+ key: 'uploadTime',
|
|
|
+ render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm'),
|
|
|
+ },
|
|
|
+ { title: '描述', dataIndex: 'description', key: 'description', render: (text: string) => text || '-' },
|
|
|
+ ];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Table
|
|
|
+ columns={columns}
|
|
|
+ dataSource={filesData}
|
|
|
+ rowKey="id"
|
|
|
+ pagination={{ pageSize: 10 }}
|
|
|
+ scroll={{ x: 800 }}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ // 操作记录标签页
|
|
|
+ const renderLogsTab = () => {
|
|
|
+ return (
|
|
|
+ <Empty
|
|
|
+ description="操作记录功能开发中"
|
|
|
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const clientName = clientDetail?.companyName || '客户详情';
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Modal
|
|
|
+ title={`${clientName} - 详情`}
|
|
|
+ open={visible}
|
|
|
+ onCancel={onClose}
|
|
|
+ footer={null}
|
|
|
+ width={1000}
|
|
|
+ className="client-detail-modal"
|
|
|
+ destroyOnClose
|
|
|
+ >
|
|
|
+ <Tabs
|
|
|
+ activeKey={activeTab}
|
|
|
+ onChange={setActiveTab}
|
|
|
+ tabPosition="top"
|
|
|
+ items={[
|
|
|
+ {
|
|
|
+ key: 'info',
|
|
|
+ label: '客户档案',
|
|
|
+ children: renderClientInfoTab(),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'contacts',
|
|
|
+ label: '相关联系人',
|
|
|
+ children: renderContactsTab(),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'contracts',
|
|
|
+ label: '合同记录',
|
|
|
+ children: renderContractsTab(),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'expenses',
|
|
|
+ label: '费用记录',
|
|
|
+ children: renderExpensesTab(),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'files',
|
|
|
+ label: '文件记录',
|
|
|
+ children: renderFilesTab(),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'logs',
|
|
|
+ label: '操作记录',
|
|
|
+ children: renderLogsTab(),
|
|
|
+ },
|
|
|
+ ]}
|
|
|
+ />
|
|
|
+ </Modal>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default ClientDetailModal;
|