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>
相关推荐
李纲明14 小时前
微信小程序 getSystemInfo 提示已废弃、屏幕高度算错胶囊还对不齐?getWindowInfo 和 getDeviceInfo 怎么接才稳
javascript·微信小程序·小程序·小程序开发·devdiy
Felven15 小时前
C. Brr Brrr Patapim
c语言·开发语言
FungLeo15 小时前
成为全栈·Vue 管理后台篇·用 fetch 建一层可控的请求内核
前端·javascript·vue.js·http·fetch·formdata·接口封装
用户842981424181016 小时前
ByteJS,让重要的 JavaScript 以字节码运行。
javascript
路溪非溪17 小时前
python标准库使用总结
开发语言·python
FungLeo17 小时前
成为全栈·Vue 管理后台篇·Vue Router如何守住多角色后台边界
前端·javascript·vue.js·rbac·vue router·路由守卫·前端权限
FungLeo18 小时前
成为全栈·Vue管理后台篇·仪表盘不是几张卡片:组合统计接口与角色视图
前端·javascript·vue.js·rbac·仪表盘·管理后台·tanstack query
程序员大阳18 小时前
HTML+CSS+JS对话式实战(7)-学生端活动报名功能开发
javascript·css·html·活动报名
SomeOtherTime18 小时前
Rust 面向对象介绍(AI回答)
开发语言·python·rust
zh_xuan19 小时前
kotlin常见面试题
开发语言·kotlin·面试题