用 Lyt.js 构建 Todo 应用:完整教程
通过一个经典的 Todo 应用,学习 Lyt.js v6.6.0 的核心用法。
最终效果
- 添加 Todo
- 标记完成/未完成
- 删除 Todo
- 筛选(全部/已完成/未完成)
- 统计待办数量
步骤 1:创建项目
bash
# 使用 Lyt.js CLI 创建项目
npm create lyt@latest my-todo-app
cd my-todo-app
npm install
npm run dev
或者手动创建:
bash
mkdir my-todo-app
cd my-todo-app
npm init -y
npm install @lytjs/core @lytjs/plugin-vite vite
# 创建目录结构
mkdir -p src/components
步骤 2:配置 Vite
typescript
// vite.config.ts
import { defineConfig } from 'vite'
import lyt from '@lytjs/plugin-vite'
export default defineConfig({
plugins: [lyt()],
})
json
// package.json
{
"scripts": {
"dev": "vite",
"build": "vite build"
}
}
步骤 3:创建入口文件
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Todo App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
typescript
// src/main.ts
import { createApp } from '@lytjs/core'
import App from './App.lyt'
createApp(App).mount('#app')
步骤 4:定义 Todo 类型
typescript
// src/types.ts
export interface Todo {
id: number
text: string
completed: boolean
}
export type FilterType = 'all' | 'active' | 'completed'
步骤 5:创建 TodoItem 组件
html
<!-- src/components/TodoItem.lyt -->
<script setup>
import { defineProps, defineEmits } from '@lytjs/core'
const props = defineProps<{
todo: Todo
}>()
const emit = defineEmits<{
toggle: [id: number]
delete: [id: number]
}>()
</script>
<template>
<li class="todo-item" :class="{ completed: todo.completed }">
<label>
<input
type="checkbox"
:checked="todo.completed"
on:change="emit('toggle', todo.id)"
/>
<span>{{ todo.text }}</span>
</label>
<button class="delete-btn" on:click="emit('delete', todo.id)">×</button>
</li>
</template>
<style>
.todo-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
border-bottom: 1px solid #eee;
}
.todo-item.completed span {
text-decoration: line-through;
color: #999;
}
.delete-btn {
background: none;
border: none;
color: #ff4d4f;
font-size: 20px;
cursor: pointer;
}
</style>
步骤 6:创建 TodoList 组件
html
<!-- src/components/TodoList.lyt -->
<script setup>
import { defineProps, defineEmits } from '@lytjs/core'
import TodoItem from './TodoItem.lyt'
const props = defineProps<{
todos: Todo[]
}>()
const emit = defineEmits<{
toggle: [id: number]
delete: [id: number]
}>()
</script>
<template>
<ul class="todo-list">
<TodoItem
each="todo in todos"
:todo="todo"
on:toggle="emit('toggle', $event)"
on:delete="emit('delete', $event)"
/>
</ul>
</template>
<style>
.todo-list {
list-style: none;
padding: 0;
margin: 0;
}
</style>
步骤 7:创建 TodoInput 组件
html
<!-- src/components/TodoInput.lyt -->
<script setup>
import { defineProps, defineEmits } from '@lytjs/core'
const emit = defineEmits<{
add: [text: string]
}>()
let inputText = ''
function handleSubmit() {
const text = inputText.trim()
if (text) {
emit('add', text)
inputText = ''
}
}
</script>
<template>
<form class="todo-input" on:submit.prevent="handleSubmit">
<input
v-model="inputText"
type="text"
placeholder="添加新待办..."
/>
<button type="submit">添加</button>
</form>
</template>
<style>
.todo-input {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.todo-input input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.todo-input button {
padding: 10px 20px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
步骤 8:创建 TodoFilter 组件
html
<!-- src/components/TodoFilter.lyt -->
<script setup>
import { defineProps, defineEmits } from '@lytjs/core'
import type { FilterType } from '../types'
const props = defineProps<{
filter: FilterType
counts: { all: number; active: number; completed: number }
}>()
const emit = defineEmits<{
'update:filter': [filter: FilterType]
}>()
const filters: FilterType[] = ['all', 'active', 'completed']
const labels = { all: '全部', active: '待完成', completed: '已完成' }
</script>
<template>
<div class="todo-filter">
<span class="count">{{ counts.active }} 项待完成</span>
<div class="filter-buttons">
<button
each="f in filters"
:class="{ active: filter === f }"
on:click="emit('update:filter', f)"
>
{{ labels[f] }}
</button>
</div>
</div>
</template>
<style>
.todo-filter {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
color: #666;
}
.filter-buttons {
display: flex;
gap: 8px;
}
.filter-buttons button {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
}
.filter-buttons button.active {
background: #1890ff;
color: white;
border-color: #1890ff;
}
</style>
步骤 9:创建主 App 组件
html
<!-- src/App.lyt -->
<script setup>
import { ref, computed } from '@lytjs/core'
import type { Todo, FilterType } from './types'
import TodoInput from './components/TodoInput.lyt'
import TodoList from './components/TodoList.lyt'
import TodoFilter from './components/TodoFilter.lyt'
let todos = ref<Todo[]>([])
let nextId = 1
let filter = ref<FilterType>('all')
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(t => !t.completed)
case 'completed':
return todos.value.filter(t => t.completed)
default:
return todos.value
}
})
const counts = computed(() => ({
all: todos.value.length,
active: todos.value.filter(t => !t.completed).length,
completed: todos.value.filter(t => t.completed).length,
}))
function addTodo(text: string) {
todos.value.push({
id: nextId++,
text,
completed: false,
})
}
function toggleTodo(id: number) {
const todo = todos.value.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
function deleteTodo(id: number) {
const index = todos.value.findIndex(t => t.id === id)
if (index > -1) {
todos.value.splice(index, 1)
}
}
</script>
<template>
<div class="app">
<h1>我的待办</h1>
<TodoInput on:add="addTodo" />
<TodoList
if="filteredTodos.length > 0"
:todos="filteredTodos"
on:toggle="toggleTodo"
on:delete="deleteTodo"
/>
<p else class="empty">暂无待办事项</p>
<TodoFilter
if="counts.all > 0"
:filter="filter"
:counts="counts"
on:update:filter="filter = $event"
/>
</div>
</template>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
}
.app {
max-width: 500px;
margin: 50px auto;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 20px;
}
.empty {
text-align: center;
color: #999;
padding: 20px;
}
</style>
步骤 10:运行项目
bash
npm run dev
访问 http://localhost:5173,你应该能看到完整的 Todo 应用。
完整目录结构
css
my-todo-app/
├── index.html
├── package.json
├── vite.config.ts
└── src/
├── main.ts
├── App.lyt
├── types.ts
└── components/
├── TodoItem.lyt
├── TodoList.lyt
├── TodoInput.lyt
└── TodoFilter.lyt
扩展练习
- 添加编辑功能:双击编辑 Todo 文字
- 持久化存储:使用 localStorage 保存数据
- 添加动画 :使用
<Transition>添加过渡效果 - 统计面板:显示完成百分比
关键语法回顾
| 功能 | Lyt.js 语法 |
|---|---|
| 条件渲染 | if="condition" |
| 列表渲染 | each="item in list" |
| 双向绑定 | v-model="value" (模板中) / model="value" (Lyt.js) |
| 事件绑定 | on:click="handler" |
| Props | defineProps<T>() |
| Emits | defineEmits<T>() |
| 响应式数据 | ref() / reactive() |
| 计算属性 | computed() |