yourname 5 달 전
부모
커밋
084fb76578

+ 12 - 0
package.json

@@ -3,17 +3,29 @@
     "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js"
   },
   "dependencies": {
+    "@ant-design/icons": "^6.0.0",
     "@hono/zod-openapi": "^0.19.8",
+    "antd": "^5.26.1",
+    "axios": "^1.10.0",
+    "debug": "^4.4.1",
     "jsonwebtoken": "^9.0.2",
     "mysql2": "^3.14.1",
+    "react": "^19.1.0",
+    "react-dom": "^19.1.0",
+    "react-router-dom": "^7.6.2",
     "reflect-metadata": "^0.2.2",
     "typeorm": "^0.3.25",
     "zod": "^3.25.67"
   },
   "devDependencies": {
+    "@types/antd": "^0.12.32",
+    "@types/axios": "^0.9.36",
     "@types/debug": "^4.1.12",
     "@types/jsonwebtoken": "^9.0.10",
     "@types/node": "^24.0.3",
+    "@types/react": "^19.1.8",
+    "@types/react-dom": "^19.1.6",
+    "@types/react-router-dom": "^5.3.3",
     "ts-node": "^10.9.2",
     "tsconfig-paths": "^4.2.0",
     "typescript": "^5.8.3"

+ 27 - 0
src/client/App.tsx

@@ -0,0 +1,27 @@
+import React from 'react';
+import { Routes, Route } from 'react-router-dom';
+import { logger } from './utils/logger';
+import MainLayout from './components/layout/MainLayout';
+import Login from './pages/login';
+import Dashboard from './pages/dashboard';
+import Customers from './pages/customers';
+import Contacts from './pages/contacts';
+import Opportunities from './pages/opportunities';
+
+const App: React.FC = () => {
+  logger.info('Rendering App component');
+  
+  return (
+    <Routes>
+      <Route path="/login" element={<Login />} />
+      <Route path="/" element={<MainLayout />}>
+        <Route index element={<Dashboard />} />
+        <Route path="customers" element={<Customers />} />
+        <Route path="contacts" element={<Contacts />} />
+        <Route path="opportunities" element={<Opportunities />} />
+      </Route>
+    </Routes>
+  );
+};
+
+export default App;

+ 83 - 0
src/client/api/index.ts

@@ -0,0 +1,83 @@
+import { hc } from 'hono/client';
+import axios from 'axios';
+import { logger } from '../utils/logger';
+import { CampaignsRoutes, ContactsRoutes, CustomersRoutes, DepartmentsRoutes, OpportunitiesRoutes, RoleRoutes, TicketsRoutes, UserRoutes } from '@/server/api';
+
+
+
+// 请求拦截器 - 添加认证token
+axios.interceptors.request.use(
+  (config) => {
+    const token = localStorage.getItem('token');
+    if (token) {
+      config.headers = {
+        ...config.headers,
+        Authorization : `Bearer ${token}`
+      }
+    }
+    logger.api(`Request: ${config.method?.toUpperCase()} ${config.url}`);
+    return config;
+  },
+  (error) => {
+    logger.error('Request error:', error);
+    return Promise.reject(error);
+  }
+);
+
+// 响应拦截器 - 处理错误
+axios.interceptors.response.use(
+  (response) => {
+    logger.api(`Response: ${response.status} ${response.config.url}`);
+    return response;
+  },
+  (error) => {
+    logger.error('Response error:', error.response?.status, error.response?.data);
+    
+    // 处理401未授权错误
+    if (error.response?.status === 401) {
+      localStorage.removeItem('token');
+      window.location.href = '/login';
+    }
+    
+    return Promise.reject(error);
+  }
+);
+
+// 适配axios到Hono客户端
+const axiosFetch: typeof fetch = async (input, init) => {
+  try {
+    const url = input.toString();
+    const method = init?.method || 'GET';
+    const headers = init?.headers as Record<string, string> || {};
+    const data = init?.body ? JSON.parse(init.body.toString()) : undefined;
+
+    const response = await axios({
+      url,
+      method,
+      headers,
+      data,
+      params: method === 'GET' ? data : undefined,
+    });
+
+    return new Response(JSON.stringify(response.data), {
+      status: response.status,
+      headers: response.headers as HeadersInit,
+    });
+  } catch (error: any) {
+    return new Response(JSON.stringify(error.response?.data || { message: error.message }), {
+      status: error.response?.status || 500,
+      headers: error.response?.headers as HeadersInit || {},
+    });
+  }
+};
+
+// 导出各模块客户端
+// export const authClient = hc<ApiRoutes>('/', { fetch: axiosFetch });
+export const customerClient = hc<CustomersRoutes>('/', { fetch: axiosFetch }).api.v1.customers;
+export const contactClient = hc<ContactsRoutes>('/', { fetch: axiosFetch }).api.v1.contacts;
+export const opportunityClient = hc<OpportunitiesRoutes>('/', { fetch: axiosFetch }).api.v1.opportunities;
+export const ticketsClient = hc<TicketsRoutes>('/', { fetch: axiosFetch }).api.v1.tickets;
+export const usersClient = hc<UserRoutes>('/', { fetch: axiosFetch }).api.v1.users;
+export const departmentsClient = hc<DepartmentsRoutes>('/', { fetch: axiosFetch }).api.v1.departments;
+export const rolesClient = hc<RoleRoutes>('/', { fetch: axiosFetch }).api.v1.roles;
+export const campaignsClient = hc<CampaignsRoutes>('/', { fetch: axiosFetch }).api.v1.campaigns;

+ 27 - 0
src/client/api/types.ts

@@ -0,0 +1,27 @@
+import type { InferRequestType, InferResponseType } from 'hono/client';
+import type { AuthRoutes } from '@/server/api/auth';
+import type { CustomerRoutes } from '@/server/api/customers';
+import type { ContactRoutes } from '@/server/api/contacts';
+import type { OpportunityRoutes } from '@/server/api/opportunities';
+
+// 认证相关类型
+export type LoginRequest = InferRequestType<typeof authClient.login.$post>['json'];
+export type LoginResponse = InferResponseType<typeof authClient.login.$post, 200>;
+
+// 客户相关类型
+export type Customer = InferResponseType<typeof customerClient[':id'].$get, 200>['data'];
+export type CustomerListResponse = InferResponseType<typeof customerClient.$get, 200>;
+export type CreateCustomerRequest = InferRequestType<typeof customerClient.$post>['json'];
+export type UpdateCustomerRequest = InferRequestType<typeof customerClient[':id'].$put>['json'];
+
+// 联系人相关类型
+export type Contact = InferResponseType<typeof contactClient[':id'].$get, 200>['data'];
+export type ContactListResponse = InferResponseType<typeof contactClient.$get, 200>;
+export type CreateContactRequest = InferRequestType<typeof contactClient.$post>['json'];
+export type UpdateContactRequest = InferRequestType<typeof contactClient[':id'].$put>['json'];
+
+// 销售机会相关类型
+export type Opportunity = InferResponseType<typeof opportunityClient[':id'].$get, 200>['data'];
+export type OpportunityListResponse = InferResponseType<typeof opportunityClient.$get, 200>;
+export type CreateOpportunityRequest = InferRequestType<typeof opportunityClient.$post>['json'];
+export type UpdateOpportunityRequest = InferRequestType<typeof opportunityClient[':id'].$put>['json'];

+ 87 - 0
src/client/components/Form.tsx

@@ -0,0 +1,87 @@
+import React from 'react';
+
+interface FormField {
+  name: string;
+  label: string;
+  type: 'text' | 'number' | 'email' | 'textarea' | 'select';
+  required?: boolean;
+  options?: { label: string; value: string }[];
+  placeholder?: string;
+}
+
+interface FormProps {
+  fields: FormField[];
+  initialValues?: Record<string, any>;
+  onChange?: (values: Record<string, any>) => void;
+}
+
+const Form: React.FC<FormProps> = ({ fields, initialValues = {}, onChange }) => {
+  const [values, setValues] = React.useState<Record<string, any>>(initialValues);
+
+  React.useEffect(() => {
+    setValues(initialValues);
+  }, [initialValues]);
+
+  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
+    const { name, value } = e.target;
+    const newValues = { ...values, [name]: value };
+    setValues(newValues);
+    if (onChange) {
+      onChange(newValues);
+    }
+  };
+
+  return (
+    <div className="space-y-4">
+      {fields.map((field) => (
+        <div key={field.name} className="space-y-2">
+          <label htmlFor={field.name} className="block text-sm font-medium text-gray-700">
+            {field.label} {field.required ? '*' : ''}
+          </label>
+          
+          {field.type === 'textarea' ? (
+            <textarea
+              id={field.name}
+              name={field.name}
+              required={field.required}
+              value={values[field.name] || ''}
+              onChange={handleChange}
+              className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
+              rows={4}
+              placeholder={field.placeholder}
+            />
+          ) : field.type === 'select' ? (
+            <select
+              id={field.name}
+              name={field.name}
+              required={field.required}
+              value={values[field.name] || ''}
+              onChange={handleChange}
+              className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
+            >
+              <option value="">请选择</option>
+              {field.options?.map((option) => (
+                <option key={option.value} value={option.value}>
+                  {option.label}
+                </option>
+              ))}
+            </select>
+          ) : (
+            <input
+              id={field.name}
+              name={field.name}
+              type={field.type}
+              required={field.required}
+              value={values[field.name] || ''}
+              onChange={handleChange}
+              className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
+              placeholder={field.placeholder}
+            />
+          )}
+        </div>
+      ))}
+    </div>
+  );
+};
+
+export default Form;

+ 114 - 0
src/client/components/Modal.tsx

@@ -0,0 +1,114 @@
+import React, { useState, useEffect } from 'react';
+import { createPortal } from 'react-dom';
+import { logger } from '../utils/logger';
+
+interface ModalProps {
+  title: string;
+  open: boolean;
+  onClose: () => void;
+  onSubmit: (data: any) => Promise<void>;
+  children: React.ReactNode;
+  submitText?: string;
+  cancelText?: string;
+}
+
+const Modal: React.FC<ModalProps> = ({
+  title,
+  open,
+  onClose,
+  onSubmit,
+  children,
+  submitText = '保存',
+  cancelText = '取消'
+}) => {
+  const [isSubmitting, setIsSubmitting] = useState(false);
+  const [error, setError] = useState('');
+
+  useEffect(() => {
+    if (open) {
+      document.body.style.overflow = 'hidden';
+      logger.ui(`Modal opened: ${title}`);
+    } else {
+      document.body.style.overflow = '';
+    }
+
+    return () => {
+      document.body.style.overflow = '';
+    };
+  }, [open, title]);
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setIsSubmitting(true);
+    setError('');
+
+    try {
+      // Find the form element and collect data
+      const form = e.target as HTMLFormElement;
+      const formData = new FormData(form);
+      const data: Record<string, any> = {};
+
+      formData.forEach((value, key) => {
+        data[key] = value;
+      });
+
+      await onSubmit(data);
+    } catch (err) {
+      logger.error('Modal submit error:', err);
+      setError(err instanceof Error ? err.message : '提交时发生错误');
+    } finally {
+      setIsSubmitting(false);
+    }
+  };
+
+  if (!open) return null;
+
+  return createPortal(
+    <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
+      <div className="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[90vh] overflow-y-auto">
+        <div className="p-6 border-b">
+          <div className="flex justify-between items-center">
+            <h2 className="text-xl font-bold">{title}</h2>
+            <button
+              onClick={onClose}
+              className="text-gray-500 hover:text-gray-700"
+            >
+              ✕
+            </button>
+          </div>
+        </div>
+
+        <form onSubmit={handleSubmit} className="p-6">
+          {error && (
+            <div className="mb-4 p-3 text-sm text-red-800 bg-red-100 rounded-md">
+              {error}
+            </div>
+          )}
+          
+          {children}
+          
+          <div className="mt-6 flex justify-end space-x-3">
+            <button
+              type="button"
+              onClick={onClose}
+              className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
+              disabled={isSubmitting}
+            >
+              {cancelText}
+            </button>
+            <button
+              type="submit"
+              className="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 disabled:opacity-50"
+              disabled={isSubmitting}
+            >
+              {isSubmitting ? '提交中...' : submitText}
+            </button>
+          </div>
+        </form>
+      </div>
+    </div>,
+    document.body
+  );
+};
+
+export default Modal;

+ 58 - 0
src/client/components/Table.tsx

@@ -0,0 +1,58 @@
+import React from 'react';
+
+interface Column<T> {
+  key: keyof T | string;
+  title: string;
+  render?: (value: any, record: T) => React.ReactNode;
+}
+
+interface TableProps<T> {
+  columns: Column<T>[];
+  data: T[];
+  rowKey: keyof T | string;
+}
+
+const Table = <T,>({ columns, data, rowKey }: TableProps<T>): React.ReactElement => {
+  if (data.length === 0) {
+    return (
+      <div className="flex justify-center items-center h-32 text-gray-500">
+        暂无数据
+      </div>
+    );
+  }
+
+  return (
+    <div className="overflow-x-auto">
+      <table className="min-w-full divide-y divide-gray-200">
+        <thead className="bg-gray-50">
+          <tr>
+            {columns.map((column) => (
+              <th
+                key={String(column.key)}
+                className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
+              >
+                {column.title}
+              </th>
+            ))}
+          </tr>
+        </thead>
+        <tbody className="bg-white divide-y divide-gray-200">
+          {data.map((record) => (
+            <tr key={String(record[rowKey as keyof T])}>
+              {columns.map((column) => {
+                const value = record[column.key as keyof T];
+                return (
+                  <td key={String(column.key)} className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
+                    {column.render ? column.render(value, record) : String(value)}
+                  </td>
+                );
+              })}
+            </tr>
+          ))}
+        </tbody>
+      </table>
+    </div>
+  );
+};
+
+export default Table;

+ 104 - 0
src/client/components/layout/MainLayout.tsx

@@ -0,0 +1,104 @@
+import React, { useState } from 'react';
+import { Outlet, Link, useNavigate } from 'react-router-dom';
+import { logger } from '../../utils/logger';
+
+const MainLayout: React.FC = () => {
+  const [isSidebarOpen, setIsSidebarOpen] = useState(true);
+  const navigate = useNavigate();
+  
+  const handleLogout = () => {
+    logger.auth('User logged out');
+    localStorage.removeItem('token');
+    navigate('/login');
+  };
+  
+  return (
+    <div className="flex h-screen bg-gray-50">
+      {/* 侧边栏 */}
+      <aside 
+        className={`bg-white shadow-md transition-all duration-300 ${
+          isSidebarOpen ? 'w-64' : 'w-0 md:w-20'
+        } overflow-hidden`}
+      >
+        <div className="p-4 border-b">
+          <h1 className={`text-xl font-bold ${!isSidebarOpen && 'md:hidden'}`}>
+            CRM系统
+          </h1>
+        </div>
+        
+        <nav className="p-4">
+          <ul className="space-y-2">
+            <li>
+              <Link 
+                to="/" 
+                className="flex items-center p-2 rounded hover:bg-gray-100"
+              >
+                <span className="mr-2">📊</span>
+                {isSidebarOpen && <span>仪表盘</span>}
+              </Link>
+            </li>
+            <li>
+              <Link 
+                to="/customers" 
+                className="flex items-center p-2 rounded hover:bg-gray-100"
+              >
+                <span className="mr-2">🏢</span>
+                {isSidebarOpen && <span>客户管理</span>}
+              </Link>
+            </li>
+            <li>
+              <Link 
+                to="/contacts" 
+                className="flex items-center p-2 rounded hover:bg-gray-100"
+              >
+                <span className="mr-2">👥</span>
+                {isSidebarOpen && <span>联系人</span>}
+              </Link>
+            </li>
+            <li>
+              <Link 
+                to="/opportunities" 
+                className="flex items-center p-2 rounded hover:bg-gray-100"
+              >
+                <span className="mr-2">💰</span>
+                {isSidebarOpen && <span>销售机会</span>}
+              </Link>
+            </li>
+          </ul>
+        </nav>
+      </aside>
+      
+      {/* 主内容区 */}
+      <div className="flex-1 flex flex-col overflow-hidden">
+        {/* 顶部导航 */}
+        <header className="bg-white shadow-sm p-4 flex justify-between items-center">
+          <button 
+            onClick={() => setIsSidebarOpen(!isSidebarOpen)}
+            className="p-2 rounded hover:bg-gray-100"
+          >
+            {isSidebarOpen ? '◀' : '▶'}
+          </button>
+          
+          <div className="flex items-center space-x-4">
+            <span className="text-sm text-gray-600">
+              {localStorage.getItem('username') || '未登录'}
+            </span>
+            <button 
+              onClick={handleLogout}
+              className="text-sm text-red-600 hover:underline"
+            >
+              退出登录
+            </button>
+          </div>
+        </header>
+        
+        {/* 页面内容 */}
+        <main className="flex-1 overflow-y-auto p-4">
+          <Outlet />
+        </main>
+      </div>
+    </div>
+  );
+};
+
+export default MainLayout;

+ 7 - 6
src/client/index.tsx

@@ -1,16 +1,17 @@
 import React from 'react';
 import ReactDOM from 'react-dom/client';
-import { BrowserRouter } from 'react-router-dom';
-import App from './App';
+import { AppProvider } from './store';
+import AppRouter from './router';
 import { logger } from './utils/logger';
 
 // 初始化日志
 logger.info('CRM前端应用启动');
 
-ReactDOM.createRoot(document.getElementById('root')!).render(
+const root = ReactDOM.createRoot(document.getElementById('root')!);
+root.render(
   <React.StrictMode>
-    <BrowserRouter>
-      <App />
-    </BrowserRouter>
+    <AppProvider>
+      <AppRouter />
+    </AppProvider>
   </React.StrictMode>
 );

+ 224 - 0
src/client/pages/contacts.tsx

@@ -0,0 +1,224 @@
+import React, { useEffect, useState } from 'react';
+import { contactClient } from '../api';
+import { Contact, ContactListResponse } from '../api/types';
+import { logger } from '../utils/logger';
+import Table from '../components/Table';
+import Modal from '../components/Modal';
+import Form from '../components/Form';
+
+const Contacts: React.FC = () => {
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState('');
+  const [contacts, setContacts] = useState<Contact[]>([]);
+  const [pagination, setPagination] = useState({
+    total: 0,
+    current: 1,
+    pageSize: 10
+  });
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [currentContact, setCurrentContact] = useState<Contact | null>(null);
+  
+  const fetchContacts = async () => {
+    try {
+      setLoading(true);
+      const response = await contactClient.$get({
+        query: { 
+          page: pagination.current, 
+          pageSize: pagination.pageSize 
+        }
+      });
+      
+      if (!response.ok) throw new Error('获取联系人列表失败');
+      
+      const data = (await response.json()) as ContactListResponse;
+      setContacts(data.data);
+      setPagination(data.pagination);
+      logger.ui('Fetched contacts successfully');
+    } catch (err) {
+      logger.error('Error fetching contacts:', err);
+      setError(err instanceof Error ? err.message : '获取联系人数据时发生错误');
+    } finally {
+      setLoading(false);
+    }
+  };
+  
+  useEffect(() => {
+    fetchContacts();
+  }, [pagination.current, pagination.pageSize]);
+  
+  const handlePageChange = (page: number) => {
+    setPagination(prev => ({ ...prev, current: page }));
+  };
+  
+  const handleAdd = () => {
+    setCurrentContact(null);
+    setIsModalOpen(true);
+  };
+  
+  const handleEdit = (contact: Contact) => {
+    setCurrentContact(contact);
+    setIsModalOpen(true);
+  };
+  
+  const handleDelete = async (id: number) => {
+    if (!confirm('确定要删除这个联系人吗?')) return;
+    
+    try {
+      // @ts-ignore
+      const response = await contactClient[':id'].$delete({
+        param: { id }
+      });
+      
+      if (!response.ok) throw new Error('删除联系人失败');
+      
+      logger.ui(`Deleted contact with id: ${id}`);
+      fetchContacts();
+    } catch (err) {
+      logger.error('Error deleting contact:', err);
+      setError(err instanceof Error ? err.message : '删除联系人时发生错误');
+    }
+  };
+  
+  const handleSave = async (formData: any) => {
+    try {
+      if (currentContact) {
+        // 更新现有联系人
+        // @ts-ignore
+        const response = await contactClient[':id'].$put({
+          param: { id: currentContact.id },
+          json: formData
+        });
+        
+        if (!response.ok) throw new Error('更新联系人失败');
+        logger.ui(`Updated contact with id: ${currentContact.id}`);
+      } else {
+        // 创建新联系人
+        const response = await contactClient.$post({
+          json: formData
+        });
+        
+        if (!response.ok) throw new Error('创建联系人失败');
+        logger.ui('Created new contact');
+      }
+      
+      setIsModalOpen(false);
+      fetchContacts();
+    } catch (err) {
+      logger.error('Error saving contact:', err);
+      setError(err instanceof Error ? err.message : '保存联系人时发生错误');
+    }
+  };
+  
+  const columns = [
+    { key: 'name', title: '姓名' },
+    { key: 'customerName', title: '所属客户' },
+    { key: 'position', title: '职位' },
+    { key: 'phone', title: '电话' },
+    { key: 'email', title: '邮箱' },
+    { key: 'createdAt', title: '创建时间' },
+    { 
+      key: 'actions', 
+      title: '操作',
+      render: (_: any, record: Contact) => (
+        <div className="flex space-x-2">
+          <button 
+            onClick={() => handleEdit(record)}
+            className="text-blue-600 hover:underline"
+          >
+            编辑
+          </button>
+          <button 
+            onClick={() => handleDelete(record.id)}
+            className="text-red-600 hover:underline"
+          >
+            删除
+          </button>
+        </div>
+      )
+    }
+  ];
+  
+  const formFields = [
+    { name: 'name', label: '姓名', type: 'text' as const, required: true },
+    { name: 'customerId', label: '所属客户', type: 'select' as const, required: true,
+      options: [
+        { label: '选择客户', value: '' },
+        // 实际项目中这里应该从API获取客户列表
+      ]
+    },
+    { name: 'position', label: '职位', type: 'text' as const },
+    { name: 'phone', label: '电话', type: 'text' as const },
+    { name: 'email', label: '邮箱', type: 'email' as const },
+    { name: 'notes', label: '备注', type: 'textarea' as const }
+  ];
+  
+  return (
+    <div className="space-y-6">
+      <div className="flex justify-between items-center">
+        <h1 className="text-2xl font-bold">联系人管理</h1>
+        <button 
+          onClick={handleAdd}
+          className="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"
+        >
+          添加联系人
+        </button>
+      </div>
+      
+      {error && (
+        <div className="p-4 text-red-600 bg-red-100 rounded-md">{error}</div>
+      )}
+      
+      <div className="bg-white rounded-lg shadow-md overflow-hidden">
+        <div className="p-4">
+          {loading ? (
+            <div className="flex justify-center items-center h-64">加载中...</div>
+          ) : (
+            <Table 
+              columns={columns} 
+              data={contacts} 
+              rowKey="id"
+            />
+          )}
+        </div>
+        
+        {/* 分页控件 */}
+        <div className="px-4 py-3 bg-gray-50 border-t flex items-center justify-between">
+          <div className="text-sm text-gray-700">
+            共 {pagination.total} 条记录,当前第 {pagination.current} 页
+          </div>
+          <div className="flex space-x-2">
+            <button 
+              onClick={() => handlePageChange(pagination.current - 1)}
+              disabled={pagination.current === 1}
+              className="px-3 py-1 border rounded disabled:opacity-50"
+            >
+              上一页
+            </button>
+            <button 
+              onClick={() => handlePageChange(pagination.current + 1)}
+              disabled={pagination.current * pagination.pageSize >= pagination.total}
+              className="px-3 py-1 border rounded disabled:opacity-50"
+            >
+              下一页
+            </button>
+          </div>
+        </div>
+      </div>
+      
+      {/* 添加/编辑联系人模态框 */}
+      <Modal
+        title={currentContact ? '编辑联系人' : '添加联系人'}
+        open={isModalOpen}
+        onClose={() => setIsModalOpen(false)}
+        onSubmit={handleSave}
+      >
+        <Form 
+          fields={formFields}
+          initialValues={currentContact || {}}
+        />
+      </Modal>
+    </div>
+  );
+};
+
+export default Contacts;

+ 219 - 0
src/client/pages/customers.tsx

@@ -0,0 +1,219 @@
+import React, { useEffect, useState } from 'react';
+import { customerClient } from '../api';
+import { Customer, CustomerListResponse } from '../api/types';
+import { logger } from '../utils/logger';
+import Table from '../components/Table';
+import Modal from '../components/Modal';
+import Form from '../components/Form';
+
+const Customers: React.FC = () => {
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState('');
+  const [customers, setCustomers] = useState<Customer[]>([]);
+  const [pagination, setPagination] = useState({
+    total: 0,
+    current: 1,
+    pageSize: 10
+  });
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [currentCustomer, setCurrentCustomer] = useState<Customer | null>(null);
+  
+  const fetchCustomers = async () => {
+    try {
+      setLoading(true);
+      const response = await customerClient.$get({
+        query: { 
+          page: pagination.current, 
+          pageSize: pagination.pageSize 
+        }
+      });
+      
+      if (!response.ok) throw new Error('获取客户列表失败');
+      
+      const data = (await response.json()) as CustomerListResponse;
+      setCustomers(data.data);
+      setPagination(data.pagination);
+      logger.ui('Fetched customers successfully');
+    } catch (err) {
+      logger.error('Error fetching customers:', err);
+      setError(err instanceof Error ? err.message : '获取客户数据时发生错误');
+    } finally {
+      setLoading(false);
+    }
+  };
+  
+  useEffect(() => {
+    fetchCustomers();
+  }, [pagination.current, pagination.pageSize]);
+  
+  const handlePageChange = (page: number) => {
+    setPagination(prev => ({ ...prev, current: page }));
+  };
+  
+  const handleAdd = () => {
+    setCurrentCustomer(null);
+    setIsModalOpen(true);
+  };
+  
+  const handleEdit = (customer: Customer) => {
+    setCurrentCustomer(customer);
+    setIsModalOpen(true);
+  };
+  
+  const handleDelete = async (id: number) => {
+    if (!confirm('确定要删除这个客户吗?')) return;
+    
+    try {
+      // @ts-ignore
+      const response = await customerClient[':id'].$delete({
+        param: { id }
+      });
+      
+      if (!response.ok) throw new Error('删除客户失败');
+      
+      logger.ui(`Deleted customer with id: ${id}`);
+      fetchCustomers();
+    } catch (err) {
+      logger.error('Error deleting customer:', err);
+      setError(err instanceof Error ? err.message : '删除客户时发生错误');
+    }
+  };
+  
+  const handleSave = async (formData: any) => {
+    try {
+      if (currentCustomer) {
+        // 更新现有客户
+        // @ts-ignore
+        const response = await customerClient[':id'].$put({
+          param: { id: currentCustomer.id },
+          json: formData
+        });
+        
+        if (!response.ok) throw new Error('更新客户失败');
+        logger.ui(`Updated customer with id: ${currentCustomer.id}`);
+      } else {
+        // 创建新客户
+        const response = await customerClient.$post({
+          json: formData
+        });
+        
+        if (!response.ok) throw new Error('创建客户失败');
+        logger.ui('Created new customer');
+      }
+      
+      setIsModalOpen(false);
+      fetchCustomers();
+    } catch (err) {
+      logger.error('Error saving customer:', err);
+      setError(err instanceof Error ? err.message : '保存客户时发生错误');
+    }
+  };
+  
+  const columns = [
+    { key: 'name', title: '客户名称' },
+    { key: 'industry', title: '行业' },
+    { key: 'contactPerson', title: '联系人' },
+    { key: 'phone', title: '电话' },
+    { key: 'email', title: '邮箱' },
+    { key: 'createdAt', title: '创建时间' },
+    { 
+      key: 'actions', 
+      title: '操作',
+      render: (_: any, record: Customer) => (
+        <div className="flex space-x-2">
+          <button 
+            onClick={() => handleEdit(record)}
+            className="text-blue-600 hover:underline"
+          >
+            编辑
+          </button>
+          <button 
+            onClick={() => handleDelete(record.id)}
+            className="text-red-600 hover:underline"
+          >
+            删除
+          </button>
+        </div>
+      )
+    }
+  ];
+  
+  const formFields = [
+    { name: 'name', label: '客户名称', type: 'text' as const, required: true },
+    { name: 'industry', label: '行业', type: 'text' as const },
+    { name: 'contactPerson', label: '联系人', type: 'text' as const },
+    { name: 'phone', label: '电话', type: 'text' as const },
+    { name: 'email', label: '邮箱', type: 'email' as const },
+    { name: 'address', label: '地址', type: 'textarea' as const }
+  ];
+  
+  return (
+    <div className="space-y-6">
+      <div className="flex justify-between items-center">
+        <h1 className="text-2xl font-bold">客户管理</h1>
+        <button 
+          onClick={handleAdd}
+          className="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"
+        >
+          添加客户
+        </button>
+      </div>
+      
+      {error && (
+        <div className="p-4 text-red-600 bg-red-100 rounded-md">{error}</div>
+      )}
+      
+      <div className="bg-white rounded-lg shadow-md overflow-hidden">
+        <div className="p-4">
+          {loading ? (
+            <div className="flex justify-center items-center h-64">加载中...</div>
+          ) : (
+            <Table 
+              columns={columns} 
+              data={customers} 
+              rowKey="id"
+            />
+          )}
+        </div>
+        
+        {/* 分页控件 */}
+        <div className="px-4 py-3 bg-gray-50 border-t flex items-center justify-between">
+          <div className="text-sm text-gray-700">
+            共 {pagination.total} 条记录,当前第 {pagination.current} 页
+          </div>
+          <div className="flex space-x-2">
+            <button 
+              onClick={() => handlePageChange(pagination.current - 1)}
+              disabled={pagination.current === 1}
+              className="px-3 py-1 border rounded disabled:opacity-50"
+            >
+              上一页
+            </button>
+            <button 
+              onClick={() => handlePageChange(pagination.current + 1)}
+              disabled={pagination.current * pagination.pageSize >= pagination.total}
+              className="px-3 py-1 border rounded disabled:opacity-50"
+            >
+              下一页
+            </button>
+          </div>
+        </div>
+      </div>
+      
+      {/* 添加/编辑客户模态框 */}
+      <Modal
+        title={currentCustomer ? '编辑客户' : '添加客户'}
+        open={isModalOpen}
+        onClose={() => setIsModalOpen(false)}
+        onSubmit={handleSave}
+      >
+        <Form 
+          fields={formFields}
+          initialValues={currentCustomer || {}}
+        />
+      </Modal>
+    </div>
+  );
+};
+
+export default Customers;

+ 120 - 0
src/client/pages/dashboard.tsx

@@ -0,0 +1,120 @@
+import React, { useEffect, useState } from 'react';
+import { customerClient, opportunityClient } from '../api';
+import { CustomerListResponse, OpportunityListResponse } from '../api/types';
+import { logger } from '../utils/logger';
+import Table from '../components/Table';
+
+const Dashboard: React.FC = () => {
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState('');
+  const [customerCount, setCustomerCount] = useState(0);
+  const [opportunityCount, setOpportunityCount] = useState(0);
+  const [recentOpportunities, setRecentOpportunities] = useState<any[]>([]);
+  
+  useEffect(() => {
+    const fetchDashboardData = async () => {
+      try {
+        setLoading(true);
+        
+        // 获取客户总数
+        const customerResponse = await customerClient.$get({
+          query: { page: 1, pageSize: 1 }
+        });
+        if (!customerResponse.ok) throw new Error('获取客户数据失败');
+        const customerData = (await customerResponse.json()) as CustomerListResponse;
+        setCustomerCount(customerData.pagination.total);
+        
+        // 获取销售机会数据
+        const opportunityResponse = await opportunityClient.$get({
+          query: { page: 1, pageSize: 5 }
+        });
+        if (!opportunityResponse.ok) throw new Error('获取销售机会数据失败');
+        const opportunityData = (await opportunityResponse.json()) as OpportunityListResponse;
+        setOpportunityCount(opportunityData.pagination.total);
+        setRecentOpportunities(opportunityData.data);
+        
+        logger.ui('Dashboard data loaded successfully');
+      } catch (err) {
+        logger.error('Error loading dashboard data:', err);
+        setError(err instanceof Error ? err.message : '加载数据时发生错误');
+      } finally {
+        setLoading(false);
+      }
+    };
+    
+    fetchDashboardData();
+  }, []);
+  
+  if (loading) {
+    return <div className="flex justify-center items-center h-64">加载中...</div>;
+  }
+  
+  if (error) {
+    return <div className="p-4 text-red-600 bg-red-100 rounded-md">{error}</div>;
+  }
+  
+  // 销售机会表格列定义
+  const opportunityColumns = [
+    { key: 'name', title: '机会名称' },
+    { key: 'customerName', title: '客户名称' },
+    { key: 'amount', title: '金额', render: (value: number) => `¥${value.toLocaleString()}` },
+    { key: 'stage', title: '阶段' },
+    { key: 'expectedCloseDate', title: '预计成交日期' }
+  ];
+  
+  return (
+    <div className="space-y-6">
+      <div>
+        <h1 className="text-2xl font-bold">仪表盘</h1>
+        <p className="text-gray-600">欢迎使用CRM系统,以下是您的业务概览</p>
+      </div>
+      
+      {/* 关键指标卡片 */}
+      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+        <div className="p-6 bg-white rounded-lg shadow-md">
+          <div className="text-sm font-medium text-gray-500">总客户数</div>
+          <div className="mt-1 text-3xl font-semibold text-gray-900">{customerCount}</div>
+          <div className="mt-4">
+            <a href="/customers" className="text-indigo-600 hover:text-indigo-900">
+              查看所有客户 →
+            </a>
+          </div>
+        </div>
+        
+        <div className="p-6 bg-white rounded-lg shadow-md">
+          <div className="text-sm font-medium text-gray-500">销售机会总数</div>
+          <div className="mt-1 text-3xl font-semibold text-gray-900">{opportunityCount}</div>
+          <div className="mt-4">
+            <a href="/opportunities" className="text-indigo-600 hover:text-indigo-900">
+              查看所有机会 →
+            </a>
+          </div>
+        </div>
+        
+        <div className="p-6 bg-white rounded-lg shadow-md">
+          <div className="text-sm font-medium text-gray-500">本月新增客户</div>
+          <div className="mt-1 text-3xl font-semibold text-gray-900">0</div>
+          <div className="mt-4">
+            <span className="text-green-600">↑ 0% 相比上月</span>
+          </div>
+        </div>
+      </div>
+      
+      {/* 最近销售机会表格 */}
+      <div className="bg-white rounded-lg shadow-md overflow-hidden">
+        <div className="px-6 py-4 border-b">
+          <h2 className="text-lg font-semibold">最近销售机会</h2>
+        </div>
+        <div className="p-4">
+          <Table 
+            columns={opportunityColumns} 
+            data={recentOpportunities} 
+            rowKey="id"
+          />
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default Dashboard;

+ 116 - 0
src/client/pages/login.tsx

@@ -0,0 +1,116 @@
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { authClient } from '../api';
+import { LoginRequest, LoginResponse } from '../api/types';
+import { logger } from '../utils/logger';
+
+const Login: React.FC = () => {
+  const [formData, setFormData] = useState<LoginRequest>({
+    username: '',
+    password: ''
+  });
+  const [error, setError] = useState('');
+  const [loading, setLoading] = useState(false);
+  const navigate = useNavigate();
+
+  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const { name, value } = e.target;
+    setFormData((prev: LoginRequest) => ({ ...prev, [name]: value }));
+  };
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setLoading(true);
+    setError('');
+
+    try {
+      logger.auth('Attempting login for user: ', formData.username);
+      const response = await authClient.$post({
+        json: formData
+      });
+
+      if (!response.ok) {
+        throw new Error('登录失败,请检查用户名和密码');
+      }
+
+      const data = (await response.json()) as LoginResponse;
+      logger.auth('Login successful for user: ', formData.username);
+      
+      // 保存认证信息
+      localStorage.setItem('token', data.token);
+      localStorage.setItem('username', formData.username);
+      
+      // 重定向到仪表盘
+      navigate('/');
+    } catch (err) {
+      logger.error('Login error:', err);
+      setError(err instanceof Error ? err.message : '登录时发生错误');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <div className="flex items-center justify-center min-h-screen bg-gray-100">
+      <div className="w-full max-w-md p-8 space-y-8 bg-white rounded-lg shadow-md">
+        <div className="text-center">
+          <h1 className="text-2xl font-bold text-gray-900">CRM系统登录</h1>
+        </div>
+        
+        {error && (
+          <div className="p-3 text-sm text-red-800 bg-red-100 rounded-md">
+            {error}
+          </div>
+        )}
+        
+        <form className="space-y-6" onSubmit={handleSubmit}>
+          <div>
+            <label htmlFor="username" className="block text-sm font-medium text-gray-700">
+              用户名
+            </label>
+            <div className="mt-1">
+              <input
+                id="username"
+                name="username"
+                type="text"
+                required
+                value={formData.username}
+                onChange={handleChange}
+                className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
+              />
+            </div>
+          </div>
+          
+          <div>
+            <label htmlFor="password" className="block text-sm font-medium text-gray-700">
+              密码
+            </label>
+            <div className="mt-1">
+              <input
+                id="password"
+                name="password"
+                type="password"
+                required
+                value={formData.password}
+                onChange={handleChange}
+                className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
+              />
+            </div>
+          </div>
+          
+          <div>
+            <button
+              type="submit"
+              disabled={loading}
+              className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
+            >
+              {loading ? '登录中...' : '登录'}
+            </button>
+          </div>
+        </form>
+      </div>
+    </div>
+  );
+};
+
+export default Login;

+ 244 - 0
src/client/pages/opportunities.tsx

@@ -0,0 +1,244 @@
+import React, { useEffect, useState } from 'react';
+import { opportunityClient } from '../api';
+import { Opportunity, OpportunityListResponse } from '../api/types';
+import { logger } from '../utils/logger';
+import Table from '../components/Table';
+import Modal from '../components/Modal';
+import Form from '../components/Form';
+
+const Opportunities: React.FC = () => {
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState('');
+  const [opportunities, setOpportunities] = useState<Opportunity[]>([]);
+  const [pagination, setPagination] = useState({
+    total: 0,
+    current: 1,
+    pageSize: 10
+  });
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [currentOpportunity, setCurrentOpportunity] = useState<Opportunity | null>(null);
+  
+  const fetchOpportunities = async () => {
+    try {
+      setLoading(true);
+      const response = await opportunityClient.$get({
+        query: { 
+          page: pagination.current, 
+          pageSize: pagination.pageSize 
+        }
+      });
+      
+      if (!response.ok) throw new Error('获取销售机会列表失败');
+      
+      const data = (await response.json()) as OpportunityListResponse;
+      setOpportunities(data.data);
+      setPagination(data.pagination);
+      logger.ui('Fetched opportunities successfully');
+    } catch (err) {
+      logger.error('Error fetching opportunities:', err);
+      setError(err instanceof Error ? err.message : '获取销售机会数据时发生错误');
+    } finally {
+      setLoading(false);
+    }
+  };
+  
+  useEffect(() => {
+    fetchOpportunities();
+  }, [pagination.current, pagination.pageSize]);
+  
+  const handlePageChange = (page: number) => {
+    setPagination(prev => ({ ...prev, current: page }));
+  };
+  
+  const handleAdd = () => {
+    setCurrentOpportunity(null);
+    setIsModalOpen(true);
+  };
+  
+  const handleEdit = (opportunity: Opportunity) => {
+    setCurrentOpportunity(opportunity);
+    setIsModalOpen(true);
+  };
+  
+  const handleDelete = async (id: number) => {
+    if (!confirm('确定要删除这个销售机会吗?')) return;
+    
+    try {
+      // @ts-ignore
+      const response = await opportunityClient[':id'].$delete({
+        param: { id }
+      });
+      
+      if (!response.ok) throw new Error('删除销售机会失败');
+      
+      logger.ui(`Deleted opportunity with id: ${id}`);
+      fetchOpportunities();
+    } catch (err) {
+      logger.error('Error deleting opportunity:', err);
+      setError(err instanceof Error ? err.message : '删除销售机会时发生错误');
+    }
+  };
+  
+  const handleSave = async (formData: any) => {
+    try {
+      if (currentOpportunity) {
+        // 更新现有销售机会
+        // @ts-ignore
+        const response = await opportunityClient[':id'].$put({
+          param: { id: currentOpportunity.id },
+          json: formData
+        });
+        
+        if (!response.ok) throw new Error('更新销售机会失败');
+        logger.ui(`Updated opportunity with id: ${currentOpportunity.id}`);
+      } else {
+        // 创建新销售机会
+        const response = await opportunityClient.$post({
+          json: formData
+        });
+        
+        if (!response.ok) throw new Error('创建销售机会失败');
+        logger.ui('Created new opportunity');
+      }
+      
+      setIsModalOpen(false);
+      fetchOpportunities();
+    } catch (err) {
+      logger.error('Error saving opportunity:', err);
+      setError(err instanceof Error ? err.message : '保存销售机会时发生错误');
+    }
+  };
+  
+  const columns = [
+    { key: 'name', title: '机会名称' },
+    { key: 'customerName', title: '客户名称' },
+    { key: 'amount', title: '金额', render: (value: number) => `¥${value.toLocaleString()}` },
+    { key: 'stage', title: '销售阶段', render: (value: string) => {
+      // 根据不同阶段显示不同样式
+      const stages = {
+        'prospect': '潜在客户',
+        'qualification': '需求确认',
+        'proposal': '方案制定',
+        'negotiation': '商务谈判',
+        'closed_won': '已成交',
+        'closed_lost': '未成交'
+      };
+      return stages[value as keyof typeof stages] || value;
+    }},
+    { key: 'expectedCloseDate', title: '预计成交日期' },
+    { key: 'createdAt', title: '创建时间' },
+    { 
+      key: 'actions', 
+      title: '操作',
+      render: (_: any, record: Opportunity) => (
+        <div className="flex space-x-2">
+          <button 
+            onClick={() => handleEdit(record)}
+            className="text-blue-600 hover:underline"
+          >
+            编辑
+          </button>
+          <button 
+            onClick={() => handleDelete(record.id)}
+            className="text-red-600 hover:underline"
+          >
+            删除
+          </button>
+        </div>
+      )
+    }
+  ];
+  
+  const formFields = [
+    { name: 'name', label: '机会名称', type: 'text' as const, required: true },
+    { name: 'customerId', label: '客户', type: 'select' as const, required: true,
+      options: [
+        { label: '选择客户', value: '' },
+        // 实际项目中这里应该从API获取客户列表
+      ]
+    },
+    { name: 'amount', label: '金额', type: 'number' as const, required: true },
+    { name: 'stage', label: '销售阶段', type: 'select' as const, required: true,
+      options: [
+        { label: '潜在客户', value: 'prospect' },
+        { label: '需求确认', value: 'qualification' },
+        { label: '方案制定', value: 'proposal' },
+        { label: '商务谈判', value: 'negotiation' },
+        { label: '已成交', value: 'closed_won' },
+        { label: '未成交', value: 'closed_lost' }
+      ]
+    },
+    { name: 'expectedCloseDate', label: '预计成交日期', type: 'text' as const, required: true },
+    { name: 'description', label: '机会描述', type: 'textarea' as const }
+  ];
+  
+  return (
+    <div className="space-y-6">
+      <div className="flex justify-between items-center">
+        <h1 className="text-2xl font-bold">销售机会管理</h1>
+        <button 
+          onClick={handleAdd}
+          className="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"
+        >
+          添加销售机会
+        </button>
+      </div>
+      
+      {error && (
+        <div className="p-4 text-red-600 bg-red-100 rounded-md">{error}</div>
+      )}
+      
+      <div className="bg-white rounded-lg shadow-md overflow-hidden">
+        <div className="p-4">
+          {loading ? (
+            <div className="flex justify-center items-center h-64">加载中...</div>
+          ) : (
+            <Table 
+              columns={columns} 
+              data={opportunities} 
+              rowKey="id"
+            />
+          )}
+        </div>
+        
+        {/* 分页控件 */}
+        <div className="px-4 py-3 bg-gray-50 border-t flex items-center justify-between">
+          <div className="text-sm text-gray-700">
+            共 {pagination.total} 条记录,当前第 {pagination.current} 页
+          </div>
+          <div className="flex space-x-2">
+            <button 
+              onClick={() => handlePageChange(pagination.current - 1)}
+              disabled={pagination.current === 1}
+              className="px-3 py-1 border rounded disabled:opacity-50"
+            >
+              上一页
+            </button>
+            <button 
+              onClick={() => handlePageChange(pagination.current + 1)}
+              disabled={pagination.current * pagination.pageSize >= pagination.total}
+              className="px-3 py-1 border rounded disabled:opacity-50"
+            >
+              下一页
+            </button>
+          </div>
+        </div>
+      </div>
+      
+      {/* 添加/编辑销售机会模态框 */}
+      <Modal
+        title={currentOpportunity ? '编辑销售机会' : '添加销售机会'}
+        open={isModalOpen}
+        onClose={() => setIsModalOpen(false)}
+        onSubmit={handleSave}
+      >
+        <Form 
+          fields={formFields}
+          initialValues={currentOpportunity || {}}
+        />
+      </Modal>
+    </div>
+  );
+};
+
+export default Opportunities;

+ 266 - 0
src/client/pages/tickets/create.tsx

@@ -0,0 +1,266 @@
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import Card from 'antd/es/card';
+import Form from 'antd/es/form';
+import Input from 'antd/es/input';
+import Select from 'antd/es/select';
+import Button from 'antd/es/button';
+import InputNumber from 'antd/es/input-number';
+import DatePicker from 'antd/es/date-picker';
+import Space from 'antd/es/space';
+import Typography from 'antd/es/typography';
+import message from 'antd/es/message';
+import TextArea from 'antd/es/input/TextArea';
+import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons';
+import { z } from 'zod';
+import { ticketsClient } from '@/client/api';
+import type { InferRequestType } from 'hono/client';
+import { logger } from '@/client/utils/logger';
+
+const { Title } = Typography;
+const { Option } = Select;
+
+// 表单验证Schema
+const CreateTicketSchema = z.object({
+  title: z.string().min(3, '标题至少3个字符').max(100, '标题最多100个字符'),
+  customerId: z.number().int().positive('请选择客户'),
+  contactId: z.number().int().positive().optional(),
+  type: z.string().min(1, '请选择工单类型'),
+  priority: z.string().min(1, '请选择优先级'),
+  description: z.string().min(10, '描述至少10个字符').max(2000, '描述最多2000个字符'),
+  dueDate: z.date().optional(),
+  assigneeId: z.number().int().positive().optional(),
+});
+
+// 定义请求类型
+type CreateTicketRequest = InferRequestType<typeof ticketsClient.$post>['json'];
+
+const CreateTicketPage = () => {
+  const navigate = useNavigate();
+  const [loading, setLoading] = useState<boolean>(false);
+  const [form] = Form.useForm<{
+    title: string;
+    customerId: number;
+    contactId?: number;
+    type: string;
+    priority: string;
+    description: string;
+    dueDate?: Date;
+    assigneeId?: number;
+  }>();
+  
+  // 模拟客户数据 - 实际项目中应从API获取
+  const customers = [
+    { id: 1, name: 'ABC公司' },
+    { id: 2, name: 'XYZ企业' },
+    { id: 3, name: '123集团' },
+  ];
+  
+  // 模拟联系人数据 - 实际项目中应根据选择的客户动态获取
+  const contacts = [
+    { id: 1, name: '张三', customerId: 1 },
+    { id: 2, name: '李四', customerId: 1 },
+    { id: 3, name: '王五', customerId: 2 },
+  ];
+  
+  // 模拟负责人数据 - 实际项目中应从API获取
+  const assignees = [
+    { id: 1, name: '技术支持-小明' },
+    { id: 2, name: '技术支持-小红' },
+    { id: 3, name: '技术支持-小刚' },
+  ];
+
+  // 处理表单提交
+  const handleSubmit = async () => {
+    try {
+      // 验证表单
+      const values = await form.validateFields();
+      
+      // 转换表单数据为API请求格式
+      const ticketData: CreateTicketRequest = {
+        title: values.title,
+        customerId: values.customerId,
+        contactId: values.contactId,
+        type: values.type,
+        priority: values.priority,
+        description: values.description,
+        dueDate: values.dueDate ? values.dueDate.toISOString() : undefined,
+        assigneeId: values.assigneeId,
+      };
+      
+      setLoading(true);
+      
+      // 调用API创建工单
+      const response = await ticketsClient.$post({
+        json: ticketData
+      });
+      
+      if (!response.ok) {
+        throw new Error('创建工单失败');
+      }
+      
+      const result = await response.json();
+      message.success('工单创建成功');
+      
+      // 跳转到工单详情页
+      const resultData: any = await response.json();
+      navigate(`/tickets/${resultData.data.id}`);
+    } catch (error) {
+      logger.error('创建工单失败:', error);
+      message.error(error instanceof Error ? error.message : '创建工单时发生错误');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 返回列表页
+  const handleBack = () => {
+    navigate('/tickets');
+  };
+
+  // 处理客户选择变化,过滤联系人
+  const handleCustomerChange = (customerId: number) => {
+    form.setFieldValue('contactId', undefined);
+    // 在实际项目中,这里应该通过API获取该客户的联系人列表
+  };
+
+  return (
+    <div className="page-container">
+      <div className="page-header" style={{ marginBottom: 24 }}>
+        <Button onClick={handleBack} style={{ marginRight: 16 }}>
+          <ArrowLeftOutlined /> 返回列表
+        </Button>
+        <Title level={2} style={{ margin: 0 }}>创建新工单</Title>
+      </div>
+
+      <Card>
+        <Form
+          form={form}
+          layout="vertical"
+          initialValues={{
+            priority: 'medium',
+            type: 'technical_support'
+          }}
+        >
+          <Space.Compact size="large" style={{ width: '100%', marginBottom: 16 }}>
+            <Form.Item
+              name="title"
+              label="工单标题"
+              rules={[{ required: true, message: '请输入工单标题' }]}
+            >
+              <Input placeholder="请输入工单标题" maxLength={100} />
+            </Form.Item>
+          </Space.Compact>
+
+          <Space.Compact size="large" style={{ width: '100%', marginBottom: 16 }}>
+            <Form.Item
+              name="customerId"
+              label="客户"
+              rules={[{ required: true, message: '请选择客户' }]}
+            >
+              <Select 
+                placeholder="请选择客户" 
+                style={{ width: '100%' }}
+                onChange={handleCustomerChange}
+              >
+                {customers.map(customer => (
+                  <Option key={customer.id} value={customer.id.toString()}>
+                    {customer.name}
+                  </Option>
+                ))}
+              </Select>
+            </Form.Item>
+
+            <Form.Item
+              name="contactId"
+              label="联系人"
+            >
+              <Select placeholder="请选择联系人" style={{ width: '100%' }}>
+                {contacts.map(contact => (
+                  <Option key={contact.id} value={contact.id.toString()}>
+                    {contact.name}
+                  </Option>
+                ))}
+              </Select>
+            </Form.Item>
+          </Space.Compact>
+
+          <Space.Compact size="large" style={{ width: '100%', marginBottom: 16 }}>
+            <Form.Item
+              name="type"
+              label="工单类型"
+              rules={[{ required: true, message: '请选择工单类型' }]}
+            >
+              <Select placeholder="请选择工单类型" style={{ width: '100%' }}>
+                <Option value="technical_support">技术支持</Option>
+                <Option value="service_request">服务请求</Option>
+                <Option value="complaint">投诉</Option>
+                <Option value="consultation">咨询</Option>
+                <Option value="other">其他</Option>
+              </Select>
+            </Form.Item>
+
+            <Form.Item
+              name="priority"
+              label="优先级"
+              rules={[{ required: true, message: '请选择优先级' }]}
+            >
+              <Select placeholder="请选择优先级" style={{ width: '100%' }}>
+                <Option value="low">低</Option>
+                <Option value="medium">中</Option>
+                <Option value="high">高</Option>
+                <Option value="urgent">紧急</Option>
+              </Select>
+            </Form.Item>
+
+            <Form.Item
+              name="dueDate"
+              label="截止日期"
+            >
+              <DatePicker style={{ width: '100%' }} placeholder="选择截止日期" />
+            </Form.Item>
+          </Space.Compact>
+
+          <Space.Compact size="large" style={{ width: '100%', marginBottom: 16 }}>
+            <Form.Item
+              name="assigneeId"
+              label="负责人"
+            >
+              <Select placeholder="请选择负责人" style={{ width: '100%' }}>
+                {assignees.map(assignee => (
+                  <Option key={assignee.id} value={assignee.id.toString()}>
+                    {assignee.name}
+                  </Option>
+                ))}
+              </Select>
+            </Form.Item>
+          </Space.Compact>
+
+          <Form.Item
+            name="description"
+            label="问题描述"
+            rules={[{ required: true, message: '请输入问题描述' }]}
+          >
+            <TextArea 
+              rows={6} 
+              placeholder="请详细描述问题..." 
+              maxLength={2000}
+              showCount
+            />
+          </Form.Item>
+
+          <div style={{ textAlign: 'right', marginTop: 24 }}>
+            <Space>
+              <Button onClick={handleBack}>取消</Button>
+              <Button type="primary" icon={<SaveOutlined />} onClick={handleSubmit} loading={loading}>
+                创建工单
+              </Button>
+            </Space>
+          </div>
+        </Form>
+      </Card>
+    </div>
+  );
+};
+
+export default CreateTicketPage;

+ 220 - 0
src/client/pages/tickets/detail.tsx

@@ -0,0 +1,220 @@
+import { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import Card from 'antd/es/card';
+import Descriptions from 'antd/es/descriptions';
+import Button from 'antd/es/button';
+import Tag from 'antd/es/tag';
+import Spin from 'antd/es/spin';
+import Divider from 'antd/es/divider';
+import Badge from 'antd/es/badge';
+import Space from 'antd/es/space';
+import Typography from 'antd/es/typography';
+import { EditOutlined, ArrowLeftOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
+import { ticketsClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+import { logger } from '@/client/utils/logger';
+
+// 定义响应类型
+type TicketDetailResponse = InferResponseType<typeof ticketsClient[':id']['$get'], 200>;
+
+const { Title, Text } = Typography;
+
+const TicketDetailPage = () => {
+  const { id } = useParams<{ id: string }>();
+  const navigate = useNavigate();
+  const [loading, setLoading] = useState<boolean>(true);
+  const [ticket, setTicket] = useState<TicketDetailResponse | null>(null);
+
+  // 获取工单详情数据
+  const fetchTicketDetail = async () => {
+    if (!id) return;
+    
+    try {
+      setLoading(true);
+      const response = await ticketsClient[':id'].$get({
+        param: { id }
+      });
+      
+      if (!response.ok) {
+        throw new Error('Failed to fetch ticket details');
+      }
+      
+      const data = await response.json() as TicketDetailResponse;
+      setTicket(data);
+    } catch (error) {
+      logger.error('Error fetching ticket details:', error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 初始加载
+  useEffect(() => {
+    fetchTicketDetail();
+  }, [id]);
+
+  // 返回列表页
+  const handleBack = () => {
+    navigate('/tickets');
+  };
+
+  // 编辑工单
+  const handleEdit = () => {
+    navigate(`/tickets/${id}/edit`);
+  };
+
+  // 更新工单状态
+  const handleStatusChange = async (newStatus: string) => {
+    if (!id || !window.confirm(`确定要将工单状态更新为${newStatus === 'closed' ? '已关闭' : '已解决'}吗?`)) {
+      return;
+    }
+    
+    try {
+      setLoading(true);
+      // 状态映射表 - 将字符串状态转换为数字状态码
+      const statusMap: Record<string, number> = {
+        'new': 0,
+        'in_progress': 1,
+        'pending': 2,
+        'resolved': 3,
+        'closed': 4,
+        'reopened': 5
+      };
+      
+      const statusCode = statusMap[newStatus];
+      if (statusCode === undefined) {
+        throw new Error('无效的工单状态');
+      }
+      
+      const response = await ticketsClient[':id'].$patch({
+        param: { id },
+        json: {
+          status: statusCode,
+          comment: newStatus === 'resolved' ? '问题已解决' : '工单已关闭'
+        }
+      });
+      
+      if (!response.ok) {
+        throw new Error('Failed to update ticket status');
+      }
+      
+      // 重新获取工单详情
+      fetchTicketDetail();
+    } catch (error) {
+      logger.error('Error updating ticket status:', error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 状态标签样式映射
+  const statusTagColorMap: Record<string, string> = {
+    open: 'blue',
+    in_progress: 'orange',
+    resolved: 'green',
+    closed: 'purple',
+    cancelled: 'red',
+  };
+
+  // 状态显示文本映射
+  const statusTextMap: Record<string, string> = {
+    open: '开放',
+    in_progress: '处理中',
+    resolved: '已解决',
+    closed: '已关闭',
+    cancelled: '已取消',
+  };
+
+  if (loading) {
+    return (
+      <div className="page-loading">
+        <Spin size="large" />
+      </div>
+    );
+  }
+
+  if (!ticket) {
+    return (
+      <div className="page-error">
+        <Text type="danger">工单不存在或已被删除</Text>
+        <Button onClick={handleBack} style={{ marginLeft: 16 }}>
+          <ArrowLeftOutlined /> 返回列表
+        </Button>
+      </div>
+    );
+  }
+
+  return (
+    <div className="page-container">
+      <div className="page-header" style={{ marginBottom: 24 }}>
+        <Button onClick={handleBack} style={{ marginRight: 16 }}>
+          <ArrowLeftOutlined /> 返回
+        </Button>
+        <Title level={2} style={{ margin: 0 }}>工单详情</Title>
+      </div>
+
+      <Card>
+        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
+          <div>
+            <Text strong style={{ fontSize: 18, marginRight: 16 }}>#{ticket.ticketNumber}</Text>
+            <Title level={3} style={{ margin: 0, display: 'inline-block' }}>{ticket.title}</Title>
+          </div>
+          <Space>
+            <Tag color={statusTagColorMap[ticket.status] || 'default'}>
+              {statusTextMap[ticket.status] || ticket.status}
+            </Tag>
+            
+            <Button type="primary" icon={<EditOutlined />} onClick={handleEdit}>
+              编辑
+            </Button>
+          </Space>
+        </div>
+
+        <Descriptions title="基本信息" column={3} bordered>
+          <Descriptions.Item label="工单编号">{ticket.ticketNumber}</Descriptions.Item>
+          <Descriptions.Item label="创建时间">{new Date(ticket.createdAt).toLocaleString()}</Descriptions.Item>
+          <Descriptions.Item label="更新时间">{new Date(ticket.updatedAt).toLocaleString()}</Descriptions.Item>
+          <Descriptions.Item label="客户名称">{ticket.customer.name}</Descriptions.Item>
+          <Descriptions.Item label="客户联系人">{ticket.contact?.name || '-'}</Descriptions.Item>
+          <Descriptions.Item label="联系人电话">{ticket.contact?.phone || '-'}</Descriptions.Item>
+          <Descriptions.Item label="优先级">
+            <Tag color={ticket.priority === 'high' ? 'red' : ticket.priority === 'medium' ? 'orange' : 'green'}>
+              {ticket.priority === 'high' ? '高' : ticket.priority === 'medium' ? '中' : '低'}
+            </Tag>
+          </Descriptions.Item>
+          <Descriptions.Item label="工单类型">{ticket.type}</Descriptions.Item>
+          <Descriptions.Item label="负责人">{ticket.assignee?.name || '未分配'}</Descriptions.Item>
+        </Descriptions>
+
+        <Divider orientation="left">问题描述</Divider>
+        <Card>
+          <Text>{ticket.description}</Text>
+        </Card>
+
+        {ticket.resolution && (
+          <>
+            <Divider orientation="left">解决方案</Divider>
+            <Card>
+              <Text>{ticket.resolution}</Text>
+            </Card>
+          </>
+        )}
+
+        <div style={{ marginTop: 24, textAlign: 'right' }}>
+          {ticket.status !== 'resolved' && ticket.status !== 'closed' && (
+            <Space>
+              <Button type="primary" icon={<CheckCircleOutlined />} onClick={() => handleStatusChange('resolved')}>
+                标记为已解决
+              </Button>
+              <Button danger icon={<CloseCircleOutlined />} onClick={() => handleStatusChange('closed')}>
+                关闭工单
+              </Button>
+            </Space>
+          )}
+        </div>
+      </Card>
+    </div>
+  );
+};
+
+export default TicketDetailPage;

+ 342 - 0
src/client/pages/tickets/edit.tsx

@@ -0,0 +1,342 @@
+import { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import Card from 'antd/es/card';
+import Form from 'antd/es/form';
+import Input from 'antd/es/input';
+import Select from 'antd/es/select';
+import Button from 'antd/es/button';
+import DatePicker from 'antd/es/date-picker';
+import Space from 'antd/es/space';
+import Typography from 'antd/es/typography';
+import message from 'antd/es/message';
+import TextArea from 'antd/es/input/TextArea';
+import Spin from 'antd/es/spin';
+import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons';
+import { z } from 'zod';
+import { ticketsClient } from '@/client/api';
+import type { InferRequestType, InferResponseType } from 'hono/client';
+import { logger } from '@/client/utils/logger';
+
+const { Title } = Typography;
+const { Option } = Select;
+
+// 表单验证Schema
+const EditTicketSchema = z.object({
+  title: z.string().min(3, '标题至少3个字符').max(100, '标题最多100个字符'),
+  customerId: z.number().int().positive('请选择客户'),
+  contactId: z.number().int().positive().optional(),
+  type: z.string().min(1, '请选择工单类型'),
+  priority: z.string().min(1, '请选择优先级'),
+  description: z.string().min(10, '描述至少10个字符').max(2000, '描述最多2000个字符'),
+  dueDate: z.date().optional(),
+  assigneeId: z.number().int().positive().optional(),
+  status: z.string().min(1, '请选择工单状态'),
+});
+
+// 定义请求和响应类型
+type UpdateTicketRequest = InferRequestType<typeof ticketsClient['$put']>['json'];
+type TicketDetailResponse = InferResponseType<typeof ticketsClient['$get'], 200>;
+
+const EditTicketPage = () => {
+  const { id } = useParams<{ id: string }>();
+  const navigate = useNavigate();
+  const [loading, setLoading] = useState<boolean>(false);
+  const [initialLoading, setInitialLoading] = useState<boolean>(true);
+  const [form] = Form.useForm<{
+    title: string;
+    customerId: string;
+    contactId?: string;
+    type: string;
+    priority: string;
+    description: string;
+    dueDate?: Date;
+    assigneeId?: string;
+    status: string;
+  }>();
+  
+  // 模拟客户数据 - 实际项目中应从API获取
+  const customers = [
+    { id: 1, name: 'ABC公司' },
+    { id: 2, name: 'XYZ企业' },
+    { id: 3, name: '123集团' },
+  ];
+  
+  // 模拟联系人数据 - 实际项目中应根据选择的客户动态获取
+  const contacts = [
+    { id: 1, name: '张三', customerId: 1 },
+    { id: 2, name: '李四', customerId: 1 },
+    { id: 3, name: '王五', customerId: 2 },
+  ];
+  
+  // 模拟负责人数据 - 实际项目中应从API获取
+  const assignees = [
+    { id: 1, name: '技术支持-小明' },
+    { id: 2, name: '技术支持-小红' },
+    { id: 3, name: '技术支持-小刚' },
+  ];
+  
+  // 工单状态选项
+  const statusOptions = [
+    { value: 'new', label: '新建' },
+    { value: 'in_progress', label: '处理中' },
+    { value: 'pending', label: '待处理' },
+    { value: 'resolved', label: '已解决' },
+    { value: 'closed', label: '已关闭' },
+    { value: 'reopened', label: '已重新打开' },
+  ];
+
+  // 获取工单详情数据
+  const fetchTicketDetail = async () => {
+    if (!id) return;
+    
+    try {
+      setInitialLoading(true);
+      const response = await ticketsClient.$get({
+        param: { id }
+      });
+      
+      if (!response.ok) {
+        throw new Error('Failed to fetch ticket details');
+      }
+      
+      const data = await response.json() as TicketDetailResponse;
+      
+      // 填充表单数据
+      form.setFieldsValue({
+        title: data.title,
+        customerId: data.customerId.toString(),
+        contactId: data.contactId?.toString(),
+        type: data.type,
+        priority: data.priority,
+        description: data.description,
+        status: data.status,
+        dueDate: data.dueDate ? new Date(data.dueDate) : undefined,
+        assigneeId: data.assigneeId?.toString(),
+      });
+    } catch (error) {
+      logger.error('Error fetching ticket details:', error);
+      message.error('加载工单详情失败');
+    } finally {
+      setInitialLoading(false);
+    }
+  };
+
+  // 初始加载
+  useEffect(() => {
+    fetchTicketDetail();
+  }, [id, form]);
+
+  // 处理表单提交
+  const handleSubmit = async () => {
+    if (!id) return;
+    
+    try {
+      // 验证表单
+      const values = await form.validateFields();
+      
+      // 转换表单数据为API请求格式
+      const ticketData: UpdateTicketRequest = {
+        title: values.title,
+        customerId: parseInt(values.customerId, 10),
+        contactId: values.contactId ? parseInt(values.contactId, 10) : undefined,
+        type: values.type,
+        priority: values.priority,
+        description: values.description,
+        status: values.status,
+        dueDate: values.dueDate ? values.dueDate.toISOString() : undefined,
+        assigneeId: values.assigneeId ? parseInt(values.assigneeId, 10) : undefined,
+      };
+      
+      setLoading(true);
+      
+      // 调用API更新工单
+      const response = await ticketsClient.$put({
+        param: { id },
+        json: ticketData
+      });
+      
+      if (!response.ok) {
+        throw new Error('更新工单失败');
+      }
+      
+      message.success('工单更新成功');
+      
+      // 跳转到工单详情页
+      navigate(`/tickets/${id}`);
+    } catch (error) {
+      logger.error('更新工单失败:', error);
+      message.error(error instanceof Error ? error.message : '更新工单时发生错误');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 返回详情页
+  const handleBack = () => {
+    navigate(`/tickets/${id}`);
+  };
+
+  // 处理客户选择变化,过滤联系人
+  const handleCustomerChange = (customerId: string) => {
+    form.setFieldValue('contactId', undefined);
+    // 在实际项目中,这里应该通过API获取该客户的联系人列表
+  };
+
+  if (initialLoading) {
+    return (
+      <div className="page-loading">
+        <Spin size="large" />
+      </div>
+    );
+  }
+
+  return (
+    <div className="page-container">
+      <div className="page-header" style={{ marginBottom: 24 }}>
+        <Button onClick={handleBack} style={{ marginRight: 16 }}>
+          <ArrowLeftOutlined /> 返回详情
+        </Button>
+        <Title level={2} style={{ margin: 0 }}>编辑工单</Title>
+      </div>
+
+      <Card>
+        <Form
+          form={form}
+          layout="vertical"
+        >
+          <Space.Compact size="large" style={{ width: '100%', marginBottom: 16 }}>
+            <Form.Item
+              name="title"
+              label="工单标题"
+              rules={[{ required: true, message: '请输入工单标题' }]}
+            >
+              <Input placeholder="请输入工单标题" maxLength={100} />
+            </Form.Item>
+          </Space.Compact>
+
+          <Space.Compact size="large" style={{ width: '100%', marginBottom: 16 }}>
+            <Form.Item
+              name="customerId"
+              label="客户"
+              rules={[{ required: true, message: '请选择客户' }]}
+            >
+              <Select 
+                placeholder="请选择客户" 
+                style={{ width: '100%' }}
+                onChange={handleCustomerChange}
+              >
+                {customers.map(customer => (
+                  <Option key={customer.id} value={customer.id.toString()}>
+                    {customer.name}
+                  </Option>
+                ))}
+              </Select>
+            </Form.Item>
+
+            <Form.Item
+              name="contactId"
+              label="联系人"
+            >
+              <Select placeholder="请选择联系人" style={{ width: '100%' }}>
+                {contacts.map(contact => (
+                  <Option key={contact.id} value={contact.id.toString()}>
+                    {contact.name}
+                  </Option>
+                ))}
+              </Select>
+            </Form.Item>
+          </Space.Compact>
+
+          <Space.Compact size="large" style={{ width: '100%', marginBottom: 16 }}>
+            <Form.Item
+              name="type"
+              label="工单类型"
+              rules={[{ required: true, message: '请选择工单类型' }]}
+            >
+              <Select placeholder="请选择工单类型" style={{ width: '100%' }}>
+                <Option value="technical_support">技术支持</Option>
+                <Option value="service_request">服务请求</Option>
+                <Option value="complaint">投诉</Option>
+                <Option value="consultation">咨询</Option>
+                <Option value="other">其他</Option>
+              </Select>
+            </Form.Item>
+
+            <Form.Item
+              name="priority"
+              label="优先级"
+              rules={[{ required: true, message: '请选择优先级' }]}
+            >
+              <Select placeholder="请选择优先级" style={{ width: '100%' }}>
+                <Option value="low">低</Option>
+                <Option value="medium">中</Option>
+                <Option value="high">高</Option>
+                <Option value="urgent">紧急</Option>
+              </Select>
+            </Form.Item>
+
+            <Form.Item
+              name="status"
+              label="工单状态"
+              rules={[{ required: true, message: '请选择工单状态' }]}
+            >
+              <Select placeholder="请选择工单状态" style={{ width: '100%' }}>
+                {statusOptions.map(option => (
+                  <Option key={option.value} value={option.value}>
+                    {option.label}
+                  </Option>
+                ))}
+              </Select>
+            </Form.Item>
+          </Space.Compact>
+
+          <Space.Compact size="large" style={{ width: '100%', marginBottom: 16 }}>
+            <Form.Item
+              name="dueDate"
+              label="截止日期"
+            >
+              <DatePicker style={{ width: '100%' }} placeholder="选择截止日期" />
+            </Form.Item>
+
+            <Form.Item
+              name="assigneeId"
+              label="负责人"
+            >
+              <Select placeholder="请选择负责人" style={{ width: '100%' }}>
+                {assignees.map(assignee => (
+                  <Option key={assignee.id} value={assignee.id.toString()}>
+                    {assignee.name}
+                  </Option>
+                ))}
+              </Select>
+            </Form.Item>
+          </Space.Compact>
+
+          <Form.Item
+            name="description"
+            label="问题描述"
+            rules={[{ required: true, message: '请输入问题描述' }]}
+          >
+            <TextArea 
+              rows={6} 
+              placeholder="请详细描述问题..." 
+              maxLength={2000}
+              showCount
+            />
+          </Form.Item>
+
+          <div style={{ textAlign: 'right', marginTop: 24 }}>
+            <Space>
+              <Button onClick={handleBack}>取消</Button>
+              <Button type="primary" icon={<SaveOutlined />} onClick={handleSubmit} loading={loading}>
+                保存修改
+              </Button>
+            </Space>
+          </div>
+        </Form>
+      </Card>
+    </div>
+  );
+};
+
+export default EditTicketPage;

+ 229 - 0
src/client/pages/tickets/index.tsx

@@ -0,0 +1,229 @@
+import { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Table, Button, Input, Space, Tag, Dropdown, Menu } from 'antd';
+import type { TableProps, TablePaginationConfig } from 'antd/es/table';
+import type { InputChangeEvent } from 'antd/es/input';
+import { PlusOutlined, SearchOutlined, MoreOutlined } from '@ant-design/icons';
+import type { TableProps } from 'antd/es/table';
+import { ticketsClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+import { logger } from '@/client/utils/logger';
+
+// 定义响应类型
+type TicketListResponse = InferResponseType<typeof ticketsClient.$get, 200>;
+type TicketItem = TicketListResponse['data'][0];
+
+const TicketsPage = () => {
+  const navigate = useNavigate();
+  const [loading, setLoading] = useState<boolean>(true);
+  const [tickets, setTickets] = useState<TicketItem[]>([]);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0,
+  });
+  const [searchText, setSearchText] = useState('');
+
+  // 获取工单列表数据
+  const fetchTickets = async () => {
+    try {
+      setLoading(true);
+      const response = await ticketsClient.$get({
+        query: {
+          page: pagination.current,
+          pageSize: pagination.pageSize,
+          search: searchText || undefined,
+        },
+      });
+      
+      if (!response.ok) {
+        throw new Error('Failed to fetch tickets');
+      }
+      
+      const data = await response.json() as TicketListResponse;
+      setTickets(data.data);
+      setPagination(prev => ({
+        ...prev,
+        total: data.pagination.total,
+      }));
+    } catch (error) {
+      logger.error('Error fetching tickets:', error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 初始加载和分页、搜索变化时重新加载
+  useEffect(() => {
+    fetchTickets();
+  }, [pagination.current, pagination.pageSize, searchText]);
+
+  // 处理分页变化
+  const handleTableChange: TableProps<TicketItem>['onChange'] = (pagination: TablePaginationConfig) => {
+    setPagination(prev => ({
+      ...prev,
+      current: pagination.current || 1,
+      pageSize: pagination.pageSize || 10,
+    }));
+  };
+
+  // 处理搜索
+  const handleSearch = () => {
+    setPagination(prev => ({
+      ...prev,
+      current: 1, // 搜索时重置到第一页
+    }));
+    fetchTickets();
+  };
+
+  // 处理创建工单
+  const handleCreate = () => {
+    navigate('/tickets/create');
+  };
+
+  // 处理查看详情
+  const handleView = (id: number) => {
+    navigate(`/tickets/${id}`);
+  };
+
+  // 处理编辑
+  const handleEdit = (id: number) => {
+    navigate(`/tickets/${id}/edit`);
+  };
+
+  // 状态标签样式映射
+  const statusTagColorMap: Record<string, string> = {
+    open: 'blue',
+    in_progress: 'orange',
+    resolved: 'green',
+    closed: 'purple',
+    cancelled: 'red',
+  };
+
+  // 状态显示文本映射
+  const statusTextMap: Record<string, string> = {
+    open: '开放',
+    in_progress: '处理中',
+    resolved: '已解决',
+    closed: '已关闭',
+    cancelled: '已取消',
+  };
+
+  // 表格列定义
+  const columns = [
+    {
+      title: '工单编号',
+      dataIndex: 'ticketNumber',
+      key: 'ticketNumber',
+      width: 120,
+    },
+    {
+      title: '标题',
+      dataIndex: 'title',
+      key: 'title',
+    },
+    {
+      title: '客户',
+      dataIndex: ['customer', 'name'],
+      key: 'customerName',
+      width: 150,
+    },
+    {
+      title: '状态',
+      key: 'status',
+      width: 120,
+      render: (_: any, record: TicketItem) => (
+        <Tag color={statusTagColorMap[record.status] || 'default'}>
+          {statusTextMap[record.status] || record.status}
+        </Tag>
+      ),
+    },
+    {
+      title: '优先级',
+      dataIndex: 'priority',
+      key: 'priority',
+      width: 100,
+      render: (priority: string) => {
+        let color = 'default';
+        if (priority === 'high') color = 'red';
+        if (priority === 'medium') color = 'orange';
+        if (priority === 'low') color = 'green';
+        
+        return (
+          <Tag color={color}>
+            {priority === 'high' ? '高' : priority === 'medium' ? '中' : '低'}
+          </Tag>
+        );
+      },
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createdAt',
+      key: 'createdAt',
+      width: 180,
+      render: (date: string) => new Date(date).toLocaleString(),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 120,
+      render: (_: any, record: TicketItem) => (
+        <Dropdown
+          overlay={
+            <Menu>
+              <Menu.Item onClick={() => handleView(record.id)}>查看</Menu.Item>
+              <Menu.Item onClick={() => handleEdit(record.id)}>编辑</Menu.Item>
+            </Menu>
+          }
+        >
+          <Button type="text" icon={<MoreOutlined />} size="small" />
+        </Dropdown>
+      ),
+    },
+  ];
+
+  return (
+    <div className="page-container">
+      <div className="page-header">
+        <h1>服务工单管理</h1>
+        <Button 
+          type="primary" 
+          icon={<PlusOutlined />} 
+          onClick={handleCreate}
+        >
+          创建工单
+        </Button>
+      </div>
+      
+      <div className="page-search">
+        <Space.Compact style={{ width: '100%' }}>
+          <Input
+            placeholder="搜索工单..."
+            prefix={<SearchOutlined />}
+            value={searchText}
+            onChange={(e: InputChangeEvent) => setSearchText(e.target.value)}
+            onPressEnter={handleSearch}
+          />
+          <Button type="primary" onClick={handleSearch}>搜索</Button>
+        </Space.Compact>
+      </div>
+      
+      <Table
+        columns={columns}
+        dataSource={tickets.map(ticket => ({ ...ticket, key: ticket.id }))}
+        rowKey="id"
+        loading={loading}
+        pagination={{
+          ...pagination,
+          showSizeChanger: true,
+          showTotal: (total: number) => `共 ${total} 条记录`,
+          pageSizeOptions: ['10', '20', '50'],
+        }}
+        onChange={handleTableChange}
+        size="middle"
+      />
+    </div>
+  );
+};
+
+export default TicketsPage;

+ 242 - 0
src/client/pages/users/index.tsx

@@ -0,0 +1,242 @@
+import { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import Table from 'antd/es/table';
+import Button from 'antd/es/button';
+import Input from 'antd/es/input';
+import Space from 'antd/es/space';
+import Tag from 'antd/es/tag';
+import Dropdown from 'antd/es/dropdown';
+import Menu from 'antd/es/menu';
+import Switch from 'antd/es/switch';
+import { PlusOutlined, SearchOutlined, MoreOutlined, UserOutlined } from '@ant-design/icons';
+import type { TableProps, TablePaginationConfig } from 'antd/es/table';
+import type { ChangeEvent } from 'react';
+import { usersClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+import { logger } from '@/client/utils/logger';
+
+// 定义响应类型
+type UserListResponse = InferResponseType<typeof usersClient.$get, 200>;
+type UserItem = UserListResponse['data'][0];
+
+const UsersPage = () => {
+  const navigate = useNavigate();
+  const [loading, setLoading] = useState<boolean>(true);
+  const [users, setUsers] = useState<UserItem[]>([]);
+  const [pagination, setPagination] = useState<TablePaginationConfig>({
+    current: 1,
+    pageSize: 10,
+    total: 0,
+  });
+  const [searchText, setSearchText] = useState('');
+
+  // 获取用户列表数据
+  const fetchUsers = async () => {
+    try {
+      setLoading(true);
+      const response = await usersClient.$get({
+        query: {
+          page: pagination.current,
+          pageSize: pagination.pageSize,
+          search: searchText || undefined,
+        },
+      });
+      
+      if (!response.ok) {
+        throw new Error('Failed to fetch users');
+      }
+      
+      const data = await response.json() as UserListResponse;
+      setUsers(data.data);
+      setPagination(prev => ({
+        ...prev,
+        total: data.pagination.total,
+      }));
+    } catch (error) {
+      logger.error('Error fetching users:', error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 初始加载和分页、搜索变化时重新加载
+  useEffect(() => {
+    fetchUsers();
+  }, [pagination.current, pagination.pageSize, searchText]);
+
+  // 处理分页变化
+  const handleTableChange: TableProps<UserItem>['onChange'] = (pagination) => {
+    setPagination(prev => ({
+      ...prev,
+      current: pagination.current || 1,
+      pageSize: pagination.pageSize || 10,
+    }));
+  };
+
+  // 处理搜索
+  const handleSearch = () => {
+    setPagination(prev => ({
+      ...prev,
+      current: 1, // 搜索时重置到第一页
+    }));
+    fetchUsers();
+  };
+
+  // 处理创建用户
+  const handleCreate = () => {
+    navigate('/users/create');
+  };
+
+  // 处理查看详情
+  const handleView = (id: number) => {
+    navigate(`/users/${id}`);
+  };
+
+  // 处理编辑
+  const handleEdit = (id: number) => {
+    navigate(`/users/${id}/edit`);
+  };
+
+  // 处理状态切换
+  const handleStatusChange = async (id: number, currentStatus: number) => {
+    try {
+      const newStatus = currentStatus === 1 ? 0 : 1;
+      // 状态映射表 - 将状态转换为数字状态码
+      const response = await usersClient[':id'].status.$post({
+        param: { id: id.toString() },
+        json: { status: newStatus }
+      });
+      
+      if (!response.ok) {
+        throw new Error('Failed to update user status');
+      }
+      
+      // 更新本地状态
+      setUsers(users.map(user => 
+        user.id === id ? { ...user, isDisabled: newStatus } : user
+      ));
+    } catch (error) {
+      logger.error('Error updating user status:', error);
+    }
+  };
+
+  // 表格列定义
+  const columns = [
+    {
+      title: '用户ID',
+      dataIndex: 'id',
+      key: 'id',
+      width: 80,
+    },
+    {
+      title: '用户名',
+      dataIndex: 'username',
+      key: 'username',
+    },
+    {
+      title: '姓名',
+      dataIndex: 'name',
+      key: 'name',
+      width: 120,
+    },
+    {
+      title: '邮箱',
+      dataIndex: 'email',
+      key: 'email',
+    },
+    {
+      title: '角色',
+      dataIndex: ['role', 'name'],
+      key: 'roleName',
+      width: 120,
+    },
+    {
+      title: '部门',
+      dataIndex: ['department', 'name'],
+      key: 'departmentName',
+      width: 120,
+    },
+    {
+      title: '状态',
+      key: 'status',
+      width: 100,
+      render: (_: any, record: UserItem) => (
+        <Switch 
+          checked={record.isDisabled === 0} 
+          checkedChildren="启用" 
+          unCheckedChildren="禁用"
+          onChange={() => handleStatusChange(record.id, record.isDisabled)}
+        />
+      ),
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createdAt',
+      key: 'createdAt',
+      width: 180,
+      render: (date: string) => new Date(date).toLocaleString(),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 120,
+      render: (_: any, record: UserItem) => (
+        <Dropdown
+          overlay={
+            <Menu>
+              <Menu.Item onClick={() => handleView(record.id)}>查看</Menu.Item>
+              <Menu.Item onClick={() => handleEdit(record.id)}>编辑</Menu.Item>
+            </Menu>
+          }
+        >
+          <Button type="text" icon={<MoreOutlined />} size="small" />
+        </Dropdown>
+      ),
+    },
+  ];
+
+  return (
+    <div className="page-container">
+      <div className="page-header">
+        <h1>用户管理</h1>
+        <Button 
+          type="primary" 
+          icon={<PlusOutlined />} 
+          onClick={handleCreate}
+        >
+          创建用户
+        </Button>
+      </div>
+      
+      <div className="page-search">
+        <Space.Compact style={{ width: '100%' }}>
+          <Input
+            placeholder="搜索用户..."
+            addonBefore={<SearchOutlined />}
+            value={searchText}
+            onChange={(e: ChangeEvent<HTMLInputElement>) => setSearchText(e.target.value)}
+            onPressEnter={handleSearch}
+          />
+          <Button type="primary" onClick={handleSearch}>搜索</Button>
+        </Space.Compact>
+      </div>
+      
+      <Table
+        columns={columns}
+        dataSource={users.map(user => ({ ...user, key: user.id }))}
+        rowKey="id"
+        loading={loading}
+        pagination={{
+          ...pagination,
+          showSizeChanger: true,
+          showTotal: (total: number) => `共 ${total} 条记录`,
+          pageSizeOptions: ['10', '20', '50'],
+        }}
+        onChange={handleTableChange}
+        size="middle"
+      />
+    </div>
+  );
+};
+
+export default UsersPage;

+ 67 - 0
src/client/router.tsx

@@ -0,0 +1,67 @@
+import React from 'react';
+import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
+import { logger } from './utils/logger';
+import MainLayout from './components/layout/MainLayout';
+import Login from './pages/login';
+import Dashboard from './pages/dashboard';
+import Customers from './pages/customers';
+import Contacts from './pages/contacts';
+import Opportunities from './pages/opportunities';
+
+// 验证用户是否已登录
+const isAuthenticated = () => {
+  const token = localStorage.getItem('token');
+  return !!token;
+};
+
+// 受保护的路由组件
+const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
+  if (!isAuthenticated()) {
+    logger.auth('未授权访问,重定向到登录页');
+    return <Navigate to="/login" replace />;
+  }
+  return <>{children}</>;
+};
+
+const router = createBrowserRouter([
+  {
+    path: '/login',
+    element: <Login />
+  },
+  {
+    path: '/',
+    element: (
+      <ProtectedRoute>
+        <MainLayout />
+      </ProtectedRoute>
+    ),
+    children: [
+      {
+        index: true,
+        element: <Dashboard />
+      },
+      {
+        path: 'customers',
+        element: <Customers />
+      },
+      {
+        path: 'contacts',
+        element: <Contacts />
+      },
+      {
+        path: 'opportunities',
+        element: <Opportunities />
+      }
+    ]
+  },
+  {
+    path: '*',
+    element: <Navigate to="/" replace />
+  }
+]);
+
+const AppRouter: React.FC = () => {
+  return <RouterProvider router={router} />;
+};
+
+export default AppRouter;

+ 102 - 0
src/client/store/index.ts

@@ -0,0 +1,102 @@
+import { createContext, useContext, useReducer, ReactNode } from 'react';
+import { logger } from '../utils/logger';
+
+// 定义状态类型
+interface AppState {
+  user: {
+    id: number | null;
+    username: string | null;
+    token: string | null;
+  };
+  loading: boolean;
+  error: string | null;
+}
+
+// 定义Action类型
+type Action = 
+  | { type: 'LOGIN_SUCCESS'; payload: { id: number; username: string; token: string } }
+  | { type: 'LOGOUT' }
+  | { type: 'SET_LOADING'; payload: boolean }
+  | { type: 'SET_ERROR'; payload: string | null };
+
+// 初始状态
+const initialState: AppState = {
+  user: {
+    id: null,
+    username: localStorage.getItem('username'),
+    token: localStorage.getItem('token')
+  },
+  loading: false,
+  error: null
+};
+
+// 创建Context
+const AppContext = createContext<{
+  state: AppState;
+  dispatch: React.Dispatch<Action>;
+} | undefined>(undefined);
+
+// Reducer函数
+function appReducer(state: AppState, action: Action): AppState {
+  switch (action.type) {
+    case 'LOGIN_SUCCESS':
+      logger.auth('User logged in successfully');
+      localStorage.setItem('token', action.payload.token);
+      localStorage.setItem('username', action.payload.username);
+      return {
+        ...state,
+        user: {
+          id: action.payload.id,
+          username: action.payload.username,
+          token: action.payload.token
+        },
+        loading: false,
+        error: null
+      };
+    case 'LOGOUT':
+      logger.auth('User logged out');
+      localStorage.removeItem('token');
+      localStorage.removeItem('username');
+      return {
+        ...state,
+        user: {
+          id: null,
+          username: null,
+          token: null
+        }
+      };
+    case 'SET_LOADING':
+      return {
+        ...state,
+        loading: action.payload
+      };
+    case 'SET_ERROR':
+      return {
+        ...state,
+        error: action.payload,
+        loading: false
+      };
+    default:
+      return state;
+  }
+}
+
+// Provider组件
+export function AppProvider({ children }: { children: ReactNode }) {
+  const [state, dispatch] = useReducer(appReducer, initialState);
+  
+  return (
+    <AppContext.Provider value={{ state, dispatch }}>
+      {children}
+    </AppContext.Provider>
+  );
+}
+
+// 自定义Hook,方便组件使用Context
+export function useAppContext() {
+  const context = useContext(AppContext);
+  if (context === undefined) {
+    throw new Error('useAppContext must be used within an AppProvider');
+  }
+  return context;
+}

+ 102 - 0
src/client/store/index.tsx

@@ -0,0 +1,102 @@
+import React, { createContext, useContext, useReducer, ReactNode, ReactElement } from 'react';
+import { logger } from '../utils/logger';
+
+// 定义状态类型
+interface AppState {
+  user: {
+    id: number | null;
+    username: string | null;
+    token: string | null;
+  };
+  loading: boolean;
+  error: string | null;
+}
+
+// 定义Action类型
+type Action = 
+  | { type: 'LOGIN_SUCCESS'; payload: { id: number; username: string; token: string } }
+  | { type: 'LOGOUT' }
+  | { type: 'SET_LOADING'; payload: boolean }
+  | { type: 'SET_ERROR'; payload: string | null };
+
+// 初始状态
+const initialState: AppState = {
+  user: {
+    id: null,
+    username: localStorage.getItem('username'),
+    token: localStorage.getItem('token')
+  },
+  loading: false,
+  error: null
+};
+
+// 创建Context
+const AppContext = createContext<{
+  state: AppState;
+  dispatch: React.Dispatch<Action>;
+} | undefined>(undefined);
+
+// Reducer函数
+function appReducer(state: AppState, action: Action): AppState {
+  switch (action.type) {
+    case 'LOGIN_SUCCESS':
+      logger.auth('User logged in successfully');
+      localStorage.setItem('token', action.payload.token);
+      localStorage.setItem('username', action.payload.username);
+      return {
+        ...state,
+        user: {
+          id: action.payload.id,
+          username: action.payload.username,
+          token: action.payload.token
+        },
+        loading: false,
+        error: null
+      };
+    case 'LOGOUT':
+      logger.auth('User logged out');
+      localStorage.removeItem('token');
+      localStorage.removeItem('username');
+      return {
+        ...state,
+        user: {
+          id: null,
+          username: null,
+          token: null
+        }
+      };
+    case 'SET_LOADING':
+      return {
+        ...state,
+        loading: action.payload
+      };
+    case 'SET_ERROR':
+      return {
+        ...state,
+        error: action.payload,
+        loading: false
+      };
+    default:
+      return state;
+  }
+}
+
+// Provider组件
+export function AppProvider({ children }: { children: ReactNode }): ReactElement {
+  const [state, dispatch] = useReducer(appReducer, initialState);
+  
+  return (
+    <AppContext.Provider value={{ state, dispatch }}>
+      {children}
+    </AppContext.Provider>
+  );
+}
+
+// 自定义Hook,方便组件使用Context
+export function useAppContext() {
+  const context = useContext(AppContext);
+  if (context === undefined) {
+    throw new Error('useAppContext must be used within an AppProvider');
+  }
+  return context;
+}

+ 14 - 0
src/client/utils/logger.ts

@@ -0,0 +1,14 @@
+import debug from 'debug';
+
+export const logger = {
+  error: debug('frontend:error'),
+  api: debug('frontend:api'),
+  auth: debug('frontend:auth'),
+  ui: debug('frontend:ui'),
+  info: debug('frontend:info')
+};
+
+// 开发环境默认启用所有日志
+if (import.meta.env.DEV) {
+  debug.enable('frontend:*');
+}

+ 11 - 0
src/client/vite-env.d.ts

@@ -0,0 +1,11 @@
+/// <reference types="vite/client" />
+
+interface ImportMetaEnv {
+  readonly VITE_API_URL: string;
+  readonly DEV: boolean;
+  // 更多环境变量...
+}
+
+interface ImportMeta {
+  readonly env: ImportMetaEnv;
+}

+ 21 - 10
src/server/api.ts

@@ -10,15 +10,26 @@ import opportunitiesRoute from '@/server/api/opportunities/index';
 import ticketsRoute from '@/server/api/tickets/index';
 import roleRoute from '@/server/api/roles/index';
 
-const api = new OpenAPIHono()
-  .route('/api/v1/contacts', contactsRoute)
-  .route('/api/v1/contracts', contractsRoute)
-  .route('/api/v1/customers', customersRoute)
-  .route('/api/v1/leads', leadsRoute)
-  .route('/api/v1/opportunities', opportunitiesRoute)
-  .route('/api/v1/tickets', ticketsRoute)
-  .route('/api/v1/roles', roleRoute)
-  .route('/api/v1/users', userRoute)
-  .route('/api/v1/campaigns', campaignsRoute);
+const api = new OpenAPIHono();
+const contactsRoutes = api.route('/api/v1/contacts', contactsRoute)
+const customersRoutes = api.route('/api/v1/customers', customersRoute)
+const leadsRoutes = api.route('/api/v1/leads', leadsRoute)
+const opportunitiesRoutes = api.route('/api/v1/opportunities', opportunitiesRoute)
+const ticketsRoutes = api.route('/api/v1/tickets', ticketsRoute)
+const roleRoutes = api.route('/api/v1/roles', roleRoute)
+const userRoutes = api.route('/api/v1/users', userRoute)
+const campaignsRoutes = api.route('/api/v1/campaigns', campaignsRoute)
+const departmentsRoutes = api.route('/api/v1/departments', departmentsRoute)
+
+export type ContactsRoutes = typeof contactsRoutes;
+export type CustomersRoutes = typeof customersRoutes
+export type LeadsRoutes = typeof leadsRoutes
+export type OpportunitiesRoutes = typeof opportunitiesRoutes
+export type TicketsRoutes = typeof ticketsRoutes
+export type RoleRoutes = typeof roleRoutes
+export type UserRoutes = typeof userRoutes
+export type CampaignsRoutes = typeof campaignsRoutes;
+export type DepartmentsRoutes = typeof departmentsRoutes;
+
 
 export default api;

+ 14 - 11
tsconfig.json

@@ -1,21 +1,24 @@
 {
   "compilerOptions": {
-    "target": "ES2020",
-    "module": "CommonJS",
-    "moduleResolution": "Node",
-    "outDir": "./dist",
-    "rootDir": "./src",
-    "strict": true,
-    "esModuleInterop": true,
+    "target": "ESNext",
+    "useDefineForClassFields": true,
+    "lib": ["DOM", "DOM.Iterable", "ESNext"],
+    "allowJs": false,
     "skipLibCheck": true,
+    "esModuleInterop": false,
+    "allowSyntheticDefaultImports": true,
+    "strict": true,
     "forceConsistentCasingInFileNames": true,
-    "experimentalDecorators": true,
-    "emitDecoratorMetadata": true,
+    "module": "ESNext",
+    "moduleResolution": "Node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "react-jsx",
     "baseUrl": ".",
     "paths": {
       "@/*": ["src/*"]
     }
   },
-  "include": ["src/**/*"],
-  "exclude": ["node_modules"]
+  "include": ["src"]
 }