fix: 1. 系统设置入口进来只会跳转到拥有权限访问的页面。
2. 优化登录样式
This commit is contained in:
+28
-5
@@ -52,23 +52,36 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
// 🔑 检查用户是否有系统设置权限
|
// 🔑 检查用户是否有系统设置权限
|
||||||
let hasSettingsAccess = false;
|
let hasSettingsAccess = false;
|
||||||
let hasCrossCheckingAccess = false;
|
let hasCrossCheckingAccess = false;
|
||||||
|
let settingsChildren: { path: string; title: string }[] = [];
|
||||||
|
|
||||||
if (userRole && frontendJWT) {
|
if (userRole && frontendJWT) {
|
||||||
const { getUserRoutesByRole } = await import('~/api/auth/user-routes');
|
const { getUserRoutesByRole } = await import('~/api/auth/user-routes');
|
||||||
const routesResult = await getUserRoutesByRole(userRole, frontendJWT, true); // includeHidden=true
|
const routesResult = await getUserRoutesByRole(userRole, frontendJWT, true); // includeHidden=true
|
||||||
|
|
||||||
if (routesResult.success && routesResult.data) {
|
if (routesResult.success && routesResult.data) {
|
||||||
// 检查是否存在顶级路由 '/settings'
|
// 查找 '/settings' 路由及其子路由
|
||||||
hasSettingsAccess = routesResult.data.some(route => route.path === '/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'
|
// 检查是否存在顶级路由 '/cross-checking'
|
||||||
hasCrossCheckingAccess = routesResult.data.some(route => route.path === '/cross-checking');
|
hasCrossCheckingAccess = routesResult.data.some(route => route.path === '/cross-checking');
|
||||||
// console.log(`🔑 [Index Loader] 用户${hasSettingsAccess ? '有' : '没有'}系统设置权限`);
|
// console.log(`🔑 [Index Loader] 用户${hasSettingsAccess ? '有' : '没有'}系统设置权限`);
|
||||||
|
// console.log(`🔑 [Index Loader] 系统设置子路由数量: ${settingsChildren.length}`);
|
||||||
// console.log(`🔑 [Index Loader] 用户${hasCrossCheckingAccess ? '有' : '没有'}交叉评查权限`);
|
// 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() {
|
export default function Index() {
|
||||||
@@ -238,6 +251,14 @@ export default function Index() {
|
|||||||
|
|
||||||
// 处理进入系统设置
|
// 处理进入系统设置
|
||||||
const handleEnterSettings = () => {
|
const handleEnterSettings = () => {
|
||||||
|
// 🔑 检查是否有系统设置的子路由
|
||||||
|
if (!loaderData.settingsChildren || loaderData.settingsChildren.length === 0) {
|
||||||
|
// 没有子路由,显示错误提示
|
||||||
|
toastService.error('您无权限访问或页面丢失');
|
||||||
|
console.warn('⚠️ [Index] 系统设置没有可访问的子路由');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
// 🔑 设置标志:表示用户通过系统设置入口进入
|
// 🔑 设置标志:表示用户通过系统设置入口进入
|
||||||
sessionStorage.setItem('settingsMode', 'true');
|
sessionStorage.setItem('settingsMode', 'true');
|
||||||
@@ -249,8 +270,10 @@ export default function Index() {
|
|||||||
sessionStorage.removeItem('crossCheckingMode');
|
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
|
// 获取 JWT token
|
||||||
const { frontendJWT } = await getUserSession(request);
|
const { frontendJWT } = await getUserSession(request);
|
||||||
|
|
||||||
// 从查询参数获取文件路径
|
// 从查询参数获取文件路径和预览标志
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const filePath = url.searchParams.get("path");
|
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) {
|
if (!filePath) {
|
||||||
return new Response("缺少文件路径参数", { status: 400 });
|
return new Response("缺少文件路径参数", { status: 400 });
|
||||||
@@ -38,13 +45,42 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
// 获取文件内容
|
// 获取文件内容
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
|
|
||||||
// 返回文件,保持原始的 Content-Type
|
// 从路径中提取文件名
|
||||||
return new Response(blob, {
|
const fileName = filePath.split('/').pop() || 'document.pdf';
|
||||||
headers: {
|
|
||||||
'Content-Type': response.headers.get('Content-Type') || 'application/pdf',
|
// 判断文件类型
|
||||||
'Cache-Control': 'public, max-age=3600', // 缓存1小时
|
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) {
|
} catch (error) {
|
||||||
console.error('PDF 代理错误:', error);
|
console.error('PDF 代理错误:', error);
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ export default function DocumentEdit() {
|
|||||||
// 下载文档
|
// 下载文档
|
||||||
const downloadDocument = async () => {
|
const downloadDocument = async () => {
|
||||||
try {
|
try {
|
||||||
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
|
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证(默认行为是下载)
|
||||||
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(documentData.path)}`;
|
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(documentData.path)}`;
|
||||||
|
|
||||||
// 使用fetch获取文件内容
|
// 使用fetch获取文件内容
|
||||||
@@ -457,8 +457,8 @@ export default function DocumentEdit() {
|
|||||||
|
|
||||||
// 在新窗口打开文档预览
|
// 在新窗口打开文档预览
|
||||||
const openPreview = () => {
|
const openPreview = () => {
|
||||||
// 使用 PDF 代理路由,自动添加 JWT 认证
|
// 使用 PDF 代理路由,自动添加 JWT 认证,添加 preview 参数实现预览
|
||||||
const previewUrl = `/api/pdf-proxy?path=${encodeURIComponent(documentData.path)}`;
|
const previewUrl = `/api/pdf-proxy?path=${encodeURIComponent(documentData.path)}&preview=true`;
|
||||||
window.open(previewUrl, '_blank');
|
window.open(previewUrl, '_blank');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -622,7 +622,7 @@ export default function DocumentEdit() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 文档预览 */}
|
{/* 文档预览 */}
|
||||||
<Card
|
{ false && <Card
|
||||||
title="文档预览"
|
title="文档预览"
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
>
|
>
|
||||||
@@ -662,6 +662,8 @@ export default function DocumentEdit() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
{/* 修改历史 */}
|
{/* 修改历史 */}
|
||||||
<Card title="修改历史" className="hidden">
|
<Card title="修改历史" className="hidden">
|
||||||
|
|||||||
+23
-11
@@ -213,6 +213,7 @@ export default function Login() {
|
|||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [passwordLoginError, setPasswordLoginError] = useState<string | null>(null);
|
const [passwordLoginError, setPasswordLoginError] = useState<string | null>(null);
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
// 从 loaderData 中获取错误信息
|
// 从 loaderData 中获取错误信息
|
||||||
const oauthError = loaderData?.flashError;
|
const oauthError = loaderData?.flashError;
|
||||||
@@ -466,17 +467,28 @@ export default function Login() {
|
|||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="password" className="form-label">密码</label>
|
<label htmlFor="password" className="form-label">密码</label>
|
||||||
<input
|
<div className="password-input-wrapper">
|
||||||
type="password"
|
<input
|
||||||
id="password"
|
type={showPassword ? "text" : "password"}
|
||||||
name="password"
|
id="password"
|
||||||
value={password}
|
name="password"
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
value={password}
|
||||||
className="form-input"
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="请输入密码"
|
className="form-input"
|
||||||
disabled={isLoading}
|
placeholder="请输入密码"
|
||||||
required
|
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>
|
</div>
|
||||||
|
|
||||||
<button
|
<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;
|
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 {
|
.admin-login-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -562,6 +606,15 @@
|
|||||||
.admin-login-text:hover {
|
.admin-login-text:hover {
|
||||||
color: #93c5fd;
|
color: #93c5fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.password-toggle-button {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-toggle-button:hover,
|
||||||
|
.password-toggle-button:focus {
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user