fix: 1. 系统设置入口进来只会跳转到拥有权限访问的页面。
2. 优化登录样式
This commit is contained in:
+28
-5
@@ -52,23 +52,36 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 🔑 检查用户是否有系统设置权限
|
||||
let hasSettingsAccess = false;
|
||||
let hasCrossCheckingAccess = false;
|
||||
let settingsChildren: { path: string; title: string }[] = [];
|
||||
|
||||
if (userRole && frontendJWT) {
|
||||
const { getUserRoutesByRole } = await import('~/api/auth/user-routes');
|
||||
const routesResult = await getUserRoutesByRole(userRole, frontendJWT, true); // includeHidden=true
|
||||
|
||||
if (routesResult.success && routesResult.data) {
|
||||
// 检查是否存在顶级路由 '/settings'
|
||||
hasSettingsAccess = routesResult.data.some(route => route.path === '/settings');
|
||||
// 查找 '/settings' 路由及其子路由
|
||||
const settingsRoute = routesResult.data.find(route => route.path === '/settings');
|
||||
if (settingsRoute) {
|
||||
hasSettingsAccess = true;
|
||||
// 提取子路由信息(仅 path 和 title)
|
||||
if (settingsRoute.children && settingsRoute.children.length > 0) {
|
||||
settingsChildren = settingsRoute.children.map(child => ({
|
||||
path: child.path,
|
||||
title: child.title
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否存在顶级路由 '/cross-checking'
|
||||
hasCrossCheckingAccess = routesResult.data.some(route => route.path === '/cross-checking');
|
||||
// console.log(`🔑 [Index Loader] 用户${hasSettingsAccess ? '有' : '没有'}系统设置权限`);
|
||||
// console.log(`🔑 [Index Loader] 系统设置子路由数量: ${settingsChildren.length}`);
|
||||
// console.log(`🔑 [Index Loader] 用户${hasCrossCheckingAccess ? '有' : '没有'}交叉评查权限`);
|
||||
}
|
||||
}
|
||||
|
||||
// 返回用户信息、入口模块和权限给客户端
|
||||
return Response.json({ userRole, userInfo, entryModules, hasSettingsAccess, hasCrossCheckingAccess });
|
||||
return Response.json({ userRole, userInfo, entryModules, hasSettingsAccess, hasCrossCheckingAccess, settingsChildren });
|
||||
}
|
||||
|
||||
export default function Index() {
|
||||
@@ -238,6 +251,14 @@ export default function Index() {
|
||||
|
||||
// 处理进入系统设置
|
||||
const handleEnterSettings = () => {
|
||||
// 🔑 检查是否有系统设置的子路由
|
||||
if (!loaderData.settingsChildren || loaderData.settingsChildren.length === 0) {
|
||||
// 没有子路由,显示错误提示
|
||||
toastService.error('您无权限访问或页面丢失');
|
||||
console.warn('⚠️ [Index] 系统设置没有可访问的子路由');
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// 🔑 设置标志:表示用户通过系统设置入口进入
|
||||
sessionStorage.setItem('settingsMode', 'true');
|
||||
@@ -249,8 +270,10 @@ export default function Index() {
|
||||
sessionStorage.removeItem('crossCheckingMode');
|
||||
}
|
||||
|
||||
// 跳转到系统设置的默认页面
|
||||
navigate('/rule-groups');
|
||||
// 跳转到第一个子路由
|
||||
const firstChildPath = loaderData.settingsChildren[0].path;
|
||||
console.log(`📌 [Index] 系统设置:跳转到第一个子路由 ${firstChildPath}`);
|
||||
navigate(firstChildPath);
|
||||
};
|
||||
|
||||
// 处理进入交叉评查
|
||||
|
||||
@@ -11,9 +11,16 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 获取 JWT token
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
// 从查询参数获取文件路径
|
||||
// 从查询参数获取文件路径和预览标志
|
||||
const url = new URL(request.url);
|
||||
const filePath = url.searchParams.get("path");
|
||||
const isPreview = url.searchParams.get("preview") === "true";
|
||||
|
||||
console.log("📄 [PDF Proxy] 请求参数:", {
|
||||
path: filePath,
|
||||
preview: url.searchParams.get("preview"),
|
||||
isPreview: isPreview
|
||||
});
|
||||
|
||||
if (!filePath) {
|
||||
return new Response("缺少文件路径参数", { status: 400 });
|
||||
@@ -38,13 +45,42 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 获取文件内容
|
||||
const blob = await response.blob();
|
||||
|
||||
// 返回文件,保持原始的 Content-Type
|
||||
return new Response(blob, {
|
||||
headers: {
|
||||
'Content-Type': response.headers.get('Content-Type') || 'application/pdf',
|
||||
'Cache-Control': 'public, max-age=3600', // 缓存1小时
|
||||
},
|
||||
});
|
||||
// 从路径中提取文件名
|
||||
const fileName = filePath.split('/').pop() || 'document.pdf';
|
||||
|
||||
// 判断文件类型
|
||||
const fileExtension = fileName.split('.').pop()?.toLowerCase();
|
||||
let contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
||||
|
||||
// 🔑 根据文件扩展名强制设置正确的 Content-Type
|
||||
if (fileExtension === 'pdf') {
|
||||
contentType = 'application/pdf';
|
||||
} else if (fileExtension === 'doc' || fileExtension === 'docx') {
|
||||
contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': 'public, max-age=3600', // 缓存1小时
|
||||
};
|
||||
|
||||
// 根据 preview 参数决定是预览还是下载
|
||||
// 默认行为:强制下载(保持向后兼容性)
|
||||
if (isPreview) {
|
||||
// 在浏览器中预览
|
||||
headers['Content-Disposition'] = `inline; filename="${encodeURIComponent(fileName)}"`;
|
||||
console.log("📄 [PDF Proxy] 设置为预览模式 (inline)");
|
||||
} else {
|
||||
// 强制下载(默认行为)
|
||||
headers['Content-Disposition'] = `attachment; filename="${encodeURIComponent(fileName)}"`;
|
||||
console.log("📄 [PDF Proxy] 设置为下载模式 (attachment)");
|
||||
}
|
||||
|
||||
console.log("📄 [PDF Proxy] 响应头:", headers);
|
||||
|
||||
// 返回文件
|
||||
return new Response(blob, { headers });
|
||||
|
||||
} catch (error) {
|
||||
console.error('PDF 代理错误:', error);
|
||||
|
||||
@@ -417,7 +417,7 @@ export default function DocumentEdit() {
|
||||
// 下载文档
|
||||
const downloadDocument = async () => {
|
||||
try {
|
||||
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
|
||||
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证(默认行为是下载)
|
||||
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(documentData.path)}`;
|
||||
|
||||
// 使用fetch获取文件内容
|
||||
@@ -457,8 +457,8 @@ export default function DocumentEdit() {
|
||||
|
||||
// 在新窗口打开文档预览
|
||||
const openPreview = () => {
|
||||
// 使用 PDF 代理路由,自动添加 JWT 认证
|
||||
const previewUrl = `/api/pdf-proxy?path=${encodeURIComponent(documentData.path)}`;
|
||||
// 使用 PDF 代理路由,自动添加 JWT 认证,添加 preview 参数实现预览
|
||||
const previewUrl = `/api/pdf-proxy?path=${encodeURIComponent(documentData.path)}&preview=true`;
|
||||
window.open(previewUrl, '_blank');
|
||||
};
|
||||
|
||||
@@ -622,7 +622,7 @@ export default function DocumentEdit() {
|
||||
</Card>
|
||||
|
||||
{/* 文档预览 */}
|
||||
<Card
|
||||
{ false && <Card
|
||||
title="文档预览"
|
||||
className="mb-4"
|
||||
>
|
||||
@@ -662,6 +662,8 @@ export default function DocumentEdit() {
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
|
||||
|
||||
{/* 修改历史 */}
|
||||
<Card title="修改历史" className="hidden">
|
||||
|
||||
+23
-11
@@ -213,6 +213,7 @@ export default function Login() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordLoginError, setPasswordLoginError] = useState<string | null>(null);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
// 从 loaderData 中获取错误信息
|
||||
const oauthError = loaderData?.flashError;
|
||||
@@ -466,17 +467,28 @@ export default function Login() {
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password" className="form-label">密码</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="form-input"
|
||||
placeholder="请输入密码"
|
||||
disabled={isLoading}
|
||||
required
|
||||
/>
|
||||
<div className="password-input-wrapper">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
id="password"
|
||||
name="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="form-input"
|
||||
placeholder="请输入密码"
|
||||
disabled={isLoading}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="password-toggle-button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
aria-label={showPassword ? "隐藏密码" : "显示密码"}
|
||||
>
|
||||
<i className={showPassword ? "ri-eye-off-line" : "ri-eye-line"}></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import { type MetaFunction } from "@remix-run/node";
|
||||
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "系统设置 - 中国烟草AI合同及卷宗审核系统" },
|
||||
{
|
||||
name: "settings",
|
||||
content: "评查点分组,入口模块管理,角色权限管理,文档类型管理,提示词管理"
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: "系统设置",
|
||||
to: "/settings" // 指定面包屑点击后跳转的路径
|
||||
};
|
||||
|
||||
/**
|
||||
* 规则管理路由布局
|
||||
*/
|
||||
export default function SettingsLayout() {
|
||||
return <Outlet />;
|
||||
}
|
||||
@@ -235,6 +235,50 @@
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* 密码输入框包装器 */
|
||||
.password-input-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.password-input-wrapper .form-input {
|
||||
padding-right: 3rem;
|
||||
}
|
||||
|
||||
/* 密码切换按钮 */
|
||||
.password-toggle-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
font-family: 'remixicon' !important;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.password-toggle-button:hover {
|
||||
color: #015c42;
|
||||
}
|
||||
|
||||
.password-toggle-button:focus {
|
||||
outline: none;
|
||||
color: #015c42;
|
||||
}
|
||||
|
||||
.password-toggle-button i {
|
||||
font-size: 1.25rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.admin-login-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -562,6 +606,15 @@
|
||||
.admin-login-text:hover {
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.password-toggle-button {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.password-toggle-button:hover,
|
||||
.password-toggle-button:focus {
|
||||
color: #60a5fa;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user