合同初步可以访问
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
interface CompactSearchBoxProps {
|
||||
onSearch: (query: string) => void;
|
||||
initialQuery?: string;
|
||||
searchTime?: string;
|
||||
}
|
||||
|
||||
export function CompactSearchBox({
|
||||
onSearch,
|
||||
initialQuery = '',
|
||||
searchTime = '搜索用时 0.3秒'
|
||||
}: CompactSearchBoxProps) {
|
||||
const [searchQuery, setSearchQuery] = useState(initialQuery);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const handleSearchInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value;
|
||||
setSearchQuery(value);
|
||||
|
||||
// 自动调整高度
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto';
|
||||
textareaRef.current.style.height = Math.max(60, textareaRef.current.scrollHeight) + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
if (searchQuery.trim()) {
|
||||
onSearch(searchQuery);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
handleSearch();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
marginBottom: '24px'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '20px 0',
|
||||
margin: '0',
|
||||
background: 'none'
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: '600px',
|
||||
margin: '0 auto',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div style={{
|
||||
background: 'white',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
padding: '12px 16px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)',
|
||||
transition: 'all 0.3s ease'
|
||||
}}>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
placeholder="例如:销售合同、设备采购协议、包含违约责任条款的合同..."
|
||||
value={searchQuery}
|
||||
onChange={handleSearchInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-label="合同模板搜索输入框"
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: '60px',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
resize: 'vertical',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
background: 'transparent',
|
||||
margin: '0',
|
||||
padding: '0',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 搜索操作区域 - 水平布局 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
marginTop: '8px',
|
||||
paddingTop: '8px',
|
||||
borderTop: '1px solid #f0f0f0'
|
||||
}}>
|
||||
{/* 搜索时间提示 - 左侧 */}
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flex: '1'
|
||||
}}>
|
||||
<i className="ri-time-line" style={{ marginRight: '4px', fontSize: '11px' }}></i>
|
||||
{searchTime}
|
||||
</div>
|
||||
|
||||
{/* 重新搜索按钮 - 右侧 */}
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={!searchQuery.trim()}
|
||||
aria-label="重新搜索"
|
||||
style={{
|
||||
background: '#00684a',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
padding: '6px 16px',
|
||||
fontSize: '13px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '6px',
|
||||
margin: '0',
|
||||
flexShrink: '0'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!e.currentTarget.disabled) {
|
||||
e.currentTarget.style.background = '#005a40';
|
||||
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!e.currentTarget.disabled) {
|
||||
e.currentTarget.style.background = '#00684a';
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i className="ri-search-line" style={{ fontSize: '13px' }}></i>
|
||||
重新搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
interface ContractSearchHeroProps {
|
||||
onSearch: (query: string) => void;
|
||||
initialQuery?: string;
|
||||
tipText?: string;
|
||||
tipIcon?: string;
|
||||
}
|
||||
|
||||
export function ContractSearchHero({
|
||||
onSearch,
|
||||
initialQuery = '',
|
||||
tipText = '支持自然语言描述,AI将为您匹配最相关的模板',
|
||||
tipIcon = 'ri-lightbulb-line'
|
||||
}: ContractSearchHeroProps) {
|
||||
const [searchQuery, setSearchQuery] = useState(initialQuery);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const handleSearchInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value;
|
||||
setSearchQuery(value);
|
||||
|
||||
// 自动调整高度
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto';
|
||||
textareaRef.current.style.height = Math.max(80, textareaRef.current.scrollHeight) + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
onSearch(searchQuery);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
handleSearch();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '60px 0',
|
||||
background: 'linear-gradient(135deg, rgba(0, 104, 74, 0.05) 0%, rgba(0, 104, 74, 0.02) 100%)',
|
||||
borderRadius: '16px',
|
||||
marginBottom: '32px'
|
||||
}}>
|
||||
<h1 style={{
|
||||
fontSize: '32px',
|
||||
fontWeight: '600',
|
||||
color: 'rgba(0, 0, 0, 0.85)',
|
||||
marginBottom: '12px',
|
||||
margin: '0 0 12px 0'
|
||||
}}>AI智能合同模板搜索</h1>
|
||||
|
||||
<p style={{
|
||||
fontSize: '16px',
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
marginBottom: '40px',
|
||||
margin: '0 0 40px 0'
|
||||
}}>输入合同名称、用途或关键内容,快速找到最适合的模板</p>
|
||||
|
||||
<div style={{
|
||||
maxWidth: '600px',
|
||||
margin: '0 auto',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div style={{
|
||||
background: 'white',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '12px',
|
||||
padding: '16px 20px',
|
||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.08)',
|
||||
transition: 'all 0.3s ease'
|
||||
}}>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
placeholder="例如:销售合同、设备采购协议、包含违约责任条款的合同..."
|
||||
value={searchQuery}
|
||||
onChange={handleSearchInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-label="合同模板搜索输入框"
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: '80px',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
resize: 'vertical',
|
||||
fontSize: '16px',
|
||||
lineHeight: '1.5',
|
||||
background: 'transparent',
|
||||
margin: '0',
|
||||
padding: '0',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 搜索操作区域 - 水平布局 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
marginTop: '16px',
|
||||
paddingTop: '16px',
|
||||
borderTop: '1px solid #f0f0f0'
|
||||
}}>
|
||||
{/* 提示文字 - 左侧 */}
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flex: '1'
|
||||
}}>
|
||||
<i className={tipIcon} style={{ marginRight: '4px', fontSize: '12px' }}></i>
|
||||
{tipText}
|
||||
</div>
|
||||
|
||||
{/* 搜索按钮 - 右侧 */}
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={!searchQuery.trim()}
|
||||
aria-label="开始搜索"
|
||||
style={{
|
||||
background: '#00684a',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
padding: '10px 24px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '6px',
|
||||
margin: '0',
|
||||
flexShrink: '0'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!e.currentTarget.disabled) {
|
||||
e.currentTarget.style.background = '#005a40';
|
||||
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!e.currentTarget.disabled) {
|
||||
e.currentTarget.style.background = '#00684a';
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i className="ri-search-line" style={{ fontSize: '14px' }}></i>
|
||||
智能搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
interface FilterItem {
|
||||
label: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface FilterTabsProps {
|
||||
filters: FilterItem[];
|
||||
activeFilter: string;
|
||||
onFilterChange: (filter: string) => void;
|
||||
}
|
||||
|
||||
export function FilterTabs({ filters, activeFilter, onFilterChange }: FilterTabsProps) {
|
||||
return (
|
||||
<div className="filter-tabs">
|
||||
{filters.map((filter) => (
|
||||
<button
|
||||
key={filter.label}
|
||||
className={`filter-tab ${activeFilter === filter.label ? 'active' : ''}`}
|
||||
onClick={() => onFilterChange(filter.label)}
|
||||
>
|
||||
{filter.label} ({filter.count})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useNavigate } from '@remix-run/react';
|
||||
|
||||
interface CategoryItem {
|
||||
name: string;
|
||||
icon: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface QuickCategoriesProps {
|
||||
categories: CategoryItem[];
|
||||
onCategoryClick?: (category: string) => void;
|
||||
}
|
||||
|
||||
export function QuickCategories({ categories, onCategoryClick }: QuickCategoriesProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleCategoryClick = (category: CategoryItem) => {
|
||||
if (onCategoryClick) {
|
||||
onCategoryClick(category.name);
|
||||
} else {
|
||||
// 默认导航到合同列表页面并筛选分类
|
||||
navigate(`/contract-template/list?category=${encodeURIComponent(category.name)}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="quick-categories">
|
||||
{categories.map((category) => (
|
||||
<div
|
||||
key={category.name}
|
||||
className="category-card"
|
||||
onClick={() => handleCategoryClick(category)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleCategoryClick(category);
|
||||
}
|
||||
}}
|
||||
aria-label={`选择${category.name}分类,共${category.count}个模板`}
|
||||
>
|
||||
<div className="category-icon">
|
||||
<i className={category.icon}></i>
|
||||
</div>
|
||||
<div className="category-title">{category.name}</div>
|
||||
<div className="category-count">{category.count}个模板</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
interface SearchResultHeaderProps {
|
||||
total: number;
|
||||
viewMode: 'grid' | 'list';
|
||||
onViewModeChange: (mode: 'grid' | 'list') => void;
|
||||
sortBy: string;
|
||||
onSortChange: (sort: string) => void;
|
||||
}
|
||||
|
||||
export function SearchResultHeader({
|
||||
total,
|
||||
viewMode,
|
||||
onViewModeChange,
|
||||
sortBy,
|
||||
onSortChange
|
||||
}: SearchResultHeaderProps) {
|
||||
return (
|
||||
<div className="result-header">
|
||||
<div className="result-info">
|
||||
为您找到 <span className="result-count">{total}</span> 个相关模板
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="view-toggle">
|
||||
<button
|
||||
className={`view-btn ${viewMode === 'grid' ? 'active' : ''}`}
|
||||
onClick={() => onViewModeChange('grid')}
|
||||
aria-label="网格视图"
|
||||
>
|
||||
<i className="ri-grid-line"></i>
|
||||
</button>
|
||||
<button
|
||||
className={`view-btn ${viewMode === 'list' ? 'active' : ''}`}
|
||||
onClick={() => onViewModeChange('list')}
|
||||
aria-label="列表视图"
|
||||
>
|
||||
<i className="ri-list-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
<select
|
||||
className="px-3 py-2 border border-gray-200 rounded-lg text-sm"
|
||||
value={sortBy}
|
||||
onChange={(e) => onSortChange(e.target.value)}
|
||||
>
|
||||
<option value="relevance">相关度排序</option>
|
||||
<option value="newest">最新更新</option>
|
||||
<option value="popular">使用频率</option>
|
||||
<option value="rating">评分最高</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from '@remix-run/react';
|
||||
|
||||
interface Template {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
description: string;
|
||||
updateTime: string;
|
||||
useCount: number;
|
||||
rating: number;
|
||||
category: string;
|
||||
}
|
||||
|
||||
interface TemplateCardProps {
|
||||
template: Template;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function TemplateCard({ template, onClick }: TemplateCardProps) {
|
||||
const [isFavorited, setIsFavorited] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleFavoriteClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setIsFavorited(!isFavorited);
|
||||
};
|
||||
|
||||
const handleActionClick = (e: React.MouseEvent, action: string) => {
|
||||
e.stopPropagation();
|
||||
|
||||
switch (action) {
|
||||
case '立即使用':
|
||||
console.log('下载并使用模板:', template.id);
|
||||
// 这里应该触发下载逻辑
|
||||
break;
|
||||
case '预览':
|
||||
// 导航到模板详情页面
|
||||
navigate(`/contract-template/detail/${template.id}`);
|
||||
break;
|
||||
default:
|
||||
console.log(`执行操作: ${action}`, template.id);
|
||||
}
|
||||
};
|
||||
|
||||
const renderStars = (rating: number) => {
|
||||
const stars = [];
|
||||
const fullStars = Math.floor(rating);
|
||||
const hasHalfStar = rating % 1 !== 0;
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (i < fullStars) {
|
||||
stars.push(<i key={i} className="ri-star-fill text-xs"></i>);
|
||||
} else if (i === fullStars && hasHalfStar) {
|
||||
stars.push(<i key={i} className="ri-star-half-fill text-xs"></i>);
|
||||
} else {
|
||||
stars.push(<i key={i} className="ri-star-line text-xs"></i>);
|
||||
}
|
||||
}
|
||||
return stars;
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="template-card"
|
||||
onClick={onClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`查看${template.title}详情`}
|
||||
>
|
||||
<div className="template-header">
|
||||
<div className="template-type">{template.type}</div>
|
||||
<div className="flex items-center gap-1 text-yellow-500">
|
||||
{renderStars(template.rating)}
|
||||
<span className="text-xs ml-1">{template.rating}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="template-title">{template.title}</h3>
|
||||
<p className="template-desc">{template.description}</p>
|
||||
|
||||
<div className="template-meta">
|
||||
<span>更新时间:{template.updateTime}</span>
|
||||
<span>使用次数:{template.useCount.toLocaleString()}</span>
|
||||
</div>
|
||||
|
||||
<div className="template-actions">
|
||||
<button
|
||||
className="action-btn primary"
|
||||
onClick={(e) => handleActionClick(e, '立即使用')}
|
||||
>
|
||||
立即使用
|
||||
</button>
|
||||
<button
|
||||
className="action-btn"
|
||||
onClick={(e) => handleActionClick(e, '预览')}
|
||||
>
|
||||
预览
|
||||
</button>
|
||||
<button
|
||||
className="action-btn"
|
||||
onClick={handleFavoriteClick}
|
||||
title={isFavorited ? '取消收藏' : '收藏'}
|
||||
>
|
||||
<i className={isFavorited ? 'ri-star-fill text-yellow-500' : 'ri-star-line'}></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { TemplateCard } from './TemplateCard';
|
||||
|
||||
interface Template {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
description: string;
|
||||
updateTime: string;
|
||||
useCount: number;
|
||||
rating: number;
|
||||
category: string;
|
||||
}
|
||||
|
||||
interface TemplateGridProps {
|
||||
templates: Template[];
|
||||
viewMode: 'grid' | 'list';
|
||||
onTemplateClick: (templateId: string) => void;
|
||||
}
|
||||
|
||||
export function TemplateGrid({ templates, viewMode, onTemplateClick }: TemplateGridProps) {
|
||||
return (
|
||||
<div className={`template-grid ${viewMode === 'list' ? 'list-view' : ''}`}>
|
||||
{templates.map((template) => (
|
||||
<TemplateCard
|
||||
key={template.id}
|
||||
template={template}
|
||||
onClick={() => onTemplateClick(template.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ interface MenuItem {
|
||||
title: string;
|
||||
path: string;
|
||||
icon: string;
|
||||
hideBreadcrumb?: boolean;
|
||||
children?: MenuItem[];
|
||||
}
|
||||
|
||||
@@ -26,6 +27,26 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
|
||||
hideBreadcrumb: true,
|
||||
icon: 'ri-search-line'
|
||||
},
|
||||
{
|
||||
id: 'contract-template',
|
||||
title: '合同模板',
|
||||
path: '/contract-template',
|
||||
icon: 'ri-file-search-line',
|
||||
children: [
|
||||
{
|
||||
id: 'contract-search-ai',
|
||||
title: '智能搜索',
|
||||
path: '/contract-template/search',
|
||||
icon: 'ri-search-line'
|
||||
},
|
||||
{
|
||||
id: 'contract-list',
|
||||
title: '合同列表',
|
||||
path: '/contract-template/list',
|
||||
icon: 'ri-folder-line'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'home',
|
||||
title: '系统概览',
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
import type { MetaFunction, LoaderFunctionArgs } from '@remix-run/node';
|
||||
import { useLoaderData, useNavigate } from '@remix-run/react';
|
||||
import { useState } from 'react';
|
||||
import styles from '~/styles/pages/contract-template.css?url';
|
||||
|
||||
export const links = () => [
|
||||
{ rel: 'stylesheet', href: styles }
|
||||
];
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
return [
|
||||
{ title: `${data?.template.title || '合同模板详情'} - 智慧法务` },
|
||||
{
|
||||
name: 'description',
|
||||
content: data?.template.description || '查看合同模板详细信息'
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
// 面包屑导航配置
|
||||
export const handle = {
|
||||
breadcrumb: (data: { template: { title: string } }) => {
|
||||
return data?.template?.title || "模板详情";
|
||||
}
|
||||
};
|
||||
|
||||
// 模拟详细数据
|
||||
const getTemplateDetail = (id: string) => {
|
||||
const templates = {
|
||||
'1': {
|
||||
id: '1',
|
||||
title: '烟草产品销售合同(2023版)',
|
||||
type: '销售合同 · 标准版',
|
||||
description: '本模板是专为烟草行业设计的标准销售合同,严格遵循《烟草专卖法》等相关法律法规,涵盖了烟草产品销售过程中的各个关键环节。',
|
||||
updateTime: '2023年10月25日',
|
||||
useCount: 2156,
|
||||
rating: 4.9,
|
||||
fileSize: '245KB',
|
||||
scope: '烟草产品销售',
|
||||
legalBasis: '《合同法》《烟草专卖法》',
|
||||
templateCode: 'XS-2023-001',
|
||||
reviews: [
|
||||
{
|
||||
user: '李经理',
|
||||
rating: 5,
|
||||
comment: '模板非常专业,条款完整,符合行业规范。我们公司一直在使用这个模板,效果很好。',
|
||||
date: '2023-10-20'
|
||||
},
|
||||
{
|
||||
user: '王总',
|
||||
rating: 4,
|
||||
comment: '模板结构清晰,易于理解和使用。特别是违约责任条款写得很详细,对我们很有帮助。',
|
||||
date: '2023-10-18'
|
||||
}
|
||||
],
|
||||
features: [
|
||||
{ title: '法律合规', description: '严格遵循烟草行业法律法规,确保合同条款合法有效', icon: 'ri-shield-check-line', color: 'green' },
|
||||
{ title: '条款完整', description: '涵盖销售全流程,条款结构完整,逻辑清晰', icon: 'ri-settings-3-line', color: 'blue' },
|
||||
{ title: '易于定制', description: '模板化设计,可根据具体业务需求灵活调整', icon: 'ri-edit-line', color: 'purple' },
|
||||
{ title: '行业标准', description: '符合烟草行业标准,被广泛使用和认可', icon: 'ri-award-line', color: 'orange' }
|
||||
],
|
||||
structure: [
|
||||
{ step: 1, title: '合同主体', description: '甲乙双方基本信息、资质证明' },
|
||||
{ step: 2, title: '标的物条款', description: '产品名称、规格、数量、质量标准' },
|
||||
{ step: 3, title: '价格与付款', description: '价格条款、付款方式、结算周期' },
|
||||
{ step: 4, title: '交付与验收', description: '交付时间、地点、方式、验收标准' },
|
||||
{ step: 5, title: '违约责任', description: '违约情形、责任承担、损失赔偿' },
|
||||
{ step: 6, title: '争议解决', description: '争议处理方式、管辖法院' }
|
||||
],
|
||||
preview: `
|
||||
中文合同预览内容...
|
||||
|
||||
烟草产品销售合同
|
||||
合同编号:_______________
|
||||
|
||||
甲方(销售方):_________________________
|
||||
地址:_____________________________________
|
||||
法定代表人:_______________ 联系电话:_______________
|
||||
烟草专卖许可证号:_________________________
|
||||
|
||||
乙方(采购方):_________________________
|
||||
地址:_____________________________________
|
||||
法定代表人:_______________ 联系电话:_______________
|
||||
烟草专卖零售许可证号:_____________________
|
||||
|
||||
根据《中华人民共和国合同法》、《中华人民共和国烟草专卖法》等相关法律法规,
|
||||
甲乙双方在平等、自愿、公平、诚信的基础上,就烟草产品销售事宜达成如下协议:
|
||||
|
||||
第一条 标的物
|
||||
1.1 产品名称:_________________________
|
||||
1.2 产品规格:_________________________
|
||||
1.3 产品数量:_________________________
|
||||
1.4 产品单价:_________________________
|
||||
1.5 合同总金额:_______________________
|
||||
|
||||
... 更多条款内容请下载完整模板查看 ...
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
return templates[id as keyof typeof templates] || null;
|
||||
};
|
||||
|
||||
export async function loader({ params }: LoaderFunctionArgs) {
|
||||
const template = getTemplateDetail(params.id!);
|
||||
|
||||
if (!template) {
|
||||
throw new Response('模板未找到', { status: 404 });
|
||||
}
|
||||
|
||||
return { template };
|
||||
}
|
||||
|
||||
export default function ContractTemplateDetail() {
|
||||
const { template } = useLoaderData<typeof loader>();
|
||||
const navigate = useNavigate();
|
||||
const [isFavorited, setIsFavorited] = useState(false);
|
||||
|
||||
const handleBack = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
console.log('下载模板:', template.id);
|
||||
// 这里应该是实际的下载逻辑
|
||||
};
|
||||
|
||||
const handlePreview = () => {
|
||||
console.log('预览模板:', template.id);
|
||||
// 这里应该打开预览模态框或新页面
|
||||
};
|
||||
|
||||
const handleFavorite = () => {
|
||||
setIsFavorited(!isFavorited);
|
||||
console.log('收藏状态:', !isFavorited);
|
||||
};
|
||||
|
||||
const handleShare = () => {
|
||||
console.log('分享模板:', template.id);
|
||||
// 这里应该是分享功能
|
||||
};
|
||||
|
||||
const renderStars = (rating: number) => {
|
||||
const stars = [];
|
||||
const fullStars = Math.floor(rating);
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (i < fullStars) {
|
||||
stars.push(<i key={i} className="ri-star-fill"></i>);
|
||||
} else {
|
||||
stars.push(<i key={i} className="ri-star-line"></i>);
|
||||
}
|
||||
}
|
||||
return stars;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="contract-search-results">
|
||||
{/* 返回按钮 */}
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="flex items-center px-3 py-2 text-sm border border-gray-200 rounded-lg hover:border-primary-color transition-colors"
|
||||
>
|
||||
<i className="ri-arrow-left-line mr-2"></i>
|
||||
返回列表
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 模板详情 */}
|
||||
<div className="template-detail max-w-4xl mx-auto">
|
||||
{/* 详情头部 */}
|
||||
<div className="detail-header bg-white rounded-xl p-8 mb-6 border border-gray-100">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="template-type">{template.type}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 text-yellow-500">
|
||||
{renderStars(template.rating)}
|
||||
<span className="text-sm ml-1">{template.rating} (156评价)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 className="detail-title text-3xl font-semibold mb-6">{template.title}</h1>
|
||||
|
||||
<div className="detail-meta grid grid-cols-2 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="meta-item">
|
||||
<span className="meta-label text-gray-500">模板编号:</span>
|
||||
<span>{template.templateCode}</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<span className="meta-label text-gray-500">更新时间:</span>
|
||||
<span>{template.updateTime}</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<span className="meta-label text-gray-500">使用次数:</span>
|
||||
<span>{template.useCount.toLocaleString()}次</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<span className="meta-label text-gray-500">文件大小:</span>
|
||||
<span>{template.fileSize}</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<span className="meta-label text-gray-500">适用范围:</span>
|
||||
<span>{template.scope}</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<span className="meta-label text-gray-500">法律依据:</span>
|
||||
<span>{template.legalBasis}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="detail-actions flex gap-3">
|
||||
<button
|
||||
className="detail-btn primary bg-primary text-white px-6 py-3 rounded-lg flex items-center gap-2 hover:bg-primary-hover"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<i className="ri-download-line"></i>
|
||||
立即下载使用
|
||||
</button>
|
||||
<button
|
||||
className="detail-btn secondary bg-white border border-gray-200 px-6 py-3 rounded-lg flex items-center gap-2 hover:border-primary"
|
||||
onClick={handlePreview}
|
||||
>
|
||||
<i className="ri-eye-line"></i>
|
||||
在线预览
|
||||
</button>
|
||||
<button
|
||||
className={`detail-btn secondary bg-white border border-gray-200 px-6 py-3 rounded-lg flex items-center gap-2 hover:border-primary ${isFavorited ? 'text-yellow-500' : ''}`}
|
||||
onClick={handleFavorite}
|
||||
>
|
||||
<i className={isFavorited ? 'ri-star-fill' : 'ri-star-line'}></i>
|
||||
收藏模板
|
||||
</button>
|
||||
<button
|
||||
className="detail-btn secondary bg-white border border-gray-200 px-6 py-3 rounded-lg flex items-center gap-2 hover:border-primary"
|
||||
onClick={handleShare}
|
||||
>
|
||||
<i className="ri-share-line"></i>
|
||||
分享
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 详情内容 */}
|
||||
<div className="detail-content bg-white rounded-xl p-8 border border-gray-100">
|
||||
{/* 模板简介 */}
|
||||
<div className="content-section mb-8">
|
||||
<h3 className="section-title text-xl font-semibold mb-4">模板简介</h3>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
{template.description}模板包含完整的合同条款结构,
|
||||
包括合同主体、标的物、价格条款、交付方式、付款条件、违约责任、争议解决等核心内容。
|
||||
适用于各类烟草产品的销售业务,能够有效保护交易双方的合法权益。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 主要特点 */}
|
||||
<div className="content-section mb-8">
|
||||
<h3 className="section-title text-xl font-semibold mb-4">主要特点</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{template.features.map((feature, index) => (
|
||||
<div key={index} className={`bg-${feature.color}-50 p-4 rounded-lg border border-${feature.color}-200`}>
|
||||
<div className="flex items-center mb-2">
|
||||
<i className={`${feature.icon} text-${feature.color}-600 mr-2`}></i>
|
||||
<span className="font-medium">{feature.title}</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">{feature.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 合同条款结构 */}
|
||||
<div className="content-section mb-8">
|
||||
<h3 className="section-title text-xl font-semibold mb-4">合同条款结构</h3>
|
||||
<div className="space-y-3">
|
||||
{template.structure.map((item) => (
|
||||
<div key={item.step} className="flex items-center p-3 bg-gray-50 rounded-lg">
|
||||
<div className="w-8 h-8 bg-primary text-white rounded-full flex items-center justify-center text-sm font-medium mr-3">
|
||||
{item.step}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{item.title}</div>
|
||||
<div className="text-sm text-gray-600">{item.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 合同预览 */}
|
||||
<div className="content-section mb-8">
|
||||
<h3 className="section-title text-xl font-semibold mb-4">合同预览</h3>
|
||||
<div className="content-preview bg-gray-50 rounded-lg p-6 font-mono text-sm line-height-6 border-l-4 border-primary">
|
||||
<pre className="whitespace-pre-wrap">{template.preview}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 用户评价 */}
|
||||
<div className="content-section">
|
||||
<h3 className="section-title text-xl font-semibold mb-4">用户评价</h3>
|
||||
<div className="space-y-4">
|
||||
{template.reviews.map((review, index) => (
|
||||
<div key={index} className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center text-sm font-medium">
|
||||
{review.user[0]}
|
||||
</div>
|
||||
<span className="font-medium">{review.user}</span>
|
||||
<div className="flex items-center gap-1 text-yellow-500">
|
||||
{renderStars(review.rating)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">{review.date}</span>
|
||||
</div>
|
||||
<p className="text-gray-600">{review.comment}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
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 { FilterTabs } from '~/components/contract-template/FilterTabs';
|
||||
import { TemplateGrid } from '~/components/contract-template/TemplateGrid';
|
||||
import { Pagination } from '~/components/ui/Pagination';
|
||||
import styles from '~/styles/pages/contract-template.css?url';
|
||||
|
||||
export const links = () => [
|
||||
{ rel: 'stylesheet', href: styles }
|
||||
];
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: '合同模板列表 - 智慧法务' },
|
||||
{
|
||||
name: 'description',
|
||||
content: '浏览和管理所有合同模板,按分类查看各种类型的合同模板。'
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
// 模拟数据 - 完整的模板库
|
||||
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: '采购合同'
|
||||
}
|
||||
];
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const category = url.searchParams.get('category') || '';
|
||||
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);
|
||||
|
||||
return {
|
||||
templates,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
category,
|
||||
type
|
||||
};
|
||||
}
|
||||
|
||||
export default function ContractTemplateList() {
|
||||
const { templates, total, page, pageSize, category } = 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');
|
||||
|
||||
const handleTemplateClick = (templateId: string) => {
|
||||
navigate(`/contract-template/detail/${templateId}`);
|
||||
};
|
||||
|
||||
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/list?${params.toString()}`);
|
||||
};
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set('page', newPage.toString());
|
||||
navigate(`/contract-template/list?${params.toString()}`);
|
||||
};
|
||||
|
||||
// 动态生成筛选选项
|
||||
const allCategories = [...new Set(mockTemplates.map(t => t.category))];
|
||||
const filters = [
|
||||
{ label: '全部', count: mockTemplates.length },
|
||||
...allCategories.map(cat => ({
|
||||
label: cat,
|
||||
count: mockTemplates.filter(t => t.category === cat).length
|
||||
}))
|
||||
];
|
||||
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
const currentCategory = category || '全部';
|
||||
|
||||
return (
|
||||
<div className="contract-search-results">
|
||||
{/* 页面头部 */}
|
||||
<div className="result-header">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-2">
|
||||
{currentCategory === '全部' ? '合同模板库' : `${currentCategory}模板`}
|
||||
</h2>
|
||||
<div className="result-info">
|
||||
共 <span className="result-count">{total}</span> 个模板
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<SearchResultHeader
|
||||
total={0}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
sortBy={sortBy}
|
||||
onSortChange={setSortBy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 筛选标签 */}
|
||||
<FilterTabs
|
||||
filters={filters}
|
||||
activeFilter={activeFilter}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
|
||||
{/* 模板网格 */}
|
||||
<TemplateGrid
|
||||
templates={templates}
|
||||
viewMode={viewMode}
|
||||
onTemplateClick={handleTemplateClick}
|
||||
/>
|
||||
|
||||
{/* 分页 */}
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
total={total}
|
||||
pageSize={pageSize}
|
||||
onChange={handlePageChange}
|
||||
showPageSizeChanger={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 面包屑导航配置
|
||||
export const handle = {
|
||||
breadcrumb: "合同列表"
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { MetaFunction } from '@remix-run/node';
|
||||
import { useNavigate } from '@remix-run/react';
|
||||
import { ContractSearchHero } from '~/components/contract-template/ContractSearchHero';
|
||||
import styles from '~/styles/pages/contract-template.css?url';
|
||||
|
||||
export const links = () => [
|
||||
{ rel: 'stylesheet', href: styles }
|
||||
];
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: 'AI智能合同模板搜索 - 智慧法务' },
|
||||
{
|
||||
name: 'description',
|
||||
content: '使用AI智能搜索快速找到最适合的合同模板,支持自然语言描述搜索。'
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
// 面包屑导航配置
|
||||
export const handle = {
|
||||
breadcrumb: "智能搜索"
|
||||
};
|
||||
|
||||
export default function ContractTemplateSearchIndex() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 模拟分类数据
|
||||
const categories = [
|
||||
{ name: '销售合同', icon: 'ri-handshake-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) => {
|
||||
if (query.trim()) {
|
||||
navigate(`/contract-template/search/results?q=${encodeURIComponent(query)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCategoryClick = (categoryName: string) => {
|
||||
navigate(`/contract-template/list?category=${encodeURIComponent(categoryName)}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="contract-template-search">
|
||||
<ContractSearchHero onSearch={handleSearch} />
|
||||
<div className="quick-categories">
|
||||
{categories.map((category, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="category-card"
|
||||
onClick={() => handleCategoryClick(category.name)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleCategoryClick(category.name);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`选择${category.name}分类,共${category.count}个模板`}
|
||||
>
|
||||
<div className="category-icon">
|
||||
<i className={category.icon}></i>
|
||||
</div>
|
||||
<div className="category-title">{category.name}</div>
|
||||
<div className="category-count">{category.count}个模板</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
import type { MetaFunction, LoaderFunctionArgs } from '@remix-run/node';
|
||||
import { useLoaderData, useSearchParams, useNavigate } from '@remix-run/react';
|
||||
import { useState } from 'react';
|
||||
import { CompactSearchBox } from '~/components/contract-template/CompactSearchBox';
|
||||
import { SearchResultHeader } from '~/components/contract-template/SearchResultHeader';
|
||||
import { FilterTabs } from '~/components/contract-template/FilterTabs';
|
||||
import { TemplateGrid } from '~/components/contract-template/TemplateGrid';
|
||||
import { Pagination } from '~/components/ui/Pagination';
|
||||
import styles from '~/styles/pages/contract-template.css?url';
|
||||
|
||||
export const links = () => [
|
||||
{ rel: 'stylesheet', href: styles }
|
||||
];
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: '搜索结果 - AI智能合同模板搜索 - 智慧法务' },
|
||||
{
|
||||
name: 'description',
|
||||
content: 'AI智能搜索合同模板结果,快速找到最适合的模板。'
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
// 面包屑导航配置
|
||||
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: '租赁合同'
|
||||
}
|
||||
];
|
||||
|
||||
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 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;
|
||||
});
|
||||
}
|
||||
|
||||
// 计算搜索耗时
|
||||
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 [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const [activeFilter, setActiveFilter] = useState('全部');
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||
const [sortBy, setSortBy] = useState('relevance');
|
||||
|
||||
const handleSearch = (newQuery: string) => {
|
||||
if (newQuery.trim()) {
|
||||
navigate(`/contract-template/search/results?q=${encodeURIComponent(newQuery)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTemplateClick = (templateId: string) => {
|
||||
navigate(`/contract-template/detail/${templateId}`);
|
||||
};
|
||||
|
||||
const handleFilterChange = (filter: string) => {
|
||||
setActiveFilter(filter);
|
||||
// 这里可以添加实际的筛选逻辑
|
||||
};
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set('page', newPage.toString());
|
||||
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 }
|
||||
];
|
||||
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
|
||||
return (
|
||||
<div className="contract-search-results">
|
||||
{/* 紧凑搜索框 */}
|
||||
<CompactSearchBox
|
||||
initialQuery={query}
|
||||
onSearch={handleSearch}
|
||||
searchTime={searchTime}
|
||||
/>
|
||||
|
||||
{/* 搜索结果头部 */}
|
||||
<SearchResultHeader
|
||||
total={total}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
sortBy={sortBy}
|
||||
onSortChange={setSortBy}
|
||||
/>
|
||||
|
||||
{/* 筛选标签 */}
|
||||
<FilterTabs
|
||||
filters={filters}
|
||||
activeFilter={activeFilter}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
|
||||
{/* 模板网格 */}
|
||||
<TemplateGrid
|
||||
templates={results}
|
||||
viewMode={viewMode}
|
||||
onTemplateClick={handleTemplateClick}
|
||||
/>
|
||||
|
||||
{/* 分页 */}
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
total={total}
|
||||
pageSize={pageSize}
|
||||
onChange={handlePageChange}
|
||||
showPageSizeChanger={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Outlet } from '@remix-run/react';
|
||||
|
||||
// 面包屑导航配置
|
||||
export const handle = {
|
||||
breadcrumb: "合同模板"
|
||||
};
|
||||
|
||||
export default function ContractTemplate() {
|
||||
return <Outlet />;
|
||||
}
|
||||
@@ -29,6 +29,9 @@
|
||||
|
||||
/* @import './components/modal.css'; */
|
||||
|
||||
/* 导入页面特定样式 */
|
||||
@import './pages/contract-template.css';
|
||||
|
||||
/* Tailwind 基础指令 */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
@@ -0,0 +1,636 @@
|
||||
/* 合同模板页面样式 */
|
||||
:root {
|
||||
--primary-color: #00684a;
|
||||
--primary-hover: #005a40;
|
||||
--primary-light: rgba(0, 104, 74, 0.1);
|
||||
--success-color: #52c41a;
|
||||
--warning-color: #faad14;
|
||||
--error-color: #ff4d4f;
|
||||
--text-color: rgba(0, 0, 0, 0.85);
|
||||
--text-secondary: rgba(0, 0, 0, 0.45);
|
||||
--border-color: #f0f0f0;
|
||||
--bg-gray: #f5f5f5;
|
||||
--gradient-bg: linear-gradient(135deg, #f8fffe 0%, #f0f9ff 100%);
|
||||
}
|
||||
|
||||
.contract-template-search {
|
||||
padding: 24px;
|
||||
background: var(--gradient-bg);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.contract-search-results {
|
||||
padding: 24px;
|
||||
background: var(--gradient-bg);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 搜索英雄区域 */
|
||||
.search-hero {
|
||||
text-align: center;
|
||||
padding: 60px 0;
|
||||
background: linear-gradient(135deg, rgba(0, 104, 74, 0.05) 0%, rgba(0, 104, 74, 0.02) 100%);
|
||||
border-radius: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.search-title {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.search-subtitle {
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 搜索框样式 */
|
||||
.search-hero .search-box,
|
||||
.compact-search-box .search-box {
|
||||
background: white;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
padding: 16px 20px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-hero .search-box:focus-within,
|
||||
.compact-search-box .search-box:focus-within {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 4px 20px rgba(0, 104, 74, 0.15);
|
||||
}
|
||||
|
||||
.search-hero .search-textarea,
|
||||
.compact-search-box .search-textarea {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
background: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-hero .search-textarea::placeholder,
|
||||
.compact-search-box .search-textarea::placeholder {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.search-hero .search-actions,
|
||||
.compact-search-box .search-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.search-hero .search-tips,
|
||||
.compact-search-box .search-tips {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.search-hero .search-tips i,
|
||||
.compact-search-box .search-tips i {
|
||||
margin-right: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.search-hero .search-btn,
|
||||
.compact-search-box .search-btn {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-hero .search-btn:hover:not(:disabled),
|
||||
.compact-search-box .search-btn:hover:not(:disabled) {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.search-hero .search-btn:disabled,
|
||||
.compact-search-box .search-btn:disabled {
|
||||
background: #d1d5db;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.search-hero .search-btn i,
|
||||
.compact-search-box .search-btn i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 快速分类 */
|
||||
.quick-categories {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.category-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.category-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: var(--primary-light);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 12px;
|
||||
color: var(--primary-color);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.category-count {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 紧凑搜索框 */
|
||||
.compact-search-box {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.compact-search-box .search-hero {
|
||||
padding: 20px 0;
|
||||
margin-bottom: 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.compact-search-box .search-title {
|
||||
display: none; /* 在紧凑模式下隐藏大标题 */
|
||||
}
|
||||
|
||||
.compact-search-box .search-subtitle {
|
||||
display: none; /* 在紧凑模式下隐藏副标题 */
|
||||
}
|
||||
|
||||
.compact-search-box .search-container {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 紧凑搜索框特定样式 */
|
||||
.compact-search-box .search-box {
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.compact-search-box .search-textarea {
|
||||
min-height: 60px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.compact-search-box .search-actions {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.compact-search-box .search-btn {
|
||||
padding: 6px 16px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.compact-search-box .search-tips {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.compact-search-box .search-tips i {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 搜索结果头部 */
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.result-info {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.result-count {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 筛选标签 */
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: white;
|
||||
color: var(--text-color);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.filter-tab.active,
|
||||
.filter-tab:hover {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 模板网格 */
|
||||
.template-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.template-grid.list-view {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.list-view .template-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.template-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.list-view .template-header {
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.template-type {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary-color);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.template-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.template-desc {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.list-view .template-desc {
|
||||
margin-bottom: 0;
|
||||
margin-left: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.template-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.list-view .template-meta {
|
||||
margin-bottom: 0;
|
||||
margin-left: 16px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.template-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.list-view .template-actions {
|
||||
margin-left: 16px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: white;
|
||||
color: var(--text-color);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
/* 视图切换 */
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
padding: 4px;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 32px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: white;
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.page-btn:hover,
|
||||
.page-btn.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 模板详情页面 */
|
||||
.template-detail {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.detail-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: var(--text-secondary);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.detail-btn.primary {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.detail-btn.primary:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
.detail-btn.secondary {
|
||||
background: white;
|
||||
color: var(--text-color);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.detail-btn.secondary:hover {
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.content-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.content-preview {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.search-hero {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.search-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.quick-categories {
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
}
|
||||
|
||||
.template-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// 合同模板相关类型定义
|
||||
|
||||
export interface Template {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
description: string;
|
||||
updateTime: string;
|
||||
useCount: number;
|
||||
rating: number;
|
||||
category: string;
|
||||
}
|
||||
|
||||
export interface TemplateDetail extends Template {
|
||||
fileSize: string;
|
||||
scope: string;
|
||||
legalBasis: string;
|
||||
templateCode: string;
|
||||
reviews: Review[];
|
||||
features: Feature[];
|
||||
structure: StructureItem[];
|
||||
preview: string;
|
||||
}
|
||||
|
||||
export interface Review {
|
||||
user: string;
|
||||
rating: number;
|
||||
comment: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export interface Feature {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface StructureItem {
|
||||
step: number;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface FilterItem {
|
||||
label: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface CategoryItem {
|
||||
name: string;
|
||||
icon: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export type ViewMode = 'grid' | 'list';
|
||||
export type SortBy = 'relevance' | 'newest' | 'popular' | 'rating';
|
||||
Reference in New Issue
Block a user