refactor(routes): split expression.tsx into modular expression/ directory

- Split 1212-line monolithic file into 6 modular files
- Extracted types, dialogs, list view into separate modules
- Main index.tsx (468 lines) handles state management and API integration
- All CRUD operations, stats, review, search, pagination preserved
- Build passes with zero TypeScript errors (3.77s)
This commit is contained in:
DrSmoothl
2026-03-01 20:23:53 +08:00
parent 7866443c9c
commit bacb5b19cd
6 changed files with 1437 additions and 1211 deletions

View File

@@ -0,0 +1,361 @@
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Edit, Eye, Trash2 } from 'lucide-react'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { useToast } from '@/hooks/use-toast'
import type { Expression } from '@/types/expression'
/**
* 表达方式列表组件桌面端Table + 移动端Card视图 + 分页)
*/
export function ExpressionList({
expressions,
loading,
total,
page,
pageSize,
selectedIds,
chatNameMap,
onEdit,
onViewDetail,
onDelete,
onToggleSelect,
onToggleSelectAll,
onPageChange,
onJumpToPage,
}: {
expressions: Expression[]
loading: boolean
total: number
page: number
pageSize: number
selectedIds: Set<number>
chatNameMap: Map<string, string>
onEdit: (expression: Expression) => void
onViewDetail: (expression: Expression) => void
onDelete: (expression: Expression) => void
onToggleSelect: (id: number) => void
onToggleSelectAll: () => void
onPageChange: (newPage: number) => void
onJumpToPage: (targetPage: string) => void
}) {
const { toast } = useToast()
const getChatName = (chatId: string): string => {
return chatNameMap.get(chatId) || chatId
}
const totalPages = Math.ceil(total / pageSize)
const handleJumpToPage = (jumpToPage: string) => {
const targetPage = parseInt(jumpToPage)
if (targetPage >= 1 && targetPage <= totalPages) {
onJumpToPage(jumpToPage)
} else {
toast({
title: '无效的页码',
description: `请输入1-${totalPages}之间的页码`,
variant: 'destructive',
})
}
}
return (
<div className="rounded-lg border bg-card">
{/* 桌面端表格视图 */}
<div className="hidden md:block">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={selectedIds.size === expressions.length && expressions.length > 0}
onCheckedChange={onToggleSelectAll}
/>
</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
...
</TableCell>
</TableRow>
) : expressions.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
</TableCell>
</TableRow>
) : (
expressions.map((expression) => (
<TableRow key={expression.id}>
<TableCell>
<Checkbox
checked={selectedIds.has(expression.id)}
onCheckedChange={() => onToggleSelect(expression.id)}
/>
</TableCell>
<TableCell className="font-medium max-w-xs truncate">
{expression.situation}
</TableCell>
<TableCell className="max-w-xs truncate">{expression.style}</TableCell>
<TableCell
className="max-w-[200px] truncate"
title={getChatName(expression.chat_id)}
style={{ wordBreak: 'keep-all' }}
>
<span className="whitespace-nowrap overflow-hidden text-ellipsis block">
{getChatName(expression.chat_id)}
</span>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="default"
size="sm"
onClick={() => onEdit(expression)}
>
<Edit className="h-4 w-4 mr-1" />
</Button>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => onViewDetail(expression)}
title="查看详情"
>
<Eye className="h-4 w-4" />
</Button>
<Button
size="sm"
onClick={() => onDelete(expression)}
className="bg-red-600 hover:bg-red-700 text-white"
>
<Trash2 className="h-4 w-4 mr-1" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* 移动端卡片视图 */}
<div className="md:hidden space-y-3 p-4">
{loading ? (
<div className="text-center py-8 text-muted-foreground">
...
</div>
) : expressions.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
</div>
) : (
expressions.map((expression) => (
<div key={expression.id} className="rounded-lg border bg-card p-4 space-y-3 overflow-hidden">
{/* 复选框和情境 */}
<div className="flex items-start gap-3">
<Checkbox
checked={selectedIds.has(expression.id)}
onCheckedChange={() => onToggleSelect(expression.id)}
className="mt-1"
/>
<div className="min-w-0 flex-1 overflow-hidden space-y-2">
<div>
<div className="text-xs text-muted-foreground mb-1"></div>
<h3 className="font-semibold text-sm line-clamp-2 w-full break-all" title={expression.situation}>
{expression.situation}
</h3>
</div>
<div>
<div className="text-xs text-muted-foreground mb-1"></div>
<p className="text-sm line-clamp-2 w-full break-all" title={expression.style}>
{expression.style}
</p>
</div>
</div>
</div>
{/* 聊天名称 */}
<div className="text-sm">
<div className="text-xs text-muted-foreground mb-1"></div>
<p
className="text-sm truncate"
title={getChatName(expression.chat_id)}
style={{ wordBreak: 'keep-all' }}
>
{getChatName(expression.chat_id)}
</p>
</div>
{/* 操作按钮 */}
<div className="flex flex-wrap gap-1 pt-2 border-t overflow-hidden">
<Button
variant="outline"
size="sm"
onClick={() => onEdit(expression)}
className="text-xs px-2 py-1 h-auto flex-shrink-0"
>
<Edit className="h-3 w-3 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onViewDetail(expression)}
className="text-xs px-2 py-1 h-auto flex-shrink-0"
>
<Eye className="h-3 w-3" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onDelete(expression)}
className="text-xs px-2 py-1 h-auto flex-shrink-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-3 w-3 mr-1" />
</Button>
</div>
</div>
))
)}
</div>
{/* 分页 */}
{total > 0 && (
<Pagination
total={total}
page={page}
pageSize={pageSize}
onPageChange={onPageChange}
onJumpToPage={handleJumpToPage}
/>
)}
</div>
)
}
/**
* 分页组件
*/
function Pagination({
total,
page,
pageSize,
onPageChange,
onJumpToPage,
}: {
total: number
page: number
pageSize: number
onPageChange: (newPage: number) => void
onJumpToPage: (targetPage: string) => void
}) {
const [jumpToPage, setJumpToPage] = useState('')
const totalPages = Math.ceil(total / pageSize)
const handleJump = () => {
if (jumpToPage) {
onJumpToPage(jumpToPage)
setJumpToPage('')
}
}
return (
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 px-4 py-3 border-t">
<div className="text-sm text-muted-foreground">
{total} {page} / {totalPages}
</div>
<div className="flex items-center gap-2">
{/* 首页 */}
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(1)}
disabled={page === 1}
className="hidden sm:flex"
>
<ChevronsLeft className="h-4 w-4" />
</Button>
{/* 上一页 */}
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(page - 1)}
disabled={page === 1}
>
<ChevronLeft className="h-4 w-4 sm:mr-1" />
<span className="hidden sm:inline"></span>
</Button>
{/* 页码跳转 */}
<div className="flex items-center gap-2">
<Input
type="number"
value={jumpToPage}
onChange={(e) => setJumpToPage(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleJump()}
placeholder={page.toString()}
className="w-16 h-8 text-center"
min={1}
max={totalPages}
/>
<Button
variant="outline"
size="sm"
onClick={handleJump}
disabled={!jumpToPage}
className="h-8"
>
</Button>
</div>
{/* 下一页 */}
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(page + 1)}
disabled={page >= totalPages}
>
<span className="hidden sm:inline"></span>
<ChevronRight className="h-4 w-4 sm:ml-1" />
</Button>
{/* 末页 */}
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(totalPages)}
disabled={page >= totalPages}
className="hidden sm:flex"
>
<ChevronsRight className="h-4 w-4" />
</Button>
</div>
</div>
)
}