You've already forked SmartisanNote.Remake
优化 编辑器工具栏位置调整;
优化 日期、字数信息布局调整;
This commit is contained in:
@@ -12,6 +12,10 @@
|
||||
--primary-dark: #4a3224; /* Darker shade of primary */
|
||||
--primary-light: #f5f0e6; /* Light background tone */
|
||||
|
||||
/* Editor typography - Consistent font size and line height */
|
||||
--editor-font-size: 19px; /* Base font size for editor */
|
||||
--editor-line-height: 1.5; /* Line height for editor */
|
||||
|
||||
/* Background colors - Warm paper-like tones */
|
||||
--background: #fbf7ed; /* Main app background - warm off-white */
|
||||
--background-secondary: #f7f2e9; /* Slightly darker background */
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="editor-container">
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<button v-for="tool in tools" :key="tool.name" :class="{ active: tool.active }" @click="tool.action" class="toolbar-btn">
|
||||
<div class="toolbar" :class="{ visible: isToolbarVisible }" @mousedown.prevent @focusin="keepToolbarVisible" @focusout="handleToolbarFocusOut">
|
||||
<button v-for="tool in tools" :key="tool.name" :class="{ active: tool.active }" @click.stop="handleToolClick(tool.action, $event)" @mousedown.prevent @focusout.prevent class="toolbar-btn">
|
||||
<img :src="tool.icon" :alt="tool.name" class="toolbar-icon" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 编辑区域 -->
|
||||
<div ref="editorRef" contenteditable="true" class="editor-content" @input="handleInput" @keydown="handleKeydown" @click="updateToolbarState" @keyup="updateToolbarState"></div>
|
||||
<div ref="editorRef" contenteditable="true" class="editor-content" @input="handleInput" @keydown="handleKeydown" @click="updateToolbarState" @keyup="updateToolbarState" @focus="showToolbar" @blur="hideToolbar"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -26,6 +26,7 @@ const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const editorRef = ref(null)
|
||||
const content = ref(props.modelValue || '')
|
||||
const isToolbarVisible = ref(false)
|
||||
|
||||
// 工具配置
|
||||
const tools = ref([
|
||||
@@ -44,7 +45,7 @@ const tools = ref([
|
||||
{
|
||||
name: 'todo',
|
||||
icon: '/assets/icons/drawable-xxhdpi/rtf_gtasks_normal.9.png',
|
||||
action: () => insertTodoList(),
|
||||
action: () => formatText('insertTodoList'),
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
@@ -109,12 +110,41 @@ const isInListOrQuote = () => {
|
||||
|
||||
// 格式化文本
|
||||
const formatText = (command, value = null) => {
|
||||
// 检查是否已经应用了相同的格式
|
||||
if (command === 'bold' && isAlreadyInFormat('bold')) return
|
||||
if (command === 'justifyCenter' && isAlreadyInFormat('center')) return
|
||||
if (command === 'formatBlock' && value === 'h2' && isAlreadyInFormat('header')) return
|
||||
if (command === 'formatBlock' && value === 'blockquote' && isAlreadyInFormat('quote')) return
|
||||
if (command === 'insertUnorderedList' && isAlreadyInFormat('list')) return
|
||||
// 检查是否已经应用了相同的格式,如果已应用则取消格式
|
||||
if (command === 'bold' && isAlreadyInFormat('bold')) {
|
||||
document.execCommand('bold', false, null)
|
||||
updateToolbarState()
|
||||
handleInput()
|
||||
return
|
||||
}
|
||||
|
||||
if (command === 'justifyCenter' && isAlreadyInFormat('center')) {
|
||||
document.execCommand('justifyLeft', false, null)
|
||||
updateToolbarState()
|
||||
handleInput()
|
||||
return
|
||||
}
|
||||
|
||||
if (command === 'formatBlock' && value === 'h2' && isAlreadyInFormat('header')) {
|
||||
document.execCommand('formatBlock', false, '<p>')
|
||||
updateToolbarState()
|
||||
handleInput()
|
||||
return
|
||||
}
|
||||
|
||||
if (command === 'insertUnorderedList' && isAlreadyInFormat('list')) {
|
||||
document.execCommand('insertUnorderedList', false, null)
|
||||
updateToolbarState()
|
||||
handleInput()
|
||||
return
|
||||
}
|
||||
|
||||
// 处理自定义待办事项
|
||||
if (command === 'insertTodoList') {
|
||||
insertTodoList()
|
||||
updateToolbarState()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查嵌套限制
|
||||
if ((command === 'insertUnorderedList' || (command === 'formatBlock' && value === 'blockquote')) && isInListOrQuote()) {
|
||||
@@ -175,12 +205,20 @@ const insertQuote = () => {
|
||||
const br = document.createElement('br')
|
||||
quoteContainer.parentNode.insertBefore(br, quoteContainer.nextSibling)
|
||||
|
||||
// 聚焦到内容区域
|
||||
const newRange = document.createRange()
|
||||
newRange.selectNodeContents(contentSpan)
|
||||
newRange.collapse(false)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(newRange)
|
||||
// 聚焦到内容区域(延迟执行,确保在handleToolClick处理完后再聚焦)
|
||||
setTimeout(() => {
|
||||
if (editorRef.value) {
|
||||
const newRange = document.createRange()
|
||||
newRange.selectNodeContents(contentSpan)
|
||||
newRange.collapse(false)
|
||||
const sel = window.getSelection()
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(newRange)
|
||||
|
||||
// 确保编辑器保持焦点状态
|
||||
editorRef.value.focus()
|
||||
}
|
||||
}, 0)
|
||||
|
||||
// 添加事件监听器到内容区域,监听内容变化
|
||||
const checkContent = () => {
|
||||
@@ -238,12 +276,24 @@ const insertTodoList = () => {
|
||||
const br = document.createElement('br')
|
||||
todoContainer.parentNode.insertBefore(br, todoContainer.nextSibling)
|
||||
|
||||
// 聚焦到内容区域
|
||||
const newRange = document.createRange()
|
||||
newRange.selectNodeContents(contentSpan)
|
||||
newRange.collapse(false)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(newRange)
|
||||
// 聚焦到内容区域(延迟执行,确保在handleToolClick处理完后再聚焦)
|
||||
setTimeout(() => {
|
||||
if (editorRef.value) {
|
||||
const newRange = document.createRange()
|
||||
newRange.selectNodeContents(contentSpan)
|
||||
newRange.collapse(false)
|
||||
const sel = window.getSelection()
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(newRange)
|
||||
|
||||
// 重要:不要重新聚焦到编辑器,因为待办事项有自己的可编辑区域
|
||||
// 但我们仍需要确保当前可编辑区域有焦点
|
||||
contentSpan.focus()
|
||||
|
||||
// 确保工具栏保持可见
|
||||
isToolbarVisible.value = true
|
||||
}
|
||||
}, 0)
|
||||
|
||||
// 添加事件监听器到图标
|
||||
icon.addEventListener('click', function () {
|
||||
@@ -275,6 +325,11 @@ const insertTodoList = () => {
|
||||
contentSpan.addEventListener('input', checkContent)
|
||||
contentSpan.addEventListener('blur', checkContent)
|
||||
|
||||
// 添加焦点事件监听器,确保工具栏在待办事项获得焦点时保持可见
|
||||
contentSpan.addEventListener('focus', () => {
|
||||
isToolbarVisible.value = true
|
||||
})
|
||||
|
||||
// 监听回车键,创建同级待办事项
|
||||
contentSpan.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -343,6 +398,11 @@ const insertTodoList = () => {
|
||||
newContentSpan.addEventListener('input', newCheckContent)
|
||||
newContentSpan.addEventListener('blur', newCheckContent)
|
||||
|
||||
// 添加焦点事件监听器,确保工具栏在待办事项获得焦点时保持可见
|
||||
newContentSpan.addEventListener('focus', () => {
|
||||
isToolbarVisible.value = true
|
||||
})
|
||||
|
||||
// 监听新内容区域的回车键
|
||||
newContentSpan.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -411,6 +471,11 @@ const insertTodoList = () => {
|
||||
nextContentSpan.addEventListener('input', nextCheckContent)
|
||||
nextContentSpan.addEventListener('blur', nextCheckContent)
|
||||
|
||||
// 添加焦点事件监听器,确保工具栏在待办事项获得焦点时保持可见
|
||||
nextContentSpan.addEventListener('focus', () => {
|
||||
isToolbarVisible.value = true
|
||||
})
|
||||
|
||||
handleInput()
|
||||
}
|
||||
})
|
||||
@@ -509,6 +574,80 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 显示工具栏
|
||||
const showToolbar = () => {
|
||||
isToolbarVisible.value = true
|
||||
}
|
||||
|
||||
// 隐藏工具栏
|
||||
const hideToolbar = () => {
|
||||
// 不立即隐藏工具栏,而是通过handleToolbarFocusOut处理
|
||||
// 添加延迟以确保点击工具栏按钮时不会立即隐藏
|
||||
setTimeout(() => {
|
||||
handleToolbarFocusOut()
|
||||
}, 200)
|
||||
}
|
||||
|
||||
// 处理工具栏按钮点击事件
|
||||
const handleToolClick = (action, event) => {
|
||||
// 阻止事件冒泡,防止触发编辑器失焦
|
||||
if (event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
// 执行工具操作
|
||||
action()
|
||||
|
||||
// 对于待办事项,不需要重新聚焦到编辑器,因为它有自己的可编辑区域
|
||||
// 其他工具需要重新聚焦到编辑器
|
||||
const isTodoAction = action === insertTodoList
|
||||
if (!isTodoAction) {
|
||||
// 重新聚焦到编辑器
|
||||
setTimeout(() => {
|
||||
if (editorRef.value) {
|
||||
editorRef.value.focus()
|
||||
}
|
||||
}, 0)
|
||||
} else {
|
||||
// 对于待办事项,确保工具栏保持可见
|
||||
isToolbarVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 保持工具栏可见
|
||||
const keepToolbarVisible = () => {
|
||||
isToolbarVisible.value = true
|
||||
}
|
||||
|
||||
// 处理工具栏失焦
|
||||
const handleToolbarFocusOut = () => {
|
||||
// 添加一个小延迟,以便处理工具栏按钮的点击事件
|
||||
setTimeout(() => {
|
||||
// 检查焦点是否在工具栏上
|
||||
const activeElement = document.activeElement
|
||||
const toolbarElement = document.querySelector('.toolbar')
|
||||
|
||||
// 如果焦点不在工具栏上才隐藏
|
||||
if (!toolbarElement || !toolbarElement.contains(activeElement)) {
|
||||
// 额外检查是否有待办事项的内容区域有焦点
|
||||
const todoContentElements = document.querySelectorAll('.todo-content')
|
||||
let todoHasFocus = false
|
||||
for (let i = 0; i < todoContentElements.length; i++) {
|
||||
if (todoContentElements[i] === activeElement || todoContentElements[i].contains(activeElement)) {
|
||||
todoHasFocus = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 只有在没有待办事项获得焦点时才隐藏工具栏
|
||||
if (!todoHasFocus) {
|
||||
isToolbarVisible.value = false
|
||||
}
|
||||
}
|
||||
}, 200) // 增加延迟时间,确保有足够时间处理点击事件
|
||||
}
|
||||
|
||||
// 监听外部值变化
|
||||
defineExpose({
|
||||
getContent: () => content.value,
|
||||
@@ -531,11 +670,24 @@ defineExpose({
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-top: 1px solid var(--border);
|
||||
background-color: var(--background-card);
|
||||
flex-shrink: 0;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 1000;
|
||||
/* 确保工具栏能正确获取焦点 */
|
||||
tabindex: 0;
|
||||
}
|
||||
|
||||
.toolbar.visible {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
@@ -549,6 +701,8 @@ defineExpose({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* 确保按钮能正确获取焦点 */
|
||||
tabindex: 0;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
@@ -572,11 +726,11 @@ defineExpose({
|
||||
|
||||
.editor-content {
|
||||
flex: 1;
|
||||
padding: 20px 10px;
|
||||
padding: 0 10px 60px 10px; /* 添加底部内边距,防止内容被工具栏遮挡 */
|
||||
outline: none;
|
||||
overflow-y: auto;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
font-size: var(--editor-font-size, 16px);
|
||||
line-height: var(--editor-line-height, 1.6);
|
||||
color: var(--note-content);
|
||||
min-height: 200px;
|
||||
background-color: var(--background-card);
|
||||
@@ -584,9 +738,9 @@ defineExpose({
|
||||
position: relative;
|
||||
/* 基准线样式 */
|
||||
background-image: linear-gradient(to bottom, var(--border) 1px, transparent 1px);
|
||||
background-size: 100% 25.6px; /* 16px * 1.6 = 25.6px */
|
||||
background-size: 100% calc(var(--editor-font-size, 16px) * var(--editor-line-height, 1.6)); /* var(--editor-font-size) * var(--editor-line-height) */
|
||||
background-repeat: repeat-y;
|
||||
background-position: 0 20px;
|
||||
background-position: 0 calc((var(--editor-font-size, 16px) * var(--editor-line-height, 1.6) - var(--editor-font-size, 16px)) / 2);
|
||||
}
|
||||
|
||||
.editor-content::before {
|
||||
@@ -607,17 +761,17 @@ defineExpose({
|
||||
/* 优化段落样式,确保与基准线对齐 */
|
||||
:deep(.editor-content p) {
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.6;
|
||||
line-height: var(--editor-line-height, 1.6);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* 自定义内容样式 - 统一行高和间距 */
|
||||
:deep(.editor-content h2) {
|
||||
font-size: 20px;
|
||||
font-size: var(--editor-font-size, 16px);
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--note-title);
|
||||
line-height: 1.6;
|
||||
line-height: var(--editor-line-height, 1.6);
|
||||
letter-spacing: 0.3px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
@@ -630,32 +784,32 @@ defineExpose({
|
||||
color: var(--text-secondary);
|
||||
background-color: var(--background-secondary);
|
||||
font-style: italic;
|
||||
line-height: 1.6;
|
||||
line-height: var(--editor-line-height, 1.6);
|
||||
}
|
||||
|
||||
:deep(.quote-container) {
|
||||
position: relative;
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.6;
|
||||
line-height: var(--editor-line-height, 1.6);
|
||||
}
|
||||
|
||||
:deep(.quote-icon) {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: var(--editor-font-size, 16px);
|
||||
height: var(--editor-font-size, 16px);
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
:deep(.quote-content) {
|
||||
border-left: 3px solid var(--primary);
|
||||
padding: 0 16px 0 32px;
|
||||
margin-left: 16px;
|
||||
padding: 0 var(--editor-font-size, 16px) 0 32px;
|
||||
margin-left: var(--editor-font-size, 16px);
|
||||
color: var(--text-secondary);
|
||||
background-color: var(--background-secondary);
|
||||
font-style: italic;
|
||||
line-height: 1.6;
|
||||
line-height: var(--editor-line-height, 1.6);
|
||||
}
|
||||
|
||||
:deep(.editor-content ul),
|
||||
@@ -663,12 +817,12 @@ defineExpose({
|
||||
margin: 0 0 12px 0;
|
||||
padding-left: 32px;
|
||||
position: relative;
|
||||
line-height: 1.6;
|
||||
line-height: var(--editor-line-height, 1.6);
|
||||
}
|
||||
|
||||
:deep(.editor-content li) {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
line-height: var(--editor-line-height, 1.6);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -702,26 +856,26 @@ defineExpose({
|
||||
:deep(.quote-container) {
|
||||
position: relative;
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.6;
|
||||
line-height: var(--editor-line-height, 1.6);
|
||||
}
|
||||
|
||||
:deep(.quote-icon) {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: var(--editor-font-size, 16px);
|
||||
height: var(--editor-font-size, 16px);
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
:deep(.quote-content) {
|
||||
border-left: 3px solid var(--primary);
|
||||
padding: 0 16px 0 32px;
|
||||
margin-left: 16px;
|
||||
padding: 0 var(--editor-font-size, 16px) 0 32px;
|
||||
margin-left: var(--editor-font-size, 16px);
|
||||
color: var(--text-secondary);
|
||||
background-color: var(--background-secondary);
|
||||
font-style: italic;
|
||||
line-height: 1.6;
|
||||
line-height: var(--editor-line-height, 1.6);
|
||||
}
|
||||
|
||||
/* 待办事项样式 */
|
||||
@@ -729,13 +883,13 @@ defineExpose({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
line-height: var(--editor-line-height, 1.6);
|
||||
}
|
||||
|
||||
:deep(.todo-icon) {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin-right: 2px;
|
||||
width: calc(var(--editor-font-size, 16px) * 2);
|
||||
height: calc(var(--editor-font-size, 16px) * 2);
|
||||
margin-right: 3px;
|
||||
margin-top: 0;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
@@ -744,7 +898,7 @@ defineExpose({
|
||||
:deep(.todo-content) {
|
||||
flex: 1;
|
||||
outline: none;
|
||||
line-height: 1.6;
|
||||
line-height: var(--editor-line-height, 1.6);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -3,17 +3,18 @@
|
||||
<div class="container">
|
||||
<Header :onBack="handleCancel" :onAction="handleSave" actionIcon="save" />
|
||||
|
||||
<!-- 顶部信息栏 -->
|
||||
<div class="header-info">
|
||||
<span class="edit-time">{{ formattedTime }}</span>
|
||||
<span>|</span>
|
||||
<span class="word-count">{{ wordCount }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 富文本编辑器 -->
|
||||
<div class="editor-container">
|
||||
<RichTextEditor ref="editorRef" v-model="content" class="rich-text-editor" />
|
||||
</div>
|
||||
|
||||
<!-- 底部信息栏 -->
|
||||
<div class="footer-info">
|
||||
<span class="edit-time">{{ formattedTime }}</span>
|
||||
<span class="word-count">{{ wordCount }} 字</span>
|
||||
</div>
|
||||
|
||||
<ion-alert
|
||||
:is-open="showAlert"
|
||||
@didDismiss="() => setShowAlert(false)"
|
||||
@@ -130,7 +131,7 @@ const setShowAlert = value => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="less" scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -148,13 +149,14 @@ const setShowAlert = value => {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.footer-info {
|
||||
.header-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
padding: 8px 16px;
|
||||
background-color: var(--background-card);
|
||||
border-top: 1px solid var(--border);
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user