完成智慧法务前端调整20250522,还有登录和主页需要完善
This commit is contained in:
@@ -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 || {}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' : ''}`}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -485,6 +485,7 @@ export default function RulesFiles() {
|
||||
onStartDateChange={(value) => handleDateChange('dateFrom', value)}
|
||||
onEndDateChange={(value) => handleDateChange('dateTo', value)}
|
||||
simple={true}
|
||||
colorMode="light"
|
||||
/>
|
||||
|
||||
<FilterSelect
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
--bg-gray: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
.review-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
This is a placeholder for the avatar image.
|
||||
Please replace with an actual avatar image 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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -0,0 +1,2 @@
|
||||
This is a placeholder for the logo image.
|
||||
Please replace with an actual logo image file.
|
||||
Reference in New Issue
Block a user