合同基本列表数据查询基本完善
This commit is contained in:
@@ -203,7 +203,6 @@ export async function apiRequest<T>(
|
|||||||
const config: AxiosRequestConfig = {
|
const config: AxiosRequestConfig = {
|
||||||
...options,
|
...options,
|
||||||
url,
|
url,
|
||||||
params,
|
|
||||||
headers
|
headers
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { MetaFunction, LoaderFunctionArgs } from '@remix-run/node';
|
import type { MetaFunction, LoaderFunctionArgs } from '@remix-run/node';
|
||||||
import { useLoaderData, useSearchParams, useNavigate } from '@remix-run/react';
|
import { useLoaderData, useSearchParams, useNavigate } from '@remix-run/react';
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { SearchResultHeader } from '~/components/contract-template/SearchResultHeader';
|
|
||||||
import { FilterTabs } from '~/components/contract-template/FilterTabs';
|
import { FilterTabs } from '~/components/contract-template/FilterTabs';
|
||||||
import { TemplateGrid } from '~/components/contract-template/TemplateGrid';
|
import { TemplateGrid } from '~/components/contract-template/TemplateGrid';
|
||||||
import { Pagination } from '~/components/ui/Pagination';
|
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';
|
import styles from '~/styles/pages/contract-template.css?url';
|
||||||
|
|
||||||
export const links = () => [
|
export const links = () => [
|
||||||
@@ -21,130 +22,137 @@ export const meta: MetaFunction = () => {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 模拟数据 - 完整的模板库
|
// 将数据库模板转换为前端显示格式
|
||||||
const mockTemplates = [
|
function transformTemplate(template: ContractTemplate) {
|
||||||
{
|
// 模拟使用次数和评分(实际项目中可以从其他表获取)
|
||||||
id: '1',
|
const mockUsageCount = Math.floor(Math.random() * 2000) + 100;
|
||||||
title: '烟草产品销售合同(2023版)',
|
const mockRating = (Math.random() * 1.5 + 3.5).toFixed(1);
|
||||||
type: '标准版',
|
|
||||||
description: '最新版本的烟草产品销售合同模板,包含完整的法律条款、风险控制措施和行业标准要求。',
|
// 根据模板属性确定类型
|
||||||
updateTime: '2023-10-25',
|
let templateType = '标准版';
|
||||||
useCount: 2156,
|
if (template.is_featured) {
|
||||||
rating: 4.9,
|
templateType = '推荐版';
|
||||||
category: '销售合同'
|
} else if (template.description && template.description.includes('简化')) {
|
||||||
},
|
templateType = '简化版';
|
||||||
{
|
} else if (template.description && (template.description.includes('专业') || template.description.includes('大客户'))) {
|
||||||
id: '2',
|
templateType = '专业版';
|
||||||
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: '采购合同'
|
|
||||||
}
|
}
|
||||||
];
|
|
||||||
|
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) {
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const category = url.searchParams.get('category') || '';
|
const category = url.searchParams.get('category') || '';
|
||||||
|
const category_id = url.searchParams.get('category_id') || '';
|
||||||
const type = url.searchParams.get('type') || '';
|
const type = url.searchParams.get('type') || '';
|
||||||
const page = parseInt(url.searchParams.get('page') || '1');
|
const page = parseInt(url.searchParams.get('page') || '1');
|
||||||
const pageSize = 6;
|
const pageSize = 6;
|
||||||
|
|
||||||
// 筛选模板
|
try {
|
||||||
let filteredTemplates = mockTemplates;
|
// 构建搜索参数
|
||||||
|
const searchParams: TemplateSearchParams = {
|
||||||
if (category) {
|
page,
|
||||||
filteredTemplates = filteredTemplates.filter(t => t.category === category);
|
pageSize,
|
||||||
}
|
sortBy: 'updated_at',
|
||||||
|
sortOrder: 'desc'
|
||||||
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);
|
|
||||||
|
|
||||||
return {
|
// 优先使用category_id,其次使用category名称
|
||||||
templates,
|
if (category_id) {
|
||||||
total,
|
searchParams.category_id = parseInt(category_id);
|
||||||
page,
|
} else if (category) {
|
||||||
pageSize,
|
searchParams.category = category;
|
||||||
category,
|
}
|
||||||
type
|
|
||||||
};
|
// 并行获取模板数据和分类数据
|
||||||
|
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() {
|
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 [searchParams] = useSearchParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [activeFilter, setActiveFilter] = useState(category || '全部');
|
const [activeFilter, setActiveFilter] = useState(category || '全部');
|
||||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||||
const [sortBy, setSortBy] = useState('newest');
|
const [sortBy, setSortBy] = useState('newest');
|
||||||
|
|
||||||
|
// 监听category变化,同步更新activeFilter状态
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveFilter(category || '全部');
|
||||||
|
}, [category]);
|
||||||
|
|
||||||
const handleTemplateClick = (templateId: string) => {
|
const handleTemplateClick = (templateId: string) => {
|
||||||
navigate(`/contract-template/detail/${templateId}`);
|
navigate(`/contract-template/detail/${templateId}`);
|
||||||
};
|
};
|
||||||
@@ -155,8 +163,17 @@ export default function ContractTemplateList() {
|
|||||||
|
|
||||||
if (filter === '全部') {
|
if (filter === '全部') {
|
||||||
params.delete('category');
|
params.delete('category');
|
||||||
|
params.delete('category_id');
|
||||||
} else {
|
} 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'); // 重置页码
|
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 = [
|
const filters = [
|
||||||
{ label: '全部', count: mockTemplates.length },
|
{ label: '全部', count: totalAllTemplates },
|
||||||
...allCategories.map(cat => ({
|
...categories.map(cat => ({
|
||||||
label: cat,
|
label: cat.name,
|
||||||
count: mockTemplates.filter(t => t.category === cat).length
|
count: cat.template_count || 0
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -183,7 +202,7 @@ export default function ContractTemplateList() {
|
|||||||
const currentCategory = category || '全部';
|
const currentCategory = category || '全部';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="contract-search-results">
|
<div className="contract-search-results" key={`${category}-${category_id}-${page}`}>
|
||||||
{/* 页面头部 */}
|
{/* 页面头部 */}
|
||||||
<div className="result-header">
|
<div className="result-header">
|
||||||
<div>
|
<div>
|
||||||
@@ -195,13 +214,33 @@ export default function ContractTemplateList() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<SearchResultHeader
|
{/* 视图切换 */}
|
||||||
total={0}
|
<div className="view-toggle">
|
||||||
viewMode={viewMode}
|
<button
|
||||||
onViewModeChange={setViewMode}
|
className={`view-btn ${viewMode === 'grid' ? 'active' : ''}`}
|
||||||
sortBy={sortBy}
|
onClick={() => setViewMode('grid')}
|
||||||
onSortChange={setSortBy}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { MetaFunction } from '@remix-run/node';
|
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 { 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';
|
import styles from '~/styles/pages/contract-template.css?url';
|
||||||
|
|
||||||
export const links = () => [
|
export const links = () => [
|
||||||
@@ -22,18 +24,50 @@ export const handle = {
|
|||||||
breadcrumb: "智能搜索"
|
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() {
|
export default function ContractTemplateSearchIndex() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { categories } = useLoaderData<typeof loader>();
|
||||||
// 模拟分类数据
|
|
||||||
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 handleSearch = (query: string) => {
|
const handleSearch = (query: string) => {
|
||||||
if (query.trim()) {
|
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 (
|
return (
|
||||||
@@ -51,13 +89,13 @@ export default function ContractTemplateSearchIndex() {
|
|||||||
<div className="quick-categories">
|
<div className="quick-categories">
|
||||||
{categories.map((category, index) => (
|
{categories.map((category, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={category.id || index}
|
||||||
className="category-card"
|
className="category-card"
|
||||||
onClick={() => handleCategoryClick(category.name)}
|
onClick={() => handleCategoryClick(category.id)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleCategoryClick(category.name);
|
handleCategoryClick(category.id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { SearchResultHeader } from '~/components/contract-template/SearchResultH
|
|||||||
import { FilterTabs } from '~/components/contract-template/FilterTabs';
|
import { FilterTabs } from '~/components/contract-template/FilterTabs';
|
||||||
import { TemplateGrid } from '~/components/contract-template/TemplateGrid';
|
import { TemplateGrid } from '~/components/contract-template/TemplateGrid';
|
||||||
import { Pagination } from '~/components/ui/Pagination';
|
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';
|
import styles from '~/styles/pages/contract-template.css?url';
|
||||||
|
|
||||||
export const links = () => [
|
export const links = () => [
|
||||||
@@ -27,172 +29,161 @@ export const handle = {
|
|||||||
breadcrumb: "搜索结果"
|
breadcrumb: "搜索结果"
|
||||||
};
|
};
|
||||||
|
|
||||||
// 模拟数据 - 扩展搜索结果
|
// 带搜索统计的分类类型
|
||||||
const mockSearchResults = [
|
interface CategoryWithSearchCount extends ContractCategory {
|
||||||
{
|
searchCount: number;
|
||||||
id: '1',
|
}
|
||||||
title: '烟草产品销售合同标准模板',
|
|
||||||
type: '销售合同',
|
// 前端显示的模板类型
|
||||||
description: '适用于烟草产品销售业务,包含完整的违约责任条款、付款方式、交付条件等核心要素,符合行业规范要求。',
|
interface DisplayTemplate {
|
||||||
updateTime: '2023-10-25',
|
id: string;
|
||||||
useCount: 1248,
|
title: string;
|
||||||
rating: 4.8,
|
type: string;
|
||||||
category: '销售合同'
|
description: string;
|
||||||
},
|
updateTime: string;
|
||||||
{
|
useCount: number;
|
||||||
id: '2',
|
rating: number;
|
||||||
title: '零售商销售协议模板',
|
category: string;
|
||||||
type: '销售合同',
|
}
|
||||||
description: '专为零售商设计的销售协议,详细规定了违约责任、退换货政策、结算方式等条款。',
|
|
||||||
updateTime: '2023-10-20',
|
// 将数据库模板转换为前端显示格式
|
||||||
useCount: 856,
|
function transformTemplate(template: ContractTemplate) {
|
||||||
rating: 4.6,
|
// 模拟使用次数和评分(实际项目中可以从其他表获取)
|
||||||
category: '销售合同'
|
const mockUsageCount = Math.floor(Math.random() * 2000) + 100;
|
||||||
},
|
const mockRating = (Math.random() * 1.5 + 3.5).toFixed(1);
|
||||||
{
|
|
||||||
id: '3',
|
return {
|
||||||
title: '设备采购合同(含违约条款)',
|
id: template.id.toString(),
|
||||||
type: '采购合同',
|
title: template.title,
|
||||||
description: '设备采购专用合同模板,包含详细的违约责任条款、质量保证、验收标准等内容。',
|
type: template.is_featured ? '推荐版' : '标准版',
|
||||||
updateTime: '2023-10-18',
|
description: template.description || '',
|
||||||
useCount: 642,
|
updateTime: new Date(template.updated_at).toLocaleDateString('zh-CN'),
|
||||||
rating: 4.7,
|
useCount: mockUsageCount,
|
||||||
category: '采购合同'
|
rating: parseFloat(mockRating),
|
||||||
},
|
category: template.category?.name || '其他'
|
||||||
{
|
};
|
||||||
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: '租赁合同'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export async function loader({ request }: LoaderFunctionArgs) {
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const query = url.searchParams.get('q') || '';
|
const query = url.searchParams.get('q') || '';
|
||||||
const category = url.searchParams.get('category') || '';
|
const category = url.searchParams.get('category') || '';
|
||||||
const page = parseInt(url.searchParams.get('page') || '1');
|
const page = parseInt(url.searchParams.get('page') || '1');
|
||||||
|
const pageSize = 6;
|
||||||
|
|
||||||
// 模拟搜索耗时
|
// 记录搜索开始时间
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
// 这里应该是实际的搜索逻辑
|
try {
|
||||||
// 目前返回模拟数据
|
// 并行获取搜索结果和分类数据
|
||||||
let filteredResults = mockSearchResults;
|
const [searchResponse, categoriesResponse] = await Promise.all([
|
||||||
|
searchContractTemplates(query, {
|
||||||
// 如果有查询条件,进行筛选
|
category,
|
||||||
if (query || category) {
|
page,
|
||||||
filteredResults = mockSearchResults.filter(item => {
|
pageSize,
|
||||||
const matchesQuery = !query ||
|
sortBy: 'updated_at',
|
||||||
item.title.toLowerCase().includes(query.toLowerCase()) ||
|
sortOrder: 'desc'
|
||||||
item.description.toLowerCase().includes(query.toLowerCase());
|
}),
|
||||||
const matchesCategory = !category || item.category === category;
|
getContractCategories()
|
||||||
return matchesQuery && matchesCategory;
|
]);
|
||||||
});
|
|
||||||
|
// 处理搜索结果
|
||||||
|
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() {
|
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 [searchParams] = useSearchParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [activeFilter, setActiveFilter] = useState('全部');
|
const [activeFilter, setActiveFilter] = useState('全部');
|
||||||
@@ -211,7 +202,16 @@ export default function ContractTemplateSearchResults() {
|
|||||||
|
|
||||||
const handleFilterChange = (filter: string) => {
|
const handleFilterChange = (filter: string) => {
|
||||||
setActiveFilter(filter);
|
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) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
@@ -220,11 +220,13 @@ export default function ContractTemplateSearchResults() {
|
|||||||
navigate(`/contract-template/search/results?${params.toString()}`);
|
navigate(`/contract-template/search/results?${params.toString()}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 动态生成筛选选项
|
||||||
const filters = [
|
const filters = [
|
||||||
{ label: '全部', count: total },
|
{ label: '全部', count: total },
|
||||||
{ label: '销售合同', count: results.filter(r => r.category === '销售合同').length },
|
...categories.map(cat => ({
|
||||||
{ label: '采购合同', count: results.filter(r => r.category === '采购合同').length },
|
label: cat.name,
|
||||||
{ label: '服务合同', count: results.filter(r => r.category === '服务合同').length }
|
count: cat.searchCount || 0
|
||||||
|
}))
|
||||||
];
|
];
|
||||||
|
|
||||||
const totalPages = Math.ceil(total / pageSize);
|
const totalPages = Math.ceil(total / pageSize);
|
||||||
|
|||||||
Reference in New Issue
Block a user