08-ToDoMVC

7/24/2022

# 一,不拆组件版本

# 1,创建项目

创建项目:

  第一步:yarn create vite todomvc --template vue
  第二步:cd todomvc
  第三步:yarn
  第四步:yarn dev
1
2
3
4

静态页面如下:

// todo.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./index.css">
    <title>Document</title>
</head>

<body>
    <section class="todoapp">
        <header class="header">
            <h1>todos</h1>
            <input class="new-todo" autofocus autocomplete="off" placeholder="What needs to be done?" />
        </header>
        <section class="main">
            <input id="toggle-all" class="toggle-all" type="checkbox" />
            <label for="toggle-all">Mark all as complete</label>
            <ul class="todo-list">
                <!-- :class="{ completed: item.done }" -->
                <li class="todo">
                    <div>
                        <input class="toggle" type="checkbox" />
                        <label>学习vue</label>
                        <button class="destroy"></button>
                    </div>
                    <input class="edit" type="text" style="display: none;" />
                </li>
            </ul>
        </section>
        <footer class="footer">
            <span class="todo-count"> <strong></strong> 1 item left </span>
            <ul class="filters">
                <li><a href="">All</a>
                </li>
                <li><a href="">Active</a>
                </li>
                <li>
                    <!-- :class="{ selected: visibility == 'Completed'} -->
                    <a href="">Completed</a>
                </li>
            </ul>
            <button class="clear-completed">
                Clear completed
            </button>
        </footer>

    </section>
</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// todo.css

html,
body {
    margin: 0;
    padding: 0;
}

button {
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    font-size: 100%;
    vertical-align: baseline;
    font-family: inherit;
    font-weight: inherit;
    color: inherit;
    -webkit-appearance: none;
    appearance: none;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

body {
    font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
    line-height: 1.4em;
    background: #f5f5f5;
    color: #4d4d4d;
    min-width: 230px;
    max-width: 550px;
    margin: 0 auto;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    font-weight: 300;
}

:focus {
    outline: 0;
}

.hidden {
    display: none;
}

.todoapp {
    background: #fff;
    margin: 130px 0 40px 0;
    position: relative;
    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
        0 25px 50px 0 rgba(0, 0, 0, 0.1);
}

.todoapp input::-webkit-input-placeholder {
    font-style: italic;
    font-weight: 300;
    color: #e6e6e6;
}

.todoapp input::-moz-placeholder {
    font-style: italic;
    font-weight: 300;
    color: #e6e6e6;
}

.todoapp input::input-placeholder {
    font-style: italic;
    font-weight: 300;
    color: #e6e6e6;
}

.todoapp h1 {
    position: absolute;
    top: -155px;
    width: 100%;
    font-size: 100px;
    font-weight: 100;
    text-align: center;
    color: rgba(175, 47, 47, 0.15);
    -webkit-text-rendering: optimizeLegibility;
    -moz-text-rendering: optimizeLegibility;
    text-rendering: optimizeLegibility;
}

.new-todo,
.edit {
    position: relative;
    margin: 0;
    width: 100%;
    font-size: 24px;
    font-family: inherit;
    font-weight: inherit;
    line-height: 1.4em;
    border: 0;
    color: inherit;
    padding: 6px;
    border: 1px solid #999;
    box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
    box-sizing: border-box;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

.new-todo {
    padding: 16px 16px 16px 60px;
    border: none;
    background: rgba(0, 0, 0, 0.003);
    box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}

.main {
    position: relative;
    z-index: 2;
    border-top: 1px solid #e6e6e6;
}

.toggle-all {
    text-align: center;
    border: none;
    /* Mobile Safari */
    opacity: 0;
    position: absolute;
}

.toggle-all+label {
    width: 60px;
    height: 34px;
    font-size: 0;
    position: absolute;
    top: -52px;
    left: -13px;
    -webkit-transform: rotate(90deg);
    transform: rotate(90deg);
}

.toggle-all+label:before {
    content: '❯';
    font-size: 22px;
    color: #e6e6e6;
    padding: 10px 27px 10px 27px;
}

.toggle-all:checked+label:before {
    color: #737373;
}

.todo-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.todo-list li {
    position: relative;
    font-size: 24px;
    border-bottom: 1px solid #ededed;
}

.todo-list li:last-child {
    border-bottom: none;
}

.todo-list li.editing {
    border-bottom: none;
    padding: 0;
}

.todo-list li .edit {
    display: block;
    width: 506px;
    padding: 12px 16px;
    margin: 0 0 0 43px;
}

.todo-list li.editing .view {
    /* display: none; */
}

.todo-list li .toggle {
    text-align: center;
    width: 40px;
    /* auto, since non-WebKit browsers doesn't support input styling */
    height: auto;
    position: absolute;
    top: 0;
    bottom: 0;
    margin: auto 0;
    border: none;
    /* Mobile Safari */
    -webkit-appearance: none;
    appearance: none;
}

.todo-list li .toggle {
    opacity: 0;
}

.todo-list li .toggle+label {
    /*
		Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
		IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
	*/
    background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
    background-repeat: no-repeat;
    background-position: center left;
}

.todo-list li .toggle:checked+label {
    background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}

.todo-list li label {
    word-break: break-all;
    padding: 15px 15px 15px 60px;
    display: block;
    line-height: 1.2;
    transition: color 0.4s;
}

.todo-list li.completed label {
    color: #d9d9d9;
    text-decoration: line-through;
}

.todo-list li .destroy {
    display: none;
    position: absolute;
    top: 0;
    right: 10px;
    bottom: 0;
    width: 40px;
    height: 40px;
    margin: auto 0;
    font-size: 30px;
    color: #cc9a9a;
    margin-bottom: 11px;
    transition: color 0.2s ease-out;
}

.todo-list li .destroy:hover {
    color: #af5b5e;
}

.todo-list li .destroy:after {
    content: '×';
}

.todo-list li:hover .destroy {
    display: block;
}

/* .todo-list li .edit {
	display: none;
} */

.todo-list li.editing:last-child {
    margin-bottom: -1px;
}

.footer {
    color: #777;
    padding: 10px 15px;
    height: 20px;
    text-align: center;
    border-top: 1px solid #e6e6e6;
}

.footer:before {
    content: '';
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    height: 50px;
    overflow: hidden;
    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), filters 0 8px 0 -3px #f6f6f6,
        0 9px 1px -3px rgba(0, 0, 0, 0.2),
        0 16px 0 -6px #f6f6f6,
        0 17px 2px -6px rgba(0, 0, 0, 0.2);
}

.todo-count {
    float: left;
    text-align: left;
}

.todo-count strong {
    font-weight: 300;
}

.filters {
    margin: 0;
    padding: 0;
    list-style: none;
    position: absolute;
    right: 0;
    left: 0;
}

.filters li {
    display: inline;
}

.filters li a {
    color: inherit;
    margin: 3px;
    padding: 3px 7px;
    text-decoration: none;
    border: 1px solid transparent;
    border-radius: 3px;
}

.filters li a:hover {
    border-color: rgba(175, 47, 47, 0.1);
}

.filters li a.selected {
    border-color: rgba(175, 47, 47, 0.2);
}

.clear-completed,
html .clear-completed:active {
    float: right;
    position: relative;
    line-height: 20px;
    text-decoration: none;
    cursor: pointer;
}

.clear-completed:hover {
    text-decoration: underline;
}

.info {
    margin: 65px auto 0;
    color: #bfbfbf;
    font-size: 10px;
    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
    text-align: center;
}

.info p {
    line-height: 1;
}

.info a {
    color: inherit;
    text-decoration: none;
    font-weight: 400;
}

.info a:hover {
    text-decoration: underline;
}

/*
	Hack to remove background from Mobile Safari.
	Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {

    .toggle-all,
    .todo-list li .toggle {
        background: none;
    }

    .todo-list li .toggle {
        height: 40px;
    }
}

@media (max-width: 430px) {
    .footer {
        height: 50px;
    }

    .filters {
        bottom: 10px;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380

# 2,把静态页面集成到项目

直接把html集成到App.vue组件中(不copy了),在main.ts中引入样式,如下:

import { createApp } from 'vue'
// 引入全局样式
// import './style.css'
import App from './App.vue'

// 导入todomvc的样式
import "./assets/index.css"

let app = createApp(App);
app.mount('#app')

1
2
3
4
5
6
7
8
9
10
11

# 3,显示todo

准备响应式数据,如下:

<script setup>
    import {
        ref,
        reactive
    } from "vue"

let todoList = reactive([{
        id: "01",
        text: "学习vue3",
        done: true,
        isEdit: false
    },
    {
        id: "02",
        text: "学习react",
        done: false,
        isEdit: false
    },
]);

</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在模板中渲染数据,如下:

 <ul class="todo-list">
     <!-- :class="{ completed: item.done }" -->
     <li :class="{ completed: item.done }" class="todo" v-for="(item, index) in todoList">
         <div>
             <input class="toggle" type="checkbox" :checked="item.done" />
             <label>{{ item.text }}</label>
             <button class="destroy"></button>
         </div>
         <input class="edit" type="text" style="display: none;" />
     </li>
 </ul>
1
2
3
4
5
6
7
8
9
10
11

效果如下:

# 4,删除todo

找到删除按钮,给删除按钮绑定点击事件,如下:

实现delTodo方法,如下:

# 5,改变单个todo的状态

找到todo前面的checkbox,绑定change事件,如下:

实现上面的方法,如下:

效果如下:

# 6,全选与全不选

如果所有的todo都完成了,全选按钮就是需要选中,需要定义一个计算属性,如下:

需要把isAllDone用在全选按钮上,如下:

测试如下:

可以点击上面的全选按钮,实现全选与全不选,给全选按钮绑定点击事件,如下:

实现对应的方法,如下:

效果如下:

# 7,编辑

给todo绑定双击事件,如下:

实现上面的方法如下:

根据isEdit状态,控制元素的显示与隐藏,如下:

效果如下:

输入框需要实现回显,如下:

效果如下:

当按回车或失去焦点,需要结束编辑,需要给输入框绑定回车事件和失去焦点事件,如下:

实现上面的方法,如下:

测试之,如下:

# 8,添加数据

给添加的输入框绑定回车事件,如下:

实现上面的方法,如下:

效果如下:

# 9,统计没有完成todo的数量

说白了,就是数一数,你的todoList中的有多少个todo的done是false,使用计算属性,如下:

在模板中使用之,如下:

测试之,如下:

# 10,筛选不同的todo

定义状态,如下:

当点击下面的三个按钮,需要改变状态,如下:

实现上面的方法,如下:

测试是否可以改变状态,如下:

点击了谁,需要给上面的三个按钮,添加样式,如下:

效果如下:

页面上到底显示如些todo,需要根据todoList和visibility来决定了,使用计算属性,如下:

此时就不能循环所有的todo了,要循环filterTodoList,如下:

效果如下:

# 11,清除所有已完成

只有当有完成的todo,才显示Clear completed这个按钮,控制这个按钮的显示与隐藏,如下:

给按钮绑定点击事件,如下:

实现对应的方法,如下:

效果如下:

# 12,数据持久化

需要所数据存储到localStorage中,之前封装过一个hook,使用之,如下:

在App组件中使用之,如下:

监听数据变化,一旦变化了,就需要存储,如下:

获取数据也需要从localStorage中获取,如下:

效果如下:

# 二,拆组件版本

# 1,拆组件

拆分如下:

TodoHeader.vue:

<template>
    <header class="header">
        <h1>todos</h1>
        <input class="new-todo" autofocus autocomplete="off" placeholder="What needs to be done?" />
    </header>
</template>
1
2
3
4
5
6

TodoMain.vue:

<template>
    <section class="main">
        <input id="toggle-all" class="toggle-all" type="checkbox" />
        <label for="toggle-all">Mark all as complete</label>
        <ul class="todo-list">
            <!-- :class="{ completed: item.done }" -->
            <li class="todo">
                <div>
                    <input class="toggle" type="checkbox" />
                    <label>学习vue</label>
                    <button class="destroy"></button>
                </div>
                <input class="edit" type="text" style="display: none;" />
            </li>
        </ul>
    </section>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

TodoFooter.vue:

<template>
    <footer class="footer">
        <span class="todo-count"> <strong></strong> 1 item left </span>
        <ul class="filters">
            <li><a href="">All</a>
            </li>
            <li><a href="">Active</a>
            </li>
            <li>
                <!-- :class="{ selected: visibility == 'Completed'} -->
                <a href="">Completed</a>
            </li>
        </ul>
        <button class="clear-completed">
            Clear completed
        </button>
    </footer>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 2,引入样式

直接把html集成到App.vue组件中(不copy了),在main.ts中引入样式,如下:

import { createApp } from 'vue'
// 引入全局样式
// import './style.css'
import App from './App.vue'

// 导入todomvc的样式
import "./assets/index.css"

let app = createApp(App);
app.mount('#app')

1
2
3
4
5
6
7
8
9
10
11

效果如下:

# 3,显示todo

准备响应式数据,如下:

接收数据,在模板中渲染数据,如下:

效果如下:

# 4,删除todo

给父绑定自定义事件,如下:

在子中触发自定义事件,并传参,如下:

父接收之,如下:

数据在父中删除,如下:

测试如下:

# 5,改变单个todo的状态

在父中绑定自定义事件,如下:

子中给checkbox绑定change方法,触发父中的自定义事件,如下:

效果如下:

# 6,全选与全不选

如果所有的todo都完成了,全选按钮就是需要选中,需要定义一个计算属性,如下:

把计算属性传递给子,如下:

子需要接收之,如下:

子使用之这,如下:

可以点击上面的全选按钮,实现全选与全不选,给全选按钮绑定点击事件,如下:

实现上面的方法,如下:

父中绑定自定义事件,如下:

实现对应的方法,如下:

测试如下:

# 7,编辑

给todo绑定双击事件,如下:

实现上面的方法如下:

在父中绑定自定义事件,如下:

实现父中方法,如下:

根据isEdit状态,控制元素的显示与隐藏,如下:

效果如下:

输入框需要实现回显,如下:

效果如下:

当按回车或失去焦点,需要结束编辑,需要给输入框绑定回车事件和失去焦点事件,如下:

实现上面的方法,如下:

父中绑定自定义事件,如下:

实现对应方式,如下:

测试之,如下:

# 8,添加数据

给添加的输入框绑定回车事件,如下:

父中定义自定义事件,如下:

实现对应的方法,如下:

效果如下:

# 9,统计没有完成todo的数量

说白了,就是数一数,你的todoList中的有多少个todo的done是false,使用计算属性,如下:

传递给子组件,如下:

子组件接收使用之,如下:

测试之,如下:

# 10,筛选不同的todo

定义状态,如下:

传递给子组件,如下:

子组件接收之,如下:

当点击下面的三个按钮,需要改变状态,如下:

实现上面的方法,如下:

父中绑定自定义事件,如下:

实现对应方法,如下:

测试是否可以改变状态,如下:

点击了谁,需要给上面的三个按钮,添加样式,如下:

效果如下:

页面上到底显示如些todo,需要根据todoList和visibility来决定了,使用计算属性,如下:

此时就传递filterTodoList,如下:

效果如下:

# 11,清除所有已完成

把filterList也传递给子TodoFooter.vue组件,如下:

子组件接收之,如下:

只有当有完成的todo,才显示Clear completed这个按钮,控制这个按钮的显示与隐藏,如下:

效果如下:

给按钮绑定点击事件,如下:

实现对应的方法,如下:

给父绑定自定义事件,如下:

实现对应方法:

效果如下:

# 12,数据持久化

需要所数据存储到localStorage中,之前封装过一个hook,使用之,如下:

在App组件中使用之,如下:

监听数据变化,一旦变化了,就需要存储,如下:

效果如下:

# 三,pinia版本

# 1,准备工作

创建项目,如下:

1670813603899

安装依赖,如下:

1670813793178

安装pinia,如下:

1670813822622

创建三个组件,如下:

1670813890830

在App组件中,引入,如下:

1670813981311

copy样式到项目中,如下:

1670814085454

在mina.js中 引入样式文件,入口文件如下:

1670814109804

项目启动,如下:

1670814137387

效果如下:

1670814204132

把三个组件的静态代码直接copy到组件中,如下:

1670814242147

1670814260323

1670814276412

效果如下:

1670814293628

# 2,显示todo

创建一个仓库,使用pinia,不使用vuex,如下:

1670814875125

在main.js中使用仓库,如下:

1670814609041

在调度工具中,查看之,如下:

1670814908803

在App.vue中获取仓库中的数据,传递给子组件,如下:

1670815222043

子组件接收数据,使用之,如下:

1670815268506

效果如下:

1670815279788

# 3,删除todo

所有的todo都是仓库中,在vuex中操作仓库中的状态,需要通过mutation,在pinia中,没有mutation,一切都是action,找到删除按钮,给删除按钮绑定点击事件,如下:

1670815589441

所以我们需要在仓库中准备一个del的action,如下:

1670815620662

效果如下 :

1670815631463

# 4,改变单个todo的状态

还是从组件下手,找到单选框,绑定事件,如下:

1670815842487

准备仓库中的action,如下:

1670815923939

测试之,如下:

1670815955110

# 5,全选与全不选

全选按钮是在TodoMain.vue中,当所有的todo都完成了,全选按钮需要选中,需要根据所有的todo的done得到一个新的状态,如果所有的todo的done是true,新的状态就是true,如下:

1670816157751

测试工具中测试如下:

1670816178462

在App组件中使用状态,父传子,如下:

1670816259307

在子组件中获取数据,使用之,如下:

1670816419927

效果如下:

1670816442114

然后点击全选按钮,实现全选与全不选,给全选按钮绑定点击事件,如下:

1670816561264

在仓库中准备对应的action,如下:

1670816638827

效果如下:

1670816681721

# 6,编辑todo

定义一个action,去修改isEdit,如下:

1670825255177

当双击某个todo时,需要调用action,如下:

1670825304033

实现方法,如下:

1670825358287

测试之,如下:

1670825403438

使用isEdit状态,如下:

1670825521587

测试之,如下:

1670825535584

然后,在输入框中输入框,当按回车键,或失去焦点,需要实现编辑,绑定回车键和失去焦点事件,如下:

1670825644921

实现对应的方法,如下:

1670825729394

在 仓库中实现action,如下:

1670825805012

测试之,如下:

1670825853351

# 7,添加todo

给输入框绑定回车事件,如下:

1670826110863

准备仓库中的action,如下:

1670826131808

测试之,如下:

1670826149740

# 8,统计没有完成的todo

数一数,todoList中有多少个todo没有完成了,如下:

1670826286174

在App组件中使用,数据传递给子组件,如下:

1670826351879

子组件需要接收之,如下:

1670826597564

效果如下:

1670826628589

# 9,筛选不同的todo

在仓库中定义状态,叫visibility和改变状态的action,如下:

1670826750710

给那三个按钮绑定点击事件,改变状态,如下:

1670826889246

测试如下:

1670826904244

控制样式,父传递visibility,如下:

1670827037082

子接收之,如下:

1670827059915

使用之,如下:

1670827088841

效果如下:

1670827099655

页面上显示什么样的todo,需要根据visibility状态进行控制,如下:

1670827194326

在App组件中,直接使用之,如下:

1670827229426

测试之,如下:

1670827245640

# 10,清除所有已完成

把所有的todo和没有完成的todo的数量传递给子组件,如下:

1670828640807

在子组件中,接收之,如下:

1670828709111

测试之,如下:

1670828746339

给清除已完成,绑定点击事件,如下:

1670828901047

实现对应的方法,如下:

1670828937919

准备action,如下:

1670829102602

测试如下:

1670829114795

# 11,数据持久化

之前封装过一个hook,copy过来,如下:

1670829219555

仓库中的数据,就需要从localStorage中获取了,如下:

1670829290587

测试如下:

1670829310705

当数据变化,就需要存储数据,如下:

1670829515489

效果如下:

1670829503805

Last Updated: 2/27/2023, 10:05:39 PM