feat: 1. 添加axios全局路由拦截进行自动添加请求jwt。 2.重新整理路由表。 3. 文档列表新增版本差异对比。 4.菜单路由可访问列表通过对接接口返回,添加全局路由检测。
5. 修改统一认证登录和管理员登录是通过接口形式进行,存储返回的accessToken。 6. 修改交叉评查的部分样式
This commit is contained in:
+170
-70
@@ -1,7 +1,34 @@
|
||||
import { toastService } from '~/components/ui';
|
||||
import { postgrestGet } from '../postgrest-client';
|
||||
import { apiRequest } from '../axios-client';
|
||||
|
||||
// 路由数据接口
|
||||
// 后端返回的路由数据接口
|
||||
export interface BackendRouteInfo {
|
||||
id: number;
|
||||
route_path: string;
|
||||
route_name: string;
|
||||
component: string;
|
||||
parent_id: number | null;
|
||||
route_title: string;
|
||||
icon: string | null;
|
||||
sort_order: number;
|
||||
is_hidden: boolean;
|
||||
is_cache: boolean;
|
||||
meta: string;
|
||||
children?: BackendRouteInfo[];
|
||||
}
|
||||
|
||||
// 后端API响应接口
|
||||
export interface BackendRoutesResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: {
|
||||
user_id: number;
|
||||
username: string;
|
||||
routes: BackendRouteInfo[];
|
||||
};
|
||||
}
|
||||
|
||||
// 旧的路由数据接口(保留用于兼容)
|
||||
export interface RouteInfo {
|
||||
id: number;
|
||||
path: string;
|
||||
@@ -458,82 +485,87 @@ const FALLBACK_MENU_DATA: Record<string, MenuItem[]> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据角色获取用户可访问的路由
|
||||
* @param roleKey 角色标识 (如: 'admin', 'common', 'deptLeader', 'groupLeader')
|
||||
* 根据角色获取用户可访问的路由(调用后端统一接口)
|
||||
* @param roleKey 角色标识 (如: 'admin', 'common', 'deptLeader', 'groupLeader') - 暂时不使用,后端通过JWT自动识别
|
||||
* @param jwt JWT token
|
||||
* @returns 用户可访问的路由列表
|
||||
*/
|
||||
export async function getUserRoutesByRole(roleKey: string, jwt?: string): Promise<{ success: boolean; data?: MenuItem[]; error?: string; shouldRedirectToHome?: boolean }> {
|
||||
try {
|
||||
console.log(`获取角色 ${roleKey} 的路由权限`);
|
||||
console.log(`🔍 [User Routes] 获取用户路由,角色: ${roleKey}`);
|
||||
|
||||
// 首先获取角色ID
|
||||
const roleResult = await postgrestGet<Array<{id: number}>>("roles", {
|
||||
filter: {
|
||||
"role_key": `eq.${roleKey}`
|
||||
},
|
||||
token: jwt
|
||||
});
|
||||
|
||||
if (roleResult.error || !roleResult.data || roleResult.data.length === 0) {
|
||||
console.error("角色不存在:", roleKey);
|
||||
toastService.error("角色不存在,请联系管理员配置权限后重新登录");
|
||||
return { success: false, error: "角色不存在", shouldRedirectToHome: true };
|
||||
if (!jwt) {
|
||||
console.error('❌ [User Routes] JWT token 未提供');
|
||||
toastService.error("认证信息缺失,请重新登录");
|
||||
return { success: false, error: "JWT token 未提供", shouldRedirectToHome: true };
|
||||
}
|
||||
|
||||
const roleId = roleResult.data[0].id;
|
||||
// 调用后端统一接口获取用户路由
|
||||
// 注意:Authorization 头会由 axios 拦截器自动添加(从 localStorage 读取)
|
||||
// 但为了确保使用正确的 token,这里仍然显式传递
|
||||
const response = await apiRequest<BackendRoutesResponse>(
|
||||
'/rbac/user/routes', // endpoint (第一个参数)
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${jwt}`
|
||||
}
|
||||
} // options (第二个参数)
|
||||
);
|
||||
|
||||
// 查询角色的路由权限
|
||||
const roleRoutesResult = await postgrestGet<Array<{route_id: number}>>("role_route", {
|
||||
filter: {
|
||||
"role_id": `eq.${roleId}`
|
||||
},
|
||||
token: jwt
|
||||
});
|
||||
// console.log('🔍 [User Routes] 后端返回:', response);
|
||||
|
||||
if (roleRoutesResult.error) {
|
||||
console.error("查询角色路由关联失败:", roleRoutesResult.error);
|
||||
toastService.error("查询角色路由关联失败,请稍后再试");
|
||||
return { success: false, error: "查询角色路由关联失败", shouldRedirectToHome: true };
|
||||
// 检查响应是否成功
|
||||
if (response.error) {
|
||||
console.error('❌ [User Routes] API 请求失败:', response.error);
|
||||
toastService.error(response.error);
|
||||
return { success: false, error: response.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 };
|
||||
// 检查响应数据
|
||||
if (!response.data) {
|
||||
console.error('❌ [User Routes] 后端未返回数据');
|
||||
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",
|
||||
token: jwt
|
||||
});
|
||||
const backendResponse = response.data;
|
||||
|
||||
if (routesResult.error) {
|
||||
console.error("查询路由信息失败:", routesResult.error);
|
||||
toastService.error("查询路由信息失败,请稍后再试");
|
||||
return { success: false, error: "查询路由信息失败", shouldRedirectToHome: true };
|
||||
// 检查业务状态码(后端使用 code: 0 表示成功)
|
||||
if (backendResponse.code !== 0 && backendResponse.code !== 200) {
|
||||
console.error(`❌ [User Routes] 后端返回错误: ${backendResponse.msg}`);
|
||||
toastService.error(backendResponse.msg || "获取路由权限失败");
|
||||
return { success: false, error: backendResponse.msg || "获取路由权限失败", shouldRedirectToHome: true };
|
||||
}
|
||||
|
||||
const routes = routesResult.data || [];
|
||||
|
||||
// 构建菜单树
|
||||
const menuItems = buildMenuTreeFromRoutes(routes);
|
||||
|
||||
console.log(`角色 ${roleKey} 可访问 ${menuItems.length} 个路由`);
|
||||
// 检查数据完整性
|
||||
if (!backendResponse.data || !Array.isArray(backendResponse.data.routes)) {
|
||||
console.error('❌ [User Routes] 后端未返回路由数据');
|
||||
toastService.error("未获取到路由权限,请联系管理员配置");
|
||||
return { success: false, error: "后端未返回路由数据", shouldRedirectToHome: true };
|
||||
}
|
||||
|
||||
const routes = backendResponse.data.routes;
|
||||
|
||||
if (routes.length === 0) {
|
||||
console.log(`⚠️ [User Routes] 用户没有分配任何路由权限`);
|
||||
toastService.error("您的角色没有分配任何路由权限,请联系管理员配置");
|
||||
return { success: false, error: "用户没有分配任何路由权限", shouldRedirectToHome: true };
|
||||
}
|
||||
|
||||
// 将后端路由格式转换为前端 MenuItem 格式
|
||||
const menuItems = convertBackendRoutesToMenuItems(routes);
|
||||
|
||||
console.log(`✅ [User Routes] 成功获取 ${menuItems.length} 个路由`);
|
||||
// console.log('📋 [User Routes] 菜单数据:', menuItems);
|
||||
|
||||
return { success: true, data: menuItems };
|
||||
|
||||
} catch (error) {
|
||||
console.error("获取用户路由时发生错误:", error);
|
||||
console.error("❌ [User Routes] 获取用户路由时发生错误:", error);
|
||||
toastService.error("获取用户路由时发生错误,请稍后再试");
|
||||
return {
|
||||
success: false,
|
||||
return {
|
||||
success: false,
|
||||
error: `获取用户路由失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
shouldRedirectToHome: true
|
||||
};
|
||||
@@ -541,14 +573,76 @@ export async function getUserRoutesByRole(roleKey: string, jwt?: string): Promis
|
||||
}
|
||||
|
||||
/**
|
||||
* 从路由信息构建菜单树结构
|
||||
* Element UI 图标到 RemixIcon 的映射
|
||||
*/
|
||||
const ICON_MAPPING: Record<string, string> = {
|
||||
'el-icon-s-home': 'ri-home-line',
|
||||
'el-icon-house': 'ri-home-4-line',
|
||||
'el-icon-document': 'ri-file-text-line',
|
||||
'el-icon-edit': 'ri-edit-line',
|
||||
'el-icon-connection': 'ri-links-line',
|
||||
'el-icon-setting': 'ri-settings-4-line',
|
||||
'el-icon-user': 'ri-user-line',
|
||||
'el-icon-tickets': 'ri-ticket-line',
|
||||
'el-icon-chat-dot-round': 'ri-chat-smile-2-line',
|
||||
'el-icon-s-order': 'ri-list-check',
|
||||
'el-icon-s-grid': 'ri-grid-line',
|
||||
'el-icon-s-comment': 'ri-chat-1-line',
|
||||
'el-icon-files': 'ri-file-copy-line',
|
||||
'el-icon-folder': 'ri-folder-line',
|
||||
'el-icon-upload': 'ri-upload-cloud-line',
|
||||
'el-icon-download': 'ri-download-cloud-line',
|
||||
'el-icon-search': 'ri-search-line',
|
||||
};
|
||||
|
||||
/**
|
||||
* 转换 Element UI 图标为 RemixIcon
|
||||
*/
|
||||
function convertIcon(elementIcon: string | null): string {
|
||||
if (!elementIcon) {
|
||||
return 'ri-file-line'; // 默认图标
|
||||
}
|
||||
return ICON_MAPPING[elementIcon] || 'ri-file-line';
|
||||
}
|
||||
|
||||
/**
|
||||
* 将后端路由格式转换为前端 MenuItem 格式
|
||||
* @param backendRoutes 后端返回的路由数组
|
||||
* @returns MenuItem 数组
|
||||
*/
|
||||
function convertBackendRoutesToMenuItems(backendRoutes: BackendRouteInfo[]): MenuItem[] {
|
||||
return backendRoutes
|
||||
.filter(route => !route.is_hidden) // 过滤隐藏的路由
|
||||
.map(route => {
|
||||
const menuItem: MenuItem = {
|
||||
id: route.route_name || `route-${route.id}`,
|
||||
title: route.route_title,
|
||||
path: route.route_path,
|
||||
icon: convertIcon(route.icon),
|
||||
order: route.sort_order,
|
||||
hideBreadcrumb: route.is_hidden
|
||||
};
|
||||
|
||||
// 递归处理子路由
|
||||
if (route.children && route.children.length > 0) {
|
||||
menuItem.children = convertBackendRoutesToMenuItems(route.children);
|
||||
}
|
||||
|
||||
return menuItem;
|
||||
})
|
||||
.sort((a, b) => a.order - b.order); // 按 sort_order 排序
|
||||
}
|
||||
|
||||
/**
|
||||
* 从路由信息构建菜单树结构(旧版本,已废弃)
|
||||
* @param routes 路由信息数组
|
||||
* @returns 菜单树结构
|
||||
* @deprecated 使用 convertBackendRoutesToMenuItems 替代
|
||||
*/
|
||||
function buildMenuTreeFromRoutes(routes: RouteInfo[]): MenuItem[] {
|
||||
// 转换为MenuItem格式
|
||||
const menuMap = new Map<number, MenuItem>();
|
||||
|
||||
|
||||
routes.forEach(route => {
|
||||
const menuItem: MenuItem = {
|
||||
id: route.name,
|
||||
@@ -558,25 +652,25 @@ function buildMenuTreeFromRoutes(routes: RouteInfo[]): MenuItem[] {
|
||||
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);
|
||||
@@ -587,7 +681,7 @@ function buildMenuTreeFromRoutes(routes: RouteInfo[]): MenuItem[] {
|
||||
parent.children.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 排序
|
||||
rootItems.sort((a, b) => a.order - b.order);
|
||||
rootItems.forEach(item => {
|
||||
@@ -595,7 +689,7 @@ function buildMenuTreeFromRoutes(routes: RouteInfo[]): MenuItem[] {
|
||||
item.children.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return rootItems;
|
||||
}
|
||||
|
||||
@@ -609,8 +703,14 @@ export function mapUserRoleToRoleKey(userRole: string): string {
|
||||
'common': 'common',
|
||||
'admin': 'admin',
|
||||
'deptLeader': 'deptLeader',
|
||||
'groupLeader': 'groupLeader'
|
||||
'groupLeader': 'groupLeader',
|
||||
// 添加常见的后端角色映射
|
||||
'super_admin': 'admin',
|
||||
'system_admin': 'admin',
|
||||
'user': 'common',
|
||||
'developer': 'admin'
|
||||
};
|
||||
|
||||
return roleMapping[userRole];
|
||||
|
||||
// 如果找不到映射,返回 userRole 本身(假设后端已经返回了正确的 role_key)
|
||||
return roleMapping[userRole] || userRole || 'common';
|
||||
}
|
||||
@@ -43,6 +43,86 @@ const axiosInstance = axios.create({
|
||||
}
|
||||
});
|
||||
|
||||
// 请求白名单 - 这些接口不需要添加 Authorization 头
|
||||
const AUTH_WHITELIST = [
|
||||
'/auth/login',
|
||||
'/auth/refresh',
|
||||
'/auth/register',
|
||||
'/oauth/token',
|
||||
'/oauth/userinfo'
|
||||
];
|
||||
|
||||
/**
|
||||
* 检查请求URL是否在白名单中
|
||||
*/
|
||||
function isInAuthWhitelist(url?: string): boolean {
|
||||
if (!url) return false;
|
||||
return AUTH_WHITELIST.some(path => url.includes(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求拦截器 - 自动添加 Authorization 头
|
||||
*/
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
// 检查是否在白名单中
|
||||
if (isInAuthWhitelist(config.url)) {
|
||||
return config;
|
||||
}
|
||||
|
||||
// 从 localStorage 获取 token (浏览器环境)
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 自定义错误类:表示需要重新登录
|
||||
*/
|
||||
export class AuthenticationError extends Error {
|
||||
constructor(message = 'Token 已过期或无效,请重新登录') {
|
||||
super(message);
|
||||
this.name = 'AuthenticationError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应拦截器 - 处理 401 错误(token 过期)
|
||||
*/
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response) => {
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
if (isAxiosError(error) && error.response?.status === 401) {
|
||||
// Token 过期或无效
|
||||
console.warn('⚠️ Token 已过期或无效,请重新登录');
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// 🌐 客户端环境:清除 localStorage 并跳转
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('user_info');
|
||||
window.location.href = '/login';
|
||||
} else {
|
||||
// 🖥️ 服务端环境:抛出特殊错误,由 loader/action 处理
|
||||
console.warn('⚠️ [Server] 检测到 401 错误,抛出 AuthenticationError');
|
||||
throw new AuthenticationError('Token 已过期或无效,请重新登录');
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 最大重试次数
|
||||
const MAX_RETRIES = 2;
|
||||
|
||||
|
||||
@@ -158,7 +158,8 @@ export async function getReviewPoints(fileId: string, request: Request) {
|
||||
token: frontendJWT
|
||||
};
|
||||
const contractStructureComparisonResponse = await postgrestGet('contract_structure_comparison', contractStructureComparisonParams);
|
||||
|
||||
// console.log('contract_structure_comparison', contractStructureComparisonResponse)
|
||||
|
||||
if (contractStructureComparisonResponse.error) {
|
||||
console.error("获取文档附件数据错误:", contractStructureComparisonResponse.error);
|
||||
return Response.json({ error: contractStructureComparisonResponse.error }, { status: contractStructureComparisonResponse.status || 500 });
|
||||
|
||||
@@ -100,7 +100,6 @@ export interface DocumentSearchParams {
|
||||
sortOrder?: string; // 排序方式
|
||||
page?: number; // 当前页码
|
||||
pageSize?: number; // 每页条数
|
||||
token?: string; // JWT token
|
||||
}
|
||||
|
||||
|
||||
@@ -169,8 +168,7 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}, do
|
||||
reviewStatus,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
sortOrder = 'upload_time_desc',
|
||||
token
|
||||
sortOrder = 'upload_time_desc'
|
||||
} = searchParams;
|
||||
|
||||
let p_typeid: number[] | null = null;
|
||||
@@ -206,8 +204,8 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}, do
|
||||
|
||||
// 并行执行获取数据和获取总数的请求
|
||||
const [filesResponse, countResponse] = await Promise.all([
|
||||
postgrestPost<ReviewFileFromSQL[]>('rpc/get_review_files_with_details', listParams, token),
|
||||
postgrestPost<number>('rpc/count_review_files', rpcParams, token)
|
||||
postgrestPost<ReviewFileFromSQL[]>('rpc/get_review_files_with_details', listParams),
|
||||
postgrestPost<number>('rpc/count_review_files', rpcParams)
|
||||
]);
|
||||
|
||||
// 处理获取文档列表的错误
|
||||
@@ -318,10 +316,9 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}, do
|
||||
* @param id 文件ID
|
||||
* @param auditStatus 审核状态
|
||||
* @param userId 用户ID
|
||||
* @param token JWT token (可选)
|
||||
* @returns 更新结果
|
||||
*/
|
||||
export async function updateDocumentAuditStatus(id: string, auditStatus: number, userId: string, token?: string): Promise<{
|
||||
export async function updateDocumentAuditStatus(id: string, auditStatus: number, userId: string): Promise<{
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
status?: number;
|
||||
@@ -347,8 +344,7 @@ export async function updateDocumentAuditStatus(id: string, auditStatus: number,
|
||||
{
|
||||
id: parseInt(id),
|
||||
user_id: parseInt(userId) // 确保只能更新自己的文档
|
||||
},
|
||||
token
|
||||
}
|
||||
);
|
||||
|
||||
console.log('📝 [updateDocumentAuditStatus] postgrestPut响应:', response);
|
||||
|
||||
+253
-1
@@ -93,6 +93,36 @@ export interface DocumentUI {
|
||||
updatedAt?: string;
|
||||
pageCount?: number;
|
||||
ocrResult?: unknown;
|
||||
// 版本管理相关字段
|
||||
historyCount?: number; // 历史版本数量(不含当前版本)
|
||||
previousIssues?: number | null; // 上一个版本的问题数量
|
||||
isExpanded?: boolean; // 是否展开历史版本(前端状态)
|
||||
historyVersions?: DocumentVersionUI[]; // 历史版本列表
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档历史版本结构
|
||||
*/
|
||||
export interface DocumentVersionUI {
|
||||
id: number;
|
||||
name: string;
|
||||
documentNumber: string;
|
||||
type: string;
|
||||
typeName: string;
|
||||
size: number;
|
||||
auditStatus: number;
|
||||
fileStatus: string;
|
||||
issues: number | null;
|
||||
issuesDiff?: number; // 与上一个版本的问题数量差异(绝对值)
|
||||
issuesDiffType?: 'increase' | 'decrease' | 'same'; // 差异类型
|
||||
uploadTime: string;
|
||||
fileType: string;
|
||||
path: string;
|
||||
isTest: boolean;
|
||||
updatedAt?: string;
|
||||
pageCount?: number;
|
||||
ocrResult?: unknown;
|
||||
versionNumber?: number; // 版本号(v2, v3, v4...)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -419,6 +449,8 @@ export async function getDocumentWithNoUserId(id: string, frontendJWT?: string):
|
||||
if (!id) {
|
||||
return { error: '文档ID不能为空', status: 400 };
|
||||
}
|
||||
|
||||
// console.log("get单个文档id", id)
|
||||
|
||||
const response = await postgrestGet<Document[]>(
|
||||
'documents',
|
||||
@@ -435,6 +467,7 @@ export async function getDocumentWithNoUserId(id: string, frontendJWT?: string):
|
||||
return { error: response.error, status: response.status };
|
||||
}
|
||||
|
||||
// console.log("respose", response)
|
||||
const extractedData = extractApiData<Document[]>(response.data);
|
||||
if (!extractedData || extractedData.length === 0) {
|
||||
return { error: '文档不存在', status: 404 };
|
||||
@@ -554,7 +587,7 @@ export async function updateDocument(id: string, document: Partial<DocumentUI> &
|
||||
// 获取更新后的完整文档数据
|
||||
const updatedResponse = await getDocument(id, userId, frontendJWT);
|
||||
|
||||
return updatedResponse;
|
||||
return updatedResponse;
|
||||
} catch (error) {
|
||||
console.error('更新文档信息失败:', error);
|
||||
return {
|
||||
@@ -562,4 +595,223 @@ export async function updateDocument(id: string, document: Partial<DocumentUI> &
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文档列表(带版本信息)- 使用 RPC 函数
|
||||
* @param searchParams 搜索参数
|
||||
* @returns 文档列表和总数
|
||||
*/
|
||||
export async function getDocumentsWithVersionInfo(searchParams: DocumentSearchParams = {}): Promise<{
|
||||
data?: { documents: DocumentUI[], total: number };
|
||||
error?: string;
|
||||
status?: number;
|
||||
}> {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
name,
|
||||
documentNumber,
|
||||
documentType,
|
||||
auditStatus,
|
||||
fileStatus,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
reviewType,
|
||||
userId,
|
||||
token
|
||||
} = searchParams;
|
||||
|
||||
// 确保userId必须存在
|
||||
if (!userId) {
|
||||
return { error: '用户身份验证失败,无法获取文档列表', status: 401 };
|
||||
}
|
||||
|
||||
// 处理文档类型
|
||||
let documentTypes: number[] | undefined;
|
||||
if (documentType) {
|
||||
documentTypes = [parseInt(documentType, 10)];
|
||||
} else if (reviewType) {
|
||||
if (reviewType === 'contract') {
|
||||
documentTypes = [1];
|
||||
} else if (reviewType === 'record') {
|
||||
documentTypes = [2, 3, 155];
|
||||
}
|
||||
}
|
||||
|
||||
// 准备RPC调用参数
|
||||
const rpcParams = {
|
||||
p_user_id: parseInt(userId, 10),
|
||||
p_page: page,
|
||||
p_page_size: pageSize,
|
||||
p_search_name: name || null,
|
||||
p_search_document_number: documentNumber || null,
|
||||
p_search_document_types: documentTypes || null,
|
||||
p_search_audit_status: auditStatus !== undefined ? parseInt(auditStatus, 10) : null,
|
||||
p_search_file_status: fileStatus || null,
|
||||
p_search_date_from: dateFrom || null,
|
||||
p_search_date_to: dateTo || null
|
||||
};
|
||||
|
||||
// 并行执行获取数据和获取总数的请求
|
||||
const [documentsResponse, countResponse] = await Promise.all([
|
||||
postgrestPost<any[], unknown>('rpc/documents_get_latest_documents_with_version_info', rpcParams, token),
|
||||
postgrestPost<number, unknown>('rpc/documents_count_latest_documents_with_filters', {
|
||||
p_user_id: rpcParams.p_user_id,
|
||||
p_search_name: rpcParams.p_search_name,
|
||||
p_search_document_number: rpcParams.p_search_document_number,
|
||||
p_search_document_types: rpcParams.p_search_document_types,
|
||||
p_search_audit_status: rpcParams.p_search_audit_status,
|
||||
p_search_file_status: rpcParams.p_search_file_status,
|
||||
p_search_date_from: rpcParams.p_search_date_from,
|
||||
p_search_date_to: rpcParams.p_search_date_to
|
||||
}, token)
|
||||
]);
|
||||
|
||||
// 处理获取文档列表的错误
|
||||
if (documentsResponse.error || !documentsResponse.data) {
|
||||
return { error: documentsResponse.error || '获取文档数据失败', status: documentsResponse.status || 500 };
|
||||
}
|
||||
|
||||
// 处理获取总数的错误
|
||||
if (countResponse.error || typeof countResponse.data !== 'number') {
|
||||
console.error('获取文档总数失败:', countResponse.error);
|
||||
}
|
||||
|
||||
const totalCount = typeof countResponse.data === 'number' ? countResponse.data : 0;
|
||||
|
||||
// 将RPC返回的数据转换为UI格式
|
||||
const documents: DocumentUI[] = documentsResponse.data.map((doc: any) => ({
|
||||
id: doc.id,
|
||||
name: doc.name,
|
||||
documentNumber: doc.document_number,
|
||||
type: doc.type_id.toString(),
|
||||
typeName: doc.type_name || '未知类型',
|
||||
size: doc.file_size,
|
||||
auditStatus: doc.audit_status ?? 0,
|
||||
fileStatus: doc.status || '',
|
||||
issues: doc.false_count ?? null,
|
||||
uploadTime: formatDate(doc.updated_at),
|
||||
fileType: getFileExtension(doc.name),
|
||||
path: doc.path,
|
||||
isTest: doc.is_test_document,
|
||||
updatedAt: formatDate(doc.updated_at),
|
||||
pageCount: doc.ocr_result?.__meta?.page_count || 0,
|
||||
ocrResult: doc.ocr_result,
|
||||
historyCount: doc.history_count || 0,
|
||||
previousIssues: doc.previous_issues
|
||||
}));
|
||||
|
||||
return {
|
||||
data: {
|
||||
documents,
|
||||
total: totalCount
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取文档列表失败:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '获取文档列表失败',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文档历史版本列表
|
||||
* @param documentName 文档名称
|
||||
* @param userId 用户ID
|
||||
* @param excludeId 排除的文档ID(当前最新版本的ID)
|
||||
* @param token JWT token
|
||||
* @returns 历史版本列表
|
||||
*/
|
||||
export async function getDocumentHistory(
|
||||
documentName: string,
|
||||
userId: string,
|
||||
excludeId: number,
|
||||
token?: string
|
||||
): Promise<{
|
||||
data?: DocumentVersionUI[];
|
||||
error?: string;
|
||||
status?: number;
|
||||
}> {
|
||||
try {
|
||||
if (!documentName) {
|
||||
return { error: '文档名称不能为空', status: 400 };
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return { error: '用户身份验证失败', status: 401 };
|
||||
}
|
||||
|
||||
// 调用 RPC 函数获取历史版本
|
||||
const response = await postgrestPost<any[], unknown>(
|
||||
'rpc/documents_get_document_history',
|
||||
{
|
||||
p_document_name: documentName,
|
||||
p_user_id: parseInt(userId, 10),
|
||||
p_exclude_id: excludeId
|
||||
},
|
||||
token
|
||||
);
|
||||
|
||||
if (response.error || !response.data) {
|
||||
return { error: response.error || '获取历史版本失败', status: response.status || 500 };
|
||||
}
|
||||
|
||||
const historyDocs = response.data;
|
||||
|
||||
// 转换为 UI 格式,并计算问题数量差异
|
||||
const documents: DocumentVersionUI[] = historyDocs.map((doc: any, index: number) => {
|
||||
// 计算与下一个版本(更早的版本)的问题数量差异
|
||||
let issuesDiff: number | undefined;
|
||||
let issuesDiffType: 'increase' | 'decrease' | 'same' | undefined;
|
||||
|
||||
if (index < historyDocs.length - 1) {
|
||||
const olderDoc = historyDocs[index + 1];
|
||||
if (doc.false_count != null && olderDoc.false_count != null) {
|
||||
const diff = doc.false_count - olderDoc.false_count;
|
||||
issuesDiff = Math.abs(diff);
|
||||
if (diff > 0) {
|
||||
issuesDiffType = 'increase';
|
||||
} else if (diff < 0) {
|
||||
issuesDiffType = 'decrease';
|
||||
} else {
|
||||
issuesDiffType = 'same';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: doc.id,
|
||||
name: doc.name,
|
||||
documentNumber: doc.document_number,
|
||||
type: doc.type_id.toString(),
|
||||
typeName: doc.type_name || '未知类型',
|
||||
size: doc.file_size,
|
||||
auditStatus: doc.audit_status ?? 0,
|
||||
fileStatus: doc.status || '',
|
||||
issues: doc.false_count ?? null,
|
||||
issuesDiff,
|
||||
issuesDiffType,
|
||||
uploadTime: formatDate(doc.created_at),
|
||||
fileType: getFileExtension(doc.name),
|
||||
path: doc.path,
|
||||
isTest: doc.is_test_document,
|
||||
updatedAt: formatDate(doc.updated_at),
|
||||
pageCount: doc.ocr_result?.__meta?.page_count || 0,
|
||||
ocrResult: doc.ocr_result,
|
||||
versionNumber: historyDocs.length - index
|
||||
};
|
||||
});
|
||||
|
||||
return { data: documents };
|
||||
} catch (error) {
|
||||
console.error('获取文档历史版本失败:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '获取文档历史版本失败',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -175,9 +175,13 @@ export async function uploadContractTemplate(
|
||||
const headers: HeadersInit = {
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
if (jwtToken) {
|
||||
headers['Authorization'] = `Bearer ${jwtToken}`;
|
||||
|
||||
// 从 localStorage 获取 token
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
@@ -222,7 +226,6 @@ export async function uploadContractTemplate(
|
||||
* @param mergeMode 合并模式:'overwrite'(覆盖原文档)或 'new'(新建文档记录)
|
||||
* @param isReprocess 是否触发重新处理
|
||||
* @param remark 备注
|
||||
* @param jwtToken JWT token
|
||||
* @returns 上传结果
|
||||
*/
|
||||
export async function appendContractAttachments(
|
||||
@@ -230,8 +233,7 @@ export async function appendContractAttachments(
|
||||
files: File[],
|
||||
mergeMode: 'overwrite' | 'new' = 'overwrite',
|
||||
isReprocess: boolean = true,
|
||||
remark?: string,
|
||||
jwtToken?: string
|
||||
remark?: string
|
||||
): Promise<{data: FileUploadResponse; error?: never} | {data?: never; error: string; status?: number}> {
|
||||
try {
|
||||
console.log('【合同附件追加】开始追加附件:', { documentId, fileCount: files.length, mergeMode });
|
||||
@@ -259,9 +261,13 @@ export async function appendContractAttachments(
|
||||
const headers: HeadersInit = {
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
if (jwtToken) {
|
||||
headers['Authorization'] = `Bearer ${jwtToken}`;
|
||||
|
||||
// 从 localStorage 获取 token
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
|
||||
+1
-66
@@ -436,72 +436,7 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
|
||||
lastMonthIssuesCount = lastMonthType2Count
|
||||
|
||||
}
|
||||
// 暂时不会存在没有指定类型得情况,暂不实现。
|
||||
// else {
|
||||
// // 如果没有指定类型,则使用原来的查询方式获取所有类型的问题数量
|
||||
// const thisMonthIssuesParams: PostgrestParams = {
|
||||
// select: 'count',
|
||||
// filter: {
|
||||
// and: `(created_at.gte.${startOfThisMonth},created_at.lte.${endOfThisMonth})`,
|
||||
// 'evaluated_results->result': 'eq.false',
|
||||
// user_id: `eq.${userId}`
|
||||
// }
|
||||
// };
|
||||
|
||||
// // 添加类型过滤条件
|
||||
// if (typeFilter) {
|
||||
// if (typeFilter.startsWith('(')) {
|
||||
// thisMonthIssuesParams.or = typeFilter;
|
||||
// } else {
|
||||
// const [field, op, value] = typeFilter.split('.');
|
||||
// if (!thisMonthIssuesParams.filter) {
|
||||
// thisMonthIssuesParams.filter = {};
|
||||
// }
|
||||
// thisMonthIssuesParams.filter[field] = `${op}.${value}`;
|
||||
// }
|
||||
// }
|
||||
|
||||
// const thisMonthIssuesResponse = await handleApiResponse<{ count: number }[]>(
|
||||
// postgrestGet('evaluation_results', thisMonthIssuesParams),
|
||||
// '获取本月问题数据失败',
|
||||
// []
|
||||
// );
|
||||
|
||||
// // 本月问题数量
|
||||
// thisMonthIssuesCount = thisMonthIssuesResponse[0]?.count || 0;
|
||||
|
||||
// // 上月问题数量
|
||||
// const lastMonthIssuesParams: PostgrestParams = {
|
||||
// select: 'count',
|
||||
// filter: {
|
||||
// and: `(created_at.gte.${startOfLastMonth},created_at.lte.${endOfLastMonth})`,
|
||||
// 'evaluated_results->result': 'eq.false',
|
||||
// user_id: `eq.${userId}`
|
||||
// }
|
||||
// };
|
||||
|
||||
// // 添加类型过滤条件
|
||||
// if (typeFilter) {
|
||||
// if (typeFilter.startsWith('(')) {
|
||||
// lastMonthIssuesParams.or = typeFilter;
|
||||
// } else {
|
||||
// const [field, op, value] = typeFilter.split('.');
|
||||
// if (!lastMonthIssuesParams.filter) {
|
||||
// lastMonthIssuesParams.filter = {};
|
||||
// }
|
||||
// lastMonthIssuesParams.filter[field] = `${op}.${value}`;
|
||||
// }
|
||||
// }
|
||||
|
||||
// const lastMonthIssuesResponse = await handleApiResponse<{ count: number }[]>(
|
||||
// postgrestGet('evaluation_results', lastMonthIssuesParams),
|
||||
// '获取上月问题数据失败',
|
||||
// []
|
||||
// );
|
||||
|
||||
// // 上月问题数量
|
||||
// lastMonthIssuesCount = lastMonthIssuesResponse[0]?.count || 0;
|
||||
// }
|
||||
|
||||
|
||||
// 计算问题数量同比增长
|
||||
let issuesGrowthValue = 0;
|
||||
|
||||
@@ -30,7 +30,7 @@ import { OAUTH_CONFIG, API_BASE_URL } from "~/config/api-config";
|
||||
* @property {'common'} common - 普通用户,有基本的系统访问权限
|
||||
* @property {'developer'} developer - 开发者/管理员,有完整的系统管理权限
|
||||
*/
|
||||
export type UserRole = 'common' | 'admin' | 'deptLeader' | 'groupLeader';
|
||||
export type UserRole = 'common' | 'admin' | 'deptLeader' | 'groupLeader' | string;
|
||||
|
||||
/**
|
||||
* 用户信息接口,对应 sso_users 表结构
|
||||
@@ -365,7 +365,7 @@ export async function getUserSession(request: Request) {
|
||||
*/
|
||||
export async function createUserSession(params: {
|
||||
isAuthenticated: boolean;
|
||||
userRole: UserRole;
|
||||
userRole: string;
|
||||
redirectTo: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* 登录客户端
|
||||
* 调用后端 /auth/login 接口,传递 OAuth 用户信息,获取 JWT token
|
||||
*/
|
||||
|
||||
import { API_BASE_URL } from "~/config/api-config";
|
||||
|
||||
/**
|
||||
* 登录请求参数(OAuth 方式)
|
||||
*/
|
||||
export interface LoginRequest {
|
||||
userInfo: {
|
||||
sub: string;
|
||||
username?: string;
|
||||
nickname?: string;
|
||||
email?: string;
|
||||
phone_number?: string;
|
||||
ou_id?: string;
|
||||
ou_name?: string;
|
||||
is_leader?: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
expiresIn: number;
|
||||
area?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录响应
|
||||
*/
|
||||
export interface LoginResponse {
|
||||
success: boolean;
|
||||
data?: {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
user_info: {
|
||||
user_id: string;
|
||||
username: string;
|
||||
nick_name: string;
|
||||
email?: string;
|
||||
phone_number?: string;
|
||||
ou_id: string;
|
||||
ou_name: string;
|
||||
is_leader: boolean;
|
||||
user_role: string;
|
||||
sub: string;
|
||||
};
|
||||
};
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用后端登录接口(OAuth 方式)
|
||||
*
|
||||
* @param loginData 登录数据(OAuth 用户信息)
|
||||
* @returns 登录响应(包含 JWT token)
|
||||
*/
|
||||
export async function loginWithOAuth(loginData: LoginRequest): Promise<LoginResponse> {
|
||||
const loginUrl = `${API_BASE_URL}/auth/login`;
|
||||
|
||||
console.log("📝 [Login Client] 调用后端 OAuth 登录接口:", loginUrl);
|
||||
|
||||
try {
|
||||
const response = await fetch(loginUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
},
|
||||
body: JSON.stringify(loginData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
console.error("❌ [Login Client] OAuth 登录请求失败:", response.status, errorData);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorData.error || errorData.message || `登录失败: ${response.status}`
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("✅ [Login Client] OAuth 登录成功");
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("❌ [Login Client] OAuth 登录请求异常:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "网络请求失败"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码登录请求参数
|
||||
*/
|
||||
export interface PasswordLoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用后端登录接口(密码方式)
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
* @returns 登录响应(包含 JWT token)
|
||||
*/
|
||||
export async function loginWithPassword(
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<LoginResponse> {
|
||||
const loginUrl = `${API_BASE_URL}/auth/login`;
|
||||
|
||||
console.log("📝 [Login Client] 调用后端密码登录接口:", loginUrl);
|
||||
|
||||
try {
|
||||
const response = await fetch(loginUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
console.error("❌ [Login Client] 密码登录请求失败:", response.status, errorData);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorData.error || errorData.message || `登录失败: ${response.status}`
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("✅ [Login Client] 密码登录成功");
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("❌ [Login Client] 密码登录请求异常:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "网络请求失败"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -106,11 +106,11 @@ function mergeAuthHeaders(
|
||||
return headers;
|
||||
}
|
||||
|
||||
// 优先使用显式传入的 token,否则从上下文获取
|
||||
const token = explicitToken || 'undefined';
|
||||
|
||||
// 如果有 token(显式传入或从上下文获取),添加到 Authorization 头部
|
||||
if (token) {
|
||||
// 优先使用显式传入的 token,否则尝试从客户端 localStorage 获取
|
||||
const token = explicitToken || (typeof window !== 'undefined' ? localStorage.getItem('access_token') : undefined);
|
||||
|
||||
// 如果有有效的 token(显式传入或从客户端获取),添加到 Authorization 头部
|
||||
if (token && token !== 'undefined') {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user