create-rue 0.0.7 → 0.0.11
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 +3 -3
- package/package.json +14 -17
- package/template/base/app/pages/Home.tsx +136 -55
- package/template/base/app/pages/Report.tsx +192 -0
- package/template/base/app/pages/TodoApp.tsx +464 -0
- package/template/base/app/pages/components/Layout.tsx +186 -32
- package/template/base/app/router.ts +2 -3
- package/template/base/package-lock.json +352 -350
- package/template/base/package.json +7 -7
- package/template/base/pnpm-lock.yaml +51 -51
- package/template/base/style.css +26 -415
- package/template/base/app/pages/Ecosystem.tsx +0 -47
- package/template/base/app/pages/Guide.tsx +0 -49
- package/template/base/app/pages/Packages.tsx +0 -52
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { computed, type FC, ref, useState, watch } from '@rue-js/rue'
|
|
2
|
+
import { RouterLink } from '@rue-js/router'
|
|
3
|
+
|
|
4
|
+
const todoStorageKey = 'rue.base.todos'
|
|
5
|
+
|
|
6
|
+
type TodoStatus = 'todo' | 'doing' | 'done'
|
|
7
|
+
type TodoFilter = 'all' | 'todo' | 'doing' | 'done' | 'archived'
|
|
8
|
+
|
|
9
|
+
type TodoItem = {
|
|
10
|
+
id: number
|
|
11
|
+
title: string
|
|
12
|
+
archived: boolean
|
|
13
|
+
status: TodoStatus
|
|
14
|
+
createdAt: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const filterOptions: Array<{ key: TodoFilter; label: string }> = [
|
|
18
|
+
{ key: 'all', label: '全部' },
|
|
19
|
+
{ key: 'todo', label: '待开始' },
|
|
20
|
+
{ key: 'doing', label: '进行中' },
|
|
21
|
+
{ key: 'done', label: '已完成' },
|
|
22
|
+
{ key: 'archived', label: '已归档' },
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const statusOptions: Array<{ key: TodoStatus; label: string }> = [
|
|
26
|
+
{ key: 'todo', label: '待开始' },
|
|
27
|
+
{ key: 'doing', label: '进行中' },
|
|
28
|
+
{ key: 'done', label: '已完成' },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
const initialTodos: TodoItem[] = [
|
|
32
|
+
{
|
|
33
|
+
id: 1,
|
|
34
|
+
title: '补充报表首页字段与展示规则',
|
|
35
|
+
status: 'doing',
|
|
36
|
+
archived: false,
|
|
37
|
+
createdAt: '今天 09:30',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 2,
|
|
41
|
+
title: '接入真实接口替换示例数据',
|
|
42
|
+
status: 'todo',
|
|
43
|
+
archived: false,
|
|
44
|
+
createdAt: '今天 10:05',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 3,
|
|
48
|
+
title: '复查主题切换和头部隐藏体验',
|
|
49
|
+
status: 'done',
|
|
50
|
+
archived: false,
|
|
51
|
+
createdAt: '昨天 18:20',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 4,
|
|
55
|
+
title: '归档旧版原型页面',
|
|
56
|
+
status: 'done',
|
|
57
|
+
archived: true,
|
|
58
|
+
createdAt: '昨天 14:05',
|
|
59
|
+
},
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
const getStoredTodos = (): TodoItem[] => {
|
|
63
|
+
const raw = localStorage.getItem(todoStorageKey)
|
|
64
|
+
if (!raw) {
|
|
65
|
+
return initialTodos
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(raw)
|
|
70
|
+
if (!Array.isArray(parsed)) {
|
|
71
|
+
return initialTodos
|
|
72
|
+
}
|
|
73
|
+
|
|
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[]
|
|
83
|
+
} catch {
|
|
84
|
+
return initialTodos
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const getNextTodoId = (todos: TodoItem[]) =>
|
|
89
|
+
todos.reduce((maxId, item) => Math.max(maxId, item.id), 0) + 1
|
|
90
|
+
|
|
91
|
+
const statusMeta: Record<
|
|
92
|
+
TodoStatus,
|
|
93
|
+
{
|
|
94
|
+
label: string
|
|
95
|
+
badgeClass: string
|
|
96
|
+
dotClass: string
|
|
97
|
+
cardClass: string
|
|
98
|
+
}
|
|
99
|
+
> = {
|
|
100
|
+
todo: {
|
|
101
|
+
label: '待开始',
|
|
102
|
+
badgeClass: 'badge badge-warning badge-outline',
|
|
103
|
+
dotClass: 'bg-warning',
|
|
104
|
+
cardClass: 'border-warning/30',
|
|
105
|
+
},
|
|
106
|
+
doing: {
|
|
107
|
+
label: '进行中',
|
|
108
|
+
badgeClass: 'badge badge-info badge-outline',
|
|
109
|
+
dotClass: 'bg-info',
|
|
110
|
+
cardClass: 'border-info/30',
|
|
111
|
+
},
|
|
112
|
+
done: {
|
|
113
|
+
label: '已完成',
|
|
114
|
+
badgeClass: 'badge badge-success badge-outline',
|
|
115
|
+
dotClass: 'bg-success',
|
|
116
|
+
cardClass: 'border-success/30',
|
|
117
|
+
},
|
|
118
|
+
}
|
|
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
|
+
|
|
156
|
+
const TodoApp: FC = () => {
|
|
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
|
+
)
|
|
171
|
+
|
|
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
|
+
}))
|
|
179
|
+
|
|
180
|
+
const visibleTodos = computed(() => {
|
|
181
|
+
const keyword = search.value.trim().toLowerCase()
|
|
182
|
+
|
|
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
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const addTodo = () => {
|
|
206
|
+
const title = draft.value.trim()
|
|
207
|
+
if (!title) {
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
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 = ''
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const removeTodo = (id: number) => {
|
|
225
|
+
todos.value = todos.value.filter(item => item.id !== id)
|
|
226
|
+
if (editingId.value === id) {
|
|
227
|
+
editingId.value = null
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const updateStatus = (id: number, status: TodoStatus) => {
|
|
232
|
+
todos.value = todos.value.map(item =>
|
|
233
|
+
item.id === id ? { ...item, status, archived: false } : item,
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const toggleArchived = (id: number) => {
|
|
238
|
+
todos.value = todos.value.map(item =>
|
|
239
|
+
item.id === id ? { ...item, archived: !item.archived } : item,
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const startEditing = (item: TodoItem) => {
|
|
244
|
+
editingId.value = item.id
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const cancelEditing = () => {
|
|
248
|
+
editingId.value = null
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const saveEditing = (id: number, title: string) => {
|
|
252
|
+
if (!title) {
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
todos.value = todos.value.map(item => (item.id === id ? { ...item, title } : item))
|
|
257
|
+
cancelEditing()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<div className="space-y-6 pb-4">
|
|
262
|
+
<section className="hero border border-base-300 bg-base-100 shadow-xl">
|
|
263
|
+
<div className="hero-content flex-col items-start gap-8 px-6 py-8 lg:flex-row lg:items-end lg:justify-between">
|
|
264
|
+
<div className="max-w-3xl space-y-4">
|
|
265
|
+
<h1 className="text-4xl font-semibold tracking-tight sm:text-5xl">Todo App 已作为 base 模板内置页面</h1>
|
|
266
|
+
<div className="flex flex-wrap gap-3">
|
|
267
|
+
<RouterLink to="/" className="btn btn-outline">
|
|
268
|
+
返回默认简报
|
|
269
|
+
</RouterLink>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<div className="stats stats-vertical bg-base-200 shadow sm:stats-horizontal">
|
|
274
|
+
<div className="stat px-6 py-4">
|
|
275
|
+
<div className="stat-title">活跃任务</div>
|
|
276
|
+
<div className="stat-value text-primary">{counts.get().total}</div>
|
|
277
|
+
</div>
|
|
278
|
+
<div className="stat px-6 py-4">
|
|
279
|
+
<div className="stat-title">已完成</div>
|
|
280
|
+
<div className="stat-value text-success">{counts.get().done}</div>
|
|
281
|
+
</div>
|
|
282
|
+
<div className="stat px-6 py-4">
|
|
283
|
+
<div className="stat-title">已归档</div>
|
|
284
|
+
<div className="stat-value text-secondary">{counts.get().archived}</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
</section>
|
|
289
|
+
|
|
290
|
+
<section className="card overflow-hidden border border-base-300 bg-base-100 shadow-sm">
|
|
291
|
+
<div className="card-body gap-6">
|
|
292
|
+
<div className="grid gap-4 lg:grid-cols-[1.3fr_0.9fr]">
|
|
293
|
+
<label className="form-control w-full">
|
|
294
|
+
<div className="label">
|
|
295
|
+
<span className="label-text font-medium">新增任务</span>
|
|
296
|
+
</div>
|
|
297
|
+
<div className="join w-full">
|
|
298
|
+
<input
|
|
299
|
+
className="input input-bordered join-item w-full"
|
|
300
|
+
value={draft.value}
|
|
301
|
+
placeholder="例如:把日报页接入真实 API"
|
|
302
|
+
onInput={(event: any) => {
|
|
303
|
+
draft.value = (event.target as HTMLInputElement).value
|
|
304
|
+
}}
|
|
305
|
+
onKeydown={(event: KeyboardEvent) => {
|
|
306
|
+
if (event.key === 'Enter') {
|
|
307
|
+
addTodo()
|
|
308
|
+
}
|
|
309
|
+
}}
|
|
310
|
+
/>
|
|
311
|
+
<button className="btn btn-primary join-item" onClick={addTodo}>
|
|
312
|
+
添加
|
|
313
|
+
</button>
|
|
314
|
+
</div>
|
|
315
|
+
</label>
|
|
316
|
+
|
|
317
|
+
<label className="form-control w-full">
|
|
318
|
+
<div className="label">
|
|
319
|
+
<span className="label-text font-medium">搜索任务</span>
|
|
320
|
+
</div>
|
|
321
|
+
<input
|
|
322
|
+
className="input input-bordered w-full"
|
|
323
|
+
value={search.value}
|
|
324
|
+
placeholder="按标题筛选任务"
|
|
325
|
+
onInput={(event: any) => {
|
|
326
|
+
search.value = (event.target as HTMLInputElement).value
|
|
327
|
+
}}
|
|
328
|
+
/>
|
|
329
|
+
</label>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<div className="flex flex-wrap gap-2">
|
|
333
|
+
{filterOptions.map(filter => (
|
|
334
|
+
<button
|
|
335
|
+
key={filter.key}
|
|
336
|
+
className={`btn btn-sm ${
|
|
337
|
+
activeFilter.value === filter.key
|
|
338
|
+
? 'btn-primary'
|
|
339
|
+
: 'btn-ghost border border-base-300'
|
|
340
|
+
}`}
|
|
341
|
+
onClick={() => {
|
|
342
|
+
activeFilter.value = filter.key
|
|
343
|
+
}}
|
|
344
|
+
>
|
|
345
|
+
{filter.label}
|
|
346
|
+
</button>
|
|
347
|
+
))}
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<div className="grid gap-4 md:grid-cols-3">
|
|
351
|
+
<div className="rounded-2xl border border-warning/30 bg-warning/10 p-5">
|
|
352
|
+
<p className="text-sm text-base-content/70">待开始</p>
|
|
353
|
+
<p className="mt-2 text-3xl font-semibold">{counts.get().todo}</p>
|
|
354
|
+
</div>
|
|
355
|
+
<div className="rounded-2xl border border-info/30 bg-info/10 p-5">
|
|
356
|
+
<p className="text-sm text-base-content/70">进行中</p>
|
|
357
|
+
<p className="mt-2 text-3xl font-semibold">{counts.get().doing}</p>
|
|
358
|
+
</div>
|
|
359
|
+
<div className="rounded-2xl border border-success/30 bg-success/10 p-5">
|
|
360
|
+
<p className="text-sm text-base-content/70">已完成</p>
|
|
361
|
+
<p className="mt-2 text-3xl font-semibold">{counts.get().done}</p>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
</section>
|
|
366
|
+
|
|
367
|
+
<section className="grid gap-4">
|
|
368
|
+
{visibleTodos.get().length ? (
|
|
369
|
+
visibleTodos.get().map(item => {
|
|
370
|
+
const meta = statusMeta[item.status]
|
|
371
|
+
const isEditing = editingId.value === item.id
|
|
372
|
+
|
|
373
|
+
return (
|
|
374
|
+
<article
|
|
375
|
+
key={item.id}
|
|
376
|
+
className={`card border bg-base-100 shadow-sm transition-all ${meta.cardClass} ${
|
|
377
|
+
item.archived ? 'opacity-75' : 'hover:-translate-y-0.5 hover:shadow-md'
|
|
378
|
+
}`}
|
|
379
|
+
>
|
|
380
|
+
<div className="card-body gap-4">
|
|
381
|
+
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
382
|
+
<div className="flex-1 space-y-3">
|
|
383
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
384
|
+
<span className={`inline-block h-2.5 w-2.5 rounded-full ${meta.dotClass}`}></span>
|
|
385
|
+
<span className={meta.badgeClass}>{meta.label}</span>
|
|
386
|
+
{item.archived && <span className="badge badge-secondary badge-outline">已归档</span>}
|
|
387
|
+
<span className="text-xs text-base-content/50">创建于 {item.createdAt}</span>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
{!isEditing && (
|
|
391
|
+
<h2
|
|
392
|
+
className={`text-xl font-semibold ${
|
|
393
|
+
item.status === 'done'
|
|
394
|
+
? 'text-base-content/50 line-through'
|
|
395
|
+
: 'text-base-content'
|
|
396
|
+
}`}
|
|
397
|
+
>
|
|
398
|
+
{item.title}
|
|
399
|
+
</h2>
|
|
400
|
+
)}
|
|
401
|
+
|
|
402
|
+
{isEditing && (
|
|
403
|
+
<EditingTitleInput
|
|
404
|
+
key={item.id}
|
|
405
|
+
initialTitle={item.title}
|
|
406
|
+
onSave={title => saveEditing(item.id, title)}
|
|
407
|
+
onCancel={cancelEditing}
|
|
408
|
+
/>
|
|
409
|
+
)}
|
|
410
|
+
|
|
411
|
+
<div className="flex flex-wrap gap-2">
|
|
412
|
+
{statusOptions.map(option => (
|
|
413
|
+
<button
|
|
414
|
+
key={option.key}
|
|
415
|
+
className={`btn btn-xs ${
|
|
416
|
+
item.status === option.key
|
|
417
|
+
? 'btn-neutral'
|
|
418
|
+
: 'btn-ghost border border-base-300'
|
|
419
|
+
}`}
|
|
420
|
+
onClick={() => updateStatus(item.id, option.key)}
|
|
421
|
+
>
|
|
422
|
+
{option.label}
|
|
423
|
+
</button>
|
|
424
|
+
))}
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
<div className="flex flex-wrap gap-2 lg:justify-end">
|
|
429
|
+
{!isEditing && (
|
|
430
|
+
<button className="btn btn-sm btn-outline" onClick={() => startEditing(item)}>
|
|
431
|
+
改名
|
|
432
|
+
</button>
|
|
433
|
+
)}
|
|
434
|
+
<button
|
|
435
|
+
className="btn btn-sm btn-outline btn-secondary"
|
|
436
|
+
onClick={() => toggleArchived(item.id)}
|
|
437
|
+
>
|
|
438
|
+
{item.archived ? '恢复' : '归档'}
|
|
439
|
+
</button>
|
|
440
|
+
<button className="btn btn-sm btn-outline btn-error" onClick={() => removeTodo(item.id)}>
|
|
441
|
+
删除
|
|
442
|
+
</button>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
</article>
|
|
447
|
+
)
|
|
448
|
+
})
|
|
449
|
+
) : (
|
|
450
|
+
<div className="card border border-dashed border-base-300 bg-base-100 shadow-sm">
|
|
451
|
+
<div className="card-body items-center py-14 text-center">
|
|
452
|
+
<h2 className="text-xl font-semibold">当前筛选下没有任务</h2>
|
|
453
|
+
<p className="max-w-md text-sm leading-6 text-base-content/70">
|
|
454
|
+
试试切换筛选、搜索关键字,或者直接新增一条任务。
|
|
455
|
+
</p>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
)}
|
|
459
|
+
</section>
|
|
460
|
+
</div>
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export default TodoApp
|