添加交叉评查菜单页面,添加单点登录相关逻辑(待完善)
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
import { UPLOAD_URL } from '../../config/api-config';
|
||||
|
||||
/**
|
||||
* 从不同格式的 API 响应中提取数据
|
||||
* @param responseData API 响应数据
|
||||
* @returns 提取后的数据或 null
|
||||
*/
|
||||
function extractApiData<T>(responseData: unknown): T | null {
|
||||
if (!responseData) return null;
|
||||
|
||||
// 格式1: { code: number, msg: string, data: T }
|
||||
if (typeof responseData === 'object' && responseData !== null &&
|
||||
'code' in responseData &&
|
||||
'data' in responseData &&
|
||||
(responseData as { data: unknown }).data) {
|
||||
return (responseData as { data: T }).data;
|
||||
}
|
||||
|
||||
// 格式2: 直接是数据对象
|
||||
return responseData as T;
|
||||
}
|
||||
|
||||
// 案卷类型枚举
|
||||
export enum CaseType {
|
||||
ADMINISTRATIVE_PENALTY = "administrative_penalty",
|
||||
ADMINISTRATIVE_PERMIT = "administrative_permit"
|
||||
}
|
||||
|
||||
// 案卷类型到type_id的映射
|
||||
export const CASE_TYPE_TO_TYPE_ID: Record<CaseType, number> = {
|
||||
[CaseType.ADMINISTRATIVE_PENALTY]: 3, // 行政处罚
|
||||
[CaseType.ADMINISTRATIVE_PERMIT]: 2, // 行政许可
|
||||
};
|
||||
|
||||
// 文件上传响应接口
|
||||
export interface CrossCheckingFileUploadResponse {
|
||||
success: boolean;
|
||||
result?: {
|
||||
id: number;
|
||||
file_name: string;
|
||||
file_size: number;
|
||||
file_url: string;
|
||||
type_id: number;
|
||||
type_description: string;
|
||||
document_number: string | null;
|
||||
storage_type: string;
|
||||
is_test_document: boolean;
|
||||
remark: string | null;
|
||||
background_processing: boolean;
|
||||
evaluation_level: string;
|
||||
};
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
// 上传的文件接口
|
||||
export interface CrossCheckingUploadedFile {
|
||||
id: string; // 本地生成的唯一ID
|
||||
file: File;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
uploadType: 'single' | 'multiple';
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文件转换为二进制数据
|
||||
*/
|
||||
export async function uploadFileToBinary(file: File): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if (reader.result instanceof ArrayBuffer) {
|
||||
resolve(reader.result);
|
||||
} else {
|
||||
reject(new Error('文件读取失败'));
|
||||
}
|
||||
};
|
||||
reader.onerror = () => reject(new Error('文件读取失败'));
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传交叉评查文件到服务器
|
||||
* @param binaryData 文件的二进制数据
|
||||
* @param fileName 文件名
|
||||
* @param fileType 文件类型
|
||||
* @param typeId 文档类型ID
|
||||
* @param priority 优先级
|
||||
* @param documentNumber 文档编号(可选)
|
||||
* @param remark 备注信息(可选)
|
||||
* @param isTestDocument 是否为测试文档
|
||||
* @param documentId 关联的文档ID(用于附件上传)
|
||||
* @param isReupload 是否为重新上传
|
||||
* @returns 上传结果
|
||||
*/
|
||||
export async function uploadCrossCheckingDocument(
|
||||
binaryData: ArrayBuffer,
|
||||
fileName: string,
|
||||
fileType: string,
|
||||
typeId: number,
|
||||
priority: string = 'normal',
|
||||
documentNumber: string = '',
|
||||
remark: string = '',
|
||||
isTestDocument: boolean = false,
|
||||
documentId: number | null = null,
|
||||
isReupload: boolean = false
|
||||
): Promise<{data: CrossCheckingFileUploadResponse; error?: never} | {data?: never; error: string; status?: number}> {
|
||||
try {
|
||||
console.log('【交叉评查上传】开始上传文档:', { fileName, fileSize: binaryData.byteLength, typeId });
|
||||
|
||||
// 创建FormData对象
|
||||
const formData = new FormData();
|
||||
|
||||
// 将二进制数据转换为Blob并添加到FormData
|
||||
const blob = new Blob([binaryData], { type: fileType });
|
||||
formData.append('file', blob, fileName);
|
||||
console.log('【交叉评查上传】Blob已创建,文件大小:', blob.size);
|
||||
|
||||
// 将信息添加到一个JSON对象中
|
||||
const uploadInfo = {
|
||||
type_id: typeId,
|
||||
evaluation_level: priority,
|
||||
document_number: documentNumber || null,
|
||||
remark: remark || null,
|
||||
is_test_document: isTestDocument,
|
||||
document_id: documentId || null,
|
||||
is_reupload: isReupload
|
||||
};
|
||||
|
||||
// 添加JSON字符串到FormData
|
||||
formData.append('upload_info', JSON.stringify(uploadInfo));
|
||||
console.log('【交叉评查上传】FormData准备完成:', JSON.stringify(uploadInfo));
|
||||
|
||||
// 根据是否有documentId决定使用哪个接口
|
||||
const uploadEndpoint = '/batch_upload';
|
||||
const uploadUrl = UPLOAD_URL + uploadEndpoint;
|
||||
console.log('【交叉评查上传】准备发送请求到服务器:', uploadUrl);
|
||||
|
||||
// 发送请求
|
||||
try {
|
||||
console.log('【交叉评查上传】开始fetch请求...');
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-File-Name': encodeURIComponent(fileName)
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
console.log('【交叉评查上传】收到服务器响应:', { status: response.status, statusText: response.statusText });
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`【交叉评查上传】上传失败 (${response.status}): ${errorText}`);
|
||||
return {
|
||||
error: `上传失败: ${response.status} ${response.statusText} - ${errorText}`,
|
||||
status: response.status
|
||||
};
|
||||
}
|
||||
|
||||
console.log('【交叉评查上传】开始解析JSON响应');
|
||||
let responseData;
|
||||
try {
|
||||
responseData = await response.json();
|
||||
console.log('【交叉评查上传】JSON响应解析成功:', responseData);
|
||||
} catch (jsonError) {
|
||||
console.error('【交叉评查上传】JSON解析失败:', jsonError);
|
||||
return {
|
||||
error: `解析响应JSON失败: ${jsonError instanceof Error ? jsonError.message : '未知错误'}`,
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
|
||||
const extractedData = extractApiData<CrossCheckingFileUploadResponse>(responseData);
|
||||
console.log('【交叉评查上传】提取的数据:', extractedData);
|
||||
|
||||
if (!extractedData) {
|
||||
console.error('【交叉评查上传】无法提取数据');
|
||||
return { error: '处理上传响应失败', status: 500 };
|
||||
}
|
||||
|
||||
console.log('【交叉评查上传】上传成功,返回数据');
|
||||
return { data: extractedData as CrossCheckingFileUploadResponse };
|
||||
} catch (fetchError) {
|
||||
console.error('【交叉评查上传】fetch请求失败:', fetchError);
|
||||
return {
|
||||
error: `fetch请求错误: ${fetchError instanceof Error ? fetchError.message : '未知错误'}`,
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('【交叉评查上传】上传过程中发生错误:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '上传失败',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上传交叉评查文件
|
||||
* @param files 文件列表
|
||||
* @param typeId 文档类型ID
|
||||
* @param priority 优先级
|
||||
* @param documentNumber 文档编号
|
||||
* @param remark 备注信息
|
||||
* @param isTestDocument 是否为测试文档
|
||||
* @returns 上传结果列表
|
||||
*/
|
||||
export async function batchUploadCrossCheckingFiles(
|
||||
files: CrossCheckingUploadedFile[],
|
||||
typeId: number,
|
||||
priority: string = 'normal',
|
||||
documentNumber: string = '',
|
||||
remark: string = '',
|
||||
isTestDocument: boolean = false
|
||||
): Promise<{
|
||||
successes: Array<{file: CrossCheckingUploadedFile; result: CrossCheckingFileUploadResponse}>;
|
||||
failures: Array<{file: CrossCheckingUploadedFile; error: string}>;
|
||||
}> {
|
||||
const successes: Array<{file: CrossCheckingUploadedFile; result: CrossCheckingFileUploadResponse}> = [];
|
||||
const failures: Array<{file: CrossCheckingUploadedFile; error: string}> = [];
|
||||
|
||||
console.log('【交叉评查批量上传】开始批量上传文件,文件数量:', files.length);
|
||||
|
||||
for (const fileInfo of files) {
|
||||
try {
|
||||
console.log('【交叉评查批量上传】上传文件:', fileInfo.name);
|
||||
|
||||
// 转换文件为二进制格式
|
||||
const binaryData = await uploadFileToBinary(fileInfo.file);
|
||||
|
||||
// 上传文件
|
||||
const result = await uploadCrossCheckingDocument(
|
||||
binaryData,
|
||||
fileInfo.file.name,
|
||||
fileInfo.file.type,
|
||||
typeId,
|
||||
priority,
|
||||
documentNumber,
|
||||
remark,
|
||||
isTestDocument,
|
||||
null, // 交叉评查文件通常没有关联的文档ID
|
||||
false
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
console.error('【交叉评查批量上传】文件上传失败:', fileInfo.name, result.error);
|
||||
failures.push({
|
||||
file: fileInfo,
|
||||
error: result.error
|
||||
});
|
||||
} else if (result.data) {
|
||||
console.log('【交叉评查批量上传】文件上传成功:', fileInfo.name);
|
||||
successes.push({
|
||||
file: fileInfo,
|
||||
result: result.data
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('【交叉评查批量上传】处理文件时发生错误:', fileInfo.name, error);
|
||||
failures.push({
|
||||
file: fileInfo,
|
||||
error: error instanceof Error ? error.message : '上传失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log('【交叉评查批量上传】批量上传完成,成功:', successes.length, '失败:', failures.length);
|
||||
return { successes, failures };
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一文件ID
|
||||
* @returns 唯一ID字符串
|
||||
*/
|
||||
export function generateFileId(): string {
|
||||
return `cross_checking_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小显示
|
||||
* @param bytes 字节数
|
||||
* @returns 格式化后的文件大小字符串
|
||||
*/
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
@@ -132,6 +132,7 @@ export async function uploadFileToBinary(file: File): Promise<ArrayBuffer> {
|
||||
* @param remark 备注信息(可选)
|
||||
* @param isTestDocument 是否为测试文档
|
||||
* @param documentId 关联的文档ID(用于合同附件上传)
|
||||
* @param isReupload 是否为重新上传
|
||||
* @returns 上传结果
|
||||
*/
|
||||
export async function uploadDocumentToServer(
|
||||
|
||||
@@ -39,16 +39,16 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
||||
const handleBack = () => {
|
||||
// 防抖处理 - 如果已经在导航中,不重复触发
|
||||
if (isNavigating) return;
|
||||
|
||||
|
||||
// 设置导航状态为true
|
||||
setIsNavigating(true);
|
||||
loadingBarService.show();
|
||||
|
||||
|
||||
// 根据来源页面返回
|
||||
const previousRoute = fileInfo.previousRoute || 'documents';
|
||||
const returnTo = previousRoute === 'documents'
|
||||
? "/documents"
|
||||
: previousRoute === 'filesUpload'
|
||||
const returnTo = previousRoute === 'documents'
|
||||
? "/documents"
|
||||
: previousRoute === 'filesUpload'
|
||||
? "/files/upload"
|
||||
: "/rules-files";
|
||||
// 立即导航返回
|
||||
@@ -61,19 +61,19 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
||||
const handleDownloadFile = async () => {
|
||||
try {
|
||||
const downloadUrl = `${DOCUMENT_URL}${fileInfo.path}`;
|
||||
|
||||
|
||||
// 使用fetch获取文件内容
|
||||
const response = await fetch(downloadUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载失败: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
// 将响应转换为Blob
|
||||
const blob = await response.blob();
|
||||
|
||||
|
||||
// 创建Blob URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
|
||||
// 创建一个隐藏的a标签并点击它
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
@@ -83,7 +83,7 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
||||
a.download = decodeURIComponent(fileName);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
|
||||
// 清理
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
@@ -222,7 +222,7 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
||||
<div className="tab-nav w-full flex justify-between">
|
||||
{/* 评查结果、AI智能分析、文件信息 */}
|
||||
<div className="flex">
|
||||
<button
|
||||
<button
|
||||
className={`tab-nav-item ${activeTab === 'preview' ? 'active' : ''}`}
|
||||
onClick={() => onTabChange('preview')}
|
||||
type="button"
|
||||
@@ -244,11 +244,11 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
||||
onClick={() => onTabChange('filecompare')}
|
||||
type="button"
|
||||
aria-pressed={activeTab === 'filecompare'}
|
||||
>
|
||||
>
|
||||
<i className="ri-flip-horizontal-line"></i> 结构比对
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
<button
|
||||
className={`tab-nav-item ${activeTab === 'fileinfo' ? 'active' : ''}`}
|
||||
onClick={() => onTabChange('fileinfo')}
|
||||
type="button"
|
||||
@@ -269,14 +269,14 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
||||
</button>
|
||||
)}
|
||||
{/* 返回上一级 */}
|
||||
<button
|
||||
<button
|
||||
className="ant-btn ant-btn-default flex items-center my-2"
|
||||
onClick={() => handleBack()}
|
||||
disabled={isNavigating}
|
||||
>
|
||||
<i className="ri-arrow-left-line mr-1"></i> {isNavigating ? '返回中...' : '返回'}
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
className="ant-btn ant-btn-default flex items-center my-2"
|
||||
onClick={handleDownloadFile}
|
||||
>
|
||||
@@ -288,7 +288,7 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
||||
>
|
||||
<i className="ri-file-copy-line mr-1"></i> 导出评查报告
|
||||
</button> */}
|
||||
<button
|
||||
<button
|
||||
className={`ant-btn ant-btn-primary my-2 flex items-center ${fileInfo.auditStatus === 1 ? 'hidden' : ''}`}
|
||||
onClick={onConfirmResults}
|
||||
>
|
||||
@@ -296,7 +296,7 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="tab-content w-full">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
+12
-11
@@ -32,18 +32,19 @@ interface ApiConfig {
|
||||
const configs: Record<string, ApiConfig> = {
|
||||
// 开发环境
|
||||
development: {
|
||||
// baseUrl: 'http://172.16.0.55:8008',
|
||||
baseUrl: 'http://172.16.0.55:8008',
|
||||
// baseUrl: 'http://172.16.0.81:3000',
|
||||
baseUrl: 'http://nas.7bm.co:3000',
|
||||
documentUrl: 'http://172.16.0.81:9000/docauditai/',
|
||||
// baseUrl: 'http://nas.7bm.co:3000',
|
||||
// documentUrl: 'http://172.16.0.81:9000/docauditai/',
|
||||
documentUrl: 'http://172.16.0.55:8008/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:8008/admin/documents',
|
||||
// uploadUrl: 'http://172.16.0.58:8008/admin/documents',
|
||||
// uploadUrl: 'http://172.16.0.58:8008/admin/documents',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO', // 需要替换为实际的Client ID
|
||||
clientSecret: 'your_client_secret', // 需要替换为实际的Client Secret
|
||||
redirectUri: 'http://localhost:3000/callback', // 回调地址
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb', // 需要替换为实际的Client Secret
|
||||
redirectUri: 'http://10.79.97.17/', // 回调地址
|
||||
appId: 'idaasoauth2' // 应用ID,用于登出
|
||||
}
|
||||
},
|
||||
@@ -65,16 +66,16 @@ const configs: Record<string, ApiConfig> = {
|
||||
// 生产环境
|
||||
production: {
|
||||
// postgrest
|
||||
baseUrl: 'http://10.79.97.16:8000',
|
||||
baseUrl: 'http://10.79.97.17:8000',
|
||||
// minio
|
||||
documentUrl: 'http://10.76.244.156:9000/docauditai/',
|
||||
// 文件上传
|
||||
uploadUrl: 'http://10.79.97.16:8000/admin/documents/upload',
|
||||
uploadUrl: 'http://10.79.97.17:8000/admin/documents',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO', // 需要替换为实际的Client ID
|
||||
clientSecret: 'your_client_secret', // 需要替换为实际的Client Secret
|
||||
redirectUri: 'http://10.79.97.17/callback', // 回调地址
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb', // 需要替换为实际的Client Secret
|
||||
redirectUri: 'http://10.79.97.17/', // 回调地址
|
||||
appId: 'idaasoauth2' // 应用ID,用于登出
|
||||
}
|
||||
},
|
||||
|
||||
+23
-19
@@ -28,16 +28,9 @@ const extractAppId = (appUrl: string): string => {
|
||||
return appUrl;
|
||||
};
|
||||
|
||||
// 获取配置值并添加调试日志
|
||||
const getApiUrl = () => {
|
||||
// 在Remix中,我们使用本地API路由作为代理,而不是直接访问Dify API
|
||||
if (typeof window !== 'undefined') {
|
||||
// 客户端:使用相对路径访问本地API
|
||||
return '/api';
|
||||
} else {
|
||||
// 服务端:也使用相对路径
|
||||
return '/api';
|
||||
}
|
||||
// 获取Dify API配置
|
||||
const getDifyApiUrl = () => {
|
||||
return getEnvVar('NEXT_PUBLIC_API_URL', 'https://api.dify.ai/v1');
|
||||
};
|
||||
|
||||
const getAppId = () => {
|
||||
@@ -46,22 +39,36 @@ const getAppId = () => {
|
||||
// console.log('🔧 Chat Config Debug:', {
|
||||
// rawAppId,
|
||||
// extractedAppId,
|
||||
// apiUrl: getApiUrl(),
|
||||
// difyApiUrl: getDifyApiUrl(),
|
||||
// hasApiKey: !!getEnvVar('NEXT_PUBLIC_APP_KEY', ''),
|
||||
// difyApiUrl: getEnvVar('NEXT_PUBLIC_API_URL', ''),
|
||||
// });
|
||||
return extractedAppId;
|
||||
};
|
||||
|
||||
// 生成用户ID (模拟服务端逻辑)
|
||||
const generateUserId = () => {
|
||||
const appId = getAppId();
|
||||
// 生成或获取会话ID (可以使用localStorage或其他方式)
|
||||
let sessionId = '';
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionId = localStorage.getItem('dify_session_id') || '';
|
||||
if (!sessionId) {
|
||||
sessionId = 'sess_' + Math.random().toString(36).substring(2, 15);
|
||||
localStorage.setItem('dify_session_id', sessionId);
|
||||
}
|
||||
}
|
||||
return `user_${appId}:${sessionId}`;
|
||||
};
|
||||
|
||||
// 聊天应用配置
|
||||
export const CHAT_CONFIG = {
|
||||
// API相关配置 - 使用本地API路由作为代理
|
||||
API_URL: getApiUrl(),
|
||||
// API相关配置 - 直接使用Dify API
|
||||
API_URL: getDifyApiUrl(),
|
||||
APP_ID: getAppId(),
|
||||
API_KEY: getEnvVar('NEXT_PUBLIC_APP_KEY', ''),
|
||||
|
||||
// Dify API 配置(用于服务端)
|
||||
DIFY_API_URL: getEnvVar('NEXT_PUBLIC_API_URL', 'https://api.dify.ai/v1'),
|
||||
// 用户生成函数
|
||||
generateUserId,
|
||||
|
||||
// 应用信息
|
||||
APP_INFO: {
|
||||
@@ -76,9 +83,6 @@ export const CHAT_CONFIG = {
|
||||
isShowPrompt: false,
|
||||
promptTemplate: 'I want you to act as a javascript console.',
|
||||
|
||||
// API相关
|
||||
API_PREFIX: '/api',
|
||||
|
||||
// 本地化
|
||||
LOCALE_COOKIE_NAME: 'locale',
|
||||
|
||||
|
||||
@@ -113,8 +113,6 @@ export default function useConversation() {
|
||||
isSetToLocalStorage = true,
|
||||
newConversationName = ''
|
||||
) => {
|
||||
// console.log('🔄 设置当前会话ID:', { id, appId, isSetToLocalStorage });
|
||||
|
||||
doSetCurrConversationId(id);
|
||||
|
||||
if (isSetToLocalStorage && id !== '-1') {
|
||||
@@ -130,19 +128,11 @@ export default function useConversation() {
|
||||
|
||||
globalThis.localStorage?.setItem(storageConversationIdKey, JSON.stringify(conversationIdInfo));
|
||||
|
||||
// console.log('💾 会话ID已保存到localStorage:', {
|
||||
// appUrlKey,
|
||||
// conversationId: id,
|
||||
// fullStorage: conversationIdInfo
|
||||
// });
|
||||
} catch (error) {
|
||||
console.error('保存会话ID到本地存储失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 不进行URL导航,保持单页面应用模式
|
||||
// console.log('✅ 会话切换完成,当前会话ID:', id);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置新会话输入
|
||||
|
||||
@@ -7,6 +7,8 @@ import chatInputStyles from "~/styles/components/chat-with-llm/chat-input.css?ur
|
||||
import chatSidebarStyles from "~/styles/components/chat-with-llm/sidebar.css?url";
|
||||
import chatThoughtProcessStyles from "~/styles/components/chat-with-llm/thought-process.css?url";
|
||||
import chatMarkdownStyles from "~/styles/components/chat-with-llm/markdown.css?url";
|
||||
// 导入测试文件用于调试
|
||||
import "~/utils/dify-test.client";
|
||||
|
||||
export function links() {
|
||||
return [
|
||||
@@ -32,6 +34,11 @@ export const meta: MetaFunction = () => {
|
||||
/**
|
||||
* 聊天主页面
|
||||
* 实现单页面应用模式,所有会话切换都在同一页面内完成
|
||||
*
|
||||
* 调试说明:
|
||||
* - 打开浏览器开发者工具的控制台
|
||||
* - 输入 window.testDify() 可以测试Dify API连接
|
||||
* - 查看网络选项卡可以监控API请求
|
||||
*/
|
||||
export default function ChatWithLLMIndex() {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,523 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { type MetaFunction, type ActionFunctionArgs } from "@remix-run/node";
|
||||
import { Form, useNavigation } from "@remix-run/react";
|
||||
import { UploadArea, type UploadAreaRef } from "~/components/ui/UploadArea";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { messageService } from "~/components/ui/MessageModal";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import crossCheckingUploadStyles from "~/styles/pages/cross-checking-upload.css?url";
|
||||
import {
|
||||
CaseType,
|
||||
CASE_TYPE_TO_TYPE_ID,
|
||||
type CrossCheckingUploadedFile,
|
||||
generateFileId,
|
||||
formatFileSize,
|
||||
batchUploadCrossCheckingFiles
|
||||
} from "~/api/cross-checking/cross-files-upload";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "交叉评查上传 - 中国烟草AI合同及卷宗审核系统" },
|
||||
{ name: "description", content: "交叉评查案卷上传和任务创建" }
|
||||
];
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: "交叉评查上传"
|
||||
};
|
||||
|
||||
export function links() {
|
||||
return [
|
||||
{ rel: "stylesheet", href: crossCheckingUploadStyles }
|
||||
];
|
||||
}
|
||||
|
||||
// 步骤枚举
|
||||
const STEPS = [
|
||||
{ id: 1, label: "创建任务" },
|
||||
{ id: 2, label: "创建评查小组" },
|
||||
{ id: 3, label: "选择卷宗" }
|
||||
];
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const formData = await request.formData();
|
||||
const caseType = formData.get("caseType") as string;
|
||||
const uploadType = formData.get("uploadType") as string;
|
||||
|
||||
console.log("交叉评查上传:", { caseType, uploadType });
|
||||
|
||||
// 这里可以处理上传后的业务逻辑
|
||||
// 例如创建任务记录等
|
||||
return Response.json({ success: true, message: "文件上传成功" });
|
||||
};
|
||||
|
||||
export default function CrossCheckingUpload() {
|
||||
// 基础状态
|
||||
const [caseType, setCaseType] = useState<CaseType>(CaseType.ADMINISTRATIVE_PENALTY);
|
||||
const [currentStep] = useState(1);
|
||||
const navigation = useNavigation();
|
||||
|
||||
// 上传配置状态 - 设置默认值
|
||||
const [priority] = useState<string>("normal");
|
||||
const [documentNumber] = useState<string>("");
|
||||
const [remark] = useState<string>("");
|
||||
const [isTestDocument] = useState<boolean>(false);
|
||||
|
||||
// 文件管理状态
|
||||
const [singleFiles, setSingleFiles] = useState<CrossCheckingUploadedFile[]>([]);
|
||||
const [multipleFiles, setMultipleFiles] = useState<CrossCheckingUploadedFile[]>([]);
|
||||
const [uploadType, setUploadType] = useState<'none' | 'single' | 'multiple'>('none');
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
// 引用
|
||||
const singleUploadRef = useRef<UploadAreaRef>(null);
|
||||
const multipleUploadRef = useRef<UploadAreaRef>(null);
|
||||
|
||||
// 获取当前typeId
|
||||
const currentTypeId = CASE_TYPE_TO_TYPE_ID[caseType];
|
||||
|
||||
// 处理案卷类型切换
|
||||
const handleCaseTypeChange = (type: CaseType) => {
|
||||
if (isUploading) {
|
||||
toastService.warning("上传进行中,无法切换案卷类型");
|
||||
return;
|
||||
}
|
||||
|
||||
setCaseType(type);
|
||||
// 清空已选择的文件和重置上传方式
|
||||
clearAllFiles();
|
||||
console.log("案卷类型切换为:", type, "typeId:", CASE_TYPE_TO_TYPE_ID[type]);
|
||||
};
|
||||
|
||||
// 清空所有文件
|
||||
const clearAllFiles = () => {
|
||||
setSingleFiles([]);
|
||||
setMultipleFiles([]);
|
||||
setUploadType('none');
|
||||
// 重置文件输入框
|
||||
singleUploadRef.current?.resetFileInput();
|
||||
multipleUploadRef.current?.resetFileInput();
|
||||
};
|
||||
|
||||
// 处理单案件文件选择
|
||||
const handleSingleFilesSelected = (files: FileList) => {
|
||||
if (uploadType === 'multiple') {
|
||||
toastService.warning("已选择多案件导入方式,无法选择单案件文件");
|
||||
return;
|
||||
}
|
||||
|
||||
const validFiles: CrossCheckingUploadedFile[] = [];
|
||||
let hasInvalidFiles = false;
|
||||
|
||||
Array.from(files).forEach(file => {
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
validFiles.push({
|
||||
id: generateFileId(),
|
||||
file,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
uploadType: 'single'
|
||||
});
|
||||
} else {
|
||||
hasInvalidFiles = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasInvalidFiles) {
|
||||
messageService.error('只能上传PDF格式的文件', {
|
||||
title: '文件类型错误',
|
||||
confirmText: '确定',
|
||||
});
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
setSingleFiles(prev => [...prev, ...validFiles]);
|
||||
setUploadType('single');
|
||||
console.log("选择单案件文件:", validFiles.length, "个");
|
||||
}
|
||||
};
|
||||
|
||||
// 处理多案件文件选择
|
||||
const handleMultipleFilesSelected = (files: FileList) => {
|
||||
if (uploadType === 'single') {
|
||||
toastService.warning("已选择单案件导入方式,无法选择多案件文件");
|
||||
return;
|
||||
}
|
||||
|
||||
const validFiles: CrossCheckingUploadedFile[] = [];
|
||||
let hasInvalidFiles = false;
|
||||
|
||||
Array.from(files).forEach(file => {
|
||||
const isZip = file.type === 'application/zip' ||
|
||||
file.type === 'application/x-zip-compressed' ||
|
||||
file.name.toLowerCase().endsWith('.zip');
|
||||
const isRar = file.type === 'application/x-rar-compressed' ||
|
||||
file.name.toLowerCase().endsWith('.rar');
|
||||
const is7z = file.type === 'application/x-7z-compressed' ||
|
||||
file.name.toLowerCase().endsWith('.7z');
|
||||
const isTar = file.type === 'application/x-tar' ||
|
||||
file.name.toLowerCase().endsWith('.tar');
|
||||
|
||||
if (isZip || isRar || is7z || isTar) {
|
||||
validFiles.push({
|
||||
id: generateFileId(),
|
||||
file,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
uploadType: 'multiple'
|
||||
});
|
||||
} else {
|
||||
hasInvalidFiles = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasInvalidFiles) {
|
||||
messageService.error('只能上传ZIP或RAR格式的压缩文件', {
|
||||
title: '文件类型错误',
|
||||
confirmText: '确定',
|
||||
});
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
setMultipleFiles(prev => [...prev, ...validFiles]);
|
||||
setUploadType('multiple');
|
||||
console.log("选择多案件文件:", validFiles.length, "个");
|
||||
}
|
||||
};
|
||||
|
||||
// 删除单个文件
|
||||
const handleRemoveFile = (fileId: string, type: 'single' | 'multiple') => {
|
||||
if (isUploading) {
|
||||
toastService.warning("上传进行中,无法删除文件");
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'single') {
|
||||
setSingleFiles(prev => {
|
||||
const newFiles = prev.filter(f => f.id !== fileId);
|
||||
if (newFiles.length === 0) {
|
||||
setUploadType('none');
|
||||
singleUploadRef.current?.resetFileInput();
|
||||
}
|
||||
return newFiles;
|
||||
});
|
||||
} else {
|
||||
setMultipleFiles(prev => {
|
||||
const newFiles = prev.filter(f => f.id !== fileId);
|
||||
if (newFiles.length === 0) {
|
||||
setUploadType('none');
|
||||
multipleUploadRef.current?.resetFileInput();
|
||||
}
|
||||
return newFiles;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 清空文件列表
|
||||
const handleClearFiles = (type: 'single' | 'multiple') => {
|
||||
if (isUploading) {
|
||||
toastService.warning("上传进行中,无法清空文件");
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'single') {
|
||||
setSingleFiles([]);
|
||||
singleUploadRef.current?.resetFileInput();
|
||||
} else {
|
||||
setMultipleFiles([]);
|
||||
multipleUploadRef.current?.resetFileInput();
|
||||
}
|
||||
setUploadType('none');
|
||||
};
|
||||
|
||||
// 处理完成上传
|
||||
const handleCompleteUpload = async () => {
|
||||
const filesToUpload = uploadType === 'single' ? singleFiles : multipleFiles;
|
||||
|
||||
if (filesToUpload.length === 0) {
|
||||
toastService.error("请先选择要上传的文件");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
|
||||
try {
|
||||
console.log("开始批量上传文件:", filesToUpload.length, "个,案卷类型:", caseType, "typeId:", currentTypeId);
|
||||
|
||||
const result = await batchUploadCrossCheckingFiles(
|
||||
filesToUpload,
|
||||
currentTypeId,
|
||||
priority,
|
||||
documentNumber,
|
||||
remark,
|
||||
isTestDocument
|
||||
);
|
||||
|
||||
const { successes, failures } = result;
|
||||
|
||||
if (failures.length === 0) {
|
||||
// 全部成功
|
||||
toastService.success(`成功上传 ${successes.length} 个文件`);
|
||||
messageService.success(`文件上传完成!成功上传 ${successes.length} 个文件,现在可以进行下一步操作。`, {
|
||||
title: '上传成功',
|
||||
confirmText: '确定',
|
||||
onConfirm: () => {
|
||||
// 清空文件列表
|
||||
clearAllFiles();
|
||||
}
|
||||
});
|
||||
} else if (successes.length === 0) {
|
||||
// 全部失败
|
||||
toastService.error(`文件上传失败,共 ${failures.length} 个文件上传失败`);
|
||||
messageService.error(`所有文件上传失败。失败原因:${failures[0].error}`, {
|
||||
title: '上传失败',
|
||||
confirmText: '确定',
|
||||
});
|
||||
} else {
|
||||
// 部分成功
|
||||
toastService.warning(`部分文件上传成功:成功 ${successes.length} 个,失败 ${failures.length} 个`);
|
||||
messageService.warning(
|
||||
`部分文件上传完成:\n成功:${successes.length} 个文件\n失败:${failures.length} 个文件\n\n失败文件:\n${failures.map(f => `${f.file.name}: ${f.error}`).join('\n')}`,
|
||||
{
|
||||
title: '部分上传成功',
|
||||
confirmText: '确定',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("批量上传失败:", error);
|
||||
toastService.error("文件上传过程中发生错误");
|
||||
messageService.error(`文件上传失败:${error instanceof Error ? error.message : '未知错误'}`, {
|
||||
title: '上传失败',
|
||||
confirmText: '确定',
|
||||
});
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 检查是否可以完成
|
||||
const canComplete = (singleFiles.length > 0 || multipleFiles.length > 0) && !isUploading;
|
||||
const isSubmitting = navigation.state === "submitting";
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
{/* 步骤指示器 */}
|
||||
<div className="steps-indicator">
|
||||
{STEPS.map((step) => (
|
||||
<div
|
||||
key={step.id}
|
||||
className={`step-item ${step.id < currentStep ? 'completed' : ''}`}
|
||||
>
|
||||
<div className={`step-circle ${step.id <= currentStep ? 'active' : 'inactive'}`}>
|
||||
{step.id}
|
||||
</div>
|
||||
<div className="step-label">{step.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 案卷类型选择器 */}
|
||||
<div className="case-type-selector">
|
||||
<div className="case-type-options">
|
||||
<button
|
||||
type="button"
|
||||
className={`case-type-option ${caseType === CaseType.ADMINISTRATIVE_PENALTY ? 'active' : 'inactive'}`}
|
||||
onClick={() => handleCaseTypeChange(CaseType.ADMINISTRATIVE_PENALTY)}
|
||||
disabled={isUploading}
|
||||
>
|
||||
行政处罚
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`case-type-option ${caseType === CaseType.ADMINISTRATIVE_PERMIT ? 'active' : 'inactive'}`}
|
||||
onClick={() => handleCaseTypeChange(CaseType.ADMINISTRATIVE_PERMIT)}
|
||||
disabled={isUploading}
|
||||
>
|
||||
行政许可
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 文件上传区域 */}
|
||||
<Form method="post" encType="multipart/form-data">
|
||||
<input type="hidden" name="caseType" value={caseType} />
|
||||
<input type="hidden" name="uploadType" value={uploadType} />
|
||||
|
||||
<div className="upload-section">
|
||||
{/* 单案件导入 */}
|
||||
<div className="upload-item">
|
||||
<div className="upload-item-header">
|
||||
<i className="upload-item-icon ri-file-text-line"></i>
|
||||
<span>单案件导入</span>
|
||||
{uploadType === 'single' && singleFiles.length > 0 && (
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={() => handleClearFiles('single')}
|
||||
disabled={isUploading}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<UploadArea
|
||||
ref={singleUploadRef}
|
||||
onFilesSelected={handleSingleFilesSelected}
|
||||
className="custom-upload-area"
|
||||
accept=".pdf"
|
||||
multiple={true}
|
||||
icon="ri-file-upload-line"
|
||||
buttonText="选择文件"
|
||||
mainText="点击或拖拽文件到此区域上传"
|
||||
tipText={
|
||||
<div className="upload-tip-error">
|
||||
请上传案件相关PDF文件
|
||||
</div>
|
||||
}
|
||||
disabled={uploadType === 'multiple' || isUploading}
|
||||
/>
|
||||
|
||||
{/* 单案件文件列表 */}
|
||||
{singleFiles.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="text-sm font-medium text-gray-700">
|
||||
已选择 {singleFiles.length} 个文件:
|
||||
</div>
|
||||
<div className="max-h-32 overflow-y-auto space-y-1">
|
||||
{singleFiles.map((file) => (
|
||||
<div key={file.id} className="flex items-center justify-between bg-gray-50 p-2 rounded">
|
||||
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
||||
<i className="ri-file-pdf-line text-red-500"></i>
|
||||
<span className="text-sm truncate">{file.name}</span>
|
||||
<span className="text-xs text-gray-500">{formatFileSize(file.size)}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveFile(file.id, 'single')}
|
||||
className="text-red-500 hover:text-red-700 p-1"
|
||||
disabled={isUploading}
|
||||
>
|
||||
<i className="ri-close-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 多案件导入 */}
|
||||
<div className="upload-item">
|
||||
<div className="upload-item-header">
|
||||
<i className="upload-item-icon ri-file-list-line"></i>
|
||||
<span>多案件导入</span>
|
||||
{uploadType === 'multiple' && multipleFiles.length > 0 && (
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={() => handleClearFiles('multiple')}
|
||||
disabled={isUploading}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<UploadArea
|
||||
ref={multipleUploadRef}
|
||||
onFilesSelected={handleMultipleFilesSelected}
|
||||
className="custom-upload-area"
|
||||
accept=".zip,.rar,.7z,.tar"
|
||||
multiple={false}
|
||||
icon="ri-folder-zip-line"
|
||||
buttonText="选择文件"
|
||||
mainText="点击或拖拽文件到此区域上传"
|
||||
tipText={
|
||||
<div className="upload-tip-error">
|
||||
请上传多个案件作为压缩包zip、rar、7z、tar文件
|
||||
</div>
|
||||
}
|
||||
disabled={uploadType === 'single' || isUploading}
|
||||
/>
|
||||
|
||||
{/* 多案件文件列表 */}
|
||||
{multipleFiles.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="text-sm font-medium text-gray-700">
|
||||
已选择 {multipleFiles.length} 个压缩包:
|
||||
</div>
|
||||
<div className="max-h-32 overflow-y-auto space-y-1">
|
||||
{multipleFiles.map((file) => (
|
||||
<div key={file.id} className="flex items-center justify-between bg-gray-50 p-2 rounded">
|
||||
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
||||
<i className="ri-folder-zip-line text-orange-500"></i>
|
||||
<span className="text-sm truncate">{file.name}</span>
|
||||
<span className="text-xs text-gray-500">{formatFileSize(file.size)}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveFile(file.id, 'multiple')}
|
||||
className="text-red-500 hover:text-red-700 p-1"
|
||||
disabled={isUploading}
|
||||
>
|
||||
<i className="ri-close-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 完成按钮 */}
|
||||
<div className="complete-button-container">
|
||||
<button
|
||||
type="button"
|
||||
className="complete-button"
|
||||
disabled={!canComplete}
|
||||
onClick={handleCompleteUpload}
|
||||
>
|
||||
{isUploading || isSubmitting ? (
|
||||
<>
|
||||
<i className="ri-loader-4-line animate-spin mr-2"></i>
|
||||
上传中...
|
||||
</>
|
||||
) : (
|
||||
"完成"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{/* 文件选择状态提示 */}
|
||||
{!canComplete && !isUploading && (
|
||||
<div className="text-center mt-4 text-gray-500 text-sm">
|
||||
请至少选择一种导入方式的文件
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 上传进度提示 */}
|
||||
{isUploading && (
|
||||
<div className="text-center mt-4">
|
||||
<div className="bg-blue-50 p-4 rounded-md border border-blue-100">
|
||||
<div className="flex items-center justify-center text-blue-800 mb-2">
|
||||
<i className="ri-loader-4-line animate-spin text-xl mr-2"></i>
|
||||
<span className="font-medium">正在上传文件...</span>
|
||||
</div>
|
||||
<p className="text-sm text-blue-700">
|
||||
正在上传 {uploadType === 'single' ? singleFiles.length : multipleFiles.length} 个文件,请稍候
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import {type MetaFunction} from "@remix-run/node";
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import { type MetaFunction } from "@remix-run/node";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{title: "文档列表 - 中国烟草AI合同及卷宗审核系统"},
|
||||
{name: "documents", content: "文档列表,新增,修改"}
|
||||
{ title: "文档列表 - 中国烟草AI合同及卷宗审核系统" },
|
||||
{ name: "documents", content: "文档列表,新增,修改" }
|
||||
]
|
||||
}
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: "文档列表"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档列表路由布局
|
||||
|
||||
+1
-1
@@ -306,7 +306,7 @@ export default function Home() {
|
||||
</div>
|
||||
<div className="ml-1">
|
||||
<p className="text-sm font-medium mb-0">{userRole === 'developer' ? '系统管理员' : '普通用户'}</p>
|
||||
<p className="text-xs text-gray-500 mb-0">{userRole === 'developer' ? '超级管理员' : '标准权限'}</p>
|
||||
{/* <p className="text-xs text-gray-500 mb-0">{userRole === 'developer' ? '超级管理员' : '标准权限'}</p> */}
|
||||
</div>
|
||||
</div>
|
||||
{/* 登出操作 */}
|
||||
|
||||
+45
-4
@@ -1,9 +1,9 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSearchParams } from "@remix-run/react";
|
||||
import { type MetaFunction, type LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||
import { useSearchParams, Form } from "@remix-run/react";
|
||||
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs, redirect } from "@remix-run/node";
|
||||
import { OAuthClient } from "~/utils/oauth-client";
|
||||
import { OAUTH_CONFIG } from "~/config/api-config";
|
||||
import { getUserSession, getSession } from "~/root";
|
||||
import { getUserSession, getSession, createUserSession } from "~/root";
|
||||
import styles from "~/styles/pages/login.css?url";
|
||||
|
||||
export const links = () => [
|
||||
@@ -39,6 +39,23 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
});
|
||||
}
|
||||
|
||||
// 处理表单提交的action函数
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const intent = formData.get("intent");
|
||||
|
||||
if (intent === "temp_admin_login") {
|
||||
// 获取重定向目标
|
||||
const session = await getSession(request);
|
||||
const redirectTo = session.get("redirectTo") || "/";
|
||||
|
||||
// 创建管理员会话
|
||||
return createUserSession(true, 'developer', redirectTo);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function Login() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const error = searchParams.get("error");
|
||||
@@ -131,10 +148,34 @@ export default function Login() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 临时管理员登录区域 */}
|
||||
<div className="temp-login-section">
|
||||
<div className="section-divider">
|
||||
<span>或</span>
|
||||
</div>
|
||||
|
||||
<Form method="post" className="temp-login-form">
|
||||
<input type="hidden" name="intent" value="temp_admin_login" />
|
||||
<button
|
||||
type="submit"
|
||||
className="temp-admin-login-button"
|
||||
>
|
||||
<i className="ri-admin-line"></i>
|
||||
临时管理员登录
|
||||
</button>
|
||||
<div className="temp-login-tips">
|
||||
<p>
|
||||
<i className="ri-alert-line"></i>
|
||||
仅供开发测试使用,将以管理员身份登录
|
||||
</p>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="login-footer">
|
||||
<p>© 2024 中国烟草 版权所有</p>
|
||||
<p>© 2025 中国烟草 版权所有</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+660
-236
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,275 @@
|
||||
/* 交叉评查上传页面样式 */
|
||||
|
||||
/* 步骤指示器样式 */
|
||||
.steps-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 2rem 0 3rem 0;
|
||||
position: relative;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-item:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
left: 75px;
|
||||
width: calc(100% - 50px);
|
||||
height: 2px;
|
||||
background-color: #e5e7eb;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.step-item.completed:not(:last-child)::after {
|
||||
background-color: #059669;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.step-circle.active {
|
||||
background-color: #059669;
|
||||
color: white;
|
||||
box-shadow: 0 0 0 4px rgba(5, 150, 105, 0.2);
|
||||
}
|
||||
|
||||
.step-circle.inactive {
|
||||
background-color: #e5e7eb;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 0.9rem;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 案卷类型选择器样式 */
|
||||
.case-type-selector {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.case-type-options {
|
||||
display: flex;
|
||||
background-color: #f3f4f6;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.case-type-option {
|
||||
padding: 0.75rem 2rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.case-type-option.active {
|
||||
background-color: #059669;
|
||||
color: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.case-type-option.inactive {
|
||||
color: #6b7280;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.case-type-option:hover:not(.active) {
|
||||
background-color: #e5e7eb;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
/* 上传区域容器样式 */
|
||||
.upload-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 3rem;
|
||||
margin-bottom: 3rem;
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.upload-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.upload-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.upload-item-icon {
|
||||
font-size: 1.5rem;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* 覆盖UploadArea组件的默认样式 */
|
||||
.custom-upload-area.upload-area {
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
min-height: 200px;
|
||||
border: 2px dashed #d1d5db !important;
|
||||
border-radius: 12px !important;
|
||||
background-color: #fafafa !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
padding: 2rem 1.5rem !important;
|
||||
}
|
||||
|
||||
.custom-upload-area.upload-area:hover {
|
||||
border-color: #059669 !important;
|
||||
background-color: #f0fdf4 !important;
|
||||
}
|
||||
|
||||
.custom-upload-area.upload-area.dragover {
|
||||
border-color: #059669 !important;
|
||||
background-color: #f0fdf4 !important;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.custom-upload-area .upload-icon {
|
||||
font-size: 2.5rem !important;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.custom-upload-area .upload-text {
|
||||
color: #6b7280;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-tip-error {
|
||||
color: #dc2626;
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 完成按钮样式 */
|
||||
.complete-button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.complete-button {
|
||||
background-color: #059669;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.875rem 3rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.complete-button:hover {
|
||||
background-color: #047857;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.complete-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.complete-button:disabled {
|
||||
background-color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.upload-section {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.steps-indicator {
|
||||
margin: 1.5rem 0 2rem 0;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.case-type-option {
|
||||
padding: 0.625rem 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.custom-upload-area.upload-area {
|
||||
max-width: 100%;
|
||||
min-height: 180px;
|
||||
padding: 1.5rem 1rem !important;
|
||||
}
|
||||
|
||||
.complete-button {
|
||||
padding: 0.75rem 2.5rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.step-item:not(:last-child)::after {
|
||||
left: 60px;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
}
|
||||
@@ -207,4 +207,114 @@
|
||||
.login-footer {
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
/* 临时管理员登录样式 */
|
||||
.temp-login-section {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.section-divider::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background-color: #e5e7eb;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.section-divider span {
|
||||
background-color: white;
|
||||
color: #9ca3af;
|
||||
padding: 0 1rem;
|
||||
font-size: 0.875rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.temp-login-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.temp-admin-login-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.875rem 1.5rem;
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #f97316 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.temp-admin-login-button:hover {
|
||||
background: linear-gradient(135deg, #f97316 0%, #f59e0b 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.temp-admin-login-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.temp-admin-login-button i {
|
||||
font-size: 1.25rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.temp-login-tips {
|
||||
text-align: center;
|
||||
color: #f59e0b;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.temp-login-tips p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.temp-login-tips i {
|
||||
font-size: 0.95rem;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
/* 暗色主题下的临时登录样式 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.section-divider::before {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.section-divider span {
|
||||
background-color: #1f2937;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { CHAT_CONFIG } from '../config/chat';
|
||||
|
||||
/**
|
||||
* 测试Dify API连接
|
||||
* 这个文件可以用来调试和测试前端直接调用Dify API的功能
|
||||
*/
|
||||
export const testDifyConnection = async () => {
|
||||
console.log('🔧 Dify Configuration:', {
|
||||
apiUrl: CHAT_CONFIG.API_URL,
|
||||
appId: CHAT_CONFIG.APP_ID,
|
||||
hasApiKey: !!CHAT_CONFIG.API_KEY,
|
||||
apiKeyPreview: CHAT_CONFIG.API_KEY ? `${CHAT_CONFIG.API_KEY.substring(0, 10)}...` : 'No API Key',
|
||||
});
|
||||
|
||||
if (!CHAT_CONFIG.API_URL || !CHAT_CONFIG.APP_ID || !CHAT_CONFIG.API_KEY) {
|
||||
console.error('❌ Dify配置不完整,请检查环境变量');
|
||||
return false;
|
||||
}
|
||||
|
||||
const user = CHAT_CONFIG.generateUserId();
|
||||
console.log('👤 Generated User ID:', user);
|
||||
|
||||
try {
|
||||
// 测试获取应用参数
|
||||
console.log('📋 测试获取应用参数...');
|
||||
const response = await fetch(`${CHAT_CONFIG.API_URL}/parameters`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${CHAT_CONFIG.API_KEY}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('❌ 获取应用参数失败:', response.status, response.statusText);
|
||||
const errorText = await response.text();
|
||||
console.error('错误详情:', errorText);
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('✅ 成功获取应用参数:', data);
|
||||
|
||||
// 测试获取会话列表
|
||||
console.log('💬 测试获取会话列表...');
|
||||
const params = new URLSearchParams({
|
||||
user,
|
||||
limit: '10',
|
||||
first_id: '',
|
||||
});
|
||||
|
||||
const conversationsResponse = await fetch(`${CHAT_CONFIG.API_URL}/conversations?${params}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${CHAT_CONFIG.API_KEY}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!conversationsResponse.ok) {
|
||||
console.error('❌ 获取会话列表失败:', conversationsResponse.status, conversationsResponse.statusText);
|
||||
const errorText = await conversationsResponse.text();
|
||||
console.error('错误详情:', errorText);
|
||||
return false;
|
||||
}
|
||||
|
||||
const conversationsData = await conversationsResponse.json();
|
||||
console.log('✅ 成功获取会话列表:', conversationsData);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 测试Dify连接时发生错误:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 在浏览器控制台中可以调用这个函数进行测试
|
||||
* 使用方法:
|
||||
* 1. 打开浏览器控制台
|
||||
* 2. 输入: window.testDify()
|
||||
* 3. 查看输出结果
|
||||
*/
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).testDify = testDifyConnection;
|
||||
}
|
||||
Reference in New Issue
Block a user