Merge branch 'shiy' into awen

This commit is contained in:
2025-04-11 18:46:01 +08:00
18 changed files with 3298 additions and 371 deletions
+764
View File
@@ -0,0 +1,764 @@
import { postgrestGet, postgrestDelete, postgrestPost, postgrestPut, type PostgrestParams } from '../postgrest-client';
import dayjs from 'dayjs';
// 定义文档类型接口
export interface DocumentType {
id: number;
name: string;
description: string | null;
evaluation_point_groups_ids: number[]; // jsonb数组字段
prompt_config?: {
summary_template?: number;
llm_extract_template?: number;
vlm_extract_template?: number;
evaluation_template?: number;
execution_template?: number;
} | null;
created_at: string;
updated_at: string;
code?: string | null;
}
// 定义用于UI展示的文档类型接口
export interface DocumentTypeUI {
id: number;
name: string;
description: string;
groups: DocumentTypeGroup[];
llm_extraction_template_id?: number | null;
vlm_extraction_template_id?: number | null;
evaluation_template_id?: number | null;
summary_template_id?: number | null;
created_at: string;
updated_at: string;
code?: string | null;
}
// 文档类型创建接口
export interface DocumentTypeCreateDTO {
name: string;
description?: string;
group_ids: string[];
llm_extraction_template_id?: number | null;
vlm_extraction_template_id?: number | null;
evaluation_template_id?: number | null;
summary_template_id?: number | null;
code?: string | null;
}
// 文档类型更新接口
export interface DocumentTypeUpdateDTO extends DocumentTypeCreateDTO {
id: number;
}
// 文档类型分组关系
export interface DocumentTypeGroup {
id: string;
name: string;
}
// 搜索参数
export interface DocumentTypeSearchParams {
name?: string;
group_id?: string;
page?: number;
pageSize?: number;
}
/**
* 格式化日期
* @param dateString 日期字符串
* @returns 格式化后的日期字符串
*/
function formatDate(dateString: string): string {
if (!dateString) return '';
try {
return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss');
} catch (error) {
console.error('日期格式化失败:', error);
return dateString;
}
}
/**
* 从不同格式的 API 响应中提取数据
* @param responseData API 响应数据
* @returns 提取后的数据或 null
*/
function extractApiData<T>(responseData: unknown): T | null {
if (!responseData) return null;
// 格式1: { code: number, msg: string, data: T }
if (typeof responseData === 'object' && responseData !== null &&
'code' in responseData &&
'data' in responseData &&
(responseData as { data: unknown }).data) {
return (responseData as { data: T }).data;
}
// 格式2: 直接是数据对象
return responseData as T;
}
/**
* 获取所有评查点分组
* @returns 评查点分组列表
*/
export async function getAllEvaluationPointGroups(): Promise<{
data?: DocumentTypeGroup[];
error?: string;
status?: number;
}> {
try {
const params: PostgrestParams = {
select: 'id, name'
};
const response = await postgrestGet<Array<{
id: number;
name: string;
}>>('evaluation_point_groups', params);
if (response.error) {
return { error: response.error, status: response.status };
}
// 使用extractApiData提取数据
const extractedData = extractApiData<Array<{
id: number;
name: string;
}>>(response.data);
if (!extractedData) {
return { data: [] };
}
// 转换为DocumentTypeGroup格式
const groups: DocumentTypeGroup[] = extractedData.map(item => ({
id: item.id.toString(),
name: item.name
}));
return { data: groups };
} catch (error) {
console.error('获取所有评查点分组失败:', error);
return { error: error instanceof Error ? error.message : '获取所有评查点分组失败' };
}
}
/**
* 根据ID获取评查点分组信息
* @param ids 评查点分组ID数组
* @returns 评查点分组信息
*/
export async function getEvaluationPointGroupsByIds(ids: number[] | number): Promise<{
data?: DocumentTypeGroup[];
error?: string;
status?: number;
}> {
try {
// 确保ids是数组
if (!ids) {
return { data: [] };
}
// 将单个ID转换为数组
const idsArray = Array.isArray(ids) ? ids : [ids];
if (idsArray.length === 0) {
return { data: [] };
}
// console.log('获取评查点分组,ID类型:', typeof ids, '转换后的ID数组:', idsArray);
const params: PostgrestParams = {
select: 'id, name',
filter: {
'id': `in.(${idsArray.join(',')})`
}
};
// console.log('获取评查点分组,查询参数:', params);
const response = await postgrestGet<Array<{
id: number;
name: string;
}>>('evaluation_point_groups', params);
if (response.error) {
return { error: response.error, status: response.status };
}
// 使用extractApiData提取数据
const extractedData = extractApiData<Array<{
id: number;
name: string;
}>>(response.data);
if (!extractedData) {
return { data: [] };
}
// 转换为DocumentTypeGroup格式
const groups: DocumentTypeGroup[] = extractedData.map(item => ({
id: item.id.toString(),
name: item.name
}));
return { data: groups };
} catch (error) {
console.error('根据ID获取评查点分组失败:', error);
return { error: error instanceof Error ? error.message : '根据ID获取评查点分组失败' };
}
}
/**
* 获取文档类型列表
* @param searchParams 搜索参数
* @returns 文档类型列表和总数
*/
export async function getDocumentTypes(searchParams: DocumentTypeSearchParams = {}): Promise<{
data?: { types: DocumentTypeUI[], total: number };
error?: string;
status?: number;
}> {
try {
const page = searchParams.page || 1;
const pageSize = searchParams.pageSize || 10;
// 构建查询参数
const params: PostgrestParams = {
select: `
id,
name,
description,
evaluation_point_groups_ids,
prompt_config,
created_at,
updated_at,
code
`,
order: 'updated_at.desc',
headers: {
'Prefer': 'count=exact'
},
limit: pageSize,
offset: (page - 1) * pageSize,
filter: {} as Record<string, string>
};
// 添加筛选条件
const filter: Record<string, string> = {};
if (searchParams.name) {
filter['name'] = `ilike.%${searchParams.name}%`;
}
// 如果有分组ID筛选条件
if (searchParams.group_id) {
filter['evaluation_point_groups_ids'] = `cs.{${searchParams.group_id}}`;
}
params.filter = filter;
// console.log('获取文档类型列表,参数:', params);
const response = await postgrestGet<DocumentType[]>('document_types', params);
if (response.error) {
return { error: response.error, status: response.status };
}
// 使用extractApiData提取数据
const extractedData = extractApiData<DocumentType[]>(response.data);
const documentTypes = extractedData || [];
// console.log('提取的文档类型数据:', documentTypes);
// 为每个文档类型获取关联的分组
const typesWithGroups = await Promise.all(documentTypes.map(async (type) => {
// 获取文档类型关联的分组IDs
let groupIds: number[] = [];
try {
// 尝试解析evaluation_point_groups_ids
if (typeof type.evaluation_point_groups_ids === 'string') {
// 如果是JSON字符串,解析它
groupIds = JSON.parse(type.evaluation_point_groups_ids as unknown as string);
} else if (Array.isArray(type.evaluation_point_groups_ids)) {
// 如果已经是数组,直接使用
groupIds = type.evaluation_point_groups_ids;
} else if (type.evaluation_point_groups_ids) {
// 其他情况,尝试将其转换为数组
groupIds = [type.evaluation_point_groups_ids as unknown as number];
}
} catch (error) {
console.error('解析分组ID失败:', error, '原始值:', type.evaluation_point_groups_ids);
groupIds = [];
}
console.log(`文档类型 ${type.id} 的分组IDs:`, groupIds);
// 获取这些ID对应的分组信息
const groupsResponse = await getEvaluationPointGroupsByIds(groupIds);
// 返回包含分组信息的文档类型
return {
...type,
groups: groupsResponse.data || []
};
}));
// 转换为UI类型
const uiTypes = typesWithGroups.map(convertToUIDocumentType);
// 获取总数
let totalCount = 0;
const responseWithHeaders = response as {
data: unknown;
headers: Record<string, string>
};
if (responseWithHeaders.headers) {
const rangeHeader = responseWithHeaders.headers['content-range'];
if (rangeHeader) {
const total = rangeHeader.split('/')[1];
if (total !== '*') {
totalCount = parseInt(total, 10);
}
}
}
return {
data: {
types: uiTypes,
total: totalCount || uiTypes.length
}
};
} catch (error) {
console.error('获取文档类型列表失败:', error);
return {
error: error instanceof Error ? error.message : '获取文档类型列表失败',
status: 500
};
}
}
/**
* 删除文档类型
* @param id 文档类型ID
* @returns 删除结果
*/
export async function deleteDocumentType(id: string): Promise<{
success?: boolean;
error?: string;
status?: number;
}> {
try {
if (!id) {
return { error: '文档类型ID不能为空', status: 400 };
}
// 删除文档类型
const response = await postgrestDelete(
'document_types',
{
filter: {
'id': `eq.${id}`
}
}
);
if (response.error) {
return { error: response.error, status: response.status };
}
return { success: true };
} catch (error) {
console.error('删除文档类型失败:', error);
return {
error: error instanceof Error ? error.message : '删除文档类型失败',
status: 500
};
}
}
/**
* 将API返回的文档类型转换为UI文档类型
*/
function convertToUIDocumentType(type: DocumentType & { groups: DocumentTypeGroup[] }): DocumentTypeUI {
// 提取提示词模板ID,确保安全处理以避免控制台警告
let llmExtractionTemplateId: number | null = null;
let vlmExtractionTemplateId: number | null = null;
let evaluationTemplateId: number | null = null;
let summaryTemplateId: number | null = null;
// 安全地获取prompt_config字段
if (type.prompt_config) {
// 转换为字符串或保持为null
if (type.prompt_config.llm_extract_template !== undefined && type.prompt_config.llm_extract_template !== null) {
llmExtractionTemplateId = type.prompt_config.llm_extract_template;
}
if (type.prompt_config.vlm_extract_template !== undefined && type.prompt_config.vlm_extract_template !== null) {
vlmExtractionTemplateId = type.prompt_config.vlm_extract_template;
}
// 注意: 后端字段可能是 evaluation_template 或 execution_template
// 优先使用 evaluation_template,如果不存在则尝试使用 execution_template
if (type.prompt_config.evaluation_template !== undefined && type.prompt_config.evaluation_template !== null) {
evaluationTemplateId = type.prompt_config.evaluation_template;
} else if (type.prompt_config.execution_template !== undefined && type.prompt_config.execution_template !== null) {
evaluationTemplateId = type.prompt_config.execution_template;
}
if (type.prompt_config.summary_template !== undefined && type.prompt_config.summary_template !== null) {
summaryTemplateId = type.prompt_config.summary_template;
}
}
return {
id: type.id,
name: type.name,
description: type.description || '',
groups: type.groups || [],
llm_extraction_template_id: llmExtractionTemplateId,
vlm_extraction_template_id: vlmExtractionTemplateId,
evaluation_template_id: evaluationTemplateId,
summary_template_id: summaryTemplateId,
created_at: formatDate(type.created_at),
updated_at: formatDate(type.updated_at),
code: type.code
};
}
/**
* 获取文档类型详情
* @param id 文档类型ID
* @returns 文档类型详情
*/
export async function getDocumentType(id: string): Promise<{
data?: DocumentTypeUI;
error?: string;
status?: number;
}> {
try {
if (!id) {
return { error: '文档类型ID不能为空', status: 400 };
}
const params: PostgrestParams = {
select: `
id,
name,
description,
evaluation_point_groups_ids,
prompt_config,
created_at,
updated_at,
code
`,
filter: {
'id': `eq.${id}`
}
};
const response = await postgrestGet<DocumentType[]>('document_types', params);
if (response.error) {
return { error: response.error, status: response.status };
}
// 使用extractApiData提取数据
const extractedData = extractApiData<DocumentType[]>(response.data);
if (!extractedData || extractedData.length === 0) {
return { error: '未找到文档类型', status: 404 };
}
const documentType = extractedData[0];
// 获取关联分组
let groupIds: number[] = [];
try {
// 尝试解析evaluation_point_groups_ids
if (typeof documentType.evaluation_point_groups_ids === 'string') {
// 如果是JSON字符串,解析它
groupIds = JSON.parse(documentType.evaluation_point_groups_ids as unknown as string);
} else if (Array.isArray(documentType.evaluation_point_groups_ids)) {
// 如果已经是数组,直接使用
groupIds = documentType.evaluation_point_groups_ids;
} else if (documentType.evaluation_point_groups_ids) {
// 其他情况,尝试将其转换为数组
groupIds = [documentType.evaluation_point_groups_ids as unknown as number];
}
} catch (error) {
console.error('解析分组ID失败:', error, '原始值:', documentType.evaluation_point_groups_ids);
groupIds = [];
}
console.log(`文档类型 ${id} 的分组IDs:`, groupIds);
const groupsResponse = await getEvaluationPointGroupsByIds(groupIds);
if (groupsResponse.error) {
return { error: groupsResponse.error, status: 500 };
}
// 添加分组信息
const typeWithGroups = {
...documentType,
groups: groupsResponse.data || []
};
return { data: convertToUIDocumentType(typeWithGroups) };
} catch (error) {
console.error('获取文档类型详情失败:', error);
return {
error: error instanceof Error ? error.message : '获取文档类型详情失败',
status: 500
};
}
}
/**
* 创建文档类型
* @param documentType 文档类型数据
* @returns 创建结果
*/
export async function createDocumentType(documentType: DocumentTypeCreateDTO): Promise<{
data?: DocumentTypeUI;
error?: string;
status?: number;
}> {
try {
// 验证必填字段
if (!documentType.name) {
return { error: '文档类型名称不能为空', status: 400 };
}
if (!documentType.group_ids || documentType.group_ids.length === 0) {
return { error: '请至少选择一个关联的评查点分组', status: 400 };
}
// 目前因为关联操作是做单选的,所以传过来的拿第一个值即可。
const groupId = documentType.group_ids[0];
if (!groupId || isNaN(parseInt(groupId, 10))) {
return { error: '无效的评查点分组ID', status: 400 };
}
const groupIds = parseInt(groupId, 10); // 修改为数组形式
// const groupIds = [parseInt(groupId, 10)]; // 修改为数组形式
// 构建提示词配置 - 确保所有字段都有明确的设置
const promptConfig: Record<string, number | null> = {
llm_extract_template: null,
vlm_extract_template: null,
// evaluation_template: null,
execution_template: null,
summary_template: null
};
// 只有当ID存在且不为空字符串时才设置值
if (documentType.llm_extraction_template_id) {
const llmId = documentType.llm_extraction_template_id;
if (isNaN(llmId)) {
return { error: '无效的llm抽取提示词模板ID', status: 400 };
}
promptConfig.llm_extract_template = llmId;
}
if (documentType.vlm_extraction_template_id) {
const vlmId = documentType.vlm_extraction_template_id;
if (isNaN(vlmId)) {
return { error: '无效的vlm抽取提示词模板ID', status: 400 };
}
promptConfig.vlm_extract_template = vlmId;
}
if (documentType.evaluation_template_id) {
const evaluationId = documentType.evaluation_template_id;
if (isNaN(evaluationId)) {
return { error: '无效的评查提示词模板ID', status: 400 };
}
promptConfig.execution_template = evaluationId;
}
if (documentType.summary_template_id) {
const summaryId = documentType.summary_template_id;
if (isNaN(summaryId)) {
return { error: '无效的总结提示词模板ID', status: 400 };
}
promptConfig.summary_template = summaryId;
}
// 构建API请求数据 - 始终包含prompt_config对象
const apiDocumentType = {
name: documentType.name.trim(),
description: documentType.description || '',
evaluation_point_groups_ids: groupIds,
prompt_config: promptConfig,
code: documentType.code || null
};
// console.log('创建文档类型请求数据:', JSON.stringify(apiDocumentType, null, 2));
// console.log('创建文档类型请求数据:', apiDocumentType);
// if(apiDocumentType){
// throw new Error('测试错误');
// }
// 发送创建请求
const response = await postgrestPost<DocumentType, typeof apiDocumentType>(
'document_types',
apiDocumentType
);
if (response.error) {
console.error('创建文档类型API返回错误:', response.error, '状态码:', response.status);
return { error: response.error, status: response.status };
}
console.log('创建文档类型响应数据:', JSON.stringify(response.data, null, 2));
// 处理响应数据
const newDocumentType = extractApiData<DocumentType>(response.data);
if (!newDocumentType) {
return { error: '创建文档类型失败: 无法获取新创建的数据', status: 500 };
}
// 获取关联分组信息
const groupsResponse = await getEvaluationPointGroupsByIds(groupIds);
// 添加分组信息并转换为UI类型
const typeWithGroups = {
...newDocumentType,
groups: groupsResponse.data || []
};
return { data: convertToUIDocumentType(typeWithGroups) };
} catch (error) {
console.error('创建文档类型失败:', error);
return {
error: error instanceof Error ? error.message : '创建文档类型失败',
status: 500
};
}
}
/**
* 更新文档类型
* @param id 文档类型ID
* @param documentType 文档类型数据
* @returns 更新结果
*/
export async function updateDocumentType(id: string, documentType: DocumentTypeUpdateDTO): Promise<{
data?: DocumentTypeUI;
error?: string;
status?: number;
}> {
try {
// 验证必填字段
if (!id) {
return { error: '文档类型ID不能为空', status: 400 };
}
if (!documentType.name) {
return { error: '文档类型名称不能为空', status: 400 };
}
if (!documentType.group_ids || documentType.group_ids.length === 0) {
return { error: '请至少选择一个关联的评查点分组', status: 400 };
}
// 将分组ID转换为数字数组
const groupIds = documentType.group_ids.map(id => parseInt(id, 10));
// 构建提示词配置 - 始终创建一个包含所有字段的对象,并明确设置值
const promptConfig: Record<string, number | null> = {
llm_extract_template: null,
vlm_extract_template: null,
evaluation_template: null,
execution_template: null,
summary_template: null
};
// 只有当ID存在且不为空字符串时才设置值
if (documentType.llm_extraction_template_id) {
const llmId = documentType.llm_extraction_template_id;
if (isNaN(llmId)) {
return { error: '无效的llm抽取提示词模板ID', status: 400 };
}
promptConfig.llm_extract_template = llmId;
}
if (documentType.vlm_extraction_template_id) {
const vlmId = documentType.vlm_extraction_template_id;
if (isNaN(vlmId)) {
return { error: '无效的vlm抽取提示词模板ID', status: 400 };
}
promptConfig.vlm_extract_template = vlmId;
}
if (documentType.evaluation_template_id) {
const evaluationId = documentType.evaluation_template_id;
if (isNaN(evaluationId)) {
return { error: '无效的评查提示词模板ID', status: 400 };
}
promptConfig.execution_template = evaluationId;
}
if (documentType.summary_template_id) {
const summaryId = documentType.summary_template_id;
if (isNaN(summaryId)) {
return { error: '无效的总结提示词模板ID', status: 400 };
}
promptConfig.summary_template = summaryId;
}
// 构建API请求数据 - 始终包含prompt_config对象
const apiDocumentType = {
name: documentType.name.trim(),
description: documentType.description || '',
evaluation_point_groups_ids: groupIds,
prompt_config: promptConfig,
code: documentType.code || null
};
console.log('更新文档类型请求数据:', JSON.stringify(apiDocumentType, null, 2));
// 发送更新请求
const response = await postgrestPut<DocumentType, typeof apiDocumentType>(
'document_types',
apiDocumentType,
{id}
);
if (response.error) {
console.error('更新文档类型API返回错误:', response.error, '状态码:', response.status);
return { error: response.error, status: response.status };
}
console.log('更新文档类型响应数据:', JSON.stringify(response.data, null, 2));
// 处理响应数据
const updatedDocumentType = extractApiData<DocumentType>(response.data);
if (!updatedDocumentType) {
return { error: '更新文档类型失败: 无法获取更新后的数据', status: 500 };
}
// 获取关联分组信息
const groupsResponse = await getEvaluationPointGroupsByIds(groupIds);
// 添加分组信息并转换为UI类型
const typeWithGroups = {
...updatedDocumentType,
groups: groupsResponse.data || []
};
return { data: convertToUIDocumentType(typeWithGroups) };
} catch (error) {
console.error('更新文档类型失败:', error);
return {
error: error instanceof Error ? error.message : '更新文档类型失败',
status: 500
};
}
}
+318
View File
@@ -0,0 +1,318 @@
import { postgrestGet, postgrestDelete, type PostgrestParams } from '../postgrest-client';
import dayjs from 'dayjs';
import { getDocumentTypes } from '../document-types/document-types';
/**
* 从不同格式的 API 响应中提取数据
* @param responseData API 响应数据
* @returns 提取后的数据或 null
*/
function extractApiData<T>(responseData: unknown): T | null {
if (!responseData) return null;
// 格式1: { code: number, msg: string, data: T }
if (typeof responseData === 'object' && responseData !== null &&
'code' in responseData &&
'data' in responseData &&
(responseData as { data: unknown }).data) {
return (responseData as { data: T }).data;
}
// 格式2: 直接是数据对象
return responseData as T;
}
/**
* 格式化日期
* @param dateString 日期字符串
* @returns 格式化后的日期字符串
*/
function formatDate(dateString: string): string {
if (!dateString) return '';
try {
return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss');
} catch (error) {
console.error('日期格式化失败:', error);
return dateString;
}
}
/**
* 查询参数
*/
export interface DocumentSearchParams {
name?: string;
documentNumber?: string;
documentType?: string;
status?: string;
dateFrom?: string;
dateTo?: string;
page?: number;
pageSize?: number;
}
/**
* 数据库文档结构
*/
export interface Document {
id: number;
user_id: number | null;
type_id: number;
name: string;
document_number: string;
path: string;
storage_type: string;
file_size: number;
upload_time: string;
is_test_document: boolean;
evaluation_level: string;
status: 'pass' | 'warning' | 'waiting' | 'processing' | 'fail';
ocr_result: unknown;
extracted_results: unknown;
summary: unknown;
remark: string;
created_at: string;
updated_at: string;
}
/**
* 前端UI文档结构
*/
export interface DocumentUI {
id: number;
name: string;
documentNumber: string;
type: string;
typeName: string;
size: number;
status: string;
issues: number | null;
uploadTime: string;
fileType: string;
path: string;
isTest: boolean;
}
/**
* 获取文件扩展名
* @param filename 文件名
* @returns 文件扩展名
*/
function getFileExtension(filename: string): string {
const parts = filename.split('.');
return parts.length > 1 ? parts.pop()?.toLowerCase() || '' : '';
}
/**
* 将API文档转换为UI文档
*/
async function convertToUIDocument(doc: Document): Promise<DocumentUI> {
// 获取文档类型信息
const typeResponse = await getDocumentTypes();
const documentTypes = typeResponse.data?.types || [];
const docType = documentTypes.find(type => type.id.toString() === doc.type_id.toString());
return {
id: doc.id,
name: doc.name,
documentNumber: doc.document_number,
type: doc.type_id.toString(),
typeName: docType?.name || '未知类型',
size: doc.file_size,
status: doc.status,
issues: 0, // 固定为0
uploadTime: formatDate(doc.updated_at),
fileType: getFileExtension(doc.name),
path: doc.path,
isTest: doc.is_test_document
};
}
/**
* 获取文档列表
* @param searchParams 搜索参数
* @returns 文档列表和总数
*/
export async function getDocuments(searchParams: DocumentSearchParams = {}): Promise<{
data?: { documents: DocumentUI[], total: number };
error?: string;
status?: number;
}> {
try {
const page = searchParams.page || 1;
const pageSize = searchParams.pageSize || 10;
// 构建查询参数
const params: PostgrestParams = {
select: '*',
order: 'updated_at.desc',
headers: {
'Prefer': 'count=exact'
},
limit: pageSize,
offset: (page - 1) * pageSize,
filter: {} as Record<string, string>
};
// 添加筛选条件
const filter: Record<string, string> = {};
if (searchParams.name) {
filter['name'] = `ilike.%${searchParams.name}%`;
}
if (searchParams.documentNumber) {
filter['document_number'] = `ilike.%${searchParams.documentNumber}%`;
}
if (searchParams.documentType) {
filter['type_id'] = `eq.${searchParams.documentType}`;
}
if (searchParams.status) {
filter['status'] = `eq.${searchParams.status}`;
}
// 处理日期范围
if (searchParams.dateFrom) {
filter['updated_at'] = `gte.${searchParams.dateFrom}`;
}
if (searchParams.dateTo) {
const dateToKey = searchParams.dateFrom ? 'and.updated_at.lte' : 'updated_at';
filter[dateToKey] = `lte.${searchParams.dateTo}`;
}
params.filter = filter;
// 发送请求
const response = await postgrestGet<Document[]>('documents', params);
if (response.error) {
return { error: response.error, status: response.status };
}
// 提取数据
const extractedData = extractApiData<Document[]>(response.data);
if (!extractedData) {
return { error: '获取文档数据失败', status: 500 };
}
// 转换为UI格式
const documents = await Promise.all(extractedData.map(convertToUIDocument));
// 获取总数
let totalCount = 0;
const responseWithHeaders = response as {
data: unknown;
headers?: Record<string, string>
};
if (responseWithHeaders.headers) {
const rangeHeader = responseWithHeaders.headers['content-range'];
if (rangeHeader) {
const total = rangeHeader.split('/')[1];
if (total !== '*') {
totalCount = parseInt(total, 10);
}
}
}
return {
data: {
documents,
total: totalCount || documents.length
}
};
} catch (error) {
console.error('获取文档列表失败:', error);
return {
error: error instanceof Error ? error.message : '获取文档列表失败',
status: 500
};
}
}
/**
* 删除文档
* @param id 文档ID
* @returns 删除结果
*/
export async function deleteDocument(id: string): Promise<{
success?: boolean;
error?: string;
status?: number;
}> {
try {
if (!id) {
return { error: '文档ID不能为空', status: 400 };
}
const response = await postgrestDelete(
'documents',
{
filter: {
'id': `eq.${id}`
}
}
);
if (response.error) {
return { error: response.error, status: response.status };
}
return { success: true };
} catch (error) {
console.error('删除文档失败:', error);
return {
error: error instanceof Error ? error.message : '删除文档失败',
status: 500
};
}
}
/**
* 获取单个文档详情
* @param id 文档ID
* @returns 文档详情
*/
export async function getDocument(id: string): Promise<{
data?: DocumentUI;
error?: string;
status?: number;
}> {
try {
if (!id) {
return { error: '文档ID不能为空', status: 400 };
}
const response = await postgrestGet<Document[]>(
'documents',
{
filter: {
'id': `eq.${id}`
},
limit: 1
}
);
if (response.error) {
return { error: response.error, status: response.status };
}
const extractedData = extractApiData<Document[]>(response.data);
if (!extractedData || extractedData.length === 0) {
return { error: '文档不存在', status: 404 };
}
const documentUI = await convertToUIDocument(extractedData[0]);
return { data: documentUI };
} catch (error) {
console.error('获取文档详情失败:', error);
return {
error: error instanceof Error ? error.message : '获取文档详情失败',
status: 500
};
}
}
+6
View File
@@ -64,16 +64,22 @@ export async function getDocumentType(id: string): Promise<DocumentType> {
} }
// 创建文档类型 // 创建文档类型
// 请使用 ~/api/document-types/document-types.ts 中的实现
/*
export async function createDocumentType(documentType: Omit<DocumentType, 'id' | 'createdAt' | 'updatedAt'>): Promise<DocumentType> { export async function createDocumentType(documentType: Omit<DocumentType, 'id' | 'createdAt' | 'updatedAt'>): Promise<DocumentType> {
const url = buildUrl('/api/document-types'); const url = buildUrl('/api/document-types');
return apiRequest<DocumentType>(url, 'POST', documentType); return apiRequest<DocumentType>(url, 'POST', documentType);
} }
*/
// 更新文档类型 // 更新文档类型
// 请使用 ~/api/document-types/document-types.ts 中的实现
/*
export async function updateDocumentType(id: string, documentType: Partial<Omit<DocumentType, 'id' | 'createdAt' | 'updatedAt'>>): Promise<DocumentType> { export async function updateDocumentType(id: string, documentType: Partial<Omit<DocumentType, 'id' | 'createdAt' | 'updatedAt'>>): Promise<DocumentType> {
const url = buildUrl(`/api/document-types/${id}`); const url = buildUrl(`/api/document-types/${id}`);
return apiRequest<DocumentType>(url, 'PUT', documentType); return apiRequest<DocumentType>(url, 'PUT', documentType);
} }
*/
// 删除文档类型 // 删除文档类型
export async function deleteDocumentType(id: string): Promise<void> { export async function deleteDocumentType(id: string): Promise<void> {
+3 -3
View File
@@ -20,7 +20,7 @@ export interface PromptTemplate {
export interface PromptTemplateUI { export interface PromptTemplateUI {
id: string; id: string;
template_name: string; template_name: string;
template_type: 'Extraction' | 'Evaluation' | 'Summary' | 'Common'; template_type: 'LLM_Extraction' | 'VLM_Extraction' | 'Evaluation' | 'Summary' | 'Common';
description: string; description: string;
template_content: string; template_content: string;
variables: Record<string, string>; // 变量定义 variables: Record<string, string>; // 变量定义
@@ -112,7 +112,7 @@ export function convertToUITemplate(template: PromptTemplate): PromptTemplateUI
return { return {
id: template.id ? template.id.toString() : '', id: template.id ? template.id.toString() : '',
template_name: template.template_name, template_name: template.template_name,
template_type: template.template_type as "Extraction" | "Evaluation" | "Summary" | "Common", template_type: template.template_type as "LLM_Extraction" | "VLM_Extraction" | "Evaluation" | "Summary" | "Common",
description: template.description || '', description: template.description || '',
template_content: template.template_content, template_content: template.template_content,
variables: template.variables, variables: template.variables,
@@ -207,7 +207,7 @@ export async function getPromptTemplates(searchParams: PromptSearchParams = {}):
return { error: '获取提示词模板数据失败', status: 500 }; return { error: '获取提示词模板数据失败', status: 500 };
} }
console.log(`成功获取${extractedData.length}条提示词模板数据`); // console.log(`成功获取${extractedData.length}条提示词模板数据`);
// 从响应头中获取总数 // 从响应头中获取总数
let totalCount = 0; let totalCount = 0;
+7 -1
View File
@@ -49,6 +49,12 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
path:'/documents', path:'/documents',
icon:'ri-file-list-3-line' icon:'ri-file-list-3-line'
} }
// {
// id:'documents-edit',
// title:'文档编辑',
// path:'/documents/edit',
// icon:'ri-file-edit-line'
// }
] ]
}, },
{ {
@@ -110,7 +116,7 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
{ {
id: 'document-types', id: 'document-types',
title: '文档类型', title: '文档类型',
path: '/doc-types', path: '/document-types',
icon: 'ri-file-list-line' icon: 'ri-file-list-line'
}, },
{ {
+32 -1
View File
@@ -14,6 +14,7 @@ interface FileTypeTagProps {
className?: string; className?: string;
size?: 'default' | 'sm' | 'lg'; size?: 'default' | 'sm' | 'lg';
showIcon?: boolean; showIcon?: boolean;
fileType?: string;
} }
export function links() { export function links() {
@@ -28,13 +29,15 @@ export function links() {
* @param className 额外的类名 * @param className 额外的类名
* @param size 尺寸:default, sm, lg * @param size 尺寸:default, sm, lg
* @param showIcon 是否显示图标,默认为true * @param showIcon 是否显示图标,默认为true
* @param fileType 文件类型,不提供则使用文档类型决定样式
*/ */
export function FileTypeTag({ export function FileTypeTag({
type, type,
text, text,
className = '', className = '',
size = 'default', size = 'default',
showIcon = true showIcon = true,
fileType
}: FileTypeTagProps) { }: FileTypeTagProps) {
// 文档类型对应的图标 // 文档类型对应的图标
const getTypeIcon = () => { const getTypeIcon = () => {
@@ -64,9 +67,37 @@ export function FileTypeTag({
// 获取文档类型对应的类名 // 获取文档类型对应的类名
const getTypeClass = () => { const getTypeClass = () => {
// 如果有文件类型,优先使用文件类型决定样式
if (fileType) {
const fileTypeClass = getFileTypeClass(fileType);
if (fileTypeClass) {
return `file-type-tag ${fileTypeClass}`;
}
}
return `file-type-tag file-type-${type}`; return `file-type-tag file-type-${type}`;
}; };
// 根据文件扩展名获取对应的样式类名
const getFileTypeClass = (ext: string) => {
ext = ext.toLowerCase();
if (ext === 'pdf') {
return 'file-type-tag-pdf';
} else if (ext === 'doc' || ext === 'docx') {
return 'file-type-tag-doc';
} else if (ext === 'xls' || ext === 'xlsx') {
return 'file-type-tag-xls';
} else if (ext === 'ppt' || ext === 'pptx') {
return 'file-type-tag-ppt';
} else if (ext === 'zip' || ext === 'rar') {
return 'file-type-tag-zip';
} else if (ext === 'jpg' || ext === 'jpeg' || ext === 'png' || ext === 'gif') {
return 'file-type-tag-img';
} else if (ext === 'txt') {
return 'file-type-tag-txt';
}
return null;
};
// 获取尺寸类名 // 获取尺寸类名
const getSizeClass = () => { const getSizeClass = () => {
return size !== 'default' ? `file-type-tag-${size}` : ''; return size !== 'default' ? `file-type-tag-${size}` : '';
+405
View File
@@ -0,0 +1,405 @@
import { useState } from "react";
import { useSearchParams, useNavigate, useLoaderData } from "@remix-run/react";
import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction, json } from "@remix-run/node";
import { Table } from "~/components/ui/Table";
import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button";
import { Pagination } from "~/components/ui/Pagination";
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
import {
getDocumentTypes,
deleteDocumentType,
getAllEvaluationPointGroups,
type DocumentTypeUI,
type DocumentTypeSearchParams,
type DocumentTypeGroup
} from "~/api/document-types/document-types";
import documentTypesStyles from "~/styles/pages/document-types_index.css?url";
// 引入CSS样式
export function links() {
return [
{ rel: "stylesheet", href: documentTypesStyles }
];
}
// 页面元数据
export const meta: MetaFunction = () => {
return [
{ title: "文档类型管理 - 中国烟草AI合同及卷宗审核系统" },
{ name: "description", content: "管理文档类型,包括查看、编辑和删除文档类型" },
];
};
// 定义加载器返回的数据类型
interface LoaderData {
types: DocumentTypeUI[];
total: number;
pageSize: number;
currentPage: number;
error?: string;
groups: DocumentTypeGroup[];
}
// 加载函数 - 获取文档类型列表
export async function loader({ request }: LoaderFunctionArgs) {
try {
const url = new URL(request.url);
const name = url.searchParams.get('name') || undefined;
const group_id = url.searchParams.get('group_id') || undefined;
const page = parseInt(url.searchParams.get('page') || '1', 10);
const pageSize = parseInt(url.searchParams.get('pageSize') || '10', 10);
// 构建搜索参数
const searchParams: DocumentTypeSearchParams = {
name,
group_id,
page,
pageSize
};
// 并行获取文档类型数据和所有评查点分组
const [typesResult, groupsResult] = await Promise.all([
getDocumentTypes(searchParams),
getAllEvaluationPointGroups()
]);
// console.log('文档类型数据:', typesResult.data?.types);
// console.log('评查点分组数据:', groupsResult.data);
if (typesResult.error) {
return json<LoaderData>(
{
types: [],
total: 0,
pageSize,
currentPage: page,
error: typesResult.error,
groups: groupsResult.data || []
},
{ status: typesResult.status || 500 }
);
}
return json<LoaderData>({
types: typesResult.data?.types || [],
total: typesResult.data?.total || 0,
pageSize,
currentPage: page,
groups: groupsResult.data || []
});
} catch (error) {
console.error("加载文档类型列表失败:", error);
return json<LoaderData>(
{
types: [],
total: 0,
pageSize: 10,
currentPage: 1,
error: "加载文档类型列表失败",
groups: []
},
{ status: 500 }
);
}
}
// 动作函数 - 处理删除请求
export async function action({ request }: ActionFunctionArgs) {
// 获取表单数据
const formData = await request.formData();
const id = formData.get("id") as string;
const intent = formData.get("intent") as string;
if (intent === "delete" && id) {
try {
const result = await deleteDocumentType(id);
if (result.error) {
return Response.json({ success: false, error: result.error }, { status: result.status || 500 });
}
return Response.json({ success: true });
} catch (error) {
return Response.json(
{ success: false, error: error instanceof Error ? error.message : "删除文档类型失败" },
{ status: 500 }
);
}
}
return Response.json({ success: false, error: "无效的操作" }, { status: 400 });
}
// 文档类型列表组件
export default function DocumentTypesList() {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const [isDeleting, setIsDeleting] = useState(false);
// 获取加载器数据
const { types, total, pageSize: initialPageSize, currentPage: initialPage, error, groups } = useLoaderData<LoaderData>();
// 获取搜索参数
const name = searchParams.get('name') || '';
const group_id = searchParams.get('group_id') || '';
const currentPage = parseInt(searchParams.get('page') || String(initialPage), 10);
const pageSize = parseInt(searchParams.get('pageSize') || String(initialPageSize), 10);
// 处理名称搜索
const handleNameSearch = (value: string) => {
const newParams = new URLSearchParams(searchParams);
if (value) {
newParams.set('name', value);
} else {
newParams.delete('name');
}
newParams.set('page', '1');
setSearchParams(newParams);
};
// 处理分组筛选
const handleGroupChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const { value } = e.target;
const newParams = new URLSearchParams(searchParams);
if (value) {
newParams.set('group_id', value);
} else {
newParams.delete('group_id');
}
newParams.set('page', '1');
setSearchParams(newParams);
};
// 处理重置筛选
const handleReset = () => {
const nameInput = document.querySelector('input[name="name"]');
if (nameInput) {
(nameInput as HTMLInputElement).value = '';
}
const groupIdInput = document.querySelector('select[name="group_id"]');
if (groupIdInput) {
(groupIdInput as HTMLSelectElement).value = '';
}
setSearchParams(new URLSearchParams());
};
// 处理删除文档类型
const handleDelete = async (id: number) => {
if (confirm('确定要删除该文档类型吗?此操作不会影响关联的评查点分组,但可能会影响使用该类型的文档评查。')) {
setIsDeleting(true);
try {
const formData = new FormData();
formData.append('id', id.toString());
formData.append('intent', 'delete');
const response = await fetch('/document-types?index', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
alert('删除成功!');
// 刷新页面
window.location.reload();
} else {
alert(`删除失败: ${result.error || '未知错误'}`);
}
} catch (error) {
alert(`删除失败: ${error instanceof Error ? error.message : '未知错误'}`);
} finally {
setIsDeleting(false);
}
}
};
// 处理编辑文档类型
const handleEdit = (id: number) => {
navigate(`/document-types/new?id=${id}`);
};
// 处理分页变更
const handlePageChange = (page: number) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('page', page.toString());
setSearchParams(newParams);
};
// 处理每页条数变更
const handlePageSizeChange = (size: number) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('pageSize', size.toString());
newParams.set('page', '1');
setSearchParams(newParams);
};
// 定义表格列配置
const columns = [
{
title: "文档类型名称",
key: "name",
width: "200px",
render: (_: unknown, record: DocumentTypeUI) => (
<div className="flex items-center">
<i className="ri-file-text-line text-primary mr-2"></i>
<span>{record.name}</span>
</div>
)
},
{
title: "描述",
key: "description",
width: "300px",
render: (_: unknown, record: DocumentTypeUI) => (
<div className="text-secondary text-sm truncate max-w-xs" title={record.description}>
{record.description}
</div>
)
},
{
title: "关联的评查点分组",
key: "groups",
render: (_: unknown, record: DocumentTypeUI) => (
<div className="groups-container">
{record.groups && record.groups.length > 0 ? (
record.groups.map(group => (
<span key={group.id} className="type-badge">
{group.name}
</span>
))
) : (
<span className="text-gray-400"></span>
)}
</div>
)
},
{
title: "创建时间",
key: "created_at",
width: "150px",
render: (_: unknown, record: DocumentTypeUI) => record.created_at
},
{
title: "更新时间",
key: "updated_at",
width: "150px",
render: (_: unknown, record: DocumentTypeUI) => record.updated_at
},
{
title: "操作",
key: "operation",
width: "150px",
render: (_: unknown, record: DocumentTypeUI) => (
<>
<button
className="operation-btn text-primary"
onClick={() => handleEdit(record.id)}
>
<i className="ri-edit-line"></i>
</button>
<button
className="operation-btn text-error !hidden"
onClick={() => handleDelete(record.id)}
disabled={isDeleting}
>
<i className="ri-delete-bin-line"></i>
</button>
</>
)
}
];
return (
<div className="document-types-page">
{error && (
<div className="alert alert-error mb-4">
<p>{error}</p>
</div>
)}
{/* 页面头部 */}
<div className="page-header">
<h2 className="page-title"></h2>
<div>
<Button
type="primary"
icon="ri-add-line"
onClick={() => navigate("/document-types/new")}
>
</Button>
</div>
</div>
{/* 搜索栏 */}
<FilterPanel
className="mb-4"
actions={
<>
<Button
type="default"
icon="ri-refresh-line"
onClick={handleReset}
className="mr-2"
>
</Button>
</>
}
noActionDivider={true}
>
<SearchFilter
label="类型名称"
placeholder="请输入文档类型名称"
value={name}
onSearch={handleNameSearch}
className="flex-1 min-w-[200px]"
instantSearch={true}
/>
<FilterSelect
label="关联分组"
name="group_id"
value={group_id}
options={[
...(groups || []).map(group => ({
value: group.id,
label: group.name
}))
]}
onChange={handleGroupChange}
className="flex-1 min-w-[200px]"
/>
</FilterPanel>
{/* 数据表格 */}
<Card bodyClassName="px-0 py-0">
<Table
columns={columns}
dataSource={types}
rowKey="id"
emptyText="暂无文档类型数据"
loading={false}
/>
{/* 分页 */}
<div className="px-4 py-3">
<Pagination
currentPage={currentPage}
total={total}
pageSize={pageSize}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[10, 20, 50, 100]}
/>
</div>
</Card>
</div>
);
}
+614
View File
@@ -0,0 +1,614 @@
import React, { useState, useEffect } from "react";
import { Form, useActionData, useLoaderData, useNavigate, useSearchParams } from "@remix-run/react";
import { redirect, type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button";
import documentTypesNewStyles from "~/styles/pages/document-types_new.css?url";
import { getAllRuleGroups, type RuleGroup } from "~/api/evaluation_points/rule-groups";
import { getDocumentType, createDocumentType, updateDocumentType } from "~/api/document-types/document-types";
import { getPromptTemplates, type PromptTemplateUI } from "~/api/prompts/prompts";
export function links() {
return [{ rel: "stylesheet", href: documentTypesNewStyles }];
}
export const handle = {
breadcrumb: (data:LoaderData) => {
if (data.isEdit) {
return "编辑文档类型";
} else {
return "新增文档类型";
}
}
};
interface LoaderData {
isEdit: boolean;
}
export const meta: MetaFunction = ({ location }) => {
const isEdit = new URLSearchParams(location.search).has("id");
return [
{ title: `${isEdit ? "编辑" : "新增"}文档类型 - 中国烟草AI合同及卷宗审核系统` },
{ name: "description", content: `${isEdit ? "编辑" : "新增"}文档类型,设置文档类型名称、描述和关联的评查点分组` }
];
};
// 定义模板类型
const TEMPLATE_TYPES = {
LLM_EXTRACTION: "LLM_Extraction",
VLM_EXTRACTION: "VLM_Extraction",
EVALUATION: "Evaluation",
SUMMARY: "Summary"
};
// 加载函数 - 获取数据
export async function loader({ request }: LoaderFunctionArgs) {
try {
const url = new URL(request.url);
const id = url.searchParams.get("id");
const isEdit = id ? true : false;
// 1. 获取评查点分组 - 使用getAllRuleGroups获取所有分组
const ruleGroupsResponse = await getAllRuleGroups();
if (ruleGroupsResponse.error) {
console.error("获取评查点分组失败:", ruleGroupsResponse.error);
}
// ruleGroupsResponse.data已经是树形结构数据,getAllRuleGroups内部已处理好parent-children关系
const groupsTree = ruleGroupsResponse.error ? [] : ruleGroupsResponse.data || [];
// 2. 获取各类型的提示词模板
const [llmExtractionTemplatesResponse, vlmExtractionTemplatesResponse, evaluationTemplatesResponse, summaryTemplatesResponse] =
await Promise.all([
getPromptTemplates({ type: TEMPLATE_TYPES.LLM_EXTRACTION }),
getPromptTemplates({ type: TEMPLATE_TYPES.VLM_EXTRACTION }),
getPromptTemplates({ type: TEMPLATE_TYPES.EVALUATION }),
getPromptTemplates({ type: TEMPLATE_TYPES.SUMMARY })
]);
// 3. 如果是编辑模式,获取文档类型详情
let documentType = undefined;
if (id) {
const typeResponse = await getDocumentType(id);
if (typeResponse.data) {
documentType = typeResponse.data;
}
}
return Response.json({
isEdit,
documentType,
ruleGroups: groupsTree,
llmExtractionTemplates: llmExtractionTemplatesResponse.data?.templates || [],
vlmExtractionTemplates: vlmExtractionTemplatesResponse.data?.templates || [],
evaluationTemplates: evaluationTemplatesResponse.data?.templates || [],
summaryTemplates: summaryTemplatesResponse.data?.templates || []
});
} catch (error) {
console.error("加载数据失败:", error);
return Response.json({
isEdit: false,
documentType: undefined,
ruleGroups: [],
llmExtractionTemplates: [],
vlmExtractionTemplates: [],
evaluationTemplates: [],
summaryTemplates: []
});
}
}
// 定义动作返回的数据类型
interface ActionData {
success?: boolean;
errors?: {
name?: string;
groups?: string;
general?: string;
llmExtractionTemplate?: string;
vlmExtractionTemplate?: string;
evaluationTemplate?: string;
summaryTemplate?: string;
};
}
// 动作函数 - 处理表单提交
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const id = formData.get("id") as string | null;
const name = formData.get("name") as string;
const description = formData.get("description") as string;
const llmExtractionTemplateId = formData.get("llm_extraction_template") as string;
const vlmExtractionTemplateId = formData.get("vlm_extraction_template") as string;
const evaluationTemplateId = formData.get("evaluation_template") as string;
const summaryTemplateId = formData.get("summary_template") as string;
// 获取选中的评查点分组ID列表
const selectedGroups = formData.getAll("checkpoint_group_ids") as string[];
// 表单验证
const errors: ActionData["errors"] = {};
// 收集所有错误
if (!name || name.trim() === "") {
errors.name = "文档类型名称不能为空";
}
if (selectedGroups.length === 0) {
errors.groups = "请至少选择一个关联的评查点分组";
}
if (!llmExtractionTemplateId) {
errors.llmExtractionTemplate = "请选择llm抽取提示词模板";
}
if (!vlmExtractionTemplateId) {
errors.vlmExtractionTemplate = "请选择vlm抽取提示词模板";
}
if (!evaluationTemplateId) {
errors.evaluationTemplate = "请选择评查提示词模板";
}
if (!summaryTemplateId) {
errors.summaryTemplate = "请选择总结提示词模板";
}
// 如果有错误,返回错误信息
if (Object.keys(errors).length > 0) {
return Response.json({ errors });
}
try {
// 构建文档类型数据
const documentTypeData = {
name,
description,
group_ids: selectedGroups,
// 确保映射关系与prompt_config字段对应正确
llm_extraction_template_id: llmExtractionTemplateId ? parseInt(llmExtractionTemplateId) : null,
vlm_extraction_template_id: vlmExtractionTemplateId ? parseInt(vlmExtractionTemplateId) : null,
evaluation_template_id: evaluationTemplateId ? parseInt(evaluationTemplateId) : null,
summary_template_id: summaryTemplateId ? parseInt(summaryTemplateId) : null
};
// 调用API创建或更新文档类型
let response;
if (id) {
// 更新文档类型
response = await updateDocumentType(id, {
...documentTypeData,
id: parseInt(id)
});
} else {
// 创建新文档类型
response = await createDocumentType(documentTypeData);
}
if (response.error) {
console.error("保存/更新文档类型失败:", response.error);
throw new Error(response.error);
}
// 操作成功,重定向到列表页
return redirect("/document-types");
} catch (error) {
console.error("保存文档类型失败:", error);
return Response.json({
success: false,
errors: {
general: error instanceof Error ? error.message : "保存文档类型失败"
}
});
}
}
export default function DocumentTypeNew() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const isEditMode = searchParams.has("id");
const {
documentType,
ruleGroups,
llmExtractionTemplates,
vlmExtractionTemplates,
evaluationTemplates,
summaryTemplates
} = useLoaderData<typeof loader>();
const actionData = useActionData<ActionData>();
// 状态管理
const [formData, setFormData] = useState({
id: documentType?.id || "",
name: documentType?.name || "",
description: documentType?.description || "",
llmExtractionTemplateId: documentType?.llm_extraction_template_id || "",
vlmExtractionTemplateId: documentType?.vlm_extraction_template_id || "",
evaluationTemplateId: documentType?.evaluation_template_id || "",
summaryTemplateId: documentType?.summary_template_id || "",
selectedGroups: documentType?.groups?.map((g: { id: string }) => g.id) || []
});
// 添加本地验证错误状态
const [localErrors, setLocalErrors] = useState<ActionData["errors"]>({} as ActionData["errors"]);
// 从actionData初始化本地错误
useEffect(() => {
if (actionData?.errors) {
setLocalErrors(actionData.errors);
}
}, [actionData]);
// 分组展开状态
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
// 当文档类型数据加载完成时更新表单
useEffect(() => {
if (documentType) {
setFormData({
id: documentType.id,
name: documentType.name,
description: documentType.description,
llmExtractionTemplateId: documentType.llm_extraction_template_id || "",
vlmExtractionTemplateId: documentType.vlm_extraction_template_id || "",
evaluationTemplateId: documentType.evaluation_template_id || "",
summaryTemplateId: documentType.summary_template_id || "",
selectedGroups: documentType.groups.map((g: { id: string }) => g.id)
});
// 初始化展开状态 - 对于有选中子分组的父分组,默认展开
const newExpandedGroups: Record<string, boolean> = {};
ruleGroups.forEach((parentGroup: RuleGroup) => {
// 如果父分组被选中或者有子分组被选中,则展开
const isParentSelected = documentType.groups.some((g: { id: string }) => g.id === parentGroup.id);
const hasSelectedChild = parentGroup.children &&
parentGroup.children.some(child =>
documentType.groups.some((g: { id: string }) => g.id === child.id)
);
if (isParentSelected || hasSelectedChild) {
newExpandedGroups[parentGroup.id] = true;
}
});
setExpandedGroups(newExpandedGroups);
}
}, [documentType, ruleGroups]);
// 处理分组勾选
const handleGroupCheckChange = (
groupId: string,
isChecked: boolean
) => {
// 单选模式:清空之前所有选择,只保留当前选中的
let newSelectedGroups: string[] = [];
if (isChecked) {
// 只添加当前选中的分组
newSelectedGroups = [groupId];
// 如果选择的是父分组,不自动选择子分组
// 如果选择的是子分组,不影响父分组状态
}
// 如果取消选中,则清空选择(在单选模式下可能不需要,但保留逻辑以防万一)
setFormData(prev => ({ ...prev, selectedGroups: newSelectedGroups }));
// 清除groups相关的错误
if (localErrors?.groups) {
setLocalErrors(prev => ({...prev, groups: undefined}));
}
};
// 修复展开/折叠功能
const handleGroupExpand = (groupId: string, event: React.MouseEvent) => {
// 阻止事件冒泡,避免触发checkbox选中
event.stopPropagation();
setExpandedGroups(prev => ({
...prev,
[groupId]: !prev[groupId]
}));
};
// 处理表单输入变化
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
// 根据name属性映射到对应的formData字段
if (name === 'llm_extraction_template') {
setFormData(prev => ({ ...prev, llmExtractionTemplateId: value }));
// 清除相关错误
if (localErrors?.llmExtractionTemplate) {
setLocalErrors(prev => ({...prev, llmExtractionTemplate: undefined}));
}
} else if (name === 'vlm_extraction_template') {
setFormData(prev => ({ ...prev, vlmExtractionTemplateId: value }));
// 清除相关错误
if (localErrors?.vlmExtractionTemplate) {
setLocalErrors(prev => ({...prev, vlmExtractionTemplate: undefined}));
}
} else if (name === 'evaluation_template') {
setFormData(prev => ({ ...prev, evaluationTemplateId: value }));
// 清除相关错误
if (localErrors?.evaluationTemplate) {
setLocalErrors(prev => ({...prev, evaluationTemplate: undefined}));
}
} else if (name === 'summary_template') {
setFormData(prev => ({ ...prev, summaryTemplateId: value }));
// 清除相关错误
if (localErrors?.summaryTemplate) {
setLocalErrors(prev => ({...prev, summaryTemplate: undefined}));
}
} else if (name === 'name') {
setFormData(prev => ({ ...prev, [name]: value }));
// 清除相关错误
if (localErrors?.name) {
setLocalErrors(prev => ({...prev, name: undefined}));
}
} else {
// 其他表单字段(description等)
setFormData(prev => ({ ...prev, [name]: value }));
}
};
return (
<div className="document-type-new-page">
{/* 页面头部 */}
<div className="page-header">
<h2 className="page-title">{isEditMode ? "编辑文档类型" : "新增文档类型"}</h2>
<div>
<Button
type="default"
icon="ri-arrow-left-line"
onClick={() => navigate("/document-types")}
className="mr-2"
>
</Button>
<Button
type="primary"
icon="ri-save-line"
form="type-form"
>
</Button>
</div>
</div>
{/* 表单内容 */}
<Card>
<Form id="type-form" method="post" noValidate>
{/* 如果是编辑模式,添加隐藏的ID字段 */}
{formData.id && <input type="hidden" name="id" value={formData.id} />}
<div className="grid grid-cols-1 gap-6">
{/* 错误提示 */}
{localErrors?.general && (
<div className="error-message general-error error-show">
<i className="ri-error-warning-line"></i>
{localErrors.general}
</div>
)}
{/* 文档类型名称 */}
<div className="form-group">
<label htmlFor="type-name" className="form-label">
<span className="text-red-500">*</span>
</label>
<input
type="text"
id="type-name"
name="name"
className={`form-input ${localErrors?.name ? 'input-error' : ''}`}
placeholder="请输入文档类型名称"
value={formData.name}
onChange={handleInputChange}
required
/>
<div className="form-tip"></div>
{localErrors?.name && (
<div className="error-message error-show">{localErrors.name}</div>
)}
</div>
{/* 类型描述 */}
<div className="form-group">
<label htmlFor="type-description" className="form-label"></label>
<textarea
id="type-description"
name="description"
className="form-textarea"
placeholder="请输入类型描述,介绍此类型文档的用途和特点"
value={formData.description}
onChange={handleInputChange}
rows={3}
></textarea>
</div>
{/* 提示词模板选择区域 */}
<div className="form-group flex space-x-4">
{/* llm抽取提示词模板 */}
<div className="w-full">
<label htmlFor="llm-extraction-template" className="form-label">llm抽取提示词模板 <span className="text-red-500">*</span></label>
<select
id="llm-extraction-template"
name="llm_extraction_template"
className={`form-select ${localErrors?.llmExtractionTemplate ? 'input-error' : ''}`}
value={formData.llmExtractionTemplateId}
onChange={handleInputChange}
>
<option value="">llm抽取提示词模板</option>
{llmExtractionTemplates.map((template: PromptTemplateUI) => (
<option key={template.id} value={template.id}>
{template.template_name}
</option>
))}
</select>
{localErrors?.llmExtractionTemplate && (
<div className="error-message error-show">{localErrors.llmExtractionTemplate}</div>
)}
<div className="form-tip">llm提示词模板</div>
</div>
{/* vlm抽取提示词模板 */}
<div className="w-full">
<label htmlFor="vlm-extraction-template" className="form-label">vlm抽取提示词模板 <span className="text-red-500">*</span></label>
<select
id="vlm-extraction-template"
name="vlm_extraction_template"
className={`form-select ${localErrors?.vlmExtractionTemplate ? 'input-error' : ''}`}
value={formData.vlmExtractionTemplateId}
onChange={handleInputChange}
>
<option value="">vlm抽取提示词模板</option>
{vlmExtractionTemplates.map((template: PromptTemplateUI) => (
<option key={template.id} value={template.id}>
{template.template_name}
</option>
))}
</select>
{localErrors?.vlmExtractionTemplate && (
<div className="error-message error-show">{localErrors.vlmExtractionTemplate}</div>
)}
<div className="form-tip">vlm提示词模板</div>
</div>
{/* 评查提示词模板 */}
<div className="w-full">
<label htmlFor="evaluation-template" className="form-label"> <span className="text-red-500">*</span></label>
<select
id="evaluation-template"
name="evaluation_template"
className={`form-select ${localErrors?.evaluationTemplate ? 'input-error' : ''}`}
value={formData.evaluationTemplateId}
onChange={handleInputChange}
>
<option value=""></option>
{evaluationTemplates.map((template: PromptTemplateUI) => (
<option key={template.id} value={template.id}>
{template.template_name}
</option>
))}
</select>
{localErrors?.evaluationTemplate && (
<div className="error-message error-show">{localErrors.evaluationTemplate}</div>
)}
<div className="form-tip"></div>
</div>
{/* 总结提示词模板 */}
<div className="w-full">
<label htmlFor="summary-template" className="form-label"> <span className="text-red-500">*</span></label>
<select
id="summary-template"
name="summary_template"
className={`form-select ${localErrors?.summaryTemplate ? 'input-error' : ''}`}
value={formData.summaryTemplateId}
onChange={handleInputChange}
>
<option value=""></option>
{summaryTemplates.map((template: PromptTemplateUI) => (
<option key={template.id} value={template.id}>
{template.template_name}
</option>
))}
</select>
{localErrors?.summaryTemplate && (
<div className="error-message error-show">{localErrors.summaryTemplate}</div>
)}
<div className="form-tip"></div>
</div>
</div>
{/* 关联评查点分组 */}
<div className="form-group">
<fieldset>
<legend className="form-label">
<span className="text-red-500">*</span>
</legend>
<div
className={`checkbox-group ${localErrors?.groups ? 'group-error' : ''}`}
aria-labelledby="checkpoint-groups-label"
role="group"
>
{ruleGroups.map((group: RuleGroup) => (
<React.Fragment key={group.id}>
{/* 父分组 */}
<div
className={`checkbox-item parent-checkbox-item ${formData.selectedGroups.includes(group.id) ? 'checked' : ''}`}
>
<button
type="button"
className="expand-icon"
onClick={(e) => handleGroupExpand(group.id, e)}
aria-label={`${expandedGroups[group.id] ? '收起' : '展开'}${group.name}分组`}
>
<i className={`ri-arrow-${expandedGroups[group.id] ? 'down' : 'right'}-s-line text-primary`}></i>
</button>
<input
type="radio"
id={`group-${group.id}`}
name="checkpoint_group_ids"
value={group.id}
checked={formData.selectedGroups.includes(group.id)}
onChange={(e) => handleGroupCheckChange(group.id, e.target.checked)}
className="radio-input"
/>
<label
htmlFor={`group-${group.id}`}
className="checkbox-label"
>
{group.name}
<span className="group-badge parent-badge"></span>
</label>
</div>
{/* 子分组 */}
{group.children && group.children.length > 0 && expandedGroups[group.id] && (
group.children.map((child: RuleGroup) => (
<div
key={child.id}
className={`checkbox-item child-checkbox-item ${formData.selectedGroups.includes(child.id) ? 'checked' : ''}`}
>
<input
type="radio"
id={`group-${child.id}`}
name="checkpoint_group_ids"
value={child.id}
checked={formData.selectedGroups.includes(child.id)}
onChange={(e) => handleGroupCheckChange(child.id, e.target.checked)}
className="radio-input"
/>
<label
htmlFor={`group-${child.id}`}
className="checkbox-label"
>
{child.name}
<span className="group-badge child-badge"></span>
</label>
</div>
))
)}
</React.Fragment>
))}
</div>
<div className="form-tip"></div>
{localErrors?.groups && (
<div className="error-message error-show">{localErrors.groups}</div>
)}
</fieldset>
</div>
</div>
</Form>
</Card>
</div>
);
}
+9 -7
View File
@@ -1,22 +1,24 @@
import { Outlet } from "react-router-dom"; import { Outlet } from "@remix-run/react";
import {type MetaFunction} from "@remix-run/node"; import { MetaFunction } from "@remix-run/node";
export const meta: MetaFunction = () => { export const meta: MetaFunction = () => {
return [ return [
{title: "文档类型列表 - 中国烟草AI合同及卷宗审核系统"}, {title: "文档类型管理 - 中国烟草AI合同及卷宗审核系统"},
{name: "document-types", content: "文档类型列表,新增,修改"} {name: "description", content: "管理文档类型,包括查看、创建、编辑和删除文档类型"}
] ]
} }
export const handle = { export const handle = {
breadcrumb: "文档类型列表" breadcrumb: "文档类型管理"
} }
/** /**
* 文档类型列表路由布局 * 文档类型管理路由布局
*/ */
export default function DocumentTypesLayout() { export default function DocumentTypesLayout() {
return ( return (
<Outlet /> <div className="document-types-layout">
<Outlet />
</div>
) )
} }
+143 -212
View File
@@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { useSearchParams, Link } from "@remix-run/react"; import { useSearchParams, Link, useLoaderData, useFetcher } from "@remix-run/react";
import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
import { Card } from "~/components/ui/Card"; import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button"; import { Button } from "~/components/ui/Button";
@@ -10,6 +10,8 @@ import { FileTypeTag } from "~/components/ui/FileTypeTag";
import { FileTag } from "~/components/ui/FileTag"; import { FileTag } from "~/components/ui/FileTag";
import { FilterPanel, FilterSelect, SearchFilter, DateRangeFilter } from "~/components/ui/FilterPanel"; import { FilterPanel, FilterSelect, SearchFilter, DateRangeFilter } from "~/components/ui/FilterPanel";
import documentsIndexStyles from "~/styles/pages/documents_index.css?url"; import documentsIndexStyles from "~/styles/pages/documents_index.css?url";
import { getDocuments, deleteDocument, type DocumentUI } from "~/api/files/documents";
import { getDocumentTypes } from "~/api/document-types/document-types";
// 导入样式 // 导入样式
export function links() { export function links() {
@@ -26,20 +28,6 @@ export const meta: MetaFunction = () => {
]; ];
}; };
interface DocumentItem {
id: string;
name: string;
documentNumber: string;
type: string;
typeName: string;
size: number;
status: string;
issues: number | null;
uploadTime: string;
fileType: string;
tags?: string[];
}
// 数据加载器 // 数据加载器
export const loader = async ({ request }: LoaderFunctionArgs) => { export const loader = async ({ request }: LoaderFunctionArgs) => {
// 获取URL查询参数 // 获取URL查询参数
@@ -51,84 +39,41 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
const dateFrom = url.searchParams.get("dateFrom") || ""; const dateFrom = url.searchParams.get("dateFrom") || "";
const dateTo = url.searchParams.get("dateTo") || ""; const dateTo = url.searchParams.get("dateTo") || "";
const page = parseInt(url.searchParams.get("page") || "1", 10); const page = parseInt(url.searchParams.get("page") || "1", 10);
const pageSize = parseInt(url.searchParams.get("pageSize") || "20", 10); const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10);
// 在实际应用中,这里会调用API获取数据 // 构建搜索参数
// const response = await fetch(`/api/documents?search=${search}&...`); const searchParams = {
// const data = await response.json(); name: search || undefined,
documentNumber: documentNumber || undefined,
// 使用模拟数据 documentType: documentType || undefined,
const mockData = { status: status || undefined,
documents: [ dateFrom: dateFrom || undefined,
{ dateTo: dateTo || undefined,
id: "1",
name: "2023年度烟草销售框架合同.pdf",
documentNumber: "XS20230001",
type: "sales-contract",
typeName: "销售合同",
size: 2.5 * 1024 * 1024, // 2.5MB
status: "pass",
issues: 0,
uploadTime: "2023-10-15 15:30",
fileType: "pdf"
},
{
id: "2",
name: "设备采购合同-打印机.docx",
documentNumber: "CG20230052",
type: "purchase-contract",
typeName: "采购合同",
size: 1.2 * 1024 * 1024, // 1.2MB
status: "warning",
issues: 3,
uploadTime: "2023-10-14 09:15",
fileType: "docx"
},
{
id: "3",
name: "烟草零售许可证.pdf",
documentNumber: "ZM2023100345",
type: "license",
typeName: "专卖许可证",
size: 0.8 * 1024 * 1024, // 0.8MB
status: "pending",
issues: null,
uploadTime: "2023-10-13 14:20",
fileType: "pdf"
},
{
id: "4",
name: "非法售烟行政处罚决定书.docx",
documentNumber: "CF20230087",
type: "punishment",
typeName: "行政处罚决定书",
size: 1.5 * 1024 * 1024, // 1.5MB
status: "processing",
issues: null,
uploadTime: "2023-10-10 16:45",
fileType: "docx"
},
{
id: "5",
name: "烟草种植承包协议-2023.pdf",
documentNumber: "CB20230024",
type: "agreement",
typeName: "承包协议",
size: 3.2 * 1024 * 1024, // 3.2MB
status: "fail",
issues: 8,
uploadTime: "2023-10-09 10:30",
fileType: "pdf",
tags: ["测试"]
},
],
total: 156,
page, page,
pageSize pageSize
}; };
// 返回数据 // 获取文档列表
return Response.json(mockData); const documentsResponse = await getDocuments(searchParams);
if (documentsResponse.error) {
throw new Error(documentsResponse.error);
}
// 获取文档类型列表,用于筛选条件
const typesResponse = await getDocumentTypes();
const documentTypes = typesResponse.data?.types || [];
const documentTypeOptions = documentTypes.map(type => ({
value: type.id,
label: type.name
}));
return Response.json({
documents: documentsResponse.data?.documents || [],
total: documentsResponse.data?.total || 0,
page,
pageSize,
documentTypeOptions
});
}; };
// 处理表单提交和删除等操作 // 处理表单提交和删除等操作
@@ -136,22 +81,31 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData(); const formData = await request.formData();
const action = formData.get("_action"); const action = formData.get("_action");
// 在实际应用中,这里会根据action类型调用相应的API
// 例如删除文档,批量删除,等等
if (action === "delete") { if (action === "delete") {
const id = formData.get("id"); const id = formData.get("id") as string;
// await fetch(`/api/documents/${id}`, { method: "DELETE" }); const response = await deleteDocument(id);
if (response.error) {
return Response.json({ success: false, message: response.error }, { status: response.status || 500 });
}
return Response.json({ success: true, message: "文档已成功删除" }); return Response.json({ success: true, message: "文档已成功删除" });
} }
if (action === "batchDelete") { if (action === "batchDelete") {
const ids = formData.getAll("ids"); const ids = formData.getAll("ids") as string[];
// await fetch(`/api/documents/batch-delete`, {
// method: "POST", // 批量删除处理
// body: JSON.stringify({ ids }), const results = await Promise.all(ids.map(id => deleteDocument(id)));
// headers: { "Content-Type": "application/json" } const failures = results.filter(r => r.error);
// });
if (failures.length > 0) {
return Response.json({
success: false,
message: `删除失败: ${failures.map(f => f.error).join(', ')}`
}, { status: 400 });
}
return Response.json({ success: true, message: `已成功删除${ids.length}个文档` }); return Response.json({ success: true, message: `已成功删除${ids.length}个文档` });
} }
@@ -159,18 +113,9 @@ export const action = async ({ request }: ActionFunctionArgs) => {
return Response.json({ success: false, message: "未知操作" }, { status: 400 }); return Response.json({ success: false, message: "未知操作" }, { status: 400 });
}; };
// 文档类型选项
const documentTypeOptions = [
{ value: "sales-contract", label: "销售合同" },
{ value: "purchase-contract", label: "采购合同" },
{ value: "license", label: "专卖许可证" },
{ value: "punishment", label: "行政处罚决定书" },
{ value: "agreement", label: "承包协议" },
];
// 文档状态选项 // 文档状态选项
const documentStatusOptions = [ const documentStatusOptions = [
{ value: "pending", label: "待审核" }, { value: "waiting", label: "待审核" },
{ value: "processing", label: "审核中" }, { value: "processing", label: "审核中" },
{ value: "pass", label: "通过" }, { value: "pass", label: "通过" },
{ value: "warning", label: "警告" }, { value: "warning", label: "警告" },
@@ -203,6 +148,8 @@ const formatFileSize = (bytes: number) => {
export default function DocumentsIndex() { export default function DocumentsIndex() {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]); const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
const loaderData = useLoaderData<typeof loader>();
const fetcher = useFetcher();
// 从URL获取当前筛选条件 // 从URL获取当前筛选条件
const search = searchParams.get("search") || ""; const search = searchParams.get("search") || "";
@@ -212,77 +159,10 @@ export default function DocumentsIndex() {
const dateFrom = searchParams.get("dateFrom") || ""; const dateFrom = searchParams.get("dateFrom") || "";
const dateTo = searchParams.get("dateTo") || ""; const dateTo = searchParams.get("dateTo") || "";
const currentPage = parseInt(searchParams.get("page") || "1", 10); const currentPage = parseInt(searchParams.get("page") || "1", 10);
const pageSize = parseInt(searchParams.get("pageSize") || "20", 10); const pageSize = parseInt(searchParams.get("pageSize") || "10", 10);
// API 返回的模拟数据 // 获取API返回的数据
const mockData = { const { documents, total, documentTypeOptions } = loaderData;
documents: [
{
id: "1",
name: "2023年度烟草销售框架合同.pdf",
documentNumber: "XS20230001",
type: "sales-contract",
typeName: "销售合同",
size: 2.5 * 1024 * 1024, // 2.5MB
status: "pass",
issues: 0,
uploadTime: "2023-10-15 15:30",
fileType: "pdf"
},
{
id: "2",
name: "设备采购合同-打印机.docx",
documentNumber: "CG20230052",
type: "purchase-contract",
typeName: "采购合同",
size: 1.2 * 1024 * 1024, // 1.2MB
status: "warning",
issues: 3,
uploadTime: "2023-10-14 09:15",
fileType: "docx"
},
{
id: "3",
name: "烟草零售许可证.pdf",
documentNumber: "ZM2023100345",
type: "license",
typeName: "专卖许可证",
size: 0.8 * 1024 * 1024, // 0.8MB
status: "pending",
issues: null,
uploadTime: "2023-10-13 14:20",
fileType: "pdf"
},
{
id: "4",
name: "非法售烟行政处罚决定书.docx",
documentNumber: "CF20230087",
type: "punishment",
typeName: "行政处罚决定书",
size: 1.5 * 1024 * 1024, // 1.5MB
status: "processing",
issues: null,
uploadTime: "2023-10-10 16:45",
fileType: "docx"
},
{
id: "5",
name: "烟草种植承包协议-2023.pdf",
documentNumber: "CB20230024",
type: "agreement",
typeName: "承包协议",
size: 3.2 * 1024 * 1024, // 3.2MB
status: "fail",
issues: 8,
uploadTime: "2023-10-09 10:30",
fileType: "pdf",
tags: ["测试"]
},
],
total: 156,
page: currentPage,
pageSize
};
// 分页处理函数 // 分页处理函数
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
@@ -359,6 +239,30 @@ export default function DocumentsIndex() {
// 重置搜索条件 // 重置搜索条件
const handleReset = () => { const handleReset = () => {
// 直接重置所有筛选条件的DOM值
const resetInput = (selector: string, value: string = "") => {
const element = document.querySelector<HTMLInputElement | HTMLSelectElement>(selector);
if (element) {
element.value = value;
// 对于搜索框,触发其input事件以激活搜索
if (element instanceof HTMLInputElement && element.type === "text") {
// 创建一个input事件
const event = new Event('input', { bubbles: true });
element.dispatchEvent(event);
}
}
};
// 重置所有搜索字段
resetInput('input[placeholder="请输入文档名称"]');
resetInput('input[placeholder="请输入文档编号"]');
resetInput('select[name="documentType"]');
resetInput('select[name="status"]');
resetInput('input[name="dateFrom"]');
resetInput('input[name="dateTo"]');
// 重置URL参数
setSearchParams(new URLSearchParams({ setSearchParams(new URLSearchParams({
page: "1", page: "1",
pageSize: pageSize.toString() pageSize: pageSize.toString()
@@ -377,33 +281,57 @@ export default function DocumentsIndex() {
// 全选处理 // 全选处理
const handleSelectAll = (checked: boolean) => { const handleSelectAll = (checked: boolean) => {
if (checked) { if (checked) {
setSelectedRowKeys(mockData.documents.map(doc => doc.id)); setSelectedRowKeys(documents.map((doc: DocumentUI) => doc.id.toString()));
} else { } else {
setSelectedRowKeys([]); setSelectedRowKeys([]);
} }
}; };
// 删除确认 // 下载文档
const confirmDelete = (id: string, name: string) => { const handleDownload = (path: string, fileName: string) => {
console.log('handleDownload',path,fileName)
// 创建一个隐藏的a标签并点击它
const a = document.createElement('a');
a.href = path;
a.download = fileName; // 设置下载的文件名
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
// 删除文档
const handleDelete = (id: string, name: string) => {
if (window.confirm(`确认删除文档 "${name}"`)) { if (window.confirm(`确认删除文档 "${name}"`)) {
// 在实际应用中这里会提交表单到action处理 // 使用fetcher提交表单
console.log('删除文档:', id, name); const formData = new FormData();
formData.append('_action', 'delete');
formData.append('id', id);
fetcher.submit(formData, { method: 'post' });
// 更新选中行 // 更新选中行
setSelectedRowKeys(selectedRowKeys.filter(key => key !== id)); setSelectedRowKeys(selectedRowKeys.filter(key => key !== id));
} }
}; };
// 批量删除确认 // 批量删除
const confirmBatchDelete = () => { const handleBatchDelete = () => {
if (selectedRowKeys.length === 0) { if (selectedRowKeys.length === 0) {
alert('请至少选择一个文档'); alert('请至少选择一个文档');
return; return;
} }
if (window.confirm(`确认删除选中的 ${selectedRowKeys.length} 个文档?`)) { if (window.confirm(`确认删除选中的 ${selectedRowKeys.length} 个文档?`)) {
// 在实际应用中这里会提交表单到action处理 // 使用fetcher提交表单
console.log('批量删除文档IDs:', selectedRowKeys); const formData = new FormData();
formData.append('_action', 'batchDelete');
// 添加所有选中的ID
selectedRowKeys.forEach(id => {
formData.append('ids', id);
});
fetcher.submit(formData, { method: 'post' });
// 清空选中行 // 清空选中行
setSelectedRowKeys([]); setSelectedRowKeys([]);
@@ -416,24 +344,24 @@ export default function DocumentsIndex() {
title: ( title: (
<input <input
type="checkbox" type="checkbox"
checked={selectedRowKeys.length === mockData.documents.length} checked={selectedRowKeys.length === documents.length}
onChange={(e) => handleSelectAll(e.target.checked)} onChange={(e) => handleSelectAll(e.target.checked)}
/> />
), ),
key: "selection", key: "selection",
width: "50px", width: "50px",
render: (_: unknown, record: DocumentItem) => ( render: (_: unknown, record: DocumentUI) => (
<input <input
type="checkbox" type="checkbox"
checked={selectedRowKeys.includes(record.id)} checked={selectedRowKeys.includes(record.id.toString())}
onChange={() => handleRowSelectionChange(record.id)} onChange={() => handleRowSelectionChange(record.id.toString())}
/> />
) )
}, },
{ {
title: "文档名称", title: "文档名称",
key: "name", key: "name",
render: (_: unknown, record: DocumentItem) => ( render: (_: unknown, record: DocumentUI) => (
<div className="flex items-center m-1"> <div className="flex items-center m-1">
<FileTag <FileTag
extension={record.fileType} extension={record.fileType}
@@ -451,10 +379,11 @@ export default function DocumentsIndex() {
text={record.typeName} text={record.typeName}
size="sm" size="sm"
showIcon={false} showIcon={false}
fileType={record.fileType}
/> />
{record.tags && record.tags.map((tag: string) => ( {record.isTest && (
<span key={tag} className="ml-2 text-xs bg-gray-100 text-gray-500 px-1 rounded">{tag}</span> <span className="ml-2 text-xs bg-gray-100 text-gray-500 px-1 rounded"></span>
))} )}
</div> </div>
</div> </div>
</div> </div>
@@ -463,41 +392,43 @@ export default function DocumentsIndex() {
{ {
title: "文档编号", title: "文档编号",
key: "documentNumber", key: "documentNumber",
render: (_: unknown, record: DocumentItem) => ( render: (_: unknown, record: DocumentUI) => (
<span className="document-number">{record.documentNumber}</span> <span className="document-number">{record.documentNumber}</span>
) )
}, },
{ {
title: "文件大小", title: "文件大小",
key: "size", key: "size",
render: (_: unknown, record: DocumentItem) => formatFileSize(record.size) width: "100px",
render: (_: unknown, record: DocumentUI) => formatFileSize(record.size)
}, },
{ {
title: "审核状态", title: "审核状态",
key: "status", key: "status",
render: (_: unknown, record: DocumentItem) => ( render: (_: unknown, record: DocumentUI) => (
<StatusBadge status={record.status} showIcon={false} /> <StatusBadge status={record.status} showIcon={false} />
) )
}, },
{ {
title: "问题数量", title: "问题数量",
key: "issues", key: "issues",
render: (_: unknown, record: DocumentItem) => ( width:"60px",
render: (_: unknown, record: DocumentUI) => (
record.issues === null ? "-" : record.issues record.issues === null ? "-" : record.issues
) )
}, },
{ {
title: "上传时间", title: "上传时间",
key: "uploadTime", key: "uploadTime",
render: (_: unknown, record: DocumentItem) => record.uploadTime render: (_: unknown, record: DocumentUI) => record.uploadTime
}, },
{ {
title: "操作", title: "操作",
key: "actions", key: "actions",
width: "280px", width: "280px",
render: (_: unknown, record: DocumentItem) => ( render: (_: unknown, record: DocumentUI) => (
<div className="operations-cell"> <div className="operations-cell">
{record.status === "pending" ? ( {record.status === "waiting" ? (
<Link <Link
to={`/documents/${record.id}/review`} to={`/documents/${record.id}/review`}
className="mr-1 hover:underline" className="mr-1 hover:underline"
@@ -523,7 +454,7 @@ export default function DocumentsIndex() {
</Link> </Link>
)} )}
<Link <Link
to={`/documents/${record.id}/edit`} to={`/documents/edit?id=${record.id}`}
className="mr-1 text-gray-500 hover:underline hover:text-gray-700" className="mr-1 text-gray-500 hover:underline hover:text-gray-700"
> >
<i className="ri-edit-line"></i> <i className="ri-edit-line"></i>
@@ -532,7 +463,7 @@ export default function DocumentsIndex() {
<button <button
type="button" type="button"
className="mr-1 text-gray-500 hover:underline hover:text-gray-700" className="mr-1 text-gray-500 hover:underline hover:text-gray-700"
onClick={() => alert(`下载文档: ${record.name}`)} onClick={() => handleDownload(record.path, record.name)}
> >
<i className="ri-download-line"></i> <i className="ri-download-line"></i>
@@ -540,7 +471,7 @@ export default function DocumentsIndex() {
<button <button
type="button" type="button"
className="text-error hover:underline hover:text-red-700" className="text-error hover:underline hover:text-red-700"
onClick={() => confirmDelete(record.id, record.name)} onClick={() => handleDelete(record.id.toString(), record.name)}
> >
<i className="ri-delete-bin-line"></i> <i className="ri-delete-bin-line"></i>
@@ -579,7 +510,7 @@ export default function DocumentsIndex() {
> >
</Button> </Button>
<Button {/* <Button
type="primary" type="primary"
icon="ri-search-line" icon="ri-search-line"
onClick={() => { onClick={() => {
@@ -588,7 +519,7 @@ export default function DocumentsIndex() {
}} }}
> >
搜索 搜索
</Button> </Button> */}
</> </>
} }
noActionDivider={true} noActionDivider={true}
@@ -648,7 +579,7 @@ export default function DocumentsIndex() {
<Button <Button
type="default" type="default"
icon="ri-delete-bin-line" icon="ri-delete-bin-line"
onClick={confirmBatchDelete} onClick={handleBatchDelete}
className="mr-2" className="mr-2"
disabled={selectedRowKeys.length === 0} disabled={selectedRowKeys.length === 0}
> >
@@ -662,14 +593,14 @@ export default function DocumentsIndex() {
</Button> </Button>
</div> </div>
<div className="text-sm text-secondary"> <div className="text-sm text-secondary">
<span className="font-medium text-primary">{mockData.total}</span> <span className="font-medium text-primary">{total}</span>
</div> </div>
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<Table <Table
columns={columns} columns={columns}
dataSource={mockData.documents} dataSource={documents}
rowKey="id" rowKey="id"
emptyText="暂无数据" emptyText="暂无数据"
/> />
@@ -678,7 +609,7 @@ export default function DocumentsIndex() {
{/* 分页 */} {/* 分页 */}
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}
total={mockData.total} total={total}
pageSize={pageSize} pageSize={pageSize}
onChange={handlePageChange} onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange} onPageSizeChange={handlePageSizeChange}
+486
View File
@@ -0,0 +1,486 @@
import { useState } from "react";
import { useLoaderData, useActionData, useNavigate, Form } from "@remix-run/react";
import { redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button";
import documentEditStyles from "~/styles/pages/documents_edit.css?url";
export function links() {
return [{ rel: "stylesheet", href: documentEditStyles }];
}
export const meta: MetaFunction = () => {
return [
{ title: "修改文档 - 中国烟草AI合同及卷宗审核系统" },
{ name: "description", content: "修改文档信息,包括文档类型、编号、状态和备注信息等" }
];
};
// 文档状态定义
enum DocumentStatus {
PENDING = "pending",
PROCESSING = "processing",
PASS = "pass",
WARNING = "warning",
FAIL = "fail"
}
// 文档状态对应的中文标签
const STATUS_LABELS: Record<DocumentStatus, string> = {
[DocumentStatus.PENDING]: "待审核",
[DocumentStatus.PROCESSING]: "审核中",
[DocumentStatus.PASS]: "通过",
[DocumentStatus.WARNING]: "警告",
[DocumentStatus.FAIL]: "不通过"
};
// 文档类型接口
interface DocumentType {
id: string;
name: string;
}
// 历史记录项接口
interface HistoryItem {
time: string;
user: string;
action: string;
details: string;
}
// 文档接口
interface Document {
id: string;
name: string;
type_id: string;
document_number: string | null;
file_size: number;
upload_time: string;
is_test_document: boolean;
status: DocumentStatus;
remark: string | null;
history: HistoryItem[];
file_url?: string;
}
// 模拟API获取文档类型列表
async function getDocumentTypes(): Promise<DocumentType[]> {
// 这里应该是实际API调用
return [
{ id: "1", name: "销售合同" },
{ id: "2", name: "采购合同" },
{ id: "3", name: "专卖许可证" },
{ id: "4", name: "行政处罚决定书" },
{ id: "5", name: "承包协议" }
];
}
// 模拟API获取文档详情
async function getDocument(id: string): Promise<Document> {
// 这里应该是实际API调用
return {
id,
name: "2023年度烟草销售框架合同.pdf",
type_id: "1", // 销售合同
document_number: "XS20230001",
file_size: 2.5 * 1024 * 1024, // 2.5MB
upload_time: "2023-10-15 15:30",
is_test_document: false,
status: DocumentStatus.PASS,
remark: "此合同为2023年度与XX公司的销售框架协议,适用于全年的烟草销售业务。",
history: [
{
time: "2023-10-15 15:30",
user: "系统",
action: "创建了此文档",
details: "首次上传文档,文档类型:销售合同,状态:待审核"
},
{
time: "2023-10-15 16:45",
user: "张三",
action: "启动了文档审核",
details: "状态由'待审核'变更为'审核中'"
},
{
time: "2023-10-15 17:20",
user: "系统",
action: "完成了文档审核",
details: "状态由'审核中'变更为'通过',未发现问题"
},
{
time: "2023-10-16 09:10",
user: "李四",
action: "修改了文档属性",
details: "添加了备注信息,完善了文档编号"
}
],
file_url: "/mock/documents/sample.pdf"
};
}
// 模拟API更新文档信息
async function updateDocument(id: string, data: Partial<Document>): Promise<Document> {
// 这里应该是实际API调用
console.log("更新文档:", id, data);
// 模拟获取原始数据
const document = await getDocument(id);
// 合并更新的数据
const updatedDocument = {
...document,
...data,
// 添加新的历史记录
history: [
{
time: new Date().toISOString().replace("T", " ").slice(0, 16),
user: "当前用户",
action: "修改了文档信息",
details: `更新了文档类型、状态和备注信息`
},
...document.history
]
};
return updatedDocument;
}
// 格式化文件大小
function formatFileSize(bytes: number): string {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
// Loader函数
export async function loader({ request }: LoaderFunctionArgs) {
try {
// 从URL查询参数获取文档ID
const url = new URL(request.url);
const id = url.searchParams.get("id");
if (!id) {
throw new Response("缺少文档ID", { status: 400 });
}
// 并行获取文档详情和文档类型列表
const [document, documentTypes] = await Promise.all([
getDocument(id),
getDocumentTypes()
]);
return Response.json({ document, documentTypes });
} catch (error) {
console.error("加载文档数据失败:", error);
throw new Response("加载文档数据失败", { status: 500 });
}
}
// Action函数处理表单提交
export async function action({ request }: ActionFunctionArgs) {
// 从URL查询参数获取文档ID
const url = new URL(request.url);
const id = url.searchParams.get("id");
if (!id) {
return Response.json({ error: "缺少文档ID" }, { status: 400 });
}
try {
const formData = await request.formData();
// 从表单数据中提取字段
const type_id = formData.get("type_id") as string;
const document_number = formData.get("document_number") as string;
const status = formData.get("status") as DocumentStatus;
const is_test_document = formData.get("is_test_document") === "on";
const remark = formData.get("remark") as string;
// 验证必填字段
if (!type_id || !status) {
return Response.json(
{
error: "缺少必填字段",
fieldErrors: {
type_id: !type_id ? "文档类型不能为空" : null,
status: !status ? "状态不能为空" : null
}
},
{ status: 400 }
);
}
// 更新文档
await updateDocument(id, {
type_id,
document_number: document_number || null,
status,
is_test_document,
remark: remark || null
});
// 重定向回文档列表
return redirect("/documents");
} catch (error) {
console.error("更新文档失败:", error);
return Response.json({ error: "更新文档失败" }, { status: 500 });
}
}
// 文档编辑页面组件
export default function DocumentEdit() {
const { document, documentTypes } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const navigate = useNavigate();
// 状态
const [localStatus, setLocalStatus] = useState<DocumentStatus>(document.status);
// 处理状态变更
const handleStatusChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setLocalStatus(e.target.value as DocumentStatus);
};
// 获取文档类型名称
const getDocumentTypeName = (typeId: string): string => {
const docType = documentTypes.find((type: DocumentType) => type.id === typeId);
return docType ? docType.name : "未知类型";
};
// 渲染状态徽章
const renderStatusBadge = (status: DocumentStatus) => {
const statusClasses: Record<DocumentStatus, string> = {
[DocumentStatus.PENDING]: "status-badge status-pending",
[DocumentStatus.PROCESSING]: "status-badge status-processing",
[DocumentStatus.PASS]: "status-badge status-pass",
[DocumentStatus.WARNING]: "status-badge status-warning",
[DocumentStatus.FAIL]: "status-badge status-fail"
};
return (
<span className={statusClasses[status]}>
{STATUS_LABELS[status]}
</span>
);
};
return (
<div className="document-edit-page">
{/* 页面头部 */}
<div className="page-header">
<h2 className="page-title"></h2>
<div>
<Button
type="default"
icon="ri-arrow-left-line"
onClick={() => navigate("/documents")}
className="mr-2"
>
</Button>
<Button
type="primary"
icon="ri-save-line"
form="edit-form"
>
</Button>
</div>
</div>
{/* 文档信息 */}
<Card className="mb-4">
<div className="document-info">
<div className="document-icon">
<i className="ri-file-pdf-line text-red-500"></i>
</div>
<div className="document-details">
<div className="document-name">{document.name}</div>
<div className="document-meta">
<div className="meta-item">
<i className="ri-file-list-line"></i>
<span>{getDocumentTypeName(document.type_id)}</span>
</div>
<div className="meta-item">
<i className="ri-time-line"></i>
<span>{document.upload_time}</span>
</div>
<div className="meta-item">
<i className="ri-hard-drive-line"></i>
<span>{formatFileSize(document.file_size)}</span>
</div>
<div className="meta-item">
{renderStatusBadge(document.status)}
</div>
</div>
</div>
</div>
<div className="alert alert-info mb-4">
<i className="ri-information-line mr-2"></i>
</div>
<Form id="edit-form" method="post">
<div className="grid grid-cols-2 gap-6">
<div className="form-group">
<label htmlFor="type-id" className="form-label"> <span className="text-red-500">*</span></label>
<select
id="type-id"
name="type_id"
className="form-select"
defaultValue={document.type_id}
required
>
{documentTypes.map((type: DocumentType) => (
<option key={type.id} value={type.id}>{type.name}</option>
))}
</select>
<div className="text-sm text-secondary mt-1"></div>
{actionData?.fieldErrors?.type_id && (
<div className="text-red-500 text-sm mt-1">{actionData.fieldErrors.type_id}</div>
)}
</div>
<div className="form-group">
<label htmlFor="document-number" className="form-label"></label>
<input
type="text"
id="document-number"
name="document_number"
className="form-input"
placeholder="请输入合同编号、许可证号等"
defaultValue={document.document_number || ""}
/>
<div className="text-sm text-secondary mt-1"></div>
</div>
<div className="form-group">
<label htmlFor="status" className="form-label"> <span className="text-red-500">*</span></label>
<select
id="status"
name="status"
className="form-select"
value={localStatus}
onChange={handleStatusChange}
required
>
<option value={DocumentStatus.PENDING}>{STATUS_LABELS[DocumentStatus.PENDING]}</option>
<option value={DocumentStatus.PROCESSING}>{STATUS_LABELS[DocumentStatus.PROCESSING]}</option>
<option value={DocumentStatus.PASS}>{STATUS_LABELS[DocumentStatus.PASS]}</option>
<option value={DocumentStatus.WARNING}>{STATUS_LABELS[DocumentStatus.WARNING]}</option>
<option value={DocumentStatus.FAIL}>{STATUS_LABELS[DocumentStatus.FAIL]}</option>
</select>
<div className="text-sm text-secondary mt-1"></div>
{actionData?.fieldErrors?.status && (
<div className="text-red-500 text-sm mt-1">{actionData.fieldErrors.status}</div>
)}
</div>
<div className="form-group">
<div className="form-label"></div>
<div className="flex items-center mt-2">
<label className="switch mr-2" htmlFor="is-test-document">
<input
type="checkbox"
id="is-test-document"
name="is_test_document"
defaultChecked={document.is_test_document}
/>
<span className="slider"></span>
<span className="sr-only"></span>
</label>
<span></span>
</div>
</div>
<div className="form-group col-span-2">
<label htmlFor="remark" className="form-label"></label>
<textarea
id="remark"
name="remark"
className="form-textarea"
placeholder="可输入文档的相关描述或备注信息"
rows={3}
defaultValue={document.remark || ""}
></textarea>
</div>
</div>
</Form>
</Card>
{/* 文档预览 */}
<Card
title="文档预览"
className="mb-4"
>
<div className="document-preview">
<div className="preview-toolbar">
<div className="flex items-center">
<i className="ri-file-pdf-line text-red-500 mr-1"></i>
<span>{document.name}</span>
</div>
<div>
<Button
type="default"
size="small"
icon="ri-download-line"
>
</Button>
</div>
</div>
<div className="preview-content">
<div className="preview-placeholder">
<i className="ri-file-pdf-line"></i>
<p></p>
<p className="text-xs mt-2">PDF文件需要外部查看器支持</p>
<Button
type="primary"
size="small"
icon="ri-external-link-line"
className="mt-4"
>
</Button>
</div>
</div>
</div>
</Card>
{/* 修改历史 */}
<Card title="修改历史">
<div className="history-timeline">
{document.history.map((item: HistoryItem, index: number) => (
<div className="timeline-item" key={`${item.time}-${index}`}>
<div className="timeline-time">{item.time}</div>
<div className="timeline-content">
<div><strong>{item.user}</strong> {item.action}</div>
<div className="text-xs text-secondary mt-1">{item.details}</div>
</div>
</div>
))}
</div>
</Card>
</div>
);
}
// 错误边界
export function ErrorBoundary() {
return (
<div className="error-container">
<h1 className="text-xl font-bold text-red-500 mb-4"></h1>
<p className="mb-4">ID是否正确</p>
<Button
type="primary"
to="/documents"
>
</Button>
</div>
);
}
+51 -38
View File
@@ -30,21 +30,11 @@ export const meta: MetaFunction = () => {
]; ];
}; };
// 文件类型定义 // 文件类型定义为字符串类型,以适应从API动态获取的ID
export enum FileType { export type FileType = string;
CONTRACT = "1",
LICENSE = "2",
PUNISHMENT = "3",
OTHER = "4"
}
// 文件类型标签映射 // 动态构建的文件类型标签映射
export const FILE_TYPE_LABELS: Record<FileType, string> = { export const FILE_TYPE_LABELS: Record<string, string> = {};
[FileType.CONTRACT]: "合同文档",
[FileType.LICENSE]: "专卖许可证",
[FileType.PUNISHMENT]: "行政处罚决定书",
[FileType.OTHER]: "其他文档"
};
// 优先级定义 // 优先级定义
export enum Priority { export enum Priority {
@@ -85,7 +75,7 @@ export enum StepStatus {
// 上传的文件信息接口 // 上传的文件信息接口
export interface UploadedFile { export interface UploadedFile {
id: string; id: number;
name: string; name: string;
size: number; size: number;
type: string; type: string;
@@ -346,6 +336,19 @@ export default function FilesUpload() {
const [queueFiles, setQueueFiles] = useState<Document[]>(documents); const [queueFiles, setQueueFiles] = useState<Document[]>(documents);
const [documentTypesState] = useState<DocumentType[]>(documentTypes); const [documentTypesState] = useState<DocumentType[]>(documentTypes);
// 构建文件类型标签映射
useEffect(() => {
// 清空之前的映射
Object.keys(FILE_TYPE_LABELS).forEach(key => {
delete FILE_TYPE_LABELS[key];
});
// 使用从API获取的文档类型构建新的映射
documentTypes.forEach(type => {
FILE_TYPE_LABELS[type.id.toString()] = type.name;
});
}, [documentTypes]);
// 上传完成后的文件信息列表 // 上传完成后的文件信息列表
const [completedFiles, setCompletedFiles] = useState<UploadedFile[]>([]); const [completedFiles, setCompletedFiles] = useState<UploadedFile[]>([]);
@@ -388,7 +391,7 @@ export default function FilesUpload() {
// 获取所有未完成的文档ID // 获取所有未完成的文档ID
const incompleteIds = queueFiles const incompleteIds = queueFiles
.filter(file => file.status !== DocumentStatus.COMPLETED) .filter(file => file.status !== DocumentStatus.COMPLETED && file.id)
.map(file => file.id); .map(file => file.id);
console.log('未完成的文档ID:', incompleteIds); console.log('未完成的文档ID:', incompleteIds);
@@ -435,12 +438,17 @@ export default function FilesUpload() {
// 处理文件类型变化 // 处理文件类型变化
const handleFileTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const handleFileTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newFileType = e.target.value as FileType; const value = e.target.value;
setFileType(newFileType); // 确保只有选择了有效的文件类型才进行设置
if (value) {
setFileType(value as FileType);
// 如果已经有选中的文件,且选择了文件类型,则开始上传 // 如果已经有选中的文件,且选择了文件类型,则开始上传
if (currentFiles.length > 0 && newFileType) { if (currentFiles.length > 0) {
startUpload(currentFiles); startUpload(currentFiles);
}
} else {
setFileType("");
} }
}; };
@@ -510,7 +518,7 @@ export default function FilesUpload() {
// 创建新的文件对象 // 创建新的文件对象
const newFile: UploadedFile = { const newFile: UploadedFile = {
id: response.result.id.toString(), id: response.result.id,
name: response.result.file_name, name: response.result.file_name,
size: response.result.file_size, size: response.result.file_size,
type: file.type, type: file.type,
@@ -537,14 +545,18 @@ export default function FilesUpload() {
setUploadSpeed("完成"); setUploadSpeed("完成");
// 更新队列 // 更新队列
const newDocuments: Document[] = uploadedFiles.map(file => ({ const newDocuments: Document[] = uploadedFiles.map(file => {
id: parseInt(file.id), // 确保id能够被正确解析为数字
name: file.name, const id = file.id;
type_id: parseInt(fileType), return {
file_size: file.size, id,
status: DocumentStatus.CUTTING, name: file.name,
created_at: new Date().toISOString() type_id: fileType ? parseInt(fileType) : 0,
})); file_size: file.size,
status: DocumentStatus.CUTTING,
created_at: new Date().toISOString()
};
});
setQueueFiles(prev => [...newDocuments, ...prev]); setQueueFiles(prev => [...newDocuments, ...prev]);
@@ -587,7 +599,7 @@ export default function FilesUpload() {
setProcessingSteps(updatedSteps); setProcessingSteps(updatedSteps);
// 获取文件ID列表 // 获取文件ID列表
const fileIds = files.map(file => parseInt(file.id)); const fileIds = files.map(file => file.id).filter(id => id > 0);
console.log('开始处理文件,设置文件处理进度定时器'); console.log('开始处理文件,设置文件处理进度定时器');
@@ -802,8 +814,8 @@ export default function FilesUpload() {
}; };
// 获取文档类型名称 // 获取文档类型名称
const getDocumentTypeName = (typeId: number) => { const getDocumentTypeName = (codeId: number) => {
const type = documentTypesState.find(t => t.id === typeId); const type = documentTypesState.find(t => t.id === codeId);
return type ? type.name : '未知类型'; return type ? type.name : '未知类型';
}; };
@@ -920,10 +932,9 @@ export default function FilesUpload() {
disabled={uploadStage !== "idle"} disabled={uploadStage !== "idle"}
> >
<option value=""></option> <option value=""></option>
<option value={FileType.CONTRACT}>{FILE_TYPE_LABELS[FileType.CONTRACT]}</option> {documentTypes.map(type => (
<option value={FileType.LICENSE}>{FILE_TYPE_LABELS[FileType.LICENSE]}</option> <option key={type.id} value={type.id}>{type.name}</option>
<option value={FileType.PUNISHMENT}>{FILE_TYPE_LABELS[FileType.PUNISHMENT]}</option> ))}
<option value={FileType.OTHER}>{FILE_TYPE_LABELS[FileType.OTHER]}</option>
</select> </select>
{actionData?.errors?.fileType && ( {actionData?.errors?.fileType && (
@@ -1007,7 +1018,9 @@ export default function FilesUpload() {
</li> </li>
<li className="file-info-item"> <li className="file-info-item">
<span className="file-info-label"></span> <span className="file-info-label"></span>
<span className="file-info-value">{FILE_TYPE_LABELS[file.fileType]}</span> <span className="file-info-value">
{FILE_TYPE_LABELS[file.fileType] || getDocumentTypeName(parseInt(file.fileType))}
</span>
</li> </li>
<li className="file-info-item"> <li className="file-info-item">
<span className="file-info-label"></span> <span className="file-info-label"></span>
+8 -3
View File
@@ -227,13 +227,17 @@ export default function PromptsIndex() {
{ {
title: "类型", title: "类型",
key: "template_type", key: "template_type",
width: "100px", width: "120px",
render: (_: unknown, record: PromptTemplateUI) => { render: (_: unknown, record: PromptTemplateUI) => {
let typeText = ''; let typeText = '';
let typeClass = ''; let typeClass = '';
switch (record.template_type) { switch (record.template_type) {
case 'Extraction': case 'LLM_Extraction':
typeText = '抽取';
typeClass = 'type-extraction';
break;
case 'VLM_Extraction':
typeText = '抽取'; typeText = '抽取';
typeClass = 'type-extraction'; typeClass = 'type-extraction';
break; break;
@@ -400,7 +404,8 @@ export default function PromptsIndex() {
name="type" name="type"
value={searchParams.get('type') || ''} value={searchParams.get('type') || ''}
options={[ options={[
{ value: "Extraction", label: "抽取(Extraction)" }, { value: "LLM_Extraction", label: "LLM抽取(LLM_Extraction)" },
{ value: "VLM_Extraction", label: "VLM抽取(VLM_Extraction)" },
{ value: "Evaluation", label: "评估(Evaluation)" }, { value: "Evaluation", label: "评估(Evaluation)" },
{ value: "Summary", label: "摘要(Summary)" }, { value: "Summary", label: "摘要(Summary)" },
{ value: "Common", label: "通用(Common)" } { value: "Common", label: "通用(Common)" }
+4 -3
View File
@@ -52,7 +52,7 @@ interface ActionData {
}; };
formData?: { formData?: {
template_name: string; template_name: string;
template_type: "Extraction" | "Evaluation" | "Summary" | "Common"; template_type: 'LLM_Extraction' | 'VLM_Extraction' | 'Evaluation' | 'Summary' | 'Common';
description: string; description: string;
template_content: string; template_content: string;
variables: string; variables: string;
@@ -108,7 +108,7 @@ export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData(); const formData = await request.formData();
const id = formData.get("id") as string; const id = formData.get("id") as string;
const template_name = formData.get("template_name") as string; const template_name = formData.get("template_name") as string;
const template_type = formData.get("template_type") as "Extraction" | "Evaluation" | "Summary" | "Common"; const template_type = formData.get("template_type") as 'LLM_Extraction' | 'VLM_Extraction' | 'Evaluation' | 'Summary' | 'Common';
const description = formData.get("description") as string; const description = formData.get("description") as string;
const template_content = formData.get("template_content") as string; const template_content = formData.get("template_content") as string;
const variables = formData.get("variables") as string; const variables = formData.get("variables") as string;
@@ -488,7 +488,8 @@ export default function PromptsNew() {
> >
<option value=""></option> <option value=""></option>
<option value="Common">(Common) - </option> <option value="Common">(Common) - </option>
<option value="Extraction">(Extraction) - </option> <option value="LLM_Extraction">LLM抽取(LLM_Extraction) - 使LLM从文档中抽取结构化信息</option>
<option value="VLM_Extraction">VLM抽取(VLM_Extraction) - 使VLM从文档中抽取结构化信息</option>
<option value="Evaluation">(Evaluation) - </option> <option value="Evaluation">(Evaluation) - </option>
<option value="Summary">(Summary) - </option> <option value="Summary">(Summary) - </option>
</select> </select>
+35
View File
@@ -0,0 +1,35 @@
.document-types-page {
@apply w-full;
}
.document-types-page .page-header {
@apply flex justify-between items-center mb-4;
}
.document-types-page .page-title {
@apply text-xl font-medium;
}
.document-types-page .type-badge {
@apply inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-primary-50 text-primary-600 mr-1 mb-1;
}
.document-types-page .groups-container {
@apply flex flex-wrap gap-1 max-w-md;
}
.document-types-page .operation-btn {
@apply inline-flex items-center text-sm px-2 py-1 rounded hover:bg-gray-100;
}
.document-types-page .operation-btn i {
@apply mr-1;
}
.document-types-page .text-primary {
@apply text-primary-600;
}
.document-types-page .text-error {
@apply text-red-600;
}
+142
View File
@@ -0,0 +1,142 @@
.document-type-new-page {
@apply w-full;
}
.document-type-new-page .page-header {
@apply flex justify-between items-center mb-4;
}
.document-type-new-page .page-title {
@apply text-xl font-medium;
}
.document-type-new-page .form-group {
@apply mb-4;
}
.document-type-new-page .form-label {
@apply block text-sm font-medium text-gray-700 mb-1;
}
.document-type-new-page .form-input,
.document-type-new-page .form-textarea,
.document-type-new-page .form-select {
@apply w-full rounded-md border border-gray-300 shadow-sm px-3 py-2 transition-all duration-300;
@apply focus:outline-none focus:border-[var(--primary-color)] focus:shadow-[0_0_0_2px_rgba(0,104,74,0.2)];
}
.document-type-new-page .form-textarea {
@apply resize-none;
}
.document-type-new-page .form-tip {
@apply text-xs text-gray-500 mt-1;
}
.document-type-new-page .input-error {
@apply border-red-500;
}
.document-type-new-page .error-message {
@apply text-sm text-red-600 mt-1 font-medium;
display: flex;
align-items: center;
}
.document-type-new-page .error-message::before {
content: "⚠️";
margin-right: 0.25rem;
}
.document-type-new-page .error-show {
display: flex !important;
color: #ff4d4f;
font-weight: 500;
visibility: visible;
}
.document-type-new-page .general-error {
@apply flex items-center p-3 mb-4 bg-red-50 rounded-md text-red-600 text-sm;
}
.document-type-new-page .general-error i {
@apply mr-2;
}
.document-type-new-page .group-error {
@apply border border-red-500 rounded-md;
}
/* 复选框样式 */
.document-type-new-page .checkbox-group {
@apply flex flex-col gap-2 mt-2;
}
.document-type-new-page .checkbox-item {
@apply flex items-center p-2 border border-gray-200 rounded cursor-pointer transition-all;
}
.document-type-new-page .checkbox-item:hover {
@apply border-[var(--primary-color)] bg-[rgba(0,104,74,0.15)];
}
.document-type-new-page .checkbox-item.checked {
@apply border-[var(--primary-color)] bg-[rgba(0,104,74,0.25)];
}
/* 父子级分组样式 */
.document-type-new-page .parent-checkbox-item {
@apply bg-gray-50 font-medium;
}
.document-type-new-page .child-checkbox-item {
@apply ml-8 border-l-2 border-l-[var(--primary-color)];
}
.document-type-new-page .expand-icon {
@apply w-6 h-6 flex items-center justify-center rounded hover:bg-[rgba(0,104,74,0.15)] mr-2 cursor-pointer;
}
.document-type-new-page .group-badge {
@apply inline-flex items-center px-2 py-0.5 rounded-full text-xs ml-2;
}
.document-type-new-page .parent-badge {
@apply bg-[rgba(0,104,74,1)] text-white;
}
.document-type-new-page .child-badge {
@apply bg-[rgba(0,104,1,0.61)] text-white;
}
/* 添加checkbox-input样式,使用视觉上更美观的自定义复选框样式 */
.document-type-new-page .checkbox-input {
@apply mr-2 h-4 w-4 text-primary-600 border-gray-300 rounded;
@apply focus:ring-primary-500 cursor-pointer;
}
.document-type-new-page .checkbox-label {
@apply text-gray-700 font-normal cursor-pointer flex-1 flex items-center;
@apply bg-transparent border-none p-0 text-left appearance-none;
outline: none;
}
.document-type-new-page .checkbox-label:focus {
@apply outline-none ring-2 ring-primary-300 rounded;
}
.document-type-new-page .checkbox-item.checked .checkbox-label {
@apply text-[var(--primary-color)] font-medium;
}
.document-type-new-page .radio-input {
@apply mr-1 h-4 w-4 text-primary-600 border-gray-300 rounded;
@apply focus:ring-primary-500 cursor-pointer;
accent-color: #00684a;
}
/* 更完整的自定义单选按钮样式(可选) */
.document-type-new-page .radio-input:checked {
background-color: #00684a;
border-color: #00684a;
}
+201
View File
@@ -0,0 +1,201 @@
.document-edit-page {
@apply w-full;
}
.document-edit-page .page-header {
@apply flex justify-between items-center mb-4;
}
.document-edit-page .page-title {
@apply text-xl font-medium;
}
.document-edit-page .form-group {
@apply mb-4;
}
.document-edit-page .form-label {
@apply block text-sm font-medium text-gray-700 mb-1;
}
.document-edit-page .form-input,
.document-edit-page .form-textarea,
.document-edit-page .form-select {
@apply w-full rounded-md border border-gray-300 shadow-sm px-3 py-2;
@apply focus:outline-none focus:ring-1 focus:ring-primary-500 focus:border-primary-500;
}
.document-edit-page .text-secondary {
@apply text-gray-500;
}
/* 文档信息样式 */
.document-info {
@apply flex items-start mb-6;
}
.document-icon {
@apply text-3xl mr-4 p-2 bg-gray-50 rounded-lg;
}
.document-details {
@apply flex-1;
}
.document-name {
@apply text-lg font-medium mb-1;
}
.document-meta {
@apply flex items-center flex-wrap gap-4 text-sm text-gray-600;
}
.meta-item {
@apply flex items-center;
}
.meta-item i {
@apply mr-1;
}
/* 状态徽章样式 */
.status-badge {
@apply inline-flex items-center px-2 py-0.5 rounded-full text-xs;
}
.status-pending {
@apply bg-blue-50 text-blue-600;
}
.status-processing {
@apply bg-yellow-50 text-yellow-600;
}
.status-pass {
@apply bg-green-50 text-green-600;
}
.status-warning {
@apply bg-orange-50 text-orange-600;
}
.status-fail {
@apply bg-red-50 text-red-600;
}
/* 提示框样式 */
.alert {
@apply flex items-center p-3 rounded-md;
}
.alert-info {
@apply bg-blue-50 border border-blue-100 text-blue-700;
}
/* 切换开关样式 */
.switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
@apply align-middle;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
@apply bg-primary-500;
}
input:focus + .slider {
@apply ring-2 ring-offset-2 ring-primary-500;
}
input:checked + .slider:before {
transform: translateX(16px);
}
/* 文档预览样式 */
.document-preview {
@apply border border-gray-200 rounded-md overflow-hidden h-96;
}
.preview-toolbar {
@apply px-4 py-2 bg-gray-50 border-b border-gray-200 flex justify-between items-center;
}
.preview-content {
@apply h-full overflow-auto p-4 bg-white;
}
.preview-placeholder {
@apply flex flex-col justify-center items-center h-full text-gray-500;
}
.preview-placeholder i {
@apply text-4xl mb-4 text-gray-300;
}
/* 历史时间线样式 */
.history-timeline {
@apply relative pl-5 mt-5;
}
.history-timeline::before {
content: '';
@apply absolute left-0 top-2 bottom-2 w-0.5 bg-gray-100;
}
.timeline-item {
@apply relative pb-4;
}
.timeline-item::before {
content: '';
@apply absolute w-2.5 h-2.5 rounded-full bg-primary-500 -left-6 top-1.5;
}
.timeline-item:last-child {
@apply pb-0;
}
.timeline-time {
@apply text-xs text-gray-500 mb-1;
}
.timeline-content {
@apply bg-gray-50 p-2 rounded;
}
/* 错误容器样式 */
.error-container {
@apply p-6;
}
+68 -101
View File
File diff suppressed because one or more lines are too long