From 8fbf9156569938de3bdf35bdd7b566b34c59c015 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Wed, 4 Jun 2025 17:31:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=B0=E7=9A=84=E9=AA=A8?= =?UTF-8?q?=E6=9E=B6=E5=B1=8F=EF=BC=8C=E5=B0=86=E8=AF=84=E6=9F=A5=E7=82=B9?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E5=92=8C=E8=AF=84=E6=9F=A5=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=88=97=E8=A1=A8=EF=BC=8C=E6=96=87=E6=A1=A3=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E6=95=B0=E6=8D=AE=E5=88=86=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/chat/chat-message.tsx | 2 +- app/components/chat/index.tsx | 34 +-- app/components/layout/Layout.tsx | 5 +- app/components/layout/Sidebar.tsx | 209 +++++++++++------ app/components/rules/new/ReviewSettings.tsx | 4 +- app/components/ui/SkeletonScreen.tsx | 128 +++++++++++ app/routes/_index.tsx | 81 ++++--- app/routes/documents._index.tsx | 32 ++- app/routes/home.tsx | 8 +- app/routes/rules-files.tsx | 27 ++- app/routes/rules._index.tsx | 241 +++++++++++--------- app/styles/pages/home.css | 31 +-- app/styles/pages/login.css | 5 +- package-lock.json | 4 +- package.json | 1 + public/images/主页背景-min.png | Bin 0 -> 509603 bytes public/images/登录页背景-min.png | Bin 0 -> 1629167 bytes 17 files changed, 537 insertions(+), 275 deletions(-) create mode 100644 app/components/ui/SkeletonScreen.tsx create mode 100644 public/images/主页背景-min.png create mode 100644 public/images/登录页背景-min.png diff --git a/app/components/chat/chat-message.tsx b/app/components/chat/chat-message.tsx index 920f4ab..44d63c8 100644 --- a/app/components/chat/chat-message.tsx +++ b/app/components/chat/chat-message.tsx @@ -114,7 +114,7 @@ export default function ChatMessage({ className="question-button text-left" onClick={() => { // 这里可以添加点击建议问题的处理逻辑 - console.log('Suggested question clicked:', question); + // console.log('Suggested question clicked:', question); }} > {question} diff --git a/app/components/chat/index.tsx b/app/components/chat/index.tsx index 8af8770..45a2600 100644 --- a/app/components/chat/index.tsx +++ b/app/components/chat/index.tsx @@ -59,7 +59,7 @@ export default function Chat() { } = useChatMessage({ onUpdateConversationList: updateConversationInList, onConversationIdChange: async (conversationId: string) => { - console.log('🔄 收到会话ID变更通知:', conversationId); + // console.log('🔄 收到会话ID变更通知:', conversationId); // 设置当前会话ID(这会触发localStorage更新) setCurrConversationId(conversationId, CHAT_CONFIG.APP_ID); @@ -67,7 +67,7 @@ export default function Chat() { // 如果是新会话,添加到会话列表 const existingConversation = conversationList.find(item => item.id === conversationId); if (!existingConversation) { - console.log('🆕 添加新会话到列表:', conversationId); + // console.log('🆕 添加新会话到列表:', conversationId); const newConversation = { id: conversationId, name: '新对话', @@ -78,7 +78,7 @@ export default function Chat() { // 检查是否需要自动重命名(新对话的第一条消息) if (!newConversationFirstMessageSent.has(conversationId)) { - console.log('🏷️ 新对话第一条消息,准备自动重命名:', conversationId); + // console.log('🏷️ 新对话第一条消息,准备自动重命名:', conversationId); // 标记该对话已发送第一条消息 setNewConversationFirstMessageSent(prev => new Set(prev).add(conversationId)); @@ -88,7 +88,7 @@ export default function Chat() { try { if (sidebarRef.current) { await sidebarRef.current.autoRename(conversationId); - console.log('✅ 新对话自动重命名完成:', conversationId); + // console.log('✅ 新对话自动重命名完成:', conversationId); } else { console.warn('⚠️ 侧边栏引用不可用,无法自动重命名'); } @@ -170,7 +170,7 @@ export default function Chat() { return; } - console.log('🔄 处理会话切换:', { currConversationId, isNewConversation }); + // console.log('🔄 处理会话切换:', { currConversationId, isNewConversation }); // 更新当前会话的输入 let notSyncToStateIntroduction = ''; @@ -193,11 +193,11 @@ export default function Chat() { // 更新当前会话的聊天列表 if (!isNewConversation && !conversationIdChangeBecauseOfNew && !isResponding) { try { - console.log('📨 获取会话历史消息:', currConversationId); + // console.log('📨 获取会话历史消息:', currConversationId); // 调用API获取历史消息 const response = await fetchChatList(currConversationId); - console.log('📋 历史消息响应:', response); + // console.log('📋 历史消息响应:', response); if (response && (response as any).data) { const { data: historyMessages } = response as any; @@ -226,7 +226,7 @@ export default function Chat() { }); }); - console.log('✅ 设置历史聊天列表:', newChatList.length, '条消息'); + // console.log('✅ 设置历史聊天列表:', newChatList.length, '条消息'); setChatList(newChatList); } else { console.warn('⚠️ 获取历史消息失败或无数据'); @@ -251,7 +251,7 @@ export default function Chat() { * 处理会话ID变化 */ const handleConversationIdChange = (id: string) => { - console.log('🔄 会话ID变化:', { id, currentId: currConversationId }); + // console.log('🔄 会话ID变化:', { id, currentId: currConversationId }); if (id === '-1') { createNewChat(); @@ -352,7 +352,7 @@ export default function Chat() { * 处理会话删除后的状态更新 */ const handleConversationDeleted = (conversationId: string) => { - console.log('🗑️ 处理会话删除后的状态更新:', conversationId); + // console.log('🗑️ 处理会话删除后的状态更新:', conversationId); // 如果删除的是当前会话,切换到新会话 if (conversationId === currConversationId) { @@ -362,19 +362,19 @@ export default function Chat() { // 从列表中移除会话 removeConversationFromList(conversationId); - console.log('✅ 会话删除状态更新完成:', conversationId); + // console.log('✅ 会话删除状态更新完成:', conversationId); }; /** * 处理会话重命名后的状态更新 */ const handleConversationRenamed = (conversationId: string, newName: string) => { - console.log('✏️ 处理会话重命名后的状态更新:', { conversationId, newName }); + // console.log('✏️ 处理会话重命名后的状态更新:', { conversationId, newName }); // 更新本地会话列表中的名称 updateConversationInList(conversationId, { name: newName }); - console.log('✅ 会话重命名状态更新完成:', conversationId, '->', newName); + // console.log('✅ 会话重命名状态更新完成:', conversationId, '->', newName); }; /** @@ -397,7 +397,7 @@ export default function Chat() { fetchAppParams() ]); - console.log('📋 获取到的数据:', { conversationData, appParams }); + // console.log('📋 获取到的数据:', { conversationData, appParams }); // 处理会话数据 const conversations = (conversationData as any).data || []; @@ -410,8 +410,8 @@ export default function Chat() { const _conversationId = getConversationIdFromStorage(CHAT_CONFIG.APP_ID); const isNotNewConversation = conversations.some((item: ConversationItem) => item.id === _conversationId); - console.log('💾 本地存储的会话ID:', _conversationId); - console.log('🔍 是否为已存在的会话:', isNotNewConversation); + // console.log('💾 本地存储的会话ID:', _conversationId); + // console.log('🔍 是否为已存在的会话:', isNotNewConversation); // 获取新会话信息 const { user_input_form, opening_statement: introduction } = (appParams as any).data || {}; @@ -432,7 +432,7 @@ export default function Chat() { // 如果存在有效的会话ID,则设置为当前会话 if (isNotNewConversation) { - console.log('🎯 设置当前会话ID:', _conversationId); + // console.log('🎯 设置当前会话ID:', _conversationId); setCurrConversationId(_conversationId, CHAT_CONFIG.APP_ID, false); } else { // 如果localStorage为空或会话不存在,自动创建新会话 diff --git a/app/components/layout/Layout.tsx b/app/components/layout/Layout.tsx index 26e3457..416463c 100644 --- a/app/components/layout/Layout.tsx +++ b/app/components/layout/Layout.tsx @@ -6,7 +6,7 @@ import { useMatches, useLocation } from '@remix-run/react'; import type { UserRole } from '~/root'; // 定义应用模块类型 -type AppModule = 'contract' | 'record' | 'model'; +type AppModule = 'contract' | 'record' | 'model' | ''; // 应用模块与reviewType的映射 const REVIEW_TYPE_TO_APP: Record = { @@ -34,7 +34,7 @@ interface Match { export function Layout({ children, userRole = 'developer' }: LayoutProps) { const [sidebarCollapsed, setSidebarCollapsed] = useState(false); - const [selectedApp, setSelectedApp] = useState('contract'); + const [selectedApp, setSelectedApp] = useState(''); const matches = useMatches() as Match[]; const location = useLocation(); @@ -102,6 +102,7 @@ export function Layout({ children, userRole = 'developer' }: LayoutProps) { return (
+ {/* 侧边栏始终保留,不再使用条件渲染 */} = { 'model': 'ri-robot-2-fill' }; -export function Sidebar({ onToggle, collapsed, userRole, selectedApp = 'contract' }: SidebarProps) { +export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: SidebarProps) { const location = useLocation(); const [expandedMenus, setExpandedMenus] = useState>({}); - const [currentApp, setCurrentApp] = useState(selectedApp); + const [currentApp, setCurrentApp] = useState(''); // 初始设置为空字符串而不是selectedApp + const [isLoading, setIsLoading] = useState(true); // 添加加载状态 const navigate = useNavigate(); // 组件挂载后从 sessionStorage 读取初始 reviewType useEffect(() => { + let mounted = true; + setIsLoading(true); // 设置加载状态 + try { const reviewType = sessionStorage.getItem('reviewType'); // console.log('初始 reviewType:', reviewType); - if (reviewType) { + if (reviewType && mounted) { setCurrentApp(reviewType); + } else if (selectedApp && mounted) { + // 如果没有reviewType,但有selectedApp,使用selectedApp + setCurrentApp(selectedApp); } } catch (error) { console.error('读取 reviewType 失败:', error); + } finally { + // 使用setTimeout确保状态在DOM更新后再变化,避免闪烁 + setTimeout(() => { + if (mounted) { + setIsLoading(false); // 数据加载完成 + } + }, 0); } - }, []); + + return () => { + mounted = false; + }; + }, [selectedApp]); // 从 sessionStorage 获取 reviewType 并设置当前应用模块 useEffect(() => { @@ -78,23 +96,44 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = 'contract // 监听路由变化,重新检查 reviewType useEffect(() => { + let mounted = true; + try { const reviewType = sessionStorage.getItem('reviewType'); - // console.log('路由变化, 检查 reviewType:', reviewType, '路径:', location.pathname); - if (reviewType) { + // 只有当reviewType变化时才设置加载状态和更新currentApp + if (reviewType && reviewType !== currentApp && mounted) { + setIsLoading(true); // 路由变化时设置加载状态 setCurrentApp(reviewType); + // 使用setTimeout确保状态在DOM更新后再变化,避免闪烁 + setTimeout(() => { + if (mounted) { + setIsLoading(false); + } + }, 0); } } catch (error) { console.error('路由变化时读取 reviewType 失败:', error); + if (mounted) { + setIsLoading(false); + } } - }, [location.pathname]); + + return () => { + mounted = false; + }; + }, [location.pathname, currentApp]); // 监听 selectedApp 属性变化 useEffect(() => { - if (selectedApp) { + if (selectedApp && selectedApp !== currentApp) { + setIsLoading(true); // 设置加载状态 setCurrentApp(selectedApp); + // 使用setTimeout确保状态在DOM更新后再变化,避免闪烁 + setTimeout(() => { + setIsLoading(false); // 数据加载完成 + }, 0); } - }, [selectedApp]); + }, [selectedApp, currentApp]); const menuItems: MenuItem[] = [ { @@ -270,6 +309,7 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = 'contract // 获取当前应用模式下应显示的菜单ID列表 const visibleMenuIds = APP_MENU_MAP[currentApp as keyof typeof APP_MENU_MAP] || APP_MENU_MAP['contract']; + // const visibleMenuIds = APP_MENU_MAP[currentApp as keyof typeof APP_MENU_MAP] // console.log('当前应用模式:', currentApp, '可见菜单ID:', visibleMenuIds); // 根据用户角色和当前应用模式过滤菜单项 @@ -319,80 +359,105 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = 'contract {!collapsed && (
- - {APP_NAME_MAP[currentApp] || '合同管理'} + {isLoading ? ( + // 加载中状态,只显示加载图标,保留布局 +
+
+ 加载中... +
+ ) : ( + <> + + {APP_NAME_MAP[currentApp] || ''} + + )}
)}
- {filteredMenuItems.map((item) => ( -
- {!item.children ? ( - { - // 只阻止冒泡,不阻止默认行为 - e.stopPropagation(); - // console.log('单级菜单点击:', item.title, '路径:', item.path); - }} - > - - {!collapsed && {item.title}} - - ) : ( - <> -
+ {Array(5).fill(0).map((_, index) => ( +
+
+
+ {!collapsed &&
} +
+
+ ))} +
+ ) : ( + // 数据加载完成后显示菜单 + filteredMenuItems.map((item) => ( +
+ {!item.children ? ( + { - // console.log('%c父菜单点击 ===> ', 'background: #722ed1; color: white; padding: 2px 4px; border-radius: 2px;', item.title); - toggleMenu(item.id, e); - }} - role="button" - tabIndex={0} - aria-expanded={expandedMenus[item.id] || false} - aria-controls={`submenu-${item.id}`} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - toggleMenu(item.id, e as unknown as React.MouseEvent); - } + // 只阻止冒泡,不阻止默认行为 + e.stopPropagation(); + // console.log('单级菜单点击:', item.title, '路径:', item.path); }} > -
- - {!collapsed && {item.title}} -
- {!collapsed && ( - - )} -
- - {(expandedMenus[item.id] || collapsed) && ( + + {!collapsed && {item.title}} + + ) : ( + <>
{ + // console.log('%c父菜单点击 ===> ', 'background: #722ed1; color: white; padding: 2px 4px; border-radius: 2px;', item.title); + toggleMenu(item.id, e); + }} + role="button" + tabIndex={0} + aria-expanded={expandedMenus[item.id] || false} + aria-controls={`submenu-${item.id}`} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggleMenu(item.id, e as unknown as React.MouseEvent); + } + }} > - {item.children - .filter(child => !child.requiredRole || child.requiredRole === userRole) - .map((child) => ( - handleSubMenuClick(child, e)} - > - - {!collapsed && {child.title}} - - ))} +
+ + {!collapsed && {item.title}} +
+ {!collapsed && ( + + )}
- )} - - )} -
- ))} + + {(expandedMenus[item.id] || collapsed) && ( +
+ {item.children + .filter(child => !child.requiredRole || child.requiredRole === userRole) + .map((child) => ( + handleSubMenuClick(child, e)} + > + + {!collapsed && {child.title}} + + ))} +
+ )} + + )} +
+ )) + )}
); diff --git a/app/components/rules/new/ReviewSettings.tsx b/app/components/rules/new/ReviewSettings.tsx index 3509c01..6644c70 100644 --- a/app/components/rules/new/ReviewSettings.tsx +++ b/app/components/rules/new/ReviewSettings.tsx @@ -1564,8 +1564,8 @@ export function ReviewSettings({ }} > - - + {/* */} + {/* */}
diff --git a/app/components/ui/SkeletonScreen.tsx b/app/components/ui/SkeletonScreen.tsx new file mode 100644 index 0000000..c6e060e --- /dev/null +++ b/app/components/ui/SkeletonScreen.tsx @@ -0,0 +1,128 @@ +// import React from 'react'; + +interface SkeletonBarProps { + width?: string; + height?: string; + className?: string; +} + +// 基础骨架条 +export function SkeletonBar({ width = 'w-full', height = 'h-5', className = '' }: SkeletonBarProps) { + return ( +
+ ); +} + +// 通用骨架屏 +export function SkeletonScreen() { + return ( +
+ + + +
+ ); +} + +// 数字统计骨架 +export function NumberSkeleton({ className = '' }: { className?: string }) { + return ( + + ); +} + +// 表格行骨架屏 +interface TableRowSkeletonProps { + count?: number; + className?: string; +} + +export function TableRowSkeleton({ count = 5, className = '' }: TableRowSkeletonProps) { + return ( +
+ {Array(count).fill(0).map((_, index) => ( +
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ ))} +
+ ); +} + +// 自定义列宽的表格行骨架屏 +interface CustomTableRowSkeletonProps { + count?: number; + columns: Array<{ + width: string; + rows?: Array<{ + width: string; + height?: string; + className?: string; + }>; + }>; + className?: string; +} + +export function CustomTableRowSkeleton({ count = 5, columns, className = '' }: CustomTableRowSkeletonProps) { + return ( +
+ {Array(count).fill(0).map((_, rowIndex) => ( +
+ {columns.map((column, colIndex) => ( +
+ {column.rows ? ( + column.rows.map((row, rowIdx) => ( + + )) + ) : ( + + )} +
+ ))} +
+ ))} +
+ ); +} + +// 加载指示器 +export function LoadingIndicator({ text = '正在加载数据...' }: { text?: string }) { + return ( +
+
+ {text} +
+ ); +} + + + diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index d3ff920..ffb9f72 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -138,54 +138,51 @@ export default function Index() { {/* 主要内容 */}
-

- 欢迎来到智慧法务平台 -

+
+

- 欢迎来到智慧法务平台 -

-
- {/* 合同管理模块 */} -
handleModuleClick('/contract-template/search', 'contract')} - onKeyDown={(e) => handleKeyDown('/contract-template/search', 'contract', e)} - role="button" - tabIndex={0} - aria-label="合同管理" - > - - 合同管理 -
+
+ {/* 合同管理模块 */} +
handleModuleClick('/contract-template/search', 'contract')} + onKeyDown={(e) => handleKeyDown('/contract-template/search', 'contract', e)} + role="button" + tabIndex={0} + aria-label="合同管理" + > + + 合同管理 +
- {/* 案卷智能评查模块 */} -
handleModuleClick('/home', 'record')} - onKeyDown={(e) => handleKeyDown('/home', 'record', e)} - role="button" - tabIndex={0} - aria-label="案卷智能评查" - > - - 案卷智能评查 -
+ {/* 案卷智能评查模块 */} +
handleModuleClick('/home', 'record')} + onKeyDown={(e) => handleKeyDown('/home', 'record', e)} + role="button" + tabIndex={0} + aria-label="案卷智能评查" + > + + 案卷智能评查 +
- {/* 智慧法务大模型模块 */} -
handleModuleClick('/chat-with-llm', 'model')} - onKeyDown={(e) => handleKeyDown('/chat-with-llm', 'model', e)} - role="button" - tabIndex={0} - aria-label="智慧法务大模型" - > - - 智慧法务大模型 + {/* 智慧法务大模型模块 */} +
handleModuleClick('/chat-with-llm', 'model')} + onKeyDown={(e) => handleKeyDown('/chat-with-llm', 'model', e)} + role="button" + tabIndex={0} + aria-label="智慧法务大模型" + > + + 智慧法务大模型 +
- {/* 底部山水背景 */} -
-
-
-
); } \ No newline at end of file diff --git a/app/routes/documents._index.tsx b/app/routes/documents._index.tsx index f8054a5..cc3723b 100644 --- a/app/routes/documents._index.tsx +++ b/app/routes/documents._index.tsx @@ -8,6 +8,7 @@ import { Pagination } from "~/components/ui/Pagination"; import { FileTypeTag } from "~/components/ui/FileTypeTag"; import { FileTag } from "~/components/ui/FileTag"; import { FilterPanel, FilterSelect, SearchFilter, DateRangeFilter } from "~/components/ui/FilterPanel"; +import { TableRowSkeleton, LoadingIndicator, NumberSkeleton } from '~/components/ui/SkeletonScreen'; import documentsIndexStyles from "~/styles/pages/documents_index.css?url"; import { getDocuments, deleteDocument, type DocumentUI } from "~/api/files/documents"; import { getDocumentTypes } from "~/api/document-types/document-types"; @@ -833,7 +834,18 @@ export default function DocumentsIndex() {
{/* 页面头部 */}
-

文档列表

+
+

文档列表

+ {isLoadingData ? ( +
+ +
+ ) : ( +
+ 共 {total} 条记录 +
+ )} +