You've already forked SmartisanNote.Remake
添加了富文本编辑器雏形
This commit is contained in:
563
src/components/RichTextEditor.vue
Normal file
563
src/components/RichTextEditor.vue
Normal file
@@ -0,0 +1,563 @@
|
|||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const editorRef = ref(null)
|
||||||
|
const content = ref(props.modelValue || '')
|
||||||
|
|
||||||
|
// 工具配置
|
||||||
|
const tools = ref([
|
||||||
|
{
|
||||||
|
name: 'bold',
|
||||||
|
icon: '/assets/icons/drawable-xxhdpi/rtf_bold_normal.9.png',
|
||||||
|
action: () => formatText('bold'),
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'center',
|
||||||
|
icon: '/assets/icons/drawable-xxhdpi/rtf_center_normal.9.png',
|
||||||
|
action: () => formatText('justifyCenter'),
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'todo',
|
||||||
|
icon: '/assets/icons/drawable-xxhdpi/rtf_gtasks_normal.9.png',
|
||||||
|
action: () => insertTodoList(),
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'list',
|
||||||
|
icon: '/assets/icons/drawable-xxhdpi/rtf_list_normal.9.png',
|
||||||
|
action: () => formatText('insertUnorderedList'),
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'header',
|
||||||
|
icon: '/assets/icons/drawable-xxhdpi/rtf_header_normal.9.png',
|
||||||
|
action: () => formatText('formatBlock', 'h2'),
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quote',
|
||||||
|
icon: '/assets/icons/drawable-xxhdpi/rtf_quot_normal.9.png',
|
||||||
|
action: () => insertQuote(),
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 格式化文本
|
||||||
|
const formatText = (command, value = null) => {
|
||||||
|
document.execCommand(command, false, value)
|
||||||
|
updateToolbarState()
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入水平线
|
||||||
|
const insertHorizontalRule = () => {
|
||||||
|
document.execCommand('insertHorizontalRule', false, null)
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入引用格式
|
||||||
|
const insertQuote = () => {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (selection.rangeCount > 0) {
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
|
||||||
|
// 创建引用容器
|
||||||
|
const quoteContainer = document.createElement('div')
|
||||||
|
quoteContainer.className = 'quote-container'
|
||||||
|
|
||||||
|
// 创建图标元素
|
||||||
|
const icon = document.createElement('img')
|
||||||
|
icon.className = 'quote-icon'
|
||||||
|
icon.src = '/assets/icons/drawable-xxhdpi/rag_quote.png'
|
||||||
|
icon.alt = '引用'
|
||||||
|
|
||||||
|
// 创建内容容器
|
||||||
|
const contentSpan = document.createElement('blockquote')
|
||||||
|
contentSpan.className = 'quote-content'
|
||||||
|
contentSpan.textContent = '引用内容'
|
||||||
|
|
||||||
|
// 组装元素
|
||||||
|
quoteContainer.appendChild(icon)
|
||||||
|
quoteContainer.appendChild(contentSpan)
|
||||||
|
|
||||||
|
// 插入到当前光标位置
|
||||||
|
range.insertNode(quoteContainer)
|
||||||
|
|
||||||
|
// 添加换行
|
||||||
|
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)
|
||||||
|
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入待办事项列表
|
||||||
|
const insertTodoList = () => {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (selection.rangeCount > 0) {
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
|
||||||
|
// 创建待办事项容器
|
||||||
|
const todoContainer = document.createElement('div')
|
||||||
|
todoContainer.contentEditable = false
|
||||||
|
todoContainer.className = 'todo-container'
|
||||||
|
|
||||||
|
// 创建图标元素
|
||||||
|
const icon = document.createElement('img')
|
||||||
|
icon.className = 'todo-icon'
|
||||||
|
icon.src = '/assets/icons/drawable-xxhdpi/rtf_icon_gtasks.png'
|
||||||
|
icon.alt = '待办事项'
|
||||||
|
|
||||||
|
// 创建内容容器
|
||||||
|
const contentSpan = document.createElement('span')
|
||||||
|
contentSpan.contentEditable = true
|
||||||
|
contentSpan.className = 'todo-content'
|
||||||
|
contentSpan.textContent = '待办事项'
|
||||||
|
|
||||||
|
// 组装元素
|
||||||
|
todoContainer.appendChild(icon)
|
||||||
|
todoContainer.appendChild(contentSpan)
|
||||||
|
|
||||||
|
// 插入到当前光标位置
|
||||||
|
range.insertNode(todoContainer)
|
||||||
|
|
||||||
|
// 添加换行
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 添加事件监听器到图标
|
||||||
|
icon.addEventListener('click', function () {
|
||||||
|
// 根据当前状态切换图标
|
||||||
|
if (this.src.includes('rtf_icon_gtasks.png')) {
|
||||||
|
this.src = '/assets/icons/drawable-xxhdpi/rtf_icon_gtasks_light.png'
|
||||||
|
contentSpan.style.color = 'var(--text-tertiary)'
|
||||||
|
contentSpan.style.textDecoration = 'line-through'
|
||||||
|
} else {
|
||||||
|
this.src = '/assets/icons/drawable-xxhdpi/rtf_icon_gtasks.png'
|
||||||
|
contentSpan.style.color = 'var(--note-content)'
|
||||||
|
contentSpan.style.textDecoration = 'none'
|
||||||
|
}
|
||||||
|
handleInput()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加事件监听器到内容区域,监听内容变化
|
||||||
|
const checkContent = () => {
|
||||||
|
// 延迟检查,确保内容已更新
|
||||||
|
setTimeout(() => {
|
||||||
|
if (contentSpan.textContent.trim() === '') {
|
||||||
|
// 如果内容为空,移除整个待办事项容器
|
||||||
|
todoContainer.remove()
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentSpan.addEventListener('input', checkContent)
|
||||||
|
contentSpan.addEventListener('blur', checkContent)
|
||||||
|
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理输入事件
|
||||||
|
const handleInput = () => {
|
||||||
|
if (editorRef.value) {
|
||||||
|
content.value = editorRef.value.innerHTML
|
||||||
|
emit('update:modelValue', content.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理键盘事件
|
||||||
|
const handleKeydown = e => {
|
||||||
|
// 处理Shift+Enter键换行
|
||||||
|
if (e.key === 'Enter' && e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
document.execCommand('insertHTML', false, '<br><br>')
|
||||||
|
handleInput()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理Tab键缩进
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault()
|
||||||
|
document.execCommand('insertHTML', false, ' ')
|
||||||
|
handleInput()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理回车键创建新列表项
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
// 检查是否在列表中
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (selection.rangeCount > 0) {
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
const container = range.startContainer
|
||||||
|
|
||||||
|
// 检查是否在ul或ol内
|
||||||
|
let listElement = null
|
||||||
|
let current = container.nodeType === Node.TEXT_NODE ? container.parentElement : container
|
||||||
|
|
||||||
|
while (current && current !== editorRef.value) {
|
||||||
|
if (current.tagName === 'LI') {
|
||||||
|
listElement = current
|
||||||
|
break
|
||||||
|
}
|
||||||
|
current = current.parentElement
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果在列表项中且为空,退出列表
|
||||||
|
if (listElement) {
|
||||||
|
const liContent = listElement.textContent.trim()
|
||||||
|
if (liContent === '') {
|
||||||
|
e.preventDefault()
|
||||||
|
// 退出列表
|
||||||
|
document.execCommand('outdent')
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新工具栏状态
|
||||||
|
const updateToolbarState = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
tools.value.forEach(tool => {
|
||||||
|
if (tool.name === 'bold') {
|
||||||
|
tool.active = document.queryCommandState('bold')
|
||||||
|
} else if (tool.name === 'center') {
|
||||||
|
tool.active = document.queryCommandState('justifyCenter')
|
||||||
|
} else if (tool.name === 'list') {
|
||||||
|
tool.active = document.queryCommandState('insertUnorderedList')
|
||||||
|
} else if (tool.name === 'quote') {
|
||||||
|
tool.active = document.queryCommandState('formatBlock')
|
||||||
|
} else if (tool.name === 'header') {
|
||||||
|
tool.active = document.queryCommandState('formatBlock')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化编辑器内容
|
||||||
|
onMounted(() => {
|
||||||
|
if (editorRef.value) {
|
||||||
|
editorRef.value.innerHTML = content.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听外部值变化
|
||||||
|
defineExpose({
|
||||||
|
getContent: () => content.value,
|
||||||
|
setContent: newContent => {
|
||||||
|
content.value = newContent
|
||||||
|
if (editorRef.value) {
|
||||||
|
editorRef.value.innerHTML = newContent
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.editor-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--background-card);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background-color: var(--background-card);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
padding: 6px;
|
||||||
|
margin-right: 6px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn:hover {
|
||||||
|
background-color: var(--background-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn.active {
|
||||||
|
background-color: var(--primary-light);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn.active .toolbar-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px 16px;
|
||||||
|
outline: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--note-content);
|
||||||
|
min-height: 200px;
|
||||||
|
background-color: var(--background-card);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content:focus,
|
||||||
|
.editor-content * {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义内容样式 */
|
||||||
|
.editor-content h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 16px 0 8px 0;
|
||||||
|
color: var(--note-title);
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding-left: 24px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content blockquote {
|
||||||
|
border-left: 3px solid var(--primary);
|
||||||
|
padding-left: 16px;
|
||||||
|
margin: 16px 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background-color: var(--background-secondary);
|
||||||
|
padding: 8px 16px 8px 16px;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-container {
|
||||||
|
position: relative;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 10px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-content {
|
||||||
|
border-left: 3px solid var(--primary);
|
||||||
|
padding-left: 32px;
|
||||||
|
margin-left: 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background-color: var(--background-secondary);
|
||||||
|
padding: 8px 16px 8px 16px;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content ul,
|
||||||
|
.editor-content ol {
|
||||||
|
margin: 16px 0;
|
||||||
|
padding-left: 32px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content li {
|
||||||
|
margin: 6px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content p {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content strong {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content em {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content u {
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px solid var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content hr {
|
||||||
|
border: none;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--border);
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content div[style*='text-align: center'] {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 待办事项样式 */
|
||||||
|
:deep(.todo-container) {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.todo-icon) {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0.2rem;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.todo-content) {
|
||||||
|
flex: 1;
|
||||||
|
outline: none;
|
||||||
|
padding: 0.125rem 0.25rem;
|
||||||
|
min-height: 1.25rem;
|
||||||
|
border-radius: 0.125rem;
|
||||||
|
color: var(--note-content);
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 引用格式样式 */
|
||||||
|
:deep(.quote-container) {
|
||||||
|
position: relative;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.quote-icon) {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0.625rem;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.quote-content) {
|
||||||
|
border-left: 0.1875rem solid var(--primary);
|
||||||
|
padding-left: 2rem;
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background-color: var(--background-secondary);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0 0.25rem 0.25rem 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 格式标识图标 */
|
||||||
|
.editor-content h2::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-image: url('/assets/icons/drawable-xxhdpi/rtf_header_normal.9.png');
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content blockquote::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-image: url('/assets/icons/drawable-xxhdpi/rtf_quot_normal.9.png');
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content ul::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-image: url('/assets/icons/drawable-xxhdpi/rtf_list_normal.9.png');
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content ol::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-image: url('/assets/icons/drawable-xxhdpi/rtf_list_normal.9.png');
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,317 +1,221 @@
|
|||||||
<template>
|
<template>
|
||||||
<ion-page>
|
<ion-page>
|
||||||
<div class="container code-fun-relative">
|
<div class="container">
|
||||||
<Header title="" :onBack="handleCancel" :onAction="handleSave" />
|
<Header :title="isEditing ? '编辑便签' : '新建便签'" :onBack="handleCancel" :onAction="handleSave" actionText="保存" />
|
||||||
<div class="code-fun-flex-col code-fun-justify-start code-fun-relative page">
|
|
||||||
<div class="code-fun-flex-col section_4 pos_3">
|
<!-- 标题输入框 -->
|
||||||
<div class="code-fun-flex-row code-fun-justify-between code-fun-items-center section_5">
|
<div class="title-input-container">
|
||||||
<div class="code-fun-flex-row code-fun-items-center">
|
<input placeholder="标题" v-model="title" class="title-input" />
|
||||||
<!-- 便签文件夹切换 -->
|
|
||||||
<div class="code-fun-flex-col code-fun-justify-start code-fun-shrink-0 section_6">
|
|
||||||
<div class="code-fun-flex-row code-fun-items-center section_7">
|
|
||||||
<img class="code-fun-shrink-0 image_7" src="/assets/icons/drawable-xxhdpi/note_empty.png" />
|
|
||||||
<span class="text_3">全部便签</span>
|
|
||||||
<img class="code-fun-shrink-0 image_9" src="/assets/icons/drawable-xxhdpi/icon_detail_mode_arrow_normal.png" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 编辑时间 -->
|
|
||||||
<span class="text_2">今天下午6:30</span>
|
|
||||||
<i>|</i>
|
|
||||||
<!-- 字数统计 -->
|
|
||||||
<span class="text_4">21</span>
|
|
||||||
</div>
|
|
||||||
<div class="code-fun-flex-row code-fun-items-center">
|
|
||||||
<!-- 是否收藏状态&收藏按钮 -->
|
|
||||||
<img class="image_6" src="/assets/icons/drawable-xxhdpi/icon_detail_star_unchecked.png" />
|
|
||||||
<!-- 编辑模式切换:RTF、MARKDOWN -->
|
|
||||||
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-center section_8 code-fun-ml-20">
|
|
||||||
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-center text-wrapper">
|
|
||||||
<span class="text_5">RTF</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<img class="image_10 code-fun-ml-20" src="/assets/icons/drawable-xxhdpi/icon_detail_mode_arrow_normal.png" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="code-fun-flex-col group">
|
|
||||||
<!-- 普通文本 -->
|
|
||||||
<span class="code-fun-self-start font text_6">手机设计</span>
|
|
||||||
<!-- 待办事项文本 -->
|
|
||||||
<div class="code-fun-flex-col code-fun-justify-start code-fun-self-stretch code-fun-relative group_2 code-fun-mt-8">
|
|
||||||
<!-- 已完成状态 -->
|
|
||||||
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-start section_10">
|
|
||||||
<div class="code-fun-flex-row section_11">
|
|
||||||
<img class="image_11" src="/assets/icons/drawable-xxhdpi/preview_rtf_icon_gtasks_light.png" />
|
|
||||||
<span class="font text_8 code-fun-ml-12">啥事实</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 未完成状态 -->
|
|
||||||
<div class="code-fun-flex-row section_9 pos_4">
|
|
||||||
<img class="image_11" src="/assets/icons/drawable-xxhdpi/preview_rtf_icon_gtasks.png" />
|
|
||||||
<span class="font text_7 code-fun-ml-12">去问问</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="code-fun-flex-col code-fun-justify-start code-fun-self-stretch code-fun-relative section_14 code-fun-mt-8">
|
|
||||||
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-start section_15">
|
|
||||||
<div class="code-fun-flex-col code-fun-justify-start code-fun-relative section_17">
|
|
||||||
<div class="code-fun-flex-col code-fun-justify-start section_16 pos_7">
|
|
||||||
<!-- 列表文本 -->
|
|
||||||
<div class="code-fun-flex-row code-fun-items-center section_18">
|
|
||||||
<img class="image_13" src="/assets/icons/drawable-xxhdpi/indicator.png" />
|
|
||||||
<span class="font text_11 code-fun-ml-20">大电锯</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="code-fun-flex-col code-fun-justify-start section_12 pos_5">
|
|
||||||
<!-- 引用文本 -->
|
|
||||||
<div class="code-fun-flex-col code-fun-items-start section_13">
|
|
||||||
<img class="code-fun-shrink-0 image_12" src="/assets/icons/drawable-xxhdpi/note_quote_editing_sign.9.png" />
|
|
||||||
<span class="text_9">设计师</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 富文本编辑器 -->
|
||||||
|
<div class="editor-container">
|
||||||
|
<RichTextEditor ref="editorRef" v-model="content" class="rich-text-editor" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部信息栏 -->
|
||||||
|
<div class="footer-info">
|
||||||
|
<span class="edit-time">{{ formattedTime }}</span>
|
||||||
|
<span class="word-count">{{ wordCount }} 字</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ion-alert
|
||||||
|
:is-open="showAlert"
|
||||||
|
@didDismiss="() => setShowAlert(false)"
|
||||||
|
header="未保存的更改"
|
||||||
|
message="您有未保存的更改,确定要丢弃吗?"
|
||||||
|
:buttons="[
|
||||||
|
{
|
||||||
|
text: '取消',
|
||||||
|
role: 'cancel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '丢弃',
|
||||||
|
handler: () => window.history.back(),
|
||||||
|
},
|
||||||
|
]"></ion-alert>
|
||||||
</div>
|
</div>
|
||||||
</ion-page>
|
</ion-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup></script>
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useAppStore } from '../stores/useAppStore'
|
||||||
|
import Header from '../components/Header.vue'
|
||||||
|
import RichTextEditor from '../components/RichTextEditor.vue'
|
||||||
|
|
||||||
<style lang="less" scoped>
|
const props = defineProps({
|
||||||
.page {
|
noteId: {
|
||||||
background-image: url('/assets/icons/drawable-xxhdpi/mixed_view_item_bg.9.png');
|
type: String,
|
||||||
background-size: 100% 10px;
|
default: null,
|
||||||
background-repeat: repeat-y;
|
},
|
||||||
width: calc(100% + 6rem);
|
})
|
||||||
margin-left: -5rem;
|
|
||||||
padding-left: 3rem;
|
const store = useAppStore()
|
||||||
height: auto;
|
const editorRef = ref(null)
|
||||||
min-height: 100vh;
|
|
||||||
box-sizing: border-box;
|
// 加载初始数据
|
||||||
.section_4 {
|
onMounted(async () => {
|
||||||
.section_5 {
|
await store.loadData()
|
||||||
padding: 0.75rem 1.75rem 1rem 2.25rem;
|
})
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
.section_6 {
|
// Check if we're editing an existing note
|
||||||
padding-top: 0.25rem;
|
const isEditing = !!props.noteId
|
||||||
background-color: rgba(0, 0, 0, 0);
|
const existingNote = isEditing ? store.notes.find(n => n.id === props.noteId) : null
|
||||||
width: 7rem;
|
|
||||||
height: 2rem;
|
// Initialize state with existing note data or empty strings
|
||||||
.section_7 {
|
const title = ref(existingNote?.title || '')
|
||||||
margin-left: 0.25rem;
|
const content = ref(existingNote?.content || '')
|
||||||
padding: 0.5rem;
|
const showAlert = ref(false)
|
||||||
background-color: rgb(226, 221, 213);
|
|
||||||
border-radius: 0.25rem;
|
// 计算属性
|
||||||
border: solid 0.063rem rgb(182, 174, 164);
|
const formattedTime = computed(() => {
|
||||||
.image_7 {
|
if (!isEditing) return '新建便签'
|
||||||
width: 0.78rem;
|
|
||||||
height: 0.81rem;
|
const date = new Date(existingNote.updatedAt)
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
// 如果是今天
|
||||||
|
if (date.toDateString() === now.toDateString()) {
|
||||||
|
return `编辑于 今天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
.text_3 {
|
|
||||||
margin-left: 0.5rem;
|
// 如果是昨天
|
||||||
color: rgb(176, 166, 156);
|
const yesterday = new Date(now)
|
||||||
font-size: 0.79rem;
|
yesterday.setDate(yesterday.getDate() - 1)
|
||||||
line-height: 0.79rem;
|
if (date.toDateString() === yesterday.toDateString()) {
|
||||||
}
|
return `编辑于 昨天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||||
.image_9 {
|
|
||||||
margin-left: 0.75rem;
|
|
||||||
width: 0.44rem;
|
|
||||||
height: 0.22rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.text_2 {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
color: rgb(178, 170, 160);
|
|
||||||
font-size: 0.98rem;
|
|
||||||
line-height: 0.98rem;
|
|
||||||
}
|
|
||||||
.image_8 {
|
|
||||||
margin-left: 1.75rem;
|
|
||||||
width: 0.094rem;
|
|
||||||
height: 0.75rem;
|
|
||||||
}
|
|
||||||
.text_4 {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
color: rgb(168, 161, 150);
|
|
||||||
font-size: 1.1rem;
|
|
||||||
line-height: 1.1rem;
|
|
||||||
}
|
|
||||||
.image_6 {
|
|
||||||
width: 1.53rem;
|
|
||||||
height: 1.44rem;
|
|
||||||
}
|
|
||||||
.section_8 {
|
|
||||||
padding: 0.25rem 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
width: 2.94rem;
|
|
||||||
height: 1.72rem;
|
|
||||||
.text-wrapper {
|
|
||||||
padding: 0.25rem 0;
|
|
||||||
background-color: rgb(232, 216, 186);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
width: 2.63rem;
|
|
||||||
border: solid 0.063rem rgb(205, 159, 89);
|
|
||||||
.text_5 {
|
|
||||||
color: rgb(152, 103, 51);
|
|
||||||
font-size: 0.81rem;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 0.81rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.image_10 {
|
|
||||||
width: 0.75rem;
|
|
||||||
height: 0.47rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.group {
|
|
||||||
padding: 0.75rem 0;
|
|
||||||
.font {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
}
|
|
||||||
.text_6 {
|
|
||||||
margin-left: 2.75rem;
|
|
||||||
color: rgb(124, 108, 87);
|
|
||||||
font-size: 1.5rem;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
}
|
|
||||||
.group_2 {
|
|
||||||
padding-top: 0.75rem;
|
|
||||||
.section_10 {
|
|
||||||
margin-left: 1.25rem;
|
|
||||||
padding-top: 3.13rem;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
height: 6.84rem;
|
|
||||||
.section_11 {
|
|
||||||
margin-left: -0.5rem;
|
|
||||||
padding: 1.25rem 2.5rem 1rem;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
width: 36rem;
|
|
||||||
.text_8 {
|
|
||||||
color: rgb(212, 207, 198);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.section_9 {
|
|
||||||
padding: 1.75rem 1.5rem 1rem;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
.text_7 {
|
|
||||||
color: rgb(122, 107, 87);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pos_4 {
|
|
||||||
position: absolute;
|
|
||||||
left: 1.69rem;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
.image_11 {
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.section_14 {
|
|
||||||
padding: 4.75rem 0 2.25rem;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
.section_15 {
|
|
||||||
margin-right: 1.5rem;
|
|
||||||
padding: 0.75rem 0 3.75rem;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
.section_17 {
|
|
||||||
padding-top: 2.75rem;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
width: 36.84rem;
|
|
||||||
height: 8.16rem;
|
|
||||||
.section_19 {
|
|
||||||
margin-right: 1.75rem;
|
|
||||||
padding-top: 1.5rem;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
width: 35.13rem;
|
|
||||||
.image-wrapper {
|
|
||||||
padding: 0.75rem 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
width: 36.84rem;
|
|
||||||
.image_14 {
|
|
||||||
margin-left: 2.5rem;
|
|
||||||
width: 0.19rem;
|
|
||||||
height: 2.72rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.section_16 {
|
|
||||||
padding-top: 1.75rem;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
width: 36.84rem;
|
|
||||||
height: 5.41rem;
|
|
||||||
.section_18 {
|
|
||||||
padding: 0.75rem 3.75rem 1.25rem;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
width: 36.84rem;
|
|
||||||
.image_13 {
|
|
||||||
width: 0.38rem;
|
|
||||||
height: 0.38rem;
|
|
||||||
}
|
|
||||||
.text_11 {
|
|
||||||
color: rgb(122, 106, 84);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pos_7 {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: -0.53rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.text-wrapper_2 {
|
|
||||||
padding: 1.25rem 0 9.5rem;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
.text_10 {
|
|
||||||
margin-left: 2.75rem;
|
|
||||||
color: rgb(116, 94, 68);
|
|
||||||
font-size: 1.5rem;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pos_6 {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 2.84rem;
|
|
||||||
}
|
|
||||||
.section_12 {
|
|
||||||
padding-bottom: 10.25rem;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
.section_13 {
|
|
||||||
margin-left: 0.75rem;
|
|
||||||
padding: 1.25rem 2.75rem;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
.image_12 {
|
|
||||||
width: 1.06rem;
|
|
||||||
height: 0.88rem;
|
|
||||||
}
|
|
||||||
.text_9 {
|
|
||||||
margin-left: 2rem;
|
|
||||||
color: rgb(161, 151, 139);
|
|
||||||
font-size: 1.31rem;
|
|
||||||
line-height: 1.31rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pos_5 {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: -1.09rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 其他情况显示月日
|
||||||
|
return `编辑于 ${date.getMonth() + 1}月${date.getDate()}日`
|
||||||
|
})
|
||||||
|
|
||||||
|
const wordCount = computed(() => {
|
||||||
|
// 移除HTML标签计算字数
|
||||||
|
const textContent = content.value.replace(/<[^>]*>/g, '')
|
||||||
|
return (title.value.length || 0) + (textContent.length || 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理保存
|
||||||
|
const handleSave = async () => {
|
||||||
|
// Validate input
|
||||||
|
if (!title.value.trim()) {
|
||||||
|
// In a full implementation, show an alert or toast
|
||||||
|
console.log('Validation error: Please enter a note title.')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEditing && existingNote) {
|
||||||
|
// Update existing note
|
||||||
|
await store.updateNote(props.noteId, {
|
||||||
|
title: title.value,
|
||||||
|
content: content.value,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Create new note
|
||||||
|
await store.addNote({
|
||||||
|
title: title.value,
|
||||||
|
content: content.value,
|
||||||
|
isStarred: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Navigate back to the previous screen
|
||||||
|
window.location.hash = '#/notes'
|
||||||
|
} catch (error) {
|
||||||
|
// In a full implementation, show an alert or toast
|
||||||
|
console.log('Save error: Failed to save note. Please try again.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理取消
|
||||||
|
const handleCancel = () => {
|
||||||
|
// Check if there are unsaved changes
|
||||||
|
const hasUnsavedChanges = title.value !== (existingNote?.title || '') || content.value !== (existingNote?.content || '')
|
||||||
|
|
||||||
|
if (hasUnsavedChanges) {
|
||||||
|
showAlert.value = true
|
||||||
|
} else {
|
||||||
|
window.location.hash = '#/notes'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const setShowAlert = value => {
|
||||||
|
showAlert.value = value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background-color: var(--background-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
padding: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn:hover {
|
||||||
|
background-color: var(--background-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-input-container {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background-color: var(--background-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-input {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--note-title);
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: var(--background-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-editor {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: var(--background-card);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ion-page>
|
|
||||||
<Header
|
|
||||||
:title="isEditing ? '编辑便签' : '新建便签'"
|
|
||||||
:onBack="handleCancel"
|
|
||||||
:onAction="handleSave"
|
|
||||||
actionText="保存"
|
|
||||||
/>
|
|
||||||
<div style="display: flex; border-bottom: 1px solid var(--border); padding: 8px 16px; background-color: var(--background-card)">
|
|
||||||
<ion-button fill="clear" @click="handleBold" style="padding: 8px; margin-right: 8px">
|
|
||||||
<img src="/assets/icons/drawable-xxhdpi/rtf_bold_normal.9.png" style="width: 24px; height: 24px" />
|
|
||||||
</ion-button>
|
|
||||||
<ion-button fill="clear" @click="handleItalic" style="padding: 8px; margin-right: 8px">
|
|
||||||
<img src="/assets/icons/drawable-xxhdpi/rtf_gtasks_normal.9.png" style="width: 24px; height: 24px" />
|
|
||||||
</ion-button>
|
|
||||||
<ion-button fill="clear" @click="handleUnderline" style="padding: 8px; margin-right: 8px">
|
|
||||||
<img src="/assets/icons/drawable-xxhdpi/rtf_list_normal.9.png" style="width: 24px; height: 24px" />
|
|
||||||
</ion-button>
|
|
||||||
<ion-button fill="clear" @click="handleList" style="padding: 8px; margin-right: 8px">
|
|
||||||
<img src="/assets/icons/drawable-xxhdpi/rtf_header_normal.9.png" style="width: 24px; height: 24px" />
|
|
||||||
</ion-button>
|
|
||||||
<ion-button fill="clear" @click="handleHeader" style="padding: 8px; margin-right: 8px">
|
|
||||||
<img src="/assets/icons/drawable-xxhdpi/rtf_quot_normal.9.png" style="width: 24px; height: 24px" />
|
|
||||||
</ion-button>
|
|
||||||
<ion-button fill="clear" @click="handleQuote" style="padding: 8px">
|
|
||||||
<img src="/assets/icons/drawable-xxhdpi/rtf_center_normal.9.png" style="width: 24px; height: 24px" />
|
|
||||||
</ion-button>
|
|
||||||
</div>
|
|
||||||
<ion-content>
|
|
||||||
<div style="padding: 16px; background-color: var(--background-card)">
|
|
||||||
<textarea
|
|
||||||
placeholder="标题"
|
|
||||||
:value="title"
|
|
||||||
@input="e => setTitle(e.target.value)"
|
|
||||||
style="font-size: 22px; font-weight: 600; color: var(--note-title); margin-bottom: 16px; padding: 8px 0; border-bottom: 1px solid var(--border); width: 100%; background-color: transparent; border: none; outline: none; resize: none; font-family: inherit"
|
|
||||||
></textarea>
|
|
||||||
<textarea
|
|
||||||
placeholder="开始写作..."
|
|
||||||
:value="content"
|
|
||||||
@input="e => setContent(e.target.value)"
|
|
||||||
style="font-size: 16px; color: var(--note-content); line-height: 24px; width: 100%; height: calc(100vh - 200px); background-color: transparent; border: none; outline: none; resize: none; font-family: inherit"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</ion-content>
|
|
||||||
|
|
||||||
<ion-alert
|
|
||||||
:is-open="showAlert"
|
|
||||||
@didDismiss="() => setShowAlert(false)"
|
|
||||||
header="未保存的更改"
|
|
||||||
message="您有未保存的更改,确定要丢弃吗?"
|
|
||||||
:buttons="[
|
|
||||||
{
|
|
||||||
text: '取消',
|
|
||||||
role: 'cancel'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '丢弃',
|
|
||||||
handler: () => window.history.back()
|
|
||||||
}
|
|
||||||
]"
|
|
||||||
></ion-alert>
|
|
||||||
</ion-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted } from 'vue';
|
|
||||||
import { useAppStore } from '../stores/useAppStore';
|
|
||||||
import { save, text, list } from 'ionicons/icons';
|
|
||||||
import Header from '../components/Header.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
noteId: {
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = useAppStore();
|
|
||||||
|
|
||||||
// 加载初始数据
|
|
||||||
onMounted(() => {
|
|
||||||
store.loadData();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if we're editing an existing note
|
|
||||||
const isEditing = !!props.noteId;
|
|
||||||
const existingNote = isEditing ? store.notes.find(n => n.id === props.noteId) : null;
|
|
||||||
|
|
||||||
// Initialize state with existing note data or empty strings
|
|
||||||
const title = ref(existingNote?.title || '');
|
|
||||||
const content = ref(existingNote?.content || '');
|
|
||||||
const showAlert = ref(false);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
// Validate input
|
|
||||||
if (!title.value.trim()) {
|
|
||||||
// In a full implementation, show an alert or toast
|
|
||||||
console.log('Validation error: Please enter a note title.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isEditing && existingNote) {
|
|
||||||
// Update existing note
|
|
||||||
await store.updateNote(props.noteId, { title: title.value, content: content.value });
|
|
||||||
} else {
|
|
||||||
// Create new note
|
|
||||||
await store.addNote({ title: title.value, content: content.value, isStarred: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate back to the previous screen
|
|
||||||
window.history.back();
|
|
||||||
} catch (error) {
|
|
||||||
// In a full implementation, show an alert or toast
|
|
||||||
console.log('Save error: Failed to save note. Please try again.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
// Check if there are unsaved changes
|
|
||||||
const hasUnsavedChanges =
|
|
||||||
title.value !== (existingNote?.title || '') ||
|
|
||||||
content.value !== (existingNote?.content || '');
|
|
||||||
|
|
||||||
if (hasUnsavedChanges) {
|
|
||||||
showAlert.value = true;
|
|
||||||
} else {
|
|
||||||
window.history.back();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Toolbar actions
|
|
||||||
const handleBold = () => {
|
|
||||||
// In a full implementation, this would apply bold formatting
|
|
||||||
console.log('Bold pressed');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleItalic = () => {
|
|
||||||
// In a full implementation, this would apply italic formatting
|
|
||||||
console.log('Italic pressed');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUnderline = () => {
|
|
||||||
// In a full implementation, this would apply underline formatting
|
|
||||||
console.log('Underline pressed');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleList = () => {
|
|
||||||
// In a full implementation, this would insert a list
|
|
||||||
console.log('List pressed');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHeader = () => {
|
|
||||||
// In a full implementation, this would apply header formatting
|
|
||||||
console.log('Header pressed');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleQuote = () => {
|
|
||||||
// In a full implementation, this would apply quote formatting
|
|
||||||
console.log('Quote pressed');
|
|
||||||
};
|
|
||||||
|
|
||||||
const setTitle = (value) => {
|
|
||||||
title.value = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setContent = (value) => {
|
|
||||||
content.value = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setShowAlert = (value) => {
|
|
||||||
showAlert.value = value;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
Reference in New Issue
Block a user