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

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
+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>