Ver código fonte

♻️ refactor(admin): 重构客户详情模态框组件结构

- 将客户详情模态框中的各标签页内容拆分为独立组件
- 新建ClientInfoTab、ContactsTab、ContractsTab、ExpensesTab、FilesTab和LogsTab组件
- 移除原文件中的renderClientInfoTab、renderContactsTab等内部函数
- 优化组件导入结构,只导入必要的API客户端
- 简化主模态框组件代码,提高可维护性和可读性
yourname 8 meses atrás
pai
commit
03e0ebfe47

+ 16 - 304
src/client/admin/components/ClientDetailModal.tsx

@@ -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,
           },
         ]}
       />

+ 78 - 0
src/client/admin/components/client-detail/ClientInfoTab.tsx

@@ -0,0 +1,78 @@
+import React from 'react';
+import { Descriptions, Spin, Empty, Tag } from 'antd';
+import { useQuery } from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import { clientClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+
+type ClientItem = InferResponseType<typeof clientClient[':id']['$get'], 200>;
+
+interface ClientInfoTabProps {
+  clientId: number;
+}
+
+const ClientInfoTab: React.FC<ClientInfoTabProps> = ({ clientId }) => {
+  // 获取客户详情
+  const { data: clientDetail, isLoading: loadingClient } = useQuery({
+    queryKey: ['client', clientId],
+    queryFn: async () => {
+      const res = await clientClient[':id'].$get({ param: { id: clientId } });
+      if (!res.ok) throw new Error('获取客户详情失败');
+      return res.json();
+    },
+    enabled: !!clientId,
+  });
+
+  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>
+  );
+};
+
+export default ClientInfoTab;

+ 53 - 0
src/client/admin/components/client-detail/ContactsTab.tsx

@@ -0,0 +1,53 @@
+import React from 'react';
+import { Table, Spin, Empty } from 'antd';
+import { useQuery } from '@tanstack/react-query';
+import { linkmanClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+
+type ContactItem = InferResponseType<typeof linkmanClient.$get, 200>;
+
+interface ContactsTabProps {
+  clientId: number;
+}
+
+const ContactsTab: React.FC<ContactsTabProps> = ({ clientId }) => {
+  // 获取相关联系人
+  const { data: contacts, isLoading: loadingContacts } = useQuery({
+    queryKey: ['contacts', clientId],
+    queryFn: async () => {
+      const res = await linkmanClient.$get({ query: { page: 1, pageSize: 100 } });
+      if (!res.ok) throw new Error('获取联系人失败');
+      return res.json();
+    },
+    select: (data) => {
+      // 过滤属于当前客户的联系人
+      return data.data.filter(contact => contact.clientId === clientId);
+    }
+  });
+
+  if (loadingContacts) return <Spin size="large" />;
+  const contactsData = contacts || [];
+  
+  if (contactsData.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={contactsData}
+      rowKey="id"
+      pagination={false}
+      scroll={{ x: 800 }}
+    />
+  );
+};
+
+export default ContactsTab;

+ 73 - 0
src/client/admin/components/client-detail/ContractsTab.tsx

@@ -0,0 +1,73 @@
+import React from 'react';
+import { Table, Spin, Empty, Tag } from 'antd';
+import { useQuery } from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import { hetongClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+
+type ContractItem = InferResponseType<typeof hetongClient.$get, 200>;
+
+interface ContractsTabProps {
+  clientId: number;
+}
+
+const ContractsTab: React.FC<ContractsTabProps> = ({ clientId }) => {
+  // 获取合同记录
+  const { data: contracts, isLoading: loadingContracts } = useQuery({
+    queryKey: ['contracts', clientId],
+    queryFn: async () => {
+      const res = await hetongClient.$get({ query: { page: 1, pageSize: 100 } });
+      if (!res.ok) throw new Error('获取合同失败');
+      return res.json();
+    },
+    select: (data) => {
+      return data.data.filter(contract => contract.clientId === clientId);
+    }
+  });
+
+  if (loadingContracts) return <Spin size="large" />;
+  const contractsData = contracts || [];
+  
+  if (contractsData.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={contractsData}
+      rowKey="id"
+      pagination={{ pageSize: 10 }}
+      scroll={{ x: 1000 }}
+    />
+  );
+};
+
+export default ContractsTab;

+ 73 - 0
src/client/admin/components/client-detail/ExpensesTab.tsx

@@ -0,0 +1,73 @@
+import React from 'react';
+import { Table, Spin, Empty, Tag } from 'antd';
+import { useQuery } from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import { expenseClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+
+type ExpenseItem = InferResponseType<typeof expenseClient.$get, 200>;
+
+interface ExpensesTabProps {
+  clientId: number;
+}
+
+const ExpensesTab: React.FC<ExpensesTabProps> = ({ clientId }) => {
+  // 获取费用记录
+  const { data: expenses, isLoading: loadingExpenses } = useQuery({
+    queryKey: ['expenses', clientId],
+    queryFn: async () => {
+      const res = await expenseClient.$get({ query: { page: 1, pageSize: 100 } });
+      if (!res.ok) throw new Error('获取费用失败');
+      return res.json();
+    },
+    select: (data) => {
+      return data.data.filter(expense => expense.clientId === clientId);
+    }
+  });
+
+  if (loadingExpenses) return <Spin size="large" />;
+  const expensesData = expenses || [];
+  
+  if (expensesData.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={expensesData}
+      rowKey="id"
+      pagination={{ pageSize: 10 }}
+      scroll={{ x: 800 }}
+    />
+  );
+};
+
+export default ExpensesTab;

+ 64 - 0
src/client/admin/components/client-detail/FilesTab.tsx

@@ -0,0 +1,64 @@
+import React from 'react';
+import { Table, Spin, Empty } from 'antd';
+import { useQuery } from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import { fileClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+
+type FileItem = InferResponseType<typeof fileClient.$get, 200>;
+
+interface FilesTabProps {
+  clientId: number;
+}
+
+const FilesTab: React.FC<FilesTabProps> = ({ clientId }) => {
+  // 获取文件记录
+  const { data: files, isLoading: loadingFiles } = useQuery({
+    queryKey: ['files', clientId],
+    queryFn: async () => {
+      // 暂时获取所有文件,后续可根据需要过滤
+      const res = await fileClient.$get({ query: { page: 1, pageSize: 100 } });
+      if (!res.ok) throw new Error('获取文件失败');
+      return res.json();
+    },
+    select: (data) => {
+      // 这里可以根据实际业务逻辑过滤
+      return data.data;
+    }
+  });
+
+  if (loadingFiles) return <Spin size="large" />;
+  const filesData = files || [];
+  
+  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 }}
+    />
+  );
+};
+
+export default FilesTab;

+ 17 - 0
src/client/admin/components/client-detail/LogsTab.tsx

@@ -0,0 +1,17 @@
+import React from 'react';
+import { Empty } from 'antd';
+
+interface LogsTabProps {
+  clientId: number;
+}
+
+const LogsTab: React.FC<LogsTabProps> = ({ clientId }) => {
+  return (
+    <Empty 
+      description="操作记录功能开发中" 
+      image={Empty.PRESENTED_IMAGE_SIMPLE}
+    />
+  );
+};
+
+export default LogsTab;