Files
leaudit-platform-frontend/app/api/home/home.ts
T

529 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { postgrestGet, type PostgrestParams } from "../postgrest-client";
import { apiRequest } from "../axios-client";
// import dayjs from 'dayjs';
/**
* 从不同格式的 API 响应中提取数据
* @param responseData API 响应数据
* @returns 提取后的数据或 null
*/
function extractApiData<T>(responseData: unknown): T | null {
if (!responseData) {
console.warn('API响应数据为空');
return null;
}
try {
// 检查是否有错误信息
if (typeof responseData === 'object' && responseData !== null) {
// 错误检查: 检查错误码,一般成功的错误码是0或200
if ('code' in responseData) {
const code = (responseData as { code: number }).code;
// 如果有错误码且不是成功状态
if (code !== 0 && code !== 200) {
const errorMsg = 'msg' in responseData
? (responseData as { msg: string }).msg
: '未知错误';
console.error(`API响应错误: [${code}] ${errorMsg}`);
return null;
}
}
// 错误检查: 检查是否包含错误消息但没有数据
if ('error' in responseData && (responseData as { error: unknown }).error) {
const error = (responseData as { error: unknown }).error;
console.error(`API响应包含错误: ${typeof error === 'string' ? error : JSON.stringify(error)}`);
return null;
}
// 格式1: { code: number, msg: string, data: T }
if ('data' in responseData) {
const data = (responseData as { data: unknown }).data;
if (!data) {
console.warn('API响应中的data字段为空');
return null;
}
return data as T;
}
}
// 格式2: 直接是数据对象
return responseData as T;
} catch (error) {
console.error('处理API响应数据时出错:', error);
return null;
}
}
/**
* 首页数据统计响应类型
*/
interface HomeStatistics {
todayPendingFiles: number;
monthlyReviewedFiles: number;
monthlyReviewGrowth: {
value: number;
isUp: boolean;
};
monthlyPassRate: number;
passRateGrowth: {
value: number;
isUp: boolean;
};
issuesDetected: number;
issuesGrowth: {
value: number;
isUp: boolean;
};
}
/**
* 后端统计接口响应类型(蛇形命名)
*/
interface BackendStatisticsResponse {
today_pending_files: number;
monthly_reviewed_files: number;
monthly_review_growth: {
value: number;
is_up: boolean;
};
monthly_pass_rate: number;
monthly_pass_rate_growth: {
value: number;
is_up: boolean;
};
monthly_detected_issues: number;
monthly_issues_growth: {
value: number;
is_up: boolean;
};
}
/**
* 获取主页数据
* @param reviewType 从客户端传入的 reviewType 值(已废弃,现在从sessionStorage读取)
* @param userId 用户ID(已废弃,后端通过JWT自动识别)
* @param token JWT token
* @returns 主页数据
*/
export async function getHomeData(reviewType?: string | null, userId?: string | number, token?: string): Promise<HomeStatistics> {
try {
// 🔑 从 sessionStorage 获取文档类型IDs
let typeIds: string | null = null;
if (typeof window !== 'undefined') {
const storedTypeIds = sessionStorage.getItem('documentTypeIds');
if (storedTypeIds) {
try {
const typeIdsArray = JSON.parse(storedTypeIds) as number[];
if (Array.isArray(typeIdsArray) && typeIdsArray.length > 0) {
typeIds = typeIdsArray.join(',');
console.log('📊 [getHomeData] 从 sessionStorage 获取文档类型:', typeIds);
}
} catch (error) {
console.error('❌ [getHomeData] 解析 documentTypeIds 失败:', error);
}
}
}
// 🔑 构建请求参数
const params: Record<string, string> = {
time_range: '30days' // 默认近30天
};
// 如果有文档类型,添加到参数
if (typeIds) {
params.type_ids = typeIds;
}
console.log('📊 [getHomeData] 请求参数:', params);
// 🔑 调用后端统计接口
const response = await apiRequest<BackendStatisticsResponse>(
'/admin/statistics/home-data',
{
method: 'GET',
headers: token ? {
'Authorization': `Bearer ${token}`
} : undefined
},
params // 查询参数
);
if (response.error) {
console.error('❌ [getHomeData] 获取统计数据失败:', response.error);
throw new Error(response.error);
}
const backendData = response.data;
if (!backendData) {
console.error('❌ [getHomeData] 后端未返回数据');
throw new Error('后端未返回统计数据');
}
console.log('✅ [getHomeData] 获取统计数据成功:', backendData);
// 🔑 将后端响应(蛇形命名)转换为前端格式(驼峰命名)
return {
todayPendingFiles: backendData.today_pending_files,
monthlyReviewedFiles: backendData.monthly_reviewed_files,
monthlyReviewGrowth: {
value: backendData.monthly_review_growth.value,
isUp: backendData.monthly_review_growth.is_up
},
monthlyPassRate: backendData.monthly_pass_rate,
passRateGrowth: {
value: backendData.monthly_pass_rate_growth.value,
isUp: backendData.monthly_pass_rate_growth.is_up
},
issuesDetected: backendData.monthly_detected_issues,
issuesGrowth: {
value: backendData.monthly_issues_growth.value,
isUp: backendData.monthly_issues_growth.is_up
}
};
} catch (error) {
console.error('获取首页数据失败:', error instanceof Error ? error.message : String(error));
// 返回默认值以防止页面崩溃
return {
todayPendingFiles: 0,
monthlyReviewedFiles: 0,
monthlyReviewGrowth: { value: 0, isUp: true },
monthlyPassRate: 0,
passRateGrowth: { value: 0, isUp: true },
issuesDetected: 0,
issuesGrowth: { value: 0, isUp: true }
};
}
}
/**
* 地区配置类型定义
*/
export interface AreaConfig {
area: string; // 地区名称
enabled: boolean; // 是否启用
sort_order: number; // 排序顺序
}
/**
* 入口模块类型定义
*/
export interface EntryModule {
id: number;
name: string;
description: string | null;
path: string | null;
areas: AreaConfig[]; // 修改为对象数组
created_at: string;
updated_at: string;
document_types?: Array<{
id: number;
name: string;
code: string | null;
}>;
}
/**
* 获取用户可访问的入口模块
* @param userArea 用户所属地区
* @param token JWT token
* @returns 入口模块列表
*/
export async function getEntryModules(userRole: string | null | undefined, userArea: string | null | undefined, token?: string): Promise<EntryModule[]> {
try {
if (!userRole || !userArea) {
console.warn('⚠️ [getEntryModules] 用户角色或地区为空,返回空模块列表');
return [];
}
// console.log('🔍 [getEntryModules] 查询地区:', userArea);
// 查询 entry_modules 表,获取所有模块(在客户端进行过滤)
const params: PostgrestParams = {
select: 'id,name,description,path,areas,created_at,updated_at',
filter: {}
};
const modulesResponse = await postgrestGet('/api/postgrest/proxy/entry_modules', { ...params, token });
if (modulesResponse.error) {
console.error('❌ [getEntryModules] 查询入口模块失败:', modulesResponse.error);
return [];
}
const allModules = extractApiData<EntryModule[]>(modulesResponse.data);
if (!allModules || allModules.length === 0) {
console.warn('⚠️ [getEntryModules] 未找到任何入口模块');
return [];
}
// 🔑 在客户端过滤:只保留包含用户地区且已启用的模块
const modules = allModules.filter(module => {
// 省级管理员可以看到所有模块
if (userRole === 'provincial_admin') {
return true;
}
// 检查 areas 数组中是否存在匹配的地区配置
if (!module.areas || !Array.isArray(module.areas)) {
return false;
}
// 查找用户地区的配置
const areaConfig = module.areas.find(config =>
config.area === userArea && config.enabled === true
);
return !!areaConfig; // 找到且启用才返回 true
});
if (modules.length === 0) {
console.warn('⚠️ [getEntryModules] 未找到已启用的入口模块');
return [];
}
// console.log(`✅ [getEntryModules] 找到 ${modules.length} 个已启用的入口模块`);
// 为每个模块查询关联的 document_types
const modulesWithTypes = await Promise.all(
modules.map(async (module) => {
try {
const typesParams: PostgrestParams = {
select: 'id,name,code',
filter: {
entry_module_id: `eq.${module.id}`
}
};
const typesResponse = await postgrestGet('/api/postgrest/proxy/document_types', { ...typesParams, token });
if (typesResponse.error) {
console.error(`❌ [getEntryModules] 查询模块 ${module.id} 的文档类型失败:`, typesResponse.error);
return { ...module, document_types: [] };
}
const documentTypes = extractApiData<Array<{ id: number; name: string; code: string | null }>>(typesResponse.data);
return {
...module,
document_types: documentTypes || []
};
} catch (error) {
console.error(`❌ [getEntryModules] 处理模块 ${module.id} 时出错:`, error);
return { ...module, document_types: [] };
}
})
);
// console.log('✅ [getEntryModules] 入口模块数据(含文档类型):', JSON.stringify(modulesWithTypes));
// 默认会多加一个 智慧法务助手 入口 默认所有人都可以用,看到
modulesWithTypes.push({
"id": 0,
"name": "智慧法务助手",
"description": "智慧法务助手",
"path": "entryModule/assistant",
"areas": [],
"created_at": "2025-11-18T21:33:33.857417+08:00",
"updated_at": "2025-11-18T22:28:51.819722+08:00",
"document_types": [
{
"id": 0,
"name": "空",
"code": "空"
}
]
}
)
return modulesWithTypes;
} catch (error) {
console.error('❌ [getEntryModules] 获取入口模块失败:', error instanceof Error ? error.message : String(error));
return [];
}
}
/**
* 高频错误评查点数据类型
*/
export interface TopErrorPoint {
rank: number;
evaluation_point_id: number;
point_name: string;
error_user_count: number;
}
export interface TopErrorPointsResponse {
available: boolean; // 标记该模块是否可用(是否有权限访问)
total: number;
items: TopErrorPoint[];
}
/**
* 获取高频错误评查点 Top N
* @param limit 返回 Top N 条记录,默认 10
* @param startDate 开始时间(格式:YYYY-MM-DD
* @param endDate 结束时间(格式:YYYY-MM-DD
* @param typeId 文档类型ID数组
* @param token JWT token
* @returns 高频错误评查点列表
*/
export async function getTopErrorPoints(
limit: number = 10,
startDate?: string,
endDate?: string,
typeId?: number[],
token?: string
): Promise<TopErrorPointsResponse> {
try {
console.log('🔍 [getTopErrorPoints] 请求参数:', { limit, startDate, endDate, typeId, hasToken: !!token });
// 构建查询参数
const params: Record<string, string | number | number[]> = {
limit: limit
};
if (startDate) {
params.start_date = startDate;
}
if (endDate) {
params.end_date = endDate;
}
if (typeId && typeId.length > 0) {
// 直接传递数组,axios 会自动处理序列化
params.type_id = typeId;
}
// 构建请求配置
const requestOptions: { method: string; headers?: Record<string, string> } = {
method: 'GET'
};
// 只有在显式传入 token 时才添加 Authorization header
// 否则让 axios 拦截器自动处理(从 localStorage 获取)
if (token) {
requestOptions.headers = {
'Authorization': `Bearer ${token}`
};
}
// 调用 API
const response = await apiRequest<TopErrorPointsResponse>(
'/admin/statistics/top-error-points',
requestOptions,
params
);
if (response.error) {
console.error('❌ [getTopErrorPoints] 获取高频错误评查点失败:', response.error);
// 请求失败(如权限不足),标记为不可用
return { available: false, total: 0, items: [] };
}
console.log('✅ [getTopErrorPoints] 成功获取高频错误评查点数据:', response.data);
// 请求成功,标记为可用(即使数据为空)
const data = response.data || { total: 0, items: [] };
return { available: true, ...data };
} catch (error) {
console.error('❌ [getTopErrorPoints] 获取高频错误评查点异常:', error instanceof Error ? error.message : String(error));
// 请求异常,标记为不可用
return { available: false, total: 0, items: [] };
}
}
/**
* 高风险用户数据类型
*/
export interface TopRiskUser {
rank: number;
user_id: number;
user_name: string;
department: string;
total_errors: number;
avg_errors_per_doc: number;
}
export interface TopRiskUsersResponse {
available: boolean; // 标记该模块是否可用(是否有权限访问)
total: number;
items: TopRiskUser[];
}
/**
* 获取高风险用户 Top N
* @param limit 返回 Top N 条记录,默认 5
* @param startDate 开始时间(格式:YYYY-MM-DD
* @param endDate 结束时间(格式:YYYY-MM-DD
* @param typeId 文档类型ID数组
* @param token JWT token
* @returns 高风险用户列表
*/
export async function getTopRiskUsers(
limit: number = 5,
startDate?: string,
endDate?: string,
typeId?: number[],
token?: string
): Promise<TopRiskUsersResponse> {
try {
console.log('🔍 [getTopRiskUsers] 请求参数:', { limit, startDate, endDate, typeId, hasToken: !!token });
// 构建查询参数
const params: Record<string, string | number | number[]> = {
limit: limit
};
if (startDate) {
params.start_date = startDate;
}
if (endDate) {
params.end_date = endDate;
}
if (typeId && typeId.length > 0) {
// 直接传递数组,axios 会自动处理序列化
params.type_id = typeId;
}
// 构建请求配置
const requestOptions: { method: string; headers?: Record<string, string> } = {
method: 'GET'
};
// 只有在显式传入 token 时才添加 Authorization header
// 否则让 axios 拦截器自动处理(从 localStorage 获取)
if (token) {
requestOptions.headers = {
'Authorization': `Bearer ${token}`
};
}
// 调用 API
const response = await apiRequest<TopRiskUsersResponse>(
'/admin/statistics/top-risk-users',
requestOptions,
params
);
if (response.error) {
console.error('❌ [getTopRiskUsers] 获取高风险用户失败:', response.error);
// 请求失败(如权限不足),标记为不可用
return { available: false, total: 0, items: [] };
}
console.log('✅ [getTopRiskUsers] 成功获取高风险用户数据:', response.data);
// 请求成功,标记为可用(即使数据为空)
const data = response.data || { total: 0, items: [] };
return { available: true, ...data };
} catch (error) {
console.error('❌ [getTopRiskUsers] 获取高风险用户异常:', error instanceof Error ? error.message : String(error));
// 请求异常,标记为不可用
return { available: false, total: 0, items: [] };
}
}