新增全局加载进度条组件

This commit is contained in:
2025-04-24 20:51:10 +08:00
parent 65b7d0739a
commit 0eaaa5b041
6 changed files with 340 additions and 144 deletions
+23 -3
View File
@@ -1,5 +1,6 @@
import { useNavigate } from "@remix-run/react";
import { toastService } from "~/components/ui/Toast";
import { useState } from "react";
interface FileInfoProps {
fileInfo: {
fileName: string;
@@ -11,12 +12,15 @@ interface FileInfoProps {
uploadUser?: string;
auditStatus?: number;
path?: string;
previousRoute?: string;
};
onConfirmResults: () => void;
}
export function FileInfo({ fileInfo, onConfirmResults }: FileInfoProps) {
const navigate = useNavigate();
const [isNavigating, setIsNavigating] = useState(false);
const handleDownloadFile = async () => {
try {
const urlBefore = 'http://172.18.0.100:9000/docauditai/';
@@ -56,7 +60,22 @@ export function FileInfo({ fileInfo, onConfirmResults }: FileInfoProps) {
};
const handleBack = () => {
navigate(-1);
// 防抖处理 - 如果已经在导航中,不重复触发
if (isNavigating) return;
// 设置导航状态为true
setIsNavigating(true);
// 根据来源页面返回
const previousRoute = fileInfo.previousRoute || 'documents';
const returnTo = previousRoute === 'documents'
? "/documents"
: previousRoute === 'filesUpload'
? "/files/upload"
: "/rules-files";
// 立即导航返回
navigate(returnTo);
};
const handleExportReport = () => {
@@ -89,8 +108,9 @@ export function FileInfo({ fileInfo, onConfirmResults }: FileInfoProps) {
<button
className="ant-btn ant-btn-default flex items-center"
onClick={() => handleBack()}
disabled={isNavigating}
>
<i className="ri-arrow-left-line mr-1"></i>
<i className="ri-arrow-left-line mr-1"></i> {isNavigating ? '返回中...' : '返回'}
</button>
<button
className="ant-btn ant-btn-default flex items-center"
+9 -3
View File
@@ -1024,9 +1024,15 @@ export function ReviewPointsList({
<div className="review-point-header flex justify-between items-start">
<div className="review-point-title flex-1 text-left min-w-[25%]">{reviewPoint.title}</div>
{/* 评查点所属分组 */}
<div className="review-point-location max-w-[40%]">
<i className="ri-file-list-line mr-1"></i>
<span>{reviewPoint.groupName}</span>
<div className="review-point-location max-w-[40%] flex items-center">
<i className="ri-file-list-line mr-1 flex-shrink-0"></i>
<span
className="truncate block whitespace-nowrap overflow-hidden hover:overflow-visible hover:text-clip hover:bg-white hover:shadow-md hover:z-10 hover:text-wrap px-1 rounded"
title={reviewPoint.groupName}
style={{ cursor: 'text', userSelect: 'all' }}
>
{reviewPoint.groupName}
</span>
</div>
<div className="flex flex-col items-center ml-2 flex-shrink-0 max-w-[15%]">
{renderStatusBadge(reviewPoint.status, reviewPoint.result)}
+131
View File
@@ -0,0 +1,131 @@
import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
interface LoadingBarProps {
isVisible: boolean;
isComplete?: boolean;
height?: number;
className?: string;
}
// 加载进度条状态管理
let showLoadingBar: (show: boolean, completeFirst?: boolean) => void = () => {};
let loadingBarVisible = false;
export function LoadingBar({
isVisible,
isComplete = false,
height = 2,
className = ''
}: LoadingBarProps) {
// 当组件挂载时,进行进度的随机增长动画
const [progress, setProgress] = useState(0);
// 处理完成状态
useEffect(() => {
if (isComplete) {
// 立即将进度设为100%
setProgress(100);
}
}, [isComplete]);
useEffect(() => {
let interval: NodeJS.Timeout;
if (isVisible && progress < 90) {
// 随机增长进度,模拟加载 - 加速更新频率
interval = setInterval(() => {
setProgress(prev => {
// 进度越大,增长越慢
const increment = Math.random() * (20 - prev / 8);
return Math.min(prev + increment, 90);
});
}, 300); // 更高频率的更新
} else if (!isVisible) {
// 隐藏时重置进度
setProgress(0);
}
return () => {
if (interval) clearInterval(interval);
};
}, [isVisible, progress]);
// 当isVisible变化时,立即触发显示或隐藏效果
useEffect(() => {
if (isVisible) {
setProgress(20); // 立刻显示更多进度
} else if (progress > 0) {
// 如果已经有进度了,先快速完成进度,然后再隐藏
setProgress(100);
const timeout = setTimeout(() => {
setProgress(0);
}, 300); // 更快的隐藏
return () => clearTimeout(timeout);
}
}, [isVisible]);
if (!isVisible && progress === 0) return null;
return (
<div className="fixed top-0 left-0 right-0 z-[9999]">
<div
className={`${className} animate-pulse`}
style={{
height: `${height}px`,
width: `${progress}%`,
opacity: progress > 0 ? 1 : 0,
transition: 'width 0.3s ease-out, opacity 0.2s ease-in-out',
background: 'linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-hover) 50%, var(--color-primary) 100%)',
boxShadow: '0 0 10px rgba(0, 104, 74, 0.7)'
}}
/>
</div>
);
}
// 创建一个全局加载进度条容器
function LoadingBarContainer() {
const [visible, setVisible] = useState(loadingBarVisible);
const [completeBeforeHide, setCompleteBeforeHide] = useState(false);
// 保存引用以便外部调用
useEffect(() => {
showLoadingBar = (show: boolean, completeFirst: boolean = false) => {
if (!show && completeFirst) {
// 当隐藏时,先触发完成动画,再隐藏
setCompleteBeforeHide(true);
// 延迟隐藏,以让完成动画显示出来
setTimeout(() => {
loadingBarVisible = false;
setVisible(false);
setCompleteBeforeHide(false);
}, 500);
} else {
loadingBarVisible = show;
setVisible(show);
if (!show) {
setCompleteBeforeHide(false);
}
}
};
}, []);
// 使用Portal确保加载进度条总是渲染在body末尾
return typeof window !== 'undefined'
? createPortal(
<LoadingBar isVisible={visible} isComplete={completeBeforeHide} />,
document.body
)
: null;
}
// 导出全局控制函数
export const loadingBarService = {
show: () => showLoadingBar(true),
hide: () => showLoadingBar(false, true),
toggle: () => showLoadingBar(!loadingBarVisible),
isVisible: () => loadingBarVisible
};
export default LoadingBarContainer;
+2
View File
@@ -19,6 +19,7 @@ import "remixicon/fonts/remixicon.css";
import styles from "~/styles/main.css?url";
import messageModalStyles from "~/styles/components/message-modal.css?url";
import toastStyles from "~/styles/components/toast.css?url";
import LoadingBarContainer from "~/components/ui/LoadingBar";
// 添加客户端hydration错误处理
// if (typeof window !== "undefined") {
@@ -87,6 +88,7 @@ export default function App() {
</MessageModalProvider>
<ScrollRestoration />
<Scripts />
<LoadingBarContainer />
</body>
</html>
);
+170 -137
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { useSearchParams, useLoaderData, useFetcher, useNavigate,Link } from "@remix-run/react";
import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
import { Card } from "~/components/ui/Card";
@@ -14,6 +14,7 @@ import { getDocumentTypes } from "~/api/document-types/document-types";
import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files";
import { toastService } from "~/components/ui/Toast";
import { messageService } from "~/components/ui/MessageModal";
import { loadingBarService } from "~/components/ui/LoadingBar";
// 导入样式
export function links() {
@@ -196,6 +197,10 @@ export default function DocumentsIndex() {
const fetcher = useFetcher<ActionResponse>();
const navigate = useNavigate();
// 添加页面加载状态管理
const [isLoadingData, setIsLoadingData] = useState(true);
const dataCache = useRef<typeof loaderData | null>(null);
// 从URL获取当前筛选条件
const search = searchParams.get("search") || "";
const documentType = searchParams.get("documentType") || "";
@@ -210,12 +215,29 @@ export default function DocumentsIndex() {
// 获取API返回的数据
const { documents, total, documentTypeOptions } = loaderData;
// 处理loader错误
// 使用并更新缓存数据
useEffect(() => {
// 如果有缓存数据,先显示缓存,再在后台加载新数据
if (dataCache.current) {
setIsLoadingData(false);
} else {
// 显示加载状态 - 确保显示加载条
loadingBarService.show();
setIsLoadingData(true);
}
// 设置缓存数据
dataCache.current = loaderData;
// 数据加载完成后,执行额外的延迟以确保UI效果
setIsLoadingData(false);
loadingBarService.hide();
// 处理loader错误
if (loaderData.error) {
toastService.error(loaderData.error);
}
}, [loaderData.error]);
}, [loaderData]);
// 使用useEffect监听fetcher状态变化并显示Toast
useEffect(() => {
@@ -564,17 +586,25 @@ export default function DocumentsIndex() {
// 检查audit_status是否为0,如果是则更新为2
if (auditStatus === 0 || auditStatus === null) {
try {
// 显示加载状态
loadingBarService.show();
const response = await updateDocumentAuditStatus(fileId.toString(), 2);
if (response.error) {
console.error('更新文件审核状态失败:', response.error);
// alert('更新文件审核状态失败:' + (response.error || '未知错误'));
toastService.error('更新文件审核状态失败:' + (response.error || '未知错误'));
loadingBarService.hide();
return;
}
} catch (error) {
console.error('更新文件审核状态时出错:', error);
// alert('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误'));
toastService.error('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误'));
loadingBarService.hide();
return;
}
} else {
// 显示加载状态
loadingBarService.show();
}
// 导航到评查详情页
@@ -764,146 +794,149 @@ export default function DocumentsIndex() {
];
return (
<div className="documents-page">
{/* 页面头部 */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-medium"></h2>
<div>
<Button
type="primary"
icon="ri-upload-line"
to="/files/upload"
className="hover:text-white"
>
</Button>
</div>
</div>
{/* 搜索筛选区 */}
<FilterPanel
actions={
<>
<Button
type="default"
icon="ri-refresh-line"
onClick={handleReset}
className="mr-2"
>
</Button>
{/* <Button
type="primary"
icon="ri-search-line"
onClick={() => {
// 保持当前筛选条件,刷新数据
// 在实际应用中,这里可能需要触发某些操作
}}
>
搜索
</Button> */}
</>
}
noActionDivider={true}
>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 w-full">
<SearchFilter
label="文档名称"
placeholder="请输入文档名称"
value={search}
onSearch={handleNameSearch}
instantSearch={true}
/>
<SearchFilter
label="文档编号"
placeholder="请输入文档编号"
value={documentNumber}
onSearch={handleDocumentNumberChange}
instantSearch={true}
/>
<FilterSelect
label="文档类型"
name="documentType"
value={documentType}
options={documentTypeOptions}
onChange={handleDocumentTypeChange}
/>
<FilterSelect
label="文件状态"
name="fileStatus"
value={fileStatus}
options={fileStatusOptions}
onChange={handleFileStatusChange}
/>
<FilterSelect
label="审核状态"
name="auditStatus"
value={auditStatus}
options={auditStatusOptions}
onChange={handleStatusChange}
/>
<DateRangeFilter
label="上传时间"
startDate={dateFrom}
endDate={dateTo}
onStartDateChange={(value) => handleDateChange('dateFrom', value)}
onEndDateChange={(value) => handleDateChange('dateTo', value)}
simple={true}
/>
</div>
</FilterPanel>
{/* 数据表格 */}
<Card>
<div className="mb-3 flex items-center justify-between">
<div className="documents-page relative">
{/* 页面内容,在加载时降低不透明度但不隐藏内容 */}
<div className={isLoadingData ? "opacity-70 pointer-events-none transition-opacity" : "transition-opacity"}>
{/* 页面头部 */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-medium"></h2>
<div>
<Button
type="default"
icon="ri-delete-bin-line"
onClick={handleBatchDelete}
className="mr-2"
disabled={selectedRowKeys.length === 0}
type="primary"
icon="ri-upload-line"
to="/files/upload"
className="hover:text-white"
>
</Button>
<Button
type="default"
icon="ri-download-line"
onClick={handleExport}
>
</Button>
</div>
<div className="text-sm text-secondary">
<span className="font-medium text-primary">{total}</span>
</div>
</div>
<div className="overflow-x-auto">
<Table
columns={columns}
dataSource={documents}
rowKey="id"
emptyText="暂无数据"
{/* 搜索筛选区 */}
<FilterPanel
actions={
<>
<Button
type="default"
icon="ri-refresh-line"
onClick={handleReset}
className="mr-2"
>
</Button>
{/* <Button
type="primary"
icon="ri-search-line"
onClick={() => {
// 保持当前筛选条件,刷新数据
// 在实际应用中,这里可能需要触发某些操作
}}
>
搜索
</Button> */}
</>
}
noActionDivider={true}
>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 w-full">
<SearchFilter
label="文档名称"
placeholder="请输入文档名称"
value={search}
onSearch={handleNameSearch}
instantSearch={true}
/>
<SearchFilter
label="文档编号"
placeholder="请输入文档编号"
value={documentNumber}
onSearch={handleDocumentNumberChange}
instantSearch={true}
/>
<FilterSelect
label="文档类型"
name="documentType"
value={documentType}
options={documentTypeOptions}
onChange={handleDocumentTypeChange}
/>
<FilterSelect
label="文件状态"
name="fileStatus"
value={fileStatus}
options={fileStatusOptions}
onChange={handleFileStatusChange}
/>
<FilterSelect
label="审核状态"
name="auditStatus"
value={auditStatus}
options={auditStatusOptions}
onChange={handleStatusChange}
/>
<DateRangeFilter
label="上传时间"
startDate={dateFrom}
endDate={dateTo}
onStartDateChange={(value) => handleDateChange('dateFrom', value)}
onEndDateChange={(value) => handleDateChange('dateTo', value)}
simple={true}
/>
</div>
</FilterPanel>
{/* 数据表格 */}
<Card>
<div className="mb-3 flex items-center justify-between">
<div>
<Button
type="default"
icon="ri-delete-bin-line"
onClick={handleBatchDelete}
className="mr-2"
disabled={selectedRowKeys.length === 0}
>
</Button>
<Button
type="default"
icon="ri-download-line"
onClick={handleExport}
>
</Button>
</div>
<div className="text-sm text-secondary">
<span className="font-medium text-primary">{total}</span>
</div>
</div>
<div className="overflow-x-auto">
<Table
columns={columns}
dataSource={documents}
rowKey="id"
emptyText="暂无数据"
/>
</div>
{/* 分页 */}
<Pagination
currentPage={currentPage}
total={total}
pageSize={pageSize}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
pageSizeOptions={[10, 20, 50, 100]}
/>
</div>
{/* 分页 */}
<Pagination
currentPage={currentPage}
total={total}
pageSize={pageSize}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
pageSizeOptions={[10, 20, 50, 100]}
/>
</Card>
</Card>
</div>
</div>
);
}
+5 -1
View File
@@ -45,6 +45,7 @@ import {
// 从ReviewPointsList组件中导入ReviewPoint类型
import { type ReviewPoint } from '~/components/reviews';
import { messageService } from "~/components/ui/MessageModal";
import { Button } from "~/components/ui/Button";
/**
@@ -485,7 +486,10 @@ export default function ReviewDetails() {
<>
{/* 文件信息和操作按钮 */}
<FileInfo
fileInfo={reviewData.fileInfo}
fileInfo={{
...reviewData.fileInfo,
previousRoute: loaderData.previousRoute
}}
onConfirmResults={handleConfirmResults}
/>