nv-radio-btn-bw 1.0.0

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.
@@ -0,0 +1,82 @@
1
+ var nvradiobtnbw=(()=>{var b=(s,t)=>()=>(t||s((t={exports:{}}).exports,t),t.exports);var u=b((p,h)=>{var i=class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this._width="60px",this._height="60px",this._borderColor="#cccccc",this._innerColor="#007bff",this._isSelected=!0,this._cb=async t=>{},this._render()}static get observedAttributes(){return["width","height","border-color","inner-color"]}attributeChangedCallback(t,e,r){if(e!==r)switch(t){case"width":this._width=r,this._updateSize();break;case"height":this._height=r,this._updateSize();break;case"border-color":this._borderColor=r,this._updateBorderColor();break;case"inner-color":this._innerColor=r,this._updateInnerColor();break}}connectedCallback(){this.shadowRoot.querySelector(".nv-radio-btn").addEventListener("click",this._handleClick.bind(this))}disconnectedCallback(){this.shadowRoot.querySelector(".nv-radio-btn").removeEventListener("click",this._handleClick)}_render(){let t=document.createElement("template");t.innerHTML=`
2
+ <style>
3
+ .nv-radio-btn {
4
+ width: ${this._width};
5
+ height: ${this._height};
6
+ border-radius: 50%;
7
+ border: 3px solid ${this._borderColor};
8
+ background: white;
9
+ cursor: pointer;
10
+ position: relative;
11
+ padding: 0;
12
+ margin: 0;
13
+ transition: all 0.2s ease;
14
+ }
15
+
16
+ .nv-radio-btn::after {
17
+ content: '';
18
+ position: absolute;
19
+ top: 50%;
20
+ left: 50%;
21
+ transform: translate(-50%, -50%);
22
+ width: ${this._getInnerSize(this._width)};
23
+ height: ${this._getInnerSize(this._height)};
24
+ border-radius: 50%;
25
+ background: ${this._innerColor};
26
+ }
27
+
28
+ .nv-radio-btn.nv-radio-btn-unselected::after {
29
+ background: transparent;
30
+ }
31
+
32
+ .nv-radio-btn:hover {
33
+ border-color: ${this._getDarkerColor(this._borderColor)};
34
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
35
+ }
36
+
37
+ .nv-radio-btn:active {
38
+ transform: scale(0.95);
39
+ }
40
+ </style>
41
+
42
+ <button class="nv-radio-btn ${this._isSelected?"":"nv-radio-btn-unselected"}" aria-label="\u5355\u9009\u6309\u94AE">
43
+ </button>
44
+ `,this.shadowRoot.appendChild(t.content.cloneNode(!0))}_getInnerSize(t){let e=this._getNumber(t),r=this._getUnit(t);return`${e*.67}${r}`}_getNumber(t){let e=t.match(/\d+/);return e?parseInt(e[0],10):60}_getUnit(t){let e=t.match(/[a-zA-Z%]+/);return e?e[0]:"px"}_updateSize(){let t=this.shadowRoot.querySelector(".nv-radio-btn");if(!t)return;t.style.width=this._width,t.style.height=this._height;let e=this.shadowRoot.querySelector("style");if(e){let r=this._getInnerSize(this._width),o=this._getInnerSize(this._height);e.textContent=`
45
+ .nv-radio-btn {
46
+ width: ${this._width};
47
+ height: ${this._height};
48
+ border-radius: 50%;
49
+ border: 3px solid ${this._borderColor};
50
+ background: white;
51
+ cursor: pointer;
52
+ position: relative;
53
+ padding: 0;
54
+ margin: 0;
55
+ transition: all 0.2s ease;
56
+ }
57
+
58
+ .nv-radio-btn::after {
59
+ content: '';
60
+ position: absolute;
61
+ top: 50%;
62
+ left: 50%;
63
+ transform: translate(-50%, -50%);
64
+ width: ${r};
65
+ height: ${o};
66
+ border-radius: 50%;
67
+ background: ${this._innerColor};
68
+ }
69
+
70
+ .nv-radio-btn.nv-radio-btn-unselected::after {
71
+ background: transparent;
72
+ }
73
+
74
+ .nv-radio-btn:hover {
75
+ border-color: ${this._getDarkerColor(this._borderColor)};
76
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
77
+ }
78
+
79
+ .nv-radio-btn:active {
80
+ transform: scale(0.95);
81
+ }
82
+ `}}_updateBorderColor(){let t=this.shadowRoot.querySelector(".nv-radio-btn");if(!t)return;t.style.borderColor=this._borderColor;let e=this.shadowRoot.querySelector("style");e&&(e.textContent=e.textContent.replace(/border: 3px solid #[^;]+;/,`border: 3px solid ${this._borderColor};`).replace(/border-color: [^;]+;/,`border-color: ${this._getDarkerColor(this._borderColor)};`))}_updateInnerColor(){if(!this.shadowRoot.querySelector(".nv-radio-btn"))return;let e=this.shadowRoot.querySelector("style");e&&(e.textContent=e.textContent.replace(/background: #[^;]+;/,`background: ${this._innerColor};`))}_getDarkerColor(t){let e=t.replace("#","");if(e.length!==6)return t;let r=parseInt(e.substr(0,2),16),o=parseInt(e.substr(2,2),16),a=parseInt(e.substr(4,2),16),n=.8,d=Math.floor(r*n),l=Math.floor(o*n),c=Math.floor(a*n);return`rgb(${d}, ${l}, ${c})`}async _handleClick(t){let e=new CustomEvent("nv-radio-btn-click",{bubbles:!0,composed:!0,detail:{timestamp:Date.now(),element:this,selected:this._isSelected}});this.dispatchEvent(e);let r=this.shadowRoot.querySelector(".nv-radio-btn");r&&(r.style.transform="scale(0.9)");try{await this._cb(this)}catch(o){console.error("\u56DE\u8C03\u51FD\u6570\u6267\u884C\u51FA\u9519:",o)}r&&(r.style.transform="")}setCallback(t){typeof t=="function"&&(this._cb=t)}slct(){if(this._isSelected)return;this._isSelected=!0;let t=this.shadowRoot.querySelector(".nv-radio-btn");t&&t.classList.remove("nv-radio-btn-unselected");let e=new CustomEvent("nv-radio-btn-select",{bubbles:!0,composed:!0,detail:{timestamp:Date.now(),element:this}});this.dispatchEvent(e)}unslct(){if(!this._isSelected)return;this._isSelected=!1;let t=this.shadowRoot.querySelector(".nv-radio-btn");t&&t.classList.add("nv-radio-btn-unselected");let e=new CustomEvent("nv-radio-btn-unselect",{bubbles:!0,composed:!0,detail:{timestamp:Date.now(),element:this}});this.dispatchEvent(e)}toggle(){this._isSelected?this.unslct():this.slct()}setWidth(t){this._width=t,this._updateSize()}setHeight(t){this._height=t,this._updateSize()}setBorderColor(t){this._borderColor=t,this._updateBorderColor()}setInnerColor(t){this._innerColor=t,this._updateInnerColor()}getState(){return{width:this._width,height:this._height,borderColor:this._borderColor,innerColor:this._innerColor,selected:this._isSelected}}};customElements.get("nv-radio-button")||customElements.define("nv-radio-button",i);h.exports={NvRadioButton:i}});return u();})();
package/TEST/tst.html ADDED
@@ -0,0 +1,510 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>自定义单选按钮元素</title>
7
+ <script>
8
+ // 定义自定义元素类
9
+ class NvRadioButton extends HTMLElement {
10
+ constructor() {
11
+ super();
12
+
13
+ // 创建Shadow DOM
14
+ this.attachShadow({ mode: 'open' });
15
+
16
+ // 默认属性
17
+ this._width = '60px';
18
+ this._height = '60px';
19
+ this._borderColor = '#cccccc';
20
+ this._innerColor = '#007bff';
21
+ this._isSelected = true;
22
+ this._cb = async (self) => {};
23
+
24
+ // 初始化渲染
25
+ this._render();
26
+ }
27
+
28
+ // 监听属性变化
29
+ static get observedAttributes() {
30
+ return ['width', 'height', 'border-color', 'inner-color'];
31
+ }
32
+
33
+ // 属性变化回调
34
+ attributeChangedCallback(name, oldValue, newValue) {
35
+ if (oldValue === newValue) return;
36
+
37
+ switch(name) {
38
+ case 'width':
39
+ this._width = newValue;
40
+ this._updateSize();
41
+ break;
42
+ case 'height':
43
+ this._height = newValue;
44
+ this._updateSize();
45
+ break;
46
+ case 'border-color':
47
+ this._borderColor = newValue;
48
+ this._updateBorderColor();
49
+ break;
50
+ case 'inner-color':
51
+ this._innerColor = newValue;
52
+ this._updateInnerColor();
53
+ break;
54
+ }
55
+ }
56
+
57
+ // 连接回调
58
+ connectedCallback() {
59
+ this.shadowRoot.querySelector('.nv-radio-btn').addEventListener('click', this._handleClick.bind(this));
60
+ }
61
+
62
+ // 断开连接回调
63
+ disconnectedCallback() {
64
+ this.shadowRoot.querySelector('.nv-radio-btn').removeEventListener('click', this._handleClick);
65
+ }
66
+
67
+ // 渲染方法
68
+ _render() {
69
+ const template = document.createElement('template');
70
+ template.innerHTML = `
71
+ <style>
72
+ .nv-radio-btn {
73
+ width: ${this._width};
74
+ height: ${this._height};
75
+ border-radius: 50%;
76
+ border: 3px solid ${this._borderColor};
77
+ background: white;
78
+ cursor: pointer;
79
+ position: relative;
80
+ padding: 0;
81
+ margin: 0;
82
+ transition: all 0.2s ease;
83
+ }
84
+
85
+ .nv-radio-btn::after {
86
+ content: '';
87
+ position: absolute;
88
+ top: 50%;
89
+ left: 50%;
90
+ transform: translate(-50%, -50%);
91
+ width: ${this._getInnerSize(this._width)};
92
+ height: ${this._getInnerSize(this._height)};
93
+ border-radius: 50%;
94
+ background: ${this._innerColor};
95
+ }
96
+
97
+ .nv-radio-btn.nv-radio-btn-unselected::after {
98
+ background: transparent;
99
+ }
100
+
101
+ .nv-radio-btn:hover {
102
+ border-color: ${this._getDarkerColor(this._borderColor)};
103
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
104
+ }
105
+
106
+ .nv-radio-btn:active {
107
+ transform: scale(0.95);
108
+ }
109
+ </style>
110
+
111
+ <button class="nv-radio-btn ${this._isSelected ? '' : 'nv-radio-btn-unselected'}" aria-label="单选按钮">
112
+ </button>
113
+ `;
114
+
115
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
116
+ }
117
+
118
+ // 获取内部尺寸
119
+ _getInnerSize(value) {
120
+ const num = this._getNumber(value);
121
+ const unit = this._getUnit(value);
122
+ return `${num * 0.67}${unit}`;
123
+ }
124
+
125
+ // 获取数字
126
+ _getNumber(value) {
127
+ const match = value.match(/\d+/);
128
+ return match ? parseInt(match[0], 10) : 60;
129
+ }
130
+
131
+ // 获取单位
132
+ _getUnit(value) {
133
+ const match = value.match(/[a-zA-Z%]+/);
134
+ return match ? match[0] : 'px';
135
+ }
136
+
137
+ // 更新尺寸
138
+ _updateSize() {
139
+ const button = this.shadowRoot.querySelector('.nv-radio-btn');
140
+ if (!button) return;
141
+
142
+ button.style.width = this._width;
143
+ button.style.height = this._height;
144
+
145
+ const styleElement = this.shadowRoot.querySelector('style');
146
+ if (styleElement) {
147
+ const innerWidth = this._getInnerSize(this._width);
148
+ const innerHeight = this._getInnerSize(this._height);
149
+
150
+ styleElement.textContent = `
151
+ .nv-radio-btn {
152
+ width: ${this._width};
153
+ height: ${this._height};
154
+ border-radius: 50%;
155
+ border: 3px solid ${this._borderColor};
156
+ background: white;
157
+ cursor: pointer;
158
+ position: relative;
159
+ padding: 0;
160
+ margin: 0;
161
+ transition: all 0.2s ease;
162
+ }
163
+
164
+ .nv-radio-btn::after {
165
+ content: '';
166
+ position: absolute;
167
+ top: 50%;
168
+ left: 50%;
169
+ transform: translate(-50%, -50%);
170
+ width: ${innerWidth};
171
+ height: ${innerHeight};
172
+ border-radius: 50%;
173
+ background: ${this._innerColor};
174
+ }
175
+
176
+ .nv-radio-btn.nv-radio-btn-unselected::after {
177
+ background: transparent;
178
+ }
179
+
180
+ .nv-radio-btn:hover {
181
+ border-color: ${this._getDarkerColor(this._borderColor)};
182
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
183
+ }
184
+
185
+ .nv-radio-btn:active {
186
+ transform: scale(0.95);
187
+ }
188
+ `;
189
+ }
190
+ }
191
+
192
+ // 更新边框颜色
193
+ _updateBorderColor() {
194
+ const button = this.shadowRoot.querySelector('.nv-radio-btn');
195
+ if (!button) return;
196
+
197
+ button.style.borderColor = this._borderColor;
198
+
199
+ const styleElement = this.shadowRoot.querySelector('style');
200
+ if (styleElement) {
201
+ styleElement.textContent = styleElement.textContent.replace(
202
+ /border: 3px solid #[^;]+;/,
203
+ `border: 3px solid ${this._borderColor};`
204
+ ).replace(
205
+ /border-color: [^;]+;/,
206
+ `border-color: ${this._getDarkerColor(this._borderColor)};`
207
+ );
208
+ }
209
+ }
210
+
211
+ // 更新内部颜色
212
+ _updateInnerColor() {
213
+ const button = this.shadowRoot.querySelector('.nv-radio-btn');
214
+ if (!button) return;
215
+
216
+ const styleElement = this.shadowRoot.querySelector('style');
217
+ if (styleElement) {
218
+ styleElement.textContent = styleElement.textContent.replace(
219
+ /background: #[^;]+;/,
220
+ `background: ${this._innerColor};`
221
+ );
222
+ }
223
+ }
224
+
225
+ // 获取更深色
226
+ _getDarkerColor(color) {
227
+ const hex = color.replace('#', '');
228
+ if (hex.length !== 6) return color;
229
+
230
+ const r = parseInt(hex.substr(0, 2), 16);
231
+ const g = parseInt(hex.substr(2, 2), 16);
232
+ const b = parseInt(hex.substr(4, 2), 16);
233
+
234
+ const darken = 0.8;
235
+ const newR = Math.floor(r * darken);
236
+ const newG = Math.floor(g * darken);
237
+ const newB = Math.floor(b * darken);
238
+
239
+ return `rgb(${newR}, ${newG}, ${newB})`;
240
+ }
241
+
242
+ // 点击处理
243
+ async _handleClick(event) {
244
+ const clickEvent = new CustomEvent('nv-radio-btn-click', {
245
+ bubbles: true,
246
+ composed: true,
247
+ detail: {
248
+ timestamp: Date.now(),
249
+ element: this,
250
+ selected: this._isSelected
251
+ }
252
+ });
253
+ this.dispatchEvent(clickEvent);
254
+
255
+ const button = this.shadowRoot.querySelector('.nv-radio-btn');
256
+ if (button) {
257
+ button.style.transform = 'scale(0.9)';
258
+ }
259
+
260
+ try {
261
+ await this._cb(this);
262
+ } catch (error) {
263
+ console.error('回调函数执行出错:', error);
264
+ }
265
+
266
+ if (button) {
267
+ button.style.transform = '';
268
+ }
269
+ }
270
+
271
+ // 设置回调函数
272
+ setCallback(cb) {
273
+ if (typeof cb === 'function') {
274
+ this._cb = cb;
275
+ }
276
+ }
277
+
278
+ // 显示中心颜色
279
+ slct() {
280
+ if (this._isSelected) return;
281
+
282
+ this._isSelected = true;
283
+ const button = this.shadowRoot.querySelector('.nv-radio-btn');
284
+ if (button) {
285
+ button.classList.remove('nv-radio-btn-unselected');
286
+ }
287
+
288
+ const selectEvent = new CustomEvent('nv-radio-btn-select', {
289
+ bubbles: true,
290
+ composed: true,
291
+ detail: {
292
+ timestamp: Date.now(),
293
+ element: this
294
+ }
295
+ });
296
+ this.dispatchEvent(selectEvent);
297
+ }
298
+
299
+ // 隐藏中心颜色
300
+ unslct() {
301
+ if (!this._isSelected) return;
302
+
303
+ this._isSelected = false;
304
+ const button = this.shadowRoot.querySelector('.nv-radio-btn');
305
+ if (button) {
306
+ button.classList.add('nv-radio-btn-unselected');
307
+ }
308
+
309
+ const unselectEvent = new CustomEvent('nv-radio-btn-unselect', {
310
+ bubbles: true,
311
+ composed: true,
312
+ detail: {
313
+ timestamp: Date.now(),
314
+ element: this
315
+ }
316
+ });
317
+ this.dispatchEvent(unselectEvent);
318
+ }
319
+
320
+ // 切换选中状态
321
+ toggle() {
322
+ if (this._isSelected) {
323
+ this.unslct();
324
+ } else {
325
+ this.slct();
326
+ }
327
+ }
328
+
329
+ // 设置宽度
330
+ setWidth(width) {
331
+ this._width = width;
332
+ this._updateSize();
333
+ }
334
+
335
+ // 设置高度
336
+ setHeight(height) {
337
+ this._height = height;
338
+ this._updateSize();
339
+ }
340
+
341
+ // 设置边框颜色
342
+ setBorderColor(color) {
343
+ this._borderColor = color;
344
+ this._updateBorderColor();
345
+ }
346
+
347
+ // 设置内部颜色
348
+ setInnerColor(color) {
349
+ this._innerColor = color;
350
+ this._updateInnerColor();
351
+ }
352
+
353
+ // 获取当前状态
354
+ getState() {
355
+ return {
356
+ width: this._width,
357
+ height: this._height,
358
+ borderColor: this._borderColor,
359
+ innerColor: this._innerColor,
360
+ selected: this._isSelected
361
+ };
362
+ }
363
+ }
364
+
365
+ // 注册自定义元素
366
+ if (!customElements.get('nv-radio-button')) {
367
+ customElements.define('nv-radio-button', NvRadioButton);
368
+ }
369
+ </script>
370
+ <style>
371
+ body {
372
+ font-family: Arial, sans-serif;
373
+ padding: 20px;
374
+ background: #f5f5f5;
375
+ }
376
+
377
+ .demo-container {
378
+ max-width: 500px;
379
+ margin: 0 auto;
380
+ background: white;
381
+ padding: 20px;
382
+ border-radius: 10px;
383
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
384
+ }
385
+
386
+ h1 {
387
+ color: #333;
388
+ margin-bottom: 20px;
389
+ font-size: 24px;
390
+ }
391
+
392
+ .demo-section {
393
+ margin-bottom: 20px;
394
+ padding: 15px;
395
+ border: 1px solid #eee;
396
+ border-radius: 8px;
397
+ }
398
+
399
+ .demo-section h2 {
400
+ color: #555;
401
+ margin-bottom: 10px;
402
+ font-size: 18px;
403
+ }
404
+
405
+ .button-group {
406
+ display: flex;
407
+ gap: 10px;
408
+ align-items: center;
409
+ flex-wrap: wrap;
410
+ margin-bottom: 10px;
411
+ }
412
+
413
+ .log-output {
414
+ margin-top: 10px;
415
+ padding: 8px;
416
+ background: #2c3e50;
417
+ color: white;
418
+ border-radius: 4px;
419
+ font-family: monospace;
420
+ font-size: 12px;
421
+ max-height: 200px;
422
+ overflow-y: auto;
423
+ }
424
+ </style>
425
+ </head>
426
+ <body>
427
+ <div class="demo-container">
428
+ <h1>自定义单选按钮元素(修复版)</h1>
429
+
430
+ <div class="demo-section">
431
+ <h2>基本测试</h2>
432
+ <div class="button-group">
433
+ <nv-radio-button id="btn1"></nv-radio-button>
434
+ <div>
435
+ <div>默认按钮 (60px × 60px)</div>
436
+ <div>当前状态: <span id="statusText">选中</span></div>
437
+ </div>
438
+ </div>
439
+
440
+ <div class="button-group">
441
+ <button onclick="testSelect()">选中(slct)</button>
442
+ <button onclick="testUnselect()">取消选中(unslct)</button>
443
+ <button onclick="testToggle()">切换(toggle)</button>
444
+ <button onclick="changeColor()">改变内部颜色</button>
445
+ </div>
446
+
447
+ <div id="log" class="log-output"></div>
448
+ </div>
449
+ </div>
450
+
451
+ <script>
452
+ // 日志函数
453
+ function log(message) {
454
+ const logElement = document.getElementById('log');
455
+ const timestamp = new Date().toLocaleTimeString();
456
+ logElement.innerHTML += `[${timestamp}] ${message}<br>`;
457
+ logElement.scrollTop = logElement.scrollHeight;
458
+ }
459
+
460
+ // 获取按钮实例
461
+ const button = document.getElementById('btn1');
462
+
463
+ // 设置回调函数
464
+ button.setCallback(async (self) => {
465
+ log('回调函数被调用');
466
+ });
467
+
468
+ // 监听事件
469
+ button.addEventListener('nv-radio-btn-select', (e) => {
470
+ log('按钮被选中');
471
+ document.getElementById('statusText').textContent = '选中';
472
+ });
473
+
474
+ button.addEventListener('nv-radio-btn-unselect', (e) => {
475
+ log('按钮被取消选中');
476
+ document.getElementById('statusText').textContent = '未选中';
477
+ });
478
+
479
+ // 测试函数
480
+ function testSelect() {
481
+ log('执行了选中操作');
482
+ button.slct();
483
+ }
484
+
485
+ function testUnselect() {
486
+ log('执行了取消选中操作');
487
+ button.unslct();
488
+ }
489
+
490
+ function testToggle() {
491
+ log('执行了切换操作');
492
+ button.toggle();
493
+ }
494
+
495
+ function changeColor() {
496
+ const colors = ['#ff0000', '#00ff00', '#0000ff', '#ff00ff', '#ffff00', '#00ffff'];
497
+ const randomColor = colors[Math.floor(Math.random() * colors.length)];
498
+ button.setInnerColor(randomColor);
499
+ log('改变了内部颜色: ' + randomColor);
500
+ }
501
+
502
+ // 初始状态检查
503
+ setTimeout(() => {
504
+ const state = button.getState();
505
+ log('初始状态: 选中=' + state.selected + ', 颜色=' + state.innerColor);
506
+ }, 100);
507
+ </script>
508
+ </body>
509
+ </html>
510
+
package/com.sh ADDED
@@ -0,0 +1 @@
1
+ nv_cli_build -m -g nvradiobtnbw -i index.js -o ./DIST/nv-radio-btn-bw.js
package/index.d.ts ADDED
@@ -0,0 +1,308 @@
1
+ /**
2
+ * 自定义单选按钮元素的 TypeScript 类型声明
3
+ * 对应自定义元素: <nv-radio-button>
4
+ */
5
+
6
+ /**
7
+ * 单选按钮事件详情接口
8
+ */
9
+ interface NvRadioButtonEventDetail {
10
+ /** 事件时间戳 */
11
+ timestamp: number;
12
+ /** 事件源元素 */
13
+ element: NvRadioButtonElement;
14
+ /** 是否选中(仅点击事件有) */
15
+ selected?: boolean;
16
+ }
17
+
18
+ /**
19
+ * 单选按钮点击事件
20
+ */
21
+ interface NvRadioButtonClickEvent extends CustomEvent<NvRadioButtonEventDetail> {
22
+ /** 事件目标 */
23
+ readonly target: NvRadioButtonElement;
24
+ }
25
+
26
+ /**
27
+ * 单选按钮选中事件
28
+ */
29
+ interface NvRadioButtonSelectEvent extends CustomEvent<NvRadioButtonEventDetail> {
30
+ /** 事件目标 */
31
+ readonly target: NvRadioButtonElement;
32
+ }
33
+
34
+ /**
35
+ * 单选按钮取消选中事件
36
+ */
37
+ interface NvRadioButtonUnselectEvent extends CustomEvent<NvRadioButtonEventDetail> {
38
+ /** 事件目标 */
39
+ readonly target: NvRadioButtonElement;
40
+ }
41
+
42
+ /**
43
+ * 单选按钮状态接口
44
+ */
45
+ interface NvRadioButtonState {
46
+ /** 宽度 */
47
+ width: string;
48
+ /** 高度 */
49
+ height: string;
50
+ /** 边框颜色 */
51
+ borderColor: string;
52
+ /** 内部颜色 */
53
+ innerColor: string;
54
+ /** 是否选中 */
55
+ selected: boolean;
56
+ }
57
+
58
+ /**
59
+ * 单选按钮元素接口
60
+ */
61
+ interface NvRadioButtonElement extends HTMLElement {
62
+ /**
63
+ * 按钮宽度
64
+ * @example "60px"
65
+ * @example "3rem"
66
+ * @example "100%"
67
+ */
68
+ width: string;
69
+
70
+ /**
71
+ * 按钮高度
72
+ * @example "60px"
73
+ * @example "3rem"
74
+ * @example "100%"
75
+ */
76
+ height: string;
77
+
78
+ /**
79
+ * 边框颜色
80
+ * @example "#cccccc"
81
+ * @example "rgb(204, 204, 204)"
82
+ */
83
+ borderColor: string;
84
+
85
+ /**
86
+ * 内部颜色
87
+ * @example "#007bff"
88
+ * @example "rgb(0, 123, 255)"
89
+ */
90
+ innerColor: string;
91
+
92
+ /**
93
+ * 设置回调函数
94
+ * @param cb 回调函数,参数为按钮实例
95
+ */
96
+ setCallback(cb: (self: NvRadioButtonElement) => Promise<void> | void): void;
97
+
98
+ /**
99
+ * 显示中心颜色
100
+ */
101
+ slct(): void;
102
+
103
+ /**
104
+ * 隐藏中心颜色
105
+ */
106
+ unslct(): void;
107
+
108
+ /**
109
+ * 切换选中状态
110
+ */
111
+ toggle(): void;
112
+
113
+ /**
114
+ * 设置宽度
115
+ * @param width 新的宽度
116
+ */
117
+ setWidth(width: string): void;
118
+
119
+ /**
120
+ * 设置高度
121
+ * @param height 新的高度
122
+ */
123
+ setHeight(height: string): void;
124
+
125
+ /**
126
+ * 设置边框颜色
127
+ * @param color 新的边框颜色
128
+ */
129
+ setBorderColor(color: string): void;
130
+
131
+ /**
132
+ * 设置内部颜色
133
+ * @param color 新的内部颜色
134
+ */
135
+ setInnerColor(color: string): void;
136
+
137
+ /**
138
+ * 获取当前状态
139
+ * @returns 包含按钮状态的对象
140
+ */
141
+ getState(): NvRadioButtonState;
142
+
143
+ /**
144
+ * 点击事件监听器
145
+ * @param type 事件类型: "nv-radio-btn-click"
146
+ * @param listener 事件监听函数
147
+ * @param options 事件监听选项
148
+ */
149
+ addEventListener(
150
+ type: "nv-radio-btn-click",
151
+ listener: (this: NvRadioButtonElement, ev: NvRadioButtonClickEvent) => any,
152
+ options?: boolean | AddEventListenerOptions
153
+ ): void;
154
+
155
+ /**
156
+ * 选中事件监听器
157
+ * @param type 事件类型: "nv-radio-btn-select"
158
+ * @param listener 事件监听函数
159
+ * @param options 事件监听选项
160
+ */
161
+ addEventListener(
162
+ type: "nv-radio-btn-select",
163
+ listener: (this: NvRadioButtonElement, ev: NvRadioButtonSelectEvent) => any,
164
+ options?: boolean | AddEventListenerOptions
165
+ ): void;
166
+
167
+ /**
168
+ * 取消选中事件监听器
169
+ * @param type 事件类型: "nv-radio-btn-unselect"
170
+ * @param listener 事件监听函数
171
+ * @param options 事件监听选项
172
+ */
173
+ addEventListener(
174
+ type: "nv-radio-btn-unselect",
175
+ listener: (this: NvRadioButtonElement, ev: NvRadioButtonUnselectEvent) => any,
176
+ options?: boolean | AddEventListenerOptions
177
+ ): void;
178
+
179
+ /**
180
+ * 通用事件监听器
181
+ */
182
+ addEventListener(
183
+ type: string,
184
+ listener: EventListenerOrEventListenerObject,
185
+ options?: boolean | AddEventListenerOptions
186
+ ): void;
187
+ }
188
+
189
+ /**
190
+ * 单选按钮类定义
191
+ */
192
+ declare class NvRadioButton extends HTMLElement implements NvRadioButtonElement {
193
+ /** 监听属性变化 */
194
+ static get observedAttributes(): string[];
195
+
196
+ /** 按钮宽度 */
197
+ width: string;
198
+
199
+ /** 按钮高度 */
200
+ height: string;
201
+
202
+ /** 边框颜色 */
203
+ borderColor: string;
204
+
205
+ /** 内部颜色 */
206
+ innerColor: string;
207
+
208
+ /** 设置回调函数 */
209
+ setCallback(cb: (self: NvRadioButtonElement) => Promise<void> | void): void;
210
+
211
+ /** 显示中心颜色 */
212
+ slct(): void;
213
+
214
+ /** 隐藏中心颜色 */
215
+ unslct(): void;
216
+
217
+ /** 切换选中状态 */
218
+ toggle(): void;
219
+
220
+ /** 设置宽度 */
221
+ setWidth(width: string): void;
222
+
223
+ /** 设置高度 */
224
+ setHeight(height: string): void;
225
+
226
+ /** 设置边框颜色 */
227
+ setBorderColor(color: string): void;
228
+
229
+ /** 设置内部颜色 */
230
+ setInnerColor(color: string): void;
231
+
232
+ /** 获取当前状态 */
233
+ getState(): NvRadioButtonState;
234
+
235
+ /**
236
+ * 属性变化回调
237
+ * @param name 属性名
238
+ * @param oldValue 旧值
239
+ * @param newValue 新值
240
+ */
241
+ attributeChangedCallback(name: string, oldValue: string, newValue: string): void;
242
+
243
+ /**
244
+ * 元素连接到DOM时的回调
245
+ */
246
+ connectedCallback(): void;
247
+
248
+ /**
249
+ * 元素从DOM断开时的回调
250
+ */
251
+ disconnectedCallback(): void;
252
+ }
253
+
254
+ /**
255
+ * 全局类型扩展
256
+ */
257
+ declare global {
258
+ /**
259
+ * 扩展 HTMLElementTagNameMap 以支持自定义元素
260
+ */
261
+ interface HTMLElementTagNameMap {
262
+ "nv-radio-button": NvRadioButtonElement;
263
+ }
264
+
265
+ /**
266
+ * 扩展 Window 对象,包含自定义元素
267
+ */
268
+ interface Window {
269
+ /**
270
+ * 单选按钮自定义元素类
271
+ */
272
+ NvRadioButton: typeof NvRadioButton;
273
+ }
274
+
275
+ /**
276
+ * 扩展 Document 对象的事件类型
277
+ */
278
+ interface Document {
279
+ addEventListener(
280
+ type: "nv-radio-btn-click",
281
+ listener: (this: Document, ev: NvRadioButtonClickEvent) => any,
282
+ options?: boolean | AddEventListenerOptions
283
+ ): void;
284
+
285
+ addEventListener(
286
+ type: "nv-radio-btn-select",
287
+ listener: (this: Document, ev: NvRadioButtonSelectEvent) => any,
288
+ options?: boolean | AddEventListenerOptions
289
+ ): void;
290
+
291
+ addEventListener(
292
+ type: "nv-radio-btn-unselect",
293
+ listener: (this: Document, ev: NvRadioButtonUnselectEvent) => any,
294
+ options?: boolean | AddEventListenerOptions
295
+ ): void;
296
+ }
297
+ }
298
+
299
+ /**
300
+ * 导出单选按钮类
301
+ */
302
+ export { NvRadioButton };
303
+
304
+ /**
305
+ * 默认导出单选按钮类型
306
+ */
307
+ export default NvRadioButton;
308
+
package/index.js ADDED
@@ -0,0 +1,364 @@
1
+ class NvRadioButton extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+
5
+ // 创建Shadow DOM
6
+ this.attachShadow({ mode: 'open' });
7
+
8
+ // 默认属性
9
+ this._width = '60px';
10
+ this._height = '60px';
11
+ this._borderColor = '#cccccc';
12
+ this._innerColor = '#007bff';
13
+ this._isSelected = true;
14
+ this._cb = async (self) => {};
15
+
16
+ // 初始化渲染
17
+ this._render();
18
+ }
19
+
20
+ // 监听属性变化
21
+ static get observedAttributes() {
22
+ return ['width', 'height', 'border-color', 'inner-color'];
23
+ }
24
+
25
+ // 属性变化回调
26
+ attributeChangedCallback(name, oldValue, newValue) {
27
+ if (oldValue === newValue) return;
28
+
29
+ switch(name) {
30
+ case 'width':
31
+ this._width = newValue;
32
+ this._updateSize();
33
+ break;
34
+ case 'height':
35
+ this._height = newValue;
36
+ this._updateSize();
37
+ break;
38
+ case 'border-color':
39
+ this._borderColor = newValue;
40
+ this._updateBorderColor();
41
+ break;
42
+ case 'inner-color':
43
+ this._innerColor = newValue;
44
+ this._updateInnerColor();
45
+ break;
46
+ }
47
+ }
48
+
49
+ // 连接回调
50
+ connectedCallback() {
51
+ this.shadowRoot.querySelector('.nv-radio-btn').addEventListener('click', this._handleClick.bind(this));
52
+ }
53
+
54
+ // 断开连接回调
55
+ disconnectedCallback() {
56
+ this.shadowRoot.querySelector('.nv-radio-btn').removeEventListener('click', this._handleClick);
57
+ }
58
+
59
+ // 渲染方法
60
+ _render() {
61
+ const template = document.createElement('template');
62
+ template.innerHTML = `
63
+ <style>
64
+ .nv-radio-btn {
65
+ width: ${this._width};
66
+ height: ${this._height};
67
+ border-radius: 50%;
68
+ border: 3px solid ${this._borderColor};
69
+ background: white;
70
+ cursor: pointer;
71
+ position: relative;
72
+ padding: 0;
73
+ margin: 0;
74
+ transition: all 0.2s ease;
75
+ }
76
+
77
+ .nv-radio-btn::after {
78
+ content: '';
79
+ position: absolute;
80
+ top: 50%;
81
+ left: 50%;
82
+ transform: translate(-50%, -50%);
83
+ width: ${this._getInnerSize(this._width)};
84
+ height: ${this._getInnerSize(this._height)};
85
+ border-radius: 50%;
86
+ background: ${this._innerColor};
87
+ }
88
+
89
+ .nv-radio-btn.nv-radio-btn-unselected::after {
90
+ background: transparent;
91
+ }
92
+
93
+ .nv-radio-btn:hover {
94
+ border-color: ${this._getDarkerColor(this._borderColor)};
95
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
96
+ }
97
+
98
+ .nv-radio-btn:active {
99
+ transform: scale(0.95);
100
+ }
101
+ </style>
102
+
103
+ <button class="nv-radio-btn ${this._isSelected ? '' : 'nv-radio-btn-unselected'}" aria-label="单选按钮">
104
+ </button>
105
+ `;
106
+
107
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
108
+ }
109
+
110
+ // 获取内部尺寸
111
+ _getInnerSize(value) {
112
+ const num = this._getNumber(value);
113
+ const unit = this._getUnit(value);
114
+ return `${num * 0.67}${unit}`;
115
+ }
116
+
117
+ // 获取数字
118
+ _getNumber(value) {
119
+ const match = value.match(/\d+/);
120
+ return match ? parseInt(match[0], 10) : 60;
121
+ }
122
+
123
+ // 获取单位
124
+ _getUnit(value) {
125
+ const match = value.match(/[a-zA-Z%]+/);
126
+ return match ? match[0] : 'px';
127
+ }
128
+
129
+ // 更新尺寸
130
+ _updateSize() {
131
+ const button = this.shadowRoot.querySelector('.nv-radio-btn');
132
+ if (!button) return;
133
+
134
+ button.style.width = this._width;
135
+ button.style.height = this._height;
136
+
137
+ const styleElement = this.shadowRoot.querySelector('style');
138
+ if (styleElement) {
139
+ const innerWidth = this._getInnerSize(this._width);
140
+ const innerHeight = this._getInnerSize(this._height);
141
+
142
+ styleElement.textContent = `
143
+ .nv-radio-btn {
144
+ width: ${this._width};
145
+ height: ${this._height};
146
+ border-radius: 50%;
147
+ border: 3px solid ${this._borderColor};
148
+ background: white;
149
+ cursor: pointer;
150
+ position: relative;
151
+ padding: 0;
152
+ margin: 0;
153
+ transition: all 0.2s ease;
154
+ }
155
+
156
+ .nv-radio-btn::after {
157
+ content: '';
158
+ position: absolute;
159
+ top: 50%;
160
+ left: 50%;
161
+ transform: translate(-50%, -50%);
162
+ width: ${innerWidth};
163
+ height: ${innerHeight};
164
+ border-radius: 50%;
165
+ background: ${this._innerColor};
166
+ }
167
+
168
+ .nv-radio-btn.nv-radio-btn-unselected::after {
169
+ background: transparent;
170
+ }
171
+
172
+ .nv-radio-btn:hover {
173
+ border-color: ${this._getDarkerColor(this._borderColor)};
174
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
175
+ }
176
+
177
+ .nv-radio-btn:active {
178
+ transform: scale(0.95);
179
+ }
180
+ `;
181
+ }
182
+ }
183
+
184
+ // 更新边框颜色
185
+ _updateBorderColor() {
186
+ const button = this.shadowRoot.querySelector('.nv-radio-btn');
187
+ if (!button) return;
188
+
189
+ button.style.borderColor = this._borderColor;
190
+
191
+ const styleElement = this.shadowRoot.querySelector('style');
192
+ if (styleElement) {
193
+ styleElement.textContent = styleElement.textContent.replace(
194
+ /border: 3px solid #[^;]+;/,
195
+ `border: 3px solid ${this._borderColor};`
196
+ ).replace(
197
+ /border-color: [^;]+;/,
198
+ `border-color: ${this._getDarkerColor(this._borderColor)};`
199
+ );
200
+ }
201
+ }
202
+
203
+ // 更新内部颜色
204
+ _updateInnerColor() {
205
+ const button = this.shadowRoot.querySelector('.nv-radio-btn');
206
+ if (!button) return;
207
+
208
+ const styleElement = this.shadowRoot.querySelector('style');
209
+ if (styleElement) {
210
+ styleElement.textContent = styleElement.textContent.replace(
211
+ /background: #[^;]+;/,
212
+ `background: ${this._innerColor};`
213
+ );
214
+ }
215
+ }
216
+
217
+ // 获取更深色
218
+ _getDarkerColor(color) {
219
+ const hex = color.replace('#', '');
220
+ if (hex.length !== 6) return color;
221
+
222
+ const r = parseInt(hex.substr(0, 2), 16);
223
+ const g = parseInt(hex.substr(2, 2), 16);
224
+ const b = parseInt(hex.substr(4, 2), 16);
225
+
226
+ const darken = 0.8;
227
+ const newR = Math.floor(r * darken);
228
+ const newG = Math.floor(g * darken);
229
+ const newB = Math.floor(b * darken);
230
+
231
+ return `rgb(${newR}, ${newG}, ${newB})`;
232
+ }
233
+
234
+ // 点击处理
235
+ async _handleClick(event) {
236
+ const clickEvent = new CustomEvent('nv-radio-btn-click', {
237
+ bubbles: true,
238
+ composed: true,
239
+ detail: {
240
+ timestamp: Date.now(),
241
+ element: this,
242
+ selected: this._isSelected
243
+ }
244
+ });
245
+ this.dispatchEvent(clickEvent);
246
+
247
+ const button = this.shadowRoot.querySelector('.nv-radio-btn');
248
+ if (button) {
249
+ button.style.transform = 'scale(0.9)';
250
+ }
251
+
252
+ try {
253
+ await this._cb(this);
254
+ } catch (error) {
255
+ console.error('回调函数执行出错:', error);
256
+ }
257
+
258
+ if (button) {
259
+ button.style.transform = '';
260
+ }
261
+ }
262
+
263
+ // 设置回调函数
264
+ setCallback(cb) {
265
+ if (typeof cb === 'function') {
266
+ this._cb = cb;
267
+ }
268
+ }
269
+
270
+ // 显示中心颜色
271
+ slct() {
272
+ if (this._isSelected) return;
273
+
274
+ this._isSelected = true;
275
+ const button = this.shadowRoot.querySelector('.nv-radio-btn');
276
+ if (button) {
277
+ button.classList.remove('nv-radio-btn-unselected');
278
+ }
279
+
280
+ const selectEvent = new CustomEvent('nv-radio-btn-select', {
281
+ bubbles: true,
282
+ composed: true,
283
+ detail: {
284
+ timestamp: Date.now(),
285
+ element: this
286
+ }
287
+ });
288
+ this.dispatchEvent(selectEvent);
289
+ }
290
+
291
+ // 隐藏中心颜色
292
+ unslct() {
293
+ if (!this._isSelected) return;
294
+
295
+ this._isSelected = false;
296
+ const button = this.shadowRoot.querySelector('.nv-radio-btn');
297
+ if (button) {
298
+ button.classList.add('nv-radio-btn-unselected');
299
+ }
300
+
301
+ const unselectEvent = new CustomEvent('nv-radio-btn-unselect', {
302
+ bubbles: true,
303
+ composed: true,
304
+ detail: {
305
+ timestamp: Date.now(),
306
+ element: this
307
+ }
308
+ });
309
+ this.dispatchEvent(unselectEvent);
310
+ }
311
+
312
+ // 切换选中状态
313
+ toggle() {
314
+ if (this._isSelected) {
315
+ this.unslct();
316
+ } else {
317
+ this.slct();
318
+ }
319
+ }
320
+
321
+ // 设置宽度
322
+ setWidth(width) {
323
+ this._width = width;
324
+ this._updateSize();
325
+ }
326
+
327
+ // 设置高度
328
+ setHeight(height) {
329
+ this._height = height;
330
+ this._updateSize();
331
+ }
332
+
333
+ // 设置边框颜色
334
+ setBorderColor(color) {
335
+ this._borderColor = color;
336
+ this._updateBorderColor();
337
+ }
338
+
339
+ // 设置内部颜色
340
+ setInnerColor(color) {
341
+ this._innerColor = color;
342
+ this._updateInnerColor();
343
+ }
344
+
345
+ // 获取当前状态
346
+ getState() {
347
+ return {
348
+ width: this._width,
349
+ height: this._height,
350
+ borderColor: this._borderColor,
351
+ innerColor: this._innerColor,
352
+ selected: this._isSelected
353
+ };
354
+ }
355
+ }
356
+
357
+ // 注册自定义元素
358
+ if (!customElements.get('nv-radio-button')) {
359
+ customElements.define('nv-radio-button', NvRadioButton);
360
+ }
361
+
362
+ module.exports = {
363
+ NvRadioButton
364
+ }
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "nv-radio-btn-bw",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "scripts": {
6
+ "test": "echo \"Error: no test specified\" && exit 1"
7
+ },
8
+ "author": "",
9
+ "license": "ISC",
10
+ "description": ""
11
+ }