diff --git a/app/components/layout/Sidebar.tsx b/app/components/layout/Sidebar.tsx index 49c81b8..5ad1551 100644 --- a/app/components/layout/Sidebar.tsx +++ b/app/components/layout/Sidebar.tsx @@ -34,7 +34,7 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) { { id: 'file-upload', title: '文件上传', - path: '/files/new', + path: '/files/upload', icon: 'ri-upload-cloud-line' }, { diff --git a/app/components/ui/FileProgress.tsx b/app/components/ui/FileProgress.tsx new file mode 100644 index 0000000..1b648d4 --- /dev/null +++ b/app/components/ui/FileProgress.tsx @@ -0,0 +1,44 @@ +import fileProgressStyles from "~/styles/components/file-progress.css?url"; + +interface FileProgressProps { + fileName: string; + fileSize?: string; + progress: number; + speed?: string; + className?: string; +} + +export function links() { + return [{ rel: "stylesheet", href: fileProgressStyles }]; +} + +export function FileProgress({ + fileName, + fileSize, + progress, + speed = "0KB/s", + className = "" +}: FileProgressProps) { + return ( +
+
+ {fileName} + {fileSize && {fileSize}} +
+
+
+
+
+ {progress}% + {speed} +
+
+ ); +} \ No newline at end of file diff --git a/app/components/ui/ProcessingSteps.tsx b/app/components/ui/ProcessingSteps.tsx new file mode 100644 index 0000000..e65402d --- /dev/null +++ b/app/components/ui/ProcessingSteps.tsx @@ -0,0 +1,41 @@ +import processingStepsStyles from "~/styles/components/processing-steps.css?url"; + +export interface Step { + title: string; + description: string; + status: 'waiting' | 'active' | 'done' | 'error'; +} + +interface ProcessingStepsProps { + steps: Step[]; + className?: string; +} + +export function links() { + return [{ rel: "stylesheet", href: processingStepsStyles }]; +} + +export function ProcessingSteps({ steps, className = "" }: ProcessingStepsProps) { + return ( +
+ {steps.map((step, index) => ( +
+
+ {step.status === 'done' && } + {step.status === 'error' && } + {step.status === 'active' && } + {step.status === 'waiting' && {index + 1}} +
+
+
{step.title}
+
{step.description}
+
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/app/components/ui/UploadArea.tsx b/app/components/ui/UploadArea.tsx new file mode 100644 index 0000000..1f1837b --- /dev/null +++ b/app/components/ui/UploadArea.tsx @@ -0,0 +1,128 @@ +import { useRef, useState, useCallback, ReactNode, forwardRef, useImperativeHandle } from "react"; +import { Button } from "./Button"; +import uploadAreaStyles from "~/styles/components/upload-area.css?url"; + +interface UploadAreaProps { + onFilesSelected: (files: FileList) => void; + className?: string; + accept?: string; + multiple?: boolean; + icon?: string; + buttonText?: string; + mainText?: string; + tipText?: ReactNode; + disabled?: boolean; +} + +export interface UploadAreaRef { + resetFileInput: () => void; +} + +export function links() { + return [{ rel: "stylesheet", href: uploadAreaStyles }]; +} + +export const UploadArea = forwardRef(({ + onFilesSelected, + className = "", + accept = "", + multiple = false, + icon = "ri-upload-cloud-2-line", + buttonText = "选择文件", + mainText = "点击或拖拽文件到此区域上传", + tipText = "", + disabled = false +}, ref) => { + const fileInputRef = useRef(null); + const [isDragOver, setIsDragOver] = useState(false); + + const resetFileInput = useCallback(() => { + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }, []); + + // 暴露resetFileInput方法给父组件 + useImperativeHandle(ref, () => ({ + resetFileInput + })); + + const handleClick = useCallback(() => { + if (!disabled && fileInputRef.current) { + fileInputRef.current.click(); + } + }, [disabled]); + + const handleFileChange = useCallback(() => { + if (fileInputRef.current?.files?.length) { + onFilesSelected(fileInputRef.current.files); + } + }, [onFilesSelected]); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + if (!disabled) { + setIsDragOver(true); + } + }, [disabled]); + + const handleDragLeave = useCallback(() => { + setIsDragOver(false); + }, []); + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(false); + + if (!disabled && e.dataTransfer.files.length > 0) { + onFilesSelected(e.dataTransfer.files); + } + }, [disabled, onFilesSelected]); + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if ((e.key === 'Enter' || e.key === ' ') && !disabled) { + e.preventDefault(); + handleClick(); + } + }, [handleClick, disabled]); + + return ( +
+ + +
{mainText}
+ {tipText &&

{tipText}

} + +
+ ); +}); + +UploadArea.displayName = "UploadArea"; \ No newline at end of file diff --git a/app/routes/files.tsx b/app/routes/files.tsx new file mode 100644 index 0000000..8390b86 --- /dev/null +++ b/app/routes/files.tsx @@ -0,0 +1,5 @@ +import { Outlet } from "@remix-run/react"; + +export default function Files() { + return ; +} \ No newline at end of file diff --git a/app/routes/files.upload.tsx b/app/routes/files.upload.tsx new file mode 100644 index 0000000..805ef34 --- /dev/null +++ b/app/routes/files.upload.tsx @@ -0,0 +1,697 @@ +import { useState, useEffect, useRef, useCallback } from "react"; +import { MetaFunction, ActionFunctionArgs } from "@remix-run/node"; +import { Card } from "~/components/ui/Card"; +import { Button } from "~/components/ui/Button"; +import { Table } from "~/components/ui/Table"; +import { UploadArea, UploadAreaRef } from "~/components/ui/UploadArea"; +import { FileProgress} from "~/components/ui/FileProgress"; +import { ProcessingSteps, Step } from "~/components/ui/ProcessingSteps"; +import uploadStyles from "~/styles/pages/files_upload.css?url"; + +export function links() { + return [ + { rel: "stylesheet", href: uploadStyles } + ]; +} + +export const meta: MetaFunction = () => { + return [ + { title: "待审核文件上传 - 中国烟草AI合同及卷宗审核系统" }, + { + name: "description", + content: "上传待审核的合同文件、专卖许可证申请、行政处罚决定书等文档,进行AI智能审核" + }, + { + name: "keywords", + content: "文件上传,合同审核,专卖许可证,行政处罚,AI审核,中国烟草" + } + ]; +}; + +// 文件类型定义 +export enum FileType { + CONTRACT = "contract", + LICENSE = "license", + PUNISHMENT = "punishment", + OTHER = "other" +} + +// 文件类型标签映射 +export const FILE_TYPE_LABELS: Record = { + [FileType.CONTRACT]: "合同文档", + [FileType.LICENSE]: "专卖许可证", + [FileType.PUNISHMENT]: "行政处罚决定书", + [FileType.OTHER]: "其他文档" +}; + +// 优先级定义 +export enum Priority { + NORMAL = "normal", + HIGH = "high", + URGENT = "urgent" +} + +// 优先级标签映射 +export const PRIORITY_LABELS: Record = { + [Priority.NORMAL]: "普通", + [Priority.HIGH]: "优先", + [Priority.URGENT]: "紧急" +}; + +// 处理状态定义 +export enum ProcessingStatus { + WAITING = "waiting", + PROCESSING = "processing", + SUCCESS = "success", + ERROR = "error" +} + +// 上传的文件信息接口 +export interface UploadedFile { + id: string; + name: string; + size: number; + type: string; + fileType: FileType; + priority: Priority; + status: ProcessingStatus; + uploadTime: string; + processingInfo?: { + progress: number; + currentStep?: number; + }; +} + +// action处理文件上传请求 +export async function action({ request }: ActionFunctionArgs) { + try { + const formData = await request.formData(); + + // 由于无法直接从Remix的action中处理文件上传, + // 实际环境中应使用FormData将文件发送到后端API + // 这里我们模拟处理过程,创建一个响应对象 + + const fileType = formData.get("fileType") as FileType; + const priority = formData.get("priority") as Priority; + + if (!fileType) { + return Response.json( + { success: false, error: "请选择文件类型" }, + { status: 400 } + ); + } + + // 模拟文件上传成功响应 + return Response.json({ + success: true, + message: "文件上传请求已接收", + fileId: `file_${Date.now()}`, + fileType, + priority: priority || Priority.NORMAL, + }); + } catch (error) { + console.error("文件上传失败:", error); + return Response.json( + { success: false, error: "文件上传失败,请重试" }, + { status: 500 } + ); + } +} + +// 上传队列中的文件列表模拟数据 +const MOCK_QUEUE_FILES: UploadedFile[] = [ + { + id: "1", + name: "烟草产品销售合同(2023版).pdf", + size: 5.2 * 1024 * 1024, // 5.2MB + type: "application/pdf", + fileType: FileType.CONTRACT, + priority: Priority.NORMAL, + status: ProcessingStatus.PROCESSING, + uploadTime: "2023-10-25 14:25:18", + processingInfo: { + progress: 60, + currentStep: 2 + } + }, + { + id: "2", + name: "专卖许可证申请表.docx", + size: 2.8 * 1024 * 1024, // 2.8MB + type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + fileType: FileType.LICENSE, + priority: Priority.HIGH, + status: ProcessingStatus.WAITING, + uploadTime: "2023-10-25 14:28:45" + }, + { + id: "3", + name: "XX公司违规处罚决定书.pdf", + size: 3.1 * 1024 * 1024, // 3.1MB + type: "application/pdf", + fileType: FileType.PUNISHMENT, + priority: Priority.NORMAL, + status: ProcessingStatus.SUCCESS, + uploadTime: "2023-10-25 14:15:30" + } +]; + +// 文件上传页面组件 +export default function FilesUpload() { + // 状态管理 + const [fileType, setFileType] = useState(""); + const [priority, setPriority] = useState(Priority.NORMAL); + const [currentFile, setCurrentFile] = useState(null); + const [uploadProgress, setUploadProgress] = useState(0); + const [uploadSpeed, setUploadSpeed] = useState("0KB/s"); + const [uploadStage, setUploadStage] = useState<"idle" | "uploading" | "processing" | "completed">("idle"); + const [processingSteps, setProcessingSteps] = useState([ + { title: "文件上传", description: "等待上传文件到服务器...", status: "waiting" }, + { title: "文档转换拆分", description: "转换文档格式,拆分文档内容", status: "waiting" }, + { title: "评查点抽取", description: "DeepSeek 抽取中", status: "waiting" }, + { title: "评查点审核", description: "DeepSeek 评查中", status: "waiting" }, + { title: "审核准备", description: "文档已准备就绪,等待审核", status: "waiting" } + ]); + + // 队列文件状态 + const [queueFiles, setQueueFiles] = useState(MOCK_QUEUE_FILES); + + // 上传完成后的文件信息 + const [completedFile, setCompletedFile] = useState(null); + + // 计时器引用 + const progressIntervalRef = useRef(null); + const processingIntervalRef = useRef(null); + + // UploadArea组件引用 + const uploadAreaRef = useRef(null); + + // 清理定时器 + useEffect(() => { + return () => { + if (progressIntervalRef.current) { + clearInterval(progressIntervalRef.current); + } + if (processingIntervalRef.current) { + clearInterval(processingIntervalRef.current); + } + }; + }, []); + + // 处理文件选择 + const handleFilesSelected = useCallback((selectedFiles: FileList) => { + console.log("selectedFiles", selectedFiles); + if (selectedFiles.length === 0) return; + + if (!fileType) { + alert("请先选择文件类型"); + // 重置文件输入框 + if (uploadAreaRef.current) { + uploadAreaRef.current.resetFileInput(); + } + return; + } + + setCurrentFile(selectedFiles[0]); + console.log("currentFile", currentFile); + startUpload(selectedFiles[0]); + }, [fileType, currentFile]); + + // 开始上传文件 + const startUpload = (file: File) => { + setUploadStage("uploading"); + setUploadProgress(0); + + // 更新步骤状态 + const updatedSteps = [...processingSteps]; + updatedSteps[0].status = "active"; + updatedSteps[0].description = `正在上传文件"${file.name}"到服务器...`; + setProcessingSteps(updatedSteps); + + // 模拟上传进度 + if (progressIntervalRef.current) { + clearInterval(progressIntervalRef.current); + } + + progressIntervalRef.current = setInterval(() => { + setUploadProgress(prev => { + const newProgress = prev + 5; + // 根据文件大小调整上传速度 + const speedFactor = Math.min(file.size / (1024 * 1024) + 1, 5); + setUploadSpeed(`${Math.floor(Math.random() * 100 * speedFactor) + 50}KB/s`); + + if (newProgress >= 100) { + if (progressIntervalRef.current) { + clearInterval(progressIntervalRef.current); + setUploadSpeed("完成"); + + // 完成上传后开始处理流程 + startProcessing(); + } + return 100; + } + + return newProgress; + }); + }, 200); + }; + + // 开始处理文件 + const startProcessing = () => { + setUploadStage("processing"); + + // 更新步骤状态 - 将第一步标记为完成 + const updatedSteps = [...processingSteps]; + updatedSteps[0].status = "done"; + updatedSteps[0].description = "文件上传已完成"; + setProcessingSteps(updatedSteps); + + let currentStepIndex = 1; + + if (processingIntervalRef.current) { + clearInterval(processingIntervalRef.current); + } + + processingIntervalRef.current = setInterval(() => { + if (currentStepIndex >= processingSteps.length) { + if (processingIntervalRef.current) { + clearInterval(processingIntervalRef.current); + completeProcessing(); + } + return; + } + + // 更新当前步骤为活动状态 + const updatedSteps = [...processingSteps]; + updatedSteps[currentStepIndex].status = "active"; + setProcessingSteps(updatedSteps); + + // 2.5秒后标记当前步骤为完成,进入下一步骤 + setTimeout(() => { + const nextUpdatedSteps = [...processingSteps]; + nextUpdatedSteps[currentStepIndex].status = "done"; + + // 更新完成状态的描述 + switch(currentStepIndex) { + case 1: + nextUpdatedSteps[currentStepIndex].description = "转换文档格式并拆分文档完成"; + break; + case 2: + nextUpdatedSteps[currentStepIndex].description = "DeepSeek 抽取已完成"; + break; + case 3: + nextUpdatedSteps[currentStepIndex].description = "DeepSeek 评查已完成"; + break; + case 4: + nextUpdatedSteps[currentStepIndex].description = "审核准备已就绪"; + break; + } + + setProcessingSteps(nextUpdatedSteps); + currentStepIndex++; + + // 如果这是最后一个步骤,确保完成 + // if (currentStepIndex >= processingSteps.length) { + // setTimeout(() => { + // completeProcessing(); + // }, 1000); + // } + }, 2000); + + }, 2500); + }; + + // 完成处理流程 + const completeProcessing = () => { + // 设置当前状态为已完成 + setUploadStage("completed"); + + // 创建完成的文件对象 + if (currentFile) { + console.log("创建完成的文件对象..."); + const newFile: UploadedFile = { + id: `file_${Date.now()}`, + name: currentFile.name, + size: currentFile.size, + type: currentFile.type, + fileType: fileType as FileType, + priority, + status: ProcessingStatus.SUCCESS, + uploadTime: getCurrentTime() + }; + + setCompletedFile(newFile); + console.log("完成文件设置:", newFile); + + // 添加到队列中 + setQueueFiles(prev => [newFile, ...prev]); + } else { + console.log("没有当前文件"); + } + }; + + // 重置上传状态 + const resetUpload = () => { + setUploadStage("idle"); + setUploadProgress(0); + setUploadSpeed("0KB/s"); + setCurrentFile(null); + setCompletedFile(null); + + // 重置步骤状态 + const resetSteps = processingSteps.map(step => ({ + ...step, + status: "waiting" as Step["status"] + })); + resetSteps[0].description = "等待上传文件到服务器..."; + resetSteps[1].description = "转换文档格式,拆分文档内容"; + resetSteps[2].description = "DeepSeek 抽取中"; + resetSteps[3].description = "DeepSeek 评查中"; + resetSteps[4].description = "文档已准备就绪,等待审核"; + + setProcessingSteps(resetSteps); + }; + + // 获取当前时间字符串 + const getCurrentTime = () => { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const seconds = String(now.getSeconds()).padStart(2, '0'); + + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; + }; + + // 格式化文件大小显示 + const formatFileSize = (bytes: number) => { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + // 表格列定义 + const columns = [ + { + title: "文件名", + key: "name", + width: "40%", + render: (_: unknown, record: UploadedFile) => ( +
+ + {record.name} +
+ ) + }, + { + title: "文件类型", + key: "fileType", + width: "15%", + render: (_: unknown, record: UploadedFile) => { + let typeClass = ""; + let typeIcon = ""; + + switch(record.fileType) { + case FileType.CONTRACT: + typeClass = "file-type-contract"; + typeIcon = "ri-file-list-3-line"; + break; + case FileType.LICENSE: + typeClass = "file-type-license"; + typeIcon = "ri-vip-crown-line"; + break; + case FileType.PUNISHMENT: + typeClass = "file-type-punishment"; + typeIcon = "ri-scales-line"; + break; + default: + typeClass = "file-type-contract"; + typeIcon = "ri-file-list-3-line"; + } + + return ( + + + {FILE_TYPE_LABELS[record.fileType]} + + ); + } + }, + { + title: "大小", + key: "size", + width: "15%", + render: (_: unknown, record: UploadedFile) => formatFileSize(record.size) + }, + { + title: "状态", + key: "status", + width: "15%", + render: (_: unknown, record: UploadedFile) => { + let statusClass = ""; + let statusIcon = ""; + let statusText = ""; + + switch(record.status) { + case ProcessingStatus.WAITING: + statusClass = "status-waiting"; + statusIcon = "ri-time-line"; + statusText = "等待中"; + break; + case ProcessingStatus.PROCESSING: + statusClass = "status-processing"; + statusIcon = "ri-loader-4-line"; + statusText = "解析中"; + break; + case ProcessingStatus.SUCCESS: + statusClass = "status-success"; + statusIcon = "ri-checkbox-circle-line"; + statusText = "已完成"; + break; + case ProcessingStatus.ERROR: + statusClass = "status-error"; + statusIcon = "ri-error-warning-line"; + statusText = "失败"; + break; + } + + return ( + + + {statusText} + + ); + } + }, + { + title: "操作", + key: "operation", + width: "15%", + render: (_: unknown, record: UploadedFile) => ( + + ) + } + ]; + + return ( +
+ {/* 页面头部 */} +
+

待审核文件上传

+
+ + {/* 文件类型选择 */} + 选择文件类型} className="mb-4"> +
+
+ + +
不同类型的文档将应用不同的审核规则
+
+
+ + +
优先级影响文档在队列中的处理顺序
+
+
+
+ + {/* 文件上传区域 */} + 文件上传} className="mb-4"> + {/* 初始上传区域 */} + {uploadStage === "idle" && ( + + )} + + {/* 上传进度显示 */} + {uploadStage !== "completed" && currentFile && ( + + )} + + {/* 处理步骤显示 */} + {(uploadStage === "processing" || uploadStage === "completed") && ( +
+ +
+ )} + + {/* 完成后的文件信息 */} + {uploadStage === "completed" && completedFile && ( +
+
+
+ + 评查成功 +
+

文件已成功上传并评查完成,请查看结果

+
+ +
+
+

文件信息

+
    +
  • + 文件名: + {completedFile.name} +
  • +
  • + 文件大小: + {formatFileSize(completedFile.size)} +
  • +
  • + 上传时间: + {completedFile.uploadTime} +
  • +
  • + 文件类型: + {FILE_TYPE_LABELS[completedFile.fileType]} +
  • +
  • + 审核规则: + 系统自动选择 +
  • +
+
+ +
+

解析结果预览

+
+
+ 合同编号: + XS-2023-1025-001 +
+
+ 合同名称: + 烟草制品销售合同 +
+
+ 签约日期: + 2023年10月20日 +
+
+ 合同金额: + ¥ 1,580,000.00 +
+
+ 当事人: + 甲方:XX烟草公司,乙方:YY贸易有限公司 +
+
+ +
+ + +
+
+
+
+ )} +
+ + {/* 上传队列 */} + +

上传队列

+ 共 {queueFiles.length} 个文件 +
+ } + > + + + + ); +} + +export function ErrorBoundary() { + return ( +
+

出错了

+

文件上传页面加载失败。请刷新页面或联系系统管理员。

+ +
+ ); +} \ No newline at end of file diff --git a/app/routes/prompts._index.tsx b/app/routes/prompts._index.tsx index 55ac2d1..1bbebde 100644 --- a/app/routes/prompts._index.tsx +++ b/app/routes/prompts._index.tsx @@ -33,10 +33,6 @@ export const meta: MetaFunction = () => { ]; }; -// 面包屑导航 -export const handle = { - breadcrumb: "提示词模板管理" -}; // 模拟数据 const MOCK_TEMPLATES: PromptTemplate[] = [ @@ -251,7 +247,7 @@ export default function PromptsIndex() { render: (_: unknown, record: PromptTemplate) => (
- {record.template_name} + {record.template_name}
) }, @@ -336,6 +332,7 @@ export default function PromptsIndex() { title: "操作", key: "operation", width: "150px", + // align: "center", render: (_: unknown, record: PromptTemplate) => (
{record.status === 'system' ? ( @@ -427,7 +424,7 @@ export default function PromptsIndex() { name="type" value={searchParams.get('type') || ''} options={[ - { value: "", label: "全部" }, + // { value: "", label: "全部" }, { value: "Extraction", label: "抽取(Extraction)" }, { value: "Evaluation", label: "评估(Evaluation)" }, { value: "Summary", label: "摘要(Summary)" }, @@ -442,7 +439,7 @@ export default function PromptsIndex() { name="status" value={searchParams.get('status') || ''} options={[ - { value: "", label: "全部" }, + // { value: "", label: "全部" }, { value: "active", label: "启用" }, { value: "inactive", label: "停用" }, { value: "system", label: "系统预设" } diff --git a/app/routes/prompts.new.tsx b/app/routes/prompts.new.tsx index 4475cdc..d1953d4 100644 --- a/app/routes/prompts.new.tsx +++ b/app/routes/prompts.new.tsx @@ -20,9 +20,22 @@ export const meta: MetaFunction = () => { // 面包屑导航 export const handle = { - breadcrumb: "编辑提示词模板" + breadcrumb: (data:LoaderData) => { + if (data.mode === "edit") { + return "编辑提示词模板"; + } else if (data.mode === "view") { + return "查看提示词模板"; + } else { + return "新增提示词模板"; + } + } }; +interface LoaderData { + template: PromptTemplate; + mode: string; +} + // 从模拟数据中获取模板 const getTemplateById = (id: string): PromptTemplate | undefined => { // 与prompts._index.tsx中的模拟数据保持一致 @@ -87,6 +100,7 @@ const getTemplateById = (id: string): PromptTemplate | undefined => { template_name: "采购合同-乙方资质抽取", template_type: "Extraction", description: "抽取采购合同中乙方的资质信息", + version: "v1.1", status: "inactive", created_by: "zhangsan", @@ -417,10 +431,10 @@ export default function PromptsNew() {
- {!isViewMode ? ( + {!isViewMode && ( - ) : ( - - - )}
@@ -565,8 +573,8 @@ export default function PromptsNew() {
-
模板内容支持使用变量,变量格式为 {"{varName}"},在使用时会自动替换。系统将自动识别模板中的变量。
-
例如:请从以下{"{docType}"}文档中抽取关键信息...
+
模板内容支持使用变量,变量格式为 {varName},在使用时会自动替换。系统将自动识别模板中的变量。
+
例如:请从以下{docType}文档中抽取关键信息...
@@ -595,7 +603,7 @@ export default function PromptsNew() {
-
系统已自动识别出模板中的变量。变量以 {"{varName}"} 形式在模板中使用,无需手动定义。
+
系统已自动识别出模板中的变量。变量以 {varName} 形式在模板中使用,无需手动定义。
@@ -612,7 +620,7 @@ export default function PromptsNew() { )) ) : (
- 暂未识别到任何变量,请在模板内容中使用 {"{变量名}"} 格式添加变量 + 暂未识别到任何变量,请在模板内容中使用 {变量名} 格式添加变量
)} diff --git a/app/routes/prompts.tsx b/app/routes/prompts.tsx index e08d1be..aa47c82 100644 --- a/app/routes/prompts.tsx +++ b/app/routes/prompts.tsx @@ -5,7 +5,7 @@ import { Outlet } from "@remix-run/react"; * 仅作为嵌套路由的容器,不包含具体内容 */ export const handle = { - breadcrumb: "提示词模板管理" + breadcrumb: "提示词管理" } export default function Prompts() { diff --git a/app/styles/components/file-progress.css b/app/styles/components/file-progress.css new file mode 100644 index 0000000..378adbf --- /dev/null +++ b/app/styles/components/file-progress.css @@ -0,0 +1,44 @@ +/** + * 文件上传进度组件样式 + */ + +:root { + --primary-color: var(--color-primary, #00684a); + --primary-hover: var(--color-primary-hover, #005a40); + --primary-light: rgba(0, 104, 74, 0.1); + --success-color: var(--color-success, #52c41a); + --warning-color: var(--color-warning, #faad14); + --error-color: var(--color-error, #ff4d4f); + --text-color: rgba(0, 0, 0, 0.85); + --text-secondary: rgba(0, 0, 0, 0.45); + --border-color: #f0f0f0; + --bg-gray: #f5f5f5; +} + +/* 进度条样式 */ +.progress-container { + @apply my-6; +} + +.progress-bar { + @apply h-2 bg-gray-100 rounded overflow-hidden mb-2; +} + +.progress-bar-inner { + @apply h-full bg-[var(--primary-color)] rounded transition-[width] duration-300 ease-in-out w-0; +} + +.progress-text { + @apply flex justify-between text-xs text-gray-500; +} + +/* 响应式调整 */ +@screen sm { + .progress-container { + @apply my-4; + } + + .progress-bar { + @apply h-1.5; + } +} \ No newline at end of file diff --git a/app/styles/components/processing-steps.css b/app/styles/components/processing-steps.css new file mode 100644 index 0000000..d478d41 --- /dev/null +++ b/app/styles/components/processing-steps.css @@ -0,0 +1,178 @@ +/** + * 处理步骤组件样式 + */ + +:root { + --primary-color: var(--color-primary, #00684a); + --primary-hover: var(--color-primary-hover, #005a40); + --primary-light: rgba(0, 104, 74, 0.1); + --success-color: var(--color-success, #52c41a); + --warning-color: var(--color-warning, #faad14); + --error-color: var(--color-error, #ff4d4f); + --text-color: rgba(0, 0, 0, 0.85); + --text-secondary: rgba(0, 0, 0, 0.45); + --border-color: #f0f0f0; + --bg-gray: #f5f5f5; +} + +/* 横向步骤样式 */ +.steps-container-horizontal { + @apply my-8 flex justify-between relative; + padding: 0 8px; +} + +.steps-container-horizontal::before { + content: ""; + @apply absolute top-[14px] left-[30px] right-[30px] h-0.5 bg-gray-200 z-0; +} + +.step-item-horizontal { + @apply relative flex flex-col items-center flex-1 text-center z-[1]; +} + +.step-icon-horizontal { + width: 30px; + height: 30px; + @apply rounded-full bg-gray-300 flex items-center justify-center mb-2 relative z-[2]; + transition: background-color 0.3s; +} + +.step-icon-horizontal i { + @apply text-white text-base; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + display: none; +} + +.step-icon-horizontal span:not(.loading-spinner) { + color: rgba(0, 0, 0, 0.45); + font-size: 14px; + font-weight: 500; +} + +.step-item-horizontal.active .step-icon-horizontal { + @apply bg-[var(--primary-color)]; + box-shadow: 0 0 0 4px rgba(0, 104, 74, 0.1); +} + +.step-item-horizontal.active .step-icon-horizontal span:not(.loading-spinner) { + color: white; +} + +.step-item-horizontal.done .step-icon-horizontal { + @apply bg-[var(--success-color)]; +} + +.step-item-horizontal.done .step-icon-horizontal i { + display: block; +} + +.step-item-horizontal.error .step-icon-horizontal { + @apply bg-[var(--error-color)]; +} + +.step-item-horizontal.error .step-icon-horizontal i { + display: block; +} + +.step-content-horizontal { + @apply px-2 max-w-[140px]; +} + +.step-title-horizontal { + @apply font-medium mb-1 text-sm whitespace-nowrap overflow-hidden text-ellipsis; + transition: color 0.3s; +} + +.step-description-horizontal { + @apply text-xs text-gray-500 leading-tight mt-1; +} + +.step-item-horizontal.active .step-title-horizontal { + @apply text-[var(--primary-color)]; + font-weight: 500; +} + +.step-item-horizontal.done .step-title-horizontal { + @apply text-[var(--success-color)]; +} + +.step-item-horizontal.error .step-title-horizontal { + @apply text-[var(--error-color)]; +} + +/* 加载动画 */ +.loading-spinner { + position: absolute; + width: 16px; + height: 16px; + left: 50%; + top: 50%; + margin-left: -8px; /* 宽度的一半 */ + margin-top: -8px; /* 高度的一半 */ + border-radius: 50%; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: var(--primary-color); + animation-name: spinner-rotate; + animation-duration: 1s; + animation-timing-function: linear; + animation-iteration-count: infinite; +} + +@keyframes spinner-rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* 响应式调整 - 只在小屏幕上应用垂直布局 */ +@media (max-width: 640px) { + .steps-container-horizontal { + @apply flex-col items-start; + } + + .steps-container-horizontal::before { + @apply hidden; + } + + .step-item-horizontal { + @apply flex-row items-start mb-5 w-full; + } + + .step-item-horizontal:last-child { + @apply mb-0; + } + + .step-content-horizontal { + @apply text-left ml-3 max-w-none; + } + + .step-icon-horizontal { + @apply mb-0 flex-shrink-0; + } + + .step-title-horizontal { + @apply text-base; + } + + .step-description-horizontal { + @apply mt-1 text-sm; + } + + /* 垂直连接线 */ + .step-item-horizontal::before { + content: ""; + position: absolute; + left: 15px; + top: 30px; + bottom: -20px; + width: 2px; + background-color: #e8e8e8; + display: block; + } + + .step-item-horizontal:last-child::before { + display: none; + } +} \ No newline at end of file diff --git a/app/styles/components/table.css b/app/styles/components/table.css index ea0de8d..6eb1966 100644 --- a/app/styles/components/table.css +++ b/app/styles/components/table.css @@ -19,7 +19,7 @@ /* 表格内容 */ .ant-table tbody td { - @apply py-3 px-4 border-b border-gray-100; + @apply py-3 px-4 border-b border-gray-100 align-middle; } /* 表格行 */ @@ -63,6 +63,25 @@ @apply text-[#00684a]; } +/* 模板名称列垂直居中样式 */ +.ant-table .flex.items-center { + height: 1.5rem; /* h-6 */ +} + +.ant-table .flex.items-center i { + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + line-height: 1; +} + +.ant-table .flex.items-center span { + display: flex; + align-items: center; + line-height: 1.2; +} + @layer components { /* 基础表格 */ .table-container { diff --git a/app/styles/components/upload-area.css b/app/styles/components/upload-area.css new file mode 100644 index 0000000..1b32351 --- /dev/null +++ b/app/styles/components/upload-area.css @@ -0,0 +1,57 @@ +/** + * 文件上传区域组件样式 + */ + +:root { + --primary-color: var(--color-primary, #00684a); + --primary-hover: var(--color-primary-hover, #005a40); + --primary-light: rgba(0, 104, 74, 0.1); + --success-color: var(--color-success, #52c41a); + --warning-color: var(--color-warning, #faad14); + --error-color: var(--color-error, #ff4d4f); + --text-color: rgba(0, 0, 0, 0.85); + --text-secondary: rgba(0, 0, 0, 0.45); + --border-color: #f0f0f0; + --bg-gray: #f5f5f5; +} + +/* 上传区域样式 */ +.upload-area { + @apply border-2 border-dashed border-gray-300 rounded-lg p-10 text-center bg-gray-50 cursor-pointer transition-all duration-300; +} + +.upload-area:hover, +.upload-area.dragover { + @apply border-[var(--primary-color)] bg-[var(--primary-light)]; +} + +.upload-area.disabled { + @apply opacity-50 cursor-not-allowed; +} + +.upload-icon { + @apply text-5xl text-gray-400 mb-4; +} + +.upload-text { + @apply text-gray-500 mb-2 font-medium; +} + +.upload-tip { + @apply text-xs text-gray-500; +} + +/* 响应式调整 */ +@screen sm { + .upload-area { + @apply p-6; + } + + .upload-icon { + @apply text-4xl mb-2; + } + + .upload-text { + @apply text-sm; + } +} \ No newline at end of file diff --git a/app/styles/main.css b/app/styles/main.css index 7dc598e..35f68c6 100644 --- a/app/styles/main.css +++ b/app/styles/main.css @@ -18,6 +18,9 @@ @import './components/file-type-tag.css'; @import './components/status-dot.css'; @import './components/tag.css'; +@import './components/file-progress.css'; +@import './components/processing-steps.css'; +@import './components/upload-area.css'; /* @import './components/modal.css'; */ /* Tailwind 基础指令 */ @@ -83,13 +86,17 @@ a { @apply text-[#00684a] hover:text-[#005a3f] transition-colors duration-200; } + } /* 组件相关样式 */ @layer components { /* 文本颜色工具类 */ .text-primary { - @apply text-[#00684a]; + @apply !text-[--color-primary]; + } + .text-error { + @apply !text-[--color-error]; } .bg-primary { diff --git a/app/styles/pages/files_upload.css b/app/styles/pages/files_upload.css new file mode 100644 index 0000000..c685715 --- /dev/null +++ b/app/styles/pages/files_upload.css @@ -0,0 +1,225 @@ +/** + * 文件上传页面样式 + */ + +.file-upload-page { + --primary-color: var(--color-primary, #00684a); + --primary-hover: var(--color-primary-hover, #005a40); + --primary-light: rgba(0, 104, 74, 0.1); + --success-color: var(--color-success, #52c41a); + --warning-color: var(--color-warning, #faad14); + --error-color: var(--color-error, #ff4d4f); + --text-color: rgba(0, 0, 0, 0.85); + --text-secondary: rgba(0, 0, 0, 0.45); + --border-color: #f0f0f0; + --bg-gray: #f5f5f5; +} + +/* 页面头部 */ +.file-upload-page .page-header { + @apply flex justify-between items-center mb-4; +} + +.file-upload-page .page-title { + @apply text-xl font-medium; +} + +/* 表单样式 */ +.file-upload-page .form-group { + @apply mb-4; +} + +.file-upload-page .form-label { + @apply block text-sm font-medium text-gray-700 mb-2; +} + +.file-upload-page .form-tip { + @apply text-xs text-gray-500 mt-1; +} + +.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; +} + +.file-upload-page .form-select:focus { + @apply border-[#00684a] shadow-[0_0_0_2px_rgba(0,104,74,0.2)] outline-none; +} + +/* 上传区域样式 */ +.file-upload-page .upload-area { + @apply border-2 border-dashed border-gray-300 rounded-lg p-10 text-center bg-gray-50 cursor-pointer transition-all duration-300; +} + +.file-upload-page .upload-area:hover, +.file-upload-page .upload-area.dragover { + @apply border-[var(--primary-color)] bg-[var(--primary-light)]; +} + +.file-upload-page .upload-icon { + @apply text-5xl text-gray-400 mb-4; +} + +.file-upload-page .upload-text { + @apply text-gray-500 mb-2 font-medium; +} + +.file-upload-page .upload-tip { + @apply text-xs text-gray-500; +} + +/* 进度条样式 */ +.file-upload-page .progress-container { + @apply my-6; +} + +.file-upload-page .progress-bar { + @apply h-2 bg-gray-100 rounded overflow-hidden mb-2; +} + +.file-upload-page .progress-bar-inner { + @apply h-full bg-[var(--primary-color)] rounded transition-[width] duration-300 ease-in-out w-0; +} + +.file-upload-page .progress-text { + @apply flex justify-between text-xs text-gray-500; +} + +/* 横向步骤样式 */ +.file-upload-page .steps-container-horizontal { + @apply my-8 flex justify-between relative; +} + +.file-upload-page .steps-container-horizontal::before { + content: ""; + @apply absolute top-[14px] left-0 right-0 h-0.5 bg-gray-200 z-0; +} + +.file-upload-page .step-item-horizontal { + @apply relative flex flex-col items-center flex-1 text-center z-[1]; +} + +.file-upload-page .step-icon-horizontal { + @apply w-[30px] h-[30px] rounded-full bg-gray-300 flex items-center justify-center mb-2 relative z-[2]; +} + +.file-upload-page .step-icon-horizontal i { + @apply hidden text-white text-base absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 m-0 p-0; +} + +.file-upload-page .step-item-horizontal.active .step-icon-horizontal { + @apply bg-[var(--primary-color)]; +} + +.file-upload-page .step-item-horizontal.done .step-icon-horizontal { + @apply bg-[var(--success-color)]; +} + +.file-upload-page .step-item-horizontal.done .step-icon-horizontal i { + @apply inline-block; +} + +.file-upload-page .step-item-horizontal.error .step-icon-horizontal { + @apply bg-[var(--error-color)]; +} + +.file-upload-page .step-content-horizontal { + @apply px-2 max-w-[140px]; +} + +.file-upload-page .step-title-horizontal { + @apply font-medium mb-1 text-sm whitespace-nowrap overflow-hidden text-ellipsis; +} + +.file-upload-page .step-description-horizontal { + @apply text-xs text-gray-500 leading-tight mt-1; +} + +.file-upload-page .step-item-horizontal.active .step-title-horizontal { + @apply text-[var(--primary-color)]; +} + +/* 文件信息样式 */ +.file-upload-page .file-info-list { + @apply list-none p-0 m-0; +} + +.file-upload-page .file-info-item { + @apply flex mb-2; +} + +.file-upload-page .file-info-label { + @apply flex-none w-20 text-gray-500; +} + +.file-upload-page .file-info-value { + @apply flex-1; +} + +/* 文件类型徽章 */ +.file-upload-page .file-type-badge { + @apply inline-flex items-center px-2 py-0.5 rounded text-xs font-medium; +} + +.file-upload-page .file-type-badge i { + @apply mr-1 text-sm; +} + +.file-upload-page .file-type-contract { + @apply bg-blue-100 text-blue-800; +} + +.file-upload-page .file-type-license { + @apply bg-green-100 text-green-800; +} + +.file-upload-page .file-type-punishment { + @apply bg-yellow-100 text-yellow-800; +} + +/* 状态徽章 */ +.file-upload-page .status-badge { + @apply inline-flex items-center px-2 py-0.5 rounded text-xs font-medium; +} + +.file-upload-page .status-waiting { + @apply bg-purple-100 text-purple-800; +} + +.file-upload-page .status-processing { + @apply bg-blue-100 text-blue-800; +} + +.file-upload-page .status-success { + @apply bg-green-100 text-green-800; +} + +.file-upload-page .status-error { + @apply bg-red-100 text-red-800; +} + +/* 动画效果 */ +@keyframes pulse { + 0% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.05); + opacity: 0.8; + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +.file-upload-page .pulse-animation { + animation: pulse 1.5s ease-in-out infinite; +} + +/* 响应式调整 */ +@screen md { + .file-upload-page .file-info-grid { + @apply grid grid-cols-2 gap-6; + } +} diff --git a/app/styles/pages/prompts_index.css b/app/styles/pages/prompts_index.css index a93f171..1b79ce5 100644 --- a/app/styles/pages/prompts_index.css +++ b/app/styles/pages/prompts_index.css @@ -42,6 +42,11 @@ @apply overflow-x-auto; } +/* 选择框focus状态 */ +.prompt-page .form-select:focus { + @apply border-[#00684a] shadow-[0_0_0_2px_rgba(0,104,74,0.2)] outline-none; +} + /* 类型标签 */ .prompt-page .type-badge { @apply inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium mr-1; @@ -65,7 +70,7 @@ /* 状态标签 */ .prompt-page .status-badge { - @apply inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium; + @apply inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium text-center; } .prompt-page .status-active { @@ -82,7 +87,7 @@ /* 操作按钮 */ .prompt-page .operation-btn { - @apply inline-flex items-center px-2 py-1 text-sm rounded-md hover:bg-gray-100 transition-colors duration-150 ease-in-out; + @apply !text-black inline-flex items-center px-3 py-1 text-sm rounded-md hover:bg-gray-100 transition-colors duration-150 ease-in-out hover:!text-[--color-primary]; } .prompt-page .operation-btn i { diff --git a/html/文件-上传.html b/html/文件-上传.html index 1e6fa47..f040ac8 100644 --- a/html/文件-上传.html +++ b/html/文件-上传.html @@ -4,8 +4,10 @@ 中国烟草AI合同及卷宗审核系统 - 待审核文件上传 - - + + +