This commit is contained in:
2025-09-24 16:43:10 +08:00
parent 12f46e6f8e
commit 9dbc054483
130 changed files with 16474 additions and 4660 deletions

658
src/apps/todo/Todo.vue Normal file
View File

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