From c169d718c5aa7a601958147879deb731464ea5d0 Mon Sep 17 00:00:00 2001 From: awen Date: Fri, 30 May 2025 18:20:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=88=97=E8=A1=A8=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E5=AF=B9=E6=8E=A5=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/contract-template/templates.ts | 4 +- .../contract-template/SearchResultHeader.tsx | 6 +- .../contract-template/TemplateCard.tsx | 106 ++++++++++++++---- .../contract-template/TemplateGrid.tsx | 8 +- app/routes/contract-template.list._index.tsx | 80 ++++++++++--- .../contract-template.search.results.tsx | 69 +++++++++--- 6 files changed, 215 insertions(+), 58 deletions(-) diff --git a/app/api/contract-template/templates.ts b/app/api/contract-template/templates.ts index a8ab031..de3ab55 100644 --- a/app/api/contract-template/templates.ts +++ b/app/api/contract-template/templates.ts @@ -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 diff --git a/app/components/contract-template/SearchResultHeader.tsx b/app/components/contract-template/SearchResultHeader.tsx index c1066d5..f117d6b 100644 --- a/app/components/contract-template/SearchResultHeader.tsx +++ b/app/components/contract-template/SearchResultHeader.tsx @@ -40,10 +40,10 @@ export function SearchResultHeader({ value={sortBy} onChange={(e) => onSortChange(e.target.value)} > - + - - + {/* + */} diff --git a/app/components/contract-template/TemplateCard.tsx b/app/components/contract-template/TemplateCard.tsx index 36d836a..745de50 100644 --- a/app/components/contract-template/TemplateCard.tsx +++ b/app/components/contract-template/TemplateCard.tsx @@ -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}详情`} > -
+ {/* 注释掉头部的type和rating显示 */} + {/*
{template.type}
{renderStars(template.rating)} {template.rating}
-
+
*/}

{template.title}

-

{template.description}

+

+ {template.description} +

更新时间:{template.updateTime} - 使用次数:{template.useCount.toLocaleString()} + {/* 注释掉使用次数显示 */} + {/* 使用次数:{template.useCount.toLocaleString()} */}
- + */}
); diff --git a/app/components/contract-template/TemplateGrid.tsx b/app/components/contract-template/TemplateGrid.tsx index 47a1046..2d916e2 100644 --- a/app/components/contract-template/TemplateGrid.tsx +++ b/app/components/contract-template/TemplateGrid.tsx @@ -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 { diff --git a/app/routes/contract-template.list._index.tsx b/app/routes/contract-template.list._index.tsx index a7df43d..743487c 100644 --- a/app/routes/contract-template.list._index.tsx +++ b/app/routes/contract-template.list._index.tsx @@ -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(); + const { templates, total, page, pageSize, category, category_id, categories, sortBy } = useLoaderData(); 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() { diff --git a/app/routes/contract-template.search.results.tsx b/app/routes/contract-template.search.results.tsx index 76fab16..4b37c2e 100644 --- a/app/routes/contract-template.search.results.tsx +++ b/app/routes/contract-template.search.results.tsx @@ -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(); @@ -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} /> {/* 筛选标签 */}