From 95381ddcc2e826351790a4acbcd655c346974b61 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Tue, 11 Nov 2025 01:16:27 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=8C=E5=96=84=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E8=AF=8D=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2=E7=9A=84=E4=BC=98?= =?UTF-8?q?=E5=8C=96=EF=BC=8C=E6=95=B0=E6=8D=AE=E5=BA=93=E4=B8=AD=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=9B=B8=E5=85=B3=E5=AD=97=E6=AE=B5=E6=9D=A5=E5=8C=BA?= =?UTF-8?q?=E5=88=86vlm=E5=92=8Cllm=E6=8F=90=E7=A4=BA=E8=AF=8D=E3=80=82?= =?UTF-8?q?=E8=AF=84=E6=9F=A5=E7=82=B9=E8=AE=BE=E7=BD=AE=E4=B8=AD=E6=8A=BD?= =?UTF-8?q?=E5=8F=96=E8=AE=BE=E7=BD=AE=E7=9A=84=E5=A4=9A=E6=A8=A1=E6=80=81?= =?UTF-8?q?=E6=8A=BD=E5=8F=96=E7=9A=84=E7=B1=BB=E5=9E=8B=E9=80=9A=E8=BF=87?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E6=95=B0=E6=8D=AE=E5=BA=93=E6=9D=A5=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E6=95=B0=E6=8D=AE=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/prompts/prompts.ts | 88 +++++++++++++++-- .../rules/new/ExtractionSettings.tsx | 78 +++++++-------- app/routes/prompts._index.tsx | 15 +-- app/routes/prompts.new.tsx | 96 ++++++++++++++++--- app/routes/rule-groups._index.tsx | 9 +- app/routes/rules-new.tsx | 42 +++++++- app/styles/pages/prompts_index.css | 9 ++ 7 files changed, 261 insertions(+), 76 deletions(-) diff --git a/app/api/prompts/prompts.ts b/app/api/prompts/prompts.ts index 67ec53a..4a49e16 100644 --- a/app/api/prompts/prompts.ts +++ b/app/api/prompts/prompts.ts @@ -14,6 +14,8 @@ export interface PromptTemplate { created_by: number; created_at: string; updated_at: string; + template_code?: string; // 模板代码(VLM_Extraction 类型时使用) + template_abbreviation?: string; // 模板简称(VLM_Extraction 类型时使用) } // 提示词模板前端接口 @@ -30,6 +32,8 @@ export interface PromptTemplateUI { created_by_username?: string; // 创建者用户名 created_at: string; updated_at: string; + template_code?: string; // 模板代码(VLM_Extraction 类型时使用) + template_abbreviation?: string; // 模板简称(VLM_Extraction 类型时使用) } // 搜索参数接口 @@ -108,7 +112,9 @@ export function convertToUITemplate(template: PromptTemplate & { sso_users?: { u created_by: template.created_by, created_by_username: template.sso_users?.username, // 从关联的用户信息中提取用户名 created_at: formatDate(template.created_at), - updated_at: formatDate(template.updated_at) + updated_at: formatDate(template.updated_at), + template_code: template.template_code, + template_abbreviation: template.template_abbreviation }; } @@ -131,7 +137,7 @@ export async function getPromptTemplates(searchParams: PromptSearchParams = {}, // 构建查询参数,包含对 sso_users 表的左连接 const params: PostgrestParams = { - select: `id,template_name,template_type,description,template_content,variables,status,version,created_by,created_at,updated_at,sso_users!created_by(username)`, + select: `id,template_name,template_type,description,template_content,variables,status,version,created_by,created_at,updated_at,template_code,template_abbreviation,sso_users!created_by(username)`, order: 'updated_at.desc', headers: { 'Prefer': 'count=exact' @@ -232,7 +238,7 @@ export async function getPromptTemplate(id: string, frontendJWT?: string): Promi } const params: PostgrestParams = { - select: `id,template_name,template_type,description,template_content,variables,status,version,created_by,created_at,updated_at,sso_users!created_by(username)`, + select: `id,template_name,template_type,description,template_content,variables,status,version,created_by,created_at,updated_at,template_code,template_abbreviation,sso_users!created_by(username)`, filter: { 'id': `eq.${id}` }, @@ -305,7 +311,9 @@ export async function createPromptTemplate(template: Partial, variables: variablesData, status: mapStatusToAPI(template.status || 'active'), version: template.version || 'v1.0', - created_by: userId // 使用当前登录用户ID + created_by: userId, // 使用当前登录用户ID + template_code: template.template_code, + template_abbreviation: template.template_abbreviation }; if(apiTemplate){ @@ -399,11 +407,19 @@ export async function updatePromptTemplate(id: string, template: Partial>( 'prompt_templates', apiTemplate, @@ -446,7 +462,7 @@ export async function deletePromptTemplate(id: string, frontendJWT?: string): Pr if (!id) { return { error: '模板ID不能为空', status: 400 }; } - + // 使用真实删除替代状态更新 const response = await postgrestDelete( 'prompt_templates', @@ -457,11 +473,11 @@ export async function deletePromptTemplate(id: string, frontendJWT?: string): Pr token: frontendJWT } ); - + if (response.error) { return { error: response.error, status: response.status }; } - + return { success: true }; } catch (error) { console.error('删除提示词模板失败:', error); @@ -470,4 +486,60 @@ export async function deletePromptTemplate(id: string, frontendJWT?: string): Pr status: 500 }; } +} + +/** + * 获取指定类型的提示词模板选项 + * @param templateType 模板类型(如 'VLM_Extraction', 'LLM_Extraction' 等) + * @param frontendJWT JWT token (可选) + * @returns 模板选项列表 { value: template_code, label: template_abbreviation } + */ +export async function getPromptTemplateOptions(templateType: string, frontendJWT?: string): Promise<{ + data?: Array<{ value: string; label: string }>; + error?: string; + status?: number; +}> { + try { + if (!templateType) { + return { error: '模板类型不能为空', status: 400 }; + } + + const params: PostgrestParams = { + select: 'template_code,template_abbreviation', + filter: { + 'template_type': `eq.${templateType}`, + 'status': 'gte.0' // 只查询有效状态的模板 + }, + order: 'template_abbreviation.asc', // 按标签排序 + token: frontendJWT + }; + + const response = await postgrestGet>('prompt_templates', params); + + if (response.error) { + console.error('获取提示词模板选项失败:', response.error); + return { error: response.error, status: response.status }; + } + + const extractedData = extractApiData>(response.data); + + if (!extractedData) { + console.error('提取提示词模板选项数据失败'); + return { error: '获取提示词模板选项失败', status: 500 }; + } + + // 转换为选项格式 + const options = extractedData.map(item => ({ + value: item.template_code, + label: item.template_abbreviation + })); + + return { data: options }; + } catch (error) { + console.error('获取提示词模板选项出错:', error); + return { + error: error instanceof Error ? error.message : '获取提示词模板选项失败', + status: 500 + }; + } } \ No newline at end of file diff --git a/app/components/rules/new/ExtractionSettings.tsx b/app/components/rules/new/ExtractionSettings.tsx index abb3331..75f95d0 100644 --- a/app/components/rules/new/ExtractionSettings.tsx +++ b/app/components/rules/new/ExtractionSettings.tsx @@ -45,6 +45,7 @@ interface ExtractionSettingsProps { export function ExtractionSettings({ onChange, initialData, + vlmFieldTypeOptions = EVALUATION_OPTIONS.vlmFieldTypeOptions, }: ExtractionSettingsProps) { // 核心数据状态 @@ -84,7 +85,10 @@ export function ExtractionSettings({ vlm: initialData?.extraction_config?.vlm?.fields || [] }); // VLM字段类型 - const [selectedVlmFieldType, setSelectedVlmFieldType] = useState('vlm_default_prompt'); + const [selectedVlmFieldType, setSelectedVlmFieldType] = useState(() => { + // 使用传入的选项中的第一个作为默认值,如果没有则使用 vlm_default_prompt + return vlmFieldTypeOptions.length > 0 ? vlmFieldTypeOptions[0].value : 'vlm_default_prompt'; + }); // 自定义字段的提示词模板 const [customVlmPrompt, setCustomVlmPrompt] = useState('请识别文档中的印章信息,提取以下字段'); // 提示词类型 @@ -117,6 +121,14 @@ export function ExtractionSettings({ setCurrentTab(tab); }; + // 当 vlmFieldTypeOptions 加载完成时,更新默认选中的类型 + useEffect(() => { + if (vlmFieldTypeOptions.length > 0 && !vlmFieldTypeOptions.find(opt => opt.value === selectedVlmFieldType)) { + // 如果当前选中的类型不在新的选项列表中,选择第一个选项 + setSelectedVlmFieldType(vlmFieldTypeOptions[0].value); + } + }, [vlmFieldTypeOptions, selectedVlmFieldType]); + // 初始化自定义字段的提示词 useEffect(() => { // 在编辑模式下,如果有自定义类型的字段,加载其 template @@ -249,52 +261,32 @@ export function ExtractionSettings({ if (typeof field === 'string') { const parts = field.split('_'); fieldName = parts[0]; - fieldType = parts.length > 1 ? parts[1] : 'default'; + fieldType = parts.length > 1 ? parts[1] : 'vlm_default_prompt'; } else { fieldName = field.name; fieldType = field.type; } - switch (fieldType) { - case 'vlm_default_prompt': - typeName = '默认'; - badgeClass = 'bg-gray-100 text-gray-800'; - break; - case 'vlm_currency_prompt': - typeName = '货币'; - badgeClass = 'bg-green-100 text-green-800'; - break; - case 'vlm_print_prompt': - typeName = '打印'; - badgeClass = 'bg-blue-100 text-blue-800'; - break; - case 'vlm_seal_prompt': - typeName = '印章'; - badgeClass = 'bg-red-100 text-red-800'; - break; - case 'vlm_acrossPageSeal_prompt': - typeName = '骑缝章'; - badgeClass = 'bg-orange-100 text-orange-800'; - break; - case 'vlm_english_prompt': - typeName = '英文'; - badgeClass = 'bg-purple-100 text-purple-800'; - break; - case 'vlm_number_prompt': - typeName = '数字'; - badgeClass = 'bg-yellow-100 text-yellow-800'; - break; - case 'vlm_handwriting_prompt': - typeName = '手写'; - badgeClass = 'bg-pink-100 text-pink-800'; - break; - case 'custom': - typeName = '自定义'; - badgeClass = 'bg-indigo-100 text-indigo-800'; - break; - default: - typeName = '默认'; - badgeClass = 'bg-gray-100 text-gray-800'; + // 首先尝试从 vlmFieldTypeOptions 中查找对应的标签 + const optionItem = vlmFieldTypeOptions.find(opt => opt.value === fieldType); + if (optionItem) { + typeName = optionItem.label; + // 根据不同类型设置不同的颜色 + switch (fieldType) { + case 'vlm_default_prompt': + badgeClass = 'bg-gray-100 text-gray-800'; + break; + case 'custom': + badgeClass = 'bg-indigo-100 text-indigo-800'; + break; + default: + // 对于从数据库获取的类型,使用统一的蓝色系 + badgeClass = 'bg-blue-100 text-blue-800'; + } + } else { + // 如果找不到,使用默认值 + typeName = '未知类型'; + badgeClass = 'bg-gray-100 text-gray-800'; } return { fieldName, fieldType, typeName, badgeClass }; @@ -408,7 +400,7 @@ export function ExtractionSettings({ value={selectedVlmFieldType} onChange={(e) => setSelectedVlmFieldType(e.target.value)} > - {EVALUATION_OPTIONS.vlmFieldTypeOptions.map((option) => ( + {vlmFieldTypeOptions.map((option) => ( diff --git a/app/routes/prompts._index.tsx b/app/routes/prompts._index.tsx index 47529d1..8588643 100644 --- a/app/routes/prompts._index.tsx +++ b/app/routes/prompts._index.tsx @@ -8,6 +8,7 @@ import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterP import { Table } from "~/components/ui/Table"; import { Pagination } from "~/components/ui/Pagination"; import { getPromptTemplates, deletePromptTemplate, type PromptTemplateUI } from "~/api/prompts/prompts"; +import { toastService } from "~/components/ui"; // 样式链接 export function links() { @@ -65,7 +66,7 @@ export async function loader({ request }: LoaderFunctionArgs) { if (result.error) { console.error('获取提示词模板失败:', result.error); return Response.json( - { + { templates: [], total: 0, pageSize, @@ -75,10 +76,10 @@ export async function loader({ request }: LoaderFunctionArgs) { { status: result.status || 500 } ); } - + // console.log(`成功加载${result.data?.templates.length || 0}条提示词模板数据`); - - return Response.json({ + + return Response.json({ templates: result.data?.templates || [], total: result.data?.total || 0, pageSize, @@ -218,10 +219,10 @@ export default function PromptsIndex() { useEffect(() => { if (fetcher.state === 'idle' && fetcher.data) { if (fetcher.data.success) { - alert('删除成功!'); - window.location.reload(); + toastService.success('删除成功!'); + // Remix 会自动重新验证 loader 数据,无需手动刷新页面 } else if (fetcher.data.error) { - alert(`删除失败: ${fetcher.data.error}`); + toastService.error(`删除失败: ${fetcher.data.error}`); } } }, [fetcher.state, fetcher.data]); diff --git a/app/routes/prompts.new.tsx b/app/routes/prompts.new.tsx index 3b1464b..74c6c93 100644 --- a/app/routes/prompts.new.tsx +++ b/app/routes/prompts.new.tsx @@ -4,6 +4,7 @@ import { Link, useLoaderData, useNavigation, useActionData, Form } from "@remix- import { Button } from "~/components/ui/Button"; import newStyles from "~/styles/pages/prompts_new.css?url"; import { getPromptTemplate, createPromptTemplate, updatePromptTemplate, type PromptTemplateUI } from "~/api/prompts/prompts"; +// import { toastService } from "~/components/ui"; // 样式链接 export function links() { @@ -40,6 +41,8 @@ interface LoaderData { // 定义本地表单数据接口 interface FormDataState extends Omit { variables: string; // 在表单状态中我们保存变量为 JSON 字符串 + template_code?: string; // 模板代码 + template_abbreviation?: string; // 模板简称 } interface ActionData { @@ -48,6 +51,8 @@ interface ActionData { template_name?: string; template_type?: string; template_content?: string; + template_code?: string; + template_abbreviation?: string; general?: string; }; formData?: { @@ -58,6 +63,8 @@ interface ActionData { variables: string; status: "active" | "inactive" | "system"; version: string; + template_code?: string; + template_abbreviation?: string; }; } @@ -110,8 +117,8 @@ export async function loader({ request }: LoaderFunctionArgs) { export async function action({ request }: ActionFunctionArgs) { const { getUserSession } = await import("~/api/login/auth.server"); const { userInfo, frontendJWT } = await getUserSession(request); - - const userId = userInfo.get('user_id') + + const userId = userInfo.user_id; const formData = await request.formData(); const id = formData.get("id") as string; const template_name = formData.get("template_name") as string; @@ -121,6 +128,8 @@ export async function action({ request }: ActionFunctionArgs) { const variables = formData.get("variables") as string; const status = formData.get("status") as string; const version = formData.get("version") as string; + const template_code = formData.get("template_code") as string; + const template_abbreviation = formData.get("template_abbreviation") as string; const errors: ActionData["errors"] = {}; @@ -137,6 +146,19 @@ export async function action({ request }: ActionFunctionArgs) { errors.template_content = "模板内容不能为空"; } + // VLM_Extraction 类型的额外验证 + if (template_type === 'VLM_Extraction') { + if (!template_code || template_code.trim() === "") { + errors.template_code = "VLM抽取类型必须填写模板code"; + // toastService.error('VLM抽取类型必须填写模板code') + } + + if (!template_abbreviation || template_abbreviation.trim() === "") { + errors.template_abbreviation = "VLM抽取类型必须填写模板简称"; + // toastService.error('VLM抽取类型必须填写模板简称') + } + } + if (Object.keys(errors).length > 0) { return Response.json({ errors }); } @@ -160,7 +182,9 @@ export async function action({ request }: ActionFunctionArgs) { template_content, variables: variablesData, status: status === "active" ? "active" : "inactive", - version: version || "v1.0" + version: version || "v1.0", + template_code: template_code || undefined, + template_abbreviation: template_abbreviation || undefined }; let result; @@ -173,7 +197,7 @@ export async function action({ request }: ActionFunctionArgs) { } if (result.error) { - return Response.json({ + return Response.json({ errors: { general: result.error }, formData: { template_name, @@ -182,7 +206,9 @@ export async function action({ request }: ActionFunctionArgs) { template_content, variables, status, - version + version, + template_code, + template_abbreviation } }); } @@ -190,9 +216,9 @@ export async function action({ request }: ActionFunctionArgs) { return redirect("/prompts"); } catch (error) { console.error("保存提示词模板失败:", error); - return Response.json({ - errors: { - general: error instanceof Error ? error.message : "保存提示词模板失败" + return Response.json({ + errors: { + general: error instanceof Error ? error.message : "保存提示词模板失败" }, formData: { template_name, @@ -201,7 +227,9 @@ export async function action({ request }: ActionFunctionArgs) { template_content, variables, status, - version + version, + template_code, + template_abbreviation } }); } @@ -252,7 +280,9 @@ export default function PromptsNew() { created_by: 1, variables: "{}", created_at: "", - updated_at: "" + updated_at: "", + template_code: "", + template_abbreviation: "" }); // 模式状态 @@ -497,7 +527,7 @@ export default function PromptsNew() { - + {actionData?.errors?.template_code && ( +
{actionData.errors.template_code}
+ )} +
VLM抽取类型必须填写,用于标识提示词类型
+ + + {/* 模板简称(VLM_Extraction类型时必填) */} +
+ + + {actionData?.errors?.template_abbreviation && ( +
{actionData.errors.template_abbreviation}
+ )} +
VLM抽取类型必须填写,用于在下拉选项中显示
+
{/* 模板描述 */}
diff --git a/app/routes/rule-groups._index.tsx b/app/routes/rule-groups._index.tsx index 6b1f481..b54e4d9 100644 --- a/app/routes/rule-groups._index.tsx +++ b/app/routes/rule-groups._index.tsx @@ -9,6 +9,7 @@ import { Table } from "~/components/ui/Table"; import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel"; // import { Pagination } from "~/components/ui/Pagination"; import { getRuleGroups, getChildGroups, type RuleGroup, deleteRuleGroup } from "~/api/evaluation_points/rule-groups"; +import { toastService } from "~/components/ui"; export function links() { return [{ rel: "stylesheet", href: indexStyles }]; @@ -214,13 +215,15 @@ export default function RuleGroupsIndex() { setExpandedGroups(prev => prev.filter(id => id !== groupId)); // 显示成功消息 - alert('删除成功'); + // alert('删除成功'); + toastService.success('删除成功') } else { - alert(`删除失败: ${result.error}`); + toastService.error(`删除失败: ${result.error}`); + console.error(`删除失败: ${result.error}`); } } catch (error) { console.error('删除分组失败:', error); - alert('删除分组失败,请稍后重试'); + toastService.error('删除分组失败,请稍后重试'); } } }; diff --git a/app/routes/rules-new.tsx b/app/routes/rules-new.tsx index 70ce34e..cbf19ae 100644 --- a/app/routes/rules-new.tsx +++ b/app/routes/rules-new.tsx @@ -51,6 +51,7 @@ import { RuleContext } from "~/contexts/RuleContext"; import { postgrestGet, postgrestPost, postgrestPut } from "~/api/postgrest-client"; import { toastService } from '~/components/ui/Toast'; import type { UserRole } from '~/root'; +import { getPromptTemplateOptions } from '~/api/prompts/prompts'; export const meta: MetaFunction = () => { return [ @@ -160,13 +161,16 @@ export default function RuleNew() { const [formData, setFormData] = useState({}); const [evaluationPointGroups, setEvaluationPointGroups] = useState([]); - + // 判断表单是否为只读模式 const isReadOnly = userRole === 'common'; - + // 添加用于共享的字段数据状态 const [extractionFields, setExtractionFields] = useState([]); + // VLM字段类型选项 + const [vlmFieldTypeOptions, setVlmFieldTypeOptions] = useState>([]); + /** * 从表单数据中提取所有字段 * 用于编辑模式下初始化字段数据 @@ -355,6 +359,34 @@ export default function RuleNew() { } }, [frontendJWT]); + /** + * 获取VLM字段类型选项 + * 从API获取多模态抽取的字段类型选项 + */ + const fetchVlmFieldTypeOptions = useCallback(async () => { + try { + // console.log("获取VLM字段类型选项"); + const response = await getPromptTemplateOptions('VLM_Extraction', frontendJWT); + + if (response.data && Array.isArray(response.data)) { + // 添加自定义选项 + const optionsWithCustom = [ + ...response.data, + { value: 'custom', label: '自定义' } + ]; + setVlmFieldTypeOptions(optionsWithCustom); + } else if (response.error) { + console.error('获取VLM字段类型选项失败:', response.error); + // 使用默认选项作为备选 + setVlmFieldTypeOptions(EVALUATION_OPTIONS.vlmFieldTypeOptions); + } + } catch (error) { + console.error('获取VLM字段类型选项失败:', error); + // 使用默认选项作为备选 + setVlmFieldTypeOptions(EVALUATION_OPTIONS.vlmFieldTypeOptions); + } + }, [frontendJWT]); + const handleSave = async () => { // console.log("保存评查点", formData); @@ -917,7 +949,9 @@ export default function RuleNew() { // 获取评查点组数据 fetchEvaluationPointGroups(); - }, [location.search, fetchEvaluationPoint, fetchEvaluationPointGroups, resetFormData]); + // 获取VLM字段类型选项 + fetchVlmFieldTypeOptions(); + }, [location.search, fetchEvaluationPoint, fetchEvaluationPointGroups, fetchVlmFieldTypeOptions, resetFormData]); // 渲染页面内容 return ( @@ -958,7 +992,7 @@ export default function RuleNew() { onChange={handleExtractionSettingsChange} initialData={formData} promptTypeOptions={EVALUATION_OPTIONS.llmPromptTypeOptions} - vlmFieldTypeOptions={EVALUATION_OPTIONS.vlmFieldTypeOptions} + vlmFieldTypeOptions={vlmFieldTypeOptions.length > 0 ? vlmFieldTypeOptions : EVALUATION_OPTIONS.vlmFieldTypeOptions} />
diff --git a/app/styles/pages/prompts_index.css b/app/styles/pages/prompts_index.css index 1b79ce5..aa76501 100644 --- a/app/styles/pages/prompts_index.css +++ b/app/styles/pages/prompts_index.css @@ -94,6 +94,15 @@ @apply mr-1; } +/* 删除按钮 - 红色 */ +.prompt-page .operation-btn.text-error { + @apply !text-red-600; +} + +.prompt-page .operation-btn.text-error:hover { + @apply !text-red-700 bg-red-50; +} + /* 分页 */ .prompt-page .pagination-info { @apply text-sm text-gray-500;