数据列表查询对接完成
This commit is contained in:
@@ -177,7 +177,7 @@ export async function getContractTemplates(searchParams: TemplateSearchParams =
|
||||
|
||||
// 构建查询参数
|
||||
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,
|
||||
offset: (page - 1) * pageSize,
|
||||
order: `${sortBy}.${sortOrder}`
|
||||
@@ -303,7 +303,7 @@ export async function getContractTemplate(id: string | number) {
|
||||
export async function getFeaturedTemplates(limit: number = 6) {
|
||||
try {
|
||||
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' },
|
||||
order: 'updated_at.desc',
|
||||
limit
|
||||
|
||||
@@ -40,10 +40,10 @@ export function SearchResultHeader({
|
||||
value={sortBy}
|
||||
onChange={(e) => onSortChange(e.target.value)}
|
||||
>
|
||||
<option value="relevance">相关度排序</option>
|
||||
<option value="relevance">相关排序</option>
|
||||
<option value="newest">最新更新</option>
|
||||
<option value="popular">使用频率</option>
|
||||
<option value="rating">评分最高</option>
|
||||
{/* <option value="popular">使用频率</option>
|
||||
<option value="rating">评分最高</option> */}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
// import { useState } from 'react';
|
||||
import { useNavigate } from '@remix-run/react';
|
||||
|
||||
interface Template {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
// type: string;
|
||||
description: string;
|
||||
updateTime: string;
|
||||
useCount: number;
|
||||
rating: number;
|
||||
// useCount: number;
|
||||
// rating: number;
|
||||
category: string;
|
||||
file_path?: string;
|
||||
file_format?: string;
|
||||
}
|
||||
|
||||
interface TemplateCardProps {
|
||||
@@ -18,21 +20,71 @@ interface TemplateCardProps {
|
||||
}
|
||||
|
||||
export function TemplateCard({ template, onClick }: TemplateCardProps) {
|
||||
const [isFavorited, setIsFavorited] = useState(false);
|
||||
// 注释掉收藏功能,后续版本再开发
|
||||
// const [isFavorited, setIsFavorited] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleFavoriteClick = (e: React.MouseEvent) => {
|
||||
/* const handleFavoriteClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
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) => {
|
||||
e.stopPropagation();
|
||||
|
||||
switch (action) {
|
||||
case '立即使用':
|
||||
console.log('下载并使用模板:', template.id);
|
||||
// 这里应该触发下载逻辑
|
||||
case '立即下载':
|
||||
// 添加调试信息
|
||||
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;
|
||||
case '预览':
|
||||
// 导航到模板详情页面
|
||||
@@ -43,7 +95,7 @@ export function TemplateCard({ template, onClick }: TemplateCardProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const renderStars = (rating: number) => {
|
||||
/* const renderStars = (rating: number) => {
|
||||
const stars = [];
|
||||
const fullStars = Math.floor(rating);
|
||||
const hasHalfStar = rating % 1 !== 0;
|
||||
@@ -58,7 +110,7 @@ export function TemplateCard({ template, onClick }: TemplateCardProps) {
|
||||
}
|
||||
}
|
||||
return stars;
|
||||
};
|
||||
}; */
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
@@ -76,28 +128,43 @@ export function TemplateCard({ template, onClick }: TemplateCardProps) {
|
||||
tabIndex={0}
|
||||
aria-label={`查看${template.title}详情`}
|
||||
>
|
||||
<div className="template-header">
|
||||
{/* 注释掉头部的type和rating显示 */}
|
||||
{/* <div className="template-header">
|
||||
<div className="template-type">{template.type}</div>
|
||||
<div className="flex items-center gap-1 text-yellow-500">
|
||||
{renderStars(template.rating)}
|
||||
<span className="text-xs ml-1">{template.rating}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<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">
|
||||
<span>更新时间:{template.updateTime}</span>
|
||||
<span>使用次数:{template.useCount.toLocaleString()}</span>
|
||||
{/* 注释掉使用次数显示 */}
|
||||
{/* <span>使用次数:{template.useCount.toLocaleString()}</span> */}
|
||||
</div>
|
||||
|
||||
<div className="template-actions mt-3">
|
||||
<button
|
||||
className="action-btn primary"
|
||||
onClick={(e) => handleActionClick(e, '立即使用')}
|
||||
onClick={(e) => handleActionClick(e, '立即下载')}
|
||||
>
|
||||
立即使用
|
||||
立即下载
|
||||
</button>
|
||||
<button
|
||||
className="action-btn"
|
||||
@@ -105,13 +172,14 @@ export function TemplateCard({ template, onClick }: TemplateCardProps) {
|
||||
>
|
||||
预览
|
||||
</button>
|
||||
<button
|
||||
{/* 注释掉收藏按钮 */}
|
||||
{/* <button
|
||||
className="action-btn"
|
||||
onClick={handleFavoriteClick}
|
||||
title={isFavorited ? '取消收藏' : '收藏'}
|
||||
>
|
||||
<i className={isFavorited ? 'ri-star-fill text-yellow-500' : 'ri-star-line'}></i>
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,12 +3,14 @@ import { TemplateCard } from './TemplateCard';
|
||||
interface Template {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
// type: string;
|
||||
description: string;
|
||||
updateTime: string;
|
||||
useCount: number;
|
||||
rating: number;
|
||||
// useCount: number;
|
||||
// rating: number;
|
||||
category: string;
|
||||
file_path?: string;
|
||||
file_format?: string;
|
||||
}
|
||||
|
||||
interface TemplateGridProps {
|
||||
|
||||
@@ -25,7 +25,7 @@ export const meta: MetaFunction = () => {
|
||||
// 将数据库模板转换为前端显示格式
|
||||
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);
|
||||
|
||||
// 根据模板属性确定类型
|
||||
@@ -36,17 +36,24 @@ function transformTemplate(template: ContractTemplate) {
|
||||
templateType = '简化版';
|
||||
} else if (template.description && (template.description.includes('专业') || template.description.includes('大客户'))) {
|
||||
templateType = '专业版';
|
||||
}
|
||||
} */
|
||||
|
||||
// 添加调试信息
|
||||
console.log('原始模板数据:', template);
|
||||
console.log('file_path:', template.file_path);
|
||||
console.log('file_format:', template.file_format);
|
||||
|
||||
return {
|
||||
id: template.id.toString(),
|
||||
title: template.title,
|
||||
type: templateType,
|
||||
// type: templateType,
|
||||
description: template.description || '',
|
||||
updateTime: new Date(template.updated_at).toLocaleDateString('zh-CN'),
|
||||
useCount: mockUsageCount,
|
||||
rating: parseFloat(mockRating),
|
||||
category: template.category?.name || '其他'
|
||||
// useCount: mockUsageCount,
|
||||
// rating: parseFloat(mockRating),
|
||||
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_id = url.searchParams.get('category_id') || '';
|
||||
const type = url.searchParams.get('type') || '';
|
||||
const sortBy = url.searchParams.get('sortBy') || 'relevance';
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = 6;
|
||||
|
||||
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 = {
|
||||
page,
|
||||
pageSize,
|
||||
sortBy: 'updated_at',
|
||||
sortOrder: 'desc'
|
||||
sortBy: dbSortBy,
|
||||
sortOrder: dbSortOrder
|
||||
};
|
||||
|
||||
// 优先使用category_id,其次使用category名称
|
||||
@@ -91,6 +127,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
category,
|
||||
category_id,
|
||||
type,
|
||||
sortBy,
|
||||
categories: []
|
||||
};
|
||||
}
|
||||
@@ -99,12 +136,12 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const categories: ContractCategoryWithCount[] = categoriesResponse.error ? [] : categoriesResponse.data || [];
|
||||
|
||||
// 转换模板数据格式
|
||||
let transformedTemplates = templatesResponse.data?.templates.map(transformTemplate) || [];
|
||||
const transformedTemplates = templatesResponse.data?.templates.map(transformTemplate) || [];
|
||||
|
||||
// 如果有类型筛选,在前端进行筛选(因为数据库中没有type字段)
|
||||
if (type) {
|
||||
// 注释掉类型筛选,因为数据库中没有type字段且已隐藏该功能
|
||||
/* if (type) {
|
||||
transformedTemplates = transformedTemplates.filter(t => t.type === type);
|
||||
}
|
||||
} */
|
||||
|
||||
// 获取当前分类信息(用于显示)
|
||||
let currentCategory = '全部';
|
||||
@@ -123,6 +160,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
category: currentCategory,
|
||||
category_id,
|
||||
type,
|
||||
sortBy,
|
||||
categories
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -135,18 +173,18 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
category,
|
||||
category_id,
|
||||
type,
|
||||
sortBy,
|
||||
categories: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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 navigate = useNavigate();
|
||||
const [activeFilter, setActiveFilter] = useState(category || '全部');
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||
const [sortBy, setSortBy] = useState('newest');
|
||||
|
||||
// 监听category变化,同步更新activeFilter状态
|
||||
useEffect(() => {
|
||||
@@ -186,6 +224,13 @@ export default function ContractTemplateList() {
|
||||
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);
|
||||
@@ -235,11 +280,12 @@ export default function ContractTemplateList() {
|
||||
<select
|
||||
className="px-3 py-3 border border-gray-200 rounded-lg text-sm"
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
onChange={(e) => handleSortChange(e.target.value)}
|
||||
>
|
||||
<option value="relevance">相关排序</option>
|
||||
<option value="newest">最新更新</option>
|
||||
<option value="popular">使用频率</option>
|
||||
<option value="rating">评分最高</option>
|
||||
{/* <option value="popular">使用频率</option>
|
||||
<option value="rating">评分最高</option> */}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,29 +38,33 @@ interface CategoryWithSearchCount extends ContractCategory {
|
||||
interface DisplayTemplate {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
// type: string;
|
||||
description: string;
|
||||
updateTime: string;
|
||||
useCount: number;
|
||||
rating: number;
|
||||
// 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);
|
||||
/* 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 ? '推荐版' : '标准版',
|
||||
// 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 || '其他'
|
||||
// useCount: mockUsageCount,
|
||||
// rating: parseFloat(mockRating),
|
||||
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 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';
|
||||
}
|
||||
|
||||
try {
|
||||
// 并行获取搜索结果和分类数据
|
||||
@@ -81,8 +112,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
category,
|
||||
page,
|
||||
pageSize,
|
||||
sortBy: 'updated_at',
|
||||
sortOrder: 'desc'
|
||||
sortBy: dbSortBy,
|
||||
sortOrder: dbSortOrder
|
||||
}),
|
||||
getContractCategories()
|
||||
]);
|
||||
@@ -97,6 +128,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
total: 0,
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
searchTime: '搜索失败',
|
||||
categories: []
|
||||
};
|
||||
@@ -155,6 +187,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
total: searchResponse.data?.total || 0,
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
searchTime: searchTimeText,
|
||||
categories: categoriesWithSearchCount
|
||||
};
|
||||
@@ -167,6 +200,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
total: 0,
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
searchTime: '搜索失败',
|
||||
categories: []
|
||||
};
|
||||
@@ -174,12 +208,13 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
}
|
||||
|
||||
export default function ContractTemplateSearchResults() {
|
||||
const { results, query, total, page, pageSize, searchTime, categories }: {
|
||||
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>();
|
||||
@@ -188,7 +223,6 @@ export default function ContractTemplateSearchResults() {
|
||||
const navigate = useNavigate();
|
||||
const [activeFilter, setActiveFilter] = useState('全部');
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||
const [sortBy, setSortBy] = useState('relevance');
|
||||
|
||||
const handleSearch = (newQuery: string) => {
|
||||
if (newQuery.trim()) {
|
||||
@@ -220,6 +254,13 @@ export default function ContractTemplateSearchResults() {
|
||||
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 },
|
||||
@@ -246,7 +287,7 @@ export default function ContractTemplateSearchResults() {
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
sortBy={sortBy}
|
||||
onSortChange={setSortBy}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
|
||||
{/* 筛选标签 */}
|
||||
|
||||
Reference in New Issue
Block a user