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.
- package/bundle.js +265 -323
- package/package.json +3 -3
- package/template/base/app/env.d.ts +1 -0
- package/template/base/app/pages/Home.tsx +1 -1
- package/template/base/app/pages/TodoApp.tsx +158 -412
- package/template/base/app/pages/components/Layout.tsx +29 -135
- package/template/base/package.json +19 -21
- package/template/base/tsconfig.json +4 -3
- package/template/base/vite.config.ts +14 -7
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { computed, type FC, ref, useState,
|
|
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
|
|
36
|
-
{ key: 'todo', label: '待开始'
|
|
37
|
-
{ key: 'doing', label: '进行中'
|
|
38
|
-
{ key: 'done', label: '已完成'
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
72
|
-
createdOrder: 1,
|
|
58
|
+
createdAt: '昨天 14:05',
|
|
73
59
|
},
|
|
74
60
|
]
|
|
75
61
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
279
|
-
if (!
|
|
280
|
-
return
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
84
|
+
return initialTodos
|
|
309
85
|
}
|
|
310
86
|
}
|
|
311
87
|
|
|
312
|
-
const
|
|
313
|
-
|
|
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
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
const
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
.
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
225
|
+
todos.value = todos.value.filter(item => item.id !== id)
|
|
441
226
|
if (editingId.value === id) {
|
|
442
|
-
|
|
443
|
-
setEditingTitle('')
|
|
227
|
+
editingId.value = null
|
|
444
228
|
}
|
|
445
229
|
}
|
|
446
230
|
|
|
447
231
|
const updateStatus = (id: number, status: TodoStatus) => {
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
455
|
-
|
|
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
|
-
|
|
461
|
-
setEditingTitle(item.title)
|
|
244
|
+
editingId.value = item.id
|
|
462
245
|
}
|
|
463
246
|
|
|
464
247
|
const cancelEditing = () => {
|
|
465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
+
draft.value = (event.target as HTMLInputElement).value
|
|
525
306
|
}}
|
|
526
307
|
onKeydown={(event: KeyboardEvent) => {
|
|
527
|
-
if (event.key === 'Enter'
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
<
|
|
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>
|