Merge branch 'shiy' into awen

This commit is contained in:
2025-04-10 10:55:25 +08:00
18 changed files with 2271 additions and 1238 deletions
+211
View File
@@ -0,0 +1,211 @@
import { postgrestGet, postgrestPut, postgrestPost, type PostgrestParams } from '../postgrest-client';
import dayjs from 'dayjs';
/**
* 格式化日期
* @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;
}
// 文档状态枚举
export enum DocumentStatus {
CUTTING = "Cutting",
EXTRACTIONING = "extractioning",
REVIEWING = "reviewing",
COMPLETED = "completed"
}
// 文档类型接口
export interface DocumentType {
id: number;
name: string;
}
// 提取结果接口
interface ExtractedResult {
[key: string]: unknown;
}
// 摘要接口
interface Summary {
[key: string]: unknown;
}
// 文档接口
export interface Document {
id: number;
name: string;
type_id: number;
file_size: number;
status: DocumentStatus;
created_at: string;
document_number?: string;
path?: string;
storage_type?: string;
is_test_document?: boolean;
evaluation_level?: string;
ocr_result?: Record<string, string>;
extracted_results?: ExtractedResult;
sumary?: Summary;
remark?: string;
}
/**
* 获取当天的文档列表
* @returns 文档列表
*/
export async function getTodayDocuments(): Promise<{data: Document[]; error?: never} | {data?: never; error: string; status?: number}> {
try {
const today = dayjs().startOf('day').format('YYYY-MM-DD');
console.log('查询当天文档,日期范围:', today);
const params: PostgrestParams = {
select: `
id,
name,
type_id,
file_size,
status,
created_at,
document_number,
path,
storage_type,
is_test_document,
evaluation_level,
ocr_result,
extracted_results,
sumary,
remark
`,
order: 'created_at.desc',
filter: {
'created_at': `gte.${today}`
}
};
// console.log('发送请求参数:', params);
const response = await postgrestGet<Document[]>('documents', params);
// console.log('API 响应:', response);
if (response.error) {
console.error('API 返回错误:', response.error);
return { error: response.error, status: response.status };
}
const extractedData = extractApiData<Document[]>(response.data);
// console.log('提取后的数据:', extractedData);
if (!extractedData) {
console.error('数据提取失败');
return { error: '获取数据失败', status: 500 };
}
return { data: extractedData };
} catch (error) {
console.error('获取当天文档列表失败:', error);
return {
error: error instanceof Error ? error.message : '获取当天文档列表失败',
status: 500
};
}
}
/**
* 获取文档类型列表
* @returns 文档类型列表
*/
export async function getDocumentTypes(): Promise<{data: DocumentType[]; error?: never} | {data?: never; error: string; status?: number}> {
try {
const params: PostgrestParams = {
select: 'id, name'
};
const response = await postgrestGet<DocumentType[]>('document_types', params);
if (response.error) {
return { error: response.error, status: response.status };
}
const extractedData = extractApiData<DocumentType[]>(response.data);
if (!extractedData) {
return { error: '获取数据失败', status: 500 };
}
return { data: extractedData };
} catch (error) {
console.error('获取文档类型列表失败:', error);
return {
error: error instanceof Error ? error.message : '获取文档类型列表失败',
status: 500
};
}
}
/**
* 获取指定文档的状态
* @param documentIds 文档ID列表
* @returns 文档状态列表
*/
export async function getDocumentsStatus(documentIds: number[]): Promise<{data: Document[]; error?: never} | {data?: never; error: string; status?: number}> {
try {
if (!documentIds || documentIds.length === 0) {
return { data: [] };
}
const params: PostgrestParams = {
select: 'id, status',
filter: {
'id': `in.(${documentIds.join(',')})`
}
};
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 };
}
return { data: extractedData };
} catch (error) {
console.error('获取文档状态失败:', error);
return {
error: error instanceof Error ? error.message : '获取文档状态失败',
status: 500
};
}
}
+493
View File
@@ -0,0 +1,493 @@
import { postgrestGet, postgrestPut, postgrestPost, postgrestDelete, type PostgrestParams } from '../postgrest-client';
import dayjs from 'dayjs';
// 提示词模板接口
export interface PromptTemplate {
id: number;
template_name: string;
template_type: string;
description: string | null;
template_content: string;
variables: Record<string, string>; // 变量定义
status: number;
version: string;
created_by: number;
created_at: string;
updated_at: string;
}
// 提示词模板前端接口
export interface PromptTemplateUI {
id: string;
template_name: string;
template_type: 'Extraction' | 'Evaluation' | 'Summary' | 'Common';
description: string;
template_content: string;
variables: Record<string, string>; // 变量定义
status: 'active' | 'inactive' | 'system';
version: string;
created_by: number;
created_at: string;
updated_at: string;
}
// 搜索参数接口
export interface PromptSearchParams {
name?: string;
type?: string;
status?: 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;
}
/**
* 将API状态值转换为UI状态值
* @param status API状态值(数字)
* @returns UI状态值(字符串)
*/
function mapStatusToUI(status: number): 'active' | 'inactive' | 'system' {
switch(status) {
case 0: return 'inactive';
case 1: return 'active';
case 2: return 'system';
default: return 'inactive';
}
}
/**
* 将UI状态值转换为API状态值
* @param status UI状态值(字符串)
* @returns API状态值(数字)
*/
function mapStatusToAPI(status: string): number {
switch(status) {
case 'active': return 1;
case 'inactive': return 0;
case 'system': return 2;
default: return 0;
}
}
/**
* 将数据库模板转换为UI模板
* @param template 数据库模板
* @returns UI模板
*/
export function convertToUITemplate(template: PromptTemplate): PromptTemplateUI {
return {
id: template.id ? template.id.toString() : '',
template_name: template.template_name,
template_type: template.template_type as "Extraction" | "Evaluation" | "Summary" | "Common",
description: template.description || '',
template_content: template.template_content,
variables: template.variables,
status: mapStatusToUI(template.status),
version: template.version,
created_by: template.created_by,
created_at: formatDate(template.created_at),
updated_at: formatDate(template.updated_at)
};
}
/**
* 获取提示词模板列表
* @param searchParams 搜索参数
* @returns 提示词模板列表和总数
*/
export async function getPromptTemplates(searchParams: PromptSearchParams = {}): Promise<{
data?: { templates: PromptTemplateUI[], total: number };
error?: string;
status?: number;
}> {
try {
console.log('获取提示词模板列表,参数:', searchParams);
const page = searchParams.page || 1;
const pageSize = searchParams.pageSize || 10;
// 构建查询参数
const params: PostgrestParams = {
select: `
id,
template_name,
template_type,
description,
template_content,
variables,
status,
version,
created_by,
created_at,
updated_at
`,
order: 'updated_at.desc',
headers: {
'Prefer': 'count=exact'
},
limit: pageSize,
offset: (page - 1) * pageSize,
filter: {} as Record<string, string>
};
// 添加筛选条件
const filter: Record<string, string> = {};
filter['status'] = `gte.0`;
if (searchParams.name) {
filter['template_name'] = `ilike.%${searchParams.name}%`;
}
if (searchParams.type) {
filter['template_type'] = `eq.${searchParams.type}`;
}
if (searchParams.status) {
let statusValue: number;
switch (searchParams.status) {
case 'active': statusValue = 1; break;
case 'inactive': statusValue = 0; break;
case 'system': statusValue = 2; break;
default: statusValue = -1;
}
if (statusValue >= 0) {
filter['status'] = `eq.${statusValue}`;
}
}
params.filter = filter;
// 发送API请求
console.log('API请求参数:', params);
const response = await postgrestGet<PromptTemplate[]>('prompt_templates', params);
if (response.error) {
console.error('API返回错误:', response.error);
return { error: response.error, status: response.status };
}
// 提取API返回的数据
const extractedData = extractApiData<PromptTemplate[]>(response.data);
if (!extractedData) {
console.error('提取数据失败,原始响应:', response.data);
return { error: '获取提示词模板数据失败', status: 500 };
}
console.log(`成功获取${extractedData.length}条提示词模板数据`);
// 从响应头中获取总数
let totalCount = 0;
const responseWithHeaders = response as { data: PromptTemplate[]; 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: {
templates: extractedData.map(convertToUITemplate),
total: totalCount
}
};
} catch (error) {
console.error('获取提示词模板出错:', error);
return {
error: error instanceof Error ? error.message : '获取提示词模板失败',
status: 500
};
}
}
/**
* 获取提示词模板详情
* @param id 模板ID
* @returns 提示词模板详情
*/
export async function getPromptTemplate(id: string): Promise<{
data?: PromptTemplateUI;
error?: string;
status?: number;
}> {
try {
if (!id) {
return { error: '模板ID不能为空', status: 400 };
}
const params: PostgrestParams = {
select: `
id,
template_name,
template_type,
description,
template_content,
variables,
status,
version,
created_by,
created_at,
updated_at
`,
filter: {
'id': `eq.${id}`
}
};
const response = await postgrestGet<PromptTemplate[]>('prompt_templates', params);
if (response.error) {
return { error: response.error, status: response.status };
}
const extractedData = extractApiData<PromptTemplate[]>(response.data);
if (!extractedData || extractedData.length === 0) {
return { error: '未找到指定模板', status: 404 };
}
return { data: convertToUITemplate(extractedData[0]) };
} catch (error) {
console.error('获取提示词模板详情失败:', error);
return {
error: error instanceof Error ? error.message : '获取提示词模板详情失败',
status: 500
};
}
}
/**
* 创建提示词模板
* @param template 提示词模板数据
* @returns 创建的提示词模板
*/
export async function createPromptTemplate(template: Partial<PromptTemplateUI>): Promise<{
data?: PromptTemplateUI;
error?: string;
status?: number;
}> {
try {
// 验证必填字段
if (!template.template_name || !template.template_type || !template.template_content) {
return { error: '模板名称、类型和内容不能为空', status: 400 };
}
// 准备变量数据
let variablesData: Record<string, string> = {};
if (typeof template.variables === 'string') {
try {
variablesData = JSON.parse(template.variables);
} catch (e) {
console.error('解析变量JSON失败:', e);
}
} else if (template.variables) {
variablesData = template.variables;
}
// 准备API数据
const apiTemplate: Partial<PromptTemplate> = {
template_name: template.template_name,
template_type: template.template_type,
description: template.description || null,
template_content: template.template_content,
variables: variablesData,
status: mapStatusToAPI(template.status || 'active'),
version: template.version || 'v1.0',
created_by: 1 // 固定创建者为1
};
if(apiTemplate){
console.log('apiTemplate', apiTemplate);
// throw new Error('测试错误');
}
const response = await postgrestPost<PromptTemplate, Partial<PromptTemplate>>(
'prompt_templates',
apiTemplate
);
if (response.error) {
return { error: response.error, status: response.status };
}
const extractedData = extractApiData<PromptTemplate>(response.data);
if (!extractedData) {
return { error: '创建提示词模板失败', status: 500 };
}
return { data: convertToUITemplate(extractedData) };
} catch (error) {
console.error('创建提示词模板失败:', error);
return {
error: error instanceof Error ? error.message : '创建提示词模板失败',
status: 500
};
}
}
/**
* 更新提示词模板
* @param id 模板ID
* @param template 提示词模板数据
* @returns 更新后的提示词模板
*/
export async function updatePromptTemplate(id: string, template: Partial<PromptTemplateUI>): Promise<{
data?: PromptTemplateUI;
error?: string;
status?: number;
}> {
try {
if (!id) {
return { error: '模板ID不能为空', status: 400 };
}
// 准备变量数据
let variablesData: Record<string, string> = {};
if (typeof template.variables === 'string') {
try {
variablesData = JSON.parse(template.variables);
} catch (e) {
console.error('解析变量JSON失败:', e);
}
} else if (template.variables) {
variablesData = template.variables;
}
// 准备API数据
const apiTemplate: Partial<PromptTemplate> = {};
if (template.template_name !== undefined) {
apiTemplate.template_name = template.template_name;
}
if (template.template_type !== undefined) {
apiTemplate.template_type = template.template_type;
}
if (template.description !== undefined) {
apiTemplate.description = template.description;
}
if (template.template_content !== undefined) {
apiTemplate.template_content = template.template_content;
}
if (template.variables !== undefined) {
apiTemplate.variables = variablesData;
}
if (template.status !== undefined) {
apiTemplate.status = mapStatusToAPI(template.status);
}
if (template.version !== undefined) {
apiTemplate.version = template.version;
}
// if(apiTemplate){
// console.log('apiTemplate', apiTemplate);
// throw new Error('测试错误');
// }
const response = await postgrestPut<PromptTemplate, Partial<PromptTemplate>>(
'prompt_templates',
apiTemplate,
{ id }
);
if (response.error) {
return { error: response.error, status: response.status };
}
const extractedData = extractApiData<PromptTemplate>(response.data);
if (!extractedData) {
return { error: '更新提示词模板失败', status: 500 };
}
return { data: convertToUITemplate(extractedData) };
} catch (error) {
console.error('更新提示词模板失败:', error);
return {
error: error instanceof Error ? error.message : '更新提示词模板失败',
status: 500
};
}
}
/**
* 删除提示词模板
* @param id 模板ID
* @returns 成功或失败信息
*/
export async function deletePromptTemplate(id: string): Promise<{
success?: boolean;
error?: string;
status?: number;
}> {
try {
if (!id) {
return { error: '模板ID不能为空', status: 400 };
}
// 使用真实删除替代状态更新
const response = await postgrestDelete<PromptTemplate>(
'prompt_templates',
{
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
};
}
}
+322
View File
@@ -0,0 +1,322 @@
import { postgrestGet, postgrestPut, postgrestPost, type PostgrestParams } from '../postgrest-client';
import dayjs from 'dayjs';
// 配置项接口
export interface ConfigItem {
id: number;
name: string;
type: string;
description: string;
environment: string;
config: Record<string, unknown>;
remark: string;
is_active: boolean;
version: string;
created_by: number;
created_at: string;
updated_at: string;
}
/**
* 格式化日期
* @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;
}
// 获取配置列表
export async function getConfigLists(params: {
name?: string;
type?: string;
environment?: string;
is_active?: boolean;
page?: number;
pageSize?: number;
}): Promise<{data: ConfigItem[]; total: number; error?: never} | {data?: never; error: string}> {
try {
const {
name,
type,
environment,
is_active,
page = 1,
pageSize = 10
} = params;
// 构建查询参数
const queryParams: PostgrestParams = {
select: '*',
order: 'created_at.desc',
limit: pageSize,
offset: (page-1)*pageSize,
filter: {} as Record<string, string>,
headers: {
'Prefer': 'count=exact'
}
};
// 添加筛选条件
const filter: Record<string, string> = {};
if (name) {
filter['name'] = `ilike.%${name}%`;
}
if (type) {
filter['type'] = `eq.${type}`;
}
if (environment) {
filter['environment'] = `eq.${environment}`;
}
if (is_active !== undefined) {
filter['is_active'] = `eq.${is_active}`;
}
queryParams.filter = filter;
// 获取数据
const response = await postgrestGet<ConfigItem[]>('configurations', queryParams);
if (response.error) {
return { error: response.error };
}
const data = extractApiData<ConfigItem[]>(response.data);
if (!data) {
return { error: '获取数据失败' };
}
// 格式化日期
const formattedData = data.map(item => ({
...item,
created_at: formatDate(item.created_at),
updated_at: formatDate(item.updated_at)
}));
// 从响应头中获取总数
let totalCount = 0;
const responseWithHeaders = response as { data: ConfigItem[]; 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: formattedData,
total: totalCount
};
} catch (error) {
console.error('获取配置列表失败:', error);
return { error: error instanceof Error ? error.message : '获取配置列表失败' };
}
}
// 获取配置类型和环境选项
export async function getConfigOptions(): Promise<{data: {types: string[]; environments: string[]}; error?: never} | {data?: never; error: string}> {
try {
// 获取类型选项
const typeResponse = await postgrestGet<{type: string}[]>('configurations', {
select: 'type'
});
if (typeResponse.error) {
return { error: typeResponse.error };
}
const typesData = extractApiData<{type: string}[]>(typeResponse.data);
if (!typesData) {
return { error: '获取类型选项失败' };
}
// 获取环境选项
const envResponse = await postgrestGet<{environment: string}[]>('configurations', {
select: 'environment'
});
if (envResponse.error) {
return { error: envResponse.error };
}
const envData = extractApiData<{environment: string}[]>(envResponse.data);
if (!envData) {
return { error: '获取环境选项失败' };
}
// 手动去重
const types = [...new Set(typesData.map(item => item.type))];
const environments = [...new Set(envData.map(item => item.environment))];
return {
data: {
types,
environments
}
};
} catch (error) {
console.error('获取配置选项失败:', error);
return { error: error instanceof Error ? error.message : '获取配置选项失败' };
}
}
// 获取配置详情
export async function getConfigDetail(id: string): Promise<{data: ConfigItem; error?: never} | {data?: never; error: string}> {
try {
const response = await postgrestGet<ConfigItem[]>('configurations', {
filter: {
'id': `eq.${id}`
}
});
if (response.error) {
return { error: response.error };
}
const data = extractApiData<ConfigItem[]>(response.data);
if (!data || data.length === 0) {
return { error: '未找到配置' };
}
const config = data[0];
return {
data: {
...config,
created_at: formatDate(config.created_at),
updated_at: formatDate(config.updated_at)
}
};
} catch (error) {
console.error('获取配置详情失败:', error);
return { error: error instanceof Error ? error.message : '获取配置详情失败' };
}
}
// 创建配置
export async function createConfig(data: {
name: string;
type: string;
environment: string;
config: Record<string, unknown>;
is_active: boolean;
remark?: string;
}): Promise<{data: ConfigItem; error?: never} | {data?: never; error: string}> {
try {
const response = await postgrestPost<ConfigItem, typeof data>('configurations', data);
if (response.error) {
return { error: response.error };
}
const createdData = extractApiData<ConfigItem>(response.data);
if (!createdData) {
return { error: '创建配置失败' };
}
return {
data: {
...createdData,
created_at: formatDate(createdData.created_at),
updated_at: formatDate(createdData.updated_at)
}
};
} catch (error) {
console.error('创建配置失败:', error);
return { error: error instanceof Error ? error.message : '创建配置失败' };
}
}
// 更新配置
export async function updateConfig(id: string, data: {
name: string;
type: string;
environment: string;
config: Record<string, unknown>;
is_active: boolean;
remark?: string;
}): Promise<{data: ConfigItem; error?: never} | {data?: never; error: string}> {
try {
const response = await postgrestPut<ConfigItem, typeof data>('configurations', data, {
id: id.toString()
});
if (response.error) {
return { error: response.error };
}
const updatedData = extractApiData<ConfigItem>(response.data);
if (!updatedData) {
return { error: '更新配置失败' };
}
return {
data: {
...updatedData,
created_at: formatDate(updatedData.created_at),
updated_at: formatDate(updatedData.updated_at)
}
};
} catch (error) {
console.error('更新配置失败:', error);
return { error: error instanceof Error ? error.message : '更新配置失败' };
}
}
// 更新配置状态
export async function updateConfigStatus(id: number, is_active: boolean): Promise<{success: boolean; error?: string}> {
try {
const response = await postgrestPut<ConfigItem, {is_active: boolean}>(
'configurations',
{ is_active },
{ id: id.toString() }
);
if (response.error) {
return { success: false, error: response.error };
}
const updatedData = extractApiData<ConfigItem>(response.data);
if (!updatedData) {
return { success: false, error: '更新配置状态失败' };
}
return { success: true };
} catch (error) {
console.error('更新配置状态失败:', error);
return {
success: false,
error: error instanceof Error ? error.message : '更新配置状态失败'
};
}
}
+13 -21
View File
@@ -37,12 +37,12 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
path: '/files/upload', path: '/files/upload',
icon: 'ri-upload-cloud-line' icon: 'ri-upload-cloud-line'
}, },
{ // {
id: 'file-list', // id: 'file-list',
title: '文件列表', // title: '文件列表',
path: '/files', // path: '/files',
icon: 'ri-file-list-3-line' // icon: 'ri-file-list-3-line'
}, // },
{ {
id:'documents', id:'documents',
title:'文档列表', title:'文档列表',
@@ -80,15 +80,7 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
title: '新增评查点', title: '新增评查点',
path: '/rules/new', path: '/rules/new',
icon: 'ri-add-circle-line' icon: 'ri-add-circle-line'
} },
]
},
{
id: 'review-management',
title: '评查结果',
path: '/reviews',
icon: 'ri-bar-chart-box-line',
children: [
{ {
id: 'review-detail', id: 'review-detail',
title: '评查详情', title: '评查详情',
@@ -109,12 +101,12 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
path: '/config-lists', path: '/config-lists',
icon: 'ri-list-check-3' icon: 'ri-list-check-3'
}, },
{ // {
id: 'basic-settings', // id: 'basic-settings',
title: '基础设置', // title: '基础设置',
path: '/settings', // path: '/settings',
icon: 'ri-equalizer-line' // icon: 'ri-equalizer-line'
}, // },
{ {
id: 'document-types', id: 'document-types',
title: '文档类型', title: '文档类型',
+18 -2
View File
@@ -50,7 +50,15 @@ export const UploadArea = forwardRef<UploadAreaRef, UploadAreaProps>(({
})); }));
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (!disabled && !shouldPreventFileSelect && fileInputRef.current) { if (disabled) return;
if (shouldPreventFileSelect) {
// 如果应该阻止文件选择,则触发表单提交
const form = fileInputRef.current?.closest('form');
if (form) {
form.requestSubmit();
}
} else if (fileInputRef.current) {
fileInputRef.current.click(); fileInputRef.current.click();
} }
}, [disabled, shouldPreventFileSelect]); }, [disabled, shouldPreventFileSelect]);
@@ -76,7 +84,15 @@ export const UploadArea = forwardRef<UploadAreaRef, UploadAreaProps>(({
e.preventDefault(); e.preventDefault();
setIsDragOver(false); setIsDragOver(false);
if (!disabled && !shouldPreventFileSelect && e.dataTransfer.files.length > 0) { if (disabled) return;
if (shouldPreventFileSelect) {
// 如果应该阻止文件选择,则触发表单提交
const form = e.currentTarget.closest('form');
if (form) {
form.requestSubmit();
}
} else if (e.dataTransfer.files.length > 0) {
onFilesSelected(e.dataTransfer.files); onFilesSelected(e.dataTransfer.files);
} }
}, [disabled, shouldPreventFileSelect, onFilesSelected]); }, [disabled, shouldPreventFileSelect, onFilesSelected]);
+84 -208
View File
@@ -7,13 +7,13 @@ import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterP
import { Pagination } from "~/components/ui/Pagination"; import { Pagination } from "~/components/ui/Pagination";
import { Table } from "~/components/ui/Table"; import { Table } from "~/components/ui/Table";
import { Tag } from "~/components/ui/Tag"; import { Tag } from "~/components/ui/Tag";
import { getConfigLists, getConfigOptions, updateConfigStatus, type ConfigItem } from "~/api/system_setting/config-lists";
import configListsStyles from "~/styles/pages/config-lists_index.css?url"; import configListsStyles from "~/styles/pages/config-lists_index.css?url";
export const links = () => [ export const links = () => [
{ rel: "stylesheet", href: configListsStyles } { rel: "stylesheet", href: configListsStyles }
]; ];
export const meta: MetaFunction = () => { export const meta: MetaFunction = () => {
return [ return [
{ title: "系统配置管理 - 中国烟草AI合同及卷宗审核系统" }, { title: "系统配置管理 - 中国烟草AI合同及卷宗审核系统" },
@@ -54,165 +54,55 @@ export const MODULE_LABELS: Record<ConfigModule, string> = {
[ConfigModule.NOTIFICATION]: '通知' [ConfigModule.NOTIFICATION]: '通知'
}; };
// 配置数据类型
interface ConfigDataType {
[key: string]: string | number | boolean | string[] | ConfigDataType | ConfigDataType[];
}
// 配置项模型
interface ConfigItem {
id: string;
configName: string;
module: ConfigModule;
environment: ConfigEnvironment;
isActive: boolean;
configData: ConfigDataType;
createdAt: string;
updatedAt: string;
}
interface LoaderData { interface LoaderData {
configs: ConfigItem[]; configs: ConfigItem[];
totalCount: number; totalCount: number;
currentPage: number; currentPage: number;
pageSize: number; pageSize: number;
totalPages: number; totalPages: number;
types: string[];
environments: string[];
} }
export async function loader({ request }: LoaderFunctionArgs) { export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url); const url = new URL(request.url);
const configName = url.searchParams.get("configName") || ""; const name = url.searchParams.get("name") || "";
const module = url.searchParams.get("module") || ""; const type = url.searchParams.get("type") || "";
const environment = url.searchParams.get("environment") || ""; const environment = url.searchParams.get("environment") || "";
const isActive = url.searchParams.get("isActive") || ""; const is_active = url.searchParams.get("is_active") ? url.searchParams.get("is_active") === "true" : undefined;
const currentPage = parseInt(url.searchParams.get("page") || "1", 10); const currentPage = parseInt(url.searchParams.get("page") || "1", 10);
const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10); const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10);
try { try {
// 模拟数据,实际项目中应从API获取 // 获取配置列表
const mockConfigs: ConfigItem[] = [ const configsResponse = await getConfigLists({
{ name,
id: "1", type,
configName: "database_connection", environment,
module: ConfigModule.SYSTEM, is_active,
environment: ConfigEnvironment.PROD, page: currentPage,
isActive: true, pageSize
configData: { });
database: {
host: "db.cluster.com",
port: 5432,
pool_size: 20,
ssl: true
},
cache: {
ttl: 3600,
max_entries: 1000
},
feature_flags: ["new_ui", "analytics_v2"]
},
createdAt: "2023-07-10 10:15:23",
updatedAt: "2023-07-15 14:30:26"
},
{
id: "2",
configName: "text_extraction_ai",
module: ConfigModule.AI,
environment: ConfigEnvironment.TEST,
isActive: true,
configData: {
model: "gpt-4",
parameters: {
temperature: 0.7,
max_tokens: 2000
},
api_key: "sk-**********",
timeout: 30
},
createdAt: "2023-07-12 08:45:12",
updatedAt: "2023-07-14 09:15:33"
},
{
id: "3",
configName: "notification_service",
module: ConfigModule.NOTIFICATION,
environment: ConfigEnvironment.DEV,
isActive: false,
configData: {
email: {
smtp_server: "smtp.example.com",
port: 587,
use_tls: true,
sender: "noreply@example.com"
},
sms: {
provider: "aliyun",
region: "cn-hangzhou",
sign_name: "AI审核系统"
}
},
createdAt: "2023-07-05 13:20:45",
updatedAt: "2023-07-10 16:45:19"
},
{
id: "4",
configName: "file_storage",
module: ConfigModule.FILE,
environment: ConfigEnvironment.PROD,
isActive: true,
configData: {
type: "oss",
region: "cn-shanghai",
bucket: "contracts-ai-review",
access_control: "private",
lifecycle_rules: [
{
prefix: "temp/",
ttl_days: 7
}
]
},
createdAt: "2023-06-28 09:30:18",
updatedAt: "2023-07-08 11:22:07"
}
];
// 过滤数据 if (configsResponse.error || !configsResponse.data) {
let filteredConfigs = [...mockConfigs]; throw new Error(configsResponse.error || "获取配置列表失败");
if (configName) {
filteredConfigs = filteredConfigs.filter(config =>
config.configName.toLowerCase().includes(configName.toLowerCase())
);
} }
if (module) { // 获取配置选项
filteredConfigs = filteredConfigs.filter(config => config.module === module); const optionsResponse = await getConfigOptions();
if (optionsResponse.error || !optionsResponse.data) {
throw new Error(optionsResponse.error || "获取配置选项失败");
} }
if (environment) {
filteredConfigs = filteredConfigs.filter(config => config.environment === environment);
}
if (isActive) {
const activeValue = isActive === 'true';
filteredConfigs = filteredConfigs.filter(config => config.isActive === activeValue);
}
// 计算分页信息
const totalCount = filteredConfigs.length;
const totalPages = Math.ceil(totalCount / pageSize);
// 分页截取
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedConfigs = filteredConfigs.slice(startIndex, endIndex);
return json<LoaderData>({ return json<LoaderData>({
configs: paginatedConfigs, configs: configsResponse.data,
totalCount, totalCount: configsResponse.total,
currentPage, currentPage,
pageSize, pageSize,
totalPages totalPages: Math.ceil(configsResponse.total / pageSize),
types: optionsResponse.data.types,
environments: optionsResponse.data.environments
}, { }, {
headers: { headers: {
"Cache-Control": "max-age=60, s-maxage=180" "Cache-Control": "max-age=60, s-maxage=180"
@@ -233,28 +123,18 @@ export async function action({ request }: ActionFunctionArgs) {
return json({ success: false, error: "缺少配置ID" }, { status: 400 }); return json({ success: false, error: "缺少配置ID" }, { status: 400 });
} }
// 进行更新启用和禁用的状态
try { try {
if (_action === 'toggleStatus') { if (_action === 'toggleStatus') {
const isActive = formData.get('isActive') === 'true'; const is_active = formData.get('is_active') === 'true';
const newStatus = !isActive;
// 实际项目中应调用API更新状态 const response = await updateConfigStatus(parseInt(configId as string), is_active);
console.log(`切换配置 ${configId} 状态为: ${newStatus}`);
// 模拟API调用 if (!response.success) {
// const response = await fetch(`/api/configs/${configId}/status`, { return json({ success: false, error: response.error }, { status: 500 });
// method: 'PATCH', }
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify({ isActive: newStatus }),
// });
// if (!response.ok) { return json({ success: true });
// throw new Error(`状态切换失败: ${response.status}`);
// }
return json({ success: true, newStatus });
} }
return json({ success: false, error: "未知操作" }, { status: 400 }); return json({ success: false, error: "未知操作" }, { status: 400 });
@@ -275,7 +155,7 @@ export function ErrorBoundary() {
} }
export default function ConfigListsIndex() { export default function ConfigListsIndex() {
const { configs, totalCount, currentPage, pageSize } = useLoaderData<typeof loader>(); const { configs, totalCount, currentPage, pageSize, types, environments } = useLoaderData<typeof loader>();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const submit = useSubmit(); const submit = useSubmit();
const [showDetailModal, setShowDetailModal] = useState(false); const [showDetailModal, setShowDetailModal] = useState(false);
@@ -300,9 +180,9 @@ export default function ConfigListsIndex() {
const handleConfigNameSearch = (value: string) => { const handleConfigNameSearch = (value: string) => {
const newParams = new URLSearchParams(searchParams); const newParams = new URLSearchParams(searchParams);
if (value) { if (value) {
newParams.set('configName', value); newParams.set('name', value);
} else { } else {
newParams.delete('configName'); newParams.delete('name');
} }
// 搜索时,重置到第一页 // 搜索时,重置到第一页
@@ -312,11 +192,11 @@ export default function ConfigListsIndex() {
}; };
const handleToggleStatus = (config: ConfigItem) => { const handleToggleStatus = (config: ConfigItem) => {
if (window.confirm(`确定要${config.isActive ? '禁用' : '启用'}该配置吗?`)) { if (window.confirm(`确定要${config.is_active ? '禁用' : '启用'}该配置吗?`)) {
const formData = new FormData(); const formData = new FormData();
formData.append('_action', 'toggleStatus'); formData.append('_action', 'toggleStatus');
formData.append('configId', config.id); formData.append('configId', config.id.toString());
formData.append('isActive', String(config.isActive)); formData.append('is_active', String(!config.is_active));
submit(formData, { method: 'post' }); submit(formData, { method: 'post' });
} }
@@ -342,9 +222,19 @@ export default function ConfigListsIndex() {
// 处理重置筛选 // 处理重置筛选
const handleReset = () => { const handleReset = () => {
setSearchParams(new URLSearchParams()); const nameInput = document.querySelector('input[placeholder="请输入配置名称"]') as HTMLInputElement;
}; const typeSelect = document.querySelector('select[name="type"]') as HTMLInputElement;
const environmentSelect = document.querySelector('select[name="environment"]') as HTMLInputElement;
const statusSelect = document.querySelector('select[name="is_active"]') as HTMLInputElement;
setSearchParams(new URLSearchParams());
if(nameInput) nameInput.value = ''
if(typeSelect) typeSelect.value = ''
if(environmentSelect) environmentSelect.value = ''
if(statusSelect) statusSelect.value = ''
};
// 关闭详情模态框 // 关闭详情模态框
const closeDetailModal = () => { const closeDetailModal = () => {
@@ -356,43 +246,42 @@ export default function ConfigListsIndex() {
const columns = [ const columns = [
{ {
title: "配置名称", title: "配置名称",
dataIndex: "configName" as keyof ConfigItem, dataIndex: "name" as keyof ConfigItem,
key: "configName", key: "name",
width: "20%" width: "20%"
}, },
{ {
title: "所属模块", title: "所属模块",
key: "module", key: "type",
width: "10%", width: "10%",
render: (_: unknown, record: ConfigItem) => MODULE_LABELS[record.module] render: (_: unknown, record: ConfigItem) => record.type
}, },
{ {
title: "环境", title: "环境",
key: "environment", key: "environment",
width: "15%", width: "15%",
render: (_: unknown, record: ConfigItem) => { render: (_: unknown, record: ConfigItem) => {
const envClass = `env-tag env-tag-${record.environment}`;
return ( return (
<span className={envClass}> <span className="env-tag">
{ENVIRONMENT_LABELS[record.environment]} {record.environment}
</span> </span>
); );
} }
}, },
{ {
title: "状态", title: "状态",
key: "isActive", key: "is_active",
width: "15%", width: "15%",
render: (_: unknown, record: ConfigItem) => ( render: (_: unknown, record: ConfigItem) => (
<Tag color={record.isActive ? 'green' : 'red'}> <Tag color={record.is_active ? 'green' : 'red'}>
{record.isActive ? '已启用' : '已禁用'} {record.is_active ? '已启用' : '已禁用'}
</Tag> </Tag>
) )
}, },
{ {
title: "最后更新时间", title: "最后更新时间",
dataIndex: "updatedAt" as keyof ConfigItem, dataIndex: "updated_at" as keyof ConfigItem,
key: "updatedAt", key: "updated_at",
width: "15%" width: "15%"
}, },
{ {
@@ -417,29 +306,17 @@ export default function ConfigListsIndex() {
</Link> </Link>
<button <button
type="button" type="button"
className={`operation-btn ${record.isActive ? '!text-[--color-warning]' : '!text-[--color-success]'}`} className={`operation-btn ${record.is_active ? '!text-[--color-warning]' : '!text-[--color-success]'}`}
onClick={() => handleToggleStatus(record)} onClick={() => handleToggleStatus(record)}
> >
<i className={record.isActive ? `ri-stop-circle-line` : `ri-play-circle-line`}></i> <i className={record.is_active ? `ri-stop-circle-line` : `ri-play-circle-line`}></i>
{record.isActive ? '禁用' : '启用'} {record.is_active ? '禁用' : '启用'}
</button> </button>
</div> </div>
) )
} }
]; ];
// 生成环境选项
const environmentOptions = Object.entries(ENVIRONMENT_LABELS).map(([value, label]) => ({
value,
label
}));
// 生成模块选项
const moduleOptions = Object.entries(MODULE_LABELS).map(([value, label]) => ({
value,
label
}));
return ( return (
<div className="config-lists"> <div className="config-lists">
{/* 页面头部 */} {/* 页面头部 */}
@@ -458,9 +335,9 @@ export default function ConfigListsIndex() {
<Button type="default" icon="ri-refresh-line" onClick={handleReset} className="mr-2"> <Button type="default" icon="ri-refresh-line" onClick={handleReset} className="mr-2">
</Button> </Button>
<Button type="primary" icon="ri-search-line"> {/* <Button type="primary" icon="ri-search-line">
查询 查询
</Button> </Button> */}
</> </>
} }
noActionDivider={true} noActionDivider={true}
@@ -468,7 +345,7 @@ export default function ConfigListsIndex() {
<SearchFilter <SearchFilter
label="配置名称" label="配置名称"
placeholder="请输入配置名称" placeholder="请输入配置名称"
value={searchParams.get('configName') || ''} value={searchParams.get('name') || ''}
onSearch={handleConfigNameSearch} onSearch={handleConfigNameSearch}
className="flex-1 min-w-[200px]" className="flex-1 min-w-[200px]"
instantSearch={true} instantSearch={true}
@@ -476,9 +353,9 @@ export default function ConfigListsIndex() {
<FilterSelect <FilterSelect
label="所属模块" label="所属模块"
name="module" name="type"
value={searchParams.get('module') || ''} value={searchParams.get('type') || ''}
options={[{ value: '', label: '全部' }, ...moduleOptions]} options={[ ...types.map(type => ({ value: type, label: type }))]}
onChange={handleFilterChange} onChange={handleFilterChange}
className="flex-1 min-w-[200px]" className="flex-1 min-w-[200px]"
/> />
@@ -487,17 +364,16 @@ export default function ConfigListsIndex() {
label="环境" label="环境"
name="environment" name="environment"
value={searchParams.get('environment') || ''} value={searchParams.get('environment') || ''}
options={[{ value: '', label: '全部' }, ...environmentOptions]} options={[ ...environments.map(env => ({ value: env, label: env }))]}
onChange={handleFilterChange} onChange={handleFilterChange}
className="flex-1 min-w-[200px]" className="flex-1 min-w-[200px]"
/> />
<FilterSelect <FilterSelect
label="状态" label="状态"
name="isActive" name="is_active"
value={searchParams.get('isActive') || ''} value={searchParams.get('is_active') || ''}
options={[ options={[
{ value: '', label: '全部' },
{ value: 'true', label: '已启用' }, { value: 'true', label: '已启用' },
{ value: 'false', label: '已禁用' } { value: 'false', label: '已禁用' }
]} ]}
@@ -545,19 +421,19 @@ export default function ConfigListsIndex() {
<div className="config-detail-content"> <div className="config-detail-content">
<div className="config-detail-item"> <div className="config-detail-item">
<div className="config-detail-label"></div> <div className="config-detail-label"></div>
<div className="config-detail-value">{selectedConfig.configName}</div> <div className="config-detail-value">{selectedConfig.name}</div>
</div> </div>
<div className="config-detail-item"> <div className="config-detail-item">
<div className="config-detail-label"></div> <div className="config-detail-label"></div>
<div className="config-detail-value">{MODULE_LABELS[selectedConfig.module]}</div> <div className="config-detail-value">{selectedConfig.type}</div>
</div> </div>
<div className="config-detail-item"> <div className="config-detail-item">
<div className="config-detail-label"></div> <div className="config-detail-label"></div>
<div className="config-detail-value"> <div className="config-detail-value">
<span className={`env-tag env-tag-${selectedConfig.environment}`}> <span className="env-tag">
{ENVIRONMENT_LABELS[selectedConfig.environment]} {selectedConfig.environment}
</span> </span>
</div> </div>
</div> </div>
@@ -565,8 +441,8 @@ export default function ConfigListsIndex() {
<div className="config-detail-item"> <div className="config-detail-item">
<div className="config-detail-label"></div> <div className="config-detail-label"></div>
<div className="config-detail-value"> <div className="config-detail-value">
<Tag color={selectedConfig.isActive ? 'green' : 'red'}> <Tag color={selectedConfig.is_active ? 'green' : 'red'}>
{selectedConfig.isActive ? '已启用' : '已禁用'} {selectedConfig.is_active ? '已启用' : '已禁用'}
</Tag> </Tag>
</div> </div>
</div> </div>
@@ -574,19 +450,19 @@ export default function ConfigListsIndex() {
<div className="config-detail-item"> <div className="config-detail-item">
<div className="config-detail-label"></div> <div className="config-detail-label"></div>
<pre className="config-detail-code"> <pre className="config-detail-code">
{JSON.stringify(selectedConfig.configData, null, 2)} {JSON.stringify(selectedConfig.config, null, 2)}
</pre> </pre>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="config-detail-item"> <div className="config-detail-item">
<div className="config-detail-label"></div> <div className="config-detail-label"></div>
<div className="config-detail-value">{selectedConfig.createdAt}</div> <div className="config-detail-value">{selectedConfig.created_at}</div>
</div> </div>
<div className="config-detail-item"> <div className="config-detail-item">
<div className="config-detail-label"></div> <div className="config-detail-label"></div>
<div className="config-detail-value">{selectedConfig.updatedAt}</div> <div className="config-detail-value">{selectedConfig.updated_at}</div>
</div> </div>
</div> </div>
</div> </div>
+171 -281
View File
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import { Button } from "~/components/ui/Button"; import { Button } from "~/components/ui/Button";
import { Card } from "~/components/ui/Card"; import { Card } from "~/components/ui/Card";
import { ConfigModule, MODULE_LABELS, ENVIRONMENT_LABELS } from "./config-lists._index"; import { ConfigModule, MODULE_LABELS, ENVIRONMENT_LABELS } from "./config-lists._index";
import { getConfigOptions, getConfigDetail, createConfig, updateConfig } from "~/api/system_setting/config-lists";
import configNewStyles from "~/styles/pages/config-lists_new.css?url"; import configNewStyles from "~/styles/pages/config-lists_new.css?url";
export const links = () => [ export const links = () => [
@@ -39,18 +40,20 @@ export const EXTENDED_ENVIRONMENT_LABELS: Record<string, string> = {
}; };
interface ConfigData { interface ConfigData {
id: string; id: number;
configName: string; name: string;
module: ConfigModule; type: string;
environment: string; // 使用扩展的环境类型 environment: string;
isActive: boolean; is_active: boolean;
configData: string; // JSON字符串 config: Record<string, unknown>;
remarks?: string; // 添加备注字段 remark?: string;
} }
interface LoaderData { interface LoaderData {
config?: ConfigData; config?: ConfigData;
isEdit: boolean; isEdit: boolean;
types: string[];
environments: string[];
} }
export async function loader({ request }: LoaderFunctionArgs) { export async function loader({ request }: LoaderFunctionArgs) {
@@ -58,193 +61,107 @@ export async function loader({ request }: LoaderFunctionArgs) {
const id = url.searchParams.get("id"); const id = url.searchParams.get("id");
let config: ConfigData | undefined = undefined; let config: ConfigData | undefined = undefined;
if (id) { // 获取配置选项
try { const optionsResponse = await getConfigOptions();
// 实际应用中,应从API获取配置详情 if (optionsResponse.error) {
// const response = await fetch(`${process.env.API_BASE_URL}/api/configs/${id}`); throw new Error(optionsResponse.error);
// if (!response.ok) throw new Error(`获取配置详情失败: ${response.status}`); }
// config = await response.json();
// config.configData = JSON.stringify(config.configData, null, 2);
// 使用模拟数据 if (id) {
if (id === "1") { // 获取配置详情
config = { const detailResponse = await getConfigDetail(id);
id: "1", if (detailResponse.error) {
configName: "database_connection", throw new Error(detailResponse.error);
module: ConfigModule.SYSTEM,
environment: ExtendedConfigEnvironment.PROD,
isActive: true,
remarks: "数据库连接配置,包含主库和从库配置",
configData: JSON.stringify({
database: {
host: "db.cluster.com",
port: 5432,
pool_size: 20,
ssl: true
},
cache: {
ttl: 3600,
max_entries: 1000
},
feature_flags: ["new_ui", "analytics_v2"]
}, null, 2)
};
} else if (id === "2") {
config = {
id: "2",
configName: "text_extraction_ai",
module: ConfigModule.AI,
environment: ExtendedConfigEnvironment.TEST,
isActive: true,
remarks: "AI文本抽取服务配置",
configData: JSON.stringify({
model: "gpt-4",
parameters: {
temperature: 0.7,
max_tokens: 2000
},
api_key: "sk-**********",
timeout: 30
}, null, 2)
};
} else if (id === "3") {
config = {
id: "3",
configName: "notification_service",
module: ConfigModule.NOTIFICATION,
environment: ExtendedConfigEnvironment.DEV,
isActive: false,
remarks: "通知服务配置,目前处于开发测试阶段",
configData: JSON.stringify({
email: {
smtp_server: "smtp.example.com",
port: 587,
use_tls: true,
sender: "noreply@example.com"
},
sms: {
provider: "aliyun",
region: "cn-hangzhou",
sign_name: "AI审核系统"
}
}, null, 2)
};
} else if (id === "4") {
config = {
id: "4",
configName: "file_storage",
module: ConfigModule.FILE,
environment: ExtendedConfigEnvironment.COMMON,
isActive: true,
remarks: "文件存储通用配置,适用于所有环境",
configData: JSON.stringify({
type: "oss",
region: "cn-shanghai",
bucket: "contracts-ai-review",
access_control: "private",
lifecycle_rules: [
{
prefix: "temp/",
ttl_days: 7
}
]
}, null, 2)
};
}
} catch (error) {
console.error("获取配置详情失败:", error);
// 在实际应用中,应该将错误信息返回给客户端
// 这里简单处理,返回空config
} }
config = detailResponse.data;
} }
return Response.json({ return Response.json({
config, config,
isEdit: !!config isEdit: !!config,
types: optionsResponse.data?.types || [],
environments: optionsResponse.data?.environments || []
}); });
} }
interface ActionData { interface ActionData {
success?: boolean; success?: boolean;
errors?: { errors?: {
configName?: string; name?: string;
module?: string; type?: string;
environment?: string; environment?: string;
configData?: string; config?: string;
general?: string; general?: string;
}; };
} }
export async function action({ request }: ActionFunctionArgs) { export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData(); const formData = await request.formData();
const configId = formData.get("id") as string; const id = formData.get("id") as string;
const configName = formData.get("configName") as string; const name = formData.get("name") as string;
const module = formData.get("module") as string; const type = formData.get("type") as string;
const environment = formData.get("environment") as string; const environment = formData.get("environment") as string;
const configData = formData.get("configData") as string; const config = formData.get("config") as string;
const isActive = formData.get("isActive") === "true"; const is_active = formData.get("is_active") === "true";
const remarks = formData.get("remarks") as string; const remark = formData.get("remark") as string;
const errors: ActionData["errors"] = {}; const errors: ActionData["errors"] = {};
// 表单验证 // 表单验证
if (!configName || configName.trim() === "") { if (!name || name.trim() === "") {
errors.configName = "配置名称不能为空"; errors.name = "配置名称不能为空";
} }
if (!module) { if (!type) {
errors.module = "请选择所属模块"; errors.type = "请选择所属模块";
} }
if (!environment) { if (!environment) {
errors.environment = "请选择环境"; errors.environment = "请选择环境";
} }
if (!configData || configData.trim() === "") { if (!config || config.trim() === "") {
errors.configData = "配置数据不能为空"; errors.config = "配置数据不能为空";
} else { } else {
try { try {
JSON.parse(configData); JSON.parse(config);
} catch (e) { } catch (e) {
errors.configData = "配置数据必须是有效的JSON格式"; errors.config = "配置数据必须是有效的JSON格式";
} }
} }
if (Object.keys(errors).length > 0) { if (Object.keys(errors).length > 0) {
return json<ActionData>({ errors }); return Response.json({ errors });
} }
try { try {
// 实际应用中,应调用API保存数据 const configData = {
console.log("保存配置:", { configId, configName, module, environment, configData, isActive, remarks }); name,
type,
environment,
config: JSON.parse(config),
is_active,
remark
};
// 模拟API调用 if (id) {
// const response = await fetch(`${process.env.API_BASE_URL}/api/configs${configId ? `/${configId}` : ''}`, { // 更新配置
// method: configId ? "PUT" : "POST", const response = await updateConfig(id, configData);
// headers: { if (response.error) {
// "Content-Type": "application/json", throw new Error(response.error);
// }, }
// body: JSON.stringify({ } else {
// id: configId, // 创建配置
// configName, const response = await createConfig(configData);
// module, if (response.error) {
// environment, throw new Error(response.error);
// configData: JSON.parse(configData), }
// isActive, }
// remarks,
// }),
// });
//
// if (!response.ok) {
// throw new Error(`保存失败: ${response.status}`);
// }
// 保存成功后重定向到列表页
return redirect("/config-lists"); return redirect("/config-lists");
} catch (error) { } catch (error) {
console.error("保存配置失败:", error); console.error("保存配置失败:", error);
return json<ActionData>({ return Response.json({
success: false, success: false,
errors: { errors: {
general: "保存配置失败,请稍后重试" general: "保存配置失败,请稍后重试"
@@ -253,18 +170,17 @@ export async function action({ request }: ActionFunctionArgs) {
} }
} }
// JSON模板数据 // 配置模板常量
const JSON_TEMPLATES = { const CONFIG_TEMPLATES = {
database: { database: {
database: { host: "localhost",
host: "localhost", port: 5432,
port: 5432, database: "mydb",
username: "db_user", username: "admin",
password: "******", password: "******",
name: "app_database", pool: {
pool_size: 10, min: 2,
timeout: 5000, max: 10
ssl: false
} }
}, },
file: { file: {
@@ -290,8 +206,7 @@ const JSON_TEMPLATES = {
}; };
export default function ConfigNew() { export default function ConfigNew() {
const { config, isEdit } = useLoaderData<typeof loader>(); const { config, isEdit, types, environments } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>(); const actionData = useActionData<typeof action>();
const navigation = useNavigation(); const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting"; const isSubmitting = navigation.state === "submitting";
@@ -304,18 +219,14 @@ export default function ConfigNew() {
const [selectedModule, setSelectedModule] = useState<string>(""); const [selectedModule, setSelectedModule] = useState<string>("");
const [selectedEnvironment, setSelectedEnvironment] = useState<string>(""); const [selectedEnvironment, setSelectedEnvironment] = useState<string>("");
// 在 ConfigNew 组件中添加状态来跟踪当前选中的模板
const [selectedTemplate, setSelectedTemplate] = useState<keyof typeof CONFIG_TEMPLATES | null>(null);
useEffect(() => { useEffect(() => {
// 初始化配置数据 // 初始化配置数据
if (config?.configData) { if (config) {
setConfigDataValue(config.configData); setConfigDataValue(JSON.stringify(config.config, null, 2));
} setSelectedModule(config.type);
// 初始化模块和环境的选中状态
if (config?.module) {
setSelectedModule(config.module);
}
if (config?.environment) {
setSelectedEnvironment(config.environment); setSelectedEnvironment(config.environment);
} }
@@ -333,7 +244,6 @@ export default function ConfigNew() {
}, },
feature_flags: ["new_ui", "analytics_v2"] feature_flags: ["new_ui", "analytics_v2"]
}, null, 2)); }, null, 2));
}, [config]); }, [config]);
// 处理JSON数据变更 // 处理JSON数据变更
@@ -360,6 +270,7 @@ export default function ConfigNew() {
// 格式化JSON // 格式化JSON
const handleFormatJson = () => { const handleFormatJson = () => {
if (configDataValue.trim() === "") return; if (configDataValue.trim() === "") return;
try { try {
@@ -375,60 +286,6 @@ export default function ConfigNew() {
} }
}; };
// 加载JSON模板
const handleLoadTemplate = (type: keyof typeof JSON_TEMPLATES) => {
const template = JSON_TEMPLATES[type];
setConfigDataValue(JSON.stringify(template, null, 2));
setJsonError(null);
};
// 模块标签点击
const handleModuleTagClick = (module: string) => {
setSelectedModule(module);
};
// 环境标签点击
const handleEnvironmentTagClick = (env: string) => {
setSelectedEnvironment(env);
};
// 显示JSON语法高亮
const renderJsonWithSyntaxHighlight = (json: string) => {
try {
// 如果是空字符串,直接返回
if (!json.trim()) return "";
// 解析并格式化JSON
const parsed = JSON.parse(json);
const formatted = JSON.stringify(parsed, null, 2);
// 添加语法高亮
return formatted
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, (match) => {
let cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
match = match.replace(':', '');
} else {
cls = 'string';
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
return `<span class="code-json ${cls}">${match}</span>`;
});
} catch (e) {
// 如果解析失败,返回原始JSON
return json;
}
};
return ( return (
<div className="config-new-page"> <div className="config-new-page">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
@@ -445,6 +302,12 @@ export default function ConfigNew() {
</div> </div>
</div> </div>
{actionData?.errors?.general && (
<div className="mb-4 w-full">
<div className="error-message general-error">{actionData.errors.general}</div>
</div>
)}
<Card className="config-form-card"> <Card className="config-form-card">
<Form method="post" id="configForm" className="config-form"> <Form method="post" id="configForm" className="config-form">
{config?.id && <input type="hidden" name="id" value={config.id} />} {config?.id && <input type="hidden" name="id" value={config.id} />}
@@ -452,18 +315,18 @@ export default function ConfigNew() {
{/* 配置名称和状态 */} {/* 配置名称和状态 */}
<div className="form-row"> <div className="form-row">
<div className="form-group"> <div className="form-group">
<label htmlFor="configName" className="form-label required"></label> <label htmlFor="name" className="form-label required"></label>
<input <input
type="text" type="text"
id="configName" id="name"
name="configName" name="name"
className={`form-input ${actionData?.errors?.configName ? 'input-error' : ''}`} className={`form-input ${actionData?.errors?.name ? 'input-error' : ''}`}
defaultValue={config?.configName || ''} defaultValue={config?.name || ''}
placeholder="请输入配置名称,如database_connection" placeholder="请输入配置名称,如database_connection"
required required
/> />
{actionData?.errors?.configName && ( {actionData?.errors?.name && (
<div className="error-message">{actionData.errors.configName}</div> <div className="error-message">{actionData.errors.name}</div>
)} )}
<div className="form-help"> <div className="form-help">
使使线 使使线
@@ -471,18 +334,18 @@ export default function ConfigNew() {
</div> </div>
<div className="form-group"> <div className="form-group">
<label htmlFor="isActive" className="form-label"></label> <label htmlFor="is_active" className="form-label"></label>
<div className="mt-2"> <div className="mt-2">
<div className="flex items-center"> <div className="flex items-center">
<input <input
type="checkbox" type="checkbox"
id="isActive" id="is_active"
name="isActive" name="is_active"
value="true" value="true"
className="form-checkbox" className="form-checkbox"
defaultChecked={config?.isActive !== false} defaultChecked={config?.is_active !== false}
/> />
<label htmlFor="isActive" className="form-checkbox-label"> <label htmlFor="is_active" className="form-checkbox-label">
</label> </label>
</div> </div>
@@ -495,33 +358,33 @@ export default function ConfigNew() {
{/* 所属模块 */} {/* 所属模块 */}
<div className="form-group"> <div className="form-group">
<label htmlFor="module" className="form-label required"></label> <label htmlFor="type" className="form-label required"></label>
<input <input
type="hidden" type="hidden"
name="module" name="type"
value={selectedModule} value={selectedModule}
/> />
<input <input
type="text" type="text"
id="moduleDisplay" id="typeDisplay"
className={`form-input ${actionData?.errors?.module ? 'input-error' : ''}`} className={`form-input ${actionData?.errors?.type ? 'input-error' : ''}`}
value={selectedModule ? MODULE_LABELS[selectedModule as ConfigModule] || selectedModule : ''} value={selectedModule}
onChange={(e) => setSelectedModule(e.target.value)}
placeholder="请输入或选择所属模块" placeholder="请输入或选择所属模块"
readOnly
required required
/> />
{actionData?.errors?.module && ( {actionData?.errors?.type && (
<div className="error-message">{actionData.errors.module}</div> <div className="error-message">{actionData.errors.type}</div>
)} )}
<div className="tag-buttons mt-2"> <div className="tag-buttons mt-2">
{Object.entries(MODULE_LABELS).map(([value, label]) => ( {types.map((type: string) => (
<button <button
key={value} key={type}
type="button" type="button"
className={`tag-button ${selectedModule === value ? 'active' : ''}`} className={`tag-button ${selectedModule === type ? 'active' : ''}`}
onClick={() => handleModuleTagClick(value)} onClick={() => setSelectedModule(type)}
> >
{label} {type}
</button> </button>
))} ))}
</div> </div>
@@ -542,23 +405,23 @@ export default function ConfigNew() {
type="text" type="text"
id="environmentDisplay" id="environmentDisplay"
className={`form-input ${actionData?.errors?.environment ? 'input-error' : ''}`} className={`form-input ${actionData?.errors?.environment ? 'input-error' : ''}`}
value={selectedEnvironment ? EXTENDED_ENVIRONMENT_LABELS[selectedEnvironment] || selectedEnvironment : ''} value={selectedEnvironment}
onChange={(e) => setSelectedEnvironment(e.target.value)}
placeholder="请输入或选择环境" placeholder="请输入或选择环境"
readOnly
required required
/> />
{actionData?.errors?.environment && ( {actionData?.errors?.environment && (
<div className="error-message">{actionData.errors.environment}</div> <div className="error-message">{actionData.errors.environment}</div>
)} )}
<div className="tag-buttons mt-2"> <div className="tag-buttons mt-2">
{Object.entries(EXTENDED_ENVIRONMENT_LABELS).map(([value, label]) => ( {environments.map((env: string) => (
<button <button
key={value} key={env}
type="button" type="button"
className={`tag-button ${selectedEnvironment === value ? 'active' : ''}`} className={`tag-button ${selectedEnvironment === env ? 'active' : ''}`}
onClick={() => handleEnvironmentTagClick(value)} onClick={() => setSelectedEnvironment(env)}
> >
{label} {env}
</button> </button>
))} ))}
</div> </div>
@@ -569,14 +432,14 @@ export default function ConfigNew() {
{/* 配置数据 */} {/* 配置数据 */}
<div className="form-group"> <div className="form-group">
<label htmlFor="configData" className="form-label required"> (JSON)</label> <label htmlFor="config" className="form-label required"> (JSON)</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4" style={{ minHeight: '390px' }}> <div className="grid grid-cols-1 md:grid-cols-2 gap-4" style={{ minHeight: '390px' }}>
{/* 左侧JSON编辑区 */} {/* 左侧JSON编辑区 */}
<div className="h-full"> <div className="h-full">
<textarea <textarea
id="configData" id="config"
name="configData" name="config"
className={`json-editor ${(actionData?.errors?.configData || jsonError) ? 'input-error' : ''}`} className={`json-editor ${(actionData?.errors?.config || jsonError) ? 'input-error' : ''}`}
value={configDataValue} value={configDataValue}
onChange={handleConfigDataChange} onChange={handleConfigDataChange}
required required
@@ -586,13 +449,16 @@ export default function ConfigNew() {
<Button <Button
type="default" type="default"
size="small" size="small"
onClick={handleFormatJson} onClick={(e) => {
e.preventDefault();
handleFormatJson();
}}
> >
<i className="ri-braces-line mr-1"></i> JSON <i className="ri-braces-line mr-1"></i> JSON
</Button> </Button>
</div> </div>
{(actionData?.errors?.configData || jsonError) && ( {(actionData?.errors?.config || jsonError) && (
<div className="error-message">{actionData?.errors?.configData || jsonError}</div> <div className="error-message">{actionData?.errors?.config || jsonError}</div>
)} )}
</div> </div>
@@ -603,10 +469,25 @@ export default function ConfigNew() {
<div className="example-title"></div> <div className="example-title"></div>
</div> </div>
<div className="example-content"> <div className="example-content">
<pre <div className="json-display">
className="example-pre" {exampleJsonValue.split('\n').map((line, index) => (
dangerouslySetInnerHTML={{ __html: renderJsonWithSyntaxHighlight(exampleJsonValue) }} <div key={index} className="json-line">
/> {line.split('').map((char, charIndex) => {
let className = 'json-char';
if (char === '{' || char === '}') className += ' json-brace';
if (char === '[' || char === ']') className += ' json-bracket';
if (char === '"') className += ' json-quote';
if (char === ':') className += ' json-colon';
if (char === ',') className += ' json-comma';
return (
<span key={charIndex} className={className}>
{char}
</span>
);
})}
</div>
))}
</div>
</div> </div>
<div className="example-footer"> <div className="example-footer">
<div className="text-sm font-medium mb-2">:</div> <div className="text-sm font-medium mb-2">:</div>
@@ -614,21 +495,36 @@ export default function ConfigNew() {
<Button <Button
type="default" type="default"
size="small" size="small"
onClick={() => handleLoadTemplate('database')} className={`${selectedTemplate === 'database' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
onClick={(e) => {
e.preventDefault();
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES.database, null, 2));
setSelectedTemplate('database');
}}
> >
</Button> </Button>
<Button <Button
type="default" type="default"
size="small" size="small"
onClick={() => handleLoadTemplate('file')} className={`${selectedTemplate === 'file' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
onClick={(e) => {
e.preventDefault();
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES.file, null, 2));
setSelectedTemplate('file');
}}
> >
</Button> </Button>
<Button <Button
type="default" type="default"
size="small" size="small"
onClick={() => handleLoadTemplate('ai')} className={`${selectedTemplate === 'ai' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
onClick={(e) => {
e.preventDefault();
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES.ai, null, 2));
setSelectedTemplate('ai');
}}
> >
AI服务配置 AI服务配置
</Button> </Button>
@@ -644,12 +540,12 @@ export default function ConfigNew() {
{/* 备注 */} {/* 备注 */}
<div className="form-group"> <div className="form-group">
<label htmlFor="remarks" className="form-label"></label> <label htmlFor="remark" className="form-label"></label>
<textarea <textarea
id="remarks" id="remark"
name="remarks" name="remark"
className="form-textarea" className="form-textarea"
defaultValue={config?.remarks || ''} defaultValue={config?.remark || ''}
rows={2} rows={2}
placeholder="请输入配置备注信息" placeholder="请输入配置备注信息"
/> />
@@ -658,12 +554,6 @@ export default function ConfigNew() {
</div> </div>
</div> </div>
{actionData?.errors?.general && (
<div className="form-row">
<div className="error-message general-error">{actionData.errors.general}</div>
</div>
)}
</Form> </Form>
</Card> </Card>
+22
View File
@@ -0,0 +1,22 @@
import { Outlet } from "react-router-dom";
import {type MetaFunction} from "@remix-run/node";
export const meta: MetaFunction = () => {
return [
{title: "文档类型列表 - 中国烟草AI合同及卷宗审核系统"},
{name: "document-types", content: "文档类型列表,新增,修改"}
]
}
export const handle = {
breadcrumb: "文档类型列表"
}
/**
* 文档类型列表路由布局
*/
export default function DocumentTypesLayout() {
return (
<Outlet />
)
}
File diff suppressed because it is too large Load Diff
+121 -140
View File
@@ -1,11 +1,13 @@
import { MetaFunction } from "@remix-run/node"; import { MetaFunction, json, type LoaderFunctionArgs } from "@remix-run/node";
import { useSearchParams, useNavigate } from "@remix-run/react"; import { useSearchParams, useNavigate, useLoaderData } from "@remix-run/react";
import { useState } from "react";
import indexStyles from "~/styles/pages/prompts_index.css?url"; import indexStyles from "~/styles/pages/prompts_index.css?url";
import { Card } from "~/components/ui/Card"; import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button"; import { Button } from "~/components/ui/Button";
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel"; import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
import { Table } from "~/components/ui/Table"; import { Table } from "~/components/ui/Table";
import { Pagination } from "~/components/ui/Pagination"; import { Pagination } from "~/components/ui/Pagination";
import { getPromptTemplates, deletePromptTemplate, type PromptTemplateUI } from "~/api/prompts/prompts";
// 定义提示词模板类型 // 定义提示词模板类型
export interface PromptTemplate { export interface PromptTemplate {
@@ -33,123 +35,61 @@ export const meta: MetaFunction = () => {
]; ];
}; };
// 定义加载器返回数据类型
// 模拟数据 interface LoaderData {
const MOCK_TEMPLATES: PromptTemplate[] = [ templates: PromptTemplateUI[];
{ total: number;
id: "1", pageSize: number;
template_name: "行政处罚-抽取通用模板", currentPage: number;
template_type: "Extraction", error?: string;
description: "本模板用于抽取行政处罚决定书编号等信息", }
version: "v1.0",
status: "system",
created_by: "system",
template_content: `你是一个专业的文档信息抽取助手。请从以下{docType}文档中抽取关键信息:
1. 处罚决定书编号
2. 处罚对象名称
3. 处罚事由
4. 处罚依据
5. 处罚内容
6. 处罚金额
7. 发文日期
请将结果以JSON格式输出,包含以上字段。如果某个字段在文档中未找到,则该字段的值设为null。`,
variables: JSON.stringify({ "docType": "文档类型" })
},
{
id: "2",
template_name: "销售合同-甲方信息评估",
template_type: "Evaluation",
description: "评估销售合同中甲方信息是否完整",
version: "v1.2",
status: "active",
created_by: "admin",
template_content: `你是一个专业的合同审核助手。请评估以下{docType}中甲方信息的完整性:
请检查以下要素是否存在且完整:
1. 甲方全称
2. 注册地址
3. 统一社会信用代码
4. 法定代表人
5. 联系方式
请给出评估结果,并标明缺失或不完整的信息。`,
variables: JSON.stringify({ "docType": "文档类型" })
},
{
id: "3",
template_name: "专卖许可证-摘要模板",
template_type: "Summary",
description: "生成专卖许可证申请文件的内容摘要",
version: "v1.0",
status: "active",
created_by: "admin",
template_content: `你是一个专业的文档摘要助手。请为以下{docType}生成一份简洁的摘要:
摘要应包含以下要点:
1. 申请人基本信息
2. 许可证类型
3. 申请事项
4. 经营范围
5. 申请日期
请控制摘要在200字以内,保留关键信息。`,
variables: JSON.stringify({ "docType": "文档类型" })
},
{
id: "4",
template_name: "采购合同-乙方资质抽取",
template_type: "Extraction",
description: "抽取采购合同中乙方的资质信息",
version: "v1.1",
status: "inactive",
created_by: "zhangsan",
template_content: `你是一个专业的合同信息抽取助手。请从以下{docType}中抽取乙方的资质信息:
需要抽取的信息包括:
1. 乙方全称
2. 资质证书类型
3. 资质证书编号
4. 资质等级
5. 证书有效期
请将结果以JSON格式输出,包含以上字段。`,
variables: JSON.stringify({ "docType": "文档类型" })
},
{
id: "5",
template_name: "合同通用-关键条款评估",
template_type: "Evaluation",
description: "评估合同中关键条款是否明确、合规",
version: "v2.0",
status: "active",
created_by: "lisi",
template_content: `你是一个专业的{industry}行业合同审核助手。请评估以下合同中的关键条款是否明确、合规:
请重点关注以下条款:
1. 合同标的
2. 价格条款
3. 付款条件
4. 交付方式
5. 违约责任
6. 争议解决
请对每一项给出评估结果,并指出不明确或存在风险的条款。`,
variables: JSON.stringify({ "industry": "行业类型", "docType": "文档类型" })
}
];
// 数据加载器 // 数据加载器
export async function loader() { export async function loader({ request }: LoaderFunctionArgs) {
try { try {
// 实际应用中,这里应该调用API获取数据 const url = new URL(request.url);
// const response = await fetch(`${process.env.API_BASE_URL}/api/prompt-templates`); const name = url.searchParams.get('name') || undefined;
// if (!response.ok) throw new Error(`获取提示词模板失败: ${response.status}`); const type = url.searchParams.get('type') || undefined;
// const templates = await response.json(); const status = url.searchParams.get('status') || undefined;
const page = parseInt(url.searchParams.get('page') || '1', 10);
const pageSize = parseInt(url.searchParams.get('pageSize') || '10', 10);
// 使用模拟数据 console.log('加载提示词模板参数:', { name, type, status, page, pageSize });
const templates = MOCK_TEMPLATES;
return Response.json({ // 从 API 获取数据
templates, const result = await getPromptTemplates({
total: templates.length, name,
pageSize: 10, type,
currentPage: 1 status,
page,
pageSize
});
if (result.error) {
console.error('获取提示词模板失败:', result.error);
return json<LoaderData>(
{
templates: [],
total: 0,
pageSize,
currentPage: page,
error: result.error
},
{ status: result.status || 500 }
);
}
console.log(`成功加载${result.data?.templates.length || 0}条提示词模板数据`);
return json<LoaderData>({
templates: result.data?.templates || [],
total: result.data?.total || 0,
pageSize,
currentPage: page
}); });
} catch (error) { } catch (error) {
console.error("加载提示词模板失败:", error); console.error("加载提示词模板失败:", error);
return Response.json( return json<LoaderData>(
{ {
templates: [], templates: [],
total: 0, total: 0,
@@ -166,6 +106,8 @@ export async function loader() {
export default function PromptsIndex() { export default function PromptsIndex() {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const { templates, total, currentPage, pageSize, error } = useLoaderData<typeof loader>();
const [isLoading, setIsLoading] = useState(false);
// 处理搜索名称 // 处理搜索名称
const handleNameSearch = (value: string) => { const handleNameSearch = (value: string) => {
@@ -210,6 +152,12 @@ export default function PromptsIndex() {
setSearchParams(new URLSearchParams()); setSearchParams(new URLSearchParams());
}; };
// 处理搜索按钮点击
// const handleSearch = () => {
// // 搜索已经由 URL 参数变化触发,这里不需要额外操作
// console.log('搜索参数:', Object.fromEntries(searchParams.entries()));
// };
// 查看模板详情 // 查看模板详情
const handleViewTemplate = (id: string) => { const handleViewTemplate = (id: string) => {
navigate(`/prompts/new?id=${id}&mode=view`); navigate(`/prompts/new?id=${id}&mode=view`);
@@ -228,23 +176,48 @@ export default function PromptsIndex() {
}; };
// 删除模板 // 删除模板
const handleDeleteTemplate = (id: string) => { const handleDeleteTemplate = async (id: string) => {
if (confirm('确定要删除该模板吗?删除后无法恢复。')) { if (confirm('确定要删除该模板吗?删除后无法恢复。')) {
// 实际应该调用API删除数据 setIsLoading(true);
console.log('删除模板ID:', id); try {
alert('删除成功!'); const result = await deletePromptTemplate(id);
// 刷新页面 if (result.error) {
window.location.reload(); alert(`删除失败: ${result.error}`);
} else {
alert('删除成功!');
// 刷新页面
window.location.reload();
}
} catch (error) {
alert(`删除失败: ${error instanceof Error ? error.message : '未知错误'}`);
} finally {
setIsLoading(false);
}
} }
}; };
// 处理分页
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 = [ const columns = [
{ {
title: "模板名称", title: "模板名称",
key: "template_name", key: "template_name",
width: "300px", width: "300px",
render: (_: unknown, record: PromptTemplate) => ( render: (_: unknown, record: PromptTemplateUI) => (
<div className="flex items-center"> <div className="flex items-center">
<i className="ri-file-list-line text-primary mr-2"></i> <i className="ri-file-list-line text-primary mr-2"></i>
<span className="truncate">{record.template_name}</span> <span className="truncate">{record.template_name}</span>
@@ -255,7 +228,7 @@ export default function PromptsIndex() {
title: "类型", title: "类型",
key: "template_type", key: "template_type",
width: "100px", width: "100px",
render: (_: unknown, record: PromptTemplate) => { render: (_: unknown, record: PromptTemplateUI) => {
let typeText = ''; let typeText = '';
let typeClass = ''; let typeClass = '';
@@ -284,7 +257,7 @@ export default function PromptsIndex() {
{ {
title: "描述", title: "描述",
key: "description", key: "description",
render: (_: unknown, record: PromptTemplate) => ( render: (_: unknown, record: PromptTemplateUI) => (
<div className="text-secondary text-sm truncate max-w-xs" title={record.description}> <div className="text-secondary text-sm truncate max-w-xs" title={record.description}>
{record.description} {record.description}
</div> </div>
@@ -294,13 +267,13 @@ export default function PromptsIndex() {
title: "版本", title: "版本",
key: "version", key: "version",
width: "80px", width: "80px",
render: (_: unknown, record: PromptTemplate) => record.version render: (_: unknown, record: PromptTemplateUI) => record.version
}, },
{ {
title: "状态", title: "状态",
key: "status", key: "status",
width: "80px", width: "80px",
render: (_: unknown, record: PromptTemplate) => { render: (_: unknown, record: PromptTemplateUI) => {
let statusText = ''; let statusText = '';
let statusClass = ''; let statusClass = '';
@@ -326,14 +299,15 @@ export default function PromptsIndex() {
title: "创建者", title: "创建者",
key: "created_by", key: "created_by",
width: "100px", width: "100px",
render: (_: unknown, record: PromptTemplate) => record.created_by render: (_: unknown, record: PromptTemplateUI) => (
<span className="text-secondary"> {record.created_by}</span>
)
}, },
{ {
title: "操作", title: "操作",
key: "operation", key: "operation",
width: "150px", width: "150px",
// align: "center", render: (_: unknown, record: PromptTemplateUI) => (
render: (_: unknown, record: PromptTemplate) => (
<div> <div>
{record.status === 'system' ? ( {record.status === 'system' ? (
<> <>
@@ -361,6 +335,7 @@ export default function PromptsIndex() {
<button <button
className="operation-btn text-error" className="operation-btn text-error"
onClick={() => handleDeleteTemplate(record.id)} onClick={() => handleDeleteTemplate(record.id)}
disabled={isLoading}
> >
<i className="ri-delete-bin-line"></i> <i className="ri-delete-bin-line"></i>
</button> </button>
@@ -400,12 +375,13 @@ export default function PromptsIndex() {
> >
</Button> </Button>
<Button {/* <Button
type="primary" type="primary"
icon="ri-search-line" icon="ri-search-line"
onClick={handleSearch}
> >
搜索 搜索
</Button> </Button> */}
</> </>
} }
noActionDivider={true} noActionDivider={true}
@@ -424,7 +400,6 @@ export default function PromptsIndex() {
name="type" name="type"
value={searchParams.get('type') || ''} value={searchParams.get('type') || ''}
options={[ options={[
// { value: "", label: "全部" },
{ value: "Extraction", label: "抽取(Extraction)" }, { value: "Extraction", label: "抽取(Extraction)" },
{ value: "Evaluation", label: "评估(Evaluation)" }, { value: "Evaluation", label: "评估(Evaluation)" },
{ value: "Summary", label: "摘要(Summary)" }, { value: "Summary", label: "摘要(Summary)" },
@@ -439,7 +414,6 @@ export default function PromptsIndex() {
name="status" name="status"
value={searchParams.get('status') || ''} value={searchParams.get('status') || ''}
options={[ options={[
// { value: "", label: "全部" },
{ value: "active", label: "启用" }, { value: "active", label: "启用" },
{ value: "inactive", label: "停用" }, { value: "inactive", label: "停用" },
{ value: "system", label: "系统预设" } { value: "system", label: "系统预设" }
@@ -449,27 +423,34 @@ export default function PromptsIndex() {
/> />
</FilterPanel> </FilterPanel>
{/* 错误信息 */}
{error && (
<div className="error-alert mb-4 p-4 bg-red-50 text-red-700 rounded-md">
<i className="ri-error-warning-line mr-2"></i> {error}
</div>
)}
{/* 数据表格 */} {/* 数据表格 */}
<Card bodyClassName="px-4 py-4"> <Card bodyClassName="px-4 py-4">
<Table <Table
columns={columns} columns={columns}
dataSource={MOCK_TEMPLATES} dataSource={templates}
rowKey="id" rowKey="id"
emptyText="暂无提示词模板数据" emptyText="暂无提示词模板数据"
loading={isLoading}
/> />
{/* 分页 */} {/* 分页 */}
<Pagination <Pagination
currentPage={1} currentPage={currentPage}
total={MOCK_TEMPLATES.length} total={total}
pageSize={10} pageSize={pageSize}
onChange={(page) => { onChange={handlePageChange}
const newParams = new URLSearchParams(searchParams); onPageSizeChange={handlePageSizeChange}
newParams.set('page', page.toString()); showTotal={true}
setSearchParams(newParams); showPageSizeChanger={true}
}} pageSizeOptions={[10, 20, 30, 50]}
showTotal={true} />
/>
</Card> </Card>
</div> </div>
); );
+213 -213
View File
@@ -1,9 +1,9 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { MetaFunction, LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node"; import { MetaFunction, LoaderFunctionArgs, ActionFunctionArgs, json, redirect } from "@remix-run/node";
import { Link, useLoaderData, useSubmit, useNavigation } from "@remix-run/react"; import { Link, useLoaderData, useNavigation, useActionData, Form } from "@remix-run/react";
import { Button } from "~/components/ui/Button"; import { Button } from "~/components/ui/Button";
import { PromptTemplate } from "./prompts._index";
import newStyles from "~/styles/pages/prompts_new.css?url"; import newStyles from "~/styles/pages/prompts_new.css?url";
import { getPromptTemplate, createPromptTemplate, updatePromptTemplate, type PromptTemplateUI } from "~/api/prompts/prompts";
// 样式链接 // 样式链接
export function links() { export function links() {
@@ -32,111 +32,34 @@ export const handle = {
}; };
interface LoaderData { interface LoaderData {
template: PromptTemplate; template: PromptTemplateUI | null;
mode: string; mode: string;
error?: string;
} }
// 从模拟数据中获取模板 // 定义本地表单数据接口
const getTemplateById = (id: string): PromptTemplate | undefined => { interface FormDataState extends Omit<PromptTemplateUI, 'variables'> {
// 与prompts._index.tsx中的模拟数据保持一致 variables: string; // 在表单状态中我们保存变量为 JSON 字符串
const MOCK_TEMPLATES: PromptTemplate[] = [ }
{
id: "1",
template_name: "行政处罚-抽取通用模板",
template_type: "Extraction",
description: "本模板用于抽取行政处罚决定书编号等信息",
version: "v1.0",
status: "system",
created_by: "system",
template_content: `你是一个专业的文档信息抽取助手。请从以下{docType}文档中抽取关键信息:
1. 处罚决定书编号
2. 处罚对象名称
3. 处罚事由
4. 处罚依据
5. 处罚内容
6. 处罚金额
7. 发文日期
请将结果以JSON格式输出,包含以上字段。如果某个字段在文档中未找到,则该字段的值设为null。`,
variables: JSON.stringify({ "docType": "文档类型" })
},
{
id: "2",
template_name: "销售合同-甲方信息评估",
template_type: "Evaluation",
description: "评估销售合同中甲方信息是否完整",
version: "v1.2",
status: "active",
created_by: "admin",
template_content: `你是一个专业的合同审核助手。请评估以下{docType}中甲方信息的完整性:
请检查以下要素是否存在且完整:
1. 甲方全称
2. 注册地址
3. 统一社会信用代码
4. 法定代表人
5. 联系方式
请给出评估结果,并标明缺失或不完整的信息。`,
variables: JSON.stringify({ "docType": "文档类型" })
},
{
id: "3",
template_name: "专卖许可证-摘要模板",
template_type: "Summary",
description: "生成专卖许可证申请文件的内容摘要",
version: "v1.0",
status: "active",
created_by: "admin",
template_content: `你是一个专业的文档摘要助手。请为以下{docType}生成一份简洁的摘要:
摘要应包含以下要点:
1. 申请人基本信息
2. 许可证类型
3. 申请事项
4. 经营范围
5. 申请日期
请控制摘要在200字以内,保留关键信息。`,
variables: JSON.stringify({ "docType": "文档类型" })
},
{
id: "4",
template_name: "采购合同-乙方资质抽取",
template_type: "Extraction",
description: "抽取采购合同中乙方的资质信息",
version: "v1.1", interface ActionData {
status: "inactive", success?: boolean;
created_by: "zhangsan", errors?: {
template_content: `你是一个专业的合同信息抽取助手。请从以下{docType}中抽取乙方的资质信息: template_name?: string;
需要抽取的信息包括: template_type?: string;
1. 乙方全称 template_content?: string;
2. 资质证书类型 general?: string;
3. 资质证书编号 };
4. 资质等级 formData?: {
5. 证书有效期 template_name: string;
请将结果以JSON格式输出,包含以上字段。`, template_type: "Extraction" | "Evaluation" | "Summary" | "Common";
variables: JSON.stringify({ "docType": "文档类型" }) description: string;
}, template_content: string;
{ variables: string;
id: "5", status: "active" | "inactive" | "system";
template_name: "合同通用-关键条款评估", version: string;
template_type: "Evaluation", };
description: "评估合同中关键条款是否明确、合规", }
version: "v2.0",
status: "active",
created_by: "lisi",
template_content: `你是一个专业的{industry}行业合同审核助手。请评估以下合同中的关键条款是否明确、合规:
请重点关注以下条款:
1. 合同标的
2. 价格条款
3. 付款条件
4. 交付方式
5. 违约责任
6. 争议解决
请对每一项给出评估结果,并指出不明确或存在风险的条款。`,
variables: JSON.stringify({ "industry": "行业类型", "docType": "文档类型" })
}
];
return MOCK_TEMPLATES.find(t => t.id === id);
};
// 加载函数 // 加载函数
export async function loader({ request }: LoaderFunctionArgs) { export async function loader({ request }: LoaderFunctionArgs) {
@@ -149,25 +72,27 @@ export async function loader({ request }: LoaderFunctionArgs) {
let template = null; let template = null;
if (id) { if (id) {
// 实际应用中,这里应该调用API获取数据 // API获取数据
// const response = await fetch(`${process.env.API_BASE_URL}/api/prompt-templates/${id}`); const result = await getPromptTemplate(id);
// if (!response.ok) throw new Error(`获取提示词模板失败: ${response.status}`);
// template = await response.json();
// 使用模拟数据 if (result.error) {
template = getTemplateById(id); console.error('获取提示词模板失败:', result.error);
throw new Error(result.error);
}
template = result.data || null;
if (!template) { if (!template) {
throw new Error(`未找到ID为${id}的模板`); throw new Error(`未找到ID为${id}的模板`);
} }
} }
return Response.json({ return json<LoaderData>({
template, template,
mode mode
}); });
} catch (error) { } catch (error) {
console.error("加载提示词模板失败:", error); console.error("加载提示词模板失败:", error);
return Response.json( return json<LoaderData>(
{ {
template: null, template: null,
mode: "create", mode: "create",
@@ -180,69 +105,102 @@ export async function loader({ request }: LoaderFunctionArgs) {
// Action函数 - 处理表单提交 // Action函数 - 处理表单提交
export async function action({ request }: ActionFunctionArgs) { export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const id = formData.get("id") as string;
const template_name = formData.get("template_name") as string;
const template_type = formData.get("template_type") as "Extraction" | "Evaluation" | "Summary" | "Common";
const description = formData.get("description") as string;
const template_content = formData.get("template_content") as string;
const variables = formData.get("variables") as string;
const status = formData.get("status") as string;
const version = formData.get("version") as string;
const errors: ActionData["errors"] = {};
// 表单验证
if (!template_name || template_name.trim() === "") {
errors.template_name = "模板名称不能为空";
}
if (!template_type) {
errors.template_type = "请选择模板类型";
}
if (!template_content || template_content.trim() === "") {
errors.template_content = "模板内容不能为空";
}
if (Object.keys(errors).length > 0) {
return json({ errors });
}
try { try {
const formData = await request.formData(); // 准备变量数据
const templateData = Object.fromEntries(formData); let variablesData: Record<string, string> = {};
try {
// 表单验证 if (variables) {
const errors: Record<string, string> = {}; variablesData = JSON.parse(variables);
if (!templateData.template_name) {
errors.template_name = "模板名称不能为空";
}
if (!templateData.template_type) {
errors.template_type = "请选择模板类型";
}
if (!templateData.template_content) {
errors.template_content = "模板内容不能为空";
}
if (Object.keys(errors).length > 0) {
return Response.json({ errors, success: false }, { status: 400 });
}
// 实际应用中,这里应该调用API保存数据
// const apiUrl = templateData.id
// ? `${process.env.API_BASE_URL}/api/prompt-templates/${templateData.id}`
// : `${process.env.API_BASE_URL}/api/prompt-templates`;
//
// const response = await fetch(apiUrl, {
// method: templateData.id ? "PUT" : "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify(templateData)
// });
//
// if (!response.ok) throw new Error(`保存提示词模板失败: ${response.status}`);
// const result = await response.json();
// 模拟API响应
console.log("提交的模板数据:", templateData);
return Response.json({
success: true,
message: "提示词模板保存成功",
template: {
...templateData,
id: templateData.id || Math.random().toString(36).substring(2, 10),
created_by: "当前用户",
created_at: new Date().toISOString()
} }
}); } catch (e) {
console.error('解析变量JSON失败:', e);
}
// 准备API数据
const apiTemplate: Partial<PromptTemplateUI> = {
template_name,
template_type,
description,
template_content,
variables: variablesData,
status: status === "active" ? "active" : "inactive",
version: version || "v1.0"
};
let result;
if (id) {
// 更新模板
result = await updatePromptTemplate(id, apiTemplate);
} else {
// 创建模板
result = await createPromptTemplate(apiTemplate);
}
if (result.error) {
return json({
errors: { general: result.error },
formData: {
template_name,
template_type,
description,
template_content,
variables,
status,
version
}
});
}
return redirect("/prompts");
} catch (error) { } catch (error) {
console.error("保存提示词模板失败:", error); console.error("保存提示词模板失败:", error);
return Response.json( return json({
{ errors: {
success: false, general: error instanceof Error ? error.message : "保存提示词模板失败"
message: error instanceof Error ? error.message : "保存提示词模板失败"
}, },
{ status: 500 } formData: {
); template_name,
template_type,
description,
template_content,
variables,
status,
version
}
});
} }
} }
// 提取变量函数 // 提取变量函数 例如:{var1} {var2} {var3}
const extractVariables = (content: string) => { const extractVariables = (content: string) => {
const regex = /{([^{}]+)}/g; const regex = /{([^{}]+)}/g;
const variables: Record<string, string> = {}; const variables: Record<string, string> = {};
@@ -260,13 +218,13 @@ const extractVariables = (content: string) => {
// 页面组件 // 页面组件
export default function PromptsNew() { export default function PromptsNew() {
const { template, mode } = useLoaderData<typeof loader>(); const { template, mode, error } = useLoaderData<typeof loader>();
const submit = useSubmit(); const actionData = useActionData<ActionData>();
const navigation = useNavigation(); const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting"; const isSubmitting = navigation.state === "submitting";
// 表单状态 // 表单状态
const [formData, setFormData] = useState<Partial<PromptTemplate>>({ const [formData, setFormData] = useState<FormDataState>({
id: "", id: "",
template_name: "", template_name: "",
template_type: "Common", template_type: "Common",
@@ -274,7 +232,10 @@ export default function PromptsNew() {
version: "v1.0", version: "v1.0",
status: "active", status: "active",
template_content: "", template_content: "",
variables: "{}" created_by: 1,
variables: "{}",
created_at: "",
updated_at: ""
}); });
// 模式状态 // 模式状态
@@ -282,43 +243,57 @@ export default function PromptsNew() {
const [pageTitle, setPageTitle] = useState("新增提示词模板"); const [pageTitle, setPageTitle] = useState("新增提示词模板");
// 变量相关状态 // 变量相关状态
// 检测到的变量
const [detectedVariables, setDetectedVariables] = useState<Record<string, string>>({}); const [detectedVariables, setDetectedVariables] = useState<Record<string, string>>({});
// 示例值
const [exampleValues, setExampleValues] = useState<Record<string, string>>({}); const [exampleValues, setExampleValues] = useState<Record<string, string>>({});
// 预览内容
const [previewContent, setPreviewContent] = useState(""); const [previewContent, setPreviewContent] = useState("");
// 初始化表单数据 // 初始化表单数据
useEffect(() => { useEffect(() => {
if (template) { if (actionData?.formData) {
// 如果有保存失败的表单数据,使用它
setFormData(prev => ({
...prev,
...actionData.formData,
variables: actionData.formData?.variables || "{}",
status: actionData.formData?.status || "active"
}));
} else if (template) {
// 否则使用模板数据
const variablesJson = typeof template.variables === 'string'
? template.variables
: JSON.stringify(template.variables);
const newFormData = { const newFormData = {
...template, ...template,
// 如果是克隆模式,则清除ID并修改名称
id: mode === "clone" ? "" : template.id, id: mode === "clone" ? "" : template.id,
template_name: mode === "clone" ? `${template.template_name} (副本)` : template.template_name, template_name: mode === "clone" ? `${template.template_name} (副本)` : template.template_name,
// 如果是克隆模式,重置版本 version: mode === "clone" ? "v1.0" : template.version,
version: mode === "clone" ? "v1.0" : template.version variables: variablesJson
}; };
setFormData(newFormData); setFormData(newFormData);
try { try {
// 解析模板变量 const vars = typeof template.variables === 'string'
const vars = JSON.parse(template.variables); ? JSON.parse(template.variables)
: template.variables;
setExampleValues(vars); setExampleValues(vars);
} catch (e) { } catch (e) {
console.error("解析变量失败:", e); console.error("解析变量失败:", e);
} }
// 检测模板内容中的变量
if (template.template_content) { if (template.template_content) {
const vars = extractVariables(template.template_content); const vars = extractVariables(template.template_content);
setDetectedVariables(vars); setDetectedVariables(vars);
} }
} }
// 设置页面模式
setIsViewMode(mode === "view"); setIsViewMode(mode === "view");
// 设置页面标题
if (mode === "view") { if (mode === "view") {
setPageTitle("查看提示词模板"); setPageTitle("查看提示词模板");
} else if (mode === "edit") { } else if (mode === "edit") {
@@ -328,7 +303,7 @@ export default function PromptsNew() {
} else { } else {
setPageTitle("新增提示词模板"); setPageTitle("新增提示词模板");
} }
}, [template, mode]); }, [template, mode, actionData?.formData]);
// 处理输入变化 // 处理输入变化
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
@@ -337,15 +312,17 @@ export default function PromptsNew() {
// 如果是模板内容,检测变量 // 如果是模板内容,检测变量
if (name === "template_content") { if (name === "template_content") {
const vars = extractVariables(value); const vars = extractVariables(value);
// console.log("检测到的变量:",vars);
setDetectedVariables(vars); setDetectedVariables(vars);
// 更新变量JSON // 更新变量JSON
const varsJson = JSON.stringify( const varsJson = JSON.stringify(
Object.keys(vars).reduce((acc, key) => { Object.keys(vars).reduce((acc, key) => {
acc[key] = exampleValues[key] || key; acc[key] = exampleValues[key] || "";
return acc; return acc;
}, {} as Record<string, string>) }, {} as Record<string, string>)
); );
// console.log("更新变量JSON",varsJson);
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
@@ -362,11 +339,19 @@ export default function PromptsNew() {
// 处理状态切换 // 处理状态切换
const handleStatusToggle = (e: React.ChangeEvent<HTMLInputElement>) => { const handleStatusToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
// console.log("状态切换前:", formData.status);
const status = e.target.checked ? "active" : "inactive"; const status = e.target.checked ? "active" : "inactive";
setFormData(prev => ({ // console.log("新状态值:", status);
...prev,
status // 直接更新formData状态
})); setFormData(prev => {
const newState = {
...prev,
status: status as "active" | "inactive" | "system"
};
// console.log("状态更新后:", newState.status);
return newState;
});
}; };
// 处理示例值变更 // 处理示例值变更
@@ -383,7 +368,7 @@ export default function PromptsNew() {
// 替换变量 // 替换变量
Object.entries(detectedVariables).forEach(([key]) => { Object.entries(detectedVariables).forEach(([key]) => {
const exampleValue = exampleValues[key] || `[${key}]`; const exampleValue = exampleValues[key] || ``;
const regex = new RegExp(`{${key}}`, 'g'); const regex = new RegExp(`{${key}}`, 'g');
content = content.replace(regex, exampleValue); content = content.replace(regex, exampleValue);
}); });
@@ -395,7 +380,7 @@ export default function PromptsNew() {
useEffect(() => { useEffect(() => {
const varsJson = JSON.stringify( const varsJson = JSON.stringify(
Object.keys(detectedVariables).reduce((acc, key) => { Object.keys(detectedVariables).reduce((acc, key) => {
acc[key] = exampleValues[key] || key; acc[key] = exampleValues[key] || "";
return acc; return acc;
}, {} as Record<string, string>) }, {} as Record<string, string>)
); );
@@ -406,23 +391,6 @@ export default function PromptsNew() {
})); }));
}, [detectedVariables, exampleValues]); }, [detectedVariables, exampleValues]);
// 处理表单提交
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isViewMode) {
return;
}
const formElement = e.target as HTMLFormElement;
const submittingFormData = new FormData(formElement);
// 确保变量JSON被包含在提交中
submittingFormData.set("variables", formData.variables || "{}");
submit(submittingFormData, { method: "post" });
};
return ( return (
<div className="prompt-new-page"> <div className="prompt-new-page">
{/* 页面头部 */} {/* 页面头部 */}
@@ -439,7 +407,7 @@ export default function PromptsNew() {
type="primary" type="primary"
icon="ri-save-line" icon="ri-save-line"
disabled={isSubmitting} disabled={isSubmitting}
onClick={() => document.getElementById("template-form")?.dispatchEvent(new Event("submit", { cancelable: true, bubbles: true }))} form="template-form"
> >
{isSubmitting ? "保存中..." : "保存"} {isSubmitting ? "保存中..." : "保存"}
</Button> </Button>
@@ -447,6 +415,21 @@ export default function PromptsNew() {
</div> </div>
</div> </div>
{/* 错误信息 */}
{error && (
<div className="alert alert-error mb-4">
<i className="ri-error-warning-line"></i>
<div>{error}</div>
</div>
)}
{actionData?.errors?.general && (
<div className="alert alert-error mb-4">
<i className="ri-error-warning-line"></i>
<div>{actionData.errors.general}</div>
</div>
)}
{/* 查看模式提示 */} {/* 查看模式提示 */}
{isViewMode && ( {isViewMode && (
<div className="alert alert-info"> <div className="alert alert-info">
@@ -458,7 +441,7 @@ export default function PromptsNew() {
)} )}
{/* 模板表单 */} {/* 模板表单 */}
<form id="template-form" method="post" onSubmit={handleSubmit}> <Form id="template-form" method="post">
{/* 模板ID - 隐藏字段 */} {/* 模板ID - 隐藏字段 */}
<input type="hidden" name="id" value={formData.id || ''} /> <input type="hidden" name="id" value={formData.id || ''} />
@@ -474,7 +457,7 @@ export default function PromptsNew() {
</label> </label>
<input <input
type="text" type="text"
className={`form-input py-1 ${isViewMode ? 'read-only-field' : ''}`} className={`form-input py-1 ${isViewMode ? 'read-only-field' : ''} ${actionData?.errors?.template_name ? 'input-error' : ''}`}
id="template-name" id="template-name"
name="template_name" name="template_name"
placeholder="请输入模板名称" placeholder="请输入模板名称"
@@ -483,6 +466,9 @@ export default function PromptsNew() {
readOnly={isViewMode} readOnly={isViewMode}
required required
/> />
{actionData?.errors?.template_name && (
<div className="error-message">{actionData.errors.template_name}</div>
)}
<div className="help-text text-xs">使&quot;-&quot;</div> <div className="help-text text-xs">使&quot;-&quot;</div>
</div> </div>
@@ -492,7 +478,7 @@ export default function PromptsNew() {
<span className="text-error">*</span> <span className="text-error">*</span>
</label> </label>
<select <select
className={`form-select py-1 ${isViewMode ? 'read-only-field' : ''}`} className={`form-select py-1 ${isViewMode ? 'read-only-field' : ''} ${actionData?.errors?.template_type ? 'input-error' : ''}`}
id="template-type" id="template-type"
name="template_type" name="template_type"
value={formData.template_type || ''} value={formData.template_type || ''}
@@ -506,6 +492,9 @@ export default function PromptsNew() {
<option value="Evaluation">(Evaluation) - </option> <option value="Evaluation">(Evaluation) - </option>
<option value="Summary">(Summary) - </option> <option value="Summary">(Summary) - </option>
</select> </select>
{actionData?.errors?.template_type && (
<div className="error-message">{actionData.errors.template_type}</div>
)}
</div> </div>
{/* 模板描述 */} {/* 模板描述 */}
@@ -535,15 +524,22 @@ export default function PromptsNew() {
<input <input
type="checkbox" type="checkbox"
id="status-toggle" id="status-toggle"
name="status"
checked={formData.status === 'active'} checked={formData.status === 'active'}
onChange={handleStatusToggle} onChange={handleStatusToggle}
disabled={isViewMode} disabled={isViewMode}
/> />
{/* 移除name属性,只使用隐藏字段传递状态值 */}
<span className="slider"></span> <span className="slider"></span>
</label> </label>
<span id="status-text">{formData.status === 'active' ? '启用' : '停用'}</span> <span id="status-text">{formData.status === 'active' ? '启用' : '停用'}</span>
</div> </div>
{/* 使用隐藏字段传递状态值 */}
<input
type="hidden"
name="status"
value={formData.status}
/>
</div> </div>
{/* 模板版本 */} {/* 模板版本 */}
@@ -584,7 +580,7 @@ export default function PromptsNew() {
<span className="text-error">*</span> <span className="text-error">*</span>
</label> </label>
<textarea <textarea
className={`form-code-editor w-full ${isViewMode ? 'read-only-field' : ''}`} className={`form-code-editor w-full ${isViewMode ? 'read-only-field' : ''} ${actionData?.errors?.template_content ? 'input-error' : ''}`}
id="template-content" id="template-content"
name="template_content" name="template_content"
placeholder="在此输入提示词模板内容..." placeholder="在此输入提示词模板内容..."
@@ -594,6 +590,9 @@ export default function PromptsNew() {
rows={15} rows={15}
required required
></textarea> ></textarea>
{actionData?.errors?.template_content && (
<div className="error-message">{actionData.errors.template_content}</div>
)}
<div className="help-text">AI完成特定任务的指令</div> <div className="help-text">AI完成特定任务的指令</div>
</div> </div>
@@ -642,6 +641,7 @@ export default function PromptsNew() {
className="form-input" className="form-input"
value={varName} value={varName}
readOnly readOnly
/> />
<input <input
type="text" type="text"
@@ -703,7 +703,7 @@ export default function PromptsNew() {
</Link> </Link>
{!isViewMode && ( {!isViewMode && (
<button <button
type="submit" form="template-form"
className="ant-btn ant-btn-primary" className="ant-btn ant-btn-primary"
disabled={isSubmitting} disabled={isSubmitting}
id="save-btn-bottom" id="save-btn-bottom"
@@ -713,7 +713,7 @@ export default function PromptsNew() {
)} )}
</div> </div>
</div> </div>
</form> </Form>
</div> </div>
); );
} }
+45 -6
View File
@@ -41,7 +41,7 @@
.config-new-page .form-input, .config-new-page .form-input,
.config-new-page .form-select, .config-new-page .form-select,
.config-new-page .form-textarea { .config-new-page .form-textarea {
@apply block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm; @apply block w-full border rounded-md shadow-sm py-2 px-3 focus:outline-none sm:text-sm;
} }
.config-new-page .form-textarea { .config-new-page .form-textarea {
@@ -97,9 +97,9 @@
/* JSON编辑器 */ /* JSON编辑器 */
.config-new-page .json-editor { .config-new-page .json-editor {
@apply w-full min-h-[320px] font-mono text-sm leading-relaxed @apply w-full min-h-[400px] font-mono text-sm leading-relaxed
border border-gray-300 rounded-md p-3 bg-gray-50 text-gray-800 border rounded-md p-3 bg-gray-50 text-gray-800
focus:outline-none focus:ring-primary-500 focus:border-primary-500; focus:outline-none focus:border-[#00684a] focus:ring focus:ring-[#00684a] focus:ring-opacity-20;
} }
.config-new-page .editor-actions { .config-new-page .editor-actions {
@@ -108,7 +108,7 @@
/* 示例卡片 */ /* 示例卡片 */
.config-new-page .example-card { .config-new-page .example-card {
@apply h-full flex flex-col bg-gray-50 rounded-md border border-gray-200 overflow-hidden; @apply h-[500px] flex flex-col bg-gray-50 rounded-md border border-gray-200 overflow-hidden;
} }
.config-new-page .example-header { .config-new-page .example-header {
@@ -120,7 +120,7 @@
} }
.config-new-page .example-content { .config-new-page .example-content {
@apply p-3 flex-grow overflow-auto; @apply p-3 flex-grow overflow-auto text-sm ;
} }
.config-new-page .example-pre { .config-new-page .example-pre {
@@ -137,3 +137,42 @@
.config-new-page .code-json .number { @apply text-purple-600; } .config-new-page .code-json .number { @apply text-purple-600; }
.config-new-page .code-json .boolean { @apply text-blue-600; } .config-new-page .code-json .boolean { @apply text-blue-600; }
.config-new-page .code-json .null { @apply text-gray-500; } .config-new-page .code-json .null { @apply text-gray-500; }
.config-new-page .json-display {
font-family: monospace;
white-space: pre;
background-color: #f8fafc;
padding: 1rem;
border-radius: 0.375rem;
overflow-x: auto;
}
.config-new-page .json-line {
line-height: 1.5;
}
.config-new-page .json-char {
color: #334155;
}
.config-new-page .json-brace {
color: #64748b;
font-weight: bold;
}
.config-new-page .json-bracket {
color: #64748b;
font-weight: bold;
}
.config-new-page .json-quote {
color: #0ea5e9;
}
.config-new-page .json-colon {
color: #64748b;
}
.config-new-page .json-comma {
color: #64748b;
}
+1 -1
View File
@@ -38,7 +38,7 @@
} }
.file-upload-page .form-select { .file-upload-page .form-select {
@apply block w-full px-3 py-2 text-base border-gray-300 rounded-md shadow-sm focus:outline-none; @apply block w-full px-3 py-2 text-base rounded-md shadow-sm focus:outline-none;
} }
.file-upload-page .form-select:focus { .file-upload-page .form-select:focus {
+2 -1
View File
@@ -41,7 +41,8 @@
.prompt-new-page .form-input:focus, .prompt-new-page .form-input:focus,
.prompt-new-page .form-select:focus, .prompt-new-page .form-select:focus,
.prompt-new-page .form-textarea:focus { .prompt-new-page .form-textarea:focus,
.prompt-new-page .form-code-editor:focus {
@apply outline-none border-[var(--primary-color)] shadow-[0_0_0_2px_rgba(0,104,74,0.2)]; @apply outline-none border-[var(--primary-color)] shadow-[0_0_0_2px_rgba(0,104,74,0.2)];
} }
+5 -3
View File
@@ -4,10 +4,12 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>中国烟草AI合同及卷宗审核系统 - 文档类型列表</title> <title>中国烟草AI合同及卷宗审核系统 - 文档类型列表</title>
<link href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/remixicon/2.5.0/remixicon.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
<!-- <link href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> -->
<!-- 引入外部CSS文件 --> <!-- 引入外部CSS文件 -->
<link href="../css/main.css" rel="stylesheet"> <link href="main.css" rel="stylesheet">
<style> <style>
.type-badge { .type-badge {
display: inline-flex; display: inline-flex;
+5 -3
View File
@@ -4,10 +4,12 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>中国烟草AI合同及卷宗审核系统 - 新增/编辑文档类型</title> <title>中国烟草AI合同及卷宗审核系统 - 新增/编辑文档类型</title>
<link href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/remixicon/2.5.0/remixicon.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
<!-- <link href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> -->
<!-- 引入外部CSS文件 --> <!-- 引入外部CSS文件 -->
<link href="../css/main.css" rel="stylesheet"> <link href="main.css" rel="stylesheet">
<style> <style>
.checkbox-group { .checkbox-group {
display: flex; display: flex;