提交 1ebb03ec authored 作者: 王鹏飞's avatar 王鹏飞

chore: update

上级 a1fc9f27
......@@ -31,7 +31,6 @@
"echarts": "^5.4.3",
"file-saver": "^2.0.5",
"highlight.js": "^11.11.1",
"html-docx-js-typescript": "^0.1.5",
"jquery": "^3.7.1",
"js-cookie": "^3.0.5",
"js-md5": "^0.8.3",
......@@ -42,8 +41,8 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13",
"react-use": "^17.6.0",
"react-router": "^7.18.1",
"react-use": "^17.6.0",
"snabbdom": "^3.6.2",
"zustand": "^4.5.5"
},
......
......@@ -59,8 +59,7 @@ const renderAudio = (elem, children, editor) => {
},
[
h('audio', {
props: { src: audioUrl, controls: true, autoPlay: true },
style: { width: '220px', height: '100px' },
props: { src: audioUrl, controls: true },
}),
],
)
......@@ -129,7 +128,7 @@ const chapterAudioToHtml = (elem, childrenHtml) => {
// 生成 HTML 代码
const html = `<div class="chapter-audio-container" data-w-e-type="chapterAudio" data-w-e-is-void data-audioUrl="${audioUrl}" data-title="${title}" style="text-align: center; padding: 10px 0;">
<div class="chapter-audio-pic" style="text-align: center;">
<audio controls src="${audioUrl}" style="width: 220px; height: 100px;" alt="${title}" />
<audio controls src="${audioUrl}" alt="${title}" />
</div>
${title ? `<div class="chapter-audio-title" style="text-align: center; color: #666; font-size: 14px; line-height: 20px;">${title}</div>` : ''}
</div>`
......
import { useState, forwardRef, useImperativeHandle } from 'react'
import { Divider, Input, Space, Button, Form, ColorPicker } from 'antd'
import { SlateTransforms, SlateEditor, SlateElement } from '@wangeditor/editor'
import {
insertBlockNodesAndFocus,
removeSelectedEmptyParagraph,
} from '../../shared/editor-operations'
import { useEffect, useState } from 'react'
import { Button, Col, ColorPicker, Input, message, Row, Space } from 'antd'
import { CloseOutlined } from '@ant-design/icons'
import { SlateEditor, SlateElement, SlateTransforms } from '@wangeditor/editor'
import { useEditorStore } from '@/stores/editorStore'
import '../../index.less'
import { getChapterTopic } from '../../shared/request'
import { bookNameList } from './question-api'
import CustomerTopic from './components/customer'
import InsertTopic from './components/insert'
import ListTopic from './components/list'
import './styles.less'
const PracticeModal = (props, ref) => {
const { bookId, chapterId, setPracticeOpenVisible } = props
const PracticeSettingsDialog = ({ editor, chapterId, bookId, closePanel }) => {
const practiceRandom = useEditorStore((state) => state.practiceRandom)
const practiceTitle = useEditorStore((state) => state.practiceTitle)
const practiceTheme = useEditorStore((state) => state.practiceTheme)
const setPracticeRandom = useEditorStore((state) => state.setPracticeRandom)
const { editor } = props
const [form] = Form.useForm()
const [themeValue, setThemeValue] = useState('#ab1941')
const [initValues, setInitValues] = useState({ practice: '', theme: '#ab1941' })
const [nodes, setNodes] = useState(null)
useImperativeHandle(ref, () => {
return {
nodes,
}
const [theme, setTheme] = useState(practiceTheme || '#ab1941')
const [title, setTitle] = useState(practiceTitle || '')
const [editingTitle, setEditingTitle] = useState(false)
const [manualAdd, setManualAdd] = useState(false)
const [customerType, setCustomerType] = useState('')
const [bookList, setBookList] = useState([])
const [selectedQuestions, setSelectedQuestions] = useState([])
const [insertedQuestions, setInsertedQuestions] = useState([])
useEffect(() => {
setTheme(practiceTheme || '#ab1941')
setTitle(practiceTitle || '')
}, [practiceTheme, practiceTitle])
useEffect(() => {
bookNameList({}).then((data) => setBookList(data || []))
}, [])
useEffect(() => {
if (!bookId || !chapterId || !practiceRandom) return
getChapterTopic({
book_id: bookId,
chapter_id: chapterId,
position: practiceRandom,
}).then((data) => {
const questions = (data || []).map((item) => ({ ...item, choose_id: item.question_id }))
setInsertedQuestions(questions)
setSelectedQuestions(questions)
})
}, [bookId, chapterId, practiceRandom])
const textColorChange = (value) => {
const hex = typeof value === 'string' ? value : value?.toHexString()
setThemeValue(hex)
form.setFieldsValue({ theme: hex })
const updatePracticeTitle = () => {
const nextTitle = title.trim()
if (!nextTitle) {
message.error('请输入标题')
return
}
const callback = (practiceNum, practiceTitle) => {
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
// JS syntax
if (SlateElement.isElement(node)) {
if (node.type === 'chapterPractice') {
return true
}
}
return false
},
for (const [node, path] of SlateEditor.nodes(editor, {
match: (node) => SlateElement.isElement(node) && node.type === 'chapterPractice',
universal: true,
})) {
if (node.practiceNum !== practiceRandom) continue
SlateTransforms.setNodes(editor, { ...node, title: nextTitle, theme }, { at: path })
setPracticeRandom({
practiceNum: node.practiceNum,
practiceTitle: nextTitle,
practiceTheme: theme,
})
setNodes(nodeEntries)
setPracticeRandom({ practiceNum, practiceTitle })
setEditingTitle(false)
return
}
}
const onFinish = (values) => {
editor.restoreSelection()
removeSelectedEmptyParagraph(editor, SlateTransforms)
const practiceNum = Math.random().toString(32).substring(2, 10)
insertBlockNodesAndFocus(editor, SlateTransforms, {
type: 'chapterPractice',
title: values.practice,
practiceNum: practiceNum,
bookId,
chapterId,
theme: values.theme,
callback: callback,
children: [{ text: '' }],
})
form.resetFields()
setPracticeOpenVisible(false)
editor.focus()
const addSelectedQuestions = () => {
if (selectedQuestions.length === 0) {
message.error('请先勾选试题')
return
}
const existed = new Set(insertedQuestions.map((item) => String(item.choose_id)))
setInsertedQuestions((current) => [
...current,
...selectedQuestions.filter((item) => !existed.has(String(item.choose_id))),
])
}
return (
<div>
<Divider />
<div className='editor-content-form'>
<Form
layout='vertical'
name='validate_other'
form={form}
onFinish={onFinish}
initialValues={initValues}
>
<Form.Item
label='标题'
name='practice'
rules={[{ required: true, message: '请输入标题' }]}
extra='最多输入100字'
>
<Input maxLength={100} placeholder='' allowClear />
</Form.Item>
<Form.Item>
<div className='justcontent-color-inline'>
<Form.Item
label='练习主题色'
name='theme'
className='flex-max'
rules={[
{ required: true, message: '请选择颜色' },
{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' },
]}
>
<Input placeholder='' allowClear />
</Form.Item>
<Form.Item label={` `}>
<ColorPicker
disabledAlpha
value={themeValue}
defaultValue={themeValue}
format='hex'
onChange={textColorChange}
/>
</Form.Item>
<Row gutter={20} className='practice-topic-row'>
<Col span={12} className='common practice-left'>
<div className='practice-header'>
<div className='practice-l-img'>
<svg className='svg-icon' aria-hidden='true'>
<use xlinkHref='#icon-writing' />
</svg>
</div>
</Form.Item>
<Form.Item className='editor-form-buttons'>
<div className='practice-l-title'>
{editingTitle ? (
<Space>
<Button type='default' onClick={() => setPracticeOpenVisible(false)}>
取消
</Button>
<Button type='primary' htmlType='submit'>
插入
</Button>
<Input value={title} onChange={(event) => setTitle(event.target.value)} />
<ColorPicker disabledAlpha value={theme} format='hex' onChange={(value) => setTheme(value.toHexString())} />
<Button type='primary' onClick={updatePracticeTitle}>确定</Button>
<Button onClick={() => setEditingTitle(false)}>取消</Button>
</Space>
</Form.Item>
</Form>
) : (
<h2 className='title' onClick={() => setEditingTitle(true)}>
<ColorPicker disabled value={theme} size='small' />
<span className='titles'>{practiceTitle}</span>
<span className='click'>(点击编辑)</span>
</h2>
)}
</div>
</div>
<div className='practice-topic-list'>
<InsertTopic
insertList={insertedQuestions}
setInsertList={setInsertedQuestions}
chooseList={selectedQuestions}
setChooseList={setSelectedQuestions}
practiceRandom={practiceRandom}
chapterId={chapterId}
bookId={bookId}
closePanel={closePanel}
/>
</div>
</Col>
<Col span={12} className='common practice-right'>
<div className='practice-header right'>
<Button onClick={() => setManualAdd((value) => !value)}>
{manualAdd ? '从题库选择' : '新增试题'}
</Button>
<Button type='link' icon={<CloseOutlined style={{ color: '#b83956' }} />} onClick={closePanel} />
</div>
<div className='practice-topic-list'>
{manualAdd ? (
<CustomerTopic
setPracticeStatus={setManualAdd}
bookNameIdData={bookList}
bookId={bookId}
setCustomerType={setCustomerType}
/>
) : (
<ListTopic
chooseList={selectedQuestions}
setChooseList={setSelectedQuestions}
addTopicList={addSelectedQuestions}
practiceStatus={manualAdd}
bookNameIdData={bookList}
bookId={bookId}
customerType={customerType}
/>
)}
</div>
</Col>
</Row>
)
}
export default forwardRef(PracticeModal)
export default PracticeSettingsDialog
......@@ -65,8 +65,7 @@ const renderVideo = (elem, children, editor) => {
},
[
h('video', {
props: { src: videoUrl, controls: true, autoPlay: true },
style: { width: '320px', height: '100px' },
props: { src: videoUrl, controls: true },
}),
],
)
......@@ -142,7 +141,7 @@ const chapterVideoToHtml = (elem, childrenHtml) => {
style="text-align: center; padding: 10px 0;"
>
<div class="chapter-video-pic" style="text-align: center;">
<video controls src="${videoUrl}" style="width: 320px; height: 100px;" alt="${title}" />
<video controls src="${videoUrl}" alt="${title}" />
</div>
${title ? `<div class="chapter-video-title" style="text-align: center; color: #666; font-size: 14px; line-height: 20px;">${title}</div>` : ''}
</div>`
......
......@@ -22,9 +22,11 @@ export interface ManagementListResult {
export interface ExportBookResult {
name?: string
content?: string
download_url?: string
}
export type ExportBookType = 'word' | 'pdf' | 'epub'
export interface ImportBookValues {
catalog_level: number
file: File
......@@ -69,8 +71,8 @@ export function updateBook(params: ApiParams) {
return axios.post('/api/admin/book/teacher/edit', params)
}
export function exportManagementBook(id: number) {
return getPayload<ExportBookResult>('/api/admin/book/teacher/export', { id })
export function exportManagementBook(id: number, type: ExportBookType) {
return getPayload<ExportBookResult>('/api/admin/book/teacher/export', { id, type })
}
export function releaseManagementBook(id: number, releaseMess?: string) {
......
import { Modal, Row, Space } from 'antd'
import { Modal, Radio, Row, Space } from 'antd'
import { useEffect, useState } from 'react'
export type ExportFormat = 'word' | 'pdf' | 'epub'
interface ExportBookModalProps {
open: boolean
onCancel: () => void
onConfirm: () => void | Promise<void>
onConfirm: (format: ExportFormat) => void | Promise<void>
}
export default function ExportBookModal({ open, onCancel, onConfirm }: ExportBookModalProps) {
const [format, setFormat] = useState<ExportFormat>('word')
useEffect(() => {
if (open) setFormat('word')
}, [open])
return (
<Modal
mask={false}
centered
open={open}
onOk={onConfirm}
onOk={() => onConfirm(format)}
onCancel={onCancel}
footer={(_, { OkBtn, CancelBtn }) => (
<Row justify='center' className='delModal' style={{ marginBottom: '30px' }}>
......@@ -24,6 +33,14 @@ export default function ExportBookModal({ open, onCancel, onConfirm }: ExportBoo
)}
okText='确认'
>
<div style={{ textAlign: 'center', paddingTop: 28 }}>
<div style={{ marginBottom: 12, color: '#666' }}>导出格式</div>
<Radio.Group value={format} onChange={(event) => setFormat(event.target.value)}>
<Radio value='word'>Word</Radio>
<Radio value='pdf'>PDF</Radio>
<Radio value='epub'>EPUB</Radio>
</Radio.Group>
</div>
<p style={{ textAlign: 'center', padding: '30px 0 18px', fontSize: 16 }}>
将导出书籍正文、图片、画廊、扩展阅读、练习题、链接、气泡提示、视频和音频
</p>
......
import { describe, expect, it } from 'vitest'
import { normalizeBookExportHtml } from './normalizeBookExportHtml'
describe('normalizeBookExportHtml', () => {
it('turns editor-only blocks into static Word content', async () => {
const html = `
<div data-export-chapter-id="8">
<div class="chapter-gallery-container" data-galleryList="${encodeURIComponent(JSON.stringify([{ url: 'https://cdn.example.com/a.png', title: '图片说明' }]))}" data-title="画廊标题"></div>
<div class="chapter-expand" data-w-e-type="chapterExpandRead" data-random="expand-1" data-name="扩展阅读"></div>
<div class="chapter-practice" data-w-e-type="chapterPractice" data-practiceNum="practice-1"></div>
<span class="chapter-item-link" data-w-e-type="chapterLink" data-title="资料链接" data-linktype="1" data-link="https://example.com"></span>
<span class="chapter-item-tooltip" data-w-e-type="chapterTooltip" data-title="术语" data-content="术语解释"></span>
<div class="chapter-video-container" data-w-e-type="chapterVideo" data-title="课程视频" data-videoUrl="https://example.com/video.mp4"></div>
</div>
`
const result = await normalizeBookExportHtml(html, {
bookId: 7,
fetchExpandRead: async (params) => {
expect(params).toEqual({ book_id: 7, chapter_id: 8, position: 'expand-1' })
return { content: '<p>扩展正文</p>' }
},
fetchPracticeQuestions: async (params) => {
expect(params).toEqual({ book_id: 7, chapter_id: 8, position: 'practice-1' })
return [
{
titles: '单选题题干',
question_style: 1,
question_list: JSON.stringify([{ option: '选项 A' }, { option: '选项 B' }]),
answers: 'A',
analysis: '题目解析',
},
]
},
})
expect(result).toContain('画廊标题')
expect(result).toContain('https://cdn.example.com/a.png')
expect(result).toContain('图片说明')
expect(result).toContain('扩展正文')
expect(result).toContain('单选题题干')
expect(result).toContain('选项 A')
expect(result).toContain('答案:A')
expect(result).toContain('资料链接')
expect(result).toContain('术语解释')
expect(result).toContain('课程视频')
expect(result).toContain('https://example.com/video.mp4')
expect(result).not.toContain('chapter-practice')
expect(result).not.toContain('<video')
})
})
export interface ExportPracticeQuestion {
titles?: string
question_style?: number | string
question_list?: string | unknown[]
answers?: string
analysis?: string
}
interface ExportParams {
book_id: number
chapter_id: number
position: string
}
interface NormalizeBookExportOptions {
bookId: number
fetchExpandRead: (params: ExportParams) => Promise<{ content?: string } | null | undefined>
fetchPracticeQuestions: (params: ExportParams) => Promise<ExportPracticeQuestion[]>
}
function getChapterId(element: Element): number | null {
const container = element.closest('[data-export-chapter-id]')
const chapterId = Number(container?.getAttribute('data-export-chapter-id'))
return Number.isInteger(chapterId) && chapterId > 0 ? chapterId : null
}
function createTextElement<K extends keyof HTMLElementTagNameMap>(
tagName: K,
text: string,
className?: string,
): HTMLElementTagNameMap[K] {
const element = document.createElement(tagName)
element.textContent = text
if (className) element.className = className
return element
}
function parseGalleryList(value: string | null): Array<{ url?: string; title?: string }> {
if (!value) return []
try {
return JSON.parse(decodeURIComponent(value))
} catch {
try {
return JSON.parse(value)
} catch {
return []
}
}
}
function replaceGallery(element: Element) {
const section = document.createElement('section')
section.className = 'word-export-gallery'
const title = element.getAttribute('data-title')?.trim()
if (title) section.append(createTextElement('h4', title))
parseGalleryList(element.getAttribute('data-galleryList')).forEach((item) => {
if (!item.url) return
const figure = document.createElement('figure')
const image = document.createElement('img')
image.src = item.url
image.alt = item.title ?? ''
figure.append(image)
if (item.title) figure.append(createTextElement('figcaption', item.title))
section.append(figure)
})
element.replaceWith(section)
}
function replaceLink(element: Element) {
const title = element.getAttribute('data-title') || element.textContent?.trim() || '链接'
const link = element.getAttribute('data-link')
if (link) {
const anchor = document.createElement('a')
anchor.href = link
anchor.textContent = title
element.replaceWith(anchor)
return
}
element.replaceWith(createTextElement('span', title))
}
function replaceTooltip(element: Element) {
const title = element.getAttribute('data-title') || element.textContent?.trim() || '提示'
const content = element.getAttribute('data-content') || ''
const note = createTextElement('span', `${title}(提示:${content})`, 'word-export-tooltip')
element.replaceWith(note)
}
function replaceMedia(element: Element, type: '视频' | '音频', urlAttribute: string) {
const title = element.getAttribute('data-title') || `${type}内容`
const url =
element.getAttribute(urlAttribute) ||
element.querySelector(type === '视频' ? 'video' : 'audio')?.getAttribute('src') ||
''
const wrapper = document.createElement('p')
wrapper.className = 'word-export-media'
wrapper.append(`${type}:${title}`)
if (url) {
const anchor = document.createElement('a')
anchor.href = url
anchor.textContent = '(点击播放)'
wrapper.append(anchor)
}
element.replaceWith(wrapper)
}
function parseQuestionList(questionList: ExportPracticeQuestion['question_list']): unknown[] {
if (Array.isArray(questionList)) return questionList
if (typeof questionList !== 'string') return []
try {
const value = JSON.parse(questionList)
return Array.isArray(value) ? value : []
} catch {
return []
}
}
function formatOption(option: unknown): string {
if (typeof option === 'string') return option
if (option && typeof option === 'object') {
const value = option as { option?: string; title?: string; content?: string; label?: string }
return value.option ?? value.title ?? value.content ?? value.label ?? ''
}
return ''
}
function createHtmlParagraph(prefix: string, html: string) {
const paragraph = document.createElement('p')
paragraph.append(prefix)
const content = new DOMParser().parseFromString(html, 'text/html')
while (content.body.firstChild) paragraph.append(content.body.firstChild)
return paragraph
}
function createPracticeSection(title: string, questions: ExportPracticeQuestion[]) {
const section = document.createElement('section')
section.className = 'word-export-practice'
section.append(createTextElement('h4', title || '练习题'))
questions.forEach((question, index) => {
section.append(createHtmlParagraph(`${index + 1}. `, question.titles ?? ''))
parseQuestionList(question.question_list).forEach((option, optionIndex) => {
const text = formatOption(option)
if (text)
section.append(createHtmlParagraph(`${String.fromCharCode(65 + optionIndex)}. `, text))
})
if (question.answers) section.append(createTextElement('p', `答案:${question.answers}`))
if (question.analysis) section.append(createTextElement('p', `解析:${question.analysis}`))
})
return section
}
async function replaceExpandRead(element: Element, options: NormalizeBookExportOptions) {
const chapterId = getChapterId(element)
const title =
element.getAttribute('data-title') || element.getAttribute('data-name') || '扩展阅读'
const random = element.getAttribute('data-random') || ''
const section = document.createElement('section')
section.className = 'word-export-expand'
section.append(createTextElement('h4', title))
if (chapterId && random) {
const result = await options.fetchExpandRead({
book_id: options.bookId,
chapter_id: chapterId,
position: random,
})
if (result?.content) {
const content = new DOMParser().parseFromString(result.content, 'text/html')
while (content.body.firstChild) section.append(content.body.firstChild)
}
}
element.replaceWith(section)
}
async function replacePractice(element: Element, options: NormalizeBookExportOptions) {
const chapterId = getChapterId(element)
const practiceNum = element.getAttribute('data-practiceNum') || ''
const title = element.getAttribute('data-title') || '练习题'
const questions =
chapterId && practiceNum
? await options.fetchPracticeQuestions({
book_id: options.bookId,
chapter_id: chapterId,
position: practiceNum,
})
: []
element.replaceWith(createPracticeSection(title, questions))
}
/** Convert WangEditor custom blocks into static HTML that Word can render. */
export async function normalizeBookExportHtml(
htmlString: string,
options: NormalizeBookExportOptions,
): Promise<string> {
const doc = new DOMParser().parseFromString(htmlString, 'text/html')
doc.body.querySelectorAll('.chapter-gallery-container').forEach(replaceGallery)
doc.body.querySelectorAll('[data-w-e-type="chapterLink"]').forEach(replaceLink)
doc.body.querySelectorAll('[data-w-e-type="chapterTooltip"]').forEach(replaceTooltip)
doc.body
.querySelectorAll(
'[data-w-e-type="chapterVideo"], [data-w-e-type="video"], .chapter-video-container',
)
.forEach((element) => replaceMedia(element, '视频', 'data-videoUrl'))
doc.body
.querySelectorAll('[data-w-e-type="chapterAudio"], .chapter-audio-container')
.forEach((element) => replaceMedia(element, '音频', 'data-audioUrl'))
await Promise.all(
Array.from(
doc.body.querySelectorAll('[data-w-e-type="chapterExpandRead"], .chapter-expand'),
).map((element) => replaceExpandRead(element, options)),
)
await Promise.all(
Array.from(
doc.body.querySelectorAll('[data-w-e-type="chapterPractice"], .chapter-practice'),
).map((element) => replacePractice(element, options)),
)
return doc.body.innerHTML
}
import { Button, DatePicker, Image, Input, Select, Space } from 'antd'
import { PlusOutlined, UploadOutlined } from '@ant-design/icons'
import type { ColumnsType } from 'antd/es/table'
import { saveAs } from 'file-saver'
import { useState } from 'react'
import { useNavigate } from 'react-router'
import { AppList, type AppListFilterItem } from '@/components/AppList'
import { expandReadInfo, getChapterTopic } from '@/components/editor/wangeditor-customer/public-api'
import { exportHtmlToWord } from '@/utils/exportWordHtmlDocx'
import { usePermission } from '@/hooks/usePermission'
import { useUserStore } from '@/stores/userStore'
......@@ -19,8 +18,7 @@ import {
type ManagementRow,
} from '../api'
import DeleteBookModal from '../components/DeleteBookModal'
import ExportBookModal from '../components/ExportBookModal'
import { normalizeBookExportHtml } from '../export/normalizeBookExportHtml'
import ExportBookModal, { type ExportFormat } from '../components/ExportBookModal'
import ImportBookDrawer from '../components/ImportBookDrawer'
import ReleaseBookDrawer, { type ReleaseBookFormValues } from '../components/ReleaseBookDrawer'
import type { ManagementFilters } from '../query'
......@@ -28,6 +26,7 @@ import { useManagementList } from '../useManagementList'
const { RangePicker } = DatePicker
const IMAGE_FALLBACK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='
export default function ManagementListView() {
const list = useManagementList()
const navigate = useNavigate()
......@@ -47,21 +46,20 @@ export default function ManagementListView() {
setDeleteOpen(false)
}
const handleExport = async () => {
const handleExport = async (format: ExportFormat) => {
if (selectedId === null) return
setExportOpen(false)
setExporting(true)
try {
const result = await exportManagementBook(selectedId)
if (!result.name) return
const content = await normalizeBookExportHtml(result.content ?? '', {
bookId: selectedId,
fetchExpandRead: expandReadInfo,
fetchPracticeQuestions: getChapterTopic,
})
await exportHtmlToWord(result.name, content)
const result = await exportManagementBook(selectedId, format)
if (!result.name) {
return
}
if (!result.download_url) throw new Error('未获取到导出文件地址')
saveAs(`/api/admin${result.download_url}`, result.name)
} catch (error) {
console.error('导出Word失败', error)
const formatLabel = format === 'pdf' ? 'PDF' : format === 'epub' ? 'EPUB' : 'Word'
console.error(`导出${formatLabel}失败`, error)
} finally {
setExporting(false)
}
......
import { asBlob } from 'html-docx-js-typescript'
import { saveAs } from 'file-saver'
/**
* 将图片 URL 转为 base64 Data URI
* 优先使用 fetch,失败后尝试 Image + Canvas 方式
*/
async function imgToBase64(src) {
// 方式1:fetch
try {
// OSS 刚更新 CORS 规则时,浏览器可能复用规则生效前的无 CORS 缓存响应。
// 导出只在用户主动触发时执行,强制重新校验可确保读取当前响应头。
const res = await fetch(src, { cache: 'reload' })
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.querySelectorAll(removeSelectors.join(', ')).forEach((element) => element.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`)
}
......@@ -108,11 +108,11 @@ export function BookDetailView() {
)
}
if (isTrialAvailable(book)) return openReader(book.readChapterId)
messageApi.info('购买功能待接入')
messageApi.info('请使用 APP 购买')
}
const selectChapter = (chapter: BookChapter) => {
if (chapterAccess(chapter)) return openReader(chapter.id)
messageApi.info('购买功能待接入')
messageApi.info('请使用 APP 购买')
}
if (detailQuery.isLoading) {
......
......@@ -68,7 +68,7 @@ function makeDocument(content: string, notes: ReaderNote[]) {
<html lang="zh-CN"><head><meta charset="utf-8"><style>
:root { color: #2b2520; background: #fffdf8; font-family: Georgia, "Noto Serif SC", "Songti SC", serif; }
body { box-sizing: border-box; max-width: 780px; margin: 0 auto; padding: 48px 40px 96px; font-size: 18px; line-height: 2; overflow-wrap: anywhere; }
img, video, audio { max-width: 100%; height: auto; } table { display: block; max-width: 100%; overflow-x: auto; } pre { overflow-x: auto; padding: 16px; background: #f5f0e7; } a { color: #a94137; }
img, video { max-width: 100%; height: auto; } audio { max-width: 100%; } table { display: block; max-width: 100%; overflow-x: auto; } pre { overflow-x: auto; padding: 16px; background: #f5f0e7; } a { color: #a94137; }
</style><style>
mark.reader-annotation { padding: 0; border-radius: 2px; color: inherit; cursor: pointer; }
mark.reader-annotation--highlight { background: var(--annotation-color, #fff08a); }
......
......@@ -18,7 +18,7 @@ importers:
version: 2.2.5
'@ezijing/ai-react':
specifier: ^1.0.40
version: 1.0.44(react@18.3.1)
version: 1.0.44(debug@4.4.3)(react@18.3.1)
'@fortaine/fetch-event-source':
specifier: ^3.0.6
version: 3.0.6
......@@ -42,7 +42,7 @@ importers:
version: 5.29.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
axios:
specifier: ^1.18.1
version: 1.18.1
version: 1.18.1(debug@4.4.3)
dayjs:
specifier: ^1.11.11
version: 1.11.21
......@@ -58,9 +58,6 @@ importers:
highlight.js:
specifier: ^11.11.1
version: 11.11.1
html-docx-js-typescript:
specifier: ^0.1.5
version: 0.1.5
jquery:
specifier: ^3.7.1
version: 3.7.1
......@@ -121,7 +118,7 @@ importers:
version: 18.3.7(@types/react@18.3.31)
'@vitejs/plugin-react':
specifier: ^6.0.3
version: 6.0.3(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7))
version: 6.0.3(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0))
jsdom:
specifier: ^29.1.1
version: 29.1.1
......@@ -139,13 +136,13 @@ importers:
version: 7.0.2
vite:
specifier: ^8.1.5
version: 8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)
version: 8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0)
vite-plugin-mkcert:
specifier: ^1.17.10
version: 1.17.12(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7))
version: 1.17.12(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0))
vitest:
specifier: ^4.1.10
version: 4.1.10(@types/node@22.20.1)(jsdom@29.1.1)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7))
version: 4.1.10(@types/node@22.20.1)(jsdom@29.1.1)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0))
apps/learning:
dependencies:
......@@ -163,7 +160,7 @@ importers:
version: 6.5.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
axios:
specifier: ^1.8.4
version: 1.18.1
version: 1.18.1(debug@4.4.3)
js-md5:
specifier: ^0.8.3
version: 0.8.3
......@@ -203,7 +200,7 @@ importers:
version: 19.2.3(@types/react@19.2.17)
'@vitejs/plugin-react':
specifier: ^6.0.3
version: 6.0.3(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7))
version: 6.0.3(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0))
jsdom:
specifier: ^29.1.1
version: 29.1.1
......@@ -218,13 +215,13 @@ importers:
version: 7.0.2
vite:
specifier: ^8.1.5
version: 8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)
version: 8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0)
vite-plugin-mkcert:
specifier: ^1.17.10
version: 1.17.12(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7))
version: 1.17.12(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0))
vitest:
specifier: ^4.1.10
version: 4.1.10(@types/node@22.20.1)(jsdom@29.1.1)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7))
version: 4.1.10(@types/node@22.20.1)(jsdom@29.1.1)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0))
packages:
......@@ -562,9 +559,22 @@ packages:
resolution: {integrity: sha512-621GAuLMvKtyZQ3IA6nlDWhV1V/7PGOTNIGLUifxt0KzM+dZIweJ6F3XvQF3QnqeNfS1N7WQ0Kil1Di/lhChEw==}
engines: {node: '>=16.15'}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/source-map@0.3.11':
resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@napi-rs/wasm-runtime@1.1.6':
resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
peerDependencies:
......@@ -1609,6 +1619,11 @@ packages:
'@xobotyi/scrollbar-width@1.9.5':
resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==}
acorn@8.17.0:
resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
engines: {node: '>=0.4.0'}
hasBin: true
address@1.2.2:
resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
engines: {node: '>= 10.0.0'}
......@@ -1680,8 +1695,8 @@ packages:
bowser@1.9.4:
resolution: {integrity: sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==}
browser-or-node@1.3.0:
resolution: {integrity: sha512-0F2z/VSnLbmEeBcUrSuDH5l0HxTXdQQzLjkmBR4cYfvg1zJrKSlmIZFqyFR8oX0NrwPhy3c3HQ6i3OxMbew4Tg==}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
builtin-status-codes@3.0.0:
resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==}
......@@ -1709,6 +1724,9 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
compute-scroll-into-view@1.0.20:
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
......@@ -1992,9 +2010,6 @@ packages:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
html-docx-js-typescript@0.1.5:
resolution: {integrity: sha512-GNojWFDYbpHSIgKml6/0oAom8mtHrHRTWKMyLRdeJQHO/CyeM6H39DYgzYvPp4OhBp2Ti8dxMKFq0/FkpYD4bg==}
html-encoding-sniffer@6.0.0:
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
......@@ -2024,9 +2039,6 @@ packages:
engines: {node: '>=0.10.0'}
hasBin: true
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immer@9.0.21:
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
......@@ -2110,17 +2122,11 @@ packages:
jstoxml@2.2.9:
resolution: {integrity: sha512-OYWlK0j+roh+eyaMROlNbS5cd5R25Y+IUpdl7cNdB8HNrkgwQzIS7L9MegxOiWNBj9dQhA/yAxiMwCC5mwNoBw==}
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
less@4.6.7:
resolution: {integrity: sha512-o3UxHBPPVY1HtCXx15/z1NlknQiWyafRNbtLEv+6xFaDRI2g2xPKIH43do9dSwt8bGLTsjNSaifa48N3d6odsQ==}
engines: {node: '>=18'}
hasBin: true
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
......@@ -2404,9 +2410,6 @@ packages:
vite-plus:
optional: true
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
parse-node-version@1.0.1:
resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==}
engines: {node: '>= 0.10'}
......@@ -2833,9 +2836,6 @@ packages:
resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==}
engines: {node: '>=6.9'}
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
side-channel-list@1.0.1:
resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
engines: {node: '>= 0.4'}
......@@ -2871,6 +2871,9 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
source-map@0.5.6:
resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==}
engines: {node: '>=0.10.0'}
......@@ -2927,6 +2930,11 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
terser@5.49.0:
resolution: {integrity: sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==}
engines: {node: '>=10'}
hasBin: true
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
......@@ -2991,9 +2999,6 @@ packages:
ts-easing@0.2.0:
resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==}
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
......@@ -3478,17 +3483,17 @@ snapshots:
'@exodus/bytes@1.15.1': {}
'@ezijing/ai-core@1.0.44':
'@ezijing/ai-core@1.0.44(debug@4.4.3)':
dependencies:
axios: 1.18.1
axios: 1.18.1(debug@4.4.3)
eventsource-parser: 3.1.0
transitivePeerDependencies:
- debug
- supports-color
'@ezijing/ai-react@1.0.44(react@18.3.1)':
'@ezijing/ai-react@1.0.44(debug@4.4.3)(react@18.3.1)':
dependencies:
'@ezijing/ai-core': 1.0.44
'@ezijing/ai-core': 1.0.44(debug@4.4.3)
react: 18.3.1
transitivePeerDependencies:
- debug
......@@ -3496,8 +3501,29 @@ snapshots:
'@fortaine/fetch-event-source@3.0.6': {}
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.31
optional: true
'@jridgewell/resolve-uri@3.1.2':
optional: true
'@jridgewell/source-map@0.3.11':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
optional: true
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
optional: true
'@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
dependencies:
'@emnapi/core': 1.11.1
......@@ -4267,10 +4293,10 @@ snapshots:
'@uppy/utils': 4.1.3
nanoid: 3.3.16
'@vitejs/plugin-react@6.0.3(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7))':
'@vitejs/plugin-react@6.0.3(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0))':
dependencies:
'@rolldown/pluginutils': 1.0.1
vite: 8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)
vite: 8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0)
'@vitest/expect@4.1.10':
dependencies:
......@@ -4281,13 +4307,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.0
'@vitest/mocker@4.1.10(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7))':
'@vitest/mocker@4.1.10(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0))':
dependencies:
'@vitest/spy': 4.1.10
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)
vite: 8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0)
'@vitest/pretty-format@4.1.10':
dependencies:
......@@ -4427,6 +4453,9 @@ snapshots:
'@xobotyi/scrollbar-width@1.9.5': {}
acorn@8.17.0:
optional: true
address@1.2.2: {}
agent-base@6.0.2:
......@@ -4617,9 +4646,9 @@ snapshots:
asynckit@0.4.0: {}
axios@1.18.1:
axios@1.18.1(debug@4.4.3):
dependencies:
follow-redirects: 1.16.0
follow-redirects: 1.16.0(debug@4.4.3)
form-data: 4.0.6
https-proxy-agent: 5.0.1
proxy-from-env: 2.1.0
......@@ -4633,7 +4662,8 @@ snapshots:
bowser@1.9.4: {}
browser-or-node@1.3.0: {}
buffer-from@1.1.2:
optional: true
builtin-status-codes@3.0.0: {}
......@@ -4657,6 +4687,9 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
commander@2.20.3:
optional: true
compute-scroll-into-view@1.0.20: {}
compute-scroll-into-view@3.1.1: {}
......@@ -4882,7 +4915,9 @@ snapshots:
file-saver@2.0.5: {}
follow-redirects@1.16.0: {}
follow-redirects@1.16.0(debug@4.4.3):
optionalDependencies:
debug: 4.4.3
form-data@4.0.6:
dependencies:
......@@ -4941,12 +4976,6 @@ snapshots:
highlight.js@11.11.1: {}
html-docx-js-typescript@0.1.5:
dependencies:
browser-or-node: 1.3.0
jszip: 3.10.1
tslib: 1.14.1
html-encoding-sniffer@6.0.0:
dependencies:
'@exodus/bytes': 1.15.1
......@@ -4979,8 +5008,6 @@ snapshots:
image-size@0.5.5:
optional: true
immediate@3.0.6: {}
immer@9.0.21: {}
indent-string@4.0.0: {}
......@@ -5061,13 +5088,6 @@ snapshots:
jstoxml@2.2.9: {}
jszip@3.10.1:
dependencies:
lie: 3.3.0
pako: 1.0.11
readable-stream: 2.3.8
setimmediate: 1.0.5
less@4.6.7:
dependencies:
copy-anything: 3.0.5
......@@ -5081,10 +5101,6 @@ snapshots:
needle: 3.5.0
source-map: 0.6.1
lie@3.3.0:
dependencies:
immediate: 3.0.6
lightningcss-android-arm64@1.32.0:
optional: true
......@@ -5323,8 +5339,6 @@ snapshots:
'@oxlint/binding-win32-ia32-msvc': 1.74.0
'@oxlint/binding-win32-x64-msvc': 1.74.0
pako@1.0.11: {}
parse-node-version@1.0.1: {}
parse5@8.0.1:
......@@ -5856,8 +5870,6 @@ snapshots:
set-harmonic-interval@1.0.1: {}
setimmediate@1.0.5: {}
side-channel-list@1.0.1:
dependencies:
es-errors: 1.3.0
......@@ -5903,6 +5915,12 @@ snapshots:
source-map-js@1.2.1: {}
source-map-support@0.5.21:
dependencies:
buffer-from: 1.1.2
source-map: 0.6.1
optional: true
source-map@0.5.6: {}
source-map@0.6.1: {}
......@@ -5956,6 +5974,14 @@ snapshots:
symbol-tree@3.2.4: {}
terser@5.49.0:
dependencies:
'@jridgewell/source-map': 0.3.11
acorn: 8.17.0
commander: 2.20.3
source-map-support: 0.5.21
optional: true
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
......@@ -6005,8 +6031,6 @@ snapshots:
ts-easing@0.2.0: {}
tslib@1.14.1: {}
tslib@2.3.0: {}
tslib@2.8.1: {}
......@@ -6080,15 +6104,15 @@ snapshots:
mz: 2.7.0
unescape: 1.0.1
vite-plugin-mkcert@1.17.12(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)):
vite-plugin-mkcert@1.17.12(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0)):
dependencies:
debug: 4.4.3
picocolors: 1.1.1
vite: 8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)
vite: 8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0)
transitivePeerDependencies:
- supports-color
vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7):
vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.5
......@@ -6100,11 +6124,12 @@ snapshots:
esbuild: 0.28.1
fsevents: 2.3.3
less: 4.6.7
terser: 5.49.0
vitest@4.1.10(@types/node@22.20.1)(jsdom@29.1.1)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)):
vitest@4.1.10(@types/node@22.20.1)(jsdom@29.1.1)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0)):
dependencies:
'@vitest/expect': 4.1.10
'@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7))
'@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0))
'@vitest/pretty-format': 4.1.10
'@vitest/runner': 4.1.10
'@vitest/snapshot': 4.1.10
......@@ -6121,7 +6146,7 @@ snapshots:
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
vite: 8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)
vite: 8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(less@4.6.7)(terser@5.49.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.20.1
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论