fix: 1. 修复角色权限管理报错误提示的问题

This commit is contained in:
2025-11-24 19:46:52 +08:00
parent 9b2ee6d9bd
commit 93bae2de17
2 changed files with 303 additions and 50 deletions
+280 -2
View File
@@ -9,9 +9,14 @@
*/ */
import { type MetaFunction } from "@remix-run/node"; import { type MetaFunction } from "@remix-run/node";
import { useState, useRef } from "react"; import { useState, useRef, useEffect } from "react";
import { DiffEditor } from "@monaco-editor/react"; import { DiffEditor } from "@monaco-editor/react";
import type { editor } from "monaco-editor"; import type { editor } from "monaco-editor";
import { pdfjs } from 'react-pdf';
import { toastService } from '~/components/ui/Toast';
// 设置 PDF.js worker(与 pdf-demo.tsx 相同)
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
export const meta: MetaFunction = () => { export const meta: MetaFunction = () => {
return [ return [
@@ -20,6 +25,17 @@ export const meta: MetaFunction = () => {
]; ];
}; };
// PDF 类型枚举
type PdfType = 'text' | 'scanned' | 'unknown';
// PDF 信息接口
interface PdfInfo {
type: PdfType;
numPages: number;
textLength: number;
confidence: number; // 文本提取置信度 (0-1)
}
// 示例合同文本 A(原始版本) // 示例合同文本 A(原始版本)
const CONTRACT_A = `中国烟草合同(原始版本) const CONTRACT_A = `中国烟草合同(原始版本)
@@ -104,6 +120,107 @@ export default function MonacoDemoPage() {
const [diffCount, setDiffCount] = useState<number>(0); const [diffCount, setDiffCount] = useState<number>(0);
const [currentDiff, setCurrentDiff] = useState<number>(0); const [currentDiff, setCurrentDiff] = useState<number>(0);
// PDF相关状态
const [pdf1Url, setPdf1Url] = useState<string>('');
const [pdf2Url, setPdf2Url] = useState<string>('');
const [pdf1Info, setPdf1Info] = useState<PdfInfo | null>(null);
const [pdf2Info, setPdf2Info] = useState<PdfInfo | null>(null);
const [isLoadingPdf1, setIsLoadingPdf1] = useState(false);
const [isLoadingPdf2, setIsLoadingPdf2] = useState(false);
const [useExample, setUseExample] = useState(true);
// PDF类型检测函数
const detectPdfType = async (pdfUrl: string): Promise<PdfInfo> => {
const loadingTask = pdfjs.getDocument(pdfUrl);
const pdf = await loadingTask.promise;
let totalTextLength = 0;
const pagesToCheck = Math.min(pdf.numPages, 3); // 检查前3页
for (let i = 1; i <= pagesToCheck; i++) {
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
const pageText = textContent.items
.map((item: any) => item.str)
.join('');
totalTextLength += pageText.length;
}
// 计算平均每页文字数量
const avgTextPerPage = totalTextLength / pagesToCheck;
// 计算置信度(0-1
const confidence = Math.min(avgTextPerPage / 500, 1);
// 判断PDF类型
let type: PdfType;
if (avgTextPerPage > 100) {
type = 'text';
} else if (avgTextPerPage > 10) {
type = 'scanned';
} else {
type = 'unknown';
}
return {
type,
numPages: pdf.numPages,
textLength: totalTextLength,
confidence
};
};
// PDF文本提取函数
const extractTextFromPdf = async (pdfUrl: string): Promise<string> => {
const loadingTask = pdfjs.getDocument(pdfUrl);
const pdf = await loadingTask.promise;
let fullText = '';
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
const pageText = textContent.items
.map((item: any) => item.str)
.join(' ');
fullText += `\n========== 第 ${i} 页 ==========\n${pageText}\n`;
}
return fullText;
};
// 加载PDF并提取文本
const loadPdfAndExtractText = async (pdfUrl: string, setPdfInfo: (info: PdfInfo | null) => void, setLoading: (loading: boolean) => void, setTextContent: (text: string) => void) => {
try {
setLoading(true);
// 1. 检测PDF类型
const pdfInfo = await detectPdfType(pdfUrl);
setPdfInfo(pdfInfo);
// 2. 提取文本
if (pdfInfo.type === 'text') {
const text = await extractTextFromPdf(pdfUrl);
setTextContent(text);
toastService.success(`PDF加载成功!共 ${pdfInfo.numPages} 页,提取了 ${pdfInfo.textLength} 个字符`);
} else if (pdfInfo.type === 'scanned') {
toastService.warning('检测到扫描版PDF,文本提取质量可能较低');
const text = await extractTextFromPdf(pdfUrl);
setTextContent(text);
} else {
toastService.error('无法识别PDF类型,可能是图片PDF');
setTextContent('');
}
} catch (error) {
console.error('PDF加载失败:', error);
toastService.error('PDF加载失败,请检查文件路径');
setPdfInfo(null);
setTextContent('');
} finally {
setLoading(false);
}
};
// Monaco Editor 挂载后的回调 // Monaco Editor 挂载后的回调
const handleEditorDidMount = (editor: editor.IStandaloneDiffEditor) => { const handleEditorDidMount = (editor: editor.IStandaloneDiffEditor) => {
diffEditorRef.current = editor; diffEditorRef.current = editor;
@@ -163,6 +280,9 @@ export default function MonacoDemoPage() {
setOriginalText(CONTRACT_A); setOriginalText(CONTRACT_A);
setModifiedText(CONTRACT_B); setModifiedText(CONTRACT_B);
setCurrentDiff(0); setCurrentDiff(0);
setUseExample(true);
setPdf1Info(null);
setPdf2Info(null);
// 重新计算差异数量 // 重新计算差异数量
setTimeout(() => { setTimeout(() => {
@@ -175,6 +295,37 @@ export default function MonacoDemoPage() {
}, 100); }, 100);
}; };
// 从URL参数加载PDF
const loadPdfsFromUrl = () => {
if (typeof window === 'undefined') return;
const searchParams = new URLSearchParams(window.location.search);
const pdf1Path = searchParams.get('pdf1');
const pdf2Path = searchParams.get('pdf2');
if (pdf1Path || pdf2Path) {
setUseExample(false);
if (pdf1Path) {
const fullUrl = `/api/pdf-proxy?path=${encodeURIComponent(pdf1Path)}`;
setPdf1Url(fullUrl);
loadPdfAndExtractText(fullUrl, setPdf1Info, setIsLoadingPdf1, setOriginalText);
}
if (pdf2Path) {
const fullUrl = `/api/pdf-proxy?path=${encodeURIComponent(pdf2Path)}`;
setPdf2Url(fullUrl);
loadPdfAndExtractText(fullUrl, setPdf2Info, setIsLoadingPdf2, setModifiedText);
}
}
};
// 组件挂载时读取URL参数
useEffect(() => {
loadPdfsFromUrl();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return ( return (
<div className="monaco-demo-page" style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}> <div className="monaco-demo-page" style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
{/* 页面头部 */} {/* 页面头部 */}
@@ -301,6 +452,69 @@ export default function MonacoDemoPage() {
</div> </div>
</div> </div>
{/* PDF加载信息 */}
{!useExample && (pdf1Info || pdf2Info || isLoadingPdf1 || isLoadingPdf2) && (
<div style={{
padding: '12px 24px',
backgroundColor: '#fff3cd',
borderBottom: '1px solid #ffc107',
fontSize: '14px',
color: '#856404'
}}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '24px' }}>
<i className="ri-file-pdf-line" style={{ fontSize: '18px', marginTop: '2px' }}></i>
<div style={{ flex: 1 }}>
<strong>PDF文档信息</strong>
<div style={{ display: 'flex', gap: '24px', marginTop: '8px' }}>
{/* PDF 1 信息 */}
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>📄 1/</div>
{isLoadingPdf1 ? (
<div style={{ color: '#666' }}> ...</div>
) : pdf1Info ? (
<div>
<div>: <span style={{
color: pdf1Info.type === 'text' ? '#28a745' : pdf1Info.type === 'scanned' ? '#ffc107' : '#dc3545',
fontWeight: 'bold'
}}>
{pdf1Info.type === 'text' ? '✅ 文本PDF' : pdf1Info.type === 'scanned' ? '⚠️ 扫描PDF' : '❌ 未知类型'}
</span></div>
<div>: {pdf1Info.numPages} </div>
<div>: {pdf1Info.textLength} </div>
<div>: {(pdf1Info.confidence * 100).toFixed(0)}%</div>
</div>
) : (
<div style={{ color: '#999' }}></div>
)}
</div>
{/* PDF 2 信息 */}
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>📄 2/</div>
{isLoadingPdf2 ? (
<div style={{ color: '#666' }}> ...</div>
) : pdf2Info ? (
<div>
<div>: <span style={{
color: pdf2Info.type === 'text' ? '#28a745' : pdf2Info.type === 'scanned' ? '#ffc107' : '#dc3545',
fontWeight: 'bold'
}}>
{pdf2Info.type === 'text' ? '✅ 文本PDF' : pdf2Info.type === 'scanned' ? '⚠️ 扫描PDF' : '❌ 未知类型'}
</span></div>
<div>: {pdf2Info.numPages} </div>
<div>: {pdf2Info.textLength} </div>
<div>: {(pdf2Info.confidence * 100).toFixed(0)}%</div>
</div>
) : (
<div style={{ color: '#999' }}></div>
)}
</div>
</div>
</div>
</div>
</div>
)}
{/* 说明信息 */} {/* 说明信息 */}
<div style={{ <div style={{
padding: '12px 24px', padding: '12px 24px',
@@ -311,13 +525,36 @@ export default function MonacoDemoPage() {
}}> }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px' }}> <div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px' }}>
<i className="ri-information-line" style={{ fontSize: '18px', marginTop: '2px' }}></i> <i className="ri-information-line" style={{ fontSize: '18px', marginTop: '2px' }}></i>
<div> <div style={{ flex: 1 }}>
<strong></strong> <strong></strong>
<ul style={{ margin: '4px 0 0 0', paddingLeft: '20px' }}> <ul style={{ margin: '4px 0 0 0', paddingLeft: '20px' }}>
<li><span style={{ color: '#28a745', fontWeight: 'bold' }}>绿</span></li> <li><span style={{ color: '#28a745', fontWeight: 'bold' }}>绿</span></li>
<li><span style={{ color: '#dc3545', fontWeight: 'bold' }}></span></li> <li><span style={{ color: '#dc3545', fontWeight: 'bold' }}></span></li>
<li><span style={{ color: '#ffc107', fontWeight: 'bold' }}></span></li> <li><span style={{ color: '#ffc107', fontWeight: 'bold' }}></span></li>
</ul> </ul>
{useExample && (
<div style={{ marginTop: '12px', paddingTop: '12px', borderTop: '1px solid #b3d9ff' }}>
<strong>💡 使</strong>
<div style={{ marginTop: '4px' }}>
URL参数加载PDF文档进行对比
<code style={{
display: 'block',
marginTop: '4px',
padding: '8px',
backgroundColor: 'rgba(0,0,0,0.05)',
borderRadius: '4px',
fontSize: '12px',
wordBreak: 'break-all'
}}>
/monaco-demo?pdf1=1&pdf2=2
</code>
<div style={{ marginTop: '4px', fontSize: '12px' }}>
: <code>/monaco-demo?pdf1=documents/contract_v1.pdf&pdf2=documents/contract_v2.pdf</code>
</div>
</div>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -353,8 +590,49 @@ export default function MonacoDemoPage() {
diffAlgorithm: 'advanced', // 使用高级差异算法 diffAlgorithm: 'advanced', // 使用高级差异算法
}} }}
/> />
{/* PDF加载中的遮罩层 */}
{(isLoadingPdf1 || isLoadingPdf2) && (
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000
}}>
<div style={{ textAlign: 'center' }}>
<div style={{
width: '48px',
height: '48px',
border: '4px solid #f3f3f3',
borderTop: '4px solid #00684a',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
margin: '0 auto 16px'
}}></div>
<div style={{ fontSize: '16px', color: '#333' }}>
PDF文档并提取文本...
</div>
{isLoadingPdf1 && <div style={{ fontSize: '14px', color: '#666', marginTop: '8px' }}>📄 1</div>}
{isLoadingPdf2 && <div style={{ fontSize: '14px', color: '#666', marginTop: '8px' }}>📄 2</div>}
</div>
</div>
)}
</div> </div>
{/* 添加旋转动画 */}
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
{/* 页面底部信息 */} {/* 页面底部信息 */}
<div style={{ <div style={{
padding: '8px 24px', padding: '8px 24px',
+23 -48
View File
@@ -55,41 +55,14 @@ function getDataScopeLabel(scope: string): string {
// ClientLoader - 加载初始数据 // ClientLoader - 加载初始数据
export async function clientLoader({ request }: ClientLoaderFunctionArgs) { export async function clientLoader({ request }: ClientLoaderFunctionArgs) {
// ==================== 权限校验 ==================== // ==================== 权限校验 ====================
// 检查用户是否有provincial_admin权限 // 不在这里做权限检查,因为路由权限已经在 root.tsx 中检查过了
try { // 如果用户能访问到这个页面,说明已经通过了路由权限检查
const userInfo = localStorage.getItem('user_info'); //
// 原有的客户端权限检查逻辑已移除,统一使用 root.tsx 的路由权限控制
if (!userInfo) { // 这样可以避免:
// 未登录,重定向到登录页 // 1. 路由权限通过但页面权限不通过的冲突
window.location.href = '/login'; // 2. localStorage.user_info 数据不完整导致的误判
throw new Error('未登录'); // 3. 重复的权限检查逻辑
}
const user = JSON.parse(userInfo);
// 检查角色或权限
// provincial_admin 角色拥有完整的RBAC管理权限
const hasPermission =
user.role === 'provincial_admin' ||
user.role_key === 'provincial_admin' ||
(user.permissions && Array.isArray(user.permissions) && user.permissions.includes('system:rbac:manage'));
if (!hasPermission) {
// 无权限,显示错误提示
console.warn('⚠️ 权限不足:需要省级管理员权限或system:rbac:manage权限');
toastService.error('权限不足,需要省级管理员权限');
// 返回空数据,但不阻止页面渲染(可以显示友好的无权限提示)
return {
roles: [],
routes: [],
users: [],
noPermission: true
};
}
} catch (error) {
console.error('权限检查失败:', error);
}
// ==================== 加载数据 ==================== // ==================== 加载数据 ====================
try { try {
@@ -102,16 +75,14 @@ export async function clientLoader({ request }: ClientLoaderFunctionArgs) {
return { return {
roles, roles,
routes, routes,
users, users
noPermission: false
}; };
} catch (error) { } catch (error) {
console.error("加载数据失败:", error); console.error("加载数据失败:", error);
return { return {
roles: [], roles: [],
routes: [], routes: [],
users: [], users: []
noPermission: false
}; };
} }
} }
@@ -1105,22 +1076,26 @@ export default function RolePermissions() {
); );
} }
// 检查是否有权限(通过角色列表是否为空判断) // ==================== 权限检查移除说明 ====================
// 注意:这是一个简单的权限检查,实际应该从loader返回的noPermission字段判断 // 原有的客户端权限检查已移除,统一使用 root.tsx 的路由权限控制
const noPermission = roles.length === 0 && routes.length === 0 && users.length === 0 && !loading; // 如果用户能访问到这个页面,说明已经通过了路由权限检查
// 不再显示"无权限"提示页面
// 无权限提示页面 // 如果数据为空,可能是数据加载失败,显示友好的空状态提示
if (noPermission) { const hasNoData = roles.length === 0 && routes.length === 0 && users.length === 0 && !loading;
// 数据加载失败提示(不是权限问题)
if (hasNoData) {
return ( return (
<div className="role-permissions-page"> <div className="role-permissions-page">
<Card className="no-permission-card"> <Card className="no-data-card">
<div className="empty-state" style={{ padding: '80px 40px' }}> <div className="empty-state" style={{ padding: '80px 40px' }}>
<i className="ri-shield-cross-line" style={{ fontSize: '96px', color: '#dcdfe6' }}></i> <i className="ri-database-2-line" style={{ fontSize: '96px', color: '#dcdfe6' }}></i>
<h2 style={{ fontSize: '24px', fontWeight: 600, color: '#303133', marginTop: '24px', marginBottom: '12px' }}> <h2 style={{ fontSize: '24px', fontWeight: 600, color: '#303133', marginTop: '24px', marginBottom: '12px' }}>
</h2> </h2>
<p style={{ fontSize: '16px', color: '#606266', marginBottom: '32px' }}> <p style={{ fontSize: '16px', color: '#606266', marginBottom: '32px' }}>
访 <strong></strong> <strong>system:rbac:manage</strong>
</p> </p>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}> <div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
<Button type="default" icon="ri-arrow-left-line" onClick={() => window.history.back()}> <Button type="default" icon="ri-arrow-left-line" onClick={() => window.history.back()}>