vue3 实现记事本手机版01

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>
相关推荐
江城开朗的豌豆3 小时前
玩转小程序生命周期:从入门到上瘾
前端·javascript·微信小程序
Cx330❀3 小时前
《C++ 继承》三大面向对象编程——继承:派生类构造、多继承、菱形虚拟继承概要
开发语言·c++
晨陌y3 小时前
从 “不会” 到 “会写”:Rust 入门基础实战,用一个小项目串完所有核心基础
开发语言·后端·rust
筱砚.3 小时前
【STL——set与multiset容器】
开发语言·c++·stl
Fanfffff7203 小时前
从TSX到JS:深入解析npm run build背后的完整构建流程
开发语言·javascript·npm
im_AMBER3 小时前
React 10
前端·javascript·笔记·学习·react.js·前端框架
Elias不吃糖3 小时前
C++ 中的浅拷贝与深拷贝:概念、规则、示例与最佳实践(笔记)
开发语言·c++·浅拷贝·深拷贝
LEEBELOVED3 小时前
R语言高效数据处理-3个自定义函数笔记
开发语言·笔记·r语言
朝新_3 小时前
【SpringMVC】SpringMVC 请求与响应全解析:从 Cookie/Session 到状态码、Header 配置
java·开发语言·笔记·springmvc·javaee