数据列表查询对接完成
This commit is contained in:
@@ -177,7 +177,7 @@ export async function getContractTemplates(searchParams: TemplateSearchParams =
|
|||||||
|
|
||||||
// 构建查询参数
|
// 构建查询参数
|
||||||
const params: PostgrestParams = {
|
const params: PostgrestParams = {
|
||||||
select: 'id,template_code,title,category_id,description,file_format,is_featured,created_at,updated_at,contract_categories(id,name,icon)',
|
select: 'id,template_code,title,category_id,description,file_path,file_format,is_featured,created_at,updated_at,pdf_file_path,contract_categories(id,name,icon,description)',
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
offset: (page - 1) * pageSize,
|
offset: (page - 1) * pageSize,
|
||||||
order: `${sortBy}.${sortOrder}`
|
order: `${sortBy}.${sortOrder}`
|
||||||
@@ -303,7 +303,7 @@ export async function getContractTemplate(id: string | number) {
|
|||||||
export async function getFeaturedTemplates(limit: number = 6) {
|
export async function getFeaturedTemplates(limit: number = 6) {
|
||||||
try {
|
try {
|
||||||
const params: PostgrestParams = {
|
const params: PostgrestParams = {
|
||||||
select: 'id,template_code,title,category_id,description,file_format,is_featured,created_at,updated_at,contract_categories(id,name,icon)',
|
select: 'id,template_code,title,category_id,description,file_path,file_format,is_featured,created_at,updated_at,pdf_file_path,contract_categories(id,name,icon,description)',
|
||||||
filter: { 'is_featured': 'eq.true' },
|
filter: { 'is_featured': 'eq.true' },
|
||||||
order: 'updated_at.desc',
|
order: 'updated_at.desc',
|
||||||
limit
|
limit
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ export function SearchResultHeader({
|
|||||||
value={sortBy}
|
value={sortBy}
|
||||||
onChange={(e) => onSortChange(e.target.value)}
|
onChange={(e) => onSortChange(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="relevance">相关度排序</option>
|
<option value="relevance">相关排序</option>
|
||||||
<option value="newest">最新更新</option>
|
<option value="newest">最新更新</option>
|
||||||
<option value="popular">使用频率</option>
|
{/* <option value="popular">使用频率</option>
|
||||||
<option value="rating">评分最高</option>
|
<option value="rating">评分最高</option> */}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { useState } from 'react';
|
// import { useState } from 'react';
|
||||||
import { useNavigate } from '@remix-run/react';
|
import { useNavigate } from '@remix-run/react';
|
||||||
|
|
||||||
interface Template {
|
interface Template {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
type: string;
|
// type: string;
|
||||||
description: string;
|
description: string;
|
||||||
updateTime: string;
|
updateTime: string;
|
||||||
useCount: number;
|
// useCount: number;
|
||||||
rating: number;
|
// rating: number;
|
||||||
category: string;
|
category: string;
|
||||||
|
file_path?: string;
|
||||||
|
file_format?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TemplateCardProps {
|
interface TemplateCardProps {
|
||||||
@@ -18,21 +20,71 @@ interface TemplateCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TemplateCard({ template, onClick }: TemplateCardProps) {
|
export function TemplateCard({ template, onClick }: TemplateCardProps) {
|
||||||
const [isFavorited, setIsFavorited] = useState(false);
|
// 注释掉收藏功能,后续版本再开发
|
||||||
|
// const [isFavorited, setIsFavorited] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleFavoriteClick = (e: React.MouseEvent) => {
|
/* const handleFavoriteClick = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsFavorited(!isFavorited);
|
setIsFavorited(!isFavorited);
|
||||||
|
}; */
|
||||||
|
|
||||||
|
// MinIO下载URL构建函数
|
||||||
|
const buildDownloadUrl = (filePath: string): string => {
|
||||||
|
// 使用实际的MinIO配置
|
||||||
|
const minioHost = 'http://nas.7bm.co:9000';
|
||||||
|
const bucketName = 'docauditai';
|
||||||
|
|
||||||
|
// 确保文件路径不以/开头
|
||||||
|
const cleanPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
|
||||||
|
|
||||||
|
return `${minioHost}/${bucketName}/${cleanPath}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 下载文件函数
|
||||||
|
const downloadFile = async (filePath: string, fileName: string) => {
|
||||||
|
try {
|
||||||
|
const downloadUrl = buildDownloadUrl(filePath);
|
||||||
|
|
||||||
|
// 清理文件名,移除可能导致问题的字符
|
||||||
|
const cleanFileName = fileName.replace(/[<>:"/\\|?*]/g, '_');
|
||||||
|
|
||||||
|
// 创建临时下载链接
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = downloadUrl;
|
||||||
|
link.download = cleanFileName;
|
||||||
|
link.target = '_blank';
|
||||||
|
|
||||||
|
// 触发下载
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
console.log('开始下载文件:', cleanFileName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载文件失败:', error);
|
||||||
|
alert('下载失败,请稍后重试');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleActionClick = (e: React.MouseEvent, action: string) => {
|
const handleActionClick = (e: React.MouseEvent, action: string) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case '立即使用':
|
case '立即下载':
|
||||||
console.log('下载并使用模板:', template.id);
|
// 添加调试信息
|
||||||
// 这里应该触发下载逻辑
|
console.log('模板数据:', template);
|
||||||
|
console.log('文件路径:', template.file_path);
|
||||||
|
console.log('文件格式:', template.file_format);
|
||||||
|
|
||||||
|
if (template.file_path) {
|
||||||
|
// 构建文件名,使用模板标题和文件格式
|
||||||
|
const fileExtension = template.file_format || 'docx';
|
||||||
|
const fileName = `${template.title}.${fileExtension}`;
|
||||||
|
downloadFile(template.file_path, fileName);
|
||||||
|
} else {
|
||||||
|
alert('文件路径不存在,无法下载');
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case '预览':
|
case '预览':
|
||||||
// 导航到模板详情页面
|
// 导航到模板详情页面
|
||||||
@@ -43,7 +95,7 @@ export function TemplateCard({ template, onClick }: TemplateCardProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderStars = (rating: number) => {
|
/* const renderStars = (rating: number) => {
|
||||||
const stars = [];
|
const stars = [];
|
||||||
const fullStars = Math.floor(rating);
|
const fullStars = Math.floor(rating);
|
||||||
const hasHalfStar = rating % 1 !== 0;
|
const hasHalfStar = rating % 1 !== 0;
|
||||||
@@ -58,7 +110,7 @@ export function TemplateCard({ template, onClick }: TemplateCardProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return stars;
|
return stars;
|
||||||
};
|
}; */
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
@@ -76,28 +128,43 @@ export function TemplateCard({ template, onClick }: TemplateCardProps) {
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label={`查看${template.title}详情`}
|
aria-label={`查看${template.title}详情`}
|
||||||
>
|
>
|
||||||
<div className="template-header">
|
{/* 注释掉头部的type和rating显示 */}
|
||||||
|
{/* <div className="template-header">
|
||||||
<div className="template-type">{template.type}</div>
|
<div className="template-type">{template.type}</div>
|
||||||
<div className="flex items-center gap-1 text-yellow-500">
|
<div className="flex items-center gap-1 text-yellow-500">
|
||||||
{renderStars(template.rating)}
|
{renderStars(template.rating)}
|
||||||
<span className="text-xs ml-1">{template.rating}</span>
|
<span className="text-xs ml-1">{template.rating}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<h3 className="template-title">{template.title}</h3>
|
<h3 className="template-title">{template.title}</h3>
|
||||||
<p className="template-desc">{template.description}</p>
|
<p
|
||||||
|
className="template-desc"
|
||||||
|
style={{
|
||||||
|
height: '4.5rem', // 固定高度约3行文字
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: 3, // 限制3行
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
lineHeight: '1.5rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{template.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="template-meta">
|
<div className="template-meta">
|
||||||
<span>更新时间:{template.updateTime}</span>
|
<span>更新时间:{template.updateTime}</span>
|
||||||
<span>使用次数:{template.useCount.toLocaleString()}</span>
|
{/* 注释掉使用次数显示 */}
|
||||||
|
{/* <span>使用次数:{template.useCount.toLocaleString()}</span> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="template-actions mt-3">
|
<div className="template-actions mt-3">
|
||||||
<button
|
<button
|
||||||
className="action-btn primary"
|
className="action-btn primary"
|
||||||
onClick={(e) => handleActionClick(e, '立即使用')}
|
onClick={(e) => handleActionClick(e, '立即下载')}
|
||||||
>
|
>
|
||||||
立即使用
|
立即下载
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="action-btn"
|
className="action-btn"
|
||||||
@@ -105,13 +172,14 @@ export function TemplateCard({ template, onClick }: TemplateCardProps) {
|
|||||||
>
|
>
|
||||||
预览
|
预览
|
||||||
</button>
|
</button>
|
||||||
<button
|
{/* 注释掉收藏按钮 */}
|
||||||
|
{/* <button
|
||||||
className="action-btn"
|
className="action-btn"
|
||||||
onClick={handleFavoriteClick}
|
onClick={handleFavoriteClick}
|
||||||
title={isFavorited ? '取消收藏' : '收藏'}
|
title={isFavorited ? '取消收藏' : '收藏'}
|
||||||
>
|
>
|
||||||
<i className={isFavorited ? 'ri-star-fill text-yellow-500' : 'ri-star-line'}></i>
|
<i className={isFavorited ? 'ri-star-fill text-yellow-500' : 'ri-star-line'}></i>
|
||||||
</button>
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { TemplateCard } from './TemplateCard';
|
|||||||
interface Template {
|
interface Template {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
type: string;
|
// type: string;
|
||||||
description: string;
|
description: string;
|
||||||
updateTime: string;
|
updateTime: string;
|
||||||
useCount: number;
|
// useCount: number;
|
||||||
rating: number;
|
// rating: number;
|
||||||
category: string;
|
category: string;
|
||||||
|
file_path?: string;
|
||||||
|
file_format?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TemplateGridProps {
|
interface TemplateGridProps {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const meta: MetaFunction = () => {
|
|||||||
// 将数据库模板转换为前端显示格式
|
// 将数据库模板转换为前端显示格式
|
||||||
function transformTemplate(template: ContractTemplate) {
|
function transformTemplate(template: ContractTemplate) {
|
||||||
// 模拟使用次数和评分(实际项目中可以从其他表获取)
|
// 模拟使用次数和评分(实际项目中可以从其他表获取)
|
||||||
const mockUsageCount = Math.floor(Math.random() * 2000) + 100;
|
/* const mockUsageCount = Math.floor(Math.random() * 2000) + 100;
|
||||||
const mockRating = (Math.random() * 1.5 + 3.5).toFixed(1);
|
const mockRating = (Math.random() * 1.5 + 3.5).toFixed(1);
|
||||||
|
|
||||||
// 根据模板属性确定类型
|
// 根据模板属性确定类型
|
||||||
@@ -36,17 +36,24 @@ function transformTemplate(template: ContractTemplate) {
|
|||||||
templateType = '简化版';
|
templateType = '简化版';
|
||||||
} else if (template.description && (template.description.includes('专业') || template.description.includes('大客户'))) {
|
} else if (template.description && (template.description.includes('专业') || template.description.includes('大客户'))) {
|
||||||
templateType = '专业版';
|
templateType = '专业版';
|
||||||
}
|
} */
|
||||||
|
|
||||||
|
// 添加调试信息
|
||||||
|
console.log('原始模板数据:', template);
|
||||||
|
console.log('file_path:', template.file_path);
|
||||||
|
console.log('file_format:', template.file_format);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: template.id.toString(),
|
id: template.id.toString(),
|
||||||
title: template.title,
|
title: template.title,
|
||||||
type: templateType,
|
// type: templateType,
|
||||||
description: template.description || '',
|
description: template.description || '',
|
||||||
updateTime: new Date(template.updated_at).toLocaleDateString('zh-CN'),
|
updateTime: new Date(template.updated_at).toLocaleDateString('zh-CN'),
|
||||||
useCount: mockUsageCount,
|
// useCount: mockUsageCount,
|
||||||
rating: parseFloat(mockRating),
|
// rating: parseFloat(mockRating),
|
||||||
category: template.category?.name || '其他'
|
category: template.category?.name || '其他',
|
||||||
|
file_path: template.file_path,
|
||||||
|
file_format: template.file_format
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,16 +62,45 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
const category = url.searchParams.get('category') || '';
|
const category = url.searchParams.get('category') || '';
|
||||||
const category_id = url.searchParams.get('category_id') || '';
|
const category_id = url.searchParams.get('category_id') || '';
|
||||||
const type = url.searchParams.get('type') || '';
|
const type = url.searchParams.get('type') || '';
|
||||||
|
const sortBy = url.searchParams.get('sortBy') || 'relevance';
|
||||||
const page = parseInt(url.searchParams.get('page') || '1');
|
const page = parseInt(url.searchParams.get('page') || '1');
|
||||||
const pageSize = 6;
|
const pageSize = 6;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 根据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';
|
||||||
|
}
|
||||||
|
|
||||||
// 构建搜索参数
|
// 构建搜索参数
|
||||||
const searchParams: TemplateSearchParams = {
|
const searchParams: TemplateSearchParams = {
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
sortBy: 'updated_at',
|
sortBy: dbSortBy,
|
||||||
sortOrder: 'desc'
|
sortOrder: dbSortOrder
|
||||||
};
|
};
|
||||||
|
|
||||||
// 优先使用category_id,其次使用category名称
|
// 优先使用category_id,其次使用category名称
|
||||||
@@ -91,6 +127,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
category,
|
category,
|
||||||
category_id,
|
category_id,
|
||||||
type,
|
type,
|
||||||
|
sortBy,
|
||||||
categories: []
|
categories: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -99,12 +136,12 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
const categories: ContractCategoryWithCount[] = categoriesResponse.error ? [] : categoriesResponse.data || [];
|
const categories: ContractCategoryWithCount[] = categoriesResponse.error ? [] : categoriesResponse.data || [];
|
||||||
|
|
||||||
// 转换模板数据格式
|
// 转换模板数据格式
|
||||||
let transformedTemplates = templatesResponse.data?.templates.map(transformTemplate) || [];
|
const transformedTemplates = templatesResponse.data?.templates.map(transformTemplate) || [];
|
||||||
|
|
||||||
// 如果有类型筛选,在前端进行筛选(因为数据库中没有type字段)
|
// 注释掉类型筛选,因为数据库中没有type字段且已隐藏该功能
|
||||||
if (type) {
|
/* if (type) {
|
||||||
transformedTemplates = transformedTemplates.filter(t => t.type === type);
|
transformedTemplates = transformedTemplates.filter(t => t.type === type);
|
||||||
}
|
} */
|
||||||
|
|
||||||
// 获取当前分类信息(用于显示)
|
// 获取当前分类信息(用于显示)
|
||||||
let currentCategory = '全部';
|
let currentCategory = '全部';
|
||||||
@@ -123,6 +160,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
category: currentCategory,
|
category: currentCategory,
|
||||||
category_id,
|
category_id,
|
||||||
type,
|
type,
|
||||||
|
sortBy,
|
||||||
categories
|
categories
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -135,18 +173,18 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
category,
|
category,
|
||||||
category_id,
|
category_id,
|
||||||
type,
|
type,
|
||||||
|
sortBy,
|
||||||
categories: []
|
categories: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ContractTemplateList() {
|
export default function ContractTemplateList() {
|
||||||
const { templates, total, page, pageSize, category, category_id, categories } = useLoaderData<typeof loader>();
|
const { templates, total, page, pageSize, category, category_id, categories, sortBy } = useLoaderData<typeof loader>();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [activeFilter, setActiveFilter] = useState(category || '全部');
|
const [activeFilter, setActiveFilter] = useState(category || '全部');
|
||||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||||
const [sortBy, setSortBy] = useState('newest');
|
|
||||||
|
|
||||||
// 监听category变化,同步更新activeFilter状态
|
// 监听category变化,同步更新activeFilter状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -186,6 +224,13 @@ export default function ContractTemplateList() {
|
|||||||
navigate(`/contract-template/list?${params.toString()}`);
|
navigate(`/contract-template/list?${params.toString()}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSortChange = (newSort: string) => {
|
||||||
|
const params = new URLSearchParams(searchParams);
|
||||||
|
params.set('sortBy', newSort);
|
||||||
|
params.delete('page'); // 重置页码
|
||||||
|
navigate(`/contract-template/list?${params.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
// 动态生成筛选选项
|
// 动态生成筛选选项
|
||||||
// 计算所有分类的总模板数量
|
// 计算所有分类的总模板数量
|
||||||
const totalAllTemplates = categories.reduce((sum, cat) => sum + (cat.template_count || 0), 0);
|
const totalAllTemplates = categories.reduce((sum, cat) => sum + (cat.template_count || 0), 0);
|
||||||
@@ -235,11 +280,12 @@ export default function ContractTemplateList() {
|
|||||||
<select
|
<select
|
||||||
className="px-3 py-3 border border-gray-200 rounded-lg text-sm"
|
className="px-3 py-3 border border-gray-200 rounded-lg text-sm"
|
||||||
value={sortBy}
|
value={sortBy}
|
||||||
onChange={(e) => setSortBy(e.target.value)}
|
onChange={(e) => handleSortChange(e.target.value)}
|
||||||
>
|
>
|
||||||
|
<option value="relevance">相关排序</option>
|
||||||
<option value="newest">最新更新</option>
|
<option value="newest">最新更新</option>
|
||||||
<option value="popular">使用频率</option>
|
{/* <option value="popular">使用频率</option>
|
||||||
<option value="rating">评分最高</option>
|
<option value="rating">评分最高</option> */}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,29 +38,33 @@ interface CategoryWithSearchCount extends ContractCategory {
|
|||||||
interface DisplayTemplate {
|
interface DisplayTemplate {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
type: string;
|
// type: string;
|
||||||
description: string;
|
description: string;
|
||||||
updateTime: string;
|
updateTime: string;
|
||||||
useCount: number;
|
// useCount: number;
|
||||||
rating: number;
|
// rating: number;
|
||||||
category: string;
|
category: string;
|
||||||
|
file_path?: string;
|
||||||
|
file_format?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将数据库模板转换为前端显示格式
|
// 将数据库模板转换为前端显示格式
|
||||||
function transformTemplate(template: ContractTemplate) {
|
function transformTemplate(template: ContractTemplate) {
|
||||||
// 模拟使用次数和评分(实际项目中可以从其他表获取)
|
// 模拟使用次数和评分(实际项目中可以从其他表获取)
|
||||||
const mockUsageCount = Math.floor(Math.random() * 2000) + 100;
|
/* const mockUsageCount = Math.floor(Math.random() * 2000) + 100;
|
||||||
const mockRating = (Math.random() * 1.5 + 3.5).toFixed(1);
|
const mockRating = (Math.random() * 1.5 + 3.5).toFixed(1); */
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: template.id.toString(),
|
id: template.id.toString(),
|
||||||
title: template.title,
|
title: template.title,
|
||||||
type: template.is_featured ? '推荐版' : '标准版',
|
// type: template.is_featured ? '推荐版' : '标准版',
|
||||||
description: template.description || '',
|
description: template.description || '',
|
||||||
updateTime: new Date(template.updated_at).toLocaleDateString('zh-CN'),
|
updateTime: new Date(template.updated_at).toLocaleDateString('zh-CN'),
|
||||||
useCount: mockUsageCount,
|
// useCount: mockUsageCount,
|
||||||
rating: parseFloat(mockRating),
|
// rating: parseFloat(mockRating),
|
||||||
category: template.category?.name || '其他'
|
category: template.category?.name || '其他',
|
||||||
|
file_path: template.file_path,
|
||||||
|
file_format: template.file_format
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,11 +72,38 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const query = url.searchParams.get('q') || '';
|
const query = url.searchParams.get('q') || '';
|
||||||
const category = url.searchParams.get('category') || '';
|
const category = url.searchParams.get('category') || '';
|
||||||
|
const sortBy = url.searchParams.get('sortBy') || 'relevance';
|
||||||
const page = parseInt(url.searchParams.get('page') || '1');
|
const page = parseInt(url.searchParams.get('page') || '1');
|
||||||
const pageSize = 6;
|
const pageSize = 6;
|
||||||
|
|
||||||
// 记录搜索开始时间
|
// 记录搜索开始时间
|
||||||
const startTime = Date.now();
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 并行获取搜索结果和分类数据
|
// 并行获取搜索结果和分类数据
|
||||||
@@ -81,8 +112,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
category,
|
category,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
sortBy: 'updated_at',
|
sortBy: dbSortBy,
|
||||||
sortOrder: 'desc'
|
sortOrder: dbSortOrder
|
||||||
}),
|
}),
|
||||||
getContractCategories()
|
getContractCategories()
|
||||||
]);
|
]);
|
||||||
@@ -97,6 +128,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
total: 0,
|
total: 0,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
|
sortBy,
|
||||||
searchTime: '搜索失败',
|
searchTime: '搜索失败',
|
||||||
categories: []
|
categories: []
|
||||||
};
|
};
|
||||||
@@ -155,6 +187,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
total: searchResponse.data?.total || 0,
|
total: searchResponse.data?.total || 0,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
|
sortBy,
|
||||||
searchTime: searchTimeText,
|
searchTime: searchTimeText,
|
||||||
categories: categoriesWithSearchCount
|
categories: categoriesWithSearchCount
|
||||||
};
|
};
|
||||||
@@ -167,6 +200,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
total: 0,
|
total: 0,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
|
sortBy,
|
||||||
searchTime: '搜索失败',
|
searchTime: '搜索失败',
|
||||||
categories: []
|
categories: []
|
||||||
};
|
};
|
||||||
@@ -174,12 +208,13 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ContractTemplateSearchResults() {
|
export default function ContractTemplateSearchResults() {
|
||||||
const { results, query, total, page, pageSize, searchTime, categories }: {
|
const { results, query, total, page, pageSize, sortBy, searchTime, categories }: {
|
||||||
results: DisplayTemplate[];
|
results: DisplayTemplate[];
|
||||||
query: string;
|
query: string;
|
||||||
total: number;
|
total: number;
|
||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
|
sortBy: string;
|
||||||
searchTime: string;
|
searchTime: string;
|
||||||
categories: CategoryWithSearchCount[];
|
categories: CategoryWithSearchCount[];
|
||||||
} = useLoaderData<typeof loader>();
|
} = useLoaderData<typeof loader>();
|
||||||
@@ -188,7 +223,6 @@ export default function ContractTemplateSearchResults() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [activeFilter, setActiveFilter] = useState('全部');
|
const [activeFilter, setActiveFilter] = useState('全部');
|
||||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||||
const [sortBy, setSortBy] = useState('relevance');
|
|
||||||
|
|
||||||
const handleSearch = (newQuery: string) => {
|
const handleSearch = (newQuery: string) => {
|
||||||
if (newQuery.trim()) {
|
if (newQuery.trim()) {
|
||||||
@@ -220,6 +254,13 @@ export default function ContractTemplateSearchResults() {
|
|||||||
navigate(`/contract-template/search/results?${params.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 = [
|
const filters = [
|
||||||
{ label: '全部', count: total },
|
{ label: '全部', count: total },
|
||||||
@@ -246,7 +287,7 @@ export default function ContractTemplateSearchResults() {
|
|||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
onViewModeChange={setViewMode}
|
onViewModeChange={setViewMode}
|
||||||
sortBy={sortBy}
|
sortBy={sortBy}
|
||||||
onSortChange={setSortBy}
|
onSortChange={handleSortChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 筛选标签 */}
|
{/* 筛选标签 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user