html
复制代码
<template>
<div class="notes-app">
<!-- 顶部导航栏 -->
<nav class="app-nav" :class="{ 'nav-with-back': currentView !== 'list' }">
<button
class="back-btn"
@click="handleBack"
v-if="currentView !== 'list'"
>
<i class="icon-arrow-left"></i>
</button>
<h1 class="app-title">
{{ currentView === 'list' ? '记事本' :
currentView === 'detail' ? '笔记详情' :
currentView === 'edit' ? '编辑笔记' : '新建笔记' }}
</h1>
<button
class="add-btn"
@click="goToCreateNote"
v-if="currentView === 'list'"
>
<i class="icon-plus"></i>
</button>
</nav>
<!-- 笔记列表视图 -->
<div class="view list-view" v-if="currentView === 'list'">
<div class="empty-state" v-if="notes.length === 0">
<div class="empty-icon">📝</div>
<p class="empty-text">还没有笔记</p>
<p class="empty-subtext">点击右上角 + 创建你的第一条笔记</p>
</div>
<ul class="notes-list" v-else>
<li
class="note-item"
v-for="note in notes"
:key="note.id"
@click="goToNoteDetail(note.id)"
>
<div class="note-info">
<h3 class="note-title">{{ note.title || '无标题' }}</h3>
<p class="note-content">{{ note.content.substring(0, 60) }}{{ note.content.length > 60 ? '...' : '' }}</p>
<time class="note-time">{{ formatDate(note.updatedAt) }}</time>
</div>
<div class="note-actions">
<button
class="action-btn edit-btn"
@click.stop="goToEditNote(note.id)"
aria-label="编辑笔记"
>
<i class="icon-edit"></i>
</button>
<button
class="action-btn delete-btn"
@click.stop="showDeleteDialog(note.id)"
aria-label="删除笔记"
>
<i class="icon-delete"></i>
</button>
</div>
</li>
</ul>
</div>
<!-- 笔记详情视图 -->
<div class="view detail-view" v-if="currentView === 'detail'">
<div class="detail-content" v-if="currentNote">
<h2 class="detail-title">{{ currentNote.title || '无标题' }}</h2>
<time class="detail-time">{{ formatDate(currentNote.updatedAt) }}</time>
<div class="detail-text">{{ currentNote.content }}</div>
</div>
<div class="detail-actions">
<button
class="detail-btn edit-detail-btn"
@click="goToEditNote(currentNote?.id)"
>
<i class="icon-edit"></i> 编辑
</button>
<button
class="detail-btn delete-detail-btn"
@click="showDeleteDialog(currentNote?.id)"
>
<i class="icon-delete"></i> 删除
</button>
</div>
</div>
<!-- 新建/编辑笔记视图 -->
<div class="view editor-view" v-if="currentView === 'create' || currentView === 'edit'">
<div class="editor-form">
<input
type="text"
v-model="noteForm.title"
class="title-input"
placeholder="请输入标题..."
maxlength="50"
>
<textarea
v-model="noteForm.content"
class="content-input"
placeholder="请输入笔记内容..."
rows="15"
></textarea>
</div>
<button
class="save-btn"
@click="saveNote"
:disabled="!noteForm.title && !noteForm.content"
>
保存
</button>
</div>
<!-- 删除确认弹窗 -->
<div class="modal-backdrop" v-if="showDeleteModal">
<div class="delete-modal">
<h3 class="modal-title">确认删除</h3>
<p class="modal-message">你确定要删除这条笔记吗?此操作不可撤销。</p>
<div class="modal-buttons">
<button
class="modal-btn cancel-btn"
@click="showDeleteModal = false"
>
取消
</button>
<button
class="modal-btn confirm-btn"
@click="confirmDelete"
>
删除
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
// 视图状态管理
const currentView = ref('list'); // list, detail, create, edit
const currentNoteId = ref(null);
const showDeleteModal = ref(false);
const noteToDelete = ref(null);
// 笔记数据
const notes = ref([]);
// 表单数据
const noteForm = ref({
title: '',
content: ''
});
// 当前选中的笔记
const currentNote = computed(() => {
return notes.value.find(note => note.id === currentNoteId.value) || null;
});
// 从本地存储加载笔记
const loadNotes = () => {
const savedNotes = localStorage.getItem('mobileNotes');
if (savedNotes) {
notes.value = JSON.parse(savedNotes);
}
};
// 保存笔记到本地存储
const saveNotesToStorage = () => {
localStorage.setItem('mobileNotes', JSON.stringify(notes.value));
};
// 初始化
onMounted(() => {
loadNotes();
});
// 监听笔记变化并保存
watch(notes, () => {
saveNotesToStorage();
}, { deep: true });
// 导航处理
const handleBack = () => {
switch(currentView.value) {
case 'detail':
case 'create':
currentView.value = 'list';
break;
case 'edit':
currentView.value = 'detail';
break;
}
};
// 跳转到笔记详情
const goToNoteDetail = (id) => {
currentNoteId.value = id;
currentView.value = 'detail';
};
// 跳转到新建笔记
const goToCreateNote = () => {
noteForm.value = { title: '', content: '' };
currentView.value = 'create';
};
// 跳转到编辑笔记
const goToEditNote = (id) => {
const note = notes.value.find(n => n.id === id);
if (note) {
noteForm.value = {
title: note.title,
content: note.content
};
currentNoteId.value = id;
currentView.value = 'edit';
}
};
// 保存笔记
const saveNote = () => {
const now = new Date().toISOString();
if (currentView.value === 'create') {
// 新建笔记
const newNote = {
id: Date.now().toString(),
title: noteForm.value.title,
content: noteForm.value.content,
createdAt: now,
updatedAt: now
};
notes.value.unshift(newNote); // 添加到数组开头,最新的在前面
} else {
// 编辑现有笔记
const index = notes.value.findIndex(n => n.id === currentNoteId.value);
if (index !== -1) {
notes.value[index].title = noteForm.value.title;
notes.value[index].content = noteForm.value.content;
notes.value[index].updatedAt = now;
}
}
// 返回列表或详情页
currentView.value = 'list';
};
// 显示删除确认
const showDeleteDialog = (id) => {
noteToDelete.value = id;
showDeleteModal.value = true;
};
// 确认删除
const confirmDelete = () => {
if (noteToDelete.value) {
notes.value = notes.value.filter(note => note.id !== noteToDelete.value);
showDeleteModal.value = false;
currentView.value = 'list';
noteToDelete.value = null;
}
};
// 格式化日期显示
const formatDate = (isoString) => {
const date = new Date(isoString);
return date.toLocaleString('zh-CN', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
</script>
<style scoped>
/* 基础样式 */
.notes-app {
max-width: 500px;
margin: 0 auto;
min-height: 100vh;
background-color: #f9f9f9;
color: #333;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
position: relative;
}
/* 导航栏 */
.app-nav {
display: flex;
align-items: center;
justify-content: center;
height: 56px;
background-color: #2196f3;
color: white;
padding: 0 16px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: sticky;
top: 0;
z-index: 10;
}
.nav-with-back {
justify-content: flex-start;
}
.app-title {
font-size: 18px;
font-weight: 500;
margin: 0;
}
.back-btn, .add-btn {
width: 40px;
height: 40px;
border-radius: 50%;
background: transparent;
border: none;
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 20px;
}
.back-btn {
margin-right: 16px;
}
.add-btn {
position: absolute;
right: 16px;
}
/* 图标样式 */
.icon-plus::before {
content: "+";
font-weight: bold;
}
.icon-arrow-left::before {
content: "←";
font-weight: bold;
}
.icon-edit::before {
content: "✏️";
}
.icon-delete::before {
content: "🗑️";
}
/* 视图容器 */
.view {
padding: 16px;
min-height: calc(100vh - 56px);
box-sizing: border-box;
}
/* 列表视图 */
.notes-list {
list-style: none;
margin: 0;
padding: 0;
}
.note-item {
display: flex;
justify-content: space-between;
align-items: center;
background-color: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
cursor: pointer;
transition: transform 0.1s ease, box-shadow 0.1s ease;
}
.note-item:active {
transform: translateY(1px);
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.note-info {
flex: 1;
overflow: hidden;
padding-right: 12px;
}
.note-title {
margin: 0 0 6px 0;
font-size: 16px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.note-content {
margin: 0 0 8px 0;
font-size: 14px;
color: #666;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
}
.note-time {
font-size: 12px;
color: #999;
}
.note-actions {
display: flex;
gap: 8px;
}
.action-btn {
width: 36px;
height: 36px;
border-radius: 50%;
border: none;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.2s ease;
}
.action-btn:active {
transform: scale(0.95);
}
.edit-btn {
color: #2196f3;
}
.delete-btn {
color: #f44336;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-icon {
font-size: 60px;
margin-bottom: 20px;
}
.empty-text {
font-size: 18px;
margin: 0 0 8px 0;
}
.empty-subtext {
font-size: 14px;
margin: 0;
color: #bbb;
}
/* 详情视图 */
.detail-view {
background-color: white;
padding-bottom: 80px;
}
.detail-content {
margin-bottom: 24px;
}
.detail-title {
margin: 0 0 16px 0;
font-size: 22px;
font-weight: 500;
line-height: 1.3;
}
.detail-time {
display: block;
font-size: 14px;
color: #999;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #eee;
}
.detail-text {
font-size: 16px;
line-height: 1.6;
color: #444;
white-space: pre-line;
}
.detail-actions {
display: flex;
gap: 16px;
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
max-width: calc(100% - 32px);
width: 100%;
}
.detail-btn {
flex: 1;
padding: 14px;
border-radius: 8px;
border: none;
font-size: 16px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
cursor: pointer;
transition: opacity 0.2s ease;
}
.detail-btn:active {
opacity: 0.8;
}
.edit-detail-btn {
background-color: #2196f3;
color: white;
}
.delete-detail-btn {
background-color: #f44336;
color: white;
}
/* 编辑器视图 */
.editor-view {
background-color: white;
padding-bottom: 80px;
}
.editor-form {
margin-bottom: 24px;
}
.title-input {
width: 100%;
font-size: 22px;
font-weight: 500;
padding: 12px 0;
margin-bottom: 16px;
border: none;
border-bottom: 1px solid #eee;
outline: none;
background: transparent;
color: #333;
}
.title-input::placeholder {
color: #ccc;
}
.content-input {
width: 100%;
font-size: 16px;
line-height: 1.6;
border: none;
outline: none;
resize: none;
background: transparent;
color: #444;
min-height: 300px;
}
.content-input::placeholder {
color: #ccc;
}
.save-btn {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: calc(100% - 32px);
max-width: 468px;
padding: 14px;
background-color: #2196f3;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease, opacity 0.2s ease;
}
.save-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.save-btn:not(:disabled):active {
opacity: 0.8;
}
/* 删除确认弹窗 */
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
padding: 0 16px;
}
.delete-modal {
background-color: white;
border-radius: 12px;
width: 100%;
max-width: 300px;
padding: 24px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
.modal-title {
margin: 0 0 12px 0;
font-size: 18px;
font-weight: 500;
text-align: center;
}
.modal-message {
margin: 0 0 24px 0;
font-size: 14px;
color: #666;
text-align: center;
line-height: 1.5;
}
.modal-buttons {
display: flex;
gap: 12px;
}
.modal-btn {
flex: 1;
padding: 10px;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s ease;
border: none;
}
.modal-btn:active {
opacity: 0.8;
}
.cancel-btn {
background-color: #f0f0f0;
color: #333;
}
.confirm-btn {
background-color: #f44336;
color: white;
}
/* 适配小屏幕 */
@media (max-width: 320px) {
.app-title {
font-size: 16px;
}
.note-title {
font-size: 15px;
}
.note-content {
font-size: 13px;
}
.detail-title {
font-size: 20px;
}
}
</style>