feat: 1. 完善起草合同页面的逻辑交互,对接minio的接口操作
This commit is contained in:
@@ -82,7 +82,7 @@ export async function getContractCategories(jwt?: string) {
|
||||
token: jwt
|
||||
};
|
||||
|
||||
const response = await postgrestGet<ContractCategory[]>('contract_categories', params);
|
||||
const response = await postgrestGet<ContractCategory[]>('/api/postgrest/proxy/contract_categories', params);
|
||||
|
||||
if (response.error) {
|
||||
return { error: response.error, status: response.status || 500 };
|
||||
@@ -107,7 +107,7 @@ export async function getContractCategories(jwt?: string) {
|
||||
export async function getContractCategoriesWithCount(jwt?: string) {
|
||||
try {
|
||||
// 获取所有分类
|
||||
const categoriesResponse = await postgrestGet<ContractCategory[]>('contract_categories', {
|
||||
const categoriesResponse = await postgrestGet<ContractCategory[]>('/api/postgrest/proxy/contract_categories', {
|
||||
select: '*',
|
||||
order: 'sort_order.asc,name.asc',
|
||||
token: jwt
|
||||
@@ -124,7 +124,7 @@ export async function getContractCategoriesWithCount(jwt?: string) {
|
||||
categories.map(async (category) => {
|
||||
try {
|
||||
// 简化方案:获取该分类下的所有模板ID,然后计算数量
|
||||
const countResponse = await postgrestGet<{ id: number }[]>('contract_templates', {
|
||||
const countResponse = await postgrestGet<{ id: number }[]>('/api/postgrest/proxy/contract_templates', {
|
||||
select: 'id',
|
||||
filter: { 'category_id': `eq.${category.id}` },
|
||||
token: jwt
|
||||
@@ -214,7 +214,7 @@ export async function getContractTemplates(searchParams: TemplateSearchParams =
|
||||
// 先查询匹配的分类ID
|
||||
let matchingCategoryIds: number[] = [];
|
||||
try {
|
||||
const categoryResponse = await postgrestGet<ContractCategory[]>('contract_categories', {
|
||||
const categoryResponse = await postgrestGet<ContractCategory[]>('/api/postgrest/proxy/contract_categories', {
|
||||
select: 'id',
|
||||
filter: { 'name': `ilike.*${cleanKeyword}*` },
|
||||
token
|
||||
@@ -245,7 +245,7 @@ export async function getContractTemplates(searchParams: TemplateSearchParams =
|
||||
|
||||
// 如果有分类名称,需要先获取分类ID
|
||||
if (category && !category_id) {
|
||||
const categoryResponse = await postgrestGet<ContractCategory[]>('contract_categories', {
|
||||
const categoryResponse = await postgrestGet<ContractCategory[]>('/api/postgrest/proxy/contract_categories', {
|
||||
select: 'id',
|
||||
filter: { 'name': `eq.${category}` },
|
||||
token
|
||||
@@ -264,7 +264,7 @@ export async function getContractTemplates(searchParams: TemplateSearchParams =
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
const response = await postgrestGet<ContractTemplate[]>('contract_templates', params);
|
||||
const response = await postgrestGet<ContractTemplate[]>('/api/postgrest/proxy/contract_templates', params);
|
||||
|
||||
if (response.error) {
|
||||
return { error: response.error, status: response.status || 500 };
|
||||
@@ -280,7 +280,7 @@ export async function getContractTemplates(searchParams: TemplateSearchParams =
|
||||
token
|
||||
};
|
||||
|
||||
const countResponse = await postgrestGet<{ id: number }[]>('contract_templates', countParams);
|
||||
const countResponse = await postgrestGet<{ id: number }[]>('/api/postgrest/proxy/contract_templates', countParams);
|
||||
let total = 0;
|
||||
if (!countResponse.error && countResponse.data) {
|
||||
const countData = extractApiData<{ id: number }[]>(countResponse.data) || [];
|
||||
@@ -318,7 +318,7 @@ export async function getContractTemplate(id: string | number, jwt?: string) {
|
||||
token: jwt
|
||||
};
|
||||
|
||||
const response = await postgrestGet<ContractTemplate[]>('contract_templates', params);
|
||||
const response = await postgrestGet<ContractTemplate[]>('/api/postgrest/proxy/contract_templates', params);
|
||||
|
||||
if (response.error) {
|
||||
return { error: response.error, status: response.status || 500 };
|
||||
@@ -355,7 +355,7 @@ export async function getFeaturedTemplates(limit: number = 6, jwt?: string) {
|
||||
token: jwt
|
||||
};
|
||||
|
||||
const response = await postgrestGet<ContractTemplate[]>('contract_templates', params);
|
||||
const response = await postgrestGet<ContractTemplate[]>('/api/postgrest/proxy/contract_templates', params);
|
||||
|
||||
if (response.error) {
|
||||
return { error: response.error, status: response.status || 500 };
|
||||
|
||||
@@ -80,134 +80,3 @@ export async function copyMinioFile(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建起草合同记录
|
||||
* @param request 创建请求
|
||||
* @param userId 用户ID
|
||||
* @param draftFilePath 可选:草稿文件路径(如果不提供,使用模板路径)
|
||||
* @returns 创建的记录
|
||||
*
|
||||
* 使用场景:
|
||||
* 1. 不传 draftFilePath:直接使用模板文件路径,在原模板上编辑
|
||||
* 2. 传 draftFilePath:使用复制后的文件路径(由文件复制接口提供)
|
||||
*/
|
||||
export async function createDraftContract(
|
||||
request: CreateDraftRequest,
|
||||
userId: number,
|
||||
draftFilePath?: string,
|
||||
jwt?: string
|
||||
): Promise<DraftedContract> {
|
||||
try {
|
||||
// 1. 查询模板信息
|
||||
const templateResponse = await postgrestGet('contract_templates', {
|
||||
select: 'id,file_path',
|
||||
filter: { id: `eq.${request.templateId}` },
|
||||
token: jwt
|
||||
});
|
||||
|
||||
if (!templateResponse.data || (Array.isArray(templateResponse.data) && templateResponse.data.length === 0)) {
|
||||
throw new Error('模板不存在');
|
||||
}
|
||||
|
||||
const template = Array.isArray(templateResponse.data) ? templateResponse.data[0] : templateResponse.data;
|
||||
|
||||
// 2. 确定使用的文件路径
|
||||
// 如果没有提供草稿路径,直接使用模板路径(适合直接编辑模板的场景)
|
||||
// 如果提供了草稿路径,使用复制后的文件路径
|
||||
const finalFilePath = draftFilePath || template.file_path;
|
||||
|
||||
console.log('[Draft Service] 创建草稿:', {
|
||||
templateId: request.templateId,
|
||||
templatePath: template.file_path,
|
||||
draftPath: draftFilePath,
|
||||
finalPath: finalFilePath,
|
||||
mode: draftFilePath ? '使用复制文件' : '直接使用模板文件'
|
||||
});
|
||||
|
||||
// 3. 创建草稿记录
|
||||
const insertResponse = await postgrestPost('drafted_contracts', {
|
||||
body: {
|
||||
template_id: request.templateId,
|
||||
file_path: finalFilePath,
|
||||
title: request.title,
|
||||
placeholder_values: {},
|
||||
status: 'draft',
|
||||
created_by: userId
|
||||
},
|
||||
select: '*',
|
||||
token: jwt
|
||||
});
|
||||
|
||||
if (!insertResponse.data) {
|
||||
throw new Error('创建草稿记录失败');
|
||||
}
|
||||
|
||||
const draft = Array.isArray(insertResponse.data) ? insertResponse.data[0] : insertResponse.data;
|
||||
return draft as DraftedContract;
|
||||
} catch (error) {
|
||||
console.error('[Draft Service] 创建草稿失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除草稿记录
|
||||
* @param draftId 草稿ID
|
||||
* @param userId 用户ID
|
||||
* @param jwt JWT token
|
||||
*/
|
||||
export async function deleteDraft(
|
||||
draftId: number,
|
||||
userId: number,
|
||||
jwt?: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = await postgrestDelete('drafted_contracts', {
|
||||
filter: {
|
||||
id: `eq.${draftId}`,
|
||||
created_by: `eq.${userId}` // 确保只能删除自己的草稿
|
||||
},
|
||||
token: jwt
|
||||
});
|
||||
|
||||
console.log('[Draft Service] 草稿已删除:', draftId);
|
||||
} catch (error) {
|
||||
console.error('[Draft Service] 删除草稿失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取草稿详情
|
||||
* @param draftId 草稿ID
|
||||
* @param userId 用户ID
|
||||
* @returns 草稿记录
|
||||
*/
|
||||
export async function getDraftById(
|
||||
draftId: number,
|
||||
userId: number,
|
||||
jwt?: string
|
||||
): Promise<DraftedContract | null> {
|
||||
try {
|
||||
const response = await postgrestGet('drafted_contracts', {
|
||||
select: '*',
|
||||
filter: {
|
||||
id: `eq.${draftId}`,
|
||||
created_by: `eq.${userId}`
|
||||
},
|
||||
token: jwt
|
||||
});
|
||||
|
||||
if (!response.data || (Array.isArray(response.data) && response.data.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const draft = Array.isArray(response.data) ? response.data[0] : response.data;
|
||||
return draft as DraftedContract;
|
||||
} catch (error) {
|
||||
console.error('[Draft Service] 获取草稿失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ async function safeGetJWT(jwtToken?: string): Promise<string> {
|
||||
*/
|
||||
export async function findIsProposer(taskId: string | number, userId: number | undefined, frontendJWT?: string): Promise<boolean> {
|
||||
// 通过postgrest的get请求去cross_examination_tasks表中进行查找assignee_id是否等于userId
|
||||
const response = await postgrestGet(`cross_examination_tasks`, {
|
||||
const response = await postgrestGet(`/api/postgrest/proxy/cross_examination_tasks`, {
|
||||
select: 'assigner_id',
|
||||
filter: {
|
||||
id: `eq.${taskId}`
|
||||
@@ -399,7 +399,7 @@ export async function confirmReviewResults(
|
||||
): Promise<{data?: unknown, error?: string, status?: number}> {
|
||||
try {
|
||||
// 通过postgrest的post请求去documents表中进行查找id等于documentId的数据,更新documents表的audit_status为1
|
||||
const response = await postgrestPut(`documents`, {
|
||||
const response = await postgrestPut(`/api/postgrest/proxy/documents`, {
|
||||
audit_status: 1
|
||||
}, {
|
||||
id: documentId
|
||||
|
||||
@@ -494,7 +494,7 @@ export async function updateDocumentAuditStatus(id: string, auditStatus: number,
|
||||
}
|
||||
|
||||
const response = await postgrestPut<TaskDocument, Partial<TaskDocument>>(
|
||||
'documents',
|
||||
'/api/postgrest/proxy/documents',
|
||||
{ audit_status: auditStatus },
|
||||
{
|
||||
id: parseInt(id)
|
||||
@@ -526,7 +526,7 @@ export async function getCrossCheckingDocumentTypes(jwtToken?: string): Promise<
|
||||
try {
|
||||
// console.log('[getCrossCheckingDocumentTypes] 开始获取交叉评查文档类型');
|
||||
|
||||
const response = await postgrestGet<DocumentType>('document_types',{
|
||||
const response = await postgrestGet<DocumentType>('/api/postgrest/proxy/document_types',{
|
||||
select: 'id,name,code,evaluation_point_groups_ids',
|
||||
filter: {
|
||||
evaluation_point_groups_ids: 'not.is.null'
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function verifyDocumentAccess(
|
||||
|
||||
try {
|
||||
// 1. 检查文档是否属于该任务(通过 cross_task_document_mapping 表)
|
||||
const documentMappingResponse = await postgrestGet('cross_task_document_mapping', {
|
||||
const documentMappingResponse = await postgrestGet('/api/postgrest/proxy/cross_task_document_mapping', {
|
||||
select: 'task_id,document_id',
|
||||
filter: {
|
||||
task_id: `eq.${taskId}`,
|
||||
@@ -66,7 +66,7 @@ export async function verifyDocumentAccess(
|
||||
}
|
||||
|
||||
// 2. 检查用户是否是该任务的参与者
|
||||
const taskResponse = await postgrestGet('cross_examination_tasks', {
|
||||
const taskResponse = await postgrestGet('/api/postgrest/proxy/cross_examination_tasks', {
|
||||
select: 'assigner_id,assignee_ids',
|
||||
filter: {
|
||||
id: `eq.${taskId}`
|
||||
|
||||
@@ -158,7 +158,7 @@ export async function getReviewPoints(fileId: string, request: Request) {
|
||||
limit: 1,
|
||||
token: frontendJWT
|
||||
};
|
||||
const contractStructureComparisonResponse = await postgrestGet('contract_structure_comparison', contractStructureComparisonParams);
|
||||
const contractStructureComparisonResponse = await postgrestGet('/api/postgrest/proxy/contract_structure_comparison', contractStructureComparisonParams);
|
||||
// console.log('contract_structure_comparison', contractStructureComparisonResponse)
|
||||
|
||||
if (contractStructureComparisonResponse.error) {
|
||||
@@ -201,7 +201,7 @@ export async function getReviewPoints(fileId: string, request: Request) {
|
||||
},
|
||||
token: frontendJWT
|
||||
};
|
||||
const evaluationResultsResponse = await postgrestGet('evaluation_results', evaluationResultsParams);
|
||||
const evaluationResultsResponse = await postgrestGet('/api/postgrest/proxy/evaluation_results', evaluationResultsParams);
|
||||
|
||||
// console.log('evaluationResultsResponse-------', evaluationResultsResponse,);
|
||||
if (evaluationResultsResponse.error) {
|
||||
@@ -230,7 +230,7 @@ export async function getReviewPoints(fileId: string, request: Request) {
|
||||
},
|
||||
token: frontendJWT
|
||||
};
|
||||
const evaluationPointsResponse = await postgrestGet('evaluation_points', evaluationPointsParams);
|
||||
const evaluationPointsResponse = await postgrestGet('/api/postgrest/proxy/evaluation_points', evaluationPointsParams);
|
||||
|
||||
if (evaluationPointsResponse.error) {
|
||||
return { error: evaluationPointsResponse.error, status: evaluationPointsResponse.status };
|
||||
@@ -257,7 +257,7 @@ export async function getReviewPoints(fileId: string, request: Request) {
|
||||
},
|
||||
token: frontendJWT
|
||||
};
|
||||
const groupsResponse = await postgrestGet('evaluation_point_groups', groupsParams);
|
||||
const groupsResponse = await postgrestGet('/api/postgrest/proxy/evaluation_point_groups', groupsParams);
|
||||
|
||||
if (groupsResponse.error) {
|
||||
return { error: groupsResponse.error, status: groupsResponse.status };
|
||||
@@ -281,7 +281,7 @@ export async function getReviewPoints(fileId: string, request: Request) {
|
||||
},
|
||||
token: frontendJWT
|
||||
};
|
||||
const manualReviewPointsResponse = await postgrestGet('audit_status', manualReviewPointsParams);
|
||||
const manualReviewPointsResponse = await postgrestGet('/api/postgrest/proxy/audit_status', manualReviewPointsParams);
|
||||
if (manualReviewPointsResponse.error) {
|
||||
return { error: manualReviewPointsResponse.error, status: manualReviewPointsResponse.status };
|
||||
}
|
||||
@@ -336,7 +336,7 @@ export async function getReviewPoints(fileId: string, request: Request) {
|
||||
},
|
||||
token: frontendJWT
|
||||
};
|
||||
const scoringProposalsResponse = await postgrestGet('cross_scoring_proposals', scoringProposalsParams);
|
||||
const scoringProposalsResponse = await postgrestGet('/api/postgrest/proxy/cross_scoring_proposals', scoringProposalsParams);
|
||||
|
||||
if (scoringProposalsResponse.error) {
|
||||
return { error: scoringProposalsResponse.error, status: scoringProposalsResponse.status };
|
||||
@@ -776,12 +776,13 @@ export async function updateReviewResult(
|
||||
}
|
||||
|
||||
// 首先获取当前评查结果数据
|
||||
const currentResultResponse = await postgrestGet('evaluation_results', {
|
||||
const currentResultResponse = await postgrestGet('/api/postgrest/proxy/evaluation_results', {
|
||||
select: '*',
|
||||
filter: { id: `eq.${resultId}` },
|
||||
token: frontendJWT
|
||||
});
|
||||
|
||||
console.log('/api/postgrest/proxy/evaluation_results',currentResultResponse.error)
|
||||
if (currentResultResponse.error) {
|
||||
return { error: currentResultResponse.error, status: currentResultResponse.status };
|
||||
}
|
||||
@@ -812,7 +813,7 @@ export async function updateReviewResult(
|
||||
|
||||
// 调用 API 更新评查点结果数据
|
||||
const resultResponse = await postgrestPut<unknown, typeof updatedData>(
|
||||
'evaluation_results',
|
||||
'/api/postgrest/proxy/evaluation_results',
|
||||
updatedData,
|
||||
{ id: resultId },
|
||||
frontendJWT
|
||||
@@ -831,7 +832,7 @@ export async function updateReviewResult(
|
||||
if (editAuditStatusId && editAuditStatusId !== '') {
|
||||
// 更新现有审核状态记录
|
||||
const auditStatusResponse = await postgrestPut(
|
||||
'audit_status',
|
||||
'/api/postgrest/proxy/audit_status',
|
||||
{
|
||||
edit_audit_status: editAuditStatusValue,
|
||||
// 重新审核时不更新message
|
||||
@@ -864,7 +865,7 @@ export async function updateReviewResult(
|
||||
};
|
||||
|
||||
// 使用postgrestPost创建新记录
|
||||
const postResponse = await postgrestPost('audit_status', newAuditStatus, frontendJWT);
|
||||
const postResponse = await postgrestPost('/api/postgrest/proxy/audit_status', newAuditStatus, frontendJWT);
|
||||
|
||||
if (postResponse.error) {
|
||||
return { error: postResponse.error, status: postResponse.status || 500 };
|
||||
@@ -938,7 +939,7 @@ export async function confirmReviewResults(documentId: string, request: Request)
|
||||
|
||||
// 调用API更新文档审核状态
|
||||
const response = await postgrestPut<{ id: string }, typeof updateDocumentParams>(
|
||||
'documents',
|
||||
'/api/postgrest/proxy/documents',
|
||||
updateDocumentParams,
|
||||
{
|
||||
id: documentId,
|
||||
|
||||
@@ -88,122 +88,6 @@ export interface RuleGroupQueryParams {
|
||||
token?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评查点分组列表(支持分页、筛选、排序)
|
||||
* @deprecated 使用 getEvaluationPointGroups 代替(FastAPI v3)
|
||||
* @param params 查询参数
|
||||
* @returns 评查点分组列表和总数
|
||||
*/
|
||||
export async function getRuleGroups_legacy(
|
||||
params?: RuleGroupQueryParams
|
||||
): Promise<{data: RuleGroup[]; totalCount?: number; error?: never} | {data?: never; error: string; status?: number}> {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
name,
|
||||
code,
|
||||
is_enabled,
|
||||
pid = '0', // 默认获取一级分组
|
||||
orderBy = 'created_at',
|
||||
order = 'desc',
|
||||
token
|
||||
} = params || {};
|
||||
|
||||
// 构建筛选条件
|
||||
const filter: Record<string, string> = {};
|
||||
|
||||
// 父级ID筛选 (pid=null或'0'表示一级分组)
|
||||
if (pid === null || pid === '0') {
|
||||
filter['pid'] = 'eq.0';
|
||||
} else if (pid) {
|
||||
filter['pid'] = `eq.${pid}`;
|
||||
}
|
||||
|
||||
// 名称模糊搜索
|
||||
if (name) {
|
||||
filter['name'] = `ilike.*${name}*`;
|
||||
}
|
||||
|
||||
// 编码模糊搜索
|
||||
if (code) {
|
||||
filter['code'] = `ilike.*${code}*`;
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (is_enabled !== undefined) {
|
||||
filter['is_enabled'] = `eq.${is_enabled}`;
|
||||
}
|
||||
|
||||
const postgrestParams: PostgrestParams = {
|
||||
select: `
|
||||
id,
|
||||
pid,
|
||||
name,
|
||||
code,
|
||||
description,
|
||||
is_enabled,
|
||||
created_at
|
||||
`,
|
||||
filter,
|
||||
order: `${orderBy}.${order}`, // PostgREST order format: field.direction
|
||||
limit: pageSize,
|
||||
offset: (page - 1) * pageSize,
|
||||
token
|
||||
};
|
||||
|
||||
const response = await postgrestGet<{code: number; msg: string; data: Array<{
|
||||
id: number;
|
||||
pid: number;
|
||||
name: string;
|
||||
code?: string;
|
||||
description?: string;
|
||||
is_enabled: boolean;
|
||||
created_at?: string;
|
||||
}>}>('evaluation_point_groups', postgrestParams);
|
||||
|
||||
if (response.error) {
|
||||
return { error: response.error, status: response.status };
|
||||
}
|
||||
|
||||
// 处理响应数据
|
||||
let groups: RuleGroup[] = [];
|
||||
if (response.data && 'code' in response.data && response.data.data) {
|
||||
groups = response.data.data.map(group => ({
|
||||
id: group.id.toString(),
|
||||
pid: group.pid.toString(),
|
||||
name: group.name,
|
||||
code: group.code,
|
||||
description: group.description,
|
||||
is_enabled: group.is_enabled,
|
||||
createdAt: group.created_at ? formatDate(group.created_at) : undefined
|
||||
}));
|
||||
} else if (Array.isArray(response.data)) {
|
||||
groups = response.data.map(group => ({
|
||||
id: group.id.toString(),
|
||||
pid: group.pid.toString(),
|
||||
name: group.name,
|
||||
code: group.code,
|
||||
description: group.description,
|
||||
is_enabled: group.is_enabled,
|
||||
createdAt: group.created_at ? formatDate(group.created_at) : undefined
|
||||
}));
|
||||
}
|
||||
|
||||
// 注意:由于当前 PostgREST 客户端不支持 count 参数,totalCount 返回当前页的记录数
|
||||
// 后续可优化为单独查询获取准确的总数
|
||||
return {
|
||||
data: groups,
|
||||
totalCount: groups.length
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取评查点分组列表失败:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '获取评查点分组列表失败',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定分组的子分组(包含评查点数量统计)
|
||||
@@ -241,204 +125,6 @@ export async function getChildGroups(parentId: string, token?: string): Promise<
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================== 批量操作接口 ====================
|
||||
|
||||
/**
|
||||
* 批量更新分组状态(启用/禁用)
|
||||
* @deprecated 使用 batchUpdateEvaluationPointGroupStatus 代替(FastAPI v3)
|
||||
* @param ids 分组ID列表
|
||||
* @param is_enabled 目标状态
|
||||
* @param token JWT token (可选)
|
||||
* @returns 更新结果
|
||||
*/
|
||||
export async function batchUpdateRuleGroupStatus_legacy(
|
||||
ids: string[],
|
||||
is_enabled: boolean,
|
||||
token?: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
updated_count: number;
|
||||
failed_ids: string[];
|
||||
errors?: Array<{ id: string; error: string }>;
|
||||
}> {
|
||||
try {
|
||||
// ========== 1. 参数验证 ==========
|
||||
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
updated_count: 0,
|
||||
failed_ids: [],
|
||||
errors: [{ id: 'validation', error: 'ID列表不能为空' }]
|
||||
};
|
||||
}
|
||||
|
||||
// 验证每个ID的有效性
|
||||
const invalidIds = ids.filter(id => !id || id.trim() === '');
|
||||
if (invalidIds.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
updated_count: 0,
|
||||
failed_ids: ids,
|
||||
errors: [{ id: 'validation', error: '存在无效的分组ID' }]
|
||||
};
|
||||
}
|
||||
|
||||
// ========== 2. 逐个更新(确保每个分组都能被正确处理) ==========
|
||||
|
||||
const failedIds: string[] = [];
|
||||
const errors: Array<{ id: string; error: string }> = [];
|
||||
let updatedCount = 0;
|
||||
|
||||
for (const id of ids) {
|
||||
try {
|
||||
// 验证分组是否存在
|
||||
const groupResponse = await getRuleGroup(id, token);
|
||||
if (groupResponse.error || !groupResponse.data) {
|
||||
failedIds.push(id);
|
||||
errors.push({ id, error: '分组不存在或无法访问' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 执行更新
|
||||
const updateResponse = await postgrestPut<ApiResponse<RuleGroup> | RuleGroup, Partial<ApiRuleGroup>>(
|
||||
'evaluation_point_groups',
|
||||
{ is_enabled },
|
||||
{ id },
|
||||
token
|
||||
);
|
||||
|
||||
if (updateResponse.error) {
|
||||
failedIds.push(id);
|
||||
errors.push({ id, error: updateResponse.error });
|
||||
} else {
|
||||
updatedCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
failedIds.push(id);
|
||||
errors.push({
|
||||
id,
|
||||
error: error instanceof Error ? error.message : '更新失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 3. 返回结果 ==========
|
||||
|
||||
return {
|
||||
success: failedIds.length === 0,
|
||||
updated_count: updatedCount,
|
||||
failed_ids: failedIds,
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('批量更新分组状态失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
updated_count: 0,
|
||||
failed_ids: ids,
|
||||
errors: [{
|
||||
id: 'batch',
|
||||
error: error instanceof Error ? error.message : '批量更新失败'
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除分组(安全的阻止删除策略)
|
||||
* @deprecated 使用 batchDeleteEvaluationPointGroups 代替(FastAPI v3)
|
||||
* @param ids 分组ID列表
|
||||
* @param token JWT token (可选)
|
||||
* @returns 删除结果
|
||||
*/
|
||||
export async function batchDeleteRuleGroups_legacy(
|
||||
ids: string[],
|
||||
token?: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
deleted_count: number;
|
||||
failed_ids: string[];
|
||||
errors?: Array<{ id: string; error: string; details?: { hasChildren?: boolean; hasPoints?: boolean } }>;
|
||||
}> {
|
||||
try {
|
||||
// ========== 1. 参数验证 ==========
|
||||
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
deleted_count: 0,
|
||||
failed_ids: [],
|
||||
errors: [{ id: 'validation', error: 'ID列表不能为空' }]
|
||||
};
|
||||
}
|
||||
|
||||
// 验证每个ID的有效性
|
||||
const invalidIds = ids.filter(id => !id || id.trim() === '');
|
||||
if (invalidIds.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
deleted_count: 0,
|
||||
failed_ids: ids,
|
||||
errors: [{ id: 'validation', error: '存在无效的分组ID' }]
|
||||
};
|
||||
}
|
||||
|
||||
// ========== 2. 逐个删除(使用安全的阻止删除策略) ==========
|
||||
|
||||
const failedIds: string[] = [];
|
||||
const errors: Array<{ id: string; error: string; details?: { hasChildren?: boolean; hasPoints?: boolean } }> = [];
|
||||
let deletedCount = 0;
|
||||
|
||||
for (const id of ids) {
|
||||
try {
|
||||
const deleteResult = await deleteRuleGroup(id, token);
|
||||
|
||||
if (!deleteResult.success) {
|
||||
failedIds.push(id);
|
||||
errors.push({
|
||||
id,
|
||||
error: deleteResult.error || '删除失败',
|
||||
details: deleteResult.details ? {
|
||||
hasChildren: deleteResult.details.hasChildren,
|
||||
hasPoints: deleteResult.details.hasPoints
|
||||
} : undefined
|
||||
});
|
||||
} else {
|
||||
deletedCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
failedIds.push(id);
|
||||
errors.push({
|
||||
id,
|
||||
error: error instanceof Error ? error.message : '删除失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 3. 返回结果 ==========
|
||||
|
||||
return {
|
||||
success: failedIds.length === 0,
|
||||
deleted_count: deletedCount,
|
||||
failed_ids: failedIds,
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('批量删除分组失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
deleted_count: 0,
|
||||
failed_ids: ids,
|
||||
errors: [{
|
||||
id: 'batch',
|
||||
error: error instanceof Error ? error.message : '批量删除失败'
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// FastAPI v3 接口函数(新版)
|
||||
// ========================================
|
||||
|
||||
@@ -136,7 +136,7 @@ export async function updateDocumentAuditStatus(id: string, auditStatus: number,
|
||||
});
|
||||
|
||||
const response = await postgrestPut<Document, Partial<Document>>(
|
||||
'documents',
|
||||
'/api/postgrest/proxy/documents',
|
||||
{ audit_status: auditStatus },
|
||||
{
|
||||
id: parseInt(id),
|
||||
|
||||
@@ -364,7 +364,7 @@ export async function getRule(id: string, token?: string): Promise<{data: Rule;
|
||||
};
|
||||
|
||||
// 获取评查点详情 - 使用正确的PostgREST格式
|
||||
const response = await postgrestGet<{code: number; msg: string; data: ApiRule} | ApiRule[]>('evaluation_points', postgrestParams);
|
||||
const response = await postgrestGet<{code: number; msg: string; data: ApiRule} | ApiRule[]>('/api/postgrest/proxy/evaluation_points', postgrestParams);
|
||||
|
||||
// 检查是否有错误响应
|
||||
if (response.error) {
|
||||
@@ -401,7 +401,7 @@ export async function getRule(id: string, token?: string): Promise<{data: Rule;
|
||||
};
|
||||
|
||||
// 查询评查点分组
|
||||
const groupResponse = await postgrestGet<{code: number; msg: string; data: {id: number; name: string}[]}>('evaluation_point_groups', groupParams);
|
||||
const groupResponse = await postgrestGet<{code: number; msg: string; data: {id: number; name: string}[]}>('/api/postgrest/proxy/evaluation_point_groups', groupParams);
|
||||
|
||||
if (groupResponse.data?.data && groupResponse.data.data.length > 0) {
|
||||
// 将分组信息添加到评查点数据中
|
||||
@@ -480,7 +480,7 @@ export async function createRule(ruleData: Omit<Rule, 'id' | 'createdAt' | 'upda
|
||||
}
|
||||
|
||||
// 检查分组是否存在
|
||||
const groupResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number; name: string; pid: number}> }>('evaluation_point_groups', {
|
||||
const groupResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number; name: string; pid: number}> }>('/api/postgrest/proxy/evaluation_point_groups', {
|
||||
filter: { 'id': `eq.${ruleData.groupId}` },
|
||||
select: 'id,name,pid',
|
||||
token
|
||||
@@ -524,7 +524,7 @@ export async function createRule(ruleData: Omit<Rule, 'id' | 'createdAt' | 'upda
|
||||
};
|
||||
|
||||
// 使用postgrestPost创建评查点
|
||||
const response = await postgrestPost<{code: number; msg: string; data: ApiRule}, typeof apiRuleData>('evaluation_points', apiRuleData, token);
|
||||
const response = await postgrestPost<{code: number; msg: string; data: ApiRule}, typeof apiRuleData>('/api/postgrest/proxy/evaluation_points', apiRuleData, token);
|
||||
|
||||
// 检查是否有错误响应
|
||||
if (response.error) {
|
||||
@@ -599,7 +599,7 @@ export async function updateRule(id: string, ruleData: Partial<Omit<Rule, 'id' |
|
||||
|
||||
// 4. 验证分组ID有效性(如果提供)
|
||||
if (ruleData.groupId !== undefined) {
|
||||
const groupResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number; name: string; pid: number}> }>('evaluation_point_groups', {
|
||||
const groupResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number; name: string; pid: number}> }>('/api/postgrest/proxy/evaluation_point_groups', {
|
||||
filter: { 'id': `eq.${ruleData.groupId}` },
|
||||
select: 'id,name,pid',
|
||||
token
|
||||
@@ -645,7 +645,7 @@ export async function updateRule(id: string, ruleData: Partial<Omit<Rule, 'id' |
|
||||
}
|
||||
|
||||
// 使用postgrestPut更新评查点 - 使用正确的PostgREST格式
|
||||
const response = await postgrestPut<{code: number; msg: string; data: ApiRule} | ApiRule[], typeof apiRuleData>('evaluation_points', apiRuleData, { id: parseInt(id) }, token);
|
||||
const response = await postgrestPut<{code: number; msg: string; data: ApiRule} | ApiRule[], typeof apiRuleData>('/api/postgrest/proxy/evaluation_points', apiRuleData, { id: parseInt(id) }, token);
|
||||
|
||||
// 检查是否有错误响应
|
||||
if (response.error) {
|
||||
@@ -816,7 +816,7 @@ export async function getRuleTypes(documentTypeIds?: number[], token?: string):
|
||||
name: string;
|
||||
evaluation_point_groups_ids: number[];
|
||||
}>
|
||||
}>('document_types', documentTypesParams);
|
||||
}>('/api/postgrest/proxy/document_types', documentTypesParams);
|
||||
|
||||
if (documentTypesResponse.error) {
|
||||
return { error: documentTypesResponse.error, status: documentTypesResponse.status };
|
||||
@@ -890,7 +890,7 @@ export async function getRuleTypes(documentTypeIds?: number[], token?: string):
|
||||
description: string;
|
||||
is_enabled: boolean;
|
||||
}>
|
||||
}>('evaluation_point_groups', groupsParams);
|
||||
}>('/api/postgrest/proxy/evaluation_point_groups', groupsParams);
|
||||
|
||||
// 检查是否有错误响应
|
||||
if (response.error) {
|
||||
@@ -975,7 +975,7 @@ export async function getRuleGroupsByType(typeId: string, token?: string): Promi
|
||||
description: string;
|
||||
is_enabled: boolean;
|
||||
}>;
|
||||
}>('evaluation_point_groups', postgrestParams);
|
||||
}>('/api/postgrest/proxy/evaluation_point_groups', postgrestParams);
|
||||
|
||||
// 检查是否有错误响应
|
||||
if (response.error) {
|
||||
@@ -1169,7 +1169,7 @@ export async function getFormattedEvaluationPoint(id: number): Promise<{
|
||||
}
|
||||
};
|
||||
|
||||
const response = await postgrestGet<{code: number; msg: string; data: ApiRule[]} | ApiRule[]>('evaluation_points', postgrestParams);
|
||||
const response = await postgrestGet<{code: number; msg: string; data: ApiRule[]} | ApiRule[]>('/api/postgrest/proxy/evaluation_points', postgrestParams);
|
||||
|
||||
if (response.error) {
|
||||
return {
|
||||
@@ -1238,7 +1238,7 @@ export async function getEvaluationPointGroups(): Promise<{
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
const response = await postgrestGet<{code: number; msg: string; data: EvaluationPointGroupType[]} | EvaluationPointGroupType[]>('evaluation_point_groups', postgrestParams);
|
||||
const response = await postgrestGet<{code: number; msg: string; data: EvaluationPointGroupType[]} | EvaluationPointGroupType[]>('/api/postgrest/proxy/evaluation_point_groups', postgrestParams);
|
||||
|
||||
if (response.error) {
|
||||
return {
|
||||
@@ -1480,14 +1480,14 @@ export async function saveEvaluationPoint(evaluationPoint: EvaluationPointInput,
|
||||
if (isEditMode) {
|
||||
// 更新操作
|
||||
response = await postgrestPut<{code: number; msg: string; data: ApiRule} | ApiRule, typeof cleanedData>(
|
||||
`evaluation_points`,
|
||||
`/api/postgrest/proxy/evaluation_points`,
|
||||
cleanedData,
|
||||
{id: cleanedData.id!}
|
||||
);
|
||||
} else {
|
||||
// 创建操作
|
||||
response = await postgrestPost<{code: number; msg: string; data: ApiRule} | ApiRule, typeof cleanedData>(
|
||||
'evaluation_points',
|
||||
'/api/postgrest/proxy/evaluation_points',
|
||||
cleanedData
|
||||
);
|
||||
}
|
||||
@@ -1566,7 +1566,7 @@ export async function getRuleStatistics(token?: string): Promise<{data: RuleStat
|
||||
is_enabled: boolean;
|
||||
risk: string;
|
||||
evaluation_point_groups_id: number | null;
|
||||
}>}>('evaluation_points', postgrestParams);
|
||||
}>}>('/api/postgrest/proxy/evaluation_points', postgrestParams);
|
||||
|
||||
// 检查是否有错误响应
|
||||
if (response.error) {
|
||||
@@ -1628,7 +1628,7 @@ export async function getRuleStatistics(token?: string): Promise<{data: RuleStat
|
||||
token
|
||||
};
|
||||
|
||||
const groupsResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number; name: string}>}>('evaluation_point_groups', groupsParams);
|
||||
const groupsResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number; name: string}>}>('/api/postgrest/proxy/evaluation_point_groups', groupsParams);
|
||||
|
||||
let groups: Array<{id: number; name: string}> = [];
|
||||
if (groupsResponse.data && 'code' in groupsResponse.data && groupsResponse.data.data) {
|
||||
|
||||
+5
-103
@@ -145,7 +145,7 @@ function getFileExtension(filename: string): string {
|
||||
* @returns 评查结果
|
||||
*/
|
||||
async function getEvaluationResults(id: number, frontendJWT?: string) {
|
||||
const response = await postgrestGet<[]>('evaluation_results', {
|
||||
const response = await postgrestGet<[]>('/api/postgrest/proxy/evaluation_results', {
|
||||
filter: {
|
||||
'document_id': `eq.${id}`
|
||||
},
|
||||
@@ -254,7 +254,7 @@ export async function deleteDocument(id: string, userId: string, token?: string)
|
||||
}
|
||||
|
||||
const response = await postgrestDelete(
|
||||
'documents',
|
||||
'/api/postgrest/proxy/documents',
|
||||
{
|
||||
filter: {
|
||||
'id': `eq.${id}`,
|
||||
@@ -298,7 +298,7 @@ export async function getDocument(id: string, userId: string, frontendJWT?: stri
|
||||
}
|
||||
|
||||
const response = await postgrestGet<Document[]>(
|
||||
'documents',
|
||||
'/api/postgrest/proxy/documents',
|
||||
{
|
||||
filter: {
|
||||
'id': `eq.${id}`,
|
||||
@@ -349,7 +349,7 @@ export async function getDocumentWithNoUserId(id: string, frontendJWT?: string):
|
||||
// console.log("get单个文档id", id)
|
||||
|
||||
const response = await postgrestGet<Document[]>(
|
||||
'documents',
|
||||
'/api/postgrest/proxy/documents',
|
||||
{
|
||||
filter: {
|
||||
'id': `eq.${id}`,
|
||||
@@ -430,7 +430,7 @@ export async function updateDocument(id: string, document: Partial<DocumentUI> &
|
||||
// console.log('更新文档API数据:', apiDocument);
|
||||
|
||||
const response = await postgrestPut<Document, Partial<Document>>(
|
||||
'documents',
|
||||
'/api/postgrest/proxy/documents',
|
||||
apiDocument,
|
||||
{
|
||||
id: parseInt(id),
|
||||
@@ -653,101 +653,3 @@ export async function getDocumentsListFromAPI(searchParams: {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文档历史版本列表
|
||||
* @param documentName 文档名称
|
||||
* @param userId 用户ID
|
||||
* @param excludeId 排除的文档ID(当前最新版本的ID)
|
||||
* @param token JWT token
|
||||
* @returns 历史版本列表
|
||||
*/
|
||||
export async function getDocumentHistory(
|
||||
documentName: string,
|
||||
userId: string,
|
||||
excludeId: number,
|
||||
token?: string
|
||||
): Promise<{
|
||||
data?: DocumentVersionUI[];
|
||||
error?: string;
|
||||
status?: number;
|
||||
}> {
|
||||
try {
|
||||
if (!documentName) {
|
||||
return { error: '文档名称不能为空', status: 400 };
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return { error: '用户身份验证失败', status: 401 };
|
||||
}
|
||||
|
||||
// 调用 RPC 函数获取历史版本
|
||||
const response = await postgrestPost<any[], unknown>(
|
||||
'rpc/documents_get_document_history',
|
||||
{
|
||||
p_document_name: documentName,
|
||||
p_user_id: parseInt(userId, 10),
|
||||
p_exclude_id: excludeId
|
||||
},
|
||||
token
|
||||
);
|
||||
|
||||
if (response.error || !response.data) {
|
||||
return { error: response.error || '获取历史版本失败', status: response.status || 500 };
|
||||
}
|
||||
|
||||
const historyDocs = response.data;
|
||||
|
||||
// 转换为 UI 格式,并计算问题数量差异
|
||||
const documents: DocumentVersionUI[] = historyDocs.map((doc: any, index: number) => {
|
||||
// 计算与下一个版本(更早的版本)的问题数量差异
|
||||
let issuesDiff: number | undefined;
|
||||
let issuesDiffType: 'increase' | 'decrease' | 'same' | undefined;
|
||||
|
||||
if (index < historyDocs.length - 1) {
|
||||
const olderDoc = historyDocs[index + 1];
|
||||
if (doc.false_count != null && olderDoc.false_count != null) {
|
||||
const diff = doc.false_count - olderDoc.false_count;
|
||||
issuesDiff = Math.abs(diff);
|
||||
if (diff > 0) {
|
||||
issuesDiffType = 'increase';
|
||||
} else if (diff < 0) {
|
||||
issuesDiffType = 'decrease';
|
||||
} else {
|
||||
issuesDiffType = 'same';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: doc.id,
|
||||
name: doc.name,
|
||||
documentNumber: doc.document_number,
|
||||
type: doc.type_id.toString(),
|
||||
typeName: doc.type_name || '未知类型',
|
||||
size: doc.file_size,
|
||||
auditStatus: doc.audit_status ?? 0,
|
||||
fileStatus: doc.status || '',
|
||||
issues: doc.false_count ?? null,
|
||||
issuesDiff,
|
||||
issuesDiffType,
|
||||
uploadTime: formatDate(doc.created_at),
|
||||
fileType: getFileExtension(doc.name),
|
||||
path: doc.path,
|
||||
isTest: doc.is_test_document,
|
||||
updatedAt: formatDate(doc.updated_at),
|
||||
pageCount: doc.ocr_result?.__meta?.page_count || 0,
|
||||
ocrResult: doc.ocr_result,
|
||||
versionNumber: historyDocs.length - index
|
||||
};
|
||||
});
|
||||
|
||||
return { data: documents };
|
||||
} catch (error) {
|
||||
console.error('获取文档历史版本失败:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '获取文档历史版本失败',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -497,7 +497,7 @@ export async function getTodayDocuments(
|
||||
}
|
||||
|
||||
// console.log('发送请求参数:', params);
|
||||
const response = await postgrestGet<Document[]>('documents', { ...params, token });
|
||||
const response = await postgrestGet<Document[]>('/api/postgrest/proxy/documents', { ...params, token });
|
||||
// console.log('API 响应:', response);
|
||||
|
||||
if (response.error) {
|
||||
@@ -552,7 +552,7 @@ export async function getDocumentTypes(token?: string): Promise<{data: DocumentT
|
||||
}
|
||||
// 如果没有 documentTypeIds,返回所有文档类型(不添加过滤条件)
|
||||
|
||||
const response = await postgrestGet<DocumentType[]>('document_types', { ...params, token });
|
||||
const response = await postgrestGet<DocumentType[]>('/api/postgrest/proxy/ocument_types', { ...params, token });
|
||||
|
||||
if (response.error) {
|
||||
return { error: response.error, status: response.status };
|
||||
@@ -600,7 +600,7 @@ export async function getDocumentsStatus(
|
||||
'id': `in.(${documentIds.join(',')})`
|
||||
}
|
||||
};
|
||||
documentsResponse = await postgrestGet<Document[]>('documents', { ...documentsParams, token });
|
||||
documentsResponse = await postgrestGet<Document[]>('/api/postgrest/proxy/documents', { ...documentsParams, token });
|
||||
}
|
||||
|
||||
// 查询合同附件状态
|
||||
@@ -613,7 +613,7 @@ export async function getDocumentsStatus(
|
||||
'id': `in.(${attachmentIds.join(',')})`
|
||||
}
|
||||
};
|
||||
attachmentResponse = await postgrestGet<ContractStructureComparison[]>('contract_structure_comparison', { ...attachmentParams, token });
|
||||
attachmentResponse = await postgrestGet<ContractStructureComparison[]>('/api/postgrest/proxy/contract_structure_comparison', { ...attachmentParams, token });
|
||||
}
|
||||
|
||||
if (documentsResponse.error && attachmentResponse.error) {
|
||||
|
||||
@@ -244,7 +244,7 @@ export async function getEntryModules(userRole: string | null | undefined, userA
|
||||
filter: {}
|
||||
};
|
||||
|
||||
const modulesResponse = await postgrestGet('entry_modules', { ...params, token });
|
||||
const modulesResponse = await postgrestGet('/api/postgrest/proxy/entry_modules', { ...params, token });
|
||||
|
||||
if (modulesResponse.error) {
|
||||
console.error('❌ [getEntryModules] 查询入口模块失败:', modulesResponse.error);
|
||||
@@ -295,7 +295,7 @@ export async function getEntryModules(userRole: string | null | undefined, userA
|
||||
}
|
||||
};
|
||||
|
||||
const typesResponse = await postgrestGet('document_types', { ...typesParams, token });
|
||||
const typesResponse = await postgrestGet('/api/postgrest/proxy/document_types', { ...typesParams, token });
|
||||
|
||||
if (typesResponse.error) {
|
||||
console.error(`❌ [getEntryModules] 查询模块 ${module.id} 的文档类型失败:`, typesResponse.error);
|
||||
|
||||
@@ -531,246 +531,3 @@ async function callIDaaSLogout(accessToken: string, appId: string): Promise<void
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户信息到数据库
|
||||
*
|
||||
* 此函数实现以下逻辑:
|
||||
* 1. 内部生成临时 JWT(user_id 为 'login',仅用于数据库操作)
|
||||
* 2. 根据 userInfo.sub 查询 sso_users 表中是否已存在该用户
|
||||
* 3. 如果存在,则更新用户信息(如果用户已有 area 值则不更新)
|
||||
* 4. 如果不存在,则插入新的用户记录
|
||||
* 5. 返回保存的用户数据和临时 JWT
|
||||
*
|
||||
* @param userInfo - 从 IDaaS 获取的用户信息
|
||||
* @param userRole - 用户角色
|
||||
* @param tokenExpiresIn - Token过期时间(秒)
|
||||
* @param area - 用户所属地区,根据端口号确定
|
||||
* @returns Promise<{success: boolean, data?: SsoUser, tempToken?: string, error?: string}>
|
||||
*/
|
||||
export async function saveUserInfo(
|
||||
userInfo: UserInfo,
|
||||
userRole: UserRole,
|
||||
tokenExpiresIn: number,
|
||||
area?: string
|
||||
): Promise<{success: boolean, data?: SsoUser, tempToken?: string, error?: string}> {
|
||||
try {
|
||||
console.log("开始保存用户信息", userInfo);
|
||||
|
||||
// 验证必要字段
|
||||
if (!userInfo.sub) {
|
||||
return { success: false, error: "用户唯一标识 sub 不能为空" };
|
||||
}
|
||||
|
||||
// 🔒 安全:在服务端生成临时 JWT,user_id 使用占位符 'login'
|
||||
// 这样客户端无法看到真实的 user_id
|
||||
const tempUserInfo: UserInfoForJWT = {
|
||||
sub: userInfo.sub,
|
||||
user_id: 'login', // 使用占位符,避免在客户端暴露真实ID
|
||||
username: 'login',
|
||||
nick_name: userInfo.nick_name || userInfo.nickname || userInfo.name || "未知用户",
|
||||
email: userInfo.email,
|
||||
phone_number: userInfo.phone_number,
|
||||
ou_id: userInfo.ou_id || "default",
|
||||
ou_name: userInfo.ou_name || "未知部门",
|
||||
is_leader: userInfo.is_leader || false,
|
||||
user_role: userRole
|
||||
};
|
||||
|
||||
const tempToken = JWTUtils.generateJWT(tempUserInfo, tokenExpiresIn);
|
||||
|
||||
// 1. 根据 sub 查询是否已存在该用户
|
||||
const existingUserResult = await postgrestGet<SsoUser[]>("sso_users", {
|
||||
filter: {
|
||||
"sub": `eq.${userInfo.sub}`,
|
||||
"deleted_at": "is.null" // 只查询未删除的记录
|
||||
},
|
||||
token: tempToken
|
||||
});
|
||||
|
||||
if (existingUserResult.error) {
|
||||
console.error("查询用户失败:", existingUserResult.error);
|
||||
return { success: false, error: `查询用户失败: ${existingUserResult.error}` };
|
||||
}
|
||||
|
||||
const existingUsers = existingUserResult.data || [];
|
||||
const existingUser = existingUsers.length > 0 ? existingUsers[0] : null;
|
||||
|
||||
// 准备要保存的用户数据
|
||||
// 注意:OAuth返回的字段是nickname,而不是nick_name
|
||||
const userData: Partial<SsoUser> = {
|
||||
sub: userInfo.sub,
|
||||
username: userInfo.username || userInfo.name || userInfo.sub,
|
||||
nick_name: userInfo.nick_name || userInfo.nickname || userInfo.name || "未知用户",
|
||||
phone_number: userInfo.phone_number || undefined,
|
||||
email: userInfo.email || undefined,
|
||||
ou_id: userInfo.ou_id || "default",
|
||||
ou_name: userInfo.ou_name || "未知部门",
|
||||
status: userInfo.status !== undefined ? userInfo.status : 0,
|
||||
is_leader: userInfo.is_leader || false,
|
||||
};
|
||||
|
||||
if (existingUser) {
|
||||
// 2. 用户已存在,执行更新操作
|
||||
console.log("用户已存在,执行更新操作", existingUser.id);
|
||||
|
||||
// 只有在现有用户没有 area 或 area 为空时,才更新 area
|
||||
if (area && !existingUser.area) {
|
||||
userData.area = area;
|
||||
console.log("用户原本无地区信息,更新地区为:", area);
|
||||
} else if (existingUser.area) {
|
||||
console.log("用户已有地区信息:", existingUser.area, "不更新");
|
||||
}
|
||||
|
||||
const updateResult = await postgrestPut<SsoUser[], Partial<SsoUser>>(
|
||||
"sso_users",
|
||||
userData,
|
||||
{ id: existingUser.id! },
|
||||
tempToken
|
||||
);
|
||||
|
||||
if (updateResult.error) {
|
||||
console.error("更新用户失败:", updateResult.error);
|
||||
return { success: false, error: `更新用户失败: ${updateResult.error}` };
|
||||
}
|
||||
|
||||
console.log("用户信息更新成功");
|
||||
return {
|
||||
success: true,
|
||||
data: Array.isArray(updateResult.data) ? updateResult.data[0] : updateResult.data as unknown as SsoUser,
|
||||
tempToken // 返回临时 JWT
|
||||
};
|
||||
} else {
|
||||
// 3. 用户不存在,执行插入操作,设置地区信息
|
||||
console.log("用户不存在,执行插入操作");
|
||||
|
||||
// 新用户直接设置 area
|
||||
if (area) {
|
||||
userData.area = area;
|
||||
console.log("新用户,设置地区为:", area);
|
||||
}
|
||||
|
||||
const insertResult = await postgrestPost<SsoUser[], SsoUser>("sso_users", userData as SsoUser, tempToken);
|
||||
|
||||
if (insertResult.error) {
|
||||
console.error("插入用户失败:", insertResult.error);
|
||||
return { success: false, error: `插入用户失败: ${insertResult.error}` };
|
||||
}
|
||||
|
||||
console.log("用户信息插入成功");
|
||||
|
||||
// 4. 给这个用户默认添加一个角色,角色为common
|
||||
const userData_with_id = Array.isArray(insertResult.data) ? insertResult.data[0] : insertResult.data as unknown as SsoUser;
|
||||
if (userData_with_id?.id) {
|
||||
await addDefaultRole(userData_with_id.id, 2, tempToken);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: userData_with_id,
|
||||
tempToken // 返回临时 JWT
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存用户信息时发生错误:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: `保存用户信息失败: ${error instanceof Error ? error.message : String(error)}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为用户添加默认角色
|
||||
*
|
||||
* @param userId - 用户ID
|
||||
* @param roleId - 角色ID,默认为2(common角色)
|
||||
* @param token - JWT令牌,用于调用postgrest服务
|
||||
* @returns 添加结果
|
||||
*/
|
||||
export async function addDefaultRole(userId: string, roleId: number = 2, token?: string) {
|
||||
try {
|
||||
console.log(`为用户 ${userId} 添加默认角色 ${roleId}`);
|
||||
|
||||
// 检查用户是否已经有此角色
|
||||
const existingRoleResult = await postgrestGet<Array<{id: number, user_id: string, role_id: number}>>("user_role", {
|
||||
filter: {
|
||||
user_id: `eq.${userId}`,
|
||||
role_id: `eq.${roleId}`
|
||||
},
|
||||
token
|
||||
});
|
||||
|
||||
if (existingRoleResult.error) {
|
||||
console.error("查询用户角色失败:", existingRoleResult.error);
|
||||
return { success: false, error: `查询用户角色失败: ${existingRoleResult.error}` };
|
||||
}
|
||||
|
||||
const existingRoles = existingRoleResult.data || [];
|
||||
if (existingRoles.length > 0) {
|
||||
console.log("用户已经拥有此角色,跳过添加");
|
||||
return { success: true, data: existingRoles[0] };
|
||||
}
|
||||
|
||||
// 添加角色
|
||||
const addRoleResult = await postgrestPost<Array<{id: number, user_id: string, role_id: number}>, {user_id: string, role_id: number}>("user_role", {
|
||||
user_id: userId,
|
||||
role_id: roleId
|
||||
}, token);
|
||||
|
||||
if (addRoleResult.error) {
|
||||
console.error("添加用户角色失败:", addRoleResult.error);
|
||||
return { success: false, error: `添加用户角色失败: ${addRoleResult.error}` };
|
||||
}
|
||||
|
||||
console.log("用户角色添加成功");
|
||||
return {
|
||||
success: true,
|
||||
data: Array.isArray(addRoleResult.data) ? addRoleResult.data[0] : addRoleResult.data
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("添加用户角色时发生错误:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: `添加用户角色失败: ${error instanceof Error ? error.message : String(error)}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过用户sub获取用户信息
|
||||
*
|
||||
* @param sub - 用户的唯一标识
|
||||
* @returns 用户信息
|
||||
*/
|
||||
export async function getUserBySub(sub: string) {
|
||||
try {
|
||||
// console.log(`查询用户: ${sub}`);
|
||||
|
||||
const userResult = await postgrestGet<SsoUser[]>("sso_users", {
|
||||
filter: {
|
||||
sub: `eq.${sub}`
|
||||
}
|
||||
});
|
||||
|
||||
if (userResult.error) {
|
||||
console.error("查询用户失败:", userResult.error);
|
||||
return { success: false, error: `查询用户失败: ${userResult.error}` };
|
||||
}
|
||||
|
||||
const users = userResult.data || [];
|
||||
const user = users.length > 0 ? users[0] : null;
|
||||
|
||||
if (!user) {
|
||||
return { success: false, error: "用户不存在" };
|
||||
}
|
||||
|
||||
return { success: true, data: user };
|
||||
} catch (error) {
|
||||
console.error("查询用户时发生错误:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: `查询用户失败: ${error instanceof Error ? error.message : String(error)}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* MinIO 存储管理 API 客户端
|
||||
* 基于 /api/v2/storage 接口
|
||||
*/
|
||||
|
||||
import { apiRequest } from '../axios-client';
|
||||
|
||||
export interface CopyFileRequest {
|
||||
source_path: string;
|
||||
destination_path: string;
|
||||
source_bucket?: string | null;
|
||||
destination_bucket?: string | null;
|
||||
}
|
||||
|
||||
export interface CopyFileResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
source_path: string;
|
||||
destination_path: string;
|
||||
}
|
||||
|
||||
export interface MoveFileRequest {
|
||||
source_path: string;
|
||||
destination_path: string;
|
||||
}
|
||||
|
||||
export interface MoveFileResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
source_path: string;
|
||||
destination_path: string;
|
||||
}
|
||||
|
||||
export interface DeleteFileRequest {
|
||||
file_path: string;
|
||||
}
|
||||
|
||||
export interface DeleteFileResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
file_path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文件
|
||||
* @param request 复制文件请求参数
|
||||
* @param token JWT token(可选)
|
||||
* @returns 复制结果
|
||||
*/
|
||||
export async function copyFile(request: CopyFileRequest, token?: string) {
|
||||
const response = await apiRequest<CopyFileResponse>(
|
||||
'/api/v2/storage/files/copy',
|
||||
{
|
||||
method: 'POST',
|
||||
data: request,
|
||||
headers: {
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动/重命名文件
|
||||
* @param request 移动文件请求参数
|
||||
* @param token JWT token(可选)
|
||||
* @returns 移动结果
|
||||
*/
|
||||
export async function moveFile(request: MoveFileRequest, token?: string) {
|
||||
const response = await apiRequest<MoveFileResponse>(
|
||||
'/api/v2/storage/files/move',
|
||||
{
|
||||
method: 'POST',
|
||||
data: request,
|
||||
headers: {
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param request 删除文件请求参数
|
||||
* @param token JWT token(可选)
|
||||
* @returns 删除结果
|
||||
*/
|
||||
export async function deleteFile(request: DeleteFileRequest, token?: string) {
|
||||
const response = await apiRequest<DeleteFileResponse>(
|
||||
'/api/v2/storage/files',
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
}
|
||||
},
|
||||
{ file_path: request.file_path }
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成新的文件路径(用于复制模板文件)
|
||||
* @param originalPath 原始文件路径
|
||||
* @param area 地区代码
|
||||
* @returns 新文件路径
|
||||
*/
|
||||
export function generateDraftFilePath(originalPath: string, area: string): string {
|
||||
// 提取文件目录和文件名
|
||||
const lastSlashIndex = originalPath.lastIndexOf('/');
|
||||
const directory = lastSlashIndex >= 0 ? originalPath.substring(0, lastSlashIndex) : '';
|
||||
const fileName = lastSlashIndex >= 0 ? originalPath.substring(lastSlashIndex + 1) : originalPath;
|
||||
|
||||
// 提取文件扩展名
|
||||
const lastDotIndex = fileName.lastIndexOf('.');
|
||||
const baseName = lastDotIndex >= 0 ? fileName.substring(0, lastDotIndex) : fileName;
|
||||
const extension = lastDotIndex >= 0 ? fileName.substring(lastDotIndex) : '';
|
||||
|
||||
// 生成时间戳和 UUID
|
||||
const timestamp = Date.now();
|
||||
const uuid = generateUUID();
|
||||
|
||||
// 构建新文件名:原文件名_地区_时间戳_uuid.扩展名
|
||||
const newFileName = `${baseName}_${area}_${timestamp}_${uuid}${extension}`;
|
||||
|
||||
// 构建完整路径
|
||||
const newPath = directory ? `${directory}/${newFileName}` : newFileName;
|
||||
|
||||
return newPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成简单的 UUID(不依赖外部库)
|
||||
* @returns UUID 字符串
|
||||
*/
|
||||
function generateUUID(): string {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
@@ -70,6 +70,7 @@ export async function highlightText(
|
||||
options?: HighlightOptions
|
||||
): Promise<HighlightResponse> {
|
||||
const color = options?.color ?? 16776960; // 默认黄色
|
||||
// const page = options?.page ?? 1; // 默认第1页
|
||||
const page = options?.page ?? null; // 默认第1页
|
||||
|
||||
console.log('[HighlightSelectText] 调用 Python 脚本高亮文本:', {
|
||||
|
||||
@@ -5,16 +5,14 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { PlaceholderSchema } from '~/types/contract-draft';
|
||||
import { messageService } from '~/components/ui/MessageModal';
|
||||
|
||||
interface PlaceholderFormProps {
|
||||
schema: PlaceholderSchema | null;
|
||||
values: Record<string, string>;
|
||||
onChange: (values: Record<string, string>) => void;
|
||||
onBatchReplace: () => void;
|
||||
onExportDocument: () => void; // 改名:导出文档
|
||||
onComplete: () => void;
|
||||
isReplacing: boolean;
|
||||
isDeleting: boolean; // 改名:是否正在删除
|
||||
isDeleting: boolean; // 是否正在删除
|
||||
onSingleReplace?: (key: string, value: string) => void; // 单个替换
|
||||
onFieldFocus?: (key: string) => void; // 字段聚焦(高亮)
|
||||
}
|
||||
@@ -23,10 +21,7 @@ export function PlaceholderForm({
|
||||
schema,
|
||||
values,
|
||||
onChange,
|
||||
onBatchReplace,
|
||||
onExportDocument,
|
||||
onComplete,
|
||||
isReplacing,
|
||||
isDeleting,
|
||||
onSingleReplace,
|
||||
onFieldFocus
|
||||
@@ -81,7 +76,19 @@ export function PlaceholderForm({
|
||||
const handleCompleteClick = () => {
|
||||
const missing = getMissingRequiredFields();
|
||||
if (missing.length > 0) {
|
||||
alert(`请填写以下必填字段:\n${missing.join('\n')}`);
|
||||
messageService.show({
|
||||
type: 'warning',
|
||||
title: '字段校验失败',
|
||||
message: '请填写以下必填字段:',
|
||||
children: (
|
||||
<ul className="list-disc list-inside mt-2 space-y-1">
|
||||
{missing.map((field, index) => (
|
||||
<li key={index} className="text-gray-700">{field}</li>
|
||||
))}
|
||||
</ul>
|
||||
),
|
||||
confirmText: '确定'
|
||||
});
|
||||
return;
|
||||
}
|
||||
onComplete();
|
||||
@@ -106,12 +113,37 @@ export function PlaceholderForm({
|
||||
<div className="h-full flex flex-col bg-white">
|
||||
{/* 表单头部 */}
|
||||
<div className="px-6 py-4 border-b border-gray-200 bg-gradient-to-r from-blue-50 to-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-[#004d38] flex items-center justify-center shadow-sm">
|
||||
<i className="ri-file-edit-line text-white text-base"></i>
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-gray-900">填写合同信息</h2>
|
||||
</div>
|
||||
|
||||
{/* 完成按钮 */}
|
||||
<button
|
||||
onClick={handleCompleteClick}
|
||||
disabled={isDeleting}
|
||||
className={`flex items-center justify-center gap-1.5 px-6 py-2 text-sm font-medium rounded-lg transition-all duration-150 ${
|
||||
isDeleting
|
||||
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-green-600 text-white hover:bg-green-700 hover:shadow-md active:scale-[0.98]'
|
||||
}`}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<i className="ri-loader-4-line animate-spin"></i>
|
||||
<span>处理中</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="ri-check-line"></i>
|
||||
<span>完成</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 表单内容区域 */}
|
||||
@@ -175,53 +207,6 @@ export function PlaceholderForm({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮区域(固定在底部) */}
|
||||
<div className="border-t border-gray-200 px-6 py-3 bg-gray-50 flex gap-2">
|
||||
<button
|
||||
onClick={onBatchReplace}
|
||||
disabled={isReplacing || isDeleting}
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 px-4 py-2 text-white text-sm font-medium rounded-lg transition-all duration-150 ${
|
||||
isReplacing || isDeleting
|
||||
? 'bg-gray-300 cursor-not-allowed'
|
||||
: 'bg-gradient-to-r from-primary to-[#004d38] hover:shadow-md active:scale-[0.98]'
|
||||
}`}
|
||||
>
|
||||
{isReplacing ? (
|
||||
<>
|
||||
<i className="ri-loader-4-line animate-spin"></i>
|
||||
<span>替换中</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="ri-refresh-line"></i>
|
||||
<span>全部替换</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleCompleteClick}
|
||||
disabled={isReplacing || isDeleting}
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 px-4 py-2 text-sm font-medium rounded-lg transition-all duration-150 ${
|
||||
isReplacing || isDeleting
|
||||
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-green-600 text-white hover:bg-green-700 hover:shadow-md active:scale-[0.98]'
|
||||
}`}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<i className="ri-loader-4-line animate-spin"></i>
|
||||
<span>处理中</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="ri-check-line"></i>
|
||||
<span>完成</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ import { FilePreview, type FilePreviewHandle } from '~/components/reviews/FilePr
|
||||
import { PlaceholderForm } from '~/components/contracts/PlaceholderForm';
|
||||
import { getDraftById, deleteDraft } from '~/api/contracts/draft-service.server';
|
||||
import { getUserSession } from '~/api/login/auth.server';
|
||||
import { deleteFile } from '~/api/storage/minio-client';
|
||||
import { toastService } from '~/components/ui/Toast';
|
||||
import { messageService } from '~/components/ui/MessageModal';
|
||||
import { downloadFile } from '~/api/axios-client';
|
||||
import { extractPlaceholdersFromDocx, generateDefaultSchema } from '~/api/contracts/docx-parser.server';
|
||||
import path from 'path';
|
||||
@@ -51,16 +53,28 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
|
||||
const jwt = frontendJWT || undefined;
|
||||
|
||||
// 【临时测试】使用测试文档和模拟数据
|
||||
// const testDocPath = path.join(process.cwd(), 'public', 'testWork', '买卖合同 (1).docx');
|
||||
const testDocPath = 'contract-template/买卖/买卖合同范本.docx';
|
||||
// 从 URL 参数获取文件路径、模板 ID 和标题
|
||||
const url = new URL(request.url);
|
||||
const filePath = url.searchParams.get('filePath');
|
||||
const templateId = url.searchParams.get('templateId');
|
||||
const title = url.searchParams.get('title');
|
||||
|
||||
// 创建临时的草稿对象(用于测试)
|
||||
if (!filePath) {
|
||||
throw new Response('文件路径参数缺失', { status: 400 });
|
||||
}
|
||||
|
||||
if (!templateId) {
|
||||
throw new Response('模板ID参数缺失', { status: 400 });
|
||||
}
|
||||
|
||||
console.log('[Loader] 起草合同:', { filePath, templateId, title });
|
||||
|
||||
// 创建草稿对象
|
||||
const draft: DraftedContract = {
|
||||
id: draftId,
|
||||
template_id: 1,
|
||||
file_path: testDocPath,
|
||||
title: '买卖合同-测试草稿',
|
||||
template_id: parseInt(templateId),
|
||||
file_path: filePath,
|
||||
title: title || '未命名合同',
|
||||
placeholder_values: {},
|
||||
status: 'draft',
|
||||
created_by: parseInt(userInfo.sub),
|
||||
@@ -72,10 +86,10 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
let placeholderSchema: PlaceholderSchema | null = null;
|
||||
|
||||
try {
|
||||
console.log('[Loader] 使用测试文档:', testDocPath);
|
||||
console.log('[Loader] 使用文件:', filePath);
|
||||
|
||||
// 提取占位符
|
||||
const placeholders = await extractPlaceholdersFromDocx(testDocPath);
|
||||
const placeholders = await extractPlaceholdersFromDocx(filePath);
|
||||
console.log('[Loader] 提取到的占位符:', placeholders);
|
||||
|
||||
// 生成默认 schema
|
||||
@@ -86,15 +100,15 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
placeholderSchema = null;
|
||||
}
|
||||
|
||||
// 创建临时的模板对象(用于测试)
|
||||
// 创建模板对象
|
||||
const template: ContractTemplate = {
|
||||
id: 1,
|
||||
title: '买卖合同模板',
|
||||
template_code: 'TEST-001',
|
||||
id: parseInt(templateId),
|
||||
title: title || '合同模板',
|
||||
template_code: 'DRAFT-' + templateId,
|
||||
category_id: 1,
|
||||
file_path: testDocPath,
|
||||
file_path: filePath,
|
||||
file_format: 'docx',
|
||||
description: '测试用买卖合同模板',
|
||||
description: '起草中的合同',
|
||||
is_featured: false,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
@@ -103,12 +117,13 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
|
||||
return Response.json({
|
||||
draft,
|
||||
template
|
||||
template,
|
||||
returnUrl: `/contract-template/detail/${templateId}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Action 函数:处理删除草稿
|
||||
* Action 函数:处理文件删除
|
||||
*/
|
||||
export async function action({ request, params }: ActionFunctionArgs) {
|
||||
const draftId = parseInt(params.draftId || '0');
|
||||
@@ -123,19 +138,26 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
return Response.json({ error: '未登录' }, { status: 401 });
|
||||
}
|
||||
|
||||
const userId = parseInt(userInfo.sub);
|
||||
const jwt = frontendJWT || undefined;
|
||||
|
||||
try {
|
||||
// 解析表单数据
|
||||
const formData = await request.formData();
|
||||
const actionType = formData.get('_action') as string;
|
||||
|
||||
if (actionType === 'delete') {
|
||||
// 删除草稿记录
|
||||
await deleteDraft(draftId, userId, jwt);
|
||||
if (actionType === 'deleteFile') {
|
||||
const filePath = formData.get('filePath') as string;
|
||||
|
||||
return Response.json({ success: true, message: '草稿已删除' });
|
||||
if (!filePath) {
|
||||
return Response.json({ error: '文件路径缺失' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 删除 MinIO 文件,传递 JWT
|
||||
await deleteFile({ file_path: filePath }, jwt);
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: '文件删除成功'
|
||||
});
|
||||
}
|
||||
|
||||
return Response.json({ error: '无效的操作类型' }, { status: 400 });
|
||||
@@ -149,14 +171,14 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
}
|
||||
|
||||
export default function ContractDraftPage() {
|
||||
const { draft, template } = useLoaderData<typeof loader>();
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const { draft, template, returnUrl } = loaderData;
|
||||
const navigate = useNavigate();
|
||||
const fetcher = useFetcher<ActionData>();
|
||||
|
||||
const [placeholderValues, setPlaceholderValues] = useState<Record<string, string>>(
|
||||
draft.placeholder_values || {}
|
||||
);
|
||||
const [isReplacing, setIsReplacing] = useState(false);
|
||||
const [highlightValue, setHighlightValue] = useState<string | undefined>(undefined);
|
||||
const [aiSuggestionReplace, setAiSuggestionReplace] = useState<{
|
||||
searchText: string;
|
||||
@@ -165,56 +187,88 @@ export default function ContractDraftPage() {
|
||||
} | undefined>(undefined);
|
||||
|
||||
const filePreviewRef = useRef<FilePreviewHandle>(null);
|
||||
const hasDeletedNormally = useRef(false); // 标记是否已通过正常流程删除
|
||||
const currentPathRef = useRef(window.location.pathname); // 保存当前路由路径
|
||||
|
||||
// 从 fetcher.state 判断是否正在操作
|
||||
const isDeleting = fetcher.state !== 'idle';
|
||||
|
||||
// 处理 fetcher 响应(删除草稿)
|
||||
// 处理 fetcher 响应(文件删除)
|
||||
useEffect(() => {
|
||||
if (fetcher.data?.success && fetcher.data.message === '草稿已删除') {
|
||||
// 删除成功,跳转到模板列表
|
||||
if (fetcher.data?.success) {
|
||||
// 标记已通过正常流程删除
|
||||
hasDeletedNormally.current = true;
|
||||
|
||||
// 文件删除成功,显示提示并跳转
|
||||
// toastService.success('合同已完成');
|
||||
|
||||
// 延迟跳转,让用户看到成功提示
|
||||
setTimeout(() => {
|
||||
if (returnUrl) {
|
||||
navigate(returnUrl);
|
||||
} else {
|
||||
navigate('/contract-template');
|
||||
}
|
||||
}, 500);
|
||||
} else if (fetcher.data?.error) {
|
||||
toastService.error(fetcher.data.error);
|
||||
}
|
||||
}, [fetcher.data, navigate]);
|
||||
}, [fetcher.data, navigate, returnUrl]);
|
||||
|
||||
// 监听页面关闭事件 - 自动删除草稿
|
||||
// 页面卸载或路由切换时清理文件
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
// 发送删除请求(使用 sendBeacon 确保请求发送)
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'delete');
|
||||
const deleteFileSync = () => {
|
||||
// 如果已经通过正常流程删除,不再重复删除
|
||||
if (hasDeletedNormally.current || !draft.file_path) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.sendBeacon(
|
||||
`/contract-draft/${draft.id}`,
|
||||
formData
|
||||
);
|
||||
// console.log('[Cleanup] 尝试删除文件:', draft.file_path);
|
||||
// console.log('[Cleanup] 使用路径:', currentPathRef.current);
|
||||
|
||||
try {
|
||||
// 直接使用同步 XMLHttpRequest 确保删除请求真正执行
|
||||
// 使用保存的原始路径,而不是当前的 window.location.pathname(可能已切换)
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', currentPathRef.current, false); // false = 同步请求
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'deleteFile');
|
||||
formData.append('filePath', draft.file_path);
|
||||
|
||||
xhr.send(formData);
|
||||
// console.log('[Cleanup] 同步删除完成,状态:', xhr.status);
|
||||
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
// console.log('[Cleanup] ✅ 文件删除成功:', response);
|
||||
} catch (e) {
|
||||
// console.log('[Cleanup] ✅ 文件删除成功(状态码200)');
|
||||
}
|
||||
} else {
|
||||
console.error('[Cleanup] ❌ 文件删除失败,状态码:', xhr.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Cleanup] 删除文件失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBeforeUnload = () => {
|
||||
deleteFileSync();
|
||||
};
|
||||
|
||||
// 监听页面卸载事件
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
// 清理事件监听器
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, [draft.id]);
|
||||
|
||||
// 组件卸载时删除草稿(处理路由跳转的情况)
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// 组件卸载时删除草稿记录
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'delete');
|
||||
|
||||
fetch(`/contract-draft/${draft.id}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
keepalive: true // 确保请求在页面关闭后仍然发送
|
||||
}).catch(err => {
|
||||
console.error('[Draft] 删除草稿失败:', err);
|
||||
});
|
||||
// 组件卸载时(路由切换),执行删除
|
||||
deleteFileSync();
|
||||
};
|
||||
}, [draft.id]);
|
||||
}, [draft.file_path]);
|
||||
|
||||
// 单个替换占位符
|
||||
const handleSingleReplace = async (key: string, value: string) => {
|
||||
@@ -231,7 +285,7 @@ export default function ContractDraftPage() {
|
||||
// 短暂延迟后清除参数,以便下次可以重新触发
|
||||
setTimeout(() => {
|
||||
setAiSuggestionReplace(undefined);
|
||||
toastService.success(`已替换 ${key}`);
|
||||
// toastService.success(`已替换 ${key}`);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
@@ -249,52 +303,7 @@ export default function ContractDraftPage() {
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// 批量替换占位符
|
||||
const handleBatchReplace = async () => {
|
||||
setIsReplacing(true);
|
||||
|
||||
try {
|
||||
// 获取 CollaboraViewer 引用
|
||||
const collaboraRef = filePreviewRef.current?.collaboraViewerRef.current;
|
||||
|
||||
if (!collaboraRef?.isReady) {
|
||||
toastService.warning('文档尚未加载完成,请稍候...');
|
||||
setIsReplacing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Draft] 开始批量替换占位符:', placeholderValues);
|
||||
|
||||
// 批量替换所有占位符
|
||||
let replaceCount = 0;
|
||||
for (const [key, value] of Object.entries(placeholderValues)) {
|
||||
if (value) { // 只替换有值的字段
|
||||
const placeholder = `{{${key}}}`;
|
||||
console.log(`[Draft] 替换: ${placeholder} -> ${value}`);
|
||||
|
||||
// 调用 unoCommands.replaceAll 方法
|
||||
if (collaboraRef.unoCommands?.replaceAll) {
|
||||
await collaboraRef.unoCommands.replaceAll(placeholder, value);
|
||||
replaceCount++;
|
||||
// 添加延迟避免 Collabora 响应不过来
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
} else {
|
||||
console.warn('[Draft] unoCommands.replaceAll 方法不可用');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Draft] 替换完成,共替换 ${replaceCount} 个占位符`);
|
||||
toastService.success(`占位符替换完成(${replaceCount}个)`);
|
||||
} catch (error) {
|
||||
console.error('[Draft] 替换失败:', error);
|
||||
toastService.error(`替换失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
} finally {
|
||||
setIsReplacing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 导出文档(下载文件)
|
||||
// 导出文档(下载当前编辑的文件)
|
||||
const handleExportDocument = async () => {
|
||||
if (!draft.file_path) {
|
||||
toastService.error('文件路径不存在,无法下载');
|
||||
@@ -304,7 +313,7 @@ export default function ContractDraftPage() {
|
||||
try {
|
||||
toastService.info('正在下载文件...');
|
||||
|
||||
// 使用统一的下载方法
|
||||
// 使用 axios-client 的 downloadFile 方法下载文件
|
||||
const blob = await downloadFile(draft.file_path);
|
||||
|
||||
// 创建Blob URL
|
||||
@@ -336,30 +345,43 @@ export default function ContractDraftPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 完成起草(下载文件 + 删除草稿记录)
|
||||
// 完成起草(下载文件 + 删除 MinIO 文件)
|
||||
const handleComplete = async () => {
|
||||
try {
|
||||
// 1. 先下载文件
|
||||
await handleExportDocument();
|
||||
|
||||
// 2. 延迟后删除草稿记录并跳转
|
||||
setTimeout(() => {
|
||||
// 2. 删除 MinIO 文件
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'delete');
|
||||
formData.append('_action', 'deleteFile');
|
||||
formData.append('filePath', draft.file_path);
|
||||
|
||||
fetcher.submit(formData, { method: 'post' });
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('[Complete] 操作失败:', error);
|
||||
toastService.error('操作失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 返回模板详情页(删除草稿)
|
||||
// 返回模板详情页
|
||||
const handleBack = () => {
|
||||
if (confirm('确定要返回吗?草稿将被删除。')) {
|
||||
// 删除草稿记录
|
||||
messageService.show({
|
||||
type: 'warning',
|
||||
title: '确认返回',
|
||||
message: '确定要返回吗?未保存的更改将丢失。',
|
||||
confirmText: '确定返回',
|
||||
cancelText: '取消',
|
||||
onConfirm: () => {
|
||||
// 删除 MinIO 文件
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'delete');
|
||||
formData.append('_action', 'deleteFile');
|
||||
formData.append('filePath', draft.file_path);
|
||||
|
||||
fetcher.submit(formData, { method: 'post' });
|
||||
// 删除成功后会自动跳转(通过 useEffect)
|
||||
|
||||
// 注意:文件删除后会在 useEffect 中跳转
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -376,11 +398,11 @@ export default function ContractDraftPage() {
|
||||
</button>
|
||||
<div className="border-l border-gray-300 h-10"></div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-xl font-bold text-gray-900 tracking-tight">{draft.title}</h1>
|
||||
<p className="text-sm text-gray-500 flex items-center gap-2">
|
||||
<i className="ri-file-text-line text-base"></i>
|
||||
<span>基于模板:{template.title}</span>
|
||||
</p>
|
||||
{/* <h1 className="text-xl font-bold text-gray-900 tracking-tight">{draft.title}</h1> */}
|
||||
<h1 className="flex items-center gap-2">
|
||||
<i className="ri-file-text-line"></i>
|
||||
<span>基于模板:{template.title.replace(/-[\d-]+$/, '')}</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -422,10 +444,7 @@ export default function ContractDraftPage() {
|
||||
schema={template.placeholder_schema as any}
|
||||
values={placeholderValues}
|
||||
onChange={setPlaceholderValues}
|
||||
onBatchReplace={handleBatchReplace}
|
||||
onExportDocument={handleExportDocument}
|
||||
onComplete={handleComplete}
|
||||
isReplacing={isReplacing}
|
||||
isDeleting={isDeleting}
|
||||
onSingleReplace={handleSingleReplace}
|
||||
onFieldFocus={handleFieldFocus}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import type { MetaFunction, LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node';
|
||||
import { redirect } from '@remix-run/node';
|
||||
import { useLoaderData, useNavigate, useSubmit } from '@remix-run/react';
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getContractTemplate } from '~/api/contract-template/templates';
|
||||
import type { ContractTemplate } from '~/api/contract-template/templates';
|
||||
import styles from '~/styles/pages/contract-template.css?url';
|
||||
import filePreviewStyles from '~/styles/components/file-preview-isolation.css?url';
|
||||
import { getUserSession } from '~/api/login/auth.server';
|
||||
import { createDraftContract } from '~/api/contracts/draft-service.server';
|
||||
import { apiRequest, downloadFile } from '~/api/axios-client';
|
||||
|
||||
// 导入FilePreview组件
|
||||
import { FilePreview } from '~/components/reviews';
|
||||
// 导入统一的下载方法和提示服务
|
||||
import { downloadFile } from '~/api/axios-client';
|
||||
import { toastService } from '~/components/ui/Toast';
|
||||
|
||||
export const links = () => [
|
||||
@@ -70,7 +69,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
* Action 函数:处理起草合同请求
|
||||
*/
|
||||
export async function action({ request, params }: ActionFunctionArgs) {
|
||||
const templateId = parseInt(params.id || '0');
|
||||
const templateId = params.id!;
|
||||
|
||||
if (!templateId) {
|
||||
return Response.json({ error: '模板ID无效' }, { status: 400 });
|
||||
@@ -86,27 +85,60 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
// 解析表单数据
|
||||
const formData = await request.formData();
|
||||
const title = formData.get('title') as string;
|
||||
const draftFilePath = formData.get('draftFilePath') as string | null;
|
||||
const originalFilePath = formData.get('originalFilePath') as string;
|
||||
|
||||
if (!title) {
|
||||
return Response.json({ error: '标题不能为空' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 创建草稿记录(到时候可以换成接口,使用接口来在minio中生成备份文件:备份文件可以用时间戳+uuid来保证唯一性。)
|
||||
// const draft = await createDraftContract(
|
||||
// {
|
||||
// templateId,
|
||||
// title,
|
||||
// draftFilePath: draftFilePath || undefined
|
||||
// },
|
||||
// parseInt(userInfo.sub),
|
||||
// draftFilePath || undefined,
|
||||
// frontendJWT || undefined
|
||||
// );
|
||||
if (!originalFilePath) {
|
||||
return Response.json({ error: '文件路径不存在' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 重定向到草稿编辑页面
|
||||
// return redirect(`/contract-draft/${draft.id}`);
|
||||
return redirect(`/contract-draft/1`);
|
||||
// 生成新文件路径
|
||||
const area = userInfo.area || 'unknown';
|
||||
const timestamp = Date.now();
|
||||
const uuid = crypto.randomUUID();
|
||||
|
||||
// 提取文件目录和文件名
|
||||
const lastSlashIndex = originalFilePath.lastIndexOf('/');
|
||||
const directory = lastSlashIndex >= 0 ? originalFilePath.substring(0, lastSlashIndex) : '';
|
||||
const fileName = lastSlashIndex >= 0 ? originalFilePath.substring(lastSlashIndex + 1) : originalFilePath;
|
||||
|
||||
// 提取文件扩展名
|
||||
const lastDotIndex = fileName.lastIndexOf('.');
|
||||
const baseName = lastDotIndex >= 0 ? fileName.substring(0, lastDotIndex) : fileName;
|
||||
const extension = lastDotIndex >= 0 ? fileName.substring(lastDotIndex) : '';
|
||||
|
||||
// 构建新文件名
|
||||
const newFileName = `${baseName}_${area}_${timestamp}_${uuid}${extension}`;
|
||||
const newFilePath = directory ? `${directory}/${newFileName}` : newFileName;
|
||||
|
||||
console.log('[Draft] 复制文件:', { originalFilePath, newFilePath });
|
||||
|
||||
// 调用 MinIO 复制文件 API(需要传递 JWT)
|
||||
const jwt = frontendJWT || undefined;
|
||||
const copyResponse = await apiRequest('/api/v2/storage/files/copy', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
source_path: originalFilePath,
|
||||
destination_path: newFilePath
|
||||
},
|
||||
headers: {
|
||||
'Authorization': jwt ? `Bearer ${jwt}` : ''
|
||||
}
|
||||
});
|
||||
|
||||
if (copyResponse.error) {
|
||||
console.error('[Draft] 文件复制失败:', copyResponse.error);
|
||||
return Response.json({ error: `文件复制失败: ${copyResponse.error}` }, { status: 500 });
|
||||
}
|
||||
|
||||
console.log('[Draft] 文件复制成功:', copyResponse.data);
|
||||
|
||||
// 重定向到草稿编辑页面,通过 URL 参数传递文件路径和模板 ID
|
||||
const draftUrl = `/contract-draft/1?filePath=${encodeURIComponent(newFilePath)}&templateId=${templateId}&title=${encodeURIComponent(title)}`;
|
||||
return redirect(draftUrl);
|
||||
} catch (error) {
|
||||
console.error('[Template Detail] 创建草稿失败:', error);
|
||||
return Response.json(
|
||||
@@ -124,6 +156,12 @@ export default function ContractTemplateDetail() {
|
||||
// 注释掉收藏功能
|
||||
// const [isFavorited, setIsFavorited] = useState(false);
|
||||
|
||||
// 防止页面加载时自动滚动到预览区域(由 Collabora iframe 的 tabIndex 导致)
|
||||
useEffect(() => {
|
||||
// 页面加载后立即滚动回顶部
|
||||
window.scrollTo({ top: 0, behavior: 'instant' });
|
||||
}, []);
|
||||
|
||||
const handleBack = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
@@ -181,21 +219,20 @@ export default function ContractTemplateDetail() {
|
||||
const handleStartDraft = () => {
|
||||
if (isCreatingDraft) return;
|
||||
|
||||
// 生成默认标题
|
||||
// const defaultTitle = `${template.title}-${new Date().toLocaleDateString('zh-CN').replace(/\//g, '')}`;
|
||||
if (!template.file_path) {
|
||||
toastService.error('模板文件路径不存在,无法起草');
|
||||
return;
|
||||
}
|
||||
|
||||
// // 提示用户输入标题
|
||||
// const title = prompt('请输入合同标题:', defaultTitle);
|
||||
// if (!title) return;
|
||||
// 生成默认标题
|
||||
const defaultTitle = `${template.title}-${new Date().toLocaleDateString('zh-CN').replace(/\//g, '')}`;
|
||||
|
||||
setIsCreatingDraft(true);
|
||||
|
||||
// 使用 Remix 的 submit 提交表单
|
||||
const formData = new FormData();
|
||||
// formData.append('title', title.trim());
|
||||
formData.append('title', '买卖合同-拟起草合同');
|
||||
// 可选:如果需要复制文件,可以先调用文件复制服务,然后传递 draftFilePath
|
||||
// formData.append('draftFilePath', draftFilePath);
|
||||
formData.append('title', defaultTitle);
|
||||
formData.append('originalFilePath', template.file_path);
|
||||
|
||||
submit(formData, { method: 'post' });
|
||||
};
|
||||
@@ -430,7 +467,7 @@ export default function ContractTemplateDetail() {
|
||||
|
||||
{/* 合同预览 - 只有当存在pdf_file_path时才显示 */}
|
||||
{fileContent && (
|
||||
<div className="content-section mb-8" id="template-preview">
|
||||
<div className="content-section mb-8">
|
||||
<h3 className="section-title text-xl font-semibold mb-4">合同预览</h3>
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<div
|
||||
|
||||
@@ -188,7 +188,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
}
|
||||
|
||||
toastService.success('更新文档成功');
|
||||
return redirect("/documents");
|
||||
return redirect("/documents/list");
|
||||
} catch (error) {
|
||||
console.error("更新文档失败2:", error);
|
||||
return Response.json({
|
||||
|
||||
@@ -382,7 +382,7 @@ export default function RuleNew() {
|
||||
const fetchEvaluationPointGroups = useCallback(async () => {
|
||||
try {
|
||||
// console.log("🔍 [fetchEvaluationPointGroups] 开始获取评查点组数据");
|
||||
const response = await postgrestGet('evaluation_point_groups', { token: frontendJWT });
|
||||
const response = await postgrestGet('/api/postgrest/proxy/evaluation_point_groups', { token: frontendJWT });
|
||||
|
||||
// console.log("🔍 [fetchEvaluationPointGroups] API响应:", response);
|
||||
|
||||
|
||||
@@ -0,0 +1,776 @@
|
||||
# MinIO 存储管理 API 文档
|
||||
|
||||
## 概述
|
||||
|
||||
MinIO 存储管理模块提供对象存储的完整管理能力,包括存储桶管理和文件操作。
|
||||
|
||||
**基础路径**: `/api/v2/storage`
|
||||
|
||||
**认证要求**: 当前无需认证(后续可按需添加)
|
||||
|
||||
---
|
||||
|
||||
## 接口列表
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/buckets` | 创建存储桶 |
|
||||
| DELETE | `/buckets` | 删除存储桶 |
|
||||
| GET | `/buckets` | 列出所有存储桶 |
|
||||
| POST | `/files/copy` | 复制文件 |
|
||||
| POST | `/files/move` | 移动/重命名文件 |
|
||||
| DELETE | `/files` | 删除单个文件 |
|
||||
| POST | `/files/batch-delete` | 批量删除文件 |
|
||||
| GET | `/files/download` | 下载文件 |
|
||||
| GET | `/files` | 列出目录文件 |
|
||||
| GET | `/files/metadata` | 获取文件元数据 |
|
||||
| GET | `/files/presigned-url` | 获取预签名URL |
|
||||
|
||||
---
|
||||
|
||||
## 存储桶管理
|
||||
|
||||
### 1. 创建存储桶
|
||||
|
||||
**POST** `/api/v2/storage/buckets`
|
||||
|
||||
创建新的 MinIO 存储桶。
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"bucket_name": "my-new-bucket"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| bucket_name | string | 是 | 存储桶名称(3-63字符,小写字母、数字、连字符) |
|
||||
|
||||
#### 响应示例
|
||||
|
||||
**成功 (200)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "存储桶创建成功",
|
||||
"bucket_name": "my-new-bucket"
|
||||
}
|
||||
```
|
||||
|
||||
**存储桶已存在 (200)**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "存储桶已存在: my-new-bucket",
|
||||
"bucket_name": "my-new-bucket"
|
||||
}
|
||||
```
|
||||
|
||||
#### cURL 示例
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/v2/storage/buckets" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"bucket_name": "my-new-bucket"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 删除存储桶
|
||||
|
||||
**DELETE** `/api/v2/storage/buckets`
|
||||
|
||||
删除 MinIO 存储桶。支持强制删除(会先删除桶内所有文件)。
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"bucket_name": "my-bucket",
|
||||
"force": true
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| bucket_name | string | 是 | 存储桶名称 |
|
||||
| force | boolean | 否 | 强制删除(默认 false,为 true 时会先删除桶内所有文件) |
|
||||
|
||||
#### 响应示例
|
||||
|
||||
**成功 (200)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "存储桶删除成功",
|
||||
"bucket_name": "my-bucket",
|
||||
"files_deleted": 15
|
||||
}
|
||||
```
|
||||
|
||||
**存储桶不存在 (200)**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "存储桶不存在: my-bucket",
|
||||
"bucket_name": "my-bucket",
|
||||
"files_deleted": 0
|
||||
}
|
||||
```
|
||||
|
||||
#### cURL 示例
|
||||
|
||||
```bash
|
||||
# 普通删除(桶必须为空)
|
||||
curl -X DELETE "http://localhost:8000/api/v2/storage/buckets" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"bucket_name": "my-bucket"}'
|
||||
|
||||
# 强制删除(会删除桶内所有文件)
|
||||
curl -X DELETE "http://localhost:8000/api/v2/storage/buckets" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"bucket_name": "my-bucket", "force": true}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 列出存储桶
|
||||
|
||||
**GET** `/api/v2/storage/buckets`
|
||||
|
||||
列出所有 MinIO 存储桶。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "共 3 个存储桶",
|
||||
"buckets": [
|
||||
{
|
||||
"name": "docauditai",
|
||||
"creation_date": "2024-01-15T08:30:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "backup",
|
||||
"creation_date": "2024-02-20T10:15:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "temp",
|
||||
"creation_date": "2024-03-01T14:00:00+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### cURL 示例
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8000/api/v2/storage/buckets"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文件操作
|
||||
|
||||
### 4. 复制文件
|
||||
|
||||
**POST** `/api/v2/storage/files/copy`
|
||||
|
||||
在 MinIO 内复制文件。支持跨存储桶复制。
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"source_path": "documents/contract.pdf",
|
||||
"destination_path": "backup/contract-2024.pdf",
|
||||
"source_bucket": null,
|
||||
"destination_bucket": null
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| source_path | string | 是 | 源文件路径 |
|
||||
| destination_path | string | 是 | 目标文件路径 |
|
||||
| source_bucket | string | 否 | 源存储桶(默认使用配置的默认桶) |
|
||||
| destination_bucket | string | 否 | 目标存储桶(默认使用配置的默认桶) |
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "文件复制成功",
|
||||
"source_path": "docauditai/documents/contract.pdf",
|
||||
"destination_path": "docauditai/backup/contract-2024.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
#### cURL 示例
|
||||
|
||||
```bash
|
||||
# 同桶内复制
|
||||
curl -X POST "http://localhost:8000/api/v2/storage/files/copy" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"source_path": "documents/contract.pdf",
|
||||
"destination_path": "backup/contract-2024.pdf"
|
||||
}'
|
||||
|
||||
# 跨桶复制
|
||||
curl -X POST "http://localhost:8000/api/v2/storage/files/copy" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"source_path": "documents/contract.pdf",
|
||||
"destination_path": "contract.pdf",
|
||||
"source_bucket": "docauditai",
|
||||
"destination_bucket": "backup"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 移动/重命名文件
|
||||
|
||||
**POST** `/api/v2/storage/files/move`
|
||||
|
||||
移动文件(复制后删除源文件)。在同一目录内移动即为重命名。
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"source_path": "documents/old-name.pdf",
|
||||
"destination_path": "documents/new-name.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| source_path | string | 是 | 源文件路径 |
|
||||
| destination_path | string | 是 | 目标文件路径 |
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "文件移动成功",
|
||||
"source_path": "documents/old-name.pdf",
|
||||
"destination_path": "documents/new-name.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
#### cURL 示例
|
||||
|
||||
```bash
|
||||
# 重命名文件
|
||||
curl -X POST "http://localhost:8000/api/v2/storage/files/move" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"source_path": "documents/report-v1.pdf",
|
||||
"destination_path": "documents/report-v2.pdf"
|
||||
}'
|
||||
|
||||
# 移动到其他目录
|
||||
curl -X POST "http://localhost:8000/api/v2/storage/files/move" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"source_path": "temp/upload.pdf",
|
||||
"destination_path": "documents/final.pdf"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 删除单个文件
|
||||
|
||||
**DELETE** `/api/v2/storage/files`
|
||||
|
||||
删除 MinIO 中的单个文件。
|
||||
|
||||
#### 请求参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| file_path | string | 是 | 文件路径 |
|
||||
|
||||
#### 响应示例
|
||||
|
||||
**成功 (200)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "文件删除成功",
|
||||
"file_path": "documents/old-file.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
**文件不存在 (200)**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "文件不存在: documents/old-file.pdf",
|
||||
"file_path": "documents/old-file.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
#### cURL 示例
|
||||
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:8000/api/v2/storage/files?file_path=documents/old-file.pdf"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. 批量删除文件
|
||||
|
||||
**POST** `/api/v2/storage/files/batch-delete`
|
||||
|
||||
批量删除多个文件。
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"file_paths": [
|
||||
"temp/file1.pdf",
|
||||
"temp/file2.pdf",
|
||||
"temp/file3.pdf"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| file_paths | string[] | 是 | 文件路径列表(至少1个) |
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "删除完成: 成功 3, 失败 0",
|
||||
"deleted_count": 3,
|
||||
"failed_count": 0,
|
||||
"failed_paths": []
|
||||
}
|
||||
```
|
||||
|
||||
**部分失败 (200)**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "删除完成: 成功 2, 失败 1",
|
||||
"deleted_count": 2,
|
||||
"failed_count": 1,
|
||||
"failed_paths": ["temp/not-exist.pdf"]
|
||||
}
|
||||
```
|
||||
|
||||
#### cURL 示例
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/v2/storage/files/batch-delete" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"file_paths": [
|
||||
"temp/file1.pdf",
|
||||
"temp/file2.pdf",
|
||||
"temp/file3.pdf"
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. 下载文件
|
||||
|
||||
**GET** `/api/v2/storage/files/download`
|
||||
|
||||
从 MinIO 下载文件。返回文件流。
|
||||
|
||||
#### 请求参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| file_path | string | 是 | 文件路径 |
|
||||
| filename | string | 否 | 下载时显示的文件名 |
|
||||
|
||||
#### 响应
|
||||
|
||||
返回文件二进制流,包含以下响应头:
|
||||
|
||||
```
|
||||
Content-Type: application/pdf
|
||||
Content-Disposition: attachment; filename="contract.pdf"
|
||||
Content-Length: 1234567
|
||||
```
|
||||
|
||||
#### cURL 示例
|
||||
|
||||
```bash
|
||||
# 下载文件
|
||||
curl -X GET "http://localhost:8000/api/v2/storage/files/download?file_path=documents/contract.pdf" \
|
||||
-o contract.pdf
|
||||
|
||||
# 指定下载文件名
|
||||
curl -X GET "http://localhost:8000/api/v2/storage/files/download?file_path=documents/contract.pdf&filename=my-contract.pdf" \
|
||||
-o my-contract.pdf
|
||||
```
|
||||
|
||||
#### 浏览器直接下载
|
||||
|
||||
```
|
||||
http://localhost:8000/api/v2/storage/files/download?file_path=documents/contract.pdf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. 列出目录文件
|
||||
|
||||
**GET** `/api/v2/storage/files`
|
||||
|
||||
列出指定目录下的文件。
|
||||
|
||||
#### 请求参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| directory_path | string | 否 | "" | 目录路径 |
|
||||
| recursive | boolean | 否 | false | 是否递归列出子目录 |
|
||||
| max_files | int | 否 | 100 | 最大返回数量(1-1000) |
|
||||
| marker | string | 否 | null | 分页标记 |
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "共 5 个文件",
|
||||
"files": [
|
||||
"documents/contract-001.pdf",
|
||||
"documents/contract-002.pdf",
|
||||
"documents/contract-003.pdf",
|
||||
"documents/report.docx",
|
||||
"documents/summary.xlsx"
|
||||
],
|
||||
"total_count": 5,
|
||||
"next_marker": null
|
||||
}
|
||||
```
|
||||
|
||||
**分页响应 (200)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "共 100 个文件",
|
||||
"files": ["..."],
|
||||
"total_count": 100,
|
||||
"next_marker": "documents/file-100.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
#### cURL 示例
|
||||
|
||||
```bash
|
||||
# 列出根目录
|
||||
curl -X GET "http://localhost:8000/api/v2/storage/files"
|
||||
|
||||
# 列出指定目录
|
||||
curl -X GET "http://localhost:8000/api/v2/storage/files?directory_path=documents"
|
||||
|
||||
# 递归列出
|
||||
curl -X GET "http://localhost:8000/api/v2/storage/files?directory_path=documents&recursive=true"
|
||||
|
||||
# 分页查询
|
||||
curl -X GET "http://localhost:8000/api/v2/storage/files?directory_path=documents&max_files=50"
|
||||
|
||||
# 获取下一页
|
||||
curl -X GET "http://localhost:8000/api/v2/storage/files?directory_path=documents&max_files=50&marker=documents/file-50.pdf"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10. 获取文件元数据
|
||||
|
||||
**GET** `/api/v2/storage/files/metadata`
|
||||
|
||||
获取文件的详细元数据信息。
|
||||
|
||||
#### 请求参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| file_path | string | 是 | 文件路径 |
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "获取成功",
|
||||
"file_info": {
|
||||
"path": "documents/contract.pdf",
|
||||
"size": 1234567,
|
||||
"content_type": "application/pdf",
|
||||
"created_time": "2024-03-15T10:30:00+00:00",
|
||||
"modified_time": "2024-03-15T10:30:00+00:00",
|
||||
"etag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
|
||||
"url": "http://minio:9000/docauditai/documents/contract.pdf?X-Amz-..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### cURL 示例
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8000/api/v2/storage/files/metadata?file_path=documents/contract.pdf"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 11. 获取预签名URL
|
||||
|
||||
**GET** `/api/v2/storage/files/presigned-url`
|
||||
|
||||
获取文件的预签名下载URL,可用于临时分享。
|
||||
|
||||
#### 请求参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| file_path | string | 是 | - | 文件路径 |
|
||||
| expires | int | 否 | 3600 | 过期时间(秒),范围 60-604800(7天) |
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "获取成功",
|
||||
"url": "http://minio:9000/docauditai/documents/contract.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...",
|
||||
"expires_in": 3600
|
||||
}
|
||||
```
|
||||
|
||||
#### cURL 示例
|
||||
|
||||
```bash
|
||||
# 默认1小时过期
|
||||
curl -X GET "http://localhost:8000/api/v2/storage/files/presigned-url?file_path=documents/contract.pdf"
|
||||
|
||||
# 自定义过期时间(1天)
|
||||
curl -X GET "http://localhost:8000/api/v2/storage/files/presigned-url?file_path=documents/contract.pdf&expires=86400"
|
||||
|
||||
# 最长7天
|
||||
curl -X GET "http://localhost:8000/api/v2/storage/files/presigned-url?file_path=documents/contract.pdf&expires=604800"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 前端集成示例
|
||||
|
||||
### JavaScript/Fetch
|
||||
|
||||
```javascript
|
||||
const API_BASE = 'http://localhost:8000/api/v2/storage';
|
||||
|
||||
// 列出存储桶
|
||||
async function listBuckets() {
|
||||
const response = await fetch(`${API_BASE}/buckets`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 创建存储桶
|
||||
async function createBucket(bucketName) {
|
||||
const response = await fetch(`${API_BASE}/buckets`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ bucket_name: bucketName })
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 复制文件
|
||||
async function copyFile(sourcePath, destPath) {
|
||||
const response = await fetch(`${API_BASE}/files/copy`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
source_path: sourcePath,
|
||||
destination_path: destPath
|
||||
})
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 重命名文件
|
||||
async function renameFile(oldPath, newPath) {
|
||||
const response = await fetch(`${API_BASE}/files/move`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
source_path: oldPath,
|
||||
destination_path: newPath
|
||||
})
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
async function deleteFile(filePath) {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/files?file_path=${encodeURIComponent(filePath)}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
function downloadFile(filePath, filename) {
|
||||
const url = `${API_BASE}/files/download?file_path=${encodeURIComponent(filePath)}`;
|
||||
if (filename) {
|
||||
url += `&filename=${encodeURIComponent(filename)}`;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
// 获取文件列表
|
||||
async function listFiles(directory = '', recursive = false) {
|
||||
const params = new URLSearchParams({
|
||||
directory_path: directory,
|
||||
recursive: recursive.toString()
|
||||
});
|
||||
const response = await fetch(`${API_BASE}/files?${params}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 获取预签名URL
|
||||
async function getPresignedUrl(filePath, expires = 3600) {
|
||||
const params = new URLSearchParams({
|
||||
file_path: filePath,
|
||||
expires: expires.toString()
|
||||
});
|
||||
const response = await fetch(`${API_BASE}/files/presigned-url?${params}`);
|
||||
return response.json();
|
||||
}
|
||||
```
|
||||
|
||||
### Vue 3 组件示例
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="file-manager">
|
||||
<div class="file-list">
|
||||
<div v-for="file in files" :key="file" class="file-item">
|
||||
<span>{{ file }}</span>
|
||||
<div class="actions">
|
||||
<button @click="handleDownload(file)">下载</button>
|
||||
<button @click="handleRename(file)">重命名</button>
|
||||
<button @click="handleDelete(file)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
const API_BASE = '/api/v2/storage';
|
||||
const files = ref([]);
|
||||
|
||||
onMounted(async () => {
|
||||
await loadFiles();
|
||||
});
|
||||
|
||||
async function loadFiles() {
|
||||
const response = await fetch(`${API_BASE}/files?directory_path=documents&recursive=true`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
files.value = data.files;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownload(filePath) {
|
||||
window.open(`${API_BASE}/files/download?file_path=${encodeURIComponent(filePath)}`);
|
||||
}
|
||||
|
||||
async function handleRename(filePath) {
|
||||
const newName = prompt('请输入新文件名:');
|
||||
if (!newName) return;
|
||||
|
||||
const directory = filePath.substring(0, filePath.lastIndexOf('/'));
|
||||
const newPath = `${directory}/${newName}`;
|
||||
|
||||
const response = await fetch(`${API_BASE}/files/move`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
source_path: filePath,
|
||||
destination_path: newPath
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
await loadFiles();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(filePath) {
|
||||
if (!confirm(`确定删除 ${filePath}?`)) return;
|
||||
|
||||
const response = await fetch(
|
||||
`${API_BASE}/files?file_path=${encodeURIComponent(filePath)}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
await loadFiles();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
所有接口返回统一格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "错误描述信息"
|
||||
}
|
||||
```
|
||||
|
||||
### 常见错误
|
||||
|
||||
| 错误 | 说明 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| 存储桶不存在 | 操作的存储桶未找到 | 检查桶名是否正确 |
|
||||
| 文件不存在 | 操作的文件路径无效 | 检查文件路径 |
|
||||
| 存储桶非空 | 删除桶时桶内有文件 | 使用 force=true 强制删除 |
|
||||
| 权限不足 | MinIO 访问凭证问题 | 检查配置的 access_key/secret_key |
|
||||
|
||||
---
|
||||
|
||||
## 测试
|
||||
|
||||
使用测试脚本进行完整测试:
|
||||
|
||||
```bash
|
||||
python scripts/test_minio_api.py --host localhost --port 8000
|
||||
```
|
||||
Reference in New Issue
Block a user