文档列表documents添加用户id的限制,添加通过统一认证之后数据库中用户数据的添加和角色的添加,添加Sidebar菜单通过数据库请求获取

This commit is contained in:
2025-07-20 21:49:40 +08:00
parent e4ce41cebe
commit d8f3d98c70
17 changed files with 1630 additions and 199 deletions
+613
View File
@@ -0,0 +1,613 @@
import { toastService } from '~/components/ui';
import { postgrestGet } from '../postgrest-client';
// 路由数据接口
export interface RouteInfo {
id: number;
path: string;
name: string;
meta: {
title: string;
icon: string;
order: number;
requiredRole?: string;
};
parent_id: number;
is_menu: number;
}
// 用户路由权限接口
export interface UserRoutePermission {
route_id: number;
role_id: number;
permission: string;
route: RouteInfo;
}
// MenuItem结构接口
export interface MenuItem {
id: string;
title: string;
path: string;
icon: string;
order: number;
hideBreadcrumb?: boolean;
requiredRole?: string;
children?: MenuItem[];
}
// 静态菜单数据作为后备 (保留用于开发和紧急情况,当前不使用)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const FALLBACK_MENU_DATA: Record<string, MenuItem[]> = {
'admin': [
{
id: 'home',
title: '系统概览',
path: '/home',
icon: 'ri-home-line',
order: 1
},
{
id: 'chat-with-llm',
title: 'AI对话',
path: '/chat-with-llm',
icon: 'ri-chat-smile-2-line',
order: 2
},
{
id: 'file-management',
title: '文件管理',
path: '/files',
icon: 'ri-folder-line',
order: 3,
children: [
{
id: 'file-upload',
title: '文件上传',
path: '/files/upload',
icon: 'ri-upload-cloud-line',
order: 1
},
{
id: 'documents',
title: '文档列表',
path: '/documents',
icon: 'ri-file-list-3-line',
order: 2
}
]
},
{
id: 'rule-management',
title: '评查规则库',
path: '/rules',
icon: 'ri-book-3-line',
order: 4,
children: [
{
id: 'rule-groups',
title: '评查点分组',
path: '/rule-groups',
icon: 'ri-folder-open-line',
order: 1
},
{
id: 'rules-list',
title: '评查点列表',
path: '/rules',
icon: 'ri-list-check-3',
order: 2
},
{
id: 'rules-file',
title: '评查文件列表',
path: '/rules-files',
icon: 'ri-list-check-2',
order: 3
}
]
},
{
id: 'contract-template',
title: '合同模板',
path: '/contract-template',
icon: 'ri-file-search-line',
order: 5,
children: [
{
id: 'contract-search-ai',
title: '智能搜索',
path: '/contract-template/search',
icon: 'ri-search-line',
order: 1
},
{
id: 'contract-list',
title: '合同列表',
path: '/contract-template/list',
icon: 'ri-folder-line',
order: 2
}
]
},
{
id: 'system-settings',
title: '系统设置',
path: '/settings',
icon: 'ri-settings-4-line',
order: 6,
requiredRole: 'developer',
children: [
{
id: 'config-lists',
title: '配置列表',
path: '/config-lists',
icon: 'ri-list-check-3',
order: 1,
requiredRole: 'developer'
},
{
id: 'document-types',
title: '文档类型',
path: '/document-types',
icon: 'ri-file-list-line',
order: 2,
requiredRole: 'developer'
},
{
id: 'prompt-management',
title: '提示词管理',
path: '/prompts',
icon: 'ri-chat-1-line',
order: 3,
requiredRole: 'developer'
}
]
},
{
id: 'cross-checking',
title: '交叉评查',
path: '/cross-checking',
icon: 'ri-color-filter-line',
order: 7
}
],
'common': [
{
id: 'home',
title: '系统概览',
path: '/home',
icon: 'ri-home-line',
order: 1
},
{
id: 'file-management',
title: '文件管理',
path: '/files',
icon: 'ri-folder-line',
order: 3,
children: [
{
id: 'file-upload',
title: '文件上传',
path: '/files/upload',
icon: 'ri-upload-cloud-line',
order: 1
},
{
id: 'documents',
title: '文档列表',
path: '/documents',
icon: 'ri-file-list-3-line',
order: 2
}
]
},
{
id: 'rule-management',
title: '评查规则库',
path: '/rules',
icon: 'ri-book-3-line',
order: 4,
children: [
{
id: 'rule-groups',
title: '评查点分组',
path: '/rule-groups',
icon: 'ri-folder-open-line',
order: 1
},
{
id: 'rules-list',
title: '评查点列表',
path: '/rules',
icon: 'ri-list-check-3',
order: 2
},
{
id: 'rules-file',
title: '评查文件列表',
path: '/rules-files',
icon: 'ri-list-check-2',
order: 3
}
]
},
{
id: 'contract-template',
title: '合同模板',
path: '/contract-template',
icon: 'ri-file-search-line',
order: 5,
children: [
{
id: 'contract-search-ai',
title: '智能搜索',
path: '/contract-template/search',
icon: 'ri-search-line',
order: 1
},
{
id: 'contract-list',
title: '合同列表',
path: '/contract-template/list',
icon: 'ri-folder-line',
order: 2
}
]
},
{
id: 'cross-checking',
title: '交叉评查',
path: '/cross-checking',
icon: 'ri-color-filter-line',
order: 7
}
],
'deptLeader': [
{
id: 'home',
title: '系统概览',
path: '/home',
icon: 'ri-home-line',
order: 1
},
{
id: 'chat-with-llm',
title: 'AI对话',
path: '/chat-with-llm',
icon: 'ri-chat-smile-2-line',
order: 2
},
{
id: 'file-management',
title: '文件管理',
path: '/files',
icon: 'ri-folder-line',
order: 3,
children: [
{
id: 'file-upload',
title: '文件上传',
path: '/files/upload',
icon: 'ri-upload-cloud-line',
order: 1
},
{
id: 'documents',
title: '文档列表',
path: '/documents',
icon: 'ri-file-list-3-line',
order: 2
}
]
},
{
id: 'rule-management',
title: '评查规则库',
path: '/rules',
icon: 'ri-book-3-line',
order: 4,
children: [
{
id: 'rule-groups',
title: '评查点分组',
path: '/rule-groups',
icon: 'ri-folder-open-line',
order: 1
},
{
id: 'rules-list',
title: '评查点列表',
path: '/rules',
icon: 'ri-list-check-3',
order: 2
},
{
id: 'rules-file',
title: '评查文件列表',
path: '/rules-files',
icon: 'ri-list-check-2',
order: 3
}
]
},
{
id: 'contract-template',
title: '合同模板',
path: '/contract-template',
icon: 'ri-file-search-line',
order: 5,
children: [
{
id: 'contract-search-ai',
title: '智能搜索',
path: '/contract-template/search',
icon: 'ri-search-line',
order: 1
},
{
id: 'contract-list',
title: '合同列表',
path: '/contract-template/list',
icon: 'ri-folder-line',
order: 2
}
]
},
{
id: 'cross-checking',
title: '交叉评查',
path: '/cross-checking',
icon: 'ri-color-filter-line',
order: 7
}
],
'groupLeader': [
{
id: 'home',
title: '系统概览',
path: '/home',
icon: 'ri-home-line',
order: 1
},
{
id: 'file-management',
title: '文件管理',
path: '/files',
icon: 'ri-folder-line',
order: 3,
children: [
{
id: 'file-upload',
title: '文件上传',
path: '/files/upload',
icon: 'ri-upload-cloud-line',
order: 1
},
{
id: 'documents',
title: '文档列表',
path: '/documents',
icon: 'ri-file-list-3-line',
order: 2
}
]
},
{
id: 'rule-management',
title: '评查规则库',
path: '/rules',
icon: 'ri-book-3-line',
order: 4,
children: [
{
id: 'rule-groups',
title: '评查点分组',
path: '/rule-groups',
icon: 'ri-folder-open-line',
order: 1
},
{
id: 'rules-list',
title: '评查点列表',
path: '/rules',
icon: 'ri-list-check-3',
order: 2
},
{
id: 'rules-file',
title: '评查文件列表',
path: '/rules-files',
icon: 'ri-list-check-2',
order: 3
}
]
},
{
id: 'contract-template',
title: '合同模板',
path: '/contract-template',
icon: 'ri-file-search-line',
order: 5,
children: [
{
id: 'contract-search-ai',
title: '智能搜索',
path: '/contract-template/search',
icon: 'ri-search-line',
order: 1
},
{
id: 'contract-list',
title: '合同列表',
path: '/contract-template/list',
icon: 'ri-folder-line',
order: 2
}
]
},
{
id: 'cross-checking',
title: '交叉评查',
path: '/cross-checking',
icon: 'ri-color-filter-line',
order: 7
}
]
};
/**
* 根据角色获取用户可访问的路由
* @param roleKey 角色标识 (如: 'admin', 'common', 'deptLeader', 'groupLeader')
* @returns 用户可访问的路由列表
*/
export async function getUserRoutesByRole(roleKey: string): Promise<{ success: boolean; data?: MenuItem[]; error?: string; shouldRedirectToHome?: boolean }> {
try {
console.log(`获取角色 ${roleKey} 的路由权限`);
// 首先获取角色ID
const roleResult = await postgrestGet<Array<{id: number}>>("roles", {
filter: {
"role_key": `eq.${roleKey}`
}
});
if (roleResult.error || !roleResult.data || roleResult.data.length === 0) {
console.error("角色不存在:", roleKey);
toastService.error("角色不存在,请联系管理员配置权限后重新登录");
return { success: false, error: "角色不存在", shouldRedirectToHome: true };
}
const roleId = roleResult.data[0].id;
// 查询角色的路由权限
const roleRoutesResult = await postgrestGet<Array<{route_id: number}>>("role_route", {
filter: {
"role_id": `eq.${roleId}`
}
});
if (roleRoutesResult.error) {
console.error("查询角色路由关联失败:", roleRoutesResult.error);
toastService.error("查询角色路由关联失败,请稍后再试");
return { success: false, error: "查询角色路由关联失败", shouldRedirectToHome: true };
}
const roleRoutes = roleRoutesResult.data || [];
const routeIds = roleRoutes.map(item => item.route_id);
if (routeIds.length === 0) {
console.log(`角色 ${roleKey} 没有分配任何路由权限`);
toastService.error("您的角色没有分配任何路由权限,请联系管理员配置权限");
return { success: false, error: "角色没有分配任何路由权限", shouldRedirectToHome: true };
}
// 查询具体的路由信息
const routesResult = await postgrestGet<RouteInfo[]>("sys_routes", {
filter: {
"id": `in.(${routeIds.join(',')})`,
"is_menu": "eq.1"
},
order: "parent_id,meta->>order"
});
if (routesResult.error) {
console.error("查询路由信息失败:", routesResult.error);
toastService.error("查询路由信息失败,请稍后再试");
return { success: false, error: "查询路由信息失败", shouldRedirectToHome: true };
}
const routes = routesResult.data || [];
// 构建菜单树
const menuItems = buildMenuTreeFromRoutes(routes);
console.log(`角色 ${roleKey} 可访问 ${menuItems.length} 个路由`);
return { success: true, data: menuItems };
} catch (error) {
console.error("获取用户路由时发生错误:", error);
toastService.error("获取用户路由时发生错误,请稍后再试");
return {
success: false,
error: `获取用户路由失败: ${error instanceof Error ? error.message : String(error)}`,
shouldRedirectToHome: true
};
}
}
/**
* 从路由信息构建菜单树结构
* @param routes 路由信息数组
* @returns 菜单树结构
*/
function buildMenuTreeFromRoutes(routes: RouteInfo[]): MenuItem[] {
// 转换为MenuItem格式
const menuMap = new Map<number, MenuItem>();
routes.forEach(route => {
const menuItem: MenuItem = {
id: route.name,
title: route.meta.title,
path: route.path,
icon: route.meta.icon,
order: route.meta.order || 0,
requiredRole: route.meta.requiredRole
};
menuMap.set(route.id, menuItem);
});
// 构建父子关系
const rootItems: MenuItem[] = [];
const itemsWithParent: Array<{ item: MenuItem; parentId: number }> = [];
routes.forEach(route => {
const menuItem = menuMap.get(route.id);
if (!menuItem) return;
if (route.parent_id === 0) {
rootItems.push(menuItem);
} else {
itemsWithParent.push({ item: menuItem, parentId: route.parent_id });
}
});
// 添加子菜单
itemsWithParent.forEach(({ item, parentId }) => {
const parent = menuMap.get(parentId);
if (parent) {
if (!parent.children) {
parent.children = [];
}
parent.children.push(item);
}
});
// 排序
rootItems.sort((a, b) => a.order - b.order);
rootItems.forEach(item => {
if (item.children) {
item.children.sort((a, b) => a.order - b.order);
}
});
return rootItems;
}
/**
* 根据用户角色映射到权限系统的角色标识
* @param userRole 前端用户角色 ('common' | 'developer')
* @returns 数据库中的角色标识
*/
export function mapUserRoleToRoleKey(userRole: string): string {
const roleMapping: Record<string, string> = {
'common': 'common',
'developer': 'admin',
'deptLeader': 'deptLeader',
'groupLeader': 'groupLeader'
};
return roleMapping[userRole] || 'common';
}
+9 -1
View File
@@ -38,6 +38,7 @@ export interface DocumentSearchParams {
page?: number;
pageSize?: number;
reviewType?: string;
userId?: string; // 添加用户ID筛选
}
/**
@@ -214,7 +215,8 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro
fileStatus,
dateFrom,
dateTo,
reviewType
reviewType,
userId
} = searchParams;
let documentTypes: number[] | undefined;
@@ -228,6 +230,11 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro
}
}
// 确保userId必须存在,如果不存在则抛出错误
if (!userId) {
return { error: '用户身份验证失败,无法获取文档列表', status: 401 };
}
const rpcParams = {
search_name: name,
search_document_number: documentNumber,
@@ -236,6 +243,7 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro
search_file_status: fileStatus,
search_date_from: dateFrom,
search_date_to: dateTo,
search_user_id: parseInt(userId, 10), // 强制要求传递用户ID
};
// 并行执行获取数据和获取总数的请求
+282
View File
@@ -8,6 +8,7 @@
* - OAuth2.0 Token 自动刷新
* - 用户登录状态检查
* - 会话创建和销毁
* - 用户信息保存到数据库
*
* 技术栈:
* - Remix Session Storage (Cookie-based)
@@ -18,6 +19,7 @@
import { createCookieSessionStorage } from "@remix-run/node";
import { tokenManager } from "./token-manager.server";
import { postgrestGet, postgrestPost, postgrestPut } from "../postgrest-client";
/**
* 用户角色类型定义
@@ -28,6 +30,42 @@ import { tokenManager } from "./token-manager.server";
*/
export type UserRole = 'common' | 'developer';
/**
* 用户信息接口,对应 sso_users 表结构
*/
export interface UserInfo {
sub: string; // IDaaS用户唯一标识
username?: string; // 显示用户名称/工号
nick_name?: string; // 用户真实姓名
nickname?: string; // OAuth返回的昵称字段
name?: string; // 用户姓名(通常映射到 nick_name)
phone_number?: string; // 手机号
email?: string; // 邮箱地址
ou_id?: string; // 所属组织单位ID
ou_name?: string; // 所属部门名称
status?: number; // 账户状态: 0=正常, 1=禁用
is_leader?: boolean; // 是否为部门负责人
}
/**
* sso_users 表记录接口
*/
export interface SsoUser {
id?: string;
sub: string;
username: string;
nick_name: string;
phone_number?: string;
email?: string;
ou_id: string;
ou_name: string;
status: number;
is_leader: boolean;
created_at?: string;
updated_at?: string;
deleted_at?: string;
}
/**
* 会话存储配置
*
@@ -230,4 +268,248 @@ export async function logout(request: Request) {
"Set-Cookie": await sessionStorage.destroySession(session), // 清除会话 Cookie
},
});
}
/**
* 保存用户信息到数据库
*
* 此函数实现以下逻辑:
* 1. 根据 userInfo.sub 查询 sso_users 表中是否已存在该用户
* 2. 如果存在,则更新用户信息
* 3. 如果不存在,则插入新的用户记录
*
* @param userInfo - 从 IDaaS 获取的用户信息
* @returns Promise<{success: boolean, data?: SsoUser, error?: string}>
*/
export async function saveUserInfo(userInfo: UserInfo): Promise<{success: boolean, data?: SsoUser, error?: string}> {
try {
console.log("开始保存用户信息", userInfo);
// 验证必要字段
if (!userInfo.sub) {
return { success: false, error: "用户唯一标识 sub 不能为空" };
}
// 1. 根据 sub 查询是否已存在该用户
const existingUserResult = await postgrestGet<SsoUser[]>("sso_users", {
filter: {
"sub": `eq.${userInfo.sub}`,
"deleted_at": "is.null" // 只查询未删除的记录
}
});
if (existingUserResult.error) {
console.error("查询用户失败:", existingUserResult.error);
return { success: false, error: `查询用户失败: ${existingUserResult.error}` };
}
const existingUsers = existingUserResult.data || [];
const existingUser = existingUsers.length > 0 ? existingUsers[0] : null;
// 准备要保存的用户数据
// 注意:OAuth返回的字段是nickname,而不是nick_name
const userData: Partial<SsoUser> = {
sub: userInfo.sub,
username: userInfo.username || userInfo.name || userInfo.sub,
nick_name: userInfo.nick_name || userInfo.nickname || userInfo.name || "未知用户",
phone_number: userInfo.phone_number || undefined,
email: userInfo.email || undefined,
ou_id: userInfo.ou_id || "default",
ou_name: userInfo.ou_name || "未知部门",
status: userInfo.status !== undefined ? userInfo.status : 0,
is_leader: userInfo.is_leader || false,
};
if (existingUser) {
// 2. 用户已存在,执行更新操作
console.log("用户已存在,执行更新操作", existingUser.id);
const updateResult = await postgrestPut<SsoUser[], Partial<SsoUser>>(
"sso_users",
userData,
{ id: existingUser.id! }
);
if (updateResult.error) {
console.error("更新用户失败:", updateResult.error);
return { success: false, error: `更新用户失败: ${updateResult.error}` };
}
console.log("用户信息更新成功");
return {
success: true,
data: Array.isArray(updateResult.data) ? updateResult.data[0] : updateResult.data as unknown as SsoUser
};
} else {
// 3. 用户不存在,执行插入操作 同时需要给这个用户默认添加一个角色,角色为common
console.log("用户不存在,执行插入操作");
const insertResult = await postgrestPost<SsoUser[], SsoUser>("sso_users", userData as SsoUser);
if (insertResult.error) {
console.error("插入用户失败:", insertResult.error);
return { success: false, error: `插入用户失败: ${insertResult.error}` };
}
console.log("用户信息插入成功");
// 4. 给这个用户默认添加一个角色,角色为common
const userData_with_id = Array.isArray(insertResult.data) ? insertResult.data[0] : insertResult.data as unknown as SsoUser;
if (userData_with_id?.id) {
await addDefaultRole(userData_with_id.id, 2);
}
return {
success: true,
data: userData_with_id
};
}
} catch (error) {
console.error("保存用户信息时发生错误:", error);
return {
success: false,
error: `保存用户信息失败: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* 为用户添加默认角色
*
* @param userId - 用户ID
* @param roleId - 角色ID,默认为2common角色)
* @returns 添加结果
*/
export async function addDefaultRole(userId: string, roleId: number = 2) {
try {
console.log(`为用户 ${userId} 添加默认角色 ${roleId}`);
// 检查用户是否已经有此角色
const existingRoleResult = await postgrestGet<Array<{id: number, user_id: string, role_id: number}>>("user_role", {
filter: {
user_id: `eq.${userId}`,
role_id: `eq.${roleId}`
}
});
if (existingRoleResult.error) {
console.error("查询用户角色失败:", existingRoleResult.error);
return { success: false, error: `查询用户角色失败: ${existingRoleResult.error}` };
}
const existingRoles = existingRoleResult.data || [];
if (existingRoles.length > 0) {
console.log("用户已经拥有此角色,跳过添加");
return { success: true, data: existingRoles[0] };
}
// 添加角色
const addRoleResult = await postgrestPost<Array<{id: number, user_id: string, role_id: number}>, {user_id: string, role_id: number}>("user_role", {
user_id: userId,
role_id: roleId
});
if (addRoleResult.error) {
console.error("添加用户角色失败:", addRoleResult.error);
return { success: false, error: `添加用户角色失败: ${addRoleResult.error}` };
}
console.log("用户角色添加成功");
return {
success: true,
data: Array.isArray(addRoleResult.data) ? addRoleResult.data[0] : addRoleResult.data
};
} catch (error) {
console.error("添加用户角色时发生错误:", error);
return {
success: false,
error: `添加用户角色失败: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* 通过用户sub获取用户信息
*
* @param sub - 用户的唯一标识
* @returns 用户信息
*/
export async function getUserBySub(sub: string) {
try {
console.log(`查询用户: ${sub}`);
const userResult = await postgrestGet<SsoUser[]>("sso_users", {
filter: {
sub: `eq.${sub}`
}
});
if (userResult.error) {
console.error("查询用户失败:", userResult.error);
return { success: false, error: `查询用户失败: ${userResult.error}` };
}
const users = userResult.data || [];
const user = users.length > 0 ? users[0] : null;
if (!user) {
return { success: false, error: "用户不存在" };
}
return { success: true, data: user };
} catch (error) {
console.error("查询用户时发生错误:", error);
return {
success: false,
error: `查询用户失败: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* 创建用户登录会话(支持用户信息)
*
* @param isAuthenticated - 是否已认证
* @param userRole - 用户角色
* @param redirectTo - 重定向URL
* @param userInfo - 可选的用户信息
* @returns HTTP重定向响应
*/
export async function createUserSessionWithInfo(
isAuthenticated: boolean,
userRole: UserRole,
redirectTo: string,
userInfo?: Partial<SsoUser>
) {
const session = await sessionStorage.getSession();
session.set("isAuthenticated", isAuthenticated);
session.set("userRole", userRole);
// 如果提供了用户信息,也保存到session中
if (userInfo) {
session.set("userInfo", {
sub: userInfo.sub,
user_id: userInfo.id,
username: userInfo.username,
nick_name: userInfo.nick_name,
email: userInfo.email,
ou_name: userInfo.ou_name,
is_leader: userInfo.is_leader,
user_role: userRole
});
}
const cookie = await sessionStorage.commitSession(session);
console.log("创建会话 - 设置Cookie:", !!cookie);
console.log("创建会话 - 用户角色:", userRole);
console.log("创建会话 - 用户信息:", userInfo?.username || "无");
console.log("创建会话 - 重定向到:", redirectTo);
return new Response(null, {
status: 302,
headers: {
Location: redirectTo,
"Set-Cookie": cookie,
},
});
}
@@ -516,14 +516,16 @@ export function ReviewPointsList({
/**
* 加载意见列表数据
*/
const loadOpinionListData = async (page: number = 1, pageSize: number = 10) => {
console.log('加载意见列表数据', selectedReviewPoint);
if (!selectedReviewPoint?.documentId) return;
const loadOpinionListData = async (page: number = 1, pageSize: number = 10, documentId?: string | number) => {
// 使用传入的documentId或者从selectedReviewPoint获取
const targetDocumentId = documentId || selectedReviewPoint?.documentId;
console.log('加载意见列表数据', targetDocumentId);
if (!targetDocumentId) return;
setOpinionListLoading(true);
try {
console.log('加载意见列表数据', selectedReviewPoint.documentId, page, pageSize);
const response = await getCrossCheckingOpinions(selectedReviewPoint.documentId, page, pageSize);
console.log('加载意见列表数据', targetDocumentId, page, pageSize);
const response = await getCrossCheckingOpinions(targetDocumentId, page, pageSize);
console.log('意见列表数据', response);
if (response.error) {
@@ -555,7 +557,8 @@ export function ReviewPointsList({
setSelectedReviewPoint(reviewPoint);
setIsOpinionListModalOpen(true);
console.log('打开意见列表模态框');
loadOpinionListData(1, 10);
// 直接传递reviewPoint的documentId,避免依赖状态更新
loadOpinionListData(1, 10, reviewPoint.documentId);
};
/**
@@ -2253,15 +2256,13 @@ export function ReviewPointsList({
{/* 悬浮状态:横向排列,显示图标,数字放大 */}
<div className="absolute top-0 right-0 opacity-0 scale-0 group-hover:opacity-100 group-hover:scale-100 flex items-center bg-blue-50 px-3 py-2 rounded-lg border border-blue-200 shadow-lg transition-all duration-300 origin-top-right">
<button className="flex items-center" aria-label="点击查看详情">
<div className="flex flex-col">
<i className="ri-chat-1-line text-blue-600 text-base"></i>
<span className="text-xl text-blue-600 font-bold">{scoringProposals.length}</span>
<span className="text-xs text-blue-500 leading-tight whitespace-wrap"></span>
<span className="text-xs text-blue-500 leading-tight whitespace-wrap"></span>
<span className="text-xs text-blue-500 leading-tight whitespace-wrap"></span>
</div>
</button>
<div className="flex flex-col">
<i className="ri-chat-1-line text-blue-600 text-base"></i>
<span className="text-xl text-blue-600 font-bold">{scoringProposals.length}</span>
<span className="text-xs text-blue-500 leading-tight whitespace-wrap"></span>
<span className="text-xs text-blue-500 leading-tight whitespace-wrap"></span>
<span className="text-xs text-blue-500 leading-tight whitespace-wrap"></span>
</div>
</div>
</button>
@@ -2451,7 +2452,7 @@ export function ReviewPointsList({
</Modal>
{/* 意见列表模态框 */}
{/* 意见列表模态框 */}
<Modal
isOpen={isOpinionListModalOpen}
onClose={handleCloseOpinionListModal}
+56 -169
View File
@@ -1,16 +1,7 @@
import { useState, useEffect } from 'react';
import { Link, useLocation, useNavigate } from '@remix-run/react';
import type { UserRole } from '~/root';
interface MenuItem {
id: string;
title: string;
path: string;
icon: string;
hideBreadcrumb?: boolean;
requiredRole?: UserRole;
children?: MenuItem[];
}
import { getUserRoutesByRole, mapUserRoleToRoleKey, type MenuItem } from '~/api/auth/user-routes';
interface SidebarProps {
onToggle: () => void;
@@ -21,8 +12,8 @@ interface SidebarProps {
// 定义不同应用模块下显示的菜单项ID
const APP_MENU_MAP = {
'contract': ['home', 'contract-template', 'file-management', 'rule-management', 'system-settings'],
'record': ['home', 'file-management', 'rule-management', 'system-settings'],
'contract': ['home', 'contract-template', 'file-management', 'rule-management', 'cross-checking', 'system-settings'],
'record': ['home', 'file-management', 'rule-management', 'cross-checking', 'system-settings'],
'model': ['chat-with-llm']
};
@@ -45,8 +36,47 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid
const [expandedMenus, setExpandedMenus] = useState<Record<string, boolean>>({});
const [currentApp, setCurrentApp] = useState<string>(''); // 初始设置为空字符串而不是selectedApp
const [isLoading, setIsLoading] = useState<boolean>(true); // 添加加载状态
const [menuItems, setMenuItems] = useState<MenuItem[]>([]); // 动态菜单项
const [isLoadingRoutes, setIsLoadingRoutes] = useState<boolean>(true); // 路由加载状态
const navigate = useNavigate();
// 获取用户路由权限
useEffect(() => {
const fetchUserRoutes = async () => {
setIsLoadingRoutes(true);
try {
const roleKey = mapUserRoleToRoleKey(userRole);
const result = await getUserRoutesByRole(roleKey);
if (result.success && result.data) {
setMenuItems(result.data);
console.log('用户路由权限加载成功:', result.data);
} else {
console.error('获取用户路由权限失败:', result.error);
// 如果需要重定向到首页
if (result.shouldRedirectToHome) {
console.log('重定向到首页');
navigate('/');
return;
}
// 其他错误情况,使用空数组
setMenuItems([]);
}
} catch (error) {
console.error('获取用户路由权限时发生错误:', error);
// 发生异常时也重定向到首页
navigate('/');
return;
} finally {
setIsLoadingRoutes(false);
}
};
fetchUserRoutes();
}, [userRole, navigate]);
// 组件挂载后从 sessionStorage 读取初始 reviewType
useEffect(() => {
let mounted = true;
@@ -135,142 +165,6 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid
}
}, [selectedApp, currentApp]);
const menuItems: MenuItem[] = [
{
id: 'home',
title: '系统概览',
path: '/home',
icon: 'ri-home-line'
},
{
id: 'chat-with-llm',
title: 'AI对话',
path: '/chat-with-llm',
icon: 'ri-chat-smile-2-line'
},
{
id: 'file-management',
title: '文件管理',
path: '/files',
icon: 'ri-folder-line',
children: [
{
id: 'file-upload',
title: '文件上传',
path: '/files/upload',
icon: 'ri-upload-cloud-line'
},
{
id: 'documents',
title: '文档列表',
path: '/documents',
icon: 'ri-file-list-3-line'
}
]
},
{
id: 'rule-management',
title: '评查规则库',
path: '/rules',
icon: 'ri-book-3-line',
children: [
{
id: 'rule-groups',
title: '评查点分组',
path: '/rule-groups',
icon: 'ri-folder-open-line'
},
{
id: 'rules-list',
title: '评查点列表',
path: '/rules',
icon: 'ri-list-check-3'
},
{
id: 'rules-file',
title: '评查文件列表',
path: '/rules-files',
icon: 'ri-list-check-2'
},
{
id: 'cross-checking',
title: '交叉评查',
path: '/cross-checking',
icon: 'ri-color-filter-line'
},
// {
// id: 'rule-new',
// title: '新增评查点',
// path: '/rules-new',
// requiredRole: 'developer',
// icon: 'ri-add-circle-line'
// },
// {
// id: 'review-detail',
// title: '评查详情',
// path: '/reviews',
// icon: 'ri-file-chart-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: 'system-settings',
title: '系统设置',
path: '/settings',
icon: 'ri-settings-4-line',
requiredRole: 'developer',
children: [
{
id: 'config-lists',
title: '配置列表',
path: '/config-lists',
icon: 'ri-list-check-3',
requiredRole: 'developer'
},
// {
// id: 'basic-settings',
// title: '基础设置',
// path: '/settings',
// icon: 'ri-equalizer-line'
// },
{
id: 'document-types',
title: '文档类型',
path: '/document-types',
icon: 'ri-file-list-line',
requiredRole: 'developer'
},
{
id: 'prompt-management',
title: '提示词管理',
path: '/prompts',
icon: 'ri-chat-1-line',
requiredRole: 'developer'
}
]
}
];
// 初始化展开状态,默认全部展开
useEffect(() => {
const initialExpandedState: Record<string, boolean> = {};
@@ -280,7 +174,7 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid
}
});
setExpandedMenus(initialExpandedState);
}, []);
}, [menuItems]);
const toggleMenu = (id: string, e: React.MouseEvent) => {
// 我们只防止事件冒泡,不阻止默认行为
@@ -318,13 +212,8 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid
// const visibleMenuIds = APP_MENU_MAP[currentApp as keyof typeof APP_MENU_MAP]
// console.log('当前应用模式:', currentApp, '可见菜单ID:', visibleMenuIds);
// 根据用户角色和当前应用模式过滤菜单项
// 根据当前应用模式过滤菜单项
const filteredMenuItems = menuItems.filter(item => {
// 如果菜单项需要特定角色但用户没有
if (item.requiredRole && item.requiredRole !== userRole) {
return false;
}
// 检查当前菜单是否在所选应用模式中显示
if (!visibleMenuIds.includes(item.id)) {
return false;
@@ -382,7 +271,7 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid
)}
<div className="py-4 px-[10px]">
{isLoading ? (
{isLoading || isLoadingRoutes ? (
// 加载中状态显示,保留菜单布局结构
<div className="py-2">
{Array(5).fill(0).map((_, index) => (
@@ -444,19 +333,17 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid
className={`submenu-container ${collapsed ? 'border-l-0 pl-0' : 'border-l border-gray-100 ml-4 pl-3'} z-20`}
id={`submenu-${item.id}`}
>
{item.children
.filter(child => !child.requiredRole || child.requiredRole === userRole)
.map((child) => (
<Link
key={child.id}
to={child.path}
className={`sidebar-menu-item ${isActive(child.path) ? 'active' : ''} flex items-center ${collapsed ? 'justify-center' : ''}`}
onClick={(e) => handleSubMenuClick(child, e)}
>
<i className={`${child.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
{!collapsed && <span>{child.title}</span>}
</Link>
))}
{item.children.map((child) => (
<Link
key={child.id}
to={child.path}
className={`sidebar-menu-item ${isActive(child.path) ? 'active' : ''} flex items-center ${collapsed ? 'justify-center' : ''}`}
onClick={(e) => handleSubMenuClick(child, e)}
>
<i className={`${child.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
{!collapsed && <span>{child.title}</span>}
</Link>
))}
</div>
)}
</>
+13 -3
View File
@@ -1,7 +1,7 @@
import { type LoaderFunctionArgs, redirect } from "@remix-run/node";
import { OAuthClient } from "~/api/login/oauth-client";
import { OAUTH_CONFIG } from "~/config/api-config";
import { sessionStorage } from "~/api/login/auth.server";
import { sessionStorage, saveUserInfo } from "~/api/login/auth.server";
import { toastService } from "~/components/ui";
export async function loader({ request }: LoaderFunctionArgs) {
@@ -58,14 +58,24 @@ export async function loader({ request }: LoaderFunctionArgs) {
session.set("tokenExpiresIn", tokenResponse.expires_in);
session.set("userInfo", userInfo.data);
// 根据用户信息判断用户角色,这里可以根据实际业务逻辑调整
const userRole = userInfo.data.username === "admin" ? "developer" : "common";
// TODO 根据用户信息判断用户角色,这里可以根据实际业务逻辑调整 暂定都是common
// const userRole = userInfo.data.username === "admin" ? "developer" : "common";
const userRole = "common";
session.set("userRole", userRole);
// 获取重定向URL
const redirectTo = url.searchParams.get("redirect") || "/";
const cookie = await sessionStorage.commitSession(session);
// 成功获取用户信息之后通过auth.server.ts中的saveUserInfo方法去写入自己的数据库中,通过sub作为唯一值去添加数据
const saveResult = await saveUserInfo(userInfo.data);
if (!saveResult.success) {
console.error("保存用户信息到数据库失败:", saveResult.error);
// 注意:即使保存到数据库失败,我们仍然继续登录流程,因为用户已经通过了身份验证
} else {
console.log("用户信息已成功保存到数据库");
}
return redirect(redirectTo, {
headers: {
+10
View File
@@ -35,6 +35,11 @@ export const meta: MetaFunction = () => {
// 数据加载器
export const loader = async ({ request }: LoaderFunctionArgs) => {
// 获取用户会话信息
const { getUserSession } = await import("~/api/login/auth.server");
const { userInfo } = await getUserSession(request);
console.log(userInfo);
// 获取URL查询参数,只保留必要的分页参数
const url = new URL(request.url);
const page = parseInt(url.searchParams.get("page") || "1", 10);
@@ -55,6 +60,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
page,
pageSize,
documentTypeOptions,
userInfo, // 传递用户信息到客户端
initialLoad: true // 标记这是初始加载
});
};
@@ -195,6 +201,9 @@ export default function DocumentsIndex() {
loadingBarService.show();
try {
// 从loader data中获取用户ID
const userId = loaderData.userInfo?.user_id?.toString();
// 构建搜索参数
const searchParams = {
name: search || undefined,
@@ -205,6 +214,7 @@ export default function DocumentsIndex() {
dateFrom: dateFrom || undefined,
dateTo: dateTo || undefined,
reviewType: storedReviewType || undefined,
userId: userId, // 添加用户ID筛选
page: currentPage,
pageSize
};
+26 -10
View File
@@ -3,7 +3,7 @@ import { useSearchParams, Form } from "@remix-run/react";
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs, redirect } from "@remix-run/node";
import { OAuthClient } from "~/api/login/oauth-client";
import { OAUTH_CONFIG } from "~/config/api-config";
import { getUserSession, getSession, createUserSession } from "~/api/login/auth.server";
import { getUserSession, getSession, createUserSessionWithInfo, getUserBySub, addDefaultRole } from "~/api/login/auth.server";
import styles from "~/styles/pages/login.css?url";
export const links = () => [
@@ -44,13 +44,29 @@ export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "temp_admin_login") {
if (intent === "test_user_login") {
// 获取重定向目标
const session = await getSession(request);
const redirectTo = session.get("redirectTo") || "/";
// 创建管理员会话
return createUserSession(true, 'developer', redirectTo);
// 使用测试用户登录
const testUserSub = "001"; // 测试用户的sub
const userResult = await getUserBySub(testUserSub);
if (userResult.success && userResult.data) {
const user = userResult.data;
// 确保用户有默认角色
if (user.id) {
await addDefaultRole(user.id, 2); // 添加common角色
}
// 创建用户会话,默认角色为common,并保存用户信息
return createUserSessionWithInfo(true, 'common', redirectTo, user);
} else {
// 如果用户不存在,重定向到登录页面并显示错误
return redirect(`/login?error=${encodeURIComponent("测试用户不存在")}`);
}
}
return null;
@@ -149,25 +165,25 @@ export default function Login() {
</div>
</div>
{/* 临时管理员登录区域 */}
{/* 测试用户登录区域 */}
<div className="temp-login-section">
<div className="section-divider">
<span></span>
</div>
<Form method="post" className="temp-login-form">
<input type="hidden" name="intent" value="temp_admin_login" />
<input type="hidden" name="intent" value="test_user_login" />
<button
type="submit"
className="temp-admin-login-button"
>
<i className="ri-admin-line"></i>
<i className="ri-user-line"></i>
</button>
<div className="temp-login-tips">
<p>
<i className="ri-alert-line"></i>
使
<i className="ri-information-line"></i>
使(testuser1)
</p>
</div>
</Form>