update add login modules

This commit is contained in:
2025-11-28 20:05:31 +08:00
parent b310413bfc
commit 2ae2b3ab4c
15 changed files with 1365 additions and 68 deletions

View 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);

View 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,
})

View 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 };
};

View 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)
})
}

View 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',
});
}
});
};

View 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);
};
}