@wsxjs/wsx-base-components 0.0.5
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 +21 -0
- package/README.md +204 -0
- package/dist/index.cjs +6 -0
- package/dist/index.js +1085 -0
- package/package.json +57 -0
- package/src/ColorPicker.css +188 -0
- package/src/ColorPicker.wsx +416 -0
- package/src/ColorPickerUtils.ts +116 -0
- package/src/ReactiveCounter.css +304 -0
- package/src/ReactiveCounter.wsx +244 -0
- package/src/SimpleReactiveDemo.wsx +58 -0
- package/src/SvgDemo.wsx +241 -0
- package/src/SvgIcon.wsx +88 -0
- package/src/ThemeSwitcher.css +91 -0
- package/src/ThemeSwitcher.wsx +97 -0
- package/src/XyButton.css +257 -0
- package/src/XyButton.wsx +356 -0
- package/src/XyButtonGroup.css +30 -0
- package/src/XyButtonGroup.wsx +124 -0
- package/src/index.ts +17 -0
- package/src/jsx-inject.ts +2 -0
- package/src/types/wsx.d.ts +6 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件类型定义
|
|
3
|
+
*/
|
|
4
|
+
export type PluginType = "text" | "marker";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 节流函数类型定义
|
|
8
|
+
*/
|
|
9
|
+
export type ThrottleFunction<T extends unknown[]> = (...args: T) => void;
|
|
10
|
+
|
|
11
|
+
const TEXT_COLOR_CACHE = "editor-js-text-color-cache";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 检查是否为CSS变量
|
|
15
|
+
* @param value 待检查的值
|
|
16
|
+
* @returns 是否为CSS变量
|
|
17
|
+
*/
|
|
18
|
+
function isColorVariable(value: string): boolean {
|
|
19
|
+
return value.startsWith("var(") && value.endsWith(")");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 提取CSS变量名
|
|
24
|
+
* @param variable CSS变量字符串
|
|
25
|
+
* @returns 变量名
|
|
26
|
+
*/
|
|
27
|
+
function extractVariableName(variable: string): string {
|
|
28
|
+
return variable.slice(4, -1).trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 获取CSS属性值
|
|
33
|
+
* @param propertyName CSS属性名
|
|
34
|
+
* @returns CSS属性值
|
|
35
|
+
*/
|
|
36
|
+
function getCSSPropertyValue(propertyName: string): string {
|
|
37
|
+
const computedStyle = getComputedStyle(document.documentElement);
|
|
38
|
+
return computedStyle.getPropertyValue(propertyName).trim() || propertyName;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert CSS variables to color string.
|
|
43
|
+
* @param colorValue original value provided by users
|
|
44
|
+
* @returns string color string
|
|
45
|
+
*/
|
|
46
|
+
export function handleCSSVariables(colorValue: string): string {
|
|
47
|
+
if (isColorVariable(colorValue)) {
|
|
48
|
+
const variableName = extractVariableName(colorValue);
|
|
49
|
+
return getCSSPropertyValue(variableName);
|
|
50
|
+
}
|
|
51
|
+
return colorValue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Cache the latest text/marker color
|
|
56
|
+
* @param defaultColor 默认颜色值
|
|
57
|
+
* @param pluginType 插件类型
|
|
58
|
+
* @returns 返回设置的默认颜色
|
|
59
|
+
*/
|
|
60
|
+
export function setDefaultColorCache(defaultColor: string, pluginType: PluginType): string {
|
|
61
|
+
sessionStorage.setItem(`${TEXT_COLOR_CACHE}-${pluginType}`, JSON.stringify(defaultColor));
|
|
62
|
+
return defaultColor;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Cache custom color
|
|
67
|
+
* @param customColor 自定义颜色值
|
|
68
|
+
* @param pluginType 插件类型
|
|
69
|
+
*/
|
|
70
|
+
export function setCustomColorCache(customColor: string, pluginType: PluginType): void {
|
|
71
|
+
sessionStorage.setItem(`${TEXT_COLOR_CACHE}-${pluginType}-custom`, JSON.stringify(customColor));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get cached custom color
|
|
76
|
+
* @param pluginType 插件类型
|
|
77
|
+
* @returns 缓存的自定义颜色值或null
|
|
78
|
+
*/
|
|
79
|
+
export function getCustomColorCache(pluginType: PluginType): string | null {
|
|
80
|
+
const cachedCustomColor = sessionStorage.getItem(`${TEXT_COLOR_CACHE}-${pluginType}-custom`);
|
|
81
|
+
return cachedCustomColor ? JSON.parse(cachedCustomColor) : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Throttle function
|
|
86
|
+
* @param fn 要节流的函数
|
|
87
|
+
* @param delay 延迟时间(毫秒)
|
|
88
|
+
* @returns 节流后的函数
|
|
89
|
+
*/
|
|
90
|
+
export function throttle<T extends unknown[]>(
|
|
91
|
+
fn: ThrottleFunction<T>,
|
|
92
|
+
delay: number
|
|
93
|
+
): (...args: T) => void {
|
|
94
|
+
let id: NodeJS.Timeout | null = null;
|
|
95
|
+
return (...args: T) => {
|
|
96
|
+
if (!id) {
|
|
97
|
+
id = setTimeout(() => {
|
|
98
|
+
fn(...args);
|
|
99
|
+
id = null;
|
|
100
|
+
}, delay);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get cached text/marker color
|
|
106
|
+
* @param defaultColor 默认颜色值
|
|
107
|
+
* @param pluginType 插件类型
|
|
108
|
+
* @returns 缓存的颜色值或默认颜色
|
|
109
|
+
*/
|
|
110
|
+
export function getDefaultColorCache(defaultColor: string, pluginType: PluginType): string {
|
|
111
|
+
const cachedDefaultColor = sessionStorage.getItem(`${TEXT_COLOR_CACHE}-${pluginType}`);
|
|
112
|
+
return cachedDefaultColor ? JSON.parse(cachedDefaultColor) : defaultColor;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const CONVERTER_BTN = "ce-inline-toolbar__dropdown";
|
|
116
|
+
export const CONVERTER_PANEL = "ce-conversion-toolbar--showed";
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
.reactive-counter {
|
|
2
|
+
max-width: 500px;
|
|
3
|
+
margin: 20px auto;
|
|
4
|
+
padding: 24px;
|
|
5
|
+
border-radius: 12px;
|
|
6
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
7
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
8
|
+
transition: all 0.3s ease;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.theme-light {
|
|
12
|
+
background: #ffffff;
|
|
13
|
+
color: #333333;
|
|
14
|
+
border: 1px solid #e1e5e9;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.theme-dark {
|
|
18
|
+
background: #1a1a1a;
|
|
19
|
+
color: #ffffff;
|
|
20
|
+
border: 1px solid #333333;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.theme-blue {
|
|
24
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
25
|
+
color: #ffffff;
|
|
26
|
+
border: 1px solid #5a67d8;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.header {
|
|
30
|
+
text-align: center;
|
|
31
|
+
margin-bottom: 24px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.header h3 {
|
|
35
|
+
margin: 0 0 8px 0;
|
|
36
|
+
font-size: 24px;
|
|
37
|
+
font-weight: 600;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.subtitle {
|
|
41
|
+
margin: 0;
|
|
42
|
+
opacity: 0.8;
|
|
43
|
+
font-size: 14px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.counter-display {
|
|
47
|
+
text-align: center;
|
|
48
|
+
margin-bottom: 32px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.count-value {
|
|
52
|
+
font-size: 48px;
|
|
53
|
+
font-weight: bold;
|
|
54
|
+
line-height: 1;
|
|
55
|
+
margin-bottom: 8px;
|
|
56
|
+
font-variant-numeric: tabular-nums;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.step-info {
|
|
60
|
+
font-size: 14px;
|
|
61
|
+
opacity: 0.7;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.controls {
|
|
65
|
+
display: flex;
|
|
66
|
+
gap: 12px;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
margin-bottom: 24px;
|
|
69
|
+
flex-wrap: wrap;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.btn {
|
|
73
|
+
padding: 10px 20px;
|
|
74
|
+
border: none;
|
|
75
|
+
border-radius: 8px;
|
|
76
|
+
font-size: 16px;
|
|
77
|
+
font-weight: 500;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
transition: all 0.2s ease;
|
|
80
|
+
min-width: 60px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.btn:disabled {
|
|
84
|
+
opacity: 0.5;
|
|
85
|
+
cursor: not-allowed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.btn-primary {
|
|
89
|
+
background: #4299e1;
|
|
90
|
+
color: white;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.btn-primary:hover:not(:disabled) {
|
|
94
|
+
background: #3182ce;
|
|
95
|
+
transform: translateY(-1px);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.btn-secondary {
|
|
99
|
+
background: #718096;
|
|
100
|
+
color: white;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.btn-secondary:hover:not(:disabled) {
|
|
104
|
+
background: #4a5568;
|
|
105
|
+
transform: translateY(-1px);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.btn-warning {
|
|
109
|
+
background: #ed8936;
|
|
110
|
+
color: white;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.btn-warning:hover:not(:disabled) {
|
|
114
|
+
background: #dd6b20;
|
|
115
|
+
transform: translateY(-1px);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.btn-success {
|
|
119
|
+
background: #48bb78;
|
|
120
|
+
color: white;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.btn-success:hover:not(:disabled) {
|
|
124
|
+
background: #38a169;
|
|
125
|
+
transform: translateY(-1px);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.btn-danger {
|
|
129
|
+
background: #f56565;
|
|
130
|
+
color: white;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.btn-danger:hover:not(:disabled) {
|
|
134
|
+
background: #e53e3e;
|
|
135
|
+
transform: translateY(-1px);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.btn-sm {
|
|
139
|
+
padding: 6px 12px;
|
|
140
|
+
font-size: 14px;
|
|
141
|
+
min-width: auto;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.step-controls,
|
|
145
|
+
.auto-controls,
|
|
146
|
+
.theme-controls,
|
|
147
|
+
.message-controls {
|
|
148
|
+
margin-bottom: 20px;
|
|
149
|
+
text-align: center;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.step-controls label {
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
justify-content: center;
|
|
156
|
+
gap: 12px;
|
|
157
|
+
font-size: 14px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.step-controls input[type="range"] {
|
|
161
|
+
flex: 0 0 150px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.step-controls span {
|
|
165
|
+
font-weight: bold;
|
|
166
|
+
min-width: 20px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.theme-controls select,
|
|
170
|
+
.message-controls input {
|
|
171
|
+
padding: 8px 12px;
|
|
172
|
+
border: 1px solid #cbd5e0;
|
|
173
|
+
border-radius: 6px;
|
|
174
|
+
font-size: 14px;
|
|
175
|
+
margin-left: 8px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.theme-dark .theme-controls select,
|
|
179
|
+
.theme-dark .message-controls input {
|
|
180
|
+
background: #2d3748;
|
|
181
|
+
border-color: #4a5568;
|
|
182
|
+
color: #ffffff;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.theme-blue .theme-controls select,
|
|
186
|
+
.theme-blue .message-controls input {
|
|
187
|
+
background: rgba(255, 255, 255, 0.1);
|
|
188
|
+
border-color: rgba(255, 255, 255, 0.2);
|
|
189
|
+
color: #ffffff;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.message-controls input {
|
|
193
|
+
width: 250px;
|
|
194
|
+
max-width: 100%;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.history {
|
|
198
|
+
margin-top: 24px;
|
|
199
|
+
padding: 16px;
|
|
200
|
+
background: rgba(0, 0, 0, 0.05);
|
|
201
|
+
border-radius: 8px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.theme-dark .history {
|
|
205
|
+
background: rgba(255, 255, 255, 0.05);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.theme-blue .history {
|
|
209
|
+
background: rgba(255, 255, 255, 0.1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.history h4 {
|
|
213
|
+
margin: 0 0 12px 0;
|
|
214
|
+
font-size: 16px;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.history-list {
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-wrap: wrap;
|
|
220
|
+
gap: 8px;
|
|
221
|
+
margin-bottom: 12px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.history-item {
|
|
225
|
+
padding: 4px 8px;
|
|
226
|
+
background: rgba(66, 153, 225, 0.1);
|
|
227
|
+
border: 1px solid rgba(66, 153, 225, 0.2);
|
|
228
|
+
border-radius: 4px;
|
|
229
|
+
font-size: 12px;
|
|
230
|
+
font-weight: 500;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.debug-info {
|
|
234
|
+
margin-top: 24px;
|
|
235
|
+
font-size: 12px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.debug-info details {
|
|
239
|
+
border: 1px solid #e2e8f0;
|
|
240
|
+
border-radius: 6px;
|
|
241
|
+
padding: 8px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.theme-dark .debug-info details {
|
|
245
|
+
border-color: #4a5568;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.theme-blue .debug-info details {
|
|
249
|
+
border-color: rgba(255, 255, 255, 0.2);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.debug-info summary {
|
|
253
|
+
cursor: pointer;
|
|
254
|
+
font-weight: 500;
|
|
255
|
+
margin-bottom: 8px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.debug-info pre {
|
|
259
|
+
margin: 0;
|
|
260
|
+
padding: 8px;
|
|
261
|
+
background: rgba(0, 0, 0, 0.05);
|
|
262
|
+
border-radius: 4px;
|
|
263
|
+
overflow-x: auto;
|
|
264
|
+
font-size: 11px;
|
|
265
|
+
line-height: 1.4;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.theme-dark .debug-info pre {
|
|
269
|
+
background: rgba(255, 255, 255, 0.05);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.theme-blue .debug-info pre {
|
|
273
|
+
background: rgba(255, 255, 255, 0.1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* 响应式设计 */
|
|
277
|
+
@media (max-width: 600px) {
|
|
278
|
+
.reactive-counter {
|
|
279
|
+
margin: 10px;
|
|
280
|
+
padding: 16px;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.count-value {
|
|
284
|
+
font-size: 36px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.controls {
|
|
288
|
+
flex-direction: column;
|
|
289
|
+
align-items: center;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.btn {
|
|
293
|
+
width: 200px;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.step-controls label {
|
|
297
|
+
flex-direction: column;
|
|
298
|
+
gap: 8px;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.message-controls input {
|
|
302
|
+
width: 100%;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/** @jsxImportSource @wsxjs/wsx-core */
|
|
2
|
+
/**
|
|
3
|
+
* 响应式计数器组件 - 展示 WSX 响应式状态系统
|
|
4
|
+
*
|
|
5
|
+
* 这个组件演示了:
|
|
6
|
+
* - 自动重渲染:状态变化时无需手动调用 rerender()
|
|
7
|
+
* - 批量更新:多个状态变化会被合并为一次重渲染
|
|
8
|
+
* - 原生性能:基于浏览器 Proxy API,零运行时开销
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ReactiveWebComponent, autoRegister, createLogger } from "@wsxjs/wsx-core";
|
|
12
|
+
import styles from "./ReactiveCounter.css?inline";
|
|
13
|
+
|
|
14
|
+
const logger = createLogger("ReactiveCounter");
|
|
15
|
+
|
|
16
|
+
@autoRegister({ tagName: "reactive-counter" })
|
|
17
|
+
export default class ReactiveCounter extends ReactiveWebComponent {
|
|
18
|
+
// 使用响应式对象 - 任何属性变化都会自动触发重渲染
|
|
19
|
+
private state = this.reactive({
|
|
20
|
+
count: 0,
|
|
21
|
+
step: 1,
|
|
22
|
+
message: "Hello WSX Reactive!",
|
|
23
|
+
isRunning: false,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// 使用 useState API - 类似 React 的 useState
|
|
27
|
+
private themeState = this.useState("theme", "light");
|
|
28
|
+
private historyState = this.useState("history", [] as number[]);
|
|
29
|
+
|
|
30
|
+
private getTheme = this.themeState[0];
|
|
31
|
+
private setTheme = this.themeState[1];
|
|
32
|
+
private getHistory = this.historyState[0];
|
|
33
|
+
private setHistory = this.historyState[1];
|
|
34
|
+
|
|
35
|
+
constructor() {
|
|
36
|
+
super({
|
|
37
|
+
styles,
|
|
38
|
+
debug: true, // 启用调试模式
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
logger.info("ReactiveCounter initialized");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
render() {
|
|
45
|
+
const theme = this.getTheme();
|
|
46
|
+
const history = this.getHistory();
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div class={`reactive-counter theme-${theme}`}>
|
|
50
|
+
<div class="header">
|
|
51
|
+
<h3>🔄 Reactive Counter</h3>
|
|
52
|
+
<p class="subtitle">{this.state.message}</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="counter-display">
|
|
56
|
+
<div class="count-value">{this.state.count}</div>
|
|
57
|
+
<div class="step-info">Step: {this.state.step}</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="controls">
|
|
61
|
+
<button
|
|
62
|
+
class="btn btn-primary"
|
|
63
|
+
onClick={this.increment}
|
|
64
|
+
disabled={this.state.isRunning}
|
|
65
|
+
>
|
|
66
|
+
+{this.state.step}
|
|
67
|
+
</button>
|
|
68
|
+
|
|
69
|
+
<button
|
|
70
|
+
class="btn btn-secondary"
|
|
71
|
+
onClick={this.decrement}
|
|
72
|
+
disabled={this.state.isRunning}
|
|
73
|
+
>
|
|
74
|
+
-{this.state.step}
|
|
75
|
+
</button>
|
|
76
|
+
|
|
77
|
+
<button
|
|
78
|
+
class="btn btn-warning"
|
|
79
|
+
onClick={this.reset}
|
|
80
|
+
disabled={this.state.isRunning}
|
|
81
|
+
>
|
|
82
|
+
Reset
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="step-controls">
|
|
87
|
+
<label>
|
|
88
|
+
Step Size:
|
|
89
|
+
<input
|
|
90
|
+
type="range"
|
|
91
|
+
min="1"
|
|
92
|
+
max="10"
|
|
93
|
+
value={this.state.step}
|
|
94
|
+
onInput={this.handleStepChange}
|
|
95
|
+
/>
|
|
96
|
+
<span>{this.state.step}</span>
|
|
97
|
+
</label>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div class="auto-controls">
|
|
101
|
+
<button
|
|
102
|
+
class={`btn ${this.state.isRunning ? "btn-danger" : "btn-success"}`}
|
|
103
|
+
onClick={this.toggleAutoIncrement}
|
|
104
|
+
>
|
|
105
|
+
{this.state.isRunning ? "Stop" : "Start"} Auto Increment
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div class="theme-controls">
|
|
110
|
+
<label>
|
|
111
|
+
Theme:
|
|
112
|
+
<select value={theme} onChange={this.handleThemeChange}>
|
|
113
|
+
<option value="light">Light</option>
|
|
114
|
+
<option value="dark">Dark</option>
|
|
115
|
+
<option value="blue">Blue</option>
|
|
116
|
+
</select>
|
|
117
|
+
</label>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div class="message-controls">
|
|
121
|
+
<input
|
|
122
|
+
type="text"
|
|
123
|
+
value={this.state.message}
|
|
124
|
+
onInput={this.handleMessageChange}
|
|
125
|
+
placeholder="Enter a message..."
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{history.length > 0 && (
|
|
130
|
+
<div class="history">
|
|
131
|
+
<h4>History (last 10):</h4>
|
|
132
|
+
<div class="history-list">
|
|
133
|
+
{history.slice(-10).map((value, index) => (
|
|
134
|
+
<span key={index} class="history-item">
|
|
135
|
+
{value}
|
|
136
|
+
</span>
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
<button class="btn btn-sm" onClick={this.clearHistory}>
|
|
140
|
+
Clear History
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
<div class="debug-info">
|
|
146
|
+
<details>
|
|
147
|
+
<summary>Debug Info</summary>
|
|
148
|
+
<pre>
|
|
149
|
+
{JSON.stringify(
|
|
150
|
+
{
|
|
151
|
+
state: this.state,
|
|
152
|
+
theme: this.getTheme(),
|
|
153
|
+
historyLength: this.getHistory().length,
|
|
154
|
+
stateSnapshot: this.getStateSnapshot(),
|
|
155
|
+
},
|
|
156
|
+
null,
|
|
157
|
+
2
|
|
158
|
+
)}
|
|
159
|
+
</pre>
|
|
160
|
+
</details>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 事件处理方法 - 直接修改状态,自动触发重渲染
|
|
167
|
+
private increment = () => {
|
|
168
|
+
this.state.count += this.state.step;
|
|
169
|
+
this.addToHistory(this.state.count);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
private decrement = () => {
|
|
173
|
+
this.state.count -= this.state.step;
|
|
174
|
+
this.addToHistory(this.state.count);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
private reset = () => {
|
|
178
|
+
this.state.count = 0;
|
|
179
|
+
this.addToHistory(0);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
private handleStepChange = (event: Event) => {
|
|
183
|
+
const input = event.target as HTMLInputElement;
|
|
184
|
+
this.state.step = parseInt(input.value);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
private handleThemeChange = (event: Event) => {
|
|
188
|
+
const select = event.target as HTMLSelectElement;
|
|
189
|
+
this.setTheme(select.value);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
private handleMessageChange = (event: Event) => {
|
|
193
|
+
const input = event.target as HTMLInputElement;
|
|
194
|
+
this.state.message = input.value;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
private toggleAutoIncrement = () => {
|
|
198
|
+
this.state.isRunning = !this.state.isRunning;
|
|
199
|
+
|
|
200
|
+
if (this.state.isRunning) {
|
|
201
|
+
this.startAutoIncrement();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
private startAutoIncrement() {
|
|
206
|
+
const interval = setInterval(() => {
|
|
207
|
+
if (!this.state.isRunning) {
|
|
208
|
+
clearInterval(interval);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.increment();
|
|
213
|
+
|
|
214
|
+
// 演示批量更新:这些变化会被合并为一次重渲染
|
|
215
|
+
if (this.state.count % 5 === 0) {
|
|
216
|
+
this.state.message = `Count reached ${this.state.count}!`;
|
|
217
|
+
}
|
|
218
|
+
}, 200);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private addToHistory(value: number) {
|
|
222
|
+
const history = this.getHistory();
|
|
223
|
+
this.setHistory([...history, value]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private clearHistory = () => {
|
|
227
|
+
this.setHistory([]);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
protected onConnected(): void {
|
|
231
|
+
logger.info("ReactiveCounter connected to DOM");
|
|
232
|
+
|
|
233
|
+
// 组件挂载时显示欢迎消息
|
|
234
|
+
setTimeout(() => {
|
|
235
|
+
this.state.message = "Ready to count! 🚀";
|
|
236
|
+
}, 500);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
protected onDisconnected(): void {
|
|
240
|
+
logger.info("ReactiveCounter disconnected from DOM");
|
|
241
|
+
// 清理定时器等资源
|
|
242
|
+
this.state.isRunning = false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** @jsxImportSource @wsxjs/wsx-core */
|
|
2
|
+
/**
|
|
3
|
+
* 简单的响应式演示组件
|
|
4
|
+
* 展示 WSX 响应式状态系统的基本功能
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ReactiveWebComponent, autoRegister, createLogger } from "@wsxjs/wsx-core";
|
|
8
|
+
|
|
9
|
+
const logger = createLogger("SimpleReactiveDemo");
|
|
10
|
+
|
|
11
|
+
@autoRegister({ tagName: "simple-reactive-demo" })
|
|
12
|
+
export default class SimpleReactiveDemo extends ReactiveWebComponent {
|
|
13
|
+
// 响应式状态 - 自动触发重渲染
|
|
14
|
+
private state = this.reactive({
|
|
15
|
+
count: 0,
|
|
16
|
+
message: "Click the button!",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
super();
|
|
21
|
+
logger.info("SimpleReactiveDemo initialized");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
render() {
|
|
25
|
+
return (
|
|
26
|
+
<div style="padding: 20px; border: 1px solid #ccc; border-radius: 8px; margin: 20px;">
|
|
27
|
+
<h3>📱 Simple Reactive Demo</h3>
|
|
28
|
+
<p>{this.state.message}</p>
|
|
29
|
+
<div style="font-size: 24px; margin: 16px 0;">
|
|
30
|
+
Count: <strong>{this.state.count}</strong>
|
|
31
|
+
</div>
|
|
32
|
+
<div>
|
|
33
|
+
<button onClick={this.increment} style="margin-right: 8px; padding: 8px 16px;">
|
|
34
|
+
+ Increment
|
|
35
|
+
</button>
|
|
36
|
+
<button onClick={this.reset} style="padding: 8px 16px;">
|
|
37
|
+
🔄 Reset
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
<div style="margin-top: 16px; font-size: 12px; color: #666;">
|
|
41
|
+
💡 State changes automatically trigger re-renders!
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private increment = () => {
|
|
48
|
+
this.state.count++;
|
|
49
|
+
this.state.message = `Clicked ${this.state.count} time${this.state.count === 1 ? "" : "s"}!`;
|
|
50
|
+
// 注意:不需要手动调用 this.rerender()!
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
private reset = () => {
|
|
54
|
+
this.state.count = 0;
|
|
55
|
+
this.state.message = "Reset! Click again!";
|
|
56
|
+
// 注意:这里也不需要手动调用 this.rerender()!
|
|
57
|
+
};
|
|
58
|
+
}
|