|
@@ -0,0 +1,326 @@
|
|
|
|
|
+import { useState, useEffect } from 'react';
|
|
|
|
|
+import { useNavigate } from 'react-router-dom';
|
|
|
|
|
+import { hc } from 'hono/client';
|
|
|
|
|
+import type { InferRequestType, InferResponseType } from 'hono/client';
|
|
|
|
|
+import { groupsClient } from '@/client/api';
|
|
|
|
|
+import { logger } from '@/client/utils/logger';
|
|
|
|
|
+
|
|
|
|
|
+// 定义类型
|
|
|
|
|
+type Group = InferResponseType<typeof groupsClient.$get, 200>['data'][0];
|
|
|
|
|
+type GroupListResponse = InferResponseType<typeof groupsClient.$get, 200>;
|
|
|
|
|
+type GroupDetailResponse = InferResponseType<typeof groupsClient[':id']['$get'], 200>;
|
|
|
|
|
+type CreateGroupRequest = InferRequestType<typeof groupsClient.$post>['json'];
|
|
|
|
|
+type UpdateGroupRequest = InferRequestType<typeof groupsClient[':id']['$put']>['json'];
|
|
|
|
|
+
|
|
|
|
|
+// 定义表单类型
|
|
|
|
|
+interface GroupFormData {
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ description?: string;
|
|
|
|
|
+ // 可以根据实际需求添加更多字段
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const Groups = () => {
|
|
|
|
|
+ const navigate = useNavigate();
|
|
|
|
|
+ const [groups, setGroups] = useState<Group[]>([]);
|
|
|
|
|
+ const [loading, setLoading] = useState(true);
|
|
|
|
|
+ const [error, setError] = useState<string | null>(null);
|
|
|
|
|
+ const [currentPage, setCurrentPage] = useState(1);
|
|
|
|
|
+ const [pageSize, setPageSize] = useState(10);
|
|
|
|
|
+ const [total, setTotal] = useState(0);
|
|
|
|
|
+ const [isModalOpen, setIsModalOpen] = useState(false);
|
|
|
|
|
+ const [isEditing, setIsEditing] = useState(false);
|
|
|
|
|
+ const [currentGroup, setCurrentGroup] = useState<Group | null>(null);
|
|
|
|
|
+ const [formData, setFormData] = useState<GroupFormData>({
|
|
|
|
|
+ name: '',
|
|
|
|
|
+ description: ''
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 获取分组列表
|
|
|
|
|
+ const fetchGroups = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ const response = await groupsClient.$get({
|
|
|
|
|
+ query: {
|
|
|
|
|
+ page: currentPage,
|
|
|
|
|
+ pageSize: pageSize
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error('Failed to fetch groups');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data: GroupListResponse = await response.json();
|
|
|
|
|
+ setGroups(data.data);
|
|
|
|
|
+ setTotal(data.pagination.total);
|
|
|
|
|
+ setError(null);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ logger.error('Error fetching groups:', err);
|
|
|
|
|
+ setError(err instanceof Error ? err.message : 'Failed to load groups');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 组件挂载时获取数据
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ fetchGroups();
|
|
|
|
|
+ }, [currentPage, pageSize]);
|
|
|
|
|
+
|
|
|
|
|
+ // 处理表单变化
|
|
|
|
|
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
|
|
|
+ const { name, value } = e.target;
|
|
|
|
|
+ setFormData(prev => ({ ...prev, [name]: value }));
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 打开创建模态框
|
|
|
|
|
+ const openCreateModal = () => {
|
|
|
|
|
+ setIsEditing(false);
|
|
|
|
|
+ setCurrentGroup(null);
|
|
|
|
|
+ setFormData({ name: '', description: '' });
|
|
|
|
|
+ setIsModalOpen(true);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 打开编辑模态框
|
|
|
|
|
+ const openEditModal = async (group: Group) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ const response = await groupsClient[':id']['$get']({
|
|
|
|
|
+ param: { id: group.id.toString() }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error('Failed to fetch group details');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data: GroupDetailResponse = await response.json();
|
|
|
|
|
+ setCurrentGroup(data.data);
|
|
|
|
|
+ setFormData({
|
|
|
|
|
+ name: data.data.name,
|
|
|
|
|
+ description: data.data.description || ''
|
|
|
|
|
+ });
|
|
|
|
|
+ setIsEditing(true);
|
|
|
|
|
+ setIsModalOpen(true);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ logger.error('Error fetching group details:', err);
|
|
|
|
|
+ setError(err instanceof Error ? err.message : 'Failed to load group details');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 关闭模态框
|
|
|
|
|
+ const closeModal = () => {
|
|
|
|
|
+ setIsModalOpen(false);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 提交表单 (创建或更新分组)
|
|
|
|
|
+ const handleSubmit = async (e: React.FormEvent) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ try {
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+
|
|
|
|
|
+ if (isEditing && currentGroup) {
|
|
|
|
|
+ // 更新分组
|
|
|
|
|
+ const response = await groupsClient[':id']['$put']({
|
|
|
|
|
+ param: { id: currentGroup.id.toString() },
|
|
|
|
|
+ json: formData as UpdateGroupRequest
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error('Failed to update group');
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 创建新分组
|
|
|
|
|
+ const response = await groupsClient.$post({
|
|
|
|
|
+ json: formData as CreateGroupRequest
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error('Failed to create group');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ closeModal();
|
|
|
|
|
+ fetchGroups(); // 重新获取列表
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ logger.error('Error saving group:', err);
|
|
|
|
|
+ setError(err instanceof Error ? err.message : 'Failed to save group');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 删除分组
|
|
|
|
|
+ const handleDelete = async (id: number) => {
|
|
|
|
|
+ if (!confirm('Are you sure you want to delete this group?')) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ const response = await groupsClient[':id']['$delete']({
|
|
|
|
|
+ param: { id: id.toString() }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error('Failed to delete group');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fetchGroups(); // 重新获取列表
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ logger.error('Error deleting group:', err);
|
|
|
|
|
+ setError(err instanceof Error ? err.message : 'Failed to delete group');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="container mx-auto py-6">
|
|
|
|
|
+ <div className="flex justify-between items-center mb-6">
|
|
|
|
|
+ <h1 className="text-2xl font-bold">Groups Management</h1>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={openCreateModal}
|
|
|
|
|
+ className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
|
|
|
|
+ >
|
|
|
|
|
+ Create New Group
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {error && (
|
|
|
|
|
+ <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
|
|
|
|
+ {error}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ <div className="bg-white shadow-md rounded my-6">
|
|
|
|
|
+ <table className="min-w-full">
|
|
|
|
|
+ <thead>
|
|
|
|
|
+ <tr className="bg-gray-100">
|
|
|
|
|
+ <th className="py-3 px-4 text-left">ID</th>
|
|
|
|
|
+ <th className="py-3 px-4 text-left">Name</th>
|
|
|
|
|
+ <th className="py-3 px-4 text-left">Description</th>
|
|
|
|
|
+ <th className="py-3 px-4 text-left">Actions</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody>
|
|
|
|
|
+ {loading ? (
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <td colSpan={4} className="py-3 px-4 text-center">Loading...</td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ ) : groups.length === 0 ? (
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <td colSpan={4} className="py-3 px-4 text-center">No groups found</td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ groups.map(group => (
|
|
|
|
|
+ <tr key={group.id} className="border-b">
|
|
|
|
|
+ <td className="py-3 px-4">{group.id}</td>
|
|
|
|
|
+ <td className="py-3 px-4">{group.name}</td>
|
|
|
|
|
+ <td className="py-3 px-4">{group.description || '-'}</td>
|
|
|
|
|
+ <td className="py-3 px-4">
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => openEditModal(group)}
|
|
|
|
|
+ className="bg-green-500 hover:bg-green-700 text-white py-1 px-2 rounded mr-2 text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ Edit
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => handleDelete(group.id)}
|
|
|
|
|
+ className="bg-red-500 hover:bg-red-700 text-white py-1 px-2 rounded text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ Delete
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ ))
|
|
|
|
|
+ )}
|
|
|
|
|
+ </tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Pagination controls */}
|
|
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ Showing {Math.min((currentPage - 1) * pageSize + 1, total)} to {Math.min(currentPage * pageSize, total)} of {total} groups
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex space-x-2">
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
|
|
|
|
|
+ disabled={currentPage === 1 || loading}
|
|
|
|
|
+ className="px-3 py-1 border rounded disabled:opacity-50"
|
|
|
|
|
+ >
|
|
|
|
|
+ Previous
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setCurrentPage(prev => prev + 1)}
|
|
|
|
|
+ disabled={currentPage * pageSize >= total || loading}
|
|
|
|
|
+ className="px-3 py-1 border rounded disabled:opacity-50"
|
|
|
|
|
+ >
|
|
|
|
|
+ Next
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Group Modal */}
|
|
|
|
|
+ {isModalOpen && (
|
|
|
|
|
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4">
|
|
|
|
|
+ <div className="bg-white rounded-lg shadow-lg w-full max-w-md">
|
|
|
|
|
+ <div className="p-6">
|
|
|
|
|
+ <h2 className="text-xl font-bold mb-4">{isEditing ? 'Edit Group' : 'Create New Group'}</h2>
|
|
|
|
|
+
|
|
|
|
|
+ <form onSubmit={handleSubmit}>
|
|
|
|
|
+ <div className="mb-4">
|
|
|
|
|
+ <label className="block text-gray-700 text-sm font-bold mb-2">
|
|
|
|
|
+ Group Name
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ name="name"
|
|
|
|
|
+ value={formData.name}
|
|
|
|
|
+ onChange={handleInputChange}
|
|
|
|
|
+ required
|
|
|
|
|
+ className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="mb-6">
|
|
|
|
|
+ <label className="block text-gray-700 text-sm font-bold mb-2">
|
|
|
|
|
+ Description
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <textarea
|
|
|
|
|
+ name="description"
|
|
|
|
|
+ value={formData.description}
|
|
|
|
|
+ onChange={handleInputChange}
|
|
|
|
|
+ className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
|
|
|
+ rows={3}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="flex justify-end space-x-3">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={closeModal}
|
|
|
|
|
+ className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
|
|
|
|
|
+ >
|
|
|
|
|
+ Cancel
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ disabled={loading}
|
|
|
|
|
+ className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline disabled:opacity-50"
|
|
|
|
|
+ >
|
|
|
|
|
+ {loading ? 'Saving...' : (isEditing ? 'Update Group' : 'Create Group')}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+export default Groups;
|