提交 912a004e authored 作者: 王鹏飞's avatar 王鹏飞

bug fixes

上级 26a42e11
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -27,7 +27,9 @@ ...@@ -27,7 +27,9 @@
"easy-formula-editor": "^0.0.2-alpha.1", "easy-formula-editor": "^0.0.2-alpha.1",
"echarts": "^5.4.3", "echarts": "^5.4.3",
"evit-gm-crypt": "^1.0.1", "evit-gm-crypt": "^1.0.1",
"file-saver": "^2.0.5",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"html-docx-js-typescript": "^0.1.5",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"js-md5": "^0.8.3", "js-md5": "^0.8.3",
......
...@@ -32,7 +32,6 @@ ...@@ -32,7 +32,6 @@
overflow: hidden; overflow: hidden;
width: 430px; width: 430px;
border-radius: 30px; border-radius: 30px;
} }
.previee-container { .previee-container {
height: 100%; height: 100%;
...@@ -108,19 +107,34 @@ ...@@ -108,19 +107,34 @@
.preview-content-show { .preview-content-show {
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
height: calc(100% - 45px); height: calc(100% - 85px);
}
.preview-page-number {
height: 40px;
line-height: 40px;
text-align: center;
font-size: 14px;
color: #999;
border-top: 1px solid #efefef;
background: #fff;
flex-shrink: 0;
} }
* { * {
line-height: 1.5; line-height: 1.5;
font-family: "黑体"; font-family: '黑体';
} }
.preview-content-html { .preview-content-html {
padding-bottom: 50px; padding-bottom: 50px;
.chapter-practice, .chapter-expand, .chapter-item-section, .chapter-item-header, .chapter-gallery-container { .chapter-practice,
.chapter-expand,
.chapter-item-section,
.chapter-item-header,
.chapter-gallery-container {
margin-top: 15px; margin-top: 15px;
margin-bottom: 15px; margin-bottom: 15px;
&.chapter-expand-inline, &.chapter-gallery-inline { &.chapter-expand-inline,
&.chapter-gallery-inline {
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
margin-top: -3px !important; margin-top: -3px !important;
...@@ -152,7 +166,7 @@ ...@@ -152,7 +166,7 @@
padding: 5px; padding: 5px;
font-size: 18px; font-size: 18px;
} }
div[data-w-e-type=video] { div[data-w-e-type='video'] {
margin: 10px; margin: 10px;
} }
p[data-slate-node]:not(:empty) { p[data-slate-node]:not(:empty) {
...@@ -361,7 +375,6 @@ ...@@ -361,7 +375,6 @@
width: 100%; width: 100%;
} }
p { p {
} }
img { img {
height: auto; height: auto;
...@@ -383,7 +396,10 @@ ...@@ -383,7 +396,10 @@
} }
} }
.chapter-item-tooltip, .chapter-item-link, .chapter-gallery-inline, .chapter-expand-inline { .chapter-item-tooltip,
.chapter-item-link,
.chapter-gallery-inline,
.chapter-expand-inline {
text-indent: 0; text-indent: 0;
svg { svg {
text-indent: 0; text-indent: 0;
...@@ -410,7 +426,7 @@ ...@@ -410,7 +426,7 @@
text-align: center; text-align: center;
padding-top: 40px; padding-top: 40px;
color: #999; color: #999;
font-size: 18px;; font-size: 18px;
} }
.opa { .opa {
position: absolute; position: absolute;
...@@ -486,9 +502,12 @@ ...@@ -486,9 +502,12 @@
} }
} }
li, dd, dt, blockquote { li,
dd,
dt,
blockquote {
font-size: 18px; font-size: 18px;
font-family: "黑体"; font-family: '黑体';
line-height: 1.5; line-height: 1.5;
margin: 15px 0; margin: 15px 0;
} }
...@@ -656,5 +675,36 @@ ...@@ -656,5 +675,36 @@
height: 100%; height: 100%;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
.ant-tree .ant-tree-treenode {
position: relative;
// padding-right: 40px;
.ant-tree-node-content-wrapper {
overflow: hidden;
}
}
.tree-node-title {
display: inline-flex;
align-items: center;
width: 100%;
.tree-node-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tree-node-page {
// position: absolute;
// right: 0;
font-size: 12px;
color: #999;
// min-width: 32px;
text-align: right;
background: #f0f0f0;
border-radius: 4px;
padding: 0 6px;
line-height: 20px;
}
}
} }
} }
...@@ -38,9 +38,27 @@ const correctList = [ ...@@ -38,9 +38,27 @@ const correctList = [
'W', 'W',
'X', 'X',
'Y', 'Y',
'Z' 'Z',
] ]
// 递归遍历树,为叶子节点分配页码,返回 Map<key, pageNumber> 和总页数
const buildPageMap = (treeData) => {
const map = {}
let page = 0
const walk = (nodes) => {
nodes.forEach((node) => {
if (node.children && node.children.length > 0) {
walk(node.children)
} else {
page += 1
map[node.key] = page
}
})
}
walk(treeData)
return { map, total: page }
}
const PreviewScreen = (props, ref) => { const PreviewScreen = (props, ref) => {
const { bookId } = props const { bookId } = props
const [gData, setGData] = useState([]) const [gData, setGData] = useState([])
...@@ -48,6 +66,9 @@ const PreviewScreen = (props, ref) => { ...@@ -48,6 +66,9 @@ const PreviewScreen = (props, ref) => {
const [prviewHtml, setPrviewHtml] = useState('') // 预览html const [prviewHtml, setPrviewHtml] = useState('') // 预览html
const [open, setOpen] = useState(false) // 抽屉 const [open, setOpen] = useState(false) // 抽屉
const [secondType, setSecondType] = useState(false) const [secondType, setSecondType] = useState(false)
const [pageMap, setPageMap] = useState({}) // 叶子节点页码映射
const [totalPages, setTotalPages] = useState(0) // 总页数
const [currentPage, setCurrentPage] = useState(0) // 当前页码
const [toolTip, setTooltip] = useState({}) const [toolTip, setTooltip] = useState({})
...@@ -73,9 +94,9 @@ const PreviewScreen = (props, ref) => { ...@@ -73,9 +94,9 @@ const PreviewScreen = (props, ref) => {
const newId = useRef(null) const newId = useRef(null)
// 递归函数,获取所有节点的key // 递归函数,获取所有节点的key
const getAllNodeKeys = nodes => { const getAllNodeKeys = (nodes) => {
let keys = [] let keys = []
nodes.forEach(node => { nodes.forEach((node) => {
keys.push(node.key) keys.push(node.key)
if (node.children && node.children.length > 0) { if (node.children && node.children.length > 0) {
keys = keys.concat(getAllNodeKeys(node.children)) keys = keys.concat(getAllNodeKeys(node.children))
...@@ -91,6 +112,9 @@ const PreviewScreen = (props, ref) => { ...@@ -91,6 +112,9 @@ const PreviewScreen = (props, ref) => {
return return
} }
const arr = convertToAntdTreeData(data.data, 'name') const arr = convertToAntdTreeData(data.data, 'name')
const { map, total } = buildPageMap(arr)
setPageMap(map)
setTotalPages(total)
setGData(arr) setGData(arr)
const allKeys = getAllNodeKeys(arr) const allKeys = getAllNodeKeys(arr)
setExpandedKeys(allKeys) setExpandedKeys(allKeys)
...@@ -98,6 +122,7 @@ const PreviewScreen = (props, ref) => { ...@@ -98,6 +122,7 @@ const PreviewScreen = (props, ref) => {
const first = findFirstNotHasChildren(arr) const first = findFirstNotHasChildren(arr)
setOldTitle(first.title) setOldTitle(first.title)
setNewChapterId(first.key) setNewChapterId(first.key)
setCurrentPage(map[first.key] || 0)
setLoading(false) setLoading(false)
} }
...@@ -105,11 +130,11 @@ const PreviewScreen = (props, ref) => { ...@@ -105,11 +130,11 @@ const PreviewScreen = (props, ref) => {
const data = await getChapterTopic({ const data = await getChapterTopic({
position: practicenum, position: practicenum,
book_id: bookid, book_id: bookid,
chapter_id: chapterid chapter_id: chapterid,
}) })
const temp = [] const temp = []
data.forEach(item => { data.forEach((item) => {
let obj = { ...item } let obj = { ...item }
if ([1, 2, 3].includes(parseInt(item.question_style))) { if ([1, 2, 3].includes(parseInt(item.question_style))) {
try { try {
...@@ -120,11 +145,11 @@ const PreviewScreen = (props, ref) => { ...@@ -120,11 +145,11 @@ const PreviewScreen = (props, ref) => {
} }
temp.push(obj) temp.push(obj)
}) })
const tempRadio = temp.filter(item => parseInt(item.question_style) === 1) const tempRadio = temp.filter((item) => parseInt(item.question_style) === 1)
const tempCheckbox = temp.filter(item => parseInt(item.question_style) === 2) const tempCheckbox = temp.filter((item) => parseInt(item.question_style) === 2)
const tempJudge = temp.filter(item => parseInt(item.question_style) === 3) const tempJudge = temp.filter((item) => parseInt(item.question_style) === 3)
const tempFill = temp.filter(item => parseInt(item.question_style) === 4) const tempFill = temp.filter((item) => parseInt(item.question_style) === 4)
const tempTextarea = temp.filter(item => parseInt(item.question_style) === 5) const tempTextarea = temp.filter((item) => parseInt(item.question_style) === 5)
setRadioList(tempRadio) setRadioList(tempRadio)
setCheckBoxList(tempCheckbox) setCheckBoxList(tempCheckbox)
...@@ -138,7 +163,7 @@ const PreviewScreen = (props, ref) => { ...@@ -138,7 +163,7 @@ const PreviewScreen = (props, ref) => {
const data = await expandReadInfo({ const data = await expandReadInfo({
book_id: bookId, book_id: bookId,
chapter_id: chapterId, chapter_id: chapterId,
position: position position: position,
}) })
if (data) { if (data) {
setExpandContent(data.content) setExpandContent(data.content)
...@@ -162,7 +187,7 @@ const PreviewScreen = (props, ref) => { ...@@ -162,7 +187,7 @@ const PreviewScreen = (props, ref) => {
document document
.querySelectorAll('.preview-content-show ')[0] .querySelectorAll('.preview-content-show ')[0]
.querySelectorAll('pre code') .querySelectorAll('pre code')
.forEach(el => { .forEach((el) => {
hljs.highlightElement(el) hljs.highlightElement(el)
}) })
}, 200) }, 200)
...@@ -176,7 +201,7 @@ const PreviewScreen = (props, ref) => { ...@@ -176,7 +201,7 @@ const PreviewScreen = (props, ref) => {
const parentOffset = $('.preview-content-it').offset() // 获取父元素相对于文档的位置 const parentOffset = $('.preview-content-it').offset() // 获取父元素相对于文档的位置
const parentRect = $('.preview-content-it')[0].getBoundingClientRect() // 获取父元素相对于文档的位置 const parentRect = $('.preview-content-it')[0].getBoundingClientRect() // 获取父元素相对于文档的位置
$('.preview-content-html').on('click', async ev => { $('.preview-content-html').on('click', async (ev) => {
ev.stopPropagation() ev.stopPropagation()
const target = ev.target const target = ev.target
...@@ -224,7 +249,7 @@ const PreviewScreen = (props, ref) => { ...@@ -224,7 +249,7 @@ const PreviewScreen = (props, ref) => {
let style1 = { let style1 = {
top: `${top}px`, top: `${top}px`,
left: `${left}px` left: `${left}px`,
} }
setTooltip({ setTooltip({
...@@ -235,7 +260,7 @@ const PreviewScreen = (props, ref) => { ...@@ -235,7 +260,7 @@ const PreviewScreen = (props, ref) => {
tooltipType, tooltipType,
inSide, inSide,
style: style1, style: style1,
squareStyle: squareStyle squareStyle: squareStyle,
}) })
setTimeout(() => { setTimeout(() => {
const childMoveHeight = $('.tooltip').outerHeight() // 获取元素的高度 const childMoveHeight = $('.tooltip').outerHeight() // 获取元素的高度
...@@ -279,7 +304,7 @@ const PreviewScreen = (props, ref) => { ...@@ -279,7 +304,7 @@ const PreviewScreen = (props, ref) => {
top: `${top}px`, top: `${top}px`,
left: `${left}px`, left: `${left}px`,
zIndex: 1000, zIndex: 1000,
opacity: 1 opacity: 1,
} }
setTooltip({ setTooltip({
title, title,
...@@ -289,7 +314,7 @@ const PreviewScreen = (props, ref) => { ...@@ -289,7 +314,7 @@ const PreviewScreen = (props, ref) => {
tooltipType, tooltipType,
inSide, inSide,
style: style2, style: style2,
squareStyle: squareStyle squareStyle: squareStyle,
}) })
}, 100) }, 100)
...@@ -380,7 +405,7 @@ const PreviewScreen = (props, ref) => { ...@@ -380,7 +405,7 @@ const PreviewScreen = (props, ref) => {
setGallery([]) setGallery([])
}) })
$('.expand-content, .gallery-prview-container').on('click', ev => { $('.expand-content, .gallery-prview-container').on('click', (ev) => {
const target = ev.target const target = ev.target
if (target.nodeName.toLowerCase() === 'img') { if (target.nodeName.toLowerCase() === 'img') {
...@@ -412,7 +437,7 @@ const PreviewScreen = (props, ref) => { ...@@ -412,7 +437,7 @@ const PreviewScreen = (props, ref) => {
const onClose = () => { const onClose = () => {
setOpen(false) setOpen(false)
} }
const openDrawer = e => { const openDrawer = (e) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
setOpen(true) setOpen(true)
...@@ -421,7 +446,7 @@ const PreviewScreen = (props, ref) => { ...@@ -421,7 +446,7 @@ const PreviewScreen = (props, ref) => {
if (info.node.children && info.node.children.length > 0) { if (info.node.children && info.node.children.length > 0) {
const temp = JSON.parse(JSON.stringify(expandedKeys)) const temp = JSON.parse(JSON.stringify(expandedKeys))
if (temp.includes(info.node.key)) { if (temp.includes(info.node.key)) {
const newExpands = temp.filter(item => item !== info.node.key) const newExpands = temp.filter((item) => item !== info.node.key)
setExpandedKeys([...newExpands]) setExpandedKeys([...newExpands])
} else { } else {
setExpandedKeys([...temp, info.node.key]) setExpandedKeys([...temp, info.node.key])
...@@ -431,6 +456,7 @@ const PreviewScreen = (props, ref) => { ...@@ -431,6 +456,7 @@ const PreviewScreen = (props, ref) => {
setNewChapterId(info.node.key) setNewChapterId(info.node.key)
newId.current = info.node.key newId.current = info.node.key
setCheckedKeys(checkedKeys) setCheckedKeys(checkedKeys)
setCurrentPage(pageMap[info.node.key] || 0)
setOpen(false) setOpen(false)
} }
} }
...@@ -498,6 +524,13 @@ const PreviewScreen = (props, ref) => { ...@@ -498,6 +524,13 @@ const PreviewScreen = (props, ref) => {
<Spin spinning={loading} wrapperClassName="chapter-loading"> <Spin spinning={loading} wrapperClassName="chapter-loading">
<div className="preview-content-html" dangerouslySetInnerHTML={{ __html: prviewHtml }}></div> <div className="preview-content-html" dangerouslySetInnerHTML={{ __html: prviewHtml }}></div>
</Spin> </Spin>
{currentPage > 0 && (
<div className="preview-page-number">
<span>
{currentPage} / {totalPages}
</span>
</div>
)}
</div> </div>
{isType && ( {isType && (
<div className="preview-content-other"> <div className="preview-content-other">
...@@ -535,7 +568,7 @@ const PreviewScreen = (props, ref) => { ...@@ -535,7 +568,7 @@ const PreviewScreen = (props, ref) => {
<div <div
className="topic-choose-title" className="topic-choose-title"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: `<span class="index">${index + 1}.</span>${item.titles}` __html: `<span class="index">${index + 1}.</span>${item.titles}`,
}}></div> }}></div>
<div className="topic_choose-list"> <div className="topic_choose-list">
...@@ -549,7 +582,7 @@ const PreviewScreen = (props, ref) => { ...@@ -549,7 +582,7 @@ const PreviewScreen = (props, ref) => {
<div <div
className="topic_content" className="topic_content"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: `<span class="correct">${correctList[cindex]}.</span> ${citem.option}` __html: `<span class="correct">${correctList[cindex]}.</span> ${citem.option}`,
}}></div> }}></div>
</div> </div>
) )
...@@ -577,7 +610,7 @@ const PreviewScreen = (props, ref) => { ...@@ -577,7 +610,7 @@ const PreviewScreen = (props, ref) => {
<div <div
className="topic-choose-title" className="topic-choose-title"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: `<span class="index">${index + 1}.</span>${item.titles}` __html: `<span class="index">${index + 1}.</span>${item.titles}`,
}}></div> }}></div>
<div className="topic_choose-list"> <div className="topic_choose-list">
...@@ -591,7 +624,7 @@ const PreviewScreen = (props, ref) => { ...@@ -591,7 +624,7 @@ const PreviewScreen = (props, ref) => {
<div <div
className="topic_content" className="topic_content"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: `<span class="correct">${correctList[cindex]}.</span> ${citem.option}` __html: `<span class="correct">${correctList[cindex]}.</span> ${citem.option}`,
}}></div> }}></div>
</div> </div>
) )
...@@ -619,7 +652,7 @@ const PreviewScreen = (props, ref) => { ...@@ -619,7 +652,7 @@ const PreviewScreen = (props, ref) => {
<div <div
className="topic-choose-title" className="topic-choose-title"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: `<span class="index">${index + 1}.</span>${item.titles}` __html: `<span class="index">${index + 1}.</span>${item.titles}`,
}}></div> }}></div>
<div className="topic_choose-list"> <div className="topic_choose-list">
...@@ -633,7 +666,7 @@ const PreviewScreen = (props, ref) => { ...@@ -633,7 +666,7 @@ const PreviewScreen = (props, ref) => {
<div <div
className="topic_content" className="topic_content"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: `<span class="correct">${correctList[cindex]}.</span> ${citem.option}` __html: `<span class="correct">${correctList[cindex]}.</span> ${citem.option}`,
}}></div> }}></div>
</div> </div>
) )
...@@ -661,7 +694,7 @@ const PreviewScreen = (props, ref) => { ...@@ -661,7 +694,7 @@ const PreviewScreen = (props, ref) => {
<div <div
className="topic-choose-title" className="topic-choose-title"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: `<span class="index">${index + 1}.</span>${item.titles}` __html: `<span class="index">${index + 1}.</span>${item.titles}`,
}}></div> }}></div>
<div className="topic_choose-list"> <div className="topic_choose-list">
...@@ -675,7 +708,7 @@ const PreviewScreen = (props, ref) => { ...@@ -675,7 +708,7 @@ const PreviewScreen = (props, ref) => {
<div <div
className="topic_content" className="topic_content"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: `<span class="correct">${correctList[cindex]}.</span> ${citem.option}` __html: `<span class="correct">${correctList[cindex]}.</span> ${citem.option}`,
}}></div> }}></div>
</div> </div>
) )
...@@ -703,7 +736,7 @@ const PreviewScreen = (props, ref) => { ...@@ -703,7 +736,7 @@ const PreviewScreen = (props, ref) => {
<div <div
className="topic-choose-title" className="topic-choose-title"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: `<span class="index">${index + 1}.</span>${item.titles}` __html: `<span class="index">${index + 1}.</span>${item.titles}`,
}}></div> }}></div>
<div className="topic_choose-list"> <div className="topic_choose-list">
...@@ -717,7 +750,7 @@ const PreviewScreen = (props, ref) => { ...@@ -717,7 +750,7 @@ const PreviewScreen = (props, ref) => {
<div <div
className="topic_content" className="topic_content"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: `${correctList[cindex]}. ${citem.option}` __html: `${correctList[cindex]}. ${citem.option}`,
}}></div> }}></div>
</div> </div>
) )
...@@ -795,6 +828,15 @@ const PreviewScreen = (props, ref) => { ...@@ -795,6 +828,15 @@ const PreviewScreen = (props, ref) => {
defaultExpandAll defaultExpandAll
blockNode blockNode
treeData={gData} treeData={gData}
titleRender={(nodeData) => {
const page = pageMap[nodeData.key]
return (
<span className="tree-node-title">
<span className="tree-node-name">{nodeData.title}</span>
{page != null && <span className="tree-node-page">P{page}</span>}
</span>
)
}}
/> />
</Drawer> </Drawer>
{/* </Spin> */} {/* </Spin> */}
......
...@@ -4,7 +4,8 @@ const { RangePicker } = DatePicker ...@@ -4,7 +4,8 @@ const { RangePicker } = DatePicker
const { TextArea } = Input const { TextArea } = Input
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import PaginationCom from '@/common/Pagination' import PaginationCom from '@/common/Pagination'
import { getList, delUser, exportBook, releaseExamine, importBook, exportToPdf } from '../request' import { getList, delUser, exportBook, releaseExamine, importBook } from '../request'
import { exportHtmlToWord } from '@/utils/exportWordHtmlDocx'
import reset from '@/assets/images/icon/reset.png' import reset from '@/assets/images/icon/reset.png'
import filter from '@/assets/images/icon/filter.png' import filter from '@/assets/images/icon/filter.png'
import add from '@/assets/images/icon/add.png' import add from '@/assets/images/icon/add.png'
...@@ -14,7 +15,6 @@ import dayjs from 'dayjs' ...@@ -14,7 +15,6 @@ import dayjs from 'dayjs'
import TableCom from '@/common/TableCom/index' import TableCom from '@/common/TableCom/index'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { downloadFile } from '@/utils/common' import { downloadFile } from '@/utils/common'
import $ from 'jquery'
const Audit = () => { const Audit = () => {
const [data, setData] = useState([]) const [data, setData] = useState([])
...@@ -34,12 +34,12 @@ const Audit = () => { ...@@ -34,12 +34,12 @@ const Audit = () => {
authors: '', authors: '',
audit_status: null, audit_status: null,
start_time: '', start_time: '',
end_time: '' end_time: '',
}) })
const [dateVal, setDateVal] = useState('') const [dateVal, setDateVal] = useState('')
// 获取操作权限 // 获取操作权限
const { operationPermissionsList } = useSelector(state => state.user) const { operationPermissionsList } = useSelector((state) => state.user)
const { userInfo } = useSelector(state => state.user) const { userInfo } = useSelector((state) => state.user)
// 重置 // 重置
const handleReset = () => { const handleReset = () => {
setFilterObj({ name: '', authors: '', audit_status: null }) setFilterObj({ name: '', authors: '', audit_status: null })
...@@ -85,22 +85,25 @@ const Audit = () => { ...@@ -85,22 +85,25 @@ const Audit = () => {
const exportSuccess = async () => { const exportSuccess = async () => {
setExportModal(false) setExportModal(false)
setLoading(true) setLoading(true)
try {
const { name, content } = await exportBook({ id }) const { name, content } = await exportBook({ id })
if (!name) return if (!name) return
const parser = new DOMParser() await exportHtmlToWord(name, content, [
const doc = parser.parseFromString(content, 'text/html') '.chapter-gallery-container',
$(doc.body) '.chapter-expand',
.find( '.chapter-practice',
".chapter-gallery-container, .chapter-expand, .chapter-practice, .chapter-item-link, .chapter-item-tooltip, div[data-w-e-type='video']" '.chapter-item-link',
) '.chapter-item-tooltip',
.remove() "div[data-w-e-type='video']",
])
const data = await exportToPdf({ name, content: $(doc.body).html() }) } catch (e) {
console.error('导出Word失败', e)
} finally {
setLoading(false) setLoading(false)
downloadFile(`temp/${data.name}`)
} }
const importSuccess = async obj => { }
const importSuccess = async (obj) => {
setUploading(true) setUploading(true)
const bool = await importBook({ ...obj, file: bookFile }) const bool = await importBook({ ...obj, file: bookFile })
// if (!bool) return; // if (!bool) return;
...@@ -124,9 +127,9 @@ const Audit = () => { ...@@ -124,9 +127,9 @@ const Audit = () => {
} }
setBookFile(file) setBookFile(file)
return false return false
},
} }
} const handleReleaseExamine = async (obj) => {
const handleReleaseExamine = async obj => {
const bool = await releaseExamine({ ...obj, id }) const bool = await releaseExamine({ ...obj, id })
if (bool) { if (bool) {
window.location.replace(window.location.href) // 几乎完美 window.location.replace(window.location.href) // 几乎完美
...@@ -147,7 +150,7 @@ const Audit = () => { ...@@ -147,7 +150,7 @@ const Audit = () => {
title: '书籍名称', title: '书籍名称',
key: 'name', key: 'name',
align: 'center', align: 'center',
dataIndex: 'name' dataIndex: 'name',
}, },
{ {
title: '书籍图片', title: '书籍图片',
...@@ -161,13 +164,13 @@ const Audit = () => { ...@@ -161,13 +164,13 @@ const Audit = () => {
src={img} src={img}
fallback="data:image/png;base64,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" fallback="data:image/png;base64,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"
/> />
) ),
}, },
{ {
title: '作者', title: '作者',
key: 'authors', key: 'authors',
align: 'center', align: 'center',
dataIndex: 'authors' dataIndex: 'authors',
}, },
{ {
title: '审核状态', title: '审核状态',
...@@ -201,26 +204,26 @@ const Audit = () => { ...@@ -201,26 +204,26 @@ const Audit = () => {
</Button> </Button>
) )
} }
} },
}, },
{ {
title: '发布次数', title: '发布次数',
key: 'release_num', key: 'release_num',
dataIndex: 'release_num', dataIndex: 'release_num',
width: 150, width: 150,
align: 'center' align: 'center',
}, },
{ {
title: '创建时间', title: '创建时间',
key: 'create_time', key: 'create_time',
align: 'center', align: 'center',
dataIndex: 'create_time' dataIndex: 'create_time',
}, },
{ {
title: '更新时间', title: '更新时间',
key: 'update_time', key: 'update_time',
align: 'center', align: 'center',
dataIndex: 'update_time' dataIndex: 'update_time',
}, },
{ {
title: '操作', title: '操作',
...@@ -287,8 +290,8 @@ const Audit = () => { ...@@ -287,8 +290,8 @@ const Audit = () => {
)} )}
</Space> </Space>
) )
} },
} },
] ]
return ( return (
<> <>
...@@ -300,7 +303,7 @@ const Audit = () => { ...@@ -300,7 +303,7 @@ const Audit = () => {
autoComplete="off" autoComplete="off"
allowClear allowClear
value={filterObj.name} value={filterObj.name}
onChange={ev => handleFilter('name', ev.target.value)} onChange={(ev) => handleFilter('name', ev.target.value)}
placeholder="请输入书籍名称" placeholder="请输入书籍名称"
id="name"></Input> id="name"></Input>
</Form.Item> </Form.Item>
...@@ -309,7 +312,7 @@ const Audit = () => { ...@@ -309,7 +312,7 @@ const Audit = () => {
autoComplete="off" autoComplete="off"
allowClear allowClear
value={filterObj.authors} value={filterObj.authors}
onChange={ev => handleFilter('authors', ev.target.value)} onChange={(ev) => handleFilter('authors', ev.target.value)}
placeholder="请输入作者" placeholder="请输入作者"
id="author"></Input> id="author"></Input>
</Form.Item> </Form.Item>
...@@ -318,7 +321,7 @@ const Audit = () => { ...@@ -318,7 +321,7 @@ const Audit = () => {
style={{ width: 160 }} style={{ width: 160 }}
allowClear allowClear
value={filterObj.audit_status} value={filterObj.audit_status}
onChange={ev => handleFilter('audit_status', ev)} onChange={(ev) => handleFilter('audit_status', ev)}
placeholder="请选择审核状态" placeholder="请选择审核状态"
id="status"> id="status">
<Select.Option value="1">待发布</Select.Option> <Select.Option value="1">待发布</Select.Option>
...@@ -330,7 +333,7 @@ const Audit = () => { ...@@ -330,7 +333,7 @@ const Audit = () => {
<Form.Item label="创建时间"> <Form.Item label="创建时间">
<RangePicker <RangePicker
value={dateVal} value={dateVal}
onChange={ev => handleFilter('startandend', ev)} onChange={(ev) => handleFilter('startandend', ev)}
id="createTime"></RangePicker> id="createTime"></RangePicker>
</Form.Item> </Form.Item>
<Space> <Space>
...@@ -344,7 +347,7 @@ const Audit = () => { ...@@ -344,7 +347,7 @@ const Audit = () => {
display: 'inline-block', display: 'inline-block',
width: '11px', width: '11px',
height: '10px', height: '10px',
pointerEvents: 'none' pointerEvents: 'none',
}}> }}>
<Image src={reset} /> <Image src={reset} />
</span> </span>
...@@ -360,7 +363,7 @@ const Audit = () => { ...@@ -360,7 +363,7 @@ const Audit = () => {
display: 'inline-block', display: 'inline-block',
width: '13px', width: '13px',
height: '12px', height: '12px',
pointerEvents: 'none' pointerEvents: 'none',
}}> }}>
<Image src={filter} /> <Image src={filter} />
</span> </span>
...@@ -369,7 +372,7 @@ const Audit = () => { ...@@ -369,7 +372,7 @@ const Audit = () => {
onClick={() => { onClick={() => {
setPage(1) setPage(1)
console.log(filterObj) console.log(filterObj)
Object.values(filterObj).some(item => item) ? init(filterObj) : init() Object.values(filterObj).some((item) => item) ? init(filterObj) : init()
}}> }}>
筛选 筛选
</Button> </Button>
...@@ -382,13 +385,13 @@ const Audit = () => { ...@@ -382,13 +385,13 @@ const Audit = () => {
display: 'inline-block', display: 'inline-block',
width: '10px', width: '10px',
height: '12px', height: '12px',
pointerEvents: 'none' pointerEvents: 'none',
}}> }}>
<Image src={reload} /> <Image src={reload} />
</span> </span>
} }
onClick={() => { onClick={() => {
Object.values(filterObj).some(item => item) ? init(filterObj) : init() Object.values(filterObj).some((item) => item) ? init(filterObj) : init()
}}> }}>
刷新 刷新
</Button> </Button>
...@@ -406,7 +409,7 @@ const Audit = () => { ...@@ -406,7 +409,7 @@ const Audit = () => {
display: 'inline-block', display: 'inline-block',
width: '11px', width: '11px',
height: '12px', height: '12px',
pointerEvents: 'none' pointerEvents: 'none',
}}> }}>
<Image src={imports} /> <Image src={imports} />
</span> </span>
...@@ -425,7 +428,7 @@ const Audit = () => { ...@@ -425,7 +428,7 @@ const Audit = () => {
display: 'inline-block', display: 'inline-block',
width: '13px', width: '13px',
height: '12px', height: '12px',
pointerEvents: 'none' pointerEvents: 'none',
}}> }}>
<Image src={add} /> <Image src={add} />
</span> </span>
...@@ -501,7 +504,7 @@ const Audit = () => { ...@@ -501,7 +504,7 @@ const Audit = () => {
color: '#999999', color: '#999999',
fontSize: 12, fontSize: 12,
marginBottom: 30, marginBottom: 30,
paddingLeft: 75 paddingLeft: 75,
}}> }}>
目录需设置为word中的标题样式或按照示例编写,防止导入失败 目录需设置为word中的标题样式或按照示例编写,防止导入失败
</p> </p>
...@@ -546,7 +549,7 @@ const Audit = () => { ...@@ -546,7 +549,7 @@ const Audit = () => {
placeholder="请输入发布信息" placeholder="请输入发布信息"
autoSize={{ autoSize={{
minRows: 5, minRows: 5,
maxRows: 8 maxRows: 8,
}} }}
/> />
</Form.Item> </Form.Item>
......
...@@ -209,7 +209,7 @@ const Examine = () => { ...@@ -209,7 +209,7 @@ const Examine = () => {
}) })
if (data) { if (data) {
if ((data.code && data.code === 3000) || data.code === '3000') { if ((data.code && data.code === 3000) || data.code === '3000') {
navigate('/books/management/list') // navigate('/books/management/list')
return return
} else { } else {
setContentMd5(newMd5) setContentMd5(newMd5)
......
import { asBlob } from 'html-docx-js-typescript'
import { saveAs } from 'file-saver'
import $ from 'jquery'
/**
* 将图片 URL 转为 base64 Data URI
* 优先使用 fetch,失败后尝试 Image + Canvas 方式
*/
async function imgToBase64(src) {
// 方式1:fetch
try {
const res = await fetch(src)
if (res.ok) {
const blob = await res.blob()
return await blobToDataUri(blob)
}
} catch (e) {
console.warn('fetch 图片失败,尝试 canvas 方式:', src, e)
}
// 方式2:Image + Canvas(可绕过部分限制)
try {
return await loadImageAsBase64(src)
} catch (e) {
console.warn('canvas 转换图片失败:', src, e)
}
return null
}
function blobToDataUri(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result)
reader.onerror = reject
reader.readAsDataURL(blob)
})
}
function loadImageAsBase64(src) {
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
try {
const canvas = document.createElement('canvas')
canvas.width = img.naturalWidth
canvas.height = img.naturalHeight
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
resolve(canvas.toDataURL('image/png'))
} catch (e) {
reject(e)
}
}
img.onerror = reject
img.src = src
})
}
/**
* 将 DOM 中所有 img 的 src 替换为 base64 Data URI
*/
async function convertImagesToBase64(docEl) {
const imgs = docEl.querySelectorAll('img')
const tasks = Array.from(imgs).map(async (img) => {
const src = img.getAttribute('src')
if (!src || src.startsWith('data:')) return
// 处理相对路径和协议相对路径
let absoluteSrc = src
if (src.startsWith('//')) {
absoluteSrc = window.location.protocol + src
} else if (src.startsWith('/')) {
absoluteSrc = window.location.origin + src
} else if (!src.startsWith('http')) {
absoluteSrc = new URL(src, window.location.href).href
}
const dataUri = await imgToBase64(absoluteSrc)
if (dataUri) {
img.setAttribute('src', dataUri)
} else {
console.warn('图片转 base64 失败,已跳过:', absoluteSrc)
img.remove()
}
})
await Promise.all(tasks)
}
/**
* 使用 html-docx-js-typescript 将 HTML 内容导出为 Word 文档(.docx)
* 图片会自动转为 base64 内嵌到文档中
* @param {string} name - 文件名(不含扩展名)
* @param {string} htmlString - 原始 HTML 字符串
* @param {string[]} removeSelectors - 需要移除的 CSS 选择器列表
*/
export async function exportHtmlToWord(name, htmlString, removeSelectors = []) {
const parser = new DOMParser()
const doc = parser.parseFromString(htmlString, 'text/html')
if (removeSelectors.length) {
$(doc.body).find(removeSelectors.join(', ')).remove()
}
// 将所有图片转为 base64 内嵌
await convertImagesToBase64(doc.body)
// 使用原生 innerHTML 避免 jQuery 序列化可能编码 base64 特殊字符
const htmlContent = doc.body.innerHTML
const fullHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
@page { size: A4; margin: 2cm; }
body { font-family: SimSun, serif; font-size: 14px; line-height: 1.6; }
table { border-collapse: collapse; width: 100%; }
td, th { border: 1px solid #000; padding: 5px; }
img { max-width: 100%; }
</style></head><body>${htmlContent}</body></html>`
const blob = await asBlob(fullHtml, { orientation: 'portrait' })
saveAs(blob, `${name}.docx`)
}
...@@ -38,7 +38,8 @@ export default defineConfig(() => { ...@@ -38,7 +38,8 @@ export default defineConfig(() => {
// rewrite: (path) => path.replace(/^\/api\/wenku/, '/'), // rewrite: (path) => path.replace(/^\/api\/wenku/, '/'),
// }, // },
'/api': { '/api': {
target: 'https://zijingebook.ezijing.com', // target: 'https://zijingebook.ezijing.com',
target: 'http://localhost:7419',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
}, },
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论