Compare commits
No commits in common. "f1ba8949f67dd00e8c66e212ce7c0f6e2780aeba" and "6831bd48315af96240c5c23a9208f17202ba90d3" have entirely different histories.
f1ba8949f6
...
6831bd4831
|
|
@ -270,14 +270,17 @@
|
|||
background-color: var(--item-hover-bg);
|
||||
}
|
||||
|
||||
/* ByteMD 内置的右侧按钮:1=目录 2=帮助 3=仅编辑 4=仅预览 5=全屏 6=源代码。
|
||||
1/2 依赖预览区渲染,3/4 与页面自身的「编辑 / 分栏 / 预览」分段控件重复,统一隐藏;
|
||||
5 由页面自绘、并入同一分段控件的「全屏」取代(DOM 仍保留,靠它驱动 ByteMD 的全屏状态);
|
||||
6「源代码」是 ByteMD 指向项目主页的外链,不属于文档编辑操作,编辑模式下统一去掉。
|
||||
隐藏后右侧只剩页面 Portal 注入的统一视图控件。
|
||||
/* ByteMD 内置的右侧开关:1=文档索引 2=帮助 3=仅编辑 4=仅预览 5=全屏 6=同步滚动。
|
||||
1/2 依赖预览区渲染(纯编辑区下是空面板),3/4 与页面自身的「编辑/分栏/预览」分段控件重复,
|
||||
统一隐藏,只保留全屏与同步滚动。
|
||||
注意:页面用 Portal 把分段控件插到 .bytemd-toolbar-right 的最后一个子节点,
|
||||
上面的序号才不会被错位,改动插入位置时记得同步这里的 nth-child。 */
|
||||
.bytemd-toolbar-right .bytemd-toolbar-icon:nth-child(-n+6) {
|
||||
.bytemd-toolbar-right .bytemd-toolbar-icon:nth-child(-n+4) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 非分栏模式下没有左右对照,同步滚动开关同样隐藏 */
|
||||
.bytemd-wrapper:not([data-view-mode="split"]) .bytemd-toolbar-right .bytemd-toolbar-icon:nth-child(6) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
@ -287,10 +290,10 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
/* 视图模式切换(Portal 注入):编辑 / 分栏 / 预览 / 全屏 合成同一分段控件,
|
||||
ByteMD 自带图标已全部隐藏,它成为工具栏右侧唯一的控件 */
|
||||
/* 视图模式切换(Portal 注入):排在同步滚动 / 全屏图标左侧,与它们同宽同高 */
|
||||
.bytemd-toolbar-right .view-mode-switch {
|
||||
order: -1;
|
||||
margin-right: 6px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
|
@ -309,11 +312,6 @@
|
|||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 全屏是开关动作而非视图选项:用主色区分「已进入全屏」,图标同时切换为退出样式 */
|
||||
.bytemd-toolbar-right .view-mode-switch .view-mode-option.is-active {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* 预览模式是只读视图:隐藏会改动源码的 Markdown 工具栏,以及失效的同步滚动开关 */
|
||||
.bytemd-wrapper[data-view-mode="preview"] .bytemd-toolbar-left {
|
||||
display: none;
|
||||
|
|
@ -342,102 +340,6 @@
|
|||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 保存按钮:自动保存开关融合在按钮内部,整体呈现为一个主色按钮
|
||||
左半边「保存」执行保存,右半边「自动保存」开关切换自动保存 */
|
||||
.save-control {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-primary);
|
||||
color: var(--text-color-inverse);
|
||||
overflow: hidden;
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.save-control:hover {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.save-control-main {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 12px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.save-control-main:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
|
||||
.save-control-main:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.save-control-sep {
|
||||
align-self: center;
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.save-control-auto {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.save-control-auto:hover {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
|
||||
/* 开关本身不单独响应点击,由外层整块统一处理,避免重复触发 */
|
||||
.save-control-auto .ant-switch {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 主色按钮上的开关重新配色:开关内直接显示「自动 / 手动」,开 / 关都清晰可辨 */
|
||||
.save-control-auto .ant-switch {
|
||||
background: rgba(0, 0, 0, 0.28);
|
||||
/* antd 默认宽度放不下两个汉字,会折行/裁切,这里给足文本空间 */
|
||||
min-width: 58px;
|
||||
}
|
||||
|
||||
.save-control-auto .ant-switch .ant-switch-handle::before {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.save-control-auto .ant-switch.ant-switch-checked {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* antd 把颜色写在子节点上,必须直接覆盖 checked / unchecked 文本,否则白字压白底看不见 */
|
||||
.save-control-auto .ant-switch .ant-switch-inner-checked,
|
||||
.save-control-auto .ant-switch .ant-switch-inner-unchecked {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.save-control-auto .ant-switch.ant-switch-checked .ant-switch-inner-checked {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.save-control-auto .ant-switch.ant-switch-checked .ant-switch-handle::before {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
/* 编辑器工具栏里的视图模式图标(文案放在 Tooltip 中) */
|
||||
.view-mode-option {
|
||||
display: inline-flex;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { Layout, Button, Modal, Input, Space, Tooltip, Upload, Select, Progress, TreeSelect, Empty, Tabs, Segmented, Switch } from 'antd'
|
||||
import { Layout, Button, Modal, Input, Space, Tooltip, Upload, Select, Progress, TreeSelect, Empty, Tabs, Segmented } from 'antd'
|
||||
import {
|
||||
FolderOutlined,
|
||||
FolderOpenOutlined,
|
||||
|
|
@ -19,9 +19,6 @@ import {
|
|||
ShareAltOutlined,
|
||||
ColumnWidthOutlined,
|
||||
EyeOutlined,
|
||||
FullscreenOutlined,
|
||||
FullscreenExitOutlined,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Editor } from '@bytemd/react'
|
||||
|
|
@ -63,8 +60,6 @@ import './DocumentEditor.css'
|
|||
|
||||
const { Sider, Content } = Layout
|
||||
const ROOT_FOLDER_KEY = '__root__'
|
||||
// 自动保存间隔:每 30 秒检查一次,仅在存在未保存改动时静默保存
|
||||
const AUTO_SAVE_INTERVAL_MS = 30 * 1000
|
||||
|
||||
function DocumentEditor() {
|
||||
const { projectId } = useParams()
|
||||
|
|
@ -110,8 +105,6 @@ function DocumentEditor() {
|
|||
const isLargeMarkdown = isLargeMarkdownContent(fileContent)
|
||||
// 编辑区视图模式:默认只打开编辑区(不打开预览区),用户切换后记住偏好
|
||||
const [editorViewMode, setEditorViewMode] = useState(readEditorViewMode)
|
||||
// 自动保存:默认开启,每 30 秒静默保存一次;关闭后回到手动保存
|
||||
const [autoSave, setAutoSave] = useState(true)
|
||||
const editorPaneRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -213,14 +206,6 @@ function DocumentEditor() {
|
|||
return () => window.removeEventListener('keydown', onKeyDown, true)
|
||||
}, [isEditorFullscreen])
|
||||
|
||||
// 全屏状态由 ByteMD 内部维护,页面自绘的全屏按钮通过点击它隐藏的原生图标来切换,
|
||||
// 保证与自带行为的退出方式(Esc)完全一致,不额外维护一份全屏状态。
|
||||
const toggleEditorFullscreen = () => {
|
||||
const pane = editorPaneRef.current
|
||||
const icon = pane?.querySelector('.bytemd-toolbar-right .bytemd-toolbar-icon:nth-child(5)')
|
||||
icon?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
}
|
||||
|
||||
const linkTreeData = useMemo(() => {
|
||||
const markSelectable = (nodes) => nodes.map((node) => {
|
||||
if (node.children?.length) {
|
||||
|
|
@ -753,10 +738,9 @@ function DocumentEditor() {
|
|||
})
|
||||
}
|
||||
|
||||
// silent:自动保存时只更新基线,不弹成功提示(保存中的动画由按钮的 loading 状态承载)
|
||||
const handleSaveFile = async ({ silent = false } = {}) => {
|
||||
const handleSaveFile = async () => {
|
||||
if (!selectedFile) {
|
||||
if (!silent) Toast.warning('提示', '请先选择文件')
|
||||
Toast.warning('提示', '请先选择文件')
|
||||
return
|
||||
}
|
||||
if (saving) return
|
||||
|
|
@ -772,9 +756,7 @@ function DocumentEditor() {
|
|||
if (snapshot === fileContent) {
|
||||
setContentBaseline(snapshot)
|
||||
}
|
||||
if (!silent) {
|
||||
Toast.success('成功', '保存成功')
|
||||
}
|
||||
Toast.success('成功', '保存成功')
|
||||
} catch (error) {
|
||||
// 错误已通过request interceptor处理
|
||||
} finally {
|
||||
|
|
@ -788,23 +770,6 @@ function DocumentEditor() {
|
|||
saveFileRef.current = handleSaveFile
|
||||
})
|
||||
|
||||
// 自动保存:开启时每 30 秒检查一次,仅在存在未保存改动且不处于加载/保存中时静默保存
|
||||
const autoSaveStatusRef = useRef({})
|
||||
useEffect(() => {
|
||||
autoSaveStatusRef.current = { selectedFile, isPdfSelected, isDirty, saving, loading }
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoSave) return undefined
|
||||
const timer = window.setInterval(() => {
|
||||
const status = autoSaveStatusRef.current
|
||||
if (!status.selectedFile || status.isPdfSelected || status.saving || status.loading) return
|
||||
if (!status.isDirty) return
|
||||
saveFileRef.current?.({ silent: true })
|
||||
}, AUTO_SAVE_INTERVAL_MS)
|
||||
return () => window.clearInterval(timer)
|
||||
}, [autoSave])
|
||||
|
||||
// 存在未保存改动时,关闭标签页/刷新前给出浏览器级提示
|
||||
useEffect(() => {
|
||||
if (!isDirty) return undefined
|
||||
|
|
@ -1479,45 +1444,17 @@ function DocumentEditor() {
|
|||
未保存
|
||||
</span>
|
||||
)}
|
||||
{/* 保存按钮:自动保存开关融合在按钮内部,左半边点击保存,右半边开关自动保存 */}
|
||||
<div className="save-control">
|
||||
<Tooltip title={autoSave ? '立即保存(Ctrl/Cmd + S)' : '保存(Ctrl/Cmd + S)'}>
|
||||
<button
|
||||
type="button"
|
||||
className="save-control-main"
|
||||
onClick={handleSaveFile}
|
||||
disabled={!selectedFile || saving}
|
||||
>
|
||||
{saving ? <LoadingOutlined spin /> : <SaveOutlined />}
|
||||
<span>{saving ? '保存中' : '保存'}</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<span className="save-control-sep" aria-hidden="true" />
|
||||
<Tooltip title={autoSave ? '关闭自动保存,改为手动保存' : '开启自动保存(每 30 秒自动保存)'}>
|
||||
<span
|
||||
className="save-control-auto"
|
||||
role="switch"
|
||||
aria-checked={autoSave}
|
||||
aria-label="自动保存"
|
||||
tabIndex={0}
|
||||
onClick={() => setAutoSave((value) => !value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
setAutoSave((value) => !value)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
checked={autoSave}
|
||||
checkedChildren="自动"
|
||||
unCheckedChildren="手动"
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip title="保存(Ctrl/Cmd + S)">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSaveFile}
|
||||
loading={saving}
|
||||
disabled={!selectedFile}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
icon={<UndoOutlined />}
|
||||
onClick={handleReset}
|
||||
|
|
@ -1630,43 +1567,24 @@ function DocumentEditor() {
|
|||
locale={bytemdZhHans}
|
||||
/>
|
||||
)}
|
||||
{/* 视图模式切换:编辑 / 分栏 / 预览 + 全屏,作为一个统一控件挂到 ByteMD 工具栏右侧。
|
||||
全屏不是视图状态而是开关动作,点击后只切换 ByteMD 的全屏,不改变当前视图模式。 */}
|
||||
{/* 视图模式切换:挂到 ByteMD 工具栏右侧,与内置「全屏」按钮同排 */}
|
||||
{toolbarRightSlot &&
|
||||
createPortal(
|
||||
<Segmented
|
||||
className="view-mode-switch"
|
||||
size="small"
|
||||
value={editorViewMode}
|
||||
onChange={(value) => {
|
||||
if (value === 'fullscreen') {
|
||||
toggleEditorFullscreen()
|
||||
return
|
||||
}
|
||||
setEditorViewMode(value)
|
||||
}}
|
||||
options={[
|
||||
...EDITOR_VIEW_MODES.map((mode) => ({
|
||||
value: mode,
|
||||
label: (
|
||||
<Tooltip title={`${EDITOR_VIEW_MODE_LABELS[mode]} · ${EDITOR_VIEW_MODE_HINTS[mode]}`}>
|
||||
<span className="view-mode-option">
|
||||
{mode === 'edit' ? <EditOutlined /> : mode === 'split' ? <ColumnWidthOutlined /> : <EyeOutlined />}
|
||||
</span>
|
||||
</Tooltip>
|
||||
),
|
||||
})),
|
||||
{
|
||||
value: 'fullscreen',
|
||||
label: (
|
||||
<Tooltip title={isEditorFullscreen ? '退出全屏(Esc)' : '全屏'}>
|
||||
<span className={`view-mode-option${isEditorFullscreen ? ' is-active' : ''}`}>
|
||||
{isEditorFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||
</span>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
]}
|
||||
onChange={setEditorViewMode}
|
||||
options={EDITOR_VIEW_MODES.map((mode) => ({
|
||||
value: mode,
|
||||
label: (
|
||||
<Tooltip title={`${EDITOR_VIEW_MODE_LABELS[mode]} · ${EDITOR_VIEW_MODE_HINTS[mode]}`}>
|
||||
<span className="view-mode-option">
|
||||
{mode === 'edit' ? <EditOutlined /> : mode === 'split' ? <ColumnWidthOutlined /> : <EyeOutlined />}
|
||||
</span>
|
||||
</Tooltip>
|
||||
),
|
||||
}))}
|
||||
/>,
|
||||
toolbarRightSlot,
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue