Merge remote-tracking branch 'origin/shiy-login' into PingChuan
This commit is contained in:
@@ -607,6 +607,13 @@ function convertIcon(elementIcon: string | null): string {
|
||||
if (!elementIcon) {
|
||||
return 'ri-file-line'; // 默认图标
|
||||
}
|
||||
|
||||
// 如果已经是 RemixIcon 格式(以 ri- 开头),直接返回
|
||||
if (elementIcon.startsWith('ri-')) {
|
||||
return elementIcon;
|
||||
}
|
||||
|
||||
// 否则尝试从 Element UI 映射表中查找
|
||||
return ICON_MAPPING[elementIcon] || 'ri-file-line';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { postgrestGet, postgrestPut } from "../postgrest-client";
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* 从不同格式的 API 响应中提取数据
|
||||
@@ -134,26 +135,18 @@ export async function submitCrossCheckingOpinion(
|
||||
evaluation_result_id: opinionData.reviewPointResultId
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals`, {
|
||||
method: 'POST',
|
||||
const response = await axios.post(`${API_BASE_URL}/admin/cross_review/proposals`, requestData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || '提交失败');
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
success: true,
|
||||
message: '意见提交成功',
|
||||
data: data
|
||||
data: response.data
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -190,23 +183,19 @@ export async function getCrossCheckingOpinions(
|
||||
// 如果没传userId,默认用1
|
||||
const realUserId = userId ?? 1;
|
||||
// 实际后端API调用,拼接API_BASE_URL
|
||||
const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals/document`, {
|
||||
method: 'POST',
|
||||
const response = await axios.post(`${API_BASE_URL}/admin/cross_review/proposals/document`, {
|
||||
user_id: realUserId,
|
||||
document_id: documentId, // 如果后端需要document_id可以加上
|
||||
page,
|
||||
page_size: pageSize
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: realUserId,
|
||||
document_id: documentId, // 如果后端需要document_id可以加上
|
||||
page,
|
||||
page_size: pageSize
|
||||
})
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('获取意见列表失败');
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
const data = response.data;
|
||||
console.log('最原始的返回data', data);
|
||||
// 处理新的数据结构,支持分页
|
||||
const responseData = data.data || data;
|
||||
@@ -328,23 +317,24 @@ export async function performOpinionAction(
|
||||
throw new Error('无效的操作类型');
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: actionData.action === 'withdraw_opinion' ? 'DELETE' : 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
const response = actionData.action === 'withdraw_opinion'
|
||||
? await axios.delete(endpoint, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
: await axios.post(endpoint, requestBody, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = response.data;
|
||||
|
||||
console.log('返回的意见列表数据',data);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.error || '操作失败');
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
success: true,
|
||||
@@ -417,20 +407,15 @@ export async function checkProposalVotes(
|
||||
document_id: documentId
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals/document/check_pending_votes`, {
|
||||
method: 'POST',
|
||||
const response = await axios.post(`${API_BASE_URL}/admin/cross_review/proposals/document/check_pending_votes`, requestData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = response.data;
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || '检查失败');
|
||||
}
|
||||
console.log("检查投票数据",data);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { UPLOAD_URL } from '../../config/api-config';
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* 从不同格式的 API 响应中提取数据
|
||||
@@ -146,8 +147,8 @@ export async function uploadCrossCheckingDocument(
|
||||
|
||||
// 发送请求
|
||||
try {
|
||||
console.log('【交叉评查上传】开始fetch请求...');
|
||||
const headers: HeadersInit = {
|
||||
console.log('【交叉评查上传】开始axios请求...');
|
||||
const headers: Record<string, string> = {
|
||||
'X-File-Name': encodeURIComponent(fileName),
|
||||
};
|
||||
|
||||
@@ -155,50 +156,35 @@ export async function uploadCrossCheckingDocument(
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData
|
||||
const response = await axios.post(uploadUrl, formData, {
|
||||
headers
|
||||
});
|
||||
|
||||
|
||||
console.log('【交叉评查上传】收到服务器响应:', { status: response.status, statusText: response.statusText });
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`【交叉评查上传】上传失败 (${response.status}): ${errorText}`);
|
||||
return {
|
||||
error: `上传失败: ${response.status} ${response.statusText} - ${errorText}`,
|
||||
status: response.status
|
||||
};
|
||||
}
|
||||
|
||||
console.log('【交叉评查上传】开始解析JSON响应');
|
||||
let responseData;
|
||||
try {
|
||||
responseData = await response.json();
|
||||
console.log('【交叉评查上传】JSON响应解析成功:', responseData);
|
||||
} catch (jsonError) {
|
||||
console.error('【交叉评查上传】JSON解析失败:', jsonError);
|
||||
return {
|
||||
error: `解析响应JSON失败: ${jsonError instanceof Error ? jsonError.message : '未知错误'}`,
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
|
||||
const extractedData = extractApiData<CrossCheckingFileUploadResponse>(responseData);
|
||||
|
||||
console.log('【交叉评查上传】JSON响应解析成功:', response.data);
|
||||
|
||||
const extractedData = extractApiData<CrossCheckingFileUploadResponse>(response.data);
|
||||
console.log('【交叉评查上传】提取的数据:', extractedData);
|
||||
|
||||
|
||||
if (!extractedData) {
|
||||
console.error('【交叉评查上传】无法提取数据');
|
||||
return { error: '处理上传响应失败', status: 500 };
|
||||
}
|
||||
|
||||
|
||||
console.log('【交叉评查上传】上传成功,返回数据');
|
||||
return { data: extractedData as CrossCheckingFileUploadResponse };
|
||||
} catch (fetchError) {
|
||||
console.error('【交叉评查上传】fetch请求失败:', fetchError);
|
||||
return {
|
||||
error: `fetch请求错误: ${fetchError instanceof Error ? fetchError.message : '未知错误'}`,
|
||||
} catch (axiosError) {
|
||||
console.error('【交叉评查上传】axios请求失败:', axiosError);
|
||||
if (axios.isAxiosError(axiosError)) {
|
||||
const errorText = axiosError.response?.data || axiosError.message;
|
||||
return {
|
||||
error: `上传失败: ${axiosError.response?.status || 500} ${axiosError.response?.statusText || ''} - ${errorText}`,
|
||||
status: axiosError.response?.status || 500
|
||||
};
|
||||
}
|
||||
return {
|
||||
error: `axios请求错误: ${axiosError instanceof Error ? axiosError.message : '未知错误'}`,
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
@@ -258,14 +244,12 @@ export async function batchUploadAndAssignCrossCheckingFiles(
|
||||
};
|
||||
formData.append('upload_info', JSON.stringify(uploadInfo));
|
||||
formData.append('assign_user_ids', JSON.stringify(assignUserIds));
|
||||
const headers: HeadersInit = {};
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData
|
||||
const response = await axios.post(uploadUrl, formData, {
|
||||
headers
|
||||
});
|
||||
const result = await response.json();
|
||||
const result = response.data;
|
||||
if (result && result.success) {
|
||||
successes.push({ file: fileInfo, result });
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { API_BASE_URL } from '../../config/api-config';
|
||||
import { postgrestPut } from '../postgrest-client';
|
||||
import axios from 'axios';
|
||||
|
||||
// 交叉评查任务状态枚举
|
||||
export enum CrossCheckingTaskStatus {
|
||||
@@ -393,33 +394,28 @@ export async function getUserTaskDocuments(page: number = 1, pageSize: number =
|
||||
// 拼接绝对路径,去除多余斜杠
|
||||
const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;
|
||||
const url = `${base}/admin/cross_review/tasks/user_tasks`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${jwtToken || ''}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
page: page,
|
||||
page_size: pageSize
|
||||
})
|
||||
|
||||
const response = await axios.post(url, {
|
||||
page: page,
|
||||
page_size: pageSize
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${jwtToken || ''}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: `HTTP ${response.status}: ${response.statusText}`
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result
|
||||
data: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `HTTP ${error.response?.status}: ${error.response?.statusText || error.message}`
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '获取用户任务列表失败'
|
||||
@@ -441,33 +437,28 @@ export async function getTaskDocuments(taskId: number, page: number = 1, pageSiz
|
||||
const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;
|
||||
const url = `${base}/admin/cross_review/tasks/${taskId}/documents`;
|
||||
// console.log('最终请求URL:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${jwtToken || ''}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
page: page,
|
||||
page_size: pageSize
|
||||
})
|
||||
|
||||
const response = await axios.post(url, {
|
||||
page: page,
|
||||
page_size: pageSize
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${jwtToken || ''}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: `HTTP ${response.status}: ${response.statusText}`
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result
|
||||
data: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `HTTP ${error.response?.status}: ${error.response?.statusText || error.message}`
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '获取任务文档列表失败'
|
||||
|
||||
@@ -25,6 +25,10 @@ export interface DocumentTypeUI {
|
||||
name: string;
|
||||
description: string;
|
||||
groups: DocumentTypeGroup[];
|
||||
entry_module?: {
|
||||
id: number;
|
||||
name: string;
|
||||
} | null;
|
||||
llm_extraction_template_id?: number | null;
|
||||
vlm_extraction_template_id?: number | null;
|
||||
evaluation_template_id?: number | null;
|
||||
@@ -39,6 +43,7 @@ export interface DocumentTypeCreateDTO {
|
||||
name: string;
|
||||
description?: string;
|
||||
group_ids: string[];
|
||||
entry_module_id?: number | null;
|
||||
llm_extraction_template_id?: number | null;
|
||||
vlm_extraction_template_id?: number | null;
|
||||
evaluation_template_id?: number | null;
|
||||
@@ -108,27 +113,27 @@ export async function getAllEvaluationPointGroups(token?: string): Promise<{
|
||||
id: number;
|
||||
name: string;
|
||||
}>>('evaluation_point_groups', params);
|
||||
|
||||
|
||||
if (response.error) {
|
||||
return { error: response.error, status: response.status };
|
||||
}
|
||||
|
||||
|
||||
// 使用extractApiData提取数据
|
||||
const extractedData = extractApiData<Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
}>>(response.data);
|
||||
|
||||
|
||||
if (!extractedData) {
|
||||
return { data: [] };
|
||||
}
|
||||
|
||||
|
||||
// 转换为DocumentTypeGroup格式
|
||||
const groups: DocumentTypeGroup[] = extractedData.map(item => ({
|
||||
id: item.id.toString(),
|
||||
name: item.name
|
||||
}));
|
||||
|
||||
|
||||
return { data: groups };
|
||||
} catch (error) {
|
||||
console.error('获取所有评查点分组失败:', error);
|
||||
@@ -136,6 +141,101 @@ export async function getAllEvaluationPointGroups(token?: string): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取父级评查分组(pid=0的分组)
|
||||
* @param token JWT token (可选)
|
||||
* @returns 父级评查点分组列表
|
||||
*/
|
||||
export async function getParentEvaluationPointGroups(token?: string): Promise<{
|
||||
data?: DocumentTypeGroup[];
|
||||
error?: string;
|
||||
status?: number;
|
||||
}> {
|
||||
try {
|
||||
const params: PostgrestParams = {
|
||||
select: 'id, name',
|
||||
filter: {
|
||||
'pid': 'eq.0'
|
||||
},
|
||||
order: 'id.asc',
|
||||
token
|
||||
};
|
||||
|
||||
const response = await postgrestGet<Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
}>>('evaluation_point_groups', params);
|
||||
|
||||
if (response.error) {
|
||||
return { error: response.error, status: response.status };
|
||||
}
|
||||
|
||||
// 使用extractApiData提取数据
|
||||
const extractedData = extractApiData<Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
}>>(response.data);
|
||||
|
||||
if (!extractedData) {
|
||||
return { data: [] };
|
||||
}
|
||||
|
||||
// 转换为DocumentTypeGroup格式
|
||||
const groups: DocumentTypeGroup[] = extractedData.map(item => ({
|
||||
id: item.id.toString(),
|
||||
name: item.name
|
||||
}));
|
||||
|
||||
return { data: groups };
|
||||
} catch (error) {
|
||||
console.error('获取父级评查点分组失败:', error);
|
||||
return { error: error instanceof Error ? error.message : '获取父级评查点分组失败' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有入口模块
|
||||
* @param token JWT token (可选)
|
||||
* @returns 入口模块列表
|
||||
*/
|
||||
export async function getEntryModules(token?: string): Promise<{
|
||||
data?: Array<{ id: number; name: string }>;
|
||||
error?: string;
|
||||
status?: number;
|
||||
}> {
|
||||
try {
|
||||
const params: PostgrestParams = {
|
||||
select: 'id, name',
|
||||
order: 'id.asc',
|
||||
token
|
||||
};
|
||||
|
||||
const response = await postgrestGet<Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
}>>('entry_modules', params);
|
||||
|
||||
if (response.error) {
|
||||
return { error: response.error, status: response.status };
|
||||
}
|
||||
|
||||
// 使用extractApiData提取数据
|
||||
const extractedData = extractApiData<Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
}>>(response.data);
|
||||
|
||||
if (!extractedData) {
|
||||
return { data: [] };
|
||||
}
|
||||
|
||||
return { data: extractedData };
|
||||
} catch (error) {
|
||||
console.error('获取入口模块失败:', error);
|
||||
return { error: error instanceof Error ? error.message : '获取入口模块失败' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取评查点分组信息
|
||||
* @param ids 评查点分组ID数组
|
||||
@@ -216,19 +316,22 @@ export async function getDocumentTypes(searchParams: DocumentTypeSearchParams =
|
||||
try {
|
||||
const page = searchParams.page || 1;
|
||||
const pageSize = searchParams.pageSize || 10;
|
||||
|
||||
// 构建查询参数
|
||||
|
||||
// 构建查询参数,使用 PostgREST 的资源嵌入语法来关联查询
|
||||
// 使用外键约束名称进行关联:entry_modules!fk_document_types_entry_module
|
||||
const params: PostgrestParams = {
|
||||
select: `
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
evaluation_point_groups_ids,
|
||||
entry_module_id,
|
||||
entry_modules!fk_document_types_entry_module(id, name),
|
||||
prompt_config,
|
||||
created_at,
|
||||
updated_at,
|
||||
code
|
||||
`,
|
||||
`.replace(/\s+/g,' ').trim(),
|
||||
order: 'updated_at.desc',
|
||||
headers: {
|
||||
'Prefer': 'count=exact'
|
||||
@@ -238,13 +341,13 @@ export async function getDocumentTypes(searchParams: DocumentTypeSearchParams =
|
||||
filter: {} as Record<string, string>,
|
||||
token: frontendJWT
|
||||
};
|
||||
|
||||
|
||||
// 添加筛选条件
|
||||
const filter: Record<string, string> = {};
|
||||
if (searchParams.name) {
|
||||
filter['name'] = `ilike.%${searchParams.name}%`;
|
||||
}
|
||||
|
||||
|
||||
// 如果有分组ID筛选条件
|
||||
if (searchParams.ruleType) {
|
||||
filter['evaluation_point_groups_ids'] = `cs.[${searchParams.ruleType}]`;
|
||||
@@ -264,32 +367,70 @@ export async function getDocumentTypes(searchParams: DocumentTypeSearchParams =
|
||||
}
|
||||
|
||||
params.filter = filter;
|
||||
|
||||
|
||||
// console.log('获取文档类型列表,参数:', params);
|
||||
const response = await postgrestGet<DocumentType[]>('document_types', params);
|
||||
|
||||
const response = await postgrestGet<(DocumentType & {
|
||||
entry_modules: { id: number; name: string } | null;
|
||||
})[]>('document_types', params);
|
||||
|
||||
if (response.error) {
|
||||
return { error: response.error, status: response.status };
|
||||
}
|
||||
|
||||
// 使用extractApiData提取数据
|
||||
const extractedData = extractApiData<DocumentType[]>(response.data);
|
||||
const documentTypes = extractedData || [];
|
||||
|
||||
// console.log('提取的文档类型数据:', documentTypes);
|
||||
|
||||
// 🔧 优化:移除评查点分组查询(文档列表UI不需要此数据)
|
||||
// 直接转换为UI类型,不查询关联的分组信息
|
||||
const uiTypes = documentTypes.map(type => ({
|
||||
...convertToUIDocumentType(type),
|
||||
groups: [] // 保持接口兼容性,但不填充数据
|
||||
}));
|
||||
|
||||
// console.log('提取的文档类型数据:', JSON.stringify(response));
|
||||
|
||||
// 使用extractApiData提取数据
|
||||
const extractedData = extractApiData<(DocumentType & {
|
||||
entry_modules: { id: number; name: string } | null;
|
||||
})[]>(response.data);
|
||||
const documentTypes = extractedData || [];
|
||||
|
||||
|
||||
// 并发查询所有需要的评查点分组信息
|
||||
const allGroupIds = new Set<number>();
|
||||
documentTypes.forEach(type => {
|
||||
if (type.evaluation_point_groups_ids) {
|
||||
const ids = Array.isArray(type.evaluation_point_groups_ids)
|
||||
? type.evaluation_point_groups_ids
|
||||
: [type.evaluation_point_groups_ids as unknown as number];
|
||||
ids.forEach(id => allGroupIds.add(id));
|
||||
}
|
||||
});
|
||||
|
||||
// 如果有分组ID,查询所有分组信息
|
||||
let groupsMap: Map<number, DocumentTypeGroup> = new Map();
|
||||
if (allGroupIds.size > 0) {
|
||||
const groupsResponse = await getEvaluationPointGroupsByIds(Array.from(allGroupIds), frontendJWT);
|
||||
if (groupsResponse.data) {
|
||||
groupsResponse.data.forEach(group => {
|
||||
groupsMap.set(parseInt(group.id, 10), group);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为UI类型,包含entry_module和groups信息
|
||||
const uiTypes = documentTypes.map(type => {
|
||||
// 获取该文档类型关联的分组
|
||||
let typeGroups: DocumentTypeGroup[] = [];
|
||||
if (type.evaluation_point_groups_ids) {
|
||||
const ids = Array.isArray(type.evaluation_point_groups_ids)
|
||||
? type.evaluation_point_groups_ids
|
||||
: [type.evaluation_point_groups_ids as unknown as number];
|
||||
typeGroups = ids.map(id => groupsMap.get(id)).filter(Boolean) as DocumentTypeGroup[];
|
||||
}
|
||||
|
||||
return {
|
||||
...convertToUIDocumentType({ ...type, groups: typeGroups }),
|
||||
entry_module: type.entry_modules || null,
|
||||
groups: typeGroups
|
||||
};
|
||||
});
|
||||
|
||||
// 获取总数
|
||||
let totalCount = 0;
|
||||
const responseWithHeaders = response as {
|
||||
data: unknown;
|
||||
headers: Record<string, string>
|
||||
const responseWithHeaders = response as {
|
||||
data: unknown;
|
||||
headers: Record<string, string>
|
||||
};
|
||||
if (responseWithHeaders.headers) {
|
||||
const rangeHeader = responseWithHeaders.headers['content-range'];
|
||||
@@ -300,7 +441,7 @@ export async function getDocumentTypes(searchParams: DocumentTypeSearchParams =
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
data: {
|
||||
types: uiTypes,
|
||||
@@ -360,7 +501,10 @@ export async function deleteDocumentType(id: string, frontendJWT?: string): Prom
|
||||
/**
|
||||
* 将API返回的文档类型转换为UI文档类型
|
||||
*/
|
||||
function convertToUIDocumentType(type: DocumentType & { groups: DocumentTypeGroup[] }): DocumentTypeUI {
|
||||
function convertToUIDocumentType(type: DocumentType & {
|
||||
groups: DocumentTypeGroup[];
|
||||
entry_modules?: { id: number; name: string } | null;
|
||||
}): DocumentTypeUI {
|
||||
// 提取提示词模板ID,确保安全处理以避免控制台警告
|
||||
let llmExtractionTemplateId: number | null = null;
|
||||
let vlmExtractionTemplateId: number | null = null;
|
||||
@@ -396,6 +540,7 @@ function convertToUIDocumentType(type: DocumentType & { groups: DocumentTypeGrou
|
||||
name: type.name,
|
||||
description: type.description || '',
|
||||
groups: type.groups || [],
|
||||
entry_module: type.entry_modules || null,
|
||||
llm_extraction_template_id: llmExtractionTemplateId,
|
||||
vlm_extraction_template_id: vlmExtractionTemplateId,
|
||||
evaluation_template_id: evaluationTemplateId,
|
||||
@@ -428,18 +573,22 @@ export async function getDocumentType(id: string, frontendJWT?: string): Promise
|
||||
name,
|
||||
description,
|
||||
evaluation_point_groups_ids,
|
||||
entry_module_id,
|
||||
entry_modules!fk_document_types_entry_module(id, name),
|
||||
prompt_config,
|
||||
created_at,
|
||||
updated_at,
|
||||
code
|
||||
`,
|
||||
`.replace(/\s+/g,' ').trim(),
|
||||
filter: {
|
||||
'id': `eq.${id}`
|
||||
},
|
||||
token: frontendJWT
|
||||
};
|
||||
|
||||
const response = await postgrestGet<DocumentType[]>('document_types', params);
|
||||
const response = await postgrestGet<(DocumentType & {
|
||||
entry_modules: { id: number; name: string } | null;
|
||||
})[]>('document_types', params);
|
||||
|
||||
if (response.error) {
|
||||
return { error: response.error, status: response.status };
|
||||
@@ -573,6 +722,7 @@ export async function createDocumentType(documentType: DocumentTypeCreateDTO, fr
|
||||
name: documentType.name.trim(),
|
||||
description: documentType.description || '',
|
||||
evaluation_point_groups_ids: groupIds,
|
||||
entry_module_id: documentType.entry_module_id || null,
|
||||
prompt_config: promptConfig,
|
||||
// code: documentType.code || null
|
||||
};
|
||||
@@ -698,6 +848,7 @@ export async function updateDocumentType(id: string, documentType: DocumentTypeU
|
||||
name: documentType.name.trim(),
|
||||
description: documentType.description || '',
|
||||
evaluation_point_groups_ids: groupIds,
|
||||
entry_module_id: documentType.entry_module_id || null,
|
||||
prompt_config: promptConfig
|
||||
};
|
||||
|
||||
|
||||
@@ -802,19 +802,10 @@ export interface RuleGroup {
|
||||
*/
|
||||
export async function getRuleTypes(documentTypeIds?: number[], token?: string): Promise<{data: RuleType[]; error?: never} | {data?: never; error: string; status?: number}> {
|
||||
try {
|
||||
// 如果没有传入 documentTypeIds,返回空数组
|
||||
if (!documentTypeIds || documentTypeIds.length === 0) {
|
||||
console.warn('getRuleTypes: 未提供 documentTypeIds');
|
||||
return { data: [] };
|
||||
}
|
||||
|
||||
// 1️⃣ 根据 documentTypeIds 查询 document_types 表
|
||||
const typeIdsStr = documentTypeIds.join(',');
|
||||
const documentTypesParams: PostgrestParams = {
|
||||
select: 'id, name, evaluation_point_groups_ids',
|
||||
filter: {
|
||||
'id': `in.(${typeIdsStr})`
|
||||
},
|
||||
filter: {},
|
||||
token
|
||||
};
|
||||
|
||||
|
||||
@@ -384,42 +384,6 @@ export async function getDocumentWithNoUserId(id: string, frontendJWT?: string):
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取文件下载链接
|
||||
* @param filePath 文件路径
|
||||
* @returns 下载链接
|
||||
*/
|
||||
export async function getFileDownloadUrl(filePath: string): Promise<{
|
||||
data?: { downloadUrl: string };
|
||||
error?: string;
|
||||
status?: number;
|
||||
}> {
|
||||
try {
|
||||
if (!filePath) {
|
||||
return { error: '文件路径不能为空', status: 400 };
|
||||
}
|
||||
|
||||
// 这里应该调用获取文件下载链接的API
|
||||
// 假设后端有这样的端点:/api/files/generate-download-url?path=xxx
|
||||
// 实际项目中需要根据你的后端API调整
|
||||
|
||||
// 临时解决方案:返回Remix路由路径
|
||||
// 这将通过Remix服务器代理对文件的访问
|
||||
return {
|
||||
data: {
|
||||
downloadUrl: `/documents/download?path=${encodeURIComponent(filePath)}`
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取文件下载链接失败:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '获取文件下载链接失败',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文档信息
|
||||
* @param id 文档ID
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { postgrestGet, type PostgrestParams } from '../postgrest-client';
|
||||
import dayjs from 'dayjs';
|
||||
import { UPLOAD_URL } from '../../config/api-config';
|
||||
import axios from 'axios';
|
||||
// import { API_BASE_URL } from '../client';
|
||||
|
||||
/**
|
||||
@@ -213,26 +214,15 @@ export async function uploadContractTemplate(
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData
|
||||
const response = await axios.post(uploadUrl, formData, {
|
||||
headers
|
||||
});
|
||||
|
||||
|
||||
console.log('【合同模板上传】服务器响应状态:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('【合同模板上传】服务器返回错误:', errorText);
|
||||
return {
|
||||
error: `服务器错误: ${response.status} ${response.statusText}`,
|
||||
status: response.status
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
const result = response.data;
|
||||
console.log('【合同模板上传】服务器返回结果:', result);
|
||||
|
||||
|
||||
if (result.success) {
|
||||
return { data: result.result };
|
||||
} else {
|
||||
@@ -299,26 +289,15 @@ export async function appendContractAttachments(
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData
|
||||
const response = await axios.post(uploadUrl, formData, {
|
||||
headers
|
||||
});
|
||||
|
||||
|
||||
console.log('【合同附件追加】服务器响应状态:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('【合同附件追加】服务器返回错误:', errorText);
|
||||
return {
|
||||
error: `服务器错误: ${response.status} ${response.statusText}`,
|
||||
status: response.status
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
const result = response.data;
|
||||
console.log('【合同附件追加】服务器返回结果:', result);
|
||||
|
||||
|
||||
if (result.success) {
|
||||
return { data: result.result };
|
||||
} else {
|
||||
@@ -388,12 +367,12 @@ export async function uploadDocumentToServer(
|
||||
// console.log('【调试】准备发送请求到服务器:', uploadUrl);
|
||||
|
||||
// 发送请求
|
||||
// const response = await fetch(`${API_BASE_URL}/admin/documents/upload`, {
|
||||
// const response = await axios.post(`${API_BASE_URL}/admin/documents/upload`, ...
|
||||
try {
|
||||
// console.log('【调试】开始fetch请求...');
|
||||
// console.log('【调试】开始axios请求...');
|
||||
|
||||
// 构建请求头,只在有JWT token时添加Authorization
|
||||
const headers: HeadersInit = {
|
||||
const headers: Record<string, string> = {
|
||||
'X-File-Name': encodeURIComponent(fileName)
|
||||
};
|
||||
|
||||
@@ -401,37 +380,16 @@ export async function uploadDocumentToServer(
|
||||
headers['Authorization'] = `Bearer ${jwtToken}`;
|
||||
}
|
||||
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData
|
||||
const response = await axios.post(uploadUrl, formData, {
|
||||
headers
|
||||
});
|
||||
|
||||
|
||||
// console.log('【调试】收到服务器响应:', { status: response.status, statusText: response.statusText });
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`【调试】上传失败 (${response.status}): ${errorText}`);
|
||||
return {
|
||||
error: `上传失败: ${response.status} ${response.statusText} - ${errorText}`,
|
||||
status: response.status
|
||||
};
|
||||
}
|
||||
|
||||
// console.log('【调试】开始解析JSON响应');
|
||||
let responseData;
|
||||
try {
|
||||
responseData = await response.json();
|
||||
// console.log('【上传调试】服务器原始JSON响应:', responseData);
|
||||
// console.log('【上传调试】响应类型:', typeof responseData);
|
||||
// console.log('【上传调试】响应keys:', Object.keys(responseData));
|
||||
} catch (jsonError) {
|
||||
console.error('【调试】JSON解析失败:', jsonError);
|
||||
return {
|
||||
error: `解析响应JSON失败: ${jsonError instanceof Error ? jsonError.message : '未知错误'}`,
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
|
||||
const responseData = response.data;
|
||||
// console.log('【上传调试】服务器原始JSON响应:', responseData);
|
||||
// console.log('【上传调试】响应类型:', typeof responseData);
|
||||
// console.log('【上传调试】响应keys:', Object.keys(responseData));
|
||||
|
||||
const extractedData = extractApiData<FileUploadResponse>(responseData);
|
||||
// console.log('【上传调试】提取后的数据:', extractedData);
|
||||
@@ -449,10 +407,17 @@ export async function uploadDocumentToServer(
|
||||
|
||||
// console.log('【调试】上传成功,返回数据');
|
||||
return { data: extractedData };
|
||||
} catch (fetchError) {
|
||||
console.error('【调试】fetch请求失败:', fetchError);
|
||||
return {
|
||||
error: `fetch请求错误: ${fetchError instanceof Error ? fetchError.message : '未知错误'}`,
|
||||
} catch (axiosError) {
|
||||
console.error('【调试】axios请求失败:', axiosError);
|
||||
if (axios.isAxiosError(axiosError)) {
|
||||
const errorText = axiosError.response?.data || axiosError.message;
|
||||
return {
|
||||
error: `上传失败: ${axiosError.response?.status || 500} ${axiosError.response?.statusText || ''} - ${errorText}`,
|
||||
status: axiosError.response?.status || 500
|
||||
};
|
||||
}
|
||||
return {
|
||||
error: `axios请求错误: ${axiosError instanceof Error ? axiosError.message : '未知错误'}`,
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { createCookieSessionStorage } from "@remix-run/node";
|
||||
import { postgrestGet, postgrestPost, postgrestPut } from "../postgrest-client";
|
||||
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
|
||||
import { OAUTH_CONFIG, API_BASE_URL } from "~/config/api-config";
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* 用户角色类型定义
|
||||
@@ -95,7 +96,7 @@ export const sessionStorage = createCookieSessionStorage({
|
||||
path: "/", // Cookie 作用域为整个应用
|
||||
sameSite: "lax", // CSRF 保护,允许顶级导航
|
||||
secrets: ["s3cr3t"], // TODO: 应该从环境变量读取
|
||||
maxAge: 60 * 60 * 2, // 2小时,与 OAuth Token 同步
|
||||
maxAge: 60 * 60 * 8, // 8小时,确保大于等于JWT token最大有效期(通常为6小时)
|
||||
secure: false, // 开发环境中禁用 HTTPS 要求
|
||||
},
|
||||
});
|
||||
@@ -369,12 +370,16 @@ export async function createUserSession(params: {
|
||||
if (params.frontendJWT) {
|
||||
session.set("frontendJWT", params.frontendJWT);
|
||||
}
|
||||
|
||||
const cookie = await sessionStorage.commitSession(session);
|
||||
// console.log("创建完整会话 - 设置Cookie:", !!cookie);
|
||||
// console.log("创建完整会话 - 用户角色:", params.userRole);
|
||||
// console.log("创建完整会话 - 重定向到:", params.redirectTo);
|
||||
|
||||
|
||||
// 🔑 根据 tokenExpiresIn 动态设置 Cookie 的 maxAge
|
||||
// 如果有 tokenExpiresIn,使用它作为 Cookie 有效期;否则使用默认值(8小时)
|
||||
const cookieMaxAge = params.tokenExpiresIn || (60 * 60 * 8); // 默认8小时
|
||||
// console.log("🍪 [createUserSession] Cookie maxAge:", cookieMaxAge, "秒 (", (cookieMaxAge / 3600).toFixed(2), "小时)");
|
||||
|
||||
const cookie = await sessionStorage.commitSession(session, {
|
||||
maxAge: cookieMaxAge // 🔑 动态设置 Cookie 有效期
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
status: 302, // HTTP 重定向状态码
|
||||
headers: {
|
||||
@@ -487,20 +492,18 @@ async function callIDaaSLogout(accessToken: string, appId: string): Promise<void
|
||||
formData.append('redirect_url', encodeURIComponent(redirectUri));
|
||||
|
||||
try {
|
||||
const response = await fetch(logoutUrl, {
|
||||
method: 'POST',
|
||||
const response = await axios.post(logoutUrl, formData.toString(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData.toString(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`IDaaS登出失败: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
console.log("IDaaS单点登出请求成功");
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error("调用IDaaS登出接口失败:", error.response?.status, error.response?.statusText);
|
||||
throw new Error(`IDaaS登出失败: ${error.response?.status} ${error.response?.statusText}`);
|
||||
}
|
||||
console.error("调用IDaaS登出接口失败:", error);
|
||||
throw error;
|
||||
}
|
||||
@@ -775,18 +778,16 @@ export async function simpleRootLogin(
|
||||
}
|
||||
|
||||
// 调用登录接口
|
||||
const loginResponse = await fetch(`${API_BASE_URL}/password_login`, {
|
||||
method: 'POST',
|
||||
const loginResponse = await axios.post(`${API_BASE_URL}/password_login`, {
|
||||
sub: username.trim(),
|
||||
password: password.trim()
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sub: username.trim(),
|
||||
password: password.trim()
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
const loginResult = await loginResponse.json();
|
||||
const loginResult = loginResponse.data;
|
||||
console.log('登录接口返回', loginResult);
|
||||
|
||||
// 检查重试次数
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { API_BASE_URL } from "~/config/api-config";
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* 登录请求参数(OAuth 方式)
|
||||
@@ -64,30 +65,26 @@ export async function loginWithOAuth(loginData: LoginRequest): Promise<LoginResp
|
||||
console.log("📝 [Login Client] 调用后端 OAuth 登录接口:", loginUrl);
|
||||
|
||||
try {
|
||||
const response = await fetch(loginUrl, {
|
||||
method: "POST",
|
||||
const response = await axios.post(loginUrl, loginData, {
|
||||
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);
|
||||
console.log("✅ [Login Client] OAuth 登录成功");
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const errorData = error.response?.data || {};
|
||||
console.error("❌ [Login Client] OAuth 登录请求失败:", error.response?.status, errorData);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorData.error || errorData.message || `登录失败: ${response.status}`
|
||||
error: errorData.error || errorData.message || `登录失败: ${error.response?.status || 'Unknown'}`
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("✅ [Login Client] OAuth 登录成功");
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("❌ [Login Client] OAuth 登录请求异常:", error);
|
||||
return {
|
||||
success: false,
|
||||
@@ -120,33 +117,29 @@ export async function loginWithPassword(
|
||||
console.log("📝 [Login Client] 调用后端密码登录接口:", loginUrl);
|
||||
|
||||
try {
|
||||
const response = await fetch(loginUrl, {
|
||||
method: "POST",
|
||||
const response = await axios.post(loginUrl, {
|
||||
username,
|
||||
password
|
||||
}, {
|
||||
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);
|
||||
console.log("✅ [Login Client] 密码登录成功");
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const errorData = error.response?.data || {};
|
||||
console.error("❌ [Login Client] 密码登录请求失败:", error.response?.status, errorData);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorData.error || errorData.message || `登录失败: ${response.status}`
|
||||
error: errorData.error || errorData.message || `登录失败: ${error.response?.status || 'Unknown'}`
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("✅ [Login Client] 密码登录成功");
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("❌ [Login Client] 密码登录请求异常:", error);
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* 2. 如果需要新的网络请求,在 `OAuthClient` 中添加
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
interface OAuthConfig {
|
||||
serverUrl: string;
|
||||
clientId: string;
|
||||
@@ -114,46 +116,38 @@ export class OAuthClient {
|
||||
});
|
||||
|
||||
try {
|
||||
// 创建 AbortController 用于超时控制
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 15000); // 15秒超时
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
const response = await axios.post(url, data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: data,
|
||||
signal: controller.signal
|
||||
timeout: 60000 // 60秒超时
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
console.log('🔧 Token响应状态:', response.status, response.statusText);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error('❌ 获取访问令牌失败:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
errorData: errorData
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenResponse = await response.json() as TokenResponse;
|
||||
const tokenResponse = response.data as TokenResponse;
|
||||
console.log('✅ 获取访问令牌成功:', {
|
||||
token_type: tokenResponse.token_type,
|
||||
expires_in: tokenResponse.expires_in,
|
||||
scope: tokenResponse.scope
|
||||
});
|
||||
|
||||
|
||||
return tokenResponse;
|
||||
} catch (error) {
|
||||
// 判断是否为超时错误
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
console.error('❌ 获取访问令牌超时(15秒):', error.message);
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
console.error('❌ 获取访问令牌超时(60秒):', error.message);
|
||||
} else if (error.response) {
|
||||
console.error('❌ 获取访问令牌失败:', {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
errorData: error.response.data
|
||||
});
|
||||
} else {
|
||||
console.error('❌ 获取访问令牌网络错误:', error.message);
|
||||
}
|
||||
} else {
|
||||
console.error('❌ 获取访问令牌网络错误:', error);
|
||||
console.error('❌ 获取访问令牌错误:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -168,31 +162,25 @@ export class OAuthClient {
|
||||
const url = `${this.config.serverUrl}/api/bff/v1.2/oauth2/userinfo`;
|
||||
|
||||
try {
|
||||
// 创建 AbortController 用于超时控制
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 15000); // 15秒超时
|
||||
|
||||
const response = await fetch(url, {
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
},
|
||||
signal: controller.signal
|
||||
timeout: 60000 // 60秒超时
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('获取用户信息失败:', response.status, response.statusText);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json() as UserInfoResponse;
|
||||
return response.data as UserInfoResponse;
|
||||
} catch (error) {
|
||||
// 判断是否为超时错误
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
console.error('❌ 获取用户信息超时(15秒):', error.message);
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
console.error('❌ 获取用户信息超时(60秒):', error.message);
|
||||
} else if (error.response) {
|
||||
console.error('获取用户信息失败:', error.response.status, error.response.statusText);
|
||||
} else {
|
||||
console.error('❌ 获取用户信息网络错误:', error.message);
|
||||
}
|
||||
} else {
|
||||
console.error('❌ 获取用户信息网络错误:', error);
|
||||
console.error('❌ 获取用户信息错误:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -219,34 +207,25 @@ export class OAuthClient {
|
||||
});
|
||||
|
||||
try {
|
||||
// 创建 AbortController 用于超时控制
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 15000); // 15秒超时
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
const response = await axios.post(url, data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: data,
|
||||
signal: controller.signal
|
||||
timeout: 60000 // 60秒超时
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error('刷新访问令牌失败:', errorData);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json() as TokenResponse;
|
||||
return response.data as TokenResponse;
|
||||
} catch (error) {
|
||||
// 判断是否为超时错误
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
console.error('❌ 刷新访问令牌超时(15秒):', error.message);
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
console.error('❌ 刷新访问令牌超时(60秒):', error.message);
|
||||
} else if (error.response) {
|
||||
console.error('刷新访问令牌失败:', error.response.data);
|
||||
} else {
|
||||
console.error('❌ 刷新访问令牌网络错误:', error.message);
|
||||
}
|
||||
} else {
|
||||
console.error('❌ 刷新访问令牌网络错误:', error);
|
||||
console.error('❌ 刷新访问令牌错误:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -266,25 +245,21 @@ export class OAuthClient {
|
||||
});
|
||||
|
||||
try {
|
||||
// 创建 AbortController 用于超时控制
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 15000); // 15秒超时
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
const response = await axios.post(url, data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: data,
|
||||
signal: controller.signal
|
||||
timeout: 60000 // 60秒超时
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
return response.ok;
|
||||
return response.status >= 200 && response.status < 300;
|
||||
} catch (error) {
|
||||
// 判断是否为超时错误
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
console.error('❌ 登出超时(15秒):', error.message);
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
console.error('❌ 登出超时(60秒):', error.message);
|
||||
} else {
|
||||
console.error('❌ 登出失败:', error);
|
||||
}
|
||||
} else {
|
||||
console.error('❌ 登出失败:', error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,560 @@
|
||||
/**
|
||||
* 角色权限管理 API
|
||||
* 用于角色、路由权限、用户角色的管理
|
||||
*/
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/**
|
||||
* 路由信息
|
||||
*/
|
||||
export interface RouteInfo {
|
||||
id: number;
|
||||
route_path: string;
|
||||
route_name: string;
|
||||
route_title: string;
|
||||
component?: string;
|
||||
parent_id?: number | null;
|
||||
icon?: string;
|
||||
sort_order: number;
|
||||
is_hidden: boolean;
|
||||
is_cache: boolean;
|
||||
status: number;
|
||||
children?: RouteInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色信息
|
||||
*/
|
||||
export interface RoleInfo {
|
||||
id: number;
|
||||
role_key: string;
|
||||
role_name: string;
|
||||
data_scope: string;
|
||||
description: string;
|
||||
parent_role_id?: number | null;
|
||||
priority: number;
|
||||
is_system_role: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色-路由权限关联
|
||||
*/
|
||||
export interface RoleRoutePermission {
|
||||
id: number;
|
||||
role_id: number;
|
||||
route_id: number;
|
||||
permission: string; // 'R' | 'RW' | 'NONE'
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
export interface UserInfo {
|
||||
id: number;
|
||||
username: string;
|
||||
nick_name: string;
|
||||
phone_number?: string;
|
||||
email?: string;
|
||||
ou_name: string;
|
||||
status: number;
|
||||
is_leader: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户-角色关联
|
||||
*/
|
||||
export interface UserRoleRelation {
|
||||
id: number;
|
||||
user_id: number;
|
||||
role_id: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// ==================== 模拟数据 ====================
|
||||
|
||||
/**
|
||||
* 模拟路由数据(树形结构)
|
||||
*/
|
||||
const mockRoutes: RouteInfo[] = [
|
||||
{
|
||||
id: 1,
|
||||
route_path: '/documents',
|
||||
route_name: 'documents',
|
||||
route_title: '文档管理',
|
||||
icon: 'ri-file-text-line',
|
||||
sort_order: 1,
|
||||
is_hidden: false,
|
||||
is_cache: true,
|
||||
status: 1,
|
||||
parent_id: null,
|
||||
children: [
|
||||
{
|
||||
id: 11,
|
||||
route_path: '/documents/list',
|
||||
route_name: 'documents-list',
|
||||
route_title: '文档列表',
|
||||
icon: 'ri-list-check',
|
||||
sort_order: 1,
|
||||
is_hidden: false,
|
||||
is_cache: true,
|
||||
status: 1,
|
||||
parent_id: 1
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
route_path: '/documents/upload',
|
||||
route_name: 'documents-upload',
|
||||
route_title: '文档上传',
|
||||
icon: 'ri-upload-line',
|
||||
sort_order: 2,
|
||||
is_hidden: false,
|
||||
is_cache: true,
|
||||
status: 1,
|
||||
parent_id: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
route_path: '/cross-checking',
|
||||
route_name: 'cross-checking',
|
||||
route_title: '交叉评查',
|
||||
icon: 'ri-exchange-line',
|
||||
sort_order: 2,
|
||||
is_hidden: false,
|
||||
is_cache: true,
|
||||
status: 1,
|
||||
parent_id: null,
|
||||
children: [
|
||||
{
|
||||
id: 21,
|
||||
route_path: '/cross-checking/tasks',
|
||||
route_name: 'cross-checking-tasks',
|
||||
route_title: '评查任务',
|
||||
icon: 'ri-task-line',
|
||||
sort_order: 1,
|
||||
is_hidden: false,
|
||||
is_cache: true,
|
||||
status: 1,
|
||||
parent_id: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
route_path: '/settings',
|
||||
route_name: 'settings',
|
||||
route_title: '系统设置',
|
||||
icon: 'ri-settings-3-line',
|
||||
sort_order: 3,
|
||||
is_hidden: false,
|
||||
is_cache: true,
|
||||
status: 1,
|
||||
parent_id: null,
|
||||
children: [
|
||||
{
|
||||
id: 31,
|
||||
route_path: '/settings/document-types',
|
||||
route_name: 'document-types',
|
||||
route_title: '文档类型管理',
|
||||
icon: 'ri-file-list-line',
|
||||
sort_order: 1,
|
||||
is_hidden: false,
|
||||
is_cache: true,
|
||||
status: 1,
|
||||
parent_id: 3
|
||||
},
|
||||
{
|
||||
id: 32,
|
||||
route_path: '/settings/rule-groups',
|
||||
route_name: 'rule-groups',
|
||||
route_title: '评查点分组',
|
||||
icon: 'ri-folder-line',
|
||||
sort_order: 2,
|
||||
is_hidden: false,
|
||||
is_cache: true,
|
||||
status: 1,
|
||||
parent_id: 3
|
||||
},
|
||||
{
|
||||
id: 33,
|
||||
route_path: '/settings/prompts',
|
||||
route_name: 'prompts',
|
||||
route_title: '提示词管理',
|
||||
icon: 'ri-message-line',
|
||||
sort_order: 3,
|
||||
is_hidden: false,
|
||||
is_cache: true,
|
||||
status: 1,
|
||||
parent_id: 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
route_path: '/role-permissions',
|
||||
route_name: 'role-permissions',
|
||||
route_title: '角色权限管理',
|
||||
icon: 'ri-shield-user-line',
|
||||
sort_order: 4,
|
||||
is_hidden: false,
|
||||
is_cache: true,
|
||||
status: 1,
|
||||
parent_id: null
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* 模拟角色数据
|
||||
*/
|
||||
const mockRoles: RoleInfo[] = [
|
||||
{
|
||||
id: 1,
|
||||
role_key: 'admin',
|
||||
role_name: '系统管理员',
|
||||
data_scope: 'ALL',
|
||||
description: '拥有系统所有权限',
|
||||
priority: 1,
|
||||
is_system_role: true,
|
||||
created_at: '2024-01-01 10:00:00',
|
||||
updated_at: '2024-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
role_key: 'provincial',
|
||||
role_name: '省级管理员',
|
||||
data_scope: 'PROVINCE',
|
||||
description: '省级权限,可管理文档类型和评查点',
|
||||
priority: 2,
|
||||
is_system_role: false,
|
||||
created_at: '2024-01-02 10:00:00',
|
||||
updated_at: '2024-01-02 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
role_key: 'city_admin',
|
||||
role_name: '市级管理员',
|
||||
data_scope: 'CITY',
|
||||
description: '市级权限,可管理本市文档',
|
||||
priority: 3,
|
||||
is_system_role: false,
|
||||
created_at: '2024-01-03 10:00:00',
|
||||
updated_at: '2024-01-03 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
role_key: 'common_user',
|
||||
role_name: '普通用户',
|
||||
data_scope: 'SELF',
|
||||
description: '普通用户,只能查看自己的文档',
|
||||
priority: 4,
|
||||
is_system_role: false,
|
||||
created_at: '2024-01-04 10:00:00',
|
||||
updated_at: '2024-01-04 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
role_key: 'reviewer',
|
||||
role_name: '评审员',
|
||||
data_scope: 'DEPARTMENT',
|
||||
description: '负责文档评审工作',
|
||||
priority: 5,
|
||||
is_system_role: false,
|
||||
created_at: '2024-01-05 10:00:00',
|
||||
updated_at: '2024-01-05 10:00:00'
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* 模拟角色-路由权限关联数据
|
||||
*/
|
||||
const mockRoleRoutePermissions: RoleRoutePermission[] = [
|
||||
// 系统管理员拥有所有权限
|
||||
{ id: 1, role_id: 1, route_id: 1, permission: 'RW', created_at: '2024-01-01 10:00:00' },
|
||||
{ id: 2, role_id: 1, route_id: 11, permission: 'RW', created_at: '2024-01-01 10:00:00' },
|
||||
{ id: 3, role_id: 1, route_id: 12, permission: 'RW', created_at: '2024-01-01 10:00:00' },
|
||||
{ id: 4, role_id: 1, route_id: 2, permission: 'RW', created_at: '2024-01-01 10:00:00' },
|
||||
{ id: 5, role_id: 1, route_id: 21, permission: 'RW', created_at: '2024-01-01 10:00:00' },
|
||||
{ id: 6, role_id: 1, route_id: 3, permission: 'RW', created_at: '2024-01-01 10:00:00' },
|
||||
{ id: 7, role_id: 1, route_id: 31, permission: 'RW', created_at: '2024-01-01 10:00:00' },
|
||||
{ id: 8, role_id: 1, route_id: 32, permission: 'RW', created_at: '2024-01-01 10:00:00' },
|
||||
{ id: 9, role_id: 1, route_id: 33, permission: 'RW', created_at: '2024-01-01 10:00:00' },
|
||||
{ id: 10, role_id: 1, route_id: 4, permission: 'RW', created_at: '2024-01-01 10:00:00' },
|
||||
|
||||
// 省级管理员
|
||||
{ id: 11, role_id: 2, route_id: 1, permission: 'RW', created_at: '2024-01-02 10:00:00' },
|
||||
{ id: 12, role_id: 2, route_id: 11, permission: 'RW', created_at: '2024-01-02 10:00:00' },
|
||||
{ id: 13, role_id: 2, route_id: 12, permission: 'RW', created_at: '2024-01-02 10:00:00' },
|
||||
{ id: 14, role_id: 2, route_id: 3, permission: 'RW', created_at: '2024-01-02 10:00:00' },
|
||||
{ id: 15, role_id: 2, route_id: 31, permission: 'RW', created_at: '2024-01-02 10:00:00' },
|
||||
{ id: 16, role_id: 2, route_id: 32, permission: 'RW', created_at: '2024-01-02 10:00:00' },
|
||||
|
||||
// 普通用户
|
||||
{ id: 17, role_id: 4, route_id: 1, permission: 'R', created_at: '2024-01-04 10:00:00' },
|
||||
{ id: 18, role_id: 4, route_id: 11, permission: 'R', created_at: '2024-01-04 10:00:00' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 模拟用户数据
|
||||
*/
|
||||
const mockUsers: UserInfo[] = [
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
nick_name: '系统管理员',
|
||||
phone_number: '13800138000',
|
||||
email: 'admin@example.com',
|
||||
ou_name: '系统管理部',
|
||||
status: 1,
|
||||
is_leader: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'zhangsan',
|
||||
nick_name: '张三',
|
||||
phone_number: '13800138001',
|
||||
email: 'zhangsan@example.com',
|
||||
ou_name: '广东省局',
|
||||
status: 1,
|
||||
is_leader: true
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'lisi',
|
||||
nick_name: '李四',
|
||||
phone_number: '13800138002',
|
||||
email: 'lisi@example.com',
|
||||
ou_name: '梅州市局',
|
||||
status: 1,
|
||||
is_leader: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
username: 'wangwu',
|
||||
nick_name: '王五',
|
||||
phone_number: '13800138003',
|
||||
email: 'wangwu@example.com',
|
||||
ou_name: '云浮市局',
|
||||
status: 1,
|
||||
is_leader: false
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
username: 'zhaoliu',
|
||||
nick_name: '赵六',
|
||||
phone_number: '13800138004',
|
||||
email: 'zhaoliu@example.com',
|
||||
ou_name: '揭阳市局',
|
||||
status: 1,
|
||||
is_leader: false
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* 模拟用户-角色关联数据
|
||||
*/
|
||||
const mockUserRoles: UserRoleRelation[] = [
|
||||
{ id: 1, user_id: 1, role_id: 1, created_at: '2024-01-01 10:00:00' },
|
||||
{ id: 2, user_id: 2, role_id: 2, created_at: '2024-01-02 10:00:00' },
|
||||
{ id: 3, user_id: 3, role_id: 3, created_at: '2024-01-03 10:00:00' },
|
||||
{ id: 4, user_id: 4, role_id: 4, created_at: '2024-01-04 10:00:00' },
|
||||
{ id: 5, user_id: 5, role_id: 5, created_at: '2024-01-05 10:00:00' }
|
||||
];
|
||||
|
||||
// ==================== API 函数 ====================
|
||||
|
||||
/**
|
||||
* 获取所有角色列表
|
||||
*/
|
||||
export async function getRoles(): Promise<RoleInfo[]> {
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return mockRoles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有路由(树形结构)
|
||||
*/
|
||||
export async function getRoutes(): Promise<RouteInfo[]> {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return mockRoutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定角色的路由权限
|
||||
* @param roleId 角色ID
|
||||
*/
|
||||
export async function getRoleRoutePermissions(roleId: number): Promise<RoleRoutePermission[]> {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
return mockRoleRoutePermissions.filter(p => p.role_id === roleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色的路由权限
|
||||
* @param roleId 角色ID
|
||||
* @param routeIds 路由ID数组
|
||||
*/
|
||||
export async function updateRoleRoutePermissions(
|
||||
roleId: number,
|
||||
routeIds: number[]
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// 在实际应用中,这里会调用后端API
|
||||
console.log('更新角色权限:', { roleId, routeIds });
|
||||
|
||||
// 模拟更新本地数据
|
||||
// 删除该角色的旧权限
|
||||
const oldPermissions = mockRoleRoutePermissions.filter(p => p.role_id === roleId);
|
||||
oldPermissions.forEach(p => {
|
||||
const index = mockRoleRoutePermissions.indexOf(p);
|
||||
if (index > -1) {
|
||||
mockRoleRoutePermissions.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加新权限
|
||||
routeIds.forEach((routeId, index) => {
|
||||
mockRoleRoutePermissions.push({
|
||||
id: Date.now() + index,
|
||||
role_id: roleId,
|
||||
route_id: routeId,
|
||||
permission: 'RW',
|
||||
created_at: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
return { success: true, message: '角色权限更新成功' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定角色的用户列表
|
||||
* @param roleId 角色ID
|
||||
*/
|
||||
export async function getRoleUsers(roleId: number): Promise<UserInfo[]> {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
// 查找具有该角色的用户ID
|
||||
const userIds = mockUserRoles
|
||||
.filter(ur => ur.role_id === roleId)
|
||||
.map(ur => ur.user_id);
|
||||
|
||||
// 返回用户详细信息
|
||||
return mockUsers.filter(u => userIds.includes(u.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有用户列表
|
||||
*/
|
||||
export async function getAllUsers(): Promise<UserInfo[]> {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return mockUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为用户分配角色
|
||||
* @param userId 用户ID
|
||||
* @param roleIds 角色ID数组
|
||||
*/
|
||||
export async function assignUserRoles(
|
||||
userId: number,
|
||||
roleIds: number[]
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
console.log('为用户分配角色:', { userId, roleIds });
|
||||
|
||||
// 模拟更新本地数据
|
||||
// 删除该用户的旧角色
|
||||
const oldRoles = mockUserRoles.filter(ur => ur.user_id === userId);
|
||||
oldRoles.forEach(ur => {
|
||||
const index = mockUserRoles.indexOf(ur);
|
||||
if (index > -1) {
|
||||
mockUserRoles.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加新角色
|
||||
roleIds.forEach((roleId, index) => {
|
||||
mockUserRoles.push({
|
||||
id: Date.now() + index,
|
||||
user_id: userId,
|
||||
role_id: roleId,
|
||||
created_at: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
return { success: true, message: '用户角色分配成功' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新角色
|
||||
* @param roleData 角色数据
|
||||
*/
|
||||
export async function createRole(
|
||||
roleData: Omit<RoleInfo, 'id' | 'created_at' | 'updated_at'>
|
||||
): Promise<{ success: boolean; message: string; data?: RoleInfo }> {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
const newRole: RoleInfo = {
|
||||
...roleData,
|
||||
id: Math.max(...mockRoles.map(r => r.id)) + 1,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
mockRoles.push(newRole);
|
||||
|
||||
return { success: true, message: '角色创建成功', data: newRole };
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色信息
|
||||
* @param roleId 角色ID
|
||||
* @param roleData 角色数据
|
||||
*/
|
||||
export async function updateRole(
|
||||
roleId: number,
|
||||
roleData: Partial<Omit<RoleInfo, 'id' | 'created_at' | 'updated_at'>>
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
const roleIndex = mockRoles.findIndex(r => r.id === roleId);
|
||||
if (roleIndex === -1) {
|
||||
return { success: false, message: '角色不存在' };
|
||||
}
|
||||
|
||||
mockRoles[roleIndex] = {
|
||||
...mockRoles[roleIndex],
|
||||
...roleData,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
return { success: true, message: '角色更新成功' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
* @param roleId 角色ID
|
||||
*/
|
||||
export async function deleteRole(roleId: number): Promise<{ success: boolean; message: string }> {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
const role = mockRoles.find(r => r.id === roleId);
|
||||
if (!role) {
|
||||
return { success: false, message: '角色不存在' };
|
||||
}
|
||||
|
||||
if (role.is_system_role) {
|
||||
return { success: false, message: '系统角色不能删除' };
|
||||
}
|
||||
|
||||
const roleIndex = mockRoles.indexOf(role);
|
||||
mockRoles.splice(roleIndex, 1);
|
||||
|
||||
return { success: true, message: '角色删除成功' };
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { get } from '../axios-client';
|
||||
import { API_BASE_URL } from '../../config/api-config';
|
||||
import axios from 'axios';
|
||||
|
||||
// 用户信息接口
|
||||
export interface UserInfo {
|
||||
@@ -56,24 +57,16 @@ export async function getOrganizationTree(includeUsers: boolean = true, jwtToken
|
||||
let responseData: OrganizationResponse;
|
||||
|
||||
if (jwtToken) {
|
||||
// 如果提供了JWT Token,则使用fetch并携带Authorization头
|
||||
// 如果提供了JWT Token,则使用axios并携带Authorization头
|
||||
const url = `${API_BASE_URL}/admin/users/organizations?include_users=${includeUsers}`;
|
||||
const response = await fetch(url, {
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${jwtToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('获取组织架构失败 (fetch):', errorText);
|
||||
return {
|
||||
success: false,
|
||||
error: `HTTP error! status: ${response.status}, ${errorText}`
|
||||
};
|
||||
}
|
||||
responseData = await response.json();
|
||||
responseData = response.data;
|
||||
} else {
|
||||
// 否则,使用原有的get方法
|
||||
const response = await get<OrganizationResponse>(
|
||||
|
||||
@@ -33,11 +33,11 @@ export function ClientAuthGuard({ isPublicPath }: ClientAuthGuardProps) {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const authenticated = isAuthenticated();
|
||||
|
||||
console.log('🔍 [Auth Guard] 认证检查', {
|
||||
token: token ? `${token.substring(0, 20)}...` : null,
|
||||
authenticated,
|
||||
pathname: location.pathname
|
||||
});
|
||||
// console.log('🔍 [Auth Guard] 认证检查', {
|
||||
// token: token ? `${token.substring(0, 20)}...` : null,
|
||||
// authenticated,
|
||||
// pathname: location.pathname
|
||||
// });
|
||||
|
||||
if (!authenticated) {
|
||||
console.log('🔒 [Auth Guard] 未认证,重定向到登录页');
|
||||
@@ -48,7 +48,7 @@ export function ClientAuthGuard({ isPublicPath }: ClientAuthGuardProps) {
|
||||
// 跳转到登录页,并传递重定向目标
|
||||
navigate(`/login?redirect=${encodeURIComponent(redirectTo)}`, { replace: true });
|
||||
} else {
|
||||
console.log('✅ [Auth Guard] 已认证,允许访问');
|
||||
// console.log('✅ [Auth Guard] 已认证,允许访问');
|
||||
}
|
||||
}, [isPublicPath, navigate, location.pathname]);
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from '../../api/cross-checking/cross-file-result';
|
||||
import { useFetcher, useNavigate } from '@remix-run/react';
|
||||
import { API_BASE_URL } from '~/config/api-config';
|
||||
import axios from 'axios';
|
||||
// import '../../styles/components/TooltipStyles.css';
|
||||
|
||||
/**
|
||||
@@ -768,18 +769,16 @@ export function ReviewPointsList({
|
||||
}
|
||||
// 打印最终请求体
|
||||
// console.log('最终请求体:', data);
|
||||
// 用原生 fetch + application/json 提交
|
||||
// 用 axios + application/json 提交
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL.replace(/\/$/, '')}/admin/cross_review/proposals`, {
|
||||
method: 'POST',
|
||||
const response = await axios.post(`${API_BASE_URL.replace(/\/$/, '')}/admin/cross_review/proposals`, data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${userInfo.frontend_jwt}`,
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
}
|
||||
});
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
const result = response.data;
|
||||
if (response.status === 200) {
|
||||
toastService.success('意见提交成功');
|
||||
|
||||
// 创建新的提案对象
|
||||
|
||||
@@ -37,7 +37,7 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
||||
|
||||
// 获取用户路由权限
|
||||
useEffect(() => {
|
||||
console.log('🔍 [Sidebar] useEffect 触发,开始获取路由权限');
|
||||
// console.log('🔍 [Sidebar] useEffect 触发,开始获取路由权限');
|
||||
|
||||
const fetchUserRoutes = async () => {
|
||||
setIsLoadingRoutes(true);
|
||||
@@ -69,7 +69,7 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
||||
|
||||
// 如果需要重定向到首页
|
||||
if (result.shouldRedirectToHome) {
|
||||
console.log('🔄 [Sidebar] 重定向到首页');
|
||||
// console.log('🔄 [Sidebar] 重定向到首页');
|
||||
navigate('/');
|
||||
return;
|
||||
}
|
||||
@@ -158,6 +158,7 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
||||
|
||||
// 处理菜单项:清理子菜单结构
|
||||
const processedMenuItems: MenuItem[] = menuItems.filter(item =>{
|
||||
// console.log('菜单项:', item.title, 'Icon:', item.icon)
|
||||
// 如果是省局访问
|
||||
if(isPort51707){
|
||||
if (selectedModuleName === '智慧法务大模型'){
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Button } from '~/components/ui/Button';
|
||||
import { toastService } from '~/components/ui/Toast';
|
||||
// import { DOCUMENT_URL } from "~/api/axios-client";
|
||||
import { uploadContractTemplate } from '~/api/files/files-upload';
|
||||
import axios from 'axios';
|
||||
|
||||
interface ReviewTabsProps {
|
||||
activeTab: string;
|
||||
@@ -65,14 +66,13 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
||||
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
|
||||
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(fileInfo.path || '')}`;
|
||||
|
||||
// 使用fetch获取文件内容
|
||||
const response = await fetch(downloadUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载失败: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
// 使用axios获取文件内容
|
||||
const response = await axios.get(downloadUrl, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
|
||||
// 将响应转换为Blob
|
||||
const blob = await response.blob();
|
||||
// axios已经返回Blob
|
||||
const blob = response.data;
|
||||
|
||||
// 创建Blob URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
@@ -179,6 +179,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
console.log("✅ [Callback] 后端登录成功,JWT token 已获取");
|
||||
const frontendJWT = loginResponse.data.access_token;
|
||||
const savedUserInfo = loginResponse.data.user_info;
|
||||
const backExpiresIn = loginResponse.data.expires_in || (60 * 60 * 8)
|
||||
|
||||
// 🔑 提取后端返回的签发时间并转换为时间戳
|
||||
let tokenIssuedAt = Date.now(); // 默认使用当前时间
|
||||
@@ -235,7 +236,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
redirectTo: callbackUrl.toString(), // 先跳转到 callback 页面保存 token
|
||||
accessToken: tokenResponse.access_token,
|
||||
refreshToken: tokenResponse.refresh_token,
|
||||
tokenExpiresIn: tokenResponse.expires_in,
|
||||
tokenExpiresIn: backExpiresIn,
|
||||
tokenIssuedAt: tokenIssuedAt, // 🔑 传递后端返回的签发时间
|
||||
userInfo: enhancedUserInfo,
|
||||
frontendJWT
|
||||
|
||||
@@ -6,14 +6,14 @@ import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Pagination } from "~/components/ui/Pagination";
|
||||
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
|
||||
import { getRuleTypes, getRuleGroupsByType, type RuleType, type RuleGroup } from "~/api/evaluation_points/rules";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import {
|
||||
getDocumentTypes,
|
||||
deleteDocumentType,
|
||||
type DocumentTypeUI,
|
||||
import {
|
||||
getDocumentTypes,
|
||||
deleteDocumentType,
|
||||
type DocumentTypeUI,
|
||||
type DocumentTypeSearchParams,
|
||||
type DocumentTypeGroup
|
||||
type DocumentTypeGroup,
|
||||
getParentEvaluationPointGroups
|
||||
} from "~/api/document-types/document-types";
|
||||
import documentTypesStyles from "~/styles/pages/document-types_index.css?url";
|
||||
|
||||
@@ -40,8 +40,7 @@ interface LoaderData {
|
||||
pageSize: number;
|
||||
currentPage: number;
|
||||
error?: string;
|
||||
groups: DocumentTypeGroup[];
|
||||
ruleTypes: RuleType[];
|
||||
parentGroups: DocumentTypeGroup[];
|
||||
frontendJWT?: string | null;
|
||||
}
|
||||
|
||||
@@ -69,11 +68,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
};
|
||||
|
||||
// 并行获取文档类型数据和父级评查点分组
|
||||
const ruleTypesResponse = await getRuleTypes(undefined, frontendJWT);
|
||||
if(ruleTypesResponse.error){
|
||||
console.error("获取父级评查点分组失败:", ruleTypesResponse.error);
|
||||
const parentGroupsResponse = await getParentEvaluationPointGroups(frontendJWT);
|
||||
if(parentGroupsResponse.error){
|
||||
console.error("获取父级评查点分组失败:", parentGroupsResponse.error);
|
||||
}
|
||||
const ruleTypes = ruleTypesResponse.error ? [] : ruleTypesResponse.data;
|
||||
const parentGroups = parentGroupsResponse.error ? [] : (parentGroupsResponse.data || []);
|
||||
|
||||
const typesResponse = await getDocumentTypes(searchParams, frontendJWT);
|
||||
if(typesResponse.error){
|
||||
@@ -81,16 +80,16 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
throw new Error(typesResponse.error);
|
||||
}
|
||||
const typesResult = typesResponse.data?.types || [];
|
||||
|
||||
// console.log('文档类型数据:', typesResult.data?.types);
|
||||
// console.log('父级评查点分组:', groupsResult.data);
|
||||
|
||||
|
||||
// console.log('文档类型数据:', typesResult);
|
||||
// console.log('父级评查点分组:', parentGroups);
|
||||
|
||||
return Response.json({
|
||||
types: typesResult,
|
||||
total: typesResponse.data?.total || typesResult.length,
|
||||
pageSize,
|
||||
currentPage: page,
|
||||
ruleTypes,
|
||||
parentGroups,
|
||||
frontendJWT
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -140,58 +139,18 @@ export default function DocumentTypesList() {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// 获取加载器数据
|
||||
const { types, total, error, ruleTypes, frontendJWT } = useLoaderData<LoaderData>();
|
||||
const { types, total, error, parentGroups, frontendJWT } = useLoaderData<LoaderData>();
|
||||
|
||||
// 获取用户角色并判断权限
|
||||
const rootData = useRouteLoaderData("root") as { userRole: string };
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
const hasEditPermission = userRole.toLowerCase().includes('provin');
|
||||
|
||||
// 状态管理
|
||||
const [ruleGroups, setRuleGroups] = useState<RuleGroup[]>([]);
|
||||
const [loadingGroups, setLoadingGroups] = useState(false);
|
||||
|
||||
// 获取当前的ruleType值
|
||||
const ruleTypeParam = searchParams.get('ruleType');
|
||||
|
||||
// 获取搜索参数
|
||||
const name = searchParams.get('name') || '';
|
||||
const currentPage = parseInt(searchParams.get('page') || String(1), 10);
|
||||
const pageSize = parseInt(searchParams.get('pageSize') || String(10), 10);
|
||||
|
||||
// 判断是否禁用子级评查分组选择,true表示禁用,false表示不禁用
|
||||
const isRuleGroupSelectDisabled = loadingGroups || !ruleTypeParam || ruleGroups.length === 0;
|
||||
|
||||
// 当评查点类型变化时,加载对应的子级评查分组
|
||||
useEffect(() => {
|
||||
// 如果选择了"全部"或未选择,则清空子级评查分组
|
||||
if (!ruleTypeParam || ruleTypeParam === 'all') {
|
||||
setRuleGroups([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载当前类型的子级评查分组
|
||||
const loadRuleGroups = async () => {
|
||||
setLoadingGroups(true);
|
||||
try {
|
||||
const response = await getRuleGroupsByType(ruleTypeParam, frontendJWT || undefined);
|
||||
if (response.data) {
|
||||
setRuleGroups(response.data);
|
||||
} else if (response.error) {
|
||||
console.error('加载子级规则组失败:', response.error);
|
||||
setRuleGroups([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载子级规则组出错:', error);
|
||||
toastService.error('加载子级规则组出错:' + error);
|
||||
setRuleGroups([]);
|
||||
} finally {
|
||||
setLoadingGroups(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadRuleGroups();
|
||||
}, [ruleTypeParam]);
|
||||
|
||||
// 处理loader加载数据的时候的错误
|
||||
useEffect(() => {
|
||||
if(error){
|
||||
@@ -216,36 +175,16 @@ export default function DocumentTypesList() {
|
||||
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
|
||||
// 如果是子级评查分组选择,但是当前应该被禁用,则不处理
|
||||
if (name === 'groupId' && isRuleGroupSelectDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (value) {
|
||||
newParams.set(name, value);
|
||||
|
||||
// 如果是评查点类型变更,清空子级评查分组选择
|
||||
if (name === 'ruleType') {
|
||||
newParams.delete('groupId');
|
||||
// 如果选择了"全部"或空值,也清空子级评查分组选择
|
||||
if (value === '' || value === 'all') {
|
||||
setRuleGroups([]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newParams.delete(name);
|
||||
|
||||
// 如果清除评查点类型,也清除规则组
|
||||
if (name === 'ruleType') {
|
||||
newParams.delete('groupId');
|
||||
setRuleGroups([]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 切换筛选条件时,重置到第一页
|
||||
newParams.set('page', '1');
|
||||
|
||||
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
@@ -317,7 +256,7 @@ export default function DocumentTypesList() {
|
||||
{
|
||||
title: "文档类型名称",
|
||||
key: "name",
|
||||
width: "200px",
|
||||
width: "180px",
|
||||
render: (_: unknown, record: DocumentTypeUI) => (
|
||||
<div className="flex items-center">
|
||||
<i className="ri-file-text-line text-primary mr-2"></i>
|
||||
@@ -328,13 +267,27 @@ export default function DocumentTypesList() {
|
||||
{
|
||||
title: "描述",
|
||||
key: "description",
|
||||
width: "300px",
|
||||
width: "250px",
|
||||
render: (_: unknown, record: DocumentTypeUI) => (
|
||||
<div className="text-secondary text-sm truncate max-w-xs" title={record.description}>
|
||||
{record.description}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "入口模块",
|
||||
key: "entry_module",
|
||||
width: "150px",
|
||||
render: (_: unknown, record: DocumentTypeUI) => (
|
||||
<div className="flex items-center">
|
||||
{record.entry_module ? (
|
||||
<span className="type-badge">{record.entry_module.name}</span>
|
||||
) : (
|
||||
<span className="text-gray-400">暂无关联入口</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "关联的评查点分组",
|
||||
key: "groups",
|
||||
@@ -436,29 +389,14 @@ export default function DocumentTypesList() {
|
||||
name="ruleType"
|
||||
value={searchParams.get('ruleType') || ''}
|
||||
options={[
|
||||
...(ruleTypes || []).map(type => ({
|
||||
value: type.id,
|
||||
label: type.name
|
||||
...(parentGroups || []).map(group => ({
|
||||
value: group.id,
|
||||
label: group.name
|
||||
}))
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
className="mr-3 w-[20%]"
|
||||
/>
|
||||
|
||||
<FilterSelect
|
||||
label="所属子级评查分组"
|
||||
name="groupId"
|
||||
value={searchParams.get('groupId') || ''}
|
||||
options={[
|
||||
...(isRuleGroupSelectDisabled ? [{ value: "", label: "请先选择评查点类型" }] : []),
|
||||
...ruleGroups.map(group => ({
|
||||
value: group.id,
|
||||
label: group.name
|
||||
}))
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
className={`mr-3 w-[20%] ${isRuleGroupSelectDisabled ? 'opacity-50' : ''}`}
|
||||
/>
|
||||
|
||||
<SearchFilter
|
||||
label="类型名称"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import documentTypesNewStyles from "~/styles/pages/document-types_new.css?url";
|
||||
import { getAllRuleGroups, type RuleGroup } from "~/api/evaluation_points/rule-groups";
|
||||
import { getDocumentType, createDocumentType, updateDocumentType } from "~/api/document-types/document-types";
|
||||
import { getDocumentType, createDocumentType, updateDocumentType, getEntryModules } from "~/api/document-types/document-types";
|
||||
import { getPromptTemplates, type PromptTemplateUI } from "~/api/prompts/prompts";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
|
||||
@@ -81,8 +81,15 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const groupsTree = ruleGroupsResponse.error ? [] : ruleGroupsResponse.data || [];
|
||||
|
||||
|
||||
// 2. 获取各类型的提示词模板
|
||||
const [llmExtractionTemplatesResponse, vlmExtractionTemplatesResponse, evaluationTemplatesResponse, summaryTemplatesResponse] =
|
||||
// 2. 获取入口模块列表
|
||||
const entryModulesResponse = await getEntryModules(frontendJWT);
|
||||
if (entryModulesResponse.error) {
|
||||
console.error("获取入口模块失败:", entryModulesResponse.error);
|
||||
}
|
||||
const entryModules = entryModulesResponse.error ? [] : (entryModulesResponse.data || []);
|
||||
|
||||
// 3. 获取各类型的提示词模板
|
||||
const [llmExtractionTemplatesResponse, vlmExtractionTemplatesResponse, evaluationTemplatesResponse, summaryTemplatesResponse] =
|
||||
await Promise.all([
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.LLM_EXTRACTION }, frontendJWT),
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.VLM_EXTRACTION }, frontendJWT),
|
||||
@@ -103,6 +110,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
isEdit,
|
||||
documentType,
|
||||
ruleGroups: groupsTree,
|
||||
entryModules,
|
||||
llmExtractionTemplates: llmExtractionTemplatesResponse.data?.templates || [],
|
||||
vlmExtractionTemplates: vlmExtractionTemplatesResponse.data?.templates || [],
|
||||
evaluationTemplates: evaluationTemplatesResponse.data?.templates || [],
|
||||
@@ -114,6 +122,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
isEdit: false,
|
||||
documentType: undefined,
|
||||
ruleGroups: [],
|
||||
entryModules: [],
|
||||
llmExtractionTemplates: [],
|
||||
vlmExtractionTemplates: [],
|
||||
evaluationTemplates: [],
|
||||
@@ -132,6 +141,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
const id = formData.get("id") as string | null;
|
||||
const name = formData.get("name") as string;
|
||||
const description = formData.get("description") as string;
|
||||
const entryModuleId = formData.get("entry_module_id") as string;
|
||||
const llmExtractionTemplateId = formData.get("llm_extraction_template") as string;
|
||||
const vlmExtractionTemplateId = formData.get("vlm_extraction_template") as string;
|
||||
const evaluationTemplateId = formData.get("evaluation_template") as string;
|
||||
@@ -179,6 +189,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
name,
|
||||
description,
|
||||
group_ids: selectedGroups,
|
||||
entry_module_id: entryModuleId ? parseInt(entryModuleId) : null,
|
||||
// 确保映射关系与prompt_config字段对应正确
|
||||
llm_extraction_template_id: llmExtractionTemplateId ? parseInt(llmExtractionTemplateId) : null,
|
||||
vlm_extraction_template_id: vlmExtractionTemplateId ? parseInt(vlmExtractionTemplateId) : null,
|
||||
@@ -222,9 +233,10 @@ export default function DocumentTypeNew() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const isEditMode = searchParams.has("id");
|
||||
|
||||
const {
|
||||
documentType,
|
||||
ruleGroups,
|
||||
const {
|
||||
documentType,
|
||||
ruleGroups,
|
||||
entryModules,
|
||||
llmExtractionTemplates,
|
||||
vlmExtractionTemplates,
|
||||
evaluationTemplates,
|
||||
@@ -244,6 +256,7 @@ export default function DocumentTypeNew() {
|
||||
id: documentType?.id || "",
|
||||
name: documentType?.name || "",
|
||||
description: documentType?.description || "",
|
||||
entryModuleId: documentType?.entry_module?.id?.toString() || "",
|
||||
llmExtractionTemplateId: documentType?.llm_extraction_template_id || "",
|
||||
vlmExtractionTemplateId: documentType?.vlm_extraction_template_id || "",
|
||||
evaluationTemplateId: documentType?.evaluation_template_id || "",
|
||||
@@ -287,6 +300,7 @@ export default function DocumentTypeNew() {
|
||||
id: documentType.id,
|
||||
name: documentType.name,
|
||||
description: documentType.description,
|
||||
entryModuleId: documentType.entry_module?.id?.toString() || "",
|
||||
llmExtractionTemplateId: documentType.llm_extraction_template_id || "",
|
||||
vlmExtractionTemplateId: documentType.vlm_extraction_template_id || "",
|
||||
evaluationTemplateId: documentType.evaluation_template_id || "",
|
||||
@@ -510,7 +524,30 @@ export default function DocumentTypeNew() {
|
||||
)}
|
||||
<div className="form-tip">例如:销售合同、采购合同、专卖许可证等</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 入口模块 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="entry-module" className="form-label">
|
||||
入口模块
|
||||
</label>
|
||||
<select
|
||||
id="entry-module"
|
||||
name="entry_module_id"
|
||||
className="form-select"
|
||||
value={formData.entryModuleId}
|
||||
onChange={handleInputChange}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<option value="">无</option>
|
||||
{entryModules.map((module: { id: number; name: string }) => (
|
||||
<option key={module.id} value={module.id}>
|
||||
{module.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="form-tip">选择此文档类型对应的入口模块(可选)</div>
|
||||
</div>
|
||||
|
||||
{/* 类型描述 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="type-description" className="form-label">类型描述</label>
|
||||
@@ -670,30 +707,19 @@ export default function DocumentTypeNew() {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 子分组 */}
|
||||
{/* 子分组 - 仅展示,不可选 */}
|
||||
{group.children && group.children.length > 0 && expandedGroups[group.id] && (
|
||||
group.children.map((child: RuleGroup) => (
|
||||
<div
|
||||
key={child.id}
|
||||
className={`checkbox-item child-checkbox-item ${formData.selectedGroups.includes(child.id) ? 'checked' : ''}`}
|
||||
className="checkbox-item child-checkbox-item"
|
||||
style={{ paddingLeft: '2.5rem', opacity: 0.9 }}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id={`group-${child.id}`}
|
||||
name="checkpoint_group_ids"
|
||||
value={child.id}
|
||||
checked={formData.selectedGroups.includes(child.id)}
|
||||
onChange={(e) => handleGroupCheckChange(child.id, e.target.checked)}
|
||||
className="radio-input"
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`group-${child.id}`}
|
||||
className="checkbox-label"
|
||||
>
|
||||
<i className="ri-subtract-line" style={{ marginRight: '8px', color: '#9ca3af' }}></i>
|
||||
<span className="checkbox-label">
|
||||
{child.name}
|
||||
<span className="group-badge child-badge">二级分组</span>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { postgrestGet } from "~/api/postgrest-client";
|
||||
import { getUserSession } from "~/api/login/auth.server";
|
||||
|
||||
/**
|
||||
* 文档下载路由 - 处理文档下载请求
|
||||
* 通过重定向到带有授权的连接来允许下载文件
|
||||
*/
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
// 获取文件路径参数
|
||||
const url = new URL(request.url);
|
||||
const filePath = url.searchParams.get("path");
|
||||
|
||||
if (!filePath) {
|
||||
return new Response("缺少文件路径参数", { status: 400 });
|
||||
}
|
||||
|
||||
// 调用Minio API获取带有授权的预签名URL
|
||||
// 这里假设后端有一个生成预签名URL的API
|
||||
const response = await postgrestGet<{ presignedUrl: string }>(
|
||||
'/minio/presign',
|
||||
{
|
||||
filter: {
|
||||
'object_path': `eq.${filePath}`,
|
||||
'expires_in': 'eq.300' // 5分钟有效期
|
||||
},
|
||||
token: frontendJWT
|
||||
}
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
console.error("获取文件下载链接失败:", response.error);
|
||||
return new Response("获取文件下载链接失败", { status: 500 });
|
||||
}
|
||||
|
||||
if (!response.data?.presignedUrl) {
|
||||
return new Response("无法获取文件下载链接", { status: 404 });
|
||||
}
|
||||
|
||||
// 重定向到预签名URL,这样浏览器就能直接下载文件
|
||||
return Response.redirect(response.data.presignedUrl);
|
||||
} catch (error) {
|
||||
console.error("文件下载处理失败:", error);
|
||||
return new Response(
|
||||
"文件下载处理失败: " + (error instanceof Error ? error.message : "未知错误"),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { ClientLoaderFunctionArgs, ClientActionFunctionArgs } from "@remix-run/react";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import {
|
||||
getRoles,
|
||||
getRoutes,
|
||||
getRoleRoutePermissions,
|
||||
updateRoleRoutePermissions,
|
||||
getRoleUsers,
|
||||
getAllUsers,
|
||||
assignUserRoles,
|
||||
createRole,
|
||||
updateRole,
|
||||
deleteRole,
|
||||
type RoleInfo,
|
||||
type RouteInfo,
|
||||
type UserInfo
|
||||
} from "~/api/role-permissions/role-permissions";
|
||||
import rolePermissionsStyles from "~/styles/pages/role-permissions.css?url";
|
||||
|
||||
// 引入样式
|
||||
export function links() {
|
||||
return [
|
||||
{ rel: "stylesheet", href: rolePermissionsStyles }
|
||||
];
|
||||
}
|
||||
|
||||
// 页面元数据
|
||||
export const meta = () => {
|
||||
return [
|
||||
{ title: "角色权限管理 - 中国烟草AI合同及卷宗审核系统" },
|
||||
{ name: "description", content: "管理系统角色和权限分配" }
|
||||
];
|
||||
};
|
||||
|
||||
// ClientLoader - 加载初始数据
|
||||
export async function clientLoader({ request }: ClientLoaderFunctionArgs) {
|
||||
try {
|
||||
const [roles, routes, users] = await Promise.all([
|
||||
getRoles(),
|
||||
getRoutes(),
|
||||
getAllUsers()
|
||||
]);
|
||||
|
||||
return {
|
||||
roles,
|
||||
routes,
|
||||
users
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("加载数据失败:", error);
|
||||
return {
|
||||
roles: [],
|
||||
routes: [],
|
||||
users: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ClientAction - 处理用户操作
|
||||
export async function clientAction({ request }: ClientActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const action = formData.get("action") as string;
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case "updatePermissions": {
|
||||
const roleId = parseInt(formData.get("roleId") as string);
|
||||
const routeIds = JSON.parse(formData.get("routeIds") as string);
|
||||
const result = await updateRoleRoutePermissions(roleId, routeIds);
|
||||
return result;
|
||||
}
|
||||
|
||||
case "assignUserRoles": {
|
||||
const userId = parseInt(formData.get("userId") as string);
|
||||
const roleIds = JSON.parse(formData.get("roleIds") as string);
|
||||
const result = await assignUserRoles(userId, roleIds);
|
||||
return result;
|
||||
}
|
||||
|
||||
case "createRole": {
|
||||
const roleData = JSON.parse(formData.get("roleData") as string);
|
||||
const result = await createRole(roleData);
|
||||
return result;
|
||||
}
|
||||
|
||||
case "updateRole": {
|
||||
const roleId = parseInt(formData.get("roleId") as string);
|
||||
const roleData = JSON.parse(formData.get("roleData") as string);
|
||||
const result = await updateRole(roleId, roleData);
|
||||
return result;
|
||||
}
|
||||
|
||||
case "deleteRole": {
|
||||
const roleId = parseInt(formData.get("roleId") as string);
|
||||
const result = await deleteRole(roleId);
|
||||
return result;
|
||||
}
|
||||
|
||||
default:
|
||||
return { success: false, message: "未知操作" };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("操作失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : "操作失败"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 主组件
|
||||
export default function RolePermissions() {
|
||||
const [roles, setRoles] = useState<RoleInfo[]>([]);
|
||||
const [routes, setRoutes] = useState<RouteInfo[]>([]);
|
||||
const [users, setUsers] = useState<UserInfo[]>([]);
|
||||
const [selectedRole, setSelectedRole] = useState<RoleInfo | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<'permissions' | 'users'>('permissions');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 路由权限相关状态
|
||||
const [selectedRouteIds, setSelectedRouteIds] = useState<number[]>([]);
|
||||
const [roleUsers, setRoleUsers] = useState<UserInfo[]>([]);
|
||||
|
||||
// 加载初始数据
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [rolesData, routesData, usersData] = await Promise.all([
|
||||
getRoles(),
|
||||
getRoutes(),
|
||||
getAllUsers()
|
||||
]);
|
||||
|
||||
setRoles(rolesData);
|
||||
setRoutes(routesData);
|
||||
setUsers(usersData);
|
||||
|
||||
// 默认选中第一个角色
|
||||
if (rolesData.length > 0) {
|
||||
handleSelectRole(rolesData[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载数据失败:", error);
|
||||
toastService.error("加载数据失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 选择角色
|
||||
const handleSelectRole = async (role: RoleInfo) => {
|
||||
setSelectedRole(role);
|
||||
|
||||
// 加载该角色的权限
|
||||
const permissions = await getRoleRoutePermissions(role.id);
|
||||
const routeIds = permissions.map(p => p.route_id);
|
||||
setSelectedRouteIds(routeIds);
|
||||
|
||||
// 加载该角色的用户列表
|
||||
const users = await getRoleUsers(role.id);
|
||||
setRoleUsers(users);
|
||||
};
|
||||
|
||||
// 递归获取所有路由ID(包括子路由)
|
||||
const getAllRouteIds = (routes: RouteInfo[]): number[] => {
|
||||
let ids: number[] = [];
|
||||
routes.forEach(route => {
|
||||
ids.push(route.id);
|
||||
if (route.children && route.children.length > 0) {
|
||||
ids = ids.concat(getAllRouteIds(route.children));
|
||||
}
|
||||
});
|
||||
return ids;
|
||||
};
|
||||
|
||||
// 切换路由权限
|
||||
const handleToggleRoute = (routeId: number, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedRouteIds([...selectedRouteIds, routeId]);
|
||||
} else {
|
||||
setSelectedRouteIds(selectedRouteIds.filter(id => id !== routeId));
|
||||
}
|
||||
};
|
||||
|
||||
// 切换父路由(包括所有子路由)
|
||||
const handleToggleParentRoute = (route: RouteInfo, checked: boolean) => {
|
||||
const childIds = route.children ? getAllRouteIds(route.children) : [];
|
||||
const allIds = [route.id, ...childIds];
|
||||
|
||||
if (checked) {
|
||||
const newIds = [...selectedRouteIds, ...allIds].filter(
|
||||
(id, index, self) => self.indexOf(id) === index
|
||||
);
|
||||
setSelectedRouteIds(newIds);
|
||||
} else {
|
||||
setSelectedRouteIds(
|
||||
selectedRouteIds.filter(id => !allIds.includes(id))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存权限
|
||||
const handleSavePermissions = async () => {
|
||||
if (!selectedRole) return;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("action", "updatePermissions");
|
||||
formData.append("roleId", selectedRole.id.toString());
|
||||
formData.append("routeIds", JSON.stringify(selectedRouteIds));
|
||||
|
||||
const response = await fetch("/role-permissions", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
toastService.success(result.message);
|
||||
} else {
|
||||
toastService.error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存权限失败:", error);
|
||||
toastService.error("保存权限失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染路由树
|
||||
const renderRouteTree = (routes: RouteInfo[], level = 0) => {
|
||||
return routes.map(route => {
|
||||
const hasChildren = route.children && route.children.length > 0;
|
||||
const isChecked = selectedRouteIds.includes(route.id);
|
||||
const allChildIds = hasChildren ? getAllRouteIds(route.children!) : [];
|
||||
const checkedChildCount = allChildIds.filter(id =>
|
||||
selectedRouteIds.includes(id)
|
||||
).length;
|
||||
const isIndeterminate =
|
||||
hasChildren && checkedChildCount > 0 && checkedChildCount < allChildIds.length;
|
||||
|
||||
return (
|
||||
<div key={route.id} className="route-item" style={{ paddingLeft: `${level * 20}px` }}>
|
||||
<div className="route-item-content">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`route-${route.id}`}
|
||||
checked={isChecked}
|
||||
ref={el => {
|
||||
if (el) el.indeterminate = isIndeterminate;
|
||||
}}
|
||||
onChange={(e) => {
|
||||
if (hasChildren) {
|
||||
handleToggleParentRoute(route, e.target.checked);
|
||||
} else {
|
||||
handleToggleRoute(route.id, e.target.checked);
|
||||
}
|
||||
}}
|
||||
className="route-checkbox"
|
||||
/>
|
||||
<label htmlFor={`route-${route.id}`} className="route-label">
|
||||
{route.icon && <i className={`${route.icon} route-icon`}></i>}
|
||||
<span className="route-title">{route.route_title}</span>
|
||||
<span className="route-path">{route.route_path}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{hasChildren && (
|
||||
<div className="route-children">
|
||||
{renderRouteTree(route.children!, level + 1)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="role-permissions-page">
|
||||
<div className="loading-container">
|
||||
<i className="ri-loader-4-line spin"></i>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="role-permissions-page">
|
||||
{/* 页面头部 */}
|
||||
<div className="page-header">
|
||||
<h2 className="page-title">
|
||||
<i className="ri-shield-user-line"></i>
|
||||
角色权限管理
|
||||
</h2>
|
||||
<div className="page-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-add-line"
|
||||
onClick={() => {
|
||||
toastService.info("创建角色功能开发中...");
|
||||
}}
|
||||
>
|
||||
新建角色
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="permissions-container">
|
||||
{/* 左侧:角色列表 */}
|
||||
<Card className="roles-panel" title="角色列表" bodyClassName="p-0">
|
||||
<div className="roles-list">
|
||||
{roles.map(role => (
|
||||
<div
|
||||
key={role.id}
|
||||
className={`role-item ${selectedRole?.id === role.id ? 'active' : ''}`}
|
||||
onClick={() => handleSelectRole(role)}
|
||||
>
|
||||
<div className="role-info">
|
||||
<div className="role-header">
|
||||
<span className="role-name">{role.role_name}</span>
|
||||
{role.is_system_role && (
|
||||
<span className="system-badge">系统角色</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="role-key">{role.role_key}</div>
|
||||
<div className="role-desc">{role.description}</div>
|
||||
<div className="role-meta">
|
||||
<span className="data-scope">
|
||||
<i className="ri-database-line"></i>
|
||||
{role.data_scope}
|
||||
</span>
|
||||
<span className="priority">
|
||||
<i className="ri-sort-asc"></i>
|
||||
优先级: {role.priority}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{!role.is_system_role && (
|
||||
<div className="role-actions">
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toastService.info("编辑角色功能开发中...");
|
||||
}}
|
||||
title="编辑"
|
||||
>
|
||||
<i className="ri-edit-line"></i>
|
||||
</button>
|
||||
<button
|
||||
className="btn-icon text-error"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (confirm(`确定要删除角色"${role.role_name}"吗?`)) {
|
||||
toastService.info("删除角色功能开发中...");
|
||||
}
|
||||
}}
|
||||
title="删除"
|
||||
>
|
||||
<i className="ri-delete-bin-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 右侧:角色详情和权限设置 */}
|
||||
<div className="permissions-detail">
|
||||
{selectedRole ? (
|
||||
<>
|
||||
{/* Tab 切换 */}
|
||||
<Card className="tabs-card">
|
||||
<div className="tabs-header">
|
||||
<button
|
||||
className={`tab-btn ${activeTab === 'permissions' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('permissions')}
|
||||
>
|
||||
<i className="ri-shield-check-line"></i>
|
||||
路由权限
|
||||
</button>
|
||||
<button
|
||||
className={`tab-btn ${activeTab === 'users' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('users')}
|
||||
>
|
||||
<i className="ri-team-line"></i>
|
||||
用户列表 ({roleUsers.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="tabs-content">
|
||||
{/* 路由权限Tab */}
|
||||
{activeTab === 'permissions' && (
|
||||
<div className="permissions-tab">
|
||||
<div className="permissions-header">
|
||||
<h3>为角色 "{selectedRole.role_name}" 分配路由权限</h3>
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-save-line"
|
||||
onClick={handleSavePermissions}
|
||||
>
|
||||
保存权限
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="routes-tree">
|
||||
{renderRouteTree(routes)}
|
||||
</div>
|
||||
|
||||
<div className="permissions-summary">
|
||||
<i className="ri-information-line"></i>
|
||||
已选择 <strong>{selectedRouteIds.length}</strong> 个路由权限
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 用户列表Tab */}
|
||||
{activeTab === 'users' && (
|
||||
<div className="users-tab">
|
||||
<div className="users-header">
|
||||
<h3>拥有角色 "{selectedRole.role_name}" 的用户</h3>
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-user-add-line"
|
||||
onClick={() => {
|
||||
toastService.info("分配用户功能开发中...");
|
||||
}}
|
||||
>
|
||||
分配用户
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="users-list">
|
||||
{roleUsers.length > 0 ? (
|
||||
roleUsers.map(user => (
|
||||
<div key={user.id} className="user-card">
|
||||
<div className="user-avatar">
|
||||
<i className="ri-user-line"></i>
|
||||
</div>
|
||||
<div className="user-info">
|
||||
<div className="user-name">
|
||||
{user.nick_name}
|
||||
{user.is_leader && (
|
||||
<span className="leader-badge">负责人</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="user-username">@{user.username}</div>
|
||||
<div className="user-org">{user.ou_name}</div>
|
||||
<div className="user-contact">
|
||||
{user.phone_number && (
|
||||
<span>
|
||||
<i className="ri-phone-line"></i>
|
||||
{user.phone_number}
|
||||
</span>
|
||||
)}
|
||||
{user.email && (
|
||||
<span>
|
||||
<i className="ri-mail-line"></i>
|
||||
{user.email}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="user-actions">
|
||||
<button
|
||||
className="btn-icon text-error"
|
||||
onClick={() => {
|
||||
if (confirm(`确定要移除用户"${user.nick_name}"的该角色吗?`)) {
|
||||
toastService.info("移除角色功能开发中...");
|
||||
}
|
||||
}}
|
||||
title="移除角色"
|
||||
>
|
||||
<i className="ri-user-unfollow-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="empty-state">
|
||||
<i className="ri-user-line"></i>
|
||||
<p>暂无用户拥有此角色</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
) : (
|
||||
<Card>
|
||||
<div className="empty-state">
|
||||
<i className="ri-shield-line"></i>
|
||||
<p>请选择一个角色查看详情</p>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+139
-141
@@ -1,6 +1,7 @@
|
||||
import { CHAT_CONFIG, ContentType, SSE_TIMEOUT } from '../config/chat';
|
||||
import type { Feedbacktype, ThoughtItem, VisionFile, MessageEnd, MessageReplace } from '../types/dify_chat';
|
||||
import { unicodeToChar } from '../utils/chat-utils';
|
||||
import axios from 'axios';
|
||||
|
||||
// 基础请求选项
|
||||
// 注意:客户端调用Remix API routes,不需要手动添加Authorization
|
||||
@@ -321,7 +322,7 @@ const handleStream = (
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
const baseFetch = (url: string, fetchOptions: any, needAllResponseContent: boolean = false) => {
|
||||
const baseFetch = async (url: string, fetchOptions: any, needAllResponseContent: boolean = false) => {
|
||||
const options = Object.assign({}, baseOptions, fetchOptions);
|
||||
|
||||
// 调用Remix API routes(如 /api/conversations)
|
||||
@@ -329,51 +330,46 @@ const baseFetch = (url: string, fetchOptions: any, needAllResponseContent: boole
|
||||
const urlWithPrefix = `${CHAT_CONFIG.API_URL}/${url.replace(/^\//, '')}`;
|
||||
|
||||
const { body } = options;
|
||||
let data = body;
|
||||
if (body && typeof body === 'object') {
|
||||
// 不再添加user参数,服务端会从JWT自动提取
|
||||
options.body = JSON.stringify(body);
|
||||
data = body;
|
||||
}
|
||||
|
||||
return fetch(urlWithPrefix, options)
|
||||
.then((res: Response) => {
|
||||
if (!res.ok) {
|
||||
console.error('❌ Request failed:', {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
url: urlWithPrefix
|
||||
});
|
||||
if (res.status === 422) {
|
||||
return res.text().then(text => {
|
||||
let errorMessage = text;
|
||||
try {
|
||||
const data = JSON.parse(text);
|
||||
errorMessage = data.message || data.error || text;
|
||||
} catch (e) {
|
||||
// 如果不是JSON,使用原始文本
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
});
|
||||
}
|
||||
throw new Error(`${res.status}: ${res.statusText}`);
|
||||
}
|
||||
|
||||
if (needAllResponseContent) {
|
||||
return res.text().then(text => {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const data = res.json();
|
||||
return data;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('❌ Request error:', err.message);
|
||||
throw err;
|
||||
try {
|
||||
const response = await axios({
|
||||
url: urlWithPrefix,
|
||||
method: options.method || 'GET',
|
||||
data: data,
|
||||
headers: options.headers,
|
||||
withCredentials: true, // 等同于 credentials: 'include'
|
||||
});
|
||||
|
||||
if (needAllResponseContent) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err)) {
|
||||
console.error('❌ Request failed:', {
|
||||
status: err.response?.status,
|
||||
statusText: err.response?.statusText,
|
||||
url: urlWithPrefix
|
||||
});
|
||||
|
||||
if (err.response?.status === 422) {
|
||||
const errorData = err.response.data;
|
||||
const errorMessage = errorData?.message || errorData?.error || JSON.stringify(errorData);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
throw new Error(`${err.response?.status || 500}: ${err.response?.statusText || err.message}`);
|
||||
}
|
||||
|
||||
console.error('❌ Request error:', (err as Error).message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -532,25 +528,25 @@ export const fetchConversations = async () => {
|
||||
const url = `${CHAT_CONFIG.API_URL}/conversations?${params}`;
|
||||
console.log('📋 [API Client] 获取会话列表:', { url, apiUrl: CHAT_CONFIG.API_URL });
|
||||
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
credentials: 'include', // 携带cookie
|
||||
}).then(res => {
|
||||
console.log('📋 [API Client] 会话列表响应:', { status: res.status, ok: res.ok });
|
||||
if (!res.ok) {
|
||||
return res.text().then(text => {
|
||||
console.error('❌ [API Client] 获取会话列表失败:', { status: res.status, body: text });
|
||||
throw new Error(`Failed to fetch conversations: ${res.status} - ${text}`);
|
||||
});
|
||||
}
|
||||
return res.json().then(data => {
|
||||
console.log('📋 [API Client] 会话列表数据:', data);
|
||||
return data;
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
withCredentials: true, // 携带cookie
|
||||
});
|
||||
}).catch(err => {
|
||||
|
||||
console.log('📋 [API Client] 会话列表响应:', { status: response.status });
|
||||
console.log('📋 [API Client] 会话列表数据:', response.data);
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err)) {
|
||||
console.error('❌ [API Client] 获取会话列表失败:', {
|
||||
status: err.response?.status,
|
||||
body: err.response?.data
|
||||
});
|
||||
throw new Error(`Failed to fetch conversations: ${err.response?.status} - ${JSON.stringify(err.response?.data)}`);
|
||||
}
|
||||
console.error('❌ [API Client] 会话列表请求异常:', err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -582,15 +578,17 @@ export const fetchChatList = async (conversationId: string) => {
|
||||
// 不再传递user参数,服务端会从JWT自动提取
|
||||
});
|
||||
|
||||
return fetch(`${CHAT_CONFIG.API_URL}/messages?${params}`, {
|
||||
method: 'GET',
|
||||
credentials: 'include', // 携带cookie
|
||||
}).then(res => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch chat list: ${res.status}`);
|
||||
try {
|
||||
const response = await axios.get(`${CHAT_CONFIG.API_URL}/messages?${params}`, {
|
||||
withCredentials: true, // 携带cookie
|
||||
});
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err)) {
|
||||
throw new Error(`Failed to fetch chat list: ${err.response?.status}`);
|
||||
}
|
||||
return res.json();
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -620,25 +618,24 @@ export const fetchAppParams = async () => {
|
||||
const url = `${CHAT_CONFIG.API_URL}/parameters`;
|
||||
console.log('⚙️ [API Client] 获取应用参数:', { url, apiUrl: CHAT_CONFIG.API_URL });
|
||||
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
credentials: 'include', // 携带cookie
|
||||
}).then(res => {
|
||||
console.log('⚙️ [API Client] 应用参数响应:', { status: res.status, ok: res.ok });
|
||||
if (!res.ok) {
|
||||
return res.text().then(text => {
|
||||
console.error('❌ [API Client] 获取应用参数失败:', { status: res.status, body: text });
|
||||
throw new Error(`Failed to fetch app params: ${res.status} - ${text}`);
|
||||
});
|
||||
}
|
||||
return res.json().then(data => {
|
||||
console.log('⚙️ [API Client] 应用参数数据:', data);
|
||||
return data;
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
withCredentials: true, // 携带cookie
|
||||
});
|
||||
}).catch(err => {
|
||||
console.log('⚙️ [API Client] 应用参数响应:', { status: response.status });
|
||||
console.log('⚙️ [API Client] 应用参数数据:', response.data);
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err)) {
|
||||
console.error('❌ [API Client] 获取应用参数失败:', {
|
||||
status: err.response?.status,
|
||||
body: err.response?.data
|
||||
});
|
||||
throw new Error(`Failed to fetch app params: ${err.response?.status} - ${JSON.stringify(err.response?.data)}`);
|
||||
}
|
||||
console.error('❌ [API Client] 应用参数请求异常:', err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -669,19 +666,20 @@ export const fetchAppParams = async () => {
|
||||
export const updateFeedback = async ({ url, body }: { url: string; body: Feedbacktype }) => {
|
||||
const messageId = url.split('/').pop(); // 从URL中提取messageId
|
||||
|
||||
return fetch(`${CHAT_CONFIG.API_URL}/messages/${messageId}/feedbacks`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include', // 携带cookie
|
||||
body: JSON.stringify(body), // 不再添加user参数
|
||||
}).then(res => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to update feedback: ${res.status}`);
|
||||
try {
|
||||
const response = await axios.post(`${CHAT_CONFIG.API_URL}/messages/${messageId}/feedbacks`, body, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
withCredentials: true, // 携带cookie
|
||||
});
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err)) {
|
||||
throw new Error(`Failed to update feedback: ${err.response?.status}`);
|
||||
}
|
||||
return res.json();
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -705,22 +703,23 @@ export const updateFeedback = async ({ url, body }: { url: string; body: Feedbac
|
||||
* ```
|
||||
*/
|
||||
export const generateConversationName = async (id: string) => {
|
||||
return fetch(`${CHAT_CONFIG.API_URL}/conversations/${id}/name`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include', // 携带cookie
|
||||
body: JSON.stringify({
|
||||
try {
|
||||
const response = await axios.post(`${CHAT_CONFIG.API_URL}/conversations/${id}/name`, {
|
||||
auto_generate: true,
|
||||
// 不再添加user参数
|
||||
}),
|
||||
}).then(res => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to generate conversation name: ${res.status}`);
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
withCredentials: true, // 携带cookie
|
||||
});
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err)) {
|
||||
throw new Error(`Failed to generate conversation name: ${err.response?.status}`);
|
||||
}
|
||||
return res.json();
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -749,23 +748,24 @@ export const generateConversationName = async (id: string) => {
|
||||
* ```
|
||||
*/
|
||||
export const renameConversation = async (id: string, name: string, autoGenerate: boolean = false) => {
|
||||
return fetch(`${CHAT_CONFIG.API_URL}/conversations/${id}/name`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include', // 携带cookie
|
||||
body: JSON.stringify({
|
||||
try {
|
||||
const response = await axios.post(`${CHAT_CONFIG.API_URL}/conversations/${id}/name`, {
|
||||
name: autoGenerate ? undefined : name,
|
||||
auto_generate: autoGenerate,
|
||||
// 不再添加user参数
|
||||
}),
|
||||
}).then(res => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to rename conversation: ${res.status}`);
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
withCredentials: true, // 携带cookie
|
||||
});
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err)) {
|
||||
throw new Error(`Failed to rename conversation: ${err.response?.status}`);
|
||||
}
|
||||
return res.json();
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -790,31 +790,29 @@ export const renameConversation = async (id: string, name: string, autoGenerate:
|
||||
export const deleteConversation = async (id: string) => {
|
||||
console.log('🗑️ [API Client] 删除会话:', id);
|
||||
|
||||
return fetch(`${CHAT_CONFIG.API_URL}/conversations/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include', // 携带cookie
|
||||
// 不再发送body和user参数
|
||||
}).then(async res => {
|
||||
console.log('🗑️ [API Client] 删除会话响应:', {
|
||||
status: res.status,
|
||||
ok: res.ok,
|
||||
statusText: res.statusText
|
||||
try {
|
||||
const response = await axios.delete(`${CHAT_CONFIG.API_URL}/conversations/${id}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
withCredentials: true, // 携带cookie
|
||||
// 不再发送body和user参数
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
// 尝试读取错误详情
|
||||
const errorText = await res.text();
|
||||
console.error('❌ [API Client] 删除会话失败详情:', errorText);
|
||||
throw new Error(`Failed to delete conversation: ${res.status}`);
|
||||
}
|
||||
console.log('🗑️ [API Client] 删除会话响应:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
console.log('🗑️ [API Client] 删除会话数据:', data);
|
||||
return data;
|
||||
});
|
||||
console.log('🗑️ [API Client] 删除会话数据:', response.data);
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err)) {
|
||||
console.error('❌ [API Client] 删除会话失败详情:', err.response?.data);
|
||||
throw new Error(`Failed to delete conversation: ${err.response?.status}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
}
|
||||
|
||||
.document-type-new-page .child-badge {
|
||||
@apply bg-[rgba(0,104,1,0.61)] text-white;
|
||||
@apply bg-[rgba(0,104,1,0.71)] text-white;
|
||||
}
|
||||
|
||||
/* 添加checkbox-input样式,使用视觉上更美观的自定义复选框样式 */
|
||||
|
||||
@@ -0,0 +1,552 @@
|
||||
/* 角色权限管理页面样式 */
|
||||
|
||||
.role-permissions-page {
|
||||
padding: 20px;
|
||||
background: #f5f7fa;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
/* 页面头部 */
|
||||
.role-permissions-page .page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.role-permissions-page .page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.role-permissions-page .page-title i {
|
||||
font-size: 28px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.role-permissions-page .page-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 主容器布局 */
|
||||
.permissions-container {
|
||||
display: grid;
|
||||
grid-template-columns: 380px 1fr;
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* 左侧角色面板 */
|
||||
.roles-panel {
|
||||
height: calc(100vh - 140px);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.roles-list {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 角色列表项 */
|
||||
.role-item {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.role-item:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.role-item.active {
|
||||
background: var(--color-primary-light);
|
||||
border-left: 3px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.role-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.role-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.role-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.system-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
border: 1px solid #91d5ff;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.role-key {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-family: 'Courier New', monospace;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.role-desc {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.role-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.role-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.role-meta i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.role-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.role-item:hover .role-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: #e6e8eb;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.btn-icon.text-error:hover {
|
||||
background: #fef0f0;
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
/* 右侧详情面板 */
|
||||
.permissions-detail {
|
||||
min-height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
/* Tab 样式 */
|
||||
.tabs-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tabs-header {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #e4e7ed;
|
||||
background: #fafbfc;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 14px 24px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #606266;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tab-btn i {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: var(--color-primary);
|
||||
border-bottom-color: var(--color-primary);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.tabs-content {
|
||||
padding: 24px;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
/* 权限Tab */
|
||||
.permissions-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.permissions-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.permissions-header h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 路由树 */
|
||||
.routes-tree {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.route-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.route-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.route-item-content:hover {
|
||||
background: #e6e8eb;
|
||||
}
|
||||
|
||||
.route-checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.route-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.route-icon {
|
||||
font-size: 18px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.route-title {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.route-path {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.route-children {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.permissions-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: #ecf5ff;
|
||||
border: 1px solid #b3d8ff;
|
||||
border-radius: 6px;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.permissions-summary i {
|
||||
font-size: 18px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.permissions-summary strong {
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 用户Tab */
|
||||
.users-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.users-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.users-header h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.users-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 用户卡片 */
|
||||
.user-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.user-card:hover {
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--color-primary), #00a870);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-avatar i {
|
||||
font-size: 28px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.leader-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: #fff7e6;
|
||||
color: #fa8c16;
|
||||
border: 1px solid #ffd591;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.user-username {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-org {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.user-contact {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.user-contact span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.user-contact i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 64px;
|
||||
color: #dcdfe6;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
gap: 16px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.loading-container i {
|
||||
font-size: 48px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式布局 */
|
||||
@media (max-width: 1200px) {
|
||||
.permissions-container {
|
||||
grid-template-columns: 320px 1fr;
|
||||
}
|
||||
|
||||
.users-list {
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.permissions-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.roles-panel {
|
||||
height: auto;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.users-list {
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.role-permissions-page {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.role-permissions-page .page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tabs-header {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.users-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user