feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
|
||||
import { Spin } from '@coze-arch/coze-design';
|
||||
|
||||
interface IPreviewMdProps {
|
||||
fileUrl: string;
|
||||
}
|
||||
|
||||
function wait(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export const PreviewMd = (props: IPreviewMdProps) => {
|
||||
const { fileUrl } = props;
|
||||
const [mdContent, setMdContent] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
fetch(fileUrl)
|
||||
.then(res => res.text())
|
||||
.then(text => {
|
||||
setLoading(false);
|
||||
setMdContent(text);
|
||||
});
|
||||
}, [fileUrl]);
|
||||
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function render() {
|
||||
if (ref.current) {
|
||||
for (
|
||||
let i = 0, len = mdContent.length;
|
||||
i < Math.ceil(len / 50_000);
|
||||
i++
|
||||
) {
|
||||
await wait(10);
|
||||
ref.current.textContent += mdContent.slice(
|
||||
i * 50_000,
|
||||
(i + 1) * 50_000,
|
||||
);
|
||||
}
|
||||
ref.current.textContent = mdContent;
|
||||
}
|
||||
}
|
||||
|
||||
render();
|
||||
}, [mdContent]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center w-full h-full flex-1 py-2 px-4">
|
||||
<Spin
|
||||
wrapperClassName="w-full h-full grow"
|
||||
spinning={loading}
|
||||
childStyle={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
{mdContent ? (
|
||||
<div>
|
||||
<pre
|
||||
className="max-w-full overflow-auto whitespace-pre-wrap break-all text-[14px] leading-[22px]"
|
||||
ref={ref}
|
||||
>
|
||||
{/* {mdContent} */}
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Spin } from '@coze-arch/coze-design';
|
||||
interface IPreviewTxtProps {
|
||||
fileUrl: string;
|
||||
}
|
||||
|
||||
function wait(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export const PreviewTxt = (props: IPreviewTxtProps) => {
|
||||
const { fileUrl } = props;
|
||||
const [txtContent, setTxtContent] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
fetch(fileUrl)
|
||||
.then(res => res.text())
|
||||
.then(text => {
|
||||
setLoading(false);
|
||||
setTxtContent(text);
|
||||
});
|
||||
}, [fileUrl]);
|
||||
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function render() {
|
||||
if (ref.current) {
|
||||
for (
|
||||
let i = 0, len = txtContent.length;
|
||||
i < Math.ceil(len / 50_000);
|
||||
i++
|
||||
) {
|
||||
await wait(10);
|
||||
if (ref.current) {
|
||||
ref.current.textContent += txtContent.slice(
|
||||
i * 50_000,
|
||||
(i + 1) * 50_000,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (ref.current) {
|
||||
ref.current.textContent = txtContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render();
|
||||
}, [txtContent]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center w-full h-full flex-1 py-2 px-4">
|
||||
<Spin
|
||||
wrapperClassName="w-full h-full grow"
|
||||
spinning={loading}
|
||||
childStyle={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
<pre
|
||||
className="max-w-full overflow-auto whitespace-pre-wrap break-all text-[14px] leading-[22px]"
|
||||
ref={ref}
|
||||
>
|
||||
{/* {txtContent} */}
|
||||
</pre>
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
import { Document, Page, pdfjs } from 'react-pdf';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { FixedSizeList as List, AutoSizer } from '@coze-common/virtual-list';
|
||||
import { Spin } from '@coze-arch/coze-design';
|
||||
import 'react-pdf/dist/Page/TextLayer.css';
|
||||
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
||||
|
||||
interface IUsePreviewPdfProps {
|
||||
fileUrl: string;
|
||||
}
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc =
|
||||
REGION === 'cn'
|
||||
? // cp-disable-next-line
|
||||
`//lf-cdn.coze.cn/obj/unpkg/pdfjs-dist/${pdfjs.version}/build/pdf.worker.min.mjs`
|
||||
: // cp-disable-next-line
|
||||
`//sf-cdn.coze.com/obj/unpkg-va/pdfjs-dist/${pdfjs.version}/build/pdf.worker.min.mjs`;
|
||||
|
||||
const options = {
|
||||
cMapUrl:
|
||||
REGION === 'cn'
|
||||
? // cp-disable-next-line
|
||||
`//lf-cdn.coze.cn/obj/unpkg/pdfjs-dist/${pdfjs.version}/cmaps/`
|
||||
: // cp-disable-next-line
|
||||
`//sf-cdn.coze.com/obj/unpkg-va/pdfjs-dist/${pdfjs.version}/cmaps/`,
|
||||
// 提升性能
|
||||
cMapPacked: true,
|
||||
};
|
||||
|
||||
export const usePreviewPdf = (props: IUsePreviewPdfProps) => {
|
||||
const { fileUrl } = props;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [numPages, setNumPages] = useState<number>(0);
|
||||
const [currentPage, setCurrentPage] = useState<number>(0);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const listRef = useRef<List>(null);
|
||||
const [pageHeight, setPageHeight] = useState(
|
||||
containerRef.current?.clientHeight,
|
||||
);
|
||||
const itemSize = Math.floor((pageHeight ?? 500) + 20);
|
||||
|
||||
const onNext = () => {
|
||||
if (currentPage < numPages - 1) {
|
||||
const nextPage = currentPage + 1;
|
||||
setCurrentPage(nextPage);
|
||||
listRef.current?.scrollToItem(nextPage, 'start');
|
||||
}
|
||||
};
|
||||
|
||||
const onBack = () => {
|
||||
if (currentPage > 0) {
|
||||
const prevPage = currentPage - 1;
|
||||
setCurrentPage(prevPage);
|
||||
listRef.current?.scrollToItem(prevPage, 'start');
|
||||
}
|
||||
};
|
||||
|
||||
const onDocumentLoadSuccess = ({
|
||||
numPages: totalPages,
|
||||
}: {
|
||||
numPages: number;
|
||||
}) => {
|
||||
setLoading(false);
|
||||
setNumPages(totalPages);
|
||||
};
|
||||
|
||||
const handleScroll = ({ scrollOffset }: { scrollOffset: number }) => {
|
||||
const newIndex = Math.floor(scrollOffset / itemSize);
|
||||
setCurrentPage(newIndex);
|
||||
};
|
||||
|
||||
const [scale, setScale] = useState(1);
|
||||
const increaseScale = () => {
|
||||
setScale(prevScale => Math.min(prevScale + 0.1, 2));
|
||||
};
|
||||
const decreaseScale = () => {
|
||||
setScale(prevScale => Math.max(prevScale - 0.1, 0.5));
|
||||
};
|
||||
|
||||
const memoizedList = useMemo(
|
||||
() =>
|
||||
!loading ? (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
height={height ?? 0}
|
||||
itemCount={numPages}
|
||||
itemSize={itemSize}
|
||||
width={width ?? 0}
|
||||
onScroll={handleScroll}
|
||||
ref={listRef}
|
||||
>
|
||||
{({
|
||||
index,
|
||||
style,
|
||||
}: {
|
||||
index: number;
|
||||
style: React.CSSProperties;
|
||||
}) => (
|
||||
<div key={`page_${index + 1}`} style={style}>
|
||||
<Page
|
||||
pageNumber={index + 1}
|
||||
className={cls(
|
||||
'flex items-center justify-center !coz-bg-primary',
|
||||
)}
|
||||
width={(width ?? 100) - 32}
|
||||
scale={scale}
|
||||
onLoadSuccess={page => {
|
||||
setPageHeight(page.height);
|
||||
}}
|
||||
loading={
|
||||
<div
|
||||
style={{
|
||||
height: containerRef.current?.clientHeight,
|
||||
}}
|
||||
></div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
</AutoSizer>
|
||||
) : null,
|
||||
[loading, numPages, pageHeight, scale, itemSize],
|
||||
);
|
||||
|
||||
const pdfNode = (
|
||||
<>
|
||||
<div className="flex flex-col items-center w-full h-full relative coz-bg-primary">
|
||||
<div
|
||||
className={cls(
|
||||
'absolute top-0 left-0 right-0 bot-0 flex items-center justify-center h-full',
|
||||
'z-10',
|
||||
!loading && 'invisible',
|
||||
)}
|
||||
>
|
||||
<Spin />
|
||||
</div>
|
||||
<div
|
||||
className={cls(
|
||||
'flex absolute top-0 left-0 right-0 overflow-auto px-5 w-full h-full justify-center',
|
||||
loading && 'invisible',
|
||||
)}
|
||||
ref={containerRef}
|
||||
>
|
||||
<Document
|
||||
file={fileUrl}
|
||||
// bug fix https://github.com/wojtekmaj/react-pdf/issues/974
|
||||
key={fileUrl}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
options={options}
|
||||
className={cls('flex w-full h-full')}
|
||||
loading={
|
||||
<Spin
|
||||
wrapperClassName="w-full h-full"
|
||||
childStyle={{
|
||||
width: containerRef.current?.clientWidth,
|
||||
height: '100%',
|
||||
}}
|
||||
></Spin>
|
||||
}
|
||||
>
|
||||
{containerRef.current ? memoizedList : null}
|
||||
</Document>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return {
|
||||
pdfNode,
|
||||
numPages,
|
||||
currentPage: currentPage + 1,
|
||||
onNext,
|
||||
onBack,
|
||||
scale,
|
||||
increaseScale,
|
||||
decreaseScale,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user