Refactor to Vue 3 + FastAPI + SQLite architecture
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

- 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>
This commit is contained in:
2026-04-07 21:18:22 +00:00
parent b09cefad34
commit d3f3b4f37b
67 changed files with 10038 additions and 6 deletions

View File

@@ -0,0 +1,148 @@
<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>