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

feat: 添加编辑器实例销毁逻辑,优化图片加载处理

上级 fe79c9e1
...@@ -2,11 +2,32 @@ import { render } from '@testing-library/react' ...@@ -2,11 +2,32 @@ import { render } from '@testing-library/react'
import { createElement } from 'react' import { createElement } from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest' import { afterEach, describe, expect, it, vi } from 'vitest'
import { useEditorLifecycle } from '@/components/editor/wangeditor-customer/shell/useEditorLifecycle' import {
destroyEditorInstance,
useEditorLifecycle,
} from '@/components/editor/wangeditor-customer/shell/useEditorLifecycle'
describe('useEditorLifecycle', () => { describe('useEditorLifecycle', () => {
afterEach(() => vi.unstubAllGlobals()) afterEach(() => vi.unstubAllGlobals())
it('releases Slate operations and history after destroying an editor', () => {
const editor = {
isDestroyed: false,
operations: [{ type: 'remove_node' }],
history: {
undos: [[{ type: 'remove_node' }]],
redos: [[{ type: 'insert_node' }]],
},
destroy: vi.fn(),
}
destroyEditorInstance(editor)
expect(editor.destroy).toHaveBeenCalledOnce()
expect(editor.operations).toEqual([])
expect(editor.history).toEqual({ undos: [], redos: [] })
})
it('does not crash while the textarea is still registering', () => { it('does not crash while the textarea is still registering', () => {
const frames = [] const frames = []
vi.stubGlobal('requestAnimationFrame', (callback) => { vi.stubGlobal('requestAnimationFrame', (callback) => {
......
...@@ -344,7 +344,7 @@ const CustomerTopic = (props, ref) => { ...@@ -344,7 +344,7 @@ const CustomerTopic = (props, ref) => {
// other config options // other config options
}} }}
mode='default' mode='default'
style={{ height: '200px', overflowY: 'hidden' }} style={{ height: '300px', overflowY: 'hidden' }}
/> />
</div> </div>
</Spin> </Spin>
......
...@@ -35,33 +35,11 @@ ...@@ -35,33 +35,11 @@
.w-e-text-container [data-slate-editor] h2.chapter-item-section { .w-e-text-container [data-slate-editor] h2.chapter-item-section {
margin: 0 !important; margin: 0 !important;
} }
.ant-spin,
.ant-spin-container {
height: 100%;
min-height: 0;
}
.w-e-text-container {
flex: 1;
min-height: 0;
height: auto;
[data-w-e-textarea='true'] {
box-sizing: border-box;
min-height: 100%;
}
}
.w-e-text-container [data-slate-editor] p:not(:empty) {
margin: 15px 0;
span[data-slate-node='text'] {
padding-top: 0px;
}
}
.w-e-text-container [data-slate-editor] p { .w-e-text-container [data-slate-editor] p {
line-height: 1.5; line-height: 1.5;
font-size: 18px; font-size: 18px;
font-family: '黑体'; font-family: '黑体';
margin-block: 0; margin: 15px 0;
span[data-slate-node='text'], span[data-slate-node='text'],
span[data-slate-node='element'] { span[data-slate-node='element'] {
...@@ -69,9 +47,19 @@ ...@@ -69,9 +47,19 @@
height: 100%; height: 100%;
line-height: 100%; line-height: 100%;
} }
span[data-slate-node='text'] { }
padding-top: 3px;
} // 块级 void 节点前的 Slate 光标占位段落不应产生一整行空白
.w-e-text-container
[data-slate-editor]
> p:has(> span:only-child > span:only-child > span[data-slate-zero-width]):has(
+ [data-slate-void='true']
) {
height: 0;
margin: 0;
padding: 0;
line-height: 0;
overflow: hidden;
} }
.w-e-text-container [data-slate-editor] .w-e-image-container { .w-e-text-container [data-slate-editor] .w-e-image-container {
...@@ -84,7 +72,6 @@ ...@@ -84,7 +72,6 @@
.w-e-text-container [data-slate-editor] img[alt='icon-inline'] { .w-e-text-container [data-slate-editor] img[alt='icon-inline'] {
vertical-align: middle; vertical-align: middle;
display: inline-block;
} }
// 插入图标后(图片节点 + 文本节点)强制整行居中 // 插入图标后(图片节点 + 文本节点)强制整行居中
...@@ -114,15 +101,6 @@ ...@@ -114,15 +101,6 @@
display: inline-block; display: inline-block;
width: 2px; width: 2px;
} }
> span:last-child {
span[data-slate-zero-width] {
display: inline-block;
width: 2px;
// &:not(:empty) {
// display: inline;
// }
}
}
> span:first-child { > span:first-child {
span[data-slate-zero-width] { span[data-slate-zero-width] {
display: inline; display: inline;
...@@ -216,9 +194,6 @@ ...@@ -216,9 +194,6 @@
box-sizing: border-box; box-sizing: border-box;
padding-bottom: 10px; padding-bottom: 10px;
align-items: center; align-items: center;
.ant-divider {
margin-top: 0;
}
.left { .left {
flex: 1; flex: 1;
width: calc(100% - 480px); width: calc(100% - 480px);
...@@ -235,8 +210,6 @@ ...@@ -235,8 +210,6 @@
} }
.right { .right {
display: flex; display: flex;
right: 0;
top: 0;
align-items: center; align-items: center;
flex: 0 0 480px; flex: 0 0 480px;
justify-content: flex-end; justify-content: flex-end;
...@@ -247,12 +220,9 @@ ...@@ -247,12 +220,9 @@
.img { .img {
color: #ab1941; color: #ab1941;
font-size: 12px; font-size: 12px;
display: flex;
align-items: center;
height: 22px; height: 22px;
line-height: 22px; line-height: 22px;
text-align: right; text-align: right;
display: block;
img { img {
width: 20px; width: 20px;
margin-right: 8px; margin-right: 8px;
...@@ -310,10 +280,6 @@ ...@@ -310,10 +280,6 @@
.ant-spin-container { .ant-spin-container {
width: 100%; width: 100%;
} }
.toolbar-customer {
flex: 1;
}
.menu-tabs-key { .menu-tabs-key {
flex: 0 0 306px; flex: 0 0 306px;
background: #fafafa; background: #fafafa;
...@@ -390,18 +356,6 @@ ...@@ -390,18 +356,6 @@
width: 22px; width: 22px;
height: 22px; height: 22px;
border-radius: 3px; border-radius: 3px;
&.color1 {
background-color: #ab1941;
}
&.color2 {
background-color: #2970f6;
}
&.color3 {
background-color: #2ad882;
}
&.color4 {
background-color: #eb3351;
}
} }
.type { .type {
font-size: 14px; font-size: 14px;
...@@ -421,62 +375,6 @@ ...@@ -421,62 +375,6 @@
} }
} }
} }
.toolbox-parent {
position: relative;
.custom-bar-box-input {
position: absolute;
z-index: 101;
height: 48px;
padding: 10px 15px;
width: 298px;
top: 165px;
box-sizing: border-box;
.box {
justify-content: space-around;
display: flex;
width: 268px;
}
.customer-box-input-item {
box-sizing: border-box;
text-align: center;
width: 60px;
p {
width: 60px;
text-align: center;
}
.ant-input-number-outlined {
border: none;
outline: none;
width: 60px;
&:hover,
&:focus {
border: none;
outline: none;
}
}
input {
width: 60px;
color: #333;
text-align: center;
border: 1px solid #d9d9d9;
border-radius: 5px;
height: 30px;
line-height: 30px;
background-color: #fff;
&:hover,
&:focus {
border-color: #b83956;
}
}
.text {
color: #999;
font-size: 12px;
margin-top: 4px;
}
}
}
}
} }
} }
} }
...@@ -539,10 +437,3 @@ ...@@ -539,10 +437,3 @@
pointer-events: none; pointer-events: none;
} }
} }
/*
['bold', 'underline', 'italic', 'through', 'code', 'sub', 'sup', 'clearStyle', 'color', 'bgColor', 'fontSize', 'fontFamily', 'indent', 'delIndent', 'justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify', 'lineHeight', 'insertImage', 'deleteImage', 'editImage', 'viewImageLink', 'imageWidth30', 'imageWidth50', 'imageWidth100', 'divider', 'emotion', 'insertLink', 'editLink', 'unLink', 'viewLink', 'codeBlock', 'blockquote', 'headerSelect', 'header1', 'header2', 'header3', 'header4', 'header5', 'todo', 'redo', 'undo', 'fullScreen', 'enter', 'bulletedList', 'numberedList', 'insertTable', 'deleteTable', 'insertTableRow', 'deleteTableRow', 'insertTableCol', 'deleteTableCol', 'tableHeader', 'tableFullWidth', 'insertVideo', 'uploadVideo', 'editVideoSize', 'uploadImage', 'codeSelectLang']
['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'deleteImage', 'editImage', 'viewImageLink', 'imageWidth30', 'imageWidth50', 'imageWidth100', '', '', '', 'editLink', 'unLink', 'viewLink', '', '', 'headerSelect', 'header1', 'header2', 'header3', 'header4', 'header5', '', '', '', '', '', '', '', '', 'deleteTable', 'insertTableRow', 'deleteTableRow', 'insertTableCol', 'deleteTableCol', 'tableHeader', 'tableFullWidth', '', 'codeSelectLang', 'editVideoSize', '', '']
*/
...@@ -16,7 +16,7 @@ import '@wangeditor/editor/dist/css/style.css' // 引入 css ...@@ -16,7 +16,7 @@ import '@wangeditor/editor/dist/css/style.css' // 引入 css
import { EditorHeader } from './EditorHeader' import { EditorHeader } from './EditorHeader'
import { EditorSidebar } from './EditorSidebar' import { EditorSidebar } from './EditorSidebar'
import { useToolbarPresentation } from './useToolbarPresentation' import { useToolbarPresentation } from './useToolbarPresentation'
import { useEditorLifecycle } from './useEditorLifecycle' import { destroyEditorInstance, useEditorLifecycle } from './useEditorLifecycle'
import { createEditorConfig, defaultEditorContent, toolbarConfig } from './editor-config' import { createEditorConfig, defaultEditorContent, toolbarConfig } from './editor-config'
import { createAutosaveScheduler } from './autosave' import { createAutosaveScheduler } from './autosave'
import { getEmptyParagraphPath, normalizeEmptyParagraph } from './paragraph-behavior' import { getEmptyParagraphPath, normalizeEmptyParagraph } from './paragraph-behavior'
...@@ -154,7 +154,7 @@ const WangEditorCustomer = (props, ref) => { ...@@ -154,7 +154,7 @@ const WangEditorCustomer = (props, ref) => {
editor.addMark('fontFamily', '黑体') editor.addMark('fontFamily', '黑体')
editor.addMark('lineHeight', 1.5) editor.addMark('lineHeight', 1.5)
return () => { return () => {
editor.destroy() destroyEditorInstance(editor)
setEditor(null) setEditor(null)
} }
}, [editor]) }, [editor])
......
import { useEffect } from 'react' import { useEffect } from 'react'
export const destroyEditorInstance = (editor) => {
if (!editor) return
if (!editor.isDestroyed) editor.destroy()
if (Array.isArray(editor.operations)) editor.operations.length = 0
if (Array.isArray(editor.history?.undos)) editor.history.undos.length = 0
if (Array.isArray(editor.history?.redos)) editor.history.redos.length = 0
}
export const useEditorLifecycle = ({ editor, gData, disabled }) => { export const useEditorLifecycle = ({ editor, gData, disabled }) => {
useEffect(() => { useEffect(() => {
if (!editor) return if (!editor) return
......
...@@ -83,7 +83,13 @@ export default function ManagementListView() { ...@@ -83,7 +83,13 @@ export default function ManagementListView() {
align: 'center', align: 'center',
dataIndex: 'img', dataIndex: 'img',
render: (_, record) => ( render: (_, record) => (
<Image alt='书籍图片' width={80} height={80} src={record.img} fallback={IMAGE_FALLBACK} /> <Image
alt='书籍图片'
width={80}
height={80}
src={record.img || IMAGE_FALLBACK}
fallback={IMAGE_FALLBACK}
/>
), ),
}, },
{ title: '作者', key: 'authors', align: 'center', dataIndex: 'authors' }, { title: '作者', key: 'authors', align: 'center', dataIndex: 'authors' },
......
...@@ -9,7 +9,7 @@ const clients = new Map() ...@@ -9,7 +9,7 @@ const clients = new Map()
async function getOSSConfig() { async function getOSSConfig() {
const { data } = await getSTSToken() const { data } = await getSTSToken()
return { return {
...data, endpoint: data.endpoint,
accessKeyId: data.AccessKeyId, accessKeyId: data.AccessKeyId,
accessKeySecret: data.AccessKeySecret, accessKeySecret: data.AccessKeySecret,
stsToken: data.SecurityToken, stsToken: data.SecurityToken,
...@@ -24,7 +24,10 @@ export async function getOSSClient(bucket = 'zxts-book-file') { ...@@ -24,7 +24,10 @@ export async function getOSSClient(bucket = 'zxts-book-file') {
bucket, bucket,
...config, ...config,
timeout: 180000, timeout: 180000,
refreshSTSToken: () => getOSSConfig(), refreshSTSToken: async () => {
const { accessKeyId, accessKeySecret, stsToken } = await getOSSConfig()
return { accessKeyId, accessKeySecret, stsToken }
},
refreshSTSTokenInterval: 14 * 60 * 1000, refreshSTSTokenInterval: 14 * 60 * 1000,
}) })
clients.set(bucket, client) clients.set(bucket, client)
...@@ -35,17 +38,10 @@ function getExtension(file) { ...@@ -35,17 +38,10 @@ function getExtension(file) {
const name = typeof file.name === 'string' ? file.name : '' const name = typeof file.name === 'string' ? file.name : ''
const fromName = name.includes('.') ? `.${name.split('.').pop().toLowerCase()}` : '' const fromName = name.includes('.') ? `.${name.split('.').pop().toLowerCase()}` : ''
if (fromName) return fromName if (fromName) return fromName
return ( return { 'image/jpeg': '.jpg', 'image/png': '.png', 'video/mp4': '.mp4' }[(file.type || '').toLowerCase()] || '.bin'
{ 'image/jpeg': '.jpg', 'image/png': '.png', 'video/mp4': '.mp4' }[
(file.type || '').toLowerCase()
] || '.bin'
)
} }
function buildObjectName( function buildObjectName(file, { pathPrefix = dayjs().format('YYYY/MM/DD'), filenamePrefix = 'file' } = {}) {
file,
{ pathPrefix = dayjs().format('YYYY/MM/DD'), filenamePrefix = 'file' } = {},
) {
const filename = `${filenamePrefix}-${Date.now()}-${Math.random().toString(36).slice(2)}${getExtension(file)}` const filename = `${filenamePrefix}-${Date.now()}-${Math.random().toString(36).slice(2)}${getExtension(file)}`
return pathPrefix ? `${pathPrefix.replace(/\/$/, '')}/${filename}` : filename return pathPrefix ? `${pathPrefix.replace(/\/$/, '')}/${filename}` : filename
} }
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论