新增全局加载进度条组件
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user