Files
vue-desktop/src/apps/todo/Todo.vue
2025-09-24 16:43:10 +08:00

658 lines
14 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<BuiltInApp app-id="todo" title="待办事项">
<div class="todo-app">
<div class="header">
<h1>📝 待办事项</h1>
<div class="stats">
总计: <span class="count">{{ todos.length }}</span> |
已完成: <span class="count">{{ completedCount }}</span> |
待办: <span class="count">{{ pendingCount }}</span>
</div>
</div>
<div class="add-todo">
<div class="input-group">
<input
v-model="newTodoText"
type="text"
class="todo-input"
placeholder="添加新的待办事项..."
maxlength="200"
@keypress.enter="addTodo"
>
<button @click="addTodo" class="add-btn" :disabled="!newTodoText.trim()">
添加
</button>
</div>
</div>
<div class="filters">
<button
v-for="filter in filters"
:key="filter.key"
class="filter-btn"
:class="{ active: currentFilter === filter.key }"
@click="setFilter(filter.key)"
>
{{ filter.label }}
</button>
</div>
<div class="todo-list" v-if="filteredTodos.length > 0">
<TransitionGroup name="todo-list" tag="div">
<div
v-for="todo in filteredTodos"
:key="todo.id"
class="todo-item"
:class="{ 'completed': todo.completed }"
>
<input
type="checkbox"
class="todo-checkbox"
:checked="todo.completed"
@change="toggleTodo(todo.id)"
>
<div class="todo-content" v-if="!todo.editing">
<span class="todo-text" :class="{ 'completed': todo.completed }">
{{ todo.text }}
</span>
<span class="todo-date">{{ formatDate(todo.createdAt) }}</span>
</div>
<div class="todo-edit" v-else>
<input
v-model="todo.editText"
type="text"
class="edit-input"
@keypress.enter="saveEdit(todo)"
@keyup.esc="cancelEdit(todo)"
@blur="saveEdit(todo)"
ref="editInputs"
>
</div>
<div class="todo-actions">
<button
v-if="!todo.editing"
@click="startEdit(todo)"
class="edit-btn"
title="编辑"
>
</button>
<button
@click="deleteTodo(todo.id)"
class="delete-btn"
title="删除"
>
🗑
</button>
</div>
</div>
</TransitionGroup>
</div>
<div v-else class="empty-state">
<div class="empty-icon">📋</div>
<h3>{{ getEmptyMessage() }}</h3>
<p>{{ getEmptyDescription() }}</p>
</div>
<!-- 批量操作 -->
<div class="bulk-actions" v-if="todos.length > 0">
<button @click="markAllCompleted" class="bulk-btn">
全部完成
</button>
<button @click="clearCompleted" class="bulk-btn" v-if="completedCount > 0">
清除已完成 ({{ completedCount }})
</button>
<button @click="clearAll" class="bulk-btn bulk-danger">
清空全部
</button>
</div>
</div>
</BuiltInApp>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue'
import BuiltInApp from '../components/BuiltInApp.vue'
interface Todo {
id: string
text: string
completed: boolean
createdAt: Date
completedAt?: Date
editing?: boolean
editText?: string
}
type FilterType = 'all' | 'pending' | 'completed'
const newTodoText = ref('')
const todos = ref<Todo[]>([])
const currentFilter = ref<FilterType>('all')
const editInputs = ref<HTMLInputElement[]>([])
const filters = [
{ key: 'all' as FilterType, label: '全部' },
{ key: 'pending' as FilterType, label: '待办' },
{ key: 'completed' as FilterType, label: '已完成' }
]
// 计算属性
const completedCount = computed(() =>
todos.value.filter(todo => todo.completed).length
)
const pendingCount = computed(() =>
todos.value.filter(todo => !todo.completed).length
)
const filteredTodos = computed(() => {
switch (currentFilter.value) {
case 'completed':
return todos.value.filter(todo => todo.completed)
case 'pending':
return todos.value.filter(todo => !todo.completed)
default:
return todos.value
}
})
// 方法
const addTodo = async () => {
const text = newTodoText.value.trim()
if (!text) return
const todo: Todo = {
id: Date.now().toString(),
text,
completed: false,
createdAt: new Date()
}
todos.value.unshift(todo)
newTodoText.value = ''
await saveTodos()
showNotification('✅ 待办事项已添加', `"${text}" 已添加到您的待办列表`)
}
const toggleTodo = async (id: string) => {
const todo = todos.value.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
todo.completedAt = todo.completed ? new Date() : undefined
await saveTodos()
if (todo.completed) {
showNotification('🎉 任务完成', `"${todo.text}" 已完成!`)
}
}
}
const startEdit = (todo: Todo) => {
todo.editing = true
todo.editText = todo.text
nextTick(() => {
const input = editInputs.value?.find(input =>
input && input.closest('.todo-item')?.getAttribute('data-id') === todo.id
)
input?.focus()
input?.select()
})
}
const saveEdit = async (todo: Todo) => {
if (todo.editText && todo.editText.trim() !== todo.text) {
todo.text = todo.editText.trim()
await saveTodos()
}
todo.editing = false
todo.editText = undefined
}
const cancelEdit = (todo: Todo) => {
todo.editing = false
todo.editText = undefined
}
const deleteTodo = async (id: string) => {
if (confirm('确定要删除这个待办事项吗?')) {
todos.value = todos.value.filter(t => t.id !== id)
await saveTodos()
}
}
const setFilter = (filter: FilterType) => {
currentFilter.value = filter
}
const markAllCompleted = async () => {
todos.value.forEach(todo => {
if (!todo.completed) {
todo.completed = true
todo.completedAt = new Date()
}
})
await saveTodos()
showNotification('✅ 全部完成', '所有待办事项已标记为完成')
}
const clearCompleted = async () => {
if (confirm(`确定要删除 ${completedCount.value} 个已完成的待办事项吗?`)) {
todos.value = todos.value.filter(todo => !todo.completed)
await saveTodos()
showNotification('🗑️ 清理完成', '已清除所有已完成的待办事项')
}
}
const clearAll = async () => {
if (confirm('确定要清空所有待办事项吗?此操作不可恢复。')) {
todos.value = []
await saveTodos()
showNotification('🗑️ 清空完成', '所有待办事项已清空')
}
}
const loadTodos = async () => {
try {
if (window.SystemSDK && window.SystemSDK.initialized) {
const result = await window.SystemSDK.storage.get('todos')
if (result.success && result.data) {
const savedTodos = JSON.parse(result.data)
todos.value = savedTodos.map((todo: any) => ({
...todo,
createdAt: new Date(todo.createdAt),
completedAt: todo.completedAt ? new Date(todo.completedAt) : undefined
}))
}
}
} catch (error) {
console.error('加载待办事项失败:', error)
}
}
const saveTodos = async () => {
try {
if (window.SystemSDK && window.SystemSDK.initialized) {
await window.SystemSDK.storage.set('todos', JSON.stringify(todos.value))
}
} catch (error) {
console.error('保存待办事项失败:', error)
}
}
const showNotification = async (title: string, message: string) => {
try {
if (window.SystemSDK?.ui?.showNotification) {
await window.SystemSDK.ui.showNotification({
title,
body: message,
duration: 3000
})
}
} catch (error) {
console.error('显示通知失败:', error)
}
}
const formatDate = (date: Date) => {
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / (1000 * 60))
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
if (diffMins < 1) return '刚刚'
if (diffMins < 60) return `${diffMins}分钟前`
if (diffHours < 24) return `${diffHours}小时前`
if (diffDays < 7) return `${diffDays}天前`
return date.toLocaleDateString('zh-CN')
}
const getEmptyMessage = () => {
switch (currentFilter.value) {
case 'completed':
return '还没有已完成的任务'
case 'pending':
return '没有待办事项'
default:
return '还没有待办事项'
}
}
const getEmptyDescription = () => {
switch (currentFilter.value) {
case 'completed':
return '完成一些任务后,它们会出现在这里'
case 'pending':
return '所有任务都已完成,真棒!'
default:
return '添加一个新的待办事项开始管理您的任务吧!'
}
}
onMounted(async () => {
await loadTodos()
})
</script>
<style scoped>
.todo-app {
height: 100%;
display: flex;
flex-direction: column;
background: #f8f9fa;
}
.header {
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-align: center;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
font-weight: 600;
}
.stats {
font-size: 14px;
opacity: 0.9;
}
.count {
font-weight: 600;
}
.add-todo {
padding: 20px;
background: white;
border-bottom: 1px solid #e9ecef;
}
.input-group {
display: flex;
gap: 10px;
}
.todo-input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 16px;
outline: none;
transition: border-color 0.3s;
}
.todo-input:focus {
border-color: #667eea;
}
.add-btn {
padding: 12px 24px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
font-weight: 500;
}
.add-btn:hover:not(:disabled) {
background: #5a67d8;
}
.add-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.filters {
padding: 0 20px 20px;
display: flex;
gap: 10px;
justify-content: center;
background: white;
}
.filter-btn {
padding: 8px 16px;
border: 1px solid #e9ecef;
background: white;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
}
.filter-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.filter-btn:hover:not(.active) {
background: #f8f9fa;
}
.todo-list {
flex: 1;
overflow-y: auto;
padding: 0 20px;
background: white;
}
.todo-item {
display: flex;
align-items: center;
padding: 16px;
margin-bottom: 10px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: all 0.3s ease;
border-left: 4px solid #667eea;
}
.todo-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.todo-item.completed {
opacity: 0.7;
border-left-color: #28a745;
}
.todo-checkbox {
margin-right: 12px;
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #28a745;
}
.todo-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.todo-text {
font-size: 16px;
transition: color 0.3s;
line-height: 1.4;
}
.todo-text.completed {
text-decoration: line-through;
color: #6c757d;
}
.todo-date {
font-size: 12px;
color: #6c757d;
}
.todo-edit {
flex: 1;
margin-right: 12px;
}
.edit-input {
width: 100%;
padding: 8px 12px;
border: 2px solid #667eea;
border-radius: 4px;
font-size: 16px;
outline: none;
}
.todo-actions {
display: flex;
gap: 8px;
}
.edit-btn,
.delete-btn {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
opacity: 0.7;
transition: opacity 0.2s;
padding: 4px;
}
.edit-btn:hover,
.delete-btn:hover {
opacity: 1;
}
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: #6c757d;
background: white;
padding: 40px;
}
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
}
.empty-state h3 {
margin-bottom: 8px;
color: #495057;
}
.bulk-actions {
padding: 20px;
background: white;
border-top: 1px solid #e9ecef;
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}
.bulk-btn {
padding: 8px 16px;
border: 1px solid #dee2e6;
background: white;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.bulk-btn:hover {
background: #f8f9fa;
}
.bulk-danger {
color: #dc3545;
border-color: #dc3545;
}
.bulk-danger:hover {
background: #dc3545;
color: white;
}
/* 动画 */
.todo-list-enter-active,
.todo-list-leave-active {
transition: all 0.3s ease;
}
.todo-list-enter-from {
opacity: 0;
transform: translateY(20px);
}
.todo-list-leave-to {
opacity: 0;
transform: translateX(-100%);
}
.todo-list-move {
transition: transform 0.3s ease;
}
/* 响应式设计 */
@media (max-width: 768px) {
.header {
padding: 15px;
}
.header h1 {
font-size: 20px;
}
.add-todo {
padding: 15px;
}
.input-group {
flex-direction: column;
}
.filters {
padding: 0 15px 15px;
flex-wrap: wrap;
}
.todo-list {
padding: 0 15px;
}
.todo-item {
padding: 12px;
}
.bulk-actions {
padding: 15px;
}
}
</style>