完成智慧法务前端调整20250522,还有登录和主页需要完善

This commit is contained in:
2025-05-27 23:48:28 +08:00
parent 742a789244
commit 690d369f57
30 changed files with 1557 additions and 292 deletions
+6 -3
View File
@@ -161,7 +161,7 @@ export async function getReviewPoints(fileId: string) {
return { data: [], stats: { total: 0, success: 0, warning: 0, error: 0, score: 0 } };
}
// 查询评查点组
// 步骤3查询评查点组
const groupsParams: PostgrestParams = {
select: '*',
filter: {
@@ -180,7 +180,7 @@ export async function getReviewPoints(fileId: string) {
return { data: [], stats: { total: 0, success: 0, warning: 0, error: 0, score: 0 } };
}
// 从audit_status表中 获取 需人工审核 的那些评查点的数据
// 步骤4从audit_status表中 获取 需人工审核 的那些评查点的数据
// console.log('evaluationPointsData1112------', evaluationPointsData.find(point => point.post_action === 'manual'));
const manualReviewPoints = evaluationPointsData.filter(point => point.post_action === 'manual');
const manualReviewPointsIds = manualReviewPoints.map(point => point.id);
@@ -315,7 +315,7 @@ export async function getReviewPoints(fileId: string) {
actionContent: point.action_config || '',
// actionContent: '用户提前在评查点输入过的修改内容',
legalBasis: point.references_laws || {}
legalBasis: point.references_laws || {},
// legalBasis: {
// name: '中华人民共和国食品安全法',
// content: '中华人民共和国食品安全法',
@@ -326,6 +326,9 @@ export async function getReviewPoints(fileId: string) {
// }
// ]
// }
// 评查配置: point.evaluation_config
evaluationConfig: point.evaluation_config || {}
};
});
+87 -27
View File
@@ -69,10 +69,11 @@ interface FilePreviewProps {
reviewPoints?: ReviewPoint[]; // 设为可选
activeReviewPointResultId: string | null;
targetPage?: number; // 新增目标页码参数
isStructuredView?: boolean; // 是否显示结构化视图
}
// export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) {
export function FilePreview({ fileContent, activeReviewPointResultId, targetPage }: FilePreviewProps) {
export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, isStructuredView = false }: FilePreviewProps) {
const [zoomLevel, setZoomLevel] = useState(100);
// const [highlightsVisible, setHighlightsVisible] = useState(true);
const contentRef = useRef<HTMLDivElement>(null);
@@ -173,6 +174,9 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
// 处理页面跳转
const prevTargetPageRef = useRef<number | undefined>(undefined);
useEffect(() => {
// 调试信息:记录组件状态
// console.log(`FilePreview更新 - isStructuredView:${isStructuredView}, targetPage:${targetPage}, activeReviewPointResultId:${activeReviewPointResultId}, numPages:${numPages}`);
// 如果有目标页码,并且与上次相同,提示用户
if(targetPage && numPages && targetPage <= numPages && targetPage === prevTargetPageRef.current){
toastService.success(`已跳转至目标页码`);
@@ -181,9 +185,6 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
if (targetPage && numPages && targetPage <= numPages && (targetPage !== prevTargetPageRef.current || activeReviewPointResultId)) {
prevTargetPageRef.current = targetPage;
let newTargetPage = targetPage;
// let newTargetPage = targetPage;
// console.log("targetPage:", targetPage);
// console.log("fileContent:", fileContent);
// 页码偏移量
try {
@@ -197,13 +198,18 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
toastService.error("访问ocrResult时出错:" + (error instanceof Error ? error.message : '未知错误'));
}
const pageElement = document.getElementById(`page-${newTargetPage}`);
const pageElementId = `page-${newTargetPage}${isStructuredView ? '-structured' : ''}`;
// console.log(`尝试跳转到元素ID: ${pageElementId}`);
const pageElement = document.getElementById(pageElementId);
if (pageElement) {
console.log(`跳转到第${newTargetPage}页,对应评查点结果ID: ${activeReviewPointResultId}`);
pageElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else {
console.warn(`未找到页面元素: ${pageElementId}`);
}
}
}, [targetPage, numPages, fileContent, activeReviewPointResultId]);
}, [targetPage, numPages, fileContent, activeReviewPointResultId, isStructuredView]);
// 获取评查点对应的样式类
// const getHighlightClass = (status: string) => {
@@ -302,9 +308,12 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
marginBottom: `${calculatePageMargin(zoomFactor)}px`, // 动态计算页面间距
};
// 为结构化视图和普通视图创建不同的ID
const pageId = isStructuredView ? `page-${i}-structured` : `page-${i}`;
// 为每一页创建组件
pages.push(
<div key={i} id={`page-${i}`} style={pageContainerStyle}>
<div key={i} id={pageId} style={pageContainerStyle}>
{/* 页码标识,显示在页面上方 */}
<div className="text-center text-gray-500 text-sm mb-2"> {i} </div>
@@ -365,7 +374,20 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
// 渲染文档内容
const renderDocumentContent = () => {
return (
// 如果路径无效,显示错误信息
if (!fileContent.path) {
return (
<div className="text-red-500 p-4">
<p></p>
</div>
);
}
// 获取文件扩展名
const fileExtension = fileContent.path.split('.').pop()?.toLowerCase();
// PDF内容渲染
const renderPdfContent = () => (
<div
style={{
...styles.pdfContainer,
@@ -390,32 +412,75 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
</Document>
</div>
);
// 结构化数据渲染
const renderStructuredData = () => (
<div className="structured-view p-4 text-left mt-4 border-t border-gray-200">
<div className="text-xs font-medium mb-2"></div>
{fileContent.ocrResult ? (
<div className="overflow-auto max-h-[300px]">
<pre className="text-xs whitespace-pre-wrap bg-gray-50 p-3 rounded">
{JSON.stringify(fileContent.ocrResult, null, 2)}
</pre>
</div>
) : (
<div className="text-gray-500 p-4 text-center">
<p></p>
</div>
)}
</div>
);
// 根据文件类型选择不同的渲染方式
if (fileExtension === 'pdf') {
// 结构化视图模式:显示PDF和结构化数据
if (isStructuredView) {
return (
<div>
{renderPdfContent()}
{renderStructuredData()}
</div>
);
}
// 普通模式:仅显示PDF
return renderPdfContent();
} else {
// 非PDF文件显示不支持消息
return (
<div className="text-gray-500 p-4">
<p>{fileExtension}</p>
</div>
);
}
};
return (
<div className="file-preview">
<div className="file-preview-header py-2 px-4">
<div className="file-preview-header py-2 px-4 text-xs sm:text-xs md:text-sm max-w-full text-overflow-ellipsis">
<div className="flex items-center">
<i className="ri-file-text-line text-primary mr-2"></i>
<span className="font-medium text-primary"></span>
<i className={`${isStructuredView ? 'ri-file-list-line' : 'ri-file-text-line'} text-primary mr-2`}></i>
<span className="font-medium text-primary">{isStructuredView ? '附件预览' : '文件预览'}</span>
</div>
<div className="file-preview-actions flex items-center">
<button
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5 "
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5"
onClick={handleScrollToTop}
title="返回顶部"
>
<i className="ri-arrow-up-double-line"></i>
<span className="ml-1"></span>
<span className="ml-1 sm:inline md:inline lg:hidden xl:inline"></span>
</button>
<button
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5"
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5"
onClick={handleZoomIn}
title="放大"
>
<i className="ri-zoom-in-line"></i>
</button>
<button
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5"
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5"
onClick={handleZoomOut}
title="缩小"
>
<i className="ri-zoom-out-line"></i>
</button>
@@ -423,7 +488,7 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
<div className="inline-flex items-center ml-2">
<input
type="text"
className="ant-input ant-input-sm py-0 px-1 text-xs h-5 leading-5 w-10 text-center
className="ant-input ant-input-sm py-0 px-1 text-xs max-h-6 leading-5 max-w-[2.5rem] text-center
focus:outline-none focus:ring-1 focus:ring-green-900"
placeholder="页码"
value={pageInputValue}
@@ -431,29 +496,24 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
onKeyDown={handlePageInputKeyDown}
/>
<button
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5 ml-1"
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 ml-1"
onClick={handlePageJump}
disabled={!numPages}
title="跳转到页面"
>
<i className="ri-arrow-right-line"></i>
</button>
{numPages && <span className="ml-1 text-xs text-gray-500">/ {numPages}</span>}
{numPages && <span className="ml-1 text-xs text-gray-500 sm:inline md:inline lg:hidden xl:inline">/ {numPages}</span>}
</div>
{/* <button
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5"
onClick={toggleHighlights}
>
<i className="ri-mark-pen-line"></i> {highlightsVisible ? '隐藏问题' : '显示问题'}
</button> */}
<span className="ml-2 text-xs text-gray-500 inline-block">{"比例:"+zoomLevel+"%"}</span>
<span className="ml-2 text-xs text-gray-500 inline-block sm:inline md:inline lg:hidden xl:inline">{"比例:"+zoomLevel+"%"}</span>
<button
className={`ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5 ml-2 ${dragMode ? 'active bg-green-300' : ''}`}
className={`ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 ml-2 ${dragMode ? 'active bg-green-300' : ''}`}
title="切换拖拽模式"
aria-pressed={dragMode}
onClick={toggleDragMode}
>
<i className="ri-drag-move-line"></i>
<span className="ml-1">{dragMode ? '(已激活)' : ''}</span>
<span className="ml-1 sm:inline md:inline lg:hidden xl:inline">{dragMode ? '(已激活)' : ''}</span>
</button>
</div>
</div>
+166 -79
View File
@@ -55,6 +55,16 @@ export interface ReviewPoint {
};
postAction?: string;
actionContent?: string;
evaluationConfig?: {
rules?: Array<{
type: string;
config?: {
fields?: string[];
pairs?: Array<{ sourceField?: string; targetField?: string }>;
logic?: string;
};
}>;
};
}
// 统计数据类型
@@ -444,84 +454,160 @@ export function ReviewPointsList({
* @returns 评查点主要内容组件
*/
const renderContent = (reviewPoint: ReviewPoint, result?: boolean) => {
// 获取evaluationConfig中type为consistency的规则 评查点一致性规则组的规则
const consistencyRules = reviewPoint.evaluationConfig?.rules?.filter(rule => rule.type === 'consistency') || [];
// 获取所有consistency规则中的fields
const allConsistencyFields: string[][] = [];
consistencyRules.forEach(rule => {
if (rule.config?.fields) {
allConsistencyFields.push(rule.config.fields);
}else if (rule.config?.pairs) {
// 处理pairs情况,提取sourceField和targetField
const fields: string[] = [];
rule.config.pairs.forEach(pair => {
if (pair.sourceField) fields.push(pair.sourceField);
if (pair.targetField) fields.push(pair.targetField);
});
if (fields.length > 0) {
allConsistencyFields.push(fields);
}
}
});
// 对content进行排序
const contentEntries = Object.entries(reviewPoint.content);
// 按照consistency规则分组
const groupedContent: Record<string, Array<[string, { page?: number | string, value?: object }]>> = {
'default': [] // 默认组,存放不属于任何consistency规则的项
};
// 为每个consistency规则创建分组
allConsistencyFields.forEach((fields, index) => {
groupedContent[`consistency_${index}`] = [];
});
// 将content按照规则分组
contentEntries.forEach(entry => {
const [key, value] = entry;
// 检查是否属于某个consistency规则
let assigned = false;
allConsistencyFields.forEach((fields, index) => {
if (fields.includes(key)) {
groupedContent[`consistency_${index}`].push(entry);
assigned = true;
}
});
// 如果不属于任何规则,放入默认组
if (!assigned) {
groupedContent['default'].push(entry);
}
});
return (
<>
{/* 修改评查结果的结构之后,显示新的结构 */}
{Object.entries(reviewPoint.content).map(([key, value], index) => !(result && value.value?.toString().trim() == '') && (
<div
key={index}
className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 last:pb-0 cursor-pointer hover:bg-gray-100 transition-colors duration-200 rounded p-1 group"
onClick={(e) => {
e.stopPropagation();
console.log(`单独点击${key}----`, reviewPoint);
const valuePage = parseInt(value.page as string);
const contentPage = parseInt(reviewPoint.contentPage?.[key] as string);
// 检查value中的page属性是否存在,优先取value中的page
if (valuePage > 0) {
console.log(`存在page且不为空:单独点击${key}---------->evaluated_results内的页码:`, valuePage);
onReviewPointSelect(reviewPoint.id, valuePage);
{/* 渲染各个分组 */}
{Object.entries(groupedContent).map(([groupKey, entries], groupIndex) => {
if (entries.length === 0) return null;
// 非默认组添加边框
const isDefaultGroup = groupKey === 'default';
return (
<div
key={groupKey}
className={`mb-1 ${!isDefaultGroup ? `border border-${result ? 'green' : 'red'}-200 rounded-md` : ''}`}
>
{/* 分组标题,只有非默认组显示 */}
{/* {!isDefaultGroup && (
<div className="text-xs font-medium text-red-500 mb-2">
规则组 {groupIndex}
</div>
)} */}
{/* 渲染组内内容 */}
{entries.map(([key, value], index) =>
!(result && value.value?.toString().trim() == '') && (
<div
key={`${groupKey}_${index}`}
className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 cursor-pointer hover:bg-gray-100 transition-colors duration-200 rounded p-1 group"
onClick={(e) => {
e.stopPropagation();
console.log(`单独点击${key}----`, reviewPoint);
const valuePage = parseInt(value.page as string);
const contentPage = parseInt(reviewPoint.contentPage?.[key] as string);
// 检查value中的page属性是否存在,优先取value中的page
if (valuePage > 0) {
console.log(`存在page且不为空:单独点击${key}---------->evaluated_results内的页码:`, valuePage);
onReviewPointSelect(reviewPoint.id, valuePage);
} else if(contentPage && contentPage > 0) {
console.log(`存在page且为空:单独点击${key}---------->ocr_result内的页码:`, contentPage);
onReviewPointSelect(reviewPoint.id, contentPage);
}else {
toastService.error(`无法找到"${key}"对应的索引内容`);
console.log(`单独点击${key}--------没有对应页码`);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const valuePage = parseInt(value.page as string);
const contentPage = parseInt(reviewPoint.contentPage?.[key] as string);
// 检查value中的page属性是否存在,优先取value中的page
if (valuePage > 0) {
onReviewPointSelect(reviewPoint.id, valuePage);
} else if(contentPage && contentPage > 0) {
onReviewPointSelect(reviewPoint.id, contentPage);
} else {
toastService.error(`无法找到"${key}"对应的索引内容`);
console.log(`单独点击${key}--------没有对应页码`);
}
}
}}
role="button"
tabIndex={0}
aria-label={`查看${key}内容详情`}
onMouseLeave={(e) => {
// 获取容器内的滚动区域元素
const scrollContainer = e.currentTarget.querySelector('.text-container');
if (scrollContainer) {
// 在文本缩回之前重置滚动位置
scrollContainer.scrollTop = 0;
}
}}
>
{/* <div className="flex justify-between items-center mb-1"> */}
<div className="flex items-center mb-1">
<span className="text-xs pr-5">
{key}
</span>
<span className={`flex-shrink-0 text-xs w-15 ${value.value?.toString().trim() ? 'text-error' : 'text-warning'}`}>
{parseInt(value.page as string)>0 || parseInt(reviewPoint.contentPage?.[key] as string)>0 ? '' : <i title="无法找到索引内容" className="ri-alert-line text-red-500 mr-2"></i>}
{value.value?.toString().trim() ? '' : '缺失'}
</span>
</div>
} else if(contentPage && contentPage > 0) {
console.log(`存在page且为空:单独点击${key}---------->ocr_result内的页码:`, contentPage);
onReviewPointSelect(reviewPoint.id, contentPage);
}else {
toastService.error(`无法找到"${key}"对应的索引内容`);
console.log(`单独点击${key}--------没有对应页码`);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const valuePage = parseInt(value.page as string);
const contentPage = parseInt(reviewPoint.contentPage?.[key] as string);
// 检查value中的page属性是否存在,优先取value中的page
if (valuePage > 0) {
onReviewPointSelect(reviewPoint.id, valuePage);
} else if(contentPage && contentPage > 0) {
onReviewPointSelect(reviewPoint.id, contentPage);
} else {
toastService.error(`无法找到"${key}"对应的索引内容`);
console.log(`单独点击${key}--------没有对应页码`);
}
}
}}
role="button"
tabIndex={0}
aria-label={`查看${key}内容详情`}
onMouseLeave={(e) => {
// 获取容器内的滚动区域元素
const scrollContainer = e.currentTarget.querySelector('.text-container');
if (scrollContainer) {
// 在文本缩回之前重置滚动位置
scrollContainer.scrollTop = 0;
}
}}
>
{/* <div className="flex justify-between items-center mb-1"> */}
<div className="flex items-center mb-1">
<span className="text-xs pr-5">
{key}
</span>
<span className={`flex-shrink-0 text-xs w-15 ${value.value?.toString().trim() ? 'text-error' : 'text-warning'}`}>
{parseInt(value.page as string)>0 || parseInt(reviewPoint.contentPage?.[key] as string)>0 ? '' : <i title="无法找到索引内容" className="ri-alert-line text-red-500 mr-2"></i>}
{value.value?.toString().trim() ? '' : '缺失'}
</span>
</div>
<div className="relative text-container max-h-96 group-hover:overflow-auto overflow-hidden">
<p
className="text-xs text-left select-text block overflow-hidden !line-clamp-2
group-hover:!line-clamp-none group-hover:bg-white group-hover:shadow-md group-hover:z-10 group-hover:relative px-1 rounded transition-all duration-300 ease-in-out cursor-text"
// title={value.value?.toString() || ''}
// style={{ userSelect: 'all' }}
>
{(value.value?.toString().trim() === '')
? ""
: value.value?.toString() || ''}
</p>
<div className="relative text-container max-h-96 group-hover:overflow-auto overflow-hidden">
<p
className="text-xs text-left select-text block overflow-hidden !line-clamp-2
group-hover:!line-clamp-none group-hover:bg-white group-hover:shadow-md group-hover:z-10 group-hover:relative px-1 rounded transition-all duration-300 ease-in-out cursor-text"
// title={value.value?.toString() || ''}
// style={{ userSelect: 'all' }}
>
{(value.value?.toString().trim() === '')
? ""
: value.value?.toString() || ''}
</p>
</div>
</div>
))}
</div>
</div>
))}
);
})}
</>
);
};
@@ -533,7 +619,7 @@ export function ReviewPointsList({
* @returns 评查点内容与建议组件
*/
const renderReviewPointContent = (reviewPoint: ReviewPoint) => {
const handleManualReviewNotesChange = (reviewPointId: string, text: string) => {
setManualReviewNotes(prev => ({
...prev,
@@ -541,8 +627,9 @@ export function ReviewPointsList({
}));
};
// 如果当前评查点不处于编辑状态,只显示简单信息
// 如果当前评查点不处于编辑状态 TODO delete
if (editingReviewPoint !== reviewPoint.id) {
// 根据result和status决定渲染哪种样式
if (reviewPoint.result === true) {
// 已通过的评查点只显示基本信息和人工审核注释
@@ -893,11 +980,11 @@ export function ReviewPointsList({
>
{/* 评查点标题和状态 */}
{/* 评查点名称 pointName*/}
<div className="review-point-title flex-1 text-left text-blue-500">{'评查点名称:' + reviewPoint.pointName}</div>
<div className="review-point-title flex-1 text-left text-blue-500">{reviewPoint.pointName}</div>
<div className="review-point-header flex justify-between items-start">
<div className="review-point-title flex-1 text-left min-w-[25%]">{reviewPoint.title}</div>
<div className="flex-1 text-left min-w-[25%] font-medium text-[13px]">{reviewPoint.title}</div>
{/* 评查点所属分组 */}
<div className="review-point-location max-w-[40%] flex items-center">
{/* <div className="review-point-location max-w-[40%] flex items-center">
<i className="ri-file-list-line mr-1 flex-shrink-0"></i>
<span
className="truncate block whitespace-nowrap overflow-hidden
@@ -907,7 +994,7 @@ export function ReviewPointsList({
>
{reviewPoint.groupName}
</span>
</div>
</div> */}
<div className="flex flex-col items-center ml-2 flex-shrink-0 max-w-[15%]">
{renderStatusBadge(reviewPoint.status, reviewPoint.result)}
{renderHumanReviewBadge(reviewPoint)}
+10 -7
View File
@@ -15,6 +15,7 @@ interface ReviewTabsProps {
previousRoute?: string;
path?: string;
auditStatus?: number;
type?: string;
};
onConfirmResults: () => void;
}
@@ -104,14 +105,16 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
>
<i className="ri-lightbulb-line"></i> AI智能分析
</button> */}
<button
className={`tab-nav-item ${activeTab === 'fileinfo' ? 'active' : ''}`}
onClick={() => onTabChange('fileinfo')}
type="button"
aria-pressed={activeTab === 'fileinfo'}
{fileInfo.type === '1' && (
<button
className={`tab-nav-item ${activeTab === 'filecompare' ? 'active' : ''}`}
onClick={() => onTabChange('filecompare')}
type="button"
aria-pressed={activeTab === 'filecompare'}
>
<i className="ri-flip-horizontal-line"></i>
</button>
<i className="ri-flip-horizontal-line"></i>
</button>
)}
<button
className={`tab-nav-item ${activeTab === 'fileinfo' ? 'active' : ''}`}
onClick={() => onTabChange('fileinfo')}
+51 -13
View File
@@ -118,8 +118,27 @@ export function ExtractionSettings({
// 自动保存字段变更状态
// 这个效果确保添加字段后自动保存到组件状态,但不自动提交更新
useEffect(() => {
setHasPendingChanges(true);
}, [fields, regexFields, promptContent])
// 初始加载时不设置hasPendingChanges为true
// 仅当用户进行了实际修改后才标记为有变更
const initialLlmFields = initialData?.extraction_config?.llm?.fields || [];
const initialVlmFields = initialData?.extraction_config?.vlm?.fields || [];
const initialRegexFields = initialData?.extraction_config?.regex?.fields || [];
const initialLlmPrompt = initialData?.extraction_config?.llm?.prompt_setting?.template || '';
const initialVlmPrompt = initialData?.extraction_config?.vlm?.prompt_setting?.template || '';
// 检查是否有实际变化
const hasLlmFieldsChanged = JSON.stringify(fields.llm) !== JSON.stringify(initialLlmFields);
const hasVlmFieldsChanged = JSON.stringify(fields.vlm) !== JSON.stringify(initialVlmFields);
const hasRegexFieldsChanged = JSON.stringify(regexFields) !== JSON.stringify(initialRegexFields);
const hasPromptContentChanged =
promptContent.llm !== initialLlmPrompt ||
promptContent.vlm !== initialVlmPrompt;
// 只有实际发生变化时才设置为true
if (hasLlmFieldsChanged || hasVlmFieldsChanged || hasRegexFieldsChanged || hasPromptContentChanged) {
setHasPendingChanges(true);
}
}, [fields, regexFields, promptContent, initialData])
// 处理字段输入变化
const handleFieldInputChange = (
@@ -541,7 +560,7 @@ export function ExtractionSettings({
<label className="form-label mb-1" htmlFor="field-input">
</label>
<div className="flex mb-2">
<div className="flex">
<input
type="text"
className="form-input mr-2"
@@ -561,6 +580,9 @@ export function ExtractionSettings({
</button>
</div>
<div className="form-tip mb-2 text-xs">
</div>
<div className="chips-container" id="fields-container">
{fields.llm.map((field, index) => (
<div className="chip" key={`llm-field-${index}`}>
@@ -581,9 +603,7 @@ export function ExtractionSettings({
</div>
))}
</div>
<div className="form-tip mt-1 text-xs">
</div>
</div>
</div>
@@ -666,11 +686,18 @@ export function ExtractionSettings({
].map((variable) => (
<button
key={variable}
type="button"
type="button"
className="var-tag"
onClick={() => applyVariableToPrompt(variable, "llm")}
>
{variable}
{variable=='docType' ? '文档类型:{docType}':
variable=='fieldsList' ? '抽取字段列表:{fieldsList}':
variable=='companyName' ? '公司名称:{companyName}':
variable=='documentId' ? '文档编号:{documentId}':
variable=='date' ? '日期:{date}':
variable=='industry' ? '行业:{industry}':
variable=='ocrText' ? 'OCR文本:{ocrText}':
variable}
</button>
))}
</div>
@@ -692,7 +719,7 @@ export function ExtractionSettings({
<label className="form-label mb-1" htmlFor="field-input-vlm">
</label>
<div className="flex mb-2">
<div className="flex">
<input
type="text"
className="form-input mr-2"
@@ -713,6 +740,9 @@ export function ExtractionSettings({
</button>
</div>
<div className="form-tip mb-2 text-xs">
</div>
<div className="chips-container" id="fields-container-vlm">
{fields.vlm.map((field, index) => {
const { fieldName, fieldType, typeName, badgeClass } =
@@ -747,9 +777,7 @@ export function ExtractionSettings({
);
})}
</div>
<div className="form-tip mt-1 text-xs">
</div>
</div>
</div>
@@ -843,7 +871,17 @@ export function ExtractionSettings({
className="var-tag"
onClick={() => applyVariableToPrompt(variable, "vlm")}
>
{variable}
{variable=='docType' ? '文档类型:{docType}':
variable=='fieldsList' ? '抽取字段列表:{fieldsList}':
variable=='companyName' ? '公司名称:{companyName}':
variable=='documentId' ? '文档编号:{documentId}':
variable=='date' ? '日期:{date}':
variable=='industry' ? '行业:{industry}':
variable=='contentType' ? '内容类型:{contentType}':
variable=='pageRange' ? '页面范围:{pageRange}':
variable=='colorMode' ? '色彩模式:{colorMode}':
variable=='ocrText' ? 'OCR文本:{ocrText}':
variable}
</button>
))}
</div>
+21 -4
View File
@@ -13,6 +13,7 @@ export interface DateRangePickerProps {
endId?: string;
format?: string;
placeholder?: string;
colorMode?: 'light' | 'dark' | 'auto';
}
export function links() {
@@ -57,7 +58,8 @@ export function DateRangePicker({
startId = "date-start",
endId = "date-end",
format = "yyyy-MM-dd",
placeholder = "请选择时间"
placeholder = "请选择时间",
colorMode = 'auto'
}: DateRangePickerProps) {
// 使用ref获取input元素
const startInputRef = useRef<HTMLInputElement>(null);
@@ -121,8 +123,15 @@ export function DateRangePicker({
setFocusedInput(null);
};
// 获取颜色模式类名
const getColorModeClass = () => {
if (colorMode === 'light') return 'color-mode-light';
if (colorMode === 'dark') return 'color-mode-dark';
return ''; // auto模式不添加额外类名
};
return (
<div className={`date-range-picker ${className}`}>
<div className={`date-range-picker ${getColorModeClass()} ${className}`}>
<div className="date-range-fields">
<div className="date-field">
<label htmlFor={startId} className="date-label">{startLabel}</label>
@@ -200,7 +209,8 @@ export function SimpleDateRangePicker({
startId = "date-start-simple",
endId = "date-end-simple",
format = "yyyy-MM-dd",
placeholder = "请选择时间"
placeholder = "请选择时间",
colorMode = 'auto'
}: Omit<DateRangePickerProps, 'startLabel' | 'endLabel'>) {
// 使用ref获取input元素
const startInputRef = useRef<HTMLInputElement>(null);
@@ -264,8 +274,15 @@ export function SimpleDateRangePicker({
setFocusedInput(null);
};
// 获取颜色模式类名
const getColorModeClass = () => {
if (colorMode === 'light') return 'color-mode-light';
if (colorMode === 'dark') return 'color-mode-dark';
return ''; // auto模式不添加额外类名
};
return (
<div className={`date-range-picker simple-date-range-picker ${className}`}>
<div className={`date-range-picker simple-date-range-picker ${getColorModeClass()} ${className}`}>
<div className="date-range-fields">
<div
className={`date-input-wrapper ${focusedInput === startId ? 'focused' : ''}`}
+68 -2
View File
@@ -6,6 +6,10 @@ export type FileType =
| 'license'
| 'punishment'
| 'agreement'
| 'contract' // 合同文档
| 'license-doc' // 行政许可卷宗
| 'punishment-doc' // 行政处罚卷宗
| 'other' // 其他
| string;
interface FileTypeTagProps {
@@ -15,6 +19,8 @@ interface FileTypeTagProps {
size?: 'default' | 'sm' | 'lg';
showIcon?: boolean;
fileType?: string;
colorMode?: 'light' | 'dark' | 'auto';
typeName?: string; // 新增typeName属性,用于根据文档类型名称判断样式
}
export function links() {
@@ -30,6 +36,8 @@ export function links() {
* @param size 尺寸:default, sm, lg
* @param showIcon 是否显示图标,默认为true
* @param fileType 文件类型,不提供则使用文档类型决定样式
* @param colorMode 颜色模式:light, dark, auto(默认),设置为auto时跟随系统
* @param typeName 文档类型名称,用于判断样式
*/
export function FileTypeTag({
type,
@@ -37,7 +45,9 @@ export function FileTypeTag({
className = '',
size = 'default',
showIcon = true,
fileType
fileType,
colorMode = 'auto',
typeName
}: FileTypeTagProps) {
// 文档类型对应的图标
const getTypeIcon = () => {
@@ -47,26 +57,75 @@ export function FileTypeTag({
'license': 'ri-vip-crown-line',
'punishment': 'ri-scales-line',
'agreement': 'ri-file-paper-line',
'pdf': 'ri-file-pdf-line',
'doc': 'ri-file-word-line',
'docx': 'ri-file-word-line',
'xls': 'ri-file-excel-line',
'xlsx': 'ri-file-excel-line',
'ppt': 'ri-file-ppt-line',
'pptx': 'ri-file-ppt-line',
'contract': 'ri-file-list-3-line', // 合同文档
'license-doc': 'ri-vip-crown-line', // 行政许可卷宗
'punishment-doc': 'ri-scales-line', // 行政处罚卷宗
'other': 'ri-file-paper-line', // 其他
};
return typeIconMap[type] || 'ri-file-text-line';
};
// 文档类型对应的文本
const getTypeText = () => {
// 如果提供了自定义文本,优先使用
if (text) return text;
// 如果提供了typeName,优先使用
if (typeName) return typeName;
const typeTextMap: Record<string, string> = {
'sales-contract': '销售合同',
'purchase-contract': '采购合同',
'license': '专卖许可证',
'punishment': '行政处罚决定书',
'agreement': '承包协议',
'pdf': 'PDF',
'doc': 'Word',
'docx': 'Word',
'xls': 'Excel',
'xlsx': 'Excel',
'ppt': 'PPT',
'contract': '合同文档', // 合同文档
'license-doc': '行政许可卷宗', // 行政许可卷宗
'punishment-doc': '行政处罚卷宗', // 行政处罚卷宗
'other': '其他文档', // 其他
};
return typeTextMap[type] || type;
};
// 获取根据typeName判断的样式类名
const getTypeNameClass = () => {
if (!typeName) return '';
// 根据typeName判断文档类型
if (typeName.includes('合同')) {
return 'file-type-tag-contract';
} else if (typeName.includes('许可') || typeName.includes('行政许可')) {
return 'file-type-tag-license-doc';
} else if (typeName.includes('处罚') || typeName.includes('行政处罚')) {
return 'file-type-tag-punishment-doc';
} else {
return 'file-type-tag-other';
}
};
// 获取文档类型对应的类名
const getTypeClass = () => {
// 如果有typeName,根据typeName判断样式
if (typeName) {
const typeNameClass = getTypeNameClass();
if (typeNameClass) {
return `file-type-tag ${typeNameClass}`;
}
}
// 如果有文件类型,优先使用文件类型决定样式
if (fileType) {
const fileTypeClass = getFileTypeClass(fileType);
@@ -108,8 +167,15 @@ export function FileTypeTag({
return !showIcon ? 'file-type-tag-no-icon' : '';
};
// 获取颜色模式类名
const getColorModeClass = () => {
if (colorMode === 'light') return 'color-mode-light';
if (colorMode === 'dark') return 'color-mode-dark';
return ''; // auto模式不添加额外类名
};
return (
<span className={`${getTypeClass()} ${getSizeClass()} ${getIconClass()} ${className}`}>
<span className={`${getTypeClass()} ${getSizeClass()} ${getIconClass()} ${getColorModeClass()} ${className}`}>
{showIcon && <i className={`${getTypeIcon()} file-type-icon`}></i>}
<span className="file-type-text">{getTypeText()}</span>
</span>
+5 -1
View File
@@ -143,6 +143,7 @@ interface DateRangeFilterProps {
startLabel?: string;
endLabel?: string;
simple?: boolean;
colorMode?: 'light' | 'dark' | 'auto';
}
/**
@@ -168,7 +169,8 @@ export function DateRangeFilter({
className = '',
startLabel = "从",
endLabel = "至",
simple = false
simple = false,
colorMode = 'auto'
}: DateRangeFilterProps) {
return (
<div className={`filter-item ${className}`}>
@@ -180,6 +182,7 @@ export function DateRangeFilter({
onStartDateChange={onStartDateChange}
onEndDateChange={onEndDateChange}
className="filter-control"
colorMode={colorMode}
/>
) : (
<DateRangePicker
@@ -190,6 +193,7 @@ export function DateRangeFilter({
startLabel={startLabel}
endLabel={endLabel}
className="filter-control"
colorMode={colorMode}
/>
)}
</div>
+123 -3
View File
@@ -8,8 +8,15 @@ import {
ScrollRestoration,
isRouteErrorResponse,
useRouteError,
type MetaFunction
type MetaFunction,
// useLoaderData
} from "@remix-run/react";
// import {
// LoaderFunctionArgs,
// redirect,
// createCookieSessionStorage,
// ActionFunctionArgs
// } from "@remix-run/node";
import { Layout } from "~/components/layout/Layout";
import { ErrorBoundary as AppErrorBoundary } from "~/components/error/ErrorBoundary";
import { MessageModalProvider } from "~/components/ui/MessageModal";
@@ -21,6 +28,104 @@ import messageModalStyles from "~/styles/components/message-modal.css?url";
import toastStyles from "~/styles/components/toast.css?url";
import LoadingBarContainer from "~/components/ui/LoadingBar";
import RouteChangeLoader from "~/components/ui/RouteChangeLoader";
// import { useState, useEffect } from "react";
// 创建基于Cookie的会话存储
// 在实际应用中,应该使用环境变量来设置密钥
// const sessionStorage = createCookieSessionStorage({
// cookie: {
// name: "__session",
// httpOnly: true,
// path: "/",
// sameSite: "lax",
// secrets: ["s3cr3t"], // 应该从环境变量读取
// secure: process.env.NODE_ENV === "production",
// },
// });
// // 获取会话对象
// export async function getSession(request: Request) {
// const cookie = request.headers.get("Cookie");
// return sessionStorage.getSession(cookie);
// }
// // 获取用户登录状态
// export async function getUserSession(request: Request) {
// const session = await getSession(request);
// return {
// isAuthenticated: session.get("isAuthenticated") === true,
// };
// }
// // 创建登录会话
// export async function createUserSession(isAuthenticated: boolean, redirectTo: string) {
// const session = await sessionStorage.getSession();
// session.set("isAuthenticated", isAuthenticated);
// return redirect(redirectTo, {
// headers: {
// "Set-Cookie": await sessionStorage.commitSession(session),
// },
// });
// }
// // 销毁会话(登出)
// export async function logout(request: Request) {
// const session = await getSession(request);
// return redirect("/login", {
// headers: {
// "Set-Cookie": await sessionStorage.destroySession(session),
// },
// });
// }
// // 添加action处理登录/登出请求
// export async function action({ request }: ActionFunctionArgs) {
// const formData = await request.formData();
// const intent = formData.get("intent");
// if (intent === "logout") {
// return logout(request);
// }
// return null;
// }
// // 添加loader函数进行全局认证检查
// export async function loader({ request }: LoaderFunctionArgs) {
// // 获取当前路径
// const url = new URL(request.url);
// const pathname = url.pathname;
// // 排除不需要登录验证的路径
// const publicPaths = ['/login', '/favicon.ico'];
// const isPublicPath = publicPaths.some(path => pathname.startsWith(path));
// // 获取用户会话
// const { isAuthenticated } = await getUserSession(request);
// // 如果访问需要认证的路径但未登录,重定向到登录页
// if (!isPublicPath && !isAuthenticated) {
// // 保存请求的URL,以便登录后重定向回来
// const session = await getSession(request);
// session.set("redirectTo", pathname);
// return redirect("/login", {
// headers: {
// "Set-Cookie": await sessionStorage.commitSession(session),
// },
// });
// }
// // 如果已登录且访问登录页,重定向到首页
// if (pathname === "/login" && isAuthenticated) {
// return redirect("/home");
// }
// // 向组件传递认证状态和当前路径
// return Response.json({ isAuthenticated, pathname });
// }
// 添加客户端hydration错误处理
// if (typeof window !== "undefined") {
@@ -58,6 +163,12 @@ export function links() {
}
export default function App() {
// const { pathname } = useLoaderData<typeof loader>();
// // 确定哪些路径不需要Layout
// const noLayoutPaths = ['/login', '/home'];
// const needsLayout = !noLayoutPaths.includes(pathname);
return (
<html lang="zh-CN">
<head>
@@ -82,9 +193,18 @@ export default function App() {
<body className="font-sans">
<MessageModalProvider>
<ToastProvider>
<Layout>
{/* {needsLayout ? (
<Layout>
<Outlet />
</Layout>
) : (
<Outlet />
</Layout>
)} */}
<Layout>
<Outlet />
</Layout>
<RouteChangeLoader />
</ToastProvider>
</MessageModalProvider>
+12 -4
View File
@@ -1,16 +1,17 @@
// import React from 'react';
import { type MetaFunction } from "@remix-run/node";
import { type MetaFunction, type LoaderFunctionArgs, redirect } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button";
import { FileTag, links as fileTagLinks } from "~/components/ui/FileTag";
// import { FileTypeTag, links as fileTypeTagLinks } from "~/components/ui/FileTypeTag";
import { Tag } from "~/components/ui/Tag";
import homeStyles from "~/styles/pages/home.css?url";
import homeStyles from "~/styles/pages/sys_overview.css?url";
import { getDocuments, type DocumentUI } from "~/api/files/documents";
import { useState, useEffect } from "react";
import { getHomeData } from "~/api/home/home";
import dayjs from 'dayjs';
import { getUserSession } from "~/root";
// 文件处理状态选项
const fileProcessingStatusOptions = [
@@ -41,8 +42,15 @@ export const meta: MetaFunction = () => {
// passRate: number;
// }
// 模拟数据,实际项目中应该从API获取
export async function loader() {
// 添加认证检查
export async function loader({ request }: LoaderFunctionArgs) {
// 检查用户登录状态
// const { isAuthenticated } = await getUserSession(request);
// if (!isAuthenticated) {
// return redirect("/login");
// }
try {
const documentSearchParams = {
page: 1,
+1 -1
View File
@@ -1,4 +1,4 @@
import { json, type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
import { useLoaderData, useSearchParams, useFetcher, Link } from "@remix-run/react";
import { useState, useEffect } from "react";
import { Button } from "~/components/ui/Button";
+5 -1
View File
@@ -75,7 +75,8 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
value: type.id,
label: type.name
}));
// console.log('typesResponse-----',JSON.stringify(documentsResponse.data?.documents[1],null,2));
return Response.json({
documents: documentsResponse.data?.documents || [],
total: documentsResponse.data?.total || 0,
@@ -643,10 +644,12 @@ export default function DocumentsIndex() {
<div className="mt-2 flex inline-block">
<FileTypeTag
type={record.type}
typeName={record.typeName}
text={record.typeName}
size="sm"
showIcon={false}
fileType={record.fileType}
colorMode="light"
/>
{record.isTest && (
<span className="ml-2 text-xs bg-gray-100 text-gray-500 px-1 rounded"></span>
@@ -878,6 +881,7 @@ export default function DocumentsIndex() {
onStartDateChange={(value) => handleDateChange('dateFrom', value)}
onEndDateChange={(value) => handleDateChange('dateTo', value)}
simple={true}
colorMode="light"
/>
</div>
</FilterPanel>
+1 -1
View File
@@ -1527,7 +1527,7 @@ export default function FilesUpload() {
id="docNumber"
name="docNumber"
className="form-input w-full"
placeholder="请输入合同编号、许可证号等"
placeholder="请输入卷宗编号、合同编号等"
value={documentNumber}
onChange={(e) => setDocumentNumber(e.target.value)}
disabled={uploadStage !== "idle"}
+180
View File
@@ -0,0 +1,180 @@
import { useState, useEffect } from 'react';
import { useNavigate, Form } from '@remix-run/react';
import { type MetaFunction, type ActionFunctionArgs, LoaderFunctionArgs, redirect, json } from "@remix-run/node";
import styles from "~/styles/pages/home.css?url";
import { getUserSession, logout } from "~/root";
export const links = () => [
{ rel: "stylesheet", href: styles }
];
export const meta: MetaFunction = () => {
return [
{ title: "中国烟草AI合同及卷宗审核系统 - 首页" },
{ name: "description", content: "中国烟草AI合同及卷宗审核系统首页" },
];
};
// 处理登出请求
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "logout") {
return logout(request);
}
return null;
}
// 验证用户登录状态
export async function loader({ request }: LoaderFunctionArgs) {
const { isAuthenticated } = await getUserSession(request);
if (!isAuthenticated) {
return redirect("/login");
}
return json({ isAuthenticated });
}
export default function Home() {
const navigate = useNavigate();
const [currentTime, setCurrentTime] = useState('');
const [currentDate, setCurrentDate] = useState('');
// 更新日期时间
useEffect(() => {
const updateDateTime = () => {
const now = new Date();
// 格式化日期: YYYY/MM/DD
const date = now.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).replace(/\//g, '/');
// 格式化时间: HH:MM
const time = now.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
hour12: false
});
setCurrentDate(date);
setCurrentTime(time);
};
// 初始化时间
updateDateTime();
// 每分钟更新一次
const interval = setInterval(updateDateTime, 60000);
return () => clearInterval(interval);
}, []);
// 处理模块点击
const handleModuleClick = (path: string) => {
navigate(path);
};
// 处理键盘事件
const handleKeyDown = (path: string, e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter' || e.key === ' ') {
handleModuleClick(path);
}
};
// 处理登出
const handleLogout = () => {
// 使用Form组件提交登出请求
const form = document.getElementById('logout-form') as HTMLFormElement;
if (form) {
form.submit();
}
};
return (
<div className="home-page">
{/* 登出表单 - 隐藏 */}
<Form method="post" id="logout-form" className="hidden">
<input type="hidden" name="intent" value="logout" />
</Form>
{/* 头部 */}
<header className="header">
<div className="logo-container">
<img src="/logo.png" alt="中国烟草" className="logo" />
<span className="logo-text"></span>
<span className="logo-text-en">CHINA TOBACCO</span>
</div>
<div className="user-info">
<span className="datetime">{currentDate} {currentTime}</span>
<div className="user">
<img src="/avatar.png" alt="用户头像" className="avatar" />
<span className="username"></span>
<button
onClick={handleLogout}
className="logout-button"
aria-label="登出"
>
<i className="ri-logout-box-line"></i>
</button>
</div>
</div>
</header>
{/* 主要内容 */}
<main className="main-content">
<h1 className="welcome-text">- -</h1>
<div className="modules-container">
{/* 合同管理模块 */}
<div
className="module-card"
onClick={() => handleModuleClick('/documents')}
onKeyDown={(e) => handleKeyDown('/documents', e)}
role="button"
tabIndex={0}
aria-label="合同管理"
>
<div className="module-icon contract-icon"></div>
<span className="module-name"></span>
</div>
{/* 案卷智能评查模块 */}
<div
className="module-card"
onClick={() => handleModuleClick('/')}
onKeyDown={(e) => handleKeyDown('/', e)}
role="button"
tabIndex={0}
aria-label="案卷智能评查"
>
<div className="module-icon review-icon"></div>
<span className="module-name"></span>
</div>
{/* 智慧法务大模型模块 */}
<div
className="module-card"
onClick={() => handleModuleClick('/prompts')}
onKeyDown={(e) => handleKeyDown('/prompts', e)}
role="button"
tabIndex={0}
aria-label="智慧法务大模型"
>
<div className="module-icon ai-icon"></div>
<span className="module-name"></span>
</div>
</div>
</main>
{/* 底部山水背景 */}
<footer className="footer">
<div className="mountains-bg"></div>
</footer>
</div>
);
}
+120
View File
@@ -0,0 +1,120 @@
import { useState } from "react";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { type MetaFunction, type ActionFunctionArgs, redirect, json, type LoaderFunctionArgs } from "@remix-run/node";
import styles from "~/styles/pages/login.css?url";
import { createUserSession, getUserSession, getSession } from "~/root";
export const links = () => [
{ rel: "stylesheet", href: styles }
];
export const meta: MetaFunction = () => {
return [
{ title: "中国烟草AI合同及卷宗审核系统 - 登录" },
{ name: "description", content: "中国烟草AI合同及卷宗审核系统登录页面" },
];
};
// 处理表单提交的action
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const username = formData.get("username") as string;
const password = formData.get("password") as string;
// 简单的登录验证,实际应用中应该进行真正的身份验证
if (!username || !password) {
return json({ error: "用户名和密码不能为空" });
}
// 在实际应用中,这里应该是对用户名和密码的验证逻辑
// 简化起见,我们直接视为登录成功
// 获取session中存储的重定向URL,如果没有则默认到/home
const session = await getSession(request);
const redirectTo = session.get("redirectTo") || "/home";
// 创建登录会话并重定向
return createUserSession(true, redirectTo);
}
// 加载器,获取当前会话状态
export async function loader({ request }: LoaderFunctionArgs) {
const { isAuthenticated } = await getUserSession(request);
// 如果已登录,重定向到首页
if (isAuthenticated) {
return redirect("/home");
}
return Response.json({ isAuthenticated });
}
export default function Login() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
// 判断是否正在提交表单
const isSubmitting = navigation.state === "submitting";
return (
<div className="login-page">
<div className="login-container">
<div className="login-header">
<img src="/logo.png" alt="中国烟草" className="login-logo" />
<h1 className="login-title">AI合同及卷宗审核系统</h1>
</div>
<div className="login-form-container">
<h2 className="login-subtitle"></h2>
<Form method="post" className="login-form">
{actionData?.error && (
<div className="error-message">{actionData.error}</div>
)}
<div className="form-group">
<label htmlFor="username"></label>
<input
type="text"
id="username"
name="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="form-input"
placeholder="请输入用户名"
required
/>
</div>
<div className="form-group">
<label htmlFor="password"></label>
<input
type="password"
id="password"
name="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="form-input"
placeholder="请输入密码"
required
/>
</div>
<button
type="submit"
className="login-button"
disabled={isSubmitting}
>
{isSubmitting ? "登录中..." : "登录"}
</button>
</Form>
</div>
<div className="login-footer">
<p>© 2024 </p>
</div>
</div>
</div>
);
}
+41 -3
View File
@@ -194,7 +194,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
// 确保reviewData有效且具有预期的属性
if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) {
console.log("reviewData-------",JSON.stringify(reviewData.document?.type,null,2));
// console.log("reviewData-------",JSON.stringify(reviewData.document?.type,null,2));
return Response.json({
previousRoute: previousRoute,
document: reviewData.document,
@@ -517,7 +517,7 @@ export default function ReviewDetails() {
};
return (
<div className="container">
<div className="review-container">
{isLoading ? (
<div className="flex justify-center items-center p-12">
<div className="loading-spinner"></div>
@@ -584,7 +584,8 @@ export default function ReviewDetails() {
fileInfo={{
previousRoute: loaderData.previousRoute,
path: document?.path,
auditStatus: document?.auditStatus
auditStatus: document?.auditStatus,
type: document?.type
}}
onConfirmResults={handleConfirmResults}
>
@@ -614,6 +615,43 @@ export default function ReviewDetails() {
</div>
)}
{/* 结构比对选项卡内容 */}
{activeTab === 'filecompare' && (
<div className="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:space-x-4">
{/* 左侧:原文件预览 */}
<div className="w-full lg:w-[38%]">
<FilePreview
fileContent={document}
reviewPoints={reviewData.reviewPoints}
activeReviewPointResultId={activeReviewPointResultId}
targetPage={targetPage}
/>
</div>
{/* 中间:附件文件预览 */}
<div className="w-full lg:w-[38%]">
<FilePreview
fileContent={document}
reviewPoints={[]}
activeReviewPointResultId={activeReviewPointResultId}
targetPage={targetPage}
isStructuredView={true}
/>
</div>
{/* 右侧:评查结果 */}
<div className="w-full lg:w-[24%]">
<ReviewPointsList
reviewPoints={reviewData.reviewPoints}
statistics={reviewData.statistics}
activeReviewPointResultId={activeReviewPointResultId}
onReviewPointSelect={handleReviewPointSelect}
onStatusChange={handleReviewPointStatusChange}
/>
</div>
</div>
)}
{/* AI智能分析选项卡内容 */}
{activeTab === 'analysis' && (
<AIAnalysis
+1
View File
@@ -485,6 +485,7 @@ export default function RulesFiles() {
onStartDateChange={(value) => handleDateChange('dateFrom', value)}
onEndDateChange={(value) => handleDateChange('dateTo', value)}
simple={true}
colorMode="light"
/>
<FilterSelect
+69 -9
View File
@@ -145,13 +145,73 @@
}
}
/* 明确指定浅色模式 */
.date-range-picker.color-mode-light .date-label {
color: #666 !important;
}
.date-range-picker.color-mode-light .date-display {
background-color: #fff !important;
border-color: #e0e0e0 !important;
color: #333 !important;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E") !important;
}
.date-range-picker.color-mode-light .date-display.placeholder {
color: #999 !important;
}
.date-range-picker.color-mode-light .date-display:hover {
border-color: #c0c0c0 !important;
background-color: #fafafa !important;
}
.date-range-picker.color-mode-light .date-input-wrapper.focused .date-display {
border-color: var(--primary-color) !important;
box-shadow: 0 0 0 2px rgba(0,104,74, 0.2) !important;
}
.date-range-picker.color-mode-light .date-separator {
color: #999 !important;
}
/* 明确指定深色模式 */
.date-range-picker.color-mode-dark .date-label {
color: #b0b0b0 !important;
}
.date-range-picker.color-mode-dark .date-display {
background-color: #1f1f1f !important;
border-color: #444 !important;
color: #e0e0e0 !important;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23aaa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E") !important;
}
.date-range-picker.color-mode-dark .date-display.placeholder {
color: #777 !important;
}
.date-range-picker.color-mode-dark .date-display:hover {
border-color: #555 !important;
background-color: #2a2a2a !important;
}
.date-range-picker.color-mode-dark .date-input-wrapper.focused .date-display {
border-color: #177ddc !important;
box-shadow: 0 0 0 2px rgba(23, 125, 220, 0.2) !important;
}
.date-range-picker.color-mode-dark .date-separator {
color: #888 !important;
}
/* 深色模式支持 */
@media (prefers-color-scheme: dark) {
.date-label {
.date-range-picker:not(.color-mode-light) .date-label {
color: #b0b0b0;
}
.date-display {
.date-range-picker:not(.color-mode-light) .date-display {
background-color: #1f1f1f;
border-color: #444;
color: #e0e0e0;
@@ -159,40 +219,40 @@
}
/* 深色模式占位符 */
.date-display.placeholder {
.date-range-picker:not(.color-mode-light) .date-display.placeholder {
color: #777;
}
/* 深色模式悬停状态 */
.date-display:hover {
.date-range-picker:not(.color-mode-light) .date-display:hover {
border-color: #555;
background-color: #2a2a2a;
}
/* 深色模式聚焦的输入容器 */
.date-input-wrapper.focused .date-display {
.date-range-picker:not(.color-mode-light) .date-input-wrapper.focused .date-display {
border-color: #177ddc;
box-shadow: 0 0 0 2px rgba(23, 125, 220, 0.2);
}
.date-input:focus + .date-display {
.date-range-picker:not(.color-mode-light) .date-input:focus + .date-display {
border-color: #177ddc;
box-shadow: 0 0 0 2px rgba(23, 125, 220, 0.2);
}
/* 深色模式焦点状态 */
.date-display:focus {
.date-range-picker:not(.color-mode-light) .date-display:focus {
border-color: #177ddc;
box-shadow: 0 0 0 2px rgba(23, 125, 220, 0.2);
}
/* 深色模式按下状态 */
.date-display:active {
.date-range-picker:not(.color-mode-light) .date-display:active {
background-color: #2d2d2d;
border-color: #666;
}
.date-separator {
.date-range-picker:not(.color-mode-light) .date-separator {
color: #888;
}
}
+156 -6
View File
@@ -80,6 +80,31 @@
@apply bg-purple-100 text-purple-800;
}
/* 新增的文档类型样式 */
/* 合同文档 */
.file-type-tag-contract {
background-color: #e8f5fd;
color: #0077cc;
}
/* 行政许可卷宗 */
.file-type-tag-license-doc {
background-color: #e8fdf5;
color: #00a67e;
}
/* 行政处罚卷宗 */
.file-type-tag-punishment-doc {
background-color: #fee7e7;
color: #e53e3e;
}
/* 其他文档 */
.file-type-tag-other {
background-color: #fef5e7;
color: #dd6b20;
}
/* 尺寸变体 */
.file-type-tag-sm {
padding: 2px 6px;
@@ -139,35 +164,160 @@
opacity: 0.9;
}
/* 明确指定浅色模式 */
.file-type-tag.color-mode-light {
background-color: #f0f0f0 !important;
color: #333 !important;
}
.file-type-tag.color-mode-light.file-type-sales-contract {
background-color: #e8f5fd !important;
color: #0077cc !important;
}
.file-type-tag.color-mode-light.file-type-purchase-contract {
background-color: #f5f0ff !important;
color: #6b46c1 !important;
}
.file-type-tag.color-mode-light.file-type-license {
background-color: #fef5e7 !important;
color: #dd6b20 !important;
}
.file-type-tag.color-mode-light.file-type-punishment {
background-color: #fee7e7 !important;
color: #e53e3e !important;
}
.file-type-tag.color-mode-light.file-type-agreement {
background-color: #e8fdf5 !important;
color: #00a67e !important;
}
/* 新增文档类型的浅色模式 */
.file-type-tag.color-mode-light.file-type-tag-contract {
background-color: #e8f5fd !important;
color: #0077cc !important;
}
.file-type-tag.color-mode-light.file-type-tag-license-doc {
background-color: #e8fdf5 !important;
color: #00a67e !important;
}
.file-type-tag.color-mode-light.file-type-tag-punishment-doc {
background-color: #fee7e7 !important;
color: #e53e3e !important;
}
.file-type-tag.color-mode-light.file-type-tag-other {
background-color: #fef5e7 !important;
color: #dd6b20 !important;
}
/* 明确指定深色模式 */
.file-type-tag.color-mode-dark {
background-color: #2d2d2d !important;
color: #e0e0e0 !important;
}
.file-type-tag.color-mode-dark.file-type-sales-contract {
background-color: rgba(0, 119, 204, 0.2) !important;
color: #4db8ff !important;
}
.file-type-tag.color-mode-dark.file-type-purchase-contract {
background-color: rgba(107, 70, 193, 0.2) !important;
color: #b794f4 !important;
}
.file-type-tag.color-mode-dark.file-type-license {
background-color: rgba(221, 107, 32, 0.2) !important;
color: #f6ad55 !important;
}
.file-type-tag.color-mode-dark.file-type-punishment {
background-color: rgba(229, 62, 62, 0.2) !important;
color: #fc8181 !important;
}
.file-type-tag.color-mode-dark.file-type-agreement {
background-color: rgba(0, 166, 126, 0.2) !important;
color: #68d5b1 !important;
}
/* 新增文档类型的深色模式 */
.file-type-tag.color-mode-dark.file-type-tag-contract {
background-color: rgba(0, 119, 204, 0.2) !important;
color: #4db8ff !important;
}
.file-type-tag.color-mode-dark.file-type-tag-license-doc {
background-color: rgba(0, 166, 126, 0.2) !important;
color: #68d5b1 !important;
}
.file-type-tag.color-mode-dark.file-type-tag-punishment-doc {
background-color: rgba(229, 62, 62, 0.2) !important;
color: #fc8181 !important;
}
.file-type-tag.color-mode-dark.file-type-tag-other {
background-color: rgba(221, 107, 32, 0.2) !important;
color: #f6ad55 !important;
}
/* 适配深色模式 */
@media (prefers-color-scheme: dark) {
.file-type-tag {
.file-type-tag:not(.color-mode-light) {
background-color: #2d2d2d;
color: #e0e0e0;
}
.file-type-sales-contract {
.file-type-tag:not(.color-mode-light).file-type-sales-contract {
background-color: rgba(0, 119, 204, 0.2);
color: #4db8ff;
}
.file-type-purchase-contract {
.file-type-tag:not(.color-mode-light).file-type-purchase-contract {
background-color: rgba(107, 70, 193, 0.2);
color: #b794f4;
}
.file-type-license {
.file-type-tag:not(.color-mode-light).file-type-license {
background-color: rgba(221, 107, 32, 0.2);
color: #f6ad55;
}
.file-type-punishment {
.file-type-tag:not(.color-mode-light).file-type-punishment {
background-color: rgba(229, 62, 62, 0.2);
color: #fc8181;
}
.file-type-agreement {
.file-type-tag:not(.color-mode-light).file-type-agreement {
background-color: rgba(0, 166, 126, 0.2);
color: #68d5b1;
}
/* 新增文档类型的深色模式自动适配 */
.file-type-tag:not(.color-mode-light).file-type-tag-contract {
background-color: rgba(0, 119, 204, 0.2);
color: #4db8ff;
}
.file-type-tag:not(.color-mode-light).file-type-tag-license-doc {
background-color: rgba(0, 166, 126, 0.2);
color: #68d5b1;
}
.file-type-tag:not(.color-mode-light).file-type-tag-punishment-doc {
background-color: rgba(229, 62, 62, 0.2);
color: #fc8181;
}
.file-type-tag:not(.color-mode-light).file-type-tag-other {
background-color: rgba(221, 107, 32, 0.2);
color: #f6ad55;
}
}
+159 -125
View File
@@ -1,127 +1,161 @@
/**
* 首页特定样式
*/
/* 仪表盘容器 */
.dashboard-container {
@apply p-2;
}
/* 统计卡片 */
.stat-grid {
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 p-4;
}
.stat-card {
@apply bg-white rounded-lg p-4 shadow transition-all duration-200 relative;
}
.stat-card:hover {
@apply transform -translate-y-[3px] shadow-[0_4px_15px_rgba(0,0,0,0.1)];
}
.stat-title {
@apply text-sm text-gray-600 mb-2.5;
}
.stat-value {
@apply text-2xl font-semibold text-gray-900 mb-2.5;
}
.stat-trend {
@apply flex items-center text-xs;
}
.stat-trend.trend-up {
@apply text-[#52c41a];
}
.stat-trend.trend-down {
@apply text-[#f5222d];
}
.stat-icon {
@apply absolute right-4 top-4 text-2xl text-[#00684a] opacity-20;
}
/* 快捷访问网格 */
.shortcut-grid {
@apply grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-4 p-4;
}
.shortcut-item {
@apply flex flex-col items-center justify-center p-4 bg-white rounded-lg shadow
transition-all duration-200 cursor-pointer text-gray-900 h-24;
}
.shortcut-item:hover {
@apply transform -translate-y-[3px] shadow-[0_4px_12px_rgba(0,0,0,0.1)]
bg-[rgba(0,104,74,0.05)] text-[#00684a];
}
.shortcut-icon {
@apply text-2xl text-[#00684a] mb-2.5;
}
.shortcut-label {
@apply text-sm text-center;
}
/* 文档列表 */
.doc-list {
@apply space-y-3 p-4;
}
.doc-item {
@apply flex justify-between items-center p-3 border border-gray-100 rounded-lg hover:bg-gray-50 transition-colors duration-200;
}
.doc-info {
@apply flex items-center;
}
.doc-icon {
@apply text-2xl mr-3 text-primary;
}
.doc-name {
@apply text-sm font-medium text-gray-800 mb-1;
}
.doc-meta {
@apply text-xs text-gray-500;
}
.doc-status {
@apply flex items-center;
}
/* 卡片样式 */
.dashboard-card {
@apply bg-white rounded-lg shadow p-5 mb-5 transition-all duration-200;
}
.dashboard-card:hover {
@apply transform -translate-y-[3px] shadow-[0_4px_15px_rgba(0,0,0,0.1)];
}
.card-title {
@apply text-base font-semibold mb-4 flex items-center text-gray-900;
}
.card-title i {
@apply text-xl text-[#00684a] mr-2;
}
/* 响应式调整 */
@screen md {
.stat-grid {
@apply grid-cols-2;
/* 主页样式 */
.home-page {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f0f7f4;
}
}
@screen lg {
.stat-grid {
@apply grid-cols-4;
/* 头部样式 */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 2rem;
background-color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
}
.logo-container {
display: flex;
align-items: center;
}
.logo {
height: 36px;
margin-right: 0.75rem;
}
.logo-text {
font-size: 1.125rem;
font-weight: 600;
color: #333;
margin-right: 0.5rem;
}
.logo-text-en {
font-size: 0.75rem;
color: #666;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05rem;
}
.user-info {
display: flex;
align-items: center;
gap: 1.5rem;
}
.datetime {
font-size: 0.875rem;
color: #666;
}
.user {
display: flex;
align-items: center;
gap: 0.5rem;
}
.avatar {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
.username {
font-size: 0.875rem;
font-weight: 500;
color: #333;
}
/* 主要内容区域 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 3rem 1.5rem;
padding-bottom: 0;
}
.welcome-text {
font-size: 1.75rem;
font-weight: 500;
color: #333;
margin-bottom: 3rem;
text-align: center;
}
.modules-container {
display: flex;
justify-content: center;
gap: 2.5rem;
margin-bottom: 3rem;
}
.module-card {
display: flex;
flex-direction: column;
align-items: center;
width: 250px;
padding: 2rem 1.5rem;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
}
.module-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.module-icon {
width: 64px;
height: 64px;
margin-bottom: 1.5rem;
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
.contract-icon {
background-image: url('/images/contract-icon.svg');
}
.review-icon {
background-image: url('/images/review-icon.svg');
}
.ai-icon {
background-image: url('/images/ai-icon.svg');
}
.module-name {
font-size: 1.125rem;
font-weight: 500;
color: #333;
}
/* 底部山水背景 */
.footer {
height: 200px;
overflow: hidden;
position: relative;
}
.mountains-bg {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('/images/mountains-bg.svg');
background-position: center bottom;
background-repeat: no-repeat;
background-size: cover;
}
+103
View File
@@ -0,0 +1,103 @@
/* 登录页面样式 */
.login-page {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #f0f7f4 0%, #c5e8e0 100%);
}
.login-container {
width: 100%;
max-width: 480px;
padding: 2rem;
background-color: white;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.login-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 2rem;
}
.login-logo {
height: 60px;
margin-bottom: 1rem;
}
.login-title {
font-size: 1.5rem;
font-weight: 600;
color: #015c42;
text-align: center;
}
.login-subtitle {
font-size: 1.25rem;
font-weight: 500;
color: #333;
margin-bottom: 1.5rem;
text-align: center;
}
.login-form-container {
margin-bottom: 2rem;
}
.login-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-size: 0.875rem;
font-weight: 500;
color: #555;
}
.form-input {
padding: 0.75rem 1rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.2s;
}
.form-input:focus {
border-color: #2cad7d;
outline: none;
box-shadow: 0 0 0 2px rgba(44, 173, 125, 0.2);
}
.login-button {
margin-top: 1rem;
padding: 0.75rem 1.5rem;
background-color: #2cad7d;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.login-button:hover {
background-color: #1e9668;
}
.login-footer {
text-align: center;
font-size: 0.875rem;
color: #777;
}
+128
View File
@@ -0,0 +1,128 @@
/**
* 首页特定样式
*/
/* 仪表盘容器 */
.dashboard-container {
@apply pt-0;
}
/* 统计卡片 */
.stat-grid {
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 p-4;
}
.stat-card {
@apply bg-white rounded-lg p-4 shadow transition-all duration-200 relative;
}
.stat-card:hover {
@apply transform -translate-y-[3px] shadow-[0_4px_15px_rgba(0,0,0,0.1)];
}
.stat-title {
@apply text-sm text-gray-600 mb-2.5;
}
.stat-value {
@apply text-2xl font-semibold text-gray-900 mb-2.5;
}
.stat-trend {
@apply flex items-center text-xs;
}
.stat-trend.trend-up {
@apply text-[#52c41a];
}
.stat-trend.trend-down {
@apply text-[#f5222d];
}
.stat-icon {
@apply absolute right-4 top-4 text-2xl text-[#00684a] opacity-20;
}
/* 快捷访问网格 */
.shortcut-grid {
@apply grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-4 p-4;
}
.shortcut-item {
@apply flex flex-col items-center justify-center p-4 bg-white rounded-lg shadow
transition-all duration-200 cursor-pointer text-gray-900 h-24;
}
.shortcut-item:hover {
@apply transform -translate-y-[3px] shadow-[0_4px_12px_rgba(0,0,0,0.1)]
bg-[rgba(0,104,74,0.05)] text-[#00684a];
}
.shortcut-icon {
@apply text-2xl text-[#00684a] mb-2.5;
}
.shortcut-label {
@apply text-sm text-center;
}
/* 文档列表 */
.doc-list {
@apply space-y-3 p-4;
}
.doc-item {
@apply flex justify-between items-center p-3 border border-gray-100 rounded-lg hover:bg-gray-50 transition-colors duration-200;
}
.doc-info {
@apply flex items-center;
}
.doc-icon {
@apply text-2xl mr-3 text-primary;
}
.doc-name {
@apply text-sm font-medium text-gray-800 mb-1;
}
.doc-meta {
@apply text-xs text-gray-500;
}
.doc-status {
@apply flex items-center;
}
/* 卡片样式 */
.dashboard-card {
@apply bg-white rounded-lg shadow p-5 mb-5 transition-all duration-200;
}
.dashboard-card:hover {
@apply transform -translate-y-[3px] shadow-[0_4px_15px_rgba(0,0,0,0.1)];
}
.card-title {
@apply text-base font-semibold mb-4 flex items-center text-gray-900;
}
.card-title i {
@apply text-xl text-[#00684a] mr-2;
}
/* 响应式调整 */
@screen md {
.stat-grid {
@apply grid-cols-2;
}
}
@screen lg {
.stat-grid {
@apply grid-cols-4;
}
}
+1 -1
View File
@@ -11,7 +11,7 @@
--bg-gray: #f5f5f5;
}
.container {
.review-container {
width: 100%;
}
+2 -2
View File
@@ -209,7 +209,7 @@
border-color: rgba(0, 104, 74, 0.3);
}
.var-tag::before {
/* .var-tag::before {
content: '{';
margin-right: 1px;
}
@@ -217,7 +217,7 @@
.var-tag::after {
content: '}';
margin-left: 1px;
}
} */
/* 法典引用样式 */
.law-reference {
+2
View File
@@ -0,0 +1,2 @@
This is a placeholder for the avatar image.
Please replace with an actual avatar image file.
+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none">
<rect width="64" height="64" rx="32" fill="#E6F4EF"/>
<path d="M22 32H26" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round"/>
<path d="M38 32H42" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round"/>
<path d="M30 24H34" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round"/>
<path d="M30 40H34" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round"/>
<path d="M32 22V18" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round"/>
<path d="M32 46V42" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round"/>
<path d="M40 24L43 21" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round"/>
<path d="M21 43L24 40" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round"/>
<path d="M40 40L43 43" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round"/>
<path d="M21 21L24 24" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round"/>
<path d="M32 36V28" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round"/>
<circle cx="32" cy="32" r="12" stroke="#2CAD7D" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none">
<rect width="64" height="64" rx="32" fill="#E6F4EF"/>
<path d="M42 18H22C20.8954 18 20 18.8954 20 20V44C20 45.1046 20.8954 46 22 46H42C43.1046 46 44 45.1046 44 44V20C44 18.8954 43.1046 18 42 18Z" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M28 24H36" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M28 32H36" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M28 40H36" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 701 B

+5
View File
@@ -0,0 +1,5 @@
<svg width="100%" height="200" viewBox="0 0 1440 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 200H1440V100C1440 100 1320 130 1200 110C1080 90 960 40 840 60C720 80 600 130 480 110C360 90 240 40 120 60C40 73.3333 0 100 0 100V200Z" fill="#53A890" fill-opacity="0.4"/>
<path d="M0 200H1440V120C1440 120 1320 150 1200 130C1080 110 960 60 840 80C720 100 600 150 480 130C360 110 240 60 120 80C40 93.3333 0 120 0 120V200Z" fill="#388276" fill-opacity="0.4"/>
<path d="M0 200H1440V140C1440 140 1320 170 1200 150C1080 130 960 80 840 100C720 120 600 170 480 150C360 130 240 80 120 100C40 113.333 0 140 0 140V200Z" fill="#246C62" fill-opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 672 B

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none">
<rect width="64" height="64" rx="32" fill="#E6F4EF"/>
<path d="M36 20H28C26.8954 20 26 20.8954 26 22V42C26 43.1046 26.8954 44 28 44H36C37.1046 44 38 43.1046 38 42V22C38 20.8954 37.1046 20 36 20Z" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M32 40C33.1046 40 34 39.1046 34 38C34 36.8954 33.1046 36 32 36C30.8954 36 30 36.8954 30 38C30 39.1046 30.8954 40 32 40Z" stroke="#2CAD7D" stroke-width="2"/>
<path d="M26 30H38" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round"/>
<path d="M42 20C44.7614 20 47 22.2386 47 25V26" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round"/>
<path d="M42 44C44.7614 44 47 41.7614 47 39V38" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round"/>
<path d="M22 20C19.2386 20 17 22.2386 17 25V26" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round"/>
<path d="M22 44C19.2386 44 17 41.7614 17 39V38" stroke="#2CAD7D" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+2
View File
@@ -0,0 +1,2 @@
This is a placeholder for the logo image.
Please replace with an actual logo image file.