create-rue 0.3.12 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,7 @@
1
- import { computed, type FC, ref, useState, watchEffect } from '@rue-js/rue'
1
+ import { computed, type FC, ref, useState, watch } from '@rue-js/rue'
2
2
  import { RouterLink } from '@rue-js/router'
3
3
 
4
4
  const todoStorageKey = 'rue.base.todos'
5
- const minuteInMs = 60 * 1000
6
- const hourInMs = 60 * minuteInMs
7
- const dayInMs = 24 * hourInMs
8
5
 
9
6
  type TodoStatus = 'todo' | 'doing' | 'done'
10
7
  type TodoFilter = 'all' | 'todo' | 'doing' | 'done' | 'archived'
@@ -15,13 +12,6 @@ type TodoItem = {
15
12
  archived: boolean
16
13
  status: TodoStatus
17
14
  createdAt: string
18
- createdOrder: number
19
- }
20
-
21
- type PersistedTodoState = {
22
- todos: TodoItem[]
23
- search: string
24
- activeFilter: TodoFilter
25
15
  }
26
16
 
27
17
  const filterOptions: Array<{ key: TodoFilter; label: string }> = [
@@ -32,10 +22,10 @@ const filterOptions: Array<{ key: TodoFilter; label: string }> = [
32
22
  { key: 'archived', label: '已归档' },
33
23
  ]
34
24
 
35
- const statusOptions: Array<{ key: TodoStatus; label: string; actionLabel: string }> = [
36
- { key: 'todo', label: '待开始', actionLabel: '设为待开始' },
37
- { key: 'doing', label: '进行中', actionLabel: '设为进行中' },
38
- { key: 'done', label: '已完成', actionLabel: '设为已完成' },
25
+ const statusOptions: Array<{ key: TodoStatus; label: string }> = [
26
+ { key: 'todo', label: '待开始' },
27
+ { key: 'doing', label: '进行中' },
28
+ { key: 'done', label: '已完成' },
39
29
  ]
40
30
 
41
31
  const initialTodos: TodoItem[] = [
@@ -44,281 +34,59 @@ const initialTodos: TodoItem[] = [
44
34
  title: '补充报表首页字段与展示规则',
45
35
  status: 'doing',
46
36
  archived: false,
47
- createdAt: new Date(Date.now() - 110 * minuteInMs).toISOString(),
48
- createdOrder: 4,
37
+ createdAt: '今天 09:30',
49
38
  },
50
39
  {
51
40
  id: 2,
52
41
  title: '接入真实接口替换示例数据',
53
42
  status: 'todo',
54
43
  archived: false,
55
- createdAt: new Date(Date.now() - 65 * minuteInMs).toISOString(),
56
- createdOrder: 3,
44
+ createdAt: '今天 10:05',
57
45
  },
58
46
  {
59
47
  id: 3,
60
48
  title: '复查主题切换和头部隐藏体验',
61
49
  status: 'done',
62
50
  archived: false,
63
- createdAt: new Date(Date.now() - 20 * hourInMs).toISOString(),
64
- createdOrder: 2,
51
+ createdAt: '昨天 18:20',
65
52
  },
66
53
  {
67
54
  id: 4,
68
55
  title: '归档旧版原型页面',
69
56
  status: 'done',
70
57
  archived: true,
71
- createdAt: new Date(Date.now() - 30 * hourInMs).toISOString(),
72
- createdOrder: 1,
58
+ createdAt: '昨天 14:05',
73
59
  },
74
60
  ]
75
61
 
76
- const padDatePart = (value: number) => String(value).padStart(2, '0')
77
-
78
- const formatCalendarDateTime = (value: Date, includeYear = true) => {
79
- const month = padDatePart(value.getMonth() + 1)
80
- const day = padDatePart(value.getDate())
81
- const hours = padDatePart(value.getHours())
82
- const minutes = padDatePart(value.getMinutes())
83
-
84
- if (includeYear) {
85
- return `${value.getFullYear()}-${month}-${day} ${hours}:${minutes}`
86
- }
87
-
88
- return `${month}-${day} ${hours}:${minutes}`
89
- }
90
-
91
- const getNextTodoId = (todos: TodoItem[]) =>
92
- todos.reduce((maxId, item) => Math.max(maxId, item.id), 0) + 1
93
-
94
- const getNextCreatedOrder = (todos: TodoItem[]) =>
95
- todos.reduce((maxOrder, item) => Math.max(maxOrder, item.createdOrder), 0) + 1
96
-
97
- const isSameCalendarDay = (left: Date, right: Date) =>
98
- left.getFullYear() === right.getFullYear() &&
99
- left.getMonth() === right.getMonth() &&
100
- left.getDate() === right.getDate()
101
-
102
- const isTodoStatus = (value: unknown): value is TodoStatus =>
103
- value === 'todo' || value === 'doing' || value === 'done'
104
-
105
- const isTodoFilter = (value: unknown): value is TodoFilter =>
106
- value === 'all' ||
107
- value === 'todo' ||
108
- value === 'doing' ||
109
- value === 'done' ||
110
- value === 'archived'
111
-
112
- const parseCreatedAtValue = (value: string, now = new Date()) => {
113
- const directDate = new Date(value)
114
- if (!Number.isNaN(directDate.getTime())) {
115
- return directDate
116
- }
117
-
118
- if (value === '刚刚') {
119
- return new Date(now.getTime() - 30 * 1000)
120
- }
121
-
122
- const minutesAgoMatch = /^(\d+)\s*分钟前$/.exec(value)
123
- if (minutesAgoMatch) {
124
- return new Date(now.getTime() - Number(minutesAgoMatch[1]) * minuteInMs)
125
- }
126
-
127
- const hoursAgoMatch = /^(\d+)\s*小时前$/.exec(value)
128
- if (hoursAgoMatch) {
129
- return new Date(now.getTime() - Number(hoursAgoMatch[1]) * hourInMs)
130
- }
131
-
132
- const todayMatch = /^今天\s+(\d{1,2}):(\d{2})$/.exec(value)
133
- if (todayMatch) {
134
- return new Date(
135
- now.getFullYear(),
136
- now.getMonth(),
137
- now.getDate(),
138
- Number(todayMatch[1]),
139
- Number(todayMatch[2]),
140
- )
141
- }
142
-
143
- const yesterdayMatch = /^昨天\s+(\d{1,2}):(\d{2})$/.exec(value)
144
- if (yesterdayMatch) {
145
- const yesterday = new Date(now)
146
- yesterday.setDate(yesterday.getDate() - 1)
147
-
148
- return new Date(
149
- yesterday.getFullYear(),
150
- yesterday.getMonth(),
151
- yesterday.getDate(),
152
- Number(yesterdayMatch[1]),
153
- Number(yesterdayMatch[2]),
154
- )
155
- }
156
-
157
- return null
158
- }
159
-
160
- const normalizeCreatedAt = (value: string, fallbackCreatedOrder: number) => {
161
- const parsed = parseCreatedAtValue(value)
162
- if (parsed) {
163
- return parsed.toISOString()
164
- }
165
-
166
- return new Date(Date.now() - Math.max(1, fallbackCreatedOrder) * minuteInMs).toISOString()
167
- }
168
-
169
- const formatTodoCreatedAt = (value: string, now = new Date()) => {
170
- const parsed = parseCreatedAtValue(value, now)
171
- if (!parsed) {
172
- return value
173
- }
174
-
175
- const diffMs = now.getTime() - parsed.getTime()
176
- if (diffMs < 0) {
177
- return formatCalendarDateTime(parsed)
178
- }
179
-
180
- if (diffMs < minuteInMs) {
181
- return '刚刚'
182
- }
183
-
184
- if (diffMs < hourInMs) {
185
- return `${Math.max(1, Math.floor(diffMs / minuteInMs))} 分钟前`
186
- }
187
-
188
- if (isSameCalendarDay(parsed, now)) {
189
- return `${Math.max(1, Math.floor(diffMs / hourInMs))} 小时前`
190
- }
191
-
192
- const yesterday = new Date(now)
193
- yesterday.setDate(yesterday.getDate() - 1)
194
- if (diffMs < 2 * dayInMs && isSameCalendarDay(parsed, yesterday)) {
195
- return `昨天 ${padDatePart(parsed.getHours())}:${padDatePart(parsed.getMinutes())}`
196
- }
197
-
198
- if (parsed.getFullYear() === now.getFullYear()) {
199
- return formatCalendarDateTime(parsed, false)
200
- }
201
-
202
- return formatCalendarDateTime(parsed)
203
- }
204
-
205
- const getTodoCreatedAtTime = (item: TodoItem) => {
206
- const parsed = parseCreatedAtValue(item.createdAt)
207
- if (!parsed) {
208
- return 0
209
- }
210
-
211
- return parsed.getTime()
212
- }
213
-
214
- const getTodoStorage = () => {
215
- if (typeof globalThis === 'undefined' || !('localStorage' in globalThis)) {
216
- return null
217
- }
218
-
219
- try {
220
- return globalThis.localStorage
221
- } catch {
222
- return null
223
- }
224
- }
225
-
226
- const parsePersistedTodoItem = (value: unknown, fallbackCreatedOrder: number): TodoItem | null => {
227
- if (!value || typeof value !== 'object') {
228
- return null
229
- }
230
-
231
- const candidate = value as Partial<TodoItem>
232
- if (
233
- typeof candidate.id !== 'number' ||
234
- typeof candidate.title !== 'string' ||
235
- typeof candidate.archived !== 'boolean' ||
236
- typeof candidate.createdAt !== 'string' ||
237
- !isTodoStatus(candidate.status)
238
- ) {
239
- return null
240
- }
241
-
242
- return {
243
- id: candidate.id,
244
- title: candidate.title,
245
- archived: candidate.archived,
246
- status: candidate.status,
247
- createdAt: normalizeCreatedAt(candidate.createdAt, fallbackCreatedOrder),
248
- createdOrder:
249
- typeof candidate.createdOrder === 'number' && Number.isFinite(candidate.createdOrder)
250
- ? candidate.createdOrder
251
- : fallbackCreatedOrder,
252
- }
253
- }
254
-
255
- const loadPersistedTodos = (value: unknown) => {
256
- if (!Array.isArray(value)) {
257
- return null
258
- }
259
-
260
- const todos = value
261
- .map((item, index, source) => parsePersistedTodoItem(item, source.length - index))
262
- .filter((item): item is TodoItem => item !== null)
263
-
264
- if (value.length === 0 || todos.length > 0) {
265
- return todos
266
- }
267
-
268
- return initialTodos
269
- }
270
-
271
- const loadPersistedTodoState = (): PersistedTodoState | null => {
272
- const storage = getTodoStorage()
273
- if (!storage) {
274
- return null
62
+ const getStoredTodos = (): TodoItem[] => {
63
+ const raw = localStorage.getItem(todoStorageKey)
64
+ if (!raw) {
65
+ return initialTodos
275
66
  }
276
67
 
277
68
  try {
278
- const raw = storage.getItem(todoStorageKey)
279
- if (!raw) {
280
- return null
281
- }
282
-
283
- const parsed = JSON.parse(raw) as unknown
284
- if (Array.isArray(parsed)) {
285
- return {
286
- todos: loadPersistedTodos(parsed) ?? initialTodos,
287
- search: '',
288
- activeFilter: 'all',
289
- }
290
- }
291
-
292
- if (!parsed || typeof parsed !== 'object') {
293
- return null
69
+ const parsed = JSON.parse(raw)
70
+ if (!Array.isArray(parsed)) {
71
+ return initialTodos
294
72
  }
295
73
 
296
- const candidate = parsed as {
297
- todos?: unknown
298
- search?: unknown
299
- activeFilter?: unknown
300
- }
301
-
302
- return {
303
- todos: loadPersistedTodos(candidate.todos) ?? initialTodos,
304
- search: typeof candidate.search === 'string' ? candidate.search : '',
305
- activeFilter: isTodoFilter(candidate.activeFilter) ? candidate.activeFilter : 'all',
306
- }
74
+ return parsed.filter(
75
+ item =>
76
+ item &&
77
+ typeof item.id === 'number' &&
78
+ typeof item.title === 'string' &&
79
+ typeof item.archived === 'boolean' &&
80
+ typeof item.status === 'string' &&
81
+ typeof item.createdAt === 'string',
82
+ ) as TodoItem[]
307
83
  } catch {
308
- return null
84
+ return initialTodos
309
85
  }
310
86
  }
311
87
 
312
- const persistTodoState = (state: PersistedTodoState) => {
313
- const storage = getTodoStorage()
314
- if (!storage) {
315
- return
316
- }
317
-
318
- try {
319
- storage.setItem(todoStorageKey, JSON.stringify(state))
320
- } catch {}
321
- }
88
+ const getNextTodoId = (todos: TodoItem[]) =>
89
+ todos.reduce((maxId, item) => Math.max(maxId, item.id), 0) + 1
322
90
 
323
91
  const statusMeta: Record<
324
92
  TodoStatus,
@@ -349,72 +117,89 @@ const statusMeta: Record<
349
117
  },
350
118
  }
351
119
 
120
+ const EditingTitleInput: FC<{
121
+ initialTitle: string
122
+ onSave: (title: string) => void
123
+ onCancel: () => void
124
+ }> = props => {
125
+ const [title, setTitle] = useState(props.initialTitle)
126
+
127
+ return (
128
+ <div className="flex flex-col gap-3 sm:flex-row">
129
+ <input
130
+ className="input input-bordered w-full"
131
+ value={title.value}
132
+ onInput={(event: any) => {
133
+ setTitle((event.target as HTMLInputElement).value)
134
+ }}
135
+ onKeydown={(event: KeyboardEvent) => {
136
+ if (event.key === 'Enter') {
137
+ props.onSave(title.value.trim())
138
+ }
139
+ if (event.key === 'Escape') {
140
+ props.onCancel()
141
+ }
142
+ }}
143
+ />
144
+ <div className="flex gap-2">
145
+ <button className="btn btn-primary btn-sm" onClick={() => props.onSave(title.value.trim())}>
146
+ 保存
147
+ </button>
148
+ <button className="btn btn-ghost btn-sm" onClick={props.onCancel}>
149
+ 取消
150
+ </button>
151
+ </div>
152
+ </div>
153
+ )
154
+ }
155
+
352
156
  const TodoApp: FC = () => {
353
- const persistedState = loadPersistedTodoState()
354
- const initialStateTodos = persistedState?.todos ?? initialTodos
355
- const initialActiveFilter: TodoFilter = persistedState?.activeFilter ?? 'all'
356
- const [todos, setTodos] = useState<TodoItem[]>(initialStateTodos)
357
- const [draft, setDraft] = useState('')
358
- const [search, setSearch] = useState(persistedState?.search ?? '')
359
- const activeFilter = ref<TodoFilter>(initialActiveFilter)
360
- const setActiveFilter = (nextFilter: TodoFilter) => {
361
- activeFilter.value = nextFilter
362
- }
363
- const [editingId, setEditingId] = useState<number | null>(null)
364
- const [editingTitle, setEditingTitle] = useState('')
365
- const nextId = ref(getNextTodoId(initialStateTodos))
366
- const nextCreatedOrder = ref(getNextCreatedOrder(initialStateTodos))
367
-
368
- watchEffect(() => {
369
- persistTodoState({
370
- todos,
371
- search: search.value,
372
- activeFilter: activeFilter.value,
373
- })
374
- })
157
+ const todos = ref<TodoItem[]>(getStoredTodos())
158
+ const draft = ref('')
159
+ const search = ref('')
160
+ const activeFilter = ref<TodoFilter>('all')
161
+ const editingId = ref<number | null>(null)
162
+ const nextId = ref(getNextTodoId(todos.value))
163
+
164
+ watch(
165
+ () => todos.value,
166
+ () => {
167
+ localStorage.setItem(todoStorageKey, JSON.stringify(todos.value))
168
+ nextId.value = getNextTodoId(todos.value)
169
+ },
170
+ )
375
171
 
376
- const counts = computed(() => {
377
- return {
378
- total: todos.filter((item) => !item.archived).length,
379
- todo: todos.filter((item) => !item.archived && item.status === 'todo').length,
380
- doing: todos.filter((item) => !item.archived && item.status === 'doing').length,
381
- done: todos.filter((item) => !item.archived && item.status === 'done').length,
382
- archived: todos.filter((item) => item.archived).length,
383
- }
384
- })
172
+ const counts = computed(() => ({
173
+ total: todos.value.filter(item => !item.archived).length,
174
+ todo: todos.value.filter(item => !item.archived && item.status === 'todo').length,
175
+ doing: todos.value.filter(item => !item.archived && item.status === 'doing').length,
176
+ done: todos.value.filter(item => !item.archived && item.status === 'done').length,
177
+ archived: todos.value.filter(item => item.archived).length,
178
+ }))
385
179
 
386
180
  const visibleTodos = computed(() => {
387
181
  const keyword = search.value.trim().toLowerCase()
388
182
 
389
- return todos
390
- .filter((item) => {
391
- const matchesKeyword = !keyword || item.title.toLowerCase().includes(keyword)
392
- if (!matchesKeyword) {
393
- return false
394
- }
395
-
396
- if (activeFilter.value === 'archived') {
397
- return item.archived
398
- }
399
-
400
- if (item.archived) {
401
- return false
402
- }
403
-
404
- if (activeFilter.value === 'all') {
405
- return true
406
- }
407
-
408
- return item.status === activeFilter.value
409
- })
410
- .sort((left, right) => {
411
- const createdAtDiff = getTodoCreatedAtTime(right) - getTodoCreatedAtTime(left)
412
- if (createdAtDiff !== 0) {
413
- return createdAtDiff
414
- }
415
-
416
- return right.createdOrder - left.createdOrder
417
- })
183
+ return todos.value.filter(item => {
184
+ const matchesKeyword = !keyword || item.title.toLowerCase().includes(keyword)
185
+ if (!matchesKeyword) {
186
+ return false
187
+ }
188
+
189
+ if (activeFilter.value === 'archived') {
190
+ return item.archived
191
+ }
192
+
193
+ if (item.archived) {
194
+ return false
195
+ }
196
+
197
+ if (activeFilter.value === 'all') {
198
+ return true
199
+ }
200
+
201
+ return item.status === activeFilter.value
202
+ })
418
203
  })
419
204
 
420
205
  const addTodo = () => {
@@ -423,56 +208,52 @@ const TodoApp: FC = () => {
423
208
  return
424
209
  }
425
210
 
426
- const nextTodo: TodoItem = {
427
- id: nextId.value++,
428
- title,
429
- status: 'todo',
430
- archived: false,
431
- createdAt: new Date().toISOString(),
432
- createdOrder: nextCreatedOrder.value++,
433
- }
434
-
435
- setTodos((current) => [nextTodo, ...current])
436
- setDraft('')
211
+ todos.value = [
212
+ {
213
+ id: nextId.value++,
214
+ title,
215
+ status: 'todo',
216
+ archived: false,
217
+ createdAt: '刚刚',
218
+ },
219
+ ...todos.value,
220
+ ]
221
+ draft.value = ''
437
222
  }
438
223
 
439
224
  const removeTodo = (id: number) => {
440
- setTodos((current) => current.filter((item) => item.id !== id))
225
+ todos.value = todos.value.filter(item => item.id !== id)
441
226
  if (editingId.value === id) {
442
- setEditingId(null)
443
- setEditingTitle('')
227
+ editingId.value = null
444
228
  }
445
229
  }
446
230
 
447
231
  const updateStatus = (id: number, status: TodoStatus) => {
448
- setTodos((current) =>
449
- current.map((item) => (item.id === id ? { ...item, status, archived: false } : item)),
232
+ todos.value = todos.value.map(item =>
233
+ item.id === id ? { ...item, status, archived: false } : item,
450
234
  )
451
235
  }
452
236
 
453
237
  const toggleArchived = (id: number) => {
454
- setTodos((current) =>
455
- current.map((item) => (item.id === id ? { ...item, archived: !item.archived } : item)),
238
+ todos.value = todos.value.map(item =>
239
+ item.id === id ? { ...item, archived: !item.archived } : item,
456
240
  )
457
241
  }
458
242
 
459
243
  const startEditing = (item: TodoItem) => {
460
- setEditingId(item.id)
461
- setEditingTitle(item.title)
244
+ editingId.value = item.id
462
245
  }
463
246
 
464
247
  const cancelEditing = () => {
465
- setEditingId(null)
466
- setEditingTitle('')
248
+ editingId.value = null
467
249
  }
468
250
 
469
- const saveEditing = (id: number) => {
470
- const title = editingTitle.value.trim()
251
+ const saveEditing = (id: number, title: string) => {
471
252
  if (!title) {
472
253
  return
473
254
  }
474
255
 
475
- setTodos((current) => current.map((item) => (item.id === id ? { ...item, title } : item)))
256
+ todos.value = todos.value.map(item => (item.id === id ? { ...item, title } : item))
476
257
  cancelEditing()
477
258
  }
478
259
 
@@ -521,11 +302,10 @@ const TodoApp: FC = () => {
521
302
  value={draft.value}
522
303
  placeholder="例如:把日报页接入真实 API"
523
304
  onInput={(event: any) => {
524
- setDraft((event.target as HTMLInputElement).value)
305
+ draft.value = (event.target as HTMLInputElement).value
525
306
  }}
526
307
  onKeydown={(event: KeyboardEvent) => {
527
- if (event.key === 'Enter' && !event.isComposing) {
528
- event.preventDefault()
308
+ if (event.key === 'Enter') {
529
309
  addTodo()
530
310
  }
531
311
  }}
@@ -545,14 +325,14 @@ const TodoApp: FC = () => {
545
325
  value={search.value}
546
326
  placeholder="按标题筛选任务"
547
327
  onInput={(event: any) => {
548
- setSearch((event.target as HTMLInputElement).value)
328
+ search.value = (event.target as HTMLInputElement).value
549
329
  }}
550
330
  />
551
331
  </label>
552
332
  </div>
553
333
 
554
334
  <div className="flex flex-wrap gap-2">
555
- {filterOptions.map((filter) => (
335
+ {filterOptions.map(filter => (
556
336
  <button
557
337
  key={filter.key}
558
338
  className={`btn btn-sm ${
@@ -561,7 +341,7 @@ const TodoApp: FC = () => {
561
341
  : 'btn-ghost border border-base-300'
562
342
  }`}
563
343
  onClick={() => {
564
- setActiveFilter(filter.key)
344
+ activeFilter.value = filter.key
565
345
  }}
566
346
  >
567
347
  {filter.label}
@@ -587,14 +367,15 @@ const TodoApp: FC = () => {
587
367
  </section>
588
368
 
589
369
  <section className="grid gap-4">
590
- {visibleTodos.get().map((item) => {
591
- const isEditing = editingId.value === item.id
592
- const editingValue = isEditing ? editingTitle.value : item.title
370
+ {visibleTodos.get().map(item => {
593
371
  const meta = statusMeta[item.status]
372
+ const isEditing = editingId.value === item.id
594
373
 
595
374
  return (
596
375
  <article
597
- key={item.id}
376
+ key={`${item.id}-${item.title}-${item.status}-${item.archived ? 'archived' : 'active'}-${
377
+ isEditing ? 'editing' : 'view'
378
+ }`}
598
379
  className={`card border bg-base-100 shadow-sm transition-all ${meta.cardClass} ${
599
380
  item.archived ? 'opacity-75' : 'hover:-translate-y-0.5 hover:shadow-md'
600
381
  }`}
@@ -610,59 +391,32 @@ const TodoApp: FC = () => {
610
391
  {item.archived && (
611
392
  <span className="badge badge-secondary badge-outline">已归档</span>
612
393
  )}
613
- <span className="text-xs text-base-content/50">
614
- 创建于 {formatTodoCreatedAt(item.createdAt)}
615
- </span>
394
+ <span className="text-xs text-base-content/50">创建于 {item.createdAt}</span>
616
395
  </div>
617
396
 
618
- <h2
619
- className={`text-xl font-semibold ${
620
- isEditing
621
- ? 'hidden'
622
- : item.status === 'done'
397
+ {!isEditing && (
398
+ <h2
399
+ className={`text-xl font-semibold ${
400
+ item.status === 'done'
623
401
  ? 'text-base-content/50 line-through'
624
402
  : 'text-base-content'
625
- }`}
626
- >
627
- {item.title}
628
- </h2>
629
-
630
- <div className={`flex flex-col gap-3 sm:flex-row ${isEditing ? '' : 'hidden'}`}>
631
- <input
632
- className="input input-bordered w-full"
633
- value={editingValue}
634
- onInput={(event: any) => {
635
- setEditingTitle((event.target as HTMLInputElement).value)
636
- }}
637
- onKeydown={(event: KeyboardEvent) => {
638
- if (event.key === 'Enter') {
639
- saveEditing(item.id)
640
- }
641
- if (event.key === 'Escape') {
642
- cancelEditing()
643
- }
644
- }}
403
+ }`}
404
+ >
405
+ {item.title}
406
+ </h2>
407
+ )}
408
+
409
+ {isEditing && (
410
+ <EditingTitleInput
411
+ key={item.id}
412
+ initialTitle={item.title}
413
+ onSave={title => saveEditing(item.id, title)}
414
+ onCancel={cancelEditing}
645
415
  />
646
- <div className="flex gap-2">
647
- <button
648
- className="btn btn-primary btn-sm"
649
- onClick={() => saveEditing(item.id)}
650
- type="button"
651
- >
652
- 保存
653
- </button>
654
- <button
655
- className="btn btn-ghost btn-sm"
656
- onClick={cancelEditing}
657
- type="button"
658
- >
659
- 取消
660
- </button>
661
- </div>
662
- </div>
416
+ )}
663
417
 
664
418
  <div className="flex flex-wrap gap-2">
665
- {statusOptions.map((option) => (
419
+ {statusOptions.map(option => (
666
420
  <button
667
421
  key={option.key}
668
422
  className={`btn btn-xs ${
@@ -671,9 +425,8 @@ const TodoApp: FC = () => {
671
425
  : 'btn-ghost border border-base-300'
672
426
  }`}
673
427
  onClick={() => updateStatus(item.id, option.key)}
674
- type="button"
675
428
  >
676
- {option.actionLabel}
429
+ {option.label}
677
430
  </button>
678
431
  ))}
679
432
  </div>
@@ -681,25 +434,19 @@ const TodoApp: FC = () => {
681
434
 
682
435
  <div className="flex flex-wrap gap-2 lg:justify-end">
683
436
  {!isEditing && (
684
- <button
685
- className="btn btn-sm btn-outline"
686
- onClick={() => startEditing(item)}
687
- type="button"
688
- >
437
+ <button className="btn btn-sm btn-outline" onClick={() => startEditing(item)}>
689
438
  改名
690
439
  </button>
691
440
  )}
692
441
  <button
693
442
  className="btn btn-sm btn-outline btn-secondary"
694
443
  onClick={() => toggleArchived(item.id)}
695
- type="button"
696
444
  >
697
445
  {item.archived ? '恢复' : '归档'}
698
446
  </button>
699
447
  <button
700
448
  className="btn btn-sm btn-outline btn-error"
701
449
  onClick={() => removeTodo(item.id)}
702
- type="button"
703
450
  >
704
451
  删除
705
452
  </button>
@@ -709,11 +456,10 @@ const TodoApp: FC = () => {
709
456
  </article>
710
457
  )
711
458
  })}
712
-
713
459
  {!visibleTodos.get().length && (
714
460
  <div className="card border border-dashed border-base-300 bg-base-100 shadow-sm">
715
461
  <div className="card-body items-center py-14 text-center">
716
- <h2 className="text-xl font-semibold">当前筛选下没有任务</h2>
462
+ <h3 className="text-xl font-semibold">当前筛选下没有任务</h3>
717
463
  <p className="max-w-md text-sm leading-6 text-base-content/70">
718
464
  试试切换筛选、搜索关键字,或者直接新增一条任务。
719
465
  </p>