create-rue 0.3.12 → 0.7.2

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
- }
69
+ const parsed = JSON.parse(raw)
70
+ if (!Array.isArray(parsed)) {
71
+ return initialTodos
290
72
  }
291
73
 
292
- if (!parsed || typeof parsed !== 'object') {
293
- return null
294
- }
295
-
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,7 +325,7 @@ 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>
@@ -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,130 +367,100 @@ 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
593
- const meta = statusMeta[item.status]
594
-
595
- return (
596
- <article
597
- key={item.id}
598
- className={`card border bg-base-100 shadow-sm transition-all ${meta.cardClass} ${
599
- item.archived ? 'opacity-75' : 'hover:-translate-y-0.5 hover:shadow-md'
600
- }`}
601
- >
602
- <div className="card-body gap-4">
603
- <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
604
- <div className="flex-1 space-y-3">
605
- <div className="flex flex-wrap items-center gap-2">
606
- <span
607
- className={`inline-block h-2.5 w-2.5 rounded-full ${meta.dotClass}`}
608
- ></span>
609
- <span className={meta.badgeClass}>{meta.label}</span>
610
- {item.archived && (
611
- <span className="badge badge-secondary badge-outline">已归档</span>
612
- )}
613
- <span className="text-xs text-base-content/50">
614
- 创建于 {formatTodoCreatedAt(item.createdAt)}
615
- </span>
616
- </div>
370
+ {visibleTodos.get().length ? (
371
+ visibleTodos.get().map((item) => {
372
+ const meta = statusMeta[item.status]
373
+ const isEditing = editingId.value === item.id
374
+
375
+ return (
376
+ <article
377
+ key={item.id}
378
+ className={`card border bg-base-100 shadow-sm transition-all ${meta.cardClass} ${
379
+ item.archived ? 'opacity-75' : 'hover:-translate-y-0.5 hover:shadow-md'
380
+ }`}
381
+ >
382
+ <div className="card-body gap-4">
383
+ <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
384
+ <div className="flex-1 space-y-3">
385
+ <div className="flex flex-wrap items-center gap-2">
386
+ <span
387
+ className={`inline-block h-2.5 w-2.5 rounded-full ${meta.dotClass}`}
388
+ ></span>
389
+ <span className={meta.badgeClass}>{meta.label}</span>
390
+ {item.archived && (
391
+ <span className="badge badge-secondary badge-outline">已归档</span>
392
+ )}
393
+ <span className="text-xs text-base-content/50">
394
+ 创建于 {item.createdAt}
395
+ </span>
396
+ </div>
617
397
 
618
- <h2
619
- className={`text-xl font-semibold ${
620
- isEditing
621
- ? 'hidden'
622
- : item.status === 'done'
623
- ? 'text-base-content/50 line-through'
624
- : '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
- }}
645
- />
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"
398
+ {!isEditing && (
399
+ <h2
400
+ className={`text-xl font-semibold ${
401
+ item.status === 'done'
402
+ ? 'text-base-content/50 line-through'
403
+ : 'text-base-content'
404
+ }`}
658
405
  >
659
- 取消
660
- </button>
406
+ {item.title}
407
+ </h2>
408
+ )}
409
+
410
+ {isEditing && (
411
+ <EditingTitleInput
412
+ key={item.id}
413
+ initialTitle={item.title}
414
+ onSave={(title) => saveEditing(item.id, title)}
415
+ onCancel={cancelEditing}
416
+ />
417
+ )}
418
+
419
+ <div className="flex flex-wrap gap-2">
420
+ {statusOptions.map((option) => (
421
+ <button
422
+ key={option.key}
423
+ className={`btn btn-xs ${
424
+ item.status === option.key
425
+ ? 'btn-neutral'
426
+ : 'btn-ghost border border-base-300'
427
+ }`}
428
+ onClick={() => updateStatus(item.id, option.key)}
429
+ >
430
+ {option.label}
431
+ </button>
432
+ ))}
661
433
  </div>
662
434
  </div>
663
435
 
664
- <div className="flex flex-wrap gap-2">
665
- {statusOptions.map((option) => (
436
+ <div className="flex flex-wrap gap-2 lg:justify-end">
437
+ {!isEditing && (
666
438
  <button
667
- key={option.key}
668
- className={`btn btn-xs ${
669
- item.status === option.key
670
- ? 'btn-neutral'
671
- : 'btn-ghost border border-base-300'
672
- }`}
673
- onClick={() => updateStatus(item.id, option.key)}
674
- type="button"
439
+ className="btn btn-sm btn-outline"
440
+ onClick={() => startEditing(item)}
675
441
  >
676
- {option.actionLabel}
442
+ 改名
677
443
  </button>
678
- ))}
679
- </div>
680
- </div>
681
-
682
- <div className="flex flex-wrap gap-2 lg:justify-end">
683
- {!isEditing && (
444
+ )}
445
+ <button
446
+ className="btn btn-sm btn-outline btn-secondary"
447
+ onClick={() => toggleArchived(item.id)}
448
+ >
449
+ {item.archived ? '恢复' : '归档'}
450
+ </button>
684
451
  <button
685
- className="btn btn-sm btn-outline"
686
- onClick={() => startEditing(item)}
687
- type="button"
452
+ className="btn btn-sm btn-outline btn-error"
453
+ onClick={() => removeTodo(item.id)}
688
454
  >
689
- 改名
455
+ 删除
690
456
  </button>
691
- )}
692
- <button
693
- className="btn btn-sm btn-outline btn-secondary"
694
- onClick={() => toggleArchived(item.id)}
695
- type="button"
696
- >
697
- {item.archived ? '恢复' : '归档'}
698
- </button>
699
- <button
700
- className="btn btn-sm btn-outline btn-error"
701
- onClick={() => removeTodo(item.id)}
702
- type="button"
703
- >
704
- 删除
705
- </button>
457
+ </div>
706
458
  </div>
707
459
  </div>
708
- </div>
709
- </article>
710
- )
711
- })}
712
-
713
- {!visibleTodos.get().length && (
460
+ </article>
461
+ )
462
+ })
463
+ ) : (
714
464
  <div className="card border border-dashed border-base-300 bg-base-100 shadow-sm">
715
465
  <div className="card-body items-center py-14 text-center">
716
466
  <h2 className="text-xl font-semibold">当前筛选下没有任务</h2>