保存
This commit is contained in:
658
src/apps/todo/Todo.vue
Normal file
658
src/apps/todo/Todo.vue
Normal 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>
|
||||
Reference in New Issue
Block a user