From d828a491eeb70ffb9339914b8393431c9e0d2c92 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Sun, 28 Dec 2025 17:58:20 +0800 Subject: [PATCH] use components for component --- packages/kv-login/.npmrc | 2 - packages/kv-login/bun.config.ts | 15 - packages/kv-login/index.html | 84 --- packages/kv-login/package.json | 43 -- packages/kv-login/readme.md | 12 - packages/kv-login/src/main.ts | 6 - packages/kv-login/src/modules/login-handle.ts | 288 --------- packages/kv-login/src/modules/mitt.ts | 134 ---- packages/kv-login/src/modules/query.ts | 10 - packages/kv-login/src/modules/wx-mp/qr.ts | 57 -- packages/kv-login/src/modules/wx/load-js.ts | 21 - .../src/modules/wx/tencent-captcha.ts | 70 --- packages/kv-login/src/modules/wx/ws-login.ts | 61 -- packages/kv-login/src/pages/kv-login.ts | 590 ------------------ packages/kv-login/src/pages/kv-message.ts | 351 ----------- packages/kv-login/types/index.d.ts | 127 ---- packages/kv-login/vite.config.ts | 15 - 17 files changed, 1886 deletions(-) delete mode 100644 packages/kv-login/.npmrc delete mode 100644 packages/kv-login/bun.config.ts delete mode 100644 packages/kv-login/index.html delete mode 100644 packages/kv-login/package.json delete mode 100644 packages/kv-login/readme.md delete mode 100644 packages/kv-login/src/main.ts delete mode 100644 packages/kv-login/src/modules/login-handle.ts delete mode 100644 packages/kv-login/src/modules/mitt.ts delete mode 100644 packages/kv-login/src/modules/query.ts delete mode 100644 packages/kv-login/src/modules/wx-mp/qr.ts delete mode 100644 packages/kv-login/src/modules/wx/load-js.ts delete mode 100644 packages/kv-login/src/modules/wx/tencent-captcha.ts delete mode 100644 packages/kv-login/src/modules/wx/ws-login.ts delete mode 100644 packages/kv-login/src/pages/kv-login.ts delete mode 100644 packages/kv-login/src/pages/kv-message.ts delete mode 100644 packages/kv-login/types/index.d.ts delete mode 100644 packages/kv-login/vite.config.ts diff --git a/packages/kv-login/.npmrc b/packages/kv-login/.npmrc deleted file mode 100644 index 7446745..0000000 --- a/packages/kv-login/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN} -//registry.npmjs.org/:_authToken=${NPM_TOKEN} \ No newline at end of file diff --git a/packages/kv-login/bun.config.ts b/packages/kv-login/bun.config.ts deleted file mode 100644 index 4ba7316..0000000 --- a/packages/kv-login/bun.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { build } from 'bun'; - -await build({ - entrypoints: ["./src/main.ts"], - outdir: './dist', - target: 'browser', - format: 'esm', - naming: { - entry: 'app.js', - }, - minify: false, - sourcemap: false, -}); - -console.log('✅ Build complete: dist/app.js'); diff --git a/packages/kv-login/index.html b/packages/kv-login/index.html deleted file mode 100644 index f164f39..0000000 --- a/packages/kv-login/index.html +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - KvMessage Demo - - - - - -
- -
- - - \ No newline at end of file diff --git a/packages/kv-login/package.json b/packages/kv-login/package.json deleted file mode 100644 index a8fb6e2..0000000 --- a/packages/kv-login/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@kevisual/kv-login", - "version": "0.1.4", - "description": "", - "main": "src/main.ts", - "scripts": { - "dev": "vite", - "build": "bun bun.config.ts", - "postbuild": "dts -i src/main.ts -o app.d.ts", - "build:test": "vite build", - "prepub": "rm -rf ./dist && pnpm run build:test", - "pub": "ev deploy ./dist -k login -v 0.1.4 -u -y yes" - }, - "keywords": [], - "files": [ - "types", - "src", - "dist" - ], - "author": "abearxiong (https://www.xiongxiao.me)", - "license": "MIT", - "packageManager": "pnpm@10.26.2", - "publishConfig": { - "access": "public" - }, - "type": "module", - "dependencies": { - "@kevisual/context": "^0.0.4", - "@kevisual/query-login": "^0.0.7", - "crypto-js": "^4.2.0", - "lit-html": "^3.3.2", - "qrcode": "^1.5.4" - }, - "exports": { - ".": "./dist/app.js", - "./types": "./types/index.d.ts" - }, - "types": "./types/index.d.ts", - "devDependencies": { - "@kevisual/api": "^0.0.8", - "@types/bun": "^1.3.5" - } -} \ No newline at end of file diff --git a/packages/kv-login/readme.md b/packages/kv-login/readme.md deleted file mode 100644 index 699f2e4..0000000 --- a/packages/kv-login/readme.md +++ /dev/null @@ -1,12 +0,0 @@ -# 可视化登录组件 - -## 主题色 - -黑白 - - -```html - -
-
-``` \ No newline at end of file diff --git a/packages/kv-login/src/main.ts b/packages/kv-login/src/main.ts deleted file mode 100644 index 43d4853..0000000 --- a/packages/kv-login/src/main.ts +++ /dev/null @@ -1,6 +0,0 @@ -import './pages/kv-login' -import './pages/kv-message' - -export { loginEmitter } from './pages/kv-login' - -export { checkPluginLogin, clearCode } from './modules/login-handle'; \ No newline at end of file diff --git a/packages/kv-login/src/modules/login-handle.ts b/packages/kv-login/src/modules/login-handle.ts deleted file mode 100644 index 359ecab..0000000 --- a/packages/kv-login/src/modules/login-handle.ts +++ /dev/null @@ -1,288 +0,0 @@ -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'; -import { Query } from '@kevisual/query'; -import { QueryLoginBrowser } from '@kevisual/api/login' -export const message = createMessage(); -type LoginOpts = { - loginMethod: 'password' | 'web' | 'phone' | 'wechat' | 'wechat-mp' | 'wechat-mp-ticket', - data: any, - el: HTMLElement -} -/** - * 登录成功后重定向到首页 - */ -export const redirectHome = () => { - 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'); - } - // 从url上清除 code 参数, 清除 state 参数 - emit({ type: 'login-success', data: {} }); - setTimeout(() => { - clearCode(); - }, 1500); -} -export const loginHandle = async (opts: LoginOpts) => { - const { loginMethod, data, el } = opts - switch (loginMethod) { - case 'password': - await loginByPassword(data) - break - case 'phone': - await loginByPhone(data) - break - case 'wechat-mp': - await loginByWeChatMp(data) - break - case 'wechat': - await loginByWeChat(data) - break - case 'web': - await loginByWeb(data) - break - default: - console.warn('未知的登录方式:', loginMethod) - } -} -const loginByWeb = async (data: {}) => { - const url = new URL("https://kevisual.cn/api/router"); - const query = new Query({ url: "https://kevisual.cn/api/router" }) - const login = new QueryLoginBrowser({ query }) - // @ts-ignore - const res = login.loginWithWeb(url.origin, {}) - console.log('打开网页登录:', res) - window.open(res.url, '_blank'); - const status = await login.pollLoginStatus(res); - if (status) { - redirectHome() - } else { - message.error('网页登录失败,请重试') - } -} -/** - * 使用用户名和密码登录 - * @param data - * @returns - */ -const loginByPassword = async (data: { username: string, password: string }) => { - console.log('使用用户名密码登录:', data) - let needLogin = true; // 这里可以根据实际情况决定是否需要登录, 只能判断密码登录和手机号登录 - - const isLogin = await query.checkLocalToken() - if (isLogin) { - const loginUser = await query.checkLocalUser() - if (loginUser?.username === data?.username) { - const res = await query.getMe() - if (res.code === 200) { - needLogin = false - console.log('已登录,跳过登录步骤') - message.success('已登录') - } - } - } - if (!needLogin) { - redirectHome() - return; - } - const res = await query.login({ - username: data.username, - password: data.password - }) - if (res.code === 200) { - console.log('登录成功') - message.success('登录成功') - redirectHome() - } else { - message.error(`登录失败: ${res.message}`) - } -} - -const loginByPhone = async (data: { phone: string, code: string }) => { - console.log('使用手机号登录:', data) -} - -const loginByWeChat = async (data: { wechatCode: string }) => { - console.log('使用微信登录:', data) -} -const loginByWeChatMp = async (data: { wechatMpCode: string }) => { - console.log('使用微信公众号登录:', data) -} - -export const clearCode = () => { - const url = new URL(window.location.href); - // 清理 URL 中的 code 参数 - url.searchParams.delete('code'); - url.searchParams.delete('state'); - url.searchParams.delete('user-check'); - url.searchParams.delete('redirect'); - window.history.replaceState({}, document.title, url.toString()); -} -export const checkWechat = async () => { - const url = new URL(window.location.href); - const code = url.searchParams.get('code'); - const state = url.searchParams.get('state'); - if (state?.includes?.('-')) { - // 公众号登录流程,不在这里处理 - return; - } - if (!code) { - return; - } - const res = await query.loginByWechat({ code }); - if (res.code === 200) { - message.success('登录成功'); - redirectHome(); - } else { - message.error(res.message || '登录失败'); - clearCode(); - } -}; - -export const checkMpWechat = async () => { - const url = new URL(window.location.href); - const originState = url.searchParams.get('state'); - const [mpLogin, state] = originState ? originState.split('-') : [null, null]; - console.log('检查微信公众号登录流程:', mpLogin, state, originState); - if (mpLogin === '1') { - // 手机端扫描的时候访问的链接,跳转到微信公众号授权页面 - checkMpWechatInWx() - } else if (mpLogin === '2') { - const code = url.searchParams.get('code'); - // 推送登录成功状态到扫码端 - const res2 = await query.post({ - path: 'wx', - key: 'mplogin', - state, - code - }) - if (res2.code === 200) { - message.success('登录成功'); - } else { - message.error(res2.message || '登录失败'); - } - closePage(); - } -} -export const checkPluginLogin = async () => { - const userCheck = 'user-check'; - const url = new URL(location.href); - const redirect = url.searchParams.get('redirect'); - const redirectUrl = redirect ? decodeURIComponent(redirect) : ''; - const checkKey = url.searchParams.get(userCheck); - if (redirect && checkKey) { - // 通过refresh_token 刷新token - const me = await query.getMe(); - if (me.code === 200) { - message.success('登录插件中...'); - const token = await query.cacheStore.getAccessToken(); - const newRedirectUrl = new URL(redirectUrl); - newRedirectUrl.searchParams.set('token', token + ''); - setTimeout(() => { - window.open(newRedirectUrl.toString(), '_blank'); - }, 2000); - return; - } - // 刷新token失败,登陆页自己跳转 - } - console.log('checkKey', checkKey, redirectUrl); -} -const isWechat = () => { - const ua = navigator.userAgent.toLowerCase(); - return /micromessenger/i.test(ua); -}; - -const closePage = (time = 2000) => { - if (!isWechat()) { - setTimeout(() => { - window.close(); - }, time); - return; - } - // @ts-ignore - if (window.WeixinJSBridge) { - setTimeout(() => { - // @ts-ignore - window.WeixinJSBridge.call('closeWindow'); - }, time); - } else { - setTimeout(() => { - window.close(); - }, time); - } -}; -const checkMpWechatInWx = async () => { - const wxAuthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect` - const appid = WX_MP_APP_ID; - const url = new URL(window.location.href); - const originState = url.searchParams.get('state'); - let [mpLogin, state] = originState ? originState.split('-') : [null, null]; - - const redirectURL = new URL(url.pathname, url.origin); - state = '2-' + state; // 标记为第二步登录 - const redirect_uri = encodeURIComponent(redirectURL.toString()) - document.body.innerHTML = `

正在准备跳转到微信公众号授权页面...

`; - const scope = `snsapi_userinfo` - if (!state) { - alert('Invalid state. Please try again later.'); - closePage(); - return; - } - const link = wxAuthUrl.replace('APPID', appid).replace('REDIRECT_URI', redirect_uri).replace('SCOPE', scope).replace('STATE', state); - setTimeout(() => { - window.location.href = link; - }, 100); -} - -setTimeout(() => { - checkMpWechat(); -}, 100); - -export const getQrCode = async () => { - const res = await query.post({ - path: 'wx', - key: 'get-qrcode-ticket' - }) - if (res.code !== 200) { - message.error('获取二维码失败'); - return null; - } - return res?.data as { ticket: string, url: string } -} - -export const checkMpQrCodeLogin = (ticket: string) => { - let run = true; - const fetchLoginStatus = async () => { - const res = await query.post({ - path: 'wx', - key: 'check-qrcode-login', - payload: { ticket } - }) - if (res.code === 200) { - message.success('登录成功'); - clearTimeout(timer); - redirectHome(); - } else { - // message.error(res.message || '登录失败'); - if (res.code === 401) { - console.log('等待扫码登录...'); - } else { - console.log('扫码登录状态:', res); - } - if (run) { - setTimeout(fetchLoginStatus, 2000); - } - } - } - const timer = setTimeout(fetchLoginStatus, 2000); - const close = () => { - console.log('停止检测扫码登录状态'); - clearTimeout(timer); - run = false - } - return close; -} diff --git a/packages/kv-login/src/modules/mitt.ts b/packages/kv-login/src/modules/mitt.ts deleted file mode 100644 index b79a923..0000000 --- a/packages/kv-login/src/modules/mitt.ts +++ /dev/null @@ -1,134 +0,0 @@ -export interface EventData { - type: string; - data: T; -} - -export type EventHandler = (event: EventData) => void; - -export class EventEmitter { - private events: Map> = new Map(); - - /** - * 监听事件 - * @param type 事件类型 - * @param handler 事件处理函数 - */ - on(type: string, handler: EventHandler): void { - if (!this.events.has(type)) { - this.events.set(type, new Set()); - } - this.events.get(type)!.add(handler); - } - - /** - * 移除事件监听器 - * @param type 事件类型 - * @param handler 事件处理函数 (可选,如果不提供则移除该类型的所有监听器) - */ - off(type: string, handler?: EventHandler): 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(event: EventData): 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(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(type: string, handler: EventHandler): void { - const onceHandler: EventHandler = (event) => { - handler(event); - this.off(type, onceHandler); - }; - this.on(type, onceHandler); - } -} - -// 创建默认的事件发射器实例 -export const eventEmitter = new EventEmitter(); - -// 导出便捷方法 -export const on = (type: string, handler: EventHandler) => eventEmitter.on(type, handler); -export const off = (type: string, handler?: EventHandler) => eventEmitter.off(type, handler); -export const emit = (event: EventData) => eventEmitter.emit(event); -export const emitSimple = (type: string, data: T) => eventEmitter.emitSimple(type, data); -export const clear = () => eventEmitter.clear(); -export const once = (type: string, handler: EventHandler) => eventEmitter.once(type, handler); - -// 默认导出 -export default eventEmitter; \ No newline at end of file diff --git a/packages/kv-login/src/modules/query.ts b/packages/kv-login/src/modules/query.ts deleted file mode 100644 index 43b43a4..0000000 --- a/packages/kv-login/src/modules/query.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Query } from '@kevisual/query' -import { QueryLoginBrowser } from '@kevisual/query-login'; - - -export const queryBase = new Query() - -export const query = new QueryLoginBrowser({ - query: queryBase, -}) - diff --git a/packages/kv-login/src/modules/wx-mp/qr.ts b/packages/kv-login/src/modules/wx-mp/qr.ts deleted file mode 100644 index bbdb2b4..0000000 --- a/packages/kv-login/src/modules/wx-mp/qr.ts +++ /dev/null @@ -1,57 +0,0 @@ -import QRCode, { QRCodeToDataURLOptions } from 'qrcode'; -import { redirectHome } from '../login-handle.ts'; -import { query } from '../query.ts'; -export const useCreateLoginQRCode = (el?: HTMLCanvasElement) => { - var opts: QRCodeToDataURLOptions = { - errorCorrectionLevel: 'H', - type: 'image/jpeg', - margin: 1, - width: 300, - }; - let timer: any = null; - const createQrcode = async (state: string) => { - const url = new URL(window.location.href); - const loginUrl = new URL(url.pathname, url.origin); - loginUrl.searchParams.set('state', '1-' + state); - console.log('生成登录二维码链接:', loginUrl.toString()); - var img = el || document.getElementById('qrcode')! as HTMLCanvasElement; - const res = await QRCode.toDataURL(img!, loginUrl.toString(), opts); - }; - const checkLogin = async (state: string) => { - const res = await fetch(`/api/router?path=wx&key=checkLogin&state=${state}`).then((res) => res.json()); - if (res.code === 200) { - console.log(res); - const token = res.data; - if (token) { - localStorage.setItem('token', token.accessToken); - await query.setLoginToken(token); - } - clear(); - setTimeout(() => { - redirectHome(); - }, 1000); - } else { - timer = setTimeout(() => { - checkLogin(state); - console.log('继续检测登录状态'); - }, 2000); - } - }; - // 随机生成一个state - const state = Math.random().toString(36).substring(2, 15); - createQrcode(state); - checkLogin(state); - const timer2 = setInterval(() => { - const state = Math.random().toString(36).substring(2, 15); - clearTimeout(timer); // 清除定时器 - createQrcode(state); // 90秒后更新二维码 - checkLogin(state); - console.log('更新二维码'); - }, 90000); - const clear = () => { - clearTimeout(timer); - clearInterval(timer2); - console.log('停止检测登录状态'); - } - return { createQrcode, clear }; -}; \ No newline at end of file diff --git a/packages/kv-login/src/modules/wx/load-js.ts b/packages/kv-login/src/modules/wx/load-js.ts deleted file mode 100644 index a02c12c..0000000 --- a/packages/kv-login/src/modules/wx/load-js.ts +++ /dev/null @@ -1,21 +0,0 @@ -// - -export const dynimicLoadTcapTcha = async (): Promise => { - return new Promise((resolve, reject) => { - const script = document.createElement('script') - script.type = 'text/javascript' - script.id = 'tencent-captcha' - if (document.getElementById('tencent-captcha')) { - resolve(true) - return - } - script.src = 'https://turing.captcha.qcloud.com/TCaptcha.js' - script.onload = () => { - resolve(true) - } - script.onerror = (error) => { - reject(error) - } - document.body.appendChild(script) - }) -} diff --git a/packages/kv-login/src/modules/wx/tencent-captcha.ts b/packages/kv-login/src/modules/wx/tencent-captcha.ts deleted file mode 100644 index a1169b2..0000000 --- a/packages/kv-login/src/modules/wx/tencent-captcha.ts +++ /dev/null @@ -1,70 +0,0 @@ -// 定义回调函数 -export function callback(res: any) { - // 第一个参数传入回调结果,结果如下: - // ret Int 验证结果,0:验证成功。2:用户主动关闭验证码。 - // ticket String 验证成功的票据,当且仅当 ret = 0 时 ticket 有值。 - // CaptchaAppId String 验证码应用ID。 - // bizState Any 自定义透传参数。 - // randstr String 本次验证的随机串,后续票据校验时需传递该参数。 - console.log('callback:', res); - // res(用户主动关闭验证码)= {ret: 2, ticket: null} - // res(验证成功) = {ret: 0, ticket: "String", randstr: "String"} - // res(请求验证码发生错误,验证码自动返回terror_前缀的容灾票据) = {ret: 0, ticket: "String", randstr: "String", errorCode: Number, errorMessage: "String"} - // 此处代码仅为验证结果的展示示例,真实业务接入,建议基于ticket和errorCode情况做不同的业务处理 - if (res.ret === 0) { - // 复制结果至剪切板 - var str = '【randstr】->【' + res.randstr + '】 【ticket】->【' + res.ticket + '】'; - var ipt = document.createElement('input'); - ipt.value = str; - document.body.appendChild(ipt); - ipt.select(); - document.body.removeChild(ipt); - alert('1. 返回结果(randstr、ticket)已复制到剪切板,ctrl+v 查看。 2. 打开浏览器控制台,查看完整返回结果。'); - } -} -export type TencentCaptcha = { - actionDuration?: number; - appid?: string; - bizState?: any; - randstr?: string; - ret: number; - sid?: string; - ticket?: string; - errorCode?: number; - errorMessage?: string; - verifyDuration?: number; -}; -// 定义验证码触发事件 -export const checkCaptcha = (captchaAppId: string): Promise => { - return new Promise((resolve, reject) => { - const callback = (res: TencentCaptcha) => { - console.log('callback:', res); - if (res.ret === 0) { - resolve(res); - } else { - reject(res); - } - }; - const appid = captchaAppId; - try { - // 生成一个验证码对象 - // CaptchaAppId:登录验证码控制台,从【验证管理】页面进行查看。如果未创建过验证,请先新建验证。注意:不可使用客户端类型为小程序的CaptchaAppId,会导致数据统计错误。 - //callback:定义的回调函数 - // @ts-ignore - var captcha = new TencentCaptcha(appid, callback, {}); - // 调用方法,显示验证码 - captcha.show(); - } catch (error) { - // 加载异常,调用验证码js加载错误处理函数 - var ticket = 'terror_1001_' + appid + '_' + Math.floor(new Date().getTime() / 1000); - // 生成容灾票据或自行做其它处理 - callback({ - ret: 0, - randstr: '@' + Math.random().toString(36).substring(2), - ticket: ticket, - errorCode: 1001, - errorMessage: 'jsload_error', - }); - } - }); -}; diff --git a/packages/kv-login/src/modules/wx/ws-login.ts b/packages/kv-login/src/modules/wx/ws-login.ts deleted file mode 100644 index bdfbeba..0000000 --- a/packages/kv-login/src/modules/wx/ws-login.ts +++ /dev/null @@ -1,61 +0,0 @@ -type WxLoginConfig = { - redirect_uri?: string; - appid?: string; - scope?: string; - state?: string; - style?: string; -}; -export const createLogin = async (config?: WxLoginConfig) => { - let redirect_uri = config?.redirect_uri; - const { appid } = config || {}; - if (!redirect_uri) { - redirect_uri = window.location.href; - } - const url = new URL(redirect_uri); // remove code and state params - url.searchParams.delete('code'); - url.searchParams.delete('state'); - redirect_uri = url.toString(); - - console.log('redirect_uri', redirect_uri); - if (!appid) { - console.error('appid is not cant be empty'); - return; - } - // @ts-ignore - const obj = new WxLogin({ - self_redirect: false, - id: 'weixinLogin', // 需要显示的容器id - appid: appid, // 微信开放平台appid wx******* - scope: 'snsapi_login', // 网页默认即可 snsapi_userinfo - redirect_uri: encodeURIComponent(redirect_uri), // 授权成功后回调的url - state: Math.ceil(Math.random() * 1000), // 可设置为简单的随机数加session用来校验 - stylelite: true, // 是否使用简洁模式 - // https://juejin.cn/post/6982473580063752223 - href: "data:text/css;base64,LmltcG93ZXJCb3ggLnFyY29kZSB7d2lkdGg6IDIwMHB4O30NCi5pbXBvd2VyQm94IC50aXRsZSB7ZGlzcGxheTogbm9uZTt9DQouaW1wb3dlckJveCAuaW5mbyB7d2lkdGg6IDIwMHB4O30NCi5zdGF0dXNfaWNvbiB7ZGlzcGxheTogbm9uZX0NCi5pbXBvd2VyQm94IC5zdGF0dXMge3RleHQtYWxpZ246IGNlbnRlcjt9" - }); - const login = document.querySelector('#weixinLogin') - if (login) { - // login 下的 iframe 样式调整 - const iframe = login.querySelector('iframe'); - if (iframe) { - // iframe.style.width = '200px'; - iframe.style.height = '300px'; - } - } - return obj; -}; -export const wxId = 'weixinLogin'; -export function setWxerwma(config?: WxLoginConfig) { - const s = document.createElement('script'); - s.type = 'text/javascript'; - s.src = '//res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js'; - s.id = 'weixinLogin-js'; - if (document.getElementById('weixinLogin-js')) { - createLogin(config); - return; - } - const wxElement = document.body.appendChild(s); - wxElement.onload = function () { - createLogin(config); - }; -} diff --git a/packages/kv-login/src/pages/kv-login.ts b/packages/kv-login/src/pages/kv-login.ts deleted file mode 100644 index 2632e14..0000000 --- a/packages/kv-login/src/pages/kv-login.ts +++ /dev/null @@ -1,590 +0,0 @@ -import { render, html } from 'lit-html' -import { unsafeHTML } from 'lit-html/directives/unsafe-html.js' -import { loginHandle, checkWechat, getQrCode, checkMpQrCodeLogin, redirectHome } 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 - name: string - icon: any - appid?: string -} -const wxmpSvg = `` -const wxOpenSvg = `` -const phone = `` -const pwd = `` -const web = `` -const icons: any = { - pwd, - web, - phone, - wxmpSvg, - wxOpenSvg -} -const DefaultLoginMethods: LoginMethod[] = [ - { id: 'password', name: '密码登录', icon: 'pwd' }, - { id: 'web', name: '网页登录', icon: 'web' }, - { id: 'wechat', name: '微信登录', icon: 'wxmpSvg', appid: "wx9378885c8390e09b" }, - { id: 'wechat-mp', name: '微信公众号', icon: 'wxOpenSvg', appid: WX_MP_APP_ID }, - { id: 'wechat-mp-ticket', name: '微信公众号', icon: 'wxOpenSvg' }, - { id: 'phone', name: '手机号登录', icon: 'phone' } -] -const LoginMethods = ['password', 'web', 'phone', 'wechat', 'wechat-mp', 'wechat-mp-ticket'] as const; -type LoginMethods = 'password' | 'web' | 'phone' | 'wechat' | 'wechat-mp' | 'wechat-mp-ticket'; - -const getLoginMethodByDomain = (): LoginMethod[] => { - let domain = window.location.host - let methods: LoginMethods[] = [] - const has51 = domain.includes('localhost') && (domain.endsWith('51515') || domain.endsWith('51015')); - if (has51) { - domain = 'localhost' - } - switch (domain) { - case 'kevisual.xiongxiao.me': - methods = ['password', 'wechat-mp'] - break; - case 'kevisual.cn': - methods = ['password', 'wechat-mp-ticket', 'wechat',] - break; - case 'localhost': - methods = ['password', 'web'] - break - default: - methods = ['password', 'web', 'phone', 'wechat', 'wechat-mp', 'wechat-mp-ticket'] - break; - } - return DefaultLoginMethods.filter(method => methods.includes(method.id)) -} -const getLoginMethod = (methods: LoginMethods[]): LoginMethod[] => { - return DefaultLoginMethods.filter(method => methods.includes(method.id)) -} -class KvLogin extends HTMLElement { - private selectedMethod: LoginMethods = 'password' - - private loginMethods: LoginMethod[] = getLoginMethodByDomain(); - setLoginMethods(methods: LoginMethod[]) { - this.loginMethods = methods - this.render() - } - constructor() { - super() - } - - connectedCallback() { - this.attachShadow({ mode: 'open' }) - this.render() - this.bindEvents() - checkWechat() - const method = this.getAttribute('method'); - if (method) { - const methods = method ? method.split(',') as LoginMethods[] : []; - if (methods.length > 0) { - const loginMethods = methods.filter(m => LoginMethods.includes(m)); - if (loginMethods.length > 0) { - this.loginMethods = getLoginMethod(loginMethods) - this.selectedMethod = loginMethods[0] - return; - } - } - this.loginMethods = getLoginMethodByDomain(); - this.selectedMethod = this.loginMethods[0].id; - } - } - #clearTimer: any = null; - private selectLoginMethod(methodId: LoginMethods) { - this.selectedMethod = methodId - this.render() - if (this.#clearTimer) { - this.#clearTimer(); - this.#clearTimer = null; - } - } - private getMethodData(methodId: LoginMethods): LoginMethod | undefined { - return this.loginMethods.find(method => method.id === methodId); - } - private bindEvents() { - if (!this.shadowRoot) return - // 使用事件委托来处理登录方式切换 - this.shadowRoot.addEventListener('click', (e) => { - const target = e.target as HTMLElement - const methodButton = target.closest('.login-method') - if (methodButton) { - const methodId = methodButton.getAttribute('data-method') as LoginMethods - if (methodId) { - this.selectLoginMethod(methodId) - } - } - }) - - // 使用事件委托来处理表单提交 - this.shadowRoot.addEventListener('submit', (e) => { - const target = e.target as HTMLElement - if (target && target.id === 'loginForm') { - e.preventDefault() - this.handleLogin() - } - }) - loginEmitter.on('login-success', () => { - console.log('收到登录成功事件,处理后续逻辑') - }); - } - - private handleLogin() { - const formData = this.getFormData() - // console.log('登录方式:', this.selectedMethod) - // console.log('登录数据:', formData) - loginHandle({ - loginMethod: this.selectedMethod, - data: formData, - el: this - }) - // 这里可以触发自定义事件,通知父组件 - this.dispatchEvent(new CustomEvent('login', { - detail: { - method: this.selectedMethod, - data: formData - }, - bubbles: true - })) - } - - private getFormData(): any { - if (!this.shadowRoot) return {} - - switch (this.selectedMethod) { - case 'password': - const username = this.shadowRoot.querySelector('#username') as HTMLInputElement - const password = this.shadowRoot.querySelector('#password') as HTMLInputElement - return { - username: username?.value || '', - password: password?.value || '' - } - case 'web': - return {} - case 'phone': - const phone = this.shadowRoot.querySelector('#phone') as HTMLInputElement - const code = this.shadowRoot.querySelector('#code') as HTMLInputElement - return { - phone: phone?.value || '', - code: code?.value || '' - } - - case 'wechat': - return { - wechatCode: 'mock_wechat_code' - } - case 'wechat-mp': - return { - wechatMpCode: 'mock_wechat_mp_code' - } - default: - return {} - } - } - - private renderPasswordForm() { - return html` - - ` - } - private renderWebForm() { - return html` - - ` - } - private renderPhoneForm() { - return html` - - ` - } - - private renderWechatForm() { - return html` - - ` - } - private renderWechatMpForm() { - const that = this - setTimeout(() => { - const qrcode = that.shadowRoot!.querySelector('#qrcode'); - const { clear } = useCreateLoginQRCode(qrcode as HTMLCanvasElement); - that.#clearTimer = clear; - }, 0) - return html` - - ` - } - private renderWechatMpTicketForm() { - const that = this; - setTimeout(async () => { - const data = await getQrCode(); - if (!data) return; - const imgEl = that.shadowRoot!.querySelector('.qrcode') as HTMLImageElement; - if (data.url) { - imgEl.src = data.url; - // TODO: 轮询检测登录状态 - const clear = checkMpQrCodeLogin(data.ticket) - // 当切换登录方式时,停止轮询 - that.#clearTimer = clear - } - }, 0) - return html` - - ` - } - - private sendVerificationCode() { - console.log('发送验证码') - // 这里可以实现发送验证码的逻辑 - } - - private refreshQR() { - console.log('刷新二维码') - // 这里可以实现刷新二维码的逻辑 - } - - - private renderLoginForm() { - const data = this.getMethodData(this.selectedMethod); - switch (this.selectedMethod) { - case 'password': - return this.renderPasswordForm() - case 'web': - return this.renderWebForm() - case 'phone': - return this.renderPhoneForm() - case 'wechat': - setWxerwma({ appid: data?.appid! || "" }); - return this.renderWechatForm() - case 'wechat-mp': - return this.renderWechatMpForm() - case 'wechat-mp-ticket': - return this.renderWechatMpTicketForm() - default: - return this.renderPasswordForm() - } - } - - render() { - if (!this.shadowRoot) return - - const renderIcon = (icon: any) => { - // 如果是emoji字符,直接返回 - if (typeof icon === 'string' && !icons[icon]) { - return html`${icon}` - } - // 如果是SVG引用,从icons对象获取 - if (typeof icon === 'string' && icons[icon]) { - return html`${unsafeHTML(icons[icon])}` - } - // 如果直接是SVG内容 - if (typeof icon === 'string' && (icon.includes('${unsafeHTML(icon)}` - } - // 默认情况 - return html`${icon}` - } - const template = html` - - - - ` - - render(template, this.shadowRoot) - } -} - -customElements.define('kv-login', KvLogin) \ No newline at end of file diff --git a/packages/kv-login/src/pages/kv-message.ts b/packages/kv-login/src/pages/kv-message.ts deleted file mode 100644 index 3cbd9f7..0000000 --- a/packages/kv-login/src/pages/kv-message.ts +++ /dev/null @@ -1,351 +0,0 @@ -import { html, render, TemplateResult } from 'lit-html' - -export interface KvMessageOptions { - type?: 'success' | 'error' | 'loading' - message: string - duration?: number - closable?: boolean - position?: 'center' | 'right' -} - -class KvMessage extends HTMLElement { - private options: KvMessageOptions - private timer: number | null = null - - constructor() { - super() - this.options = { - type: 'success', - message: '', - duration: 2000, - closable: true - } - } - - connectedCallback() { - this.render() - } - - setOptions(options: KvMessageOptions) { - this.options = { ...this.options, ...options } - this.render() - } - - private render() { - const { type, message, closable } = this.options - - const getTypeIcon = () => { - switch (type) { - case 'success': - return '✓' - case 'error': - return '✕' - case 'loading': - return html`
` - default: - return '' - } - } - - const template: TemplateResult = html` - - -
-
- ${getTypeIcon()} -
-
${message}
- ${closable ? html` - - ` : ''} -
- ` - - render(template, this) - - if (type !== 'loading' && this.options.duration && this.options.duration > 0) { - this.setTimer() - } - } - - private setTimer() { - if (this.timer) { - clearTimeout(this.timer) - } - - this.timer = window.setTimeout(() => { - this.remove() - }, this.options.duration) - } - - remove() { - if (this.timer) { - clearTimeout(this.timer) - this.timer = null - } - - this.classList.add('removing') - - setTimeout(() => { - if (this.parentNode) { - this.parentNode.removeChild(this) - } - }, 300) - } - - disconnectedCallback() { - if (this.timer) { - clearTimeout(this.timer) - this.timer = null - } - } -} - -customElements.define('kv-message', KvMessage) - -export class KvMessageManager { - private static instance: KvMessageManager - private container: HTMLElement | null = null - private defaultPosition: 'center' | 'right' = 'center' - - static getInstance(): KvMessageManager { - if (!KvMessageManager.instance) { - KvMessageManager.instance = new KvMessageManager() - } - return KvMessageManager.instance - } - - setDefaultPosition(position: 'center' | 'right') { - this.defaultPosition = position - } - - private getContainer(position?: 'center' | 'right'): HTMLElement { - const finalPosition = position || this.defaultPosition - - if (!this.container) { - this.container = document.getElementById('messages') - if (!this.container) { - this.container = document.createElement('div') - this.container.id = 'messages' - - if (finalPosition === 'center') { - this.container.style.cssText = ` - position: fixed; - top: 20px; - left: 50%; - transform: translateX(-50%); - z-index: 9999; - display: flex; - gap: 8px; - flex-direction: column; - align-items: center; - pointer-events: none; - ` - } else { - this.container.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - z-index: 9999; - display: flex; - gap: 8px; - flex-direction: column; - align-items: flex-end; - pointer-events: none; - ` - } - - document.body.appendChild(this.container) - } - } - return this.container - } - - show(options: KvMessageOptions): KvMessage { - const container = this.getContainer(options.position) - - const message = document.createElement('kv-message') as KvMessage - message.setOptions(options) - - message.style.cssText = 'pointer-events: auto;' - - container.appendChild(message) - - return message - } - - success(message: string, options?: { duration?: number; position?: 'center' | 'right'; closable?: boolean }): KvMessage { - return this.show({ - type: 'success', - message, - duration: options?.duration || 2000, - position: options?.position, - closable: options?.closable - }) - } - - error(message: string, options?: { duration?: number; position?: 'center' | 'right'; closable?: boolean }): KvMessage { - return this.show({ - type: 'error', - message, - duration: options?.duration || 3000, - position: options?.position, - closable: options?.closable - }) - } - - loading(message: string, options?: { position?: 'center' | 'right'; closable?: boolean }): KvMessage { - return this.show({ - type: 'loading', - message, - duration: 0, - position: options?.position, - closable: options?.closable - }) - } - - remove(message: KvMessage) { - message.remove() - } - - clear() { - const container = this.getContainer() - const messages = container.querySelectorAll('kv-message') - messages.forEach(message => { - (message as KvMessage).remove() - }) - } -} - -export const createMessage = () => KvMessageManager.getInstance() - -// 将 createMessage 暴露到全局,以便 HTML 中的 JavaScript 可以使用 -declare global { - interface Window { - createMessage: typeof createMessage - } -} - -window.createMessage = createMessage \ No newline at end of file diff --git a/packages/kv-login/types/index.d.ts b/packages/kv-login/types/index.d.ts deleted file mode 100644 index 4515b11..0000000 --- a/packages/kv-login/types/index.d.ts +++ /dev/null @@ -1,127 +0,0 @@ -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( - type: K, - listener: (this: KvLogin, ev: KvLoginEventMap[K]) => void, - options?: boolean | AddEventListenerOptions - ): void; - removeEventListener( - 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, 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 -}; \ No newline at end of file diff --git a/packages/kv-login/vite.config.ts b/packages/kv-login/vite.config.ts deleted file mode 100644 index c56a679..0000000 --- a/packages/kv-login/vite.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from 'vite'; - -const idDev = process.env.NODE_ENV === 'development'; -export default defineConfig({ - base: idDev ? '/' : '/root/kv-login-test/', - server: { - proxy: { - '/api': { - target: 'https://kevisual.xiongxiao.me', - changeOrigin: true, - secure: false, - } - } - } -});