@kevisual/kv-login 0.1.1 → 0.1.3

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,577 @@
1
+ import { render, html } from 'lit-html'
2
+ import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'
3
+ import { loginHandle, checkWechat, getQrCode, checkMpQrCodeLogin } from '../modules/login-handle.ts'
4
+ import { setWxerwma } from '../modules/wx/ws-login.ts';
5
+ import { useCreateLoginQRCode } from '../modules/wx-mp/qr.ts';
6
+ import { eventEmitter } from '../modules/mitt.ts';
7
+ import { useContextKey } from '@kevisual/context'
8
+ export const loginEmitter = useContextKey('login-emitter', eventEmitter);
9
+ export const WX_MP_APP_ID = "wxff97d569b1db16b6";
10
+ interface LoginMethod {
11
+ id: LoginMethods
12
+ name: string
13
+ icon: any
14
+ appid?: string
15
+ }
16
+ const wxmpSvg = `<svg t="1764510467010" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1958" width="32" height="32"><path d="M615.904 388.48c8.8 0 17.536 0.64 26.176 1.6-23.52-109.536-140.608-190.912-274.272-190.912C218.4 199.2 96 301.056 96 430.4c0 74.656 40.736 135.936 108.768 183.488l-27.2 81.792 95.04-47.648c33.984 6.72 61.28 13.632 95.2 13.632 8.544 0 16.992-0.416 25.376-1.088a202.496 202.496 0 0 1-8.384-56.96c0-118.752 101.984-215.136 231.104-215.136zM469.76 314.784c20.48 0 34.016 13.472 34.016 33.92 0 20.352-13.536 34.016-34.016 34.016-20.384 0-40.832-13.664-40.832-34.016 0-20.448 20.448-33.92 40.832-33.92zM279.52 382.72c-20.384 0-40.928-13.664-40.928-34.016 0-20.448 20.544-33.92 40.928-33.92 20.352 0 33.92 13.472 33.92 33.92 0 20.384-13.568 34.016-33.92 34.016z" fill="" p-id="1959"></path><path d="M864 600.352c0-108.672-108.736-197.28-230.88-197.28-129.344 0-231.2 88.576-231.2 197.28 0 108.864 101.856 197.248 231.2 197.248 27.072 0 54.368-6.816 81.568-13.632l74.56 40.8-20.448-67.904C823.328 715.936 864 661.664 864 600.352z m-305.856-34.016c-13.536 0-27.2-13.44-27.2-27.2 0-13.568 13.664-27.2 27.2-27.2 20.576 0 34.016 13.632 34.016 27.2 0 13.76-13.44 27.2-34.016 27.2z m149.536 0c-13.44 0-27.008-13.44-27.008-27.2 0-13.568 13.568-27.2 27.008-27.2 20.352 0 34.016 13.632 34.016 27.2 0 13.76-13.664 27.2-34.016 27.2z" fill="" p-id="1960"></path></svg>`
17
+ const wxOpenSvg = `<svg t="1764511395617" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3882" width="32" height="32"><path d="M256 259.584c-29.184 0-51.2 14.848-51.2 44.032s29.184 44.032 51.2 44.032c29.184 0 44.032-14.848 44.032-44.032s-22.016-44.032-44.032-44.032zM541.184 303.616c0-29.184-14.848-44.032-44.032-44.032-29.184 0-51.2 14.848-51.2 44.032s29.184 44.032 51.2 44.032c29.696 0 44.032-22.016 44.032-44.032zM614.4 508.416c-14.848 0-36.352 14.848-36.352 36.352 0 14.848 14.848 36.352 36.352 36.352 29.184 0 44.032-14.848 44.032-36.352 0-14.336-14.848-36.352-44.032-36.352z" p-id="3883"></path><path d="M1024 625.152c0-138.752-124.416-256-285.184-270.848-29.184-153.6-189.952-263.168-373.248-263.168C160.768 91.648 0 230.4 0 406.016c0 95.232 44.032 175.616 138.752 241.152L109.568 742.4c0 7.168 0 14.848 7.168 22.016h14.848l117.248-58.368h14.848c36.352 7.168 66.048 14.848 109.568 14.848 14.848 0 44.032-7.168 44.032-7.168C460.8 822.784 578.048 896 716.8 896c36.352 0 73.216-7.168 102.4-14.848l87.552 51.2h14.848c7.168-7.168 7.168-7.168 7.168-14.848l-22.016-87.552c80.896-58.368 117.248-131.584 117.248-204.8z m-621.568 51.2h-36.352c-36.352 0-66.048-7.168-95.232-14.848l-22.016-7.168h-7.168L153.6 698.368l22.016-66.048c0-7.168 0-14.848-7.168-14.848C80.384 559.616 36.352 486.4 36.352 398.848 36.352 245.248 182.784 128 358.4 128c160.768 0 300.032 95.232 329.216 226.816-168.448 0-300.032 117.248-300.032 263.168 7.168 22.016 14.848 44.032 14.848 58.368z m467.968 132.096c-7.168 7.168-7.168 7.168-7.168 14.848l14.848 51.2L819.2 844.8h-14.848c-29.184 7.168-66.048 14.848-95.232 14.848-146.432 0-270.848-102.4-270.848-226.816 0-131.584 124.416-233.984 270.848-233.984s270.848 102.4 270.848 226.816c0 65.536-36.352 123.904-109.568 182.784z" p-id="3884"></path><path d="M804.352 508.416c-14.848 0-36.352 14.848-36.352 36.352 0 14.848 14.848 36.352 36.352 36.352 29.184 0 44.032-14.848 44.032-36.352 0-14.336-14.336-36.352-44.032-36.352z" p-id="3885"></path></svg>`
18
+ const phone = `<svg t="1764511425462" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5097" width="32" height="32"><path d="M820.409449 797.228346q0 25.19685-10.07874 46.866142t-27.716535 38.299213-41.322835 26.204724-50.897638 9.574803l-357.795276 0q-27.212598 0-50.897638-9.574803t-41.322835-26.204724-27.716535-38.299213-10.07874-46.866142l0-675.275591q0-25.19685 10.07874-47.370079t27.716535-38.80315 41.322835-26.204724 50.897638-9.574803l357.795276 0q27.212598 0 50.897638 9.574803t41.322835 26.204724 27.716535 38.80315 10.07874 47.370079l0 675.275591zM738.771654 170.330709l-455.559055 0 0 577.511811 455.559055 0 0-577.511811zM510.992126 776.062992q-21.165354 0-36.787402 15.11811t-15.622047 37.291339q0 21.165354 15.622047 36.787402t36.787402 15.622047q22.173228 0 37.291339-15.622047t15.11811-36.787402q0-22.173228-15.11811-37.291339t-37.291339-15.11811zM591.622047 84.661417q0-8.062992-5.03937-12.598425t-11.086614-4.535433l-128 0q-5.03937 0-10.582677 4.535433t-5.543307 12.598425 5.03937 12.598425 11.086614 4.535433l128 0q6.047244 0 11.086614-4.535433t5.03937-12.598425z" p-id="5098"></path></svg>`
19
+ const pwd = `<svg t="1764511500570" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10511" width="32" height="32"><path d="M768.9216 422.72768 372.06016 422.72768C378.88 365.21984 329.37984 131.42016 512.2048 125.72672c173.83424-6.59456 146.78016 213.34016 146.78016 213.34016l85.13536 0.57344c0 0 24.73984-294.4-231.91552-295.8336C232.09984 58.01984 297.82016 377.18016 289.28 422.72768c1.98656 0 4.56704 0 7.29088 0-55.88992 0-101.21216 45.34272-101.21216 101.21216l0 337.38752c0 55.88992 45.34272 101.21216 101.21216 101.21216l472.35072 0c55.88992 0 101.21216-45.34272 101.21216-101.21216L870.13376 523.93984C870.13376 468.0704 824.79104 422.72768 768.9216 422.72768zM566.4768 717.02528l0 76.84096c0 18.57536-15.1552 33.73056-33.73056 33.73056-18.57536 0-33.73056-15.1552-33.73056-33.73056l0-76.84096c-20.09088-11.69408-33.73056-33.21856-33.73056-58.12224 0-37.2736 30.208-67.4816 67.4816-67.4816 37.2736 0 67.4816 30.208 67.4816 67.4816C600.22784 683.80672 586.58816 705.3312 566.4768 717.02528z" fill="#272636" p-id="10512"></path></svg>`
20
+
21
+ const icons: any = {
22
+ pwd,
23
+ phone,
24
+ wxmpSvg,
25
+ wxOpenSvg
26
+ }
27
+ const DefaultLoginMethods: LoginMethod[] = [
28
+ { id: 'password', name: '密码登录', icon: 'pwd' },
29
+ { id: 'wechat', name: '微信登录', icon: 'wxmpSvg', appid: "wx9378885c8390e09b" },
30
+ { id: 'wechat-mp', name: '微信公众号', icon: 'wxOpenSvg', appid: WX_MP_APP_ID },
31
+ { id: 'wechat-mp-ticket', name: '微信公众号', icon: 'wxOpenSvg' },
32
+ { id: 'phone', name: '手机号登录', icon: 'phone' }
33
+ ]
34
+ const LoginMethods = ['password', 'phone', 'wechat', 'wechat-mp', 'wechat-mp-ticket'] as const;
35
+ type LoginMethods = 'password' | 'phone' | 'wechat' | 'wechat-mp' | 'wechat-mp-ticket';
36
+
37
+ const getLoginMethodByDomain = (): LoginMethod[] => {
38
+ let domain = window.location.host
39
+ let methods: LoginMethods[] = []
40
+ const has51015 = domain.includes('51015');
41
+ if (has51015) {
42
+ domain = 'localhost:51015'
43
+ }
44
+ switch (domain) {
45
+ case 'kevisual.xiongxiao.me':
46
+ methods = ['password', 'wechat-mp']
47
+ break;
48
+ case 'kevisual.cn':
49
+ methods = ['password', 'wechat-mp-ticket', 'wechat',]
50
+ break;
51
+ case 'localhost:51015':
52
+ methods = ['password']
53
+ break
54
+ default:
55
+ methods = ['password', 'phone', 'wechat', 'wechat-mp', 'wechat-mp-ticket']
56
+ break;
57
+ }
58
+ return DefaultLoginMethods.filter(method => methods.includes(method.id))
59
+ }
60
+ const getLoginMethod = (methods: LoginMethods[]): LoginMethod[] => {
61
+ return DefaultLoginMethods.filter(method => methods.includes(method.id))
62
+ }
63
+ class KvLogin extends HTMLElement {
64
+ private selectedMethod: LoginMethods = 'password'
65
+
66
+ private loginMethods: LoginMethod[] = getLoginMethodByDomain();
67
+ setLoginMethods(methods: LoginMethod[]) {
68
+ this.loginMethods = methods
69
+ this.render()
70
+ }
71
+ constructor() {
72
+ super()
73
+ }
74
+
75
+ connectedCallback() {
76
+ this.attachShadow({ mode: 'open' })
77
+ this.render()
78
+ this.bindEvents()
79
+ checkWechat()
80
+ const method = this.getAttribute('method');
81
+ if (method) {
82
+ const methods = method ? method.split(',') as LoginMethods[] : [];
83
+ if (methods.length > 0) {
84
+ const loginMethods = methods.filter(m => LoginMethods.includes(m));
85
+ if (loginMethods.length > 0) {
86
+ this.loginMethods = getLoginMethod(loginMethods)
87
+ this.selectedMethod = loginMethods[0]
88
+ return;
89
+ }
90
+ }
91
+ this.loginMethods = getLoginMethodByDomain();
92
+ this.selectedMethod = this.loginMethods[0].id;
93
+ }
94
+ }
95
+ #clearTimer: any = null;
96
+ private selectLoginMethod(methodId: LoginMethods) {
97
+ this.selectedMethod = methodId
98
+ this.render()
99
+ if (this.#clearTimer) {
100
+ this.#clearTimer();
101
+ this.#clearTimer = null;
102
+ }
103
+ }
104
+ private getMethodData(methodId: LoginMethods): LoginMethod | undefined {
105
+ return this.loginMethods.find(method => method.id === methodId);
106
+ }
107
+ private bindEvents() {
108
+ if (!this.shadowRoot) return
109
+ // 使用事件委托来处理登录方式切换
110
+ this.shadowRoot.addEventListener('click', (e) => {
111
+ const target = e.target as HTMLElement
112
+ const methodButton = target.closest('.login-method')
113
+ if (methodButton) {
114
+ const methodId = methodButton.getAttribute('data-method') as LoginMethods
115
+ if (methodId) {
116
+ this.selectLoginMethod(methodId)
117
+ }
118
+ }
119
+ })
120
+
121
+ // 使用事件委托来处理表单提交
122
+ this.shadowRoot.addEventListener('submit', (e) => {
123
+ const target = e.target as HTMLElement
124
+ if (target && target.id === 'loginForm') {
125
+ e.preventDefault()
126
+ this.handleLogin()
127
+ }
128
+ })
129
+ loginEmitter.on('login-success', () => {
130
+ console.log('收到登录成功事件,处理后续逻辑')
131
+ });
132
+ }
133
+
134
+ private handleLogin() {
135
+ const formData = this.getFormData()
136
+ // console.log('登录方式:', this.selectedMethod)
137
+ // console.log('登录数据:', formData)
138
+ loginHandle({
139
+ loginMethod: this.selectedMethod,
140
+ data: formData,
141
+ el: this
142
+ })
143
+ // 这里可以触发自定义事件,通知父组件
144
+ this.dispatchEvent(new CustomEvent('login', {
145
+ detail: {
146
+ method: this.selectedMethod,
147
+ data: formData
148
+ },
149
+ bubbles: true
150
+ }))
151
+ }
152
+
153
+ private getFormData(): any {
154
+ if (!this.shadowRoot) return {}
155
+
156
+ switch (this.selectedMethod) {
157
+ case 'password':
158
+ const username = this.shadowRoot.querySelector('#username') as HTMLInputElement
159
+ const password = this.shadowRoot.querySelector('#password') as HTMLInputElement
160
+ return {
161
+ username: username?.value || '',
162
+ password: password?.value || ''
163
+ }
164
+
165
+ case 'phone':
166
+ const phone = this.shadowRoot.querySelector('#phone') as HTMLInputElement
167
+ const code = this.shadowRoot.querySelector('#code') as HTMLInputElement
168
+ return {
169
+ phone: phone?.value || '',
170
+ code: code?.value || ''
171
+ }
172
+
173
+ case 'wechat':
174
+ return {
175
+ wechatCode: 'mock_wechat_code'
176
+ }
177
+ case 'wechat-mp':
178
+ return {
179
+ wechatMpCode: 'mock_wechat_mp_code'
180
+ }
181
+ default:
182
+ return {}
183
+ }
184
+ }
185
+
186
+ private renderPasswordForm() {
187
+ return html`
188
+ <form id="loginForm" class="login-form">
189
+ <div class="form-group">
190
+ <input
191
+ type="text"
192
+ id="username"
193
+ name="username"
194
+ placeholder="请输入用户名"
195
+ autocomplete="username"
196
+ required
197
+ />
198
+ </div>
199
+ <div class="form-group">
200
+ <input
201
+ type="password"
202
+ id="password"
203
+ name="password"
204
+ placeholder="请输入密码"
205
+ autocomplete="current-password"
206
+ required
207
+ />
208
+ </div>
209
+ <button type="submit" class="login-button">登录</button>
210
+ </form>
211
+ `
212
+ }
213
+
214
+ private renderPhoneForm() {
215
+ return html`
216
+ <form id="loginForm" class="login-form">
217
+ <div class="form-group">
218
+ <input
219
+ type="tel"
220
+ id="phone"
221
+ name="phone"
222
+ placeholder="请输入手机号"
223
+ pattern="[0-9]{11}"
224
+ autocomplete="tel"
225
+ required
226
+ />
227
+ </div>
228
+ <div class="form-group code-group">
229
+ <input
230
+ type="text"
231
+ id="code"
232
+ name="code"
233
+ placeholder="请输入验证码"
234
+ autocomplete="one-time-code"
235
+ required
236
+ />
237
+ <button type="button" class="code-button" @click=${this.sendVerificationCode}>获取验证码</button>
238
+ </div>
239
+ <button type="submit" class="login-button">登录</button>
240
+ </form>
241
+ `
242
+ }
243
+
244
+ private renderWechatForm() {
245
+ return html`
246
+ <div class="wechat-login">
247
+ <slot></slot>
248
+ </div>
249
+ `
250
+ }
251
+ private renderWechatMpForm() {
252
+ const that = this
253
+ setTimeout(() => {
254
+ const qrcode = that.shadowRoot!.querySelector('#qrcode');
255
+ const { clear } = useCreateLoginQRCode(qrcode as HTMLCanvasElement);
256
+ that.#clearTimer = clear;
257
+ }, 0)
258
+ return html`
259
+ <div class="wechat-login">
260
+ <div class="qr-container">
261
+ <div class="qr-placeholder">
262
+ <canvas id='qrcode' width='300' height='300'></canvas>
263
+ <p class="qr-desc">请使用微信扫描二维码登录</p>
264
+ </div>
265
+ </div>
266
+ </div>
267
+ `
268
+ }
269
+ private renderWechatMpTicketForm() {
270
+ const that = this;
271
+ setTimeout(async () => {
272
+ const data = await getQrCode();
273
+ if (!data) return;
274
+ const imgEl = that.shadowRoot!.querySelector('.qrcode') as HTMLImageElement;
275
+ if (data.url) {
276
+ imgEl.src = data.url;
277
+ // TODO: 轮询检测登录状态
278
+ const clear = checkMpQrCodeLogin(data.ticket)
279
+ // 当切换登录方式时,停止轮询
280
+ that.#clearTimer = clear
281
+ }
282
+ }, 0)
283
+ return html`
284
+ <div class="wechat-login">
285
+ <div class="qr-container">
286
+ <div class="qr-placeholder">
287
+ <img class="qrcode" width="300" height="300" data-appid="" data-size="200" data-ticket=""></img>
288
+ <p class="qr-desc">请使用微信扫描二维码登录</p>
289
+ </div>
290
+ </div>
291
+ </div>
292
+ `
293
+ }
294
+
295
+ private sendVerificationCode() {
296
+ console.log('发送验证码')
297
+ // 这里可以实现发送验证码的逻辑
298
+ }
299
+
300
+ private refreshQR() {
301
+ console.log('刷新二维码')
302
+ // 这里可以实现刷新二维码的逻辑
303
+ }
304
+
305
+
306
+ private renderLoginForm() {
307
+ const data = this.getMethodData(this.selectedMethod);
308
+ switch (this.selectedMethod) {
309
+ case 'password':
310
+ return this.renderPasswordForm()
311
+ case 'phone':
312
+ return this.renderPhoneForm()
313
+ case 'wechat':
314
+ setWxerwma({ appid: data?.appid! || "" });
315
+ return this.renderWechatForm()
316
+ case 'wechat-mp':
317
+ return this.renderWechatMpForm()
318
+ case 'wechat-mp-ticket':
319
+ return this.renderWechatMpTicketForm()
320
+ default:
321
+ return this.renderPasswordForm()
322
+ }
323
+ }
324
+
325
+ render() {
326
+ if (!this.shadowRoot) return
327
+
328
+ const renderIcon = (icon: any) => {
329
+ // 如果是emoji字符,直接返回
330
+ if (typeof icon === 'string' && !icons[icon]) {
331
+ return html`<span class="method-icon-emoji">${icon}</span>`
332
+ }
333
+ // 如果是SVG引用,从icons对象获取
334
+ if (typeof icon === 'string' && icons[icon]) {
335
+ return html`<span class="method-icon-svg">${unsafeHTML(icons[icon])}</span>`
336
+ }
337
+ // 如果直接是SVG内容
338
+ if (typeof icon === 'string' && (icon.includes('<svg') || icon.includes('<?xml'))) {
339
+ return html`<span class="method-icon-svg">${unsafeHTML(icon)}</span>`
340
+ }
341
+ // 默认情况
342
+ return html`<span class="method-icon-emoji">${icon}</span>`
343
+ }
344
+ const template = html`
345
+ <style>
346
+ :host {
347
+ display: block;
348
+ width: 100%;
349
+ min-width: 400px;
350
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
351
+ }
352
+
353
+ .login-sidebar {
354
+ background: white;
355
+ border-radius: 12px;
356
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
357
+ overflow: hidden;
358
+ }
359
+
360
+ .login-methods {
361
+ display: flex;
362
+ background: #f5f5f5;
363
+ border-bottom: 1px solid #000000;
364
+ }
365
+
366
+ .login-method {
367
+ flex: 1;
368
+ padding: 16px 8px;
369
+ border: none;
370
+ background: none;
371
+ cursor: pointer;
372
+ display: flex;
373
+ flex-direction: column;
374
+ align-items: center;
375
+ gap: 4px;
376
+ transition: all 0.3s ease;
377
+ position: relative;
378
+ }
379
+
380
+ .login-method:hover {
381
+ background: #d0d0d0;
382
+ }
383
+
384
+ .login-method.active {
385
+ background: white;
386
+ color: #000000;
387
+ }
388
+
389
+ .login-method.active::after {
390
+ content: '';
391
+ position: absolute;
392
+ bottom: 0;
393
+ left: 0;
394
+ right: 0;
395
+ height: 2px;
396
+ background: #000000;
397
+ }
398
+
399
+ .method-icon {
400
+ font-size: 20px;
401
+ display: flex;
402
+ align-items: center;
403
+ justify-content: center;
404
+ width: 24px;
405
+ height: 24px;
406
+ }
407
+
408
+ .method-icon-emoji {
409
+ font-size: 20px;
410
+ line-height: 1;
411
+ }
412
+
413
+ .method-icon-svg {
414
+ display: flex;
415
+ align-items: center;
416
+ justify-content: center;
417
+ }
418
+
419
+ .method-icon-svg svg {
420
+ width: 32px;
421
+ height: 32px;
422
+ display: block;
423
+ }
424
+
425
+ .method-name {
426
+ font-size: 12px;
427
+ font-weight: 500;
428
+ }
429
+
430
+ .login-content {
431
+ padding: 32px 24px;
432
+ }
433
+ .impowerBox .qrcode {
434
+ width: 200px !important;
435
+ }
436
+ .login-form {
437
+ display: flex;
438
+ flex-direction: column;
439
+ gap: 16px;
440
+ }
441
+
442
+ .form-group {
443
+ position: relative;
444
+ }
445
+
446
+ .form-group input {
447
+ width: 100%;
448
+ padding: 12px 16px;
449
+ border: 2px solid #cccccc;
450
+ border-radius: 8px;
451
+ font-size: 14px;
452
+ transition: border-color 0.3s ease;
453
+ box-sizing: border-box;
454
+ }
455
+
456
+ .form-group input:focus {
457
+ outline: none;
458
+ border-color: #000000;
459
+ }
460
+
461
+ .code-group {
462
+ display: flex;
463
+ gap: 12px;
464
+ }
465
+
466
+ .code-group input {
467
+ flex: 1;
468
+ }
469
+
470
+ .code-button {
471
+ padding: 0 16px;
472
+ background: #6c757d;
473
+ color: white;
474
+ border: none;
475
+ border-radius: 8px;
476
+ font-size: 14px;
477
+ cursor: pointer;
478
+ white-space: nowrap;
479
+ transition: background-color 0.3s ease;
480
+ }
481
+
482
+ .code-button:hover {
483
+ background: #5a6268;
484
+ }
485
+
486
+ .login-button {
487
+ padding: 12px;
488
+ background: #000000;
489
+ color: white;
490
+ border: none;
491
+ border-radius: 8px;
492
+ font-size: 16px;
493
+ font-weight: 500;
494
+ cursor: pointer;
495
+ transition: background-color 0.3s ease;
496
+ }
497
+
498
+ .login-button:hover {
499
+ background: #333333;
500
+ }
501
+
502
+ .wechat-login {
503
+ display: flex;
504
+ flex-direction: column;
505
+ align-items: center;
506
+ gap: 20px;
507
+ }
508
+
509
+ .qr-container {
510
+ width: 340px;
511
+ height: 340px;
512
+ border: 2px solid #000000;
513
+ border-radius: 8px;
514
+ display: flex;
515
+ align-items: center;
516
+ justify-content: center;
517
+ }
518
+
519
+ .qr-placeholder {
520
+ text-align: center;
521
+ color: #333333;
522
+ }
523
+
524
+ .qr-icon {
525
+ font-size: 48px;
526
+ margin-bottom: 8px;
527
+ }
528
+
529
+ .qr-desc {
530
+ font-size: 12px;
531
+ margin-top: 4px;
532
+ }
533
+
534
+ .refresh-button {
535
+ padding: 8px 16px;
536
+ background: #6c757d;
537
+ color: white;
538
+ border: none;
539
+ border-radius: 6px;
540
+ font-size: 14px;
541
+ cursor: pointer;
542
+ transition: background-color 0.3s ease;
543
+ }
544
+
545
+ .refresh-button:hover {
546
+ background: #5a6268;
547
+ }
548
+ .method-icon svg {
549
+ width: 24px;
550
+ height: 24px;
551
+ }
552
+ </style>
553
+
554
+ <div class="login-sidebar">
555
+ <div class="login-methods">
556
+ ${this.loginMethods.map(method => html`
557
+ <button
558
+ class="login-method ${this.selectedMethod === method.id ? 'active' : ''}"
559
+ data-method="${method.id}"
560
+ >
561
+ ${renderIcon(method.icon)}
562
+ <span class="method-name">${method.name}</span>
563
+ </button>
564
+ `)}
565
+ </div>
566
+
567
+ <div class="login-content">
568
+ ${this.renderLoginForm()}
569
+ </div>
570
+ </div>
571
+ `
572
+
573
+ render(template, this.shadowRoot)
574
+ }
575
+ }
576
+
577
+ customElements.define('kv-login', KvLogin)