合同基本列表数据查询基本完善

This commit is contained in:
2025-05-30 17:40:19 +08:00
parent d0c479f9d4
commit d292dcfccf
5 changed files with 714 additions and 295 deletions
-1
View File
@@ -203,7 +203,6 @@ export async function apiRequest<T>(
const config: AxiosRequestConfig = {
...options,
url,
params,
headers
};
+341
View File
@@ -0,0 +1,341 @@
import { postgrestGet, type PostgrestParams } from "../postgrest-client";
/**
* 数据提取工具函数
* 统一处理不同格式的API响应
*/
function extractApiData<T>(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<ContractCategory[]>('contract_categories', params);
if (response.error) {
return { error: response.error, status: response.status || 500 };
}
const categories = extractApiData<ContractCategory[]>(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<ContractCategory[]>('contract_categories', {
select: '*',
order: 'sort_order.asc,name.asc'
});
if (categoriesResponse.error) {
return { error: categoriesResponse.error, status: categoriesResponse.status || 500 };
}
const categories = extractApiData<ContractCategory[]>(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<string, string> = {};
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<ContractCategory[]>('contract_categories', {
select: 'id',
filter: { 'name': `eq.${category}` }
});
if (categoryResponse.data) {
const categories = extractApiData<ContractCategory[]>(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<ContractTemplate[]>('contract_templates', params);
if (response.error) {
return { error: response.error, status: response.status || 500 };
}
const templates = extractApiData<ContractTemplate[]>(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<ContractTemplate[]>('contract_templates', params);
if (response.error) {
return { error: response.error, status: response.status || 500 };
}
const templates = extractApiData<ContractTemplate[]>(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<ContractTemplate[]>('contract_templates', params);
if (response.error) {
return { error: response.error, status: response.status || 500 };
}
const templates = extractApiData<ContractTemplate[]>(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<TemplateSearchParams, 'keyword'> = {}
) {
return await getContractTemplates({
keyword: query,
...filters
});
}
+160 -121
View File
@@ -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<typeof loader>();
const { templates, total, page, pageSize, category, category_id, categories } = 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(() => {
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 (
<div className="contract-search-results">
<div className="contract-search-results" key={`${category}-${category_id}-${page}`}>
{/* 页面头部 */}
<div className="result-header">
<div>
@@ -195,13 +214,33 @@ export default function ContractTemplateList() {
</div>
</div>
<div className="flex items-center gap-4">
<SearchResultHeader
total={0}
viewMode={viewMode}
onViewModeChange={setViewMode}
sortBy={sortBy}
onSortChange={setSortBy}
/>
{/* 视图切换 */}
<div className="view-toggle">
<button
className={`view-btn ${viewMode === 'grid' ? 'active' : ''}`}
onClick={() => setViewMode('grid')}
aria-label="网格视图"
>
<i className="ri-grid-line"></i>
</button>
<button
className={`view-btn ${viewMode === 'list' ? 'active' : ''}`}
onClick={() => setViewMode('list')}
aria-label="列表视图"
>
<i className="ri-list-check"></i>
</button>
</div>
{/* 排序选择 */}
<select
className="px-3 py-3 border border-gray-200 rounded-lg text-sm"
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value="newest"></option>
<option value="popular">使</option>
<option value="rating"></option>
</select>
</div>
</div>
+54 -16
View File
@@ -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<typeof loader>();
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() {
<div className="quick-categories">
{categories.map((category, index) => (
<div
key={index}
key={category.id || index}
className="category-card"
onClick={() => 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"
+159 -157
View File
@@ -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<CategoryWithSearchCount> => {
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<typeof loader>();
const { results, query, total, page, pageSize, searchTime, categories }: {
results: DisplayTemplate[];
query: string;
total: number;
page: number;
pageSize: number;
searchTime: string;
categories: CategoryWithSearchCount[];
} = useLoaderData<typeof loader>();
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);