Some checks failed
- 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>
210 lines
7.4 KiB
Vue
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>
|