feat(frontend): integrate GraphRAG scored evaluation results
- Add getUnifiedEvaluationResults API function - Extend ReviewPointsListProps with flowType, scoredResults, scoredSummary - Add ScoredResultCard rendering for graphrag flow_type - Modify reviews.tsx loader to call unified API - Add scored evaluation component imports Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,53 @@ interface StatsData {
|
|||||||
score: number;
|
score: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphRAG Scored 评查结果类型
|
||||||
|
interface FieldScore {
|
||||||
|
field_path: string;
|
||||||
|
evaluation_as: string;
|
||||||
|
weight: number;
|
||||||
|
scored: number;
|
||||||
|
max_score: number;
|
||||||
|
status: string; // 'filled' | 'placeholder'
|
||||||
|
value: string;
|
||||||
|
page?: string;
|
||||||
|
ai_feedback?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScoredEvaluationResult {
|
||||||
|
evaluation_point_id: number;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
passed: boolean;
|
||||||
|
machine_score: number;
|
||||||
|
score: number;
|
||||||
|
percentage: number;
|
||||||
|
total_score: number;
|
||||||
|
total_weight: number;
|
||||||
|
pass_threshold: number;
|
||||||
|
result_type: 'scored';
|
||||||
|
field_results: FieldScore[];
|
||||||
|
missing_fields?: string[];
|
||||||
|
ai_suggestion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EvaluationSummary {
|
||||||
|
total_points: number;
|
||||||
|
passed_count: number;
|
||||||
|
failed_count: number;
|
||||||
|
total_score: number;
|
||||||
|
total_full_score: number;
|
||||||
|
average_percentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UnifiedEvaluationResponse {
|
||||||
|
document_id: number;
|
||||||
|
flow_type: 'graphrag' | 'legacy';
|
||||||
|
results: ScoredEvaluationResult[];
|
||||||
|
summary: EvaluationSummary;
|
||||||
|
evaluated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
// 在文件顶部添加的类型定义,在interface区块前添加
|
// 在文件顶部添加的类型定义,在interface区块前添加
|
||||||
interface OcrDataResult {
|
interface OcrDataResult {
|
||||||
pages?: number[];
|
pages?: number[];
|
||||||
@@ -1126,3 +1173,49 @@ export async function getReviewPoints_fromApi(fileId: string, request: Request)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取统一评查结果(GraphRAG Scored 模式)
|
||||||
|
*
|
||||||
|
* @param fileId 文档ID
|
||||||
|
* @param request Remix请求对象
|
||||||
|
* @returns 统一评查结果
|
||||||
|
*/
|
||||||
|
export async function getUnifiedEvaluationResults(fileId: string, request: Request) {
|
||||||
|
try {
|
||||||
|
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||||
|
if (!userInfo?.user_id) {
|
||||||
|
return { error: '用户身份验证失败', status: 401 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await apiRequest<UnifiedEvaluationResponse>(
|
||||||
|
`/api/v2/evaluation/results-unified/${fileId}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${frontendJWT}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
console.error('❌ [getUnifiedEvaluationResults] API调用失败:', response.error);
|
||||||
|
return { error: response.error, status: response.status || 500 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('❌ [getUnifiedEvaluationResults] API响应数据为空');
|
||||||
|
return { error: 'API响应数据为空', status: 500 };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [getUnifiedEvaluationResults] 调用失败:', error);
|
||||||
|
return {
|
||||||
|
error: error instanceof Error ? error.message : '获取评查结果失败',
|
||||||
|
status: 500
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface FieldResultItem {
|
||||||
|
field_path: string;
|
||||||
|
evaluation_as: string;
|
||||||
|
weight: number;
|
||||||
|
scored: number;
|
||||||
|
status: string; // 'filled' | 'placeholder'
|
||||||
|
value: string;
|
||||||
|
page?: string;
|
||||||
|
ai_feedback?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FieldResultListProps {
|
||||||
|
items: FieldResultItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FieldResultList({ items }: FieldResultListProps) {
|
||||||
|
return (
|
||||||
|
<div className="field-result-list">
|
||||||
|
{items.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.field_path}
|
||||||
|
className={`field-result-item field-result-item--${item.status}`}
|
||||||
|
>
|
||||||
|
<div className="field-result-item__header">
|
||||||
|
<span className="field-result-item__name">
|
||||||
|
{item.status === 'filled' ? '✅' : '⚠️'} {item.evaluation_as}
|
||||||
|
</span>
|
||||||
|
<span className="field-result-item__score">
|
||||||
|
{item.scored}/{item.weight}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="field-result-item__value">
|
||||||
|
{item.value}
|
||||||
|
</div>
|
||||||
|
{item.ai_feedback && (
|
||||||
|
<div className="field-result-item__feedback">
|
||||||
|
💬 {item.ai_feedback}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface ScoreBarProps {
|
||||||
|
score: number; // Actual score (e.g., 3 for 3/5)
|
||||||
|
fullScore: number; // Max score (e.g., 5)
|
||||||
|
percentage: number; // 0.0-1.0
|
||||||
|
passed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScoreBar({ score, fullScore, percentage, passed }: ScoreBarProps) {
|
||||||
|
const pct = Math.round(percentage * 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="score-bar" data-passed={passed}>
|
||||||
|
<div className="score-bar__header">
|
||||||
|
<span className="score-bar__percentage">{pct}%</span>
|
||||||
|
<span className={`score-bar__status ${passed ? 'passed' : 'failed'}`}>
|
||||||
|
{passed ? '✅ 通过' : '❌ 未通过'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="score-bar__track">
|
||||||
|
<div
|
||||||
|
className={`score-bar__fill ${passed ? 'passed' : 'failed'}`}
|
||||||
|
style={{ width: `${pct}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="score-bar__footer">
|
||||||
|
得分 {score.toFixed(1)} / 满分 {fullScore.toFixed(1)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ScoreBar } from './ScoreBar';
|
||||||
|
import { FieldResultList } from './FieldResultList';
|
||||||
|
|
||||||
|
interface ScoredResultCardProps {
|
||||||
|
result: {
|
||||||
|
evaluation_point_id: number;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
passed: boolean;
|
||||||
|
machine_score: number; // e.g., 3
|
||||||
|
score: number; // e.g., 5 (full score from evaluation_points)
|
||||||
|
percentage: number;
|
||||||
|
total_score: number; // e.g., 60
|
||||||
|
total_weight: number; // e.g., 100
|
||||||
|
field_results: Array<{
|
||||||
|
field_path: string;
|
||||||
|
evaluation_as: string;
|
||||||
|
weight: number;
|
||||||
|
scored: number;
|
||||||
|
status: string;
|
||||||
|
value: string;
|
||||||
|
page?: string;
|
||||||
|
ai_feedback?: string;
|
||||||
|
}>;
|
||||||
|
missing_fields?: string[];
|
||||||
|
ai_suggestion?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScoredResultCard({ result }: ScoredResultCardProps) {
|
||||||
|
return (
|
||||||
|
<div className="scored-result-card" data-passed={result.passed}>
|
||||||
|
<div className="scored-result-card__header">
|
||||||
|
<span className="scored-result-card__code">{result.code}</span>
|
||||||
|
<span className="scored-result-card__name">{result.name}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScoreBar
|
||||||
|
score={result.machine_score}
|
||||||
|
fullScore={result.score}
|
||||||
|
percentage={result.percentage}
|
||||||
|
passed={result.passed}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FieldResultList items={result.field_results} />
|
||||||
|
|
||||||
|
{result.ai_suggestion && (
|
||||||
|
<div className="scored-result-card__suggestion">
|
||||||
|
<strong>💡 建议:</strong>
|
||||||
|
<span>{result.ai_suggestion}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { ScoreBar } from './ScoreBar';
|
||||||
|
export { FieldResultList } from './FieldResultList';
|
||||||
|
export { ScoredResultCard } from './ScoredResultCard';
|
||||||
@@ -24,6 +24,7 @@ import { Tooltip } from '../ui/Tooltip';
|
|||||||
import { CorporateInfoModal } from '../corporate-information';
|
import { CorporateInfoModal } from '../corporate-information';
|
||||||
import type { BusinessInfoResult, DishonestyResult } from '../corporate-information';
|
import type { BusinessInfoResult, DishonestyResult } from '../corporate-information';
|
||||||
import { queryCompanyInfo } from '~/api/corporate-information/qichacha';
|
import { queryCompanyInfo } from '~/api/corporate-information/qichacha';
|
||||||
|
import { ScoredResultCard } from '~/components/evaluation';
|
||||||
// import '../../styles/components/TooltipStyles.css';
|
// import '../../styles/components/TooltipStyles.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -145,6 +146,45 @@ interface Statistics {
|
|||||||
score: number;
|
score: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphRAG Scored 评查结果类型
|
||||||
|
interface FieldScore {
|
||||||
|
field_path: string;
|
||||||
|
evaluation_as: string;
|
||||||
|
weight: number;
|
||||||
|
scored: number;
|
||||||
|
max_score: number;
|
||||||
|
status: string;
|
||||||
|
value: string;
|
||||||
|
page?: string;
|
||||||
|
ai_feedback?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScoredEvaluationResult {
|
||||||
|
evaluation_point_id: number;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
passed: boolean;
|
||||||
|
machine_score: number;
|
||||||
|
score: number;
|
||||||
|
percentage: number;
|
||||||
|
total_score: number;
|
||||||
|
total_weight: number;
|
||||||
|
pass_threshold: number;
|
||||||
|
result_type: 'scored';
|
||||||
|
field_results: FieldScore[];
|
||||||
|
missing_fields?: string[];
|
||||||
|
ai_suggestion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EvaluationSummary {
|
||||||
|
total_points: number;
|
||||||
|
passed_count: number;
|
||||||
|
failed_count: number;
|
||||||
|
total_score: number;
|
||||||
|
total_full_score: number;
|
||||||
|
average_percentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface ReviewPointsListProps {
|
interface ReviewPointsListProps {
|
||||||
reviewPoints: ReviewPoint[];
|
reviewPoints: ReviewPoint[];
|
||||||
statistics: Statistics;
|
statistics: Statistics;
|
||||||
@@ -153,6 +193,10 @@ interface ReviewPointsListProps {
|
|||||||
onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
|
onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
|
||||||
fileFormat?: string; // 文档格式类型(PDF、DOCX等)
|
fileFormat?: string; // 文档格式类型(PDF、DOCX等)
|
||||||
onAiSuggestionReplace?: (searchText: string, replaceText: string, pageNumber: number) => void; // AI建议替换回调
|
onAiSuggestionReplace?: (searchText: string, replaceText: string, pageNumber: number) => void; // AI建议替换回调
|
||||||
|
// GraphRAG Scored 模式支持
|
||||||
|
flowType?: 'graphrag' | 'legacy';
|
||||||
|
scoredResults?: ScoredEvaluationResult[];
|
||||||
|
scoredSummary?: EvaluationSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -411,7 +455,10 @@ export function ReviewPointsList({
|
|||||||
onReviewPointSelect,
|
onReviewPointSelect,
|
||||||
onStatusChange,
|
onStatusChange,
|
||||||
fileFormat,
|
fileFormat,
|
||||||
onAiSuggestionReplace
|
onAiSuggestionReplace,
|
||||||
|
flowType,
|
||||||
|
scoredResults,
|
||||||
|
scoredSummary
|
||||||
}: ReviewPointsListProps) {
|
}: ReviewPointsListProps) {
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const [editingReviewPoint, setEditingReviewPoint] = useState<string | null>(null); // 当前正在编辑的评查点ID
|
const [editingReviewPoint, setEditingReviewPoint] = useState<string | null>(null); // 当前正在编辑的评查点ID
|
||||||
@@ -2475,7 +2522,12 @@ export function ReviewPointsList({
|
|||||||
|
|
||||||
{/* 评查点列表 */}
|
{/* 评查点列表 */}
|
||||||
<div className="review-points-list">
|
<div className="review-points-list">
|
||||||
{filteredReviewPoints.length > 0 ? (
|
{/* GraphRAG Scored 模式渲染 */}
|
||||||
|
{flowType === 'graphrag' && scoredResults && scoredResults.length > 0 ? (
|
||||||
|
scoredResults.map(result => (
|
||||||
|
<ScoredResultCard key={result.evaluation_point_id} result={result} />
|
||||||
|
))
|
||||||
|
) : filteredReviewPoints.length > 0 ? (
|
||||||
filteredReviewPoints.map(reviewPoint => (
|
filteredReviewPoints.map(reviewPoint => (
|
||||||
<div
|
<div
|
||||||
key={reviewPoint.id}
|
key={reviewPoint.id}
|
||||||
|
|||||||
+60
-10
@@ -30,7 +30,7 @@ import { useState, useEffect, useRef, useCallback } from "react";
|
|||||||
import { useNavigate, useLoaderData, useFetcher } from "@remix-run/react";
|
import { useNavigate, useLoaderData, useFetcher } from "@remix-run/react";
|
||||||
import type { FilePreviewHandle } from "~/components/reviews/FilePreview";
|
import type { FilePreviewHandle } from "~/components/reviews/FilePreview";
|
||||||
import reviewsStyles from "~/styles/reviews.css?url";
|
import reviewsStyles from "~/styles/reviews.css?url";
|
||||||
import { getReviewPoints, getReviewPoints_fromApi, updateReviewResult, confirmReviewResults } from "~/api/evaluation_points/reviews";
|
import { getReviewPoints, getReviewPoints_fromApi, getUnifiedEvaluationResults, updateReviewResult, confirmReviewResults } from "~/api/evaluation_points/reviews";
|
||||||
import { toastService } from "~/components/ui/Toast";
|
import { toastService } from "~/components/ui/Toast";
|
||||||
|
|
||||||
// 导入评查详情页面组件
|
// 导入评查详情页面组件
|
||||||
@@ -193,18 +193,20 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
const { getUserSession } = await import("~/api/login/auth.server");
|
const { getUserSession } = await import("~/api/login/auth.server");
|
||||||
const { userInfo, frontendJWT } = await getUserSession(request);
|
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||||
|
|
||||||
// 🆕 使用新的后端API获取评查点数据(单次请求替代原7次请求)
|
// 🆕 使用新的统一API获取评查点数据
|
||||||
const reviewData = await getReviewPoints_fromApi(id, request);
|
// 先尝试新的统一评查接口
|
||||||
|
const unifiedData = await getUnifiedEvaluationResults(id, request);
|
||||||
|
|
||||||
// ⚠️ 原方法已注释(保留以备回退)
|
// 如果统一接口返回错误或flow_type为legacy,使用原有API
|
||||||
// const reviewData = await getReviewPoints(id, request);
|
if ('error' in unifiedData || !unifiedData.flow_type) {
|
||||||
|
console.log("[Reviews Loader] 统一接口不可用,使用旧接口...");
|
||||||
|
const reviewData = await getReviewPoints_fromApi(id, request);
|
||||||
|
|
||||||
if ('error' in reviewData && reviewData.error) {
|
if ('error' in reviewData && reviewData.error) {
|
||||||
console.error("[Reviews Loader] 获取评查点数据错误:", reviewData.error);
|
console.error("[Reviews Loader] 获取评查点数据错误:", reviewData.error);
|
||||||
return Response.json({ result: false, message: reviewData.error });
|
return Response.json({ result: false, message: reviewData.error });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保reviewData有效且具有预期的属性
|
|
||||||
if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) {
|
if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) {
|
||||||
return Response.json({
|
return Response.json({
|
||||||
previousRoute: previousRoute,
|
previousRoute: previousRoute,
|
||||||
@@ -214,13 +216,58 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
statistics: reviewData.stats,
|
statistics: reviewData.stats,
|
||||||
comparison_document: reviewData.comparison_document,
|
comparison_document: reviewData.comparison_document,
|
||||||
userInfo,
|
userInfo,
|
||||||
frontendJWT
|
frontendJWT,
|
||||||
|
flowType: 'legacy',
|
||||||
|
scoredResults: null,
|
||||||
|
scoredSummary: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一接口成功返回,判断流程类型
|
||||||
|
if (unifiedData.flow_type === 'graphrag') {
|
||||||
|
// 先获取文档基本信息(统一接口不返回文档内容)
|
||||||
|
const reviewData = await getReviewPoints_fromApi(id, request);
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
previousRoute: previousRoute,
|
||||||
|
document: ('document' in reviewData && !('error' in reviewData)) ? reviewData.document : null,
|
||||||
|
reviewPoints: unifiedData.results,
|
||||||
|
reviewInfo: { reviewTime: unifiedData.evaluated_at, reviewModel: 'GraphRAG', ruleGroup: '', result: '', issueCount: unifiedData.summary?.total_points || 0 },
|
||||||
|
statistics: {
|
||||||
|
total: unifiedData.summary?.total_points || 0,
|
||||||
|
success: unifiedData.summary?.passed_count || 0,
|
||||||
|
error: unifiedData.summary?.failed_count || 0,
|
||||||
|
warning: 0,
|
||||||
|
score: unifiedData.summary?.total_score || 0
|
||||||
|
},
|
||||||
|
comparison_document: ('comparison_document' in reviewData && !('error' in reviewData)) ? reviewData.comparison_document : null,
|
||||||
|
userInfo,
|
||||||
|
frontendJWT,
|
||||||
|
flowType: 'graphrag',
|
||||||
|
scoredResults: unifiedData.results,
|
||||||
|
scoredSummary: unifiedData.summary
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error("[Reviews Loader] 返回的评查数据格式不正确,完整数据:", JSON.stringify(reviewData, null, 2));
|
// legacy 流程但统一接口可用,也走原有逻辑
|
||||||
return Response.json({ result: false, message: '返回的评查数据格式不正确' });
|
const reviewData = await getReviewPoints_fromApi(id, request);
|
||||||
|
if ('error' in reviewData && reviewData.error) {
|
||||||
|
return Response.json({ result: false, message: reviewData.error });
|
||||||
|
}
|
||||||
|
return Response.json({
|
||||||
|
previousRoute: previousRoute,
|
||||||
|
document: reviewData.document,
|
||||||
|
reviewPoints: reviewData.data,
|
||||||
|
reviewInfo: reviewData.reviewInfo,
|
||||||
|
statistics: reviewData.stats,
|
||||||
|
comparison_document: reviewData.comparison_document,
|
||||||
|
userInfo,
|
||||||
|
frontendJWT,
|
||||||
|
flowType: 'legacy',
|
||||||
|
scoredResults: null,
|
||||||
|
scoredSummary: null
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('[Reviews Loader] 获取评查数据失败:', error);
|
console.error('[Reviews Loader] 获取评查数据失败:', error);
|
||||||
console.error('[Reviews Loader] 错误堆栈:', error instanceof Error ? error.stack : '无堆栈信息');
|
console.error('[Reviews Loader] 错误堆栈:', error instanceof Error ? error.stack : '无堆栈信息');
|
||||||
return Response.json({ result: false, message: `获取评查数据失败: ${error instanceof Error ? error.message : '未知错误'}` });
|
return Response.json({ result: false, message: `获取评查数据失败: ${error instanceof Error ? error.message : '未知错误'}` });
|
||||||
@@ -909,6 +956,9 @@ export default function ReviewDetails() {
|
|||||||
onStatusChange={handleReviewPointStatusChange}
|
onStatusChange={handleReviewPointStatusChange}
|
||||||
fileFormat={reviewData.fileInfo.fileFormat}
|
fileFormat={reviewData.fileInfo.fileFormat}
|
||||||
onAiSuggestionReplace={handleAiSuggestionReplace}
|
onAiSuggestionReplace={handleAiSuggestionReplace}
|
||||||
|
flowType={reviewData.flowType}
|
||||||
|
scoredResults={reviewData.scoredResults}
|
||||||
|
scoredSummary={reviewData.scoredSummary}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user