223 lines
8.4 KiB
TypeScript
223 lines
8.4 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import type { MenuItem } from '~/api/auth/user-routes';
|
|
import { Sidebar } from './Sidebar';
|
|
// import { Header } from './Header';
|
|
import { Breadcrumb } from './Breadcrumb';
|
|
import { useMatches, useLocation } from '@remix-run/react';
|
|
import type { UserRole } from '~/root';
|
|
|
|
interface LayoutProps {
|
|
children: React.ReactNode;
|
|
userRole?: UserRole;
|
|
frontendJWT?: string;
|
|
isMobile?: boolean; // 是否为移动端设备(服务端通过 User-Agent 检测)
|
|
menuItems?: MenuItem[];
|
|
}
|
|
|
|
// 添加一个接口表示路由handle可能包含的属性
|
|
interface RouteHandle {
|
|
hideBreadcrumb?: boolean;
|
|
collapseSidebar?: boolean;
|
|
noPadding?: boolean;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface Match {
|
|
handle?: RouteHandle;
|
|
pathname: string;
|
|
data: unknown;
|
|
}
|
|
|
|
type RulesTestDetailData = {
|
|
pack?: {
|
|
documentType?: string;
|
|
mainType?: string;
|
|
businessType?: string;
|
|
fields?: unknown[];
|
|
subDocuments?: unknown[];
|
|
visualElements?: unknown[];
|
|
};
|
|
};
|
|
|
|
export function Layout({ children, userRole = 'developer' as UserRole, frontendJWT = '', isMobile = false, menuItems = [] }: LayoutProps) {
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
const [effectiveUserRole, setEffectiveUserRole] = useState<UserRole>(userRole);
|
|
const [effectiveFrontendJWT, setEffectiveFrontendJWT] = useState<string>(frontendJWT);
|
|
const matches = useMatches() as Match[];
|
|
const location = useLocation();
|
|
|
|
// 检查当前路径是否应该隐藏侧边栏
|
|
const noLayoutPaths = ['/login', '/'];
|
|
// 移动端设备强制隐藏侧边栏
|
|
const shouldHideSidebar = isMobile || noLayoutPaths.includes(location.pathname);
|
|
|
|
// 检查当前路由是否应该隐藏默认面包屑
|
|
// 移动端设备强制隐藏面包屑(避免显示首页链接)
|
|
const shouldHideBreadcrumb = isMobile || shouldHideSidebar || matches.some(match =>
|
|
match.handle && match.handle.hideBreadcrumb === true
|
|
);
|
|
|
|
// 检查当前路由是否要求无 padding
|
|
const shouldNoPadding = matches.some(match =>
|
|
match.handle && match.handle.noPadding === true
|
|
);
|
|
|
|
// 从 localStorage 读取用户信息和 JWT 作为备用方案
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined') return;
|
|
|
|
try {
|
|
// 如果服务端没有传递 userRole,从 localStorage 读取
|
|
if (!userRole || userRole === '') {
|
|
const storedUserInfoStr = localStorage.getItem('user_info');
|
|
if (storedUserInfoStr) {
|
|
const storedUserInfo = JSON.parse(storedUserInfoStr);
|
|
const storedUserRole = storedUserInfo.user_role || 'common';
|
|
console.log('📖 [Layout] 从 localStorage 读取用户角色:', storedUserRole);
|
|
setEffectiveUserRole(storedUserRole as UserRole);
|
|
}
|
|
} else {
|
|
setEffectiveUserRole(userRole);
|
|
}
|
|
|
|
// 如果服务端没有传递 frontendJWT,从 localStorage 读取
|
|
if (!frontendJWT || frontendJWT === '') {
|
|
const storedToken = localStorage.getItem('access_token');
|
|
if (storedToken) {
|
|
console.log('📖 [Layout] 从 localStorage 读取 JWT token');
|
|
setEffectiveFrontendJWT(storedToken);
|
|
}
|
|
} else {
|
|
setEffectiveFrontendJWT(frontendJWT);
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ [Layout] 读取 localStorage 失败:', error);
|
|
}
|
|
}, [userRole, frontendJWT]);
|
|
|
|
// 从localStorage中获取侧边栏状态
|
|
useEffect(() => {
|
|
// 检查是否为移动端
|
|
const isMobile = window.innerWidth <= 768;
|
|
|
|
// 检查当前路由是否要求收缩侧边栏
|
|
const shouldCollapse = matches.some(match =>
|
|
match.handle && match.handle.collapseSidebar === true
|
|
);
|
|
|
|
if (isMobile) {
|
|
setSidebarCollapsed(true);
|
|
} else if (shouldCollapse) {
|
|
setSidebarCollapsed(true);
|
|
} else {
|
|
// 从localStorage获取侧边栏状态
|
|
const savedState = localStorage.getItem('sidebarCollapsed');
|
|
if (savedState) {
|
|
setSidebarCollapsed(savedState === 'true');
|
|
}
|
|
}
|
|
}, [location.pathname]);
|
|
|
|
const toggleSidebar = () => {
|
|
const newState = !sidebarCollapsed;
|
|
setSidebarCollapsed(newState);
|
|
localStorage.setItem('sidebarCollapsed', String(newState));
|
|
};
|
|
|
|
// 切换应用模块
|
|
// const changeAppModule = (appId: AppModule) => {
|
|
// setSelectedApp(appId);
|
|
// localStorage.setItem('selectedApp', appId);
|
|
// };
|
|
|
|
// 如果是无布局页面,只渲染内容
|
|
if (shouldHideSidebar) {
|
|
return <>{children}</>;
|
|
}
|
|
|
|
const isRulesTestDetail = location.pathname.startsWith('/rulesTest/detail');
|
|
const isRulesTestTopbarPage = isRulesTestDetail;
|
|
const rulesTestDetailData = matches.find(match => match.pathname.startsWith('/rulesTest/detail'))?.data as RulesTestDetailData | undefined;
|
|
const detailPack = rulesTestDetailData?.pack;
|
|
const detailPackFilterMainType = detailPack?.businessType || detailPack?.mainType || '';
|
|
const showFieldNav = (detailPack?.fields?.length || 0) > 0;
|
|
const showSubDocumentNav = (detailPack?.subDocuments?.length || 0) > 0;
|
|
const showVisualNav = (detailPack?.visualElements?.length || 0) > 0;
|
|
const rulesListHref = detailPack?.documentType
|
|
? `/rulesTest/list?documentType=${encodeURIComponent(detailPack.documentType)}${detailPackFilterMainType ? `&mainType=${encodeURIComponent(detailPackFilterMainType)}` : ''}`
|
|
: '/rulesTest/list';
|
|
|
|
return (
|
|
<div className="layout-container">
|
|
{/* 侧边栏始终保留,不再使用条件渲染 */}
|
|
<Sidebar
|
|
collapsed={sidebarCollapsed}
|
|
onToggle={toggleSidebar}
|
|
userRole={effectiveUserRole}
|
|
frontendJWT={effectiveFrontendJWT}
|
|
menuItems={menuItems}
|
|
/>
|
|
|
|
{/* 规则详情页顶部栏 */}
|
|
{isRulesTestDetail && (
|
|
<div className={`page-topbar ${sidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
|
|
<div className="topbar-content">
|
|
<div className="topbar-left">
|
|
<span className="topbar-icon" aria-hidden="true">
|
|
<i className="ri-file-settings-line"></i>
|
|
</span>
|
|
<div className="topbar-heading">
|
|
<h2 className="topbar-title">规则配置详情</h2>
|
|
<span className="topbar-breadcrumb">
|
|
<a href={rulesListHref}>规则列表</a>
|
|
<span className="separator">/</span>
|
|
<span>配置详情</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="topbar-right">
|
|
<a className="topbar-action" href={rulesListHref}>
|
|
<i className="ri-arrow-left-line"></i>
|
|
<span>返回列表</span>
|
|
</a>
|
|
<a className="topbar-action secondary" href="/rules/list">
|
|
<i className="ri-history-line"></i>
|
|
<span>查看旧版本</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div className="topbar-nav">
|
|
{showFieldNav && <a className="topbar-nav-link" href="#fields"><i className="ri-input-field"></i>字段抽取</a>}
|
|
{showSubDocumentNav && <a className="topbar-nav-link" href="#sub-documents"><i className="ri-file-list-3-line"></i>案卷文书</a>}
|
|
{showVisualNav && <a className="topbar-nav-link" href="#visual-elements"><i className="ri-stamp-line"></i>视觉要素</a>}
|
|
<a className="topbar-nav-link" href="#rules"><i className="ri-list-check-3"></i>评查规则</a>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className={`main-content ${sidebarCollapsed ? 'sidebar-collapsed' : ''} ${isRulesTestDetail ? 'rules-detail-main' : ''}`}>
|
|
{/* 应用模块选择器 */}
|
|
{/* <div className="app-module-selector py-2 px-4 border-b border-gray-100 flex items-center">
|
|
{APP_MODULES.map(app => (
|
|
<button
|
|
key={app.id}
|
|
onClick={() => changeAppModule(app.id as AppModule)}
|
|
className={`app-module-btn mr-4 py-2 px-4 rounded-md flex items-center ${
|
|
selectedApp === app.id ? 'bg-green-50 text-green-700 border border-green-200' : 'hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
<i className={`${app.icon} mr-2`}></i>
|
|
<span>{app.name}</span>
|
|
</button>
|
|
))}
|
|
</div> */}
|
|
|
|
<div className={`content-container${shouldNoPadding ? ' !p-0' : ''}`}>
|
|
{!shouldHideBreadcrumb && !isRulesTestTopbarPage && <Breadcrumb />}
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|