|
|
@@ -1,18 +1,16 @@
|
|
|
import React, { useState } from 'react';
|
|
|
-import { Modal, Tabs, Descriptions, Table, Spin, Empty, Tag } from 'antd';
|
|
|
+import { Modal, Tabs } from 'antd';
|
|
|
import { App } from 'antd';
|
|
|
+import ClientInfoTab from './client-detail/ClientInfoTab';
|
|
|
+import ContactsTab from './client-detail/ContactsTab';
|
|
|
+import ContractsTab from './client-detail/ContractsTab';
|
|
|
+import ExpensesTab from './client-detail/ExpensesTab';
|
|
|
+import FilesTab from './client-detail/FilesTab';
|
|
|
+import LogsTab from './client-detail/LogsTab';
|
|
|
import { useQuery } from '@tanstack/react-query';
|
|
|
-import dayjs from 'dayjs';
|
|
|
-import { clientClient, linkmanClient, hetongClient, expenseClient, fileClient } from '@/client/api';
|
|
|
+import { clientClient } 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 {
|
|
|
@@ -25,8 +23,8 @@ const ClientDetailModal: React.FC<ClientDetailModalProps> = ({ clientId, visible
|
|
|
const { message } = App.useApp();
|
|
|
const [activeTab, setActiveTab] = useState('info');
|
|
|
|
|
|
- // 获取客户详情
|
|
|
- const { data: clientDetail, isLoading: loadingClient } = useQuery({
|
|
|
+ // 获取客户基本信息用于标题
|
|
|
+ const { data: clientDetail } = useQuery({
|
|
|
queryKey: ['client', clientId],
|
|
|
queryFn: async () => {
|
|
|
if (!clientId) return null;
|
|
|
@@ -37,292 +35,6 @@ const ClientDetailModal: React.FC<ClientDetailModalProps> = ({ clientId, visible
|
|
|
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 (
|
|
|
@@ -343,32 +55,32 @@ const ClientDetailModal: React.FC<ClientDetailModalProps> = ({ clientId, visible
|
|
|
{
|
|
|
key: 'info',
|
|
|
label: '客户档案',
|
|
|
- children: renderClientInfoTab(),
|
|
|
+ children: clientId ? <ClientInfoTab clientId={clientId} /> : null,
|
|
|
},
|
|
|
{
|
|
|
key: 'contacts',
|
|
|
label: '相关联系人',
|
|
|
- children: renderContactsTab(),
|
|
|
+ children: clientId ? <ContactsTab clientId={clientId} /> : null,
|
|
|
},
|
|
|
{
|
|
|
key: 'contracts',
|
|
|
label: '合同记录',
|
|
|
- children: renderContractsTab(),
|
|
|
+ children: clientId ? <ContractsTab clientId={clientId} /> : null,
|
|
|
},
|
|
|
{
|
|
|
key: 'expenses',
|
|
|
label: '费用记录',
|
|
|
- children: renderExpensesTab(),
|
|
|
+ children: clientId ? <ExpensesTab clientId={clientId} /> : null,
|
|
|
},
|
|
|
{
|
|
|
key: 'files',
|
|
|
label: '文件记录',
|
|
|
- children: renderFilesTab(),
|
|
|
+ children: clientId ? <FilesTab clientId={clientId} /> : null,
|
|
|
},
|
|
|
{
|
|
|
key: 'logs',
|
|
|
label: '操作记录',
|
|
|
- children: renderLogsTab(),
|
|
|
+ children: clientId ? <LogsTab clientId={clientId} /> : null,
|
|
|
},
|
|
|
]}
|
|
|
/>
|