diff --git a/app/components/layout/Sidebar.tsx b/app/components/layout/Sidebar.tsx index b9d1f4b..620ef9f 100644 --- a/app/components/layout/Sidebar.tsx +++ b/app/components/layout/Sidebar.tsx @@ -1,399 +1,399 @@ -import { useState, useEffect } from 'react'; -import { Link, useLocation, useNavigate } from '@remix-run/react'; -import type { UserRole } from '~/root'; - -interface MenuItem { - id: string; - title: string; - path: string; - icon: string; - hideBreadcrumb?: boolean; - requiredRole?: UserRole; - children?: MenuItem[]; -} - -interface SidebarProps { - onToggle: () => void; - collapsed: boolean; - userRole: UserRole; - selectedApp?: string; // 添加所选应用模块参数 -} - -// 定义不同应用模块下显示的菜单项ID -const APP_MENU_MAP = { - 'contract': ['home', 'contract-template', 'file-management', 'rule-management', 'system-settings'], - 'record': ['home', 'file-management', 'rule-management', 'system-settings'], - 'model': ['chat-with-llm'] -}; - -// 应用模块名称映射 -const APP_NAME_MAP: Record = { - 'contract': '合同管理', - 'record': '案卷智能评查', - 'model': '智慧法务大模型' -}; - -// 应用模块图标映射 -const APP_ICON_MAP: Record = { - 'contract': 'ri-file-list-2-fill', - 'record': 'ri-folder-shared-fill', - 'model': 'ri-robot-2-fill' -}; - -export function Sidebar({ onToggle, collapsed, userRole, selectedApp = 'contract' }: SidebarProps) { - const location = useLocation(); - const [expandedMenus, setExpandedMenus] = useState>({}); - const [currentApp, setCurrentApp] = useState(selectedApp); - const navigate = useNavigate(); - - // 组件挂载后从 sessionStorage 读取初始 reviewType - useEffect(() => { - try { - const reviewType = sessionStorage.getItem('reviewType'); - // console.log('初始 reviewType:', reviewType); - if (reviewType) { - setCurrentApp(reviewType); - } - } catch (error) { - console.error('读取 reviewType 失败:', error); - } - }, []); - - // 从 sessionStorage 获取 reviewType 并设置当前应用模块 - useEffect(() => { - // 监听 sessionStorage 变化(主要用于多标签页情况) - const handleStorageChange = (e: StorageEvent) => { - if (e.key === 'reviewType' && e.newValue) { - setCurrentApp(e.newValue); - } - }; - - // 添加事件监听器 - window.addEventListener('storage', handleStorageChange); - - return () => { - window.removeEventListener('storage', handleStorageChange); - }; - }, []); - - // 监听路由变化,重新检查 reviewType - useEffect(() => { - try { - const reviewType = sessionStorage.getItem('reviewType'); - // console.log('路由变化, 检查 reviewType:', reviewType, '路径:', location.pathname); - if (reviewType) { - setCurrentApp(reviewType); - } - } catch (error) { - console.error('路由变化时读取 reviewType 失败:', error); - } - }, [location.pathname]); - - // 监听 selectedApp 属性变化 - useEffect(() => { - if (selectedApp) { - setCurrentApp(selectedApp); - } - }, [selectedApp]); - - const menuItems: MenuItem[] = [ - { - id: 'home', - title: '系统概览', - path: '/home', - icon: 'ri-home-line' - }, - { - id: 'chat-with-llm', - title: 'AI对话', - path: '/chat-with-llm', - icon: 'ri-chat-smile-2-line' - }, - { - id: 'file-management', - title: '文件管理', - path: '/files', - icon: 'ri-folder-line', - children: [ - { - id: 'file-upload', - title: '文件上传', - path: '/files/upload', - icon: 'ri-upload-cloud-line' - }, - { - id: 'documents', - title: '文档列表', - path: '/documents', - icon: 'ri-file-list-3-line' - } - ] - }, - { - id: 'rule-management', - title: '评查规则库', - path: '/rules', - icon: 'ri-book-3-line', - children: [ - { - id: 'rule-groups', - title: '评查点分组', - path: '/rule-groups', - icon: 'ri-folder-open-line' - }, - { - id: 'rules-list', - title: '评查点列表', - path: '/rules', - icon: 'ri-list-check-3' - }, - { - id: 'rules-file', - title: '评查文件列表', - path: '/rules-files', - icon: 'ri-list-check-2' - }, - // { - // id: 'rule-new', - // title: '新增评查点', - // path: '/rules-new', - // requiredRole: 'developer', - // icon: 'ri-add-circle-line' - // }, - // { - // id: 'review-detail', - // title: '评查详情', - // path: '/reviews', - // icon: 'ri-file-chart-line' - // } - ] - }, - { - id: 'contract-template', - title: '合同模板', - path: '/contract-template', - icon: 'ri-file-search-line', - children: [ - { - id: 'contract-search-ai', - title: '智能搜索', - path: '/contract-template/search', - icon: 'ri-search-line' - }, - { - id: 'contract-list', - title: '合同列表', - path: '/contract-template/list', - icon: 'ri-folder-line' - } - ] - }, - { - id: 'system-settings', - title: '系统设置', - path: '/settings', - icon: 'ri-settings-4-line', - requiredRole: 'developer', - children: [ - { - id: 'config-lists', - title: '配置列表', - path: '/config-lists', - icon: 'ri-list-check-3', - requiredRole: 'developer' - }, - // { - // id: 'basic-settings', - // title: '基础设置', - // path: '/settings', - // icon: 'ri-equalizer-line' - // }, - { - id: 'document-types', - title: '文档类型', - path: '/document-types', - icon: 'ri-file-list-line', - requiredRole: 'developer' - }, - { - id: 'prompt-management', - title: '提示词管理', - path: '/prompts', - icon: 'ri-chat-1-line', - requiredRole: 'developer' - } - ] - } - ]; - - // 初始化展开状态,默认全部展开 - useEffect(() => { - const initialExpandedState: Record = {}; - menuItems.forEach(item => { - if (item.children) { - initialExpandedState[item.id] = true; - } - }); - setExpandedMenus(initialExpandedState); - }, []); - - const toggleMenu = (id: string, e: React.MouseEvent) => { - // 我们只防止事件冒泡,不阻止默认行为 - e.stopPropagation(); - - // console.log('父菜单展开/折叠:', id); - - setExpandedMenus(prev => ({ - ...prev, - [id]: !prev[id] - })); - }; - - const isActive = (path: string) => { - return location.pathname === path || location.pathname.startsWith(`${path}/`); - }; - - // 处理侧边栏切换事件 - const handleToggleSidebar = (e: React.MouseEvent) => { - // console.log('侧边栏折叠/展开'); - // 只防止事件冒泡,不阻止默认行为 - e.stopPropagation(); - onToggle(); - }; - - // 处理子菜单项点击事件 - const handleSubMenuClick = (child: MenuItem, e: React.MouseEvent) => { - // 只需要阻止冒泡,不阻止默认行为 - e.stopPropagation(); - // console.log('子菜单点击:', child.title, '路径:', child.path); - }; - - // 获取当前应用模式下应显示的菜单ID列表 - const visibleMenuIds = APP_MENU_MAP[currentApp as keyof typeof APP_MENU_MAP] || APP_MENU_MAP['contract']; - // console.log('当前应用模式:', currentApp, '可见菜单ID:', visibleMenuIds); - - // 根据用户角色和当前应用模式过滤菜单项 - const filteredMenuItems = menuItems.filter(item => { - // 如果菜单项需要特定角色但用户没有 - if (item.requiredRole && item.requiredRole !== userRole) { - return false; - } - - // 检查当前菜单是否在所选应用模式中显示 - if (!visibleMenuIds.includes(item.id)) { - return false; - } - - return true; - }); - - return ( -
-
-
{ - navigate('/'); - }} - role="button" - tabIndex={0} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - navigate('/'); - } - }} - > - 智慧法务 - {!collapsed &&

智慧法务

} -
- -
- - {!collapsed && ( -
-
- - {APP_NAME_MAP[currentApp] || '合同管理'} -
-
- )} - -
- {filteredMenuItems.map((item) => ( -
- {!item.children ? ( - { - // 只阻止冒泡,不阻止默认行为 - e.stopPropagation(); - // console.log('单级菜单点击:', item.title, '路径:', item.path); - }} - > - - {!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); - } - }} - > -
- - {!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}} - - ))} -
- )} - - )} -
- ))} -
-
- ); +import { useState, useEffect } from 'react'; +import { Link, useLocation, useNavigate } from '@remix-run/react'; +import type { UserRole } from '~/root'; + +interface MenuItem { + id: string; + title: string; + path: string; + icon: string; + hideBreadcrumb?: boolean; + requiredRole?: UserRole; + children?: MenuItem[]; +} + +interface SidebarProps { + onToggle: () => void; + collapsed: boolean; + userRole: UserRole; + selectedApp?: string; // 添加所选应用模块参数 +} + +// 定义不同应用模块下显示的菜单项ID +const APP_MENU_MAP = { + 'contract': ['home', 'contract-template', 'file-management', 'rule-management', 'system-settings'], + 'record': ['home', 'file-management', 'rule-management', 'system-settings'], + 'model': ['chat-with-llm'] +}; + +// 应用模块名称映射 +const APP_NAME_MAP: Record = { + 'contract': '合同管理', + 'record': '案卷智能评查', + 'model': '智慧法务大模型' +}; + +// 应用模块图标映射 +const APP_ICON_MAP: Record = { + 'contract': 'ri-file-list-2-fill', + 'record': 'ri-folder-shared-fill', + 'model': 'ri-robot-2-fill' +}; + +export function Sidebar({ onToggle, collapsed, userRole, selectedApp = 'contract' }: SidebarProps) { + const location = useLocation(); + const [expandedMenus, setExpandedMenus] = useState>({}); + const [currentApp, setCurrentApp] = useState(selectedApp); + const navigate = useNavigate(); + + // 组件挂载后从 sessionStorage 读取初始 reviewType + useEffect(() => { + try { + const reviewType = sessionStorage.getItem('reviewType'); + // console.log('初始 reviewType:', reviewType); + if (reviewType) { + setCurrentApp(reviewType); + } + } catch (error) { + console.error('读取 reviewType 失败:', error); + } + }, []); + + // 从 sessionStorage 获取 reviewType 并设置当前应用模块 + useEffect(() => { + // 监听 sessionStorage 变化(主要用于多标签页情况) + const handleStorageChange = (e: StorageEvent) => { + if (e.key === 'reviewType' && e.newValue) { + setCurrentApp(e.newValue); + } + }; + + // 添加事件监听器 + window.addEventListener('storage', handleStorageChange); + + return () => { + window.removeEventListener('storage', handleStorageChange); + }; + }, []); + + // 监听路由变化,重新检查 reviewType + useEffect(() => { + try { + const reviewType = sessionStorage.getItem('reviewType'); + // console.log('路由变化, 检查 reviewType:', reviewType, '路径:', location.pathname); + if (reviewType) { + setCurrentApp(reviewType); + } + } catch (error) { + console.error('路由变化时读取 reviewType 失败:', error); + } + }, [location.pathname]); + + // 监听 selectedApp 属性变化 + useEffect(() => { + if (selectedApp) { + setCurrentApp(selectedApp); + } + }, [selectedApp]); + + const menuItems: MenuItem[] = [ + { + id: 'home', + title: '系统概览', + path: '/home', + icon: 'ri-home-line' + }, + { + id: 'chat-with-llm', + title: 'AI对话', + path: '/chat-with-llm', + icon: 'ri-chat-smile-2-line' + }, + { + id: 'file-management', + title: '文件管理', + path: '/files', + icon: 'ri-folder-line', + children: [ + { + id: 'file-upload', + title: '文件上传', + path: '/files/upload', + icon: 'ri-upload-cloud-line' + }, + { + id: 'documents', + title: '文档列表', + path: '/documents', + icon: 'ri-file-list-3-line' + } + ] + }, + { + id: 'rule-management', + title: '评查规则库', + path: '/rules', + icon: 'ri-book-3-line', + children: [ + { + id: 'rule-groups', + title: '评查点分组', + path: '/rule-groups', + icon: 'ri-folder-open-line' + }, + { + id: 'rules-list', + title: '评查点列表', + path: '/rules', + icon: 'ri-list-check-3' + }, + { + id: 'rules-file', + title: '评查文件列表', + path: '/rules-files', + icon: 'ri-list-check-2' + }, + // { + // id: 'rule-new', + // title: '新增评查点', + // path: '/rules-new', + // requiredRole: 'developer', + // icon: 'ri-add-circle-line' + // }, + // { + // id: 'review-detail', + // title: '评查详情', + // path: '/reviews', + // icon: 'ri-file-chart-line' + // } + ] + }, + { + id: 'contract-template', + title: '合同模板', + path: '/contract-template', + icon: 'ri-file-search-line', + children: [ + { + id: 'contract-search-ai', + title: '智能搜索', + path: '/contract-template/search', + icon: 'ri-search-line' + }, + { + id: 'contract-list', + title: '合同列表', + path: '/contract-template/list', + icon: 'ri-folder-line' + } + ] + }, + { + id: 'system-settings', + title: '系统设置', + path: '/settings', + icon: 'ri-settings-4-line', + requiredRole: 'developer', + children: [ + { + id: 'config-lists', + title: '配置列表', + path: '/config-lists', + icon: 'ri-list-check-3', + requiredRole: 'developer' + }, + // { + // id: 'basic-settings', + // title: '基础设置', + // path: '/settings', + // icon: 'ri-equalizer-line' + // }, + { + id: 'document-types', + title: '文档类型', + path: '/document-types', + icon: 'ri-file-list-line', + requiredRole: 'developer' + }, + { + id: 'prompt-management', + title: '提示词管理', + path: '/prompts', + icon: 'ri-chat-1-line', + requiredRole: 'developer' + } + ] + } + ]; + + // 初始化展开状态,默认全部展开 + useEffect(() => { + const initialExpandedState: Record = {}; + menuItems.forEach(item => { + if (item.children) { + initialExpandedState[item.id] = true; + } + }); + setExpandedMenus(initialExpandedState); + }, []); + + const toggleMenu = (id: string, e: React.MouseEvent) => { + // 我们只防止事件冒泡,不阻止默认行为 + e.stopPropagation(); + + // console.log('父菜单展开/折叠:', id); + + setExpandedMenus(prev => ({ + ...prev, + [id]: !prev[id] + })); + }; + + const isActive = (path: string) => { + return location.pathname === path || location.pathname.startsWith(`${path}/`); + }; + + // 处理侧边栏切换事件 + const handleToggleSidebar = (e: React.MouseEvent) => { + // console.log('侧边栏折叠/展开'); + // 只防止事件冒泡,不阻止默认行为 + e.stopPropagation(); + onToggle(); + }; + + // 处理子菜单项点击事件 + const handleSubMenuClick = (child: MenuItem, e: React.MouseEvent) => { + // 只需要阻止冒泡,不阻止默认行为 + e.stopPropagation(); + // console.log('子菜单点击:', child.title, '路径:', child.path); + }; + + // 获取当前应用模式下应显示的菜单ID列表 + const visibleMenuIds = APP_MENU_MAP[currentApp as keyof typeof APP_MENU_MAP] || APP_MENU_MAP['contract']; + // console.log('当前应用模式:', currentApp, '可见菜单ID:', visibleMenuIds); + + // 根据用户角色和当前应用模式过滤菜单项 + const filteredMenuItems = menuItems.filter(item => { + // 如果菜单项需要特定角色但用户没有 + if (item.requiredRole && item.requiredRole !== userRole) { + return false; + } + + // 检查当前菜单是否在所选应用模式中显示 + if (!visibleMenuIds.includes(item.id)) { + return false; + } + + return true; + }); + + return ( +
+
+
{ + navigate('/'); + }} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + navigate('/'); + } + }} + > + 智慧法务 + {!collapsed &&

智慧法务

} +
+ +
+ + {!collapsed && ( +
+
+ + {APP_NAME_MAP[currentApp] || '合同管理'} +
+
+ )} + +
+ {filteredMenuItems.map((item) => ( +
+ {!item.children ? ( + { + // 只阻止冒泡,不阻止默认行为 + e.stopPropagation(); + // console.log('单级菜单点击:', item.title, '路径:', item.path); + }} + > + + {!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); + } + }} + > +
+ + {!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}} + + ))} +
+ )} + + )} +
+ ))} +
+
+ ); } \ No newline at end of file diff --git a/app/root.tsx b/app/root.tsx index fb6e777..a73da21 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,271 +1,271 @@ -// import React from 'react'; -import { - Links, - // LiveReload, // 不再需要,使用Vite时会与内置HMR冲突 - Meta, - Outlet, - Scripts, - ScrollRestoration, - isRouteErrorResponse, - useRouteError, - type MetaFunction, - useLoaderData -} from "@remix-run/react"; -import { - LoaderFunctionArgs, - redirect, - createCookieSessionStorage, - ActionFunctionArgs -} from "@remix-run/node"; -import { Layout } from "~/components/layout/Layout"; -import { ErrorBoundary as AppErrorBoundary } from "~/components/error/ErrorBoundary"; -import { MessageModalProvider } from "~/components/ui/MessageModal"; -import { ToastProvider } from "~/components/ui/Toast"; -import "remixicon/fonts/remixicon.css"; -// 导入样式 -import styles from "~/styles/main.css?url"; -import messageModalStyles from "~/styles/components/message-modal.css?url"; -import toastStyles from "~/styles/components/toast.css?url"; -import LoadingBarContainer from "~/components/ui/LoadingBar"; -import RouteChangeLoader from "~/components/ui/RouteChangeLoader"; -// import { useState, useEffect } from "react"; - -// 定义用户角色类型 -export type UserRole = 'common' | 'developer'; - -// 定义需要高级权限的路径 -export const developerOnlyPaths = [ - '/settings', - '/config-lists', - '/document-types', - '/prompts' -]; - -// 创建基于Cookie的会话存储 -// 在实际应用中,应该使用环境变量来设置密钥 -const sessionStorage = createCookieSessionStorage({ - cookie: { - name: "__session", - httpOnly: true, - path: "/", - sameSite: "lax", - secrets: ["s3cr3t"], // 应该从环境变量读取 - secure: process.env.NODE_ENV === "production", - }, -}); - -// 获取会话对象 -export async function getSession(request: Request) { - const cookie = request.headers.get("Cookie"); - return sessionStorage.getSession(cookie); -} - -// 获取用户登录状态 -export async function getUserSession(request: Request) { - const session = await getSession(request); - return { - isAuthenticated: session.get("isAuthenticated") === true, - userRole: session.get("userRole") || 'common' as UserRole - }; -} - -// 创建登录会话 -export async function createUserSession(isAuthenticated: boolean, userRole: UserRole, redirectTo: string) { - const session = await sessionStorage.getSession(); - session.set("isAuthenticated", isAuthenticated); - session.set("userRole", userRole); - console.log("session-----", session.get("userRole")); - return redirect(redirectTo, { - headers: { - "Set-Cookie": await sessionStorage.commitSession(session), - }, - }); -} - -// 销毁会话(登出) -export async function logout(request: Request) { - const session = await getSession(request); - - return redirect("/login", { - headers: { - "Set-Cookie": await sessionStorage.destroySession(session), - }, - }); -} - -// 添加action处理登录/登出请求 -export async function action({ request }: ActionFunctionArgs) { - const formData = await request.formData(); - const intent = formData.get("intent"); - - if (intent === "logout") { - return logout(request); - } - - return null; -} - -// 添加loader函数进行全局认证检查并传递环境变量给客户端 -export async function loader({ request }: LoaderFunctionArgs) { - // 获取当前路径 - const url = new URL(request.url); - const pathname = url.pathname; - - // 排除不需要登录验证的路径 - const publicPaths = ['/login', '/favicon.ico']; - const isPublicPath = publicPaths.some(path => pathname.startsWith(path)); - - // 获取用户会话 - const { isAuthenticated, userRole } = await getUserSession(request); - // console.log("Auth status:", { isAuthenticated, userRole, pathname }); - - // 如果访问需要认证的路径但未登录,重定向到登录页 - if (!isPublicPath && !isAuthenticated) { - // 保存请求的URL,以便登录后重定向回来 - const session = await getSession(request); - - // 如果路径是/home,则将重定向目标设置为/ - const redirectTarget = pathname === "/home" ? "/" : pathname; - // 保存重定向目标 - session.set("redirectTo", redirectTarget); - - return redirect("/login", { - headers: { - "Set-Cookie": await sessionStorage.commitSession(session), - }, - }); - } - - // 如果已登录且访问登录页,重定向到首页 - if (pathname === "/login" && isAuthenticated) { - // console.log("Already authenticated, redirecting from login to /"); - return redirect("/"); - } - - // 检查访问权限 - 如果是common用户访问了开发者专属页面,重定向到首页 - if (userRole === 'common' && developerOnlyPaths.some(path => pathname.startsWith(path))) { - return redirect("/"); - } - - // 向组件传递认证状态、当前路径和环境变量 - return Response.json({ - isAuthenticated, - userRole, - pathname, - ENV: { - NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, - NEXT_PUBLIC_APP_ID: process.env.NEXT_PUBLIC_APP_ID, - NEXT_PUBLIC_APP_KEY: process.env.NEXT_PUBLIC_APP_KEY, - }, - }); -} - - -export const meta: MetaFunction = () => { - return [ - { charSet: "utf-8" }, - { name: "viewport", content: "width=device-width,initial-scale=1" }, - { title: "中国烟草AI合同及卷宗审核系统" }, - { name: "description", content: "专业的AI合同及卷宗评查系统,提供智能审核、风险评估和规范化建议" }, - { name: "robots", content: "noindex,nofollow" } // 内部系统,防止被搜索引擎索引 - ]; -}; - -// 使用links函数为应用加载CSS和其他资源 -export function links() { - return [ - { rel: "stylesheet", href: styles }, - { rel: "stylesheet", href: messageModalStyles }, - { rel: "stylesheet", href: toastStyles }, - // 添加 Antd 样式 - { rel: "stylesheet", href: "https://cdn.jsdelivr.net/npm/antd@5/dist/reset.css" }, - { rel: "icon", type: "image/svg+xml", href: "/logo.svg" }, - // { rel: "preconnect", href: "https://fonts.googleapis.com" }, - // { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }, - // { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" } - ]; -} - -export default function App() { - const { userRole, ENV } = useLoaderData(); - - - return ( - - - - -