This commit is contained in:
longdayi 2025-01-25 00:52:30 +08:00
parent 8981f75f21
commit 9f6df6d948
9 changed files with 31 additions and 352 deletions

View File

@ -1,16 +1,6 @@
import { SearchFilters } from "./SearchFilter";
import { useQueryClient } from '@tanstack/react-query';
export function Header() {
const handleSearch = (value: string) => {
};
const handleFilterChange = () => {
};
return (
<header className="bg-gradient-to-r from-primary to-primary-400 p-6">
<h1 className="text-3xl font-bold text-white"></h1>
@ -24,15 +14,7 @@ export function Header() {
<span></span>
</div>
</div>
<SearchFilters
className="mt-4"
searchTerm=""
filterCategory={null}
filterStatus={null}
onSearchChange={handleSearch}
onCategoryChange={handleFilterChange}
onStatusChange={handleFilterChange}
/>
</header>
);
}

View File

@ -1,167 +0,0 @@
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
import { useMemo } from 'react';
interface PaginationProps {
totalItems: number;
itemsPerPage: number;
currentPage?: number;
onPageChange?: (page: number) => void;
}
export function Pagination({
totalItems,
itemsPerPage,
currentPage = 1,
onPageChange = () => { },
}: PaginationProps) {
const STYLE_CONFIG = {
colors: {
primary: 'bg-primary', // USAF Blue
hover: 'hover:bg-[#00264d]', // Darker USAF Blue
disabled: 'bg-disabled',
text: {
primary: 'text-primary',
light: 'text-white',
secondary: 'text-tertiary-400'
}
},
components: {
container: `
flex items-center justify-between
px-4 py-3 sm:px-6
`,
pagination: `
inline-flex shadow-sm rounded-md
divide-x divide-gray-200
`,
button: `
relative inline-flex items-center justify-center
min-w-[2.5rem] h-10
text-sm font-medium
transition-colors duration-200
focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2
`
}
};
// Memoized calculations
const totalPages = useMemo(() => Math.ceil(totalItems / itemsPerPage), [totalItems, itemsPerPage]);
const startItem = (currentPage - 1) * itemsPerPage + 1;
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
// Navigation handlers
const handlePrevious = () => currentPage > 1 && onPageChange(currentPage - 1);
const handleNext = () => currentPage < totalPages && onPageChange(currentPage + 1);
// Generate page numbers with improved logic
const getPageNumbers = () => {
const maxVisiblePages = 5;
if (totalPages <= maxVisiblePages) {
return Array.from({ length: totalPages }, (_, i) => i + 1);
}
if (currentPage <= 3) {
return [1, 2, 3, '...', totalPages];
}
if (currentPage >= totalPages - 2) {
return [1, '...', totalPages - 2, totalPages - 1, totalPages];
}
return [
1,
'...',
currentPage - 1,
currentPage,
currentPage + 1,
'...',
totalPages
];
};
const renderPageButton = (pageNum: number | string, index: number) => {
if (pageNum === '...') {
return (
<span
key={`ellipsis-${index}`}
className={`
${STYLE_CONFIG.components.button}
${STYLE_CONFIG.colors.text.secondary}
bg-white
`}
>
&#8230;
</span>
);
}
const isCurrentPage = currentPage === pageNum;
const buttonStyle = isCurrentPage
? `
${STYLE_CONFIG.components.button}
${STYLE_CONFIG.colors.primary}
${STYLE_CONFIG.colors.text.light}
${STYLE_CONFIG.colors.hover}
`
: `
${STYLE_CONFIG.components.button}
bg-white
${STYLE_CONFIG.colors.text.primary}
hover:bg-gray-50
`;
return (
<button
key={pageNum}
onClick={() => onPageChange(pageNum as number)}
className={buttonStyle}
>
{pageNum}
</button>
);
};
return (
<div className={STYLE_CONFIG.components.container}>
<div className="flex-1 flex items-center justify-between">
<div>
<p className={STYLE_CONFIG.colors.text.secondary}>
Showing <span className="font-semibold">{startItem}</span> to{' '}
<span className="font-semibold">{endItem}</span> of{' '}
<span className="font-semibold">{totalItems}</span> results
</p>
</div>
<nav className="relative z-0 inline-flex">
<div className={STYLE_CONFIG.components.pagination}>
<button
onClick={handlePrevious}
disabled={currentPage === 1}
className={`
${STYLE_CONFIG.components.button}
rounded-l-md
${currentPage === 1 ? 'opacity-50 cursor-not-allowed' : ''}
bg-white
${STYLE_CONFIG.colors.text.primary}
hover:bg-gray-50
`}
>
<ChevronLeftIcon className="h-5 w-5" />
</button>
{getPageNumbers().map(renderPageButton)}
<button
onClick={handleNext}
disabled={currentPage === totalPages}
className={`
${STYLE_CONFIG.components.button}
rounded-r-md
${currentPage === totalPages ? 'opacity-50 cursor-not-allowed' : ''}
bg-white
${STYLE_CONFIG.colors.text.primary}
hover:bg-gray-50
`}
>
<ChevronRightIcon className="h-5 w-5" />
</button>
</div>
</nav>
</div>
</div>
);
}

View File

@ -1,90 +0,0 @@
import { SearchOutlined } from '@ant-design/icons';
import { Form, Input, Select, Spin } from 'antd';
import { useEffect } from 'react';
interface SearchFiltersProps {
searchTerm: string;
onSearchChange: (value: string) => void;
filterCategory: string;
onCategoryChange: (value: string) => void;
filterStatus: string;
onStatusChange: (value: string) => void;
className?: string;
isLoading?: boolean;
}
const LoadingIndicator = () => (
<Spin size="small" className="ml-2" />
);
export function SearchFilters({
searchTerm,
onSearchChange,
filterCategory,
onCategoryChange,
filterStatus,
onStatusChange,
className,
isLoading = false
}: SearchFiltersProps) {
const [form] = Form.useForm();
// 统一处理表单初始值
const initialValues = {
search: searchTerm,
category: filterCategory,
status: filterStatus
};
useEffect(() => {
form.setFieldsValue(initialValues);
}, [searchTerm, filterCategory, filterStatus, form]);
return (
<Form
form={form}
layout="vertical"
className={className}
initialValues={initialValues}
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Form.Item name="search" noStyle>
<Input
prefix={<SearchOutlined />}
placeholder="搜索关键词、发件人或单位..."
onChange={(e) => onSearchChange(e.target.value)}
allowClear
suffix={isLoading ? <LoadingIndicator /> : null}
/>
</Form.Item>
<Form.Item name="category" noStyle>
<Select
className="w-full"
onChange={onCategoryChange}
options={[
{ value: 'all', label: '所有分类' },
{ value: 'complaint', label: '投诉' },
{ value: 'suggestion', label: '建议' },
{ value: 'request', label: '请求' },
{ value: 'feedback', label: '反馈' }
]}
/>
</Form.Item>
<Form.Item name="status" noStyle>
<Select
className="w-full"
onChange={onStatusChange}
options={[
{ value: 'all', label: '所有状态' },
{ value: 'pending', label: '待处理' },
{ value: 'in-progress', label: '处理中' },
{ value: 'resolved', label: '已解决' }
]}
/>
</Form.Item>
</div>
</Form>
);
}

View File

@ -1,37 +1,14 @@
import { letters } from "./constants";
import { Header } from "./Header";
import { LetterCard } from "./LetterCard";
import { Pagination } from "./Pagination";
import { SearchFilters } from "./SearchFilter";
import { useLetterFilters } from "./useLetterFilters";
export default function LetterListPage() {
const {
searchTerm,
setSearchTerm,
filterCategory,
setFilterCategory,
filterStatus,
setFilterStatus,
filteredLetters,
} = useLetterFilters(letters);
return (
// 添加 flex flex-col 使其成为弹性布局容器
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-slate-100 flex flex-col">
<Header />
{/* 添加 flex-grow 使内容区域自动填充剩余空间 */}
<div className="flex-grow flex flex-col gap-2">
{filteredLetters.map((letter) => (
<LetterCard key={letter.id} letter={letter} />
))}
</div>
{/* Pagination 会自然固定在底部 */}
<Pagination
totalItems={filteredLetters.length}
itemsPerPage={2}
/>
</div>
);
}

View File

@ -1,33 +0,0 @@
import { useState, useMemo } from 'react';
import { Letter } from './types';
export function useLetterFilters(letters: Letter[]) {
const [currentPage, setCurrentPage] = useState(1);
const [searchTerm, setSearchTerm] = useState('');
const [filterStatus, setFilterStatus] = useState<string>('all');
const [filterCategory, setFilterCategory] = useState<string>('all');
const filteredLetters = useMemo(() => {
return letters.filter(letter => {
const matchesSearch = [letter.title, letter.sender, letter.unit]
.some(field => field.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesCategory = filterCategory === 'all' || letter.category === filterCategory;
const matchesStatus = filterStatus === 'all' || letter.status === filterStatus;
return matchesSearch && matchesCategory && matchesStatus;
});
}, [letters, searchTerm, filterCategory, filterStatus]);
return {
currentPage,
setCurrentPage,
searchTerm,
setSearchTerm,
filterStatus,
setFilterStatus,
filterCategory,
setFilterCategory,
filteredLetters,
};
}

View File

@ -1,14 +1,14 @@
import { EyeOutlined, LikeOutlined, LikeFilled, UserOutlined, BankOutlined, CalendarOutlined, FileTextOutlined } from '@ant-design/icons';
import { Button, Typography, Space, Tooltip } from 'antd';
import toast from 'react-hot-toast';
import { Letter } from './types';
import { getBadgeStyle } from './utils';
import { useState } from 'react';
import { getBadgeStyle } from '@web/src/app/main/letter/list/utils';
import { PostDto } from '@nice/common';
import dayjs from 'dayjs';
const { Title, Paragraph, Text } = Typography;
interface LetterCardProps {
letter: Letter;
letter: PostDto;
}
export function LetterCard({ letter }: LetterCardProps) {
@ -54,9 +54,7 @@ export function LetterCard({ letter }: LetterCardProps) {
{letter.title}
</a>
</Title>
{letter.priority && (
<Badge type="priority" value={letter.priority} className="ml-2" />
)}
</div>
{/* Meta Info */}
@ -64,17 +62,17 @@ export function LetterCard({ letter }: LetterCardProps) {
<Space size="middle">
<Space>
<UserOutlined className="text-secondary-400" />
<Text strong>{letter.sender}</Text>
<Text strong>{letter.author.showname}</Text>
</Space>
<Text type="secondary">|</Text>
<Space>
<BankOutlined className="text-secondary-400" />
<Text>{letter.unit}</Text>
<Text>{letter.author.department.name}</Text>
</Space>
</Space>
<Space>
<CalendarOutlined className="text-secondary-400" />
<Text type="secondary">{letter.date}</Text>
<Text type="secondary">{dayjs(letter.createdAt).format('YYYY-MM-DD')}</Text>
</Space>
</div>
@ -94,8 +92,8 @@ export function LetterCard({ letter }: LetterCardProps) {
{/* Badges & Interactions */}
<div className="flex justify-between items-center">
<Space size="small" wrap className="flex-1">
<Badge type="category" value={letter.category} />
<Badge type="status" value={letter.status} />
<Badge type="category" value={'11'} />
<Badge type="status" value={'22'} />
</Space>
<div className="flex items-center gap-4">

View File

@ -0,0 +1,18 @@
import { api, RouterInputs } from "@nice/client";
import { Prisma } from "packages/common/dist";
import { LetterCard } from "../LetterCard";
export default function LetterList({ params }: { params: RouterInputs["post"]["findManyWithPagination"] }) {
const { data, isLoading } = api.post.findManyWithPagination.useQuery({
page: 1,
pageSize: 1,
where: {},
select: {}
})
return <div className="flex-grow flex flex-col gap-2">
{data?.items.map((letter: any) => (
<LetterCard key={letter.id} letter={letter} />
))}
</div>
}

View File

@ -1,5 +0,0 @@
import { api } from "@nice/client";
export default function LetterList(){
}

View File

@ -9,7 +9,6 @@ export function useStaff() {
const getStaff = (key: string) => {
return findQueryData<Staff>(queryClient, api.staff, key);
};
return {
...useEntity("staff", {
create: {