Merge remote-tracking branch 'origin/shiy-login' into PingChuan

This commit is contained in:
PingChuan
2025-12-02 15:40:32 +08:00
23 changed files with 606 additions and 176 deletions
@@ -62,7 +62,7 @@ export function DocumentListModal({
try {
// 更新文档状态,传递JWT
const updatedFile = await updateDocumentAuditStatus(fileId, 2, frontendJWT);
console.log('更新后的文档状态:', updatedFile);
// console.log('更新后的文档状态:', updatedFile);
} catch (error) {
console.error('更新文件审核状态时出错:', error);
toastService.error(`更新文件审核状态时出错:${error instanceof Error ? error.message : '未知错误'}`);
@@ -182,7 +182,7 @@ interface ReviewPointsListProps {
reviewPoints: ReviewPoint[];
statistics: Statistics;
activeReviewPointResultId: string | null;
onReviewPointSelect: (id: string, page?: number, charPositions?: CharPosition[]) => void;
onReviewPointSelect: (id: string, page?: number, charPositions?: CharPosition[], value?: string) => void;
onStatusChange?: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
scoringProposals?: ScoringProposal[];
jwtToken?: string; // 添加JWT token参数
@@ -779,16 +779,17 @@ export function ReviewPointsList({
}
// 打印最终请求体
// console.log('最终请求体:', data);
// console.log('jwtToken:', jwtToken);
// 用 axios + application/json 提交
try {
const response = await axios.post(`${API_BASE_URL.replace(/\/$/, '')}/admin/cross_review/proposals`, data, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${userInfo.frontend_jwt}`,
'Authorization': `Bearer ${jwtToken}`,
}
});
const result = response.data;
if (response.status === 200) {
if (result.code === 200 || result.code === 0) {
toastService.success('意见提交成功');
// 创建新的提案对象
@@ -814,11 +815,24 @@ export function ReviewPointsList({
handleCloseOpinionModal();
} else {
toastService.error(result.detail || '提交意见失败');
throw new Error(result.msg || '提交意见失败')
// toastService.error(result.msg || '提交意见失败');
}
} catch (error) {
console.error('提交意见失败:', error);
toastService.error('提交意见失败,请稍后重试');
// 正确处理 axios 错误响应
let errorMessage = '提交意见失败,请稍后重试';
if (axios.isAxiosError(error) && error.response?.data) {
// 从 axios 错误响应中提取 msg 字段
errorMessage = error.response.data.msg || errorMessage;
} else if (error instanceof Error) {
// 处理普通 Error 对象
errorMessage = error.message || errorMessage;
}
toastService.error(errorMessage);
}
setIsSubmittingOpinion(false);
};
@@ -1407,7 +1421,7 @@ export function ReviewPointsList({
for (const item of chain) {
if (item.data.page && typeof onReviewPointSelect === 'function') {
hasPage = true;
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions);
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions, item.data.value);
break;
}
}
@@ -1421,7 +1435,7 @@ export function ReviewPointsList({
// 遍历chain找到第一个有效的page
for (const item of chain) {
if (item.data.page && typeof onReviewPointSelect === 'function') {
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions);
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions, item.data.value);
break;
}
}
@@ -1461,11 +1475,11 @@ export function ReviewPointsList({
// 假设onReviewPointSelect在作用域内可用
const reviewPointId = reviewPoint.id as string;
if (reviewPointId && typeof onReviewPointSelect === 'function') {
onReviewPointSelect(reviewPointId, Number(item.data.page), item.data.char_positions);
onReviewPointSelect(reviewPointId, Number(item.data.page), item.data.char_positions, item.data.value);
}
}
else if(reviewPoint.contentPage && reviewPoint.contentPage[item.field]){
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[item.field]));
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[item.field]), item.data.char_positions, item.data.value);
}
else{
toastService.error(`没有找到${item.field}对应的索引内容`);
@@ -1544,11 +1558,11 @@ export function ReviewPointsList({
if (chain[0].data.page) {
const reviewPointId = reviewPoint.id as string;
if (reviewPointId && typeof onReviewPointSelect === 'function') {
onReviewPointSelect(reviewPointId, chain[0].data.page, chain[0].data.char_positions);
onReviewPointSelect(reviewPointId, chain[0].data.page, chain[0].data.char_positions, chain[0].data.value);
}
}
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[0].field]){
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[0].field]));
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[0].field]), chain[0].data.char_positions, chain[0].data.value);
}
else{
toastService.error(`没有找到${chain[0].field}对应的索引内容`);
@@ -1570,11 +1584,11 @@ export function ReviewPointsList({
if (chain[1].data.page) {
const reviewPointId = reviewPoint.id as string;
if (reviewPointId && typeof onReviewPointSelect === 'function') {
onReviewPointSelect(reviewPointId, chain[1].data.page, chain[1].data.char_positions);
onReviewPointSelect(reviewPointId, chain[1].data.page, chain[1].data.char_positions, chain[1].data.value);
}
}
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[1].field]){
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[1].field]));
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[1].field]), chain[1].data.char_positions, chain[1].data.value);
}
else{
toastService.error(`没有找到${chain[1].field}对应的索引内容`);
@@ -1710,9 +1724,9 @@ export function ReviewPointsList({
onClick={(e) => {
e.stopPropagation();
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions);
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value);
}else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]));
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value);
}else{
toastService.error(`没有找到${fieldKey}对应的索引内容`);
}
@@ -1721,7 +1735,9 @@ export function ReviewPointsList({
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions);
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value);
}else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value);
}else{
toastService.error(`没有找到${fieldKey}对应的索引内容`);
}
@@ -1832,9 +1848,9 @@ export function ReviewPointsList({
onClick={(e) => {
e.stopPropagation();
if (value.page && typeof onReviewPointSelect === 'function') {
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions);
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions, value.value);
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]));
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions, value.value);
}else{
toastService.error(`没有找到${key}对应的索引内容`);
}
@@ -1844,9 +1860,9 @@ export function ReviewPointsList({
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (value.page && typeof onReviewPointSelect === 'function') {
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions);
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions, value.value);
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]));
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions, value.value);
}else{
toastService.error(`没有找到${key}对应的索引内容`);
}
@@ -2404,29 +2420,26 @@ export function ReviewPointsList({
<>
<div className="relative">
{/* 悬浮的意见数量显示 - 固定在左侧 */}
<button
className="absolute left-[-35px] top-16 z-10 group cursor-pointer"
<button
className="absolute left-[-35px] top-16 z-10 cursor-pointer"
onClick={() => handleOpenOpinionListModal(reviewPoints[0])}
type="button"
aria-label="查看意见列表"
>
{/* 默认状态:竖向排列,窄宽度 */}
<div className="flex flex-col items-center bg-blue-50 px-2 py-2 rounded-lg border border-blue-200 shadow-md transition-all duration-300 group-hover:scale-0 group-hover:opacity-0 origin-top-right">
<i className="ri-chat-1-line text-blue-600 text-base"></i>
<span className="text-base text-blue-600 font-bold leading-tight whitespace-nowrap">{scoringProposals.length}</span>
<span className="text-xs text-blue-500 leading-tight whitespace-wrap"></span>
<span className="text-xs text-blue-500 leading-tight whitespace-wrap"></span>
<span className="text-xs text-blue-500 leading-tight whitespace-wrap"></span>
</div>
{/* 悬浮状态:横向排列,显示图标,数字放大 */}
<div className="absolute top-0 right-0 opacity-0 scale-0 group-hover:opacity-100 group-hover:scale-100 flex items-center bg-blue-50 px-3 py-2 rounded-lg border border-blue-200 shadow-lg transition-all duration-300 origin-top-right">
<div className="flex flex-col">
<i className="ri-chat-1-line text-blue-600 text-base"></i>
<span className="text-xl text-blue-600 font-bold">{scoringProposals.length}</span>
<span className="text-xs text-blue-500 leading-tight whitespace-wrap"></span>
<span className="text-xs text-blue-500 leading-tight whitespace-wrap"></span>
<span className="text-xs text-blue-500 leading-tight whitespace-wrap"></span>
<div className={`relative flex flex-col items-center bg-gradient-to-br from-blue-50 to-blue-100 px-2 py-2 rounded-lg border border-blue-300 shadow-md transition-all duration-200 ease-out hover:scale-110 hover:shadow-xl active:scale-95 ${scoringProposals.length === 0 ? 'opacity-50' : 'opacity-100'}`}>
{/* 脉动提示点 - 仅当有意见时显示 */}
{scoringProposals.length > 0 && (
<span className="absolute -top-1 -right-1 flex h-3 w-3">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-3 w-3 bg-blue-500"></span>
</span>
)}
<i className="ri-chat-1-line text-blue-600 text-lg mb-0.5"></i>
<span className="text-lg text-blue-700 font-bold leading-tight">{scoringProposals.length}</span>
<div className="flex flex-col items-center text-[10px] text-blue-600 leading-tight mt-0.5">
<span></span>
<span></span>
<span></span>
</div>
</div>
</button>
@@ -2656,14 +2669,16 @@ export function ReviewPointsList({
</div>
) : (
<>
<Table
columns={[
<div style={{ minHeight: '500px', display: 'flex', flexDirection: 'column' }}>
<div style={{ flex: 1, minHeight: '450px' }}>
<Table
columns={[
{
title: "评查点名称",
key: "evaluation_point_name",
width: "15%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="text-sm">{record.evaluation_point_name}</div>
<div className="text-sm text-left py-1">{record.evaluation_point_name}</div>
)
},
{
@@ -2671,7 +2686,7 @@ export function ReviewPointsList({
key: "problem_message",
width: "18%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="text-sm text-left">{record.problem_message}</div>
<div className="text-sm text-left py-1">{record.problem_message}</div>
)
},
{
@@ -2682,14 +2697,14 @@ export function ReviewPointsList({
const reason = record.reason || '';
const display = reason.length > 20 ? reason.slice(0, 20) + '...' : reason;
return (
<span title={reason}>{display}</span>
<div className="text-sm text-left py-1" title={reason}>{display}</div>
);
}
},
{
title: "调整分数",
key: "proposed_score",
width: "5%",
width: "8%",
align: "center" as const,
render: (_: unknown, record: CrossCheckingOpinion) => (
<span className={`text-sm font-medium ${record.proposed_score >= 0 ? 'text-green-600' : 'text-red-600'}`}>
@@ -2700,8 +2715,7 @@ export function ReviewPointsList({
{
title: "投票人",
key: "votes",
width: "22%",
align: "center" as const,
width: "24%",
render: (_: unknown, record: CrossCheckingOpinion) => {
// 投票类型配置
const voterGroups = [
@@ -2728,19 +2742,20 @@ export function ReviewPointsList({
}
];
return (
<div className="flex flex-col gap-1.5 py-1 min-w-[120px]">
<div className="flex flex-col items-start gap-1.5 py-1 w-full">
{voterGroups.map((group) => (
Array.isArray(group.voters) && group.voters.length > 0 && (
<div key={group.type} className="flex flex-wrap gap-1">
<div key={group.type} className="flex items-start gap-1 w-full">
{group.voters.map((name, idx) => (
<span
key={`${group.type}-${name}-${idx}`}
className={`
px-1.5 py-0.5 rounded text-xs font-medium
${group.color} ${group.bg} ${group.border}
whitespace-nowrap overflow-hidden text-ellipsis max-w-[80px]
whitespace-nowrap
transition-all hover:scale-[1.03] hover:shadow-sm
`}
title={name}
>
{name}
</span>
@@ -2757,9 +2772,10 @@ export function ReviewPointsList({
key: "proposer",
width: "8%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="flex items-center justify-center text-left">
<div className="flex items-center justify-center py-1">
<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"
className="px-1.5 py-0.5 rounded text-xs font-medium text-yellow-700 bg-yellow-100 border border-yellow-200 whitespace-nowrap transition-all hover:scale-[1.03] hover:shadow-sm"
title={record.proposer}
>
{record.proposer}
</span>
@@ -2769,15 +2785,16 @@ export function ReviewPointsList({
{
title: "发起时间",
key: "created_at",
width: "12%",
width: "8%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="text-sm text-left">{record.created_at}</div>
<div className="text-sm text-left py-1">{record.created_at}</div>
)
},
{
title: "投票状态",
key: "opinion_status",
width: "12%",
align: "center" as const,
render: (_: unknown, record: CrossCheckingOpinion) => {
let label = '';
let color = '';
@@ -2796,7 +2813,7 @@ export function ReviewPointsList({
color = 'text-yellow-600';
break;
}
return <span className={`font-bold ${color}`}>{label}</span>;
return <span className={`text-sm font-bold ${color}`}>{label}</span>;
}
},
{
@@ -2812,25 +2829,27 @@ export function ReviewPointsList({
}
}
]}
dataSource={opinionListData}
rowKey="proposal_id"
emptyText="暂无意见数据"
className="opinion-list-table"
/>
dataSource={opinionListData}
rowKey="proposal_id"
emptyText="暂无意见数据"
className="opinion-list-table"
/>
</div>
{/* 分页组件 */}
{opinionListTotal > 0 && (
<Pagination
{/* 分页组件 */}
{opinionListTotal > 0 && (
<Pagination
currentPage={opinionListCurrentPage}
total={opinionListTotal}
pageSize={opinionListPageSize}
onChange={handleOpinionListPageChange}
onPageSizeChange={handleOpinionListPageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[5,10, 20, 30, 50]}
/>
)}
onChange={handleOpinionListPageChange}
onPageSizeChange={handleOpinionListPageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[5,10, 20, 30, 50]}
/>
)}
</div>
</>
)}
</div>
+40 -2
View File
@@ -14,6 +14,13 @@ import '../../styles/components/chat-with-llm/index.css';
const { Content } = Layout;
// 扩展 Window 接口以支持自定义属性
declare global {
interface Window {
hasSetInitialSidebarState?: boolean;
}
}
/**
* 主聊天组件
* 实现单页面应用模式,参考webapp-conversation的初始化逻辑
@@ -348,6 +355,11 @@ export default function Chat() {
if (conversationId !== currConversationId) {
setCurrConversationId(conversationId, CHAT_CONFIG.APP_ID);
}
// 移动端选中对话后自动隐藏侧边栏
if (isMobile && !sidebarCollapsed) {
setSidebarCollapsed(true);
}
};
/**
@@ -485,7 +497,15 @@ export default function Chat() {
// 检查屏幕尺寸
useEffect(() => {
const checkScreenSize = () => {
setIsMobile(window.innerWidth < 992);
const isMobileDevice = window.innerWidth < 992;
setIsMobile(isMobileDevice);
// 移动端默认隐藏侧边栏,桌面端默认显示
// 只在初次加载时设置,避免影响用户的手动切换
if (!window.hasSetInitialSidebarState) {
setSidebarCollapsed(isMobileDevice);
window.hasSetInitialSidebarState = true;
}
};
// 初始检查
@@ -537,7 +557,7 @@ export default function Chat() {
const conversationIntroduction = currConversationInfo?.introduction || '';
return (
<Layout style={{ height: '100%', display: 'flex', flexDirection: 'row' }}>
<Layout style={{ height: '100%', display: 'flex', flexDirection: 'row', position: 'relative' }}>
{/* 移动端遮罩层 */}
{!sidebarCollapsed && isMobile && (
<div
@@ -546,6 +566,24 @@ export default function Chat() {
/>
)}
{/* ChatSidebar 隐藏时显示的展开按钮 */}
{sidebarCollapsed && (
<button
onClick={handleSidebarToggle}
className="fixed left-0 top-1/2 -translate-y-1/2 z-[998] bg-white hover:bg-gray-100 shadow-lg rounded-r-lg px-2 py-4 transition-all duration-200 border border-l-0 border-gray-200"
style={{
width: '32px',
height: '48px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
aria-label="展开对话列表"
>
<i className="ri-menu-unfold-line text-lg text-gray-600"></i>
</button>
)}
{/* 侧边栏 */}
<ChatSidebar
ref={sidebarRef}
+2 -1
View File
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { Link, useLocation, useNavigate } from '@remix-run/react';
import type { UserRole } from '~/root';
import { getUserRoutesByRole, mapUserRoleToRoleKey, type MenuItem } from '~/api/auth/user-routes';
import { DOCUMENT_URL } from '~/config/api-config';
interface SidebarProps {
onToggle: () => void;
@@ -320,7 +321,7 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
<div className={`flex items-center ${collapsed ? 'justify-center' : ''}`}>
{selectedModulePicPath && (
<img
src={selectedModulePicPath}
src={selectedModuleName === '智慧法务大模型' || selectedModuleName === '交叉评查' ? selectedModulePicPath : `${DOCUMENT_URL}${selectedModulePicPath}`}
alt={selectedModuleName}
className={`${collapsed ? 'w-8 h-8' : 'w-6 h-6 mr-3'}`}
/>
+1
View File
@@ -211,6 +211,7 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
// console.log('[FilePreview] 渲染PDF预览', { real_path, targetPage, charPositions });
// console.log('[FilePreview] 渲染PDF预览', { fileContent });
const pageOffset = fileContent.ocrResult?.__meta?.page_offset || fileContent.ocr_result?.__meta?.page_offset || 0;
// console.log('pageOffset', pageOffset)
return (
<PdfPreview
filePath={real_path}