Files
schedule-planner/frontend/src/views/NotesView.vue
Hera Zhao d3f3b4f37b
Some checks failed
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Successful in 3s
PR Preview / teardown-preview (pull_request) Has been skipped
Test / e2e-test (push) Failing after 55s
PR Preview / deploy-preview (pull_request) Failing after 40s
Refactor to Vue 3 + FastAPI + SQLite architecture
- Backend: FastAPI + SQLite (WAL mode), 22 tables, ~40 API endpoints
- Frontend: Vue 3 + Vite + Pinia + Vue Router, 8 views, 3 stores
- Database: migrate from JSON file to SQLite with proper schema
- Dockerfile: multi-stage build (node + python)
- Deploy: K8s manifests (namespace, deployment, service, ingress, pvc, backup)
- CI/CD: Gitea Actions (test, deploy, PR preview at pr-$id.planner.oci.euphon.net)
- Tests: 20 Cypress E2E test files, 196 test cases, ~85% coverage
- Doc: test-coverage.md with full feature coverage report

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 21:18:22 +00:00

149 lines
4.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="notes-layout">
<!-- 快速输入 -->
<div class="capture-card">
<div class="capture-row">
<textarea
class="capture-input"
v-model="newText"
placeholder="想到什么,写下来…"
rows="1"
@input="autoResize"
@keydown.enter.exact.prevent="saveNote"
></textarea>
<button class="capture-btn" @click="saveNote"></button>
</div>
<div class="tag-btns">
<button
v-for="t in tagOptions"
:key="t.tag"
class="tag-btn"
:class="{ active: selectedTag === t.tag }"
@click="selectedTag = t.tag"
:title="t.tag"
>{{ t.icon }}</button>
</div>
</div>
<!-- 筛选 -->
<div class="toolbar">
<input class="search-input" v-model="searchQuery" placeholder="搜索…">
<div class="filter-row">
<button
class="filter-btn"
:class="{ active: filterTag === 'all' }"
@click="filterTag = 'all'"
>全部</button>
<button
v-for="t in tagOptions"
:key="t.tag"
class="filter-btn"
:class="{ active: filterTag === t.tag }"
@click="filterTag = t.tag"
>{{ t.icon }} {{ t.tag }}</button>
</div>
</div>
<!-- 列表 -->
<div v-if="filtered.length === 0" class="empty-hint">
还没有记录在上方输入框快速记录吧
</div>
<div v-for="note in filtered" :key="note.id" class="note-card">
<div class="note-header">
<span class="note-tag" :style="{ background: tagColor(note.tag) }">{{ tagIcon(note.tag) }} {{ note.tag }}</span>
<span class="note-time">{{ formatTime(note.created_at) }}</span>
</div>
<div v-if="editingId === note.id" class="note-edit">
<textarea v-model="editText" class="edit-textarea" rows="3"></textarea>
<div class="edit-actions">
<button class="btn btn-close" @click="editingId = null">取消</button>
<button class="btn btn-accent" @click="saveEdit(note)">保存</button>
</div>
</div>
<div v-else class="note-text" @click="startEdit(note)">{{ note.text }}</div>
<div class="note-actions">
<button class="note-action-btn" @click="startEdit(note)">编辑</button>
<button class="note-action-btn danger" @click="removeNote(note.id)">删除</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { usePlannerStore } from '../stores/planner'
const store = usePlannerStore()
const tagOptions = [
{ tag: '灵感', icon: '💡' },
{ tag: '待办', icon: '✅' },
{ tag: '提醒', icon: '⏰' },
{ tag: '读书', icon: '📖' },
{ tag: '睡眠', icon: '🌙' },
{ tag: '健康', icon: '💊' },
{ tag: '健身', icon: '💪' },
{ tag: '音乐', icon: '🎵' },
]
const TAG_COLORS = {
'灵感': '#fff3cd', '待办': '#d1ecf1', '提醒': '#f8d7da', '读书': '#d4edda',
'睡眠': '#e2d9f3', '健康': '#fce4ec', '健身': '#e8f5e9', '音乐': '#fff8e1',
}
const TAG_ICONS = Object.fromEntries(tagOptions.map(t => [t.tag, t.icon]))
const newText = ref('')
const selectedTag = ref('灵感')
const searchQuery = ref('')
const filterTag = ref('all')
const editingId = ref(null)
const editText = ref('')
function tagColor(tag) { return TAG_COLORS[tag] || '#f5f5f5' }
function tagIcon(tag) { return TAG_ICONS[tag] || '📝' }
const filtered = computed(() => {
let list = store.notes
if (filterTag.value !== 'all') list = list.filter(n => n.tag === filterTag.value)
if (searchQuery.value) {
const q = searchQuery.value.toLowerCase()
list = list.filter(n => n.text.toLowerCase().includes(q))
}
return list
})
function uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 7) }
function autoResize(e) {
e.target.style.height = 'auto'
e.target.style.height = Math.min(e.target.scrollHeight, 160) + 'px'
}
async function saveNote() {
const text = newText.value.trim()
if (!text) return
await store.addNote({ id: uid(), text, tag: selectedTag.value, created_at: new Date().toISOString() })
newText.value = ''
}
function startEdit(note) {
editingId.value = note.id
editText.value = note.text
}
async function saveEdit(note) {
await store.updateNote({ ...note, text: editText.value })
editingId.value = null
}
async function removeNote(id) {
await store.deleteNote(id)
}
function formatTime(ts) {
if (!ts) return ''
const d = new Date(ts)
return `${d.getMonth() + 1}/${d.getDate()} ${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`
}
</script>