优化数据隔离,进行权限控制

This commit is contained in:
2025-06-03 15:17:09 +08:00
parent 15ef4a3ced
commit 057563ba5e
10 changed files with 244 additions and 94 deletions
+15
View File
@@ -68,6 +68,21 @@ export function Layout({ children, userRole = 'developer' }: LayoutProps) {
}
}, []);
// 路由变化时,检查并更新应用模块
useEffect(() => {
if (typeof window !== 'undefined') {
try {
const reviewType = sessionStorage.getItem('reviewType');
console.log('Layout 路由变化, reviewType:', reviewType, '路径:', location.pathname);
if (reviewType && REVIEW_TYPE_TO_APP[reviewType]) {
setSelectedApp(REVIEW_TYPE_TO_APP[reviewType]);
}
} catch (error) {
console.error('路由变化时获取reviewType失败:', error);
}
}
}, [location.pathname]);
const toggleSidebar = () => {
const newState = !sidebarCollapsed;
setSidebarCollapsed(newState);
+55 -51
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Link, useLocation } from '@remix-run/react';
import { Link, useLocation, useNavigate } from '@remix-run/react';
import type { UserRole } from '~/root';
interface MenuItem {
@@ -44,31 +44,24 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = 'contract
const location = useLocation();
const [expandedMenus, setExpandedMenus] = useState<Record<string, boolean>>({});
const [currentApp, setCurrentApp] = useState<string>(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(() => {
// 初始加载时获取 reviewType
const updateReviewType = () => {
if (typeof window !== 'undefined') {
const reviewType = sessionStorage.getItem('reviewType');
if (reviewType) {
setCurrentApp(reviewType);
}
}
};
// 首次执行
updateReviewType();
// 设置轮询,每秒检查一次 reviewType 变化
const intervalId = setInterval(updateReviewType, 1000);
// 添加自定义事件监听
const handleReviewTypeChange = () => {
updateReviewType();
};
// 监听 sessionStorage 变化
// 监听 sessionStorage 变化(主要用于多标签页情况)
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'reviewType' && e.newValue) {
setCurrentApp(e.newValue);
@@ -76,23 +69,23 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = 'contract
};
// 添加事件监听器
window.addEventListener('reviewTypeChange', handleReviewTypeChange);
window.addEventListener('storage', handleStorageChange);
return () => {
clearInterval(intervalId);
window.removeEventListener('reviewTypeChange', handleReviewTypeChange);
window.removeEventListener('storage', handleStorageChange);
};
}, []);
// 监听路由变化,重新检查 reviewType
useEffect(() => {
if (typeof window !== 'undefined') {
try {
const reviewType = sessionStorage.getItem('reviewType');
// console.log('路由变化, 检查 reviewType:', reviewType, '路径:', location.pathname);
if (reviewType) {
setCurrentApp(reviewType);
}
} catch (error) {
console.error('路由变化时读取 reviewType 失败:', error);
}
}, [location.pathname]);
@@ -110,26 +103,6 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = 'contract
path: '/home',
icon: 'ri-home-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: 'file-management',
title: '文件管理',
@@ -178,6 +151,7 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = 'contract
id: 'rule-new',
title: '新增评查点',
path: '/rules-new',
requiredRole: 'developer',
icon: 'ri-add-circle-line'
},
// {
@@ -188,6 +162,26 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = 'contract
// }
]
},
{
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: '系统设置',
@@ -270,6 +264,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'];
// console.log('当前应用模式:', currentApp, '可见菜单ID:', visibleMenuIds);
// 根据用户角色和当前应用模式过滤菜单项
const filteredMenuItems = menuItems.filter(item => {
@@ -289,7 +284,19 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = 'contract
return (
<div className={`sidebar ${collapsed ? 'collapsed' : ''}`}>
<div className="py-6 px-4 border-b border-gray-100 flex justify-between items-center">
<div className="flex items-center">
<div className="flex items-center"
onClick={() => {
navigate('/');
}}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
navigate('/');
}
}}
>
<img src="/logo.svg" alt="智慧法务" className="w-12 h-12 mr-2" />
{!collapsed && <h2 className="text-lg font-medium"></h2>}
</div>
@@ -309,9 +316,6 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = 'contract
<i className={`${APP_ICON_MAP[currentApp] || 'ri-file-list-2-fill'} mr-2 text-xl`}></i>
<span className="font-medium">{APP_NAME_MAP[currentApp] || '合同管理'}</span>
</div>
<div className="text-xs text-gray-500 mt-1">
: {APP_NAME_MAP[currentApp] || '合同管理'}
</div>
</div>
)}
+1
View File
@@ -168,6 +168,7 @@ export function links() {
{ rel: "stylesheet", href: styles },
{ rel: "stylesheet", href: messageModalStyles },
{ rel: "stylesheet", href: toastStyles },
{ 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" }
+16 -9
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { useNavigate, Form } from '@remix-run/react';
import { useNavigate, Form, useLoaderData } from '@remix-run/react';
import { type MetaFunction, type ActionFunctionArgs, LoaderFunctionArgs, redirect } from "@remix-run/node";
import styles from "~/styles/pages/home.css?url";
import dayjs from 'dayjs';
@@ -30,21 +30,27 @@ export async function action({ request }: ActionFunctionArgs) {
// 验证用户登录状态
export async function loader({ request }: LoaderFunctionArgs) {
const { isAuthenticated } = await getUserSession(request);
const { isAuthenticated, userRole } = await getUserSession(request);
if (!isAuthenticated) {
return redirect("/login");
}
return null;
return Response.json({ userRole });
}
export default function Index() {
const navigate = useNavigate();
const { userRole } = useLoaderData<typeof loader>();
const [currentDateTime, setCurrentDateTime] = useState({
date: '',
time: ''
});
// 打印服务器端传递的用户角色
useEffect(() => {
console.log('_index 服务器返回的用户角色:', userRole);
}, [userRole]);
// 更新日期时间
useEffect(() => {
const updateDateTime = () => {
@@ -83,10 +89,8 @@ export default function Index() {
// 处理登出
const handleLogout = () => {
// 清除sessionStorage中的用户角色信息
// 清除sessionStorage中的所有数据
if (typeof window !== 'undefined') {
sessionStorage.removeItem('userRole');
// 可以根据需要清除其他会话数据
sessionStorage.clear();
}
@@ -94,6 +98,9 @@ export default function Index() {
const form = document.getElementById('logout-form') as HTMLFormElement;
if (form) {
form.submit();
} else {
// 如果找不到表单,直接导航到登录页
navigate('/login');
}
};
@@ -117,7 +124,7 @@ export default function Index() {
<span className="datetime">{currentDateTime.date} {currentDateTime.time}</span>
<div className="user">
<img src="/avatar.png" alt="用户头像" className="avatar" />
<span className="username"></span>
<span className="username">{userRole === 'developer' ? '系统管理员' : '普通用户'}</span>
<button
onClick={handleLogout}
className="logout-button"
@@ -163,8 +170,8 @@ export default function Index() {
{/* 智慧法务大模型模块 */}
<div
className="module-card"
onClick={() => handleModuleClick('/prompts', 'model')}
onKeyDown={(e) => handleKeyDown('/prompts', 'model', e)}
onClick={() => handleModuleClick('/', 'model')}
onKeyDown={(e) => handleKeyDown('/', 'model', e)}
role="button"
tabIndex={0}
aria-label="智慧法务大模型"
+17 -2
View File
@@ -22,10 +22,12 @@ import {
DocumentStatus
} from "~/api/files/files-upload";
import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files";
import { links as fileTypeTagLinks } from "~/components/ui/FileTypeTag";
export function links() {
return [
{ rel: "stylesheet", href: uploadStyles }
{ rel: "stylesheet", href: uploadStyles },
...fileTypeTagLinks()
];
}
@@ -1446,8 +1448,21 @@ export default function FilesUpload() {
width: "15%",
render: (_: unknown, record: Document) => {
const typeName = getDocumentTypeName(record.type_id);
// 根据typeName判断应用哪种样式类名
let typeClass = "file-type-badge";
if (typeName.includes('合同')) {
typeClass += " file-type-tag-contract";
} else if (typeName.includes('许可') || typeName.includes('行政许可')) {
typeClass += " file-type-tag-license-doc";
} else if (typeName.includes('处罚') || typeName.includes('行政处罚')) {
typeClass += " file-type-tag-punishment-doc";
} else {
typeClass += " file-type-tag-other";
}
return (
<span className="file-type-badge">
<span className={typeClass}>
{typeName}
</span>
);
+69 -14
View File
@@ -1,6 +1,6 @@
// import React from 'react';
import { type MetaFunction } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { useLoaderData, useNavigate, Form } from "@remix-run/react";
import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button";
import { FileTag, links as fileTagLinks } from "~/components/ui/FileTag";
@@ -11,6 +11,9 @@ import { getDocuments, type DocumentUI, type DocumentSearchParams } from "~/api/
import { useState, useEffect } from "react";
import { getHomeData } from "~/api/home/home";
import dayjs from 'dayjs';
import type { UserRole } from '~/root';
import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
import { logout, getUserSession } from "~/root";
// import { getUserSession } from "~/root";
// 文件处理状态选项
@@ -43,15 +46,11 @@ export const meta: MetaFunction = () => {
// }
// 添加认证检查
export async function loader() {
// 检查用户登录状态
// const { isAuthenticated } = await getUserSession(request);
// if (!isAuthenticated) {
// return redirect("/login");
// }
export async function loader({ request }: LoaderFunctionArgs) {
try {
// 从根loader获取用户角色
const { userRole } = await getUserSession(request);
// 返回默认值,实际数据将在客户端根据 sessionStorage 加载
return Response.json({
homeData: {
@@ -64,7 +63,8 @@ export async function loader() {
issuesGrowth: { value: 0, isUp: true }
},
recentFiles: [],
reviewType: null
reviewType: null,
userRole: userRole
});
} catch (error) {
// 错误处理
@@ -76,8 +76,21 @@ export async function loader() {
}
}
// 处理登出请求
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "logout") {
return logout(request);
}
return null;
}
export default function Home() {
const { homeData: initialHomeData, recentFiles: initialRecentFiles } = useLoaderData<typeof loader>();
const navigate = useNavigate();
const { homeData: initialHomeData, recentFiles: initialRecentFiles, userRole: serverUserRole } = useLoaderData<typeof loader>();
const [recentFiles, setRecentFiles] = useState<DocumentUI[]>(initialRecentFiles || []);
const [homeData, setHomeData] = useState(initialHomeData);
const [currentDateTime, setCurrentDateTime] = useState({
@@ -85,6 +98,12 @@ export default function Home() {
time: ''
});
const [isLoading, setIsLoading] = useState(true);
const userRole = serverUserRole as UserRole;
// 打印服务器端传递的用户角色
useEffect(() => {
console.log('服务器返回的用户角色:', serverUserRole);
}, [serverUserRole]);
// 更新当前时间
useEffect(() => {
@@ -107,6 +126,27 @@ export default function Home() {
return () => clearInterval(timerID);
}, []);
// 处理登出操作
const handleLogout = () => {
// 清除sessionStorage中的所有数据
if (typeof window !== 'undefined') {
sessionStorage.removeItem('userRole');
sessionStorage.removeItem('reviewType');
sessionStorage.removeItem('previousReviewType');
// 可以根据需要清除其他会话数据
sessionStorage.clear();
}
// 使用Form组件提交登出请求
const form = document.getElementById('logout-form') as HTMLFormElement;
if (form) {
form.submit();
} else {
// 如果找不到表单,直接导航到登录页
navigate('/login');
}
};
// 在客户端挂载时,根据 sessionStorage 中的 reviewType 加载正确的数据
useEffect(() => {
const loadData = async () => {
@@ -246,6 +286,11 @@ export default function Home() {
return (
<div className="dashboard-container">
{/* 登出表单 - 隐藏 */}
<Form method="post" id="logout-form" className="hidden">
<input type="hidden" name="intent" value="logout" />
</Form>
{/* 页面头部 */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-medium"></h2>
@@ -257,13 +302,23 @@ export default function Home() {
</div>
<div className="user-profile p-4 border-b border-gray-100 flex items-center">
<div className="avatar w-10 h-10 rounded-full bg-primary text-white flex items-center justify-center">
<span></span>
<span>{userRole === 'developer' ? '管' : '用'}</span>
</div>
<div className="ml-3">
<p className="text-sm font-medium"></p>
<p className="text-xs text-gray-500"></p>
<p className="text-sm font-medium">{userRole === 'developer' ? '系统管理员' : '普通用户'}</p>
<p className="text-xs text-gray-500">{userRole === 'developer' ? '超级管理员' : '标准权限'}</p>
</div>
</div>
{/* 登出操作 */}
<Button
type="default"
size="small"
className="ml-4 hover:bg-gray-100"
onClick={handleLogout}
>
<i className="ri-logout-box-line mr-1"></i>
</Button>
</div>
</div>
+19 -4
View File
@@ -28,6 +28,13 @@ export async function action({ request }: ActionFunctionArgs) {
if (!username || !password) {
return Response.json({ error: "用户名和密码不能为空" });
}
if (userRole === 'developer') {
if (username !== 'admin' || password !== 'admin') {
// toastService.error("管理员用户名或密码错误");
return Response.json({ error: "管理员用户名或密码错误" });
}
}
// 在实际应用中,这里应该是对用户名和密码的验证逻辑
// 简化起见,我们直接视为登录成功
@@ -61,6 +68,13 @@ export default function Login() {
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
// 使用 useEffect 确保错误提示只显示一次
// useEffect(() => {
// if(actionData?.error) {
// toastService.error(actionData.error);
// }
// }, [actionData?.error]);
// 判断是否正在提交表单
const isSubmitting = navigation.state === "submitting";
@@ -76,7 +90,10 @@ export default function Login() {
<h2 className="login-subtitle"></h2>
<Form method="post" className="login-form">
{actionData?.error && (
<div className="error-message">{actionData.error}</div>
<div className="error-message-container">
<div className="error-icon"><i className="ri-error-warning-line"></i></div>
<div className="error-text">{actionData.error}</div>
</div>
)}
<div className="form-group">
@@ -89,7 +106,6 @@ export default function Login() {
onChange={(e) => setUsername(e.target.value)}
className="form-input"
placeholder="请输入用户名"
required
/>
</div>
@@ -103,7 +119,6 @@ export default function Login() {
onChange={(e) => setPassword(e.target.value)}
className="form-input"
placeholder="请输入密码"
required
/>
</div>
@@ -118,7 +133,7 @@ export default function Login() {
required
>
<option value="common"></option>
<option value="developer"></option>
<option value="developer"></option>
</select>
</div>
+10 -14
View File
@@ -33,7 +33,7 @@ import { ReviewSettings } from "~/components/rules/new/ReviewSettings";
import { ActionButtons } from "~/components/rules/new/ActionButtons";
import { PageHeader } from "~/components/rules/new/PageHeader";
import rulesStyles from "~/styles/rules.css?url";
import { useNavigate, useLocation } from "@remix-run/react";
import { useNavigate, useLocation, useRouteLoaderData } from "@remix-run/react";
// 导入评查点模型定义和常量
import type {
EvaluationPoint,
@@ -153,12 +153,15 @@ export default function RuleNew() {
const [isEditMode, setIsEditMode] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [instanceKey, setInstanceKey] = useState<string>('new');
const [userRole, setUserRole] = useState<UserRole>('common');
// 从root路由获取用户角色,而不是从sessionStorage
const rootData = useRouteLoaderData("root") as { userRole: UserRole };
const userRole = rootData?.userRole || 'common';
const [formData, setFormData] = useState<EvaluationPoint>({});
const [evaluationPointGroups, setEvaluationPointGroups] = useState<EvaluationPointGroup[]>([]);
// 检查用户是否为开发者角色
const isDeveloper = userRole === 'developer';
// 判断表单是否为只读模式
const isReadOnly = userRole === 'common';
// 添加用于共享的字段数据状态
const [extractionFields, setExtractionFields] = useState<string[]>([]);
@@ -710,13 +713,6 @@ export default function RuleNew() {
const id = searchParams.get('id');
const mode = searchParams.get('mode');
// 从sessionStorage获取用户角色
if (typeof window !== 'undefined') {
const userRoleFromSession = sessionStorage.getItem('userRole') as UserRole || 'common';
// console.log("userRoleFromSession-----",userRoleFromSession);
setUserRole(userRoleFromSession);
}
// 编辑或复制模式下设置加载状态
if (id || mode === 'copy') {
setIsLoading(true);
@@ -746,9 +742,9 @@ export default function RuleNew() {
<div className="container">
{/* 页面标题和右上角保存按钮 */}
<PageHeader
title={isEditMode ? "编辑评查点" : "新增评查点"}
title={isEditMode ? (isReadOnly ? "查看评查点" : "编辑评查点") : "新增评查点"}
onSave={handleSave}
showSaveButton={isDeveloper}
showSaveButton={!isReadOnly}
/>
{/* 加载状态显示 */}
@@ -817,7 +813,7 @@ export default function RuleNew() {
onSave={handleSave}
onSaveDraft={handleSaveDraft}
isEditMode={isEditMode}
showButtons={isDeveloper}
showButtons={!isReadOnly}
/>
</div>
</RuleContext.Provider>
+30
View File
@@ -53,6 +53,36 @@
gap: 1.5rem;
}
/* 优化的错误提示样式 */
.error-message-container {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
background-color: #fef2f2;
border: 1px solid #fee2e2;
border-radius: 6px;
animation: fadeIn 0.3s ease-in-out;
}
.error-icon {
color: #ef4444;
font-size: 1.25rem;
margin-right: 0.75rem;
display: flex;
align-items: center;
}
.error-text {
color: #b91c1c;
font-size: 0.875rem;
font-weight: 500;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.form-group {
display: flex;
flex-direction: column;
+12
View File
@@ -31,5 +31,17 @@ export default defineConfig({
open: true,
allowedHosts: ['nas.7bm.co', 'localhost', '127.0.0.1'], // 允许的主机名列表1
cors: true,
// HMR配置
hmr: {
// 控制HMR更新时行为
overlay: false,
},
},
// 优化依赖预构建配置
optimizeDeps: {
// 防止依赖预构建时触发页面刷新导致路由中断
force: false,
// 预构建这些依赖,避免首次加载时出现重新构建
include: ['react-pdf', 'pdfjs-dist','dayjs','@remix-run/node','react-dom','axios','dayjs/plugin/utc','react-router-dom','jszip'],
},
});