From d292dcfccf825ae428f096d1786ed0d39c4ded1e Mon Sep 17 00:00:00 2001 From: awen Date: Fri, 30 May 2025 17:40:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=88=E5=90=8C=E5=9F=BA=E6=9C=AC=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=95=B0=E6=8D=AE=E6=9F=A5=E8=AF=A2=E5=9F=BA=E6=9C=AC?= =?UTF-8?q?=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/axios-client.ts | 1 - app/api/contract-template/templates.ts | 341 ++++++++++++++++++ app/routes/contract-template.list._index.tsx | 281 ++++++++------- .../contract-template.search._index.tsx | 70 +++- .../contract-template.search.results.tsx | 316 ++++++++-------- 5 files changed, 714 insertions(+), 295 deletions(-) create mode 100644 app/api/contract-template/templates.ts diff --git a/app/api/axios-client.ts b/app/api/axios-client.ts index 9dca5e6..5e44334 100644 --- a/app/api/axios-client.ts +++ b/app/api/axios-client.ts @@ -203,7 +203,6 @@ export async function apiRequest( const config: AxiosRequestConfig = { ...options, url, - params, headers }; diff --git a/app/api/contract-template/templates.ts b/app/api/contract-template/templates.ts new file mode 100644 index 0000000..a8ab031 --- /dev/null +++ b/app/api/contract-template/templates.ts @@ -0,0 +1,341 @@ +import { postgrestGet, type PostgrestParams } from "../postgrest-client"; + +/** + * 数据提取工具函数 + * 统一处理不同格式的API响应 + */ +function extractApiData(responseData: unknown): T | null { + if (!responseData) { + return null; + } + + // 格式1: { code: number, msg: string, data: T } + if (typeof responseData === 'object' && responseData !== null && + 'code' in responseData && + 'data' in responseData && + (responseData as { data: unknown }).data) { + return (responseData as { data: T }).data; + } + + // 格式2: 直接是数据对象 + return responseData as T; +} + +// 类型定义 +export interface ContractCategory { + id: number; + name: string; + icon?: string; + description?: string; + sort_order: number; + created_at: string; + updated_at: string; +} + +export interface ContractTemplate { + id: number; + template_code: string; + title: string; + category_id: number; + description?: string; + file_path?: string; + file_format: 'docx' | 'pdf' | 'txt'; + is_featured: boolean; + created_at: string; + updated_at: string; + pdf_file_path?: string; + // 关联的分类信息 + category?: ContractCategory; +} + +export interface TemplateSearchParams { + keyword?: string; + category?: string; + category_id?: number; + file_format?: string; + is_featured?: boolean; + page?: number; + pageSize?: number; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; +} + +export interface SearchResult { + templates: ContractTemplate[]; + total: number; + page: number; + pageSize: number; + totalPages: number; +} + +/** + * 获取所有合同分类 + */ +export async function getContractCategories() { + try { + const params: PostgrestParams = { + select: '*', + order: 'sort_order.asc,name.asc' + }; + + const response = await postgrestGet('contract_categories', params); + + if (response.error) { + return { error: response.error, status: response.status || 500 }; + } + + const categories = extractApiData(response.data) || []; + + return { data: categories }; + } catch (error) { + console.error('获取合同分类失败:', error); + return { + error: error instanceof Error ? error.message : '获取合同分类失败', + status: 500 + }; + } +} + +/** + * 获取所有合同分类及其模板数量(使用聚合查询) + */ +export async function getContractCategoriesWithCount() { + try { + // 获取所有分类 + const categoriesResponse = await postgrestGet('contract_categories', { + select: '*', + order: 'sort_order.asc,name.asc' + }); + + if (categoriesResponse.error) { + return { error: categoriesResponse.error, status: categoriesResponse.status || 500 }; + } + + const categories = extractApiData(categoriesResponse.data) || []; + + // 获取每个分类的模板数量 + const categoriesWithCount = await Promise.all( + categories.map(async (category) => { + try { + // 简化方案:获取该分类下的所有模板ID,然后计算数量 + const countResponse = await postgrestGet<{ id: number }[]>('contract_templates', { + select: 'id', + filter: { 'category_id': `eq.${category.id}` } + }); + + let templateCount = 0; + if (!countResponse.error && countResponse.data) { + const templates = extractApiData<{ id: number }[]>(countResponse.data) || []; + templateCount = templates.length; + } + + return { + ...category, + template_count: templateCount + }; + } catch (error) { + console.error(`获取分类${category.name}的模板数量失败:`, error); + return { + ...category, + template_count: 0 + }; + } + }) + ); + + return { data: categoriesWithCount }; + } catch (error) { + console.error('获取分类及模板数量失败:', error); + return { + error: error instanceof Error ? error.message : '获取分类及模板数量失败', + status: 500 + }; + } +} + +// 扩展分类接口,包含模板数量 +export interface ContractCategoryWithCount extends ContractCategory { + template_count: number; +} + +/** + * 获取合同模板列表(支持筛选和分页) + */ +export async function getContractTemplates(searchParams: TemplateSearchParams = {}) { + try { + const { + keyword, + category, + category_id, + file_format, + is_featured, + page = 1, + pageSize = 6, + sortBy = 'updated_at', + sortOrder = 'desc' + } = searchParams; + + // 构建查询参数 + const params: PostgrestParams = { + select: 'id,template_code,title,category_id,description,file_format,is_featured,created_at,updated_at,contract_categories(id,name,icon)', + limit: pageSize, + offset: (page - 1) * pageSize, + order: `${sortBy}.${sortOrder}` + }; + + // 构建过滤条件 + const filters: Record = {}; + + if (category_id) { + filters['category_id'] = `eq.${category_id}`; + } + + if (file_format) { + filters['file_format'] = `eq.${file_format}`; + } + + if (is_featured !== undefined) { + filters['is_featured'] = `eq.${is_featured}`; + } + + // 处理关键词搜索 + if (keyword && keyword.trim()) { + const cleanKeyword = keyword.trim(); + // 使用PostgREST的or条件进行模糊搜索,正确的格式需要括号 + params.or = `(title.ilike.*${cleanKeyword}*,description.ilike.*${cleanKeyword}*,template_code.ilike.*${cleanKeyword}*)`; + } + + // 如果有分类名称,需要先获取分类ID + if (category && !category_id) { + const categoryResponse = await postgrestGet('contract_categories', { + select: 'id', + filter: { 'name': `eq.${category}` } + }); + + if (categoryResponse.data) { + const categories = extractApiData(categoryResponse.data) || []; + if (categories.length > 0) { + filters['category_id'] = `eq.${categories[0].id}`; + } + } + } + + if (Object.keys(filters).length > 0) { + params.filter = filters; + } + + // 执行查询 + const response = await postgrestGet('contract_templates', params); + + if (response.error) { + return { error: response.error, status: response.status || 500 }; + } + + const templates = extractApiData(response.data) || []; + + // 获取总数(用于分页) + const countParams: PostgrestParams = { + select: 'id', + filter: params.filter, + or: params.or + }; + + const countResponse = await postgrestGet<{ id: number }[]>('contract_templates', countParams); + let total = 0; + if (!countResponse.error && countResponse.data) { + const countData = extractApiData<{ id: number }[]>(countResponse.data) || []; + total = countData.length; + } + + return { + data: { + templates, + total, + page, + pageSize, + totalPages: Math.ceil(total / pageSize) + } as SearchResult + }; + } catch (error) { + console.error('获取合同模板失败:', error); + return { + error: error instanceof Error ? error.message : '获取合同模板失败', + status: 500 + }; + } +} + +/** + * 根据ID获取单个合同模板 + */ +export async function getContractTemplate(id: string | number) { + try { + const params: PostgrestParams = { + 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: { 'id': `eq.${id}` } + }; + + const response = await postgrestGet('contract_templates', params); + + if (response.error) { + return { error: response.error, status: response.status || 500 }; + } + + const templates = extractApiData(response.data) || []; + + if (templates.length === 0) { + return { error: '模板不存在', status: 404 }; + } + + return { data: templates[0] }; + } catch (error) { + console.error('获取合同模板详情失败:', error); + return { + error: error instanceof Error ? error.message : '获取合同模板详情失败', + status: 500 + }; + } +} + +/** + * 获取推荐模板 + */ +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)', + filter: { 'is_featured': 'eq.true' }, + order: 'updated_at.desc', + limit + }; + + const response = await postgrestGet('contract_templates', params); + + if (response.error) { + return { error: response.error, status: response.status || 500 }; + } + + const templates = extractApiData(response.data) || []; + + return { data: templates }; + } catch (error) { + console.error('获取推荐模板失败:', error); + return { + error: error instanceof Error ? error.message : '获取推荐模板失败', + status: 500 + }; + } +} + +/** + * 搜索合同模板(智能搜索) + */ +export async function searchContractTemplates( + query: string, + filters: Omit = {} +) { + return await getContractTemplates({ + keyword: query, + ...filters + }); +} \ No newline at end of file diff --git a/app/routes/contract-template.list._index.tsx b/app/routes/contract-template.list._index.tsx index 4721025..a7df43d 100644 --- a/app/routes/contract-template.list._index.tsx +++ b/app/routes/contract-template.list._index.tsx @@ -1,10 +1,11 @@ import type { MetaFunction, LoaderFunctionArgs } from '@remix-run/node'; import { useLoaderData, useSearchParams, useNavigate } from '@remix-run/react'; -import { useState } from 'react'; -import { SearchResultHeader } from '~/components/contract-template/SearchResultHeader'; +import { useState, useEffect } from 'react'; import { FilterTabs } from '~/components/contract-template/FilterTabs'; import { TemplateGrid } from '~/components/contract-template/TemplateGrid'; import { Pagination } from '~/components/ui/Pagination'; +import { getContractTemplates, getContractCategoriesWithCount } from '~/api/contract-template/templates'; +import type { ContractTemplate, TemplateSearchParams, ContractCategoryWithCount } from '~/api/contract-template/templates'; import styles from '~/styles/pages/contract-template.css?url'; export const links = () => [ @@ -21,130 +22,137 @@ export const meta: MetaFunction = () => { ]; }; -// 模拟数据 - 完整的模板库 -const mockTemplates = [ - { - id: '1', - title: '烟草产品销售合同(2023版)', - type: '标准版', - description: '最新版本的烟草产品销售合同模板,包含完整的法律条款、风险控制措施和行业标准要求。', - updateTime: '2023-10-25', - useCount: 2156, - rating: 4.9, - category: '销售合同' - }, - { - id: '2', - title: '零售商销售协议模板', - type: '标准版', - description: '专为零售商设计的销售协议,涵盖商品配送、结算方式、退换货政策等关键条款。', - updateTime: '2023-10-20', - useCount: 1834, - rating: 4.8, - category: '销售合同' - }, - { - id: '3', - title: '大客户销售合同模板', - type: '专业版', - description: '适用于大客户的专业销售合同,包含定制化条款、特殊优惠政策和长期合作框架。', - updateTime: '2023-10-18', - useCount: 1245, - rating: 4.7, - category: '销售合同' - }, - { - id: '4', - title: '小额销售合同(简化版)', - type: '简化版', - description: '适用于小额交易的简化版销售合同,条款精简但保证法律效力。', - updateTime: '2023-10-15', - useCount: 956, - rating: 4.6, - category: '销售合同' - }, - { - id: '5', - title: '区域代理销售合同', - type: '标准版', - description: '区域代理商专用销售合同,明确代理权限、销售目标和考核标准。', - updateTime: '2023-10-12', - useCount: 743, - rating: 4.5, - category: '销售合同' - }, - { - id: '6', - title: '批发销售合同模板', - type: '标准版', - description: '适用于大宗批发业务的销售合同,包含数量折扣、物流配送等专业条款。', - updateTime: '2023-10-10', - useCount: 612, - rating: 4.4, - category: '销售合同' - }, - { - id: '7', - title: '设备采购合同标准模板', - type: '标准版', - description: '设备采购专用合同模板,包含详细的技术要求、验收标准、质保条款等内容。', - updateTime: '2023-10-08', - useCount: 589, - rating: 4.6, - category: '采购合同' - }, - { - id: '8', - title: '服务类采购合同', - type: '标准版', - description: '适用于各类服务采购,包含服务标准、交付要求、考核指标等条款。', - updateTime: '2023-10-05', - useCount: 432, - rating: 4.5, - category: '采购合同' +// 将数据库模板转换为前端显示格式 +function transformTemplate(template: ContractTemplate) { + // 模拟使用次数和评分(实际项目中可以从其他表获取) + const mockUsageCount = Math.floor(Math.random() * 2000) + 100; + const mockRating = (Math.random() * 1.5 + 3.5).toFixed(1); + + // 根据模板属性确定类型 + let templateType = '标准版'; + if (template.is_featured) { + templateType = '推荐版'; + } else if (template.description && template.description.includes('简化')) { + templateType = '简化版'; + } else if (template.description && (template.description.includes('专业') || template.description.includes('大客户'))) { + templateType = '专业版'; } -]; + + return { + id: template.id.toString(), + title: template.title, + type: templateType, + description: template.description || '', + updateTime: new Date(template.updated_at).toLocaleDateString('zh-CN'), + useCount: mockUsageCount, + rating: parseFloat(mockRating), + category: template.category?.name || '其他' + }; +} export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); const category = url.searchParams.get('category') || ''; + const category_id = url.searchParams.get('category_id') || ''; const type = url.searchParams.get('type') || ''; const page = parseInt(url.searchParams.get('page') || '1'); const pageSize = 6; - // 筛选模板 - let filteredTemplates = mockTemplates; - - if (category) { - filteredTemplates = filteredTemplates.filter(t => t.category === category); - } - - if (type) { - filteredTemplates = filteredTemplates.filter(t => t.type === type); - } - - const total = filteredTemplates.length; - const startIndex = (page - 1) * pageSize; - const templates = filteredTemplates.slice(startIndex, startIndex + pageSize); + try { + // 构建搜索参数 + const searchParams: TemplateSearchParams = { + page, + pageSize, + sortBy: 'updated_at', + sortOrder: 'desc' + }; - return { - templates, - total, - page, - pageSize, - category, - type - }; + // 优先使用category_id,其次使用category名称 + if (category_id) { + searchParams.category_id = parseInt(category_id); + } else if (category) { + searchParams.category = category; + } + + // 并行获取模板数据和分类数据 + const [templatesResponse, categoriesResponse] = await Promise.all([ + getContractTemplates(searchParams), + getContractCategoriesWithCount() + ]); + + // 处理模板数据 + if (templatesResponse.error) { + console.error('获取模板列表失败:', templatesResponse.error); + return { + templates: [], + total: 0, + page, + pageSize, + category, + category_id, + type, + categories: [] + }; + } + + // 处理分类数据 + const categories: ContractCategoryWithCount[] = categoriesResponse.error ? [] : categoriesResponse.data || []; + + // 转换模板数据格式 + let transformedTemplates = templatesResponse.data?.templates.map(transformTemplate) || []; + + // 如果有类型筛选,在前端进行筛选(因为数据库中没有type字段) + if (type) { + transformedTemplates = transformedTemplates.filter(t => t.type === type); + } + + // 获取当前分类信息(用于显示) + let currentCategory = '全部'; + if (category_id) { + const cat = categories.find(c => c.id === parseInt(category_id)); + currentCategory = cat?.name || '全部'; + } else if (category) { + currentCategory = category; + } + + return { + templates: transformedTemplates, + total: templatesResponse.data?.total || 0, + page, + pageSize, + category: currentCategory, + category_id, + type, + categories + }; + } catch (error) { + console.error('加载模板列表失败:', error); + return { + templates: [], + total: 0, + page, + pageSize, + category, + category_id, + type, + categories: [] + }; + } } export default function ContractTemplateList() { - const { templates, total, page, pageSize, category } = useLoaderData(); + const { templates, total, page, pageSize, category, category_id, categories } = 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(() => { + setActiveFilter(category || '全部'); + }, [category]); + const handleTemplateClick = (templateId: string) => { navigate(`/contract-template/detail/${templateId}`); }; @@ -155,8 +163,17 @@ export default function ContractTemplateList() { if (filter === '全部') { params.delete('category'); + params.delete('category_id'); } else { - params.set('category', filter); + // 根据分类名称找到对应的ID + const selectedCategory = categories.find(cat => cat.name === filter); + if (selectedCategory) { + params.set('category_id', selectedCategory.id.toString()); + params.delete('category'); // 删除旧的category参数 + } else { + params.set('category', filter); + params.delete('category_id'); + } } params.delete('page'); // 重置页码 @@ -170,12 +187,14 @@ export default function ContractTemplateList() { }; // 动态生成筛选选项 - const allCategories = [...new Set(mockTemplates.map(t => t.category))]; + // 计算所有分类的总模板数量 + const totalAllTemplates = categories.reduce((sum, cat) => sum + (cat.template_count || 0), 0); + const filters = [ - { label: '全部', count: mockTemplates.length }, - ...allCategories.map(cat => ({ - label: cat, - count: mockTemplates.filter(t => t.category === cat).length + { label: '全部', count: totalAllTemplates }, + ...categories.map(cat => ({ + label: cat.name, + count: cat.template_count || 0 })) ]; @@ -183,7 +202,7 @@ export default function ContractTemplateList() { const currentCategory = category || '全部'; return ( -
+
{/* 页面头部 */}
@@ -195,13 +214,33 @@ export default function ContractTemplateList() {
- + {/* 视图切换 */} +
+ + +
+ {/* 排序选择 */} +
diff --git a/app/routes/contract-template.search._index.tsx b/app/routes/contract-template.search._index.tsx index fe48245..876eb2a 100644 --- a/app/routes/contract-template.search._index.tsx +++ b/app/routes/contract-template.search._index.tsx @@ -1,6 +1,8 @@ import type { MetaFunction } from '@remix-run/node'; -import { useNavigate } from '@remix-run/react'; +import { useNavigate, useLoaderData } from '@remix-run/react'; import { ContractSearchHero } from '~/components/contract-template/ContractSearchHero'; +import { getContractCategoriesWithCount } from '~/api/contract-template/templates'; +import type { ContractCategoryWithCount } from '~/api/contract-template/templates'; import styles from '~/styles/pages/contract-template.css?url'; export const links = () => [ @@ -22,18 +24,50 @@ export const handle = { breadcrumb: "智能搜索" }; +/** + * 转换分类数据为前端显示格式 + * @param category 分类数据 + * @returns 转换后的分类数据 + */ +function transformCategory(category: ContractCategoryWithCount) { + return { + id: category.id, + name: category.name, + icon: category.icon || 'ri-file-text-line', + count: category.template_count + }; +} + +/** + * 加载分类数据 + * @returns 分类数据 + */ +export async function loader() { + try { + // 使用聚合查询获取分类及其模板数量 + const categoriesResponse = await getContractCategoriesWithCount(); + + // 处理分类数据 + if (categoriesResponse.error) { + console.error('获取分类失败:', categoriesResponse.error); + return { categories: [] }; + } + + const categories = categoriesResponse.data || []; + + // 转换分类数据格式 + const categoriesWithCount = categories.map(transformCategory); + + return { categories: categoriesWithCount }; + } catch (error) { + console.error('加载分类数据失败:', error); + return { categories: [] }; + } +} + export default function ContractTemplateSearchIndex() { const navigate = useNavigate(); - - // 模拟分类数据 - const categories = [ - { name: '销售合同', icon: 'ri-contract-line', count: 128 }, - { name: '采购合同', icon: 'ri-shopping-cart-line', count: 96 }, - { name: '物流运输', icon: 'ri-truck-line', count: 64 }, - { name: '人事劳务', icon: 'ri-user-settings-line', count: 52 }, - { name: '租赁合同', icon: 'ri-building-line', count: 38 }, - { name: '保密协议', icon: 'ri-shield-check-line', count: 24 } - ]; + const { categories } = useLoaderData(); const handleSearch = (query: string) => { if (query.trim()) { @@ -41,8 +75,12 @@ export default function ContractTemplateSearchIndex() { } }; - const handleCategoryClick = (categoryName: string) => { - navigate(`/contract-template/list?category=${encodeURIComponent(categoryName)}`); + /** + * 处理分类点击事件 - 使用ID而不是名称 + * @param categoryId 分类ID + */ + const handleCategoryClick = (categoryId: number) => { + navigate(`/contract-template/list?category_id=${categoryId}`); }; return ( @@ -51,13 +89,13 @@ export default function ContractTemplateSearchIndex() {
{categories.map((category, index) => (
handleCategoryClick(category.name)} + onClick={() => handleCategoryClick(category.id)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - handleCategoryClick(category.name); + handleCategoryClick(category.id); } }} role="button" diff --git a/app/routes/contract-template.search.results.tsx b/app/routes/contract-template.search.results.tsx index 31010b9..76fab16 100644 --- a/app/routes/contract-template.search.results.tsx +++ b/app/routes/contract-template.search.results.tsx @@ -6,6 +6,8 @@ import { SearchResultHeader } from '~/components/contract-template/SearchResultH 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'; export const links = () => [ @@ -27,172 +29,161 @@ export const handle = { breadcrumb: "搜索结果" }; -// 模拟数据 - 扩展搜索结果 -const mockSearchResults = [ - { - id: '1', - title: '烟草产品销售合同标准模板', - type: '销售合同', - description: '适用于烟草产品销售业务,包含完整的违约责任条款、付款方式、交付条件等核心要素,符合行业规范要求。', - updateTime: '2023-10-25', - useCount: 1248, - rating: 4.8, - category: '销售合同' - }, - { - id: '2', - title: '零售商销售协议模板', - type: '销售合同', - description: '专为零售商设计的销售协议,详细规定了违约责任、退换货政策、结算方式等条款。', - updateTime: '2023-10-20', - useCount: 856, - rating: 4.6, - category: '销售合同' - }, - { - id: '3', - title: '设备采购合同(含违约条款)', - type: '采购合同', - description: '设备采购专用合同模板,包含详细的违约责任条款、质量保证、验收标准等内容。', - updateTime: '2023-10-18', - useCount: 642, - rating: 4.7, - category: '采购合同' - }, - { - id: '4', - title: '批发销售合同模板', - type: '销售合同', - description: '适用于大宗批发业务的销售合同,强化了违约责任条款和风险控制措施。', - updateTime: '2023-10-15', - useCount: 534, - rating: 4.5, - category: '销售合同' - }, - { - id: '5', - title: '技术服务合同(标准版)', - type: '服务合同', - description: '技术服务类合同模板,包含服务标准、违约责任、知识产权保护等关键条款。', - updateTime: '2023-10-12', - useCount: 423, - rating: 4.4, - category: '服务合同' - }, - { - id: '6', - title: '区域代理销售合同', - type: '销售合同', - description: '区域代理商专用销售合同,明确代理权限、销售目标、违约责任等核心条款。', - updateTime: '2023-10-10', - useCount: 312, - rating: 4.3, - category: '销售合同' - }, - { - id: '7', - title: '物流运输服务合同', - type: '物流运输', - description: '专业的物流运输服务合同模板,涵盖运输责任、保险、违约赔偿等关键条款。', - updateTime: '2023-10-08', - useCount: 267, - rating: 4.2, - category: '物流运输' - }, - { - id: '8', - title: '仓储管理服务协议', - type: '物流运输', - description: '仓储管理专用合同,包含货物保管、出入库管理、损失责任等详细条款。', - updateTime: '2023-10-05', - useCount: 189, - rating: 4.1, - category: '物流运输' - }, - { - id: '9', - title: '劳务派遣合同模板', - type: '人事劳务', - description: '劳务派遣服务合同,明确派遣关系、工资福利、社保缴纳等人事管理条款。', - updateTime: '2023-10-03', - useCount: 345, - rating: 4.6, - category: '人事劳务' - }, - { - id: '10', - title: '商业机密保护协议', - type: '保密协议', - description: '企业商业机密保护专用协议,涵盖信息范围、保密义务、违约责任等核心内容。', - updateTime: '2023-10-01', - useCount: 156, - rating: 4.5, - category: '保密协议' - }, - { - id: '11', - title: '办公场地租赁合同', - type: '租赁合同', - description: '办公场地租赁标准合同,包含租金支付、物业管理、违约处理等全面条款。', - updateTime: '2023-09-28', - useCount: 278, - rating: 4.3, - category: '租赁合同' - }, - { - id: '12', - title: '设备租赁协议(长期)', - type: '租赁合同', - description: '长期设备租赁专用协议,详细规定租赁期限、维护责任、续租条件等关键条款。', - updateTime: '2023-09-25', - useCount: 198, - rating: 4.4, - category: '租赁合同' - } -]; +// 带搜索统计的分类类型 +interface CategoryWithSearchCount extends ContractCategory { + searchCount: number; +} + +// 前端显示的模板类型 +interface DisplayTemplate { + id: string; + title: string; + type: string; + description: string; + updateTime: string; + useCount: number; + rating: number; + category: 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 || '其他' + }; +} 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 page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = 6; - // 模拟搜索耗时 + // 记录搜索开始时间 const startTime = Date.now(); - // 这里应该是实际的搜索逻辑 - // 目前返回模拟数据 - let filteredResults = mockSearchResults; - - // 如果有查询条件,进行筛选 - if (query || category) { - filteredResults = mockSearchResults.filter(item => { - const matchesQuery = !query || - item.title.toLowerCase().includes(query.toLowerCase()) || - item.description.toLowerCase().includes(query.toLowerCase()); - const matchesCategory = !category || item.category === category; - return matchesQuery && matchesCategory; - }); + try { + // 并行获取搜索结果和分类数据 + const [searchResponse, categoriesResponse] = await Promise.all([ + searchContractTemplates(query, { + category, + page, + pageSize, + sortBy: 'updated_at', + sortOrder: 'desc' + }), + getContractCategories() + ]); + + // 处理搜索结果 + if (searchResponse.error) { + console.error('搜索合同模板失败:', searchResponse.error); + return { + results: [], + query, + category, + total: 0, + page, + pageSize, + 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 => { + try { + const categorySearchResponse = await searchContractTemplates(query, { + category: cat.name, + page: 1, + pageSize: 1000 // 设置较大的pageSize来获取总数 + }); + + 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, + searchTime: searchTimeText, + categories: categoriesWithSearchCount + }; + } catch (error) { + console.error('加载搜索结果失败:', error); + return { + results: [], + query, + category, + total: 0, + page, + pageSize, + searchTime: '搜索失败', + categories: [] + }; } - - // 计算搜索耗时 - const endTime = Date.now(); - const searchTime = (endTime - startTime) / 1000; - const searchTimeText = `搜索用时 ${searchTime.toFixed(1)}秒`; - - return { - results: filteredResults, - query, - category, - total: filteredResults.length, - page, - pageSize: 6, - searchTime: searchTimeText - }; } export default function ContractTemplateSearchResults() { - const { results, query, total, page, pageSize, searchTime } = useLoaderData(); + const { results, query, total, page, pageSize, searchTime, categories }: { + results: DisplayTemplate[]; + query: string; + total: number; + page: number; + pageSize: number; + searchTime: string; + categories: CategoryWithSearchCount[]; + } = useLoaderData(); + const [searchParams] = useSearchParams(); const navigate = useNavigate(); const [activeFilter, setActiveFilter] = useState('全部'); @@ -211,7 +202,16 @@ export default function ContractTemplateSearchResults() { 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) => { @@ -220,11 +220,13 @@ export default function ContractTemplateSearchResults() { navigate(`/contract-template/search/results?${params.toString()}`); }; + // 动态生成筛选选项 const filters = [ { label: '全部', count: total }, - { label: '销售合同', count: results.filter(r => r.category === '销售合同').length }, - { label: '采购合同', count: results.filter(r => r.category === '采购合同').length }, - { label: '服务合同', count: results.filter(r => r.category === '服务合同').length } + ...categories.map(cat => ({ + label: cat.name, + count: cat.searchCount || 0 + })) ]; const totalPages = Math.ceil(total / pageSize);