@darkchest/wck 0.0.1
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/LICENSE +7 -0
- package/demo/Todo.vue +664 -0
- package/package.json +36 -0
- package/readme.md +774 -0
- package/src/compiler-helper.js +34 -0
- package/src/compiler-template.js +199 -0
- package/src/compilers/compileScriptDefault.js +333 -0
- package/src/compilers/compileScriptSetup.js +5 -0
- package/src/compilers/compileStyles.js +29 -0
- package/src/compilers/compileTemplate.js +62 -0
- package/src/component.js +2 -0
- package/src/index.js +50 -0
- package/src/petite-vue/petite-vue.es.js +1 -0
- package/src/petite-vue/petite-vue.iife.js +1 -0
- package/src/petite-vue/petite-vue.umd.js +1 -0
- package/src/utils/Logger.js +7 -0
- package/src/utils/index.js +2 -0
- package/src/utils/isComp.js +29 -0
package/readme.md
ADDED
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
# @darkchest/wck
|
|
2
|
+
|
|
3
|
+
## 🌉 Vue 单文件组件 (SFC) 转web-component组件桥接器
|
|
4
|
+
|
|
5
|
+
`@darkchest/wck` (全称@darkchest/WebComponentKit)是一个通过包装Vue单文件组件 (SFC) 并将它转换为通用的web-component组件的解决方案。
|
|
6
|
+
|
|
7
|
+
## 前言
|
|
8
|
+
|
|
9
|
+
总会有一些场景, 我们无法使用现代化的框架系统.
|
|
10
|
+
|
|
11
|
+
在习惯了现代化数据驱动的开发模式下再次回到操作dom来更新UI的模式(没有了事件, scss, 模板, 数据监听等), 的确让人感到非常不便. 并且因为全局作用域的关系, 我们很容易碰上变量, 样式的冲突导致的预期之外的bug. 更不幸的是由于没有固定的模板结构, 导致我们的代码有更大概率出现面条化的风险.
|
|
12
|
+
|
|
13
|
+
所以, 如果能够以vue2的options格式去写web-component, 在开发阶段借助vue的ide插件帮助我们进行代码提示及错误警告, 避免代码面条化. 在使用时又以web-component的方式去使用, 将组件内部的逻辑封装在shadowDOM中, 对象只暴露出必要的props和methods, 以及一些events, 这就足够应用大部分的需求了. 这不就是应对困境的最好方案了吗?
|
|
14
|
+
|
|
15
|
+
这也是我开发@darkchest/wck的初衷!
|
|
16
|
+
|
|
17
|
+
实际上在这之前我开发过几个版本的解决方案:
|
|
18
|
+
1. 通过vue3官方提供的方案构建web-component组件, 最终打包了vue3的代码到组件中导致整个组件体积巨形膨胀(一个小组件体积上涨到90kb+), 并且回头考虑到既然都直接上了vue3, 那么再写web-component的组件好像没什么意义, 此方案废弃.
|
|
19
|
+
|
|
20
|
+
2. 通过lit来封装web-component组件, 整体来说是很顺利的. 但是在开发过程中, 由于要使用装饰器(不太熟悉ts), 且要自行实现render, 整个开发过程不太舒适并且没有代码提示, 最终打包后体积大约为30kb+, 此方案搁置.
|
|
21
|
+
|
|
22
|
+
3. 通过官方提供的@vue/compiler-sfc解析vue单文件组件 (SFC), 并将其script部分进行转换标准的web-component组件的逻辑部分(已完成), 但在解析template部分时出现重大问题, 由于template部分解析出来属于vue特有的抽象语法树, 手上又没有详尽的资料又不想去啃源码, 导致template部分无法转换方案被废弃.
|
|
23
|
+
|
|
24
|
+
4. 最终, 我找到了[petite-vue](https://github.com/vuejs/petite-vue), 根据网络描述, 它是由 Vue.js 团队推出的重量仅约6KB的小型Vue版本,专为网页上的渐进式增强设计。 它保留了Vue的核心模板语法与响应式机制,但特别优化用于在已有的HTML页面上增添少量交互效果. 经过测试它无需经过编译即可直接支持template语法(v-for, v-if, v-model, v-html, @click等), 允许直接在dom上应用template语法且直接生效. 于是, 将方案3中的script转换与petite-vue的模板解析合并, 并抹平与标准web-component的差异后的最终方案终于完成(与标准版vue仍有差距, 但是核心功能已实现, 用于开发web组件应该是足够了), 并且最小体积降至16kb(未进行gzip压缩).
|
|
25
|
+
|
|
26
|
+
祝好!~
|
|
27
|
+
|
|
28
|
+
## ✨ 核心特性
|
|
29
|
+
|
|
30
|
+
- ✅ 支持props属性(注意vue中允许使用大驼峰和小驼峰定义属性, 但是web组件中只允许小写, 所以vue中定义的属性在编译时会转成小写和中划线连字符形式)
|
|
31
|
+
- ✅ 支持$el, $parent, $children, $root属性
|
|
32
|
+
- ✅ 支持$emit事件
|
|
33
|
+
- ✅ 支持methods方法
|
|
34
|
+
- ✅ 支持compputed属性
|
|
35
|
+
- ✅ 支持watch属性
|
|
36
|
+
- ✅ 支持slot匿名和具名插槽
|
|
37
|
+
- ✅ 支持mounted/onMounted, destroyed/unmounted/onUnmounted生命周期
|
|
38
|
+
|
|
39
|
+
### 关于props大小写问题
|
|
40
|
+
由于vue中定义props是允许大小写字母的.而web组件只允许小写字母(包含中划线), 所以当我们在vue组件中定义一个带有大写字母的属性时(例如appTitle), 那么我们将它转成web组件后, 插件会在web组件中将appTitle声明为apptitle和app-title两种形式, 所以这俩种写法最后都有效: <todo-list apptitle="*我的清单*"></todo-list>和<todo-list app-title="*我的清单*"></todo-list>.
|
|
41
|
+
|
|
42
|
+
### 关于属性, 方法, 事件, 生命周期钩子
|
|
43
|
+
我们通过props, methods, 定义的属性和方法, 都会直接通过web组件暴露出去. 页面上可以直接操作调用并实现双向同步的逻辑.
|
|
44
|
+
如果我们需要对外透出自定义事件, 也可以在组件内通过$emit(name, data)来触发事件, 这样在页面上可以使用on/off/once方法来监听该事件.
|
|
45
|
+
所有组件都会默认触发mounted/unmounted两个生命周期事件方便监听并进行某些初始化操作.
|
|
46
|
+
可以定义onMounted/onUnmounted生命周期钩子来监听组件的初始化与销毁事件.
|
|
47
|
+
|
|
48
|
+
## 🛠️ 安装与配置
|
|
49
|
+
|
|
50
|
+
### 1. 安装依赖
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install @darkchest/wck
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. 使用示例(重要: 请参考TodoList.vue文件示例, 该文件中已包含所有核心功能)
|
|
57
|
+
```javascript
|
|
58
|
+
// vite.config.js
|
|
59
|
+
import { defineConfig } from 'vite';
|
|
60
|
+
import path from 'path';
|
|
61
|
+
import wck from '@darkchest/wck';
|
|
62
|
+
|
|
63
|
+
export default defineConfig({
|
|
64
|
+
plugins: [
|
|
65
|
+
wck(), // 设置转换插件
|
|
66
|
+
],
|
|
67
|
+
build: {
|
|
68
|
+
lib: {
|
|
69
|
+
entry: path.resolve(__dirname, 'src/index.js'),
|
|
70
|
+
name: 'MyLib', // 在iife和umd模式下必填
|
|
71
|
+
formats: ['es', 'iife', 'umd'],
|
|
72
|
+
fileName: (format, name) => `${name}.${format}.min.js`
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
resolve: {
|
|
76
|
+
alias: {
|
|
77
|
+
'@': resolve(__dirname, 'src'), // 配置别名
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
// src/index.js
|
|
85
|
+
import TodoList from './src/components/TodoList.vue';
|
|
86
|
+
|
|
87
|
+
lit.component('todo-list', TodoList);
|
|
88
|
+
// 注意: web-component的标签名必须为中划线命名(有且至少一个中划线), 否则无法成功注册.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```vue
|
|
92
|
+
// src/components/TodoList.vue
|
|
93
|
+
|
|
94
|
+
<template>
|
|
95
|
+
<div id="app">
|
|
96
|
+
<div class="todo-container">
|
|
97
|
+
<!-- Header Slot -->
|
|
98
|
+
<header>
|
|
99
|
+
<slot name="header">
|
|
100
|
+
<h1 class="app-title">{{ appTitle }}</h1>
|
|
101
|
+
<p class="app-description">使用 Vue2 实现的 Todo List 应用</p>
|
|
102
|
+
</slot>
|
|
103
|
+
</header>
|
|
104
|
+
|
|
105
|
+
<!-- Add Todo Form -->
|
|
106
|
+
<div class="add-todo">
|
|
107
|
+
<input
|
|
108
|
+
type="text"
|
|
109
|
+
v-model="newTodo"
|
|
110
|
+
placeholder="请输入待办事项..."
|
|
111
|
+
@keyup.enter="addTodo"
|
|
112
|
+
class="todo-input"
|
|
113
|
+
/>
|
|
114
|
+
<button @click="addTodo" class="add-btn">添加</button>
|
|
115
|
+
<button @click="clearCompleted" class="clear-btn">清除已完成</button>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<!-- Filter Controls -->
|
|
119
|
+
<div class="filter-controls">
|
|
120
|
+
<button
|
|
121
|
+
@click="filter = 'all'"
|
|
122
|
+
:class="{ active: filter === 'all' }"
|
|
123
|
+
class="filter-btn"
|
|
124
|
+
>
|
|
125
|
+
全部 ({{ totalTodos }})
|
|
126
|
+
</button>
|
|
127
|
+
<button
|
|
128
|
+
@click="filter = 'active'"
|
|
129
|
+
:class="{ active: filter === 'active' }"
|
|
130
|
+
class="filter-btn"
|
|
131
|
+
>
|
|
132
|
+
未完成 ({{ activeTodosCount }})
|
|
133
|
+
</button>
|
|
134
|
+
<button
|
|
135
|
+
@click="filter = 'completed'"
|
|
136
|
+
:class="{ active: filter === 'completed' }"
|
|
137
|
+
class="filter-btn"
|
|
138
|
+
>
|
|
139
|
+
已完成 ({{ completedTodosCount }})
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!-- Todo List -->
|
|
144
|
+
<div class="todo-list">
|
|
145
|
+
<div v-if="filteredTodos.length === 0" class="empty-state">
|
|
146
|
+
<p v-if="todos.length === 0">暂无待办事项,请添加一个吧!</p>
|
|
147
|
+
<p v-else>没有{{ filter === 'active' ? '未完成' : '已完成' }}的事项</p>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div
|
|
151
|
+
v-for="todo in filteredTodos"
|
|
152
|
+
:key="todo.id"
|
|
153
|
+
class="todo-item"
|
|
154
|
+
:class="{ completed: todo.completed }"
|
|
155
|
+
>
|
|
156
|
+
<div class="todo-content">
|
|
157
|
+
<input
|
|
158
|
+
type="checkbox"
|
|
159
|
+
v-model="todo.completed"
|
|
160
|
+
class="todo-checkbox"
|
|
161
|
+
:id="'todo-' + todo.id"
|
|
162
|
+
/>
|
|
163
|
+
<label :for="'todo-' + todo.id" class="todo-text">
|
|
164
|
+
<span>{{ todo.text }}</span>
|
|
165
|
+
<span class="todo-date">{{ formatDate(todo.createdAt) }}</span>
|
|
166
|
+
</label>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="todo-actions">
|
|
169
|
+
<button @click="editTodo(todo)" class="edit-btn">编辑</button>
|
|
170
|
+
<button @click="deleteTodo(todo.id)" class="delete-btn">删除</button>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<!-- Stats -->
|
|
176
|
+
<div class="stats">
|
|
177
|
+
<p v-if="todos.length > 0">
|
|
178
|
+
已完成 {{ completedTodosCount }} / 总共 {{ totalTodos }} 个待办事项
|
|
179
|
+
<span v-if="hasCompletedTodos" class="completion-rate">
|
|
180
|
+
(完成率: {{ completionRate }}%)
|
|
181
|
+
</span>
|
|
182
|
+
</p>
|
|
183
|
+
|
|
184
|
+
<!-- Progress Bar Slot -->
|
|
185
|
+
<slot name="progress">
|
|
186
|
+
<div class="progress-container">
|
|
187
|
+
<div class="progress-bar" :style="{ width: completionRate + '%' }"></div>
|
|
188
|
+
</div>
|
|
189
|
+
</slot>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- Footer Slot -->
|
|
193
|
+
<footer>
|
|
194
|
+
<slot name="footer">
|
|
195
|
+
<p class="footer-text">双击事项可标记为完成/未完成</p>
|
|
196
|
+
<p class="footer-text">使用 Vue2 实现 - 包含 Props, Data, Methods, Slots, Watch, Computed, 生命周期</p>
|
|
197
|
+
</slot>
|
|
198
|
+
</footer>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<!-- Edit Modal -->
|
|
202
|
+
<div v-if="editingTodo" class="modal-overlay" @click="cancelEdit">
|
|
203
|
+
<div class="modal" @click.stop>
|
|
204
|
+
<h3>编辑待办事项</h3>
|
|
205
|
+
<input
|
|
206
|
+
type="text"
|
|
207
|
+
v-model="editingTodo.text"
|
|
208
|
+
@keyup.enter="saveEdit"
|
|
209
|
+
class="edit-input"
|
|
210
|
+
/>
|
|
211
|
+
<div class="modal-actions">
|
|
212
|
+
<button @click="saveEdit" class="save-btn">保存</button>
|
|
213
|
+
<button @click="cancelEdit" class="cancel-btn">取消</button>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</template>
|
|
219
|
+
|
|
220
|
+
<script>
|
|
221
|
+
export default {
|
|
222
|
+
name: 'TodoApp',
|
|
223
|
+
|
|
224
|
+
// Props 示例
|
|
225
|
+
props: {
|
|
226
|
+
initialTodos: {
|
|
227
|
+
type: [Array, Object],
|
|
228
|
+
default: () => []
|
|
229
|
+
},
|
|
230
|
+
appTitle: {
|
|
231
|
+
type: String,
|
|
232
|
+
default: '我的待办清单'
|
|
233
|
+
},
|
|
234
|
+
enableLocalStorage: {
|
|
235
|
+
type: Boolean,
|
|
236
|
+
default: true
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
// Data 示例
|
|
241
|
+
data() {
|
|
242
|
+
return {
|
|
243
|
+
newTodo: '',
|
|
244
|
+
todos: [],
|
|
245
|
+
filter: 'all',
|
|
246
|
+
editingTodo: null,
|
|
247
|
+
lastTodoId: 0
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
// Computed 示例
|
|
252
|
+
computed: {
|
|
253
|
+
// 计算过滤后的待办事项
|
|
254
|
+
filteredTodos() {
|
|
255
|
+
switch (this.filter) {
|
|
256
|
+
case 'active':
|
|
257
|
+
return this.todos.filter(todo => !todo.completed);
|
|
258
|
+
case 'completed':
|
|
259
|
+
return this.todos.filter(todo => todo.completed);
|
|
260
|
+
default:
|
|
261
|
+
return this.todos;
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
// 计算未完成事项数量
|
|
266
|
+
activeTodosCount() {
|
|
267
|
+
return this.todos.filter(todo => !todo.completed).length;
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
// 计算已完成事项数量
|
|
271
|
+
completedTodosCount() {
|
|
272
|
+
return this.todos.filter(todo => todo.completed).length;
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
// 计算总事项数量
|
|
276
|
+
totalTodos() {
|
|
277
|
+
return this.todos.length;
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
// 计算完成率
|
|
281
|
+
completionRate() {
|
|
282
|
+
if (this.totalTodos === 0) return 0;
|
|
283
|
+
return Math.round((this.completedTodosCount / this.totalTodos) * 100);
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
// 检查是否有已完成事项
|
|
287
|
+
hasCompletedTodos() {
|
|
288
|
+
return this.completedTodosCount > 0;
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
// Watch 示例
|
|
293
|
+
watch: {
|
|
294
|
+
// 监听 todos 变化并保存到 localStorage
|
|
295
|
+
todos: {
|
|
296
|
+
handler(newTodos, oldTodos) {
|
|
297
|
+
if (this.enableLocalStorage) {
|
|
298
|
+
localStorage.setItem('vue-todos', JSON.stringify(newTodos));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 发送事件给父组件(如果需要)
|
|
302
|
+
this.$emit('todos-updated', newTodos);
|
|
303
|
+
},
|
|
304
|
+
deep: true
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
// 监听 filter 变化
|
|
308
|
+
filter(newFilter, oldFilter) {
|
|
309
|
+
console.log('过滤器已更改为: '+newFilter);
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
// Methods 示例
|
|
314
|
+
methods: {
|
|
315
|
+
// 添加新的待办事项
|
|
316
|
+
addTodo() {
|
|
317
|
+
if (this.newTodo.trim() === '') return;
|
|
318
|
+
|
|
319
|
+
this.todos.push({
|
|
320
|
+
id: ++this.lastTodoId,
|
|
321
|
+
text: this.newTodo.trim(),
|
|
322
|
+
completed: false,
|
|
323
|
+
createdAt: new Date()
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
this.newTodo = '';
|
|
327
|
+
console.log('已添加新的待办事项');
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
// 删除待办事项
|
|
331
|
+
deleteTodo(id) {
|
|
332
|
+
const index = this.todos.findIndex(todo => todo.id === id);
|
|
333
|
+
if (index !== -1) {
|
|
334
|
+
this.todos.splice(index, 1);
|
|
335
|
+
console.log('已删除待办事项 ID: '+id);
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
// 编辑待办事项
|
|
340
|
+
editTodo(todo) {
|
|
341
|
+
this.editingTodo = { ...todo };
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
// 保存编辑
|
|
345
|
+
saveEdit() {
|
|
346
|
+
if (this.editingTodo && this.editingTodo.text.trim() !== '') {
|
|
347
|
+
const index = this.todos.findIndex(todo => todo.id === this.editingTodo.id);
|
|
348
|
+
if (index !== -1) {
|
|
349
|
+
this.$set(this.todos, index, { ...this.editingTodo });
|
|
350
|
+
console.log('已更新待办事项 ID: '+this.editingTodo.id);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
this.editingTodo = null;
|
|
354
|
+
// 已知这里会报错, 这是petite-vue内部的bug, 因为并不影响使用, 所以我不打算修复(没有petite-vue源码).
|
|
355
|
+
// 感觉这是因为v-if的优先级没有v-model高导致的, 所以我们可以通过额外设置一个开关变量来绕开这个报错, 只要不设置对象为null就行.
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
// 取消编辑
|
|
359
|
+
cancelEdit() {
|
|
360
|
+
this.editingTodo = null;
|
|
361
|
+
// 已知这里会报错, 这是petite-vue内部的bug, 因为并不影响使用, 所以我不打算修复(没有petite-vue源码).
|
|
362
|
+
// 感觉这是因为v-if的优先级没有v-model高导致的, 所以我们可以通过额外设置一个开关变量来绕开这个报错, 只要不设置对象为null就行.
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
// 清除已完成事项
|
|
366
|
+
clearCompleted() {
|
|
367
|
+
const originalLength = this.todos.length;
|
|
368
|
+
this.todos = this.todos.filter(todo => !todo.completed);
|
|
369
|
+
const clearedCount = originalLength - this.todos.length;
|
|
370
|
+
console.log('已清除 '+clearedCount+' 个已完成事项');
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
// 格式化日期
|
|
374
|
+
formatDate(date) {
|
|
375
|
+
return new Date(date).toLocaleDateString('zh-CN', {
|
|
376
|
+
month: 'short',
|
|
377
|
+
day: 'numeric',
|
|
378
|
+
hour: '2-digit',
|
|
379
|
+
minute: '2-digit'
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
// 生命周期钩子示例
|
|
385
|
+
created() {
|
|
386
|
+
console.log('TodoApp 组件已创建');
|
|
387
|
+
|
|
388
|
+
// 从 props 或 localStorage 初始化 todos
|
|
389
|
+
if (this.initialTodos && this.initialTodos.length > 0) {
|
|
390
|
+
this.todos = [...this.initialTodos];
|
|
391
|
+
this.lastTodoId = Math.max(...this.initialTodos.map(todo => todo.id), 0);
|
|
392
|
+
} else if (this.enableLocalStorage) {
|
|
393
|
+
const savedTodos = localStorage.getItem('vue-todos');
|
|
394
|
+
if (savedTodos) {
|
|
395
|
+
try {
|
|
396
|
+
this.todos = JSON.parse(savedTodos);
|
|
397
|
+
this.lastTodoId = Math.max(...this.todos.map(todo => todo.id), 0);
|
|
398
|
+
console.log('从 localStorage 加载待办事项');
|
|
399
|
+
} catch (e) {
|
|
400
|
+
console.error('解析保存的待办事项失败:', e);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
mounted() {
|
|
407
|
+
console.log('TodoApp 组件已挂载到 DOM');
|
|
408
|
+
|
|
409
|
+
// 为每个待办事项添加双击切换完成状态的功能
|
|
410
|
+
this.$nextTick(() => {
|
|
411
|
+
const todoItems = document.querySelectorAll('.todo-item');
|
|
412
|
+
todoItems.forEach(item => {
|
|
413
|
+
item.addEventListener('dblclick', () => {
|
|
414
|
+
const todoId = parseInt(item.dataset.id);
|
|
415
|
+
const todoIndex = this.todos.findIndex(todo => todo.id === todoId);
|
|
416
|
+
if (todoIndex !== -1) {
|
|
417
|
+
this.todos[todoIndex].completed = !this.todos[todoIndex].completed;
|
|
418
|
+
console.log('双击切换待办事项状态 ID: '+todoId);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
beforeUpdate() {
|
|
426
|
+
console.log('TodoApp 组件即将更新');
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
updated() {
|
|
430
|
+
console.log('TodoApp 组件已更新');
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
beforeDestroy() {
|
|
434
|
+
console.log('TodoApp 组件即将销毁');
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
destroyed() {
|
|
438
|
+
console.log('TodoApp 组件已销毁');
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
</script>
|
|
442
|
+
|
|
443
|
+
<style lang="scss" scoped>
|
|
444
|
+
#app {
|
|
445
|
+
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
|
446
|
+
-webkit-font-smoothing: antialiased;
|
|
447
|
+
-moz-osx-font-smoothing: grayscale;
|
|
448
|
+
color: #2c3e50;
|
|
449
|
+
max-width: 800px;
|
|
450
|
+
margin: 0 auto;
|
|
451
|
+
padding: 20px;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.todo-container {
|
|
455
|
+
background: white;
|
|
456
|
+
border-radius: 10px;
|
|
457
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
458
|
+
padding: 30px;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
header {
|
|
462
|
+
text-align: center;
|
|
463
|
+
margin-bottom: 30px;
|
|
464
|
+
border-bottom: 1px solid #eee;
|
|
465
|
+
padding-bottom: 20px;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.app-title {
|
|
469
|
+
color: #42b983;
|
|
470
|
+
font-size: 2.5rem;
|
|
471
|
+
margin-bottom: 10px;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.app-description {
|
|
475
|
+
color: #7f8c8d;
|
|
476
|
+
font-size: 1rem;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.add-todo {
|
|
480
|
+
display: flex;
|
|
481
|
+
gap: 10px;
|
|
482
|
+
margin-bottom: 20px;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.todo-input {
|
|
486
|
+
flex: 1;
|
|
487
|
+
padding: 12px 15px;
|
|
488
|
+
border: 2px solid #e0e0e0;
|
|
489
|
+
border-radius: 6px;
|
|
490
|
+
font-size: 16px;
|
|
491
|
+
transition: border-color 0.3s;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.todo-input:focus {
|
|
495
|
+
border-color: #42b983;
|
|
496
|
+
outline: none;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.add-btn, .clear-btn {
|
|
500
|
+
padding: 12px 20px;
|
|
501
|
+
border: none;
|
|
502
|
+
border-radius: 6px;
|
|
503
|
+
font-weight: bold;
|
|
504
|
+
cursor: pointer;
|
|
505
|
+
transition: all 0.3s;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.add-btn {
|
|
509
|
+
background-color: #42b983;
|
|
510
|
+
color: white;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.add-btn:hover {
|
|
514
|
+
background-color: #3aa876;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.clear-btn {
|
|
518
|
+
background-color: #f0f0f0;
|
|
519
|
+
color: #333;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.clear-btn:hover {
|
|
523
|
+
background-color: #e0e0e0;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.filter-controls {
|
|
527
|
+
display: flex;
|
|
528
|
+
gap: 10px;
|
|
529
|
+
margin-bottom: 25px;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.filter-btn {
|
|
533
|
+
flex: 1;
|
|
534
|
+
padding: 10px;
|
|
535
|
+
background-color: #f8f9fa;
|
|
536
|
+
border: 1px solid #dee2e6;
|
|
537
|
+
border-radius: 6px;
|
|
538
|
+
cursor: pointer;
|
|
539
|
+
transition: all 0.3s;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.filter-btn.active {
|
|
543
|
+
background-color: #42b983;
|
|
544
|
+
color: white;
|
|
545
|
+
border-color: #42b983;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.filter-btn:hover:not(.active) {
|
|
549
|
+
background-color: #e9ecef;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.todo-list {
|
|
553
|
+
margin-bottom: 25px;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.empty-state {
|
|
557
|
+
text-align: center;
|
|
558
|
+
padding: 40px 20px;
|
|
559
|
+
color: #95a5a6;
|
|
560
|
+
font-style: italic;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.todo-item {
|
|
564
|
+
display: flex;
|
|
565
|
+
justify-content: space-between;
|
|
566
|
+
align-items: center;
|
|
567
|
+
padding: 15px;
|
|
568
|
+
border-bottom: 1px solid #f0f0f0;
|
|
569
|
+
transition: all 0.3s;
|
|
570
|
+
border-radius: 6px;
|
|
571
|
+
margin-bottom: 8px;
|
|
572
|
+
background-color: #f9f9f9;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.todo-item:hover {
|
|
576
|
+
background-color: #f0f0f0;
|
|
577
|
+
transform: translateY(-2px);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.todo-item.completed {
|
|
581
|
+
opacity: 0.8;
|
|
582
|
+
background-color: #f0f9f0;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.todo-content {
|
|
586
|
+
display: flex;
|
|
587
|
+
align-items: center;
|
|
588
|
+
gap: 15px;
|
|
589
|
+
flex: 1;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.todo-checkbox {
|
|
593
|
+
width: 20px;
|
|
594
|
+
height: 20px;
|
|
595
|
+
cursor: pointer;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.todo-text {
|
|
599
|
+
flex: 1;
|
|
600
|
+
cursor: pointer;
|
|
601
|
+
display: flex;
|
|
602
|
+
justify-content: space-between;
|
|
603
|
+
align-items: center;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.todo-item.completed .todo-text span:first-child {
|
|
607
|
+
text-decoration: line-through;
|
|
608
|
+
color: #95a5a6;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.todo-date {
|
|
612
|
+
font-size: 0.8rem;
|
|
613
|
+
color: #95a5a6;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.todo-actions {
|
|
617
|
+
display: flex;
|
|
618
|
+
gap: 8px;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.edit-btn, .delete-btn {
|
|
622
|
+
padding: 6px 12px;
|
|
623
|
+
border: none;
|
|
624
|
+
border-radius: 4px;
|
|
625
|
+
font-size: 0.9rem;
|
|
626
|
+
cursor: pointer;
|
|
627
|
+
transition: all 0.2s;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.edit-btn {
|
|
631
|
+
background-color: #f0f0f0;
|
|
632
|
+
color: #333;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.edit-btn:hover {
|
|
636
|
+
background-color: #e0e0e0;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.delete-btn {
|
|
640
|
+
background-color: #ff6b6b;
|
|
641
|
+
color: white;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.delete-btn:hover {
|
|
645
|
+
background-color: #ff5252;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.stats {
|
|
649
|
+
margin-top: 25px;
|
|
650
|
+
padding-top: 20px;
|
|
651
|
+
border-top: 1px solid #eee;
|
|
652
|
+
color: #7f8c8d;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.completion-rate {
|
|
656
|
+
color: #42b983;
|
|
657
|
+
font-weight: bold;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.progress-container {
|
|
661
|
+
height: 10px;
|
|
662
|
+
background-color: #f0f0f0;
|
|
663
|
+
border-radius: 5px;
|
|
664
|
+
margin-top: 10px;
|
|
665
|
+
overflow: hidden;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.progress-bar {
|
|
669
|
+
height: 100%;
|
|
670
|
+
background-color: #42b983;
|
|
671
|
+
border-radius: 5px;
|
|
672
|
+
transition: width 0.5s ease;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
footer {
|
|
676
|
+
margin-top: 30px;
|
|
677
|
+
padding-top: 20px;
|
|
678
|
+
border-top: 1px solid #eee;
|
|
679
|
+
text-align: center;
|
|
680
|
+
color: #95a5a6;
|
|
681
|
+
font-size: 0.9rem;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.footer-text {
|
|
685
|
+
margin: 5px 0;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/* 编辑模态框样式 */
|
|
689
|
+
.modal-overlay {
|
|
690
|
+
position: fixed;
|
|
691
|
+
top: 0;
|
|
692
|
+
left: 0;
|
|
693
|
+
right: 0;
|
|
694
|
+
bottom: 0;
|
|
695
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
696
|
+
display: flex;
|
|
697
|
+
justify-content: center;
|
|
698
|
+
align-items: center;
|
|
699
|
+
z-index: 1000;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.modal {
|
|
703
|
+
background-color: white;
|
|
704
|
+
padding: 30px;
|
|
705
|
+
border-radius: 10px;
|
|
706
|
+
width: 90%;
|
|
707
|
+
max-width: 500px;
|
|
708
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.modal h3 {
|
|
712
|
+
margin-top: 0;
|
|
713
|
+
margin-bottom: 20px;
|
|
714
|
+
color: #2c3e50;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.edit-input {
|
|
718
|
+
width: 100%;
|
|
719
|
+
padding: 12px 15px;
|
|
720
|
+
border: 2px solid #42b983;
|
|
721
|
+
border-radius: 6px;
|
|
722
|
+
font-size: 16px;
|
|
723
|
+
margin-bottom: 20px;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
.modal-actions {
|
|
727
|
+
display: flex;
|
|
728
|
+
gap: 10px;
|
|
729
|
+
justify-content: flex-end;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.save-btn, .cancel-btn {
|
|
733
|
+
padding: 10px 20px;
|
|
734
|
+
border: none;
|
|
735
|
+
border-radius: 6px;
|
|
736
|
+
font-weight: bold;
|
|
737
|
+
cursor: pointer;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.save-btn {
|
|
741
|
+
background-color: #42b983;
|
|
742
|
+
color: white;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.save-btn:hover {
|
|
746
|
+
background-color: #3aa876;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.cancel-btn {
|
|
750
|
+
background-color: #f0f0f0;
|
|
751
|
+
color: #333;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.cancel-btn:hover {
|
|
755
|
+
background-color: #e0e0e0;
|
|
756
|
+
}
|
|
757
|
+
</style>
|
|
758
|
+
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
```html
|
|
762
|
+
// index.html
|
|
763
|
+
<!DOCTYPE html>
|
|
764
|
+
<html lang="en">
|
|
765
|
+
<head>
|
|
766
|
+
<meta charset="UTF-8" />
|
|
767
|
+
<title>my web components</title>
|
|
768
|
+
<script type="module" src="./dist/index.iife.min.js"></script>
|
|
769
|
+
</head>
|
|
770
|
+
<body>
|
|
771
|
+
<todo-list app-title="*待办清单*"></todo-list>
|
|
772
|
+
</body>
|
|
773
|
+
</html>
|
|
774
|
+
```
|