feat: 1.修改提示词模板的不用角色的操作权限。
2. 对接数据看板的数据。 3. 添加入口模块管理的页面。
This commit is contained in:
+56
-21
@@ -12,7 +12,7 @@ export type ApiResponse<T> = {
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type QueryParams = Record<string, string | number | boolean | undefined>;
|
||||
export type QueryParams = Record<string, string | number | boolean | undefined | number[] | string[]>;
|
||||
|
||||
// 获取 API 基础 URL (从配置文件导入)
|
||||
// const API_BASE_URL = 'http://172.16.0.58:8008';
|
||||
@@ -52,6 +52,12 @@ const AUTH_WHITELIST = [
|
||||
'/oauth/userinfo'
|
||||
];
|
||||
|
||||
// 错误容忍白名单 - 这些接口即使返回 401/403 也不触发强制登出
|
||||
const ERROR_TOLERANT_WHITELIST = [
|
||||
'/admin/statistics/top-error-points',
|
||||
'/admin/statistics/top-risk-users'
|
||||
];
|
||||
|
||||
/**
|
||||
* 检查请求URL是否在白名单中
|
||||
*/
|
||||
@@ -60,6 +66,14 @@ function isInAuthWhitelist(url?: string): boolean {
|
||||
return AUTH_WHITELIST.some(path => url.includes(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查请求URL是否在错误容忍白名单中
|
||||
*/
|
||||
function isInErrorTolerantWhitelist(url?: string): boolean {
|
||||
if (!url) return false;
|
||||
return ERROR_TOLERANT_WHITELIST.some(path => url.includes(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求拦截器 - 自动添加 Authorization 头
|
||||
*/
|
||||
@@ -104,10 +118,18 @@ axiosInstance.interceptors.response.use(
|
||||
},
|
||||
(error) => {
|
||||
if (isAxiosError(error) && error.response?.status === 401) {
|
||||
// 检查是否在错误容忍白名单中
|
||||
const requestUrl = error.config?.url;
|
||||
if (isInErrorTolerantWhitelist(requestUrl)) {
|
||||
console.warn('⚠️ [容错白名单] 接口返回 401,但不触发强制登出:', requestUrl);
|
||||
// 直接返回错误,不触发登出
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Token 过期或无效
|
||||
console.warn('⚠️ Token 已过期或无效,请重新登录');
|
||||
console.warn('⚠️ 401 错误详情:', {
|
||||
url: error.config?.url,
|
||||
url: requestUrl,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
data: error.response?.data,
|
||||
@@ -208,7 +230,12 @@ function buildUrl(endpoint: string, params?: QueryParams): string {
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
url.searchParams.append(key, String(value));
|
||||
// 处理数组参数:使用逗号分隔
|
||||
if (Array.isArray(value)) {
|
||||
url.searchParams.append(key, value.join(','));
|
||||
} else {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -292,16 +319,19 @@ export async function apiRequest<T>(
|
||||
try {
|
||||
// 构建 URL
|
||||
const url = buildUrl(endpoint, params);
|
||||
|
||||
// 设置默认请求头
|
||||
const headers = options.headers || {};
|
||||
if (!headers['Content-Type'] && options.method !== 'GET') {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
|
||||
// 只有在 options.headers 存在时才处理,否则让拦截器处理
|
||||
let headers = options.headers;
|
||||
if (headers) {
|
||||
// 设置默认请求头(仅当 headers 已存在时)
|
||||
if (!headers['Content-Type'] && options.method !== 'GET') {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
if (!headers['Accept']) {
|
||||
headers['Accept'] = 'application/json';
|
||||
}
|
||||
}
|
||||
if (!headers['Accept']) {
|
||||
headers['Accept'] = 'application/json';
|
||||
}
|
||||
|
||||
|
||||
// 针对 PostgREST 的额外处理
|
||||
if (endpoint.includes('evaluation_point_groups') && (options.method === 'POST' || options.method === 'PATCH')) {
|
||||
// console.log('使用 PostgREST 特定配置处理请求');
|
||||
@@ -315,10 +345,10 @@ export async function apiRequest<T>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// console.log(`📦 axios-client.ts->请求URL: ${url}`);
|
||||
// console.log(`axios-client.ts->发送 ${options.method || 'GET'} 请求到: ${url}`);
|
||||
|
||||
|
||||
// 处理body参数,转换为data
|
||||
if (options.body) {
|
||||
// console.log(`axios-client.ts->请求体: \n${options.body}`);
|
||||
@@ -327,20 +357,25 @@ export async function apiRequest<T>(
|
||||
// console.log(`axios-client.ts->请求体: \n${typeof options.data === 'string' ? options.data : JSON.stringify(options.data)}`);
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
// 构建请求配置
|
||||
// 如果没有传入 headers,就不设置 headers,让拦截器自动添加
|
||||
const config: AxiosRequestConfig = {
|
||||
...options,
|
||||
url,
|
||||
headers,
|
||||
// 确保使用默认超时时间
|
||||
timeout: options.timeout || DEFAULT_TIMEOUT
|
||||
};
|
||||
|
||||
// 🔍 调试:打印 Authorization 头
|
||||
if (headers['Authorization']) {
|
||||
// console.log('🔑 [apiRequest] 请求包含 Authorization 头:', headers['Authorization'].substring(0, 20) + '...');
|
||||
} else {
|
||||
console.warn('⚠️ [apiRequest] 请求缺少 Authorization 头!headers:', Object.keys(headers));
|
||||
// 只有在 headers 存在时才设置
|
||||
if (headers) {
|
||||
config.headers = headers;
|
||||
|
||||
// 🔍 调试:打印 Authorization 头
|
||||
if (headers['Authorization']) {
|
||||
// console.log('🔑 [apiRequest] 请求包含 Authorization 头:', headers['Authorization'].substring(0, 20) + '...');
|
||||
} else {
|
||||
console.warn('⚠️ [apiRequest] 请求缺少 Authorization 头!headers:', Object.keys(headers));
|
||||
}
|
||||
}
|
||||
// console.log(`📦 axios-client.ts->请求配置: \n${JSON.stringify(config)}`);
|
||||
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* 入口模块管理 API 客户端
|
||||
* 提供入口模块的增删改查功能
|
||||
*/
|
||||
|
||||
import { postgrestGet, postgrestPost, postgrestPut, postgrestDelete } from "../postgrest-client";
|
||||
|
||||
/**
|
||||
* 入口模块数据接口
|
||||
*/
|
||||
export interface EntryModule {
|
||||
id?: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
path?: string; // logo图片路径
|
||||
areas?: string[]; // 地区数组
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 入口模块搜索参数
|
||||
*/
|
||||
export interface EntryModuleSearchParams {
|
||||
name?: string;
|
||||
area?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 入口模块列表响应
|
||||
*/
|
||||
export interface EntryModulesResponse {
|
||||
modules: EntryModule[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取入口模块列表
|
||||
* @param searchParams 搜索参数
|
||||
* @param jwtToken JWT令牌
|
||||
* @returns 入口模块列表和总数
|
||||
*/
|
||||
export async function getEntryModules(
|
||||
searchParams: EntryModuleSearchParams = {},
|
||||
jwtToken?: string | null
|
||||
): Promise<{ data?: EntryModulesResponse; error?: string }> {
|
||||
try {
|
||||
const { name, area, page = 1, pageSize = 10 } = searchParams;
|
||||
|
||||
// 构建过滤条件
|
||||
const filter: Record<string, string> = {};
|
||||
|
||||
if (name) {
|
||||
filter.name = `ilike.*${name}*`;
|
||||
}
|
||||
|
||||
// 如果有地区筛选,使用 JSONB 查询
|
||||
if (area) {
|
||||
filter.areas = `cs.{"${area}"}`; // cs = contains (JSONB数组包含)
|
||||
}
|
||||
|
||||
// 计算分页
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
// 构建查询参数(一次请求获取数据和总数)
|
||||
const queryParams: any = {
|
||||
select: "*",
|
||||
order: "created_at.desc",
|
||||
limit: pageSize,
|
||||
offset: offset,
|
||||
headers: {
|
||||
'Prefer': 'count=exact'
|
||||
},
|
||||
token: jwtToken
|
||||
};
|
||||
|
||||
// 只在有过滤条件时添加 filter
|
||||
if (Object.keys(filter).length > 0) {
|
||||
queryParams.filter = filter;
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
const result = await postgrestGet<EntryModule[]>("entry_modules", queryParams);
|
||||
|
||||
if (result.error) {
|
||||
return { error: result.error };
|
||||
}
|
||||
|
||||
// 从 Content-Range 头获取总数
|
||||
let totalCount = 0;
|
||||
const responseWithHeaders = result as {
|
||||
data: unknown;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
if (responseWithHeaders.headers) {
|
||||
const rangeHeader = responseWithHeaders.headers['content-range'];
|
||||
if (rangeHeader) {
|
||||
const total = rangeHeader.split('/')[1];
|
||||
if (total !== '*') {
|
||||
totalCount = parseInt(total, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
modules: result.data || [],
|
||||
total: totalCount || (result.data?.length || 0)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("获取入口模块列表失败:", error);
|
||||
return { error: error instanceof Error ? error.message : "获取入口模块列表失败" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取入口模块
|
||||
* @param id 入口模块ID
|
||||
* @param jwtToken JWT令牌
|
||||
* @returns 入口模块数据
|
||||
*/
|
||||
export async function getEntryModuleById(
|
||||
id: number,
|
||||
jwtToken?: string | null
|
||||
): Promise<{ data?: EntryModule; error?: string }> {
|
||||
try {
|
||||
const result = await postgrestGet<EntryModule[]>("entry_modules", {
|
||||
filter: { id: `eq.${id}` },
|
||||
token: jwtToken
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
return { error: result.error };
|
||||
}
|
||||
|
||||
const module = result.data?.[0];
|
||||
if (!module) {
|
||||
return { error: "入口模块不存在" };
|
||||
}
|
||||
|
||||
return { data: module };
|
||||
} catch (error) {
|
||||
console.error("获取入口模块失败:", error);
|
||||
return { error: error instanceof Error ? error.message : "获取入口模块失败" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建入口模块
|
||||
* @param module 入口模块数据
|
||||
* @param jwtToken JWT令牌
|
||||
* @returns 创建的入口模块
|
||||
*/
|
||||
export async function createEntryModule(
|
||||
module: Omit<EntryModule, "id" | "created_at" | "updated_at">,
|
||||
jwtToken?: string | null
|
||||
): Promise<{ data?: EntryModule; error?: string }> {
|
||||
try {
|
||||
const result = await postgrestPost<EntryModule[], EntryModule>(
|
||||
"entry_modules",
|
||||
module as EntryModule,
|
||||
jwtToken
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
return { error: result.error };
|
||||
}
|
||||
|
||||
const createdModule = Array.isArray(result.data) ? result.data[0] : result.data;
|
||||
return { data: createdModule as EntryModule };
|
||||
} catch (error) {
|
||||
console.error("创建入口模块失败:", error);
|
||||
return { error: error instanceof Error ? error.message : "创建入口模块失败" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新入口模块
|
||||
* @param id 入口模块ID
|
||||
* @param module 更新的入口模块数据
|
||||
* @param jwtToken JWT令牌
|
||||
* @returns 更新的入口模块
|
||||
*/
|
||||
export async function updateEntryModule(
|
||||
id: number,
|
||||
module: Partial<Omit<EntryModule, "id" | "created_at" | "updated_at">>,
|
||||
jwtToken?: string | null
|
||||
): Promise<{ data?: EntryModule; error?: string }> {
|
||||
try {
|
||||
const result = await postgrestPut<EntryModule[], Partial<EntryModule>>(
|
||||
"entry_modules",
|
||||
module,
|
||||
{ id: `eq.${id}` },
|
||||
jwtToken
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
return { error: result.error };
|
||||
}
|
||||
|
||||
const updatedModule = Array.isArray(result.data) ? result.data[0] : result.data;
|
||||
return { data: updatedModule as EntryModule };
|
||||
} catch (error) {
|
||||
console.error("更新入口模块失败:", error);
|
||||
return { error: error instanceof Error ? error.message : "更新入口模块失败" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除入口模块
|
||||
* @param id 入口模块ID
|
||||
* @param jwtToken JWT令牌
|
||||
* @returns 是否成功
|
||||
*/
|
||||
export async function deleteEntryModule(
|
||||
id: number,
|
||||
jwtToken?: string | null
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const result = await postgrestDelete(
|
||||
"entry_modules",
|
||||
{ id: `eq.${id}` },
|
||||
jwtToken
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
return { success: false, error: result.error };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("删除入口模块失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "删除入口模块失败"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -507,12 +507,12 @@ export async function getDocumentsListFromAPI(searchParams: {
|
||||
if (dateFrom) params.start_time = dateFrom;
|
||||
if (dateTo) params.end_time = dateTo;
|
||||
|
||||
// 处理文档类型ID数组 - 传递为数组或单个值
|
||||
// 处理文档类型ID数组 - 转换为逗号分隔的字符串
|
||||
if (documentTypeIds && documentTypeIds.length > 0) {
|
||||
params.type_id = documentTypeIds;
|
||||
params.type_id = documentTypeIds.join(',');
|
||||
}
|
||||
|
||||
console.log('📤 [getDocumentsListFromAPI] 请求参数:', params);
|
||||
// console.log('📤 [getDocumentsListFromAPI] 请求参数:', params);
|
||||
|
||||
// 调用后端API
|
||||
const axios = await import('axios').then(m => m.default);
|
||||
@@ -529,7 +529,7 @@ export async function getDocumentsListFromAPI(searchParams: {
|
||||
const totalCount = data.total || 0;
|
||||
const totalPages = data.total_pages || 0;
|
||||
|
||||
console.log(`📥 [getDocumentsListFromAPI] 获取到 ${backendDocuments.length} 个文档,总数: ${totalCount}`);
|
||||
// console.log(`📥 [getDocumentsListFromAPI] 获取到 ${backendDocuments.length} 个文档,总数: ${totalCount}`);
|
||||
|
||||
// 转换后端数据为前端 DocumentUI 格式
|
||||
const convertedDocuments: DocumentUI[] = backendDocuments.map((doc: any) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { postgrestGet, postgrestPost, type PostgrestParams } from "../postgrest-client";
|
||||
import { apiRequest } from "../axios-client";
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/**
|
||||
@@ -607,3 +608,185 @@ export async function getEntryModules(userRole: string | null | undefined, userA
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 高频错误评查点数据类型
|
||||
*/
|
||||
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: [] };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user