新增文件上传页面,新增文件上传的公共组件(进度条,步骤条,上传区域)
This commit is contained in:
@@ -34,7 +34,7 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
|
|||||||
{
|
{
|
||||||
id: 'file-upload',
|
id: 'file-upload',
|
||||||
title: '文件上传',
|
title: '文件上传',
|
||||||
path: '/files/new',
|
path: '/files/upload',
|
||||||
icon: 'ri-upload-cloud-line'
|
icon: 'ri-upload-cloud-line'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className={`progress-container ${className}`}>
|
||||||
|
<div className="mb-2 flex justify-between items-center">
|
||||||
|
<span className="font-medium">{fileName}</span>
|
||||||
|
{fileSize && <span className="text-secondary text-sm">{fileSize}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="progress-bar">
|
||||||
|
<div
|
||||||
|
className="progress-bar-inner"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow={progress}
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={100}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div className="progress-text">
|
||||||
|
<span>{progress}%</span>
|
||||||
|
<span>{speed}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className={`steps-container-horizontal ${className}`}>
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`step-item-horizontal ${step.status}`}
|
||||||
|
data-step={index + 1}
|
||||||
|
>
|
||||||
|
<div className="step-icon-horizontal">
|
||||||
|
{step.status === 'done' && <i className="ri-check-line"></i>}
|
||||||
|
{step.status === 'error' && <i className="ri-close-line"></i>}
|
||||||
|
{step.status === 'active' && <span className="loading-spinner"></span>}
|
||||||
|
{step.status === 'waiting' && <span>{index + 1}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="step-content-horizontal">
|
||||||
|
<div className="step-title-horizontal">{step.title}</div>
|
||||||
|
<div className="step-description-horizontal">{step.description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<UploadAreaRef, UploadAreaProps>(({
|
||||||
|
onFilesSelected,
|
||||||
|
className = "",
|
||||||
|
accept = "",
|
||||||
|
multiple = false,
|
||||||
|
icon = "ri-upload-cloud-2-line",
|
||||||
|
buttonText = "选择文件",
|
||||||
|
mainText = "点击或拖拽文件到此区域上传",
|
||||||
|
tipText = "",
|
||||||
|
disabled = false
|
||||||
|
}, ref) => {
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(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<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!disabled) {
|
||||||
|
setIsDragOver(true);
|
||||||
|
}
|
||||||
|
}, [disabled]);
|
||||||
|
|
||||||
|
const handleDragLeave = useCallback(() => {
|
||||||
|
setIsDragOver(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragOver(false);
|
||||||
|
|
||||||
|
if (!disabled && e.dataTransfer.files.length > 0) {
|
||||||
|
onFilesSelected(e.dataTransfer.files);
|
||||||
|
}
|
||||||
|
}, [disabled, onFilesSelected]);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if ((e.key === 'Enter' || e.key === ' ') && !disabled) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleClick();
|
||||||
|
}
|
||||||
|
}, [handleClick, disabled]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`upload-area ${isDragOver ? 'dragover' : ''} ${disabled ? 'disabled' : ''} ${className}`}
|
||||||
|
onClick={handleClick}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
role="button"
|
||||||
|
tabIndex={disabled ? -1 : 0}
|
||||||
|
aria-label="上传文件区域"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept={accept}
|
||||||
|
multiple={multiple}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<i className={`${icon} upload-icon`} aria-hidden="true"></i>
|
||||||
|
<div className="upload-text">{mainText}</div>
|
||||||
|
{tipText && <p className="upload-tip mb-2">{tipText}</p>}
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon="ri-file-upload-line"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
UploadArea.displayName = "UploadArea";
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { Outlet } from "@remix-run/react";
|
||||||
|
|
||||||
|
export default function Files() {
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
@@ -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, string> = {
|
||||||
|
[FileType.CONTRACT]: "合同文档",
|
||||||
|
[FileType.LICENSE]: "专卖许可证",
|
||||||
|
[FileType.PUNISHMENT]: "行政处罚决定书",
|
||||||
|
[FileType.OTHER]: "其他文档"
|
||||||
|
};
|
||||||
|
|
||||||
|
// 优先级定义
|
||||||
|
export enum Priority {
|
||||||
|
NORMAL = "normal",
|
||||||
|
HIGH = "high",
|
||||||
|
URGENT = "urgent"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先级标签映射
|
||||||
|
export const PRIORITY_LABELS: Record<Priority, string> = {
|
||||||
|
[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<FileType | "">("");
|
||||||
|
const [priority, setPriority] = useState<Priority>(Priority.NORMAL);
|
||||||
|
const [currentFile, setCurrentFile] = useState<File | null>(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<Step[]>([
|
||||||
|
{ 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<UploadedFile[]>(MOCK_QUEUE_FILES);
|
||||||
|
|
||||||
|
// 上传完成后的文件信息
|
||||||
|
const [completedFile, setCompletedFile] = useState<UploadedFile | null>(null);
|
||||||
|
|
||||||
|
// 计时器引用
|
||||||
|
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const processingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
// UploadArea组件引用
|
||||||
|
const uploadAreaRef = useRef<UploadAreaRef>(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) => (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<i className={`${record.type.includes('pdf') ? 'ri-file-pdf-line text-red-500' : 'ri-file-word-2-line text-blue-500'} mr-2 text-lg`}></i>
|
||||||
|
<span className="truncate">{record.name}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 (
|
||||||
|
<span className={`file-type-badge ${typeClass}`}>
|
||||||
|
<i className={typeIcon}></i>
|
||||||
|
{FILE_TYPE_LABELS[record.fileType]}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 (
|
||||||
|
<span className={`status-badge ${statusClass}`}>
|
||||||
|
<i className={`${statusIcon} mr-1`}></i>
|
||||||
|
{statusText}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "operation",
|
||||||
|
width: "15%",
|
||||||
|
render: (_: unknown, record: UploadedFile) => (
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
size="small"
|
||||||
|
disabled={record.status !== ProcessingStatus.SUCCESS}
|
||||||
|
icon="ri-eye-line"
|
||||||
|
onClick={() => alert(`查看文件详情: ${record.name}`)}
|
||||||
|
>
|
||||||
|
查看
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="file-upload-page">
|
||||||
|
{/* 页面头部 */}
|
||||||
|
<div className="page-header">
|
||||||
|
<h2 className="page-title">待审核文件上传</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 文件类型选择 */}
|
||||||
|
<Card title={<h3>选择文件类型</h3>} className="mb-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="file-type-select" className="form-label">文件类型 <span className="text-red-500">*</span></label>
|
||||||
|
<select
|
||||||
|
id="file-type-select"
|
||||||
|
className="form-select"
|
||||||
|
value={fileType}
|
||||||
|
onChange={(e) => setFileType(e.target.value as FileType)}
|
||||||
|
disabled={uploadStage !== "idle"}
|
||||||
|
>
|
||||||
|
<option value="">请选择文件类型</option>
|
||||||
|
<option value={FileType.CONTRACT}>合同文档</option>
|
||||||
|
<option value={FileType.LICENSE}>专卖许可证</option>
|
||||||
|
<option value={FileType.PUNISHMENT}>行政处罚决定书</option>
|
||||||
|
<option value={FileType.OTHER}>其他文档</option>
|
||||||
|
</select>
|
||||||
|
<div className="form-tip">不同类型的文档将应用不同的审核规则</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="priority-select" className="form-label">审核优先级</label>
|
||||||
|
<select
|
||||||
|
id="priority-select"
|
||||||
|
className="form-select"
|
||||||
|
value={priority}
|
||||||
|
onChange={(e) => setPriority(e.target.value as Priority)}
|
||||||
|
disabled={uploadStage !== "idle"}
|
||||||
|
>
|
||||||
|
<option value={Priority.NORMAL}>普通</option>
|
||||||
|
<option value={Priority.HIGH}>优先</option>
|
||||||
|
<option value={Priority.URGENT}>紧急</option>
|
||||||
|
</select>
|
||||||
|
<div className="form-tip">优先级影响文档在队列中的处理顺序</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 文件上传区域 */}
|
||||||
|
<Card title={<h3>文件上传</h3>} className="mb-4">
|
||||||
|
{/* 初始上传区域 */}
|
||||||
|
{uploadStage === "idle" && (
|
||||||
|
<UploadArea
|
||||||
|
ref={uploadAreaRef}
|
||||||
|
onFilesSelected={handleFilesSelected}
|
||||||
|
multiple={false}
|
||||||
|
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png"
|
||||||
|
tipText="支持单个或批量上传,文件格式:PDF、Word、Excel、图片"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 上传进度显示 */}
|
||||||
|
{uploadStage !== "completed" && currentFile && (
|
||||||
|
<FileProgress
|
||||||
|
fileName={currentFile.name}
|
||||||
|
fileSize={formatFileSize(currentFile.size)}
|
||||||
|
progress={uploadProgress}
|
||||||
|
speed={uploadSpeed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 处理步骤显示 */}
|
||||||
|
{(uploadStage === "processing" || uploadStage === "completed") && (
|
||||||
|
<div className="mt-4 mb-4">
|
||||||
|
<ProcessingSteps steps={processingSteps} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 完成后的文件信息 */}
|
||||||
|
{uploadStage === "completed" && completedFile && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="bg-green-50 p-4 rounded-md mb-4 border border-green-100">
|
||||||
|
<div className="flex items-center text-green-800 mb-2">
|
||||||
|
<i className="ri-checkbox-circle-line text-xl mr-2"></i>
|
||||||
|
<span className="font-medium">评查成功</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-green-700">文件已成功上传并评查完成,请查看结果</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="file-info-grid">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-3">文件信息</h4>
|
||||||
|
<ul className="file-info-list">
|
||||||
|
<li className="file-info-item">
|
||||||
|
<span className="file-info-label">文件名:</span>
|
||||||
|
<span className="file-info-value">{completedFile.name}</span>
|
||||||
|
</li>
|
||||||
|
<li className="file-info-item">
|
||||||
|
<span className="file-info-label">文件大小:</span>
|
||||||
|
<span className="file-info-value">{formatFileSize(completedFile.size)}</span>
|
||||||
|
</li>
|
||||||
|
<li className="file-info-item">
|
||||||
|
<span className="file-info-label">上传时间:</span>
|
||||||
|
<span className="file-info-value">{completedFile.uploadTime}</span>
|
||||||
|
</li>
|
||||||
|
<li className="file-info-item">
|
||||||
|
<span className="file-info-label">文件类型:</span>
|
||||||
|
<span className="file-info-value">{FILE_TYPE_LABELS[completedFile.fileType]}</span>
|
||||||
|
</li>
|
||||||
|
<li className="file-info-item">
|
||||||
|
<span className="file-info-label">审核规则:</span>
|
||||||
|
<span className="file-info-value">系统自动选择</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-3">解析结果预览</h4>
|
||||||
|
<div className="bg-gray-50 p-3 rounded-md border border-gray-200">
|
||||||
|
<div className="mb-2">
|
||||||
|
<span className="text-gray-500 text-sm">合同编号:</span>
|
||||||
|
<span className="pulse-animation">XS-2023-1025-001</span>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<span className="text-gray-500 text-sm">合同名称:</span>
|
||||||
|
<span className="pulse-animation">烟草制品销售合同</span>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<span className="text-gray-500 text-sm">签约日期:</span>
|
||||||
|
<span className="pulse-animation">2023年10月20日</span>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<span className="text-gray-500 text-sm">合同金额:</span>
|
||||||
|
<span className="pulse-animation">¥ 1,580,000.00</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500 text-sm">当事人:</span>
|
||||||
|
<span className="pulse-animation">甲方:XX烟草公司,乙方:YY贸易有限公司</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex justify-between">
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
icon="ri-refresh-line"
|
||||||
|
onClick={resetUpload}
|
||||||
|
>
|
||||||
|
上传新文件
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon="ri-file-search-line"
|
||||||
|
>
|
||||||
|
查看详情并审核
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 上传队列 */}
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3>上传队列</h3>
|
||||||
|
<span className="text-gray-500 text-sm">共 {queueFiles.length} 个文件</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={queueFiles}
|
||||||
|
rowKey="id"
|
||||||
|
emptyText="暂无上传文件"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorBoundary() {
|
||||||
|
return (
|
||||||
|
<div className="error-container p-6">
|
||||||
|
<h1 className="text-xl font-bold text-red-500 mb-4">出错了</h1>
|
||||||
|
<p className="mb-4">文件上传页面加载失败。请刷新页面或联系系统管理员。</p>
|
||||||
|
<Button type="primary" to="/">返回首页</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -33,10 +33,6 @@ export const meta: MetaFunction = () => {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 面包屑导航
|
|
||||||
export const handle = {
|
|
||||||
breadcrumb: "提示词模板管理"
|
|
||||||
};
|
|
||||||
|
|
||||||
// 模拟数据
|
// 模拟数据
|
||||||
const MOCK_TEMPLATES: PromptTemplate[] = [
|
const MOCK_TEMPLATES: PromptTemplate[] = [
|
||||||
@@ -251,7 +247,7 @@ export default function PromptsIndex() {
|
|||||||
render: (_: unknown, record: PromptTemplate) => (
|
render: (_: unknown, record: PromptTemplate) => (
|
||||||
<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>{record.template_name}</span>
|
<span className="truncate">{record.template_name}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -336,6 +332,7 @@ export default function PromptsIndex() {
|
|||||||
title: "操作",
|
title: "操作",
|
||||||
key: "operation",
|
key: "operation",
|
||||||
width: "150px",
|
width: "150px",
|
||||||
|
// align: "center",
|
||||||
render: (_: unknown, record: PromptTemplate) => (
|
render: (_: unknown, record: PromptTemplate) => (
|
||||||
<div>
|
<div>
|
||||||
{record.status === 'system' ? (
|
{record.status === 'system' ? (
|
||||||
@@ -427,7 +424,7 @@ export default function PromptsIndex() {
|
|||||||
name="type"
|
name="type"
|
||||||
value={searchParams.get('type') || ''}
|
value={searchParams.get('type') || ''}
|
||||||
options={[
|
options={[
|
||||||
{ value: "", label: "全部" },
|
// { 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)" },
|
||||||
@@ -442,7 +439,7 @@ export default function PromptsIndex() {
|
|||||||
name="status"
|
name="status"
|
||||||
value={searchParams.get('status') || ''}
|
value={searchParams.get('status') || ''}
|
||||||
options={[
|
options={[
|
||||||
{ value: "", label: "全部" },
|
// { value: "", label: "全部" },
|
||||||
{ value: "active", label: "启用" },
|
{ value: "active", label: "启用" },
|
||||||
{ value: "inactive", label: "停用" },
|
{ value: "inactive", label: "停用" },
|
||||||
{ value: "system", label: "系统预设" }
|
{ value: "system", label: "系统预设" }
|
||||||
|
|||||||
+21
-13
@@ -20,9 +20,22 @@ export const meta: MetaFunction = () => {
|
|||||||
|
|
||||||
// 面包屑导航
|
// 面包屑导航
|
||||||
export const handle = {
|
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 => {
|
const getTemplateById = (id: string): PromptTemplate | undefined => {
|
||||||
// 与prompts._index.tsx中的模拟数据保持一致
|
// 与prompts._index.tsx中的模拟数据保持一致
|
||||||
@@ -87,6 +100,7 @@ const getTemplateById = (id: string): PromptTemplate | undefined => {
|
|||||||
template_name: "采购合同-乙方资质抽取",
|
template_name: "采购合同-乙方资质抽取",
|
||||||
template_type: "Extraction",
|
template_type: "Extraction",
|
||||||
description: "抽取采购合同中乙方的资质信息",
|
description: "抽取采购合同中乙方的资质信息",
|
||||||
|
|
||||||
version: "v1.1",
|
version: "v1.1",
|
||||||
status: "inactive",
|
status: "inactive",
|
||||||
created_by: "zhangsan",
|
created_by: "zhangsan",
|
||||||
@@ -417,10 +431,10 @@ export default function PromptsNew() {
|
|||||||
<div>
|
<div>
|
||||||
<Link to="/prompts" className="mr-2">
|
<Link to="/prompts" className="mr-2">
|
||||||
<Button type="default" icon="ri-arrow-left-line">
|
<Button type="default" icon="ri-arrow-left-line">
|
||||||
返回
|
返回列表
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{!isViewMode ? (
|
{!isViewMode && (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon="ri-save-line"
|
icon="ri-save-line"
|
||||||
@@ -429,12 +443,6 @@ export default function PromptsNew() {
|
|||||||
>
|
>
|
||||||
{isSubmitting ? "保存中..." : "保存"}
|
{isSubmitting ? "保存中..." : "保存"}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
|
||||||
<Link to="/prompts">
|
|
||||||
<Button type="default" icon="ri-arrow-left-line">
|
|
||||||
返回列表
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -565,8 +573,8 @@ export default function PromptsNew() {
|
|||||||
<div className="alert alert-warning mb-4">
|
<div className="alert alert-warning mb-4">
|
||||||
<i className="ri-information-line"></i>
|
<i className="ri-information-line"></i>
|
||||||
<div>
|
<div>
|
||||||
<div>模板内容支持使用变量,变量格式为 <code>{"{varName}"}</code>,在使用时会自动替换。系统将自动识别模板中的变量。</div>
|
<div>模板内容支持使用变量,变量格式为 {varName},在使用时会自动替换。系统将自动识别模板中的变量。</div>
|
||||||
<div className="mt-1">例如:<code>请从以下{"{docType}"}文档中抽取关键信息...</code></div>
|
<div className="mt-1">例如:请从以下{docType}文档中抽取关键信息...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -595,7 +603,7 @@ export default function PromptsNew() {
|
|||||||
<div className="alert alert-info mb-3">
|
<div className="alert alert-info mb-3">
|
||||||
<i className="ri-lightbulb-line"></i>
|
<i className="ri-lightbulb-line"></i>
|
||||||
<div>
|
<div>
|
||||||
<div>系统已自动识别出模板中的变量。变量以 <code>{"{varName}"}</code> 形式在模板中使用,无需手动定义。</div>
|
<div>系统已自动识别出模板中的变量。变量以 {varName} 形式在模板中使用,无需手动定义。</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -612,7 +620,7 @@ export default function PromptsNew() {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="text-secondary text-sm italic" id="no-vars-message">
|
<div className="text-secondary text-sm italic" id="no-vars-message">
|
||||||
暂未识别到任何变量,请在模板内容中使用 {"{变量名}"} 格式添加变量
|
暂未识别到任何变量,请在模板内容中使用 {变量名} 格式添加变量
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Outlet } from "@remix-run/react";
|
|||||||
* 仅作为嵌套路由的容器,不包含具体内容
|
* 仅作为嵌套路由的容器,不包含具体内容
|
||||||
*/
|
*/
|
||||||
export const handle = {
|
export const handle = {
|
||||||
breadcrumb: "提示词模板管理"
|
breadcrumb: "提示词管理"
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Prompts() {
|
export default function Prompts() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
/* 表格内容 */
|
/* 表格内容 */
|
||||||
.ant-table tbody td {
|
.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];
|
@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 {
|
@layer components {
|
||||||
/* 基础表格 */
|
/* 基础表格 */
|
||||||
.table-container {
|
.table-container {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-1
@@ -18,6 +18,9 @@
|
|||||||
@import './components/file-type-tag.css';
|
@import './components/file-type-tag.css';
|
||||||
@import './components/status-dot.css';
|
@import './components/status-dot.css';
|
||||||
@import './components/tag.css';
|
@import './components/tag.css';
|
||||||
|
@import './components/file-progress.css';
|
||||||
|
@import './components/processing-steps.css';
|
||||||
|
@import './components/upload-area.css';
|
||||||
/* @import './components/modal.css'; */
|
/* @import './components/modal.css'; */
|
||||||
|
|
||||||
/* Tailwind 基础指令 */
|
/* Tailwind 基础指令 */
|
||||||
@@ -83,13 +86,17 @@
|
|||||||
a {
|
a {
|
||||||
@apply text-[#00684a] hover:text-[#005a3f] transition-colors duration-200;
|
@apply text-[#00684a] hover:text-[#005a3f] transition-colors duration-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 组件相关样式 */
|
/* 组件相关样式 */
|
||||||
@layer components {
|
@layer components {
|
||||||
/* 文本颜色工具类 */
|
/* 文本颜色工具类 */
|
||||||
.text-primary {
|
.text-primary {
|
||||||
@apply text-[#00684a];
|
@apply !text-[--color-primary];
|
||||||
|
}
|
||||||
|
.text-error {
|
||||||
|
@apply !text-[--color-error];
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-primary {
|
.bg-primary {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,11 @@
|
|||||||
@apply overflow-x-auto;
|
@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 {
|
.prompt-page .type-badge {
|
||||||
@apply inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium mr-1;
|
@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 {
|
.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 {
|
.prompt-page .status-active {
|
||||||
@@ -82,7 +87,7 @@
|
|||||||
|
|
||||||
/* 操作按钮 */
|
/* 操作按钮 */
|
||||||
.prompt-page .operation-btn {
|
.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 {
|
.prompt-page .operation-btn i {
|
||||||
|
|||||||
+4
-2
@@ -4,8 +4,10 @@
|
|||||||
<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"> -->
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary-color: #00684a;
|
--primary-color: #00684a;
|
||||||
|
|||||||
+5
-3
@@ -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>
|
||||||
.status-badge {
|
.status-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
+2
-1
@@ -40,5 +40,6 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [
|
||||||
|
],
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
|||||||
Reference in New Issue
Block a user