Unverified Commit bcbbee8c authored by Xiaomeng Zhao's avatar Xiaomeng Zhao Committed by GitHub
Browse files

Merge pull request #2622 from myhloli/dev

Dev
parents 3cc3f754 ced5a7b4
@import '@/styles/variable.scss';
.gradientBtn {
width: 179px;
height: 37px;
border-radius: 4px;
font-size: 14px;
color: rgba(255, 255, 255, 0.95);
// background: linear-gradient(110deg, #38A0FF -33.56%, #0D53DE 32.84%, #5246FF 102.05%);
background: #3477EB;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background: #3477EB;
}
}
.linearBlue {
// TIP: 这里为啥用bg呢,因为ui稿给的参数是假的
background: url('@/assets/pdf/pdf-upload.png');
background-size: cover;
}
.tryText {
font-size: 13px;
line-height: 20px;
background: linear-gradient(107deg, #38A0FF -24.14%, #0D53DE 30.09%, #5246FF 86.61%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.extractorContainer {
min-width: $page-min-witch;
}
\ No newline at end of file
import DarkLogo from "@/assets/svg/logo.svg";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import commonStyles from "./index.module.scss";
import { EXTRACTOR_TYPE_LIST } from "@/types/extract-task-type";
import extractorPdfIcon from "@/assets/pdf/extractor-pdf.svg";
import extractorTableIcon from "@/assets/pdf/extractor-table.svg";
import extractorFormulaIcon from "@/assets/pdf/extractor-formula.svg";
import { useIntl } from "react-intl";
import cls from "classnames";
import ExtractorGuide from "@/pages/extract/components/extractor-guide";
import ExtractorQueue from "@/pages/extract/components/extractor-queue";
import ExtractorLang from "@/pages/extract/components/extractor-lang";
interface IExtractorSideProps {
className?: string;
}
interface TabItem {
label: string;
type: string;
}
export const ExtractorSide = ({ className = "" }: IExtractorSideProps) => {
const navigate = useNavigate();
const params = useParams();
const location = useLocation();
const { messages } = useIntl();
const menuClass =
"px-2 py-2.5 mb-1 text-[0.875rem] text-[#121316]/[0.8] font-semibold rounded h-10 flex items-center cursor-pointer hover:bg-[#0d53de]/[0.05]";
const handleMenuClick = (type: string) => {
navigate(`/OpenSourceTools/Extractor/${type}`);
};
const goToOpenSource = () => {
navigate("/OpenSourceTools/Extractor/");
};
const tabList =
(messages?.["extractor.side.tabList"] as unknown[] as TabItem[]) || [];
const getIconStyle = (type: string) => {
const activeClassName = "!bg-[#0d53de]/[0.05] !text-[#0D53DE]";
const path = location.pathname;
const regex = /\/Extractor\/([^/]+)(\/|$)/;
const match = params?.jobID ? "" : path.match(regex)?.[1] || "/";
const getIcon = () => {
switch (type) {
case EXTRACTOR_TYPE_LIST.pdf:
return extractorPdfIcon;
case EXTRACTOR_TYPE_LIST.table:
return extractorTableIcon;
case EXTRACTOR_TYPE_LIST.formula:
return extractorFormulaIcon;
}
};
return {
icon: getIcon(),
tabClassName: match === type ? activeClassName : "",
};
};
return (
<div
className={cls(
`w-[240px] min-w-[240px] h-full px-4 py-6 flex flex-col justify-start border-r-[1px] border-y-0 border-l-0 border-solid border-[#EBECF0] select-none`,
commonStyles.linearBlue,
className
)}
>
<div className={""}>
<div className="h-[2rem] mb-6 flex justify-between items-center">
<img
className="h-full cursor-pointer"
src={DarkLogo}
alt=""
onClick={goToOpenSource}
/>
<ExtractorGuide />
</div>
{/* tab-list */}
<div className="mb-2">
{tabList.map((i) => (
<div
key={i.type}
className={cls(menuClass, getIconStyle(i.type)?.tabClassName)}
onClick={() => handleMenuClick(i.type)}
>
<img src={getIconStyle(i.type).icon} className="mr-2 w-6 h-6" />
{i.label}
</div>
))}
</div>
</div>
<div className="bg-[#0d53de]/[0.08] w-full h-[1px] mt-2 mb-4"></div>
<ExtractorQueue className="flex-1 overflow-y-auto mb-6" />
<ExtractorLang className="absolute bottom-6" />
</div>
);
};
.extractorGuide {
:global {
.ant-popover-content, .ant-popover-inner {
border-radius: 12px !important;
overflow: hidden;
box-shadow: 0px 8px 26px 0px rgba(0, 0, 0, 0.12);
}
.ant-popover-inner-content {
padding: 24px !important;
}
.ant-popover-arrow {
display: none !important;
}
}
}
import { Popover } from "antd";
import guideToolsSvg from "@/assets/pdf/guideTools.svg";
import style from "./index.module.scss";
import { useIntl } from "react-intl";
import IconFont from "@/components/icon-font";
import { windowOpen } from "@/utils/windowOpen";
interface GuideItem {
type: string;
icon: string;
"zh-CN-title": string;
title: string;
desc: string;
goToText: string;
link: string;
}
const ExtractorGuide = () => {
const { formatMessage, messages } = useIntl();
const EXTRACTOR_GUIDE_ITEM_LIST = (messages?.["extractor.side.guide_list"] ||
[]) as unknown as GuideItem[];
const content = (
<div>
<div className="text-[1.25rem] font-semibold mt-3 mb-2 ml-4">
{formatMessage({
id: "extractor.guide.title",
})}
</div>
<hgroup>
{EXTRACTOR_GUIDE_ITEM_LIST?.map((i) => {
return (
<div
key={i.type}
className="flex p-4 items-center cursor-pointer hover:bg-[#F4F5F9] rounded group h-[6.5rem]"
onClick={() => windowOpen(i.link)}
>
<img
src={i.icon}
alt=""
className="w-[1.5rem] h-[1.5rem] transition-all mr-[0.75rem]"
/>
<div className="">
<div className="font-semibold transition-all text-[1rem]">
{i.title}
</div>
<div className="text-base text-[13px] text-[#121316]/[0.6] transition-all ">
{i.desc}
</div>
<div className="h-0 mt-2 overflow-hidden !text-[13px] text-[#121316]/[0.8] transition-all group-hover:h-auto">
{i.goToText}
<IconFont type="icon-ArrowRightOutlined" className="ml-1" />
</div>
</div>
</div>
);
})}
</hgroup>
</div>
);
return (
<Popover
overlayClassName={style.extractorGuide}
content={content}
showArrow={false}
placement="right"
>
<img
className="w-[1.32rem] h-[1.32rem] p-0.5 hover:rotate-45 transition-all cursor-pointer rounded"
src={guideToolsSvg}
alt="guideToolsSvg"
/>
</Popover>
);
};
export default ExtractorGuide;
import LangChangeIcon from "@/assets/pdf/lang-change.svg";
import { useLanguageStore } from "@/store/languageStore";
import cls from "classnames";
interface ExtractorLangProps {
className?: string;
}
const ExtractorLang: React.FC<ExtractorLangProps> = ({ className }) => {
const { toggleLanguage } = useLanguageStore();
const changeLang = () => {
toggleLanguage?.();
};
return (
<>
<img
onClick={() => changeLang()}
src={LangChangeIcon}
alt="LangChangeIcon"
className={cls(
"w-[1.5rem] h-[1.5rem] cursor-pointer object-cover hover:bg-[#0D53DE]/[0.1] rounded cursor-pointer",
className
)}
/>
</>
);
};
export default ExtractorLang;
import IconFont from "@/components/icon-font";
import { useIntl } from "react-intl";
import extractorQueueSvg from "@/assets/pdf/extractor-queue.svg";
import { useNavigate, useParams } from "react-router-dom";
import {
EXTRACTOR_TYPE_LIST,
ExtractTaskType,
} from "@/types/extract-task-type";
import cls from "classnames";
import { useLatest, useRequest } from "ahooks";
import { deleteExtractJob, getExtractorHistory } from "@/api/extract";
import { message, Popconfirm, Tooltip } from "antd";
import { useEffect } from "react";
import { ADD_TASK_LIST, UPDATE_TASK_LIST } from "@/constant/event";
import { findIndex } from "lodash";
import { TextTooltip } from "@/components/text-tooltip";
interface ExtractorQueueProps {
className?: string;
}
const ExtractorQueue: React.FC<ExtractorQueueProps> = ({ className }) => {
const { formatMessage, locale } = useIntl();
const navigate = useNavigate();
const params = useParams();
const { data: taskList, mutate } = useRequest(() => {
return getExtractorHistory({
pageNo: 1,
pageSize: 100,
}).then((res) => {
return res?.list?.filter((i) => !!i.id && !!i.type) || [];
});
});
let timeout: NodeJS.Timeout | null = null;
const activeClassName = "!bg-[#0d53de]/[0.05] !text-[#0D53DE]";
const handleExtractor = (originType: ExtractTaskType, id: string) => {
const type = originType?.split("-")[0];
const detailType = originType?.split("-")[1];
if (type === EXTRACTOR_TYPE_LIST.formula.toLowerCase()) {
navigate(`/OpenSourceTools/Extractor/formula/${id}?type=${detailType}`);
} else if (type === EXTRACTOR_TYPE_LIST.pdf.toLowerCase()) {
navigate(`/OpenSourceTools/Extractor/PDF/${id}`);
} else if (type === EXTRACTOR_TYPE_LIST.table.toLocaleLowerCase()) {
navigate(`/OpenSourceTools/Extractor/table/${id}`);
}
return;
};
const cancel = (e?: React.MouseEvent<HTMLElement>) => {
e?.stopPropagation();
e?.preventDefault();
};
const confirm = (id: string) => {
const deleteIndex = findIndex(taskList, (i) => i.id === id);
const nextJob = taskList?.[deleteIndex + 1]
? taskList?.[deleteIndex + 1]
: taskList?.[deleteIndex - 1];
mutate(taskList?.filter((i) => i.id !== id));
deleteExtractJob(id).then(() => {
message.success(formatMessage({ id: "extractor.queue.delete.success" }));
});
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
if (nextJob?.id) {
handleExtractor(nextJob?.type as any, nextJob?.id);
} else {
navigate("/OpenSourceTools/Extractor");
}
}, 10);
};
const taskListRef = useLatest(taskList);
const handleAddList = ({ detail }: CustomEvent) => {
const taskData = detail as any;
mutate(
[
{
fileName: taskData?.fileName,
id: taskData?.id,
type: taskData?.type,
state: taskData?.state, // 提取状态
} as any,
].concat(taskListRef?.current)
);
};
useEffect(() => {
const handleUpdateList = ({ detail }: CustomEvent) => {
const taskData = detail as any;
taskListRef?.current?.forEach((i) => {
if (i.id === taskData?.id) {
i.state = taskData?.state || taskData?.state;
}
});
mutate(taskListRef?.current);
};
document.addEventListener(
UPDATE_TASK_LIST,
handleUpdateList as EventListener
);
document.addEventListener(ADD_TASK_LIST, handleAddList as EventListener);
return () => {
document.removeEventListener(
UPDATE_TASK_LIST,
handleUpdateList as EventListener
);
document.removeEventListener(
ADD_TASK_LIST,
handleAddList as EventListener
);
};
}, []);
useEffect(() => {
mutate(taskListRef?.current);
}, [locale]);
return (
<div className={cls("w-full flex flex-col mb-3", className)}>
<header className="flex items-center px-2 py-[0.625rem] text-[#121316]/[0.8] text-[0.875rem] font-semibold">
<img
src={extractorQueueSvg}
className="w-6 h-6 mr-2 "
alt="extractorQueueSvg"
/>
{formatMessage({
id: "extractor.queue",
})}
</header>
<hgroup className="overflow-auto flex-1 scrollbar-thin">
{taskList?.map((i, index) => {
return (
<div
className={cls(
"group h-[2.5rem] flex items-center px-4 py-2.5 mb-1 text-[#121316]/[0.8] pl-10 text-sm rounded h-10 flex items-center cursor-pointer hover:bg-[#0d53de]/[0.05]",
params?.jobID === String(i?.id) && activeClassName
)}
key={i?.fileName + index + i?.id}
onClick={() => handleExtractor(i.type as any, i.id)}
>
<span className="truncate mr-2 max-w-[calc(100%-2rem)]">
<TextTooltip trigger="hover" str={i?.fileName} />
</span>
<>
{i?.state === "failed" && (
<Tooltip
title={formatMessage({
id: "extractor.error",
})}
>
<IconFont
type={"icon-attentionFilled"}
className="text-[#FF8800] mr-1"
/>
</Tooltip>
)}
<Popconfirm
title={formatMessage({ id: "extractor.queue.delete" })}
description={<div className="my-4"></div>}
onConfirm={(e) => {
e?.stopPropagation();
e?.preventDefault();
confirm(i.id);
}}
onCancel={cancel}
okText={formatMessage({ id: "common.confirm" })}
cancelText={formatMessage({ id: "common.cancel" })}
okButtonProps={{
style: {
backgroundColor: "#F5483B",
},
}}
>
<IconFont
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
type="icon-shanchu"
className="hidden ml-auto text-[1rem] text-[#121316]/[0.8] hover:text-[#0D53DE] group-hover:block"
/>
</Popconfirm>
</>
</div>
);
})}
</hgroup>
</div>
);
};
export default ExtractorQueue;
.githubBtn {
position: relative;
width: 100%;
cursor: pointer;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.25rem;
overflow: hidden;
border-radius: 8px;
cursor: pointer;
filter: blur(0px);
z-index: 0;
&::before {
width: 100%;
height: 100%;
display: block;
content: "";
position: absolute;
top: 0;
left: 0;
background: linear-gradient(to bottom, rgba(185,214,246,1) -100%, rgba(244,247,254,) 100%);
z-index: 0;
}
& > span {
border-radius: 7px;
display:inline-flex;
width: calc(100% - 2px);
height: calc(100% - 2px);
background: linear-gradient(180deg, #5C93FF1F -160.94%, rgba(255, 255, 255, 1) 80%);
z-index: 1;
filter: blur(0px);
justify-content: center;
align-items: center;
font-size: 16px;
&:hover {
background: linear-gradient(180deg, #5C93FF1F -60.94%, rgba(255, 255, 255, 1) 80%);
filter: blur(0px);
}
span:nth-child(3){
color: var(--80-text-4, rgba(18, 19, 22, 0.80));
-webkit-background-clip: text;
background-clip: text;
}
}
}
.githubText {
/* 正文/加粗text-1-semibold */
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 21px; /* 150% */
color: #121316;
}
import githubSvg from "@/assets/pdf/github.svg";
import { windowOpen } from "@/utils/windowOpen";
import styles from "./index.module.scss";
import cls from "classnames";
const ExtractorRepo = () => {
return (
<div
className={cls(styles.githubBtn)}
onClick={() =>
windowOpen("https://github.com/opendatalab/MinerU", "_blank")
}
>
<span className="text-sm ">
<img src={githubSvg} className="mr-2" />
<span className="!text-[14px] ml-[0.5rem]">🎉</span>
</span>
</div>
);
};
export default ExtractorRepo;
import LoadingAnimation from "@/components/loading-animation";
import { ExclamationCircleFilled } from "@ant-design/icons";
import cls from "classnames";
export const IframeLoading = ({
filename,
type,
text,
errorElement,
classNameTitle = "",
showHeader,
}: {
filename?: string;
type: "loading" | "error";
text?: string;
errorElement?: React.ReactElement;
classNameTitle?: string;
showHeader?: boolean;
}) => {
return (
<div className="flex flex-col h-full text-sm text-[#121316]/[0.8] whitespace-nowrap ">
{showHeader && (
<div
className={cls(
"h-[47px] border-0 border-solid border-b-[1px] border-[#EBECF0] w-full pl-[24px]",
classNameTitle
)}
>
{filename}
</div>
)}
<div className="flex-1 flex justify-center items-center">
{type === "error" ? (
errorElement ? (
errorElement
) : (
<>
<ExclamationCircleFilled
style={{ color: "#FF8800" }}
rotate={180}
/>
<span className="ml-2.5">上传失败,请</span>
<span className="text-[#0D53DE] ml-1 cursor-pointer">
重新上传
</span>
</>
)
) : (
<>
<LoadingAnimation />
<span className="ml-2.5">{text || "PDF 上传中,请稍等..."}</span>
</>
)}
</div>
</div>
);
};
import React, {
useEffect,
useRef,
useState,
useMemo,
forwardRef,
useImperativeHandle,
useCallback,
} from "react";
import cls from "classnames";
import { isObjEqual } from "@/utils/render";
import { useSize } from "ahooks";
interface IImageLayersViewerProps {
imageUrl: string;
imageWidth: number;
imageHeight: number;
layout: Array<{
category_id: number;
poly: number[];
score: number;
latex?: string;
}>;
layerVisible?: boolean;
disableZoom?: boolean;
className?: string;
onChange?: (data: { scale: number }) => void;
}
export interface ImageLayerViewerRef {
containerRef: HTMLDivElement | null;
zoomIn: () => void;
zoomOut: () => void;
scale: number;
updateScaleAndPosition: () => void;
}
const ImageLayerViewer = forwardRef<
ImageLayerViewerRef,
IImageLayersViewerProps
>(
(
{
imageUrl,
imageHeight,
imageWidth,
onChange,
layout,
disableZoom,
className = "",
layerVisible = true,
},
ref
) => {
const containerRef = useRef<HTMLDivElement>(null);
const imageCanvasRef = useRef<HTMLCanvasElement>(null);
const overlayCanvasRef = useRef<HTMLCanvasElement>(null);
const rafRef = useRef<number | null>(null);
const containerSize = useSize(containerRef);
const [scale, setScale] = useState(1);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [padding, setPadding] = useState({ left: 0, top: 0 });
const minZoom = 0.1;
const maxZoom = 3;
const zoomSensitivity = 0.001;
const zoomStep = 0.1;
const dpr = useMemo(() => window.devicePixelRatio || 1, []);
const image = useMemo(() => {
const img = new Image();
img.src = imageUrl;
return img;
}, [imageUrl]);
const calculateInitialScaleAndPosition = useCallback(() => {
if (!containerRef.current)
return { initialScale: 1, initialPosition: { x: 0, y: 0 } };
const containerWidth = containerRef.current.clientWidth;
const containerHeight = containerRef.current.clientHeight;
const scaleX = containerWidth / imageWidth;
const scaleY = containerHeight / imageHeight;
const initialScale = Math.min(scaleX, scaleY, 1); // Ensure it doesn't scale up initially
const scaledWidth = imageWidth * initialScale;
const scaledHeight = imageHeight * initialScale;
const initialPosition = {
x: (containerWidth - scaledWidth) / 2,
y: (containerHeight - scaledHeight) / 2,
};
return { initialScale, initialPosition };
}, [imageWidth, imageHeight]);
const updateScaleAndPosition = useCallback(() => {
const { initialScale, initialPosition } =
calculateInitialScaleAndPosition();
setScale(initialScale);
setPosition(initialPosition);
setPadding({ left: 0, top: 0 });
}, [calculateInitialScaleAndPosition]);
useEffect(() => {
updateScaleAndPosition();
}, [imageWidth, imageHeight]);
const drawImage = useCallback(() => {
const ctx = imageCanvasRef.current?.getContext("2d");
if (!ctx || !image.complete) return;
const scaledWidth = imageWidth * scale;
const scaledHeight = imageHeight * scale;
ctx.canvas.width = scaledWidth * dpr;
ctx.canvas.height = scaledHeight * dpr;
ctx.canvas.style.width = `${scaledWidth}px`;
ctx.canvas.style.height = `${scaledHeight}px`;
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, scaledWidth, scaledHeight);
ctx.drawImage(image, 0, 0, scaledWidth, scaledHeight);
}, [image, imageWidth, imageHeight, scale, dpr]);
const drawLayout = useCallback(() => {
const ctx = overlayCanvasRef.current?.getContext("2d");
if (!ctx) return;
const scaledWidth = imageWidth * scale;
const scaledHeight = imageHeight * scale;
ctx.canvas.width = scaledWidth * dpr;
ctx.canvas.height = scaledHeight * dpr;
ctx.canvas.style.width = `${scaledWidth}px`;
ctx.canvas.style.height = `${scaledHeight}px`;
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, scaledWidth, scaledHeight);
layout?.forEach((item) => {
const [x1, y1, x2, y2, x3, y3, x4, y4] = item.poly.map(
(coord) => coord * scale
);
switch (item.category_id) {
case 9:
ctx.fillStyle = "rgba(230, 113, 230, 0.4)";
ctx.strokeStyle = "rgba(230, 113, 230, 1)";
break;
case 8:
ctx.fillStyle = "rgba(240, 240, 124, 0.4)";
ctx.strokeStyle = "rgba(240, 240, 124, 1)";
break;
case 13:
ctx.fillStyle = "rgba(150, 232, 172, 0.4)";
ctx.strokeStyle = "rgba(150, 232, 172, 1)";
break;
case 14:
ctx.fillStyle = "rgba(230, 122, 171, 0.4)";
ctx.strokeStyle = "rgba(230, 122, 171, 1)";
break;
default:
ctx.fillStyle = "transparent";
ctx.strokeStyle = "transparent";
}
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineTo(x3, y3);
ctx.lineTo(x4, y4);
ctx.closePath();
ctx.fill();
ctx.stroke();
});
}, [layout, scale, dpr]);
const updateScale = useCallback(
(newScale: number, clientX: number, clientY: number) => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
const containerWidth = rect.width;
const containerHeight = rect.height;
const x = clientX - rect.left;
const y = clientY - rect.top;
const prevScaledWidth = imageWidth * scale;
const prevScaledHeight = imageHeight * scale;
const newScaledWidth = imageWidth * newScale;
const newScaledHeight = imageHeight * newScale;
let newPosition = {
x:
position.x -
((x - position.x) * (newScaledWidth - prevScaledWidth)) /
prevScaledWidth,
y:
position.y -
((y - position.y) * (newScaledHeight - prevScaledHeight)) /
prevScaledHeight,
};
// Center the image if it's smaller than the container
if (newScaledWidth < containerWidth) {
newPosition.x = (containerWidth - newScaledWidth) / 2;
}
if (newScaledHeight < containerHeight) {
newPosition.y = (containerHeight - newScaledHeight) / 2;
}
setScale(newScale);
setPosition(newPosition);
// Calculate new padding
const newPadding = {
left: Math.max(0, -newPosition.x),
top: Math.max(0, -newPosition.y),
};
setPadding(newPadding);
}
},
[scale, position, imageWidth, imageHeight]
);
const handleZoom = useCallback(
(delta: number, clientX: number, clientY: number) => {
const newScale = scale * Math.exp(-delta * zoomSensitivity);
const boundedNewScale = Math.max(minZoom, Math.min(newScale, maxZoom));
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
}
rafRef.current = requestAnimationFrame(() => {
updateScale(boundedNewScale, clientX, clientY);
});
},
[scale, updateScale]
);
const handleCenterZoom = useCallback(
(zoomIn: boolean) => {
const newScale = zoomIn
? scale * (1 + zoomStep)
: scale / (1 + zoomStep);
const boundedNewScale = Math.max(minZoom, Math.min(newScale, maxZoom));
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
const centerX = rect.width / 2;
const centerY = rect.height / 2;
updateScale(boundedNewScale, centerX, centerY);
}
},
[scale, updateScale]
);
const zoomIn = useCallback(() => {
handleCenterZoom(true);
}, [handleCenterZoom]);
const zoomOut = useCallback(() => {
handleCenterZoom(false);
}, [handleCenterZoom]);
useImperativeHandle(
ref,
() => ({
containerRef: containerRef.current,
zoomIn,
zoomOut,
scale,
updateScaleAndPosition,
}),
[zoomIn, zoomOut, scale]
);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const handleWheel = (e: WheelEvent) => {
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
handleZoom(e.deltaY * 4.8, e.clientX, e.clientY);
}
};
container.addEventListener("wheel", handleWheel, { passive: false });
return () => {
container.removeEventListener("wheel", handleWheel);
};
}, [handleZoom]);
useEffect(() => {
if (containerRef?.current) {
containerRef.current?.scrollTo({
left: padding.left,
top: padding.top,
});
}
}, [padding]);
useEffect(() => {
const draw = () => {
drawImage();
drawLayout();
};
if (image.complete) {
draw();
} else {
image.onload = draw;
}
}, [image, drawImage, drawLayout]);
useEffect(() => {
if (overlayCanvasRef.current) {
overlayCanvasRef.current.style.opacity = layerVisible ? "1" : "0";
}
}, [layerVisible]);
useEffect(() => {
onChange?.({ scale });
}, [scale]);
return (
<div
className={cls(
className,
"w-full h-full overflow-auto scrollbar-thin relative"
)}
ref={containerRef}
>
<div
style={{
paddingLeft: `${padding.left}px`,
paddingTop: `${padding.top}px`,
}}
>
<div
className="absolute"
style={{
width: `${imageWidth * scale}px`,
height: `${imageHeight * scale}px`,
transform: `translate(${position.x}px, ${position.y}px)`,
}}
>
<canvas
ref={imageCanvasRef}
style={{
width: `${imageWidth * scale}px`,
height: `${imageHeight * scale}px`,
}}
/>
<canvas
ref={overlayCanvasRef}
className="absolute top-0 left-0"
style={{
width: `${imageWidth * scale}px`,
height: `${imageHeight * scale}px`,
}}
/>
</div>
</div>
</div>
);
}
);
export default React.memo(ImageLayerViewer, isObjEqual);
// @import '../../../../global.scss';
.customStyle {
padding: 2rem;
padding-top: 0rem;
& > div {
max-width: 100%;
max-height: 100%;
// @include scrollBar(red);
}
.katex-display {
margin-top: 0px !important;
// @include scrollBar(red);
}
}
import React from 'react';
import 'katex/dist/katex.min.css';
import { BlockMath } from 'react-katex';
import style from './index.module.scss';
import classNames from 'classnames';
interface LatexRendererProps {
formula: string;
className?: string;
'aria-label'?: string;
title?: string;
}
function LatexRenderer({ formula, className = '', 'aria-label': ariaLabel, title }: LatexRendererProps) {
try {
return (
<div
className={`${className} max-w-[100%] max-h-[100%] scrollbar-thin ${style.customStyle}`}
aria-label={ariaLabel}
>
<BlockMath math={formula} className="scrollbar-thin" />
</div>
);
} catch (error) {
console.error('Error rendering Latex:', error);
return <div>Unable to render Latex formula.</div>;
}
}
export default LatexRenderer;
$circle-width: 16px;
.container {
position: relative;
width:4 * $circle-width;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.dot-pulse {
position: relative;
left: -9999px;
width: $circle-width;
height: $circle-width;
border-radius: 50%;
background-color: var(--color);
color: var(--color);
box-shadow: 9999px 0 0 -3px;
animation: dot-pulse 1.5s infinite linear;
animation-delay: 0.25s;
}
.dot-pulse::before,
.dot-pulse::after {
content: '';
display: inline-block;
position: absolute;
top: 0;
width: $circle-width;
height: $circle-width;
border-radius: 50%;
background-color: var(--color);
color: var(--color);
}
.dot-pulse::before {
box-shadow: 9974px 0 0 -3px;
animation: dot-pulse-before 1.5s infinite linear;
animation-delay: 0s;
}
.dot-pulse::after {
box-shadow: 10024px 0 0 -3px;
animation: dot-pulse-after 1.5s infinite linear;
animation-delay: 0.5s;
}
@keyframes dot-pulse-before {
0% {
box-shadow: 9974px 0 0 -3px;
}
30% {
box-shadow: 9974px 0 0 2px;
}
60%,
100% {
box-shadow: 9974px 0 0 -3px;
}
}
@keyframes dot-pulse {
0% {
box-shadow: 9999px 0 0 -3px;
}
30% {
box-shadow: 9999px 0 0 3px;
}
60%,
100% {
box-shadow: 9999px 0 0 -3px;
}
}
@keyframes dot-pulse-after {
0% {
box-shadow: 10024px 0 0 -3px;
}
30% {
box-shadow: 10024px 0 0 2px;
}
60%,
100% {
box-shadow: 10024px 0 0 -3px;
}
}
import classNames from "classnames";
import style from "./index.module.scss";
const LoadingIcon = ({
color,
className,
}: {
color: string;
className?: string;
}) => {
return (
<div className={classNames(style.container, className)}>
<div
className={style.dotPulse}
style={{ "--color": color || "grey" } as any}
></div>
</div>
);
};
export default LoadingIcon;
/*light*/
// 自定义滚动跳
.scrollBar {
// 火狐
scrollbar-color: #EBECF0 transparent;
scrollbar-width: thin;
// 定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸
&::-webkit-scrollbar {
width: 4px;
height: 6px;
background-color: transparent;
}
// 定义滚动条轨道 内阴影+圆角
&::-webkit-scrollbar-track {
background-color: #fff;
border-radius: 10px;
box-shadow: transparent;
}
// 定义滑块 内阴影+圆角
&::-webkit-scrollbar-thumb {
background: #EBECF0;
border-radius: 10px;
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.6);
}
}
.mdViewerWrap {
font-size: 10px;
}
.mdViewerWrap {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
margin: 0;
color: #1f2328;
background-color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
scroll-behavior: auto;
}
.mdViewerWrap .octicon {
display: inline-block;
fill: currentColor;
vertical-align: text-bottom;
}
.mdViewerWrap h1:hover .anchor .octicon-link:before,
.mdViewerWrap h2:hover .anchor .octicon-link:before,
.mdViewerWrap h3:hover .anchor .octicon-link:before,
.mdViewerWrap h4:hover .anchor .octicon-link:before,
.mdViewerWrap h5:hover .anchor .octicon-link:before,
.mdViewerWrap h6:hover .anchor .octicon-link:before {
width: 16px;
height: 16px;
content: " ";
display: inline-block;
background-color: currentColor;
-webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
}
.mdViewerWrap details,
.mdViewerWrap figcaption,
.mdViewerWrap figure {
display: block;
}
.mdViewerWrap summary {
display: list-item;
}
.mdViewerWrap [hidden] {
display: none !important;
}
.mdViewerWrap a {
background-color: transparent;
color: #0969da;
text-decoration: none;
}
.mdViewerWrap abbr[title] {
border-bottom: none;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
.mdViewerWrap b,
.mdViewerWrap strong {
font-weight: 600;
}
.mdViewerWrap dfn {
font-style: italic;
}
.mdViewerWrap h1 {
margin: 0.67em 0;
font-weight: 600;
padding-bottom: 0.3em;
font-size: 2em;
border-bottom: 1px solid #d0d7deb3;
}
.mdViewerWrap mark {
background-color: #fff8c5;
color: #1f2328;
}
.mdViewerWrap small {
font-size: 90%;
}
.mdViewerWrap sub,
.mdViewerWrap sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
.mdViewerWrap sub {
bottom: -0.25em;
}
.mdViewerWrap sup {
top: -0.5em;
}
.mdViewerWrap img {
border-style: none;
max-width: 100%;
box-sizing: content-box;
background-color: #ffffff;
}
.mdViewerWrap code,
.mdViewerWrap kbd,
.mdViewerWrap pre,
.mdViewerWrap samp {
font-family: monospace;
font-size: 1em;
}
.mdViewerWrap figure {
margin: 1em 40px;
}
.mdViewerWrap hr {
box-sizing: content-box;
overflow: hidden;
background: transparent;
border-bottom: 1px solid #d0d7deb3;
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: #d0d7de;
border: 0;
}
.mdViewerWrap input {
font: inherit;
margin: 0;
overflow: visible;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.mdViewerWrap [type="button"],
.mdViewerWrap [type="reset"],
.mdViewerWrap [type="submit"] {
-webkit-appearance: button;
appearance: button;
}
.mdViewerWrap [type="checkbox"],
.mdViewerWrap [type="radio"] {
box-sizing: border-box;
padding: 0;
}
.mdViewerWrap [type="number"]::-webkit-inner-spin-button,
.mdViewerWrap [type="number"]::-webkit-outer-spin-button {
height: auto;
}
.mdViewerWrap [type="search"]::-webkit-search-cancel-button,
.mdViewerWrap [type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
appearance: none;
}
.mdViewerWrap ::-webkit-input-placeholder {
color: inherit;
opacity: 0.54;
}
.mdViewerWrap ::-webkit-file-upload-button {
-webkit-appearance: button;
appearance: button;
font: inherit;
}
.mdViewerWrap a:hover {
text-decoration: underline;
}
.mdViewerWrap ::placeholder {
color: #636c76;
opacity: 1;
}
.mdViewerWrap hr::before {
display: table;
content: "";
}
.mdViewerWrap hr::after {
display: table;
clear: both;
content: "";
}
.mdViewerWrap table {
border-spacing: 0;
border-collapse: collapse;
display: block;
width: max-content;
max-width: 100%;
overflow: auto;
}
.mdViewerWrap td,
.mdViewerWrap th {
padding: 0;
}
.mdViewerWrap details summary {
cursor: pointer;
}
.mdViewerWrap details:not([open]) > *:not(summary) {
display: none;
}
.mdViewerWrap a:focus,
.mdViewerWrap [role="button"]:focus,
.mdViewerWrap input[type="radio"]:focus,
.mdViewerWrap input[type="checkbox"]:focus {
outline: 2px solid #0969da;
outline-offset: -2px;
box-shadow: none;
}
.mdViewerWrap a:focus:not(:focus-visible),
.mdViewerWrap [role="button"]:focus:not(:focus-visible),
.mdViewerWrap input[type="radio"]:focus:not(:focus-visible),
.mdViewerWrap input[type="checkbox"]:focus:not(:focus-visible) {
outline: solid 1px transparent;
}
.mdViewerWrap a:focus-visible,
.mdViewerWrap [role="button"]:focus-visible,
.mdViewerWrap input[type="radio"]:focus-visible,
.mdViewerWrap input[type="checkbox"]:focus-visible {
outline: 2px solid #0969da;
outline-offset: -2px;
box-shadow: none;
}
.mdViewerWrap a:not([class]):focus,
.mdViewerWrap a:not([class]):focus-visible,
.mdViewerWrap input[type="radio"]:focus,
.mdViewerWrap input[type="radio"]:focus-visible,
.mdViewerWrap input[type="checkbox"]:focus,
.mdViewerWrap input[type="checkbox"]:focus-visible {
outline-offset: 0;
}
.mdViewerWrap kbd {
display: inline-block;
padding: 3px 5px;
font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace;
line-height: 10px;
color: #1f2328;
vertical-align: middle;
background-color: #f6f8fa;
border: solid 1px #afb8c133;
border-bottom-color: #afb8c133;
border-radius: 6px;
box-shadow: inset 0 -1px 0 #afb8c133;
}
.mdViewerWrap h1,
.mdViewerWrap h2,
.mdViewerWrap h3,
.mdViewerWrap h4,
.mdViewerWrap h5,
.mdViewerWrap h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.mdViewerWrap h2 {
font-weight: 600;
padding-bottom: 0.3em;
font-size: 1.5em;
border-bottom: 1px solid #d0d7deb3;
}
.mdViewerWrap h3 {
font-weight: 600;
font-size: 1.25em;
}
.mdViewerWrap h4 {
font-weight: 600;
font-size: 1em;
}
.mdViewerWrap h5 {
font-weight: 600;
font-size: 0.875em;
}
.mdViewerWrap h6 {
font-weight: 600;
font-size: 0.85em;
color: #636c76;
}
.mdViewerWrap p {
margin-top: 0;
margin-bottom: 10px;
font-size: 1.25em;
}
.mdViewerWrap blockquote {
margin: 0;
padding: 0 1em;
color: #636c76;
border-left: 0.25em solid #d0d7de;
}
.mdViewerWrap ul,
.mdViewerWrap ol {
margin-top: 0;
margin-bottom: 0;
padding-left: 2em;
}
.mdViewerWrap ol ol,
.mdViewerWrap ul ol {
list-style-type: lower-roman;
}
.mdViewerWrap ul ul ol,
.mdViewerWrap ul ol ol,
.mdViewerWrap ol ul ol,
.mdViewerWrap ol ol ol {
list-style-type: lower-alpha;
}
.mdViewerWrap dd {
margin-left: 0;
}
.mdViewerWrap tt,
.mdViewerWrap code,
.mdViewerWrap samp {
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace;
font-size: 12px;
}
.mdViewerWrap pre {
margin-top: 0;
margin-bottom: 0;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace;
font-size: 12px;
word-wrap: normal;
}
.mdViewerWrap .octicon {
display: inline-block;
overflow: visible !important;
vertical-align: text-bottom;
fill: currentColor;
}
.mdViewerWrap input::-webkit-outer-spin-button,
.mdViewerWrap input::-webkit-inner-spin-button {
margin: 0;
-webkit-appearance: none;
appearance: none;
}
.mdViewerWrap .mr-2 {
margin-right: 0.5rem !important;
}
.mdViewerWrap::before {
display: table;
content: "";
}
.mdViewerWrap::after {
display: table;
clear: both;
content: "";
}
.mdViewerWrap > *:first-child {
margin-top: 0 !important;
}
.mdViewerWrap > *:last-child {
margin-bottom: 0 !important;
}
.mdViewerWrap a:not([href]) {
color: inherit;
text-decoration: none;
}
.mdViewerWrap .absent {
color: #d1242f;
}
.mdViewerWrap .anchor {
float: left;
padding-right: 4px;
margin-left: -20px;
line-height: 1;
}
.mdViewerWrap .anchor:focus {
outline: none;
}
.mdViewerWrap p,
.mdViewerWrap blockquote,
.mdViewerWrap ul,
.mdViewerWrap ol,
.mdViewerWrap dl,
.mdViewerWrap table,
.mdViewerWrap pre,
.mdViewerWrap details {
margin-top: 0;
margin-bottom: 16px;
}
.mdViewerWrap blockquote > :first-child {
margin-top: 0;
}
.mdViewerWrap blockquote > :last-child {
margin-bottom: 0;
}
.mdViewerWrap h1 .octicon-link,
.mdViewerWrap h2 .octicon-link,
.mdViewerWrap h3 .octicon-link,
.mdViewerWrap h4 .octicon-link,
.mdViewerWrap h5 .octicon-link,
.mdViewerWrap h6 .octicon-link {
color: #1f2328;
vertical-align: middle;
visibility: hidden;
}
.mdViewerWrap h1:hover .anchor,
.mdViewerWrap h2:hover .anchor,
.mdViewerWrap h3:hover .anchor,
.mdViewerWrap h4:hover .anchor,
.mdViewerWrap h5:hover .anchor,
.mdViewerWrap h6:hover .anchor {
text-decoration: none;
}
.mdViewerWrap h1:hover .anchor .octicon-link,
.mdViewerWrap h2:hover .anchor .octicon-link,
.mdViewerWrap h3:hover .anchor .octicon-link,
.mdViewerWrap h4:hover .anchor .octicon-link,
.mdViewerWrap h5:hover .anchor .octicon-link,
.mdViewerWrap h6:hover .anchor .octicon-link {
visibility: visible;
}
.mdViewerWrap h1 tt,
.mdViewerWrap h1 code,
.mdViewerWrap h2 tt,
.mdViewerWrap h2 code,
.mdViewerWrap h3 tt,
.mdViewerWrap h3 code,
.mdViewerWrap h4 tt,
.mdViewerWrap h4 code,
.mdViewerWrap h5 tt,
.mdViewerWrap h5 code,
.mdViewerWrap h6 tt,
.mdViewerWrap h6 code {
padding: 0 0.2em;
font-size: inherit;
}
.mdViewerWrap summary h1,
.mdViewerWrap summary h2,
.mdViewerWrap summary h3,
.mdViewerWrap summary h4,
.mdViewerWrap summary h5,
.mdViewerWrap summary h6 {
display: inline-block;
}
.mdViewerWrap summary h1 .anchor,
.mdViewerWrap summary h2 .anchor,
.mdViewerWrap summary h3 .anchor,
.mdViewerWrap summary h4 .anchor,
.mdViewerWrap summary h5 .anchor,
.mdViewerWrap summary h6 .anchor {
margin-left: -40px;
}
.mdViewerWrap summary h1,
.mdViewerWrap summary h2 {
padding-bottom: 0;
border-bottom: 0;
}
.mdViewerWrap ul.no-list,
.mdViewerWrap ol.no-list {
padding: 0;
list-style-type: none;
}
.mdViewerWrap ol[type="a s"] {
list-style-type: lower-alpha;
}
.mdViewerWrap ol[type="A s"] {
list-style-type: upper-alpha;
}
.mdViewerWrap ol[type="i s"] {
list-style-type: lower-roman;
}
.mdViewerWrap ol[type="I s"] {
list-style-type: upper-roman;
}
.mdViewerWrap ol[type="1"] {
list-style-type: decimal;
}
.mdViewerWrap div > ol:not([type]) {
list-style-type: decimal;
}
.mdViewerWrap ul ul,
.mdViewerWrap ul ol,
.mdViewerWrap ol ol,
.mdViewerWrap ol ul {
margin-top: 0;
margin-bottom: 0;
}
.mdViewerWrap li > p {
margin-top: 16px;
}
.mdViewerWrap li + li {
margin-top: 0.25em;
}
.mdViewerWrap dl {
padding: 0;
}
.mdViewerWrap dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: 600;
}
.mdViewerWrap dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
.mdViewerWrap table th {
font-weight: 600;
}
.mdViewerWrap table th,
.mdViewerWrap table td {
padding: 6px 13px;
border: 1px solid #d0d7de;
}
.mdViewerWrap table td > :last-child {
margin-bottom: 0;
}
.mdViewerWrap table tr {
background-color: #ffffff;
border-top: 1px solid #d0d7deb3;
}
.mdViewerWrap table tr:nth-child(2n) {
background-color: #f6f8fa;
}
.mdViewerWrap table img {
background-color: transparent;
}
.mdViewerWrap img[align="right"] {
padding-left: 20px;
}
.mdViewerWrap img[align="left"] {
padding-right: 20px;
}
.mdViewerWrap .emoji {
max-width: none;
vertical-align: text-top;
background-color: transparent;
}
.mdViewerWrap span.frame {
display: block;
overflow: hidden;
}
.mdViewerWrap span.frame > span {
display: block;
float: left;
width: auto;
padding: 7px;
margin: 13px 0 0;
overflow: hidden;
border: 1px solid #d0d7de;
}
.mdViewerWrap span.frame span img {
display: block;
float: left;
}
.mdViewerWrap span.frame span span {
display: block;
padding: 5px 0 0;
clear: both;
color: #1f2328;
}
.mdViewerWrap span.align-center {
display: block;
overflow: hidden;
clear: both;
}
.mdViewerWrap span.align-center > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: center;
}
.mdViewerWrap span.align-center span img {
margin: 0 auto;
text-align: center;
}
.mdViewerWrap span.align-right {
display: block;
overflow: hidden;
clear: both;
}
.mdViewerWrap span.align-right > span {
display: block;
margin: 13px 0 0;
overflow: hidden;
text-align: right;
}
.mdViewerWrap span.align-right span img {
margin: 0;
text-align: right;
}
.mdViewerWrap span.float-left {
display: block;
float: left;
margin-right: 13px;
overflow: hidden;
}
.mdViewerWrap span.float-left span {
margin: 13px 0 0;
}
.mdViewerWrap span.float-right {
display: block;
float: right;
margin-left: 13px;
overflow: hidden;
}
.mdViewerWrap span.float-right > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: right;
}
.mdViewerWrap code,
.mdViewerWrap tt {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
white-space: break-spaces;
background-color: #afb8c133;
border-radius: 6px;
}
.mdViewerWrap code br,
.mdViewerWrap tt br {
display: none;
}
.mdViewerWrap del code {
text-decoration: inherit;
}
.mdViewerWrap samp {
font-size: 85%;
}
.mdViewerWrap pre code {
font-size: 100%;
}
.mdViewerWrap pre > code {
padding: 0;
margin: 0;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.mdViewerWrap .highlight {
margin-bottom: 16px;
}
.mdViewerWrap .highlight pre {
margin-bottom: 0;
word-break: normal;
}
.mdViewerWrap .highlight pre,
.mdViewerWrap pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
color: #1f2328;
background-color: #f6f8fa;
border-radius: 6px;
}
.mdViewerWrap pre code,
.mdViewerWrap pre tt {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
.mdViewerWrap .csv-data td,
.mdViewerWrap .csv-data th {
padding: 5px;
overflow: hidden;
font-size: 12px;
line-height: 1;
text-align: left;
white-space: nowrap;
}
.mdViewerWrap .csv-data .blob-num {
padding: 10px 8px 9px;
text-align: right;
background: #ffffff;
border: 0;
}
.mdViewerWrap .csv-data tr {
border-top: 0;
}
.mdViewerWrap .csv-data th {
font-weight: 600;
background: #f6f8fa;
border-top: 0;
}
.mdViewerWrap [data-footnote-ref]::before {
content: "[";
}
.mdViewerWrap [data-footnote-ref]::after {
content: "]";
}
.mdViewerWrap .footnotes {
font-size: 12px;
color: #636c76;
border-top: 1px solid #d0d7de;
}
.mdViewerWrap .footnotes ol {
padding-left: 16px;
}
.mdViewerWrap .footnotes ol ul {
display: inline-block;
padding-left: 16px;
margin-top: 16px;
}
.mdViewerWrap .footnotes li {
position: relative;
}
.mdViewerWrap .footnotes li:target::before {
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -24px;
pointer-events: none;
content: "";
border: 2px solid #0969da;
border-radius: 6px;
}
.mdViewerWrap .footnotes li:target {
color: #1f2328;
}
.mdViewerWrap .footnotes .data-footnote-backref g-emoji {
font-family: monospace;
}
.mdViewerWrap .pl-c {
color: #57606a;
}
.mdViewerWrap .pl-c1,
.mdViewerWrap .pl-s .pl-v {
color: #0550ae;
}
.mdViewerWrap .pl-e,
.mdViewerWrap .pl-en {
color: #6639ba;
}
.mdViewerWrap .pl-smi,
.mdViewerWrap .pl-s .pl-s1 {
color: #24292f;
}
.mdViewerWrap .pl-ent {
color: #0550ae;
}
.mdViewerWrap .pl-k {
color: #cf222e;
}
.mdViewerWrap .pl-s,
.mdViewerWrap .pl-pds,
.mdViewerWrap .pl-s .pl-pse .pl-s1,
.mdViewerWrap .pl-sr,
.mdViewerWrap .pl-sr .pl-cce,
.mdViewerWrap .pl-sr .pl-sre,
.mdViewerWrap .pl-sr .pl-sra {
color: #0a3069;
}
.mdViewerWrap .pl-v,
.mdViewerWrap .pl-smw {
color: #953800;
}
.mdViewerWrap .pl-bu {
color: #82071e;
}
.mdViewerWrap .pl-ii {
color: #f6f8fa;
background-color: #82071e;
}
.mdViewerWrap .pl-c2 {
color: #f6f8fa;
background-color: #cf222e;
}
.mdViewerWrap .pl-sr .pl-cce {
font-weight: bold;
color: #116329;
}
.mdViewerWrap .pl-ml {
color: #3b2300;
}
.mdViewerWrap .pl-mh,
.mdViewerWrap .pl-mh .pl-en,
.mdViewerWrap .pl-ms {
font-weight: bold;
color: #0550ae;
}
.mdViewerWrap .pl-mi {
font-style: italic;
color: #24292f;
}
.mdViewerWrap .pl-mb {
font-weight: bold;
color: #24292f;
}
.mdViewerWrap .pl-md {
color: #82071e;
background-color: #ffebe9;
}
.mdViewerWrap .pl-mi1 {
color: #116329;
background-color: #dafbe1;
}
.mdViewerWrap .pl-mc {
color: #953800;
background-color: #ffd8b5;
}
.mdViewerWrap .pl-mi2 {
color: #eaeef2;
background-color: #0550ae;
}
.mdViewerWrap .pl-mdr {
font-weight: bold;
color: #8250df;
}
.mdViewerWrap .pl-ba {
color: #57606a;
}
.mdViewerWrap .pl-sg {
color: #8c959f;
}
.mdViewerWrap .pl-corl {
text-decoration: underline;
color: #0a3069;
}
.mdViewerWrap [role="button"]:focus:not(:focus-visible),
.mdViewerWrap [role="tabpanel"][tabindex="0"]:focus:not(:focus-visible),
.mdViewerWrap button:focus:not(:focus-visible),
.mdViewerWrap summary:focus:not(:focus-visible),
.mdViewerWrap a:focus:not(:focus-visible) {
outline: none;
box-shadow: none;
}
.mdViewerWrap [tabindex="0"]:focus:not(:focus-visible),
.mdViewerWrap details-dialog:focus:not(:focus-visible) {
outline: none;
}
.mdViewerWrap g-emoji {
display: inline-block;
min-width: 1ch;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1em;
font-style: normal !important;
font-weight: 400;
line-height: 1;
vertical-align: -0.075em;
}
.mdViewerWrap g-emoji img {
width: 1em;
height: 1em;
}
.mdViewerWrap .task-list-item {
list-style-type: none;
}
.mdViewerWrap .task-list-item label {
font-weight: 400;
}
.mdViewerWrap .task-list-item.enabled label {
cursor: pointer;
}
.mdViewerWrap .task-list-item + .task-list-item {
margin-top: 0.25rem;
}
.mdViewerWrap .task-list-item .handle {
display: none;
}
.mdViewerWrap .task-list-item-checkbox {
margin: 0 0.2em 0.25em -1.4em;
vertical-align: middle;
}
.mdViewerWrap .contains-task-list:dir(rtl) .task-list-item-checkbox {
margin: 0 -1.6em 0.25em 0.2em;
}
.mdViewerWrap .contains-task-list {
position: relative;
}
.mdViewerWrap .contains-task-list:hover .task-list-item-convert-container,
.mdViewerWrap
.contains-task-list:focus-within
.task-list-item-convert-container {
display: block;
width: auto;
height: 24px;
overflow: visible;
clip: auto;
}
.mdViewerWrap ::-webkit-calendar-picker-indicator {
filter: invert(50%);
}
.mdViewerWrap .markdown-alert {
padding: 0.5rem 1rem;
margin-bottom: 1rem;
color: inherit;
border-left: 0.25em solid #d0d7de;
}
.mdViewerWrap .markdown-alert > :first-child {
margin-top: 0;
}
.mdViewerWrap .markdown-alert > :last-child {
margin-bottom: 0;
}
.mdViewerWrap .markdown-alert .markdown-alert-title {
display: flex;
font-weight: 500;
align-items: center;
line-height: 1;
}
.mdViewerWrap .markdown-alert.markdown-alert-note {
border-left-color: #0969da;
}
.mdViewerWrap .markdown-alert.markdown-alert-note .markdown-alert-title {
color: #0969da;
}
.mdViewerWrap .markdown-alert.markdown-alert-important {
border-left-color: #8250df;
}
.mdViewerWrap .markdown-alert.markdown-alert-important .markdown-alert-title {
color: #8250df;
}
.mdViewerWrap .markdown-alert.markdown-alert-warning {
border-left-color: #bf8700;
}
.mdViewerWrap .markdown-alert.markdown-alert-warning .markdown-alert-title {
color: #9a6700;
}
.mdViewerWrap .markdown-alert.markdown-alert-tip {
border-left-color: #1a7f37;
}
.mdViewerWrap .markdown-alert.markdown-alert-tip .markdown-alert-title {
color: #1a7f37;
}
.mdViewerWrap .markdown-alert.markdown-alert-caution {
border-left-color: #cf222e;
}
.mdViewerWrap .markdown-alert.markdown-alert-caution .markdown-alert-title {
color: #d1242f;
}
.mdViewerWrap > *:first-child > .heading-element:first-child {
margin-top: 0 !important;
}
import { useEffect, useRef, useState } from "react";
import { Tooltip } from "antd";
import cls from "classnames";
import styles from "./index.module.scss";
import { useDeepCompareEffect, useHover } from "ahooks";
import IconFont from "@/components/icon-font";
import { downloadFileUseAScript } from "@/utils/download";
import { MD_DRIVE_PDF } from "@/constant/event";
import { useIntl } from "react-intl";
import LazyUrlMarkdown from "../url-markdown";
import exitFullScreenSvg from "@/assets/pdf/exitFullScreen.svg";
import fullScreenSvg from "@/assets/pdf/fullScreen.svg";
import { MD_PREVIEW_TYPE } from "@/types/extract-task-type";
import _ from "lodash";
import { TaskIdResItem } from "@/api/extract";
import useMdStore from "@/store/mdStore";
import CodeMirror from "@/components/code-mirror";
import { useParams } from "react-router-dom";
import SaveStatus, { SaveStatusRef } from "@/components/SaveStatus";
interface IMdViewerProps {
md?: string;
className?: string;
filename?: string;
url?: string;
taskInfo: TaskIdResItem;
curPage: number;
fullScreen?: boolean;
setFullScreen?: (value?: boolean) => void;
}
const MdViewer: React.FC<IMdViewerProps> = ({
fullScreen,
setFullScreen,
taskInfo,
className = "",
curPage,
}) => {
const mdViewerPef = useRef<HTMLDivElement>(null);
const url = taskInfo?.fullMdLink || "";
const containerRef = useRef<HTMLDivElement>(null);
const isHovering = useHover(containerRef);
const { formatMessage } = useIntl();
const [displayType, setDisplayType] = useState(MD_PREVIEW_TYPE.preview);
const params = useParams();
const {
setAllMdContentWithAnchor,
allMdContentWithAnchor,
setMdUrlArr,
mdContents,
updateMdContent,
} = useMdStore();
const [lineWrap, setLineWrap] = useState(false);
const threshold = 562 - 427;
const statusRef = useRef<SaveStatusRef>(null);
const menuList = [
{
name: formatMessage({ id: "extractor.markdown.preview" }),
code: MD_PREVIEW_TYPE.preview,
},
{
name: formatMessage({ id: "extractor.markdown.code" }),
code: MD_PREVIEW_TYPE.code,
},
];
const getVisibleFromType = (str: string, type: string) => {
return str === type
? "relative w-full h-full"
: "w-0 h-0 overflow-hidden hidden";
};
const pushMdViewerScroll = (scrollType?: "instant" | "smooth") => {
const container = document.getElementById(`md-container`);
// md渲染的时候用一个元素包括anchor
const element =
displayType === MD_PREVIEW_TYPE.preview
? document.getElementById(`md-anchor-${curPage - 1}`)?.parentElement
: document.getElementById(`code-${curPage - 1}`);
if (element && container) {
container.scrollTo({
top: element.offsetTop - 124,
behavior: scrollType || "smooth",
});
}
};
useEffect(() => {
if (isHovering) return;
pushMdViewerScroll();
}, [curPage, isHovering]);
useEffect(() => {
pushMdViewerScroll("instant");
}, [displayType]);
useEffect(() => {
if (!isHovering) return;
const handleScroll = () => {
if (!containerRef.current) return;
taskInfo?.markdownUrl?.forEach((page, index) => {
const element =
displayType === MD_PREVIEW_TYPE.preview
? document.getElementById(`md-anchor-${index}`)?.parentElement
: document.getElementById(`code-${index}`);
if (element) {
const rect = element.getBoundingClientRect();
if (rect.top <= threshold) {
document.dispatchEvent(
new CustomEvent(MD_DRIVE_PDF, {
detail: index,
})
);
}
}
});
};
const container = containerRef.current;
if (container) {
container.addEventListener("scroll", handleScroll);
}
return () => {
if (container) {
container?.removeEventListener("scroll", handleScroll);
}
};
}, [taskInfo, isHovering, displayType]);
useDeepCompareEffect(() => {
if (taskInfo?.markdownUrl) {
setMdUrlArr(taskInfo?.markdownUrl);
}
statusRef?.current?.reset();
}, [taskInfo?.markdownUrl, params?.jobID]);
const handleContentChange = (val: string, index: number) => {
setAllMdContentWithAnchor(val);
statusRef?.current?.triggerSave();
if (taskInfo?.file_key) {
updateMdContent(taskInfo.file_key!, index, val);
}
};
return (
<div className={cls(className)} ref={mdViewerPef}>
<div
className={cls(
"h-[49px] px-6 border-0 border-solid border-b-[1px] border-[#EBECF0] w-full pl-[24px] flex justify-between items-center"
)}
>
<ul className="p-1 list-none mb-0 inline-block rounded-sm mr-auto bg-[#F4F5F9] select-none">
{menuList.map((item) => (
<li
key={item.code}
className={`mx-[0.125rem] px-2 leading-[25px] inline-block rounded-sm text-[14px] cursor-pointer text-color ${
displayType === item.code && "bg-white text-primary"
}`}
onClick={() => setDisplayType(item.code)}
>
{item.name}
</li>
))}
</ul>
<SaveStatus ref={statusRef} />
{displayType === "code" && (
<>
<Tooltip
title={
fullScreen
? formatMessage({ id: "extractor.button.lineWrap" })
: formatMessage({
id: "extractor.button.lineWrap",
})
}
>
<IconFont
type="icon-line-wrap"
className={cls(
"text-lg text-[#464a53] leading-0 ml-[1rem] cursor-pointer hover:bg-[#F4F5F9] p-1 rounded",
lineWrap && "!text-[#0D53DE]"
)}
onClick={() => setLineWrap?.(!lineWrap)}
/>
</Tooltip>
<span className="w-[1px] h-[0.75rem] bg-[#D7D8DD] mx-[1rem]"></span>
</>
)}
<Tooltip
title={
fullScreen
? formatMessage({ id: "extractor.button.exitFullScreen" })
: formatMessage({
id: "extractor.button.fullScreen",
})
}
>
<span
className="cursor-pointer w-[1.5rem] user-select-none flex items-center justify-center h-[1.5rem] hover:bg-[#F4F5F9] rounded "
onClick={() => setFullScreen?.(!fullScreen)}
>
{!fullScreen ? (
<img
className=" w-[1.125rem] h-[1.125rem] "
src={fullScreenSvg}
/>
) : (
<img
className=" w-[1.125rem] h-[1.125rem] "
src={exitFullScreenSvg}
/>
)}
</span>
</Tooltip>
<span className="w-[1px] h-[0.75rem] bg-[#D7D8DD] ml-[1rem]"></span>
<Tooltip title={formatMessage({ id: "extractor.button.download" })}>
<IconFont
type="icon-xiazai"
className="text-lg text-[#464a53] leading-0 ml-[1rem] cursor-pointer hover:bg-[#F4F5F9] p-1 rounded"
onClick={() =>
downloadFileUseAScript(
url,
`${_(taskInfo?.fileName).split(".").slice(0, -1).join(".")}.md`
)
}
/>
</Tooltip>
</div>
<div
className={cls(
"bg-white !h-[calc(100%-60px)] px-6 py-8 overflow-auto w-full max-w-[100%]",
styles.scrollBar
)}
id="md-container"
ref={containerRef}
>
<div
className={cls(
getVisibleFromType(displayType, MD_PREVIEW_TYPE.preview)
)}
>
<LazyUrlMarkdown
markdownClass={"relative"}
content={allMdContentWithAnchor}
/>
</div>
<div
className={cls(getVisibleFromType(displayType, MD_PREVIEW_TYPE.code))}
>
{taskInfo?.markdownUrl?.map((url: string, index: number) => {
const md = mdContents[url]?.content || "";
if (!md) return null;
return (
<div key={url} id={`code-${index}`} className="opacity-1 z-[-1]">
<CodeMirror
value={md}
lineWrapping={lineWrap}
onChange={(val) => handleContentChange(val, index)}
editable
className="w-full h-full"
/>
</div>
);
})}
</div>
</div>
</div>
);
};
export default MdViewer;
# 欢迎来到我的博客
这是我的个人博客,我会在这里分享我的想法、经验和学习笔记。
## 关于我
我是一名热爱技术的程序员,专注于 Web 开发领域。我喜欢探索新的技术和框架,并不断学习和提升自己的技能。
在这个博客中,你可以找到以下内容:
- 技术文章和教程
- 个人项目分享
- 学习笔记和总结
- 生活点滴和随笔
如果你对我的文章感兴趣,欢迎留言交流和讨论。
## 最新文章
### 1. 如何使用 React Hooks 优化组件性能
在这篇文章中,我将介绍 React Hooks 的基本概念,并通过示例说明如何使用 Hooks 来优化组件的性能。我们会讨论以下内容:
- useState 和 useEffect 的使用
- useCallback 和 useMemo 的性能优化技巧
- 自定义 Hooks 的创建和使用
通过学习和应用这些技巧,你可以编写出更加高效和可维护的 React 组件。
### 2. 探索 Node.js 的异步编程
Node.js 以其非阻塞的异步编程模型而闻名。在这篇文章中,我们将深入探讨 Node.js 中的异步编程概念和技巧。主要内容包括:
- 回调函数的使用
- Promise 的链式调用和错误处理
- async/await 的使用和优势
- 事件循环和异步 I/O
通过掌握这些异步编程技巧,你可以更好地利用 Node.js 的优势,编写出高性能和可扩展的应用程序。
## 联系我
如果你想与我联系,可以通过以下方式找到我:
- 邮件: example@example.com
- GitHub: [https://github.com/username](https://github.com/username)
- Twitter: [@username](https://twitter.com/username)
欢迎与我交流和分享你的想法!
---
# 第二页
## 我的项目
在这个部分,我将介绍一些我最近参与的个人项目。
### 1. ToDo 应用
这是一个简单的 ToDo 应用,使用 React 和 Firebase 实现。主要功能包括:
- 添加和删除任务
- 标记任务为已完成
- 实时同步和数据持久化
通过这个项目,我学习了 React 组件的基本开发和 Firebase 的实时数据库的使用。你可以在这个仓库中找到完整的源代码: [https://github.com/username/todo-app](https://github.com/username/todo-app)
### 2. 天气预报应用
这是一个基于 Node.js 和 Express 的天气预报应用。它使用第三方 API 获取天气数据,并以 Web 页面的形式展示给用户。主要功能包括:
- 根据用户输入的城市名获取天气信息
- 显示当前天气状况和未来几天的天气预报
- 支持多个城市的天气查询
通过这个项目,我学习了如何使用 Node.js 和 Express 构建 Web 应用,以及如何与外部 API 进行交互。你可以在这个仓库中找到完整的源代码: [https://github.com/username/weather-app](https://github.com/username/weather-app)
## 总结
这就是我的博客的前两页内容。我会不定期更新文章和分享我的项目。如果你对我的文章或项目感兴趣,欢迎留言交流。
如果你有任何建议或意见,也欢迎随时联系我。希望我的博客能给你带来一些有价值的信息和启发。
谢谢阅读!
import cls from "classnames";
import {
ExtractorUploadButton,
LinearButton,
} from "../pdf-upload-button/index";
import { useState } from "react";
import MdViewer from "../md-viewer";
import PDFViewerMemo from "../pdf-viewer";
import LoadingIcon from "../../components/loading-icon";
import emptySvg from "@/assets/svg/empty.svg";
import { useIntl, FormattedMessage } from "react-intl";
import { useJobExtraction } from "@/store/jobProgress";
import { postReUploadExtractTask } from "@/api/extract";
import { useParams } from "react-router-dom";
interface IPdfExtractionProps {
className?: string;
}
const PdfExtraction = ({ className = "" }: IPdfExtractionProps) => {
const {
refreshQueue,
taskInfo,
isLoading: queueLoading,
isError: compileError,
} = useJobExtraction();
const [pdfState, setPdfState] = useState({
page: 1,
});
const curPage = pdfState.page;
const { jobID } = useParams();
const { formatMessage } = useIntl();
const [fullScreen, setFullScreen] = useState<boolean>(false);
const afterUploadSuccess = () => {
refreshQueue();
};
const isQueueAndExtract = queueLoading;
const hiddenQueuePage = !isQueueAndExtract
? "!w-0 !h-0 overflow-hidden "
: "";
const hiddenResultPage = isQueueAndExtract ? "!w-0 !h-0 overflow-hidden" : "";
const getLayoutClassName = (_fullScreen?: boolean) => {
return {
left: _fullScreen ? "!w-0 !h-0 overflow-hidden hidden" : "min-w-[50%]",
right: _fullScreen ? "w-full " : "min-w-[50%]",
};
};
const afterAsyncCheck = async () => {
return Promise.resolve(true);
};
const getExtractionStatusText = (rank: number) => {
switch (true) {
case rank > 1:
return (
<FormattedMessage
id="extractor.common.extracting.queue"
values={{
id: taskInfo?.rank || 0,
}}
/>
);
case rank === 1:
return formatMessage({ id: "extractor.common.extracting" });
default:
return "";
}
};
return (
<>
<div
className={cls(
"flex flex-col items-center justify-center w-full h-full",
hiddenQueuePage
)}
>
<LoadingIcon className="w-12" color={"#0D53DE"} />
<div className="text-base text-[#121316]/[0.8] mt-4 min-h-6">
{getExtractionStatusText(taskInfo?.rank)}
</div>
</div>
<div className={cls("h-full w-full", className, hiddenResultPage)}>
{!compileError ? (
<div
className={cls(
"flex h-[calc(100%-16px)] relative grid ",
fullScreen ? "grid-cols-1" : "grid-cols-2"
)}
>
<div className={cls(getLayoutClassName(fullScreen).left)}>
<PDFViewerMemo
taskInfo={taskInfo}
onChange={(p) => setPdfState(p)}
/>
</div>
<div
className={cls(
"!overflow-auto",
getLayoutClassName(fullScreen).right
)}
style={{
borderLeft: "1px solid #EBECF0",
}}
>
<MdViewer
curPage={curPage}
taskInfo={taskInfo}
className="!h-full w-full flex flex-col flex-s"
fullScreen={fullScreen}
setFullScreen={(bool?: boolean) => setFullScreen(!!bool)}
/>
</div>
</div>
) : (
<div className="ml-[50%] translate-x-[-50%] !h-[calc(100%-70px)] flex-1 flex items-center h-[110px] flex-col justify-center">
<img src={emptySvg} alt="emptySvg" />
<span className="text-[#121316]/[0.8] mt-2">
{formatMessage({
id: "extractor.failed",
})}
</span>
<LinearButton
onClick={async () => {
await postReUploadExtractTask(String(jobID));
refreshQueue();
}}
className="mt-4"
text={formatMessage({
id: "common.retry",
})}
/>
</div>
)}
</div>
</>
);
};
export default PdfExtraction;
.gradientBtn {
width: 188px;
height: 37px;
border-radius: 8px;
font-size: 14px;
color: rgba(255, 255, 255, 0.95);
background: linear-gradient(110deg, #38A0FF -33.56%, #0D53DE 32.84%, #5246FF 102.05%);
// background: #3477EB;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background: #3477EB;
}
:global {
.ant-upload-list-item-container {
// display: none !important;
}
.ant-upload {
display: flex;
align-items: center;
justify-content: center;
width: 100% !important;
height: 100% !important;
line-height: 100% !important;
}
.ant-upload-drag {
border: none !important;
}
}
}
.linearBtn {
width: 188px;
height: 37px;
border-radius: 8px;
font-size: 14px;
color: rgba(255, 255, 255, 0.95);
background: linear-gradient(110deg, #38A0FF -33.56%, #0D53DE 32.84%, #5246FF 102.05%);
// background: #3477EB;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
color: rgba(255, 255, 255, 0.95) !important;
background: linear-gradient(110deg, #38A0FF -33.56%, #38A0FF 32.84%, #38A0FF 102.05%) !important;
}
&:active {
}
}
import { Button } from "antd";
import cls from "classnames";
import UploadingOutlined from "@/assets/imgs/online.experience/UploadingOutlined.svg";
import styles from "./index.module.scss";
import Upload from "@/components/upload";
import { ReactNode } from "react";
import {
postExtractTask,
postReUploadExtractTask,
SubmitRes,
} from "@/api/extract";
import { useParams } from "react-router-dom";
import { ExtractTaskType } from "@/types/extract-task-type";
interface IPdfUploadButtonProps {
afterUploadSuccess?: (submitTask: SubmitRes) => void;
afterAsyncCheck?: () => Promise<boolean>;
text?: string | ReactNode;
className?: string;
showIcon?: boolean;
beforeUpload?: () => void;
onUploadError?: () => void;
accept: string;
extractType: ExtractTaskType;
taskType?: string;
submitType?: "submit" | "reUpload";
isOcr?: boolean;
}
interface ILinearButtonProps {
className?: string;
text?: string | ReactNode;
onClick?: () => void;
}
export const LinearButton = ({
className = "",
onClick,
text,
}: ILinearButtonProps) => {
return (
<button
onClick={() => onClick?.()}
className={cls(styles.linearBtn, className)}
>
{text}
</button>
);
};
export const ExtractorUploadButton = ({
text = "上传PDF",
className = "",
afterAsyncCheck,
afterUploadSuccess,
beforeUpload: beforeLocalUpload,
onUploadError,
showIcon = true,
accept,
extractType,
taskType,
submitType,
isOcr,
}: IPdfUploadButtonProps) => {
const urlParams = useParams();
const beforeUpload = async () => {
beforeLocalUpload?.();
const isCheck = await afterAsyncCheck?.();
return isCheck;
};
const onChange = async (pdfFile: any) => {
if (pdfFile?.file?.status === "done") {
const res =
submitType === "reUpload"
? await postReUploadExtractTask(String(urlParams?.jobID))
: await postExtractTask({
fileKey: pdfFile?.file?.response?.data?.data?.file_key,
fileName: pdfFile?.file?.name,
taskType: extractType,
isOcr,
});
if (res) {
if (!("error" in res)) {
afterUploadSuccess?.({
...(res || {}),
type: extractType,
} as any);
} else {
onUploadError?.();
}
} else {
onUploadError?.();
}
}
};
return (
<>
<Upload
isDragger
accept={accept}
className={cls(styles.gradientBtn, "mb-4", className)}
beforeUpload={beforeUpload}
showUploadList={false}
onChange={onChange}
openRead={true}
taskType={taskType}
>
<div className="flex justify-center items-center ">
{showIcon && <img src={UploadingOutlined} className="mr-1" />}
{text}
</div>
</Upload>
</>
);
};
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment