Files
leaudit-platform-frontend/app/routes/contract-template.search.results.tsx
T

326 lines
9.3 KiB
TypeScript

import type { MetaFunction, LoaderFunctionArgs } from '@remix-run/node';
import { useLoaderData, useSearchParams, useNavigate } from '@remix-run/react';
import { useState } from 'react';
import { CompactSearchBox } from '~/components/contract-template/CompactSearchBox';
import { SearchResultHeader } from '~/components/contract-template/SearchResultHeader';
import { FilterTabs } from '~/components/contract-template/FilterTabs';
import { TemplateGrid } from '~/components/contract-template/TemplateGrid';
import { Pagination } from '~/components/ui/Pagination';
import { searchContractTemplates, getContractCategories } from '~/api/contract-template/templates';
import type { ContractTemplate, ContractCategory } from '~/api/contract-template/templates';
import styles from '~/styles/pages/contract-template.css?url';
import { getUserSession } from '~/api/login/auth.server';
export const links = () => [
{ rel: 'stylesheet', href: styles }
];
export const meta: MetaFunction = () => {
return [
{ title: '搜索结果 - AI智能合同模板搜索 - 智慧法务' },
{
name: 'description',
content: 'AI智能搜索合同模板结果,快速找到最适合的模板。'
}
];
};
// 面包屑导航配置
export const handle = {
breadcrumb: "搜索结果"
};
// 带搜索统计的分类类型
interface CategoryWithSearchCount extends ContractCategory {
searchCount: number;
}
// 前端显示的模板类型
interface DisplayTemplate {
id: string;
title: string;
// type: string;
description: string;
updateTime: string;
// useCount: number;
// rating: number;
category: string;
file_path?: string;
file_format?: string;
}
// 将数据库模板转换为前端显示格式
function transformTemplate(template: ContractTemplate) {
// 模拟使用次数和评分(实际项目中可以从其他表获取)
/* const mockUsageCount = Math.floor(Math.random() * 2000) + 100;
const mockRating = (Math.random() * 1.5 + 3.5).toFixed(1); */
return {
id: template.id.toString(),
title: template.title,
// type: template.is_featured ? '推荐版' : '标准版',
description: template.description || '',
updateTime: new Date(template.updated_at).toLocaleDateString('zh-CN'),
// useCount: mockUsageCount,
// rating: parseFloat(mockRating),
category: template.category?.name || '其他',
file_path: template.file_path,
file_format: template.file_format
};
}
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const query = url.searchParams.get('q') || '';
const category = url.searchParams.get('category') || '';
const sortBy = url.searchParams.get('sortBy') || 'relevance';
const page = parseInt(url.searchParams.get('page') || '1');
const pageSize = 6;
// 记录搜索开始时间
const startTime = Date.now();
// 根据sortBy值设置数据库排序参数
let dbSortBy = 'id';
let dbSortOrder: 'asc' | 'desc' = 'asc';
switch (sortBy) {
case 'relevance':
dbSortBy = 'id';
dbSortOrder = 'asc';
break;
case 'newest':
dbSortBy = 'updated_at';
dbSortOrder = 'desc';
break;
/* case 'popular':
dbSortBy = 'created_at';
dbSortOrder = 'desc';
break;
case 'rating':
dbSortBy = 'is_featured';
dbSortOrder = 'desc';
break; */
default:
dbSortBy = 'id';
dbSortOrder = 'asc';
}
// 获取 JWT
const { frontendJWT } = await getUserSession(request);
const jwt = frontendJWT || undefined;
try {
// 并行获取搜索结果和分类数据
const [searchResponse, categoriesResponse] = await Promise.all([
searchContractTemplates(query, {
category,
page,
pageSize,
sortBy: dbSortBy,
sortOrder: dbSortOrder,
token: jwt
}),
getContractCategories(jwt)
]);
// 处理搜索结果
if (searchResponse.error) {
console.error('搜索合同模板失败:', searchResponse.error);
return {
results: [],
query,
category,
total: 0,
page,
pageSize,
sortBy,
searchTime: '搜索失败',
categories: []
};
}
// 处理分类数据
const categories = categoriesResponse.error ? [] : categoriesResponse.data || [];
// 转换模板数据格式
const transformedResults = searchResponse.data?.templates.map(transformTemplate) || [];
// 为每个分类获取搜索结果统计
let categoriesWithSearchCount: CategoryWithSearchCount[] = [];
if (query && query.trim()) {
// 并行为每个分类获取搜索结果数量
const categorySearchPromises = categories.map(async (cat): Promise<CategoryWithSearchCount> => {
try {
const categorySearchResponse = await searchContractTemplates(query, {
category: cat.name,
page: 1,
pageSize: 1000, // 设置较大的pageSize来获取总数
token: jwt
});
const count = categorySearchResponse.error ? 0 : (categorySearchResponse.data?.total || 0);
return {
...cat,
searchCount: count
};
} catch (error) {
console.error(`获取分类${cat.name}的搜索统计失败:`, error);
return {
...cat,
searchCount: 0
};
}
});
categoriesWithSearchCount = await Promise.all(categorySearchPromises);
} else {
// 如果没有搜索关键词,searchCount设为0
categoriesWithSearchCount = categories.map(cat => ({
...cat,
searchCount: 0
}));
}
// 计算搜索耗时
const endTime = Date.now();
const searchTime = (endTime - startTime) / 1000;
const searchTimeText = `搜索用时 ${searchTime.toFixed(1)}`;
return {
results: transformedResults,
query,
category,
total: searchResponse.data?.total || 0,
page,
pageSize,
sortBy,
searchTime: searchTimeText,
categories: categoriesWithSearchCount
};
} catch (error) {
console.error('加载搜索结果失败:', error);
return {
results: [],
query,
category,
total: 0,
page,
pageSize,
sortBy,
searchTime: '搜索失败',
categories: []
};
}
}
export default function ContractTemplateSearchResults() {
const { results, query, total, page, pageSize, sortBy, searchTime, categories }: {
results: DisplayTemplate[];
query: string;
total: number;
page: number;
pageSize: number;
sortBy: string;
searchTime: string;
categories: CategoryWithSearchCount[];
} = useLoaderData<typeof loader>();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [activeFilter, setActiveFilter] = useState('全部');
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const handleSearch = (newQuery: string) => {
if (newQuery.trim()) {
navigate(`/contract-template/search/results?q=${encodeURIComponent(newQuery)}`);
}
};
const handleTemplateClick = (templateId: string) => {
navigate(`/contract-template/detail/${templateId}`);
};
const handleFilterChange = (filter: string) => {
setActiveFilter(filter);
const params = new URLSearchParams(searchParams);
if (filter === '全部') {
params.delete('category');
} else {
params.set('category', filter);
}
params.delete('page'); // 重置页码
navigate(`/contract-template/search/results?${params.toString()}`);
};
const handlePageChange = (newPage: number) => {
const params = new URLSearchParams(searchParams);
params.set('page', newPage.toString());
navigate(`/contract-template/search/results?${params.toString()}`);
};
const handleSortChange = (newSort: string) => {
const params = new URLSearchParams(searchParams);
params.set('sortBy', newSort);
params.delete('page'); // 重置页码
navigate(`/contract-template/search/results?${params.toString()}`);
};
// 动态生成筛选选项
const filters = [
{ label: '全部', count: total },
...categories.map(cat => ({
label: cat.name,
count: cat.searchCount || 0
}))
];
const totalPages = Math.ceil(total / pageSize);
return (
<div className="contract-search-results">
{/* 紧凑搜索框 */}
<CompactSearchBox
initialQuery={query}
onSearch={handleSearch}
searchTime={searchTime}
/>
{/* 搜索结果头部 */}
<SearchResultHeader
total={total}
viewMode={viewMode}
onViewModeChange={setViewMode}
sortBy={sortBy}
onSortChange={handleSortChange}
/>
{/* 筛选标签 */}
<FilterTabs
filters={filters}
activeFilter={activeFilter}
onFilterChange={handleFilterChange}
/>
{/* 模板网格 */}
<TemplateGrid
templates={results}
viewMode={viewMode}
onTemplateClick={handleTemplateClick}
/>
{/* 分页 */}
{totalPages > 1 && (
<Pagination
currentPage={page}
total={total}
pageSize={pageSize}
onChange={handlePageChange}
showPageSizeChanger={false}
/>
)}
</div>
);
}