Files
schedule-planner/frontend/src/views/TasksView.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

210 lines
7.4 KiB
Vue

<template>
<div class="tasks-layout">
<!-- Sub tabs -->
<div class="sub-tabs">
<button class="sub-tab" :class="{ active: subTab === 'todo' }" @click="subTab = 'todo'">待办</button>
<button class="sub-tab" :class="{ active: subTab === 'goals' }" @click="subTab = 'goals'">目标</button>
<button class="sub-tab" :class="{ active: subTab === 'checklists' }" @click="subTab = 'checklists'">清单</button>
</div>
<!-- 待办 -->
<div v-if="subTab === 'todo'" class="todo-section">
<div class="toolbar">
<input class="search-input" v-model="todoSearch" placeholder="搜索待办…">
<label class="toggle-label">
<input type="checkbox" v-model="showDone">
<span>显示已完成</span>
</label>
</div>
<!-- 收集箱 -->
<div class="inbox-card">
<div class="capture-row">
<textarea
class="capture-input"
v-model="inboxText"
placeholder="脑子里有什么事?先丢进来…"
rows="1"
@keydown.enter.exact.prevent="addInbox"
></textarea>
<button class="capture-btn" @click="addInbox">+</button>
</div>
<div v-for="item in store.inbox" :key="item.id" class="inbox-item">
<span>{{ item.text }}</span>
<div class="inbox-item-actions">
<button @click="moveToQuadrant(item, 'q1')" title="紧急重要">🔴</button>
<button @click="moveToQuadrant(item, 'q2')" title="重要">🟡</button>
<button @click="moveToQuadrant(item, 'q3')" title="紧急">🔵</button>
<button @click="moveToQuadrant(item, 'q4')" title="其他"></button>
<button @click="store.deleteInbox(item.id)"></button>
</div>
</div>
</div>
<!-- 四象限 -->
<div class="quadrant-grid">
<div v-for="q in quadrants" :key="q.key" class="quadrant" :class="q.class">
<div class="quadrant-title">{{ q.title }}</div>
<div class="quadrant-desc">{{ q.desc }}</div>
<div v-for="todo in getQuadrantTodos(q.key)" :key="todo.id" class="todo-item">
<input type="checkbox" :checked="todo.done" @change="toggleTodo(todo)">
<span :class="{ done: todo.done }">{{ todo.text }}</span>
<button class="remove-btn" @click="store.deleteTodo(todo.id)"></button>
</div>
<div class="add-todo-row">
<input
:placeholder="'添加到' + q.title + '…'"
@keydown.enter.prevent="addTodoToQuadrant($event, q.key)"
>
</div>
</div>
</div>
</div>
<!-- 目标 -->
<div v-if="subTab === 'goals'" class="goals-section">
<div class="section-header">
<h3>我的目标</h3>
<button class="btn btn-accent" @click="openGoalForm()">+ 新目标</button>
</div>
<div v-for="goal in store.goals" :key="goal.id" class="goal-card">
<div class="goal-header">
<strong>{{ goal.name }}</strong>
<span v-if="goal.month" class="goal-month">截止 {{ goal.month }}</span>
<button class="remove-btn" @click="store.deleteGoal(goal.id)"></button>
</div>
</div>
<div v-if="showGoalForm" class="edit-form">
<input v-model="goalName" placeholder="目标名称">
<input v-model="goalMonth" type="month">
<div class="edit-actions">
<button class="btn btn-close" @click="showGoalForm = false">取消</button>
<button class="btn btn-accent" @click="saveGoal">保存</button>
</div>
</div>
</div>
<!-- 清单 -->
<div v-if="subTab === 'checklists'" class="checklists-section">
<div class="section-header">
<h3>我的清单</h3>
<button class="btn btn-accent" @click="addChecklist">+ 新清单</button>
</div>
<div v-for="cl in store.checklists" :key="cl.id" class="checklist-card">
<div class="checklist-header">
<input
class="checklist-title-input"
:value="cl.title"
@blur="updateChecklistTitle(cl, $event.target.value)"
>
<button class="remove-btn" @click="store.deleteChecklist(cl.id)"></button>
</div>
<div v-for="(item, idx) in parseItems(cl.items)" :key="idx" class="checklist-item">
<input type="checkbox" :checked="item.done" @change="toggleChecklistItem(cl, idx)">
<span :class="{ done: item.done }">{{ item.text }}</span>
</div>
<div class="add-todo-row">
<input placeholder="添加项目…" @keydown.enter.prevent="addChecklistItem(cl, $event)">
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { usePlannerStore } from '../stores/planner'
const store = usePlannerStore()
const subTab = ref('todo')
const todoSearch = ref('')
const showDone = ref(false)
const inboxText = ref('')
const showGoalForm = ref(false)
const goalName = ref('')
const goalMonth = ref('')
const quadrants = [
{ key: 'q1', title: '紧急且重要', desc: '立即处理', class: 'q-urgent-important' },
{ key: 'q2', title: '重要不紧急', desc: '计划安排', class: 'q-important' },
{ key: 'q3', title: '紧急不重要', desc: '委派他人', class: 'q-urgent' },
{ key: 'q4', title: '不紧急不重要', desc: '减少或消除', class: 'q-neither' },
]
function uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 7) }
function getQuadrantTodos(q) {
let list = store.todos.filter(t => t.quadrant === q)
if (!showDone.value) list = list.filter(t => !t.done)
if (todoSearch.value) {
const s = todoSearch.value.toLowerCase()
list = list.filter(t => t.text.toLowerCase().includes(s))
}
return list
}
async function addInbox() {
const text = inboxText.value.trim()
if (!text) return
await store.addInbox({ id: uid(), text })
inboxText.value = ''
}
async function moveToQuadrant(item, quadrant) {
await store.addTodo({ id: uid(), text: item.text, quadrant, done: 0 })
await store.deleteInbox(item.id)
}
async function toggleTodo(todo) {
await store.updateTodo({ ...todo, done: todo.done ? 0 : 1 })
}
async function addTodoToQuadrant(e, quadrant) {
const text = e.target.value.trim()
if (!text) return
await store.addTodo({ id: uid(), text, quadrant, done: 0 })
e.target.value = ''
}
function openGoalForm() {
showGoalForm.value = true
goalName.value = ''
goalMonth.value = ''
}
async function saveGoal() {
if (!goalName.value.trim()) return
await store.addGoal({ id: uid(), name: goalName.value.trim(), month: goalMonth.value, checks: '{}' })
showGoalForm.value = false
}
function parseItems(items) {
try { return JSON.parse(items) } catch { return [] }
}
async function addChecklist() {
await store.addChecklist({ id: uid(), title: '新清单', items: '[]', archived: 0 })
}
async function updateChecklistTitle(cl, title) {
if (title !== cl.title) {
await store.updateChecklist({ ...cl, title })
}
}
async function toggleChecklistItem(cl, idx) {
const items = parseItems(cl.items)
items[idx].done = !items[idx].done
await store.updateChecklist({ ...cl, items: JSON.stringify(items) })
}
async function addChecklistItem(cl, e) {
const text = e.target.value.trim()
if (!text) return
const items = parseItems(cl.items)
items.push({ text, done: false })
await store.updateChecklist({ ...cl, items: JSON.stringify(items) })
e.target.value = ''
}
</script>