合同初步可以访问
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: '系统概览',
|
||||
|
||||
Reference in New Issue
Block a user