This commit is contained in:
2025-12-01 02:19:44 +08:00
parent 84775dd878
commit a51a366f00
21 changed files with 2628 additions and 229 deletions

View File

@@ -74,7 +74,7 @@
<div class="demo-container">
<div class="login-section">
<h2>登录组件</h2>
<kv-login id="loginComponent">
<kv-login>
<div id="weixinLogin"></div>
</kv-login>
</div>

View File

@@ -1,6 +1,6 @@
{
"name": "@kevisual/kv-login",
"version": "0.0.3",
"version": "0.0.6",
"description": "",
"main": "src/main.ts",
"scripts": {
@@ -8,24 +8,27 @@
"build": "vite build --config vite-lib.config.ts",
"build:test": "vite build",
"prepub": "rm -rf ./dist && pnpm run build:test",
"pub": "ev deploy ./dist -k kv-login-test -v 0.0.2 -u -y yes"
"pub": "ev deploy ./dist -k kv-login-test -v 0.0.6 -u -y yes"
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.19.0",
"packageManager": "pnpm@10.24.0",
"publishConfig": {
"access": "public"
},
"type": "module",
"dependencies": {
"@kevisual/query-login": "^0.0.6",
"@kevisual/context": "^0.0.4",
"@kevisual/query-login": "^0.0.7",
"lit-html": "^3.3.1",
"qrcode": "^1.5.4"
},
"exports": {
".": "./dist/kv-login.es.js",
"./kv-login.es.js": "./dist/kv-login.es.js",
"./kv-login.umd.js": "./dist/kv-login.umd.js"
}
"./kv-login.umd.js": "./dist/kv-login.umd.js",
"./types": "./types/index.d.ts"
},
"types": "./types/index.d.ts"
}

View File

@@ -5,3 +5,8 @@
黑白
```html
<kv-login>
<div id="weixinLogin"></div>
</kv-login>
```

View File

@@ -1,2 +1,4 @@
import './pages/kv-login'
import './pages/kv-message'
import './pages/kv-message'
export { loginEmitter } from './pages/kv-login'

View File

@@ -1,23 +1,25 @@
import { query } from './query.ts';
import { createMessage } from '../pages/kv-message.ts';
import { WX_MP_APP_ID } from '../pages/kv-login.ts';
import { emit } from './mitt.ts';
export const message = createMessage();
type LoginOpts = {
loginMethod: 'password' | 'phone' | 'wechat' | 'wechat-mp' | 'wechat-mp-ticket',
data: any,
el: HTMLElement
}
/**
* 登录成功后重定向到首页
*/
export const redirectHome = () => {
console.log('重定向到首页')
const href = window.location.href;
const url = new URL(href);
const redirect = url.searchParams.get('redirect');
if (redirect) {
const href = decodeURIComponent(redirect);
window.open(href, '_self');
} else {
window.open('/root/home', '_self');
}
emit({ type: 'login-success', data: {} });
}
export const loginHandle = async (opts: LoginOpts) => {
const { loginMethod, data, el } = opts

View File

@@ -0,0 +1,134 @@
export interface EventData<T = any> {
type: string;
data: T;
}
export type EventHandler<T = any> = (event: EventData<T>) => void;
export class EventEmitter {
private events: Map<string, Set<EventHandler>> = new Map();
/**
* 监听事件
* @param type 事件类型
* @param handler 事件处理函数
*/
on<T = any>(type: string, handler: EventHandler<T>): void {
if (!this.events.has(type)) {
this.events.set(type, new Set());
}
this.events.get(type)!.add(handler);
}
/**
* 移除事件监听器
* @param type 事件类型
* @param handler 事件处理函数 (可选,如果不提供则移除该类型的所有监听器)
*/
off<T = any>(type: string, handler?: EventHandler<T>): void {
if (!this.events.has(type)) {
return;
}
if (handler) {
this.events.get(type)!.delete(handler);
// 如果该类型没有监听器了,删除该类型
if (this.events.get(type)!.size === 0) {
this.events.delete(type);
}
} else {
// 移除该类型的所有监听器
this.events.delete(type);
}
}
/**
* 触发事件
* @param event 事件对象,包含 type 和 data
*/
emit<T = any>(event: EventData<T>): void {
const { type } = event;
if (!this.events.has(type)) {
return;
}
const handlers = this.events.get(type)!;
handlers.forEach(handler => {
try {
handler(event);
} catch (error) {
console.error(`Error in event handler for type "${type}":`, error);
}
});
}
/**
* 触发事件简化版本直接传递type和data
* @param type 事件类型
* @param data 事件数据
*/
emitSimple<T = any>(type: string, data: T): void {
this.emit({ type, data });
}
/**
* 清空所有事件监听器
*/
clear(): void {
this.events.clear();
}
/**
* 获取指定类型的监听器数量
* @param type 事件类型
* @returns 监听器数量
*/
listenerCount(type: string): number {
return this.events.get(type)?.size || 0;
}
/**
* 获取所有事件类型
* @returns 事件类型数组
*/
eventNames(): string[] {
return Array.from(this.events.keys());
}
/**
* 检查是否有指定类型的监听器
* @param type 事件类型
* @returns 是否有监听器
*/
hasListeners(type: string): boolean {
return this.events.has(type) && this.events.get(type)!.size > 0;
}
/**
* 只监听一次事件
* @param type 事件类型
* @param handler 事件处理函数
*/
once<T = any>(type: string, handler: EventHandler<T>): void {
const onceHandler: EventHandler<T> = (event) => {
handler(event);
this.off(type, onceHandler);
};
this.on(type, onceHandler);
}
}
// 创建默认的事件发射器实例
export const eventEmitter = new EventEmitter();
// 导出便捷方法
export const on = <T = any>(type: string, handler: EventHandler<T>) => eventEmitter.on(type, handler);
export const off = <T = any>(type: string, handler?: EventHandler<T>) => eventEmitter.off(type, handler);
export const emit = <T = any>(event: EventData<T>) => eventEmitter.emit(event);
export const emitSimple = <T = any>(type: string, data: T) => eventEmitter.emitSimple(type, data);
export const clear = () => eventEmitter.clear();
export const once = <T = any>(type: string, handler: EventHandler<T>) => eventEmitter.once(type, handler);
// 默认导出
export default eventEmitter;

View File

@@ -3,6 +3,9 @@ import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'
import { loginHandle, checkWechat, getQrCode, checkMpQrCodeLogin } from '../modules/login-handle.ts'
import { setWxerwma } from '../modules/wx/ws-login.ts';
import { useCreateLoginQRCode } from '../modules/wx-mp/qr.ts';
import { eventEmitter } from '../modules/mitt.ts';
import { useContextKey } from '@kevisual/context'
export const loginEmitter = useContextKey('login-emitter', eventEmitter);
export const WX_MP_APP_ID = "wxff97d569b1db16b6";
interface LoginMethod {
id: LoginMethods
@@ -46,7 +49,6 @@ const getLoginMethodByDomain = (): LoginMethod[] => {
}
return DefaultLoginMethods.filter(method => methods.includes(method.id))
}
console.log('可用登录方式:', getLoginMethodByDomain().map(m => m.name).join(', '));
class KvLogin extends HTMLElement {
private selectedMethod: LoginMethods = 'password'
@@ -80,7 +82,6 @@ class KvLogin extends HTMLElement {
}
private bindEvents() {
if (!this.shadowRoot) return
// 使用事件委托来处理登录方式切换
this.shadowRoot.addEventListener('click', (e) => {
const target = e.target as HTMLElement
@@ -101,6 +102,9 @@ class KvLogin extends HTMLElement {
this.handleLogin()
}
})
loginEmitter.on('login-success', () => {
console.log('收到登录成功事件,处理后续逻辑')
});
}
private handleLogin() {
@@ -331,8 +335,8 @@ class KvLogin extends HTMLElement {
.login-methods {
display: flex;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
background: #f5f5f5;
border-bottom: 1px solid #000000;
}
.login-method {
@@ -350,7 +354,7 @@ class KvLogin extends HTMLElement {
}
.login-method:hover {
background: #e9ecef;
background: #d0d0d0;
}
.login-method.active {
@@ -418,7 +422,7 @@ class KvLogin extends HTMLElement {
.form-group input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e9ecef;
border: 2px solid #cccccc;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s ease;
@@ -481,7 +485,7 @@ class KvLogin extends HTMLElement {
.qr-container {
width: 340px;
height: 340px;
border: 2px dashed #cccccc;
border: 2px solid #000000;
border-radius: 8px;
display: flex;
align-items: center;
@@ -490,7 +494,7 @@ class KvLogin extends HTMLElement {
.qr-placeholder {
text-align: center;
color: #6c757d;
color: #333333;
}
.qr-icon {

127
packages/kv-login/types/index.d.ts vendored Normal file
View File

@@ -0,0 +1,127 @@
type LoginMethods = 'password' | 'phone' | 'wechat' | 'wechat-mp' | 'wechat-mp-ticket';
interface KvLoginEventMap {
login: CustomEvent<{
method: LoginMethods;
data: LoginFormData[LoginMethods] | any;
}>;
/**
* 登录方式切换事件
*/
methodChange: CustomEvent<{
method: LoginMethods;
previousMethod?: LoginMethods;
}>;
/**
* 登录验证失败事件
*/
validationError: CustomEvent<{
method: LoginMethods;
errors: string[];
formData: LoginFormData[LoginMethods] | any;
}>;
}
interface KvLogin extends HTMLElement {
/**
* 设置登录方式
*/
setLoginMethods(methods: LoginMethod[]): void;
/**
* 添加自定义登录方式
*/
addLoginMethod(method: LoginMethod): void;
/**
* 移除登录方式
*/
removeLoginMethod(methodId: LoginMethods): void;
/**
* 获取当前选中的登录方式
*/
getSelectedMethod(): LoginMethods;
/**
* 设置默认登录方式
*/
setDefaultMethod(methodId: LoginMethods): void;
addEventListener<K extends keyof KvLoginEventMap>(
type: K,
listener: (this: KvLogin, ev: KvLoginEventMap[K]) => void,
options?: boolean | AddEventListenerOptions
): void;
removeEventListener<K extends keyof KvLoginEventMap>(
type: K,
listener: (this: KvLogin, ev: KvLoginEventMap[K]) => void,
options?: boolean | EventListenerOptions
): void;
}
declare global {
interface HTMLElementTagNameMap {
'kv-login': KvLogin;
}
namespace JSX {
interface IntrinsicElements {
'kv-login': KvLoginAttributes;
}
}
}
interface KvLoginAttributes extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> {
/**
* 自定义登录方式配置
*/
loginMethods?: LoginMethod[];
/**
* 自定义样式类名
*/
customClass?: string;
/**
* 是否显示登录方式选择器
*/
showMethodSelector?: boolean;
/**
* 默认选中的登录方式
*/
defaultMethod?: LoginMethods;
}
interface LoginMethod {
id: LoginMethods;
name: string;
icon: string | any; // 可以是emoji字符串、SVG字符串或其他图标类型
appid?: string;
disabled?: boolean;
order?: number; // 用于排序
}
interface LoginFormData {
password?: {
username: string;
password: string;
};
phone?: {
phone: string;
code: string;
};
wechat?: {
wechatCode: string;
};
'wechat-mp'?: {
wechatMpCode: string;
};
'wechat-mp-ticket'?: {
wechatMpCode: string;
ticket: string;
};
}
export {
KvLogin,
KvLoginEventMap,
KvLoginAttributes,
LoginMethods,
LoginMethod,
LoginFormData
};