Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
wangsen
MinerU
Commits
fdb6d661
Unverified
Commit
fdb6d661
authored
Sep 27, 2024
by
Xiaomeng Zhao
Committed by
GitHub
Sep 27, 2024
Browse files
Merge pull request #668 from LollipopsAndWine/dev
feat: 集成前端界面,配置一键启动
parents
93713047
5834b147
Changes
430
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
1291 additions
and
0 deletions
+1291
-0
projects/web/src/pages/extract/formula/formula-detail-left/index.tsx
...b/src/pages/extract/formula/formula-detail-left/index.tsx
+101
-0
projects/web/src/pages/extract/formula/formula-detail-right/index.tsx
.../src/pages/extract/formula/formula-detail-right/index.tsx
+162
-0
projects/web/src/pages/extract/formula/formula-detail/index.tsx
...ts/web/src/pages/extract/formula/formula-detail/index.tsx
+143
-0
projects/web/src/pages/extract/formula/formula-popover/index.module.scss
...c/pages/extract/formula/formula-popover/index.module.scss
+18
-0
projects/web/src/pages/extract/formula/formula-popover/index.tsx
...s/web/src/pages/extract/formula/formula-popover/index.tsx
+54
-0
projects/web/src/pages/extract/formula/formula-upload/index.module.scss
...rc/pages/extract/formula/formula-upload/index.module.scss
+87
-0
projects/web/src/pages/extract/formula/formula-upload/index.tsx
...ts/web/src/pages/extract/formula/formula-upload/index.tsx
+132
-0
projects/web/src/pages/extract/formula/index.module.scss
projects/web/src/pages/extract/formula/index.module.scss
+0
-0
projects/web/src/pages/extract/formula/index.tsx
projects/web/src/pages/extract/formula/index.tsx
+11
-0
projects/web/src/pages/extract/index.module.scss
projects/web/src/pages/extract/index.module.scss
+0
-0
projects/web/src/pages/extract/index.tsx
projects/web/src/pages/extract/index.tsx
+0
-0
projects/web/src/pages/extract/table/index.tsx
projects/web/src/pages/extract/table/index.tsx
+5
-0
projects/web/src/pages/extract/table/table-detail/index.tsx
projects/web/src/pages/extract/table/table-detail/index.tsx
+5
-0
projects/web/src/pages/home.module.scss
projects/web/src/pages/home.module.scss
+0
-0
projects/web/src/pages/home.tsx
projects/web/src/pages/home.tsx
+75
-0
projects/web/src/routes/index.tsx
projects/web/src/routes/index.tsx
+17
-0
projects/web/src/store/jobProgress.ts
projects/web/src/store/jobProgress.ts
+183
-0
projects/web/src/store/languageStore.ts
projects/web/src/store/languageStore.ts
+47
-0
projects/web/src/store/mdStore.ts
projects/web/src/store/mdStore.ts
+250
-0
projects/web/src/styles/variable.scss
projects/web/src/styles/variable.scss
+1
-0
No files found.
projects/web/src/pages/extract/formula/formula-detail-left/index.tsx
0 → 100644
View file @
fdb6d661
import
showLayerIcon
from
"
@/assets/pdf/extractor-show-layer.svg
"
;
import
hiddenLayerIcon
from
"
@/assets/pdf/extractor-hidden-layer.svg
"
;
import
{
useRef
,
useState
}
from
"
react
"
;
import
IconFont
from
"
@/components/icon-font
"
;
import
ImageLayerViewer
,
{
ImageLayerViewerRef
,
}
from
"
../../components/image-layer-viwer
"
;
import
{
useUpdate
}
from
"
ahooks
"
;
import
{
TaskIdProgress
,
TaskIdResItem
}
from
"
@/api/extract
"
;
import
{
Tooltip
}
from
"
antd
"
;
import
{
useIntl
}
from
"
react-intl
"
;
interface
IFormulaDetailLeftProps
{
taskInfo
:
TaskIdProgress
&
TaskIdResItem
;
}
const
FormulaDetailLeft
=
({
taskInfo
}:
IFormulaDetailLeftProps
)
=>
{
const
imageRef
=
useRef
<
ImageLayerViewerRef
>
(
null
);
const
{
formatMessage
}
=
useIntl
();
const
[
layerVisible
,
setLayerVisible
]
=
useState
(
true
);
const
update
=
useUpdate
();
if
(
!
taskInfo
?.
fileInfo
?.
height
||
!
taskInfo
?.
fileInfo
?.
width
)
{
console
.
info
(
"
formula extractor interface error: the picture size is invalid
"
);
}
return
(
<
div
className
=
"w-full h-full"
>
<
div
className
=
{
`flex border-solid border-0 !border-b-[1px] border-[#EBECF0] items-center px-4 h-[48px]`
}
>
<
Tooltip
title
=
{
<>
{
layerVisible
?
formatMessage
({
id
:
"
extractor.button.hiddenLayer
"
,
})
:
formatMessage
({
id
:
"
extractor.button.showLayer
"
,
})
}
</>
}
>
<
span
className
=
"ml-auto mr-2 cursor-pointer hover:bg-[#f4f5f9] w-6 text-center inline-block rounded leading-normal"
onClick
=
{
()
=>
setLayerVisible
(
!
layerVisible
)
}
>
{
taskInfo
?.
type
===
"
formula-detect
"
?
null
:
layerVisible
?
(
<
img
src
=
{
hiddenLayerIcon
}
alt
=
"Hide Layer"
/>
)
:
(
<
img
src
=
{
showLayerIcon
}
alt
=
"Show Layer"
/>
)
}
</
span
>
</
Tooltip
>
{
taskInfo
?.
type
===
"
formula-detect
"
?
null
:
(
<
span
className
=
"w-[1px] leading-normal h-[12px] bg-[#D7D8DD] mr-1"
></
span
>
)
}
<
div
className
=
"select-none w-[7.8rem] flex justify-center"
>
<
IconFont
className
=
"rounded mx-2 cursor-pointer hover:bg-[#F4F5F9]"
type
=
"icon-SubtractOutlined"
onClick
=
{
()
=>
{
imageRef
?.
current
?.
zoomOut
();
}
}
/>
<
span
className
=
"mx-2"
>
{
((
imageRef
?.
current
?.
scale
||
0
)
*
100
||
1
).
toFixed
(
0
)
}
%
</
span
>
<
IconFont
className
=
"rounded mx-2 cursor-pointer hover:bg-[#F4F5F9]"
type
=
"icon-PlusOutlined"
onClick
=
{
()
=>
{
imageRef
?.
current
?.
zoomIn
();
}
}
/>
</
div
>
</
div
>
<
ImageLayerViewer
imageHeight
=
{
taskInfo
?.
fileInfo
?.
height
||
0
}
imageWidth
=
{
taskInfo
?.
fileInfo
?.
width
||
0
}
layout
=
{
taskInfo
.
content
as
any
[]
}
ref
=
{
imageRef
}
onChange
=
{
()
=>
{
// imageRef?.current?.scale为了这个更新
update
();
}
}
className
=
{
"
!h-[calc(100%-48px)]
"
}
layerVisible
=
{
taskInfo
?.
type
===
"
formula-detect
"
?
false
:
layerVisible
}
imageUrl
=
{
taskInfo
?.
url
}
/>
</
div
>
);
};
export
default
FormulaDetailLeft
;
projects/web/src/pages/extract/formula/formula-detail-right/index.tsx
0 → 100644
View file @
fdb6d661
import
ImageLayerViewer
from
"
../../components/image-layer-viwer
"
;
import
exitFullScreenSvg
from
"
@/assets/pdf/exitFullScreen.svg
"
;
import
fullScreenSvg
from
"
@/assets/pdf/fullScreen.svg
"
;
import
{
Tooltip
}
from
"
antd
"
;
import
{
useIntl
}
from
"
react-intl
"
;
import
{
TaskIdProgress
,
TaskIdResItem
}
from
"
@/api/extract
"
;
import
IconFont
from
"
@/components/icon-font
"
;
import
{
CopyToClipboard
}
from
"
react-copy-to-clipboard
"
;
import
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
"
react
"
;
import
{
message
}
from
"
antd
"
;
import
{
MD_PREVIEW_TYPE
}
from
"
@/types/extract-task-type
"
;
import
CodeMirror
from
"
@/components/code-mirror
"
;
import
LatexRenderer
from
"
../../components/latex-renderer
"
;
import
{
useParams
}
from
"
react-router-dom
"
;
interface
IImageOriginViewerProps
{
fullScreen
?:
boolean
;
setFullScreen
?:
(
val
:
boolean
)
=>
void
;
taskInfo
:
TaskIdProgress
&
TaskIdResItem
;
}
const
FormulaDetailRight
=
({
fullScreen
,
setFullScreen
,
taskInfo
,
}:
IImageOriginViewerProps
)
=>
{
const
CONTROL_BAR_HEIGHT
=
48
;
const
{
formatMessage
}
=
useIntl
();
const
[
displayType
,
setDisplayType
]
=
useState
(
MD_PREVIEW_TYPE
.
preview
);
const
imageViewerRef
=
useRef
<
any
>
();
const
formulaType
=
taskInfo
?.
type
;
const
params
=
useParams
();
const
jobID
=
params
?.
jobID
;
const
formulaLateX
=
useMemo
(()
=>
{
return
taskInfo
?.
content
?.
map
((
i
:
any
)
=>
i
?.
latex
+
"
\\\\\n
"
).
join
(
""
);
},
[
taskInfo
?.
content
]);
const
handleCopy
=
()
=>
{};
const
menuList
=
[
{
name
:
formatMessage
({
id
:
"
extractor.markdown.preview
"
}),
code
:
MD_PREVIEW_TYPE
.
preview
,
},
{
name
:
formatMessage
({
id
:
"
extractor.markdown.code
"
}),
code
:
MD_PREVIEW_TYPE
.
code
,
},
];
useEffect
(()
=>
{
imageViewerRef
?.
current
?.
updateScaleAndPosition
();
},
[
fullScreen
]);
useEffect
(()
=>
{
setDisplayType
(
MD_PREVIEW_TYPE
.
preview
);
},
[
jobID
]);
return
(
<
div
className
=
"w-full h-full"
>
<
header
className
=
{
`flex border-solid border-0 !border-b-[1px] border-[#EBECF0] px-4 items-center h-[
${
CONTROL_BAR_HEIGHT
}
px]`
}
>
{
formulaType
===
"
formula-extract
"
&&
(
<
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
>
)
}
<
Tooltip
title
=
{
fullScreen
?
formatMessage
({
id
:
"
extractor.button.exitFullScreen
"
})
:
formatMessage
({
id
:
"
extractor.button.fullScreen
"
,
})
}
>
<
span
className
=
"cursor-pointer ml-auto w-[1.5rem] 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
>
{
formulaType
===
"
formula-extract
"
&&
(
<
div
className
=
"flex items-center"
>
<
span
className
=
"w-[1px] h-[0.75rem] bg-[#D7D8DD] mx-[1rem]"
></
span
>
<
Tooltip
title
=
{
formatMessage
({
id
:
"
common.copy
"
})
}
>
<
CopyToClipboard
text
=
{
formulaLateX
}
onCopy
=
{
()
=>
{
message
.
success
(
formatMessage
({
id
:
"
common.copySuccess
"
}));
}
}
>
<
span
>
<
IconFont
type
=
"icon-copy"
className
=
"text-[#464a53] !text-[1.32rem] leading-0 cursor-pointer hover:bg-[#F4F5F9] rounded"
onClick
=
{
()
=>
handleCopy
()
}
/>
</
span
>
</
CopyToClipboard
>
</
Tooltip
>
</
div
>
)
}
</
header
>
{
displayType
===
MD_PREVIEW_TYPE
.
preview
?
(
formulaType
===
"
formula-extract
"
?
(
<
div
className
=
"w-full h-[calc(100%-48px)] flex items-center justify-center scrollbar-thin-layer overflow-auto"
>
<
LatexRenderer
formula
=
{
formulaLateX
}
className
=
"text-base sm:text-lg md:text-xl lg:text-2xl xl:text-3xl"
/>
</
div
>
)
:
(
<
ImageLayerViewer
imageHeight
=
{
taskInfo
?.
fileInfo
?.
height
||
0
}
imageWidth
=
{
taskInfo
?.
fileInfo
?.
width
||
0
}
layout
=
{
taskInfo
?.
type
===
"
formula-extract
"
?
[]
:
(
taskInfo
.
content
as
any
[])
}
className
=
{
"
!h-[calc(100%-48px)]
"
}
imageUrl
=
{
taskInfo
?.
url
}
ref
=
{
imageViewerRef
}
/>
)
)
:
(
<
div
className
=
{
"
!h-[calc(100%-48px)] flex items-center justify-center w-full px-4 scroll-thin overflow-auto
"
}
>
<
CodeMirror
className
=
"w-full"
value
=
{
formulaLateX
}
/>
</
div
>
)
}
</
div
>
);
};
export
default
FormulaDetailRight
;
projects/web/src/pages/extract/formula/formula-detail/index.tsx
0 → 100644
View file @
fdb6d661
import
cls
from
"
classnames
"
;
import
{
useEffect
,
useState
}
from
"
react
"
;
import
LoadingIcon
from
"
../../components/loading-icon
"
;
import
{
SubmitRes
}
from
"
@/api/extract
"
;
import
emptySvg
from
"
@/assets/svg/empty.svg
"
;
import
{
FormattedMessage
}
from
"
react-intl
"
;
import
FormulaDetailLeft
from
"
../formula-detail-left
"
;
import
FormulaDetailRight
from
"
../formula-detail-right
"
;
import
{
useIntl
}
from
"
react-intl
"
;
import
{
ExtractorUploadButton
}
from
"
../../components/pdf-upload-button
"
;
import
useExtractorJobProgress
from
"
@/store/jobProgress
"
;
interface
IPdfExtractionProps
{
setUploadShow
:
(
bool
:
boolean
)
=>
void
;
className
?:
string
;
}
const
FormulaDetail
=
({
className
=
""
}:
IPdfExtractionProps
)
=>
{
const
{
taskInfo
,
queueLoading
,
interfaceError
:
compileError
,
refreshQueue
,
jobID
,
}
=
useExtractorJobProgress
();
const
[
fullScreen
,
setFullScreen
]
=
useState
<
boolean
>
(
false
);
const
{
formatMessage
}
=
useIntl
();
const
isQueueAndExtract
=
queueLoading
;
const
hiddenQueuePage
=
!
isQueueAndExtract
?
"
opacity-0
"
:
""
;
const
hiddenResultPage
=
isQueueAndExtract
?
"
z-[-1] opacity-0
"
:
""
;
const
getLayoutClassName
=
(
_fullScreen
?:
boolean
)
=>
{
return
{
left
:
_fullScreen
?
"
w-0 overflow-hidden
"
:
"
w-[50%] max-w-[50%]
"
,
right
:
_fullScreen
?
"
w-full
"
:
"
w-[50%] max-w-[50%]
"
,
};
};
const
afterUploadSuccess
=
(
data
:
SubmitRes
)
=>
{
refreshQueue
();
};
const
afterAsyncCheck
=
()
=>
{
return
Promise
.
resolve
(
true
);
};
useEffect
(()
=>
{
setFullScreen
(
false
);
},
[
jobID
]);
return
(
<>
<
div
className
=
{
cls
(
"
flex flex-col justify-center items-center h-[60px] w-[300px] bg-white h-full w-full absolute top-0 left-0
"
,
hiddenQueuePage
)
}
>
<
LoadingIcon
className
=
"w-12"
color
=
{
"
#0D53DE
"
}
/>
<
div
className
=
"text-base text-[#121316]/[0.8] mt-4"
>
{
taskInfo
?.
rank
>
1
?
(
<
FormattedMessage
id
=
"extractor.common.extracting.queue"
values
=
{
{
id
:
taskInfo
?.
rank
||
0
,
}
}
/>
)
:
taskInfo
.
state
===
"
done
"
||
taskInfo
?.
state
===
"
unknown
"
?
(
formatMessage
({
id
:
"
extractor.common.loading
"
,
})
)
:
(
formatMessage
({
id
:
"
extractor.common.extracting
"
,
})
)
}
</
div
>
</
div
>
<
div
className
=
{
cls
(
"
h-full w-full relative
"
,
className
,
hiddenResultPage
)
}
>
{
!
compileError
?
(
<
div
className
=
"w-full flex h-full"
>
<
div
className
=
{
cls
(
"
h-full
"
,
getLayoutClassName
(
fullScreen
).
left
)
}
>
<
FormulaDetailLeft
taskInfo
=
{
taskInfo
}
/>
</
div
>
<
div
className
=
{
cls
(
"
!overflow-auto
"
,
getLayoutClassName
(
fullScreen
).
right
)
}
style
=
{
{
borderLeft
:
"
1px solid #EBECF0
"
,
}
}
>
<
FormulaDetailRight
fullScreen
=
{
fullScreen
}
setFullScreen
=
{
setFullScreen
}
taskInfo
=
{
taskInfo
}
/>
</
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
>
<
ExtractorUploadButton
className
=
"!mb-0 !w-[120px] !m-6"
accept
=
"image/png, image/jpg, .png ,.jpg"
afterUploadSuccess
=
{
afterUploadSuccess
}
taskType
=
"extract"
afterAsyncCheck
=
{
afterAsyncCheck
}
extractType
=
{
taskInfo
?.
type
}
submitType
=
"reUpload"
showIcon
=
{
false
}
text
=
{
<
span
className
=
"text-white"
>
{
formatMessage
({
id
:
"
extractor.button.reUpload
"
,
})
}
</
span
>
}
/>
</
div
>
)
}
</
div
>
</>
);
};
export
default
FormulaDetail
;
projects/web/src/pages/extract/formula/formula-popover/index.module.scss
0 → 100644
View file @
fdb6d661
.formulaPopover
{
: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
;
}
}
}
projects/web/src/pages/extract/formula/formula-popover/index.tsx
0 → 100644
View file @
fdb6d661
import
React
,
{
ReactNode
}
from
"
react
"
;
import
{
Popover
}
from
"
antd
"
;
import
IconFont
from
"
@/components/icon-font
"
;
import
{
useIntl
}
from
"
react-intl
"
;
import
style
from
"
./index.module.scss
"
;
interface
IFormulaPopoverProps
{
type
:
string
;
text
?:
string
|
ReactNode
;
}
const
FormulaPopover
=
({
type
,
text
}:
IFormulaPopoverProps
)
=>
{
const
{
formatMessage
}
=
useIntl
();
const
content
=
(
<
div
className
=
"flex flex-col w-[20rem] items-center"
>
{
/* 顺序反了 */
}
{
formatMessage
({
id
:
type
===
"
detect
"
?
"
extractor.formula.popover.extract
"
:
"
extractor.formula.popover.detect
"
,
})
}
<
img
className
=
"w-full mt-4"
src
=
{
type
===
"
extract
"
?
"
https://static.openxlab.org.cn/opendatalab/assets/pdf/svg/extract-formula-extract.svg
"
:
"
https://static.openxlab.org.cn/opendatalab/assets/pdf/svg/extract-formula-detect.svg
"
}
alt
=
"formula-popover"
/>
</
div
>
);
return
(
<
span
className
=
{
""
}
>
<
Popover
content
=
{
content
}
placement
=
"right"
showArrow
=
{
false
}
overlayClassName
=
{
style
.
formulaPopover
}
>
<
span
className
=
"group inline-flex items-center"
>
{
text
}
<
IconFont
type
=
"icon-QuestionCircleOutlined"
className
=
"text-[#121316]/[0.6] text-[15px] mt-[2px] leading-[1rem] group-hover:text-[#0D53DE]"
/>
</
span
>
</
Popover
>
</
span
>
);
};
export
default
FormulaPopover
;
projects/web/src/pages/extract/formula/formula-upload/index.module.scss
0 → 100644
View file @
fdb6d661
.uploadText
{
font-feature-settings
:
'liga'
off
,
'clig'
off
;
font-family
:
"PingFang SC"
;
font-size
:
18px
;
font-style
:
normal
;
font-weight
:
600
;
line-height
:
24px
;
/* 133.333% */
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
;
}
.uploadDescText
{
font-size
:
13px
;
line-height
:
20px
;
font-weight
:
400
;
background
:
linear-gradient
(
107deg
,
rgba
(
18
,
19
,
22
,
0
.6
)
-24
.14%
,
rgba
(
18
,
19
,
22
,
0
.6
)
100
.09%
);
background-clip
:
text
;
-webkit-background-clip
:
text
;
-webkit-text-fill-color
:
transparent
;
margin-bottom
:
1rem
;
margin-top
:
0
.5rem
;
}
.linearText
{
font-size
:
13px
;
line-height
:
20px
;
font-weight
:
400
;
background
:
linear-gradient
(
107deg
,
rgba
(
18
,
19
,
22
,
0
.6
)
-24
.14%
,
rgba
(
18
,
19
,
22
,
0
.6
)
100
.09%
);
background-clip
:
text
;
-webkit-background-clip
:
text
;
-webkit-text-fill-color
:
transparent
;
&
-item
{
font-weight
:
400
;
font-size
:
13px
;
line-height
:
20px
;
margin-right
:
1rem
;
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
;
&
:hover
{
background
:
#3477EB
;
background
:
linear-gradient
(
107deg
,
#3477EB
-24
.14%
,
#3477EB
100
.09%
);
background-clip
:
text
;
-webkit-background-clip
:
text
;
-webkit-text-fill-color
:
transparent
;
}
}
}
.uploadSection
{
border-radius
:
12px
;
border
:
1px
dashed
var
(
---
Brand1-6
,
#0D53DE
);
background
:
linear-gradient
(
180deg
,
rgba
(
92
,
147
,
255
,
0
.10
)
-130
.23%
,
rgba
(
255
,
255
,
255
,
1
)
83
.57%
);
display
:
flex
;
flex-direction
:
column
;
justify-content
:
center
;
align-items
:
center
;
filter
:
blur
(
0px
);
height
:
280px
!
important
;
width
:
600px
!
important
;
&
:hover
{
background
:
linear-gradient
(
180deg
,
rgb
(
245
,
248
,
255
)
-130
.23%
,
rgb
(
245
,
248
,
255
)
83
.57%
);
}
}
.textBtn
{
background-image
:
none
!
important
;
background-clip
:
text
;
-webkit-background-clip
:
text
;
-webkit-text-fill-color
:
transparent
;
background
:
linear-gradient
(
111deg
,
#0D53DE
-21
.44%
,
#5246FF
102%
)
!
important
;
background-clip
:
text
!
important
;
-webkit-background-clip
:
text
!
important
;
-webkit-text-fill-color
:
transparent
!
important
;
height
:
1
.5rem
!
important
;
font-weight
:
600
;
height
:
280px
!
important
;
width
:
600px
!
important
;
overflow
:
hidden
;
}
projects/web/src/pages/extract/formula/formula-upload/index.tsx
0 → 100644
View file @
fdb6d661
import
{
useIntl
}
from
"
react-intl
"
;
import
IconFont
from
"
@/components/icon-font
"
;
import
{
useState
}
from
"
react
"
;
import
cls
from
"
classnames
"
;
import
{
ExtractorUploadButton
}
from
"
../../components/pdf-upload-button
"
;
import
UploadBg
from
"
@/assets/imgs/online.experience/file-upload-bg.svg
"
;
import
style
from
"
./index.module.scss
"
;
import
{
SubmitRes
}
from
"
@/api/extract
"
;
import
{
ADD_TASK_LIST
}
from
"
@/constant/event
"
;
import
{
FORMULA_TYPE
}
from
"
@/types/extract-task-type
"
;
import
{
useNavigate
}
from
"
react-router-dom
"
;
const
FORMULA_ITEM_LIST
=
[
{
type
:
FORMULA_TYPE
.
detect
,
[
`zh-CN-name`
]:
"
公式检测
"
,
[
`en-US-name`
]:
"
Formula Detection
"
,
},
{
type
:
FORMULA_TYPE
.
extract
,
[
`zh-CN-name`
]:
"
公式识别
"
,
[
`en-US-name`
]:
"
Formula Recognition
"
,
},
];
const
FormulaUpload
=
()
=>
{
const
navigate
=
useNavigate
();
const
{
formatMessage
,
locale
}
=
useIntl
();
const
[
formulaType
,
setFormulaType
]
=
useState
(
FORMULA_TYPE
.
detect
);
const
afterUploadSuccess
=
(
data
:
SubmitRes
)
=>
{
navigate
(
`/OpenSourceTools/Extractor/formula/
${
data
?.
id
}
`
);
setTimeout
(()
=>
{
document
.
dispatchEvent
(
new
CustomEvent
(
ADD_TASK_LIST
,
{
detail
:
data
,
})
);
},
10
);
};
const
afterAsyncCheck
=
()
=>
{
return
Promise
.
resolve
(
true
);
};
return
(
<
div
className
=
"relative w-full h-full flex flex-col items-center justify-center "
>
<
div
className
=
"absolute top-10 left-8 hover:!text-[#0D53DE] cursor-pointer"
onClick
=
{
()
=>
navigate
(
"
/OpenSourceTools/Extractor
"
)
}
>
<
IconFont
type
=
"icon-fanhui"
className
=
"mr-2"
/>
<
span
>
{
formatMessage
({
id
:
"
extractor.home
"
})
}
</
span
>
</
div
>
<
div
className
=
"translate-y-[-60px] flex flex-col items-center "
>
<
div
className
=
"mb-[2.25rem]"
>
{
FORMULA_ITEM_LIST
.
map
((
i
)
=>
{
return
(
<
span
key
=
{
i
.
type
}
onClick
=
{
()
=>
setFormulaType
(
i
?.
type
)
}
className
=
{
cls
(
"
relative text-[1.5rem] text-[#121316] cursor-pointer mx-[1.5rem]
"
,
formulaType
===
i
?.
type
&&
"
!text-[#0D53DE] font-semibold
"
)
}
>
{
i
?.[
`
${
locale
||
"
zh-CN
"
}
-name`
as
"
en-US-name
"
]
}
{
formulaType
===
i
?.
type
&&
(
<
span
className
=
"absolute bottom-[-0.75rem] right-[50%] translate-x-[50%] w-[3rem] bg-[#0D53DE] rounded-[2px] h-[0.25rem]"
></
span
>
)
}
</
span
>
);
})
}
</
div
>
<
div
className
=
"text-[1.25rem] text-[#121316]/[0.8] mb-[3rem] text-center w-max-[50rem]"
>
{
formatMessage
({
id
:
formulaType
===
"
extract
"
?
"
extractor.formula.title2
"
:
"
extractor.formula.title
"
,
})
}
</
div
>
<
ExtractorUploadButton
accept
=
"image/png, image/jpg, .png ,.jpg"
afterUploadSuccess
=
{
afterUploadSuccess
}
taskType
=
"extract"
afterAsyncCheck
=
{
afterAsyncCheck
}
extractType
=
{
formulaType
===
FORMULA_TYPE
.
extract
?
"
formula-extract
"
:
"
formula-detect
"
}
className
=
{
style
.
textBtn
}
showIcon
=
{
false
}
text
=
{
<
div
className
=
{
cls
(
style
.
uploadSection
,
"
border-[1px] border-dashed border-[#0D53DE] rounded-xl flex flex-col items-center justify-center
"
)
}
>
<
img
src
=
{
UploadBg
}
className
=
"mb-4"
/>
<
span
className
=
{
cls
(
style
.
uploadText
,
"
text-[18px] leading-[20px]
"
)
}
>
{
formatMessage
({
id
:
"
extractor.formula.upload.text
"
})
}
</
span
>
<
span
className
=
{
cls
(
style
.
uploadDescText
)
}
>
{
formatMessage
({
id
:
"
extractor.formula.upload.accept
"
})
}
</
span
>
<
div
>
<
span
className
=
{
cls
(
style
.
linearText
,
"
cursor-pointer
"
)
}
>
{
formatMessage
({
id
:
"
extractor.formula.upload.try
"
,
})
}
</
span
>
</
div
>
</
div
>
}
></
ExtractorUploadButton
>
</
div
>
<
div
className
=
"absolute bottom-[1.5rem] text-[13px] text-[#121316]/[0.35] text-center leading-[20px] max-w-[64rem]"
>
{
formatMessage
({
id
:
"
extractor.law
"
,
})
}
</
div
>
</
div
>
);
};
export
default
FormulaUpload
;
projects/web
_api/web_api/common/__init__.py
→
projects/web
/src/pages/extract/formula/index.module.scss
View file @
fdb6d661
File moved
projects/web/src/pages/extract/formula/index.tsx
0 → 100644
View file @
fdb6d661
import
{
Outlet
}
from
"
react-router-dom
"
;
const
Formula
=
()
=>
{
return
(
<
div
className
=
"relative w-full h-full flex flex-col items-center justify-center "
>
<
Outlet
/>
</
div
>
);
};
export
default
Formula
;
projects/web
_api/web_api/config/__init__.py
→
projects/web
/src/pages/extract/index.module.scss
View file @
fdb6d661
File moved
projects/web
_api/web_api/static/__init__.py
→
projects/web
/src/pages/extract/index.tsx
View file @
fdb6d661
File moved
projects/web/src/pages/extract/table/index.tsx
0 → 100644
View file @
fdb6d661
const
ExtractorTable
=
()
=>
{
return
<>
ExtractorTable
</>;
};
export
default
ExtractorTable
;
projects/web/src/pages/extract/table/table-detail/index.tsx
0 → 100644
View file @
fdb6d661
const
TableDetail
=
()
=>
{
return
<>
TableDetail
</>;
};
export
default
TableDetail
;
projects/web/src/pages/home.module.scss
0 → 100644
View file @
fdb6d661
projects/web/src/pages/home.tsx
0 → 100644
View file @
fdb6d661
"
use client
"
;
import
ErrorBoundary
from
"
@/components/error-boundary
"
;
import
styles
from
"
./home.module.scss
"
;
import
{
SlotID
,
Path
}
from
"
@/constant/route
"
;
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
"
;
export
function
WindowContent
()
{
const
location
=
useLocation
();
const
isHome
=
location
.
pathname
===
Path
.
Home
;
return
(
<>
<
ExtractorSide
className
=
{
isHome
?
styles
[
"
sidebar-show
"
]
:
""
}
/>
<
div
className
=
"flex-1"
>
<
Outlet
/>
</
div
>
</>
);
}
function
Screen
()
{
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
/>
}
/>
<
Route
path
=
"/OpenSourceTools/Extractor/PDF/:jobID"
element
=
{
<
PDFExtractionJob
/>
}
/>
<
Route
path
=
"*"
element
=
{
<
Navigate
to
=
"/OpenSourceTools/Extractor/PDF"
replace
/>
}
/>
</
Route
>
</
Routes
>
</
div
>
);
};
return
<>
{
renderContent
()
}
</>;
}
export
function
Home
()
{
return
(
<
ErrorBoundary
>
<
LanguageProvider
>
<
HashRouter
>
<
Screen
/>
</
HashRouter
>
</
LanguageProvider
>
</
ErrorBoundary
>
);
}
projects/web/src/routes/index.tsx
0 → 100644
View file @
fdb6d661
import
{
Routes
,
Route
}
from
"
react-router-dom
"
;
import
PDFUpload
from
"
@/pages/extract/components/pdf-upload
"
;
import
PDFExtractionJob
from
"
@/pages/extract/components/pdf-extraction
"
;
function
AppRoutes
()
{
return
(
<>
<
Route
path
=
"/OpenSourceTools/Extractor/PDF"
element
=
{
<
PDFUpload
/>
}
/>
<
Route
path
=
"/OpenSourceTools/Extractor/PDF/:jobID"
element
=
{
<
PDFExtractionJob
/>
}
/>
</>
);
}
export
default
AppRoutes
;
projects/web/src/store/jobProgress.ts
0 → 100644
View file @
fdb6d661
import
{
getExtractTaskIdProgress
,
getPdfExtractQueue
,
TaskIdResItem
,
}
from
"
@/api/extract
"
;
import
{
create
}
from
"
zustand
"
;
import
{
useCallback
,
useEffect
,
useRef
,
useState
}
from
"
react
"
;
import
{
useParams
}
from
"
react-router-dom
"
;
import
{
UPDATE_TASK_LIST
}
from
"
@/constant/event
"
;
import
{
useQuery
}
from
"
@tanstack/react-query
"
;
interface
ExtractorState
{
taskInfo
:
TaskIdResItem
;
queueLoading
:
boolean
|
null
;
interfaceError
:
boolean
;
setTaskInfo
:
(
taskInfo
:
TaskIdResItem
)
=>
void
;
setQueueLoading
:
(
loading
:
boolean
|
null
)
=>
void
;
setInterfaceError
:
(
error
:
boolean
)
=>
void
;
}
const
defaultTaskInfo
:
TaskIdResItem
=
{
id
:
0
,
rank
:
0
,
state
:
"
pending
"
,
url
:
""
,
type
:
"
unknown
"
,
queues
:
-
1
,
};
const
useExtractorStore
=
create
<
ExtractorState
>
((
set
)
=>
({
taskInfo
:
defaultTaskInfo
,
queueLoading
:
null
,
interfaceError
:
false
,
setTaskInfo
:
(
taskInfo
:
any
)
=>
set
({
taskInfo
}),
setQueueLoading
:
(
loading
)
=>
set
({
queueLoading
:
loading
}),
setInterfaceError
:
(
error
)
=>
set
({
interfaceError
:
error
}),
}));
export
const
useJobExtraction
=
()
=>
{
const
{
jobID
}
=
useParams
<
{
jobID
:
string
}
>
();
const
{
setTaskInfo
,
setQueueLoading
,
queueLoading
,
taskInfo
,
interfaceError
,
setInterfaceError
,
}
=
useExtractorStore
();
const
timeoutRef
=
useRef
<
NodeJS
.
Timeout
|
null
>
(
null
);
const
[
isPolling
,
setIsPolling
]
=
useState
(
true
);
const
stopTaskLoading
=
()
=>
{
setQueueLoading
(
false
);
};
// Query for task progress
const
taskProgressQuery
=
useQuery
({
queryKey
:
[
"
taskProgress
"
,
jobID
],
queryFn
:
()
=>
{
setQueueLoading
(
true
);
setIsPolling
(
true
);
return
getExtractTaskIdProgress
(
jobID
!
)
.
then
((
res
)
=>
{
if
(
res
?.
state
===
"
done
"
||
res
?.
state
===
"
failed
"
)
{
stopTaskLoading
();
document
.
dispatchEvent
(
new
CustomEvent
(
"
UPDATE_TASK_LIST
"
,
{
detail
:
{
state
:
res
.
state
,
id
:
jobID
},
})
);
}
if
(
res
)
{
setTaskInfo
(
res
);
}
return
res
;
})
.
catch
(()
=>
{
stopTaskLoading
();
setTaskInfo
({
state
:
"
failed
"
});
});
},
enabled
:
false
,
});
// Query for queue status
const
queueStatusQuery
=
useQuery
({
queryKey
:
[
"
queueStatus
"
,
jobID
],
queryFn
:
async
()
=>
{
setQueueLoading
(
true
);
const
response
=
await
getPdfExtractQueue
(
jobID
).
then
((
res
)
=>
{
// setTaskInfo({ rand: "failed" });
if
(
res
)
{
const
targetPendingRunningJob
=
res
?.
filter
(
(
i
)
=>
String
(
i
.
id
)
===
jobID
)?.[
0
];
if
(
targetPendingRunningJob
)
{
setTaskInfo
(
targetPendingRunningJob
);
}
else
{
setIsPolling
(
false
);
setQueueLoading
(
false
);
getExtractTaskIdProgress
(
jobID
!
).
then
((
res
)
=>
{
setTaskInfo
(
res
as
any
);
});
}
}
return
res
;
});
return
response
;
},
enabled
:
isPolling
&&
(
taskProgressQuery
?.
data
?.
state
===
"
running
"
||
taskProgressQuery
?.
data
?.
state
===
"
pending
"
),
refetchInterval
:
2000
,
// Poll every 2 seconds
});
useEffect
(()
=>
{
if
(
taskProgressQuery
.
data
?.
state
===
"
done
"
)
{
stopTaskLoading
();
setInterfaceError
(
false
);
setIsPolling
(
false
);
if
(
timeoutRef
.
current
)
{
clearTimeout
(
timeoutRef
.
current
);
}
else
{
timeoutRef
.
current
=
setTimeout
(()
=>
{
document
.
dispatchEvent
(
new
CustomEvent
(
UPDATE_TASK_LIST
,
{
detail
:
{
state
:
"
done
"
,
jobID
},
})
);
},
10
);
}
}
else
if
(
taskProgressQuery
.
data
?.
state
===
"
failed
"
)
{
stopTaskLoading
();
setInterfaceError
(
true
);
setIsPolling
(
false
);
if
(
timeoutRef
.
current
)
{
clearTimeout
(
timeoutRef
.
current
);
}
else
{
timeoutRef
.
current
=
setTimeout
(()
=>
{
document
.
dispatchEvent
(
new
CustomEvent
(
UPDATE_TASK_LIST
,
{
detail
:
{
state
:
"
failed
"
,
jobID
},
})
);
},
10
);
}
}
// TIP这里得用taskInfo
},
[
taskProgressQuery
.
data
]);
const
refreshQueue
=
()
=>
{
// stop last ID polling
setIsPolling
(
false
);
setTaskInfo
(
defaultTaskInfo
);
taskProgressQuery
.
refetch
();
};
useEffect
(()
=>
{
if
(
jobID
)
{
// stop last ID polling d
setTaskInfo
(
defaultTaskInfo
);
taskProgressQuery
.
refetch
();
}
},
[
jobID
]);
return
{
taskInfo
:
taskInfo
,
isLoading
:
queueLoading
,
isError
:
interfaceError
||
taskProgressQuery
.
isError
||
queueStatusQuery
.
isError
,
refreshQueue
,
};
};
projects/web/src/store/languageStore.ts
0 → 100644
View file @
fdb6d661
import
{
create
}
from
"
zustand
"
;
import
{
Language
}
from
"
@/constant
"
;
import
{
LOCALE_STORAGE_KEY
}
from
"
@/constant/storage
"
;
type
LanguageType
=
(
typeof
Language
)[
keyof
typeof
Language
];
type
LanguageStore
=
{
language
:
LanguageType
;
setLanguage
:
(
language
:
LanguageType
)
=>
void
;
toggleLanguage
:
()
=>
void
;
};
const
getInitialLanguage
=
():
LanguageType
=>
{
// Try to get language setting from localStorage
const
savedLanguage
=
localStorage
.
getItem
(
LOCALE_STORAGE_KEY
)
as
LanguageType
;
if
(
savedLanguage
&&
Object
.
values
(
Language
).
includes
(
savedLanguage
))
{
return
savedLanguage
;
}
// If no valid language setting in localStorage, try to get browser language
const
browserLanguage
=
navigator
.
language
.
toLowerCase
();
if
(
browserLanguage
.
startsWith
(
"
zh
"
))
{
return
Language
.
ZH_CN
;
}
else
if
(
browserLanguage
.
startsWith
(
"
en
"
))
{
return
Language
.
EN_US
;
}
// Default to Chinese
return
Language
.
ZH_CN
;
};
export
const
useLanguageStore
=
create
<
LanguageStore
>
((
set
)
=>
({
language
:
getInitialLanguage
(),
setLanguage
:
(
language
)
=>
{
localStorage
.
setItem
(
LOCALE_STORAGE_KEY
,
language
);
set
({
language
});
},
toggleLanguage
:
()
=>
set
((
state
)
=>
{
const
newLanguage
=
state
.
language
===
Language
.
ZH_CN
?
Language
.
EN_US
:
Language
.
ZH_CN
;
localStorage
.
setItem
(
LOCALE_STORAGE_KEY
,
newLanguage
);
return
{
language
:
newLanguage
};
}),
}));
projects/web/src/store/mdStore.ts
0 → 100644
View file @
fdb6d661
// mdStore.ts
import
{
create
}
from
"
zustand
"
;
import
axios
from
"
axios
"
;
import
{
updateMarkdownContent
,
UpdateMarkdownRequest
}
from
"
@/api/extract
"
;
// 确保路径正确
interface
MdContent
{
content
:
string
;
isLoading
:
boolean
;
}
type
AnchorType
=
|
"
span
"
|
"
div
"
|
"
comment
"
|
"
data-attribute
"
|
"
hr
"
|
"
mark
"
|
"
p
"
;
interface
AnchorOptions
{
type
:
AnchorType
;
prefix
?:
string
;
style
?:
string
;
className
?:
string
;
customAttributes
?:
Record
<
string
,
string
>
;
}
const
defaultAnchorOptions
:
AnchorOptions
=
{
type
:
"
span
"
,
prefix
:
"
md-anchor-
"
,
style
:
"
display:none;
"
,
className
:
""
,
customAttributes
:
{},
};
interface
MdState
{
mdContents
:
Record
<
string
,
MdContent
>
;
allMdContent
:
string
;
allMdContentWithAnchor
:
string
;
error
:
Error
|
null
;
currentRequestId
:
number
;
setMdUrlArr
:
(
urls
:
string
[])
=>
Promise
<
void
>
;
getAllMdContent
:
(
data
:
string
[])
=>
string
;
setAllMdContent
:
(
val
?:
string
)
=>
void
;
setAllMdContentWithAnchor
:
(
val
?:
string
)
=>
void
;
getContentWithAnchors
:
(
data
:
string
[],
options
?:
Partial
<
AnchorOptions
>
)
=>
string
;
jumpToAnchor
:
(
anchorId
:
string
)
=>
number
;
reset
:
()
=>
void
;
updateMdContent
:
(
fileKey
:
string
,
pageNumber
:
string
|
number
,
newContent
:
string
)
=>
Promise
<
void
>
;
}
const
MAX_CONCURRENT_REQUESTS
=
2
;
const
initialState
=
{
mdContents
:
{},
allMdContent
:
""
,
allMdContentWithAnchor
:
""
,
error
:
null
,
currentRequestId
:
0
,
};
const
useMdStore
=
create
<
MdState
>
((
set
,
get
)
=>
({
...
initialState
,
reset
:
()
=>
{
set
(
initialState
);
},
setAllMdContent
:
(
value
?:
string
)
=>
{
set
(()
=>
({
allMdContent
:
value
,
}));
},
setAllMdContentWithAnchor
:
(
value
?:
string
)
=>
{
set
(()
=>
({
allMdContentWithAnchor
:
value
,
}));
},
setMdUrlArr
:
async
(
urls
:
string
[])
=>
{
const
requestId
=
get
().
currentRequestId
+
1
;
set
((
state
)
=>
({
currentRequestId
:
requestId
,
error
:
null
}));
const
fetchContent
=
async
(
url
:
string
):
Promise
<
[
string
,
string
]
>
=>
{
try
{
const
response
=
await
axios
.
get
<
string
>
(
url
);
return
[
url
,
response
.
data
];
}
catch
(
error
)
{
if
(
get
().
currentRequestId
===
requestId
)
{
set
((
state
)
=>
({
error
:
error
as
Error
}));
}
return
[
url
,
""
];
}
};
const
fetchWithConcurrency
=
async
(
urls
:
string
[]
):
Promise
<
[
string
,
string
][]
>
=>
{
const
queue
=
[...
urls
];
const
results
:
[
string
,
string
][]
=
[];
const
inProgress
=
new
Set
<
Promise
<
[
string
,
string
]
>>
();
while
(
queue
.
length
>
0
||
inProgress
.
size
>
0
)
{
while
(
inProgress
.
size
<
MAX_CONCURRENT_REQUESTS
&&
queue
.
length
>
0
)
{
const
url
=
queue
.
shift
()
!
;
const
promise
=
fetchContent
(
url
);
inProgress
.
add
(
promise
);
promise
.
then
((
result
)
=>
{
results
.
push
(
result
);
inProgress
.
delete
(
promise
);
});
}
if
(
inProgress
.
size
>
0
)
{
await
Promise
.
race
(
inProgress
);
}
}
return
results
;
};
const
results
=
await
fetchWithConcurrency
(
urls
);
if
(
get
().
currentRequestId
===
requestId
)
{
const
newMdContents
:
Record
<
string
,
MdContent
>
=
{};
results
.
forEach
(([
url
,
content
])
=>
{
newMdContents
[
url
]
=
{
content
,
isLoading
:
false
};
});
set
((
state
)
=>
({
mdContents
:
newMdContents
,
allMdContent
:
state
.
getAllMdContent
(
results
.
map
((
i
)
=>
i
[
1
])),
allMdContentWithAnchor
:
state
.
getContentWithAnchors
(
results
.
map
((
i
)
=>
i
[
1
])
),
}));
}
},
getAllMdContent
:
(
data
)
=>
{
return
data
?.
join
(
"
\n\n
"
);
},
getContentWithAnchors
:
(
data
:
string
[],
options
?:
Partial
<
AnchorOptions
>
)
=>
{
const
opts
=
{
...
defaultAnchorOptions
,
...
options
};
const
generateAnchorTag
=
(
index
:
number
)
=>
{
const
id
=
`
${
opts
.
prefix
}${
index
}
`
;
const
attributes
=
Object
.
entries
(
opts
.
customAttributes
||
{})
.
map
(([
key
,
value
])
=>
`
${
key
}
="
${
value
}
"`
)
.
join
(
"
"
);
switch
(
opts
.
type
)
{
case
"
span
"
:
case
"
div
"
:
case
"
mark
"
:
case
"
p
"
:
return
`<
${
opts
.
type
}
id="
${
id
}
" style="
${
opts
.
style
}
" class="
${
opts
.
className
}
"
${
attributes
}
></
${
opts
.
type
}
>`
;
case
"
comment
"
:
return
`<!-- anchor:
${
id
}
-->`
;
case
"
data-attribute
"
:
return
`<span data-anchor="
${
id
}
" style="
${
opts
.
style
}
" class="
${
opts
.
className
}
"
${
attributes
}
></span>`
;
case
"
hr
"
:
return
`<hr id="
${
id
}
" style="
${
opts
.
style
}
" class="
${
opts
.
className
}
"
${
attributes
}
>`
;
default
:
return
`<span id="
${
id
}
" style="
${
opts
.
style
}
" class="
${
opts
.
className
}
"
${
attributes
}
></span>`
;
}
};
return
data
?.
map
((
content
,
index
)
=>
{
const
anchorTag
=
generateAnchorTag
(
index
);
return
`
${
anchorTag
}
\n\n
${
content
}
`
;
})
.
join
(
"
\n\n
"
);
},
jumpToAnchor
:
(
anchorId
:
string
)
=>
{
const
{
mdContents
}
=
get
();
const
contentArray
=
Object
.
values
(
mdContents
).
map
(
(
content
)
=>
content
.
content
);
let
totalLength
=
0
;
for
(
let
i
=
0
;
i
<
contentArray
.
length
;
i
++
)
{
if
(
anchorId
===
`md-anchor-
${
i
}
`
)
{
return
totalLength
;
}
totalLength
+=
contentArray
[
i
].
length
+
2
;
// +2 for "\n\n"
}
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
;
projects/web/src/styles/variable.scss
0 → 100644
View file @
fdb6d661
$page-min-witch
:
1260px
;
\ No newline at end of file
Prev
1
…
15
16
17
18
19
20
21
22
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment