添加登录内容,尚未完善,先创建分支
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { type LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||
import { OAuthClient } from "~/utils/oauth-client";
|
||||
import { OAUTH_CONFIG } from "~/config/api-config";
|
||||
import { sessionStorage } from "~/root";
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const code = url.searchParams.get("code");
|
||||
const state = url.searchParams.get("state");
|
||||
const error = url.searchParams.get("error");
|
||||
const error_description = url.searchParams.get("error_description");
|
||||
|
||||
// 检查是否有错误
|
||||
if (error) {
|
||||
console.error("OAuth2.0授权失败:", error, error_description);
|
||||
return redirect(`/login?error=${encodeURIComponent(error_description || error)}`);
|
||||
}
|
||||
|
||||
// 检查是否有授权码
|
||||
if (!code) {
|
||||
console.error("OAuth2.0回调缺少授权码");
|
||||
return redirect("/login?error=missing_code");
|
||||
}
|
||||
|
||||
// 验证状态值(可选,但建议实现)
|
||||
// 这里简单验证state是否以_idp结尾
|
||||
if (!state || !state.endsWith("_idp")) {
|
||||
console.error("OAuth2.0状态值验证失败");
|
||||
return redirect("/login?error=invalid_state");
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建OAuth客户端
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG);
|
||||
|
||||
// 获取访问令牌
|
||||
const tokenResponse = await oauthClient.getAccessToken(code);
|
||||
if (!tokenResponse) {
|
||||
console.error("获取访问令牌失败");
|
||||
return redirect("/login?error=token_error");
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const userInfo = await oauthClient.getUserInfo(tokenResponse.access_token);
|
||||
if (!userInfo || !userInfo.success) {
|
||||
console.error("获取用户信息失败:", userInfo);
|
||||
return redirect("/login?error=userinfo_error");
|
||||
}
|
||||
|
||||
// 创建会话
|
||||
const session = await sessionStorage.getSession();
|
||||
session.set("isAuthenticated", true);
|
||||
session.set("accessToken", tokenResponse.access_token);
|
||||
session.set("refreshToken", tokenResponse.refresh_token);
|
||||
session.set("tokenIssuedAt", Date.now());
|
||||
session.set("tokenExpiresIn", tokenResponse.expires_in);
|
||||
session.set("userInfo", userInfo.data);
|
||||
|
||||
// 根据用户信息判断用户角色,这里可以根据实际业务逻辑调整
|
||||
const userRole = userInfo.data.username === "admin" ? "developer" : "common";
|
||||
session.set("userRole", userRole);
|
||||
|
||||
// 获取重定向URL
|
||||
const redirectTo = url.searchParams.get("redirect") || "/";
|
||||
|
||||
const cookie = await sessionStorage.commitSession(session);
|
||||
|
||||
return redirect(redirectTo, {
|
||||
headers: {
|
||||
"Set-Cookie": cookie
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("OAuth2.0回调处理失败:", error);
|
||||
return redirect("/login?error=callback_error");
|
||||
}
|
||||
}
|
||||
|
||||
export default function Callback() {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">正在处理登录...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {type MetaFunction} from "@remix-run/node";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{title: "交叉评查 - 中国烟草AI合同及卷宗审核系统"},
|
||||
{name: "cross-checking", content: "交叉评查"}
|
||||
]
|
||||
}
|
||||
|
||||
// export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
// const { user } = await requireUser(request);
|
||||
// return json({ user });
|
||||
// }
|
||||
|
||||
// export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
// const { user } = await requireUser(request);
|
||||
// return json({ user });
|
||||
// }
|
||||
|
||||
export default function CrossCheckingIndex() {
|
||||
return (
|
||||
<div>
|
||||
<h1>交叉评查</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import {type MetaFunction} from "@remix-run/node";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{title: "交叉评查 - 中国烟草AI合同及卷宗审核系统"},
|
||||
{name: "cross-checking", content: "交叉评查"}
|
||||
]
|
||||
}
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: "交叉评查"
|
||||
}
|
||||
|
||||
/**
|
||||
* 交叉评查路由布局
|
||||
*/
|
||||
export default function CrossCheckingLayout() {
|
||||
return (
|
||||
<Outlet />
|
||||
)
|
||||
}
|
||||
@@ -643,7 +643,7 @@ export default function DocumentsIndex() {
|
||||
// 检查audit_status是否为0,如果是则更新为2
|
||||
if (auditStatus === 0 || auditStatus === null) {
|
||||
try {
|
||||
|
||||
// console.log('开始审核',fileId,auditStatus)
|
||||
const response = await updateDocumentAuditStatus(fileId.toString(), 2);
|
||||
if (response.error) {
|
||||
console.error('更新文件审核状态失败:', response.error);
|
||||
@@ -656,7 +656,7 @@ export default function DocumentsIndex() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('更新成功,开始跳转')
|
||||
// 导航到评查详情页
|
||||
navigate(`/reviews?id=${fileId}&previousRoute=documents`);
|
||||
};
|
||||
|
||||
+229
-221
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { MetaFunction, ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { Form, useActionData, useLoaderData, useNavigate, useBlocker } from "@remix-run/react";
|
||||
import { Form, useActionData, useLoaderData, useNavigate } from "@remix-run/react";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Table } from "~/components/ui/Table";
|
||||
@@ -121,49 +121,10 @@ async function handleFileUpload(
|
||||
priority: Priority,
|
||||
documentNumber: string | null,
|
||||
remark: string | null,
|
||||
isTestDocument: boolean
|
||||
isTestDocument: boolean,
|
||||
documentId?: number | null,
|
||||
isReupload: boolean = false
|
||||
): Promise<FileUploadResponse> {
|
||||
// try {
|
||||
// // 使用封装的上传函数
|
||||
// const response = await uploadDocumentToServer(
|
||||
// binaryData,
|
||||
// fileName,
|
||||
// fileType,
|
||||
// documentType,
|
||||
// PRIORITY_TO_CHINESE[priority],
|
||||
// documentNumber,
|
||||
// remark,
|
||||
// isTestDocument
|
||||
// );
|
||||
|
||||
// if (response.error) {
|
||||
// console.error('[API] 上传错误:', response.error);
|
||||
// return {
|
||||
// success: false,
|
||||
// error: response.error
|
||||
// };
|
||||
// }
|
||||
|
||||
// // 确保返回有效的FileUploadResponse对象
|
||||
// // console.log('上传成功:', response.data);
|
||||
// if (response.data) {
|
||||
// return response.data;
|
||||
// }
|
||||
|
||||
// // 如果没有数据,则返回错误
|
||||
// // console.log('上传失败:', response.error);
|
||||
// return {
|
||||
// success: false,
|
||||
// error: '上传失败,未获取到响应数据'
|
||||
// };
|
||||
// } catch (error) {
|
||||
// console.error('[API] 上传错误:', error);
|
||||
// return {
|
||||
// success: false,
|
||||
// error: error instanceof Error ? error.message : '上传失败'
|
||||
// };
|
||||
// }
|
||||
|
||||
const response = await uploadDocumentToServer(
|
||||
binaryData,
|
||||
fileName,
|
||||
@@ -172,7 +133,9 @@ async function handleFileUpload(
|
||||
priority,
|
||||
documentNumber,
|
||||
remark,
|
||||
isTestDocument
|
||||
isTestDocument,
|
||||
documentId,
|
||||
isReupload
|
||||
);
|
||||
|
||||
if (response.error || !response.data) {
|
||||
@@ -309,9 +272,9 @@ export default function FilesUpload() {
|
||||
|
||||
// 合同文件上传状态
|
||||
// 这些变量暂时未使用,但保留以备将来扩展
|
||||
// const [isContractType, setIsContractType] = useState<boolean>(false);
|
||||
// const [contractMainFiles, setContractMainFiles] = useState<File[]>([]);
|
||||
// const [contractAttachmentFiles, setContractAttachmentFiles] = useState<File[]>([]);
|
||||
const [isContractType, setIsContractType] = useState<boolean>(false);
|
||||
const [contractMainFiles, setContractMainFiles] = useState<File[]>([]);
|
||||
const [contractAttachmentFiles, setContractAttachmentFiles] = useState<File[]>([]);
|
||||
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [uploadSpeed, setUploadSpeed] = useState("0KB/s");
|
||||
@@ -336,13 +299,13 @@ export default function FilesUpload() {
|
||||
if (typeof window !== 'undefined') {
|
||||
const storedReviewType = sessionStorage.getItem('reviewType');
|
||||
setReviewType(storedReviewType);
|
||||
|
||||
// 根据 reviewType 过滤文档类型和文档列表
|
||||
filterDocumentTypes(storedReviewType, loaderData.documentTypes);
|
||||
filterDocuments(storedReviewType);
|
||||
|
||||
// 如果reviewType是contract,自动选择合同文档类型
|
||||
if (storedReviewType === 'contract') {
|
||||
setIsContractType(true);
|
||||
// 查找ID为1的合同文档类型
|
||||
const contractType = loaderData.documentTypes.find(type => type.id === 1);
|
||||
if (contractType) {
|
||||
@@ -385,7 +348,11 @@ export default function FilesUpload() {
|
||||
const filterDocuments = async (reviewType: string | null) => {
|
||||
if (!reviewType) {
|
||||
// 如果没有特定的 reviewType,使用原始数据
|
||||
setQueueFiles(loaderData.documents);
|
||||
const documents = loaderData.documents;
|
||||
setQueueFiles(documents);
|
||||
|
||||
// 启动状态检查定时器
|
||||
startStatusChecker(documents);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -399,12 +366,20 @@ export default function FilesUpload() {
|
||||
setQueueFiles(loaderData.documents);
|
||||
return;
|
||||
}
|
||||
const documents = response.data || [];
|
||||
console.log('过滤文档列表成功:', documents);
|
||||
setQueueFiles(documents);
|
||||
|
||||
setQueueFiles(response.data || []);
|
||||
// 数据加载完成后立即启动状态检查定时器
|
||||
startStatusChecker(documents);
|
||||
} catch (error) {
|
||||
console.error('过滤文档列表失败:', error);
|
||||
// 出错时使用原始数据
|
||||
setQueueFiles(loaderData.documents);
|
||||
const documents = loaderData.documents;
|
||||
setQueueFiles(documents);
|
||||
|
||||
// 即使出错也启动状态检查定时器
|
||||
startStatusChecker(documents);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -424,8 +399,10 @@ export default function FilesUpload() {
|
||||
// 上传完成后的文件信息列表
|
||||
const [completedFiles, setCompletedFiles] = useState<UploadedFile[]>([]);
|
||||
|
||||
// 计时器引用
|
||||
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
// 计时器引用 - 分离为三个独立的定时器
|
||||
const uploadProgressIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const processingStatusIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const queueStatusIntervalRef = useRef<NodeJS.Timeout | null>(null); // 原 statusCheckIntervalRef
|
||||
|
||||
// UploadArea组件引用
|
||||
const uploadAreaRef = useRef<UploadAreaRef>(null);
|
||||
@@ -451,69 +428,122 @@ export default function FilesUpload() {
|
||||
}
|
||||
}, [actionData]);
|
||||
|
||||
// 状态检查定时器引用
|
||||
const statusCheckIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 添加组件挂载状态引用
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
|
||||
// useEffect 处理上传队列状态检查定时器 - 只在组件卸载时清除
|
||||
useEffect(() => {
|
||||
// console.log('设置上传队列状态检查定时器');
|
||||
console.log('设置上传队列状态检查定时器');
|
||||
|
||||
// 标记组件已挂载
|
||||
isMountedRef.current = true;
|
||||
|
||||
// 设置定时器检查队列中文件的状态,初始先加载一次查询
|
||||
checkQueueStatus();
|
||||
statusCheckIntervalRef.current = setInterval(checkQueueStatus, 10000);
|
||||
|
||||
// 只在组件卸载时清除
|
||||
return () => {
|
||||
// console.log('组件卸载,清除上传队列状态检查定时器');
|
||||
// 标记组件已卸载
|
||||
isMountedRef.current = false;
|
||||
if (statusCheckIntervalRef.current) {
|
||||
clearInterval(statusCheckIntervalRef.current);
|
||||
statusCheckIntervalRef.current = null;
|
||||
if (queueStatusIntervalRef.current) {
|
||||
clearInterval(queueStatusIntervalRef.current);
|
||||
queueStatusIntervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 检查队列中未完成文档的状态
|
||||
const checkQueueStatus = async () => {
|
||||
// 启动状态检查定时器的函数
|
||||
const startStatusChecker = (files: Document[]) => {
|
||||
console.log('启动状态检查定时器,队列文件数量:', files.length);
|
||||
|
||||
// 清除之前的定时器
|
||||
if (queueStatusIntervalRef.current) {
|
||||
clearInterval(queueStatusIntervalRef.current);
|
||||
}
|
||||
|
||||
// 只有当有文件时才启动定时器
|
||||
if (files.length > 0) {
|
||||
// 立即检查一次
|
||||
checkQueueStatusWithFiles(files);
|
||||
|
||||
// 启动定时器
|
||||
queueStatusIntervalRef.current = setInterval(() => {
|
||||
if (isMountedRef.current) {
|
||||
// 获取最新的queueFiles状态
|
||||
setQueueFiles(currentFiles => {
|
||||
checkQueueStatusWithFiles(currentFiles);
|
||||
return currentFiles; // 不改变状态,只是为了获取最新值
|
||||
});
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
};
|
||||
|
||||
// 检查指定文件列表的状态
|
||||
const checkQueueStatusWithFiles = async (files: Document[]) => {
|
||||
try {
|
||||
// console.log('开始检查队列状态,当前队列文件:', queueFiles);
|
||||
// console.log('开始检查队列状态,当前队列文件:', files);
|
||||
|
||||
// 获取所有未完成的文档ID
|
||||
const incompleteIds = queueFiles
|
||||
.filter(file => file.status !== DocumentStatus.PROCESSED && file.id)
|
||||
.map(file => file.id);
|
||||
// 直接从sessionStorage读取reviewType,避免异步状态更新问题
|
||||
const currentReviewType = typeof window !== 'undefined' ? sessionStorage.getItem('reviewType') : null;
|
||||
// console.log('从sessionStorage读取的reviewType:', currentReviewType);
|
||||
|
||||
// console.log('未完成的文档ID:', incompleteIds);
|
||||
// 获取所有未完成的文档
|
||||
const incompleteFiles = files.filter(file =>
|
||||
file.status !== DocumentStatus.PROCESSED && file.id
|
||||
);
|
||||
|
||||
if (incompleteIds.length === 0) {
|
||||
// console.log('没有未完成的文档,跳过状态检查');
|
||||
if (incompleteFiles.length === 0) {
|
||||
console.log('没有未完成的文档,跳过状态检查');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取这些文档的最新状态
|
||||
const statusResponse = await getDocumentsStatus(incompleteIds);
|
||||
let statusResponse;
|
||||
|
||||
// 如果是合同类型,需要分类处理
|
||||
console.log('当前reviewType:', currentReviewType);
|
||||
if (currentReviewType === 'contract') {
|
||||
// 分类文档ID
|
||||
const mainDocumentIds: number[] = [];
|
||||
const attachmentIds: number[] = [];
|
||||
|
||||
incompleteFiles.forEach(file => {
|
||||
// 检查是否存在template_contract_path属性来判断是否为合同附件
|
||||
if ('template_contract_path' in file && file.template_contract_path) {
|
||||
attachmentIds.push(file.id);
|
||||
} else {
|
||||
mainDocumentIds.push(file.id);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('合同主文件ID:', mainDocumentIds);
|
||||
console.log('合同附件ID:', attachmentIds);
|
||||
|
||||
// 分别查询状态
|
||||
statusResponse = await getDocumentsStatus(mainDocumentIds, attachmentIds);
|
||||
} else {
|
||||
// 非合同类型,使用原有逻辑
|
||||
const incompleteIds = incompleteFiles.map(file => file.id);
|
||||
// console.log('未完成的文档ID:', incompleteIds);
|
||||
statusResponse = await getDocumentsStatus(incompleteIds);
|
||||
}
|
||||
|
||||
// console.log('状态检查响应:', statusResponse);
|
||||
|
||||
if (statusResponse.data) {
|
||||
// 更新队列中的文档状态
|
||||
// 更新队列中的文档状态,使用批量更新避免频繁渲染
|
||||
setQueueFiles(prevFiles => {
|
||||
let hasChanges = false;
|
||||
const updatedFiles = prevFiles.map(file => {
|
||||
const updatedStatus = statusResponse.data.find(doc => doc.id === file.id);
|
||||
if (updatedStatus) {
|
||||
// console.log(`文档 ${file.id} 状态更新: ${file.status} -> ${updatedStatus.status}`);
|
||||
if (updatedStatus && updatedStatus.status !== file.status) {
|
||||
console.log(`文档 ${file.id} 状态更新: ${file.status} -> ${updatedStatus.status}`);
|
||||
hasChanges = true;
|
||||
return { ...file, status: updatedStatus.status };
|
||||
}
|
||||
return file;
|
||||
});
|
||||
// console.log('更新后的队列文件:', updatedFiles);
|
||||
return updatedFiles;
|
||||
|
||||
// 只有在确实有变化时才返回新数组
|
||||
return hasChanges ? updatedFiles : prevFiles;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -574,11 +604,11 @@ export default function FilesUpload() {
|
||||
// currentFiles: currentFiles.length
|
||||
// });
|
||||
|
||||
// setIsContractType(isContract);
|
||||
setIsContractType(isContract);
|
||||
|
||||
// 重置文件状态
|
||||
// setContractMainFiles([]);
|
||||
// setContractAttachmentFiles([]);
|
||||
setContractMainFiles([]);
|
||||
setContractAttachmentFiles([]);
|
||||
setCurrentFiles([]);
|
||||
|
||||
// 如果已经有选中的文件,且选择了文件类型,且不是合同类型,则开始上传
|
||||
@@ -593,14 +623,13 @@ export default function FilesUpload() {
|
||||
|
||||
} else {
|
||||
setFileType("");
|
||||
// setIsContractType(false);
|
||||
setIsContractType(false);
|
||||
// 如果用户选择了空选项,显示错误信息
|
||||
setFileTypeError("上传文件之前请选择文件类型");
|
||||
}
|
||||
};
|
||||
|
||||
// 处理合同主文件选择 - 暂时未使用,保留以备将来扩展
|
||||
/*
|
||||
// 处理合同主文件选择
|
||||
const handleContractMainFilesSelected = (files: FileList) => {
|
||||
try {
|
||||
// console.log('【调试-handleContractMainFilesSelected】开始处理合同主文件选择, 文件数量:', files.length);
|
||||
@@ -639,7 +668,7 @@ export default function FilesUpload() {
|
||||
// console.log('【调试-handleContractMainFilesSelected】有效文件数量:', validFiles.length);
|
||||
// console.log('【调试-handleContractMainFilesSelected】有效文件:', validFiles.map(f => ({ name: f.name, size: f.size, type: f.type })));
|
||||
|
||||
// setContractMainFiles(validFiles);
|
||||
setContractMainFiles(validFiles);
|
||||
} else {
|
||||
console.error('【调试-handleContractMainFilesSelected】没有有效的PDF文件或组件已卸载');
|
||||
}
|
||||
@@ -650,10 +679,8 @@ export default function FilesUpload() {
|
||||
console.error('【调试-handleContractMainFilesSelected】处理合同主文件选择时发生错误:', error);
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
// 处理合同附件选择 - 暂时未使用,保留以备将来扩展
|
||||
/*
|
||||
// 处理合同附件选择
|
||||
const handleContractAttachmentFilesSelected = (files: FileList) => {
|
||||
try {
|
||||
// console.log('【调试-handleContractAttachmentFilesSelected】开始处理合同附件选择, 文件数量:', files.length);
|
||||
@@ -692,7 +719,7 @@ export default function FilesUpload() {
|
||||
// console.log('【调试-handleContractAttachmentFilesSelected】有效文件数量:', validFiles.length);
|
||||
// console.log('【调试-handleContractAttachmentFilesSelected】有效文件:', validFiles.map(f => ({ name: f.name, size: f.size, type: f.type })));
|
||||
|
||||
// setContractAttachmentFiles(validFiles);
|
||||
setContractAttachmentFiles(validFiles);
|
||||
} else {
|
||||
console.error('【调试-handleContractAttachmentFilesSelected】没有有效的PDF文件或组件已卸载');
|
||||
}
|
||||
@@ -703,17 +730,15 @@ export default function FilesUpload() {
|
||||
console.error('【调试-handleContractAttachmentFilesSelected】处理合同附件选择时发生错误:', error);
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
// 检查并准备上传 - 暂时未使用,保留以备将来扩展
|
||||
/*
|
||||
// 检查并准备上传
|
||||
const checkAndPrepareUpload = (mainFiles: File[], attachmentFiles: File[]) => {
|
||||
try {
|
||||
// console.log('【调试-checkAndPrepareUpload】开始检查并准备上传文件', {
|
||||
// mainFilesCount: mainFiles.length,
|
||||
// attachmentFilesCount: attachmentFiles.length,
|
||||
// fileType
|
||||
// });
|
||||
console.log('【调试-checkAndPrepareUpload】开始检查并准备上传文件', {
|
||||
mainFilesCount: mainFiles.length,
|
||||
attachmentFilesCount: attachmentFiles.length,
|
||||
fileType
|
||||
});
|
||||
|
||||
// 检查组件是否已卸载
|
||||
if (!isMountedRef.current) {
|
||||
@@ -749,11 +774,11 @@ export default function FilesUpload() {
|
||||
}
|
||||
|
||||
// 记录主文件和附件文件信息
|
||||
// console.log('【调试-checkAndPrepareUpload】合同主文件:', mainFiles.map(f => ({ name: f.name, size: f.size, type: f.type })));
|
||||
console.log('【调试-checkAndPrepareUpload】合同主文件:', mainFiles.map(f => ({ name: f.name, size: f.size, type: f.type })));
|
||||
if (attachmentFiles.length > 0) {
|
||||
// console.log('【调试-checkAndPrepareUpload】合同附件文件:', attachmentFiles.map(f => ({ name: f.name, size: f.size, type: f.type })));
|
||||
console.log('【调试-checkAndPrepareUpload】合同附件文件:', attachmentFiles.map(f => ({ name: f.name, size: f.size, type: f.type })));
|
||||
} else {
|
||||
// console.log('【调试-checkAndPrepareUpload】无合同附件文件');
|
||||
console.log('【调试-checkAndPrepareUpload】无合同附件文件');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -778,7 +803,7 @@ export default function FilesUpload() {
|
||||
setCurrentFiles(allFiles);
|
||||
|
||||
// 将准备上传的操作移到这里,暂时不执行
|
||||
// console.log('【调试-checkAndPrepareUpload】准备上传', allFiles.length, '个文件');
|
||||
console.log('【调试-checkAndPrepareUpload】准备上传', allFiles.length, '个文件');
|
||||
|
||||
if (fileType) {
|
||||
try {
|
||||
@@ -829,12 +854,11 @@ export default function FilesUpload() {
|
||||
}
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
// 开始上传文件
|
||||
const startUpload = async (files: File[]) => {
|
||||
try {
|
||||
// console.log('【调试-startUpload】开始上传过程,文件数量:', files.length);
|
||||
console.log('【调试-startUpload】开始上传过程,文件数量:', files.length);
|
||||
|
||||
// 检查组件是否已卸载
|
||||
if (!isMountedRef.current) {
|
||||
@@ -878,14 +902,14 @@ export default function FilesUpload() {
|
||||
// console.log("【调试-startUpload】开始转换文件到二进制格式...");
|
||||
|
||||
// 模拟上传进度
|
||||
if (progressIntervalRef.current) {
|
||||
clearInterval(progressIntervalRef.current);
|
||||
if (uploadProgressIntervalRef.current) {
|
||||
clearInterval(uploadProgressIntervalRef.current);
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
let lastUploadedSize = 0;
|
||||
|
||||
progressIntervalRef.current = setInterval(() => {
|
||||
uploadProgressIntervalRef.current = setInterval(() => {
|
||||
const currentTime = Date.now();
|
||||
const timeElapsed = (currentTime - startTime) / 1000; // 转换为秒
|
||||
const currentSpeed = (uploadedSize - lastUploadedSize) / timeElapsed; // 字节/秒
|
||||
@@ -902,7 +926,12 @@ export default function FilesUpload() {
|
||||
// 上传所有文件
|
||||
const uploadedFiles: UploadedFile[] = [];
|
||||
|
||||
let temp_n = 0;
|
||||
let firstFileDocumentId: number | null = null; // 保存第一个文件的document_id
|
||||
|
||||
for (const file of files) {
|
||||
temp_n++;
|
||||
console.log('【调试-startUpload】上传文件:','第', temp_n, '个文件', file.name);
|
||||
try {
|
||||
// console.log(`【调试-startUpload】准备上传文件: ${file.name}, 大小: ${formatFileSize(file.size)}`);
|
||||
|
||||
@@ -939,7 +968,9 @@ export default function FilesUpload() {
|
||||
priority,
|
||||
documentNumber || null,
|
||||
remark || null,
|
||||
isTestDocument
|
||||
isTestDocument,
|
||||
temp_n > 1 ? firstFileDocumentId : null, // 第二个文件及以后使用第一个文件的document_id
|
||||
false
|
||||
);
|
||||
|
||||
const timeoutPromise = new Promise<FileUploadResponse>((_, reject) => {
|
||||
@@ -964,6 +995,12 @@ export default function FilesUpload() {
|
||||
|
||||
response = uploadResult;
|
||||
|
||||
// 保存第一个文件的document_id,用于后续附件上传
|
||||
if (temp_n === 1 && response.result?.id) {
|
||||
firstFileDocumentId = response.result.id;
|
||||
console.log('【调试-startUpload】保存第一个文件的document_id:', firstFileDocumentId);
|
||||
}
|
||||
|
||||
// console.log(`【调试-startUpload】文件 ${file.name} 上传响应:`, response);
|
||||
} catch (error) {
|
||||
// 检查组件是否已卸载
|
||||
@@ -1010,8 +1047,8 @@ export default function FilesUpload() {
|
||||
}
|
||||
|
||||
// 清除进度定时器
|
||||
if (progressIntervalRef.current) {
|
||||
clearInterval(progressIntervalRef.current);
|
||||
if (uploadProgressIntervalRef.current) {
|
||||
clearInterval(uploadProgressIntervalRef.current);
|
||||
}
|
||||
|
||||
// 更新上传状态
|
||||
@@ -1052,8 +1089,8 @@ export default function FilesUpload() {
|
||||
setProcessingSteps(errorSteps);
|
||||
|
||||
// 清除进度定时器
|
||||
if (progressIntervalRef.current) {
|
||||
clearInterval(progressIntervalRef.current);
|
||||
if (uploadProgressIntervalRef.current) {
|
||||
clearInterval(uploadProgressIntervalRef.current);
|
||||
}
|
||||
|
||||
// 显示错误提示
|
||||
@@ -1107,8 +1144,8 @@ export default function FilesUpload() {
|
||||
// console.log('【调试-startProcessing】开始处理文件,设置文件处理进度定时器');
|
||||
|
||||
// 清除之前的进度定时器(如果存在)
|
||||
if (progressIntervalRef.current) {
|
||||
clearInterval(progressIntervalRef.current);
|
||||
if (processingStatusIntervalRef.current) {
|
||||
clearInterval(processingStatusIntervalRef.current);
|
||||
}
|
||||
|
||||
// 立即开始检查状态
|
||||
@@ -1120,7 +1157,7 @@ export default function FilesUpload() {
|
||||
}
|
||||
|
||||
// 设置文件处理进度定时器,每10秒检查一次状态
|
||||
progressIntervalRef.current = setInterval(() => {
|
||||
processingStatusIntervalRef.current = setInterval(() => {
|
||||
// console.log('【调试-startProcessing】文件处理进度定时器触发,检查文件状态');
|
||||
try {
|
||||
checkProcessingStatus(fileIds);
|
||||
@@ -1133,9 +1170,9 @@ export default function FilesUpload() {
|
||||
console.error('【调试-startProcessing】处理文件过程中发生错误:', error);
|
||||
|
||||
// 清除进度定时器
|
||||
if (progressIntervalRef.current) {
|
||||
clearInterval(progressIntervalRef.current);
|
||||
progressIntervalRef.current = null;
|
||||
if (processingStatusIntervalRef.current) {
|
||||
clearInterval(processingStatusIntervalRef.current);
|
||||
processingStatusIntervalRef.current = null;
|
||||
}
|
||||
|
||||
// 更新步骤状态为错误
|
||||
@@ -1205,9 +1242,9 @@ export default function FilesUpload() {
|
||||
// console.log('【调试-checkProcessingStatus】所有文件处理完成,更新步骤状态为完成');
|
||||
|
||||
// 清除文件处理进度定时器
|
||||
if (progressIntervalRef.current) {
|
||||
clearInterval(progressIntervalRef.current);
|
||||
progressIntervalRef.current = null;
|
||||
if (processingStatusIntervalRef.current) {
|
||||
clearInterval(processingStatusIntervalRef.current);
|
||||
processingStatusIntervalRef.current = null;
|
||||
// console.log('【调试-checkProcessingStatus】文件处理完成,清除文件处理进度定时器');
|
||||
}
|
||||
|
||||
@@ -1316,10 +1353,15 @@ export default function FilesUpload() {
|
||||
|
||||
// 重置上传状态 - 不清除队列状态检查定时器
|
||||
const resetUpload = () => {
|
||||
// 清除文件处理进度定时器
|
||||
if (progressIntervalRef.current) {
|
||||
clearInterval(progressIntervalRef.current);
|
||||
progressIntervalRef.current = null;
|
||||
// 清除上传和处理相关的定时器
|
||||
if (uploadProgressIntervalRef.current) {
|
||||
clearInterval(uploadProgressIntervalRef.current);
|
||||
uploadProgressIntervalRef.current = null;
|
||||
}
|
||||
|
||||
if (processingStatusIntervalRef.current) {
|
||||
clearInterval(processingStatusIntervalRef.current);
|
||||
processingStatusIntervalRef.current = null;
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
@@ -1331,8 +1373,8 @@ export default function FilesUpload() {
|
||||
setCompletedFiles([]);
|
||||
|
||||
// 重置合同文件状态
|
||||
// setContractMainFiles([]);
|
||||
// setContractAttachmentFiles([]);
|
||||
setContractMainFiles([]);
|
||||
setContractAttachmentFiles([]);
|
||||
|
||||
// 重置步骤状态
|
||||
setProcessingSteps([
|
||||
@@ -1390,6 +1432,9 @@ export default function FilesUpload() {
|
||||
const handleViewFile = async (record: Document) => {
|
||||
try {
|
||||
// console.log('【调试-handleViewFile】开始处理查看文件,文件ID:', record.id);
|
||||
// console.log('【调试-handleViewFile】开始处理查看文件,文件:', record);
|
||||
|
||||
// 点击查看
|
||||
|
||||
// 检查audit_status是否为0,如果是则更新为2
|
||||
if (record.audit_status === 0 || record.audit_status === null) {
|
||||
@@ -1448,8 +1493,8 @@ export default function FilesUpload() {
|
||||
width: "40%",
|
||||
render: (_: unknown, record: Document) => (
|
||||
<div className="flex items-center">
|
||||
<i className={`${record.name.includes('.pdf') ? 'ri-file-pdf-line text-red-500' : 'ri-file-word-2-line text-blue-500'} mr-2 text-lg`}></i>
|
||||
<span className="truncate">{record.name}</span>
|
||||
<i className={`${record.name?.includes('.pdf') ? 'ri-file-pdf-line text-red-500' : 'ri-file-word-2-line text-blue-500'} mr-2 text-lg`}></i>
|
||||
<span className="truncate">{record.name || '未知文件'}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
@@ -1559,45 +1604,6 @@ export default function FilesUpload() {
|
||||
}
|
||||
];
|
||||
|
||||
// 添加路由阻止器
|
||||
// const shouldBlock = uploadStage === "uploading" || uploadStage === "processing";
|
||||
|
||||
// 使用useBlocker来阻止页面导航
|
||||
// const blocker = useBlocker(
|
||||
// ({ nextLocation }) => {
|
||||
// return shouldBlock && window.location.pathname !== nextLocation.pathname;
|
||||
// }
|
||||
// );
|
||||
|
||||
// // 处理阻止导航的逻辑
|
||||
// useEffect(() => {
|
||||
// if (blocker.state === "blocked") {
|
||||
// const confirmed = window.confirm(
|
||||
// "文件正在上传或处理中,离开页面将中断操作。确定要离开吗?"
|
||||
// );
|
||||
// if (confirmed) {
|
||||
// blocker.proceed();
|
||||
// } else {
|
||||
// blocker.reset();
|
||||
// }
|
||||
// }
|
||||
// }, [blocker]);
|
||||
|
||||
// 添加页面刷新/关闭提示
|
||||
// useEffect(() => {
|
||||
// const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
// if (shouldBlock) {
|
||||
// e.preventDefault();
|
||||
// e.returnValue = "文件正在上传或处理中,离开页面将中断操作。确定要离开吗?";
|
||||
// return e.returnValue;
|
||||
// }
|
||||
// };
|
||||
|
||||
// window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
// return () => {
|
||||
// window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
// };
|
||||
// }, [shouldBlock]);
|
||||
|
||||
return (
|
||||
<div className="file-upload-page">
|
||||
@@ -1688,7 +1694,7 @@ export default function FilesUpload() {
|
||||
{/* 自定义标题栏 */}
|
||||
<div className="w-full flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium">文件上传</h3>
|
||||
{/* {isContractType && uploadStage === "idle" && (
|
||||
{isContractType && uploadStage === "idle" && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-upload-cloud-line"
|
||||
@@ -1696,15 +1702,14 @@ export default function FilesUpload() {
|
||||
>
|
||||
开始上传
|
||||
</Button>
|
||||
)} */}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 初始上传区域 */}
|
||||
{uploadStage === "idle" && (
|
||||
<>
|
||||
{/* {!isContractType ? ( */}
|
||||
{/* {true ? ( */}
|
||||
{/* // 标准上传区域 - 非合同类型 */}
|
||||
{!isContractType ? (
|
||||
// 标准上传区域 - 非合同类型
|
||||
<UploadArea
|
||||
ref={uploadAreaRef}
|
||||
onFilesSelected={handleFilesSelected}
|
||||
@@ -1713,53 +1718,53 @@ export default function FilesUpload() {
|
||||
tipText="支持单个或多个pdf文件上传,文件格式:PDF"
|
||||
shouldPreventFileSelect={!fileType}
|
||||
/>
|
||||
{/* ) : ( */}
|
||||
{/* 合同文件上传区域 - 双区域并排 */}
|
||||
{/* <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
) : (
|
||||
// 合同文件上传区域 - 双区域并排
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">合同主文件</h4>
|
||||
<UploadArea
|
||||
// onFilesSelected={handleContractMainFilesSelected}
|
||||
// ref={contractMainFileRef}
|
||||
// multiple={false}
|
||||
// accept=".pdf"
|
||||
// tipText="请上传合同主文件,格式:PDF"
|
||||
// mainText="上传合同主文件"
|
||||
// buttonText="选择主文件"
|
||||
// icon="ri-file-text-line"
|
||||
// shouldPreventFileSelect={!fileType}
|
||||
// />
|
||||
// {contractMainFiles.length > 0 && (
|
||||
// <div className="mt-2 text-sm text-green-600">
|
||||
// <i className="ri-checkbox-circle-line"></i>
|
||||
// 已选择主文件: <span className="font-medium">{contractMainFiles[0].name}</span>
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// <div>
|
||||
// <h4 className="font-medium mb-2">合同附件</h4>
|
||||
// <UploadArea
|
||||
// onFilesSelected={handleContractAttachmentFilesSelected}
|
||||
// ref={contractAttachmentFileRef}
|
||||
// multiple={false}
|
||||
// accept=".pdf"
|
||||
// tipText="请上传合同附件,格式:PDF"
|
||||
// mainText="上传合同附件"
|
||||
// buttonText="选择附件"
|
||||
// icon="ri-file-copy-line"
|
||||
// shouldPreventFileSelect={!fileType}
|
||||
// />
|
||||
// {contractAttachmentFiles.length > 0 && (
|
||||
// <div className="mt-2 text-sm text-green-600">
|
||||
// <i className="ri-checkbox-circle-line"></i>
|
||||
// 已选择附件: {contractAttachmentFiles.map((file, index) => (
|
||||
// <span key={index} className="font-medium">{file.name}</span>
|
||||
// ))}
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
onFilesSelected={handleContractMainFilesSelected}
|
||||
ref={contractMainFileRef}
|
||||
multiple={false}
|
||||
accept=".pdf"
|
||||
tipText="请上传合同主文件,格式:PDF"
|
||||
mainText="上传合同主文件"
|
||||
buttonText="选择主文件"
|
||||
icon="ri-file-text-line"
|
||||
shouldPreventFileSelect={!fileType}
|
||||
/>
|
||||
{contractMainFiles.length > 0 && (
|
||||
<div className="mt-2 text-sm text-green-600">
|
||||
<i className="ri-checkbox-circle-line"></i>
|
||||
已选择主文件: <span className="font-medium">{contractMainFiles[0].name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">合同附件</h4>
|
||||
<UploadArea
|
||||
onFilesSelected={handleContractAttachmentFilesSelected}
|
||||
ref={contractAttachmentFileRef}
|
||||
multiple={false}
|
||||
accept=".pdf"
|
||||
tipText="请上传合同附件,格式:PDF"
|
||||
mainText="上传合同附件"
|
||||
buttonText="选择附件"
|
||||
icon="ri-file-copy-line"
|
||||
shouldPreventFileSelect={!fileType}
|
||||
/>
|
||||
{contractAttachmentFiles.length > 0 && (
|
||||
<div className="mt-2 text-sm text-green-600">
|
||||
<i className="ri-checkbox-circle-line"></i>
|
||||
已选择附件: {contractAttachmentFiles.map((file, index) => (
|
||||
<span key={index} className="font-medium">{file.name}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 测试文档标记 */}
|
||||
<div className="switch-container mb-4">
|
||||
@@ -1915,8 +1920,11 @@ export default function FilesUpload() {
|
||||
icon="ri-refresh-line"
|
||||
onClick={() => {
|
||||
// 清除所有定时器
|
||||
if (progressIntervalRef.current) {
|
||||
clearInterval(progressIntervalRef.current);
|
||||
if (uploadProgressIntervalRef.current) {
|
||||
clearInterval(uploadProgressIntervalRef.current);
|
||||
}
|
||||
if (processingStatusIntervalRef.current) {
|
||||
clearInterval(processingStatusIntervalRef.current);
|
||||
}
|
||||
// 重置状态
|
||||
resetUpload();
|
||||
|
||||
+93
-140
@@ -1,8 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import { useActionData, Form } from "@remix-run/react";
|
||||
import { type MetaFunction, type ActionFunctionArgs, redirect, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { useEffect } from "react";
|
||||
import { useSearchParams } from "@remix-run/react";
|
||||
import { type MetaFunction, type LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||
import { OAuthClient } from "~/utils/oauth-client";
|
||||
import { OAUTH_CONFIG } from "~/config/api-config";
|
||||
import { getUserSession, getSession } from "~/root";
|
||||
import styles from "~/styles/pages/login.css?url";
|
||||
import { getUserSession, getSession, type UserRole, sessionStorage } from "~/root";
|
||||
|
||||
export const links = () => [
|
||||
{ rel: "stylesheet", href: styles }
|
||||
@@ -15,71 +17,6 @@ export const meta: MetaFunction = () => {
|
||||
];
|
||||
};
|
||||
|
||||
// 处理表单提交的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;
|
||||
const userRole = formData.get("userRole") as UserRole || 'common';
|
||||
|
||||
// console.log("userRole-----", userRole);
|
||||
|
||||
// 简单的登录验证,实际应用中应该进行真正的身份验证
|
||||
if (!username || !password) {
|
||||
return Response.json({ error: "用户名和密码不能为空" });
|
||||
}
|
||||
|
||||
if(userRole === 'common') {
|
||||
// console.log("username-----", username);
|
||||
// console.log("password-----", password);
|
||||
const validUsers = [
|
||||
{ username: 'gdycuser', password: 'gdyc06111' },
|
||||
{ username: 'gdycuser2', password: 'gdyc06112' },
|
||||
{ username: 'gdycuser3', password: 'gdyc06113' }
|
||||
];
|
||||
const validUser = validUsers.find(user => user.username === username && user.password === password);
|
||||
if (!validUser) {
|
||||
return Response.json({ error: "普通用户用户名或密码错误" });
|
||||
}
|
||||
}
|
||||
|
||||
// console.log("login success", userRole);
|
||||
|
||||
// 管理员登录
|
||||
if (userRole === 'developer') {
|
||||
const validAdminUsers = [
|
||||
{ username: 'admin', password: 'admin0611' },
|
||||
// { username: 'admin2', password: 'admin06112' },
|
||||
// { username: 'admin3', password: 'admin06113' }
|
||||
];
|
||||
const validAdminUser = validAdminUsers.find(user => user.username === username && user.password === password);
|
||||
if (!validAdminUser) {
|
||||
return Response.json({ error: "管理员用户名或密码错误" });
|
||||
}
|
||||
}
|
||||
|
||||
// 获取session中存储的重定向URL,如果没有则默认到/
|
||||
const session = await getSession(request);
|
||||
// 查看session中存储的redirectTo值
|
||||
const redirectTo = session.get("redirectTo") || "/";
|
||||
// console.log("登录后重定向到:", redirectTo);
|
||||
|
||||
// 创建会话cookie
|
||||
const newSession = await sessionStorage.getSession();
|
||||
newSession.set("isAuthenticated", true);
|
||||
newSession.set("userRole", userRole);
|
||||
const cookie = await sessionStorage.commitSession(newSession);
|
||||
|
||||
// console.log("设置cookie:", !!cookie);
|
||||
|
||||
// 使用新方法进行重定向
|
||||
return redirect(redirectTo, {
|
||||
headers: {
|
||||
"Set-Cookie": cookie
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 加载器,获取当前会话状态
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const { isAuthenticated } = await getUserSession(request);
|
||||
@@ -88,96 +25,112 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
if (isAuthenticated) {
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
// 获取重定向URL并保存到session
|
||||
const url = new URL(request.url);
|
||||
const redirectTo = url.searchParams.get("redirect") || "/";
|
||||
|
||||
return Response.json({ isAuthenticated });
|
||||
const session = await getSession(request);
|
||||
session.set("redirectTo", redirectTo);
|
||||
|
||||
return Response.json({
|
||||
isAuthenticated: false,
|
||||
redirectTo
|
||||
});
|
||||
}
|
||||
|
||||
export default function Login() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [userRole, setUserRole] = useState<UserRole>("common");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const actionData = useActionData<typeof action>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const error = searchParams.get("error");
|
||||
|
||||
// 获取错误消息的友好描述
|
||||
const getErrorMessage = (error: string | null) => {
|
||||
if (!error) return null;
|
||||
|
||||
switch (error) {
|
||||
case "missing_code":
|
||||
return "登录过程中缺少授权码,请重新登录";
|
||||
case "invalid_state":
|
||||
return "登录状态验证失败,请重新登录";
|
||||
case "token_error":
|
||||
return "获取访问令牌失败,请重新登录";
|
||||
case "userinfo_error":
|
||||
return "获取用户信息失败,请重新登录";
|
||||
case "callback_error":
|
||||
return "登录回调处理失败,请重新登录";
|
||||
default:
|
||||
return decodeURIComponent(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理OAuth2.0登录
|
||||
const handleOAuthLogin = () => {
|
||||
try {
|
||||
// 创建OAuth客户端
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG);
|
||||
|
||||
// 生成状态值
|
||||
const state = oauthClient.generateState();
|
||||
|
||||
// 将状态值保存到localStorage(用于后续验证)
|
||||
localStorage.setItem("oauth_state", state);
|
||||
|
||||
// 获取授权URL
|
||||
const authorizeUrl = oauthClient.getAuthorizeUrl(state);
|
||||
|
||||
// 重定向到IDaaS登录页面
|
||||
window.location.href = authorizeUrl;
|
||||
} catch (error) {
|
||||
console.error("启动OAuth2.0登录失败:", error);
|
||||
alert("登录系统初始化失败,请联系系统管理员");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 检查OAuth配置是否完整
|
||||
if (!OAUTH_CONFIG.serverUrl || !OAUTH_CONFIG.clientId || !OAUTH_CONFIG.clientSecret) {
|
||||
console.error("OAuth2.0配置不完整:", OAUTH_CONFIG);
|
||||
}
|
||||
}, []);
|
||||
|
||||
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-container">
|
||||
<div className="error-icon"><i className="ri-error-warning-line"></i></div>
|
||||
<div className="error-text">{actionData.error}</div>
|
||||
</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="请输入用户名"
|
||||
/>
|
||||
<h2 className="login-subtitle">统一身份认证登录</h2>
|
||||
|
||||
{error && (
|
||||
<div className="error-message-container">
|
||||
<div className="error-icon"><i className="ri-error-warning-line"></i></div>
|
||||
<div className="error-text">{getErrorMessage(error)}</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">密码</label>
|
||||
<div className="password-input-container">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
id="password"
|
||||
name="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="form-input password-input"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="password-toggle-btn"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
aria-label={showPassword ? "隐藏密码" : "显示密码"}
|
||||
>
|
||||
<i className={showPassword ? "ri-eye-off-line" : "ri-eye-line"}></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="userRole">用户角色</label>
|
||||
<select
|
||||
id="userRole"
|
||||
name="userRole"
|
||||
value={userRole}
|
||||
onChange={(e) => setUserRole(e.target.value as UserRole)}
|
||||
className="form-input"
|
||||
required
|
||||
>
|
||||
<option value="common">普通用户</option>
|
||||
<option value="developer">管理员</option>
|
||||
</select>
|
||||
)}
|
||||
|
||||
<div className="oauth-login-section">
|
||||
<div className="login-description">
|
||||
<p>请点击下方按钮进行统一身份认证登录</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="login-button"
|
||||
onClick={handleOAuthLogin}
|
||||
className="oauth-login-button"
|
||||
type="button"
|
||||
>
|
||||
登录
|
||||
<i className="ri-shield-user-line"></i>
|
||||
统一身份认证登录
|
||||
</button>
|
||||
</Form>
|
||||
|
||||
<div className="login-tips">
|
||||
<p>
|
||||
<i className="ri-information-line"></i>
|
||||
系统将跳转到统一身份认证平台进行登录
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="login-footer">
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { type LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||
import { OAuthClient } from "~/utils/oauth-client";
|
||||
import { OAUTH_CONFIG } from "~/config/api-config";
|
||||
import { sessionStorage } from "~/root";
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const session = await sessionStorage.getSession(request.headers.get("Cookie"));
|
||||
|
||||
// 获取访问令牌
|
||||
const accessToken = session.get("accessToken");
|
||||
|
||||
if (accessToken) {
|
||||
try {
|
||||
// 创建OAuth客户端
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG);
|
||||
|
||||
// 构建登出后重定向URL
|
||||
const url = new URL(request.url);
|
||||
const redirectUrl = url.searchParams.get("redirect") || `${url.protocol}//${url.host}/login`;
|
||||
|
||||
// 调用IDaaS单点登出
|
||||
const logoutSuccess = await oauthClient.logout(accessToken, redirectUrl);
|
||||
|
||||
if (!logoutSuccess) {
|
||||
console.warn("IDaaS单点登出失败,但仍清除本地会话");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("单点登出过程中出错:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 无论IDaaS登出是否成功,都清除本地会话
|
||||
const cookie = await sessionStorage.destroySession(session);
|
||||
|
||||
return redirect("/login", {
|
||||
headers: {
|
||||
"Set-Cookie": cookie
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default function Logout() {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">正在退出登录...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+37
-14
@@ -39,7 +39,8 @@ import {
|
||||
FilePreview,
|
||||
ReviewPointsList,
|
||||
AIAnalysis,
|
||||
FileDetails
|
||||
FileDetails,
|
||||
Comparison
|
||||
} from "~/components/reviews";
|
||||
|
||||
// 从ReviewPointsList组件中导入ReviewPoint类型
|
||||
@@ -200,7 +201,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
document: reviewData.document,
|
||||
reviewPoints: reviewData.data,
|
||||
reviewInfo: reviewData.reviewInfo,
|
||||
statistics: reviewData.stats
|
||||
statistics: reviewData.stats,
|
||||
comparison_document: reviewData.comparison_document
|
||||
});
|
||||
} else {
|
||||
console.error("返回的评查数据格式不正确",JSON.stringify(reviewData,null,2));
|
||||
@@ -215,12 +217,13 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
export default function ReviewDetails() {
|
||||
const navigate = useNavigate();
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const { document, reviewPoints, statistics, reviewInfo } = loaderData;
|
||||
const { document, reviewPoints, statistics, reviewInfo, comparison_document } = loaderData;
|
||||
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
|
||||
const [activeTab, setActiveTab] = useState<string>('preview'); // 'preview', 'analysis', 'fileinfo'
|
||||
const [reviewData, setReviewData] = useState<ReviewData | null>(null);
|
||||
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null);
|
||||
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
|
||||
const [templateTargetPage, setTemplateTargetPage] = useState<number | undefined>(undefined);
|
||||
|
||||
// loader 数据加载出错
|
||||
useEffect(()=>{
|
||||
@@ -584,6 +587,7 @@ export default function ReviewDetails() {
|
||||
activeTab={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
fileInfo={{
|
||||
id: document?.id,
|
||||
previousRoute: loaderData.previousRoute,
|
||||
path: document?.path,
|
||||
auditStatus: document?.auditStatus,
|
||||
@@ -621,7 +625,7 @@ export default function ReviewDetails() {
|
||||
{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%]">
|
||||
<div className={`w-full ${comparison_document.template_contract_path ? 'lg:w-[38%]' : 'lg:w-[56%]'}`}>
|
||||
<FilePreview
|
||||
fileContent={document}
|
||||
reviewPoints={reviewData.reviewPoints}
|
||||
@@ -631,24 +635,43 @@ export default function ReviewDetails() {
|
||||
</div>
|
||||
|
||||
{/* 中间:附件文件预览 */}
|
||||
<div className="w-full lg:w-[38%]">
|
||||
<div className={`w-full ${comparison_document.template_contract_path ? 'lg:w-[38%]' : 'lg:w-[20%]'}`}>
|
||||
<FilePreview
|
||||
fileContent={document}
|
||||
fileContent={comparison_document}
|
||||
reviewPoints={[]}
|
||||
activeReviewPointResultId={activeReviewPointResultId}
|
||||
targetPage={targetPage}
|
||||
targetPage={templateTargetPage}
|
||||
isStructuredView={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 右侧:评查结果 */}
|
||||
{/* 右侧:结构比较结果 */}
|
||||
<div className="w-full lg:w-[24%]">
|
||||
<ReviewPointsList
|
||||
reviewPoints={reviewData.reviewPoints}
|
||||
statistics={reviewData.statistics}
|
||||
activeReviewPointResultId={activeReviewPointResultId}
|
||||
onReviewPointSelect={handleReviewPointSelect}
|
||||
onStatusChange={handleReviewPointStatusChange}
|
||||
<Comparison
|
||||
comparison_document={comparison_document}
|
||||
onPageJump={(sourcePage, templatePage) => {
|
||||
// 同时处理主文件和模板文件的页码跳转
|
||||
if (sourcePage > 0) {
|
||||
// 如果目标页码与当前页码相同,先重置再设置以强制触发更新
|
||||
if (sourcePage === targetPage) {
|
||||
setTargetPage(undefined);
|
||||
setTimeout(() => setTargetPage(sourcePage), 0);
|
||||
} else {
|
||||
setTargetPage(sourcePage);
|
||||
}
|
||||
console.log(`跳转到主文件第${sourcePage}页`);
|
||||
}
|
||||
if (templatePage > 0) {
|
||||
// 如果目标页码与当前页码相同,先重置再设置以强制触发更新
|
||||
if (templatePage === templateTargetPage) {
|
||||
setTemplateTargetPage(undefined);
|
||||
setTimeout(() => setTemplateTargetPage(templatePage), 0);
|
||||
} else {
|
||||
setTemplateTargetPage(templatePage);
|
||||
}
|
||||
console.log(`跳转到模板文件第${templatePage}页`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -123,7 +123,7 @@ export default function RulesFiles() {
|
||||
|
||||
// 根据 reviewType 添加类型过滤
|
||||
if (reviewType === 'contract') {
|
||||
searchParams.fileType = '1';
|
||||
searchParams.fileType = 'contract';
|
||||
} else if (reviewType === 'record') {
|
||||
// 在 API 层处理 type_id 为 2 或 3 的过滤
|
||||
searchParams.fileType = 'record';
|
||||
|
||||
Reference in New Issue
Block a user