mirror of
https://github.com/opendatalab/MinerU.git
synced 2026-09-24 23:10:23 +08:00
Merge pull request #7 from LollipopsAndWine/feat/ade/dev
feat: conflict
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
+52
-9
@@ -1,12 +1,55 @@
|
||||
### 前端本地开发
|
||||
# MinerU web
|
||||
|
||||
1. 安装 nodejs 18 和 pnpm;
|
||||
```javascript
|
||||
npm install -g pnpm
|
||||
## Table of Contents
|
||||
- [Local Frontend Development](#local-frontend-development)
|
||||
- [Technology Stack](#technology-stack)
|
||||
|
||||
## Local Frontend Development
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 18.x
|
||||
- pnpm
|
||||
|
||||
### Installation Steps
|
||||
|
||||
1. Install Node.js 18
|
||||
- Visit the [Node.js official website](https://nodejs.org/) to download and install Node.js version 18.x
|
||||
|
||||
2. Install pnpm
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
3. Clone the repository
|
||||
```git clone https://github.com/opendatalab/MinerU
|
||||
cd ./projects/web
|
||||
```
|
||||
4. Install dependencies
|
||||
```
|
||||
pnpm install
|
||||
```
|
||||
5. Run the development server
|
||||
```
|
||||
pnpm run dev
|
||||
```
|
||||
6. ⚠️ Note: This command is for local development only, do not use for deployment!
|
||||
Open your browser and visit http://localhost:5173 (or another address output in the console)
|
||||
|
||||
7. Ensure that the backend service in ./projects/web_demo is running
|
||||
|
||||
8. If you encounter an error when executing `pnpm install`, you can switch to an alternative package manager.
|
||||
```
|
||||
npm install -g yarn
|
||||
yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
|
||||
## Building the Project
|
||||
```
|
||||
2. 执行 `pnpm install && pnpm run dev` 即可。⚠️ 注意:此命令仅用于本地开发,不要用于部署!
|
||||
3. build
|
||||
pnpm run build
|
||||
```
|
||||
1.pnpm run build
|
||||
2.npm run build
|
||||
```
|
||||
## Technology Stack
|
||||
- React
|
||||
- Tailwind CSS
|
||||
- typeScript
|
||||
- zustand
|
||||
- ahooks
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# MinerU web
|
||||
|
||||
|
||||
## 目录
|
||||
- [前端本地开发](#前端本地开发)
|
||||
- [技术栈](#技术栈)
|
||||
## 前端本地开发
|
||||
|
||||
### 前置条件
|
||||
- Node.js 18.x
|
||||
- pnpm
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. 安装 Node.js 18
|
||||
- 访问 [Node.js 官网](https://nodejs.org/) 下载并安装 Node.js 18.x 版本
|
||||
|
||||
2. 安装 pnpm
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
```
|
||||
3. 克隆仓库
|
||||
```
|
||||
1. git clone https://github.com/opendatalab/MinerU
|
||||
2. cd ./projects/web
|
||||
```
|
||||
|
||||
4. 安装依赖
|
||||
```
|
||||
pnpm install
|
||||
```
|
||||
|
||||
5. 运行开发服务器
|
||||
```
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
6. ⚠️ 注意:此命令仅用于本地开发,不要用于部署!
|
||||
打开浏览器访问 http://localhost:5173(或控制台输出的其他地址)
|
||||
构建项目
|
||||
要构建生产版本,请执行以下命令:
|
||||
|
||||
```
|
||||
pnpm run build
|
||||
```
|
||||
7. 请确保./projects/web_demo后端服务启动
|
||||
|
||||
8. 如果pnpm install执行error,可更换包管理器
|
||||
```
|
||||
npm install -g yarn
|
||||
yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
- React
|
||||
- Tailwind CSS
|
||||
- typeScript
|
||||
- zustand
|
||||
- ahooks
|
||||
@@ -95,6 +95,7 @@ export interface TaskIdResItem {
|
||||
type: ExtractTaskType | "unknown";
|
||||
state: "running" | "done" | "pending" | "failed" | "unknown";
|
||||
markdownUrl: string[];
|
||||
file_key?: string;
|
||||
}
|
||||
|
||||
export type TaskIdRes = TaskIdResItem[];
|
||||
@@ -166,3 +167,30 @@ export const localUpload = (file: File) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export interface UpdateMarkdownRequest {
|
||||
file_key: string;
|
||||
data: {
|
||||
[pageNumber: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateMarkdownResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const updateMarkdownContent = async (
|
||||
params: UpdateMarkdownRequest
|
||||
): Promise<UpdateMarkdownResponse | null> => {
|
||||
return axios
|
||||
.put<UpdateMarkdownResponse>("/api/v2/extract/markdown", params)
|
||||
.then((res) => {
|
||||
if (!res?.data?.error) {
|
||||
return res.data.data;
|
||||
} else {
|
||||
handleErrorMsg(res);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
forwardRef,
|
||||
} from "react";
|
||||
|
||||
interface SaveStatusProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface SaveStatusRef {
|
||||
triggerSave: () => void;
|
||||
}
|
||||
|
||||
const SaveStatus = forwardRef<SaveStatusRef, SaveStatusProps>(
|
||||
({ className }, ref) => {
|
||||
const [lastSaveTime, setLastSaveTime] = useState<Date | null>(null);
|
||||
const [showSaved, setShowSaved] = useState(false);
|
||||
const [timeSinceLastSave, setTimeSinceLastSave] = useState("");
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
triggerSave: () => {
|
||||
setLastSaveTime(new Date());
|
||||
setShowSaved(true);
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (showSaved) {
|
||||
const timer = setTimeout(() => {
|
||||
setShowSaved(false);
|
||||
}, 10000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [showSaved]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateTimeSinceLastSave = () => {
|
||||
if (lastSaveTime) {
|
||||
const now = new Date();
|
||||
const diffInMinutes = Math.floor(
|
||||
(now.getTime() - lastSaveTime.getTime()) / 60000
|
||||
);
|
||||
if (diffInMinutes > 0) {
|
||||
setTimeSinceLastSave(`${diffInMinutes} 分钟前`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const timer = setInterval(updateTimeSinceLastSave, 60000);
|
||||
updateTimeSinceLastSave(); // 立即更新一次
|
||||
return () => clearInterval(timer);
|
||||
}, [lastSaveTime]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{showSaved && <span>已保存</span>}
|
||||
{!showSaved && lastSaveTime && (
|
||||
<span>最近修改:{timeSinceLastSave}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default SaveStatus;
|
||||
@@ -17,6 +17,7 @@ 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;
|
||||
@@ -48,10 +49,12 @@ const MdViewer: React.FC<IMdViewerProps> = ({
|
||||
allMdContentWithAnchor,
|
||||
setMdUrlArr,
|
||||
mdContents,
|
||||
updateMdContent,
|
||||
} = useMdStore();
|
||||
const [lineWrap, setLineWrap] = useState(false);
|
||||
|
||||
const threshold = 562 - 427;
|
||||
const statusRef = useRef<SaveStatusRef>(null);
|
||||
|
||||
const menuList = [
|
||||
{
|
||||
@@ -137,8 +140,12 @@ const MdViewer: React.FC<IMdViewerProps> = ({
|
||||
}
|
||||
}, [taskInfo?.markdownUrl, params?.jobID]);
|
||||
|
||||
const handleContentChange = (val: string) => {
|
||||
const handleContentChange = (val: string, index: number) => {
|
||||
setAllMdContentWithAnchor(val);
|
||||
statusRef?.current?.triggerSave();
|
||||
if (taskInfo?.file_key) {
|
||||
updateMdContent(taskInfo.file_key!, index, val);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -161,12 +168,13 @@ const MdViewer: React.FC<IMdViewerProps> = ({
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<SaveStatus ref={statusRef} />
|
||||
{displayType === "code" && (
|
||||
<>
|
||||
<Tooltip
|
||||
title={
|
||||
fullScreen
|
||||
? formatMessage({ id: "extractor.button.exitFullScreen" })
|
||||
? formatMessage({ id: "extractor.button.lineWrap" })
|
||||
: formatMessage({
|
||||
id: "extractor.button.lineWrap",
|
||||
})
|
||||
@@ -253,7 +261,7 @@ const MdViewer: React.FC<IMdViewerProps> = ({
|
||||
<CodeMirror
|
||||
value={md}
|
||||
lineWrapping={lineWrap}
|
||||
onChange={handleContentChange}
|
||||
onChange={(val) => handleContentChange(val, index)}
|
||||
editable
|
||||
className="w-full h-full"
|
||||
/>
|
||||
|
||||
@@ -3,23 +3,24 @@
|
||||
import ErrorBoundary from "@/components/error-boundary";
|
||||
import styles from "./home.module.scss";
|
||||
import { SlotID, Path } from "@/constant/route";
|
||||
import { HashRouter, Routes, Route, Outlet } from "react-router-dom";
|
||||
import {
|
||||
BrowserRouter,
|
||||
Routes,
|
||||
Route,
|
||||
Outlet,
|
||||
Navigate,
|
||||
useLocation,
|
||||
HashRouter,
|
||||
} from "react-router-dom";
|
||||
import { ExtractorSide } from "./extract-side";
|
||||
import { LanguageProvider } from "@/context/language-provider";
|
||||
import PDFUpload from "@/pages/extract/components/pdf-upload";
|
||||
import PDFExtractionJob from "@/pages/extract/components/pdf-extraction";
|
||||
|
||||
// judge if the app has hydrated
|
||||
// const useHasHydrated = () => {
|
||||
// const [hasHydrated, setHasHydrated] = useState<boolean>(false);
|
||||
// useEffect(() => {
|
||||
// setHasHydrated(true);
|
||||
// }, []);
|
||||
// return hasHydrated;
|
||||
// };
|
||||
|
||||
export function WindowContent() {
|
||||
const location = useLocation();
|
||||
const isHome = location.pathname === Path.Home;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExtractorSide className={isHome ? styles["sidebar-show"] : ""} />
|
||||
@@ -31,13 +32,15 @@ export function WindowContent() {
|
||||
}
|
||||
|
||||
function Screen() {
|
||||
// if you do not need to use the renderContent for rendering router, you can use the other render function to interrupt before the renderContent
|
||||
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<div className="w-full h-full flex" id={SlotID.AppBody}>
|
||||
<Routes>
|
||||
<Route path="/" element={<WindowContent />}>
|
||||
<Route
|
||||
index
|
||||
element={<Navigate to="/OpenSourceTools/Extractor/PDF" replace />}
|
||||
/>
|
||||
<Route
|
||||
path="/OpenSourceTools/Extractor/PDF"
|
||||
element={<PDFUpload />}
|
||||
@@ -46,15 +49,13 @@ function Screen() {
|
||||
path="/OpenSourceTools/Extractor/PDF/:jobID"
|
||||
element={<PDFExtractionJob />}
|
||||
/>
|
||||
{/* <Route path="*" element={<PDFUpload />} /> */}
|
||||
<Route
|
||||
path="*"
|
||||
element={<Navigate to="/OpenSourceTools/Extractor/PDF" replace />}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
// <ExtractorSide className={isHome ? styles["sidebar-show"] : ""} />
|
||||
// <WindowContent className="flex-1">
|
||||
// <AppRoutes />
|
||||
// </WindowContent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -62,11 +63,6 @@ function Screen() {
|
||||
}
|
||||
|
||||
export function Home() {
|
||||
// leave this comment to check if the app has hydrated
|
||||
// if (!useHasHydrated()) {
|
||||
// return <LoadingAnimation />;
|
||||
// }
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<LanguageProvider>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// mdStore.ts
|
||||
import { create } from "zustand";
|
||||
import axios from "axios";
|
||||
import { updateMarkdownContent, UpdateMarkdownRequest } from "@/api/extract"; // 确保路径正确
|
||||
|
||||
interface MdContent {
|
||||
content: string;
|
||||
@@ -48,6 +49,11 @@ interface MdState {
|
||||
) => string;
|
||||
jumpToAnchor: (anchorId: string) => number;
|
||||
reset: () => void;
|
||||
updateMdContent: (
|
||||
fileKey: string,
|
||||
pageNumber: string | number,
|
||||
newContent: string
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
const MAX_CONCURRENT_REQUESTS = 2;
|
||||
@@ -122,7 +128,6 @@ const useMdStore = create<MdState>((set, get) => ({
|
||||
|
||||
const results = await fetchWithConcurrency(urls);
|
||||
|
||||
// 只有当这是最新的请求时,才更新状态
|
||||
if (get().currentRequestId === requestId) {
|
||||
const newMdContents: Record<string, MdContent> = {};
|
||||
results.forEach(([url, content]) => {
|
||||
@@ -191,6 +196,55 @@ const useMdStore = create<MdState>((set, get) => ({
|
||||
}
|
||||
return -1; // Anchor not found
|
||||
},
|
||||
|
||||
updateMdContent: async (
|
||||
fileKey: string,
|
||||
pageNumber: string,
|
||||
newContent: string
|
||||
) => {
|
||||
try {
|
||||
const params: UpdateMarkdownRequest = {
|
||||
file_key: fileKey,
|
||||
data: {
|
||||
[pageNumber]: newContent,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await updateMarkdownContent(params);
|
||||
|
||||
if (result && result.success) {
|
||||
// 更新本地状态
|
||||
set((state) => {
|
||||
const updatedMdContents = { ...state.mdContents };
|
||||
if (updatedMdContents[fileKey]) {
|
||||
updatedMdContents[fileKey] = {
|
||||
...updatedMdContents[fileKey],
|
||||
content: newContent,
|
||||
};
|
||||
}
|
||||
|
||||
// 重新计算 allMdContent 和 allMdContentWithAnchor
|
||||
const contentArray = Object.values(updatedMdContents).map(
|
||||
(content) => content.content
|
||||
);
|
||||
const newAllMdContent = state.getAllMdContent(contentArray);
|
||||
const newAllMdContentWithAnchor =
|
||||
state.getContentWithAnchors(contentArray);
|
||||
|
||||
return {
|
||||
mdContents: updatedMdContents,
|
||||
allMdContent: newAllMdContent,
|
||||
allMdContentWithAnchor: newAllMdContentWithAnchor,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
throw new Error("Failed to update Markdown content");
|
||||
}
|
||||
} catch (error) {
|
||||
set({ error: error as Error });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export default useMdStore;
|
||||
|
||||
Reference in New Issue
Block a user