update add login modules
This commit is contained in:
84
packages/kv-login/index.html
Normal file
84
packages/kv-login/index.html
Normal file
@@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KvMessage Demo</title>
|
||||
<script type="module" src="./src/main.ts"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.demo-button.success {
|
||||
background: #52c41a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.demo-button.success:hover {
|
||||
background: #389e0d;
|
||||
}
|
||||
|
||||
.demo-button.error {
|
||||
background: #ff4d4f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.demo-button.error:hover {
|
||||
background: #cf1322;
|
||||
}
|
||||
|
||||
.demo-button.loading {
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.demo-button.loading:hover {
|
||||
background: #096dd9;
|
||||
}
|
||||
|
||||
.login-section {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="demo-container">
|
||||
<div class="login-section">
|
||||
<h2>登录组件</h2>
|
||||
<kv-login id="loginComponent">
|
||||
<div id="weixinLogin"></div>
|
||||
</kv-login>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
22
packages/kv-login/package.json
Normal file
22
packages/kv-login/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "kv-login",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"prepub": "rm -rf ./dist && pnpm run build",
|
||||
"pub":"ev deploy ./dist -k kv-login-test -v 0.0.1 -u -y yes"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.19.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@kevisual/query-login": "^0.0.6",
|
||||
"lit-html": "^3.3.1",
|
||||
"qrcode": "^1.5.4"
|
||||
}
|
||||
}
|
||||
2
packages/kv-login/src/main.ts
Normal file
2
packages/kv-login/src/main.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import './pages/kv-login'
|
||||
import './pages/kv-message'
|
||||
188
packages/kv-login/src/modules/login-handle.ts
Normal file
188
packages/kv-login/src/modules/login-handle.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { query } from './query.ts';
|
||||
import { createMessage } from '../pages/kv-message.ts';
|
||||
import { WX_MP_APP_ID } from '../pages/kv-login.ts';
|
||||
export const message = createMessage();
|
||||
type LoginOpts = {
|
||||
loginMethod: 'password' | 'phone' | 'wechat' | 'wechat-mp',
|
||||
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');
|
||||
}
|
||||
}
|
||||
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
|
||||
default:
|
||||
console.warn('未知的登录方式:', loginMethod)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const clearCode = () => {
|
||||
const url = new URL(window.location.href);
|
||||
// 清理 URL 中的 code 参数
|
||||
url.searchParams.delete('code');
|
||||
url.searchParams.delete('state');
|
||||
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();
|
||||
}
|
||||
}
|
||||
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 = `<p>正在准备跳转到微信公众号授权页面...</p>`;
|
||||
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);
|
||||
10
packages/kv-login/src/modules/query.ts
Normal file
10
packages/kv-login/src/modules/query.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Query } from '@kevisual/query'
|
||||
import { QueryLoginBrowser } from '@kevisual/query-login';
|
||||
|
||||
|
||||
export const queryBase = new Query()
|
||||
|
||||
export const query = new QueryLoginBrowser({
|
||||
query: queryBase,
|
||||
})
|
||||
|
||||
57
packages/kv-login/src/modules/wx-mp/qr.ts
Normal file
57
packages/kv-login/src/modules/wx-mp/qr.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
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 };
|
||||
};
|
||||
21
packages/kv-login/src/modules/wx/load-js.ts
Normal file
21
packages/kv-login/src/modules/wx/load-js.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// <script src="https://turing.captcha.qcloud.com/TCaptcha.js"></script>
|
||||
|
||||
export const dynimicLoadTcapTcha = async (): Promise<boolean> => {
|
||||
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)
|
||||
})
|
||||
}
|
||||
70
packages/kv-login/src/modules/wx/tencent-captcha.ts
Normal file
70
packages/kv-login/src/modules/wx/tencent-captcha.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
// 定义回调函数
|
||||
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<TencentCaptcha> => {
|
||||
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',
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
61
packages/kv-login/src/modules/wx/ws-login.ts
Normal file
61
packages/kv-login/src/modules/wx/ws-login.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
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);
|
||||
};
|
||||
}
|
||||
449
packages/kv-login/src/pages/kv-login.ts
Normal file
449
packages/kv-login/src/pages/kv-login.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
import { render, html } from 'lit-html'
|
||||
import { loginHandle, checkWechat } from '../modules/login-handle.ts'
|
||||
import { setWxerwma } from '../modules/wx/ws-login.ts';
|
||||
import { useCreateLoginQRCode } from '../modules/wx-mp/qr.ts';
|
||||
export const WX_MP_APP_ID = "wxff97d569b1db16b6";
|
||||
interface LoginMethod {
|
||||
id: LoginMethods
|
||||
name: string
|
||||
icon: string
|
||||
appid?: string
|
||||
}
|
||||
const DefaultLoginMethods: LoginMethod[] = [
|
||||
{ id: 'password', name: '密码登录', icon: '🔒' },
|
||||
{ id: 'wechat', name: '微信登录', icon: '💬', appid: "wx9378885c8390e09b" },
|
||||
{ id: 'wechat-mp', name: '微信公众号登录', icon: '💬', appid: WX_MP_APP_ID },
|
||||
// { id: 'phone', name: '手机号登录', icon: '📱' }
|
||||
]
|
||||
type LoginMethods = 'password' | 'phone' | 'wechat' | 'wechat-mp'
|
||||
|
||||
class KvLogin extends HTMLElement {
|
||||
private selectedMethod: LoginMethods = 'password'
|
||||
|
||||
private loginMethods: LoginMethod[] = DefaultLoginMethods
|
||||
setLoginMethods(methods: LoginMethod[]) {
|
||||
this.loginMethods = methods
|
||||
this.render()
|
||||
}
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.attachShadow({ mode: 'open' })
|
||||
this.render()
|
||||
this.bindEvents()
|
||||
checkWechat()
|
||||
}
|
||||
#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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 '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`
|
||||
<form id="loginForm" class="login-form">
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="请输入用户名"
|
||||
autocomplete="username"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="请输入密码"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" class="login-button">登录</button>
|
||||
</form>
|
||||
`
|
||||
}
|
||||
|
||||
private renderPhoneForm() {
|
||||
return html`
|
||||
<form id="loginForm" class="login-form">
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
name="phone"
|
||||
placeholder="请输入手机号"
|
||||
pattern="[0-9]{11}"
|
||||
autocomplete="tel"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group code-group">
|
||||
<input
|
||||
type="text"
|
||||
id="code"
|
||||
name="code"
|
||||
placeholder="请输入验证码"
|
||||
autocomplete="one-time-code"
|
||||
required
|
||||
/>
|
||||
<button type="button" class="code-button" @click=${this.sendVerificationCode}>获取验证码</button>
|
||||
</div>
|
||||
<button type="submit" class="login-button">登录</button>
|
||||
</form>
|
||||
`
|
||||
}
|
||||
|
||||
private renderWechatForm() {
|
||||
return html`
|
||||
<div class="wechat-login">
|
||||
<slot></slot>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
private renderWechatMpForm() {
|
||||
const that = this
|
||||
setTimeout(() => {
|
||||
const qrcode = that.shadowRoot!.querySelector('#qrcode');
|
||||
const { clear } = useCreateLoginQRCode(qrcode as HTMLCanvasElement);
|
||||
that.#clearTimer = clear;
|
||||
}, 0)
|
||||
return html`
|
||||
<div class="wechat-login">
|
||||
<div class="qr-container">
|
||||
<div class="qr-placeholder">
|
||||
<canvas id='qrcode' width='300' height='300'></canvas>
|
||||
<p class="qr-desc">请使用微信扫描二维码登录</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
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 'phone':
|
||||
return this.renderPhoneForm()
|
||||
case 'wechat':
|
||||
setWxerwma({ appid: data?.appid! || "" });
|
||||
return this.renderWechatForm()
|
||||
case 'wechat-mp':
|
||||
return this.renderWechatMpForm()
|
||||
default:
|
||||
return this.renderPasswordForm()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.shadowRoot) return
|
||||
|
||||
const template = html`
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 400px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.login-sidebar {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-methods {
|
||||
display: flex;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.login-method {
|
||||
flex: 1;
|
||||
padding: 16px 8px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.login-method:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.login-method.active {
|
||||
background: white;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.login-method.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: #007bff;
|
||||
}
|
||||
|
||||
.method-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.method-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-content {
|
||||
padding: 32px 24px;
|
||||
}
|
||||
.impowerBox .qrcode {
|
||||
width: 200px !important;
|
||||
}
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.code-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.code-group input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.code-button {
|
||||
padding: 0 16px;
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.code-button:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
padding: 12px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.wechat-login {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
border: 2px dashed #e9ecef;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.qr-placeholder {
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.qr-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.qr-desc {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.refresh-button {
|
||||
padding: 8px 16px;
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.refresh-button:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="login-sidebar">
|
||||
<div class="login-methods">
|
||||
${this.loginMethods.map(method => html`
|
||||
<button
|
||||
class="login-method ${this.selectedMethod === method.id ? 'active' : ''}"
|
||||
data-method="${method.id}"
|
||||
>
|
||||
<span class="method-icon">${method.icon}</span>
|
||||
<span class="method-name">${method.name}</span>
|
||||
</button>
|
||||
`)}
|
||||
</div>
|
||||
|
||||
<div class="login-content">
|
||||
${this.renderLoginForm()}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
render(template, this.shadowRoot)
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('kv-login', KvLogin)
|
||||
351
packages/kv-login/src/pages/kv-message.ts
Normal file
351
packages/kv-login/src/pages/kv-message.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
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`<div class="loading-spinner"></div>`
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const template: TemplateResult = html`
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
background: white;
|
||||
position: relative;
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.message-container.success {
|
||||
border-left: 4px solid #52c41a;
|
||||
}
|
||||
|
||||
.message-container.error {
|
||||
border-left: 4px solid #ff4d4f;
|
||||
}
|
||||
|
||||
.message-container.loading {
|
||||
border-left: 4px solid #1890ff;
|
||||
}
|
||||
|
||||
.message-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.success .message-icon {
|
||||
color: #52c41a;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error .message-icon {
|
||||
color: #ff4d4f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.loading .message-icon {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #1890ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.message-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.message-close:hover {
|
||||
color: #666;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.removing {
|
||||
animation: slideOut 0.3s ease-out forwards;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="message-container ${type}">
|
||||
<div class="message-icon">
|
||||
${getTypeIcon()}
|
||||
</div>
|
||||
<div class="message-content">${message}</div>
|
||||
${closable ? html`
|
||||
<button class="message-close" @click=${() => this.remove()}>×</button>
|
||||
` : ''}
|
||||
</div>
|
||||
`
|
||||
|
||||
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
|
||||
15
packages/kv-login/vite.config.ts
Normal file
15
packages/kv-login/vite.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -15,7 +15,6 @@ const useCreateLoginQRCode = (config: Config) => {
|
||||
margin: 1,
|
||||
width: 300,
|
||||
};
|
||||
const [state, setState] = useState('');
|
||||
let timer = useRef<any>(null);
|
||||
const loginUrl = config?.wxmpLogin?.loginUrl || '';
|
||||
if (!loginUrl) {
|
||||
|
||||
100
pnpm-lock.yaml
generated
100
pnpm-lock.yaml
generated
@@ -115,6 +115,18 @@ importers:
|
||||
specifier: ^7.2.2
|
||||
version: 7.2.2(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)
|
||||
|
||||
packages/kv-login:
|
||||
dependencies:
|
||||
'@kevisual/query-login':
|
||||
specifier: ^0.0.6
|
||||
version: 0.0.6(@kevisual/query@0.0.29(ws@8.18.0)(zod@3.25.76))(rollup@4.52.5)(tslib@2.8.1)(typescript@5.8.3)
|
||||
lit-html:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1
|
||||
qrcode:
|
||||
specifier: ^1.5.4
|
||||
version: 1.5.4
|
||||
|
||||
packages/user-login:
|
||||
dependencies:
|
||||
'@floating-ui/dom':
|
||||
@@ -931,15 +943,6 @@ packages:
|
||||
tslib:
|
||||
optional: true
|
||||
|
||||
'@rollup/pluginutils@5.1.4':
|
||||
resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
|
||||
'@rollup/pluginutils@5.3.0':
|
||||
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -983,67 +986,56 @@ packages:
|
||||
resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.52.5':
|
||||
resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.52.5':
|
||||
resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.52.5':
|
||||
resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.52.5':
|
||||
resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.52.5':
|
||||
resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==}
|
||||
@@ -1224,9 +1216,6 @@ packages:
|
||||
'@types/estree-jsx@1.0.5':
|
||||
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
|
||||
|
||||
'@types/estree@1.0.7':
|
||||
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
@@ -1279,6 +1268,9 @@ packages:
|
||||
'@types/sax@1.2.7':
|
||||
resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||
|
||||
'@types/unist@2.0.11':
|
||||
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||
|
||||
@@ -1691,14 +1683,6 @@ packages:
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
fdir@6.4.4:
|
||||
resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==}
|
||||
peerDependencies:
|
||||
picomatch: ^3 || ^4
|
||||
peerDependenciesMeta:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -1993,6 +1977,9 @@ packages:
|
||||
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
lit-html@3.3.1:
|
||||
resolution: {integrity: sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==}
|
||||
|
||||
locate-path@5.0.0:
|
||||
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2019,9 +2006,6 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
magic-string@0.30.17:
|
||||
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
@@ -2343,10 +2327,6 @@ packages:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
picomatch@4.0.2:
|
||||
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
picomatch@4.0.3:
|
||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -3146,7 +3126,7 @@ snapshots:
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
js-tokens: 4.0.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
@@ -3697,19 +3677,19 @@ snapshots:
|
||||
|
||||
'@rollup/plugin-commonjs@28.0.3(rollup@4.52.5)':
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.1.4(rollup@4.52.5)
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
|
||||
commondir: 1.0.1
|
||||
estree-walker: 2.0.2
|
||||
fdir: 6.4.4(picomatch@4.0.2)
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
is-reference: 1.2.1
|
||||
magic-string: 0.30.17
|
||||
picomatch: 4.0.2
|
||||
magic-string: 0.30.21
|
||||
picomatch: 4.0.3
|
||||
optionalDependencies:
|
||||
rollup: 4.52.5
|
||||
|
||||
'@rollup/plugin-node-resolve@16.0.1(rollup@4.52.5)':
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.1.4(rollup@4.52.5)
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
|
||||
'@types/resolve': 1.20.2
|
||||
deepmerge: 4.3.1
|
||||
is-module: 1.0.0
|
||||
@@ -3719,21 +3699,13 @@ snapshots:
|
||||
|
||||
'@rollup/plugin-typescript@12.1.2(rollup@4.52.5)(tslib@2.8.1)(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.1.4(rollup@4.52.5)
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
|
||||
resolve: 1.22.10
|
||||
typescript: 5.8.3
|
||||
optionalDependencies:
|
||||
rollup: 4.52.5
|
||||
tslib: 2.8.1
|
||||
|
||||
'@rollup/pluginutils@5.1.4(rollup@4.52.5)':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.7
|
||||
estree-walker: 2.0.2
|
||||
picomatch: 4.0.2
|
||||
optionalDependencies:
|
||||
rollup: 4.52.5
|
||||
|
||||
'@rollup/pluginutils@5.3.0(rollup@4.52.5)':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
@@ -3973,8 +3945,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
|
||||
'@types/estree@1.0.7': {}
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/fontkit@2.0.8':
|
||||
@@ -4029,6 +3999,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 22.10.3
|
||||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
'@types/unist@2.0.11': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
@@ -4525,10 +4497,6 @@ snapshots:
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fdir@6.4.4(picomatch@4.0.2):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.2
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.3):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
@@ -4795,7 +4763,7 @@ snapshots:
|
||||
|
||||
is-reference@1.2.1:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.7
|
||||
'@types/estree': 1.0.8
|
||||
|
||||
is-wsl@3.1.0:
|
||||
dependencies:
|
||||
@@ -4866,6 +4834,10 @@ snapshots:
|
||||
lightningcss-win32-arm64-msvc: 1.30.2
|
||||
lightningcss-win32-x64-msvc: 1.30.2
|
||||
|
||||
lit-html@3.3.1:
|
||||
dependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
locate-path@5.0.0:
|
||||
dependencies:
|
||||
p-locate: 4.1.0
|
||||
@@ -4888,10 +4860,6 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
|
||||
magic-string@0.30.17:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@@ -5466,8 +5434,6 @@ snapshots:
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
picomatch@4.0.2: {}
|
||||
|
||||
picomatch@4.0.3: {}
|
||||
|
||||
pngjs@5.0.0: {}
|
||||
@@ -5687,7 +5653,7 @@ snapshots:
|
||||
|
||||
rollup-plugin-dts@6.2.1(rollup@4.52.5)(typescript@5.8.3):
|
||||
dependencies:
|
||||
magic-string: 0.30.17
|
||||
magic-string: 0.30.21
|
||||
rollup: 4.52.5
|
||||
typescript: 5.8.3
|
||||
optionalDependencies:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"extends": "@kevisual/types/json/frontend.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
|
||||
Reference in New Issue
Block a user