修复系统概览数据不准确的查询。修复交叉评查意见列表的数量查询。优化全局消息提示的层级。优化提交意见进行局部更新。
This commit is contained in:
+18
-6
@@ -20,6 +20,9 @@ export type QueryParams = Record<string, string | number | boolean | undefined>;
|
||||
// const API_BASE_URL = 'http://172.18.0.100:3000';
|
||||
// const API_BASE_URL = 'http://172.16.0.119:9000/admin';
|
||||
|
||||
// 调试:打印当前API_BASE_URL的值
|
||||
console.log('🔍 axios-client.ts - API_BASE_URL.value:', API_BASE_URL.value);
|
||||
|
||||
// 文档URL前缀 (从配置文件导入)
|
||||
// export const DOCUMENT_URL = 'http://nas.7bm.co:9000/docauditai/';
|
||||
export { DOCUMENT_URL };
|
||||
@@ -32,7 +35,7 @@ const DEFAULT_TIMEOUT = 30000; // 增加到30秒
|
||||
|
||||
// 创建 axios 实例
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
baseURL: API_BASE_URL.value === '/api' ? '' : API_BASE_URL.value, // 如果是相对路径,则不设置baseURL
|
||||
timeout: DEFAULT_TIMEOUT, // 增加超时时间
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -97,10 +100,17 @@ function buildUrl(endpoint: string, params?: QueryParams): string {
|
||||
if (endpoint.startsWith('http')) {
|
||||
fullUrl = endpoint;
|
||||
} else {
|
||||
// 确保API_BASE_URL格式正确
|
||||
const baseUrl = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;
|
||||
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||
fullUrl = `${baseUrl}${path}`;
|
||||
// 处理相对路径的情况
|
||||
if (API_BASE_URL.value === '/api') {
|
||||
// 如果是相对路径,直接使用endpoint
|
||||
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||
fullUrl = path;
|
||||
} else {
|
||||
// 确保API_BASE_URL格式正确
|
||||
const baseUrl = API_BASE_URL.value.endsWith('/') ? API_BASE_URL.value.slice(0, -1) : API_BASE_URL.value;
|
||||
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||
fullUrl = `${baseUrl}${path}`;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -189,6 +199,8 @@ export async function apiRequest<T>(
|
||||
if (USE_MOCK_DATA) {
|
||||
return getMockResponse<T>(endpoint);
|
||||
}
|
||||
|
||||
console.log('api-base-url-----------',API_BASE_URL.value)
|
||||
|
||||
try {
|
||||
// 构建 URL
|
||||
@@ -387,4 +399,4 @@ export async function downloadFile(path: string): Promise<Blob> {
|
||||
console.error('下载文件失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ function extractApiData<T>(responseData: unknown): T | null {
|
||||
export interface SubmitOpinionRequest {
|
||||
reviewPointResultId: string | number;
|
||||
documentId: string | number;
|
||||
evaluationPointId: number; // 必须是数字ID
|
||||
evaluationPointId: number | null; // 必须是数字ID
|
||||
auditOpinion: string;
|
||||
deductionScore: number;
|
||||
}
|
||||
@@ -60,6 +60,7 @@ export interface CrossCheckingOpinion {
|
||||
problem_message: string;
|
||||
proposer_id: number;
|
||||
created_at: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +133,7 @@ export async function submitCrossCheckingOpinion(
|
||||
evaluation_result_id: opinionData.reviewPointResultId
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals`, {
|
||||
const response = await fetch(`${API_BASE_URL.value}/admin/cross_review/proposals`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -188,7 +189,7 @@ export async function getCrossCheckingOpinions(
|
||||
// 如果没传userId,默认用1
|
||||
const realUserId = userId ?? 1;
|
||||
// 实际后端API调用,拼接API_BASE_URL
|
||||
const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals/document`, {
|
||||
const response = await fetch(`${API_BASE_URL.value}/admin/cross_review/proposals/document`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -225,6 +226,7 @@ export async function getCrossCheckingOpinions(
|
||||
problem_message?: string;
|
||||
proposer_id: number;
|
||||
created_at: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
// 适配后端返回结构,使用新字段
|
||||
@@ -241,7 +243,8 @@ export async function getCrossCheckingOpinions(
|
||||
can_vote: item.can_vote ?? false,
|
||||
problem_message: item.problem_message || '',
|
||||
proposer_id: item.proposer_id,
|
||||
created_at: item.created_at
|
||||
created_at: item.created_at,
|
||||
status: item.status
|
||||
})) : [];
|
||||
|
||||
return {
|
||||
@@ -300,24 +303,24 @@ export async function performOpinionAction(
|
||||
switch (actionData.action) {
|
||||
case 'agree':
|
||||
message = '已赞同该意见';
|
||||
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
|
||||
endpoint = `${API_BASE_URL.value}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
|
||||
requestBody = { vote_type: 'agree', voter_id: userInfo?.user_id };
|
||||
break;
|
||||
case 'disagree':
|
||||
message = '已反对该意见';
|
||||
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
|
||||
endpoint = `${API_BASE_URL.value}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
|
||||
requestBody = { vote_type: 'disagree', voter_id: userInfo?.user_id };
|
||||
break;
|
||||
case 'withdraw_vote':
|
||||
message = '已撤销投票';
|
||||
// 撤销投票的接口,根据实际API调整
|
||||
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
|
||||
endpoint = `${API_BASE_URL.value}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
|
||||
requestBody = { vote_type: 'cancel', voter_id: userInfo?.user_id };
|
||||
break;
|
||||
case 'withdraw_opinion':
|
||||
message = '已撤销意见';
|
||||
// 撤销意见的接口,根据实际API调整
|
||||
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}`;
|
||||
endpoint = `${API_BASE_URL.value}/admin/cross_review/proposals/${actionData.opinionId}`;
|
||||
requestBody = {};
|
||||
break;
|
||||
default:
|
||||
@@ -412,7 +415,7 @@ export async function checkProposalVotes(
|
||||
document_id: documentId
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals/document/check_pending_votes`, {
|
||||
const response = await fetch(`${API_BASE_URL.value}/admin/cross_review/proposals/document/check_pending_votes`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -389,7 +389,7 @@ export async function getCrossCheckingStats(userInfo?: { user_id?: number; [key:
|
||||
export async function getUserTaskDocuments(page: number = 1, pageSize: number = 10, jwtToken?: string): Promise<ApiResponse<UserTaskApiResponse>> {
|
||||
try {
|
||||
// 拼接绝对路径,去除多余斜杠
|
||||
const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;
|
||||
const base = API_BASE_URL.value.endsWith('/') ? API_BASE_URL.value.slice(0, -1) : API_BASE_URL.value;
|
||||
const url = `${base}/admin/cross_review/tasks/user_tasks`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
@@ -436,7 +436,7 @@ export async function getUserTaskDocuments(page: number = 1, pageSize: number =
|
||||
export async function getTaskDocuments(taskId: number, page: number = 1, pageSize: number = 10, jwtToken?: string): Promise<ApiResponse<TaskDocumentApiResponse>> {
|
||||
try {
|
||||
// 拼接绝对路径,去除多余斜杠
|
||||
const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;
|
||||
const base = API_BASE_URL.value.endsWith('/') ? API_BASE_URL.value.slice(0, -1) : API_BASE_URL.value;
|
||||
const url = `${base}/admin/cross_review/tasks/${taskId}/documents`;
|
||||
// console.log('最终请求URL:', url);
|
||||
|
||||
|
||||
@@ -324,7 +324,8 @@ export async function getReviewPoints(fileId: string, request: Request) {
|
||||
const scoringProposalsParams: PostgrestParams = {
|
||||
select: '*',
|
||||
filter: {
|
||||
'document_id': `eq.${fileId}`
|
||||
'document_id': `eq.${fileId}`,
|
||||
'deleted_at': `is.null`
|
||||
}
|
||||
};
|
||||
const scoringProposalsResponse = await postgrestGet('cross_scoring_proposals', scoringProposalsParams);
|
||||
|
||||
+12
-7
@@ -105,8 +105,8 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
|
||||
const startOfLastMonth = dayjs().subtract(1, 'month').startOf('month').format('YYYY-MM-DD HH:mm:ss');
|
||||
const endOfLastMonth = dayjs().subtract(1, 'month').endOf('month').format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
console.log('传入的 reviewType', reviewType);
|
||||
console.log('传入的 userId', userId);
|
||||
// console.log('传入的 reviewType', reviewType);
|
||||
// console.log('传入的 userId', userId);
|
||||
|
||||
// 基于 reviewType 构建类型过滤条件
|
||||
const typeFilter = buildTypeFilter(reviewType || null);
|
||||
@@ -181,7 +181,7 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
|
||||
select: 'count',
|
||||
filter: {
|
||||
and: `(audit_status.neq.0,audit_status.neq.2)`,
|
||||
updated_at: `gte.${startOfThisMonth}`,
|
||||
upload_time: `gte.${startOfThisMonth}`,
|
||||
is_test_document: `eq.false`,
|
||||
user_id: `eq.${userId}`
|
||||
}
|
||||
@@ -212,8 +212,8 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
|
||||
const lastMonthReviewedParams: PostgrestParams = {
|
||||
select: 'count',
|
||||
filter: {
|
||||
or: `(audit_status.eq.1,audit_status.eq.-1)`,
|
||||
and: `(updated_at.gte.${startOfLastMonth},updated_at.lte.${endOfLastMonth})`,
|
||||
// or: `(audit_status.eq.1,audit_status.eq.-1)`,
|
||||
and: `(upload_time.gte.${startOfLastMonth},upload_time.lte.${endOfLastMonth},audit_status.neq.0,audit_status.neq.2)`,
|
||||
is_test_document: `eq.false`,
|
||||
user_id: `eq.${userId}`
|
||||
}
|
||||
@@ -226,7 +226,7 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
|
||||
if (!lastMonthReviewedParams.filter) {
|
||||
lastMonthReviewedParams.filter = {};
|
||||
}
|
||||
lastMonthReviewedParams.filter.or = lastMonthReviewedParams.filter.or + ',' + typeFilter;
|
||||
lastMonthReviewedParams.filter.or = typeFilter;
|
||||
} else {
|
||||
const [field, op, value] = typeFilter.split('.');
|
||||
if (!lastMonthReviewedParams.filter) {
|
||||
@@ -243,6 +243,8 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
|
||||
);
|
||||
// 上月已审核文件数量
|
||||
const lastMonthReviewed = lastMonthReviewedCount[0]?.count || 0;
|
||||
// console.log('上月已审核文件查询参数', lastMonthReviewedParams);
|
||||
// console.log('上月已审核文件数量', lastMonthReviewed);
|
||||
|
||||
// 计算同比增长
|
||||
let reviewGrowthValue = 0;
|
||||
@@ -285,8 +287,11 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
|
||||
'获取本月审核通过数量失败',
|
||||
[]
|
||||
);
|
||||
// console.log('本月审核通过数量查询参数', thisMonthTotalParams);
|
||||
// 本月审核通过数量
|
||||
const thisMonthPassTotal = thisMonthTotalCount[0]?.count || 0;
|
||||
// console.log('本月审核通过数量', thisMonthPassTotal);
|
||||
// console.log('本月已审核文件数量', monthlyReviewedFiles);
|
||||
|
||||
// 本月审核通过率
|
||||
const monthlyPassRate = (thisMonthPassTotal > 0 && monthlyReviewedFiles > 0)
|
||||
@@ -298,7 +303,7 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
|
||||
select: 'count',
|
||||
filter: {
|
||||
audit_status: `eq.1`,
|
||||
and: `(updated_at.gte.${startOfLastMonth},updated_at.lte.${endOfLastMonth})`,
|
||||
and: `(upload_time.gte.${startOfLastMonth},upload_time.lte.${endOfLastMonth})`,
|
||||
is_test_document: `eq.false`,
|
||||
user_id: `eq.${userId}`
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export class TokenManager {
|
||||
private oauthClient: OAuthClient;
|
||||
|
||||
constructor() {
|
||||
this.oauthClient = new OAuthClient(OAUTH_CONFIG);
|
||||
this.oauthClient = new OAuthClient(OAUTH_CONFIG.value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,4 +151,4 @@ export class TokenManager {
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const tokenManager = new TokenManager();
|
||||
export const tokenManager = new TokenManager();
|
||||
@@ -57,7 +57,7 @@ export async function getOrganizationTree(includeUsers: boolean = true, jwtToken
|
||||
|
||||
if (jwtToken) {
|
||||
// 如果提供了JWT Token,则使用fetch并携带Authorization头
|
||||
const url = `${API_BASE_URL}/admin/users/organizations?include_users=${includeUsers}`;
|
||||
const url = `${API_BASE_URL.value}/admin/users/organizations?include_users=${includeUsers}`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${jwtToken}`,
|
||||
|
||||
@@ -175,6 +175,7 @@ interface ReviewPointsListProps {
|
||||
scoringProposals?: ScoringProposal[];
|
||||
jwtToken?: string; // 添加JWT token参数
|
||||
userInfo?: UserInfo; // 添加用户信息参数
|
||||
onOpinionSubmitted?: (newProposal: ScoringProposal) => void; // 新增:意见提交成功后的回调
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -433,12 +434,14 @@ export function ReviewPointsList({
|
||||
onReviewPointSelect,
|
||||
scoringProposals = [],
|
||||
jwtToken,
|
||||
userInfo
|
||||
userInfo,
|
||||
onOpinionSubmitted
|
||||
}: ReviewPointsListProps) {
|
||||
// 状态管理
|
||||
const [searchText, setSearchText] = useState(''); // 搜索文本
|
||||
const [statusFilter, setStatusFilter] = useState<string | null>(null); // 状态过滤
|
||||
const [evaluationResultIds, setEvaluationResultIds] = useState<number[]>([]); // 评分提案的evaluation_result_id
|
||||
const [localScoringProposals, setLocalScoringProposals] = useState<ScoringProposal[]>(scoringProposals); // 本地状态管理scoringProposals
|
||||
const fetcher = useFetcher();
|
||||
|
||||
// 归一化 reviewPoints,确保每个点都有 id 字段
|
||||
@@ -452,17 +455,21 @@ export function ReviewPointsList({
|
||||
setNormalizedReviewPoints(norm);
|
||||
}, [reviewPoints]);
|
||||
|
||||
// 在组件中使用scoringProposals(这里只是简单使用以避免linter警告)
|
||||
// 将来可以用于显示相关的评分提案信息
|
||||
// 同步外部scoringProposals到本地状态
|
||||
useEffect(() => {
|
||||
if (scoringProposals && scoringProposals.length > 0) {
|
||||
// console.log('收到评分提案数据:', scoringProposals.length, '个提案');
|
||||
setLocalScoringProposals(scoringProposals);
|
||||
}, [scoringProposals]);
|
||||
|
||||
// 在组件中使用localScoringProposals
|
||||
useEffect(() => {
|
||||
if (localScoringProposals && localScoringProposals.length > 0) {
|
||||
// console.log('收到评分提案数据:', localScoringProposals.length, '个提案');
|
||||
// 获取提案的evaluation_result_id
|
||||
const evaluationResultIds = scoringProposals.map(proposal => Number(proposal.evaluation_result_id));
|
||||
const evaluationResultIds = localScoringProposals.map(proposal => Number(proposal.evaluation_result_id));
|
||||
setEvaluationResultIds(evaluationResultIds);
|
||||
// console.log('提案的evaluation_result_id:', evaluationResultIds);
|
||||
}
|
||||
}, [scoringProposals]);
|
||||
}, [localScoringProposals]);
|
||||
|
||||
// 提出意见模态框相关状态
|
||||
const [isOpinionModalOpen, setIsOpinionModalOpen] = useState(false);
|
||||
@@ -618,14 +625,14 @@ export function ReviewPointsList({
|
||||
* 打开意见列表模态框
|
||||
*/
|
||||
const handleOpenOpinionListModal = (reviewPoint: ReviewPoint) => {
|
||||
console.log('查看reviewPoint', reviewPoint);
|
||||
if (scoringProposals.length === 0) {
|
||||
// console.log('查看reviewPoint', reviewPoint);
|
||||
if (localScoringProposals.length === 0) {
|
||||
toastService.warning('当前文件尚未有人提出过意见');
|
||||
return;
|
||||
}
|
||||
setSelectedReviewPoint(reviewPoint);
|
||||
setIsOpinionListModalOpen(true);
|
||||
console.log('打开意见列表模态框');
|
||||
// console.log('打开意见列表模态框');
|
||||
// 直接传递reviewPoint的documentId,避免依赖状态更新
|
||||
loadOpinionListData(1, 10, reviewPoint.documentId);
|
||||
};
|
||||
@@ -714,14 +721,14 @@ export function ReviewPointsList({
|
||||
}
|
||||
|
||||
// 新增:详细打印每个校验条件
|
||||
console.log('校验前 selectedReviewPoint:', selectedReviewPoint);
|
||||
console.log('校验前 opinionForm:', opinionForm);
|
||||
console.log('校验前 userInfo:', userInfo);
|
||||
console.log('documentId:', selectedReviewPoint.documentId, 'isNaN:', isNaN(Number(selectedReviewPoint.documentId)), 'typeof:', typeof selectedReviewPoint.documentId);
|
||||
console.log('pointId:', selectedReviewPoint.pointId, 'isNaN:', isNaN(Number(selectedReviewPoint.pointId)), 'typeof:', typeof selectedReviewPoint.pointId);
|
||||
console.log('deductionScore:', opinionForm.deductionScore, 'typeof:', typeof opinionForm.deductionScore, 'isNaN:', isNaN(Number(opinionForm.deductionScore)));
|
||||
console.log('auditOpinion:', opinionForm.auditOpinion, 'trim:', String(opinionForm.auditOpinion).trim(), 'typeof:', typeof opinionForm.auditOpinion);
|
||||
console.log('user_id:', userInfo?.user_id, 'typeof:', typeof userInfo?.user_id);
|
||||
// console.log('校验前 selectedReviewPoint:', selectedReviewPoint);
|
||||
// console.log('校验前 opinionForm:', opinionForm);
|
||||
// console.log('校验前 userInfo:', userInfo);
|
||||
// console.log('documentId:', selectedReviewPoint.documentId, 'isNaN:', isNaN(Number(selectedReviewPoint.documentId)), 'typeof:', typeof selectedReviewPoint.documentId);
|
||||
// console.log('pointId:', selectedReviewPoint.pointId, 'isNaN:', isNaN(Number(selectedReviewPoint.pointId)), 'typeof:', typeof selectedReviewPoint.pointId);
|
||||
// console.log('deductionScore:', opinionForm.deductionScore, 'typeof:', typeof opinionForm.deductionScore, 'isNaN:', isNaN(Number(opinionForm.deductionScore)));
|
||||
// console.log('auditOpinion:', opinionForm.auditOpinion, 'trim:', String(opinionForm.auditOpinion).trim(), 'typeof:', typeof opinionForm.auditOpinion);
|
||||
// console.log('user_id:', userInfo?.user_id, 'typeof:', typeof userInfo?.user_id);
|
||||
|
||||
// 更严谨的校验逻辑
|
||||
if (
|
||||
@@ -741,12 +748,12 @@ export function ReviewPointsList({
|
||||
}
|
||||
|
||||
// 打印所有关键数据
|
||||
console.log('selectedReviewPoint:', selectedReviewPoint);
|
||||
console.log('opinionForm:', opinionForm);
|
||||
console.log('userInfo:', userInfo);
|
||||
// console.log('selectedReviewPoint:', selectedReviewPoint);
|
||||
// console.log('opinionForm:', opinionForm);
|
||||
// console.log('userInfo:', userInfo);
|
||||
|
||||
// 组装后端要求的字段名和内容
|
||||
const data: Record<string, any> = {
|
||||
const data = {
|
||||
document_id: Number(selectedReviewPoint.documentId),
|
||||
evaluation_point_id: Number(selectedReviewPoint.pointId),
|
||||
proposed_score: Number(opinionForm.deductionScore),
|
||||
@@ -759,10 +766,10 @@ export function ReviewPointsList({
|
||||
data.evaluation_result_id = Number(selectedReviewPoint.evaluationPointId);
|
||||
}
|
||||
// 打印最终请求体
|
||||
console.log('最终请求体:', data);
|
||||
// console.log('最终请求体:', data);
|
||||
// 用原生 fetch + application/json 提交
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL.replace(/\/$/, '')}/admin/cross_review/proposals`, {
|
||||
const response = await fetch(`${API_BASE_URL.value.replace(/\/$/, '')}/admin/cross_review/proposals`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -773,6 +780,28 @@ export function ReviewPointsList({
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
toastService.success('意见提交成功');
|
||||
|
||||
// 创建新的提案对象
|
||||
const newProposal: ScoringProposal = {
|
||||
id: result.id || Date.now(), // 使用返回的ID或时间戳作为临时ID
|
||||
evaluation_result_id: data.evaluation_result_id,
|
||||
proposer_id: data.proposer_id as number,
|
||||
proposed_score: data.proposed_score,
|
||||
reason: data.reason,
|
||||
status: 'pending', // 默认状态
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
document_id: data.document_id
|
||||
};
|
||||
|
||||
// 更新本地状态
|
||||
setLocalScoringProposals(prev => [...prev, newProposal]);
|
||||
|
||||
// 调用父组件回调(如果提供)
|
||||
if (onOpinionSubmitted) {
|
||||
onOpinionSubmitted(newProposal);
|
||||
}
|
||||
|
||||
handleCloseOpinionModal();
|
||||
} else {
|
||||
toastService.error(result.detail || '提交意见失败');
|
||||
@@ -2487,7 +2516,7 @@ export function ReviewPointsList({
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-green-700 border border-transparent rounded-md text-sm font-medium text-white hover:bg-green-600 disabled:opacity-50"
|
||||
onClick={handleSubmitOpinion}
|
||||
onClick={() => handleSubmitOpinion()}
|
||||
disabled={isSubmittingOpinion}
|
||||
>
|
||||
{isSubmittingOpinion ? '提交中...' : '发起投票'}
|
||||
@@ -2652,7 +2681,7 @@ export function ReviewPointsList({
|
||||
{
|
||||
title: "投票人",
|
||||
key: "votes",
|
||||
width: "25%",
|
||||
width: "22%",
|
||||
align: "center" as const,
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => {
|
||||
// 投票类型配置
|
||||
@@ -2707,9 +2736,9 @@ export function ReviewPointsList({
|
||||
{
|
||||
title: "意见发起人",
|
||||
key: "proposer",
|
||||
width: "4%",
|
||||
width: "8%",
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex items-center justify-center text-left">
|
||||
<span
|
||||
className="px-1.5 py-0.5 rounded text-xs font-medium text-yellow-700 bg-yellow-100 border border-yellow-200 whitespace-nowrap overflow-hidden text-ellipsis max-w-[80px] transition-all hover:scale-[1.03] hover:shadow-sm"
|
||||
>
|
||||
@@ -2721,7 +2750,7 @@ export function ReviewPointsList({
|
||||
{
|
||||
title: "发起时间",
|
||||
key: "created_at",
|
||||
width: "18%",
|
||||
width: "12%",
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => (
|
||||
<div className="text-sm text-left">{record.created_at}</div>
|
||||
)
|
||||
@@ -2729,7 +2758,7 @@ export function ReviewPointsList({
|
||||
{
|
||||
title: "投票状态",
|
||||
key: "opinion_status",
|
||||
width: "10%",
|
||||
width: "12%",
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => {
|
||||
let label = '';
|
||||
let color = '';
|
||||
@@ -2754,7 +2783,7 @@ export function ReviewPointsList({
|
||||
{
|
||||
title: "操作",
|
||||
key: "operation",
|
||||
width: "18%",
|
||||
width: "auto",
|
||||
align: "center" as const,
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => {
|
||||
const isPerforming = (action: string) => performingAction === `${record.proposal_id}-${action}`;
|
||||
@@ -2869,7 +2898,7 @@ function OpinionActions({ record, isPerforming, handleOpinionAction, userInfo }:
|
||||
</>
|
||||
)}
|
||||
{/* 仅当can_vote为false时显示撤销投票按钮 */}
|
||||
{!record.can_vote && (
|
||||
{!record.can_vote && !isProposer && (
|
||||
<Button
|
||||
type="default"
|
||||
className="bg-yellow-600 hover:bg-yellow-700 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3"
|
||||
|
||||
@@ -202,7 +202,7 @@ export function Toast({
|
||||
aria-live="polite"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={{ zIndex: 99999, position: 'relative' }}
|
||||
style={{ zIndex: 999999, position: 'relative' }}
|
||||
>
|
||||
<div className="toast-content">
|
||||
<div className="toast-icon-wrapper">
|
||||
|
||||
+155
-49
@@ -32,14 +32,16 @@ interface ApiConfig {
|
||||
const configs: Record<string, ApiConfig> = {
|
||||
// 开发环境
|
||||
development: {
|
||||
// baseUrl: '/api', // 改为相对路径,让nginx处理
|
||||
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/',
|
||||
|
||||
documentUrl: 'http://172.16.0.55:8008/docauditai/',
|
||||
// documentUrl: '/api/docauditai/',
|
||||
|
||||
// uploadUrl: '/api/admin/documents', // 改为相对路径
|
||||
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',
|
||||
@@ -103,35 +105,50 @@ const getClientConfigs = (env: string): Record<string, Partial<ApiConfig>> => {
|
||||
// 开发环境 - 本地nginx代理配置
|
||||
return {
|
||||
'client-a': {
|
||||
baseUrl: 'http://localhost:8001',
|
||||
uploadUrl: 'http://localhost:8001/admin/documents',
|
||||
baseUrl: '/api', // 改为相对路径,让nginx处理
|
||||
uploadUrl: '/api/admin/documents', // 改为相对路径
|
||||
documentUrl: '/api/docauditai/',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://localhost:8001/callback',
|
||||
redirectUri: 'http://172.16.0.34:5174/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
},
|
||||
'client-b': {
|
||||
baseUrl: 'http://localhost:8002',
|
||||
uploadUrl: 'http://localhost:8002/admin/documents',
|
||||
baseUrl: '/api', // 改为相对路径,让nginx处理
|
||||
uploadUrl: '/api/admin/documents', // 改为相对路径
|
||||
documentUrl: '/api/docauditai/',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://localhost:8002/callback',
|
||||
redirectUri: 'http://172.16.0.34:5175/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
},
|
||||
'client-c': {
|
||||
baseUrl: 'http://localhost:8003',
|
||||
uploadUrl: 'http://localhost:8003/admin/documents',
|
||||
baseUrl: '/api', // 改为相对路径,让nginx处理
|
||||
uploadUrl: '/api/admin/documents', // 改为相对路径
|
||||
documentUrl: '/api/docauditai/',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://localhost:8003/callback',
|
||||
redirectUri: 'http://172.16.0.34:5176/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
},
|
||||
'client-d': {
|
||||
baseUrl: '/api', // 改为相对路径,让nginx处理
|
||||
uploadUrl: '/api/admin/documents', // 改为相对路径
|
||||
documentUrl: '/api/docauditai/',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://172.16.0.34:5177/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
}
|
||||
@@ -139,36 +156,51 @@ const getClientConfigs = (env: string): Record<string, Partial<ApiConfig>> => {
|
||||
} else {
|
||||
// 生产环境 - 服务器配置
|
||||
return {
|
||||
'client-a': {
|
||||
baseUrl: 'http://10.79.97.17:51701',
|
||||
uploadUrl: 'http://10.79.97.17:51701/admin/documents',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://10.79.97.17:51701/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
},
|
||||
'client-b': {
|
||||
baseUrl: 'http://10.79.97.17:51702',
|
||||
uploadUrl: 'http://10.79.97.17:51702/admin/documents',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://10.79.97.17:51702/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
},
|
||||
'client-c': {
|
||||
'provincial': {
|
||||
baseUrl: 'http://10.79.97.17:51704',
|
||||
uploadUrl: 'http://10.79.97.17:51704/admin/documents',
|
||||
documentUrl: 'http://10.76.244.156:9000/docauditai/',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://10.79.97.17:51704/callback',
|
||||
redirectUri: 'http://10.79.97.17/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
},
|
||||
'meizhou': {
|
||||
baseUrl: 'http://10.79.97.17:51705',
|
||||
uploadUrl: 'http://10.79.97.17:51705/admin/documents',
|
||||
documentUrl: 'http://10.76.244.156:9000/docauditai/',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://10.79.97.17/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
},
|
||||
'jieyang': {
|
||||
baseUrl: 'http://10.79.97.17:51706',
|
||||
uploadUrl: 'http://10.79.97.17:51706/admin/documents',
|
||||
documentUrl: 'http://10.76.244.156:9000/docauditai/',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://10.79.97.17/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
},
|
||||
'yunfu': {
|
||||
baseUrl: 'http://10.79.97.17:51707',
|
||||
uploadUrl: 'http://10.79.97.17:51707/admin/documents',
|
||||
documentUrl: 'http://10.76.244.156:9000/docauditai/',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://10.79.97.17/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
}
|
||||
@@ -182,8 +214,46 @@ const getCurrentEnvironment = (): string => {
|
||||
return process.env.NEXT_PUBLIC_API_ENV || process.env.NODE_ENV || 'development';
|
||||
};
|
||||
|
||||
// 获取客户端ID
|
||||
const getClientId = (): string => {
|
||||
// 获取客户端ID - 支持从请求头动态获取
|
||||
const getClientId = (request?: Request): string => {
|
||||
// SSR: 通过请求头的 host 判断
|
||||
if (request && typeof window === 'undefined') {
|
||||
// 1. 优先 X-Client-ID
|
||||
const clientIdFromHeader = request.headers.get('X-Client-ID');
|
||||
if (clientIdFromHeader) return clientIdFromHeader;
|
||||
|
||||
// 2. 通过 host 端口判断
|
||||
const host = request.headers.get('host'); // 例如 172.24.238.60:5177
|
||||
if (host) {
|
||||
const port = host.split(':')[1];
|
||||
const portToClient: Record<string, string> = {
|
||||
'5174': 'client-a',
|
||||
'5175': 'client-b',
|
||||
'5176': 'client-c',
|
||||
'5177': 'client-d'
|
||||
};
|
||||
if (port && portToClient[port]) {
|
||||
return portToClient[port];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 浏览器端
|
||||
if (typeof window !== 'undefined') {
|
||||
const port = window.location.port;
|
||||
const portToClient: Record<string, string> = {
|
||||
'5174': 'client-a',
|
||||
'5175': 'client-b',
|
||||
'5176': 'client-c',
|
||||
'5177': 'client-d'
|
||||
};
|
||||
if (port && portToClient[port]) {
|
||||
console.log(`🎯 浏览器端检测到客户端ID: ${portToClient[port]} (端口: ${port})`);
|
||||
return portToClient[port];
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到环境变量
|
||||
return process.env.CLIENT_ID || process.env.NEXT_PUBLIC_CLIENT_ID || 'main';
|
||||
};
|
||||
|
||||
@@ -204,9 +274,9 @@ const getConfigFromEnv = (defaultConfig: ApiConfig): ApiConfig => {
|
||||
};
|
||||
|
||||
// 获取当前配置 - 支持客户端特定配置
|
||||
const getCurrentConfig = (): ApiConfig => {
|
||||
const getCurrentConfig = (request?: Request): ApiConfig => {
|
||||
const env = getCurrentEnvironment();
|
||||
const clientId = getClientId();
|
||||
const clientId = getClientId(request);
|
||||
const defaultConfig = configs[env] || configs.development;
|
||||
|
||||
// 获取当前环境的客户端特定配置
|
||||
@@ -234,16 +304,52 @@ const getCurrentConfig = (): ApiConfig => {
|
||||
return finalConfig;
|
||||
};
|
||||
|
||||
// 导出当前环境的配置
|
||||
// 导出当前环境的配置(静态,用于兼容性)
|
||||
export const apiConfig = getCurrentConfig();
|
||||
|
||||
// 导出具体的配置项,方便使用
|
||||
export const {
|
||||
baseUrl: API_BASE_URL,
|
||||
documentUrl: DOCUMENT_URL,
|
||||
uploadUrl: UPLOAD_URL,
|
||||
oauth: OAUTH_CONFIG
|
||||
} = apiConfig;
|
||||
// 导出动态配置获取函数(支持从请求头获取客户端ID)
|
||||
export const getApiConfig = (request?: Request): ApiConfig => {
|
||||
return getCurrentConfig(request);
|
||||
};
|
||||
|
||||
// 导出具体的配置项,方便使用(现在是真正动态的)
|
||||
// 使用getter函数实现动态获取,避免ES模块中exports未定义的问题
|
||||
export const API_BASE_URL = {
|
||||
get value() {
|
||||
return getCurrentConfig().baseUrl;
|
||||
}
|
||||
};
|
||||
|
||||
export const DOCUMENT_URL = {
|
||||
get value() {
|
||||
return getCurrentConfig().documentUrl;
|
||||
}
|
||||
};
|
||||
|
||||
export const UPLOAD_URL = {
|
||||
get value() {
|
||||
return getCurrentConfig().uploadUrl;
|
||||
}
|
||||
};
|
||||
|
||||
export const OAUTH_CONFIG = {
|
||||
get value() {
|
||||
return getCurrentConfig().oauth;
|
||||
}
|
||||
};
|
||||
|
||||
// 动态获取配置项的函数
|
||||
export const getApiBaseUrl = (request?: Request): string => {
|
||||
return getApiConfig(request).baseUrl;
|
||||
};
|
||||
|
||||
export const getUploadUrl = (request?: Request): string => {
|
||||
return getApiConfig(request).uploadUrl;
|
||||
};
|
||||
|
||||
export const getOAuthConfig = (request?: Request) => {
|
||||
return getApiConfig(request).oauth;
|
||||
};
|
||||
|
||||
// 导出所有配置,供调试使用
|
||||
export { configs };
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
try {
|
||||
// 创建OAuth客户端
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG);
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG.value);
|
||||
|
||||
// 获取访问令牌
|
||||
const tokenResponse = await oauthClient.getAccessToken(code);
|
||||
@@ -130,4 +130,4 @@ export default function Callback() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -262,35 +262,8 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
return Response.json({ success: true, data: response.data });
|
||||
}
|
||||
|
||||
if (intent === "submitCrossCheckingOpinion") {
|
||||
const { submitCrossCheckingOpinion } = await import("~/api/cross-checking/cross-file-result");
|
||||
|
||||
const reviewPointResultId = formData.get("reviewPointResultId") as string;
|
||||
const documentId = formData.get("documentId") as string;
|
||||
const auditPoint = formData.get("auditPoint") as string;
|
||||
const foundIssue = formData.get("foundIssue") as string;
|
||||
const auditOpinion = formData.get("auditOpinion") as string;
|
||||
const deductionScore = parseFloat(formData.get("deductionScore") as string);
|
||||
|
||||
const opinionData = {
|
||||
reviewPointResultId,
|
||||
documentId,
|
||||
auditPoint,
|
||||
foundIssue,
|
||||
auditOpinion,
|
||||
deductionScore
|
||||
};
|
||||
|
||||
const response = await submitCrossCheckingOpinion(opinionData, frontendJWT);
|
||||
|
||||
if (response.error) {
|
||||
return Response.json({ success: false, error: response.error }, { status: response.status || 500 });
|
||||
}
|
||||
|
||||
return Response.json({ success: true, data: response.data });
|
||||
}
|
||||
|
||||
if (intent === "getCrossCheckingOpinions") {
|
||||
if (intent === "getCrossCheckingOpinions") {
|
||||
const { getCrossCheckingOpinions } = await import("~/api/cross-checking/cross-file-result");
|
||||
|
||||
const documentId = formData.get("documentId") as string;
|
||||
@@ -328,7 +301,18 @@ export default function CrossCheckingResult() {
|
||||
const [reviewData, setReviewData] = useState<ReviewData | null>(null);
|
||||
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null);
|
||||
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
|
||||
const [localScoringProposals, setLocalScoringProposals] = useState<ScoringProposal[]>(scoring_proposals || []); // 本地状态管理scoringProposals
|
||||
|
||||
// 同步外部scoring_proposals到本地状态
|
||||
useEffect(() => {
|
||||
setLocalScoringProposals(scoring_proposals || []);
|
||||
}, [scoring_proposals]);
|
||||
|
||||
// 处理意见提交成功的回调
|
||||
const handleOpinionSubmitted = (newProposal: ScoringProposal) => {
|
||||
setLocalScoringProposals(prev => [...prev, newProposal]);
|
||||
};
|
||||
|
||||
// loader 数据加载出错
|
||||
useEffect(()=>{
|
||||
loadingBarService.hide();
|
||||
@@ -555,7 +539,7 @@ export default function CrossCheckingResult() {
|
||||
|
||||
const responseData = checkRes.data as CheckProposalResponse;
|
||||
const pendingProposals = responseData?.data?.pending_proposals || [];
|
||||
console.log("pendingProposals", pendingProposals);
|
||||
// console.log("pendingProposals", pendingProposals);
|
||||
|
||||
// 3. 构建模态框消息
|
||||
let modalMessage: string = '';
|
||||
@@ -698,9 +682,10 @@ export default function CrossCheckingResult() {
|
||||
activeReviewPointResultId={activeReviewPointResultId}
|
||||
onReviewPointSelect={handleReviewPointSelect}
|
||||
onStatusChange={handleReviewPointStatusChange}
|
||||
scoringProposals={scoring_proposals as ScoringProposal[]}
|
||||
scoringProposals={localScoringProposals}
|
||||
jwtToken={jwtToken}
|
||||
userInfo={userInfo}
|
||||
onOpinionSubmitted={handleOpinionSubmitted}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -311,14 +311,10 @@ export default function CrossCheckingUpload() {
|
||||
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) {
|
||||
|
||||
if (isZip || is7z) {
|
||||
validFiles.push({
|
||||
id: generateFileId(),
|
||||
file,
|
||||
@@ -333,7 +329,7 @@ export default function CrossCheckingUpload() {
|
||||
});
|
||||
|
||||
if (hasInvalidFiles) {
|
||||
messageService.error('只能上传ZIP或RAR格式的压缩文件', {
|
||||
messageService.error('只能上传ZIP或7Z格式的压缩文件', {
|
||||
title: '文件类型错误',
|
||||
confirmText: '确定',
|
||||
});
|
||||
@@ -879,14 +875,14 @@ export default function CrossCheckingUpload() {
|
||||
ref={multipleUploadRef}
|
||||
onFilesSelected={handleMultipleFilesSelected}
|
||||
className="custom-upload-area"
|
||||
accept=".zip,.rar,.7z,.tar"
|
||||
accept=".zip,.7z"
|
||||
multiple={false}
|
||||
icon="ri-folder-zip-line"
|
||||
buttonText="选择文件"
|
||||
mainText="点击或拖拽文件到此区域上传"
|
||||
tipText={
|
||||
<div className="upload-tip-error">
|
||||
请上传多个案件作为压缩包zip、rar、7z、tar文件
|
||||
请上传多个案件作为压缩包zip、7z文件
|
||||
</div>
|
||||
}
|
||||
disabled={uploadType === 'single' || isUploading}
|
||||
|
||||
+14
-14
@@ -122,16 +122,16 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
// 打印session信息
|
||||
console.log("=== 测试用户登录 - Session信息 ===");
|
||||
console.log("保存到session的userInfo:", enhancedUserInfo);
|
||||
console.log("session数据结构:", {
|
||||
isAuthenticated: true,
|
||||
userRole: userRole,
|
||||
accessToken: "mock_access_token_for_test",
|
||||
refreshToken: "mock_refresh_token_for_test",
|
||||
tokenIssuedAt: Date.now(),
|
||||
tokenExpiresIn: mockTokenExpiresIn,
|
||||
frontendJWT: frontendJWT,
|
||||
userInfo: enhancedUserInfo
|
||||
});
|
||||
// console.log("session数据结构:", {
|
||||
// isAuthenticated: true,
|
||||
// userRole: userRole,
|
||||
// accessToken: "mock_access_token_for_test",
|
||||
// refreshToken: "mock_refresh_token_for_test",
|
||||
// tokenIssuedAt: Date.now(),
|
||||
// tokenExpiresIn: mockTokenExpiresIn,
|
||||
// frontendJWT: frontendJWT,
|
||||
// userInfo: enhancedUserInfo
|
||||
// });
|
||||
|
||||
const cookie = await sessionStorage.commitSession(session);
|
||||
|
||||
@@ -184,7 +184,7 @@ export default function Login() {
|
||||
const handleOAuthLogin = () => {
|
||||
try {
|
||||
// 创建OAuth客户端
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG);
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG.value);
|
||||
|
||||
// 生成状态值
|
||||
const state = oauthClient.generateState();
|
||||
@@ -205,8 +205,8 @@ export default function Login() {
|
||||
|
||||
useEffect(() => {
|
||||
// 检查OAuth配置是否完整
|
||||
if (!OAUTH_CONFIG.serverUrl || !OAUTH_CONFIG.clientId || !OAUTH_CONFIG.clientSecret) {
|
||||
console.error("OAuth2.0配置不完整:", OAUTH_CONFIG);
|
||||
if (!OAUTH_CONFIG.value.serverUrl || !OAUTH_CONFIG.value.clientId || !OAUTH_CONFIG.value.clientSecret) {
|
||||
console.error("OAuth2.0配置不完整:", OAUTH_CONFIG.value);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -280,4 +280,4 @@ export default function Login() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
if (accessToken) {
|
||||
try {
|
||||
// 创建OAuth客户端
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG);
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG.value);
|
||||
|
||||
// 构建登出后重定向URL
|
||||
const url = new URL(request.url);
|
||||
@@ -48,4 +48,4 @@ export default function Logout() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* 客户端配置测试页面
|
||||
* 用于验证Nginx代理和客户端ID检测是否正常工作
|
||||
*/
|
||||
import { json, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import { getApiConfig } from "~/config/api-config";
|
||||
import { detectClientFromRequest, getRequestDebugInfo } from "~/utils/client-detection";
|
||||
|
||||
/**
|
||||
* 服务器端loader函数 - 获取配置和调试信息
|
||||
*/
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 获取客户端配置
|
||||
const config = getApiConfig(request);
|
||||
|
||||
// 获取客户端检测信息
|
||||
const detectedClientId = detectClientFromRequest(request);
|
||||
|
||||
// 获取调试信息
|
||||
const debugInfo = getRequestDebugInfo(request);
|
||||
|
||||
// 获取当前URL信息
|
||||
const url = new URL(request.url);
|
||||
|
||||
return json({
|
||||
config,
|
||||
detectedClientId,
|
||||
debugInfo,
|
||||
serverInfo: {
|
||||
url: url.href,
|
||||
host: url.host,
|
||||
port: url.port,
|
||||
pathname: url.pathname
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端配置测试页面组件
|
||||
*/
|
||||
export default function ClientConfigTest() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
// 浏览器端检测客户端ID
|
||||
const browserClientId = typeof window !== 'undefined' ? (() => {
|
||||
const port = window.location.port;
|
||||
const portToClient: Record<string, string> = {
|
||||
'5174': 'client-a',
|
||||
'5175': 'client-b',
|
||||
'5176': 'client-c',
|
||||
'5177': 'client-d'
|
||||
};
|
||||
return port && portToClient[port] ? portToClient[port] : 'unknown';
|
||||
})() : 'server-side';
|
||||
|
||||
const browserPort = typeof window !== 'undefined' ? window.location.port : 'server-side';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-6">
|
||||
🧪 客户端配置测试页面
|
||||
</h1>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<h2 className="text-lg font-semibold text-blue-900 mb-3">📍 访问信息</h2>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div><strong>当前URL:</strong> {data.serverInfo.url}</div>
|
||||
<div><strong>主机:</strong> {data.serverInfo.host}</div>
|
||||
<div><strong>端口:</strong> {data.serverInfo.port || '默认端口'}</div>
|
||||
<div><strong>路径:</strong> {data.serverInfo.pathname}</div>
|
||||
<div><strong>浏览器端口:</strong> {browserPort}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<h2 className="text-lg font-semibold text-green-900 mb-3">🎯 客户端检测</h2>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div><strong>服务器端检测:</strong>
|
||||
<span className={`ml-2 px-2 py-1 rounded text-xs ${
|
||||
data.detectedClientId !== 'main' ? 'bg-green-200 text-green-800' : 'bg-yellow-200 text-yellow-800'
|
||||
}`}>
|
||||
{data.detectedClientId}
|
||||
</span>
|
||||
</div>
|
||||
<div><strong>浏览器端检测:</strong>
|
||||
<span className={`ml-2 px-2 py-1 rounded text-xs ${
|
||||
browserClientId !== 'unknown' ? 'bg-green-200 text-green-800' : 'bg-yellow-200 text-yellow-800'
|
||||
}`}>
|
||||
{browserClientId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nginx请求头信息 */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">🔍 Nginx请求头信息</h2>
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<strong>X-Client-ID:</strong>
|
||||
<span className={`ml-2 px-2 py-1 rounded text-xs ${
|
||||
data.debugInfo.clientId ? 'bg-green-200 text-green-800' : 'bg-red-200 text-red-800'
|
||||
}`}>
|
||||
{data.debugInfo.clientId || '未检测到'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>X-Original-Port:</strong>
|
||||
<span className={`ml-2 px-2 py-1 rounded text-xs ${
|
||||
data.debugInfo.originalPort ? 'bg-green-200 text-green-800' : 'bg-red-200 text-red-800'
|
||||
}`}>
|
||||
{data.debugInfo.originalPort || '未检测到'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>X-Forwarded-Port:</strong>
|
||||
<span className="ml-2 px-2 py-1 rounded text-xs bg-gray-200 text-gray-800">
|
||||
{data.debugInfo.forwardedPort || '未设置'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>X-Real-IP:</strong>
|
||||
<span className="ml-2 px-2 py-1 rounded text-xs bg-gray-200 text-gray-800">
|
||||
{data.debugInfo.realIp || '未设置'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 当前配置信息 */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">⚙️ 当前API配置</h2>
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div><strong>Base URL:</strong> {data.config.baseUrl}</div>
|
||||
<div><strong>Upload URL:</strong> {data.config.uploadUrl}</div>
|
||||
<div><strong>Document URL:</strong> {data.config.documentUrl}</div>
|
||||
<div><strong>OAuth Server:</strong> {data.config.oauth.serverUrl}</div>
|
||||
<div><strong>OAuth Redirect:</strong> {data.config.oauth.redirectUri}</div>
|
||||
<div><strong>OAuth Client ID:</strong> {data.config.oauth.clientId.substring(0, 20)}...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 状态检查 */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">✅ 状态检查</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
data.debugInfo.clientId ? 'bg-green-500' : 'bg-red-500'
|
||||
}`}></div>
|
||||
<span className={data.debugInfo.clientId ? 'text-green-700' : 'text-red-700'}>
|
||||
{data.debugInfo.clientId ? '✅ Nginx X-Client-ID 传递正常' : '❌ Nginx X-Client-ID 未传递'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
data.detectedClientId !== 'main' ? 'bg-green-500' : 'bg-yellow-500'
|
||||
}`}></div>
|
||||
<span className={data.detectedClientId !== 'main' ? 'text-green-700' : 'text-yellow-700'}>
|
||||
{data.detectedClientId !== 'main' ? '✅ 服务器端客户端检测成功' : '⚠️ 服务器端使用默认配置'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
browserClientId !== 'unknown' ? 'bg-green-500' : 'bg-yellow-500'
|
||||
}`}></div>
|
||||
<span className={browserClientId !== 'unknown' ? 'text-green-700' : 'text-yellow-700'}>
|
||||
{browserClientId !== 'unknown' ? '✅ 浏览器端客户端检测成功' : '⚠️ 浏览器端使用默认配置'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
data.config.baseUrl.includes(data.detectedClientId.replace('client-', '517')) ? 'bg-green-500' : 'bg-yellow-500'
|
||||
}`}></div>
|
||||
<span className={data.config.baseUrl.includes(data.detectedClientId.replace('client-', '517')) ? 'text-green-700' : 'text-yellow-700'}>
|
||||
{data.config.baseUrl.includes(data.detectedClientId.replace('client-', '517')) ? '✅ 配置匹配正确' : '⚠️ 配置可能不匹配'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 调试信息 */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">🔧 完整调试信息</h2>
|
||||
<details className="bg-gray-50 p-4 rounded-lg">
|
||||
<summary className="cursor-pointer text-sm font-medium text-gray-700 mb-2">
|
||||
点击查看详细信息
|
||||
</summary>
|
||||
<pre className="text-xs bg-white p-3 rounded border overflow-auto">
|
||||
{JSON.stringify({
|
||||
serverData: data,
|
||||
browserInfo: {
|
||||
clientId: browserClientId,
|
||||
port: browserPort,
|
||||
userAgent: typeof window !== 'undefined' ? navigator.userAgent : 'server-side'
|
||||
}
|
||||
}, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{/* 测试链接 */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">🔗 其他客户端测试链接</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{[
|
||||
{ port: '5174', client: 'client-a', name: '客户端A' },
|
||||
{ port: '5175', client: 'client-b', name: '客户端B' },
|
||||
{ port: '5176', client: 'client-c', name: '客户端C' },
|
||||
{ port: '5177', client: 'client-d', name: '客户端D' }
|
||||
].map(({ port, client, name }) => (
|
||||
<a
|
||||
key={port}
|
||||
href={`http://localhost:${port}/test/client-config`}
|
||||
className={`block p-3 rounded-lg text-center text-sm font-medium transition-colors ${
|
||||
browserPort === port
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-blue-100 text-blue-700 hover:bg-blue-200'
|
||||
}`}
|
||||
>
|
||||
{name}<br/>
|
||||
<span className="text-xs opacity-75">:{port}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-xs text-gray-500">
|
||||
最后更新: {data.timestamp}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9999;
|
||||
z-index: 99999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 客户端检测工具函数
|
||||
* 用于在服务器端从请求头中获取客户端信息
|
||||
*/
|
||||
|
||||
/**
|
||||
* 从请求头中检测客户端ID
|
||||
* @param request - Remix Request对象
|
||||
* @returns 客户端ID字符串
|
||||
*/
|
||||
export const detectClientFromRequest = (request: Request): string => {
|
||||
// 从Nginx传递的头部获取客户端ID
|
||||
const clientId = request.headers.get('X-Client-ID');
|
||||
const originalPort = request.headers.get('X-Original-Port');
|
||||
|
||||
if (clientId) {
|
||||
console.log(`🎯 检测到客户端ID: ${clientId} (端口: ${originalPort})`);
|
||||
return clientId;
|
||||
}
|
||||
|
||||
// 根据端口映射客户端ID(备用方案)
|
||||
const portToClient: Record<string, string> = {
|
||||
'5174': 'client-a',
|
||||
'5175': 'client-b',
|
||||
'5176': 'client-c',
|
||||
'5177': 'client-d'
|
||||
};
|
||||
|
||||
if (originalPort && portToClient[originalPort]) {
|
||||
console.log(`🎯 通过端口映射检测到客户端: ${portToClient[originalPort]} (端口: ${originalPort})`);
|
||||
return portToClient[originalPort];
|
||||
}
|
||||
|
||||
console.log('⚠️ 未能检测到客户端ID,使用默认值: main');
|
||||
return 'main';
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取请求的调试信息
|
||||
* @param request - Remix Request对象
|
||||
* @returns 调试信息对象
|
||||
*/
|
||||
export const getRequestDebugInfo = (request: Request) => {
|
||||
return {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
clientId: request.headers.get('X-Client-ID'),
|
||||
originalPort: request.headers.get('X-Original-Port'),
|
||||
forwardedPort: request.headers.get('X-Forwarded-Port'),
|
||||
realIp: request.headers.get('X-Real-IP'),
|
||||
forwardedFor: request.headers.get('X-Forwarded-For'),
|
||||
userAgent: request.headers.get('User-Agent')
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user