修复系统概览数据不准确的查询。修复交叉评查意见列表的数量查询。优化全局消息提示的层级。优化提交意见进行局部更新。

This commit is contained in:
2025-07-25 09:49:36 +08:00
parent 3dab54d551
commit ccd5cdf71e
29 changed files with 2444 additions and 1035 deletions
+18 -6
View File
@@ -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;
}
}
}
+12 -9
View File
@@ -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',
+2 -2
View File
@@ -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);
+2 -1
View File
@@ -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
View File
@@ -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}`
}
+2 -2
View File
@@ -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();
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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
View File
@@ -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 };
+2 -2
View File
@@ -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>
);
}
}
+15 -30
View File
@@ -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>
+5 -9
View File
@@ -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">
ziprar7ztar文件
zip7z文件
</div>
}
disabled={uploadType === 'single' || isUploading}
+14 -14
View File
@@ -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>
);
}
}
+2 -2
View File
@@ -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>
);
}
}
+249
View File
@@ -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>
);
}
+1 -1
View File
@@ -9,7 +9,7 @@
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
z-index: 99999;
display: flex;
flex-direction: column;
gap: 10px;
+54
View File
@@ -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')
};
};